From a4eeb6c4b101d17f08956c2e9961aab461edf4b3 Mon Sep 17 00:00:00 2001 From: Calvin Date: Sat, 13 Dec 2025 22:57:37 +0800 Subject: [PATCH 1/4] fix: preserve thinking time when stopping reply Fixes #11886 Signed-off-by: Calvin --- .../callbacks/baseCallbacks.ts | 29 +++++++++++++++++-- .../messageStreaming/callbacks/index.ts | 15 +++++----- .../callbacks/thinkingCallbacks.ts | 6 ++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts index ed9bdd584..146266fd9 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts @@ -29,10 +29,20 @@ interface BaseCallbacksDependencies { assistantMsgId: string saveUpdatesToDB: any assistant: Assistant + getCurrentThinkingInfo?: () => { blockId: string | null; millsec: number } } export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => { - const { blockManager, dispatch, getState, topicId, assistantMsgId, saveUpdatesToDB, assistant } = deps + const { + blockManager, + dispatch, + getState, + topicId, + assistantMsgId, + saveUpdatesToDB, + assistant, + getCurrentThinkingInfo + } = deps const startTime = Date.now() const notificationService = NotificationService.getInstance() @@ -111,13 +121,28 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => { if (currentMessage) { const allBlockRefs = findAllBlocks(currentMessage) const blockState = getState().messageBlocks + // 获取当前思考信息(如果有),用于保留实际思考时间 + const thinkingInfo = getCurrentThinkingInfo?.() for (const blockRef of allBlockRefs) { const block = blockState.entities[blockRef.id] if (block && block.status === MessageBlockStatus.STREAMING && block.id !== possibleBlockId) { + // 构建更新对象 + const changes: Record = { + status: isErrorTypeAbort ? MessageBlockStatus.PAUSED : MessageBlockStatus.ERROR + } + // 如果是 thinking block 且有思考时间信息,保留实际思考时间 + if ( + block.type === MessageBlockType.THINKING && + thinkingInfo?.blockId === block.id && + thinkingInfo?.millsec && + thinkingInfo.millsec > 0 + ) { + changes.thinking_millsec = thinkingInfo.millsec + } dispatch( updateOneBlock({ id: block.id, - changes: { status: isErrorTypeAbort ? MessageBlockStatus.PAUSED : MessageBlockStatus.ERROR } + changes }) ) } diff --git a/src/renderer/src/services/messageStreaming/callbacks/index.ts b/src/renderer/src/services/messageStreaming/callbacks/index.ts index 2bb1d158b..1587cb936 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/index.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/index.ts @@ -23,6 +23,12 @@ interface CallbacksDependencies { export const createCallbacks = (deps: CallbacksDependencies) => { const { blockManager, dispatch, getState, topicId, assistantMsgId, saveUpdatesToDB, assistant } = deps + // 首先创建 thinkingCallbacks ,以便传递 getCurrentThinkingInfo 给 baseCallbacks + const thinkingCallbacks = createThinkingCallbacks({ + blockManager, + assistantMsgId + }) + // 创建基础回调 const baseCallbacks = createBaseCallbacks({ blockManager, @@ -31,13 +37,8 @@ export const createCallbacks = (deps: CallbacksDependencies) => { topicId, assistantMsgId, saveUpdatesToDB, - assistant - }) - - // 创建各类回调 - const thinkingCallbacks = createThinkingCallbacks({ - blockManager, - assistantMsgId + assistant, + getCurrentThinkingInfo: thinkingCallbacks.getCurrentThinkingInfo }) const toolCallbacks = createToolCallbacks({ diff --git a/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts index aeb160fd0..3c718c4a6 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/thinkingCallbacks.ts @@ -19,6 +19,12 @@ export const createThinkingCallbacks = (deps: ThinkingCallbacksDependencies) => let thinking_millsec_now: number = 0 return { + // 获取当前思考时间(用于停止回复时保留思考时间) + getCurrentThinkingInfo: () => ({ + blockId: thinkingBlockId, + millsec: thinking_millsec_now > 0 ? performance.now() - thinking_millsec_now : 0 + }), + onThinkingStart: async () => { if (blockManager.hasInitialPlaceholder) { const changes: Partial = { From 1ce2076185e9cbe355f6ec0757741d9ffc34e22a Mon Sep 17 00:00:00 2001 From: Calvin Date: Sat, 13 Dec 2025 23:07:09 +0800 Subject: [PATCH 2/4] fix: also preserve thinking time when stopping during thinking This extends the previous fix to also handle the case when the user stops the reply while thinking is still in progress (not just after thinking is complete). Signed-off-by: Calvin --- .../messageStreaming/callbacks/baseCallbacks.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts index 146266fd9..7000ad3d5 100644 --- a/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts +++ b/src/renderer/src/services/messageStreaming/callbacks/baseCallbacks.ts @@ -108,10 +108,17 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => { const possibleBlockId = findBlockIdForCompletion() if (possibleBlockId) { - // 更改上一个block的状态为ERROR - const changes = { + // 更改上一个block的状态为ERROR/PAUSED + const changes: Record = { status: isErrorTypeAbort ? MessageBlockStatus.PAUSED : MessageBlockStatus.ERROR } + // 如果是 thinking block,保留实际思考时间 + if (blockManager.lastBlockType === MessageBlockType.THINKING) { + const thinkingInfo = getCurrentThinkingInfo?.() + if (thinkingInfo?.blockId === possibleBlockId && thinkingInfo?.millsec && thinkingInfo.millsec > 0) { + changes.thinking_millsec = thinkingInfo.millsec + } + } blockManager.smartBlockUpdate(possibleBlockId, changes, blockManager.lastBlockType!, true) } From cc6fcda8565402e33e32ceb2f3293e93f31f771d Mon Sep 17 00:00:00 2001 From: Calvin Date: Sun, 14 Dec 2025 00:11:10 +0800 Subject: [PATCH 3/4] fix: auto-complete thinking when text output starts This fixes the issue where the thinking timer continues running after thinking is complete and text output begins. Some AI providers don't send a reasoning-end event explicitly, so we now auto-complete thinking when a text-start event is received with accumulated reasoning content. Fixes #11796 Signed-off-by: Calvin --- src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 5de2ac345..1c0dbe8ae 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -145,6 +145,15 @@ export class AiSdkToChunkAdapter { } // === 文本相关事件 === case 'text-start': + // 如果有未完成的思考内容,先生成 THINKING_COMPLETE + // 这处理了某些提供商不发送 reasoning-end 事件的情况 + if (final.reasoningContent) { + this.onChunk({ + type: ChunkType.THINKING_COMPLETE, + text: final.reasoningContent + }) + final.reasoningContent = '' + } this.onChunk({ type: ChunkType.TEXT_START }) From eb2aaf1759ac3275f29f18fb61082db880495f41 Mon Sep 17 00:00:00 2001 From: Calvin Date: Mon, 15 Dec 2025 01:09:01 +0800 Subject: [PATCH 4/4] refactor: extract emitThinkingCompleteIfNeeded to reduce duplication Extract the shared logic for emitting THINKING_COMPLETE chunk into a reusable method. This removes code duplication between text-start and reasoning-end event handlers as suggested in code review. Signed-off-by: Calvin --- .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 1c0dbe8ae..a53ac3c81 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -120,6 +120,23 @@ export class AiSdkToChunkAdapter { } } + /** + * 如果有累积的思考内容,发送 THINKING_COMPLETE chunk 并清空 + * @param final 包含 reasoningContent 的状态对象 + * @returns 是否发送了 THINKING_COMPLETE chunk + */ + private emitThinkingCompleteIfNeeded(final: { reasoningContent: string; [key: string]: any }): boolean { + if (final.reasoningContent) { + this.onChunk({ + type: ChunkType.THINKING_COMPLETE, + text: final.reasoningContent + }) + final.reasoningContent = '' + return true + } + return false + } + /** * 转换 AI SDK chunk 为 Cherry Studio chunk 并调用回调 * @param chunk AI SDK 的 chunk 数据 @@ -147,13 +164,7 @@ export class AiSdkToChunkAdapter { case 'text-start': // 如果有未完成的思考内容,先生成 THINKING_COMPLETE // 这处理了某些提供商不发送 reasoning-end 事件的情况 - if (final.reasoningContent) { - this.onChunk({ - type: ChunkType.THINKING_COMPLETE, - text: final.reasoningContent - }) - final.reasoningContent = '' - } + this.emitThinkingCompleteIfNeeded(final) this.onChunk({ type: ChunkType.TEXT_START }) @@ -224,11 +235,7 @@ export class AiSdkToChunkAdapter { }) break case 'reasoning-end': - this.onChunk({ - type: ChunkType.THINKING_COMPLETE, - text: final.reasoningContent || '' - }) - final.reasoningContent = '' + this.emitThinkingCompleteIfNeeded(final) break // === 工具调用相关事件(原始 AI SDK 事件,如果没有被中间件处理) ===