weaver-e9-assistant-cli 1.1.4 → 1.1.11
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 +2 -0
- package/dist/auth-token.js +4 -4
- package/dist/cache-store.js +73 -0
- package/dist/cli.js +1508 -69
- package/dist/client.js +1 -1
- package/dist/cube.js +16 -38
- package/dist/formula.js +246 -0
- package/dist/hrm.js +33 -19
- package/dist/kq.js +65 -0
- package/dist/memory.js +968 -0
- package/dist/nodemap.js +232 -0
- package/dist/odoc.js +7 -2
- package/dist/parse.js +4 -0
- package/dist/subordinate.js +111 -35
- package/dist/supervise.js +152 -0
- package/dist/teamreport.js +77 -0
- package/dist/todobatch.js +20 -9
- package/dist/tododetail.js +516 -0
- package/dist/todosubmit.js +657 -17
- package/dist/util.js +21 -0
- package/dist/wfagent.js +538 -0
- package/dist/wfcodeblock.js +761 -0
- package/dist/wfcreate.js +29 -1
- package/dist/wfform.js +286 -67
- package/dist/wfstart.js +518 -79
- package/dist/wfsummary.js +1 -1
- package/dist/wfupload.js +48 -4
- package/dist/workflow.js +4 -2
- package/dist/workplan.js +21 -6
- package/package.json +2 -2
package/dist/nodemap.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流程节点名解析:nodeid → nodename。
|
|
3
|
+
*
|
|
4
|
+
* 背景:待办/已办/我发起列表服务端**只给当前节点的 id**——行字段 `currentnodeid` 的值形如 "2882",
|
|
5
|
+
* 且行里没有节点名(实测 `currentnodeidspan` 也只是 id 本身,整行不含 currentnodename/currentnodespan)。
|
|
6
|
+
* 要显示名字只能按流程另取节点表。
|
|
7
|
+
*
|
|
8
|
+
* 接口(2026-10-14 实测 e9pc.e-cology.com.cn):
|
|
9
|
+
* POST /api/workflow/test/getNodeInfo body: { workflowId }
|
|
10
|
+
* → { nodeInfo: [{id, name, type}], wfInfo: {workflowName, version}, authority, tabInfo, ... }
|
|
11
|
+
* - 只需 workflowId;文档里的 requestid/userId/type 都非必需(逐个试过)。
|
|
12
|
+
* - ⚠️ 路径必须带 `/api` 前缀:`/workflow/test/getNodeInfo`(无前缀)返回登录页 HTML。
|
|
13
|
+
* - ⚠️ 接口挂在「流程测试」模块下,非所有部署/版本都有 → **一律 fail-soft**:
|
|
14
|
+
* 拿不到就返回 null,调用方退回显示节点 id,绝不让列表整体报错。
|
|
15
|
+
* - `type`:0=发起节点 / 1=审批节点 / 3=归档节点。
|
|
16
|
+
*
|
|
17
|
+
* 缓存:`~/.e9/node_map_<envkey>.json`,按 workflowId 存,TTL 30 天
|
|
18
|
+
* (节点表几乎不变;响应带 version,流程改版后重新拉取自然覆盖)。
|
|
19
|
+
* - `E9_NODEMAP_CACHE_DIR` 覆盖缓存目录(测试隔离用,避免污染真实 ~/.e9)。
|
|
20
|
+
* - `E9_NO_NODE_NAME=1` 或 CLI `--no-node-name` 整体停用解析。
|
|
21
|
+
*/
|
|
22
|
+
import { createApiFromProfile } from './client.js';
|
|
23
|
+
import { endpointFor, profileNameFromBaseUrl } from './config.js';
|
|
24
|
+
import { stripHtml } from './parse.js';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
|
|
28
|
+
/** 节点表缓存 TTL(天)。 */
|
|
29
|
+
export const NODEMAP_TTL_MS = (Number(process.env.E9_NODEMAP_TTL_DAYS ?? '30') || 30) * 24 * 3600 * 1000;
|
|
30
|
+
/** 并发拉取上限(一次列表页通常 ≤10 个流程,全量拉取路径可能几十个,别一次打满)。 */
|
|
31
|
+
const FETCH_CHUNK = 6;
|
|
32
|
+
|
|
33
|
+
function str(v) {
|
|
34
|
+
if (v === undefined || v === null || v === '')
|
|
35
|
+
return undefined;
|
|
36
|
+
return String(v);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 缓存目录(测试可用 E9_NODEMAP_CACHE_DIR 覆盖)。 */
|
|
40
|
+
export function nodeMapCacheDir() {
|
|
41
|
+
return process.env.E9_NODEMAP_CACHE_DIR || path.join(os.homedir(), '.e9');
|
|
42
|
+
}
|
|
43
|
+
/** 缓存文件路径:~/.e9/node_map_<envkey>.json(按环境隔离,nodeid 不跨环境通用)。 */
|
|
44
|
+
export function nodeMapCachePath(cfg) {
|
|
45
|
+
const baseUrl = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
46
|
+
return path.join(nodeMapCacheDir(), `node_map_${profileNameFromBaseUrl(baseUrl)}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 是否停用节点名解析(E9_NO_NODE_NAME=1)。 */
|
|
50
|
+
export function isNodeNameDisabled() {
|
|
51
|
+
return process.env.E9_NO_NODE_NAME === '1';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 纯函数:把 getNodeInfo 响应归一化为 { workflowName, version, nodes:{id:name} }。
|
|
56
|
+
* 结构不符(登录页/空表/非 JSON)返回 null——调用方据此 fail-soft。
|
|
57
|
+
*/
|
|
58
|
+
export function parseNodeInfo(json) {
|
|
59
|
+
const list = json?.nodeInfo;
|
|
60
|
+
if (!Array.isArray(list))
|
|
61
|
+
return null;
|
|
62
|
+
const nodes = {};
|
|
63
|
+
for (const n of list) {
|
|
64
|
+
const id = str(n?.id);
|
|
65
|
+
if (!id)
|
|
66
|
+
continue;
|
|
67
|
+
nodes[id] = stripHtml(n?.name) || id;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(nodes).length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
workflowName: str(json?.wfInfo?.workflowName),
|
|
73
|
+
version: str(json?.wfInfo?.version),
|
|
74
|
+
nodes,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
import { cacheGet, cacheUpdate } from './cache-store.js';
|
|
79
|
+
|
|
80
|
+
/** 读缓存文件;坏文件/不存在返回 {}。 */
|
|
81
|
+
function readCache(cfg) {
|
|
82
|
+
const raw = cacheGet(nodeMapCachePath(cfg));
|
|
83
|
+
return raw?.workflows ?? {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 写回缓存(单流程);写入失败不阻断(节点名只是锦上添花)。 */
|
|
87
|
+
function writeCache(cfg, workflowId, entry) {
|
|
88
|
+
const baseUrl = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
89
|
+
cacheUpdate(nodeMapCachePath(cfg), (current) => {
|
|
90
|
+
const workflows = current?.workflows ?? {};
|
|
91
|
+
workflows[workflowId] = entry;
|
|
92
|
+
return {
|
|
93
|
+
envkey: profileNameFromBaseUrl(baseUrl),
|
|
94
|
+
baseUrl,
|
|
95
|
+
updatedAt: new Date().toISOString(),
|
|
96
|
+
workflows,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isStale(entry) {
|
|
102
|
+
if (!entry || !entry.fetchedAt)
|
|
103
|
+
return true;
|
|
104
|
+
return Date.now() - new Date(entry.fetchedAt).getTime() > NODEMAP_TTL_MS;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 取某流程的节点表(带缓存)。任何失败返回 null(fail-soft,不抛)。
|
|
109
|
+
* @returns {Promise<{workflowName?:string, version?:string, nodes:Record<string,string>}|null>}
|
|
110
|
+
*/
|
|
111
|
+
export async function getNodeMap(cfg, workflowId, opts = {}) {
|
|
112
|
+
const wfId = str(workflowId);
|
|
113
|
+
if (!wfId)
|
|
114
|
+
return null;
|
|
115
|
+
const cached = readCache(cfg)[wfId];
|
|
116
|
+
if (!opts.refresh && cached && !isStale(cached) && cached.nodes)
|
|
117
|
+
return { workflowName: cached.workflowName, version: cached.version, nodes: cached.nodes };
|
|
118
|
+
const api = opts.api ?? await createApiFromProfile(cfg);
|
|
119
|
+
let json = null;
|
|
120
|
+
try {
|
|
121
|
+
const r = await api.post('/api/workflow/test/getNodeInfo', { workflowId: wfId });
|
|
122
|
+
json = r?.json ?? null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
json = null; // 网络/鉴权异常同上:降级,不打断列表
|
|
126
|
+
}
|
|
127
|
+
const parsed = parseNodeInfo(json);
|
|
128
|
+
if (!parsed)
|
|
129
|
+
return null;
|
|
130
|
+
writeCache(cfg, wfId, { ...parsed, workflowId: wfId, fetchedAt: new Date().toISOString() });
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 列出**本地缓存**里所有流程的节点表(供「按节点名跨流程解析」用)。
|
|
136
|
+
* 缓存来自历次列表查询;从没查过的流程不在其中。
|
|
137
|
+
*/
|
|
138
|
+
export function listCachedNodeMaps(cfg) {
|
|
139
|
+
const workflows = readCache(cfg);
|
|
140
|
+
return Object.entries(workflows)
|
|
141
|
+
.filter(([, w]) => w && w.nodes && Object.keys(w.nodes).length)
|
|
142
|
+
.map(([workflowId, w]) => ({ workflowId: String(workflowId), workflowName: w.workflowName, nodes: w.nodes }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 按**节点名**解析节点 id(供 CLI `--node-name`)。
|
|
147
|
+
*
|
|
148
|
+
* 节点 id 是**流程内编号**(每个流程一套),故解析必须先限定流程范围:
|
|
149
|
+
* - 给了 `workflowId`:只在该流程的节点表内找(按需拉取,不依赖缓存);
|
|
150
|
+
* - 未给:在**本地缓存**的所有流程节点表内找(缓存来自历次列表查询)。
|
|
151
|
+
*
|
|
152
|
+
* 匹配规则:**先精确同名**;无精确才做模糊(互相包含)。多候选一律**全部返回**,
|
|
153
|
+
* 由调用方列给用户选——严禁自动取首条(本仓库硬规则)。
|
|
154
|
+
*
|
|
155
|
+
* @returns {Promise<{candidates:Array<{workflowId,workflowName,nodeId,nodeName,matchType}>,
|
|
156
|
+
* exact:Array, scoped:boolean, poolSize:number, unavailable:boolean}>}
|
|
157
|
+
* `unavailable`:限定了流程但节点表取不到(接口不存在/异常)——与"确实没有同名节点"要区分开。
|
|
158
|
+
*/
|
|
159
|
+
export async function resolveNodeByName(cfg, name, opts = {}) {
|
|
160
|
+
const want = String(name ?? '').trim();
|
|
161
|
+
const empty = { candidates: [], exact: [], scoped: Boolean(opts.workflowId), poolSize: 0, unavailable: false };
|
|
162
|
+
if (!want)
|
|
163
|
+
return empty;
|
|
164
|
+
let pool = [];
|
|
165
|
+
if (opts.workflowId) {
|
|
166
|
+
const m = await getNodeMap(cfg, opts.workflowId, opts);
|
|
167
|
+
if (!m)
|
|
168
|
+
return { ...empty, unavailable: true };
|
|
169
|
+
pool = [{ workflowId: String(opts.workflowId), workflowName: m.workflowName, nodes: m.nodes }];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
pool = listCachedNodeMaps(cfg);
|
|
173
|
+
}
|
|
174
|
+
const candidates = [];
|
|
175
|
+
for (const w of pool) {
|
|
176
|
+
for (const [nodeId, nodeName] of Object.entries(w.nodes)) {
|
|
177
|
+
if (!nodeName)
|
|
178
|
+
continue;
|
|
179
|
+
const isExact = nodeName === want;
|
|
180
|
+
const isFuzzy = !isExact && (nodeName.includes(want) || want.includes(nodeName));
|
|
181
|
+
if (isExact || isFuzzy)
|
|
182
|
+
candidates.push({ workflowId: w.workflowId, workflowName: w.workflowName, nodeId, nodeName, matchType: isExact ? 'exact' : 'fuzzy' });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
candidates,
|
|
187
|
+
exact: candidates.filter((c) => c.matchType === 'exact'),
|
|
188
|
+
scoped: Boolean(opts.workflowId),
|
|
189
|
+
poolSize: pool.length,
|
|
190
|
+
unavailable: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 从列表行取流程 id(normalizeRow 归一字段优先,回退原始行)。 */
|
|
195
|
+
function workflowIdOf(item) {
|
|
196
|
+
return str(item?.workflowId) ?? str(item?.raw?.workflowid) ?? str(item?.raw?.workflowId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 给列表行补 `nodeName`(就地写回)。返回补上的行数。
|
|
201
|
+
*
|
|
202
|
+
* - 行需带 `node`(节点 id);流程 id 取 `workflowId` 或 `raw.workflowid`。
|
|
203
|
+
* - 按流程去重,每个流程只请求一次;同一流程的多行共用一张表。
|
|
204
|
+
* - 任何失败都不抛:拿不到节点表的行保持原样(渲染层回退显示 id)。
|
|
205
|
+
*/
|
|
206
|
+
export async function annotateNodeNames(cfg, items, opts = {}) {
|
|
207
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
208
|
+
return 0;
|
|
209
|
+
if (opts.disabled)
|
|
210
|
+
return 0;
|
|
211
|
+
// 只对「node 是纯数字 id」的行解析:服务端不同列表的 span 给的东西不一样——
|
|
212
|
+
// 待办/公文给的是**节点 id**(需要解析),流程代理办理记录给的是**节点名**(解析无从匹配,纯浪费请求)。
|
|
213
|
+
const rows = items.filter((it) => /^\d+$/.test(str(it?.node) ?? '') && workflowIdOf(it));
|
|
214
|
+
if (rows.length === 0)
|
|
215
|
+
return 0;
|
|
216
|
+
const wfIds = [...new Set(rows.map(workflowIdOf))];
|
|
217
|
+
const maps = new Map();
|
|
218
|
+
for (let i = 0; i < wfIds.length; i += FETCH_CHUNK) {
|
|
219
|
+
const chunk = wfIds.slice(i, i + FETCH_CHUNK);
|
|
220
|
+
const got = await Promise.all(chunk.map((w) => getNodeMap(cfg, w, opts)));
|
|
221
|
+
chunk.forEach((w, idx) => maps.set(w, got[idx]));
|
|
222
|
+
}
|
|
223
|
+
let filled = 0;
|
|
224
|
+
for (const it of rows) {
|
|
225
|
+
const name = maps.get(workflowIdOf(it))?.nodes?.[str(it.node)];
|
|
226
|
+
if (name) {
|
|
227
|
+
it.nodeName = name;
|
|
228
|
+
filled++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return filled;
|
|
232
|
+
}
|
package/dist/odoc.js
CHANGED
|
@@ -119,8 +119,13 @@ export function normalizeOdocRow(row) {
|
|
|
119
119
|
createDate: stringify(get(['createdate'])),
|
|
120
120
|
sendDepartment: stringify(get(['senddepartment'])),
|
|
121
121
|
lastOperateDate: stringify(get(['lastoperatedate'])),
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// ⚠️ E9 公文列表这两列的**字段名与内容是反的**(2026-10-14 实测 wf=552 发文):
|
|
123
|
+
// currentnodeidspan → "拟稿"(公文环节名,是标签不是 id)
|
|
124
|
+
// currentnodespan → "2868"(真正的**节点 id**,用 getNodeInfo 可解析出节点名「拟稿发起」)
|
|
125
|
+
// 故不再沿用误导性的原名,改为语义正确的 stageName / node;nodeName 由 nodemap 回填。
|
|
126
|
+
stageName: stripHtml(get(['currentnodeidspan'])) || stringify(get(['currentnodeid'])),
|
|
127
|
+
node: stripHtml(get(['currentnodespan'])) || stringify(get(['currentnode'])),
|
|
128
|
+
nodeName: undefined,
|
|
124
129
|
zbr: stringify(get(['zbr'])),
|
|
125
130
|
wbr: stringify(get(['wbr'])),
|
|
126
131
|
docAbstract: stripHtml(get(['docabstractspan'])) || stringify(get(['docabstract'])),
|
package/dist/parse.js
CHANGED
|
@@ -158,7 +158,11 @@ export function normalizeRow(row, columns) {
|
|
|
158
158
|
creatorId,
|
|
159
159
|
createdate: spanOr('createdate', ['createdatespan', 'createDate']),
|
|
160
160
|
receivedate: spanOr('receivedate', ['receivedate', 'recieveDate']),
|
|
161
|
+
// 注意:服务端流程列表**只有节点 id**(currentnodeidspan 实测也是 id 本身,整行无节点名),
|
|
162
|
+
// 节点名由 nodemap.annotateNodeNames 按流程节点表回填到 nodeName。
|
|
161
163
|
node: stripHtml(get(['currentnodeidspan', 'currentnodespan'])) || stringify(get(['currentnodeid', 'nodeName', 'nodename'])),
|
|
164
|
+
nodeName: undefined,
|
|
165
|
+
workflowId: stringify(get(['workflowid', 'workflowId'])),
|
|
162
166
|
status: stripHtml(get(['statusspan'])) || stringify(get(['status'])),
|
|
163
167
|
level,
|
|
164
168
|
requestmark: stringify(get(['requestmark'])),
|
package/dist/subordinate.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* - 姓名→userid 解析唯一入口是 blog at-list(SKILL.md 硬规则),候选按 精确>前缀>拼音 排序后过滤到范围。
|
|
17
17
|
* - 下属表找不到 id 列 → scope 标记 reliable:false → 非本人查询拒绝(安全但功能受限,实跑校准回填规则)。
|
|
18
18
|
*/
|
|
19
|
-
import fs from 'node:fs';
|
|
20
19
|
import path from 'node:path';
|
|
21
20
|
import os from 'node:os';
|
|
22
21
|
import { createApiFromProfile } from './client.js';
|
|
@@ -24,16 +23,22 @@ import { extractDataKey, extractRows, extractColumns, extractTotal, pickByPaths,
|
|
|
24
23
|
import { getCurrentUser } from './wfuser.js';
|
|
25
24
|
import { getAtAllList } from './blog.js';
|
|
26
25
|
import { profileNameFromBaseUrl } from './config.js';
|
|
26
|
+
import { cacheGet, cacheSet } from './cache-store.js';
|
|
27
27
|
|
|
28
28
|
/** 全层级下属翻页上限(超限仍未收全 → reliable:false)。 */
|
|
29
29
|
export const MAX_SCOPE_PAGES = 20;
|
|
30
|
+
/**
|
|
31
|
+
* 授权集合拉取的单页条数。服务端 table/datas 认 pageSize(实测 pageSize=500 一次返回全部 96 条),
|
|
32
|
+
* 用大页把「逐页排队」压成通常 1 次请求;超过 500 人才继续翻页。
|
|
33
|
+
*/
|
|
34
|
+
const SCOPE_PAGE_SIZE = 500;
|
|
30
35
|
/** 授权集合缓存 TTL(默认 12h,比台账目录的 7 天短——越权集合同理会随组织变动,宁短勿长)。 */
|
|
31
36
|
export const SCOPE_TTL_MS = (Number(process.env.E9_SCOPE_TTL_HOURS ?? '12') || 12) * 3600 * 1000;
|
|
32
37
|
/** 缓存目录(测试可用 E9_SCOPE_CACHE_DIR 覆盖,避免污染真实 ~/.e9)。 */
|
|
33
38
|
function scopeCacheDir() {
|
|
34
39
|
return process.env.E9_SCOPE_CACHE_DIR || path.join(os.homedir(), '.e9');
|
|
35
40
|
}
|
|
36
|
-
function scopeCachePath(cfg) {
|
|
41
|
+
export function scopeCachePath(cfg) {
|
|
37
42
|
const envkey = profileNameFromBaseUrl(cfg.baseUrl);
|
|
38
43
|
return path.join(scopeCacheDir(), `subordinate_scope_${envkey}.json`);
|
|
39
44
|
}
|
|
@@ -56,10 +61,13 @@ export async function fetchSubordinateDataKey(cfg, { lastname = '', showAllLevel
|
|
|
56
61
|
return { dataKey, raw: r?.json ?? null };
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/**
|
|
60
|
-
export async function fetchSubordinateDataPage(cfg, dataKey, page) {
|
|
64
|
+
/** 取下属表一页数据(pageSize 可选;不传则跟随服务端默认每页条数)。 */
|
|
65
|
+
export async function fetchSubordinateDataPage(cfg, dataKey, page, { pageSize } = {}) {
|
|
61
66
|
const api = await createApiFromProfile(cfg);
|
|
62
|
-
const
|
|
67
|
+
const body = { dataKey, current: String(page), sortParams: '[]' };
|
|
68
|
+
if (pageSize != null)
|
|
69
|
+
body.pageSize = String(pageSize);
|
|
70
|
+
const r = await api.post('/api/ec/dev/table/datas', body);
|
|
63
71
|
const json = r?.json ?? {};
|
|
64
72
|
return {
|
|
65
73
|
rows: extractRows(json),
|
|
@@ -129,14 +137,19 @@ export async function getSubordinates(cfg, opts = {}) {
|
|
|
129
137
|
const page = Number(opts.page ?? '1') || 1;
|
|
130
138
|
const { dataKey, raw: searchRaw } = await fetchSubordinateDataKey(cfg, { lastname, showAllLevel: allLevel });
|
|
131
139
|
const data = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
132
|
-
|
|
133
|
-
const
|
|
140
|
+
// counts 按需:首页且未填满一页 → 总数即本页条数,省一次请求;否则才查总数。
|
|
141
|
+
const pageSize = data.pageSize ?? (data.rows.length || null);
|
|
142
|
+
const firstPageComplete = page === 1 && data.pageSize && data.rows.length < data.pageSize;
|
|
143
|
+
let count = { total: null, raw: null };
|
|
144
|
+
if (!firstPageComplete)
|
|
145
|
+
count = await fetchSubordinateCount(cfg, dataKey);
|
|
146
|
+
const total = count.total ?? (firstPageComplete ? data.rows.length : null);
|
|
134
147
|
const items = data.rows.map((r) => normalizeSubordinateRow(r, data.columns));
|
|
135
148
|
return {
|
|
136
149
|
lastname,
|
|
137
150
|
allLevel,
|
|
138
151
|
page,
|
|
139
|
-
pageSize
|
|
152
|
+
pageSize,
|
|
140
153
|
total,
|
|
141
154
|
count: items.length,
|
|
142
155
|
dataKey,
|
|
@@ -146,6 +159,53 @@ export async function getSubordinates(cfg, opts = {}) {
|
|
|
146
159
|
};
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
// ─── 全量下属列表(批量按人查询用) ──────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 拉取全部下属(直接/全部层级),供「按人批量查数据」类命令使用。
|
|
166
|
+
* - 大页 pageSize(默认 500)减少翻页;翻页串行(同一 dataKey 并发翻页不可靠)。
|
|
167
|
+
* - names 非空时按姓名精确过滤,并返回未匹配名单(不静默丢人)。
|
|
168
|
+
* - complete=false 表示达到 MAX_SCOPE_PAGES 仍未收全(调用方须提示可能不完整)。
|
|
169
|
+
*/
|
|
170
|
+
export async function listAllSubordinates(cfg, { allLevel = false, names = [], pageSize = SCOPE_PAGE_SIZE } = {}) {
|
|
171
|
+
const wanted = (Array.isArray(names) ? names : [names]).map((n) => String(n ?? '').trim()).filter(Boolean);
|
|
172
|
+
const { dataKey } = await fetchSubordinateDataKey(cfg, { lastname: '', showAllLevel: !!allLevel });
|
|
173
|
+
let page = 1;
|
|
174
|
+
let complete = false;
|
|
175
|
+
let columns = [];
|
|
176
|
+
let rows = [];
|
|
177
|
+
while (page <= MAX_SCOPE_PAGES) {
|
|
178
|
+
const d = await fetchSubordinateDataPage(cfg, dataKey, page, { pageSize });
|
|
179
|
+
if (page === 1)
|
|
180
|
+
columns = d.columns;
|
|
181
|
+
rows = rows.concat(d.rows);
|
|
182
|
+
if (d.rows.length === 0 || (d.pageSize && d.rows.length < d.pageSize)) {
|
|
183
|
+
complete = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
page++;
|
|
187
|
+
}
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const all = [];
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
const item = normalizeSubordinateRow(row, columns);
|
|
192
|
+
const id = String(item.id ?? '');
|
|
193
|
+
if (!id || seen.has(id))
|
|
194
|
+
continue;
|
|
195
|
+
seen.add(id);
|
|
196
|
+
all.push({ id, name: item.name, fields: item.fields });
|
|
197
|
+
}
|
|
198
|
+
let items = all;
|
|
199
|
+
let unmatched = [];
|
|
200
|
+
if (wanted.length) {
|
|
201
|
+
const wantedSet = new Set(wanted);
|
|
202
|
+
items = all.filter((it) => wantedSet.has(String(it.name ?? '').trim()));
|
|
203
|
+
const found = new Set(items.map((it) => String(it.name ?? '').trim()));
|
|
204
|
+
unmatched = wanted.filter((n) => !found.has(n));
|
|
205
|
+
}
|
|
206
|
+
return { items, columns, total: all.length, count: items.length, unmatched, complete };
|
|
207
|
+
}
|
|
208
|
+
|
|
149
209
|
// ─── 授权集合加载 + 缓存(严格 fail-closed) ─────────────
|
|
150
210
|
|
|
151
211
|
/**
|
|
@@ -175,7 +235,10 @@ export async function loadScope(cfg, opts = {}) {
|
|
|
175
235
|
return { ...cached, selfId, selfName, userIds: new Set(cached.userIds), userNames: new Set(cached.userNames), fromCache: true };
|
|
176
236
|
}
|
|
177
237
|
}
|
|
178
|
-
// 3.
|
|
238
|
+
// 3. 拉全层级下属:大页 + 串行翻页
|
|
239
|
+
// - 服务端认 pageSize,SCOPE_PAGE_SIZE=500 通常一次拿全,避免逐页排队;
|
|
240
|
+
// - ⚠ 不能并发翻页:实测同一 dataKey 并发请求 table/datas 时,服务端偶发返回空页
|
|
241
|
+
// (5 次里 1 次出现连续两页 0 行),会导致 scope 判定不可靠。此处必须串行。
|
|
179
242
|
const userIds = new Set([selfId]);
|
|
180
243
|
const userNames = new Set([selfName].filter(Boolean));
|
|
181
244
|
let reliable = false;
|
|
@@ -188,7 +251,7 @@ export async function loadScope(cfg, opts = {}) {
|
|
|
188
251
|
let page = 1;
|
|
189
252
|
let hitEnd = false;
|
|
190
253
|
while (page <= MAX_SCOPE_PAGES) {
|
|
191
|
-
const d = await fetchSubordinateDataPage(cfg, dataKey, page);
|
|
254
|
+
const d = await fetchSubordinateDataPage(cfg, dataKey, page, { pageSize: SCOPE_PAGE_SIZE });
|
|
192
255
|
if (page === 1) {
|
|
193
256
|
idCol = detectSubordinateIdColumn(d.columns);
|
|
194
257
|
nameCol = detectSubordinateNameColumn(d.columns);
|
|
@@ -233,35 +296,23 @@ export async function loadScope(cfg, opts = {}) {
|
|
|
233
296
|
|
|
234
297
|
/** 读缓存;selfId 不匹配 / 过期 / 不可靠 → null。 */
|
|
235
298
|
function readScopeCache(cfg, selfId) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return null;
|
|
240
|
-
if (Date.now() - (raw.fetchedAt || 0) > SCOPE_TTL_MS)
|
|
241
|
-
return null;
|
|
242
|
-
return raw;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
299
|
+
const raw = cacheGet(scopeCachePath(cfg), SCOPE_TTL_MS);
|
|
300
|
+
if (!raw || raw.selfId !== selfId || raw.reliable !== true) return null;
|
|
301
|
+
return raw;
|
|
247
302
|
}
|
|
248
303
|
|
|
249
304
|
/** 写缓存(失败不阻断)。 */
|
|
250
305
|
function writeScopeCache(cfg, scope) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
fetchedAt: scope.fetchedAt,
|
|
262
|
-
}, null, 2) + '\n', 'utf8');
|
|
263
|
-
}
|
|
264
|
-
catch { /* 缓存写失败不阻断查询 */ }
|
|
306
|
+
cacheSet(scopeCachePath(cfg), {
|
|
307
|
+
envkey: profileNameFromBaseUrl(cfg.baseUrl),
|
|
308
|
+
baseUrl: cfg.baseUrl,
|
|
309
|
+
selfId: scope.selfId,
|
|
310
|
+
selfName: scope.selfName,
|
|
311
|
+
userIds: [...scope.userIds],
|
|
312
|
+
userNames: [...scope.userNames],
|
|
313
|
+
reliable: scope.reliable,
|
|
314
|
+
fetchedAt: scope.fetchedAt,
|
|
315
|
+
});
|
|
265
316
|
}
|
|
266
317
|
|
|
267
318
|
/** 是否在授权集合内(纯函数,便于单测)。 */
|
|
@@ -342,6 +393,31 @@ export async function assertNamesInScope(cfg, names, opts = {}) {
|
|
|
342
393
|
return resolved.length === 1 ? resolved[0] : resolved;
|
|
343
394
|
}
|
|
344
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 仅把姓名解析成 uid,**不做任何范围过滤**。
|
|
398
|
+
*
|
|
399
|
+
* 与 assertNamesInScope 的区别:后者用于「查询他人」类筛选,越界要拦;
|
|
400
|
+
* 而本函数用于**给他人派活**的场景(如流程代理把待办委托给谁——委托对象不限于下属),
|
|
401
|
+
* 只需遵守「人员搜索必须走 blog at-list」这条硬规则,不施加授权集合限制。
|
|
402
|
+
* 恰 1 个候选才返回;0 个 / 多个一律抛错并**列全候选**(不自动取首条)。
|
|
403
|
+
*/
|
|
404
|
+
export async function resolveNameToUid(cfg, name) {
|
|
405
|
+
const n = String(name ?? '').trim();
|
|
406
|
+
if (!n)
|
|
407
|
+
throw new Error('人员姓名为空。');
|
|
408
|
+
if (/^\d+$/.test(n))
|
|
409
|
+
return n;
|
|
410
|
+
const r = await getAtAllList(cfg, { name: n });
|
|
411
|
+
const candidates = sortPersonCandidates(r.items, n);
|
|
412
|
+
if (candidates.length === 0)
|
|
413
|
+
throw new Error(`未找到「${n}」对应的人员(at-list 无候选)。`);
|
|
414
|
+
if (candidates.length > 1) {
|
|
415
|
+
const list = candidates.slice(0, 10).map((c) => `${stripHtml(String(c.name ?? ''))}(${c.uid})`).join('、');
|
|
416
|
+
throw new Error(`「${n}」匹配到 ${candidates.length} 个人员,请确认后用 --agent <用户id> 指定:${list}${candidates.length > 10 ? ' …' : ''}`);
|
|
417
|
+
}
|
|
418
|
+
return String(candidates[0].uid ?? '');
|
|
419
|
+
}
|
|
420
|
+
|
|
345
421
|
/** 候选过滤到授权集合(纯函数)。策略未启用(默认)时不过滤,全部视为在范围内。 */
|
|
346
422
|
export function filterCandidatesToScope(scope, candidates) {
|
|
347
423
|
if (!isScopeEnforced()) {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E9 流程督办(supervise)模块。
|
|
3
|
+
*
|
|
4
|
+
* 链路(HAR 抓包实证 + 前端 bundle 逆向):
|
|
5
|
+
* 1) POST /api/workflow/supervise/getTree body{menuIds,menuPathIds} → topTab[](页签)+ treedata(流程类型树)
|
|
6
|
+
* 2) POST /api/workflow/supervise/getTreeCount body 同上 → {flowAllCounts, flowOverCounts, treecount{}} 各类型计数
|
|
7
|
+
* 3) POST /api/workflow/supervise/getConditionInfo body 空 → conditioninfo(搜索条件定义,结构与待办同构)
|
|
8
|
+
* 4) POST /api/workflow/supervise/getSessionkey body{同待办过滤项 + tabKey} → sessionkey(**恒顶层**)
|
|
9
|
+
* tabKey:0=全部 / 1=未督办 / 2=已督办 / 3=我的关注
|
|
10
|
+
* 5) POST /api/ec/dev/table/datas body{dataKey=sessionkey,current,sortParams} → columns + datas 行
|
|
11
|
+
* 6) POST /api/ec/dev/table/counts body{dataKey} → {count}
|
|
12
|
+
* 7) POST /api/workflow/supervise/doSupervise body{requestids,remark} → 发起督办(勾选多条用逗号分隔)
|
|
13
|
+
*
|
|
14
|
+
* 列表行 `ops` 含 4 个操作(前端 href 实证):督办(handle) / 短信催办(smsHandle) / 邮件催办(emailHandle) / 关注(doAttention)。
|
|
15
|
+
* ⚠️ 短信/邮件催办的**实际发送接口未抓到**(前端 getSendEmailInfo 取信息后要打开弹窗填写再发),故本模块只做「督办」。
|
|
16
|
+
*/
|
|
17
|
+
import { endpointFor } from './config.js';
|
|
18
|
+
import { createApiFromProfile } from './client.js';
|
|
19
|
+
import { extractRows, extractColumns, extractTotal, pickByPaths, stripHtml } from './parse.js';
|
|
20
|
+
|
|
21
|
+
/** 督办菜单 id(抓包实证:menuIds=1,235)。 */
|
|
22
|
+
const SUPERVISE_MENU = { menuIds: '1,235', menuPathIds: '1,235' };
|
|
23
|
+
|
|
24
|
+
/** 督办页签:tabKey ↔ 中文名(getTree.topTab 实证)。 */
|
|
25
|
+
export const SUPERVISE_TABS = {
|
|
26
|
+
'全部': '0', '未督办': '1', '已督办': '2', '我的关注': '3',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 页签名/别名 → tabKey(数字直传)。 */
|
|
30
|
+
export function resolveSuperviseTab(spec) {
|
|
31
|
+
const s = String(spec ?? '').trim();
|
|
32
|
+
if (!s)
|
|
33
|
+
return '0';
|
|
34
|
+
if (/^[0-3]$/.test(s))
|
|
35
|
+
return s;
|
|
36
|
+
const key = Object.keys(SUPERVISE_TABS).find((k) => k === s || k.includes(s));
|
|
37
|
+
return key ? SUPERVISE_TABS[key] : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 督办页签 + 流程类型树。POST /api/workflow/supervise/getTree */
|
|
41
|
+
export async function getSuperviseTree(cfg) {
|
|
42
|
+
const api = await createApiFromProfile(cfg);
|
|
43
|
+
const r = await api.post('/api/workflow/supervise/getTree', SUPERVISE_MENU);
|
|
44
|
+
const j = r.json ?? {};
|
|
45
|
+
return {
|
|
46
|
+
tabs: (j.topTab ?? []).map((t) => ({ key: String(t.key ?? ''), title: String(t.title ?? '') })),
|
|
47
|
+
tree: j.treedata ?? [],
|
|
48
|
+
raw: j,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 督办计数。POST /api/workflow/supervise/getTreeCount(flowAllCounts=总数、flowOverCounts=未督办)。 */
|
|
53
|
+
export async function getSuperviseCount(cfg) {
|
|
54
|
+
const api = await createApiFromProfile(cfg);
|
|
55
|
+
const r = await api.post('/api/workflow/supervise/getTreeCount', SUPERVISE_MENU);
|
|
56
|
+
const j = r.json ?? {};
|
|
57
|
+
return {
|
|
58
|
+
total: j.flowAllCounts ?? null,
|
|
59
|
+
pending: j.flowOverCounts ?? null,
|
|
60
|
+
treeCount: j.treecount ?? {},
|
|
61
|
+
raw: j,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 督办搜索条件(结构与待办同构)。POST /api/workflow/supervise/getConditionInfo */
|
|
66
|
+
export async function getSuperviseConditions(cfg) {
|
|
67
|
+
const api = await createApiFromProfile(cfg);
|
|
68
|
+
const r = await api.post('/api/workflow/supervise/getConditionInfo', {});
|
|
69
|
+
const j = r.json ?? {};
|
|
70
|
+
const groups = (j.conditioninfo ?? []).map((g) => ({
|
|
71
|
+
title: String(g.title ?? ''),
|
|
72
|
+
items: (g.items ?? []).map((it) => ({
|
|
73
|
+
label: String(it.label ?? ''),
|
|
74
|
+
domkey: Array.isArray(it.domkey) ? it.domkey.join(',') : String(it.domkey ?? ''),
|
|
75
|
+
type: String(it.conditionType ?? ''),
|
|
76
|
+
})),
|
|
77
|
+
}));
|
|
78
|
+
return { groups, raw: j };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 督办列表。getSessionkey → table/datas → table/counts(同待办 table 链路)。
|
|
83
|
+
* opts: { tabKey, keyword, page, pageSize, ...过滤项 }
|
|
84
|
+
*/
|
|
85
|
+
export async function listSupervise(cfg, opts = {}) {
|
|
86
|
+
const api = await createApiFromProfile(cfg);
|
|
87
|
+
const tabKey = String(opts.tabKey ?? '0');
|
|
88
|
+
const body = {
|
|
89
|
+
requestname: opts.keyword ?? '',
|
|
90
|
+
workcode: '', requestlevel: '', creatertype: '0',
|
|
91
|
+
ownerdepartmentid: opts.creatorDept ?? '', creatersubcompanyid: opts.creatorSubcom ?? '',
|
|
92
|
+
receivedateselect: '0', receivedatefrom: '', receivedateto: '',
|
|
93
|
+
createdateselect: '0', createdatefrom: '', createdateto: '',
|
|
94
|
+
wfstatu: '1', currentnodetype: '', unophrmid: '', docids: '',
|
|
95
|
+
hrmcreaterid: opts.creator ?? '', crmids: '', proids: '',
|
|
96
|
+
...SUPERVISE_MENU, tabKey,
|
|
97
|
+
};
|
|
98
|
+
const r1 = await api.post('/api/workflow/supervise/getSessionkey', body);
|
|
99
|
+
const sessionkey = pickByPaths(r1.json, ['sessionkey', 'data.sessionkey']);
|
|
100
|
+
if (!sessionkey)
|
|
101
|
+
throw new Error(`未拿到督办 sessionkey(getSessionkey 响应:${(r1.text ?? '').slice(0, 200)})`);
|
|
102
|
+
const page = Number(opts.page ?? '1') || 1;
|
|
103
|
+
const r2 = await api.post('/api/ec/dev/table/datas', { dataKey: sessionkey, current: String(page), sortParams: '[]', pageSize: String(opts.pageSize ?? '10') });
|
|
104
|
+
const columns = extractColumns(r2.json);
|
|
105
|
+
const rows = extractRows(r2.json);
|
|
106
|
+
const items = rows.map((row) => normalizeSuperviseRow(row, columns));
|
|
107
|
+
let total = null;
|
|
108
|
+
let countsRaw = null;
|
|
109
|
+
try {
|
|
110
|
+
const r3 = await api.post('/api/ec/dev/table/counts', { dataKey: sessionkey });
|
|
111
|
+
countsRaw = r3.json;
|
|
112
|
+
total = extractTotal(r3.json);
|
|
113
|
+
}
|
|
114
|
+
catch { /* counts 失败不阻断 */ }
|
|
115
|
+
let pageSize = pickByPaths(r2.json, ['pageSize', 'data.pageSize']);
|
|
116
|
+
if (typeof pageSize !== 'number' && /^\d+$/.test(String(pageSize ?? '')))
|
|
117
|
+
pageSize = parseInt(String(pageSize), 10);
|
|
118
|
+
return { tabKey, page, total, pageSize: pageSize ?? null, items, sessionkey, columns, raw: { getSessionkey: r1.json, datas: r2.json, counts: countsRaw } };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 规范化一行督办(可读值优先取 `<field>span`,回退原值)。 */
|
|
122
|
+
export function normalizeSuperviseRow(row, columns) {
|
|
123
|
+
if (!row || typeof row !== 'object')
|
|
124
|
+
return {};
|
|
125
|
+
void columns;
|
|
126
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
127
|
+
const spanOr = (k, extra = []) => stripHtml(get([`${k}span`, k, ...extra]));
|
|
128
|
+
return {
|
|
129
|
+
requestId: String(pickByPaths(row, ['requestid', 'randomField2', 'randomFieldId']) ?? ''),
|
|
130
|
+
title: stripHtml(get(['requestnamespan'])) || String(get(['requestname', 'requestName']) ?? ''),
|
|
131
|
+
creator: spanOr('creater', ['createrName']),
|
|
132
|
+
createTime: spanOr('createtime', ['createdate']),
|
|
133
|
+
node: stripHtml(get(['currentnodeidspan', 'currentnodespan'])) || String(get(['currentnodeid', 'currentnodename']) ?? ''),
|
|
134
|
+
unoperators: stripHtml(get(['unoperatorsspan', 'unoperators'])),
|
|
135
|
+
workflowId: String(get(['workflowid', 'randomField0']) ?? ''),
|
|
136
|
+
raw: row,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 发起督办。POST /api/workflow/supervise/doSupervise(body: requestids + remark)。 */
|
|
141
|
+
export async function doSupervise(cfg, opts = {}) {
|
|
142
|
+
const ep = endpointFor(cfg);
|
|
143
|
+
const requestIds = String(opts.requestIds ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
144
|
+
const remark = String(opts.remark ?? '');
|
|
145
|
+
if (!requestIds.length)
|
|
146
|
+
throw new Error('doSupervise 需要 requestIds(逗号分隔多个)。');
|
|
147
|
+
if (!opts.send)
|
|
148
|
+
return { sent: false, endpoint: { baseUrl: ep.baseUrl }, requestIds, remark };
|
|
149
|
+
const api = await createApiFromProfile(cfg);
|
|
150
|
+
const r = await api.post('/api/workflow/supervise/doSupervise', { requestids: requestIds.join(','), remark });
|
|
151
|
+
return { sent: true, endpoint: { baseUrl: ep.baseUrl }, requestIds, remark, response: r };
|
|
152
|
+
}
|