@dsh-workbench/core 0.3.6 → 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 +1260 -1928
- package/dist/client/client.body.js +1260 -1928
- package/dist/client/index.js +1260 -1928
- package/dist/host/index.js +1240 -790
- package/dist/types/app/client.d.ts +76 -3388
- 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/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/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/long-tasks.d.ts +10 -0
- 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-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 +23 -23
- package/skills/workbench-module-diy/PANEL.md +1 -1
- package/skills/workbench-module-diy/TOOLS.md +3 -1
- 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
|
/**
|
|
@@ -287,23 +100,6 @@ function stopRouteOf(unit) {
|
|
|
287
100
|
}
|
|
288
101
|
//#endregion
|
|
289
102
|
//#region ../shared/dist/lib/text.js
|
|
290
|
-
/**
|
|
291
|
-
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
292
|
-
* @param value - 任意值;不是字符串的先 `String()`。
|
|
293
|
-
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
294
|
-
*/
|
|
295
|
-
function quoteShellArg(value) {
|
|
296
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
|
|
300
|
-
* @param text - 原文;`null` / `undefined` 当空串。
|
|
301
|
-
* @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
|
|
302
|
-
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
303
|
-
*/
|
|
304
|
-
function tailLines(text, lines = 6) {
|
|
305
|
-
return String(text ?? "").trim().split("\n").slice(-lines).join("\n");
|
|
306
|
-
}
|
|
307
103
|
function candidateStarts(raw) {
|
|
308
104
|
const starts = [];
|
|
309
105
|
for (let index = 0; index < raw.length; index += 1) {
|
|
@@ -345,233 +141,36 @@ function parseJsonLoose(text) {
|
|
|
345
141
|
var DAY = 864e5;
|
|
346
142
|
30 * DAY;
|
|
347
143
|
365 * DAY;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region ../shared/dist/lib/board-fact.js
|
|
348
146
|
/**
|
|
349
|
-
*
|
|
147
|
+
* 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
|
|
148
|
+
* @param root - 工作区根路径。
|
|
149
|
+
* @param target - 要判的路径。
|
|
150
|
+
* @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
350
151
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"
|
|
354
|
-
|
|
355
|
-
"ready",
|
|
356
|
-
"error"
|
|
357
|
-
];
|
|
358
|
-
function asRecord$8(value) {
|
|
359
|
-
return typeof value === "object" && value !== null ? value : void 0;
|
|
360
|
-
}
|
|
361
|
-
function textOf$7(value, max) {
|
|
362
|
-
if (typeof value !== "string") return "";
|
|
363
|
-
const trimmed = value.trim();
|
|
364
|
-
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
365
|
-
}
|
|
366
|
-
function atOf(value) {
|
|
367
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
368
|
-
if (value < 0) return null;
|
|
369
|
-
return Math.trunc(value);
|
|
370
|
-
}
|
|
371
|
-
function errorOf(raw) {
|
|
372
|
-
const record = asRecord$8(raw);
|
|
373
|
-
if (record === void 0) return void 0;
|
|
374
|
-
const code = textOf$7(record["code"], 120);
|
|
375
|
-
const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
|
|
376
|
-
if (code === "" && message === "") return void 0;
|
|
377
|
-
return {
|
|
378
|
-
code: code === "" ? "unknown" : code,
|
|
379
|
-
message
|
|
380
|
-
};
|
|
152
|
+
function insideWorkshop$1(root, target) {
|
|
153
|
+
if (target === root) return true;
|
|
154
|
+
const base = root.endsWith("/") ? root : `${root}/`;
|
|
155
|
+
return target.startsWith(base);
|
|
381
156
|
}
|
|
382
157
|
/**
|
|
383
|
-
*
|
|
384
|
-
* @param
|
|
385
|
-
* @
|
|
386
|
-
*
|
|
387
|
-
*/
|
|
388
|
-
function parseCardsReport(payload) {
|
|
389
|
-
const record = asRecord$8(payload);
|
|
390
|
-
if (record === void 0) return {
|
|
391
|
-
ok: false,
|
|
392
|
-
message: "请求体要是一个对象"
|
|
393
|
-
};
|
|
394
|
-
const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
395
|
-
if (workspace === "") return {
|
|
396
|
-
ok: false,
|
|
397
|
-
message: "workspace 不能是空的 —— 面板按工作区存"
|
|
398
|
-
};
|
|
399
|
-
const raw = record["cards"];
|
|
400
|
-
if (!Array.isArray(raw)) return {
|
|
401
|
-
ok: false,
|
|
402
|
-
message: "cards 要是一个数组"
|
|
403
|
-
};
|
|
404
|
-
const cards = [];
|
|
405
|
-
for (const item of raw.slice(0, 32)) {
|
|
406
|
-
const card = asRecord$8(item);
|
|
407
|
-
if (card === void 0) continue;
|
|
408
|
-
const unit = textOf$7(card["unit"], 200);
|
|
409
|
-
const route = textOf$7(card["route"], 400);
|
|
410
|
-
const phase = textOf$7(card["phase"], 20);
|
|
411
|
-
const at = atOf(card["at"]);
|
|
412
|
-
if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
|
|
413
|
-
const error = errorOf(card["error"]);
|
|
414
|
-
cards.push({
|
|
415
|
-
unit,
|
|
416
|
-
route,
|
|
417
|
-
phase,
|
|
418
|
-
at,
|
|
419
|
-
...error === void 0 ? {} : { error }
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
if (cards.length === 0) return {
|
|
423
|
-
ok: false,
|
|
424
|
-
message: "cards 里没有一条能认的(unit / route / phase / at 都要有)"
|
|
425
|
-
};
|
|
426
|
-
return {
|
|
427
|
-
ok: true,
|
|
428
|
-
value: {
|
|
429
|
-
workspace,
|
|
430
|
-
cards
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* 收一次卡片崩溃上报。
|
|
436
|
-
* @param payload - `POST` 的请求体。
|
|
437
|
-
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
438
|
-
*/
|
|
439
|
-
function parseCrashReport(payload) {
|
|
440
|
-
const record = asRecord$8(payload);
|
|
441
|
-
if (record === void 0) return {
|
|
442
|
-
ok: false,
|
|
443
|
-
message: "请求体要是一个对象"
|
|
444
|
-
};
|
|
445
|
-
const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
|
|
446
|
-
const unit = textOf$7(record["unit"], 200);
|
|
447
|
-
const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
|
|
448
|
-
if (workspace === "") return {
|
|
449
|
-
ok: false,
|
|
450
|
-
message: "workspace 不能是空的"
|
|
451
|
-
};
|
|
452
|
-
if (unit === "") return {
|
|
453
|
-
ok: false,
|
|
454
|
-
message: "unit 不能是空的"
|
|
455
|
-
};
|
|
456
|
-
if (reason === "") return {
|
|
457
|
-
ok: false,
|
|
458
|
-
message: "reason 不能是空的(没有原因的崩溃没法查)"
|
|
459
|
-
};
|
|
460
|
-
return {
|
|
461
|
-
ok: true,
|
|
462
|
-
value: {
|
|
463
|
-
workspace,
|
|
464
|
-
unit,
|
|
465
|
-
reason,
|
|
466
|
-
at: atOf(record["at"]) ?? Date.now()
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
472
|
-
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
158
|
+
* 把探针读数归成左栏那句人话的档位。
|
|
159
|
+
* @param probe - 探针读数。
|
|
160
|
+
* @param workshop - 当前工作区根路径;`null` 一律 `unknown`。
|
|
161
|
+
* @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
|
|
473
162
|
*/
|
|
474
|
-
function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (oldest.done === true) return;
|
|
480
|
-
entry.delete(oldest.value);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
function workspaceEntryOf(workspace) {
|
|
484
|
-
const existing = workspaces.get(workspace);
|
|
485
|
-
if (existing !== void 0) return existing;
|
|
486
|
-
const created = {
|
|
487
|
-
at: 0,
|
|
488
|
-
units: /* @__PURE__ */ new Map()
|
|
489
|
-
};
|
|
490
|
-
workspaces.set(workspace, created);
|
|
491
|
-
trim(workspaces, 64);
|
|
492
|
-
return created;
|
|
493
|
-
}
|
|
494
|
-
function unitEntryOf(entry, unit) {
|
|
495
|
-
const existing = entry.units.get(unit);
|
|
496
|
-
if (existing !== void 0) return existing;
|
|
497
|
-
const created = {
|
|
498
|
-
at: 0,
|
|
499
|
-
cards: /* @__PURE__ */ new Map()
|
|
500
|
-
};
|
|
501
|
-
entry.units.set(unit, created);
|
|
502
|
-
return created;
|
|
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";
|
|
503
168
|
}
|
|
504
|
-
return
|
|
505
|
-
|
|
506
|
-
const entry = workspaceEntryOf(report.workspace);
|
|
507
|
-
for (const card of report.cards) {
|
|
508
|
-
const unit = unitEntryOf(entry, card.unit);
|
|
509
|
-
unit.cards.delete(card.route);
|
|
510
|
-
unit.cards.set(card.route, card);
|
|
511
|
-
trim(unit.cards, 12);
|
|
512
|
-
if (card.at > unit.at) unit.at = card.at;
|
|
513
|
-
if (card.at > entry.at) entry.at = card.at;
|
|
514
|
-
}
|
|
515
|
-
},
|
|
516
|
-
crash(report) {
|
|
517
|
-
const entry = workspaceEntryOf(report.workspace);
|
|
518
|
-
const unit = unitEntryOf(entry, report.unit);
|
|
519
|
-
const previous = unit.crash;
|
|
520
|
-
const counted = previous !== void 0 && previous.reason === report.reason && report.at >= previous.at && report.at - previous.at <= 6e5;
|
|
521
|
-
unit.crash = {
|
|
522
|
-
unit: report.unit,
|
|
523
|
-
reason: report.reason,
|
|
524
|
-
at: report.at,
|
|
525
|
-
count: counted ? previous.count + 1 : 1
|
|
526
|
-
};
|
|
527
|
-
if (report.at > unit.at) unit.at = report.at;
|
|
528
|
-
if (report.at > entry.at) entry.at = report.at;
|
|
529
|
-
},
|
|
530
|
-
snapshot(workspace) {
|
|
531
|
-
if (workspace === null) return {
|
|
532
|
-
workspace: null,
|
|
533
|
-
at: 0,
|
|
534
|
-
units: []
|
|
535
|
-
};
|
|
536
|
-
const entry = workspaces.get(workspace);
|
|
537
|
-
if (entry === void 0) return {
|
|
538
|
-
workspace,
|
|
539
|
-
at: 0,
|
|
540
|
-
units: []
|
|
541
|
-
};
|
|
542
|
-
const units = [...entry.units.entries()].map(([unit, one]) => {
|
|
543
|
-
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
544
|
-
const okAt = cards.reduce((best, card) => card.phase === "ready" && (best === null || card.at > best) ? card.at : best, null);
|
|
545
|
-
return {
|
|
546
|
-
unit,
|
|
547
|
-
at: one.at,
|
|
548
|
-
okAt,
|
|
549
|
-
cards,
|
|
550
|
-
...one.crash === void 0 ? {} : { crash: one.crash }
|
|
551
|
-
};
|
|
552
|
-
});
|
|
553
|
-
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
554
|
-
return {
|
|
555
|
-
workspace,
|
|
556
|
-
at: entry.at,
|
|
557
|
-
units
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
};
|
|
169
|
+
if (probe.where !== "" && !insideWorkshop$1(workshop, probe.where)) return "off-workspace";
|
|
170
|
+
return "panel-closed";
|
|
561
171
|
}
|
|
562
172
|
//#endregion
|
|
563
|
-
//#region ../shared/dist/
|
|
564
|
-
/**
|
|
565
|
-
* 判一个路径落不落在这个工作区里(按路径前缀比,不是字符串包含)。
|
|
566
|
-
* @param root - 工作区根路径。
|
|
567
|
-
* @param target - 要判的路径。
|
|
568
|
-
* @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
569
|
-
*/
|
|
570
|
-
function insideWorkshop$1(root, target) {
|
|
571
|
-
if (target === root) return true;
|
|
572
|
-
const base = root.endsWith("/") ? root : `${root}/`;
|
|
573
|
-
return target.startsWith(base);
|
|
574
|
-
}
|
|
173
|
+
//#region ../shared/dist/host/board-fact.js
|
|
575
174
|
/**
|
|
576
175
|
* 把一次探针读数归成椅面归属。
|
|
577
176
|
* @param probe - 探针读数。
|
|
@@ -584,52 +183,214 @@ function boardFactOf(probe, workshop) {
|
|
|
584
183
|
if (probe.board === "" || workshop === null) return "unknown";
|
|
585
184
|
return insideWorkshop$1(workshop, probe.board) ? "workshop" : "other";
|
|
586
185
|
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region ../shared/dist/host/shell-text.js
|
|
587
188
|
/**
|
|
588
|
-
*
|
|
589
|
-
* @param
|
|
590
|
-
* @
|
|
591
|
-
* @returns 椅面画着 → 本板 `ok` / 别板 `other-board`;没画着 → 页面已走 `off-workspace`,否则 `panel-closed`。
|
|
189
|
+
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
190
|
+
* @param value - 任意值;不是字符串的先 `String()`。
|
|
191
|
+
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
592
192
|
*/
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
if (probe.bar && probe.open && probe.drawn) {
|
|
596
|
-
if (probe.board === "") return "unknown";
|
|
597
|
-
return insideWorkshop$1(workshop, probe.board) ? "ok" : "other-board";
|
|
598
|
-
}
|
|
599
|
-
if (probe.where !== "" && !insideWorkshop$1(workshop, probe.where)) return "off-workspace";
|
|
600
|
-
return "panel-closed";
|
|
193
|
+
function quoteShellArg(value) {
|
|
194
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
601
195
|
}
|
|
602
|
-
//#endregion
|
|
603
|
-
//#region ../shared/dist/lib/diy-debug.js
|
|
604
196
|
/**
|
|
605
|
-
*
|
|
606
|
-
* @param
|
|
607
|
-
* @
|
|
197
|
+
* 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
|
|
198
|
+
* @param text - 原文;`null` / `undefined` 当空串。
|
|
199
|
+
* @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
|
|
200
|
+
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
608
201
|
*/
|
|
609
|
-
function
|
|
610
|
-
return
|
|
202
|
+
function tailLines(text, lines = 6) {
|
|
203
|
+
return String(text ?? "").trim().split("\n").slice(-lines).join("\n");
|
|
611
204
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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)];
|
|
615
210
|
}
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
211
|
+
function backgroundLine(command, argv) {
|
|
212
|
+
return [
|
|
213
|
+
...backgroundPrefix(),
|
|
214
|
+
quoteShellArg(command),
|
|
215
|
+
...argv.map(quoteShellArg)
|
|
216
|
+
].join(" ");
|
|
620
217
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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();
|
|
232
|
+
return {
|
|
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
|
|
290
|
+
};
|
|
291
|
+
current = key;
|
|
292
|
+
continue;
|
|
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) }
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function helpUrlOf(envelope) {
|
|
309
|
+
return /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? "")?.[0];
|
|
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 = /[.,;:!?]+$/;
|
|
315
|
+
/**
|
|
316
|
+
* 把 kind:part/part 文本读回引用。
|
|
317
|
+
* @param text 待解析文本。
|
|
318
|
+
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
319
|
+
*/
|
|
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
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
334
|
+
* @param text 任意文本;null / undefined 当空串。
|
|
335
|
+
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
336
|
+
*/
|
|
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);
|
|
347
|
+
}
|
|
348
|
+
return refs;
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
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
|
|
365
|
+
/**
|
|
366
|
+
* 把工具入参收成人话动作。
|
|
367
|
+
* @param value - 任意入参值。
|
|
368
|
+
* @returns `'pause'` / `'resume'` / `'stop'` 原样;别的值(含 `undefined`、多余空白、大小写不同)给 `null`,由调用方拒。
|
|
369
|
+
*/
|
|
370
|
+
function parseDebugIntent(value) {
|
|
371
|
+
return value === "pause" || value === "resume" || value === "stop" ? value : null;
|
|
372
|
+
}
|
|
373
|
+
function alive$1(state, now) {
|
|
374
|
+
if (state === null) return null;
|
|
375
|
+
return now - state.at <= 144e5 ? state : null;
|
|
376
|
+
}
|
|
377
|
+
function ownerOf$1(state, sessionId, now) {
|
|
378
|
+
const held = alive$1(state, now);
|
|
379
|
+
if (held === null) return "none";
|
|
380
|
+
return held.sessionId === sessionId ? held : "not-owner";
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 造一份新的调试约定存储(进程内存,同一时刻只可能有一份有效约定)。
|
|
384
|
+
* @returns 存取口;动作成不成都在返回的 `DebugMove` 里,不抛异常。
|
|
385
|
+
*/
|
|
386
|
+
function createDebugStore() {
|
|
387
|
+
let held = null;
|
|
388
|
+
const live = (now) => alive$1(held, now);
|
|
389
|
+
return {
|
|
390
|
+
live,
|
|
391
|
+
begin(pact) {
|
|
392
|
+
const current = alive$1(held, pact.now);
|
|
393
|
+
if (current !== null) return {
|
|
633
394
|
ok: false,
|
|
634
395
|
code: current.sessionId === pact.sessionId ? "already-held" : "held-by-other"
|
|
635
396
|
};
|
|
@@ -714,6 +475,19 @@ function createDebugStore() {
|
|
|
714
475
|
function parseRecordIntent(raw) {
|
|
715
476
|
return raw === "peek" || raw === "halt" || raw === "stop" ? raw : null;
|
|
716
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
|
+
}
|
|
717
491
|
function alive(state, now) {
|
|
718
492
|
if (state === null) return null;
|
|
719
493
|
return now - state.at <= 9e5 ? state : null;
|
|
@@ -730,6 +504,26 @@ function ownerOf(state, sessionId, now) {
|
|
|
730
504
|
function createRecordStore() {
|
|
731
505
|
let held = null;
|
|
732
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
|
+
};
|
|
733
527
|
return {
|
|
734
528
|
live,
|
|
735
529
|
begin(pact) {
|
|
@@ -752,6 +546,11 @@ function createRecordStore() {
|
|
|
752
546
|
startedAt: pact.now,
|
|
753
547
|
t0: null,
|
|
754
548
|
t1: null,
|
|
549
|
+
shots: [],
|
|
550
|
+
shotsSeen: 0,
|
|
551
|
+
shotTries: null,
|
|
552
|
+
beats: 0,
|
|
553
|
+
beatMs: 0,
|
|
755
554
|
at: pact.now
|
|
756
555
|
};
|
|
757
556
|
held = state;
|
|
@@ -808,6 +607,50 @@ function createRecordStore() {
|
|
|
808
607
|
state
|
|
809
608
|
};
|
|
810
609
|
},
|
|
610
|
+
shotTaken(sessionId, shot, now) {
|
|
611
|
+
const found = ownerOf(held, sessionId, now);
|
|
612
|
+
if (typeof found === "string") return {
|
|
613
|
+
ok: false,
|
|
614
|
+
code: found
|
|
615
|
+
};
|
|
616
|
+
const state = withShot(found, shot, now, void 0);
|
|
617
|
+
held = state;
|
|
618
|
+
return {
|
|
619
|
+
ok: true,
|
|
620
|
+
state
|
|
621
|
+
};
|
|
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
|
+
},
|
|
811
654
|
stop(sessionId, now) {
|
|
812
655
|
const found = ownerOf(held, sessionId, now);
|
|
813
656
|
if (typeof found === "string") return {
|
|
@@ -823,173 +666,276 @@ function createRecordStore() {
|
|
|
823
666
|
};
|
|
824
667
|
}
|
|
825
668
|
//#endregion
|
|
826
|
-
//#region ../shared/dist/host/
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return [
|
|
833
|
-
...backgroundPrefix(),
|
|
834
|
-
quoteShellArg(command),
|
|
835
|
-
...argv.map(quoteShellArg)
|
|
836
|
-
].join(" ");
|
|
837
|
-
}
|
|
838
|
-
//#endregion
|
|
839
|
-
//#region ../shared/dist/host/shell.js
|
|
840
|
-
var DEFAULT_MAX_BYTES = 8388608;
|
|
841
|
-
async function runShell(shell, command, options = {}) {
|
|
842
|
-
const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
|
|
843
|
-
const spec = shell.resolve({
|
|
844
|
-
command,
|
|
845
|
-
...workdir === void 0 ? {} : { workdir },
|
|
846
|
-
timeoutMs,
|
|
847
|
-
onExpiry: "kill",
|
|
848
|
-
stdoutMaxBytes: DEFAULT_MAX_BYTES,
|
|
849
|
-
...signal === void 0 ? {} : { signal }
|
|
850
|
-
});
|
|
851
|
-
const result = await (await shell.execute(spec)).result();
|
|
669
|
+
//#region ../shared/dist/host/facts.js
|
|
670
|
+
function createFactLog(options = {}) {
|
|
671
|
+
const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
672
|
+
let rev = 0;
|
|
673
|
+
const last = /* @__PURE__ */ new Map();
|
|
674
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
852
675
|
return {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
676
|
+
epoch: () => epoch,
|
|
677
|
+
revision: () => rev,
|
|
678
|
+
bump(facts) {
|
|
679
|
+
rev += 1;
|
|
680
|
+
const named = [...new Set(facts.filter((name) => name !== ""))];
|
|
681
|
+
for (const name of named) last.set(name, rev);
|
|
682
|
+
const change = {
|
|
683
|
+
rev,
|
|
684
|
+
facts: named
|
|
685
|
+
};
|
|
686
|
+
for (const listener of [...listeners]) listener(change);
|
|
687
|
+
return change;
|
|
688
|
+
},
|
|
689
|
+
since(seen) {
|
|
690
|
+
const facts = [...last.entries()].filter(([, at]) => at > seen).map(([name]) => name);
|
|
691
|
+
return {
|
|
692
|
+
epoch,
|
|
693
|
+
rev,
|
|
694
|
+
facts
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
subscribe(listener) {
|
|
698
|
+
listeners.add(listener);
|
|
699
|
+
return () => {
|
|
700
|
+
listeners.delete(listener);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
859
703
|
};
|
|
860
704
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
function unquote(raw) {
|
|
869
|
-
const value = raw.trim();
|
|
870
|
-
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
|
871
|
-
return value;
|
|
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;
|
|
872
712
|
}
|
|
873
|
-
function
|
|
874
|
-
|
|
875
|
-
return
|
|
713
|
+
function wroteNow(result) {
|
|
714
|
+
const wrote = (result?.value)?.wrote;
|
|
715
|
+
return typeof wrote === "boolean" ? wrote : void 0;
|
|
876
716
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
892
808
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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()
|
|
899
844
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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);
|
|
913
858
|
}
|
|
914
|
-
scalars[key] = unquote(raw);
|
|
915
|
-
current = key;
|
|
916
859
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
function workbenchRoot() {
|
|
939
|
-
return `${dshHome()}/workbench`;
|
|
940
|
-
}
|
|
941
|
-
function workspacesRoot() {
|
|
942
|
-
return `${workbenchRoot()}/workspaces`;
|
|
943
|
-
}
|
|
944
|
-
//#endregion
|
|
945
|
-
//#region ../shared/dist/host/facts.js
|
|
946
|
-
function createFactLog(options = {}) {
|
|
947
|
-
const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
948
|
-
let rev = 0;
|
|
949
|
-
const last = /* @__PURE__ */ new Map();
|
|
950
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
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
|
+
}
|
|
951
881
|
return {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
for (const listener of [...listeners]) listener(change);
|
|
963
|
-
return change;
|
|
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
|
+
}
|
|
964
892
|
},
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
|
971
903
|
};
|
|
904
|
+
if (report.at > unit.at) unit.at = report.at;
|
|
905
|
+
if (report.at > entry.at) entry.at = report.at;
|
|
972
906
|
},
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
977
935
|
};
|
|
978
936
|
}
|
|
979
937
|
};
|
|
980
938
|
}
|
|
981
|
-
function toolUnits(manifests) {
|
|
982
|
-
const map = /* @__PURE__ */ new Map();
|
|
983
|
-
for (const manifest of manifests) for (const tool of manifest.tools ?? []) map.set(tool.name, {
|
|
984
|
-
unit: manifest.id,
|
|
985
|
-
writes: tool.writes
|
|
986
|
-
});
|
|
987
|
-
return map;
|
|
988
|
-
}
|
|
989
|
-
function wroteNow(result) {
|
|
990
|
-
const wrote = (result?.value)?.wrote;
|
|
991
|
-
return typeof wrote === "boolean" ? wrote : void 0;
|
|
992
|
-
}
|
|
993
939
|
//#endregion
|
|
994
940
|
//#region ../shared/dist/host/provision.js
|
|
995
941
|
var PROVISION_ACTIONS = [
|
|
@@ -1488,7 +1434,7 @@ function changesHandler(log) {
|
|
|
1488
1434
|
}
|
|
1489
1435
|
function preflightHandler(preflight) {
|
|
1490
1436
|
return (search) => {
|
|
1491
|
-
preflight.run({
|
|
1437
|
+
preflight.run({ hard: flag(search, "hard") });
|
|
1492
1438
|
return {
|
|
1493
1439
|
ok: true,
|
|
1494
1440
|
...preflight.snapshot()
|
|
@@ -1654,6 +1600,9 @@ function createLongTaskRegistry(deps) {
|
|
|
1654
1600
|
id: snapshot.id
|
|
1655
1601
|
}));
|
|
1656
1602
|
},
|
|
1603
|
+
live() {
|
|
1604
|
+
return [...runs.values()].filter((snapshot) => LIVE.has(snapshot.status));
|
|
1605
|
+
},
|
|
1657
1606
|
start(options) {
|
|
1658
1607
|
const running = this.active(options.workspace, options.target);
|
|
1659
1608
|
if (running !== null) return running;
|
|
@@ -2084,62 +2033,210 @@ async function approvalFor(call, context = {}) {
|
|
|
2084
2033
|
reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf$1(error)}`
|
|
2085
2034
|
};
|
|
2086
2035
|
}
|
|
2087
|
-
if (verdict?.kind === "read" || verdict?.kind === "allow") return void 0;
|
|
2088
|
-
if (verdict?.kind !== "ask") return {
|
|
2089
|
-
kind: "deny",
|
|
2090
|
-
reason: `${call.name ?? "这把工具"} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去`
|
|
2091
|
-
};
|
|
2092
|
-
let facts;
|
|
2093
|
-
try {
|
|
2094
|
-
facts = await context.detailOf?.(call);
|
|
2095
|
-
} catch (error) {
|
|
2096
|
-
return {
|
|
2097
|
-
kind: "deny",
|
|
2098
|
-
reason: `拿不到"这次会发生什么",已经停下:${messageOf$1(error)}`
|
|
2099
|
-
};
|
|
2036
|
+
if (verdict?.kind === "read" || verdict?.kind === "allow") return void 0;
|
|
2037
|
+
if (verdict?.kind !== "ask") return {
|
|
2038
|
+
kind: "deny",
|
|
2039
|
+
reason: `${call.name ?? "这把工具"} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去`
|
|
2040
|
+
};
|
|
2041
|
+
let facts;
|
|
2042
|
+
try {
|
|
2043
|
+
facts = await context.detailOf?.(call);
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
return {
|
|
2046
|
+
kind: "deny",
|
|
2047
|
+
reason: `拿不到"这次会发生什么",已经停下:${messageOf$1(error)}`
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
if (facts !== void 0 && "blocked" in facts) return {
|
|
2051
|
+
kind: "deny",
|
|
2052
|
+
reason: facts.blocked
|
|
2053
|
+
};
|
|
2054
|
+
return {
|
|
2055
|
+
kind: "ask",
|
|
2056
|
+
reason: verdict.reason,
|
|
2057
|
+
displayReason: compose(facts, verdict, context.declaredNote === true ? approvalNoteOf(call) : void 0)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region ../shared/dist/host/tool-kit.js
|
|
2062
|
+
function sessionWorkspace(exec) {
|
|
2063
|
+
const cwd = exec?.agent?.session?.header?.cwd;
|
|
2064
|
+
return typeof cwd === "string" && cwd.trim() !== "" ? cwd.trim() : null;
|
|
2065
|
+
}
|
|
2066
|
+
function sessionIdOf(exec) {
|
|
2067
|
+
const id = exec?.agent?.session?.id;
|
|
2068
|
+
return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
|
|
2069
|
+
}
|
|
2070
|
+
function textOutput(text) {
|
|
2071
|
+
return [{
|
|
2072
|
+
type: "text",
|
|
2073
|
+
text
|
|
2074
|
+
}];
|
|
2075
|
+
}
|
|
2076
|
+
function textRender(_args, value) {
|
|
2077
|
+
const text = value?.text;
|
|
2078
|
+
return textOutput(typeof text === "string" ? text : JSON.stringify(value));
|
|
2079
|
+
}
|
|
2080
|
+
var TEXT_OUTPUT = {
|
|
2081
|
+
schema: {
|
|
2082
|
+
type: "object",
|
|
2083
|
+
properties: {},
|
|
2084
|
+
additionalProperties: true
|
|
2085
|
+
},
|
|
2086
|
+
render: textRender
|
|
2087
|
+
};
|
|
2088
|
+
function jsonText(value) {
|
|
2089
|
+
return JSON.stringify(value);
|
|
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
|
+
}
|
|
2100
2203
|
}
|
|
2101
|
-
if (
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
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;
|
|
2105
2218
|
return {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
+
}
|
|
2109
2237
|
};
|
|
2110
2238
|
}
|
|
2111
2239
|
//#endregion
|
|
2112
|
-
//#region ../shared/dist/host/tool-kit.js
|
|
2113
|
-
function sessionWorkspace(exec) {
|
|
2114
|
-
const cwd = exec?.agent?.session?.header?.cwd;
|
|
2115
|
-
return typeof cwd === "string" && cwd.trim() !== "" ? cwd.trim() : null;
|
|
2116
|
-
}
|
|
2117
|
-
function sessionIdOf(exec) {
|
|
2118
|
-
const id = exec?.agent?.session?.id;
|
|
2119
|
-
return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
|
|
2120
|
-
}
|
|
2121
|
-
function textOutput(text) {
|
|
2122
|
-
return [{
|
|
2123
|
-
type: "text",
|
|
2124
|
-
text
|
|
2125
|
-
}];
|
|
2126
|
-
}
|
|
2127
|
-
function textRender(_args, value) {
|
|
2128
|
-
const text = value?.text;
|
|
2129
|
-
return textOutput(typeof text === "string" ? text : JSON.stringify(value));
|
|
2130
|
-
}
|
|
2131
|
-
var TEXT_OUTPUT = {
|
|
2132
|
-
schema: {
|
|
2133
|
-
type: "object",
|
|
2134
|
-
properties: {},
|
|
2135
|
-
additionalProperties: true
|
|
2136
|
-
},
|
|
2137
|
-
render: textRender
|
|
2138
|
-
};
|
|
2139
|
-
function jsonText(value) {
|
|
2140
|
-
return JSON.stringify(value);
|
|
2141
|
-
}
|
|
2142
|
-
//#endregion
|
|
2143
2240
|
//#region ../shared/dist/host/value-handoff.js
|
|
2144
2241
|
function handoffId(entry) {
|
|
2145
2242
|
return `${String(entry.fact.length)}:${entry.fact}${entry.key}`;
|
|
@@ -3923,6 +4020,18 @@ async function resolveAt(registry, dir) {
|
|
|
3923
4020
|
function looksLikeWorkshop(dir) {
|
|
3924
4021
|
return existsSync(join(dir, "packages")) || existsSync(join(dir, "pnpm-workspace.yaml"));
|
|
3925
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
|
+
}
|
|
3926
4035
|
async function workshopRootOf(ctx, workspace) {
|
|
3927
4036
|
const start = typeof workspace === "string" ? workspace.trim() : "";
|
|
3928
4037
|
if (start === "") return null;
|
|
@@ -4015,10 +4124,19 @@ function showOrReason(panels, unit, workspace) {
|
|
|
4015
4124
|
};
|
|
4016
4125
|
}
|
|
4017
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
|
+
*/
|
|
4018
4136
|
async function workshopOf(ctx, exec) {
|
|
4019
4137
|
const session = sessionWorkspace(exec);
|
|
4020
|
-
const root = await workshopRootOf(ctx, session);
|
|
4021
|
-
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 新建一个模块包`);
|
|
4022
4140
|
return {
|
|
4023
4141
|
packagesDir: root.packagesDir,
|
|
4024
4142
|
workspace: root.root,
|
|
@@ -4540,7 +4658,7 @@ async function stopNetworkExact(shell, file, timeoutMs = SWEEP_TIMEOUT_MS) {
|
|
|
4540
4658
|
`[ -n "$p" ] && kill "$p" 2>/dev/null`,
|
|
4541
4659
|
`[ -n "$r" ] && pkill -P "$r" 2>/dev/null`,
|
|
4542
4660
|
`[ -n "$r" ] && kill "$r" 2>/dev/null`,
|
|
4543
|
-
`if [ "$(cat ${pidFile} 2>/dev/null)" = "$p" ]; then rm -f ${
|
|
4661
|
+
`if [ "$(cat ${pidFile} 2>/dev/null)" = "$p" ]; then rm -f ${pidFile} ${reaperFile}; fi`,
|
|
4544
4662
|
"true"
|
|
4545
4663
|
].join("; "), { timeoutMs })).exitCode === 0;
|
|
4546
4664
|
}
|
|
@@ -6609,7 +6727,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6609
6727
|
const OFF = ${JSON.stringify(RECORD_OFF_KEY)};
|
|
6610
6728
|
const CAP = ${String(50)};
|
|
6611
6729
|
const BODY_MAX = ${String(16384)};
|
|
6612
|
-
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 });
|
|
6613
6731
|
const readKeep = () => {
|
|
6614
6732
|
try {
|
|
6615
6733
|
const raw = sessionStorage.getItem(KEEP);
|
|
@@ -6644,23 +6762,64 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6644
6762
|
const repaint = () => {
|
|
6645
6763
|
if (typeof state.__repaint === 'function') state.__repaint();
|
|
6646
6764
|
};
|
|
6647
|
-
const
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
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,
|
|
6652
6786
|
body: slice(body === undefined || body === null ? '' : body),
|
|
6653
|
-
status:
|
|
6654
|
-
ct:
|
|
6655
|
-
response:
|
|
6787
|
+
status: 0,
|
|
6788
|
+
ct: '',
|
|
6789
|
+
response: '',
|
|
6656
6790
|
at: Date.now(),
|
|
6657
|
-
}
|
|
6791
|
+
};
|
|
6792
|
+
state.calls.push(row);
|
|
6658
6793
|
if (state.calls.length > CAP) {
|
|
6659
6794
|
state.dropped = (typeof state.dropped === 'number' ? state.dropped : 0) + (state.calls.length - CAP);
|
|
6660
6795
|
state.calls.splice(0, state.calls.length - CAP);
|
|
6661
6796
|
}
|
|
6797
|
+
pending.push(row);
|
|
6662
6798
|
keep();
|
|
6663
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);
|
|
6664
6823
|
};
|
|
6665
6824
|
const fresh = typeof window[OFF] !== 'function';
|
|
6666
6825
|
if (fresh) {
|
|
@@ -6691,6 +6850,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6691
6850
|
const url = typeof asked === 'string' ? asked : asked !== null && typeof asked === 'object' ? asked.url : '';
|
|
6692
6851
|
const method = init.method !== undefined ? init.method : asked !== null && typeof asked === 'object' ? asked.method : 'GET';
|
|
6693
6852
|
const body = init.body === undefined ? '' : init.body;
|
|
6853
|
+
const row = begin(method, url, body);
|
|
6694
6854
|
const done = realFetch.apply(this, args);
|
|
6695
6855
|
try {
|
|
6696
6856
|
done.then((response) => {
|
|
@@ -6698,11 +6858,11 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6698
6858
|
const ct = response.headers.get('content-type') || '';
|
|
6699
6859
|
const clone = response.clone();
|
|
6700
6860
|
const target = response.url || url;
|
|
6701
|
-
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, ''));
|
|
6702
6862
|
} catch (error) {}
|
|
6703
6863
|
}).catch((error) => {
|
|
6704
6864
|
try {
|
|
6705
|
-
|
|
6865
|
+
finish(row, method, url, body, 0, 'fetch-error', String(error !== null && typeof error === 'object' && error.message !== undefined ? error.message : error));
|
|
6706
6866
|
} catch (inner) {}
|
|
6707
6867
|
});
|
|
6708
6868
|
} catch (error) {}
|
|
@@ -6720,9 +6880,10 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6720
6880
|
xhr.removeEventListener('loadend', xhr.__dshLoadEnd);
|
|
6721
6881
|
} catch (error) {}
|
|
6722
6882
|
}
|
|
6883
|
+
const row = begin(xhr.__dshMethod, xhr.__dshUrl, body);
|
|
6723
6884
|
const onEnd = () => {
|
|
6724
6885
|
try {
|
|
6725
|
-
|
|
6886
|
+
finish(row, xhr.__dshMethod, xhr.__dshUrl, body, xhr.status, ctOf(xhr), textOf(xhr));
|
|
6726
6887
|
} catch (error) {}
|
|
6727
6888
|
};
|
|
6728
6889
|
xhr.__dshLoadEnd = onEnd;
|
|
@@ -6732,7 +6893,7 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6732
6893
|
if (hasBeacon) {
|
|
6733
6894
|
navigator.sendBeacon = function (url, data) {
|
|
6734
6895
|
try {
|
|
6735
|
-
|
|
6896
|
+
begin('POST', url, data === undefined || data === null ? '' : data);
|
|
6736
6897
|
} catch (error) {}
|
|
6737
6898
|
return realBeacon.apply(navigator, arguments);
|
|
6738
6899
|
};
|
|
@@ -6801,23 +6962,31 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6801
6962
|
const label = document.createElement('span');
|
|
6802
6963
|
label.setAttribute('style', 'flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums');
|
|
6803
6964
|
const start = document.createElement('button');
|
|
6965
|
+
const shot = document.createElement('button');
|
|
6804
6966
|
const end = document.createElement('button');
|
|
6805
6967
|
start.type = 'button';
|
|
6806
6968
|
start.setAttribute('title', '开始录制');
|
|
6807
6969
|
start.setAttribute('aria-label', '开始录制');
|
|
6808
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>截图';
|
|
6809
6975
|
end.type = 'button';
|
|
6810
6976
|
end.setAttribute('title', '结束录制');
|
|
6811
6977
|
end.setAttribute('aria-label', '结束录制');
|
|
6812
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>结束';
|
|
6813
|
-
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');
|
|
6814
6980
|
const paint = () => {
|
|
6815
6981
|
const phase = state.phase;
|
|
6816
|
-
|
|
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) + ' 张' : '');
|
|
6817
6984
|
dot.style.display = phase === 'recording' ? 'inline-block' : 'none';
|
|
6818
6985
|
start.disabled = phase !== 'armed';
|
|
6986
|
+
shot.disabled = phase !== 'recording';
|
|
6819
6987
|
end.disabled = phase !== 'recording';
|
|
6820
6988
|
start.setAttribute('style', look(phase === 'armed'));
|
|
6989
|
+
shot.setAttribute('style', look(phase === 'recording'));
|
|
6821
6990
|
end.setAttribute('style', look(phase === 'recording'));
|
|
6822
6991
|
};
|
|
6823
6992
|
state.__repaint = paint;
|
|
@@ -6869,12 +7038,22 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6869
7038
|
state.href = location.href;
|
|
6870
7039
|
state.calls = [];
|
|
6871
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');
|
|
6872
7050
|
keep();
|
|
6873
7051
|
paint();
|
|
6874
7052
|
};
|
|
6875
7053
|
end.onclick = () => {
|
|
6876
7054
|
state.phase = 'finished';
|
|
6877
7055
|
state.t1 = Date.now();
|
|
7056
|
+
beatOf('stop');
|
|
6878
7057
|
keep();
|
|
6879
7058
|
paint();
|
|
6880
7059
|
};
|
|
@@ -6882,15 +7061,17 @@ var RECORD_ARM_SCRIPT = `(() => {
|
|
|
6882
7061
|
bar.appendChild(grip);
|
|
6883
7062
|
bar.appendChild(dot);
|
|
6884
7063
|
bar.appendChild(label);
|
|
7064
|
+
bar.appendChild(shot);
|
|
6885
7065
|
bar.appendChild(start);
|
|
6886
7066
|
bar.appendChild(end);
|
|
6887
7067
|
if (typeof state.barX === 'number' && typeof state.barY === 'number') place(state.barX, state.barY);
|
|
6888
7068
|
paint();
|
|
6889
|
-
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 };
|
|
6890
7070
|
})()`;
|
|
6891
|
-
var RECORD_READ_SCRIPT = `(() => {
|
|
7071
|
+
var RECORD_READ_SCRIPT = `(async () => {
|
|
6892
7072
|
const KEY = ${JSON.stringify(RECORD_KEY)};
|
|
6893
7073
|
const KEEP = ${JSON.stringify(RECORD_KEEP_KEY)};
|
|
7074
|
+
const BEAT_WAIT = 3000;
|
|
6894
7075
|
let state = window[KEY] !== null && typeof window[KEY] === 'object' ? window[KEY] : null;
|
|
6895
7076
|
if (state === null) {
|
|
6896
7077
|
try {
|
|
@@ -6900,8 +7081,29 @@ var RECORD_READ_SCRIPT = `(() => {
|
|
|
6900
7081
|
state = null;
|
|
6901
7082
|
}
|
|
6902
7083
|
}
|
|
6903
|
-
if (state === null || typeof state !== 'object') return { ok: false, code: 'not-armed', phase: 'none', calls: [] };
|
|
6904
|
-
|
|
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 : [] };
|
|
6905
7107
|
})()`;
|
|
6906
7108
|
var RECORD_STOP_SCRIPT = `(() => {
|
|
6907
7109
|
const KEY = ${JSON.stringify(RECORD_KEY)};
|
|
@@ -7160,6 +7362,10 @@ function recordPageOf(payload) {
|
|
|
7160
7362
|
here: typeof raw["here"] === "string" ? raw["here"] : "",
|
|
7161
7363
|
dropped: typeof raw["dropped"] === "number" ? raw["dropped"] : 0,
|
|
7162
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"] : "",
|
|
7163
7369
|
calls
|
|
7164
7370
|
};
|
|
7165
7371
|
}
|
|
@@ -7240,21 +7446,43 @@ function sizeText(bytes) {
|
|
|
7240
7446
|
function rowTextOf(one) {
|
|
7241
7447
|
const mark = one.marked ? " ⭐" : " ";
|
|
7242
7448
|
const where = one.source === "capture" ? "捕获" : "钩子";
|
|
7243
|
-
|
|
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(" · ");
|
|
7244
7470
|
}
|
|
7245
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);
|
|
7246
7476
|
if (!input.armed) return `那一页上还没有录制条(${DIY_RECORD_PREPARE_TOOL} 起的那一场可能被导航冲掉了)—— 先 ${DIY_RECORD_PREPARE_TOOL},或让他回到那一页再 ${DIY_RECORD_READ_TOOL}。`;
|
|
7247
7477
|
if (input.phase === "armed") return "他还没按「开始录制」—— 等他按了再读。";
|
|
7248
|
-
if (input.phase === "recording") return `还在录(页面钩子上已经攒了 ${String(input.hookCaught)} 条)——
|
|
7249
|
-
const
|
|
7250
|
-
const
|
|
7251
|
-
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);
|
|
7252
7481
|
const restShown = input.rows.filter((one) => !one.marked).slice(-Math.max(0, CALLS_SHOWN - markShown.length));
|
|
7253
7482
|
const shown = [...markShown, ...restShown];
|
|
7254
|
-
const lines = [];
|
|
7255
|
-
if (input.rows.length > 0) lines.push(
|
|
7256
|
-
else lines.push(input.capturedTruncated ? `录完了:这一段(就解析到的那部分而言)一条请求都没抓到(页面钩子 0 条 · 解析到的会话捕获里落在这个时间段里 0 条)——
|
|
7257
|
-
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}`);
|
|
7258
7486
|
if (input.dropped > 0) lines.push(`(缓冲区满了,最早丢了 ${String(input.dropped)} 条)`);
|
|
7259
7487
|
if (input.captureRound === "fail") lines.push("(会话捕获最近一轮没跑成(opencli 那条报了错)—— 这一场可能只有页面钩子。)");
|
|
7260
7488
|
else if (input.captureRound === "") lines.push("(会话捕获还没回过一轮 —— 再等一拍再看,别急着下\"没抓到\"的结论。)");
|
|
@@ -7275,6 +7503,7 @@ function clip(text, maxBytes) {
|
|
|
7275
7503
|
function sizeLabel(size, exact) {
|
|
7276
7504
|
return exact ? `${String(size)} 字节` : `${String(size)} 字符(opencli 没给 body_full_size,按字符数算)`;
|
|
7277
7505
|
}
|
|
7506
|
+
/** 把字段证据表插进详情正文:接在出参那一块之后、`请求头` 那一行(或第一条附注)之前;没有锚点就接在末尾。空串原样回。 */
|
|
7278
7507
|
function renderRecordDetail(input) {
|
|
7279
7508
|
if (!input.found) return [`没找到这一条:${input.note}`, `可用的取法:② 还在这场录制里时用清单里的 key 或 index;③ 这一场已经收工的话,给 unit 让我去读 shots/rec-<unit>.jsonl.last;④ 都不行就让他打开 DevTools,把那次请求复制成 curl。`].join("\n");
|
|
7280
7509
|
const req = clip(input.request, input.maxBytes);
|
|
@@ -7329,7 +7558,10 @@ var POLL_MS = 1e3;
|
|
|
7329
7558
|
var CAPTURE_TAIL_LINES = 2e4;
|
|
7330
7559
|
var CAPTURE_POLL_S = 3;
|
|
7331
7560
|
var CAPTURE_ARM_S = 2;
|
|
7332
|
-
|
|
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)(\\?|$))`;
|
|
7333
7565
|
var RECORD_WATCH_MS = 3e3;
|
|
7334
7566
|
var RECORD_WATCH_TRIES = 5;
|
|
7335
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\")}});";
|
|
@@ -7354,6 +7586,48 @@ function captureFileOf(packagesDir, unit) {
|
|
|
7354
7586
|
function lastCaptureFileOf(file) {
|
|
7355
7587
|
return `${file}.last`;
|
|
7356
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
|
+
}
|
|
7357
7631
|
async function startRecordCapture(shell, command, session, file) {
|
|
7358
7632
|
const bin = command === "" ? "opencli" : command;
|
|
7359
7633
|
const arm = backgroundLine(bin, [
|
|
@@ -7393,6 +7667,30 @@ async function releaseLeftoversOf(input) {
|
|
|
7393
7667
|
captureFile: input.captureFile
|
|
7394
7668
|
});
|
|
7395
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
|
+
*/
|
|
7396
7694
|
async function watchFinishStepOf(input) {
|
|
7397
7695
|
const live = input.store.live(Date.now());
|
|
7398
7696
|
if (live === null) {
|
|
@@ -7406,11 +7704,15 @@ async function watchFinishStepOf(input) {
|
|
|
7406
7704
|
stop: true,
|
|
7407
7705
|
delivered: false
|
|
7408
7706
|
};
|
|
7409
|
-
if (live.phase === "finished")
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7707
|
+
if (live.phase === "finished") {
|
|
7708
|
+
await closeRecordTabQuiet(input);
|
|
7709
|
+
return {
|
|
7710
|
+
stop: true,
|
|
7711
|
+
delivered: false
|
|
7712
|
+
};
|
|
7713
|
+
}
|
|
7413
7714
|
let page;
|
|
7715
|
+
const started = Date.now();
|
|
7414
7716
|
try {
|
|
7415
7717
|
page = recordPageOf((await actBrowser(input.shell, input.command, BROWSER_RECORD_SESSION, ["eval", RECORD_READ_SCRIPT])).payload);
|
|
7416
7718
|
} catch {
|
|
@@ -7419,6 +7721,7 @@ async function watchFinishStepOf(input) {
|
|
|
7419
7721
|
delivered: false
|
|
7420
7722
|
};
|
|
7421
7723
|
}
|
|
7724
|
+
if (page.beat) input.store.beat(input.sessionId, Math.max(0, Date.now() - started), Date.now());
|
|
7422
7725
|
const held = input.store.live(Date.now());
|
|
7423
7726
|
if (held === null) {
|
|
7424
7727
|
await releaseLeftoversOf(input);
|
|
@@ -7431,22 +7734,31 @@ async function watchFinishStepOf(input) {
|
|
|
7431
7734
|
stop: true,
|
|
7432
7735
|
delivered: false
|
|
7433
7736
|
};
|
|
7434
|
-
if (held.phase === "finished")
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7737
|
+
if (held.phase === "finished") {
|
|
7738
|
+
await stopCaptureQuiet(input);
|
|
7739
|
+
await closeRecordTabQuiet(input);
|
|
7740
|
+
return {
|
|
7741
|
+
stop: true,
|
|
7742
|
+
delivered: false
|
|
7743
|
+
};
|
|
7744
|
+
}
|
|
7438
7745
|
if (!page.ok) return {
|
|
7439
7746
|
stop: false,
|
|
7440
7747
|
delivered: false
|
|
7441
7748
|
};
|
|
7749
|
+
const due = shotDueOf(held, page.shots);
|
|
7750
|
+
if (due !== null) await takeShotOf(input, held, due);
|
|
7442
7751
|
if (page.phase === "finished" && page.t1 !== null) {
|
|
7443
7752
|
const now = Date.now();
|
|
7444
|
-
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);
|
|
7445
7755
|
const caught = page.calls.length;
|
|
7446
7756
|
const summary = `工作台 DIY:他按了「结束录制」(${String(caught)} 条)`;
|
|
7447
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);
|
|
7448
7760
|
return {
|
|
7449
|
-
stop:
|
|
7761
|
+
stop: true,
|
|
7450
7762
|
delivered: true
|
|
7451
7763
|
};
|
|
7452
7764
|
}
|
|
@@ -7456,18 +7768,25 @@ async function watchFinishStepOf(input) {
|
|
|
7456
7768
|
delivered: false
|
|
7457
7769
|
};
|
|
7458
7770
|
}
|
|
7771
|
+
/**
|
|
7772
|
+
* 看护的一轮(就是 `watchFinishStepOf`,留着这个名字是给测试与调用点一个稳定的形)。
|
|
7773
|
+
* @param input - 看护那一拍的依赖。@returns 收不收工、有没有叫醒。
|
|
7774
|
+
*/
|
|
7775
|
+
async function watchRoundOf(input) {
|
|
7776
|
+
return watchFinishStepOf(input);
|
|
7777
|
+
}
|
|
7459
7778
|
function watchRecordFinish(input) {
|
|
7460
7779
|
let misses = 0;
|
|
7461
7780
|
const tick = () => {
|
|
7462
|
-
|
|
7781
|
+
watchRoundOf(input).then((out) => {
|
|
7463
7782
|
misses = 0;
|
|
7464
|
-
if (!out.stop) setTimeout(tick,
|
|
7783
|
+
if (!out.stop) setTimeout(tick, 0);
|
|
7465
7784
|
}).catch(() => {
|
|
7466
7785
|
misses += 1;
|
|
7467
7786
|
if (misses < RECORD_WATCH_TRIES) setTimeout(tick, RECORD_WATCH_MS);
|
|
7468
7787
|
});
|
|
7469
7788
|
};
|
|
7470
|
-
setTimeout(tick,
|
|
7789
|
+
setTimeout(tick, 0);
|
|
7471
7790
|
}
|
|
7472
7791
|
async function captureLinesOf(shell, file, timeoutMs = START_TIMEOUT_MS) {
|
|
7473
7792
|
const step = await runShell(shell, `wc -l < ${quoteShellArg(file)} 2>/dev/null || echo 0`, { timeoutMs });
|
|
@@ -7549,30 +7868,12 @@ function detailFactsOf(input) {
|
|
|
7549
7868
|
responseSizeExact: input.responseSizeExact,
|
|
7550
7869
|
ambiguous: input.ambiguous,
|
|
7551
7870
|
source: input.source,
|
|
7552
|
-
text: renderRecordDetail(
|
|
7553
|
-
found: true,
|
|
7554
|
-
where: input.where,
|
|
7555
|
-
method: input.method,
|
|
7556
|
-
url: input.url,
|
|
7557
|
-
status: input.status,
|
|
7558
|
-
ct: input.ct,
|
|
7559
|
-
at: input.at,
|
|
7560
|
-
request: input.request,
|
|
7561
|
-
response: input.response,
|
|
7562
|
-
headers: input.headers,
|
|
7563
|
-
requestFullSize: input.requestFullSize,
|
|
7564
|
-
responseFullSize: input.responseFullSize,
|
|
7565
|
-
requestSizeExact: input.requestSizeExact,
|
|
7566
|
-
responseSizeExact: input.responseSizeExact,
|
|
7567
|
-
ambiguous: input.ambiguous,
|
|
7568
|
-
source: input.source,
|
|
7569
|
-
maxBytes: input.maxBytes,
|
|
7570
|
-
note: ""
|
|
7571
|
-
})
|
|
7871
|
+
text: renderRecordDetail(input)
|
|
7572
7872
|
});
|
|
7573
7873
|
}
|
|
7574
7874
|
function detailOfEntry(entry, maxBytes, ambiguous = 1) {
|
|
7575
7875
|
return detailFactsOf({
|
|
7876
|
+
found: true,
|
|
7576
7877
|
where: DETAIL_WHERE_CAPTURE,
|
|
7577
7878
|
key: entry.key,
|
|
7578
7879
|
method: entry.method,
|
|
@@ -7589,11 +7890,13 @@ function detailOfEntry(entry, maxBytes, ambiguous = 1) {
|
|
|
7589
7890
|
responseSizeExact: entry.responseSizeExact,
|
|
7590
7891
|
ambiguous,
|
|
7591
7892
|
source: "capture",
|
|
7592
|
-
maxBytes
|
|
7893
|
+
maxBytes,
|
|
7894
|
+
note: ""
|
|
7593
7895
|
});
|
|
7594
7896
|
}
|
|
7595
7897
|
function detailOfCall(call, maxBytes) {
|
|
7596
7898
|
return detailFactsOf({
|
|
7899
|
+
found: true,
|
|
7597
7900
|
where: DETAIL_WHERE_HOOK,
|
|
7598
7901
|
key: call.key,
|
|
7599
7902
|
method: call.method,
|
|
@@ -7610,31 +7913,45 @@ function detailOfCall(call, maxBytes) {
|
|
|
7610
7913
|
responseSizeExact: false,
|
|
7611
7914
|
ambiguous: 0,
|
|
7612
7915
|
source: "hook",
|
|
7613
|
-
maxBytes
|
|
7916
|
+
maxBytes,
|
|
7917
|
+
note: ""
|
|
7614
7918
|
});
|
|
7615
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
|
+
}
|
|
7616
7924
|
function tabsOf(raw) {
|
|
7617
7925
|
const start = raw.indexOf("[");
|
|
7618
7926
|
const end = raw.lastIndexOf("]");
|
|
7619
|
-
if (start === -1 || end <= start) return
|
|
7927
|
+
if (start === -1 || end <= start) return null;
|
|
7620
7928
|
try {
|
|
7621
7929
|
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
7622
|
-
if (!Array.isArray(parsed)) return
|
|
7930
|
+
if (!Array.isArray(parsed)) return null;
|
|
7623
7931
|
return parsed.filter((one) => typeof one === "object" && one !== null);
|
|
7624
7932
|
} catch {
|
|
7625
|
-
return
|
|
7933
|
+
return null;
|
|
7626
7934
|
}
|
|
7627
7935
|
}
|
|
7628
7936
|
async function closeRecordTab(shell, command, session, url) {
|
|
7629
7937
|
const pages = tabsOf((await actBrowser(shell, command, session, ["tab", "list"])).stdout);
|
|
7630
|
-
|
|
7938
|
+
if (pages !== null && pages.length === 0) return true;
|
|
7939
|
+
const pick = pages?.find((one) => typeof one["url"] === "string" && one["url"] === url) ?? pages?.[0];
|
|
7631
7940
|
const page = pick === void 0 ? "" : typeof pick["page"] === "string" ? pick["page"] : "";
|
|
7632
7941
|
if (page !== "") {
|
|
7633
7942
|
if ((await actBrowser(shell, command, session, [
|
|
7634
7943
|
"tab",
|
|
7635
7944
|
"close",
|
|
7636
7945
|
page
|
|
7637
|
-
])).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
|
+
}
|
|
7638
7955
|
}
|
|
7639
7956
|
return (await actBrowser(shell, command, session, ["close"])).ok;
|
|
7640
7957
|
}
|
|
@@ -7656,6 +7973,81 @@ async function releaseQuietly(input) {
|
|
|
7656
7973
|
}
|
|
7657
7974
|
}
|
|
7658
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
|
|
7659
8051
|
//#region src/modules/panels/model/diy-record-tools.ts
|
|
7660
8052
|
function registerRecordTools(ctx, store) {
|
|
7661
8053
|
const tools = ctx.get("tools");
|
|
@@ -7665,7 +8057,7 @@ function registerRecordTools(ctx, store) {
|
|
|
7665
8057
|
}
|
|
7666
8058
|
const prepareTool = {
|
|
7667
8059
|
name: DIY_RECORD_PREPARE_TOOL,
|
|
7668
|
-
description: `开一扇浏览器窗口并顶到最前,让他自己录一次真实的写(开窗要点头一次);他按开始/结束后用 ${DIY_RECORD_READ_TOOL} 取那一段、${DIY_RECORD_STOP_TOOL}
|
|
8060
|
+
description: `开一扇浏览器窗口并顶到最前,让他自己录一次真实的写(开窗要点头一次);他按开始/结束后用 ${DIY_RECORD_READ_TOOL} 取那一段、${DIY_RECORD_STOP_TOOL} 收工。他按浮条上的「截图」不用另外点头(同一场同一屏,屏幕在开窗那一拍已经借过)—— 那张图与提交那一刻的快照一起进 ${DIY_RECORD_DETAIL_TOOL} 的字段证据表。`,
|
|
7669
8061
|
parameters: {
|
|
7670
8062
|
type: "object",
|
|
7671
8063
|
properties: {
|
|
@@ -7744,23 +8136,14 @@ function registerRecordTools(ctx, store) {
|
|
|
7744
8136
|
now: Date.now()
|
|
7745
8137
|
}), held);
|
|
7746
8138
|
try {
|
|
7747
|
-
const opened = await
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
"new",
|
|
8139
|
+
const opened = await openRecordPage({
|
|
8140
|
+
shell,
|
|
8141
|
+
command,
|
|
8142
|
+
session: BROWSER_RECORD_SESSION,
|
|
7752
8143
|
url,
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
if (!opened.ok) throw new Error(`开不了录制窗:${opened.stderr || opened.stdout || "opencli 报了个错"}。先让用户把工作台那一页切到前台(第一次要这样),或跑 opencli doctor 看扩展连没连上。`);
|
|
7757
|
-
await runBrowserStep(shell, command, [
|
|
7758
|
-
"browser",
|
|
7759
|
-
BROWSER_RECORD_SESSION,
|
|
7760
|
-
"wait",
|
|
7761
|
-
"time",
|
|
7762
|
-
String(SETTLE_MS / 1e3)
|
|
7763
|
-
], OPEN_TIMEOUT_MS);
|
|
8144
|
+
captureFile
|
|
8145
|
+
});
|
|
8146
|
+
if (!opened.ok) throw new Error(opened.why);
|
|
7764
8147
|
const armed = await actBrowser(shell, command, BROWSER_RECORD_SESSION, ["eval", RECORD_ARM_SCRIPT]);
|
|
7765
8148
|
const page = recordPageOf(armed.payload);
|
|
7766
8149
|
if (!armed.ok || !page.ok) throw new Error(`浮条没装上:${armed.stderr || armed.stdout || "那一页还在加载"} —— 等他那一页站稳了再试。`);
|
|
@@ -7838,32 +8221,39 @@ function registerRecordTools(ctx, store) {
|
|
|
7838
8221
|
if (store.live(Date.now())?.phase === "recording") store.finished(sessionId, page.t1, Date.now());
|
|
7839
8222
|
}
|
|
7840
8223
|
};
|
|
7841
|
-
|
|
7842
|
-
let
|
|
7843
|
-
let page = recordPageOf(
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
await
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
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;
|
|
7850
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
|
+
}
|
|
7851
8240
|
}
|
|
7852
|
-
const timedOut = budget > 0 && page.phase !== "finished";
|
|
8241
|
+
const timedOut = !over && budget > 0 && page.phase !== "finished";
|
|
7853
8242
|
const waitedMs = opened === 0 ? 0 : Date.now() - opened;
|
|
7854
8243
|
if (timedOut) {
|
|
7855
8244
|
const waited = String(Math.round(budget / 1e3));
|
|
7856
8245
|
holdNotice(sessionId, noticeMessage(`录制还没结束(等了约 ${waited} 秒):他按了「结束」再 read 一次。`, `工作台 DIY:录制还没结束(等了约 ${waited} 秒)`));
|
|
7857
8246
|
}
|
|
7858
8247
|
const state = store.live(Date.now()) ?? held;
|
|
8248
|
+
const ledger = state.shots;
|
|
7859
8249
|
const capture = await captureReadOf(shell, state.captureFile);
|
|
7860
8250
|
const bundle = bundleOf(capture, page, state);
|
|
7861
8251
|
const hookCaught = page.calls.length;
|
|
7862
8252
|
const nav = page.href !== "" && page.here !== "" && page.href !== page.here;
|
|
7863
8253
|
const noBody = bodyMissingOf(bundle.merged);
|
|
7864
|
-
const
|
|
7865
|
-
phase: page.phase,
|
|
7866
|
-
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,
|
|
7867
8257
|
hookCaught,
|
|
7868
8258
|
rows: bundle.rows,
|
|
7869
8259
|
merged: bundle.merged.length,
|
|
@@ -7876,11 +8266,17 @@ function registerRecordTools(ctx, store) {
|
|
|
7876
8266
|
dropped: page.dropped,
|
|
7877
8267
|
nav,
|
|
7878
8268
|
filter: filterTextOf(state.expect),
|
|
7879
|
-
captureRound: bundle.capture.round
|
|
7880
|
-
|
|
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");
|
|
7881
8277
|
return plainJson({
|
|
7882
8278
|
workshop: packagesDir,
|
|
7883
|
-
ok:
|
|
8279
|
+
ok: stepOk && page.ok,
|
|
7884
8280
|
phase: state.phase,
|
|
7885
8281
|
t0: state.t0,
|
|
7886
8282
|
t1: state.t1,
|
|
@@ -7894,6 +8290,9 @@ function registerRecordTools(ctx, store) {
|
|
|
7894
8290
|
hookCaught,
|
|
7895
8291
|
noBody,
|
|
7896
8292
|
nav,
|
|
8293
|
+
shots: ledger,
|
|
8294
|
+
beats: state.beats,
|
|
8295
|
+
beatMs: state.beatMs,
|
|
7897
8296
|
waitedMs,
|
|
7898
8297
|
timedOut,
|
|
7899
8298
|
text
|
|
@@ -7971,7 +8370,9 @@ function registerRecordTools(ctx, store) {
|
|
|
7971
8370
|
const candidates = pickAllOf(captureEntriesOf(await captureLinesWithKeyOf(shell, file, key)), key);
|
|
7972
8371
|
const fromCapture = pickOf(candidates, key, at);
|
|
7973
8372
|
if (fromCapture !== null) return detailOfEntry(fromCapture, maxBytes, candidates.length);
|
|
7974
|
-
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);
|
|
7975
8376
|
if (call !== null) return detailOfCall(call, maxBytes);
|
|
7976
8377
|
return plainJson({
|
|
7977
8378
|
workshop: packagesDir,
|
|
@@ -7996,7 +8397,7 @@ function registerRecordTools(ctx, store) {
|
|
|
7996
8397
|
ambiguous: 0,
|
|
7997
8398
|
source: "",
|
|
7998
8399
|
maxBytes,
|
|
7999
|
-
note: `「${key}」既不在这一场的快照原文(${file})里,也不在这一页的钩子那一份里`
|
|
8400
|
+
note: over ? `「${key}」不在这一场的快照原文(${file})里 —— 这一场已经结束 / 收工,那一页也收掉了,钩子那一份问不到` : `「${key}」既不在这一场的快照原文(${file})里,也不在这一页的钩子那一份里`
|
|
8000
8401
|
})
|
|
8001
8402
|
});
|
|
8002
8403
|
}
|
|
@@ -11391,6 +11792,37 @@ var DSH_PLUGINS_ROUTE = "dsh-plugins";
|
|
|
11391
11792
|
//#region src/shared/api/unit-manifests.ts
|
|
11392
11793
|
var UNIT_MANIFESTS_ROUTE = "unit-manifests";
|
|
11393
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
|
|
11394
11826
|
//#region src/shared/api/host/context-inject.ts
|
|
11395
11827
|
var CONTEXT_SOURCE_KIND = "workbench";
|
|
11396
11828
|
function collectContextRefs(messages) {
|
|
@@ -11529,8 +11961,6 @@ function createPreflight(options) {
|
|
|
11529
11961
|
at: Date.now()
|
|
11530
11962
|
};
|
|
11531
11963
|
let running = null;
|
|
11532
|
-
let latest = null;
|
|
11533
|
-
let rerun = false;
|
|
11534
11964
|
function set(next) {
|
|
11535
11965
|
current = {
|
|
11536
11966
|
...current,
|
|
@@ -11725,21 +12155,16 @@ function createPreflight(options) {
|
|
|
11725
12155
|
...settled.ok ? {} : { bridgeIssues: settled.issues }
|
|
11726
12156
|
});
|
|
11727
12157
|
}
|
|
12158
|
+
function freshReady() {
|
|
12159
|
+
return current.phase === "ready" && Date.now() - current.at < 3e4;
|
|
12160
|
+
}
|
|
11728
12161
|
async function start(runOptions = {}) {
|
|
11729
|
-
if (running !== null)
|
|
11730
|
-
|
|
11731
|
-
return running;
|
|
11732
|
-
}
|
|
11733
|
-
if (runOptions.force !== true && latest !== null) return latest;
|
|
12162
|
+
if (running !== null) return running;
|
|
12163
|
+
if (runOptions.hard !== true && freshReady()) return current;
|
|
11734
12164
|
const inflight = doRun();
|
|
11735
|
-
latest = inflight;
|
|
11736
12165
|
running = inflight;
|
|
11737
12166
|
await inflight.catch(() => void 0);
|
|
11738
12167
|
running = null;
|
|
11739
|
-
if (rerun) {
|
|
11740
|
-
rerun = false;
|
|
11741
|
-
return start({ force: true });
|
|
11742
|
-
}
|
|
11743
12168
|
return inflight;
|
|
11744
12169
|
}
|
|
11745
12170
|
return {
|
|
@@ -12007,10 +12432,31 @@ async function probeReleases(units, options) {
|
|
|
12007
12432
|
}
|
|
12008
12433
|
return found;
|
|
12009
12434
|
}
|
|
12435
|
+
var pendingProbes = /* @__PURE__ */ new Map();
|
|
12010
12436
|
async function probeUnits(options) {
|
|
12011
12437
|
const { shell, command = "opencli", units, force = false } = options;
|
|
12012
12438
|
const hits = units.map((unit) => unitCache.get(`${command}:${unit.unit}`));
|
|
12013
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;
|
|
12014
12460
|
const result = await runShell(shell, `${command} plugin list -f json`, { timeoutMs: OPENCLI_TIMEOUT_MS }).then((value) => ({
|
|
12015
12461
|
kind: "ok",
|
|
12016
12462
|
stdout: value.exitCode === 0 ? value.stdout : "",
|
|
@@ -13398,6 +13844,10 @@ function apply(ctx) {
|
|
|
13398
13844
|
const factsOfMap = /* @__PURE__ */ new Map();
|
|
13399
13845
|
const routesOfUnitMap = /* @__PURE__ */ new Map();
|
|
13400
13846
|
const ownersMap = /* @__PURE__ */ new Map();
|
|
13847
|
+
routes[LONG_TASKS_ROUTE] = () => ({
|
|
13848
|
+
ok: true,
|
|
13849
|
+
runs: liveLongTaskRows(longTaskRuns)
|
|
13850
|
+
});
|
|
13401
13851
|
const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
|
|
13402
13852
|
const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
|
|
13403
13853
|
const deniedNow = (context) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(context) ?? []))];
|
|
@@ -13447,7 +13897,7 @@ function apply(ctx) {
|
|
|
13447
13897
|
values,
|
|
13448
13898
|
longTasks: {
|
|
13449
13899
|
scope: process.env["DSH_HOME"] ?? "",
|
|
13450
|
-
bump: (names) => facts.bump(names),
|
|
13900
|
+
bump: (names) => facts.bump([...names, LONG_TASKS_ROUTE]),
|
|
13451
13901
|
notify: (change) => notifyLongTaskStop(ctx.get("agents"), host.unit, change)
|
|
13452
13902
|
},
|
|
13453
13903
|
longTaskRuns
|