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
package/dist/index.js ADDED
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { CaptureError, errorInfo, VERSION } from './common.js';
4
+ import { Store } from './store.js';
5
+ import { fetchPages, fetchCatalog } from './platform.js';
6
+ import { capture, launchChrome } from './capture.js';
7
+ import { pack } from './archive.js';
8
+ import { nextHtml, acceptHtml, failHtml, retryHtml } from './html.js';
9
+ import { discoverMenus } from './menus.js';
10
+ import { collectForms } from './forms.js';
11
+ import { applicationInput } from './application.js';
12
+ import { readEnvironmentAuthContext, readEnvironmentAuthStatus, readActiveEnvironmentAuthProfile, setEnvironmentAuth, verifyEnvironmentAuth, listEnvironmentAuthProfiles, useEnvironmentAuthProfile, } from './vendor/environment-auth.js';
13
+ const spec = {
14
+ json: { type: 'boolean' },
15
+ help: { type: 'boolean' },
16
+ version: { type: 'boolean' },
17
+ 'app-id': { type: 'string' },
18
+ 'app-url': { type: 'string' },
19
+ 'allow-temporary-records': { type: 'boolean' },
20
+ kind: { type: 'string' },
21
+ dir: { type: 'string' },
22
+ concurrency: { type: 'string' },
23
+ width: { type: 'string' },
24
+ height: { type: 'string' },
25
+ 'timeout-ms': { type: 'string' },
26
+ 'stable-ms': { type: 'string' },
27
+ retries: { type: 'string' },
28
+ 'base-url': { type: 'string' },
29
+ eteamsid: { type: 'string' },
30
+ 'eteamsid-stdin': { type: 'boolean' },
31
+ local: { type: 'boolean' },
32
+ 'page-id': { type: 'string' },
33
+ token: { type: 'string' },
34
+ reason: { type: 'string' },
35
+ };
36
+ let secret = '';
37
+ const safeAuth = (s) => ({
38
+ authenticated: s.authenticated,
39
+ status: s.status,
40
+ profile: s.profile,
41
+ baseUrl: s.baseUrl,
42
+ userId: s.userId,
43
+ tenantKey: s.tenantKey,
44
+ });
45
+ const help = `e10-ebuilder-prototype ${VERSION}
46
+ run --app-id ID --dir DIR [--concurrency 4] [--json]
47
+ run --app-url URL --dir DIR [--json]
48
+ init --app-id ID --dir DIR [--allow-temporary-records]
49
+ next|status|discover|collect|capture|pack|retry --dir DIR
50
+ html next --dir DIR (allocate/resume up to concurrency host-AI jobs)
51
+ html accept --dir DIR --kind page|form --page-id ID --token TOKEN
52
+ html fail --dir DIR --kind page|form --page-id ID --token TOKEN --reason TEXT
53
+ html retry --dir DIR [--kind page|form --page-id ID] (retry failures, or regenerate one target)
54
+ run pauses at HTML for host AI; after accept, run/pack builds the ZIP.
55
+ retry: retry failed pages and rebuild archive; run resumes interrupted work.
56
+ auth set --base-url URL --eteamsid-stdin (also supports --eteamsid VALUE)
57
+ auth status [--local]
58
+ auth profile list|current|use NAME
59
+ doctor
60
+ Options: --width 1440 --height 900 --timeout-ms 10000 --stable-ms 800 --retries 1
61
+ NEXT: run --app-id ID --dir DIR`;
62
+ async function main() {
63
+ const { values: v, positionals: pos } = parseArgs({
64
+ options: spec,
65
+ allowPositionals: true,
66
+ strict: true,
67
+ });
68
+ const command = pos[0];
69
+ const output = (value) => {
70
+ if (v.json)
71
+ console.log(JSON.stringify(value));
72
+ else {
73
+ const { next, fix, ...rest } = value;
74
+ console.log(JSON.stringify(rest, null, 2));
75
+ if (fix)
76
+ console.log(`FIX: ${fix}`);
77
+ console.log(`NEXT: ${next || 'none'}`);
78
+ }
79
+ };
80
+ if (v.help || (!command && !v.version)) {
81
+ if (v.json)
82
+ output({ help, next: 'run --app-id ID --dir DIR' });
83
+ else
84
+ console.log(help);
85
+ return;
86
+ }
87
+ if (v.version || command === 'version') {
88
+ output({ version: VERSION, next: 'none' });
89
+ return;
90
+ }
91
+ if (command === 'doctor') {
92
+ const chrome = await launchChrome();
93
+ const version = chrome.version;
94
+ await chrome.close();
95
+ output({
96
+ ok: true,
97
+ version: VERSION,
98
+ node: process.version,
99
+ platform: process.platform,
100
+ chrome: version,
101
+ defaultConcurrency: 4,
102
+ next: 'auth status',
103
+ });
104
+ return;
105
+ }
106
+ if (command === 'auth') {
107
+ if (pos[1] === 'set') {
108
+ if (!v['base-url'])
109
+ throw new CaptureError('ARGUMENT_REQUIRED', '需要 --base-url');
110
+ if (v['eteamsid-stdin'] && v.eteamsid)
111
+ throw new CaptureError('ARGUMENT_CONFLICT', '仅选择一种 ETEAMSID 输入方式');
112
+ secret = v.eteamsid || '';
113
+ if (v['eteamsid-stdin']) {
114
+ const chunks = [];
115
+ for await (const chunk of process.stdin) {
116
+ chunks.push(Buffer.from(chunk));
117
+ if (chunks.reduce((n, b) => n + b.length, 0) > 16384)
118
+ throw new CaptureError('INPUT_TOO_LARGE', 'ETEAMSID 输入过长');
119
+ }
120
+ secret = Buffer.concat(chunks).toString().trim();
121
+ }
122
+ if (!secret)
123
+ throw new CaptureError('ARGUMENT_REQUIRED', '需要 --eteamsid-stdin 或 --eteamsid');
124
+ output({
125
+ ...(await setEnvironmentAuth({ baseUrl: v['base-url'], eteamsId: secret })),
126
+ authenticated: true,
127
+ next: 'run --app-id ID --dir DIR',
128
+ });
129
+ return;
130
+ }
131
+ if (pos[1] === 'status') {
132
+ const status = v.local ? readEnvironmentAuthStatus() : await verifyEnvironmentAuth();
133
+ output({
134
+ ...safeAuth(status),
135
+ next: status.authenticated ? 'run --app-id ID --dir DIR' : 'auth set',
136
+ });
137
+ return;
138
+ }
139
+ if (pos[1] === 'profile') {
140
+ if (pos[2] === 'list')
141
+ output({ profiles: listEnvironmentAuthProfiles(), next: 'auth profile use NAME' });
142
+ else if (pos[2] === 'current')
143
+ output({ profile: readActiveEnvironmentAuthProfile(), next: 'run --app-id ID --dir DIR' });
144
+ else if (pos[2] === 'use' && pos[3]) {
145
+ const a = useEnvironmentAuthProfile(pos[3]);
146
+ output({ profile: pos[3], baseUrl: a.baseUrl, next: 'run --app-id ID --dir DIR' });
147
+ }
148
+ else
149
+ throw new CaptureError('COMMAND_INVALID', 'auth profile list|current|use NAME');
150
+ return;
151
+ }
152
+ throw new CaptureError('COMMAND_INVALID', 'auth set|status|profile');
153
+ }
154
+ if (![
155
+ 'init',
156
+ 'next',
157
+ 'status',
158
+ 'discover',
159
+ 'collect',
160
+ 'capture',
161
+ 'pack',
162
+ 'run',
163
+ 'retry',
164
+ 'html',
165
+ ].includes(command))
166
+ throw new CaptureError('COMMAND_INVALID', `未知命令: ${command}`);
167
+ if (!v.dir)
168
+ throw new CaptureError('ARGUMENT_REQUIRED', '需要 --dir');
169
+ const store = new Store(v.dir);
170
+ const settings = {};
171
+ for (const [arg, key, min, max] of [
172
+ ['concurrency', 'concurrency', 1, 8],
173
+ ['width', 'width', 320, 3840],
174
+ ['height', 'height', 320, 2160],
175
+ ['timeout-ms', 'timeoutMs', 2000, 600000],
176
+ ['stable-ms', 'stableMs', 200, 10000],
177
+ ['retries', 'retries', 0, 3],
178
+ ]) {
179
+ if (v[arg] !== undefined) {
180
+ const value = Number(v[arg]);
181
+ if (!Number.isInteger(value) || value < min || value > max)
182
+ throw new CaptureError('ARGUMENT_INVALID', `${arg} 范围 ${min}..${max}`);
183
+ settings[key] = value;
184
+ }
185
+ }
186
+ const report = async (s) => {
187
+ const { results, htmlResults, formResults, formHtmlResults, ...status } = await store.status(s);
188
+ const local = ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
189
+ ? readEnvironmentAuthStatus()
190
+ : { authenticated: true };
191
+ const state = !local.authenticated && ['DISCOVER', 'COLLECT', 'CAPTURE'].includes(status.state)
192
+ ? 'WAITING_AUTH'
193
+ : status.state;
194
+ const next = state === 'WAITING_AUTH'
195
+ ? 'auth set'
196
+ : state === 'DISCOVER'
197
+ ? 'discover'
198
+ : state === 'COLLECT'
199
+ ? 'collect'
200
+ : state === 'CAPTURE'
201
+ ? 'capture'
202
+ : state === 'HTML'
203
+ ? 'html next'
204
+ : state === 'PACKAGE'
205
+ ? 'pack'
206
+ : state === 'PARTIAL'
207
+ ? 'retry'
208
+ : 'none';
209
+ output({
210
+ ...status,
211
+ state,
212
+ appId: s.appId,
213
+ directory: store.root,
214
+ warnedPages: results
215
+ .filter((r) => r?.status === 'succeeded' && r.warnings?.length)
216
+ .map((r) => ({ id: r.id, name: r.name, warnings: r.warnings })),
217
+ failedPages: results
218
+ .filter((r) => r?.status === 'failed')
219
+ .map((r) => ({ id: r.id, name: r.name, error: r.history.at(-1)?.error })),
220
+ failedHtmlPages: htmlResults
221
+ .filter((r, i) => r?.status === 'failed' && r.sourceSha256 === results[i]?.sha256)
222
+ .map((r) => ({ id: r.id, error: r.error })),
223
+ failedForms: formResults.flatMap((r, i) => r?.status === 'failed' ? [{ id: r.id, name: s.formPages?.[i].name, error: r.error }] : []),
224
+ failedFormHtml: formHtmlResults.flatMap((r, i) => r?.status === 'failed' && r.sourceSha256 === formResults[i]?.sha256
225
+ ? [{ id: r.id, error: r.error }]
226
+ : []),
227
+ formWarnings: formResults.flatMap((r, i) => r?.warnings?.length
228
+ ? [{ id: r.id, name: s.formPages?.[i].name, warnings: r.warnings }]
229
+ : []),
230
+ skippedMenus: s.skippedMenus,
231
+ next,
232
+ });
233
+ if (state === 'PARTIAL')
234
+ process.exitCode = 2;
235
+ };
236
+ if (command === 'status' || command === 'next') {
237
+ await report(await store.load());
238
+ return;
239
+ }
240
+ await store.lock(async () => {
241
+ let s;
242
+ if (command === 'init' || command === 'run') {
243
+ if (v['app-id'] || v['app-url']) {
244
+ const input = applicationInput(v['app-url'] || v['app-id']);
245
+ if (v['app-id'] && applicationInput(v['app-id']).appId !== input.appId)
246
+ throw new CaptureError('APP_INPUT_CONFLICT', '应用 ID 与地址不一致');
247
+ s = await store.init(input.appId, settings);
248
+ if (input.origin) {
249
+ if ((s.requestedOrigin && s.requestedOrigin !== input.origin) ||
250
+ (s.environment && s.environment.origin !== input.origin))
251
+ throw new CaptureError('ENVIRONMENT_MISMATCH', '任务目录已绑定其它环境');
252
+ s.requestedOrigin = input.origin;
253
+ await store.save(s);
254
+ }
255
+ }
256
+ else if (command === 'run')
257
+ s = await store.load();
258
+ else
259
+ throw new CaptureError('ARGUMENT_REQUIRED', 'init 需要 --app-id 或 --app-url');
260
+ }
261
+ else
262
+ s = await store.load();
263
+ if (v['allow-temporary-records']) {
264
+ if (!['run', 'init'].includes(command))
265
+ throw new CaptureError('ARGUMENT_INVALID', '临时记录开关只能在 init/run 时启用');
266
+ if (s.discoveredAt && !s.allowTemporaryRecords)
267
+ throw new CaptureError('SETTINGS_MISMATCH', '已采集的只读任务请使用新目录启用临时参考记录');
268
+ s.allowTemporaryRecords = true;
269
+ await store.save(s);
270
+ }
271
+ if (command === 'init') {
272
+ await report(s);
273
+ return;
274
+ }
275
+ if (command === 'pack') {
276
+ await pack(store, s);
277
+ await report(s);
278
+ return;
279
+ }
280
+ if (command === 'html') {
281
+ const kind = v.kind || 'page';
282
+ if (kind !== 'page' && kind !== 'form')
283
+ throw new CaptureError('ARGUMENT_INVALID', 'kind 必须为 page 或 form');
284
+ if (pos[1] === 'next') {
285
+ output(await nextHtml(store, s));
286
+ return;
287
+ }
288
+ if (pos[1] === 'retry') {
289
+ await retryHtml(store, s, v['page-id'] ? { id: v['page-id'], kind } : undefined);
290
+ await report(s);
291
+ return;
292
+ }
293
+ if (!['accept', 'fail'].includes(pos[1]))
294
+ throw new CaptureError('COMMAND_INVALID', 'html next|accept|fail|retry');
295
+ if (!v['page-id'] || !v.token)
296
+ throw new CaptureError('ARGUMENT_REQUIRED', '需要 --page-id 和 --token');
297
+ if (pos[1] === 'accept')
298
+ await acceptHtml(store, s, v['page-id'], v.token, kind);
299
+ else {
300
+ if (!v.reason)
301
+ throw new CaptureError('ARGUMENT_REQUIRED', '需要 --reason,不能包含登录信息');
302
+ await failHtml(store, s, v['page-id'], v.token, v.reason, kind);
303
+ }
304
+ await report(s);
305
+ return;
306
+ }
307
+ if (command === 'retry')
308
+ await retryHtml(store, s);
309
+ const before = await store.status(s);
310
+ const needsCapture = ['capture', 'run', 'retry'].includes(command) &&
311
+ (before.pending > 0 || (command === 'retry' && before.failed > 0));
312
+ const needsDiscover = (!s.pages ||
313
+ (s.siteRequired && (!s.forms || !s.appName)) ||
314
+ (s.menuRequired && (!s.menus || !s.formPages))) &&
315
+ ['discover', 'run'].includes(command);
316
+ const needsCollect = s.menuRequired &&
317
+ ['collect', 'run', 'retry'].includes(command) &&
318
+ (before.temporaryRecords.pending > 0 ||
319
+ before.collection.pending > 0 ||
320
+ (command === 'retry' && before.collection.failed > 0));
321
+ if (command === 'collect' && (!s.formPages || !s.menus))
322
+ throw new CaptureError('CATALOG_REQUIRED', '先执行 discover 获取发布菜单');
323
+ if (needsDiscover || needsCapture || needsCollect) {
324
+ const auth = readEnvironmentAuthContext();
325
+ secret = auth.eteamsId;
326
+ store.bind(s, auth);
327
+ await store.save(s);
328
+ if (needsDiscover) {
329
+ if (s.menuRequired)
330
+ Object.assign(s, await discoverMenus(auth, s.appId));
331
+ else {
332
+ if (!s.pages)
333
+ s.pages = await fetchPages(auth, s.appId);
334
+ if (s.siteRequired && (!s.forms || !s.appName))
335
+ Object.assign(s, await fetchCatalog(auth, s.appId));
336
+ }
337
+ s.discoveredAt = new Date().toISOString();
338
+ await store.save(s);
339
+ }
340
+ if (s.menuRequired && ['collect', 'run', 'retry'].includes(command))
341
+ await collectForms(store, s, auth, command === 'retry');
342
+ if (['capture', 'run', 'retry'].includes(command))
343
+ await capture(store, s, auth, {
344
+ retryFailed: command === 'retry',
345
+ progress: (event) => console.error(JSON.stringify(event)),
346
+ });
347
+ }
348
+ if ((command === 'run' || command === 'retry') && (await store.status(s)).state === 'PACKAGE')
349
+ await pack(store, s);
350
+ await report(s);
351
+ });
352
+ }
353
+ main().catch((e) => {
354
+ const error = errorInfo(e, secret);
355
+ const value = {
356
+ ok: false,
357
+ error,
358
+ fix: error.code === 'E10_LOGIN_REQUIRED'
359
+ ? '执行 auth set 后用原目录重新 run'
360
+ : '根据 error 修正后用原目录恢复',
361
+ next: 'none',
362
+ };
363
+ if (process.argv.includes('--json'))
364
+ console.log(JSON.stringify(value));
365
+ else {
366
+ console.error(JSON.stringify(error, null, 2));
367
+ console.error(`FIX: ${value.fix}\nNEXT: none`);
368
+ }
369
+ process.exitCode = 1;
370
+ });
@@ -0,0 +1,32 @@
1
+ import type { E10AuthContext } from './vendor/environment-auth.js';
2
+ import type { FormPage, MenuItem, SkippedMenu } from './model.js';
3
+ export declare function navigationKey(value: string): string;
4
+ type Target = {
5
+ kind: 'page';
6
+ pageId: string;
7
+ } | {
8
+ kind: 'combined';
9
+ appId: string;
10
+ combinationId?: string;
11
+ } | Omit<FormPage, 'id' | 'menuId' | 'name' | 'appId'>;
12
+ export declare function parseMenus(data: any, appId: string, origin: string, prefix?: string): {
13
+ menus: MenuItem[];
14
+ forms: FormPage[];
15
+ skipped: SkippedMenu[];
16
+ combinations: {
17
+ key: string;
18
+ target: Extract<Target, {
19
+ kind: "combined";
20
+ }>;
21
+ }[];
22
+ };
23
+ export declare function menuItems(menus: MenuItem[]): MenuItem[];
24
+ export declare function discoverMenus(auth: E10AuthContext, appId: string): Promise<{
25
+ appName: string;
26
+ menus: MenuItem[];
27
+ skippedMenus: SkippedMenu[];
28
+ pages: import("./model.js").PageItem[];
29
+ forms: import("./model.js").FormItem[];
30
+ formPages: FormPage[];
31
+ }>;
32
+ export {};