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.
- package/README.md +113 -0
- package/dist/api.d.ts +12 -0
- package/dist/api.js +125 -0
- package/dist/application.d.ts +7 -0
- package/dist/application.js +16 -0
- package/dist/archive.d.ts +130 -0
- package/dist/archive.js +151 -0
- package/dist/capture.d.ts +15 -0
- package/dist/capture.js +440 -0
- package/dist/common.d.ts +20 -0
- package/dist/common.js +87 -0
- package/dist/dom.d.mts +1 -0
- package/dist/dom.mjs +58 -0
- package/dist/form-context.d.ts +3 -0
- package/dist/form-context.js +58 -0
- package/dist/form-runtime.d.mts +2 -0
- package/dist/form-runtime.mjs +149 -0
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +603 -0
- package/dist/html.d.ts +22 -0
- package/dist/html.js +427 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +370 -0
- package/dist/menus.d.ts +32 -0
- package/dist/menus.js +330 -0
- package/dist/model.d.ts +164 -0
- package/dist/model.js +8 -0
- package/dist/offline-store.d.mts +5 -0
- package/dist/offline-store.mjs +80 -0
- package/dist/platform.d.ts +10 -0
- package/dist/platform.js +123 -0
- package/dist/readiness.d.ts +124 -0
- package/dist/readiness.js +529 -0
- package/dist/runtime-support.d.mts +52 -0
- package/dist/runtime-support.mjs +279 -0
- package/dist/site.d.ts +34 -0
- package/dist/site.js +195 -0
- package/dist/store.d.ts +90 -0
- package/dist/store.js +296 -0
- package/dist/templates/form-guide.md +539 -0
- package/dist/templates/index.html +803 -0
- package/dist/templates/placeholder.html +143 -0
- package/dist/templates/workflow-guide.md +95 -0
- package/dist/templates/workflow-presets.json +89 -0
- package/dist/temporary-records.d.ts +15 -0
- package/dist/temporary-records.js +286 -0
- package/dist/vendor/environment-auth.d.ts +61 -0
- package/dist/vendor/environment-auth.js +455 -0
- package/dist/workflow-runtime.d.mts +2 -0
- package/dist/workflow-runtime.mjs +298 -0
- package/dist/workflows.d.ts +28 -0
- package/dist/workflows.js +90 -0
- package/docs/PROTOCOL.md +299 -0
- package/package.json +45 -0
package/dist/forms.js
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { atomicJson, CaptureError, errorInfo, fileDigest, mapLimit } from './common.js';
|
|
4
|
+
import { E10Client, parseApiJson } from './api.js';
|
|
5
|
+
import { recoverTemporaryRecords, withTemporaryRecord, SeedUnavailable, temporarySummary, } from './temporary-records.js';
|
|
6
|
+
import { navigationKey } from './menus.js';
|
|
7
|
+
import { APPROVAL_PATH, normalizeWorkflows, workflowPreset, workflowType } from './workflows.js';
|
|
8
|
+
export const formInputPath = (store, key) => path.join(store.meta, 'form-inputs', `${navigationKey(key)}.json`);
|
|
9
|
+
const collectionPath = (store, key) => path.join(store.meta, 'form-results', `${navigationKey(key)}.json`);
|
|
10
|
+
export async function formResult(store, key) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(await fs.readFile(collectionPath(store, key), 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT')
|
|
16
|
+
return;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function verifiedForm(store, result) {
|
|
21
|
+
if (!result ||
|
|
22
|
+
result.status !== 'succeeded' ||
|
|
23
|
+
!result.sha256 ||
|
|
24
|
+
(!result.objId && result.kind !== 'workflow'))
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
return (await fileDigest(formInputPath(store, result.id))) === result.sha256;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const flag = (value) => value === true || value === 1 || value === '1' || value === 'true';
|
|
34
|
+
const decimal = (value, label) => {
|
|
35
|
+
if (!(typeof value === 'string' || (typeof value === 'number' && Number.isSafeInteger(value))) ||
|
|
36
|
+
!/^\d+$/.test(String(value)))
|
|
37
|
+
throw new CaptureError('FORM_ID_INVALID', `${label} 缺少有效 ID`);
|
|
38
|
+
return String(value);
|
|
39
|
+
};
|
|
40
|
+
export function configObject(value) {
|
|
41
|
+
if (value == null || value === '')
|
|
42
|
+
return {};
|
|
43
|
+
const result = typeof value === 'string' ? parseApiJson(value) : value;
|
|
44
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
45
|
+
throw new CaptureError('FORM_CONFIG_INVALID', '表单配置不是对象');
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
const systems = new Set([
|
|
49
|
+
'1:name',
|
|
50
|
+
'2:creator',
|
|
51
|
+
'3:create_time',
|
|
52
|
+
'4:update_time',
|
|
53
|
+
'5:id',
|
|
54
|
+
'6:updater',
|
|
55
|
+
'8:data_status',
|
|
56
|
+
'11:batchpubtags',
|
|
57
|
+
'12:batchmytags',
|
|
58
|
+
'9:flow_status',
|
|
59
|
+
'10:current_step',
|
|
60
|
+
'16:flow_system_number',
|
|
61
|
+
]);
|
|
62
|
+
export function normalizeFields(groups, objId) {
|
|
63
|
+
if (!Array.isArray(groups))
|
|
64
|
+
throw new CaptureError('FIELDS_INVALID', '字段接口不是数组');
|
|
65
|
+
const fields = [], seen = new Set();
|
|
66
|
+
const walk = (nodes, groupId, groupName, detail) => {
|
|
67
|
+
for (const node of nodes) {
|
|
68
|
+
if (!node || typeof node !== 'object')
|
|
69
|
+
throw new CaptureError('FIELDS_INVALID', '字段项无效');
|
|
70
|
+
if (Array.isArray(node.fields)) {
|
|
71
|
+
const childId = String(node.id || groupId);
|
|
72
|
+
walk(node.fields, childId, String(node.name || groupName), detail || childId !== objId);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const id = decimal(node.id, '字段'), config = configObject(node.config);
|
|
76
|
+
if (seen.has(id))
|
|
77
|
+
throw new CaptureError('FIELD_DUPLICATE', `重复字段 ${id}`);
|
|
78
|
+
seen.add(id);
|
|
79
|
+
const subFormId = String(node.subFormId || (detail ? groupId : ''));
|
|
80
|
+
fields.push({
|
|
81
|
+
...node,
|
|
82
|
+
id,
|
|
83
|
+
config,
|
|
84
|
+
groupId,
|
|
85
|
+
groupName,
|
|
86
|
+
subFormId,
|
|
87
|
+
isDetail: !!subFormId || detail,
|
|
88
|
+
system: ['isSystem', 'systemField', 'isSystemField'].some((key) => flag(node[key])) ||
|
|
89
|
+
systems.has(`${id}:${node.name}`),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(groups, objId, '', false);
|
|
94
|
+
return fields;
|
|
95
|
+
}
|
|
96
|
+
export function nlistPage(data) {
|
|
97
|
+
let current = data;
|
|
98
|
+
for (let i = 0; i < 5 && current && typeof current === 'object'; i++, current = current.data) {
|
|
99
|
+
if (current.page && typeof current.page === 'object') {
|
|
100
|
+
const page = current.page, component = page.comps?.[1];
|
|
101
|
+
if (!component || !component.config)
|
|
102
|
+
throw new CaptureError('NLIST_CONFIG_INVALID', '缺少已验证的 data.page.comps[1].config');
|
|
103
|
+
const config = configObject(component.config);
|
|
104
|
+
return {
|
|
105
|
+
page,
|
|
106
|
+
config,
|
|
107
|
+
objId: decimal(config.objId, 'NList 表单'),
|
|
108
|
+
listId: decimal(config.ebListId, 'NList 列表'),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw new CaptureError('NLIST_CONFIG_INVALID', '响应缺少 data.page');
|
|
113
|
+
}
|
|
114
|
+
export function normalizeButtons(raw, scope, family) {
|
|
115
|
+
let sources;
|
|
116
|
+
if (family === 'layout')
|
|
117
|
+
sources = [['data', raw]];
|
|
118
|
+
else if (family === 'list') {
|
|
119
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
120
|
+
throw new CaptureError('BUTTON_INVALID', '传统列表按钮结构未知');
|
|
121
|
+
sources = [
|
|
122
|
+
['buttons', raw?.buttons ?? []],
|
|
123
|
+
['LineEndDropDownButtons', raw?.LineEndDropDownButtons ?? [], 'LineEndDropDown'],
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const config = configObject(raw?.buttonConfig);
|
|
128
|
+
sources = [
|
|
129
|
+
['buttonConfig.btns', config.btns ?? []],
|
|
130
|
+
['topOperatBtns', raw?.topOperatBtns ?? [], 'topOperatBtns'],
|
|
131
|
+
['bottomOperatBtns', raw?.bottomOperatBtns ?? [], 'bottomOperatBtns'],
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
const boolean = (value, fallback) => {
|
|
135
|
+
if (value == null || value === '')
|
|
136
|
+
return fallback;
|
|
137
|
+
if ([true, false, 0, 1, '0', '1', 'true', 'false'].includes(value))
|
|
138
|
+
return flag(value);
|
|
139
|
+
throw new CaptureError('BUTTON_INVALID', '按钮开关格式未知');
|
|
140
|
+
};
|
|
141
|
+
const scopeKey = JSON.stringify(Object.fromEntries(Object.entries(scope).sort(([a], [b]) => a.localeCompare(b))));
|
|
142
|
+
const items = sources.flatMap(([source, value, position]) => {
|
|
143
|
+
const buttons = typeof value === 'string' ? parseApiJson(value) : value;
|
|
144
|
+
if (!Array.isArray(buttons))
|
|
145
|
+
throw new CaptureError('BUTTON_INVALID', '按钮集合不是数组');
|
|
146
|
+
return buttons.map((button, index) => {
|
|
147
|
+
if (!button || typeof button !== 'object')
|
|
148
|
+
throw new CaptureError('BUTTON_INVALID', '按钮配置无效');
|
|
149
|
+
let positions = button.showPosition ?? (position ? [position] : []);
|
|
150
|
+
if (typeof positions === 'string')
|
|
151
|
+
positions = [positions];
|
|
152
|
+
if (!Array.isArray(positions))
|
|
153
|
+
throw new CaptureError('BUTTON_INVALID', '按钮位置无效');
|
|
154
|
+
return {
|
|
155
|
+
id: String(button.id ?? `${source}[${index}]`),
|
|
156
|
+
key: button.buttonKey,
|
|
157
|
+
name: button.name,
|
|
158
|
+
positions,
|
|
159
|
+
enabled: boolean(button.enable, true),
|
|
160
|
+
hidden: boolean(button.hidden, false),
|
|
161
|
+
order: button.showOrder ?? index,
|
|
162
|
+
sourcePath: `${source}[${index}]`,
|
|
163
|
+
buttonRef: JSON.stringify([scopeKey, String(button.id ?? index), `${source}[${index}]`]),
|
|
164
|
+
config: button,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
return { status: 'ready', scope, scopeKey, sourceFamily: family, items, raw };
|
|
169
|
+
}
|
|
170
|
+
export async function collectForms(store, state, auth, retryFailed = false) {
|
|
171
|
+
if (!state.menuRequired || !state.formPages)
|
|
172
|
+
return;
|
|
173
|
+
const client = new E10Client(auth), cache = new Map();
|
|
174
|
+
const cached = (key, fn) => {
|
|
175
|
+
if (!cache.has(key))
|
|
176
|
+
cache.set(key, fn());
|
|
177
|
+
return cache.get(key);
|
|
178
|
+
};
|
|
179
|
+
const jobs = [];
|
|
180
|
+
for (const item of state.formPages) {
|
|
181
|
+
const previous = await formResult(store, item.id);
|
|
182
|
+
if (await verifiedForm(store, previous)) {
|
|
183
|
+
item.objId = previous.objId;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (previous?.status === 'failed' && !retryFailed)
|
|
187
|
+
continue;
|
|
188
|
+
jobs.push({ item, previous });
|
|
189
|
+
}
|
|
190
|
+
if (!jobs.length && !(await temporarySummary(store)).pending)
|
|
191
|
+
return;
|
|
192
|
+
const independent = await client.request('GET', `/api/ebuilder/form/app/isIndependentDeploy?appId=${state.appId}`);
|
|
193
|
+
if (typeof independent !== 'boolean')
|
|
194
|
+
throw new CaptureError('DEPLOYMENT_INVALID', '独立部署状态不是布尔值');
|
|
195
|
+
const prefix = '/api/ebuilder' + (independent ? state.appId : '');
|
|
196
|
+
await recoverTemporaryRecords(store, state, client, prefix);
|
|
197
|
+
if (!jobs.length)
|
|
198
|
+
return;
|
|
199
|
+
let temporaryQueue = Promise.resolve(), temporaryFailure;
|
|
200
|
+
const temporary = (fn) => {
|
|
201
|
+
const result = temporaryQueue.then(async () => {
|
|
202
|
+
if (temporaryFailure)
|
|
203
|
+
throw temporaryFailure;
|
|
204
|
+
try {
|
|
205
|
+
return await fn();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (!(error instanceof SeedUnavailable))
|
|
209
|
+
temporaryFailure = error;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
temporaryQueue = result.then(() => undefined, () => undefined);
|
|
214
|
+
return result;
|
|
215
|
+
};
|
|
216
|
+
const get = (suffix) => client.request('GET', prefix + '/form' + suffix);
|
|
217
|
+
const post = (suffix, body) => client.request('POST', prefix + '/form' + suffix, body);
|
|
218
|
+
const sourceForms = new Map((state.forms || []).map((f) => [f.id, f]));
|
|
219
|
+
const safeAttempt = async (label, warnings, fn) => {
|
|
220
|
+
try {
|
|
221
|
+
return await fn();
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (['E10_LOGIN_REQUIRED', 'TEMPORARY_RECORD_STOP'].includes(error.code))
|
|
225
|
+
throw error;
|
|
226
|
+
warnings.push(`${label}不可用,${client.clean(errorInfo(error).message)}`);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const collectObject = (formId, withButtons) => cached(`form:${formId}`, async () => {
|
|
231
|
+
const warnings = [];
|
|
232
|
+
const groups = await client.request('POST', '/api/ebuilder/common/ds/fields', {
|
|
233
|
+
groupId: state.appId,
|
|
234
|
+
sourceId: formId,
|
|
235
|
+
sourceType: 'FORM',
|
|
236
|
+
detailFieldsGroup: 'true',
|
|
237
|
+
enableMerge: 'true',
|
|
238
|
+
customParam: '{"terminal":""}',
|
|
239
|
+
params: '[]',
|
|
240
|
+
}, true);
|
|
241
|
+
const fields = normalizeFields(groups, formId);
|
|
242
|
+
for (const field of fields) {
|
|
243
|
+
const kind = String(field.compType || field.type).toLowerCase();
|
|
244
|
+
if (!field.system && ['select', 'radiobox', 'checkbox', 'cascader'].includes(kind)) {
|
|
245
|
+
field.options = await client.request('GET', `/api/ebuilder/common/ds/options?fieldName=${field.id}&objId=${formId}&sourceType=FORM&groupId=${state.appId}&optionLevel=1`);
|
|
246
|
+
if (kind === 'cascader')
|
|
247
|
+
warnings.push(`字段 ${field.id} 的级联选项仅取得第一层`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
let record, referenceKnown = false;
|
|
251
|
+
await safeAttempt('参考记录查询', warnings, async () => {
|
|
252
|
+
const data = await post('/dataset/v2/getAllData', {
|
|
253
|
+
datajson: {
|
|
254
|
+
header: { objId: formId },
|
|
255
|
+
pageInfo: { pageNo: 1, pageSize: 1 },
|
|
256
|
+
operationinfo: { isReturnDetail: false },
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
const rows = data?.datajson?.datas;
|
|
260
|
+
if (!Array.isArray(rows))
|
|
261
|
+
throw new CaptureError('REFERENCE_INVALID', '查询缺少 datas 数组');
|
|
262
|
+
referenceKnown = true;
|
|
263
|
+
if (rows.length) {
|
|
264
|
+
const value = rows[0]?.mainTable?.id;
|
|
265
|
+
if (value?.fieldId != null && String(value.fieldId) !== '5')
|
|
266
|
+
throw new CaptureError('REFERENCE_INVALID', '记录 ID 字段无效');
|
|
267
|
+
record = decimal(value?.fieldValue, '参考记录');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
const layout = {
|
|
271
|
+
source: 'fields',
|
|
272
|
+
reason: record ? 'export-unavailable' : 'no-accessible-record',
|
|
273
|
+
};
|
|
274
|
+
const contexts = {};
|
|
275
|
+
const buttonsFor = async (mode, layoutId, contextRecord) => {
|
|
276
|
+
if (!contextRecord && mode !== '1')
|
|
277
|
+
return { status: 'unavailable', reason: 'no-accessible-record', items: [] };
|
|
278
|
+
const dataId = mode === '1' ? '0' : contextRecord;
|
|
279
|
+
const data = await post('/data/getFormButtons', {
|
|
280
|
+
layoutType: 'formLayout',
|
|
281
|
+
objId: formId,
|
|
282
|
+
dataId,
|
|
283
|
+
formDataId: dataId,
|
|
284
|
+
type: mode,
|
|
285
|
+
fromRecycle: false,
|
|
286
|
+
otherOperator: '',
|
|
287
|
+
objectAppId: '',
|
|
288
|
+
browser: 'Chrome',
|
|
289
|
+
layoutId,
|
|
290
|
+
isTamper: '0',
|
|
291
|
+
paramWrapper: {
|
|
292
|
+
id: dataId,
|
|
293
|
+
fromRecycle: false,
|
|
294
|
+
type: mode,
|
|
295
|
+
objid: formId,
|
|
296
|
+
dataid: dataId,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
return normalizeButtons(data, {
|
|
300
|
+
appId: state.appId,
|
|
301
|
+
objId: formId,
|
|
302
|
+
kind: 'layout',
|
|
303
|
+
layoutId,
|
|
304
|
+
mode,
|
|
305
|
+
recordContext: mode === '1' ? 'new' : 'reference-snapshot',
|
|
306
|
+
}, 'layout');
|
|
307
|
+
};
|
|
308
|
+
const loadContexts = async (reference) => {
|
|
309
|
+
if (reference) {
|
|
310
|
+
const html = await safeAttempt('布局参考导出', warnings, async () => {
|
|
311
|
+
const exported = await post('/single/exportDataDetailPDF', {
|
|
312
|
+
objId: formId,
|
|
313
|
+
fileName: 'prototype-layout-reference',
|
|
314
|
+
exportWord: false,
|
|
315
|
+
exportExcel: false,
|
|
316
|
+
exportHtml: true,
|
|
317
|
+
exportPdf: false,
|
|
318
|
+
wordLayout: {},
|
|
319
|
+
excelLayout: {},
|
|
320
|
+
formDataId: reference,
|
|
321
|
+
});
|
|
322
|
+
if (exported?.success !== true)
|
|
323
|
+
throw new CaptureError('REFERENCE_INVALID', '布局导出未成功');
|
|
324
|
+
return client.reference(decimal(exported.pdfFileId, '导出文件'));
|
|
325
|
+
});
|
|
326
|
+
if (html) {
|
|
327
|
+
layout.source = 'html-reference';
|
|
328
|
+
layout.html = html;
|
|
329
|
+
delete layout.reason;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Collect default and explicitly requested layout/record contexts while a temporary reference still exists.
|
|
333
|
+
const requested = new Map();
|
|
334
|
+
if (withButtons)
|
|
335
|
+
requested.set('', { layoutId: '' });
|
|
336
|
+
for (const p of state.formPages || [])
|
|
337
|
+
if (withButtons && p.objId === formId && p.kind === 'layout')
|
|
338
|
+
requested.set(p.id, {
|
|
339
|
+
layoutId: p.layoutId || '',
|
|
340
|
+
record: p.recordId && p.recordId !== '0' ? p.recordId : undefined,
|
|
341
|
+
});
|
|
342
|
+
const snapshots = new Map();
|
|
343
|
+
for (const [key, context] of requested) {
|
|
344
|
+
const modes = {};
|
|
345
|
+
for (const [mode, name] of [
|
|
346
|
+
['1', 'add'],
|
|
347
|
+
['0', 'view'],
|
|
348
|
+
['2', 'edit'],
|
|
349
|
+
]) {
|
|
350
|
+
const recordId = context.record || reference, scopeKey = `${context.layoutId}:${mode}:${mode === '1' ? '0' : recordId || ''}`;
|
|
351
|
+
if (!snapshots.has(scopeKey))
|
|
352
|
+
snapshots.set(scopeKey, (await safeAttempt(`${name}布局按钮`, warnings, () => buttonsFor(mode, context.layoutId, recordId))) || { status: 'unavailable', reason: 'request-failed', items: [] });
|
|
353
|
+
modes[name] = snapshots.get(scopeKey);
|
|
354
|
+
}
|
|
355
|
+
contexts[key] = modes;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
await loadContexts(record);
|
|
359
|
+
if (layout.source === 'fields' && referenceKnown && state.allowTemporaryRecords) {
|
|
360
|
+
try {
|
|
361
|
+
await temporary(() => withTemporaryRecord(store, state, client, prefix, formId, fields, async (temporaryId) => {
|
|
362
|
+
await loadContexts(temporaryId);
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (!(error instanceof SeedUnavailable))
|
|
367
|
+
throw error;
|
|
368
|
+
warnings.push(error.message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (layout.source === 'fields')
|
|
372
|
+
warnings.push(state.allowTemporaryRecords
|
|
373
|
+
? '未取得布局 HTML,使用全部自定义字段生成布局'
|
|
374
|
+
: '未取得布局 HTML,使用全部自定义字段生成布局;未启用临时参考记录');
|
|
375
|
+
return { fields, layout, warnings, contexts };
|
|
376
|
+
});
|
|
377
|
+
const loadWorkflowCatalog = () => cached('workflow-catalog', async () => {
|
|
378
|
+
try {
|
|
379
|
+
const data = await client.request('POST', `${APPROVAL_PATH}?apid=${state.appId}&cid=${state.appId}`, { appId: state.appId, viewType: '', sortParams: [], searchParamData: {} });
|
|
380
|
+
const value = normalizeWorkflows(data, state.appId, new Set(sourceForms.keys()));
|
|
381
|
+
await atomicJson(path.join(store.meta, 'workflow-catalog.json'), client.clean(value));
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
await atomicJson(path.join(store.meta, 'workflow-catalog.json'), {
|
|
386
|
+
status: 'unavailable',
|
|
387
|
+
error: client.clean(errorInfo(error)),
|
|
388
|
+
});
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
const collectMenu = async ({ item, previous }) => {
|
|
393
|
+
const receipt = {
|
|
394
|
+
id: item.id,
|
|
395
|
+
status: 'running',
|
|
396
|
+
attempt: (previous?.attempt || 0) + 1,
|
|
397
|
+
updatedAt: new Date().toISOString(),
|
|
398
|
+
};
|
|
399
|
+
await atomicJson(collectionPath(store, item.id), receipt);
|
|
400
|
+
try {
|
|
401
|
+
if (item.kind === 'workflow') {
|
|
402
|
+
const catalog = await loadWorkflowCatalog();
|
|
403
|
+
const objectIds = [
|
|
404
|
+
...new Set(catalog.templates.map((t) => t.objId)),
|
|
405
|
+
].sort();
|
|
406
|
+
const forms = [];
|
|
407
|
+
for (const objId of objectIds) {
|
|
408
|
+
// Ordinary menu collection runs first; reuse its verified snapshot on resume too.
|
|
409
|
+
let shared;
|
|
410
|
+
for (const page of state.formPages || []) {
|
|
411
|
+
if (page.kind === 'workflow' || page.objId !== objId)
|
|
412
|
+
continue;
|
|
413
|
+
const result = await formResult(store, page.id);
|
|
414
|
+
if (await verifiedForm(store, result)) {
|
|
415
|
+
shared = JSON.parse(await fs.readFile(formInputPath(store, page.id), 'utf8'));
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
shared ||= await collectObject(objId, false);
|
|
420
|
+
forms.push({
|
|
421
|
+
objId,
|
|
422
|
+
name: sourceForms.get(objId).name,
|
|
423
|
+
fields: shared.fields,
|
|
424
|
+
layout: shared.layout,
|
|
425
|
+
warnings: shared.warnings,
|
|
426
|
+
workflowRefs: catalog.templates
|
|
427
|
+
.filter((t) => t.objId === objId)
|
|
428
|
+
.map((t) => t.workflowId),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const preset = workflowPreset(workflowType(item.workflowType));
|
|
432
|
+
const scope = {
|
|
433
|
+
appId: state.appId,
|
|
434
|
+
kind: 'workflow',
|
|
435
|
+
workflowType: item.workflowType,
|
|
436
|
+
bindingPath: item.id,
|
|
437
|
+
};
|
|
438
|
+
const buttons = {
|
|
439
|
+
...normalizeButtons({ buttons: preset.buttons }, scope, 'list'),
|
|
440
|
+
sourceFamily: 'workflow',
|
|
441
|
+
source: preset.source,
|
|
442
|
+
mockOnly: true,
|
|
443
|
+
};
|
|
444
|
+
for (const button of buttons.items)
|
|
445
|
+
button.pageButtonRef = JSON.stringify([item.id, button.buttonRef]);
|
|
446
|
+
const warnings = [...new Set(forms.flatMap((f) => f.warnings))];
|
|
447
|
+
if (catalog.skipped.length)
|
|
448
|
+
warnings.push(`流程目录跳过 ${catalog.skipped.length} 个非 EB 或归属/配置无效的流程;详见私有 workflowCatalog.skipped`);
|
|
449
|
+
if (!catalog.templates.length)
|
|
450
|
+
warnings.push('没有可用的 EB 流程模板,保留已发布流程入口并显示空态');
|
|
451
|
+
await atomicJson(formInputPath(store, item.id), client.clean({
|
|
452
|
+
schema: 1,
|
|
453
|
+
appId: state.appId,
|
|
454
|
+
name: item.name,
|
|
455
|
+
page: item,
|
|
456
|
+
workflowCatalog: catalog,
|
|
457
|
+
workflowPreset: preset,
|
|
458
|
+
forms,
|
|
459
|
+
buttons,
|
|
460
|
+
dataStatus: catalog.templates.length ? 'ready' : 'empty',
|
|
461
|
+
warnings,
|
|
462
|
+
}));
|
|
463
|
+
Object.assign(receipt, {
|
|
464
|
+
kind: 'workflow',
|
|
465
|
+
objectIds,
|
|
466
|
+
status: 'succeeded',
|
|
467
|
+
warnings,
|
|
468
|
+
sha256: await fileDigest(formInputPath(store, item.id)),
|
|
469
|
+
updatedAt: new Date().toISOString(),
|
|
470
|
+
});
|
|
471
|
+
await atomicJson(collectionPath(store, item.id), receipt);
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
let list, listButtons, objId = item.objId;
|
|
475
|
+
if (item.kind === 'nlist') {
|
|
476
|
+
const data = await cached(`nlist:${item.viewId}`, () => get(`/viewdesigner/viewPageInfo?pageId=${item.viewId}&terminalType=PC`));
|
|
477
|
+
const parsed = nlistPage(data);
|
|
478
|
+
if (parsed.page.appId != null && String(parsed.page.appId) !== state.appId)
|
|
479
|
+
throw new CaptureError('FORM_APP_MISMATCH', 'NList 所属应用不匹配');
|
|
480
|
+
if (objId && objId !== parsed.objId)
|
|
481
|
+
throw new CaptureError('FORM_ID_MISMATCH', '菜单与 NList 表单不匹配');
|
|
482
|
+
objId = parsed.objId;
|
|
483
|
+
list = {
|
|
484
|
+
kind: 'nlist',
|
|
485
|
+
hostPageId: item.viewId,
|
|
486
|
+
listId: parsed.listId,
|
|
487
|
+
config: parsed.config,
|
|
488
|
+
};
|
|
489
|
+
listButtons = parsed.config;
|
|
490
|
+
}
|
|
491
|
+
else if (item.kind === 'list') {
|
|
492
|
+
const context = await cached(`list:${item.viewId}`, () => post('/list/listInit', { listId: item.viewId }));
|
|
493
|
+
if (String(context?.appId) !== state.appId ||
|
|
494
|
+
(context.listId != null && String(context.listId) !== item.viewId))
|
|
495
|
+
throw new CaptureError('FORM_APP_MISMATCH', '传统列表所属应用或列表 ID 不匹配');
|
|
496
|
+
const resolved = decimal(context.objId ?? context.objid, '传统列表表单');
|
|
497
|
+
if (objId && objId !== resolved)
|
|
498
|
+
throw new CaptureError('FORM_ID_MISMATCH', '菜单与列表表单不匹配');
|
|
499
|
+
objId = resolved;
|
|
500
|
+
const query = `apid=${state.appId}&tpaid=${item.viewId}&objId=${objId}`;
|
|
501
|
+
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}`,
|
|
508
|
+
};
|
|
509
|
+
if (context.conditionId && String(context.conditionId) !== '0')
|
|
510
|
+
routes.conditions = `/plugin/conditionEditor/getById?apid=${state.appId}&id=${decimal(context.conditionId, '条件')}&type=search&objId=${objId}`;
|
|
511
|
+
const responses = await Promise.allSettled(Object.entries(routes).map(async ([key, route]) => [
|
|
512
|
+
key,
|
|
513
|
+
await cached(route, () => get(route)),
|
|
514
|
+
]));
|
|
515
|
+
for (const result of responses)
|
|
516
|
+
if (result.status === 'rejected')
|
|
517
|
+
throw result.reason;
|
|
518
|
+
const config = Object.fromEntries(responses.map((r) => r.value));
|
|
519
|
+
listButtons = config.buttons;
|
|
520
|
+
delete config.buttons;
|
|
521
|
+
list = { kind: 'list', listId: item.viewId, config };
|
|
522
|
+
}
|
|
523
|
+
if (!objId || !sourceForms.has(objId))
|
|
524
|
+
throw new CaptureError('FORM_MEMBERSHIP_INVALID', '菜单对应表单不属于当前应用');
|
|
525
|
+
item.objId = objId;
|
|
526
|
+
receipt.objId = objId;
|
|
527
|
+
const formId = objId;
|
|
528
|
+
const shared = await collectObject(formId, true);
|
|
529
|
+
const warnings = [...shared.warnings];
|
|
530
|
+
const formButtons = structuredClone(shared.contexts[item.kind === 'layout' ? item.id : ''] || shared.contexts['']);
|
|
531
|
+
if (formButtons.view.status !== 'ready' || formButtons.edit.status !== 'ready')
|
|
532
|
+
warnings.push('查看/编辑模式按钮缺少记录上下文,不能借用列表按钮');
|
|
533
|
+
if (item.layoutId && shared.layout.source === 'html-reference')
|
|
534
|
+
warnings.push('导出的 HTML 为表单默认参考,不能据此宣称指定布局已精确取得');
|
|
535
|
+
const publicPage = { ...item };
|
|
536
|
+
delete publicPage.recordId;
|
|
537
|
+
const input = {
|
|
538
|
+
schema: 1,
|
|
539
|
+
appId: state.appId,
|
|
540
|
+
objId: formId,
|
|
541
|
+
name: sourceForms.get(formId).name,
|
|
542
|
+
page: publicPage,
|
|
543
|
+
fields: shared.fields,
|
|
544
|
+
list,
|
|
545
|
+
layout: shared.layout,
|
|
546
|
+
formButtons,
|
|
547
|
+
warnings,
|
|
548
|
+
buttons: item.kind === 'layout'
|
|
549
|
+
? formButtons[item.mode || 'view']
|
|
550
|
+
: normalizeButtons(listButtons, {
|
|
551
|
+
appId: state.appId,
|
|
552
|
+
objId: formId,
|
|
553
|
+
kind: item.kind,
|
|
554
|
+
listId: list.listId,
|
|
555
|
+
hostPageId: list.hostPageId || '',
|
|
556
|
+
}, item.kind),
|
|
557
|
+
};
|
|
558
|
+
for (const set of [input.buttons, ...Object.values(input.formButtons)])
|
|
559
|
+
for (const button of set?.items || [])
|
|
560
|
+
button.pageButtonRef = JSON.stringify([item.id, button.buttonRef]);
|
|
561
|
+
const sanitized = client.clean(input);
|
|
562
|
+
await atomicJson(formInputPath(store, item.id), sanitized);
|
|
563
|
+
receipt.sha256 = await fileDigest(formInputPath(store, item.id));
|
|
564
|
+
receipt.warnings = warnings;
|
|
565
|
+
receipt.status = 'succeeded';
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
if (['E10_LOGIN_REQUIRED', 'TEMPORARY_RECORD_STOP'].includes(error.code))
|
|
569
|
+
return error;
|
|
570
|
+
receipt.status = 'failed';
|
|
571
|
+
receipt.error = client.clean(errorInfo(error));
|
|
572
|
+
}
|
|
573
|
+
receipt.updatedAt = new Date().toISOString();
|
|
574
|
+
await atomicJson(collectionPath(store, item.id), receipt);
|
|
575
|
+
return undefined;
|
|
576
|
+
};
|
|
577
|
+
const outcomes = await mapLimit(jobs.filter((j) => j.item.kind !== 'workflow'), state.settings.concurrency, collectMenu);
|
|
578
|
+
if (!outcomes.some(Boolean) && jobs.some((j) => j.item.kind === 'workflow')) {
|
|
579
|
+
// A changed directory must invalidate every dependent workflow input, including accepted HTML.
|
|
580
|
+
try {
|
|
581
|
+
const catalog = await loadWorkflowCatalog();
|
|
582
|
+
for (const item of state.formPages) {
|
|
583
|
+
if (item.kind !== 'workflow' || jobs.some((j) => j.item.id === item.id))
|
|
584
|
+
continue;
|
|
585
|
+
const previous = await formResult(store, item.id);
|
|
586
|
+
if (await verifiedForm(store, previous)) {
|
|
587
|
+
const input = JSON.parse(await fs.readFile(formInputPath(store, item.id), 'utf8'));
|
|
588
|
+
if (JSON.stringify(input.workflowCatalog) !== JSON.stringify(client.clean(catalog)))
|
|
589
|
+
jobs.push({ item, previous });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
/* Each workflow receipt records the shared failure below. */
|
|
595
|
+
}
|
|
596
|
+
outcomes.push(...(await mapLimit(jobs.filter((j) => j.item.kind === 'workflow'), state.settings.concurrency, collectMenu)));
|
|
597
|
+
}
|
|
598
|
+
delete state.archive;
|
|
599
|
+
await store.save(state);
|
|
600
|
+
const fatal = outcomes.find(Boolean);
|
|
601
|
+
if (fatal)
|
|
602
|
+
throw fatal;
|
|
603
|
+
}
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HtmlResult, TaskState } from './model.js';
|
|
2
|
+
import { Store } from './store.js';
|
|
3
|
+
export declare function nextHtml(store: Store, s: TaskState): Promise<{
|
|
4
|
+
state: string;
|
|
5
|
+
concurrency: number;
|
|
6
|
+
jobs: any[];
|
|
7
|
+
html: {
|
|
8
|
+
required: boolean;
|
|
9
|
+
succeeded: number;
|
|
10
|
+
failed: number;
|
|
11
|
+
running: number;
|
|
12
|
+
pending: number;
|
|
13
|
+
};
|
|
14
|
+
next: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function validateHtml(source: string, screenshotSha256: string): void;
|
|
17
|
+
export declare function acceptHtml(store: Store, s: TaskState, pageId: string, token: string, kind?: 'page' | 'form'): Promise<HtmlResult>;
|
|
18
|
+
export declare function failHtml(store: Store, s: TaskState, pageId: string, token: string, reason: string, kind?: 'page' | 'form'): Promise<HtmlResult>;
|
|
19
|
+
export declare function retryHtml(store: Store, s: TaskState, target?: {
|
|
20
|
+
id: string;
|
|
21
|
+
kind: 'page' | 'form';
|
|
22
|
+
}): Promise<void>;
|