Files
deerflow-code/frontend-web/src/core/streamdown/inline-think.ts
T
董宏凯andCursor 15a46b903a 增强文档改写配图与深度研究流式稳定性,并完善 8BF/会商工作区。
全文改写支持可选配图与思考过程展示,深度研究接入 DashScope 原生出图并剥离内联 think 标签;8BF 会商补齐第一步工作区与任务卡回退,同时限制热重载扫描范围。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 17:46:11 +08:00

91 lines
2.9 KiB
TypeScript

/**
* Some OpenAI-compatible reasoning models write ``<think>...</think>`` into
* the ordinary text stream rather than a dedicated reasoning field. Markdown
* sandboxes must never render those fragments as document content. Callers
* can separately render the returned thinking part in a labelled UI trace.
*
* The filter is stateful because either tag can be split across arbitrary SSE
* deltas. Hold only a possible tag suffix, so confirmed Markdown continues
* to appear with the normal token-streaming cadence.
*/
export class InlineThinkTagFilter {
private static readonly OPEN_TAG = "<think>";
private static readonly CLOSE_TAG = "</think>";
private insideThink = false;
private pending = "";
private possibleTagPrefixSize(content: string, tag: string) {
const lowered = content.toLowerCase();
for (let size = Math.min(content.length, tag.length - 1); size > 0; size -= 1) {
if (lowered.slice(-size) === tag.slice(0, size)) return size;
}
return 0;
}
pushParts(chunk: string): { text: string; thinking: string } {
if (!chunk) return { text: "", thinking: "" };
let remaining = this.pending + chunk;
this.pending = "";
let visible = "";
let thinking = "";
while (remaining) {
const tag = this.insideThink
? InlineThinkTagFilter.CLOSE_TAG
: InlineThinkTagFilter.OPEN_TAG;
const tagIndex = remaining.toLowerCase().indexOf(tag);
if (tagIndex >= 0) {
if (this.insideThink) thinking += remaining.slice(0, tagIndex);
else visible += remaining.slice(0, tagIndex);
remaining = remaining.slice(tagIndex + tag.length);
this.insideThink = !this.insideThink;
continue;
}
const suffixSize = this.possibleTagPrefixSize(remaining, tag);
const confirmed = suffixSize ? remaining.slice(0, -suffixSize) : remaining;
if (this.insideThink) thinking += confirmed;
else visible += confirmed;
this.pending = suffixSize ? remaining.slice(-suffixSize) : "";
break;
}
return { text: visible, thinking };
}
push(chunk: string) {
return this.pushParts(chunk).text;
}
/** Flush a completed stream into its visible/thinking channels. */
finishParts(): { text: string; thinking: string } {
if (this.insideThink) {
const thinking = this.pending;
this.insideThink = false;
this.pending = "";
return { text: "", thinking };
}
const tail = this.pending;
this.pending = "";
return { text: tail, thinking: "" };
}
finish() {
return this.finishParts().text;
}
/** Replace a rendered checkpoint while retaining the correct tag state. */
replace(content: string) {
this.insideThink = false;
this.pending = "";
return this.push(content);
}
}
export function stripInlineThinkTags(content: string) {
const filter = new InlineThinkTagFilter();
return filter.push(content) + filter.finish();
}