Предложения исправлений в комментариях агентов: suggestedText + кнопка «Применить» #315
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?
Суть
Агент (MCP
create_commentили AI-чатcreateComment) при создании inline-комментария может приложить предлагаемую замену выделенного текста. Человек в панели комментариев видит блок «было → стало» и кнопку «Применить». Нажатие заменяет заякоренный фрагмент страницы на предложенный текст — только если фрагмент никто не изменил с момента создания предложения. Human-in-the-loop by design: агент предлагает, человек применяет.Формат предложения
Не unified diff и не патч — пара строк с replace-семантикой, тот же паттерн, что в
edit_page_text:selection(существующее поле, ≤250) — одновременно якорь и «старая» сторона.suggestedText(новое, plain text, ≤2000) — полная замена фрагмента. Обязан отличаться отselection, пустая строка запрещена.Ключевая механика: привязка к выделению, не к тексту
Применение не ищет текст в документе — замена идёт по марке
commentс даннымcommentIdв Yjs-документе:selection, позиция не важна.Неоднозначность существует только в момент создания марки агентом (у агента нет курсора, якорь задаётся текстом;
applyAnchorInDocсейчас молча берёт первое вхождение —packages/mcp/src/lib/comment-anchor.ts). Для обычного комментария цена ошибки — марка не там; сsuggestedTextклик «Применить» отредактировал бы не то место. Поэтому новое правило: при наличииsuggestedTextвхождениеselectionобязано быть уникальным в документе. ≥2 вхождений → отказ с подсказкой агенту расширить selection контекстом (бюджет 250 символов). Проверка и в пре-чеке (canAnchorInDocпоgetPageJson), и — авторитетно — в живом anchor-шаге внутриmutatePageContent(abort + rollback комментария, тот же механизм, что при «selection not found»). Комментарии безsuggestedTextсохраняют текущее поведение (первое вхождение). Человеческие комментарии из UI не затронуты — там якорь позиционный (yjsSelection). Вариантoccurrence: Nотвергнут: LLM плохо считают вхождения.Почему применение — на сервере, а не в браузере
Клиентский вариант (Tiptap-транзакция) отвергнут: два одновременных «Применить» оба проходят локальную проверку, Yjs мёржит обе вставки → испорченный текст; проверка шла бы против возможно устаревшего локального дока. Серверный вариант: check-and-replace атомарно внутри
connection.transact()на инстансе-владельце документа. Инфраструктура готова: кастомные событияCollaborationHandler(паттернsetCommentMark/resolveCommentMark), аRedisSyncExtension.handleEventвозвращает результат хендлера кросс-процессно (customEventStart/customEventComplete+replyId) — API-сервер получает вердикт из collab-процесса.Дизайн по слоям
1. БД — миграция
2026….-comment-suggestions.tsТолько добавление колонок в
comments(+migration:codegenдляdb.d.ts; таймстемп позже последней применённой миграции на develop):suggested_text text NULLsuggestion_applied_at timestamptz NULLsuggestion_applied_by_id uuid NULL REFERENCES users(id)2. Сервер — создание
CreateCommentDto+@IsOptional() @IsString() @MaxLength(2000) suggestedText.CommentService.create(apps/server/src/core/comment/comment.service.ts):suggestedTextдопустим только для top-level inline-комментария сselection; обязан отличаться отselection(иначе применение неотличимо от «уже применено»).3. Сервер — применение
Эндпоинт
POST /comments/apply-suggestion({ commentId }):suggestedText, не применён, тред не резолвлен; права —pageAccessService.validateCanEdit(неvalidateCanComment: применение меняет текст страницы).applyCommentSuggestionвcollaboration.handler.ts+ хелпер вyjs.util.ts:CommentService.applySuggestionразбирает вердикт:applied: true→suggestionAppliedAt/ById, авторезолв треда через существующийresolveComment(comment, true, user)(марка получаетresolved=true, уведомления штатно), wscommentUpdated, auditCOMMENT_SUGGESTION_APPLIED;applied: falseиcurrentText === suggestedText→ идемпотентная ветка «уже применено» (лечит двойной клик, гонку двух пользователей и падение между мутацией дока и записью БД): дописать applied-поля, вернуть успех;409 Conflictс конкретным сообщением иcurrentTextв теле.Открытые редакторы получают замену и резолв марки через штатный Yjs-синк.
4. Клиент
IComment+suggestedText,suggestionAppliedAt,suggestionAppliedById(apps/client/src/features/comment/types/comment.types.ts).comment-list-item.tsx: под цитатой selection — дифф-блок (старый текст зачёркнут/красный фон → новый зелёным; опционально пословная LCS-подсветка). Кнопка «Применить» видима при: естьsuggestedText,!suggestionAppliedAt,!resolvedAt,page.permissions.canEdit(прокинуть изcomment-list-with-tabs.tsx, где он уже есть). После применения — бейдж «Применено».useApplySuggestionMutationвcomment-query.tsпо образцу resolve-мутации; на 409 — конкретное уведомление с актуальным текстом из ответа (конвенция форка об ошибках — никаких generic-сообщений).5. Агентские инструменты
create_comment(packages/mcp/src/index.ts,client.ts): опциональныйsuggestedText(zod, ≤2000, требуетselection, запрещён для reply) → payload/comments/create; описание тула объясняет семантику.comment-anchor.ts:countAnchorMatches+ строгая уникальность приsuggestedText.filterComment/list_commentsотдают новые поля.createComment(ai-chat-tools.service.ts) + сигнатура вDocmostClientLike(docmost-client.loader.ts) — зеркально; проходит через тот жеDocmostClient.createComment, правило уникальности наследуется.commentта же) — синхронизация зеркала схемы вpackages/mcp/src/lib/не нужна.Крайние случаи
transact()сериализует; второй → идемпотентная ветка «уже применено»currentText === suggestedTextCOLLAB_DISABLE_REDIShandleYjsEventмолча возвращаетundefined— для apply недопустимо: локальный fallback-вызов хендлера, при невозможности — жёсткая ошибка, не тихий no-opВне скоупа v1
План работ
db.d.ts.yjs.util.ts:replaceYjsMarkedText+ юнит-тесты вyjs.util.spec.ts.collaboration.handler.ts: событиеapplyCommentSuggestion; fallback вcollaboration.gateway.ts.CommentService.create(хранение) +applySuggestion, контроллер/comments/apply-suggestion, audit-событие, тесты сервиса.comment-list-item.test.tsx.suggestedText,countAnchorMatches+ строгая уникальность,filterComment, contract-тестclient-host-contract, тесты на неоднозначный якорь.Порядок: 1→2→3→4 последовательно, 5 и 6 параллельно после 4.