增强文档改写配图与深度研究流式稳定性,并完善 8BF/会商工作区。
全文改写支持可选配图与思考过程展示,深度研究接入 DashScope 原生出图并剥离内联 think 标签;8BF 会商补齐第一步工作区与任务卡回退,同时限制热重载扫描范围。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
819e537aa3
commit
15a46b903a
48 files changed
+3724
-616
No files matched your search
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in new issue
Block a user