weaver-e9-assistant-cli 1.0.0 → 1.1.2
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 +101 -8
- package/dist/cli.js +360 -59
- package/dist/client.js +26 -5
- package/dist/cube.js +121 -18
- package/dist/email.js +4 -1
- package/dist/subordinate.js +433 -0
- package/dist/todobatch.js +7 -0
- package/dist/wfuser.js +37 -0
- package/dist/workflow.js +33 -19
- 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, 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';
|
|
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, isScopeEnforced } 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>]',
|
|
@@ -58,12 +60,15 @@ function usage() {
|
|
|
58
60
|
' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
|
|
59
61
|
' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
|
|
60
62
|
' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
|
|
61
|
-
' e9 blog list [--date YYYY-MM-DD] [--scope
|
|
63
|
+
' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
|
|
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] 同意对方的关注申请',
|
|
71
|
+
' e9 blog share-users 分享给我的人(用户列表)',
|
|
67
72
|
' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
|
|
68
73
|
' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
|
|
69
74
|
' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
|
|
@@ -113,6 +118,7 @@ function usage() {
|
|
|
113
118
|
' e9 workplan create --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm" [--description-file <路径>] 新建日程(默认 dry-run)',
|
|
114
119
|
' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
|
|
115
120
|
' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
|
|
121
|
+
' e9 workplan export [--type <list|html>] [--group <部门|分部|下级|...>] [--date <YYYY-MM-DD>] [--workplan-type <id>] [--out-dir <目录>] [--yes] 导出日程(list=日程清单 / html=日程一览表,默认 dry-run)',
|
|
116
122
|
' e9 odoc todo [--page <n>] [--subject <标题>] [--dispatch-num <发文字号>] [--type <公文种类id>] [--topic <主题分类id>] 公文待办列表(getCustomPageList table,每页 10 条)',
|
|
117
123
|
' e9 odoc count [--view] 公文待办/待阅计数(发文/收文/签报)',
|
|
118
124
|
' e9 odoc create-list [--type 1|2] 发文/收文流程列表(1=发文拟稿 2=收文登记,复用 create start 发起)',
|
|
@@ -138,6 +144,7 @@ function usage() {
|
|
|
138
144
|
' e9 cube menus [--refresh] 列全部建模台账(目录接口为主、菜单兜底,缓存7天,一次拿全 customid+mode+app)',
|
|
139
145
|
' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
|
|
140
146
|
' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
|
|
147
|
+
' e9 subordinate list [--name <姓名>] [--all-level] [--page <n>] [--json] [--raw] [--refresh] 查询我的下属(默认直接下属;--all-level 全部层级)',
|
|
141
148
|
' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
|
|
142
149
|
' e9 config 查看当前配置来源与状态',
|
|
143
150
|
' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
|
|
@@ -1308,6 +1315,8 @@ async function runBlog(args) {
|
|
|
1308
1315
|
console.error('blog view 需要 --user-id <用户id>');
|
|
1309
1316
|
process.exit(1);
|
|
1310
1317
|
}
|
|
1318
|
+
// ★越权防护:只能看本人 + 全部层级下属的微博主页,越界拒绝
|
|
1319
|
+
await assertUserIdsInScope(cfg, [userId], { domain: 'blog' });
|
|
1311
1320
|
const r = await getBlogPersonalInfo(cfg, userId);
|
|
1312
1321
|
if (args.flags.json) {
|
|
1313
1322
|
console.log(JSON.stringify(r, null, 2));
|
|
@@ -1351,16 +1360,25 @@ async function runBlog(args) {
|
|
|
1351
1360
|
process.exit(1);
|
|
1352
1361
|
}
|
|
1353
1362
|
const send = !!args.flags.yes;
|
|
1354
|
-
// 被评论人 userid(bediscussantId) + workdate:手动优先,否则从微博列表按 id
|
|
1363
|
+
// 被评论人 userid(bediscussantId) + workdate:手动优先,否则从微博列表按 id 查。
|
|
1364
|
+
// 修复 #4:--owner <作者userid> 跨账号直达(a 回复 b 的微博时作者即被评论人,无需再翻列表);
|
|
1365
|
+
// 未给 --owner 时保持原逻辑——从当前登录者自己的微博列表按 id 查找。
|
|
1355
1366
|
let discussantId = flagStr(args, 'bediscussant-id');
|
|
1356
1367
|
let workdate = flagStr(args, 'workdate');
|
|
1368
|
+
const owner = flagStr(args, 'owner');
|
|
1369
|
+
if (!discussantId && owner) {
|
|
1370
|
+
discussantId = owner;
|
|
1371
|
+
}
|
|
1372
|
+
if (!workdate && owner) {
|
|
1373
|
+
workdate = flagStr(args, 'date') ?? todayStr();
|
|
1374
|
+
}
|
|
1357
1375
|
if (!discussantId || !workdate) {
|
|
1358
1376
|
const date = flagStr(args, 'date') ?? todayStr();
|
|
1359
1377
|
// 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
|
|
1360
1378
|
const list = await getMyBlogAll(cfg, date);
|
|
1361
1379
|
const hit = (list.items || []).find(b => String(b.id) === String(blogId));
|
|
1362
1380
|
if (!hit) {
|
|
1363
|
-
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --bediscussant-id <作者userid> --workdate <YYYY-MM-DD> 手动指定,或 --date 指定微博日期。`);
|
|
1381
|
+
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --bediscussant-id <作者userid> --workdate <YYYY-MM-DD> 手动指定,或 --owner <作者userid> 跨账号直达,或 --date 指定微博日期。`);
|
|
1364
1382
|
process.exit(1);
|
|
1365
1383
|
}
|
|
1366
1384
|
discussantId = discussantId || (hit.raw?.userid ? String(hit.raw.userid) : '') || (hit.raw?.creater ? String(hit.raw.creater) : '') || (hit.userid ? String(hit.userid) : '');
|
|
@@ -1371,12 +1389,22 @@ async function runBlog(args) {
|
|
|
1371
1389
|
console.log(`[dry-run] 未发送。将回复微博 id=${r.blogId}(被评论人 userid=${discussantId}, workdate=${workdate})。确认后加 --yes。`);
|
|
1372
1390
|
return;
|
|
1373
1391
|
}
|
|
1374
|
-
console.log(`[已回复] blogId=${r.blogId} status=${r.response?.status}`);
|
|
1375
1392
|
const j = r.response?.json;
|
|
1376
1393
|
const ok = j?.status === '1' || j?.status === 1 || j?.api_status === '1';
|
|
1394
|
+
// 修复 #5:--json 输出完整响应(含 blogReplyList 每条的 replyId),不再截断 300 字符;
|
|
1395
|
+
// 同时避免前置普通输出行污染 json(--json 时先输出 JSON 再 return)。
|
|
1396
|
+
if (args.flags.json) {
|
|
1397
|
+
console.log(JSON.stringify({ blogId: r.blogId, workdate, discussantId, status: r.response?.status, ok, response: j }, null, 2));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
console.log(`[已回复] blogId=${r.blogId} status=${r.response?.status}`);
|
|
1377
1401
|
console.log(` ${ok ? '✅ 回复成功' : '⚠ 回复失败'}`);
|
|
1378
|
-
if (j)
|
|
1379
|
-
|
|
1402
|
+
if (j) {
|
|
1403
|
+
const list = j.blogReplyList ?? j.blogReplyPoList ?? [];
|
|
1404
|
+
if (Array.isArray(list) && list.length)
|
|
1405
|
+
console.log(` 最新回复: ${list.map((it) => `replyId=${it.replyId ?? it.id}`).join(', ')}`);
|
|
1406
|
+
console.log('响应:', JSON.stringify(j).slice(0, 300), '(--json 查看完整回复列表)');
|
|
1407
|
+
}
|
|
1380
1408
|
return;
|
|
1381
1409
|
}
|
|
1382
1410
|
if (sub === 'like') {
|
|
@@ -1388,16 +1416,33 @@ async function runBlog(args) {
|
|
|
1388
1416
|
const send = !!args.flags.yes;
|
|
1389
1417
|
// 手动参数优先;否则自动从微博列表解析 workdate / 当前用户 id / 当前点赞状态。
|
|
1390
1418
|
// SPA bundle 实证 addOrDeleteZan 请求体:workdate + userId + optType(1=赞/0=取消) + discussId(微博id)。
|
|
1419
|
+
// 修复 #4:--owner <作者userid> 跨账号点赞——workdate 取 date/今天,userId 仍为当前登录者(点赞人)。
|
|
1391
1420
|
let workdate = flagStr(args, 'workdate');
|
|
1392
1421
|
let userId = flagStr(args, 'user-id');
|
|
1393
1422
|
let optType = flagStr(args, 'opt-type');
|
|
1423
|
+
const owner = flagStr(args, 'owner');
|
|
1424
|
+
const date = flagStr(args, 'date') ?? todayStr();
|
|
1425
|
+
if (owner) {
|
|
1426
|
+
workdate = workdate || date;
|
|
1427
|
+
if (!userId) {
|
|
1428
|
+
const me = await getCurrentUserInfo(cfg);
|
|
1429
|
+
userId = me.id ? String(me.id) : '';
|
|
1430
|
+
}
|
|
1431
|
+
if (!optType) {
|
|
1432
|
+
// 跨账号已赞判定:从 owner 的微博首页列表找该条,看 blogZanBean.blogZanPoList 是否含当前用户
|
|
1433
|
+
const list = await getBlogHomepageList(cfg, { blogUserId: owner, endDate: date, startDate: date });
|
|
1434
|
+
const hit = (list.items || []).find((b) => String(b.id) === String(blogId));
|
|
1435
|
+
const zanList = Array.isArray(hit?.raw?.blogZanBean?.blogZanPoList) ? hit.raw.blogZanBean.blogZanPoList : [];
|
|
1436
|
+
const alreadyLiked = zanList.some((z) => String(z.zanHrmid ?? '') === String(userId));
|
|
1437
|
+
optType = alreadyLiked ? '0' : '1';
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1394
1440
|
if (!workdate || !userId || !optType) {
|
|
1395
|
-
const date = flagStr(args, 'date') ?? todayStr();
|
|
1396
1441
|
// 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
|
|
1397
1442
|
const list = await getMyBlogAll(cfg, date);
|
|
1398
1443
|
const hit = (list.items || []).find((b) => String(b.id) === String(blogId));
|
|
1399
1444
|
if (!hit) {
|
|
1400
|
-
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --workdate <YYYY-MM-DD> --user-id <当前用户id> --opt-type <1|0> 手动指定,或 --date 指定微博日期。`);
|
|
1445
|
+
console.error(`未在微博列表(${date})找到 id=${blogId}。用 --workdate <YYYY-MM-DD> --user-id <当前用户id> --opt-type <1|0> 手动指定,或 --owner <作者userid> 跨账号直达,或 --date 指定微博日期。`);
|
|
1401
1446
|
process.exit(1);
|
|
1402
1447
|
}
|
|
1403
1448
|
workdate = workdate || String(hit.workdate ?? '') || String(hit.createdate ?? '').slice(0, 10);
|
|
@@ -1478,8 +1523,9 @@ async function runBlog(args) {
|
|
|
1478
1523
|
return;
|
|
1479
1524
|
}
|
|
1480
1525
|
console.log(`${BOX_TOP}`);
|
|
1481
|
-
console.log(`E9 微博 工作指数 & 心情指数`);
|
|
1526
|
+
console.log(`E9 微博 考勤指数 & 工作指数 & 心情指数`);
|
|
1482
1527
|
console.log(DIVIDER);
|
|
1528
|
+
console.log(` 考勤指数: ${wi.schedule.index ?? '?'} ${wi.schedule.title ?? ''}`);
|
|
1483
1529
|
console.log(` 工作指数: ${wi.work.index ?? '?'} ${wi.work.title ?? ''}`);
|
|
1484
1530
|
console.log(` 心情指数: ${wi.mood.index ?? '?'} ${wi.mood.title ?? ''}`);
|
|
1485
1531
|
console.log(DIVIDER);
|
|
@@ -1554,6 +1600,60 @@ async function runBlog(args) {
|
|
|
1554
1600
|
console.log('响应:', JSON.stringify(r.response.json).slice(0, 400));
|
|
1555
1601
|
return;
|
|
1556
1602
|
}
|
|
1603
|
+
if (sub === 'follow') {
|
|
1604
|
+
// 发起关注。E9 微博关注需对方同意:attentionOpt(attentionStatus=3) 发申请 → 对方 dealMsgRemind(opt=1) 同意。
|
|
1605
|
+
const userId = flagStr(args, 'id');
|
|
1606
|
+
if (!userId) {
|
|
1607
|
+
console.error('blog follow 需要 --id <目标用户id>(被关注者 userid)');
|
|
1608
|
+
process.exit(1);
|
|
1609
|
+
}
|
|
1610
|
+
const send = !!args.flags.yes;
|
|
1611
|
+
const r = await attentionOpt(cfg, { userId, send });
|
|
1612
|
+
if (!r.sent) {
|
|
1613
|
+
console.log(`[dry-run] 未发送。将向 userId=${r.userId} 发起关注申请(attentionStatus=${r.attentionStatus})。确认后加 --yes。`);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
console.log(`[已发送关注申请] 目标 userId=${r.userId} attentionStatus=${r.attnStatus ?? '?'}`);
|
|
1617
|
+
console.log(` ${r.ok ? '✅ 关注申请已发出(等待对方同意,互关后可访问对方微博)' : '⚠ 关注申请失败'}`);
|
|
1618
|
+
if (r.raw) {
|
|
1619
|
+
const hint = r.raw?.errorCode === '002' || r.raw?.msg?.includes('登录信息超时')
|
|
1620
|
+
? ' ⚠ cookie 已过期,请先 `e9 auth token-login` 刷新后重试。'
|
|
1621
|
+
: ' (cookie 通道接口,token 通道不生效)';
|
|
1622
|
+
if (!r.ok)
|
|
1623
|
+
console.log(` 响应: ${JSON.stringify(r.raw).slice(0, 200)}${hint}`);
|
|
1624
|
+
}
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (sub === 'agree-follow') {
|
|
1628
|
+
// 同意关注申请:getMsgRemindList 找"向你申请关注"(remindType=1) 的记录 → dealMsgRemind(id, relatedId, opt=1)。
|
|
1629
|
+
// 实测字段方向不定(remindid/relatedid 中任一可能是申请人),用宽匹配;
|
|
1630
|
+
// relatedId 抓包实证 = 当前登录者自己的 userid(sysadmin=1),与记录字段无关。
|
|
1631
|
+
const userId = flagStr(args, 'id');
|
|
1632
|
+
if (!userId) {
|
|
1633
|
+
console.error('blog agree-follow 需要 --id <申请人用户id>');
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
const send = !!args.flags.yes;
|
|
1637
|
+
const reminds = await getMsgRemindList(cfg);
|
|
1638
|
+
const req = (reminds.items || []).find((it) => it.remindType === '1' && (it.remindid === String(userId) || it.relatedid === String(userId)));
|
|
1639
|
+
if (!req) {
|
|
1640
|
+
console.error(`未在消息提醒中找到 ${userId} 的关注申请(remindType=1)。可用 \`blog reminders --json\` 查看全部提醒;若对方已取消申请则无需处理。`);
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
// 修复 #9:dealMsgRemind 的 relatedId = 申请人(发起关注者)userid,抓包实证;
|
|
1644
|
+
// 早期误用当前登录者 id 导致「同意成功但互关未建立」。userId 入参即申请人。
|
|
1645
|
+
const relatedId = String(userId);
|
|
1646
|
+
if (!send) {
|
|
1647
|
+
console.log(`[dry-run] 将同意 ${userId} 的关注申请(提醒记录 id=${req.id}, relatedId=${relatedId})。确认后加 --yes。`);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const r = await dealMsgRemind(cfg, { id: req.id, relatedId, opt: 1, send });
|
|
1651
|
+
console.log(`[已处理关注申请] 申请人 userId=${userId} 提醒 id=${r.id}`);
|
|
1652
|
+
console.log(` ${r.ok ? '✅ 已同意关注,互关完成' : '⚠ 处理失败'}`);
|
|
1653
|
+
if (r.raw && !r.ok)
|
|
1654
|
+
console.log(` 响应: ${JSON.stringify(r.raw).slice(0, 200)}`);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1557
1657
|
if (sub === 'delete-reply') {
|
|
1558
1658
|
// 删除微博评论(正文不可删,只能撤回;评论可删)。POST /api/blog/base/deleteBlogReplay。
|
|
1559
1659
|
const replyId = flagStr(args, 'id');
|
|
@@ -1598,9 +1698,12 @@ async function runBlog(args) {
|
|
|
1598
1698
|
console.log('(无提醒)');
|
|
1599
1699
|
return;
|
|
1600
1700
|
}
|
|
1701
|
+
const TYPE_LABEL = { '1': '关注申请', '8': '系统提醒' };
|
|
1601
1702
|
for (let i = 0; i < r.items.length; i++) {
|
|
1602
1703
|
const it = r.items[i];
|
|
1603
|
-
|
|
1704
|
+
const label = TYPE_LABEL[it.remindType] ?? (it.type ? `类型${it.type}` : '提醒');
|
|
1705
|
+
const time = [it.createDate, it.createTime].filter(Boolean).join(' ');
|
|
1706
|
+
console.log(`${i + 1}. [${label}] ${it.content ?? ''}${time ? ` (${time})` : ''}`);
|
|
1604
1707
|
}
|
|
1605
1708
|
return;
|
|
1606
1709
|
}
|
|
@@ -1665,6 +1768,24 @@ async function runBlog(args) {
|
|
|
1665
1768
|
}
|
|
1666
1769
|
return;
|
|
1667
1770
|
}
|
|
1771
|
+
if (sub === 'share-users') {
|
|
1772
|
+
const r = await getShareToMeUserList(cfg);
|
|
1773
|
+
if (args.flags.json) {
|
|
1774
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
console.log(`E9 微博 分享给我的人 共 ${r.count ?? r.items.length} 人`);
|
|
1778
|
+
if (r.items.length === 0) {
|
|
1779
|
+
console.log('(无人分享给我)');
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
1783
|
+
const it = r.items[i];
|
|
1784
|
+
const meta = [it.deptName, it.jobtitle, it.isattention ? '已关注' : null].filter(Boolean).join(' | ');
|
|
1785
|
+
console.log(`${i + 1}. ${it.username ?? '(未知)'}${meta ? ` (${meta})` : ''}`);
|
|
1786
|
+
}
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1668
1789
|
if (sub === 'search-fields') {
|
|
1669
1790
|
const r = await getBlogPageSearchCondition(cfg);
|
|
1670
1791
|
if (args.flags.json) {
|
|
@@ -1696,7 +1817,7 @@ async function runBlog(args) {
|
|
|
1696
1817
|
console.log(JSON.stringify(r.template, null, 2));
|
|
1697
1818
|
return;
|
|
1698
1819
|
}
|
|
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}`);
|
|
1820
|
+
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}`);
|
|
1700
1821
|
process.exit(1);
|
|
1701
1822
|
}
|
|
1702
1823
|
async function runWorkplan(args) {
|
|
@@ -1724,36 +1845,73 @@ async function runWorkplan(args) {
|
|
|
1724
1845
|
return;
|
|
1725
1846
|
}
|
|
1726
1847
|
if (sub === 'list') {
|
|
1727
|
-
// 使用 getCalendarData 替代 AllCalendar,支持组织视图
|
|
1728
1848
|
const group = flagStr(args, 'group');
|
|
1729
|
-
|
|
1849
|
+
const date = flagStr(args, 'date');
|
|
1850
|
+
const type = flagStr(args, 'type');
|
|
1851
|
+
// 默认(无 --group)=「我的日程」:查当前用户自己的日程,走 MyCalendar(selectUser 空=当前用户)。
|
|
1852
|
+
// 只有显式传 --group 才走组织视图 getCalendarData(部门/分部/下级等)。
|
|
1853
|
+
if (!group) {
|
|
1854
|
+
const selectUser = flagStr(args, 'select-user');
|
|
1855
|
+
// ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
|
|
1856
|
+
if (selectUser)
|
|
1857
|
+
await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
|
|
1858
|
+
const r = await getMyCalendar(cfg, { selectDate: date, viewType: '3', selectUser: selectUser ?? '', workPlanType: type });
|
|
1859
|
+
if (args.flags.raw) {
|
|
1860
|
+
console.error('===== RAW MyCalendar =====');
|
|
1861
|
+
console.error(JSON.stringify(r.raw, null, 2));
|
|
1862
|
+
console.error('===== 解析结果 =====');
|
|
1863
|
+
}
|
|
1864
|
+
if (args.flags.json) {
|
|
1865
|
+
console.log(JSON.stringify({
|
|
1866
|
+
selectDate: r.selectDate, count: r.items.length,
|
|
1867
|
+
items: r.items.map((it) => withLink('workplan', {
|
|
1868
|
+
id: it.id, name: it.name,
|
|
1869
|
+
beginDateTime: it.beginDateTime, endDateTime: it.endDateTime,
|
|
1870
|
+
color: it.color, typeName: it.typeName, allDay: it.allDay,
|
|
1871
|
+
}, wpBase)),
|
|
1872
|
+
}, null, 2));
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
console.log(`E9 日程 [${r.selectDate}] 共 ${r.items.length} 条`);
|
|
1876
|
+
if (r.items.length === 0) {
|
|
1877
|
+
console.log('(该区间无日程)');
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
1881
|
+
const it = r.items[i];
|
|
1882
|
+
const time = [it.beginDateTime, it.endDateTime].filter(Boolean).join(' ~ ');
|
|
1883
|
+
const allDay = it.allDay === '1' ? ' [全天]' : '';
|
|
1884
|
+
console.log(`${i + 1}. ${hyperlinkFor('workplan', it, wpBase, it.name ?? '(无名)')}${allDay}${time ? ` [${time}]` : ''} ${it.typeName ?? ''}`);
|
|
1885
|
+
}
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
// 组织视图(--group 部门/分部/下级等)
|
|
1730
1889
|
const bd = await getBaseData(cfg);
|
|
1731
1890
|
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
|
-
|
|
1891
|
+
const groupMap = { '部门': '1', '分部': '2', '人力资源': '3', '人力': '3', '所有上级': '4', '上级': '4', '直接下级': '5', '下级': '5', '常用组': '6', '我的关注': '7', '关注': '7' };
|
|
1892
|
+
const gkey = groupMap[group] || group;
|
|
1893
|
+
const sg = bd.selectGroup.find((g) => g.key === gkey);
|
|
1894
|
+
if (sg) {
|
|
1895
|
+
if (gkey === '1') {
|
|
1896
|
+
objType = '1';
|
|
1897
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
1898
|
+
}
|
|
1899
|
+
else if (gkey === '2') {
|
|
1900
|
+
objType = '2';
|
|
1901
|
+
objIds = bd.subComp[0]?.id ?? '';
|
|
1902
|
+
}
|
|
1903
|
+
else if (gkey === '3') {
|
|
1904
|
+
// 人力资源:按人员查询,默认当前登录人(即「我的日程」)。当前用户 id 从 cookie 的 loginuuids 取(导出/日历走 cookie,token 可能过期)。
|
|
1905
|
+
objType = '3';
|
|
1906
|
+
const _uid = String(cfg.cookie || '').match(/loginuuids=([^;]*)/i);
|
|
1907
|
+
objIds = _uid ? decodeURIComponent(_uid[1].trim()) : '';
|
|
1908
|
+
}
|
|
1909
|
+
else if (gkey === '5') {
|
|
1910
|
+
objType = '1';
|
|
1911
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
1749
1912
|
}
|
|
1750
1913
|
}
|
|
1751
|
-
const r = await getCalendarData(cfg, {
|
|
1752
|
-
selectDate: flagStr(args, 'date'),
|
|
1753
|
-
objType,
|
|
1754
|
-
objIds,
|
|
1755
|
-
workPlanType: flagStr(args, 'type'),
|
|
1756
|
-
});
|
|
1914
|
+
const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type });
|
|
1757
1915
|
if (args.flags.raw) {
|
|
1758
1916
|
console.error('===== RAW getCalendarData =====');
|
|
1759
1917
|
console.error(JSON.stringify(r.raw, null, 2));
|
|
@@ -1775,7 +1933,7 @@ async function runWorkplan(args) {
|
|
|
1775
1933
|
}, null, 2));
|
|
1776
1934
|
return;
|
|
1777
1935
|
}
|
|
1778
|
-
const groupLabel =
|
|
1936
|
+
const groupLabel = ` · ${group}`;
|
|
1779
1937
|
console.log(`E9 日程 [${r.selectDate}${groupLabel}] 共 ${r.count} 条`);
|
|
1780
1938
|
if (r.count === 0) {
|
|
1781
1939
|
console.log('(该区间无日程)');
|
|
@@ -1830,8 +1988,10 @@ async function runWorkplan(args) {
|
|
|
1830
1988
|
}
|
|
1831
1989
|
console.log(`[已完成] workid=${r.workid} status=${r.response?.status}`);
|
|
1832
1990
|
const j = r.response?.json;
|
|
1833
|
-
|
|
1834
|
-
|
|
1991
|
+
// 修复 #2:以 status===true 为成功标志(失败时 status===false + error 字段),
|
|
1992
|
+
// 避免「api_status:true 但 status:false」被误判成功。
|
|
1993
|
+
const ok = (j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true) && !j?.error;
|
|
1994
|
+
console.log(` ${ok ? '✅ 完成成功' : '⚠ 完成失败'}${j?.error ? `:${j.error}` : ''}`);
|
|
1835
1995
|
if (j)
|
|
1836
1996
|
console.log('响应:', JSON.stringify(j).slice(0, 300));
|
|
1837
1997
|
return;
|
|
@@ -1850,8 +2010,10 @@ async function runWorkplan(args) {
|
|
|
1850
2010
|
}
|
|
1851
2011
|
console.log(`[已删除] workid=${r.workid} status=${r.response?.status}`);
|
|
1852
2012
|
const j = r.response?.json;
|
|
1853
|
-
|
|
1854
|
-
|
|
2013
|
+
// 修复 #2:后端删除失败时返回 {api_status:true, status:false, error:"..."},
|
|
2014
|
+
// 旧判断只看 api_status 误判成功。以 status===true 且无 error 为成功。
|
|
2015
|
+
const ok = (j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true) && !j?.error;
|
|
2016
|
+
console.log(` ${ok ? '✅ 删除成功' : '⚠ 删除失败'}${j?.error ? `:${j.error}` : ''}`);
|
|
1855
2017
|
if (j)
|
|
1856
2018
|
console.log('响应:', JSON.stringify(j).slice(0, 300));
|
|
1857
2019
|
return;
|
|
@@ -1906,12 +2068,16 @@ async function runWorkplan(args) {
|
|
|
1906
2068
|
}
|
|
1907
2069
|
catch { /* 获取分组失败不阻断 */ }
|
|
1908
2070
|
}
|
|
2071
|
+
// ★越权防护:按人筛选目标必须 ∈ {本人 + 全部层级下属}
|
|
2072
|
+
const createrId = flagStr(args, 'creater');
|
|
2073
|
+
const receiveId = flagStr(args, 'receiver');
|
|
2074
|
+
await assertUserIdsInScope(cfg, [createrId, receiveId].filter(Boolean), { domain: 'workplan' });
|
|
1909
2075
|
const r = await searchWorkplan(cfg, {
|
|
1910
2076
|
name: flagStr(args, 'name'),
|
|
1911
2077
|
type: flagStr(args, 'type'),
|
|
1912
2078
|
status: flagStr(args, 'status'),
|
|
1913
|
-
createrId
|
|
1914
|
-
receiveId
|
|
2079
|
+
createrId,
|
|
2080
|
+
receiveId,
|
|
1915
2081
|
timeSag: groupFilter ?? flagStr(args, 'time-sag') ?? '0',
|
|
1916
2082
|
beginDate: flagStr(args, 'begin'),
|
|
1917
2083
|
endDate: flagStr(args, 'end'),
|
|
@@ -2006,7 +2172,11 @@ async function runWorkplan(args) {
|
|
|
2006
2172
|
return;
|
|
2007
2173
|
}
|
|
2008
2174
|
if (sub === 'my-calendar') {
|
|
2009
|
-
const
|
|
2175
|
+
const selectUser = flagStr(args, 'select-user');
|
|
2176
|
+
// ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
|
|
2177
|
+
if (selectUser)
|
|
2178
|
+
await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
|
|
2179
|
+
const r = await getMyCalendar(cfg, { selectDate: flagStr(args, 'date'), viewType: flagStr(args, 'view-type'), selectUser, workPlanType: flagStr(args, 'type') });
|
|
2010
2180
|
if (args.flags.json) {
|
|
2011
2181
|
console.log(JSON.stringify(r, null, 2));
|
|
2012
2182
|
return;
|
|
@@ -2066,7 +2236,54 @@ async function runWorkplan(args) {
|
|
|
2066
2236
|
console.log(`${BOX_TOP}`);
|
|
2067
2237
|
return;
|
|
2068
2238
|
}
|
|
2069
|
-
|
|
2239
|
+
if (sub === 'export') {
|
|
2240
|
+
const type = flagStr(args, 'type') ?? 'list';
|
|
2241
|
+
if (type !== 'list' && type !== 'html') {
|
|
2242
|
+
console.error('workplan export 的 --type 只支持 list(日程清单)或 html(日程一览表)');
|
|
2243
|
+
process.exit(1);
|
|
2244
|
+
}
|
|
2245
|
+
const group = flagStr(args, 'group') ?? '部门';
|
|
2246
|
+
const date = flagStr(args, 'date');
|
|
2247
|
+
const bd = await getBaseData(cfg);
|
|
2248
|
+
let objType = '1', objIds = bd.dept[0]?.id ?? '';
|
|
2249
|
+
const groupMap = { '部门': '1', '分部': '2', '人力资源': '3', '人力': '3', '所有上级': '4', '上级': '4', '直接下级': '5', '下级': '5', '常用组': '6', '我的关注': '7', '关注': '7' };
|
|
2250
|
+
const gkey = groupMap[group] || group;
|
|
2251
|
+
const sg = bd.selectGroup.find((g) => g.key === gkey);
|
|
2252
|
+
if (sg) {
|
|
2253
|
+
if (gkey === '1') {
|
|
2254
|
+
objType = '1';
|
|
2255
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
2256
|
+
}
|
|
2257
|
+
else if (gkey === '2') {
|
|
2258
|
+
objType = '2';
|
|
2259
|
+
objIds = bd.subComp[0]?.id ?? '';
|
|
2260
|
+
}
|
|
2261
|
+
else if (gkey === '3') {
|
|
2262
|
+
// 人力资源:按人员查询,默认当前登录人(即「我的日程」)。当前用户 id 从 cookie 的 loginuuids 取(导出/日历走 cookie,token 可能过期)。
|
|
2263
|
+
objType = '3';
|
|
2264
|
+
const _uid = String(cfg.cookie || '').match(/loginuuids=([^;]*)/i);
|
|
2265
|
+
objIds = _uid ? decodeURIComponent(_uid[1].trim()) : '';
|
|
2266
|
+
}
|
|
2267
|
+
else if (gkey === '5') {
|
|
2268
|
+
objType = '1';
|
|
2269
|
+
objIds = bd.dept[0]?.id ?? '';
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const label = type === 'html' ? '日程一览表' : '日程清单';
|
|
2273
|
+
if (!args.flags.yes) {
|
|
2274
|
+
console.log(`[dry-run] 将导出${label}:${date ?? '今天'} · ${group}(objType=${objType} objIds=${objIds})`);
|
|
2275
|
+
console.log('确认无误后加 --yes 真正导出并下载。');
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const r = await exportWorkplanExcel(cfg, {
|
|
2279
|
+
type, selectDate: date, objType, objIds,
|
|
2280
|
+
workPlanType: flagStr(args, 'workplan-type'),
|
|
2281
|
+
outDir: flagStr(args, 'out-dir'),
|
|
2282
|
+
});
|
|
2283
|
+
console.log(`✅ 已导出${label}:${r.outPath}`);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
console.error(`workplan 暂只支持 list / view / finish / delete / types / search-conditions / search / create / my-calendar / attention / base-data / export / import-template / import,收到: ${sub}`);
|
|
2070
2287
|
process.exit(1);
|
|
2071
2288
|
}
|
|
2072
2289
|
async function runOdoc(args) {
|
|
@@ -2139,6 +2356,10 @@ async function runOdoc(args) {
|
|
|
2139
2356
|
console.error(`odoc 暂只支持 todo / count / create-list,收到: ${sub}`);
|
|
2140
2357
|
process.exit(1);
|
|
2141
2358
|
}
|
|
2359
|
+
// ★越权防护:公文列表按人过滤(签发人/拟稿操作人/在办人/未办人)目标必须 ∈ {本人 + 全部层级下属}
|
|
2360
|
+
const odocPersonIds = [flagStr(args, 'issued-userid'), flagStr(args, 'owner-id'), flagStr(args, 'dealing-userid'), flagStr(args, 'undealed-userid')].filter(Boolean);
|
|
2361
|
+
if (odocPersonIds.length)
|
|
2362
|
+
await assertUserIdsInScope(cfg, odocPersonIds, { domain: 'odoc' });
|
|
2142
2363
|
const r = await listOdocTodo(cfg, {
|
|
2143
2364
|
requestName: flagStr(args, 'subject'),
|
|
2144
2365
|
dispatchNum: flagStr(args, 'dispatch-num'),
|
|
@@ -3456,6 +3677,13 @@ async function runCube(args) {
|
|
|
3456
3677
|
const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
|
|
3457
3678
|
if (condRes.unresolved.length)
|
|
3458
3679
|
printConditionUnresolved(condRes.unresolved);
|
|
3680
|
+
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3681
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3682
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3683
|
+
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3684
|
+
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3685
|
+
if (condRes.personConditions?.length)
|
|
3686
|
+
await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'cube', customid });
|
|
3459
3687
|
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
3688
|
if (args.flags.json) {
|
|
3461
3689
|
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 +3706,13 @@ async function runCube(args) {
|
|
|
3478
3706
|
const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
|
|
3479
3707
|
if (condRes.unresolved.length)
|
|
3480
3708
|
printConditionUnresolved(condRes.unresolved);
|
|
3709
|
+
// ★越权防护(fail-closed):条件中存在无法确认是否按人过滤的条件(定义读取失败/未知 domkey)→ 硬拒绝
|
|
3710
|
+
// 策略未启用(默认)时不拦,仅保留代码路径。
|
|
3711
|
+
if (isScopeEnforced() && condRes.unverifiablePerson)
|
|
3712
|
+
throw new Error('拒绝越权查询:台账查询条件无法确认是否按人过滤(条件定义读取失败或含未识别字段)。为防越权,本次查询已拒绝,请稍后重试或改用中文字段名。');
|
|
3713
|
+
// ★越权防护:人员条件(填报人/审批人/报销人/经手人等)目标必须 ∈ {本人 + 全部层级下属}
|
|
3714
|
+
if (condRes.personConditions?.length)
|
|
3715
|
+
await assertNamesInScope(cfg, condRes.personConditions.map((p) => p.value), { domain: 'cube', customid });
|
|
3481
3716
|
const conditionValues = condRes.conditionValues;
|
|
3482
3717
|
const r = await listCube(cfg, { customid, page: Number(flagStr(args, 'page') ?? '1') || 1, keyword: flagStr(args, 'keyword') ?? '', conditionValues });
|
|
3483
3718
|
if (args.flags.json) {
|
|
@@ -3507,6 +3742,48 @@ async function runCube(args) {
|
|
|
3507
3742
|
console.log(` ${meta}`);
|
|
3508
3743
|
}
|
|
3509
3744
|
}
|
|
3745
|
+
async function runSubordinate(args) {
|
|
3746
|
+
const sub = args._[1] ?? 'list';
|
|
3747
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
3748
|
+
if (sub !== 'list') {
|
|
3749
|
+
console.error('subordinate 只支持 list。');
|
|
3750
|
+
process.exit(1);
|
|
3751
|
+
}
|
|
3752
|
+
const name = flagStr(args, 'name');
|
|
3753
|
+
const allLevel = flagBool(args, 'all-level');
|
|
3754
|
+
const page = Number(flagStr(args, 'page') ?? '1') || 1;
|
|
3755
|
+
// 姓名目标 → 越权校验(恰 1 个在授权范围内才放行,否则拒绝);无 --name = 全部直属下属(本人天然在范围)
|
|
3756
|
+
const lastname = name ? await assertNamesInScope(cfg, [name], { domain: 'subordinate' }) : '';
|
|
3757
|
+
const r = await getSubordinates(cfg, { lastname, allLevel, page });
|
|
3758
|
+
if (args.flags.raw) {
|
|
3759
|
+
console.error('===== RAW getSearchResult =====');
|
|
3760
|
+
console.error(JSON.stringify(r.raw.getSearchResult, null, 2));
|
|
3761
|
+
console.error('===== RAW datas =====');
|
|
3762
|
+
console.error(JSON.stringify(r.raw.datas, null, 2));
|
|
3763
|
+
console.error('===== RAW counts =====');
|
|
3764
|
+
console.error(JSON.stringify(r.raw.counts, null, 2));
|
|
3765
|
+
console.error('===== 解析结果 =====');
|
|
3766
|
+
}
|
|
3767
|
+
if (args.flags.json) {
|
|
3768
|
+
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));
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
console.log(`我的下属${allLevel ? '(全部层级)' : '(直接下属)'}${name ? ` · 姓名过滤「${name}」` : ''} [第 ${page} 页${r.pageSize != null ? ` · 每页 ${r.pageSize} 条` : ''}${r.total != null ? ` / 共 ${r.total} 人` : ''}] 当前 ${r.count} 人`);
|
|
3772
|
+
if (r.count === 0) {
|
|
3773
|
+
console.log('(无下属)');
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
const cols = (r.columns || []).filter((c) => c.dataIndex && c.dataIndex !== 'randomFieldId');
|
|
3777
|
+
if (cols.length)
|
|
3778
|
+
console.log(`字段(${cols.length}): ${cols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
|
|
3779
|
+
for (let i = 0; i < r.items.length; i++) {
|
|
3780
|
+
const it = r.items[i];
|
|
3781
|
+
console.log(`${i + 1}. ${it.name || '(无姓名)'} [id=${it.id || ''}]`);
|
|
3782
|
+
const meta = it.fields.filter((f) => f.value && f.title && f.dataIndex !== it.idCol).slice(0, 6).map((f) => `${f.title}=${f.value}`).join(' | ');
|
|
3783
|
+
if (meta)
|
|
3784
|
+
console.log(` ${meta}`);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3510
3787
|
function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
|
|
3511
3788
|
const visible = items.slice(0, max);
|
|
3512
3789
|
for (let i = 0; i < visible.length; i++) {
|
|
@@ -3605,7 +3882,11 @@ async function main() {
|
|
|
3605
3882
|
const cmd = args._[0] ?? 'todo';
|
|
3606
3883
|
if (cmd === 'version') {
|
|
3607
3884
|
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.
|
|
3885
|
+
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.2', buildHash }, null, 2));
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
if (cmd === 'whoami') {
|
|
3889
|
+
await runWhoami(args);
|
|
3609
3890
|
return;
|
|
3610
3891
|
}
|
|
3611
3892
|
if (cmd === 'config') {
|
|
@@ -3656,6 +3937,10 @@ async function main() {
|
|
|
3656
3937
|
await runCube(args);
|
|
3657
3938
|
return;
|
|
3658
3939
|
}
|
|
3940
|
+
if (cmd === 'subordinate') {
|
|
3941
|
+
await runSubordinate(args);
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3659
3944
|
if (cmd !== 'todo') {
|
|
3660
3945
|
console.error(`未知命令: ${cmd}\n\n${usage()}`);
|
|
3661
3946
|
process.exit(1);
|
|
@@ -3752,15 +4037,12 @@ async function main() {
|
|
|
3752
4037
|
}
|
|
3753
4038
|
const effectiveCreatorName = flagStr(args, 'creator-name') ?? (typeof savedQueryFlags['creator-name'] === 'string' ? savedQueryFlags['creator-name'] : undefined);
|
|
3754
4039
|
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);
|
|
4040
|
+
// ★越权防护:姓名目标先经 blog at-list 解析(SKILL.md 硬规则唯一入口),恰 1 个在授权范围内才放行
|
|
4041
|
+
creatorFilter = await assertNamesInScope(cfg, [effectiveCreatorName], { domain: 'todo' });
|
|
3763
4042
|
}
|
|
4043
|
+
// ★越权防护:按人筛选(--creator / resume 路径)目标必须 ∈ {本人 + 全部层级下属}
|
|
4044
|
+
if (creatorFilter)
|
|
4045
|
+
await assertUserIdsInScope(cfg, [creatorFilter], { domain: 'todo' });
|
|
3764
4046
|
const queryFlag = (key) => {
|
|
3765
4047
|
const current = flagStr(args, key);
|
|
3766
4048
|
if (current !== undefined)
|
|
@@ -3994,6 +4276,25 @@ async function runTodoRetract(args) {
|
|
|
3994
4276
|
console.log(` 请在 OA 中核对流程状态:${buildRequestFormUrl(endpointFor(cfg).baseUrl, requestId)}`);
|
|
3995
4277
|
console.log(BOX_TOP);
|
|
3996
4278
|
}
|
|
4279
|
+
async function runWhoami(args) {
|
|
4280
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
4281
|
+
const u = await getCurrentUser(cfg);
|
|
4282
|
+
const admin = await getCurrentUserIsAdmin(cfg);
|
|
4283
|
+
if (args.flags.json) {
|
|
4284
|
+
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));
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
console.log(`${BOX_TOP}`);
|
|
4288
|
+
console.log(`当前登录人 ${u.username || '?'}(${u.userid || '?'})`);
|
|
4289
|
+
console.log(DIVIDER);
|
|
4290
|
+
console.log(` 部门: ${u.deptname || '?'}(${u.deptid || '?'})`);
|
|
4291
|
+
if (u.subcompanyname)
|
|
4292
|
+
console.log(` 分部: ${u.subcompanyname}`);
|
|
4293
|
+
if (u.jobs)
|
|
4294
|
+
console.log(` 岗位: ${u.jobs}`);
|
|
4295
|
+
console.log(` 系统管理员: ${admin.isAdmin === true ? '✅ 是' : admin.isAdmin === false ? '否' : '⚠ 无法确认(未取到 isAdmin 字段)'}`);
|
|
4296
|
+
console.log(`${BOX_TOP}`);
|
|
4297
|
+
}
|
|
3997
4298
|
function runConfig(args) {
|
|
3998
4299
|
const sub = args._[1];
|
|
3999
4300
|
if (sub === 'set') {
|