e10-ebuilder-prototype 0.5.4 → 0.5.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/README.md +56 -14
- package/dist/archive.d.ts +1 -1
- package/dist/archive.js +3 -0
- package/dist/capture.js +27 -1
- package/dist/common.d.ts +2 -1
- package/dist/common.js +24 -4
- package/dist/form-behavior-runtime.d.mts +1 -0
- package/dist/form-behavior-runtime.mjs +681 -0
- package/dist/form-behavior.d.ts +11 -0
- package/dist/form-behavior.js +169 -0
- package/dist/form-context.d.ts +4 -0
- package/dist/form-context.js +7 -5
- package/dist/form-generation.d.ts +24 -0
- package/dist/form-generation.js +367 -0
- package/dist/form-guidance.d.ts +3 -0
- package/dist/form-guidance.js +28 -0
- package/dist/form-runtime.mjs +11 -2
- package/dist/host-ledger.d.ts +23 -0
- package/dist/host-ledger.js +183 -0
- package/dist/host-watch.d.ts +102 -0
- package/dist/host-watch.js +112 -0
- package/dist/html-handoff.d.ts +1 -0
- package/dist/html-handoff.js +30 -2
- package/dist/html-inspect.d.ts +4 -1
- package/dist/html-inspect.js +48 -50
- package/dist/html-interact.d.ts +36 -0
- package/dist/html-interact.js +216 -0
- package/dist/html-review-budget.d.ts +9 -0
- package/dist/html-review-budget.js +47 -0
- package/dist/html.d.ts +86 -2
- package/dist/html.js +185 -26
- package/dist/index.js +166 -44
- package/dist/model.d.ts +2 -1
- package/dist/offline-render.d.ts +11 -0
- package/dist/offline-render.js +71 -0
- package/dist/offline-store.mjs +10 -0
- package/dist/runtime-support.d.mts +34 -0
- package/dist/runtime-support.mjs +67 -0
- package/dist/site.js +66 -51
- package/dist/store.d.ts +1 -0
- package/dist/store.js +23 -6
- package/dist/templates/form-guide.md +6 -20
- package/dist/templates/form-task-core.md +82 -0
- package/dist/templates/index.html +5 -3
- package/dist/templates/workflow-guide.md +4 -2
- package/dist/vendor/environment-auth.d.ts +1 -0
- package/dist/vendor/environment-auth.js +4 -4
- package/docs/PROTOCOL.md +378 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { hostCapabilities, workBuddyHostBlocker, isHostDiagnosticCommand, } from './runtime-support.mjs';
|
|
3
5
|
import { parseArgs } from 'node:util';
|
|
4
6
|
import { CaptureError, errorInfo, VERSION, digest } from './common.js';
|
|
5
|
-
import {
|
|
6
|
-
import { fetchPages, fetchCatalog } from './platform.js';
|
|
7
|
-
import { capture, launchChrome } from './capture.js';
|
|
8
|
-
import { pack } from './archive.js';
|
|
9
|
-
import { inspectHtml } from './html-inspect.js';
|
|
10
|
-
import { readyHtml, acceptReadyHtml } from './html-handoff.js';
|
|
11
|
-
import { nextHtml, acceptHtml, failHtml, retryHtml } from './html.js';
|
|
12
|
-
import { discoverMenus } from './menus.js';
|
|
13
|
-
import { collectForms } from './forms.js';
|
|
14
|
-
import { applicationInput } from './application.js';
|
|
15
|
-
import { readEnvironmentAuthContext, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } from './vendor/environment-auth.js';
|
|
7
|
+
import { parseHostEvents, parseHostActiveTokens, updateHostLedger } from './host-ledger.js';
|
|
16
8
|
const spec = {
|
|
17
9
|
json: { type: 'boolean' },
|
|
18
10
|
brief: { type: 'boolean' },
|
|
19
11
|
refill: { type: 'boolean' },
|
|
20
12
|
'accept-ready': { type: 'boolean' },
|
|
13
|
+
'host-active-tokens': { type: 'string' },
|
|
14
|
+
'host-events': { type: 'string' },
|
|
15
|
+
'after-state': { type: 'string' },
|
|
16
|
+
'watch-ms': { type: 'string' },
|
|
21
17
|
summary: { type: 'string' },
|
|
18
|
+
'repair-reason': { type: 'string' },
|
|
19
|
+
steps: { type: 'string' },
|
|
22
20
|
help: { type: 'boolean' },
|
|
23
21
|
version: { type: 'boolean' },
|
|
24
22
|
'app-id': { type: 'string' },
|
|
@@ -41,6 +39,7 @@ const spec = {
|
|
|
41
39
|
reason: { type: 'string' },
|
|
42
40
|
};
|
|
43
41
|
let secret = '';
|
|
42
|
+
let pendingOutput;
|
|
44
43
|
const safeAuth = (s) => ({
|
|
45
44
|
authenticated: s.authenticated,
|
|
46
45
|
status: s.status,
|
|
@@ -55,14 +54,18 @@ run --app-url URL --dir DIR [--json]
|
|
|
55
54
|
init --app-id ID --dir DIR [--allow-temporary-records]
|
|
56
55
|
next|status|discover|collect|capture|pack|retry --dir DIR
|
|
57
56
|
html next --dir DIR (allocate/resume up to concurrency host-AI jobs)
|
|
58
|
-
html inspect --dir DIR --kind page|form --page-id ID --token TOKEN [--width 390]
|
|
57
|
+
html inspect --dir DIR --kind page|form --page-id ID --token TOKEN [--width 390] [--repair-reason TEXT]
|
|
58
|
+
html interact --dir DIR --kind page|form --page-id ID --token TOKEN --steps FILE [--width 390]
|
|
59
59
|
html ready --dir DIR --kind page|form --page-id ID --token TOKEN --summary TEXT
|
|
60
|
-
html next --dir DIR --accept-ready --brief
|
|
60
|
+
html next --dir DIR --accept-ready --brief [--host-events '[]'] (durable worker identities)
|
|
61
|
+
html watch --dir DIR --host-active-tokens '["TOKEN"]' [--after-state DIGEST --watch-ms 60000]
|
|
61
62
|
html accept --dir DIR --kind page|form --page-id ID --token TOKEN [--refill --brief]
|
|
62
63
|
html fail --dir DIR --kind page|form --page-id ID --token TOKEN --reason TEXT
|
|
63
64
|
html retry --dir DIR [--kind page|form --page-id ID] (retry failures, or regenerate one target)
|
|
64
65
|
run pauses at HTML for host AI; after accept, run/pack builds the ZIP.
|
|
65
|
-
retry: retry failed
|
|
66
|
+
retry: explicitly retry failed sources and HTML; verified successes are reused.
|
|
67
|
+
run/capture resume pending work but never retry failed screenshots.
|
|
68
|
+
Task settings are fixed at init. Resume reports effectiveSettings; conflicting flags fail.
|
|
66
69
|
auth set --base-url URL --eteamsid-stdin (also supports --eteamsid VALUE)
|
|
67
70
|
auth status [--local]
|
|
68
71
|
auth profile list|current|use NAME
|
|
@@ -76,16 +79,27 @@ async function main() {
|
|
|
76
79
|
strict: true,
|
|
77
80
|
});
|
|
78
81
|
const command = pos[0];
|
|
82
|
+
const hostEvents = v['host-events'] === undefined ? undefined : parseHostEvents(v['host-events']);
|
|
83
|
+
if ((hostEvents !== undefined && !(command === 'html' && pos[1] === 'next')) ||
|
|
84
|
+
(hostEvents !== undefined && v['host-active-tokens'] !== undefined))
|
|
85
|
+
throw new CaptureError('ARGUMENT_INVALID', '--host-events 只用于 html next,不能与 --host-active-tokens 混用');
|
|
86
|
+
const activeTokens = v['host-active-tokens'] === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: parseHostActiveTokens(v['host-active-tokens']);
|
|
89
|
+
if (activeTokens !== undefined &&
|
|
90
|
+
!(command === 'html' &&
|
|
91
|
+
(['next', 'watch'].includes(pos[1]) || (v.refill && ['accept', 'fail'].includes(pos[1])))))
|
|
92
|
+
throw new CaptureError('ARGUMENT_INVALID', '--host-active-tokens 仅用于 html next/watch 或 accept/fail --refill');
|
|
93
|
+
if ((v['after-state'] !== undefined || v['watch-ms'] !== undefined) &&
|
|
94
|
+
!(command === 'html' && pos[1] === 'watch'))
|
|
95
|
+
throw new CaptureError('ARGUMENT_INVALID', '--after-state/--watch-ms 仅用于 html watch');
|
|
96
|
+
if (v['repair-reason'] !== undefined && !(command === 'html' && pos[1] === 'inspect'))
|
|
97
|
+
throw new CaptureError('ARGUMENT_INVALID', '--repair-reason 仅用于 html inspect');
|
|
98
|
+
if (v.steps !== undefined && !(command === 'html' && pos[1] === 'interact'))
|
|
99
|
+
throw new CaptureError('ARGUMENT_INVALID', '--steps 仅用于 html interact');
|
|
79
100
|
const output = (value) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else {
|
|
83
|
-
const { next, fix, ...rest } = value;
|
|
84
|
-
console.log(JSON.stringify(rest, null, 2));
|
|
85
|
-
if (fix)
|
|
86
|
-
console.log(`FIX: ${fix}`);
|
|
87
|
-
console.log(`NEXT: ${next || 'none'}`);
|
|
88
|
-
}
|
|
101
|
+
// Final output is committed only after the transaction releases its lock.
|
|
102
|
+
pendingOutput = { value, json: !!v.json };
|
|
89
103
|
};
|
|
90
104
|
if (v.help || (!command && !v.version)) {
|
|
91
105
|
if (v.json)
|
|
@@ -98,7 +112,14 @@ async function main() {
|
|
|
98
112
|
output({ version: VERSION, next: 'none' });
|
|
99
113
|
return;
|
|
100
114
|
}
|
|
115
|
+
const hostBlocker = workBuddyHostBlocker();
|
|
116
|
+
if (hostBlocker && !isHostDiagnosticCommand(pos)) {
|
|
117
|
+
output(hostBlocker);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
101
121
|
if (command === 'doctor') {
|
|
122
|
+
const { launchChrome } = await import('./capture.js');
|
|
102
123
|
const chrome = await launchChrome();
|
|
103
124
|
const version = chrome.version;
|
|
104
125
|
await chrome.close();
|
|
@@ -109,11 +130,13 @@ async function main() {
|
|
|
109
130
|
platform: process.platform,
|
|
110
131
|
chrome: version,
|
|
111
132
|
defaultConcurrency: 6,
|
|
133
|
+
host: hostCapabilities(),
|
|
112
134
|
next: 'auth status',
|
|
113
135
|
});
|
|
114
136
|
return;
|
|
115
137
|
}
|
|
116
138
|
if (command === 'auth') {
|
|
139
|
+
const { AUTH_REQUIRED_FIX, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } = await import('./vendor/environment-auth.js');
|
|
117
140
|
if (pos[1] === 'set') {
|
|
118
141
|
if (!v['base-url'])
|
|
119
142
|
throw new CaptureError('ARGUMENT_REQUIRED', '需要 --base-url');
|
|
@@ -142,6 +165,7 @@ async function main() {
|
|
|
142
165
|
const status = v.local ? readEnvironmentAuthStatus() : await verifyEnvironmentAuth();
|
|
143
166
|
output({
|
|
144
167
|
...safeAuth(status),
|
|
168
|
+
...(!status.authenticated ? { fix: AUTH_REQUIRED_FIX } : {}),
|
|
145
169
|
next: status.authenticated ? 'run --app-id ID --dir DIR' : 'auth set',
|
|
146
170
|
});
|
|
147
171
|
return;
|
|
@@ -176,6 +200,7 @@ async function main() {
|
|
|
176
200
|
throw new CaptureError('COMMAND_INVALID', `未知命令: ${command}`);
|
|
177
201
|
if (!v.dir)
|
|
178
202
|
throw new CaptureError('ARGUMENT_REQUIRED', '需要 --dir');
|
|
203
|
+
const { Store } = await import('./store.js');
|
|
179
204
|
const store = new Store(v.dir);
|
|
180
205
|
const settings = {};
|
|
181
206
|
for (const [arg, key, min, max] of [
|
|
@@ -195,13 +220,15 @@ async function main() {
|
|
|
195
220
|
}
|
|
196
221
|
const report = async (s) => {
|
|
197
222
|
const { results, htmlResults, formResults, formHtmlResults, ...status } = await store.status(s);
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
const failedSources = status.failed + status.collection.failed;
|
|
224
|
+
const auth = ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
|
|
225
|
+
? await import('./vendor/environment-auth.js')
|
|
226
|
+
: undefined;
|
|
227
|
+
const local = auth ? auth.readEnvironmentAuthStatus() : { authenticated: true };
|
|
201
228
|
const state = !local.authenticated && ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
|
|
202
229
|
? 'WAITING_AUTH'
|
|
203
230
|
: status.state;
|
|
204
|
-
|
|
231
|
+
let next = state === 'WAITING_AUTH'
|
|
205
232
|
? 'auth set'
|
|
206
233
|
: state === 'DISCOVER'
|
|
207
234
|
? 'discover'
|
|
@@ -216,17 +243,43 @@ async function main() {
|
|
|
216
243
|
: state === 'PARTIAL'
|
|
217
244
|
? 'retry'
|
|
218
245
|
: 'none';
|
|
246
|
+
// A capture failure must come with its exact recovery command, even when
|
|
247
|
+
// successful form inputs have already moved the overall state to HTML.
|
|
248
|
+
if (failedSources && command !== 'retry' && state !== 'WAITING_AUTH' && state !== 'DISCOVER')
|
|
249
|
+
next = 'retry';
|
|
219
250
|
output({
|
|
220
251
|
...status,
|
|
221
252
|
state,
|
|
253
|
+
...(state === 'WAITING_AUTH' ? { fix: auth.AUTH_REQUIRED_FIX } : {}),
|
|
222
254
|
appId: s.appId,
|
|
223
255
|
directory: store.root,
|
|
256
|
+
effectiveSettings: s.settings,
|
|
257
|
+
...(failedSources
|
|
258
|
+
? {
|
|
259
|
+
sourceRecovery: {
|
|
260
|
+
command: process.execPath,
|
|
261
|
+
args: [fileURLToPath(import.meta.url), 'retry', '--dir', store.root, '--json'],
|
|
262
|
+
reuseVerifiedSuccesses: true,
|
|
263
|
+
attemptedThisCommand: command === 'retry',
|
|
264
|
+
guidance: command === 'retry'
|
|
265
|
+
? '本次已重试。仍失败时保留原因并继续可生成项,交付标明 PARTIAL;没有新证据不要循环重试。'
|
|
266
|
+
: '需要重试失败源时直接执行此命令一次。run/capture 不重试失败项;不要查 help、重建目录或尝试加超时参数。',
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
224
270
|
warnedPages: results
|
|
225
271
|
.filter((r) => r?.status === 'succeeded' && r.warnings?.length)
|
|
226
272
|
.map((r) => ({ id: r.id, name: r.name, warnings: r.warnings })),
|
|
227
273
|
failedPages: results
|
|
228
274
|
.filter((r) => r?.status === 'failed')
|
|
229
|
-
.map((r) => ({
|
|
275
|
+
.map((r) => ({
|
|
276
|
+
id: r.id,
|
|
277
|
+
name: r.name,
|
|
278
|
+
stage: r.history.at(-1)?.stage,
|
|
279
|
+
attempts: r.history.length,
|
|
280
|
+
elapsedMs: r.history.at(-1)?.elapsedMs,
|
|
281
|
+
error: r.history.at(-1)?.error,
|
|
282
|
+
})),
|
|
230
283
|
generationJobs: [...htmlResults, ...formHtmlResults]
|
|
231
284
|
.filter((r) => r?.status === 'running')
|
|
232
285
|
.map((r) => ({
|
|
@@ -253,10 +306,28 @@ async function main() {
|
|
|
253
306
|
process.exitCode = 2;
|
|
254
307
|
};
|
|
255
308
|
if (command === 'status' || command === 'next') {
|
|
256
|
-
|
|
309
|
+
const s = await store.load();
|
|
310
|
+
store.assertSettings(s, settings);
|
|
311
|
+
await report(s);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (command === 'html' && pos[1] === 'watch') {
|
|
315
|
+
const { watchHost } = await import('./host-watch.js');
|
|
316
|
+
if (activeTokens === undefined)
|
|
317
|
+
throw new CaptureError('ARGUMENT_REQUIRED', 'html watch 需要 --host-active-tokens');
|
|
318
|
+
if (Object.keys(settings).length || v.refill || v['accept-ready'])
|
|
319
|
+
throw new CaptureError('ARGUMENT_INVALID', 'html watch 只读,不接受队列修改或任务设置');
|
|
320
|
+
output(await watchHost(store, {
|
|
321
|
+
activeTokens,
|
|
322
|
+
afterState: v['after-state'],
|
|
323
|
+
watchMs: v['watch-ms'] === undefined ? undefined : Number(v['watch-ms']),
|
|
324
|
+
}));
|
|
257
325
|
return;
|
|
258
326
|
}
|
|
259
|
-
if (command === 'html' && ['inspect', 'ready'].includes(pos[1])) {
|
|
327
|
+
if (command === 'html' && ['inspect', 'interact', 'ready'].includes(pos[1])) {
|
|
328
|
+
const ignored = Object.keys(settings).filter((key) => !(['inspect', 'interact'].includes(pos[1]) && key === 'width'));
|
|
329
|
+
if (ignored.length)
|
|
330
|
+
throw new CaptureError('ARGUMENT_INVALID', `html ${pos[1]} 不支持这些参数:${ignored.join(', ')};校对仅支持 --width`);
|
|
260
331
|
const kind = v.kind || 'page';
|
|
261
332
|
if (kind !== 'page' && kind !== 'form')
|
|
262
333
|
throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
|
|
@@ -267,10 +338,22 @@ async function main() {
|
|
|
267
338
|
const reviewLock = new Store(path.join(store.meta, 'inspection-locks', digest(`${kind}:${v['page-id']}:${v.token}`)));
|
|
268
339
|
await reviewLock.lock(async () => {
|
|
269
340
|
if (pos[1] === 'ready') {
|
|
341
|
+
const { readyHtml } = await import('./html-handoff.js');
|
|
270
342
|
output(await readyHtml(store, await store.load(), v['page-id'], v.token, kind, v.summary || ''));
|
|
271
343
|
return;
|
|
272
344
|
}
|
|
273
|
-
|
|
345
|
+
if (pos[1] === 'interact') {
|
|
346
|
+
if (!v.steps)
|
|
347
|
+
throw new CaptureError('ARGUMENT_REQUIRED', 'html interact 需要 --steps JSON文件');
|
|
348
|
+
const { interactHtml } = await import('./html-interact.js');
|
|
349
|
+
const result = await interactHtml(store, await store.load(), v['page-id'], v.token, kind, path.resolve(v.steps), settings.width);
|
|
350
|
+
output({ ...result, next: result.ok ? 'host-review' : 'repair-draft' });
|
|
351
|
+
if (!result.ok)
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const { inspectHtmlViewports } = await import('./html-inspect.js');
|
|
356
|
+
const inspection = await inspectHtmlViewports(store, await store.load(), v['page-id'], v.token, kind, settings.width, v['repair-reason']);
|
|
274
357
|
output({ ...inspection, next: inspection.ok ? 'host-review' : 'repair-draft' });
|
|
275
358
|
if (!inspection.ok)
|
|
276
359
|
process.exitCode = 1;
|
|
@@ -281,6 +364,7 @@ async function main() {
|
|
|
281
364
|
let s;
|
|
282
365
|
if (command === 'init' || command === 'run') {
|
|
283
366
|
if (v['app-id'] || v['app-url']) {
|
|
367
|
+
const { applicationInput } = await import('./application.js');
|
|
284
368
|
const input = applicationInput(v['app-url'] || v['app-id']);
|
|
285
369
|
if (v['app-id'] && applicationInput(v['app-id']).appId !== input.appId)
|
|
286
370
|
throw new CaptureError('APP_INPUT_CONFLICT', '应用 ID 与地址不一致');
|
|
@@ -300,6 +384,7 @@ async function main() {
|
|
|
300
384
|
}
|
|
301
385
|
else
|
|
302
386
|
s = await store.load();
|
|
387
|
+
store.assertSettings(s, settings);
|
|
303
388
|
if (v['allow-temporary-records']) {
|
|
304
389
|
if (!['run', 'init'].includes(command))
|
|
305
390
|
throw new CaptureError('ARGUMENT_INVALID', '临时记录开关只能在 init/run 时启用');
|
|
@@ -313,17 +398,23 @@ async function main() {
|
|
|
313
398
|
return;
|
|
314
399
|
}
|
|
315
400
|
if (command === 'pack') {
|
|
401
|
+
const { pack } = await import('./archive.js');
|
|
316
402
|
await pack(store, s);
|
|
317
403
|
await report(s);
|
|
318
404
|
return;
|
|
319
405
|
}
|
|
320
406
|
if (command === 'html') {
|
|
407
|
+
const { nextHtml, acceptHtml, failHtml, retryHtml } = await import('./html.js');
|
|
321
408
|
const kind = v.kind || 'page';
|
|
322
409
|
if (kind !== 'page' && kind !== 'form')
|
|
323
410
|
throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
|
|
324
411
|
if (pos[1] === 'next') {
|
|
325
|
-
|
|
326
|
-
|
|
412
|
+
if (hostEvents !== undefined)
|
|
413
|
+
await updateHostLedger(store, s, hostEvents);
|
|
414
|
+
const handoff = v['accept-ready']
|
|
415
|
+
? await (await import('./html-handoff.js')).acceptReadyHtml(store, s)
|
|
416
|
+
: undefined;
|
|
417
|
+
output({ ...(await nextHtml(store, s, { brief: v.brief, activeTokens })), ...handoff });
|
|
327
418
|
return;
|
|
328
419
|
}
|
|
329
420
|
if (pos[1] === 'retry') {
|
|
@@ -343,13 +434,13 @@ async function main() {
|
|
|
343
434
|
await failHtml(store, s, v['page-id'], v.token, v.reason, kind);
|
|
344
435
|
}
|
|
345
436
|
if (v.refill)
|
|
346
|
-
output(await nextHtml(store, s, { brief: v.brief }));
|
|
437
|
+
output(await nextHtml(store, s, { brief: v.brief, activeTokens }));
|
|
347
438
|
else
|
|
348
439
|
await report(s);
|
|
349
440
|
return;
|
|
350
441
|
}
|
|
351
442
|
if (command === 'retry')
|
|
352
|
-
await retryHtml(store, s);
|
|
443
|
+
await (await import('./html.js')).retryHtml(store, s);
|
|
353
444
|
const before = await store.status(s);
|
|
354
445
|
const needsCapture = ['capture', 'run', 'retry'].includes(command) &&
|
|
355
446
|
(before.pending > 0 || (command === 'retry' && before.failed > 0));
|
|
@@ -365,14 +456,16 @@ async function main() {
|
|
|
365
456
|
if (command === 'collect' && (!s.formPages || !s.menus))
|
|
366
457
|
throw new CaptureError('CATALOG_REQUIRED', '先执行 discover 获取发布菜单');
|
|
367
458
|
if (needsDiscover || needsCapture || needsCollect) {
|
|
459
|
+
const { readEnvironmentAuthContext } = await import('./vendor/environment-auth.js');
|
|
368
460
|
const auth = readEnvironmentAuthContext();
|
|
369
461
|
secret = auth.eteamsId;
|
|
370
462
|
store.bind(s, auth);
|
|
371
463
|
await store.save(s);
|
|
372
464
|
if (needsDiscover) {
|
|
373
465
|
if (s.menuRequired)
|
|
374
|
-
Object.assign(s, await discoverMenus(auth, s.appId));
|
|
466
|
+
Object.assign(s, await (await import('./menus.js')).discoverMenus(auth, s.appId));
|
|
375
467
|
else {
|
|
468
|
+
const { fetchPages, fetchCatalog } = await import('./platform.js');
|
|
376
469
|
if (!s.pages)
|
|
377
470
|
s.pages = await fetchPages(auth, s.appId);
|
|
378
471
|
if (s.siteRequired && (!s.forms || !s.appName))
|
|
@@ -382,26 +475,55 @@ async function main() {
|
|
|
382
475
|
await store.save(s);
|
|
383
476
|
}
|
|
384
477
|
if (s.menuRequired && ['collect', 'run', 'retry'].includes(command))
|
|
385
|
-
await collectForms(store, s, auth, command === 'retry');
|
|
478
|
+
await (await import('./forms.js')).collectForms(store, s, auth, command === 'retry');
|
|
386
479
|
if (['capture', 'run', 'retry'].includes(command))
|
|
387
|
-
await capture(store, s, auth, {
|
|
480
|
+
await (await import('./capture.js')).capture(store, s, auth, {
|
|
388
481
|
retryFailed: command === 'retry',
|
|
389
482
|
progress: (event) => console.error(JSON.stringify(event)),
|
|
390
483
|
});
|
|
391
484
|
}
|
|
392
485
|
if ((command === 'run' || command === 'retry') && (await store.status(s)).state === 'PACKAGE')
|
|
393
|
-
await pack(store, s);
|
|
486
|
+
await (await import('./archive.js')).pack(store, s);
|
|
394
487
|
await report(s);
|
|
395
488
|
});
|
|
396
489
|
}
|
|
397
|
-
main()
|
|
398
|
-
|
|
490
|
+
main()
|
|
491
|
+
.then(() => {
|
|
492
|
+
if (!pendingOutput)
|
|
493
|
+
return;
|
|
494
|
+
const { value, json } = pendingOutput;
|
|
495
|
+
if (json)
|
|
496
|
+
console.log(JSON.stringify(value));
|
|
497
|
+
else {
|
|
498
|
+
const { next, fix, ...rest } = value;
|
|
499
|
+
console.log(JSON.stringify(rest, null, 2));
|
|
500
|
+
if (fix)
|
|
501
|
+
console.log(`FIX: ${fix}`);
|
|
502
|
+
console.log(`NEXT: ${next || 'none'}`);
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
.catch(async (e) => {
|
|
506
|
+
let error = errorInfo(e, secret);
|
|
507
|
+
const guard = error.code.startsWith('SAFE_DELETE_BULK_')
|
|
508
|
+
? error.message.match(/\[safe-delete\]\[SAFE_DELETE_BULK_[A-Z_]+\][^\r\n]*/)?.[0]
|
|
509
|
+
: undefined;
|
|
510
|
+
if (guard) {
|
|
511
|
+
// WorkBuddy parses the first marker in stdout + stderr as raw JSON.
|
|
512
|
+
// A marker escaped inside our JSON hides the actual authorization signal.
|
|
513
|
+
// Preserve the native signal on stderr; structured stdout retains its code.
|
|
514
|
+
console.error(guard);
|
|
515
|
+
error = JSON.parse(JSON.stringify(error).replaceAll('[safe-delete][SAFE_DELETE_BULK_', 'WorkBuddy[SAFE_DELETE_BULK_'));
|
|
516
|
+
}
|
|
399
517
|
const value = {
|
|
400
518
|
ok: false,
|
|
401
519
|
error,
|
|
402
|
-
fix: error.code
|
|
403
|
-
? '
|
|
404
|
-
:
|
|
520
|
+
fix: error.code.startsWith('SAFE_DELETE_BULK_')
|
|
521
|
+
? 'WorkBuddy 宿主删除保护阻塞了命令。停止自动重试并保留草稿/回执/锁;不要手删锁目录、切换删除方式或改安全设置。由宿主正常授权流程处理后,先核对现有回执和原生任务状态,再用原目录恢复;不要把本次当作完成或重新生成页面。'
|
|
522
|
+
: error.code === 'E10_LOGIN_REQUIRED'
|
|
523
|
+
? (await import('./vendor/environment-auth.js')).AUTH_REQUIRED_FIX
|
|
524
|
+
: error.code === 'SETTINGS_MISMATCH'
|
|
525
|
+
? '按 error.details.effectiveSettings 恢复:移除不同参数;截图失败执行 retry,run/capture 不会重试失败项。'
|
|
526
|
+
: '根据 error 修正后用原目录恢复',
|
|
405
527
|
next: 'none',
|
|
406
528
|
};
|
|
407
529
|
if (process.argv.includes('--json'))
|
package/dist/model.d.ts
CHANGED
|
@@ -117,9 +117,10 @@ export interface HtmlResult {
|
|
|
117
117
|
kind?: 'page' | 'form';
|
|
118
118
|
status: 'pending' | 'running' | 'succeeded' | 'failed';
|
|
119
119
|
token: string;
|
|
120
|
+
dispatchName?: string;
|
|
120
121
|
attempt: number;
|
|
121
122
|
sourceSha256: string;
|
|
122
|
-
promptVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
123
|
+
promptVersion: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17;
|
|
123
124
|
startedAt: string;
|
|
124
125
|
finishedAt?: string;
|
|
125
126
|
file?: string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
/** Inspect and pack must execute the same bytes under the same file-origin rules. */
|
|
3
|
+
export declare function loadOfflinePage(page: Page, filename: string, form: boolean): Promise<{
|
|
4
|
+
ok: boolean;
|
|
5
|
+
stage: string;
|
|
6
|
+
ready: boolean;
|
|
7
|
+
errors: string[];
|
|
8
|
+
runtimeErrors: string[];
|
|
9
|
+
details: any;
|
|
10
|
+
url: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { bounded } from './common.js';
|
|
3
|
+
/** Inspect and pack must execute the same bytes under the same file-origin rules. */
|
|
4
|
+
export async function loadOfflinePage(page, filename, form) {
|
|
5
|
+
const url = pathToFileURL(filename).href;
|
|
6
|
+
const errors = [], runtimeErrors = [];
|
|
7
|
+
let stage = 'navigation';
|
|
8
|
+
page.on('pageerror', (error) => {
|
|
9
|
+
if (runtimeErrors.length < 10)
|
|
10
|
+
runtimeErrors.push(`${error.name}: ${error.message}`.slice(0, 500));
|
|
11
|
+
});
|
|
12
|
+
page.on('websocket', () => errors.push('页面尝试建立网络连接'));
|
|
13
|
+
await page.route('**/*', async (route) => {
|
|
14
|
+
if (route.request().url() === url || /^(data:|about:)/.test(route.request().url()))
|
|
15
|
+
await route.continue();
|
|
16
|
+
else {
|
|
17
|
+
if (errors.length < 10)
|
|
18
|
+
errors.push('页面尝试访问外部或旁路资源');
|
|
19
|
+
await route.abort();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
await page.addInitScript(() => {
|
|
23
|
+
;
|
|
24
|
+
window.__E10_INSPECTION_ERRORS__ = [];
|
|
25
|
+
const record = (message, url = '', line = 0, column = 0) => {
|
|
26
|
+
const entries = window.__E10_INSPECTION_ERRORS__;
|
|
27
|
+
if (entries.length < 10)
|
|
28
|
+
entries.push({ message: String(message).slice(0, 500), url, line, column });
|
|
29
|
+
};
|
|
30
|
+
addEventListener('error', (e) => {
|
|
31
|
+
if (e.message)
|
|
32
|
+
record(e.message, e.filename, e.lineno, e.colno);
|
|
33
|
+
});
|
|
34
|
+
addEventListener('unhandledrejection', (e) => record(e.reason?.message || String(e.reason)));
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
await page.goto(url, { waitUntil: 'load', timeout: 10000 });
|
|
38
|
+
stage = 'initialization';
|
|
39
|
+
if (form)
|
|
40
|
+
await page.waitForFunction(() => window.__E10_FORM_READY__ === true ||
|
|
41
|
+
window.__E10_INSPECTION_ERRORS__?.length > 0, undefined, { timeout: 10000 });
|
|
42
|
+
stage = 'fonts';
|
|
43
|
+
await bounded(page.evaluate(() => document.fonts.ready), 10000, 'FORM_FONTS_TIMEOUT');
|
|
44
|
+
stage = 'ready';
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
errors.push(`${stage}: ${error instanceof Error ? error.message : String(error)}`.slice(0, 700));
|
|
48
|
+
}
|
|
49
|
+
const evidence = await page
|
|
50
|
+
.evaluate(() => ({
|
|
51
|
+
ready: window.__E10_FORM_READY__ === true,
|
|
52
|
+
details: window.__E10_INSPECTION_ERRORS__ || [],
|
|
53
|
+
}))
|
|
54
|
+
.catch(() => ({ ready: false, details: [] }));
|
|
55
|
+
for (const e of evidence.details)
|
|
56
|
+
if (!runtimeErrors.some((v) => v.includes(e.message)) && runtimeErrors.length < 10)
|
|
57
|
+
runtimeErrors.push(e.message);
|
|
58
|
+
if (runtimeErrors.length)
|
|
59
|
+
errors.push('页面存在 JavaScript 运行错误');
|
|
60
|
+
if (form && !evidence.ready)
|
|
61
|
+
errors.push('表单未完成初始化,检查具体脚本错误及 __E10_FORM_READY__ 设置');
|
|
62
|
+
return {
|
|
63
|
+
ok: !errors.length,
|
|
64
|
+
stage,
|
|
65
|
+
ready: evidence.ready,
|
|
66
|
+
errors,
|
|
67
|
+
runtimeErrors,
|
|
68
|
+
details: evidence.details,
|
|
69
|
+
url,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/offline-store.mjs
CHANGED
|
@@ -44,6 +44,16 @@ export function createOfflineStorage(appId) {
|
|
|
44
44
|
return {
|
|
45
45
|
valid,
|
|
46
46
|
operate(scope, operation, value) {
|
|
47
|
+
if (operation === 'compareSave') {
|
|
48
|
+
check(scope, value?.expected);
|
|
49
|
+
check(scope, value?.next);
|
|
50
|
+
const next = read();
|
|
51
|
+
if (JSON.stringify(next.objects[scope]) !== JSON.stringify(value.expected))
|
|
52
|
+
throw new Error('FORM_CONFLICT: 本地记录已变化,请重新打开后修改');
|
|
53
|
+
next.objects[scope] = clone(value.next);
|
|
54
|
+
persist(next);
|
|
55
|
+
return clone(next.objects[scope]);
|
|
56
|
+
}
|
|
47
57
|
check(scope, value);
|
|
48
58
|
if (!['load', 'save', 'reset'].includes(operation))
|
|
49
59
|
throw new Error('未知本地操作');
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
export function hostCapabilities(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): {
|
|
2
|
+
code?: string | undefined;
|
|
3
|
+
fix?: string | undefined;
|
|
4
|
+
background: string;
|
|
5
|
+
agentTeams: string;
|
|
6
|
+
backgroundTasks: string;
|
|
7
|
+
evidenceSource: string;
|
|
8
|
+
evidenceScope: string;
|
|
9
|
+
verification: string;
|
|
10
|
+
};
|
|
11
|
+
export function workBuddyHostBlocker(env?: NodeJS.ProcessEnv, required?: boolean): {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
blocked: boolean;
|
|
14
|
+
state: string;
|
|
15
|
+
error: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
userMessage: string;
|
|
20
|
+
fix: string;
|
|
21
|
+
next: string;
|
|
22
|
+
host: {
|
|
23
|
+
code?: string | undefined;
|
|
24
|
+
fix?: string | undefined;
|
|
25
|
+
background: string;
|
|
26
|
+
agentTeams: string;
|
|
27
|
+
backgroundTasks: string;
|
|
28
|
+
evidenceSource: string;
|
|
29
|
+
evidenceScope: string;
|
|
30
|
+
verification: string;
|
|
31
|
+
};
|
|
32
|
+
} | null;
|
|
33
|
+
/** @param {string[]} commands */
|
|
34
|
+
export function isHostDiagnosticCommand(commands: string[]): boolean;
|
|
1
35
|
export function environmentValue(env: any, name: any, platform?: NodeJS.Platform): any;
|
|
2
36
|
export function productStateRoot(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, homeDirectory?: string): string;
|
|
3
37
|
export function npmEnvironment(env?: NodeJS.ProcessEnv): {
|
package/dist/runtime-support.mjs
CHANGED
|
@@ -9,6 +9,73 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const here = fileURLToPath(import.meta.url);
|
|
10
10
|
const fail = (code, message = code) => Object.assign(new Error(message), { code });
|
|
11
11
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
// Inspect only the two capability flags inherited from the host. A reserved CLI
|
|
13
|
+
// token or a saved preference is not proof that a background Agent is running.
|
|
14
|
+
export function hostCapabilities(env = process.env, platform = process.platform) {
|
|
15
|
+
const flag = (name) => {
|
|
16
|
+
const value = environmentValue(env, name, platform)?.trim() || '';
|
|
17
|
+
return /^(1|true)$/i.test(value) ? true : /^(0|false)$/i.test(value) ? false : undefined;
|
|
18
|
+
};
|
|
19
|
+
const teams = flag('CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS');
|
|
20
|
+
const disabled = flag('CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS');
|
|
21
|
+
const agentTeams = teams === true ? 'enabled' : teams === false ? 'disabled' : 'unknown';
|
|
22
|
+
const backgroundTasks = disabled === true ? 'disabled' : disabled === false ? 'allowed' : 'unknown';
|
|
23
|
+
const background = disabled === true || teams === false ? 'disabled' : teams === true ? 'enabled' : 'unknown';
|
|
24
|
+
return {
|
|
25
|
+
background,
|
|
26
|
+
agentTeams,
|
|
27
|
+
backgroundTasks,
|
|
28
|
+
evidenceSource: 'inherited-host-environment',
|
|
29
|
+
evidenceScope: 'current-session',
|
|
30
|
+
verification: 'native-task-id-and-independent-completion-required',
|
|
31
|
+
...(background === 'disabled'
|
|
32
|
+
? {
|
|
33
|
+
code: 'HOST_BACKGROUND_DISABLED',
|
|
34
|
+
fix: workBuddyHostFix,
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const workBuddyHostFix = '请在 WorkBuddy 左下角头像 → 设置 → 智能体 → 智能体团队管理中检查智能体团队:若显示“取消禁用”,请点击;若显示“禁用”,表示界面设置已开启,请勿再次点击。然后完全退出并重新打开 WorkBuddy,回到本任务回复“继续”。若界面已开启且重启后仍受阻,可能是界面设置与当前会话能力状态不同步,请向 WorkBuddy 反馈该状态异常,不要反复切换开关。复查通过后会从原任务目录继续。';
|
|
40
|
+
const workBuddyHostReason = '应用原型生成是高并发任务,需要多个智能体同时生成和校对不同页面。未开启智能体团队时,只能逐页串行处理,整体耗时会非常长。为避免长时间等待,必须先开启智能体团队,才能继续生成。';
|
|
41
|
+
// The wrapper requires positive evidence even before runtime installation. Direct
|
|
42
|
+
// CLI use on other hosts remains supported; inherited WorkBuddy flags opt it in.
|
|
43
|
+
export function workBuddyHostBlocker(env = process.env, required = false) {
|
|
44
|
+
const host = hostCapabilities(env);
|
|
45
|
+
const detected = [
|
|
46
|
+
'CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
47
|
+
'CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS',
|
|
48
|
+
].some((name) => environmentValue(env, name) !== undefined);
|
|
49
|
+
if (host.background === 'enabled' || (!required && !detected))
|
|
50
|
+
return null;
|
|
51
|
+
const message = host.backgroundTasks === 'disabled'
|
|
52
|
+
? '当前 WorkBuddy 会话报告后台任务不可用,应用原型任务已暂停。'
|
|
53
|
+
: host.agentTeams === 'disabled'
|
|
54
|
+
? '当前 WorkBuddy 会话报告智能体团队未生效,应用原型任务已暂停。'
|
|
55
|
+
: '当前无法确认 WorkBuddy 会话的智能体团队可用,应用原型任务已暂停。';
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
blocked: true,
|
|
59
|
+
state: 'WAITING_HOST',
|
|
60
|
+
error: {
|
|
61
|
+
code: host.background === 'disabled' ? 'HOST_BACKGROUND_DISABLED' : 'HOST_BACKGROUND_UNVERIFIED',
|
|
62
|
+
message,
|
|
63
|
+
},
|
|
64
|
+
userMessage: `${message}\n\n${workBuddyHostReason}\n\n${workBuddyHostFix}`,
|
|
65
|
+
fix: workBuddyHostFix,
|
|
66
|
+
next: 'enable-agent-teams',
|
|
67
|
+
host,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** @param {string[]} commands */
|
|
71
|
+
export function isHostDiagnosticCommand(commands) {
|
|
72
|
+
const [command, subcommand, action] = commands;
|
|
73
|
+
return (['doctor', 'status', 'next', 'version', '--help', '--version'].includes(command) ||
|
|
74
|
+
(command === 'html' && subcommand === 'watch') ||
|
|
75
|
+
(command === 'auth' &&
|
|
76
|
+
(subcommand === 'status' ||
|
|
77
|
+
(subcommand === 'profile' && ['list', 'current'].includes(action)))));
|
|
78
|
+
}
|
|
12
79
|
export function environmentValue(env, name, platform = process.platform) {
|
|
13
80
|
return platform === 'win32'
|
|
14
81
|
? Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]
|