e10-ebuilder-prototype 0.5.0

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 (54) hide show
  1. package/README.md +113 -0
  2. package/dist/api.d.ts +12 -0
  3. package/dist/api.js +125 -0
  4. package/dist/application.d.ts +7 -0
  5. package/dist/application.js +16 -0
  6. package/dist/archive.d.ts +130 -0
  7. package/dist/archive.js +151 -0
  8. package/dist/capture.d.ts +15 -0
  9. package/dist/capture.js +440 -0
  10. package/dist/common.d.ts +20 -0
  11. package/dist/common.js +87 -0
  12. package/dist/dom.d.mts +1 -0
  13. package/dist/dom.mjs +58 -0
  14. package/dist/form-context.d.ts +3 -0
  15. package/dist/form-context.js +58 -0
  16. package/dist/form-runtime.d.mts +2 -0
  17. package/dist/form-runtime.mjs +149 -0
  18. package/dist/forms.d.ts +51 -0
  19. package/dist/forms.js +603 -0
  20. package/dist/html.d.ts +22 -0
  21. package/dist/html.js +427 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +370 -0
  24. package/dist/menus.d.ts +32 -0
  25. package/dist/menus.js +330 -0
  26. package/dist/model.d.ts +164 -0
  27. package/dist/model.js +8 -0
  28. package/dist/offline-store.d.mts +5 -0
  29. package/dist/offline-store.mjs +80 -0
  30. package/dist/platform.d.ts +10 -0
  31. package/dist/platform.js +123 -0
  32. package/dist/readiness.d.ts +124 -0
  33. package/dist/readiness.js +529 -0
  34. package/dist/runtime-support.d.mts +52 -0
  35. package/dist/runtime-support.mjs +279 -0
  36. package/dist/site.d.ts +34 -0
  37. package/dist/site.js +195 -0
  38. package/dist/store.d.ts +90 -0
  39. package/dist/store.js +296 -0
  40. package/dist/templates/form-guide.md +539 -0
  41. package/dist/templates/index.html +803 -0
  42. package/dist/templates/placeholder.html +143 -0
  43. package/dist/templates/workflow-guide.md +95 -0
  44. package/dist/templates/workflow-presets.json +89 -0
  45. package/dist/temporary-records.d.ts +15 -0
  46. package/dist/temporary-records.js +286 -0
  47. package/dist/vendor/environment-auth.d.ts +61 -0
  48. package/dist/vendor/environment-auth.js +455 -0
  49. package/dist/workflow-runtime.d.mts +2 -0
  50. package/dist/workflow-runtime.mjs +298 -0
  51. package/dist/workflows.d.ts +28 -0
  52. package/dist/workflows.js +90 -0
  53. package/docs/PROTOCOL.md +299 -0
  54. package/package.json +45 -0
@@ -0,0 +1,279 @@
1
+ // Portable runtime primitives shared by the installed CLI and the pre-install Skill.
2
+ // Windows cmd invocation/cleanup follow ui-code-agent's Windows canary fixes (2026-08-25).
3
+ import fs from 'node:fs/promises';
4
+ import { existsSync, renameSync } from 'node:fs';
5
+ import { spawn } from 'node:child_process';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { fileURLToPath } from 'node:url';
9
+ const here = fileURLToPath(import.meta.url);
10
+ const fail = (code, message = code) => Object.assign(new Error(message), { code });
11
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ export function environmentValue(env, name, platform = process.platform) {
13
+ return platform === 'win32'
14
+ ? Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1]
15
+ : env[name];
16
+ }
17
+ export function productStateRoot(env = process.env, platform = process.platform, homeDirectory = os.homedir()) {
18
+ const api = platform === 'win32' ? path.win32 : path.posix;
19
+ const get = (name) => environmentValue(env, name, platform)?.trim();
20
+ if (get('E10_PAGE_CAPTURE_HOME'))
21
+ return api.resolve(get('E10_PAGE_CAPTURE_HOME'));
22
+ const base = platform === 'win32'
23
+ ? get('LOCALAPPDATA') || api.join(get('USERPROFILE') || homeDirectory, 'AppData', 'Local')
24
+ : platform === 'darwin'
25
+ ? api.join(homeDirectory, 'Library', 'Application Support')
26
+ : get('XDG_STATE_HOME') || api.join(homeDirectory, '.local', 'state');
27
+ // Keep the established auth/runtime namespace across the product rename.
28
+ return api.join(base, 'e10-page-capture');
29
+ }
30
+ export function npmEnvironment(env = process.env) {
31
+ const childEnv = { ...env };
32
+ for (const key of Object.keys(childEnv)) {
33
+ if ([
34
+ 'npm_config_update_notifier',
35
+ 'npm_config_cache',
36
+ 'codebuddy_session_id',
37
+ 'claude_session_id',
38
+ ].includes(key.toLowerCase()))
39
+ delete childEnv[key];
40
+ }
41
+ childEnv.npm_config_update_notifier = 'false';
42
+ return childEnv;
43
+ }
44
+ export function resolveNpmPath({ platform = process.platform, env = process.env, nodePath = process.execPath, fileExists = existsSync, } = {}) {
45
+ const api = platform === 'win32' ? path.win32 : path.posix;
46
+ const get = (name) => environmentValue(env, name, platform);
47
+ const directory = api.dirname(nodePath);
48
+ const candidates = [
49
+ get('npm_execpath'),
50
+ api.join(directory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
51
+ api.join(directory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
52
+ ];
53
+ const names = platform === 'win32' ? ['npm.cmd', 'npm.bat'] : ['npm'];
54
+ for (const dir of [directory, ...(get('PATH') || '').split(platform === 'win32' ? ';' : ':')]) {
55
+ if (dir)
56
+ for (const name of names)
57
+ candidates.push(api.join(dir.replace(/^"|"$/g, ''), name));
58
+ }
59
+ const found = candidates.find((p) => p && api.isAbsolute(p) && fileExists(p));
60
+ if (!found)
61
+ throw fail('NPM_NOT_FOUND', '未找到 npm;请使用包含 npm 的 Node.js >=22.5');
62
+ return api.resolve(found);
63
+ }
64
+ export function processInvocation(command, args, platform = process.platform, env = process.env, nodePath = process.execPath) {
65
+ if (/\.(?:mjs|cjs|js)$/i.test(command))
66
+ return { command: nodePath, args: [command, ...args], shell: false, env };
67
+ if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(command))
68
+ return { command, args, shell: false, env };
69
+ const childEnv = { ...env }, prefix = 'E10_CAPTURE_CMD_VALUE_';
70
+ for (const key of Object.keys(childEnv))
71
+ if (key.toLowerCase().startsWith(prefix.toLowerCase()))
72
+ delete childEnv[key];
73
+ const refs = [command, ...args].map((value, i) => {
74
+ if (/[\0\r\n]/.test(value))
75
+ throw fail('INVALID_ARGUMENT', 'Windows cmd 参数包含 NUL/CR/LF');
76
+ const key = prefix + i;
77
+ childEnv[key] = value.replaceAll('"', '""');
78
+ return `"%${key}%"`;
79
+ });
80
+ return {
81
+ command: environmentValue(env, 'ComSpec', platform)?.trim() || 'cmd.exe',
82
+ args: ['/d', '/s', '/v:off', '/c', `"${refs.join(' ')}"`],
83
+ shell: false,
84
+ env: childEnv,
85
+ windowsVerbatimArguments: true,
86
+ };
87
+ }
88
+ export function processTreeTerminationInvocation(pid, platform = process.platform) {
89
+ if (!Number.isSafeInteger(pid) || pid <= 0)
90
+ throw fail('INVALID_PID');
91
+ return platform === 'win32'
92
+ ? { command: 'taskkill.exe', args: ['/pid', String(pid), '/t', '/f'], shell: false }
93
+ : null;
94
+ }
95
+ export function processAlive(pid) {
96
+ try {
97
+ process.kill(pid, 0);
98
+ return true;
99
+ }
100
+ catch (e) {
101
+ if (e.code === 'ESRCH')
102
+ return false;
103
+ throw e;
104
+ }
105
+ }
106
+ export async function terminateProcessTree(pid) {
107
+ if (!Number.isSafeInteger(pid) || pid <= 0)
108
+ throw fail('INVALID_PID');
109
+ if (!processAlive(pid))
110
+ return;
111
+ const call = processTreeTerminationInvocation(pid);
112
+ if (call) {
113
+ await new Promise((resolve, reject) => {
114
+ const p = spawn(call.command, call.args, { shell: false, stdio: 'ignore', windowsHide: true });
115
+ const timer = setTimeout(() => {
116
+ p.kill();
117
+ reject(fail('PROCESS_TREE_KILL_TIMEOUT'));
118
+ }, 5000);
119
+ p.once('error', (e) => {
120
+ clearTimeout(timer);
121
+ reject(e);
122
+ });
123
+ p.once('close', () => {
124
+ clearTimeout(timer);
125
+ resolve();
126
+ });
127
+ });
128
+ }
129
+ else {
130
+ try {
131
+ process.kill(pid, 'SIGKILL');
132
+ }
133
+ catch (e) {
134
+ if (e.code !== 'ESRCH')
135
+ throw e;
136
+ }
137
+ }
138
+ const deadline = Date.now() + 5000;
139
+ while (processAlive(pid) && Date.now() < deadline)
140
+ await delay(50);
141
+ if (processAlive(pid))
142
+ throw fail('PROCESS_STILL_RUNNING');
143
+ }
144
+ export async function runChild(command, args, { cwd, stdio = 'inherit', timeout = 120000, env = process.env } = {}) {
145
+ const call = processInvocation(command, args, process.platform, env);
146
+ return new Promise((resolve, reject) => {
147
+ const p = spawn(call.command, call.args, { ...call, cwd, stdio, windowsHide: true });
148
+ let stopping = false;
149
+ const cleanup = () => {
150
+ clearTimeout(timer);
151
+ process.removeListener('SIGINT', interrupt);
152
+ process.removeListener('SIGTERM', interrupt);
153
+ };
154
+ const stop = async (code) => {
155
+ if (stopping)
156
+ return;
157
+ stopping = true;
158
+ cleanup();
159
+ try {
160
+ if (p.pid)
161
+ await terminateProcessTree(p.pid);
162
+ reject(fail(code));
163
+ }
164
+ catch (e) {
165
+ reject(e);
166
+ }
167
+ };
168
+ const interrupt = () => {
169
+ void stop('CHILD_INTERRUPTED');
170
+ };
171
+ const timer = setTimeout(() => {
172
+ void stop('CHILD_TIMEOUT');
173
+ }, timeout);
174
+ process.once('SIGINT', interrupt);
175
+ process.once('SIGTERM', interrupt);
176
+ p.once('error', (e) => {
177
+ cleanup();
178
+ if (!stopping)
179
+ reject(e);
180
+ });
181
+ p.once('close', (code) => {
182
+ cleanup();
183
+ if (!stopping)
184
+ resolve(code ?? 1);
185
+ });
186
+ });
187
+ }
188
+ export function chromeExecutable({ platform = process.platform, env = process.env, homeDirectory = os.homedir(), fileExists = existsSync, } = {}) {
189
+ const api = platform === 'win32' ? path.win32 : path.posix;
190
+ const get = (name) => environmentValue(env, name, platform)?.trim();
191
+ const explicit = get('E10_PAGE_CAPTURE_CHROME');
192
+ if (explicit) {
193
+ if (!api.isAbsolute(explicit) || !fileExists(explicit))
194
+ throw fail('CHROME_PATH_INVALID', 'E10_PAGE_CAPTURE_CHROME 必须指向已存在的 Chrome 绝对路径');
195
+ return api.resolve(explicit);
196
+ }
197
+ const candidates = platform === 'win32'
198
+ ? [
199
+ get('ProgramFiles'),
200
+ get('ProgramFiles(x86)'),
201
+ get('LOCALAPPDATA'),
202
+ api.join(get('USERPROFILE') || homeDirectory, 'AppData', 'Local'),
203
+ ]
204
+ .filter(Boolean)
205
+ .map((dir) => api.join(dir, 'Google', 'Chrome', 'Application', 'chrome.exe'))
206
+ : platform === 'darwin'
207
+ ? [
208
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
209
+ api.join(homeDirectory, 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
210
+ ]
211
+ : ['/opt/google/chrome/chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'];
212
+ const found = candidates.find(fileExists);
213
+ if (!found)
214
+ throw fail('CHROME_NOT_FOUND', '未找到 Google Chrome;请安装或设置 E10_PAGE_CAPTURE_CHROME');
215
+ return found;
216
+ }
217
+ export async function renameWithRetry(source, target, rename = fs.rename) {
218
+ for (let i = 0;; i++) {
219
+ try {
220
+ await rename(source, target);
221
+ return;
222
+ }
223
+ catch (e) {
224
+ if (i === 5 || !['EPERM', 'EACCES', 'EBUSY'].includes(e.code))
225
+ throw e;
226
+ await delay(50 * (i + 1));
227
+ }
228
+ }
229
+ }
230
+ export function renameWithRetrySync(source, target) {
231
+ for (let i = 0;; i++) {
232
+ try {
233
+ renameSync(source, target);
234
+ return;
235
+ }
236
+ catch (e) {
237
+ if (i === 5 || !['EPERM', 'EACCES', 'EBUSY'].includes(e.code))
238
+ throw e;
239
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (i + 1));
240
+ }
241
+ }
242
+ }
243
+ export function reservedWindowsName(name) {
244
+ return /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(name);
245
+ }
246
+ // Removing a managed directory runs outside the long-lived host: Windows Sandbox
247
+ // file deletion can otherwise remain pending even after Chrome has already exited.
248
+ export async function cleanupOwnedDirectory(target, parent, prefix) {
249
+ assertCleanupTarget(target, parent, prefix);
250
+ if (!existsSync(target))
251
+ return;
252
+ const code = await runChild(process.execPath, [here, '--cleanup-owned', target, parent, prefix], {
253
+ stdio: 'ignore',
254
+ timeout: process.platform === 'win32' ? 30000 : 15000,
255
+ });
256
+ if (code !== 0 || existsSync(target))
257
+ throw fail('DIRECTORY_CLEANUP_FAILED', `清理未完成: ${target}`);
258
+ }
259
+ function assertCleanupTarget(target, parent, prefix) {
260
+ const relative = path.relative(path.resolve(parent), path.resolve(target));
261
+ if (!prefix ||
262
+ !relative ||
263
+ relative === '..' ||
264
+ path.isAbsolute(relative) ||
265
+ relative.includes(path.sep) ||
266
+ !relative.startsWith(prefix))
267
+ throw fail('CLEANUP_TARGET_INVALID');
268
+ }
269
+ if (process.argv[1] &&
270
+ path.resolve(process.argv[1]) === here &&
271
+ process.argv[2] === '--cleanup-owned') {
272
+ const [target, parent, prefix] = process.argv.slice(3);
273
+ assertCleanupTarget(target, parent, prefix);
274
+ const actualParent = await fs.realpath(parent);
275
+ const actualTargetParent = await fs.realpath(path.dirname(target));
276
+ if (actualParent !== actualTargetParent)
277
+ throw fail('CLEANUP_TARGET_INVALID');
278
+ await fs.rm(target, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 });
279
+ }
package/dist/site.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Store } from './store.js';
2
+ import type { TaskState, MenuItem } from './model.js';
3
+ export declare const TEMPLATE_VERSION = 7;
4
+ export declare const pageHtmlPath: (pageId: string) => string;
5
+ export declare const formHtmlPath: (formId: string) => string;
6
+ export declare const escapeHtml: (value: string) => string;
7
+ export interface SiteItem {
8
+ id: string;
9
+ name: string;
10
+ kind: 'page' | 'form';
11
+ html: string;
12
+ png?: string;
13
+ available: boolean;
14
+ objId?: string;
15
+ objectIds?: string[];
16
+ workflowType?: string;
17
+ }
18
+ export declare function renderIndex(appName: string, items: SiteItem[], menus?: MenuItem[], appId?: string): Promise<string>;
19
+ export declare function renderPlaceholder(name: string, form?: boolean, failed?: boolean): Promise<string>;
20
+ export declare function buildSite(store: Store, s: TaskState, status: Awaited<ReturnType<Store['status']>>): Promise<{
21
+ items: SiteItem[];
22
+ metadata: Map<string, {
23
+ readonly sha256: string;
24
+ readonly bytes: number;
25
+ }>;
26
+ templateVersion: number;
27
+ finish(manifest: unknown): Promise<{
28
+ entry: string;
29
+ files: {
30
+ path: string;
31
+ sha256: string;
32
+ }[];
33
+ }>;
34
+ }>;
package/dist/site.js ADDED
@@ -0,0 +1,195 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { filenames } from './store.js';
5
+ import { atomicJson, bounded, CaptureError, fileDigest, id, mapLimit } from './common.js';
6
+ import { renameWithRetry } from './runtime-support.mjs';
7
+ import { launchChrome } from './capture.js';
8
+ import { navigationKey } from './menus.js';
9
+ import { createOfflineStorage } from './offline-store.mjs';
10
+ import { verifiedForm } from './forms.js';
11
+ export const TEMPLATE_VERSION = 7;
12
+ export const pageHtmlPath = (pageId) => `page/${id(pageId)}/${id(pageId)}.html`;
13
+ export const formHtmlPath = (formId) => `form/${navigationKey(formId)}/${navigationKey(formId)}.html`;
14
+ export const escapeHtml = (value) => value.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
15
+ export async function renderIndex(appName, items, menus, appId) {
16
+ const template = await fs.readFile(new URL('./templates/index.html', import.meta.url), 'utf8');
17
+ const encode = (data) => JSON.stringify(data)
18
+ .replaceAll('<', '\\u003c')
19
+ .replaceAll('\u2028', '\\u2028')
20
+ .replaceAll('\u2029', '\\u2029');
21
+ return template
22
+ .replace('__OFFLINE_STORAGE__', () => createOfflineStorage.toString())
23
+ .replace('__APP_TITLE__', () => escapeHtml(appName))
24
+ .replace('__CATALOG_JSON__', () => encode({ appName, items, menus, appId }));
25
+ }
26
+ export async function renderPlaceholder(name, form = true, failed = false) {
27
+ const template = await fs.readFile(new URL('./templates/placeholder.html', import.meta.url), 'utf8');
28
+ const values = {
29
+ NAME: name,
30
+ BADGE: form ? (failed ? '表单 · 暂不可用' : '表单 · 暂未对接') : '页面 · 暂不可用',
31
+ DESCRIPTION: form && !failed ? '表单功能暂未对接。' : '此页面暂不可用,请稍后重试。',
32
+ };
33
+ return template.replace(/__(NAME|BADGE|DESCRIPTION)__/g, (_, key) => escapeHtml(values[key]));
34
+ }
35
+ async function writeAtomic(filename, bytes) {
36
+ await fs.mkdir(path.dirname(filename), { recursive: true });
37
+ const temporary = `${filename}.${randomUUID()}.tmp`;
38
+ try {
39
+ await fs.writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
40
+ await renameWithRetry(temporary, filename);
41
+ }
42
+ finally {
43
+ await fs.rm(temporary, { force: true });
44
+ }
45
+ }
46
+ export async function buildSite(store, s, status) {
47
+ if (!s.pages || !s.forms || !s.appName)
48
+ throw new CaptureError('CATALOG_REQUIRED', '先执行 run 完成应用和表单目录发现');
49
+ const files = new Set(), items = [], sources = new Map();
50
+ const target = (relative) => store.artifact(`site/${relative}`);
51
+ const put = async (relative, bytes) => {
52
+ await writeAtomic(target(relative), bytes);
53
+ files.add(relative);
54
+ };
55
+ for (const [i, p] of s.pages.entries()) {
56
+ const r = status.results[i], html = status.htmlResults[i];
57
+ const item = {
58
+ id: p.id,
59
+ name: p.name,
60
+ kind: 'page',
61
+ html: pageHtmlPath(p.id),
62
+ available: await store.verifiedHtml(html, r),
63
+ };
64
+ if (await store.verified(r)) {
65
+ item.png = `page/${p.id}/${filenames([p]).get(p.id)}`;
66
+ await put(item.png, await fs.readFile(store.artifact(r.file)));
67
+ }
68
+ sources.set(item.html, item.available
69
+ ? await fs.readFile(store.artifact(html.file), 'utf8')
70
+ : await renderPlaceholder(p.name, false));
71
+ items.push(item);
72
+ }
73
+ for (const [i, f] of (s.menuRequired ? s.formPages || [] : s.forms).entries()) {
74
+ const pngName = filenames([
75
+ { id: f.id, name: f.name, appId: s.appId, url: '', terminal: 'PC' },
76
+ ]).get(f.id);
77
+ const source = status.formResults[i], html = status.formHtmlResults[i];
78
+ const available = !!s.menuRequired &&
79
+ (await verifiedForm(store, source)) &&
80
+ (await store.verifiedHtml(html, source));
81
+ const item = {
82
+ id: f.id,
83
+ name: f.name,
84
+ kind: 'form',
85
+ html: formHtmlPath(f.id),
86
+ png: `form/${f.id}/${pngName}`,
87
+ available,
88
+ ...(s.menuRequired
89
+ ? {
90
+ objId: source?.objId,
91
+ ...(source?.kind === 'workflow'
92
+ ? { objectIds: source.objectIds, workflowType: f.workflowType }
93
+ : {}),
94
+ }
95
+ : {}),
96
+ };
97
+ sources.set(item.html, available
98
+ ? await fs.readFile(store.artifact(html.file), 'utf8')
99
+ : await renderPlaceholder(f.name, true, !!s.menuRequired));
100
+ items.push(item);
101
+ }
102
+ if (items.some((item) => item.kind === 'form')) {
103
+ const chrome = await launchChrome();
104
+ try {
105
+ const context = await chrome.browser.newContext({
106
+ viewport: { width: 1440, height: 900 },
107
+ offline: true,
108
+ });
109
+ try {
110
+ const outcomes = await mapLimit(items.filter((i) => i.kind === 'form'), s.settings.concurrency, async (item) => {
111
+ let page, failure = null;
112
+ try {
113
+ page = await context.newPage();
114
+ const errors = [];
115
+ page.on('pageerror', (error) => errors.push(error.message));
116
+ await page.setContent(sources.get(item.html), { waitUntil: 'load', timeout: 10000 });
117
+ if (s.menuRequired && item.available)
118
+ await page.waitForFunction(() => window.__E10_FORM_READY__ === true, undefined, { timeout: 10000 });
119
+ await page.evaluate(() => document.fonts.ready);
120
+ if (errors.length)
121
+ throw new CaptureError('FORM_RENDER_FAILED', `表单 ${item.id} 本地渲染出现脚本错误`);
122
+ await put(item.png, await page.screenshot({ fullPage: true, timeout: 10000 }));
123
+ }
124
+ catch (error) {
125
+ failure = error;
126
+ if (s.menuRequired && item.available) {
127
+ const receipt = await store.htmlResult(item.id, 'form');
128
+ if (receipt) {
129
+ receipt.status = 'failed';
130
+ receipt.finishedAt = new Date().toISOString();
131
+ receipt.error = {
132
+ code: 'FORM_RENDER_FAILED',
133
+ message: '本地表单渲染或初始化失败;执行 html retry 后修复并重新生成',
134
+ };
135
+ await store.saveHtmlResult(receipt);
136
+ }
137
+ failure = new CaptureError('FORM_RENDER_FAILED', `表单 ${item.id} 本地渲染失败,执行 html retry 后修复并重新生成`);
138
+ }
139
+ }
140
+ finally {
141
+ if (page)
142
+ try {
143
+ await bounded(page.close(), 8000, 'PAGE_CLOSE_TIMEOUT');
144
+ }
145
+ catch (error) {
146
+ failure ||= error;
147
+ }
148
+ }
149
+ return failure;
150
+ });
151
+ const error = outcomes.find(Boolean);
152
+ if (error)
153
+ throw error;
154
+ }
155
+ finally {
156
+ await bounded(context.close(), 8000, 'CONTEXT_CLOSE_TIMEOUT');
157
+ }
158
+ }
159
+ finally {
160
+ await chrome.close();
161
+ }
162
+ }
163
+ // URL path separators are always '/', even when the filesystem is Windows.
164
+ const menu = items.map((item) => ({
165
+ ...item,
166
+ html: item.html.split('/').map(encodeURIComponent).join('/'),
167
+ png: item.png?.split('/').map(encodeURIComponent).join('/'),
168
+ }));
169
+ for (const item of items)
170
+ await put(item.html, sources.get(item.html));
171
+ await put('index.html', await renderIndex(s.appName, menu, s.menuRequired ? s.menus : undefined, s.appId));
172
+ const metadata = new Map(await mapLimit([...files], 4, async (relative) => [
173
+ relative,
174
+ {
175
+ sha256: await fileDigest(target(relative)),
176
+ bytes: (await fs.stat(target(relative))).size,
177
+ },
178
+ ]));
179
+ return {
180
+ items,
181
+ metadata,
182
+ templateVersion: TEMPLATE_VERSION,
183
+ async finish(manifest) {
184
+ await atomicJson(target('manifest.json'), manifest);
185
+ files.add('manifest.json');
186
+ return {
187
+ entry: 'site/index.html',
188
+ files: await mapLimit([...files].sort(), 4, async (relative) => ({
189
+ path: `site/${relative}`,
190
+ sha256: await fileDigest(target(relative)),
191
+ })),
192
+ };
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,90 @@
1
+ import { type TaskState, type PageResult, type Settings, type PageItem, type HtmlResult } from './model.js';
2
+ import type { E10AuthContext } from './vendor/environment-auth.js';
3
+ export declare const htmlArtifact: (key: string, kind?: "page" | "form") => string;
4
+ export declare class Store {
5
+ readonly root: string;
6
+ readonly meta: string;
7
+ readonly artifacts: string;
8
+ constructor(dir: string);
9
+ load(): Promise<TaskState>;
10
+ save(s: TaskState): Promise<void>;
11
+ init(appId: string, settings?: Partial<Settings>): Promise<TaskState>;
12
+ bind(s: TaskState, a: E10AuthContext): void;
13
+ receiptPath(pageId: string): string;
14
+ result(pageId: string): Promise<PageResult | undefined>;
15
+ saveResult(r: PageResult): Promise<void>;
16
+ htmlReceiptPath(pageId: string, kind?: 'page' | 'form'): string;
17
+ htmlResult(pageId: string, kind?: 'page' | 'form'): Promise<HtmlResult | undefined>;
18
+ saveHtmlResult(r: HtmlResult): Promise<void>;
19
+ verifiedHtml(r: HtmlResult | undefined, png: Pick<PageResult, 'id' | 'status' | 'sha256'> | undefined): Promise<boolean>;
20
+ artifact(relative: string): string;
21
+ verified(r: PageResult | undefined): Promise<boolean>;
22
+ status(s: TaskState): Promise<{
23
+ state: string;
24
+ total: number;
25
+ succeeded: number;
26
+ failed: number;
27
+ pending: number;
28
+ results: (PageResult | undefined)[];
29
+ htmlResults: (HtmlResult | undefined)[];
30
+ formResults: (import("./model.js").FormCollection | undefined)[];
31
+ formHtmlResults: (HtmlResult | undefined)[];
32
+ pageHtml: {
33
+ required: boolean;
34
+ succeeded: number;
35
+ failed: number;
36
+ running: number;
37
+ pending: number;
38
+ };
39
+ formHtml: {
40
+ succeeded: number;
41
+ failed: number;
42
+ running: number;
43
+ pending: number;
44
+ };
45
+ collection: {
46
+ succeeded: number;
47
+ failed: number;
48
+ pending: number;
49
+ };
50
+ temporaryRecords: {
51
+ total: number;
52
+ pending: number;
53
+ cleaned: number;
54
+ };
55
+ html: {
56
+ required: boolean;
57
+ succeeded: number;
58
+ failed: number;
59
+ running: number;
60
+ pending: number;
61
+ };
62
+ appName: string | undefined;
63
+ forms: {
64
+ total: number;
65
+ integrated: number;
66
+ placeholders: number;
67
+ collection: {
68
+ succeeded: number;
69
+ failed: number;
70
+ pending: number;
71
+ };
72
+ html: {
73
+ succeeded: number;
74
+ failed: number;
75
+ running: number;
76
+ pending: number;
77
+ };
78
+ } | {
79
+ total: number;
80
+ integrated: number;
81
+ placeholders: number;
82
+ collection?: undefined;
83
+ html?: undefined;
84
+ };
85
+ entry: string | undefined;
86
+ archive: string | undefined;
87
+ }>;
88
+ lock<T>(action: () => Promise<T>): Promise<T>;
89
+ }
90
+ export declare function filenames(pages: PageItem[]): Map<string, string>;