Set-focus must not append a second narration, and a server-owned stem must not also appear as a question in the assistant body. Co-authored-by: Cursor <cursoragent@cursor.com>
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
/**
|
||
* Server-owned collect stems join the same assistant turn. Comparison is
|
||
* exact identity of the prompt string being attached, never a semantic
|
||
* “did this body already ask”.
|
||
*/
|
||
|
||
const SENTENCE_SPLIT = /(?<=[。!??\n])/;
|
||
const NARRATIVE_SENTENCE = /范围|记下|对照|\d{1,2}:\d{2}/;
|
||
|
||
function isQuestionSentence(text: string, stem: string): boolean {
|
||
if (!text) return false;
|
||
if (stem && text === stem) return true;
|
||
const prefix = stem.slice(0, 12);
|
||
if (prefix && text.startsWith(prefix)) return true;
|
||
return /[??]$/.test(text);
|
||
}
|
||
|
||
export function stripQuestionSentences(body: string, stem: string): string {
|
||
const prompt = stem.trim();
|
||
const spoken = body.trim();
|
||
if (!spoken) return "";
|
||
const kept: string[] = [];
|
||
let dropContinuation = false;
|
||
for (const part of spoken.split(SENTENCE_SPLIT)) {
|
||
const text = part.trim();
|
||
if (!text) continue;
|
||
if (isQuestionSentence(text, prompt)) {
|
||
dropContinuation = true;
|
||
continue;
|
||
}
|
||
if (dropContinuation && !NARRATIVE_SENTENCE.test(text)) {
|
||
dropContinuation = false;
|
||
continue;
|
||
}
|
||
dropContinuation = false;
|
||
kept.push(part);
|
||
}
|
||
return kept.join("").trim();
|
||
}
|
||
|
||
export function composeCollectSpokenAssistantText(body: string, prompt: string): string {
|
||
const stem = prompt.trim();
|
||
const spoken = body.trim();
|
||
if (!stem) return spoken;
|
||
if (!spoken || spoken === stem) return stem;
|
||
const stripped = stripQuestionSentences(spoken, stem);
|
||
if (!stripped) return stem;
|
||
const suffix = `\n\n${stem}`;
|
||
if (stripped.includes(stem)) return stripped;
|
||
if (stripped.length >= suffix.length && stripped.slice(stripped.length - suffix.length) === suffix) {
|
||
return stripped;
|
||
}
|
||
return `${stripped}${suffix}`;
|
||
}
|
||
|
||
export function detachCollectSpokenAssistantText(body: string, prompt: string): string {
|
||
const stem = prompt.trim();
|
||
const spoken = body.trim();
|
||
if (!stem || !spoken || spoken === stem) return spoken;
|
||
const suffix = `\n\n${stem}`;
|
||
if (spoken.length >= suffix.length && spoken.slice(spoken.length - suffix.length) === suffix) {
|
||
return spoken.slice(0, spoken.length - suffix.length).trim();
|
||
}
|
||
return spoken;
|
||
}
|