weaver-e9-assistant-cli 1.1.3 → 1.1.5

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/cli.js CHANGED
@@ -3,7 +3,8 @@
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, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE } from './workflow.js';
6
+ import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE, parseTimeRange } from './workflow.js';
7
+ import { annotateNodeNames, isNodeNameDisabled, resolveNodeByName } from './nodemap.js';
7
8
  import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
8
9
  import { pageUrl, availablePages } from './pageopen.js';
9
10
  import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
@@ -12,7 +13,7 @@ import { loadSubmission, updateSubmission } from './submissionstate.js';
12
13
  import { saveSubmissionPlan, loadSubmissionPlan } from './submissionplan.js';
13
14
  import { saveCandidateQuery, loadCandidateQuery, persistSearchResult } from './querystate.js';
14
15
  import { getFlowSummary } from './wfsummary.js';
15
- import { submitTodo, retractRequest } from './todosubmit.js';
16
+ import { submitTodo, retractRequest, validateFieldValuesAtNode } from './todosubmit.js';
16
17
  import { rejectTodo } from './todoreject.js';
17
18
  import { transferTodo } from './todotransfer.js';
18
19
  import { forwardTodo, circulateTodo } from './todoforward.js';
@@ -20,18 +21,20 @@ import { ccTodo } from './todocc.js';
20
21
  import { askIdeaTodo } from './todoaskidea.js';
21
22
  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
23
  import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
23
- import { describeFormFields } from './wfform.js';
24
+ import { describeFormFields, VIEWATTR_LABEL } from './wfform.js';
24
25
  import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
25
26
  import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus } from './auth.js';
26
27
  import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
27
28
  import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel, exportWorkplanExcel } from './workplan.js';
28
29
  import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
29
- import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from './kq.js';
30
+ import { getSuperviseTree, getSuperviseCount, getSuperviseConditions, listSupervise, doSupervise, resolveSuperviseTab, SUPERVISE_TABS } from './supervise.js';
31
+ import { getAgentList, getAgentConditions, listAgentTemplates, getAddAgentInfo, verifyOverlap, saveAgentTemplate, addAgent, takeBackAgent, takeBackAllAgent, getAgentDetail, editAgent, editAgentBatch, getBatchEditInfo, buildAgentBody, resolveView as resolveWfagentView, resolveStatus as resolveWfagentStatus, WFAGENT_VIEWS, WFAGENT_STATUS } from './wfagent.js';
32
+ import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj, getKqReport, getKqMonthReport } from './kq.js';
30
33
  import { collectQueryableBusiness } from './report.js';
31
34
  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
35
  import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
33
- import { getSubordinates, assertUserIdsInScope, assertNamesInScope, isScopeEnforced } from './subordinate.js';
34
- import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds } from './hrm.js';
36
+ import { getSubordinates, assertUserIdsInScope, assertNamesInScope, resolveNameToUid, isScopeEnforced } from './subordinate.js';
37
+ import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds, expandHrmOrgTree } from './hrm.js';
35
38
  import { parseArgs, flagStr, flagBool } from './args.js';
36
39
  import { todayStr } from './util.js';
37
40
  import { BOX_TOP, DIVIDER } from './render.js';
@@ -46,8 +49,8 @@ function usage() {
46
49
  ' [--workflow <id>] [--creator <id>] [--creator-dept <id>] [--creator-subcom <id>]',
47
50
  ' [--create-time <range>] [--receive-time <range>] [--operate-time <range>]',
48
51
  ' [--request-level <level>] [--request-mark <code>] [--workflow-type <type>]',
49
- ' [--node-id <id>] [--flow-status <status>] [--sort <spec>] [--sub-category <tab>]',
50
- ' [--total-only] [--ungrouped] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
52
+ ' [--node-id <id> | --node-name <节点名>] [--flow-status <status>] [--sort <spec>] [--sub-category <tab>]',
53
+ ' [--total-only] [--ungrouped] [--no-node-name] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
51
54
  ' e9 todo summary --requestid <id> 查看流程摘要(表单字段+值+流转历史)',
52
55
  ' e9 todo submit --requestid <id> [--remark "同意"] [--yes] 提交待办到下一节点(默认签字 OK)',
53
56
  ' e9 todo reject --requestid <id> [--remark "退回"] [--reject-to-node <nodeid>] [--reject-to-type <t>] [--reject-node <nodes>] [--yes] 退回/驳回待办',
@@ -58,7 +61,7 @@ function usage() {
58
61
  ' e9 todo ask-idea --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "意见"] [--yes] 意见征询(向某人征询意见,不改审批人)',
59
62
  ' e9 todo batch-submit --request-ids "1,2" --user-ids "u1,u2" [--remark "同意"] [--yes] 批量提交(先预览)',
60
63
  ' e9 todo batch-forward --request-ids "1,2" --receiver <uid> [--remark "转发"] [--yes] 批量转发(先预览)',
61
- ' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
64
+ ' e9 todo batch-attention --request-ids "1,2" [--category <todo|done|mine>] [--attention-id <类型id>] [--yes] 批量关注(先预览;--category 选在哪类列表定位,默认待办)',
62
65
  ' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
63
66
  ' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
64
67
  ' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
@@ -106,7 +109,7 @@ function usage() {
106
109
  ' e9 email recall --id <邮件id> [--yes] 撤回已发邮件',
107
110
  ' e9 email attachment upload --file <路径> 上传附件拿 fileid;attachment list 附件列表',
108
111
  ' e9 email size 邮箱容量/未读统计;email accounts 接收账户列表',
109
- ' e9 workplan list [--date <YYYY-MM-DD>] [--view-type <1|3>] [--group <部门|分部|下级|...>] 日程列表(日历数据)',
112
+ ' e9 workplan list [--date <YYYY-MM-DD>] [--view-type <1当天|2周|3月>] [--group <部门|分部|下级|...>] 日程列表(默认我的日程;--group 走组织视图)',
110
113
  ' e9 workplan my-calendar [--date <YYYY-MM-DD>] [--view-type <1|3>] 日程月视图(MyCalendar,浏览器主用)',
111
114
  ' e9 workplan view --id <id> 查看日程详情',
112
115
  ' e9 workplan finish --id <id> [--yes] 完成日程',
@@ -120,11 +123,28 @@ function usage() {
120
123
  ' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
121
124
  ' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
122
125
  ' e9 workplan export [--type <list|html>] [--group <部门|分部|下级|...>] [--date <YYYY-MM-DD>] [--workplan-type <id>] [--out-dir <目录>] [--yes] 导出日程(list=日程清单 / html=日程一览表,默认 dry-run)',
126
+ ' e9 supervise list [--tab <全部|未督办|已督办|我的关注>] [--keyword <text>] [--page <n>] [--json] 流程督办列表',
127
+ ' e9 supervise count [--json] 流程督办计数(总数/未督办)',
128
+ ' e9 supervise conditions [--json] 流程督办可用搜索条件',
129
+ ' e9 supervise send --requestids "1,2" [--remark "请尽快处理"] [--yes] 发起督办(默认 dry-run)',
130
+ ' e9 wfagent list [--view <mine|all|records>] [--status <全部|未结束|未开始|代理中|收回中|已结束>]',
131
+ ' [--agent <代理人>] [--be-agent <被代理人>] [--begin <日期|"起 止">] [--end <日期|"起 止">]',
132
+ ' [--page <n>] [--json] 流程代理列表(mine=我设置的 / all=全部 / records=代理办理记录)',
133
+ ' e9 wfagent conditions [--view <mine|all|records>] [--json] 流程代理可用搜索条件',
134
+ ' e9 wfagent templates [--name <名>] [--workflow <id>] [--page <n>] [--json] 代理模板列表',
135
+ ' e9 wfagent create --agent <代理人> --be-agent <被代理人> --begin <YYYY-MM-DD> --end <YYYY-MM-DD>',
136
+ ' [--range 全部 | --workflow <流程id,id>] [--save-template <模板名>] [--yes] 新建代理(默认 dry-run)',
137
+ ' e9 wfagent edit --agent-ids "537,538" [--begin <日期>] [--end <日期>] [--time <HH:MM-HH:MM>] [--yes] 批量编辑(只改起止日期/时间)',
138
+ ' e9 wfagent edit --agent-id <代理id> [--end <YYYY-MM-DD>] [--begin <YYYY-MM-DD>] [--time <HH:MM-HH:MM>] [--yes] 编辑代理(默认 dry-run,显示改前→改后)',
139
+ ' e9 wfagent takeback --agent-ids "69,70" [--back-running <0|1>] [--yes] 收回代理(默认 dry-run;id 取列表的「代理id」)',
140
+ ' e9 wfagent takeback --all [--be-agent <被代理人>] [--yes] 收回该被代理人名下**全部**代理(默认 dry-run,批量)',
123
141
  ' e9 odoc todo [--page <n>] [--subject <标题>] [--dispatch-num <发文字号>] [--type <公文种类id>] [--topic <主题分类id>] 公文待办列表(getCustomPageList table,每页 10 条)',
124
142
  ' e9 odoc count [--view] 公文待办/待阅计数(发文/收文/签报)',
125
143
  ' e9 odoc create-list [--type 1|2] 发文/收文流程列表(1=发文拟稿 2=收文登记,复用 create start 发起)',
126
144
  ' e9 kq punch on|off [--group-id <id>] [--userid <id>] [--date <YYYY-MM-DD>] [--yes] 打卡(上班/下班卡,默认 dry-run 展示 encryptObj,--yes 真打)',
127
145
  ' e9 kq status 查看今日打卡班次(getButtons timeline)',
146
+ ' e9 kq report [--user <id>|--user-name <姓名>] [--month YYYY-MM|--year YYYY] [--json] 考勤统计 + 假期余额(默认本人·本月)',
147
+ ' e9 kq detail [--user <id>|--user-name <姓名>] [--month YYYY-MM] [--json] 月度考勤明细(签到签退时间/IP)',
128
148
  ' e9 cube list --customid <id> [--page <n>] [--keyword <text>] [--condition <k=v;...>] [--json] 建模台账列表(--condition 支持中文字段名自动映射 domkey)',
129
149
  ' e9 cube fields --customid <id> 建模查询条件探查(数据状态/筛选字段)',
130
150
  ' e9 cube export --customid <id> [--filename <名>] [--condition <k=v;...>] [--keyword <text>] [--out-dir <目录>] [--plain] [--yes] 导出建模台账为 Excel(默认 dry-run;--plain 明文 modeCustomid 逃生口,默认 base64)',
@@ -146,6 +166,7 @@ function usage() {
146
166
  ' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
147
167
  ' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
148
168
  ' e9 subordinate list [--name <姓名>] [--all-level] [--page <n>] [--json] [--raw] [--refresh] 查询我的下属(默认直接下属;--all-level 全部层级)',
169
+ ' e9 hrm tree [--id <组织id>] [--type <1分部|2部门>] [--depth <n>] [--dept <部门名|id>] [--subcompany <分部名|id>] [--json] 组织架构树',
149
170
  ' e9 hrm tabs 列出人事搜索范围(全部人员/同部门/我的下属)',
150
171
  ' e9 hrm conditions [--tab <全部人员|同部门|我的下属>] 列出该范围可用搜索条件(label→domkey)',
151
172
  ' e9 hrm search [--tab <全部人员|同部门|我的下属>] [--name <姓名>] [--condition "岗位=经理;性别=男"]',
@@ -264,7 +285,7 @@ function usage() {
264
285
  ' email size 邮箱容量统计;email accounts 接收账户列表',
265
286
  ' workplan list 日程列表(POST /api/workplan/calendar/getCalendarData,支持组织视图)',
266
287
  ' --date 参考日期 YYYY-MM-DD,默认今天',
267
- ' --view-type 视图:1=当天 / 3=月视图(默认)',
288
+ ' --view-type 视图:1=当天 / 2=周 / 3=月(默认)',
268
289
  ' --group 组织视图:部门/分部/上级/下级/常用组/关注;不传=默认当前用户',
269
290
  ' --type 日程类型 id 筛选,留空=全部',
270
291
  ' --json/--raw 同 todo',
@@ -345,7 +366,7 @@ function escapeLinkText(text) {
345
366
  return String(text).replace(/([\[\]()\\])/g, '\\$1');
346
367
  }
347
368
  /** 统一拼各域 list/search 项的 markdown 超链接;无 id/无可链目标时回退纯文本。
348
- * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'
369
+ * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'|'wfagent'
349
370
  * text: 展示文本(调用方已计算好回退值);ctx: cube 需 {modeId,formId,customid} */
350
371
  function hyperlinkFor(domain, item, base, text, ctx = {}) {
351
372
  const meta = linkMetaFor(domain, item, base, ctx);
@@ -360,6 +381,7 @@ function linkMetaFor(domain, item, base, ctx = {}) {
360
381
  switch (domain) {
361
382
  case 'todo':
362
383
  case 'odoc':
384
+ case 'wfagent': // 仅 records 视图的行有 requestId(代理产生的流程);mine/all 是代理配置、无流程详情页
363
385
  if (item?.requestId)
364
386
  url = buildRequestFormUrl(b, item.requestId);
365
387
  urlType = url ? 'detail' : '';
@@ -1747,15 +1769,16 @@ async function runBlog(args) {
1747
1769
  return { ...it, matchType, _sort: matchType === 'exact' ? 0 : matchType === 'prefix' ? 1 : 2 };
1748
1770
  });
1749
1771
  annotated.sort((a, b) => a._sort - b._sort);
1772
+ const atBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
1750
1773
  if (args.flags.json) {
1751
- console.log(JSON.stringify({ pageIndex: r.pageIndex, pageSize: r.pageSize, noMore: r.noMore, query: q || undefined, items: annotated.map(({ _sort, ...rest }) => rest) }, null, 2));
1774
+ console.log(JSON.stringify({ pageIndex: r.pageIndex, pageSize: r.pageSize, noMore: r.noMore, query: q || undefined, items: annotated.map(({ _sort, ...rest }) => withLink('hrm', { id: rest.uid, name: rest.name }, atBase, rest)) }, null, 2));
1752
1775
  return;
1753
1776
  }
1754
1777
  console.log(`E9 微博 @ 提及用户列表 [第 ${r.pageIndex} 页]${r.noMore ? '' : '(还有更多)'} 共 ${annotated.length} 条${q ? ` · 搜索「${flagStr(args, 'name')}」` : ''}`);
1755
1778
  for (let i = 0; i < annotated.length; i++) {
1756
1779
  const it = annotated[i];
1757
1780
  const tag = it.matchType === 'exact' ? ' ★精确匹配' : it.matchType === 'prefix' ? ' ·前缀' : '';
1758
- console.log(`${i + 1}. ${it.name} (uid=${it.uid})${tag}`);
1781
+ console.log(`${i + 1}. ${hyperlinkFor('hrm', { id: it.uid }, atBase, it.name)} (uid=${it.uid})${tag}`);
1759
1782
  }
1760
1783
  return;
1761
1784
  }
@@ -1859,6 +1882,13 @@ async function runWorkplan(args) {
1859
1882
  const group = flagStr(args, 'group');
1860
1883
  const date = flagStr(args, 'date');
1861
1884
  const type = flagStr(args, 'type');
1885
+ // --view-type:1=当天 / 2=周 / 3=月(默认)。
1886
+ // 此前 list 分支把 viewType 写死为 '3',导致 --view-type 静默失效(传 1/2 都还是月视图,且不报错)。
1887
+ const viewType = flagStr(args, 'view-type') ?? '3';
1888
+ if (!['1', '2', '3'].includes(viewType)) {
1889
+ console.error(`workplan list 的 --view-type 只支持 1(当天)/ 2(周)/ 3(月),收到: ${viewType}`);
1890
+ process.exit(1);
1891
+ }
1862
1892
  // 默认(无 --group)=「我的日程」:查当前用户自己的日程,走 MyCalendar(selectUser 空=当前用户)。
1863
1893
  // 只有显式传 --group 才走组织视图 getCalendarData(部门/分部/下级等)。
1864
1894
  if (!group) {
@@ -1866,7 +1896,7 @@ async function runWorkplan(args) {
1866
1896
  // ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
1867
1897
  if (selectUser)
1868
1898
  await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
1869
- const r = await getMyCalendar(cfg, { selectDate: date, viewType: '3', selectUser: selectUser ?? '', workPlanType: type });
1899
+ const r = await getMyCalendar(cfg, { selectDate: date, viewType, selectUser: selectUser ?? '', workPlanType: type });
1870
1900
  if (args.flags.raw) {
1871
1901
  console.error('===== RAW MyCalendar =====');
1872
1902
  console.error(JSON.stringify(r.raw, null, 2));
@@ -1883,7 +1913,10 @@ async function runWorkplan(args) {
1883
1913
  }, null, 2));
1884
1914
  return;
1885
1915
  }
1886
- console.log(`E9 日程 [${r.selectDate}] 共 ${r.items.length} 条`);
1916
+ // 标明视图与覆盖区间——周/月视图下只看 selectDate 无法判断统计了哪几天
1917
+ const viewLabel = viewType === '1' ? '当天' : viewType === '2' ? '本周' : '本月';
1918
+ const range = r.range?.start && r.range?.end ? ` ${r.range.start} ~ ${r.range.end}` : '';
1919
+ console.log(`E9 日程 [${r.selectDate} · ${viewLabel}]${range} 共 ${r.items.length} 条`);
1887
1920
  if (r.items.length === 0) {
1888
1921
  console.log('(该区间无日程)');
1889
1922
  return;
@@ -1922,7 +1955,8 @@ async function runWorkplan(args) {
1922
1955
  objIds = bd.dept[0]?.id ?? '';
1923
1956
  }
1924
1957
  }
1925
- const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type });
1958
+ // 组织视图同样跟随 --view-type(getCalendarData 的 bywhat:1=日 / 2=周 / 3=月)
1959
+ const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type, bywhat: viewType });
1926
1960
  if (args.flags.raw) {
1927
1961
  console.error('===== RAW getCalendarData =====');
1928
1962
  console.error(JSON.stringify(r.raw, null, 2));
@@ -1945,7 +1979,11 @@ async function runWorkplan(args) {
1945
1979
  return;
1946
1980
  }
1947
1981
  const groupLabel = ` · ${group}`;
1948
- console.log(`E9 日程 [${r.selectDate}${groupLabel}] 共 ${r.count} 条`);
1982
+ // 组织视图无 start/end 字段,区间从 days.value 推导(首日~末日)
1983
+ const gDays = r.days?.value ?? [];
1984
+ const gRange = gDays.length ? ` ${gDays[0]} ~ ${gDays[gDays.length - 1]}` : '';
1985
+ const gViewLabel = viewType === '1' ? '当天' : viewType === '2' ? '本周' : '本月';
1986
+ console.log(`E9 日程 [${r.selectDate} · ${gViewLabel}${groupLabel}]${gRange} 共 ${r.count} 条`);
1949
1987
  if (r.count === 0) {
1950
1988
  console.log('(该区间无日程)');
1951
1989
  return;
@@ -2297,6 +2335,560 @@ async function runWorkplan(args) {
2297
2335
  console.error(`workplan 暂只支持 list / view / finish / delete / types / search-conditions / search / create / my-calendar / attention / base-data / export / import-template / import,收到: ${sub}`);
2298
2336
  process.exit(1);
2299
2337
  }
2338
+ async function runSupervise(args) {
2339
+ const sub = args._[1] ?? 'list';
2340
+ const cfg = loadConfig(flagStr(args, 'config'));
2341
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2342
+ if (sub === 'count') {
2343
+ const r = await getSuperviseCount(cfg);
2344
+ if (args.flags.json) {
2345
+ console.log(JSON.stringify({ total: r.total, pending: r.pending }, null, 2));
2346
+ return;
2347
+ }
2348
+ console.log(`E9 流程督办计数:共 ${r.total ?? '?'} 条,其中未督办 ${r.pending ?? '?'} 条。`);
2349
+ return;
2350
+ }
2351
+ if (sub === 'conditions') {
2352
+ const r = await getSuperviseConditions(cfg);
2353
+ if (args.flags.json) {
2354
+ console.log(JSON.stringify(r.groups, null, 2));
2355
+ return;
2356
+ }
2357
+ console.log('E9 流程督办 搜索条件:');
2358
+ for (const g of r.groups) {
2359
+ console.log(` ${g.title}:`);
2360
+ for (const it of g.items)
2361
+ console.log(` - ${it.label} (${it.domkey}${it.type ? `, ${it.type}` : ''})`);
2362
+ }
2363
+ return;
2364
+ }
2365
+ if (sub === 'send') {
2366
+ const requestIds = flagStr(args, 'requestids') ?? '';
2367
+ if (!requestIds) {
2368
+ console.error('supervise send 需要 --requestids "id1,id2"');
2369
+ process.exit(1);
2370
+ }
2371
+ const remark = flagStr(args, 'remark') ?? '';
2372
+ const r = await doSupervise(cfg, { requestIds, remark, send: !!args.flags.yes });
2373
+ if (!r.sent) {
2374
+ console.log(BOX_TOP);
2375
+ console.log(` [dry-run] 将发起督办(${r.requestIds.length} 条)`);
2376
+ console.log(DIVIDER);
2377
+ console.log(` requestids = ${r.requestIds.join(', ')}`);
2378
+ console.log(` 督办意见 = ${r.remark || '(空)'}`);
2379
+ console.log(' 确认无误后加 --yes 真正发起督办。');
2380
+ console.log(BOX_TOP);
2381
+ return;
2382
+ }
2383
+ const j = r.response?.json;
2384
+ const ok = j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true;
2385
+ console.log(`${ok ? '✅' : '⚠'} 督办已请求处理(${r.requestIds.length} 条) HTTP ${r.response?.status}`);
2386
+ if (j)
2387
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
2388
+ return;
2389
+ }
2390
+ if (sub !== 'list') {
2391
+ console.error(`supervise 暂只支持 list / count / conditions / send,收到: ${sub}`);
2392
+ process.exit(1);
2393
+ }
2394
+ const tabSpec = flagStr(args, 'tab');
2395
+ const tabKey = resolveSuperviseTab(tabSpec);
2396
+ if (tabKey === undefined) {
2397
+ console.error(`未知督办页签「${tabSpec}」,可选:${Object.keys(SUPERVISE_TABS).join(' / ')}`);
2398
+ process.exit(1);
2399
+ }
2400
+ const r = await listSupervise(cfg, {
2401
+ tabKey,
2402
+ keyword: flagStr(args, 'keyword'),
2403
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2404
+ pageSize: flagStr(args, 'page-size'),
2405
+ creator: flagStr(args, 'creator'),
2406
+ creatorDept: flagStr(args, 'creator-dept'),
2407
+ creatorSubcom: flagStr(args, 'creator-subcom'),
2408
+ });
2409
+ const tabName = Object.keys(SUPERVISE_TABS).find((k) => SUPERVISE_TABS[k] === tabKey) ?? tabKey;
2410
+ if (args.flags.raw) {
2411
+ console.error('===== RAW getSessionkey =====');
2412
+ console.error(JSON.stringify(r.raw.getSessionkey, null, 2));
2413
+ console.error('===== RAW datas =====');
2414
+ console.error(JSON.stringify(r.raw.datas, null, 2));
2415
+ }
2416
+ if (args.flags.json) {
2417
+ console.log(JSON.stringify({
2418
+ tab: tabName, tabKey, page: r.page, pageSize: r.pageSize, total: r.total, count: r.items.length,
2419
+ items: r.items.map((it) => withLink('todo', it, base)),
2420
+ }, null, 2));
2421
+ return;
2422
+ }
2423
+ console.log(`E9 流程督办 [${tabName}] 第 ${r.page} 页 共 ${r.total ?? '?'} 条`);
2424
+ if (!r.items.length) {
2425
+ console.log('(无数据)');
2426
+ return;
2427
+ }
2428
+ for (let i = 0; i < r.items.length; i++) {
2429
+ const it = r.items[i];
2430
+ console.log(`${i + 1}. ${hyperlinkFor('todo', it, base, it.title || '(无标题)')}`);
2431
+ const meta = [it.creator, it.node, it.createTime, it.unoperators ? `未操作者=${it.unoperators}` : null].filter(Boolean).join(' | ');
2432
+ if (meta)
2433
+ console.log(` ${meta}`);
2434
+ }
2435
+ if (r.total != null && r.items.length < r.total)
2436
+ console.log(`还有 ${r.total - r.items.length} 条,用 --page <页码> 继续。`);
2437
+ }
2438
+
2439
+ /**
2440
+ * 流程代理(wfagent):list / conditions / templates / create。
2441
+ * ⚠️ 编辑、收回、批量编辑、模板删除在 E9 里存在但**未抓到包**,本域未实现(见 references/wfagent.md)。
2442
+ * ⚠️ create 的 4 个开关(isCreateAgenter/isProxyDeal/isSysCreateAgenter/isPendThing)语义未校准,
2443
+ * 默认值 1 照抄抓包样本,dry-run 预览里会显式打印,可用对应命令行开关覆盖。
2444
+ */
2445
+ async function runWfagent(args) {
2446
+ const sub = args._[1] ?? 'list';
2447
+ const cfg = loadConfig(flagStr(args, 'config'));
2448
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2449
+ const viewSpec = flagStr(args, 'view');
2450
+ const view = resolveWfagentView(viewSpec);
2451
+ if (view === undefined) {
2452
+ console.error(`未知代理视图「${viewSpec}」,可选:${Object.keys(WFAGENT_VIEWS).join(' / ')}(mine=我设置的 / all=全部 / records=代理办理记录)`);
2453
+ process.exit(1);
2454
+ }
2455
+
2456
+ if (sub === 'conditions') {
2457
+ const r = await getAgentConditions(cfg, { view });
2458
+ if (args.flags.json) {
2459
+ console.log(JSON.stringify(r.groups, null, 2));
2460
+ return;
2461
+ }
2462
+ console.log(`E9 流程代理 搜索条件(视图:${WFAGENT_VIEWS[view].title}):`);
2463
+ for (const g of r.groups) {
2464
+ console.log(` ${g.title}:`);
2465
+ for (const it of g.items)
2466
+ console.log(` - ${it.label} (${it.domkey}${it.type ? `, ${it.type}` : ''})${it.options.length ? ` 可选:${it.options.map((o) => `${o.name}=${o.key}`).join(' / ')}` : ''}`);
2467
+ }
2468
+ return;
2469
+ }
2470
+
2471
+ if (sub === 'templates') {
2472
+ const r = await listAgentTemplates(cfg, {
2473
+ name: flagStr(args, 'name'),
2474
+ workflowId: flagStr(args, 'workflow'),
2475
+ agentUid: flagStr(args, 'agent'),
2476
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2477
+ });
2478
+ if (args.flags.json) {
2479
+ console.log(JSON.stringify({ page: r.page, total: r.total, count: r.items.length, items: r.items.map(({ raw, ...rest }) => rest) }, null, 2));
2480
+ return;
2481
+ }
2482
+ console.log(`E9 代理模板 第 ${r.page} 页 共 ${r.total ?? '?'} 条`);
2483
+ if (!r.items.length) {
2484
+ console.log('(无数据)');
2485
+ return;
2486
+ }
2487
+ for (let i = 0; i < r.items.length; i++) {
2488
+ const it = r.items[i];
2489
+ // 模板没有可跳转的流程详情页,故不出链接(列表超链接规则只针对流程列表)
2490
+ console.log(`${i + 1}. ${it.name || '(未命名)'} [模板id=${it.templateId}]`);
2491
+ console.log(` 代理人=${it.agent || '?'} | 流程范围=${it.workflowRange || '?'}`);
2492
+ }
2493
+ return;
2494
+ }
2495
+
2496
+ if (sub === 'create') {
2497
+ const agentSpec = flagStr(args, 'agent');
2498
+ if (!agentSpec) {
2499
+ console.error('wfagent create 需要 --agent <代理人>(姓名或用户id)');
2500
+ process.exit(1);
2501
+ }
2502
+ // 代理人=委托对象,不限于下属 → 只走 at-list 姓名解析,不做授权范围过滤
2503
+ const agentUid = await resolveNameToUid(cfg, agentSpec);
2504
+ // 被代理人默认=当前登录人(getAddAgentInfo 的 formValue);显式指定他人时按越权策略校验
2505
+ const beAgentSpec = flagStr(args, 'be-agent');
2506
+ let beAgentUid = '';
2507
+ let beAgentLabel = '';
2508
+ if (beAgentSpec) {
2509
+ beAgentUid = await assertNamesInScope(cfg, [beAgentSpec], { domain: 'wfagent' });
2510
+ beAgentLabel = beAgentSpec;
2511
+ }
2512
+ else {
2513
+ const info = await getAddAgentInfo(cfg);
2514
+ beAgentUid = info.defaultBeAgentUid;
2515
+ beAgentLabel = info.defaultBeAgent || beAgentUid;
2516
+ if (!beAgentUid) {
2517
+ console.error('未能从 getAddAgentInfo 取到当前登录人作为被代理人,请用 --be-agent 显式指定。');
2518
+ process.exit(1);
2519
+ }
2520
+ }
2521
+ const { beginTime, endTime } = parseTimeSpec(flagStr(args, 'time'));
2522
+ const body = buildAgentBody({
2523
+ agentUid,
2524
+ beAgentUid,
2525
+ beginDate: flagStr(args, 'begin'),
2526
+ endDate: flagStr(args, 'end'),
2527
+ beginTime,
2528
+ endTime,
2529
+ workflowIds: flagStr(args, 'workflow'),
2530
+ range: flagStr(args, 'range'),
2531
+ isCreateAgenter: flagStr(args, 'agent-create-flow'),
2532
+ isProxyDeal: flagStr(args, 'proxy-deal'),
2533
+ isSysCreateAgenter: flagStr(args, 'sys-create'),
2534
+ isPendThing: flagStr(args, 'pend-thing'),
2535
+ });
2536
+ const overlap = await verifyOverlap(cfg, body);
2537
+ const templateName = flagStr(args, 'save-template');
2538
+ if (!args.flags.yes) {
2539
+ console.log(BOX_TOP);
2540
+ console.log(' [dry-run] 新建流程代理');
2541
+ console.log(DIVIDER);
2542
+ console.log(` 被代理人 = ${beAgentLabel}(${body.bagentuid})`);
2543
+ console.log(` 代理人 = ${agentSpec}(${body.agentuid})`);
2544
+ console.log(` 代理期间 = ${body.beginDate} ~ ${body.endDate}${beginTime || endTime ? ` ${beginTime || '00:00'}~${endTime || '23:59'}` : '(全天)'}`);
2545
+ console.log(` 流程范围 = ${body.agentrange === '2' ? '全部流程' : `指定流程 ${body.rangetype}`}`);
2546
+ if (body.agentrange === '2')
2547
+ console.log(' ⚠️ 「全部流程」= 会为**你的每一个流程各建一条**代理记录(实测本环境 466 条),**不是 1 条**!\n 只想试一条请改用 --workflow <单个流程id>。建议保存后到 OA「流程代理」页核对。');
2548
+ console.log(` 开关 = isCreateAgenter=${body.isCreateAgenter} isProxyDeal=${body.isProxyDeal} isSysCreateAgenter=${body.isSysCreateAgenter} isPendThing=${body.isPendThing}`);
2549
+ console.log(' 开关含义(据 workflow_agent 表结构校准):isCreateAgenter=代理创建权限 / isProxyDeal=流转中的待办 /');
2550
+ console.log(' isPendThing=是否把待办事宜转给代理人 / isSysCreateAgenter=系统创建代理标记。默认 1 来自抓包样本;');
2551
+ console.log(' 如需调整:--agent-create-flow / --proxy-deal / --sys-create / --pend-thing');
2552
+ console.log(` 重叠校验 = ${overlap.overlapCount ? `⚠️ 与 ${overlap.overlapCount} 条已有代理重叠(overlapIds=${overlap.overlapIds})` : '✅ 无重叠'}`);
2553
+ if (templateName)
2554
+ console.log(` 存为模板 = ${templateName}`);
2555
+ console.log(DIVIDER);
2556
+ console.log(` 完整请求体:${JSON.stringify(body)}`);
2557
+ console.log(BOX_TOP);
2558
+ console.log(' 确认无误后加 --yes 真正保存。');
2559
+ return;
2560
+ }
2561
+ let tplResult = null;
2562
+ if (templateName)
2563
+ tplResult = await saveAgentTemplate(cfg, body, templateName);
2564
+ const r = await addAgent(cfg, { body, send: true });
2565
+ const ok = /success/i.test(String(r.result ?? ''));
2566
+ console.log(`${ok ? '✅' : '⚠'} 代理保存已请求(agentresult=${r.result || '无'})`);
2567
+ if (tplResult)
2568
+ console.log(` 模板「${templateName}」:${tplResult.ok ? '✅ 已保存' : '⚠️ 未确认'}`);
2569
+ console.log(` 响应:${JSON.stringify(r.raw ?? {})}`);
2570
+ // 回读核验:到列表里找这条(找不回报「未确认」,不谎报成功)
2571
+ // ⚠️ 必须带 status=1(未结束)并**翻页**:按人筛选在 all 视图可能命中数百条已结束的历史,
2572
+ // 只翻第 1 页会漏掉刚建的那条,误报「回读未确认」。
2573
+ try {
2574
+ const want = (it) => it.agentUid === body.agentuid && it.beginDate === body.beginDate && it.endDate === body.endDate;
2575
+ let hit = null;
2576
+ for (let page = 1; page <= 5 && !hit; page++) {
2577
+ const check = await getAgentList(cfg, { view: 'all', agent: body.agentuid, beAgent: body.bagentuid, status: '1', page, pageSize: '100' });
2578
+ hit = (check.items || []).find(want) ?? null;
2579
+ if ((check.items || []).length < 100)
2580
+ break;
2581
+ }
2582
+ console.log(hit
2583
+ ? ` ✅ 已核验:列表中出现该条代理(状态 ${hit.status || '?'})`
2584
+ : ' ⚠️ 回读未确认:列表中暂未查到该条,请到 OA「流程代理」页核对。');
2585
+ }
2586
+ catch (e) {
2587
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2588
+ }
2589
+ return;
2590
+ }
2591
+
2592
+ if (sub === 'edit') {
2593
+ // --agent-ids(≥2 条)走**批量**编辑(只改起止日期/时间);--agent-id 走单条编辑
2594
+ const batchSpec = flagStr(args, 'agent-ids');
2595
+ if (batchSpec) {
2596
+ const time = parseTimeSpec(flagStr(args, 'time'));
2597
+ const opts = {
2598
+ agentIds: batchSpec,
2599
+ beginDate: flagStr(args, 'begin'),
2600
+ endDate: flagStr(args, 'end'),
2601
+ ...(time.beginTime || time.endTime ? { beginTime: time.beginTime, endTime: time.endTime } : {}),
2602
+ };
2603
+ const r = await editAgentBatch(cfg, { ...opts, send: !!args.flags.yes });
2604
+ if (!r.sent) {
2605
+ console.log(BOX_TOP);
2606
+ console.log(` [dry-run] 批量编辑代理(${r.body.agentids.split(',').length} 条)`);
2607
+ console.log(DIVIDER);
2608
+ console.log(` 代理id = ${r.body.agentids}`);
2609
+ console.log(` 起止日期 = ${r.body.beginDate || '(空)'} ~ ${r.body.endDate || '(空)'}`);
2610
+ console.log(` 起止时间 = ${r.body.beginTime || '(空)'} ~ ${r.body.endTime || '(空)'}`);
2611
+ console.log(' ⚠️ 批量编辑是**整体覆盖**这 4 个字段——只改一部分请用单条编辑(--agent-id)。');
2612
+ console.log(` 重叠校验 = ${r.overlap.overlapCount ? `⚠️ 与 ${r.overlap.overlapCount} 条已有代理重叠(overlapIds=${r.overlap.overlapIds})` : '✅ 无重叠'}`);
2613
+ console.log(DIVIDER);
2614
+ console.log(` 完整请求体:${JSON.stringify(r.body)}`);
2615
+ console.log(BOX_TOP);
2616
+ console.log(' 确认无误后加 --yes 真正保存。');
2617
+ return;
2618
+ }
2619
+ console.log(`${/success/i.test(r.result) ? '✅' : '⚠'} 批量编辑已保存(agentresult=${r.result || '无'})`);
2620
+ try {
2621
+ const ids = r.body.agentids.split(',');
2622
+ const rows = await getAgentList(cfg, { view: 'mine', status: '1', page: 1, pageSize: '100' });
2623
+ for (const id of ids) {
2624
+ const hit = (rows.items || []).find((it) => String(it.agentId) === String(id));
2625
+ console.log(hit
2626
+ ? ` 代理 ${id}:起止 = ${hit.beginDate} ~ ${hit.endDate} 状态 = ${hit.status}`
2627
+ : ` 代理 ${id}:⚠️ 生效中的列表里没找到(可能被改到「已结束」)`);
2628
+ }
2629
+ const allHit = ids.every((id) => (rows.items || []).some((it) => String(it.agentId) === String(id)));
2630
+ console.log(allHit ? ' ✅ 已核验:所选代理均已出现并生效。' : ' ⚠️ 回读未确认,请核查。');
2631
+ }
2632
+ catch (e) {
2633
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2634
+ }
2635
+ return;
2636
+ }
2637
+ const agentId = flagStr(args, 'agent-id');
2638
+ if (!agentId) {
2639
+ console.error('wfagent edit 需要 --agent-id <单条> 或 --agent-ids "537,538" <批量>');
2640
+ process.exit(1);
2641
+ }
2642
+ // 编辑 = 取详情 → 改字段 → 原样回吐,所以先拿详情(它同时也是 dry-run 的"改前")
2643
+ const detail = await getAgentDetail(cfg, agentId);
2644
+ const time = parseTimeSpec(flagStr(args, 'time'));
2645
+ const overrides = {
2646
+ beginDate: flagStr(args, 'begin'),
2647
+ endDate: flagStr(args, 'end'),
2648
+ ...(time.beginTime || time.endTime ? { beginTime: time.beginTime, endTime: time.endTime } : {}),
2649
+ isCreateAgenter: flagStr(args, 'agent-create-flow'),
2650
+ isProxyDeal: flagStr(args, 'proxy-deal'),
2651
+ isSysCreateAgenter: flagStr(args, 'sys-create'),
2652
+ isPendThing: flagStr(args, 'pend-thing'),
2653
+ };
2654
+ const fmtSpan = (d, t) => `${d}${t ? ` ${t}` : ''}`;
2655
+ const beforeSpan = `${fmtSpan(detail.current.beginDate, detail.current.beginTime)} ~ ${fmtSpan(detail.current.endDate, detail.current.endTime)}`;
2656
+ if (!Object.values(overrides).some((v) => v !== undefined)) {
2657
+ console.log(`代理 ${agentId}(${detail.current.workflowName})当前值:`);
2658
+ console.log(` 代理期间 = ${beforeSpan}`);
2659
+ console.log(' 没给任何要改的字段。可改:--begin / --end / --time / --agent-create-flow / --proxy-deal / --sys-create / --pend-thing');
2660
+ console.log(' ⚠️ 代理人、流程范围不可改(要换请先 takeback 再 create)。');
2661
+ return;
2662
+ }
2663
+ const r = await editAgent(cfg, { agentId, detail, ...overrides, send: !!args.flags.yes });
2664
+ if (!r.sent) {
2665
+ console.log(BOX_TOP);
2666
+ console.log(` [dry-run] 编辑代理 ${agentId}(${detail.current.workflowName})`);
2667
+ console.log(DIVIDER);
2668
+ console.log(` 代理期间 = ${beforeSpan} ← 改前`);
2669
+ console.log(` = ${fmtSpan(r.body.beginDate, r.body.beginTime)} ~ ${fmtSpan(r.body.endDate, r.body.endTime)} ← 改后`);
2670
+ console.log(` 开关 = isCreateAgenter=${r.body.row_0_isCreateAgenter} isProxyDeal=${r.body.row_0_isProxyDeal} isSysCreateAgenter=${r.body.row_0_isSysCreateAgenter} isPendThing=${r.body.row_0_isPendThing}`);
2671
+ console.log(` 重叠校验 = ${r.overlap.overlapCount ? `⚠️ 与 ${r.overlap.overlapCount} 条已有代理重叠(overlapIds=${r.overlap.overlapIds})` : '✅ 无重叠'}`);
2672
+ console.log(DIVIDER);
2673
+ console.log(` 完整请求体:${JSON.stringify(r.body)}`);
2674
+ console.log(BOX_TOP);
2675
+ console.log(' 确认无误后加 --yes 真正保存。');
2676
+ return;
2677
+ }
2678
+ console.log(`${/success/i.test(r.result) ? '✅' : '⚠'} 编辑已保存(agentresult=${r.result || '无'})`);
2679
+ try {
2680
+ const rows = await getAgentList(cfg, { view: 'mine', status: '1', page: 1, pageSize: '100' });
2681
+ const hit = (rows.items || []).find((it) => String(it.agentId) === String(agentId));
2682
+ if (!hit) {
2683
+ console.log(' ⚠️ 回读未确认:生效中的列表里没找到该条(可能已被改到「已结束」)。');
2684
+ }
2685
+ else {
2686
+ const beginOk = !overrides.beginDate || String(hit.beginDate).startsWith(String(overrides.beginDate));
2687
+ const endOk = !overrides.endDate || String(hit.endDate) === String(overrides.endDate);
2688
+ console.log(` 回读:起止 = ${hit.beginDate} ~ ${hit.endDate} 状态 = ${hit.status}`);
2689
+ console.log(beginOk && endOk ? ' ✅ 已核验:改后的日期与提交一致。' : ' ⚠️ 回读与提交不一致,请核查。');
2690
+ }
2691
+ }
2692
+ catch (e) {
2693
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2694
+ }
2695
+ return;
2696
+ }
2697
+
2698
+ if (sub === 'takeback') {
2699
+ // --all:收回该被代理人名下的全部代理(界面「收回全部代理」,symbol=pt)
2700
+ if (args.flags.all === true) {
2701
+ const beAgentSpec = flagStr(args, 'be-agent');
2702
+ let beAgentUid = '';
2703
+ if (beAgentSpec)
2704
+ beAgentUid = await resolveNameToUid(cfg, beAgentSpec);
2705
+ else
2706
+ beAgentUid = (await getAddAgentInfo(cfg)).defaultBeAgentUid;
2707
+ if (!beAgentUid) {
2708
+ console.error('收回全部需要被代理人(--be-agent <姓名|id>),且未能从 getAddAgentInfo 取到当前登录人。');
2709
+ process.exit(1);
2710
+ }
2711
+ const backRunning = flagStr(args, 'back-running');
2712
+ const r = await takeBackAllAgent(cfg, { beAgentUid, backRunning, send: !!args.flags.yes });
2713
+ if (!r.sent) {
2714
+ console.log(BOX_TOP);
2715
+ console.log(' [dry-run] 将收回该被代理人名下的**全部**代理');
2716
+ console.log(DIVIDER);
2717
+ console.log(` 被代理人 = ${beAgentSpec || beAgentUid}(${r.beAgentUid})`);
2718
+ console.log(` backRunning= ${r.backRunning}(1=连流转中的一并收回)`);
2719
+ console.log(` 请求体 = symbol=pt & agentid=<自动取该被代理人名下第一条生效中的代理> & bagentuid=${r.beAgentUid} & backRunning=${r.backRunning}`);
2720
+ console.log(' ⚠️ 这是**批量**动作,会把这个被代理人名下所有代理一并标记「已结束」。');
2721
+ console.log(' 确认无误后加 --yes 真正收回。');
2722
+ console.log(BOX_TOP);
2723
+ return;
2724
+ }
2725
+ if (r.noop) {
2726
+ console.log(`该被代理人(${r.beAgentUid})名下没有生效中的代理,无需收回。`);
2727
+ return;
2728
+ }
2729
+ console.log(`${r.flag ? '✅' : '⚠'} 已发起收回全部代理(锚点 agentid=${r.agentId},flag=${r.flag})`);
2730
+ try {
2731
+ const left = await getAgentList(cfg, { view: 'all', beAgent: r.beAgentUid, status: '1', page: 1 });
2732
+ console.log(Number(left.total) === 0
2733
+ ? ` ✅ 回读核验:该被代理人名下生效中的代理已为 0 条。`
2734
+ : ` ⚠️ 回读核验:仍有 ${left.total} 条生效中的代理,请核查。`);
2735
+ }
2736
+ catch (e) {
2737
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2738
+ }
2739
+ return;
2740
+ }
2741
+ const idSpec = flagStr(args, 'agent-ids');
2742
+ if (!idSpec) {
2743
+ console.error('wfagent takeback 需要 --agent-ids "69,70"(列表里的「代理id」,逗号分隔多个),或 --all 收回全部');
2744
+ process.exit(1);
2745
+ }
2746
+ const ids = idSpec.split(',').map((s) => s.trim()).filter(Boolean);
2747
+ const backRunning = flagStr(args, 'back-running');
2748
+ const r = await takeBackAgent(cfg, {
2749
+ agentIds: ids.join(','),
2750
+ backRunning,
2751
+ send: !!args.flags.yes,
2752
+ // 逐条发;大批量时每 25 条报一次进度(走 stderr,不污染 --json)
2753
+ onProgress: (i, total, id) => {
2754
+ if (total > 25 && (i === 1 || i % 25 === 0))
2755
+ console.error(` 正在收回 ${i}/${total}(agentid=${id})…`);
2756
+ },
2757
+ });
2758
+ if (!r.sent) {
2759
+ console.log(BOX_TOP);
2760
+ console.log(` [dry-run] 将收回 ${ids.length} 条代理`);
2761
+ console.log(DIVIDER);
2762
+ console.log(` 代理id = ${ids.slice(0, 20).join(', ')}${ids.length > 20 ? ` …(共 ${ids.length} 个)` : ''}`);
2763
+ console.log(` backRunning= ${r.backRunning}(1=连流转中的一并收回)`);
2764
+ console.log(` 请求体 = symbol=it & agentid=<每个id> & backRunning=${r.backRunning}`);
2765
+ console.log(' 确认无误后加 --yes 真正收回。');
2766
+ console.log(BOX_TOP);
2767
+ return;
2768
+ }
2769
+ const failed = r.results.filter((x) => !x.flag);
2770
+ console.log(`${failed.length ? '⚠' : '✅'} 已发起收回 ${ids.length} 条:接口回 flag=true 的 ${r.okCount} 条${failed.length ? `,失败/未确认 ${failed.length} 条(${failed.slice(0, 5).map((x) => x.agentId).join(',')}${failed.length > 5 ? ' …' : ''})` : ''}`);
2771
+ // ⚠️ 收回是「把 agenttype 置 0(已结束)」,行**不会消失**;且少传 symbol 时接口回 true 却毫无效果
2772
+ // ——所以必须按**状态码**回读核验,不能看"行在不在"。
2773
+ try {
2774
+ const after = new Map();
2775
+ let page = 1;
2776
+ while (true) {
2777
+ const p = await getAgentList(cfg, { view: 'mine', page, pageSize: '100' });
2778
+ for (const it of p.items)
2779
+ after.set(String(it.agentId), it.statusCode);
2780
+ if (p.items.length < 100 || page > 20)
2781
+ break;
2782
+ page++;
2783
+ }
2784
+ const notDone = ids.filter((id) => after.get(String(id)) !== '0');
2785
+ console.log(notDone.length
2786
+ ? ` ⚠️ 回读核验:${ids.length - notDone.length}/${ids.length} 条已为「已结束」,仍有 ${notDone.length} 条没变(${notDone.slice(0, 8).join(',')}${notDone.length > 8 ? ' …' : ''})——请核查。`
2787
+ : ` ✅ 回读核验:${ids.length} 条均已标记「已结束」。`);
2788
+ }
2789
+ catch (e) {
2790
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2791
+ }
2792
+ return;
2793
+ }
2794
+
2795
+ if (sub !== 'list') {
2796
+ console.error(`wfagent 暂只支持 list / conditions / templates / create / edit / takeback,收到: ${sub}`);
2797
+ process.exit(1);
2798
+ }
2799
+ const statusSpec = flagStr(args, 'status');
2800
+ const status = resolveWfagentStatus(statusSpec);
2801
+ if (status === undefined) {
2802
+ console.error(`未知代理状态「${statusSpec}」,可选:${Object.keys(WFAGENT_STATUS).join(' / ')}`);
2803
+ process.exit(1);
2804
+ }
2805
+ // 列表的按人筛选是「查他人数据」→ 走范围校验(姓名经 blog at-list 解析,硬规则)
2806
+ const listAgentSpec = flagStr(args, 'agent');
2807
+ const listBeAgentSpec = flagStr(args, 'be-agent');
2808
+ const agentFilter = listAgentSpec ? await assertNamesInScope(cfg, [listAgentSpec], { domain: 'wfagent' }) : undefined;
2809
+ const beAgentFilter = listBeAgentSpec ? await assertNamesInScope(cfg, [listBeAgentSpec], { domain: 'wfagent' }) : undefined;
2810
+ const r = await getAgentList(cfg, {
2811
+ view,
2812
+ status,
2813
+ agent: agentFilter,
2814
+ beAgent: beAgentFilter,
2815
+ wftype: flagStr(args, 'wftype'),
2816
+ workflowPath: flagStr(args, 'workflow-path'),
2817
+ // 日期沿用仓库惯例:单值=当天,双值="起 止"(同 todo --create-time)
2818
+ beginFrom: parseTimeRange(flagStr(args, 'begin') ?? '').from,
2819
+ beginTo: parseTimeRange(flagStr(args, 'begin') ?? '').to,
2820
+ endFrom: parseTimeRange(flagStr(args, 'end') ?? '').from,
2821
+ endTo: parseTimeRange(flagStr(args, 'end') ?? '').to,
2822
+ dept: flagStr(args, 'dept'),
2823
+ subcom: flagStr(args, 'subcom'),
2824
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2825
+ pageSize: flagStr(args, 'page-size'),
2826
+ });
2827
+ // 代理办理记录是「流程列表」→ 复用节点名解析,把 currentnodeid(id)显示成节点名
2828
+ if (view === 'records')
2829
+ await annotateNodeNamesForCli(cfg, r.items, args);
2830
+ if (args.flags.raw) {
2831
+ console.error('===== RAW getList =====');
2832
+ console.error(JSON.stringify(r.raw.getList, null, 2));
2833
+ console.error('===== RAW datas =====');
2834
+ console.error(JSON.stringify(r.raw.datas, null, 2));
2835
+ console.error('===== RAW counts =====');
2836
+ console.error(JSON.stringify(r.raw.counts, null, 2));
2837
+ console.error('===== 解析结果 =====');
2838
+ }
2839
+ if (args.flags.json) {
2840
+ console.log(JSON.stringify({
2841
+ view,
2842
+ viewTitle: r.viewTitle,
2843
+ page: r.page,
2844
+ pageSize: r.pageSize,
2845
+ total: r.total,
2846
+ count: r.items.length,
2847
+ items: r.items.map((it) => withLink('wfagent', it, base)),
2848
+ }, null, 2));
2849
+ return;
2850
+ }
2851
+ console.log(`E9 流程代理 · ${r.viewTitle} 第 ${r.page} 页(每页 ${r.pageSize ?? 10} 条) 共 ${r.total ?? '?'} 条`);
2852
+ if (!r.items.length) {
2853
+ console.log('(无数据)');
2854
+ if (args.flags.raw)
2855
+ console.error('提示:用 --raw 查看真实响应校准解析。');
2856
+ return;
2857
+ }
2858
+ for (let i = 0; i < r.items.length; i++) {
2859
+ const it = r.items[i];
2860
+ if (view === 'records') {
2861
+ // 代理产生的流程:标题必须渲染成流程详情超链接(列表超链接硬规则)
2862
+ console.log(`${i + 1}. ${hyperlinkFor('wfagent', it, base, it.title || '(无标题)')}`);
2863
+ const meta = [
2864
+ it.principal ? `被代理人=${it.principal}` : null,
2865
+ it.handler ? `代理人=${it.handler}` : null,
2866
+ nodeLabel(it),
2867
+ it.creator ? `流程发起人=${it.creator}` : null,
2868
+ it.createTime,
2869
+ ].filter(Boolean).join(' | ');
2870
+ if (meta)
2871
+ console.log(` ${meta}`);
2872
+ }
2873
+ else {
2874
+ console.log(`${i + 1}. ${it.agent || '(未知代理人)'} ← 被代理人 ${it.beAgent || it.operator || '?'}`);
2875
+ const meta = [
2876
+ it.workflowName,
2877
+ it.workflowType,
2878
+ `${it.beginDate ?? '?'} ~ ${it.endDate ?? '?'}`,
2879
+ it.status,
2880
+ it.operator ? `创建人=${it.operator}` : null,
2881
+ it.agentId ? `代理id=${it.agentId}` : null,
2882
+ ].filter(Boolean).join(' | ');
2883
+ if (meta)
2884
+ console.log(` ${meta}`);
2885
+ }
2886
+ }
2887
+ const pageSize = r.pageSize ?? 10;
2888
+ if (r.total != null && r.page * pageSize < r.total)
2889
+ console.log(`还有更多,用 --page <页码> 继续(每页 ${pageSize} 条)。`);
2890
+ }
2891
+
2300
2892
  async function runOdoc(args) {
2301
2893
  const sub = args._[1] ?? 'todo';
2302
2894
  const cfg = loadConfig(flagStr(args, 'config'));
@@ -2395,6 +2987,7 @@ async function runOdoc(args) {
2395
2987
  customTabId: flagStr(args, 'custom-tab-id'),
2396
2988
  page: Number(flagStr(args, 'page') ?? '1') || 1,
2397
2989
  });
2990
+ await annotateNodeNamesForCli(cfg, r.items, args);
2398
2991
  if (args.flags.raw) {
2399
2992
  console.error('===== RAW getCustomPageList =====');
2400
2993
  console.error(JSON.stringify(r.raw.getCustomPageList, null, 2));
@@ -2419,8 +3012,9 @@ async function runOdoc(args) {
2419
3012
  odocTypeName: it.odocTypeName,
2420
3013
  issuedNum: it.issuedNum,
2421
3014
  receiveNum: it.receiveNum,
2422
- currentNodeId: it.currentNodeId,
2423
- currentNode: it.currentNode,
3015
+ stageName: it.stageName,
3016
+ node: it.node,
3017
+ nodeName: it.nodeName,
2424
3018
  createrId: it.createrId,
2425
3019
  createDate: it.createDate,
2426
3020
  lastOperateDate: it.lastOperateDate,
@@ -2442,10 +3036,12 @@ async function runOdoc(args) {
2442
3036
  const it = r.items[i];
2443
3037
  const nameLinked = hyperlinkFor('odoc', it, odocBase, it.requestName ?? '(无标题)');
2444
3038
  console.log(`${i + 1}. ${nameLinked}`);
3039
+ // 环节名(如「拟稿」)+ 当前节点(解析出名字时用「名字(id)」,否则退回 id)
3040
+ const nodeText = [it.stageName, nodeLabel(it)].filter(Boolean).join(' · ');
2445
3041
  const meta = [
2446
3042
  it.odocTypeName,
2447
3043
  it.issuedNum ? `发文字号=${it.issuedNum}` : null,
2448
- it.currentNodeId,
3044
+ nodeText || null,
2449
3045
  it.createrId ? `拟稿人=${it.createrId}` : null,
2450
3046
  it.createDate,
2451
3047
  it.lastOperateDate ? `最后修改=${it.lastOperateDate}` : null,
@@ -2566,7 +3162,88 @@ async function runKq(args) {
2566
3162
  console.log(BOX_TOP);
2567
3163
  return;
2568
3164
  }
2569
- console.error(`kq 暂只支持 status / punch,收到: ${sub}`);
3165
+ if (sub === 'report' || sub === 'detail') {
3166
+ const month = flagStr(args, 'month');
3167
+ const year = flagStr(args, 'year');
3168
+ if (month && year) {
3169
+ console.error('--month 与 --year 只能传一个。');
3170
+ process.exit(1);
3171
+ }
3172
+ // 目标人员:--user <id> / --user-name <姓名>;都不传 = 当前登录人(cookie loginuuids)
3173
+ let targetId = flagStr(args, 'user') ?? '';
3174
+ const userName = flagStr(args, 'user-name');
3175
+ if (!targetId && userName)
3176
+ targetId = await assertNamesInScope(cfg, [userName], { domain: 'kq' });
3177
+ if (!targetId)
3178
+ targetId = parseUserIdFromCookie(cfg.cookie);
3179
+ if (!targetId) {
3180
+ console.error('未能确定目标人员:请传 --user <id> 或 --user-name <姓名>,或确认 cookie 含 loginuuids。');
3181
+ process.exit(1);
3182
+ }
3183
+ // 越权边界(默认停用;E9_SCOPE_ENFORCE=1 时启用):目标须 ∈ {本人 + 全部层级下属}
3184
+ await assertUserIdsInScope(cfg, [targetId], { domain: 'kq' });
3185
+ const type = year && !month ? '1' : '2';
3186
+ const typevalue = month || year || undefined;
3187
+ if (sub === 'detail') {
3188
+ const r = await getKqMonthReport(cfg, { typevalue, resourceId: targetId });
3189
+ if (args.flags.raw) {
3190
+ console.error('===== RAW getHrmKQMonthReportInfo =====');
3191
+ console.error(JSON.stringify(r.raw, null, 2));
3192
+ }
3193
+ if (args.flags.json) {
3194
+ console.log(JSON.stringify({ typevalue: r.typevalue, resourceId: r.resourceId, days: r.days }, null, 2));
3195
+ return;
3196
+ }
3197
+ console.log(`E9 考勤明细 ${r.typevalue} 人员 id=${targetId} 共 ${r.days.length} 天`);
3198
+ const withSign = r.days.filter((d) => d.signIn || d.signOut || d.types.length);
3199
+ if (!withSign.length) {
3200
+ console.log('(本月无打卡记录)');
3201
+ return;
3202
+ }
3203
+ for (const d of withSign) {
3204
+ const flags = d.types.length ? ` [${d.types.join(',')}]` : '';
3205
+ console.log(` ${d.date} 签到 ${d.signIn || '—'} → 签退 ${d.signOut || '—'}${flags}`);
3206
+ }
3207
+ return;
3208
+ }
3209
+ const r = await getKqReport(cfg, { type, typevalue, resourceId: targetId });
3210
+ if (args.flags.raw) {
3211
+ console.error('===== RAW getHrmKQReportInfo =====');
3212
+ console.error(JSON.stringify(r.raw, null, 2));
3213
+ }
3214
+ if (args.flags.json) {
3215
+ console.log(JSON.stringify({
3216
+ typevalue: r.typevalue, view: type === '1' ? 'year' : 'month', resourceId: r.resourceId,
3217
+ attendance: r.attendance, durations: r.durations, exceptions: r.exceptions, leave: r.leave,
3218
+ }, null, 2));
3219
+ return;
3220
+ }
3221
+ console.log(BOX_TOP);
3222
+ console.log(`E9 考勤统计 ${r.typevalue}(${type === '1' ? '年' : '月'}视图) 人员 id=${targetId}`);
3223
+ console.log(DIVIDER);
3224
+ console.log(` 出勤统计:应出勤 ${r.attendance.workDays}`);
3225
+ for (const it of r.attendance.items)
3226
+ console.log(` ${it.title} = ${it.value}`);
3227
+ const line = (label, arr) => {
3228
+ if (!arr.length)
3229
+ return;
3230
+ console.log(` ${label}:${arr.map((x) => `${x.title} ${x.value}`).join(' | ')}`);
3231
+ };
3232
+ line('流程时长', r.durations);
3233
+ line('异常', r.exceptions);
3234
+ if (r.leave.length) {
3235
+ console.log(DIVIDER);
3236
+ console.log(' 假期余额(可请天数):');
3237
+ for (const g of r.leave) {
3238
+ const items = g.items.map((x) => `${x.title} ${x.value}`).join(' | ');
3239
+ if (items)
3240
+ console.log(` ${g.title}:${items}`);
3241
+ }
3242
+ }
3243
+ console.log(BOX_TOP);
3244
+ return;
3245
+ }
3246
+ console.error(`kq 暂只支持 status / punch / report / detail,收到: ${sub}`);
2570
3247
  process.exit(1);
2571
3248
  }
2572
3249
  /** CLI 侧当前时刻 HH:mm:ss(与 kq.ts 的 nowHHmmss 同逻辑,CLI 输出用)。 */
@@ -3781,8 +4458,9 @@ async function runSubordinate(args) {
3781
4458
  console.error(JSON.stringify(r.raw.counts, null, 2));
3782
4459
  console.error('===== 解析结果 =====');
3783
4460
  }
4461
+ const subBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3784
4462
  if (args.flags.json) {
3785
- 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));
4463
+ console.log(JSON.stringify({ lastname, allLevel, page, pageSize: r.pageSize, total: r.total, count: r.count, items: r.items.map((it) => withLink('hrm', { id: it.id, name: it.name, fields: it.fields }, subBase)) }, null, 2));
3786
4464
  return;
3787
4465
  }
3788
4466
  console.log(`我的下属${allLevel ? '(全部层级)' : '(直接下属)'}${name ? ` · 姓名过滤「${name}」` : ''} [第 ${page} 页${r.pageSize != null ? ` · 每页 ${r.pageSize} 条` : ''}${r.total != null ? ` / 共 ${r.total} 人` : ''}] 当前 ${r.count} 人`);
@@ -3790,13 +4468,16 @@ async function runSubordinate(args) {
3790
4468
  console.log('(无下属)');
3791
4469
  return;
3792
4470
  }
3793
- const cols = (r.columns || []).filter((c) => c.dataIndex && c.dataIndex !== 'randomFieldId');
4471
+ // 表头同样排除 id 列(服务端把 id 列的标题误标为「是否在线」,列出来会误导)与 randomFieldId
4472
+ const hdrIdCol = r.items[0]?.idCol;
4473
+ const cols = (r.columns || []).filter((c) => c.dataIndex && c.dataIndex !== 'randomFieldId' && c.dataIndex !== hdrIdCol);
3794
4474
  if (cols.length)
3795
4475
  console.log(`字段(${cols.length}): ${cols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
3796
4476
  for (let i = 0; i < r.items.length; i++) {
3797
4477
  const it = r.items[i];
3798
- console.log(`${i + 1}. ${it.name || '(无姓名)'} [id=${it.id || ''}]`);
3799
- const meta = it.fields.filter((f) => f.value && f.title && f.dataIndex !== it.idCol).slice(0, 6).map((f) => `${f.title}=${f.value}`).join(' | ');
4478
+ console.log(`${i + 1}. ${hyperlinkFor('hrm', { id: it.id }, subBase, it.name || '(无姓名)')} [id=${it.id || ''}]`);
4479
+ // 排除内部列:id 列(其标题被服务端误标为「是否在线」)、姓名列(已作可点标题)、randomFieldId(id 的副本)
4480
+ const meta = it.fields.filter((f) => f.value && f.title && f.dataIndex !== it.idCol && f.dataIndex !== it.nameCol && f.dataIndex !== 'randomFieldId').slice(0, 6).map((f) => `${f.title}=${f.value}`).join(' | ');
3800
4481
  if (meta)
3801
4482
  console.log(` ${meta}`);
3802
4483
  }
@@ -3804,6 +4485,18 @@ async function runSubordinate(args) {
3804
4485
 
3805
4486
  // ─── 组织人事(HRM)搜索 ─────────────────────────────────
3806
4487
 
4488
+ /**
4489
+ * hrm search 结果里值为「字典编码」的列(dataIndex)。
4490
+ * E9 在人事搜索接口(hrm/search/getHrmSearchResult)里**不解析**这些字段——给的是 id/code,
4491
+ * 而同为人员列表的 subordinateinfo/getSearchResult 会解析(部门/分部/直接上级都给中文名)。
4492
+ * 实测:岗位 jobtitlespan 给的就是 "1830"、性别 sexspan 给 "1"。OA 的「查询人员」页同样只显示编码。
4493
+ * 故渲染时标注「(编码)」,避免用户把 1830 误读成名称。
4494
+ */
4495
+ const HRM_CODE_FIELDS = new Set([
4496
+ 'sex', 'accounttype', 'belongto', 'jobtitle', 'jobGroupId',
4497
+ 'jobactivity', 'jobcall', 'joblevel', 'status', 'systemlanguage', 'locationid',
4498
+ ]);
4499
+
3807
4500
  /** 组织节点名称 → id(用于 --dept/--subcompany 名称定位;纯数字原样直传)。 */
3808
4501
  async function resolveHrmOrgValue(cfg, keyword, { matchType, flag }) {
3809
4502
  const node = await findHrmOrgNode(cfg, keyword, { matchType });
@@ -3859,8 +4552,56 @@ async function runHrm(args) {
3859
4552
  }
3860
4553
  return;
3861
4554
  }
4555
+ if (sub === 'tree') {
4556
+ // 组织架构树:无参列顶层分部;--dept/--subcompany 定位到该组织后展开;--id(+可选 --type)直接指定起点;--depth 展示层数
4557
+ const depth = Math.min(Math.max(Number(flagStr(args, 'depth') ?? '1') || 1, 1), 5);
4558
+ const deptSpec = flagStr(args, 'dept');
4559
+ const subSpec = flagStr(args, 'subcompany');
4560
+ let start = { type: '0', id: '0' };
4561
+ let located = null;
4562
+ if (deptSpec || subSpec) {
4563
+ const kw = deptSpec ?? subSpec;
4564
+ const wantType = deptSpec ? '2' : '1';
4565
+ const label = deptSpec ? '部门' : '分部';
4566
+ if (/^\d+$/.test(kw)) {
4567
+ located = { id: kw, type: wantType, name: kw };
4568
+ }
4569
+ else {
4570
+ located = await findHrmOrgNode(cfg, kw, { matchType: wantType });
4571
+ if (!located)
4572
+ throw new Error(`未找到${label}「${kw}」。可先跑 e9 hrm tree 查看顶层分部,或改用数字 id。`);
4573
+ }
4574
+ start = { type: located.type, id: located.id };
4575
+ }
4576
+ else if (flagStr(args, 'id')) {
4577
+ start = { type: flagStr(args, 'type') ?? '1', id: flagStr(args, 'id') };
4578
+ }
4579
+ const r = await expandHrmOrgTree(cfg, { ...start, depth });
4580
+ if (args.flags.json) {
4581
+ console.log(JSON.stringify({ start, located, depth, nodes: r.nodes, total: r.total, truncated: r.truncated }, null, 2));
4582
+ return;
4583
+ }
4584
+ console.log(located
4585
+ ? `E9 组织架构 · ${located.name || located.id}(type=${start.type} id=${start.id})`
4586
+ : 'E9 组织架构 · 顶层分部');
4587
+ if (!r.nodes.length) {
4588
+ console.log('(无下级组织)');
4589
+ return;
4590
+ }
4591
+ const render = (nodes, indent) => {
4592
+ for (const n of nodes) {
4593
+ const kind = n.type === '1' ? '分部' : n.type === '2' ? '部门' : `type=${n.type}`;
4594
+ console.log(`${indent}${n.name} [${kind} id=${n.id}]`);
4595
+ if (n.children.length)
4596
+ render(n.children, indent + ' ');
4597
+ }
4598
+ };
4599
+ render(r.nodes, ' ');
4600
+ console.log(`\n共 ${r.total} 个组织节点${r.truncated ? '(已达显示上限,结果被截断)' : ''}。`);
4601
+ return;
4602
+ }
3862
4603
  if (sub !== 'search') {
3863
- console.error(`hrm 子命令: tabs/conditions/search,收到: ${sub}`);
4604
+ console.error(`hrm 子命令: tabs/conditions/search/tree,收到: ${sub}`);
3864
4605
  process.exit(1);
3865
4606
  }
3866
4607
  const tab = flagStr(args, 'tab') ?? '全部人员';
@@ -3940,19 +4681,52 @@ async function runHrm(args) {
3940
4681
  const it = r.items[i];
3941
4682
  const head = hyperlinkFor('hrm', it, base, it.name || '(无姓名)');
3942
4683
  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(' | ');
4684
+ // 行 meta 去掉姓名(head 已显示)与 id 类列,避免重复;字典编码字段标注「(编码)」
4685
+ const meta = it.fields
4686
+ .filter((f) => !['lastname', 'id', 'randomField0', 'randomFieldId'].includes(f.dataIndex))
4687
+ .slice(0, 8)
4688
+ .map((f) => `${f.title}=${f.value}${f.value && HRM_CODE_FIELDS.has(f.dataIndex) ? '(编码)' : ''}`)
4689
+ .join(' | ');
3945
4690
  if (meta)
3946
4691
  console.log(` ${meta}`);
3947
4692
  }
3948
4693
  }
3949
4694
 
4695
+ /**
4696
+ * 节点名解析:默认开启,`--no-node-name` 或环境变量 E9_NO_NODE_NAME=1 停用。
4697
+ * 接口失败/接口不存在时静默降级(列表照常出,只是节点列回退显示 id)。
4698
+ */
4699
+ function annotateNodeNamesForCli(cfg, items, args) {
4700
+ // --raw 是校准/排障出口,保持原样不额外请求;--no-node-name / E9_NO_NODE_NAME=1 显式关闭。
4701
+ const disabled = args.flags['no-node-name'] === true || args.flags.raw === true || isNodeNameDisabled();
4702
+ return annotateNodeNames(cfg, items, { disabled });
4703
+ }
4704
+ /** 解析 --time "HH:MM-HH:MM";不传返回空串(= 全天)。 */
4705
+ function parseTimeSpec(raw) {
4706
+ const s = String(raw ?? '').trim();
4707
+ if (!s)
4708
+ return { beginTime: '', endTime: '' };
4709
+ const m = s.split(/[-~—]/).map((x) => x.trim()).filter(Boolean);
4710
+ if (m.length !== 2) {
4711
+ console.error('--time 格式应为 "HH:MM-HH:MM"');
4712
+ process.exit(1);
4713
+ }
4714
+ return { beginTime: m[0], endTime: m[1] };
4715
+ }
4716
+
4717
+ /** 节点显示:解析到名字时用「名字(id)」,否则退回 id。 */
4718
+ function nodeLabel(item) {
4719
+ const id = item?.node ? String(item.node) : '';
4720
+ if (!id)
4721
+ return undefined;
4722
+ return item.nodeName ? `${item.nodeName}(${id})` : id;
4723
+ }
3950
4724
  function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
3951
4725
  const visible = items.slice(0, max);
3952
4726
  for (let i = 0; i < visible.length; i++) {
3953
4727
  const it = visible[i];
3954
4728
  const head = it.title ?? '(无标题)';
3955
- const meta = [it.creator, it.currentOperator ? `未操作者=${it.currentOperator}` : null, it.node, it.createdate].filter(Boolean).join(' | ');
4729
+ const meta = [it.creator, it.currentOperator ? `未操作者=${it.currentOperator}` : null, nodeLabel(it), it.createdate].filter(Boolean).join(' | ');
3956
4730
  const headLinked = hyperlinkFor('todo', it, base, head);
3957
4731
  console.log(`${indent}${i + 1}. ${headLinked}${it.level ? ` (${it.level})` : ''}`);
3958
4732
  if (meta)
@@ -3980,9 +4754,15 @@ async function runTodoBatch(args) {
3980
4754
  }
3981
4755
  else {
3982
4756
  // 批量关注:dry-run 也先查关注类型(只读),供用户确认加入哪个关注类型
4757
+ // 关注在 待办/已办/我发起 三个分类下都有「我的关注」页签,故定位列表可用 --category 指定(默认待办)
4758
+ const category = flagStr(args, 'category') ?? 'todo';
4759
+ if (!['todo', 'done', 'mine'].includes(category)) {
4760
+ console.error(`batch-attention 的 --category 只支持 todo(待办)/ done(已办)/ mine(我发起),收到: ${category}`);
4761
+ process.exit(1);
4762
+ }
3983
4763
  const attentionTypes = await getAttentionTypes(cfg, requestIds);
3984
4764
  const attentionId = flagStr(args, 'attention-id') ?? attentionTypes[0]?.id;
3985
- preview = { operation: '批量关注', requestIds, attentionId, attentionTypes: attentionTypes.map((t) => ({ id: t.id, showname: t.showname })) };
4765
+ preview = { operation: '批量关注', category, requestIds, attentionId, attentionTypes: attentionTypes.map((t) => ({ id: t.id, showname: t.showname })) };
3986
4766
  }
3987
4767
  if (!args.flags.yes) {
3988
4768
  console.log(BOX_TOP);
@@ -3998,7 +4778,7 @@ async function runTodoBatch(args) {
3998
4778
  else if (sub === 'batch-forward')
3999
4779
  result = await batchForward(cfg, { requestIds, receiver: preview.receiver, remark: preview.remark });
4000
4780
  else
4001
- result = await batchAttention(cfg, { requestIds, attentionId: preview.attentionId });
4781
+ result = await batchAttention(cfg, { requestIds, attentionId: preview.attentionId, category: preview.category });
4002
4782
  const body = result.result?.json ?? {};
4003
4783
  const ok = result.result?.ok && body.success !== false && body.status !== false;
4004
4784
  console.log(`${ok ? '✅' : '⚠'} ${preview.operation}已请求处理(${requestIds.length} 条)。`);
@@ -4046,7 +4826,7 @@ async function main() {
4046
4826
  const cmd = args._[0] ?? 'todo';
4047
4827
  if (cmd === 'version') {
4048
4828
  const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
4049
- console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.3', buildHash }, null, 2));
4829
+ console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.5', buildHash }, null, 2));
4050
4830
  return;
4051
4831
  }
4052
4832
  if (cmd === 'whoami') {
@@ -4085,6 +4865,14 @@ async function main() {
4085
4865
  await runWorkplan(args);
4086
4866
  return;
4087
4867
  }
4868
+ if (cmd === 'supervise') {
4869
+ await runSupervise(args);
4870
+ return;
4871
+ }
4872
+ if (cmd === 'wfagent') {
4873
+ await runWfagent(args);
4874
+ return;
4875
+ }
4088
4876
  if (cmd === 'odoc') {
4089
4877
  await runOdoc(args);
4090
4878
  return;
@@ -4219,6 +5007,43 @@ async function main() {
4219
5007
  return typeof saved === 'string' ? saved : undefined;
4220
5008
  };
4221
5009
  const queryBool = (key) => args.flags[key] === true || savedQueryFlags[key] === true;
5010
+ // ★按节点名筛选(--node-name):节点 id 是**流程内编号**,必须先定位流程才能解析成 id。
5011
+ // 与 --node-id 互斥;未给 --workflow 时只在**本地缓存**的节点表里找(缓存来自历次列表查询)。
5012
+ let nodeIdFilter = queryFlag('node-id');
5013
+ const nodeNameFilter = queryFlag('node-name');
5014
+ if (nodeNameFilter) {
5015
+ // --json 时信息一律走 stderr,保证 stdout 是纯 JSON
5016
+ const say = args.flags.json ? console.error : console.log;
5017
+ if (nodeIdFilter)
5018
+ throw new Error('--node-name 与 --node-id 不能同时用(两者都在指定当前节点)。');
5019
+ const wfScope = String(workflowFilter ?? '').split(',').map((s) => s.trim()).filter(Boolean);
5020
+ if (wfScope.length > 1)
5021
+ throw new Error(`--node-name 需要唯一的流程范围,但 --workflow 传了 ${wfScope.length} 个流程;请只保留一个。`);
5022
+ const resolved = await resolveNodeByName(cfg, nodeNameFilter, { workflowId: wfScope[0] });
5023
+ const pick = resolved.exact.length === 1 ? resolved.exact[0] : null;
5024
+ if (!pick) {
5025
+ if (resolved.unavailable) {
5026
+ console.error(`取不到流程 ${wfScope[0]} 的节点表(/api/workflow/test/getNodeInfo 不可用),无法按节点名筛选;请改用 --node-id <id>。`);
5027
+ process.exit(1);
5028
+ }
5029
+ if (resolved.candidates.length === 0) {
5030
+ console.error(resolved.scoped
5031
+ ? `流程 ${wfScope[0]} 的节点表里没有匹配「${nodeNameFilter}」的节点。`
5032
+ : `本地缓存的 ${resolved.poolSize} 个流程节点表里没有匹配「${nodeNameFilter}」的节点。可加 --workflow <流程id> 限定流程,或先在待办列表里翻到该流程任意一条以缓存其节点表。`);
5033
+ process.exit(1);
5034
+ }
5035
+ say(BOX_TOP);
5036
+ say(` 「${nodeNameFilter}」匹配到 ${resolved.candidates.length} 个节点,请选一个后再用 --node-id 指定:`);
5037
+ say(DIVIDER);
5038
+ for (const c of resolved.candidates)
5039
+ say(` ${String(c.nodeId).padStart(6, ' ')} ${c.nodeName}${c.matchType === 'exact' ? ' ★(精确)' : ''} ← ${c.workflowName ?? '(未命名流程)'}(流程 ${c.workflowId})`);
5040
+ say(BOX_TOP);
5041
+ say(` 例:e9 todo ${sub} --node-id ${resolved.candidates[0].nodeId} --workflow ${resolved.candidates[0].workflowId}`);
5042
+ process.exit(0);
5043
+ }
5044
+ nodeIdFilter = pick.nodeId;
5045
+ say(`按节点名筛选:「${pick.nodeName}」→ 节点 id=${pick.nodeId}(流程 ${pick.workflowName ?? pick.workflowId})`);
5046
+ }
4222
5047
  let workflowTypeFilter = queryFlag('workflow-type');
4223
5048
  if (workflowTypeFilter && !/^\d+$/.test(workflowTypeFilter)) {
4224
5049
  const resolvedType = await resolveWorkflowType(cfg, workflowTypeFilter, { category, viewcondition });
@@ -4248,7 +5073,7 @@ async function main() {
4248
5073
  requestLevel: queryFlag('request-level'),
4249
5074
  requestMark: queryFlag('request-mark'),
4250
5075
  workflowType: workflowTypeFilter,
4251
- nodeId: queryFlag('node-id'),
5076
+ nodeId: nodeIdFilter,
4252
5077
  flowStatus: queryFlag('flow-status'),
4253
5078
  totalOnly: queryBool('total-only'),
4254
5079
  debug: queryBool('debug'),
@@ -4258,6 +5083,7 @@ async function main() {
4258
5083
  const shouldGroup = !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
4259
5084
  if (shouldGroup) {
4260
5085
  const grouped = await getOaTabGroups(cfg, { ...listOpts, category });
5086
+ await annotateNodeNamesForCli(cfg, grouped.groups.flatMap((g) => g.items), args);
4261
5087
  const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
4262
5088
  const groupedSnapshot = {
4263
5089
  category,
@@ -4295,6 +5121,7 @@ async function main() {
4295
5121
  return;
4296
5122
  }
4297
5123
  const result = await getWfList(cfg, listOpts);
5124
+ await annotateNodeNamesForCli(cfg, result.items, args);
4298
5125
  const savedResult = persistSearchResult(result, `E9 ${name ?? key} 查询`);
4299
5126
  if (args.flags.raw) {
4300
5127
  console.error('===== RAW getWfListParams =====');
@@ -4325,6 +5152,7 @@ async function main() {
4325
5152
  creatorId: it.creatorId,
4326
5153
  currentOperator: it.currentOperator,
4327
5154
  node: it.node,
5155
+ nodeName: it.nodeName,
4328
5156
  createdate: it.createdate,
4329
5157
  receivedate: it.receivedate,
4330
5158
  status: it.status,
@@ -4406,15 +5234,39 @@ async function runTodoSubmit(args) {
4406
5234
  }
4407
5235
  const remark = flagStr(args, 'remark') ?? 'OK';
4408
5236
  const send = !!args.flags.yes;
5237
+ // --field-values '<json>':提交时修改字段值。此前 CLI 声明了该参数却从未解析/传入(静默失效)。
5238
+ const fvRaw = flagStr(args, 'field-values');
5239
+ let fieldValues;
5240
+ if (fvRaw) {
5241
+ try {
5242
+ fieldValues = JSON.parse(fvRaw);
5243
+ }
5244
+ catch {
5245
+ console.error(`--field-values 不是合法 JSON:${fvRaw}`);
5246
+ process.exit(1);
5247
+ }
5248
+ if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues)) {
5249
+ console.error('--field-values 需为对象,如 \'{"70702":"新值"}\'');
5250
+ process.exit(1);
5251
+ }
5252
+ }
4409
5253
  if (!send) {
4410
5254
  console.log(BOX_TOP);
4411
5255
  console.log(` [dry-run] 提交待办 requestid=${requestId}`);
4412
5256
  console.log(` 签字意见 = ${remark}`);
5257
+ if (fieldValues && Object.keys(fieldValues).length)
5258
+ console.log(` 字段修改 = ${JSON.stringify(fieldValues)}`);
5259
+ // dry-run 即校验字段属性(当前节点):只读/隐藏字段禁止填值——避免"预览通过、提交才报错"
5260
+ if (fieldValues && Object.keys(fieldValues).length) {
5261
+ const bad = await validateFieldValuesAtNode(cfg, requestId, fieldValues);
5262
+ if (bad.length)
5263
+ throw new Error(`拒绝填写当前节点的只读/隐藏字段:${bad.map((b) => `${b.label || b.id}(${VIEWATTR_LABEL[b.viewattr] ?? 'va=' + b.viewattr})`).join('、')}。本节点只允许修改「可编辑」字段,不得绕过节点字段权限自行填值。`);
5264
+ }
4413
5265
  console.log(' 确认无误后加 --yes 真正提交。');
4414
5266
  console.log(BOX_TOP);
4415
5267
  return;
4416
5268
  }
4417
- const res = await submitTodo(cfg, { requestId, remark });
5269
+ const res = await submitTodo(cfg, { requestId, remark, fieldValues });
4418
5270
  console.log(BOX_TOP);
4419
5271
  const j = res.json;
4420
5272
  const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';