f8a40da56c
- parseLastExchange привязывает результат инструмента к действию по tool_use_id, склеивает text-блоки, усекает до 1200 симв.; [ВЫДАЧА] в Слое 1 теперь наполняется - resolveCaseActivation: похожее имя дела (опечатка/подстрока) -> переспросить, не заводя дело-двойник; хук secretary-prompt-hook выводит подсказку с кандидатами - TDD: тесты secretary-transcript/flag/prompt-hook; полный свод зелёный Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
3.0 KiB
JavaScript
57 lines
3.0 KiB
JavaScript
// Детект команды секретаря в тексте промпта. Кавычки/код снимаются до сопоставления,
|
|
// чтобы цитирование не срабатывало (приём как в существующих детекторах).
|
|
function stripQuoted(text) {
|
|
return String(text || '')
|
|
.replace(/«[^»]*»/g, ' ')
|
|
.replace(/"[^"]*"/g, ' ')
|
|
.replace(/`[^`]*`/g, ' ');
|
|
}
|
|
|
|
export function detectSecretaryCommand(promptText) {
|
|
const t = stripQuoted(promptText).toLowerCase();
|
|
if (/выключи\s+секретар/.test(t)) return 'off';
|
|
if (/включи\s+секретар/.test(t)) return 'on';
|
|
return null;
|
|
}
|
|
|
|
// Имя файла-флажка ПО СЕССИИ: своя записка у каждого окна, параллельные сессии не топчут
|
|
// друг друга (общий флажок раньше перетирался последним «включи»).
|
|
export function secretaryModeFileName(session) {
|
|
return `secretary-mode-${session || 'unknown'}.json`;
|
|
}
|
|
|
|
// Расстояние Левенштейна (для ловли опечатки в имени дела).
|
|
function levenshtein(a, b) {
|
|
const m = a.length, n = b.length;
|
|
const d = Array.from({ length: m + 1 }, (_, i) => { const row = new Array(n + 1).fill(0); row[0] = i; return row; });
|
|
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
for (let i = 1; i <= m; i++) {
|
|
for (let j = 1; j <= n; j++) {
|
|
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
}
|
|
}
|
|
return d[m][n];
|
|
}
|
|
|
|
// «Похоже» = опечатка (правка ≤2 при длине обоих ≥4) ИЛИ сокращение (подстрока, длина короткого ≥3).
|
|
function isSimilar(a, b) {
|
|
if (a === b) return false;
|
|
const short = Math.min(a.length, b.length);
|
|
if (short >= 3 && (a.includes(b) || b.includes(a))) return true;
|
|
if (a.length >= 4 && b.length >= 4 && levenshtein(a, b) <= 2) return true;
|
|
return false;
|
|
}
|
|
|
|
// Сверка введённого имени дела со списком существующих (папки docs/secretary/<дело>).
|
|
// Точное совпадение → активировать существующее; похоже, но не точно → переспросить;
|
|
// не похоже / список пуст → активировать как новое (имя как ввёл).
|
|
export function resolveCaseActivation(requested, existing = []) {
|
|
const req = String(requested || '').trim().toLowerCase();
|
|
const list = (existing || []).map((e) => String(e || '').trim()).filter(Boolean);
|
|
const exact = list.find((e) => e.toLowerCase() === req);
|
|
if (exact) return { action: 'activate', work: exact };
|
|
const candidates = list.filter((e) => isSimilar(e.toLowerCase(), req));
|
|
if (candidates.length > 0) return { action: 'confirm', candidates };
|
|
return { action: 'activate', work: String(requested || '').trim() };
|
|
}
|