diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index 53853f1314..7c2283f4bf 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -28,8 +28,7 @@ import AiSdkToChunkAdapter from './AiSdkToChunkAdapter' import LegacyAiProvider from './index' import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/aisdk/AiSdkMiddlewareBuilder' import { CompletionsResult } from './middleware/schemas' -import smoothReasoningPlugin from './plugins/smoothReasoningPlugin' -import textPlugin from './plugins/textPlugin' +import reasoningTimePlugin from './plugins/reasoningTimePlugin' import { getAiSdkProviderId } from './provider/factory' /** @@ -119,11 +118,11 @@ export default class ModernAiProvider { private buildPlugins(middlewareConfig: AiSdkMiddlewareConfig) { const plugins: AiPlugin[] = [] // 1. 总是添加通用插件 - plugins.push(textPlugin) + // plugins.push(textPlugin) // 2. 推理模型时添加推理插件 if (middlewareConfig.enableReasoning) { - plugins.push(smoothReasoningPlugin) + plugins.push(reasoningTimePlugin) } // 3. 启用Prompt工具调用时添加工具插件 @@ -151,7 +150,6 @@ export default class ModernAiProvider { }) ) } - console.log( '最终插件列表:', plugins.map((p) => p.name) diff --git a/src/renderer/src/aiCore/plugins/reasoningTimePlugin.ts b/src/renderer/src/aiCore/plugins/reasoningTimePlugin.ts new file mode 100644 index 0000000000..99e731e56f --- /dev/null +++ b/src/renderer/src/aiCore/plugins/reasoningTimePlugin.ts @@ -0,0 +1,67 @@ +import { definePlugin } from '@cherrystudio/ai-core' + +export default definePlugin({ + name: 'reasoningTimePlugin', + + transformStream: () => () => { + // === 时间跟踪状态 === + let thinkingStartTime = 0 + let hasStartedThinking = false + let accumulatedThinkingContent = '' + + return new TransformStream({ + transform(chunk, controller) { + if (chunk.type !== 'reasoning') { + // === 处理 reasoning 结束 === + if (hasStartedThinking) { + console.log(`[ReasoningPlugin] Ending reasoning.`) + + // 生成 reasoning-signature + controller.enqueue({ + type: 'reasoning-signature', + text: accumulatedThinkingContent, + thinking_millsec: performance.now() - thinkingStartTime + }) + + // 重置状态 + accumulatedThinkingContent = '' + hasStartedThinking = false + thinkingStartTime = 0 + } + + controller.enqueue(chunk) + return + } + + // === 处理 reasoning 类型 === + + // 1. 时间跟踪逻辑 + if (!hasStartedThinking) { + hasStartedThinking = true + thinkingStartTime = performance.now() + console.log(`[ReasoningPlugin] Starting reasoning session`) + } + accumulatedThinkingContent += chunk.textDelta + + // 2. 直接透传 chunk,并附加上时间 + console.log(`[ReasoningPlugin] Forwarding reasoning chunk: "${chunk.textDelta}"`) + controller.enqueue({ + ...chunk, + thinking_millsec: performance.now() - thinkingStartTime + }) + }, + + // === flush 处理流结束时仍在reasoning状态的场景 === + flush(controller) { + if (hasStartedThinking) { + console.log(`[ReasoningPlugin] Final flush for reasoning-signature.`) + controller.enqueue({ + type: 'reasoning-signature', + text: accumulatedThinkingContent, + thinking_millsec: performance.now() - thinkingStartTime + }) + } + } + }) + } +}) diff --git a/src/renderer/src/aiCore/plugins/smoothReasoningPlugin.ts b/src/renderer/src/aiCore/plugins/smoothReasoningPlugin.ts index 9459761cd3..b020afa21a 100644 --- a/src/renderer/src/aiCore/plugins/smoothReasoningPlugin.ts +++ b/src/renderer/src/aiCore/plugins/smoothReasoningPlugin.ts @@ -1,7 +1,9 @@ +// 可能会废弃,在流上做delay还是有问题 + import { definePlugin } from '@cherrystudio/ai-core' const chunkingRegex = /([\u4E00-\u9FFF])|\S+\s+/ -const delayInMs = 20 +const delayInMs = 50 export default definePlugin({ name: 'reasoningPlugin', @@ -15,12 +17,32 @@ export default definePlugin({ let hasStartedThinking = false let accumulatedThinkingContent = '' + // === 日志计数器 === + let chunkCount = 0 + let delayCount = 0 + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) - const detectChunk = (buffer: string) => { - const match = chunkingRegex.exec(buffer) - if (!match) return null - return buffer.slice(0, match.index) + match?.[0] + // 收集所有当前可匹配的chunks + const collectMatches = (inputBuffer: string) => { + const matches: string[] = [] + let tempBuffer = inputBuffer + let match + + // 重置regex状态 + chunkingRegex.lastIndex = 0 + + while ((match = chunkingRegex.exec(tempBuffer)) !== null) { + matches.push(match[0]) + tempBuffer = tempBuffer.slice(match.index + match[0].length) + // 重置regex以从头开始匹配剩余内容 + chunkingRegex.lastIndex = 0 + } + + return { + matches, + remaining: tempBuffer + } } return new TransformStream({ @@ -28,8 +50,13 @@ export default definePlugin({ if (chunk.type !== 'reasoning') { // === 处理 reasoning 结束 === if (hasStartedThinking && accumulatedThinkingContent) { + console.log( + `[ReasoningPlugin] Ending reasoning. Final stats: chunks=${chunkCount}, delays=${delayCount}, efficiency=${(chunkCount / Math.max(delayCount, 1)).toFixed(2)}x` + ) + // 先输出剩余的 buffer if (buffer.length > 0) { + console.log(`[ReasoningPlugin] Flushing remaining buffer: "${buffer}"`) controller.enqueue({ type: 'reasoning', textDelta: buffer, @@ -49,6 +76,8 @@ export default definePlugin({ accumulatedThinkingContent = '' hasStartedThinking = false thinkingStartTime = 0 + chunkCount = 0 + delayCount = 0 } controller.enqueue(chunk) @@ -61,28 +90,56 @@ export default definePlugin({ if (!hasStartedThinking) { hasStartedThinking = true thinkingStartTime = performance.now() + console.log(`[ReasoningPlugin] Starting reasoning session`) } accumulatedThinkingContent += chunk.textDelta - // 2. Smooth 处理逻辑 + // 2. 动态Smooth处理逻辑 + const beforeBuffer = buffer buffer += chunk.textDelta - let match - while ((match = detectChunk(buffer)) != null) { - controller.enqueue({ - type: 'reasoning', - textDelta: match, - thinking_millsec: performance.now() - thinkingStartTime - }) - buffer = buffer.slice(match.length) + console.log(`[ReasoningPlugin] Received chunk: "${chunk.textDelta}", buffer: "${beforeBuffer}" → "${buffer}"`) + // 收集所有当前可以匹配的chunks + const { matches, remaining } = collectMatches(buffer) + + if (matches.length > 0) { + console.log( + `[ReasoningPlugin] Collected ${matches.length} matches: [${matches.map((m) => `"${m}"`).join(', ')}], remaining: "${remaining}"` + ) + + // 批量输出所有匹配的chunks + for (const matchText of matches) { + controller.enqueue({ + type: 'reasoning', + textDelta: matchText, + thinking_millsec: performance.now() - thinkingStartTime + }) + chunkCount++ + } + + // 更新buffer为剩余内容 + buffer = remaining + + // 只等待一次,而不是每个chunk都等待 + delayCount++ + console.log( + `[ReasoningPlugin] Delaying ${delayInMs}ms (delay #${delayCount}, efficiency: ${(chunkCount / delayCount).toFixed(2)} chunks/delay)` + ) + const delayStart = performance.now() await delay(delayInMs) + const actualDelay = performance.now() - delayStart + console.log(`[ReasoningPlugin] Delay completed: expected=${delayInMs}ms, actual=${actualDelay.toFixed(1)}ms`) + } else { + console.log(`[ReasoningPlugin] No matches found, keeping in buffer: "${buffer}"`) } + // 如果没有匹配,保留在buffer中等待下次数据 }, // === flush 处理剩余 buffer === flush(controller) { if (buffer.length > 0) { + console.log(`[ReasoningPlugin] Final flush: "${buffer}"`) controller.enqueue({ type: 'reasoning', textDelta: buffer, diff --git a/src/renderer/src/aiCore/plugins/textPlugin.ts b/src/renderer/src/aiCore/plugins/textPlugin.ts index af2b002d61..84b1a3f5d6 100644 --- a/src/renderer/src/aiCore/plugins/textPlugin.ts +++ b/src/renderer/src/aiCore/plugins/textPlugin.ts @@ -1,10 +1,12 @@ +// 可能会废弃,在流上做delay还是有问题 + import { definePlugin, smoothStream } from '@cherrystudio/ai-core' export default definePlugin({ name: 'textPlugin', transformStream: () => smoothStream({ - delayInMs: 20, + delayInMs: 50, // 中文3个字符一个chunk,英文一个单词一个chunk chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/ }) diff --git a/src/renderer/src/store/thunk/messageThunk.ts b/src/renderer/src/store/thunk/messageThunk.ts index 67e0d68196..5e59953df0 100644 --- a/src/renderer/src/store/thunk/messageThunk.ts +++ b/src/renderer/src/store/thunk/messageThunk.ts @@ -148,7 +148,7 @@ const getBlockThrottler = (id: string) => { } const rafId = requestAnimationFrame(() => { - // store.dispatch(updateOneBlock({ id, changes: blockUpdate })) + store.dispatch(updateOneBlock({ id, changes: blockUpdate })) blockUpdateRafs.delete(id) }) @@ -167,7 +167,7 @@ const getBlockThrottler = (id: string) => { */ const throttledBlockUpdate = (id: string, blockUpdate: any) => { const throttler = getBlockThrottler(id) - store.dispatch(updateOneBlock({ id, changes: blockUpdate })) + // store.dispatch(updateOneBlock({ id, changes: blockUpdate })) throttler(blockUpdate) }