e10-ebuilder-prototype 0.5.0 → 0.5.4

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # e10-ebuilder-prototype
2
2
 
3
3
  根据 E10 **应用 ID 或后台应用地址生成应用原型,包含已发布菜单中的页面、建模列表和流程页面**。
4
- 当前版本 0.5.0。保持我们的 TypeScript CLI 为主:CLI 管登录、菜单发现、配置采集、截图、任务队列、校验、恢复与打包;宿主 AI 根据完整截图或建模配置生成独立 HTML。CLI 不调用 LLM,不要求额外模型 API Key,也不调用 ui-code-agent 生成。
4
+ 版本 0.5.4,默认并发 6。保持我们的 TypeScript CLI 为主:CLI 管登录、菜单发现、配置采集、截图、任务队列、校验、恢复与打包;宿主 AI 根据完整截图或建模配置生成独立 HTML。CLI 不调用 LLM,不要求额外模型 API Key,也不调用 ui-code-agent 生成。
5
5
 
6
6
  ## 范围与交付
7
7
 
@@ -31,7 +31,7 @@ ZIP 名为 `应用页面-<appId>.zip`,位于任务目录 artifacts/;本地
31
31
  需要 Node >=22.5、npm 和已安装 Google Chrome,playwright-core 固定 1.62.1,不下载浏览器。
32
32
 
33
33
  ```sh
34
- npm install -g e10-ebuilder-prototype@0.5.0 --registry=https://registry.npmjs.org/
34
+ npm install -g e10-ebuilder-prototype@0.5.4 --registry=https://registry.npmjs.org/
35
35
  e10-ebuilder-prototype auth set --base-url https://your-e10.example.com --eteamsid-stdin
36
36
  e10-ebuilder-prototype run --app-id 1234567890123456789 --dir ./work/app-demo --json
37
37
  # 或使用后台应用地址:
@@ -45,17 +45,28 @@ e10-ebuilder-prototype run --app-url "https://your-e10.example.com/sp/ebdapp/det
45
45
  `run` 完成确定性工作后停在 HTML,由宿主继续:
46
46
 
47
47
  ```sh
48
- e10-ebuilder-prototype html next --dir ./work/app-demo --json
49
- # 宿主按 jobsprompt 写入各自 outputPath,并做视觉/交互检查
50
- e10-ebuilder-prototype html accept --dir ./work/app-demo --kind page --page-id "<ID>" --token "<TOKEN>" --json
48
+ e10-ebuilder-prototype html next --dir ./work/app-demo --brief --json
49
+ # 宿主读取各 jobpromptPath,写完整草稿;CLI 离线渲染后实际看图
50
+ e10-ebuilder-prototype html inspect --dir ./work/app-demo --kind page --page-id "<ID>" --token "<TOKEN>" --json
51
+ e10-ebuilder-prototype html accept --dir ./work/app-demo --kind page --page-id "<ID>" --token "<TOKEN>" --refill --brief --json
51
52
  e10-ebuilder-prototype html accept --dir ./work/app-demo --kind form --page-id "<菜单键>" --token "<TOKEN>" --json
52
53
  e10-ebuilder-prototype pack --dir ./work/app-demo --json
53
54
  ```
54
55
 
55
- 同一个生成队列最多 4 个任务,可按用户需求 --concurrency 1..8。page 输入 screenshotPath;form 输入 sourcePath,附带 contextPath 分片索引、guidePath 完整规范、runtimePath 固定存储脚本及 navigationPath 已发布目标映射。一个协调者串行 accept/fail/next,任务只写自己的草稿;宿主支持且允许并行时补位执行,否则如实采用顺序生成。
56
+ 同一个生成队列默认最多 6 个任务,可按用户需求 --concurrency 1..8。page 输入 screenshotPath;form 输入 sourcePath,附带 contextPath 分片索引、guidePath 完整规范、runtimePath 固定存储脚本及 navigationPath 已发布目标映射。一个协调者串行 accept/fail/next,任务只写自己的草稿;宿主支持且允许并行时补位执行,否则如实采用顺序生成。
56
57
 
57
58
  生成文件为真实 DOM、内联 CSS/JS/SVG,无外部资源或后台调用。页面忠实还原视觉结构,空数据用一致 Mock 内容填充。表单按当前模式/字段/选项/按钮及布局依据生成,固定种子约 20 条主记录、明细每条 2–5 行;所有统计由数据推导。完整规范随 CLI 打包,不能用“能打开表格”代替验收。
58
59
 
60
+ ## 独立部署检测兼容(0.5.1)
61
+
62
+ isIndependentDeploy 请求失败时,默认按非独立部署继续采集,并记录兼容提示。需要部署路由的接口使用 `/api/ebuilder/form/`;检测成功返回 true 时使用 `/api/ebuilder{appId}/form/`。后续配置接口的真实错误照常报告,采集未成功不会标为完成。
63
+
64
+ ## 传统列表配置(0.5.2)
65
+
66
+ 基础设置、显示列、搜索、统计和固定条件统一使用 `/api/bs/ebuilder/form/` 接口,URL 在原参数基础上增加小写 `appid`。这些接口固定走后台路径,需要账号具备应用后台访问权限,不受独立部署检测结果影响。列表初始化、排序、按钮等接口沿用各自路径。
67
+
68
+ 固定条件接口成功但未返回 `data` 时按未配置条件处理;权限、HTTP 和业务错误仍明确报错。已有失败任务可在升级后执行 `retry --dir <原任务目录>`。
69
+
59
70
  ## 参考布局与临时记录
60
71
 
61
72
  默认只读业务记录:先读取至多一条现有记录,若可用则请求导出 HTML 作为私有布局参考;没有参考时根据全部自定义字段生成布局,报告缺项。导出可能在源站生成参考文件。
@@ -81,7 +92,7 @@ e10-ebuilder-prototype pack --dir ./work/app-demo --json
81
92
 
82
93
  ## 截图和资源限制
83
94
 
84
- PC 默认 1440×900,MOBILE-only 页面 390×844;默认并发 4。页面初始渲染上限 10 秒、稳定窗口 800ms;超时本轮失败,其他可恢复失败额外重试一次。就绪后立即继续,不固定睡眠。
95
+ PC 默认 1440×900,MOBILE-only 页面 390×844;默认并发 6。页面初始渲染上限 10 秒、稳定窗口 800ms;超时本轮失败,其他可恢复失败额外重试一次。就绪后立即继续,不固定睡眠。
85
96
  只用页面组件请求、可见加载、字体/图片、主体内容与稳定性判断;全局后台请求不阻塞。随后滚动检查、编码和清理另有边界,详见协议。
86
97
 
87
98
  截图覆盖当前已加载/可滚动内容,不自动点击标签或分页。承载 EB 主体的 iframe 完整滚动展开,普通内嵌网站保留设计视口。稳定业务错误可原样截图并带 warning,未知空白、超时、身份失效、内容丢失仍失败。源站错误截图不等于业务功能健康。
@@ -111,3 +122,16 @@ Windows 使用原生 Node,无需 Bash/WSL/Python;支持 WorkBuddy 内置 npm
111
122
  流程列表的列、标签、筛选和按钮来自内置固定原型模板;目录接口提供真实流程名称和关联表单,不读取真实流程实例/审批权限。宿主按 CLI 的 workflow-guide 生成右侧页面,固定 E10WorkflowStore 负责本地已读、批量提交、草稿续填、收藏/关注、实例评论以及关联记录联合保存。所有流程动作仅修改本地模拟数据。流程仍使用 kind=form 队列和 form/<菜单键>/ 交付路径,job.workflowType 区分流程类别。
112
123
 
113
124
  完整流程协议见 [PROTOCOL.md](docs/PROTOCOL.md)。
125
+
126
+ ## WorkBuddy 生成效率(0.5.4)
127
+
128
+ 首次 status 未就绪时仅提示“首次使用需要初始化工作环境,大概需要几分钟,完成后自动继续。”
129
+ ensure 保留后台进度与耗时,成功后自动继续,后续正常使用不再提示初始化或依赖名称。不要给 CLI 命令附加 tail 或吞掉退出码。
130
+
131
+ 页面先看完整原图再写完整首版,不要求逐像素测量。html inspect 统一生成离线校对截图,
132
+ 相同源码与完整截图可复用;宿主实际看图并集中修正,最多两轮视觉微调。业务缺项或运行错误仍需处理。
133
+
134
+ WorkBuddy 子任务显式使用主任务当前实际模型,保留可继承的思考配置,不写死某个模型。
135
+ 后台生成完成一个接收一个,accept/fail --refill --brief 同时返回新任务;promptPath 将页面细节
136
+ 留在对应子任务。连接失败确认旧写者已退出后恢复一次,已有草稿可继续;持续失败明确报告。
137
+ 并发额度不是实际活跃数,也不保证线性提速,结论以同输入同模型的实际验收计时为准。
package/dist/api.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { E10AuthContext } from './vendor/environment-auth.js';
2
2
  /** Preserve integer identifiers before JSON.parse can round them. */
3
3
  export declare function parseApiJson(text: string): unknown;
4
- export declare function apiData(payload: unknown, label: string): any;
4
+ export declare function apiData(payload: unknown, label: string, options?: {
5
+ allowMissingData?: boolean;
6
+ }): any;
5
7
  export declare class E10Client {
6
8
  private auth;
7
9
  constructor(auth: E10AuthContext);
package/dist/api.js CHANGED
@@ -10,7 +10,7 @@ export function parseApiJson(text) {
10
10
  });
11
11
  return JSON.parse(quoted);
12
12
  }
13
- export function apiData(payload, label) {
13
+ export function apiData(payload, label, options = {}) {
14
14
  const p = payload;
15
15
  if (p && ['401', '403'].includes(String(p.code)))
16
16
  throw new CaptureError('E10_LOGIN_REQUIRED', '登录或权限已失效,请重新执行 auth set');
@@ -22,7 +22,7 @@ export function apiData(payload, label) {
22
22
  p.fail === true ||
23
23
  (typeof p.message === 'object' && p.message && String(p.message.code ?? '0') !== '0') ||
24
24
  (p.data?.datajson && p.data.datajson.status !== true) ||
25
- !('data' in p))
25
+ (!('data' in p) && (!options.allowMissingData || !['0', '200'].includes(String(p.code)))))
26
26
  throw new CaptureError('API_RESPONSE_INVALID', `${label} 返回结构或业务状态异常`);
27
27
  return p.data;
28
28
  }
package/dist/archive.d.ts CHANGED
@@ -54,9 +54,9 @@ export declare function pack(store: Store, s: TaskState): Promise<{
54
54
  sha256: string | undefined;
55
55
  bytes: number | undefined;
56
56
  file: string | undefined;
57
- status: "running" | "succeeded" | "failed";
57
+ status: "running" | "succeeded" | "failed" | "pending";
58
58
  sourceSha256: string;
59
- promptVersion: 1 | 2 | 4 | 3 | 5;
59
+ promptVersion: 1 | 2 | 6 | 3 | 4 | 5 | 7 | 8 | 9;
60
60
  startedAt: string;
61
61
  finishedAt: string | undefined;
62
62
  error: {
package/dist/common.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.5.4";
2
2
  export declare class CaptureError extends Error {
3
3
  code: string;
4
4
  details?: unknown | undefined;
package/dist/common.js CHANGED
@@ -3,7 +3,7 @@ import { renameWithRetry } from './runtime-support.mjs';
3
3
  import { createReadStream } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
- export const VERSION = '0.5.0';
6
+ export const VERSION = '0.5.4';
7
7
  export class CaptureError extends Error {
8
8
  code;
9
9
  details;
@@ -1,3 +1,3 @@
1
1
  import type { Store } from './store.js';
2
- /** Generated source views are disposable; regenerate from the verified canonical input on resume. */
2
+ /** Bounded, lossless views. Fixed metadata is grouped, never reinterpreted or discarded. */
3
3
  export declare function formContext(store: Store, key: string): Promise<string>;
@@ -1,10 +1,16 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { fileDigest } from './common.js';
3
+ import { digest, fileDigest, CaptureError } from './common.js';
4
+ import { formOptionData } from './form-options.js';
4
5
  import { formInputPath } from './forms.js';
5
6
  import { navigationKey } from './menus.js';
7
+ const limit = 18000;
8
+ const lineLimit = 1800; // WorkBuddy Read truncates long individual lines, not only large files.
6
9
  function* parts(value, location) {
7
- if (JSON.stringify(value).length <= 18000) {
10
+ if (JSON.stringify(location).length > lineLimit - 96)
11
+ throw new CaptureError('FORM_CONTEXT_LIMIT', '配置属性路径超过宿主可完整读取的单行长度');
12
+ const encoded = JSON.stringify({ entries: [{ location, value }] }, null, 2);
13
+ if (encoded.length <= limit && encoded.split('\n').every((line) => line.length <= lineLimit)) {
8
14
  yield { location, value };
9
15
  return;
10
16
  }
@@ -17,34 +23,76 @@ function* parts(value, location) {
17
23
  yield* parts(item, `${location}.${key}`);
18
24
  }
19
25
  else if (typeof value === 'string') {
20
- for (let offset = 0; offset < value.length; offset += 6000)
26
+ for (let offset = 0; offset < value.length;) {
27
+ let low = offset + 1, high = Math.min(value.length, offset + lineLimit - 32);
28
+ while (low < high) {
29
+ const middle = Math.ceil((low + high) / 2);
30
+ if (JSON.stringify(value.slice(offset, middle)).length <= lineLimit - 32)
31
+ low = middle;
32
+ else
33
+ high = middle - 1;
34
+ }
35
+ let end = low;
36
+ if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]))
37
+ end--;
21
38
  yield {
22
- location: `${location} (characters ${offset}:${Math.min(value.length, offset + 6000)})`,
23
- value: value.slice(offset, offset + 6000),
39
+ location: `${location} (characters ${offset}:${end})`,
40
+ value: value.slice(offset, end),
24
41
  };
42
+ offset = end;
43
+ }
25
44
  }
26
45
  else
27
46
  yield { location, value };
28
47
  }
29
- /** Generated source views are disposable; regenerate from the verified canonical input on resume. */
48
+ /** Bounded, lossless views. Fixed metadata is grouped, never reinterpreted or discarded. */
30
49
  export async function formContext(store, key) {
31
- const source = formInputPath(store, key), input = JSON.parse(await fs.readFile(source, 'utf8'));
32
- const folder = path.join(store.meta, 'form-context', navigationKey(key), await fileDigest(source));
50
+ const source = formInputPath(store, key), sourceSha256 = await fileDigest(source);
51
+ const folder = path.join(store.meta, 'form-context', navigationKey(key), sourceSha256);
52
+ const indexPath = path.join(folder, 'index.json');
53
+ const previous = await fs
54
+ .readFile(indexPath, 'utf8')
55
+ .then(JSON.parse)
56
+ .catch(() => null);
57
+ if (previous?.schema === 4 &&
58
+ previous.sourceSha256 === sourceSha256 &&
59
+ Array.isArray(previous.fragments)) {
60
+ const valid = await Promise.all(previous.fragments.map(async (part) => typeof part.path === 'string' &&
61
+ path.basename(part.path) === part.path &&
62
+ (await fileDigest(path.join(folder, part.path)).catch(() => '')) === part.sha256));
63
+ if (valid.length && valid.every(Boolean))
64
+ return indexPath;
65
+ }
66
+ const input = JSON.parse(await fs.readFile(source, 'utf8'));
67
+ const options = formOptionData(input);
33
68
  await fs.mkdir(folder, { recursive: true });
34
- const index = [];
35
- for (const [category, value] of Object.entries(input)) {
36
- let i = 0;
69
+ const fragments = [];
70
+ let batch = [];
71
+ const flush = async () => {
72
+ if (!batch.length)
73
+ return;
74
+ const filename = `context-${fragments.length + 1}.json`;
75
+ const bytes = JSON.stringify({ entries: batch }, null, 2);
76
+ await fs.writeFile(path.join(folder, filename), bytes, { mode: 0o600 });
77
+ fragments.push({
78
+ path: filename,
79
+ locations: batch.map((part) => part.location),
80
+ sha256: digest(bytes),
81
+ });
82
+ batch = [];
83
+ };
84
+ for (const [category, value] of Object.entries(options.context)) {
37
85
  for (const part of parts(value, category)) {
38
- const filename = `${category.replace(/[^a-zA-Z0-9_-]/g, '_')}-${++i}.json`;
39
- await fs.writeFile(path.join(folder, filename), JSON.stringify(part, null, 2), {
40
- mode: 0o600,
41
- });
42
- index.push({ path: filename, location: part.location });
86
+ if (batch.length && JSON.stringify({ entries: [...batch, part] }, null, 2).length > limit)
87
+ await flush();
88
+ batch.push(part);
43
89
  }
44
90
  }
45
- const indexPath = path.join(folder, 'index.json');
91
+ await flush();
46
92
  await fs.writeFile(indexPath, JSON.stringify({
93
+ schema: 4,
47
94
  source,
95
+ sourceSha256,
48
96
  appId: input.appId,
49
97
  objId: input.objId,
50
98
  menu: input.page,
@@ -52,7 +100,9 @@ export async function formContext(store, key) {
52
100
  customFieldCount: input.fields?.filter((f) => !f.system).length,
53
101
  layoutSource: input.layout?.source,
54
102
  warnings: input.warnings,
55
- fragments: index,
103
+ optionDatasets: options.descriptions,
104
+ format: 'Each fragment contains entries [{location,value}]; read each fragment once. Long strings use character ranges and concatenate in order. A {$e10Options:id,count,sample} replaces a large options array: call E10FormOptions.get(id) in the generated HTML to retrieve the complete original array. sample describes shape only; never use it as the full choices. CLI injects all dataset values during inspect/accept. Original input retains every property; host-readable lines are bounded.',
105
+ fragments,
56
106
  }, null, 2), { mode: 0o600 });
57
107
  return indexPath;
58
108
  }
@@ -0,0 +1,12 @@
1
+ /** Large static choice catalogs travel as data, never as model-authored business code. */
2
+ export declare function formOptionData(input: any): {
3
+ context: any;
4
+ datasets: Record<string, any[]>;
5
+ descriptions: {
6
+ id: string;
7
+ count: number;
8
+ locations: string[];
9
+ }[];
10
+ };
11
+ export declare function formOptionsRuntime(datasets: Record<string, any[]>): string;
12
+ export declare function attachFormOptions(source: string, datasets: Record<string, any[]>): string;
@@ -0,0 +1,54 @@
1
+ import { digest } from './common.js';
2
+ /** Large static choice catalogs travel as data, never as model-authored business code. */
3
+ export function formOptionData(input) {
4
+ const datasets = Object.create(null);
5
+ const descriptions = new Map();
6
+ const sample = (value, depth = 0) => {
7
+ if (Array.isArray(value))
8
+ return depth >= 2
9
+ ? `[${value.length} items]`
10
+ : value.slice(0, 2).map((v) => sample(v, depth + 1));
11
+ if (value && typeof value === 'object')
12
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sample(v, depth + 1)]));
13
+ return typeof value === 'string' && value.length > 160 ? value.slice(0, 160) + '…' : value;
14
+ };
15
+ const visit = (value, location, key = '') => {
16
+ if (key === 'options' && Array.isArray(value) && JSON.stringify(value).length > 8000) {
17
+ const id = digest(JSON.stringify(value));
18
+ datasets[id] = value;
19
+ if (!descriptions.has(id))
20
+ descriptions.set(id, { id, count: value.length, locations: [] });
21
+ descriptions.get(id).locations.push(location);
22
+ return { $e10Options: id, count: value.length, sample: sample(value) };
23
+ }
24
+ if (Array.isArray(value))
25
+ return value.map((v, i) => visit(v, `${location}[${i}]`));
26
+ if (value && typeof value === 'object')
27
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, visit(v, location ? `${location}.${k}` : k, k)]));
28
+ return value;
29
+ };
30
+ return { context: visit(input, ''), datasets, descriptions: [...descriptions.values()] };
31
+ }
32
+ function installOptions(datasets) {
33
+ Object.defineProperty(window, 'E10FormOptions', {
34
+ value: Object.freeze({
35
+ get: (id) => {
36
+ if (!Object.prototype.hasOwnProperty.call(datasets, id))
37
+ throw new Error('本菜单没有对应的选项数据集');
38
+ return JSON.parse(JSON.stringify(datasets[id]));
39
+ },
40
+ }),
41
+ configurable: true,
42
+ });
43
+ }
44
+ export function formOptionsRuntime(datasets) {
45
+ const json = JSON.stringify(JSON.stringify(datasets))
46
+ .replaceAll('<', '\\u003c')
47
+ .replaceAll('\u2028', '\\u2028')
48
+ .replaceAll('\u2029', '\\u2029');
49
+ return `<script id="e10-form-options">(${installOptions.toString()})(JSON.parse(${json}));</script>`;
50
+ }
51
+ export function attachFormOptions(source, datasets) {
52
+ const clean = source.replace(/<script\b[^>]*\bid\s*=\s*["']e10-form-options["'][^>]*>[\s\S]*?<\/script\s*>/gi, '');
53
+ return clean.replace(/<head(?:\s[^>]*)?>/i, (match) => match + formOptionsRuntime(datasets));
54
+ }
package/dist/forms.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { atomicJson, CaptureError, errorInfo, fileDigest, mapLimit } from './common.js';
4
- import { E10Client, parseApiJson } from './api.js';
4
+ import { E10Client, apiData, parseApiJson } from './api.js';
5
5
  import { recoverTemporaryRecords, withTemporaryRecord, SeedUnavailable, temporarySummary, } from './temporary-records.js';
6
6
  import { navigationKey } from './menus.js';
7
7
  import { APPROVAL_PATH, normalizeWorkflows, workflowPreset, workflowType } from './workflows.js';
@@ -45,6 +45,30 @@ export function configObject(value) {
45
45
  throw new CaptureError('FORM_CONFIG_INVALID', '表单配置不是对象');
46
46
  return result;
47
47
  }
48
+ function listConditions(conditionId, raw) {
49
+ let rootData = typeof raw === 'string'
50
+ ? raw
51
+ : raw && Object.hasOwn(raw, 'rootdata')
52
+ ? raw.rootdata
53
+ : raw?.rootData;
54
+ if (typeof rootData === 'string' && rootData.trim()) {
55
+ try {
56
+ rootData = parseApiJson(rootData);
57
+ }
58
+ catch {
59
+ // Preserve unrecognized condition text for the host, as the source collector does.
60
+ }
61
+ }
62
+ const configured = rootData != null &&
63
+ rootData !== '' &&
64
+ !(typeof rootData === 'object' && Object.keys(rootData).length === 0);
65
+ return {
66
+ conditionId: conditionId == null ? '' : String(conditionId),
67
+ configured,
68
+ rootData: configured ? rootData : {},
69
+ raw: raw ?? {},
70
+ };
71
+ }
48
72
  const systems = new Set([
49
73
  '1:name',
50
74
  '2:creator',
@@ -189,7 +213,13 @@ export async function collectForms(store, state, auth, retryFailed = false) {
189
213
  }
190
214
  if (!jobs.length && !(await temporarySummary(store)).pending)
191
215
  return;
192
- const independent = await client.request('GET', `/api/ebuilder/form/app/isIndependentDeploy?appId=${state.appId}`);
216
+ const deploymentWarnings = [];
217
+ const independent = await client
218
+ .request('GET', `/api/ebuilder/form/app/isIndependentDeploy?appId=${state.appId}`)
219
+ .catch(() => {
220
+ deploymentWarnings.push('isIndependentDeploy 请求失败,已默认按非独立部署应用采集');
221
+ return false;
222
+ });
193
223
  if (typeof independent !== 'boolean')
194
224
  throw new CaptureError('DEPLOYMENT_INVALID', '独立部署状态不是布尔值');
195
225
  const prefix = '/api/ebuilder' + (independent ? state.appId : '');
@@ -395,6 +425,7 @@ export async function collectForms(store, state, auth, retryFailed = false) {
395
425
  status: 'running',
396
426
  attempt: (previous?.attempt || 0) + 1,
397
427
  updatedAt: new Date().toISOString(),
428
+ ...(deploymentWarnings.length ? { warnings: [...deploymentWarnings] } : {}),
398
429
  };
399
430
  await atomicJson(collectionPath(store, item.id), receipt);
400
431
  try {
@@ -443,7 +474,9 @@ export async function collectForms(store, state, auth, retryFailed = false) {
443
474
  };
444
475
  for (const button of buttons.items)
445
476
  button.pageButtonRef = JSON.stringify([item.id, button.buttonRef]);
446
- const warnings = [...new Set(forms.flatMap((f) => f.warnings))];
477
+ const warnings = [
478
+ ...new Set([...deploymentWarnings, ...forms.flatMap((f) => f.warnings)]),
479
+ ];
447
480
  if (catalog.skipped.length)
448
481
  warnings.push(`流程目录跳过 ${catalog.skipped.length} 个非 EB 或归属/配置无效的流程;详见私有 workflowCatalog.skipped`);
449
482
  if (!catalog.templates.length)
@@ -498,19 +531,26 @@ export async function collectForms(store, state, auth, retryFailed = false) {
498
531
  throw new CaptureError('FORM_ID_MISMATCH', '菜单与列表表单不匹配');
499
532
  objId = resolved;
500
533
  const query = `apid=${state.appId}&tpaid=${item.viewId}&objId=${objId}`;
534
+ const bs = '/api/bs/ebuilder/form';
535
+ const bsQuery = `${query}&appid=${state.appId}`;
501
536
  const routes = {
502
- basicSettings: `/list/getBaseInfo/${item.viewId}?${query}`,
503
- displayColumns: `/list/getListFields/${item.viewId}?${query}&isVirtualForm=false&clientType=PC`,
504
- sorting: `/viewlist/getViewOrder?${query}&viewType=tableView&viewId=${item.viewId}&isVirtualForm=false`,
505
- search: `/listFilter/getSearchFilters/${item.viewId}?${query}&isVirtualForm=false&viewType=tableView`,
506
- statistics: `/listStatistics/getStatisticsList/${item.viewId}?${query}&isVirtualForm=false`,
507
- buttons: `/list/getListButtons/${item.viewId}?passid=${objId}`,
537
+ basicSettings: `${bs}/list/getBaseInfo/${item.viewId}?${bsQuery}`,
538
+ displayColumns: `${bs}/list/getListFields/${item.viewId}?${bsQuery}&isVirtualForm=false&clientType=PC`,
539
+ sorting: `${prefix}/form/viewlist/getViewOrder?${query}&viewType=tableView&viewId=${item.viewId}&isVirtualForm=false`,
540
+ search: `${bs}/listFilter/getSearchFilters/${item.viewId}?${bsQuery}&isVirtualForm=false&viewType=tableView`,
541
+ statistics: `${bs}/listStatistics/getStatisticsList/${item.viewId}?${bsQuery}&isVirtualForm=false`,
542
+ buttons: `${prefix}/form/list/getListButtons/${item.viewId}?passid=${objId}`,
508
543
  };
509
544
  if (context.conditionId && String(context.conditionId) !== '0')
510
- routes.conditions = `/plugin/conditionEditor/getById?apid=${state.appId}&id=${decimal(context.conditionId, '条件')}&type=search&objId=${objId}`;
545
+ routes.conditions = `${bs}/plugin/conditionEditor/getById?apid=${state.appId}&id=${decimal(context.conditionId, '条件')}&type=search&objId=${objId}&appid=${state.appId}`;
511
546
  const responses = await Promise.allSettled(Object.entries(routes).map(async ([key, route]) => [
512
547
  key,
513
- await cached(route, () => get(route)),
548
+ await cached(route, async () => {
549
+ if (key !== 'conditions')
550
+ return client.request('GET', route);
551
+ const payload = await client.request('GET', route, undefined, false, true);
552
+ return apiData(payload, route.split('?')[0], { allowMissingData: true });
553
+ }),
514
554
  ]));
515
555
  for (const result of responses)
516
556
  if (result.status === 'rejected')
@@ -518,6 +558,7 @@ export async function collectForms(store, state, auth, retryFailed = false) {
518
558
  const config = Object.fromEntries(responses.map((r) => r.value));
519
559
  listButtons = config.buttons;
520
560
  delete config.buttons;
561
+ config.conditions = listConditions(context.conditionId, config.conditions);
521
562
  list = { kind: 'list', listId: item.viewId, config };
522
563
  }
523
564
  if (!objId || !sourceForms.has(objId))
@@ -526,7 +567,7 @@ export async function collectForms(store, state, auth, retryFailed = false) {
526
567
  receipt.objId = objId;
527
568
  const formId = objId;
528
569
  const shared = await collectObject(formId, true);
529
- const warnings = [...shared.warnings];
570
+ const warnings = [...deploymentWarnings, ...shared.warnings];
530
571
  const formButtons = structuredClone(shared.contexts[item.kind === 'layout' ? item.id : ''] || shared.contexts['']);
531
572
  if (formButtons.view.status !== 'ready' || formButtons.edit.status !== 'ready')
532
573
  warnings.push('查看/编辑模式按钮缺少记录上下文,不能借用列表按钮');
@@ -0,0 +1,25 @@
1
+ import type { TaskState } from './model.js';
2
+ import type { Store } from './store.js';
3
+ /** The worker attests its review; this is not an independent visual quality score. */
4
+ export declare function readyHtml(store: Store, state: TaskState, pageId: string, token: string, kind: 'page' | 'form', summary: string): Promise<{
5
+ ok: boolean;
6
+ kind: "page" | "form";
7
+ pageId: string;
8
+ receiptPath: string;
9
+ next: string;
10
+ frozen: boolean;
11
+ }>;
12
+ /** Called only by the coordinator under the queue lock. Each acceptance is durable. */
13
+ export declare function acceptReadyHtml(store: Store, state: TaskState): Promise<{
14
+ accepted: {
15
+ kind: "page" | "form";
16
+ pageId: string;
17
+ summary: any;
18
+ }[];
19
+ rejected: {
20
+ kind: "page" | "form";
21
+ pageId: string;
22
+ code: string;
23
+ message: string;
24
+ }[];
25
+ }>;
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { atomicJson, CaptureError, digest, fileDigest } from './common.js';
4
+ import { prepareHtml, acceptHtml, draftPath } from './html.js';
5
+ async function evidence(store, state, pageId, token, kind) {
6
+ const prepared = await prepareHtml(store, state, pageId, token, kind);
7
+ if (prepared.r.status !== 'running')
8
+ throw new CaptureError('HTML_JOB_NOT_RUNNING', '只可提交当前运行中的草稿');
9
+ const directory = path.join(path.dirname(prepared.draft), 'review');
10
+ const sourceSha256 = digest(prepared.bytes);
11
+ const widths = kind === 'form' ? [1440, 390] : [prepared.png.width];
12
+ const inspections = [];
13
+ for (const width of widths) {
14
+ const filename = path.join(directory, `inspection-${width}.json`);
15
+ const receipt = await fs
16
+ .readFile(filename, 'utf8')
17
+ .then(JSON.parse)
18
+ .catch(() => null);
19
+ if (!receipt?.ok ||
20
+ receipt.sourceSha256 !== sourceSha256 ||
21
+ receipt.viewport?.width !== width ||
22
+ !receipt.screenshots?.length)
23
+ throw new CaptureError('HTML_REVIEW_REQUIRED', `需要当前草稿 ${width}px 校对成功并实际看图`);
24
+ for (const image of receipt.screenshots) {
25
+ if (typeof image.path !== 'string' ||
26
+ path.dirname(image.path) !== directory ||
27
+ (await fileDigest(image.path).catch(() => '')) !== image.sha256)
28
+ throw new CaptureError('HTML_REVIEW_REQUIRED', '校对截图已变化,需要重新校对');
29
+ }
30
+ inspections.push({ width, sha256: await fileDigest(filename) });
31
+ }
32
+ return { directory, sourceSha256, inspections };
33
+ }
34
+ /** The worker attests its review; this is not an independent visual quality score. */
35
+ export async function readyHtml(store, state, pageId, token, kind, summary) {
36
+ if (!summary?.trim() || summary.length > 4000)
37
+ throw new CaptureError('ARGUMENT_INVALID', 'summary 需为已实际检查和剩余差异的简短说明(1..4000字符)');
38
+ const proof = await evidence(store, state, pageId, token, kind);
39
+ const receiptPath = path.join(proof.directory, 'host-review.json');
40
+ await atomicJson(receiptPath, {
41
+ schema: 1,
42
+ kind,
43
+ pageId,
44
+ token,
45
+ sourceSha256: proof.sourceSha256,
46
+ inspections: proof.inspections,
47
+ summary: summary.trim(),
48
+ reviewedAt: new Date().toISOString(),
49
+ });
50
+ return { ok: true, kind, pageId, receiptPath, next: 'notify-coordinator', frozen: true };
51
+ }
52
+ /** Called only by the coordinator under the queue lock. Each acceptance is durable. */
53
+ export async function acceptReadyHtml(store, state) {
54
+ const status = await store.status(state);
55
+ const accepted = [], rejected = [];
56
+ for (const result of [...status.htmlResults, ...status.formHtmlResults]) {
57
+ if (result?.status !== 'running')
58
+ continue;
59
+ const kind = result.kind || 'page';
60
+ const receiptPath = path.join(path.dirname(draftPath(store, result)), 'review', 'host-review.json');
61
+ const receipt = await fs
62
+ .readFile(receiptPath, 'utf8')
63
+ .then(JSON.parse)
64
+ .catch(() => null);
65
+ if (!receipt)
66
+ continue;
67
+ try {
68
+ const proof = await evidence(store, state, result.id, result.token, kind);
69
+ if (receipt.schema !== 1 ||
70
+ receipt.token !== result.token ||
71
+ receipt.pageId !== result.id ||
72
+ receipt.kind !== kind ||
73
+ receipt.sourceSha256 !== proof.sourceSha256 ||
74
+ JSON.stringify(receipt.inspections) !== JSON.stringify(proof.inspections) ||
75
+ typeof receipt.summary !== 'string' ||
76
+ !receipt.summary.trim() ||
77
+ receipt.summary.length > 4000)
78
+ throw new CaptureError('HTML_REVIEW_STALE', '完成回执与当前草稿/校对不一致');
79
+ await acceptHtml(store, state, result.id, result.token, kind, proof.sourceSha256);
80
+ accepted.push({ kind, pageId: result.id, summary: receipt.summary });
81
+ }
82
+ catch (error) {
83
+ if (!(error instanceof CaptureError))
84
+ throw error;
85
+ rejected.push({ kind, pageId: result.id, code: error.code, message: error.message });
86
+ }
87
+ }
88
+ return { accepted, rejected };
89
+ }
@@ -0,0 +1,4 @@
1
+ import type { Store } from './store.js';
2
+ import type { TaskState } from './model.js';
3
+ /** Deterministic local rendering, never a visual similarity assertion or model call. */
4
+ export declare function inspectHtml(store: Store, state: TaskState, pageId: string, token: string, kind?: 'page' | 'form', width?: number): Promise<any>;