@duke-dsh-plugins/dsh-model-prompt-injector 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 duke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # dsh-model-prompt-injector
2
+
3
+ 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)中**本地已配置的模型**追加系统提示词的插件:按 `provider/model` 精确匹配(或 `provider/*` 服务商级通配)把规则文本注入到**系统提示词末尾**,在设置面板中逐模型管理,规则**持久化保存、重启不丢**。
4
+
5
+ ## 功能
6
+
7
+ - **按模型注入**:每次向模型发起请求时,命中规则的提示词追加到系统提示词末尾(内置段落排序之后,order 9950),对主会话、子代理(subagent / workflow)同样生效。
8
+ - **两级规则**:`provider/*` 服务商级规则对该服务商所有模型生效;`provider/model` 模型级规则只对该模型生效;两者同时存在时按「服务商级 → 模型级」顺序叠加。
9
+ - **设置页管理**:设置 → 「模型提示词」页自动列出本地已配置的服务商与模型(与「模型」设置页同一数据源),逐行添加/编辑/清除,已注入的条目带徽标与内容预览。
10
+ - **持久化**:规则保存在 `<DSH_HOME>/model-prompt-injector/config.json`(默认 `~/.dsh/model-prompt-injector/config.json`),重启 DSH 后自动恢复。
11
+ - **即时生效**:保存规则后,下一次模型请求即携带新提示词,无需重启。
12
+
13
+ ## 安装(标准 DSH bundle)
14
+
15
+ ```bash
16
+ dsh plugin --profile web add @duke-dsh-plugins/dsh-model-prompt-injector --registry=https://registry.npmjs.org
17
+ ```
18
+
19
+ > 显式指定 npmjs registry,避免被本地/镜像 registry 配置带偏。安装后**重启 DSH**(Host 加载、typert 注册、client bundle 注入都在启动时发生),设置面板出现「模型提示词」页。
20
+
21
+ 卸载:
22
+
23
+ ```bash
24
+ dsh plugin --profile web remove @duke-dsh-plugins/dsh-model-prompt-injector
25
+ ```
26
+
27
+ ## 使用
28
+
29
+ 1. 打开 **设置 → 模型提示词**(在「模型」页之后)。
30
+ 2. 页面列出本地已配置的服务商卡片;每张卡片第一行是「所有模型(服务商级)」,其下是该服务商配置中的每个模型。
31
+ 3. 点击目标行的「添加提示词 / 编辑」,在文本框中编写要追加的系统提示词,**保存**即时生效;「清除规则」删除该条。
32
+ 4. 规则匹配是**路由级精确匹配**(`provider/model`,大小写敏感,如 `minimax-cn/MiniMax-M3`);模型 id 以「模型」设置页中配置的值为准。
33
+
34
+ ## 规则示例
35
+
36
+ | 规则 key | 作用范围 |
37
+ | --- | --- |
38
+ | `deepseek-official/*` | DeepSeek 官方路由的所有模型 |
39
+ | `minimax-cn/MiniMax-M3` | 仅 MiniMax-M3 |
40
+ | `zai-coding-cn/glm-5.3` | 仅 GLM-5.3 |
41
+
42
+ 典型用法:给弱一些的模型追加一段严谨的工作协议(先验证再声称、最小改动、契约对齐……)以提升输出质量;给特定模型追加项目约定或输出格式要求。
43
+
44
+ ## 实现要点
45
+
46
+ - 注入基于 `systemPrompt` 服务的**动态段落**:段落文本是每次模型 step 组装时求值的函数,运行时组装上下文携带 `agent`,从 `agent.options.provider/model` 取到本次请求的目标路由做规则匹配;未命中返回空串,渲染器自动丢弃空段落(零开销)。
47
+ - 规则读写走插件自有的 Typert Remote 服务(`modelPromptInjector.getState / setRule`),Client 经 `ctx.remote.$mount` 自挂载命名空间后调用。
48
+
49
+ ## 开发
50
+
51
+ ```bash
52
+ npm run check # node --check index.js client.js typert.host.js
53
+ ```
54
+
55
+ 本地调试安装(link:,插件目录需已有 node_modules,见 AGENTS.md):
56
+
57
+ ```bash
58
+ npm install --no-save --registry=https://registry.npmjs.org @deepseek-ai/cordis@4.0.2 @deepseek-ai/dsh-typert-protocol@0.1.2-rc.1 zod@4.5.4
59
+ dsh plugin --profile web add /path/to/dsh-model-prompt-injector
60
+ # 重启 DSH 生效
61
+ ```
62
+
63
+ ## 发布
64
+
65
+ 打 `v*` 标签推送 GitHub,`.github/workflows/release.yml` 自动构建 tgz、发布 GitHub Release,并经 npm OIDC Trusted Publishing 发布到 npmjs(需在 npmjs.com 包设置中先配置 trusted publisher:本仓库 + `.github/workflows/release.yml`)。
66
+
67
+ ## License
68
+
69
+ MIT © duke
package/client.js ADDED
@@ -0,0 +1,414 @@
1
+ /**
2
+ * dsh-model-prompt-injector — Client half (web bundle).
3
+ *
4
+ * Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Adds a
5
+ * "模型提示词" page to the Settings panel (`settings.section`, right after
6
+ * 模型): every locally configured provider card lists its models, each row
7
+ * edits that route's appended system-prompt rule, and a provider-wide
8
+ * `provider/*` row covers the whole provider.
9
+ *
10
+ * Host communication goes through the `modelPromptInjector` Remote namespace
11
+ * (`ctx.remote.modelPromptInjector.*`), published by the Host half in
12
+ * `index.js` and mounted below via `ctx.remote.$mount`.
13
+ *
14
+ * Style notes: no `?.` / `??` (conservative bundle syntax, same as
15
+ * dsh-agent-approval / dsh-token-stats); buttons are the official Button atom
16
+ * (`@deepseek-ai/dsh-client-ui-primitives`, backed by the
17
+ * `--dsw-alias-button-*` token family) so light/dark themes are automatic.
18
+ */
19
+ window.__ModuleLoader__.load({
20
+ id: "@duke-dsh-plugins/dsh-model-prompt-injector",
21
+ factory: (require) => {
22
+ var module = { exports: {} };
23
+ var exports = module.exports;
24
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
+ const React = require("react");
26
+ const ui = require("@deepseek-ai/dsh-client-ui-primitives");
27
+
28
+ // ---- CSS (package-owned, DSH design tokens) ------------------------------
29
+ const CSS = `
30
+ .mpi-page{display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-size:13px;max-width:860px}
31
+ .mpi-intro{margin:0;color:var(--dsw-alias-label-secondary);line-height:1.7}
32
+ .mpi-provider{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-1);display:flex;flex-direction:column;gap:6px}
33
+ .mpi-provider-head{display:flex;align-items:baseline;gap:8px;padding-bottom:4px}
34
+ .mpi-provider-name{font-weight:600;font-size:14px}
35
+ .mpi-id{color:var(--dsw-alias-label-secondary);font-family:monospace;font-size:12px}
36
+ .mpi-state{margin-left:auto;font-size:11px}
37
+ .mpi-state-on{color:var(--dsw-alias-state-success-primary)}
38
+ .mpi-state-off{color:var(--dsw-alias-label-secondary)}
39
+ .mpi-empty{margin:4px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px}
40
+ .mpi-error{margin:4px 0 0;color:var(--dsw-alias-state-error-primary);font-size:12px}
41
+ .mpi-headrow{display:flex;align-items:center;justify-content:space-between;gap:8px}
42
+ .mpi-row{border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;padding-bottom:2px}
43
+ .mpi-row-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
44
+ .mpi-row-title{display:flex;align-items:baseline;gap:8px;min-width:0;flex-wrap:wrap}
45
+ .mpi-badge{font-size:11px;padding:1px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}
46
+ .mpi-preview{margin:6px 0 0;font-size:12px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
47
+ .mpi-editor{margin-top:8px;display:flex;flex-direction:column;gap:8px}
48
+ .mpi-textarea{font:inherit;font-size:13px;line-height:1.6;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;resize:vertical;min-height:96px}
49
+ .mpi-textarea:focus{outline:none;border-color:var(--dsw-alias-border-l3)}
50
+ .mpi-actions{display:flex;gap:8px;align-items:center}
51
+
52
+ /* Settings nav icon: DSH 0.1.x settings.section only projects id/order/
53
+ label, and the settings shell paints a generic gear for every external
54
+ section (client-ui-settings-general's navIcon()). registerSettingsNavIcon
55
+ marks our own nav row; hide the shell's gear and draw the
56
+ message-square-plus Lucide glyph as a currentColor mask so it follows the
57
+ native nav hover/active colors without changing the shell's icon rhythm. */
58
+ [data-dsh-model-prompt-injector-settings-nav]>svg:first-child{display:none}
59
+ [data-dsh-model-prompt-injector-settings-nav]::before{content:'';flex:none;width:16px;height:16px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'/%3E%3Cpath d='M12 7v6'/%3E%3Cpath d='M9 10h6'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'/%3E%3Cpath d='M12 7v6'/%3E%3Cpath d='M9 10h6'/%3E%3C/svg%3E") center/contain no-repeat}
60
+ `;
61
+
62
+ // ---- Settings nav icon ----------------------------------------------------
63
+ // DSH 0.1.x does not yet carry an icon through the settings.section
64
+ // registration contract: its shell projects only id/order/label and paints
65
+ // a generic gear for every external section. Mark only this plugin's
66
+ // localized nav row so the CSS above can replace the fallback gear; the
67
+ // disposer clears the marker for HMR / plugin disable.
68
+ const SETTINGS_LABEL = "模型提示词";
69
+ const SETTINGS_NAV_MARKER = "data-dsh-model-prompt-injector-settings-nav";
70
+
71
+ function registerSettingsNavIcon(label) {
72
+ let disposed = false;
73
+ const sync = function () {
74
+ if (disposed) return;
75
+ const currentLabel = String(label).trim();
76
+ const buttons = document.querySelectorAll('[role="dialog"] nav button');
77
+ for (let i = 0; i < buttons.length; i++) {
78
+ const button = buttons[i];
79
+ const text = button.textContent ? button.textContent.trim() : "";
80
+ if (currentLabel.length > 0 && text === currentLabel) {
81
+ button.setAttribute(SETTINGS_NAV_MARKER, "");
82
+ } else {
83
+ button.removeAttribute(SETTINGS_NAV_MARKER);
84
+ }
85
+ }
86
+ };
87
+ sync();
88
+ const observer = new MutationObserver(sync);
89
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
90
+ return function () {
91
+ disposed = true;
92
+ observer.disconnect();
93
+ const marked = document.querySelectorAll("[" + SETTINGS_NAV_MARKER + "]");
94
+ for (let i = 0; i < marked.length; i++) marked[i].removeAttribute(SETTINGS_NAV_MARKER);
95
+ };
96
+ }
97
+
98
+ // ---- Client Remote contribution -------------------------------------------
99
+ // The browser-side `remote.modelPromptInjector` service only exists after
100
+ // this module mounts its namespace via ctx.remote.$mount(): dsh-api-remotes'
101
+ // client assembly mounts only the official namespaces, so a plugin must
102
+ // mount its own. Mirrors the invocations in typert.host.js. zod is not
103
+ // requirable in the browser module loader, so codecs use passthrough
104
+ // schemas — the runtime contract only requires typeSymbol + schema.parse().
105
+ const passthrough = () => ({ parse: (v) => v });
106
+ const result = (typeSymbol) => ({ mode: "strict", typeSymbol, schema: passthrough() });
107
+ const CLIENT_REMOTE = {
108
+ package: "dsh-model-prompt-injector",
109
+ descriptors: [
110
+ {
111
+ id: "dsh-model-prompt-injector#modelPromptInjector/getState",
112
+ service: "modelPromptInjector",
113
+ namespace: "modelPromptInjector",
114
+ method: "getState",
115
+ invocation: { kind: "direct" },
116
+ parameters: [],
117
+ result: result("dsh-model-prompt-injector#ModelPromptInjectorStateResult"),
118
+ },
119
+ {
120
+ id: "dsh-model-prompt-injector#modelPromptInjector/setRule",
121
+ service: "modelPromptInjector",
122
+ namespace: "modelPromptInjector",
123
+ method: "setRule",
124
+ invocation: { kind: "direct" },
125
+ parameters: [
126
+ {
127
+ name: "request",
128
+ wire: "request",
129
+ source: "json",
130
+ codec: {
131
+ mode: "strict",
132
+ typeSymbol: "dsh-model-prompt-injector#ModelPromptInjectorSetRuleRequest",
133
+ schema: passthrough(),
134
+ },
135
+ },
136
+ ],
137
+ result: result("dsh-model-prompt-injector#ModelPromptInjectorSetRuleResult"),
138
+ },
139
+ ],
140
+ };
141
+
142
+ async function apply(ctx) {
143
+ // Mount the modelPromptInjector namespace before anything touches it;
144
+ // the mount's lifetime is bound to this plugin's context by $mount.
145
+ await ctx.remote.$mount(CLIENT_REMOTE);
146
+
147
+ const styleTag = document.createElement("style");
148
+ styleTag.textContent = CSS;
149
+ document.head.appendChild(styleTag);
150
+ ctx.effect(() => () => styleTag.remove());
151
+
152
+ // Mark our settings-nav row so the CSS above replaces the shell's
153
+ // fallback gear (no icon field exists in settings.section yet).
154
+ ctx.effect(() => registerSettingsNavIcon(SETTINGS_LABEL));
155
+
156
+ // ctx.get() reads the service without the property-accessor inject guard.
157
+ const remote = ctx.get("remote.modelPromptInjector");
158
+
159
+ // ---- helpers ------------------------------------------------------------
160
+
161
+ /**
162
+ * The Remote gateway returns `res.value` = the Host method's full
163
+ * `{ ok, value }` envelope; unwrap it (tolerate both shapes) and surface
164
+ * either error layer.
165
+ */
166
+ function pick(res) {
167
+ if (res && res.ok === false) {
168
+ const err = res.error || {};
169
+ throw new Error(err.message || err.code || "request failed");
170
+ }
171
+ const v = res && res.value;
172
+ if (v && typeof v === "object" && v.ok === false) {
173
+ const err = v.error || {};
174
+ throw new Error(err.message || err.code || "request failed");
175
+ }
176
+ if (v && typeof v === "object" && v.ok === true) return v.value;
177
+ return v;
178
+ }
179
+
180
+ const h = React.createElement;
181
+
182
+ /** Map rule key -> rule for O(1) row lookups. */
183
+ function rulesByKey(rules) {
184
+ const map = {};
185
+ for (let i = 0; i < rules.length; i++) {
186
+ const rule = rules[i];
187
+ if (rule && typeof rule.key === "string") map[rule.key] = rule;
188
+ }
189
+ return map;
190
+ }
191
+
192
+ // ---- rule editor (one row's textarea + actions) -------------------------
193
+
194
+ function RuleEditor(props) {
195
+ const [draft, setDraft] = React.useState(props.initial);
196
+ const [busy, setBusy] = React.useState(false);
197
+ const [error, setError] = React.useState("");
198
+ const save = function (prompt) {
199
+ setBusy(true);
200
+ setError("");
201
+ remote
202
+ .setRule({ provider: props.provider, model: props.model, prompt })
203
+ .then((res) => {
204
+ setBusy(false);
205
+ const value = pick(res) || {};
206
+ props.onSaved(Array.isArray(value.rules) ? value.rules : []);
207
+ })
208
+ .catch((e) => {
209
+ setBusy(false);
210
+ setError(e && e.message ? e.message : String(e));
211
+ });
212
+ };
213
+ return h(
214
+ "div",
215
+ { className: "mpi-editor" },
216
+ h("textarea", {
217
+ className: "mpi-textarea",
218
+ value: draft,
219
+ placeholder: "输入要追加到系统提示词末尾的内容…",
220
+ onChange: (event) => setDraft(event.target.value),
221
+ }),
222
+ error ? h("p", { className: "mpi-error" }, error) : null,
223
+ h(
224
+ "div",
225
+ { className: "mpi-actions" },
226
+ h(ui.Button, { variant: "primary", size: "sm", disabled: busy, onClick: () => save(draft) }, "保存"),
227
+ props.initial.length > 0
228
+ ? h(ui.Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => save("") }, "清除规则")
229
+ : null,
230
+ h(ui.Button, { variant: "ghost", size: "sm", disabled: busy, onClick: props.onCancel }, "取消"),
231
+ ),
232
+ );
233
+ }
234
+
235
+ // ---- one rule row (provider-wide or one model) --------------------------
236
+
237
+ function RuleRow(props) {
238
+ const rule = props.rule;
239
+ const has = !!(rule && typeof rule.prompt === "string" && rule.prompt.length > 0);
240
+ return h(
241
+ "div",
242
+ { className: "mpi-row" },
243
+ h(
244
+ "div",
245
+ { className: "mpi-row-head" },
246
+ h(
247
+ "div",
248
+ { className: "mpi-row-title" },
249
+ h("span", null, props.title),
250
+ h("span", { className: "mpi-id" }, props.subtitle),
251
+ has ? h("span", { className: "mpi-badge" }, "已注入") : null,
252
+ ),
253
+ h(
254
+ ui.Button,
255
+ { variant: "ghost", size: "sm", onClick: () => props.onToggle(props.rowKey) },
256
+ props.open ? "收起" : has ? "编辑" : "添加提示词",
257
+ ),
258
+ ),
259
+ has && !props.open ? h("div", { className: "mpi-preview" }, rule.prompt) : null,
260
+ props.open
261
+ ? h(RuleEditor, {
262
+ provider: props.provider,
263
+ model: props.model,
264
+ initial: has ? rule.prompt : "",
265
+ onSaved: (rules) => {
266
+ props.onSaved(rules);
267
+ props.onToggle(props.rowKey);
268
+ },
269
+ onCancel: () => props.onToggle(props.rowKey),
270
+ })
271
+ : null,
272
+ );
273
+ }
274
+
275
+ // ---- one provider card ---------------------------------------------------
276
+
277
+ function ProviderCard(props) {
278
+ const info = props.info;
279
+ const rows = [];
280
+ const wildKey = info.provider + "/*";
281
+ rows.push(
282
+ h(RuleRow, {
283
+ key: wildKey,
284
+ rowKey: wildKey,
285
+ provider: info.provider,
286
+ model: "*",
287
+ title: "所有模型(服务商级)",
288
+ subtitle: wildKey,
289
+ rule: props.ruleMap[wildKey],
290
+ open: props.openKey === wildKey,
291
+ onToggle: props.onToggle,
292
+ onSaved: props.onSaved,
293
+ }),
294
+ );
295
+ const models = Array.isArray(info.models) ? info.models : [];
296
+ for (let i = 0; i < models.length; i++) {
297
+ const m = models[i];
298
+ const key = info.provider + "/" + m.id;
299
+ rows.push(
300
+ h(RuleRow, {
301
+ key,
302
+ rowKey: key,
303
+ provider: info.provider,
304
+ model: m.id,
305
+ title: m.name && m.name.length > 0 ? m.name : m.id,
306
+ subtitle: m.id,
307
+ rule: props.ruleMap[key],
308
+ open: props.openKey === key,
309
+ onToggle: props.onToggle,
310
+ onSaved: props.onSaved,
311
+ }),
312
+ );
313
+ }
314
+ return h(
315
+ "section",
316
+ { className: "mpi-provider" },
317
+ h(
318
+ "header",
319
+ { className: "mpi-provider-head" },
320
+ h("span", { className: "mpi-provider-name" }, info.displayName && info.displayName.length > 0 ? info.displayName : info.provider),
321
+ h("span", { className: "mpi-id" }, info.provider),
322
+ h("span", { className: "mpi-state " + (info.active ? "mpi-state-on" : "mpi-state-off") }, info.active ? "运行中" : "未激活"),
323
+ ),
324
+ rows,
325
+ models.length === 0
326
+ ? h("p", { className: "mpi-empty" }, "该服务商的设置中未找到 models 列表,可使用上方的服务商级规则。")
327
+ : null,
328
+ );
329
+ }
330
+
331
+ // ---- settings page (settings.section) -----------------------------------
332
+
333
+ function Section() {
334
+ const [state, setState] = React.useState({ loading: true, rules: [], providers: [], error: "" });
335
+ const [openKey, setOpenKey] = React.useState("");
336
+
337
+ const load = function () {
338
+ remote
339
+ .getState()
340
+ .then((res) => {
341
+ const value = pick(res) || {};
342
+ setState({
343
+ loading: false,
344
+ rules: Array.isArray(value.rules) ? value.rules : [],
345
+ providers: Array.isArray(value.providers) ? value.providers : [],
346
+ error: "",
347
+ });
348
+ })
349
+ .catch((e) => {
350
+ setState({ loading: false, rules: [], providers: [], error: "读取配置失败:" + (e && e.message ? e.message : String(e)) });
351
+ });
352
+ };
353
+
354
+ React.useEffect(load, []);
355
+
356
+ const applyRules = function (rules) {
357
+ setState((current) => ({ loading: false, rules, providers: current.providers, error: "" }));
358
+ };
359
+ const toggle = function (key) {
360
+ setOpenKey((current) => (current === key ? "" : key));
361
+ };
362
+
363
+ let body;
364
+ if (state.loading) {
365
+ body = h("p", { className: "mpi-empty" }, "正在读取本地模型配置…");
366
+ } else if (state.error) {
367
+ body = h("p", { className: "mpi-error" }, state.error);
368
+ } else if (state.providers.length === 0) {
369
+ body = h("p", { className: "mpi-empty" }, "未找到本地已配置的服务商,请先在「模型」设置中添加服务商与模型。");
370
+ } else {
371
+ const ruleMap = rulesByKey(state.rules);
372
+ body = state.providers.map((info) =>
373
+ h(ProviderCard, {
374
+ key: info.provider,
375
+ info,
376
+ ruleMap,
377
+ openKey,
378
+ onToggle: toggle,
379
+ onSaved: applyRules,
380
+ }),
381
+ );
382
+ }
383
+
384
+ return h(
385
+ "div",
386
+ { className: "mpi-page" },
387
+ h(
388
+ "p",
389
+ { className: "mpi-intro" },
390
+ "为本地已配置的模型追加系统提示词:每次向该模型发起请求时,规则内容会注入到系统提示词的末尾。服务商级规则对该服务商的所有模型生效,并与模型级规则按顺序叠加。规则持久化保存,DSH 重启后仍然有效。",
391
+ ),
392
+ h(
393
+ "div",
394
+ { className: "mpi-headrow" },
395
+ h("span", { className: "mpi-empty" }, state.loading ? "" : state.rules.length + " 条规则"),
396
+ h(ui.Button, { variant: "ghost", size: "sm", onClick: load }, "刷新"),
397
+ ),
398
+ body,
399
+ );
400
+ }
401
+
402
+ ctx.slots.inject("settings.section", () =>
403
+ ctx.slots.register(
404
+ { name: "settings.section", id: "model-prompt-injector", order: 12, label: () => SETTINGS_LABEL },
405
+ () => h(Section),
406
+ ),
407
+ );
408
+ }
409
+
410
+ exports.apply = apply;
411
+ exports.inject = ["slots", "remote"];
412
+ return exports;
413
+ },
414
+ });
@@ -0,0 +1,5 @@
1
+ # dsh bundle patch: inserts this plugin into a profile's layer stack.
2
+ # Only an insert row — this plugin owns no config overrides.
3
+ - insert:
4
+ - id: model-prompt-injector
5
+ name: '@duke-dsh-plugins/dsh-model-prompt-injector'
package/index.js ADDED
@@ -0,0 +1,358 @@
1
+ /**
2
+ * dsh-model-prompt-injector — Host half.
3
+ *
4
+ * A Cordis "class plugin": this module exports `ModelPromptInjectorService`
5
+ * extending `TypertRemoteService`. The DSH loader instantiates the class and
6
+ * registers it as the `modelPromptInjector` service; the Typert Gateway
7
+ * exposes its Remote-marked methods to the browser Client half under the
8
+ * `modelPromptInjector` Remote namespace.
9
+ *
10
+ * What it does:
11
+ *
12
+ * 1. INJECT — registers ONE dynamic system-prompt section
13
+ * (`model-prompt-injector:extra`, order 9950 — after every shipped
14
+ * section, so rule text lands at the END of the system prompt). The
15
+ * section text is a function evaluated before EVERY model step; the
16
+ * runtime assembly context carries the agent (`assembleContextFor`
17
+ * returns `{ agent, scope, signal }`), so `context.agent.options`
18
+ * tells the exact provider/model the upcoming request targets. Rules
19
+ * matching that route are joined and returned; unmatched routes return
20
+ * "" and the prompt renderer drops empty sections entirely.
21
+ *
22
+ * 2. PERSIST — rules live in `<DSH_HOME>/model-prompt-injector/config.json`
23
+ * (outside any profile's node_modules, so reinstalls and upgrades never
24
+ * touch it) and are hydrated at init: rule edits survive DSH restarts.
25
+ *
26
+ * 3. SERVE — two Remote methods for the Settings page: `getState`
27
+ * (rule table + the directory of LOCALLY CONFIGURED providers/models,
28
+ * enumerated exactly like the Models settings page: the configurable
29
+ * provider directory + each namespace's `[...settingsPath, "models"]`)
30
+ * and `setRule` (upsert; a blank prompt deletes).
31
+ *
32
+ * Rule matching:
33
+ * - key shape `provider/model` targets one exact model (ids are
34
+ * case-sensitive route ids, e.g. `minimax-cn/MiniMax-M3`);
35
+ * - key shape `provider/*` is a provider-wide rule applying to every model
36
+ * of that provider. Both kinds stack, provider-wide first (the `*` key
37
+ * sorts before any model id in the table's ascending key order).
38
+ *
39
+ * Mount on the HOST plane (the package's cordis.patch.yml insert row): the
40
+ * section registration must live in the root scope so EVERY agent's assembly
41
+ * (main sessions, subagents, workflow children) sees it.
42
+ */
43
+
44
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
45
+ import { Service } from "@deepseek-ai/cordis";
46
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
47
+ import { homedir } from "node:os";
48
+ import { join } from "node:path";
49
+
50
+ // ---- constants --------------------------------------------------------------
51
+
52
+ /** The registered prompt section name (unique; nobody else shadows it). */
53
+ const SECTION_NAME = "model-prompt-injector:extra";
54
+ /**
55
+ * Section placement: the shipped SECTION_ORDERS max out at
56
+ * STRUCTURED_OUTPUT = 9900 (see @deepseek-ai/dsh-system-prompt), so 9950
57
+ * appends rule text at the very end of the system prompt.
58
+ */
59
+ const SECTION_ORDER = 9950;
60
+
61
+ /**
62
+ * On-disk persistence for the rule table. Lives under DSH_HOME, outside any
63
+ * profile's node_modules so reinstalls and upgrades never touch it.
64
+ */
65
+ const DATA_DIR = join(process.env.DSH_HOME || join(homedir(), ".dsh"), "model-prompt-injector");
66
+ const CONFIG_FILE = join(DATA_DIR, "config.json");
67
+
68
+ /** Rule key = `provider/model`; `*` as model marks the provider-wide rule. */
69
+ function ruleKey(provider, model) {
70
+ return provider + "/" + model;
71
+ }
72
+
73
+ // ---- helpers ----------------------------------------------------------------
74
+
75
+ /**
76
+ * Mark one instance method as a Remote export without relying on decorator
77
+ * syntax (Node ESM does not support the proposal decorators here). We drive
78
+ * the same `Remote(name)` decorator manually through a synthetic decorator
79
+ * context and run the registered initializers against the instance.
80
+ */
81
+ function markRemoteMethod(instance, method, exportName) {
82
+ const decorator = Remote(method, undefined);
83
+ const initializers = [];
84
+ decorator(undefined, {
85
+ kind: "method",
86
+ name: method,
87
+ static: false,
88
+ private: false,
89
+ addInitializer: (fn) => initializers.push(fn),
90
+ });
91
+ for (const fn of initializers) fn.call(instance);
92
+ }
93
+
94
+ /** Coerce one persisted/remote record into a clean rule, or null when invalid. */
95
+ function sanitizeRule(input) {
96
+ if (!input || typeof input !== "object") return null;
97
+ const provider = typeof input.provider === "string" ? input.provider.trim() : "";
98
+ const model = typeof input.model === "string" ? input.model.trim() : "";
99
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
100
+ if (provider.length === 0 || model.length === 0 || prompt.trim().length === 0) return null;
101
+ const updatedAt = typeof input.updatedAt === "string" ? input.updatedAt : new Date().toISOString();
102
+ return { key: ruleKey(provider, model), provider, model, prompt, updatedAt };
103
+ }
104
+
105
+ /** Defensive path navigation over a plain settings value (never throws). */
106
+ function getPath(value, path) {
107
+ let node = value;
108
+ for (const key of path) {
109
+ if (node === null || typeof node !== "object") return undefined;
110
+ node = node[key];
111
+ }
112
+ return node;
113
+ }
114
+
115
+ /** Best-effort error text. */
116
+ function errText(e) {
117
+ return e && typeof e.message === "string" ? e.message : String(e);
118
+ }
119
+
120
+ // ---- service ----------------------------------------------------------------
121
+
122
+ export class ModelPromptInjectorService extends TypertRemoteService {
123
+ /**
124
+ * No hard dependencies: every capability surface (`systemPrompt` for the
125
+ * injection, `llm`/`settings` for the directory) is mounted optionally so
126
+ * the plugin keeps serving its settings page (and stored rules) even when
127
+ * one registry is absent from the composition.
128
+ */
129
+
130
+ /**
131
+ * Cordis instantiates class plugins with `new Callback(ctx, config)` — the
132
+ * second argument is the plugin config, NOT the service key. Pass the exact
133
+ * service key to `super()`.
134
+ */
135
+ constructor(ctx, config) {
136
+ super(ctx, "modelPromptInjector");
137
+ }
138
+
139
+ /**
140
+ * Cordis class-plugin initializer: mark the Remote methods, hydrate the
141
+ * persisted rule table, then mount the injection section.
142
+ */
143
+ async [Service.init]() {
144
+ markRemoteMethod(this, "getState", "getState");
145
+ markRemoteMethod(this, "setRule", "setRule");
146
+
147
+ /**
148
+ * The rule table: [{ key, provider, model, prompt, updatedAt }], kept
149
+ * ascending by key so a provider-wide `provider/*` entry (0x2A) always
150
+ * sorts before that provider's exact-model entries and therefore injects
151
+ * first. Persisted in config.json.
152
+ */
153
+ this._rules = [];
154
+
155
+ await this._loadPersisted();
156
+
157
+ // The injection itself: one dynamic section in the ROOT scope, so every
158
+ // agent's per-step assembly (main sessions, subagents, workflow children)
159
+ // evaluates it. `ctx.inject` mounts the contribution only while the
160
+ // systemPrompt registry is composed and unwinds cleanly with it.
161
+ this.ctx.inject(["systemPrompt"], (scope) => {
162
+ scope.systemPrompt.section({
163
+ name: SECTION_NAME,
164
+ order: SECTION_ORDER,
165
+ text: (context) => this._extraPrompt(context),
166
+ });
167
+ });
168
+ }
169
+
170
+ // ---- injection --------------------------------------------------------------
171
+
172
+ /**
173
+ * Section text, evaluated before every model step. The runtime assembly
174
+ * context carries the agent (`assembleContextFor` returns
175
+ * `{ agent, scope, signal }`), and `agent.options.provider/model` is the
176
+ * route the upcoming request targets — the same source the loop builds the
177
+ * (deep-frozen) request header from, so matching here is exact. Returns ""
178
+ * for unmatched routes: the prompt renderer drops empty sections, so
179
+ * unmatched models pay nothing. Never throws — a section evaluation failure
180
+ * would otherwise poison the whole assembly.
181
+ */
182
+ _extraPrompt(context) {
183
+ try {
184
+ const agent = context ? context.agent : undefined;
185
+ const options = agent ? agent.options : undefined;
186
+ if (!options) return "";
187
+ const provider = options.provider;
188
+ const model = options.model;
189
+ if (typeof provider !== "string" || provider.length === 0) return "";
190
+ if (typeof model !== "string" || model.length === 0) return "";
191
+ const parts = [];
192
+ for (const rule of this._rules) {
193
+ if (rule.provider !== provider) continue;
194
+ if (rule.model === "*" || rule.model === model) parts.push(rule.prompt);
195
+ }
196
+ return parts.join("\n\n");
197
+ } catch (e) {
198
+ return "";
199
+ }
200
+ }
201
+
202
+ // ---- persistence ----------------------------------------------------------
203
+
204
+ /** Hydrate the rule table from config.json (never throws). */
205
+ async _loadPersisted() {
206
+ try {
207
+ const raw = await readFile(CONFIG_FILE, "utf8");
208
+ const data = JSON.parse(raw);
209
+ if (data && Array.isArray(data.rules)) {
210
+ const clean = [];
211
+ for (const entry of data.rules) {
212
+ const rule = sanitizeRule(entry);
213
+ if (rule !== null) clean.push(rule);
214
+ }
215
+ clean.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
216
+ this._rules = clean;
217
+ }
218
+ } catch (e) {
219
+ /* first run or a corrupt file both start empty */
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Persist the rule table (fire-and-forget, best-effort): a persistence
225
+ * failure must never break the settings UI or the injection path — the
226
+ * in-memory table stays authoritative for this process either way.
227
+ */
228
+ _persist() {
229
+ const payload = JSON.stringify({ version: 1, rules: this._rules }, null, 2);
230
+ mkdir(DATA_DIR, { recursive: true })
231
+ .then(() => writeFile(CONFIG_FILE, payload, "utf8"))
232
+ .catch(() => {});
233
+ }
234
+
235
+ /** Detached, display-ordered copy of the rule table. */
236
+ _rulesSnapshot() {
237
+ return this._rules.map((rule) => ({
238
+ key: rule.key,
239
+ provider: rule.provider,
240
+ model: rule.model,
241
+ prompt: rule.prompt,
242
+ updatedAt: rule.updatedAt,
243
+ }));
244
+ }
245
+
246
+ // ---- Remote methods ---------------------------------------------------------
247
+
248
+ /**
249
+ * Settings page snapshot: the rule table plus the directory of locally
250
+ * configured providers/models (see _directory).
251
+ */
252
+ async getState() {
253
+ return { ok: true, value: { rules: this._rulesSnapshot(), providers: this._directory() } };
254
+ }
255
+
256
+ /**
257
+ * Upsert one rule; a blank prompt deletes it. Returns the full table so the
258
+ * client can replace its state wholesale.
259
+ */
260
+ async setRule(request) {
261
+ try {
262
+ const provider = typeof request.provider === "string" ? request.provider.trim() : "";
263
+ const model = typeof request.model === "string" ? request.model.trim() : "";
264
+ const prompt = typeof request.prompt === "string" ? request.prompt.trim() : "";
265
+ if (provider.length === 0 || model.length === 0) {
266
+ return { ok: false, error: { code: "invalid-argument", message: "provider and model are required" } };
267
+ }
268
+ const key = ruleKey(provider, model);
269
+ const index = this._rules.findIndex((rule) => rule.key === key);
270
+ if (prompt.length === 0) {
271
+ if (index !== -1) this._rules.splice(index, 1);
272
+ } else {
273
+ const next = { key, provider, model, prompt, updatedAt: new Date().toISOString() };
274
+ if (index === -1) this._rules.push(next);
275
+ else this._rules[index] = next;
276
+ this._rules.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
277
+ }
278
+ this._persist();
279
+ return { ok: true, value: { rules: this._rulesSnapshot() } };
280
+ } catch (e) {
281
+ return { ok: false, error: { code: "internal", message: errText(e) } };
282
+ }
283
+ }
284
+
285
+ // ---- provider/model directory ---------------------------------------------
286
+
287
+ /**
288
+ * The directory of LOCALLY CONFIGURED providers and their model catalogs,
289
+ * enumerated exactly like the Models settings page: walk the configurable
290
+ * provider directory, keep entries whose settings namespace is registered
291
+ * and whose settingsPath resolves, then read each entry's model list at
292
+ * `[...settingsPath, "models"]`. Active routes (a mounted adapter) are
293
+ * flagged via `llm.listProviders()`. Only detached leaf values cross the
294
+ * wire; a missing `llm`/`settings` registry degrades to an empty list.
295
+ */
296
+ _directory() {
297
+ const out = [];
298
+ const llm = this.ctx.get("llm");
299
+ if (llm === undefined) return out;
300
+ const settings = this.ctx.get("settings");
301
+ const active = new Set();
302
+ try {
303
+ for (const info of llm.listProviders()) {
304
+ if (info && typeof info.id === "string") active.add(info.id);
305
+ }
306
+ } catch (e) {
307
+ /* an empty active set only affects the badge */
308
+ }
309
+ let entries = [];
310
+ try {
311
+ entries = llm.listConfigurableProviders();
312
+ } catch (e) {
313
+ return out;
314
+ }
315
+ for (const entry of entries) {
316
+ try {
317
+ const settingsPath = Array.isArray(entry.settingsPath) ? entry.settingsPath : [];
318
+ let value;
319
+ if (settings !== undefined) {
320
+ try {
321
+ value = settings.get(entry.settingsNs);
322
+ } catch (e) {
323
+ value = undefined;
324
+ }
325
+ }
326
+ const configured =
327
+ value !== undefined && (settingsPath.length === 0 || getPath(value, settingsPath) !== undefined);
328
+ if (!configured) continue;
329
+ const rawModels = getPath(value, settingsPath.concat(["models"]));
330
+ const models = [];
331
+ if (Array.isArray(rawModels)) {
332
+ for (const item of rawModels) {
333
+ if (item && typeof item === "object" && typeof item.id === "string" && item.id.length > 0) {
334
+ models.push({
335
+ id: item.id,
336
+ name: typeof item.name === "string" && item.name.length > 0 ? item.name : item.id,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ out.push({
342
+ provider: String(entry.provider),
343
+ displayName:
344
+ typeof entry.displayName === "string" && entry.displayName.length > 0
345
+ ? entry.displayName
346
+ : String(entry.provider),
347
+ active: active.has(entry.provider),
348
+ models,
349
+ });
350
+ } catch (e) {
351
+ /* one unreadable entry does not sink the directory */
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+ }
357
+
358
+ export default ModelPromptInjectorService;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@duke-dsh-plugins/dsh-model-prompt-injector",
3
+ "version": "1.0.0",
4
+ "description": "为 DeepSeek Harness 中本地已配置的模型追加系统提示词:按 provider/model 精确匹配或服务商级通配(provider/*)注入到系统提示词末尾,设置面板逐模型管理,规则持久化、重启不丢。",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/MoonlitDropOfBlood/dsh-model-prompt-injector.git"
8
+ },
9
+ "homepage": "https://github.com/MoonlitDropOfBlood/dsh-model-prompt-injector",
10
+ "bugs": { "url": "https://github.com/MoonlitDropOfBlood/dsh-model-prompt-injector/issues" },
11
+ "publishConfig": { "access": "public" },
12
+ "keywords": [
13
+ "dsh",
14
+ "deepseek-harness",
15
+ "plugin",
16
+ "system-prompt",
17
+ "prompt",
18
+ "model",
19
+ "persona",
20
+ "llm"
21
+ ],
22
+ "type": "module",
23
+ "main": "index.js",
24
+ "exports": {
25
+ ".": "./index.js",
26
+ "./client": "./client.js",
27
+ "./typert": "./typert.host.js",
28
+ "./cordis.patch.yml": "./cordis.patch.yml",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "index.js",
33
+ "client.js",
34
+ "typert.host.js",
35
+ "cordis.patch.yml",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "license": "MIT",
40
+ "dsh": {
41
+ "bundle": { "patch": "./cordis.patch.yml" },
42
+ "client": {
43
+ "platform": "web"
44
+ }
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "4.0.1 || 4.0.2",
48
+ "@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2 || 0.1.2-rc.1"
49
+ },
50
+ "dependencies": {
51
+ "zod": "^4.4.3"
52
+ },
53
+ "scripts": {
54
+ "check": "node --check index.js && node --check client.js && node --check typert.host.js",
55
+ "test": "node --check index.js && node --check client.js && node --check typert.host.js"
56
+ }
57
+ }
package/typert.host.js ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * dsh-model-prompt-injector — Typert Host manifest.
3
+ *
4
+ * Hand-written TYPERT manifest (the format the DSH typert-loader consumes
5
+ * from the package's `./typert` export). It describes the
6
+ * `modelPromptInjector` Remote service the Host half publishes so the
7
+ * browser Client half can call it through `ctx.remote.modelPromptInjector.*`
8
+ * (after mounting the namespace via `ctx.remote.$mount` — see client.js).
9
+ *
10
+ * Keep the invocation ids, service/namespace names and method names in sync
11
+ * with `index.js` (ModelPromptInjectorService) and `client.js`.
12
+ *
13
+ * Result schemas are STRICT: every Host return value must match exactly
14
+ * (fields present, types correct), or the gateway validation fails.
15
+ */
16
+
17
+ import { z } from "zod";
18
+
19
+ // ---- shared shapes ----------------------------------------------------------
20
+
21
+ const ruleSchema = z
22
+ .object({
23
+ key: z.string(),
24
+ provider: z.string(),
25
+ model: z.string(),
26
+ prompt: z.string(),
27
+ updatedAt: z.string(),
28
+ })
29
+ .readonly();
30
+
31
+ const targetModelSchema = z
32
+ .object({
33
+ id: z.string(),
34
+ name: z.string(),
35
+ })
36
+ .readonly();
37
+
38
+ const targetProviderSchema = z
39
+ .object({
40
+ provider: z.string(),
41
+ displayName: z.string(),
42
+ active: z.boolean(),
43
+ models: z.array(targetModelSchema).readonly(),
44
+ })
45
+ .readonly();
46
+
47
+ const stateValueSchema = z
48
+ .object({
49
+ rules: z.array(ruleSchema).readonly(),
50
+ providers: z.array(targetProviderSchema).readonly(),
51
+ })
52
+ .readonly();
53
+
54
+ const rulesValueSchema = z
55
+ .object({
56
+ rules: z.array(ruleSchema).readonly(),
57
+ })
58
+ .readonly();
59
+
60
+ /** The shared ok|error envelope. */
61
+ function okResult(valueSchema) {
62
+ return z.union([
63
+ z
64
+ .object({
65
+ ok: z.literal(true).readonly(),
66
+ value: valueSchema.readonly(),
67
+ })
68
+ .readonly(),
69
+ z
70
+ .object({
71
+ ok: z.literal(false).readonly(),
72
+ error: z
73
+ .object({
74
+ code: z.string().readonly(),
75
+ message: z.string().readonly().optional(),
76
+ })
77
+ .readonly(),
78
+ })
79
+ .readonly(),
80
+ ]);
81
+ }
82
+
83
+ const stateResultSchema = okResult(stateValueSchema);
84
+ const setRuleResultSchema = okResult(rulesValueSchema);
85
+
86
+ // ---- per-invocation parameter schemas ---------------------------------------
87
+
88
+ const _modelPromptInjector_setRule_parameter_0$schema = z.object({
89
+ provider: z.string(),
90
+ model: z.string(),
91
+ prompt: z.string(),
92
+ });
93
+
94
+ export const TYPERT = {
95
+ package: "@duke-dsh-plugins/dsh-model-prompt-injector",
96
+ face: "host",
97
+ schemas: [],
98
+ invocations: [
99
+ {
100
+ id: "dsh-model-prompt-injector#modelPromptInjector/getState",
101
+ service: "modelPromptInjector",
102
+ namespace: "modelPromptInjector",
103
+ method: "getState",
104
+ invocation: { kind: "direct" },
105
+ parameters: [],
106
+ result: {
107
+ mode: "strict",
108
+ typeSymbol: "dsh-model-prompt-injector#ModelPromptInjectorStateResult",
109
+ schema: stateResultSchema,
110
+ },
111
+ sourceLocation: { file: "index.js", line: 1, column: 1 },
112
+ },
113
+ {
114
+ id: "dsh-model-prompt-injector#modelPromptInjector/setRule",
115
+ service: "modelPromptInjector",
116
+ namespace: "modelPromptInjector",
117
+ method: "setRule",
118
+ invocation: { kind: "direct" },
119
+ parameters: [
120
+ {
121
+ name: "request",
122
+ wire: "request",
123
+ source: "json",
124
+ codec: {
125
+ mode: "strict",
126
+ typeSymbol: "dsh-model-prompt-injector#ModelPromptInjectorSetRuleRequest",
127
+ schema: _modelPromptInjector_setRule_parameter_0$schema,
128
+ },
129
+ },
130
+ ],
131
+ result: {
132
+ mode: "strict",
133
+ typeSymbol: "dsh-model-prompt-injector#ModelPromptInjectorSetRuleResult",
134
+ schema: setRuleResultSchema,
135
+ },
136
+ sourceLocation: { file: "index.js", line: 1, column: 1 },
137
+ },
138
+ ],
139
+ model: {
140
+ services: [
141
+ {
142
+ description:
143
+ "Per-model system-prompt injection service: appends persisted per-route rules (exact provider/model or provider-wide provider/*) to the end of the system prompt on every matching model step, and serves the Settings page state (rule table + locally configured provider/model directory) to the DeepSeek Harness web UI.",
144
+ summary: "Per-model system-prompt injection service.",
145
+ tags: [],
146
+ jsDoc:
147
+ "/**\n * Per-model system-prompt injection: persisted rules appended to the system prompt tail per matching route.\n */",
148
+ key: "modelPromptInjector",
149
+ exportName: "ModelPromptInjectorService",
150
+ members: [
151
+ {
152
+ kind: "method",
153
+ name: "getState",
154
+ signature: "@Remote('getState') async getState(): Promise<ModelPromptInjectorStateResult>",
155
+ summary: "Snapshot for the Settings page (rule table + configured provider/model directory).",
156
+ jsDoc:
157
+ "/**\n * Return the persisted rule table plus the directory of locally configured providers and their models.\n * @returns success or a business failure.\n */",
158
+ },
159
+ {
160
+ kind: "method",
161
+ name: "setRule",
162
+ signature:
163
+ "@Remote('setRule') async setRule(request: ModelPromptInjectorSetRuleRequest): Promise<ModelPromptInjectorSetRuleResult>",
164
+ summary: "Upsert one rule (blank prompt deletes it) and persist the table.",
165
+ jsDoc:
166
+ "/**\n * Upsert the rule for one exact provider/model or a provider-wide model of \"*\"; a blank prompt deletes the rule.\n * @param request - { provider, model, prompt }.\n * @returns the full rule table.\n */",
167
+ },
168
+ ],
169
+ types: [
170
+ {
171
+ name: "ModelPromptInjectorRule",
172
+ declaration:
173
+ "export interface ModelPromptInjectorRule {\n readonly key: string;\n readonly provider: string;\n readonly model: string;\n readonly prompt: string;\n readonly updatedAt: string;\n}",
174
+ },
175
+ {
176
+ name: "ModelPromptInjectorTargetModel",
177
+ declaration:
178
+ "export interface ModelPromptInjectorTargetModel {\n readonly id: string;\n readonly name: string;\n}",
179
+ },
180
+ {
181
+ name: "ModelPromptInjectorTargetProvider",
182
+ declaration:
183
+ "export interface ModelPromptInjectorTargetProvider {\n readonly provider: string;\n readonly displayName: string;\n readonly active: boolean;\n readonly models: readonly ModelPromptInjectorTargetModel[];\n}",
184
+ },
185
+ {
186
+ name: "ModelPromptInjectorSetRuleRequest",
187
+ declaration:
188
+ "export interface ModelPromptInjectorSetRuleRequest {\n readonly provider: string;\n readonly model: string;\n readonly prompt: string;\n}",
189
+ },
190
+ {
191
+ name: "ModelPromptInjectorStateValue",
192
+ declaration:
193
+ "export interface ModelPromptInjectorStateValue {\n readonly rules: readonly ModelPromptInjectorRule[];\n readonly providers: readonly ModelPromptInjectorTargetProvider[];\n}",
194
+ },
195
+ {
196
+ name: "ModelPromptInjectorStateResult",
197
+ declaration:
198
+ "export type ModelPromptInjectorStateResult = { ok: true; value: ModelPromptInjectorStateValue } | { ok: false; error: { code: string; message?: string } };",
199
+ },
200
+ {
201
+ name: "ModelPromptInjectorRulesValue",
202
+ declaration:
203
+ "export interface ModelPromptInjectorRulesValue {\n readonly rules: readonly ModelPromptInjectorRule[];\n}",
204
+ },
205
+ {
206
+ name: "ModelPromptInjectorSetRuleResult",
207
+ declaration:
208
+ "export type ModelPromptInjectorSetRuleResult = { ok: true; value: ModelPromptInjectorRulesValue } | { ok: false; error: { code: string; message?: string } };",
209
+ },
210
+ ],
211
+ },
212
+ ],
213
+ events: [],
214
+ objects: [],
215
+ },
216
+ };