From 12d7a5de5a3c0724c3e2df9ebd5cadc5e05fee16 Mon Sep 17 00:00:00 2001 From: agent_coder Date: Fri, 10 Jul 2026 05:28:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(ai-chat):=20=D0=BF=D1=80=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D0=B8=D1=82=D1=8C=20drawio=5Fshapes/drawio=5Fguide?= =?UTF-8?q?=20in-app=20=E2=80=94=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20SHARED=5FTOOL=5FSPECS-?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=B8=D1=82=D0=B5=D1=82=20(#424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Стадия-1 (#434) уже была довяжена in-app в develop (f46d89ea, agent_vscode) для CRUD-тулов; два новых чистых read-only хелпера стадии-2 остались незаброшенными → contract-parity спека падала 6 ассертами (по 3 на drawio_shapes/drawio_guide). В отличие от CRUD-тулов это ЧИСТЫЕ функции без сетевого вызова, поэтому НЕ client-методы: - реэкспорт searchShapes / getGuideSection (+ тип SearchShapesOptions) из entry пакета @docmost/mcp; loadDocmostMcp() пробрасывает их так же, как sharedToolSpecs (типы SearchShapesFn/GetGuideSectionFn); - две записи sharedTool(...) в forUser() после drawioUpdate: drawioShapes повторяет серверный вызов searchShapes(query,{category,limit}) и форму { query, count, results }; drawioGuide — getGuideSection(section) (omit section -> index); голый объект без jsonContent-envelope, как у соседних in-app хендлеров; - DocmostClientLike и HOST_CONTRACT_METHODS-вайтлист НЕ тронуты (это не методы клиента); - три тест-мока (contract/service/tool-tiers) получили type-only no-op заглушки под расширенный тип loadDocmostMcp() — тела инструментов в этих тестах не исполняются, contract-спека реально гоняет настоящий SHARED_TOOL_SPECS. Внутреннее ревью обвязки: APPROVE, 0 находок. shared-tool-specs.contract: 211/211 (было 6 падений); client-host-contract drift-guard 3/0; tsc EXIT 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../tools/ai-chat-tools.service.spec.ts | 8 ++++++ .../ai-chat/tools/ai-chat-tools.service.ts | 26 +++++++++++++++++-- .../ai-chat/tools/docmost-client.loader.ts | 20 ++++++++++++++ .../tools/shared-tool-specs.contract.spec.ts | 10 +++++++ .../src/core/ai-chat/tools/tool-tiers.spec.ts | 7 +++++ packages/mcp/src/index.ts | 7 +++++ 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts index 57f90b7d..3cba8c6d 100644 --- a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts +++ b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts @@ -13,6 +13,14 @@ import { SHARED_TOOL_SPECS } from '../../../../../../packages/mcp/src/tool-specs const mockLoaded = (DocmostClient: loader.DocmostClientCtor) => ({ DocmostClient, sharedToolSpecs: SHARED_TOOL_SPECS as Record, + // Pure no-network draw.io helpers (#424). Type-correct stubs: these tests + // never execute the drawio_shapes / drawio_guide tool bodies. + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); /** diff --git a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts index c7609a77..12cf9df7 100644 --- a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts +++ b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts @@ -169,8 +169,12 @@ export class AiChatToolsService { // provenance tokens) and load the shared tool-spec registry. Client // construction is shared with the page-change detection path (#274) via // buildDocmostClient so both go over the exact same authenticated route. - const { sharedToolSpecs, createCommentSignalTracker } = - await loadDocmostMcp(); + const { + sharedToolSpecs, + createCommentSignalTracker, + searchShapes, + getGuideSection, + } = await loadDocmostMcp(); const client = await this.buildDocmostClient( user, sessionId, @@ -760,6 +764,24 @@ export class AiChatToolsService { await client.drawioUpdate(pageId, node, xml, baseHash), ), + // Schema + description live in @docmost/mcp's SHARED_TOOL_SPECS (#424). + // Pure no-network helper — calls searchShapes directly, no client method. + // Result shape mirrors the MCP server: { query, count, results }. + drawioShapes: sharedTool( + sharedToolSpecs.drawioShapes, + async ({ query, category, limit }) => { + const results = searchShapes(query, { category, limit }); + return { query, count: results.length, results }; + }, + ), + + // Schema + description live in @docmost/mcp's SHARED_TOOL_SPECS (#424). + // Pure no-network helper — calls getGuideSection directly, no client method. + drawioGuide: sharedTool( + sharedToolSpecs.drawioGuide, + async ({ section }) => getGuideSection(section), + ), + // Schema + description now live in @docmost/mcp's SHARED_TOOL_SPECS (#294). // The table reference parameter was unified to `table` (was `tableRef`). tableInsertRow: sharedTool( diff --git a/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts b/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts index c5f1cc8b..e1165996 100644 --- a/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts +++ b/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts @@ -337,6 +337,19 @@ export type CommentSignalTrackerFactory = (options: { debounceMs?: number; }) => CommentSignalTrackerLike; +// Pure, no-network draw.io helpers (#424). These are plain functions on the +// module (NOT DocmostClient methods) — the in-app AI-SDK service calls them +// directly to wire drawio_shapes / drawio_guide, mirroring the MCP server. +export type SearchShapesFn = ( + query: string, + opts?: { category?: string; limit?: number }, +) => Array>; +export type GetGuideSectionFn = (section?: string) => { + section: string; + content: string; + sections: string[]; +}; + interface DocmostMcpModule { DocmostClient: DocmostClientCtor; SHARED_TOOL_SPECS: Record; @@ -344,6 +357,8 @@ interface DocmostMcpModule { // loader in unit tests. The in-app layer treats an absent factory as "signal // disabled" — a pure no-op that leaves tool results byte-identical. createCommentSignalTracker?: CommentSignalTrackerFactory; + searchShapes: SearchShapesFn; + getGuideSection: GetGuideSectionFn; } // TS with module:commonjs downlevels a literal `import()` to `require()`, which @@ -368,6 +383,8 @@ export async function loadDocmostMcp(): Promise<{ DocmostClient: DocmostClientCtor; sharedToolSpecs: Record; createCommentSignalTracker?: CommentSignalTrackerFactory; + searchShapes: SearchShapesFn; + getGuideSection: GetGuideSectionFn; }> { if (!modulePromise) { modulePromise = (async () => { @@ -396,5 +413,8 @@ export async function loadDocmostMcp(): Promise<{ // Optional: forwarded when present so the in-app layer can build the passive // comment signal (#417); undefined on a stale build => signal disabled. createCommentSignalTracker: mod.createCommentSignalTracker, + // Pure no-network draw.io helpers (#424); not client methods. + searchShapes: mod.searchShapes, + getGuideSection: mod.getGuideSection, }; } diff --git a/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts b/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts index f62f0cc7..3d708db0 100644 --- a/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts +++ b/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts @@ -45,6 +45,16 @@ describe('SHARED_TOOL_SPECS contract parity', () => { string, loader.SharedToolSpec >, + // Pure no-network draw.io helpers (#424). The contract test never executes + // a tool body, so type-correct stubs suffice (the real functions can't be + // imported here — drawio-shapes.ts uses import.meta, incompatible with the + // CommonJS jest transform). + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); const service = new AiChatToolsService( tokenServiceStub as never, diff --git a/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts b/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts index 301be48f..24d0485d 100644 --- a/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts +++ b/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts @@ -124,6 +124,13 @@ describe('deferred catalog ↔ live forUser() toolset partition (#332, F3)', () return {} as DocmostClientLike; } as unknown as loader.DocmostClientCtor, sharedToolSpecs: SHARED_TOOL_SPECS as Record, + // Pure no-network draw.io helpers (#424); tool bodies are never executed here. + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); const service = new AiChatToolsService( { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 461d3465..81cc4086 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -48,6 +48,13 @@ export type { CommentSignalProbeResult, CommentSignalTrackerOptions, } from "./comment-signal.js"; +// Re-export the pure, no-network draw.io helpers (#424) so the in-app AI-SDK +// service can wire drawio_shapes / drawio_guide off the loaded module. These are +// NOT client methods (no page/backend hit) — the in-app handler calls them +// directly, mirroring how the standalone MCP server wires them here. +export { searchShapes } from "./lib/drawio-shapes.js"; +export type { SearchShapesOptions } from "./lib/drawio-shapes.js"; +export { getGuideSection } from "./lib/drawio-guide.js"; // Read version from package.json const __filename = fileURLToPath(import.meta.url);