@yangdcm/dsh-expert-team 1.2.3 → 1.3.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/CHANGELOG.md CHANGED
@@ -3,6 +3,76 @@
3
3
  本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
4
4
  `package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
5
5
 
6
+ ## 1.3.1
7
+
8
+ **只改文案与文档,不动任何功能逻辑。**
9
+
10
+ ### 设置页那句"需重启"改成与事实一致
11
+
12
+ - 页面顶部原文是「改动即保存(`编制` / `门禁` 两类需重启 `dsh web` 后生效)」——这在 1.1.x 成立,
13
+ **1.2.0 起不再成立**:设置写入后两条路径都会在进程内重算派生值(容量上限 `resolveLimits`、
14
+ 轮次上限 `resolveRoundLimits`、档位门 `resolveTierGate`、A 线开关 `resolveLeadToolFace`,见
15
+ `reapplySettingsDerived()`):
16
+ - **宿主路径**(宿主有 settings 服务时,也就是官方面板/浮层实际在用的那条):
17
+ `scope.watch()` → `onResolved` → 重算;保存回执原文即"已在进程内重算,**无需重启**";
18
+ - **文件回退路径**(宿主不可用、写 `settings.json`):路由里同样显式调用了重算;
19
+ 但该路径的保存回执仍按既有契约返回 `needsRestart: true`(被 `settings.test.mjs` 钉住),
20
+ 因此文案**不能一概而论**。
21
+ - 新文案如实覆盖两种形态:「改动即保存。宿主设置可用时(默认)上限与档位门在进程内即时重算,
22
+ 无需重启;若保存提示"重启后生效",按提示操作。」(英文版同步)
23
+ - 没有测试断言过这句话,故无需改任何断言。
24
+
25
+ ### README 新增「装上之后怎么用」
26
+
27
+ - 写实"装完**重启一次** `dsh web` 即自动就位":插件加载时把「专家团模式」预设铺到
28
+ `$DSH_HOME/.agent-presets/expert-team`(带版本戳、升级整目录重铺),**不需要手工创建预设**。
29
+ - 实测依据(2026-09-15):删掉该目录模拟全新安装 → 仅重启一次 ⇒ 目录被**自动新建**
30
+ (APFS birth time 落在该次启动内,比插件落盘晚 621 秒 ⇒ 排除"安装时铺")、版本戳 = 插件版本、
31
+ 与包内逐字节一致;选择器里随即出现「专家团模式」,`会话插件` 页 12/12 角色行齐全。
32
+
33
+ ### README 新增「自定义预设」
34
+
35
+ - 创建入口与机制(`设置 → Agent 预设 → 用「创造模式」创作自定义预设`,本质是"复制一份既有预设")。
36
+ - 陷阱写清:**不要**用 `expert-team` 这个 id 去「创建 preset」——那只会得到所选**源**的组合
37
+ (通常是标准模式),既不是我们的预设,还会占住该 id 让自动重铺失效;要给专家团做定制,
38
+ 请以「专家团模式」为源、**另用一个自己的 id**,改动只写在自己的目录里(`expert-team/` 归插件所有,
39
+ 加载/升级会整目录重铺)。
40
+ ## 1.3.0
41
+
42
+ **设置页搬进官方「设置」菜单 + 预设在插件加载时就位 + 门禁两处修复**
43
+
44
+ ### 设置页进官方设置菜单(`settings.section`)
45
+
46
+ - `client.js` 新增 `SettingsSection` 组件并通过 `ctx.slots.inject('settings.section', …)` 注册
47
+ (`id: expert-team`、`order: 50`、label 走语言 thunk)⇒ 设置出现在 **设置 →「专家团」**,
48
+ 与其它插件的设置同一入口、同一套面板 chrome。
49
+ - **浮层第 5 个「设」页签已移除**:同一份表单不再两处渲染。
50
+ - 样式改由 `ensureCss()` 在 `apply()` 里注入 —— 官方设置菜单**可以在没有会话时打开**,
51
+ 而原实现只在 HeaderButton 渲染路径注入 CSS ⇒ 那种情况下面板是一堆裸控件。
52
+ - 读设置失败改为**显式报错 + 重试按钮**(旧写法把 404 静默变成 null ⇒ 永久停在"正在读取设置…")。
53
+ - 数据层不变:宿主命名空间 `expert-team`(随插件市场的备份/恢复走;改值后上限/轮次/档位门即时重算)。
54
+ - **诚实边界**:`默认班底`(角色 id 数组)刻意不上宿主 schema(类型表达不可靠),
55
+ 继续由该页里的对应控件与 `$DSH_HOME/expert-team/settings.json` 负责。
56
+
57
+ ### 预设在插件加载时就位(修一次真实事故)
58
+
59
+ - `apply()` 里 `void ensurePresetInstalled()` / `void ensureSkillInstalled()`:
60
+ **插件装着 ⇒ 它的 preset 就该在位**(失败只 warn,绝不影响插件加载)。
61
+ - 事故:`/team uninstall` 按设计回收了 `$DSH_HOME/.agent-presets/expert-team`,而铺设此前只发生在
62
+ `/team <任务>` 时 ⇒ **使用该预设的会话当场丢掉 12 个角色工具**(`subagent_pm`…),
63
+ 选择器里那条也退化成裸 id(只剩标准模式的描述)。现在加载即铺;对带我们版本戳的目录还会
64
+ **整目录重铺** ⇒ 该状态可自愈;对"无戳且内容不像我们"的同名目录仍**不覆盖**(尊重用户内容)但会出声告警。
65
+ - `/team uninstall` 输出补契约说明:插件仍装着时,下次加载会重新铺一份;真正的卸载是在插件市场移除插件。
66
+
67
+ ### 门禁修复(`scripts/check-sync.mjs`)
68
+
69
+ - **普通模式崩溃**:`ABSENT_OPTIONAL`(可选映射目标不存在)此前落进读 `onlySrc/onlyDst/differ` 的分支
70
+ ⇒ `TypeError`;而 skill 是第一个映射,于是整份报告**一行都打不出来**。现在四种状态各有显式出口。
71
+ - `preset` 映射改为**可选**(全新安装、刚 uninstall 都属正常缺席;**存在时照旧逐文件比对**,
72
+ 仍能抓"改了源码没同步")。
73
+ - `.expert-team-version`(归属标记、不是包内资产)计入忽略项 —— 否则 1.3.0 起每个用户带戳后
74
+ `gate:sync` 会假红。
75
+
6
76
  ## 1.2.3
7
77
 
8
78
  **给 lead 工具面收窄加开关**(因为 1.2.2 让一个从未生效的行为突然生效了)
@@ -45,9 +115,12 @@
45
115
  ### 设置接进宿主命名空间
46
116
 
47
117
  - 插件加载时把设置注册为宿主命名空间 `expert-team`(`ctx.inject(['settings'], …)` 作为优雅降级边界):
48
- - 出现在 **设置 → 插件 → 插件配置**,由宿主按我们给的 schema 渲染 —— schema
49
- `lib/settings.js` 的 `SETTINGS_SPEC` **生成**(字段/范围/默认值都是单一真源,不抄第二份);
50
- - 随插件市场的**备份与恢复**一起走(市场只备份"profile 插件清单 + 设置",自造 JSON 不在内);
118
+ - **数据层已就位**:设置由宿主服务持有,随插件市场的备份/恢复走;schema 由
119
+ `lib/settings.js` 的 `SETTINGS_SPEC` **生成**(字段/范围/默认值单一真源,不抄第二份)。
120
+ ⚠️ **更正(2026-09-14 实机核验)**:本节先前写的"出现在 设置 插件 → 插件配置"当时**并不成立** ——
121
+ 那张页面渲染的是「宿主服务的命名空间 ∩ 已注册卡片」的交集,我们只交付了数据层。
122
+ → **已于 1.3.0 解决**:设置页走 `settings.section` 槽,作为 **设置 →「专家团」** 一整页出现
123
+ (不是 dshmarket 那种"插件配置"卡片),见 1.3.0 段。
51
124
  - 改动**即时生效**:`scope.watch()` → 用同一份 `config` 重算容量上限/轮次上限/档位门,不必重启。
52
125
  - `base` 用**当前生效设置**填充 ⇒ 面板一打开看到的就是真正在用的值,**不需要任何一次性迁移**;
53
126
  优先级不变:`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 — plugin market (recommended)**
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,18 +85,48 @@ 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
- > **Where settings live**: on load the plugin registers its settings as the host namespace `expert-team`,
88
- > so they appear under **Settings Plugins → Plugin configuration** and take effect immediately (limits,
89
- > round caps and the tier gate are recomputed in-process — no restart), and they travel with the plugin
90
- > market's **backup and restore**. **Honest boundary**: the default roster (an array of role ids) is *not*
91
- > on the official page its value type cannot be expressed reliably in the host schema, so it stays with
92
- > the overlay's settings tab and `$DSH_HOME/expert-team/settings.json`. On a host with no settings service
93
- > every setting falls back to that file.
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.
94
99
  >
95
100
  > **The A-line switch**: the "narrow the lead's tool face" gate (`gates.leadToolFace`, default `on`)
96
101
  > decides whether execution tools (`bash/write/edit/grep/glob`) are taken away from the lead and given
97
102
  > to the role subagents. Turn it off with `config.leadToolFace` or `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off`.
98
103
 
104
+ ### After installing
105
+
106
+ 1. **Restart `dsh web` once**: on load the plugin lays the 「专家团模式」 preset down into
107
+ `$DSH_HOME/.agent-presets/expert-team` (version-stamped; upgrades re-lay the whole directory) —
108
+ **no manual preset creation needed**. After that restart the preset picker lists it, and all 12 role
109
+ subagent tools are in place (visible under `Settings → Plugins → Plugin list → Session plugins`).
110
+ 2. Switch the session to that preset, then run `/team <one-line goal>`.
111
+ 3. Change settings under **Settings →「专家团」** (values live in the host namespace `expert-team`, so they
112
+ travel with the plugin market's backup/restore).
113
+
114
+ **Troubleshooting**: a missing preset, or a same-id preset squatting on ours, is healed by **one `dsh web`
115
+ restart** (the plugin re-lays on load); running `/team <task>` once does the same. See the
116
+ [Troubleshooting](#troubleshooting) section below for the details — including the easiest trap to fall into:
117
+ **never** create a preset with the id `expert-team`.
118
+
119
+ ### Custom presets (when you want to change expert-team's defaults)
120
+
121
+ - **Create**: `Settings → Agent presets → create a custom preset with "creation mode"` (its mechanism is
122
+ "copy an existing preset"; the result lands in `$DSH_HOME/.agent-presets/<id>/`).
123
+ - **To customise expert-team, copy 「专家团模式」 as the source under an id of your own** (e.g. `my-team`): the
124
+ copy already carries the 12 role tools and their skill directory, and your edits stay inside `my-team/`.
125
+ **Never edit files under `expert-team/`** — that copy belongs to the plugin and is re-laid wholesale on
126
+ load/upgrade.
127
+ - A newly created preset **may only appear in the picker after a `dsh web` restart** (the host reads its roster
128
+ at startup).
129
+
99
130
  ## Quick start
100
131
 
101
132
  ```
@@ -145,6 +176,25 @@ npm run check:name # verify no placeholder package name is left behind
145
176
  `$DSH_HOME`, and `gate:preset` borrows the Config schemas shipped inside your local dsh install
146
177
  (the dsh path is auto-detected; override with `DSH_INSTALL`). Neither runs in CI.
147
178
 
179
+ ## Troubleshooting
180
+
181
+ **No「专家团」page in the settings menu?** Make sure you are on 1.3.0 or newer
182
+ (`dsh plugin --profile web add @yangdcm/dsh-expert-team` to upgrade) and then **restart `dsh web`** — the page
183
+ did not exist before 1.3.0.
184
+
185
+ **The「专家团模式」preset shows as the bare id (`expert-team`) and sessions have none of the 12 role tools?**
186
+ That means `$DSH_HOME/.agent-presets/expert-team/` does not hold our preset (it was deleted, or a same-named
187
+ preset took the id). Fix it in this order:
188
+
189
+ 1. Run `/team <any small task>` once — the plugin re-lays a version-stamped copy from the package (simplest; from
190
+ 1.3.0 it also does this automatically on load);
191
+ 2. Or copy over it: `cp -f <package>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
192
+ 3. Do **not** "create preset" with the same id in Settings → Agent presets — that only gives you a composition of
193
+ the *source* you picked (e.g. standard mode), not ours.
194
+
195
+ Afterwards **restart `dsh web`** (the preset roster is fixed at startup; a page refresh is not enough) and open a
196
+ **new session** (a session's preset is fixed when it is created).
197
+
148
198
  ## Known limitations
149
199
 
150
200
  - **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,16 +80,37 @@ pnpm install && dsh web
79
80
  > API),但带版本戳:升级后整目录重铺,不会静默停在旧版本。用 `/team uninstall` 可回收本插件
80
81
  > 铺下的副本(只删带我们版本戳的目录,用户自己写的同名内容一律保留)。
81
82
  >
82
- > **设置在哪改**:插件加载时把设置注册成宿主命名空间 `expert-team` 出现在
83
- > **设置 → 插件 → 插件配置**,改动**即时生效**(上限/轮次/档位门在进程内重算,不必重启),
84
- > 并且随插件市场的**备份与恢复**一起走。**诚实边界**:`默认班底`(角色 id 数组)不在官方页面里
85
- > —— 它的取值类型在宿主 schema 里表达不可靠,继续由浮层设置页签与
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 服务时,全部设置退回该文件。
87
91
  >
88
92
  > **A 线开关**:设置里的「门禁 → 收窄 lead 工具面」(`gates.leadToolFace`,默认 `on`)决定
89
93
  > 是否把执行类工具(`bash/write/edit/grep/glob`)从 lead 手上拿走、交给角色子代理。
90
94
  > 也可用 `config.leadToolFace` 或环境变量 `DSH_EXPERT_TEAM_LEAD_TOOLFACE=off` 关闭。
91
95
 
96
+ ### 装上之后怎么用
97
+
98
+ 1. 装完**重启一次 `dsh web`**:插件加载时会把「专家团模式」预设自动铺到 `$DSH_HOME/.agent-presets/expert-team`
99
+ (带版本戳,升级会整目录重铺)—— **不需要手工创建预设**。重启后预设选择器里就有「专家团模式」,
100
+ 12 个角色化专家 subagent 工具也全部就位(可在 `设置 → 插件 → 插件列表 → 会话插件` 看到它们)。
101
+ 2. 会话切到「专家团模式」,再 `/team <一句话目标>`。
102
+ 3. 改设置走 **设置 →「专家团」**(值存在宿主命名空间 `expert-team`,随插件市场的备份/恢复一起走)。
103
+
104
+ **排障**:预设丢了、或被同名预设占住 —— **重启一次 `dsh web` 即自愈**(插件加载会重铺),也可跑一次 `/team <任务>`。
105
+ 细节见下面「排障」一节,其中包含那条最容易踩的坑:**不要**用 `expert-team` 这个 id 去「创建 preset」。
106
+
107
+ ### 自定义预设(想改专家团默认行为时)
108
+
109
+ - **创建**:`设置 → Agent 预设 → 用「创造模式」创作自定义预设`(其机制是"复制一份既有预设",产出落在 `$DSH_HOME/.agent-presets/<id>/`)。
110
+ - **要定制专家团,请以「专家团模式」为源、换一个你自己的 id**(例如 `my-team`):复制出来的目录天然带上 12 个角色工具与它的 skill 目录,
111
+ 你的改动只写在 `my-team/` 里。**永远不要改 `expert-team/` 里的文件** —— 那份是插件所有的,加载/升级会整目录重铺覆盖。
112
+ - 新建的预设**可能要重启 `dsh web` 后**才出现在选择器里(宿主名册在启动时读取)。
113
+
92
114
  ## 快速上手
93
115
 
94
116
  ```
@@ -137,6 +159,21 @@ npm run check:name # 检查占位包名残留
137
159
  是**开发机专用**门禁:`gate:sync` 比对本机 `$DSH_HOME` 下的自举副本,`gate:preset` 借用本机
138
160
  dsh 安装里插件自带的 Config schema(dsh 路径自动探测,可用 `DSH_INSTALL` 覆盖),因此**不在 CI 里跑**。
139
161
 
162
+ ## 排障
163
+
164
+ **设置菜单里找不到「专家团」那一页?** 确认版本 ≥ 1.3.0(`dsh plugin --profile web add @yangdcm/dsh-expert-team`
165
+ 升级,或 `npm view @yangdcm/dsh-expert-team version` 看线上版本),然后**重启 `dsh web`** —— 1.3.0 之前没有这一页。
166
+
167
+ **「专家团模式」显示成裸 id(`expert-team`)、会话里没有 12 个角色工具?** 说明
168
+ `$DSH_HOME/.agent-presets/expert-team/` 里不是我们那份(被删过,或被一份同名预设占了)。按优先级修:
169
+
170
+ 1. 跑一次 `/team <任意小任务>` —— 插件会用包内资产重铺一份带版本戳的(最省事;1.3.0 起插件加载时也会自动铺);
171
+ 2. 或从包内覆盖:`cp -f <包路径>/presets/expert-team/{agent.cordis.yml,preset.yml} ~/.dsh/.agent-presets/expert-team/`;
172
+ 3. **不要**用同一个 id 在「设置 → Agent 预设」里「创建 preset」——那只会得到你选的**源**的组合(例如标准模式),
173
+ 不是我们这份。
174
+
175
+ 改完**重启 `dsh web`**(预设名册在启动时固化,刷新页面不够),再开**新会话**(预设只在会话创建时固定)。
176
+
140
177
  ## 已知限制
141
178
 
142
179
  - **本机来源守卫已就位,但不是鉴权**: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
- * 设置页签:**只做三件事** —— 读(GET /settings)、渲染(照 schema)、存(POST,自动保存)。
1036
- * 失败一律照实显示(400 的错误列表 / 网络错误),**并把控件回滚到服务端的值** ——
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 SettingsTab() {
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
- fetch('/plugins/dsh-expert-team/settings').then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
1046
- if (alive && d && d.ok) setData(d)
1047
- }).catch(function (e) { if (alive) setErr(String(e && e.message ? e.message : e)) })
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 (err) return h('div', { className: 'exp-settings' }, h('div', { style: { color: '#c0392b' } }, ' ' + esc(err)))
1065
- if (!data || !data.schema) return h('div', { className: 'exp-empty' }, t('(正在读取设置…)', '(loading settings…)'))
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,30 @@ 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: 72 },
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: 150 },
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
- r.hint ? h('span', { className: 'exp-muted', style: { marginLeft: 6 } }, esc(r.hint)) : null))
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' }, rows, h('div', { className: 'exp-muted', style: { marginTop: 6 } }, esc(msg || '')))
1147
+ return h('div', { className: 'exp-settings' },
1148
+ // 这句必须按**事实**说:宿主设置可用时(默认,也就是官方面板与浮层都在写的那个真源)
1149
+ // 写入会经 `watch` → `reapplySettingsDerived()` 在**进程内即时重算**上限与档位门
1150
+ // (`lib/command.js:5476` 的保存回执原文就是"无需重启");只有**回退到 settings.json**
1151
+ // 时才有"重启后生效"的保守提示,且那条由每次保存的回执逐次给出(`needsRestart`)。
1152
+ // 所以这里既不写"两类需重启"(在默认形态下是假的),也不写"永远不用重启"(回退路径会假)。
1153
+ h('div', { className: 'exp-settings-head' }, esc(t('改动即保存。宿主设置可用时(默认)上限与档位门在进程内即时重算,无需重启;若保存提示"重启后生效",按提示操作。', 'Saved on change. With host settings available (the default), caps and the tier gate are recomputed in-process — no restart needed; if a save says "applies after restart", follow that.'))),
1154
+ rows,
1155
+ h('div', { className: 'exp-settings-msg' + (err ? ' bad' : '') }, esc(err ? '✗ ' + err : (msg || ''))))
1093
1156
  }
1094
1157
 
1095
1158
  function DecisionCard(props) {
@@ -1944,9 +2007,13 @@ window.__ModuleLoader__.load({
1944
2007
  var headCls = 'exp-head' + (docked || viewMode ? '' : ' exp-grab')
1945
2008
 
1946
2009
  try {
1947
- var TAB_ZH = { team: '人', tasks: '事', info: '料', board: '盘', settings: '设' }
2010
+ // 「设」页签已移除:设置页搬进**官方「设置」菜单**(`settings.section` 槽,见 apply())。
2011
+ // 为什么改:浮层页签是一个只有打开浮层才够得着的第二入口,且它依赖一个「读取设置」的
2012
+ // 自建请求路径 —— 一旦宿主路由没注册就永远停在「正在读取设置…」。搬进官方菜单后与
2013
+ // 其它插件的设置同一入口、同一套面板 chrome。
2014
+ var TAB_ZH = { team: '人', tasks: '事', info: '料', board: '盘' }
1948
2015
  var tabBar = h('div', { className: 'exp-tabs' },
1949
- ['team', 'tasks', 'info', 'board', 'settings'].map(function (tb) {
2016
+ ['team', 'tasks', 'info', 'board'].map(function (tb) {
1950
2017
  return h('span', { key: tb, className: 'exp-tab' + (tab === tb ? ' on' : ''), onClick: function () { setTab(tb) } },
1951
2018
  esc((langNow === 'en' ? TAB_ZH[tb] : TAB_ZH[tb])) + (tb === 'tasks' ? ' ' + tasks.length : ''))
1952
2019
  }),
@@ -2189,7 +2256,7 @@ window.__ModuleLoader__.load({
2189
2256
  h('ol', { className: 'exp-path' }, stepper),
2190
2257
  (data.pendingDecision && data.pendingDecision.options && data.pendingDecision.options.length) ? h(DecisionCard, { decision: data.pendingDecision, onChoose: function (id, label) { decide({ choice: label || id }) } }) : null,
2191
2258
  tabBar,
2192
- tab === 'team' ? teamTab : (tab === 'tasks' ? tasksTab : (tab === 'board' ? boardTab : (tab === 'settings' ? h(SettingsTab, null) : infoTab)))
2259
+ tab === 'team' ? teamTab : (tab === 'tasks' ? tasksTab : (tab === 'board' ? boardTab : infoTab))
2193
2260
  ] : null))
2194
2261
  } catch (e) {
2195
2262
  return h('div', { className: 'exp-preview' }, 'PANEL ERR: ' + esc(String(e && e.stack ? e.stack : e)).slice(0, 800))
@@ -2253,14 +2320,7 @@ window.__ModuleLoader__.load({
2253
2320
  function toggle() { if (!isOpen) { dockState = true; saveLS('et-dock', true); notify(); setOpen() } else setOpen() }
2254
2321
  // CSS must exist for the HEADER button too — the panel's own <style> is
2255
2322
  // only in the DOM while the panel is open. Inject once, globally.
2256
- try {
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) {}
2323
+ ensureCss()
2264
2324
  var badge = teamBadge(live)
2265
2325
  var tip = '专家团 · ' + badge.title + '|点击' + (isOpen ? '收起' : '打开') + '并排面板(全屏画布:切到上方「🧑‍🔬 专家团」标签)'
2266
2326
  return h('button', { className: 'exp-open exp-live-badge' + (badge.cls || ''), title: tip, onClick: toggle },
@@ -2397,6 +2457,22 @@ window.__ModuleLoader__.load({
2397
2457
  exports._syncPending = syncPending
2398
2458
  exports._resolvePending = resolvePending
2399
2459
 
2460
+ // CSS 必须在这里就注入:官方「设置」面板可以在**没有会话**(新会话/首屏)时打开,
2461
+ // 而原来这份 CSS 只挂在 HeaderButton 的渲染里 —— 那样设置面板会是一堆无样式的裸控件。
2462
+ ensureCss()
2463
+
2464
+ // 设置页进**官方设置菜单**(`settings.section` 是宿主 ui-settings-general 声明、其它
2465
+ // 插件(agent-preset / models / plugins)同样在用的槽)。`ctx.slots.inject` 等的是**声明**
2466
+ // 而不是包顺序:宿主没声明该槽时这条贡献不会挂上,插件其余部分照常工作。
2467
+ ctx.slots.inject('settings.section', function () {
2468
+ return ctx.slots.register({
2469
+ name: 'settings.section',
2470
+ id: 'expert-team',
2471
+ order: 50,
2472
+ label: function () { return t('专家团', 'Expert team') }
2473
+ }, wrap(SettingsSection))
2474
+ })
2475
+
2400
2476
  ctx.slots.inject('conversation.session.header.actions', function () {
2401
2477
  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
2478
  })
package/lib/command.js CHANGED
@@ -886,10 +886,40 @@ async function uninstallInstalled() {
886
886
  '说明:skill 现在默认走**运行时注册**,本来就不写盘;这里回收的是历史版本留下的副本,',
887
887
  '或宿主没有 skill 注册表时的回退副本。',
888
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
+ '本命令清的是当前副本,真正的卸载是在插件市场里移除插件本身。',
889
896
  );
890
897
  return { kind: 'success', text: lines.join('\n') };
891
898
  }
892
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
+
893
923
  async function ensureSkillInstalled() {
894
924
  // 运行时注册成功 ⇒ 一个文件都不写(默认路径;也是"卸载即干净"的前提)
895
925
  if (RUNTIME_SKILL_REGISTERED) return SKILL_DIR;
@@ -4478,6 +4508,16 @@ export function apply(ctx, config) {
4478
4508
  base: currentSettings(),
4479
4509
  onResolved: () => reapplySettingsDerived(),
4480
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());
4481
4521
  void loadSessionRuns(); // session→run memory for overlay auto-select
4482
4522
  ctx.commands.register({
4483
4523
  name: 'team',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
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": {