dsh-yolo-mode 0.5.0 → 0.5.2-beta.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/CHANGELOG.md CHANGED
@@ -2,6 +2,92 @@
2
2
 
3
3
  本项目遵循 [Semantic Versioning](https://semver.org/)。
4
4
 
5
+ ## [0.5.2-beta.0] - 2026-09-24
6
+
7
+ ### Beta(DSH 0.1.7-rc.1 适配,测试版)
8
+
9
+ - 本分支(`dev-0.1.7.rc1`,尚未合并 `main`)相对 `main` 的适配改动:设置子系统按
10
+ 0.1.7 的 `SettingsForms` 新模型重写(`lib/settings.js` / `lib/index.js` /
11
+ `lib/remote.js`),`@deepseek-ai/dsh-llm`、`dsh-timeout`、`dsh-settings`、
12
+ `dsh-client-connection`、`dsh-client-ui-slots`、`dsh-client-locale` 的 peer 范围升至
13
+ `^0.1.7-rc.1`,`dsh.compatibility.dshReleases` 新增 `"0.1.7-rc.1": "compatible"`,
14
+ README 徽章与兼容性说明同步。详见下方 `0.5.2` 条目。
15
+ - 发布为 npm 测试版(`--tag beta`);`latest` 保持不变。正式版 `0.5.2` 待 `main` 合并后发布。
16
+
17
+ ## [0.5.2] - 2026-09-24
18
+
19
+ ### 兼容:设置子系统迁移至 DSH 0.1.7-rc.1 的 `SettingsForms`
20
+
21
+ DSH 0.1.7 移除了 `@deepseek-ai/dsh-settings` 的 `SettingsProvider.installSection`
22
+ (默认导出改为 `SettingsForms`),本插件原设置子系统因此在新版抛
23
+ `TypeError: settings.installSection is not a function` 而无法激活。本次按官方新模型重写:
24
+
25
+ - **设置声明 → 插件 Config schema**(`lib/settings.js` 的 `YoloSettingsSchema`,
26
+ 经 `lib/index.js` 的 `export const Config` 暴露):每个顶层字段标注
27
+ `.volatile()`,声明为可热更——写回经 loader 的就地更新,插件无需重启即可生效
28
+ (与旧模型「只改某些字段不重启」行为一致)。
29
+ - **设置读取 → `config.<field>.get()`**(`readYoloConfig`):`effectiveConfig()`
30
+ 每次裁决读取实时解析值(schema 默认 + 条目 config),再 `normalizeConfig`。
31
+ - **页面策略 → `settings.configure({ auto: false }, fiber)`**
32
+ (`installYoloSettingsPage`):插件自带 Web 设置页,`auto:false` 禁止宿主再自动
33
+ 生成重复页面;settings 服务缺失时零侵入跳过。
34
+ - **自发布设置桥与客户端不变**:`lib/remote.js` 的 `settingsView` / `settingsMutate`
35
+ 端点本就建立在 `describe` / `mutate` / `writable` / `SettingsConflictError` 之上,
36
+ 这些方法面在 `SettingsForms` 中保持不变,端点形状与乐观锁语义无需改动,客户端
37
+ 无需改动。
38
+ - **双层 → 单层映射**:旧「插件行 config 为 base 层 + settings.yaml 用户层」映射为
39
+ 0.1.7「Profile 插件条目 config 单层」;旧 `settings.yaml` 由
40
+ `SettingsForms.importLegacyDocument` 一次性导入到 Profile。
41
+
42
+ ### 兼容性
43
+
44
+ - `peerDependencies` 的 `@deepseek-ai/dsh*` 下限提升至 `^0.1.7-rc.1`(`dsh-host-apiproxy`
45
+ / `dsh-client-runtime` 两个历史例外保持 `^0.1.1-rc.2`)。
46
+ - `dsh.compatibility.dshReleases` 增列 `0.1.7-rc.1: compatible`。
47
+ - README 徽章与兼容性行同步至 0.1.7-rc.1。
48
+
49
+ ### 测试
50
+
51
+ - `test/probe.test.mjs` 重写至 0.1.7 API:真实 cordis 经 Config schema 解析条目
52
+ config、断言 `configure({auto:false})` 被调用、并用 cosmokit `updateVolatile`
53
+ (loader `_commitVolatile` 的同一原语)验证 volatile 就地更新后裁决即时生效。
54
+ - 其余测试文件保持通过;`npm test` 132 项全绿。
55
+
56
+ ## [0.5.1] - 2026-09-22
57
+
58
+ ### 修复:裁判对推理型模型恒失败(Issue #1)
59
+
60
+ - **默认 `judge.maxTokens` 256 → 4096**(`lib/policy.js` 默认配置与 `normalizeConfig`
61
+ 合并值、`lib/judge.js` 兜底值同步)。根因:推理型裁判模型把 token 预算全部消耗在
62
+ reasoning 块上,`maxTokens` 过小时 `content` 为空(`finish_reason=length`),裁判抛
63
+ `BAD_OUTPUT` 并按 `error` 回退——`balanced` 预设下表现为「每次都转人工」,而
64
+ `judgeConfigured:true` 掩盖了失败。用户实测 4096 为可用值(256/1024 输出为 0)。
65
+ - **审计条目新增 `error` 字段**(`lib/index.js`):裁判失败时写入 `JudgeError.code`
66
+ (`BAD_OUTPUT` / `TIMEOUT` / `STREAM_ERROR` / `NO_ADAPTER` 等),使
67
+ `outcome:"delegate"` 的条目可区分「裁判失败转人工」与「裁判主动授意转人工」;
68
+ 成功路径字段形状不变(不新增 `error`)。
69
+ - **失败可见**(`lib/state.js`):新增 `stats.judgeFailures` 计数与
70
+ `judgeHealth.lastError/lastErrorTime`,经 `getStatusPayload` 以
71
+ `judgeErrors: { count, lastError?, lastErrorTime? }` 暴露;裁判异常不再仅停留于
72
+ `logger.warn`。
73
+ - **文档**:README 标注推理型模型必须调大 `judge.maxTokens`(默认已改 4096),并补充
74
+ 审计 `error` 字段与 `judgeErrors` 载荷说明。
75
+
76
+ ### 测试
77
+
78
+ - `test/policy.test.mjs`:默认 `maxTokens` 断言更新为 4096。
79
+ - `test/judge.test.mjs`:新增「未显式传 `maxTokens` → 默认 4096 传入 `llm.stream`」用例。
80
+ - `test/state.test.mjs`:新增裁判失败时 `judgeFailures` 递增、`judgeErrors` 载荷装配用例。
81
+ - `test/probe.test.mjs`:新增真实-Cordis 用例——裁判产出非 JSON(`BAD_OUTPUT`)→
82
+ 审计条目带 `error:"BAD_OUTPUT"` 且回退 delegate;成功路径断言 recent 条目**不含** `error`。
83
+
84
+ ### 兼容性
85
+
86
+ - **新增 `dsh.compatibility` 声明**(DSH STORE 上架契约):逐版本声明
87
+ `dshReleases` 兼容矩阵——`0.1.5-rc.1` / `0.1.5-rc.2` / `0.1.6-alpha.2` 均为
88
+ `compatible`(三版本已在本机真实装载运行,插件正常加载、零错误);`node` 范围
89
+ `>=20`,与 `engines.node` 一致。未实测的版本不声明(扫描时按 `unknown` 处理)。
90
+
5
91
  ## [0.5.0] - 2026-09-04
6
92
 
7
93
  ### 兼容:DSH 0.1.2-alpha.4
package/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/dsh-yolo-mode)](https://www.npmjs.com/package/dsh-yolo-mode)
8
8
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
- [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.2--alpha.4-blue)](https://github.com/deepseek-ai/deepseek-harness)
9
+ [![DeepSeek Harness](https://img.shields.io/badge/DSH%200.1.5--rc.1%20%2F%20rc.2%20%2F%200.1.6--alpha.2%20%2F%200.1.7--rc.1-blue)](https://github.com/deepseek-ai/deepseek-harness)
10
10
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
11
11
 
12
- > **兼容性**:v0.5.0 支持 DSH **0.1.2-alpha.4+**(真实-Cordis 探针 125/125 通过;settings 已迁移至 alpha.4 的 `SettingsProvider` 类服务)。旧版 DSH(0.1.0-rc.6 / rc.8)请使用最后兼容的 npm 版本 **0.4.1**。
12
+ > **兼容性**:v0.5.2 支持 DSH **0.1.5-rc.1+**,已在 **0.1.5-rc.1 / 0.1.5-rc.2 / 0.1.6-alpha.2 / 0.1.7-rc.1** 实测(真实-Cordis 探针通过;设置子系统已迁移至 0.1.7 的 `SettingsForms`——插件 Config schema + volatile 热更 + 自建设置页)。旧版 DSH(0.1.0-rc.6 / rc.8)请使用最后兼容的 npm 版本 **0.4.1**。
13
13
 
14
14
  ---
15
15
 
@@ -77,11 +77,13 @@ dsh plugin --profile web add <项目绝对路径>
77
77
  | `judge.model` | `string` | `''` | 裁判模型;与 provider 同非空才启用裁判 |
78
78
  | `judge.systemPrompt` | `string` | `''` | 裁判 system prompt;空 = 按预设取默认 |
79
79
  | `judge.timeoutMs` | `number` | `20000` | 单次裁判超时(毫秒) |
80
- | `judge.maxTokens` | `number` | `256` | 裁判输出最大 token 数 |
80
+ | `judge.maxTokens` | `number` | `4096` | 裁判输出最大 token 数(推理型模型需调大,见下方说明) |
81
81
  | `judge.concurrency` | `number` | `2` | 并发裁判上限,溢出按错误回退 |
82
82
  | `includeSubagents` | `boolean` | `true` | 子代理会话是否同样裁决 |
83
83
  | `auditFile` | `string` | `''` | 审计日志路径;空 = `%TEMP%/dsh-yolo/judge.log` |
84
84
 
85
+ > ⚠️ **推理型模型必须调大 `judge.maxTokens`**:若裁判模型会先输出一段内部推理(reasoning / chain-of-thought)再给出结论 JSON,则 token 预算会被推理内容消耗;预算过小时 `content` 为空、`finish_reason=length`,裁判以 `BAD_OUTPUT` 失败并回退为「转人工」(默认预设 `balanced` 下表现为**每次都弹人工审批**)。默认值已从 `256` 提升至 **`4096`**;若仍遇到回退,请继续调大(如 `8192`)。审计日志中此类失败会带 `error: "BAD_OUTPUT"`。
86
+
85
87
  ### 权限层级(`levels`)
86
88
 
87
89
  ```yaml
@@ -130,7 +132,8 @@ levels:
130
132
  - **防回环**:裁判 prompt 与 agent 上下文隔离,防止模型借 Web 审批回环自批准 `danger-full-access`。
131
133
  - **不改写策略**:仅在 `ask` 策略下作为应答者,不改变 DSH 的沙箱 / 审批词汇。
132
134
  - **默认保守**:默认预设 `balanced`(不确定转人工),不默认启用 `permissive` / `yolo`。
133
- - **审计**:每次裁决落一行 JSONL,含 `{time, sessionId, origin, toolName, callId?, targetMode, currentMode, justification, decision, outcome, reason?}`。
135
+ - **审计**:每次裁决落一行 JSONL,含 `{time, sessionId, origin, toolName, callId?, targetMode, currentMode, justification, decision, outcome, reason?, error?}`。`error` 仅在**裁判失败**时出现(值为 `JudgeError` 错误码,如 `BAD_OUTPUT` / `TIMEOUT` / `STREAM_ERROR` / `NO_ADAPTER`);`outcome:"delegate"` 且有 `error`=裁判失败转人工,无 `error`=裁判主动转人工,二者可区分。
136
+ - **失败可见**:statusView 载荷携带 `judgeErrors: { count, lastError?, lastErrorTime? }`(累计裁判失败次数与最近一次错误码),裁判异常不再只停留在 `logger.warn`。
134
137
 
135
138
  ## 开发
136
139
 
package/lib/index.js CHANGED
@@ -5,10 +5,11 @@
5
5
  * 抢在 dsh-host-apiproxy 的人工应答者之前介入,对沙箱升权申请(
6
6
  * `escalate sandbox to <mode>: <justification>`)按预设策略自动裁决。
7
7
  *
8
- * v0.3.0 重做(design.md §12.3):
8
+ * v0.3.0 重做(design.md §12.3);v0.5.2 迁移至 0.1.7 的 SettingsForms:
9
9
  * - `inject: ['llm','settings']`,用 `ctx.llm` / `ctx.settings`;
10
- * - 经 `installYoloSettings` 注册 settings 分区,`effectiveConfig()` 每次裁决
11
- * 时取 resolved(默认 + base(行配置) + 用户层)后 `normalizeConfig`;
10
+ * - 设置声明为本插件的 Config schema(`export const Config`,volatile 字段可热更),
11
+ * `effectiveConfig()` 每次裁决用 `config.<field>.get()` 读取实时值后 `normalizeConfig`;
12
+ * - 页面策略经 `installYoloSettingsPage`(`settings.configure({auto:false})`)注册;
12
13
  * - `getJudge()` 按 judge 配置键缓存裁判实例(用 `ctx.llm`),键变化重建;
13
14
  * - 审计统计写入 lib/state.js 模块级单例(bridge 条目已读);
14
15
  * - **不再注册任何 webServer 路由**(由独立 lib/bridge-entry.js 承担)。
@@ -28,13 +29,17 @@ import {
28
29
  ESCALATION_RE,
29
30
  } from './policy.js';
30
31
  import { createJudge, defaultJudgePromptFor } from './judge.js';
31
- import { installYoloSettings, validateYoloSettings, pruneEmpty } from './settings.js';
32
+ import { YoloSettingsSchema, installYoloSettingsPage, readYoloConfig, pruneEmpty } from './settings.js';
32
33
  import { recordDecision, sessionOrigin } from './state.js';
33
34
  import { resolveAuditFile } from './audit.js';
34
35
 
35
36
  export const name = 'dsh-yolo-mode';
36
37
 
37
- /** settings 命名空间(与插件行 id 一致;纯小写 kebab-case)。主条目经 installYoloSettings 使用。 */
38
+ /**
39
+ * 插件 Config schema(0.1.7 起即权威设置 schema;条目 id `yolo-mode` 即
40
+ * `describe()`/`mutate()` 的 ns)。cordis 从本导出读取并用其解析条目 config。
41
+ */
42
+ export const Config = YoloSettingsSchema;
38
43
 
39
44
  /** 反向扫描会话事件,取与 req.callId 匹配的 tool/call 实参摘要(1200 字符截断)。 */
40
45
  function extractArgumentsSummary(req, session) {
@@ -76,35 +81,22 @@ export const inject = ['llm', 'settings'];
76
81
  * @param {import('@deepseek-ai/cordis').Context} ctx
77
82
  * @param {object} [rawConfig] 插件行 config(全字段可选)
78
83
  */
79
- export function apply(ctx, rawConfig) {
80
- // 行配置通过(fail-loud):非法配置立即抛错 → 加载失败。
81
- const rowCfg = rawConfig ?? {};
82
- normalizeConfig(rowCfg);
83
-
84
+ export function apply(ctx, config) {
84
85
  const logger = ctx.logger('yolo-mode');
85
86
 
86
- // ---- settings 分区(resolved = defaults + base(行配置) + 用户层)----
87
- // installSettingsSection 在 settings 服务存在时,把 resolved scope 设为 active 源;
88
- // 服务缺失/卸载时回退到 `entry`(插件行 config)。
89
- let sourceThunk = undefined;
90
- installYoloSettings(ctx, rowCfg, {
91
- setSource: (thunk) => {
92
- sourceThunk = thunk;
93
- },
94
- onChange: () => {
95
- // resolved 已由 setSource 的 thunk 覆盖;judge 缓存按键在 getJudge 中自愈。
96
- },
97
- validate: validateYoloSettings,
98
- });
99
-
100
- /** 每次裁决读取的有效配置(resolved settings;未就绪时插件行 config)后规范化。 */
87
+ /** 每次裁决读取的有效配置(Config schema 默认 + 条目 config 实时值)后规范化。 */
101
88
  function effectiveConfig() {
102
- const raw = sourceThunk ? sourceThunk() : rowCfg;
103
- // resolved 含 schema 空集合默认(modes:[]/levels:{}),必须先剔除再 normalizeConfig,
104
- // 否则 fail-loud 拒绝空 modes(与 settings.js 的 validate 钩子同一根因)。
105
- return normalizeConfig(pruneEmpty(raw));
89
+ // 解析值含 schema 空集合默认(modes:[]/levels:{}),必须先剔除再 normalizeConfig,
90
+ // 否则 fail-loud 拒绝空 modes(与 settings.js 的写时校验同一根因)。
91
+ return normalizeConfig(pruneEmpty(readYoloConfig(config)));
106
92
  }
107
93
 
94
+ // 初始配置 fail-loud:非法配置立即抛错 → 加载失败(与旧 apply 行为一致)。
95
+ effectiveConfig();
96
+
97
+ // ---- settings 页面策略(0.1.7 SettingsForms;auto:false = 自建 UI)----
98
+ installYoloSettingsPage(ctx);
99
+
108
100
  const auditFile = () => resolveAuditFile(effectiveConfig());
109
101
 
110
102
  let dirEnsured = false;
@@ -247,13 +239,17 @@ export function apply(ctx, rawConfig) {
247
239
 
248
240
  // 7. 裁决映射(judge 走裁判,含未配置/失败/不确定回退)。
249
241
  const judge = getJudge();
250
- let judgeReason; // 裁判 reason 仅在 judge 路径产出时记录(审计 reason?)。
242
+ let judgeReason; // 裁判 reason 仅在 judge 路径产出时记录(审计 reason 字段)。
243
+ let judgeError; // 裁判失败的错误码(审计 error 字段;成功/非 judge 路径为 undefined)。
251
244
  const result = await (async () => {
252
245
  if (decision === 'allow') return { outcome: 'allowed-once' };
253
246
  if (decision === 'deny') return { outcome: 'rejected' };
254
247
  if (decision === 'delegate') return { delegate: true };
255
248
  // decision === 'judge'
256
- if (!judge) return fallback('error', cfg);
249
+ if (!judge) {
250
+ judgeError = 'NO_ADAPTER'; // provider/model 未配置:与 judge.js 的 NO_ADAPTER 同语义
251
+ return fallback('error', cfg);
252
+ }
257
253
  try {
258
254
  const r = await judge({
259
255
  toolName: req.toolName,
@@ -268,6 +264,7 @@ export function apply(ctx, rawConfig) {
268
264
  if (r.decision === 'deny') return { outcome: 'rejected' };
269
265
  return fallback('unsure', cfg); // 不确定
270
266
  } catch (err) {
267
+ judgeError = (err && typeof err === 'object' && err.code) ? String(err.code) : 'UNKNOWN';
271
268
  logger.warn('LLM 裁判失败,按预设 error 回退', errorDescriptor(err));
272
269
  return fallback('error', cfg);
273
270
  }
@@ -275,7 +272,8 @@ export function apply(ctx, rawConfig) {
275
272
 
276
273
  const outcome = result.delegate ? 'delegate' : result.outcome;
277
274
 
278
- // 8. 审计。
275
+ // 8. 审计。裁判失败时带 error 字段(区分「裁判失败」与「裁判主动转人工」);
276
+ // 成功路径字段形状不变(不新增 error)。
279
277
  audit({
280
278
  time: Date.now(),
281
279
  sessionId: (session && session.id) || (req.agent && req.agent.id),
@@ -288,6 +286,7 @@ export function apply(ctx, rawConfig) {
288
286
  decision,
289
287
  outcome,
290
288
  reason: judgeReason,
289
+ ...(judgeError !== undefined ? { error: judgeError } : {}),
291
290
  });
292
291
 
293
292
  // 9. delegate → next() 透明委托;否则返回归一化 outcome。
package/lib/judge.js CHANGED
@@ -151,7 +151,7 @@ function throwAbort(signal, upstream) {
151
151
  * @param {string} opts.model model id
152
152
  * @param {string} [opts.systemPrompt] 空/缺省 → 内置裁判 prompt(含防回环要求)
153
153
  * @param {number} [opts.timeoutMs=20000] 单次裁判调用超时(毫秒)
154
- * @param {number} [opts.maxTokens=256] 最大输出 token
154
+ * @param {number} [opts.maxTokens=4096] 最大输出 token(推理型模型需调大,见 README)
155
155
  * @param {number} [opts.concurrency=2] 信号量上限
156
156
  * @param {AbortSignal} [opts.signal] 上游取消信号(ABORTED 时中止;可空)
157
157
  * @returns {Function} async judge(input) -> {{decision:'allow'|'deny'|'unsure', reason:string}}
@@ -159,7 +159,7 @@ function throwAbort(signal, upstream) {
159
159
  export function createJudge({ llm, provider, model, systemPrompt, timeoutMs, maxTokens, concurrency, signal }) {
160
160
  const sys = typeof systemPrompt === 'string' && systemPrompt.trim() !== '' ? systemPrompt : DEFAULT_SYSTEM_PROMPT
161
161
  const ms = isPositiveInt(timeoutMs) ? timeoutMs : 20000
162
- const mt = isPositiveInt(maxTokens) ? maxTokens : 256
162
+ const mt = isPositiveInt(maxTokens) ? maxTokens : 4096
163
163
  const cap = isPositiveInt(concurrency) ? concurrency : 2
164
164
 
165
165
  // 信号量计数(活跃调用数)。进入者先同步占位,用后的 try/finally 释放。
package/lib/policy.js CHANGED
@@ -87,7 +87,7 @@ function defaultConfig() {
87
87
  preset: 'balanced',
88
88
  modes: ['workspace-write'],
89
89
  levels: {},
90
- judge: { provider: '', model: '', systemPrompt: '', timeoutMs: 20000, maxTokens: 256, concurrency: 2 },
90
+ judge: { provider: '', model: '', systemPrompt: '', timeoutMs: 20000, maxTokens: 4096, concurrency: 2 },
91
91
  includeSubagents: true,
92
92
  auditFile: '',
93
93
  }
@@ -187,7 +187,7 @@ export function normalizeConfig(raw) {
187
187
  model: jraw.model === undefined ? '' : jraw.model,
188
188
  systemPrompt: jraw.systemPrompt === undefined ? '' : jraw.systemPrompt,
189
189
  timeoutMs: jraw.timeoutMs === undefined ? 20000 : jraw.timeoutMs,
190
- maxTokens: jraw.maxTokens === undefined ? 256 : jraw.maxTokens,
190
+ maxTokens: jraw.maxTokens === undefined ? 4096 : jraw.maxTokens,
191
191
  concurrency: jraw.concurrency === undefined ? 2 : jraw.concurrency,
192
192
  }
193
193
  for (const field of ['provider', 'model', 'systemPrompt']) {
package/lib/remote.js CHANGED
@@ -28,10 +28,11 @@
28
28
  /**
29
29
  * 纯映射 / 线信封辅助在顶部(无 cordis),可在普通 node 环境单测。
30
30
  *
31
- * DSH 0.1.2-alpha.4 兼容:dsh-settings 的 `settingsNamespace()` 品牌函数已
32
- * 删除,命名空间为纯 kebab-case 字符串字面量(`yolo-mode`);`mutate` /
33
- * `describe` / `writable` 仍为 `SettingsProvider` 实例方法,
34
- * `SettingsConflictError` 导出保留(code 'SETTINGS_CONFLICT')。
31
+ * DSH 0.1.7-rc.1 兼容:dsh-settings 默认导出由 `SettingsProvider` 改为
32
+ * `SettingsForms`,但其 `describe` / `writable` / `mutate` / `replace` /
33
+ * `update` 方法面与 `SettingsConflictError`(code 'SETTINGS_CONFLICT')不变,
34
+ * 故本桥的线协议与端点形状无需改动,仅内部实现改由 `SettingsForms` 承担。
35
+ * 命名空间仍为纯 kebab-case 字符串字面量(`yolo-mode`,等于插件条目 id)。
35
36
  *
36
37
  * @module lib/remote.js
37
38
  */
@@ -247,8 +248,8 @@ function yoloConflict(conflict) {
247
248
  /**
248
249
  * 对 settings seam 执行一次路径 op 变更并映射为 RpcResult(携带新 redacted view,
249
250
  * 或 settings-conflict / settings-rejected 错误)。纯函数可注入 mutate/describe 测试。
250
- * alpha.4 起命名空间为纯 kebab-case 字符串(`settingsNamespace()` 品牌已删除),
251
- * `ns` 原样透传给 SettingsProvider.mutate(其内部按 `^[a-z][a-z0-9-]*$` 校验)。
251
+ * 0.1.7 起 `ns` 即插件条目 id,原样透传给 SettingsForms.mutate;非 volatile 路径
252
+ * 会被其拒绝(映射为 settings-rejected)。
252
253
  * @param {Function} mutate settings.mutate 绑定
253
254
  * @param {Function} describe settings.describe 绑定
254
255
  * @param {string} ns 命名空间字符串
package/lib/settings.js CHANGED
@@ -5,14 +5,18 @@
5
5
  * `yolo-mode`,用 schemastery 宽松 schema 解析分区值,写时校验委托
6
6
  * ./policy.js 的 `normalizeConfig`(fail-loud 抛错即拒绝写入)。
7
7
  *
8
- * 分层层(design.md §12.3):插件行 config 作为 settings 的 `base` 层,
9
- * 用户文档写在上层;`installYoloSettings` 走 `SettingsProvider.installSection`
10
- * 的可选-settings 消费者接线,部署无 settings 服务时打 debug 日志跳过(零侵入)。
8
+ * 分层层(design.md §12.3 + 0.1.7 单层迁移):0.1.7 起设置不再由
9
+ * settings.yaml 覆盖层承载,而是**当前 Profile 的插件条目 config 单层**——
10
+ * schemastery Config schema 的默认值即 base 层,用户在设置页的改动写回该
11
+ * 条目 config。故本模块导出的 `YoloSettingsSchema` 同时是插件 Config schema
12
+ * (见 lib/index.js 的 `export const Config`),每个可热更字段用 `.volatile()`
13
+ * 声明;读取用 `config.<field>.get()`。
11
14
  *
12
- * DSH 0.1.2-alpha.4 兼容:dsh-settings 改为类服务——默认导出 `SettingsProvider`,
13
- * `installSettingsSection` / `settingsNamespace` 函数导出已删除;命名空间改为
14
- * 纯 kebab-case 字符串字面量(模板校验 `^[a-z][a-z0-9-]*$`),消费接线改为
15
- * `ctx.settings.installSection(owner, ns, schema, entry, hooks)`。
15
+ * DSH 0.1.7-rc.1 兼容:dsh-settings 默认导出由 `SettingsProvider`
16
+ * (`installSection`)改为 `SettingsForms`(`describe/update/replace/mutate`)。
17
+ * `installSection` 已删除;页面策略改走
18
+ * `ctx.settings.configure({ auto }, ownerFiber)`(auto:false = 插件自建 UI)。
19
+ * 旧的 `settings.yaml` 由 `SettingsForms.importLegacyDocument` 一次性导入。
16
20
  *
17
21
  * 纯 JavaScript(ESM),宿主代码仅 import node: 内置与同包 peer
18
22
  * (@deepseek-ai/dsh-settings、@deepseek-ai/schemastery)。
@@ -29,26 +33,53 @@ export const YOLO_SETTINGS_NAMESPACE = 'yolo-mode';
29
33
  const PRESETS = ['off', 'strict', 'balanced', 'permissive', 'yolo', 'custom'];
30
34
 
31
35
  /**
32
- * schemastery 宽松 schema:全字段可选(object 字段默认可选),
33
- * 使未写入的空白分区解析为空对象,绝不泄漏默认值覆盖插件行 config。
34
- * 真正的形状/语义校验由写时 `validateYoloSettings = normalizeConfig` 完成(fail-loud)。
36
+ * schemastery Config schema(0.1.7 起同时是插件的权威设置 schema):全字段可选
37
+ * (object 字段默认可选),使未写入的空白分区解析为空对象,绝不泄漏默认值覆盖
38
+ * 条目 config 的普通层。每个顶层字段用 `.volatile()` 声明为可热更——设置页的
39
+ * 写回经 loader 的 `_commitVolatile` 就地更新引用,插件无需重启;这与旧模型
40
+ * "只改某些字段不重启插件" 的行为一致。
41
+ *
42
+ * 注意:`.volatile()` 不可嵌套(schemastery 校验"volatile 字段需固定对象路径"),
43
+ * 故只标注顶层字段;其子路径由 `isVolatilePath` 视作可热更。
44
+ *
45
+ * 真正的形状/语义校验由写时 `validateYoloSettings = normalizeConfig` 完成(fail-loud)
46
+ * 以及 schemastery 在 configEditor 写回时的解析校验。
35
47
  */
36
48
  export const YoloSettingsSchema = z.object({
37
- preset: z.union([...PRESETS]).default('balanced'),
38
- modes: z.array(z.string()),
39
- levels: z.dict(z.any()),
40
- judge: z.object({
41
- provider: z.string(),
42
- model: z.string(),
43
- systemPrompt: z.string(),
44
- timeoutMs: z.natural(),
45
- maxTokens: z.natural(),
46
- concurrency: z.natural(),
47
- }),
48
- includeSubagents: z.boolean(),
49
- auditFile: z.string(),
49
+ preset: z.union([...PRESETS]).default('balanced').volatile(),
50
+ modes: z.array(z.string()).volatile(),
51
+ levels: z.dict(z.any()).volatile(),
52
+ judge: z
53
+ .object({
54
+ provider: z.string(),
55
+ model: z.string(),
56
+ systemPrompt: z.string(),
57
+ timeoutMs: z.natural(),
58
+ maxTokens: z.natural(),
59
+ concurrency: z.natural(),
60
+ })
61
+ .volatile(),
62
+ includeSubagents: z.boolean().volatile(),
63
+ auditFile: z.string().volatile(),
50
64
  });
51
65
 
66
+ /**
67
+ * 从已解析的 Config 读取当前设置值(0.1.7:`config.<field>.get()`)。
68
+ * volatile 引用由 loader 就地更新,故每次裁决读取即为最新值。
69
+ * @param {object} config apply(ctx, config) 收到的已解析 Config
70
+ * @returns {object} 仅含 schema 字段的普通对象(未设字段为 undefined/空集合)
71
+ */
72
+ export function readYoloConfig(config) {
73
+ return {
74
+ preset: config.preset.get(),
75
+ modes: config.modes.get(),
76
+ levels: config.levels.get(),
77
+ judge: config.judge.get(),
78
+ includeSubagents: config.includeSubagents.get(),
79
+ auditFile: config.auditFile.get(),
80
+ };
81
+ }
82
+
52
83
  /**
53
84
  * 剔除 schema 解析出的"空集合默认值"(schemastery 对 array/dict 缺省解析为
54
85
  * []/{},标量缺省为 absent),避免把这些默认空值当成用户配置喂给
@@ -88,20 +119,17 @@ export function validateYoloSettings(value) {
88
119
  }
89
120
 
90
121
  /**
91
- * 安装 yolo-mode settings 分区(可选-settings 消费者接线)。
92
- * settings 服务缺失时打 debug 日志并跳过(插件继续按插件行 config 运行)。
93
- * @param {import('@deepseek-ai/cordis').Context} ctx
94
- * @param {object} entry 插件行 config(成为 settings `base` 层)
95
- * @param {import('@deepseek-ai/dsh-settings').SettingsSectionHooks<object>} hooks
96
- * { setSource, onChange, validate? }
122
+ * 注册 yolo-mode 的设置页面策略(0.1.7 `SettingsForms.configure`)。
123
+ *
124
+ * 插件自带 Web 客户端设置页(slot `settings.section`),故 `auto:false`
125
+ * 禁止宿主再自动生成重复页面;页面策略注册到本插件 fiber 的 effects,
126
+ * 卸载时自动注销。settings 服务缺失时 `ctx.inject` 回调不运行(零侵入,
127
+ * 插件继续按 Config schema 默认 + 条目 config 运行)。
128
+ *
129
+ * @param {import('@deepseek-ai/cordis').Context} ctx 主插件 fiber 的上下文
97
130
  */
98
- export function installYoloSettings(ctx, entry, hooks) {
99
- const settings = ctx.get('settings');
100
- if (settings === undefined) {
101
- ctx.logger.debug(
102
- '[yolo-mode] no settings service mounted; using composition config and skipping settings section registration',
103
- );
104
- return;
105
- }
106
- settings.installSection(ctx, YOLO_SETTINGS_NAMESPACE, YoloSettingsSchema, entry, hooks);
131
+ export function installYoloSettingsPage(ctx) {
132
+ ctx.inject(['settings'], (child) => {
133
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
134
+ });
107
135
  }
package/lib/state.js CHANGED
@@ -20,8 +20,13 @@ import { resolveAuditFile } from './audit.js'
20
20
  /** recent 环形缓冲上限(design.md §11.1/12.4,≤20)。 */
21
21
  export const RECENT_CAP = 20;
22
22
 
23
- /** 模块级统计计数(设计 §3.8/12.3;主条目内存写,桥接条目只读)。 */
24
- export const stats = { total: 0, allowed: 0, rejected: 0, delegated: 0 };
23
+ /** 模块级统计计数(设计 §3.8/12.3;主条目内存写,桥接条目只读)。
24
+ * judgeFailures 统计裁判失败次数(审计条目带 error 字段的次数),使「裁判挂了」
25
+ * 在状态面板可见、区别于「裁判主动转人工」。 */
26
+ export const stats = { total: 0, allowed: 0, rejected: 0, delegated: 0, judgeFailures: 0 };
27
+
28
+ /** 最近一次裁判失败的健康信息(code + 时间戳);无失败时 lastError 为 undefined。 */
29
+ export const judgeHealth = { lastError: undefined, lastErrorTime: undefined };
25
30
 
26
31
  /** recent 环形缓冲:新条目 unshift 到头部,超过上限截断尾部(倒序,≤RECENT_CAP)。 */
27
32
  const recent = [];
@@ -55,7 +60,8 @@ export function sessionOrigin(session) {
55
60
  * 记录一次裁决到模块级统计与 recent 环(主条目 audit() 调用)。
56
61
  * @param {object} entry 审计条目:
57
62
  * { time, sessionId, origin, toolName?, callId?, targetMode, currentMode?,
58
- * justification, decision, outcome, reason? }
63
+ * justification, decision, outcome, reason?, error? }
64
+ * error 仅在裁判失败时存在(JudgeError.code);成功路径不携带该字段。
59
65
  */
60
66
  export function recordDecision(entry) {
61
67
  recent.unshift({
@@ -65,6 +71,7 @@ export function recordDecision(entry) {
65
71
  decision: entry.decision,
66
72
  outcome: entry.outcome,
67
73
  ...(entry.reason !== undefined ? { reason: entry.reason } : {}),
74
+ ...(entry.error !== undefined ? { error: entry.error } : {}),
68
75
  });
69
76
  if (recent.length > RECENT_CAP) recent.length = RECENT_CAP;
70
77
 
@@ -72,13 +79,18 @@ export function recordDecision(entry) {
72
79
  if (entry.outcome === 'delegate') stats.delegated += 1;
73
80
  else if (entry.outcome === 'allowed-once') stats.allowed += 1;
74
81
  else if (entry.outcome === 'rejected') stats.rejected += 1;
82
+ if (entry.error !== undefined) {
83
+ stats.judgeFailures += 1;
84
+ judgeHealth.lastError = entry.error;
85
+ judgeHealth.lastErrorTime = entry.time;
86
+ }
75
87
  }
76
88
 
77
89
  /**
78
90
  * 以浅拷贝装配 statusView 载荷(design.md §12.4)。
79
91
  * @param {object} cfg 含 { preset, judge?: { provider?, model? }, auditFile? } 的配置字形
80
92
  * (主条目传 effectiveConfig();桥接条目传 settings.describe 推导的 view 字形)
81
- * @returns {{preset:string, judgeConfigured:boolean, presetDefaults:object, stats:object, recent:Array<object>, auditFile:string}}
93
+ * @returns {{preset:string, judgeConfigured:boolean, presetDefaults:object, stats:object, judgeErrors:object, recent:Array<object>, auditFile:string}}
82
94
  */
83
95
  export function getStatusPayload(cfg) {
84
96
  const c = cfg && typeof cfg === 'object' ? cfg : {};
@@ -88,6 +100,13 @@ export function getStatusPayload(cfg) {
88
100
  judgeConfigured: Boolean(j.provider && j.model),
89
101
  presetDefaults,
90
102
  stats: { ...stats },
103
+ // 裁判失败健康信息:累计失败次数 + 最近一次错误码/时间(无失败时 lastError 缺省)。
104
+ judgeErrors: {
105
+ count: stats.judgeFailures,
106
+ ...(judgeHealth.lastError !== undefined
107
+ ? { lastError: judgeHealth.lastError, lastErrorTime: judgeHealth.lastErrorTime }
108
+ : {}),
109
+ },
91
110
  recent: recent.map((r) => ({ ...r })),
92
111
  // 生效的审计日志路径(resolveAuditFile 与主条目 audit() 同一解析规则),
93
112
  // 供客户端「打开日志」按钮展示/复用。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-yolo-mode",
3
- "version": "0.5.0",
3
+ "version": "0.5.2-beta.0",
4
4
  "description": "dsh-yolo-mode —— DeepSeek Harness 双面包插件:当会话处于可写沙箱模式且审批策略为 ask 时,用大模型自动裁决沙箱升权申请,支持内置预设与自定义权限层级,并提供宿主 settings + 自发布设置桥(/yolo-mode)与 Web 客户端 UI。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -41,22 +41,22 @@
41
41
  "peerDependencies": {
42
42
  "react": "^18.2.0",
43
43
  "@deepseek-ai/cordis": "^4.0.1",
44
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.4",
45
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.4",
46
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.4",
44
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
45
+ "@deepseek-ai/dsh-timeout": "^0.1.7-rc.1",
46
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
47
47
  "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.2",
48
48
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
49
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.4",
50
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.4",
51
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.4",
49
+ "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
50
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
51
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
52
52
  "@deepseek-ai/schemastery": "^3.18.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "rolldown": "^1.2.4",
56
56
  "@deepseek-ai/cordis": "^4.0.2",
57
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.4",
58
- "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.4",
59
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.4",
57
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
58
+ "@deepseek-ai/dsh-timeout": "^0.1.7-rc.1",
59
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
60
60
  "@deepseek-ai/schemastery": "^3.18.1"
61
61
  },
62
62
  "scripts": {
@@ -76,6 +76,15 @@
76
76
  "connection",
77
77
  "remote"
78
78
  ]
79
+ },
80
+ "compatibility": {
81
+ "dshReleases": {
82
+ "0.1.5-rc.1": "compatible",
83
+ "0.1.5-rc.2": "compatible",
84
+ "0.1.6-alpha.2": "compatible",
85
+ "0.1.7-rc.1": "compatible"
86
+ },
87
+ "node": ">=20"
79
88
  }
80
89
  }
81
- }
90
+ }