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,172 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { getWfList } from './workflow.js';
3
+ import { extractRows } from './parse.js';
4
+
5
+ /**
6
+ * 流程批量操作(待办域)。
7
+ *
8
+ * 接口契约以 2026-09-09 抓包实证为准:
9
+ * - 批量提交 POST /api/workflow/reqform/requestBatchSubmit
10
+ * multiSubIds=<rid,rid,>&belongtoUserids=<uid,uid,>&pagefromtype=1&needBatchSubmit=1
11
+ * - 批量转发 GET /api/workflow/forward/getBatchForwardInfo(预检)
12
+ * POST /api/workflow/reqform/batchForward
13
+ * forwardflag=1&field5=<接收人>&multiSubIds=<rid_uid,rid_uid>&operate=save
14
+ * - 批量关注 POST /api/workflow/requestAttention/getAttentionTypeSet(查类型)
15
+ * POST /api/workflow/requestAttention/saveAttention
16
+ * src=batchadd&requestids=<rid,rid>&ids=<类型id,>
17
+ * - 门禁 POST /api/ec/dev/table/checks(dataKey + checkId + randomDatas=每行 randomField0~14+randomFieldId)
18
+ * - 二次验证 POST /api/workflow/secondauth/getSecondAuthConfig4List(requestIds=rid_uid + __weaver_request_url__=base64(baseUrl))
19
+ *
20
+ * multiSubIds 尾逗号、ids 尾逗号、belongtoUserids 尾逗号均为抓包原样,不能去掉。
21
+ */
22
+
23
+ /** 将用户输入的逗号列表规范化,并在真正调用前阻止常见的错配。 */
24
+ export function parseIdList(value, label) {
25
+ const ids = String(value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
26
+ if (!ids.length)
27
+ throw new Error(`${label}不能为空,请用英文逗号分隔多个 ID。`);
28
+ if (ids.some((id) => !/^\d+$/.test(id)))
29
+ throw new Error(`${label}只能包含数字 ID。`);
30
+ return ids;
31
+ }
32
+
33
+ /** 从待办列表原始行提取门禁所需的随机字段(与抓包 checks 的 randomDatas 结构一致)。 */
34
+ function pickRandomFields(row) {
35
+ const out = {};
36
+ for (let i = 0; i <= 14; i++)
37
+ out[`randomField${i}`] = String(row?.[`randomField${i}`] ?? '');
38
+ out.randomFieldId = String(row?.randomFieldId ?? row?.requestid ?? row?.randomField2 ?? '');
39
+ return out;
40
+ }
41
+
42
+ /** 分类 → 人类可读列表名(用于进度与报错文案)。 */
43
+ const CATEGORY_LABEL = { todo: '待办(待处理)', done: '已办', mine: '我发起的' };
44
+
45
+ /**
46
+ * 在指定分类列表中取出与 requestIds 匹配的原始行(含 randomField)+ dataKey。跨页匹配直到全部命中或到末尾。
47
+ * opts.category:'todo'(默认,待处理)/ 'done'(已办)/ 'mine'(我发起的)——关注在三个分类里都可用;
48
+ * 提交/转发只对待办有意义,调用方自行传 'todo'。
49
+ */
50
+ async function loadRowsFor(cfg, requestIds, opts = {}) {
51
+ const category = opts.category ?? 'todo';
52
+ const label = CATEGORY_LABEL[category] ?? category;
53
+ // 修复 #6:逐页定位是最耗时环节,实时打印进度,避免长时间无输出被误判卡死。
54
+ const wanted = new Set(requestIds.map(String));
55
+ const found = new Map();
56
+ let dataKey = '';
57
+ console.error(` [批量] 正在「${label}」列表定位 ${requestIds.length} 条流程(按请求顺序逐页搜索)...`);
58
+ for (let current = 1; current <= 10; current++) {
59
+ console.error(` [批量] 读取${label}第 ${current} 页...`);
60
+ const list = await getWfList(cfg, { category, page: current, pageSize: '100' });
61
+ dataKey = list.dataKey || dataKey;
62
+ const rows = extractRows(list.raw?.datas);
63
+ for (const row of rows) {
64
+ const rid = String(row?.requestid ?? row?.randomField2 ?? row?.randomFieldId ?? '');
65
+ if (wanted.has(rid) && !found.has(rid))
66
+ found.set(rid, row);
67
+ }
68
+ console.error(` [批量] 第 ${current} 页已定位 ${found.size}/${requestIds.length} 条`);
69
+ if (rows.length < 100 || found.size === wanted.size)
70
+ break;
71
+ if (!rows.length)
72
+ break;
73
+ }
74
+ const missing = requestIds.map(String).filter((id) => !found.has(id));
75
+ if (missing.length)
76
+ throw new Error(`批量操作需要在「${label}」列表中定位这 ${missing.length} 条,未找到:${missing.join(', ')}。请确认 request-id 属于该列表(可用 --category 切换 待办/已办/我发起),或该流程已不在列表中。`);
77
+ // 保持调用方传入顺序,便于回执与 multiSubIds 对应
78
+ const rows = requestIds.map((id) => found.get(String(id)));
79
+ return { dataKey, rows };
80
+ }
81
+
82
+ /** 批量门禁(抓包实证):checks(dataKey+randomDatas) + getSecondAuthConfig4List(requestIds=rid_uid+__weaver_request_url__=base64)。 */
83
+ async function runGate(api, checkId, rows, dataKey) {
84
+ console.error(` [批量] 执行门禁检查(${checkId})...`);
85
+ const check = await api.post('/api/ec/dev/table/checks', {
86
+ dataKey,
87
+ checkId,
88
+ randomDatas: JSON.stringify(rows.map(pickRandomFields)),
89
+ });
90
+ const ridUids = rows
91
+ .map((r) => `${r?.randomField2 ?? r?.requestid}_${r?.randomField4 ?? r?.userid ?? ''}`)
92
+ .join(',');
93
+ const secondAuth = await api.post('/api/workflow/secondauth/getSecondAuthConfig4List', {
94
+ requestIds: ridUids,
95
+ __weaver_request_url__: Buffer.from(api.baseUrl).toString('base64'),
96
+ });
97
+ return { check, secondAuth };
98
+ }
99
+
100
+ /** 批量提交。调用方必须先展示预览,且只有 --yes 才会调用本函数。 */
101
+ export async function batchSubmit(cfg, opts) {
102
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
103
+ const userIds = parseIdList(opts.userIds, 'userIds');
104
+ if (requestIds.length !== userIds.length)
105
+ throw new Error('requestIds 与 userIds 数量必须一致,确保每条待办对应正确处理人。');
106
+ const api = await createApiFromProfile(cfg);
107
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds);
108
+ const gate = await runGate(api, 'batchSubmit', rows, dataKey);
109
+ console.error(` [批量] 提交 ${requestIds.length} 条流程(requestBatchSubmit)...`);
110
+ const result = await api.post('/api/workflow/reqform/requestBatchSubmit', {
111
+ multiSubIds: requestIds.join(',') + ',',
112
+ remark: String(opts.remark ?? 'OK'),
113
+ pagefromtype: '1',
114
+ belongtoUserids: userIds.join(',') + ',',
115
+ needBatchSubmit: '1',
116
+ });
117
+ console.error(` [批量] 提交响应已返回(status=${result?.status ?? '?'})`);
118
+ return { requestIds, userIds, gate, result };
119
+ }
120
+
121
+ /** 批量转发(抓包实证:batchForward,multiSubIds=requestId_userId)。 */
122
+ export async function batchForward(cfg, opts) {
123
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
124
+ const receiver = parseIdList(opts.receiver, 'receiver');
125
+ const api = await createApiFromProfile(cfg);
126
+ // 前置预检(抓包:getBatchForwardInfo → {"allowAllPersonGroup":true})
127
+ const info = await api.get('/api/workflow/forward/getBatchForwardInfo');
128
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds);
129
+ const gate = await runGate(api, 'batchForward', rows, dataKey);
130
+ // multiSubIds = requestId_userId(userId 为待办当前处理人,来自列表行 randomField4)
131
+ const multiSubIds = rows
132
+ .map((r) => `${r?.randomField2 ?? r?.requestid}_${r?.randomField4 ?? r?.userid ?? ''}`)
133
+ .join(',');
134
+ const result = await api.post('/api/workflow/reqform/batchForward', {
135
+ forwardflag: '1',
136
+ field5: receiver.join(','),
137
+ remark: String(opts.remark ?? ''),
138
+ multiSubIds,
139
+ operate: 'save',
140
+ });
141
+ return { requestIds, receiver, info, gate, result };
142
+ }
143
+
144
+ /** 查询关注类型(只读,供 dry-run 预览展示与 batchAttention 选择类型)。 */
145
+ export async function getAttentionTypes(cfg, requestIds) {
146
+ const api = await createApiFromProfile(cfg);
147
+ const resp = await api.post('/api/workflow/requestAttention/getAttentionTypeSet', {
148
+ src: 'getData2',
149
+ requestid: requestIds.join(','),
150
+ });
151
+ return resp?.json?.datas ?? [];
152
+ }
153
+
154
+ /** 批量关注(抓包实证:getAttentionTypeSet → saveAttention(src=batchadd, ids=类型id,))。 */
155
+ export async function batchAttention(cfg, opts) {
156
+ const requestIds = parseIdList(opts.requestIds, 'requestIds');
157
+ const api = await createApiFromProfile(cfg);
158
+ const types = await getAttentionTypes(cfg, requestIds);
159
+ const attentionId = String(opts.attentionId ?? types[0]?.id ?? '');
160
+ if (!attentionId)
161
+ throw new Error('未能获取关注类型(getAttentionTypeSet 返回空),请确认该环境的“关注类型”已配置,或手动传 --attention-id。');
162
+ // 关注在 待办/已办/我发起 三个分类下都有「我的关注」页签,故定位列表可指定(默认待办)
163
+ const category = opts.category ?? 'todo';
164
+ const { dataKey, rows } = await loadRowsFor(cfg, requestIds, { category });
165
+ const gate = await runGate(api, 'batchAttention', rows, dataKey);
166
+ const result = await api.post('/api/workflow/requestAttention/saveAttention', {
167
+ src: 'batchadd',
168
+ requestids: requestIds.join(','),
169
+ ids: attentionId + ',',
170
+ });
171
+ return { requestIds, category, attentionTypes: types, attentionId, gate, result };
172
+ }
package/dist/todocc.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { todayStr, nowTimeStr } from './util.js';
3
+ /**
4
+ * 抄送/批注操作。POST remarkOperation(isremark=9, actiontype=remarkOperation)。
5
+ * 先 loadForm 拿签名,再 remarkOperation 抄送。
6
+ *
7
+ * 修复(P0-4):submit_token key 旧硬编码 `1_<rid>_...`(前缀=1=sysadmin),非 sysadmin 取不到 token;
8
+ * 改用 `p.f_weaver_belongto_userid` 动态拼 key,兜底旧 `1_` 前缀。
9
+ */
10
+ export async function ccTodo(cfg, opts) {
11
+ const { requestId } = opts;
12
+ const remarkText = opts.remark ?? '抄送';
13
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
14
+ const api = await createApiFromProfile(cfg);
15
+ // 第一步 loadForm → 拿签名
16
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
17
+ const j0 = r0.json ?? {};
18
+ const p0 = j0.params ?? {};
19
+ const sp0 = j0.submitParams ?? {};
20
+ const wfId = String(p0.workflowid ?? '');
21
+ const nodeId = String(p0.nodeid ?? '');
22
+ let p = p0;
23
+ let sp = sp0;
24
+ if (wfId && nodeId) {
25
+ const r1 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId, workflowid: wfId, nodeid: nodeId });
26
+ const j1 = r1.json ?? {};
27
+ p = j1.params ?? p0;
28
+ sp = j1.submitParams ?? sp0;
29
+ }
30
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
31
+ const submitTokenKey = `${userId}_${requestId}_request_submit_token`;
32
+ const submitToken = String(sp[submitTokenKey] ?? sp[`1_${requestId}_request_submit_token`] ?? '');
33
+ const sigDecoded = Buffer.from(String(p.signatureAttributesStr ?? ''), 'base64').toString('utf8');
34
+ const lastOpDate = (sigDecoded.match(/lastOperateDate=([^|]*)/)?.[1] ?? '').trim();
35
+ const lastOpTime = (sigDecoded.match(/lastOperateTime=([^|]*)/)?.[1] ?? '').trim();
36
+ const todayStrVal = todayStr();
37
+ const timeStr = nowTimeStr();
38
+ const body = {
39
+ formid: String(sp.formid ?? p.formid ?? ''),
40
+ isSubmitDirectNode: '',
41
+ openByDefaultBrowser: '',
42
+ iscreate: '0',
43
+ creatertype: '0',
44
+ isdialog: '1',
45
+ lastOperateDate: lastOpDate,
46
+ createdoc: '',
47
+ [submitTokenKey]: submitToken,
48
+ nodeid: nodeId,
49
+ workflowid: wfId,
50
+ isbill: String(sp.isbill ?? p.isbill ?? '1'),
51
+ authStr: String(p.authStr ?? ''),
52
+ 'f_weaver_belongto_userid': userId,
53
+ currenttime: timeStr,
54
+ 'f_weaver_belongto_usertype': '0',
55
+ agentorByAgentId: '-1',
56
+ isMultiDoc: '',
57
+ inputcheck: '',
58
+ comemessage: '',
59
+ lastOperateTime: lastOpTime,
60
+ temphasUseTempletSucceed: '',
61
+ workflowRequestLogId: '',
62
+ edesign_layout: '',
63
+ requestid: requestId,
64
+ isremark: '9',
65
+ creater: String(p.creater ?? userId),
66
+ htmlfieldids: '',
67
+ SubmitToNodeid: '',
68
+ isCptwf: 'false',
69
+ isovertime: '',
70
+ agentType: '0',
71
+ authSignatureStr: String(p.authSignatureStr ?? ''),
72
+ nodetype: String(p.nodetype ?? sp.nodetype ?? '1'),
73
+ needoutprint: '',
74
+ lastOperator: '1',
75
+ topage: '',
76
+ isFormSignature: '0',
77
+ remindTypes: '',
78
+ fromFlowDoc: '',
79
+ RejectNodes: '',
80
+ linkageUUID: String(sp.linkageUUID ?? ''),
81
+ billid: String(sp.billid ?? ''),
82
+ lastnodeid: '',
83
+ uploadType: '',
84
+ isSignMustInput: '',
85
+ RejectToNodeid: '',
86
+ isWorkflowDoc: 'false',
87
+ src: 'save',
88
+ annexmaxUploadImageSize: '',
89
+ takisremark: '0',
90
+ workflowtype: String(sp.workflowtype ?? p.workflowtype ?? ''),
91
+ remarkLocation: '',
92
+ needcheck: '',
93
+ needcheckLock: 'false',
94
+ selectfieldvalue: '',
95
+ RejectToType: '',
96
+ currentdate: todayStrVal,
97
+ needwfback: '1',
98
+ remark: remarkHtml,
99
+ 'field-annexupload': '',
100
+ signdocids: '',
101
+ signworkflowids: '',
102
+ annexdocids: '',
103
+ annexdocinfos: '',
104
+ handWrittenSign: '',
105
+ isOdocRequest: '0',
106
+ enableIntervenor: '',
107
+ verifyRequiredRange: 'field-9999,',
108
+ linkageUnFinishedKey: '',
109
+ remarkquote: remarkHtml,
110
+ actiontype: 'remarkOperation',
111
+ isFirstSubmit: '0',
112
+ existChangeRange: '',
113
+ requestname: String(p.requestname ?? ''),
114
+ requestlevel: '0',
115
+ chatsType: '-1',
116
+ messageType: '-1',
117
+ 'field-9': '0',
118
+ 'field-10': '4',
119
+ mainFieldUnEmptyCount: '0',
120
+ detailFieldUnEmptyCount: '0',
121
+ signatureAttributesStr: String(p.signatureAttributesStr ?? ''),
122
+ signatureSecretKey: String(p.signatureSecretKey ?? ''),
123
+ selectNextFlow: '0',
124
+ openDataVerify: '0',
125
+ wfTestStr: '',
126
+ };
127
+ return api.post('/api/workflow/reqform/remarkOperation', body);
128
+ }
@@ -0,0 +1,66 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { resolveReceivers } from './wfreceiver.js';
3
+ /**
4
+ * 转发/传阅。链路(与意见征询同构,流程抓包8.har/7.har 实证 forwardflag=1/4 走同一 ForwardDialog):
5
+ * loadForm(取 workflowid/nodeid/操作人 userid) → signInput(forwardflag 预载) →
6
+ * remarkOperate(operate=save, forwardflag, field5=接收人) → {"success":true,...}。
7
+ *
8
+ * 修复(0.5.1):旧实现走 requestOperation(src=submit, forwardOperators=接收人) 且不带 forwardflag,
9
+ * 被服务端当成"批准"——用户实测转发被记录成"批准"(签字意见写"转发"然后点通过)。
10
+ * 前端 bundle 实证 doReviewE9(转发 forwardflag="1") / doChuanyue(传阅 forwardflag="4")
11
+ * 打开同一 ForwardDialog,提交统一走 signInput → remarkOperate:
12
+ * · forward_token_<requestid>_<userid> 由客户端 Date.now() 生成,signInput 与 remarkOperate 须用同一值
13
+ * · signInput:IsSubmitedOpinion=1 & IsBeForwardTodo=1 & field5= 空
14
+ * · remarkOperate:operate=save & field5=接收人 & isSubmitSign/isChuanyue/isRemind
15
+ * · 转发(flag=1) 的 signInput 带 forwardOperators= 空串;传阅(flag=4) 不带 —— 与抓包一致
16
+ * · 传阅(flag=4) 的 isSubmitSign/isChuanyue/isRemind 为空串;转发(flag=1) 为 0 —— 与抓包一致
17
+ */
18
+ async function forwardOrCirculate(cfg, opts, forwardflag) {
19
+ const { requestId } = opts;
20
+ const remarkText = opts.remark ?? '';
21
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
22
+ const api = await createApiFromProfile(cfg);
23
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: String(requestId) });
24
+ const p = (r0.json ?? {}).params ?? {};
25
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
26
+ const userType = String(p.f_weaver_belongto_usertype ?? '0');
27
+ const workflowid = String(p.workflowid ?? '');
28
+ const nodeid = String(p.nodeid ?? '');
29
+ const field5 = await resolveReceivers(cfg, opts);
30
+ if (!field5)
31
+ throw new Error(`${forwardflag === '1' ? '转发' : '传阅'}需要目标人:传 --receiver <userId> 或 --receiver-name <姓名>`);
32
+ const tokenKey = `forward_token_${requestId}_${userId}`;
33
+ const token = String(Date.now());
34
+ const common = {
35
+ isShowReplyNotice: '0', takOverMins: '30', remark: remarkHtml,
36
+ workflowRequestLogId: '', forwardflag, requestid: String(requestId),
37
+ nodeid, workflowid, 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': userType,
38
+ agentType: '0', agentorByAgentId: '-1', secLevelInfo: '[object Object]',
39
+ [tokenKey]: token,
40
+ };
41
+ // ① signInput 预载(forwardflag,field5 空)
42
+ const signBody = forwardflag === '1'
43
+ ? { ...common, field5: '', IsSubmitedOpinion: '1', IsBeForwardTodo: '1', forwardOperators: '' }
44
+ : { ...common, field5: '', IsSubmitedOpinion: '1', IsBeForwardTodo: '1' };
45
+ await api.post('/api/workflow/reqform/signInput', signBody);
46
+ // ② remarkOperate 提交(operate=save,field5=接收人)
47
+ const remFlags = forwardflag === '4' ? '' : '0';
48
+ return api.post('/api/workflow/reqform/remarkOperate', {
49
+ ...common, field5,
50
+ operate: 'save',
51
+ openByDefaultBrowser: '', IsBeForwardSubmitAlready: '', IsBeForwardAlready: '',
52
+ IsSubmitedOpinion: '', IsBeForwardTodo: '',
53
+ isSubmitSign: remFlags, isChuanyue: remFlags, isRemind: remFlags,
54
+ signworkflowids: '', signdocids: '', remarkLocation: '',
55
+ 'field-annexupload': '', 'field-annexupload-request': String(requestId),
56
+ handWrittenSign: '0', remindTypes: '',
57
+ });
58
+ }
59
+ /** 转发(分享给某人看,不改变审批人) */
60
+ export async function forwardTodo(cfg, opts) {
61
+ return forwardOrCirculate(cfg, opts, '1');
62
+ }
63
+ /** 传阅 */
64
+ export async function circulateTodo(cfg, opts) {
65
+ return forwardOrCirculate(cfg, opts, '4');
66
+ }
@@ -0,0 +1,215 @@
1
+ import { createApiFromProfile } from './client.js';
2
+ import { todayStr, nowTimeStr } from './util.js';
3
+ /**
4
+ * 退回/驳回流程。HAR 正确链路:
5
+ * loadForm(2步拿签名) → judgeRejectWay(判断退回方式) → getForwardDatas(取可退回节点) →
6
+ * doBack(selectRow=选定节点行id) → requestOperation(src=reject)
7
+ *
8
+ * 修复(P0-3):旧实现跳过 judgeRejectWay/getForwardDatas/doBack,直接 requestOperation(src=reject),
9
+ * 且 RejectNodes/RejectToNodeid/RejectToType 全空 → 报"流程数据已更改"。
10
+ *
11
+ * ⚠ getForwardDatas 响应结构未抓到(HAR 只抓请求),selectRow/RejectToNodeid 的取值路径属 best-effort,
12
+ * 需实跑校准;如失败可用 --reject-node/--reject-to-node/--reject-to-type 手工指定。
13
+ */
14
+ export async function rejectTodo(cfg, opts) {
15
+ const { requestId } = opts;
16
+ const remarkText = opts.remark ?? '退回';
17
+ const remarkHtml = remarkText.split(/\r?\n/).map((l) => `<p>${l}</p>`).join('');
18
+ const api = await createApiFromProfile(cfg);
19
+ // 第一步 loadForm → 拿签名
20
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
21
+ const j0 = r0.json ?? {};
22
+ const p0 = j0.params ?? {};
23
+ const sp0 = j0.submitParams ?? {};
24
+ const wfId = String(p0.workflowid ?? '');
25
+ const nodeId = String(p0.nodeid ?? '');
26
+ let p = p0;
27
+ let sp = sp0;
28
+ if (wfId && nodeId) {
29
+ const r1 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId, workflowid: wfId, nodeid: nodeId });
30
+ const j1 = r1.json ?? {};
31
+ p = j1.params ?? p0;
32
+ sp = j1.submitParams ?? sp0;
33
+ }
34
+ const userId = String(p.f_weaver_belongto_userid ?? '1');
35
+ // ② judgeRejectWay(HAR:requestid/workflowid/nodeid/f_weaver_belongto_userid/...)
36
+ try {
37
+ await api.post('/api/workflow/reqform/judgeRejectWay', {
38
+ requestid: requestId,
39
+ workflowid: wfId,
40
+ nodeid: nodeId,
41
+ 'f_weaver_belongto_userid': userId,
42
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
43
+ wfTestStr: '',
44
+ });
45
+ }
46
+ catch { /* 判断失败不阻断,继续走退回 */ }
47
+ // ③ getForwardDatas(GET,取可退回节点列表;响应未抓,best-effort 解析 selectRow + nodeid)
48
+ let rejectNodes = [];
49
+ let selectRow = '';
50
+ let rejectToNode = opts.rejectToNode ?? '';
51
+ let rejectToType = opts.rejectToType ?? '';
52
+ let rejectNodesField = opts.rejectNode ?? '';
53
+ try {
54
+ const rFd = await api.get('/api/workflow/reqform/getForwardDatas', {
55
+ requestid: requestId, workflowid: wfId, nodeid: nodeId,
56
+ 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
57
+ });
58
+ const fdText = rFd.text ?? '';
59
+ // best-effort:从响应里提节点行 id(selectRow)与 nodeid/nodename
60
+ rejectNodes = [...fdText.matchAll(/["']?selectRow["']?\s*[:=]\s*["']?(\d+)["']?[^}]*?["']?nodeid["']?\s*[:=]\s*["']?(\d+)["']?(?:[^}]*?["']?nodename["']?\s*[:=]\s*["']?([^"',}]+)["']?)?/gi)]
61
+ .map((m) => ({ selectRow: m[1], nodeid: m[2], nodename: m[3] ?? '' }));
62
+ if (rejectNodes.length === 0) {
63
+ // 兜底:分别提 selectRow 与 nodeid
64
+ const srs = [...fdText.matchAll(/selectRow["']?\s*[:=]\s*["']?(\d+)/gi)].map((m) => m[1]);
65
+ const nids = [...fdText.matchAll(/nodeid["']?\s*[:=]\s*["']?(\d+)/gi)].map((m) => m[1]);
66
+ rejectNodes = srs.map((sr, i) => ({ selectRow: sr, nodeid: nids[i] ?? '', nodename: '' }));
67
+ }
68
+ // 选定目标节点:用户传 --reject-to-node(=nodeid) 优先,否则取第一个
69
+ const picked = rejectToNode
70
+ ? rejectNodes.find((n) => n.nodeid === rejectToNode) ?? rejectNodes[0]
71
+ : rejectNodes[0];
72
+ if (picked) {
73
+ selectRow = picked.selectRow;
74
+ if (!rejectToNode)
75
+ rejectToNode = picked.nodeid;
76
+ }
77
+ }
78
+ catch { /* getForwardDatas 失败不阻断 */ }
79
+ // ④ doBack(selectRow)(HAR:selectRow=<节点行id>)
80
+ if (selectRow) {
81
+ try {
82
+ await api.post('/api/workflow/reqform/doBack', {
83
+ selectRow,
84
+ requestid: requestId,
85
+ workflowid: wfId,
86
+ nodeid: nodeId,
87
+ 'f_weaver_belongto_userid': userId,
88
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
89
+ });
90
+ }
91
+ catch { /* doBack 失败不阻断 */ }
92
+ }
93
+ const sigDecoded = Buffer.from(String(p.signatureAttributesStr ?? ''), 'base64').toString('utf8');
94
+ const lastOpDate = (sigDecoded.match(/lastOperateDate=([^|]*)/)?.[1] ?? '').trim();
95
+ const lastOpTime = (sigDecoded.match(/lastOperateTime=([^|]*)/)?.[1] ?? '').trim();
96
+ const todayStrVal = todayStr();
97
+ const timeStr = nowTimeStr();
98
+ const submitTokenKey = `${userId}_${requestId}_request_submit_token`;
99
+ const submitToken = String(sp[submitTokenKey] ?? Date.now());
100
+ // ⑤ requestOperation(src=reject),带上退回目标(HAR 实测旧实现三字段全空是失败根因)
101
+ const body = {
102
+ formid: String(sp.formid ?? p.formid ?? ''),
103
+ isSubmitDirectNode: '',
104
+ openByDefaultBrowser: '',
105
+ iscreate: '0',
106
+ creatertype: '0',
107
+ isdialog: '1',
108
+ lastOperateDate: lastOpDate,
109
+ createdoc: '',
110
+ nodeid: nodeId,
111
+ workflowid: wfId,
112
+ isbill: String(sp.isbill ?? p.isbill ?? '1'),
113
+ authStr: String(p.authStr ?? ''),
114
+ 'f_weaver_belongto_userid': userId,
115
+ currenttime: timeStr,
116
+ 'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
117
+ agentorByAgentId: '-1',
118
+ isMultiDoc: '',
119
+ inputcheck: '',
120
+ comemessage: '',
121
+ lastOperateTime: lastOpTime,
122
+ temphasUseTempletSucceed: '',
123
+ workflowRequestLogId: '',
124
+ edesign_layout: '',
125
+ [submitTokenKey]: submitToken,
126
+ requestid: requestId,
127
+ isremark: '0',
128
+ creater: String(p.creater ?? userId),
129
+ htmlfieldids: '',
130
+ SubmitToNodeid: '',
131
+ isCptwf: 'false',
132
+ isovertime: '',
133
+ agentType: '0',
134
+ authSignatureStr: String(p.authSignatureStr ?? ''),
135
+ nodetype: String(p.nodetype ?? sp.nodetype ?? '0'),
136
+ needoutprint: '',
137
+ lastOperator: String(sp.lastOperator ?? p.lastOperator ?? userId),
138
+ topage: '',
139
+ isFormSignature: '0',
140
+ remindTypes: '',
141
+ fromFlowDoc: '',
142
+ RejectNodes: rejectNodesField,
143
+ linkageUUID: String(sp.linkageUUID ?? ''),
144
+ billid: String(sp.billid ?? ''),
145
+ lastnodeid: '',
146
+ uploadType: '',
147
+ isSignMustInput: '',
148
+ RejectToNodeid: rejectToNode,
149
+ isWorkflowDoc: 'false',
150
+ src: 'reject',
151
+ annexmaxUploadImageSize: '',
152
+ takisremark: '0',
153
+ workflowtype: String(sp.workflowtype ?? p.workflowtype ?? ''),
154
+ remarkLocation: '',
155
+ needcheck: '',
156
+ needcheckLock: 'false',
157
+ selectfieldvalue: '',
158
+ RejectToType: rejectToType,
159
+ currentdate: todayStrVal,
160
+ needwfback: '0',
161
+ remark: remarkHtml,
162
+ 'field-annexupload': '',
163
+ signdocids: '',
164
+ signworkflowids: '',
165
+ annexdocids: '',
166
+ annexdocinfos: '',
167
+ handWrittenSign: '',
168
+ isOdocRequest: '0',
169
+ enableIntervenor: '',
170
+ verifyRequiredRange: 'field-9999,',
171
+ linkageUnFinishedKey: '',
172
+ remarkquote: remarkHtml,
173
+ actiontype: 'requestOperation',
174
+ isFirstSubmit: '0',
175
+ existChangeRange: '',
176
+ requestname: String(p.requestname ?? ''),
177
+ requestlevel: '0',
178
+ chatsType: '-1',
179
+ messageType: '-1',
180
+ 'field-9': '0',
181
+ 'field-10': '4',
182
+ mainFieldUnEmptyCount: '0',
183
+ detailFieldUnEmptyCount: '0',
184
+ signatureAttributesStr: String(p.signatureAttributesStr ?? ''),
185
+ signatureSecretKey: String(p.signatureSecretKey ?? ''),
186
+ selectNextFlow: '0',
187
+ openDataVerify: '0',
188
+ wfTestStr: '',
189
+ };
190
+ return api.post('/api/workflow/reqform/requestOperation', body);
191
+ }
192
+ /**
193
+ * 获取可退回的节点列表(judgeRejectWay → getForwardDatas)。
194
+ */
195
+ export async function getRejectNodes(cfg, requestId) {
196
+ const api = await createApiFromProfile(cfg);
197
+ const r0 = await api.post('/api/workflow/reqform/loadForm', { iscreate: '0', requestid: requestId });
198
+ const j0 = r0.json ?? {};
199
+ const p0 = j0.params ?? {};
200
+ const wfId = String(p0.workflowid ?? '');
201
+ const nodeId = String(p0.nodeid ?? '');
202
+ const userId = String(p0.f_weaver_belongto_userid ?? '1');
203
+ try {
204
+ await api.post('/api/workflow/reqform/judgeRejectWay', {
205
+ requestid: requestId, workflowid: wfId, nodeid: nodeId,
206
+ 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': String(p0.f_weaver_belongto_usertype ?? '0'),
207
+ wfTestStr: '',
208
+ });
209
+ }
210
+ catch { /* 忽略 */ }
211
+ return api.get('/api/workflow/reqform/getForwardDatas', {
212
+ requestid: requestId, workflowid: wfId, nodeid: nodeId,
213
+ 'f_weaver_belongto_userid': userId, 'f_weaver_belongto_usertype': String(p0.f_weaver_belongto_usertype ?? '0'),
214
+ });
215
+ }