weaver-e9-assistant-cli 1.0.0 → 1.1.1
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.js +18 -8
- package/dist/blog.js +65 -2
- package/dist/cli.js +332 -56
- package/dist/client.js +26 -5
- package/dist/cube.js +121 -18
- package/dist/email.js +4 -1
- package/dist/subordinate.js +419 -0
- package/dist/todobatch.js +7 -0
- package/dist/wfuser.js +37 -0
- package/dist/workplan.js +37 -0
- package/package.json +27 -25
package/dist/cli.js
CHANGED
|
@@ -18,18 +18,19 @@ 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 } 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 } 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';
|
|
25
25
|
import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus } from './auth.js';
|
|
26
26
|
import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
|
|
27
|
-
import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel } from './workplan.js';
|
|
27
|
+
import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel, exportWorkplanExcel } from './workplan.js';
|
|
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
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';
|
|
32
|
-
import { getCurrentUser } from './wfuser.js';
|
|
32
|
+
import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
|
|
33
|
+
import { getSubordinates, assertUserIdsInScope, assertNamesInScope } from './subordinate.js';
|
|
33
34
|
import { parseArgs, flagStr, flagBool } from './args.js';
|
|
34
35
|
import { todayStr } from './util.js';
|
|
35
36
|
import { BOX_TOP, DIVIDER } from './render.js';
|
|
@@ -39,6 +40,7 @@ function usage() {
|
|
|
39
40
|
'',
|
|
40
41
|
'用法:',
|
|
41
42
|
' e9 version',
|
|
43
|
+
' e9 whoami 查看当前登录人(userid/用户名/是否系统管理员 isAdmin)',
|
|
42
44
|
' e9 todo list [--tab <key>] [--page <n>] [--keyword <text>] [--exclude-current-user]',
|
|
43
45
|
' [--workflow <id>] [--creator <id>] [--creator-dept <id>] [--creator-subcom <id>]',
|
|
44
46
|
' [--create-time <range>] [--receive-time <range>] [--operate-time <range>]',
|
|
@@ -62,8 +64,10 @@ function usage() {
|
|
|
62
64
|
' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
|
|
63
65
|
' e9 blog view --user-id <id> 查看某人微博主页',
|
|
64
66
|
' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
|
|
65
|
-
' e9 blog reply --id <微博id> --content "<内容>" [--yes]
|
|
66
|
-
' e9 blog like --id <微博id> [--yes]
|
|
67
|
+
' e9 blog reply --id <微博id> --content "<内容>" [--owner <作者userid>] [--yes] 回复微博(跨账号加 --owner 直达作者)',
|
|
68
|
+
' e9 blog like --id <微博id> [--owner <作者userid>] [--yes] 点赞/取消点赞(默认 dry-run;跨账号加 --owner)',
|
|
69
|
+
' e9 blog follow --id <目标userid> [--yes] 发起微博关注申请(对方同意后互关可访问主页)',
|
|
70
|
+
' e9 blog agree-follow --id <申请人userid> [--yes] 同意对方的关注申请',
|
|
67
71
|
' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
|
|
68
72
|
' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
|
|
69
73
|
' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
|
|
@@ -113,6 +117,7 @@ function usage() {
|
|
|
113
117
|
' e9 workplan create --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm" [--description-file <路径>] 新建日程(默认 dry-run)',
|
|
114
118
|
' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
|
|
115
119
|
' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
|
|
120
|
+
' e9 workplan export [--type <list|html>] [--group <部门|分部|下级|...>] [--date <YYYY-MM-DD>] [--workplan-type <id>] [--out-dir <目录>] [--yes] 导出日程(list=日程清单 / html=日程一览表,默认 dry-run)',
|
|
116
121
|
' e9 odoc todo [--page <n>] [--subject <标题>] [--dispatch-num <发文字号>] [--type <公文种类id>] [--topic <主题分类id>] 公文待办列表(getCustomPageList table,每页 10 条)',
|
|
117
122
|
' e9 odoc count [--view] 公文待办/待阅计数(发文/收文/签报)',
|
|
118
123
|
' e9 odoc create-list [--type 1|2] 发文/收文流程列表(1=发文拟稿 2=收文登记,复用 create start 发起)',
|
|
@@ -138,6 +143,7 @@ function usage() {
|
|
|
138
143
|
' e9 cube menus [--refresh] 列全部建模台账(目录接口为主、菜单兜底,缓存7天,一次拿全 customid+mode+app)',
|
|
139
144
|
' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
|
|
140
145
|
' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
|
|
146
|
+
' e9 subordinate list [--name <姓名>] [--all-level] [--page <n>] [--json] [--raw] [--refresh] 查询我的下属(默认直接下属;--all-level 全部层级)',
|
|
141
147
|
' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
|
|
142
148
|
' e9 config 查看当前配置来源与状态',
|
|
143
149
|
' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
|
|
@@ -1308,6 +1314,8 @@ async function runBlog(args) {
|
|
|
1308
1314
|
console.error('blog view 需要 --user-id <用户id>');
|
|
1309
1315
|
process.exit(1);
|
|
1310
1316
|
}
|
|
1317
|
+
// ★越权防护:只能看本人 + 全部层级下属的微博主页,越界拒绝
|
|
1318
|
+
await assertUserIdsInScope(cfg, [userId], { domain: 'blog' });
|
|
1311
1319
|
const r = await getBlogPersonalInfo(cfg, userId);
|
|
1312
1320
|
if (args.flags.json) {
|
|
1313
1321
|
console.log(JSON.stringify(r, null, 2));
|
|
@@ -1351,16 +1359,25 @@ async function runBlog(args) {
|
|
|
1351
1359
|
process.exit(1);
|
|
1352
1360
|
}
|
|
1353
1361
|
const send = !!args.flags.yes;
|
|
1354
|
-
// 被评论人 userid(bediscussantId) + workdate:手动优先,否则从微博列表按 id
|
|
1362
|
+
// 被评论人 userid(bediscussantId) + workdate:手动优先,否则从微博列表按 id 查。
|
|
1363
|
+
// 修复 #4:--owner <作者userid> 跨账号直达(a 回复 b 的微博时作者即被评论人,无需再翻列表);
|
|
1364
|
+
// 未给 --owner 时保持原逻辑——从当前登录者自己的微博列表按 id 查找。
|
|
1355
1365
|
let discussantId = flagStr(args, 'bediscussant-id');
|
|
1356
1366
|
let workdate = flagStr(args, 'workdate');
|
|
1367
|
+
const owner = flagStr(args, 'owner');
|
|
1368
|
+
if (!discussantId && owner) {
|
|
1369
|
+
discussantId = owner;
|
|
1370
|
+
}
|
|
1371
|
+
if (!workdate && owner) {
|
|
1372
|
+
workdate = flagStr(args, 'date') ?? todayStr();
|
|
1373
|
+
}
|
|
1357
1374
|
if (!discussantId || !workdate) {
|
|
1358
1375
|
const date = flagStr(args, 'date') ?? todayStr();
|
|
1359
1376
|
// 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
|
|
1360
1377
|
const list = await getMyBlogAll(cfg, date);
|
|
1361
1378
|
const hit = (list.items || []).find(b => String(b.id) === String(blogId));
|
|
1362
1379
|
if (!hit) {
|
|
1363
|
-
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --bediscussant-id <作者userid> --workdate <YYYY-MM-DD> 手动指定,或 --date 指定微博日期。`);
|
|
1380
|
+
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --bediscussant-id <作者userid> --workdate <YYYY-MM-DD> 手动指定,或 --owner <作者userid> 跨账号直达,或 --date 指定微博日期。`);
|
|
1364
1381
|
process.exit(1);
|
|
1365
1382
|
}
|
|
1366
1383
|
discussantId = discussantId || (hit.raw?.userid ? String(hit.raw.userid) : '') || (hit.raw?.creater ? String(hit.raw.creater) : '') || (hit.userid ? String(hit.userid) : '');
|
|
@@ -1371,12 +1388,22 @@ async function runBlog(args) {
|
|
|
1371
1388
|
console.log(`[dry-run] 未发送。将回复微博 id=${r.blogId}(被评论人 userid=${discussantId}, workdate=${workdate})。确认后加 --yes。`);
|
|
1372
1389
|
return;
|
|
1373
1390
|
}
|
|
1374
|
-
console.log(`[已回复] blogId=${r.blogId} status=${r.response?.status}`);
|
|
1375
1391
|
const j = r.response?.json;
|
|
1376
1392
|
const ok = j?.status === '1' || j?.status === 1 || j?.api_status === '1';
|
|
1393
|
+
// 修复 #5:--json 输出完整响应(含 blogReplyList 每条的 replyId),不再截断 300 字符;
|
|
1394
|
+
// 同时避免前置普通输出行污染 json(--json 时先输出 JSON 再 return)。
|
|
1395
|
+
if (args.flags.json) {
|
|
1396
|
+
console.log(JSON.stringify({ blogId: r.blogId, workdate, discussantId, status: r.response?.status, ok, response: j }, null, 2));
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
console.log(`[已回复] blogId=${r.blogId} status=${r.response?.status}`);
|
|
1377
1400
|
console.log(` ${ok ? '✅ 回复成功' : '⚠ 回复失败'}`);
|
|
1378
|
-
if (j)
|
|
1379
|
-
|
|
1401
|
+
if (j) {
|
|
1402
|
+
const list = j.blogReplyList ?? j.blogReplyPoList ?? [];
|
|
1403
|
+
if (Array.isArray(list) && list.length)
|
|
1404
|
+
console.log(` 最新回复: ${list.map((it) => `replyId=${it.replyId ?? it.id}`).join(', ')}`);
|
|
1405
|
+
console.log('响应:', JSON.stringify(j).slice(0, 300), '(--json 查看完整回复列表)');
|
|
1406
|
+
}
|
|
1380
1407
|
return;
|
|
1381
1408
|
}
|
|
1382
1409
|
if (sub === 'like') {
|
|
@@ -1388,16 +1415,33 @@ async function runBlog(args) {
|
|
|
1388
1415
|
const send = !!args.flags.yes;
|
|
1389
1416
|
// 手动参数优先;否则自动从微博列表解析 workdate / 当前用户 id / 当前点赞状态。
|
|
1390
1417
|
// SPA bundle 实证 addOrDeleteZan 请求体:workdate + userId + optType(1=赞/0=取消) + discussId(微博id)。
|
|
1418
|
+
// 修复 #4:--owner <作者userid> 跨账号点赞——workdate 取 date/今天,userId 仍为当前登录者(点赞人)。
|
|
1391
1419
|
let workdate = flagStr(args, 'workdate');
|
|
1392
1420
|
let userId = flagStr(args, 'user-id');
|
|
1393
1421
|
let optType = flagStr(args, 'opt-type');
|
|
1422
|
+
const owner = flagStr(args, 'owner');
|
|
1423
|
+
const date = flagStr(args, 'date') ?? todayStr();
|
|
1424
|
+
if (owner) {
|
|
1425
|
+
workdate = workdate || date;
|
|
1426
|
+
if (!userId) {
|
|
1427
|
+
const me = await getCurrentUserInfo(cfg);
|
|
1428
|
+
userId = me.id ? String(me.id) : '';
|
|
1429
|
+
}
|
|
1430
|
+
if (!optType) {
|
|
1431
|
+
// 跨账号已赞判定:从 owner 的微博首页列表找该条,看 blogZanBean.blogZanPoList 是否含当前用户
|
|
1432
|
+
const list = await getBlogHomepageList(cfg, { blogUserId: owner, endDate: date, startDate: date });
|
|
1433
|
+
const hit = (list.items || []).find((b) => String(b.id) === String(blogId));
|
|
1434
|
+
const zanList = Array.isArray(hit?.raw?.blogZanBean?.blogZanPoList) ? hit.raw.blogZanBean.blogZanPoList : [];
|
|
1435
|
+
const alreadyLiked = zanList.some((z) => String(z.zanHrmid ?? '') === String(userId));
|
|
1436
|
+
optType = alreadyLiked ? '0' : '1';
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1394
1439
|
if (!workdate || !userId || !optType) {
|
|
1395
|
-
const date = flagStr(args, 'date') ?? todayStr();
|
|
1396
1440
|
// 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
|
|
1397
1441
|
const list = await getMyBlogAll(cfg, date);
|
|
1398
1442
|
const hit = (list.items || []).find((b) => String(b.id) === String(blogId));
|
|
1399
1443
|
if (!hit) {
|
|
1400
|
-
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --workdate <YYYY-MM-DD> --user-id <当前用户id> --opt-type <1|0> 手动指定,或 --date 指定微博日期。`);
|
|
1444
|
+
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --workdate <YYYY-MM-DD> --user-id <当前用户id> --opt-type <1|0> 手动指定,或 --owner <作者userid> 跨账号直达,或 --date 指定微博日期。`);
|
|
1401
1445
|
process.exit(1);
|
|
1402
1446
|
}
|
|
1403
1447
|
workdate = workdate || String(hit.workdate ?? '') || String(hit.createdate ?? '').slice(0, 10);
|
|
@@ -1554,6 +1598,60 @@ async function runBlog(args) {
|
|
|
1554
1598
|
console.log('响应:', JSON.stringify(r.response.json).slice(0, 400));
|
|
1555
1599
|
return;
|
|
1556
1600
|
}
|
|
1601
|
+
if (sub === 'follow') {
|
|
1602
|
+
// 发起关注。E9 微博关注需对方同意:attentionOpt(attentionStatus=3) 发申请 → 对方 dealMsgRemind(opt=1) 同意。
|
|
1603
|
+
const userId = flagStr(args, 'id');
|
|
1604
|
+
if (!userId) {
|
|
1605
|
+
console.error('blog follow 需要 --id <目标用户id>(被关注者 userid)');
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
const send = !!args.flags.yes;
|
|
1609
|
+
const r = await attentionOpt(cfg, { userId, send });
|
|
1610
|
+
if (!r.sent) {
|
|
1611
|
+
console.log(`[dry-run] 未发送。将向 userId=${r.userId} 发起关注申请(attentionStatus=${r.attentionStatus})。确认后加 --yes。`);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
console.log(`[已发送关注申请] 目标 userId=${r.userId} attentionStatus=${r.attnStatus ?? '?'}`);
|
|
1615
|
+
console.log(` ${r.ok ? '✅ 关注申请已发出(等待对方同意,互关后可访问对方微博)' : '⚠ 关注申请失败'}`);
|
|
1616
|
+
if (r.raw) {
|
|
1617
|
+
const hint = r.raw?.errorCode === '002' || r.raw?.msg?.includes('登录信息超时')
|
|
1618
|
+
? ' ⚠ cookie 已过期,请先 `e9 auth token-login` 刷新后重试。'
|
|
1619
|
+
: ' (cookie 通道接口,token 通道不生效)';
|
|
1620
|
+
if (!r.ok)
|
|
1621
|
+
console.log(` 响应: ${JSON.stringify(r.raw).slice(0, 200)}${hint}`);
|
|
1622
|
+
}
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (sub === 'agree-follow') {
|
|
1626
|
+
// 同意关注申请:getMsgRemindList 找"向你申请关注"(remindType=1) 的记录 → dealMsgRemind(id, relatedId, opt=1)。
|
|
1627
|
+
// 实测字段方向不定(remindid/relatedid 中任一可能是申请人),用宽匹配;
|
|
1628
|
+
// relatedId 抓包实证 = 当前登录者自己的 userid(sysadmin=1),与记录字段无关。
|
|
1629
|
+
const userId = flagStr(args, 'id');
|
|
1630
|
+
if (!userId) {
|
|
1631
|
+
console.error('blog agree-follow 需要 --id <申请人用户id>');
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
const send = !!args.flags.yes;
|
|
1635
|
+
const reminds = await getMsgRemindList(cfg);
|
|
1636
|
+
const req = (reminds.items || []).find((it) => it.remindType === '1' && (it.remindid === String(userId) || it.relatedid === String(userId)));
|
|
1637
|
+
if (!req) {
|
|
1638
|
+
console.error(`未在消息提醒中找到 ${userId} 的关注申请(remindType=1)。可用 \`blog reminders --json\` 查看全部提醒;若对方已取消申请则无需处理。`);
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
// 修复 #9:dealMsgRemind 的 relatedId = 申请人(发起关注者)userid,抓包实证;
|
|
1642
|
+
// 早期误用当前登录者 id 导致「同意成功但互关未建立」。userId 入参即申请人。
|
|
1643
|
+
const relatedId = String(userId);
|
|
1644
|
+
if (!send) {
|
|
1645
|
+
console.log(`[dry-run] 将同意 ${userId} 的关注申请(提醒记录 id=${req.id}, relatedId=${relatedId})。确认后加 --yes。`);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const r = await dealMsgRemind(cfg, { id: req.id, relatedId, opt: 1, send });
|
|
1649
|
+
console.log(`[已处理关注申请] 申请人 userId=${userId} 提醒 id=${r.id}`);
|
|
1650
|
+
console.log(` ${r.ok ? '✅ 已同意关注,互关完成' : '⚠ 处理失败'}`);
|
|
1651
|
+
if (r.raw && !r.ok)
|
|
1652
|
+
console.log(` 响应: ${JSON.stringify(r.raw).slice(0, 200)}`);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1557
1655
|
if (sub === 'delete-reply') {
|
|
1558
1656
|
// 删除微博评论(正文不可删,只能撤回;评论可删)。POST /api/blog/base/deleteBlogReplay。
|
|
1559
1657
|
const replyId = flagStr(args, 'id');
|
|
@@ -1696,7 +1794,7 @@ async function runBlog(args) {
|
|
|
1696
1794
|
console.log(JSON.stringify(r.template, null, 2));
|
|
1697
1795
|
return;
|
|
1698
1796
|
}
|
|
1699
|
-
console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / user-info / reminders / apps / at-list / visitors / search-fields / template,收到: ${sub}`);
|
|
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}`);
|
|
1700
1798
|
process.exit(1);
|
|
1701
1799
|
}
|
|
1702
1800
|
async function runWorkplan(args) {
|
|
@@ -1724,36 +1822,73 @@ async function runWorkplan(args) {
|
|
|
1724
1822
|
return;
|
|
1725
1823
|
}
|
|
1726
1824
|
if (sub === 'list') {
|
|
1727
|
-
// 使用 getCalendarData 替代 AllCalendar,支持组织视图
|
|
1728
1825
|
const group = flagStr(args, 'group');
|
|
1729
|
-
|
|
1826
|
+
const date = flagStr(args, 'date');
|
|
1827
|
+
const type = flagStr(args, 'type');
|
|
1828
|
+
// 默认(无 --group)=「我的日程」:查当前用户自己的日程,走 MyCalendar(selectUser 空=当前用户)。
|
|
1829
|
+
// 只有显式传 --group 才走组织视图 getCalendarData(部门/分部/下级等)。
|
|
1830
|
+
if (!group) {
|
|
1831
|
+
const selectUser = flagStr(args, 'select-user');
|
|
1832
|
+
// ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
|
|
1833
|
+
if (selectUser)
|
|
1834
|
+
await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
|
|
1835
|
+
const r = await getMyCalendar(cfg, { selectDate: date, viewType: '3', selectUser: selectUser ?? '', workPlanType: type });
|
|
1836
|
+
if (args.flags.raw) {
|
|
1837
|
+
console.error('===== RAW MyCalendar =====');
|
|
1838
|
+
console.error(JSON.stringify(r.raw, null, 2));
|
|
1839
|
+
console.error('===== 解析结果 =====');
|
|
1840
|
+
}
|
|
1841
|
+
if (args.flags.json) {
|
|
1842
|
+
console.log(JSON.stringify({
|
|
1843
|
+
selectDate: r.selectDate, count: r.items.length,
|
|
1844
|
+
items: r.items.map((it) => withLink('workplan', {
|
|
1845
|
+
id: it.id, name: it.name,
|
|
1846
|
+
beginDateTime: it.beginDateTime, endDateTime: it.endDateTime,
|
|
1847
|
+
color: it.color, typeName: it.typeName, allDay: it.allDay,
|
|
1848
|
+
}, wpBase)),
|
|
1849
|
+
}, null, 2));
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
console.log(`E9 日程 [${r.selectDate}] 共 ${r.items.length} 条`);
|
|
1853
|
+
if (r.items.length === 0) {
|
|
1854
|
+
console.log('(该区间无日程)');
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
1858
|
+
const it = r.items[i];
|
|
1859
|
+
const time = [it.beginDateTime, it.endDateTime].filter(Boolean).join(' ~ ');
|
|
1860
|
+
const allDay = it.allDay === '1' ? ' [全天]' : '';
|
|
1861
|
+
console.log(`${i + 1}. ${hyperlinkFor('workplan', it, wpBase, it.name ?? '(无名)')}${allDay}${time ? ` [${time}]` : ''} ${it.typeName ?? ''}`);
|
|
1862
|
+
}
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
// 组织视图(--group 部门/分部/下级等)
|
|
1730
1866
|
const bd = await getBaseData(cfg);
|
|
1731
1867
|
let objType = '1', objIds = bd.dept[0]?.id ?? '';
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
if (
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1868
|
+
const groupMap = { '部门': '1', '分部': '2', '人力资源': '3', '人力': '3', '所有上级': '4', '上级': '4', '直接下级': '5', '下级': '5', '常用组': '6', '我的关注': '7', '关注': '7' };
|
|
1869
|
+
const gkey = groupMap[group] || group;
|
|
1870
|
+
const sg = bd.selectGroup.find((g) => g.key === gkey);
|
|
1871
|
+
if (sg) {
|
|
1872
|
+
if (gkey === '1') {
|
|
1873
|
+
objType = '1';
|
|
1874
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
1875
|
+
}
|
|
1876
|
+
else if (gkey === '2') {
|
|
1877
|
+
objType = '2';
|
|
1878
|
+
objIds = bd.subComp[0]?.id ?? '';
|
|
1879
|
+
}
|
|
1880
|
+
else if (gkey === '3') {
|
|
1881
|
+
// 人力资源:按人员查询,默认当前登录人(即「我的日程」)。当前用户 id 从 cookie 的 loginuuids 取(导出/日历走 cookie,token 可能过期)。
|
|
1882
|
+
objType = '3';
|
|
1883
|
+
const _uid = String(cfg.cookie || '').match(/loginuuids=([^;]*)/i);
|
|
1884
|
+
objIds = _uid ? decodeURIComponent(_uid[1].trim()) : '';
|
|
1885
|
+
}
|
|
1886
|
+
else if (gkey === '5') {
|
|
1887
|
+
objType = '1';
|
|
1888
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
1749
1889
|
}
|
|
1750
1890
|
}
|
|
1751
|
-
const r = await getCalendarData(cfg, {
|
|
1752
|
-
selectDate: flagStr(args, 'date'),
|
|
1753
|
-
objType,
|
|
1754
|
-
objIds,
|
|
1755
|
-
workPlanType: flagStr(args, 'type'),
|
|
1756
|
-
});
|
|
1891
|
+
const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type });
|
|
1757
1892
|
if (args.flags.raw) {
|
|
1758
1893
|
console.error('===== RAW getCalendarData =====');
|
|
1759
1894
|
console.error(JSON.stringify(r.raw, null, 2));
|
|
@@ -1775,7 +1910,7 @@ async function runWorkplan(args) {
|
|
|
1775
1910
|
}, null, 2));
|
|
1776
1911
|
return;
|
|
1777
1912
|
}
|
|
1778
|
-
const groupLabel =
|
|
1913
|
+
const groupLabel = ` · ${group}`;
|
|
1779
1914
|
console.log(`E9 日程 [${r.selectDate}${groupLabel}] 共 ${r.count} 条`);
|
|
1780
1915
|
if (r.count === 0) {
|
|
1781
1916
|
console.log('(该区间无日程)');
|
|
@@ -1830,8 +1965,10 @@ async function runWorkplan(args) {
|
|
|
1830
1965
|
}
|
|
1831
1966
|
console.log(`[已完成] workid=${r.workid} status=${r.response?.status}`);
|
|
1832
1967
|
const j = r.response?.json;
|
|
1833
|
-
|
|
1834
|
-
|
|
1968
|
+
// 修复 #2:以 status===true 为成功标志(失败时 status===false + error 字段),
|
|
1969
|
+
// 避免「api_status:true 但 status:false」被误判成功。
|
|
1970
|
+
const ok = (j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true) && !j?.error;
|
|
1971
|
+
console.log(` ${ok ? '✅ 完成成功' : '⚠ 完成失败'}${j?.error ? `:${j.error}` : ''}`);
|
|
1835
1972
|
if (j)
|
|
1836
1973
|
console.log('响应:', JSON.stringify(j).slice(0, 300));
|
|
1837
1974
|
return;
|
|
@@ -1850,8 +1987,10 @@ async function runWorkplan(args) {
|
|
|
1850
1987
|
}
|
|
1851
1988
|
console.log(`[已删除] workid=${r.workid} status=${r.response?.status}`);
|
|
1852
1989
|
const j = r.response?.json;
|
|
1853
|
-
|
|
1854
|
-
|
|
1990
|
+
// 修复 #2:后端删除失败时返回 {api_status:true, status:false, error:"..."},
|
|
1991
|
+
// 旧判断只看 api_status 误判成功。以 status===true 且无 error 为成功。
|
|
1992
|
+
const ok = (j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true) && !j?.error;
|
|
1993
|
+
console.log(` ${ok ? '✅ 删除成功' : '⚠ 删除失败'}${j?.error ? `:${j.error}` : ''}`);
|
|
1855
1994
|
if (j)
|
|
1856
1995
|
console.log('响应:', JSON.stringify(j).slice(0, 300));
|
|
1857
1996
|
return;
|
|
@@ -1906,12 +2045,16 @@ async function runWorkplan(args) {
|
|
|
1906
2045
|
}
|
|
1907
2046
|
catch { /* 获取分组失败不阻断 */ }
|
|
1908
2047
|
}
|
|
2048
|
+
// ★越权防护:按人筛选目标必须 ∈ {本人 + 全部层级下属}
|
|
2049
|
+
const createrId = flagStr(args, 'creater');
|
|
2050
|
+
const receiveId = flagStr(args, 'receiver');
|
|
2051
|
+
await assertUserIdsInScope(cfg, [createrId, receiveId].filter(Boolean), { domain: 'workplan' });
|
|
1909
2052
|
const r = await searchWorkplan(cfg, {
|
|
1910
2053
|
name: flagStr(args, 'name'),
|
|
1911
2054
|
type: flagStr(args, 'type'),
|
|
1912
2055
|
status: flagStr(args, 'status'),
|
|
1913
|
-
createrId
|
|
1914
|
-
receiveId
|
|
2056
|
+
createrId,
|
|
2057
|
+
receiveId,
|
|
1915
2058
|
timeSag: groupFilter ?? flagStr(args, 'time-sag') ?? '0',
|
|
1916
2059
|
beginDate: flagStr(args, 'begin'),
|
|
1917
2060
|
endDate: flagStr(args, 'end'),
|
|
@@ -2006,7 +2149,11 @@ async function runWorkplan(args) {
|
|
|
2006
2149
|
return;
|
|
2007
2150
|
}
|
|
2008
2151
|
if (sub === 'my-calendar') {
|
|
2009
|
-
const
|
|
2152
|
+
const selectUser = flagStr(args, 'select-user');
|
|
2153
|
+
// ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
|
|
2154
|
+
if (selectUser)
|
|
2155
|
+
await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
|
|
2156
|
+
const r = await getMyCalendar(cfg, { selectDate: flagStr(args, 'date'), viewType: flagStr(args, 'view-type'), selectUser, workPlanType: flagStr(args, 'type') });
|
|
2010
2157
|
if (args.flags.json) {
|
|
2011
2158
|
console.log(JSON.stringify(r, null, 2));
|
|
2012
2159
|
return;
|
|
@@ -2066,7 +2213,54 @@ async function runWorkplan(args) {
|
|
|
2066
2213
|
console.log(`${BOX_TOP}`);
|
|
2067
2214
|
return;
|
|
2068
2215
|
}
|
|
2069
|
-
|
|
2216
|
+
if (sub === 'export') {
|
|
2217
|
+
const type = flagStr(args, 'type') ?? 'list';
|
|
2218
|
+
if (type !== 'list' && type !== 'html') {
|
|
2219
|
+
console.error('workplan export 的 --type 只支持 list(日程清单)或 html(日程一览表)');
|
|
2220
|
+
process.exit(1);
|
|
2221
|
+
}
|
|
2222
|
+
const group = flagStr(args, 'group') ?? '部门';
|
|
2223
|
+
const date = flagStr(args, 'date');
|
|
2224
|
+
const bd = await getBaseData(cfg);
|
|
2225
|
+
let objType = '1', objIds = bd.dept[0]?.id ?? '';
|
|
2226
|
+
const groupMap = { '部门': '1', '分部': '2', '人力资源': '3', '人力': '3', '所有上级': '4', '上级': '4', '直接下级': '5', '下级': '5', '常用组': '6', '我的关注': '7', '关注': '7' };
|
|
2227
|
+
const gkey = groupMap[group] || group;
|
|
2228
|
+
const sg = bd.selectGroup.find((g) => g.key === gkey);
|
|
2229
|
+
if (sg) {
|
|
2230
|
+
if (gkey === '1') {
|
|
2231
|
+
objType = '1';
|
|
2232
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
2233
|
+
}
|
|
2234
|
+
else if (gkey === '2') {
|
|
2235
|
+
objType = '2';
|
|
2236
|
+
objIds = bd.subComp[0]?.id ?? '';
|
|
2237
|
+
}
|
|
2238
|
+
else if (gkey === '3') {
|
|
2239
|
+
// 人力资源:按人员查询,默认当前登录人(即「我的日程」)。当前用户 id 从 cookie 的 loginuuids 取(导出/日历走 cookie,token 可能过期)。
|
|
2240
|
+
objType = '3';
|
|
2241
|
+
const _uid = String(cfg.cookie || '').match(/loginuuids=([^;]*)/i);
|
|
2242
|
+
objIds = _uid ? decodeURIComponent(_uid[1].trim()) : '';
|
|
2243
|
+
}
|
|
2244
|
+
else if (gkey === '5') {
|
|
2245
|
+
objType = '1';
|
|
2246
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const label = type === 'html' ? '日程一览表' : '日程清单';
|
|
2250
|
+
if (!args.flags.yes) {
|
|
2251
|
+
console.log(`[dry-run] 将导出${label}:${date ?? '今天'} · ${group}(objType=${objType} objIds=${objIds})`);
|
|
2252
|
+
console.log('确认无误后加 --yes 真正导出并下载。');
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
const r = await exportWorkplanExcel(cfg, {
|
|
2256
|
+
type, selectDate: date, objType, objIds,
|
|
2257
|
+
workPlanType: flagStr(args, 'workplan-type'),
|
|
2258
|
+
outDir: flagStr(args, 'out-dir'),
|
|
2259
|
+
});
|
|
2260
|
+
console.log(`✅ 已导出${label}:${r.outPath}`);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
console.error(`workplan 暂只支持 list / view / finish / delete / types / search-conditions / search / create / my-calendar / attention / base-data / export / import-template / import,收到: ${sub}`);
|
|
2070
2264
|
process.exit(1);
|
|
2071
2265
|
}
|
|
2072
2266
|
async function runOdoc(args) {
|
|
@@ -2139,6 +2333,10 @@ async function runOdoc(args) {
|
|
|
2139
2333
|
console.error(`odoc 暂只支持 todo / count / create-list,收到: ${sub}`);
|
|
2140
2334
|
process.exit(1);
|
|
2141
2335
|
}
|
|
2336
|
+
// ★越权防护:公文列表按人过滤(签发人/拟稿操作人/在办人/未办人)目标必须 ∈ {本人 + 全部层级下属}
|
|
2337
|
+
const odocPersonIds = [flagStr(args, 'issued-userid'), flagStr(args, 'owner-id'), flagStr(args, 'dealing-userid'), flagStr(args, 'undealed-userid')].filter(Boolean);
|
|
2338
|
+
if (odocPersonIds.length)
|
|
2339
|
+
await assertUserIdsInScope(cfg, odocPersonIds, { domain: 'odoc' });
|
|
2142
2340
|
const r = await listOdocTodo(cfg, {
|
|
2143
2341
|
requestName: flagStr(args, 'subject'),
|
|
2144
2342
|
dispatchNum: flagStr(args, 'dispatch-num'),
|
|
@@ -3456,6 +3654,12 @@ async function runCube(args) {
|
|
|
3456
3654
|
const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
|
|
3457
3655
|
if (condRes.unresolved.length)
|
|
3458
3656
|
printConditionUnresolved(condRes.unresolved);
|
|
3657
|
+
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3658
|
+
if (condRes.unverifiablePerson)
|
|
3659
|
+
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3660
|
+
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3661
|
+
if (condRes.personConditions?.length)
|
|
3662
|
+
await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'cube', customid });
|
|
3459
3663
|
const r = await exportCubeExcel(cfg, { customid, filename, yes: !!args.flags.yes, outDir: flagStr(args, 'out-dir'), modeCustomidPlain: !!args.flags.plain, conditionValues: condRes.conditionValues, keyword: flagStr(args, 'keyword') ?? '' });
|
|
3460
3664
|
if (args.flags.json) {
|
|
3461
3665
|
console.log(JSON.stringify({ customid, dataKey: r.dataKey, filename: r.filename, downloaded: r.downloaded, outPath: r.outPath, size: r.size, sourceTotal: r.sourceTotal, validation: r.validation, expBody: r.expBody }, null, 2));
|
|
@@ -3478,6 +3682,12 @@ async function runCube(args) {
|
|
|
3478
3682
|
const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
|
|
3479
3683
|
if (condRes.unresolved.length)
|
|
3480
3684
|
printConditionUnresolved(condRes.unresolved);
|
|
3685
|
+
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3686
|
+
if (condRes.unverifiablePerson)
|
|
3687
|
+
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3688
|
+
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3689
|
+
if (condRes.personConditions?.length)
|
|
3690
|
+
await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'cube', customid });
|
|
3481
3691
|
const conditionValues = condRes.conditionValues;
|
|
3482
3692
|
const r = await listCube(cfg, { customid, page: Number(flagStr(args, 'page') ?? '1') || 1, keyword: flagStr(args, 'keyword') ?? '', conditionValues });
|
|
3483
3693
|
if (args.flags.json) {
|
|
@@ -3507,6 +3717,48 @@ async function runCube(args) {
|
|
|
3507
3717
|
console.log(` ${meta}`);
|
|
3508
3718
|
}
|
|
3509
3719
|
}
|
|
3720
|
+
async function runSubordinate(args) {
|
|
3721
|
+
const sub = args._[1] ?? 'list';
|
|
3722
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
3723
|
+
if (sub !== 'list') {
|
|
3724
|
+
console.error('subordinate 只支持 list。');
|
|
3725
|
+
process.exit(1);
|
|
3726
|
+
}
|
|
3727
|
+
const name = flagStr(args, 'name');
|
|
3728
|
+
const allLevel = flagBool(args, 'all-level');
|
|
3729
|
+
const page = Number(flagStr(args, 'page') ?? '1') || 1;
|
|
3730
|
+
// 姓名目标 → 越权校验(恰 1 个在授权范围内才放行,否则拒绝);无 --name = 全部直属下属(本人天然在范围)
|
|
3731
|
+
const lastname = name ? await assertNamesInScope(cfg, [name], { domain: 'subordinate' }) : '';
|
|
3732
|
+
const r = await getSubordinates(cfg, { lastname, allLevel, page });
|
|
3733
|
+
if (args.flags.raw) {
|
|
3734
|
+
console.error('===== RAW getSearchResult =====');
|
|
3735
|
+
console.error(JSON.stringify(r.raw.getSearchResult, null, 2));
|
|
3736
|
+
console.error('===== RAW datas =====');
|
|
3737
|
+
console.error(JSON.stringify(r.raw.datas, null, 2));
|
|
3738
|
+
console.error('===== RAW counts =====');
|
|
3739
|
+
console.error(JSON.stringify(r.raw.counts, null, 2));
|
|
3740
|
+
console.error('===== 解析结果 =====');
|
|
3741
|
+
}
|
|
3742
|
+
if (args.flags.json) {
|
|
3743
|
+
console.log(JSON.stringify({ lastname, allLevel, page, pageSize: r.pageSize, total: r.total, count: r.count, items: r.items.map((it) => ({ id: it.id, name: it.name, fields: it.fields })) }, null, 2));
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
console.log(`我的下属${allLevel ? '(全部层级)' : '(直接下属)'}${name ? ` · 姓名过滤「${name}」` : ''} [第 ${page} 页${r.pageSize != null ? ` · 每页 ${r.pageSize} 条` : ''}${r.total != null ? ` / 共 ${r.total} 人` : ''}] 当前 ${r.count} 人`);
|
|
3747
|
+
if (r.count === 0) {
|
|
3748
|
+
console.log('(无下属)');
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
const cols = (r.columns || []).filter((c) => c.dataIndex && c.dataIndex !== 'randomFieldId');
|
|
3752
|
+
if (cols.length)
|
|
3753
|
+
console.log(`字段(${cols.length}): ${cols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
|
|
3754
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
3755
|
+
const it = r.items[i];
|
|
3756
|
+
console.log(`${i + 1}. ${it.name || '(无姓名)'} [id=${it.id || ''}]`);
|
|
3757
|
+
const meta = it.fields.filter((f) => f.value && f.title && f.dataIndex !== it.idCol).slice(0, 6).map((f) => `${f.title}=${f.value}`).join(' | ');
|
|
3758
|
+
if (meta)
|
|
3759
|
+
console.log(` ${meta}`);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3510
3762
|
function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
|
|
3511
3763
|
const visible = items.slice(0, max);
|
|
3512
3764
|
for (let i = 0; i < visible.length; i++) {
|
|
@@ -3605,7 +3857,11 @@ async function main() {
|
|
|
3605
3857
|
const cmd = args._[0] ?? 'todo';
|
|
3606
3858
|
if (cmd === 'version') {
|
|
3607
3859
|
const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
|
|
3608
|
-
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.
|
|
3860
|
+
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.1', buildHash }, null, 2));
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
if (cmd === 'whoami') {
|
|
3864
|
+
await runWhoami(args);
|
|
3609
3865
|
return;
|
|
3610
3866
|
}
|
|
3611
3867
|
if (cmd === 'config') {
|
|
@@ -3656,6 +3912,10 @@ async function main() {
|
|
|
3656
3912
|
await runCube(args);
|
|
3657
3913
|
return;
|
|
3658
3914
|
}
|
|
3915
|
+
if (cmd === 'subordinate') {
|
|
3916
|
+
await runSubordinate(args);
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3659
3919
|
if (cmd !== 'todo') {
|
|
3660
3920
|
console.error(`未知命令: ${cmd}\n\n${usage()}`);
|
|
3661
3921
|
process.exit(1);
|
|
@@ -3752,15 +4012,12 @@ async function main() {
|
|
|
3752
4012
|
}
|
|
3753
4013
|
const effectiveCreatorName = flagStr(args, 'creator-name') ?? (typeof savedQueryFlags['creator-name'] === 'string' ? savedQueryFlags['creator-name'] : undefined);
|
|
3754
4014
|
if (!creatorFilter && effectiveCreatorName) {
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
if (exact.length !== 1) {
|
|
3758
|
-
console.log(`“${effectiveCreatorName}”匹配到 ${people.items.length} 位人员,请改用 --creator <人员 ID>:`);
|
|
3759
|
-
people.items.forEach((person, index) => console.log(`${index + 1}. ${person.name} [${person.uid}]${person.name === effectiveCreatorName ? ' ★精确匹配' : ''}`));
|
|
3760
|
-
return;
|
|
3761
|
-
}
|
|
3762
|
-
creatorFilter = String(exact[0].uid);
|
|
4015
|
+
// ★越权防护:姓名目标先经 blog at-list 解析(SKILL.md 硬规则唯一入口),恰 1 个在授权范围内才放行
|
|
4016
|
+
creatorFilter = await assertNamesInScope(cfg, [effectiveCreatorName], { domain: 'todo' });
|
|
3763
4017
|
}
|
|
4018
|
+
// ★越权防护:按人筛选(--creator / resume 路径)目标必须 ∈ {本人 + 全部层级下属}
|
|
4019
|
+
if (creatorFilter)
|
|
4020
|
+
await assertUserIdsInScope(cfg, [creatorFilter], { domain: 'todo' });
|
|
3764
4021
|
const queryFlag = (key) => {
|
|
3765
4022
|
const current = flagStr(args, key);
|
|
3766
4023
|
if (current !== undefined)
|
|
@@ -3994,6 +4251,25 @@ async function runTodoRetract(args) {
|
|
|
3994
4251
|
console.log(` 请在 OA 中核对流程状态:${buildRequestFormUrl(endpointFor(cfg).baseUrl, requestId)}`);
|
|
3995
4252
|
console.log(BOX_TOP);
|
|
3996
4253
|
}
|
|
4254
|
+
async function runWhoami(args) {
|
|
4255
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
4256
|
+
const u = await getCurrentUser(cfg);
|
|
4257
|
+
const admin = await getCurrentUserIsAdmin(cfg);
|
|
4258
|
+
if (args.flags.json) {
|
|
4259
|
+
console.log(JSON.stringify({ userid: u.userid, username: u.username, deptid: u.deptid, deptname: u.deptname, subcompanyname: u.subcompanyname, jobs: u.jobs, isAdmin: admin.isAdmin }, null, 2));
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
console.log(`${BOX_TOP}`);
|
|
4263
|
+
console.log(`当前登录人 ${u.username || '?'}(${u.userid || '?'})`);
|
|
4264
|
+
console.log(DIVIDER);
|
|
4265
|
+
console.log(` 部门: ${u.deptname || '?'}(${u.deptid || '?'})`);
|
|
4266
|
+
if (u.subcompanyname)
|
|
4267
|
+
console.log(` 分部: ${u.subcompanyname}`);
|
|
4268
|
+
if (u.jobs)
|
|
4269
|
+
console.log(` 岗位: ${u.jobs}`);
|
|
4270
|
+
console.log(` 系统管理员: ${admin.isAdmin === true ? '✅ 是' : admin.isAdmin === false ? '否' : '⚠ 无法确认(未取到 isAdmin 字段)'}`);
|
|
4271
|
+
console.log(`${BOX_TOP}`);
|
|
4272
|
+
}
|
|
3997
4273
|
function runConfig(args) {
|
|
3998
4274
|
const sub = args._[1];
|
|
3999
4275
|
if (sub === 'set') {
|