weaver-e9-assistant-cli 1.1.4 → 1.1.11

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
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-nocheck
3
3
  import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import tls from 'node:tls';
4
6
  import { createHash } from 'node:crypto';
5
- import { loadConfig, resolveViewcondition, endpointFor, setConfigField } from './config.js';
6
- import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE } from './workflow.js';
7
+ import { loadConfig, resolveViewcondition, endpointFor, setConfigField, profileNameFromBaseUrl } from './config.js';
8
+ import { cacheGet } from './cache-store.js';
9
+ import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE, parseTimeRange } from './workflow.js';
10
+ import { annotateNodeNames, isNodeNameDisabled, resolveNodeByName, nodeMapCachePath } from './nodemap.js';
7
11
  import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
8
12
  import { pageUrl, availablePages } from './pageopen.js';
9
13
  import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
@@ -12,7 +16,8 @@ import { loadSubmission, updateSubmission } from './submissionstate.js';
12
16
  import { saveSubmissionPlan, loadSubmissionPlan } from './submissionplan.js';
13
17
  import { saveCandidateQuery, loadCandidateQuery, persistSearchResult } from './querystate.js';
14
18
  import { getFlowSummary } from './wfsummary.js';
15
- import { submitTodo, retractRequest } from './todosubmit.js';
19
+ import { submitTodo, retractRequest, prepareTodoSubmission, buildTodoSubmitBody } from './todosubmit.js';
20
+ import { parseDetailFileSpec, describeDetailPlan } from './tododetail.js';
16
21
  import { rejectTodo } from './todoreject.js';
17
22
  import { transferTodo } from './todotransfer.js';
18
23
  import { forwardTodo, circulateTodo } from './todoforward.js';
@@ -20,21 +25,32 @@ import { ccTodo } from './todocc.js';
20
25
  import { askIdeaTodo } from './todoaskidea.js';
21
26
  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
27
  import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
23
- import { describeFormFields } from './wfform.js';
28
+ import { describeFormFields, VIEWATTR_LABEL, fieldStatusLabel } from './wfform.js';
29
+ import { loadMemory, saveShortcut, findShortcut, findShortcutFuzzy, hitShortcut, forgetShortcut, saveFieldDefault, getFieldDefaults, setAutoSubmit, clearMemory, listShortcuts, clearFormCache, saveUserPreference, getUserPreferences, setMemoryAvailable, cleanupShortcuts, findExpiredShortcuts, SHORTCUT_EXPIRY_DAYS, getFieldValueHistory, exportMemory, importMemory, refreshStaleFormCaches, memoryCachePath, memoryCacheDir } from './memory.js';
24
30
  import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
25
31
  import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus } from './auth.js';
26
32
  import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
27
33
  import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel, exportWorkplanExcel } from './workplan.js';
34
+ import { collectSubordinateReports } from './teamreport.js';
28
35
  import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
29
- import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from './kq.js';
36
+ import { getSuperviseTree, getSuperviseCount, getSuperviseConditions, listSupervise, doSupervise, resolveSuperviseTab, SUPERVISE_TABS } from './supervise.js';
37
+ 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';
38
+ import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj, getKqReport, getKqMonthReport } from './kq.js';
30
39
  import { collectQueryableBusiness } from './report.js';
31
- import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel, assertEditableCardValues } from './cube.js';
40
+ import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel, assertEditableCardValues, cubeCachePath } from './cube.js';
32
41
  import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
33
- import { getSubordinates, assertUserIdsInScope, assertNamesInScope, isScopeEnforced } from './subordinate.js';
42
+ import { getSubordinates, assertUserIdsInScope, assertNamesInScope, resolveNameToUid, isScopeEnforced, scopeCachePath } from './subordinate.js';
34
43
  import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds, expandHrmOrgTree } from './hrm.js';
35
44
  import { parseArgs, flagStr, flagBool } from './args.js';
36
45
  import { todayStr } from './util.js';
37
46
  import { BOX_TOP, DIVIDER } from './render.js';
47
+ // ─── TLS 版本上限 ─────────────────────────────────────────
48
+ // E9 服务端的 TLS 1.3 握手实测约 2.3s(同一台机器 TLS 1.2 约 0.1s),
49
+ // 而 CLI 每次都是新进程、必须重新握手 → 把客户端上限压到 TLS 1.2,
50
+ // 单条命令可省约 2.2s。服务端仅支持 TLS 1.3 时设 E9_TLS_MAX=1.3 关闭该限制。
51
+ if ((process.env.E9_TLS_MAX ?? '1.2') !== '1.3') {
52
+ tls.DEFAULT_MAX_VERSION = 'TLSv1.2';
53
+ }
38
54
  function usage() {
39
55
  return [
40
56
  'E9 个人助手 CLI',
@@ -46,10 +62,11 @@ function usage() {
46
62
  ' [--workflow <id>] [--creator <id>] [--creator-dept <id>] [--creator-subcom <id>]',
47
63
  ' [--create-time <range>] [--receive-time <range>] [--operate-time <range>]',
48
64
  ' [--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>]',
65
+ ' [--node-id <id> | --node-name <节点名>] [--flow-status <status>] [--sort <spec>] [--sub-category <tab>]',
66
+ ' [--total-only] [--ungrouped] [--no-node-name] [--no-operators] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
51
67
  ' e9 todo summary --requestid <id> 查看流程摘要(表单字段+值+流转历史)',
52
- ' e9 todo submit --requestid <id> [--remark "同意"] [--yes] 提交待办到下一节点(默认签字 OK)',
68
+ ' e9 todo submit --requestid <id> [--remark "同意"] [--field-values <json>] [--detail-file <path>] [--file <字段id[_行号]>=<路径>] [--yes]',
69
+ ' 提交待办到下一节点(默认签字 OK;可改字段、增删改明细行、上传附件)',
53
70
  ' e9 todo reject --requestid <id> [--remark "退回"] [--reject-to-node <nodeid>] [--reject-to-type <t>] [--reject-node <nodes>] [--yes] 退回/驳回待办',
54
71
  ' e9 todo transfer --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转办"] [--yes] 转办(处理权转移)',
55
72
  ' e9 todo forward --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转发"] [--yes] 转发(分享给某人看)',
@@ -58,10 +75,11 @@ function usage() {
58
75
  ' e9 todo ask-idea --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "意见"] [--yes] 意见征询(向某人征询意见,不改审批人)',
59
76
  ' e9 todo batch-submit --request-ids "1,2" --user-ids "u1,u2" [--remark "同意"] [--yes] 批量提交(先预览)',
60
77
  ' 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] 批量关注(先预览,默认加入首个关注类型)',
78
+ ' e9 todo batch-attention --request-ids "1,2" [--category <todo|done|mine>] [--attention-id <类型id>] [--yes] 批量关注(先预览;--category 选在哪类列表定位,默认待办)',
62
79
  ' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
63
80
  ' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
64
81
  ' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|下级|未读|共享|分享给我|特别关注>] [--page <n>] [--json] [--raw]',
82
+ ' e9 blog subordinates [--date YYYY-MM-DD] [--all-level] [--names "张三,李四"] [--json] 批量查下属日报(按人并发,含未写清单)',
65
83
  ' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
66
84
  ' e9 blog view --user-id <id> 查看某人微博主页',
67
85
  ' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
@@ -91,7 +109,7 @@ function usage() {
91
109
  ' e9 create open --name <流程名> 按名查找 id 并生成发起 URL(多个候选则列出)',
92
110
  ' e9 create open --requestid <请求id> [--id <wf>] 生成已有请求表单 URL(处理/提交待办用,手动填+提交)',
93
111
  ' e9 create fields --id <流程id> 模拟打开发起表单,列出字段(可编辑/只读/隐藏/控件类型)',
94
- ' e9 create start [--id <id>|--name <名>] [--values <json>|--values-file <path>] [--detail-file <path>] [--file <字段id>=<路径>] [--requestname <名>] [--yes --plan <id>] 预览生成确认计划;提交复用确认字段',
112
+ ' e9 create start [--id <id>|--name <名>] [--values <json>|--values-file <path>] [--detail-file <path>] [--file <字段id>=<路径>] [--requestname <名>] [--dry-run] [--yes --plan <id>] 预览生成确认计划;提交复用确认字段',
95
113
  ' e9 create flow --requestid <请求id> --workflowid <流程id> [--next-user <uid>] [--yes] 公文流转:取可流转候选→确认→推送下一节点(公文 create start 后续步骤)',
96
114
  ' e9 page-open --page todo|done|mine|center|newflow|view [--request-id <id>] 生成流程中心页面链接',
97
115
  ' e9 email list [--page <n>] [--subject <text>] [--from <text>] [--to <text>] [--date <语义>] [--datetype <n>] [--startdate <YYYY-MM-DD>] [--enddate <YYYY-MM-DD>] [--folder inbox|sent|<id>] 收件箱/已发送邮件列表(单步 GET)',
@@ -106,7 +124,7 @@ function usage() {
106
124
  ' e9 email recall --id <邮件id> [--yes] 撤回已发邮件',
107
125
  ' e9 email attachment upload --file <路径> 上传附件拿 fileid;attachment list 附件列表',
108
126
  ' e9 email size 邮箱容量/未读统计;email accounts 接收账户列表',
109
- ' e9 workplan list [--date <YYYY-MM-DD>] [--view-type <1|3>] [--group <部门|分部|下级|...>] 日程列表(日历数据)',
127
+ ' e9 workplan list [--date <YYYY-MM-DD>] [--view-type <1当天|2周|3月>] [--group <部门|分部|下级|...>] 日程列表(默认我的日程;--group 走组织视图)',
110
128
  ' e9 workplan my-calendar [--date <YYYY-MM-DD>] [--view-type <1|3>] 日程月视图(MyCalendar,浏览器主用)',
111
129
  ' e9 workplan view --id <id> 查看日程详情',
112
130
  ' e9 workplan finish --id <id> [--yes] 完成日程',
@@ -115,16 +133,33 @@ function usage() {
115
133
  ' e9 workplan search-conditions 搜索可用条件(分组+字段)',
116
134
  ' e9 workplan attention 关注的日程',
117
135
  ' e9 workplan base-data 日历初始化数据(含部门/分部/组织视图)',
118
- ' e9 workplan search [--name <text>] [--type <id>] [--status <id>] [--creater <id>] [--begin <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--group <全部|今天|本周|...>] [--page <n>] 日程表搜索(可过滤分页)',
136
+ ' e9 workplan search [--name <text>] [--type <id>] [--status <id>] [--creater <id>] [--begin <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--group <全部|今天|本周|...>] [--page <n>] [--no-count] 日程表搜索(可过滤分页;--no-count 跳过总数请求更快)',
119
137
  ' e9 workplan create --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm" [--description-file <路径>] 新建日程(默认 dry-run)',
120
138
  ' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
121
139
  ' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
122
140
  ' e9 workplan export [--type <list|html>] [--group <部门|分部|下级|...>] [--date <YYYY-MM-DD>] [--workplan-type <id>] [--out-dir <目录>] [--yes] 导出日程(list=日程清单 / html=日程一览表,默认 dry-run)',
141
+ ' e9 supervise list [--tab <全部|未督办|已督办|我的关注>] [--keyword <text>] [--page <n>] [--json] 流程督办列表',
142
+ ' e9 supervise count [--json] 流程督办计数(总数/未督办)',
143
+ ' e9 supervise conditions [--json] 流程督办可用搜索条件',
144
+ ' e9 supervise send --requestids "1,2" [--remark "请尽快处理"] [--yes] 发起督办(默认 dry-run)',
145
+ ' e9 wfagent list [--view <mine|all|records>] [--status <全部|未结束|未开始|代理中|收回中|已结束>]',
146
+ ' [--agent <代理人>] [--be-agent <被代理人>] [--begin <日期|"起 止">] [--end <日期|"起 止">]',
147
+ ' [--page <n>] [--json] 流程代理列表(mine=我设置的 / all=全部 / records=代理办理记录)',
148
+ ' e9 wfagent conditions [--view <mine|all|records>] [--json] 流程代理可用搜索条件',
149
+ ' e9 wfagent templates [--name <名>] [--workflow <id>] [--page <n>] [--json] 代理模板列表',
150
+ ' e9 wfagent create --agent <代理人> --be-agent <被代理人> --begin <YYYY-MM-DD> --end <YYYY-MM-DD>',
151
+ ' [--range 全部 | --workflow <流程id,id>] [--save-template <模板名>] [--yes] 新建代理(默认 dry-run)',
152
+ ' e9 wfagent edit --agent-ids "537,538" [--begin <日期>] [--end <日期>] [--time <HH:MM-HH:MM>] [--yes] 批量编辑(只改起止日期/时间)',
153
+ ' e9 wfagent edit --agent-id <代理id> [--end <YYYY-MM-DD>] [--begin <YYYY-MM-DD>] [--time <HH:MM-HH:MM>] [--yes] 编辑代理(默认 dry-run,显示改前→改后)',
154
+ ' e9 wfagent takeback --agent-ids "69,70" [--back-running <0|1>] [--yes] 收回代理(默认 dry-run;id 取列表的「代理id」)',
155
+ ' e9 wfagent takeback --all [--be-agent <被代理人>] [--yes] 收回该被代理人名下**全部**代理(默认 dry-run,批量)',
123
156
  ' e9 odoc todo [--page <n>] [--subject <标题>] [--dispatch-num <发文字号>] [--type <公文种类id>] [--topic <主题分类id>] 公文待办列表(getCustomPageList table,每页 10 条)',
124
157
  ' e9 odoc count [--view] 公文待办/待阅计数(发文/收文/签报)',
125
158
  ' e9 odoc create-list [--type 1|2] 发文/收文流程列表(1=发文拟稿 2=收文登记,复用 create start 发起)',
126
159
  ' e9 kq punch on|off [--group-id <id>] [--userid <id>] [--date <YYYY-MM-DD>] [--yes] 打卡(上班/下班卡,默认 dry-run 展示 encryptObj,--yes 真打)',
127
160
  ' e9 kq status 查看今日打卡班次(getButtons timeline)',
161
+ ' e9 kq report [--user <id>|--user-name <姓名>] [--month YYYY-MM|--year YYYY] [--json] 考勤统计 + 假期余额(默认本人·本月)',
162
+ ' e9 kq detail [--user <id>|--user-name <姓名>] [--month YYYY-MM] [--json] 月度考勤明细(签到签退时间/IP)',
128
163
  ' e9 cube list --customid <id> [--page <n>] [--keyword <text>] [--condition <k=v;...>] [--json] 建模台账列表(--condition 支持中文字段名自动映射 domkey)',
129
164
  ' e9 cube fields --customid <id> 建模查询条件探查(数据状态/筛选字段)',
130
165
  ' e9 cube export --customid <id> [--filename <名>] [--condition <k=v;...>] [--keyword <text>] [--out-dir <目录>] [--plain] [--yes] 导出建模台账为 Excel(默认 dry-run;--plain 明文 modeCustomid 逃生口,默认 base64)',
@@ -152,11 +187,28 @@ function usage() {
152
187
  ' e9 hrm search [--tab <全部人员|同部门|我的下属>] [--name <姓名>] [--condition "岗位=经理;性别=男"]',
153
188
  ' [--dept <部门名|id>] [--subcompany <分部名|id>] [--all-level] [--page <n>] [--json] [--raw] 人事搜索(姓名=详情卡片超链接)',
154
189
  ' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
190
+ ' e9 memory list 列出已记住的流程快捷方式(按最近使用排序)',
191
+ ' e9 memory save <关键词> --workflow-id <id> [--workflow-name <名>] 记住一个流程快捷方式',
192
+ ' e9 memory forget <关键词> 忘记一个快捷方式',
193
+ ' e9 memory show <关键词> 查看快捷方式详情',
194
+ ' e9 memory clear 清空当前环境+用户全部记忆',
195
+ ' e9 memory clear-cache <workflow-id> 清除指定流程的表单缓存(表单结构变了时手动刷新)',
196
+ ' e9 memory cleanup [--dry-run] [--max-age-days <n>] 清理过期快捷方式(默认 90 天未使用)',
197
+ ' e9 memory export [--output <file>] 导出当前用户记忆到文件',
198
+ ' e9 memory import <file> 从文件导入记忆(合并模式,已有保留)',
199
+ ' e9 memory refresh 手动巡检所有表单缓存(对比字段结构,有变化自动更新)',
200
+ ' e9 memory user-pref [show|<key> [<value>]] 查看/设置用户偏好(如简洁回复、不展示 requestId)',
201
+ ' e9 memory disable 临时禁用记忆(不删数据,后续操作跳过记忆)',
202
+ ' e9 memory enable 重新启用记忆',
203
+ ' e9 memory auto-submit <关键词> [on|off] 设置是否自动提交(省略 on|off 查看状态)',
204
+ '',
205
+ ' e9 cache stats 打印各维度缓存文件大小、条目数、最后更新时间',
155
206
  ' e9 config 查看当前配置来源与状态',
156
207
  ' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
157
208
  ' e9 config init 首次配置引导',
158
209
  '',
159
210
  '说明:',
211
+ ' E9_TLS_MAX TLS 客户端上限,默认 1.2(E9 服务端 1.3 握手慢约 2.2s);服务端仅支持 1.3 时设 1.3',
160
212
  ' todo list 获取我的待办流程列表(跑 getWfListParams→splitPageKey→datas→counts 链路)',
161
213
  ' --tab tab key,对应 config.tabs 中的某项,默认 defaultTab',
162
214
  ' --page 页码,默认 1',
@@ -164,10 +216,15 @@ function usage() {
164
216
  ' --json 以 JSON 输出(适合 AI 工具消费)',
165
217
  ' --raw 额外打印各步原始响应(首次调试必用)',
166
218
  ' --debug 打印每步请求状态',
219
+ ' --no-operators 不查当前处理人(该接口较慢,分组视图会触发 5 次;可显著提速)',
167
220
  ' blog list 获取微博列表(默认今天,支持 --scope 切换范围)',
168
221
  ' --date 日期 YYYY-MM-DD,默认今天',
169
222
  ' --scope 范围:全部/关注/共享/下级(默认=自己的微博)',
170
223
  ' --page 页码,默认 1',
224
+ ' blog subordinates 批量查下属日报(先取下属列表,再按人并发查)',
225
+ ' --date 日报日期 YYYY-MM-DD,默认今天',
226
+ ' --all-level 全部层级下属(默认直接下属)',
227
+ ' --names "张三,李四" 只看指定下属(精确姓名,逗号/顿号/空格分隔)',
171
228
  ' blog search 搜索微博(GET /api/blog/base/getMyBlogListBySearch)',
172
229
  ' --content 搜索关键词(必填)',
173
230
  ' --start-date/--end-date 日期区间 YYYY-MM-DD',
@@ -214,7 +271,7 @@ function usage() {
214
271
  ' create start 发起流程:无 --values 列待填字段(含明细表;多选浏览框自动列前10选项);',
215
272
  ' --values/--values-file 给值(key=字段id,值=文本/option id;中文用 --values-file);',
216
273
  ' --detail-file 给明细表行数据(JSON 数组 [{index, rows:[{fieldId:value}]}],index=明细表序号0起,每行多字段);',
217
- ' --file <字段id>=<本地路径> 上传图片/附件(逗号分隔多个);默认 dry-run,--yes 真发',
274
+ ' --file <字段id>=<本地路径> 上传图片/附件;明细行用 <字段id>_<行号>=<路径>(逗号分隔多个);默认 dry-run,--yes 真发',
218
275
  ' email list 收件箱邮件列表(单步 GET /api/email/list/allList)',
219
276
  ' --page 页码,默认 1',
220
277
  ' --subject 主题关键字过滤',
@@ -265,7 +322,7 @@ function usage() {
265
322
  ' email size 邮箱容量统计;email accounts 接收账户列表',
266
323
  ' workplan list 日程列表(POST /api/workplan/calendar/getCalendarData,支持组织视图)',
267
324
  ' --date 参考日期 YYYY-MM-DD,默认今天',
268
- ' --view-type 视图:1=当天 / 3=月视图(默认)',
325
+ ' --view-type 视图:1=当天 / 2=周 / 3=月(默认)',
269
326
  ' --group 组织视图:部门/分部/上级/下级/常用组/关注;不传=默认当前用户',
270
327
  ' --type 日程类型 id 筛选,留空=全部',
271
328
  ' --json/--raw 同 todo',
@@ -284,7 +341,8 @@ function usage() {
284
341
  ' workplan search-conditions 搜索可用条件(POST /api/workplan/search/getCondition)',
285
342
  ' workplan attention 关注的日程(POST /api/workplan/calendar/getAttentionData)',
286
343
  ' workplan base-data 日历初始化数据(POST /api/workplan/calendar/getBaseData)',
287
- ' workplan search 日程表搜索(POST getlist → ec/dev/table/datas → counts,同待办 table 链路)',
344
+ ' workplan search 日程表搜索(POST getlist → ec/dev/table/datas → counts 按需,同待办 table 链路)',
345
+ ' --no-count 跳过总数请求(少一次 HTTP)',
288
346
  ' --name 名称关键字',
289
347
  ' --group 分组:全部/今天/本周/本月/本季/本年',
290
348
  ' --type 日程类型 id / --status 状态 id / --creater 创建人 id / --receiver 接收人 id',
@@ -346,7 +404,7 @@ function escapeLinkText(text) {
346
404
  return String(text).replace(/([\[\]()\\])/g, '\\$1');
347
405
  }
348
406
  /** 统一拼各域 list/search 项的 markdown 超链接;无 id/无可链目标时回退纯文本。
349
- * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'
407
+ * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'|'hrm'|'wfagent'
350
408
  * text: 展示文本(调用方已计算好回退值);ctx: cube 需 {modeId,formId,customid} */
351
409
  function hyperlinkFor(domain, item, base, text, ctx = {}) {
352
410
  const meta = linkMetaFor(domain, item, base, ctx);
@@ -361,6 +419,7 @@ function linkMetaFor(domain, item, base, ctx = {}) {
361
419
  switch (domain) {
362
420
  case 'todo':
363
421
  case 'odoc':
422
+ case 'wfagent': // 仅 records 视图的行有 requestId(代理产生的流程);mine/all 是代理配置、无流程详情页
364
423
  if (item?.requestId)
365
424
  url = buildRequestFormUrl(b, item.requestId);
366
425
  urlType = url ? 'detail' : '';
@@ -490,6 +549,21 @@ async function runCreatePrepare(args) {
490
549
  console.log(`\n确认后:e9 create create ${updated.id} --confirm --submit`);
491
550
  }
492
551
 
552
+ /** 打印流程公式本地计算结果(主表/明细行)。 */
553
+ function printFormulaResults(prepared) {
554
+ const applied = prepared?.formulas?.applied ?? [];
555
+ if (applied.length > 0) {
556
+ console.log(' 📐 公式计算:');
557
+ for (const f of applied)
558
+ console.log(` ${f.dest} = ${f.value}${f.row >= 0 ? `(明细第 ${f.row + 1} 行)` : ''} (${f.formula})`);
559
+ }
560
+ const skipped = prepared?.formulas?.skipped ?? [];
561
+ if (skipped.length > 0) {
562
+ console.error(' ⚠ 未计算的公式(不支持或缺少引用):');
563
+ for (const s of skipped.slice(0, 8))
564
+ console.error(` ${s.dest}: ${s.reason}`);
565
+ }
566
+ }
493
567
  async function runContextCreate(args) {
494
568
  const id = args._[2];
495
569
  if (!id)
@@ -498,6 +572,7 @@ async function runContextCreate(args) {
498
572
  if (!args.flags.confirm || !args.flags.submit) {
499
573
  const cfg = loadConfig(flagStr(args, 'config'));
500
574
  const prepared = await prepareCreateSubmission(cfg, context.workflowId, context.values, context.requestName, context.details);
575
+ printFormulaResults(prepared);
501
576
  const plan = saveSubmissionPlan({ ...prepared, files: context.files });
502
577
  updateContext(id, { planId: plan.id });
503
578
  console.log(BOX_TOP);
@@ -657,25 +732,46 @@ async function runCreateStart(args) {
657
732
  const send = !!args.flags.yes;
658
733
  let wfId;
659
734
  if (!id && name) {
660
- // 按名查;多个候选则列出让用户选
661
- const hits = await findCreateWfByName(cfg, name);
662
- if (hits.length === 0) {
663
- console.error(`按名「${name}」未找到流程。`);
664
- process.exit(1);
735
+ // ★ 记忆优先(精确 + 模糊):查用户是否记住过这个关键词对应的流程
736
+ const { mem } = await loadMemory(cfg);
737
+ let shortcut = findShortcut(cfg, mem.selfId, name);
738
+ let matchedBy = 'exact';
739
+ if (!shortcut) {
740
+ const fuzzy = findShortcutFuzzy(cfg, mem.selfId, name);
741
+ if (fuzzy) {
742
+ shortcut = fuzzy.shortcut;
743
+ matchedBy = fuzzy.matchedBy;
744
+ }
665
745
  }
666
- if (hits.length > 1) {
667
- console.log(BOX_TOP);
668
- console.log(` 「${name}」找到 ${hits.length} 个相似流程,请选一个:`);
669
- console.log(DIVIDER);
670
- for (let i = 0; i < hits.length; i++) {
671
- const w = hits[i];
672
- console.log(` ${String(i + 1).padStart(2, ' ')}. ${w.name} [id=${w.id ?? ''}]${w.spell ? ` (${w.spell})` : ''}`);
746
+ if (shortcut) {
747
+ // 用模糊匹配到的实际 key 来更新 hit 计数(而非用户输入的 name)
748
+ const hitKey = matchedBy === 'exact' ? name : (fuzzy?.keyword || name);
749
+ hitShortcut(cfg, mem.selfId, hitKey);
750
+ wfId = shortcut.workflowId;
751
+ if (matchedBy !== 'exact') {
752
+ console.error(`记忆关联: 「${name}」→「${shortcut.workflowName || shortcut.workflowId}」(id=${shortcut.workflowId})`);
673
753
  }
674
- console.log(BOX_TOP);
675
- console.log(' 用 --id <流程id> 指定其中一个再跑。');
676
- process.exit(0);
754
+ } else {
755
+ // 按名查;多个候选则列出让用户选
756
+ const hits = await findCreateWfByName(cfg, name, mem.selfId);
757
+ if (hits.length === 0) {
758
+ console.error(`按名「${name}」未找到流程。`);
759
+ process.exit(1);
760
+ }
761
+ if (hits.length > 1) {
762
+ console.log(BOX_TOP);
763
+ console.log(` 「${name}」找到 ${hits.length} 个相似流程,请选一个:`);
764
+ console.log(DIVIDER);
765
+ for (let i = 0; i < hits.length; i++) {
766
+ const w = hits[i];
767
+ console.log(` ${String(i + 1).padStart(2, ' ')}. ${w.name} [id=${w.id ?? ''}]${w.spell ? ` (${w.spell})` : ''}`);
768
+ }
769
+ console.log(BOX_TOP);
770
+ console.log(' 用 --id <流程id> 指定其中一个再跑。');
771
+ process.exit(0);
772
+ }
773
+ wfId = hits[0].id ?? '';
677
774
  }
678
- wfId = hits[0].id ?? '';
679
775
  }
680
776
  else {
681
777
  try {
@@ -688,7 +784,7 @@ async function runCreateStart(args) {
688
784
  }
689
785
  const r = await getStartableFields(cfg, wfId);
690
786
  const requestname = flagStr(args, 'requestname');
691
- // --file <字段id>=<本地路径>(逗号分隔多个):上传图片/附件
787
+ // --file <字段id>[ _<行号>]=<本地路径>(逗号分隔多个):上传图片/附件;明细行带 _<行号>
692
788
  const fileRaw = flagStr(args, 'file');
693
789
  const files = [];
694
790
  if (fileRaw) {
@@ -736,23 +832,25 @@ async function runCreateStart(args) {
736
832
  process.exit(1);
737
833
  }
738
834
  }
739
- // 无值来源且无 --file 且无 --detail-file:打印待填字段清单(供 AI 问答)
740
- if (!valuesRawResolved && files.length === 0 && details.length === 0) {
835
+ // 无值来源且无 --file 且无 --detail-file:打印待填字段清单(供 AI 问答)。
836
+ // 例外:--yes --plan 是复现已确认计划,允许不再重复传值/明细/附件。
837
+ const replayPlan = send && Boolean(flagStr(args, 'plan'));
838
+ if (!valuesRawResolved && files.length === 0 && details.length === 0 && !replayPlan) {
741
839
  if (args.flags.json) {
742
- console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, fields: r.fields, detailTables: r.detailTables, submitParams: r.submitParams }, null, 2));
840
+ console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, hiddenCount: r.hiddenCount, fields: r.fields, detailTables: r.detailTables, submitParams: r.submitParams }, null, 2));
743
841
  return;
744
842
  }
745
843
  console.log(BOX_TOP);
746
844
  console.log(` E9 发起流程 · ${r.workflowName ?? 'workflowid=' + r.workflowId} (id=${r.workflowId})`);
747
- console.log(` 主表业务字段 ${r.fields.length} 个 · 明细表 ${r.detailTables.length} 个 · verifyRight=${r.verifyRight ? '✅ 有权限' : '❌ 无权限'}`);
845
+ console.log(` 主表业务字段 ${r.fields.length} 个 · 明细表 ${r.detailTables.length} 个 · verifyRight=${r.verifyRight ? '✅ 有权限' : '❌ 无权限'}${r.hiddenCount ? ` · 发起节点隐藏字段 ${r.hiddenCount} 个(未展示)` : ''}`);
748
846
  console.log(BOX_TOP);
749
- console.log(' 注:viewattr 是 loadForm 静态值,不反映运行时可编辑性(va=3 隐藏实际可能可填)。填哪些由你定。');
847
+ console.log(' 注:字段是否可填以「可编辑 / 🚫原因」标注为准;viewattr 0=隐藏/1=只读不可填、2=可编辑/3=必填可填,另叠加 isonlyshow/isedit/isview 权限位。');
750
848
  if (r.fields.length === 0) {
751
849
  console.log('\n (无主表业务字段——该流程可能是自由流程或仅系统字段)');
752
850
  }
753
851
  for (const f of r.fields) {
754
852
  console.log(`\n ▸ [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''}`);
755
- const meta = [f.htmltypeLabel, f.viewattrLabel ? `静态${f.viewattrLabel}` : null, f.inputHint, f.currentValue ? `当前值=${f.currentValue}` : null, f.defaultValue ? `默认=${f.defaultValue}` : null].filter(Boolean).join(' · ');
853
+ const meta = [f.htmltypeLabel, fieldStatusLabel({ ...f, currentValue: f.currentValue ?? f.defaultValue }), f.inputHint].filter(Boolean).join(' · ');
756
854
  if (meta)
757
855
  console.log(` ${meta}`);
758
856
  if (f.options && f.options.length > 0) {
@@ -773,7 +871,7 @@ async function runCreateStart(args) {
773
871
  console.log('\n ─────────────────────────────────────────────');
774
872
  console.log(' 给值:--values-file \'<json>\' key=字段id,值=文本/option id(中文用 --values-file 避免乱码)');
775
873
  console.log(' 明细:--detail-file \'<json>\' [{index:0, rows:[{22106:"v",22107:"v2"}, ...]}, ...](index=明细表序号0起,每行多字段)');
776
- console.log(' 上传:--file <字段id>=<本地路径> 逗号分隔多个;图片按扩展名自动判 listType');
874
+ console.log(' 上传:--file <字段id>=<本地路径> 明细行用 <字段id>_<行号>=<路径>;逗号分隔多个;图片按扩展名自动判 listType');
777
875
  console.log(' 发起:默认 dry-run,确认后加 --yes 真提交');
778
876
  return;
779
877
  }
@@ -787,6 +885,8 @@ async function runCreateStart(args) {
787
885
  console.error(`--values / --values-file 不是合法 JSON:${valuesRawResolved}`);
788
886
  process.exit(1);
789
887
  }
888
+ // ★ 记忆默认值已移到 prepareCreateSubmission 内、表单代码块执行之后应用,
889
+ // 保证优先级为:用户手填 > 表单代码块 > 记忆默认值 > 内置标签默认值。
790
890
  }
791
891
  // 预览与提交必须共用同一份已解析字段,避免默认值、联动或浏览框名称在确认后改变。
792
892
  if (!send || args.flags.dryRun) {
@@ -799,7 +899,8 @@ async function runCreateStart(args) {
799
899
  console.log(` ▸ field${fl.fieldId} ← ${fl.filePath}`);
800
900
  console.log(' 附件上传会产生新文件 ID,不能冻结到本次确认计划;请在无附件字段变更时单独确认。');
801
901
  }
802
- const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details);
902
+ const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details, r);
903
+ printFormulaResults(prepared);
803
904
  const plan = saveSubmissionPlan({ ...prepared, files });
804
905
  if (details.length > 0) {
805
906
  console.log(BOX_TOP);
@@ -821,7 +922,7 @@ async function runCreateStart(args) {
821
922
  }
822
923
  const plan = loadSubmissionPlan(planId, wfId);
823
924
  // 真提交只刷新 loadForm 的短期签名参数;业务字段、明细与浏览框名称完全复用确认计划。
824
- const submitResult = await submitCreateRequest(cfg, wfId, plan.values, plan.requestName, files, plan.details, plan);
925
+ const submitResult = await submitCreateRequest(cfg, wfId, plan.values, plan.requestName, files, plan.details, plan, { keyword: name || '', formData: r });
825
926
  const res = submitResult.response;
826
927
  const j = res.json;
827
928
  const rid = j?.requestid ?? j?.data?.requestid ?? j?.data?.resultInfo?.requestid ?? j?.data?.requestId;
@@ -1081,7 +1182,9 @@ async function runCreateFields(args) {
1081
1182
  process.exit(1);
1082
1183
  }
1083
1184
  if (!id) {
1084
- const hits = await findCreateWfByName(cfg, name);
1185
+ let selfId = '';
1186
+ try { const { mem } = await loadMemory(cfg); selfId = mem.selfId; } catch { }
1187
+ const hits = await findCreateWfByName(cfg, name, selfId);
1085
1188
  if (hits.length === 0) {
1086
1189
  console.error(`按名「${name}」未找到流程`);
1087
1190
  process.exit(1);
@@ -1095,15 +1198,23 @@ async function runCreateFields(args) {
1095
1198
  id = hits[0].id;
1096
1199
  }
1097
1200
  const r = await describeFormFields(cfg, id);
1201
+ // 发起节点隐藏字段(viewattr=0)不展示——它们在后续节点才填,列出来会误导用户/AI 去填。
1202
+ let hiddenCount = 0;
1203
+ const visibleTables = r.tables.map((t) => {
1204
+ hiddenCount += t.fields.filter((f) => f.viewattr === 0).length;
1205
+ return { ...t, fields: t.fields.filter((f) => f.viewattr !== 0) };
1206
+ });
1098
1207
  if (args.flags.json) {
1099
- console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, totalFields: r.totalFields, tables: r.tables }, null, 2));
1208
+ console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, totalFields: r.totalFields, hiddenCount, tables: visibleTables }, null, 2));
1100
1209
  return;
1101
1210
  }
1102
1211
  console.log(`E9 表单字段 [${r.workflowName ?? 'workflowid=' + r.workflowId}] ${r.totalFields} 个字段 verifyRight=${r.verifyRight}`);
1103
- for (const t of r.tables) {
1212
+ if (hiddenCount > 0)
1213
+ console.log(`(另有 ${hiddenCount} 个「发起节点隐藏」字段未展示——它们在后续节点才填)`);
1214
+ for (const t of visibleTables) {
1104
1215
  console.log(`\n■ ${t.key === 'main' ? '主表' : t.key}(${t.fields.length} 字段)`);
1105
1216
  for (const f of t.fields) {
1106
- console.log(` [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''} ${f.viewattrLabel ? '静态' + f.viewattrLabel : '?'} ${f.htmltypeLabel ?? '?'}`);
1217
+ console.log(` [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''} ${fieldStatusLabel(f)} ${f.htmltypeLabel ?? '?'}`);
1107
1218
  // 选择框选项(selectattr.selectitemlist)
1108
1219
  const items = f.raw?.selectattr?.selectitemlist;
1109
1220
  if (Array.isArray(items) && items.length > 0) {
@@ -1112,7 +1223,7 @@ async function runCreateFields(args) {
1112
1223
  }
1113
1224
  }
1114
1225
  console.log('\n注:必填标记不在 loadForm 接口(前端 DOM 控制),本表不显示必填。');
1115
- console.log(' 可编辑/只读/隐藏由 viewattr 给出——viewattr 是表单设计态静态值(va=1只读/2可编辑/3隐藏),不反映运行时实际可编辑性(va=3 隐藏实际可能可填,运行时权限以节点配置/联动为准)。');
1226
+ console.log(' viewattr 取值:0=隐藏 / 1=只读 / 2=可编辑 / 3=必填;另叠加 isonlyshow/isedit/isview 权限位。');
1116
1227
  }
1117
1228
  async function runCreateOpen(args) {
1118
1229
  const cfg = loadConfig(flagStr(args, 'config'));
@@ -1142,7 +1253,9 @@ async function runCreateOpen(args) {
1142
1253
  wfId = id;
1143
1254
  }
1144
1255
  else {
1145
- const hits = await findCreateWfByName(cfg, name);
1256
+ let selfId = '';
1257
+ try { const { mem } = await loadMemory(cfg); selfId = mem.selfId; } catch { }
1258
+ const hits = await findCreateWfByName(cfg, name, selfId);
1146
1259
  if (hits.length === 0) {
1147
1260
  console.error(`按名「${name}」未找到可建流程。先跑 e9 create list --keyword "${name}" 看候选。`);
1148
1261
  process.exit(1);
@@ -1200,6 +1313,58 @@ async function runBlog(args) {
1200
1313
  const sub = args._[1] ?? 'list';
1201
1314
  const cfg = loadConfig(flagStr(args, 'config'));
1202
1315
  const blogBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
1316
+ if (sub === 'subordinates') {
1317
+ const date = flagStr(args, 'date') ?? todayStr();
1318
+ const allLevel = flagBool(args, 'all-level');
1319
+ const names = String(flagStr(args, 'names') ?? '').split(/[,,、\s]+/).map((n) => n.trim()).filter(Boolean);
1320
+ const r = await collectSubordinateReports(cfg, { date, allLevel, names });
1321
+ if (r.unmatched.length)
1322
+ console.error(`⚠ 未在下属列表中找到:${r.unmatched.join('、')}`);
1323
+ if (!r.complete)
1324
+ console.error('⚠ 下属数量超过单次拉取上限,结果可能不完整;请用 --names 缩小范围。');
1325
+ if (args.flags.json) {
1326
+ console.log(JSON.stringify({
1327
+ date: r.date,
1328
+ allLevel: r.allLevel,
1329
+ total: r.total,
1330
+ writtenCount: r.writtenCount,
1331
+ unwrittenCount: r.total - r.writtenCount,
1332
+ unmatched: r.unmatched,
1333
+ items: r.items.map((it) => ({
1334
+ ...withLink('hrm', { id: it.id, name: it.name }, blogBase),
1335
+ written: it.written,
1336
+ count: it.count,
1337
+ recalled: it.recalled,
1338
+ error: it.error,
1339
+ posts: it.posts.map((post) => withLink('blog', post, blogBase)),
1340
+ })),
1341
+ }, null, 2));
1342
+ return;
1343
+ }
1344
+ if (r.total === 0) {
1345
+ console.log('(无下属)');
1346
+ return;
1347
+ }
1348
+ const levelLabel = allLevel ? '全部层级' : '直接下属';
1349
+ console.log(`E9 下属日报 [${r.date} · ${levelLabel}] 共 ${r.total} 人:已写 ${r.writtenCount} 人 / 未写 ${r.total - r.writtenCount} 人`);
1350
+ for (let i = 0; i < r.items.length; i++) {
1351
+ const it = r.items[i];
1352
+ const who = hyperlinkFor('hrm', { id: it.id }, blogBase, it.name || '(无姓名)');
1353
+ if (it.error) {
1354
+ console.log(`${i + 1}. ${who} ⚠ 查询失败:${it.error}`);
1355
+ continue;
1356
+ }
1357
+ if (!it.written) {
1358
+ console.log(`${i + 1}. ${who} ⚠️ 未写${it.recalled ? `(${it.recalled} 条已撤回)` : ''}`);
1359
+ continue;
1360
+ }
1361
+ const latest = it.posts[0];
1362
+ const snippet = String(latest?.content ?? '').replace(/\s+/g, ' ').slice(0, 50) || '(无内容)';
1363
+ const link = latest ? hyperlinkFor('blog', latest, blogBase, snippet) : snippet;
1364
+ console.log(`${i + 1}. ${who} ✅ 已写(${it.count} 条) 最新:${link}`);
1365
+ }
1366
+ return;
1367
+ }
1203
1368
  if (sub === 'check') {
1204
1369
  const date = flagStr(args, 'date') ?? todayStr();
1205
1370
  // check 要判定整天的正常发布记录:逐页拉取 + 重复检测终止(服务端无 total、越界页重复回放)
@@ -1830,7 +1995,7 @@ async function runBlog(args) {
1830
1995
  console.log(JSON.stringify(r.template, null, 2));
1831
1996
  return;
1832
1997
  }
1833
- 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}`);
1998
+ console.error(`blog 暂只支持 list / subordinates / 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}`);
1834
1999
  process.exit(1);
1835
2000
  }
1836
2001
  async function runWorkplan(args) {
@@ -1861,6 +2026,13 @@ async function runWorkplan(args) {
1861
2026
  const group = flagStr(args, 'group');
1862
2027
  const date = flagStr(args, 'date');
1863
2028
  const type = flagStr(args, 'type');
2029
+ // --view-type:1=当天 / 2=周 / 3=月(默认)。
2030
+ // 此前 list 分支把 viewType 写死为 '3',导致 --view-type 静默失效(传 1/2 都还是月视图,且不报错)。
2031
+ const viewType = flagStr(args, 'view-type') ?? '3';
2032
+ if (!['1', '2', '3'].includes(viewType)) {
2033
+ console.error(`workplan list 的 --view-type 只支持 1(当天)/ 2(周)/ 3(月),收到: ${viewType}`);
2034
+ process.exit(1);
2035
+ }
1864
2036
  // 默认(无 --group)=「我的日程」:查当前用户自己的日程,走 MyCalendar(selectUser 空=当前用户)。
1865
2037
  // 只有显式传 --group 才走组织视图 getCalendarData(部门/分部/下级等)。
1866
2038
  if (!group) {
@@ -1868,7 +2040,7 @@ async function runWorkplan(args) {
1868
2040
  // ★越权防护:--select-user 指定他人日程时目标必须 ∈ {本人 + 全部层级下属}
1869
2041
  if (selectUser)
1870
2042
  await assertUserIdsInScope(cfg, [selectUser], { domain: 'workplan' });
1871
- const r = await getMyCalendar(cfg, { selectDate: date, viewType: '3', selectUser: selectUser ?? '', workPlanType: type });
2043
+ const r = await getMyCalendar(cfg, { selectDate: date, viewType, selectUser: selectUser ?? '', workPlanType: type });
1872
2044
  if (args.flags.raw) {
1873
2045
  console.error('===== RAW MyCalendar =====');
1874
2046
  console.error(JSON.stringify(r.raw, null, 2));
@@ -1885,7 +2057,10 @@ async function runWorkplan(args) {
1885
2057
  }, null, 2));
1886
2058
  return;
1887
2059
  }
1888
- console.log(`E9 日程 [${r.selectDate}] 共 ${r.items.length} 条`);
2060
+ // 标明视图与覆盖区间——周/月视图下只看 selectDate 无法判断统计了哪几天
2061
+ const viewLabel = viewType === '1' ? '当天' : viewType === '2' ? '本周' : '本月';
2062
+ const range = r.range?.start && r.range?.end ? ` ${r.range.start} ~ ${r.range.end}` : '';
2063
+ console.log(`E9 日程 [${r.selectDate} · ${viewLabel}]${range} 共 ${r.items.length} 条`);
1889
2064
  if (r.items.length === 0) {
1890
2065
  console.log('(该区间无日程)');
1891
2066
  return;
@@ -1924,7 +2099,8 @@ async function runWorkplan(args) {
1924
2099
  objIds = bd.dept[0]?.id ?? '';
1925
2100
  }
1926
2101
  }
1927
- const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type });
2102
+ // 组织视图同样跟随 --view-type(getCalendarData 的 bywhat:1=日 / 2=周 / 3=月)
2103
+ const r = await getCalendarData(cfg, { selectDate: date, objType, objIds, workPlanType: type, bywhat: viewType });
1928
2104
  if (args.flags.raw) {
1929
2105
  console.error('===== RAW getCalendarData =====');
1930
2106
  console.error(JSON.stringify(r.raw, null, 2));
@@ -1947,7 +2123,11 @@ async function runWorkplan(args) {
1947
2123
  return;
1948
2124
  }
1949
2125
  const groupLabel = ` · ${group}`;
1950
- console.log(`E9 日程 [${r.selectDate}${groupLabel}] 共 ${r.count} 条`);
2126
+ // 组织视图无 start/end 字段,区间从 days.value 推导(首日~末日)
2127
+ const gDays = r.days?.value ?? [];
2128
+ const gRange = gDays.length ? ` ${gDays[0]} ~ ${gDays[gDays.length - 1]}` : '';
2129
+ const gViewLabel = viewType === '1' ? '当天' : viewType === '2' ? '本周' : '本月';
2130
+ console.log(`E9 日程 [${r.selectDate} · ${gViewLabel}${groupLabel}]${gRange} 共 ${r.count} 条`);
1951
2131
  if (r.count === 0) {
1952
2132
  console.log('(该区间无日程)');
1953
2133
  return;
@@ -2102,6 +2282,7 @@ async function runWorkplan(args) {
2102
2282
  prjids: flagStr(args, 'prj-id'),
2103
2283
  crmids: flagStr(args, 'crm-id'),
2104
2284
  page: Number(flagStr(args, 'page') ?? '1') || 1,
2285
+ withCount: !flagBool(args, 'no-count'),
2105
2286
  });
2106
2287
  if (args.flags.raw) {
2107
2288
  console.error('===== RAW getlist =====');
@@ -2299,6 +2480,560 @@ async function runWorkplan(args) {
2299
2480
  console.error(`workplan 暂只支持 list / view / finish / delete / types / search-conditions / search / create / my-calendar / attention / base-data / export / import-template / import,收到: ${sub}`);
2300
2481
  process.exit(1);
2301
2482
  }
2483
+ async function runSupervise(args) {
2484
+ const sub = args._[1] ?? 'list';
2485
+ const cfg = loadConfig(flagStr(args, 'config'));
2486
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2487
+ if (sub === 'count') {
2488
+ const r = await getSuperviseCount(cfg);
2489
+ if (args.flags.json) {
2490
+ console.log(JSON.stringify({ total: r.total, pending: r.pending }, null, 2));
2491
+ return;
2492
+ }
2493
+ console.log(`E9 流程督办计数:共 ${r.total ?? '?'} 条,其中未督办 ${r.pending ?? '?'} 条。`);
2494
+ return;
2495
+ }
2496
+ if (sub === 'conditions') {
2497
+ const r = await getSuperviseConditions(cfg);
2498
+ if (args.flags.json) {
2499
+ console.log(JSON.stringify(r.groups, null, 2));
2500
+ return;
2501
+ }
2502
+ console.log('E9 流程督办 搜索条件:');
2503
+ for (const g of r.groups) {
2504
+ console.log(` ${g.title}:`);
2505
+ for (const it of g.items)
2506
+ console.log(` - ${it.label} (${it.domkey}${it.type ? `, ${it.type}` : ''})`);
2507
+ }
2508
+ return;
2509
+ }
2510
+ if (sub === 'send') {
2511
+ const requestIds = flagStr(args, 'requestids') ?? '';
2512
+ if (!requestIds) {
2513
+ console.error('supervise send 需要 --requestids "id1,id2"');
2514
+ process.exit(1);
2515
+ }
2516
+ const remark = flagStr(args, 'remark') ?? '';
2517
+ const r = await doSupervise(cfg, { requestIds, remark, send: !!args.flags.yes });
2518
+ if (!r.sent) {
2519
+ console.log(BOX_TOP);
2520
+ console.log(` [dry-run] 将发起督办(${r.requestIds.length} 条)`);
2521
+ console.log(DIVIDER);
2522
+ console.log(` requestids = ${r.requestIds.join(', ')}`);
2523
+ console.log(` 督办意见 = ${r.remark || '(空)'}`);
2524
+ console.log(' 确认无误后加 --yes 真正发起督办。');
2525
+ console.log(BOX_TOP);
2526
+ return;
2527
+ }
2528
+ const j = r.response?.json;
2529
+ const ok = j?.status === true || j?.status === '1' || j?.status === 1 || j?.success === true;
2530
+ console.log(`${ok ? '✅' : '⚠'} 督办已请求处理(${r.requestIds.length} 条) HTTP ${r.response?.status}`);
2531
+ if (j)
2532
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
2533
+ return;
2534
+ }
2535
+ if (sub !== 'list') {
2536
+ console.error(`supervise 暂只支持 list / count / conditions / send,收到: ${sub}`);
2537
+ process.exit(1);
2538
+ }
2539
+ const tabSpec = flagStr(args, 'tab');
2540
+ const tabKey = resolveSuperviseTab(tabSpec);
2541
+ if (tabKey === undefined) {
2542
+ console.error(`未知督办页签「${tabSpec}」,可选:${Object.keys(SUPERVISE_TABS).join(' / ')}`);
2543
+ process.exit(1);
2544
+ }
2545
+ const r = await listSupervise(cfg, {
2546
+ tabKey,
2547
+ keyword: flagStr(args, 'keyword'),
2548
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2549
+ pageSize: flagStr(args, 'page-size'),
2550
+ creator: flagStr(args, 'creator'),
2551
+ creatorDept: flagStr(args, 'creator-dept'),
2552
+ creatorSubcom: flagStr(args, 'creator-subcom'),
2553
+ });
2554
+ const tabName = Object.keys(SUPERVISE_TABS).find((k) => SUPERVISE_TABS[k] === tabKey) ?? tabKey;
2555
+ if (args.flags.raw) {
2556
+ console.error('===== RAW getSessionkey =====');
2557
+ console.error(JSON.stringify(r.raw.getSessionkey, null, 2));
2558
+ console.error('===== RAW datas =====');
2559
+ console.error(JSON.stringify(r.raw.datas, null, 2));
2560
+ }
2561
+ if (args.flags.json) {
2562
+ console.log(JSON.stringify({
2563
+ tab: tabName, tabKey, page: r.page, pageSize: r.pageSize, total: r.total, count: r.items.length,
2564
+ items: r.items.map((it) => withLink('todo', it, base)),
2565
+ }, null, 2));
2566
+ return;
2567
+ }
2568
+ console.log(`E9 流程督办 [${tabName}] 第 ${r.page} 页 共 ${r.total ?? '?'} 条`);
2569
+ if (!r.items.length) {
2570
+ console.log('(无数据)');
2571
+ return;
2572
+ }
2573
+ for (let i = 0; i < r.items.length; i++) {
2574
+ const it = r.items[i];
2575
+ console.log(`${i + 1}. ${hyperlinkFor('todo', it, base, it.title || '(无标题)')}`);
2576
+ const meta = [it.creator, it.node, it.createTime, it.unoperators ? `未操作者=${it.unoperators}` : null].filter(Boolean).join(' | ');
2577
+ if (meta)
2578
+ console.log(` ${meta}`);
2579
+ }
2580
+ if (r.total != null && r.items.length < r.total)
2581
+ console.log(`还有 ${r.total - r.items.length} 条,用 --page <页码> 继续。`);
2582
+ }
2583
+
2584
+ /**
2585
+ * 流程代理(wfagent):list / conditions / templates / create。
2586
+ * ⚠️ 编辑、收回、批量编辑、模板删除在 E9 里存在但**未抓到包**,本域未实现(见 references/wfagent.md)。
2587
+ * ⚠️ create 的 4 个开关(isCreateAgenter/isProxyDeal/isSysCreateAgenter/isPendThing)语义未校准,
2588
+ * 默认值 1 照抄抓包样本,dry-run 预览里会显式打印,可用对应命令行开关覆盖。
2589
+ */
2590
+ async function runWfagent(args) {
2591
+ const sub = args._[1] ?? 'list';
2592
+ const cfg = loadConfig(flagStr(args, 'config'));
2593
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2594
+ const viewSpec = flagStr(args, 'view');
2595
+ const view = resolveWfagentView(viewSpec);
2596
+ if (view === undefined) {
2597
+ console.error(`未知代理视图「${viewSpec}」,可选:${Object.keys(WFAGENT_VIEWS).join(' / ')}(mine=我设置的 / all=全部 / records=代理办理记录)`);
2598
+ process.exit(1);
2599
+ }
2600
+
2601
+ if (sub === 'conditions') {
2602
+ const r = await getAgentConditions(cfg, { view });
2603
+ if (args.flags.json) {
2604
+ console.log(JSON.stringify(r.groups, null, 2));
2605
+ return;
2606
+ }
2607
+ console.log(`E9 流程代理 搜索条件(视图:${WFAGENT_VIEWS[view].title}):`);
2608
+ for (const g of r.groups) {
2609
+ console.log(` ${g.title}:`);
2610
+ for (const it of g.items)
2611
+ console.log(` - ${it.label} (${it.domkey}${it.type ? `, ${it.type}` : ''})${it.options.length ? ` 可选:${it.options.map((o) => `${o.name}=${o.key}`).join(' / ')}` : ''}`);
2612
+ }
2613
+ return;
2614
+ }
2615
+
2616
+ if (sub === 'templates') {
2617
+ const r = await listAgentTemplates(cfg, {
2618
+ name: flagStr(args, 'name'),
2619
+ workflowId: flagStr(args, 'workflow'),
2620
+ agentUid: flagStr(args, 'agent'),
2621
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2622
+ });
2623
+ if (args.flags.json) {
2624
+ console.log(JSON.stringify({ page: r.page, total: r.total, count: r.items.length, items: r.items.map(({ raw, ...rest }) => rest) }, null, 2));
2625
+ return;
2626
+ }
2627
+ console.log(`E9 代理模板 第 ${r.page} 页 共 ${r.total ?? '?'} 条`);
2628
+ if (!r.items.length) {
2629
+ console.log('(无数据)');
2630
+ return;
2631
+ }
2632
+ for (let i = 0; i < r.items.length; i++) {
2633
+ const it = r.items[i];
2634
+ // 模板没有可跳转的流程详情页,故不出链接(列表超链接规则只针对流程列表)
2635
+ console.log(`${i + 1}. ${it.name || '(未命名)'} [模板id=${it.templateId}]`);
2636
+ console.log(` 代理人=${it.agent || '?'} | 流程范围=${it.workflowRange || '?'}`);
2637
+ }
2638
+ return;
2639
+ }
2640
+
2641
+ if (sub === 'create') {
2642
+ const agentSpec = flagStr(args, 'agent');
2643
+ if (!agentSpec) {
2644
+ console.error('wfagent create 需要 --agent <代理人>(姓名或用户id)');
2645
+ process.exit(1);
2646
+ }
2647
+ // 代理人=委托对象,不限于下属 → 只走 at-list 姓名解析,不做授权范围过滤
2648
+ const agentUid = await resolveNameToUid(cfg, agentSpec);
2649
+ // 被代理人默认=当前登录人(getAddAgentInfo 的 formValue);显式指定他人时按越权策略校验
2650
+ const beAgentSpec = flagStr(args, 'be-agent');
2651
+ let beAgentUid = '';
2652
+ let beAgentLabel = '';
2653
+ if (beAgentSpec) {
2654
+ beAgentUid = await assertNamesInScope(cfg, [beAgentSpec], { domain: 'wfagent' });
2655
+ beAgentLabel = beAgentSpec;
2656
+ }
2657
+ else {
2658
+ const info = await getAddAgentInfo(cfg);
2659
+ beAgentUid = info.defaultBeAgentUid;
2660
+ beAgentLabel = info.defaultBeAgent || beAgentUid;
2661
+ if (!beAgentUid) {
2662
+ console.error('未能从 getAddAgentInfo 取到当前登录人作为被代理人,请用 --be-agent 显式指定。');
2663
+ process.exit(1);
2664
+ }
2665
+ }
2666
+ const { beginTime, endTime } = parseTimeSpec(flagStr(args, 'time'));
2667
+ const body = buildAgentBody({
2668
+ agentUid,
2669
+ beAgentUid,
2670
+ beginDate: flagStr(args, 'begin'),
2671
+ endDate: flagStr(args, 'end'),
2672
+ beginTime,
2673
+ endTime,
2674
+ workflowIds: flagStr(args, 'workflow'),
2675
+ range: flagStr(args, 'range'),
2676
+ isCreateAgenter: flagStr(args, 'agent-create-flow'),
2677
+ isProxyDeal: flagStr(args, 'proxy-deal'),
2678
+ isSysCreateAgenter: flagStr(args, 'sys-create'),
2679
+ isPendThing: flagStr(args, 'pend-thing'),
2680
+ });
2681
+ const overlap = await verifyOverlap(cfg, body);
2682
+ const templateName = flagStr(args, 'save-template');
2683
+ if (!args.flags.yes) {
2684
+ console.log(BOX_TOP);
2685
+ console.log(' [dry-run] 新建流程代理');
2686
+ console.log(DIVIDER);
2687
+ console.log(` 被代理人 = ${beAgentLabel}(${body.bagentuid})`);
2688
+ console.log(` 代理人 = ${agentSpec}(${body.agentuid})`);
2689
+ console.log(` 代理期间 = ${body.beginDate} ~ ${body.endDate}${beginTime || endTime ? ` ${beginTime || '00:00'}~${endTime || '23:59'}` : '(全天)'}`);
2690
+ console.log(` 流程范围 = ${body.agentrange === '2' ? '全部流程' : `指定流程 ${body.rangetype}`}`);
2691
+ if (body.agentrange === '2')
2692
+ console.log(' ⚠️ 「全部流程」= 会为**你的每一个流程各建一条**代理记录(实测本环境 466 条),**不是 1 条**!\n 只想试一条请改用 --workflow <单个流程id>。建议保存后到 OA「流程代理」页核对。');
2693
+ console.log(` 开关 = isCreateAgenter=${body.isCreateAgenter} isProxyDeal=${body.isProxyDeal} isSysCreateAgenter=${body.isSysCreateAgenter} isPendThing=${body.isPendThing}`);
2694
+ console.log(' 开关含义(据 workflow_agent 表结构校准):isCreateAgenter=代理创建权限 / isProxyDeal=流转中的待办 /');
2695
+ console.log(' isPendThing=是否把待办事宜转给代理人 / isSysCreateAgenter=系统创建代理标记。默认 1 来自抓包样本;');
2696
+ console.log(' 如需调整:--agent-create-flow / --proxy-deal / --sys-create / --pend-thing');
2697
+ console.log(` 重叠校验 = ${overlap.overlapCount ? `⚠️ 与 ${overlap.overlapCount} 条已有代理重叠(overlapIds=${overlap.overlapIds})` : '✅ 无重叠'}`);
2698
+ if (templateName)
2699
+ console.log(` 存为模板 = ${templateName}`);
2700
+ console.log(DIVIDER);
2701
+ console.log(` 完整请求体:${JSON.stringify(body)}`);
2702
+ console.log(BOX_TOP);
2703
+ console.log(' 确认无误后加 --yes 真正保存。');
2704
+ return;
2705
+ }
2706
+ let tplResult = null;
2707
+ if (templateName)
2708
+ tplResult = await saveAgentTemplate(cfg, body, templateName);
2709
+ const r = await addAgent(cfg, { body, send: true });
2710
+ const ok = /success/i.test(String(r.result ?? ''));
2711
+ console.log(`${ok ? '✅' : '⚠'} 代理保存已请求(agentresult=${r.result || '无'})`);
2712
+ if (tplResult)
2713
+ console.log(` 模板「${templateName}」:${tplResult.ok ? '✅ 已保存' : '⚠️ 未确认'}`);
2714
+ console.log(` 响应:${JSON.stringify(r.raw ?? {})}`);
2715
+ // 回读核验:到列表里找这条(找不回报「未确认」,不谎报成功)
2716
+ // ⚠️ 必须带 status=1(未结束)并**翻页**:按人筛选在 all 视图可能命中数百条已结束的历史,
2717
+ // 只翻第 1 页会漏掉刚建的那条,误报「回读未确认」。
2718
+ try {
2719
+ const want = (it) => it.agentUid === body.agentuid && it.beginDate === body.beginDate && it.endDate === body.endDate;
2720
+ let hit = null;
2721
+ for (let page = 1; page <= 5 && !hit; page++) {
2722
+ const check = await getAgentList(cfg, { view: 'all', agent: body.agentuid, beAgent: body.bagentuid, status: '1', page, pageSize: '100' });
2723
+ hit = (check.items || []).find(want) ?? null;
2724
+ if ((check.items || []).length < 100)
2725
+ break;
2726
+ }
2727
+ console.log(hit
2728
+ ? ` ✅ 已核验:列表中出现该条代理(状态 ${hit.status || '?'})`
2729
+ : ' ⚠️ 回读未确认:列表中暂未查到该条,请到 OA「流程代理」页核对。');
2730
+ }
2731
+ catch (e) {
2732
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2733
+ }
2734
+ return;
2735
+ }
2736
+
2737
+ if (sub === 'edit') {
2738
+ // --agent-ids(≥2 条)走**批量**编辑(只改起止日期/时间);--agent-id 走单条编辑
2739
+ const batchSpec = flagStr(args, 'agent-ids');
2740
+ if (batchSpec) {
2741
+ const time = parseTimeSpec(flagStr(args, 'time'));
2742
+ const opts = {
2743
+ agentIds: batchSpec,
2744
+ beginDate: flagStr(args, 'begin'),
2745
+ endDate: flagStr(args, 'end'),
2746
+ ...(time.beginTime || time.endTime ? { beginTime: time.beginTime, endTime: time.endTime } : {}),
2747
+ };
2748
+ const r = await editAgentBatch(cfg, { ...opts, send: !!args.flags.yes });
2749
+ if (!r.sent) {
2750
+ console.log(BOX_TOP);
2751
+ console.log(` [dry-run] 批量编辑代理(${r.body.agentids.split(',').length} 条)`);
2752
+ console.log(DIVIDER);
2753
+ console.log(` 代理id = ${r.body.agentids}`);
2754
+ console.log(` 起止日期 = ${r.body.beginDate || '(空)'} ~ ${r.body.endDate || '(空)'}`);
2755
+ console.log(` 起止时间 = ${r.body.beginTime || '(空)'} ~ ${r.body.endTime || '(空)'}`);
2756
+ console.log(' ⚠️ 批量编辑是**整体覆盖**这 4 个字段——只改一部分请用单条编辑(--agent-id)。');
2757
+ console.log(` 重叠校验 = ${r.overlap.overlapCount ? `⚠️ 与 ${r.overlap.overlapCount} 条已有代理重叠(overlapIds=${r.overlap.overlapIds})` : '✅ 无重叠'}`);
2758
+ console.log(DIVIDER);
2759
+ console.log(` 完整请求体:${JSON.stringify(r.body)}`);
2760
+ console.log(BOX_TOP);
2761
+ console.log(' 确认无误后加 --yes 真正保存。');
2762
+ return;
2763
+ }
2764
+ console.log(`${/success/i.test(r.result) ? '✅' : '⚠'} 批量编辑已保存(agentresult=${r.result || '无'})`);
2765
+ try {
2766
+ const ids = r.body.agentids.split(',');
2767
+ const rows = await getAgentList(cfg, { view: 'mine', status: '1', page: 1, pageSize: '100' });
2768
+ for (const id of ids) {
2769
+ const hit = (rows.items || []).find((it) => String(it.agentId) === String(id));
2770
+ console.log(hit
2771
+ ? ` 代理 ${id}:起止 = ${hit.beginDate} ~ ${hit.endDate} 状态 = ${hit.status}`
2772
+ : ` 代理 ${id}:⚠️ 生效中的列表里没找到(可能被改到「已结束」)`);
2773
+ }
2774
+ const allHit = ids.every((id) => (rows.items || []).some((it) => String(it.agentId) === String(id)));
2775
+ console.log(allHit ? ' ✅ 已核验:所选代理均已出现并生效。' : ' ⚠️ 回读未确认,请核查。');
2776
+ }
2777
+ catch (e) {
2778
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2779
+ }
2780
+ return;
2781
+ }
2782
+ const agentId = flagStr(args, 'agent-id');
2783
+ if (!agentId) {
2784
+ console.error('wfagent edit 需要 --agent-id <单条> 或 --agent-ids "537,538" <批量>');
2785
+ process.exit(1);
2786
+ }
2787
+ // 编辑 = 取详情 → 改字段 → 原样回吐,所以先拿详情(它同时也是 dry-run 的"改前")
2788
+ const detail = await getAgentDetail(cfg, agentId);
2789
+ const time = parseTimeSpec(flagStr(args, 'time'));
2790
+ const overrides = {
2791
+ beginDate: flagStr(args, 'begin'),
2792
+ endDate: flagStr(args, 'end'),
2793
+ ...(time.beginTime || time.endTime ? { beginTime: time.beginTime, endTime: time.endTime } : {}),
2794
+ isCreateAgenter: flagStr(args, 'agent-create-flow'),
2795
+ isProxyDeal: flagStr(args, 'proxy-deal'),
2796
+ isSysCreateAgenter: flagStr(args, 'sys-create'),
2797
+ isPendThing: flagStr(args, 'pend-thing'),
2798
+ };
2799
+ const fmtSpan = (d, t) => `${d}${t ? ` ${t}` : ''}`;
2800
+ const beforeSpan = `${fmtSpan(detail.current.beginDate, detail.current.beginTime)} ~ ${fmtSpan(detail.current.endDate, detail.current.endTime)}`;
2801
+ if (!Object.values(overrides).some((v) => v !== undefined)) {
2802
+ console.log(`代理 ${agentId}(${detail.current.workflowName})当前值:`);
2803
+ console.log(` 代理期间 = ${beforeSpan}`);
2804
+ console.log(' 没给任何要改的字段。可改:--begin / --end / --time / --agent-create-flow / --proxy-deal / --sys-create / --pend-thing');
2805
+ console.log(' ⚠️ 代理人、流程范围不可改(要换请先 takeback 再 create)。');
2806
+ return;
2807
+ }
2808
+ const r = await editAgent(cfg, { agentId, detail, ...overrides, send: !!args.flags.yes });
2809
+ if (!r.sent) {
2810
+ console.log(BOX_TOP);
2811
+ console.log(` [dry-run] 编辑代理 ${agentId}(${detail.current.workflowName})`);
2812
+ console.log(DIVIDER);
2813
+ console.log(` 代理期间 = ${beforeSpan} ← 改前`);
2814
+ console.log(` = ${fmtSpan(r.body.beginDate, r.body.beginTime)} ~ ${fmtSpan(r.body.endDate, r.body.endTime)} ← 改后`);
2815
+ 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}`);
2816
+ console.log(` 重叠校验 = ${r.overlap.overlapCount ? `⚠️ 与 ${r.overlap.overlapCount} 条已有代理重叠(overlapIds=${r.overlap.overlapIds})` : '✅ 无重叠'}`);
2817
+ console.log(DIVIDER);
2818
+ console.log(` 完整请求体:${JSON.stringify(r.body)}`);
2819
+ console.log(BOX_TOP);
2820
+ console.log(' 确认无误后加 --yes 真正保存。');
2821
+ return;
2822
+ }
2823
+ console.log(`${/success/i.test(r.result) ? '✅' : '⚠'} 编辑已保存(agentresult=${r.result || '无'})`);
2824
+ try {
2825
+ const rows = await getAgentList(cfg, { view: 'mine', status: '1', page: 1, pageSize: '100' });
2826
+ const hit = (rows.items || []).find((it) => String(it.agentId) === String(agentId));
2827
+ if (!hit) {
2828
+ console.log(' ⚠️ 回读未确认:生效中的列表里没找到该条(可能已被改到「已结束」)。');
2829
+ }
2830
+ else {
2831
+ const beginOk = !overrides.beginDate || String(hit.beginDate).startsWith(String(overrides.beginDate));
2832
+ const endOk = !overrides.endDate || String(hit.endDate) === String(overrides.endDate);
2833
+ console.log(` 回读:起止 = ${hit.beginDate} ~ ${hit.endDate} 状态 = ${hit.status}`);
2834
+ console.log(beginOk && endOk ? ' ✅ 已核验:改后的日期与提交一致。' : ' ⚠️ 回读与提交不一致,请核查。');
2835
+ }
2836
+ }
2837
+ catch (e) {
2838
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2839
+ }
2840
+ return;
2841
+ }
2842
+
2843
+ if (sub === 'takeback') {
2844
+ // --all:收回该被代理人名下的全部代理(界面「收回全部代理」,symbol=pt)
2845
+ if (args.flags.all === true) {
2846
+ const beAgentSpec = flagStr(args, 'be-agent');
2847
+ let beAgentUid = '';
2848
+ if (beAgentSpec)
2849
+ beAgentUid = await resolveNameToUid(cfg, beAgentSpec);
2850
+ else
2851
+ beAgentUid = (await getAddAgentInfo(cfg)).defaultBeAgentUid;
2852
+ if (!beAgentUid) {
2853
+ console.error('收回全部需要被代理人(--be-agent <姓名|id>),且未能从 getAddAgentInfo 取到当前登录人。');
2854
+ process.exit(1);
2855
+ }
2856
+ const backRunning = flagStr(args, 'back-running');
2857
+ const r = await takeBackAllAgent(cfg, { beAgentUid, backRunning, send: !!args.flags.yes });
2858
+ if (!r.sent) {
2859
+ console.log(BOX_TOP);
2860
+ console.log(' [dry-run] 将收回该被代理人名下的**全部**代理');
2861
+ console.log(DIVIDER);
2862
+ console.log(` 被代理人 = ${beAgentSpec || beAgentUid}(${r.beAgentUid})`);
2863
+ console.log(` backRunning= ${r.backRunning}(1=连流转中的一并收回)`);
2864
+ console.log(` 请求体 = symbol=pt & agentid=<自动取该被代理人名下第一条生效中的代理> & bagentuid=${r.beAgentUid} & backRunning=${r.backRunning}`);
2865
+ console.log(' ⚠️ 这是**批量**动作,会把这个被代理人名下所有代理一并标记「已结束」。');
2866
+ console.log(' 确认无误后加 --yes 真正收回。');
2867
+ console.log(BOX_TOP);
2868
+ return;
2869
+ }
2870
+ if (r.noop) {
2871
+ console.log(`该被代理人(${r.beAgentUid})名下没有生效中的代理,无需收回。`);
2872
+ return;
2873
+ }
2874
+ console.log(`${r.flag ? '✅' : '⚠'} 已发起收回全部代理(锚点 agentid=${r.agentId},flag=${r.flag})`);
2875
+ try {
2876
+ const left = await getAgentList(cfg, { view: 'all', beAgent: r.beAgentUid, status: '1', page: 1 });
2877
+ console.log(Number(left.total) === 0
2878
+ ? ` ✅ 回读核验:该被代理人名下生效中的代理已为 0 条。`
2879
+ : ` ⚠️ 回读核验:仍有 ${left.total} 条生效中的代理,请核查。`);
2880
+ }
2881
+ catch (e) {
2882
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2883
+ }
2884
+ return;
2885
+ }
2886
+ const idSpec = flagStr(args, 'agent-ids');
2887
+ if (!idSpec) {
2888
+ console.error('wfagent takeback 需要 --agent-ids "69,70"(列表里的「代理id」,逗号分隔多个),或 --all 收回全部');
2889
+ process.exit(1);
2890
+ }
2891
+ const ids = idSpec.split(',').map((s) => s.trim()).filter(Boolean);
2892
+ const backRunning = flagStr(args, 'back-running');
2893
+ const r = await takeBackAgent(cfg, {
2894
+ agentIds: ids.join(','),
2895
+ backRunning,
2896
+ send: !!args.flags.yes,
2897
+ // 逐条发;大批量时每 25 条报一次进度(走 stderr,不污染 --json)
2898
+ onProgress: (i, total, id) => {
2899
+ if (total > 25 && (i === 1 || i % 25 === 0))
2900
+ console.error(` 正在收回 ${i}/${total}(agentid=${id})…`);
2901
+ },
2902
+ });
2903
+ if (!r.sent) {
2904
+ console.log(BOX_TOP);
2905
+ console.log(` [dry-run] 将收回 ${ids.length} 条代理`);
2906
+ console.log(DIVIDER);
2907
+ console.log(` 代理id = ${ids.slice(0, 20).join(', ')}${ids.length > 20 ? ` …(共 ${ids.length} 个)` : ''}`);
2908
+ console.log(` backRunning= ${r.backRunning}(1=连流转中的一并收回)`);
2909
+ console.log(` 请求体 = symbol=it & agentid=<每个id> & backRunning=${r.backRunning}`);
2910
+ console.log(' 确认无误后加 --yes 真正收回。');
2911
+ console.log(BOX_TOP);
2912
+ return;
2913
+ }
2914
+ const failed = r.results.filter((x) => !x.flag);
2915
+ 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 ? ' …' : ''})` : ''}`);
2916
+ // ⚠️ 收回是「把 agenttype 置 0(已结束)」,行**不会消失**;且少传 symbol 时接口回 true 却毫无效果
2917
+ // ——所以必须按**状态码**回读核验,不能看"行在不在"。
2918
+ try {
2919
+ const after = new Map();
2920
+ let page = 1;
2921
+ while (true) {
2922
+ const p = await getAgentList(cfg, { view: 'mine', page, pageSize: '100' });
2923
+ for (const it of p.items)
2924
+ after.set(String(it.agentId), it.statusCode);
2925
+ if (p.items.length < 100 || page > 20)
2926
+ break;
2927
+ page++;
2928
+ }
2929
+ const notDone = ids.filter((id) => after.get(String(id)) !== '0');
2930
+ console.log(notDone.length
2931
+ ? ` ⚠️ 回读核验:${ids.length - notDone.length}/${ids.length} 条已为「已结束」,仍有 ${notDone.length} 条没变(${notDone.slice(0, 8).join(',')}${notDone.length > 8 ? ' …' : ''})——请核查。`
2932
+ : ` ✅ 回读核验:${ids.length} 条均已标记「已结束」。`);
2933
+ }
2934
+ catch (e) {
2935
+ console.log(` ⚠️ 回读核验失败:${e.message}`);
2936
+ }
2937
+ return;
2938
+ }
2939
+
2940
+ if (sub !== 'list') {
2941
+ console.error(`wfagent 暂只支持 list / conditions / templates / create / edit / takeback,收到: ${sub}`);
2942
+ process.exit(1);
2943
+ }
2944
+ const statusSpec = flagStr(args, 'status');
2945
+ const status = resolveWfagentStatus(statusSpec);
2946
+ if (status === undefined) {
2947
+ console.error(`未知代理状态「${statusSpec}」,可选:${Object.keys(WFAGENT_STATUS).join(' / ')}`);
2948
+ process.exit(1);
2949
+ }
2950
+ // 列表的按人筛选是「查他人数据」→ 走范围校验(姓名经 blog at-list 解析,硬规则)
2951
+ const listAgentSpec = flagStr(args, 'agent');
2952
+ const listBeAgentSpec = flagStr(args, 'be-agent');
2953
+ const agentFilter = listAgentSpec ? await assertNamesInScope(cfg, [listAgentSpec], { domain: 'wfagent' }) : undefined;
2954
+ const beAgentFilter = listBeAgentSpec ? await assertNamesInScope(cfg, [listBeAgentSpec], { domain: 'wfagent' }) : undefined;
2955
+ const r = await getAgentList(cfg, {
2956
+ view,
2957
+ status,
2958
+ agent: agentFilter,
2959
+ beAgent: beAgentFilter,
2960
+ wftype: flagStr(args, 'wftype'),
2961
+ workflowPath: flagStr(args, 'workflow-path'),
2962
+ // 日期沿用仓库惯例:单值=当天,双值="起 止"(同 todo --create-time)
2963
+ beginFrom: parseTimeRange(flagStr(args, 'begin') ?? '').from,
2964
+ beginTo: parseTimeRange(flagStr(args, 'begin') ?? '').to,
2965
+ endFrom: parseTimeRange(flagStr(args, 'end') ?? '').from,
2966
+ endTo: parseTimeRange(flagStr(args, 'end') ?? '').to,
2967
+ dept: flagStr(args, 'dept'),
2968
+ subcom: flagStr(args, 'subcom'),
2969
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2970
+ pageSize: flagStr(args, 'page-size'),
2971
+ });
2972
+ // 代理办理记录是「流程列表」→ 复用节点名解析,把 currentnodeid(id)显示成节点名
2973
+ if (view === 'records')
2974
+ await annotateNodeNamesForCli(cfg, r.items, args);
2975
+ if (args.flags.raw) {
2976
+ console.error('===== RAW getList =====');
2977
+ console.error(JSON.stringify(r.raw.getList, null, 2));
2978
+ console.error('===== RAW datas =====');
2979
+ console.error(JSON.stringify(r.raw.datas, null, 2));
2980
+ console.error('===== RAW counts =====');
2981
+ console.error(JSON.stringify(r.raw.counts, null, 2));
2982
+ console.error('===== 解析结果 =====');
2983
+ }
2984
+ if (args.flags.json) {
2985
+ console.log(JSON.stringify({
2986
+ view,
2987
+ viewTitle: r.viewTitle,
2988
+ page: r.page,
2989
+ pageSize: r.pageSize,
2990
+ total: r.total,
2991
+ count: r.items.length,
2992
+ items: r.items.map((it) => withLink('wfagent', it, base)),
2993
+ }, null, 2));
2994
+ return;
2995
+ }
2996
+ console.log(`E9 流程代理 · ${r.viewTitle} 第 ${r.page} 页(每页 ${r.pageSize ?? 10} 条) 共 ${r.total ?? '?'} 条`);
2997
+ if (!r.items.length) {
2998
+ console.log('(无数据)');
2999
+ if (args.flags.raw)
3000
+ console.error('提示:用 --raw 查看真实响应校准解析。');
3001
+ return;
3002
+ }
3003
+ for (let i = 0; i < r.items.length; i++) {
3004
+ const it = r.items[i];
3005
+ if (view === 'records') {
3006
+ // 代理产生的流程:标题必须渲染成流程详情超链接(列表超链接硬规则)
3007
+ console.log(`${i + 1}. ${hyperlinkFor('wfagent', it, base, it.title || '(无标题)')}`);
3008
+ const meta = [
3009
+ it.principal ? `被代理人=${it.principal}` : null,
3010
+ it.handler ? `代理人=${it.handler}` : null,
3011
+ nodeLabel(it),
3012
+ it.creator ? `流程发起人=${it.creator}` : null,
3013
+ it.createTime,
3014
+ ].filter(Boolean).join(' | ');
3015
+ if (meta)
3016
+ console.log(` ${meta}`);
3017
+ }
3018
+ else {
3019
+ console.log(`${i + 1}. ${it.agent || '(未知代理人)'} ← 被代理人 ${it.beAgent || it.operator || '?'}`);
3020
+ const meta = [
3021
+ it.workflowName,
3022
+ it.workflowType,
3023
+ `${it.beginDate ?? '?'} ~ ${it.endDate ?? '?'}`,
3024
+ it.status,
3025
+ it.operator ? `创建人=${it.operator}` : null,
3026
+ it.agentId ? `代理id=${it.agentId}` : null,
3027
+ ].filter(Boolean).join(' | ');
3028
+ if (meta)
3029
+ console.log(` ${meta}`);
3030
+ }
3031
+ }
3032
+ const pageSize = r.pageSize ?? 10;
3033
+ if (r.total != null && r.page * pageSize < r.total)
3034
+ console.log(`还有更多,用 --page <页码> 继续(每页 ${pageSize} 条)。`);
3035
+ }
3036
+
2302
3037
  async function runOdoc(args) {
2303
3038
  const sub = args._[1] ?? 'todo';
2304
3039
  const cfg = loadConfig(flagStr(args, 'config'));
@@ -2397,6 +3132,7 @@ async function runOdoc(args) {
2397
3132
  customTabId: flagStr(args, 'custom-tab-id'),
2398
3133
  page: Number(flagStr(args, 'page') ?? '1') || 1,
2399
3134
  });
3135
+ await annotateNodeNamesForCli(cfg, r.items, args);
2400
3136
  if (args.flags.raw) {
2401
3137
  console.error('===== RAW getCustomPageList =====');
2402
3138
  console.error(JSON.stringify(r.raw.getCustomPageList, null, 2));
@@ -2421,8 +3157,9 @@ async function runOdoc(args) {
2421
3157
  odocTypeName: it.odocTypeName,
2422
3158
  issuedNum: it.issuedNum,
2423
3159
  receiveNum: it.receiveNum,
2424
- currentNodeId: it.currentNodeId,
2425
- currentNode: it.currentNode,
3160
+ stageName: it.stageName,
3161
+ node: it.node,
3162
+ nodeName: it.nodeName,
2426
3163
  createrId: it.createrId,
2427
3164
  createDate: it.createDate,
2428
3165
  lastOperateDate: it.lastOperateDate,
@@ -2444,10 +3181,12 @@ async function runOdoc(args) {
2444
3181
  const it = r.items[i];
2445
3182
  const nameLinked = hyperlinkFor('odoc', it, odocBase, it.requestName ?? '(无标题)');
2446
3183
  console.log(`${i + 1}. ${nameLinked}`);
3184
+ // 环节名(如「拟稿」)+ 当前节点(解析出名字时用「名字(id)」,否则退回 id)
3185
+ const nodeText = [it.stageName, nodeLabel(it)].filter(Boolean).join(' · ');
2447
3186
  const meta = [
2448
3187
  it.odocTypeName,
2449
3188
  it.issuedNum ? `发文字号=${it.issuedNum}` : null,
2450
- it.currentNodeId,
3189
+ nodeText || null,
2451
3190
  it.createrId ? `拟稿人=${it.createrId}` : null,
2452
3191
  it.createDate,
2453
3192
  it.lastOperateDate ? `最后修改=${it.lastOperateDate}` : null,
@@ -2568,7 +3307,88 @@ async function runKq(args) {
2568
3307
  console.log(BOX_TOP);
2569
3308
  return;
2570
3309
  }
2571
- console.error(`kq 暂只支持 status / punch,收到: ${sub}`);
3310
+ if (sub === 'report' || sub === 'detail') {
3311
+ const month = flagStr(args, 'month');
3312
+ const year = flagStr(args, 'year');
3313
+ if (month && year) {
3314
+ console.error('--month 与 --year 只能传一个。');
3315
+ process.exit(1);
3316
+ }
3317
+ // 目标人员:--user <id> / --user-name <姓名>;都不传 = 当前登录人(cookie loginuuids)
3318
+ let targetId = flagStr(args, 'user') ?? '';
3319
+ const userName = flagStr(args, 'user-name');
3320
+ if (!targetId && userName)
3321
+ targetId = await assertNamesInScope(cfg, [userName], { domain: 'kq' });
3322
+ if (!targetId)
3323
+ targetId = parseUserIdFromCookie(cfg.cookie);
3324
+ if (!targetId) {
3325
+ console.error('未能确定目标人员:请传 --user <id> 或 --user-name <姓名>,或确认 cookie 含 loginuuids。');
3326
+ process.exit(1);
3327
+ }
3328
+ // 越权边界(默认停用;E9_SCOPE_ENFORCE=1 时启用):目标须 ∈ {本人 + 全部层级下属}
3329
+ await assertUserIdsInScope(cfg, [targetId], { domain: 'kq' });
3330
+ const type = year && !month ? '1' : '2';
3331
+ const typevalue = month || year || undefined;
3332
+ if (sub === 'detail') {
3333
+ const r = await getKqMonthReport(cfg, { typevalue, resourceId: targetId });
3334
+ if (args.flags.raw) {
3335
+ console.error('===== RAW getHrmKQMonthReportInfo =====');
3336
+ console.error(JSON.stringify(r.raw, null, 2));
3337
+ }
3338
+ if (args.flags.json) {
3339
+ console.log(JSON.stringify({ typevalue: r.typevalue, resourceId: r.resourceId, days: r.days }, null, 2));
3340
+ return;
3341
+ }
3342
+ console.log(`E9 考勤明细 ${r.typevalue} 人员 id=${targetId} 共 ${r.days.length} 天`);
3343
+ const withSign = r.days.filter((d) => d.signIn || d.signOut || d.types.length);
3344
+ if (!withSign.length) {
3345
+ console.log('(本月无打卡记录)');
3346
+ return;
3347
+ }
3348
+ for (const d of withSign) {
3349
+ const flags = d.types.length ? ` [${d.types.join(',')}]` : '';
3350
+ console.log(` ${d.date} 签到 ${d.signIn || '—'} → 签退 ${d.signOut || '—'}${flags}`);
3351
+ }
3352
+ return;
3353
+ }
3354
+ const r = await getKqReport(cfg, { type, typevalue, resourceId: targetId });
3355
+ if (args.flags.raw) {
3356
+ console.error('===== RAW getHrmKQReportInfo =====');
3357
+ console.error(JSON.stringify(r.raw, null, 2));
3358
+ }
3359
+ if (args.flags.json) {
3360
+ console.log(JSON.stringify({
3361
+ typevalue: r.typevalue, view: type === '1' ? 'year' : 'month', resourceId: r.resourceId,
3362
+ attendance: r.attendance, durations: r.durations, exceptions: r.exceptions, leave: r.leave,
3363
+ }, null, 2));
3364
+ return;
3365
+ }
3366
+ console.log(BOX_TOP);
3367
+ console.log(`E9 考勤统计 ${r.typevalue}(${type === '1' ? '年' : '月'}视图) 人员 id=${targetId}`);
3368
+ console.log(DIVIDER);
3369
+ console.log(` 出勤统计:应出勤 ${r.attendance.workDays}`);
3370
+ for (const it of r.attendance.items)
3371
+ console.log(` ${it.title} = ${it.value}`);
3372
+ const line = (label, arr) => {
3373
+ if (!arr.length)
3374
+ return;
3375
+ console.log(` ${label}:${arr.map((x) => `${x.title} ${x.value}`).join(' | ')}`);
3376
+ };
3377
+ line('流程时长', r.durations);
3378
+ line('异常', r.exceptions);
3379
+ if (r.leave.length) {
3380
+ console.log(DIVIDER);
3381
+ console.log(' 假期余额(可请天数):');
3382
+ for (const g of r.leave) {
3383
+ const items = g.items.map((x) => `${x.title} ${x.value}`).join(' | ');
3384
+ if (items)
3385
+ console.log(` ${g.title}:${items}`);
3386
+ }
3387
+ }
3388
+ console.log(BOX_TOP);
3389
+ return;
3390
+ }
3391
+ console.error(`kq 暂只支持 status / punch / report / detail,收到: ${sub}`);
2572
3392
  process.exit(1);
2573
3393
  }
2574
3394
  /** CLI 侧当前时刻 HH:mm:ss(与 kq.ts 的 nowHHmmss 同逻辑,CLI 输出用)。 */
@@ -3149,6 +3969,439 @@ function fmtMenus(e, kw) {
3149
3969
  const t = s.length > 32 ? s.slice(0, 32) + '…' : s;
3150
3970
  return ` 菜单=${t}`;
3151
3971
  }
3972
+ // ─── memory ──────────────────────────────────────
3973
+ async function runMemory(args) {
3974
+ const sub = args._[1] ?? 'list';
3975
+ const cfg = loadConfig(flagStr(args, 'config'));
3976
+ const { mem, selfId, selfName } = await loadMemory(cfg);
3977
+
3978
+ if (!selfId) {
3979
+ console.error('无法确认当前用户身份,记忆功能不可用。请先登录(e9 auth token-login)。');
3980
+ process.exit(1);
3981
+ }
3982
+
3983
+ if (sub === 'list') {
3984
+ // e9 memory list
3985
+ const items = listShortcuts(cfg, selfId);
3986
+ const cacheCount = Object.keys(mem.formCache || {}).length;
3987
+ const prefs = mem.user?.preferences || {};
3988
+
3989
+ console.log(`当前用户 ${selfName}(${selfId}) 记忆状态:`);
3990
+ console.log(` 可用: ${mem.available ? '✅ 启用' : '❌ 禁用(e9 memory enable 恢复)'}`);
3991
+ console.log(` 快捷方式: ${items.length} 个`);
3992
+ console.log(` 表单缓存: ${cacheCount} 个流程${mem.lastCacheCheck ? ` (上次巡检: ${timeAgo(new Date(mem.lastCacheCheck))})` : ''}`);
3993
+ if (Object.keys(prefs).length > 0) {
3994
+ console.log(` 用户偏好: ${Object.entries(prefs).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ')}`);
3995
+ }
3996
+ console.log('');
3997
+
3998
+ if (items.length === 0) {
3999
+ console.log(`还没有记住任何流程快捷方式。\n使用 e9 memory save <关键词> --workflow-id <id> 来记住一个。`);
4000
+ return;
4001
+ }
4002
+ console.log(`快捷方式列表(按最近使用排序):`);
4003
+ const now = Date.now();
4004
+ const expiryMs = (SHORTCUT_EXPIRY_DAYS || 90) * 24 * 60 * 60 * 1000;
4005
+ const warnMs = expiryMs * 0.75; // 75% 时提醒"即将过期"
4006
+ let staleCount = 0;
4007
+ items.forEach((s, i) => {
4008
+ const ago = s.lastUsed ? timeAgo(new Date(s.lastUsed)) : '从未使用';
4009
+ const lastTs = new Date(s.lastUsed || 0).getTime();
4010
+ const ageMs = now - lastTs;
4011
+ let badge = '';
4012
+ if (ageMs > expiryMs) {
4013
+ badge = ' ⚠️ 过期';
4014
+ staleCount++;
4015
+ } else if (ageMs > warnMs) {
4016
+ badge = ' ⏳ 即将过期';
4017
+ }
4018
+ // 表单缓存年龄
4019
+ const fc = mem.formCache?.[String(s.workflowId)];
4020
+ let cacheInfo = '';
4021
+ if (fc) {
4022
+ const validatedAgo = fc.lastValidatedAt ? timeAgo(new Date(fc.lastValidatedAt)) : (fc.cachedAt ? timeAgo(new Date(fc.cachedAt)) : '?');
4023
+ cacheInfo = ` 缓存验证: ${validatedAgo}`;
4024
+ }
4025
+ console.log(` ${i + 1}. ${s.keyword} → ${s.workflowName || '(无名称)'} (id=${s.workflowId}) 使用 ${s.hitCount} 次 ${ago}${badge}${cacheInfo}`);
4026
+ });
4027
+ if (staleCount > 0) {
4028
+ console.log(`\n⚠️ ${staleCount} 个快捷方式已超过 ${SHORTCUT_EXPIRY_DAYS || 90} 天未使用,可用 e9 memory cleanup [--dry-run] 清理。`);
4029
+ }
4030
+ return;
4031
+ }
4032
+
4033
+ if (sub === 'save') {
4034
+ // e9 memory save <关键词> --workflow-id <id> [--workflow-name <名>]
4035
+ const keyword = args._[2];
4036
+ const workflowId = flagStr(args, 'workflow-id') || flagStr(args, 'id');
4037
+ const workflowName = flagStr(args, 'workflow-name') || flagStr(args, 'name') || '';
4038
+ if (!keyword || !workflowId) {
4039
+ console.error('用法: e9 memory save <关键词> --workflow-id <id> [--workflow-name <名称>]');
4040
+ process.exit(1);
4041
+ }
4042
+ const s = saveShortcut(cfg, selfId, selfName, keyword, workflowId, workflowName);
4043
+ console.log(`已记住:${keyword} → ${s.workflowName || '(无名称)'} (id=${s.workflowId})`);
4044
+ return;
4045
+ }
4046
+
4047
+ if (sub === 'forget') {
4048
+ // e9 memory forget <关键词>(精确优先,未命中走模糊)
4049
+ const keyword = args._[2];
4050
+ if (!keyword) {
4051
+ console.error('用法: e9 memory forget <关键词>');
4052
+ process.exit(1);
4053
+ }
4054
+ let actualKey = keyword;
4055
+ if (!findShortcut(cfg, selfId, keyword)) {
4056
+ const fuzzy = findShortcutFuzzy(cfg, selfId, keyword);
4057
+ if (fuzzy) {
4058
+ actualKey = fuzzy.keyword;
4059
+ console.error(`模糊匹配: 「${keyword}」→「${actualKey}」,将删除「${actualKey}」`);
4060
+ }
4061
+ }
4062
+ const ok = forgetShortcut(cfg, selfId, actualKey);
4063
+ console.log(ok ? `已忘记:${actualKey}` : `未找到快捷方式:${keyword}`);
4064
+ return;
4065
+ }
4066
+
4067
+ if (sub === 'show') {
4068
+ // e9 memory show <关键词>(精确优先,未命中走模糊)
4069
+ const keyword = args._[2];
4070
+ if (!keyword) {
4071
+ console.error('用法: e9 memory show <关键词>');
4072
+ process.exit(1);
4073
+ }
4074
+ let s = findShortcut(cfg, selfId, keyword);
4075
+ let actualKey = keyword;
4076
+ if (!s) {
4077
+ const fuzzy = findShortcutFuzzy(cfg, selfId, keyword);
4078
+ if (fuzzy) {
4079
+ s = fuzzy.shortcut;
4080
+ actualKey = fuzzy.keyword;
4081
+ console.error(`模糊匹配: 「${keyword}」→「${actualKey}」`);
4082
+ }
4083
+ }
4084
+ if (!s) {
4085
+ console.log(`未找到快捷方式:${keyword}`);
4086
+ return;
4087
+ }
4088
+ console.log(`关键词: ${actualKey}`);
4089
+ console.log(`流程 ID: ${s.workflowId}`);
4090
+ console.log(`流程名称: ${s.workflowName || '(无名称)'}`);
4091
+ console.log(`使用次数: ${s.hitCount}`);
4092
+ console.log(`最近使用: ${s.lastUsed ? new Date(s.lastUsed).toLocaleString() : '从未'}`);
4093
+ console.log(`创建时间: ${s.createdAt ? new Date(s.createdAt).toLocaleString() : '未知'}`);
4094
+ const lastTs = new Date(s.lastUsed || s.createdAt || 0).getTime();
4095
+ const ageDays = Math.round((Date.now() - lastTs) / (24 * 60 * 60 * 1000));
4096
+ const expiryDays = SHORTCUT_EXPIRY_DAYS || 90;
4097
+ if (ageDays > expiryDays) {
4098
+ console.log(`⚠️ 过期警告: ${ageDays} 天未使用(阈值 ${expiryDays} 天),建议清理`);
4099
+ } else if (ageDays > expiryDays * 0.75) {
4100
+ console.log(`⏳ 即将过期: ${ageDays} 天未使用(阈值 ${expiryDays} 天),剩余 ${expiryDays - ageDays} 天`);
4101
+ }
4102
+ const defaults = getFieldDefaults(cfg, selfId, s.workflowId);
4103
+ if (Object.keys(defaults).length > 0) {
4104
+ console.log(`字段默认值 + 使用频次(从高到低):`);
4105
+ Object.entries(defaults).forEach(([fid, f]) => {
4106
+ const history = getFieldValueHistory(cfg, selfId, s.workflowId, fid);
4107
+ if (history.length > 1) {
4108
+ // 有多次使用记录:显示频次分布
4109
+ const top3 = history.slice(0, 3).map(h => `'${h.value}' ×${h.count}`);
4110
+ console.log(` ${f.label || fid} (fieldId=${fid}): 默认=${f.default} | ${top3.join(' ')}${history.length > 3 ? ' ...' : ''}`);
4111
+ } else {
4112
+ console.log(` ${f.label || fid} (fieldId=${fid}): ${f.default}`);
4113
+ }
4114
+ });
4115
+ }
4116
+ const autoSubmit = mem.autoSubmit || [];
4117
+ console.log(`自动提交: ${autoSubmit.includes(actualKey) ? '✅ 开启' : '❌ 关闭'}`);
4118
+ return;
4119
+ }
4120
+
4121
+ if (sub === 'clear') {
4122
+ // e9 memory clear
4123
+ const ok = clearMemory(cfg, selfId);
4124
+ console.log(ok ? `已清空当前环境+用户 ${selfName} 的全部记忆。` : '清空失败,请检查文件权限。');
4125
+ return;
4126
+ }
4127
+
4128
+ if (sub === 'clear-cache') {
4129
+ // e9 memory clear-cache <workflow-id>
4130
+ const workflowId = args._[2];
4131
+ if (!workflowId) {
4132
+ console.error('用法: e9 memory clear-cache <workflow-id>');
4133
+ process.exit(1);
4134
+ }
4135
+ const ok = clearFormCache(cfg, selfId, workflowId);
4136
+ console.log(ok ? `已清除流程 ${workflowId} 的表单缓存(下次发起会重新拉取字段结构)。` : `未找到流程 ${workflowId} 的表单缓存。`);
4137
+ return;
4138
+ }
4139
+
4140
+ if (sub === 'disable') {
4141
+ // e9 memory disable
4142
+ setMemoryAvailable(cfg, selfId, false);
4143
+ console.log(`已禁用记忆(数据保留,可通过 e9 memory enable 恢复)。`);
4144
+ return;
4145
+ }
4146
+
4147
+ if (sub === 'enable') {
4148
+ // e9 memory enable
4149
+ setMemoryAvailable(cfg, selfId, true);
4150
+ console.log(`已启用记忆。`);
4151
+ return;
4152
+ }
4153
+
4154
+ if (sub === 'user-pref') {
4155
+ // e9 memory user-pref [show|<key> [<value>]]
4156
+ const key = args._[2];
4157
+ const val = args._[3];
4158
+ if (!key || key === 'show') {
4159
+ // 展示当前用户偏好
4160
+ const prefs = getUserPreferences(cfg, selfId);
4161
+ if (Object.keys(prefs).length === 0) {
4162
+ console.log(`用户 ${selfName}(${selfId}) 还没有设置任何偏好。\n可设置项:简洁回复(concise)、隐藏流程ID(hideRequestId)、我的称呼(nickname)。`);
4163
+ return;
4164
+ }
4165
+ console.log(`用户 ${selfName}(${selfId}) 偏好:`);
4166
+ for (const [k, v] of Object.entries(prefs)) {
4167
+ console.log(` ${k}: ${JSON.stringify(v)}`);
4168
+ }
4169
+ return;
4170
+ }
4171
+ if (val === undefined) {
4172
+ // 查看单个偏好
4173
+ const prefs = getUserPreferences(cfg, selfId);
4174
+ console.log(`${key}: ${prefs[key] !== undefined ? JSON.stringify(prefs[key]) : '(未设置)'}`);
4175
+ return;
4176
+ }
4177
+ // 设置偏好
4178
+ let parsed = val;
4179
+ if (val === 'true') parsed = true;
4180
+ else if (val === 'false') parsed = false;
4181
+ else if (/^\d+$/.test(val)) parsed = Number(val);
4182
+ saveUserPreference(cfg, selfId, key, parsed);
4183
+ console.log(`已设置偏好: ${key} = ${JSON.stringify(parsed)}`);
4184
+ return;
4185
+ }
4186
+
4187
+ if (sub === 'auto-submit') {
4188
+ // e9 memory auto-submit <关键词> [on|off]
4189
+ const keyword = args._[2];
4190
+ if (!keyword) {
4191
+ console.error('用法: e9 memory auto-submit <关键词> [on|off]');
4192
+ process.exit(1);
4193
+ }
4194
+ const toggle = args._[3];
4195
+ if (!toggle) {
4196
+ const autoSubmit = mem.autoSubmit || [];
4197
+ console.log(`${keyword}: 自动提交 ${autoSubmit.includes(keyword) ? '✅ 开启' : '❌ 关闭'}`);
4198
+ return;
4199
+ }
4200
+ const enabled = toggle === 'on' || toggle === 'true' || toggle === '1';
4201
+ setAutoSubmit(cfg, selfId, keyword, enabled);
4202
+ console.log(`${keyword}: 自动提交已${enabled ? '开启' : '关闭'}`);
4203
+ return;
4204
+ }
4205
+
4206
+ if (sub === 'cleanup') {
4207
+ // e9 memory cleanup [--dry-run] [--max-age-days <n>]
4208
+ const dryRun = !!args.flags['dry-run'];
4209
+ const maxAgeDays = Number(flagStr(args, 'max-age-days') || SHORTCUT_EXPIRY_DAYS);
4210
+
4211
+ const expired = findExpiredShortcuts(cfg, selfId, maxAgeDays);
4212
+ if (expired.length === 0) {
4213
+ console.log(`没有过期快捷方式(阈值:${maxAgeDays} 天未使用)。`);
4214
+ return;
4215
+ }
4216
+
4217
+ console.log(`找到 ${expired.length} 个过期快捷方式(超过 ${maxAgeDays} 天未使用):`);
4218
+ for (const e of expired) {
4219
+ const ago = e.lastUsed ? timeAgo(new Date(e.lastUsed)) : '从未使用';
4220
+ console.log(` · ${e.keyword} → ${e.workflowName || '(无名称)'} (id=${e.workflowId}) 使用 ${e.hitCount} 次 ${ago}`);
4221
+ }
4222
+
4223
+ if (dryRun) {
4224
+ console.log(`\n(--dry-run 模式,未实际删除。去掉 --dry-run 真清理。)`);
4225
+ return;
4226
+ }
4227
+
4228
+ const removed = cleanupShortcuts(cfg, selfId, maxAgeDays);
4229
+ console.log(`\n已清理 ${removed.length} 个过期快捷方式。`);
4230
+ return;
4231
+ }
4232
+
4233
+ if (sub === 'export') {
4234
+ // e9 memory export [--output <file>]
4235
+ const outPath = flagStr(args, 'output') || `e9-memory-${selfId}-${new Date().toISOString().slice(0, 10)}.json`;
4236
+ const json = exportMemory(cfg, selfId);
4237
+ fs.writeFileSync(outPath, json, 'utf8');
4238
+ const stats = JSON.parse(json);
4239
+ const shortCount = Object.keys(stats.shortcuts || {}).length;
4240
+ const fieldCount = Object.values(stats.fieldDefaults || {}).reduce((n, f) => n + Object.keys(f).length, 0);
4241
+ console.log(`记忆已导出到: ${outPath}`);
4242
+ console.log(` 快捷方式: ${shortCount} 个`);
4243
+ console.log(` 字段默认: ${fieldCount} 个`);
4244
+ console.log(` 表单缓存: ${Object.keys(stats.formCache || {}).length} 个流程`);
4245
+ return;
4246
+ }
4247
+
4248
+ if (sub === 'import') {
4249
+ // e9 memory import <file>
4250
+ const filePath = args._[2];
4251
+ if (!filePath) {
4252
+ console.error('用法: e9 memory import <file>');
4253
+ process.exit(1);
4254
+ }
4255
+ if (!fs.existsSync(filePath)) {
4256
+ console.error(`文件不存在: ${filePath}`);
4257
+ process.exit(1);
4258
+ }
4259
+ const input = fs.readFileSync(filePath, 'utf8');
4260
+ const result = importMemory(cfg, selfId, selfName, input);
4261
+ console.log(`记忆导入完成:`);
4262
+ console.log(` 新增快捷方式: ${result.addedShortcuts.length} 个`);
4263
+ if (result.addedShortcuts.length > 0) {
4264
+ result.addedShortcuts.forEach(k => console.log(` + ${k}`));
4265
+ }
4266
+ console.log(` 新增字段默认: ${result.addedFields} 个`);
4267
+ if (result.skippedShortcuts.length > 0) {
4268
+ console.log(` 跳过已有: ${result.skippedShortcuts.length} 个(${result.skippedShortcuts.join(', ')})`);
4269
+ }
4270
+ return;
4271
+ }
4272
+
4273
+ if (sub === 'refresh') {
4274
+ // e9 memory refresh — 手动触发缓存巡检
4275
+ console.log(`正在巡检 ${Object.keys(mem.shortcuts || {}).length} 个快捷方式对应的表单缓存...`);
4276
+ const result = await refreshStaleFormCaches(cfg, selfId);
4277
+ if (result.checked === 0) {
4278
+ console.log('所有缓存均在 24 小时内已验证,无需刷新。');
4279
+ return;
4280
+ }
4281
+ console.log(`检查 ${result.checked} 个流程:`);
4282
+ for (const d of result.details) {
4283
+ if (d.status === 'ok') {
4284
+ console.log(` ✅ ${d.name || d.workflowId} — 字段结构无变化`);
4285
+ } else if (d.status === 'updated') {
4286
+ console.log(` 🔄 ${d.name || d.workflowId} — 字段数变化: ${d.oldCount} → ${d.newCount},已更新缓存`);
4287
+ } else if (d.status === 'no-right') {
4288
+ console.log(` ⚠️ ${d.name || d.workflowId} — 无权限,保留旧缓存`);
4289
+ } else {
4290
+ console.log(` ❌ ${d.name || d.workflowId} — 刷新失败: ${d.error || '未知错误'}`);
4291
+ }
4292
+ }
4293
+ if (result.errors > 0) console.log(`\n${result.errors} 个流程刷新失败(网络问题或表单不存在)。`);
4294
+ console.log(`\n完成:更新 ${result.updated} 个,检查通过 ${result.details.filter(d => d.status === 'ok').length} 个。`);
4295
+ return;
4296
+ }
4297
+
4298
+ console.error(`memory 暂只支持 list / save / forget / show / clear / clear-cache / cleanup / export / import / refresh / enable / disable / user-pref / auto-submit 子命令,收到: ${sub}`);
4299
+ }
4300
+
4301
+ // ─── 缓存统计 ─────────────────────────────────────
4302
+
4303
+ async function runCache(args) {
4304
+ const sub = args._[1] ?? 'stats';
4305
+ if (sub !== 'stats') {
4306
+ console.error(`cache 子命令暂只支持: stats,收到: ${sub}`);
4307
+ process.exit(1);
4308
+ }
4309
+ const cfg = loadConfig(flagStr(args, 'config'));
4310
+ const baseUrl = endpointFor(cfg)?.baseUrl || '';
4311
+ const envkey = profileNameFromBaseUrl(baseUrl);
4312
+
4313
+ // user_memory 需要 selfId
4314
+ let selfId = '';
4315
+ try {
4316
+ const { getCurrentUser } = await import('./wfuser.js');
4317
+ const u = await getCurrentUser(cfg);
4318
+ selfId = String(u?.userid ?? '');
4319
+ } catch { /* stats 尽力而为 */ }
4320
+
4321
+ const dimensions = [
4322
+ {
4323
+ name: 'node_map',
4324
+ paths: () => [nodeMapCachePath(cfg)],
4325
+ countFn: (raw) => Object.keys(raw?.workflows ?? {}).length,
4326
+ },
4327
+ {
4328
+ name: 'cube_catalog',
4329
+ paths: () => [cubeCachePath(cfg)],
4330
+ countFn: (raw) => raw?.count ?? (Array.isArray(raw?.entries) ? raw.entries.length : 0),
4331
+ },
4332
+ {
4333
+ name: 'subordinate_scope',
4334
+ paths: () => [scopeCachePath(cfg)],
4335
+ countFn: (raw) => (raw?.userIds ? raw.userIds.length : 0),
4336
+ },
4337
+ {
4338
+ name: 'user_memory',
4339
+ paths: () => {
4340
+ if (selfId) return [memoryCachePath(cfg, selfId)];
4341
+ // selfId 获取不到则 glob 目录下的匹配文件
4342
+ try {
4343
+ const dir = memoryCacheDir();
4344
+ const files = fs.readdirSync(dir).filter(f =>
4345
+ f.startsWith(`user_memory_${envkey}_`) && f.endsWith('.json')
4346
+ );
4347
+ return files.map(f => path.join(dir, f));
4348
+ } catch { return []; }
4349
+ },
4350
+ countFn: (raw) => {
4351
+ const sc = Object.keys(raw?.shortcuts ?? {}).length;
4352
+ const fc = Object.keys(raw?.formCache ?? {}).length;
4353
+ const fd = Object.keys(raw?.fieldDefaults ?? {}).length;
4354
+ return `快捷=${sc}, 表单缓存=${fc}, 字段默认=${fd}`;
4355
+ },
4356
+ },
4357
+ ];
4358
+
4359
+ console.log(`缓存统计 (envkey=${envkey || '未知'}):`);
4360
+ console.log('');
4361
+
4362
+ for (const dim of dimensions) {
4363
+ const paths = dim.paths();
4364
+ if (paths.length === 0) {
4365
+ console.log(`${dim.name}: (未找到缓存文件)`);
4366
+ continue;
4367
+ }
4368
+ for (const p of paths) {
4369
+ let stat = null;
4370
+ try { stat = fs.statSync(p); } catch { /* 文件可能不存在 */ }
4371
+ const raw = cacheGet(p); // 不加 TTL,stats 只读原始数据
4372
+ if (!raw) {
4373
+ if (stat) {
4374
+ console.log(`${dim.name}: ${formatSize(stat.size)} (无法解析)`);
4375
+ }
4376
+ continue;
4377
+ }
4378
+ const tsField = raw.fetchedAt || raw.updatedAt;
4379
+ const age = tsField ? timeAgo(new Date(tsField)) : '未知';
4380
+ const countStr = dim.countFn(raw);
4381
+ console.log(`${dim.name}: ${p}`);
4382
+ console.log(` 条目: ${countStr} | 更新时间: ${age} | 大小: ${formatSize(stat?.size ?? 0)}`);
4383
+ }
4384
+ }
4385
+ }
4386
+
4387
+ function formatSize(bytes) {
4388
+ if (bytes < 1024) return `${bytes} B`;
4389
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4390
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4391
+ }
4392
+
4393
+ function timeAgo(date) {
4394
+ const ms = Date.now() - date.getTime();
4395
+ const s = Math.floor(ms / 1000);
4396
+ if (s < 60) return `${s}秒前`;
4397
+ const m = Math.floor(s / 60);
4398
+ if (m < 60) return `${m}分钟前`;
4399
+ const h = Math.floor(m / 60);
4400
+ if (h < 24) return `${h}小时前`;
4401
+ const d = Math.floor(h / 24);
4402
+ return `${d}天前`;
4403
+ }
4404
+
3152
4405
  /**
3153
4406
  * #7 统一名称搜索:用户只说名字、不知归属(台账/菜单/流程)时,一次搜三类,避免猜错域来回。
3154
4407
  * 台账:cube find(台账目录 list_name + 菜单挂载名)
@@ -4017,12 +5270,41 @@ async function runHrm(args) {
4017
5270
  }
4018
5271
  }
4019
5272
 
5273
+ /**
5274
+ * 节点名解析:默认开启,`--no-node-name` 或环境变量 E9_NO_NODE_NAME=1 停用。
5275
+ * 接口失败/接口不存在时静默降级(列表照常出,只是节点列回退显示 id)。
5276
+ */
5277
+ function annotateNodeNamesForCli(cfg, items, args) {
5278
+ // --raw 是校准/排障出口,保持原样不额外请求;--no-node-name / E9_NO_NODE_NAME=1 显式关闭。
5279
+ const disabled = args.flags['no-node-name'] === true || args.flags.raw === true || isNodeNameDisabled();
5280
+ return annotateNodeNames(cfg, items, { disabled });
5281
+ }
5282
+ /** 解析 --time "HH:MM-HH:MM";不传返回空串(= 全天)。 */
5283
+ function parseTimeSpec(raw) {
5284
+ const s = String(raw ?? '').trim();
5285
+ if (!s)
5286
+ return { beginTime: '', endTime: '' };
5287
+ const m = s.split(/[-~—]/).map((x) => x.trim()).filter(Boolean);
5288
+ if (m.length !== 2) {
5289
+ console.error('--time 格式应为 "HH:MM-HH:MM"');
5290
+ process.exit(1);
5291
+ }
5292
+ return { beginTime: m[0], endTime: m[1] };
5293
+ }
5294
+
5295
+ /** 节点显示:解析到名字时用「名字(id)」,否则退回 id。 */
5296
+ function nodeLabel(item) {
5297
+ const id = item?.node ? String(item.node) : '';
5298
+ if (!id)
5299
+ return undefined;
5300
+ return item.nodeName ? `${item.nodeName}(${id})` : id;
5301
+ }
4020
5302
  function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
4021
5303
  const visible = items.slice(0, max);
4022
5304
  for (let i = 0; i < visible.length; i++) {
4023
5305
  const it = visible[i];
4024
5306
  const head = it.title ?? '(无标题)';
4025
- const meta = [it.creator, it.currentOperator ? `未操作者=${it.currentOperator}` : null, it.node, it.createdate].filter(Boolean).join(' | ');
5307
+ const meta = [it.creator, it.currentOperator ? `未操作者=${it.currentOperator}` : null, nodeLabel(it), it.createdate].filter(Boolean).join(' | ');
4026
5308
  const headLinked = hyperlinkFor('todo', it, base, head);
4027
5309
  console.log(`${indent}${i + 1}. ${headLinked}${it.level ? ` (${it.level})` : ''}`);
4028
5310
  if (meta)
@@ -4050,9 +5332,15 @@ async function runTodoBatch(args) {
4050
5332
  }
4051
5333
  else {
4052
5334
  // 批量关注:dry-run 也先查关注类型(只读),供用户确认加入哪个关注类型
5335
+ // 关注在 待办/已办/我发起 三个分类下都有「我的关注」页签,故定位列表可用 --category 指定(默认待办)
5336
+ const category = flagStr(args, 'category') ?? 'todo';
5337
+ if (!['todo', 'done', 'mine'].includes(category)) {
5338
+ console.error(`batch-attention 的 --category 只支持 todo(待办)/ done(已办)/ mine(我发起),收到: ${category}`);
5339
+ process.exit(1);
5340
+ }
4053
5341
  const attentionTypes = await getAttentionTypes(cfg, requestIds);
4054
5342
  const attentionId = flagStr(args, 'attention-id') ?? attentionTypes[0]?.id;
4055
- preview = { operation: '批量关注', requestIds, attentionId, attentionTypes: attentionTypes.map((t) => ({ id: t.id, showname: t.showname })) };
5343
+ preview = { operation: '批量关注', category, requestIds, attentionId, attentionTypes: attentionTypes.map((t) => ({ id: t.id, showname: t.showname })) };
4056
5344
  }
4057
5345
  if (!args.flags.yes) {
4058
5346
  console.log(BOX_TOP);
@@ -4068,7 +5356,7 @@ async function runTodoBatch(args) {
4068
5356
  else if (sub === 'batch-forward')
4069
5357
  result = await batchForward(cfg, { requestIds, receiver: preview.receiver, remark: preview.remark });
4070
5358
  else
4071
- result = await batchAttention(cfg, { requestIds, attentionId: preview.attentionId });
5359
+ result = await batchAttention(cfg, { requestIds, attentionId: preview.attentionId, category: preview.category });
4072
5360
  const body = result.result?.json ?? {};
4073
5361
  const ok = result.result?.ok && body.success !== false && body.status !== false;
4074
5362
  console.log(`${ok ? '✅' : '⚠'} ${preview.operation}已请求处理(${requestIds.length} 条)。`);
@@ -4116,7 +5404,7 @@ async function main() {
4116
5404
  const cmd = args._[0] ?? 'todo';
4117
5405
  if (cmd === 'version') {
4118
5406
  const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
4119
- console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.4', buildHash }, null, 2));
5407
+ console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.11', buildHash }, null, 2));
4120
5408
  return;
4121
5409
  }
4122
5410
  if (cmd === 'whoami') {
@@ -4155,6 +5443,14 @@ async function main() {
4155
5443
  await runWorkplan(args);
4156
5444
  return;
4157
5445
  }
5446
+ if (cmd === 'supervise') {
5447
+ await runSupervise(args);
5448
+ return;
5449
+ }
5450
+ if (cmd === 'wfagent') {
5451
+ await runWfagent(args);
5452
+ return;
5453
+ }
4158
5454
  if (cmd === 'odoc') {
4159
5455
  await runOdoc(args);
4160
5456
  return;
@@ -4167,6 +5463,14 @@ async function main() {
4167
5463
  await runSearch(args);
4168
5464
  return;
4169
5465
  }
5466
+ if (cmd === 'memory') {
5467
+ await runMemory(args);
5468
+ return;
5469
+ }
5470
+ if (cmd === 'cache') {
5471
+ await runCache(args);
5472
+ return;
5473
+ }
4170
5474
  if (cmd === 'cube') {
4171
5475
  await runCube(args);
4172
5476
  return;
@@ -4261,7 +5565,9 @@ async function main() {
4261
5565
  workflowFilter = String(saved.candidates[candidate - 1].id);
4262
5566
  }
4263
5567
  else if (workflowFilter && !/^\d+(\s*,\s*\d+)*$/.test(workflowFilter)) {
4264
- const candidates = await findCreateWfByName(cfg, workflowFilter);
5568
+ let selfId = '';
5569
+ try { const { mem } = await loadMemory(cfg); selfId = mem.selfId; } catch { }
5570
+ const candidates = await findCreateWfByName(cfg, workflowFilter, selfId);
4265
5571
  if (candidates.length === 0)
4266
5572
  throw new Error(`未找到名称包含“${workflowFilter}”的流程。`);
4267
5573
  if (candidates.length > 1) {
@@ -4289,6 +5595,43 @@ async function main() {
4289
5595
  return typeof saved === 'string' ? saved : undefined;
4290
5596
  };
4291
5597
  const queryBool = (key) => args.flags[key] === true || savedQueryFlags[key] === true;
5598
+ // ★按节点名筛选(--node-name):节点 id 是**流程内编号**,必须先定位流程才能解析成 id。
5599
+ // 与 --node-id 互斥;未给 --workflow 时只在**本地缓存**的节点表里找(缓存来自历次列表查询)。
5600
+ let nodeIdFilter = queryFlag('node-id');
5601
+ const nodeNameFilter = queryFlag('node-name');
5602
+ if (nodeNameFilter) {
5603
+ // --json 时信息一律走 stderr,保证 stdout 是纯 JSON
5604
+ const say = args.flags.json ? console.error : console.log;
5605
+ if (nodeIdFilter)
5606
+ throw new Error('--node-name 与 --node-id 不能同时用(两者都在指定当前节点)。');
5607
+ const wfScope = String(workflowFilter ?? '').split(',').map((s) => s.trim()).filter(Boolean);
5608
+ if (wfScope.length > 1)
5609
+ throw new Error(`--node-name 需要唯一的流程范围,但 --workflow 传了 ${wfScope.length} 个流程;请只保留一个。`);
5610
+ const resolved = await resolveNodeByName(cfg, nodeNameFilter, { workflowId: wfScope[0] });
5611
+ const pick = resolved.exact.length === 1 ? resolved.exact[0] : null;
5612
+ if (!pick) {
5613
+ if (resolved.unavailable) {
5614
+ console.error(`取不到流程 ${wfScope[0]} 的节点表(/api/workflow/test/getNodeInfo 不可用),无法按节点名筛选;请改用 --node-id <id>。`);
5615
+ process.exit(1);
5616
+ }
5617
+ if (resolved.candidates.length === 0) {
5618
+ console.error(resolved.scoped
5619
+ ? `流程 ${wfScope[0]} 的节点表里没有匹配「${nodeNameFilter}」的节点。`
5620
+ : `本地缓存的 ${resolved.poolSize} 个流程节点表里没有匹配「${nodeNameFilter}」的节点。可加 --workflow <流程id> 限定流程,或先在待办列表里翻到该流程任意一条以缓存其节点表。`);
5621
+ process.exit(1);
5622
+ }
5623
+ say(BOX_TOP);
5624
+ say(` 「${nodeNameFilter}」匹配到 ${resolved.candidates.length} 个节点,请选一个后再用 --node-id 指定:`);
5625
+ say(DIVIDER);
5626
+ for (const c of resolved.candidates)
5627
+ say(` ${String(c.nodeId).padStart(6, ' ')} ${c.nodeName}${c.matchType === 'exact' ? ' ★(精确)' : ''} ← ${c.workflowName ?? '(未命名流程)'}(流程 ${c.workflowId})`);
5628
+ say(BOX_TOP);
5629
+ say(` 例:e9 todo ${sub} --node-id ${resolved.candidates[0].nodeId} --workflow ${resolved.candidates[0].workflowId}`);
5630
+ process.exit(0);
5631
+ }
5632
+ nodeIdFilter = pick.nodeId;
5633
+ say(`按节点名筛选:「${pick.nodeName}」→ 节点 id=${pick.nodeId}(流程 ${pick.workflowName ?? pick.workflowId})`);
5634
+ }
4292
5635
  let workflowTypeFilter = queryFlag('workflow-type');
4293
5636
  if (workflowTypeFilter && !/^\d+$/.test(workflowTypeFilter)) {
4294
5637
  const resolvedType = await resolveWorkflowType(cfg, workflowTypeFilter, { category, viewcondition });
@@ -4318,9 +5661,11 @@ async function main() {
4318
5661
  requestLevel: queryFlag('request-level'),
4319
5662
  requestMark: queryFlag('request-mark'),
4320
5663
  workflowType: workflowTypeFilter,
4321
- nodeId: queryFlag('node-id'),
5664
+ nodeId: nodeIdFilter,
4322
5665
  flowStatus: queryFlag('flow-status'),
4323
5666
  totalOnly: queryBool('total-only'),
5667
+ // 当前处理人接口在服务端较慢(分组视图会并发触发 5 次),--no-operators 可跳过
5668
+ withOperators: !queryBool('no-operators'),
4324
5669
  debug: queryBool('debug'),
4325
5670
  excludeCreatorId,
4326
5671
  };
@@ -4328,6 +5673,7 @@ async function main() {
4328
5673
  const shouldGroup = !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
4329
5674
  if (shouldGroup) {
4330
5675
  const grouped = await getOaTabGroups(cfg, { ...listOpts, category });
5676
+ await annotateNodeNamesForCli(cfg, grouped.groups.flatMap((g) => g.items), args);
4331
5677
  const scope = CATEGORY_SCOPE[category] ?? CATEGORY_SCOPE.todo;
4332
5678
  const groupedSnapshot = {
4333
5679
  category,
@@ -4365,6 +5711,7 @@ async function main() {
4365
5711
  return;
4366
5712
  }
4367
5713
  const result = await getWfList(cfg, listOpts);
5714
+ await annotateNodeNamesForCli(cfg, result.items, args);
4368
5715
  const savedResult = persistSearchResult(result, `E9 ${name ?? key} 查询`);
4369
5716
  if (args.flags.raw) {
4370
5717
  console.error('===== RAW getWfListParams =====');
@@ -4395,6 +5742,7 @@ async function main() {
4395
5742
  creatorId: it.creatorId,
4396
5743
  currentOperator: it.currentOperator,
4397
5744
  node: it.node,
5745
+ nodeName: it.nodeName,
4398
5746
  createdate: it.createdate,
4399
5747
  receivedate: it.receivedate,
4400
5748
  status: it.status,
@@ -4476,21 +5824,112 @@ async function runTodoSubmit(args) {
4476
5824
  }
4477
5825
  const remark = flagStr(args, 'remark') ?? 'OK';
4478
5826
  const send = !!args.flags.yes;
5827
+ // --field-values '<json>':提交时修改字段值。
5828
+ const fvRaw = flagStr(args, 'field-values');
5829
+ let fieldValues;
5830
+ if (fvRaw) {
5831
+ try {
5832
+ fieldValues = JSON.parse(fvRaw);
5833
+ }
5834
+ catch {
5835
+ console.error(`--field-values 不是合法 JSON:${fvRaw}`);
5836
+ process.exit(1);
5837
+ }
5838
+ if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues)) {
5839
+ console.error('--field-values 需为对象,如 \'{"70702":"新值"}\'');
5840
+ process.exit(1);
5841
+ }
5842
+ }
5843
+ // --file <字段id[_行号]>=<路径>(逗号分隔多个):主表/明细附件
5844
+ const fileRaw = flagStr(args, 'file');
5845
+ const files = [];
5846
+ if (fileRaw) {
5847
+ for (const seg of fileRaw.split(',')) {
5848
+ if (!seg.trim())
5849
+ continue;
5850
+ const i = seg.indexOf('=');
5851
+ if (i <= 0) {
5852
+ console.error(`--file 格式应为 <字段id>=<路径>(明细行用 <字段id>_<行号>=<路径>),收到: ${seg}`);
5853
+ process.exit(1);
5854
+ }
5855
+ files.push({ fieldId: seg.slice(0, i).trim(), filePath: seg.slice(i + 1).trim() });
5856
+ }
5857
+ }
5858
+ for (const fl of files) {
5859
+ if (!fs.existsSync(fl.filePath)) {
5860
+ console.error(`文件不存在: ${fl.filePath}`);
5861
+ process.exit(1);
5862
+ }
5863
+ }
5864
+ // --detail-file <json>:明细行修改/新增/删除
5865
+ const detailFileRaw = flagStr(args, 'detail-file');
5866
+ let detailFileJson = [];
5867
+ if (detailFileRaw) {
5868
+ try {
5869
+ detailFileJson = JSON.parse(fs.readFileSync(detailFileRaw, 'utf8'));
5870
+ parseDetailFileSpec(detailFileJson); // 提前做结构校验
5871
+ }
5872
+ catch (e) {
5873
+ console.error(`--detail-file 解析失败:${e.message}`);
5874
+ process.exit(1);
5875
+ }
5876
+ }
5877
+ const hasInputs = (fieldValues && Object.keys(fieldValues).length) || files.length || detailFileJson.length;
5878
+
4479
5879
  if (!send) {
4480
5880
  console.log(BOX_TOP);
4481
5881
  console.log(` [dry-run] 提交待办 requestid=${requestId}`);
4482
5882
  console.log(` 签字意见 = ${remark}`);
5883
+ if (fieldValues && Object.keys(fieldValues).length)
5884
+ console.log(` 字段修改 = ${JSON.stringify(fieldValues)}`);
5885
+ if (!hasInputs) {
5886
+ console.log(' 确认无误后加 --yes 真正提交。');
5887
+ console.log(BOX_TOP);
5888
+ return;
5889
+ }
5890
+ try {
5891
+ // dry-run:读 loadForm/detailData 并校验,但不触发任何上传
5892
+ const prep = await prepareTodoSubmission(cfg, {
5893
+ requestId, remark, fieldValues, files, detailFile: detailFileJson, preview: true,
5894
+ });
5895
+ for (const line of describeDetailPlan(prep.plan))
5896
+ console.log(` 明细 ${line}`);
5897
+ if (files.length) {
5898
+ console.log(' 附件(提交时上传):');
5899
+ for (const target of prep.fileTargets)
5900
+ console.log(` ${target.key} ← ${target.filePath}`);
5901
+ }
5902
+ for (const w of prep.warnings ?? [])
5903
+ console.log(` ⚠ ${w}`);
5904
+ if (prep.formula?.applied?.length)
5905
+ console.log(` 公式重算 ${prep.formula.applied.length} 项`);
5906
+ if (prep.formula?.skipped?.length)
5907
+ console.log(` ⚠ 公式跳过 ${prep.formula.skipped.length} 项:${prep.formula.skipped.map((s) => s.reason).slice(0, 3).join(';')}`);
5908
+ console.log(' 提交体预览:');
5909
+ console.log(JSON.stringify(buildTodoSubmitBody(prep), null, 2));
5910
+ }
5911
+ catch (e) {
5912
+ console.error(`预览失败:${e.message}`);
5913
+ process.exit(1);
5914
+ }
4483
5915
  console.log(' 确认无误后加 --yes 真正提交。');
4484
5916
  console.log(BOX_TOP);
4485
5917
  return;
4486
5918
  }
4487
- const res = await submitTodo(cfg, { requestId, remark });
5919
+ const res = await submitTodo(cfg, { requestId, remark, fieldValues, files, detailFile: detailFileJson });
4488
5920
  console.log(BOX_TOP);
4489
5921
  const j = res.json;
4490
- const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';
4491
- console.log(` ${ok ? '✅ 提交成功' : '⚠ 提交异常'} status=${res.status}`);
5922
+ const data = j?.data ?? j ?? {};
5923
+ const type = data?.type ?? j?.type;
5924
+ const info = data?.messageInfo ?? j?.messageInfo;
5925
+ const reason = info?.detail || info?.bottom || info?.title || '';
5926
+ const ok = type === 'SUCCESS';
5927
+ const asyncEmpty = type === 'ASYNC_SUBMIT' && !reason;
5928
+ console.log(` ${ok ? '✅ 提交成功' : asyncEmpty ? '⚠ 提交结果未确认(ASYNC_SUBMIT)' : '⚠ 提交异常'} status=${res.status}`);
5929
+ if (reason)
5930
+ console.log(` 原因:${reason}`);
4492
5931
  if (j)
4493
- console.log('响应:', JSON.stringify(j).slice(0, 400));
5932
+ console.log('响应:', JSON.stringify(j).slice(0, 600));
4494
5933
  else
4495
5934
  console.log('响应(非JSON):', (res.text ?? '').slice(0, 300));
4496
5935
  console.log(BOX_TOP);