fix: thinking 检测位置约束,防止正文字面量误触发 (Issue #64)

将所有 includes('<thinking>') 替换为 hasLeadingThinking(),
只在 <thinking> 出现在响应开头时才触发提取,
防止用户消息或模型正文中的字面量标签误触发 extractThinking 导致内容丢失。
This commit is contained in:
小海
2026-03-18 14:06:53 +08:00
parent 8a5117bbb1
commit f9bebeb4b5
3 changed files with 26 additions and 14 deletions

View File

@@ -20,7 +20,7 @@ import { convertToCursorRequest, parseToolCalls, hasToolCalls } from './converte
import { sendCursorRequest, sendCursorRequestFull } from './cursor-client.js';
import { getConfig } from './config.js';
import { createRequestLogger, type RequestLogger } from './logger.js';
import { createIncrementalTextStreamer, splitLeadingThinkingBlocks, stripThinkingTags } from './streaming-text.js';
import { createIncrementalTextStreamer, hasLeadingThinking, splitLeadingThinkingBlocks, stripThinkingTags } from './streaming-text.js';
function msgId(): string {
return 'msg_' + uuidv4().replace(/-/g, '').substring(0, 24);
@@ -1072,7 +1072,7 @@ async function handleDirectTextStream(
hasTools: false,
});
if (!finalThinkingContent && finalRawResponse.includes('<thinking>')) {
if (!finalThinkingContent && hasLeadingThinking(finalRawResponse)) {
const { thinkingContent: extracted } = extractThinking(finalRawResponse);
if (extracted) {
finalThinkingContent = extracted;
@@ -1360,7 +1360,7 @@ async function handleStream(res: Response, cursorReq: CursorChatRequest, body: A
// ★ Thinking 提取(在拒绝检测之前,防止 thinking 内容触发 isRefusal 误判)
// 混合流式阶段可能已经提取了 thinking优先使用
let thinkingContent = hybridThinkingContent || '';
if (fullResponse.includes('<thinking>')) {
if (hasLeadingThinking(fullResponse)) {
const { thinkingContent: extracted, strippedText } = extractThinking(fullResponse);
if (extracted) {
if (!thinkingContent) thinkingContent = extracted;
@@ -1393,7 +1393,7 @@ async function handleStream(res: Response, cursorReq: CursorChatRequest, body: A
activeCursorReq = await convertToCursorRequest(retryBody);
await executeStream(true); // 重试不传回调(纯缓冲模式)
// 重试后也需要剥离 thinking 标签
if (fullResponse.includes('<thinking>')) {
if (hasLeadingThinking(fullResponse)) {
const { thinkingContent: retryThinking, strippedText: retryStripped } = extractThinking(fullResponse);
if (retryThinking) {
thinkingContent = retryThinking;
@@ -1798,7 +1798,7 @@ async function handleNonStream(res: Response, cursorReq: CursorChatRequest, body
// ★ Thinking 提取(在拒绝检测之前)
// 始终剥离 thinking 标签,避免泄漏到最终文本中
let thinkingContent = '';
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
const { thinkingContent: extracted, strippedText } = extractThinking(fullText);
if (extracted) {
thinkingContent = extracted;
@@ -1826,7 +1826,7 @@ async function handleNonStream(res: Response, cursorReq: CursorChatRequest, body
activeCursorReq = await convertToCursorRequest(retryBody);
fullText = await sendCursorRequestFull(activeCursorReq);
// 重试后也需要剥离 thinking 标签
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
const { thinkingContent: retryThinking, strippedText: retryStripped } = extractThinking(fullText);
if (retryThinking) {
thinkingContent = retryThinking;

View File

@@ -28,7 +28,7 @@ import { convertToCursorRequest, parseToolCalls, hasToolCalls } from './converte
import { sendCursorRequest, sendCursorRequestFull } from './cursor-client.js';
import { getConfig } from './config.js';
import { createRequestLogger } from './logger.js';
import { createIncrementalTextStreamer, splitLeadingThinkingBlocks, stripThinkingTags } from './streaming-text.js';
import { createIncrementalTextStreamer, hasLeadingThinking, splitLeadingThinkingBlocks, stripThinkingTags } from './streaming-text.js';
import {
autoContinueCursorToolResponseFull,
autoContinueCursorToolResponseStream,
@@ -907,7 +907,7 @@ async function handleOpenAIStream(
// ★ Thinking 提取(在拒绝检测之前)
let reasoningContent: string | undefined = hybridThinkingContent || undefined;
if (fullResponse.includes('<thinking>')) {
if (hasLeadingThinking(fullResponse)) {
const { thinkingContent: extracted, strippedText } = extractThinking(fullResponse);
if (extracted) {
if (thinkingEnabled && !reasoningContent) {
@@ -1118,7 +1118,7 @@ async function handleOpenAINonStream(
// ★ Thinking 提取必须在拒绝检测之前 — 否则 thinking 内容中的关键词会触发 isRefusal 误判
const thinkingEnabled = anthropicReq.thinking?.type === 'enabled';
let reasoningContent: string | undefined;
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
const { thinkingContent: extracted, strippedText } = extractThinking(fullText);
if (extracted) {
if (thinkingEnabled) {
@@ -1140,7 +1140,7 @@ async function handleOpenAINonStream(
activeCursorReq = retryCursorReq;
fullText = await sendCursorRequestFull(activeCursorReq);
// 重试响应也需要先剥离 thinking
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
fullText = extractThinking(fullText).strippedText;
}
if (!shouldRetry()) break;
@@ -1510,7 +1510,7 @@ async function handleResponsesStream(
await executeStream();
// Thinking 提取
if (fullResponse.includes('<thinking>')) {
if (hasLeadingThinking(fullResponse)) {
const { strippedText } = extractThinking(fullResponse);
fullResponse = strippedText;
}
@@ -1527,7 +1527,7 @@ async function handleResponsesStream(
const retryBody = buildRetryRequest(anthropicReq, retryCount - 1);
activeCursorReq = await convertToCursorRequest(retryBody);
await executeStream();
if (fullResponse.includes('<thinking>')) {
if (hasLeadingThinking(fullResponse)) {
fullResponse = extractThinking(fullResponse).strippedText;
}
}
@@ -1713,7 +1713,7 @@ async function handleResponsesNonStream(
const hasTools = (anthropicReq.tools?.length ?? 0) > 0;
// Thinking 提取
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
fullText = extractThinking(fullText).strippedText;
}
@@ -1725,7 +1725,7 @@ async function handleResponsesNonStream(
const retryCursorReq = await convertToCursorRequest(retryBody);
activeCursorReq = retryCursorReq;
fullText = await sendCursorRequestFull(activeCursorReq);
if (fullText.includes('<thinking>')) {
if (hasLeadingThinking(fullText)) {
fullText = extractThinking(fullText).strippedText;
}
if (!shouldRetry()) break;

View File

@@ -52,6 +52,18 @@ export function stripThinkingTags(text: string): string {
return text.slice(0, startIdx).trim();
}
/**
* 检测文本是否以 <thinking> 开头(允许前导空白)。
*
* ★ 修复 Issue #64用位置约束替代宽松的 includes('<thinking>')
* 防止用户消息或模型正文中的字面量 <thinking> 误触发 extractThinking
* 导致正文内容被错误截断或丢失。
*/
export function hasLeadingThinking(text: string): boolean {
if (!text) return false;
return /^\s*<thinking>/.test(text);
}
/**
* 只解析“前导 thinking 块”。
*