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.
- package/README.md +37 -2
- package/dist/accounts.js +30 -0
- package/dist/args.js +36 -0
- package/dist/auth-token.js +783 -0
- package/dist/auth.js +331 -0
- package/dist/blog.js +562 -0
- package/dist/browser-links.js +224 -0
- package/dist/captcha-display.js +30 -0
- package/dist/cli.js +5533 -0
- package/dist/client.js +254 -0
- package/dist/config.js +170 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1771 -0
- package/dist/documents.js +98 -0
- package/dist/email.js +841 -0
- package/dist/field-preparation.js +237 -0
- package/dist/fileutil.js +20 -0
- package/dist/formula-runtime.js +55 -0
- package/dist/hrm.js +478 -0
- package/dist/keepalive.js +99 -0
- package/dist/kq.js +280 -0
- package/dist/linkage-calculations.js +68 -0
- package/dist/links.js +34 -0
- package/dist/meetings.js +172 -0
- package/dist/nodemap.js +242 -0
- package/dist/odoc.js +199 -0
- package/dist/office-tools.js +155 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +177 -0
- package/dist/password-input.js +84 -0
- package/dist/password-login.js +208 -0
- package/dist/person-selection.js +81 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/session-worker.js +54 -0
- package/dist/session.js +280 -0
- package/dist/submissionplan.js +45 -0
- package/dist/submissionstate.js +81 -0
- package/dist/subordinate.js +458 -0
- package/dist/supervise.js +152 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +172 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +325 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfagent.js +538 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +169 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +558 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +83 -0
- package/dist/wfuser.js +96 -0
- package/dist/workflow-runtime.js +177 -0
- package/dist/workflow.js +570 -0
- package/dist/workplan.js +431 -0
- package/package.json +29 -6
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { createApiFromProfile } from './client.js';
|
|
2
|
+
import { extractDataKey, extractRows, extractTotal, extractColumns, normalizeRow, pickByPaths, stripHtml } from './parse.js';
|
|
3
|
+
/**
|
|
4
|
+
* 复刻 BDIP5 流程中心取数链路。支持 todo(待办)/done(已办)/mine(我发起) 三大分类。
|
|
5
|
+
*
|
|
6
|
+
* 1) getWfListParams(viewcondition) -> 拿 dataKey(todo 有;done/mine 返空,靠 splitPageKey)
|
|
7
|
+
* 2) splitPageKey(viewcondition + 过滤项 + viewScope) -> 刷新 dataKey(三大分类都靠这步拿 dataKey)
|
|
8
|
+
* 3) ec/dev/table/datas(dataKey, current=页码) -> 数据行
|
|
9
|
+
* 4) ec/dev/table/counts(dataKey) -> 总数
|
|
10
|
+
*
|
|
11
|
+
* 实跑校准(2026-08-25):
|
|
12
|
+
* - todo: viewScope=doing, complete=0, menuIds=1,13
|
|
13
|
+
* - done: viewScope=done, complete=2, menuIds=1,90(getWfListParams 返空,splitPageKey 拿 dataKey)
|
|
14
|
+
* - mine: viewScope=mine, complete=2, menuIds=1,14
|
|
15
|
+
* viewScope/complete/menuIds 来自 HAR 抓包。
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_FILTERS = {
|
|
18
|
+
defaultTabVal: '0',
|
|
19
|
+
requestname: '',
|
|
20
|
+
wfcode: '',
|
|
21
|
+
workflowid: '',
|
|
22
|
+
createdateselect: '0',
|
|
23
|
+
createdatefrom: '',
|
|
24
|
+
createdateto: '',
|
|
25
|
+
creatertype: '0',
|
|
26
|
+
workcode: '',
|
|
27
|
+
doingStatus: '0',
|
|
28
|
+
ownerdepartmentid: '',
|
|
29
|
+
creatersubcompanyid: '',
|
|
30
|
+
workflowtype: '',
|
|
31
|
+
requestlevel: '',
|
|
32
|
+
recievedateselect: '0',
|
|
33
|
+
recievedatefrom: '',
|
|
34
|
+
recievedateto: '',
|
|
35
|
+
wfstatu: '1',
|
|
36
|
+
nodetype: '',
|
|
37
|
+
unophrmid: '',
|
|
38
|
+
docids: '',
|
|
39
|
+
createrid: '',
|
|
40
|
+
operatedateselect: '0',
|
|
41
|
+
operatedatefrom: '',
|
|
42
|
+
operatedateto: '',
|
|
43
|
+
archivestatus: '0',
|
|
44
|
+
secLevel: '',
|
|
45
|
+
crmids: '',
|
|
46
|
+
proids: '',
|
|
47
|
+
};
|
|
48
|
+
/** 各分类的 viewScope/complete/menuIds/默认viewcondition(来自 HAR 抓包实跑确认)。
|
|
49
|
+
* menuIds 可由 cfg 覆盖:cfg.doneMenuIds/mineMenuIds;viewcondition 可由 opts 覆盖(子 tab)。 */
|
|
50
|
+
export const CATEGORY_SCOPE = {
|
|
51
|
+
todo: { viewScope: 'doing', complete: '0', menuIdsKey: 'menuIds', defaultViewcondition: '5', fullTab: '0', pageTitle: '待办事宜', countEndpoint: 'doingCountInfo' },
|
|
52
|
+
done: { viewScope: 'done', complete: '2', menuIdsKey: 'doneMenuIds', defaultMenuIds: '1,90', defaultViewcondition: '10', fullTab: '10', pageTitle: '已办事宜', countEndpoint: 'doneCountInfo' },
|
|
53
|
+
mine: { viewScope: 'mine', complete: '2', menuIdsKey: 'mineMenuIds', defaultMenuIds: '1,14', defaultViewcondition: '16', fullTab: '16', pageTitle: '我的请求', countEndpoint: 'mineCountInfo' },
|
|
54
|
+
};
|
|
55
|
+
/** 排序字段映射表 */
|
|
56
|
+
const SORT_FIELD_MAP = {
|
|
57
|
+
"发起时间": "t1.createdate,t1.createtime",
|
|
58
|
+
"接收时间": "receivedate,receivetime",
|
|
59
|
+
"流程标题": "t1.requestname",
|
|
60
|
+
"流程编号": "t1.requestmark",
|
|
61
|
+
"紧急程度": "t1.requestlevel",
|
|
62
|
+
"当前节点": "t1.currentnodename",
|
|
63
|
+
"操作时间": "t1.operatedate,t1.operatetime",
|
|
64
|
+
"结束时间": "t1.lastoperatedate,t1.lastoperatetime",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function parseSortSpec(spec) {
|
|
68
|
+
if (!spec)
|
|
69
|
+
return '[]';
|
|
70
|
+
const items = spec.split(",").map(s => s.trim()).filter(Boolean);
|
|
71
|
+
const arr = items.map(item => {
|
|
72
|
+
const [field, order] = item.split(":").map(s => s.trim());
|
|
73
|
+
const orderkey = SORT_FIELD_MAP[field] || field;
|
|
74
|
+
const sortOrder = order === "asc" ? "ascend" : "descend";
|
|
75
|
+
return { orderkey, sortOrder };
|
|
76
|
+
});
|
|
77
|
+
return JSON.stringify(arr);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 时间范围解析 */
|
|
81
|
+
export function parseTimeRange(range) {
|
|
82
|
+
if (!range)
|
|
83
|
+
return { from: '', to: '' };
|
|
84
|
+
const parts = range.trim().split(/\s+/);
|
|
85
|
+
if (parts.length === 1)
|
|
86
|
+
return { from: parts[0], to: parts[0] };
|
|
87
|
+
return { from: parts[0] || "", to: parts[1] || "" };
|
|
88
|
+
}
|
|
89
|
+
/** 日期过滤的预设值(OA reqlist 的 createdateselect/recievedateselect/operatedateselect 共用):
|
|
90
|
+
* 0=全部 1=今天 2=本周 3=本月 4=本季 5=本年;自定义区间用 select=0 + from/to。 */
|
|
91
|
+
const DATE_PRESETS = { '全部': '0', '不限': '0', '今天': '1', '本周': '2', '本月': '3', '本季': '4', '本年': '5' };
|
|
92
|
+
/**
|
|
93
|
+
* 解析日期过滤,返回 { select, from, to }:
|
|
94
|
+
* - 预设名(今天/本周/本月/本季/本年/全部/不限)→ select=1~5 或 0,from/to 空;
|
|
95
|
+
* - 数字 0~5 → 直接当 select;
|
|
96
|
+
* - 日期范围("YYYY-MM-DD [YYYY-MM-DD]")→ select='0' + from/to(自定义区间)。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveDateFilter(spec) {
|
|
99
|
+
const s = String(spec ?? '').trim();
|
|
100
|
+
if (!s)
|
|
101
|
+
return { select: '0', from: '', to: '' };
|
|
102
|
+
if (DATE_PRESETS[s] !== undefined)
|
|
103
|
+
return { select: DATE_PRESETS[s], from: '', to: '' };
|
|
104
|
+
if (/^[0-5]$/.test(s))
|
|
105
|
+
return { select: s, from: '', to: '' };
|
|
106
|
+
const parts = s.split(/\s+/);
|
|
107
|
+
return { select: '0', from: parts[0] || '', to: parts[1] || parts[0] || '' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 子分类 viewcondition 映射表 */
|
|
111
|
+
export const TODO_TABS = {
|
|
112
|
+
"全部": "0", "待处理": "5", "待阅": "6", "被退回": "7",
|
|
113
|
+
"被督办": "4", "未读": "1", "反馈": "2", "超时": "3", "我的关注": "37",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** 已办页签(实测 .18 doneBaseInfo.groupinfo;旧表把 17/18 误记成「审批中/已结束」)。 */
|
|
117
|
+
export const DONE_TABS = {
|
|
118
|
+
"全部": "10", "未归档": "11", "已归档": "12", "待回复": "13",
|
|
119
|
+
"未读": "14", "反馈": "15", "我的关注": "38",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** 我发起页签(实测 .18 mineBaseInfo.groupinfo)。 */
|
|
123
|
+
export const MINE_TABS = {
|
|
124
|
+
"全部": "16", "未归档": "17", "已归档": "18",
|
|
125
|
+
"未读": "19", "反馈": "20", "我的关注": "39",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function resolveSubCategory(spec, category) {
|
|
129
|
+
if (!spec)
|
|
130
|
+
return undefined;
|
|
131
|
+
// 直接是数字,直接返回
|
|
132
|
+
if (/^\d+$/.test(spec))
|
|
133
|
+
return spec;
|
|
134
|
+
const map = category === 'todo' ? TODO_TABS : category === 'done' ? DONE_TABS : MINE_TABS;
|
|
135
|
+
const key = Object.keys(map).find(k => k === spec || k.includes(spec));
|
|
136
|
+
return key ? map[key] : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getWfList(cfg, opts) {
|
|
140
|
+
// 多流程筛选不依赖后端是否支持逗号分隔:分别查询后合并,避免漏结果。
|
|
141
|
+
const workflowIds = String(opts.workflow ?? '').split(',').map((id) => id.trim()).filter(Boolean);
|
|
142
|
+
if (!opts._singleWorkflow && workflowIds.length > 1) {
|
|
143
|
+
const batches = await Promise.all(workflowIds.map((workflow) => getWfList(cfg, { ...opts, workflow, _singleWorkflow: true })));
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const items = batches.flatMap((r) => r.items).filter((item) => {
|
|
146
|
+
const id = String(item.requestId ?? '');
|
|
147
|
+
if (!id || seen.has(id))
|
|
148
|
+
return !id;
|
|
149
|
+
seen.add(id);
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
category: opts.category ?? 'todo',
|
|
154
|
+
dataKey: '',
|
|
155
|
+
items,
|
|
156
|
+
columns: batches.find((r) => r.columns.length)?.columns ?? [],
|
|
157
|
+
total: batches.reduce((sum, r) => sum + (Number(r.total) || 0), 0),
|
|
158
|
+
page: opts.page ?? 1,
|
|
159
|
+
pageSize: batches.find((r) => r.pageSize)?.pageSize ?? null,
|
|
160
|
+
multiWorkflow: workflowIds,
|
|
161
|
+
raw: { batches: batches.map((r) => r.raw) },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const { page = 1, keyword = '', debug = false, category = 'todo' } = opts;
|
|
165
|
+
const viewcondition = resolveSubCategory(opts.subCategory, category) ?? opts.viewcondition ?? CATEGORY_SCOPE[category]?.defaultViewcondition ?? '5';
|
|
166
|
+
const api = await createApiFromProfile(cfg);
|
|
167
|
+
const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
|
|
168
|
+
const menuIds = String(cfg[scope.menuIdsKey] ?? scope.defaultMenuIds ?? cfg.menuIds ?? '1,13');
|
|
169
|
+
const menuPathIds = String(cfg[scope.menuIdsKey] ?? cfg.menuPathIds ?? menuIds);
|
|
170
|
+
// 1) getWfListParams(todo 返 dataKey;done/mine 返空,靠 splitPageKey)
|
|
171
|
+
const r1 = await api.post('/api/workflow/reqlist/getWfListParams', buildScopeBody(viewcondition, scope, menuIds, menuPathIds));
|
|
172
|
+
if (debug)
|
|
173
|
+
logStep('getWfListParams', r1);
|
|
174
|
+
// 2) splitPageKey(带过滤项;keyword 进 requestname;done/mine 需带 date2during=0)—— 三大分类都靠这步拿 dataKey
|
|
175
|
+
const filters = buildFilters(opts);
|
|
176
|
+
if (category !== 'todo')
|
|
177
|
+
filters.date2during = '0';
|
|
178
|
+
const r2 = await api.post('/api/workflow/reqlist/splitPageKey', buildSplitBody(viewcondition, filters, scope, menuIds, menuPathIds));
|
|
179
|
+
if (debug)
|
|
180
|
+
logStep('splitPageKey', r2);
|
|
181
|
+
// dataKey 优先取 splitPageKey 的响应,取不到再回退 getWfListParams
|
|
182
|
+
let dataKey = extractDataKey(r2.json) ?? extractDataKey(r1.json);
|
|
183
|
+
if (!dataKey) {
|
|
184
|
+
throw new Error(`未能从响应中提取 dataKey(category=${category})。\n` +
|
|
185
|
+
'用 --raw 查看真实响应,或确认 cookie 是否已过期。\n' +
|
|
186
|
+
`getWfListParams 响应: ${truncate(r1.text)}\n` +
|
|
187
|
+
`splitPageKey 响应: ${truncate(r2.text)}`);
|
|
188
|
+
}
|
|
189
|
+
// total-only 用于语音/聊天中的快速确认:跳过大结果集的数据请求,只取数量。
|
|
190
|
+
let r3 = null;
|
|
191
|
+
if (!opts.totalOnly || opts.excludeCreatorId) {
|
|
192
|
+
const sortParams = parseSortSpec(opts.sort);
|
|
193
|
+
r3 = await api.post('/api/ec/dev/table/datas', {
|
|
194
|
+
dataKey,
|
|
195
|
+
current: String(page),
|
|
196
|
+
sortParams,
|
|
197
|
+
// 显式每页 10 条,与 printTodoItems 的“展示前 10 条”一致,避免 BDIP5 默认 30 条/页导致
|
|
198
|
+
// --page N 按 30 条跳页、本页第 11~30 条永远看不到、翻页时条数跳变成 30 的错位。
|
|
199
|
+
pageSize: opts.pageSize ?? '10',
|
|
200
|
+
});
|
|
201
|
+
if (debug)
|
|
202
|
+
logStep('ec/dev/table/datas', r3);
|
|
203
|
+
}
|
|
204
|
+
// 4) ec/dev/table/counts(total-only 的唯一数据读取)
|
|
205
|
+
const r4 = await api.post('/api/ec/dev/table/counts', { dataKey });
|
|
206
|
+
if (debug)
|
|
207
|
+
logStep('ec/dev/table/counts', r4);
|
|
208
|
+
let rows = r3 ? extractRows(r3.json) : [];
|
|
209
|
+
const columns = r3 ? extractColumns(r3.json) : [];
|
|
210
|
+
const total = extractTotal(r4.json) ?? (r3 ? extractTotal(r3.json) : null);
|
|
211
|
+
let pageSize = r3 ? pickNum(r3.json, ['pageSize', 'data.pageSize']) : null;
|
|
212
|
+
let fetchedPages = r3 ? 1 : 0;
|
|
213
|
+
// BDIP5 没有“不等于当前创建人”接口。需要排除时必须先拉完整结果集,不能只过滤当前页。
|
|
214
|
+
if (r3 && opts.excludeCreatorId) {
|
|
215
|
+
const firstPageRows = page === 1 ? rows : extractRows((await api.post('/api/ec/dev/table/datas', {
|
|
216
|
+
dataKey, current: '1', sortParams: parseSortSpec(opts.sort), pageSize: '100',
|
|
217
|
+
})).json);
|
|
218
|
+
rows = firstPageRows;
|
|
219
|
+
// 负向筛选固定 100/页全量拉取(下方循环 pageSize:'100'),页数基准不随列表分页 pageSize=10 变化,
|
|
220
|
+
// 否则 expectedPages 会被放大 10 倍、误触发 500 页安全上限。
|
|
221
|
+
const effectivePageSize = 100;
|
|
222
|
+
const expectedPages = Math.ceil((Number(total) || 0) / effectivePageSize);
|
|
223
|
+
if (expectedPages > 500)
|
|
224
|
+
throw new Error(`负向筛选需读取 ${expectedPages} 页,超过 500 页安全上限;请先增加其它筛选条件。`);
|
|
225
|
+
for (let current = 2; current <= expectedPages; current++) {
|
|
226
|
+
const next = await api.post('/api/ec/dev/table/datas', {
|
|
227
|
+
dataKey, current: String(current), sortParams: parseSortSpec(opts.sort), pageSize: '100',
|
|
228
|
+
});
|
|
229
|
+
const nextRows = extractRows(next.json);
|
|
230
|
+
rows.push(...nextRows);
|
|
231
|
+
fetchedPages++;
|
|
232
|
+
if (!nextRows.length)
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (Number(total) > rows.length)
|
|
236
|
+
throw new Error(`负向筛选未能读取完整列表:服务端共 ${total} 条,实际只取到 ${rows.length} 条。`);
|
|
237
|
+
pageSize = effectivePageSize;
|
|
238
|
+
}
|
|
239
|
+
let items = rows.map((r) => normalizeRow(r, columns));
|
|
240
|
+
const sourceTotal = total;
|
|
241
|
+
if (opts.excludeCreatorId)
|
|
242
|
+
items = items.filter((item) => String(item.creatorId ?? '') !== String(opts.excludeCreatorId));
|
|
243
|
+
const filteredTotal = opts.excludeCreatorId ? items.length : total;
|
|
244
|
+
if (opts.totalOnly)
|
|
245
|
+
items = [];
|
|
246
|
+
// 待办才取当前处理人(已办/我发起无未操作者概念)
|
|
247
|
+
if (category === 'todo' && !opts.totalOnly) {
|
|
248
|
+
try {
|
|
249
|
+
const datasPayload = JSON.stringify(rows.map((r) => ({
|
|
250
|
+
requestid: r?.requestid ?? r?.randomField2 ?? '',
|
|
251
|
+
userid: r?.userid ?? r?.randomField4 ?? '',
|
|
252
|
+
unoperators: r?.unoperators ?? '',
|
|
253
|
+
})).filter((d) => d.requestid));
|
|
254
|
+
const r5 = await api.post('/api/workflow/reqlist/getUnoperators', {
|
|
255
|
+
datas: datasPayload, actiontype: 'batch', showAll: 'false', dataKey,
|
|
256
|
+
});
|
|
257
|
+
const opsMap = r5.json ?? {};
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
const uid = String(row?.userid ?? row?.randomField4 ?? '');
|
|
260
|
+
const rid = String(row?.requestid ?? row?.randomField2 ?? '');
|
|
261
|
+
const key = `${uid}${rid}loding`;
|
|
262
|
+
const item = items.find((candidate) => String(candidate.requestId ?? '') === rid);
|
|
263
|
+
if (item && opsMap[key])
|
|
264
|
+
item.currentOperator = stripHtml(opsMap[key]).replace(/\s+/g, ' ').trim() || undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch { /* 取处理人失败不影响主列表 */ }
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
category,
|
|
271
|
+
dataKey,
|
|
272
|
+
items,
|
|
273
|
+
columns,
|
|
274
|
+
total: filteredTotal,
|
|
275
|
+
sourceTotal,
|
|
276
|
+
page,
|
|
277
|
+
pageSize,
|
|
278
|
+
fetchedPages,
|
|
279
|
+
excludedCreatorId: opts.excludeCreatorId || undefined,
|
|
280
|
+
raw: { getWfListParams: r1.json, splitPageKey: r2.json, datas: r3?.json ?? null, counts: r4.json },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 面向用户的待办概览:把需要本人处理、转发/抄送和关注拆开显示。
|
|
286
|
+
* 每个分类独立分页;同一 requestId 只保留在优先级更高的分类,避免重复打扰用户。
|
|
287
|
+
*/
|
|
288
|
+
export async function getSmartTodoGroups(cfg, opts = {}) {
|
|
289
|
+
const groupDefs = [
|
|
290
|
+
{ key: 'action', title: '需您亲自处理', subCategory: '待处理' },
|
|
291
|
+
{ key: 'forwarded', title: '转发和抄送给您的流程', subCategory: '转发' },
|
|
292
|
+
{ key: 'following', title: '您关注的流程', subCategory: '关注' },
|
|
293
|
+
];
|
|
294
|
+
const results = await Promise.all(groupDefs.map(async (group) => ({
|
|
295
|
+
...group,
|
|
296
|
+
result: await getWfList(cfg, { ...opts, category: 'todo', subCategory: group.subCategory, _singleWorkflow: false }),
|
|
297
|
+
})));
|
|
298
|
+
return mergeSmartTodoGroups(results, opts.page ?? 1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 纯函数,便于测试分组去重规则。 */
|
|
302
|
+
export function mergeSmartTodoGroups(results, page = 1) {
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
const groups = results.map((group) => ({
|
|
305
|
+
...group,
|
|
306
|
+
items: group.result.items.filter((item) => {
|
|
307
|
+
const id = String(item.requestId ?? '');
|
|
308
|
+
if (!id || seen.has(id))
|
|
309
|
+
return !id;
|
|
310
|
+
seen.add(id);
|
|
311
|
+
return true;
|
|
312
|
+
}),
|
|
313
|
+
}));
|
|
314
|
+
return {
|
|
315
|
+
groups,
|
|
316
|
+
actionTotal: Number(groups.find((group) => group.key === 'action')?.result?.total) || 0,
|
|
317
|
+
categoryTotals: Object.fromEntries(groups.map((group) => [group.key, Number(group.result.total) || 0])),
|
|
318
|
+
// 仅用于诊断,名称明确说明是分类相加,绝不能称为“待处理总数”。
|
|
319
|
+
categoryTotalSum: groups.reduce((sum, group) => sum + (Number(group.result.total) || 0), 0),
|
|
320
|
+
page,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ─── 按 OA 二级分类页签分组(待办/已办/我发起通用)────────────
|
|
325
|
+
|
|
326
|
+
/** 内置页签表(CountInfo 不返回 topTab 时回退;实测仅待办返回 topTab,已办/我发起均不返回)。 */
|
|
327
|
+
function builtinTabs(category) {
|
|
328
|
+
const map = category === 'done' ? DONE_TABS : category === 'mine' ? MINE_TABS : TODO_TABS;
|
|
329
|
+
return Object.entries(map).map(([title, viewcondition]) => ({ title, viewcondition: String(viewcondition), groupid: '' }));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 取 OA 二级分类页签清单(三级数据源,逐级回退):
|
|
334
|
+
* ① {scope}CountInfo.topTab——**只有待办返回**(实测 .18);附带各页签计数 totalcount[groupid]。
|
|
335
|
+
* ② {scope}BaseInfo.groupinfo——**已办/我发起的唯一来源**(实测 .18:done 7 个页签、mine 6 个);无计数。
|
|
336
|
+
* ③ 内置页签名表(网络异常时的兜底);无计数。
|
|
337
|
+
* 已剔除「全部」页签——它是其它所有页签的并集,单列会吞掉全部分类。
|
|
338
|
+
*/
|
|
339
|
+
export async function getOaTabs(cfg, category) {
|
|
340
|
+
const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
|
|
341
|
+
const menuIds = String(cfg[scope.menuIdsKey] ?? scope.defaultMenuIds ?? cfg.menuIds ?? '1,13');
|
|
342
|
+
const menuPathIds = String(cfg[scope.menuIdsKey] ?? cfg.menuPathIds ?? menuIds);
|
|
343
|
+
const mapTab = (t) => ({ title: String(t?.title ?? ''), viewcondition: String(t?.viewcondition ?? ''), groupid: String(t?.groupid ?? '') });
|
|
344
|
+
let tabs = [];
|
|
345
|
+
let counts = {};
|
|
346
|
+
let source = 'builtin';
|
|
347
|
+
const api = await createApiFromProfile(cfg);
|
|
348
|
+
// ① CountInfo.topTab(待办有;已办/我发起实测不返回)
|
|
349
|
+
try {
|
|
350
|
+
const r = await api.post(`/api/workflow/reqlist/${scope.countEndpoint}`, buildScopeBody(scope.defaultViewcondition, scope, menuIds, menuPathIds));
|
|
351
|
+
const j = r.json ?? {};
|
|
352
|
+
counts = j.totalcount ?? {};
|
|
353
|
+
tabs = (j.topTab ?? []).map(mapTab).filter((t) => t.title && t.viewcondition !== '');
|
|
354
|
+
if (tabs.length)
|
|
355
|
+
source = 'countinfo';
|
|
356
|
+
}
|
|
357
|
+
catch { /* 落到下一级 */ }
|
|
358
|
+
// ② BaseInfo.groupinfo(已办/我发起的权威来源)
|
|
359
|
+
if (!tabs.length) {
|
|
360
|
+
try {
|
|
361
|
+
const r = await api.post(`/api/workflow/reqlist/${scope.viewScope}BaseInfo`, buildScopeBody(scope.defaultViewcondition, scope, menuIds, menuPathIds));
|
|
362
|
+
tabs = (r.json?.groupinfo ?? []).map(mapTab).filter((t) => t.title && t.viewcondition !== '');
|
|
363
|
+
if (tabs.length)
|
|
364
|
+
source = 'baseinfo';
|
|
365
|
+
}
|
|
366
|
+
catch { /* 落到下一级 */ }
|
|
367
|
+
}
|
|
368
|
+
// ③ 内置表兜底
|
|
369
|
+
if (!tabs.length)
|
|
370
|
+
tabs = builtinTabs(category);
|
|
371
|
+
return { tabs: tabs.filter((t) => t.viewcondition !== String(scope.fullTab)), counts, scope, source };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 按 OA 页签分组取数:每个页签独立查询、并发执行,同一 requestId 只保留在靠前的页签里(避免重复推荐)。
|
|
376
|
+
* 只列有数据的分类:计数已知且为 0 的页签直接跳过(不查询);查完 total=0 的也不展示。
|
|
377
|
+
*/
|
|
378
|
+
export async function getOaTabGroups(cfg, opts = {}) {
|
|
379
|
+
const category = opts.category ?? 'todo';
|
|
380
|
+
const { tabs, counts } = await getOaTabs(cfg, category);
|
|
381
|
+
const results = await Promise.all(tabs.map(async (tab) => {
|
|
382
|
+
const known = tab.groupid && counts[tab.groupid] !== undefined ? Number(counts[tab.groupid]) : NaN;
|
|
383
|
+
if (Number.isFinite(known) && known === 0)
|
|
384
|
+
return null; // 计数已知为 0 → 不查询
|
|
385
|
+
const result = await getWfList(cfg, { ...opts, category, subCategory: tab.viewcondition, _singleWorkflow: false });
|
|
386
|
+
return { tab, result };
|
|
387
|
+
}));
|
|
388
|
+
const seen = new Set();
|
|
389
|
+
const groups = [];
|
|
390
|
+
for (const entry of results) {
|
|
391
|
+
if (!entry)
|
|
392
|
+
continue;
|
|
393
|
+
const { tab, result } = entry;
|
|
394
|
+
if (!result.total)
|
|
395
|
+
continue; // 无数据不展示
|
|
396
|
+
const items = result.items.filter((item) => {
|
|
397
|
+
const id = String(item.requestId ?? '');
|
|
398
|
+
if (!id || seen.has(id))
|
|
399
|
+
return !id;
|
|
400
|
+
seen.add(id);
|
|
401
|
+
return true;
|
|
402
|
+
});
|
|
403
|
+
groups.push({ key: tab.viewcondition, title: tab.title, viewcondition: tab.viewcondition, total: result.total, items, result: { ...result, items } });
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
category,
|
|
407
|
+
page: opts.page ?? 1,
|
|
408
|
+
groups,
|
|
409
|
+
categoryTotals: Object.fromEntries(groups.map((g) => [g.viewcondition, Number(g.total) || 0])),
|
|
410
|
+
// 仅用于诊断:各分类相加(页签间有交集,绝不能称为“总数”)
|
|
411
|
+
categoryTotalSum: groups.reduce((sum, g) => sum + (Number(g.total) || 0), 0),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** 解析 doingBaseInfo.treedata 中的流程类型节点。 */
|
|
416
|
+
export function parseWorkflowTypes(tree) {
|
|
417
|
+
const out = [];
|
|
418
|
+
const visit = (node, path = []) => {
|
|
419
|
+
if (Array.isArray(node)) {
|
|
420
|
+
node.forEach((item) => visit(item, path));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!node || typeof node !== 'object')
|
|
424
|
+
return;
|
|
425
|
+
const name = stripHtml(node.name ?? node.title ?? node.label ?? '').trim();
|
|
426
|
+
const domid = String(node.domid ?? node.domId ?? '');
|
|
427
|
+
const id = domid.startsWith('type_') ? domid.slice(5) : (node.type === 'type' ? String(node.key ?? node.id ?? '') : '');
|
|
428
|
+
const nextPath = name ? [...path, name] : path;
|
|
429
|
+
if (id)
|
|
430
|
+
out.push({ id, name, path: nextPath.join(' / ') });
|
|
431
|
+
for (const key of ['childs', 'children', 'nodes', 'data'])
|
|
432
|
+
if (node[key]) visit(node[key], nextPath);
|
|
433
|
+
};
|
|
434
|
+
visit(tree);
|
|
435
|
+
return [...new Map(out.filter((item) => item.name).map((item) => [item.id, item])).values()];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function matchWorkflowTypes(types, spec) {
|
|
439
|
+
const q = String(spec ?? '').trim().toLowerCase();
|
|
440
|
+
if (!q)
|
|
441
|
+
return [];
|
|
442
|
+
const ranked = types.map((item) => {
|
|
443
|
+
const name = String(item.name ?? '').trim().toLowerCase();
|
|
444
|
+
const score = name === q ? 3 : name.startsWith(q) ? 2 : name.includes(q) ? 1 : 0;
|
|
445
|
+
return { ...item, score };
|
|
446
|
+
}).filter((item) => item.score > 0);
|
|
447
|
+
return ranked.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name, 'zh-CN'));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export async function getWorkflowTypes(cfg, opts = {}) {
|
|
451
|
+
const category = opts.category ?? 'todo';
|
|
452
|
+
const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
|
|
453
|
+
const viewcondition = opts.viewcondition ?? scope.defaultViewcondition;
|
|
454
|
+
const menuIds = String(cfg[scope.menuIdsKey] ?? scope.defaultMenuIds ?? cfg.menuIds ?? '1,13');
|
|
455
|
+
const menuPathIds = String(cfg[scope.menuIdsKey] ?? cfg.menuPathIds ?? menuIds);
|
|
456
|
+
const api = await createApiFromProfile(cfg);
|
|
457
|
+
const response = await api.post('/api/workflow/reqlist/doingBaseInfo', {
|
|
458
|
+
...buildScopeBody(viewcondition, scope, menuIds, menuPathIds), actiontype: 'baseinfo',
|
|
459
|
+
});
|
|
460
|
+
return { items: parseWorkflowTypes(response.json?.treedata ?? response.json?.data?.treedata), raw: response.json };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export async function resolveWorkflowType(cfg, spec, opts = {}) {
|
|
464
|
+
if (/^\d+$/.test(String(spec ?? '').trim()))
|
|
465
|
+
return { id: String(spec).trim(), name: String(spec).trim(), candidates: [] };
|
|
466
|
+
const result = await getWorkflowTypes(cfg, opts);
|
|
467
|
+
const candidates = matchWorkflowTypes(result.items, spec);
|
|
468
|
+
if (candidates.length === 1 || (candidates[0]?.score === 3 && candidates[1]?.score !== 3))
|
|
469
|
+
return { ...candidates[0], candidates };
|
|
470
|
+
if (!candidates.length)
|
|
471
|
+
throw new Error(`找不到流程类型“${spec}”。`);
|
|
472
|
+
const error = new Error(`流程类型“${spec}”匹配到多项,请指定类型 ID:\n${candidates.map((item) => `- ${item.name} [${item.id}]`).join('\n')}`);
|
|
473
|
+
error.candidates = candidates;
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 从 opts 构造 DEFAULT_FILTERS 覆盖 */
|
|
478
|
+
function buildFilters(opts) {
|
|
479
|
+
const f = { ...DEFAULT_FILTERS, requestname: opts.keyword ?? '' };
|
|
480
|
+
if (opts.workflow)
|
|
481
|
+
f.workflowid = opts.workflow;
|
|
482
|
+
if (opts.creator)
|
|
483
|
+
f.createrid = opts.creator;
|
|
484
|
+
if (opts.creatorDept)
|
|
485
|
+
f.ownerdepartmentid = opts.creatorDept;
|
|
486
|
+
if (opts.creatorSubcom)
|
|
487
|
+
f.creatersubcompanyid = opts.creatorSubcom;
|
|
488
|
+
if (opts.createTime) {
|
|
489
|
+
const ct = resolveDateFilter(opts.createTime);
|
|
490
|
+
f.createdateselect = ct.select;
|
|
491
|
+
f.createdatefrom = ct.from;
|
|
492
|
+
f.createdateto = ct.to;
|
|
493
|
+
}
|
|
494
|
+
if (opts.receiveTime) {
|
|
495
|
+
const rt = resolveDateFilter(opts.receiveTime);
|
|
496
|
+
f.recievedateselect = rt.select;
|
|
497
|
+
f.recievedatefrom = rt.from;
|
|
498
|
+
f.recievedateto = rt.to;
|
|
499
|
+
}
|
|
500
|
+
if (opts.operateTime) {
|
|
501
|
+
const ot = resolveDateFilter(opts.operateTime);
|
|
502
|
+
f.operatedateselect = ot.select;
|
|
503
|
+
f.operatedatefrom = ot.from;
|
|
504
|
+
f.operatedateto = ot.to;
|
|
505
|
+
}
|
|
506
|
+
if (opts.requestLevel)
|
|
507
|
+
f.requestlevel = opts.requestLevel;
|
|
508
|
+
if (opts.requestMark)
|
|
509
|
+
f.wfcode = opts.requestMark;
|
|
510
|
+
if (opts.workflowType)
|
|
511
|
+
f.workflowtype = opts.workflowType;
|
|
512
|
+
if (opts.nodeId)
|
|
513
|
+
f.nodetype = opts.nodeId;
|
|
514
|
+
if (opts.flowStatus)
|
|
515
|
+
f.wfstatu = opts.flowStatus;
|
|
516
|
+
return f;
|
|
517
|
+
}
|
|
518
|
+
function pickNum(obj, paths) {
|
|
519
|
+
const v = pickByPaths(obj, paths);
|
|
520
|
+
if (typeof v === 'number')
|
|
521
|
+
return v;
|
|
522
|
+
if (typeof v === 'string' && /^\d+$/.test(v))
|
|
523
|
+
return parseInt(v, 10);
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
/** getWfListParams 的提交体(按分类的 viewScope/complete)。 */
|
|
527
|
+
function buildScopeBody(viewcondition, scope, menuIds, menuPathIds) {
|
|
528
|
+
return {
|
|
529
|
+
method: 'all',
|
|
530
|
+
offical: '',
|
|
531
|
+
officalType: '-1',
|
|
532
|
+
hideNoDataTab: 'false',
|
|
533
|
+
viewScope: scope.viewScope,
|
|
534
|
+
complete: scope.complete,
|
|
535
|
+
viewcondition,
|
|
536
|
+
menuIds,
|
|
537
|
+
menuPathIds,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* splitPageKey 的提交体(带过滤项 + 分类 viewScope)。直接构造 Record,值保持原样——
|
|
542
|
+
* 由 api.post 的 URLSearchParams 统一单次编码,避免二次编码 bug。
|
|
543
|
+
*/
|
|
544
|
+
export function buildSplitBody(viewcondition, filters, scope, menuIds, menuPathIds) {
|
|
545
|
+
return {
|
|
546
|
+
method: 'all',
|
|
547
|
+
offical: '',
|
|
548
|
+
officalType: '-1',
|
|
549
|
+
hideNoDataTab: 'false',
|
|
550
|
+
viewScope: scope.viewScope,
|
|
551
|
+
complete: scope.complete,
|
|
552
|
+
viewcondition,
|
|
553
|
+
...filters,
|
|
554
|
+
menuIds,
|
|
555
|
+
menuPathIds,
|
|
556
|
+
actiontype: 'splitpage',
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/** @deprecated 用 buildScopeBody 替代(保留向后兼容)。 */
|
|
560
|
+
export function buildGetWfListParamsBody(viewcondition, cfg) {
|
|
561
|
+
return buildScopeBody(viewcondition, CATEGORY_SCOPE.todo, String(cfg.menuIds), String(cfg.menuPathIds ?? cfg.menuIds));
|
|
562
|
+
}
|
|
563
|
+
function logStep(name, r) {
|
|
564
|
+
console.error(`[debug] ${name} -> status=${r.status} json=${r.json ? 'yes' : 'no'} len=${r.text.length}`);
|
|
565
|
+
}
|
|
566
|
+
function truncate(s, n = 300) {
|
|
567
|
+
if (!s)
|
|
568
|
+
return '(空)';
|
|
569
|
+
return s.length > n ? s.slice(0, n) + '…(已截断)' : s;
|
|
570
|
+
}
|