






















@@ -9,6 +9,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
99import type { InlineCodeState } from "../markdown/code-spans.js";
1010import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-spans.js";
1111import { normalizeOptionalString } from "../shared/string-coerce.js";
12+import { findFinalTagMatches } from "../shared/text/final-tags.js";
1213import { hasOrphanReasoningCloseBoundary } from "../shared/text/reasoning-tags.js";
1314import { mediaUrlsFromGeneratedAttachments } from "./generated-attachments.js";
1415import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
@@ -43,7 +44,6 @@ import type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.t
4344import { stripDowngradedToolCallText, THINKING_TAG_SCAN_RE } from "./pi-embedded-utils.js";
4445import { hasNonzeroUsage, normalizeUsage, type UsageLike } from "./usage.js";
454646-const FINAL_TAG_SCAN_RE = /<\s*(\/?)\s*final\s*>/gi;
4747const STREAM_STRIPPED_BLOCK_TAG_NAMES = [
4848"final",
4949"think",
@@ -60,15 +60,17 @@ function isPotentialTrailingBlockTagFragment(fragment: string): boolean {
6060if (!fragment.startsWith("<") || fragment.includes(">")) {
6161return false;
6262}
63-const normalized = fragment.toLowerCase().replace(/\s+/g, "");
64-if (!normalized.startsWith("<")) {
65-return false;
63+const body = fragment.toLowerCase().slice(1).trimStart().replace(/^\//, "").trimStart();
64+if (!body) {
65+return true;
6666}
67-const candidate = normalized.slice(1).replace(/^\//, "");
68-if (!candidate) {
67+const namePart = body.split(/[\s/>]/, 1)[0] ?? "";
68+if (!namePart) {
6969return true;
7070}
71-return STREAM_STRIPPED_BLOCK_TAG_NAMES.some((name) => name.startsWith(candidate));
71+return STREAM_STRIPPED_BLOCK_TAG_NAMES.some((name) => {
72+return name.startsWith(namePart) || namePart === name;
73+});
7274}
73757476function splitTrailingBlockTagFragment(
@@ -636,34 +638,42 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
636638const finalCodeSpans = buildCodeSpanIndex(processed, inlineStateStart);
637639if (!params.enforceFinalTag) {
638640state.inlineCode = finalCodeSpans.inlineState;
639-FINAL_TAG_SCAN_RE.lastIndex = 0;
640-return stripTagsOutsideCodeSpans(processed, FINAL_TAG_SCAN_RE, finalCodeSpans.isInside);
641+return stripFinalTagsOutsideCodeSpans(processed, finalCodeSpans.isInside);
641642}
642643643644// If enforcement is enabled, only return text that appeared inside a <final> block.
644645let result = "";
645-FINAL_TAG_SCAN_RE.lastIndex = 0;
646646let lastFinalIndex = 0;
647647let inFinal = state.final;
648648let everInFinal = state.final;
649649650-for (const match of processed.matchAll(FINAL_TAG_SCAN_RE)) {
651-const idx = match.index ?? 0;
650+for (const match of findFinalTagMatches(processed)) {
651+const idx = match.index;
652652if (finalCodeSpans.isInside(idx)) {
653653continue;
654654}
655-const isClose = match[1] === "/";
655+const isClose = match.isClose;
656+const isSelfClosing = match.isSelfClosing;
656657657-if (!inFinal && !isClose) {
658+if (isSelfClosing) {
659+if (inFinal) {
660+result += processed.slice(lastFinalIndex, idx);
661+inFinal = false;
662+} else {
663+inFinal = true;
664+everInFinal = true;
665+}
666+lastFinalIndex = idx + match.text.length;
667+} else if (!inFinal && !isClose) {
658668// Found <final> start tag.
659669inFinal = true;
660670everInFinal = true;
661-lastFinalIndex = idx + match[0].length;
671+lastFinalIndex = idx + match.text.length;
662672} else if (inFinal && isClose) {
663673// Found </final> end tag.
664674result += processed.slice(lastFinalIndex, idx);
665675inFinal = false;
666-lastFinalIndex = idx + match[0].length;
676+lastFinalIndex = idx + match.text.length;
667677}
668678}
669679@@ -683,24 +693,19 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
683693// missed (e.g. nested tags or hallucinations) to prevent leakage.
684694const resultCodeSpans = buildCodeSpanIndex(result, inlineStateStart);
685695state.inlineCode = resultCodeSpans.inlineState;
686-return stripTagsOutsideCodeSpans(result, FINAL_TAG_SCAN_RE, resultCodeSpans.isInside);
696+return stripFinalTagsOutsideCodeSpans(result, resultCodeSpans.isInside);
687697};
688698689-const stripTagsOutsideCodeSpans = (
690-text: string,
691-pattern: RegExp,
692-isInside: (index: number) => boolean,
693-) => {
699+const stripFinalTagsOutsideCodeSpans = (text: string, isInside: (index: number) => boolean) => {
694700let output = "";
695701let lastIndex = 0;
696-pattern.lastIndex = 0;
697-for (const match of text.matchAll(pattern)) {
698-const idx = match.index ?? 0;
702+for (const match of findFinalTagMatches(text)) {
703+const idx = match.index;
699704if (isInside(idx)) {
700705continue;
701706}
702707output += text.slice(lastIndex, idx);
703-lastIndex = idx + match[0].length;
708+lastIndex = idx + match.text.length;
704709}
705710output += text.slice(lastIndex);
706711return output;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。