Инструмент search_in_page: внутристраничный поиск подстроки/regex для агента #330
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?
Проблема
Агент (в первую очередь редакторские роли — Корректор, Фактчекер) ищет вхождения по странице вслепую: гоняет
get_nodeпо блокам подряд (14, 173, 187, 188…), сжигая тысячи токенов на перебор и рассуждения, чтобы найти, например, все слова без «ё», прямые кавычки вместо «ёлочек», «т.е.». Подходящего инструмента нет:search— глобальный full-text по воркспейсу, не по одной странице, и не возвращает позиции внутри документа;get_outline— только top-level блоки, без поиска по содержимому;get_node— один блок по id.Нужен инструмент «найди подстроку/паттерн внутри конкретной страницы и верни, где именно».
Ключевое архитектурное решение: клиентский поиск по ProseMirror JSON
Инструмент читает документ через существующий путь чтения (
getPageRaw) и ищет в памяти по дереву ProseMirror. Следствия:packages/mcp/src/lib/.SHARED_TOOL_SPECS(packages/mcp/src/tool-specs.ts) → автоматически появляется в обоих транспортах: внешний/mcpи встроенный AI-chat агент (роли работают именно во встроенном).<span data-comment-id=…>— агент не спотыкается о якоря комментариев (в т.ч. resolved), которые в markdown-выводеget_pageзасоряют текст;сильно меньше 8кб, где часть под comment-mark, находится как цельная строка.Сигнатура
pageIdqueryregexfalsequeryкак регуляркуcaseSensitivefalselimit50(max ~200)Движок — литерал + regex (согласовано): по умолчанию подстрока,
regex:trueвключает регулярку. Для корректора/фактчекера regex даёт классы символов и границы слов (слова без «ё», прямые кавычки["'],\bт\.е\.).Формат ответа
nodeId— тот же формат, что принимаютget_node/patch_node/анкоринг комментария, поэтому агент из результата сразу идёт ставить точечный комментарий сsuggestedText.before/afterдают достаточно контекста, чтобы выбрать уникальное выделение (требование фичи suggestions — selection должен быть уникален).Семантика обхода и nodeId
blockPlainTextизpackages/mcp/src/lib/node-ops.ts) и ищем в ней — совпадение переживает границы марок.nodeId=attrs.idконтейнера; если его нет (ячейки/строки таблиц) —#<topLevelIndex>ближайшего top-level блока (семантикаgetNodeByRef,node-ops.ts).Edge cases
query→ отказ с пояснением.totalvslimit: всегда отдаёмtotalиtruncated— «сотня вхождений» не должна молча обрезаться.Карта реализации (файлы)
packages/mcp/src/lib/page-search.ts(new)searchInDoc(doc, query, opts)+ типы — тестируется изолированно, какcomment-anchor.tspackages/mcp/src/client.tssearchInPage(pageId, …)— читаетgetPageRaw, зовётsearchInDocpackages/mcp/src/tool-specs.tssearchInPage(mcpName: search_in_page,inAppKey, description,buildShape)packages/mcp/src/index.tsregisterShared(...)+ строка вSERVER_INSTRUCTIONSapps/server/src/core/ai-chat/tools/ai-chat-tools.service.tssharedTool(...)для встроенного агентаapps/server/src/core/ai-chat/tools/docmost-client.loader.tssearchInPageвDocmostClientLikepackages/mcp/build/*tsc(артефакты в git)packages/mcp/test/unit/page-search.test.mjs(new)Промпт-интеграция
Без подсказки агент продолжит перебирать
get_node. В комплекте: строка вSERVER_INSTRUCTIONS(«ищешь вхождения по странице →search_in_page, а не перебор блоков») и одна фраза в промптах корректора и фактчекера («для однотипных проблем — ё, кавычки, единицы — сначалаsearch_in_page, затем точечный комментарий на каждое вхождение»), с бампом версий этих ролей.Тесты
literal и regex; case-sensitivity; совпадение через границу марок;
nodeIdдля paragraph/heading/ячейки таблицы;limit/total/truncated; невалидный regex → ошибка; пустой query.Вне скоупа v1
edit_page_text).search).