dsh-agent-foreman 0.1.0 → 0.2.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/README.md CHANGED
@@ -59,13 +59,22 @@ standing system prompt is needed.
59
59
 
60
60
  ### Where members show up
61
61
 
62
- There is no custom in-chat panel any more: in team mode a contractor **is** a real Agent
63
- Teams member, so the native `Agent Teams` button in the session header lists them and
64
- shows each one's model. The settings page keeps the full roster editor.
62
+ **Settings → 包工头 → 实时工地** is a polling board over `api/live`: whether the foreman is
63
+ working, each contractor's role and model, and every order's status / dependencies / error /
64
+ artifact path for the current round. It pauses itself while the page is hidden.
65
+
66
+ The important part: the board shows both the **declared model** (what the roster says) and the
67
+ **actual model** (read back from the child session's header). When they differ the row is
68
+ flagged — that is the direct answer to "which model is actually running this order". In team
69
+ mode the actual model comes from the host's `listMembers()` model field.
70
+
71
+ In team mode a contractor **is** a real Agent Teams member, so the native `Agent Teams` button in
72
+ the session header lists them too. The settings page keeps the full roster editor.
65
73
 
66
74
  Roster notes:
67
75
 
68
- - A contractor is a name, an optional prompt, a provider, a model, an effort level and a concurrency cap.
76
+ - A contractor is a name, an optional role (a lightweight trade label for routing, not a fixed
77
+ persona), an optional prompt, a provider, a model, an effort level and a concurrency cap.
69
78
  **Leave the provider on "follow the current session" and the child simply runs on whatever model
70
79
  the parent session is using.**
71
80
  - To use a different model: pick the provider first — **the model dropdown is derived from it** and
@@ -73,13 +82,43 @@ Roster notes:
73
82
  - Settings page collapses policy behind "派单策略"; the roster table keeps name / provider / model /
74
83
  effort / concurrency / enabled, and 👁 opens that contractor's prompt editor.
75
84
 
85
+ ### The roster *is* the whitelist
86
+
87
+ The host keeps its own **subagent model whitelist** (`subagent-model-selection`), which decides
88
+ which provider/model routes child agents may use. That list and the roster are configured
89
+ separately, so when they drift apart the roster says `trae` while the children quietly run
90
+ something else — with no error anywhere.
91
+
92
+ This plugin folds that into one place: **every enabled contractor with an explicit provider+model
93
+ is collected into the host whitelist automatically.** You only maintain the roster.
94
+
95
+ - Synced once at plugin startup, again after saving the roster, and on demand via
96
+ `POST /api/whitelist/sync`.
97
+ - If no contractor declares an explicit route (all "follow the current session"), the host
98
+ configuration is left untouched — an empty list makes the host throw.
99
+ - Applies to **new sessions only**. The host snapshots the whitelist into a session when that
100
+ session is first created, so existing sessions keep the old list; open a new session after
101
+ changing the roster.
102
+ - A failed sync never blocks dispatch — it only logs. `/api/state` exposes the live `whitelist`
103
+ field.
104
+
76
105
  Dispatch notes:
77
106
 
78
107
  - `orders[].task` **must be self-contained** — the child cannot see the parent's context.
79
- - `orders[].contractor` rotates through the roster when omitted; unknown names fail loudly.
80
- - `orders[].dependsOn` only references orders in the same batch; dependents run serially after.
108
+ - `orders[].contractor` accepts a contractor **name or id**, or a `role` trade label; name/id
109
+ matching takes precedence over role matching. Multiple instances with that role rotate among
110
+ themselves, an omitted contractor rotates through the whole roster, and an unknown selector
111
+ reports an error instead of silently reassigning the work.
112
+ - `orders[].dependsOn` only references orders in the same batch. An order becomes ready as soon
113
+ as **all its dependencies complete successfully**; it does not wait for an entire dependency
114
+ layer. Unrelated work still runs concurrently, up to the global default of **4** (maximum 8)
115
+ and each contractor's own concurrency cap.
116
+ - Failure, timeout or cancellation of an order marks its downstream orders **skipped**, and a
117
+ skipped dependency blocks the rest of its chain. The board records the blocking error rather
118
+ than executing downstream work with missing inputs.
81
119
  - `waitSeconds` caps the whole batch; unfinished orders are aborted rather than silently reported done.
82
- - `concurrency` defaults to 4, capped at 8.
120
+ - Live status exposes dependency relationships, errors and artifact paths alongside progress;
121
+ it is a status view, not a separate real-time DAG editor.
83
122
 
84
123
  ## Policy
85
124
 
@@ -96,7 +135,12 @@ Dispatch notes:
96
135
  - Windows: `%LOCALAPPDATA%/dsh-agent-foreman/`
97
136
  - Elsewhere: `<baseDir>/data/dsh-agent-foreman/`
98
137
 
99
- `roster.json` (contractors + policy), `runs.jsonl` (one line per dispatch round).
138
+ `roster.json` stores contractors (including roles) and policy. Run snapshots live under `runs/`.
139
+ After restarting, you can query and review a **settled** board from its saved snapshot; work that
140
+ was interrupted is not automatically re-dispatched or resumed. Recovery restores explicit status
141
+ for inspection and review, not a background execution engine. Full delivery text, when available,
142
+ is stored separately under `artifacts/` and linked from the board by its path.
143
+
100
144
  A corrupt roster never blocks startup — it is treated as empty and logged.
101
145
 
102
146
  ## Security
@@ -108,44 +152,84 @@ A corrupt roster never blocks startup — it is treated as empty and logged.
108
152
  ## Development
109
153
 
110
154
  ```bash
111
- node --test test/*.test.mjs # 26 cases: tools, engine, routes, CSRF, ledger
155
+ npm install --ignore-scripts
156
+ npm test
112
157
  ```
113
158
 
114
- `test/harness.test.mjs` runs the real host half against a fake host, so no DSH instance is needed.
159
+ Install development dependencies first: the schema package is both a runtime peer dependency and
160
+ a development dependency used by the test suite. `test/harness.test.mjs` runs the real host half
161
+ against a fake host, so no DSH instance is needed. The suite covers tools, engine, routes, CSRF,
162
+ ledger and compatibility regressions.
115
163
 
116
164
  ## Compared to
117
165
 
118
- | | Agent Team (built-in) | swarm-orchestrator | Foreman |
166
+ | | Agent Team (built-in) | dsh-agent-teams | Foreman |
119
167
  | --- | --- | --- | --- |
120
- | Per-member model | no | no | **yes** |
121
- | Preset roles | fixed | 4 fixed roles | none — you define |
122
- | Shared board | yes | yes | yes |
123
- | Rework loop | no | limited | yes, with feedback |
124
- | Settings page | yes | no | yes, roster + live model catalog |
168
+ | Per-member model | no | no | **yes** — `agentOptions` locked at creation |
169
+ | Cross-round memory | no | yes — previousResult | yes — history in prompt |
170
+ | Native team tab | yes | yes | yes — shared tasks |
171
+ | Self-report recovery | no | yes — update_task | yes — `foreman_report` |
172
+ | Real route echo | no | no | **yes** — declared vs actual model |
173
+ | Whitelist sync | no | no | **yes** — roster is the single source |
174
+ | Job site panel | no | yes — activity panel | **yes** — header button + real model |
175
+ | Dep scheduling + failure block | no | yes — DAG | yes — ready scheduling + skipped |
176
+ | Rework loop | no | manual | yes — auto with feedback |
177
+ | Settings page | yes | no | yes — roster + model catalog |
178
+
179
+ **Key difference**: Foreman's **per-member model selection + real route echo** is unique.
180
+ dsh-agent-teams excels at full task DAG (claim/attempt/quality gates) for complex multi-phase
181
+ projects; Foreman focuses on **model-based dispatch and per-order review** with fewer tools
182
+ (5 vs 14) and faster onboarding.
125
183
 
126
184
  ## Team mode (experimental)
127
185
 
128
186
  Off by default. Tick **团队模式** under Settings → 包工头 → 派单策略 and each
129
187
  contractor becomes a real member of the host's **agent team**: the plugin spawns
130
188
  it with `agentTeams.spawnTeammate`, switches its session model with
131
- `sessions.selectModel`, sends the work order as a team message, then reads the
189
+ `sessionController.selectModel`, sends the work order as a team message, then reads the
132
190
  delivery back from the member's session events.
133
191
 
134
192
  Members therefore show up in the team area and can change model in their own
135
193
  session. Every step depends on host team services, so **any failure falls back to
136
194
  the plain activation path** and the board's `teamMode` field says which one ran.
195
+
196
+ **Why delivery waits for `active`**: the host's `sendMessage` throws
197
+ `active teammate "worker" not found` until the member leaves `provisioning` and becomes
198
+ `active`. The plugin therefore polls `listMembers()` before delivering; if the member never
199
+ gets there (or reports `failed`), it falls back to the plain path instead of surfacing an error.
137
200
  Known trade-offs:
138
201
 
139
202
  - Member names must be ASCII (`lower-kebab-case`), so Chinese contractor names all
140
203
  become `worker`, `worker-2`, … — the Chinese name and model live in the member
141
204
  description, which the team area shows.
142
- - A member's first turn runs on the parent's model (the model can only be switched
143
- after the session exists — hence the one-line handshake).
205
+ - A member's first turn runs on the parent's model (`spawnTeammate` accepts no model field —
206
+ hence the one-line handshake).
207
+ - The member's **actual** model is echoed by `listMembers()`, written into the board's
208
+ `teamMode` note; if the roster model differs it is shown side by side.
144
209
  - Switching is bounded by the host's per-subagent model allow-list; values outside
145
210
  it are refused and the board shows `model switch: failed · reason`.
146
211
  - Members stay in the team after the round.
147
212
 
148
213
 
214
+ ## Changelog
215
+
216
+ ### 0.2.0 (2026-10-28)
217
+
218
+ - **Self-member system**: `subagents.startActivation` + `agentOptions` locks model at creation,
219
+ no longer relying on `spawnTeammate` (which cannot specify models)
220
+ - **Cross-round memory**: history concatenated into prompt (like agent-teams' previousResult)
221
+ - **`foreman_report` self-report tool**: product recovery without reading member sessions
222
+ - **Real route echo**: `actualRoute` / `routeMismatch` — declared vs actual model comparison
223
+ - **Whitelist sync**: roster auto-syncs to host subagent model whitelist
224
+ - **Job site panel**: "🏗 工地" button in session header with live model and order status
225
+ - **Dependency-ready scheduling**: no layer barriers, failure blocks downstream as `skipped`
226
+ - **Snapshot recovery**: `runs/` atomic snapshots, queryable after restart, never auto-reruns
227
+ - **Roster roles**: `role` field supports role-based order rotation
228
+
229
+ ### 0.1.0
230
+
231
+ - Initial release: roster management, dispatch engine, board, settings page
232
+
149
233
  ## License
150
234
 
151
235
  MIT
package/README.zh.md CHANGED
@@ -84,30 +84,56 @@ dsh plugin add E:/path/to/dsh-agent-foreman
84
84
 
85
85
  ### 成员在哪看
86
86
 
87
- 不再自建聊天浮层了:契约商在团队模式下本身就是宿主的「智能体团队」成员,
88
- 点会话头的 `智能体团队` 就能看到谁在、各自什么模型。设置页仍保留完整的
89
- 「承包人岗位 + 模型 + 提示词 + 并发」编辑。
87
+ **设置 → 包工头 → 实时工地**:一块轮询 `api/live` 的看板,显示包工头在不在跑、
88
+ 每个承包商的岗位与模型、本轮每张工单的状态 / 依赖 / 错误 / 产物路径。页面切到后台
89
+ 就自动停表,不占资源。
90
+
91
+ 关键一点:看板同时显示**声明模型**和**真实模型**。声明的是名册里配的那套,真实的是
92
+ 从子会话的 header 里读回来的实际路由。两者不一致时该行会标出来——这正好回答了
93
+ "它到底跑的是谁的模型"。团队模式下则读宿主 `listMembers()` 回显的成员实际模型。
94
+
95
+ 团队模式下承包商本身就是宿主的「智能体团队」成员,点会话头的 `智能体团队` 也能看到。
90
96
 
91
97
  ### 名册要点
92
98
 
93
- - 承包商 = 名字 + 提示词 + provider + 模型 + 思考等级 + 并发上限。**provider 选「跟随当前会话」时,
94
- 子智能体直接跑父会话当前的模型**,不需要知道它是什么。
99
+ - 承包商 = 名字 + 可选岗位(`role`,只用于派单路由,不是固定角色体系)+ 提示词 + provider +
100
+ 模型 + 思考等级 + 并发上限。**provider 选「跟随当前会话」时,子智能体直接跑父会话当前的模型**,
101
+ 不需要知道它是什么。
95
102
  - 想换模型就分两步:先选 provider,**模型下拉只列这个 provider 自己的模型**(由 provider 推出来,
96
103
  没有模型的 provider 不会出现在下拉里)。
97
- - 设置页把策略收进了「派单策略」折叠区,名册只留 名字 / provider / 模型 / 思考等级 / 并发 /
104
+ - 设置页把策略收进了「派单策略」折叠区,名册只留 名字 / 岗位 / provider / 模型 / 思考等级 / 并发 /
98
105
  启用,点名字后面的 👁 编辑这个承包商的提示词(派单时塞进工单,和工单冲突时以工单为准)。
99
106
 
107
+ ### 名册就是白名单
108
+
109
+ 宿主自己有一份**子智能体模型白名单**(设置项 `subagent-model-selection`),它决定子智能体
110
+ 被允许跑哪些 provider/模型。这份名单和名册是两处独立配置,一旦不一致就会出现"名册里写着
111
+ trae,实际却在跑别的模型"——而且没有任何报错。
112
+
113
+ 本插件把这件事收成一处:**名册里所有启用且配了 provider+model 的承包商,会自动汇总进宿主
114
+ 白名单**。你只需要维护名册。
115
+
116
+ - 插件启动时同步一次;设置页保存名册后再同步一次;也可 `POST /api/whitelist/sync` 手动触发。
117
+ - 名册里没有任何显式路由(都用「跟随当前会话」)时**不动**宿主原配置——空名单会让宿主报错。
118
+ - 只影响**新会话**。宿主的白名单在会话首次建立时就被快照进该会话了,已经开着的会话改不了,
119
+ 所以改完名册要**开新会话**才生效。
120
+ - 同步失败不会影响派单,只记日志;`/api/state` 的 `whitelist` 字段能看当前真实名单。
121
+
100
122
  ![设置页的名册](docs/settings-roster.png)
101
123
 
102
124
  ### 派单参数要点
103
125
 
104
126
  - `orders[].task` **必须自足**:子智能体看不到父会话的任何上下文,背景、要动的文件、
105
127
  期望结果都要写进去。
106
- - `orders[].contractor` 写承包商名字;省略则全名册轮转;点名不存在的人会直接报错并列出可选名字。
107
- - `orders[].dependsOn` 只能引用本轮内的工单;有依赖的工单会排在后面串行跑。
128
+ - `orders[].contractor` 可写承包商**名字或 id**,也可写 `role` 岗位名;名字/id 匹配优先于岗位匹配。
129
+ 同岗位的多个实例之间轮转,省略时全名册轮转;不存在的名字、id 或岗位会直接报错,不会悄悄换人。
130
+ - `orders[].dependsOn` 只能引用本轮内的工单。所有依赖都成功完成后,这张工单就会开工,
131
+ **不需要等整张拓扑层跑完**;无依赖关系的工单仍可并行,受总并发默认 **4**(上限 8)和单人并发上限约束。
132
+ - 某张工单失败、超时或被取消后,它的下游工单标记为 **skipped**;被跳过的依赖继续阻断更下游,
133
+ 不会在缺少上游产物时硬跑。看板会保留阻断原因,便于验收和排查。
108
134
  - `waitSeconds` 是这一轮的总墙钟上限,到点会中止没跑完的工单并把现状返回——不会让父智能体
109
135
  拿到一份假的"全部完成"。
110
- - `concurrency` 默认 4,上限 8。
136
+ - 实时状态会显示依赖关系、错误和产物路径;它是状态视图,不另造一套实时 DAG 编辑界面。
111
137
 
112
138
  ### 验收与返工
113
139
 
@@ -132,8 +158,11 @@ dsh plugin add E:/path/to/dsh-agent-foreman
132
158
  - Windows:`%LOCALAPPDATA%/dsh-agent-foreman/`
133
159
  - 其它平台:`<baseDir>/data/dsh-agent-foreman/`
134
160
 
135
- 里面两个文件:`roster.json`(名册与策略)、`runs.jsonl`(每轮台账)。
136
- 坏了不会阻断启动:读不出来的名册会被当成空名册并记一条日志。
161
+ `roster.json` 保存名册(含岗位)与策略;运行快照保存在 `runs/`。重启后可以查询、验收已经落定的
162
+ 看板快照,但**中断的工作不会自动重新派发或续跑**。这里的显式恢复是恢复可检查、可验收的状态,
163
+ 不是偷偷挂一个后台执行引擎。完整交付正文在可用时另存于 `artifacts/`,看板通过产物路径关联。
164
+
165
+ 名册坏了不会阻断启动:读不出来的名册会被当成空名册并记一条日志。
137
166
 
138
167
  ## 安全边界
139
168
 
@@ -161,13 +190,21 @@ node --test test/*.test.mjs # 37 条用例:工具、引擎、路由、CSRF
161
190
 
162
191
  这样承包商既能在「智能体团队」区看见,也能在它自己的会话里换模型。
163
192
 
193
+ **为什么必须等成员 active 再投递**:宿主 `agent-teams` 的 `sendMessage` 在成员还没
194
+ 进入 `active` 阶段时会直接抛 `active teammate "worker" not found`。成员 spawn 之后
195
+ 要先经历 `provisioning`(初始 prompt 被确认),才转 `active`。本插件在投递前会轮询
196
+ `listMembers()` 等这个状态;等不到(宿主没有该接口 / 成员 failed)就退回普通模式,
197
+ 不把失败抛给用户。
198
+
164
199
  这条链路依赖宿主的团队服务,因此**任何一步不可用都会自动退回普通模式**,
165
200
  看板每单的 `teamMode` 字段会写明是「团队模式」还是「失败退回」。已知取舍:
166
201
 
167
202
  - 成员名只能是 ASCII(宿主只收 `lower-kebab-case`),所以中文承包商统一落成
168
203
  `worker`、`worker-2`…,中文名与模型写在成员的描述里;
169
- - 成员第一轮用的是父会话的模型(建成员时给不了模型),切模型从第二轮才生效
170
- ——所以建成员时只发一句「待命」,工单随后再发;
204
+ - 成员第一轮用的是父会话的模型(`spawnTeammate` 本身不接受 model 字段,建成员时给不了),
205
+ 切模型从第二轮才生效——所以建成员时只发一句「待命」,工单随后再发;
206
+ - 成员的**实际模型**由宿主 `listMembers()` 的 `model` 字段回显,会写进看板的 `teamMode`
207
+ 备注;名册配的模型与它不一致时会并列写出;
171
208
  - 切模型受宿主给子会话的**模型白名单**限制,白名单里没有的 provider/模型会被拒,
172
209
  这种单在看板上会写明「模型切换:failed · 原因」;
173
210
  - 每轮派单结束时成员会留在团队里(可继续用,也可手动删除);
@@ -175,14 +212,39 @@ node --test test/*.test.mjs # 37 条用例:工具、引擎、路由、CSRF
175
212
 
176
213
  ## 与其它方案的区别
177
214
 
178
- | | 智能体团队(内置) | swarm-orchestrator | 包工头 |
215
+ | | 智能体团队(内置) | dsh-agent-teams | 包工头 |
179
216
  | --- | --- | --- | --- |
180
- | 成员可选模型 | ✗ | ✗ | **✓** |
181
- | 角色预设 | 固定 | 4 个固定角色 | 不预置,你定义 |
182
- | 共享看板 | ✓ | ✓ | ✓ |
183
- | 返工闭环 | ✗ | 有限 | ✓ 带反馈重派 |
217
+ | 成员可选模型 | ✗ | ✗ | **✓** `agentOptions` 创建时锁定 |
218
+ | 跨轮记忆 | ✗ | ✓ previousResult | ✓ 历史拼 prompt |
219
+ | 原生团队标签页 | ✓ | ✓ | ✓ 共享任务 |
220
+ | 自报回收 | ✗ | ✓ update_task | ✓ `foreman_report` |
221
+ | 真实路由回显 | ✗ | ✗ | **✓** 声明 vs 实际模型对照 |
222
+ | 白名单同步 | ✗ | ✗ | **✓** 名册 = 宿主白名单唯一来源 |
223
+ | 工地看板 | ✗ | ✓ 活动面板 | **✓** 会话头「工地」+ 真实模型 |
224
+ | 依赖调度 + 失败阻断 | ✗ | ✓ DAG | ✓ 就绪调度 + skipped 递归阻断 |
225
+ | 返工闭环 | ✗ | 手动 | ✓ 带反馈自动重派 |
184
226
  | 设置页 | ✓ | ✗ | ✓ 名册 + 模型目录 |
185
227
 
228
+ **核心差异**:包工头的**模型可选 + 真实路由回显**是其它方案没有的。​dsh-agent-teams 的强项是完整任务 DAG(claim/attempt/quality gates),适合复杂多阶段项目;包工头侧重**按模型拆单、逐单验收**,工具更少(5 vs 14),上手更快。
229
+
230
+ ## 版本说明
231
+
232
+ ### 0.2.0(2026-10-28)
233
+
234
+ - **自建成员系统**:`subagents.startActivation` + `agentOptions` 锁定模型,不再依赖 `spawnTeammate`(后者无法指定模型)
235
+ - **跨轮记忆**:历史产出拼进 prompt(同 agent-teams 的 previousResult)
236
+ - **`foreman_report` 自报工具**:产物回收不依赖读成员会话(同 agent-teams 的 update_task)
237
+ - **真实路由回显**:`actualRoute` / `routeMismatch`,声明模型 vs 实际模型对照
238
+ - **白名单同步**:名册自动同步宿主子智能体模型白名单
239
+ - **工地看板**:会话头「🏗 工地」按钮,实时显示承包商模型与工单状态
240
+ - **依赖就绪调度**:不整层等待,失败递归阻断为 `skipped`
241
+ - **快照恢复**:`runs/` 原子快照,重启后可查可验收,绝不自动重跑
242
+ - **名册岗位**:`role` 字段支持按岗位轮转派单
243
+
244
+ ### 0.1.0
245
+
246
+ - 初始版本:名册管理、派单引擎、看板、设置页
247
+
186
248
  ## License
187
249
 
188
250
  MIT
package/lib/client.js CHANGED
@@ -282,6 +282,99 @@ window.__ModuleLoader__.load({
282
282
  );
283
283
  }
284
284
 
285
+ /** 工地面板:轮询 /api/live,显示包工头与承包商的实时状态。 */
286
+ function WorksitePanel({ onClose }) {
287
+ const [live, setLive] = react.useState(null);
288
+ react.useEffect(() => {
289
+ let cancelled = false;
290
+ const tick = () => {
291
+ fetch(LIVE_URL, { headers: { accept: 'application/json' } })
292
+ .then(res => res.json())
293
+ .then(body => { if (!cancelled) setLive(body); })
294
+ .catch(() => {});
295
+ };
296
+ tick();
297
+ const timer = setInterval(() => { if (!document.hidden) tick(); }, 2500);
298
+ return () => { cancelled = true; clearInterval(timer); };
299
+ }, []);
300
+ const t = s => (zh() ? s : s);
301
+ return h('div', {
302
+ style: {
303
+ position: 'absolute', top: '36px', right: '0', zIndex: 100,
304
+ width: '340px', maxHeight: '480px', overflowY: 'auto',
305
+ background: 'var(--dsw-alias-bg-layer-1, #232327)', color: 'var(--dsw-alias-label-primary, #e8e8ea)',
306
+ border: '1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3))',
307
+ borderRadius: '10px', padding: '12px', fontSize: '12px',
308
+ boxShadow: '0 16px 40px rgba(0,0,0,.45)',
309
+ },
310
+ },
311
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' } },
312
+ h('strong', null, zh() ? '工地' : 'Job Site'),
313
+ h('button', { onClick: onClose, style: { background: 'none', border: 'none', color: 'var(--dsw-alias-label-tertiary, #9a9aa2)', cursor: 'pointer', fontSize: '14px' } }, '✕'),
314
+ ),
315
+ live === null
316
+ ? h('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)' } }, zh() ? '加载中…' : 'Loading…')
317
+ : h(WorksiteBoard, { live }),
318
+ );
319
+ }
320
+
321
+ /** 工地看板本体:包工头 + 承包商 + 本轮工单。 */
322
+ function WorksiteBoard({ live }) {
323
+ const lead = live.lead || {};
324
+ const members = live.members || [];
325
+ const run = live.run;
326
+ const t = s => (zh() ? s : s);
327
+ return h('div', null,
328
+ // 包工头状态
329
+ h('div', { style: { marginBottom: '8px', padding: '6px 8px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2, rgba(128,128,128,.1))' } },
330
+ h('span', { style: { fontWeight: '600' } }, '👷 ' + (lead.name || t('包工头'))),
331
+ h('span', { style: { marginLeft: '6px', color: lead.status === 'running' ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary, #9a9aa2)' } },
332
+ lead.status === 'running' ? (zh() ? '· 干活中' : '· working')
333
+ : lead.waiting ? (zh() ? '· 待验收' : '· awaiting review')
334
+ : (zh() ? '· 空闲' : '· idle')),
335
+ ),
336
+ // 承包商网格
337
+ h('div', { style: { fontWeight: '600', margin: '8px 0 4px' } }, zh() ? '承包商' : 'Contractors'),
338
+ members.map(m => h('div', {
339
+ key: m.id || m.name,
340
+ style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '3px 0', borderBottom: '1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,.15))' },
341
+ },
342
+ h('span', { style: { width: '7px', height: '7px', borderRadius: '50%', flex: 'none', background: m.status === 'running' ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-bg-layer-3, rgba(128,128,128,.3))' } }),
343
+ h('span', { style: { fontWeight: '600' } }, m.name || ''),
344
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)', flex: '1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, m.model || (zh() ? '跟随父会话' : 'inherit')),
345
+ h('span', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)' } }, (m.orders || 0) + ' 单'),
346
+ )),
347
+ // 本轮工单
348
+ run ? h('div', null,
349
+ h('div', { style: { fontWeight: '600', margin: '8px 0 4px' } }, zh() ? '本轮工单' : 'Current orders'),
350
+ (run.orders || []).map(o => h('div', {
351
+ key: o.id,
352
+ style: { padding: '3px 0', borderBottom: '1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,.15))' },
353
+ },
354
+ h('span', { style: { fontFamily: 'monospace', color: 'var(--dsw-alias-label-tertiary, #9a9aa2)' } },
355
+ o.status === 'done' ? '✔ ' : o.status === 'skipped' ? '⊘ ' : (o.status === 'failed' || o.status === 'timeout') ? '✖ ' : '· '),
356
+ h('span', null, o.id + ' ' + (o.title || '')),
357
+ h('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)', fontSize: '11px', marginLeft: '14px' } },
358
+ (o.contractor || '') + (o.route ? ' · ' + o.route : '')),
359
+ o.error ? h('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)', fontSize: '11px', marginLeft: '14px' } }, o.error) : null,
360
+ )),
361
+ ) : h('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9a9aa2)', margin: '8px 0' } }, zh() ? '还没有派单' : 'No orders yet'),
362
+ );
363
+ }
364
+
365
+ /** 会话头的「工地」按钮 + 点开面板。 */
366
+ function WorksiteAction() {
367
+ const [open, setOpen] = react.useState(false);
368
+ return h('div', { style: { position: 'relative', display: 'inline-block' } },
369
+ h('button', {
370
+ onClick: () => setOpen(o => !o),
371
+ title: zh() ? '工地看板' : 'Job Site',
372
+ style: { display: 'inline-flex', alignItems: 'center', gap: '4px', padding: '3px 8px', minHeight: '28px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28))', borderRadius: '6px', background: 'transparent', color: 'var(--dsw-alias-label-secondary, #b9b9c0)', cursor: 'pointer', fontSize: '12px' },
373
+ }, '🏗 ' + (zh() ? '工地' : 'Job Site')),
374
+ open ? h(WorksitePanel, { onClose: () => setOpen(false) }) : null,
375
+ );
376
+ }
377
+
285
378
  /**
286
379
  * 挂载客户端半边。
287
380
  * @param ctx - 客户端 cordis 上下文。
@@ -299,6 +392,7 @@ window.__ModuleLoader__.load({
299
392
  console.warn('[dsh-agent-foreman] slots 服务不可用,设置页不会被注册。');
300
393
  return undefined;
301
394
  }
395
+ // 设置页:名册编辑 + 策略 + 历史。
302
396
  const release = slots.inject('settings.section', () => slots.register({
303
397
  name: 'settings.section',
304
398
  id: SEAT_ID,
@@ -307,6 +401,14 @@ window.__ModuleLoader__.load({
307
401
  label: () => (zh() ? '包工头' : 'Foreman'),
308
402
  }, ForemanSettings));
309
403
  seats.push(release);
404
+ // 会话头:「工地」按钮 + 实时看板(主面板,不藏在设置里)。
405
+ const releaseWorksite = slots.inject('conversation.session.header.actions', () => slots.register({
406
+ name: 'conversation.session.header.actions',
407
+ id: SEAT_ID + '-worksite',
408
+ order: -19,
409
+ label: () => (zh() ? '工地' : 'Job Site'),
410
+ }, WorksiteAction));
411
+ seats.push(releaseWorksite);
310
412
  return undefined;
311
413
  });
312
414
 
@@ -316,7 +418,7 @@ window.__ModuleLoader__.load({
316
418
  release?.();
317
419
  }
318
420
  catch (error) {
319
- console.warn('[dsh-agent-foreman] 释放设置页座位失败:', error);
421
+ console.warn('[dsh-agent-foreman] 释放座位失败:', error);
320
422
  }
321
423
  }
322
424
  seats.length = 0;