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 +103 -19
- package/README.zh.md +80 -18
- package/lib/client.js +103 -1
- package/lib/engine.js +606 -79
- package/lib/index.js +20 -1
- package/lib/page.js +379 -23
- package/lib/routes.js +115 -13
- package/lib/store.js +315 -10
- package/lib/team.js +677 -59
- package/lib/tools.js +92 -5
- package/lib/whitelist.js +320 -0
- package/package.json +67 -64
package/README.md
CHANGED
|
@@ -59,13 +59,22 @@ standing system prompt is needed.
|
|
|
59
59
|
|
|
60
60
|
### Where members show up
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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`
|
|
80
|
-
|
|
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
|
-
-
|
|
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`
|
|
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
|
-
|
|
155
|
+
npm install --ignore-scripts
|
|
156
|
+
npm test
|
|
112
157
|
```
|
|
113
158
|
|
|
114
|
-
|
|
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) |
|
|
166
|
+
| | Agent Team (built-in) | dsh-agent-teams | Foreman |
|
|
119
167
|
| --- | --- | --- | --- |
|
|
120
|
-
| Per-member model | no | no | **yes** |
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
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
|
-
`
|
|
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 (
|
|
143
|
-
|
|
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 +
|
|
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
|

|
|
101
123
|
|
|
102
124
|
### 派单参数要点
|
|
103
125
|
|
|
104
126
|
- `orders[].task` **必须自足**:子智能体看不到父会话的任何上下文,背景、要动的文件、
|
|
105
127
|
期望结果都要写进去。
|
|
106
|
-
- `orders[].contractor`
|
|
107
|
-
|
|
128
|
+
- `orders[].contractor` 可写承包商**名字或 id**,也可写 `role` 岗位名;名字/id 匹配优先于岗位匹配。
|
|
129
|
+
同岗位的多个实例之间轮转,省略时全名册轮转;不存在的名字、id 或岗位会直接报错,不会悄悄换人。
|
|
130
|
+
- `orders[].dependsOn` 只能引用本轮内的工单。所有依赖都成功完成后,这张工单就会开工,
|
|
131
|
+
**不需要等整张拓扑层跑完**;无依赖关系的工单仍可并行,受总并发默认 **4**(上限 8)和单人并发上限约束。
|
|
132
|
+
- 某张工单失败、超时或被取消后,它的下游工单标记为 **skipped**;被跳过的依赖继续阻断更下游,
|
|
133
|
+
不会在缺少上游产物时硬跑。看板会保留阻断原因,便于验收和排查。
|
|
108
134
|
- `waitSeconds` 是这一轮的总墙钟上限,到点会中止没跑完的工单并把现状返回——不会让父智能体
|
|
109
135
|
拿到一份假的"全部完成"。
|
|
110
|
-
-
|
|
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
|
-
|
|
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
|
-
| | 智能体团队(内置) |
|
|
215
|
+
| | 智能体团队(内置) | dsh-agent-teams | 包工头 |
|
|
179
216
|
| --- | --- | --- | --- |
|
|
180
|
-
| 成员可选模型 | ✗ | ✗ | **✓** |
|
|
181
|
-
|
|
|
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]
|
|
421
|
+
console.warn('[dsh-agent-foreman] 释放座位失败:', error);
|
|
320
422
|
}
|
|
321
423
|
}
|
|
322
424
|
seats.length = 0;
|