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/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # e10-ebuilder-prototype
2
+
3
+ 根据 E10 **应用 ID 或后台应用地址生成应用原型,包含已发布菜单中的页面、建模列表和流程页面**。
4
+ 当前版本 0.5.0。保持我们的 TypeScript CLI 为主:CLI 管登录、菜单发现、配置采集、截图、任务队列、校验、恢复与打包;宿主 AI 根据完整截图或建模配置生成独立 HTML。CLI 不调用 LLM,不要求额外模型 API Key,也不调用 ui-code-agent 生成。
5
+
6
+ ## 范围与交付
7
+
8
+ - `/api/cusapp/app/allMenus?appId=ID&terminalType=PC&isFront=0` 决定范围,未发布到 PC 菜单的页面/表单不生成独立入口;流程关联表单可作为字段和布局依赖采集。实际菜单名称、层级、顺序、默认项、同名入口均保留;回收站和设置排除。
9
+ - PAGE 菜单对应普通搭建、ECode HTML、ECode React,统一截图并由宿主还原;同一页面的多个菜单共享产物而不合并入口。
10
+ - SEARCH/VIEWPORT 按当前菜单的传统列表/NList 配置生成,不取一个表单的首列表。LAYOUT 直接打开对应增/查/改模式;支持已识别的 EB URL 和组合菜单标签。
11
+ - 表单迁入参考包的采集契约、按钮隔离、字段控件与视图规范、增改查、主明细、评论/日志、本地 Mock 交互规范;同表单的多菜单共享记录。未知组件/条件/动作明确记录替代方式,不把未知后台动作接到线上。
12
+ - 主框架由 CLI 固定生成,商务蓝风格;无顶部预览/设计/设置按钮。菜单仅切换本地 iframe,主框架不刷新。支持搜索、分组折叠、浏览器前进后退、刷新与移动端导航。
13
+ - 解压后直接打开 `index.html`,不需要本地服务器。各业务 HTML 单独打开也可使用。
14
+
15
+ ```text
16
+ index.html
17
+ page/<页面ID>/<页面ID>.html
18
+ page/<页面ID>/<名称>.png
19
+ form/<菜单键>/<菜单键>.html
20
+ form/<菜单键>/<名称>.png
21
+ manifest.json
22
+ ```
23
+
24
+ 组合子菜单的菜单键为 `<父菜单键>-<子菜单ID>`。表单目录不用 objId 或底层 formId,避免多个入口覆盖。
25
+ ZIP 名为 `应用页面-<appId>.zip`,位于任务目录 artifacts/;本地入口为 artifacts/site/index.html。
26
+ 原始表单配置、私有布局参考、真实参考记录 ID、草稿和认证信息不进入 ZIP。
27
+ 页面 PNG 是源站截图;表单 PNG 是从生成 HTML 离线渲染的效果图。
28
+
29
+ ## 使用
30
+
31
+ 需要 Node >=22.5、npm 和已安装 Google Chrome,playwright-core 固定 1.62.1,不下载浏览器。
32
+
33
+ ```sh
34
+ npm install -g e10-ebuilder-prototype@0.5.0 --registry=https://registry.npmjs.org/
35
+ e10-ebuilder-prototype auth set --base-url https://your-e10.example.com --eteamsid-stdin
36
+ e10-ebuilder-prototype run --app-id 1234567890123456789 --dir ./work/app-demo --json
37
+ # 或使用后台应用地址:
38
+ e10-ebuilder-prototype run --app-url "https://your-e10.example.com/sp/ebdapp/detail/1234567890123456789" --dir ./work/app-demo --json
39
+ ```
40
+
41
+ 登录值从标准输入传入并加密保存;支持 auth status / auth status --local / auth profile list|current|use NAME。
42
+ 地址会解析 ID 并核对环境,已有任务还绑定用户与租户。ID 保持字符串,不转成浮点数。
43
+ 状态目录 `.e10-page-capture` 与 E10_PAGE_CAPTURE_HOME/CHROME 等设置沿用,既有登录无需迁移。
44
+
45
+ `run` 完成确定性工作后停在 HTML,由宿主继续:
46
+
47
+ ```sh
48
+ e10-ebuilder-prototype html next --dir ./work/app-demo --json
49
+ # 宿主按 jobs 的 prompt 写入各自 outputPath,并做视觉/交互检查
50
+ e10-ebuilder-prototype html accept --dir ./work/app-demo --kind page --page-id "<ID>" --token "<TOKEN>" --json
51
+ e10-ebuilder-prototype html accept --dir ./work/app-demo --kind form --page-id "<菜单键>" --token "<TOKEN>" --json
52
+ e10-ebuilder-prototype pack --dir ./work/app-demo --json
53
+ ```
54
+
55
+ 同一个生成队列最多 4 个任务,可按用户需求 --concurrency 1..8。page 输入 screenshotPath;form 输入 sourcePath,附带 contextPath 分片索引、guidePath 完整规范、runtimePath 固定存储脚本及 navigationPath 已发布目标映射。一个协调者串行 accept/fail/next,任务只写自己的草稿;宿主支持且允许并行时补位执行,否则如实采用顺序生成。
56
+
57
+ 生成文件为真实 DOM、内联 CSS/JS/SVG,无外部资源或后台调用。页面忠实还原视觉结构,空数据用一致 Mock 内容填充。表单按当前模式/字段/选项/按钮及布局依据生成,固定种子约 20 条主记录、明细每条 2–5 行;所有统计由数据推导。完整规范随 CLI 打包,不能用“能打开表格”代替验收。
58
+
59
+ ## 参考布局与临时记录
60
+
61
+ 默认只读业务记录:先读取至多一条现有记录,若可用则请求导出 HTML 作为私有布局参考;没有参考时根据全部自定义字段生成布局,报告缺项。导出可能在源站生成参考文件。
62
+
63
+ 与参考包相同的临时记录能力已迁入。明确允许这类采集写入时,在**新任务**加 `--allow-temporary-records`。启用后,如无法取得现有布局,按可构造字段创建最多一条带唯一标记的参考记录/表单,用它取得布局及查改按钮,随后按确切 ID 和标记删除并回查。不能可靠构造必填人员/关联/明细时使用字段兜底。
64
+
65
+ 创建前原子记录私有台账;响应丢失不重放写入,恢复时按精确标记只读核实;删除前再次核验归属,删除响应丢失只回查,不重复删除。清理未确认会阻止新临时写入和打包,恢复登录后 collect 先处理台账。默认不会因为读取参考包就自动开启线上写入。最终原型的操作始终只改本地 Mock 数据。
66
+
67
+ ## 状态与恢复
68
+
69
+ `DISCOVER → COLLECT → CAPTURE → HTML → PACKAGE → DONE/PARTIAL`;需要登录时 WAITING_AUTH。
70
+ 顶层 `next|status` 只读,`html next` 分配任务。`run --dir DIR` 恢复中断,校验通过的配置/PNG/HTML复用。
71
+ `retry` 重试失败配置/截图并重置失败生成;`html retry` 只重试 HTML。HTML/打包不读取登录环境。
72
+ 接收后需修正某页可用 `html retry --kind page|form --page-id ID --dir DIR`,只重开指定目标。
73
+ `html fail --kind page|form --page-id ID --token TOKEN --reason TEXT` 记录生成失败,其余目标继续。
74
+
75
+ 待完成任务阻止打包。采集、截图或生成失败只能交付 PARTIAL 和明确失败占位;不能当成已接入表单。
76
+ 表单打包必须在离线 Chrome 初始化成功且无脚本错误;渲染失败可通过 html retry 修复。
77
+ 所有产物及最终 ZIP 以 SHA-256 校验。退出码 0 正常、2 PARTIAL、1 阻断错误。
78
+ 旧目录无 menuRequired 时保留旧清单/占位语义,使用新目录启用本次功能。
79
+
80
+ 固定 E10FormStore 提供 load/save/reset、navigate/parameters。主框架验证发送窗口和对象范围,按 appId:objId 存储;独立打开有 localStorage/内存回退。同表单多列表共享主数据,筛选、按钮和草稿独立。浏览器禁止持久化时回退仅维持当前会话。
81
+
82
+ ## 截图和资源限制
83
+
84
+ PC 默认 1440×900,MOBILE-only 页面 390×844;默认并发 4。页面初始渲染上限 10 秒、稳定窗口 800ms;超时本轮失败,其他可恢复失败额外重试一次。就绪后立即继续,不固定睡眠。
85
+ 只用页面组件请求、可见加载、字体/图片、主体内容与稳定性判断;全局后台请求不阻塞。随后滚动检查、编码和清理另有边界,详见协议。
86
+
87
+ 截图覆盖当前已加载/可滚动内容,不自动点击标签或分页。承载 EB 主体的 iframe 完整滚动展开,普通内嵌网站保留设计视口。稳定业务错误可原样截图并带 warning,未知空白、超时、身份失效、内容丢失仍失败。源站错误截图不等于业务功能健康。
88
+
89
+ 每次尝试结束即关 Page,批次结束关本工具创建的上下文与 Chrome。默认最大高度 50000px、60M 像素。仅关闭自己拥有的进程;清理失败阻止完成。
90
+
91
+ ## 测试和 Skill 交付
92
+
93
+ ```sh
94
+ npm ci --registry=https://registry.npmjs.org/
95
+ npm test
96
+ npm run test:package
97
+ npm run pack:skill
98
+ ```
99
+
100
+ artifacts/ 生成 npm tarball、本地版 Skill ZIP、registry 四文件版 Skill ZIP。本地版含本产品 tarball 及散列,依赖从公网 npm 安装;registry 版从公网 npm 安装固定版本,要求对应版本先发布。打包命令不会自动发布 npm 或上传 Skill。
101
+
102
+ WorkBuddy 推荐提示词和执行流程见 [Skill](integrations/workbuddy-skill/SKILL.md),迁入能力对应关系见 [表单能力核对](docs/FORM-PARITY.md),协议见 [PROTOCOL](docs/PROTOCOL.md)。
103
+
104
+ Windows 使用原生 Node,无需 Bash/WSL/Python;支持 WorkBuddy 内置 npm、中文/空格路径、受控进程清理与暂时文件占用重试。系统 Chrome 自动发现,非标准位置可指定 E10_PAGE_CAPTURE_CHROME。
105
+ `npm run test:windows` 必须在 Windows 实机执行;macOS 的模拟分支不算实机通过,详见 [WINDOWS](docs/WINDOWS.md)。
106
+
107
+ ## 流程菜单(0.5.0)
108
+
109
+ 支持已发布的待办、已办、我提交的、共享、下属待办、全部流程、草稿、流程监控和流程新建入口,包含 WORKFLOW、明确 workflow URL 和组合子标签。只生成真实菜单;流程引用但没有独立菜单的 EB 表单仅作为字段/选项/布局依赖采集。普通表单的各菜单配置和按钮保持独立。
110
+
111
+ 流程列表的列、标签、筛选和按钮来自内置固定原型模板;目录接口提供真实流程名称和关联表单,不读取真实流程实例/审批权限。宿主按 CLI 的 workflow-guide 生成右侧页面,固定 E10WorkflowStore 负责本地已读、批量提交、草稿续填、收藏/关注、实例评论以及关联记录联合保存。所有流程动作仅修改本地模拟数据。流程仍使用 kind=form 队列和 form/<菜单键>/ 交付路径,job.workflowType 区分流程类别。
112
+
113
+ 完整流程协议见 [PROTOCOL.md](docs/PROTOCOL.md)。
package/dist/api.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { E10AuthContext } from './vendor/environment-auth.js';
2
+ /** Preserve integer identifiers before JSON.parse can round them. */
3
+ export declare function parseApiJson(text: string): unknown;
4
+ export declare function apiData(payload: unknown, label: string): any;
5
+ export declare class E10Client {
6
+ private auth;
7
+ constructor(auth: E10AuthContext);
8
+ get origin(): string;
9
+ clean<T>(value: T): T;
10
+ request(method: 'GET' | 'POST', pathname: string, body?: unknown, multipart?: boolean, rawEnvelope?: boolean): Promise<any>;
11
+ reference(fileId: string): Promise<string>;
12
+ }
package/dist/api.js ADDED
@@ -0,0 +1,125 @@
1
+ import { CaptureError } from './common.js';
2
+ /** Preserve integer identifiers before JSON.parse can round them. */
3
+ export function parseApiJson(text) {
4
+ const quoted = text.replace(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (part) => {
5
+ if (/^-?\d{16,}$/.test(part) &&
6
+ (BigInt(part) > BigInt(Number.MAX_SAFE_INTEGER) ||
7
+ BigInt(part) < BigInt(Number.MIN_SAFE_INTEGER)))
8
+ return JSON.stringify(part);
9
+ return part;
10
+ });
11
+ return JSON.parse(quoted);
12
+ }
13
+ export function apiData(payload, label) {
14
+ const p = payload;
15
+ if (p && ['401', '403'].includes(String(p.code)))
16
+ throw new CaptureError('E10_LOGIN_REQUIRED', '登录或权限已失效,请重新执行 auth set');
17
+ if (!p ||
18
+ typeof p !== 'object' ||
19
+ Array.isArray(p) ||
20
+ (p.code !== undefined && !['0', '200'].includes(String(p.code))) ||
21
+ p.status === false ||
22
+ p.fail === true ||
23
+ (typeof p.message === 'object' && p.message && String(p.message.code ?? '0') !== '0') ||
24
+ (p.data?.datajson && p.data.datajson.status !== true) ||
25
+ !('data' in p))
26
+ throw new CaptureError('API_RESPONSE_INVALID', `${label} 返回结构或业务状态异常`);
27
+ return p.data;
28
+ }
29
+ export class E10Client {
30
+ auth;
31
+ constructor(auth) {
32
+ this.auth = auth;
33
+ }
34
+ get origin() {
35
+ return new URL(this.auth.baseUrl).origin;
36
+ }
37
+ clean(value) {
38
+ const visit = (v) => {
39
+ if (typeof v === 'string')
40
+ return v.replaceAll(this.auth.eteamsId, '[redacted]');
41
+ if (Array.isArray(v))
42
+ return v.map(visit);
43
+ if (v && typeof v === 'object')
44
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [
45
+ k,
46
+ /^(cookie|eteamsid|authorization|password|access[_-]?token|client[_-]?secret)$/i.test(k)
47
+ ? '[redacted]'
48
+ : visit(x),
49
+ ]));
50
+ return v;
51
+ };
52
+ return visit(value);
53
+ }
54
+ async request(method, pathname, body, multipart = false, rawEnvelope = false) {
55
+ if (!pathname.startsWith('/') ||
56
+ pathname.startsWith('//') ||
57
+ new URL(pathname, this.origin).origin !== this.origin)
58
+ throw new CaptureError('API_ORIGIN_INVALID', '接口必须位于当前登录环境');
59
+ const headers = {
60
+ Cookie: `ETEAMSID=${this.auth.eteamsId}`,
61
+ Accept: 'application/json',
62
+ Origin: this.origin,
63
+ Referer: this.origin + '/',
64
+ };
65
+ let encoded;
66
+ if (body !== undefined) {
67
+ if (multipart) {
68
+ const data = new FormData();
69
+ for (const [key, value] of Object.entries(body))
70
+ data.set(key, String(value));
71
+ encoded = data;
72
+ }
73
+ else {
74
+ headers['Content-Type'] = 'application/json';
75
+ encoded = JSON.stringify(body);
76
+ }
77
+ }
78
+ const response = await fetch(new URL(pathname, this.origin), {
79
+ method,
80
+ headers,
81
+ body: encoded,
82
+ redirect: 'manual',
83
+ signal: AbortSignal.timeout(30000),
84
+ });
85
+ if ([301, 302, 303, 307, 308, 401, 403].includes(response.status))
86
+ throw new CaptureError('E10_LOGIN_REQUIRED', '登录或权限已失效,请重新执行 auth set');
87
+ if (!response.ok)
88
+ throw new CaptureError('API_HTTP', `${pathname.split('?')[0]} HTTP ${response.status}`, {
89
+ status: response.status,
90
+ });
91
+ let payload;
92
+ try {
93
+ payload = parseApiJson(await response.text());
94
+ }
95
+ catch {
96
+ throw new CaptureError('API_JSON_INVALID', '接口未返回有效 JSON');
97
+ }
98
+ return this.clean(rawEnvelope ? payload : apiData(payload, pathname.split('?')[0]));
99
+ }
100
+ async reference(fileId) {
101
+ if (!/^\d+$/.test(fileId))
102
+ throw new CaptureError('REFERENCE_INVALID', '布局文件 ID 无效');
103
+ const response = await fetch(new URL(`/api/file/remotedownload/${fileId}/download/true`, this.origin), {
104
+ headers: { Cookie: `ETEAMSID=${this.auth.eteamsId}` },
105
+ redirect: 'manual',
106
+ signal: AbortSignal.timeout(30000),
107
+ });
108
+ if ([301, 302, 303, 307, 308, 401, 403].includes(response.status))
109
+ throw new CaptureError('E10_LOGIN_REQUIRED', '布局下载需要重新登录');
110
+ if (!response.ok || !response.body)
111
+ throw new CaptureError('REFERENCE_HTTP', `布局下载 HTTP ${response.status}`);
112
+ const chunks = [];
113
+ let size = 0;
114
+ for await (const chunk of response.body) {
115
+ size += chunk.length;
116
+ if (size > 20 * 1024 * 1024)
117
+ throw new CaptureError('REFERENCE_TOO_LARGE', '布局参考超过 20 MiB');
118
+ chunks.push(chunk);
119
+ }
120
+ const html = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
121
+ if (!/<(?:html|body|div|table|style)\b/i.test(html))
122
+ throw new CaptureError('REFERENCE_INVALID', '下载内容不是布局 HTML');
123
+ return this.clean(html);
124
+ }
125
+ }
@@ -0,0 +1,7 @@
1
+ export declare function applicationInput(value: string): {
2
+ appId: string;
3
+ origin: undefined;
4
+ } | {
5
+ appId: string;
6
+ origin: string;
7
+ };
@@ -0,0 +1,16 @@
1
+ import { CaptureError, id } from './common.js';
2
+ export function applicationInput(value) {
3
+ if (/^\d+$/.test(value))
4
+ return { appId: id(value), origin: undefined };
5
+ let url;
6
+ try {
7
+ url = new URL(value);
8
+ }
9
+ catch {
10
+ throw new CaptureError('APP_INPUT_INVALID', '请提供应用 ID 或完整后台应用地址');
11
+ }
12
+ const match = url.pathname.match(/^\/sp\/ebdapp\/[^/]+\/(\d+)(?:\/.*)?$/);
13
+ if (!match || !['https:', 'http:'].includes(url.protocol) || url.username || url.password)
14
+ throw new CaptureError('APP_INPUT_INVALID', '应用地址应为 http(s)://环境/sp/ebdapp/入口/应用ID/...');
15
+ return { appId: id(match[1]), origin: url.origin };
16
+ }
@@ -0,0 +1,130 @@
1
+ import type { TaskState } from './model.js';
2
+ import { Store } from './store.js';
3
+ export declare function pack(store: Store, s: TaskState): Promise<{
4
+ forms: {
5
+ id: string;
6
+ menuId: string;
7
+ name: string;
8
+ kind: "list" | "nlist" | "layout" | "workflow";
9
+ workflowType: import("./model.js").WorkflowType | undefined;
10
+ objId: string | undefined;
11
+ viewId: string | undefined;
12
+ mode: "view" | "add" | "edit" | undefined;
13
+ layoutId: string | undefined;
14
+ status: string;
15
+ collection: "running" | "succeeded" | "failed" | undefined;
16
+ warnings: string[];
17
+ error: {
18
+ code: string;
19
+ message: string;
20
+ } | undefined;
21
+ html: string | undefined;
22
+ png: string | undefined;
23
+ pngKind: string;
24
+ htmlSha256: string | undefined;
25
+ pngSha256: string | undefined;
26
+ sourceSha256: string | undefined;
27
+ }[] | {
28
+ status: string;
29
+ html: string | undefined;
30
+ png: string | undefined;
31
+ pngKind: string;
32
+ htmlSha256: string | undefined;
33
+ pngSha256: string | undefined;
34
+ id: string;
35
+ name: string;
36
+ appId: string;
37
+ url: string;
38
+ }[] | undefined;
39
+ pages: {
40
+ html: {
41
+ sourceHtmlSha256?: string | undefined;
42
+ sha256?: string | undefined;
43
+ bytes?: number | undefined;
44
+ file?: string | undefined;
45
+ status: string;
46
+ reason: string;
47
+ sourceSha256?: undefined;
48
+ promptVersion?: undefined;
49
+ startedAt?: undefined;
50
+ finishedAt?: undefined;
51
+ error?: undefined;
52
+ } | {
53
+ sourceHtmlSha256?: string | undefined;
54
+ sha256: string | undefined;
55
+ bytes: number | undefined;
56
+ file: string | undefined;
57
+ status: "running" | "succeeded" | "failed";
58
+ sourceSha256: string;
59
+ promptVersion: 1 | 2 | 4 | 3 | 5;
60
+ startedAt: string;
61
+ finishedAt: string | undefined;
62
+ error: {
63
+ code: string;
64
+ message: string;
65
+ } | undefined;
66
+ reason?: undefined;
67
+ } | undefined;
68
+ file?: string;
69
+ preview?: string | undefined;
70
+ id: string;
71
+ name: string;
72
+ url: string;
73
+ status?: "running" | "succeeded" | "failed" | undefined;
74
+ history?: import("./model.js").Attempt[] | undefined;
75
+ warnings?: {
76
+ code: string;
77
+ message: string;
78
+ }[];
79
+ sha256?: string;
80
+ width?: number;
81
+ height?: number;
82
+ bytes?: number;
83
+ appId: string;
84
+ terminal: "PC" | "MOBILE";
85
+ type?: "PAGE" | "ECODE_HTML" | "ECODE_REACT";
86
+ }[];
87
+ menus?: import("./model.js").MenuItem[] | undefined;
88
+ skippedMenus?: import("./model.js").SkippedMenu[] | undefined;
89
+ formSummary?: {
90
+ total: number;
91
+ integrated: number;
92
+ placeholders: number;
93
+ collection: {
94
+ succeeded: number;
95
+ failed: number;
96
+ pending: number;
97
+ };
98
+ html: {
99
+ succeeded: number;
100
+ failed: number;
101
+ running: number;
102
+ pending: number;
103
+ };
104
+ } | {
105
+ total: number;
106
+ integrated: number;
107
+ placeholders: number;
108
+ collection?: undefined;
109
+ html?: undefined;
110
+ } | undefined;
111
+ schema: number;
112
+ appId: string;
113
+ origin: string | undefined;
114
+ createdAt: string;
115
+ settings: import("./model.js").Settings;
116
+ status: string;
117
+ total: number;
118
+ succeeded: number;
119
+ failed: number;
120
+ html: {
121
+ required: boolean;
122
+ succeeded: number;
123
+ failed: number;
124
+ running: number;
125
+ pending: number;
126
+ };
127
+ appName: string | undefined;
128
+ entry: string | undefined;
129
+ templateVersion: number | undefined;
130
+ }>;
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs/promises';
2
+ import { renameWithRetry } from './runtime-support.mjs';
3
+ import { createWriteStream } from 'node:fs';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { ZipFile } from 'yazl';
6
+ import { atomicJson, CaptureError, fileDigest } from './common.js';
7
+ import { buildSite } from './site.js';
8
+ export async function pack(store, s) {
9
+ const status = await store.status(s);
10
+ if (!s.pages ||
11
+ status.pending ||
12
+ status.collection.pending ||
13
+ status.temporaryRecords.pending ||
14
+ status.state === 'DISCOVER')
15
+ throw new CaptureError('CAPTURE_INCOMPLETE', '菜单发现、表单采集或页面截图未完成,请执行 run');
16
+ if (status.html.pending)
17
+ throw new CaptureError('HTML_INCOMPLETE', '仍有待生成或未接收的 HTML,先执行 html next 并由宿主 AI 完成');
18
+ const partial = !!(status.failed ||
19
+ status.collection.failed ||
20
+ (s.htmlRequired && status.html.failed));
21
+ delete s.archive;
22
+ await store.save(s);
23
+ const site = s.siteRequired ? await buildSite(store, s, status) : undefined;
24
+ const manifest = {
25
+ schema: s.menuRequired ? 4 : 3,
26
+ appId: s.appId,
27
+ origin: s.environment?.origin,
28
+ createdAt: new Date().toISOString(),
29
+ settings: s.settings,
30
+ status: partial ? 'PARTIAL' : 'DONE',
31
+ total: status.total,
32
+ succeeded: status.succeeded,
33
+ failed: status.failed,
34
+ html: status.html,
35
+ appName: s.appName,
36
+ entry: site ? 'index.html' : undefined,
37
+ templateVersion: site?.templateVersion,
38
+ ...(s.menuRequired
39
+ ? { menus: s.menus, skippedMenus: s.skippedMenus, formSummary: status.forms }
40
+ : {}),
41
+ forms: s.menuRequired
42
+ ? (s.formPages || []).map((f, i) => {
43
+ const item = site?.items.find((item) => item.kind === 'form' && item.id === f.id);
44
+ return {
45
+ id: f.id,
46
+ menuId: f.menuId,
47
+ name: f.name,
48
+ kind: f.kind,
49
+ workflowType: f.workflowType,
50
+ objId: status.formResults[i]?.objId,
51
+ viewId: f.viewId,
52
+ mode: f.mode,
53
+ layoutId: f.layoutId,
54
+ status: item?.available ? 'succeeded' : 'failed',
55
+ collection: status.formResults[i]?.status,
56
+ warnings: status.formResults[i]?.warnings || [],
57
+ error: status.formResults[i]?.error || status.formHtmlResults[i]?.error,
58
+ html: item?.html,
59
+ png: item?.png,
60
+ pngKind: item?.available ? 'generated' : 'placeholder',
61
+ htmlSha256: item && site?.metadata.get(item.html)?.sha256,
62
+ pngSha256: item?.png && site?.metadata.get(item.png)?.sha256,
63
+ sourceSha256: status.formResults[i]?.sha256,
64
+ };
65
+ })
66
+ : s.forms?.map((f) => ({
67
+ ...f,
68
+ status: 'not-integrated',
69
+ html: site?.items.find((i) => i.kind === 'form' && i.id === f.id)?.html,
70
+ png: site?.items.find((i) => i.kind === 'form' && i.id === f.id)?.png,
71
+ pngKind: 'placeholder',
72
+ htmlSha256: site?.metadata.get(site.items.find((i) => i.kind === 'form' && i.id === f.id).html)?.sha256,
73
+ pngSha256: site?.metadata.get(site.items.find((i) => i.kind === 'form' && i.id === f.id).png)?.sha256,
74
+ })),
75
+ pages: s.pages.map((p, i) => {
76
+ const r = status.htmlResults[i];
77
+ const html = !s.htmlRequired
78
+ ? undefined
79
+ : status.results[i]?.status !== 'succeeded'
80
+ ? { status: 'skipped', reason: '截图失败,未生成 HTML' }
81
+ : r && {
82
+ status: r.status,
83
+ file: r.file,
84
+ sha256: r.sha256,
85
+ bytes: r.bytes,
86
+ sourceSha256: r.sourceSha256,
87
+ promptVersion: r.promptVersion,
88
+ startedAt: r.startedAt,
89
+ finishedAt: r.finishedAt,
90
+ error: r.error,
91
+ };
92
+ const preview = site?.items.find((item) => item.kind === 'page' && item.id === p.id);
93
+ return {
94
+ ...p,
95
+ ...status.results[i],
96
+ ...(preview ? { file: preview.png, preview: preview.html } : {}),
97
+ html: html && {
98
+ ...html,
99
+ ...(preview && 'file' in html && html.file
100
+ ? {
101
+ file: preview.html,
102
+ ...site.metadata.get(preview.html),
103
+ sourceHtmlSha256: html.sha256,
104
+ }
105
+ : {}),
106
+ },
107
+ };
108
+ }),
109
+ };
110
+ await fs.mkdir(store.artifacts, { recursive: true });
111
+ await atomicJson(store.artifact('manifest.json'), manifest);
112
+ const siteReceipt = await site?.finish(manifest);
113
+ const filename = `应用页面-${s.appId}.zip`;
114
+ const temp = store.artifact(`${filename}.tmp`);
115
+ const zip = new ZipFile();
116
+ if (siteReceipt)
117
+ for (const file of siteReceipt.files)
118
+ zip.addFile(store.artifact(file.path), file.path.slice('site/'.length));
119
+ else
120
+ zip.addBuffer(Buffer.from(JSON.stringify(manifest, null, 2)), 'manifest.json');
121
+ for (const [i, r] of status.results.entries()) {
122
+ if (!site && r?.status === 'succeeded' && r.file)
123
+ zip.addFile(store.artifact(r.file), r.file);
124
+ const html = status.htmlResults[i];
125
+ if (!site && s.htmlRequired && (await store.verifiedHtml(html, r)))
126
+ zip.addFile(store.artifact(html.file), html.file);
127
+ for (const a of r?.history || [])
128
+ if (a.diagnostic) {
129
+ await fs.access(store.artifact(a.diagnostic));
130
+ zip.addFile(store.artifact(a.diagnostic), a.diagnostic);
131
+ }
132
+ }
133
+ try {
134
+ const writing = pipeline(zip.outputStream, createWriteStream(temp, { mode: 0o600 }));
135
+ zip.end();
136
+ await writing;
137
+ await renameWithRetry(temp, store.artifact(filename));
138
+ }
139
+ finally {
140
+ await fs.rm(temp, { force: true });
141
+ }
142
+ s.archive = {
143
+ path: filename,
144
+ sha256: await fileDigest(store.artifact(filename)),
145
+ completedAt: new Date().toISOString(),
146
+ partial,
147
+ site: siteReceipt,
148
+ };
149
+ await store.save(s);
150
+ return manifest;
151
+ }
@@ -0,0 +1,15 @@
1
+ import type { TaskState } from './model.js';
2
+ import type { E10AuthContext } from './vendor/environment-auth.js';
3
+ import { Store } from './store.js';
4
+ export declare function launchChrome(): Promise<{
5
+ browser: import("playwright-core").Browser;
6
+ pid: number;
7
+ version: string;
8
+ directory: string;
9
+ close: () => Promise<void>;
10
+ }>;
11
+ export declare function capture(store: Store, state: TaskState, auth: E10AuthContext, options?: {
12
+ retryFailed?: boolean;
13
+ requireEntrance?: boolean;
14
+ progress?: (event: unknown) => void;
15
+ }): Promise<void>;