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/blog.js
CHANGED
|
@@ -85,12 +85,14 @@ export function isBlogRecalled(item) {
|
|
|
85
85
|
// ─── 微博首页列表(支持组织视图)─────────────────────────
|
|
86
86
|
/**
|
|
87
87
|
* 获取微博首页列表,支持按 scope 查看不同范围。
|
|
88
|
-
* scope
|
|
89
|
-
*
|
|
88
|
+
* scope 映射(groupId,抓包实证不同 scope 走不同接口):
|
|
89
|
+
* 全部关注/关注/下级(-1)、特别关注(-5) → getBlogHomepageList
|
|
90
|
+
* 未读(-2) → getMyBlogHomepageNew
|
|
91
|
+
* 分享给我/共享(-3) → getWeiboPageShareToMeList
|
|
90
92
|
*/
|
|
91
93
|
export async function getBlogHomepageList(cfg, opts = {}) {
|
|
92
94
|
const api = await createApiFromProfile(cfg);
|
|
93
|
-
const scopeMap = { '全部': '-1', '关注': '-2', '共享': '-3', '
|
|
95
|
+
const scopeMap = { '全部': '-1', '全部关注': '-1', '关注': '-1', '下级': '-1', '未读': '-2', '共享': '-3', '分享给我': '-3', '特别关注': '-5', 'all': '-1', 'attention': '-1', 'subordinate': '-1', 'sub': '-1', 'unread': '-2', 'share': '-3', 'special': '-5' };
|
|
94
96
|
const groupId = scopeMap[opts.scope] ?? opts.scope ?? '-1';
|
|
95
97
|
const endDate = opts.endDate ?? todayStr();
|
|
96
98
|
// 默认查最近 30 天
|
|
@@ -106,11 +108,34 @@ export async function getBlogHomepageList(cfg, opts = {}) {
|
|
|
106
108
|
currentPage: opts.page ?? 1,
|
|
107
109
|
pageSize: 10,
|
|
108
110
|
};
|
|
109
|
-
const
|
|
111
|
+
const endpoint = groupId === '-2' ? '/api/blog/base/getMyBlogHomepageNew'
|
|
112
|
+
: groupId === '-3' ? '/api/blog/base/getWeiboPageShareToMeList'
|
|
113
|
+
: '/api/blog/base/getBlogHomepageList';
|
|
114
|
+
const r = await api.get(endpoint, params);
|
|
110
115
|
const all = extractBlogItems(r.json);
|
|
111
116
|
const items = all.filter((it) => it.id || it.content);
|
|
112
117
|
return { scope: opts.scope ?? '全部', groupId, startDate, endDate, page: opts.page ?? 1, total: r.json.total ?? items.length, items, raw: r.json };
|
|
113
118
|
}
|
|
119
|
+
// ─── 分享给我的人(用户列表)─────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* 获取「分享给我」的用户列表(区别于 getWeiboPageShareToMeList 查分享给我的日报内容)。
|
|
122
|
+
* GET /api/blog/base/getShareToMeUserList?groupId=all
|
|
123
|
+
*/
|
|
124
|
+
export async function getShareToMeUserList(cfg, opts = {}) {
|
|
125
|
+
const api = await createApiFromProfile(cfg);
|
|
126
|
+
const r = await api.get('/api/blog/base/getShareToMeUserList', { groupId: opts.groupId ?? 'all' });
|
|
127
|
+
const j = r.json || {};
|
|
128
|
+
const items = (j.shareToMeUserList || []).map((u) => ({
|
|
129
|
+
userId: stringify(u.userId),
|
|
130
|
+
username: stringify(u.username),
|
|
131
|
+
deptName: stringify(u.deptName),
|
|
132
|
+
jobtitle: stringify(u.jobtitle),
|
|
133
|
+
isattention: stringify(u.isattention) === '1',
|
|
134
|
+
islower: stringify(u.islower) === '1',
|
|
135
|
+
})).filter((u) => u.userId);
|
|
136
|
+
return { count: j.shareToMeUserCount, items, raw: j };
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
// ─── 微博搜索 ────────────────────────────────────────────
|
|
115
140
|
export async function searchMyBlog(cfg, opts = {}) {
|
|
116
141
|
const api = await createApiFromProfile(cfg);
|
|
@@ -245,6 +270,7 @@ export async function getBlogWorkIndex(cfg) {
|
|
|
245
270
|
const r = await api.get('/api/blog/base/getBlogWorkIndex');
|
|
246
271
|
const j = r.json || {};
|
|
247
272
|
return {
|
|
273
|
+
schedule: { name: j.schedule?.name, title: j.schedule?.scheduleTitle, index: j.schedule?.scheduleIndex },
|
|
248
274
|
mood: { name: j.mood?.name, title: j.mood?.moodIndexTitle, index: j.mood?.moodIndex },
|
|
249
275
|
work: { name: j.work?.name, title: j.work?.workIndexTitle, index: j.work?.workIndex },
|
|
250
276
|
raw: j,
|
|
@@ -377,9 +403,13 @@ export async function getMsgRemindList(cfg) {
|
|
|
377
403
|
const j = r.json || {};
|
|
378
404
|
return {
|
|
379
405
|
items: (j.remindList || []).map((it) => ({
|
|
380
|
-
id: stringify(it.id),
|
|
381
|
-
|
|
382
|
-
|
|
406
|
+
id: stringify(it.id),
|
|
407
|
+
// 提醒内容在 msg 字段(含 HTML,需去标签);旧字段 content 仅兜底
|
|
408
|
+
content: stripHtml(it.msg) || stringify(it.content),
|
|
409
|
+
creator: stringify(it.creator ?? it.username),
|
|
410
|
+
createDate: stringify(it.createdate ?? it.createDate),
|
|
411
|
+
createTime: stringify(it.createtime),
|
|
412
|
+
type: stringify(it.remindType ?? it.remindtype ?? it.type),
|
|
383
413
|
// 关注申请专用字段(抓包实证):remindid=申请人 userid、relatedid=当前登录者、remindType=1
|
|
384
414
|
remindid: stringify(it.remindid),
|
|
385
415
|
relatedid: stringify(it.relatedid),
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { loadConfig, resolveViewcondition, endpointFor, setConfigField } from './config.js';
|
|
6
|
-
import { getWfList,
|
|
6
|
+
import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE } from './workflow.js';
|
|
7
7
|
import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
|
|
8
8
|
import { pageUrl, availablePages } from './pageopen.js';
|
|
9
9
|
import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
|
|
@@ -18,7 +18,7 @@ import { transferTodo } from './todotransfer.js';
|
|
|
18
18
|
import { forwardTodo, circulateTodo } from './todoforward.js';
|
|
19
19
|
import { ccTodo } from './todocc.js';
|
|
20
20
|
import { askIdeaTodo } from './todoaskidea.js';
|
|
21
|
-
import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind } from './blog.js';
|
|
21
|
+
import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply, attentionOpt, dealMsgRemind, getShareToMeUserList } from './blog.js';
|
|
22
22
|
import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
|
|
23
23
|
import { describeFormFields } from './wfform.js';
|
|
24
24
|
import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
|
|
@@ -28,9 +28,10 @@ import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBas
|
|
|
28
28
|
import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
|
|
29
29
|
import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from './kq.js';
|
|
30
30
|
import { collectQueryableBusiness } from './report.js';
|
|
31
|
-
import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel } from './cube.js';
|
|
31
|
+
import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel, assertEditableCardValues } from './cube.js';
|
|
32
32
|
import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
|
|
33
|
-
import { getSubordinates, assertUserIdsInScope, assertNamesInScope } from './subordinate.js';
|
|
33
|
+
import { getSubordinates, assertUserIdsInScope, assertNamesInScope, isScopeEnforced } from './subordinate.js';
|
|
34
|
+
import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds } from './hrm.js';
|
|
34
35
|
import { parseArgs, flagStr, flagBool } from './args.js';
|
|
35
36
|
import { todayStr } from './util.js';
|
|
36
37
|
import { BOX_TOP, DIVIDER } from './render.js';
|
|
@@ -60,7 +61,7 @@ function usage() {
|
|
|
60
61
|
' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
|
|
61
62
|
' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
|
|
62
63
|
' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
|
|
63
|
-
' e9 blog list [--date YYYY-MM-DD] [--scope
|
|
64
|
+
' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
|
|
64
65
|
' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
|
|
65
66
|
' e9 blog view --user-id <id> 查看某人微博主页',
|
|
66
67
|
' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
|
|
@@ -68,6 +69,7 @@ function usage() {
|
|
|
68
69
|
' e9 blog like --id <微博id> [--owner <作者userid>] [--yes] 点赞/取消点赞(默认 dry-run;跨账号加 --owner)',
|
|
69
70
|
' e9 blog follow --id <目标userid> [--yes] 发起微博关注申请(对方同意后互关可访问主页)',
|
|
70
71
|
' e9 blog agree-follow --id <申请人userid> [--yes] 同意对方的关注申请',
|
|
72
|
+
' e9 blog share-users 分享给我的人(用户列表)',
|
|
71
73
|
' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
|
|
72
74
|
' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
|
|
73
75
|
' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
|
|
@@ -144,6 +146,10 @@ function usage() {
|
|
|
144
146
|
' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
|
|
145
147
|
' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
|
|
146
148
|
' e9 subordinate list [--name <姓名>] [--all-level] [--page <n>] [--json] [--raw] [--refresh] 查询我的下属(默认直接下属;--all-level 全部层级)',
|
|
149
|
+
' e9 hrm tabs 列出人事搜索范围(全部人员/同部门/我的下属)',
|
|
150
|
+
' e9 hrm conditions [--tab <全部人员|同部门|我的下属>] 列出该范围可用搜索条件(label→domkey)',
|
|
151
|
+
' e9 hrm search [--tab <全部人员|同部门|我的下属>] [--name <姓名>] [--condition "岗位=经理;性别=男"]',
|
|
152
|
+
' [--dept <部门名|id>] [--subcompany <分部名|id>] [--all-level] [--page <n>] [--json] [--raw] 人事搜索(姓名=详情卡片超链接)',
|
|
147
153
|
' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
|
|
148
154
|
' e9 config 查看当前配置来源与状态',
|
|
149
155
|
' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
|
|
@@ -339,7 +345,7 @@ function escapeLinkText(text) {
|
|
|
339
345
|
return String(text).replace(/([\[\]()\\])/g, '\\$1');
|
|
340
346
|
}
|
|
341
347
|
/** 统一拼各域 list/search 项的 markdown 超链接;无 id/无可链目标时回退纯文本。
|
|
342
|
-
* domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'
|
|
348
|
+
* domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'
|
|
343
349
|
* text: 展示文本(调用方已计算好回退值);ctx: cube 需 {modeId,formId,customid} */
|
|
344
350
|
function hyperlinkFor(domain, item, base, text, ctx = {}) {
|
|
345
351
|
const meta = linkMetaFor(domain, item, base, ctx);
|
|
@@ -388,6 +394,12 @@ function linkMetaFor(domain, item, base, ctx = {}) {
|
|
|
388
394
|
url = `${b}/spa/workplan/static/index.html#/main/wp/myWorkPlan`;
|
|
389
395
|
urlType = url ? 'list-generic' : '';
|
|
390
396
|
break;
|
|
397
|
+
case 'hrm':
|
|
398
|
+
// 人事详情卡片(真机验证通过):SPA hash 路由 #/main/hrm/card/cardInfo/<hrmresource.id>
|
|
399
|
+
if (item?.id)
|
|
400
|
+
url = buildHrmCardUrl(b, item.id);
|
|
401
|
+
urlType = url ? 'detail' : '';
|
|
402
|
+
break;
|
|
391
403
|
}
|
|
392
404
|
return {
|
|
393
405
|
url: url || null,
|
|
@@ -1522,8 +1534,9 @@ async function runBlog(args) {
|
|
|
1522
1534
|
return;
|
|
1523
1535
|
}
|
|
1524
1536
|
console.log(`${BOX_TOP}`);
|
|
1525
|
-
console.log(`E9 微博 工作指数 & 心情指数`);
|
|
1537
|
+
console.log(`E9 微博 考勤指数 & 工作指数 & 心情指数`);
|
|
1526
1538
|
console.log(DIVIDER);
|
|
1539
|
+
console.log(` 考勤指数: ${wi.schedule.index ?? '?'} ${wi.schedule.title ?? ''}`);
|
|
1527
1540
|
console.log(` 工作指数: ${wi.work.index ?? '?'} ${wi.work.title ?? ''}`);
|
|
1528
1541
|
console.log(` 心情指数: ${wi.mood.index ?? '?'} ${wi.mood.title ?? ''}`);
|
|
1529
1542
|
console.log(DIVIDER);
|
|
@@ -1696,9 +1709,12 @@ async function runBlog(args) {
|
|
|
1696
1709
|
console.log('(无提醒)');
|
|
1697
1710
|
return;
|
|
1698
1711
|
}
|
|
1712
|
+
const TYPE_LABEL = { '1': '关注申请', '8': '系统提醒' };
|
|
1699
1713
|
for (let i = 0; i < r.items.length; i++) {
|
|
1700
1714
|
const it = r.items[i];
|
|
1701
|
-
|
|
1715
|
+
const label = TYPE_LABEL[it.remindType] ?? (it.type ? `类型${it.type}` : '提醒');
|
|
1716
|
+
const time = [it.createDate, it.createTime].filter(Boolean).join(' ');
|
|
1717
|
+
console.log(`${i + 1}. [${label}] ${it.content ?? ''}${time ? ` (${time})` : ''}`);
|
|
1702
1718
|
}
|
|
1703
1719
|
return;
|
|
1704
1720
|
}
|
|
@@ -1763,6 +1779,24 @@ async function runBlog(args) {
|
|
|
1763
1779
|
}
|
|
1764
1780
|
return;
|
|
1765
1781
|
}
|
|
1782
|
+
if (sub === 'share-users') {
|
|
1783
|
+
const r = await getShareToMeUserList(cfg);
|
|
1784
|
+
if (args.flags.json) {
|
|
1785
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
console.log(`E9 微博 分享给我的人 共 ${r.count ?? r.items.length} 人`);
|
|
1789
|
+
if (r.items.length === 0) {
|
|
1790
|
+
console.log('(无人分享给我)');
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
1794
|
+
const it = r.items[i];
|
|
1795
|
+
const meta = [it.deptName, it.jobtitle, it.isattention ? '已关注' : null].filter(Boolean).join(' | ');
|
|
1796
|
+
console.log(`${i + 1}. ${it.username ?? '(未知)'}${meta ? ` (${meta})` : ''}`);
|
|
1797
|
+
}
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1766
1800
|
if (sub === 'search-fields') {
|
|
1767
1801
|
const r = await getBlogPageSearchCondition(cfg);
|
|
1768
1802
|
if (args.flags.json) {
|
|
@@ -1794,7 +1828,7 @@ async function runBlog(args) {
|
|
|
1794
1828
|
console.log(JSON.stringify(r.template, null, 2));
|
|
1795
1829
|
return;
|
|
1796
1830
|
}
|
|
1797
|
-
console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / search-fields / template,收到: ${sub}`);
|
|
1831
|
+
console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / follow / agree-follow / user-info / reminders / apps / at-list / visitors / share-users / search-fields / template,收到: ${sub}`);
|
|
1798
1832
|
process.exit(1);
|
|
1799
1833
|
}
|
|
1800
1834
|
async function runWorkplan(args) {
|
|
@@ -3409,6 +3443,12 @@ async function runCube(args) {
|
|
|
3409
3443
|
catch {
|
|
3410
3444
|
curFields = null;
|
|
3411
3445
|
}
|
|
3446
|
+
// ★字段属性硬规则:只读字段禁止填值,编辑 dry-run 也要拦(真实提交由 submitCubeCard 兜底)。
|
|
3447
|
+
// 判据必须取「新建态」字段定义——curFields 带 billid 是查看态,isedit 恒为 false,会误伤全部字段。
|
|
3448
|
+
const defFields = (await getCardFields(cfg, { modeId, formId })).fields;
|
|
3449
|
+
assertEditableCardValues(defFields, values);
|
|
3450
|
+
if (files.length)
|
|
3451
|
+
assertEditableCardValues(defFields, Object.fromEntries(files.map((f) => [String(f.fieldId), '1'])));
|
|
3412
3452
|
// 查看态不带值时:list 反查该条记录的行值作对比兜底
|
|
3413
3453
|
if (!(curFields && curFields.fields.some((f) => f.value !== undefined && f.value !== null && f.value !== ''))) {
|
|
3414
3454
|
try {
|
|
@@ -3655,7 +3695,8 @@ async function runCube(args) {
|
|
|
3655
3695
|
if (condRes.unresolved.length)
|
|
3656
3696
|
printConditionUnresolved(condRes.unresolved);
|
|
3657
3697
|
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3658
|
-
|
|
3698
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3699
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3659
3700
|
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3660
3701
|
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3661
3702
|
if (condRes.personConditions?.length)
|
|
@@ -3683,7 +3724,8 @@ async function runCube(args) {
|
|
|
3683
3724
|
if (condRes.unresolved.length)
|
|
3684
3725
|
printConditionUnresolved(condRes.unresolved);
|
|
3685
3726
|
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3686
|
-
|
|
3727
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3728
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3687
3729
|
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3688
3730
|
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3689
3731
|
if (condRes.personConditions?.length)
|
|
@@ -3759,6 +3801,152 @@ async function runSubordinate(args) {
|
|
|
3759
3801
|
console.log(` ${meta}`);
|
|
3760
3802
|
}
|
|
3761
3803
|
}
|
|
3804
|
+
|
|
3805
|
+
// ─── 组织人事(HRM)搜索 ─────────────────────────────────
|
|
3806
|
+
|
|
3807
|
+
/** 组织节点名称 → id(用于 --dept/--subcompany 名称定位;纯数字原样直传)。 */
|
|
3808
|
+
async function resolveHrmOrgValue(cfg, keyword, { matchType, flag }) {
|
|
3809
|
+
const node = await findHrmOrgNode(cfg, keyword, { matchType });
|
|
3810
|
+
if (!node)
|
|
3811
|
+
throw new Error(`${flag} 无法在组织树中定位「${keyword}」(type=${matchType})。可先用数字 ID 直传,或确认名称在人事组织树中存在。`);
|
|
3812
|
+
return node.id;
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
/** 未识别条件的提示(含相近候选),与 cube 的 printConditionUnresolved 一致——不给静默放空。 */
|
|
3816
|
+
function printHrmConditionUnresolved(unresolved) {
|
|
3817
|
+
for (const u of unresolved) {
|
|
3818
|
+
const cands = (u.candidates ?? []).join('、');
|
|
3819
|
+
console.error(`⚠ 人事条件「${u.label}」未匹配到搜索条件字段${cands ? `。相近字段候选: ${cands}` : ''}(可用 e9 hrm conditions 查看全部可用条件名)。`);
|
|
3820
|
+
}
|
|
3821
|
+
process.exit(1);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
async function runHrm(args) {
|
|
3825
|
+
const sub = args._[1] ?? 'search';
|
|
3826
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
3827
|
+
if (sub === 'tabs') {
|
|
3828
|
+
const r = await getHrmTabs(cfg);
|
|
3829
|
+
if (args.flags.json) {
|
|
3830
|
+
console.log(JSON.stringify({ tabs: r.tabs }, null, 2));
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
console.log('E9 人事搜索范围:');
|
|
3834
|
+
if (r.tabs.length === 0) {
|
|
3835
|
+
console.log('(无可用范围,可能无组织人事查看权限)');
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
for (const t of r.tabs)
|
|
3839
|
+
console.log(` ${t.title} (${t.typeid})`);
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
if (sub === 'conditions') {
|
|
3843
|
+
const tab = flagStr(args, 'tab') ?? '全部人员';
|
|
3844
|
+
const mouldid = resolveHrmTab(tab);
|
|
3845
|
+
const r = await getHrmConditions(cfg, { mouldid });
|
|
3846
|
+
if (args.flags.json) {
|
|
3847
|
+
console.log(JSON.stringify({ tab, mouldid, groups: r.groups }, null, 2));
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
console.log(`E9 人事搜索条件 [${tab}]:`);
|
|
3851
|
+
if (r.groups.length === 0) {
|
|
3852
|
+
console.log('(无可用条件)');
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
for (const g of r.groups) {
|
|
3856
|
+
console.log(` ${g.title}:`);
|
|
3857
|
+
for (const it of g.items)
|
|
3858
|
+
console.log(` - ${it.label} (${it.domkey}${it.conditionType ? `, ${it.conditionType}` : ''})`);
|
|
3859
|
+
}
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (sub !== 'search') {
|
|
3863
|
+
console.error(`hrm 子命令: tabs/conditions/search,收到: ${sub}`);
|
|
3864
|
+
process.exit(1);
|
|
3865
|
+
}
|
|
3866
|
+
const tab = flagStr(args, 'tab') ?? '全部人员';
|
|
3867
|
+
const tabkey = resolveHrmTab(tab);
|
|
3868
|
+
const page = Number(flagStr(args, 'page') ?? '1') || 1;
|
|
3869
|
+
const allLevel = flagBool(args, 'all-level');
|
|
3870
|
+
const name = flagStr(args, 'name');
|
|
3871
|
+
const dept = flagStr(args, 'dept');
|
|
3872
|
+
const subcompany = flagStr(args, 'subcompany');
|
|
3873
|
+
const spec = flagStr(args, 'condition') ?? '';
|
|
3874
|
+
|
|
3875
|
+
const values = {};
|
|
3876
|
+
// ★越权防护:按名过滤目标必须 ∈ {本人 + 全部层级下属}(与 subordinate/todo/cube 一致:默认停用 no-op)
|
|
3877
|
+
if (name) {
|
|
3878
|
+
await assertNamesInScope(cfg, [name], { domain: 'hrm' });
|
|
3879
|
+
values.resourcename = name;
|
|
3880
|
+
}
|
|
3881
|
+
// --dept/--subcompany:数字直传 domkey;名称走组织树 BFS 定位(部门 type 2 / 分部 type 1)
|
|
3882
|
+
if (dept)
|
|
3883
|
+
values.departmentid = /^\d+$/.test(dept) ? dept : await resolveHrmOrgValue(cfg, dept, { matchType: '2', flag: '--dept' });
|
|
3884
|
+
if (subcompany)
|
|
3885
|
+
values.subcompanyid1 = /^\d+$/.test(subcompany) ? subcompany : await resolveHrmOrgValue(cfg, subcompany, { matchType: '1', flag: '--subcompany' });
|
|
3886
|
+
// --condition 中文条件(label→domkey 自动映射)
|
|
3887
|
+
const condRes = await resolveHrmCondition(cfg, { mouldid: tabkey, condition: spec });
|
|
3888
|
+
if (condRes.unresolved.length)
|
|
3889
|
+
printHrmConditionUnresolved(condRes.unresolved);
|
|
3890
|
+
// ★越权防护(fail-closed):无法确认是否按人过滤的条件 → 硬拒绝;策略未启用(默认)不拦,仅保留代码路径
|
|
3891
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3892
|
+
throw new Error('拒绝越权查询:人事搜索条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3893
|
+
// ★越权防护:按人条件(姓名/直接上级/助理)目标必须 ∈ {本人 + 全部层级下属}
|
|
3894
|
+
// assertNamesInScope 同时返回解析后的 uid(blog at-list 硬规则唯一入口)。
|
|
3895
|
+
// ★E9 实测校准(2026-09-20 e9pc):姓名=INPUT 认姓名文本;直接上级/助理=BROWSER 人字段认 **hrmresource id**
|
|
3896
|
+
// (=userid,本人 7111 验证 manager=7111 → 9 个直接下属;manager=杨文元sh → 0)。故 BROWSER 人字段值回填为 uid。
|
|
3897
|
+
if (condRes.personConditions?.length) {
|
|
3898
|
+
const resolvedIds = await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'hrm' });
|
|
3899
|
+
condRes.conditionValues = applyResolvedPersonIds(condRes.conditionValues, condRes.personConditions, resolvedIds);
|
|
3900
|
+
}
|
|
3901
|
+
Object.assign(values, condRes.conditionValues);
|
|
3902
|
+
// ★E9 实测校准:--condition 里的 部门/分部 已由 resolveHrmCondition 重映射为 departmentid/subcompanyid1
|
|
3903
|
+
// (条件定义 domkey=department/subcompany 不是搜索接口认的参数);名称值在此经组织树解析为 id(同 --dept/--subcompany 路径)。
|
|
3904
|
+
for (const [key, matchType, label] of [['departmentid', '2', '部门'], ['subcompanyid1', '1', '分部']]) {
|
|
3905
|
+
const raw = values[key];
|
|
3906
|
+
if (raw != null && raw !== '' && !/^\d+$/.test(String(raw)))
|
|
3907
|
+
values[key] = await resolveHrmOrgValue(cfg, String(raw), { matchType, flag: `--condition "${label}=${raw}"` });
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
const r = await hrmList(cfg, { tabkey, values, showAllLevel: allLevel, page });
|
|
3911
|
+
const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
3912
|
+
if (args.flags.raw) {
|
|
3913
|
+
console.error('===== RAW getHrmSearchResult =====');
|
|
3914
|
+
console.error(JSON.stringify(r.raw.search, null, 2));
|
|
3915
|
+
console.error('===== RAW datas =====');
|
|
3916
|
+
console.error(JSON.stringify(r.raw.datas, null, 2));
|
|
3917
|
+
console.error('===== RAW counts =====');
|
|
3918
|
+
console.error(JSON.stringify(r.raw.counts, null, 2));
|
|
3919
|
+
console.error('===== 解析结果 =====');
|
|
3920
|
+
}
|
|
3921
|
+
if (args.flags.json) {
|
|
3922
|
+
console.log(JSON.stringify({
|
|
3923
|
+
tab, tabkey, page,
|
|
3924
|
+
pageSize: r.pageSize, total: r.total, count: r.count,
|
|
3925
|
+
sessionkey: r.sessionkey,
|
|
3926
|
+
columns: r.columns.map((c) => ({ dataIndex: c.dataIndex, title: c.title })),
|
|
3927
|
+
items: r.items.map((it) => withLink('hrm', { id: it.id, name: it.name, fields: it.fields.reduce((acc, f) => { acc[f.dataIndex] = f.value; return acc; }, {}) }, base)),
|
|
3928
|
+
}, null, 2));
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
console.log(`E9 人事 [${tab}]${allLevel ? '(含子部门)' : ''} 第 ${page} 页${r.pageSize != null ? ` · 每页 ${r.pageSize} 条` : ''}${r.total != null ? ` / 共 ${r.total} 人` : ''} 当前 ${r.count} 人`);
|
|
3932
|
+
if (r.count === 0) {
|
|
3933
|
+
console.log('(无匹配人员)');
|
|
3934
|
+
return;
|
|
3935
|
+
}
|
|
3936
|
+
const cols = (r.columns || []).filter((c) => c.dataIndex && !['id', 'randomField0', 'randomFieldId'].includes(c.dataIndex));
|
|
3937
|
+
if (cols.length)
|
|
3938
|
+
console.log(`字段(${cols.length}): ${cols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
|
|
3939
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
3940
|
+
const it = r.items[i];
|
|
3941
|
+
const head = hyperlinkFor('hrm', it, base, it.name || '(无姓名)');
|
|
3942
|
+
console.log(`${i + 1}. ${head} [id=${it.id || ''}]`);
|
|
3943
|
+
// 行 meta 去掉姓名(head 已显示)与 id 类列,避免重复
|
|
3944
|
+
const meta = it.fields.filter((f) => !['lastname', 'id', 'randomField0', 'randomFieldId'].includes(f.dataIndex)).slice(0, 8).map((f) => `${f.title}=${f.value}`).join(' | ');
|
|
3945
|
+
if (meta)
|
|
3946
|
+
console.log(` ${meta}`);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3762
3950
|
function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
|
|
3763
3951
|
const visible = items.slice(0, max);
|
|
3764
3952
|
for (let i = 0; i < visible.length; i++) {
|
|
@@ -3848,6 +4036,7 @@ async function runReport(args) {
|
|
|
3848
4036
|
console.log(`- 未查询成功的来源:${report.failures.map((item) => item.index).join(', ')}`);
|
|
3849
4037
|
}
|
|
3850
4038
|
|
|
4039
|
+
|
|
3851
4040
|
async function main() {
|
|
3852
4041
|
const args = parseArgs(process.argv.slice(2));
|
|
3853
4042
|
if (args.flags.help || args.flags.h) {
|
|
@@ -3857,7 +4046,7 @@ async function main() {
|
|
|
3857
4046
|
const cmd = args._[0] ?? 'todo';
|
|
3858
4047
|
if (cmd === 'version') {
|
|
3859
4048
|
const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
|
|
3860
|
-
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.
|
|
4049
|
+
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.3', buildHash }, null, 2));
|
|
3861
4050
|
return;
|
|
3862
4051
|
}
|
|
3863
4052
|
if (cmd === 'whoami') {
|
|
@@ -3916,6 +4105,10 @@ async function main() {
|
|
|
3916
4105
|
await runSubordinate(args);
|
|
3917
4106
|
return;
|
|
3918
4107
|
}
|
|
4108
|
+
if (cmd === 'hrm') {
|
|
4109
|
+
await runHrm(args);
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
3919
4112
|
if (cmd !== 'todo') {
|
|
3920
4113
|
console.error(`未知命令: ${cmd}\n\n${usage()}`);
|
|
3921
4114
|
process.exit(1);
|
|
@@ -4061,33 +4254,37 @@ async function main() {
|
|
|
4061
4254
|
debug: queryBool('debug'),
|
|
4062
4255
|
excludeCreatorId,
|
|
4063
4256
|
};
|
|
4064
|
-
//
|
|
4065
|
-
const shouldGroup =
|
|
4257
|
+
// 默认按 OA 二级分类(流程中心页签)分组展示——待办/已办/我发起通用;只列有数据的分类。
|
|
4258
|
+
const shouldGroup = !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
|
|
4066
4259
|
if (shouldGroup) {
|
|
4067
|
-
const grouped = await
|
|
4260
|
+
const grouped = await getOaTabGroups(cfg, { ...listOpts, category });
|
|
4261
|
+
const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
|
|
4068
4262
|
const groupedSnapshot = {
|
|
4069
|
-
|
|
4263
|
+
category,
|
|
4070
4264
|
categoryTotals: grouped.categoryTotals,
|
|
4071
4265
|
categoryTotalSum: grouped.categoryTotalSum,
|
|
4072
4266
|
page: grouped.page,
|
|
4073
4267
|
groups: grouped.groups.map((group) => ({
|
|
4074
|
-
|
|
4268
|
+
viewcondition: group.viewcondition,
|
|
4075
4269
|
title: group.title,
|
|
4076
|
-
total: group.
|
|
4270
|
+
total: group.total,
|
|
4077
4271
|
items: group.items,
|
|
4078
4272
|
raw: group.result.raw,
|
|
4079
4273
|
})),
|
|
4080
4274
|
};
|
|
4081
|
-
const savedGroupedResult = persistSearchResult(groupedSnapshot,
|
|
4275
|
+
const savedGroupedResult = persistSearchResult(groupedSnapshot, `E9 ${scope.pageTitle}分类概览`);
|
|
4276
|
+
const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
4082
4277
|
if (args.flags.json) {
|
|
4083
|
-
|
|
4084
|
-
console.log(JSON.stringify({ page: grouped.page, actionTotal: grouped.actionTotal, categoryTotals: grouped.categoryTotals, groups: grouped.groups.map((g) => ({ key: g.key, title: g.title, total: g.result.total, items: g.items.map((item) => withLink('todo', item, base)) })) }, null, 2));
|
|
4278
|
+
console.log(JSON.stringify({ category, page: grouped.page, categoryTotals: grouped.categoryTotals, groups: grouped.groups.map((g) => ({ viewcondition: g.viewcondition, title: g.title, total: g.total, items: g.items.map((item) => withLink('todo', item, base)) })) }, null, 2));
|
|
4085
4279
|
return;
|
|
4086
4280
|
}
|
|
4087
|
-
|
|
4088
|
-
|
|
4281
|
+
if (!grouped.groups.length) {
|
|
4282
|
+
console.log(`E9 ${scope.pageTitle}:各分类均无数据。`);
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
console.log(`E9 ${scope.pageTitle}(按 OA 分类页签分组,只列有数据的分类)`);
|
|
4089
4286
|
for (const group of grouped.groups) {
|
|
4090
|
-
console.log(`\n### ${group.title}(共 ${group.
|
|
4287
|
+
console.log(`\n### ${group.title}(共 ${group.total ?? '?'} 条,第 ${group.result.page} 页显示前 10 条)`);
|
|
4091
4288
|
if (group.items.length)
|
|
4092
4289
|
printTodoItems(group.items, base);
|
|
4093
4290
|
else
|
package/dist/cube.js
CHANGED
|
@@ -1161,6 +1161,25 @@ function scanChangeValue(obj) {
|
|
|
1161
1161
|
* opts: { modeId, formId, billid?, values:{id:value}, files:[{fieldId,filePath}], trigger?, pageexpandid?, cf? }
|
|
1162
1162
|
* ⚠ 必须全程 cookie 鉴权(layoutBase + doSubmit 同 session),否则 doSubmit 返 repeat。
|
|
1163
1163
|
*/
|
|
1164
|
+
/**
|
|
1165
|
+
* ★字段属性硬规则:只读字段(isedit=false)禁止由用户/助手填值——不得绕过卡片字段属性自行填值。
|
|
1166
|
+
* 返回被违规填值的只读字段清单(空数组 = 无违规)。
|
|
1167
|
+
*/
|
|
1168
|
+
export function findNonEditableCardFields(fields, values) {
|
|
1169
|
+
return (fields ?? []).filter((f) => {
|
|
1170
|
+
if (f.isedit)
|
|
1171
|
+
return false; // 可编辑字段允许填
|
|
1172
|
+
const id = String(f.id);
|
|
1173
|
+
return values?.[id] !== undefined || values?.[`field${id}`] !== undefined;
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/** 只读字段被填值 → 硬拒绝。 */
|
|
1177
|
+
export function assertEditableCardValues(fields, values) {
|
|
1178
|
+
const bad = findNonEditableCardFields(fields, values);
|
|
1179
|
+
if (bad.length)
|
|
1180
|
+
throw new Error(`拒绝填写只读字段:${bad.map((f) => `${f.label ?? f.name ?? f.id}`).join('、')}。台账卡片只允许填写「可编辑」字段,不得绕过字段属性自行填值。`);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1164
1183
|
export async function submitCubeCard(cfg, opts = {}) {
|
|
1165
1184
|
const ep = endpointFor(cfg);
|
|
1166
1185
|
const cookie = ep.cookie;
|
|
@@ -1170,6 +1189,12 @@ export async function submitCubeCard(cfg, opts = {}) {
|
|
|
1170
1189
|
const iscreate = billid ? '0' : '1';
|
|
1171
1190
|
const cf = opts.cf ?? await getCardFields(cfg, { modeId, formId, billid });
|
|
1172
1191
|
const { layoutid, cardToken, _key, uuid, fields } = cf;
|
|
1192
|
+
// ★字段属性硬规则:只读字段禁止填值。判据取「新建态」字段定义——带 billid 时 cf 是查看态,
|
|
1193
|
+
// 查看态字段 isedit 恒为 false(全部只读),不能作为可编辑性判据。
|
|
1194
|
+
const defFields = billid ? (await getCardFields(cfg, { modeId, formId })).fields : fields;
|
|
1195
|
+
assertEditableCardValues(defFields, opts.values);
|
|
1196
|
+
if (opts.files?.length)
|
|
1197
|
+
assertEditableCardValues(defFields, Object.fromEntries(opts.files.map((f) => [String(f.fieldId), '1'])));
|
|
1173
1198
|
const t = String(Date.now());
|
|
1174
1199
|
const cq = (o = {}) => ({ type: '1', modeId, formId, guid: 'card', t, _key, uuid, billid, ...o });
|
|
1175
1200
|
// ① 卡片打开链路(cookie,同 session)
|
|
@@ -1242,6 +1267,12 @@ export async function previewCubeCard(cfg, opts = {}) {
|
|
|
1242
1267
|
const values = { ...(opts.values ?? {}) };
|
|
1243
1268
|
// 附件:只记录清单(dry-run 不上传),预览提示"--yes 时上传"
|
|
1244
1269
|
const files = (opts.files ?? []).map((f) => ({ fieldId: String(f.fieldId), filePath: f.filePath }));
|
|
1270
|
+
// ★字段属性硬规则:只读字段禁止填值——dry-run 也要拦,避免「预览通过、提交才报错」。
|
|
1271
|
+
// 判据取「新建态」字段定义(billid 时 fields 是查看态,全为只读,不能作判据)。
|
|
1272
|
+
const defFields = billid ? (await getCardFields(cfg, { modeId, formId })).fields : fields;
|
|
1273
|
+
assertEditableCardValues(defFields, opts.values);
|
|
1274
|
+
if (files.length)
|
|
1275
|
+
assertEditableCardValues(defFields, Object.fromEntries(files.map((f) => [f.fieldId, '1'])));
|
|
1245
1276
|
// 联动解析(真实接口、无写副作用;失败不阻断——resolveCubeLinkage 内部 catch 返回原 values)
|
|
1246
1277
|
let linked = [];
|
|
1247
1278
|
if (opts.trigger === undefined) {
|