@dingpenghui/dsh-conversation-language 0.0.0-stage → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,175 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # dsh-conversation-language
2
+
3
+ DSH 插件:对话语言切换器
4
+
5
+ ## 功能
6
+
7
+ 允许在中文和英文之间切换对话语言。切换后 AI 将使用对应语言进行思考和回复。
8
+
9
+ ### 特性
10
+
11
+ - 🖱️ 点击插件名即可打开**详情页**(含语言选择表单与保存/恢复默认按钮),通用设置不再出现重复入口
12
+ - 🔄 动态更新 Persona(系统提示),无需重启即可生效
13
+ - 💾 设置通过 profile 的 Cordis patch 持久化(当前 profile 的 `cordis.patch.yml`)
14
+ - 🎨 匹配 DSH 原生界面样式
15
+
16
+ ## 安装
17
+
18
+ > ⚠️ **安装完成后必须重启 DeepSeek Harness**,插件才能生效。
19
+
20
+ ### 方式一:使用 dsh 命令(推荐)
21
+
22
+ ```bash
23
+ # 先 cd 到插件目录
24
+ cd /path/to/dsh-conversation-language
25
+ dsh plugin --profile web add .
26
+ ```
27
+
28
+ ### 方式二:手动安装
29
+
30
+ 1. 克隆插件到 plugins 目录:
31
+ ```bash
32
+ git clone https://github.com/Dingpenghui-good/dsh-conversation-language.git ~/.dsh/plugins/dsh-conversation-language
33
+ # Windows: git clone https://github.com/Dingpenghui-good/dsh-conversation-language.git %USERPROFILE%\.dsh\plugins\dsh-conversation-language
34
+ ```
35
+
36
+ 2. 添加到 `~/.dsh/profiles/web/package.json`:
37
+ ```json
38
+ {
39
+ "dependencies": {
40
+ "@dingpenghui/dsh-conversation-language": "link:C:/Users/<你的用户名>/.dsh/plugins/dsh-conversation-language"
41
+ },
42
+ "dsh": {
43
+ "profile": {
44
+ "bundles": [
45
+ // ... 其他 bundles
46
+ "@dingpenghui/dsh-conversation-language"
47
+ ]
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ 3. 运行安装:
54
+ ```bash
55
+ cd ~/.dsh/profiles/web
56
+ pnpm install
57
+ ```
58
+
59
+ 4. **重启 DeepSeek Harness**。
60
+
61
+ ## 配置
62
+
63
+ ### 方式一:通过插件详情页
64
+
65
+ 重启 DSH 后,打开 **插件 → @dingpenghui/dsh-conversation-language**,在详情页的语言选择表单中切换并保存。
66
+
67
+ ### 方式二:直接修改 profile patch
68
+
69
+ 编辑当前 profile 的 `cordis.patch.yml`(Windows:`%USERPROFILE%\.dsh\profiles\web\cordis.patch.yml`):
70
+
71
+ ```yaml
72
+ - id: tool-conversation-language
73
+ config:
74
+ conversationLanguage: en # 或 zh
75
+ ```
76
+
77
+ 然后重启 DSH。
78
+
79
+ > 2.0.0 起设置不再写入 `~/.dsh/settings.yaml`:新架构中每个插件 entry 的
80
+ > `Config` schema 由 settings 框架投影,写入目标是当前 profile 的 Cordis patch。
81
+
82
+ ## 使用
83
+
84
+ 切换语言后,AI 将自动以对应语言进行思考和回复,并让所有面向用户可见的文本保持同一语言——
85
+ 包括工具调用的参数值(`pwsh`/`bash` 卡片上显示的 `description`、`todo_write` 的 `content`、
86
+ `subagent` 的 `description`、任务与作业名称等)。也可通过 `get_conversation_language` Tool
87
+ 查询当前设置。
88
+
89
+ ## 技术说明
90
+
91
+ | 项目 | 值 |
92
+ |------|-----|
93
+ | Settings Namespace | `tool-conversation-language`(= profile entry id) |
94
+ | Schema | `{ conversationLanguage: 'zh' \| 'en' }`(volatile,默认 `zh`) |
95
+ | 默认值 | `zh` (中文) |
96
+ | Persona Override | `system-prompt/assemble` waterfall 拦截,按语言在 persona section 追加指令 |
97
+ | Tool 注册 | `get_conversation_language` — 查询当前语言设置 |
98
+
99
+ ### 架构(DSH 0.2.0-rc.1+,v3.2.0)
100
+
101
+ - **宿主侧**(`src/index.ts`):声明 `Config` schema(`z.union(['zh','en']).volatile().default('zh')`),
102
+ settings 框架按 entry id 自动注册命名空间;`system-prompt/assemble` 拦截器在每次组装时
103
+ 通过 `ctx.settings.describe()` 读取实时值,语言切换无需重启。`apply` 注入
104
+ `['systemPrompt', 'tools']`——settings 服务缺失时回退默认 `zh`。
105
+ - **客户端侧**(`src/client/`):
106
+ - **详情页**(唯一入口):`configForms.whileServed([NS], …)` 守卫注册
107
+ `plugins.bundle.config`(key = 包名)与 `plugins.row.config`(key = `<包名>#<row-id>`)
108
+ 两个 slot,渲染 `PluginDetailPage`(摘要行 / 语言表单 + 保存 / 恢复默认)。
109
+ 表单走 `SettingsFormModel`(staged form)+ `SettingsForm`,
110
+ 在 0.1.7 表面下这两个 slot 名不存在时 `slots.inject` 静默 no-op,不抛异常。
111
+ - v3.2.0 起移除了通用设置行(`settings.general.item`),语言设置仅在插件详情页提供,
112
+ 避免同一功能在两个入口重复出现。
113
+ - 共享常量(namespace、语言类型)位于 `src/shared.ts`,保证客户端 bundle 不 value-import
114
+ 任何宿主侧包(客户端 bundle purity gate)。
115
+
116
+ ### 插件结构
117
+
118
+ ```
119
+ dsh-conversation-language/
120
+ ├── src/
121
+ │ ├── index.ts # Host 层:Config schema + persona 拦截 + tool
122
+ │ ├── shared.ts # 共享常量(namespace / 语言类型)
123
+ │ ├── client/
124
+ │ │ ├── index.ts # Client 层:UI 注册(configForms + slots)
125
+ │ │ ├── PluginDetailPage.tsx # React 组件(详情页:表单 + 保存/恢复)
126
+ │ │ ├── PluginDetailPage.module.css
127
+ │ │ └── plugin-detail-controller.ts # 详情页 staged-form 控制器(SettingsFormModel 桥接)
128
+ │ └── locales/
129
+ │ └── index.ts # 国际化字典(zh / en)
130
+ ├── lib/ # 构建产物(tsdown 生成,安装时必须存在)
131
+ ├── cordis.patch.yml # Cordis bundle patch(entry id = settings namespace)
132
+ ├── build-and-verify.mjs # 一键构建 + 全量验证流水线
133
+ ├── runtime-verify.mjs # 宿主侧运行时回归
134
+ ├── client-verify-0.1.7.mjs # 0.1.7-alpha.2 回归验证
135
+ ├── contract-test.mjs # 0.2.0-rc.1 客户端契约模拟验证
136
+ ├── package.json
137
+ └── README.md
138
+ ```
139
+
140
+ ## 兼容性
141
+
142
+ | DSH 版本 | 插件版本 | 说明 |
143
+ |----------|---------|------|
144
+ | 0.1.2-rc.1 | ≤ 1.4.0 | 旧的单一 `deployment:persona` section(`PERSONA_SECTION`) |
145
+ | 0.1.5-rc.1 | ≥ 1.5.0 | persona 拆分为 prefix/suffix;宿主侧 settings 采用旧的 `settings.register()` 命名空间模式 |
146
+ | 0.1.7-alpha.2+ | ≥ 2.0.0 | settings 框架重构为 plugin entry `Config` schema + `describe()/update()/mutate()`;客户端 `settingsScope` 服务移除,改用 `configForms` |
147
+ | **0.2.0-rc.1** | **≥ 3.1.0** | 宿主侧改用 `settings` service(`describe()` + `mutate()`);客户端注册 `plugins.bundle.config` / `plugins.row.config` slot 实现详情页;`SettingsFormModel`(staged form)+ `SettingsForm` 渲染保存/恢复默认;0.1.7 表面下 slot 降级静默 no-op |
148
+ | **0.2.0-rc.1** | **≥ 3.2.0** | 移除通用设置行(`settings.general.item`),语言设置仅在插件详情页提供(唯一入口) |
149
+
150
+ ## 开发
151
+
152
+ ```bash
153
+ cd dsh-conversation-language
154
+ pnpm install
155
+ pnpm run build # 构建产物到 lib/;安装前必须执行此步
156
+ ```
157
+
158
+ 升级 DSH 大版本后,可对当前 node_modules 中的 `@deepseek-ai/*` 版本做回归:
159
+
160
+ ```bash
161
+ node runtime-verify.mjs # 宿主侧:tool 注册/执行、system-prompt/assemble 拦截
162
+ node client-verify-0.1.7.mjs # 0.1.7 回归:CJS 包装 / purity gate / slot 降级
163
+ node contract-test.mjs # 0.2.0-rc.1 契约:apply(ctx) 注册行 + whileServed 详情页 + 词典 + 写回
164
+ node build-and-verify.mjs # 一键流水线:tsdown build + tsc + 上述全部验证
165
+ ```
166
+
167
+ > ⚠️ **必须执行构建**,插件运行时依赖 `lib/` 下的构建产物,不能直接使用源码。
168
+ >
169
+ > 请使用 **pnpm** 安装依赖:`@deepseek-ai/dsh-*@0.1.7-alpha.2` 等预发布版本在 npm 注册表中
170
+ > 处于「已发布但不可被 npm 解析」状态,`npm install` 会以 `ETARGET` 失败;pnpm 可以正常
171
+ > 解析并安装这些版本。
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,12 @@
1
+ # 对话语言切换插件配置
2
+ # 安装方式: dsh plugin --profile web add <path-to-plugin>
3
+ #
4
+ # The row id IS the settings namespace: the settings framework keys every
5
+ # descriptor by the entry's id, and the client addresses the form through
6
+ # `configForms.get('tool-conversation-language')`. The schema default
7
+ # (conversationLanguage: 'zh') materializes when the row ships no config.
8
+ - insert:
9
+ - id: tool-conversation-language
10
+ name: '@dingpenghui/dsh-conversation-language'
11
+ config:
12
+ conversationLanguage: 'zh'
package/lib/client.js ADDED
@@ -0,0 +1,252 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dingpenghui/dsh-conversation-language",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/plugin-detail-controller.ts
10
+ /** 语言字段的 format/parse 规范:只接受 'zh' / 'en',空串清除回继承层。 */
11
+ function languageFieldSpec() {
12
+ const accepted = ["zh", "en"];
13
+ return {
14
+ field: "conversationLanguage",
15
+ format: (value) => typeof value === "string" ? value : "",
16
+ parse: (text) => {
17
+ const trimmed = text.trim();
18
+ if (accepted.includes(trimmed)) return {
19
+ kind: "set",
20
+ value: trimmed
21
+ };
22
+ if (trimmed === "") return { kind: "clear" };
23
+ }
24
+ };
25
+ }
26
+ /**
27
+ * 把命名空间的 `ConfigForm` 适配为 `SettingsFormModel` 需要的
28
+ * `SettingsFormScope`:两者读快照形状一致,只有 `mutate` 操作类型不同
29
+ * (`SettingsFormPathOp` vs `SettingsPathOpView`),页面只做读与原子写,
30
+ * 故按结构复用。
31
+ */
32
+ function asFormScope(scope) {
33
+ return {
34
+ getSnapshot: () => {
35
+ const s = scope.getSnapshot();
36
+ return {
37
+ status: s.status,
38
+ value: s.value,
39
+ base: s.base,
40
+ user: s.user,
41
+ writable: s.writable,
42
+ revision: s.revision
43
+ };
44
+ },
45
+ subscribe: (listener) => scope.subscribe(listener),
46
+ mutate: (ops, expectedRevision) => scope.mutate(ops, expectedRevision)
47
+ };
48
+ }
49
+ /** 把一个宿主 settings 命名空间桥接到详情页的 staged form。 */
50
+ var ConversationLanguagePageController = class {
51
+ form;
52
+ store;
53
+ /** @param scope - 共享配置表单(命名空间的 ConfigForm)。 */
54
+ constructor(scope) {
55
+ this.form = new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(asFormScope(scope), [languageFieldSpec()]);
56
+ this.store = this.form.bind(() => this.projection());
57
+ }
58
+ projection() {
59
+ return {
60
+ ...this.form.shell(),
61
+ conversationLanguage: this.form.field("conversationLanguage")
62
+ };
63
+ }
64
+ /**
65
+ * 构建详情页 slot 注册注入的面。
66
+ * @returns 页面快照与表单动作。
67
+ */
68
+ inject() {
69
+ return {
70
+ hooks: { conversationLanguagePage: this.store },
71
+ ...this.form.actions()
72
+ };
73
+ }
74
+ /** 释放表单订阅。 */
75
+ dispose() {
76
+ this.form.dispose();
77
+ }
78
+ };
79
+ //#endregion
80
+ //#region \0dsh-css:E:\dsh-workspace\dsh-conversation-language\src\client\PluginDetailPage.module.css.mjs
81
+ const css = ".hZ1jBa_page{flex-direction:column;gap:16px;padding-bottom:16px;display:flex}.hZ1jBa_field{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:8px;padding:16px 0;display:flex}.hZ1jBa_fieldText{flex-direction:column;gap:4px;display:flex}.hZ1jBa_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.hZ1jBa_hint{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:400;line-height:18px}.hZ1jBa_control{width:100%}";
82
+ const tagId = "@dingpenghui/dsh-conversation-language/PluginDetailPage.module.css";
83
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
84
+ const tag = document.createElement("style");
85
+ tag.dataset.plugin = "@dingpenghui/dsh-conversation-language";
86
+ tag.dataset.pluginCss = tagId;
87
+ tag.textContent = css;
88
+ document.head.appendChild(tag);
89
+ }
90
+ var PluginDetailPage_module_css_default = {
91
+ "title": "hZ1jBa_title",
92
+ "page": "hZ1jBa_page",
93
+ "field": "hZ1jBa_field",
94
+ "fieldText": "hZ1jBa_fieldText",
95
+ "control": "hZ1jBa_control",
96
+ "hint": "hZ1jBa_hint"
97
+ };
98
+ //#endregion
99
+ //#region src/client/plugin-detail-page.tsx
100
+ /** 语言选项(与宿主 schema 的枚举一致)。 */
101
+ const LANGUAGE_OPTIONS = [{
102
+ value: "zh",
103
+ label: "中文"
104
+ }, {
105
+ value: "en",
106
+ label: "English"
107
+ }];
108
+ /** 从 locale 字典派生 SettingsForm 的标签文案。 */
109
+ function formLabels(t) {
110
+ return {
111
+ unavailable: t("form.unavailable"),
112
+ readOnly: t("form.readOnly"),
113
+ saveFailed: t("form.saveFailed"),
114
+ save: t("form.save"),
115
+ saving: t("form.saving")
116
+ };
117
+ }
118
+ /**
119
+ * 渲染详情页:一行摘要(`view: 'summary'`)或语言选择表单(`view: 'page'`)。
120
+ * @param props - 视图、locale 文案、表单快照与动作。
121
+ * @returns 摘要文字,或带保存控件的语言表单。
122
+ */
123
+ function ConversationLanguageDetailPage(props) {
124
+ const { t } = props;
125
+ const state = props.useConversationLanguagePage((snapshot) => snapshot);
126
+ if (props.view === "summary") return t("description");
127
+ const disabled = !state.writable;
128
+ const selectedValue = state.conversationLanguage.text === "en" ? "en" : "zh";
129
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
130
+ labels: formLabels(t),
131
+ state,
132
+ onSave: props.save,
133
+ onDiscard: props.discard,
134
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
135
+ className: PluginDetailPage_module_css_default.page,
136
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
137
+ className: PluginDetailPage_module_css_default.field,
138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
139
+ className: PluginDetailPage_module_css_default.fieldText,
140
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
141
+ className: PluginDetailPage_module_css_default.title,
142
+ children: t("page.label")
143
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
144
+ className: PluginDetailPage_module_css_default.hint,
145
+ children: t("page.hint")
146
+ })]
147
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedControl, {
148
+ id: "conversation-language",
149
+ value: selectedValue,
150
+ options: LANGUAGE_OPTIONS,
151
+ onChange: (next) => {
152
+ props.edit("conversationLanguage", next);
153
+ },
154
+ label: t("page.label"),
155
+ disabled,
156
+ className: PluginDetailPage_module_css_default.control
157
+ })]
158
+ })
159
+ })
160
+ });
161
+ }
162
+ //#endregion
163
+ //#region src/locales/index.ts
164
+ /** Localization dictionaries (detail page only) */
165
+ const zh = {
166
+ "title": "对话语言",
167
+ "description": "选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。",
168
+ "page.label": "对话语言",
169
+ "page.hint": "选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。",
170
+ "form.unavailable": "该插件当前未加载,暂时无法配置。",
171
+ "form.readOnly": "本部署的设置为只读。",
172
+ "form.saveFailed": "保存未被接受,已保留供你修改。",
173
+ "form.save": "保存",
174
+ "form.saving": "保存中…",
175
+ "form.overridden": "已覆盖",
176
+ "form.reset": "恢复默认",
177
+ "form.invalid": "无效值"
178
+ };
179
+ const en = {
180
+ "title": "Conversation Language",
181
+ "description": "Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.",
182
+ "page.label": "Conversation language",
183
+ "page.hint": "Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.",
184
+ "form.unavailable": "This plugin is not loaded, so it cannot be configured right now.",
185
+ "form.readOnly": "This deployment stores settings read-only.",
186
+ "form.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
187
+ "form.save": "Save",
188
+ "form.saving": "Saving…",
189
+ "form.overridden": "Overridden",
190
+ "form.reset": "Reset to default",
191
+ "form.invalid": "Invalid value"
192
+ };
193
+ //#endregion
194
+ //#region src/shared.ts
195
+ /**
196
+ * Shared constants usable by both the Host bundle and the Client bundle.
197
+ * The client reads the settings namespace through this barrel so it never
198
+ * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.
199
+ */
200
+ /** The settings namespace is the profile entry id of this plugin row. */
201
+ const CONVERSATION_LANGUAGE_NAMESPACE = "tool-conversation-language";
202
+ /** npm 包名:`plugins.bundle.config` slot 的 key。 */
203
+ const PLUGIN_PACKAGE_NAME = "@dingpenghui/dsh-conversation-language";
204
+ /** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */
205
+ const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${CONVERSATION_LANGUAGE_NAMESPACE}`;
206
+ //#endregion
207
+ //#region src/client/index.ts
208
+ const DICT_NS = "settings.conversation-language";
209
+ const inject = [
210
+ "slots",
211
+ "locale",
212
+ "configForms",
213
+ "connection"
214
+ ];
215
+ function apply(ctx) {
216
+ const slots = ctx.get("slots");
217
+ const locale = ctx.get("locale");
218
+ ctx.effect(() => locale.register(DICT_NS, {
219
+ zh,
220
+ en
221
+ }), "conversation-language: dictionaries");
222
+ const detailPage = new ConversationLanguagePageController(ctx.get("configForms").get(CONVERSATION_LANGUAGE_NAMESPACE));
223
+ ctx.effect(() => () => {
224
+ detailPage.dispose();
225
+ }, "conversation-language: detail form subscriptions");
226
+ ctx.effect(() => ctx.get("configForms").whileServed([CONVERSATION_LANGUAGE_NAMESPACE], () => {
227
+ const bundleDisposer = slots.inject("plugins.bundle.config", () => slots.register({
228
+ name: "plugins.bundle.config",
229
+ key: PLUGIN_PACKAGE_NAME,
230
+ locale: DICT_NS,
231
+ inject: () => detailPage.inject()
232
+ }, ConversationLanguageDetailPage));
233
+ const rowDisposer = slots.inject("plugins.row.config", () => slots.register({
234
+ name: "plugins.row.config",
235
+ key: PLUGIN_ROW_CONFIG_KEY,
236
+ locale: DICT_NS,
237
+ inject: () => detailPage.inject()
238
+ }, ConversationLanguageDetailPage));
239
+ return () => {
240
+ rowDisposer();
241
+ bundleDisposer();
242
+ };
243
+ }), "conversation-language: detail page");
244
+ }
245
+ //#endregion
246
+ exports.apply = apply;
247
+ exports.inject = inject;
248
+ return module.exports;
249
+ }
250
+ });
251
+
252
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["SettingsFormModel","SettingsForm","css","SegmentedControl","zhDict","enDict"],"sources":["../src/client/plugin-detail-controller.ts","../src/client/plugin-detail-page.tsx","../src/locales/index.ts","../src/shared.ts","../src/client/index.ts"],"sourcesContent":["/**\r\n * 对话语言插件详情页的 staged form:把宿主 settings 命名空间桥接到\r\n * 插件详情页(plugins.bundle.config / plugins.row.config)的保存控件。\r\n * 参照 ui-settings-shell 的 ShellCardController 模式。\r\n */\r\nimport type { SnapshotStore } from '@deepseek-ai/dsh-client-store'\r\nimport {\r\n SettingsFormModel,\r\n type SettingsFieldState,\r\n type SettingsFormActions,\r\n type SettingsFormScope,\r\n type SettingsFormShell,\r\n} from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'\r\nimport type { ConversationLanguage } from '../shared.ts'\r\n\r\n/** 详情页编辑的字段 —— 命名空间 schema 的子集。 */\r\nexport interface ConversationLanguageSettings {\r\n /** 对话语言。 */\r\n conversationLanguage?: ConversationLanguage\r\n}\r\n\r\n/** 详情页渲染的表单状态。 */\r\nexport interface ConversationLanguagePageState extends SettingsFormShell {\r\n conversationLanguage: SettingsFieldState\r\n}\r\n\r\n/** 注册侧注入给详情页组件的面。 */\r\nexport interface ConversationLanguagePageFace extends SettingsFormActions {\r\n hooks: {\r\n /** 页面快照,渲染器绑定为 useConversationLanguagePage。 */\r\n conversationLanguagePage: SnapshotStore<ConversationLanguagePageState>\r\n }\r\n}\r\n\r\n/** 语言字段的 format/parse 规范:只接受 'zh' / 'en',空串清除回继承层。 */\r\nfunction languageFieldSpec() {\r\n const accepted: readonly string[] = ['zh', 'en']\r\n return {\r\n field: 'conversationLanguage',\r\n format: (value: unknown) => (typeof value === 'string' ? value : ''),\r\n parse: (text: string) => {\r\n const trimmed = text.trim()\r\n if (accepted.includes(trimmed)) return { kind: 'set' as const, value: trimmed }\r\n if (trimmed === '') return { kind: 'clear' as const }\r\n return undefined\r\n },\r\n }\r\n}\r\n\r\n/**\r\n * 把命名空间的 `ConfigForm` 适配为 `SettingsFormModel` 需要的\r\n * `SettingsFormScope`:两者读快照形状一致,只有 `mutate` 操作类型不同\r\n * (`SettingsFormPathOp` vs `SettingsPathOpView`),页面只做读与原子写,\r\n * 故按结构复用。\r\n */\r\nfunction asFormScope(\r\n scope: ConfigForm<ConversationLanguageSettings>\r\n): SettingsFormScope<ConversationLanguageSettings> {\r\n return {\r\n getSnapshot: () => {\r\n const s = scope.getSnapshot()\r\n return {\r\n status: s.status,\r\n value: s.value,\r\n base: s.base,\r\n user: s.user,\r\n writable: s.writable,\r\n revision: s.revision,\r\n }\r\n },\r\n subscribe: (listener) => scope.subscribe(listener),\r\n mutate: (ops, expectedRevision) => scope.mutate(ops as never, expectedRevision),\r\n }\r\n}\r\n\r\n/** 把一个宿主 settings 命名空间桥接到详情页的 staged form。 */\r\nexport class ConversationLanguagePageController {\r\n private readonly form: SettingsFormModel<ConversationLanguageSettings>\r\n private readonly store: SnapshotStore<ConversationLanguagePageState>\r\n\r\n /** @param scope - 共享配置表单(命名空间的 ConfigForm)。 */\r\n constructor(scope: ConfigForm<ConversationLanguageSettings>) {\r\n this.form = new SettingsFormModel(asFormScope(scope), [languageFieldSpec()])\r\n this.store = this.form.bind(() => this.projection())\r\n }\r\n\r\n private projection(): ConversationLanguagePageState {\r\n return {\r\n ...this.form.shell(),\r\n conversationLanguage: this.form.field('conversationLanguage'),\r\n }\r\n }\r\n\r\n /**\r\n * 构建详情页 slot 注册注入的面。\r\n * @returns 页面快照与表单动作。\r\n */\r\n inject(): ConversationLanguagePageFace {\r\n return { hooks: { conversationLanguagePage: this.store }, ...this.form.actions() }\r\n }\r\n\r\n /** 释放表单订阅。 */\r\n dispose(): void {\r\n this.form.dispose()\r\n }\r\n}\r\n","/**\r\n * 对话语言插件详情页:点击插件列表中的插件名字后打开的画面。\r\n * 参照 ui-settings-shell 的 ShellCard 模式:一行摘要(`view: 'summary'`)\r\n * 或语言选择表单(`view: 'page'`),表单带保存控件。\r\n */\r\nimport type { PropsLocale, PropsRuntime, InjectFace, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'\r\nimport { SegmentedControl, SettingsForm } from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport type { SegmentedControlOption } from '@deepseek-ai/dsh-client-ui-primitives'\r\nimport type { ConversationLanguagePageFace } from './plugin-detail-controller.ts'\r\nimport type { ConversationLanguage } from '../shared.ts'\r\nimport css from './PluginDetailPage.module.css'\r\n\r\n/** 渲染器为详情页绑定的 props。 */\r\nexport type ConversationLanguageDetailPageProps =\r\n & PropsRuntime<'plugins.bundle.config'>\r\n & PropsLocale<'settings.conversation-language'>\r\n & InjectFace<ConversationLanguagePageFace>\r\n\r\n/** 语言选项(与宿主 schema 的枚举一致)。 */\r\nconst LANGUAGE_OPTIONS: readonly SegmentedControlOption<ConversationLanguage>[] = [\r\n { value: 'zh', label: '中文' },\r\n { value: 'en', label: 'English' },\r\n]\r\n\r\n/** 从 locale 字典派生 SettingsForm 的标签文案。 */\r\nfunction formLabels(t: TranslateNS<'settings.conversation-language'>) {\r\n return {\r\n unavailable: t('form.unavailable'),\r\n readOnly: t('form.readOnly'),\r\n saveFailed: t('form.saveFailed'),\r\n save: t('form.save'),\r\n saving: t('form.saving'),\r\n }\r\n}\r\n\r\n/**\r\n * 渲染详情页:一行摘要(`view: 'summary'`)或语言选择表单(`view: 'page'`)。\r\n * @param props - 视图、locale 文案、表单快照与动作。\r\n * @returns 摘要文字,或带保存控件的语言表单。\r\n */\r\nexport function ConversationLanguageDetailPage(props: ConversationLanguageDetailPageProps) {\r\n const { t } = props\r\n const state = props.useConversationLanguagePage((snapshot) => snapshot)\r\n\r\n if (props.view === 'summary') {\r\n return t('description')\r\n }\r\n\r\n const disabled = !state.writable\r\n const language = state.conversationLanguage\r\n // 文本稿映射到分段控件:'zh'/'en' 显示选中;空稿(清除/默认)回落到 'zh' 显示。\r\n const selectedValue = language.text === 'en' ? 'en' : 'zh'\r\n\r\n return (\r\n <SettingsForm labels={formLabels(t)} state={state} onSave={props.save} onDiscard={props.discard}>\r\n <div className={css.page}>\r\n <div className={css.field}>\r\n <div className={css.fieldText}>\r\n <div className={css.title}>{t('page.label')}</div>\r\n <div className={css.hint}>{t('page.hint')}</div>\r\n </div>\r\n <SegmentedControl\r\n id=\"conversation-language\"\r\n value={selectedValue}\r\n options={LANGUAGE_OPTIONS}\r\n onChange={(next) => {\r\n props.edit('conversationLanguage', next)\r\n }}\r\n label={t('page.label')}\r\n disabled={disabled}\r\n className={css.control}\r\n />\r\n </div>\r\n </div>\r\n </SettingsForm>\r\n )\r\n}\r\n","/** Localization dictionaries (detail page only) */\r\nexport const zh = {\r\n \"title\": \"对话语言\",\r\n \"description\": \"选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。\",\r\n \"page.label\": \"对话语言\",\r\n \"page.hint\": \"选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。\",\r\n \"form.unavailable\": \"该插件当前未加载,暂时无法配置。\",\r\n \"form.readOnly\": \"本部署的设置为只读。\",\r\n \"form.saveFailed\": \"保存未被接受,已保留供你修改。\",\r\n \"form.save\": \"保存\",\r\n \"form.saving\": \"保存中…\",\r\n \"form.overridden\": \"已覆盖\",\r\n \"form.reset\": \"恢复默认\",\r\n \"form.invalid\": \"无效值\",\r\n} as const\r\n\r\nexport const en = {\r\n \"title\": \"Conversation Language\",\r\n \"description\": \"Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.\",\r\n \"page.label\": \"Conversation language\",\r\n \"page.hint\": \"Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.\",\r\n \"form.unavailable\": \"This plugin is not loaded, so it cannot be configured right now.\",\r\n \"form.readOnly\": \"This deployment stores settings read-only.\",\r\n \"form.saveFailed\": \"The deployment did not accept these values; they were left for you to correct.\",\r\n \"form.save\": \"Save\",\r\n \"form.saving\": \"Saving…\",\r\n \"form.overridden\": \"Overridden\",\r\n \"form.reset\": \"Reset to default\",\r\n \"form.invalid\": \"Invalid value\",\r\n} as const\r\n","/**\r\n * Shared constants usable by both the Host bundle and the Client bundle.\r\n * The client reads the settings namespace through this barrel so it never\r\n * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.\r\n */\r\n\r\n/** The settings namespace is the profile entry id of this plugin row. */\r\nexport const CONVERSATION_LANGUAGE_NAMESPACE = 'tool-conversation-language'\r\n\r\n/** 对话语言: 'zh' (中文) 或 'en' (英文), 默认 'zh' */\r\nexport type ConversationLanguage = 'zh' | 'en'\r\n\r\n/** npm 包名:`plugins.bundle.config` slot 的 key。 */\r\nexport const PLUGIN_PACKAGE_NAME = '@dingpenghui/dsh-conversation-language'\r\n\r\n/** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */\r\nexport const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${CONVERSATION_LANGUAGE_NAMESPACE}`\r\n","/**\r\n * Client-side entry for the conversation language switcher plugin.\r\n * Registers a plugin detail page (the screen opened when clicking the plugin's\r\n * name in the Plugins list) through the Plugins page's `plugins.bundle.config`\r\n * and `plugins.row.config` slots.\r\n *\r\n * Architecture (DSH 0.2.0-rc.1): the detail page binds to the\r\n * Host's settings form through `ctx.configForms.get(CONVERSATION_LANGUAGE_NAMESPACE)`\r\n * — the settings framework mirrors the Host descriptor into the browser and\r\n * queues writes against the active profile's Cordis patch. Detail-page\r\n * registration follows the `ui-settings-shell` pattern:\r\n * `ctx.configForms.whileServed([NS], …)` + `ctx.slots.inject(...)`, so a\r\n * deployment that never composed the owner shows no trace of the page.\r\n *\r\n * The language switcher is reachable only from the plugin detail page —\r\n * we no longer register a row into the General Settings section\r\n * (`settings.general.item`); the plugin list page is the single entry point.\r\n */\r\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\r\n// Type-only: pulls the SlotRegistry service merge (ctx.slots).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\r\n// Type-only: the Plugins page slot contract (plugins.bundle.config /\r\n// plugins.row.config); the owner's SlotMap merge, never a runtime import.\r\nimport type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'\r\n// Type-only: pulls the ctx.locale merge.\r\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\r\n// Type-only: the ctx.configForms Context merge and the settings slot types.\r\n// Cross-plugin collaboration goes through the service, never a value import\r\n// (client bundle purity gate).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\r\n\r\nimport { ConversationLanguagePageController } from './plugin-detail-controller.ts'\r\nimport type { ConversationLanguageSettings } from './plugin-detail-controller.ts'\r\nimport { ConversationLanguageDetailPage } from './plugin-detail-page.tsx'\r\nimport { zh as zhDict, en as enDict } from '../locales/index.ts'\r\n// Shared barrel: keeps this bundle free of Host-only package value imports.\r\nimport {\r\n CONVERSATION_LANGUAGE_NAMESPACE,\r\n PLUGIN_PACKAGE_NAME,\r\n PLUGIN_ROW_CONFIG_KEY,\r\n} from '../shared.ts'\r\n\r\nconst DICT_NS = 'settings.conversation-language'\r\n\r\nexport const inject = ['slots', 'locale', 'configForms', 'connection'] as const\r\n\r\nexport function apply(ctx: ClientContext): void {\r\n const slots = ctx.get('slots')!\r\n const locale = ctx.get('locale')!\r\n\r\n // Register locale dictionaries\r\n ctx.effect(() => locale.register(DICT_NS, { zh: zhDict, en: enDict }), 'conversation-language: dictionaries')\r\n\r\n // 详情页:把语言选择表单挂到插件详情页。\r\n // 参照 ui-settings-shell:whileServed 保证部署从未组合宿主侧的\r\n // 命名空间时,页面不留任何痕迹;注册随 served 变化自动增删。\r\n const detailPage = new ConversationLanguagePageController(\r\n ctx.get('configForms')!.get<ConversationLanguageSettings>(CONVERSATION_LANGUAGE_NAMESPACE)\r\n )\r\n ctx.effect(() => () => {\r\n detailPage.dispose()\r\n }, 'conversation-language: detail form subscriptions')\r\n\r\n ctx.effect(() => ctx.get('configForms')!.whileServed([CONVERSATION_LANGUAGE_NAMESPACE], () => {\r\n // Bundle 详情页(点击插件名字打开的画面):描述与行之间的配置区块。\r\n const bundleDisposer = slots.inject('plugins.bundle.config', () => slots.register({\r\n name: 'plugins.bundle.config',\r\n key: PLUGIN_PACKAGE_NAME,\r\n locale: DICT_NS,\r\n inject: () => detailPage.inject(),\r\n }, ConversationLanguageDetailPage))\r\n // 行详情页(行上\"配置\"控件打开的画面):`<package>#<row id>` key。\r\n const rowDisposer = slots.inject('plugins.row.config', () => slots.register({\r\n name: 'plugins.row.config',\r\n key: PLUGIN_ROW_CONFIG_KEY,\r\n locale: DICT_NS,\r\n inject: () => detailPage.inject(),\r\n }, ConversationLanguageDetailPage))\r\n return () => {\r\n rowDisposer()\r\n bundleDisposer()\r\n }\r\n }), 'conversation-language: detail page')\r\n}\r\n"],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/shared.d.ts
4
+ /**
5
+ * Shared constants usable by both the Host bundle and the Client bundle.
6
+ * The client reads the settings namespace through this barrel so it never
7
+ * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.
8
+ */
9
+ /** The settings namespace is the profile entry id of this plugin row. */
10
+ declare const CONVERSATION_LANGUAGE_NAMESPACE = "tool-conversation-language";
11
+ /** 对话语言: 'zh' (中文) 或 'en' (英文), 默认 'zh' */
12
+ type ConversationLanguage = 'zh' | 'en';
13
+ //#endregion
14
+ //#region src/index.d.ts
15
+ /** Plugin config: the language the conversation runs in. */
16
+ interface Config {
17
+ /**
18
+ * Conversation language the model thinks and replies in. Volatile: the
19
+ * settings UI writes it through the active profile's Cordis patch.
20
+ */
21
+ conversationLanguage: 'zh' | 'en';
22
+ }
23
+ /** Runtime schema for the conversation-language row. */
24
+ declare const Config: z<Schemastery.ObjectS<NoInfer<{
25
+ conversationLanguage: z<"zh" | "en", "zh" | "en", "volatile-defined">;
26
+ }>>, Schemastery.ObjectT<NoInfer<{
27
+ conversationLanguage: z<"zh" | "en", "zh" | "en", "volatile-defined">;
28
+ }>>, "plain">;
29
+ declare const name = "conversation-language";
30
+ declare const inject: readonly ["systemPrompt", "tools"];
31
+ /**
32
+ * Register the tool and the `system-prompt/assemble` waterfall interception.
33
+ * The language is read from the settings framework on every assembly so a
34
+ * switch takes effect on the very next model step without a remount.
35
+ * @param ctx - the plugin context carrying the settings, systemPrompt, and tools services.
36
+ */
37
+ declare function apply(ctx: Context): void;
38
+ //#endregion
39
+ export { CONVERSATION_LANGUAGE_NAMESPACE, Config, type ConversationLanguage, apply, inject, name };
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/shared.ts","../src/index.ts"],"mappings":";;;;;;;;;cAOa;;KAGD;;;;UCuBK;;;;;EAKf;;;cAIW,QAAM,EAAA,YAAA,QAAA;;;;;cAyBN;cACA;;;;;;;iBAQG,MAAM,KAAK"}
package/lib/index.js ADDED
@@ -0,0 +1,88 @@
1
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ //#region src/shared.ts
5
+ /**
6
+ * Shared constants usable by both the Host bundle and the Client bundle.
7
+ * The client reads the settings namespace through this barrel so it never
8
+ * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.
9
+ */
10
+ /** The settings namespace is the profile entry id of this plugin row. */
11
+ const CONVERSATION_LANGUAGE_NAMESPACE = "tool-conversation-language";
12
+ //#endregion
13
+ //#region src/index.ts
14
+ /** Runtime schema for the conversation-language row. */
15
+ const Config = z.object({ conversationLanguage: z.union([z.const("zh"), z.const("en")]).volatile().default("zh") });
16
+ const LANGUAGE_SUFFIX_ZH = `
17
+
18
+ 【语言指令】
19
+ 当前对话语言为「中文」。你必须:
20
+ 1. 所有思考过程使用中文
21
+ 2. 所有回复内容使用中文
22
+ 3. 所有面向用户可见的文本都必须使用中文,包括工具调用的参数值:pwsh/bash 的 description、todo_write 的 content、subagent 的 description、任务与作业的名称等,都要用中文书写;不要沿用工具 schema 里的英文示例措辞(例如 "List files in current directory")
23
+ 4. 如果思考中使用了英文,立即纠正回中文`;
24
+ const LANGUAGE_SUFFIX_EN = `
25
+
26
+ [Language Instruction]
27
+ The current conversation language is "English". You MUST:
28
+ 1. Use English for all thinking processes
29
+ 2. Use English for all response content
30
+ 3. Use English for every user-visible string, including tool-call argument values: the description argument of pwsh/bash, todo_write content, subagent descriptions, and job or task titles. Do not reuse the English wording of a tool schema's examples.
31
+ 4. If you accidentally think in another language, immediately correct back to English`;
32
+ const name = "conversation-language";
33
+ const inject = ["systemPrompt", "tools"];
34
+ /**
35
+ * Register the tool and the `system-prompt/assemble` waterfall interception.
36
+ * The language is read from the settings framework on every assembly so a
37
+ * switch takes effect on the very next model step without a remount.
38
+ * @param ctx - the plugin context carrying the settings, systemPrompt, and tools services.
39
+ */
40
+ function apply(ctx) {
41
+ const settings = ctx.get("settings");
42
+ /** Read the live conversation language from the settings framework. */
43
+ const getLanguage = () => {
44
+ if (settings === void 0) return "zh";
45
+ return (settings.describe().find((row) => row.ns === "tool-conversation-language")?.value)?.conversationLanguage ?? "zh";
46
+ };
47
+ ctx.on("system-prompt/assemble", async (assembly, context, next) => {
48
+ const lang = getLanguage();
49
+ const personaSection = assembly.sections.find((s) => s.name === PERSONA_PREFIX_SECTION) ?? assembly.sections.find((s) => s.name === PERSONA_SUFFIX_SECTION);
50
+ if (personaSection) personaSection.text += lang === "en" ? LANGUAGE_SUFFIX_EN : LANGUAGE_SUFFIX_ZH;
51
+ for (const tool of assembly.tools) if (lang === "zh" && !String(tool.description).startsWith("[中文思考]")) tool.description = `[中文思考] ${tool.description}`;
52
+ else if (lang === "en" && !String(tool.description).startsWith("[English Thinking]")) tool.description = `[English Thinking] ${tool.description}`;
53
+ return next();
54
+ });
55
+ ctx.tools.register(defineTool({
56
+ name: "get_conversation_language",
57
+ description: "获取当前对话语言设置",
58
+ parameters: {},
59
+ output: {
60
+ schema: {
61
+ type: "object",
62
+ properties: {
63
+ language: {
64
+ type: "string",
65
+ enum: ["zh", "en"]
66
+ },
67
+ label: { type: "string" }
68
+ },
69
+ additionalProperties: false
70
+ },
71
+ render: (_args, value) => [{
72
+ type: "text",
73
+ text: `当前对话语言:${value.label} (${value.language})`
74
+ }]
75
+ },
76
+ execute: async () => {
77
+ const lang = getLanguage();
78
+ return {
79
+ language: lang,
80
+ label: lang === "zh" ? "中文" : "English"
81
+ };
82
+ }
83
+ }));
84
+ }
85
+ //#endregion
86
+ export { CONVERSATION_LANGUAGE_NAMESPACE, Config, apply, inject, name };
87
+
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/shared.ts","../src/index.ts"],"sourcesContent":["/**\r\n * Shared constants usable by both the Host bundle and the Client bundle.\r\n * The client reads the settings namespace through this barrel so it never\r\n * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.\r\n */\r\n\r\n/** The settings namespace is the profile entry id of this plugin row. */\r\nexport const CONVERSATION_LANGUAGE_NAMESPACE = 'tool-conversation-language'\r\n\r\n/** 对话语言: 'zh' (中文) 或 'en' (英文), 默认 'zh' */\r\nexport type ConversationLanguage = 'zh' | 'en'\r\n\r\n/** npm 包名:`plugins.bundle.config` slot 的 key。 */\r\nexport const PLUGIN_PACKAGE_NAME = '@dingpenghui/dsh-conversation-language'\r\n\r\n/** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */\r\nexport const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${CONVERSATION_LANGUAGE_NAMESPACE}`\r\n","/**\r\n * Conversation Language Switcher Plugin for DeepSeek Harness\r\n *\r\n * Provides a setting in General Settings to switch conversation language\r\n * between Chinese and English. Uses closed-over state + waterfall interception\r\n * to maximize language consistency across all scenarios (simple Q&A, tool calls,\r\n * skill analysis, etc.).\r\n *\r\n * Install: dsh plugin --profile web add <path-to-plugin>\r\n *\r\n * Architecture (DSH 0.1.7-alpha.2+):\r\n * - Host half declares a `Config` schema with a volatile `conversationLanguage`\r\n * field; the settings framework projects it to the browser and persists it\r\n * through the active profile's Cordis patch.\r\n * - The `system-prompt/assemble` waterfall reads the live settings value on\r\n * every assembly, so language switches take effect without a restart.\r\n */\r\n\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport type { AssembleContext, PromptAssembly } from '@deepseek-ai/dsh-system-prompt'\r\nimport { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from '@deepseek-ai/dsh-system-prompt'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { defineTool } from '@deepseek-ai/dsh-tools'\r\n// Ensure module augmentation for systemPrompt service and system-prompt/assemble event is visible\r\nimport type {} from '@deepseek-ai/dsh-system-prompt'\r\n// Pull the SettingsForms Context merge (ctx.settings) into this program.\r\nimport type {} from '@deepseek-ai/dsh-settings'\r\nimport { CONVERSATION_LANGUAGE_NAMESPACE, type ConversationLanguage } from './shared.ts'\r\n\r\nexport { CONVERSATION_LANGUAGE_NAMESPACE }\r\nexport type { ConversationLanguage }\r\n\r\n/** Plugin config: the language the conversation runs in. */\r\nexport interface Config {\r\n /**\r\n * Conversation language the model thinks and replies in. Volatile: the\r\n * settings UI writes it through the active profile's Cordis patch.\r\n */\r\n conversationLanguage: 'zh' | 'en'\r\n}\r\n\r\n/** Runtime schema for the conversation-language row. */\r\nexport const Config = z.object({\r\n conversationLanguage: z.union([z.const('zh'), z.const('en')]).volatile().default('zh'),\r\n})\r\n\r\n// Persona templates — append language instruction to whatever preset persona\r\n// the current session uses. The base persona is detected from the assembly at\r\n// runtime; we only inject the language section.\r\nconst LANGUAGE_SUFFIX_ZH = `\r\n\r\n【语言指令】\r\n当前对话语言为「中文」。你必须:\r\n1. 所有思考过程使用中文\r\n2. 所有回复内容使用中文\r\n3. 所有面向用户可见的文本都必须使用中文,包括工具调用的参数值:pwsh/bash 的 description、todo_write 的 content、subagent 的 description、任务与作业的名称等,都要用中文书写;不要沿用工具 schema 里的英文示例措辞(例如 \"List files in current directory\")\r\n4. 如果思考中使用了英文,立即纠正回中文`\r\n\r\nconst LANGUAGE_SUFFIX_EN = `\r\n\r\n[Language Instruction]\r\nThe current conversation language is \"English\". You MUST:\r\n1. Use English for all thinking processes\r\n2. Use English for all response content\r\n3. Use English for every user-visible string, including tool-call argument values: the description argument of pwsh/bash, todo_write content, subagent descriptions, and job or task titles. Do not reuse the English wording of a tool schema's examples.\r\n4. If you accidentally think in another language, immediately correct back to English`\r\n\r\nexport const name = 'conversation-language'\r\nexport const inject = ['systemPrompt', 'tools'] as const\r\n\r\n/**\r\n * Register the tool and the `system-prompt/assemble` waterfall interception.\r\n * The language is read from the settings framework on every assembly so a\r\n * switch takes effect on the very next model step without a remount.\r\n * @param ctx - the plugin context carrying the settings, systemPrompt, and tools services.\r\n */\r\nexport function apply(ctx: Context): void {\r\n const settings = ctx.get('settings')\r\n\r\n /** Read the live conversation language from the settings framework. */\r\n const getLanguage = (): ConversationLanguage => {\r\n if (settings === undefined) return 'zh'\r\n const descriptor = settings.describe().find(row => row.ns === CONVERSATION_LANGUAGE_NAMESPACE)\r\n const value = descriptor?.value as { conversationLanguage?: ConversationLanguage } | undefined\r\n return value?.conversationLanguage ?? 'zh'\r\n }\r\n\r\n // 使用 system-prompt/assemble waterfall 拦截\r\n // 动态读取当前 preset 的 persona,追加语言部分\r\n ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>) => {\r\n const lang = getLanguage()\r\n\r\n // 查找当前 preset 的 persona section\r\n // DSH 0.1.5+ 将 persona 拆分为 prefix(order 0,preset 影子)与 suffix\r\n // (order 10200)两个 section;优先追加到 prefix(与原 0.1.2 的\r\n // 'deployment:persona' 位置一致),prefix 缺失时回退到 suffix。\r\n const personaSection =\r\n assembly.sections.find(s => s.name === PERSONA_PREFIX_SECTION)\r\n ?? assembly.sections.find(s => s.name === PERSONA_SUFFIX_SECTION)\r\n if (personaSection) {\r\n // 在 persona 末尾追加语言指令\r\n personaSection.text += lang === 'en' ? LANGUAGE_SUFFIX_EN : LANGUAGE_SUFFIX_ZH\r\n }\r\n\r\n // 向 tool schemas 添加语言前缀\r\n for (const tool of assembly.tools) {\r\n if (lang === 'zh' && !String(tool.description).startsWith('[中文思考]')) {\r\n tool.description = `[中文思考] ${tool.description}`\r\n } else if (lang === 'en' && !String(tool.description).startsWith('[English Thinking]')) {\r\n tool.description = `[English Thinking] ${tool.description}`\r\n }\r\n }\r\n\r\n return next()\r\n })\r\n\r\n // Register tool to query the current language\r\n ctx.tools.register(defineTool({\r\n name: 'get_conversation_language',\r\n description: '获取当前对话语言设置',\r\n parameters: {},\r\n output: {\r\n schema: {\r\n type: 'object',\r\n properties: {\r\n language: { type: 'string', enum: ['zh', 'en'] },\r\n label: { type: 'string' },\r\n },\r\n additionalProperties: false,\r\n },\r\n render: (_args, value) => [{\r\n type: 'text',\r\n text: `当前对话语言:${value.label} (${value.language})`,\r\n }],\r\n },\r\n execute: async () => {\r\n const lang = getLanguage()\r\n return {\r\n language: lang,\r\n label: lang === 'zh' ? '中文' : 'English',\r\n }\r\n },\r\n }))\r\n}\r\n"],"mappings":";;;;;;;;;;AAOA,MAAa,kCAAkC;;;;ACmC/C,MAAa,SAAS,EAAE,OAAO,EAC7B,sBAAsB,EAAE,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,QAAQ,IAAI,EACvF,CAAC;AAKD,MAAM,qBAAqB;;;;;;;;AAS3B,MAAM,qBAAqB;;;;;;;;AAS3B,MAAa,OAAO;AACpB,MAAa,SAAS,CAAC,gBAAgB,OAAO;;;;;;;AAQ9C,SAAgB,MAAM,KAAoB;CACxC,MAAM,WAAW,IAAI,IAAI,UAAU;;CAGnC,MAAM,oBAA0C;EAC9C,IAAI,aAAa,KAAA,GAAW,OAAO;EAGnC,QAFmB,SAAS,SAAS,CAAC,CAAC,MAAK,QAAO,IAAI,OAAA,4BAChC,CAAC,EAAE,MAAA,EACZ,wBAAwB;CACxC;CAIA,IAAI,GAAG,0BAA0B,OAAO,UAA0B,SAA0B,SAAwC;EAClI,MAAM,OAAO,YAAY;EAMzB,MAAM,iBACJ,SAAS,SAAS,MAAK,MAAK,EAAE,SAAS,sBAAsB,KAC1D,SAAS,SAAS,MAAK,MAAK,EAAE,SAAS,sBAAsB;EAClE,IAAI,gBAEF,eAAe,QAAQ,SAAS,OAAO,qBAAqB;EAI9D,KAAK,MAAM,QAAQ,SAAS,OAC1B,IAAI,SAAS,QAAQ,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC,WAAW,QAAQ,GAChE,KAAK,cAAc,UAAU,KAAK;OAC7B,IAAI,SAAS,QAAQ,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC,WAAW,oBAAoB,GACnF,KAAK,cAAc,sBAAsB,KAAK;EAIlD,OAAO,KAAK;CACd,CAAC;CAGD,IAAI,MAAM,SAAS,WAAW;EAC5B,MAAM;EACN,aAAa;EACb,YAAY,CAAC;EACb,QAAQ;GACN,QAAQ;IACN,MAAM;IACN,YAAY;KACV,UAAU;MAAE,MAAM;MAAU,MAAM,CAAC,MAAM,IAAI;KAAE;KAC/C,OAAO,EAAE,MAAM,SAAS;IAC1B;IACA,sBAAsB;GACxB;GACA,SAAS,OAAO,UAAU,CAAC;IACzB,MAAM;IACN,MAAM,UAAU,MAAM,MAAM,IAAI,MAAM,SAAS;GACjD,CAAC;EACH;EACA,SAAS,YAAY;GACnB,MAAM,OAAO,YAAY;GACzB,OAAO;IACL,UAAU;IACV,OAAO,SAAS,OAAO,OAAO;GAChC;EACF;CACF,CAAC,CAAC;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
- {
2
- "name": "@dingpenghui/dsh-conversation-language",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@dingpenghui/dsh-conversation-language",
3
+ "version": "3.2.2",
4
+ "description": "DSH plugin for switching conversation language between Chinese and English",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "dsh": {
8
+ "bundle": {
9
+ "patch": "./cordis.patch.yml"
10
+ },
11
+ "client": {
12
+ "inject": [
13
+ "@deepseek-ai/dsh-client-locale",
14
+ "@deepseek-ai/dsh-client-ui-primitives",
15
+ "@deepseek-ai/dsh-client-ui-renderer",
16
+ "@deepseek-ai/dsh-client-ui-settings",
17
+ "@deepseek-ai/dsh-client-ui-slots",
18
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
19
+ "@deepseek-ai/dsh-client-store"
20
+ ],
21
+ "platform": "web"
22
+ }
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Dingpenghui-good/dsh-conversation-language.git"
27
+ },
28
+ "keywords": [
29
+ "dsh",
30
+ "deepseek-harness",
31
+ "plugin",
32
+ "conversation",
33
+ "language",
34
+ "cordis"
35
+ ],
36
+ "author": "Dingpenghui-good",
37
+ "files": [
38
+ "lib",
39
+ "src",
40
+ "package.json",
41
+ "README.md",
42
+ "cordis.patch.yml"
43
+ ],
44
+ "exports": {
45
+ ".": "./lib/index.js",
46
+ "./client": "./lib/client.js",
47
+ "./package.json": "./package.json"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "dev": "tsdown --watch"
52
+ },
53
+ "dependencies": {
54
+ "@deepseek-ai/cordis": "^4.0.2",
55
+ "@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
56
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.1",
57
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
61
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
62
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
63
+ "@deepseek-ai/schemastery": "^3.18.4",
64
+ "@types/react": "^18.3.12",
65
+ "react": "^18.2.0"
66
+ },
67
+ "peerDependencies": {
68
+ "typescript": ">=5.0.0"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "license": "MIT",
74
+ "devDependencies": {
75
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
76
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
77
+ "lightningcss": "^1.29.2",
78
+ "tsdown": "^0.22.14",
79
+ "typescript": "^5.9.3"
80
+ }
81
+ }
@@ -0,0 +1,40 @@
1
+ /* 对话语言插件详情页样式(对齐 ui-settings-shell 的 SettingsForm 布局) */
2
+
3
+ .page {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 16px;
7
+ padding-bottom: 16px;
8
+ }
9
+
10
+ .field {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 8px;
14
+ padding: 16px 0;
15
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
16
+ }
17
+
18
+ .fieldText {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 4px;
22
+ }
23
+
24
+ .title {
25
+ font-size: 14px;
26
+ font-weight: 400;
27
+ line-height: 22px;
28
+ color: var(--dsw-alias-label-primary);
29
+ }
30
+
31
+ .hint {
32
+ font-size: 12px;
33
+ font-weight: 400;
34
+ line-height: 18px;
35
+ color: var(--dsw-alias-label-secondary);
36
+ }
37
+
38
+ .control {
39
+ width: 100%;
40
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Client-side entry for the conversation language switcher plugin.
3
+ * Registers a plugin detail page (the screen opened when clicking the plugin's
4
+ * name in the Plugins list) through the Plugins page's `plugins.bundle.config`
5
+ * and `plugins.row.config` slots.
6
+ *
7
+ * Architecture (DSH 0.2.0-rc.1): the detail page binds to the
8
+ * Host's settings form through `ctx.configForms.get(CONVERSATION_LANGUAGE_NAMESPACE)`
9
+ * — the settings framework mirrors the Host descriptor into the browser and
10
+ * queues writes against the active profile's Cordis patch. Detail-page
11
+ * registration follows the `ui-settings-shell` pattern:
12
+ * `ctx.configForms.whileServed([NS], …)` + `ctx.slots.inject(...)`, so a
13
+ * deployment that never composed the owner shows no trace of the page.
14
+ *
15
+ * The language switcher is reachable only from the plugin detail page —
16
+ * we no longer register a row into the General Settings section
17
+ * (`settings.general.item`); the plugin list page is the single entry point.
18
+ */
19
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
20
+ // Type-only: pulls the SlotRegistry service merge (ctx.slots).
21
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
22
+ // Type-only: the Plugins page slot contract (plugins.bundle.config /
23
+ // plugins.row.config); the owner's SlotMap merge, never a runtime import.
24
+ import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
25
+ // Type-only: pulls the ctx.locale merge.
26
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
27
+ // Type-only: the ctx.configForms Context merge and the settings slot types.
28
+ // Cross-plugin collaboration goes through the service, never a value import
29
+ // (client bundle purity gate).
30
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
31
+
32
+ import { ConversationLanguagePageController } from './plugin-detail-controller.ts'
33
+ import type { ConversationLanguageSettings } from './plugin-detail-controller.ts'
34
+ import { ConversationLanguageDetailPage } from './plugin-detail-page.tsx'
35
+ import { zh as zhDict, en as enDict } from '../locales/index.ts'
36
+ // Shared barrel: keeps this bundle free of Host-only package value imports.
37
+ import {
38
+ CONVERSATION_LANGUAGE_NAMESPACE,
39
+ PLUGIN_PACKAGE_NAME,
40
+ PLUGIN_ROW_CONFIG_KEY,
41
+ } from '../shared.ts'
42
+
43
+ const DICT_NS = 'settings.conversation-language'
44
+
45
+ export const inject = ['slots', 'locale', 'configForms', 'connection'] as const
46
+
47
+ export function apply(ctx: ClientContext): void {
48
+ const slots = ctx.get('slots')!
49
+ const locale = ctx.get('locale')!
50
+
51
+ // Register locale dictionaries
52
+ ctx.effect(() => locale.register(DICT_NS, { zh: zhDict, en: enDict }), 'conversation-language: dictionaries')
53
+
54
+ // 详情页:把语言选择表单挂到插件详情页。
55
+ // 参照 ui-settings-shell:whileServed 保证部署从未组合宿主侧的
56
+ // 命名空间时,页面不留任何痕迹;注册随 served 变化自动增删。
57
+ const detailPage = new ConversationLanguagePageController(
58
+ ctx.get('configForms')!.get<ConversationLanguageSettings>(CONVERSATION_LANGUAGE_NAMESPACE)
59
+ )
60
+ ctx.effect(() => () => {
61
+ detailPage.dispose()
62
+ }, 'conversation-language: detail form subscriptions')
63
+
64
+ ctx.effect(() => ctx.get('configForms')!.whileServed([CONVERSATION_LANGUAGE_NAMESPACE], () => {
65
+ // Bundle 详情页(点击插件名字打开的画面):描述与行之间的配置区块。
66
+ const bundleDisposer = slots.inject('plugins.bundle.config', () => slots.register({
67
+ name: 'plugins.bundle.config',
68
+ key: PLUGIN_PACKAGE_NAME,
69
+ locale: DICT_NS,
70
+ inject: () => detailPage.inject(),
71
+ }, ConversationLanguageDetailPage))
72
+ // 行详情页(行上"配置"控件打开的画面):`<package>#<row id>` key。
73
+ const rowDisposer = slots.inject('plugins.row.config', () => slots.register({
74
+ name: 'plugins.row.config',
75
+ key: PLUGIN_ROW_CONFIG_KEY,
76
+ locale: DICT_NS,
77
+ inject: () => detailPage.inject(),
78
+ }, ConversationLanguageDetailPage))
79
+ return () => {
80
+ rowDisposer()
81
+ bundleDisposer()
82
+ }
83
+ }), 'conversation-language: detail page')
84
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 对话语言插件详情页的 staged form:把宿主 settings 命名空间桥接到
3
+ * 插件详情页(plugins.bundle.config / plugins.row.config)的保存控件。
4
+ * 参照 ui-settings-shell 的 ShellCardController 模式。
5
+ */
6
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
7
+ import {
8
+ SettingsFormModel,
9
+ type SettingsFieldState,
10
+ type SettingsFormActions,
11
+ type SettingsFormScope,
12
+ type SettingsFormShell,
13
+ } from '@deepseek-ai/dsh-client-ui-primitives'
14
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
15
+ import type { ConversationLanguage } from '../shared.ts'
16
+
17
+ /** 详情页编辑的字段 —— 命名空间 schema 的子集。 */
18
+ export interface ConversationLanguageSettings {
19
+ /** 对话语言。 */
20
+ conversationLanguage?: ConversationLanguage
21
+ }
22
+
23
+ /** 详情页渲染的表单状态。 */
24
+ export interface ConversationLanguagePageState extends SettingsFormShell {
25
+ conversationLanguage: SettingsFieldState
26
+ }
27
+
28
+ /** 注册侧注入给详情页组件的面。 */
29
+ export interface ConversationLanguagePageFace extends SettingsFormActions {
30
+ hooks: {
31
+ /** 页面快照,渲染器绑定为 useConversationLanguagePage。 */
32
+ conversationLanguagePage: SnapshotStore<ConversationLanguagePageState>
33
+ }
34
+ }
35
+
36
+ /** 语言字段的 format/parse 规范:只接受 'zh' / 'en',空串清除回继承层。 */
37
+ function languageFieldSpec() {
38
+ const accepted: readonly string[] = ['zh', 'en']
39
+ return {
40
+ field: 'conversationLanguage',
41
+ format: (value: unknown) => (typeof value === 'string' ? value : ''),
42
+ parse: (text: string) => {
43
+ const trimmed = text.trim()
44
+ if (accepted.includes(trimmed)) return { kind: 'set' as const, value: trimmed }
45
+ if (trimmed === '') return { kind: 'clear' as const }
46
+ return undefined
47
+ },
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 把命名空间的 `ConfigForm` 适配为 `SettingsFormModel` 需要的
53
+ * `SettingsFormScope`:两者读快照形状一致,只有 `mutate` 操作类型不同
54
+ * (`SettingsFormPathOp` vs `SettingsPathOpView`),页面只做读与原子写,
55
+ * 故按结构复用。
56
+ */
57
+ function asFormScope(
58
+ scope: ConfigForm<ConversationLanguageSettings>
59
+ ): SettingsFormScope<ConversationLanguageSettings> {
60
+ return {
61
+ getSnapshot: () => {
62
+ const s = scope.getSnapshot()
63
+ return {
64
+ status: s.status,
65
+ value: s.value,
66
+ base: s.base,
67
+ user: s.user,
68
+ writable: s.writable,
69
+ revision: s.revision,
70
+ }
71
+ },
72
+ subscribe: (listener) => scope.subscribe(listener),
73
+ mutate: (ops, expectedRevision) => scope.mutate(ops as never, expectedRevision),
74
+ }
75
+ }
76
+
77
+ /** 把一个宿主 settings 命名空间桥接到详情页的 staged form。 */
78
+ export class ConversationLanguagePageController {
79
+ private readonly form: SettingsFormModel<ConversationLanguageSettings>
80
+ private readonly store: SnapshotStore<ConversationLanguagePageState>
81
+
82
+ /** @param scope - 共享配置表单(命名空间的 ConfigForm)。 */
83
+ constructor(scope: ConfigForm<ConversationLanguageSettings>) {
84
+ this.form = new SettingsFormModel(asFormScope(scope), [languageFieldSpec()])
85
+ this.store = this.form.bind(() => this.projection())
86
+ }
87
+
88
+ private projection(): ConversationLanguagePageState {
89
+ return {
90
+ ...this.form.shell(),
91
+ conversationLanguage: this.form.field('conversationLanguage'),
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 构建详情页 slot 注册注入的面。
97
+ * @returns 页面快照与表单动作。
98
+ */
99
+ inject(): ConversationLanguagePageFace {
100
+ return { hooks: { conversationLanguagePage: this.store }, ...this.form.actions() }
101
+ }
102
+
103
+ /** 释放表单订阅。 */
104
+ dispose(): void {
105
+ this.form.dispose()
106
+ }
107
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 对话语言插件详情页:点击插件列表中的插件名字后打开的画面。
3
+ * 参照 ui-settings-shell 的 ShellCard 模式:一行摘要(`view: 'summary'`)
4
+ * 或语言选择表单(`view: 'page'`),表单带保存控件。
5
+ */
6
+ import type { PropsLocale, PropsRuntime, InjectFace, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
7
+ import { SegmentedControl, SettingsForm } from '@deepseek-ai/dsh-client-ui-primitives'
8
+ import type { SegmentedControlOption } from '@deepseek-ai/dsh-client-ui-primitives'
9
+ import type { ConversationLanguagePageFace } from './plugin-detail-controller.ts'
10
+ import type { ConversationLanguage } from '../shared.ts'
11
+ import css from './PluginDetailPage.module.css'
12
+
13
+ /** 渲染器为详情页绑定的 props。 */
14
+ export type ConversationLanguageDetailPageProps =
15
+ & PropsRuntime<'plugins.bundle.config'>
16
+ & PropsLocale<'settings.conversation-language'>
17
+ & InjectFace<ConversationLanguagePageFace>
18
+
19
+ /** 语言选项(与宿主 schema 的枚举一致)。 */
20
+ const LANGUAGE_OPTIONS: readonly SegmentedControlOption<ConversationLanguage>[] = [
21
+ { value: 'zh', label: '中文' },
22
+ { value: 'en', label: 'English' },
23
+ ]
24
+
25
+ /** 从 locale 字典派生 SettingsForm 的标签文案。 */
26
+ function formLabels(t: TranslateNS<'settings.conversation-language'>) {
27
+ return {
28
+ unavailable: t('form.unavailable'),
29
+ readOnly: t('form.readOnly'),
30
+ saveFailed: t('form.saveFailed'),
31
+ save: t('form.save'),
32
+ saving: t('form.saving'),
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 渲染详情页:一行摘要(`view: 'summary'`)或语言选择表单(`view: 'page'`)。
38
+ * @param props - 视图、locale 文案、表单快照与动作。
39
+ * @returns 摘要文字,或带保存控件的语言表单。
40
+ */
41
+ export function ConversationLanguageDetailPage(props: ConversationLanguageDetailPageProps) {
42
+ const { t } = props
43
+ const state = props.useConversationLanguagePage((snapshot) => snapshot)
44
+
45
+ if (props.view === 'summary') {
46
+ return t('description')
47
+ }
48
+
49
+ const disabled = !state.writable
50
+ const language = state.conversationLanguage
51
+ // 文本稿映射到分段控件:'zh'/'en' 显示选中;空稿(清除/默认)回落到 'zh' 显示。
52
+ const selectedValue = language.text === 'en' ? 'en' : 'zh'
53
+
54
+ return (
55
+ <SettingsForm labels={formLabels(t)} state={state} onSave={props.save} onDiscard={props.discard}>
56
+ <div className={css.page}>
57
+ <div className={css.field}>
58
+ <div className={css.fieldText}>
59
+ <div className={css.title}>{t('page.label')}</div>
60
+ <div className={css.hint}>{t('page.hint')}</div>
61
+ </div>
62
+ <SegmentedControl
63
+ id="conversation-language"
64
+ value={selectedValue}
65
+ options={LANGUAGE_OPTIONS}
66
+ onChange={(next) => {
67
+ props.edit('conversationLanguage', next)
68
+ }}
69
+ label={t('page.label')}
70
+ disabled={disabled}
71
+ className={css.control}
72
+ />
73
+ </div>
74
+ </div>
75
+ </SettingsForm>
76
+ )
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Conversation Language Switcher Plugin for DeepSeek Harness
3
+ *
4
+ * Provides a setting in General Settings to switch conversation language
5
+ * between Chinese and English. Uses closed-over state + waterfall interception
6
+ * to maximize language consistency across all scenarios (simple Q&A, tool calls,
7
+ * skill analysis, etc.).
8
+ *
9
+ * Install: dsh plugin --profile web add <path-to-plugin>
10
+ *
11
+ * Architecture (DSH 0.1.7-alpha.2+):
12
+ * - Host half declares a `Config` schema with a volatile `conversationLanguage`
13
+ * field; the settings framework projects it to the browser and persists it
14
+ * through the active profile's Cordis patch.
15
+ * - The `system-prompt/assemble` waterfall reads the live settings value on
16
+ * every assembly, so language switches take effect without a restart.
17
+ */
18
+
19
+ import type { Context } from '@deepseek-ai/cordis'
20
+ import type { AssembleContext, PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
21
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from '@deepseek-ai/dsh-system-prompt'
22
+ import z from '@deepseek-ai/schemastery'
23
+ import { defineTool } from '@deepseek-ai/dsh-tools'
24
+ // Ensure module augmentation for systemPrompt service and system-prompt/assemble event is visible
25
+ import type {} from '@deepseek-ai/dsh-system-prompt'
26
+ // Pull the SettingsForms Context merge (ctx.settings) into this program.
27
+ import type {} from '@deepseek-ai/dsh-settings'
28
+ import { CONVERSATION_LANGUAGE_NAMESPACE, type ConversationLanguage } from './shared.ts'
29
+
30
+ export { CONVERSATION_LANGUAGE_NAMESPACE }
31
+ export type { ConversationLanguage }
32
+
33
+ /** Plugin config: the language the conversation runs in. */
34
+ export interface Config {
35
+ /**
36
+ * Conversation language the model thinks and replies in. Volatile: the
37
+ * settings UI writes it through the active profile's Cordis patch.
38
+ */
39
+ conversationLanguage: 'zh' | 'en'
40
+ }
41
+
42
+ /** Runtime schema for the conversation-language row. */
43
+ export const Config = z.object({
44
+ conversationLanguage: z.union([z.const('zh'), z.const('en')]).volatile().default('zh'),
45
+ })
46
+
47
+ // Persona templates — append language instruction to whatever preset persona
48
+ // the current session uses. The base persona is detected from the assembly at
49
+ // runtime; we only inject the language section.
50
+ const LANGUAGE_SUFFIX_ZH = `
51
+
52
+ 【语言指令】
53
+ 当前对话语言为「中文」。你必须:
54
+ 1. 所有思考过程使用中文
55
+ 2. 所有回复内容使用中文
56
+ 3. 所有面向用户可见的文本都必须使用中文,包括工具调用的参数值:pwsh/bash 的 description、todo_write 的 content、subagent 的 description、任务与作业的名称等,都要用中文书写;不要沿用工具 schema 里的英文示例措辞(例如 "List files in current directory")
57
+ 4. 如果思考中使用了英文,立即纠正回中文`
58
+
59
+ const LANGUAGE_SUFFIX_EN = `
60
+
61
+ [Language Instruction]
62
+ The current conversation language is "English". You MUST:
63
+ 1. Use English for all thinking processes
64
+ 2. Use English for all response content
65
+ 3. Use English for every user-visible string, including tool-call argument values: the description argument of pwsh/bash, todo_write content, subagent descriptions, and job or task titles. Do not reuse the English wording of a tool schema's examples.
66
+ 4. If you accidentally think in another language, immediately correct back to English`
67
+
68
+ export const name = 'conversation-language'
69
+ export const inject = ['systemPrompt', 'tools'] as const
70
+
71
+ /**
72
+ * Register the tool and the `system-prompt/assemble` waterfall interception.
73
+ * The language is read from the settings framework on every assembly so a
74
+ * switch takes effect on the very next model step without a remount.
75
+ * @param ctx - the plugin context carrying the settings, systemPrompt, and tools services.
76
+ */
77
+ export function apply(ctx: Context): void {
78
+ const settings = ctx.get('settings')
79
+
80
+ /** Read the live conversation language from the settings framework. */
81
+ const getLanguage = (): ConversationLanguage => {
82
+ if (settings === undefined) return 'zh'
83
+ const descriptor = settings.describe().find(row => row.ns === CONVERSATION_LANGUAGE_NAMESPACE)
84
+ const value = descriptor?.value as { conversationLanguage?: ConversationLanguage } | undefined
85
+ return value?.conversationLanguage ?? 'zh'
86
+ }
87
+
88
+ // 使用 system-prompt/assemble waterfall 拦截
89
+ // 动态读取当前 preset 的 persona,追加语言部分
90
+ ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>) => {
91
+ const lang = getLanguage()
92
+
93
+ // 查找当前 preset 的 persona section
94
+ // DSH 0.1.5+ 将 persona 拆分为 prefix(order 0,preset 影子)与 suffix
95
+ // (order 10200)两个 section;优先追加到 prefix(与原 0.1.2 的
96
+ // 'deployment:persona' 位置一致),prefix 缺失时回退到 suffix。
97
+ const personaSection =
98
+ assembly.sections.find(s => s.name === PERSONA_PREFIX_SECTION)
99
+ ?? assembly.sections.find(s => s.name === PERSONA_SUFFIX_SECTION)
100
+ if (personaSection) {
101
+ // 在 persona 末尾追加语言指令
102
+ personaSection.text += lang === 'en' ? LANGUAGE_SUFFIX_EN : LANGUAGE_SUFFIX_ZH
103
+ }
104
+
105
+ // 向 tool schemas 添加语言前缀
106
+ for (const tool of assembly.tools) {
107
+ if (lang === 'zh' && !String(tool.description).startsWith('[中文思考]')) {
108
+ tool.description = `[中文思考] ${tool.description}`
109
+ } else if (lang === 'en' && !String(tool.description).startsWith('[English Thinking]')) {
110
+ tool.description = `[English Thinking] ${tool.description}`
111
+ }
112
+ }
113
+
114
+ return next()
115
+ })
116
+
117
+ // Register tool to query the current language
118
+ ctx.tools.register(defineTool({
119
+ name: 'get_conversation_language',
120
+ description: '获取当前对话语言设置',
121
+ parameters: {},
122
+ output: {
123
+ schema: {
124
+ type: 'object',
125
+ properties: {
126
+ language: { type: 'string', enum: ['zh', 'en'] },
127
+ label: { type: 'string' },
128
+ },
129
+ additionalProperties: false,
130
+ },
131
+ render: (_args, value) => [{
132
+ type: 'text',
133
+ text: `当前对话语言:${value.label} (${value.language})`,
134
+ }],
135
+ },
136
+ execute: async () => {
137
+ const lang = getLanguage()
138
+ return {
139
+ language: lang,
140
+ label: lang === 'zh' ? '中文' : 'English',
141
+ }
142
+ },
143
+ }))
144
+ }
@@ -0,0 +1,30 @@
1
+ /** Localization dictionaries (detail page only) */
2
+ export const zh = {
3
+ "title": "对话语言",
4
+ "description": "选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。",
5
+ "page.label": "对话语言",
6
+ "page.hint": "选择 AI 思考与回复使用的语言,保存后下一轮模型步骤即时生效。",
7
+ "form.unavailable": "该插件当前未加载,暂时无法配置。",
8
+ "form.readOnly": "本部署的设置为只读。",
9
+ "form.saveFailed": "保存未被接受,已保留供你修改。",
10
+ "form.save": "保存",
11
+ "form.saving": "保存中…",
12
+ "form.overridden": "已覆盖",
13
+ "form.reset": "恢复默认",
14
+ "form.invalid": "无效值",
15
+ } as const
16
+
17
+ export const en = {
18
+ "title": "Conversation Language",
19
+ "description": "Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.",
20
+ "page.label": "Conversation language",
21
+ "page.hint": "Choose the language the AI thinks and replies in. Takes effect on the next model step after saving.",
22
+ "form.unavailable": "This plugin is not loaded, so it cannot be configured right now.",
23
+ "form.readOnly": "This deployment stores settings read-only.",
24
+ "form.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
25
+ "form.save": "Save",
26
+ "form.saving": "Saving…",
27
+ "form.overridden": "Overridden",
28
+ "form.reset": "Reset to default",
29
+ "form.invalid": "Invalid value",
30
+ } as const
package/src/shared.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared constants usable by both the Host bundle and the Client bundle.
3
+ * The client reads the settings namespace through this barrel so it never
4
+ * value-imports a Host-only package such as `@deepseek-ai/dsh-system-prompt`.
5
+ */
6
+
7
+ /** The settings namespace is the profile entry id of this plugin row. */
8
+ export const CONVERSATION_LANGUAGE_NAMESPACE = 'tool-conversation-language'
9
+
10
+ /** 对话语言: 'zh' (中文) 或 'en' (英文), 默认 'zh' */
11
+ export type ConversationLanguage = 'zh' | 'en'
12
+
13
+ /** npm 包名:`plugins.bundle.config` slot 的 key。 */
14
+ export const PLUGIN_PACKAGE_NAME = '@dingpenghui/dsh-conversation-language'
15
+
16
+ /** `plugins.row.config` slot 的 key:`<package>#<row id>`。 */
17
+ export const PLUGIN_ROW_CONFIG_KEY = `${PLUGIN_PACKAGE_NAME}#${CONVERSATION_LANGUAGE_NAMESPACE}`
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Declaration-merge the `settings.conversation-language` locale namespace into
3
+ * the slots `LocaleNamespaceMap`, so `locale: DICT_NS` is accepted and the
4
+ * framework-injected `t` seat carries the typed dictionary-key domain.
5
+ *
6
+ * The value is the union of this namespace's dictionary keys (0.2.0-rc.1
7
+ * shape — not an object, a string-literal union). It must exactly match the
8
+ * keys present in `zh`/`en` in `src/locales/index.ts`; `LocaleDictOf` checks
9
+ * a typed registration's dictionary against it.
10
+ * `export {}` makes this a module file so the `declare module` block is a
11
+ * module augmentation (merges with the resolved slots module) rather than an
12
+ * ambient module declaration.
13
+ */
14
+ export {}
15
+
16
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
+ interface LocaleNamespaceMap {
18
+ 'settings.conversation-language':
19
+ | 'title'
20
+ | 'description'
21
+ | 'page.label'
22
+ | 'page.hint'
23
+ | 'form.unavailable'
24
+ | 'form.readOnly'
25
+ | 'form.saveFailed'
26
+ | 'form.save'
27
+ | 'form.saving'
28
+ | 'form.overridden'
29
+ | 'form.reset'
30
+ | 'form.invalid'
31
+ }
32
+ }
package/src/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Language settings type definition
3
+ */
4
+ export interface ConversationLanguageSettings {
5
+ /** 对话语言: 'zh' (中文) 或 'en' (英文), 默认 'zh' */
6
+ conversationLanguage?: 'zh' | 'en'
7
+ }
8
+
9
+ /** The settings namespace is the profile entry id of the plugin row. */
10
+ export const LANGUAGE_SETTINGS_NAMESPACE = 'tool-conversation-language' as const
11
+
12
+ /** Default language */
13
+ export const DEFAULT_LANGUAGE: 'zh' | 'en' = 'zh'
@@ -0,0 +1,6 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.module.css' {
4
+ const classes: Readonly<Record<string, string>>
5
+ export default classes
6
+ }