fix: thinking time on stop (#11900)

* fix: preserve thinking time when stopping reply

Fixes #11886

Signed-off-by: Calvin <calvinvwei@gmail.com>

* 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 <calvinvwei@gmail.com>

* 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 <calvinvwei@gmail.com>

* 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 <calvinvwei@gmail.com>

---------

Signed-off-by: Calvin <calvinvwei@gmail.com>
This commit is contained in:
Calvin Wade 2026-01-04 19:43:44 +08:00 committed by kangfenmao
parent d27d750bc5
commit b4aeced1f9
4 changed files with 70 additions and 17 deletions

View File

@ -120,6 +120,21 @@ export class AiSdkToChunkAdapter {
}
}
/**
* THINKING_COMPLETE chunk
* @param final reasoningContent
* @returns THINKING_COMPLETE chunk
*/
private emitThinkingCompleteIfNeeded(final: { reasoningContent: string; [key: string]: any }) {
if (final.reasoningContent) {
this.onChunk({
type: ChunkType.THINKING_COMPLETE,
text: final.reasoningContent
})
final.reasoningContent = ''
}
}
/**
* AI SDK chunk Cherry Studio chunk
* @param chunk AI SDK chunk
@ -145,6 +160,9 @@ export class AiSdkToChunkAdapter {
}
// === 文本相关事件 ===
case 'text-start':
// 如果有未完成的思考内容,先生成 THINKING_COMPLETE
// 这处理了某些提供商不发送 reasoning-end 事件的情况
this.emitThinkingCompleteIfNeeded(final)
this.onChunk({
type: ChunkType.TEXT_START
})
@ -215,11 +233,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 事件,如果没有被中间件处理) ===

View File

@ -8,7 +8,7 @@ import { updateOneBlock } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { newMessagesActions } from '@renderer/store/newMessage'
import type { Assistant } from '@renderer/types'
import type { PlaceholderMessageBlock, Response } from '@renderer/types/newMessage'
import type { PlaceholderMessageBlock, Response, ThinkingMessageBlock } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { uuid } from '@renderer/utils'
import { isAbortError, serializeError } from '@renderer/utils/error'
@ -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()
@ -98,10 +108,17 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => {
const possibleBlockId = findBlockIdForCompletion()
if (possibleBlockId) {
// 更改上一个block的状态为ERROR
const changes = {
// 更改上一个block的状态为ERROR/PAUSED
const changes: Partial<ThinkingMessageBlock> = {
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)
}
@ -111,13 +128,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: Partial<ThinkingMessageBlock> = {
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
})
)
}

View File

@ -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({

View File

@ -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<MessageBlock> = {