weaver-e9-assistant-cli 1.1.5 → 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/dist/auth-token.js +4 -4
- package/dist/cache-store.js +73 -0
- package/dist/cli.js +714 -57
- 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/memory.js +968 -0
- package/dist/nodemap.js +232 -242
- package/dist/subordinate.js +86 -35
- package/dist/teamreport.js +77 -0
- package/dist/tododetail.js +516 -0
- package/dist/todosubmit.js +619 -26
- package/dist/util.js +21 -0
- package/dist/wfcodeblock.js +761 -0
- package/dist/wfcreate.js +29 -1
- package/dist/wfform.js +286 -67
- package/dist/wfstart.js +516 -81
- 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
CHANGED
|
@@ -1,242 +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
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let filled = 0;
|
|
234
|
-
for (const it of rows) {
|
|
235
|
-
const name = maps.get(workflowIdOf(it))?.nodes?.[str(it.node)];
|
|
236
|
-
if (name) {
|
|
237
|
-
it.nodeName = name;
|
|
238
|
-
filled++;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return filled;
|
|
242
|
-
}
|
|
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
|
+
}
|