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/client.js CHANGED
@@ -171,7 +171,7 @@ export class E9Api {
171
171
  }
172
172
  }
173
173
  if (!er)
174
- throw new Error(`请求失败:${method} ${url}。服务器可能不可达或网络不通。`);
174
+ throw new Error(`请求失败:${method} ${url}。服务器可能不可达或网络不通;若该服务器仅支持 TLS 1.3,请设置环境变量 E9_TLS_MAX=1.3 后重试。`);
175
175
  return er;
176
176
  }
177
177
  /**
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 p = cubeCachePath(cfg);
1405
- try {
1406
- if (!fs.existsSync(p))
1407
- return null;
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 p = cubeCachePath(cfg);
1420
- try {
1421
- fs.mkdirSync(path.dirname(p), { recursive: true });
1422
- const baseUrl = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
1423
- const raw = {
1424
- envkey: profileNameFromBaseUrl(baseUrl),
1425
- baseUrl,
1426
- fetchedAt: new Date().toISOString(),
1427
- source,
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 && !isCacheStale(cache) && Array.isArray(cache.entries) && cache.entries.length > 0) {
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
  }
@@ -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
- const queue = top.datas.map((d) => ({ ...d, depth: 0 }));
398
- while (queue.length) {
399
- const node = queue.shift();
400
- if (node.name === kw && (!matchType || node.type === matchType))
401
- return node;
402
- if (!node.isParent || node.depth >= maxDepth)
403
- continue;
404
- try {
405
- const children = await getHrmSearchTree(cfg, { type: node.type, id: node.id, isVirtual: node.isVirtual });
406
- for (const c of children.datas)
407
- queue.push({ ...c, depth: node.depth + 1 });
408
- }
409
- catch { /* 子树拉取失败跳过该分支(同名节点仍可能在后继队列命中) */ }
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
- const out = [];
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
- const node = { id: n.id, name: n.name, type: n.type, isParent: n.isParent, children: [] };
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
- return out;
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 };