client.ts был god-object'ом на 5206 строк / ~65 методов / 5 ответственностей —
любая правка рисковала всем write-path, обработка ошибок неоднородна. Разнесли
на доменные модули; DocmostClient остаётся ТОНКИМ ФАСАДОМ с прежним внешним
контрактом. Чистый рефакторинг, поведение не меняется. closes#450
- Фасад client.ts (93 строки) композирует 10 миксинов над общим абстрактным
базовым классом. Паттерн — МИКСИНЫ (не context-object), т.к. тесты
субклассируют DocmostClient и переопределяют seam'ы (mutatePage/replacePage/
resolvePageId/getPageRaw/…); единая цепочка прототипов сохраняет виртуальную
диспетчеризацию this.<method> сквозь модули.
- client/context.ts (база, ≤700): общее состояние (axios, apiUrl, токены, кэши
resolvePageId/collab-token, sandbox/metrics-синки), конструктор + оба
интерсептора, login/ensureAuthenticated/paginateAll/resolvePageId/
mutateLiveContentUnlocked + write-seam'ы mutatePage/replacePage. private-поля
расширены до protected (внутреннее, не в публичном контракте) чтобы
соседние миксины видели общее состояние.
- Модули (каждый ≤730): read, pages, nodes-write, media (images/attachments/
drawio), comments, transforms (markdown + JS-sandbox transformPage), tables,
stash, doc-validate. errors.ts — единый REST error-mapping (formatDocmost
AxiosError, довершение #437; тексты сообщений без изменений).
- Внешний контракт СОХРАНЁН (доказано компиляцией с обеих сторон): 53 публичных
метода на DocmostClient через `implements IXMixin`-интерфейсы (без ручного
зеркала, #446); Pick<DocmostClient, DocmostClientMethod> in-app и Pick в
tool-specs резолвятся; множество async-методов 59==59 идентично. Нагруженные
seam'ы (replaceImage один-лок #425, self-resolve #449, единый error-путь) —
байт-идентичны. Ни одного дубля публичного метода; stub+impl всегда
затеняется реальной реализацией.
- zod v3→v4: investigate-only, РЕКОМЕНДАЦИЯ отложить — SDK 1.29 поддерживает v4
(peer ^3.25||^4.0), но мажор-бамп трогает всю схема-поверхность и оба хоста,
смешивать со структурным распилом рискованно. Отдельным follow-up.
Стоит на #449 (#475). Тесты: mcp node --test 800/800 (== база), tsc чисто в
client/ (2 pre-existing ws-типа в нетронутом lib/). Ни одной строки кода не
потеряно (нормализованный дифф), убраны 52 дублированных JSDoc-блока.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mutatePageContent wrote agent edits back by DELETING the whole Yjs fragment and
re-applying a fresh Y.Doc. Yjs is a CRDT — the editor anchors its selection to
node ids — so wiping every id made an open editor's cursor lose its anchor and
snap to the end of the document on every agent write. It was most visible on
comment anchoring (issue #152): a comment changes no text, yet the cursor jumped.
(Before commit 4201f0a3 the anchoring silently no-op'd, so the destructive write
never ran for comments — hence the regression.)
Fix: write via `updateYFragment` (y-prosemirror) — the same routine the editor
uses to sync its own edits into Yjs. It structurally diffs the new doc against
the live fragment and touches only changed nodes, preserving the ids of unchanged
ones, so the cursor stays put. This improves ALL agent write tools (text edits,
node ops, comments, replace) — minimal diff instead of full replace: less collab
noise, stable block-ids, other users' cursors no longer disrupted.
- collaboration.ts: new `applyDocToFragment` (sanitize -> PMNode.fromJSON against
a memoized docmost schema -> updateYFragment in one transact), keeping the
`findUnstorableAttr` encode diagnostic; swap the destructive write-back for it.
- package.json: `y-prosemirror` promoted to a direct dependency (was transitive).
- test: comment-cursor-stability.test.mjs — a Yjs RelativePosition (the cursor
anchor) survives both a sibling edit and a comment-mark anchoring (the old
full-replace tombstoned it -> null). 292 package tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- openai provider: use .chat() (Chat Completions) instead of the default callable
(Responses API), which gateways reject on multi-turn -> 400.
- updateAiProviderSettings: assemble settings.ai.provider via jsonb_build_object
with ::text-cast bound params + jsonb_typeof self-heal (postgres.js was
double-encoding it into an array; the ::text cast avoids 'could not determine
data type of parameter').
- chat agent: drop the hard maxOutputTokens cap (truncated complex tool calls);
keep a tiny cap only on the test-connection ping.
- testConnection + chat stream: surface the real provider error (statusCode+message)
to logs and the UI instead of generic masks; never log the API key.
- chat UI: typing indicator, incremental streaming render, tool 'running' status, Stop.
Also bundled (prior uncommitted ai-chat work):
- history 'AI agent' provenance badge; vector RAG (pgvector image + page_embeddings
+ AI_QUEUE indexer + space-scoped semanticSearch); external MCP servers backend
(@ai-sdk/mcp client, SSRF IP-pinning, encrypted headers, admin CRUD/Test);
yjs duplicate-instance fix via pnpm patch (single CJS instance server-side).
Replace the removed enterprise EE MCP (private apps/server/src/ee submodule,
license-gated /mcp route) with our docmost-mcp, vendored as an isolated ESM
workspace package and served by the server over HTTP — no enterprise license.
Backend:
- Add packages/mcp (@docmost/mcp): vendored docmost-mcp refactored into a
side-effect-free createDocmostMcpServer() factory (38 tools preserved),
stdio entry kept in stdio.ts, Streamable-HTTP session manager in http.ts.
- Add apps/server McpModule: @Post/@Get/@Delete('mcp') (served at /mcp via the
existing global-prefix exclude), @SkipTransform + reply.hijack to bridge raw
Fastify req/res into the SDK transport. The module dynamically imports the
ESM-only package from CommonJS via a Function-indirected import resolved with
require.resolve + file:// URL. Gated by the workspace ai.mcp toggle, a
service-account (MCP_DOCMOST_EMAIL/PASSWORD/API_URL) and optional MCP_TOKEN;
per-session idle eviction (MCP_SESSION_IDLE_MS).
- Drop the enterprise license check on mcpEnabled in workspace.service.
- Dockerfile: copy packages/mcp into the production image.
- .env.example: document MCP_DOCMOST_*, MCP_TOKEN, MCP_SESSION_IDLE_MS.
Frontend:
- Recreate the community "AI & MCP" workspace-settings panel (mcp-settings.tsx):
admin-only toggle on settings.ai.mcp with optimistic update, copyable
${APP_URL}/mcp URL; wired into workspace-settings page. Reuses existing i18n.
Fixes:
- Pin packages/mcp tiptap deps to 3.20.4 (matching the client) and inline
getStyleProperty, preventing a duplicate @tiptap/core@3.26.1 from leaking into
the client editor via pnpm shamefully-hoist (was breaking apps/client tsc).