dsh-thinking-lang
面向 DeepSeek Harness(dsh-desk) 的插件:设置智能体内部推理(思考)所用的语言。
目标环境: DeepSeek Harness 0.2.0-rc.2+(dsh -V)。License: MIT。
本项目的代码、文案与文档均由 DeepSeek 生成(在人工指导下迭代产出)。它不是 DeepSeek Harness 官方组件,未经官方审核;使用前请自行阅读源码。
它做什么
在设置 → 通用里新增一行「思考语言」,位于宿主自身的语言设置项下方。整行只有一个下拉框:
| 配置项 | 取值 | 作用 |
|---|---|---|
| 思考语言 | 自动 / 简体中文 / 繁體中文 / English | 内部推理与规划所用的语言 |
auto 是怎么判定的
选「自动」时,按顺序取第一个能映射到语言目录的来源:
| 顺序 | 来源 | 取值方式 |
|---|---|---|
| 1 | 操作系统语言 | POSIX 环境变量 LC_ALL / LC_MESSAGES / LANG;Windows 上这些通常为空,改用 Intl.DateTimeFormat().resolvedOptions().locale(跟随系统区域设置) |
| 2 | DSH 界面语言 | 设置服务里 locale 命名空间的 preference 字段 |
| 3 | 兜底 | 硬编码的 zh-CN(FALLBACK_TAG) |
标签先被清洗成 BCP-47(_→-,去掉 .UTF-8、@latin 这类 POSIX 修饰),再交给平台自带的 Intl.Locale 拆成语言/文字/地区三段,不手写标签文法。查找从最具体到最一般:完整标签 → 带文字的 → 带地区的 → 裸主标签,所以 en-GB 会落到 en。
中文是唯一的例外——两个条目共用 zh 这个主标签,决定简繁的是文字而不是地区。这部分交给 CLDR:maximize() 会把短标签省略的文字补出来,Hant 判为繁體、其余判为简体。于是 zh-TW/zh-HK/zh-MO/zh-Hant 都是繁體,zh/zh-SG/zh-Hans 都是简体,两张对照表都不需要。
| 系统 | 界面 | 结果 |
|---|---|---|
zh-CN |
en-US |
简体中文 |
en-US |
zh-CN |
English |
fr-FR(目录里没有) |
en-US |
English(落到下一级) |
| 取不到 | 取不到 | 简体中文 |
在下拉里显式选了某个语言时,那个选择优先于上面全部三级,与系统/界面语言无关。
提示词是怎么注入的
一份提示词被拆成两半,注入两个不同的位置——区别不在内容,而在求值时机。
systemPrompt.section |
systemPrompt.context |
|
|---|---|---|
| 由谁生成 | sectionText |
contextText |
| 内容 | 一行规则,直接写出语言名 | 只有语言名加一句强调 |
| 随什么变化 | 随思考语言变 | 随思考语言变 |
| 求值时机 | 每会话组装一次 | 每次模型调用重新求值 |
| 顺序 | order: 1 |
order: -10 |
实际注入的文本
两段文本都由 src/core.ts 生成。这里是公开文档,不转载注入原文——它们会直接进入模型上下文,属于实现细节,只以「去看代码」的方式给出入口。
systemPrompt.section |
systemPrompt.context |
|
|---|---|---|
| 生成函数 | sectionText |
contextText |
| 形状 | 一行规则,句中嵌入所选语言的英文名 | 一个固定标记,后接所选语言的英文名与一句强调 |
| 长度 | 124–136 字符(每种语言一份) | 53–65 字符 |
| 语言名形式 | 英文名,如 Simplified Chinese / Traditional Chinese / English |
同左 |
要看注入原文,读 src/core.ts 里这两个函数;改完 npm run build 即可生效。
这个拆分带来两个直接结果:
- 切语言在下一轮立即生效 —— context 每一步都重新求值,不必重启或新开会话。
- section 里那个语言名要等新会话 —— 它在会话组装时定稿。同一个会话中途改语言,context 那行会立刻跟着变,但 section 里的名字要等下次组装才跟上。
两段都带语言,是有意的冗余:section 是权威版本(位于 persona 之后、所有工具说明之前),context 只负责实时性。想去掉重复就删掉 context——代价是切语言必须新开会话。
顺序与优先级
- section 的
order: 1紧跟在宿主的 persona 之后、第一个策略段之前——也就是渲染出来的 persona 那句话的下一行。 - context 的
order: -10排在其它每步注入块之前。注意:section 与 context 属于两个不同的序列,两者的 order 数值不可直接比较。
取值依据是 DSH 自己的分段表:
@deepseek-ai/dsh-system-prompt里的SECTION_ORDERS——身份-1000、部署 persona0、PLAN_POLICY500、工具段从1000起。order: 1正好落在 persona 与第一个策略段之间。
Token 开销(实测)
用 DeepSeek V3 的词表实测(@lenml/tokenizer-deepseek_v3,不计特殊 token):
| 语言 | section | context |
|---|---|---|
| 简体中文 | 29 | 16 |
| 繁體中文 | 29 | 16 |
| English | 28 | 15 |
语言名本身只占 1–2 token,所以换语言对每轮开销几乎没有影响。
section 每会话一次,不随轮次增长;context 每轮一次。跑 100 轮:
| 语言 | section | context × 100 | 合计 |
|---|---|---|---|
| 简体 / 繁體 | 29 | 1600 | 1629 |
| English | 28 | 1500 | 1528 |
⚠️ 切换语言的缓存代价
这才是本插件唯一需要留意的开销,量级远大于上面那几十个 token。
DeepSeek 的上下文缓存自动生效、无需改代码,但它只认从第 0 个 token 起完全相同的前缀——官方原话:「只有当两个请求的前缀内容相同时(从第 0 个 token 开始相同),才算重复。中间开始的重复不能被缓存命中。」(API 上线硬盘缓存)
而 section 内嵌了所选语言的名字,位置又相当靠前(order: 1,紧跟 persona)。于是:
改一次思考语言,从 section 那一行往后的前缀全部改变——包括其余 system prompt(工具说明等)和全部对话历史,那一轮统统按「缓存未命中」计费。
差价是 50 倍(deepseek-flash 空闲时段:命中 0.02 元 / 百万 token,未命中 1 元 / 百万 token;高峰时段同样 50 倍。见模型 & 价格)。
context 那行同样随语言变化,只是它 inject 的深度不同、影响面可能小一些;两段中任何一段变了,从它往后的前缀都不再匹配。
好消息是一次性的:切完语言后新前缀会重新被缓存,从下一轮起恢复正常命中。所以代价约等于「重新算一遍完整上下文」这一次。
实用建议:选定一种语言就别频繁来回切,尤其别在一轮对话中间切。切换不影响正确性,只影响钱。
安装
npm install
npm run build
dsh plugin --profile desktop add .
装完重启 GUI。语言约束对新会话生效;切语言本身在已有会话的下一轮就生效。
构建
npm install
npm run check # typecheck + build
构建分两段:
tsc -p tsconfig.json编译宿主端到lib/;- tsdown 把客户端打成 CommonJS 到
client/client.raw.cjs,再由scripts/wrap-client-bundle.mjs套上 DSH 的__ModuleLoader__工厂外壳,输出client/client.js。
选 CommonJS 是为了让产物天生就是 require(...) 形态——DSH 的客户端工厂只提供 require,若先打成 ESM 就还得事后用正则把导入语句改回来。
Node(构建)需要 ^22.18.0 || >=24.11.0;插件运行时不依赖本机 Node。
目录结构
| 路径 | 说明 |
|---|---|
src/core.ts |
语言目录、配置归一化、两份提示词生成。纯函数,无宿主依赖,客户端与宿主端共用。locale 解析走平台自带的 Intl.Locale |
src/index.ts |
宿主入口:Config schema、section 与 context 注册、语言解析(含操作系统语言探测) |
src/client/index.ts |
客户端入口:在 settings.general.item 插槽注册设置行 |
src/client/panel.ts |
单行设置控件与样式 |
src/client/locales.ts |
三套行文案(简 / 繁 / 英) |
cordis.patch.yml |
bundle patch,登记宿主入口 |
PROVENANCE.md |
设置行样式的来源记录,以及从 app.asar 读宿主源码的方法(维护用) |
lib/ · client/ |
构建产物(不入库) |
依赖面
| 类型 | 包 | 说明 |
|---|---|---|
| 运行时 | @deepseek-ai/schemastery |
DSH 的设置 schema 格式,export const Config 必需 |
| 构建期 | typescript · tsdown · @types/node |
只在 npm run build 时用到 |
| 宿主提供 | @deepseek-ai/dsh-client-ui-primitives |
客户端用 tryRequire 按需取用;宿主没有时降级为原生控件 |
不依赖任何 DSH 类型包。 宿主上下文由 src/index.ts 里的本地 HostContext 接口结构化描述,所以包里没有 @deepseek-ai/cordis(既不是 dependency 也不是 peerDependency)。
宿主端对包只暴露四个导出:name、inject、Config、apply——其余全部是模块内私有。
设置行文案的语言(预留功能,默认关闭)
设置行的文案可以跟随思考语言而不是界面语言——选 繁體中文,这一行的标题与说明就变繁体。整套机制已经实现并接线完毕,但当前不生效。
| 开关 | src/client/locales.ts 里的 COPY_FOLLOWS_THINKING_LANGUAGE,默认 false |
| 启用 | 改成 true → npm run build → 重启 GUI |
| 还要改别的文件吗 | 不用。三套字典与选取函数 dictionaryFor 都在 locales.ts 里 |
因为开关是编译期常量,构建时 rolldown 会把这条死分支连同繁体字典一起摇掉——当前产物里没有这些文案,行为与「只跟随界面语言」完全一致。语言选择器的选项始终显示各语言的本族语名(简体中文 / English),所以即使文案换成了你不熟悉的语言,也能从同一个下拉里切回来。
已知限制
- section 的位置绑住宿主的分段表:
order: 1取自@deepseek-ai/dsh-system-prompt的SECTION_ORDERS(persona0、PLAN_POLICY500)。宿主若调整那张表,改src/index.ts的SECTION_ORDER。 - 设置行的位置绑住宿主语言项的 order:宿主语言行注册在
order: 0(@deepseek-ai/dsh-client-locale),本插件用order: 1紧贴其后。宿主若调整,改src/client/index.ts的ROW_ORDER即可。 - 只支持三种思考语言:简体中文、繁體中文、English。要增删语言,改
src/core.ts的CATALOGUE,并在src/client/locales.ts补上对应文案。 - 没有聊天命令:切换语言只走设置页。如需
/thinking-language,可参照dsh-thinking-language的命令实现补上。 auto依赖运行时能报出系统语言:Linux/macOS 走环境变量,Windows 走Intl。两者都拿不到时会静默落到界面语言,再不行才是zh-CN——顺序与取值方式见上文「auto是怎么判定的」。- 与其它语言插件互斥:本插件的插件 id 与设置命名空间都是
thinking-language,与同名的思考语言插件无法共存;包名dsh-thinking-lang与dsh-thinking-language也极为接近,注意区分。
License
MIT,见 LICENSE。
No comments yet. Be the first to write one.