dsh-auto-check 0.0.0-stage → 0.1.1

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 YanMo
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 CHANGED
@@ -1,3 +1,103 @@
1
- # Temporary Holding Version
1
+ # dsh-auto-check
2
+
3
+ 为 DeepSeek Harness 的「自动审查」(Auto review)单独指定审查模型。
4
+
5
+ 自动审查默认跟随当前会话的主模型。主模型如果是价格较高的模型,每次工具调用前的审查都会消耗同样的高价 Token。本插件在 `llm/stream` 层拦截自动审查请求,把它改写到单独配置的 provider / model,其余请求原样放行。
6
+
7
+ ## 工作方式
8
+
9
+ - 自动审查请求的 `system` 提示以 `REVIEW_POLICY\n` 开头,这是插件识别它的唯一稳定标记。
10
+ - 命中后,插件用配置的 provider / model 重新发起一次 LLM 请求;未配置或只配置了一半时,保持原有行为。
11
+ - 改写后的请求会被标记,避免再次进入本插件造成递归。
12
+ - 不替换、不修改自带的 `auto-review` 插件;自动审查的策略、审批和拒绝逻辑全部保持原样。
13
+
14
+ ## 配置
15
+
16
+ 1. 打开 **设置 → 插件**(Plugins)。
17
+ 2. 在官方插件列表中找到 **自动审查模型**。
18
+ 3. 选择一个审查模型并保存;选择「跟随主模型」可恢复默认行为。
19
+ 4. 按需选择「审查协议」:
20
+ - **自动识别**(默认):目标模型名包含 `Qwen3Guard` 时使用 Qwen3Guard 适配,否则使用标准 JSON 协议。
21
+ - **标准 JSON**:要求审查模型严格输出 `{risk, decision, reason?}`,适用于通用模型。
22
+ - **Qwen3Guard 安全分级**:适用于 Qwen3Guard-Gen 系列(0.6B / 4B / 8B)。
23
+
24
+ 模型列表来自当前会话可路由的模型目录。只影响自动审查,不影响主对话或子代理。
25
+
26
+ ## Qwen3Guard 适配
27
+
28
+ Qwen3Guard-Gen 原生输出不是 JSON,而是两行文本:
29
+
30
+ ```text
31
+ Safety: Safe | Controversial | Unsafe
32
+ Categories: <分类列表 | None>
33
+ ```
34
+
35
+ 本插件在 Qwen3Guard 协议下:
36
+
37
+ - 去掉 auto review 的 JSON 策略 `system`,只把待审内容作为一条 user 消息发送;
38
+ - 把 `Safe` 映射为低风险放行;
39
+ - 把 `Controversial` 映射为中风险拒绝,`Unsafe` 映射为高风险拒绝,拒绝原因保留分类;
40
+ - 把原生文本流转换为 auto review 期望的「一个 text block + stop」流;
41
+ - 模型没有返回 `Safety:` 标签时按高风险拒绝(fail-closed)。
42
+
43
+ > Qwen3Guard 只判断内容安全,不判断工具调用的业务正确性;`Safe` 只代表内容没有命中安全分类,不代表这次工具调用一定符合你的意图。
44
+
45
+ > Qwen3Guard 只支持 Gen 系列的安全分级输出,Stream 系列是逐 token 审核,不适用本适配。`Controversial` 在本插件中映射为中风险拒绝:在 auto review 的默认审批策略下,它可能被转成一次用户确认,而不是直接丢弃;如果审批策略是 `never`,则会直接拒绝。若模型别名不含 `Qwen3Guard`,请在「审查协议」中显式选择 Qwen3Guard。
46
+
47
+ > 审查模型不可用(例如凭据失效、模型下线)时,自动审查会按原有失败策略拒绝工具调用。换回「跟随主模型」或选择一个可用模型即可恢复。
48
+
49
+ ## 安装
50
+
51
+ ```powershell
52
+ # 在 DSH 中通过插件管理器安装本目录:
53
+ # plugin_manager install_bundle target=<本目录绝对路径>
54
+ ```
55
+
56
+ 安装后包会以 `link:` 方式加入 profile。Host 侧代码的模块和 Config schema 会被 Node 缓存,修改 `lib/` 后需要重启 DSH Desktop(Electron)才能重新加载;只刷新页面不会更新 Host 插件。客户端 `client.js` 改动在刷新页面后生效。
57
+
58
+ ## 开发验证
59
+
60
+ ```powershell
61
+ node --test test/review-route.test.mjs
62
+ node --check lib/index.js
63
+ node --check client.js
64
+ ```
65
+
66
+ 测试覆盖:普通请求放行、审查请求改写、递归回放、空配置回退、同路由不重复改写、Qwen3Guard 协议探测、请求改写、标签映射与流转换。
67
+
68
+ ## 发布到 npm
69
+
70
+ 仓库包含 `.github/workflows/publish.yml`:推送 `v*` tag 时测试并**暂存**发布到 npm,使用 Trusted Publishing(OIDC),不保存长期 npm token。
71
+
72
+ 首次发布需要手动认领包名:
73
+
74
+ ```powershell
75
+ npm login --registry=https://registry.npmjs.org
76
+ npm publish --registry=https://registry.npmjs.org --access public
77
+ ```
78
+
79
+ 然后在 npmjs.com 的包设置 → Trusted Publishing 中添加 GitHub Actions:
80
+
81
+ - Owner:`ltzXiaoYanMo`
82
+ - Repository:`dsh-auto-check`
83
+ - Workflow:`publish.yml`
84
+
85
+ 之后的发布流程:
86
+
87
+ ```powershell
88
+ git tag v0.1.1
89
+ git push origin v0.1.1
90
+ ```
91
+
92
+ 工作流以 `npm stage publish` 把版本放入 npm 暂存区;因为 Trusted Publisher 配置的是 `npm stage publish` 权限,最终发布需要维护者完成 2FA 审批:
93
+
94
+ ```powershell
95
+ npm login --registry=https://registry.npmjs.org
96
+ npm stage list
97
+ npm stage approve <stage-id>
98
+ ```
99
+
100
+ 也可以在 npmjs.com 的包页面完成审批。
101
+
102
+ `dsh-plugins` 是仓库里程碑 tag,不会触发 npm 发布。
2
103
 
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.
package/client.js ADDED
@@ -0,0 +1,380 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-auto-check",
3
+ factory: (require) => {
4
+ const { jsx, jsxs } = require("react/jsx-runtime");
5
+ const {
6
+ SettingsForm,
7
+ SettingsFormModel,
8
+ settingsTextField,
9
+ } = require("@deepseek-ai/dsh-client-ui-primitives");
10
+
11
+ const NS = "settings.autoCheck";
12
+ const ENTRY_ID = "dsh-auto-check";
13
+ const SEP = "\u0000";
14
+
15
+ const css =
16
+ ".dshAutoCheckField{display:grid;gap:6px;padding:12px 0}" +
17
+ ".dshAutoCheckLabel{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}" +
18
+ ".dshAutoCheckSelect{background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;min-height:32px;padding:4px 8px;width:100%;max-width:520px}" +
19
+ ".dshAutoCheckSelect:disabled{opacity:.6}" +
20
+ ".dshAutoCheckHint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}" +
21
+ ".dshAutoCheckNotice{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}" +
22
+ ".dshAutoCheckError{display:flex;align-items:center;gap:12px;margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:1.5}" +
23
+ ".dshAutoCheckError button{background:0 0;border:0;color:var(--dsw-alias-brand-primary);cursor:pointer;font:inherit;padding:0}";
24
+ const tagId = "dsh-auto-check/settings.css";
25
+ if (
26
+ typeof document !== "undefined" &&
27
+ document.querySelector('style[data-plugin-css="' + tagId + '"]') === null
28
+ ) {
29
+ const tag = document.createElement("style");
30
+ tag.dataset.plugin = "dsh-auto-check";
31
+ tag.dataset.pluginCss = tagId;
32
+ tag.textContent = css;
33
+ document.head.appendChild(tag);
34
+ }
35
+
36
+ const zh = {
37
+ title: "自动审查模型",
38
+ description: "为自动审查单独选择审查模型,不再跟随主模型。",
39
+ reviewerModel: "审查模型",
40
+ followMain: "跟随主模型(默认)",
41
+ hint: "自动审查的请求会改走这里选择的模型;留空则保持原有行为。只影响自动审查,不影响主对话。",
42
+ protocol: "审查协议",
43
+ protocolAuto: "自动识别(模型名含 Qwen3Guard)",
44
+ protocolJson: "标准 JSON(通用模型)",
45
+ protocolQwen3Guard: "Qwen3Guard 安全分级",
46
+ protocolHint: "Qwen3Guard 模式会去掉 auto review 的 JSON 策略,把待审内容作为用户消息发送,并把 Safety 标签映射为风险等级:Safe 放行,Controversial / Unsafe 拒绝。",
47
+ protocolWarning: "Controversial 按中风险拒绝;默认审批策略下可能转人工确认,审批策略为 never 时直接拒绝。",
48
+ loading: "正在加载可用模型…",
49
+ loadFailed: "无法加载模型列表。",
50
+ retry: "重试",
51
+ partial: "部分提供商暂不可用,列表可能不完整。",
52
+ unlisted: "当前配置(不在列表)",
53
+ empty: "没有可用的模型。",
54
+ unavailable: "此设置当前不可用。",
55
+ readOnly: "此设置当前为只读。",
56
+ saveFailed: "保存失败,请重试。",
57
+ save: "保存",
58
+ saving: "保存中…",
59
+ };
60
+ const en = {
61
+ title: "Auto review model",
62
+ description: "Choose a dedicated reviewer model for Auto review instead of following the main model.",
63
+ reviewerModel: "Reviewer model",
64
+ followMain: "Follow the main model (default)",
65
+ hint: "Auto review requests are routed to this model. Leave it empty to keep the original behaviour. Only Auto review is affected.",
66
+ protocol: "Review protocol",
67
+ protocolAuto: "Auto-detect (Qwen3Guard in model name)",
68
+ protocolJson: "Standard JSON (general models)",
69
+ protocolQwen3Guard: "Qwen3Guard safety grading",
70
+ protocolHint: "Qwen3Guard mode removes auto review's JSON policy, sends the pending content as the user message, and maps the Safety label to a risk level: Safe allows; Controversial / Unsafe deny.",
71
+ protocolWarning: "Controversial is denied as medium risk; the default approval policy may turn it into a user confirmation, while a `never` override denies it outright.",
72
+ loading: "Loading available models…",
73
+ loadFailed: "Could not load the model list.",
74
+ retry: "Retry",
75
+ partial: "Some providers are unavailable, so the list may be incomplete.",
76
+ unlisted: "Current setting (not listed)",
77
+ empty: "No models are available.",
78
+ unavailable: "This setting is currently unavailable.",
79
+ readOnly: "This setting is read-only.",
80
+ saveFailed: "Could not save. Try again.",
81
+ save: "Save",
82
+ saving: "Saving…",
83
+ };
84
+
85
+ const labels = (t) => ({
86
+ unavailable: t("unavailable"),
87
+ readOnly: t("readOnly"),
88
+ saveFailed: t("saveFailed"),
89
+ save: t("save"),
90
+ saving: t("saving"),
91
+ });
92
+
93
+ class AutoCheckCardController {
94
+ constructor(ctx, scope) {
95
+ this.ctx = ctx;
96
+ this.form = new SettingsFormModel(scope, [
97
+ settingsTextField("provider"),
98
+ settingsTextField("model"),
99
+ settingsTextField("protocol"),
100
+ ]);
101
+ this.actions = this.form.actions();
102
+ this.store = this.form.bind(() => this.projection());
103
+ this.groups = [];
104
+ this.catalogStatus = "loading";
105
+ this.catalogPartial = false;
106
+ this.generation = 0;
107
+ void this.refreshCatalog();
108
+ }
109
+
110
+ projection() {
111
+ return {
112
+ ...this.form.shell(),
113
+ provider: this.form.field("provider"),
114
+ model: this.form.field("model"),
115
+ protocol: this.form.field("protocol"),
116
+ groups: this.groups,
117
+ catalogStatus: this.catalogStatus,
118
+ catalogPartial: this.catalogPartial,
119
+ };
120
+ }
121
+
122
+ publish() {
123
+ this.form.publish();
124
+ }
125
+
126
+ async refreshCatalog() {
127
+ const generation = ++this.generation;
128
+ this.catalogStatus = "loading";
129
+ this.catalogPartial = false;
130
+ this.publish();
131
+ try {
132
+ const response = await this.ctx.remote.session.modelCatalog();
133
+ if (generation !== this.generation) return;
134
+ if (response !== undefined && response.ok) {
135
+ this.groups = response.value.groups;
136
+ this.catalogPartial = response.value.failures.length > 0;
137
+ this.catalogStatus = "ready";
138
+ } else {
139
+ this.catalogStatus = "error";
140
+ }
141
+ } catch {
142
+ if (generation !== this.generation) return;
143
+ this.catalogStatus = "error";
144
+ }
145
+ this.publish();
146
+ }
147
+
148
+ selectRoute(route) {
149
+ if (route === null) {
150
+ this.actions.edit("provider", "");
151
+ this.actions.edit("model", "");
152
+ this.actions.edit("protocol", "auto");
153
+ return;
154
+ }
155
+ this.actions.edit("provider", route.provider);
156
+ this.actions.edit("model", route.model);
157
+ const protocol = this.form.field("protocol").text.trim().toLowerCase();
158
+ if (protocol === "qwen3guard" && !/qwen3guard/i.test(route.model)) {
159
+ this.actions.edit("protocol", "auto");
160
+ }
161
+ }
162
+
163
+ inject() {
164
+ return {
165
+ hooks: { autoCheckCard: this.store },
166
+ ...this.actions,
167
+ };
168
+ }
169
+
170
+ dispose() {
171
+ this.form.dispose();
172
+ }
173
+ }
174
+
175
+ function AutoCheckCard(props) {
176
+ const { t } = props;
177
+ const state = props.useAutoCheckCard((snapshot) => snapshot);
178
+ if (props.view === "summary") return t("description");
179
+
180
+ const provider = state.provider.text.trim();
181
+ const model = state.model.text.trim();
182
+ const protocol = state.protocol.text.trim() || "auto";
183
+ const current = provider !== "" && model !== "" ? provider + SEP + model : "";
184
+ const routes = new Set();
185
+ for (const group of state.groups) {
186
+ for (const item of group.models) routes.add(group.id + SEP + item.id);
187
+ }
188
+ const unlisted = current !== "" && !routes.has(current);
189
+
190
+ const options = [
191
+ jsx("option", { value: "", children: t("followMain") }, "__follow_main__"),
192
+ ];
193
+ for (const group of state.groups) {
194
+ options.push(
195
+ jsxs(
196
+ "optgroup",
197
+ {
198
+ label: group.name,
199
+ children: group.models.map((item) =>
200
+ jsx(
201
+ "option",
202
+ {
203
+ value: group.id + SEP + item.id,
204
+ children: item.name + " · " + group.name,
205
+ },
206
+ group.id + SEP + item.id,
207
+ ),
208
+ ),
209
+ },
210
+ group.id,
211
+ ),
212
+ );
213
+ }
214
+ if (unlisted) {
215
+ options.push(
216
+ jsx(
217
+ "option",
218
+ {
219
+ value: current,
220
+ children:
221
+ provider + " / " + model + " (" + t("unlisted") + ")",
222
+ },
223
+ "__unlisted__",
224
+ ),
225
+ );
226
+ }
227
+
228
+ const onSelect = (event) => {
229
+ const value = event.target.value;
230
+ if (value === "") {
231
+ props.selectRoute(null);
232
+ return;
233
+ }
234
+ const index = value.indexOf(SEP);
235
+ props.selectRoute({
236
+ provider: value.slice(0, index),
237
+ model: value.slice(index + SEP.length),
238
+ });
239
+ };
240
+
241
+ return jsx(SettingsForm, {
242
+ labels: labels(t),
243
+ state,
244
+ onSave: props.save,
245
+ onDiscard: props.discard,
246
+ children: jsxs("div", {
247
+ children: [
248
+ jsxs("div", {
249
+ className: "dshAutoCheckField",
250
+ children: [
251
+ jsx("label", {
252
+ className: "dshAutoCheckLabel",
253
+ htmlFor: "dsh-auto-check-model",
254
+ children: t("reviewerModel"),
255
+ }),
256
+ jsx("select", {
257
+ className: "dshAutoCheckSelect",
258
+ id: "dsh-auto-check-model",
259
+ value: current,
260
+ disabled: !state.writable || state.saving,
261
+ onChange: onSelect,
262
+ children: options,
263
+ }),
264
+ ],
265
+ }),
266
+ jsxs("div", {
267
+ className: "dshAutoCheckField",
268
+ children: [
269
+ jsx("label", {
270
+ className: "dshAutoCheckLabel",
271
+ htmlFor: "dsh-auto-check-protocol",
272
+ children: t("protocol"),
273
+ }),
274
+ jsx("select", {
275
+ className: "dshAutoCheckSelect",
276
+ id: "dsh-auto-check-protocol",
277
+ value: protocol,
278
+ disabled: !state.writable || state.saving,
279
+ onChange: (event) => props.edit("protocol", event.target.value),
280
+ children: [
281
+ jsx("option", { value: "auto", children: t("protocolAuto") }, "auto"),
282
+ jsx("option", { value: "json", children: t("protocolJson") }, "json"),
283
+ jsx("option", { value: "qwen3guard", children: t("protocolQwen3Guard") }, "qwen3guard"),
284
+ ],
285
+ }),
286
+ ],
287
+ }),
288
+ jsx("p", {
289
+ className: "dshAutoCheckHint",
290
+ children: t("hint"),
291
+ }),
292
+ jsx("p", {
293
+ className: "dshAutoCheckHint",
294
+ children: t("protocolHint"),
295
+ }),
296
+ protocol === "qwen3guard"
297
+ ? jsx("p", {
298
+ className: "dshAutoCheckNotice",
299
+ children: t("protocolWarning"),
300
+ })
301
+ : null,
302
+ state.catalogStatus === "loading"
303
+ ? jsx("p", {
304
+ className: "dshAutoCheckNotice",
305
+ role: "status",
306
+ children: t("loading"),
307
+ })
308
+ : null,
309
+ state.catalogStatus === "error"
310
+ ? jsxs("p", {
311
+ className: "dshAutoCheckError",
312
+ role: "alert",
313
+ children: [
314
+ jsx("span", { children: t("loadFailed") }),
315
+ jsx("button", {
316
+ type: "button",
317
+ disabled: state.saving,
318
+ onClick: () => props.retryCatalog(),
319
+ children: t("retry"),
320
+ }),
321
+ ],
322
+ })
323
+ : null,
324
+ state.catalogPartial
325
+ ? jsx("p", {
326
+ className: "dshAutoCheckNotice",
327
+ children: t("partial"),
328
+ })
329
+ : null,
330
+ state.catalogStatus === "ready" && state.groups.length === 0
331
+ ? jsx("p", {
332
+ className: "dshAutoCheckNotice",
333
+ children: t("empty"),
334
+ })
335
+ : null,
336
+ ],
337
+ }),
338
+ });
339
+ }
340
+
341
+ const inject = ["slots", "locale", "remote", "remote.session", "configForms"];
342
+
343
+ function apply(ctx) {
344
+ const t = ctx.locale.bind(NS);
345
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-auto-check: dictionaries");
346
+
347
+ const card = new AutoCheckCardController(
348
+ ctx,
349
+ ctx.configForms.get(ENTRY_ID),
350
+ );
351
+ ctx.effect(() => () => card.dispose(), "dsh-auto-check: form subscription");
352
+
353
+ ctx.effect(
354
+ () =>
355
+ ctx.configForms.whileServed([ENTRY_ID], () =>
356
+ ctx.slots.inject("plugins.item", () =>
357
+ ctx.slots.register(
358
+ {
359
+ name: "plugins.item",
360
+ id: ENTRY_ID,
361
+ order: 34,
362
+ label: () => t("title"),
363
+ locale: NS,
364
+ inject: () => ({
365
+ ...card.inject(),
366
+ retryCatalog: () => card.refreshCatalog(),
367
+ selectRoute: (route) => card.selectRoute(route),
368
+ }),
369
+ },
370
+ AutoCheckCard,
371
+ ),
372
+ ),
373
+ ),
374
+ "dsh-auto-check: page",
375
+ );
376
+ }
377
+
378
+ return { inject, apply };
379
+ },
380
+ });
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-auto-check
3
+ name: dsh-auto-check
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#1f6feb"/>
3
+ <path d="M20 44V20h7.5l4.5 13 4.5-13H44v24h-6V30l-4 12h-4l-4-12v14z" fill="#fff"/>
4
+ <path d="M46 38l3 6 6 3-6 3-3 6-3-6-6-3 6-3z" fill="#ffd166"/>
5
+ </svg>
package/lib/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { routeReview } from "./review-route.js";
4
+
5
+ /**
6
+ * Owns the reviewer route and rewrites Auto review requests before they reach
7
+ * the adapter. An empty provider or model keeps the shipped behaviour: the
8
+ * reviewer follows the main model selected for the session.
9
+ */
10
+ class AutoCheckReviewer extends Service {
11
+ static Config = z.object({
12
+ provider: z.string().default("").volatile(),
13
+ model: z.string().default("").volatile(),
14
+ protocol: z.string().default("auto").volatile(),
15
+ });
16
+
17
+ constructor(ctx, config) {
18
+ super(ctx, "autoCheckReviewer");
19
+ this.config = config;
20
+
21
+ ctx.inject(["settings"], (child) => {
22
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
23
+ });
24
+
25
+ ctx.inject(["llm"], (child) => {
26
+ child.effect(() =>
27
+ child.on(
28
+ "llm/stream",
29
+ (options, next) =>
30
+ routeReview(
31
+ options,
32
+ {
33
+ provider: this.config.provider.get(),
34
+ model: this.config.model.get(),
35
+ protocol: this.config.protocol.get(),
36
+ },
37
+ (request) => child.llm.stream(request),
38
+ ) ?? next(),
39
+ { global: true, prepend: true },
40
+ ),
41
+ );
42
+ });
43
+ }
44
+ }
45
+
46
+ export { AutoCheckReviewer as default };
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Pure routing and protocol adaptation for Auto review requests.
3
+ *
4
+ * The shipped auto-review plugin renders its policy as the request's `system`
5
+ * prompt, and that prompt starts with this exact line. The request carries no
6
+ * session id or other metadata, so the prefix is the only stable marker.
7
+ */
8
+ export const REVIEW_POLICY_PREFIX = "REVIEW_POLICY\n";
9
+
10
+ /**
11
+ * Requests we have already rewritten. `llm/stream` listeners may re-enter
12
+ * `ctx.llm.stream`, and the re-entered request must pass through to the adapter
13
+ * instead of matching this plugin again. The set is module scoped because the
14
+ * profile loads this plugin once.
15
+ */
16
+ const rewritten = new WeakSet();
17
+
18
+ /**
19
+ * Whether one LLM request is the Auto review request.
20
+ * @param {{ system?: unknown }} options
21
+ * @returns {boolean}
22
+ */
23
+ export function isReviewRequest(options) {
24
+ return (
25
+ typeof options.system === "string" &&
26
+ options.system.startsWith(REVIEW_POLICY_PREFIX)
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Qwen3Guard model ids are usually `Qwen/Qwen3Guard-Gen-4B` or
32
+ * `Qwen3Guard-Gen-8B`; the family name is enough to select the adapter.
33
+ * @param {unknown} model
34
+ * @returns {boolean}
35
+ */
36
+ export function isQwen3GuardModel(model) {
37
+ return typeof model === "string" && /qwen3guard/i.test(model);
38
+ }
39
+
40
+ /**
41
+ * Resolve the output protocol for a target route.
42
+ *
43
+ * `auto` uses the Qwen3Guard adapter only when the target model is in that
44
+ * family, so selecting a Qwen3Guard model works without a second setting.
45
+ * `json` keeps the shipped auto-review protocol. `qwen3guard` forces the
46
+ * adapter even for a differently named model (for example a local alias).
47
+ *
48
+ * @param {{ protocol?: unknown }} route
49
+ * @param {unknown} targetModel
50
+ * @returns {"json" | "qwen3guard"}
51
+ */
52
+ export function resolveReviewProtocol(route, targetModel) {
53
+ const protocol = String(route.protocol ?? "auto").trim().toLowerCase();
54
+ if (protocol === "qwen3guard") return "qwen3guard";
55
+ if (protocol === "json") return "json";
56
+ return isQwen3GuardModel(targetModel) ? "qwen3guard" : "json";
57
+ }
58
+
59
+ /**
60
+ * Collect the text Auto review sent for the pending tool call.
61
+ * @param {{ messages?: unknown }} options
62
+ * @returns {string}
63
+ */
64
+ export function reviewText(options) {
65
+ const messages = Array.isArray(options.messages) ? options.messages : [];
66
+ const parts = [];
67
+ for (const message of messages) {
68
+ if (typeof message?.content === "string") {
69
+ parts.push(message.content);
70
+ continue;
71
+ }
72
+ if (!Array.isArray(message?.content)) continue;
73
+ for (const block of message.content) {
74
+ if (block?.type === "text" && typeof block.text === "string") {
75
+ parts.push(block.text);
76
+ }
77
+ }
78
+ }
79
+ if (parts.length > 0) return parts.join("\n\n");
80
+ return typeof options.messages === "string" ? options.messages : "";
81
+ }
82
+
83
+ /**
84
+ * Build the request Qwen3Guard expects.
85
+ *
86
+ * Qwen3Guard-Gen uses a fixed chat template for safety classification and does
87
+ * not follow auto-review's JSON policy. Sending the policy as a system message
88
+ * would be classified as part of the content, so only the pending-action text
89
+ * is forwarded as the user turn. The output is short (`Safety:` plus
90
+ * `Categories:`), so 128 tokens is ample.
91
+ *
92
+ * @param {object} options - immutable Auto review request.
93
+ * @param {string} provider
94
+ * @param {string} model
95
+ * @returns {object}
96
+ */
97
+ export function qwen3GuardRequest(options, provider, model) {
98
+ const { system: _system, messages: _messages, ...rest } = options;
99
+ return {
100
+ ...rest,
101
+ provider,
102
+ model,
103
+ messages: [
104
+ {
105
+ role: "user",
106
+ content: [{ type: "text", text: reviewText(options) }],
107
+ },
108
+ ],
109
+ maxTokens: 128,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Map a Qwen3Guard safety label to auto-review's risk/decision protocol.
115
+ *
116
+ * `Safe` allows. `Unsafe` is high risk and denies. `Controversial` is context
117
+ * dependent; this adapter treats it as medium risk and denies so the user can
118
+ * still decide through auto-review's ask path when the downstream policy
119
+ * allows. A missing label fails closed.
120
+ *
121
+ * @param {string} text
122
+ * @returns {{ risk: "low" | "medium" | "high", decision: "allow" | "deny", reason?: string }}
123
+ */
124
+ export function qwen3GuardDecision(text) {
125
+ const label = /Safety:\s*(Safe|Unsafe|Controversial)/i.exec(text)?.[1]?.toLowerCase();
126
+ const categories = /Categories:\s*([^\n]+)/i.exec(text)?.[1]?.trim();
127
+ const detail =
128
+ categories !== undefined && !/^none$/i.test(categories)
129
+ ? `categories: ${categories}`
130
+ : "no category";
131
+
132
+ if (label === "safe") return { risk: "low", decision: "allow" };
133
+ if (label === "controversial") {
134
+ return {
135
+ risk: "medium",
136
+ decision: "deny",
137
+ reason: `Qwen3Guard: Controversial (${detail})`,
138
+ };
139
+ }
140
+ if (label === "unsafe") {
141
+ return {
142
+ risk: "high",
143
+ decision: "deny",
144
+ reason: `Qwen3Guard: Unsafe (${detail})`,
145
+ };
146
+ }
147
+
148
+ const snippet = text.trim().replace(/\s+/g, " ").slice(0, 200);
149
+ return {
150
+ risk: "high",
151
+ decision: "deny",
152
+ reason: snippet === ""
153
+ ? "Qwen3Guard returned no Safety label"
154
+ : `Qwen3Guard returned no Safety label: ${snippet}`,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Consume Qwen3Guard's native text stream and emit the one-text-block JSON
160
+ * stream auto-review's `readDecision` requires.
161
+ *
162
+ * Adapter errors are passed through as their terminal finish chunk so
163
+ * auto-review keeps its existing fail-closed error path. A successful stream
164
+ * becomes `{risk, decision, reason?}`, preceded by no reasoning blocks.
165
+ *
166
+ * @param {AsyncIterable<object>} stream
167
+ * @returns {AsyncGenerator<object>}
168
+ */
169
+ export async function* adaptQwen3GuardStream(stream) {
170
+ let text = "";
171
+ let usage;
172
+ let finish;
173
+
174
+ for await (const chunk of stream) {
175
+ if (chunk === null || typeof chunk !== "object") continue;
176
+ switch (chunk.type) {
177
+ case "text-delta":
178
+ text += typeof chunk.text === "string" ? chunk.text : "";
179
+ break;
180
+ case "usage":
181
+ usage = chunk;
182
+ break;
183
+ case "finish":
184
+ finish = chunk;
185
+ break;
186
+ default:
187
+ break;
188
+ }
189
+ }
190
+
191
+ if (
192
+ finish !== undefined &&
193
+ (finish.reason?.kind === "error" || finish.reason?.kind === "aborted")
194
+ ) {
195
+ yield finish;
196
+ return;
197
+ }
198
+
199
+ const body = JSON.stringify(qwen3GuardDecision(text));
200
+ yield { type: "block-start", index: 0, blockType: "text" };
201
+ yield { type: "text-delta", index: 0, text: body };
202
+ yield { type: "block-end", index: 0, block: { type: "text", text: body } };
203
+ if (usage !== undefined) yield usage;
204
+ yield { type: "finish", reason: { kind: "stop" } };
205
+ }
206
+
207
+ /**
208
+ * Rewrite an Auto review request to the configured reviewer route.
209
+ *
210
+ * Returns `undefined` when the request should continue down the normal
211
+ * waterfall: it is not a review, the target route is incomplete, it already
212
+ * names the configured route under the plain JSON protocol, or it is the
213
+ * re-entered copy created by this function.
214
+ *
215
+ * @param {object} options - immutable LLM request.
216
+ * @param {{ provider?: unknown, model?: unknown, protocol?: unknown }} route - configured route.
217
+ * @param {(request: object) => unknown} dispatch - sends a request to the LLM service.
218
+ * @returns the dispatched stream for a rewritten request, otherwise `undefined`.
219
+ */
220
+ export function routeReview(options, route, dispatch) {
221
+ if (rewritten.has(options)) {
222
+ rewritten.delete(options);
223
+ return undefined;
224
+ }
225
+
226
+ if (!isReviewRequest(options)) return undefined;
227
+
228
+ const provider = String(route.provider ?? "").trim();
229
+ const model = String(route.model ?? "").trim();
230
+ if ((provider === "") !== (model === "")) return undefined;
231
+ const targetProvider = provider === "" ? options.provider : provider;
232
+ const targetModel = model === "" ? options.model : model;
233
+ if (typeof targetProvider !== "string" || typeof targetModel !== "string") {
234
+ return undefined;
235
+ }
236
+ if (targetProvider === "" || targetModel === "") return undefined;
237
+
238
+ const protocol = resolveReviewProtocol(route, targetModel);
239
+ const sameRoute =
240
+ options.provider === targetProvider && options.model === targetModel;
241
+ if (protocol === "json" && sameRoute) return undefined;
242
+
243
+ const next =
244
+ protocol === "qwen3guard"
245
+ ? qwen3GuardRequest(options, targetProvider, targetModel)
246
+ : { ...options, provider: targetProvider, model: targetModel };
247
+ rewritten.add(next);
248
+ const stream = dispatch(next);
249
+ return protocol === "qwen3guard" ? adaptQwen3GuardStream(stream) : stream;
250
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Auto review model",
4
+ "description": "Choose a dedicated reviewer model for Auto review instead of following the main model."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "自动审查模型",
4
+ "description": "为自动审查单独选择审查模型,不再跟随主模型。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "dsh-auto-check",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Give DeepSeek Harness Auto review its own reviewer model instead of following the main model.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json",
11
+ "./locale/*.json": "./locale/*.json",
12
+ "./icon": "./icon.svg"
13
+ },
14
+ "files": [
15
+ "lib/*.js",
16
+ "client.js",
17
+ "locale/*.json",
18
+ "icon.svg",
19
+ "cordis.patch.yml",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/ltzXiaoYanMo/dsh-auto-check.git"
27
+ },
28
+ "homepage": "https://github.com/ltzXiaoYanMo/dsh-auto-check#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/ltzXiaoYanMo/dsh-auto-check/issues"
31
+ },
32
+ "scripts": {
33
+ "test": "node --test test/review-route.test.mjs"
34
+ },
35
+ "dsh": {
36
+ "bundle": {
37
+ "patch": "cordis.patch.yml"
38
+ },
39
+ "client": {
40
+ "platform": "web",
41
+ "inject": [
42
+ "@deepseek-ai/dsh-client-locale",
43
+ "@deepseek-ai/dsh-client-ui-settings",
44
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
45
+ "@deepseek-ai/dsh-api-remotes"
46
+ ]
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "dsh-auto-check": "^0.1.0"
51
+ }
52
+ }