weaver-e9-assistant-cli 1.1.5 → 1.1.12
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/client.js
CHANGED
package/dist/cube.js
CHANGED
|
@@ -31,6 +31,7 @@ import { createApiFromProfile, e9Request } from './client.js';
|
|
|
31
31
|
import { endpointFor, profileNameFromBaseUrl } from './config.js';
|
|
32
32
|
import { pickByPaths, stripHtml, extractColumns, extractRows, extractTotal } from './parse.js';
|
|
33
33
|
import { sleep } from './util.js';
|
|
34
|
+
import { cacheGet, cacheSet } from './cache-store.js';
|
|
34
35
|
import fs from 'node:fs';
|
|
35
36
|
import path from 'node:path';
|
|
36
37
|
import os from 'node:os';
|
|
@@ -1399,47 +1400,24 @@ export function cubeCachePath(cfg) {
|
|
|
1399
1400
|
const envkey = profileNameFromBaseUrl(baseUrl);
|
|
1400
1401
|
return path.join(os.homedir(), '.e9', `cube_catalog_${envkey}.json`);
|
|
1401
1402
|
}
|
|
1402
|
-
/** 读缓存;返回 {entries, fetchedAt, source, baseUrl} 或 null
|
|
1403
|
+
/** 读缓存;返回 {entries, fetchedAt, source, baseUrl} 或 null(含 TTL 检查)。 */
|
|
1403
1404
|
function readCubeCache(cfg) {
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
1409
|
-
if (!raw || !Array.isArray(raw.entries))
|
|
1410
|
-
return null;
|
|
1411
|
-
return raw;
|
|
1412
|
-
}
|
|
1413
|
-
catch {
|
|
1414
|
-
return null;
|
|
1415
|
-
}
|
|
1405
|
+
const TTL_MS = CUBE_CATALOG_TTL_DAYS * 24 * 3600 * 1000;
|
|
1406
|
+
const raw = cacheGet(cubeCachePath(cfg), TTL_MS);
|
|
1407
|
+
if (!raw || !Array.isArray(raw.entries)) return null;
|
|
1408
|
+
return raw;
|
|
1416
1409
|
}
|
|
1417
1410
|
/** 写缓存(全量预热)。写入失败不阻断查询。 */
|
|
1418
1411
|
function writeCubeCache(cfg, entries, source) {
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
count: entries.length,
|
|
1429
|
-
entries,
|
|
1430
|
-
};
|
|
1431
|
-
fs.writeFileSync(p, JSON.stringify(raw, null, 2) + '\n', 'utf8');
|
|
1432
|
-
}
|
|
1433
|
-
catch (e) {
|
|
1434
|
-
console.error('[cube cache] 写入失败:', e.message);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
/** 缓存是否过期(超 TTL 或无 fetchedAt)。 */
|
|
1438
|
-
function isCacheStale(cache) {
|
|
1439
|
-
if (!cache || !cache.fetchedAt)
|
|
1440
|
-
return true;
|
|
1441
|
-
const ageMs = Date.now() - new Date(cache.fetchedAt).getTime();
|
|
1442
|
-
return ageMs > CUBE_CATALOG_TTL_DAYS * 24 * 3600 * 1000;
|
|
1412
|
+
const baseUrl = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
1413
|
+
cacheSet(cubeCachePath(cfg), {
|
|
1414
|
+
envkey: profileNameFromBaseUrl(baseUrl),
|
|
1415
|
+
baseUrl,
|
|
1416
|
+
fetchedAt: new Date().toISOString(),
|
|
1417
|
+
source,
|
|
1418
|
+
count: entries.length,
|
|
1419
|
+
entries,
|
|
1420
|
+
});
|
|
1443
1421
|
}
|
|
1444
1422
|
/**
|
|
1445
1423
|
* 路径1:调目录接口拉全量台账。
|
|
@@ -1640,7 +1618,7 @@ export async function listCubeMenus(cfg, opts = {}) {
|
|
|
1640
1618
|
// 1. 缓存命中
|
|
1641
1619
|
if (!opts.refresh && !opts.noCache) {
|
|
1642
1620
|
const cache = readCubeCache(cfg);
|
|
1643
|
-
if (cache &&
|
|
1621
|
+
if (cache && Array.isArray(cache.entries) && cache.entries.length > 0) {
|
|
1644
1622
|
return { entries: cache.entries, source: cache.source || 'catalog', count: cache.entries.length, fetchedAt: cache.fetchedAt, fromCache: true };
|
|
1645
1623
|
}
|
|
1646
1624
|
}
|
package/dist/formula.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流程表单公式(loadForm.linkageCfg.formulaCfg)本地计算。
|
|
3
|
+
*
|
|
4
|
+
* E9 的简单公式由前端 formulaNew_wev8.js 在浏览器本地执行(不是服务端接口),
|
|
5
|
+
* 表达式形如 `[main.27792]*[main.27793]`,目标主表/明细字段。
|
|
6
|
+
* 本模块支持四则运算 + 单元格引用;遇到函数(SUM/IF/...)、跨表明细引用等
|
|
7
|
+
* 未实现语法时跳过并在 skipped 里说明,绝不猜结果。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function parseMaybeJson(value, fallback) {
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value;
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(value);
|
|
16
|
+
return parsed ?? fallback;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toNumber(value) {
|
|
26
|
+
if (value === undefined || value === null || String(value).trim() === '')
|
|
27
|
+
return 0;
|
|
28
|
+
const n = Number(String(value).replace(/,/g, '').trim());
|
|
29
|
+
if (!Number.isFinite(n))
|
|
30
|
+
throw new Error(`非数字值: ${value}`);
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatNumber(n) {
|
|
35
|
+
if (!Number.isFinite(n))
|
|
36
|
+
throw new Error('计算结果不是有效数字');
|
|
37
|
+
// 避免 0.1+0.2 之类的浮点噪声;业务金额通常 2 位,保留 10 位足够
|
|
38
|
+
return String(Number(n.toFixed(10)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getValue(obj, id) {
|
|
42
|
+
if (!obj)
|
|
43
|
+
return undefined;
|
|
44
|
+
const fieldKey = `field${id}`;
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(obj, fieldKey))
|
|
46
|
+
return obj[fieldKey];
|
|
47
|
+
return obj[id];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setValue(obj, id, value) {
|
|
51
|
+
if (!obj)
|
|
52
|
+
return;
|
|
53
|
+
const fieldKey = `field${id}`;
|
|
54
|
+
// 保持调用方原有键风格:对象里已有 field<id> 键时继续用 field<id>,否则用纯 id
|
|
55
|
+
const usesFieldPrefix = Object.keys(obj).some((k) => /^field\d+/.test(k));
|
|
56
|
+
if (usesFieldPrefix || Object.prototype.hasOwnProperty.call(obj, fieldKey))
|
|
57
|
+
obj[fieldKey] = value;
|
|
58
|
+
else
|
|
59
|
+
obj[id] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tokenize(expression) {
|
|
63
|
+
const s = String(expression)
|
|
64
|
+
.replace(/×/g, '*').replace(/÷/g, '/')
|
|
65
|
+
.replace(/+/g, '+').replace(/-/g, '-');
|
|
66
|
+
const tokens = [];
|
|
67
|
+
for (let i = 0; i < s.length;) {
|
|
68
|
+
const c = s[i];
|
|
69
|
+
if (/\s/.test(c)) {
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === '[') {
|
|
74
|
+
const end = s.indexOf(']', i);
|
|
75
|
+
if (end < 0)
|
|
76
|
+
throw new Error('公式缺少 ]');
|
|
77
|
+
tokens.push({ t: 'ref', v: s.slice(i + 1, end).trim() });
|
|
78
|
+
i = end + 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (/[0-9.]/.test(c)) {
|
|
82
|
+
let j = i;
|
|
83
|
+
while (j < s.length && /[0-9.]/.test(s[j]))
|
|
84
|
+
j++;
|
|
85
|
+
tokens.push({ t: 'num', v: s.slice(i, j) });
|
|
86
|
+
i = j;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ('+-*/()'.includes(c)) {
|
|
90
|
+
tokens.push({ t: 'op', v: c });
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`不支持的公式字符: ${c}`);
|
|
95
|
+
}
|
|
96
|
+
return tokens;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function evaluate(expression, { resolve }) {
|
|
100
|
+
const tokens = tokenize(expression);
|
|
101
|
+
let pos = 0;
|
|
102
|
+
const peek = () => tokens[pos];
|
|
103
|
+
const eat = (v) => {
|
|
104
|
+
if (!peek() || peek().v !== v)
|
|
105
|
+
throw new Error(`公式语法错误:期望 ${v}`);
|
|
106
|
+
pos++;
|
|
107
|
+
};
|
|
108
|
+
const parseExpression = () => {
|
|
109
|
+
let value = parseTerm();
|
|
110
|
+
while (peek() && (peek().v === '+' || peek().v === '-')) {
|
|
111
|
+
const op = tokens[pos++].v;
|
|
112
|
+
const right = parseTerm();
|
|
113
|
+
value = op === '+' ? value + right : value - right;
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
};
|
|
117
|
+
const parseTerm = () => {
|
|
118
|
+
let value = parseFactor();
|
|
119
|
+
while (peek() && (peek().v === '*' || peek().v === '/')) {
|
|
120
|
+
const op = tokens[pos++].v;
|
|
121
|
+
const right = parseFactor();
|
|
122
|
+
if (op === '/' && right === 0)
|
|
123
|
+
throw new Error('除数为 0');
|
|
124
|
+
value = op === '*' ? value * right : value / right;
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
const parseFactor = () => {
|
|
129
|
+
const token = peek();
|
|
130
|
+
if (!token)
|
|
131
|
+
throw new Error('公式不完整');
|
|
132
|
+
if (token.t === 'op' && token.v === '-') {
|
|
133
|
+
pos++;
|
|
134
|
+
return -parseFactor();
|
|
135
|
+
}
|
|
136
|
+
if (token.t === 'op' && token.v === '(') {
|
|
137
|
+
pos++;
|
|
138
|
+
const value = parseExpression();
|
|
139
|
+
eat(')');
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
if (token.t === 'num') {
|
|
143
|
+
pos++;
|
|
144
|
+
return Number(token.v);
|
|
145
|
+
}
|
|
146
|
+
if (token.t === 'ref') {
|
|
147
|
+
pos++;
|
|
148
|
+
return resolve(token.v);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`公式语法错误:${token.v}`);
|
|
151
|
+
};
|
|
152
|
+
const value = parseExpression();
|
|
153
|
+
if (pos !== tokens.length)
|
|
154
|
+
throw new Error('公式语法错误:存在多余内容');
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 执行 formulaCfg 里的公式(主表 + 明细)。
|
|
160
|
+
* values: 主表字段值(key 可为 "<id>" 或 "field<id>")
|
|
161
|
+
* details: [{index, rows:[{<fieldId>: value}]}]
|
|
162
|
+
* 返回 { applied:[{dest, formula, value, table, row}], skipped:[{dest, formula, reason}] }
|
|
163
|
+
*/
|
|
164
|
+
export function applyFormulas(formData, values, details = []) {
|
|
165
|
+
const cfg = formData?.raw?.linkageCfg?.formulaCfg ?? formData?.linkageCfg?.formulaCfg;
|
|
166
|
+
const formulas = Array.isArray(cfg?.formulas) ? cfg.formulas : [];
|
|
167
|
+
const appliedMap = new Map();
|
|
168
|
+
const skipped = [];
|
|
169
|
+
if (formulas.length === 0)
|
|
170
|
+
return { applied: [], skipped };
|
|
171
|
+
|
|
172
|
+
// 公式可能互相引用,最多迭代 3 轮直到稳定
|
|
173
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
174
|
+
let changed = false;
|
|
175
|
+
for (const f of formulas) {
|
|
176
|
+
const destList = parseMaybeJson(f.destfield, []);
|
|
177
|
+
const textList = parseMaybeJson(f.formulatxt, []);
|
|
178
|
+
const dests = Array.isArray(destList) ? destList : [destList];
|
|
179
|
+
const texts = Array.isArray(textList) ? textList : [textList];
|
|
180
|
+
const count = Math.max(dests.length, texts.length);
|
|
181
|
+
for (let i = 0; i < count; i++) {
|
|
182
|
+
const dest = String(dests[i] ?? '').trim();
|
|
183
|
+
const formula = String(texts[i] ?? '').trim();
|
|
184
|
+
if (!dest || !formula)
|
|
185
|
+
continue;
|
|
186
|
+
const dot = dest.indexOf('.');
|
|
187
|
+
if (dot < 0)
|
|
188
|
+
continue;
|
|
189
|
+
const table = dest.slice(0, dot);
|
|
190
|
+
const destId = dest.slice(dot + 1);
|
|
191
|
+
if (!destId)
|
|
192
|
+
continue;
|
|
193
|
+
let rows;
|
|
194
|
+
if (table === 'main') {
|
|
195
|
+
rows = [null];
|
|
196
|
+
}
|
|
197
|
+
else if (table.startsWith('detail_')) {
|
|
198
|
+
const detailIndex = Number(table.slice('detail_'.length)) - 1;
|
|
199
|
+
rows = details.find((d) => Number(d.index) === detailIndex)?.rows ?? [];
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
skipped.push({ dest, formula, reason: `不支持的表格: ${table}` });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
206
|
+
const row = rows[rowIndex];
|
|
207
|
+
try {
|
|
208
|
+
const result = formatNumber(evaluate(formula, {
|
|
209
|
+
resolve: (ref) => {
|
|
210
|
+
const refDot = ref.indexOf('.');
|
|
211
|
+
if (refDot < 0)
|
|
212
|
+
throw new Error(`不支持的引用: [${ref}]`);
|
|
213
|
+
const refTable = ref.slice(0, refDot);
|
|
214
|
+
const refId = ref.slice(refDot + 1);
|
|
215
|
+
if (refTable === 'main')
|
|
216
|
+
return toNumber(getValue(values, refId));
|
|
217
|
+
if (refTable.startsWith('detail_')) {
|
|
218
|
+
if (!row || refTable !== table)
|
|
219
|
+
throw new Error(`跨表引用暂不支持: [${ref}]`);
|
|
220
|
+
return toNumber(getValue(row, refId));
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`不支持的引用: [${ref}]`);
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
const oldValue = table === 'main' ? getValue(values, destId) : getValue(row, destId);
|
|
226
|
+
if (String(oldValue ?? '') !== result) {
|
|
227
|
+
if (table === 'main')
|
|
228
|
+
setValue(values, destId, result);
|
|
229
|
+
else
|
|
230
|
+
setValue(row, destId, result);
|
|
231
|
+
changed = true;
|
|
232
|
+
}
|
|
233
|
+
const key = `${table}.${destId}#${table === 'main' ? -1 : rowIndex}`;
|
|
234
|
+
appliedMap.set(key, { dest, formula, value: result, table, row: table === 'main' ? -1 : rowIndex });
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
skipped.push({ dest, formula, reason: e?.message ?? String(e) });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!changed)
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
return { applied: [...appliedMap.values()], skipped };
|
|
246
|
+
}
|
package/dist/hrm.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { createApiFromProfile } from './client.js';
|
|
22
22
|
import { extractDataKey, extractRows, extractColumns, extractTotal, pickByPaths, stripHtml } from './parse.js';
|
|
23
|
+
import { mapLimit } from './util.js';
|
|
24
|
+
|
|
25
|
+
/** 组织树并发上限:兄弟部门/子树并行拉取,避免逐节点排队;同时防打满服务端。 */
|
|
26
|
+
const ORG_TREE_CONCURRENCY = 4;
|
|
23
27
|
|
|
24
28
|
// ─── Tab 定义 ────────────────────────────────────────────
|
|
25
29
|
|
|
@@ -394,19 +398,24 @@ export async function findHrmOrgNode(cfg, keyword, { matchType = null, maxDepth
|
|
|
394
398
|
if (!kw)
|
|
395
399
|
return null;
|
|
396
400
|
const top = await getHrmSearchTree(cfg, { type: '0', id: '0', isVirtual: '0' });
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
401
|
+
// 逐层 BFS:同层各节点互不依赖,并发受限拉取子节点;匹配顺序仍按层序。
|
|
402
|
+
let level = top.datas.map((d) => ({ ...d, depth: 0 }));
|
|
403
|
+
while (level.length) {
|
|
404
|
+
const hit = level.find((node) => node.name === kw && (!matchType || node.type === matchType));
|
|
405
|
+
if (hit)
|
|
406
|
+
return hit;
|
|
407
|
+
const parents = level.filter((node) => node.isParent && node.depth < maxDepth);
|
|
408
|
+
if (!parents.length)
|
|
409
|
+
break;
|
|
410
|
+
const children = await mapLimit(parents, ORG_TREE_CONCURRENCY, async (node) => {
|
|
411
|
+
try {
|
|
412
|
+
return await getHrmSearchTree(cfg, { type: node.type, id: node.id, isVirtual: node.isVirtual });
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return null; // 子树拉取失败跳过该分支(同名节点仍可能在其他分支命中)
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
level = children.flatMap((r, i) => r ? r.datas.map((c) => ({ ...c, depth: parents[i].depth + 1 })) : []);
|
|
410
419
|
}
|
|
411
420
|
return null;
|
|
412
421
|
}
|
|
@@ -423,19 +432,24 @@ export async function expandHrmOrgTree(cfg, { type = '0', id = '0', depth = 1, m
|
|
|
423
432
|
if (levels < 1)
|
|
424
433
|
return [];
|
|
425
434
|
const r = await getHrmSearchTree(cfg, { type: t, id: i });
|
|
426
|
-
|
|
435
|
+
// 先按数组顺序占用节点预算,再并发展开各子树——输出顺序与串行版一致。
|
|
436
|
+
const allowed = [];
|
|
427
437
|
for (const n of r.datas) {
|
|
428
438
|
if (count >= maxNodes) {
|
|
429
439
|
truncated = true;
|
|
430
440
|
break;
|
|
431
441
|
}
|
|
432
442
|
count++;
|
|
433
|
-
|
|
434
|
-
if (n.isParent && levels > 1)
|
|
435
|
-
node.children = await walk(n.type, n.id, levels - 1);
|
|
436
|
-
out.push(node);
|
|
443
|
+
allowed.push(n);
|
|
437
444
|
}
|
|
438
|
-
|
|
445
|
+
const children = await mapLimit(allowed, ORG_TREE_CONCURRENCY, async (n) => {
|
|
446
|
+
if (n.isParent && levels > 1)
|
|
447
|
+
return walk(n.type, n.id, levels - 1);
|
|
448
|
+
return [];
|
|
449
|
+
});
|
|
450
|
+
return allowed.map((n, idx) => ({
|
|
451
|
+
id: n.id, name: n.name, type: n.type, isParent: n.isParent, children: children[idx],
|
|
452
|
+
}));
|
|
439
453
|
}
|
|
440
454
|
const nodes = await walk(type, id, depth);
|
|
441
455
|
return { nodes, total: count, truncated };
|