weaver-e9-assistant-cli 1.1.1 → 1.1.3
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/blog.js +37 -7
- package/dist/cli.js +221 -24
- package/dist/cube.js +31 -0
- package/dist/hrm.js +448 -0
- package/dist/parse.js +15 -1
- package/dist/subordinate.js +433 -419
- package/dist/wfstart.js +34 -2
- package/dist/wfuser.js +8 -9
- package/dist/workflow.js +133 -24
- package/package.json +2 -2
package/dist/wfstart.js
CHANGED
|
@@ -431,10 +431,42 @@ async function resolveFieldLinkages(cfg, workflowId, f, merged) {
|
|
|
431
431
|
* 明细浏览框字段解析并提交 id 与 name;确认后由提交计划冻结,避免二次解析漂移。
|
|
432
432
|
* 返回 E9Response(成功通常含 requestid)。
|
|
433
433
|
*/
|
|
434
|
+
/**
|
|
435
|
+
* ★字段属性硬规则:只允许填写「可编辑」(viewattr=1) 的字段。
|
|
436
|
+
* 只读(2)/隐藏(3) 字段**禁止**由用户/助手填值——不得绕过表单字段属性自行填值。
|
|
437
|
+
* 明细表字段同样按 viewattr 判定。无属性信息(viewattr 缺失)不拦,交给后端判定,避免误伤。
|
|
438
|
+
*/
|
|
439
|
+
export function findNonEditableFields(form, values, details = []) {
|
|
440
|
+
const byId = new Map();
|
|
441
|
+
for (const table of form?.tables ?? []) {
|
|
442
|
+
for (const fld of table.fields ?? [])
|
|
443
|
+
byId.set(String(fld.id), fld);
|
|
444
|
+
}
|
|
445
|
+
const bad = [];
|
|
446
|
+
const checkId = (rawId) => {
|
|
447
|
+
const id = String(rawId ?? '').replace(/^field/, '');
|
|
448
|
+
const fld = byId.get(id);
|
|
449
|
+
if (!fld || fld.viewattr == null || fld.viewattr === 1)
|
|
450
|
+
return;
|
|
451
|
+
bad.push({ id, label: fld.label, viewattr: fld.viewattr });
|
|
452
|
+
};
|
|
453
|
+
for (const k of Object.keys(values ?? {}))
|
|
454
|
+
checkId(k);
|
|
455
|
+
for (const d of details ?? [])
|
|
456
|
+
for (const row of d?.rows ?? [])
|
|
457
|
+
for (const k of Object.keys(row ?? {}))
|
|
458
|
+
checkId(k);
|
|
459
|
+
return bad;
|
|
460
|
+
}
|
|
461
|
+
|
|
434
462
|
export async function prepareCreateSubmission(cfg, workflowId, values, requestname, details = []) {
|
|
435
463
|
const f = await describeFormFields(cfg, workflowId);
|
|
436
464
|
if (f.verifyRight === false)
|
|
437
465
|
throw new Error('无权限(verifyRight:false),跑 e9 auth token-login');
|
|
466
|
+
// ★字段属性硬规则:只读/隐藏字段禁止填值(不得绕过字段属性自行填值)
|
|
467
|
+
const badFields = findNonEditableFields(f, values, details);
|
|
468
|
+
if (badFields.length)
|
|
469
|
+
throw new Error(`拒绝填写只读/隐藏字段:${badFields.map((b) => `${b.label ?? b.id}(${VIEWATTR_LABEL[b.viewattr] ?? 'va=' + b.viewattr})`).join('、')}。表单只允许填写「可编辑」字段,不得绕过字段属性自行填值。`);
|
|
438
470
|
const name = requestname || `${f.workflowName ?? '流程'}-系统管理员-${todayStr()}`;
|
|
439
471
|
const merged = { ...values };
|
|
440
472
|
// 用户未填的人员/部门/日期字段,用当前用户信息默认补上
|
|
@@ -449,8 +481,8 @@ export async function prepareCreateSubmission(cfg, workflowId, values, requestna
|
|
|
449
481
|
const id = String(fld.id);
|
|
450
482
|
if (merged[id] !== undefined)
|
|
451
483
|
continue; // 用户已填
|
|
452
|
-
if (fld.viewattr === 2)
|
|
453
|
-
continue; //
|
|
484
|
+
if (fld.viewattr === 2 || fld.viewattr === 3)
|
|
485
|
+
continue; // 只读/隐藏字段由系统/联动控制,不补默认值(不得绕过字段属性自行填值)
|
|
454
486
|
const dv = defaultForField(fld.label, fld.htmltype, user);
|
|
455
487
|
if (dv)
|
|
456
488
|
merged[id] = dv;
|
package/dist/wfuser.js
CHANGED
|
@@ -4,18 +4,17 @@
|
|
|
4
4
|
* 接口(用户抓包确认):GET /api/hrm/login/getAccountList?__random__=<ts>
|
|
5
5
|
* aiAgentToken 鉴权。返回 data.{userid, username, deptid, deptname, subcompanyname, jobs}。
|
|
6
6
|
*/
|
|
7
|
-
import { AuthTokenSession } from './auth-token.js';
|
|
8
7
|
import { createApiFromProfile } from './client.js';
|
|
9
8
|
import { todayStr } from './util.js';
|
|
10
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* 取当前登录用户信息。GET /api/hrm/login/getAccountList?__random__=<ts>
|
|
11
|
+
* 走 E9Api(ensureValid + 双头 + 重试 + 失效逃生,见 client.js)——2026-09-20 e9pc 实测:
|
|
12
|
+
* 裸 fetch(aiAgentToken) 会 404(缺头/token 状态不同步),E9Api 通道正常返回 accountlist。
|
|
13
|
+
*/
|
|
11
14
|
export async function getCurrentUser(cfg) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
headers: { aiAgentToken: sess.token, 'X-Requested-With': 'XMLHttpRequest' },
|
|
16
|
-
signal: AbortSignal.timeout(12000),
|
|
17
|
-
});
|
|
18
|
-
const j = await r.json();
|
|
15
|
+
const api = await createApiFromProfile(cfg);
|
|
16
|
+
const r = await api.get('/api/hrm/login/getAccountList', { __random__: Date.now() });
|
|
17
|
+
const j = r?.json ?? {};
|
|
19
18
|
const d = j?.data ?? {};
|
|
20
19
|
// accountlist 里 iscurrent=1 的是当前账号;data 顶层也有当前用户字段
|
|
21
20
|
const list = Array.isArray(d.accountlist) ? d.accountlist : [];
|
package/dist/workflow.js
CHANGED
|
@@ -48,9 +48,9 @@ export const DEFAULT_FILTERS = {
|
|
|
48
48
|
/** 各分类的 viewScope/complete/menuIds/默认viewcondition(来自 HAR 抓包实跑确认)。
|
|
49
49
|
* menuIds 可由 cfg 覆盖:cfg.doneMenuIds/mineMenuIds;viewcondition 可由 opts 覆盖(子 tab)。 */
|
|
50
50
|
export const CATEGORY_SCOPE = {
|
|
51
|
-
todo: { viewScope: 'doing', complete: '0', menuIdsKey: 'menuIds', defaultViewcondition: '5' },
|
|
52
|
-
done: { viewScope: 'done', complete: '2', menuIdsKey: 'doneMenuIds', defaultMenuIds: '1,90', defaultViewcondition: '10' },
|
|
53
|
-
mine: { viewScope: 'mine', complete: '2', menuIdsKey: 'mineMenuIds', defaultMenuIds: '1,14', defaultViewcondition: '16' },
|
|
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
54
|
};
|
|
55
55
|
/** 排序字段映射表 */
|
|
56
56
|
const SORT_FIELD_MAP = {
|
|
@@ -86,19 +86,43 @@ export function parseTimeRange(range) {
|
|
|
86
86
|
return { from: parts[0], to: parts[0] };
|
|
87
87
|
return { from: parts[0] || "", to: parts[1] || "" };
|
|
88
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
|
+
}
|
|
89
109
|
|
|
90
110
|
/** 子分类 viewcondition 映射表 */
|
|
91
111
|
export const TODO_TABS = {
|
|
92
112
|
"全部": "0", "待处理": "5", "待阅": "6", "被退回": "7",
|
|
93
|
-
"
|
|
113
|
+
"被督办": "4", "未读": "1", "反馈": "2", "超时": "3", "我的关注": "37",
|
|
94
114
|
};
|
|
95
115
|
|
|
116
|
+
/** 已办页签(实测 .18 doneBaseInfo.groupinfo;旧表把 17/18 误记成「审批中/已结束」)。 */
|
|
96
117
|
export const DONE_TABS = {
|
|
97
|
-
"全部": "10", "
|
|
118
|
+
"全部": "10", "未归档": "11", "已归档": "12", "待回复": "13",
|
|
119
|
+
"未读": "14", "反馈": "15", "我的关注": "38",
|
|
98
120
|
};
|
|
99
121
|
|
|
122
|
+
/** 我发起页签(实测 .18 mineBaseInfo.groupinfo)。 */
|
|
100
123
|
export const MINE_TABS = {
|
|
101
|
-
"全部": "16", "
|
|
124
|
+
"全部": "16", "未归档": "17", "已归档": "18",
|
|
125
|
+
"未读": "19", "反馈": "20", "我的关注": "39",
|
|
102
126
|
};
|
|
103
127
|
|
|
104
128
|
export function resolveSubCategory(spec, category) {
|
|
@@ -297,6 +321,97 @@ export function mergeSmartTodoGroups(results, page = 1) {
|
|
|
297
321
|
};
|
|
298
322
|
}
|
|
299
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
|
+
|
|
300
415
|
/** 解析 doingBaseInfo.treedata 中的流程类型节点。 */
|
|
301
416
|
export function parseWorkflowTypes(tree) {
|
|
302
417
|
const out = [];
|
|
@@ -371,28 +486,22 @@ function buildFilters(opts) {
|
|
|
371
486
|
if (opts.creatorSubcom)
|
|
372
487
|
f.creatersubcompanyid = opts.creatorSubcom;
|
|
373
488
|
if (opts.createTime) {
|
|
374
|
-
const ct =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
f.createdateto = ct.to;
|
|
379
|
-
}
|
|
489
|
+
const ct = resolveDateFilter(opts.createTime);
|
|
490
|
+
f.createdateselect = ct.select;
|
|
491
|
+
f.createdatefrom = ct.from;
|
|
492
|
+
f.createdateto = ct.to;
|
|
380
493
|
}
|
|
381
494
|
if (opts.receiveTime) {
|
|
382
|
-
const rt =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
f.recievedateto = rt.to;
|
|
387
|
-
}
|
|
495
|
+
const rt = resolveDateFilter(opts.receiveTime);
|
|
496
|
+
f.recievedateselect = rt.select;
|
|
497
|
+
f.recievedatefrom = rt.from;
|
|
498
|
+
f.recievedateto = rt.to;
|
|
388
499
|
}
|
|
389
500
|
if (opts.operateTime) {
|
|
390
|
-
const ot =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
f.operatedateto = ot.to;
|
|
395
|
-
}
|
|
501
|
+
const ot = resolveDateFilter(opts.operateTime);
|
|
502
|
+
f.operatedateselect = ot.select;
|
|
503
|
+
f.operatedatefrom = ot.from;
|
|
504
|
+
f.operatedateto = ot.to;
|
|
396
505
|
}
|
|
397
506
|
if (opts.requestLevel)
|
|
398
507
|
f.requestlevel = opts.requestLevel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weaver-e9-assistant-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "泛微 e-cology E9 HTTP 能力 CLI 封装(待办、微博、新建流程、邮件、公文、日程、考勤、建模台账)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node dist/cli.js",
|
|
20
20
|
"e9": "node dist/cli.js",
|
|
21
|
-
"build": "node --check dist/cli.js && node --check dist/cube.js && node --check dist/workplan.js && node --check dist/fileutil.js && node --check dist/kq.js && node --check dist/links.js && node --check dist/report.js && node --check dist/workflow.js && node --check dist/parse.js && node --check dist/todosubmit.js && node --check dist/wfsummary.js && node --check dist/wfstart.js && node --check dist/submissionplan.js && node --check dist/submissionstate.js && node --check dist/auth-token.js && node --check dist/todobatch.js && node --check dist/pageopen.js && node --check dist/createcontext.js && node --check dist/querystate.js && node --check dist/wfops.js && node --check dist/subordinate.js",
|
|
21
|
+
"build": "node --check dist/cli.js && node --check dist/cube.js && node --check dist/workplan.js && node --check dist/fileutil.js && node --check dist/kq.js && node --check dist/links.js && node --check dist/report.js && node --check dist/workflow.js && node --check dist/parse.js && node --check dist/todosubmit.js && node --check dist/wfsummary.js && node --check dist/wfstart.js && node --check dist/submissionplan.js && node --check dist/submissionstate.js && node --check dist/auth-token.js && node --check dist/todobatch.js && node --check dist/pageopen.js && node --check dist/createcontext.js && node --check dist/querystate.js && node --check dist/wfops.js && node --check dist/subordinate.js && node --check dist/hrm.js",
|
|
22
22
|
"test": "node --test \"tests/**/*.test.js\"",
|
|
23
23
|
"prepack": "npm test",
|
|
24
24
|
"release": "node scripts/release.js",
|