weaver-e9-assistant-cli 1.1.5 → 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,10 +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';
7
+ import { loadConfig, resolveViewcondition, endpointFor, setConfigField, profileNameFromBaseUrl } from './config.js';
8
+ import { cacheGet } from './cache-store.js';
6
9
  import { getWfList, getOaTabGroups, resolveWorkflowType, CATEGORY_SCOPE, parseTimeRange } from './workflow.js';
7
- import { annotateNodeNames, isNodeNameDisabled, resolveNodeByName } from './nodemap.js';
10
+ import { annotateNodeNames, isNodeNameDisabled, resolveNodeByName, nodeMapCachePath } from './nodemap.js';
8
11
  import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
9
12
  import { pageUrl, availablePages } from './pageopen.js';
10
13
  import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
@@ -13,7 +16,8 @@ import { loadSubmission, updateSubmission } from './submissionstate.js';
13
16
  import { saveSubmissionPlan, loadSubmissionPlan } from './submissionplan.js';
14
17
  import { saveCandidateQuery, loadCandidateQuery, persistSearchResult } from './querystate.js';
15
18
  import { getFlowSummary } from './wfsummary.js';
16
- import { submitTodo, retractRequest, validateFieldValuesAtNode } from './todosubmit.js';
19
+ import { submitTodo, retractRequest, prepareTodoSubmission, buildTodoSubmitBody } from './todosubmit.js';
20
+ import { parseDetailFileSpec, describeDetailPlan } from './tododetail.js';
17
21
  import { rejectTodo } from './todoreject.js';
18
22
  import { transferTodo } from './todotransfer.js';
19
23
  import { forwardTodo, circulateTodo } from './todoforward.js';
@@ -21,23 +25,32 @@ import { ccTodo } from './todocc.js';
21
25
  import { askIdeaTodo } from './todoaskidea.js';
22
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';
23
27
  import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
24
- import { describeFormFields, VIEWATTR_LABEL } 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';
25
30
  import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
26
31
  import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus } from './auth.js';
27
32
  import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
28
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';
29
35
  import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
30
36
  import { getSuperviseTree, getSuperviseCount, getSuperviseConditions, listSupervise, doSupervise, resolveSuperviseTab, SUPERVISE_TABS } from './supervise.js';
31
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';
32
38
  import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj, getKqReport, getKqMonthReport } from './kq.js';
33
39
  import { collectQueryableBusiness } from './report.js';
34
- import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel, assertEditableCardValues } from './cube.js';
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';
35
41
  import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
36
- import { getSubordinates, assertUserIdsInScope, assertNamesInScope, resolveNameToUid, isScopeEnforced } from './subordinate.js';
42
+ import { getSubordinates, assertUserIdsInScope, assertNamesInScope, resolveNameToUid, isScopeEnforced, scopeCachePath } from './subordinate.js';
37
43
  import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds, expandHrmOrgTree } from './hrm.js';
38
44
  import { parseArgs, flagStr, flagBool } from './args.js';
39
45
  import { todayStr } from './util.js';
40
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
+ }
41
54
  function usage() {
42
55
  return [
43
56
  'E9 个人助手 CLI',
@@ -50,9 +63,10 @@ function usage() {
50
63
  ' [--create-time <range>] [--receive-time <range>] [--operate-time <range>]',
51
64
  ' [--request-level <level>] [--request-mark <code>] [--workflow-type <type>]',
52
65
  ' [--node-id <id> | --node-name <节点名>] [--flow-status <status>] [--sort <spec>] [--sub-category <tab>]',
53
- ' [--total-only] [--ungrouped] [--no-node-name] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
66
+ ' [--total-only] [--ungrouped] [--no-node-name] [--no-operators] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
54
67
  ' e9 todo summary --requestid <id> 查看流程摘要(表单字段+值+流转历史)',
55
- ' 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;可改字段、增删改明细行、上传附件)',
56
70
  ' e9 todo reject --requestid <id> [--remark "退回"] [--reject-to-node <nodeid>] [--reject-to-type <t>] [--reject-node <nodes>] [--yes] 退回/驳回待办',
57
71
  ' e9 todo transfer --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转办"] [--yes] 转办(处理权转移)',
58
72
  ' e9 todo forward --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转发"] [--yes] 转发(分享给某人看)',
@@ -65,6 +79,7 @@ function usage() {
65
79
  ' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
66
80
  ' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
67
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] 批量查下属日报(按人并发,含未写清单)',
68
83
  ' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
69
84
  ' e9 blog view --user-id <id> 查看某人微博主页',
70
85
  ' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
@@ -94,7 +109,7 @@ function usage() {
94
109
  ' e9 create open --name <流程名> 按名查找 id 并生成发起 URL(多个候选则列出)',
95
110
  ' e9 create open --requestid <请求id> [--id <wf>] 生成已有请求表单 URL(处理/提交待办用,手动填+提交)',
96
111
  ' e9 create fields --id <流程id> 模拟打开发起表单,列出字段(可编辑/只读/隐藏/控件类型)',
97
- ' 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>] 预览生成确认计划;提交复用确认字段',
98
113
  ' e9 create flow --requestid <请求id> --workflowid <流程id> [--next-user <uid>] [--yes] 公文流转:取可流转候选→确认→推送下一节点(公文 create start 后续步骤)',
99
114
  ' e9 page-open --page todo|done|mine|center|newflow|view [--request-id <id>] 生成流程中心页面链接',
100
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)',
@@ -118,7 +133,7 @@ function usage() {
118
133
  ' e9 workplan search-conditions 搜索可用条件(分组+字段)',
119
134
  ' e9 workplan attention 关注的日程',
120
135
  ' e9 workplan base-data 日历初始化数据(含部门/分部/组织视图)',
121
- ' 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 跳过总数请求更快)',
122
137
  ' e9 workplan create --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm" [--description-file <路径>] 新建日程(默认 dry-run)',
123
138
  ' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
124
139
  ' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
@@ -172,11 +187,28 @@ function usage() {
172
187
  ' e9 hrm search [--tab <全部人员|同部门|我的下属>] [--name <姓名>] [--condition "岗位=经理;性别=男"]',
173
188
  ' [--dept <部门名|id>] [--subcompany <分部名|id>] [--all-level] [--page <n>] [--json] [--raw] 人事搜索(姓名=详情卡片超链接)',
174
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 打印各维度缓存文件大小、条目数、最后更新时间',
175
206
  ' e9 config 查看当前配置来源与状态',
176
207
  ' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
177
208
  ' e9 config init 首次配置引导',
178
209
  '',
179
210
  '说明:',
211
+ ' E9_TLS_MAX TLS 客户端上限,默认 1.2(E9 服务端 1.3 握手慢约 2.2s);服务端仅支持 1.3 时设 1.3',
180
212
  ' todo list 获取我的待办流程列表(跑 getWfListParams→splitPageKey→datas→counts 链路)',
181
213
  ' --tab tab key,对应 config.tabs 中的某项,默认 defaultTab',
182
214
  ' --page 页码,默认 1',
@@ -184,10 +216,15 @@ function usage() {
184
216
  ' --json 以 JSON 输出(适合 AI 工具消费)',
185
217
  ' --raw 额外打印各步原始响应(首次调试必用)',
186
218
  ' --debug 打印每步请求状态',
219
+ ' --no-operators 不查当前处理人(该接口较慢,分组视图会触发 5 次;可显著提速)',
187
220
  ' blog list 获取微博列表(默认今天,支持 --scope 切换范围)',
188
221
  ' --date 日期 YYYY-MM-DD,默认今天',
189
222
  ' --scope 范围:全部/关注/共享/下级(默认=自己的微博)',
190
223
  ' --page 页码,默认 1',
224
+ ' blog subordinates 批量查下属日报(先取下属列表,再按人并发查)',
225
+ ' --date 日报日期 YYYY-MM-DD,默认今天',
226
+ ' --all-level 全部层级下属(默认直接下属)',
227
+ ' --names "张三,李四" 只看指定下属(精确姓名,逗号/顿号/空格分隔)',
191
228
  ' blog search 搜索微博(GET /api/blog/base/getMyBlogListBySearch)',
192
229
  ' --content 搜索关键词(必填)',
193
230
  ' --start-date/--end-date 日期区间 YYYY-MM-DD',
@@ -234,7 +271,7 @@ function usage() {
234
271
  ' create start 发起流程:无 --values 列待填字段(含明细表;多选浏览框自动列前10选项);',
235
272
  ' --values/--values-file 给值(key=字段id,值=文本/option id;中文用 --values-file);',
236
273
  ' --detail-file 给明细表行数据(JSON 数组 [{index, rows:[{fieldId:value}]}],index=明细表序号0起,每行多字段);',
237
- ' --file <字段id>=<本地路径> 上传图片/附件(逗号分隔多个);默认 dry-run,--yes 真发',
274
+ ' --file <字段id>=<本地路径> 上传图片/附件;明细行用 <字段id>_<行号>=<路径>(逗号分隔多个);默认 dry-run,--yes 真发',
238
275
  ' email list 收件箱邮件列表(单步 GET /api/email/list/allList)',
239
276
  ' --page 页码,默认 1',
240
277
  ' --subject 主题关键字过滤',
@@ -304,7 +341,8 @@ function usage() {
304
341
  ' workplan search-conditions 搜索可用条件(POST /api/workplan/search/getCondition)',
305
342
  ' workplan attention 关注的日程(POST /api/workplan/calendar/getAttentionData)',
306
343
  ' workplan base-data 日历初始化数据(POST /api/workplan/calendar/getBaseData)',
307
- ' 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)',
308
346
  ' --name 名称关键字',
309
347
  ' --group 分组:全部/今天/本周/本月/本季/本年',
310
348
  ' --type 日程类型 id / --status 状态 id / --creater 创建人 id / --receiver 接收人 id',
@@ -511,6 +549,21 @@ async function runCreatePrepare(args) {
511
549
  console.log(`\n确认后:e9 create create ${updated.id} --confirm --submit`);
512
550
  }
513
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
+ }
514
567
  async function runContextCreate(args) {
515
568
  const id = args._[2];
516
569
  if (!id)
@@ -519,6 +572,7 @@ async function runContextCreate(args) {
519
572
  if (!args.flags.confirm || !args.flags.submit) {
520
573
  const cfg = loadConfig(flagStr(args, 'config'));
521
574
  const prepared = await prepareCreateSubmission(cfg, context.workflowId, context.values, context.requestName, context.details);
575
+ printFormulaResults(prepared);
522
576
  const plan = saveSubmissionPlan({ ...prepared, files: context.files });
523
577
  updateContext(id, { planId: plan.id });
524
578
  console.log(BOX_TOP);
@@ -678,25 +732,46 @@ async function runCreateStart(args) {
678
732
  const send = !!args.flags.yes;
679
733
  let wfId;
680
734
  if (!id && name) {
681
- // 按名查;多个候选则列出让用户选
682
- const hits = await findCreateWfByName(cfg, name);
683
- if (hits.length === 0) {
684
- console.error(`按名「${name}」未找到流程。`);
685
- 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
+ }
686
745
  }
687
- if (hits.length > 1) {
688
- console.log(BOX_TOP);
689
- console.log(` 「${name}」找到 ${hits.length} 个相似流程,请选一个:`);
690
- console.log(DIVIDER);
691
- for (let i = 0; i < hits.length; i++) {
692
- const w = hits[i];
693
- 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})`);
694
753
  }
695
- console.log(BOX_TOP);
696
- console.log(' 用 --id <流程id> 指定其中一个再跑。');
697
- 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 ?? '';
698
774
  }
699
- wfId = hits[0].id ?? '';
700
775
  }
701
776
  else {
702
777
  try {
@@ -709,7 +784,7 @@ async function runCreateStart(args) {
709
784
  }
710
785
  const r = await getStartableFields(cfg, wfId);
711
786
  const requestname = flagStr(args, 'requestname');
712
- // --file <字段id>=<本地路径>(逗号分隔多个):上传图片/附件
787
+ // --file <字段id>[ _<行号>]=<本地路径>(逗号分隔多个):上传图片/附件;明细行带 _<行号>
713
788
  const fileRaw = flagStr(args, 'file');
714
789
  const files = [];
715
790
  if (fileRaw) {
@@ -757,23 +832,25 @@ async function runCreateStart(args) {
757
832
  process.exit(1);
758
833
  }
759
834
  }
760
- // 无值来源且无 --file 且无 --detail-file:打印待填字段清单(供 AI 问答)
761
- 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) {
762
839
  if (args.flags.json) {
763
- 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));
764
841
  return;
765
842
  }
766
843
  console.log(BOX_TOP);
767
844
  console.log(` E9 发起流程 · ${r.workflowName ?? 'workflowid=' + r.workflowId} (id=${r.workflowId})`);
768
- 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} 个(未展示)` : ''}`);
769
846
  console.log(BOX_TOP);
770
- console.log(' 注:viewattr 是 loadForm 静态值,不反映运行时可编辑性(va=3 隐藏实际可能可填)。填哪些由你定。');
847
+ console.log(' 注:字段是否可填以「可编辑 / 🚫原因」标注为准;viewattr 0=隐藏/1=只读不可填、2=可编辑/3=必填可填,另叠加 isonlyshow/isedit/isview 权限位。');
771
848
  if (r.fields.length === 0) {
772
849
  console.log('\n (无主表业务字段——该流程可能是自由流程或仅系统字段)');
773
850
  }
774
851
  for (const f of r.fields) {
775
852
  console.log(`\n ▸ [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''}`);
776
- 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(' · ');
777
854
  if (meta)
778
855
  console.log(` ${meta}`);
779
856
  if (f.options && f.options.length > 0) {
@@ -794,7 +871,7 @@ async function runCreateStart(args) {
794
871
  console.log('\n ─────────────────────────────────────────────');
795
872
  console.log(' 给值:--values-file \'<json>\' key=字段id,值=文本/option id(中文用 --values-file 避免乱码)');
796
873
  console.log(' 明细:--detail-file \'<json>\' [{index:0, rows:[{22106:"v",22107:"v2"}, ...]}, ...](index=明细表序号0起,每行多字段)');
797
- console.log(' 上传:--file <字段id>=<本地路径> 逗号分隔多个;图片按扩展名自动判 listType');
874
+ console.log(' 上传:--file <字段id>=<本地路径> 明细行用 <字段id>_<行号>=<路径>;逗号分隔多个;图片按扩展名自动判 listType');
798
875
  console.log(' 发起:默认 dry-run,确认后加 --yes 真提交');
799
876
  return;
800
877
  }
@@ -808,6 +885,8 @@ async function runCreateStart(args) {
808
885
  console.error(`--values / --values-file 不是合法 JSON:${valuesRawResolved}`);
809
886
  process.exit(1);
810
887
  }
888
+ // ★ 记忆默认值已移到 prepareCreateSubmission 内、表单代码块执行之后应用,
889
+ // 保证优先级为:用户手填 > 表单代码块 > 记忆默认值 > 内置标签默认值。
811
890
  }
812
891
  // 预览与提交必须共用同一份已解析字段,避免默认值、联动或浏览框名称在确认后改变。
813
892
  if (!send || args.flags.dryRun) {
@@ -820,7 +899,8 @@ async function runCreateStart(args) {
820
899
  console.log(` ▸ field${fl.fieldId} ← ${fl.filePath}`);
821
900
  console.log(' 附件上传会产生新文件 ID,不能冻结到本次确认计划;请在无附件字段变更时单独确认。');
822
901
  }
823
- const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details);
902
+ const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details, r);
903
+ printFormulaResults(prepared);
824
904
  const plan = saveSubmissionPlan({ ...prepared, files });
825
905
  if (details.length > 0) {
826
906
  console.log(BOX_TOP);
@@ -842,7 +922,7 @@ async function runCreateStart(args) {
842
922
  }
843
923
  const plan = loadSubmissionPlan(planId, wfId);
844
924
  // 真提交只刷新 loadForm 的短期签名参数;业务字段、明细与浏览框名称完全复用确认计划。
845
- 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 });
846
926
  const res = submitResult.response;
847
927
  const j = res.json;
848
928
  const rid = j?.requestid ?? j?.data?.requestid ?? j?.data?.resultInfo?.requestid ?? j?.data?.requestId;
@@ -1102,7 +1182,9 @@ async function runCreateFields(args) {
1102
1182
  process.exit(1);
1103
1183
  }
1104
1184
  if (!id) {
1105
- 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);
1106
1188
  if (hits.length === 0) {
1107
1189
  console.error(`按名「${name}」未找到流程`);
1108
1190
  process.exit(1);
@@ -1116,15 +1198,23 @@ async function runCreateFields(args) {
1116
1198
  id = hits[0].id;
1117
1199
  }
1118
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
+ });
1119
1207
  if (args.flags.json) {
1120
- 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));
1121
1209
  return;
1122
1210
  }
1123
1211
  console.log(`E9 表单字段 [${r.workflowName ?? 'workflowid=' + r.workflowId}] ${r.totalFields} 个字段 verifyRight=${r.verifyRight}`);
1124
- for (const t of r.tables) {
1212
+ if (hiddenCount > 0)
1213
+ console.log(`(另有 ${hiddenCount} 个「发起节点隐藏」字段未展示——它们在后续节点才填)`);
1214
+ for (const t of visibleTables) {
1125
1215
  console.log(`\n■ ${t.key === 'main' ? '主表' : t.key}(${t.fields.length} 字段)`);
1126
1216
  for (const f of t.fields) {
1127
- 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 ?? '?'}`);
1128
1218
  // 选择框选项(selectattr.selectitemlist)
1129
1219
  const items = f.raw?.selectattr?.selectitemlist;
1130
1220
  if (Array.isArray(items) && items.length > 0) {
@@ -1133,7 +1223,7 @@ async function runCreateFields(args) {
1133
1223
  }
1134
1224
  }
1135
1225
  console.log('\n注:必填标记不在 loadForm 接口(前端 DOM 控制),本表不显示必填。');
1136
- console.log(' 可编辑/只读/隐藏由 viewattr 给出——viewattr 是表单设计态静态值(va=1只读/2可编辑/3隐藏),不反映运行时实际可编辑性(va=3 隐藏实际可能可填,运行时权限以节点配置/联动为准)。');
1226
+ console.log(' viewattr 取值:0=隐藏 / 1=只读 / 2=可编辑 / 3=必填;另叠加 isonlyshow/isedit/isview 权限位。');
1137
1227
  }
1138
1228
  async function runCreateOpen(args) {
1139
1229
  const cfg = loadConfig(flagStr(args, 'config'));
@@ -1163,7 +1253,9 @@ async function runCreateOpen(args) {
1163
1253
  wfId = id;
1164
1254
  }
1165
1255
  else {
1166
- 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);
1167
1259
  if (hits.length === 0) {
1168
1260
  console.error(`按名「${name}」未找到可建流程。先跑 e9 create list --keyword "${name}" 看候选。`);
1169
1261
  process.exit(1);
@@ -1221,6 +1313,58 @@ async function runBlog(args) {
1221
1313
  const sub = args._[1] ?? 'list';
1222
1314
  const cfg = loadConfig(flagStr(args, 'config'));
1223
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
+ }
1224
1368
  if (sub === 'check') {
1225
1369
  const date = flagStr(args, 'date') ?? todayStr();
1226
1370
  // check 要判定整天的正常发布记录:逐页拉取 + 重复检测终止(服务端无 total、越界页重复回放)
@@ -1851,7 +1995,7 @@ async function runBlog(args) {
1851
1995
  console.log(JSON.stringify(r.template, null, 2));
1852
1996
  return;
1853
1997
  }
1854
- 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}`);
1855
1999
  process.exit(1);
1856
2000
  }
1857
2001
  async function runWorkplan(args) {
@@ -2138,6 +2282,7 @@ async function runWorkplan(args) {
2138
2282
  prjids: flagStr(args, 'prj-id'),
2139
2283
  crmids: flagStr(args, 'crm-id'),
2140
2284
  page: Number(flagStr(args, 'page') ?? '1') || 1,
2285
+ withCount: !flagBool(args, 'no-count'),
2141
2286
  });
2142
2287
  if (args.flags.raw) {
2143
2288
  console.error('===== RAW getlist =====');
@@ -3824,6 +3969,439 @@ function fmtMenus(e, kw) {
3824
3969
  const t = s.length > 32 ? s.slice(0, 32) + '…' : s;
3825
3970
  return ` 菜单=${t}`;
3826
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
+
3827
4405
  /**
3828
4406
  * #7 统一名称搜索:用户只说名字、不知归属(台账/菜单/流程)时,一次搜三类,避免猜错域来回。
3829
4407
  * 台账:cube find(台账目录 list_name + 菜单挂载名)
@@ -4826,7 +5404,7 @@ async function main() {
4826
5404
  const cmd = args._[0] ?? 'todo';
4827
5405
  if (cmd === 'version') {
4828
5406
  const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
4829
- console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.5', buildHash }, null, 2));
5407
+ console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.11', buildHash }, null, 2));
4830
5408
  return;
4831
5409
  }
4832
5410
  if (cmd === 'whoami') {
@@ -4885,6 +5463,14 @@ async function main() {
4885
5463
  await runSearch(args);
4886
5464
  return;
4887
5465
  }
5466
+ if (cmd === 'memory') {
5467
+ await runMemory(args);
5468
+ return;
5469
+ }
5470
+ if (cmd === 'cache') {
5471
+ await runCache(args);
5472
+ return;
5473
+ }
4888
5474
  if (cmd === 'cube') {
4889
5475
  await runCube(args);
4890
5476
  return;
@@ -4979,7 +5565,9 @@ async function main() {
4979
5565
  workflowFilter = String(saved.candidates[candidate - 1].id);
4980
5566
  }
4981
5567
  else if (workflowFilter && !/^\d+(\s*,\s*\d+)*$/.test(workflowFilter)) {
4982
- 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);
4983
5571
  if (candidates.length === 0)
4984
5572
  throw new Error(`未找到名称包含“${workflowFilter}”的流程。`);
4985
5573
  if (candidates.length > 1) {
@@ -5076,6 +5664,8 @@ async function main() {
5076
5664
  nodeId: nodeIdFilter,
5077
5665
  flowStatus: queryFlag('flow-status'),
5078
5666
  totalOnly: queryBool('total-only'),
5667
+ // 当前处理人接口在服务端较慢(分组视图会并发触发 5 次),--no-operators 可跳过
5668
+ withOperators: !queryBool('no-operators'),
5079
5669
  debug: queryBool('debug'),
5080
5670
  excludeCreatorId,
5081
5671
  };
@@ -5234,7 +5824,7 @@ async function runTodoSubmit(args) {
5234
5824
  }
5235
5825
  const remark = flagStr(args, 'remark') ?? 'OK';
5236
5826
  const send = !!args.flags.yes;
5237
- // --field-values '<json>':提交时修改字段值。此前 CLI 声明了该参数却从未解析/传入(静默失效)。
5827
+ // --field-values '<json>':提交时修改字段值。
5238
5828
  const fvRaw = flagStr(args, 'field-values');
5239
5829
  let fieldValues;
5240
5830
  if (fvRaw) {
@@ -5250,29 +5840,96 @@ async function runTodoSubmit(args) {
5250
5840
  process.exit(1);
5251
5841
  }
5252
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
+
5253
5879
  if (!send) {
5254
5880
  console.log(BOX_TOP);
5255
5881
  console.log(` [dry-run] 提交待办 requestid=${requestId}`);
5256
5882
  console.log(` 签字意见 = ${remark}`);
5257
5883
  if (fieldValues && Object.keys(fieldValues).length)
5258
5884
  console.log(` 字段修改 = ${JSON.stringify(fieldValues)}`);
5259
- // dry-run 即校验字段属性(当前节点):只读/隐藏字段禁止填值——避免"预览通过、提交才报错"
5260
- if (fieldValues && Object.keys(fieldValues).length) {
5261
- const bad = await validateFieldValuesAtNode(cfg, requestId, fieldValues);
5262
- if (bad.length)
5263
- throw new Error(`拒绝填写当前节点的只读/隐藏字段:${bad.map((b) => `${b.label || b.id}(${VIEWATTR_LABEL[b.viewattr] ?? 'va=' + b.viewattr})`).join('、')}。本节点只允许修改「可编辑」字段,不得绕过节点字段权限自行填值。`);
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);
5264
5914
  }
5265
5915
  console.log(' 确认无误后加 --yes 真正提交。');
5266
5916
  console.log(BOX_TOP);
5267
5917
  return;
5268
5918
  }
5269
- const res = await submitTodo(cfg, { requestId, remark, fieldValues });
5919
+ const res = await submitTodo(cfg, { requestId, remark, fieldValues, files, detailFile: detailFileJson });
5270
5920
  console.log(BOX_TOP);
5271
5921
  const j = res.json;
5272
- const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';
5273
- 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}`);
5274
5931
  if (j)
5275
- console.log('响应:', JSON.stringify(j).slice(0, 400));
5932
+ console.log('响应:', JSON.stringify(j).slice(0, 600));
5276
5933
  else
5277
5934
  console.log('响应(非JSON):', (res.text ?? '').slice(0, 300));
5278
5935
  console.log(BOX_TOP);