dsh-agent-foreman 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +235 -3
- package/README.zh.md +250 -0
- package/cordis.patch.yml +11 -0
- package/docs/settings-roster.png +0 -0
- package/icon.svg +8 -0
- package/lib/catalog.js +137 -0
- package/lib/client.js +433 -0
- package/lib/command.js +101 -0
- package/lib/engine.js +1292 -0
- package/lib/index.js +173 -0
- package/lib/page.js +881 -0
- package/lib/routes.js +525 -0
- package/lib/skill.js +79 -0
- package/lib/store.js +705 -0
- package/lib/team.js +1102 -0
- package/lib/tools.js +506 -0
- package/lib/whitelist.js +320 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
package/lib/page.js
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包工头 · 设置页。
|
|
3
|
+
*
|
|
4
|
+
* 这个页面由宿主半边自己吐纯 HTML,客户端半边只用 iframe 把它塞进
|
|
5
|
+
* 「设置 → 包工头」。这样做的代价是不能用宿主的设计系统,好处是不需要打包、
|
|
6
|
+
* 不需要 React、插件装上去就能用——与 dsh-cursor-dev 的面板同一套路子。
|
|
7
|
+
*
|
|
8
|
+
* 页面只做三件事:编名册、调策略、看台账。看板正文在聊天里,不在这里。
|
|
9
|
+
*
|
|
10
|
+
* 名册刻意只有"名字 + 模型"两个必填项:模型留空就是**跟随当前会话**(宿主直接
|
|
11
|
+
* 继承父会话的 provider/模型/思考等级,不需要用户再填一遍 provider)。只有
|
|
12
|
+
* 确实想换人换模型时才从一个下拉里挑——provider 由所选模型自带,不单独成列。
|
|
13
|
+
*
|
|
14
|
+
* 所有字符串都是字面量拼接而不是内层模板字符串,避免和外面这层模板的
|
|
15
|
+
* `${}` 互相干扰。
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-agent-foreman/page
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 页面的静态部分:样式。 */
|
|
21
|
+
const STYLE = String.raw`
|
|
22
|
+
:root {
|
|
23
|
+
color-scheme: light dark;
|
|
24
|
+
--fg: #1f2328;
|
|
25
|
+
--dim: #6b7280;
|
|
26
|
+
--line: #d8dce3;
|
|
27
|
+
--bg: #ffffff;
|
|
28
|
+
--soft: #f6f7f9;
|
|
29
|
+
--accent: #d97706;
|
|
30
|
+
--accent-soft: #fef3c7;
|
|
31
|
+
--ok: #15803d;
|
|
32
|
+
--bad: #b91c1c;
|
|
33
|
+
}
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
:root {
|
|
36
|
+
--fg: #e6e8eb;
|
|
37
|
+
--dim: #9aa4b2;
|
|
38
|
+
--line: #333a45;
|
|
39
|
+
--bg: #16191d;
|
|
40
|
+
--soft: #1d2126;
|
|
41
|
+
--accent: #fbbf24;
|
|
42
|
+
--accent-soft: #3a2f14;
|
|
43
|
+
--ok: #4ade80;
|
|
44
|
+
--bad: #f87171;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
* { box-sizing: border-box; }
|
|
48
|
+
body {
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: 16px;
|
|
51
|
+
font: 13px/1.55 -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
52
|
+
color: var(--fg);
|
|
53
|
+
background: var(--bg);
|
|
54
|
+
}
|
|
55
|
+
h1 { font-size: 16px; margin: 0 0 2px; display: flex; align-items: center; gap: 8px; }
|
|
56
|
+
h1 .hat { font-size: 18px; }
|
|
57
|
+
.sub { color: var(--dim); margin: 0 0 14px; }
|
|
58
|
+
h2 { font-size: 13px; margin: 0 0 8px; }
|
|
59
|
+
section { border: 1px solid var(--line); border-radius: 10px; padding: 12px; margin-bottom: 14px; background: var(--soft); }
|
|
60
|
+
table { width: 100%; border-collapse: collapse; }
|
|
61
|
+
th { text-align: left; font-weight: 600; font-size: 11px; color: var(--dim); padding: 4px 6px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
62
|
+
td { padding: 4px 6px; border-bottom: 1px dashed var(--line); vertical-align: middle; }
|
|
63
|
+
tr:last-child td { border-bottom: none; }
|
|
64
|
+
input[type="text"], input[type="number"], select {
|
|
65
|
+
width: 100%;
|
|
66
|
+
font: inherit;
|
|
67
|
+
color: var(--fg);
|
|
68
|
+
background: var(--bg);
|
|
69
|
+
border: 1px solid var(--line);
|
|
70
|
+
border-radius: 6px;
|
|
71
|
+
padding: 4px 6px;
|
|
72
|
+
}
|
|
73
|
+
input:focus, select:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
|
74
|
+
select:disabled { opacity: .55; }
|
|
75
|
+
button {
|
|
76
|
+
font: inherit;
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
border-radius: 6px;
|
|
79
|
+
border: 1px solid var(--line);
|
|
80
|
+
background: var(--bg);
|
|
81
|
+
color: var(--fg);
|
|
82
|
+
padding: 4px 10px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
button:hover { border-color: var(--accent); }
|
|
86
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #1a1206; font-weight: 600; }
|
|
87
|
+
button.ghost { border-style: dashed; color: var(--dim); }
|
|
88
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
89
|
+
.toolbar { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
|
|
90
|
+
.field { display: grid; grid-template-columns: 1fr 110px; gap: 8px; align-items: center; margin-bottom: 6px; }
|
|
91
|
+
.field label { color: var(--dim); }
|
|
92
|
+
.field input { text-align: right; }
|
|
93
|
+
.hint { color: var(--dim); font-size: 11px; margin-top: 6px; }
|
|
94
|
+
.status { min-height: 18px; font-size: 12px; margin-top: 8px; }
|
|
95
|
+
.status.ok { color: var(--ok); }
|
|
96
|
+
.status.bad { color: var(--bad); }
|
|
97
|
+
.run { border-bottom: 1px dashed var(--line); padding: 6px 0; font-size: 12px; }
|
|
98
|
+
.run:last-child { border-bottom: none; }
|
|
99
|
+
.run .id { font-family: ui-monospace, Consolas, monospace; color: var(--accent); }
|
|
100
|
+
.run .meta { color: var(--dim); }
|
|
101
|
+
.order { margin-left: 10px; color: var(--dim); }
|
|
102
|
+
.order-sub { margin-left: 24px; color: var(--dim); font-size: 11px; }
|
|
103
|
+
.order-sub.warn { color: var(--bad); }
|
|
104
|
+
.empty { color: var(--dim); font-size: 12px; }
|
|
105
|
+
details { margin-top: 4px; }
|
|
106
|
+
summary { cursor: pointer; color: var(--dim); font-size: 12px; }
|
|
107
|
+
td.actions { display: flex; gap: 6px; align-items: center; }
|
|
108
|
+
td.actions button { padding: 4px 6px; }
|
|
109
|
+
td.actions button.icon { line-height: 1; }
|
|
110
|
+
.modal[hidden] { display: none; }
|
|
111
|
+
.modal {
|
|
112
|
+
position: fixed; inset: 0; z-index: 40;
|
|
113
|
+
display: flex; align-items: center; justify-content: center;
|
|
114
|
+
background: rgba(0, 0, 0, 0.45);
|
|
115
|
+
}
|
|
116
|
+
.modal-card {
|
|
117
|
+
width: min(640px, 92vw);
|
|
118
|
+
background: var(--bg); color: var(--fg);
|
|
119
|
+
border: 1px solid var(--line); border-radius: 10px;
|
|
120
|
+
padding: 14px 16px; box-shadow: 0 18px 48px rgba(0, 0, 0, 0.4);
|
|
121
|
+
}
|
|
122
|
+
.modal-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
|
|
123
|
+
.modal textarea {
|
|
124
|
+
width: 100%; box-sizing: border-box; resize: vertical;
|
|
125
|
+
font: inherit; line-height: 1.6; padding: 8px 10px;
|
|
126
|
+
background: var(--input-bg, transparent); color: var(--fg);
|
|
127
|
+
border: 1px solid var(--line); border-radius: 6px;
|
|
128
|
+
}
|
|
129
|
+
details[open] summary { margin-bottom: 8px; }
|
|
130
|
+
.live-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 12px; }
|
|
131
|
+
.live-head .who { font-weight: 600; }
|
|
132
|
+
.live-head .meta { color: var(--dim); }
|
|
133
|
+
.pill {
|
|
134
|
+
display: inline-block; border: 1px solid var(--line); border-radius: 999px;
|
|
135
|
+
padding: 0 8px; font-size: 11px; line-height: 18px; background: var(--bg); color: var(--dim);
|
|
136
|
+
}
|
|
137
|
+
.pill.live { border-color: var(--accent); color: var(--accent); }
|
|
138
|
+
.pill.wait { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
|
|
139
|
+
.pill.done { border-color: var(--ok); color: var(--ok); }
|
|
140
|
+
.pill.off { color: var(--dim); }
|
|
141
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 8px; margin-top: 10px; }
|
|
142
|
+
.card { border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; background: var(--bg); font-size: 12px; }
|
|
143
|
+
.card .row { display: flex; gap: 6px; justify-content: space-between; align-items: baseline; }
|
|
144
|
+
.card .row + .row { margin-top: 4px; }
|
|
145
|
+
.card .who { font-weight: 600; }
|
|
146
|
+
.card .meta { color: var(--dim); }
|
|
147
|
+
.mono { font-family: ui-monospace, Consolas, monospace; }
|
|
148
|
+
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--line); vertical-align: middle; }
|
|
149
|
+
.dot.on { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
150
|
+
.warn { color: var(--bad); font-weight: 600; font-size: 11px; }
|
|
151
|
+
.live-order { font-size: 12px; padding: 5px 0; border-bottom: 1px dashed var(--line); }
|
|
152
|
+
.live-order:last-child { border-bottom: none; }
|
|
153
|
+
.live-order .mark { font-family: ui-monospace, Consolas, monospace; color: var(--accent); }
|
|
154
|
+
.live-order .id { font-family: ui-monospace, Consolas, monospace; }
|
|
155
|
+
.live-order .meta { color: var(--dim); }
|
|
156
|
+
.live-order .sub { color: var(--dim); margin-left: 14px; font-size: 11px; }
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 渲染设置页。
|
|
161
|
+
* @returns 完整 HTML 文档。
|
|
162
|
+
*/
|
|
163
|
+
export function renderPage() {
|
|
164
|
+
const body = String.raw`
|
|
165
|
+
<h1><span class="hat">👷</span>包工头</h1>
|
|
166
|
+
<p class="sub">这里只填你雇来干活的人;拆单出方案的包工头就是父智能体自己,不占名册。想按专长分工就多雇几个实例,各起各的名字、各用各的模型(例如「力工·前端」「力工·渲染」)。每个承包商的提示词点名字后面的眼睛随时改。</p>
|
|
167
|
+
|
|
168
|
+
<section>
|
|
169
|
+
<h2>承包商名册</h2>
|
|
170
|
+
<table>
|
|
171
|
+
<thead>
|
|
172
|
+
<tr><th style="width:20%">名字</th><th style="width:20%">provider</th><th style="width:22%">模型</th><th style="width:13%">思考等级</th><th style="width:7%">并发</th><th style="width:5%">启用</th><th style="width:13%"></th></tr>
|
|
173
|
+
</thead>
|
|
174
|
+
<tbody id="roster"></tbody>
|
|
175
|
+
</table>
|
|
176
|
+
<div class="toolbar">
|
|
177
|
+
<button id="add">+ 添加承包商</button>
|
|
178
|
+
<button class="primary" id="save">保存名册</button>
|
|
179
|
+
<button class="ghost" id="refresh">刷新模型目录</button>
|
|
180
|
+
<span id="roster-status" class="status"></span>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="hint">名字是派单时填的 <code>contractor</code>(精确点名到这个人);想按专长分工就多雇几个实例、各起各的名字,例如「力工-前端」「力工-后端」。并发是同一个承包商同时能接几张单(默认 1,串行);启用的才参与派单。先选 provider,模型下拉只列这个 provider 自己的模型;provider 选「跟随当前会话」就用父会话当前的模型。眼睛按钮打开该承包商的提示词,写清它擅长什么、该怎么干,派单时会塞进它的工单里。聊天里父模型用 <code>foreman_dispatch</code> 派单、<code>foreman_review</code> 验收。</div>
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
<section>
|
|
186
|
+
<h2>实时工地 <span class="hint" id="live-tick"></span></h2>
|
|
187
|
+
<div class="live-head" id="live-lead"></div>
|
|
188
|
+
<div class="grid" id="live-members"></div>
|
|
189
|
+
<div style="margin-top:12px">
|
|
190
|
+
<div class="hint" style="margin:0 0 4px">本轮工单</div>
|
|
191
|
+
<div id="live-orders"></div>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="hint">每 2.5 秒问一次 <code>/api/live</code>;页面切到后台时暂停,切回来立刻刷新。「声明」是你在这个页面里给这个承包商配的 provider/模型,「真实」是本轮它实际跑出来的路由——两者不一致就说明选模型没落到子会话上。</div>
|
|
194
|
+
</section>
|
|
195
|
+
|
|
196
|
+
<section>
|
|
197
|
+
<h2>最近派单</h2>
|
|
198
|
+
<div class="toolbar" style="margin-top:6px">
|
|
199
|
+
<label for="run-picker" class="hint" style="margin:0">查看轮次</label>
|
|
200
|
+
<select id="run-picker" style="width:auto;min-width:280px;max-width:100%"></select>
|
|
201
|
+
<span id="run-picker-hint" class="hint" style="margin:0"></span>
|
|
202
|
+
</div>
|
|
203
|
+
<div id="runs"></div>
|
|
204
|
+
</section>
|
|
205
|
+
|
|
206
|
+
<section>
|
|
207
|
+
<details>
|
|
208
|
+
<summary>派单策略(并发 / 超时 / 返工次数)</summary>
|
|
209
|
+
<div class="field"><label>同时在场承包商</label><input type="number" id="concurrency" min="1" max="8" step="1"></div>
|
|
210
|
+
<div class="field"><label>单张工单超时(秒)</label><input type="number" id="timeoutSeconds" min="10" max="3600" step="10"></div>
|
|
211
|
+
<div class="field"><label>打回后返工次数</label><input type="number" id="maxRework" min="0" max="3" step="1"></div>
|
|
212
|
+
<div class="field"><label>子智能体再派深度</label><input type="number" id="maxDepth" min="0" max="2" step="1"></div>
|
|
213
|
+
<div class="field"><label>subagents provider</label><input type="text" id="provider" placeholder="spawn"></div>
|
|
214
|
+
<div class="field"><label>团队模式</label><label class="check"><input type="checkbox" id="teamMode"> 承包商作为「智能体团队」成员干活(可换模型,走不通自动退回普通模式)</label></div>
|
|
215
|
+
<div class="toolbar">
|
|
216
|
+
<button class="primary" id="save-policy">保存策略</button>
|
|
217
|
+
<span id="policy-status" class="status"></span>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="hint" id="providers-hint"></div>
|
|
220
|
+
</details>
|
|
221
|
+
</section>
|
|
222
|
+
|
|
223
|
+
<div class="modal" id="prompt-modal" hidden>
|
|
224
|
+
<div class="modal-card">
|
|
225
|
+
<div class="modal-head"><strong id="prompt-title">提示词</strong><span class="hint" id="prompt-count"></span></div>
|
|
226
|
+
<textarea id="prompt-text" rows="12" placeholder="写给它一个人的说明:擅长什么、按什么标准干、有哪些禁忌。派单时这段会塞进它的工单提示词里,派单任务本身优先。"></textarea>
|
|
227
|
+
<div class="toolbar">
|
|
228
|
+
<button class="primary" id="prompt-save">保存</button>
|
|
229
|
+
<button class="ghost" id="prompt-cancel">取消</button>
|
|
230
|
+
<span id="prompt-status" class="status"></span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
const script = String.raw`
|
|
237
|
+
(function () {
|
|
238
|
+
var P = '/plugins/dsh-agent-foreman';
|
|
239
|
+
var state = { members: [], policy: {}, runs: [], providers: [], subagentProviders: [] };
|
|
240
|
+
var draft = [];
|
|
241
|
+
var catalog = { providers: [] };
|
|
242
|
+
var seq = 0;
|
|
243
|
+
|
|
244
|
+
function el(id) { return document.getElementById(id); }
|
|
245
|
+
function status(id, text, bad) {
|
|
246
|
+
var node = el(id);
|
|
247
|
+
node.textContent = text;
|
|
248
|
+
node.className = 'status ' + (bad ? 'bad' : 'ok');
|
|
249
|
+
if (text !== '') setTimeout(function () { if (node.textContent === text) { node.textContent = ''; node.className = 'status'; } }, 6000);
|
|
250
|
+
}
|
|
251
|
+
function request(path, options) {
|
|
252
|
+
return fetch(P + path, Object.assign({ headers: { 'content-type': 'application/json' } }, options || {}))
|
|
253
|
+
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
var editing = null;
|
|
257
|
+
function openPrompt(member) {
|
|
258
|
+
editing = member;
|
|
259
|
+
el('prompt-title').textContent = '提示词 · ' + ((member.name || '').trim() || '未命名');
|
|
260
|
+
el('prompt-text').value = member.prompt || '';
|
|
261
|
+
el('prompt-count').textContent = [...(member.prompt || '')].length + ' 字';
|
|
262
|
+
el('prompt-status').textContent = '';
|
|
263
|
+
el('prompt-modal').hidden = false;
|
|
264
|
+
el('prompt-text').focus();
|
|
265
|
+
}
|
|
266
|
+
function closePrompt() { el('prompt-modal').hidden = true; editing = null; }
|
|
267
|
+
el('prompt-text').oninput = function () {
|
|
268
|
+
el('prompt-count').textContent = [...el('prompt-text').value].length + ' 字';
|
|
269
|
+
};
|
|
270
|
+
el('prompt-save').onclick = function () {
|
|
271
|
+
if (!editing) { closePrompt(); return; }
|
|
272
|
+
editing.prompt = el('prompt-text').value;
|
|
273
|
+
var title = (editing.name || '').trim() || '未命名';
|
|
274
|
+
closePrompt();
|
|
275
|
+
renderRoster();
|
|
276
|
+
status('roster-status', '已改「' + title + '」的提示词,点「保存名册」生效');
|
|
277
|
+
};
|
|
278
|
+
el('prompt-cancel').onclick = closePrompt;
|
|
279
|
+
el('prompt-modal').onclick = function (event) {
|
|
280
|
+
if (event.target === el('prompt-modal')) closePrompt();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
function option(value, label, selected) {
|
|
284
|
+
var node = document.createElement('option');
|
|
285
|
+
node.value = value;
|
|
286
|
+
node.textContent = label;
|
|
287
|
+
if (selected) node.selected = true;
|
|
288
|
+
return node;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderRoster() {
|
|
292
|
+
var host = el('roster');
|
|
293
|
+
host.innerHTML = '';
|
|
294
|
+
if (draft.length === 0) {
|
|
295
|
+
var tr = document.createElement('tr');
|
|
296
|
+
var td = document.createElement('td');
|
|
297
|
+
td.colSpan = 7;
|
|
298
|
+
td.className = 'empty';
|
|
299
|
+
td.textContent = '还没有承包商。点「添加承包商」,起个名字就行——模型默认跟当前会话一样。';
|
|
300
|
+
tr.appendChild(td);
|
|
301
|
+
host.appendChild(tr);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
draft.forEach(function (member, index) {
|
|
305
|
+
var tr = document.createElement('tr');
|
|
306
|
+
|
|
307
|
+
var nameTd = document.createElement('td');
|
|
308
|
+
var name = document.createElement('input');
|
|
309
|
+
name.type = 'text';
|
|
310
|
+
name.value = member.name || '';
|
|
311
|
+
name.placeholder = '例如 力工-前端';
|
|
312
|
+
name.oninput = function () { member.name = name.value; };
|
|
313
|
+
nameTd.appendChild(name);
|
|
314
|
+
tr.appendChild(nameTd);
|
|
315
|
+
|
|
316
|
+
var providerTd = document.createElement('td');
|
|
317
|
+
var provider = document.createElement('select');
|
|
318
|
+
var providers = catalog.providers || [];
|
|
319
|
+
provider.appendChild(option('', '跟随当前会话', (member.provider || '') === ''));
|
|
320
|
+
var knownProvider = (member.provider || '') === '';
|
|
321
|
+
providers.forEach(function (entry) {
|
|
322
|
+
// 没有模型的 provider 不进下拉:选了也挑不到模型,只会添乱。
|
|
323
|
+
if ((entry.models || []).length === 0) return;
|
|
324
|
+
if (entry.id === member.provider) knownProvider = true;
|
|
325
|
+
provider.appendChild(option(entry.id, entry.name, member.provider === entry.id));
|
|
326
|
+
});
|
|
327
|
+
if (!knownProvider) provider.appendChild(option(member.provider, member.provider + '(目录里已找不到)', true));
|
|
328
|
+
providerTd.appendChild(provider);
|
|
329
|
+
tr.appendChild(providerTd);
|
|
330
|
+
|
|
331
|
+
var modelTd = document.createElement('td');
|
|
332
|
+
var model = document.createElement('select');
|
|
333
|
+
modelTd.appendChild(model);
|
|
334
|
+
tr.appendChild(modelTd);
|
|
335
|
+
|
|
336
|
+
var effortTd = document.createElement('td');
|
|
337
|
+
var effort = document.createElement('select');
|
|
338
|
+
effortTd.appendChild(effort);
|
|
339
|
+
tr.appendChild(effortTd);
|
|
340
|
+
|
|
341
|
+
var parallelTd = document.createElement('td');
|
|
342
|
+
var parallel = document.createElement('input');
|
|
343
|
+
parallel.type = 'number';
|
|
344
|
+
parallel.min = '1';
|
|
345
|
+
parallel.max = '8';
|
|
346
|
+
parallel.step = '1';
|
|
347
|
+
parallel.value = String(member.maxParallel || 1);
|
|
348
|
+
parallel.title = '同一个承包商同时能接几张单';
|
|
349
|
+
parallel.oninput = function () { member.maxParallel = Number(parallel.value) || 1; };
|
|
350
|
+
parallelTd.appendChild(parallel);
|
|
351
|
+
tr.appendChild(parallelTd);
|
|
352
|
+
|
|
353
|
+
var enabledTd = document.createElement('td');
|
|
354
|
+
var enabled = document.createElement('input');
|
|
355
|
+
enabled.type = 'checkbox';
|
|
356
|
+
enabled.checked = member.enabled !== false;
|
|
357
|
+
enabled.onchange = function () { member.enabled = enabled.checked; };
|
|
358
|
+
enabledTd.appendChild(enabled);
|
|
359
|
+
tr.appendChild(enabledTd);
|
|
360
|
+
|
|
361
|
+
var actionsTd = document.createElement('td');
|
|
362
|
+
actionsTd.className = 'actions';
|
|
363
|
+
var eye = document.createElement('button');
|
|
364
|
+
eye.className = 'ghost icon';
|
|
365
|
+
eye.textContent = '👁';
|
|
366
|
+
eye.title = '编辑这个承包商的提示词';
|
|
367
|
+
eye.onclick = function () { openPrompt(member); };
|
|
368
|
+
actionsTd.appendChild(eye);
|
|
369
|
+
var remove = document.createElement('button');
|
|
370
|
+
remove.textContent = '移除';
|
|
371
|
+
remove.title = '从名册移除(保存后生效)';
|
|
372
|
+
remove.style.padding = '4px 6px';
|
|
373
|
+
remove.onclick = function () { draft.splice(index, 1); renderRoster(); };
|
|
374
|
+
actionsTd.appendChild(remove);
|
|
375
|
+
tr.appendChild(actionsTd);
|
|
376
|
+
|
|
377
|
+
/** 模型由 provider 推出:只列这个 provider 自己的模型。 */
|
|
378
|
+
function fillModels() {
|
|
379
|
+
model.innerHTML = '';
|
|
380
|
+
if ((member.provider || '') === '') {
|
|
381
|
+
model.appendChild(option('', '跟父会话一致', true));
|
|
382
|
+
model.disabled = true;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
model.disabled = false;
|
|
386
|
+
var list = [];
|
|
387
|
+
providers.forEach(function (entry) { if (entry.id === member.provider) list = entry.models || []; });
|
|
388
|
+
model.appendChild(option('', '默认', (member.model || '') === ''));
|
|
389
|
+
var found = (member.model || '') === '';
|
|
390
|
+
list.forEach(function (entry) {
|
|
391
|
+
if (entry.id === member.model) found = true;
|
|
392
|
+
model.appendChild(option(entry.id, entry.name, member.model === entry.id));
|
|
393
|
+
});
|
|
394
|
+
// 之前配过但目录里已经没有的模型仍然显示,避免看起来被悄悄清空。
|
|
395
|
+
if (!found) model.appendChild(option(member.model, member.model + '(已不在目录里)', true));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function fillEfforts(efforts, defaultEffort) {
|
|
399
|
+
effort.innerHTML = '';
|
|
400
|
+
effort.appendChild(option('', defaultEffort ? '默认(' + defaultEffort + ')' : '默认', (member.reasoningEffort || '') === ''));
|
|
401
|
+
(efforts || []).forEach(function (entry) { effort.appendChild(option(entry.id, entry.name, member.reasoningEffort === entry.id)); });
|
|
402
|
+
effort.disabled = (member.provider || '') === '';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function loadEfforts() {
|
|
406
|
+
if (!member.provider || !member.model) { fillEfforts([], ''); return; }
|
|
407
|
+
request('/api/efforts?provider=' + encodeURIComponent(member.provider) + '&model=' + encodeURIComponent(member.model))
|
|
408
|
+
.then(function (result) {
|
|
409
|
+
var body = result.body || {};
|
|
410
|
+
fillEfforts(body.efforts || [], body.defaultEffort);
|
|
411
|
+
})
|
|
412
|
+
.catch(function () { fillEfforts([], ''); });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
provider.onchange = function () {
|
|
416
|
+
member.provider = provider.value;
|
|
417
|
+
// provider 换了,旧模型/旧等级必然失效,必须清掉。
|
|
418
|
+
member.model = '';
|
|
419
|
+
member.reasoningEffort = '';
|
|
420
|
+
fillModels();
|
|
421
|
+
fillEfforts([], '');
|
|
422
|
+
};
|
|
423
|
+
model.onchange = function () {
|
|
424
|
+
member.model = model.value;
|
|
425
|
+
member.reasoningEffort = '';
|
|
426
|
+
loadEfforts();
|
|
427
|
+
};
|
|
428
|
+
effort.onchange = function () { member.reasoningEffort = effort.value; };
|
|
429
|
+
|
|
430
|
+
fillModels();
|
|
431
|
+
fillEfforts([], '');
|
|
432
|
+
loadEfforts();
|
|
433
|
+
host.appendChild(tr);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderPolicy() {
|
|
438
|
+
var policy = state.policy || {};
|
|
439
|
+
el('concurrency').value = policy.concurrency;
|
|
440
|
+
el('timeoutSeconds').value = policy.timeoutSeconds;
|
|
441
|
+
el('maxRework').value = policy.maxRework;
|
|
442
|
+
el('maxDepth').value = policy.maxDepth;
|
|
443
|
+
el('provider').value = policy.provider || '';
|
|
444
|
+
el('teamMode').checked = policy.teamMode === true;
|
|
445
|
+
var hint = el('providers-hint');
|
|
446
|
+
hint.textContent = state.subagentProviders && state.subagentProviders.length
|
|
447
|
+
? '当前宿主注册的 subagents provider:' + state.subagentProviders.join('、')
|
|
448
|
+
: '读不到 subagents provider 列表(未启动或不可用)。';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 「最近派单」:下拉框选一轮,下面只渲染那一轮。
|
|
453
|
+
*
|
|
454
|
+
* state.runs 是倒序的(新在前)。默认选第一项(也就是最近一轮);用户手动选过
|
|
455
|
+
* 之后记住选择,但若那个 runId 已经不在列表里(例如被台账裁剪)就回退到最近一轮,
|
|
456
|
+
* 避免下拉框和下面内容对不上。
|
|
457
|
+
*/
|
|
458
|
+
var selectedRunId = '';
|
|
459
|
+
|
|
460
|
+
function statusText(status) {
|
|
461
|
+
var zh = {
|
|
462
|
+
running: '干活中', rework: '返工中', 'waiting-review': '待验收',
|
|
463
|
+
accepted: '已验收', closed: '已结案',
|
|
464
|
+
};
|
|
465
|
+
return zh[status] || status || '';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderPickers() {
|
|
469
|
+
var runs = state.runs || [];
|
|
470
|
+
var picker = el('run-picker');
|
|
471
|
+
var hint = el('run-picker-hint');
|
|
472
|
+
picker.innerHTML = '';
|
|
473
|
+
if (runs.length === 0) {
|
|
474
|
+
picker.disabled = true;
|
|
475
|
+
picker.appendChild(option('', '(还没有派过单)', true));
|
|
476
|
+
if (hint) hint.textContent = '';
|
|
477
|
+
selectedRunId = '';
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
picker.disabled = false;
|
|
481
|
+
if (!runs.some(function (run) { return run.id === selectedRunId; }))
|
|
482
|
+
selectedRunId = runs[0].id;
|
|
483
|
+
runs.forEach(function (run, index) {
|
|
484
|
+
var when = run.at ? new Date(run.at).toLocaleString() : '';
|
|
485
|
+
var goal = (run.goal || '(无目标)');
|
|
486
|
+
if (goal.length > 24) goal = goal.slice(0, 24) + '…';
|
|
487
|
+
var orderCount = (run.orders || []).length;
|
|
488
|
+
var label = run.id + ' · ' + goal + ' · ' + statusText(run.status)
|
|
489
|
+
+ ' · ' + orderCount + ' 单' + (when ? ' · ' + when : '');
|
|
490
|
+
if (index === 0) label = '【最近】' + label;
|
|
491
|
+
picker.appendChild(option(run.id, label, run.id === selectedRunId));
|
|
492
|
+
});
|
|
493
|
+
if (hint)
|
|
494
|
+
hint.textContent = '共 ' + runs.length + ' 轮';
|
|
495
|
+
picker.onchange = function () {
|
|
496
|
+
selectedRunId = picker.value;
|
|
497
|
+
renderRuns();
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderRuns() {
|
|
502
|
+
var host = el('runs');
|
|
503
|
+
host.innerHTML = '';
|
|
504
|
+
var runs = state.runs || [];
|
|
505
|
+
if (runs.length === 0) {
|
|
506
|
+
host.innerHTML = '<div class="empty">还没有派过单。在聊天里让父智能体派一轮之后,每轮都会记在这里。</div>';
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
var run = null;
|
|
510
|
+
for (var i = 0; i < runs.length; i += 1) {
|
|
511
|
+
if (runs[i].id === selectedRunId) { run = runs[i]; break; }
|
|
512
|
+
}
|
|
513
|
+
if (run === null) run = runs[0];
|
|
514
|
+
|
|
515
|
+
var div = document.createElement('div');
|
|
516
|
+
div.className = 'run';
|
|
517
|
+
var head = document.createElement('div');
|
|
518
|
+
head.innerHTML = '<span class="id"></span> <span class="meta"></span>';
|
|
519
|
+
head.querySelector('.id').textContent = run.id;
|
|
520
|
+
var when = run.at ? new Date(run.at).toLocaleString() : '';
|
|
521
|
+
head.querySelector('.meta').textContent = ' · ' + (run.goal || '(无目标)')
|
|
522
|
+
+ ' · ' + when + ' · ' + statusText(run.status);
|
|
523
|
+
div.appendChild(head);
|
|
524
|
+
|
|
525
|
+
var orders = run.orders || [];
|
|
526
|
+
if (orders.length === 0) {
|
|
527
|
+
var empty = document.createElement('div');
|
|
528
|
+
empty.className = 'empty';
|
|
529
|
+
empty.textContent = '这一轮没有工单记录。';
|
|
530
|
+
div.appendChild(empty);
|
|
531
|
+
}
|
|
532
|
+
orders.forEach(function (order) {
|
|
533
|
+
var line = document.createElement('div');
|
|
534
|
+
line.className = 'order';
|
|
535
|
+
var verdict = order.verdict === 'pass' ? ' ✔已验收' : order.verdict === 'fail' ? ' ✖打回' : '';
|
|
536
|
+
var mark = order.status === 'done' ? '✔'
|
|
537
|
+
: order.status === 'skipped' ? '⊘'
|
|
538
|
+
: (order.status === 'failed' || order.status === 'timeout') ? '✖'
|
|
539
|
+
: order.status === 'running' ? '…' : '·';
|
|
540
|
+
// 真实路由优先显示:声明对不代表子会话真的跑在那档模型上。
|
|
541
|
+
var route = order.actualRoute || order.route || '';
|
|
542
|
+
var label = (order.actualRoute ? '真实 ' : '声明 ') + route;
|
|
543
|
+
line.textContent = mark + ' ' + order.id + ' ' + order.title
|
|
544
|
+
+ ' · ' + (order.contractor || '未分配')
|
|
545
|
+
+ (route ? ' / ' + label : '')
|
|
546
|
+
+ ' · ' + Math.round((order.durationMs || 0) / 100) / 10 + 's'
|
|
547
|
+
+ verdict;
|
|
548
|
+
div.appendChild(line);
|
|
549
|
+
var extra = [];
|
|
550
|
+
if (order.routeMismatch === true) extra.push('⚠ 真实模型与名册不一致');
|
|
551
|
+
if (order.error) extra.push(order.error);
|
|
552
|
+
if (extra.length > 0) {
|
|
553
|
+
var sub = document.createElement('div');
|
|
554
|
+
sub.className = 'order-sub' + (order.routeMismatch === true ? ' warn' : '');
|
|
555
|
+
sub.textContent = extra.join(' · ');
|
|
556
|
+
div.appendChild(sub);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
host.appendChild(div);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function loadCatalog() {
|
|
563
|
+
return request('/api/catalog').then(function (result) {
|
|
564
|
+
if (result.ok && result.body && result.body.providers) catalog = result.body;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function loadState() {
|
|
569
|
+
return request('/api/state').then(function (result) {
|
|
570
|
+
if (!result.ok) throw new Error((result.body && result.body.error) || '读取状态失败');
|
|
571
|
+
state = result.body;
|
|
572
|
+
draft = (state.members || []).map(function (member) { return Object.assign({}, member); });
|
|
573
|
+
renderRoster();
|
|
574
|
+
renderPolicy();
|
|
575
|
+
renderPickers();
|
|
576
|
+
renderRuns();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
el('add').onclick = function () {
|
|
581
|
+
draft.push({ id: 'member-' + (++seq) + '-' + Date.now().toString(36), name: '', prompt: '', provider: '', model: '', reasoningEffort: '', maxParallel: 1, enabled: true });
|
|
582
|
+
renderRoster();
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
el('save').onclick = function () {
|
|
586
|
+
var rows = draft.map(function (member) {
|
|
587
|
+
return {
|
|
588
|
+
id: member.id,
|
|
589
|
+
name: (member.name || '').trim(),
|
|
590
|
+
prompt: member.prompt || '',
|
|
591
|
+
provider: (member.provider || '').trim(),
|
|
592
|
+
model: (member.model || '').trim(),
|
|
593
|
+
reasoningEffort: (member.reasoningEffort || '').trim(),
|
|
594
|
+
maxParallel: Number(member.maxParallel) || 1,
|
|
595
|
+
enabled: member.enabled !== false,
|
|
596
|
+
};
|
|
597
|
+
}).filter(function (member) { return member.name !== ''; });
|
|
598
|
+
request('/api/members', { method: 'POST', body: JSON.stringify({ members: rows }) })
|
|
599
|
+
.then(function (result) {
|
|
600
|
+
if (!result.ok) { status('roster-status', (result.body && result.body.error) || '保存失败', true); return; }
|
|
601
|
+
draft = (result.body.members || []).map(function (member) { return Object.assign({}, member); });
|
|
602
|
+
renderRoster();
|
|
603
|
+
status('roster-status', '已保存 ' + draft.length + ' 个承包商');
|
|
604
|
+
return loadState();
|
|
605
|
+
})
|
|
606
|
+
.catch(function (error) { status('roster-status', String(error.message || error), true); });
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
el('refresh').onclick = function () {
|
|
610
|
+
loadCatalog().then(function () { renderRoster(); status('roster-status', '模型目录已刷新'); });
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
el('save-policy').onclick = function () {
|
|
614
|
+
var patch = {
|
|
615
|
+
concurrency: Number(el('concurrency').value),
|
|
616
|
+
timeoutSeconds: Number(el('timeoutSeconds').value),
|
|
617
|
+
maxRework: Number(el('maxRework').value),
|
|
618
|
+
maxDepth: Number(el('maxDepth').value),
|
|
619
|
+
provider: el('provider').value,
|
|
620
|
+
teamMode: el('teamMode').checked,
|
|
621
|
+
};
|
|
622
|
+
request('/api/policy', { method: 'POST', body: JSON.stringify(patch) })
|
|
623
|
+
.then(function (result) {
|
|
624
|
+
if (!result.ok) { status('policy-status', (result.body && result.body.error) || '保存失败', true); return; }
|
|
625
|
+
state.policy = result.body.policy;
|
|
626
|
+
renderPolicy();
|
|
627
|
+
status('policy-status', '策略已保存');
|
|
628
|
+
})
|
|
629
|
+
.catch(function (error) { status('policy-status', String(error.message || error), true); });
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/** 实时工地:轮询 /api/live,页面隐藏时停表。 */
|
|
633
|
+
var liveTimer = null;
|
|
634
|
+
var liveFailures = 0;
|
|
635
|
+
|
|
636
|
+
var RUN_LABEL = { running: '干活中', rework: '返工中', 'waiting-review': '待验收', accepted: '已验收', closed: '已结案' };
|
|
637
|
+
var RUN_CLASS = { running: 'live', rework: 'live', 'waiting-review': 'wait', accepted: 'done', closed: 'done' };
|
|
638
|
+
var ORDER_MARK = { queued: '·', running: '…', done: '✔', failed: '✖', timeout: '✖', skipped: '⊘' };
|
|
639
|
+
|
|
640
|
+
function text(value) { return value === undefined || value === null ? '' : String(value); }
|
|
641
|
+
|
|
642
|
+
function pill(label, cls) {
|
|
643
|
+
var node = document.createElement('span');
|
|
644
|
+
node.className = 'pill' + (cls ? ' ' + cls : '');
|
|
645
|
+
node.textContent = label;
|
|
646
|
+
return node;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function renderLead(run) {
|
|
650
|
+
var host = el('live-lead');
|
|
651
|
+
host.innerHTML = '';
|
|
652
|
+
if (!run) {
|
|
653
|
+
host.appendChild(pill('待命', 'off'));
|
|
654
|
+
var empty = document.createElement('span');
|
|
655
|
+
empty.className = 'meta';
|
|
656
|
+
empty.textContent = '还没有派过单';
|
|
657
|
+
host.appendChild(empty);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
var who = document.createElement('span');
|
|
661
|
+
who.className = 'who';
|
|
662
|
+
who.textContent = '包工头';
|
|
663
|
+
host.appendChild(who);
|
|
664
|
+
host.appendChild(pill(RUN_LABEL[run.status] || text(run.status) || '未知', RUN_CLASS[run.status] || ''));
|
|
665
|
+
var id = document.createElement('span');
|
|
666
|
+
id.className = 'meta mono';
|
|
667
|
+
id.textContent = '轮次 ' + (text(run.id) || '—');
|
|
668
|
+
host.appendChild(id);
|
|
669
|
+
var goal = document.createElement('span');
|
|
670
|
+
goal.className = 'meta';
|
|
671
|
+
goal.textContent = '目标:' + (text(run.goal) || '(无目标)');
|
|
672
|
+
host.appendChild(goal);
|
|
673
|
+
if (run.matched === false) host.appendChild(pill('不是本会话', 'off'));
|
|
674
|
+
if (run.recovered === true) host.appendChild(pill('已恢复', 'off'));
|
|
675
|
+
if (run.interrupted === true) host.appendChild(pill('中断过', 'wait'));
|
|
676
|
+
if (text(run.updatedAt) !== '') {
|
|
677
|
+
var when = document.createElement('span');
|
|
678
|
+
when.className = 'meta';
|
|
679
|
+
when.textContent = '更新于 ' + new Date(run.updatedAt).toLocaleTimeString();
|
|
680
|
+
host.appendChild(when);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function renderLiveMembers(members) {
|
|
685
|
+
var host = el('live-members');
|
|
686
|
+
host.innerHTML = '';
|
|
687
|
+
var list = members || [];
|
|
688
|
+
if (list.length === 0) {
|
|
689
|
+
host.innerHTML = '<div class="empty">名册还是空的,先去下面「承包商名册」雇人。</div>';
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
list.forEach(function (member) {
|
|
693
|
+
var card = document.createElement('div');
|
|
694
|
+
card.className = 'card';
|
|
695
|
+
|
|
696
|
+
var head = document.createElement('div');
|
|
697
|
+
head.className = 'row';
|
|
698
|
+
var who = document.createElement('span');
|
|
699
|
+
who.className = 'who';
|
|
700
|
+
who.textContent = text(member.name) || '未命名';
|
|
701
|
+
head.appendChild(who);
|
|
702
|
+
var dot = document.createElement('span');
|
|
703
|
+
dot.className = 'dot' + (member.status === 'running' ? ' on' : '');
|
|
704
|
+
dot.title = member.status === 'running' ? '在跑' : '空闲';
|
|
705
|
+
head.appendChild(dot);
|
|
706
|
+
card.appendChild(head);
|
|
707
|
+
|
|
708
|
+
var busyRow = document.createElement('div');
|
|
709
|
+
busyRow.className = 'row';
|
|
710
|
+
var busy = document.createElement('span');
|
|
711
|
+
busy.className = 'meta';
|
|
712
|
+
busy.textContent = '在跑 ' + (Number(member.orders) || 0) + ' 单';
|
|
713
|
+
busyRow.appendChild(busy);
|
|
714
|
+
card.appendChild(busyRow);
|
|
715
|
+
|
|
716
|
+
var declared = document.createElement('div');
|
|
717
|
+
declared.className = 'row';
|
|
718
|
+
var declaredLabel = document.createElement('span');
|
|
719
|
+
declaredLabel.className = 'meta';
|
|
720
|
+
declaredLabel.textContent = '声明 provider/model';
|
|
721
|
+
declared.appendChild(declaredLabel);
|
|
722
|
+
var declaredValue = document.createElement('span');
|
|
723
|
+
declaredValue.className = 'mono';
|
|
724
|
+
declaredValue.textContent = text(member.model) || '跟随当前会话';
|
|
725
|
+
declared.appendChild(declaredValue);
|
|
726
|
+
card.appendChild(declared);
|
|
727
|
+
|
|
728
|
+
var actual = document.createElement('div');
|
|
729
|
+
actual.className = 'row';
|
|
730
|
+
var actualLabel = document.createElement('span');
|
|
731
|
+
actualLabel.className = 'meta';
|
|
732
|
+
actualLabel.textContent = '真实 provider/model';
|
|
733
|
+
actual.appendChild(actualLabel);
|
|
734
|
+
var actualValue = document.createElement('span');
|
|
735
|
+
actualValue.className = 'mono';
|
|
736
|
+
actualValue.textContent = text(member.actualModel) || '还没跑过';
|
|
737
|
+
actual.appendChild(actualValue);
|
|
738
|
+
card.appendChild(actual);
|
|
739
|
+
|
|
740
|
+
// 声明与真实不一致:给一行醒目文字,别让人以为选模型生效了。
|
|
741
|
+
if (text(member.actualModel) !== '' && text(member.model) !== '' && text(member.actualModel) !== text(member.model)) {
|
|
742
|
+
var warn = document.createElement('div');
|
|
743
|
+
warn.className = 'warn';
|
|
744
|
+
warn.textContent = '真实模型与名册不一致';
|
|
745
|
+
card.appendChild(warn);
|
|
746
|
+
}
|
|
747
|
+
if (member.enabled === false) {
|
|
748
|
+
var off = document.createElement('div');
|
|
749
|
+
off.className = 'meta';
|
|
750
|
+
off.textContent = '(已停用)';
|
|
751
|
+
card.appendChild(off);
|
|
752
|
+
}
|
|
753
|
+
host.appendChild(card);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function renderLiveOrders(run) {
|
|
758
|
+
var host = el('live-orders');
|
|
759
|
+
host.innerHTML = '';
|
|
760
|
+
var orders = (run && run.orders) || [];
|
|
761
|
+
if (!run) {
|
|
762
|
+
host.innerHTML = '<div class="empty">还没有派过单。父智能体一开派,工单会实时出现在这里。</div>';
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (orders.length === 0) {
|
|
766
|
+
host.innerHTML = '<div class="empty">本轮没有工单。</div>';
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
orders.forEach(function (order) {
|
|
770
|
+
var line = document.createElement('div');
|
|
771
|
+
line.className = 'live-order';
|
|
772
|
+
|
|
773
|
+
var head = document.createElement('div');
|
|
774
|
+
var mark = document.createElement('span');
|
|
775
|
+
mark.className = 'mark';
|
|
776
|
+
mark.textContent = ORDER_MARK[order.status] || '?';
|
|
777
|
+
head.appendChild(mark);
|
|
778
|
+
head.appendChild(document.createTextNode(' '));
|
|
779
|
+
var id = document.createElement('span');
|
|
780
|
+
id.className = 'id';
|
|
781
|
+
id.textContent = text(order.id);
|
|
782
|
+
head.appendChild(id);
|
|
783
|
+
head.appendChild(document.createTextNode(' '));
|
|
784
|
+
var title = document.createElement('span');
|
|
785
|
+
title.textContent = text(order.title);
|
|
786
|
+
head.appendChild(title);
|
|
787
|
+
var meta = document.createElement('span');
|
|
788
|
+
meta.className = 'meta';
|
|
789
|
+
var seconds = order.durationMs === undefined ? '' : ' · ' + (Math.round((Number(order.durationMs) || 0) / 100) / 10) + 's';
|
|
790
|
+
var depends = (order.dependsOn || []).length ? ' · 依赖 ' + order.dependsOn.join('、') : '';
|
|
791
|
+
var verdict = order.verdict === 'pass' ? ' · 已验收' : order.verdict === 'fail' ? ' · 打回' : '';
|
|
792
|
+
meta.textContent = ' · ' + (text(order.contractor) || '未分配') + seconds + depends + verdict;
|
|
793
|
+
head.appendChild(meta);
|
|
794
|
+
line.appendChild(head);
|
|
795
|
+
|
|
796
|
+
var extra = '';
|
|
797
|
+
if (order.status === 'skipped' || order.status === 'failed' || order.status === 'timeout')
|
|
798
|
+
extra = text(order.error);
|
|
799
|
+
if (text(order.routeNote) !== '')
|
|
800
|
+
extra = extra === '' ? text(order.routeNote) : extra + ' · ' + text(order.routeNote);
|
|
801
|
+
if (order.routeMismatch === true && text(order.actualRoute) !== '')
|
|
802
|
+
extra = extra === '' ? '真实路由 ' + text(order.actualRoute) : extra + ' · 真实路由 ' + text(order.actualRoute);
|
|
803
|
+
if (extra !== '') {
|
|
804
|
+
var err = document.createElement('div');
|
|
805
|
+
err.className = 'sub' + (order.routeMismatch === true ? ' warn' : '');
|
|
806
|
+
err.textContent = (order.status === 'skipped' ? '⊘ ' : '') + extra;
|
|
807
|
+
line.appendChild(err);
|
|
808
|
+
}
|
|
809
|
+
if (text(order.artifactPath) !== '') {
|
|
810
|
+
var artifact = document.createElement('div');
|
|
811
|
+
artifact.className = 'sub mono';
|
|
812
|
+
artifact.textContent = '产物 ' + text(order.artifactPath);
|
|
813
|
+
line.appendChild(artifact);
|
|
814
|
+
}
|
|
815
|
+
host.appendChild(line);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function renderLive(body) {
|
|
820
|
+
var live = body || {};
|
|
821
|
+
renderLead(live.run || null);
|
|
822
|
+
renderLiveMembers(live.members || []);
|
|
823
|
+
renderLiveOrders(live.run || null);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function liveTick() {
|
|
827
|
+
if (document.hidden) return;
|
|
828
|
+
fetch(P + '/api/live')
|
|
829
|
+
.then(function (res) { return res.json(); })
|
|
830
|
+
.then(function (body) {
|
|
831
|
+
if (!body || body.ok !== true) throw new Error('live 读取失败');
|
|
832
|
+
liveFailures = 0;
|
|
833
|
+
el('live-tick').textContent = '';
|
|
834
|
+
el('live-tick').className = 'hint';
|
|
835
|
+
renderLive(body);
|
|
836
|
+
})
|
|
837
|
+
.catch(function () {
|
|
838
|
+
liveFailures += 1;
|
|
839
|
+
el('live-tick').textContent = '连接中断,重试中(第 ' + liveFailures + ' 次)';
|
|
840
|
+
el('live-tick').className = 'hint warn';
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function startLive() {
|
|
845
|
+
if (liveTimer !== null) return;
|
|
846
|
+
liveTick();
|
|
847
|
+
liveTimer = setInterval(liveTick, 2500);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function stopLive() {
|
|
851
|
+
if (liveTimer === null) return;
|
|
852
|
+
clearInterval(liveTimer);
|
|
853
|
+
liveTimer = null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
document.addEventListener('visibilitychange', function () {
|
|
857
|
+
if (document.hidden) stopLive();
|
|
858
|
+
else startLive();
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
loadCatalog().then(loadState).then(startLive).catch(function (error) {
|
|
862
|
+
el('roster').innerHTML = '<tr><td class="empty">加载失败:' + String(error.message || error) + '</td></tr>';
|
|
863
|
+
});
|
|
864
|
+
})();
|
|
865
|
+
`;
|
|
866
|
+
|
|
867
|
+
return `<!doctype html>
|
|
868
|
+
<html lang="zh-CN">
|
|
869
|
+
<head>
|
|
870
|
+
<meta charset="utf-8">
|
|
871
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
872
|
+
<title>包工头</title>
|
|
873
|
+
<style>${STYLE}</style>
|
|
874
|
+
</head>
|
|
875
|
+
<body>
|
|
876
|
+
${body}
|
|
877
|
+
<script>${script}</script>
|
|
878
|
+
</body>
|
|
879
|
+
</html>
|
|
880
|
+
`;
|
|
881
|
+
}
|