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/auth-token.js +4 -4
- package/dist/cache-store.js +73 -0
- package/dist/cli.js +714 -57
- package/dist/client.js +1 -1
- package/dist/cube.js +16 -38
- package/dist/formula.js +246 -0
- package/dist/hrm.js +33 -19
- package/dist/memory.js +968 -0
- package/dist/nodemap.js +232 -242
- package/dist/subordinate.js +86 -35
- package/dist/teamreport.js +77 -0
- package/dist/tododetail.js +516 -0
- package/dist/todosubmit.js +619 -26
- package/dist/util.js +21 -0
- package/dist/wfcodeblock.js +761 -0
- package/dist/wfcreate.js +29 -1
- package/dist/wfform.js +286 -67
- package/dist/wfstart.js +516 -81
- package/dist/wfsummary.js +1 -1
- package/dist/wfupload.js +48 -4
- package/dist/workflow.js +4 -2
- package/dist/workplan.js +21 -6
- package/package.json +2 -2
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,
|
|
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]
|
|
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>=<本地路径>
|
|
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
|
|
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
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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 (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
console.
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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('
|
|
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,
|
|
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>=<本地路径>
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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>'
|
|
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
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
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
|
|
5273
|
-
|
|
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,
|
|
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);
|