bdip-office-assistant-cli 0.0.0-stage → 1.0.1

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 (63) hide show
  1. package/README.md +37 -2
  2. package/dist/accounts.js +30 -0
  3. package/dist/args.js +36 -0
  4. package/dist/auth-token.js +783 -0
  5. package/dist/auth.js +331 -0
  6. package/dist/blog.js +562 -0
  7. package/dist/browser-links.js +224 -0
  8. package/dist/captcha-display.js +30 -0
  9. package/dist/cli.js +5533 -0
  10. package/dist/client.js +254 -0
  11. package/dist/config.js +170 -0
  12. package/dist/createcontext.js +41 -0
  13. package/dist/crypto.js +139 -0
  14. package/dist/cube.js +1771 -0
  15. package/dist/documents.js +98 -0
  16. package/dist/email.js +841 -0
  17. package/dist/field-preparation.js +237 -0
  18. package/dist/fileutil.js +20 -0
  19. package/dist/formula-runtime.js +55 -0
  20. package/dist/hrm.js +478 -0
  21. package/dist/keepalive.js +99 -0
  22. package/dist/kq.js +280 -0
  23. package/dist/linkage-calculations.js +68 -0
  24. package/dist/links.js +34 -0
  25. package/dist/meetings.js +172 -0
  26. package/dist/nodemap.js +242 -0
  27. package/dist/odoc.js +199 -0
  28. package/dist/office-tools.js +155 -0
  29. package/dist/pageopen.js +29 -0
  30. package/dist/parse.js +177 -0
  31. package/dist/password-input.js +84 -0
  32. package/dist/password-login.js +208 -0
  33. package/dist/person-selection.js +81 -0
  34. package/dist/querystate.js +38 -0
  35. package/dist/render.js +21 -0
  36. package/dist/report.js +50 -0
  37. package/dist/session-worker.js +54 -0
  38. package/dist/session.js +280 -0
  39. package/dist/submissionplan.js +45 -0
  40. package/dist/submissionstate.js +81 -0
  41. package/dist/subordinate.js +458 -0
  42. package/dist/supervise.js +152 -0
  43. package/dist/todoaskidea.js +54 -0
  44. package/dist/todobatch.js +172 -0
  45. package/dist/todocc.js +128 -0
  46. package/dist/todoforward.js +66 -0
  47. package/dist/todoreject.js +215 -0
  48. package/dist/todosubmit.js +325 -0
  49. package/dist/todotransfer.js +54 -0
  50. package/dist/util.js +25 -0
  51. package/dist/wfagent.js +538 -0
  52. package/dist/wfcreate.js +102 -0
  53. package/dist/wfform.js +169 -0
  54. package/dist/wfops.js +40 -0
  55. package/dist/wfreceiver.js +43 -0
  56. package/dist/wfstart.js +558 -0
  57. package/dist/wfsummary.js +89 -0
  58. package/dist/wfupload.js +83 -0
  59. package/dist/wfuser.js +96 -0
  60. package/dist/workflow-runtime.js +177 -0
  61. package/dist/workflow.js +570 -0
  62. package/dist/workplan.js +431 -0
  63. package/package.json +29 -6
@@ -0,0 +1,558 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { describeFormFields, HTMLTYPE_LABEL, getBrowserOptions } from './wfform.js';
3
+ import { findCreateWfByName } from './wfcreate.js';
4
+ import { todayStr } from './util.js';
5
+ import { beginSubmission, updateSubmission } from './submissionstate.js';
6
+ const VIEWATTR_LABEL = { 1: '只读', 2: '可编辑', 3: '必填', 4: '隐藏' };
7
+ /** 已知系统字段 id(负数),单独标注,不作为业务填值候选。 */
8
+ const SYS_FIELD_IDS = new Set(['-1', '-2', '-4', '-9', '-10']);
9
+ export async function resolveWorkflowId(cfg, id, name) {
10
+ if (id)
11
+ return id;
12
+ if (!name)
13
+ throw new Error('需要 --id 或 --name');
14
+ const hits = await findCreateWfByName(cfg, name);
15
+ if (hits.length === 0)
16
+ throw new Error(`按名「${name}」未找到流程`);
17
+ if (hits.length > 1) {
18
+ const list = hits.map((w) => `${w.name} [id=${w.id ?? ''}]`).join('\n ');
19
+ throw new Error(`按名「${name}」找到 ${hits.length} 个候选,请指定 --id:\n ${list}`);
20
+ }
21
+ return hits[0].id ?? '';
22
+ }
23
+ /**
24
+ * 取主表业务字段清单(isdetail==0,排除系统字段)。**不按 viewattr 过滤**(静态 viewattr
25
+ * 不反映运行时可编辑性)。从 maindata 取当前值与选项。供 AI 向用户问答"填哪些"。
26
+ */
27
+ export async function getStartableFields(cfg, workflowId) {
28
+ const f = await describeFormFields(cfg, workflowId);
29
+ if (f.verifyRight === false)
30
+ throw new Error(`无权限(verifyRight:false),跑 bdip auth token-login 后重试`);
31
+ const main = f.tables.find((t) => t.key === 'main');
32
+ const fields = (main?.fields ?? [])
33
+ .filter((fld) => String(fld.isdetail ?? 0) === '0' && !SYS_FIELD_IDS.has(String(fld.id)))
34
+ .map((fld) => toStartable(fld, f.maindata));
35
+ // 浏览框(htmltype=3)且无 maindata 选项的,调通用 browser data 接口取前 10
36
+ for (const fld of fields) {
37
+ if (fld.htmltype === 3 && (!fld.options || fld.options.length === 0)) {
38
+ try {
39
+ fld.options = await getBrowserOptions(cfg, workflowId, fld.id, 10);
40
+ fld.inputHint = fld.options.length > 0 ? `浏览框(前10: 给 option id)` : '浏览选择(给 id)';
41
+ }
42
+ catch {
43
+ /* 取选项失败不影响主流程,用户仍可给 id */
44
+ }
45
+ }
46
+ }
47
+ // 当前用户相关字段(申请人/部门/日期等)填默认值
48
+ let user = null;
49
+ try {
50
+ user = await getCurrentUser(cfg);
51
+ }
52
+ catch { /* 取不到不影响 */ }
53
+ if (user) {
54
+ for (const fld of fields) {
55
+ const dv = defaultForField(fld.label, fld.htmltype, user);
56
+ if (dv)
57
+ fld.defaultValue = dv;
58
+ }
59
+ }
60
+ // 明细表(detail_1/detail_2/...):index=提交体后缀(detail_1→0,detail_2→1),取字段+浏览框选项
61
+ const detailTables = [];
62
+ for (const t of f.tables) {
63
+ if (!t.key.startsWith('detail_'))
64
+ continue;
65
+ const idx = Number(t.key.substring('detail_'.length)) - 1;
66
+ if (!Number.isFinite(idx) || idx < 0)
67
+ continue;
68
+ const dflds = t.fields.filter((fld) => !SYS_FIELD_IDS.has(String(fld.id)));
69
+ const dStartable = dflds.map((fld) => toStartable(fld, f.maindata));
70
+ for (const fld of dStartable) {
71
+ if (fld.htmltype === 3 && (!fld.options || fld.options.length === 0)) {
72
+ try {
73
+ fld.options = await getBrowserOptions(cfg, workflowId, fld.id, 10);
74
+ fld.inputHint = fld.options.length > 0 ? `明细浏览框(前10: 给 option id)` : '明细浏览选择(给 id)';
75
+ }
76
+ catch { /* 取选项失败不阻断 */ }
77
+ }
78
+ }
79
+ detailTables.push({ index: idx, key: t.key, fields: dStartable });
80
+ }
81
+ return {
82
+ workflowId: f.workflowId,
83
+ workflowName: f.workflowName,
84
+ verifyRight: f.verifyRight,
85
+ fields,
86
+ detailTables,
87
+ maindata: f.maindata,
88
+ submitParams: f.submitParams,
89
+ params: f.params,
90
+ linkageCfg: f.raw?.linkageCfg,
91
+ };
92
+ }
93
+ function toStartable(fld, maindata) {
94
+ const key = `field${fld.id}`;
95
+ const cell = maindata?.[key];
96
+ const special = Array.isArray(cell?.specialobj) ? cell.specialobj : [];
97
+ let options = special.filter((o) => o && (o.id != null || o.name != null)).map((o) => ({ id: String(o.id ?? ''), name: String(o.name ?? '') }));
98
+ const ht = fld.htmltype;
99
+ // 选择框(htmltype=5):选项在 fieldinfomap.selectattr.selectitemlist(不在 maindata)
100
+ if (ht === 5 && options.length === 0) {
101
+ const items = fld.raw?.selectattr?.selectitemlist;
102
+ if (Array.isArray(items)) {
103
+ options = items.filter((it) => it && it.selectname != null).map((it) => ({ id: String(it.selectvalue), name: String(it.selectname) }));
104
+ }
105
+ }
106
+ let inputHint;
107
+ if (ht === 3)
108
+ inputHint = '浏览选择(给 option id)';
109
+ else if (ht === 5 && options.length > 0)
110
+ inputHint = `选择框(给 option id)`;
111
+ else if (options.length > 1)
112
+ inputHint = '选择框(给 option id)';
113
+ else if (ht === 2)
114
+ inputHint = '多行文本';
115
+ else
116
+ inputHint = '文本';
117
+ const va = fld.viewattr;
118
+ return {
119
+ id: String(fld.id),
120
+ label: fld.label,
121
+ name: fld.name,
122
+ htmltype: ht,
123
+ htmltypeLabel: ht != null ? HTMLTYPE_LABEL[ht] ?? `ht=${ht}` : undefined,
124
+ viewattr: va,
125
+ viewattrLabel: va != null ? VIEWATTR_LABEL[va] ?? `va=${va}` : undefined,
126
+ currentValue: cell?.value != null ? String(cell.value) : undefined,
127
+ options: options.length > 0 ? options : undefined,
128
+ inputHint,
129
+ };
130
+ }
131
+ /**
132
+ * 构造新建提交体(扁平 form-urlencoded,匹配 create curl 字段集)。
133
+ * values: { "<id>": "值" }(业务字段值;图片/附件字段值=imagefileid;多选浏览框值=逗号分隔选项id)。
134
+ * requestname: 请求标题。browserNames: { "<fieldId>": "名称1##~~@@##名称2" }(多选浏览框的显示名,设 field<id>name)。
135
+ * details: 明细表行数据 [{index, rows:[{fieldId:value}, ...]}],index 对应 detailTables[].index(detail_1→0)。
136
+ * 每行每字段生成 field<id>_<rownum>=value(rownum 从 0 开始,每个明细表独立编号);
137
+ * + nodesnum<index>/indexnum<index>=行数 + submitdtlid<index>=0,0,...(每行一个0带尾逗号)+ deldtlid<index>=(空);
138
+ * + detailFieldUnEmptyCount=所有明细行非空字段总数;existChangeRange 追加 field<id>_<rownum>。
139
+ *
140
+ * 字段值结构(用户 create curl 确认):
141
+ * - 文本/数字字段:field<id>=<值>
142
+ * - 图片/附件字段:field<id>=<imagefileid>(annexdocids/annexdocinfos/field-annexupload 全空)
143
+ * - 多选浏览框:field<id>=<id1>,<id2> + field<id>name=<名1>##~~@@##<名2>
144
+ * - 明细行:field<id>_<rownum>=<值>(rownum 从 0,每明细表独立)
145
+ * - existChangeRange=field<id>,field<id>...(field 前缀);verifyRequiredRange=field-9999,field-1,(标题必填)
146
+ */
147
+ export function buildCreateSubmitBody(workflowId, values, requestname, params, submitParams, browserNames = {}, details = []) {
148
+ const wf = String(workflowId);
149
+ const p = params ?? {};
150
+ const sp = submitParams ?? {};
151
+ const filledIds = Object.keys(values).map((k) => (k.startsWith('field') ? k.replace(/^field/, '') : k));
152
+ // 当前流程创建人 userid(loadForm params 给定)。submit_token 等字段的 key 带此 userid 前缀,
153
+ // 不能硬编码 "1"——非 sysadmin=1 的环境(如阿汤哥 222222600)会取不到 submit_token 致"创建人和系统登录人不一致"。
154
+ const uid = String(p.f_weaver_belongto_userid ?? sp.f_weaver_belongto_userid ?? '1');
155
+ // 公文(odoc)流程标识:loadForm params.isWorkflowDoc=true 或 workflowtype=26。
156
+ // isOdocRequest/odocBtnFlag 浏览器提交体必带(否则 requestOperation 不进公文流转态,只保存不流转)。
157
+ const isWorkflowDoc = String(p.isWorkflowDoc ?? sp.isWorkflowDoc ?? 'false');
158
+ const isOdoc = isWorkflowDoc === 'true' || String(sp.workflowtype ?? p.workflowtype ?? '') === '26';
159
+ const body = {
160
+ formid: String(sp.formid ?? p.formid ?? ''),
161
+ 'f_weaver_belongto_userid': uid,
162
+ isWorkflowDoc,
163
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? sp.f_weaver_belongto_usertype ?? '0'),
164
+ nodetype: String(sp.nodetype ?? '0'),
165
+ method: '',
166
+ needoutprint: '',
167
+ src: 'submit',
168
+ isMultiDoc: '',
169
+ [`${uid}_${wf}_addrequest_submit_token`]: String(sp[`${uid}_${wf}_addrequest_submit_token`] ?? sp[`1_${wf}_addrequest_submit_token`] ?? ''),
170
+ topage: '',
171
+ workflowtype: String(sp.workflowtype ?? p.workflowtype ?? ''),
172
+ iscreate: '1',
173
+ comemessage: '',
174
+ remindTypes: '',
175
+ rand: '',
176
+ requestid: '-1',
177
+ linkageUUID: String(sp.linkageUUID ?? ''),
178
+ htmlfieldids: '',
179
+ needwfback: '1',
180
+ lastloginuserid: String(p.lastloginuserid ?? uid),
181
+ nodeid: String(sp.nodeid ?? p.nodeid ?? ''),
182
+ workflowid: wf,
183
+ isbill: String(sp.isbill ?? '1'),
184
+ annexdocids: '',
185
+ signdocids: '',
186
+ annexdocinfos: '',
187
+ handWrittenSign: '',
188
+ remark: '',
189
+ 'field-annexupload': '',
190
+ signworkflowids: '',
191
+ remarkLocation: '',
192
+ isOdocRequest: isOdoc ? '1' : '0',
193
+ enableIntervenor: '',
194
+ odocBtnFlag: isOdoc ? 'odocTransfer1' : '',
195
+ verifyRequiredRange: 'field-9999,field-1,',
196
+ linkageUnFinishedKey: '',
197
+ remarkquote: '',
198
+ actiontype: 'requestOperation',
199
+ isFirstSubmit: '',
200
+ existChangeRange: filledIds.map((id) => `field${id}`).join(','),
201
+ requestname,
202
+ requestlevel: String(values['-2'] ?? '0'),
203
+ 'field-10': values['-10'] ?? values['field-10'] ?? '4',
204
+ 'field-9': '',
205
+ };
206
+ // 业务字段值(扁平 field<id>=<value>);浏览框字段额外设 field<id>name
207
+ for (const [k, v] of Object.entries(values)) {
208
+ const id = k.startsWith('field') ? k.replace(/^field/, '') : k;
209
+ if (SYS_FIELD_IDS.has(id))
210
+ continue; // 系统字段已单独处理
211
+ body[`field${id}`] = v;
212
+ if (browserNames[id])
213
+ body[`field${id}name`] = browserNames[id];
214
+ }
215
+ body.remark = String(values['-4'] ?? '');
216
+ body.mainFieldUnEmptyCount = String(filledIds.filter(id => !SYS_FIELD_IDS.has(id) && body['field'+id] !== '' && body['field'+id] != null).length);
217
+ // 明细表行:每行每字段 field<id>_<rownum>=value(rownum 从 0,每明细表独立)+ 行控制字段
218
+ const existDetail = [];
219
+ let detailUnEmpty = 0;
220
+ for (const dt of details) {
221
+ const rows = dt.rows || [];
222
+ if (rows.length === 0)
223
+ continue; // 空明细表不设行控制(新建场景无删行)
224
+ body[`nodesnum${dt.index}`] = String(rows.length);
225
+ body[`indexnum${dt.index}`] = String(rows.length);
226
+ body[`submitdtlid${dt.index}`] = rows.map(() => '0').join(',') + ',';
227
+ body[`deldtlid${dt.index}`] = '';
228
+ rows.forEach((row, rownum) => {
229
+ for (const [k, v] of Object.entries(row)) {
230
+ const id = k.startsWith('field') ? k.replace(/^field/, '') : k;
231
+ if (SYS_FIELD_IDS.has(id))
232
+ continue;
233
+ body[`field${id}_${rownum}`] = v;
234
+ if (browserNames[id])
235
+ body[`field${id}_${rownum}name`] = browserNames[id];
236
+ if (v !== '') {
237
+ detailUnEmpty++;
238
+ existDetail.push(`field${id}_${rownum}`);
239
+ }
240
+ }
241
+ });
242
+ }
243
+ body.detailFieldUnEmptyCount = String(detailUnEmpty);
244
+ // existChangeRange 追加明细非空字段(带 _<rownum> 后缀)
245
+ if (existDetail.length > 0)
246
+ body.existChangeRange = (body.existChangeRange ? body.existChangeRange + ',' : '') + existDetail.join(',');
247
+ body.signatureAttributesStr = String(p.signatureAttributesStr ?? '');
248
+ body.signatureSecretKey = String(p.signatureSecretKey ?? '');
249
+ body.selectNextFlow = '0';
250
+ body.openDataVerify = '0';
251
+ body.wfTestStr = '';
252
+ return body;
253
+ }
254
+ /**
255
+ * 解析多选浏览框(htmltype=3)字段的显示名。
256
+ * values 里某字段值=逗号分隔选项 id,这里查 browser data 接口拼成 "名1##~~@@##名2",
257
+ * 供提交体设 field<id>name。取不到不阻断(提交体仍发 field<id>=ids)。
258
+ *
259
+ * 由 create start 的 dry-run 预览与真提交共享,避免两处重复 getBrowserOptions+拼名逻辑。
260
+ */
261
+ export async function resolveBrowserNames(cfg, workflowId, fields, values, details = []) {
262
+ const bn = {};
263
+ for (const fld of fields) {
264
+ if (fld.htmltype !== 3)
265
+ continue;
266
+ const id = String(fld.id);
267
+ const fieldValues = [values[id]];
268
+ for (const detail of details) {
269
+ for (const row of detail.rows ?? [])
270
+ fieldValues.push(row[id] ?? row[`field${id}`]);
271
+ }
272
+ const ids = [...new Set(fieldValues
273
+ .filter((value) => value !== undefined && value !== null && value !== '')
274
+ .flatMap((value) => String(value).split(','))
275
+ .map((value) => value.trim())
276
+ .filter(Boolean))];
277
+ if (ids.length === 0)
278
+ continue;
279
+ try {
280
+ const opts = await getBrowserOptions(cfg, workflowId, String(fld.id), 50);
281
+ const names = ids
282
+ .map((id) => opts.find((o) => o.id === id)?.name ?? '')
283
+ .filter(Boolean);
284
+ if (names.length > 0)
285
+ bn[id] = names.join('##~~@@##');
286
+ }
287
+ catch {
288
+ /* 解析失败不阻断 */
289
+ }
290
+ }
291
+ return bn;
292
+ }
293
+ /**
294
+ * 选择框/浏览框字段:中文选项名 → 选项 ID 自动转换。
295
+ * 遍历所有表(主表+明细表)的字段,若字段 htmltype=3(浏览框) 或 5(选择框),
296
+ * 将用户传入的选项名(中文)匹配为选项 id。name→id 匹配完成后原地修改 merged。
297
+ * 选项来源:先查 maindata 的 specialobj,再查 fieldinfomap 的 selectattr.selectitemlist。
298
+ */
299
+ function resolveOptionNames(tables, maindata, merged) {
300
+ for (const t of tables) {
301
+ for (const fld of t.fields ?? []) {
302
+ const ht = fld.htmltype;
303
+ if (ht !== 3 && ht !== 5)
304
+ continue;
305
+ const id = String(fld.id);
306
+ const v = merged[id];
307
+ if (v === undefined || v === '')
308
+ continue;
309
+ // 收集选项:先 maindata.specialobj → 再 fieldinfomap.selectattr
310
+ const key = `field${id}`;
311
+ const cell = maindata?.[key];
312
+ const special = Array.isArray(cell?.specialobj) ? cell.specialobj : [];
313
+ let selectItems = special.filter((o) => o && (o.id != null || o.name != null)).map((o) => ({ selectvalue: String(o.id ?? ''), selectname: String(o.name ?? '') }));
314
+ if (selectItems.length === 0) {
315
+ const rawItems = fld.raw?.selectattr?.selectitemlist;
316
+ if (Array.isArray(rawItems))
317
+ selectItems = rawItems.filter((it) => it && it.selectname != null);
318
+ }
319
+ if (selectItems.length === 0)
320
+ continue;
321
+ // 按逗号拆分(支持多选)
322
+ const parts = String(v).split(',').map((s) => s.trim());
323
+ const converted = parts.map((p) => {
324
+ const match = selectItems.find((it) => it && String(it.selectname).trim() === p);
325
+ if (match)
326
+ return String(match.selectvalue ?? match.id ?? '');
327
+ return p; // 不匹配则保持原值(可能是 id 本身)
328
+ });
329
+ const result = converted.join(',');
330
+ if (result !== String(v)) {
331
+ merged[id] = result;
332
+ }
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * 字段联动:已填字段触发联动,自动补上关联字段值。
338
+ * 遍历所有表的字段,若有 linkageId 且已填值,调 reqDataInputResult 获取联动值,
339
+ * 合并进 merged(不覆盖用户已填的值)。
340
+ *
341
+ * 接口:POST /api/workflow/linkage/reqDataInputResult
342
+ * 参数:linkageid(来自 fieldinfomap.linkageId)、linkageUUID(来自 loadForm)
343
+ * 响应:assignInfo_{linkageId}.changeValue = { fieldNNN: { value, specialobj } }
344
+ */
345
+
346
+ /**
347
+ * 真正提交(新建流程)。POST /api/workflow/reqform/requestOperation(aiAgentToken)。
348
+ * files: {fieldId, filePath, listType?}[] —— 先上传拿 imagefileid,并入 field<id>(不设 annex 字段)。
349
+ * values 里多选浏览框字段的值=逗号分隔选项id,自动解析对应 name 设 field<id>name。
350
+ * details: 明细表行数据 [{index, rows:[{fieldId:value}]}],每明细表 index 对应 detailTables[].index(detail_1→0)。
351
+ * 明细浏览框字段解析并提交 id 与 name;确认后由提交计划冻结,避免二次解析漂移。
352
+ * 返回 E9Response(成功通常含 requestid)。
353
+ */
354
+ /**
355
+ * ★字段属性硬规则:只允许填写「可编辑/必填」(viewattr=2/3) 的字段。
356
+ * 只读(1)/隐藏(4) 字段**禁止**由用户/助手填值——不得绕过表单字段属性自行填值。
357
+ * 明细表字段同样按 viewattr 判定。无属性信息(viewattr 缺失)不拦,交给后端判定,避免误伤。
358
+ */
359
+ export function findNonEditableFields(form, values, details = []) {
360
+ const byId = new Map();
361
+ for (const table of form?.tables ?? []) {
362
+ for (const fld of table.fields ?? [])
363
+ byId.set(String(fld.id), fld);
364
+ }
365
+ const bad = [];
366
+ const checkId = (rawId) => {
367
+ const id = String(rawId ?? '').replace(/^field/, '');
368
+ const fld = byId.get(id);
369
+ if (!fld)
370
+ return; // 未知字段交给后端判定
371
+ // 只拦「明确的」只读(1)/隐藏(4);va=2/3 可编辑或必填放行;va=0 或缺失=属性未设置(实测量到「签字意见/标题/紧急程度/密级」
372
+ // 等系统字段是 va=0),按"无法判定即不拦"处理,避免把本可填的系统字段误拒。
373
+ if (Number(fld.viewattr) !== 1 && Number(fld.viewattr) !== 4)
374
+ return;
375
+ bad.push({ id, label: fld.label, viewattr: fld.viewattr });
376
+ };
377
+ for (const k of Object.keys(values ?? {}))
378
+ checkId(k);
379
+ for (const d of details ?? [])
380
+ for (const row of d?.rows ?? [])
381
+ for (const k of Object.keys(row ?? {}))
382
+ checkId(k);
383
+ return bad;
384
+ }
385
+
386
+ export async function prepareCreateSubmission(cfg, workflowId, values, requestname, details = []) {
387
+ const {prepareFields} = await import('./field-preparation.js');
388
+ const api=await createApiFromProfile(cfg);
389
+ const r=await api.post('/api/workflow/reqform/loadForm',{iscreate:'1',workflowid:String(workflowId)},{forceCookie:true});
390
+ const form=r.json;
391
+ const plan={workflowId:String(workflowId),requestName:requestname||form?.params?.workflowname,values,details,accountId:String(form?.params?.currentUserid),baseUrl:cfg.baseUrl};
392
+ const prepared=await prepareFields(cfg,plan,{api,form});
393
+ return {...plan,workflowName:form.params.workflowname,prepared,body:prepared.body};
394
+ }
395
+ export async function submitCreateRequest(cfg, workflowId, values, requestname, files = [], details = [], prepared = undefined) {
396
+ const {actionBody,classifyWrite,verifyAction,checkBaseline,checkAdapter}=await import('./field-preparation.js');
397
+ const {detailTables}=await import('./workflow-runtime.js');
398
+ const plan=prepared??await prepareCreateSubmission(cfg,workflowId,values,requestname,details);
399
+ if((plan.files??files).length)throw Error('附件需先上传并纳入完整预览,不在确认后静默修改');
400
+ if(!plan.prepared)throw Error('旧版提交计划缺少统一联动准备,请重新预览');
401
+ const api=await createApiFromProfile(cfg);
402
+ const r=await api.post('/api/workflow/reqform/loadForm',{workflowid:String(workflowId),iscreate:'1'},{forceCookie:true});
403
+ await checkAdapter(cfg,plan,r.json);
404
+ const originalTables=await detailTables(api,r.json,workflowId);
405
+ checkBaseline(plan.prepared,r.json,originalTables);
406
+ const body=actionBody(plan.prepared,r.json,'submit');
407
+ const started=beginSubmission({workflowId,workflowName:plan.workflowName,requestName:plan.requestName,values:plan.values});
408
+ if(started.duplicate)throw Error('已有提交记录,禁止重复新建,请先核验');
409
+ if(plan.id)updateSubmission(started.record.id,{verificationPlanId:plan.id});
410
+ try {
411
+ const response=await api.post('/api/workflow/reqform/requestOperation',body,{forceCookie:true});
412
+ const result=classifyWrite(response);
413
+ if(result.state==='failed'){const submission=updateSubmission(started.record.id,{status:'failed',...result});return {response,submission,outcome:'failed'};}
414
+ let problems=['返回结果不明'];
415
+ if(result.state==='written-unverified'&&result.requestId){
416
+ const read=await api.post('/api/workflow/reqform/loadForm',{workflowid:String(workflowId),requestid:String(result.requestId),iscreate:'0'},{forceCookie:true});
417
+ const tables=await detailTables(api,read.json,workflowId,result.requestId);
418
+ problems=verifyAction(plan.prepared.expected,read.json,tables,'submit');
419
+ }
420
+ const submission=updateSubmission(started.record.id,{status:problems.length?'uncertain':'created',...result,problems});
421
+ return {response,submission,outcome:problems.length?'uncertain':'created'};
422
+ }catch(error){updateSubmission(started.record.id,{status:'uncertain',error:String(error.message)});throw Error('提交结果待核验,禁止自动重试:'+error.message);}
423
+ }
424
+ export async function getOdocNextNodes(cfg, requestid, workflowId, params, submitParams) {
425
+ const p = params ?? {};
426
+ const sp = submitParams ?? {};
427
+ const uid = String(p.f_weaver_belongto_userid ?? sp.f_weaver_belongto_userid ?? '1');
428
+ const api = await createApiFromProfile(cfg);
429
+ const r = await api.post('/api/odoc/odocrequest/loadData', {
430
+ dataId: '', requestid, workflowid: String(workflowId),
431
+ nodeid: String(sp.nodeid ?? p.nodeid ?? ''), formid: String(sp.formid ?? p.formid ?? ''),
432
+ isbill: String(sp.isbill ?? p.isbill ?? '1'),
433
+ 'f_weaver_belongto_userid': uid, 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? sp.f_weaver_belongto_usertype ?? '0'),
434
+ creater: uid, creatertype: '0', requestType: '2', remindTypes: '',
435
+ authSignatureStr: '', authStr: '', agentType: '', agentorByAgentId: '',
436
+ });
437
+ const j = r.json ?? {};
438
+ const list = Array.isArray(j.nextNodeList) ? j.nextNodeList : [];
439
+ return list.map((n) => ({
440
+ nodeid: Number(n.nodeid),
441
+ linkId: Number(n.linkId),
442
+ nodename: String(n.nodename ?? ''),
443
+ operators: Array.isArray(n.nodeOperators) ? n.nodeOperators.map((o) => ({ id: String(o.id ?? o.operatorid ?? ''), name: String(o.name ?? o.operatorname ?? '') })) : [],
444
+ defaultUserIds: String(n.nodeOperatorIds ?? ''),
445
+ isMustSelected: !!n.isMustSelected,
446
+ }));
447
+ }
448
+ /**
449
+ * 公文流转下一步。POST /api/odoc/odocrequest/flowNext(aiAgentToken 鉴权)。
450
+ *
451
+ * 公文流程 requestOperation(iscreate=1)只保存+进 ASYNC_SUBMIT 态,未推送。调本接口把请求推到
452
+ * 下一审批节点,公文才算"提交完成"。nextDirections 必须指定流转对象(userIds+nodeid+linkId),
453
+ * 由调用方从 getOdocNextNodes 选定后传入——空 nextDirections 是假成功(不真流转)。
454
+ *
455
+ * 链路(浏览器抓包对齐):getLastOperateInfo 取真实 lastOperate → POST flowNext(带选定 nextDirections)。
456
+ * 复用 requestOperation 提交体的 verifyRequiredRange(必填校验范围,flowNext 同样校验)。
457
+ *
458
+ * 返回 {success, msg}(success=1 即"提交成功",流程已流转到下一节点)。
459
+ */
460
+ export async function odocFlowNext(cfg, requestid, workflowId, params, submitParams, submitBody, nextDirections) {
461
+ const p = params ?? {};
462
+ const sp = submitParams ?? {};
463
+ const uid = String(p.f_weaver_belongto_userid ?? sp.f_weaver_belongto_userid ?? '1');
464
+ const wf = String(workflowId);
465
+ const api = await createApiFromProfile(cfg);
466
+ // 1) getLastOperateInfo — 取真实 lastOperate(flowNext body 必须带,否则报"处理前已流转"误报)
467
+ let lastOperateDate = todayStr();
468
+ let lastOperator = uid;
469
+ let lastOperateTime = '';
470
+ try {
471
+ const last = await api.post('/api/odoc/odocrequest/getLastOperateInfo', { requestid });
472
+ const lj = last.json ?? {};
473
+ if (lj.lastOperateDate)
474
+ lastOperateDate = String(lj.lastOperateDate);
475
+ if (lj.lastOperator)
476
+ lastOperator = String(lj.lastOperator);
477
+ if (lj.lastOperateTime)
478
+ lastOperateTime = String(lj.lastOperateTime);
479
+ }
480
+ catch { /* 取不到用默认,不阻断 */ }
481
+ // 2) flowNext — nextDirections 由调用方选定(来自 getOdocNextNodes),空则不流转
482
+ const body = {
483
+ formid: String(sp.formid ?? p.formid ?? ''),
484
+ 'f_weaver_belongto_userid': uid,
485
+ isWorkflowDoc: 'true',
486
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? sp.f_weaver_belongto_usertype ?? '0'),
487
+ nodetype: String(sp.nodetype ?? p.nodetype ?? '0'),
488
+ method: '', needoutprint: '', src: '',
489
+ isMultiDoc: '', topage: '',
490
+ workflowtype: String(sp.workflowtype ?? p.workflowtype ?? '26'),
491
+ iscreate: '0', comemessage: '', remindTypes: '', rand: '',
492
+ requestid, linkageUUID: '', htmlfieldids: '', needwfback: '',
493
+ lastloginuserid: String(p.lastloginuserid ?? uid),
494
+ nodeid: String(sp.nodeid ?? p.nodeid ?? ''),
495
+ workflowid: wf,
496
+ isbill: String(sp.isbill ?? p.isbill ?? '1'),
497
+ [`${uid}_${wf}_addrequest_submit_token`]: '',
498
+ annexdocids: '', signdocids: '', annexdocinfos: '', handWrittenSign: '', remark: '',
499
+ 'field-annexupload': '', signworkflowids: '', remarkLocation: '',
500
+ isOdocRequest: '1', enableIntervenor: '0', odocBtnFlag: 'odocTransfer1',
501
+ verifyRequiredRange: submitBody.verifyRequiredRange ?? 'field-9999,field-1,',
502
+ linkageUnFinishedKey: '', isremark: '0',
503
+ [`${uid}_${requestid}_request_submit_token`]: '',
504
+ lastOperateDate, lastOperator, lastOperateTime,
505
+ creater: uid, creatertype: '0', requestType: '2',
506
+ authSignatureStr: '', authStr: '', agentType: '', agentorByAgentId: '',
507
+ isend: '1', assignRemark: '',
508
+ nextDirections: JSON.stringify(nextDirections.map((d) => ({
509
+ userIds: d.userIds, ccUserIds: '', userSignType: '', userTransferType: '', userCopyType: '',
510
+ nodeid: d.nodeid, linkId: d.linkId,
511
+ }))),
512
+ };
513
+ const r = await api.post('/api/odoc/odocrequest/flowNext', body);
514
+ const j = r.json ?? {};
515
+ return { success: j.success === 1 || j.success === true, msg: String(j.msg ?? '') };
516
+ }
517
+ /**
518
+ * 审批路径预览:获取流程的所有节点及操作者信息。
519
+ * POST /api/workflow/layout/getSimpleModeData
520
+ * 返回 nodeInfo 数组,每节点含 nodeName/operatorName/nodeType 等。
521
+ */
522
+ export async function getWorkflowPreview(cfg, requestId) {
523
+ const api = await createApiFromProfile(cfg);
524
+ // 先通过 loadForm 获取 workflowid
525
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
526
+ const j0 = r0.json ?? {};
527
+ const p0 = j0.params ?? {};
528
+ const wfId = String(p0.workflowid ?? '');
529
+ if (!wfId)
530
+ throw new Error('无法获取 workflowid');
531
+ const r = await api.post('/api/workflow/layout/getSimpleModeData', {
532
+ requestid: requestId,
533
+ requestType: '0',
534
+ workflowid: wfId,
535
+ });
536
+ const j = r.json ?? {};
537
+ const nodeInfo = Array.isArray(j.nodeInfo) ? j.nodeInfo : [];
538
+ const forecastInfo = j.forecastInfo ?? null;
539
+ return {
540
+ nodeInfo: nodeInfo.map((n) => ({
541
+ nodeName: String(n.nodeName ?? ''),
542
+ nodeOperatorName: String(n.nodeOperatorName ?? ''),
543
+ nodeType: n.nodeType,
544
+ isCurrent: n.isCurrent === true || n.isCurrent === 'true',
545
+ isPassed: n.isPassed === true || n.isPassed === 'true',
546
+ })),
547
+ forecastInfo: forecastInfo ? {
548
+ forecastNodeName: String(forecastInfo.forecastNodeName ?? ''),
549
+ forecastUserName: String(forecastInfo.forecastUserName ?? ''),
550
+ } : null,
551
+ };
552
+ }
553
+ /**
554
+ * 联动解析:根据已填字段值,调用 reqDataInputResult 获取联动字段值。
555
+ * 从 loadForm.linkageCfg.dataInputCfg 读取联动配置,对每个触发字段逐一调用。
556
+ * 返回 { "<fieldId>": { value, specialobj } } 的变更值映射。
557
+ */
558
+ export async function resolveLinkage() { throw Error('独立联动接口已停用;请使用统一字段准备链路,避免丢失明细/公式联动'); }
@@ -0,0 +1,89 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ export async function getFlowSummary(cfg, requestId) {
3
+ const api = await createApiFromProfile(cfg);
4
+ // 第一步 loadForm(不带 wf+node)→ 拿 workflowid/nodeid
5
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
6
+ const j0 = r0.json ?? {};
7
+ const wfId = String(j0.params?.workflowid ?? '');
8
+ const nodeId = String(j0.params?.nodeid ?? '');
9
+ // 第二步 loadForm(带 wf+node)→ 拿 per-request 签名(不带则签名字段空)
10
+ let j = j0;
11
+ if (wfId && nodeId) {
12
+ const r1 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId, workflowid: wfId, nodeid: nodeId });
13
+ j = r1.json ?? j0;
14
+ }
15
+ const fm = j.tableInfo?.main?.fieldinfomap ?? {};
16
+ const md = j.maindata ?? {};
17
+ const fields = Object.entries(fm).map(([id, f]) => ({
18
+ id,
19
+ label: f?.fieldlabel,
20
+ name: f?.fieldname,
21
+ htmltypeLabel: htmltypeLabel(f?.htmltype),
22
+ value: md[`field${id}`]?.value != null ? String(md[`field${id}`].value) : undefined,
23
+ })).filter((f) => f.value);
24
+ // 流转日志:先调 getRequestLogBaseInfo 取真实的 viewLogIds/creatorNodeId
25
+ const p = j.params ?? {};
26
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
27
+ const sigStr = String(p.signatureAttributesStr ?? '');
28
+ const sigKey = String(p.signatureSecretKey ?? '');
29
+ const commonLogParams = {
30
+ requestid: requestId,
31
+ requestType: '0',
32
+ workflowid: String(p.workflowid ?? ''),
33
+ nodeid: String(p.nodeid ?? ''),
34
+ signatureAttributesStr: sigStr,
35
+ signatureSecretKey: sigKey,
36
+ 'f_weaver_belongto_userid': userId,
37
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
38
+ authStr: String(p.authStr ?? ''),
39
+ authSignatureStr: String(p.authSignatureStr ?? ''),
40
+ wfTestStr: '',
41
+ };
42
+ let logs = [];
43
+ let logDebug = { viewLogIds: '', creatorNodeId: '', source: 'none' };
44
+ try {
45
+ // ① getRequestLogBaseInfo
46
+ let viewLogIds = '';
47
+ let creatorNodeId = '';
48
+ const rBase = await api.post('/api/workflow/reqform/getRequestLogBaseInfo', { ...commonLogParams });
49
+ const baseText = rBase.text ?? '';
50
+ const vlm = baseText.match(/["']?viewLogIds["']?\s*[:=]\s*["']?(\d+(?:%2C|,)?(?:\d+(?:%2C|,)?)*)["']?/i);
51
+ const cnm = baseText.match(/["']?creatorNodeId["']?\s*[:=]\s*["']?(\d+)["']?/i);
52
+ if (vlm)
53
+ viewLogIds = vlm[1].replace(/%2C/gi, ',');
54
+ if (cnm)
55
+ creatorNodeId = cnm[1];
56
+ if (!viewLogIds && p.viewLogIds)
57
+ viewLogIds = String(p.viewLogIds);
58
+ if (!creatorNodeId && p.creatorNodeId)
59
+ creatorNodeId = String(p.creatorNodeId);
60
+ logDebug = { viewLogIds, creatorNodeId, source: (viewLogIds || creatorNodeId) ? 'getRequestLogBaseInfo' : 'none' };
61
+ const rl = await api.post('/api/workflow/reqform/getRequestLogList', {
62
+ ...commonLogParams,
63
+ isFromWfForm: '1',
64
+ isprint: '0', isurger: 'false', submit: '0', pgnumber: '1',
65
+ maxrequestlogid: '', viewLogIds, creatorNodeId,
66
+ isHideInput: '', isFormSignature: '', wfsignlddtcnt: '10', orderbytype: '1',
67
+ logpagesize: '', loadmethod: 'split', operatorid: '', deptid: '', subcomid: '',
68
+ content: '', nodename: '', createdateselect: '0', createdatefrom: '', createdateto: '',
69
+ fromCommunication: '', __random__: String(Date.now()),
70
+ });
71
+ logs = rl.json?.loglist ?? [];
72
+ }
73
+ catch { /* 日志取失败不阻断 */ }
74
+ return {
75
+ requestId,
76
+ workflowName: j.cellInfo?.sysFieldInfo?.workflowname,
77
+ workflowId: String(j.params?.workflowid ?? ''),
78
+ nodeId: String(j.params?.nodeid ?? ''),
79
+ verifyRight: j.params?.verifyRight === true,
80
+ fields,
81
+ logs,
82
+ logDebug,
83
+ raw: j,
84
+ };
85
+ }
86
+ function htmltypeLabel(ht) {
87
+ const m = { 1: '文本', 2: '多行文本', 3: '浏览选择', 4: '附件', 5: '选择框', 6: '复选框' };
88
+ return m[ht] ?? `ht=${ht}`;
89
+ }