ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Cherry Studio 文本翻译主进程化改造:translate-on-main 架构设计与 v2 设计定稿解析

Cherry Studio 文本翻译主进程化改造:translate-on-main 架构设计与 v2 设计定稿解析 Cherry Studio 文本翻译主进程化改造translate-on-main 架构设计与 v2 设计定稿解析【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio导读本文基于 Cherry Studio 仓库中 v2 重构设计文档 translate-on-main.md 展开系统讲解文本翻译功能在 v2 迁移中被整体搬入主进程Main后的最终架构边界、持久化归属与遗留开放问题。读完本文你将掌握translate.open的完整调用链、translateService单例的职责划分、Home 聊天消息的data-translation持久化机制以及 Qwen-MT 目标语言、源语言检测、翻译历史等尚未收敛的设计缺口并能在源码中逐一验证这些结论。一、设计定稿背景本文档的定位translate-on-main.md是 v2 重构阶段的设计成果记录design outcome而非 API 参考手册。文档开头明确声明当前的事实源source of truth是 docs/references/ai/translation.md本文档只负责记录“翻译被迁移到 Main 进程”这一 v2 设计决策的最终形态。理解这一点很重要文中所有结论都以 v2 迁移后的代码为准凡涉及已删除的旧实现如TranslationBackend、useTranslateMessage都是历史遗留不应再作为开发依据。二、最终边界翻译在 Main 进程完成但不构成一次聊天轮次v2 迁移的核心成果是模型选择、提示词构造、provider 流式请求全部移入 Main 进程同时明确翻译不是一次聊天chat turn。文档给出的最终调用链如下renderer translateText - IpcApi translate.open - translateService.open - AiStreamManager.streamPrompt - WebContentsListener - renderer ai.stream.* subscribers对照正式文档 translation.md 中的“Active flow”可以还原出更完整的端到端路径MessageMenuBar - MessageListActions.translateMessage - homeMessageListAdapter.translateMessage |- ChatWrite.editMessage(data-translation) - translateText - ipcApi.request(translate.open, { streamId, text, targetLangCode }) - translateHandlers[translate.open] - translateService.open - AiStreamManager.streamPrompt - WebContentsListener - ai.stream.chunk / done / error关键设计约束翻译没有 assistant、没有消息历史、没有工具、不经过聊天的RequestFeature栈Qwen-MT 模型接收原始文本由模型自行完成语言配对其他模型接收配置好的翻译提示词主进程流式返回的 chunk 通过WebContentsListener以ai.stream.*事件回传给渲染进程。从源码看这一边界在 translateService.ts 的open()方法中得到落实它校验streamId前缀与targetLangCode解析模型与提示词然后直接调用application.get(AiStreamManager).streamPrompt(...)打开流并同步返回streamId。2.1translateService为什么是直接导入单例文档强调translateService保持直接导入的单例direct-import singleton形态。原因是它不拥有任何长生命周期资源也没有持久化副作用路由注册由生命周期托管的IpcApiServiceIpcApi 层负责。这一结论在源码注释中有明确说明translateService.ts按 CLAUDE.md 的生命周期决策指南TranslateService是无状态编排器——从主进程的 Preference/DataApi 解析翻译模型、构造插值提示词、把配置的模型参数按模型能力做门控然后通过WebContentsListener把流交给AiStreamManager.streamPrompt。渲染进程订阅者按translate:*前缀的 streamId 过滤ai.stream.chunk / done / error事件中止则经ai.stream.abort回流。对比同样住在 Main 的 translate.ts 处理器它只负责从ctx.senderId解析调用方的WebContents并委托给translateService.open是典型的“薄处理器 无状态服务”分层。三、IPC 契约translate.open的请求与响应3.1 请求只有三个字段translate.open是**仅 chunkschunks-only**路由渲染进程只发送三个字段契约定义见 translate.ts 模式完整说明见 translation.mdipcApi.request(translate.open, { streamId, text, targetLangCode })字段类型说明streamIdstring渲染进程生成必须以translate:前缀开头。命名空间隔离可防止中止时ai.stream.abort({ topicId: streamId })与真实聊天 topic id 冲突textstring待翻译的源文本targetLangCodeTranslateLangCode目标语言代码必须是具体配置的语言不能是unknown响应同步返回{ streamId }类型定义见 translateService.ts渲染进程据此过滤流事件。3.2 订阅先于调用主进程是同步开流translateText在 translateText.ts 中的实现体现了一个关键时序约束渲染进程必须在调用translate.open之前订阅ai.stream.chunk、ai.stream.done、ai.stream.error因为 Main 在translate.open内部同步启动流第一个 chunk 可能落在open()resolve 之后、任何 await 之后的订阅注册之前。// 订阅在前 unsubscribers.push(ipcApi.on(ai.stream.chunk, ({ topicId, chunk }) { ... })) unsubscribers.push(ipcApi.on(ai.stream.done, ({ topicId }) { ... })) unsubscribers.push(ipcApi.on(ai.stream.error, ({ topicId, error }) { ... })) // 调用在后 ipcApi.request(translate.open, { streamId, text, targetLangCode })...该 helper 同时承担三类职责对应 translation.md 的所有权表生成translate:前缀 UUID 的流 ID先订阅再开流并累计 chunk只累积type text-delta且带字符串delta的块桥接AbortSignal——信号触发时调用ai.stream.abort最终由主进程通过流 error 事件驱动 reject。done时对累计文本做trim()空结果会以translate.error.empty拒绝error时保留error.name例如AbortError以便下游用isAbortError(...)正确分类用户主动停止。3.3 模型参数不跨 IPC由 Main 自行读取translate.open的请求里没有messageId、没有sourceLangCode也没有温度 / top-p / reasoning-effort。这些采样参数统一存放在 Preference 的feature.translate.*命名空间下由Main 进程自己读取feature.translate.model_id— 翻译模型string | null默认nullfeature.translate.model_prompt— 翻译提示词模板默认TRANSLATE_PROMPTfeature.translate.temperature/enable_temperature— 默认1/falsefeature.translate.top_p/enable_top_p— 默认1/falsefeature.translate.reasoning_effort— 默认none默认值见 preferenceSchemas.ts键定义见同文件 L462-L496。这样设计带来两个效果统一设置translate.open的每一个调用方都拿到同一套配置无需各自传参安全边界渲染进程无法请求用户未配置过的取值。文档特别指出保持版式的 PDF 翻译不走此路由——PdfTranslationService通过 API 网关驱动 BabelDoc且不读取任何feature.translate.*配置见 translate.ts 处理器 中独立的translate.pdf.*路由以及 translate.ts 模式 中 PDF 输入输出 schema。四、翻译服务端实现模型解析、提示词构造与参数门控4.1 模型解析feature.translate.model_id与唯一模型 IDresolveTranslatePayloadtranslateService.ts读取feature.translate.model_id必须是合法UniqueModelId否则抛出translate.error.not_configured随后用parseUniqueModelId拆出providerId/modelId通过主进程的modelService.getByKey与providerService.getByProviderId取回模型行。两者任一缺失同样视为未配置。4.2 提示词构造Qwen-MT 走原始文本其余走插值模板提示词构造是翻译与普通聊天最显著的分野若模型命中isQwenMTModel实现见 model.ts规范化后的基础模型名包含qwen-mt则content直接就是原始文本——由 Qwen-MT 模型自行处理语言配对不做提示词插值否则用feature.translate.model_prompt模板做占位符替换const content isQwenMTModel(model) ? text : preferenceService .get(feature.translate.model_prompt) .replaceAll(/{{target_language}}|{{text}}/g, (placeholder) placeholder {{target_language}} ? targetLanguage.value : text )即模板支持{{target_language}}与{{text}}两个占位符分别替换为目标语言名称与源文本。该行为与渲染进程侧的 v1 行为保持一致。4.3 采样参数门控callOverrides是唯一通道resolveRequestParameterstranslateService.ts读取feature.translate.temperature / enable_temperature / top_p / enable_top_p / reasoning_effort并用reasoningKindFor先按模型解析 reasoning 选择omit/off/effort再经getTemperature/getTopP与模型能力做门控最终放进callOverrides。这里有个值得注意的实现细节源码注释有完整阐述translateService.ts采样参数之所以走callOverrides是因为对没有 assistant 的调用方streamPrompt没有其他通道可传参数——该字段本来就是为 API 网关这类调用准备的下游再门控时只会丢弃topKfilterStandardParams因此温度与 top-p 会原样到达协议层翻译必须在这里自行门控这迫使门控发生在管线解析 reasoning之前——与 assistant 设置的做法相反buildAgentParams是在之后门控。共享normalizeRequestedSelectionresolveSelection弥补了模型侧的差距但端点侧仍有缺口此处读取的是模型行物化时投影的词汇表而管线会按请求实际使用的端点重新投影。这正是开放问题 #19693 要解决的把门控移进管线内部使模型与端点同时可知。4.4 流的派发与中止open()创建WebContentsListener(sender, req.streamId)后调用streamManager.streamPrompt({ streamId, uniqueModelId, prompt, listener, reasoningEffort, callOverrides })并记录一条含streamId / uniqueModelId / reasoningEffort / callOverrides的info日志——这是翻译请求实际所带参数的唯一记录。渲染进程的signal.abort通过ai.stream.abort({ topicId })回流主进程经流 error 事件驱动渲染端 reject。五、持久化归属translate.open不落库Home 消息自己写5.1 路由本身零持久化translate.open是chunks-only路由请求只有{ streamId, text, targetLangCode }没有 message target不写消息也不写翻译历史表。也就是说 Main 侧没有任何消息目标无法从这个路由直接落库。文档明确translate.open不写translate_history行。5.2 Home 聊天的data-translation持久化流程Home 聊天通过页面适配器走既有的聊天写边界持久化翻译结果链路为homeMessageListAdapter.translateMessagehomeMessageListAdapter.tsx→ChatWrite.editMessage。具体四步translation.md中止旧翻译homeMessageListAdapter.translateMessage先中止同一条消息上更早的翻译流提交空占位 part写入一个空的data-translationpart让加载 UI 有一个已提交的目标源码中可见part.type data-translation的插入与基于baseParts.filter(part part.type ! data-translation)的替换逻辑见 homeMessageListAdapter.tsx逐块替换每个累计的响应通过ChatWrite.editMessage替换该 part写操作被序列化较慢的写不会覆盖较新的 chunk完成与失败处理完成时等待挂起的写操作收尾失败或中止时若该控制器仍拥有当前翻译则移除加载占位 part。这套机制把消息持久化与所有其他消息编辑归入同一所有者没有 message target 的调用方如 TranslatePage、划词翻译则把返回文本保留在本地。translate.open本身不写translate_history行。5.3 其他调用方本地消费返回文本TranslatePage 与选区翻译等渲染层界面都通过useTranslate到达同一个translateTexthelper区别只是它们在本地消费返回的文本而不是把它挂到聊天消息上。因此translateText的返回值就是完整的译文调用方自行决定含义与是否持久化——这正是文档“渲染进程调用方决定译文含义与是否持久化”所有权划分的落地。六、为什么没有翻译浮层Overlay存储旧路径的删除决策文档用一整节解释了一个容易被误解的设计决策为什么没有引入翻译 Overlay 存储或 Cache 项。旧实现是一条消息绑定路径渲染端挂载useTranslateMessage把流式文本写入TranslationOverlayContext向translate.open传递messageIdMain 侧挂载TranslationBackend负责回写。当 Home 消息菜单迁移到homeMessageListAdapter.translateMessage后useTranslateMessage不再有任何生产调用方于是这条孤立的旧分支被直接移除而不是优化成带键的外部存储或 Cache 键。移除而非保留的理由在于不存在需要协调的独立临时状态所有者——活跃的流状态归 Home 适配器本地所有用户可见的译文是消息业务数据通过ChatWrite写入其他翻译调用方在本地持有返回文本。如果将来要重新引入 Overlay 或 Cache 支撑的存储需要先出现一个具体的生产消费者以及一个现有“调用方所有”流程无法满足的生命周期——目前并不存在这样的需求。七、开放问题Open Questions尚未收敛的设计缺口文档在结尾列出了四个开放问题这些是翻译模块后续演进的重点7.1 Qwen-MT 目标语言未显式下发当前代码对 Qwen-MT 发送原始文本没有通过providerOptions.dashscope.translation_options.target_lang显式覆盖目标语言。这意味着 Qwen-MT 完全依赖模型自身对语言配对的判断Dashscope 侧的目标语言选项未参与。7.2 源语言检测仍在渲染进程useDetectLang仍由渲染进程持有源码位置useDetectLang.ts其中调用了isQwenMTModel判断是否走 Qwen-MT 路径。源语言检测尚未迁移到 Main。7.3 翻译历史未写入文本翻译目前不写translate_history行——这与 PDF 翻译形成对比PDF 翻译路由translate.pdf.start会在运行记录中登记翻译历史并把产物交给 FileManager见 translate.ts 模式 注释。文本翻译的“历史”能力仍是空白。7.4 无 assistant 调用方的参数门控待收敛#19693#9884为翻译提供了独立的 temperature / top-p / reasoning-effort 偏好但它必须先针对模型本身做门控才能放到callOverrides上见 4.3 节的实现细节把这个门控折回请求管线内部#19693仍是待办。这会让门控同时基于模型与端点投影结果消除当前词汇表投影差异带来的缺口。八、验证测试与文档索引本文涉及的全部架构结论都可以在仓库测试中复现关注点测试文件流 ID、chunk 累计、终态事件、错误与中止translateText.test.ts翻译在最终写入落定前保持活跃homeMessageListAdapter.test.tsx模型/提示词解析、参数门控、请求校验、流派发translateService.test.ts发送方managed window解析与处理器委托translate.test.ts若需进一步阅读正式版参考文档 translation.md 是此设计文档的“事实源”对照IPC 契约可查 translate.ts 模式生命周期决策依据可参考 v2 重构文档目录 v2-refactor-temp/docs/ai/ 下的其他设计成果记录。结语translate-on-main是 v2 重构中“能力上移、状态下放”的典型样本模型选择、提示词构造与流式请求全部收敛到 Main 进程同时通过“路由零持久化 调用方各自主导持久化”的边界切分避免了翻译模块膨胀出独立的状态管理层。translateService以无状态单例形态保持轻量Home 消息借道既有ChatWrite边界完成data-translationpart 的序列化更新旧 Overlay 路径则被果断删除。四个开放问题Qwen-MT 目标语言、源语言检测归属、翻译历史、参数门控管线化清晰标出了下一阶段的工作面值得在后续迭代中持续跟踪。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表