e10-ebuilder-prototype 0.5.5 → 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 (46) hide show
  1. package/README.md +45 -13
  2. package/dist/archive.d.ts +1 -1
  3. package/dist/archive.js +3 -0
  4. package/dist/capture.js +1 -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-runtime.mjs +11 -2
  16. package/dist/host-ledger.d.ts +23 -0
  17. package/dist/host-ledger.js +183 -0
  18. package/dist/host-watch.d.ts +102 -0
  19. package/dist/host-watch.js +112 -0
  20. package/dist/html-handoff.d.ts +1 -0
  21. package/dist/html-handoff.js +30 -2
  22. package/dist/html-inspect.d.ts +2 -2
  23. package/dist/html-inspect.js +23 -51
  24. package/dist/html-interact.d.ts +36 -0
  25. package/dist/html-interact.js +216 -0
  26. package/dist/html-review-budget.d.ts +9 -0
  27. package/dist/html-review-budget.js +47 -0
  28. package/dist/html.d.ts +77 -5
  29. package/dist/html.js +155 -41
  30. package/dist/index.js +119 -44
  31. package/dist/model.d.ts +2 -1
  32. package/dist/offline-render.d.ts +11 -0
  33. package/dist/offline-render.js +71 -0
  34. package/dist/offline-store.mjs +10 -0
  35. package/dist/runtime-support.d.mts +29 -1
  36. package/dist/runtime-support.mjs +55 -5
  37. package/dist/site.js +66 -51
  38. package/dist/store.js +18 -4
  39. package/dist/templates/form-guide.md +6 -20
  40. package/dist/templates/form-task-core.md +45 -5
  41. package/dist/templates/index.html +5 -3
  42. package/dist/templates/workflow-guide.md +4 -2
  43. package/dist/vendor/environment-auth.d.ts +1 -0
  44. package/dist/vendor/environment-auth.js +4 -4
  45. package/docs/PROTOCOL.md +341 -39
  46. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,27 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { hostCapabilities } from './runtime-support.mjs';
4
+ import { hostCapabilities, workBuddyHostBlocker, isHostDiagnosticCommand, } from './runtime-support.mjs';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { CaptureError, errorInfo, VERSION, digest } from './common.js';
7
- import { Store } from './store.js';
8
- import { fetchPages, fetchCatalog } from './platform.js';
9
- import { capture, launchChrome } from './capture.js';
10
- import { pack } from './archive.js';
11
- import { inspectHtmlViewports } from './html-inspect.js';
12
- import { readyHtml, acceptReadyHtml } from './html-handoff.js';
13
- import { nextHtml, acceptHtml, failHtml, retryHtml, parseHostActiveTokens } from './html.js';
14
- import { discoverMenus } from './menus.js';
15
- import { collectForms } from './forms.js';
16
- import { applicationInput } from './application.js';
17
- import { readEnvironmentAuthContext, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } from './vendor/environment-auth.js';
7
+ import { parseHostEvents, parseHostActiveTokens, updateHostLedger } from './host-ledger.js';
18
8
  const spec = {
19
9
  json: { type: 'boolean' },
20
10
  brief: { type: 'boolean' },
21
11
  refill: { type: 'boolean' },
22
12
  'accept-ready': { type: 'boolean' },
23
13
  'host-active-tokens': { type: 'string' },
14
+ 'host-events': { type: 'string' },
15
+ 'after-state': { type: 'string' },
16
+ 'watch-ms': { type: 'string' },
24
17
  summary: { type: 'string' },
18
+ 'repair-reason': { type: 'string' },
19
+ steps: { type: 'string' },
25
20
  help: { type: 'boolean' },
26
21
  version: { type: 'boolean' },
27
22
  'app-id': { type: 'string' },
@@ -44,6 +39,7 @@ const spec = {
44
39
  reason: { type: 'string' },
45
40
  };
46
41
  let secret = '';
42
+ let pendingOutput;
47
43
  const safeAuth = (s) => ({
48
44
  authenticated: s.authenticated,
49
45
  status: s.status,
@@ -58,9 +54,11 @@ run --app-url URL --dir DIR [--json]
58
54
  init --app-id ID --dir DIR [--allow-temporary-records]
59
55
  next|status|discover|collect|capture|pack|retry --dir DIR
60
56
  html next --dir DIR (allocate/resume up to concurrency host-AI jobs)
61
- 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]
62
59
  html ready --dir DIR --kind page|form --page-id ID --token TOKEN --summary TEXT
63
- html next --dir DIR --accept-ready --brief [--host-active-tokens '["TOKEN"]']
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]
64
62
  html accept --dir DIR --kind page|form --page-id ID --token TOKEN [--refill --brief]
65
63
  html fail --dir DIR --kind page|form --page-id ID --token TOKEN --reason TEXT
66
64
  html retry --dir DIR [--kind page|form --page-id ID] (retry failures, or regenerate one target)
@@ -81,23 +79,27 @@ async function main() {
81
79
  strict: true,
82
80
  });
83
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 混用');
84
86
  const activeTokens = v['host-active-tokens'] === undefined
85
87
  ? undefined
86
88
  : parseHostActiveTokens(v['host-active-tokens']);
87
89
  if (activeTokens !== undefined &&
88
90
  !(command === 'html' &&
89
- (pos[1] === 'next' || (v.refill && ['accept', 'fail'].includes(pos[1])))))
90
- throw new CaptureError('ARGUMENT_INVALID', '--host-active-tokens 仅用于 html next 或 accept/fail --refill');
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');
91
100
  const output = (value) => {
92
- if (v.json)
93
- console.log(JSON.stringify(value));
94
- else {
95
- const { next, fix, ...rest } = value;
96
- console.log(JSON.stringify(rest, null, 2));
97
- if (fix)
98
- console.log(`FIX: ${fix}`);
99
- console.log(`NEXT: ${next || 'none'}`);
100
- }
101
+ // Final output is committed only after the transaction releases its lock.
102
+ pendingOutput = { value, json: !!v.json };
101
103
  };
102
104
  if (v.help || (!command && !v.version)) {
103
105
  if (v.json)
@@ -110,7 +112,14 @@ async function main() {
110
112
  output({ version: VERSION, next: 'none' });
111
113
  return;
112
114
  }
115
+ const hostBlocker = workBuddyHostBlocker();
116
+ if (hostBlocker && !isHostDiagnosticCommand(pos)) {
117
+ output(hostBlocker);
118
+ process.exitCode = 1;
119
+ return;
120
+ }
113
121
  if (command === 'doctor') {
122
+ const { launchChrome } = await import('./capture.js');
114
123
  const chrome = await launchChrome();
115
124
  const version = chrome.version;
116
125
  await chrome.close();
@@ -127,6 +136,7 @@ async function main() {
127
136
  return;
128
137
  }
129
138
  if (command === 'auth') {
139
+ const { AUTH_REQUIRED_FIX, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } = await import('./vendor/environment-auth.js');
130
140
  if (pos[1] === 'set') {
131
141
  if (!v['base-url'])
132
142
  throw new CaptureError('ARGUMENT_REQUIRED', '需要 --base-url');
@@ -155,6 +165,7 @@ async function main() {
155
165
  const status = v.local ? readEnvironmentAuthStatus() : await verifyEnvironmentAuth();
156
166
  output({
157
167
  ...safeAuth(status),
168
+ ...(!status.authenticated ? { fix: AUTH_REQUIRED_FIX } : {}),
158
169
  next: status.authenticated ? 'run --app-id ID --dir DIR' : 'auth set',
159
170
  });
160
171
  return;
@@ -189,6 +200,7 @@ async function main() {
189
200
  throw new CaptureError('COMMAND_INVALID', `未知命令: ${command}`);
190
201
  if (!v.dir)
191
202
  throw new CaptureError('ARGUMENT_REQUIRED', '需要 --dir');
203
+ const { Store } = await import('./store.js');
192
204
  const store = new Store(v.dir);
193
205
  const settings = {};
194
206
  for (const [arg, key, min, max] of [
@@ -209,9 +221,10 @@ async function main() {
209
221
  const report = async (s) => {
210
222
  const { results, htmlResults, formResults, formHtmlResults, ...status } = await store.status(s);
211
223
  const failedSources = status.failed + status.collection.failed;
212
- const local = ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
213
- ? readEnvironmentAuthStatus()
214
- : { authenticated: true };
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 };
215
228
  const state = !local.authenticated && ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
216
229
  ? 'WAITING_AUTH'
217
230
  : status.state;
@@ -237,6 +250,7 @@ async function main() {
237
250
  output({
238
251
  ...status,
239
252
  state,
253
+ ...(state === 'WAITING_AUTH' ? { fix: auth.AUTH_REQUIRED_FIX } : {}),
240
254
  appId: s.appId,
241
255
  directory: store.root,
242
256
  effectiveSettings: s.settings,
@@ -297,8 +311,21 @@ async function main() {
297
311
  await report(s);
298
312
  return;
299
313
  }
300
- if (command === 'html' && ['inspect', 'ready'].includes(pos[1])) {
301
- const ignored = Object.keys(settings).filter((key) => !(pos[1] === 'inspect' && key === 'width'));
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
+ }));
325
+ return;
326
+ }
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'));
302
329
  if (ignored.length)
303
330
  throw new CaptureError('ARGUMENT_INVALID', `html ${pos[1]} 不支持这些参数:${ignored.join(', ')};校对仅支持 --width`);
304
331
  const kind = v.kind || 'page';
@@ -311,10 +338,22 @@ async function main() {
311
338
  const reviewLock = new Store(path.join(store.meta, 'inspection-locks', digest(`${kind}:${v['page-id']}:${v.token}`)));
312
339
  await reviewLock.lock(async () => {
313
340
  if (pos[1] === 'ready') {
341
+ const { readyHtml } = await import('./html-handoff.js');
314
342
  output(await readyHtml(store, await store.load(), v['page-id'], v.token, kind, v.summary || ''));
315
343
  return;
316
344
  }
317
- const inspection = await inspectHtmlViewports(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']);
318
357
  output({ ...inspection, next: inspection.ok ? 'host-review' : 'repair-draft' });
319
358
  if (!inspection.ok)
320
359
  process.exitCode = 1;
@@ -325,6 +364,7 @@ async function main() {
325
364
  let s;
326
365
  if (command === 'init' || command === 'run') {
327
366
  if (v['app-id'] || v['app-url']) {
367
+ const { applicationInput } = await import('./application.js');
328
368
  const input = applicationInput(v['app-url'] || v['app-id']);
329
369
  if (v['app-id'] && applicationInput(v['app-id']).appId !== input.appId)
330
370
  throw new CaptureError('APP_INPUT_CONFLICT', '应用 ID 与地址不一致');
@@ -358,16 +398,22 @@ async function main() {
358
398
  return;
359
399
  }
360
400
  if (command === 'pack') {
401
+ const { pack } = await import('./archive.js');
361
402
  await pack(store, s);
362
403
  await report(s);
363
404
  return;
364
405
  }
365
406
  if (command === 'html') {
407
+ const { nextHtml, acceptHtml, failHtml, retryHtml } = await import('./html.js');
366
408
  const kind = v.kind || 'page';
367
409
  if (kind !== 'page' && kind !== 'form')
368
410
  throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
369
411
  if (pos[1] === 'next') {
370
- const handoff = v['accept-ready'] ? await acceptReadyHtml(store, s) : undefined;
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;
371
417
  output({ ...(await nextHtml(store, s, { brief: v.brief, activeTokens })), ...handoff });
372
418
  return;
373
419
  }
@@ -394,7 +440,7 @@ async function main() {
394
440
  return;
395
441
  }
396
442
  if (command === 'retry')
397
- await retryHtml(store, s);
443
+ await (await import('./html.js')).retryHtml(store, s);
398
444
  const before = await store.status(s);
399
445
  const needsCapture = ['capture', 'run', 'retry'].includes(command) &&
400
446
  (before.pending > 0 || (command === 'retry' && before.failed > 0));
@@ -410,14 +456,16 @@ async function main() {
410
456
  if (command === 'collect' && (!s.formPages || !s.menus))
411
457
  throw new CaptureError('CATALOG_REQUIRED', '先执行 discover 获取发布菜单');
412
458
  if (needsDiscover || needsCapture || needsCollect) {
459
+ const { readEnvironmentAuthContext } = await import('./vendor/environment-auth.js');
413
460
  const auth = readEnvironmentAuthContext();
414
461
  secret = auth.eteamsId;
415
462
  store.bind(s, auth);
416
463
  await store.save(s);
417
464
  if (needsDiscover) {
418
465
  if (s.menuRequired)
419
- Object.assign(s, await discoverMenus(auth, s.appId));
466
+ Object.assign(s, await (await import('./menus.js')).discoverMenus(auth, s.appId));
420
467
  else {
468
+ const { fetchPages, fetchCatalog } = await import('./platform.js');
421
469
  if (!s.pages)
422
470
  s.pages = await fetchPages(auth, s.appId);
423
471
  if (s.siteRequired && (!s.forms || !s.appName))
@@ -427,28 +475,55 @@ async function main() {
427
475
  await store.save(s);
428
476
  }
429
477
  if (s.menuRequired && ['collect', 'run', 'retry'].includes(command))
430
- await collectForms(store, s, auth, command === 'retry');
478
+ await (await import('./forms.js')).collectForms(store, s, auth, command === 'retry');
431
479
  if (['capture', 'run', 'retry'].includes(command))
432
- await capture(store, s, auth, {
480
+ await (await import('./capture.js')).capture(store, s, auth, {
433
481
  retryFailed: command === 'retry',
434
482
  progress: (event) => console.error(JSON.stringify(event)),
435
483
  });
436
484
  }
437
485
  if ((command === 'run' || command === 'retry') && (await store.status(s)).state === 'PACKAGE')
438
- await pack(store, s);
486
+ await (await import('./archive.js')).pack(store, s);
439
487
  await report(s);
440
488
  });
441
489
  }
442
- main().catch((e) => {
443
- 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
+ }
444
517
  const value = {
445
518
  ok: false,
446
519
  error,
447
- fix: error.code === 'E10_LOGIN_REQUIRED'
448
- ? '执行 auth set 后用原目录重新 run'
449
- : error.code === 'SETTINGS_MISMATCH'
450
- ? '按 error.details.effectiveSettings 恢复:移除不同参数;截图失败执行 retry,run/capture 不会重试失败项。'
451
- : '根据 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 修正后用原目录恢复',
452
527
  next: 'none',
453
528
  };
454
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 | 10 | 11 | 12;
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,9 +1,37 @@
1
- export function hostCapabilities(env?: NodeJS.ProcessEnv): {
1
+ export function hostCapabilities(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): {
2
2
  code?: string | undefined;
3
3
  fix?: string | undefined;
4
4
  background: string;
5
+ agentTeams: string;
6
+ backgroundTasks: string;
7
+ evidenceSource: string;
8
+ evidenceScope: string;
5
9
  verification: string;
6
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;
7
35
  export function environmentValue(env: any, name: any, platform?: NodeJS.Platform): any;
8
36
  export function productStateRoot(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, homeDirectory?: string): string;
9
37
  export function npmEnvironment(env?: NodeJS.ProcessEnv): {
@@ -11,21 +11,71 @@ const fail = (code, message = code) => Object.assign(new Error(message), { code
11
11
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
12
  // Inspect only the two capability flags inherited from the host. A reserved CLI
13
13
  // token or a saved preference is not proof that a background Agent is running.
14
- export function hostCapabilities(env = process.env) {
15
- const teams = env.CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS;
16
- const disabled = /^(1|true)$/i.test(env.CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS || '');
17
- const background = disabled || teams === '0' ? 'disabled' : teams === '1' ? 'enabled' : 'unknown';
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';
18
24
  return {
19
25
  background,
26
+ agentTeams,
27
+ backgroundTasks,
28
+ evidenceSource: 'inherited-host-environment',
29
+ evidenceScope: 'current-session',
20
30
  verification: 'native-task-id-and-independent-completion-required',
21
31
  ...(background === 'disabled'
22
32
  ? {
23
33
  code: 'HOST_BACKGROUND_DISABLED',
24
- fix: 'WorkBuddy 设置中关闭“禁用智能体团队”,并重新启动任务宿主后复查。当前 shell export 无法改变父进程;不要把同步 Agent 或 CLI running 说成后台并发。',
34
+ fix: workBuddyHostFix,
25
35
  }
26
36
  : {}),
27
37
  };
28
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
+ }
29
79
  export function environmentValue(env, name, platform = process.platform) {
30
80
  return platform === 'win32'
31
81
  ? Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]