[refactor][ai-chat] Дублирование определений инструментов (in-app агент vs standalone MCP) — единый реестр спеков #294
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Дублирование определений инструментов: in-app агент vs standalone MCP-пакет
Статус: частично закрыто. Квирк «node как объект ИЛИ JSON-строка» вынесен в общий хелпер
parseNodeArg(см. «Прогресс»); остальной долг (единый реестр спеков + унификация конвертера) всё ещё открыт. Это forward-looking стоимость поддержки, НЕ баг — код корректен сегодня. Держим запись открытой, чтобы при росте набора инструментов долг не разъезжался молча.Прогресс
refactor/ai-chat-tool-spec-registry, PR #114). Шесть рукописных копий нормализации «node как объект ИЛИ JSON-строка» свёрнуты вparseNodeArg: по одному источнику на пакет —packages/mcp/src/lib/parse-node-arg.ts(standalone) иapps/server/src/core/ai-chat/tools/parse-node-arg.ts(in-app). Две копии намеренны (ESM/CJS-граница), поведение тождественно.DocmostClientLikeиз реального типа — отложены (см. «Фикс»): требуют пересечения ESM/CJS-границы для данных+zod и ломают тест-стабы in-app инструментов при точных типах. Делать инкрементально.Суть
Один и тот же набор инструментов поверх одного
DocmostClientописан тремя независимыми рукописными слоями. Каждое добавление инструмента или правка его model-facing описания требует синхронной правки в 2–3 местах, а parity-баги (расхождение копий) приходится чинить/переоткрывать дважды.Где дублируется (три слоя)
packages/mcp/src/index.ts(~38registerTool). Для внешних MCP-клиентов (stdio/http). На каждый инструмент: zod-схема + длинное model-facing описание + тонкийexecute, вызывающийDocmostClient.apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts(~39tool({...})черезai-SDK). Своя zod-схема + своё описание + свойexecuteповерх ТОГО ЖЕ клиента (@docmost/mcpгрузится вtools/docmost-client.loader.ts:188через динамическийimport()).DocmostClientLikeвapps/server/src/core/ai-chat/tools/docmost-client.loader.ts:9(в комментарии прямо: «Signatures here mirror that file exactly»), скопирован руками изpackages/mcp/src/client.ts.Что именно продублировано (с подтверждением по коду)
Квирк «node как объект ИЛИ JSON-строка» реализован дважды (НЕ в общем клиенте)— закрыто (PR #114): вынесен вparseNodeArg(по хелперу на пакет), 6 inline-копий устранены:patchNode,insertNode,updatePageJson→apps/server/src/core/ai-chat/tools/parse-node-arg.ts;patch_node,insert_node,update_page_json→packages/mcp/src/lib/parse-node-arg.ts.transformPage(dryRun) описана в обоих:ai-chat-tools.service.ts:~935иindex.ts:~1006.Почему разделение слоёв 1 и 2 само по себе оправдано
У путей разный транспорт и auth-контекст, и это правильно держать раздельно: in-app путь чеканит per-user JWT + provenance collab-токен (подписанная agent-claim,
docmost-client.loader.ts:159—getCollabToken), а standalone обслуживает внешних клиентов по stdio/http. Но это оправдывает два тонких адаптера (execute+ auth-обвязка), а НЕ две рукописные копии МЕТАДАННЫХ (схема + описание + квирки). Метаданные можно объявить один раз и переиспользовать обоими транспортами.Доказательство стоимости (наблюдалось при фиксе edit_page_text)
При исправлении ложного «успеха»
edit_page_text(refuse форматных правок +verify-отчёт):DocmostClient→ автоматически дошло до обоих агентов ОДНОЙ правкой. Это «хороший» случай — логика в едином источнике.index.ts(кодером) и отдельно вai-chat-tools.service.ts:617, где описание продолжало рекламировать «Markdown wrappers tolerated via strip-and-retry» — ровно ту формулировку, что ввела исходного агента в заблуждение. Копия молча разъехалась и какое-то время встроенный агент получал устаревшую подсказку. Это и есть материализованный parity-баг.Фикс
Единый реестр спеков (полное устранение дублирования). Вынести в
packages/mcpодин источник на инструмент:name+ zod-схема + model-facing описание + общий хелпер нормализации node-строки (для patch/insert/update). Иindex.ts, иai-chat-tools.service.tsимпортируют спеки и добавляют только свойexecute/auth.DocmostClientLike— выводить из типа реального клиента (type-only import / генерация), а не копировать руками.@docmost/mcp— ESM-only, сервер грузит его через трюкnew Function('import(specifier)')(docmost-client.loader.ts:174), потому чтоmodule:commonjsдаунлевелитimport()вrequire(). Реестр спеков (данные + zod) должен пересекать ту же ESM/CJS-границу — выполнимо тем же динамическим импортом;ai-SDKtool()и MCPregisterTool()имеют разную форму, поэтому реестр экспортирует транспорт-агностичные{name, schema, description}, а каждая сторона оборачивает их сама.zod— общая зависимость обоих пакетов, типы переносятся.Связанные
parseNodeArg).