mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 23:10:20 +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
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export class PluginManager {
|
|||||||
stopStream: () => void
|
stopStream: () => void
|
||||||
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
}) => 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: {
|
(options: {
|
||||||
tools?: TOOLS
|
tools?: TOOLS
|
||||||
stopStream: () => void
|
stopStream: () => void
|
||||||
|
|||||||
@ -33,8 +33,8 @@ export interface AiPlugin {
|
|||||||
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
||||||
|
|
||||||
// 【Stream】流处理 - 直接使用 AI SDK
|
// 【Stream】流处理 - 直接使用 AI SDK
|
||||||
transformStream?: <TOOLS extends ToolSet>() => (options: {
|
transformStream?: <TOOLS extends ToolSet>(options: {
|
||||||
tools?: TOOLS
|
tools: TOOLS
|
||||||
stopStream: () => void
|
stopStream: () => void
|
||||||
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,9 @@ export class AiSdkToChunkAdapter {
|
|||||||
case 'reasoning':
|
case 'reasoning':
|
||||||
this.onChunk({
|
this.onChunk({
|
||||||
type: ChunkType.THINKING_DELTA,
|
type: ChunkType.THINKING_DELTA,
|
||||||
text: chunk.textDelta || ''
|
text: chunk.textDelta || '',
|
||||||
|
// 自定义字段
|
||||||
|
thinking_millsec: chunk.thinking_millsec || 0
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'redacted-reasoning':
|
case 'redacted-reasoning':
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
ProviderConfigFactory,
|
ProviderConfigFactory,
|
||||||
type ProviderId,
|
type ProviderId,
|
||||||
type ProviderSettingsMap,
|
type ProviderSettingsMap,
|
||||||
smoothStream,
|
|
||||||
StreamTextParams
|
StreamTextParams
|
||||||
} from '@cherrystudio/ai-core'
|
} from '@cherrystudio/ai-core'
|
||||||
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
|
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
|
||||||
@ -26,6 +25,8 @@ import AiSdkToChunkAdapter from './AiSdkToChunkAdapter'
|
|||||||
import LegacyAiProvider from './index'
|
import LegacyAiProvider from './index'
|
||||||
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/aisdk/AiSdkMiddlewareBuilder'
|
import { AiSdkMiddlewareConfig, buildAiSdkMiddlewares } from './middleware/aisdk/AiSdkMiddlewareBuilder'
|
||||||
import { CompletionsResult } from './middleware/schemas'
|
import { CompletionsResult } from './middleware/schemas'
|
||||||
|
import reasonPlugin from './plugins/reasonPlugin'
|
||||||
|
import textPlugin from './plugins/textPlugin'
|
||||||
import { getAiSdkProviderId } from './provider/factory'
|
import { getAiSdkProviderId } from './provider/factory'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +112,13 @@ export default class ModernAiProvider {
|
|||||||
// TODO:如果后续在调用completions时需要切换provider的话,
|
// TODO:如果后续在调用completions时需要切换provider的话,
|
||||||
// 初始化时不构建中间件,等到需要时再构建
|
// 初始化时不构建中间件,等到需要时再构建
|
||||||
const config = providerToAiSdkConfig(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(
|
public async completions(
|
||||||
@ -160,7 +167,7 @@ export default class ModernAiProvider {
|
|||||||
|
|
||||||
// 动态构建中间件数组
|
// 动态构建中间件数组
|
||||||
const middlewares = buildAiSdkMiddlewares(finalConfig)
|
const middlewares = buildAiSdkMiddlewares(finalConfig)
|
||||||
console.log('构建的中间件:', middlewares.length)
|
console.log('构建的中间件:', middlewares)
|
||||||
|
|
||||||
// 创建带有中间件的执行器
|
// 创建带有中间件的执行器
|
||||||
if (middlewareConfig.onChunk) {
|
if (middlewareConfig.onChunk) {
|
||||||
@ -168,14 +175,7 @@ export default class ModernAiProvider {
|
|||||||
const adapter = new AiSdkToChunkAdapter(middlewareConfig.onChunk)
|
const adapter = new AiSdkToChunkAdapter(middlewareConfig.onChunk)
|
||||||
const streamResult = await this.modernExecutor.streamText(
|
const streamResult = await this.modernExecutor.streamText(
|
||||||
modelId,
|
modelId,
|
||||||
{
|
params,
|
||||||
...params,
|
|
||||||
experimental_transform: smoothStream({
|
|
||||||
delayInMs: 80,
|
|
||||||
// 中文3个字符一个chunk,英文一个单词一个chunk
|
|
||||||
chunking: /([\u4E00-\u9FFF]{3})|\S+\s+/
|
|
||||||
})
|
|
||||||
},
|
|
||||||
middlewares.length > 0 ? { middlewares } : undefined
|
middlewares.length > 0 ? { middlewares } : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
|||||||
middleware: simulateStreamingMiddleware()
|
middleware: simulateStreamingMiddleware()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
console.log('builder.build()', builder.buildNamed())
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,14 +22,14 @@ export default function thinkingTimeMiddleware(): LanguageModelV1Middleware {
|
|||||||
if (chunk.type === 'reasoning' || chunk.type === 'redacted-reasoning') {
|
if (chunk.type === 'reasoning' || chunk.type === 'redacted-reasoning') {
|
||||||
if (!hasThinkingContent) {
|
if (!hasThinkingContent) {
|
||||||
hasThinkingContent = true
|
hasThinkingContent = true
|
||||||
thinkingStartTime = Date.now()
|
thinkingStartTime = performance.now()
|
||||||
}
|
}
|
||||||
accumulatedThinkingContent += chunk.textDelta || ''
|
accumulatedThinkingContent += chunk.textDelta || ''
|
||||||
// 将所有 chunk 原样传递下去
|
// 将所有 chunk 原样传递下去
|
||||||
controller.enqueue(chunk)
|
controller.enqueue({ ...chunk, thinking_millsec: performance.now() - thinkingStartTime })
|
||||||
} else {
|
} else {
|
||||||
if (hasThinkingContent && thinkingStartTime > 0) {
|
if (hasThinkingContent && thinkingStartTime > 0) {
|
||||||
const thinkingTime = Date.now() - thinkingStartTime
|
const thinkingTime = performance.now() - thinkingStartTime
|
||||||
const thinkingCompleteChunk = {
|
const thinkingCompleteChunk = {
|
||||||
type: 'reasoning-signature',
|
type: 'reasoning-signature',
|
||||||
text: accumulatedThinkingContent,
|
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(
|
const ThinkingTimeSeconds = memo(
|
||||||
({ blockThinkingTime, isThinking }: { blockThinkingTime?: number; isThinking: boolean }) => {
|
({ blockThinkingTime, isThinking }: { blockThinkingTime: number; isThinking: boolean }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
// console.log('blockThinkingTime', blockThinkingTime)
|
||||||
const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0)
|
// const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0)
|
||||||
|
|
||||||
// FIXME: 这里统计的和请求处统计的有一定误差
|
// FIXME: 这里统计的和请求处统计的有一定误差
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
let timer: NodeJS.Timeout | null = null
|
// let timer: NodeJS.Timeout | null = null
|
||||||
if (isThinking) {
|
// if (isThinking) {
|
||||||
timer = setInterval(() => {
|
// timer = setInterval(() => {
|
||||||
setThinkingTime((prev) => prev + 100)
|
// setThinkingTime((prev) => prev + 100)
|
||||||
}, 100)
|
// }, 100)
|
||||||
} else if (timer) {
|
// } else if (timer) {
|
||||||
// 立即清除计时器
|
// // 立即清除计时器
|
||||||
clearInterval(timer)
|
// clearInterval(timer)
|
||||||
timer = null
|
// timer = null
|
||||||
}
|
// }
|
||||||
|
|
||||||
return () => {
|
// return () => {
|
||||||
if (timer) {
|
// if (timer) {
|
||||||
clearInterval(timer)
|
// clearInterval(timer)
|
||||||
timer = null
|
// timer = null
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}, [isThinking])
|
// }, [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', {
|
return t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', {
|
||||||
seconds: thinkingTimeSeconds
|
seconds: thinkingTimeSeconds
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export interface MainTextMessageBlock extends BaseMessageBlock {
|
|||||||
export interface ThinkingMessageBlock extends BaseMessageBlock {
|
export interface ThinkingMessageBlock extends BaseMessageBlock {
|
||||||
type: MessageBlockType.THINKING
|
type: MessageBlockType.THINKING
|
||||||
content: string
|
content: string
|
||||||
thinking_millsec?: number
|
thinking_millsec: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 翻译块
|
// 翻译块
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user