@yangdcm/dsh-expert-team 1.2.2 → 1.3.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 +54 -3
- package/README.en.md +40 -12
- package/README.md +32 -9
- package/client.js +96 -25
- package/lib/command.js +76 -1
- package/lib/settings.js +1 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
|
|
4
4
|
`package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
|
|
5
5
|
|
|
6
|
+
## 1.3.0
|
|
7
|
+
|
|
8
|
+
**设置页搬进官方「设置」菜单 + 预设在插件加载时就位 + 门禁两处修复**
|
|
9
|
+
|
|
10
|
+
### 设置页进官方设置菜单(`settings.section`)
|
|
11
|
+
|
|
12
|
+
- `client.js` 新增 `SettingsSection` 组件并通过 `ctx.slots.inject('settings.section', …)` 注册
|
|
13
|
+
(`id: expert-team`、`order: 50`、label 走语言 thunk)⇒ 设置出现在 **设置 →「专家团」**,
|
|
14
|
+
与其它插件的设置同一入口、同一套面板 chrome。
|
|
15
|
+
- **浮层第 5 个「设」页签已移除**:同一份表单不再两处渲染。
|
|
16
|
+
- 样式改由 `ensureCss()` 在 `apply()` 里注入 —— 官方设置菜单**可以在没有会话时打开**,
|
|
17
|
+
而原实现只在 HeaderButton 渲染路径注入 CSS ⇒ 那种情况下面板是一堆裸控件。
|
|
18
|
+
- 读设置失败改为**显式报错 + 重试按钮**(旧写法把 404 静默变成 null ⇒ 永久停在"正在读取设置…")。
|
|
19
|
+
- 数据层不变:宿主命名空间 `expert-team`(随插件市场的备份/恢复走;改值后上限/轮次/档位门即时重算)。
|
|
20
|
+
- **诚实边界**:`默认班底`(角色 id 数组)刻意不上宿主 schema(类型表达不可靠),
|
|
21
|
+
继续由该页里的对应控件与 `$DSH_HOME/expert-team/settings.json` 负责。
|
|
22
|
+
|
|
23
|
+
### 预设在插件加载时就位(修一次真实事故)
|
|
24
|
+
|
|
25
|
+
- `apply()` 里 `void ensurePresetInstalled()` / `void ensureSkillInstalled()`:
|
|
26
|
+
**插件装着 ⇒ 它的 preset 就该在位**(失败只 warn,绝不影响插件加载)。
|
|
27
|
+
- 事故:`/team uninstall` 按设计回收了 `$DSH_HOME/.agent-presets/expert-team`,而铺设此前只发生在
|
|
28
|
+
`/team <任务>` 时 ⇒ **使用该预设的会话当场丢掉 12 个角色工具**(`subagent_pm`…),
|
|
29
|
+
选择器里那条也退化成裸 id(只剩标准模式的描述)。现在加载即铺;对带我们版本戳的目录还会
|
|
30
|
+
**整目录重铺** ⇒ 该状态可自愈;对"无戳且内容不像我们"的同名目录仍**不覆盖**(尊重用户内容)但会出声告警。
|
|
31
|
+
- `/team uninstall` 输出补契约说明:插件仍装着时,下次加载会重新铺一份;真正的卸载是在插件市场移除插件。
|
|
32
|
+
|
|
33
|
+
### 门禁修复(`scripts/check-sync.mjs`)
|
|
34
|
+
|
|
35
|
+
- **普通模式崩溃**:`ABSENT_OPTIONAL`(可选映射目标不存在)此前落进读 `onlySrc/onlyDst/differ` 的分支
|
|
36
|
+
⇒ `TypeError`;而 skill 是第一个映射,于是整份报告**一行都打不出来**。现在四种状态各有显式出口。
|
|
37
|
+
- `preset` 映射改为**可选**(全新安装、刚 uninstall 都属正常缺席;**存在时照旧逐文件比对**,
|
|
38
|
+
仍能抓"改了源码没同步")。
|
|
39
|
+
- `.expert-team-version`(归属标记、不是包内资产)计入忽略项 —— 否则 1.3.0 起每个用户带戳后
|
|
40
|
+
`gate:sync` 会假红。
|
|
41
|
+
|
|
42
|
+
## 1.2.3
|
|
43
|
+
|
|
44
|
+
**给 lead 工具面收窄加开关**(因为 1.2.2 让一个从未生效的行为突然生效了)
|
|
45
|
+
|
|
46
|
+
- 背景:A 线"把执行类工具从 lead 手上拿走"此前因取错作用域**从未真正生效**,1.2.2 修好后它会
|
|
47
|
+
立刻改变日常形态 —— expert-team 会话的 lead 不再持有 `bash/write/edit/grep/glob`。
|
|
48
|
+
这是设计意图,但**没有开关的行为变更不该只留一个"忍着或回退版本"的选项**。
|
|
49
|
+
- 新增开关 `gates.leadToolFace`(`on`/`off`,默认 `on`):优先级与档位门一致
|
|
50
|
+
**`config.leadToolFace` > `DSH_EXPERT_TEAM_LEAD_TOOLFACE` > 设置(官方面板/浮层) > 默认 on**;
|
|
51
|
+
非法值一律回默认 on(不猜、不报错 —— 与档位门同一条纪律)。
|
|
52
|
+
- 关掉时**出声一次**(`[expert-team] lead 工具面收窄已关闭…`):否则"我明明关了"与
|
|
53
|
+
"开关没生效"看起来一模一样。
|
|
6
54
|
## 1.2.2
|
|
7
55
|
|
|
8
56
|
**修掉"lead 工具面收窄静默失效"**(拿真机启动日志换来的)
|
|
@@ -33,9 +81,12 @@
|
|
|
33
81
|
### 设置接进宿主命名空间
|
|
34
82
|
|
|
35
83
|
- 插件加载时把设置注册为宿主命名空间 `expert-team`(`ctx.inject(['settings'], …)` 作为优雅降级边界):
|
|
36
|
-
-
|
|
37
|
-
`lib/settings.js` 的 `SETTINGS_SPEC`
|
|
38
|
-
|
|
84
|
+
- **数据层已就位**:设置由宿主服务持有,随插件市场的备份/恢复走;schema 由
|
|
85
|
+
`lib/settings.js` 的 `SETTINGS_SPEC` **生成**(字段/范围/默认值单一真源,不抄第二份)。
|
|
86
|
+
⚠️ **更正(2026-09-14 实机核验)**:本节先前写的"出现在 设置 → 插件 → 插件配置"当时**并不成立** ——
|
|
87
|
+
那张页面渲染的是「宿主服务的命名空间 ∩ 已注册卡片」的交集,我们只交付了数据层。
|
|
88
|
+
→ **已于 1.3.0 解决**:设置页走 `settings.section` 槽,作为 **设置 →「专家团」** 一整页出现
|
|
89
|
+
(不是 dshmarket 那种"插件配置"卡片),见 1.3.0 段。
|
|
39
90
|
- 改动**即时生效**:`scope.watch()` → 用同一份 `config` 重算容量上限/轮次上限/档位门,不必重启。
|
|
40
91
|
- `base` 用**当前生效设置**填充 ⇒ 面板一打开看到的就是真正在用的值,**不需要任何一次性迁移**;
|
|
41
92
|
优先级不变:`schema 默认 < 我们传的 base < 官方面板用户层`。
|
package/README.en.md
CHANGED
|
@@ -54,17 +54,18 @@ The roster is trimmed per task; small jobs start only the roles they need.
|
|
|
54
54
|
`subagent` tool with personas written into the prompt — nothing breaks, you just lose the
|
|
55
55
|
config-level boundary guarantees.
|
|
56
56
|
|
|
57
|
-
**Option 1 —
|
|
58
|
-
|
|
59
|
-
`dsh web` → **Settings → Plugin market** → search for "专家团" / "expert team" → install → refresh.
|
|
60
|
-
|
|
61
|
-
**Option 2 — CLI**
|
|
57
|
+
**Option 1 — CLI (recommended)**
|
|
62
58
|
|
|
63
59
|
```sh
|
|
64
60
|
dsh plugin --profile web add @yangdcm/dsh-expert-team
|
|
65
61
|
# then restart dsh web so the new bundle joins the composition
|
|
66
62
|
```
|
|
67
63
|
|
|
64
|
+
**Option 2 — plugin market** (listing not submitted yet, so it may not be searchable)
|
|
65
|
+
|
|
66
|
+
If it has been listed: `dsh web` → **Settings → Plugin market** → search for "专家团" / "expert team" → install → refresh.
|
|
67
|
+
```
|
|
68
|
+
|
|
68
69
|
**Option 3 — from source (development)**
|
|
69
70
|
|
|
70
71
|
```sh
|
|
@@ -84,13 +85,21 @@ pnpm install && dsh web
|
|
|
84
85
|
> upgrade instead of silently going stale. `/team uninstall` reclaims the copies this plugin laid down —
|
|
85
86
|
> it only removes directories carrying our stamp, and never touches content you authored yourself.
|
|
86
87
|
>
|
|
87
|
-
> **
|
|
88
|
-
>
|
|
89
|
-
>
|
|
90
|
-
>
|
|
91
|
-
>
|
|
92
|
-
>
|
|
93
|
-
>
|
|
88
|
+
> **The preset is laid down when the plugin loads**: after `/team uninstall`, a restart of `dsh web`
|
|
89
|
+
> re-materialises it automatically (no manual rescue needed).
|
|
90
|
+
>
|
|
91
|
+
> **Where settings live**: **Settings →「专家团」** — a full page inside the host's official settings menu
|
|
92
|
+
> (the `settings.section` slot, `id: expert-team`, `order: 50`), sharing the entry point and panel chrome with
|
|
93
|
+
> every other plugin's settings. The data layer is the host namespace `expert-team` (the host owns it, it travels
|
|
94
|
+
> with the plugin market's **backup and restore**, and a changed value recomputes limits, round caps and the tier
|
|
95
|
+
> gate in-process — no restart). The overlay's old "settings" tab has been removed: one form, one place.
|
|
96
|
+
> **Honest boundary**: the default roster (an array of role ids) is deliberately *not* in the host schema (its value
|
|
97
|
+
> type cannot be expressed reliably); it stays with the corresponding control on that page and
|
|
98
|
+
> `$DSH_HOME/expert-team/settings.json`. On a host with no settings service every setting falls back to that file.
|
|
99
|
+
>
|
|
100
|
+
> **The A-line switch**: the "narrow the lead's tool face" gate (`gates.leadToolFace`, default `on`)
|
|
101
|
+
> decides whether execution tools (`bash/write/edit/grep/glob`) are taken away from the lead and given
|
|
102
|
+
> to the role subagents. Turn it off with `config.leadToolFace` or `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off`.
|
|
94
103
|
|
|
95
104
|
## Quick start
|
|
96
105
|
|
|
@@ -141,6 +150,25 @@ npm run check:name # verify no placeholder package name is left behind
|
|
|
141
150
|
`$DSH_HOME`, and `gate:preset` borrows the Config schemas shipped inside your local dsh install
|
|
142
151
|
(the dsh path is auto-detected; override with `DSH_INSTALL`). Neither runs in CI.
|
|
143
152
|
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
**No「专家团」page in the settings menu?** Make sure you are on 1.3.0 or newer
|
|
156
|
+
(`dsh plugin --profile web add @yangdcm/dsh-expert-team` to upgrade) and then **restart `dsh web`** — the page
|
|
157
|
+
did not exist before 1.3.0.
|
|
158
|
+
|
|
159
|
+
**The「专家团模式」preset shows as the bare id (`expert-team`) and sessions have none of the 12 role tools?**
|
|
160
|
+
That means `$DSH_HOME/.agent-presets/expert-team/` does not hold our preset (it was deleted, or a same-named
|
|
161
|
+
preset took the id). Fix it in this order:
|
|
162
|
+
|
|
163
|
+
1. Run `/team <any small task>` once — the plugin re-lays a version-stamped copy from the package (simplest; from
|
|
164
|
+
1.3.0 it also does this automatically on load);
|
|
165
|
+
2. Or copy over it: `cp -f <package>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
|
|
166
|
+
3. Do **not** "create preset" with the same id in Settings → Agent presets — that only gives you a composition of
|
|
167
|
+
the *source* you picked (e.g. standard mode), not ours.
|
|
168
|
+
|
|
169
|
+
Afterwards **restart `dsh web`** (the preset roster is fixed at startup; a page refresh is not enough) and open a
|
|
170
|
+
**new session** (a session's preset is fixed when it is created).
|
|
171
|
+
|
|
144
172
|
## Known limitations
|
|
145
173
|
|
|
146
174
|
- **The local routes are guarded, but that is not authentication**: all 11 overlay routes now check
|
package/README.md
CHANGED
|
@@ -51,17 +51,18 @@
|
|
|
51
51
|
- 会话使用 **「专家团模式」** preset 时,12 个角色工具(`subagent_pm` / `subagent_architect` / …)才可用;
|
|
52
52
|
否则自动退回通用 `subagent`(角色人设写进 prompt),功能不丢、只是少了配置层的边界保证
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`dsh web` → **设置 → 插件市场** → 搜索「专家团」→ 一键安装 → 刷新页面。
|
|
57
|
-
|
|
58
|
-
**方式二:命令行**
|
|
54
|
+
**方式一:命令行(推荐)**
|
|
59
55
|
|
|
60
56
|
```sh
|
|
61
57
|
dsh plugin --profile web add @yangdcm/dsh-expert-team
|
|
62
58
|
# 然后重启 dsh web,使新 bundle 进入组合
|
|
63
59
|
```
|
|
64
60
|
|
|
61
|
+
**方式二:插件市场**(收录尚未提交 ⇒ 目前可能搜不到)
|
|
62
|
+
|
|
63
|
+
若已被收录:`dsh web` → **设置 → 插件市场** → 搜索「专家团」→ 一键安装 → 刷新页面。
|
|
64
|
+
```
|
|
65
|
+
|
|
65
66
|
**方式三:从源码(开发/未发布时)**
|
|
66
67
|
|
|
67
68
|
```sh
|
|
@@ -79,11 +80,18 @@ pnpm install && dsh web
|
|
|
79
80
|
> API),但带版本戳:升级后整目录重铺,不会静默停在旧版本。用 `/team uninstall` 可回收本插件
|
|
80
81
|
> 铺下的副本(只删带我们版本戳的目录,用户自己写的同名内容一律保留)。
|
|
81
82
|
>
|
|
82
|
-
>
|
|
83
|
-
>
|
|
84
|
-
>
|
|
85
|
-
>
|
|
83
|
+
> **插件加载时就把 preset 铺到位**:`/team uninstall` 回收之后,重启 `dsh web` 即会**自愈重铺**(不需要手工救)。
|
|
84
|
+
>
|
|
85
|
+
> **设置在哪改**:**设置 →「专家团」** —— 官方设置菜单里的一整页(`settings.section` 槽,
|
|
86
|
+
> `id: expert-team`、`order: 50`),与其它插件的设置同一入口、同一套面板 chrome。数据层是宿主命名空间
|
|
87
|
+
> `expert-team`(宿主持有、随插件市场的**备份与恢复**一起走;改值后上限/轮次/档位门在进程内**即时重算**,
|
|
88
|
+
> 不必重启)。浮层里原来的「设」页签已移除 —— 同一份表单只在一处渲染。
|
|
89
|
+
> **诚实边界**:`默认班底`(角色 id 数组)刻意不上宿主 schema(类型表达不可靠),由该页里的对应控件与
|
|
86
90
|
> `$DSH_HOME/expert-team/settings.json` 负责;宿主没有 settings 服务时,全部设置退回该文件。
|
|
91
|
+
>
|
|
92
|
+
> **A 线开关**:设置里的「门禁 → 收窄 lead 工具面」(`gates.leadToolFace`,默认 `on`)决定
|
|
93
|
+
> 是否把执行类工具(`bash/write/edit/grep/glob`)从 lead 手上拿走、交给角色子代理。
|
|
94
|
+
> 也可用 `config.leadToolFace` 或环境变量 `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off` 关闭。
|
|
87
95
|
|
|
88
96
|
## 快速上手
|
|
89
97
|
|
|
@@ -133,6 +141,21 @@ npm run check:name # 检查占位包名残留
|
|
|
133
141
|
是**开发机专用**门禁:`gate:sync` 比对本机 `$DSH_HOME` 下的自举副本,`gate:preset` 借用本机
|
|
134
142
|
dsh 安装里插件自带的 Config schema(dsh 路径自动探测,可用 `DSH_INSTALL` 覆盖),因此**不在 CI 里跑**。
|
|
135
143
|
|
|
144
|
+
## 排障
|
|
145
|
+
|
|
146
|
+
**设置菜单里找不到「专家团」那一页?** 确认版本 ≥ 1.3.0(`dsh plugin --profile web add @yangdcm/dsh-expert-team`
|
|
147
|
+
升级,或 `npm view @yangdcm/dsh-expert-team version` 看线上版本),然后**重启 `dsh web`** —— 1.3.0 之前没有这一页。
|
|
148
|
+
|
|
149
|
+
**「专家团模式」显示成裸 id(`expert-team`)、会话里没有 12 个角色工具?** 说明
|
|
150
|
+
`$DSH_HOME/.agent-presets/expert-team/` 里不是我们那份(被删过,或被一份同名预设占了)。按优先级修:
|
|
151
|
+
|
|
152
|
+
1. 跑一次 `/team <任意小任务>` —— 插件会用包内资产重铺一份带版本戳的(最省事;1.3.0 起插件加载时也会自动铺);
|
|
153
|
+
2. 或从包内覆盖:`cp -f <包路径>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
|
|
154
|
+
3. **不要**用同一个 id 在「设置 → Agent 预设」里「创建 preset」——那只会得到你选的**源**的组合(例如标准模式),
|
|
155
|
+
不是我们这份。
|
|
156
|
+
|
|
157
|
+
改完**重启 `dsh web`**(预设名册在启动时固化,刷新页面不够),再开**新会话**(预设只在会话创建时固定)。
|
|
158
|
+
|
|
136
159
|
## 已知限制
|
|
137
160
|
|
|
138
161
|
- **本机来源守卫已就位,但不是鉴权**:11 条浮层路由现在统一校验 Host(挡 DNS rebinding)、
|
package/client.js
CHANGED
|
@@ -199,6 +199,20 @@ window.__ModuleLoader__.load({
|
|
|
199
199
|
'.exp-preview{margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:9px;background:var(--dsw-alias-bg-layer-1,var(--bg,#fff));font-size:12px;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;max-height:330px;overflow:auto}' +
|
|
200
200
|
'.exp-empty{color:var(--dsw-alias-label-secondary,var(--text,#57606a));font-size:12px;padding:8px 0}' +
|
|
201
201
|
'.exp-err{color:#b3291e;font-size:12px;padding:8px 0}' +
|
|
202
|
+
// ── 设置表单(官方「设置」面板的专家团分节;浮层里已不再有「设」页签)──
|
|
203
|
+
// 这些类以前**没有任何 CSS**(只有 JS 里的 className),在浮层里靠浏览器默认样式勉强能看;
|
|
204
|
+
// 搬进官方设置面板后需要一个真正的表单布局:左标签、中控件、右说明。
|
|
205
|
+
'.exp-settings{font-size:12.5px;color:var(--dsw-alias-label-primary,var(--text,#1f2328));max-width:760px}' +
|
|
206
|
+
'.exp-settings-head{margin:0 0 4px;font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-secondary,var(--text,#57606a))}' +
|
|
207
|
+
'.exp-settings-group{margin:16px 0 2px;font-size:11.5px;font-weight:700;letter-spacing:.02em;color:var(--dsw-alias-label-secondary,var(--text,#57606a))}' +
|
|
208
|
+
'.exp-settings-row{display:flex;align-items:flex-start;gap:10px;padding:7px 0;border-top:1px solid var(--dsw-alias-border-l1,var(--border,#eef1f5))}' +
|
|
209
|
+
'.exp-settings-label{flex:0 0 188px;max-width:188px;font-size:12.5px;line-height:1.5}' +
|
|
210
|
+
'.exp-settings-ctl{flex:none;display:flex;align-items:center;min-height:20px}' +
|
|
211
|
+
'.exp-settings-note{flex:1;min-width:0;font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary,var(--text,#57606a))}' +
|
|
212
|
+
'.exp-settings-msg{margin-top:10px;padding:6px 0;font-size:11.5px;font-weight:700;color:#1a7f5a}' +
|
|
213
|
+
'.exp-settings-msg.bad{color:#b3291e;font-weight:600}' +
|
|
214
|
+
'.exp-settings-retry{margin-top:10px;padding:4px 12px;border:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));border-radius:8px;background:var(--dsw-alias-bg-layer-3,var(--bg,#fff));color:inherit;cursor:pointer;font-size:12px}' +
|
|
215
|
+
'.exp-settings-retry:hover{background:var(--dsw-alias-bg-layer-2,var(--bg-subtle,#f6f8fa))}' +
|
|
202
216
|
'.exp-viol{color:#fff;background:linear-gradient(90deg,#c62828,#e53935);font-size:12px;font-weight:600;padding:7px 10px;border-radius:8px;margin:0 0 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
|
|
203
217
|
'.exp-decision{margin:8px 0;padding:12px 14px;border:1px solid var(--dsw-alias-state-business-primary,#0969da);border-radius:10px;background:linear-gradient(180deg,#f6f9ff,var(--dsw-alias-bg-layer-1,#fff))}' +
|
|
204
218
|
'.exp-decision-title{font-weight:700;font-size:13px;color:var(--dsw-alias-state-business-primary,#0969da)}' +
|
|
@@ -257,6 +271,20 @@ window.__ModuleLoader__.load({
|
|
|
257
271
|
+ '.etv-g-node.lead{stroke:var(--etv-lead);stroke-width:1.6}'
|
|
258
272
|
+ '.etv-link{position:relative;height:16px;margin-left:var(--etv-indent)}.etv-link i{position:absolute;left:6px;top:0;bottom:0;width:2px;background:var(--etv-line)}.etv-link b{position:absolute;left:1.5px;bottom:-2px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}.etv-list{position:relative;margin:2px 0 6px var(--etv-indent);padding-left:16px}.etv-list-i{position:absolute;left:0;top:0;bottom:14px;width:2px;background:var(--etv-line)}.etv-row{position:relative;margin:0 0 10px}.etv-row::before{content:"";position:absolute;left:-16px;top:28px;width:14px;height:2px;background:var(--etv-line)}.etv-row::after{content:"";position:absolute;left:-19px;top:25px;width:6px;height:6px;border-radius:50%;background:var(--etv-line)}'
|
|
259
273
|
// ── session store (mirrors dsh-md-preview) ──
|
|
274
|
+
/**
|
|
275
|
+
* 注入本插件的样式表(幂等)。**必须由任何会渲染本插件 UI 的入口调用** ——
|
|
276
|
+
* 原来只有 HeaderButton 在渲染时注入,于是「会话还没建/首屏打开官方设置面板」时
|
|
277
|
+
* 设置表单是一堆无样式控件。
|
|
278
|
+
*/
|
|
279
|
+
function ensureCss() {
|
|
280
|
+
try {
|
|
281
|
+
if (typeof document === 'undefined' || document.querySelector('style[data-et-css]')) return
|
|
282
|
+
var stEl = document.createElement('style')
|
|
283
|
+
stEl.setAttribute('data-et-css', '1')
|
|
284
|
+
stEl.textContent = CSS
|
|
285
|
+
document.head.appendChild(stEl)
|
|
286
|
+
} catch (e) {}
|
|
287
|
+
}
|
|
260
288
|
var currentSessionId = null
|
|
261
289
|
var open = false // default collapsed so the panel never auto-blocks the workspace; open via the「专家团」header button
|
|
262
290
|
var listeners = new Set()
|
|
@@ -1032,21 +1060,42 @@ window.__ModuleLoader__.load({
|
|
|
1032
1060
|
}
|
|
1033
1061
|
|
|
1034
1062
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1063
|
+
* 设置页:**只做四件事** —— 读(GET /settings)、渲染(照 schema)、存(POST,自动保存)、
|
|
1064
|
+
* **读失败要照实说**。
|
|
1065
|
+
*
|
|
1066
|
+
* 为什么读失败必须显式报错:旧实现是 `r.ok ? r.json() : null` —— 非 200 被静默丢成 null,
|
|
1067
|
+
* 于是永远停在「(正在读取设置…)」,既不报错也不超时。实测运行中的 `dsh web` 若启动早于
|
|
1068
|
+
* 本功能(宿主路由只在插件加载时注册),`GET /settings` 就是 **404**,用户看到的就是无限加载。
|
|
1069
|
+
*
|
|
1070
|
+
* 保存失败(400 的错误列表)与读失败分开渲染:读失败整页显示 +「重试」;保存失败只在表单
|
|
1071
|
+
* 下方出红字,**不把已经填好的表单整页换掉**,控件始终回滚到服务端的值 ——
|
|
1037
1072
|
* "界面显示 A、服务端是 B"是最难排查的状态,宁可回滚也不装作成功。
|
|
1038
1073
|
*/
|
|
1039
|
-
function
|
|
1074
|
+
function SettingsSection() {
|
|
1040
1075
|
var sS = useState(null); var data = sS[0], setData = sS[1]
|
|
1041
1076
|
var mS = useState(''); var msg = mS[0], setMsg = mS[1]
|
|
1042
1077
|
var eS = useState(''); var err = eS[0], setErr = eS[1]
|
|
1078
|
+
var lS = useState(''); var loadErr = lS[0], setLoadErr = lS[1]
|
|
1079
|
+
var rS = useState(0); var retry = rS[0], setRetry = rS[1]
|
|
1080
|
+
var loadingS = useState(true); var loading = loadingS[0], setLoading = loadingS[1]
|
|
1043
1081
|
useEffect(function () {
|
|
1044
1082
|
var alive = true
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1083
|
+
setLoading(true); setLoadErr('')
|
|
1084
|
+
fetch('/plugins/dsh-expert-team/settings').then(function (r) {
|
|
1085
|
+
return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
|
|
1086
|
+
}).then(function (res) {
|
|
1087
|
+
if (!alive) return
|
|
1088
|
+
setLoading(false)
|
|
1089
|
+
if (res.ok && res.d && res.d.ok && res.d.schema) { setData(res.d); return }
|
|
1090
|
+
var detail = (res.d && (res.d.error || (res.d.errors || []).join(';'))) || ''
|
|
1091
|
+
setLoadErr(t('读取设置失败:HTTP ', 'Reading settings failed: HTTP ') + res.status + (detail ? ' ' + t('(', '(') + detail + t(')', ')') : ''))
|
|
1092
|
+
}).catch(function (e) {
|
|
1093
|
+
if (!alive) return
|
|
1094
|
+
setLoading(false)
|
|
1095
|
+
setLoadErr(t('读取设置失败:', 'Reading settings failed: ') + String(e && e.message ? e.message : e))
|
|
1096
|
+
})
|
|
1048
1097
|
return function () { alive = false }
|
|
1049
|
-
}, [])
|
|
1098
|
+
}, [retry])
|
|
1050
1099
|
function save(patch) {
|
|
1051
1100
|
setErr(''); setMsg('保存中…')
|
|
1052
1101
|
fetch('/plugins/dsh-expert-team/settings', {
|
|
@@ -1061,8 +1110,14 @@ window.__ModuleLoader__.load({
|
|
|
1061
1110
|
setMsg(res.d.needsRestart ? '已保存 —— **重启 dsh web 后生效**' : '已保存')
|
|
1062
1111
|
}).catch(function (e) { setErr(String(e && e.message ? e.message : e)); setMsg('') })
|
|
1063
1112
|
}
|
|
1064
|
-
if (
|
|
1065
|
-
if (
|
|
1113
|
+
if (loading) return h('div', { className: 'exp-settings' }, h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)')))
|
|
1114
|
+
if (loadErr) return h('div', { className: 'exp-settings' },
|
|
1115
|
+
h('div', { className: 'exp-err' }, '✗ ' + esc(loadErr)),
|
|
1116
|
+
h('div', { className: 'exp-settings-note' },
|
|
1117
|
+
esc(t('若是 404:运行中的 dsh web 启动早于本功能 —— 宿主的 /settings 路由在插件加载时才注册,重启 dsh web 后点「重试」即可。',
|
|
1118
|
+
'On a 404: the running dsh web started before this feature — the host /settings route registers at plugin load. Restart dsh web, then retry.'))),
|
|
1119
|
+
h('button', { className: 'exp-settings-retry', onClick: function () { setRetry(retry + 1) } }, esc(t('重试', 'Retry'))))
|
|
1120
|
+
if (!data || !data.schema) return h('div', { className: 'exp-settings' }, h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)')))
|
|
1066
1121
|
var model = settingsFormModel(data.schema, data.settings)
|
|
1067
1122
|
var rows = []
|
|
1068
1123
|
model.forEach(function (g) {
|
|
@@ -1074,22 +1129,25 @@ window.__ModuleLoader__.load({
|
|
|
1074
1129
|
} else if (r.type === 'bool') {
|
|
1075
1130
|
ctl = h('input', { type: 'checkbox', checked: !!r.value, onChange: function (e) { save({ [r.path]: e.target.checked }) } })
|
|
1076
1131
|
} else if (r.type === 'int') {
|
|
1077
|
-
ctl = h('input', { type: 'number', value: String(r.value), min: r.min === null ? undefined : r.min, max: r.max === null ? undefined : r.max, style: { width:
|
|
1132
|
+
ctl = h('input', { type: 'number', value: String(r.value), min: r.min === null ? undefined : r.min, max: r.max === null ? undefined : r.max, style: { width: 84 },
|
|
1078
1133
|
onChange: function (e) { var v = e.target.value; if (v !== '' && /^-?\d+$/.test(v)) save({ [r.path]: Number(v) }) } })
|
|
1079
1134
|
} else if (r.type === 'enum') {
|
|
1080
1135
|
ctl = h('select', { value: String(r.value), onChange: function (e) { save({ [r.path]: e.target.value }) } },
|
|
1081
1136
|
(r.values || []).map(function (v) { return h('option', { key: v, value: v }, esc(v)) }))
|
|
1082
1137
|
} else {
|
|
1083
|
-
ctl = h('input', { type: 'text', value: String(r.value), placeholder: t('留空 = 按档位默认', 'empty = tier default'), style: { width:
|
|
1138
|
+
ctl = h('input', { type: 'text', value: String(r.value), placeholder: t('留空 = 按档位默认', 'empty = tier default'), style: { width: 168 },
|
|
1084
1139
|
onBlur: function (e) { var v = e.target.value.trim(); save({ [r.path]: v === '' ? null : v.split(',').map(function (x) { return x.trim() }).filter(Boolean) }) } })
|
|
1085
1140
|
}
|
|
1086
1141
|
rows.push(h('div', { key: r.path, className: 'exp-settings-row' },
|
|
1087
1142
|
h('span', { className: 'exp-settings-label', title: r.hint }, esc(r.label)),
|
|
1088
|
-
ctl,
|
|
1089
|
-
|
|
1143
|
+
h('span', { className: 'exp-settings-ctl' }, ctl),
|
|
1144
|
+
h('span', { className: 'exp-settings-note', title: r.hint }, r.hint ? esc(r.hint) : null)))
|
|
1090
1145
|
})
|
|
1091
1146
|
})
|
|
1092
|
-
return h('div', { className: 'exp-settings' },
|
|
1147
|
+
return h('div', { className: 'exp-settings' },
|
|
1148
|
+
h('div', { className: 'exp-settings-head' }, esc(t('改动即保存(`编制` / `门禁` 两类需重启 dsh web 后生效)。', 'Saved on change; `roster` and `gates` apply after a dsh web restart.'))),
|
|
1149
|
+
rows,
|
|
1150
|
+
h('div', { className: 'exp-settings-msg' + (err ? ' bad' : '') }, esc(err ? '✗ ' + err : (msg || ''))))
|
|
1093
1151
|
}
|
|
1094
1152
|
|
|
1095
1153
|
function DecisionCard(props) {
|
|
@@ -1944,9 +2002,13 @@ window.__ModuleLoader__.load({
|
|
|
1944
2002
|
var headCls = 'exp-head' + (docked || viewMode ? '' : ' exp-grab')
|
|
1945
2003
|
|
|
1946
2004
|
try {
|
|
1947
|
-
|
|
2005
|
+
// 「设」页签已移除:设置页搬进**官方「设置」菜单**(`settings.section` 槽,见 apply())。
|
|
2006
|
+
// 为什么改:浮层页签是一个只有打开浮层才够得着的第二入口,且它依赖一个「读取设置」的
|
|
2007
|
+
// 自建请求路径 —— 一旦宿主路由没注册就永远停在「正在读取设置…」。搬进官方菜单后与
|
|
2008
|
+
// 其它插件的设置同一入口、同一套面板 chrome。
|
|
2009
|
+
var TAB_ZH = { team: '人', tasks: '事', info: '料', board: '盘' }
|
|
1948
2010
|
var tabBar = h('div', { className: 'exp-tabs' },
|
|
1949
|
-
['team', 'tasks', 'info', 'board'
|
|
2011
|
+
['team', 'tasks', 'info', 'board'].map(function (tb) {
|
|
1950
2012
|
return h('span', { key: tb, className: 'exp-tab' + (tab === tb ? ' on' : ''), onClick: function () { setTab(tb) } },
|
|
1951
2013
|
esc((langNow === 'en' ? TAB_ZH[tb] : TAB_ZH[tb])) + (tb === 'tasks' ? ' ' + tasks.length : ''))
|
|
1952
2014
|
}),
|
|
@@ -2189,7 +2251,7 @@ window.__ModuleLoader__.load({
|
|
|
2189
2251
|
h('ol', { className: 'exp-path' }, stepper),
|
|
2190
2252
|
(data.pendingDecision && data.pendingDecision.options && data.pendingDecision.options.length) ? h(DecisionCard, { decision: data.pendingDecision, onChoose: function (id, label) { decide({ choice: label || id }) } }) : null,
|
|
2191
2253
|
tabBar,
|
|
2192
|
-
tab === 'team' ? teamTab : (tab === 'tasks' ? tasksTab : (tab === 'board' ? boardTab :
|
|
2254
|
+
tab === 'team' ? teamTab : (tab === 'tasks' ? tasksTab : (tab === 'board' ? boardTab : infoTab))
|
|
2193
2255
|
] : null))
|
|
2194
2256
|
} catch (e) {
|
|
2195
2257
|
return h('div', { className: 'exp-preview' }, 'PANEL ERR: ' + esc(String(e && e.stack ? e.stack : e)).slice(0, 800))
|
|
@@ -2253,14 +2315,7 @@ window.__ModuleLoader__.load({
|
|
|
2253
2315
|
function toggle() { if (!isOpen) { dockState = true; saveLS('et-dock', true); notify(); setOpen() } else setOpen() }
|
|
2254
2316
|
// CSS must exist for the HEADER button too — the panel's own <style> is
|
|
2255
2317
|
// only in the DOM while the panel is open. Inject once, globally.
|
|
2256
|
-
|
|
2257
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-et-css]')) {
|
|
2258
|
-
var stEl = document.createElement('style')
|
|
2259
|
-
stEl.setAttribute('data-et-css', '1')
|
|
2260
|
-
stEl.textContent = CSS
|
|
2261
|
-
document.head.appendChild(stEl)
|
|
2262
|
-
}
|
|
2263
|
-
} catch (e) {}
|
|
2318
|
+
ensureCss()
|
|
2264
2319
|
var badge = teamBadge(live)
|
|
2265
2320
|
var tip = '专家团 · ' + badge.title + '|点击' + (isOpen ? '收起' : '打开') + '并排面板(全屏画布:切到上方「🧑🔬 专家团」标签)'
|
|
2266
2321
|
return h('button', { className: 'exp-open exp-live-badge' + (badge.cls || ''), title: tip, onClick: toggle },
|
|
@@ -2397,6 +2452,22 @@ window.__ModuleLoader__.load({
|
|
|
2397
2452
|
exports._syncPending = syncPending
|
|
2398
2453
|
exports._resolvePending = resolvePending
|
|
2399
2454
|
|
|
2455
|
+
// CSS 必须在这里就注入:官方「设置」面板可以在**没有会话**(新会话/首屏)时打开,
|
|
2456
|
+
// 而原来这份 CSS 只挂在 HeaderButton 的渲染里 —— 那样设置面板会是一堆无样式的裸控件。
|
|
2457
|
+
ensureCss()
|
|
2458
|
+
|
|
2459
|
+
// 设置页进**官方设置菜单**(`settings.section` 是宿主 ui-settings-general 声明、其它
|
|
2460
|
+
// 插件(agent-preset / models / plugins)同样在用的槽)。`ctx.slots.inject` 等的是**声明**
|
|
2461
|
+
// 而不是包顺序:宿主没声明该槽时这条贡献不会挂上,插件其余部分照常工作。
|
|
2462
|
+
ctx.slots.inject('settings.section', function () {
|
|
2463
|
+
return ctx.slots.register({
|
|
2464
|
+
name: 'settings.section',
|
|
2465
|
+
id: 'expert-team',
|
|
2466
|
+
order: 50,
|
|
2467
|
+
label: function () { return t('专家团', 'Expert team') }
|
|
2468
|
+
}, wrap(SettingsSection))
|
|
2469
|
+
})
|
|
2470
|
+
|
|
2400
2471
|
ctx.slots.inject('conversation.session.header.actions', function () {
|
|
2401
2472
|
return ctx.slots.register({ name: 'conversation.session.header.actions', id: 'expert-team-open', order: 900, inject: function (sessionId) { return sessionId ? { sessionId: sessionId } : {} } }, wrap(HeaderButton))
|
|
2402
2473
|
})
|
package/lib/command.js
CHANGED
|
@@ -565,6 +565,7 @@ function reapplySettingsDerived() {
|
|
|
565
565
|
resolveLimits(PLUGIN_CONFIG, limitsBaseFromSettings(s));
|
|
566
566
|
resolveRoundLimits(PLUGIN_CONFIG, roundLimitsBaseFromSettings(s));
|
|
567
567
|
resolveTierGate(PLUGIN_CONFIG, s.gates);
|
|
568
|
+
resolveLeadToolFace(PLUGIN_CONFIG, s.gates);
|
|
568
569
|
return true;
|
|
569
570
|
} catch (e) {
|
|
570
571
|
console.warn('[expert-team] 重算设置派生值失败(保持上一次生效值):' + String(e && e.message ? e.message : e));
|
|
@@ -885,10 +886,40 @@ async function uninstallInstalled() {
|
|
|
885
886
|
'说明:skill 现在默认走**运行时注册**,本来就不写盘;这里回收的是历史版本留下的副本,',
|
|
886
887
|
'或宿主没有 skill 注册表时的回退副本。',
|
|
887
888
|
'本命令不碰工作区里的 `team/` 运行目录,也不碰插件包本身(那由插件市场卸载)。',
|
|
889
|
+
// 契约要写清:`$DSH_HOME/expert-team/` 下既有"安装副本"(installed.json,会被删),
|
|
890
|
+
// 也有**用户数据**(跨项目经验与会话→run 记忆)。后者不是安装产物,卸载命令无权处置。
|
|
891
|
+
'状态文件(`$DSH_HOME/expert-team/` 下的 `LEARNINGS.md`、`session-runs.json`、`workspaces.json`)**保留**',
|
|
892
|
+
'—— 它们是跨项目经验与会话→run 记忆,属于你的数据,不是安装副本。',
|
|
893
|
+
// 2026-09-15 事故后补的契约:本命令只清"当前那份副本",插件只要还装着,下次加载就会重新铺。
|
|
894
|
+
'注意:只要插件**仍安装着**,下次加载会重新铺一份「专家团模式」preset(带版本戳)——',
|
|
895
|
+
'本命令清的是当前副本,真正的卸载是在插件市场里移除插件本身。',
|
|
888
896
|
);
|
|
889
897
|
return { kind: 'success', text: lines.join('\n') };
|
|
890
898
|
}
|
|
891
899
|
|
|
900
|
+
/**
|
|
901
|
+
* 后台铺一份运行时资产(preset / skill 回退副本),**失败绝不影响插件加载**。
|
|
902
|
+
*
|
|
903
|
+
* 为什么用 fire-and-forget:`apply()` 是同步契约,绝不能为了铺盘把插件挂载拖住或拖挂。
|
|
904
|
+
* 为什么只警告一次:铺失败通常意味着 `$DSH_HOME` 不可写(只读盘/权限),每次加载都刷同样的 warn
|
|
905
|
+
* 会把"响亮"变成噪声,而噪声的代价是所有告警一起被降权。
|
|
906
|
+
*/
|
|
907
|
+
const INSTALL_FAILURE_WARNED = new Set();
|
|
908
|
+
function warnInstallFailureOnce(kind) {
|
|
909
|
+
if (INSTALL_FAILURE_WARNED.has(kind)) return;
|
|
910
|
+
INSTALL_FAILURE_WARNED.add(kind);
|
|
911
|
+
console.warn(`[expert-team] 未能铺下 ${kind} 副本(插件其余功能不受影响):$DSH_HOME 可能不可写`);
|
|
912
|
+
}
|
|
913
|
+
function installInBackground(kind, run) {
|
|
914
|
+
try {
|
|
915
|
+
Promise.resolve(run())
|
|
916
|
+
.then((target) => { if (!target) warnInstallFailureOnce(kind); })
|
|
917
|
+
.catch(() => warnInstallFailureOnce(kind));
|
|
918
|
+
} catch {
|
|
919
|
+
warnInstallFailureOnce(kind);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
892
923
|
async function ensureSkillInstalled() {
|
|
893
924
|
// 运行时注册成功 ⇒ 一个文件都不写(默认路径;也是"卸载即干净"的前提)
|
|
894
925
|
if (RUNTIME_SKILL_REGISTERED) return SKILL_DIR;
|
|
@@ -1611,6 +1642,28 @@ const TIER_GATE_VALUES = ['soft', 'hard'];
|
|
|
1611
1642
|
const TIER_GATE_ENV = 'DSH_EXPERT_TEAM_TIER_GATE';
|
|
1612
1643
|
let TIER_GATE = DEFAULT_TIER_GATE;
|
|
1613
1644
|
|
|
1645
|
+
// ── lead 工具面收窄的开关(2026-09-14 加)──────────────────────────────────────
|
|
1646
|
+
// 为什么需要它:A 线的"把执行类工具从 lead 手上拿走"此前因一个 bug **从未真正生效**
|
|
1647
|
+
// (清单取错作用域,见 lib/lead-toolface.js)。修好之后它会立刻改变日常使用形态 ——
|
|
1648
|
+
// lead(含正在跟你对话的那个)不再持有 bash/write/edit/grep/glob,执行全部落到角色子代理。
|
|
1649
|
+
// 这是设计意图,但**没有开关的行为变更不该只留给用户一个"忍着或回退版本"**。
|
|
1650
|
+
// 优先级与档位门完全一致:**config > env > 设置控制台 > 内置默认(on)**。
|
|
1651
|
+
const DEFAULT_LEAD_TOOLFACE = 'on';
|
|
1652
|
+
const LEAD_TOOLFACE_VALUES = ['on', 'off'];
|
|
1653
|
+
const LEAD_TOOLFACE_ENV = 'DSH_EXPERT_TEAM_LEAD_TOOLFACE';
|
|
1654
|
+
let LEAD_TOOLFACE = DEFAULT_LEAD_TOOLFACE;
|
|
1655
|
+
|
|
1656
|
+
/** 解析 lead 工具面收窄开关(非法值一律回默认 on —— 与档位门同理,不猜不报错)。 */
|
|
1657
|
+
function resolveLeadToolFace(config, base) {
|
|
1658
|
+
const cfgRaw = String((config && typeof config === 'object' && config.leadToolFace) || '').trim().toLowerCase();
|
|
1659
|
+
const envRaw = String(((typeof process !== 'undefined' && process.env) || {})[LEAD_TOOLFACE_ENV] || '').trim().toLowerCase();
|
|
1660
|
+
const baseRaw = String((base && typeof base === 'object' ? base.leadToolFace : base) || '').trim().toLowerCase();
|
|
1661
|
+
LEAD_TOOLFACE = LEAD_TOOLFACE_VALUES.includes(cfgRaw) ? cfgRaw
|
|
1662
|
+
: (LEAD_TOOLFACE_VALUES.includes(envRaw) ? envRaw
|
|
1663
|
+
: (LEAD_TOOLFACE_VALUES.includes(baseRaw) ? baseRaw : DEFAULT_LEAD_TOOLFACE));
|
|
1664
|
+
return LEAD_TOOLFACE;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1614
1667
|
/**
|
|
1615
1668
|
* 解析档位门是软还是硬。**非法值一律回默认(soft)**,不猜也不报错 ——
|
|
1616
1669
|
* 一个拼错的开关值把流程卡死(hard)比静默放行更糟,而"卡死"正是最贵的失败模式。
|
|
@@ -4416,12 +4469,13 @@ const WRITE_TRACER = createWriteTracer();
|
|
|
4416
4469
|
* 去重只影响**重复次数**,不影响"第一次一定喊",也不合并不同 status(两种零仍然分得清)。
|
|
4417
4470
|
*/
|
|
4418
4471
|
const LEAD_TOOLFACE_WARNED = new Set();
|
|
4472
|
+
let LEAD_TOOLFACE_OFF_LOGGED = false;
|
|
4419
4473
|
function warnLeadToolFaceOnce(status, detail) {
|
|
4420
4474
|
if (LEAD_TOOLFACE_WARNED.has(status)) return;
|
|
4421
4475
|
LEAD_TOOLFACE_WARNED.add(status);
|
|
4422
4476
|
console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
|
|
4423
4477
|
}
|
|
4424
|
-
export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
|
|
4478
|
+
export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
|
|
4425
4479
|
|
|
4426
4480
|
export function apply(ctx, config) {
|
|
4427
4481
|
// 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
|
|
@@ -4439,6 +4493,8 @@ export function apply(ctx, config) {
|
|
|
4439
4493
|
resolveRoundLimits(config, roundLimitsBaseFromSettings());
|
|
4440
4494
|
// G 线:档位门软/硬(config.tierGate > DSH_EXPERT_TEAM_TIER_GATE > 设置 > soft)。
|
|
4441
4495
|
resolveTierGate(config, currentSettings().gates);
|
|
4496
|
+
// A 线开关:config > env > 设置 > 默认(on)。关掉即 lead 保留全部工具。
|
|
4497
|
+
resolveLeadToolFace(config, currentSettings().gates);
|
|
4442
4498
|
// 批 2-1:宿主若提供 ctx.fs,就让它接管工件写入(真实版本栅栏);否则保持同语义的回退实现。
|
|
4443
4499
|
// 机会式探测而非 inject —— 见 ARTIFACT 声明处的说明(inject 缺失服务会让整条命令挂不上)。
|
|
4444
4500
|
const hostFsAdopted = adoptHostFs(ctx);
|
|
@@ -4452,6 +4508,16 @@ export function apply(ctx, config) {
|
|
|
4452
4508
|
base: currentSettings(),
|
|
4453
4509
|
onResolved: () => reapplySettingsDerived(),
|
|
4454
4510
|
});
|
|
4511
|
+
// ── 插件装着 ⇒ 它的 preset 就该在位 ─────────────────────────────────────────
|
|
4512
|
+
// 为什么必须在**加载时**铺、而不是等首次 `/team <任务>`:preset 是**会话创建时**读取的,
|
|
4513
|
+
// 中途缺失会让正在使用它的会话直接丢掉角色工具(2026-09-15 真实事故:用户跑 `/team uninstall`
|
|
4514
|
+
// 之后「专家团模式」消失,会话里 `subagent_pm/researcher/...` 12 个角色工具全部消失)。
|
|
4515
|
+
// 契约因此是「**插件装着 ⇒ preset 在位**」:`/team uninstall` 只清"当前那份副本",
|
|
4516
|
+
// 插件只要还装着,下次加载就重新铺;真正的卸载是在插件市场里移除插件本身。
|
|
4517
|
+
// 用 fire-and-forget:铺不上($DSH_HOME 不可写等)绝不能让插件挂不上。
|
|
4518
|
+
installInBackground('preset', () => ensurePresetInstalled());
|
|
4519
|
+
// skill 已有运行时注册时这里立即返回(零成本);只有宿主没有 skill 注册表时才真复制。
|
|
4520
|
+
installInBackground('skill', () => ensureSkillInstalled());
|
|
4455
4521
|
void loadSessionRuns(); // session→run memory for overlay auto-select
|
|
4456
4522
|
ctx.commands.register({
|
|
4457
4523
|
name: 'team',
|
|
@@ -4611,6 +4677,15 @@ export function apply(ctx, config) {
|
|
|
4611
4677
|
const isRoot = agents?.roots?.().includes(agent) === true;
|
|
4612
4678
|
if (!shouldNarrowLeadToolFace({ presetId, isRoot }).apply) return; // 无关 agent:静默返回,不是失败
|
|
4613
4679
|
|
|
4680
|
+
if (LEAD_TOOLFACE === 'off') {
|
|
4681
|
+
// 关掉时**出声一次**:否则"我明明关了"与"开关没生效"看起来一模一样(两种零纪律)。
|
|
4682
|
+
if (!LEAD_TOOLFACE_OFF_LOGGED) {
|
|
4683
|
+
LEAD_TOOLFACE_OFF_LOGGED = true;
|
|
4684
|
+
console.log(`[expert-team] lead 工具面收窄已关闭(${LEAD_TOOLFACE_ENV}=off 或设置 gates.leadToolFace=off)⇒ lead 保留全部工具`);
|
|
4685
|
+
}
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4614
4689
|
// ③ **必须取 agent 作用域**的名字清单:`view()` 不传 scope = 全局视图,而模型可见工具
|
|
4615
4690
|
// 在 0.1.5 起由 preset 注册在 agent 平面 ⇒ 全局视图非空但缺那几个名字,收窄会静默失效
|
|
4616
4691
|
// 且给出"宿主里没有这些工具"的错误结论(拿真机日志换来的教训,详见 lead-toolface.js)。
|
package/lib/settings.js
CHANGED
|
@@ -57,6 +57,7 @@ export const SETTINGS_SPEC = {
|
|
|
57
57
|
boundarySpec: { type: 'bool', default: true, label: '规格边界章节(SPEC.md)', hint: '边界没填就不让进 implement' },
|
|
58
58
|
loopGuard: { type: 'bool', default: true, label: '振荡检测(A→B→A→B)', hint: '宿主那份只管同工具重复,这份管来回振荡' },
|
|
59
59
|
tierGate: { type: 'enum', values: ['soft', 'hard'], default: 'soft', label: '档位选择门', hint: 'soft = 建议先生效;hard = 不选不开工' },
|
|
60
|
+
leadToolFace: { type: 'enum', values: ['on', 'off'], default: 'on', label: '收窄 lead 工具面', hint: 'on = 执行类工具(bash/write/edit/grep/glob)从 lead 手上拿走,交给角色子代理(省 token);off = lead 保留全部工具' },
|
|
60
61
|
maxReviewRounds: { type: 'int', min: 1, max: 20, default: 3, label: '评审轮次上限', hint: '到顶的正确动作是升级用户,不是再来一轮' },
|
|
61
62
|
maxTestRounds: { type: 'int', min: 1, max: 20, default: 3, label: '测试轮次上限', hint: '同上' },
|
|
62
63
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yangdcm/dsh-expert-team",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "dsh「专家团」bundle:一句自然语言自动组建/持久化一支 12 角色多智能体团队,共享工作区协议 + 阶段门控编排 + 结构化交接 + 质量门禁/自动调度,实现者直接改代码并产出持久工件;带 live 团队浮层(质量门禁/覆盖率/工件预览)。 · Role-based multi-agent expert team for DeepSeek Harness: one sentence in, a staged and gated team delivery out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"platform": "web",
|
|
50
50
|
"inject": [
|
|
51
51
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
52
|
-
"@deepseek-ai/dsh-client-ui-conversation"
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-settings-general"
|
|
53
54
|
]
|
|
54
55
|
},
|
|
55
56
|
"compatibility": {
|