@dsh-workbench/core 0.3.5 → 0.3.7
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/client.js +1372 -1928
- package/dist/client/client.body.js +1372 -1928
- package/dist/client/index.js +1372 -1928
- package/dist/host/index.js +1569 -751
- package/dist/types/app/client.d.ts +76 -3355
- package/dist/types/app/layouts/env-gate/boot-gate.d.ts +8 -1
- package/dist/types/app/layouts/rightbar/market-busy.d.ts +28 -0
- package/dist/types/app/layouts/rightbar/market-hint.d.ts +7 -0
- package/dist/types/app/layouts/sidebar/DebugFooterAction.d.ts +2 -1
- package/dist/types/app/layouts/sidebar/RecordFooterAction.d.ts +2 -1
- package/dist/types/app/layouts/sidebar/SessionList.d.ts +3 -1
- package/dist/types/app/layouts/sidebar/session-pick.d.ts +2 -0
- package/dist/types/app/layouts/sidebar/use-attention.d.ts +7 -1
- package/dist/types/modules/panels/client.d.ts +1 -1
- package/dist/types/modules/panels/model/approval.d.ts +8 -0
- package/dist/types/modules/panels/model/diy-context.d.ts +9 -0
- package/dist/types/modules/panels/model/diy-record-beat.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-record-close.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-record-hook.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-record-open.d.ts +28 -0
- package/dist/types/modules/panels/model/diy-record-open.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-record-shot.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-record-text.d.ts +16 -1
- package/dist/types/modules/panels/model/diy-record-tools.d.ts +2 -1
- package/dist/types/modules/panels/model/diy-record.d.ts +40 -1
- package/dist/types/modules/panels/model/diy-report.d.ts +1 -1
- package/dist/types/modules/panels/model/long-task-tools.d.ts +15 -0
- package/dist/types/modules/panels/model/long-task-tools.test.d.ts +1 -0
- package/dist/types/modules/panels/unit.d.ts +5 -3
- package/dist/types/modules/sessions/index.d.ts +1 -1
- package/dist/types/modules/sessions/model/attention-long-task.test.d.ts +1 -0
- package/dist/types/modules/sessions/model/attention.d.ts +23 -1
- package/dist/types/modules/sessions/model/session-node.d.ts +12 -1
- package/dist/types/shared/api/host/approval-call.d.ts +16 -0
- package/dist/types/shared/api/host/long-task-notice.d.ts +30 -0
- package/dist/types/shared/api/host/long-tasks.d.ts +10 -0
- package/dist/types/shared/api/host/mount-units.d.ts +3 -1
- package/dist/types/shared/api/host/preflight.d.ts +7 -1
- package/dist/types/shared/api/host/workshop-root.d.ts +9 -0
- package/dist/types/shared/api/long-tasks.d.ts +19 -0
- package/dist/types/shared/hooks/panel-state-report.d.ts +1 -1
- package/dist/types/shared/hooks/use-diy-debug.d.ts +1 -1
- package/dist/types/shared/hooks/use-diy-record.d.ts +1 -1
- package/dist/types/shared/hooks/use-long-task.d.ts +8 -2
- package/dist/types/shared/hooks/use-shell-long-tasks.d.ts +8 -0
- package/package.json +2 -2
- package/skills/workbench-module-diy/BUILD.md +1 -1
- package/skills/workbench-module-diy/FAMILY.md +1 -1
- package/skills/workbench-module-diy/KIT.md +26 -26
- package/skills/workbench-module-diy/PANEL.md +1 -1
- package/skills/workbench-module-diy/TOOLING.md +5 -1
- package/skills/workbench-module-diy/TOOLS.md +22 -2
- package/skills/workbench-module-diy/WRITES.md +20 -3
package/dist/host/index.js
CHANGED
|
@@ -56,8 +56,12 @@ var VERSION_CHECK_TIMEOUT_MS = 1e4;
|
|
|
56
56
|
var PROBE_TTL_MS = 3e4;
|
|
57
57
|
/** 跑本地 git 命令的超时(毫秒);短于对外的 `OPENCLI_TIMEOUT_MS`。 */
|
|
58
58
|
var GIT_TIMEOUT_MS = 1e4;
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region ../shared/dist/api/context-ref.js
|
|
61
|
+
/**
|
|
62
|
+
* 引用标记的固定名字;完整标记是 @<这个名字>:<编码文本>,改它等于作废历史会话里已写下的引用。
|
|
63
|
+
*/
|
|
64
|
+
var CONTEXT_MARKER_NAME = "工作台";
|
|
61
65
|
/**
|
|
62
66
|
* 把引用拼回标记体里的那一段文本(不含 @ 与标记名)。
|
|
63
67
|
* @param ref 要拼的引用。
|
|
@@ -66,41 +70,6 @@ var TRAILING_PUNCTUATION = /[.,;:!?]+$/;
|
|
|
66
70
|
function encodeRef(ref) {
|
|
67
71
|
return `${ref.kind}:${ref.parts.join("/")}`;
|
|
68
72
|
}
|
|
69
|
-
/**
|
|
70
|
-
* 把 kind:part/part 文本读回引用。
|
|
71
|
-
* @param text 待解析文本。
|
|
72
|
-
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
73
|
-
*/
|
|
74
|
-
function decodeRef(text) {
|
|
75
|
-
const at = text.indexOf(":");
|
|
76
|
-
if (at <= 0 || at === text.length - 1) return void 0;
|
|
77
|
-
const kind = text.slice(0, at);
|
|
78
|
-
if (!/^[a-z][a-z0-9-]*$/.test(kind)) return void 0;
|
|
79
|
-
const parts = text.slice(at + 1).split("/").filter((part) => part !== "");
|
|
80
|
-
if (parts.length === 0) return void 0;
|
|
81
|
-
return {
|
|
82
|
-
kind,
|
|
83
|
-
parts
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
88
|
-
* @param text 任意文本;null / undefined 当空串。
|
|
89
|
-
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
90
|
-
*/
|
|
91
|
-
function parseMarkers(text) {
|
|
92
|
-
const seen = /* @__PURE__ */ new Set();
|
|
93
|
-
const refs = [];
|
|
94
|
-
for (const match of String(text ?? "").matchAll(MARKER)) {
|
|
95
|
-
const ref = decodeRef(match[1].replace(TRAILING_PUNCTUATION, ""));
|
|
96
|
-
if (ref === void 0) continue;
|
|
97
|
-
const key = encodeRef(ref);
|
|
98
|
-
if (seen.has(key)) continue;
|
|
99
|
-
seen.add(key);
|
|
100
|
-
refs.push(ref);
|
|
101
|
-
}
|
|
102
|
-
return refs;
|
|
103
|
-
}
|
|
104
73
|
//#endregion
|
|
105
74
|
//#region ../shared/dist/api/unit.js
|
|
106
75
|
/**
|
|
@@ -111,162 +80,6 @@ function parseMarkers(text) {
|
|
|
111
80
|
function hasEnvSupply(manifest) {
|
|
112
81
|
return (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0;
|
|
113
82
|
}
|
|
114
|
-
/**
|
|
115
|
-
* 外壳愿意认的协议版本;不在表里的模块当场被拒,不猜语义。
|
|
116
|
-
*/
|
|
117
|
-
var SUPPORTED_API_VERSIONS = [1];
|
|
118
|
-
/**
|
|
119
|
-
* 协议认得的全部键;写了别的键不会被采纳(外壳静默忽略),所以字段名拼错不会有提示。
|
|
120
|
-
*/
|
|
121
|
-
var DECLARATION_KEYS = [
|
|
122
|
-
"apiVersion",
|
|
123
|
-
"unit",
|
|
124
|
-
"card",
|
|
125
|
-
"tools",
|
|
126
|
-
"host",
|
|
127
|
-
"env"
|
|
128
|
-
];
|
|
129
|
-
/**
|
|
130
|
-
* 协议要求必写的键;env 不在其中(没有环境需要就不写)。
|
|
131
|
-
*/
|
|
132
|
-
var DECLARATION_REQUIRED_KEYS = [
|
|
133
|
-
"apiVersion",
|
|
134
|
-
"unit",
|
|
135
|
-
"card",
|
|
136
|
-
"tools",
|
|
137
|
-
"host"
|
|
138
|
-
];
|
|
139
|
-
var CARD_KEYS = ["titleKey", "order"];
|
|
140
|
-
var TOOL_KEYS = [
|
|
141
|
-
"name",
|
|
142
|
-
"writes",
|
|
143
|
-
"key"
|
|
144
|
-
];
|
|
145
|
-
var TOOL_KEY_SHAPE = /^[a-z][a-zA-Z0-9]*$/;
|
|
146
|
-
var HOST_KEYS = ["inject", "optional"];
|
|
147
|
-
function text$1(value) {
|
|
148
|
-
return typeof value === "string" ? value.trim() : "";
|
|
149
|
-
}
|
|
150
|
-
function isRecord(value) {
|
|
151
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
152
|
-
}
|
|
153
|
-
function outsideKeys(value, allowed) {
|
|
154
|
-
return Object.keys(value).filter((key) => !allowed.includes(key));
|
|
155
|
-
}
|
|
156
|
-
function listOf(keys) {
|
|
157
|
-
return keys.map((key) => JSON.stringify(key)).join(" / ");
|
|
158
|
-
}
|
|
159
|
-
function stringArrayProblems(at, value) {
|
|
160
|
-
if (!Array.isArray(value)) return [`${at} 要是一串字符串,现在是 ${JSON.stringify(value)}`];
|
|
161
|
-
return value.flatMap((item, index) => text$1(item) === "" ? [`${at}[${index}] 不是非空字符串:${JSON.stringify(item)}`] : []);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* 查清单里 dsh.workbench 那一段的形状,回一串给人看的问题(中文,带字段路径)。
|
|
165
|
-
* @param subject 报错时用的主语,一般是包名。
|
|
166
|
-
* @param raw 清单里那段原始值,可能什么都不是。
|
|
167
|
-
* @returns 空数组表示形状过得去;版本不对就当场返回(后面的字段无从判起)。
|
|
168
|
-
*/
|
|
169
|
-
function declarationProblemsOf(subject, raw) {
|
|
170
|
-
if (!isRecord(raw)) return [`${subject}: dsh.workbench 不是一个对象 —— ${JSON.stringify(raw)}`];
|
|
171
|
-
const version = raw.apiVersion;
|
|
172
|
-
if (version === void 0) return [`${subject}: dsh.workbench 没有 apiVersion —— 协议从 v1 起,写 \`"apiVersion": 1\`。缺它 = 外壳不认这个模块(ADR-0038 §2.3)`];
|
|
173
|
-
if (!Number.isInteger(version) || !SUPPORTED_API_VERSIONS.includes(version)) return [`${subject}: dsh.workbench.apiVersion 是 ${JSON.stringify(version)},这里只认 ${SUPPORTED_API_VERSIONS.join(" / ")}(要整数)`];
|
|
174
|
-
const problems = [];
|
|
175
|
-
const unknown = outsideKeys(raw, DECLARATION_KEYS);
|
|
176
|
-
if (unknown.length > 0) problems.push(`${subject}: dsh.workbench 里有协议 v1 不认识的键 ${listOf(unknown)} —— 只有 ${listOf(DECLARATION_KEYS)}。(拼错的键会被外壳**静默忽略**:那句声明等于没写)`);
|
|
177
|
-
for (const key of DECLARATION_REQUIRED_KEYS) if (raw[key] === void 0) problems.push(`${subject}: dsh.workbench 缺 ${key}` + (key === "card" ? "(这个模块没有卡片就写 `\"card\": null`)" : ""));
|
|
178
|
-
const card = raw.card;
|
|
179
|
-
if (card !== void 0 && card !== null) {
|
|
180
|
-
if (!isRecord(card)) problems.push(`${subject}: dsh.workbench.card 要么是对象、要么是 null,现在是 ${JSON.stringify(card)}`);
|
|
181
|
-
else {
|
|
182
|
-
const extra = outsideKeys(card, CARD_KEYS);
|
|
183
|
-
if (extra.length > 0) problems.push(`${subject}: dsh.workbench.card 里有不认的键 ${listOf(extra)} —— card 只有 ${listOf(CARD_KEYS)}(卡片默认显不显示由运行期的面板配置决定,清单里没有这一位)`);
|
|
184
|
-
if (text$1(card.titleKey) === "") problems.push(`${subject}: dsh.workbench.card.titleKey 要是非空字符串(面板那一格的文案键)`);
|
|
185
|
-
if (typeof card.order !== "number" || !Number.isFinite(card.order)) problems.push(`${subject}: dsh.workbench.card.order 要是数字,现在是 ${JSON.stringify(card.order)}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const tools = raw.tools;
|
|
189
|
-
if (tools !== void 0) {
|
|
190
|
-
if (!Array.isArray(tools)) problems.push(`${subject}: dsh.workbench.tools 要是一串声明,现在是 ${JSON.stringify(tools)}`);
|
|
191
|
-
else {
|
|
192
|
-
const seen = /* @__PURE__ */ new Set();
|
|
193
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
194
|
-
tools.forEach((tool, index) => {
|
|
195
|
-
const at = `dsh.workbench.tools[${index}]`;
|
|
196
|
-
if (!isRecord(tool)) {
|
|
197
|
-
problems.push(`${subject}: ${at} 要是对象,现在是 ${JSON.stringify(tool)}`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const extra = outsideKeys(tool, TOOL_KEYS);
|
|
201
|
-
if (extra.length > 0) problems.push(`${subject}: ${at} 里有不认的键 ${listOf(extra)} —— tools 只有 ${listOf(TOOL_KEYS)}`);
|
|
202
|
-
const name = text$1(tool.name);
|
|
203
|
-
if (name === "") problems.push(`${subject}: ${at}.name 要是非空字符串`);
|
|
204
|
-
else if (seen.has(name)) problems.push(`${subject}: ${at}.name「${name}」与前面那条撞了 —— 工具名会进 dsh 的工具表,不许重`);
|
|
205
|
-
else seen.add(name);
|
|
206
|
-
if (typeof tool.writes !== "boolean") problems.push(`${subject}: ${at}.writes 要是 true / false(它是"这会写"的唯一声明,缺了审批无从认领)`);
|
|
207
|
-
if (tool.key !== void 0) {
|
|
208
|
-
const key = text$1(tool.key);
|
|
209
|
-
if (!TOOL_KEY_SHAPE.test(key)) problems.push(`${subject}: ${at}.key 要是 lowerCamelCase 的标识符(如 \`sync\` / \`podLogs\`),现在是 ${JSON.stringify(tool.key)} —— 它会变成生成物里的键名,所以不引号、不许带点或横线`);
|
|
210
|
-
else if (seenKeys.has(key)) problems.push(`${subject}: ${at}.key「${key}」与前面那条撞了 —— 生成物里的键只能有一个`);
|
|
211
|
-
else seenKeys.add(key);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const host = raw.host;
|
|
217
|
-
if (host !== void 0) {
|
|
218
|
-
if (!isRecord(host)) problems.push(`${subject}: dsh.workbench.host 要是对象,现在是 ${JSON.stringify(host)}`);
|
|
219
|
-
else {
|
|
220
|
-
const extra = outsideKeys(host, HOST_KEYS);
|
|
221
|
-
if (extra.length > 0) problems.push(`${subject}: dsh.workbench.host 里有不认的键 ${listOf(extra)} —— host 只有 ${listOf(HOST_KEYS)}`);
|
|
222
|
-
if (host.inject === void 0) problems.push(`${subject}: dsh.workbench.host.inject 没写 —— 它声明这个模块要外壳的哪几个服务,外壳靠它判"能不能挂"。一个都不要也要写 \`"inject": []\``);
|
|
223
|
-
else problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.inject`, host.inject));
|
|
224
|
-
if (host.optional !== void 0) problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.optional`, host.optional));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (raw.env !== void 0 && !isRecord(raw.env)) problems.push(`${subject}: dsh.workbench.env 要是对象(command / plugins / libs)—— 语义由门禁的 env 判据查,这里只查形状`);
|
|
228
|
-
return problems;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* 读一份清单:先查形状,过了再把声明规整成 manifest。
|
|
232
|
-
* @param subject 报错时用的主语。
|
|
233
|
-
* @param raw 清单里 dsh.workbench 那段原始值。
|
|
234
|
-
* @returns 有问题时只有 problems;没问题时 declaration 与 manifest 都给,manifest 里的字符串已 trim。
|
|
235
|
-
*/
|
|
236
|
-
function readDeclaration(subject, raw) {
|
|
237
|
-
const problems = declarationProblemsOf(subject, raw);
|
|
238
|
-
if (problems.length > 0) return { problems };
|
|
239
|
-
const declaration = raw;
|
|
240
|
-
const card = declaration.card;
|
|
241
|
-
return {
|
|
242
|
-
declaration,
|
|
243
|
-
problems: [],
|
|
244
|
-
manifest: {
|
|
245
|
-
id: text$1(declaration.unit),
|
|
246
|
-
card: card === null ? null : {
|
|
247
|
-
titleKey: text$1(card.titleKey),
|
|
248
|
-
order: card.order
|
|
249
|
-
},
|
|
250
|
-
tools: declaration.tools.map((tool) => ({
|
|
251
|
-
name: text$1(tool.name),
|
|
252
|
-
writes: tool.writes
|
|
253
|
-
})),
|
|
254
|
-
host: {
|
|
255
|
-
inject: [...declaration.host.inject ?? []],
|
|
256
|
-
...declaration.host.optional === void 0 ? {} : { optional: [...declaration.host.optional] }
|
|
257
|
-
},
|
|
258
|
-
...declaration.env === void 0 ? {} : { env: declaration.env }
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* 由单元 id 拼出它那一族事实的范围名。
|
|
264
|
-
* @param unit 单元 id,不带后缀。
|
|
265
|
-
* @returns <unit>* —— 永远是范围名,不是某一条事实。
|
|
266
|
-
*/
|
|
267
|
-
function unitFactScope(unit) {
|
|
268
|
-
return `${unit}*`;
|
|
269
|
-
}
|
|
270
83
|
//#endregion
|
|
271
84
|
//#region ../shared/dist/api/long-task.js
|
|
272
85
|
/**
|
|
@@ -277,25 +90,16 @@ function unitFactScope(unit) {
|
|
|
277
90
|
function runsRouteOf(unit) {
|
|
278
91
|
return `${unit}/runs`;
|
|
279
92
|
}
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region ../shared/dist/lib/text.js
|
|
282
|
-
/**
|
|
283
|
-
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
284
|
-
* @param value - 任意值;不是字符串的先 `String()`。
|
|
285
|
-
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
286
|
-
*/
|
|
287
|
-
function quoteShellArg(value) {
|
|
288
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
289
|
-
}
|
|
290
93
|
/**
|
|
291
|
-
*
|
|
292
|
-
* @param
|
|
293
|
-
* @
|
|
294
|
-
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
94
|
+
* 停止那条写路由(走 actions 通道的资源名):`<单元>/runs/stop`。
|
|
95
|
+
* @param unit 单元 id。
|
|
96
|
+
* @returns <unit>/runs/stop。
|
|
295
97
|
*/
|
|
296
|
-
function
|
|
297
|
-
return
|
|
98
|
+
function stopRouteOf(unit) {
|
|
99
|
+
return `${unit}/runs/stop`;
|
|
298
100
|
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region ../shared/dist/lib/text.js
|
|
299
103
|
function candidateStarts(raw) {
|
|
300
104
|
const starts = [];
|
|
301
105
|
for (let index = 0; index < raw.length; index += 1) {
|
|
@@ -337,262 +141,227 @@ function parseJsonLoose(text) {
|
|
|
337
141
|
var DAY = 864e5;
|
|
338
142
|
30 * DAY;
|
|
339
143
|
365 * DAY;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region ../shared/dist/lib/board-fact.js
|
|
340
146
|
/**
|
|
341
|
-
*
|
|
147
|
+
* 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
|
|
148
|
+
* @param root - 工作区根路径。
|
|
149
|
+
* @param target - 要判的路径。
|
|
150
|
+
* @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
342
151
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
"ready",
|
|
348
|
-
"error"
|
|
349
|
-
];
|
|
350
|
-
function asRecord$8(value) {
|
|
351
|
-
return typeof value === "object" && value !== null ? value : void 0;
|
|
352
|
-
}
|
|
353
|
-
function textOf$6(value, max) {
|
|
354
|
-
if (typeof value !== "string") return "";
|
|
355
|
-
const trimmed = value.trim();
|
|
356
|
-
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
357
|
-
}
|
|
358
|
-
function atOf(value) {
|
|
359
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
360
|
-
if (value < 0) return null;
|
|
361
|
-
return Math.trunc(value);
|
|
362
|
-
}
|
|
363
|
-
function errorOf(raw) {
|
|
364
|
-
const record = asRecord$8(raw);
|
|
365
|
-
if (record === void 0) return void 0;
|
|
366
|
-
const code = textOf$6(record["code"], 120);
|
|
367
|
-
const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
|
|
368
|
-
if (code === "" && message === "") return void 0;
|
|
369
|
-
return {
|
|
370
|
-
code: code === "" ? "unknown" : code,
|
|
371
|
-
message
|
|
372
|
-
};
|
|
152
|
+
function insideWorkshop$1(root, target) {
|
|
153
|
+
if (target === root) return true;
|
|
154
|
+
const base = root.endsWith("/") ? root : `${root}/`;
|
|
155
|
+
return target.startsWith(base);
|
|
373
156
|
}
|
|
374
157
|
/**
|
|
375
|
-
*
|
|
376
|
-
* @param
|
|
377
|
-
* @
|
|
378
|
-
*
|
|
158
|
+
* 把探针读数归成左栏那句人话的档位。
|
|
159
|
+
* @param probe - 探针读数。
|
|
160
|
+
* @param workshop - 当前工作区根路径;`null` 一律 `unknown`。
|
|
161
|
+
* @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
|
|
379
162
|
*/
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
};
|
|
386
|
-
const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
387
|
-
if (workspace === "") return {
|
|
388
|
-
ok: false,
|
|
389
|
-
message: "workspace 不能是空的 —— 面板按工作区存"
|
|
390
|
-
};
|
|
391
|
-
const raw = record["cards"];
|
|
392
|
-
if (!Array.isArray(raw)) return {
|
|
393
|
-
ok: false,
|
|
394
|
-
message: "cards 要是一个数组"
|
|
395
|
-
};
|
|
396
|
-
const cards = [];
|
|
397
|
-
for (const item of raw.slice(0, 32)) {
|
|
398
|
-
const card = asRecord$8(item);
|
|
399
|
-
if (card === void 0) continue;
|
|
400
|
-
const unit = textOf$6(card["unit"], 200);
|
|
401
|
-
const route = textOf$6(card["route"], 400);
|
|
402
|
-
const phase = textOf$6(card["phase"], 20);
|
|
403
|
-
const at = atOf(card["at"]);
|
|
404
|
-
if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
|
|
405
|
-
const error = errorOf(card["error"]);
|
|
406
|
-
cards.push({
|
|
407
|
-
unit,
|
|
408
|
-
route,
|
|
409
|
-
phase,
|
|
410
|
-
at,
|
|
411
|
-
...error === void 0 ? {} : { error }
|
|
412
|
-
});
|
|
163
|
+
function boardSituation(probe, workshop) {
|
|
164
|
+
if (!probe.workbench || workshop === null) return "unknown";
|
|
165
|
+
if (probe.bar && probe.open && probe.drawn) {
|
|
166
|
+
if (probe.board === "") return "unknown";
|
|
167
|
+
return insideWorkshop$1(workshop, probe.board) ? "ok" : "other-board";
|
|
413
168
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
message: "cards 里没有一条能认的(unit / route / phase / at 都要有)"
|
|
417
|
-
};
|
|
418
|
-
return {
|
|
419
|
-
ok: true,
|
|
420
|
-
value: {
|
|
421
|
-
workspace,
|
|
422
|
-
cards
|
|
423
|
-
}
|
|
424
|
-
};
|
|
169
|
+
if (probe.where !== "" && !insideWorkshop$1(workshop, probe.where)) return "off-workspace";
|
|
170
|
+
return "panel-closed";
|
|
425
171
|
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region ../shared/dist/host/board-fact.js
|
|
426
174
|
/**
|
|
427
|
-
*
|
|
428
|
-
* @param
|
|
429
|
-
* @
|
|
175
|
+
* 把一次探针读数归成椅面归属。
|
|
176
|
+
* @param probe - 探针读数。
|
|
177
|
+
* @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
|
|
178
|
+
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
430
179
|
*/
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
};
|
|
437
|
-
const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
438
|
-
const unit = textOf$6(record["unit"], 200);
|
|
439
|
-
const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
|
|
440
|
-
if (workspace === "") return {
|
|
441
|
-
ok: false,
|
|
442
|
-
message: "workspace 不能是空的"
|
|
443
|
-
};
|
|
444
|
-
if (unit === "") return {
|
|
445
|
-
ok: false,
|
|
446
|
-
message: "unit 不能是空的"
|
|
447
|
-
};
|
|
448
|
-
if (reason === "") return {
|
|
449
|
-
ok: false,
|
|
450
|
-
message: "reason 不能是空的(没有原因的崩溃没法查)"
|
|
451
|
-
};
|
|
452
|
-
return {
|
|
453
|
-
ok: true,
|
|
454
|
-
value: {
|
|
455
|
-
workspace,
|
|
456
|
-
unit,
|
|
457
|
-
reason,
|
|
458
|
-
at: atOf(record["at"]) ?? Date.now()
|
|
459
|
-
}
|
|
460
|
-
};
|
|
180
|
+
function boardFactOf(probe, workshop) {
|
|
181
|
+
if (!probe.workbench) return "unknown";
|
|
182
|
+
if (!probe.bar || !probe.open || !probe.drawn) return "hidden";
|
|
183
|
+
if (probe.board === "" || workshop === null) return "unknown";
|
|
184
|
+
return insideWorkshop$1(workshop, probe.board) ? "workshop" : "other";
|
|
461
185
|
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region ../shared/dist/host/shell-text.js
|
|
462
188
|
/**
|
|
463
|
-
*
|
|
464
|
-
* @
|
|
189
|
+
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
190
|
+
* @param value - 任意值;不是字符串的先 `String()`。
|
|
191
|
+
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
465
192
|
*/
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
193
|
+
function quoteShellArg(value) {
|
|
194
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
|
|
198
|
+
* @param text - 原文;`null` / `undefined` 当空串。
|
|
199
|
+
* @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
|
|
200
|
+
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
201
|
+
*/
|
|
202
|
+
function tailLines(text, lines = 6) {
|
|
203
|
+
return String(text ?? "").trim().split("\n").slice(-lines).join("\n");
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region ../shared/dist/host/opencli-window.js
|
|
207
|
+
var OPENCLI_WINDOW_ENV = "OPENCLI_WINDOW=background";
|
|
208
|
+
function backgroundPrefix() {
|
|
209
|
+
return [quoteShellArg("env"), quoteShellArg(OPENCLI_WINDOW_ENV)];
|
|
210
|
+
}
|
|
211
|
+
function backgroundLine(command, argv) {
|
|
212
|
+
return [
|
|
213
|
+
...backgroundPrefix(),
|
|
214
|
+
quoteShellArg(command),
|
|
215
|
+
...argv.map(quoteShellArg)
|
|
216
|
+
].join(" ");
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region ../shared/dist/host/shell.js
|
|
220
|
+
var DEFAULT_MAX_BYTES = 8388608;
|
|
221
|
+
async function runShell(shell, command, options = {}) {
|
|
222
|
+
const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
|
|
223
|
+
const spec = shell.resolve({
|
|
224
|
+
command,
|
|
225
|
+
...workdir === void 0 ? {} : { workdir },
|
|
226
|
+
timeoutMs,
|
|
227
|
+
onExpiry: "kill",
|
|
228
|
+
stdoutMaxBytes: DEFAULT_MAX_BYTES,
|
|
229
|
+
...signal === void 0 ? {} : { signal }
|
|
230
|
+
});
|
|
231
|
+
const result = await (await shell.execute(spec)).result();
|
|
496
232
|
return {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
233
|
+
exitCode: result.exitCode,
|
|
234
|
+
timedOut: result.timedOut === true,
|
|
235
|
+
aborted: result.aborted === true,
|
|
236
|
+
stdout: result.stdout.text,
|
|
237
|
+
stderr: result.stderr.text,
|
|
238
|
+
truncated: result.stdout.truncated || result.stderr.truncated
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region ../shared/dist/host/browser-read.js
|
|
243
|
+
var BROWSER_SCREEN_SESSION = "workbench";
|
|
244
|
+
var BROWSER_READ_SESSION = "workbench-read";
|
|
245
|
+
var BROWSER_RECORD_SESSION = "workbench-rec";
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region ../shared/dist/host/cli-error.js
|
|
248
|
+
function unquote(raw) {
|
|
249
|
+
const value = raw.trim();
|
|
250
|
+
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
function joinBlock(previous, line, fold) {
|
|
254
|
+
if (previous === "") return line;
|
|
255
|
+
return fold ? `${previous} ${line}` : `${previous}\n${line}`;
|
|
256
|
+
}
|
|
257
|
+
function parseCliErrorEnvelope(text) {
|
|
258
|
+
if (text === void 0 || text.trim() === "") return void 0;
|
|
259
|
+
const lines = text.split("\n");
|
|
260
|
+
const start = lines.findIndex((line) => /^error:\s*$/.test(line.trim()) || /^error:\s*\S/.test(line.trim()));
|
|
261
|
+
if (start === -1) return void 0;
|
|
262
|
+
const scalars = {};
|
|
263
|
+
let current;
|
|
264
|
+
let block;
|
|
265
|
+
const errorIndent = lines[start]?.search(/\S/) ?? 0;
|
|
266
|
+
for (const line of lines.slice(start + 1)) {
|
|
267
|
+
if (line.trim() === "") continue;
|
|
268
|
+
const indent = line.search(/\S/);
|
|
269
|
+
if (block !== void 0 && indent > block.indent) {
|
|
270
|
+
scalars[block.key] = joinBlock(scalars[block.key] ?? "", line.trim(), block.fold);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
block = void 0;
|
|
274
|
+
if (indent <= errorIndent) break;
|
|
275
|
+
const match = /^\s*(code|message|help|exitCode):\s*(.*)$/.exec(line);
|
|
276
|
+
if (match === null) {
|
|
277
|
+
if (current !== void 0) scalars[current] = `${scalars[current] ?? ""} ${line.trim()}`.trim();
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const [, key, value] = match;
|
|
281
|
+
if (key === void 0) continue;
|
|
282
|
+
const raw = (value ?? "").trim();
|
|
283
|
+
const marker = /^([|>])[0-9]*([+-]?)$/.exec(raw);
|
|
284
|
+
if (marker !== null) {
|
|
285
|
+
scalars[key] = "";
|
|
286
|
+
block = {
|
|
287
|
+
key,
|
|
288
|
+
fold: marker[1] === ">",
|
|
289
|
+
indent
|
|
550
290
|
};
|
|
291
|
+
current = key;
|
|
292
|
+
continue;
|
|
551
293
|
}
|
|
294
|
+
scalars[key] = unquote(raw);
|
|
295
|
+
current = key;
|
|
296
|
+
}
|
|
297
|
+
const code = scalars["code"];
|
|
298
|
+
const message = scalars["message"];
|
|
299
|
+
if (code === void 0 || code === "") return void 0;
|
|
300
|
+
const exitCode = scalars["exitCode"];
|
|
301
|
+
return {
|
|
302
|
+
code,
|
|
303
|
+
message: message ?? code,
|
|
304
|
+
...scalars["help"] === void 0 || scalars["help"] === "" ? {} : { help: scalars["help"] },
|
|
305
|
+
...exitCode === void 0 || Number.isNaN(Number(exitCode)) ? {} : { exitCode: Number(exitCode) }
|
|
552
306
|
};
|
|
553
307
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
|
|
558
|
-
* @param root - 工作区根路径。
|
|
559
|
-
* @param target - 要判的路径。
|
|
560
|
-
* @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
561
|
-
*/
|
|
562
|
-
function insideWorkshop$1(root, target) {
|
|
563
|
-
if (target === root) return true;
|
|
564
|
-
const base = root.endsWith("/") ? root : `${root}/`;
|
|
565
|
-
return target.startsWith(base);
|
|
308
|
+
function helpUrlOf(envelope) {
|
|
309
|
+
return /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? "")?.[0];
|
|
566
310
|
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region ../shared/dist/host/context-ref.js
|
|
313
|
+
var MARKER = new RegExp(`@${CONTEXT_MARKER_NAME}:([a-z][a-z0-9-]*:[A-Za-z0-9._~%#/-]+)`, "g");
|
|
314
|
+
var TRAILING_PUNCTUATION = /[.,;:!?]+$/;
|
|
567
315
|
/**
|
|
568
|
-
*
|
|
569
|
-
* @param
|
|
570
|
-
* @
|
|
571
|
-
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
316
|
+
* 把 kind:part/part 文本读回引用。
|
|
317
|
+
* @param text 待解析文本。
|
|
318
|
+
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
572
319
|
*/
|
|
573
|
-
function
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
320
|
+
function decodeRef(text) {
|
|
321
|
+
const at = text.indexOf(":");
|
|
322
|
+
if (at <= 0 || at === text.length - 1) return void 0;
|
|
323
|
+
const kind = text.slice(0, at);
|
|
324
|
+
if (!/^[a-z][a-z0-9-]*$/.test(kind)) return void 0;
|
|
325
|
+
const parts = text.slice(at + 1).split("/").filter((part) => part !== "");
|
|
326
|
+
if (parts.length === 0) return void 0;
|
|
327
|
+
return {
|
|
328
|
+
kind,
|
|
329
|
+
parts
|
|
330
|
+
};
|
|
578
331
|
}
|
|
579
332
|
/**
|
|
580
|
-
*
|
|
581
|
-
* @param
|
|
582
|
-
* @
|
|
583
|
-
* @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
|
|
333
|
+
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
334
|
+
* @param text 任意文本;null / undefined 当空串。
|
|
335
|
+
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
584
336
|
*/
|
|
585
|
-
function
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
337
|
+
function parseMarkers(text) {
|
|
338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
339
|
+
const refs = [];
|
|
340
|
+
for (const match of String(text ?? "").matchAll(MARKER)) {
|
|
341
|
+
const ref = decodeRef(match[1].replace(TRAILING_PUNCTUATION, ""));
|
|
342
|
+
if (ref === void 0) continue;
|
|
343
|
+
const key = encodeRef(ref);
|
|
344
|
+
if (seen.has(key)) continue;
|
|
345
|
+
seen.add(key);
|
|
346
|
+
refs.push(ref);
|
|
590
347
|
}
|
|
591
|
-
|
|
592
|
-
return "panel-closed";
|
|
348
|
+
return refs;
|
|
593
349
|
}
|
|
594
350
|
//#endregion
|
|
595
|
-
//#region ../shared/dist/
|
|
351
|
+
//#region ../shared/dist/host/data-root.js
|
|
352
|
+
function dshHome() {
|
|
353
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
354
|
+
if (configured !== void 0 && configured !== "") return configured;
|
|
355
|
+
return `${process.env.HOME ?? ""}/.dsh`;
|
|
356
|
+
}
|
|
357
|
+
function workbenchRoot() {
|
|
358
|
+
return `${dshHome()}/workbench`;
|
|
359
|
+
}
|
|
360
|
+
function workspacesRoot() {
|
|
361
|
+
return `${workbenchRoot()}/workspaces`;
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region ../shared/dist/host/diy-debug.js
|
|
596
365
|
/**
|
|
597
366
|
* 把工具入参收成人话动作。
|
|
598
367
|
* @param value - 任意入参值。
|
|
@@ -706,6 +475,19 @@ function createDebugStore() {
|
|
|
706
475
|
function parseRecordIntent(raw) {
|
|
707
476
|
return raw === "peek" || raw === "halt" || raw === "stop" ? raw : null;
|
|
708
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
|
|
480
|
+
* @param state - 当前有效的那份约定。
|
|
481
|
+
* @param pageShots - 页面浮条上那个「已拍」计数。
|
|
482
|
+
* @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
|
|
483
|
+
*/
|
|
484
|
+
function shotDueOf(state, pageShots) {
|
|
485
|
+
if (!Number.isFinite(pageShots)) return null;
|
|
486
|
+
const n = Math.floor(pageShots);
|
|
487
|
+
if (n <= state.shotsSeen) return null;
|
|
488
|
+
if (state.shotTries !== null && state.shotTries.n === n && state.shotTries.tries >= 2) return null;
|
|
489
|
+
return n;
|
|
490
|
+
}
|
|
709
491
|
function alive(state, now) {
|
|
710
492
|
if (state === null) return null;
|
|
711
493
|
return now - state.at <= 9e5 ? state : null;
|
|
@@ -722,6 +504,26 @@ function ownerOf(state, sessionId, now) {
|
|
|
722
504
|
function createRecordStore() {
|
|
723
505
|
let held = null;
|
|
724
506
|
const live = (now) => alive(held, now);
|
|
507
|
+
const withShot = (found, shot, now, error) => {
|
|
508
|
+
const row = error === void 0 ? shot : {
|
|
509
|
+
n: shot.n,
|
|
510
|
+
at: shot.at,
|
|
511
|
+
path: shot.path,
|
|
512
|
+
error
|
|
513
|
+
};
|
|
514
|
+
const kept = found.shots.filter((one) => one.n !== shot.n);
|
|
515
|
+
const tries = found.shotTries !== null && found.shotTries.n === shot.n ? found.shotTries.tries : 0;
|
|
516
|
+
return {
|
|
517
|
+
...found,
|
|
518
|
+
shots: [...kept, row].sort((one, two) => one.n - two.n),
|
|
519
|
+
shotsSeen: error === void 0 ? Math.max(found.shotsSeen, shot.n) : found.shotsSeen,
|
|
520
|
+
shotTries: error === void 0 ? null : {
|
|
521
|
+
n: shot.n,
|
|
522
|
+
tries: tries + 1
|
|
523
|
+
},
|
|
524
|
+
at: now
|
|
525
|
+
};
|
|
526
|
+
};
|
|
725
527
|
return {
|
|
726
528
|
live,
|
|
727
529
|
begin(pact) {
|
|
@@ -744,6 +546,11 @@ function createRecordStore() {
|
|
|
744
546
|
startedAt: pact.now,
|
|
745
547
|
t0: null,
|
|
746
548
|
t1: null,
|
|
549
|
+
shots: [],
|
|
550
|
+
shotsSeen: 0,
|
|
551
|
+
shotTries: null,
|
|
552
|
+
beats: 0,
|
|
553
|
+
beatMs: 0,
|
|
747
554
|
at: pact.now
|
|
748
555
|
};
|
|
749
556
|
held = state;
|
|
@@ -800,139 +607,64 @@ function createRecordStore() {
|
|
|
800
607
|
state
|
|
801
608
|
};
|
|
802
609
|
},
|
|
803
|
-
|
|
610
|
+
shotTaken(sessionId, shot, now) {
|
|
804
611
|
const found = ownerOf(held, sessionId, now);
|
|
805
612
|
if (typeof found === "string") return {
|
|
806
613
|
ok: false,
|
|
807
614
|
code: found
|
|
808
615
|
};
|
|
809
|
-
|
|
616
|
+
const state = withShot(found, shot, now, void 0);
|
|
617
|
+
held = state;
|
|
810
618
|
return {
|
|
811
619
|
ok: true,
|
|
812
|
-
state
|
|
620
|
+
state
|
|
813
621
|
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
var BROWSER_READ_SESSION = "workbench-read";
|
|
857
|
-
var BROWSER_RECORD_SESSION = "workbench-rec";
|
|
858
|
-
//#endregion
|
|
859
|
-
//#region ../shared/dist/host/cli-error.js
|
|
860
|
-
function unquote(raw) {
|
|
861
|
-
const value = raw.trim();
|
|
862
|
-
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
|
863
|
-
return value;
|
|
864
|
-
}
|
|
865
|
-
function joinBlock(previous, line, fold) {
|
|
866
|
-
if (previous === "") return line;
|
|
867
|
-
return fold ? `${previous} ${line}` : `${previous}\n${line}`;
|
|
868
|
-
}
|
|
869
|
-
function parseCliErrorEnvelope(text) {
|
|
870
|
-
if (text === void 0 || text.trim() === "") return void 0;
|
|
871
|
-
const lines = text.split("\n");
|
|
872
|
-
const start = lines.findIndex((line) => /^error:\s*$/.test(line.trim()) || /^error:\s*\S/.test(line.trim()));
|
|
873
|
-
if (start === -1) return void 0;
|
|
874
|
-
const scalars = {};
|
|
875
|
-
let current;
|
|
876
|
-
let block;
|
|
877
|
-
const errorIndent = lines[start]?.search(/\S/) ?? 0;
|
|
878
|
-
for (const line of lines.slice(start + 1)) {
|
|
879
|
-
if (line.trim() === "") continue;
|
|
880
|
-
const indent = line.search(/\S/);
|
|
881
|
-
if (block !== void 0 && indent > block.indent) {
|
|
882
|
-
scalars[block.key] = joinBlock(scalars[block.key] ?? "", line.trim(), block.fold);
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
block = void 0;
|
|
886
|
-
if (indent <= errorIndent) break;
|
|
887
|
-
const match = /^\s*(code|message|help|exitCode):\s*(.*)$/.exec(line);
|
|
888
|
-
if (match === null) {
|
|
889
|
-
if (current !== void 0) scalars[current] = `${scalars[current] ?? ""} ${line.trim()}`.trim();
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
const [, key, value] = match;
|
|
893
|
-
if (key === void 0) continue;
|
|
894
|
-
const raw = (value ?? "").trim();
|
|
895
|
-
const marker = /^([|>])[0-9]*([+-]?)$/.exec(raw);
|
|
896
|
-
if (marker !== null) {
|
|
897
|
-
scalars[key] = "";
|
|
898
|
-
block = {
|
|
899
|
-
key,
|
|
900
|
-
fold: marker[1] === ">",
|
|
901
|
-
indent
|
|
622
|
+
},
|
|
623
|
+
shotFailed(sessionId, shot, now) {
|
|
624
|
+
const found = ownerOf(held, sessionId, now);
|
|
625
|
+
if (typeof found === "string") return {
|
|
626
|
+
ok: false,
|
|
627
|
+
code: found
|
|
628
|
+
};
|
|
629
|
+
const state = withShot(found, shot, now, shot.error === void 0 ? "落图失败" : shot.error);
|
|
630
|
+
held = state;
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
state
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
beat(sessionId, ms, now) {
|
|
637
|
+
const found = ownerOf(held, sessionId, now);
|
|
638
|
+
if (typeof found === "string") return {
|
|
639
|
+
ok: false,
|
|
640
|
+
code: found
|
|
641
|
+
};
|
|
642
|
+
const state = {
|
|
643
|
+
...found,
|
|
644
|
+
beats: found.beats + 1,
|
|
645
|
+
beatMs: Math.max(0, Math.floor(ms)),
|
|
646
|
+
at: now
|
|
647
|
+
};
|
|
648
|
+
held = state;
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
state
|
|
652
|
+
};
|
|
653
|
+
},
|
|
654
|
+
stop(sessionId, now) {
|
|
655
|
+
const found = ownerOf(held, sessionId, now);
|
|
656
|
+
if (typeof found === "string") return {
|
|
657
|
+
ok: false,
|
|
658
|
+
code: found
|
|
659
|
+
};
|
|
660
|
+
held = null;
|
|
661
|
+
return {
|
|
662
|
+
ok: true,
|
|
663
|
+
state: found
|
|
902
664
|
};
|
|
903
|
-
current = key;
|
|
904
|
-
continue;
|
|
905
665
|
}
|
|
906
|
-
scalars[key] = unquote(raw);
|
|
907
|
-
current = key;
|
|
908
|
-
}
|
|
909
|
-
const code = scalars["code"];
|
|
910
|
-
const message = scalars["message"];
|
|
911
|
-
if (code === void 0 || code === "") return void 0;
|
|
912
|
-
const exitCode = scalars["exitCode"];
|
|
913
|
-
return {
|
|
914
|
-
code,
|
|
915
|
-
message: message ?? code,
|
|
916
|
-
...scalars["help"] === void 0 || scalars["help"] === "" ? {} : { help: scalars["help"] },
|
|
917
|
-
...exitCode === void 0 || Number.isNaN(Number(exitCode)) ? {} : { exitCode: Number(exitCode) }
|
|
918
666
|
};
|
|
919
667
|
}
|
|
920
|
-
function helpUrlOf(envelope) {
|
|
921
|
-
return /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? "")?.[0];
|
|
922
|
-
}
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region ../shared/dist/host/data-root.js
|
|
925
|
-
function dshHome() {
|
|
926
|
-
const configured = process.env.DSH_HOME?.trim();
|
|
927
|
-
if (configured !== void 0 && configured !== "") return configured;
|
|
928
|
-
return `${process.env.HOME ?? ""}/.dsh`;
|
|
929
|
-
}
|
|
930
|
-
function workbenchRoot() {
|
|
931
|
-
return `${dshHome()}/workbench`;
|
|
932
|
-
}
|
|
933
|
-
function workspacesRoot() {
|
|
934
|
-
return `${workbenchRoot()}/workspaces`;
|
|
935
|
-
}
|
|
936
668
|
//#endregion
|
|
937
669
|
//#region ../shared/dist/host/facts.js
|
|
938
670
|
function createFactLog(options = {}) {
|
|
@@ -970,18 +702,240 @@ function createFactLog(options = {}) {
|
|
|
970
702
|
}
|
|
971
703
|
};
|
|
972
704
|
}
|
|
973
|
-
function toolUnits(manifests) {
|
|
974
|
-
const map = /* @__PURE__ */ new Map();
|
|
975
|
-
for (const manifest of manifests) for (const tool of manifest.tools ?? []) map.set(tool.name, {
|
|
976
|
-
unit: manifest.id,
|
|
977
|
-
writes: tool.writes
|
|
978
|
-
});
|
|
979
|
-
return map;
|
|
980
|
-
}
|
|
981
|
-
function wroteNow(result) {
|
|
982
|
-
const wrote = (result?.value)?.wrote;
|
|
983
|
-
return typeof wrote === "boolean" ? wrote : void 0;
|
|
984
|
-
}
|
|
705
|
+
function toolUnits(manifests) {
|
|
706
|
+
const map = /* @__PURE__ */ new Map();
|
|
707
|
+
for (const manifest of manifests) for (const tool of manifest.tools ?? []) map.set(tool.name, {
|
|
708
|
+
unit: manifest.id,
|
|
709
|
+
writes: tool.writes
|
|
710
|
+
});
|
|
711
|
+
return map;
|
|
712
|
+
}
|
|
713
|
+
function wroteNow(result) {
|
|
714
|
+
const wrote = (result?.value)?.wrote;
|
|
715
|
+
return typeof wrote === "boolean" ? wrote : void 0;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 由单元 id 拼出它那一族事实的范围名。
|
|
719
|
+
* @param unit 单元 id,不带后缀。
|
|
720
|
+
* @returns <unit>* —— 永远是范围名,不是某一条事实。
|
|
721
|
+
*/
|
|
722
|
+
function unitFactScope(unit) {
|
|
723
|
+
return `${unit}*`;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
|
|
727
|
+
*/
|
|
728
|
+
var PANEL_STATE_MAX_TEXT = 2e3;
|
|
729
|
+
var PHASES = [
|
|
730
|
+
"idle",
|
|
731
|
+
"loading",
|
|
732
|
+
"ready",
|
|
733
|
+
"error"
|
|
734
|
+
];
|
|
735
|
+
function asRecord$8(value) {
|
|
736
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
737
|
+
}
|
|
738
|
+
function textOf$7(value, max) {
|
|
739
|
+
if (typeof value !== "string") return "";
|
|
740
|
+
const trimmed = value.trim();
|
|
741
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
742
|
+
}
|
|
743
|
+
function atOf(value) {
|
|
744
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
745
|
+
if (value < 0) return null;
|
|
746
|
+
return Math.trunc(value);
|
|
747
|
+
}
|
|
748
|
+
function errorOf(raw) {
|
|
749
|
+
const record = asRecord$8(raw);
|
|
750
|
+
if (record === void 0) return void 0;
|
|
751
|
+
const code = textOf$7(record["code"], 120);
|
|
752
|
+
const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
|
|
753
|
+
if (code === "" && message === "") return void 0;
|
|
754
|
+
return {
|
|
755
|
+
code: code === "" ? "unknown" : code,
|
|
756
|
+
message
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* 收一批卡片读数上报。
|
|
761
|
+
* @param payload - `POST` 的请求体。
|
|
762
|
+
* @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
|
|
763
|
+
* 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
|
|
764
|
+
*/
|
|
765
|
+
function parseCardsReport(payload) {
|
|
766
|
+
const record = asRecord$8(payload);
|
|
767
|
+
if (record === void 0) return {
|
|
768
|
+
ok: false,
|
|
769
|
+
message: "请求体要是一个对象"
|
|
770
|
+
};
|
|
771
|
+
const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
772
|
+
if (workspace === "") return {
|
|
773
|
+
ok: false,
|
|
774
|
+
message: "workspace 不能是空的 —— 面板按工作区存"
|
|
775
|
+
};
|
|
776
|
+
const raw = record["cards"];
|
|
777
|
+
if (!Array.isArray(raw)) return {
|
|
778
|
+
ok: false,
|
|
779
|
+
message: "cards 要是一个数组"
|
|
780
|
+
};
|
|
781
|
+
const cards = [];
|
|
782
|
+
for (const item of raw.slice(0, 32)) {
|
|
783
|
+
const card = asRecord$8(item);
|
|
784
|
+
if (card === void 0) continue;
|
|
785
|
+
const unit = textOf$7(card["unit"], 200);
|
|
786
|
+
const route = textOf$7(card["route"], 400);
|
|
787
|
+
const phase = textOf$7(card["phase"], 20);
|
|
788
|
+
const at = atOf(card["at"]);
|
|
789
|
+
if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
|
|
790
|
+
const error = errorOf(card["error"]);
|
|
791
|
+
cards.push({
|
|
792
|
+
unit,
|
|
793
|
+
route,
|
|
794
|
+
phase,
|
|
795
|
+
at,
|
|
796
|
+
...error === void 0 ? {} : { error }
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (cards.length === 0) return {
|
|
800
|
+
ok: false,
|
|
801
|
+
message: "cards 里没有一条能认的(unit / route / phase / at 都要有)"
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
ok: true,
|
|
805
|
+
value: {
|
|
806
|
+
workspace,
|
|
807
|
+
cards
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* 收一次卡片崩溃上报。
|
|
813
|
+
* @param payload - `POST` 的请求体。
|
|
814
|
+
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
815
|
+
*/
|
|
816
|
+
function parseCrashReport(payload) {
|
|
817
|
+
const record = asRecord$8(payload);
|
|
818
|
+
if (record === void 0) return {
|
|
819
|
+
ok: false,
|
|
820
|
+
message: "请求体要是一个对象"
|
|
821
|
+
};
|
|
822
|
+
const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
823
|
+
const unit = textOf$7(record["unit"], 200);
|
|
824
|
+
const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
|
|
825
|
+
if (workspace === "") return {
|
|
826
|
+
ok: false,
|
|
827
|
+
message: "workspace 不能是空的"
|
|
828
|
+
};
|
|
829
|
+
if (unit === "") return {
|
|
830
|
+
ok: false,
|
|
831
|
+
message: "unit 不能是空的"
|
|
832
|
+
};
|
|
833
|
+
if (reason === "") return {
|
|
834
|
+
ok: false,
|
|
835
|
+
message: "reason 不能是空的(没有原因的崩溃没法查)"
|
|
836
|
+
};
|
|
837
|
+
return {
|
|
838
|
+
ok: true,
|
|
839
|
+
value: {
|
|
840
|
+
workspace,
|
|
841
|
+
unit,
|
|
842
|
+
reason,
|
|
843
|
+
at: atOf(record["at"]) ?? Date.now()
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
849
|
+
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
850
|
+
*/
|
|
851
|
+
function createPanelStateStore() {
|
|
852
|
+
const workspaces = /* @__PURE__ */ new Map();
|
|
853
|
+
function trim(entry, max) {
|
|
854
|
+
while (entry.size > max) {
|
|
855
|
+
const oldest = entry.keys().next();
|
|
856
|
+
if (oldest.done === true) return;
|
|
857
|
+
entry.delete(oldest.value);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function workspaceEntryOf(workspace) {
|
|
861
|
+
const existing = workspaces.get(workspace);
|
|
862
|
+
if (existing !== void 0) return existing;
|
|
863
|
+
const created = {
|
|
864
|
+
at: 0,
|
|
865
|
+
units: /* @__PURE__ */ new Map()
|
|
866
|
+
};
|
|
867
|
+
workspaces.set(workspace, created);
|
|
868
|
+
trim(workspaces, 64);
|
|
869
|
+
return created;
|
|
870
|
+
}
|
|
871
|
+
function unitEntryOf(entry, unit) {
|
|
872
|
+
const existing = entry.units.get(unit);
|
|
873
|
+
if (existing !== void 0) return existing;
|
|
874
|
+
const created = {
|
|
875
|
+
at: 0,
|
|
876
|
+
cards: /* @__PURE__ */ new Map()
|
|
877
|
+
};
|
|
878
|
+
entry.units.set(unit, created);
|
|
879
|
+
return created;
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
cards(report) {
|
|
883
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
884
|
+
for (const card of report.cards) {
|
|
885
|
+
const unit = unitEntryOf(entry, card.unit);
|
|
886
|
+
unit.cards.delete(card.route);
|
|
887
|
+
unit.cards.set(card.route, card);
|
|
888
|
+
trim(unit.cards, 12);
|
|
889
|
+
if (card.at > unit.at) unit.at = card.at;
|
|
890
|
+
if (card.at > entry.at) entry.at = card.at;
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
crash(report) {
|
|
894
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
895
|
+
const unit = unitEntryOf(entry, report.unit);
|
|
896
|
+
const previous = unit.crash;
|
|
897
|
+
const counted = previous !== void 0 && previous.reason === report.reason && report.at >= previous.at && report.at - previous.at <= 6e5;
|
|
898
|
+
unit.crash = {
|
|
899
|
+
unit: report.unit,
|
|
900
|
+
reason: report.reason,
|
|
901
|
+
at: report.at,
|
|
902
|
+
count: counted ? previous.count + 1 : 1
|
|
903
|
+
};
|
|
904
|
+
if (report.at > unit.at) unit.at = report.at;
|
|
905
|
+
if (report.at > entry.at) entry.at = report.at;
|
|
906
|
+
},
|
|
907
|
+
snapshot(workspace) {
|
|
908
|
+
if (workspace === null) return {
|
|
909
|
+
workspace: null,
|
|
910
|
+
at: 0,
|
|
911
|
+
units: []
|
|
912
|
+
};
|
|
913
|
+
const entry = workspaces.get(workspace);
|
|
914
|
+
if (entry === void 0) return {
|
|
915
|
+
workspace,
|
|
916
|
+
at: 0,
|
|
917
|
+
units: []
|
|
918
|
+
};
|
|
919
|
+
const units = [...entry.units.entries()].map(([unit, one]) => {
|
|
920
|
+
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
921
|
+
const okAt = cards.reduce((best, card) => card.phase === "ready" && (best === null || card.at > best) ? card.at : best, null);
|
|
922
|
+
return {
|
|
923
|
+
unit,
|
|
924
|
+
at: one.at,
|
|
925
|
+
okAt,
|
|
926
|
+
cards,
|
|
927
|
+
...one.crash === void 0 ? {} : { crash: one.crash }
|
|
928
|
+
};
|
|
929
|
+
});
|
|
930
|
+
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
931
|
+
return {
|
|
932
|
+
workspace,
|
|
933
|
+
at: entry.at,
|
|
934
|
+
units
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
985
939
|
//#endregion
|
|
986
940
|
//#region ../shared/dist/host/provision.js
|
|
987
941
|
var PROVISION_ACTIONS = [
|
|
@@ -1480,7 +1434,7 @@ function changesHandler(log) {
|
|
|
1480
1434
|
}
|
|
1481
1435
|
function preflightHandler(preflight) {
|
|
1482
1436
|
return (search) => {
|
|
1483
|
-
preflight.run({
|
|
1437
|
+
preflight.run({ hard: flag(search, "hard") });
|
|
1484
1438
|
return {
|
|
1485
1439
|
ok: true,
|
|
1486
1440
|
...preflight.snapshot()
|
|
@@ -1596,11 +1550,17 @@ var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
|
|
|
1596
1550
|
var RECENT_MS = 6e5;
|
|
1597
1551
|
function createLongTaskRegistry(deps) {
|
|
1598
1552
|
const runs = /* @__PURE__ */ new Map();
|
|
1553
|
+
const controls = /* @__PURE__ */ new Map();
|
|
1599
1554
|
const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1600
1555
|
let counter = 0;
|
|
1601
1556
|
const nextId = deps.nextId ?? (() => `run-${Date.now()}-${counter += 1}`);
|
|
1602
1557
|
const recentMs = deps.recentMs ?? RECENT_MS;
|
|
1603
1558
|
const listeners = /* @__PURE__ */ new Set();
|
|
1559
|
+
const live = (id) => {
|
|
1560
|
+
const current = runs.get(id);
|
|
1561
|
+
if (current === void 0 || !LIVE.has(current.status)) return null;
|
|
1562
|
+
return current;
|
|
1563
|
+
};
|
|
1604
1564
|
const notify = (change) => {
|
|
1605
1565
|
for (const listener of [...listeners]) try {
|
|
1606
1566
|
listener(change);
|
|
@@ -1635,7 +1595,13 @@ function createLongTaskRegistry(deps) {
|
|
|
1635
1595
|
if (snapshot.workspace !== workspace) continue;
|
|
1636
1596
|
newest.set(snapshot.target, snapshot);
|
|
1637
1597
|
}
|
|
1638
|
-
return [...newest.values()].filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor).reverse().map((snapshot) =>
|
|
1598
|
+
return [...newest.values()].filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor).reverse().map((snapshot) => ({
|
|
1599
|
+
...deps.view(snapshot),
|
|
1600
|
+
id: snapshot.id
|
|
1601
|
+
}));
|
|
1602
|
+
},
|
|
1603
|
+
live() {
|
|
1604
|
+
return [...runs.values()].filter((snapshot) => LIVE.has(snapshot.status));
|
|
1639
1605
|
},
|
|
1640
1606
|
start(options) {
|
|
1641
1607
|
const running = this.active(options.workspace, options.target);
|
|
@@ -1650,15 +1616,16 @@ function createLongTaskRegistry(deps) {
|
|
|
1650
1616
|
step: "",
|
|
1651
1617
|
steps: [],
|
|
1652
1618
|
startedAt,
|
|
1653
|
-
at: startedAt
|
|
1619
|
+
at: startedAt,
|
|
1620
|
+
...options.session === void 0 ? {} : { session: options.session }
|
|
1654
1621
|
});
|
|
1655
1622
|
notify({
|
|
1656
1623
|
kind: "start",
|
|
1657
1624
|
snapshot: runs.get(id)
|
|
1658
1625
|
});
|
|
1659
1626
|
const onStep = (step) => {
|
|
1660
|
-
const current =
|
|
1661
|
-
if (current ===
|
|
1627
|
+
const current = live(id);
|
|
1628
|
+
if (current === null) return;
|
|
1662
1629
|
const steps = [...current.steps, `${step.kind === "fail" ? "✗ " : ""}${step.text}`].slice(-20);
|
|
1663
1630
|
const extra = deps.step?.(step, current) ?? {};
|
|
1664
1631
|
if (step.loginUrl !== void 0) {
|
|
@@ -1685,17 +1652,24 @@ function createLongTaskRegistry(deps) {
|
|
|
1685
1652
|
step
|
|
1686
1653
|
});
|
|
1687
1654
|
};
|
|
1655
|
+
const control = new AbortController();
|
|
1656
|
+
controls.set(id, control);
|
|
1688
1657
|
deps.run({
|
|
1689
1658
|
payload: options.payload,
|
|
1690
|
-
onStep
|
|
1659
|
+
onStep,
|
|
1660
|
+
signal: control.signal
|
|
1691
1661
|
}).then((result) => {
|
|
1692
|
-
const current =
|
|
1662
|
+
const current = live(id);
|
|
1663
|
+
if (current === null) return;
|
|
1664
|
+
controls.delete(id);
|
|
1693
1665
|
patch(id, {
|
|
1694
1666
|
status: "ok",
|
|
1695
1667
|
...deps.outcome?.(result, current) ?? {},
|
|
1696
1668
|
loginUrl: void 0
|
|
1697
1669
|
}, { kind: "settle" });
|
|
1698
1670
|
}).catch((error) => {
|
|
1671
|
+
if (live(id) === null) return;
|
|
1672
|
+
controls.delete(id);
|
|
1699
1673
|
patch(id, {
|
|
1700
1674
|
status: "failed",
|
|
1701
1675
|
why: error instanceof Error ? error.message : String(error),
|
|
@@ -1704,6 +1678,21 @@ function createLongTaskRegistry(deps) {
|
|
|
1704
1678
|
});
|
|
1705
1679
|
return runs.get(id);
|
|
1706
1680
|
},
|
|
1681
|
+
stop(id, by) {
|
|
1682
|
+
const current = live(id);
|
|
1683
|
+
if (current === null) return false;
|
|
1684
|
+
controls.get(id)?.abort();
|
|
1685
|
+
controls.delete(id);
|
|
1686
|
+
patch(id, {
|
|
1687
|
+
...deps.stopped?.(current) ?? {},
|
|
1688
|
+
status: "stopped",
|
|
1689
|
+
loginUrl: void 0
|
|
1690
|
+
}, {
|
|
1691
|
+
kind: "settle",
|
|
1692
|
+
by
|
|
1693
|
+
});
|
|
1694
|
+
return true;
|
|
1695
|
+
},
|
|
1707
1696
|
snapshot(id) {
|
|
1708
1697
|
return runs.get(id) ?? null;
|
|
1709
1698
|
},
|
|
@@ -1736,6 +1725,7 @@ function createLongTaskHost(options) {
|
|
|
1736
1725
|
offs.push(registry.onChange((change) => {
|
|
1737
1726
|
const named = [runsRouteOf(options.unit), ...facts?.(change) ?? []];
|
|
1738
1727
|
options.bump([...new Set(named)]);
|
|
1728
|
+
options.notify?.(change);
|
|
1739
1729
|
}));
|
|
1740
1730
|
return registry;
|
|
1741
1731
|
} },
|
|
@@ -1802,7 +1792,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
|
|
|
1802
1792
|
function ledgerKeyOf(entry) {
|
|
1803
1793
|
return `${entry.opencliHome}\u0000${entry.package}`;
|
|
1804
1794
|
}
|
|
1805
|
-
function textOf$
|
|
1795
|
+
function textOf$6(value) {
|
|
1806
1796
|
if (typeof value !== "string") return void 0;
|
|
1807
1797
|
const trimmed = value.trim();
|
|
1808
1798
|
return trimmed === "" ? void 0 : trimmed;
|
|
@@ -1810,13 +1800,13 @@ function textOf$5(value) {
|
|
|
1810
1800
|
function entryOf$1(value) {
|
|
1811
1801
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1812
1802
|
const box = value;
|
|
1813
|
-
const opencliHome = textOf$
|
|
1814
|
-
const pkg = textOf$
|
|
1815
|
-
const installedAt = textOf$
|
|
1803
|
+
const opencliHome = textOf$6(box["opencliHome"]);
|
|
1804
|
+
const pkg = textOf$6(box["package"]);
|
|
1805
|
+
const installedAt = textOf$6(box["installedAt"]);
|
|
1816
1806
|
if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
|
|
1817
|
-
const name = textOf$
|
|
1818
|
-
const registry = textOf$
|
|
1819
|
-
const releasedAt = textOf$
|
|
1807
|
+
const name = textOf$6(box["name"]);
|
|
1808
|
+
const registry = textOf$6(box["registry"]);
|
|
1809
|
+
const releasedAt = textOf$6(box["releasedAt"]);
|
|
1820
1810
|
return {
|
|
1821
1811
|
opencliHome,
|
|
1822
1812
|
package: pkg,
|
|
@@ -2098,6 +2088,154 @@ var TEXT_OUTPUT = {
|
|
|
2098
2088
|
function jsonText(value) {
|
|
2099
2089
|
return JSON.stringify(value);
|
|
2100
2090
|
}
|
|
2091
|
+
/**
|
|
2092
|
+
* 外壳愿意认的协议版本;不在表里的模块当场被拒,不猜语义。
|
|
2093
|
+
*/
|
|
2094
|
+
var SUPPORTED_API_VERSIONS = [1];
|
|
2095
|
+
/**
|
|
2096
|
+
* 协议认得的全部键;写了别的键不会被采纳(外壳静默忽略),所以字段名拼错不会有提示。
|
|
2097
|
+
*/
|
|
2098
|
+
var DECLARATION_KEYS = [
|
|
2099
|
+
"apiVersion",
|
|
2100
|
+
"unit",
|
|
2101
|
+
"card",
|
|
2102
|
+
"tools",
|
|
2103
|
+
"host",
|
|
2104
|
+
"env"
|
|
2105
|
+
];
|
|
2106
|
+
/**
|
|
2107
|
+
* 协议要求必写的键;env 不在其中(没有环境需要就不写)。
|
|
2108
|
+
*/
|
|
2109
|
+
var DECLARATION_REQUIRED_KEYS = [
|
|
2110
|
+
"apiVersion",
|
|
2111
|
+
"unit",
|
|
2112
|
+
"card",
|
|
2113
|
+
"tools",
|
|
2114
|
+
"host"
|
|
2115
|
+
];
|
|
2116
|
+
var CARD_KEYS = ["titleKey", "order"];
|
|
2117
|
+
var TOOL_KEYS = [
|
|
2118
|
+
"name",
|
|
2119
|
+
"writes",
|
|
2120
|
+
"key"
|
|
2121
|
+
];
|
|
2122
|
+
var TOOL_KEY_SHAPE = /^[a-z][a-zA-Z0-9]*$/;
|
|
2123
|
+
var HOST_KEYS = ["inject", "optional"];
|
|
2124
|
+
function text$1(value) {
|
|
2125
|
+
return typeof value === "string" ? value.trim() : "";
|
|
2126
|
+
}
|
|
2127
|
+
function isRecord(value) {
|
|
2128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2129
|
+
}
|
|
2130
|
+
function outsideKeys(value, allowed) {
|
|
2131
|
+
return Object.keys(value).filter((key) => !allowed.includes(key));
|
|
2132
|
+
}
|
|
2133
|
+
function listOf(keys) {
|
|
2134
|
+
return keys.map((key) => JSON.stringify(key)).join(" / ");
|
|
2135
|
+
}
|
|
2136
|
+
function stringArrayProblems(at, value) {
|
|
2137
|
+
if (!Array.isArray(value)) return [`${at} 要是一串字符串,现在是 ${JSON.stringify(value)}`];
|
|
2138
|
+
return value.flatMap((item, index) => text$1(item) === "" ? [`${at}[${index}] 不是非空字符串:${JSON.stringify(item)}`] : []);
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* 查清单里 dsh.workbench 那一段的形状,回一串给人看的问题(中文,带字段路径)。
|
|
2142
|
+
* @param subject 报错时用的主语,一般是包名。
|
|
2143
|
+
* @param raw 清单里那段原始值,可能什么都不是。
|
|
2144
|
+
* @returns 空数组表示形状过得去;版本不对就当场返回(后面的字段无从判起)。
|
|
2145
|
+
*/
|
|
2146
|
+
function declarationProblemsOf(subject, raw) {
|
|
2147
|
+
if (!isRecord(raw)) return [`${subject}: dsh.workbench 不是一个对象 —— ${JSON.stringify(raw)}`];
|
|
2148
|
+
const version = raw.apiVersion;
|
|
2149
|
+
if (version === void 0) return [`${subject}: dsh.workbench 没有 apiVersion —— 协议从 v1 起,写 \`"apiVersion": 1\`。缺它 = 外壳不认这个模块(ADR-0038 §2.3)`];
|
|
2150
|
+
if (!Number.isInteger(version) || !SUPPORTED_API_VERSIONS.includes(version)) return [`${subject}: dsh.workbench.apiVersion 是 ${JSON.stringify(version)},这里只认 ${SUPPORTED_API_VERSIONS.join(" / ")}(要整数)`];
|
|
2151
|
+
const problems = [];
|
|
2152
|
+
const unknown = outsideKeys(raw, DECLARATION_KEYS);
|
|
2153
|
+
if (unknown.length > 0) problems.push(`${subject}: dsh.workbench 里有协议 v1 不认识的键 ${listOf(unknown)} —— 只有 ${listOf(DECLARATION_KEYS)}。(拼错的键会被外壳**静默忽略**:那句声明等于没写)`);
|
|
2154
|
+
for (const key of DECLARATION_REQUIRED_KEYS) if (raw[key] === void 0) problems.push(`${subject}: dsh.workbench 缺 ${key}` + (key === "card" ? "(这个模块没有卡片就写 `\"card\": null`)" : ""));
|
|
2155
|
+
const card = raw.card;
|
|
2156
|
+
if (card !== void 0 && card !== null) {
|
|
2157
|
+
if (!isRecord(card)) problems.push(`${subject}: dsh.workbench.card 要么是对象、要么是 null,现在是 ${JSON.stringify(card)}`);
|
|
2158
|
+
else {
|
|
2159
|
+
const extra = outsideKeys(card, CARD_KEYS);
|
|
2160
|
+
if (extra.length > 0) problems.push(`${subject}: dsh.workbench.card 里有不认的键 ${listOf(extra)} —— card 只有 ${listOf(CARD_KEYS)}(卡片默认显不显示由运行期的面板配置决定,清单里没有这一位)`);
|
|
2161
|
+
if (text$1(card.titleKey) === "") problems.push(`${subject}: dsh.workbench.card.titleKey 要是非空字符串(面板那一格的文案键)`);
|
|
2162
|
+
if (typeof card.order !== "number" || !Number.isFinite(card.order)) problems.push(`${subject}: dsh.workbench.card.order 要是数字,现在是 ${JSON.stringify(card.order)}`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
const tools = raw.tools;
|
|
2166
|
+
if (tools !== void 0) {
|
|
2167
|
+
if (!Array.isArray(tools)) problems.push(`${subject}: dsh.workbench.tools 要是一串声明,现在是 ${JSON.stringify(tools)}`);
|
|
2168
|
+
else {
|
|
2169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2170
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
2171
|
+
tools.forEach((tool, index) => {
|
|
2172
|
+
const at = `dsh.workbench.tools[${index}]`;
|
|
2173
|
+
if (!isRecord(tool)) {
|
|
2174
|
+
problems.push(`${subject}: ${at} 要是对象,现在是 ${JSON.stringify(tool)}`);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const extra = outsideKeys(tool, TOOL_KEYS);
|
|
2178
|
+
if (extra.length > 0) problems.push(`${subject}: ${at} 里有不认的键 ${listOf(extra)} —— tools 只有 ${listOf(TOOL_KEYS)}`);
|
|
2179
|
+
const name = text$1(tool.name);
|
|
2180
|
+
if (name === "") problems.push(`${subject}: ${at}.name 要是非空字符串`);
|
|
2181
|
+
else if (seen.has(name)) problems.push(`${subject}: ${at}.name「${name}」与前面那条撞了 —— 工具名会进 dsh 的工具表,不许重`);
|
|
2182
|
+
else seen.add(name);
|
|
2183
|
+
if (typeof tool.writes !== "boolean") problems.push(`${subject}: ${at}.writes 要是 true / false(它是"这会写"的唯一声明,缺了审批无从认领)`);
|
|
2184
|
+
if (tool.key !== void 0) {
|
|
2185
|
+
const key = text$1(tool.key);
|
|
2186
|
+
if (!TOOL_KEY_SHAPE.test(key)) problems.push(`${subject}: ${at}.key 要是 lowerCamelCase 的标识符(如 \`sync\` / \`podLogs\`),现在是 ${JSON.stringify(tool.key)} —— 它会变成生成物里的键名,所以不引号、不许带点或横线`);
|
|
2187
|
+
else if (seenKeys.has(key)) problems.push(`${subject}: ${at}.key「${key}」与前面那条撞了 —— 生成物里的键只能有一个`);
|
|
2188
|
+
else seenKeys.add(key);
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
const host = raw.host;
|
|
2194
|
+
if (host !== void 0) {
|
|
2195
|
+
if (!isRecord(host)) problems.push(`${subject}: dsh.workbench.host 要是对象,现在是 ${JSON.stringify(host)}`);
|
|
2196
|
+
else {
|
|
2197
|
+
const extra = outsideKeys(host, HOST_KEYS);
|
|
2198
|
+
if (extra.length > 0) problems.push(`${subject}: dsh.workbench.host 里有不认的键 ${listOf(extra)} —— host 只有 ${listOf(HOST_KEYS)}`);
|
|
2199
|
+
if (host.inject === void 0) problems.push(`${subject}: dsh.workbench.host.inject 没写 —— 它声明这个模块要外壳的哪几个服务,外壳靠它判"能不能挂"。一个都不要也要写 \`"inject": []\``);
|
|
2200
|
+
else problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.inject`, host.inject));
|
|
2201
|
+
if (host.optional !== void 0) problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.optional`, host.optional));
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (raw.env !== void 0 && !isRecord(raw.env)) problems.push(`${subject}: dsh.workbench.env 要是对象(command / plugins / libs)—— 语义由门禁的 env 判据查,这里只查形状`);
|
|
2205
|
+
return problems;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* 读一份清单:先查形状,过了再把声明规整成 manifest。
|
|
2209
|
+
* @param subject 报错时用的主语。
|
|
2210
|
+
* @param raw 清单里 dsh.workbench 那段原始值。
|
|
2211
|
+
* @returns 有问题时只有 problems;没问题时 declaration 与 manifest 都给,manifest 里的字符串已 trim。
|
|
2212
|
+
*/
|
|
2213
|
+
function readDeclaration(subject, raw) {
|
|
2214
|
+
const problems = declarationProblemsOf(subject, raw);
|
|
2215
|
+
if (problems.length > 0) return { problems };
|
|
2216
|
+
const declaration = raw;
|
|
2217
|
+
const card = declaration.card;
|
|
2218
|
+
return {
|
|
2219
|
+
declaration,
|
|
2220
|
+
problems: [],
|
|
2221
|
+
manifest: {
|
|
2222
|
+
id: text$1(declaration.unit),
|
|
2223
|
+
card: card === null ? null : {
|
|
2224
|
+
titleKey: text$1(card.titleKey),
|
|
2225
|
+
order: card.order
|
|
2226
|
+
},
|
|
2227
|
+
tools: declaration.tools.map((tool) => ({
|
|
2228
|
+
name: text$1(tool.name),
|
|
2229
|
+
writes: tool.writes
|
|
2230
|
+
})),
|
|
2231
|
+
host: {
|
|
2232
|
+
inject: [...declaration.host.inject ?? []],
|
|
2233
|
+
...declaration.host.optional === void 0 ? {} : { optional: [...declaration.host.optional] }
|
|
2234
|
+
},
|
|
2235
|
+
...declaration.env === void 0 ? {} : { env: declaration.env }
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2101
2239
|
//#endregion
|
|
2102
2240
|
//#region ../shared/dist/host/value-handoff.js
|
|
2103
2241
|
function handoffId(entry) {
|
|
@@ -2789,6 +2927,8 @@ var DIY_RECORD_PREPARE_TOOL = "workbench_diy_record_prepare";
|
|
|
2789
2927
|
var DIY_RECORD_READ_TOOL = "workbench_diy_record_read";
|
|
2790
2928
|
var DIY_RECORD_DETAIL_TOOL = "workbench_diy_record_detail";
|
|
2791
2929
|
var DIY_RECORD_STOP_TOOL = "workbench_diy_record_stop";
|
|
2930
|
+
var LONG_TASKS_LIST_TOOL = "workbench_long_tasks_list";
|
|
2931
|
+
var LONG_TASKS_STOP_TOOL = "workbench_long_tasks_stop";
|
|
2792
2932
|
var DIY_DEBUG_START_TOOL = "workbench_diy_debug_start";
|
|
2793
2933
|
var DIY_DEBUG_PAUSE_TOOL = "workbench_diy_debug_pause";
|
|
2794
2934
|
var DIY_DEBUG_RESUME_TOOL = "workbench_diy_debug_resume";
|
|
@@ -2821,7 +2961,8 @@ var READ_TOOLS = [
|
|
|
2821
2961
|
DIY_LOGS_STOP_TOOL,
|
|
2822
2962
|
DIY_RECORD_READ_TOOL,
|
|
2823
2963
|
DIY_RECORD_DETAIL_TOOL,
|
|
2824
|
-
DIY_RECORD_STOP_TOOL
|
|
2964
|
+
DIY_RECORD_STOP_TOOL,
|
|
2965
|
+
LONG_TASKS_LIST_TOOL
|
|
2825
2966
|
];
|
|
2826
2967
|
var WRITE_TOOLS = [
|
|
2827
2968
|
PANELS_SHOW_TOOL,
|
|
@@ -2843,7 +2984,8 @@ var WRITE_TOOLS = [
|
|
|
2843
2984
|
DIY_DEBUG_START_TOOL,
|
|
2844
2985
|
DIY_DEBUG_PAUSE_TOOL,
|
|
2845
2986
|
DIY_DEBUG_RESUME_TOOL,
|
|
2846
|
-
DIY_DEBUG_STOP_TOOL
|
|
2987
|
+
DIY_DEBUG_STOP_TOOL,
|
|
2988
|
+
LONG_TASKS_STOP_TOOL
|
|
2847
2989
|
];
|
|
2848
2990
|
var DIY_TOOLS = [
|
|
2849
2991
|
DIY_LIST_TOOL,
|
|
@@ -2902,6 +3044,7 @@ var panelsUnit = {
|
|
|
2902
3044
|
//#endregion
|
|
2903
3045
|
//#region src/modules/panels/model/approval.ts
|
|
2904
3046
|
var SPEC_LIMIT = 80;
|
|
3047
|
+
var TARGET_LIMIT = 20;
|
|
2905
3048
|
function missing(id) {
|
|
2906
3049
|
return id === void 0 || id === "";
|
|
2907
3050
|
}
|
|
@@ -2976,7 +3119,32 @@ function askOf(reason, question, fallback) {
|
|
|
2976
3119
|
fallback
|
|
2977
3120
|
};
|
|
2978
3121
|
}
|
|
3122
|
+
function textOf$5(raw, key) {
|
|
3123
|
+
const value = raw[key];
|
|
3124
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3125
|
+
}
|
|
3126
|
+
function clipped(text) {
|
|
3127
|
+
const chars = [...text];
|
|
3128
|
+
return chars.length > TARGET_LIMIT ? `${chars.slice(0, TARGET_LIMIT).join("")}…` : text;
|
|
3129
|
+
}
|
|
3130
|
+
function stopVerdict(call, runs) {
|
|
3131
|
+
const raw = argsOf$2(call);
|
|
3132
|
+
const unit = textOf$5(raw, "unit");
|
|
3133
|
+
const id = textOf$5(raw, "id");
|
|
3134
|
+
if (unit === "" || id === "") return { kind: "allow" };
|
|
3135
|
+
const run = runs?.().get(unit)?.snapshot(id) ?? null;
|
|
3136
|
+
if (runs !== void 0 && run === null) return { kind: "allow" };
|
|
3137
|
+
const session = run?.session ?? "";
|
|
3138
|
+
if (session !== "" && session === call.session) return { kind: "allow" };
|
|
3139
|
+
const target = run === null ? "" : run.target.trim();
|
|
3140
|
+
const where = target === "" ? `工作台 [${unit}] 那一趟长任务` : `工作台 [${unit}] 那一趟「${clipped(target)}」长任务`;
|
|
3141
|
+
const whereEn = target === "" ? `a long-running workbench job [${unit}]` : `a long-running workbench job [${unit}] "${clipped(target)}"`;
|
|
3142
|
+
const origin = session === "" ? "用户自己按的" : "别的会话起的";
|
|
3143
|
+
const originEn = session === "" ? "the user started" : "another session started";
|
|
3144
|
+
return askOf(`${LONG_TASKS_STOP_TOOL} ${unit} stops a long run someone else started`, pair(`会停掉${where},${origin},允许吗?`, `it stops ${whereEn} ${originEn}. Allow?`), pair(`agent 想停掉${where},${origin},允许吗?`, `The agent wants to stop ${whereEn} ${originEn}. Allow?`));
|
|
3145
|
+
}
|
|
2979
3146
|
var panelsApproval = (call) => {
|
|
3147
|
+
if (call.name === "workbench_long_tasks_stop") return stopVerdict(call, void 0);
|
|
2980
3148
|
if (call.name === "workbench_diy_debug_start" || call.name === "workbench_diy_debug_resume") {
|
|
2981
3149
|
const starting = call.name === DIY_DEBUG_START_TOOL;
|
|
2982
3150
|
const unit = argsOf$2(call)["unit"];
|
|
@@ -3048,6 +3216,15 @@ var panelsApproval = (call) => {
|
|
|
3048
3216
|
default: return;
|
|
3049
3217
|
}
|
|
3050
3218
|
};
|
|
3219
|
+
/**
|
|
3220
|
+
* 面板族的审批策略:`panelsApproval` 再认长任务那一档(写成功要用户点头)。
|
|
3221
|
+
* `longTaskRuns` 用来查"这一趟是谁起的" —— 自己起的免问,别的会话起的 / 用户按的要问。
|
|
3222
|
+
* @param longTaskRuns 当前挂着的单元各自那把长任务注册表;没给就按"别人的"问(保守的那一侧)。
|
|
3223
|
+
* @returns 审批策略:认领的写工具回 ask / allow,不是本族的回 undefined。
|
|
3224
|
+
*/
|
|
3225
|
+
function panelsApprovalFor(longTaskRuns) {
|
|
3226
|
+
return (call) => call.name === "workbench_long_tasks_stop" ? stopVerdict(call, longTaskRuns) : panelsApproval(call);
|
|
3227
|
+
}
|
|
3051
3228
|
//#endregion
|
|
3052
3229
|
//#region src/modules/panels/model/config.ts
|
|
3053
3230
|
var EMPTY_BOARD = {
|
|
@@ -3162,40 +3339,225 @@ function added(list, id) {
|
|
|
3162
3339
|
function dropped(list, id) {
|
|
3163
3340
|
return list.filter((item) => item !== id);
|
|
3164
3341
|
}
|
|
3165
|
-
function withShown(board, id, visible) {
|
|
3166
|
-
const next = {
|
|
3167
|
-
shown: added(board.shown, id),
|
|
3168
|
-
hidden: dropped(board.hidden, id)
|
|
3169
|
-
};
|
|
3170
|
-
return visible === void 0 ? next : {
|
|
3171
|
-
...next,
|
|
3172
|
-
order: added(board.order ?? visible, id)
|
|
3173
|
-
};
|
|
3342
|
+
function withShown(board, id, visible) {
|
|
3343
|
+
const next = {
|
|
3344
|
+
shown: added(board.shown, id),
|
|
3345
|
+
hidden: dropped(board.hidden, id)
|
|
3346
|
+
};
|
|
3347
|
+
return visible === void 0 ? next : {
|
|
3348
|
+
...next,
|
|
3349
|
+
order: added(board.order ?? visible, id)
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
function withHidden(board, id) {
|
|
3353
|
+
const order = dropped(board.order ?? [], id);
|
|
3354
|
+
return {
|
|
3355
|
+
shown: dropped(board.shown, id),
|
|
3356
|
+
hidden: added(board.hidden, id),
|
|
3357
|
+
...order.length === 0 ? {} : { order }
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
function withoutPanel(board, id) {
|
|
3361
|
+
return {
|
|
3362
|
+
shown: dropped(board.shown, id),
|
|
3363
|
+
hidden: dropped(board.hidden, id)
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
function withMoved(board, id, index, visible) {
|
|
3367
|
+
const current = visible.includes(id) ? [...visible] : [...visible, id];
|
|
3368
|
+
const from = current.indexOf(id);
|
|
3369
|
+
const to = Math.max(0, Math.min(current.length - 1, Math.trunc(index)));
|
|
3370
|
+
if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
|
|
3371
|
+
return {
|
|
3372
|
+
shown: added(board.shown, id),
|
|
3373
|
+
hidden: dropped(board.hidden, id),
|
|
3374
|
+
order: current
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
//#endregion
|
|
3378
|
+
//#region src/modules/panels/model/long-task-tools.ts
|
|
3379
|
+
var STATUS_WORDS = {
|
|
3380
|
+
running: "进行中",
|
|
3381
|
+
"needs-login": "要登录",
|
|
3382
|
+
stopped: "被停了",
|
|
3383
|
+
ok: "已完成",
|
|
3384
|
+
failed: "出错"
|
|
3385
|
+
};
|
|
3386
|
+
var NO_WORKSPACE = "不知道当前会话在哪个工作区 —— 长任务按工作区记,这条读不了。";
|
|
3387
|
+
var NO_RUNS = "这个工作区现在没有长任务在跑(也没有十分钟内刚收尾的)。";
|
|
3388
|
+
var NO_RUN = "没有这一趟 —— unit/id 对不上。";
|
|
3389
|
+
var STALE_REGISTRY = "这一份外壳还是上一版的注册表,停不了 —— 重新起一下工作台就能停";
|
|
3390
|
+
var LIST_OUTPUT = {
|
|
3391
|
+
schema: {
|
|
3392
|
+
type: "object",
|
|
3393
|
+
properties: {
|
|
3394
|
+
text: { type: "string" },
|
|
3395
|
+
runs: {
|
|
3396
|
+
type: "array",
|
|
3397
|
+
items: {
|
|
3398
|
+
type: "object",
|
|
3399
|
+
properties: {
|
|
3400
|
+
unit: { type: "string" },
|
|
3401
|
+
id: { type: "string" },
|
|
3402
|
+
target: { type: "string" },
|
|
3403
|
+
status: { type: "string" }
|
|
3404
|
+
},
|
|
3405
|
+
additionalProperties: true
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
},
|
|
3409
|
+
additionalProperties: true
|
|
3410
|
+
},
|
|
3411
|
+
render: (_args, value) => [{
|
|
3412
|
+
type: "text",
|
|
3413
|
+
text: typeof value?.text === "string" ? value.text : JSON.stringify(value)
|
|
3414
|
+
}]
|
|
3415
|
+
};
|
|
3416
|
+
var STOP_OUTPUT = {
|
|
3417
|
+
schema: {
|
|
3418
|
+
type: "object",
|
|
3419
|
+
properties: {
|
|
3420
|
+
text: { type: "string" },
|
|
3421
|
+
stopped: { type: "boolean" }
|
|
3422
|
+
},
|
|
3423
|
+
additionalProperties: true
|
|
3424
|
+
},
|
|
3425
|
+
render: (_args, value) => [{
|
|
3426
|
+
type: "text",
|
|
3427
|
+
text: typeof value?.text === "string" ? value.text : JSON.stringify(value)
|
|
3428
|
+
}]
|
|
3429
|
+
};
|
|
3430
|
+
function str(value) {
|
|
3431
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3432
|
+
}
|
|
3433
|
+
function arg(raw, key) {
|
|
3434
|
+
return str(typeof raw === "object" && raw !== null ? raw[key] : void 0);
|
|
3435
|
+
}
|
|
3436
|
+
function statusWord(status) {
|
|
3437
|
+
return STATUS_WORDS[status] ?? status;
|
|
3438
|
+
}
|
|
3439
|
+
function originOf(session, caller) {
|
|
3440
|
+
if (session === "") return "用户按的";
|
|
3441
|
+
return session === caller ? "我起的" : "别的会话起的";
|
|
3442
|
+
}
|
|
3443
|
+
function listed$1(runs, workspace) {
|
|
3444
|
+
const out = [];
|
|
3445
|
+
for (const [unit, registry] of runs?.() ?? []) for (const row of registry.current(workspace)) {
|
|
3446
|
+
const id = str(row["id"]);
|
|
3447
|
+
const run = id === "" ? null : registry.snapshot(id);
|
|
3448
|
+
out.push({
|
|
3449
|
+
unit,
|
|
3450
|
+
id,
|
|
3451
|
+
target: str(row["target"]) || str(run?.target),
|
|
3452
|
+
status: str(row["status"]) || str(run?.status),
|
|
3453
|
+
step: str(row["step"]) || str(row["why"]) || str(run?.step) || str(run?.why),
|
|
3454
|
+
session: str(row["session"]) || str(run?.session)
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
return out;
|
|
3458
|
+
}
|
|
3459
|
+
function lineOf(run, index, caller) {
|
|
3460
|
+
const parts = [
|
|
3461
|
+
run.unit,
|
|
3462
|
+
run.target,
|
|
3463
|
+
statusWord(run.status),
|
|
3464
|
+
run.step,
|
|
3465
|
+
run.id === "" ? "" : `id=${run.id}`,
|
|
3466
|
+
originOf(run.session, caller)
|
|
3467
|
+
];
|
|
3468
|
+
return `${String(index + 1)}. ${parts.filter((part) => part !== "").join(" · ")}`;
|
|
3174
3469
|
}
|
|
3175
|
-
function
|
|
3176
|
-
|
|
3177
|
-
return {
|
|
3178
|
-
shown: dropped(board.shown, id),
|
|
3179
|
-
hidden: added(board.hidden, id),
|
|
3180
|
-
...order.length === 0 ? {} : { order }
|
|
3181
|
-
};
|
|
3470
|
+
function renderList$1(runs, workspace, caller) {
|
|
3471
|
+
if (runs.length === 0) return NO_RUNS;
|
|
3472
|
+
return [`当前工作区:${workspace}`, ...runs.map((run, index) => lineOf(run, index, caller))].join("\n");
|
|
3182
3473
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3474
|
+
/**
|
|
3475
|
+
* 注册长任务那两把外壳工具:看得见(`workbench_long_tasks_list`)与停得了任何单元的一趟
|
|
3476
|
+
* (`workbench_long_tasks_stop`)。`longTaskRuns` 是晚绑定的取值器:两把都从它取注册表,
|
|
3477
|
+
* 所以单元晚挂上 / 被摘掉都跟得上;没给的话停那把一律回"没有这一趟"。
|
|
3478
|
+
* @param ctx 宿主上下文,`tools` 服务不在时什么都不注册。
|
|
3479
|
+
* @param longTaskRuns 当前挂着的单元各自那把长任务注册表。
|
|
3480
|
+
* @returns 注销函数(没有 tools 服务就是空数组)。
|
|
3481
|
+
*/
|
|
3482
|
+
function registerLongTaskTools(ctx, longTaskRuns) {
|
|
3483
|
+
const tools = ctx.get("tools");
|
|
3484
|
+
if (tools === void 0) {
|
|
3485
|
+
ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过外壳长任务工具注册");
|
|
3486
|
+
return [];
|
|
3487
|
+
}
|
|
3488
|
+
const listTool = {
|
|
3489
|
+
name: LONG_TASKS_LIST_TOOL,
|
|
3490
|
+
description: "看这个工作区在跑(或十分钟内刚收尾)的长任务:每行是 单元 · 目标 · 状态 · 当前一步 · id · 谁起的;要停哪一趟用 workbench_long_tasks_stop。",
|
|
3491
|
+
parameters: {
|
|
3492
|
+
type: "object",
|
|
3493
|
+
properties: {},
|
|
3494
|
+
additionalProperties: false
|
|
3495
|
+
},
|
|
3496
|
+
output: LIST_OUTPUT,
|
|
3497
|
+
async execute(_raw, exec) {
|
|
3498
|
+
const workspace = sessionWorkspace(exec);
|
|
3499
|
+
if (workspace === null) return {
|
|
3500
|
+
runs: [],
|
|
3501
|
+
text: NO_WORKSPACE
|
|
3502
|
+
};
|
|
3503
|
+
const runs = listed$1(longTaskRuns, workspace);
|
|
3504
|
+
return {
|
|
3505
|
+
runs,
|
|
3506
|
+
text: renderList$1(runs, workspace, sessionIdOf(exec))
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3187
3509
|
};
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3510
|
+
const stopTool = {
|
|
3511
|
+
name: LONG_TASKS_STOP_TOOL,
|
|
3512
|
+
description: "停掉某一趟长任务(unit 与 id 从 workbench_long_tasks_list 拿)。只是让外壳给它发中断信号,平台侧那趟活儿的收尾看模块自己。",
|
|
3513
|
+
parameters: {
|
|
3514
|
+
type: "object",
|
|
3515
|
+
properties: {
|
|
3516
|
+
unit: {
|
|
3517
|
+
type: "string",
|
|
3518
|
+
description: "那一趟属于的单元 id(如 cmdb)。"
|
|
3519
|
+
},
|
|
3520
|
+
id: {
|
|
3521
|
+
type: "string",
|
|
3522
|
+
description: "那一趟的 id(列表里 id= 后面那串)。"
|
|
3523
|
+
}
|
|
3524
|
+
},
|
|
3525
|
+
required: ["unit", "id"],
|
|
3526
|
+
additionalProperties: false
|
|
3527
|
+
},
|
|
3528
|
+
output: STOP_OUTPUT,
|
|
3529
|
+
async execute(raw, exec) {
|
|
3530
|
+
const unit = arg(raw, "unit");
|
|
3531
|
+
const id = arg(raw, "id");
|
|
3532
|
+
if (unit === "" || id === "") return {
|
|
3533
|
+
text: NO_RUN,
|
|
3534
|
+
stopped: false
|
|
3535
|
+
};
|
|
3536
|
+
const registry = longTaskRuns?.().get(unit);
|
|
3537
|
+
if (registry === void 0 || typeof registry.stop !== "function") return {
|
|
3538
|
+
text: registry === void 0 ? NO_RUN : STALE_REGISTRY,
|
|
3539
|
+
stopped: false
|
|
3540
|
+
};
|
|
3541
|
+
const run = registry.snapshot(id);
|
|
3542
|
+
if (run === null) return {
|
|
3543
|
+
text: NO_RUN,
|
|
3544
|
+
stopped: false
|
|
3545
|
+
};
|
|
3546
|
+
if (run.workspace !== sessionWorkspace(exec)) return {
|
|
3547
|
+
text: `这一趟不在你当前的工作区(它在 ${run.workspace})—— 没动它。`,
|
|
3548
|
+
stopped: false
|
|
3549
|
+
};
|
|
3550
|
+
if (!registry.stop(id, sessionIdOf(exec) ?? void 0)) return {
|
|
3551
|
+
text: `这一趟已经不在跑了(${statusWord(run.status)})—— 没动它。`,
|
|
3552
|
+
stopped: false
|
|
3553
|
+
};
|
|
3554
|
+
return {
|
|
3555
|
+
text: `停了:${unit} · ${run.target}(${id})。这一趟的活儿收到中断信号 —— 它认不认、停在哪一步看它自己。`,
|
|
3556
|
+
stopped: true
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3198
3559
|
};
|
|
3560
|
+
return [tools.register(listTool), tools.register(stopTool)];
|
|
3199
3561
|
}
|
|
3200
3562
|
//#endregion
|
|
3201
3563
|
//#region src/shared/api/host/diy-components.ts
|
|
@@ -3658,6 +4020,18 @@ async function resolveAt(registry, dir) {
|
|
|
3658
4020
|
function looksLikeWorkshop(dir) {
|
|
3659
4021
|
return existsSync(join(dir, "packages")) || existsSync(join(dir, "pnpm-workspace.yaml"));
|
|
3660
4022
|
}
|
|
4023
|
+
/**
|
|
4024
|
+
* 工坊**自己的家**(`$DSH_HOME/workbench/diy`)—— 宿主启动时就是按它把工坊登记成工作区的。
|
|
4025
|
+
* DIY 会话的工坊根取这一份,**不随会话 cwd 漂**:cwd 一离开工坊(用户把自己的项目设成工作区),
|
|
4026
|
+
* 从 cwd 往上找会撞上任意一个"带 `packages/` 的仓库",于是模块列表、`shots/` 落点、装机名单全跟着变样。
|
|
4027
|
+
* 这一档拿不到(没有 `dshHomePath` 服务)时才退回 cwd 那套。
|
|
4028
|
+
* @param ctx - 宿主上下文(要从里面取 `dshHomePath`)。
|
|
4029
|
+
* @returns 工坊的家;取不到给 `null`。
|
|
4030
|
+
*/
|
|
4031
|
+
function workshopHomeOf(ctx) {
|
|
4032
|
+
const path = workshopPathOf(ctx);
|
|
4033
|
+
return path === null ? null : rootAt(path, WORKSHOP_TITLE);
|
|
4034
|
+
}
|
|
3661
4035
|
async function workshopRootOf(ctx, workspace) {
|
|
3662
4036
|
const start = typeof workspace === "string" ? workspace.trim() : "";
|
|
3663
4037
|
if (start === "") return null;
|
|
@@ -3750,10 +4124,19 @@ function showOrReason(panels, unit, workspace) {
|
|
|
3750
4124
|
};
|
|
3751
4125
|
}
|
|
3752
4126
|
}
|
|
4127
|
+
/**
|
|
4128
|
+
* 这一次 DIY 调用落在哪个工坊上:**先认工坊自己的家**(`$DSH_HOME/workbench/diy`,宿主就是按它把工坊
|
|
4129
|
+
* 登记成工作区的),**不跟着会话 cwd 走** —— 用户把自己的项目设成工作区时,cwd 往上找会撞上任意一个
|
|
4130
|
+
* 带 `packages/` 的仓库,模块列表与 `shots/` 落点会一起变样(真机踩过:录制落点跑到仓库根、截图 EPERM)。
|
|
4131
|
+
* 只有拿不到工坊的家时,才退回"从会话工作区往上找像工坊的目录"。
|
|
4132
|
+
* @param ctx - 宿主上下文。
|
|
4133
|
+
* @param exec - 这次工具调用(退回那一档要读它的会话工作区)。
|
|
4134
|
+
* @returns 工坊的 packages 目录、根路径与标题。
|
|
4135
|
+
*/
|
|
3753
4136
|
async function workshopOf(ctx, exec) {
|
|
3754
4137
|
const session = sessionWorkspace(exec);
|
|
3755
|
-
const root = await workshopRootOf(ctx, session);
|
|
3756
|
-
if (root === null) throw new Error(
|
|
4138
|
+
const root = workshopHomeOf(ctx) ?? await workshopRootOf(ctx, session);
|
|
4139
|
+
if (root === null) throw new Error(`这台机器上没有工坊可落地(既没有 \$DSH_HOME/workbench/diy,会话工作区也没有模块包)(这个会话的工作区 ${session ?? "(不知道)"})—— 先在工坊里造一个模块,或者按工程域在仓库里用 dsh-wb-module 新建一个模块包`);
|
|
3757
4140
|
return {
|
|
3758
4141
|
packagesDir: root.packagesDir,
|
|
3759
4142
|
workspace: root.root,
|
|
@@ -4275,7 +4658,7 @@ async function stopNetworkExact(shell, file, timeoutMs = SWEEP_TIMEOUT_MS) {
|
|
|
4275
4658
|
`[ -n "$p" ] && kill "$p" 2>/dev/null`,
|
|
4276
4659
|
`[ -n "$r" ] && pkill -P "$r" 2>/dev/null`,
|
|
4277
4660
|
`[ -n "$r" ] && kill "$r" 2>/dev/null`,
|
|
4278
|
-
`if [ "$(cat ${pidFile} 2>/dev/null)" = "$p" ]; then rm -f ${
|
|
4661
|
+
`if [ "$(cat ${pidFile} 2>/dev/null)" = "$p" ]; then rm -f ${pidFile} ${reaperFile}; fi`,
|
|
4279
4662
|
"true"
|
|
4280
4663
|
].join("; "), { timeoutMs })).exitCode === 0;
|
|
4281
4664
|
}
|
|
@@ -6344,7 +6727,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6344
6727
|
const OFF = ${JSON.stringify(RECORD_OFF_KEY)};
|
|
6345
6728
|
const CAP = ${String(50)};
|
|
6346
6729
|
const BODY_MAX = ${String(16384)};
|
|
6347
|
-
const blank = () => ({ phase: 'armed', t0: null, t1: null, href: location.href, dropped: 0, calls: [] });
|
|
6730
|
+
const blank = () => ({ phase: 'armed', t0: null, t1: null, href: location.href, dropped: 0, calls: [], shots: 0, shotsAt: null });
|
|
6348
6731
|
const readKeep = () => {
|
|
6349
6732
|
try {
|
|
6350
6733
|
const raw = sessionStorage.getItem(KEEP);
|
|
@@ -6379,23 +6762,64 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6379
6762
|
const repaint = () => {
|
|
6380
6763
|
if (typeof state.__repaint === 'function') state.__repaint();
|
|
6381
6764
|
};
|
|
6382
|
-
const
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6765
|
+
const beatOf = (kind) => {
|
|
6766
|
+
try {
|
|
6767
|
+
state.beatN = (typeof state.beatN === 'number' ? state.beatN : 0) + 1;
|
|
6768
|
+
state.beatKind = kind;
|
|
6769
|
+
state.beatAt = Date.now();
|
|
6770
|
+
keep();
|
|
6771
|
+
if (typeof window.__dshBeatWake === 'function') {
|
|
6772
|
+
const wake = window.__dshBeatWake;
|
|
6773
|
+
window.__dshBeatWake = null;
|
|
6774
|
+
wake();
|
|
6775
|
+
}
|
|
6776
|
+
} catch (error) {}
|
|
6777
|
+
};
|
|
6778
|
+
const pending = [];
|
|
6779
|
+
const begin = (method, url, body) => {
|
|
6780
|
+
if (state.phase !== 'recording') return null;
|
|
6781
|
+
const target = String(url === undefined || url === null ? '' : url);
|
|
6782
|
+
const verb = String(method === '' || method === undefined ? 'GET' : method).toUpperCase();
|
|
6783
|
+
const row = {
|
|
6784
|
+
method: verb,
|
|
6785
|
+
url: target,
|
|
6387
6786
|
body: slice(body === undefined || body === null ? '' : body),
|
|
6388
|
-
status:
|
|
6389
|
-
ct:
|
|
6390
|
-
response:
|
|
6787
|
+
status: 0,
|
|
6788
|
+
ct: '',
|
|
6789
|
+
response: '',
|
|
6391
6790
|
at: Date.now(),
|
|
6392
|
-
}
|
|
6791
|
+
};
|
|
6792
|
+
state.calls.push(row);
|
|
6393
6793
|
if (state.calls.length > CAP) {
|
|
6394
6794
|
state.dropped = (typeof state.dropped === 'number' ? state.dropped : 0) + (state.calls.length - CAP);
|
|
6395
6795
|
state.calls.splice(0, state.calls.length - CAP);
|
|
6396
6796
|
}
|
|
6797
|
+
pending.push(row);
|
|
6397
6798
|
keep();
|
|
6398
6799
|
repaint();
|
|
6800
|
+
return row;
|
|
6801
|
+
};
|
|
6802
|
+
const complete = (row, status, ct, response) => {
|
|
6803
|
+
row.status = typeof status === 'number' ? status : 0;
|
|
6804
|
+
row.ct = String(ct === undefined || ct === null ? '' : ct);
|
|
6805
|
+
row.response = slice(response === undefined || response === null ? '' : response);
|
|
6806
|
+
};
|
|
6807
|
+
const record = (method, url, body, status, ct, response) => {
|
|
6808
|
+
const row = begin(method, url, body);
|
|
6809
|
+
if (row === null) return;
|
|
6810
|
+
complete(row, status, ct, response);
|
|
6811
|
+
keep();
|
|
6812
|
+
};
|
|
6813
|
+
const finish = (row, method, url, body, status, ct, response) => {
|
|
6814
|
+
const at = row === null || row === undefined ? -1 : pending.indexOf(row);
|
|
6815
|
+
if (at >= 0) pending.splice(at, 1);
|
|
6816
|
+
if (at >= 0 && state.calls.indexOf(row) >= 0) {
|
|
6817
|
+
complete(row, status, ct, response);
|
|
6818
|
+
keep();
|
|
6819
|
+
repaint();
|
|
6820
|
+
return;
|
|
6821
|
+
}
|
|
6822
|
+
record(method, url, body, status, ct, response);
|
|
6399
6823
|
};
|
|
6400
6824
|
const fresh = typeof window[OFF] !== 'function';
|
|
6401
6825
|
if (fresh) {
|
|
@@ -6426,6 +6850,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6426
6850
|
const url = typeof asked === 'string' ? asked : asked !== null && typeof asked === 'object' ? asked.url : '';
|
|
6427
6851
|
const method = init.method !== undefined ? init.method : asked !== null && typeof asked === 'object' ? asked.method : 'GET';
|
|
6428
6852
|
const body = init.body === undefined ? '' : init.body;
|
|
6853
|
+
const row = begin(method, url, body);
|
|
6429
6854
|
const done = realFetch.apply(this, args);
|
|
6430
6855
|
try {
|
|
6431
6856
|
done.then((response) => {
|
|
@@ -6433,11 +6858,11 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6433
6858
|
const ct = response.headers.get('content-type') || '';
|
|
6434
6859
|
const clone = response.clone();
|
|
6435
6860
|
const target = response.url || url;
|
|
6436
|
-
clone.text().then((text) =>
|
|
6861
|
+
clone.text().then((text) => finish(row, method, target, body, response.status, ct, text)).catch(() => finish(row, method, target, body, response.status, ct, ''));
|
|
6437
6862
|
} catch (error) {}
|
|
6438
6863
|
}).catch((error) => {
|
|
6439
6864
|
try {
|
|
6440
|
-
|
|
6865
|
+
finish(row, method, url, body, 0, 'fetch-error', String(error !== null && typeof error === 'object' && error.message !== undefined ? error.message : error));
|
|
6441
6866
|
} catch (inner) {}
|
|
6442
6867
|
});
|
|
6443
6868
|
} catch (error) {}
|
|
@@ -6455,9 +6880,10 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6455
6880
|
xhr.removeEventListener('loadend', xhr.__dshLoadEnd);
|
|
6456
6881
|
} catch (error) {}
|
|
6457
6882
|
}
|
|
6883
|
+
const row = begin(xhr.__dshMethod, xhr.__dshUrl, body);
|
|
6458
6884
|
const onEnd = () => {
|
|
6459
6885
|
try {
|
|
6460
|
-
|
|
6886
|
+
finish(row, xhr.__dshMethod, xhr.__dshUrl, body, xhr.status, ctOf(xhr), textOf(xhr));
|
|
6461
6887
|
} catch (error) {}
|
|
6462
6888
|
};
|
|
6463
6889
|
xhr.__dshLoadEnd = onEnd;
|
|
@@ -6467,7 +6893,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6467
6893
|
if (hasBeacon) {
|
|
6468
6894
|
navigator.sendBeacon = function (url, data) {
|
|
6469
6895
|
try {
|
|
6470
|
-
|
|
6896
|
+
begin('POST', url, data === undefined || data === null ? '' : data);
|
|
6471
6897
|
} catch (error) {}
|
|
6472
6898
|
return realBeacon.apply(navigator, arguments);
|
|
6473
6899
|
};
|
|
@@ -6536,23 +6962,31 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6536
6962
|
const label = document.createElement('span');
|
|
6537
6963
|
label.setAttribute('style', 'flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums');
|
|
6538
6964
|
const start = document.createElement('button');
|
|
6965
|
+
const shot = document.createElement('button');
|
|
6539
6966
|
const end = document.createElement('button');
|
|
6540
6967
|
start.type = 'button';
|
|
6541
6968
|
start.setAttribute('title', '开始录制');
|
|
6542
6969
|
start.setAttribute('aria-label', '开始录制');
|
|
6543
6970
|
start.innerHTML = '<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="4" width="9" height="8" rx="2"></rect><path d="M10.5 7.4 14.5 5v6l-4-2.4z"></path></svg>开始';
|
|
6971
|
+
shot.type = 'button';
|
|
6972
|
+
shot.setAttribute('title', '拍一张当前页面的截图');
|
|
6973
|
+
shot.setAttribute('aria-label', '拍一张当前页面的截图');
|
|
6974
|
+
shot.innerHTML = '<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5V1.5H5M11 1.5h3.5V5M14.5 11v3.5H11M5 14.5H1.5V11"></path><rect x="6" y="6" width="4" height="4" rx="1"></rect></svg>截图';
|
|
6544
6975
|
end.type = 'button';
|
|
6545
6976
|
end.setAttribute('title', '结束录制');
|
|
6546
6977
|
end.setAttribute('aria-label', '结束录制');
|
|
6547
6978
|
end.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true" focusable="false"><rect x="2" y="2" width="8" height="8" rx="1.5" fill="currentColor"></rect></svg>结束';
|
|
6548
|
-
const look = (on) => 'display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:0;border-radius:6px;font-size:12px;line-height:1;font-family:inherit;white-space:nowrap;cursor:' + (on ? 'pointer' : 'default') + ';color
|
|
6979
|
+
const look = (on) => 'display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border:0;border-radius:6px;font-size:12px;line-height:1;font-family:inherit;white-space:nowrap;cursor:' + (on ? 'pointer' : 'default') + ';color:' + (on ? '#fff' : 'rgba(255,255,255,.45)') + ';background:' + (on ? '#3b82f6' : '#2b3444');
|
|
6549
6980
|
const paint = () => {
|
|
6550
6981
|
const phase = state.phase;
|
|
6551
|
-
|
|
6982
|
+
const taken = typeof state.shots === 'number' ? state.shots : 0;
|
|
6983
|
+
label.textContent = (phase === 'recording' ? '录制中 · ' : phase === 'finished' ? '录制完成 · ' : '录制 · 等开始 · ') + String(state.calls.length) + ' 条' + (taken > 0 ? ' · 已拍 ' + String(taken) + ' 张' : '');
|
|
6552
6984
|
dot.style.display = phase === 'recording' ? 'inline-block' : 'none';
|
|
6553
6985
|
start.disabled = phase !== 'armed';
|
|
6986
|
+
shot.disabled = phase !== 'recording';
|
|
6554
6987
|
end.disabled = phase !== 'recording';
|
|
6555
6988
|
start.setAttribute('style', look(phase === 'armed'));
|
|
6989
|
+
shot.setAttribute('style', look(phase === 'recording'));
|
|
6556
6990
|
end.setAttribute('style', look(phase === 'recording'));
|
|
6557
6991
|
};
|
|
6558
6992
|
state.__repaint = paint;
|
|
@@ -6604,12 +7038,22 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6604
7038
|
state.href = location.href;
|
|
6605
7039
|
state.calls = [];
|
|
6606
7040
|
state.dropped = 0;
|
|
7041
|
+
beatOf('start');
|
|
7042
|
+
keep();
|
|
7043
|
+
paint();
|
|
7044
|
+
};
|
|
7045
|
+
shot.onclick = () => {
|
|
7046
|
+
if (state.phase !== 'recording') return;
|
|
7047
|
+
state.shots = (typeof state.shots === 'number' ? state.shots : 0) + 1;
|
|
7048
|
+
state.shotsAt = Date.now();
|
|
7049
|
+
beatOf('shot');
|
|
6607
7050
|
keep();
|
|
6608
7051
|
paint();
|
|
6609
7052
|
};
|
|
6610
7053
|
end.onclick = () => {
|
|
6611
7054
|
state.phase = 'finished';
|
|
6612
7055
|
state.t1 = Date.now();
|
|
7056
|
+
beatOf('stop');
|
|
6613
7057
|
keep();
|
|
6614
7058
|
paint();
|
|
6615
7059
|
};
|
|
@@ -6617,15 +7061,17 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6617
7061
|
bar.appendChild(grip);
|
|
6618
7062
|
bar.appendChild(dot);
|
|
6619
7063
|
bar.appendChild(label);
|
|
7064
|
+
bar.appendChild(shot);
|
|
6620
7065
|
bar.appendChild(start);
|
|
6621
7066
|
bar.appendChild(end);
|
|
6622
7067
|
if (typeof state.barX === 'number' && typeof state.barY === 'number') place(state.barX, state.barY);
|
|
6623
7068
|
paint();
|
|
6624
|
-
return { ok: true, fresh, phase: state.phase, t0: state.t0 === undefined ? null : state.t0, t1: state.t1 === undefined ? null : state.t1, href: state.href || location.href, size: state.calls.length, dropped: typeof state.dropped === 'number' ? state.dropped : 0 };
|
|
7069
|
+
return { ok: true, fresh, phase: state.phase, t0: state.t0 === undefined ? null : state.t0, t1: state.t1 === undefined ? null : state.t1, href: state.href || location.href, size: state.calls.length, shots: typeof state.shots === 'number' ? state.shots : 0, shotsAt: typeof state.shotsAt === 'number' ? state.shotsAt : null, dropped: typeof state.dropped === 'number' ? state.dropped : 0 };
|
|
6625
7070
|
})()`;
|
|
6626
|
-
var RECORD_READ_SCRIPT = `(() => {
|
|
7071
|
+
var RECORD_READ_SCRIPT = `(async () => {
|
|
6627
7072
|
const KEY = ${JSON.stringify(RECORD_KEY)};
|
|
6628
7073
|
const KEEP = ${JSON.stringify(RECORD_KEEP_KEY)};
|
|
7074
|
+
const BEAT_WAIT = 3000;
|
|
6629
7075
|
let state = window[KEY] !== null && typeof window[KEY] === 'object' ? window[KEY] : null;
|
|
6630
7076
|
if (state === null) {
|
|
6631
7077
|
try {
|
|
@@ -6635,8 +7081,29 @@ var RECORD_READ_SCRIPT = `(() => {
|
|
|
6635
7081
|
state = null;
|
|
6636
7082
|
}
|
|
6637
7083
|
}
|
|
6638
|
-
if (state === null || typeof state !== 'object') return { ok: false, code: 'not-armed', phase: 'none', calls: [] };
|
|
6639
|
-
|
|
7084
|
+
if (state === null || typeof state !== 'object') return { ok: false, code: 'not-armed', phase: 'none', calls: [], shots: 0, shotsAt: null, beat: false, beatKind: '' };
|
|
7085
|
+
const from = typeof state.beatSeen === 'number' ? state.beatSeen : 0;
|
|
7086
|
+
let resolveWake = null;
|
|
7087
|
+
const woken = new Promise((resolve) => {
|
|
7088
|
+
resolveWake = resolve;
|
|
7089
|
+
});
|
|
7090
|
+
const onBeat = () => {
|
|
7091
|
+
const go = resolveWake;
|
|
7092
|
+
resolveWake = null;
|
|
7093
|
+
if (go !== null) go();
|
|
7094
|
+
};
|
|
7095
|
+
window.__dshBeatWake = onBeat;
|
|
7096
|
+
const changed = () => (typeof state.beatN === 'number' ? state.beatN : 0) !== from;
|
|
7097
|
+
if (!changed()) {
|
|
7098
|
+
let timer = null;
|
|
7099
|
+
await Promise.race([woken, new Promise((resolve) => { timer = setTimeout(resolve, BEAT_WAIT); })]);
|
|
7100
|
+
if (timer !== null) clearTimeout(timer);
|
|
7101
|
+
}
|
|
7102
|
+
if (window.__dshBeatWake === onBeat) window.__dshBeatWake = null;
|
|
7103
|
+
const after = typeof state.beatN === 'number' ? state.beatN : 0;
|
|
7104
|
+
const beat = after !== from;
|
|
7105
|
+
state.beatSeen = after;
|
|
7106
|
+
return { ok: true, phase: typeof state.phase === 'string' ? state.phase : 'armed', t0: state.t0 === undefined ? null : state.t0, t1: state.t1 === undefined ? null : state.t1, href: typeof state.href === 'string' ? state.href : '', here: location.href, dropped: typeof state.dropped === 'number' ? state.dropped : 0, shots: typeof state.shots === 'number' ? state.shots : 0, shotsAt: typeof state.shotsAt === 'number' ? state.shotsAt : null, beat, beatKind: beat && typeof state.beatKind === 'string' ? state.beatKind : '', calls: Array.isArray(state.calls) ? state.calls : [] };
|
|
6640
7107
|
})()`;
|
|
6641
7108
|
var RECORD_STOP_SCRIPT = `(() => {
|
|
6642
7109
|
const KEY = ${JSON.stringify(RECORD_KEY)};
|
|
@@ -6895,6 +7362,10 @@ function recordPageOf(payload) {
|
|
|
6895
7362
|
here: typeof raw["here"] === "string" ? raw["here"] : "",
|
|
6896
7363
|
dropped: typeof raw["dropped"] === "number" ? raw["dropped"] : 0,
|
|
6897
7364
|
unhooked: raw["unhooked"] === 1 || raw["unhooked"] === true,
|
|
7365
|
+
shots: typeof raw["shots"] === "number" ? raw["shots"] : 0,
|
|
7366
|
+
shotsAt: typeof raw["shotsAt"] === "number" ? raw["shotsAt"] : null,
|
|
7367
|
+
beat: raw["beat"] === true,
|
|
7368
|
+
beatKind: typeof raw["beatKind"] === "string" ? raw["beatKind"] : "",
|
|
6898
7369
|
calls
|
|
6899
7370
|
};
|
|
6900
7371
|
}
|
|
@@ -6975,21 +7446,43 @@ function sizeText(bytes) {
|
|
|
6975
7446
|
function rowTextOf(one) {
|
|
6976
7447
|
const mark = one.marked ? " ⭐" : " ";
|
|
6977
7448
|
const where = one.source === "capture" ? "捕获" : "钩子";
|
|
6978
|
-
|
|
7449
|
+
const identity = one.key === "" ? `${one.method} ${one.url}` : one.key;
|
|
7450
|
+
const how = one.key === "" ? "无 key" : `at ${String(one.at)}`;
|
|
7451
|
+
return `· [${String(one.index)}]${mark} ${identity}(${where} · 入参 ${one.hasRequest ? "有" : "无"} · 出参 ${sizeText(one.size)} · ${how})`;
|
|
7452
|
+
}
|
|
7453
|
+
function shortPath(path) {
|
|
7454
|
+
const at = path.lastIndexOf("shots/");
|
|
7455
|
+
return at >= 0 ? path.slice(at) : path;
|
|
7456
|
+
}
|
|
7457
|
+
function shotFaultLines(shots) {
|
|
7458
|
+
return shots.filter((one) => one.error !== void 0).map((one) => `(他按了「截图」,但有一张图没落下来:${shortPath(one.path)} —— ${one.error ?? ""})`);
|
|
7459
|
+
}
|
|
7460
|
+
function beatLineOf(beats, beatMs) {
|
|
7461
|
+
return beats > 0 ? `门铃:${String(beats)} 次(最近 ${String(beatMs)} 毫秒)` : "";
|
|
7462
|
+
}
|
|
7463
|
+
function evidenceTailOf(shots, beats, beatMs) {
|
|
7464
|
+
const parts = [];
|
|
7465
|
+
const landed = shots.filter((one) => one.error === void 0).length;
|
|
7466
|
+
if (landed > 0) parts.push(`图 ${String(landed)} 张`);
|
|
7467
|
+
const beat = beatLineOf(beats, beatMs);
|
|
7468
|
+
if (beat !== "") parts.push(beat);
|
|
7469
|
+
return parts.join(" · ");
|
|
6979
7470
|
}
|
|
6980
7471
|
function renderRecordRead(input) {
|
|
7472
|
+
const shots = input.shots ?? [];
|
|
7473
|
+
const tail = evidenceTailOf(shots, input.beats ?? 0, input.beatMs ?? 0);
|
|
7474
|
+
const sep = tail === "" ? "" : ` · ${tail}`;
|
|
7475
|
+
const faults = shotFaultLines(shots);
|
|
6981
7476
|
if (!input.armed) return `那一页上还没有录制条(${DIY_RECORD_PREPARE_TOOL} 起的那一场可能被导航冲掉了)—— 先 ${DIY_RECORD_PREPARE_TOOL},或让他回到那一页再 ${DIY_RECORD_READ_TOOL}。`;
|
|
6982
7477
|
if (input.phase === "armed") return "他还没按「开始录制」—— 等他按了再读。";
|
|
6983
|
-
if (input.phase === "recording") return `还在录(页面钩子上已经攒了 ${String(input.hookCaught)} 条)——
|
|
6984
|
-
const
|
|
6985
|
-
const
|
|
6986
|
-
const markShown = input.rows.filter((one) => one.marked).slice(0, CALLS_SHOWN);
|
|
7478
|
+
if (input.phase === "recording") return [`还在录(页面钩子上已经攒了 ${String(input.hookCaught)} 条)—— 等他按「结束录制」。${sep}`, ...faults].join("\n");
|
|
7479
|
+
const marked = input.rows.filter((one) => one.marked);
|
|
7480
|
+
const markShown = marked.slice(0, CALLS_SHOWN);
|
|
6987
7481
|
const restShown = input.rows.filter((one) => !one.marked).slice(-Math.max(0, CALLS_SHOWN - markShown.length));
|
|
6988
7482
|
const shown = [...markShown, ...restShown];
|
|
6989
|
-
const lines = [];
|
|
6990
|
-
if (input.rows.length > 0) lines.push(
|
|
6991
|
-
else lines.push(input.capturedTruncated ? `录完了:这一段(就解析到的那部分而言)一条请求都没抓到(页面钩子 0 条 · 解析到的会话捕获里落在这个时间段里 0 条)——
|
|
6992
|
-
if (input.capturedTruncated) lines.push(input.capturedTotalLines > input.capturedTailLimit ? `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行,这里只解析了最后 ${String(input.capturedTailLimit)} 行 —— 更早的没看;上面那几个"有几条 / 没落"的捕获数只对最后这段成立)` : `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行(没顶到 ${String(input.capturedTailLimit)} 行那顶帽),是**这一读本身被输出上限截了** —— 读到的只是最近一段,上面那几个捕获数只对读到的这段成立)`);
|
|
7483
|
+
const lines = [...input.capturedTruncated ? [input.capturedTotalLines > input.capturedTailLimit ? `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行,这里只解析了最后 ${String(input.capturedTailLimit)} 行 —— 更早的没看;下面的清单只对最后这段成立)` : `(捕获文件这一场一共落了 ${String(input.capturedTotalLines)} 行(没顶到 ${String(input.capturedTailLimit)} 行那顶帽),是**这一读本身被输出上限截了** —— 读到的只是最近一段,下面的清单只对读到的这段成立)`] : [], ...faults];
|
|
7484
|
+
if (input.rows.length > 0) lines.push(`录完了 · ${String(input.merged)} 条(⭐ ${String(marked.length)})${sep}`, ...shown.map(rowTextOf));
|
|
7485
|
+
else lines.push(input.capturedTruncated ? `录完了:这一段(就解析到的那部分而言)一条请求都没抓到(页面钩子 0 条 · 解析到的会话捕获里落在这个时间段里 0 条)—— 但捕获文件被截过,见上面那行。${sep}` : `录完了:这一段一条请求都没抓到(页面钩子 0 条 · 会话捕获落在这个时间段里 0 条,两边都是 0 —— 不是筛掉的)。${sep}`);
|
|
6993
7486
|
if (input.dropped > 0) lines.push(`(缓冲区满了,最早丢了 ${String(input.dropped)} 条)`);
|
|
6994
7487
|
if (input.captureRound === "fail") lines.push("(会话捕获最近一轮没跑成(opencli 那条报了错)—— 这一场可能只有页面钩子。)");
|
|
6995
7488
|
else if (input.captureRound === "") lines.push("(会话捕获还没回过一轮 —— 再等一拍再看,别急着下\"没抓到\"的结论。)");
|
|
@@ -7010,6 +7503,7 @@ function clip(text, maxBytes) {
|
|
|
7010
7503
|
function sizeLabel(size, exact) {
|
|
7011
7504
|
return exact ? `${String(size)} 字节` : `${String(size)} 字符(opencli 没给 body_full_size,按字符数算)`;
|
|
7012
7505
|
}
|
|
7506
|
+
/** 把字段证据表插进详情正文:接在出参那一块之后、`请求头` 那一行(或第一条附注)之前;没有锚点就接在末尾。空串原样回。 */
|
|
7013
7507
|
function renderRecordDetail(input) {
|
|
7014
7508
|
if (!input.found) return [`没找到这一条:${input.note}`, `可用的取法:② 还在这场录制里时用清单里的 key 或 index;③ 这一场已经收工的话,给 unit 让我去读 shots/rec-<unit>.jsonl.last;④ 都不行就让他打开 DevTools,把那次请求复制成 curl。`].join("\n");
|
|
7015
7509
|
const req = clip(input.request, input.maxBytes);
|
|
@@ -7064,7 +7558,10 @@ var POLL_MS = 1e3;
|
|
|
7064
7558
|
var CAPTURE_TAIL_LINES = 2e4;
|
|
7065
7559
|
var CAPTURE_POLL_S = 3;
|
|
7066
7560
|
var CAPTURE_ARM_S = 2;
|
|
7067
|
-
|
|
7561
|
+
/**
|
|
7562
|
+
* 捕获那一层要滤掉的 URL:静态资源(`data:` 与常见后缀)。用法 `new RegExp(...).test(url)`,命中即丢。
|
|
7563
|
+
*/
|
|
7564
|
+
var STATIC_URL_PATTERN = `^(data:)|(\\.(js|css|png|jpe?g|gif|svg|woff2?|ico|map|webp|mp4)(\\?|$))`;
|
|
7068
7565
|
var RECORD_WATCH_MS = 3e3;
|
|
7069
7566
|
var RECORD_WATCH_TRIES = 5;
|
|
7070
7567
|
var DROP_STATIC_JS = "let s=\"\";process.stdin.on(\"data\",(d)=>s+=d).on(\"end\",()=>{const re=new RegExp(process.argv[1],\"i\");for(const line of s.split(\"\\n\")){const t=line.trim();if(t===\"\")continue;let one;try{one=JSON.parse(t)}catch{process.stdout.write(t+\"\\n\");continue}if(Array.isArray(one.entries)){one.entries=one.entries.filter((e)=>!re.test(String((e||{}).url||\"\")));if(one.entries.length===0)continue}process.stdout.write(JSON.stringify(one)+\"\\n\")}});";
|
|
@@ -7089,6 +7586,48 @@ function captureFileOf(packagesDir, unit) {
|
|
|
7089
7586
|
function lastCaptureFileOf(file) {
|
|
7090
7587
|
return `${file}.last`;
|
|
7091
7588
|
}
|
|
7589
|
+
/**
|
|
7590
|
+
* 这一族自己的落图路径:跟 `rec-<unit>.jsonl` 同目录、`rec-shot-` 前缀(工坊那一页的图另有名字)。
|
|
7591
|
+
* @param captureFile - 这一场捕获文件;图就落在它旁边。@param unit - 单元名。@param n - 页面「已拍」计数。
|
|
7592
|
+
* @returns 形如 `…/shots/rec-shot-<unit>-<n>.png` 的路径。
|
|
7593
|
+
*/
|
|
7594
|
+
function shotFileOf(captureFile, unit, n) {
|
|
7595
|
+
return join(dirname(captureFile), `rec-shot-${unit}-${n}.png`);
|
|
7596
|
+
}
|
|
7597
|
+
/**
|
|
7598
|
+
* 把这一张现场图落进 `shots/`:argv **只有 `screenshot <path>`,一个视口参数都不给** —— `--full-page` 走
|
|
7599
|
+
* `captureBeyondViewport`、`--width` / `--height` 走 `setDeviceMetricsOverride`:真机上按一下页面**看得见地闪**
|
|
7600
|
+
* (2026-10-21 两次报过)、还可能让填了一半的表单跳位;调试族那把平时也是这一档(先 `eval` 量盒子再本地 `clip`)。
|
|
7601
|
+
* 真机上这个平台整页高与视口一样(1512×698)—— `--full-page` 一点没多给,白闪。
|
|
7602
|
+
* @param input - 看护那一拍的依赖(shell / 命令 / 会话 / 台账)。
|
|
7603
|
+
* @param held - 当前那份约定(路径按它的 `unit` 拼)。@param n - 页面浮条上那个「已拍」计数。
|
|
7604
|
+
*/
|
|
7605
|
+
async function takeShotOf(input, held, n) {
|
|
7606
|
+
const path = shotFileOf(input.captureFile, held.unit, n);
|
|
7607
|
+
const at = Date.now();
|
|
7608
|
+
try {
|
|
7609
|
+
const step = await actBrowser(input.shell, input.command, BROWSER_RECORD_SESSION, ["screenshot", path]);
|
|
7610
|
+
if (step.ok) input.store.shotTaken(input.sessionId, {
|
|
7611
|
+
n,
|
|
7612
|
+
at,
|
|
7613
|
+
path
|
|
7614
|
+
}, Date.now());
|
|
7615
|
+
else input.store.shotFailed(input.sessionId, {
|
|
7616
|
+
n,
|
|
7617
|
+
at,
|
|
7618
|
+
path,
|
|
7619
|
+
error: actErrorOf(step)
|
|
7620
|
+
}, Date.now());
|
|
7621
|
+
} catch (error) {
|
|
7622
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
7623
|
+
input.store.shotFailed(input.sessionId, {
|
|
7624
|
+
n,
|
|
7625
|
+
at,
|
|
7626
|
+
path,
|
|
7627
|
+
error: text
|
|
7628
|
+
}, Date.now());
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7092
7631
|
async function startRecordCapture(shell, command, session, file) {
|
|
7093
7632
|
const bin = command === "" ? "opencli" : command;
|
|
7094
7633
|
const arm = backgroundLine(bin, [
|
|
@@ -7128,6 +7667,30 @@ async function releaseLeftoversOf(input) {
|
|
|
7128
7667
|
captureFile: input.captureFile
|
|
7129
7668
|
});
|
|
7130
7669
|
}
|
|
7670
|
+
async function stopCaptureQuiet(input) {
|
|
7671
|
+
try {
|
|
7672
|
+
return await stopNetworkExact(input.shell, input.captureFile);
|
|
7673
|
+
} catch {
|
|
7674
|
+
return false;
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
async function closeRecordTabQuiet(input) {
|
|
7678
|
+
try {
|
|
7679
|
+
return await closeRecordTab(input.shell, input.command, BROWSER_RECORD_SESSION, input.url);
|
|
7680
|
+
} catch {
|
|
7681
|
+
return false;
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
/**
|
|
7685
|
+
* 看护的一拍(= 一圈循环):读数那一条 `eval` **自己在页面里最多等 `RECORD_WATCH_MS`**(浮条在他按
|
|
7686
|
+
* 「开始 / 截图 / 结束」时会唤醒挂在页面上的那个 waiter ⇒ 一按就回来),所以这一轮 = **恰好一条 `eval`**:
|
|
7687
|
+
* 命中就立刻做那一拍,没人按就照旧 3 秒一拍 —— 兜底周期一字未变。该落图就落图;认到「结束」就封口、
|
|
7688
|
+
* **叫醒 agent**、**关掉录制窗**、收工(关窗排在叫醒之后:关窗慢 / 失败不许拖住叫醒,失败也不抛 ——
|
|
7689
|
+
* 那一扇是我们为录制借来的,ADR-0064 补记)。
|
|
7690
|
+
* ⚠️ 读数那条**不许**改成 opencli 的 `wait xhr`:它**读一次清一次**那只捕获环(`cdp.js:346-353`),而 reaper
|
|
7691
|
+
* 也读同一只环才拿得到 body ⇒ 会把 `rec-<unit>.jsonl` 吃空。理由与实测见 `.scratch/diy-write-record/issues/06-doorbell.md` §1.1。
|
|
7692
|
+
* @param input - 看护那一拍的依赖。@returns `stop` = 这条循环可以收了;`delivered` = 这一拍叫醒过 agent。
|
|
7693
|
+
*/
|
|
7131
7694
|
async function watchFinishStepOf(input) {
|
|
7132
7695
|
const live = input.store.live(Date.now());
|
|
7133
7696
|
if (live === null) {
|
|
@@ -7141,11 +7704,15 @@ async function watchFinishStepOf(input) {
|
|
|
7141
7704
|
stop: true,
|
|
7142
7705
|
delivered: false
|
|
7143
7706
|
};
|
|
7144
|
-
if (live.phase === "finished")
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7707
|
+
if (live.phase === "finished") {
|
|
7708
|
+
await closeRecordTabQuiet(input);
|
|
7709
|
+
return {
|
|
7710
|
+
stop: true,
|
|
7711
|
+
delivered: false
|
|
7712
|
+
};
|
|
7713
|
+
}
|
|
7148
7714
|
let page;
|
|
7715
|
+
const started = Date.now();
|
|
7149
7716
|
try {
|
|
7150
7717
|
page = recordPageOf((await actBrowser(input.shell, input.command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT])).payload);
|
|
7151
7718
|
} catch {
|
|
@@ -7154,6 +7721,7 @@ async function watchFinishStepOf(input) {
|
|
|
7154
7721
|
delivered: false
|
|
7155
7722
|
};
|
|
7156
7723
|
}
|
|
7724
|
+
if (page.beat) input.store.beat(input.sessionId, Math.max(0, Date.now() - started), Date.now());
|
|
7157
7725
|
const held = input.store.live(Date.now());
|
|
7158
7726
|
if (held === null) {
|
|
7159
7727
|
await releaseLeftoversOf(input);
|
|
@@ -7166,22 +7734,31 @@ async function watchFinishStepOf(input) {
|
|
|
7166
7734
|
stop: true,
|
|
7167
7735
|
delivered: false
|
|
7168
7736
|
};
|
|
7169
|
-
if (held.phase === "finished")
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7737
|
+
if (held.phase === "finished") {
|
|
7738
|
+
await stopCaptureQuiet(input);
|
|
7739
|
+
await closeRecordTabQuiet(input);
|
|
7740
|
+
return {
|
|
7741
|
+
stop: true,
|
|
7742
|
+
delivered: false
|
|
7743
|
+
};
|
|
7744
|
+
}
|
|
7173
7745
|
if (!page.ok) return {
|
|
7174
7746
|
stop: false,
|
|
7175
7747
|
delivered: false
|
|
7176
7748
|
};
|
|
7749
|
+
const due = shotDueOf(held, page.shots);
|
|
7750
|
+
if (due !== null) await takeShotOf(input, held, due);
|
|
7177
7751
|
if (page.phase === "finished" && page.t1 !== null) {
|
|
7178
7752
|
const now = Date.now();
|
|
7179
|
-
if (held.phase === "
|
|
7753
|
+
if (held.phase === "armed") input.store.started(input.sessionId, page.t0 ?? 0, now);
|
|
7754
|
+
if (input.store.live(Date.now())?.phase === "recording") input.store.finished(input.sessionId, page.t1, now);
|
|
7180
7755
|
const caught = page.calls.length;
|
|
7181
7756
|
const summary = `工作台 DIY:他按了「结束录制」(${String(caught)} 条)`;
|
|
7182
7757
|
deliverDebugNotice(input.ctx.get("agents"), input.sessionId, noticeMessage(`他按了「结束录制」(页面钩子上 ${String(caught)} 条)—— 去 ${DIY_RECORD_READ_TOOL} 取这一段清单,要某一条原文用 ${DIY_RECORD_DETAIL_TOOL}。`, summary), "wake");
|
|
7758
|
+
await stopCaptureQuiet(input);
|
|
7759
|
+
await closeRecordTabQuiet(input);
|
|
7183
7760
|
return {
|
|
7184
|
-
stop:
|
|
7761
|
+
stop: true,
|
|
7185
7762
|
delivered: true
|
|
7186
7763
|
};
|
|
7187
7764
|
}
|
|
@@ -7191,18 +7768,25 @@ async function watchFinishStepOf(input) {
|
|
|
7191
7768
|
delivered: false
|
|
7192
7769
|
};
|
|
7193
7770
|
}
|
|
7771
|
+
/**
|
|
7772
|
+
* 看护的一轮(就是 `watchFinishStepOf`,留着这个名字是给测试与调用点一个稳定的形)。
|
|
7773
|
+
* @param input - 看护那一拍的依赖。@returns 收不收工、有没有叫醒。
|
|
7774
|
+
*/
|
|
7775
|
+
async function watchRoundOf(input) {
|
|
7776
|
+
return watchFinishStepOf(input);
|
|
7777
|
+
}
|
|
7194
7778
|
function watchRecordFinish(input) {
|
|
7195
7779
|
let misses = 0;
|
|
7196
7780
|
const tick = () => {
|
|
7197
|
-
|
|
7781
|
+
watchRoundOf(input).then((out) => {
|
|
7198
7782
|
misses = 0;
|
|
7199
|
-
if (!out.stop) setTimeout(tick,
|
|
7783
|
+
if (!out.stop) setTimeout(tick, 0);
|
|
7200
7784
|
}).catch(() => {
|
|
7201
7785
|
misses += 1;
|
|
7202
7786
|
if (misses < RECORD_WATCH_TRIES) setTimeout(tick, RECORD_WATCH_MS);
|
|
7203
7787
|
});
|
|
7204
7788
|
};
|
|
7205
|
-
setTimeout(tick,
|
|
7789
|
+
setTimeout(tick, 0);
|
|
7206
7790
|
}
|
|
7207
7791
|
async function captureLinesOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
|
|
7208
7792
|
const step = await runShell(shell, `wc -l < ${quoteShellArg(file)} 2>/dev/null || echo 0`, { timeoutMs });
|
|
@@ -7284,30 +7868,12 @@ function detailFactsOf(input) {
|
|
|
7284
7868
|
responseSizeExact: input.responseSizeExact,
|
|
7285
7869
|
ambiguous: input.ambiguous,
|
|
7286
7870
|
source: input.source,
|
|
7287
|
-
text: renderRecordDetail(
|
|
7288
|
-
found: true,
|
|
7289
|
-
where: input.where,
|
|
7290
|
-
method: input.method,
|
|
7291
|
-
url: input.url,
|
|
7292
|
-
status: input.status,
|
|
7293
|
-
ct: input.ct,
|
|
7294
|
-
at: input.at,
|
|
7295
|
-
request: input.request,
|
|
7296
|
-
response: input.response,
|
|
7297
|
-
headers: input.headers,
|
|
7298
|
-
requestFullSize: input.requestFullSize,
|
|
7299
|
-
responseFullSize: input.responseFullSize,
|
|
7300
|
-
requestSizeExact: input.requestSizeExact,
|
|
7301
|
-
responseSizeExact: input.responseSizeExact,
|
|
7302
|
-
ambiguous: input.ambiguous,
|
|
7303
|
-
source: input.source,
|
|
7304
|
-
maxBytes: input.maxBytes,
|
|
7305
|
-
note: ""
|
|
7306
|
-
})
|
|
7871
|
+
text: renderRecordDetail(input)
|
|
7307
7872
|
});
|
|
7308
7873
|
}
|
|
7309
7874
|
function detailOfEntry(entry, maxBytes, ambiguous = 1) {
|
|
7310
7875
|
return detailFactsOf({
|
|
7876
|
+
found: true,
|
|
7311
7877
|
where: DETAIL_WHERE_CAPTURE,
|
|
7312
7878
|
key: entry.key,
|
|
7313
7879
|
method: entry.method,
|
|
@@ -7324,11 +7890,13 @@ function detailOfEntry(entry, maxBytes, ambiguous = 1) {
|
|
|
7324
7890
|
responseSizeExact: entry.responseSizeExact,
|
|
7325
7891
|
ambiguous,
|
|
7326
7892
|
source: "capture",
|
|
7327
|
-
maxBytes
|
|
7893
|
+
maxBytes,
|
|
7894
|
+
note: ""
|
|
7328
7895
|
});
|
|
7329
7896
|
}
|
|
7330
7897
|
function detailOfCall(call, maxBytes) {
|
|
7331
7898
|
return detailFactsOf({
|
|
7899
|
+
found: true,
|
|
7332
7900
|
where: DETAIL_WHERE_HOOK,
|
|
7333
7901
|
key: call.key,
|
|
7334
7902
|
method: call.method,
|
|
@@ -7345,31 +7913,45 @@ function detailOfCall(call, maxBytes) {
|
|
|
7345
7913
|
responseSizeExact: false,
|
|
7346
7914
|
ambiguous: 0,
|
|
7347
7915
|
source: "hook",
|
|
7348
|
-
maxBytes
|
|
7916
|
+
maxBytes,
|
|
7917
|
+
note: ""
|
|
7349
7918
|
});
|
|
7350
7919
|
}
|
|
7920
|
+
function blanksOf(pages, keep) {
|
|
7921
|
+
if (pages === null) return [];
|
|
7922
|
+
return pages.filter((one) => typeof one["url"] === "string" && (one["url"] === "" || one["url"] === "about:blank")).map((one) => typeof one["page"] === "string" ? one["page"] : "").filter((one) => one !== "" && one !== keep);
|
|
7923
|
+
}
|
|
7351
7924
|
function tabsOf(raw) {
|
|
7352
7925
|
const start = raw.indexOf("[");
|
|
7353
7926
|
const end = raw.lastIndexOf("]");
|
|
7354
|
-
if (start === -1 || end <= start) return
|
|
7927
|
+
if (start === -1 || end <= start) return null;
|
|
7355
7928
|
try {
|
|
7356
7929
|
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
7357
|
-
if (!Array.isArray(parsed)) return
|
|
7930
|
+
if (!Array.isArray(parsed)) return null;
|
|
7358
7931
|
return parsed.filter((one) => typeof one === "object" && one !== null);
|
|
7359
7932
|
} catch {
|
|
7360
|
-
return
|
|
7933
|
+
return null;
|
|
7361
7934
|
}
|
|
7362
7935
|
}
|
|
7363
7936
|
async function closeRecordTab(shell, command, session, url) {
|
|
7364
7937
|
const pages = tabsOf((await actBrowser(shell, command, session, ["tab", "list"])).stdout);
|
|
7365
|
-
|
|
7938
|
+
if (pages !== null && pages.length === 0) return true;
|
|
7939
|
+
const pick = pages?.find((one) => typeof one["url"] === "string" && one["url"] === url) ?? pages?.[0];
|
|
7366
7940
|
const page = pick === void 0 ? "" : typeof pick["page"] === "string" ? pick["page"] : "";
|
|
7367
7941
|
if (page !== "") {
|
|
7368
7942
|
if ((await actBrowser(shell, command, session, [
|
|
7369
7943
|
"tab",
|
|
7370
7944
|
"close",
|
|
7371
7945
|
page
|
|
7372
|
-
])).ok)
|
|
7946
|
+
])).ok) {
|
|
7947
|
+
for (const one of blanksOf(pages, page)) await actBrowser(shell, command, session, [
|
|
7948
|
+
"tab",
|
|
7949
|
+
"close",
|
|
7950
|
+
one
|
|
7951
|
+
]);
|
|
7952
|
+
await actBrowser(shell, command, session, ["close"]);
|
|
7953
|
+
return true;
|
|
7954
|
+
}
|
|
7373
7955
|
}
|
|
7374
7956
|
return (await actBrowser(shell, command, session, ["close"])).ok;
|
|
7375
7957
|
}
|
|
@@ -7391,6 +7973,81 @@ async function releaseQuietly(input) {
|
|
|
7391
7973
|
}
|
|
7392
7974
|
}
|
|
7393
7975
|
//#endregion
|
|
7976
|
+
//#region src/modules/panels/model/diy-record-open.ts
|
|
7977
|
+
var PAGE_HREF_SCRIPT = "JSON.stringify({ href: location.href })";
|
|
7978
|
+
/**
|
|
7979
|
+
* 两个地址是不是"同一页":比之前先把查询串 / 锚点 / 末尾斜杠抹掉 —— `open` 导航之后
|
|
7980
|
+
* 地址栏常常多一个斜杠或一串 hash,那不算开错了页。
|
|
7981
|
+
* @param here - 那一页现在真实所在的地址(`location.href`)。@param want - 我们要它去的地址。@returns 同一页为 `true`。
|
|
7982
|
+
*/
|
|
7983
|
+
function samePageOf(here, want) {
|
|
7984
|
+
const norm = (one) => one.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
7985
|
+
return here !== "" && norm(here) === norm(want);
|
|
7986
|
+
}
|
|
7987
|
+
async function whereAtOf(input) {
|
|
7988
|
+
const step = await actBrowser(input.shell, input.command, input.session, ["eval", PAGE_HREF_SCRIPT]);
|
|
7989
|
+
const href = step.payload === void 0 ? "" : step.payload["href"];
|
|
7990
|
+
return typeof href === "string" ? href : "";
|
|
7991
|
+
}
|
|
7992
|
+
async function settleOf(input) {
|
|
7993
|
+
await runBrowserStep(input.shell, input.command, [
|
|
7994
|
+
"browser",
|
|
7995
|
+
input.session,
|
|
7996
|
+
"wait",
|
|
7997
|
+
"time",
|
|
7998
|
+
String(SETTLE_MS / 1e3)
|
|
7999
|
+
], OPEN_TIMEOUT_MS);
|
|
8000
|
+
}
|
|
8001
|
+
/**
|
|
8002
|
+
* 把录制那一页开起来,并且**确认它真的开起来了**才回来。
|
|
8003
|
+
*
|
|
8004
|
+
* 先收掉这条会话的残留(否则新页会开进上一次留下的那扇旧窗里,不顶前台、看上去"根本没弹窗");
|
|
8005
|
+
* 再 `tab new --window foreground` 开我们的页;**然后读一次 `location.href` 核实** —— 开出来是空白页
|
|
8006
|
+
* 或别的地址时,回落到 opencli 的 `open`(它落在会话租约那个标签上)再核一次;两次都不对就
|
|
8007
|
+
* `ok: false`,让调用方把这句话原样报给 agent,**不许在没开起来的页上装浮条说"已开始录制"**。
|
|
8008
|
+
* @param input - 这条会话的名字 / 目标地址 / 捕获文件(收残留那一拍要用)。
|
|
8009
|
+
* @returns `ok: false` 时 `at` 是那一页现在真实的地址(`''` = 空白页),`why` 是给 agent 看的一句话。
|
|
8010
|
+
*/
|
|
8011
|
+
async function openRecordPage(input) {
|
|
8012
|
+
await releaseQuietly(input);
|
|
8013
|
+
const opened = await runBrowserStep(input.shell, input.command, [
|
|
8014
|
+
"browser",
|
|
8015
|
+
input.session,
|
|
8016
|
+
"tab",
|
|
8017
|
+
"new",
|
|
8018
|
+
input.url,
|
|
8019
|
+
"--window",
|
|
8020
|
+
"foreground"
|
|
8021
|
+
], OPEN_TIMEOUT_MS);
|
|
8022
|
+
if (!opened.ok) return {
|
|
8023
|
+
ok: false,
|
|
8024
|
+
at: "",
|
|
8025
|
+
why: `开不了录制窗:${opened.stderr || opened.stdout || "opencli 报了个错"}。先让用户把工作台那一页切到前台(第一次要这样),或跑 opencli doctor 看扩展连没连上。`
|
|
8026
|
+
};
|
|
8027
|
+
await settleOf(input);
|
|
8028
|
+
let at = await whereAtOf(input);
|
|
8029
|
+
if (!samePageOf(at, input.url)) {
|
|
8030
|
+
await runBrowserStep(input.shell, input.command, [
|
|
8031
|
+
"browser",
|
|
8032
|
+
input.session,
|
|
8033
|
+
"open",
|
|
8034
|
+
input.url
|
|
8035
|
+
], OPEN_TIMEOUT_MS);
|
|
8036
|
+
await settleOf(input);
|
|
8037
|
+
at = await whereAtOf(input);
|
|
8038
|
+
}
|
|
8039
|
+
if (!samePageOf(at, input.url)) return {
|
|
8040
|
+
ok: false,
|
|
8041
|
+
at,
|
|
8042
|
+
why: `那一页没开起来(现在开的是 ${at === "" ? "一张空白页" : at})—— 再看一眼 Chrome 里那扇录制窗,或跑 opencli doctor 看扩展连没连上。`
|
|
8043
|
+
};
|
|
8044
|
+
return {
|
|
8045
|
+
ok: true,
|
|
8046
|
+
at,
|
|
8047
|
+
why: ""
|
|
8048
|
+
};
|
|
8049
|
+
}
|
|
8050
|
+
//#endregion
|
|
7394
8051
|
//#region src/modules/panels/model/diy-record-tools.ts
|
|
7395
8052
|
function registerRecordTools(ctx, store) {
|
|
7396
8053
|
const tools = ctx.get("tools");
|
|
@@ -7400,7 +8057,7 @@ function registerRecordTools(ctx, store) {
|
|
|
7400
8057
|
}
|
|
7401
8058
|
const prepareTool = {
|
|
7402
8059
|
name: DIY_RECORD_PREPARE_TOOL,
|
|
7403
|
-
description: `开一扇浏览器窗口并顶到最前,让他自己录一次真实的写(开窗要点头一次);他按开始/结束后用 ${DIY_RECORD_READ_TOOL} 取那一段、${DIY_RECORD_STOP_TOOL}
|
|
8060
|
+
description: `开一扇浏览器窗口并顶到最前,让他自己录一次真实的写(开窗要点头一次);他按开始/结束后用 ${DIY_RECORD_READ_TOOL} 取那一段、${DIY_RECORD_STOP_TOOL} 收工。他按浮条上的「截图」不用另外点头(同一场同一屏,屏幕在开窗那一拍已经借过)—— 那张图与提交那一刻的快照一起进 ${DIY_RECORD_DETAIL_TOOL} 的字段证据表。`,
|
|
7404
8061
|
parameters: {
|
|
7405
8062
|
type: "object",
|
|
7406
8063
|
properties: {
|
|
@@ -7479,23 +8136,14 @@ function registerRecordTools(ctx, store) {
|
|
|
7479
8136
|
now: Date.now()
|
|
7480
8137
|
}), held);
|
|
7481
8138
|
try {
|
|
7482
|
-
const opened = await
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
"new",
|
|
8139
|
+
const opened = await openRecordPage({
|
|
8140
|
+
shell,
|
|
8141
|
+
command,
|
|
8142
|
+
session: BROWSER_RECORD_SESSION,
|
|
7487
8143
|
url,
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
if (!opened.ok) throw new Error(`开不了录制窗:${opened.stderr || opened.stdout || "opencli 报了个错"}。先让用户把工作台那一页切到前台(第一次要这样),或跑 opencli doctor 看扩展连没连上。`);
|
|
7492
|
-
await runBrowserStep(shell, command, [
|
|
7493
|
-
"browser",
|
|
7494
|
-
BROWSER_RECORD_SESSION,
|
|
7495
|
-
"wait",
|
|
7496
|
-
"time",
|
|
7497
|
-
String(SETTLE_MS / 1e3)
|
|
7498
|
-
], OPEN_TIMEOUT_MS);
|
|
8144
|
+
captureFile
|
|
8145
|
+
});
|
|
8146
|
+
if (!opened.ok) throw new Error(opened.why);
|
|
7499
8147
|
const armed = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
|
|
7500
8148
|
const page = recordPageOf(armed.payload);
|
|
7501
8149
|
if (!armed.ok || !page.ok) throw new Error(`浮条没装上:${armed.stderr || armed.stdout || "那一页还在加载"} —— 等他那一页站稳了再试。`);
|
|
@@ -7573,32 +8221,39 @@ function registerRecordTools(ctx, store) {
|
|
|
7573
8221
|
if (store.live(Date.now())?.phase === "recording") store.finished(sessionId, page.t1, Date.now());
|
|
7574
8222
|
}
|
|
7575
8223
|
};
|
|
7576
|
-
|
|
7577
|
-
let
|
|
7578
|
-
let page = recordPageOf(
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
await
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
if (next.ok) page = next;
|
|
8224
|
+
const over = held.phase === "finished";
|
|
8225
|
+
let stepOk = true;
|
|
8226
|
+
let page = recordPageOf(void 0);
|
|
8227
|
+
if (!over) {
|
|
8228
|
+
await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
|
|
8229
|
+
let step = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT]);
|
|
8230
|
+
page = recordPageOf(step.payload);
|
|
8231
|
+
stepOk = step.ok;
|
|
7585
8232
|
sync(page);
|
|
8233
|
+
while (budget > 0 && page.phase !== "finished" && Date.now() < deadline) {
|
|
8234
|
+
await pauseFor(Math.min(POLL_MS, Math.max(0, deadline - Date.now())));
|
|
8235
|
+
step = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT]);
|
|
8236
|
+
const next = recordPageOf(step.payload);
|
|
8237
|
+
if (next.ok) page = next;
|
|
8238
|
+
sync(page);
|
|
8239
|
+
}
|
|
7586
8240
|
}
|
|
7587
|
-
const timedOut = budget > 0 && page.phase !== "finished";
|
|
8241
|
+
const timedOut = !over && budget > 0 && page.phase !== "finished";
|
|
7588
8242
|
const waitedMs = opened === 0 ? 0 : Date.now() - opened;
|
|
7589
8243
|
if (timedOut) {
|
|
7590
8244
|
const waited = String(Math.round(budget / 1e3));
|
|
7591
8245
|
holdNotice(sessionId, noticeMessage(`录制还没结束(等了约 ${waited} 秒):他按了「结束」再 read 一次。`, `工作台 DIY:录制还没结束(等了约 ${waited} 秒)`));
|
|
7592
8246
|
}
|
|
7593
8247
|
const state = store.live(Date.now()) ?? held;
|
|
8248
|
+
const ledger = state.shots;
|
|
7594
8249
|
const capture = await captureReadOf(shell, state.captureFile);
|
|
7595
8250
|
const bundle = bundleOf(capture, page, state);
|
|
7596
8251
|
const hookCaught = page.calls.length;
|
|
7597
8252
|
const nav = page.href !== "" && page.here !== "" && page.href !== page.here;
|
|
7598
8253
|
const noBody = bodyMissingOf(bundle.merged);
|
|
7599
|
-
const
|
|
7600
|
-
phase: page.phase,
|
|
7601
|
-
armed: page.ok,
|
|
8254
|
+
const readText = renderRecordRead({
|
|
8255
|
+
phase: stepOk ? page.ok ? page.phase : state.phase : state.phase,
|
|
8256
|
+
armed: over ? true : stepOk ? page.ok : true,
|
|
7602
8257
|
hookCaught,
|
|
7603
8258
|
rows: bundle.rows,
|
|
7604
8259
|
merged: bundle.merged.length,
|
|
@@ -7611,11 +8266,17 @@ function registerRecordTools(ctx, store) {
|
|
|
7611
8266
|
dropped: page.dropped,
|
|
7612
8267
|
nav,
|
|
7613
8268
|
filter: filterTextOf(state.expect),
|
|
7614
|
-
captureRound: bundle.capture.round
|
|
7615
|
-
|
|
8269
|
+
captureRound: bundle.capture.round,
|
|
8270
|
+
shots: ledger,
|
|
8271
|
+
beats: state.beats,
|
|
8272
|
+
beatMs: state.beatMs
|
|
8273
|
+
});
|
|
8274
|
+
const note = over ? ["他按「结束」时那一扇录制窗就收掉了 —— 下面是磁盘上(捕获文件)这一段的清单。"] : [];
|
|
8275
|
+
const said = stepOk ? [readText, timedOut ? `还在录 —— 等他按「结束」(要等就带 waitMs,帽 ${String(WAIT_CAP_MS / 6e4)} 分钟)。` : ""] : ["那一页上读不到了(那一扇录制窗可能已经关了)—— 台账与捕获文件这一段的清单照样在:", readText];
|
|
8276
|
+
const text = [...note, ...said].filter((one) => one !== "").join("\n");
|
|
7616
8277
|
return plainJson({
|
|
7617
8278
|
workshop: packagesDir,
|
|
7618
|
-
ok:
|
|
8279
|
+
ok: stepOk && page.ok,
|
|
7619
8280
|
phase: state.phase,
|
|
7620
8281
|
t0: state.t0,
|
|
7621
8282
|
t1: state.t1,
|
|
@@ -7629,6 +8290,9 @@ function registerRecordTools(ctx, store) {
|
|
|
7629
8290
|
hookCaught,
|
|
7630
8291
|
noBody,
|
|
7631
8292
|
nav,
|
|
8293
|
+
shots: ledger,
|
|
8294
|
+
beats: state.beats,
|
|
8295
|
+
beatMs: state.beatMs,
|
|
7632
8296
|
waitedMs,
|
|
7633
8297
|
timedOut,
|
|
7634
8298
|
text
|
|
@@ -7706,7 +8370,9 @@ function registerRecordTools(ctx, store) {
|
|
|
7706
8370
|
const candidates = pickAllOf(captureEntriesOf(await captureLinesWithKeyOf(shell, file, key)), key);
|
|
7707
8371
|
const fromCapture = pickOf(candidates, key, at);
|
|
7708
8372
|
if (fromCapture !== null) return detailOfEntry(fromCapture, maxBytes, candidates.length);
|
|
7709
|
-
const
|
|
8373
|
+
const over = held === null || held.phase === "finished";
|
|
8374
|
+
const livePage = page !== null || over ? page : recordPageOf((await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT])).payload);
|
|
8375
|
+
const call = livePage === null ? null : hookCallOf(livePage, key);
|
|
7710
8376
|
if (call !== null) return detailOfCall(call, maxBytes);
|
|
7711
8377
|
return plainJson({
|
|
7712
8378
|
workshop: packagesDir,
|
|
@@ -7731,7 +8397,7 @@ function registerRecordTools(ctx, store) {
|
|
|
7731
8397
|
ambiguous: 0,
|
|
7732
8398
|
source: "",
|
|
7733
8399
|
maxBytes,
|
|
7734
|
-
note: `「${key}」既不在这一场的快照原文(${file})里,也不在这一页的钩子那一份里`
|
|
8400
|
+
note: over ? `「${key}」不在这一场的快照原文(${file})里 —— 这一场已经结束 / 收工,那一页也收掉了,钩子那一份问不到` : `「${key}」既不在这一场的快照原文(${file})里,也不在这一页的钩子那一份里`
|
|
7735
8401
|
})
|
|
7736
8402
|
});
|
|
7737
8403
|
}
|
|
@@ -9618,8 +10284,21 @@ function panelsHandoffEntry(target = {}) {
|
|
|
9618
10284
|
key: `path:${path}`
|
|
9619
10285
|
};
|
|
9620
10286
|
}
|
|
10287
|
+
function listedRunUnits(result) {
|
|
10288
|
+
const outer = typeof result === "object" && result !== null ? result : {};
|
|
10289
|
+
const unwrapped = outer["value"];
|
|
10290
|
+
const runs = (typeof unwrapped === "object" && unwrapped !== null ? unwrapped : outer)["runs"];
|
|
10291
|
+
if (!Array.isArray(runs)) return [];
|
|
10292
|
+
const units = /* @__PURE__ */ new Set();
|
|
10293
|
+
for (const row of runs) {
|
|
10294
|
+
const unit = typeof row === "object" && row !== null ? row.unit : void 0;
|
|
10295
|
+
if (typeof unit === "string" && unit !== "") units.add(unit);
|
|
10296
|
+
}
|
|
10297
|
+
return [...units];
|
|
10298
|
+
}
|
|
9621
10299
|
function panelsFactsOf(call, manifests) {
|
|
9622
10300
|
if (call.source === "workbench_panels_list") return [PANELS_FACT];
|
|
10301
|
+
if (call.source === "workbench_long_tasks_list") return listedRunUnits(call.result).map(runsRouteOf);
|
|
9623
10302
|
if (call.source === "workbench_modules_list") return [PANELS_FACT, CAPABILITY_FACT];
|
|
9624
10303
|
if (call.source === "workbench_modules_update_workbench") return [PANELS_FACT, CAPABILITY_FACT];
|
|
9625
10304
|
if (call.source === "workbench_diy_list") return [PANELS_FACT];
|
|
@@ -10183,7 +10862,7 @@ function registerPanelTools(ctx, panels, values) {
|
|
|
10183
10862
|
var OWNED = [{
|
|
10184
10863
|
unit: panelsUnit.id,
|
|
10185
10864
|
manifest: panelsUnit,
|
|
10186
|
-
mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
|
|
10865
|
+
mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values, longTaskRuns }) {
|
|
10187
10866
|
const ledger = createPluginLedger({
|
|
10188
10867
|
store,
|
|
10189
10868
|
home: isolatedHome()
|
|
@@ -10271,10 +10950,11 @@ var OWNED = [{
|
|
|
10271
10950
|
...registerPanelTools(ctx, panels, values),
|
|
10272
10951
|
...registerMarketTools(ctx, market),
|
|
10273
10952
|
...registerDiyTools(ctx, market, panels, debug),
|
|
10274
|
-
...registerRecordTools(ctx, record)
|
|
10953
|
+
...registerRecordTools(ctx, record),
|
|
10954
|
+
...registerLongTaskTools(ctx, longTaskRuns)
|
|
10275
10955
|
],
|
|
10276
10956
|
factsOf: (call) => panelsFactsOf(call, manifests()),
|
|
10277
|
-
needsApproval:
|
|
10957
|
+
needsApproval: panelsApprovalFor(longTaskRuns),
|
|
10278
10958
|
gate: {
|
|
10279
10959
|
deniedTools: (context) => {
|
|
10280
10960
|
const boards = panels.boards(context.workspace);
|
|
@@ -10331,7 +11011,29 @@ function withRunsRoute(result, unit) {
|
|
|
10331
11011
|
runs: path === "" ? [] : runs.current(path)
|
|
10332
11012
|
};
|
|
10333
11013
|
}
|
|
10334
|
-
}
|
|
11014
|
+
},
|
|
11015
|
+
actions: [...result.actions ?? [], {
|
|
11016
|
+
resource: stopRouteOf(unit),
|
|
11017
|
+
handle(payload) {
|
|
11018
|
+
if (typeof runs.stop !== "function") return {
|
|
11019
|
+
ok: false,
|
|
11020
|
+
text: "这一份外壳还是上一版的注册表,停不了 —— 重新起一下工作台就能停"
|
|
11021
|
+
};
|
|
11022
|
+
const id = payload?.id;
|
|
11023
|
+
if (typeof id !== "string" || id.trim() === "") return {
|
|
11024
|
+
ok: false,
|
|
11025
|
+
text: "要停哪一趟?给一个 id 才成"
|
|
11026
|
+
};
|
|
11027
|
+
if (runs.snapshot(id) === null) return {
|
|
11028
|
+
ok: false,
|
|
11029
|
+
text: "没有这一趟 —— id 对不上,没什么可停的"
|
|
11030
|
+
};
|
|
11031
|
+
return {
|
|
11032
|
+
ok: true,
|
|
11033
|
+
stopped: runs.stop(id)
|
|
11034
|
+
};
|
|
11035
|
+
}
|
|
11036
|
+
}]
|
|
10335
11037
|
};
|
|
10336
11038
|
}
|
|
10337
11039
|
function declaresNote(definition) {
|
|
@@ -10500,6 +11202,7 @@ function mountUnits(units, deps) {
|
|
|
10500
11202
|
const declaredNotes = /* @__PURE__ */ new Set();
|
|
10501
11203
|
const skipped = [];
|
|
10502
11204
|
const longTaskDisposers = /* @__PURE__ */ new Map();
|
|
11205
|
+
const longTaskRuns = deps.longTaskRuns ?? /* @__PURE__ */ new Map();
|
|
10503
11206
|
const ledger = {
|
|
10504
11207
|
failed: /* @__PURE__ */ new Set(),
|
|
10505
11208
|
failures: []
|
|
@@ -10520,7 +11223,8 @@ function mountUnits(units, deps) {
|
|
|
10520
11223
|
const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
|
|
10521
11224
|
scope: deps.longTasks.scope,
|
|
10522
11225
|
unit,
|
|
10523
|
-
bump: deps.longTasks.bump
|
|
11226
|
+
bump: deps.longTasks.bump,
|
|
11227
|
+
...deps.longTasks.notify === void 0 ? {} : { notify: deps.longTasks.notify }
|
|
10524
11228
|
});
|
|
10525
11229
|
let result;
|
|
10526
11230
|
try {
|
|
@@ -10535,6 +11239,7 @@ function mountUnits(units, deps) {
|
|
|
10535
11239
|
...deps.probe === void 0 ? {} : { probe: deps.probe },
|
|
10536
11240
|
...deps.values === void 0 ? {} : { values: deps.values },
|
|
10537
11241
|
...longTasks === void 0 ? {} : { longTasks: longTasks.host },
|
|
11242
|
+
longTaskRuns: () => longTaskRuns,
|
|
10538
11243
|
store
|
|
10539
11244
|
});
|
|
10540
11245
|
} catch (error) {
|
|
@@ -10555,6 +11260,8 @@ function mountUnits(units, deps) {
|
|
|
10555
11260
|
};
|
|
10556
11261
|
mounted.push(item);
|
|
10557
11262
|
if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
|
|
11263
|
+
const runs = item.result.longTasks;
|
|
11264
|
+
if (runs !== void 0) longTaskRuns.set(unit, runs);
|
|
10558
11265
|
}
|
|
10559
11266
|
const collected = collectRoutes(mounted, ledger);
|
|
10560
11267
|
const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
|
|
@@ -10575,6 +11282,8 @@ function mountUnits(units, deps) {
|
|
|
10575
11282
|
if (!ledger.failed.has(item)) continue;
|
|
10576
11283
|
longTaskDisposers.get(item)?.();
|
|
10577
11284
|
longTaskDisposers.delete(item);
|
|
11285
|
+
const runs = item.result.longTasks;
|
|
11286
|
+
if (runs !== void 0 && longTaskRuns.get(item.host.manifest.id) === runs) longTaskRuns.delete(item.host.manifest.id);
|
|
10578
11287
|
}
|
|
10579
11288
|
return {
|
|
10580
11289
|
mounted: alive,
|
|
@@ -10594,6 +11303,10 @@ function mountUnits(units, deps) {
|
|
|
10594
11303
|
disposeLongTasks: () => {
|
|
10595
11304
|
for (const dispose of [...longTaskDisposers.values()]) dispose();
|
|
10596
11305
|
longTaskDisposers.clear();
|
|
11306
|
+
for (const item of mounted) {
|
|
11307
|
+
const runs = item.result.longTasks;
|
|
11308
|
+
if (runs !== void 0 && longTaskRuns.get(item.host.manifest.id) === runs) longTaskRuns.delete(item.host.manifest.id);
|
|
11309
|
+
}
|
|
10597
11310
|
}
|
|
10598
11311
|
};
|
|
10599
11312
|
}
|
|
@@ -11079,6 +11792,37 @@ var DSH_PLUGINS_ROUTE = "dsh-plugins";
|
|
|
11079
11792
|
//#region src/shared/api/unit-manifests.ts
|
|
11080
11793
|
var UNIT_MANIFESTS_ROUTE = "unit-manifests";
|
|
11081
11794
|
//#endregion
|
|
11795
|
+
//#region src/shared/api/long-tasks.ts
|
|
11796
|
+
/**
|
|
11797
|
+
* 外壳自己的长任务读口:一次把**全进程还在跑的**长任务交出来(不分单元、不分工作区)——
|
|
11798
|
+
* 左栏拿它点会话行与项目下拉里那颗 loading 点。
|
|
11799
|
+
*/
|
|
11800
|
+
var LONG_TASKS_ROUTE = "long-tasks";
|
|
11801
|
+
//#endregion
|
|
11802
|
+
//#region src/shared/api/host/long-tasks.ts
|
|
11803
|
+
/**
|
|
11804
|
+
* 把「单元 → 它那把长任务注册表」收成一条条**还在跑**的行(`live()` 已经只给活着的那几趟,
|
|
11805
|
+
* 收尾的是面板那一行的事);注册表是上一版、没有 `live()` 就跳过这个单元 —— 与停止那条动作
|
|
11806
|
+
* 同一个理由:那一版里没有这份读口,不是这一版坏了。
|
|
11807
|
+
* @param runs 当前挂着的单元各自那把长任务注册表(外壳进程里就这一份)。
|
|
11808
|
+
* @returns 正在跑的长任务(含 `needs-login` 那种),每条带上是哪个单元起的。
|
|
11809
|
+
*/
|
|
11810
|
+
function liveLongTaskRows(runs) {
|
|
11811
|
+
const rows = [];
|
|
11812
|
+
for (const [unit, registry] of runs) {
|
|
11813
|
+
if (typeof registry.live !== "function") continue;
|
|
11814
|
+
for (const snapshot of registry.live()) rows.push({
|
|
11815
|
+
unit,
|
|
11816
|
+
id: snapshot.id,
|
|
11817
|
+
workspace: snapshot.workspace,
|
|
11818
|
+
target: snapshot.target,
|
|
11819
|
+
status: snapshot.status,
|
|
11820
|
+
...snapshot.session === void 0 ? {} : { session: snapshot.session }
|
|
11821
|
+
});
|
|
11822
|
+
}
|
|
11823
|
+
return rows;
|
|
11824
|
+
}
|
|
11825
|
+
//#endregion
|
|
11082
11826
|
//#region src/shared/api/host/context-inject.ts
|
|
11083
11827
|
var CONTEXT_SOURCE_KIND = "workbench";
|
|
11084
11828
|
function collectContextRefs(messages) {
|
|
@@ -11217,8 +11961,6 @@ function createPreflight(options) {
|
|
|
11217
11961
|
at: Date.now()
|
|
11218
11962
|
};
|
|
11219
11963
|
let running = null;
|
|
11220
|
-
let latest = null;
|
|
11221
|
-
let rerun = false;
|
|
11222
11964
|
function set(next) {
|
|
11223
11965
|
current = {
|
|
11224
11966
|
...current,
|
|
@@ -11413,21 +12155,16 @@ function createPreflight(options) {
|
|
|
11413
12155
|
...settled.ok ? {} : { bridgeIssues: settled.issues }
|
|
11414
12156
|
});
|
|
11415
12157
|
}
|
|
12158
|
+
function freshReady() {
|
|
12159
|
+
return current.phase === "ready" && Date.now() - current.at < 3e4;
|
|
12160
|
+
}
|
|
11416
12161
|
async function start(runOptions = {}) {
|
|
11417
|
-
if (running !== null)
|
|
11418
|
-
|
|
11419
|
-
return running;
|
|
11420
|
-
}
|
|
11421
|
-
if (runOptions.force !== true && latest !== null) return latest;
|
|
12162
|
+
if (running !== null) return running;
|
|
12163
|
+
if (runOptions.hard !== true && freshReady()) return current;
|
|
11422
12164
|
const inflight = doRun();
|
|
11423
|
-
latest = inflight;
|
|
11424
12165
|
running = inflight;
|
|
11425
12166
|
await inflight.catch(() => void 0);
|
|
11426
12167
|
running = null;
|
|
11427
|
-
if (rerun) {
|
|
11428
|
-
rerun = false;
|
|
11429
|
-
return start({ force: true });
|
|
11430
|
-
}
|
|
11431
12168
|
return inflight;
|
|
11432
12169
|
}
|
|
11433
12170
|
return {
|
|
@@ -11695,10 +12432,31 @@ async function probeReleases(units, options) {
|
|
|
11695
12432
|
}
|
|
11696
12433
|
return found;
|
|
11697
12434
|
}
|
|
12435
|
+
var pendingProbes = /* @__PURE__ */ new Map();
|
|
11698
12436
|
async function probeUnits(options) {
|
|
11699
12437
|
const { shell, command = "opencli", units, force = false } = options;
|
|
11700
12438
|
const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
|
|
11701
12439
|
if (!force && hits.every((hit) => hit !== void 0)) return hits;
|
|
12440
|
+
const key = `${command}:${units.map((unit) => unit.unit).join("|")}`;
|
|
12441
|
+
if (!force) {
|
|
12442
|
+
const shared = pendingProbes.get(key);
|
|
12443
|
+
if (shared !== void 0) return shared;
|
|
12444
|
+
}
|
|
12445
|
+
const task = loadCapabilities({
|
|
12446
|
+
shell,
|
|
12447
|
+
command,
|
|
12448
|
+
units,
|
|
12449
|
+
force
|
|
12450
|
+
});
|
|
12451
|
+
pendingProbes.set(key, task);
|
|
12452
|
+
try {
|
|
12453
|
+
return await task;
|
|
12454
|
+
} finally {
|
|
12455
|
+
if (pendingProbes.get(key) === task) pendingProbes.delete(key);
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
async function loadCapabilities(options) {
|
|
12459
|
+
const { shell, command, units, force } = options;
|
|
11702
12460
|
const result = await runShell(shell, `${command} plugin list -f json`, { timeoutMs: OPENCLI_TIMEOUT_MS }).then((value) => ({
|
|
11703
12461
|
kind: "ok",
|
|
11704
12462
|
stdout: value.exitCode === 0 ? value.stdout : "",
|
|
@@ -12905,6 +13663,64 @@ function warn(ctx, message) {
|
|
|
12905
13663
|
ctx.logger?.warn?.(`[dsh-workbench] ${message}`);
|
|
12906
13664
|
}
|
|
12907
13665
|
//#endregion
|
|
13666
|
+
//#region src/shared/api/host/approval-call.ts
|
|
13667
|
+
/**
|
|
13668
|
+
* 把 dsh 交来的那次工具调用折成审批策略要看的那份 call:工作区与发起会话都从 `exec` 现取,取不到就不写那个键。
|
|
13669
|
+
* @param exec dsh 交来的执行上下文;没有就只有实参这一格。
|
|
13670
|
+
* @param name 工具名;`undefined` = 这次调用没名字,不写这个键。
|
|
13671
|
+
* @returns 交给 `approvalFor` 的 `ApprovalCall`。
|
|
13672
|
+
*/
|
|
13673
|
+
function approvalCallOf(exec, name) {
|
|
13674
|
+
const workspace = sessionWorkspace(exec) ?? void 0;
|
|
13675
|
+
const session = sessionIdOf(exec) ?? void 0;
|
|
13676
|
+
return {
|
|
13677
|
+
...name === void 0 ? {} : { name },
|
|
13678
|
+
arguments: exec?.arguments,
|
|
13679
|
+
...workspace === void 0 ? {} : { workspace },
|
|
13680
|
+
...session === void 0 ? {} : { session }
|
|
13681
|
+
};
|
|
13682
|
+
}
|
|
13683
|
+
//#endregion
|
|
13684
|
+
//#region src/shared/api/host/long-task-notice.ts
|
|
13685
|
+
/**
|
|
13686
|
+
* 这条 notice 的来源标识(`source.kind`)—— 长任务自己一格,与调试 / 录制分开。
|
|
13687
|
+
*/
|
|
13688
|
+
var LONG_TASK_NOTICE_KIND = "workbench-long-task";
|
|
13689
|
+
/**
|
|
13690
|
+
* 判这一趟被停要不要回流给发起它的那个会话,并把话写好。
|
|
13691
|
+
* 只有"结算 + `stopped` + 这一趟记得会话 + 不是它自己停的"才回一条;跑完 / 失败 / 界面起的(没记会话)/ 自己停的回 `null`。
|
|
13692
|
+
* @param unit 这一趟属于哪个单元(进文案)。
|
|
13693
|
+
* @param change 长任务那一次变更;只有 `kind: 'settle'` 且 `snapshot.status === 'stopped'` 才可能成。
|
|
13694
|
+
* @returns 该发的那条(带收件会话),或者 `null`(不发)。
|
|
13695
|
+
*/
|
|
13696
|
+
function longTaskStopNotice(unit, change) {
|
|
13697
|
+
if (change.kind !== "settle") return null;
|
|
13698
|
+
const snapshot = change.snapshot;
|
|
13699
|
+
if (snapshot.status !== "stopped") return null;
|
|
13700
|
+
const session = snapshot.session;
|
|
13701
|
+
if (session === void 0 || session === "") return null;
|
|
13702
|
+
if (change.by === session) return null;
|
|
13703
|
+
const target = snapshot.target;
|
|
13704
|
+
const summary = `长任务:${unit} 的「${target}」被停了`;
|
|
13705
|
+
return {
|
|
13706
|
+
session,
|
|
13707
|
+
summary,
|
|
13708
|
+
message: noticeMessage(`[长任务 · ${unit}]「${target}」那一趟被${change.by === void 0 ? "用户" : "别的会话"}停了 —— 别再把它当"还在跑";要接着做就重新起一趟(workbench_long_tasks_list 看当前这几趟)。`, summary, LONG_TASK_NOTICE_KIND)
|
|
13709
|
+
};
|
|
13710
|
+
}
|
|
13711
|
+
/**
|
|
13712
|
+
* 把"这一趟被停了"送出去:正在跑的会话 inject 一条、闲着的跳过、没有这个 agent 就当没发生(模式 `drop`)。
|
|
13713
|
+
* @param agents 会话表(宿主从 `ctx.get('agents')` 拿);拿不到就什么都不发。
|
|
13714
|
+
* @param unit 这一趟属于哪个单元。
|
|
13715
|
+
* @param change 长任务那一次变更。
|
|
13716
|
+
* @returns 送出去的结果;判据说不该发(`longTaskStopNotice` 回 `null`)就回 `null`。
|
|
13717
|
+
*/
|
|
13718
|
+
function notifyLongTaskStop(agents, unit, change) {
|
|
13719
|
+
const notice = longTaskStopNotice(unit, change);
|
|
13720
|
+
if (notice === null) return null;
|
|
13721
|
+
return deliverDebugNotice(agents, notice.session, notice.message, "drop");
|
|
13722
|
+
}
|
|
13723
|
+
//#endregion
|
|
12908
13724
|
//#region src/app/host.ts
|
|
12909
13725
|
var name = "dsh-workbench";
|
|
12910
13726
|
var inject = ["webServer"];
|
|
@@ -13024,9 +13840,14 @@ function apply(ctx) {
|
|
|
13024
13840
|
});
|
|
13025
13841
|
const mounted = /* @__PURE__ */ new Map();
|
|
13026
13842
|
const mountFailures = /* @__PURE__ */ new Map();
|
|
13843
|
+
const longTaskRuns = /* @__PURE__ */ new Map();
|
|
13027
13844
|
const factsOfMap = /* @__PURE__ */ new Map();
|
|
13028
13845
|
const routesOfUnitMap = /* @__PURE__ */ new Map();
|
|
13029
13846
|
const ownersMap = /* @__PURE__ */ new Map();
|
|
13847
|
+
routes[LONG_TASKS_ROUTE] = () => ({
|
|
13848
|
+
ok: true,
|
|
13849
|
+
runs: liveLongTaskRows(longTaskRuns)
|
|
13850
|
+
});
|
|
13030
13851
|
const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
|
|
13031
13852
|
const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
|
|
13032
13853
|
const deniedNow = (context) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(context) ?? []))];
|
|
@@ -13076,8 +13897,10 @@ function apply(ctx) {
|
|
|
13076
13897
|
values,
|
|
13077
13898
|
longTasks: {
|
|
13078
13899
|
scope: process.env["DSH_HOME"] ?? "",
|
|
13079
|
-
bump: (names) => facts.bump(names)
|
|
13080
|
-
|
|
13900
|
+
bump: (names) => facts.bump([...names, LONG_TASKS_ROUTE]),
|
|
13901
|
+
notify: (change) => notifyLongTaskStop(ctx.get("agents"), host.unit, change)
|
|
13902
|
+
},
|
|
13903
|
+
longTaskRuns
|
|
13081
13904
|
});
|
|
13082
13905
|
} catch (error) {
|
|
13083
13906
|
mountFailures.set(id, {
|
|
@@ -13255,12 +14078,7 @@ function apply(ctx) {
|
|
|
13255
14078
|
const tool = name === void 0 ? void 0 : unitsOfTool().get(name);
|
|
13256
14079
|
const owner = tool === void 0 ? void 0 : mounted.get(tool.unit)?.result;
|
|
13257
14080
|
const detailOf = owner === void 0 ? void 0 : (call) => owner.approvalDetail?.(call);
|
|
13258
|
-
const
|
|
13259
|
-
const call = {
|
|
13260
|
-
...name === void 0 ? {} : { name },
|
|
13261
|
-
arguments: exec.arguments,
|
|
13262
|
-
...workspace === void 0 ? {} : { workspace }
|
|
13263
|
-
};
|
|
14081
|
+
const call = approvalCallOf(exec, name);
|
|
13264
14082
|
const policy = owner?.needsApproval;
|
|
13265
14083
|
const declaredNote = name !== void 0 && declaredNoteTools().has(name);
|
|
13266
14084
|
return await approvalFor(call, {
|