diff --git a/packages/aiCore/src/core/plugins/index.ts b/packages/aiCore/src/core/plugins/index.ts index 4c534a6e81..1b7c00eeb9 100644 --- a/packages/aiCore/src/core/plugins/index.ts +++ b/packages/aiCore/src/core/plugins/index.ts @@ -18,7 +18,9 @@ export function createContext(providerId: string, modelId: string, originalParam } // 插件构建器 - 便于创建插件 -export function definePlugin(plugin: AiPlugin): AiPlugin { +export function definePlugin(plugin: AiPlugin): AiPlugin +export function definePlugin AiPlugin>(pluginFactory: T): T +export function definePlugin(plugin: AiPlugin | ((...args: any[]) => AiPlugin)) { return plugin } diff --git a/packages/aiCore/src/core/plugins/manager.ts b/packages/aiCore/src/core/plugins/manager.ts index 7d4c64f5a7..2ac3983f50 100644 --- a/packages/aiCore/src/core/plugins/manager.ts +++ b/packages/aiCore/src/core/plugins/manager.ts @@ -127,7 +127,7 @@ export class PluginManager { stopStream: () => void }) => TransformStream, TextStreamPart> > { - return this.plugins.map((plugin) => plugin.transformStream?.()).filter(Boolean) as Array< + return this.plugins.map((plugin) => plugin.transformStream).filter(Boolean) as Array< (options: { tools?: TOOLS stopStream: () => void diff --git a/packages/aiCore/src/core/plugins/types.ts b/packages/aiCore/src/core/plugins/types.ts index f266ff600c..4eee48c5f9 100644 --- a/packages/aiCore/src/core/plugins/types.ts +++ b/packages/aiCore/src/core/plugins/types.ts @@ -33,8 +33,8 @@ export interface AiPlugin { onError?: (error: Error, context: AiRequestContext) => void | Promise // 【Stream】流处理 - 直接使用 AI SDK - transformStream?: () => (options: { - tools?: TOOLS + transformStream?: (options: { + tools: TOOLS stopStream: () => void }) => TransformStream, TextStreamPart> diff --git a/src/renderer/src/aiCore/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/AiSdkToChunkAdapter.ts index e8e5bca48d..2d258ba1d3 100644 --- a/src/renderer/src/aiCore/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/AiSdkToChunkAdapter.ts @@ -88,7 +88,9 @@ export class AiSdkToChunkAdapter { case 'reasoning': this.onChunk({ type: ChunkType.THINKING_DELTA, - text: chunk.textDelta || '' + text: chunk.textDelta || '', + // 自定义字段 + thinking_millsec: chunk.thinking_millsec || 0 }) break case 'redacted-reasoning': diff --git a/src/renderer/src/aiCore/index_new.ts b/src/renderer/src/aiCore/index_new.ts index 7b2b872b6f..c99b71c71d 100644 --- a/src/renderer/src/aiCore/index_new.ts +++ b/src/renderer/src/aiCore/index_new.ts @@ -13,7 +13,6 @@ import { ProviderConfigFactory, type ProviderId, type ProviderSettingsMap, - smoothStream, StreamTextParams } from '@cherrystudio/ai-core' import { isDedicatedImageGenerationModel } from '@renderer/config/models' @@ -26,6 +25,8 @@ import AiSdkToChunkAdapter from './AiSdkToChunkAdapter' import LegacyAiProvider from './index' import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/aisdk/AiSdkMiddlewareBuilder' import { CompletionsResult } from './middleware/schemas' +import reasonPlugin from './plugins/reasonPlugin' +import textPlugin from './plugins/textPlugin' import { getAiSdkProviderId } from './provider/factory' /** @@ -111,7 +112,13 @@ export default class ModernAiProvider { // TODO:如果后续在调用completions时需要切换provider的话, // 初始化时不构建中间件,等到需要时再构建 const config = providerToAiSdkConfig(provider) - this.modernExecutor = createExecutor(config.providerId, config.options) + this.modernExecutor = createExecutor(config.providerId, config.options, [ + reasonPlugin({ + delayInMs: 80, + chunkingRegex: /([\u4E00-\u9FFF]{3})|\S+\s+/ + }), + textPlugin + ]) } public async completions( @@ -160,7 +167,7 @@ export default class ModernAiProvider { // 动态构建中间件数组 const middlewares = buildAiSdkMiddlewares(finalConfig) - console.log('构建的中间件:', middlewares.length) + console.log('构建的中间件:', middlewares) // 创建带有中间件的执行器 if (middlewareConfig.onChunk) { @@ -168,14 +175,7 @@ export default class ModernAiProvider { const adapter = new AiSdkToChunkAdapter(middlewareConfig.onChunk) const streamResult = await this.modernExecutor.streamText( modelId, - { - ...params, - experimental_transform: smoothStream({ - delayInMs: 80, - // 中文3个字符一个chunk,英文一个单词一个chunk - chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/ - }) - }, + params, middlewares.length > 0 ? { middlewares } : undefined ) diff --git a/src/renderer/src/aiCore/middleware/aisdk/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/aisdk/AiSdkMiddlewareBuilder.ts index f6756ef31a..775ccf3451 100644 --- a/src/renderer/src/aiCore/middleware/aisdk/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/aisdk/AiSdkMiddlewareBuilder.ts @@ -138,7 +138,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo middleware: simulateStreamingMiddleware() }) } - + console.log('builder.build()', builder.buildNamed()) return builder.build() } diff --git a/src/renderer/src/aiCore/middleware/aisdk/ThinkingTimeMiddleware.ts b/src/renderer/src/aiCore/middleware/aisdk/ThinkingTimeMiddleware.ts index 7fc4d33f6f..3da2676c4b 100644 --- a/src/renderer/src/aiCore/middleware/aisdk/ThinkingTimeMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/aisdk/ThinkingTimeMiddleware.ts @@ -22,14 +22,14 @@ export default function thinkingTimeMiddleware(): LanguageModelV1Middleware { if (chunk.type === 'reasoning' || chunk.type === 'redacted-reasoning') { if (!hasThinkingContent) { hasThinkingContent = true - thinkingStartTime = Date.now() + thinkingStartTime = performance.now() } accumulatedThinkingContent += chunk.textDelta || '' // 将所有 chunk 原样传递下去 - controller.enqueue(chunk) + controller.enqueue({ ...chunk, thinking_millsec: performance.now() - thinkingStartTime }) } else { if (hasThinkingContent && thinkingStartTime > 0) { - const thinkingTime = Date.now() - thinkingStartTime + const thinkingTime = performance.now() - thinkingStartTime const thinkingCompleteChunk = { type: 'reasoning-signature', text: accumulatedThinkingContent, diff --git a/src/renderer/src/aiCore/plugins/reasonPlugin.ts b/src/renderer/src/aiCore/plugins/reasonPlugin.ts new file mode 100644 index 0000000000..9c709d24eb --- /dev/null +++ b/src/renderer/src/aiCore/plugins/reasonPlugin.ts @@ -0,0 +1,42 @@ +import { definePlugin } from '@cherrystudio/ai-core' + +export default definePlugin(({ delayInMs, chunkingRegex }: { delayInMs: number; chunkingRegex: RegExp }) => ({ + name: 'reasonPlugin', + + transformStream: () => { + let buffer = '' + 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] + } + return new TransformStream({ + async transform(chunk, controller) { + if (chunk.type !== 'reasoning') { + if (buffer.length > 0) { + controller.enqueue({ type: 'reasoning', textDelta: buffer, thinking_millsec: chunk.thinking_millsec }) + buffer = '' + } + + controller.enqueue(chunk) + return + } + + buffer += chunk.textDelta + let match + + while ((match = detectChunk(buffer)) != null) { + controller.enqueue({ type: 'reasoning', textDelta: match, thinking_millsec: chunk.thinking_millsec }) + buffer = buffer.slice(match.length) + + await delay(delayInMs) + } + } + }) + } +})) diff --git a/src/renderer/src/aiCore/plugins/textPlugin.ts b/src/renderer/src/aiCore/plugins/textPlugin.ts new file mode 100644 index 0000000000..2ec6019ffb --- /dev/null +++ b/src/renderer/src/aiCore/plugins/textPlugin.ts @@ -0,0 +1,10 @@ +import { definePlugin, smoothStream } from '@cherrystudio/ai-core' + +export default definePlugin({ + name: 'textPlugin', + transformStream: smoothStream({ + delayInMs: 80, + // 中文3个字符一个chunk,英文一个单词一个chunk + chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/ + }) +}) diff --git a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx index 74d16a80f0..72f2871586 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx @@ -107,33 +107,33 @@ const ThinkingBlock: React.FC = ({ block }) => { } const ThinkingTimeSeconds = memo( - ({ blockThinkingTime, isThinking }: { blockThinkingTime?: number; isThinking: boolean }) => { + ({ blockThinkingTime, isThinking }: { blockThinkingTime: number; isThinking: boolean }) => { const { t } = useTranslation() - - const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0) + // console.log('blockThinkingTime', blockThinkingTime) + // const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0) // FIXME: 这里统计的和请求处统计的有一定误差 - useEffect(() => { - let timer: NodeJS.Timeout | null = null - if (isThinking) { - timer = setInterval(() => { - setThinkingTime((prev) => prev + 100) - }, 100) - } else if (timer) { - // 立即清除计时器 - clearInterval(timer) - timer = null - } + // useEffect(() => { + // let timer: NodeJS.Timeout | null = null + // if (isThinking) { + // timer = setInterval(() => { + // setThinkingTime((prev) => prev + 100) + // }, 100) + // } else if (timer) { + // // 立即清除计时器 + // clearInterval(timer) + // timer = null + // } - return () => { - if (timer) { - clearInterval(timer) - timer = null - } - } - }, [isThinking]) + // return () => { + // if (timer) { + // clearInterval(timer) + // timer = null + // } + // } + // }, [isThinking]) - const thinkingTimeSeconds = useMemo(() => (thinkingTime / 1000).toFixed(1), [thinkingTime]) + const thinkingTimeSeconds = useMemo(() => (blockThinkingTime / 1000).toFixed(1), [blockThinkingTime]) return t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', { seconds: thinkingTimeSeconds diff --git a/src/renderer/src/types/newMessage.ts b/src/renderer/src/types/newMessage.ts index 8e1a5c26dd..8700ca3fcf 100644 --- a/src/renderer/src/types/newMessage.ts +++ b/src/renderer/src/types/newMessage.ts @@ -72,7 +72,7 @@ export interface MainTextMessageBlock extends BaseMessageBlock { export interface ThinkingMessageBlock extends BaseMessageBlock { type: MessageBlockType.THINKING content: string - thinking_millsec?: number + thinking_millsec: number } // 翻译块