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.
Files changed (49) hide show
  1. package/README.md +56 -14
  2. package/dist/archive.d.ts +1 -1
  3. package/dist/archive.js +3 -0
  4. package/dist/capture.js +27 -1
  5. package/dist/common.d.ts +2 -1
  6. package/dist/common.js +24 -4
  7. package/dist/form-behavior-runtime.d.mts +1 -0
  8. package/dist/form-behavior-runtime.mjs +681 -0
  9. package/dist/form-behavior.d.ts +11 -0
  10. package/dist/form-behavior.js +169 -0
  11. package/dist/form-context.d.ts +4 -0
  12. package/dist/form-context.js +7 -5
  13. package/dist/form-generation.d.ts +24 -0
  14. package/dist/form-generation.js +367 -0
  15. package/dist/form-guidance.d.ts +3 -0
  16. package/dist/form-guidance.js +28 -0
  17. package/dist/form-runtime.mjs +11 -2
  18. package/dist/host-ledger.d.ts +23 -0
  19. package/dist/host-ledger.js +183 -0
  20. package/dist/host-watch.d.ts +102 -0
  21. package/dist/host-watch.js +112 -0
  22. package/dist/html-handoff.d.ts +1 -0
  23. package/dist/html-handoff.js +30 -2
  24. package/dist/html-inspect.d.ts +4 -1
  25. package/dist/html-inspect.js +48 -50
  26. package/dist/html-interact.d.ts +36 -0
  27. package/dist/html-interact.js +216 -0
  28. package/dist/html-review-budget.d.ts +9 -0
  29. package/dist/html-review-budget.js +47 -0
  30. package/dist/html.d.ts +86 -2
  31. package/dist/html.js +185 -26
  32. package/dist/index.js +166 -44
  33. package/dist/model.d.ts +2 -1
  34. package/dist/offline-render.d.ts +11 -0
  35. package/dist/offline-render.js +71 -0
  36. package/dist/offline-store.mjs +10 -0
  37. package/dist/runtime-support.d.mts +34 -0
  38. package/dist/runtime-support.mjs +67 -0
  39. package/dist/site.js +66 -51
  40. package/dist/store.d.ts +1 -0
  41. package/dist/store.js +23 -6
  42. package/dist/templates/form-guide.md +6 -20
  43. package/dist/templates/form-task-core.md +82 -0
  44. package/dist/templates/index.html +5 -3
  45. package/dist/templates/workflow-guide.md +4 -2
  46. package/dist/vendor/environment-auth.d.ts +1 -0
  47. package/dist/vendor/environment-auth.js +4 -4
  48. package/docs/PROTOCOL.md +378 -23
  49. 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 { Store } from './store.js';
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 pages and rebuild archive; run resumes interrupted work.
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
- if (v.json)
81
- console.log(JSON.stringify(value));
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 local = ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
199
- ? readEnvironmentAuthStatus()
200
- : { authenticated: true };
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
- const next = state === 'WAITING_AUTH'
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) => ({ id: r.id, name: r.name, error: r.history.at(-1)?.error })),
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
- await report(await store.load());
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
- const inspection = await inspectHtml(store, await store.load(), v['page-id'], v.token, kind, settings.width);
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
- const handoff = v['accept-ready'] ? await acceptReadyHtml(store, s) : undefined;
326
- output({ ...(await nextHtml(store, s, { brief: v.brief })), ...handoff });
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().catch((e) => {
398
- const error = errorInfo(e, secret);
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 === 'E10_LOGIN_REQUIRED'
403
- ? '执行 auth set 后用原目录重新 run'
404
- : '根据 error 修正后用原目录恢复',
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
+ }
@@ -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): {
@@ -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]