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
@@ -1,11 +1,39 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
3
+ import { loadOfflinePage } from './offline-render.js';
4
4
  import { bounded, CaptureError, digest, fileDigest, atomicJson } from './common.js';
5
5
  import { launchChrome } from './capture.js';
6
6
  import { prepareHtml } from './html.js';
7
+ import { reviewRevision, validateRepairReason } from './html-review-budget.js';
8
+ /** One host call and at most one owned Chrome for the required form viewports. */
9
+ export async function inspectHtmlViewports(store, state, pageId, token, kind = 'page', width, repairReason) {
10
+ validateRepairReason(repairReason);
11
+ if (kind === 'page' || width !== undefined)
12
+ return inspectHtml(store, state, pageId, token, kind, width, undefined, repairReason);
13
+ let chrome;
14
+ const started = Date.now();
15
+ try {
16
+ const getChrome = async () => (chrome ??= await launchChrome());
17
+ const inspections = [];
18
+ for (const w of [1440, 390])
19
+ inspections.push(await inspectHtml(store, state, pageId, token, kind, w, getChrome, repairReason));
20
+ if (new Set(inspections.map((r) => r.sourceSha256)).size !== 1)
21
+ throw new CaptureError('HTML_REVIEW_STALE', '两种视口校对期间草稿发生变化,请重新校对');
22
+ return {
23
+ ok: inspections.every((r) => r.ok),
24
+ reused: inspections.every((r) => r.reused),
25
+ inspections,
26
+ screenshots: inspections.flatMap((r) => r.screenshots),
27
+ elapsedMs: Date.now() - started,
28
+ };
29
+ }
30
+ finally {
31
+ await chrome?.close();
32
+ }
33
+ }
7
34
  /** Deterministic local rendering, never a visual similarity assertion or model call. */
8
- export async function inspectHtml(store, state, pageId, token, kind = 'page', width) {
35
+ export async function inspectHtml(store, state, pageId, token, kind = 'page', width, sharedChrome, repairReason) {
36
+ validateRepairReason(repairReason);
9
37
  const prepared = await prepareHtml(store, state, pageId, token, kind);
10
38
  const viewport = {
11
39
  width: width ?? (kind === 'page' ? prepared.png.width : 1440),
@@ -17,7 +45,7 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
17
45
  await fs.mkdir(directory, { recursive: true });
18
46
  const receiptPath = path.join(directory, `inspection-${viewport.width}.json`);
19
47
  const sourceSha256 = digest(prepared.bytes);
20
- const key = digest(JSON.stringify({ version: 3, sourceSha256, input: prepared.png.sha256, viewport }));
48
+ const key = digest(JSON.stringify({ version: 4, sourceSha256, input: prepared.png.sha256, viewport }));
21
49
  const old = await fs
22
50
  .readFile(receiptPath, 'utf8')
23
51
  .then(JSON.parse)
@@ -28,16 +56,17 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
28
56
  if (valid.length && valid.every(Boolean))
29
57
  return { ...old, reused: true, receiptPath };
30
58
  }
59
+ const reviewBudget = await reviewRevision(directory, sourceSha256, repairReason);
31
60
  const started = Date.now();
32
61
  const errors = new Set();
33
62
  const runtimeErrors = [];
34
63
  const runtimeErrorDetails = [];
35
64
  const screenshots = [];
65
+ let fixedBehavior;
36
66
  const previewPath = path.join(directory, 'preview.html');
37
67
  await fs.writeFile(previewPath, prepared.bytes, { mode: 0o600 });
38
- const previewUrl = pathToFileURL(previewPath).href;
39
68
  const sourceLines = prepared.bytes.toString('utf8').split(/\r?\n/);
40
- const chrome = await launchChrome();
69
+ const chrome = await (sharedChrome ? sharedChrome() : launchChrome());
41
70
  try {
42
71
  const context = await chrome.browser.newContext({
43
72
  viewport,
@@ -45,52 +74,18 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
45
74
  serviceWorkers: 'block',
46
75
  });
47
76
  try {
48
- await context.route('**/*', async (route) => {
49
- if (route.request().url() === previewUrl || /^(data:|about:)/.test(route.request().url()))
50
- await route.continue();
51
- else {
52
- errors.add('页面尝试访问外部或旁路资源');
53
- await route.abort();
54
- }
55
- });
56
77
  const page = await context.newPage();
57
78
  try {
58
- // Capture browser source locations, including parse errors without a JS stack.
59
- await page.addInitScript(() => {
60
- ;
61
- window.__E10_INSPECTION_ERRORS__ = [];
62
- addEventListener('error', (event) => {
63
- const errors = window.__E10_INSPECTION_ERRORS__;
64
- if (errors.length < 10 && event.message)
65
- errors.push({
66
- message: event.message.slice(0, 500),
67
- url: event.filename,
68
- line: event.lineno,
69
- column: event.colno,
70
- });
71
- });
72
- });
73
- page.on('pageerror', (error) => {
74
- errors.add('页面存在 JavaScript 运行错误');
75
- if (runtimeErrors.length < 10)
76
- runtimeErrors.push(`${error.name}: ${error.message}`.slice(0, 500));
77
- });
78
- page.on('websocket', () => errors.add('页面尝试建立网络连接'));
79
79
  await bounded((async () => {
80
- await page.goto(previewUrl, { waitUntil: 'load', timeout: 10000 });
81
- if (kind === 'form') {
82
- try {
83
- await page.waitForFunction(() => window.__E10_FORM_READY__ === true ||
84
- window.__E10_INSPECTION_ERRORS__?.length > 0, undefined, { timeout: 10000 });
85
- }
86
- catch (error) {
87
- if (!(error instanceof Error) || error.name !== 'TimeoutError')
88
- throw error;
89
- }
90
- if (!(await page.evaluate(() => window.__E10_FORM_READY__ === true)))
91
- errors.add('表单未完成初始化,检查脚本错误及 __E10_FORM_READY__ 设置');
92
- }
93
- await page.evaluate(() => document.fonts.ready);
80
+ const rendered = await loadOfflinePage(page, previewPath, kind === 'form');
81
+ for (const error of rendered.errors)
82
+ errors.add(error);
83
+ runtimeErrors.push(...rendered.runtimeErrors);
84
+ if (kind === 'form')
85
+ fixedBehavior = await page.evaluate(() => ({
86
+ initialized: window.__E10_FORM_BEHAVIOR__?.initialized === true,
87
+ limitations: (window.__E10_FORM_BEHAVIOR__?.limitations || []).slice(0, 40),
88
+ }));
94
89
  const layout = await page.evaluate(() => ({
95
90
  height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
96
91
  width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth),
@@ -124,7 +119,7 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
124
119
  }
125
120
  const browserErrors = await page.evaluate(() => window.__E10_INSPECTION_ERRORS__ || []);
126
121
  for (const error of browserErrors.slice(0, 10)) {
127
- if (error.url !== previewUrl || !Number.isInteger(error.line) || error.line < 1)
122
+ if (error.url !== rendered.url || !Number.isInteger(error.line) || error.line < 1)
128
123
  continue;
129
124
  runtimeErrorDetails.push({
130
125
  message: String(error.message).slice(0, 500),
@@ -145,7 +140,8 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
145
140
  }
146
141
  }
147
142
  finally {
148
- await chrome.close();
143
+ if (!sharedChrome)
144
+ await chrome.close();
149
145
  }
150
146
  const current = await prepareHtml(store, state, pageId, token, kind);
151
147
  if (digest(current.bytes) !== sourceSha256)
@@ -159,9 +155,11 @@ export async function inspectHtml(store, state, pageId, token, kind = 'page', wi
159
155
  errors: [...errors],
160
156
  runtimeErrors,
161
157
  runtimeErrorDetails,
158
+ fixedBehavior,
162
159
  elapsedMs: Date.now() - started,
163
160
  reviewedAt: new Date().toISOString(),
164
161
  visual: 'host-review-required',
162
+ reviewBudget,
165
163
  };
166
164
  await atomicJson(receiptPath, receipt);
167
165
  return { ...receipt, reused: false, receiptPath };
@@ -0,0 +1,36 @@
1
+ import type { Store } from './store.js';
2
+ import type { TaskState } from './model.js';
3
+ type Step = {
4
+ action: 'click' | 'fill' | 'select' | 'press' | 'check' | 'uncheck' | 'reload' | 'assert';
5
+ selector?: string;
6
+ value?: string;
7
+ dialog?: 'accept' | 'dismiss';
8
+ check?: 'visible' | 'hidden' | 'text' | 'value' | 'count';
9
+ expected?: string | number;
10
+ };
11
+ export declare function parseInteractionPlan(raw: string): Step[];
12
+ /** Fixed, token-private offline interaction runner. No server, new dependencies or arbitrary scripts. */
13
+ export declare function interactHtml(store: Store, state: TaskState, pageId: string, token: string, kind: 'page' | 'form', stepsFile: string, width?: number): Promise<{
14
+ receiptPath: string;
15
+ ok: boolean;
16
+ sourceSha256: string;
17
+ steps: Step[];
18
+ results: {
19
+ index: number;
20
+ action: string;
21
+ ok: boolean;
22
+ error?: string;
23
+ }[];
24
+ errors: string[];
25
+ screenshots: {
26
+ path: string;
27
+ sha256: string;
28
+ }[];
29
+ viewport: {
30
+ width: number;
31
+ height: number;
32
+ };
33
+ elapsedMs: number;
34
+ checkedAt: string;
35
+ }>;
36
+ export {};
@@ -0,0 +1,216 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { atomicJson, bounded, CaptureError, digest } from './common.js';
4
+ import { launchChrome } from './capture.js';
5
+ import { prepareHtml } from './html.js';
6
+ import { loadOfflinePage } from './offline-render.js';
7
+ export function parseInteractionPlan(raw) {
8
+ const invalid = (field, expected, index) => {
9
+ const step = index === undefined ? undefined : index + 1;
10
+ throw new CaptureError('INTERACTION_PLAN_INVALID', `${step === undefined ? '' : `第 ${step} 步:`}${field} ${expected};只修正步骤 JSON 后重跑 html interact,无需重生成 HTML 或重跑 inspect`, { field, ...(step === undefined ? {} : { step }), expected });
11
+ };
12
+ if (raw.length > 64000)
13
+ invalid('plan', '不能超过 64KB');
14
+ let plan;
15
+ try {
16
+ plan = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ invalid('plan', '必须是合法 JSON 对象,例如 {"steps":[{"action":"assert","selector":"h1","check":"visible"}]}');
20
+ }
21
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan))
22
+ invalid('plan', '必须是含 steps 数组的对象 {"steps":[...]},不能直接传数组');
23
+ const steps = plan?.steps;
24
+ if (!Array.isArray(steps) || !steps.length || steps.length > 40)
25
+ invalid('steps', '必须是 1..40 个操作的数组');
26
+ for (const [index, s] of steps.entries()) {
27
+ const field = `steps[${index}]`;
28
+ if (!s || typeof s !== 'object' || Array.isArray(s))
29
+ invalid(field, '必须是操作对象', index);
30
+ if (!['click', 'fill', 'select', 'press', 'check', 'uncheck', 'reload', 'assert'].includes(s.action))
31
+ invalid(`${field}.action`, '必须是 click/fill/select/press/check/uncheck/reload/assert,无脚本执行', index);
32
+ if (s.action !== 'reload' &&
33
+ (typeof s.selector !== 'string' || !s.selector.trim() || s.selector.length > 500))
34
+ invalid(`${field}.selector`, '必须是 1..500 字符的非空 CSS 选择器', index);
35
+ if (['fill', 'select', 'press'].includes(s.action) &&
36
+ (typeof s.value !== 'string' || s.value.length > 4000))
37
+ invalid(`${field}.value`, '必须是最多 4000 字符的字符串,例如 "李四" 或 "Enter"', index);
38
+ if (s.dialog !== undefined && !['accept', 'dismiss'].includes(s.dialog))
39
+ invalid(`${field}.dialog`, '只能是 "accept" 或 "dismiss"', index);
40
+ if (s.action === 'assert') {
41
+ if (!['visible', 'hidden', 'text', 'value', 'count'].includes(s.check))
42
+ invalid(`${field}.check`, '必须是 visible/hidden/text/value/count', index);
43
+ if (['text', 'value'].includes(s.check) &&
44
+ (typeof s.expected !== 'string' || s.expected.length > 4000))
45
+ invalid(`${field}.expected`, `${s.check} 断言要求最多 4000 字符的字符串,例如 "已保存"`, index);
46
+ if (s.check === 'count' &&
47
+ (!Number.isInteger(s.expected) || s.expected < 0 || s.expected > 10000))
48
+ invalid(`${field}.expected`, 'count 断言要求 0..10000 的整数,例如 6,不能写成字符串 "6"', index);
49
+ }
50
+ }
51
+ if (steps.at(-1).action !== 'assert')
52
+ invalid(`steps[${steps.length - 1}].action`, '最后一步必须是 assert 结果断言', steps.length - 1);
53
+ return steps;
54
+ }
55
+ async function runStep(page, step) {
56
+ if (step.action === 'reload') {
57
+ await page.reload({ waitUntil: 'load', timeout: 5000 });
58
+ return;
59
+ }
60
+ const target = page.locator(step.selector);
61
+ switch (step.action) {
62
+ case 'click':
63
+ await target.click();
64
+ return;
65
+ case 'fill':
66
+ await target.fill(step.value);
67
+ return;
68
+ case 'select':
69
+ await target.selectOption(step.value);
70
+ return;
71
+ case 'press':
72
+ await target.press(step.value);
73
+ return;
74
+ case 'check':
75
+ await target.check();
76
+ return;
77
+ case 'uncheck':
78
+ await target.uncheck();
79
+ return;
80
+ }
81
+ // Retry DOM assertions briefly for event handlers that update asynchronously.
82
+ const deadline = Date.now() + 2500;
83
+ let actual;
84
+ do {
85
+ if (step.check === 'visible') {
86
+ actual = await target.isVisible();
87
+ if (actual)
88
+ return;
89
+ }
90
+ if (step.check === 'hidden') {
91
+ actual = await target.isVisible();
92
+ if (!actual)
93
+ return;
94
+ }
95
+ if (step.check === 'text') {
96
+ actual = await target.innerText();
97
+ if (actual === step.expected)
98
+ return;
99
+ }
100
+ if (step.check === 'value') {
101
+ actual = await target.inputValue();
102
+ if (actual === step.expected)
103
+ return;
104
+ }
105
+ if (step.check === 'count') {
106
+ actual = await target.count();
107
+ if (actual === step.expected)
108
+ return;
109
+ }
110
+ await new Promise((resolve) => setTimeout(resolve, 50));
111
+ } while (Date.now() < deadline);
112
+ throw new CaptureError('INTERACTION_ASSERTION_FAILED', `${step.check} 断言失败:${step.selector};实际值 ${JSON.stringify(actual)?.slice(0, 500)}`);
113
+ }
114
+ /** Fixed, token-private offline interaction runner. No server, new dependencies or arbitrary scripts. */
115
+ export async function interactHtml(store, state, pageId, token, kind, stepsFile, width = 1440) {
116
+ if (!Number.isInteger(width) || width < 320 || width > 3840)
117
+ throw new CaptureError('ARGUMENT_INVALID', '交互检查宽度范围 320..3840');
118
+ const stat = await fs.lstat(stepsFile);
119
+ if (!stat.isFile() || stat.size > 64000)
120
+ throw new CaptureError('INTERACTION_PLAN_INVALID', 'steps 必须为不超过 64KB 的普通 JSON 文件');
121
+ const steps = parseInteractionPlan(await fs.readFile(stepsFile, 'utf8'));
122
+ const prepared = await prepareHtml(store, state, pageId, token, kind);
123
+ const sourceSha256 = digest(prepared.bytes);
124
+ const directory = path.join(path.dirname(prepared.draft), 'review');
125
+ await fs.mkdir(directory, { recursive: true });
126
+ const previewPath = path.join(directory, 'interaction-preview.html');
127
+ await fs.writeFile(previewPath, prepared.bytes, { mode: 0o600 });
128
+ const started = Date.now(), results = [];
129
+ const errors = [], screenshots = [];
130
+ const chrome = await launchChrome();
131
+ try {
132
+ const context = await chrome.browser.newContext({
133
+ viewport: { width, height: 900 },
134
+ offline: true,
135
+ serviceWorkers: 'block',
136
+ });
137
+ try {
138
+ const page = await context.newPage();
139
+ page.setDefaultTimeout(2500);
140
+ let dialogPolicy = 'dismiss';
141
+ page.on('dialog', async (dialog) => {
142
+ await (dialogPolicy === 'accept' ? dialog.accept() : dialog.dismiss()).catch(() => { });
143
+ });
144
+ context.on('page', (popup) => {
145
+ if (popup !== page) {
146
+ errors.push('交互尝试打开新窗口');
147
+ void popup.close().catch(() => { });
148
+ }
149
+ });
150
+ try {
151
+ await bounded((async () => {
152
+ const loaded = await loadOfflinePage(page, previewPath, kind === 'form');
153
+ if (!loaded.ok)
154
+ errors.push(...loaded.errors, ...loaded.runtimeErrors);
155
+ if (loaded.ok)
156
+ for (const [index, step] of steps.entries()) {
157
+ dialogPolicy = step.dialog || 'dismiss';
158
+ try {
159
+ await runStep(page, step);
160
+ if (page.url().split('#')[0] !== loaded.url)
161
+ throw new Error('交互离开当前离线页面');
162
+ results.push({ index, action: step.action, ok: true });
163
+ }
164
+ catch (error) {
165
+ results.push({
166
+ index,
167
+ action: step.action,
168
+ ok: false,
169
+ error: String(error).slice(0, 1000),
170
+ });
171
+ break;
172
+ }
173
+ if (loaded.errors.length || loaded.runtimeErrors.length || errors.length)
174
+ break;
175
+ }
176
+ errors.push(...loaded.errors, ...loaded.runtimeErrors);
177
+ const browserErrors = await page.evaluate(() => window.__E10_INSPECTION_ERRORS__ || []);
178
+ errors.push(...browserErrors.map((e) => String(e.message).slice(0, 500)));
179
+ const screenshotPath = path.join(directory, 'interaction-final.png');
180
+ const bytes = await page.screenshot({ timeout: 5000, animations: 'disabled' });
181
+ await fs.writeFile(screenshotPath, bytes, { mode: 0o600 });
182
+ screenshots.push({ path: screenshotPath, sha256: digest(bytes) });
183
+ })(), 30000, 'HTML_INTERACTION_TIMEOUT');
184
+ }
185
+ catch (error) {
186
+ errors.push(String(error).slice(0, 1000));
187
+ }
188
+ finally {
189
+ await bounded(page.close(), 8000, 'PAGE_CLOSE_TIMEOUT');
190
+ }
191
+ }
192
+ finally {
193
+ await bounded(context.close(), 8000, 'CONTEXT_CLOSE_TIMEOUT');
194
+ }
195
+ }
196
+ finally {
197
+ await chrome.close();
198
+ }
199
+ const current = await prepareHtml(store, state, pageId, token, kind);
200
+ if (digest(current.bytes) !== sourceSha256)
201
+ throw new CaptureError('HTML_REVIEW_STALE', '交互期间草稿发生变化,请重新验证');
202
+ const receiptPath = path.join(directory, 'interaction.json');
203
+ const receipt = {
204
+ ok: !errors.length && results.length === steps.length && results.every((r) => r.ok),
205
+ sourceSha256,
206
+ steps,
207
+ results,
208
+ errors: [...new Set(errors)],
209
+ screenshots,
210
+ viewport: { width, height: 900 },
211
+ elapsedMs: Date.now() - started,
212
+ checkedAt: new Date().toISOString(),
213
+ };
214
+ await atomicJson(receiptPath, receipt);
215
+ return { ...receipt, receiptPath };
216
+ }
@@ -0,0 +1,9 @@
1
+ export declare function validateRepairReason(reason?: string): string | undefined;
2
+ /** One writer per token. Both viewports and damaged-image repairs share a revision. */
3
+ export declare function reviewRevision(directory: string, sha256: string, reason?: string): Promise<{
4
+ historyPath: string;
5
+ repairReason?: any;
6
+ revision: any;
7
+ standardRevisionLimit: number;
8
+ remainingStandardRevisions: number;
9
+ }>;
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { atomicJson, CaptureError } from './common.js';
4
+ export function validateRepairReason(reason) {
5
+ if (reason !== undefined && (!reason.trim() || reason.trim().length > 500))
6
+ throw new CaptureError('ARGUMENT_INVALID', '--repair-reason 需要 1..500 字符的具体内容/业务/运行错误说明');
7
+ return reason?.trim();
8
+ }
9
+ /** One writer per token. Both viewports and damaged-image repairs share a revision. */
10
+ export async function reviewRevision(directory, sha256, reason) {
11
+ reason = validateRepairReason(reason);
12
+ const filename = path.join(directory, 'review-history.json');
13
+ let history;
14
+ try {
15
+ history = JSON.parse(await fs.readFile(filename, 'utf8'));
16
+ }
17
+ catch (error) {
18
+ if (error.code !== 'ENOENT')
19
+ throw new CaptureError('HTML_REVIEW_HISTORY', '校对轮次记录无法读取,保留记录并报告错误');
20
+ history = { schema: 1, revisions: [] };
21
+ }
22
+ if (history.schema !== 1 ||
23
+ !Array.isArray(history.revisions) ||
24
+ history.revisions.some((r) => !r || typeof r.sourceSha256 !== 'string'))
25
+ throw new CaptureError('HTML_REVIEW_HISTORY', '校对轮次记录格式无效,不能重置计数继续检查');
26
+ let index = history.revisions.findIndex((r) => r.sourceSha256 === sha256);
27
+ if (index === -1) {
28
+ if (history.revisions.length >= 3 && !reason)
29
+ throw new CaptureError('HTML_REVIEW_BUDGET', '已校对首版及两轮修正。停止装饰微调;若仍有缺模块、业务或运行错误,修复后追加 --repair-reason 说明具体问题;未完成不能 ready。');
30
+ index = history.revisions.length;
31
+ history.revisions.push({
32
+ sourceSha256: sha256,
33
+ firstAttemptAt: new Date().toISOString(),
34
+ ...(reason ? { repairReason: reason } : {}),
35
+ });
36
+ await atomicJson(filename, history);
37
+ }
38
+ return {
39
+ revision: index + 1,
40
+ standardRevisionLimit: 3,
41
+ remainingStandardRevisions: Math.max(0, 3 - history.revisions.length),
42
+ ...(history.revisions[index].repairReason
43
+ ? { repairReason: history.revisions[index].repairReason }
44
+ : {}),
45
+ historyPath: filename,
46
+ };
47
+ }
package/dist/html.d.ts CHANGED
@@ -1,15 +1,99 @@
1
1
  import type { HtmlResult, PageResult, TaskState, FormCollection } from './model.js';
2
2
  import { Store } from './store.js';
3
+ export { parseHostActiveTokens } from './host-ledger.js';
3
4
  export declare function draftPath(store: Store, r: HtmlResult): string;
4
5
  export declare function nextHtml(store: Store, s: TaskState, options?: {
5
6
  brief?: boolean;
7
+ activeTokens?: string[];
6
8
  }): Promise<{
7
9
  state: string;
8
10
  concurrency: number;
9
11
  host: {
10
- modelPolicy: string;
11
- scheduling: string;
12
+ code?: string | undefined;
13
+ fix?: string | undefined;
14
+ background: string;
15
+ agentTeams: string;
16
+ backgroundTasks: string;
17
+ evidenceSource: string;
18
+ evidenceScope: string;
19
+ verification: string;
12
20
  maxVisualCorrections: number;
21
+ watch?: {
22
+ command: string;
23
+ args: string[];
24
+ mode: string;
25
+ } | undefined;
26
+ availableSlots?: number | undefined;
27
+ dispatchTokens?: string[] | undefined;
28
+ slots?: ({
29
+ token: string;
30
+ htmlStatus: string;
31
+ checkReason: string;
32
+ reviewSubmitted: boolean;
33
+ kind?: undefined;
34
+ pageId?: undefined;
35
+ name?: undefined;
36
+ lastLocalActivityAt?: undefined;
37
+ localIdleMs?: undefined;
38
+ } | {
39
+ token: string;
40
+ kind: "page" | "form";
41
+ pageId: string;
42
+ name: string;
43
+ htmlStatus: "running" | "succeeded" | "failed" | "pending";
44
+ reviewSubmitted: boolean;
45
+ lastLocalActivityAt: string;
46
+ localIdleMs: number;
47
+ checkReason: string | undefined;
48
+ })[] | undefined;
49
+ observationState?: string | undefined;
50
+ reconcile?: ({
51
+ token: string;
52
+ htmlStatus: string;
53
+ checkReason: string;
54
+ reviewSubmitted: boolean;
55
+ kind?: undefined;
56
+ pageId?: undefined;
57
+ name?: undefined;
58
+ lastLocalActivityAt?: undefined;
59
+ localIdleMs?: undefined;
60
+ } | {
61
+ token: string;
62
+ kind: "page" | "form";
63
+ pageId: string;
64
+ name: string;
65
+ htmlStatus: "running" | "succeeded" | "failed" | "pending";
66
+ reviewSubmitted: boolean;
67
+ lastLocalActivityAt: string;
68
+ localIdleMs: number;
69
+ checkReason: string | undefined;
70
+ })[] | undefined;
71
+ activeTokens?: string[] | undefined;
72
+ activeCount?: number | undefined;
73
+ activeCountMeaning?: string | undefined;
74
+ deliveredAwaitingExit?: number | undefined;
75
+ scheduling: string;
76
+ schedulingInstruction: string;
77
+ completionPolicy: string;
78
+ workers?: {
79
+ taskId: string;
80
+ token: string;
81
+ kind: "page" | "form";
82
+ pageId: string;
83
+ startedAt: string;
84
+ nativeName?: string;
85
+ outcome?: string;
86
+ endedAt?: string;
87
+ }[] | undefined;
88
+ accounting?: string | undefined;
89
+ terminalWithoutHandoff?: {
90
+ taskId: string;
91
+ token: string;
92
+ pageId: string;
93
+ kind: "page" | "form";
94
+ outcome: string | undefined;
95
+ next: string;
96
+ }[] | undefined;
13
97
  };
14
98
  jobs: any[];
15
99
  html: {