mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
feat: enhance plugin system with new reasoning and text plugins
- Introduced `reasonPlugin` and `textPlugin` to improve chunk processing and handling of reasoning content. - Updated `transformStream` method signatures for better type safety and usability. - Enhanced `ThinkingTimeMiddleware` to accurately track thinking time using `performance.now()`. - Refactored `ThinkingBlock` component to utilize block thinking time directly, improving performance and clarity. - Added logging for middleware builder to assist in debugging and monitoring middleware configurations.
This commit is contained in:
parent
e4c0ea035f
commit
f23a026a28
@ -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<T extends (...args: any[]) => AiPlugin>(pluginFactory: T): T
|
||||
export function definePlugin(plugin: AiPlugin | ((...args: any[]) => AiPlugin)) {
|
||||
return plugin
|
||||
}
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@ export class PluginManager {
|
||||
stopStream: () => void
|
||||
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
||||
> {
|
||||
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
|
||||
|
||||
@ -33,8 +33,8 @@ export interface AiPlugin {
|
||||
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
||||
|
||||
// 【Stream】流处理 - 直接使用 AI SDK
|
||||
transformStream?: <TOOLS extends ToolSet>() => (options: {
|
||||
tools?: TOOLS
|
||||
transformStream?: <TOOLS extends ToolSet>(options: {
|
||||
tools: TOOLS
|
||||
stopStream: () => void
|
||||
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
||||
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
middleware: simulateStreamingMiddleware()
|
||||
})
|
||||
}
|
||||
|
||||
console.log('builder.build()', builder.buildNamed())
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
42
src/renderer/src/aiCore/plugins/reasonPlugin.ts
Normal file
42
src/renderer/src/aiCore/plugins/reasonPlugin.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
10
src/renderer/src/aiCore/plugins/textPlugin.ts
Normal file
10
src/renderer/src/aiCore/plugins/textPlugin.ts
Normal file
@ -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+/
|
||||
})
|
||||
})
|
||||
@ -107,33 +107,33 @@ const ThinkingBlock: React.FC<Props> = ({ 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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
// 翻译块
|
||||
|
||||
Loading…
Reference in New Issue
Block a user