weaver-e9-assistant-cli 1.0.0

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 ADDED
@@ -0,0 +1,4041 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import fs from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { loadConfig, resolveViewcondition, endpointFor, setConfigField } from './config.js';
6
+ import { getWfList, getSmartTodoGroups, resolveWorkflowType } from './workflow.js';
7
+ import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTypes } from './todobatch.js';
8
+ import { pageUrl, availablePages } from './pageopen.js';
9
+ import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
10
+ import { createContext, loadContext, updateContext } from './createcontext.js';
11
+ import { loadSubmission, updateSubmission } from './submissionstate.js';
12
+ import { saveSubmissionPlan, loadSubmissionPlan } from './submissionplan.js';
13
+ import { saveCandidateQuery, loadCandidateQuery, persistSearchResult } from './querystate.js';
14
+ import { getFlowSummary } from './wfsummary.js';
15
+ import { submitTodo, retractRequest } from './todosubmit.js';
16
+ import { rejectTodo } from './todoreject.js';
17
+ import { transferTodo } from './todotransfer.js';
18
+ import { forwardTodo, circulateTodo } from './todoforward.js';
19
+ import { ccTodo } from './todocc.js';
20
+ import { askIdeaTodo } from './todoaskidea.js';
21
+ import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkIndex, getBlogMenuCountdata, getVisitorList, getBlogNote, getCurrentUserInfo, getMsgRemindList, getDefaultTemplate, getBlogAppList, getAtAllList, getBlogPageSearchCondition, getBlogHomepageList, searchMyBlog, getBlogPersonalInfo, sendRemindUnsubmit, addBlogReply, addOrDeleteZan, saveBlogNote, recallBlog, deleteBlogReply } from './blog.js';
22
+ import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
23
+ import { describeFormFields } from './wfform.js';
24
+ import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
25
+ import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus } from './auth.js';
26
+ import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
27
+ import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel } from './workplan.js';
28
+ import { listOdocTodo, getOdocTodoCount, listOdocCreate } from './odoc.js';
29
+ import { punch, getButtons, resolvePunchItem, signSecret, buildEncryptObj } from './kq.js';
30
+ import { collectQueryableBusiness } from './report.js';
31
+ import { listCube, getCubeCondition, getCubeInit, exportCubeExcel, getSearchColumnInfo, listCubeExcel, getCubeCountData, getCubeGroupData, getExpCardConfig, getModeBrowserOptions, getCardFields, submitCubeCard, deleteCubeData, printCubePdf, listCubeMenus, findCubeByName, fetchMenuMounts, previewCubeCard, fetchCubeRowsByIds, resolveCubeCondition, downloadCubeImportTemplate, importCubeExcel } from './cube.js';
32
+ import { getCurrentUser } from './wfuser.js';
33
+ import { parseArgs, flagStr, flagBool } from './args.js';
34
+ import { todayStr } from './util.js';
35
+ import { BOX_TOP, DIVIDER } from './render.js';
36
+ function usage() {
37
+ return [
38
+ 'E9 个人助手 CLI',
39
+ '',
40
+ '用法:',
41
+ ' e9 version',
42
+ ' e9 todo list [--tab <key>] [--page <n>] [--keyword <text>] [--exclude-current-user]',
43
+ ' [--workflow <id>] [--creator <id>] [--creator-dept <id>] [--creator-subcom <id>]',
44
+ ' [--create-time <range>] [--receive-time <range>] [--operate-time <range>]',
45
+ ' [--request-level <level>] [--request-mark <code>] [--workflow-type <type>]',
46
+ ' [--node-id <id>] [--flow-status <status>] [--sort <spec>] [--sub-category <tab>]',
47
+ ' [--total-only] [--ungrouped] [--creator-name <姓名>] [--resume-query <id> --candidate <n>]',
48
+ ' e9 todo summary --requestid <id> 查看流程摘要(表单字段+值+流转历史)',
49
+ ' e9 todo submit --requestid <id> [--remark "同意"] [--yes] 提交待办到下一节点(默认签字 OK)',
50
+ ' e9 todo reject --requestid <id> [--remark "退回"] [--reject-to-node <nodeid>] [--reject-to-type <t>] [--reject-node <nodes>] [--yes] 退回/驳回待办',
51
+ ' e9 todo transfer --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转办"] [--yes] 转办(处理权转移)',
52
+ ' e9 todo forward --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "转发"] [--yes] 转发(分享给某人看)',
53
+ ' e9 todo circulate --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "传阅"] [--yes] 传阅',
54
+ ' e9 todo cc --requestid <id> [--remark "抄送"] [--yes] 抄送',
55
+ ' e9 todo ask-idea --requestid <id> --receiver <uid> | --receiver-name <姓名> [--remark "意见"] [--yes] 意见征询(向某人征询意见,不改审批人)',
56
+ ' e9 todo batch-submit --request-ids "1,2" --user-ids "u1,u2" [--remark "同意"] [--yes] 批量提交(先预览)',
57
+ ' e9 todo batch-forward --request-ids "1,2" --receiver <uid> [--remark "转发"] [--yes] 批量转发(先预览)',
58
+ ' e9 todo batch-attention --request-ids "1,2" [--attention-id <类型id>] [--yes] 批量关注(先预览,默认加入首个关注类型)',
59
+ ' e9 todo done [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 已办列表',
60
+ ' e9 todo mine [--page <n>] [--keyword <text>] [--workflow <id>] [--creator <id>] [--sort <spec>] [--sub-category <tab>] [--json] [--raw] 我发起的流程',
61
+ ' e9 blog list [--date YYYY-MM-DD] [--scope <全部|关注|共享|下级>] [--page <n>] [--json] [--raw]',
62
+ ' e9 blog search --content <关键词> [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>] [--page <n>] 搜索我的微博',
63
+ ' e9 blog view --user-id <id> 查看某人微博主页',
64
+ ' e9 blog remind --user-id <id> [--workdate YYYY-MM-DD] [--yes] 催办(提醒某人写日报,默认今天)',
65
+ ' e9 blog reply --id <微博id> --content "<内容>" [--yes] 回复微博',
66
+ ' e9 blog like --id <微博id> [--yes] 点赞/取消点赞(默认 dry-run)',
67
+ ' e9 blog check [--date YYYY-MM-DD] 查某天是否已写日报(按 isReCalled 区分正常发布/已撤回);默认今天,无正常发布记录退出码 1',
68
+ ' e9 report collect [--date YYYY-MM-DD] [--json] 汇总当前环境可查询业务数据(非操作审计)',
69
+ ' e9 blog create --content "<文本>" [--content-file <路径>] [--share-type N] [--date YYYY-MM-DD] [--discuss-id <id>] [--yes]',
70
+ ' e9 blog recall --id <微博id> [--workdate <YYYY-MM-DD>] [--restore] [--yes] 撤回微博(--restore 恢复已撤回)',
71
+ ' e9 blog note --save --content "<文本>" [--content-file <路径>] [--date <YYYY-MM-DD>] [--yes] 存草稿/便签(E9 微博无独立草稿,存到工作笔记)',
72
+ ' e9 blog delete-reply --id <评论id> --discuss-id <微博id> [--workdate <YYYY-MM-DD>] [--yes] 删除微博评论(微博正文不可删,只能撤回)',
73
+ ' e9 auth token-login --consent 登录拿 aiAgentToken(先经隐私确认:信息会经大模型处理;preflight:探测地址可达+自动装 playwright+开浏览器登录,失败引导 cookie-login)',
74
+ ' e9 auth cookie-login [--cookie <串> | --file <路径>] [--consent] 手动导入 cookie 走 cookie 鉴权(需先隐私确认;无需浏览器,无桌面机器的后路)',
75
+ ' e9 auth status [--check] 查看当前环境 token 状态/剩余时间;--check 联网探活地址可达+鉴权是否真有效',
76
+ ' e9 auth consent true|false 设置隐私确认:true=同意信息经大模型处理(登录免 --consent),false=撤销同意',
77
+ ' e9 config set llmConsent true 一次性同意隐私确认(写入配置,登录免 --consent)',
78
+ ' e9 create list [--keyword <text>] [--tab <key>] [--json] [--raw] 我有权限新建的流程列表',
79
+ ' e9 create workflows [--name <关键词>] 查找可发起流程(四步式第 1 步)',
80
+ ' e9 create form --workflow-id <id> 初始化表单并创建上下文(第 2 步)',
81
+ ' e9 create prepare <context_id> --data <JSON> 保存字段值并预览(第 3 步)',
82
+ ' e9 create create <context_id> --confirm --submit 二次确认后发起(第 4 步)',
83
+ ' e9 create verify <submission_id> 核验超时发起是否已创建(禁止盲目重试)',
84
+ ' e9 create open --id <流程id> 生成发起该流程的页面 URL',
85
+ ' e9 create open --name <流程名> 按名查找 id 并生成发起 URL(多个候选则列出)',
86
+ ' e9 create open --requestid <请求id> [--id <wf>] 生成已有请求表单 URL(处理/提交待办用,手动填+提交)',
87
+ ' e9 create fields --id <流程id> 模拟打开发起表单,列出字段(可编辑/只读/隐藏/控件类型)',
88
+ ' e9 create start [--id <id>|--name <名>] [--values <json>|--values-file <path>] [--detail-file <path>] [--file <字段id>=<路径>] [--requestname <名>] [--yes --plan <id>] 预览生成确认计划;提交复用确认字段',
89
+ ' e9 create flow --requestid <请求id> --workflowid <流程id> [--next-user <uid>] [--yes] 公文流转:取可流转候选→确认→推送下一节点(公文 create start 后续步骤)',
90
+ ' e9 page-open --page todo|done|mine|center|newflow|view [--request-id <id>] 生成流程中心页面链接',
91
+ ' 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)',
92
+ ' e9 email view --id <邮件id> [--mark-read] 查看邮件详情(元信息+正文+附件清单,默认只读)',
93
+ ' e9 email send --to <邮箱> --subject <主题> --content <正文> [--attachment <文件>] 写信+发送(可带附件,默认 dry-run)',
94
+ ' e9 email read --id <ids> [--unread] 标记已读/未读',
95
+ ' e9 email star --id <ids> [--unstar] 标星/取消标星',
96
+ ' e9 email delete --id <ids> [--permanent] [--yes] 删除邮件(默认移回收站;--permanent 彻底删除)',
97
+ ' e9 email move --id <ids> --folder <folderid> [--yes] 移动邮件到文件夹(0收件箱/-1已发送/-2草稿/-3已删除)',
98
+ ' e9 email label list|add|apply 标签:list 列表 / add 新建 / apply 贴去标签',
99
+ ' e9 email waitdeal --id <ids> --time <YYYY-MM-DD> [--way 5|1] [--note <文本>] [--yes] 设为待办邮件(提醒处理)',
100
+ ' e9 email recall --id <邮件id> [--yes] 撤回已发邮件',
101
+ ' e9 email attachment upload --file <路径> 上传附件拿 fileid;attachment list 附件列表',
102
+ ' e9 email size 邮箱容量/未读统计;email accounts 接收账户列表',
103
+ ' e9 workplan list [--date <YYYY-MM-DD>] [--view-type <1|3>] [--group <部门|分部|下级|...>] 日程列表(日历数据)',
104
+ ' e9 workplan my-calendar [--date <YYYY-MM-DD>] [--view-type <1|3>] 日程月视图(MyCalendar,浏览器主用)',
105
+ ' e9 workplan view --id <id> 查看日程详情',
106
+ ' e9 workplan finish --id <id> [--yes] 完成日程',
107
+ ' e9 workplan delete --id <id> [--yes] 删除日程',
108
+ ' e9 workplan types 列出日程类型',
109
+ ' e9 workplan search-conditions 搜索可用条件(分组+字段)',
110
+ ' e9 workplan attention 关注的日程',
111
+ ' e9 workplan base-data 日历初始化数据(含部门/分部/组织视图)',
112
+ ' e9 workplan search [--name <text>] [--type <id>] [--status <id>] [--creater <id>] [--begin <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--group <全部|今天|本周|...>] [--page <n>] 日程表搜索(可过滤分页)',
113
+ ' e9 workplan create --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm" [--description-file <路径>] 新建日程(默认 dry-run)',
114
+ ' e9 workplan import-template [--out-dir <目录>] 下载原生日程导入模板',
115
+ ' e9 workplan import --file <模板.xls[x]> [--yes] 批量导入日程(默认仅预览)',
116
+ ' e9 odoc todo [--page <n>] [--subject <标题>] [--dispatch-num <发文字号>] [--type <公文种类id>] [--topic <主题分类id>] 公文待办列表(getCustomPageList table,每页 10 条)',
117
+ ' e9 odoc count [--view] 公文待办/待阅计数(发文/收文/签报)',
118
+ ' e9 odoc create-list [--type 1|2] 发文/收文流程列表(1=发文拟稿 2=收文登记,复用 create start 发起)',
119
+ ' e9 kq punch on|off [--group-id <id>] [--userid <id>] [--date <YYYY-MM-DD>] [--yes] 打卡(上班/下班卡,默认 dry-run 展示 encryptObj,--yes 真打)',
120
+ ' e9 kq status 查看今日打卡班次(getButtons timeline)',
121
+ ' e9 cube list --customid <id> [--page <n>] [--keyword <text>] [--condition <k=v;...>] [--json] 建模台账列表(--condition 支持中文字段名自动映射 domkey)',
122
+ ' e9 cube fields --customid <id> 建模查询条件探查(数据状态/筛选字段)',
123
+ ' e9 cube export --customid <id> [--filename <名>] [--condition <k=v;...>] [--keyword <text>] [--out-dir <目录>] [--plain] [--yes] 导出建模台账为 Excel(默认 dry-run;--plain 明文 modeCustomid 逃生口,默认 base64)',
124
+ ' e9 cube import-template --customid <id> [--out-dir <目录>] 下载原生台账导入模板',
125
+ ' e9 cube import --customid <id> --file <模板.xls[x]> [--yes] 批量导入台账(默认仅预览)',
126
+ ' e9 cube view --customid <id> --billid <id> 单条记录完整详情(label=值,行尾附卡片链接)',
127
+ ' e9 cube search-info --customid <id> 台账字段下拉选项全表(合并字段中文名)',
128
+ ' e9 cube excel --customid <id> [--menu-ids <m,..>] [--page <n>] Excel 视图取数(列含可编辑标记)',
129
+ ' e9 cube count --customid <id> [--menu-ids <m,..>] 聚合统计',
130
+ ' e9 cube group --customid <id> [--menu-ids <m,..>] 分组统计',
131
+ ' e9 cube card --customid <id> --mode-id <m> 卡片导出配置',
132
+ ' e9 cube browser --customid <id> --mode-id <m> --field-id <f> --detail-type <dt> 建模浏览框选项',
133
+ ' e9 cube card-fields --customid <id> 卡片字段定义(label/控件类型/可编辑/必填)',
134
+ ' e9 cube create --customid <id> --values <json> 新建台账记录(默认 dry-run,--yes 真提交)',
135
+ ' e9 cube edit --customid <id> --billid <id> --values <json> 编辑台账记录',
136
+ ' e9 cube delete --customid <id> --billids <id,...> 删除台账记录(默认 dry-run,--yes 真删)',
137
+ ' e9 cube print --customid <id> --billid <id> 打开合同详情页,用户在浏览器点打印另存 PDF',
138
+ ' e9 cube menus [--refresh] 列全部建模台账(目录接口为主、菜单兜底,缓存7天,一次拿全 customid+mode+app)',
139
+ ' e9 cube find <名称> 按名模糊匹配台账(列候选消歧,返回 customid+mode+app,供 list/create 等用)',
140
+ ' e9 cube find <名称> --tree-detail 展开 tree 测试类台账的全部树节点路径(默认只显首条菜单路径)',
141
+ ' e9 search <名称> 统一搜台账+菜单+流程(只说名字、不知归属时用,避免猜错域)',
142
+ ' e9 config 查看当前配置来源与状态',
143
+ ' e9 config set <field> <value> 设置配置(field: baseUrl/cookie/username/menuIds/...;首次用 set baseUrl)',
144
+ ' e9 config init 首次配置引导',
145
+ '',
146
+ '说明:',
147
+ ' todo list 获取我的待办流程列表(跑 getWfListParams→splitPageKey→datas→counts 链路)',
148
+ ' --tab tab key,对应 config.tabs 中的某项,默认 defaultTab',
149
+ ' --page 页码,默认 1',
150
+ ' --keyword 按标题(requestname)过滤',
151
+ ' --json 以 JSON 输出(适合 AI 工具消费)',
152
+ ' --raw 额外打印各步原始响应(首次调试必用)',
153
+ ' --debug 打印每步请求状态',
154
+ ' blog list 获取微博列表(默认今天,支持 --scope 切换范围)',
155
+ ' --date 日期 YYYY-MM-DD,默认今天',
156
+ ' --scope 范围:全部/关注/共享/下级(默认=自己的微博)',
157
+ ' --page 页码,默认 1',
158
+ ' blog search 搜索微博(GET /api/blog/base/getMyBlogListBySearch)',
159
+ ' --content 搜索关键词(必填)',
160
+ ' --start-date/--end-date 日期区间 YYYY-MM-DD',
161
+ ' --page 页码,默认 1',
162
+ ' blog view 查看某人微博主页(GET /api/blog/base/getBlogPersonalInfo)',
163
+ ' --user-id 用户 ID(必填)',
164
+ ' blog remind 催办(提醒某人写日报,GET /api/blog/base/sendRemindUnsubmit?remindId&workdate)',
165
+ ' --user-id 用户 ID(必填,即被催办人 remindId)',
166
+ ' --workdate 日报日期 YYYY-MM-DD(默认今天)',
167
+ ' --yes 真正发送(不加则只打印目标)',
168
+ ' blog reply 回复微博(POST /api/blog/base/addBlogReply)',
169
+ ' --id 微博 id(必填)',
170
+ ' --content 回复内容(必填)',
171
+ ' --yes 真正发送(不加则只打印目标)',
172
+ ' blog like 点赞/取消点赞(POST /api/blog/base/addOrDeleteZan,默认 dry-run,--yes 执行)',
173
+ ' --id 微博 id(必填)',
174
+ ' --workdate 微博日期(不传自动解析)',
175
+ ' --user-id 当前用户 id(不传自动解析)',
176
+ ' --opt-type 1=赞 / 0=取消(不传按当前点赞状态自动切换)',
177
+ ' --date 微博所在日期(列表自动查不到时指定)',
178
+ ' blog create 发布/补录/更新微博(默认 dry-run,加 --yes 才真发)',
179
+ ' --content 微博正文(纯文本,自动按行包 <p>)',
180
+ ' --content-file 微博正文来自文件(避免命令行中文/转义问题)',
181
+ ' --share-type 分享范围,默认 1',
182
+ ' --date 今天留空=发布今天;填过去日期=补录那天',
183
+ ' --discuss-id 目标微博 id(提供则走"更新整体替换"模式,用于续写已有微博)',
184
+ ' --yes 真正发送(不加则只打印请求体)',
185
+ ' blog stats 工作指数 + 心情指数 + 微博统计(调用 getBlogWorkIndex + getBlogMenuCountdata)',
186
+ ' blog note 草稿/工作笔记',
187
+ ' --date 日期 YYYY-MM-DD(默认今天)',
188
+ ' blog user-info 当前用户信息(id / 姓名 / 部门 / 岗位)',
189
+ ' blog reminders 消息提醒列表',
190
+ ' blog apps 可用应用列表(心情、文档、日程、流程等)',
191
+ ' blog at-list @ 提及用户列表',
192
+ ' --name 姓名关键字过滤 / --page 页码,默认 1',
193
+ ' blog visitors 访客列表',
194
+ ' --type visit=访问我的 / access=我访问的(默认 visit)',
195
+ ' --page 页码,默认 1',
196
+ ' blog search-fields 搜索可用字段(列出 getBlogPageSearchCondition 返回的字段名/标签/类型)',
197
+ ' blog template 默认模板(如果有配置)',
198
+ ' create list 列可建流程(--keyword/--tab/--json/--raw)',
199
+ ' create open 生成发起/已有请求表单 URL(--id/--name 新建,或 --requestid 已有)',
200
+ ' create fields 列表单字段(可编辑/只读/隐藏/控件类型)',
201
+ ' create start 发起流程:无 --values 列待填字段(含明细表;多选浏览框自动列前10选项);',
202
+ ' --values/--values-file 给值(key=字段id,值=文本/option id;中文用 --values-file);',
203
+ ' --detail-file 给明细表行数据(JSON 数组 [{index, rows:[{fieldId:value}]}],index=明细表序号0起,每行多字段);',
204
+ ' --file <字段id>=<本地路径> 上传图片/附件(逗号分隔多个);默认 dry-run,--yes 真发',
205
+ ' email list 收件箱邮件列表(单步 GET /api/email/list/allList)',
206
+ ' --page 页码,默认 1',
207
+ ' --subject 主题关键字过滤',
208
+ ' --from 发件人过滤',
209
+ ' --to 收件人过滤',
210
+ ' --date 语义化日期:today/yesterday/前天/大前天/this-week/last-week/this-month/last-month/recent-7/recent-30',
211
+ ' + 通用 前N天/N天前(如 前天=前2天、5天前);中文别名 今天/昨天/前天/本周/上周/本月/上月/最近7天/最近30天',
212
+ ' 给定即强制 datetype=6 + 算出的 startdate/enddate,覆盖 --datetype/--startdate/--enddate',
213
+ ' --datetype 日期范围过滤(已知 5=预设范围、6=自定义区间;不传=不过滤)',
214
+ ' --startdate 自定义区间起 YYYY-MM-DD(配 --datetype 6)',
215
+ ' --enddate 自定义区间止 YYYY-MM-DD(配 --datetype 6)',
216
+ ' --folder 文件夹:inbox/收件箱(默认)、sent/已发送,或数字 folderid(自定义)',
217
+ ' --json/--raw 同 todo(--raw 首次跑必用,校准解析字段)',
218
+ ' email view 查看邮件详情(mailView 元信息 + mailContentView 正文两步)',
219
+ ' --id 邮件 id(从 email list 取)',
220
+ ' --mark-read 标记已读(写操作,默认只读不标)',
221
+ ' email send 写信+发送(POST /api/email/base/send,默认 dry-run)',
222
+ ' --to 外部收件人邮箱(逗号分隔,如 a@x.com,b@y.com)',
223
+ ' --cc/--bcc 外部抄送/密送',
224
+ ' --internal-to 内部收件人资源 id(逗号分隔;内部走 internaltonew,外部走 to)',
225
+ ' --internal-cc/--internal-bcc 内部抄送/密送',
226
+ ' --subject 主题(必填)',
227
+ ' --content 正文(纯文本自动按行包 <p>;中文建议用 --content-file 避免乱码)',
228
+ ' --content-file 正文来自文件(避免命令行中文/转义问题)',
229
+ ' --html 正文已是 HTML(默认按纯文本包 <p>)',
230
+ ' --priority 0=普通(默认)/3=紧急(实跑观察值)',
231
+ ' --need-receipt 要求回执',
232
+ ' --timing 定时发送时间(留空=立即发)',
233
+ ' --classification / --hrm-accountid 覆盖账号配置(默认取自 getEmailAllConfig)',
234
+ ' --mailid 草稿再发模式:传已有草稿/邮件 id,则 mailid=oldmailid=id + flag=4(编辑后重发)',
235
+ ' --attachment 本地附件路径(逗号分隔多个;先 uploadFile 拿 fileid → accids)',
236
+ ' --yes 真正发送(不加则只打印请求体)',
237
+ ' email read/star/delete/move/waitdeal/recall 邮件操作(写操作 delete/move/waitdeal/recall 默认 dry-run,--yes 真执行)',
238
+ ' --id 邮件 id(逗号分隔多个;email list 取)',
239
+ ' --unread 标为未读(默认已读);--unstar 取消标星(默认标星)',
240
+ ' --permanent delete 时彻底删除(默认移回收站)',
241
+ ' --folder move 目标文件夹:0=收件箱 -1=已发送 -2=草稿箱 -3=已删除 或自定义 folderid',
242
+ ' --time waitdeal 处理日期 YYYY-MM-DD(必填)',
243
+ ' --way waitdeal 提醒方式:5=消息中心提醒(默认)/1=短信提醒',
244
+ ' --remind-time / --note / --has-remind / --has-note waitdeal 提醒时间/备注',
245
+ ' email label 标签(getMailLabel→table/datas)',
246
+ ' email label list 标签列表(名称/颜色/未读/总数)',
247
+ ' email label add --name <名> [--color #hex] [--yes] 新建标签(返回标签id)',
248
+ ' email label apply --id <ids> --label <标签id> [--remove] [--yes] 给邮件贴/去标签',
249
+ ' email attachment 附件(uploadFile/attachment/list→table/datas)',
250
+ ' email attachment upload --file <路径> [--name <名>] 上传附件,返回 fileid(send --attachment 内部复用)',
251
+ ' email attachment list [--filename <关键词>] [--subject <关键词>] 附件列表',
252
+ ' email size 邮箱容量统计;email accounts 接收账户列表',
253
+ ' workplan list 日程列表(POST /api/workplan/calendar/getCalendarData,支持组织视图)',
254
+ ' --date 参考日期 YYYY-MM-DD,默认今天',
255
+ ' --view-type 视图:1=当天 / 3=月视图(默认)',
256
+ ' --group 组织视图:部门/分部/上级/下级/常用组/关注;不传=默认当前用户',
257
+ ' --type 日程类型 id 筛选,留空=全部',
258
+ ' --json/--raw 同 todo',
259
+ ' workplan my-calendar 日程月视图(POST /api/workplan/calendar/MyCalendar,浏览器主用)',
260
+ ' --date/--view-type/--select-user 同 list',
261
+ ' --json/--raw 同 todo',
262
+ ' workplan view 查看日程详情(POST /api/workplan/base/view)',
263
+ ' --id 日程 id(必填)',
264
+ ' workplan finish 完成日程(POST /api/workplan/base/finish,默认 dry-run)',
265
+ ' --id 日程 id(必填)',
266
+ ' --yes 真正完成(不加则只打印目标)',
267
+ ' workplan delete 删除日程(POST /api/workplan/base/delete,默认 dry-run)',
268
+ ' --id 日程 id(必填)',
269
+ ' --yes 真正删除(不加则只打印目标)',
270
+ ' workplan types 列出日程类型(POST /api/workplan/type/getSelectType)',
271
+ ' workplan search-conditions 搜索可用条件(POST /api/workplan/search/getCondition)',
272
+ ' workplan attention 关注的日程(POST /api/workplan/calendar/getAttentionData)',
273
+ ' workplan base-data 日历初始化数据(POST /api/workplan/calendar/getBaseData)',
274
+ ' workplan search 日程表搜索(POST getlist → ec/dev/table/datas → counts,同待办 table 链路)',
275
+ ' --name 名称关键字',
276
+ ' --group 分组:全部/今天/本周/本月/本季/本年',
277
+ ' --type 日程类型 id / --status 状态 id / --creater 创建人 id / --receiver 接收人 id',
278
+ ' --begin/--end 日期区间 YYYY-MM-DD / --time-sag 时间范围标志(0=全部)',
279
+ ' --overtime 是否加班 / --creater-dep 创建人部门 / --creater-sub 创建人分部',
280
+ ' --doc-ids/--request-ids/--prj-id/--crm-id 关联文档/流程/项目/客户',
281
+ ' --page 页码,默认 1 / --json/--raw 同 todo',
282
+ ' workplan create 新建日程(POST /api/workplan/base/add,默认 dry-run)',
283
+ ' --name 日程名称(必填)',
284
+ ' --begin 开始时间 "YYYY-MM-DD HH:mm"(必填)',
285
+ ' --end 结束时间 "YYYY-MM-DD HH:mm"(必填)',
286
+ ' --description 描述(明文,不转 HTML,对齐抓包;中文建议用 --description-file)',
287
+ ' --description-file 描述来自文件(避免命令行中文/转义问题)',
288
+ ' --members 成员人员 id,逗号分隔(默认当前用户)',
289
+ ' --urgent 0=普通(默认)/1=紧急',
290
+ ' --type 日程类型,默认 0',
291
+ ' --secret 密级,默认 4(公开)',
292
+ ' --yes 真正新建(不加则只打印请求体)',
293
+ ' odoc todo 公文待办列表(POST getCustomPageList → ec/dev/table/datas → counts,同待办 table 链路)',
294
+ ' --subject 公文标题关键字(requestname)',
295
+ ' --dispatch-num 发文字号(dispatchnum)',
296
+ ' --type 公文种类 id / --topic 主题分类 id / --urgent 紧急程度 id / --secret 密级 id',
297
+ ' --send-unit 发文单位 / --receive-unit 收文单位 / --issued-userid 签发人 id',
298
+ ' --owner-id 拟稿/操作人 id / --dealing-userid 在办人 id / --undealed-userid 未办人 id',
299
+ ' --department 部门 id / --read-status 已读状态 / --doc-abstract 正文摘要',
300
+ ' --receive-num 来文文号 / --workflow-id 流程 id / --odoc-subject 主题词',
301
+ ' --btn-type 1=待办(默认) / --custom-list-id 列表 id(默认 -12)/ --custom-tab-id tab id(默认 -24)',
302
+ ' --page 页码,默认 1 / --json/--raw 同 todo',
303
+ ' odoc count 公文待办/待阅计数(POST /api/odoc/odocTodoList/getTodoCount)',
304
+ ' --view 待阅计数(不加=待办计数),返回发文/收文/签报',
305
+ ' odoc create-list 发文/收文流程列表(POST /api/odoc/odocCenter/createOfficalList)',
306
+ ' --type 1=发文拟稿(默认)/ 2=收文登记;返回 wfbeans,复用 create start 发起',
307
+ ' config 打印当前加载的配置文件路径 / cookie 是否配置 / 已定义的 tab 列表',
308
+ '',
309
+ '示例:',
310
+ ' e9 todo list --json',
311
+ ' e9 todo list --tab todo --page 2 --json',
312
+ ' e9 todo list --raw # 首次跑,把输出贴回来校准解析',
313
+ ' e9 email list --json',
314
+ ' e9 email list --raw # 首次跑,把 RAW JSON 贴回来收敛字段名',
315
+ ' e9 email list --date 本周 # 语义化日期(自动转 datetype=6 + 起止)',
316
+ ' e9 workplan list --raw # 首次跑,把 RAW JSON 贴回来校准日程字段',
317
+ ' e9 workplan list --date 2026-08-18 --group 下级 # 查看直接下级日程',
318
+ ' e9 workplan list --date 2026-08-12 --view-type 1 # 看某天',
319
+ ' e9 workplan view --id 138 # 查看日程详情',
320
+ ' e9 workplan types # 列出所有日程类型',
321
+ ' e9 workplan search-conditions # 查看可用搜索条件',
322
+ ' e9 workplan create --name "开会" --begin "2026-08-11 14:00" --end "2026-08-11 15:00" --yes',
323
+ ' e9 odoc todo --json # 公文待办列表',
324
+ ' e9 odoc count # 公文待办计数(发文/收文/签报)',
325
+ ' e9 odoc create-list --type 1 # 发文流程列表',
326
+ ].join('\n');
327
+ }
328
+ /** 转义 markdown 链接文本中的特殊字符([ ] ( ) 与反斜杠)。
329
+ * 待办/公文标题可能是 E9 服务端渲染的 requestnamespan(如「[退回]【普通】【通知】人事任命通知」),
330
+ * 标题自带方括号若不做转义,`[${text}](url)` 里的 ] 会被解析器误判为链接文本闭合符,
331
+ * 导致链接只包住部分标题甚至整体失效。按 CommonMark 规范用反斜杠转义,合规解析器渲染为字面量括号。 */
332
+ function escapeLinkText(text) {
333
+ return String(text).replace(/([\[\]()\\])/g, '\\$1');
334
+ }
335
+ /** 统一拼各域 list/search 项的 markdown 超链接;无 id/无可链目标时回退纯文本。
336
+ * domain: 'todo'|'odoc'|'create'|'blog'|'email'|'cube'|'workplan'
337
+ * text: 展示文本(调用方已计算好回退值);ctx: cube 需 {modeId,formId,customid} */
338
+ function hyperlinkFor(domain, item, base, text, ctx = {}) {
339
+ const meta = linkMetaFor(domain, item, base, ctx);
340
+ return meta.url ? `[${escapeLinkText(text)}](${meta.url})` : String(text);
341
+ }
342
+
343
+ /** 文本和 JSON 共用同一链接判定,禁止上层模型自行拼 URL。 */
344
+ function linkMetaFor(domain, item, base, ctx = {}) {
345
+ const b = String(base ?? '').replace(/\/+$/, '');
346
+ let url = '';
347
+ let urlType = '';
348
+ switch (domain) {
349
+ case 'todo':
350
+ case 'odoc':
351
+ if (item?.requestId)
352
+ url = buildRequestFormUrl(b, item.requestId);
353
+ urlType = url ? 'detail' : '';
354
+ break;
355
+ case 'create':
356
+ if (item?.id)
357
+ url = buildCreateWfUrl(b, item.id);
358
+ urlType = url ? 'create-empty' : '';
359
+ break;
360
+ case 'blog':
361
+ if (item?.id)
362
+ url = buildBlogMyUrl(b);
363
+ urlType = url ? 'list-generic' : '';
364
+ break;
365
+ case 'email':
366
+ // SPA bundle 实证(index.js):点邮件实际跳 main/email/view?id=<mailid>&title=<主题>
367
+ // 路由表只有 view/inbox/sent/folder/new 等,没有 viewMail——旧地址 #/main/email/viewMail?mailid= 匹配不到路由会白屏。
368
+ if (item?.id)
369
+ url = `${b}/spa/email/static/index.html#/main/email/view?id=${item.id}&title=${encodeURIComponent(item.subject ?? '查看邮件')}`;
370
+ urlType = url ? 'detail' : '';
371
+ break;
372
+ case 'cube':
373
+ url = buildCubeCardUrl(b, { modeId: ctx.modeId, formId: ctx.formId, billid: item?.id, customid: ctx.customId ?? ctx.customid });
374
+ urlType = url ? 'detail' : '';
375
+ break;
376
+ case 'cube-search':
377
+ url = normalizeE9Url(b, ctx.menuUrl ?? item?.url) || buildCubeSearchUrl(b, item?.customid ?? item?.id) || '';
378
+ urlType = url ? (ctx.menuUrl || item?.url ? 'menu' : 'search') : '';
379
+ break;
380
+ case 'workplan':
381
+ if (item?.id)
382
+ url = `${b}/spa/workplan/static/index.html#/main/wp/myWorkPlan`;
383
+ urlType = url ? 'list-generic' : '';
384
+ break;
385
+ }
386
+ return {
387
+ url: url || null,
388
+ urlType: urlType || null,
389
+ urlUnavailableReason: url ? null : (item?.id || item?.requestId ? '缺少生成链接所需的上下文参数' : '结果没有可用 ID'),
390
+ };
391
+ }
392
+
393
+ function withLink(domain, item, base, ctx = {}) {
394
+ const id = item?.id ?? item?.requestId ?? null;
395
+ const title = item?.title ?? item?.name ?? item?.requestName ?? item?.subject ?? item?.content ?? null;
396
+ return { ...item, id, title, ...linkMetaFor(domain, item, base, ctx) };
397
+ }
398
+
399
+ async function runCreateWorkflows(args) {
400
+ const cfg = loadConfig(flagStr(args, 'config'));
401
+ const keyword = flagStr(args, 'name') ?? flagStr(args, 'keyword') ?? '';
402
+ const result = await getCreateWfList(cfg, { keyword, tab: flagStr(args, 'tab') ?? '1' });
403
+ const workflows = result.categories.flatMap((category) => category.workflows.map((workflow) => ({ ...workflow, category: category.name })));
404
+ if (args.flags.json) {
405
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
406
+ console.log(JSON.stringify({ total: workflows.length, workflows: workflows.map((item) => withLink('create', item, base)) }, null, 2));
407
+ return;
408
+ }
409
+ console.log(`可发起流程共 ${workflows.length} 个${keyword ? `,匹配“${keyword}”` : ''}:`);
410
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
411
+ workflows.slice(0, 20).forEach((workflow, index) => console.log(`${index + 1}. ${hyperlinkFor('create', workflow, base, workflow.name ?? '(无名称)')} [${workflow.id}] ${workflow.category ?? ''}`));
412
+ if (workflows.length > 20)
413
+ console.log(`还有 ${workflows.length - 20} 个,请用 --name 缩小范围。`);
414
+ }
415
+
416
+ async function runCreateForm(args) {
417
+ const cfg = loadConfig(flagStr(args, 'config'));
418
+ const workflowId = flagStr(args, 'workflow-id') ?? flagStr(args, 'id');
419
+ if (!workflowId)
420
+ throw new Error('create form 需要 --workflow-id <流程 ID>。');
421
+ const form = await getStartableFields(cfg, workflowId);
422
+ const context = createContext({ workflowId: form.workflowId ?? workflowId, workflowName: form.workflowName });
423
+ const output = { contextId: context.id, workflowId: context.workflowId, workflowName: context.workflowName, fields: form.fields, detailTables: form.detailTables };
424
+ if (args.flags.json) {
425
+ console.log(JSON.stringify(output, null, 2));
426
+ return;
427
+ }
428
+ console.log(`已初始化“${context.workflowName || context.workflowId}”表单。`);
429
+ console.log(`上下文:${context.id}`);
430
+ for (const field of form.fields) {
431
+ console.log(`- [${field.id}] ${field.label ?? '(未命名字段)'}${field.required ? '(必填)' : ''}${field.inputHint ? `:${field.inputHint}` : ''}`);
432
+ }
433
+ console.log(`\n下一步:e9 create prepare ${context.id} --data '<JSON>'`);
434
+ }
435
+
436
+ function readContextData(args) {
437
+ const raw = flagStr(args, 'data');
438
+ const file = flagStr(args, 'data-file');
439
+ let text = raw;
440
+ if (!text && file) {
441
+ text = fs.readFileSync(file, 'utf8');
442
+ }
443
+ if (!text)
444
+ throw new Error('create prepare 需要 --data <JSON> 或 --data-file <文件>。');
445
+ let data;
446
+ try {
447
+ data = JSON.parse(text);
448
+ }
449
+ catch {
450
+ throw new Error('--data 不是合法 JSON 对象。');
451
+ }
452
+ if (!data || Array.isArray(data) || typeof data !== 'object')
453
+ throw new Error('--data 必须是字段 ID 到值的 JSON 对象。');
454
+ return data;
455
+ }
456
+
457
+ async function runCreatePrepare(args) {
458
+ const id = args._[2];
459
+ if (!id)
460
+ throw new Error('create prepare 需要 <context_id>。');
461
+ const data = readContextData(args);
462
+ const context = loadContext(id);
463
+ const updated = updateContext(id, {
464
+ values: { ...context.values, ...data },
465
+ requestName: flagStr(args, 'requestname') ?? context.requestName,
466
+ });
467
+ console.log(`已保存 ${Object.keys(data).length} 个字段到上下文 ${updated.id}。`);
468
+ console.log('预览:');
469
+ for (const [field, value] of Object.entries(updated.values))
470
+ console.log(`- field${field} = ${value}`);
471
+ console.log(`\n确认后:e9 create create ${updated.id} --confirm --submit`);
472
+ }
473
+
474
+ async function runContextCreate(args) {
475
+ const id = args._[2];
476
+ if (!id)
477
+ throw new Error('create create 需要 <context_id>。');
478
+ const context = loadContext(id);
479
+ if (!args.flags.confirm || !args.flags.submit) {
480
+ const cfg = loadConfig(flagStr(args, 'config'));
481
+ const prepared = await prepareCreateSubmission(cfg, context.workflowId, context.values, context.requestName, context.details);
482
+ const plan = saveSubmissionPlan({ ...prepared, files: context.files });
483
+ updateContext(id, { planId: plan.id });
484
+ console.log(BOX_TOP);
485
+ console.log(` 发起预览 · ${context.workflowName || context.workflowId}`);
486
+ console.log(DIVIDER);
487
+ console.log(JSON.stringify(prepared.body, null, 2));
488
+ console.log(`\n确认无误后:e9 create create ${id} --confirm --submit(将使用计划 ${plan.id} 原样提交已确认字段)`);
489
+ return;
490
+ }
491
+ const cfg = loadConfig(flagStr(args, 'config'));
492
+ if (!context.planId)
493
+ throw new Error('此上下文尚未生成确认计划。请先不带 --confirm --submit 运行一次预览。');
494
+ const plan = loadSubmissionPlan(context.planId, context.workflowId);
495
+ const result = await submitCreateRequest(cfg, context.workflowId, plan.values, plan.requestName, [], plan.details, plan);
496
+ const response = result.response;
497
+ const body = response.json ?? {};
498
+ const requestId = body.requestid ?? body.data?.requestid ?? body.data?.requestId;
499
+ if (result.outcome === 'created')
500
+ console.log(`✅ 已提交“${context.workflowName || context.workflowId}”${requestId ? `,requestid=${requestId}` : ''}。`);
501
+ else
502
+ console.log(`⚠ 已收到响应,但是否创建成功尚未确认(submission_id=${result.submission.id})。请运行 create verify ${result.submission.id},不要重复提交。`);
503
+ console.log(JSON.stringify(body, null, 2));
504
+ }
505
+
506
+ async function runCreateVerify(args) {
507
+ const id = args._[2];
508
+ if (!id)
509
+ throw new Error('create verify 需要 <submission_id>。');
510
+ const submission = loadSubmission(id);
511
+ const cfg = loadConfig(flagStr(args, 'config'));
512
+ const result = await getWfList(cfg, {
513
+ category: 'mine', page: 1, keyword: submission.requestName, createTime: submission.createdAt.slice(0, 10),
514
+ });
515
+ const candidates = result.items.filter((item) => item.title === submission.requestName || item.rawTitle === submission.requestName);
516
+ if (candidates.length === 1) {
517
+ const saved = updateSubmission(id, { status: 'created', requestId: candidates[0].requestId, verifiedAt: new Date().toISOString() });
518
+ console.log(`✅ 已核验首次发起成功:requestid=${saved.requestId}。未再次提交。`);
519
+ return;
520
+ }
521
+ if (candidates.length === 0) {
522
+ updateSubmission(id, { status: 'uncertain', verifiedAt: new Date().toISOString(), candidateRequestIds: [] });
523
+ console.log('⚠ 暂未查到可唯一确认的流程。请稍后再核验;在确认前系统不会自动重试。');
524
+ return;
525
+ }
526
+ updateSubmission(id, { status: 'uncertain', verifiedAt: new Date().toISOString(), candidateRequestIds: candidates.map((item) => item.requestId) });
527
+ console.log('⚠ 查到多条同名候选,无法安全判断首次发起的是哪一条:');
528
+ candidates.forEach((item) => console.log(`- ${item.title} requestid=${item.requestId} ${item.createdate ?? ''}`));
529
+ console.log('请在 OA 中确认保留项;在确认前系统不会自动重试。');
530
+ }
531
+
532
+ async function runCreate(args) {
533
+ const sub = args._[1] ?? 'list';
534
+ if (sub === 'workflows') {
535
+ await runCreateWorkflows(args);
536
+ return;
537
+ }
538
+ if (sub === 'form') {
539
+ await runCreateForm(args);
540
+ return;
541
+ }
542
+ if (sub === 'prepare') {
543
+ await runCreatePrepare(args);
544
+ return;
545
+ }
546
+ if (sub === 'create') {
547
+ await runContextCreate(args);
548
+ return;
549
+ }
550
+ if (sub === 'verify') {
551
+ await runCreateVerify(args);
552
+ return;
553
+ }
554
+ if (sub === 'open') {
555
+ await runCreateOpen(args);
556
+ return;
557
+ }
558
+ if (sub === 'fields') {
559
+ await runCreateFields(args);
560
+ return;
561
+ }
562
+ if (sub === 'start') {
563
+ await runCreateStart(args);
564
+ return;
565
+ }
566
+ if (sub === 'flow') {
567
+ await runCreateFlow(args);
568
+ return;
569
+ }
570
+ if (sub !== 'list') {
571
+ console.error(`create 暂只支持 list / workflows / form / prepare / create / verify / open / fields / start 子命令,收到: ${sub}`);
572
+ process.exit(1);
573
+ }
574
+ const cfg = loadConfig(flagStr(args, 'config'));
575
+ const tab = flagStr(args, 'tab') ?? '1';
576
+ const keyword = flagStr(args, 'keyword') ?? '';
577
+ const r = await getCreateWfList(cfg, { tab, keyword });
578
+ if (args.flags.raw) {
579
+ console.error('===== RAW createreq/wfinfo =====');
580
+ console.error(JSON.stringify(r.raw, null, 2));
581
+ console.error('===== 解析结果 =====');
582
+ }
583
+ if (args.flags.json) {
584
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
585
+ console.log(JSON.stringify({
586
+ tab: r.tab,
587
+ tabs: r.tabs,
588
+ totalCategories: r.totalCategories,
589
+ totalWorkflows: r.totalWorkflows,
590
+ categories: r.categories.map((c) => ({
591
+ id: c.id,
592
+ name: c.name,
593
+ color: c.color,
594
+ workflows: c.workflows.map((w) => withLink('create', {
595
+ id: w.id,
596
+ name: w.name,
597
+ typeId: w.typeId,
598
+ spell: w.spell,
599
+ letter: w.letter,
600
+ pcurl: w.pcurl,
601
+ }, base)),
602
+ })),
603
+ }, null, 2));
604
+ return;
605
+ }
606
+ console.log(BOX_TOP);
607
+ console.log(` E9 新建流程 · [${r.tab.title ?? r.tab.key}]`);
608
+ console.log(` ${r.totalCategories} 个分类 / ${r.totalWorkflows} 个流程${keyword ? ` · 关键词「${keyword}」` : ''}`);
609
+ console.log(` tabs: ${r.tabs.map((t) => `${t.key}=${t.title}`).join(' | ')}`);
610
+ if (tab === '3') {
611
+ console.log(' 注:常用流程由服务端统计,本接口未按常用过滤,以下同"全部流程"。');
612
+ }
613
+ console.log(BOX_TOP);
614
+ if (r.categories.length === 0) {
615
+ console.log('\n (无数据,换个 --tab 或检查 cookie/token)');
616
+ if (args.flags.raw)
617
+ console.error('提示:用 --raw 查看真实响应校准解析。');
618
+ return;
619
+ }
620
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
621
+ let idx = 0;
622
+ for (const c of r.categories) {
623
+ console.log(`\n ■ ${c.name ?? '(未命名分类)'} (${c.workflows.length})`);
624
+ for (const w of c.workflows) {
625
+ idx++;
626
+ const num = String(idx).padStart(3, ' ');
627
+ console.log(` ${num}. ${hyperlinkFor('create', w, base, w.name ?? '(无名称)')} [id=${w.id ?? ''}]${w.spell ? ` (${w.spell})` : ''}`);
628
+ }
629
+ }
630
+ }
631
+ async function runCreateStart(args) {
632
+ const cfg = loadConfig(flagStr(args, 'config'));
633
+ const id = flagStr(args, 'id');
634
+ const name = flagStr(args, 'name');
635
+ const valuesRaw = flagStr(args, 'values');
636
+ const valuesFile = flagStr(args, 'values-file');
637
+ const detailFile = flagStr(args, 'detail-file');
638
+ const send = !!args.flags.yes;
639
+ let wfId;
640
+ if (!id && name) {
641
+ // 按名查;多个候选则列出让用户选
642
+ const hits = await findCreateWfByName(cfg, name);
643
+ if (hits.length === 0) {
644
+ console.error(`按名「${name}」未找到流程。`);
645
+ process.exit(1);
646
+ }
647
+ if (hits.length > 1) {
648
+ console.log(BOX_TOP);
649
+ console.log(` 「${name}」找到 ${hits.length} 个相似流程,请选一个:`);
650
+ console.log(DIVIDER);
651
+ for (let i = 0; i < hits.length; i++) {
652
+ const w = hits[i];
653
+ console.log(` ${String(i + 1).padStart(2, ' ')}. ${w.name} [id=${w.id ?? ''}]${w.spell ? ` (${w.spell})` : ''}`);
654
+ }
655
+ console.log(BOX_TOP);
656
+ console.log(' 用 --id <流程id> 指定其中一个再跑。');
657
+ process.exit(0);
658
+ }
659
+ wfId = hits[0].id ?? '';
660
+ }
661
+ else {
662
+ try {
663
+ wfId = await resolveWorkflowId(cfg, id, name);
664
+ }
665
+ catch (e) {
666
+ console.error(e.message);
667
+ process.exit(1);
668
+ }
669
+ }
670
+ const r = await getStartableFields(cfg, wfId);
671
+ const requestname = flagStr(args, 'requestname');
672
+ // --file <字段id>=<本地路径>(逗号分隔多个):上传图片/附件
673
+ const fileRaw = flagStr(args, 'file');
674
+ const files = [];
675
+ if (fileRaw) {
676
+ for (const seg of fileRaw.split(',')) {
677
+ const i = seg.indexOf('=');
678
+ if (i <= 0) {
679
+ console.error(`--file 格式应为 <字段id>=<路径>,收到: ${seg}`);
680
+ process.exit(1);
681
+ }
682
+ files.push({ fieldId: seg.slice(0, i).trim(), filePath: seg.slice(i + 1).trim() });
683
+ }
684
+ }
685
+ // 解析值来源:--values 或 --values-file(文件避免命令行中文/转义问题)
686
+ let valuesRawResolved = valuesRaw;
687
+ if (!valuesRawResolved && valuesFile) {
688
+ try {
689
+ const fs = await import('node:fs');
690
+ valuesRawResolved = fs.readFileSync(valuesFile, 'utf8');
691
+ }
692
+ catch (e) {
693
+ console.error(`读取 --values-file 失败:${e.message}`);
694
+ process.exit(1);
695
+ }
696
+ }
697
+ // --detail-file <json>:明细表行数据 [{index, rows:[{fieldId:value}]}](index: detail_1→0, detail_2→1)
698
+ let details = [];
699
+ if (detailFile) {
700
+ try {
701
+ const fs = await import('node:fs');
702
+ const parsed = JSON.parse(fs.readFileSync(detailFile, 'utf8'));
703
+ if (!Array.isArray(parsed))
704
+ throw new Error('detail JSON 应为数组 [{index, rows:[{...}]}]');
705
+ details = parsed.map((d) => ({
706
+ index: Number(d.index),
707
+ rows: (d.rows || []).map((rw) => {
708
+ const row = {};
709
+ for (const [k, v] of Object.entries(rw))
710
+ row[k] = String(v);
711
+ return row;
712
+ }),
713
+ }));
714
+ }
715
+ catch (e) {
716
+ console.error(`--detail-file 解析失败:${e.message}`);
717
+ process.exit(1);
718
+ }
719
+ }
720
+ // 无值来源且无 --file 且无 --detail-file:打印待填字段清单(供 AI 问答)
721
+ if (!valuesRawResolved && files.length === 0 && details.length === 0) {
722
+ if (args.flags.json) {
723
+ console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, fields: r.fields, detailTables: r.detailTables, submitParams: r.submitParams }, null, 2));
724
+ return;
725
+ }
726
+ console.log(BOX_TOP);
727
+ console.log(` E9 发起流程 · ${r.workflowName ?? 'workflowid=' + r.workflowId} (id=${r.workflowId})`);
728
+ console.log(` 主表业务字段 ${r.fields.length} 个 · 明细表 ${r.detailTables.length} 个 · verifyRight=${r.verifyRight ? '✅ 有权限' : '❌ 无权限'}`);
729
+ console.log(BOX_TOP);
730
+ console.log(' 注:viewattr 是 loadForm 静态值,不反映运行时可编辑性(va=3 隐藏实际可能可填)。填哪些由你定。');
731
+ if (r.fields.length === 0) {
732
+ console.log('\n (无主表业务字段——该流程可能是自由流程或仅系统字段)');
733
+ }
734
+ for (const f of r.fields) {
735
+ console.log(`\n ▸ [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''}`);
736
+ const meta = [f.htmltypeLabel, f.viewattrLabel ? `静态${f.viewattrLabel}` : null, f.inputHint, f.currentValue ? `当前值=${f.currentValue}` : null, f.defaultValue ? `默认=${f.defaultValue}` : null].filter(Boolean).join(' · ');
737
+ if (meta)
738
+ console.log(` ${meta}`);
739
+ if (f.options && f.options.length > 0) {
740
+ console.log(` 选项: ${f.options.map((o) => `${o.id}=${o.name}`).join(' | ')}`);
741
+ }
742
+ }
743
+ // 明细表字段(多行,用 --detail-file 给行数据)
744
+ for (const dt of r.detailTables) {
745
+ console.log(`\n ▦ 明细表 ${dt.key}(index=${dt.index},多行)`);
746
+ for (const f of dt.fields) {
747
+ const meta = [f.htmltypeLabel, f.inputHint].filter(Boolean).join(' · ');
748
+ console.log(` ▸ [${f.id}] ${f.label ?? '(无标签)'}${meta ? ` (${meta})` : ''}`);
749
+ if (f.options && f.options.length > 0) {
750
+ console.log(` 选项: ${f.options.map((o) => `${o.id}=${o.name}`).join(' | ')}`);
751
+ }
752
+ }
753
+ }
754
+ console.log('\n ─────────────────────────────────────────────');
755
+ console.log(' 给值:--values-file \'<json>\' key=字段id,值=文本/option id(中文用 --values-file 避免乱码)');
756
+ console.log(' 明细:--detail-file \'<json>\' [{index:0, rows:[{22106:"v",22107:"v2"}, ...]}, ...](index=明细表序号0起,每行多字段)');
757
+ console.log(' 上传:--file <字段id>=<本地路径> 逗号分隔多个;图片按扩展名自动判 listType');
758
+ console.log(' 发起:默认 dry-run,确认后加 --yes 真提交');
759
+ return;
760
+ }
761
+ // 有值来源或 --file:构造提交体
762
+ let values = {};
763
+ if (valuesRawResolved) {
764
+ try {
765
+ values = JSON.parse(valuesRawResolved);
766
+ }
767
+ catch {
768
+ console.error(`--values / --values-file 不是合法 JSON:${valuesRawResolved}`);
769
+ process.exit(1);
770
+ }
771
+ }
772
+ // 预览与提交必须共用同一份已解析字段,避免默认值、联动或浏览框名称在确认后改变。
773
+ if (!send || args.flags.dryRun) {
774
+ console.log(BOX_TOP);
775
+ console.log(` [dry-run] 未发送 · 流程: ${r.workflowName ?? wfId}`);
776
+ console.log(` requestname = ${requestname || `${r.workflowName ?? '流程'}-系统管理员-${todayStr()}`}`);
777
+ if (files.length > 0) {
778
+ console.log(' 待上传(--yes 时先上传拿 fileid 再提交):');
779
+ for (const fl of files)
780
+ console.log(` ▸ field${fl.fieldId} ← ${fl.filePath}`);
781
+ console.log(' 附件上传会产生新文件 ID,不能冻结到本次确认计划;请在无附件字段变更时单独确认。');
782
+ }
783
+ const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details);
784
+ const plan = saveSubmissionPlan({ ...prepared, files });
785
+ if (details.length > 0) {
786
+ console.log(BOX_TOP);
787
+ console.log(' 明细表行(--detail-file):');
788
+ for (const dt of details) {
789
+ console.log(` ▦ 明细表 index=${dt.index}(${dt.rows.length} 行):`);
790
+ dt.rows.forEach((row, i) => console.log(` 第${i}行: ${Object.entries(row).map(([k, v]) => `field${k}=${v}`).join(', ')}`));
791
+ }
792
+ }
793
+ console.log('提交体(POST /api/workflow/reqform/requestOperation):');
794
+ console.log(JSON.stringify(prepared.body, null, 2));
795
+ console.log(`\n 确认无误后:加 --yes --plan ${plan.id} 真发起。将原样提交本次确认的业务字段和明细浏览框名称;附件仅在提交时上传。`);
796
+ return;
797
+ }
798
+ const planId = flagStr(args, 'plan');
799
+ if (!planId) {
800
+ console.error('为防止确认值与实际提交值漂移,--yes 必须使用预览输出的 --plan <plan_id>。');
801
+ process.exit(1);
802
+ }
803
+ const plan = loadSubmissionPlan(planId, wfId);
804
+ // 真提交只刷新 loadForm 的短期签名参数;业务字段、明细与浏览框名称完全复用确认计划。
805
+ const submitResult = await submitCreateRequest(cfg, wfId, plan.values, plan.requestName, files, plan.details, plan);
806
+ const res = submitResult.response;
807
+ const j = res.json;
808
+ const rid = j?.requestid ?? j?.data?.requestid ?? j?.data?.resultInfo?.requestid ?? j?.data?.requestId;
809
+ // 普通流程 requestOperation 返回 SUCCESS(已流转);公文流程返回 ASYNC_SUBMIT(仅保存,需再 flowNext 流转)。
810
+ const isAsync = j?.data?.type === 'ASYNC_SUBMIT' || j?.type === 'ASYNC_SUBMIT';
811
+ const ok = submitResult.outcome === 'created';
812
+ console.log(BOX_TOP);
813
+ console.log(` 发起结果 · ${r.workflowName ?? wfId}`);
814
+ console.log(` ${ok ? '✅ 创建成功' : `⚠ 创建结果待核验(submission_id=${submitResult.submission.id}),请勿重试`} status=${res.status}${rid ? ` requestid=${rid}` : ''}`);
815
+ // 公文流程:保存成功但未流转。调 loadData 取可流转候选,展示给用户确认后再 flowNext。
816
+ if (isAsync && rid) {
817
+ try {
818
+ const nodes = await getOdocNextNodes(cfg, String(rid), wfId, r.params, r.submitParams);
819
+ if (nodes.length === 0) {
820
+ console.log(' ℹ 公文无可流转候选(流程可能已到末节点或需在浏览器处理)');
821
+ }
822
+ else {
823
+ console.log(' 公文已保存,待流转到下一节点(未自动流转)。可流转候选:');
824
+ for (const n of nodes) {
825
+ const ops = n.operators.map((o) => `${o.id}=${o.name}`).join(', ') || `(默认 ${n.defaultUserIds})`;
826
+ console.log(` · [节点 ${n.nodeid} · ${n.nodename} · link ${n.linkId}] ${ops}`);
827
+ }
828
+ console.log(` 确认流转对象后:e9 create flow --requestid ${rid} [--next-user <uid>]`);
829
+ }
830
+ }
831
+ catch (e) {
832
+ console.log(` ⚠ 取流转候选失败:${e.message}`);
833
+ }
834
+ }
835
+ console.log(BOX_TOP);
836
+ if (j)
837
+ console.log('响应:', JSON.stringify(j).slice(0, 600));
838
+ else
839
+ console.log('响应(非JSON):', (res.text ?? '').slice(0, 300));
840
+ }
841
+ // === 新增流程操作命令 ===
842
+ async function runTodoReject(args) {
843
+ const cfg = loadConfig(flagStr(args, 'config'));
844
+ const requestId = flagStr(args, 'requestid');
845
+ if (!requestId) {
846
+ console.error('todo reject 需要 --requestid <请求id>');
847
+ process.exit(1);
848
+ }
849
+ const send = !!args.flags.yes;
850
+ const rejectToNode = flagStr(args, 'reject-to-node');
851
+ const rejectToType = flagStr(args, 'reject-to-type');
852
+ const rejectNode = flagStr(args, 'reject-node');
853
+ if (!send) {
854
+ console.log(BOX_TOP);
855
+ console.log(` [dry-run] 退回流程 requestid=${requestId}`);
856
+ console.log(` 退回目标:${rejectToNode ? `节点 ${rejectToNode}` : '自动取第一个可退回节点'}${rejectToType ? ` · 类型 ${rejectToType}` : ''}`);
857
+ console.log(' 可加 --reject-to-node <nodeid> 指定退回节点;--reject-to-type <类型>;--reject-node <nodes>');
858
+ console.log(' 加 --yes 真退回');
859
+ return;
860
+ }
861
+ const remark = flagStr(args, 'remark') ?? '退回';
862
+ const res = await rejectTodo(cfg, { requestId, remark, rejectToNode, rejectToType, rejectNode });
863
+ const j = res.json;
864
+ const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';
865
+ console.log(BOX_TOP);
866
+ console.log(` ${ok ? '✅ 退回成功' : '⚠ 退回异常'} status=${res.status} requestid=${requestId}`);
867
+ if (j)
868
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
869
+ }
870
+ async function runTodoTransfer(args) {
871
+ const cfg = loadConfig(flagStr(args, 'config'));
872
+ const requestId = flagStr(args, 'requestid');
873
+ if (!requestId) {
874
+ console.error('todo transfer 需要 --requestid <请求id>');
875
+ process.exit(1);
876
+ }
877
+ const send = !!args.flags.yes;
878
+ const receiver = flagStr(args, 'receiver');
879
+ const receiverName = flagStr(args, 'receiver-name');
880
+ if (!send) {
881
+ console.log(BOX_TOP);
882
+ console.log(` [dry-run] 转办流程 requestid=${requestId}`);
883
+ console.log(` 目标人:${receiver ? `ID ${receiver}` : (receiverName ? `姓名 ${receiverName}` : '【未指定】')}`);
884
+ console.log(' 必传 --receiver <userId> 或 --receiver-name <姓名>(逗号分隔多个)');
885
+ console.log(' 加 --yes 真转办');
886
+ return;
887
+ }
888
+ const remark = flagStr(args, 'remark') ?? '';
889
+ const res = await transferTodo(cfg, { requestId, remark, receiver, receiverName });
890
+ const j = res.json;
891
+ const ok = j?.success === true;
892
+ console.log(BOX_TOP);
893
+ console.log(` ${ok ? '✅ 转办成功' : '⚠ 转办异常'} status=${res.status} requestid=${requestId}`);
894
+ if (j)
895
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
896
+ }
897
+ async function runTodoForward(args) {
898
+ const cfg = loadConfig(flagStr(args, 'config'));
899
+ const requestId = flagStr(args, 'requestid');
900
+ if (!requestId) {
901
+ console.error('todo forward 需要 --requestid <请求id>');
902
+ process.exit(1);
903
+ }
904
+ const send = !!args.flags.yes;
905
+ const receiver = flagStr(args, 'receiver');
906
+ const receiverName = flagStr(args, 'receiver-name');
907
+ if (!send) {
908
+ console.log(BOX_TOP);
909
+ console.log(` [dry-run] 转发流程 requestid=${requestId}`);
910
+ console.log(` 目标人:${receiver ? `ID ${receiver}` : (receiverName ? `姓名 ${receiverName}` : '【未指定】')}`);
911
+ console.log(' 必传 --receiver <userId> 或 --receiver-name <姓名>');
912
+ console.log(' 加 --yes 真转发');
913
+ return;
914
+ }
915
+ const remark = flagStr(args, 'remark') ?? '';
916
+ const res = await forwardTodo(cfg, { requestId, remark, receiver, receiverName });
917
+ const j = res.json;
918
+ const ok = j?.success === true;
919
+ console.log(BOX_TOP);
920
+ console.log(` ${ok ? '✅ 转发成功' : '⚠ 转发异常'} status=${res.status} requestid=${requestId}`);
921
+ if (j)
922
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
923
+ }
924
+ async function runTodoCirculate(args) {
925
+ const cfg = loadConfig(flagStr(args, 'config'));
926
+ const requestId = flagStr(args, 'requestid');
927
+ if (!requestId) {
928
+ console.error('todo circulate 需要 --requestid <请求id>');
929
+ process.exit(1);
930
+ }
931
+ const send = !!args.flags.yes;
932
+ const receiver = flagStr(args, 'receiver');
933
+ const receiverName = flagStr(args, 'receiver-name');
934
+ if (!send) {
935
+ console.log(BOX_TOP);
936
+ console.log(` [dry-run] 传阅流程 requestid=${requestId}`);
937
+ console.log(` 目标人:${receiver ? `ID ${receiver}` : (receiverName ? `姓名 ${receiverName}` : '【未指定】')}`);
938
+ console.log(' 必传 --receiver <userId> 或 --receiver-name <姓名>');
939
+ console.log(' 加 --yes 真传阅');
940
+ return;
941
+ }
942
+ const remark = flagStr(args, 'remark') ?? '';
943
+ const res = await circulateTodo(cfg, { requestId, remark, receiver, receiverName });
944
+ const j = res.json;
945
+ const ok = j?.success === true;
946
+ console.log(BOX_TOP);
947
+ console.log(` ${ok ? '✅ 传阅成功' : '⚠ 传阅异常'} status=${res.status} requestid=${requestId}`);
948
+ if (j)
949
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
950
+ }
951
+ async function runTodoAskIdea(args) {
952
+ const cfg = loadConfig(flagStr(args, 'config'));
953
+ const requestId = flagStr(args, 'requestid');
954
+ if (!requestId) {
955
+ console.error('todo ask-idea 需要 --requestid <请求id>');
956
+ process.exit(1);
957
+ }
958
+ const send = !!args.flags.yes;
959
+ const receiver = flagStr(args, 'receiver');
960
+ const receiverName = flagStr(args, 'receiver-name');
961
+ if (!send) {
962
+ console.log(BOX_TOP);
963
+ console.log(` [dry-run] 意见征询 requestid=${requestId}`);
964
+ console.log(` 征询对象:${receiver ? `ID ${receiver}` : (receiverName ? `姓名 ${receiverName}` : '【未指定】')}`);
965
+ console.log(' 必传 --receiver <userId> 或 --receiver-name <姓名>');
966
+ console.log(' 加 --yes 真征询');
967
+ return;
968
+ }
969
+ const remark = flagStr(args, 'remark') ?? '';
970
+ const res = await askIdeaTodo(cfg, { requestId, remark, receiver, receiverName });
971
+ const j = res.json;
972
+ const ok = j?.success === true;
973
+ console.log(BOX_TOP);
974
+ console.log(` ${ok ? '✅ 意见征询成功' : '⚠ 意见征询异常'} status=${res.status} requestid=${requestId}`);
975
+ if (j)
976
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
977
+ }
978
+ async function runTodoCc(args) {
979
+ const cfg = loadConfig(flagStr(args, 'config'));
980
+ const requestId = flagStr(args, 'requestid');
981
+ if (!requestId) {
982
+ console.error('todo cc 需要 --requestid <请求id>');
983
+ process.exit(1);
984
+ }
985
+ const send = !!args.flags.yes;
986
+ if (!send) {
987
+ console.log(BOX_TOP);
988
+ console.log(` [dry-run] 抄送流程 requestid=${requestId}`);
989
+ console.log(' 加 --yes 真抄送');
990
+ return;
991
+ }
992
+ const remark = flagStr(args, 'remark') ?? '抄送';
993
+ const res = await ccTodo(cfg, { requestId, remark });
994
+ const j = res.json;
995
+ const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';
996
+ console.log(BOX_TOP);
997
+ console.log(` ${ok ? '✅ 抄送成功' : '⚠ 抄送异常'} status=${res.status} requestid=${requestId}`);
998
+ if (j)
999
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
1000
+ }
1001
+ /**
1002
+ * 公文流转:对 create start 保存但未流转的公文请求,取可流转候选 → 用户确认 → flowNext 推送。
1003
+ * 链路(浏览器抓包对齐):requestOperation(ASYNC_SUBMIT) → loadData 取候选 → 确认 → flowNext。
1004
+ * 公文必须显式选定流转对象(userIds+nodeid+linkId),空 nextDirections 是假成功不真流转。
1005
+ */
1006
+ async function runCreateFlow(args) {
1007
+ const cfg = loadConfig(flagStr(args, 'config'));
1008
+ const rid = flagStr(args, 'requestid');
1009
+ const wfId = flagStr(args, 'workflowid') || flagStr(args, 'id');
1010
+ const nextUser = flagStr(args, 'next-user');
1011
+ if (!rid) {
1012
+ console.error('create flow 需要 --requestid <请求id>(来自 create start 输出)');
1013
+ process.exit(1);
1014
+ }
1015
+ if (!wfId) {
1016
+ console.error('create flow 需要 --workflowid <流程id>(与 --requestid 配套,来自 create start 输出)');
1017
+ process.exit(1);
1018
+ }
1019
+ const r = await describeFormFields(cfg, wfId);
1020
+ let nodes;
1021
+ try {
1022
+ nodes = await getOdocNextNodes(cfg, rid, wfId, r.params, r.submitParams);
1023
+ }
1024
+ catch (e) {
1025
+ console.error(`取流转候选失败:${e.message}`);
1026
+ process.exit(1);
1027
+ }
1028
+ if (nodes.length === 0) {
1029
+ console.log(`请求 ${rid} 无可流转候选(可能已流转/到末节点/非公文流程)。`);
1030
+ return;
1031
+ }
1032
+ console.log(`可流转候选(请求 ${rid} · ${r.workflowName ?? wfId}):`);
1033
+ for (const n of nodes) {
1034
+ const ops = n.operators.map((o) => `${o.id}=${o.name}`).join(', ') || `(默认 ${n.defaultUserIds})`;
1035
+ console.log(` · [节点 ${n.nodeid} · ${n.nodename} · link ${n.linkId}] ${ops}`);
1036
+ }
1037
+ const node = nodes[0];
1038
+ const userIds = nextUser ?? node.defaultUserIds;
1039
+ if (!userIds) {
1040
+ console.error('候选无默认操作人。用 --next-user <uid> 指定(uid 见上方候选列表)。');
1041
+ process.exit(1);
1042
+ }
1043
+ const opName = node.operators.find((o) => userIds.split(',').includes(o.id))?.name ?? '';
1044
+ if (!nextUser)
1045
+ console.log(`未指定 --next-user,用默认操作人 ${userIds}${opName ? '(' + opName + ')' : ''}`);
1046
+ if (!args.flags.yes) {
1047
+ console.log(`\n将把请求 ${rid} 流转到 [${node.nodename}] 给 ${userIds}。加 --yes 确认流转。`);
1048
+ return;
1049
+ }
1050
+ const flow = await odocFlowNext(cfg, rid, wfId, r.params, r.submitParams, { verifyRequiredRange: 'field-9999,field-1,' }, [{ userIds, nodeid: node.nodeid, linkId: node.linkId }]);
1051
+ console.log(BOX_TOP);
1052
+ console.log(` 公文流转 · ${r.workflowName ?? wfId} requestid=${rid}`);
1053
+ console.log(` ${flow.success ? '✅ 流转成功' : '⚠ 流转异常'} ${flow.msg}`);
1054
+ console.log(BOX_TOP);
1055
+ }
1056
+ async function runCreateFields(args) {
1057
+ const cfg = loadConfig(flagStr(args, 'config'));
1058
+ let id = flagStr(args, 'id');
1059
+ const name = flagStr(args, 'name');
1060
+ if (!id && !name) {
1061
+ console.error('create fields 需要 --id <流程id> 或 --name <流程名>');
1062
+ process.exit(1);
1063
+ }
1064
+ if (!id) {
1065
+ const hits = await findCreateWfByName(cfg, name);
1066
+ if (hits.length === 0) {
1067
+ console.error(`按名「${name}」未找到流程`);
1068
+ process.exit(1);
1069
+ }
1070
+ if (hits.length > 1) {
1071
+ console.error(`按名「${name}」找到 ${hits.length} 个候选,请指定 --id:`);
1072
+ for (const w of hits)
1073
+ console.error(` ${w.name} [id=${w.id ?? ''}]`);
1074
+ process.exit(1);
1075
+ }
1076
+ id = hits[0].id;
1077
+ }
1078
+ const r = await describeFormFields(cfg, id);
1079
+ if (args.flags.json) {
1080
+ console.log(JSON.stringify({ workflowId: r.workflowId, workflowName: r.workflowName, verifyRight: r.verifyRight, totalFields: r.totalFields, tables: r.tables }, null, 2));
1081
+ return;
1082
+ }
1083
+ console.log(`E9 表单字段 [${r.workflowName ?? 'workflowid=' + r.workflowId}] ${r.totalFields} 个字段 verifyRight=${r.verifyRight}`);
1084
+ for (const t of r.tables) {
1085
+ console.log(`\n■ ${t.key === 'main' ? '主表' : t.key}(${t.fields.length} 字段)`);
1086
+ for (const f of t.fields) {
1087
+ console.log(` [${f.id}] ${f.label ?? '(无标签)'}${f.name ? ` (${f.name})` : ''} ${f.viewattrLabel ? '静态' + f.viewattrLabel : '?'} ${f.htmltypeLabel ?? '?'}`);
1088
+ // 选择框选项(selectattr.selectitemlist)
1089
+ const items = f.raw?.selectattr?.selectitemlist;
1090
+ if (Array.isArray(items) && items.length > 0) {
1091
+ console.log(` 选项: ${items.map((it) => `${it.selectvalue}=${it.selectname}`).join(' | ')}`);
1092
+ }
1093
+ }
1094
+ }
1095
+ console.log('\n注:必填标记不在 loadForm 接口(前端 DOM 控制),本表不显示必填。');
1096
+ console.log(' 可编辑/只读/隐藏由 viewattr 给出——viewattr 是表单设计态静态值(va=1只读/2可编辑/3隐藏),不反映运行时实际可编辑性(va=3 隐藏实际可能可填,运行时权限以节点配置/联动为准)。');
1097
+ }
1098
+ async function runCreateOpen(args) {
1099
+ const cfg = loadConfig(flagStr(args, 'config'));
1100
+ const id = flagStr(args, 'id');
1101
+ const name = flagStr(args, 'name');
1102
+ const requestId = flagStr(args, 'requestid');
1103
+ // --requestid:打开已有请求表单(处理/提交待办用),不要求 --id/--name
1104
+ if (requestId) {
1105
+ const base = endpointFor(cfg).baseUrl;
1106
+ const url = buildRequestFormUrl(base, requestId, id);
1107
+ if (args.flags.json) {
1108
+ console.log(JSON.stringify({ requestid: requestId, workflowid: id, url }, null, 2));
1109
+ return;
1110
+ }
1111
+ console.log(`打开请求表单(requestid=${requestId}${id ? ` workflowid=${id}` : ''}):`);
1112
+ console.log(url);
1113
+ console.log('\n浏览器里手动填业务字段 + 点提交(自动提交被签名卡住,见 create start 说明)。');
1114
+ return;
1115
+ }
1116
+ if (!id && !name) {
1117
+ console.error('create open 需要 --id <流程id> / --name <流程名>(新建)或 --requestid <请求id>(打开已有请求)');
1118
+ process.exit(1);
1119
+ }
1120
+ let wfId;
1121
+ let matchedName;
1122
+ if (id) {
1123
+ wfId = id;
1124
+ }
1125
+ else {
1126
+ const hits = await findCreateWfByName(cfg, name);
1127
+ if (hits.length === 0) {
1128
+ console.error(`按名「${name}」未找到可建流程。先跑 e9 create list --keyword "${name}" 看候选。`);
1129
+ process.exit(1);
1130
+ }
1131
+ if (hits.length > 1) {
1132
+ console.error(`按名「${name}」找到 ${hits.length} 个候选,请指定 --id:`);
1133
+ for (const w of hits)
1134
+ console.error(` ${w.name} [id=${w.id ?? ''}]`);
1135
+ process.exit(1);
1136
+ }
1137
+ wfId = hits[0].id;
1138
+ matchedName = hits[0].name;
1139
+ }
1140
+ const base = endpointFor(cfg).baseUrl;
1141
+ const url = buildCreateWfUrl(base, wfId);
1142
+ if (args.flags.json) {
1143
+ console.log(JSON.stringify({ workflowid: wfId, name: matchedName, url, urlType: 'create-empty', urlUnavailableReason: null }, null, 2));
1144
+ return;
1145
+ }
1146
+ console.log(`打开空白新建页${matchedName ? `「${matchedName}」` : ''}(workflowid=${wfId};预览值不会自动带入):`);
1147
+ console.log(url);
1148
+ }
1149
+ async function runAuth(args) {
1150
+ const sub = args._[1] ?? 'status';
1151
+ const cfg = loadConfig(flagStr(args, 'config'));
1152
+ if (sub === 'token-login') {
1153
+ await cmdTokenLogin(cfg, args);
1154
+ return;
1155
+ }
1156
+ if (sub === 'cookie-login') {
1157
+ await cmdCookieLogin(cfg, args);
1158
+ return;
1159
+ }
1160
+ if (sub === 'status') {
1161
+ await cmdAuthStatus(cfg, args);
1162
+ return;
1163
+ }
1164
+ if (sub === 'consent') {
1165
+ const val = String(args._[2] ?? '').toLowerCase();
1166
+ if (val !== 'true' && val !== 'false') {
1167
+ console.error('用法: e9 auth consent true|false');
1168
+ console.error(' true = 同意信息经大模型处理(登录免 --consent);false = 撤销同意');
1169
+ process.exit(1);
1170
+ }
1171
+ setConfigField('llmConsent', val === 'true', flagStr(args, 'config'));
1172
+ console.log(val === 'true'
1173
+ ? '✔ 已同意隐私确认:后续 auth token-login / cookie-login 免 --consent。'
1174
+ : '⚠ 已撤销隐私同意:后续登录需重新确认(--consent)。');
1175
+ return;
1176
+ }
1177
+ console.error(`auth 暂只支持 token-login / cookie-login / status / consent,收到: ${sub}`);
1178
+ process.exit(1);
1179
+ }
1180
+ async function runBlog(args) {
1181
+ const sub = args._[1] ?? 'list';
1182
+ const cfg = loadConfig(flagStr(args, 'config'));
1183
+ const blogBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
1184
+ if (sub === 'check') {
1185
+ const date = flagStr(args, 'date') ?? todayStr();
1186
+ // check 要判定整天的正常发布记录:逐页拉取 + 重复检测终止(服务端无 total、越界页重复回放)
1187
+ const r = await getMyBlogAll(cfg, date);
1188
+ // 撤回状态按 isReCalled 判定(已抓包验证:0=正常发布,1/2=已撤回),不再用列表存在与否代替。
1189
+ const todayItems = r.items.filter((it) => (it.workdate || (it.createdate ?? '').slice(0, 10)) === date);
1190
+ const recalled = todayItems.filter((it) => isBlogRecalled(it));
1191
+ const normal = todayItems.filter((it) => !isBlogRecalled(it));
1192
+ const written = normal.length > 0;
1193
+ if (args.flags.json) {
1194
+ console.log(JSON.stringify({
1195
+ date,
1196
+ foundCount: todayItems.length,
1197
+ normalCount: normal.length,
1198
+ recalledCount: recalled.length,
1199
+ written,
1200
+ note: written ? `发现 ${normal.length} 条正常发布的微博,判定已写日报` : recalled.length > 0 ? '微博均已撤回,无正常发布记录,不能判定已写' : '列表无微博记录',
1201
+ }, null, 2));
1202
+ if (!written)
1203
+ process.exit(1);
1204
+ return;
1205
+ }
1206
+ if (written) {
1207
+ console.log(`✅ ${date} 已写日报:${normal.length} 条正常发布的微博`);
1208
+ if (recalled.length > 0)
1209
+ console.log(`⚠️ 另有 ${recalled.length} 条已撤回微博(isReCalled≠0),未计入`);
1210
+ return;
1211
+ }
1212
+ if (recalled.length > 0) {
1213
+ console.log(`⚠️ ${date} 发现 ${recalled.length} 条微博但均已撤回(isReCalled≠0),无正常发布记录,不能判定已写日报`);
1214
+ process.exit(1);
1215
+ }
1216
+ console.log(`⚠️ ${date} 列表无微博记录`);
1217
+ process.exit(1);
1218
+ }
1219
+ if (sub === 'list') {
1220
+ const scope = flagStr(args, 'scope');
1221
+ if (scope) {
1222
+ const r = await getBlogHomepageList(cfg, {
1223
+ scope,
1224
+ endDate: flagStr(args, 'date'),
1225
+ startDate: flagStr(args, 'start-date'),
1226
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
1227
+ });
1228
+ if (args.flags.json) {
1229
+ console.log(JSON.stringify({ scope: r.scope, groupId: r.groupId, startDate: r.startDate, endDate: r.endDate, page: r.page, total: r.total, count: r.items.length, items: r.items.map((it) => withLink('blog', { id: it.id, content: it.content, workdate: it.workdate, creator: it.creator, createdate: it.createdate, shareType: it.shareType, isReCalled: it.isReCalled }, blogBase)) }, null, 2));
1230
+ return;
1231
+ }
1232
+ console.log(`E9 微博 [${r.scope} ${r.startDate}~${r.endDate}] 共 ${r.total ?? '?'} 条`);
1233
+ if (r.items.length === 0) {
1234
+ console.log('(无数据)');
1235
+ return;
1236
+ }
1237
+ const visible = r.items.slice(0, 10);
1238
+ for (let i = 0; i < visible.length; i++) {
1239
+ const it = visible[i];
1240
+ console.log(`${i + 1}. [${it.workdate ?? it.createdate ?? ''}] ${it.creator ?? '(未知)'} ${hyperlinkFor('blog', it, blogBase, it.content ?? '(无内容)')}`);
1241
+ }
1242
+ if (r.items.length > visible.length)
1243
+ console.log(`还有 ${r.items.length - visible.length} 条未显示,请用 --page <页码> 继续(每页 10 条)。`);
1244
+ return;
1245
+ }
1246
+ const date = flagStr(args, 'date') ?? todayStr();
1247
+ const page = Number(flagStr(args, 'page') ?? '1') || 1;
1248
+ const r = await getMyBlogList(cfg, date, page);
1249
+ if (args.flags.raw) {
1250
+ console.error('===== RAW getMyBlogList =====');
1251
+ console.error(JSON.stringify(r.raw, null, 2));
1252
+ console.error('===== 解析结果 =====');
1253
+ }
1254
+ if (args.flags.json) {
1255
+ console.log(JSON.stringify({ endDate: r.endDate, total: r.total, count: r.items.length, items: r.items.map((it) => withLink('blog', { id: it.id, content: it.content, workdate: it.workdate, creator: it.creator, createdate: it.createdate, shareType: it.shareType, isReCalled: it.isReCalled }, blogBase)) }, null, 2));
1256
+ return;
1257
+ }
1258
+ console.log(`E9 微博 [我的微博 ${r.endDate}] 第 ${r.page} 页(每页 ${r.pageSize} 条)`);
1259
+ if (r.items.length === 0) {
1260
+ console.log('(无数据)');
1261
+ return;
1262
+ }
1263
+ for (let i = 0; i < r.items.length; i++) {
1264
+ const it = r.items[i];
1265
+ console.log(`${i + 1}. [${it.workdate ?? it.createdate ?? ''}] ${hyperlinkFor('blog', it, blogBase, it.content ?? '(无内容)')}`);
1266
+ const meta = [it.creator, it.shareType ? `shareType=${it.shareType}` : null].filter(Boolean).join(' | ');
1267
+ if (meta)
1268
+ console.log(` ${meta}`);
1269
+ }
1270
+ // getMyBlogList 无总数,只能按本页是否满一页提示可能有更多;服务端越界会重复回放,翻到重复内容即已到末尾
1271
+ if (r.items.length === r.pageSize)
1272
+ console.log(`本页 ${r.pageSize} 条,可能还有更多,用 --page ${r.page + 1} 继续(每页 ${r.pageSize} 条);若内容与上页重复则已到末尾。`);
1273
+ return;
1274
+ }
1275
+ if (sub === 'search') {
1276
+ const content = flagStr(args, 'content');
1277
+ if (!content) {
1278
+ console.error('blog search 需要 --content <关键词>');
1279
+ process.exit(1);
1280
+ }
1281
+ const r = await searchMyBlog(cfg, {
1282
+ content,
1283
+ startDate: flagStr(args, 'start-date'),
1284
+ endDate: flagStr(args, 'end-date'),
1285
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
1286
+ });
1287
+ if (args.flags.json) {
1288
+ console.log(JSON.stringify({ content: r.content, page: r.page, total: r.total, count: r.items.length, items: r.items.map((it) => withLink('blog', { id: it.id, content: it.content, workdate: it.workdate, creator: it.creator, createdate: it.createdate, shareType: it.shareType, isReCalled: it.isReCalled }, blogBase)) }, null, 2));
1289
+ return;
1290
+ }
1291
+ console.log(`E9 微博搜索 [${r.content}] 共 ${r.total ?? '?'} 条`);
1292
+ if (r.items.length === 0) {
1293
+ console.log('(无匹配)');
1294
+ return;
1295
+ }
1296
+ const visible = r.items.slice(0, 10);
1297
+ for (let i = 0; i < visible.length; i++) {
1298
+ const it = visible[i];
1299
+ console.log(`${i + 1}. [${it.workdate ?? it.createdate ?? ''}] ${it.creator ?? '(未知)'} ${hyperlinkFor('blog', it, blogBase, it.content ?? '(无内容)')}`);
1300
+ }
1301
+ if (r.items.length > visible.length)
1302
+ console.log(`还有 ${r.items.length - visible.length} 条未显示,请用 --page <页码> 继续(每页 10 条)。`);
1303
+ return;
1304
+ }
1305
+ if (sub === 'view') {
1306
+ const userId = flagStr(args, 'user-id');
1307
+ if (!userId) {
1308
+ console.error('blog view 需要 --user-id <用户id>');
1309
+ process.exit(1);
1310
+ }
1311
+ const r = await getBlogPersonalInfo(cfg, userId);
1312
+ if (args.flags.json) {
1313
+ console.log(JSON.stringify(r, null, 2));
1314
+ return;
1315
+ }
1316
+ console.log(`${BOX_TOP}`);
1317
+ console.log(`微博主页 ${r.username ?? '?'}(${r.userId})`);
1318
+ console.log(DIVIDER);
1319
+ console.log(` 部门: ${r.deptName ?? '?'}(${r.deptId ?? '?'})`);
1320
+ console.log(` 微博数: ${r.blogCount ?? '?'} | 关注: ${r.myAttentionCount ?? '?'} | 粉丝: ${r.attentionMeCount ?? '?'}`);
1321
+ console.log(` 是否下级: ${r.isLower ? '是' : '否'} | 关注状态: ${r.attentionStatus ?? '?'}`);
1322
+ console.log(`${BOX_TOP}`);
1323
+ return;
1324
+ }
1325
+ if (sub === 'remind') {
1326
+ const userId = flagStr(args, 'user-id') || flagStr(args, 'remind-id');
1327
+ if (!userId) {
1328
+ console.error('blog remind 需要 --user-id <用户id>(必填)');
1329
+ process.exit(1);
1330
+ }
1331
+ const workdate = flagStr(args, 'workdate') || todayStr();
1332
+ const send = !!args.flags.yes;
1333
+ const r = await sendRemindUnsubmit(cfg, { remindId: userId, workdate, send });
1334
+ if (!r.sent) {
1335
+ console.log(`[dry-run] 未发送。将催办 userId=${r.remindId} workdate=${r.workdate}。确认后加 --yes。`);
1336
+ return;
1337
+ }
1338
+ console.log(`[已催办] userId=${r.remindId} workdate=${r.workdate} status=${r.response?.status}`);
1339
+ const j = r.response?.json;
1340
+ const ok = j?.api_status === true || j?.status === '1' || j?.status === true;
1341
+ console.log(` ${ok ? '✅ 催办成功' : '⚠ 催办失败'}`);
1342
+ if (j)
1343
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
1344
+ return;
1345
+ }
1346
+ if (sub === 'reply') {
1347
+ const blogId = flagStr(args, 'id');
1348
+ const content = flagStr(args, 'content');
1349
+ if (!blogId || !content) {
1350
+ console.error('blog reply 需要 --id <微博id> --content <内容>');
1351
+ process.exit(1);
1352
+ }
1353
+ const send = !!args.flags.yes;
1354
+ // 被评论人 userid(bediscussantId) + workdate:手动优先,否则从微博列表按 id 查
1355
+ let discussantId = flagStr(args, 'bediscussant-id');
1356
+ let workdate = flagStr(args, 'workdate');
1357
+ if (!discussantId || !workdate) {
1358
+ const date = flagStr(args, 'date') ?? todayStr();
1359
+ // 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
1360
+ const list = await getMyBlogAll(cfg, date);
1361
+ const hit = (list.items || []).find(b => String(b.id) === String(blogId));
1362
+ if (!hit) {
1363
+ console.error(`未在微博列表(${date})找到 id=${blogId}。用 --bediscussant-id <作者userid> --workdate <YYYY-MM-DD> 手动指定,或 --date 指定微博日期。`);
1364
+ process.exit(1);
1365
+ }
1366
+ discussantId = discussantId || (hit.raw?.userid ? String(hit.raw.userid) : '') || (hit.raw?.creater ? String(hit.raw.creater) : '') || (hit.userid ? String(hit.userid) : '');
1367
+ workdate = workdate || (hit.workdate ? String(hit.workdate) : '') || (hit.raw?.workdate ? String(hit.raw.workdate) : '') || String(hit.createdate ?? '').slice(0, 10);
1368
+ }
1369
+ const r = await addBlogReply(cfg, { blogId, content, send, discussantId, workdate });
1370
+ if (!r.sent) {
1371
+ console.log(`[dry-run] 未发送。将回复微博 id=${r.blogId}(被评论人 userid=${discussantId}, workdate=${workdate})。确认后加 --yes。`);
1372
+ return;
1373
+ }
1374
+ console.log(`[已回复] blogId=${r.blogId} status=${r.response?.status}`);
1375
+ const j = r.response?.json;
1376
+ const ok = j?.status === '1' || j?.status === 1 || j?.api_status === '1';
1377
+ console.log(` ${ok ? '✅ 回复成功' : '⚠ 回复失败'}`);
1378
+ if (j)
1379
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
1380
+ return;
1381
+ }
1382
+ if (sub === 'like') {
1383
+ const blogId = flagStr(args, 'id');
1384
+ if (!blogId) {
1385
+ console.error('blog like 需要 --id <微博id>');
1386
+ process.exit(1);
1387
+ }
1388
+ const send = !!args.flags.yes;
1389
+ // 手动参数优先;否则自动从微博列表解析 workdate / 当前用户 id / 当前点赞状态。
1390
+ // SPA bundle 实证 addOrDeleteZan 请求体:workdate + userId + optType(1=赞/0=取消) + discussId(微博id)。
1391
+ let workdate = flagStr(args, 'workdate');
1392
+ let userId = flagStr(args, 'user-id');
1393
+ let optType = flagStr(args, 'opt-type');
1394
+ if (!workdate || !userId || !optType) {
1395
+ const date = flagStr(args, 'date') ?? todayStr();
1396
+ // 逐页拉取当天全部微博(重复检测终止),避免目标微博落在单页之外
1397
+ const list = await getMyBlogAll(cfg, date);
1398
+ const hit = (list.items || []).find((b) => String(b.id) === String(blogId));
1399
+ if (!hit) {
1400
+ console.error(`未在微博列表(${date})找到 id=${blogId}。用 --workdate <YYYY-MM-DD> --user-id <当前用户id> --opt-type <1|0> 手动指定,或 --date 指定微博日期。`);
1401
+ process.exit(1);
1402
+ }
1403
+ workdate = workdate || String(hit.workdate ?? '') || String(hit.createdate ?? '').slice(0, 10);
1404
+ if (!userId) {
1405
+ const me = await getCurrentUserInfo(cfg);
1406
+ userId = me.id ? String(me.id) : '';
1407
+ }
1408
+ if (!optType) {
1409
+ // 已赞判定:blogZanBean.blogZanPoList 含当前用户 zanHrmid(与前端 bundle 同款逻辑)
1410
+ const zanList = Array.isArray(hit.raw?.blogZanBean?.blogZanPoList) ? hit.raw.blogZanBean.blogZanPoList : [];
1411
+ const alreadyLiked = zanList.some((z) => String(z.zanHrmid ?? '') === String(userId));
1412
+ optType = alreadyLiked ? '0' : '1';
1413
+ }
1414
+ if (!workdate || !userId) {
1415
+ console.error('未能自动解析 workdate/userId,请用 --workdate --user-id 手动指定。');
1416
+ process.exit(1);
1417
+ }
1418
+ }
1419
+ const r = await addOrDeleteZan(cfg, { blogId, workdate, userId, optType, send });
1420
+ if (!r.sent) {
1421
+ console.log(`[dry-run] 未发送。将${optType === '1' ? '点赞' : '取消点赞'}微博 id=${r.blogId}(workdate=${workdate}, userId=${userId})。确认后加 --yes。`);
1422
+ return;
1423
+ }
1424
+ if (args.flags.json) {
1425
+ console.log(JSON.stringify({ blogId: r.blogId, optType: r.optType, workdate, userId, isZan: r.isZan, response: r.response?.json }, null, 2));
1426
+ return;
1427
+ }
1428
+ const j = r.response?.json;
1429
+ const verb = r.optType === '1' ? '点赞' : '取消点赞';
1430
+ console.log(`[${verb}] blogId=${r.blogId} status=${r.response?.status}`);
1431
+ console.log(` ${r.ok ? '✅ 操作成功' : '⚠ 操作失败'}`);
1432
+ if (j)
1433
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
1434
+ return;
1435
+ }
1436
+ if (sub === 'create') {
1437
+ let content = flagStr(args, 'content');
1438
+ const contentFile = flagStr(args, 'content-file');
1439
+ if (!content && contentFile) {
1440
+ try {
1441
+ const fs = await import('node:fs');
1442
+ content = fs.readFileSync(contentFile, 'utf8');
1443
+ }
1444
+ catch (e) {
1445
+ console.error(`读取 --content-file 失败:${e.message}`);
1446
+ process.exit(1);
1447
+ }
1448
+ }
1449
+ if (!content) {
1450
+ console.error('blog create 需要 --content "<文本>" 或 --content-file <路径>');
1451
+ process.exit(1);
1452
+ }
1453
+ const send = !!args.flags.yes;
1454
+ const r = await createBlog(cfg, { content, shareType: flagStr(args, 'share-type'), workdate: flagStr(args, 'date'), discussId: flagStr(args, 'discuss-id'), send });
1455
+ if (!r.sent) {
1456
+ console.log(`[dry-run] 未发送。模式: ${r.mode} 目标: ${r.endpoint.baseUrl}/api/blog/base/saveOrUpdateBlog`);
1457
+ console.log('请求体:');
1458
+ console.log(JSON.stringify(r.body, null, 2));
1459
+ console.log('\n确认无误后加 --yes 真正发布。');
1460
+ return;
1461
+ }
1462
+ console.log(`[已发送] 模式: ${r.mode} ${r.endpoint.baseUrl}/api/blog/base/saveOrUpdateBlog status=${r.response?.status}`);
1463
+ const j = r.response?.json;
1464
+ const ok = j?.api_status === true || j?.api_status === '1' || j?.status === '1' || j?.status === true || j?.success === true;
1465
+ console.log(` ${ok ? '✅ 发送成功' : '⚠ 发送失败'}`);
1466
+ if (j)
1467
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
1468
+ else
1469
+ console.log('响应(非JSON):', (r.response?.text ?? '').slice(0, 300));
1470
+ if (!ok)
1471
+ process.exit(1);
1472
+ return;
1473
+ }
1474
+ if (sub === 'stats') {
1475
+ const [wi, mc] = await Promise.all([getBlogWorkIndex(cfg), getBlogMenuCountdata(cfg)]);
1476
+ if (args.flags.json) {
1477
+ console.log(JSON.stringify({ workIndex: wi, menuCounts: mc }, null, 2));
1478
+ return;
1479
+ }
1480
+ console.log(`${BOX_TOP}`);
1481
+ console.log(`E9 微博 工作指数 & 心情指数`);
1482
+ console.log(DIVIDER);
1483
+ console.log(` 工作指数: ${wi.work.index ?? '?'} ${wi.work.title ?? ''}`);
1484
+ console.log(` 心情指数: ${wi.mood.index ?? '?'} ${wi.mood.title ?? ''}`);
1485
+ console.log(DIVIDER);
1486
+ console.log('统计:');
1487
+ const c = mc.counts;
1488
+ console.log(` 微博数: ${c.blogCount ?? '?'} | 待审批: ${c.requestCount ?? '?'} | 更新: ${c.updateCount ?? '?'}`);
1489
+ console.log(` 评论: ${c.commentCount ?? '?'} | 提醒: ${c.remindCount ?? '?'} | 关注: ${c.myAttentionCount ?? '?'}`);
1490
+ console.log(`${BOX_TOP}`);
1491
+ return;
1492
+ }
1493
+ if (sub === 'note') {
1494
+ const date = flagStr(args, 'date') ?? todayStr();
1495
+ // --save:写入草稿/便签(saveBlogNote)。E9 微博无独立草稿,存到工作笔记。
1496
+ if (flagBool(args, 'save')) {
1497
+ let content = flagStr(args, 'content');
1498
+ const contentFile = flagStr(args, 'content-file');
1499
+ if (!content && contentFile) {
1500
+ try {
1501
+ const fs = await import('node:fs');
1502
+ content = fs.readFileSync(contentFile, 'utf8');
1503
+ }
1504
+ catch (e) {
1505
+ console.error(`读取 --content-file 失败:${e.message}`);
1506
+ process.exit(1);
1507
+ }
1508
+ }
1509
+ if (!content) {
1510
+ console.error('blog note --save 需要 --content "<文本>" 或 --content-file <路径>');
1511
+ process.exit(1);
1512
+ }
1513
+ const send = !!args.flags.yes;
1514
+ const r = await saveBlogNote(cfg, { content, workdate: date, send });
1515
+ if (!r.sent) {
1516
+ console.log(`[dry-run] 未发送。将保存草稿到 ${r.workdate}(POST /api/blog/base/saveBlogNote)。确认后加 --yes。`);
1517
+ return;
1518
+ }
1519
+ console.log(`[已保存] 草稿 workdate=${r.workdate} status=${r.response?.status}`);
1520
+ console.log(` ${r.ok ? '✅ 保存成功' : '⚠ 保存失败'}`);
1521
+ if (r.response?.json)
1522
+ console.log('响应:', JSON.stringify(r.response.json).slice(0, 300));
1523
+ return;
1524
+ }
1525
+ const r = await getBlogNote(cfg, date);
1526
+ if (args.flags.json) {
1527
+ console.log(JSON.stringify(r, null, 2));
1528
+ return;
1529
+ }
1530
+ console.log(`E9 微博草稿 [${r.workdate ?? date}]`);
1531
+ if (r.content)
1532
+ console.log(r.content);
1533
+ else
1534
+ console.log('(无草稿)');
1535
+ return;
1536
+ }
1537
+ if (sub === 'recall') {
1538
+ // 微博撤回/恢复。E9 微博不可删除,撤回是唯一收回手段(POST /api/blog/myblog/recallblog)。
1539
+ const blogId = flagStr(args, 'id');
1540
+ if (!blogId) {
1541
+ console.error('blog recall 需要 --id <微博id>(建议同时给 --workdate)');
1542
+ process.exit(1);
1543
+ }
1544
+ const opType = flagBool(args, 'restore') ? 0 : 1; // --restore 恢复已撤回
1545
+ const send = !!args.flags.yes;
1546
+ const r = await recallBlog(cfg, { blogId, workdate: flagStr(args, 'workdate'), opType, send });
1547
+ if (!r.sent) {
1548
+ console.log(`[dry-run] 未发送。将${opType === 1 ? '撤回' : '恢复'}微博 blogId=${r.blogId}(opType=${opType})。确认后加 --yes。`);
1549
+ return;
1550
+ }
1551
+ console.log(`[已${opType === 1 ? '撤回' : '恢复'}] blogId=${r.blogId} status=${r.response?.status}`);
1552
+ console.log(` ${r.ok ? '✅ 操作成功' : '⚠ 操作失败'}`);
1553
+ if (r.response?.json)
1554
+ console.log('响应:', JSON.stringify(r.response.json).slice(0, 400));
1555
+ return;
1556
+ }
1557
+ if (sub === 'delete-reply') {
1558
+ // 删除微博评论(正文不可删,只能撤回;评论可删)。POST /api/blog/base/deleteBlogReplay。
1559
+ const replyId = flagStr(args, 'id');
1560
+ const discussId = flagStr(args, 'discuss-id');
1561
+ if (!replyId || !discussId) {
1562
+ console.error('blog delete-reply 需要 --id <评论id> --discuss-id <微博id>');
1563
+ process.exit(1);
1564
+ }
1565
+ const send = !!args.flags.yes;
1566
+ const r = await deleteBlogReply(cfg, { replyId, discussId, workdate: flagStr(args, 'workdate'), send });
1567
+ if (!r.sent) {
1568
+ console.log(`[dry-run] 未发送。将删除评论 replyId=${r.replyId}(discussId=${discussId})。确认后加 --yes。`);
1569
+ return;
1570
+ }
1571
+ console.log(`[已删除评论] replyId=${r.replyId} status=${r.response?.status}`);
1572
+ console.log(` ${r.ok ? '✅ 删除成功' : '⚠ 删除失败'}`);
1573
+ if (r.response?.json)
1574
+ console.log('响应:', JSON.stringify(r.response.json).slice(0, 300));
1575
+ return;
1576
+ }
1577
+ if (sub === 'user-info') {
1578
+ const r = await getCurrentUserInfo(cfg);
1579
+ if (args.flags.json) {
1580
+ console.log(JSON.stringify(r, null, 2));
1581
+ return;
1582
+ }
1583
+ console.log(`${BOX_TOP}`);
1584
+ console.log(`E9 微博 当前用户`);
1585
+ console.log(DIVIDER);
1586
+ console.log(` ID: ${r.id} | 姓名: ${r.lastname} | 部门: ${r.departmentName} | 岗位: ${r.jobtitle}`);
1587
+ console.log(`${BOX_TOP}`);
1588
+ return;
1589
+ }
1590
+ if (sub === 'reminders') {
1591
+ const r = await getMsgRemindList(cfg);
1592
+ if (args.flags.json) {
1593
+ console.log(JSON.stringify(r, null, 2));
1594
+ return;
1595
+ }
1596
+ console.log(`E9 微博 消息提醒 共 ${r.items.length} 条`);
1597
+ if (r.items.length === 0) {
1598
+ console.log('(无提醒)');
1599
+ return;
1600
+ }
1601
+ for (let i = 0; i < r.items.length; i++) {
1602
+ const it = r.items[i];
1603
+ console.log(`${i + 1}. [${it.type ?? ''}] ${it.content ?? ''} ${it.creator ?? ''} ${it.createDate ?? ''}`);
1604
+ }
1605
+ return;
1606
+ }
1607
+ if (sub === 'apps') {
1608
+ const r = await getBlogAppList(cfg);
1609
+ if (args.flags.json) {
1610
+ console.log(JSON.stringify(r, null, 2));
1611
+ return;
1612
+ }
1613
+ console.log('E9 微博 可用应用:');
1614
+ for (const g of r.appList) {
1615
+ console.log(` ${g.name}: ${g.items.join(', ')}`);
1616
+ }
1617
+ return;
1618
+ }
1619
+ if (sub === 'at-list') {
1620
+ const page = Number(flagStr(args, 'page') ?? '1') || 1;
1621
+ const q = (flagStr(args, 'name') ?? '').trim().toLowerCase();
1622
+ const r = await getAtAllList(cfg, { pageIndex: page, name: flagStr(args, 'name') ?? '' });
1623
+ // 人员搜索消歧排序:精确匹配(name==搜索词) > 前缀 > 拼音缩写/包含(修复#5)
1624
+ // 原接口按拼音缩写返回,精确匹配常被埋在列表中段,这里前置并标注。
1625
+ const annotated = r.items.map((it) => {
1626
+ const name = String(it.name ?? '').trim();
1627
+ const nameLow = name.toLowerCase();
1628
+ let matchType = 'other';
1629
+ if (q && nameLow === q)
1630
+ matchType = 'exact';
1631
+ else if (q && nameLow.startsWith(q))
1632
+ matchType = 'prefix';
1633
+ return { ...it, matchType, _sort: matchType === 'exact' ? 0 : matchType === 'prefix' ? 1 : 2 };
1634
+ });
1635
+ annotated.sort((a, b) => a._sort - b._sort);
1636
+ if (args.flags.json) {
1637
+ console.log(JSON.stringify({ pageIndex: r.pageIndex, pageSize: r.pageSize, noMore: r.noMore, query: q || undefined, items: annotated.map(({ _sort, ...rest }) => rest) }, null, 2));
1638
+ return;
1639
+ }
1640
+ console.log(`E9 微博 @ 提及用户列表 [第 ${r.pageIndex} 页]${r.noMore ? '' : '(还有更多)'} 共 ${annotated.length} 条${q ? ` · 搜索「${flagStr(args, 'name')}」` : ''}`);
1641
+ for (let i = 0; i < annotated.length; i++) {
1642
+ const it = annotated[i];
1643
+ const tag = it.matchType === 'exact' ? ' ★精确匹配' : it.matchType === 'prefix' ? ' ·前缀' : '';
1644
+ console.log(`${i + 1}. ${it.name} (uid=${it.uid})${tag}`);
1645
+ }
1646
+ return;
1647
+ }
1648
+ if (sub === 'visitors') {
1649
+ const recordType = flagStr(args, 'type') ?? 'visit';
1650
+ const page = Number(flagStr(args, 'page') ?? '1') || 1;
1651
+ const r = await getVisitorList(cfg, { recordType, page });
1652
+ if (args.flags.json) {
1653
+ console.log(JSON.stringify(r, null, 2));
1654
+ return;
1655
+ }
1656
+ const label = recordType === 'access' ? '我访问的' : '访问我的';
1657
+ console.log(`E9 微博 访客列表 [${label}] [第 ${r.page} 页] 共 ${r.total} 条`);
1658
+ if (r.items.length === 0) {
1659
+ console.log('(无访客)');
1660
+ return;
1661
+ }
1662
+ for (let i = 0; i < r.items.length; i++) {
1663
+ const it = r.items[i];
1664
+ console.log(`${i + 1}. ${it.visitorName || it.visitor || it.name || '(未知)'} ${it.visitDate ?? ''}`);
1665
+ }
1666
+ return;
1667
+ }
1668
+ if (sub === 'search-fields') {
1669
+ const r = await getBlogPageSearchCondition(cfg);
1670
+ if (args.flags.json) {
1671
+ console.log(JSON.stringify(r, null, 2));
1672
+ return;
1673
+ }
1674
+ console.log('E9 微博 搜索可用字段:');
1675
+ let lastGroup = '';
1676
+ for (const f of r.fields) {
1677
+ if (f.group && f.group !== lastGroup) {
1678
+ console.log(` [${f.group}]`);
1679
+ lastGroup = f.group;
1680
+ }
1681
+ console.log(` ${f.label} (${f.type}) domkey=${f.domkey}`);
1682
+ }
1683
+ return;
1684
+ }
1685
+ if (sub === 'template') {
1686
+ const r = await getDefaultTemplate(cfg);
1687
+ if (args.flags.json) {
1688
+ console.log(JSON.stringify(r, null, 2));
1689
+ return;
1690
+ }
1691
+ console.log(`E9 微博 默认模板`);
1692
+ if (r.isEmpty) {
1693
+ console.log('(未配置模板)');
1694
+ return;
1695
+ }
1696
+ console.log(JSON.stringify(r.template, null, 2));
1697
+ return;
1698
+ }
1699
+ console.error(`blog 暂只支持 list / search / view / remind / reply / like / create / check / stats / note / recall / delete-reply / user-info / reminders / apps / at-list / visitors / search-fields / template,收到: ${sub}`);
1700
+ process.exit(1);
1701
+ }
1702
+ async function runWorkplan(args) {
1703
+ const sub = args._[1] ?? 'list';
1704
+ const cfg = loadConfig(flagStr(args, 'config'));
1705
+ const wpBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
1706
+ if (sub === 'import-template') {
1707
+ const r = await downloadWorkplanImportTemplate(cfg, { outDir: flagStr(args, 'out-dir'), filename: flagStr(args, 'filename') });
1708
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
1709
+ else {
1710
+ console.log(`日程批量导入模板已下载到:${r.outPath}`);
1711
+ console.log(`必填字段:${r.requiredFields.join('、')}`);
1712
+ console.log('可选择:自己填写;把文件交给助手填写;或直接描述每行数据。填写时只允许改数据单元格。');
1713
+ }
1714
+ return;
1715
+ }
1716
+ if (sub === 'import') {
1717
+ const file = flagStr(args, 'file');
1718
+ if (!file) throw new Error('workplan import 需要 --file <已填写模板路径>。');
1719
+ const r = await importWorkplanExcel(cfg, { file, yes: !!args.flags.yes, secretLevel: flagStr(args, 'secret-level') });
1720
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
1721
+ else if (!r.imported && r.requiresConfirmation) console.log(`将导入日程文件:${r.filePath}\n确认文件内容无误后加 --yes。`);
1722
+ else console.log(`${r.imported ? '✅ 日程导入成功' : '⚠ 日程导入失败'}${r.message ? `:${r.message}` : ''}`);
1723
+ if (args.flags.yes && !r.imported) process.exitCode = 1;
1724
+ return;
1725
+ }
1726
+ if (sub === 'list') {
1727
+ // 使用 getCalendarData 替代 AllCalendar,支持组织视图
1728
+ const group = flagStr(args, 'group');
1729
+ // 先获取 baseData 拿组织信息
1730
+ const bd = await getBaseData(cfg);
1731
+ let objType = '1', objIds = bd.dept[0]?.id ?? '';
1732
+ if (group) {
1733
+ const groupMap = { '部门': '1', '分部': '2', '人力': '3', '上级': '4', '下级': '5', '常用组': '6', '关注': '7' };
1734
+ const gkey = groupMap[group] || group;
1735
+ const sg = bd.selectGroup.find((g) => g.key === gkey);
1736
+ if (sg) {
1737
+ if (gkey === '1') {
1738
+ objType = '1';
1739
+ objIds = bd.dept[0]?.id ?? '';
1740
+ }
1741
+ else if (gkey === '2') {
1742
+ objType = '2';
1743
+ objIds = bd.subComp[0]?.id ?? '';
1744
+ }
1745
+ else if (gkey === '5') {
1746
+ objType = '1';
1747
+ objIds = bd.dept[0]?.id ?? '';
1748
+ }
1749
+ }
1750
+ }
1751
+ const r = await getCalendarData(cfg, {
1752
+ selectDate: flagStr(args, 'date'),
1753
+ objType,
1754
+ objIds,
1755
+ workPlanType: flagStr(args, 'type'),
1756
+ });
1757
+ if (args.flags.raw) {
1758
+ console.error('===== RAW getCalendarData =====');
1759
+ console.error(JSON.stringify(r.raw, null, 2));
1760
+ console.error('===== 解析结果 =====');
1761
+ }
1762
+ if (args.flags.json) {
1763
+ console.log(JSON.stringify({
1764
+ selectDate: r.selectDate,
1765
+ count: r.count,
1766
+ memberids: r.memberids,
1767
+ events: r.events.map((ev) => withLink('workplan', {
1768
+ id: ev.id, name: ev.name,
1769
+ beginDate: ev.beginDate, beginTime: ev.beginTime,
1770
+ endDate: ev.endDate, endTime: ev.endTime,
1771
+ color: ev.color, workplanType: ev.workplanType,
1772
+ creater: ev.creater, description: ev.description,
1773
+ memberName: ev.memberName,
1774
+ }, wpBase)),
1775
+ }, null, 2));
1776
+ return;
1777
+ }
1778
+ const groupLabel = group ? ` · ${group}` : '';
1779
+ console.log(`E9 日程 [${r.selectDate}${groupLabel}] 共 ${r.count} 条`);
1780
+ if (r.count === 0) {
1781
+ console.log('(该区间无日程)');
1782
+ return;
1783
+ }
1784
+ for (const d of r.datas) {
1785
+ console.log(`\n ── ${d.memberName || d.memberid} ──`);
1786
+ for (let i = 0; i < d.info.length; i++) {
1787
+ const ev = d.info[i];
1788
+ const time = [ev.beginDate, ev.beginTime].filter(Boolean).join(' ') + ' ~ ' + [ev.endDate, ev.endTime].filter(Boolean).join(' ');
1789
+ console.log(` ${i + 1}. ${hyperlinkFor('workplan', ev, wpBase, ev.name ?? '(无名)')} [${time}]`);
1790
+ if (ev.description)
1791
+ console.log(` ${ev.description}`);
1792
+ }
1793
+ }
1794
+ return;
1795
+ }
1796
+ if (sub === 'view') {
1797
+ const id = flagStr(args, 'id');
1798
+ if (!id) {
1799
+ console.error('workplan view 需要 --id <日程id>');
1800
+ process.exit(1);
1801
+ }
1802
+ const r = await viewWorkplan(cfg, id);
1803
+ if (args.flags.json) {
1804
+ console.log(JSON.stringify(r, null, 2));
1805
+ return;
1806
+ }
1807
+ console.log(`${BOX_TOP}`);
1808
+ console.log(`日程详情 id=${r.id}`);
1809
+ console.log(DIVIDER);
1810
+ console.log(` 名称: ${r.name ?? '(无)'}`);
1811
+ console.log(` 时间: ${r.beginDateTime ?? '?'} ~ ${r.endDateTime ?? '?'}`);
1812
+ console.log(` 描述: ${r.description ?? '(无)'}`);
1813
+ console.log(` 类型: ${r.workPlanType ?? '?'} | 紧急: ${r.urgentLevel ?? '?'}`);
1814
+ console.log(` 成员: ${r.memberIDs ?? '(无)'}`);
1815
+ console.log(` 操作: ${(r.buttons || []).join(' / ')}`);
1816
+ console.log(`${BOX_TOP}`);
1817
+ return;
1818
+ }
1819
+ if (sub === 'finish') {
1820
+ const id = flagStr(args, 'id');
1821
+ if (!id) {
1822
+ console.error('workplan finish 需要 --id <日程id>');
1823
+ process.exit(1);
1824
+ }
1825
+ const send = !!args.flags.yes;
1826
+ const r = await finishWorkplan(cfg, id, { send });
1827
+ if (!r.sent) {
1828
+ console.log(`[dry-run] 未发送。将完成日程 workid=${r.workid}。确认后加 --yes。`);
1829
+ return;
1830
+ }
1831
+ console.log(`[已完成] workid=${r.workid} status=${r.response?.status}`);
1832
+ const j = r.response?.json;
1833
+ const ok = j?.api_status === true || j?.status === '1' || j?.status === true || j?.success === true;
1834
+ console.log(` ${ok ? '✅ 完成成功' : '⚠ 完成失败'}`);
1835
+ if (j)
1836
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
1837
+ return;
1838
+ }
1839
+ if (sub === 'delete') {
1840
+ const id = flagStr(args, 'id');
1841
+ if (!id) {
1842
+ console.error('workplan delete 需要 --id <日程id>');
1843
+ process.exit(1);
1844
+ }
1845
+ const send = !!args.flags.yes;
1846
+ const r = await deleteWorkplan(cfg, id, { send });
1847
+ if (!r.sent) {
1848
+ console.log(`[dry-run] 未发送。将删除日程 workid=${r.workid}。确认后加 --yes。⚠️ 删除不可恢复!`);
1849
+ return;
1850
+ }
1851
+ console.log(`[已删除] workid=${r.workid} status=${r.response?.status}`);
1852
+ const j = r.response?.json;
1853
+ const ok = j?.api_status === true || j?.status === '1' || j?.status === true || j?.success === true;
1854
+ console.log(` ${ok ? '✅ 删除成功' : '⚠ 删除失败'}`);
1855
+ if (j)
1856
+ console.log('响应:', JSON.stringify(j).slice(0, 300));
1857
+ return;
1858
+ }
1859
+ if (sub === 'types') {
1860
+ const r = await getSelectType(cfg);
1861
+ if (args.flags.json) {
1862
+ console.log(JSON.stringify(r, null, 2));
1863
+ return;
1864
+ }
1865
+ console.log(`E9 日程类型 共 ${r.types.length} 种`);
1866
+ for (const t of r.types) {
1867
+ console.log(` ${t.key}. ${t.name}`);
1868
+ }
1869
+ return;
1870
+ }
1871
+ if (sub === 'search-conditions') {
1872
+ const r = await getWorkplanSearchCondition(cfg);
1873
+ if (args.flags.json) {
1874
+ console.log(JSON.stringify(r, null, 2));
1875
+ return;
1876
+ }
1877
+ console.log(`${BOX_TOP}`);
1878
+ console.log('E9 日程 搜索条件');
1879
+ console.log(DIVIDER);
1880
+ console.log('分组:');
1881
+ for (const g of r.groups) {
1882
+ console.log(` ${g.id} ${g.title}`);
1883
+ }
1884
+ console.log('\n搜索字段:');
1885
+ let lastGroup = '';
1886
+ for (const f of r.fields) {
1887
+ if (f.group && f.group !== lastGroup) {
1888
+ console.log(` [${f.group}]`);
1889
+ lastGroup = f.group;
1890
+ }
1891
+ const opts = f.options.length > 0 ? ` 选项: ${f.options.map((o) => `${o.key}=${o.name}`).join(', ')}` : '';
1892
+ console.log(` ${f.label} (${f.type}) domkey=${f.domkey}${opts}`);
1893
+ }
1894
+ console.log(`${BOX_TOP}`);
1895
+ return;
1896
+ }
1897
+ if (sub === 'search') {
1898
+ // 支持 --group 分组搜索
1899
+ let groupFilter = flagStr(args, 'group');
1900
+ if (groupFilter) {
1901
+ try {
1902
+ const cond = await getWorkplanSearchCondition(cfg);
1903
+ const g = cond.groups.find((g) => g.id === groupFilter || g.title === groupFilter);
1904
+ if (g)
1905
+ groupFilter = String(g.viewcondition);
1906
+ }
1907
+ catch { /* 获取分组失败不阻断 */ }
1908
+ }
1909
+ const r = await searchWorkplan(cfg, {
1910
+ name: flagStr(args, 'name'),
1911
+ type: flagStr(args, 'type'),
1912
+ status: flagStr(args, 'status'),
1913
+ createrId: flagStr(args, 'creater'),
1914
+ receiveId: flagStr(args, 'receiver'),
1915
+ timeSag: groupFilter ?? flagStr(args, 'time-sag') ?? '0',
1916
+ beginDate: flagStr(args, 'begin'),
1917
+ endDate: flagStr(args, 'end'),
1918
+ isOvertime: flagStr(args, 'overtime'),
1919
+ createrDep: flagStr(args, 'creater-dep'),
1920
+ createrSub: flagStr(args, 'creater-sub'),
1921
+ docids: flagStr(args, 'doc-ids'),
1922
+ requestids: flagStr(args, 'request-ids'),
1923
+ prjids: flagStr(args, 'prj-id'),
1924
+ crmids: flagStr(args, 'crm-id'),
1925
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
1926
+ });
1927
+ if (args.flags.raw) {
1928
+ console.error('===== RAW getlist =====');
1929
+ console.error(JSON.stringify(r.raw.getlist, null, 2));
1930
+ console.error('===== RAW datas =====');
1931
+ console.error(JSON.stringify(r.raw.datas, null, 2));
1932
+ console.error('===== RAW counts =====');
1933
+ console.error(JSON.stringify(r.raw.counts, null, 2));
1934
+ console.error('===== 解析结果 =====');
1935
+ }
1936
+ if (args.flags.json) {
1937
+ console.log(JSON.stringify({
1938
+ dataKey: r.dataKey, page: r.page, total: r.total, pageSize: r.pageSize, count: r.items.length,
1939
+ items: r.items.map((it) => withLink('workplan', {
1940
+ id: it.id, name: it.name, type: it.typeId, typeName: it.typeName,
1941
+ urgent: it.urgentLevel, urgentName: it.urgentName,
1942
+ creater: it.creater, createrId: it.createrId,
1943
+ status: it.status, statusName: it.statusName,
1944
+ begin: it.beginDate, end: it.endDate,
1945
+ }, wpBase)),
1946
+ }, null, 2));
1947
+ return;
1948
+ }
1949
+ console.log(`E9 日程搜索 [第 ${r.page} 页${r.total != null ? ` / 共 ${r.total}` : ''}] 当前 ${r.items.length} 条`);
1950
+ if (r.items.length === 0) {
1951
+ console.log('(无匹配日程)');
1952
+ return;
1953
+ }
1954
+ const visible = r.items.slice(0, 10);
1955
+ for (let i = 0; i < visible.length; i++) {
1956
+ const it = visible[i];
1957
+ const time = [it.beginDate, it.endDate].filter(Boolean).join(' ~ ');
1958
+ console.log(`${i + 1}. ${hyperlinkFor('workplan', it, wpBase, it.name ?? '(无名)')}${time ? ` [${time}]` : ''}`);
1959
+ const meta = [it.typeName, it.urgentName, it.statusName, it.creater].filter(Boolean).join(' | ');
1960
+ if (meta)
1961
+ console.log(` ${meta}`);
1962
+ }
1963
+ if (r.items.length > visible.length)
1964
+ console.log(`还有 ${r.items.length - visible.length} 条未显示,请用 --page <页码> 继续(每页 10 条)。`);
1965
+ return;
1966
+ }
1967
+ if (sub === 'create') {
1968
+ const planName = flagStr(args, 'name');
1969
+ const beginDateTime = flagStr(args, 'begin');
1970
+ const endDateTime = flagStr(args, 'end');
1971
+ if (!planName || !beginDateTime || !endDateTime) {
1972
+ console.error('workplan create 需要 --name <名称> --begin "YYYY-MM-DD HH:mm" --end "YYYY-MM-DD HH:mm"');
1973
+ process.exit(1);
1974
+ }
1975
+ let description = flagStr(args, 'description');
1976
+ const descriptionFile = flagStr(args, 'description-file');
1977
+ if (!description && descriptionFile) {
1978
+ try {
1979
+ const fs = await import('node:fs');
1980
+ description = fs.readFileSync(descriptionFile, 'utf8');
1981
+ }
1982
+ catch (e) {
1983
+ console.error(`读取 --description-file 失败:${e.message}`);
1984
+ process.exit(1);
1985
+ }
1986
+ }
1987
+ const send = !!args.flags.yes;
1988
+ const r = await createWorkplan(cfg, { planName, beginDateTime, endDateTime, workPlanType: flagStr(args, 'type'), secretLevel: flagStr(args, 'secret'), urgentLevel: flagStr(args, 'urgent'), description, memberIDs: flagStr(args, 'members'), docIDs: flagStr(args, 'doc-ids'), requestIDs: flagStr(args, 'request-ids'), send });
1989
+ if (!r.sent) {
1990
+ console.log(`[dry-run] 未发送。目标: ${r.endpoint.baseUrl}/api/workplan/base/add`);
1991
+ console.log('请求体:');
1992
+ console.log(JSON.stringify(r.body, null, 2));
1993
+ console.log('\n确认无误后加 --yes 真正新建。');
1994
+ return;
1995
+ }
1996
+ console.log(`[已新建] ${r.endpoint.baseUrl}/api/workplan/base/add status=${r.response?.status}`);
1997
+ const j = r.response?.json;
1998
+ const ok = j?.api_status === true || j?.api_status === '1' || j?.status === '1' || j?.status === true || j?.success === true;
1999
+ console.log(` ${ok ? '✅ 新建成功' : '⚠ 新建失败(api_status 未通过)'}`);
2000
+ if (j)
2001
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
2002
+ else
2003
+ console.log('响应(非JSON):', (r.response?.text ?? '').slice(0, 300));
2004
+ if (!ok)
2005
+ process.exit(1);
2006
+ return;
2007
+ }
2008
+ if (sub === 'my-calendar') {
2009
+ const r = await getMyCalendar(cfg, { selectDate: flagStr(args, 'date'), viewType: flagStr(args, 'view-type'), selectUser: flagStr(args, 'select-user'), workPlanType: flagStr(args, 'type') });
2010
+ if (args.flags.json) {
2011
+ console.log(JSON.stringify(r, null, 2));
2012
+ return;
2013
+ }
2014
+ console.log(`E9 日程 MyCalendar [${r.selectDate}] viewType=${r.viewType} ${r.range.start} ~ ${r.range.end} 共 ${r.items.length} 条`);
2015
+ if (r.items.length === 0) {
2016
+ console.log('(无日程)');
2017
+ return;
2018
+ }
2019
+ for (let i = 0; i < r.items.length; i++) {
2020
+ const it = r.items[i];
2021
+ const time = [it.beginDateTime, it.endDateTime].filter(Boolean).join(' ~ ');
2022
+ const allDay = it.allDay === '1' ? ' [全天]' : '';
2023
+ console.log(`${i + 1}. ${hyperlinkFor('workplan', it, wpBase, it.name ?? '(无名)')}${allDay}${time ? ` [${time}]` : ''} ${it.typeName ?? ''}`);
2024
+ }
2025
+ return;
2026
+ }
2027
+ if (sub === 'attention') {
2028
+ const r = await getAttentionData(cfg);
2029
+ if (args.flags.json) {
2030
+ console.log(JSON.stringify(r, null, 2));
2031
+ return;
2032
+ }
2033
+ console.log(`E9 日程 关注的日程 totalpage=${r.totalpage} 共 ${r.items.length} 条`);
2034
+ if (r.items.length === 0) {
2035
+ console.log('(无关注数据)');
2036
+ return;
2037
+ }
2038
+ for (let i = 0; i < r.items.length; i++) {
2039
+ const it = r.items[i] || {};
2040
+ const time = [it.begindate, it.enddate].filter(Boolean).join(' ~ ');
2041
+ console.log(`${i + 1}. ${hyperlinkFor('workplan', it, wpBase, it.name || '(无名)')}${time ? ` [${time}]` : ''}`);
2042
+ }
2043
+ return;
2044
+ }
2045
+ if (sub === 'base-data') {
2046
+ const r = await getBaseData(cfg);
2047
+ if (args.flags.json) {
2048
+ console.log(JSON.stringify(r, null, 2));
2049
+ return;
2050
+ }
2051
+ console.log(`${BOX_TOP}`);
2052
+ console.log('E9 日程 日历初始化数据');
2053
+ console.log(DIVIDER);
2054
+ if (r.dept.length > 0)
2055
+ console.log(` 部门: ${r.dept.map((d) => `${d.name}(${d.id})`).join(', ')}`);
2056
+ if (r.subComp.length > 0)
2057
+ console.log(` 分部: ${r.subComp.map((s) => `${s.name}(${s.id})`).join(', ')}`);
2058
+ console.log(` 工作时段: ${r.wpSetInfo.amStart ?? '?'}~${r.wpSetInfo.amEnd ?? '?'} / ${r.wpSetInfo.pmStart ?? '?'}~${r.wpSetInfo.pmEnd ?? '?'}`);
2059
+ console.log(` 时间范围: ${r.timeRange.start}~${r.timeRange.end} | 同步天数: ${r.syncDay} | 周起始: ${r.weekstartday}`);
2060
+ console.log(` 视图类型: ${r.viewType ?? '?'} | 时区偏移: ${r.zone_offset ?? '?'}`);
2061
+ console.log(` 可选组: ${r.selectGroup.map((g) => `${g.name}(${g.key})`).join(', ')}`);
2062
+ if (r.selectType.length > 0)
2063
+ console.log(` 日程类型: ${r.selectType.map((t) => `${t.name}(${t.key})`).join(', ')}`);
2064
+ if (r.calendarInit)
2065
+ console.log(` 日历区间: ${r.calendarInit.range.start} ~ ${r.calendarInit.range.end} (${r.calendarInit.events.length} 条)`);
2066
+ console.log(`${BOX_TOP}`);
2067
+ return;
2068
+ }
2069
+ console.error(`workplan 暂只支持 list / view / finish / delete / types / search-conditions / search / create / my-calendar / attention / base-data / import-template / import,收到: ${sub}`);
2070
+ process.exit(1);
2071
+ }
2072
+ async function runOdoc(args) {
2073
+ const sub = args._[1] ?? 'todo';
2074
+ const cfg = loadConfig(flagStr(args, 'config'));
2075
+ const odocBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2076
+ if (sub === 'count') {
2077
+ const r = await getOdocTodoCount(cfg, { view: flagBool(args, 'view') });
2078
+ if (args.flags.raw) {
2079
+ console.error('===== RAW getTodoCount =====');
2080
+ console.error(JSON.stringify(r.raw, null, 2));
2081
+ console.error('===== 解析结果 =====');
2082
+ }
2083
+ if (args.flags.json) {
2084
+ console.log(JSON.stringify({ view: r.view, items: r.items }, null, 2));
2085
+ return;
2086
+ }
2087
+ console.log(`E9 公文${r.view ? '待阅' : '待办'}计数`);
2088
+ if (r.items.length === 0) {
2089
+ console.log('(无数据)');
2090
+ return;
2091
+ }
2092
+ let total = 0;
2093
+ for (const it of r.items) {
2094
+ console.log(` ${it.title}(${it.officalType}): ${it.count}`);
2095
+ total += it.count;
2096
+ }
2097
+ console.log(` 合计: ${total}`);
2098
+ return;
2099
+ }
2100
+ if (sub === 'create-list') {
2101
+ const r = await listOdocCreate(cfg, { officalType: flagStr(args, 'type') ?? '1' });
2102
+ if (args.flags.raw) {
2103
+ console.error('===== RAW createOfficalList =====');
2104
+ console.error(JSON.stringify(r.raw, null, 2));
2105
+ console.error('===== 解析结果 =====');
2106
+ }
2107
+ if (args.flags.json) {
2108
+ console.log(JSON.stringify({
2109
+ officalType: r.officalType,
2110
+ toplabel: r.toplabel,
2111
+ categories: r.categories.map((c) => ({
2112
+ id: c.id,
2113
+ name: c.name,
2114
+ workflows: c.workflows.map((w) => withLink('create', { id: w.id, name: w.name, typeId: w.typeId, spell: w.spell }, odocBase)),
2115
+ })),
2116
+ }, null, 2));
2117
+ return;
2118
+ }
2119
+ console.log(`E9 公文流程列表 · ${r.toplabel ?? (r.officalType === '1' ? '发文拟稿' : '收文登记')}`);
2120
+ if (r.categories.length === 0) {
2121
+ console.log('(无数据)');
2122
+ if (args.flags.raw)
2123
+ console.error('提示:用 --raw 查看真实响应校准解析。');
2124
+ return;
2125
+ }
2126
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2127
+ let idx = 0;
2128
+ for (const c of r.categories) {
2129
+ console.log(`\n ■ ${c.name ?? '(未命名分类)'} (${c.workflows.length})`);
2130
+ for (const w of c.workflows) {
2131
+ idx++;
2132
+ console.log(` ${String(idx).padStart(3, ' ')}. ${hyperlinkFor('create', w, base, w.name ?? '(无名称)')} [id=${w.id ?? ''}]${w.spell ? ` (${w.spell})` : ''}`);
2133
+ }
2134
+ }
2135
+ console.log('\n 发起公文复用 create start:e9 create start --id <流程id> ...');
2136
+ return;
2137
+ }
2138
+ if (sub !== 'todo') {
2139
+ console.error(`odoc 暂只支持 todo / count / create-list,收到: ${sub}`);
2140
+ process.exit(1);
2141
+ }
2142
+ const r = await listOdocTodo(cfg, {
2143
+ requestName: flagStr(args, 'subject'),
2144
+ dispatchNum: flagStr(args, 'dispatch-num'),
2145
+ odocType: flagStr(args, 'type'),
2146
+ topicType: flagStr(args, 'topic'),
2147
+ urgencyDegree: flagStr(args, 'urgent'),
2148
+ secretLevel: flagStr(args, 'secret'),
2149
+ sendUnit: flagStr(args, 'send-unit'),
2150
+ receiveUnit: flagStr(args, 'receive-unit'),
2151
+ issuedUserid: flagStr(args, 'issued-userid'),
2152
+ ownerId: flagStr(args, 'owner-id'),
2153
+ dealingUserid: flagStr(args, 'dealing-userid'),
2154
+ undealedUserid: flagStr(args, 'undealed-userid'),
2155
+ departmentId: flagStr(args, 'department'),
2156
+ readStatus: flagStr(args, 'read-status'),
2157
+ odocSubject: flagStr(args, 'odoc-subject'),
2158
+ docAbstract: flagStr(args, 'doc-abstract'),
2159
+ receiveNum: flagStr(args, 'receive-num'),
2160
+ workflowId: flagStr(args, 'workflow-id'),
2161
+ btnType: flagStr(args, 'btn-type'),
2162
+ customListId: flagStr(args, 'custom-list-id'),
2163
+ customTabId: flagStr(args, 'custom-tab-id'),
2164
+ page: Number(flagStr(args, 'page') ?? '1') || 1,
2165
+ });
2166
+ if (args.flags.raw) {
2167
+ console.error('===== RAW getCustomPageList =====');
2168
+ console.error(JSON.stringify(r.raw.getCustomPageList, null, 2));
2169
+ console.error('===== RAW datas =====');
2170
+ console.error(JSON.stringify(r.raw.datas, null, 2));
2171
+ console.error('===== RAW counts =====');
2172
+ console.error(JSON.stringify(r.raw.counts, null, 2));
2173
+ console.error('===== 解析结果 =====');
2174
+ }
2175
+ if (args.flags.json) {
2176
+ console.log(JSON.stringify({
2177
+ dataKey: r.dataKey,
2178
+ page: r.page,
2179
+ total: r.total,
2180
+ pageSize: r.pageSize,
2181
+ count: r.items.length,
2182
+ columns: r.columns.map((c) => ({ dataIndex: c.dataIndex, title: c.title })),
2183
+ items: r.items.map((it) => withLink('odoc', {
2184
+ requestId: it.requestId,
2185
+ requestName: it.requestName,
2186
+ odocType: it.odocType,
2187
+ odocTypeName: it.odocTypeName,
2188
+ issuedNum: it.issuedNum,
2189
+ receiveNum: it.receiveNum,
2190
+ currentNodeId: it.currentNodeId,
2191
+ currentNode: it.currentNode,
2192
+ createrId: it.createrId,
2193
+ createDate: it.createDate,
2194
+ lastOperateDate: it.lastOperateDate,
2195
+ zbr: it.zbr,
2196
+ wbr: it.wbr,
2197
+ workflowId: it.workflowId,
2198
+ }, odocBase)),
2199
+ }, null, 2));
2200
+ return;
2201
+ }
2202
+ console.log(`E9 公文待办 [第 ${r.page} 页(每页 10 条)${r.total != null ? ` / 共 ${r.total}` : ''}] 当前 ${r.items.length} 条`);
2203
+ if (r.items.length === 0) {
2204
+ console.log('(无数据)');
2205
+ if (args.flags.raw)
2206
+ console.error('提示:用 --raw 查看真实响应校准解析。');
2207
+ return;
2208
+ }
2209
+ for (let i = 0; i < r.items.length; i++) {
2210
+ const it = r.items[i];
2211
+ const nameLinked = hyperlinkFor('odoc', it, odocBase, it.requestName ?? '(无标题)');
2212
+ console.log(`${i + 1}. ${nameLinked}`);
2213
+ const meta = [
2214
+ it.odocTypeName,
2215
+ it.issuedNum ? `发文字号=${it.issuedNum}` : null,
2216
+ it.currentNodeId,
2217
+ it.createrId ? `拟稿人=${it.createrId}` : null,
2218
+ it.createDate,
2219
+ it.lastOperateDate ? `最后修改=${it.lastOperateDate}` : null,
2220
+ ].filter(Boolean).join(' | ');
2221
+ if (meta)
2222
+ console.log(` ${meta}`);
2223
+ }
2224
+ const remaining = (r.total ?? 0) - r.page * 10;
2225
+ if (remaining > 0)
2226
+ console.log(`还有 ${remaining} 条,用 --page ${r.page + 1} 继续(每页 10 条)。`);
2227
+ }
2228
+ async function runKq(args) {
2229
+ const sub = args._[1] ?? 'status';
2230
+ const cfg = loadConfig(flagStr(args, 'config'));
2231
+ if (sub === 'status') {
2232
+ const r = await getButtons(cfg);
2233
+ if (args.flags.raw) {
2234
+ console.error('===== RAW getButtons =====');
2235
+ console.error(JSON.stringify(r.raw, null, 2));
2236
+ console.error('===== 解析结果 =====');
2237
+ }
2238
+ if (args.flags.json) {
2239
+ console.log(JSON.stringify({ date: r.date, now: r.now, groupname: r.groupname, lastname: r.lastname, timeline: r.timeline }, null, 2));
2240
+ return;
2241
+ }
2242
+ console.log(`${BOX_TOP}`);
2243
+ console.log(`E9 考勤打卡 ${r.lastname} | ${r.groupname} | 今天=${r.date} | 服务器时间=${r.now}`);
2244
+ console.log(DIVIDER);
2245
+ if (r.timeline.length === 0) {
2246
+ console.log('(今日无打卡班次)');
2247
+ console.log(BOX_TOP);
2248
+ return;
2249
+ }
2250
+ for (const it of r.timeline) {
2251
+ const tag = it.type === 'on' ? '上班卡' : '下班卡';
2252
+ const done = it.signTime ? `已打 ${it.signTime}` : '未打';
2253
+ const active = it.active === '1' ? '【当前时段】' : '';
2254
+ console.log(` ${it.date} ${tag} ${active} 班次 ${it.time} | ${done}`);
2255
+ }
2256
+ console.log(BOX_TOP);
2257
+ return;
2258
+ }
2259
+ if (sub === 'punch') {
2260
+ const type = (args._[2] ?? 'off');
2261
+ if (type !== 'on' && type !== 'off') {
2262
+ console.error(`kq punch 参数须为 on(上班卡)或 off(下班卡),收到: ${type}`);
2263
+ process.exit(1);
2264
+ }
2265
+ const groupId = flagStr(args, 'group-id') ?? '1';
2266
+ const userid = flagStr(args, 'userid');
2267
+ const date = flagStr(args, 'date');
2268
+ const signTime = flagStr(args, 'sign-time');
2269
+ const yes = flagBool(args, 'yes');
2270
+ if (signTime !== undefined) {
2271
+ console.error('kq punch 不支持指定 --sign-time。该参数不是修改既有打卡时间的安全接口;请使用 OA 考勤补签/更正流程。');
2272
+ process.exit(1);
2273
+ }
2274
+ // 先 getButtons 展示目标班次 + 生成 encryptObj 给用户确认
2275
+ const buttons = await getButtons(cfg);
2276
+ let item;
2277
+ try {
2278
+ ({ item } = resolvePunchItem(buttons, { type, date }));
2279
+ }
2280
+ catch (e) {
2281
+ console.error(e.message);
2282
+ process.exit(1);
2283
+ }
2284
+ const uid = userid ?? parseUserIdFromCookie(cfg.cookie) ?? '';
2285
+ if (!uid) {
2286
+ console.error('未取到 userid:需 --userid,或 config.cookie 须含 loginuuids(token-login 写回)');
2287
+ process.exit(1);
2288
+ }
2289
+ const ts = Date.now();
2290
+ const encryptObj = buildEncryptObj(uid, groupId, ts);
2291
+ const punchSignTime = nowHHmmssCLI();
2292
+ console.log(`${BOX_TOP}`);
2293
+ console.log(`E9 考勤打卡 ${buttons.lastname} | ${buttons.groupname}`);
2294
+ console.log(DIVIDER);
2295
+ console.log(` 类型:${type === 'on' ? '上班卡' : '下班卡'} 日期:${item.date} 班次:${item.time}`);
2296
+ console.log(` 打卡时间(signTime):${punchSignTime}`);
2297
+ console.log(` userid=${uid} groupid=${groupId}`);
2298
+ console.log(` encryptObj=${encryptObj}`);
2299
+ console.log(` signSecret("${uid}${groupId}")=${signSecret(`${uid}${groupId}`)}`);
2300
+ console.log(DIVIDER);
2301
+ if (!yes) {
2302
+ console.log(' ↑ dry-run 预览(未真打)。加 --yes 真正打卡。');
2303
+ console.log(BOX_TOP);
2304
+ return;
2305
+ }
2306
+ const beforeSignTime = item.signTime;
2307
+ const r = await punch(cfg, { type, groupId: groupId, userid: uid, date, timestamp: ts });
2308
+ if (args.flags.raw) {
2309
+ console.error('===== RAW punchButton =====');
2310
+ console.error(JSON.stringify(r.raw, null, 2));
2311
+ }
2312
+ if (r.success === '1') {
2313
+ console.log(` ✅ ${r.message} ${r.signdate} ${r.signtime}`);
2314
+ }
2315
+ else {
2316
+ // 部分 E9 环境会在已经写入考勤时返回 kqstatus=2(如早退/迟到提示)。
2317
+ // 对这类非标准结果必须回读,不可直接标记失败,更不可自动重试。
2318
+ let afterItem;
2319
+ try {
2320
+ const after = await getButtons(cfg);
2321
+ ({ item: afterItem } = resolvePunchItem(after, { type, date }));
2322
+ }
2323
+ catch { /* 回读失败时保留原始不确定结果 */ }
2324
+ const wrote = !!afterItem?.signTime && afterItem.signTime !== beforeSignTime;
2325
+ if (wrote) {
2326
+ console.log(` ⚠️ 已打卡,但系统返回业务提示:${r.message || `kqstatus=${r.kqstatus}`}`);
2327
+ console.log(` 回读确认:${afterItem.date} ${type === 'on' ? '上班卡' : '下班卡'} 已记录 ${afterItem.signTime}。未自动重试。`);
2328
+ }
2329
+ else {
2330
+ console.log(` ⚠️ ${r.message || '打卡结果未确认'}(success=${r.success} status=${r.status} kqstatus=${r.kqstatus})`);
2331
+ console.log(' 未检测到新的打卡记录;为避免重复打卡,系统没有自动重试。');
2332
+ }
2333
+ }
2334
+ console.log(BOX_TOP);
2335
+ return;
2336
+ }
2337
+ console.error(`kq 暂只支持 status / punch,收到: ${sub}`);
2338
+ process.exit(1);
2339
+ }
2340
+ /** CLI 侧当前时刻 HH:mm:ss(与 kq.ts 的 nowHHmmss 同逻辑,CLI 输出用)。 */
2341
+ function nowHHmmssCLI() {
2342
+ const d = new Date();
2343
+ const p = (n) => String(n).padStart(2, '0');
2344
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
2345
+ }
2346
+ /** 从 cookie 串解析 loginuuids(与 kq.ts 同逻辑)。 */
2347
+ function parseUserIdFromCookie(cookie) {
2348
+ if (!cookie)
2349
+ return '';
2350
+ const m = cookie.match(/loginuuids=([^;]+)/);
2351
+ return m ? m[1].trim() : '';
2352
+ }
2353
+ async function runEmail(args) {
2354
+ const sub = args._[1] ?? 'list';
2355
+ if (sub === 'view') {
2356
+ await runEmailView(args);
2357
+ return;
2358
+ }
2359
+ if (sub === 'send') {
2360
+ await runEmailSend(args);
2361
+ return;
2362
+ }
2363
+ if (sub === 'read' || sub === 'star' || sub === 'delete' || sub === 'move' || sub === 'waitdeal' || sub === 'recall') {
2364
+ await runEmailOperate(args, sub);
2365
+ return;
2366
+ }
2367
+ if (sub === 'label') {
2368
+ await runEmailLabel(args);
2369
+ return;
2370
+ }
2371
+ if (sub === 'attachment') {
2372
+ await runEmailAttachment(args);
2373
+ return;
2374
+ }
2375
+ if (sub === 'size') {
2376
+ await runEmailSize(args);
2377
+ return;
2378
+ }
2379
+ if (sub === 'accounts') {
2380
+ await runEmailAccounts(args);
2381
+ return;
2382
+ }
2383
+ if (sub !== 'list') {
2384
+ console.error(`email 支持 list/view/send/read/star/delete/move/label/waitdeal/recall/attachment/size/accounts,收到: ${sub}`);
2385
+ process.exit(1);
2386
+ }
2387
+ const cfg = loadConfig(flagStr(args, 'config'));
2388
+ // --date 语义化日期:解析后强制 datetype=6 + startdate/enddate,覆盖手动日期参数
2389
+ const dateKw = flagStr(args, 'date');
2390
+ let datetype = flagStr(args, 'datetype');
2391
+ let startdate = flagStr(args, 'startdate');
2392
+ let enddate = flagStr(args, 'enddate');
2393
+ if (dateKw) {
2394
+ const range = resolveDateRange(dateKw);
2395
+ if (!range) {
2396
+ console.error(`--date "${dateKw}" 无法识别。支持: today/yesterday/前天/大前天/this-week/last-week/this-month/last-month/recent-7/recent-30\n` +
2397
+ ` + 通用 前N天/N天前(如 前天、5天前);中文别名 今天/昨天/前天/本周/上周/本月/上月/最近7天/最近30天`);
2398
+ process.exit(1);
2399
+ }
2400
+ datetype = '6';
2401
+ startdate = range.startdate;
2402
+ enddate = range.enddate;
2403
+ }
2404
+ const r = await getEmailList(cfg, {
2405
+ folder: flagStr(args, 'folder'),
2406
+ current: Number(flagStr(args, 'page') ?? '1') || 1,
2407
+ subject: flagStr(args, 'subject'),
2408
+ from: flagStr(args, 'from'),
2409
+ to: flagStr(args, 'to'),
2410
+ datetype,
2411
+ startdate,
2412
+ enddate,
2413
+ });
2414
+ const emailBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2415
+ if (args.flags.raw) {
2416
+ console.error('===== RAW getEmailList =====');
2417
+ console.error(JSON.stringify(r.raw, null, 2));
2418
+ console.error('===== 解析结果 =====');
2419
+ }
2420
+ if (args.flags.json) {
2421
+ console.log(JSON.stringify({
2422
+ folder: r.folderKey,
2423
+ folderName: r.folderName,
2424
+ folderid: r.folderid,
2425
+ page: r.current,
2426
+ total: r.total,
2427
+ count: r.items.length,
2428
+ items: r.items.map((it) => withLink('email', {
2429
+ id: it.id,
2430
+ subject: it.subject,
2431
+ from: it.from,
2432
+ to: it.to,
2433
+ fromAddress: it.fromAddress,
2434
+ date: it.date,
2435
+ read: it.read,
2436
+ starred: it.starred,
2437
+ hasAttachment: it.hasAttachment,
2438
+ }, emailBase)),
2439
+ }, null, 2));
2440
+ return;
2441
+ }
2442
+ console.log(`E9 邮件 [${r.folderName} folderid=${r.folderid}] 第 ${r.current} 页(每页 ${r.pageSize} 条) 共 ${r.total ?? '?'} 条`);
2443
+ if (r.items.length === 0) {
2444
+ console.log('(无数据)');
2445
+ if (args.flags.raw)
2446
+ console.error('提示:若无数据但页面有,用 --raw 查看真实响应校准解析。');
2447
+ return;
2448
+ }
2449
+ const visible = r.items.slice(0, r.pageSize);
2450
+ for (let i = 0; i < visible.length; i++) {
2451
+ const it = visible[i];
2452
+ const flags = [
2453
+ it.read === false ? '未读' : null,
2454
+ it.starred ? '★' : null,
2455
+ it.hasAttachment ? '附件' : null,
2456
+ ].filter(Boolean).join(' ');
2457
+ // 邮件详情 URL(E9 email SPA;格式待校准:以浏览器实际打开为准)
2458
+ const subj = it.subject ?? '(无主题)';
2459
+ const subjLinked = hyperlinkFor('email', it, emailBase, subj);
2460
+ console.log(`${i + 1}. ${subjLinked}`);
2461
+ // 收件箱显示发件人(from),已发送显示收件人(to)——各自有值才显示,自动适配
2462
+ const who = [it.from, it.to, it.fromAddress].filter(Boolean).join(' → ');
2463
+ const whoShort = who.length > 40 ? who.slice(0, 38) + '…' : who;
2464
+ const meta = [whoShort, it.date, flags].filter(Boolean).join(' | ');
2465
+ if (meta)
2466
+ console.log(` ${meta}`);
2467
+ }
2468
+ // allList 服务端固定每页条数(本环境 14),直读服务端页天然连续;
2469
+ // 还有多少条按可靠总数推算
2470
+ const remaining = (r.total ?? 0) - r.current * r.pageSize;
2471
+ if (remaining > 0)
2472
+ console.log(`还有 ${remaining} 条,用 --page ${r.current + 1} 继续(每页 ${r.pageSize} 条)。`);
2473
+ }
2474
+ async function runEmailSend(args) {
2475
+ const cfg = loadConfig(flagStr(args, 'config'));
2476
+ const subject = flagStr(args, 'subject');
2477
+ // 正文来源:--content 或 --content-file(文件避免命令行中文/转义问题,同 create start)
2478
+ let content = flagStr(args, 'content');
2479
+ const contentFile = flagStr(args, 'content-file');
2480
+ if (!content && contentFile) {
2481
+ try {
2482
+ const fs = await import('node:fs');
2483
+ content = fs.readFileSync(contentFile, 'utf8');
2484
+ }
2485
+ catch (e) {
2486
+ console.error(`读取 --content-file 失败:${e.message}`);
2487
+ process.exit(1);
2488
+ }
2489
+ }
2490
+ if (!subject) {
2491
+ console.error('email send 需要 --subject "<主题>"');
2492
+ process.exit(1);
2493
+ }
2494
+ if (!content) {
2495
+ console.error('email send 需要 --content "<正文>" 或 --content-file <路径>');
2496
+ process.exit(1);
2497
+ }
2498
+ const to = flagStr(args, 'to');
2499
+ const internalTo = flagStr(args, 'internal-to');
2500
+ if (!to && !internalTo) {
2501
+ console.error('email send 需要至少一个收件人:--to <外部邮箱> 或 --internal-to <内部人员id>');
2502
+ process.exit(1);
2503
+ }
2504
+ const send = !!args.flags.yes;
2505
+ const attachmentFiles = (flagStr(args, 'attachment') ?? '').split(/[,,]/).map((s) => s.trim()).filter(Boolean);
2506
+ const r = await sendEmail(cfg, {
2507
+ to,
2508
+ cc: flagStr(args, 'cc'),
2509
+ bcc: flagStr(args, 'bcc'),
2510
+ internalTo,
2511
+ internalCc: flagStr(args, 'internal-cc'),
2512
+ internalBcc: flagStr(args, 'internal-bcc'),
2513
+ subject,
2514
+ content,
2515
+ contentIsHtml: flagBool(args, 'html'),
2516
+ priority: flagStr(args, 'priority'),
2517
+ needReceipt: flagBool(args, 'need-receipt'),
2518
+ timingDate: flagStr(args, 'timing'),
2519
+ classification: flagStr(args, 'classification'),
2520
+ hrmAccountid: flagStr(args, 'hrm-accountid'),
2521
+ mailid: flagStr(args, 'mailid'),
2522
+ attachmentFiles,
2523
+ send,
2524
+ });
2525
+ if (args.flags.raw) {
2526
+ console.error('===== RAW getEmailAllConfig =====');
2527
+ console.error(JSON.stringify(r.accountConfig.raw, null, 2));
2528
+ if (r.response) {
2529
+ console.error('===== RAW send 响应 =====');
2530
+ console.error(JSON.stringify(r.response.json ?? r.response.text, null, 2));
2531
+ }
2532
+ console.error('===== 解析结果 =====');
2533
+ }
2534
+ if (!r.sent) {
2535
+ console.log(BOX_TOP);
2536
+ console.log(` [dry-run] 未发送 · ${r.endpoint.baseUrl}/api/email/base/send`);
2537
+ console.log(` classification=${r.accountConfig.classification} hrmAccountid=${r.accountConfig.hrmAccountid || '(空,回退1)'}`);
2538
+ console.log(DIVIDER);
2539
+ console.log('收件人:');
2540
+ if (to)
2541
+ console.log(` 外部 To: ${to}`);
2542
+ if (flagStr(args, 'cc'))
2543
+ console.log(` 外部 Cc: ${flagStr(args, 'cc')}`);
2544
+ if (flagStr(args, 'bcc'))
2545
+ console.log(` 外部 Bcc: ${flagStr(args, 'bcc')}`);
2546
+ if (internalTo)
2547
+ console.log(` 内部 To: ${internalTo}`);
2548
+ if (flagStr(args, 'internal-cc'))
2549
+ console.log(` 内部 Cc: ${flagStr(args, 'internal-cc')}`);
2550
+ if (flagStr(args, 'internal-bcc'))
2551
+ console.log(` 内部 Bcc: ${flagStr(args, 'internal-bcc')}`);
2552
+ console.log(` 主题: ${subject}`);
2553
+ if (attachmentFiles.length) {
2554
+ console.log(` 附件(${attachmentFiles.length}):`);
2555
+ for (const f of attachmentFiles)
2556
+ console.log(` ▸ ${f}`);
2557
+ }
2558
+ console.log(DIVIDER);
2559
+ console.log('请求体:');
2560
+ console.log(JSON.stringify(r.body, null, 2));
2561
+ console.log('\n 确认无误后加 --yes 真正发送。');
2562
+ console.log(BOX_TOP);
2563
+ return;
2564
+ }
2565
+ const j = r.response?.json;
2566
+ const ok = j?.status === '1' || j?.status === true || j?.success === true || r.response?.ok;
2567
+ console.log(BOX_TOP);
2568
+ console.log(` 发送结果 · ${subject}`);
2569
+ console.log(` ${ok ? '✅ 已发送' : '⚠ 提交完成但状态异常'} status=${r.response?.status}`);
2570
+ console.log(BOX_TOP);
2571
+ if (j)
2572
+ console.log('响应:', JSON.stringify(j).slice(0, 500));
2573
+ else
2574
+ console.log('响应(非JSON):', (r.response?.text ?? '').slice(0, 300));
2575
+ }
2576
+ async function runEmailView(args) {
2577
+ const cfg = loadConfig(flagStr(args, 'config'));
2578
+ const id = flagStr(args, 'id');
2579
+ if (!id) {
2580
+ console.error('email view 需要 --id <邮件id>(从 email list 取 id)');
2581
+ process.exit(1);
2582
+ }
2583
+ const r = await getEmailView(cfg, id);
2584
+ if (args.flags.raw) {
2585
+ console.error('===== RAW mailView =====');
2586
+ console.error(JSON.stringify(r.rawMailView, null, 2));
2587
+ console.error('===== RAW mailContentView =====');
2588
+ console.error(JSON.stringify(r.rawContent, null, 2));
2589
+ console.error('===== 解析结果 =====');
2590
+ }
2591
+ // 可选:标记已读(写操作,默认不执行,需 --mark-read)
2592
+ if (flagBool(args, 'mark-read')) {
2593
+ const ok = await setMailReadStatus(cfg, id, true);
2594
+ console.error(`标记已读: ${ok ? '✅' : '⚠ 状态异常'}`);
2595
+ }
2596
+ const m = r.meta;
2597
+ if (args.flags.json) {
2598
+ console.log(JSON.stringify({
2599
+ id: m.id,
2600
+ subject: m.subject,
2601
+ from: m.from,
2602
+ fromAddress: m.fromAddress,
2603
+ to: m.to,
2604
+ cc: m.cc,
2605
+ senddate: m.senddate,
2606
+ receivedate: m.receivedate,
2607
+ size: m.size,
2608
+ hasAttachment: m.hasAttachment,
2609
+ attachments: m.attachments,
2610
+ importance: m.importance,
2611
+ content: r.contentText,
2612
+ contentHtml: r.contentHtml,
2613
+ }, null, 2));
2614
+ return;
2615
+ }
2616
+ console.log(BOX_TOP);
2617
+ console.log(` E9 邮件详情 · ${m.subject ?? '(无主题)'}`);
2618
+ console.log(` id=${m.id ?? id}`);
2619
+ console.log(DIVIDER);
2620
+ const rows = [
2621
+ ['发件人', m.from ? `${m.from}${m.fromAddress ? ` <${m.fromAddress}>` : ''}` : undefined],
2622
+ ['收件人', m.to],
2623
+ ['抄送', m.cc],
2624
+ ['主题', m.subject],
2625
+ ['发送时间', m.senddate],
2626
+ ['接收时间', m.receivedate],
2627
+ ['大小', m.size],
2628
+ ['重要性', m.importance],
2629
+ ];
2630
+ for (const [k, v] of rows)
2631
+ if (v)
2632
+ console.log(` ${k}: ${v}`);
2633
+ if (m.attachments && m.attachments.length > 0) {
2634
+ console.log(DIVIDER);
2635
+ console.log(` 附件(${m.attachments.length}):`);
2636
+ for (const a of m.attachments)
2637
+ console.log(` ▸ ${a.name ?? '(无名)'}${a.size ? ` ${a.size}` : ''}${a.fileid ? ` [fileid=${a.fileid}]` : ''}`);
2638
+ }
2639
+ if (r.contentText) {
2640
+ console.log(DIVIDER);
2641
+ console.log(' 正文:');
2642
+ // 正文可能很长,人类可读截断到约 800 字符
2643
+ const text = r.contentText.length > 800 ? r.contentText.slice(0, 800) + '…(截断,--json 看全文)' : r.contentText;
2644
+ console.log(` ${text}`);
2645
+ }
2646
+ console.log(BOX_TOP);
2647
+ }
2648
+ /** email 操作子命令统一处理:read/star/delete/move/waitdeal/recall。写操作默认 dry-run,--yes 执行。 */
2649
+ async function runEmailOperate(args, sub) {
2650
+ const cfg = loadConfig(flagStr(args, 'config'));
2651
+ const ids = flagStr(args, 'id');
2652
+ if (!ids) {
2653
+ console.error(`email ${sub} 需要 --id <邮件id>(逗号分隔多个,从 email list 取)`);
2654
+ process.exit(1);
2655
+ }
2656
+ const send = !!args.flags.yes;
2657
+ let r;
2658
+ switch (sub) {
2659
+ case 'read': {
2660
+ const unread = flagBool(args, 'unread');
2661
+ const ok = await setMailReadStatus(cfg, ids, !unread);
2662
+ console.log(BOX_TOP);
2663
+ console.log(` 标记${unread ? '未读' : '已读'} ${ids} ${ok ? '✅ 成功' : '⚠ 状态异常'}`);
2664
+ console.log(BOX_TOP);
2665
+ return;
2666
+ }
2667
+ case 'star': {
2668
+ const unstar = flagBool(args, 'unstar');
2669
+ r = await setEmailStar(cfg, ids, !unstar);
2670
+ console.log(BOX_TOP);
2671
+ console.log(` ${unstar ? '取消标星' : '标星'} ${ids} ${r.ok ? '✅ 成功' : '⚠ 状态异常'}`);
2672
+ if (args.flags.raw)
2673
+ console.log(JSON.stringify(r.raw, null, 2));
2674
+ console.log(BOX_TOP);
2675
+ return;
2676
+ }
2677
+ case 'delete': {
2678
+ const permanent = flagBool(args, 'permanent');
2679
+ const label = permanent ? '彻底删除' : '删除(移回收站)';
2680
+ if (!send) {
2681
+ console.log(BOX_TOP);
2682
+ console.log(` [dry-run] 未执行 · ${label} ${ids}`);
2683
+ console.log(` ${permanent ? '此操作不可恢复' : '回收站可恢复'},确认后加 --yes 执行。`);
2684
+ console.log(BOX_TOP);
2685
+ return;
2686
+ }
2687
+ r = await deleteEmails(cfg, ids, { permanent });
2688
+ console.log(BOX_TOP);
2689
+ console.log(` ${label} ${ids} ${r.ok ? '✅ 成功' : '⚠ 状态异常'}`);
2690
+ if (args.flags.raw)
2691
+ console.log(JSON.stringify(r.raw, null, 2));
2692
+ console.log(BOX_TOP);
2693
+ return;
2694
+ }
2695
+ case 'move': {
2696
+ const folder = flagStr(args, 'folder');
2697
+ if (!folder) {
2698
+ console.error('email move 需要 --folder <folderid>(0收件箱/-1已发送/-2草稿/-3已删除 或自定义)');
2699
+ process.exit(1);
2700
+ }
2701
+ if (!send) {
2702
+ console.log(BOX_TOP);
2703
+ console.log(` [dry-run] 未执行 · 移动 ${ids} → 文件夹 ${folder}`);
2704
+ console.log(' 确认后加 --yes 执行。');
2705
+ console.log(BOX_TOP);
2706
+ return;
2707
+ }
2708
+ r = await moveEmails(cfg, ids, folder);
2709
+ console.log(BOX_TOP);
2710
+ console.log(` 移动 ${ids} → ${folder} ${r.ok ? '✅ 成功' : '⚠ 状态异常'}`);
2711
+ if (args.flags.raw)
2712
+ console.log(JSON.stringify(r.raw, null, 2));
2713
+ console.log(BOX_TOP);
2714
+ return;
2715
+ }
2716
+ case 'waitdeal': {
2717
+ const time = flagStr(args, 'time');
2718
+ if (!time) {
2719
+ console.error('email waitdeal 需要 --time <YYYY-MM-DD>(处理日期)');
2720
+ process.exit(1);
2721
+ }
2722
+ const way = flagStr(args, 'way') ?? '5';
2723
+ const note = flagStr(args, 'note');
2724
+ const remindTime = flagStr(args, 'remind-time');
2725
+ const hasRemind = flagBool(args, 'has-remind');
2726
+ const hasNote = flagBool(args, 'has-note');
2727
+ if (!send) {
2728
+ console.log(BOX_TOP);
2729
+ console.log(` [dry-run] 未执行 · 设待办邮件 ${ids}`);
2730
+ console.log(` 处理日期=${time} 提醒方式=${way === '1' ? '短信提醒' : '消息中心提醒'}${remindTime ? ` 提醒时间=${remindTime}` : ''}${note ? ` 备注=${note}` : ''}`);
2731
+ console.log(' 确认后加 --yes 执行。');
2732
+ console.log(BOX_TOP);
2733
+ return;
2734
+ }
2735
+ r = await setEmailWaitdeal(cfg, ids, { time, way, remindTime, note, hasNote, hasRemind });
2736
+ console.log(BOX_TOP);
2737
+ console.log(` 设待办邮件 ${ids} ${r.ok ? '✅ 成功' : '⚠ 状态异常'}`);
2738
+ if (args.flags.raw)
2739
+ console.log(JSON.stringify(r.raw, null, 2));
2740
+ console.log(BOX_TOP);
2741
+ return;
2742
+ }
2743
+ case 'recall': {
2744
+ const single = ids.split(/[,,]/)[0].trim();
2745
+ if (!send) {
2746
+ console.log(BOX_TOP);
2747
+ console.log(` [dry-run] 未执行 · 撤回邮件 ${single}`);
2748
+ console.log(' ⚠ 撤回仅对未读/未删除的已发送内部邮件有效,且不可恢复。确认后加 --yes 执行。');
2749
+ console.log(BOX_TOP);
2750
+ return;
2751
+ }
2752
+ r = await recallEmail(cfg, single);
2753
+ console.log(BOX_TOP);
2754
+ console.log(` 撤回邮件 ${single} ${r.ok ? '✅ 成功' : '⚠ 状态异常(可能已读/已删/外部邮件)'}`);
2755
+ if (args.flags.raw)
2756
+ console.log(JSON.stringify(r.raw, null, 2));
2757
+ console.log(BOX_TOP);
2758
+ return;
2759
+ }
2760
+ }
2761
+ }
2762
+ /** email label list/add/apply。 */
2763
+ async function runEmailLabel(args) {
2764
+ const cfg = loadConfig(flagStr(args, 'config'));
2765
+ const op = args._[2] ?? 'list';
2766
+ if (op === 'list') {
2767
+ const r = await getEmailLabels(cfg);
2768
+ if (args.flags.json) {
2769
+ console.log(JSON.stringify({ count: r.items.length, items: r.items.map((it) => ({ id: it.id, name: it.name, color: it.color, unreadCount: it.unreadCount, totalCount: it.totalCount })) }, null, 2));
2770
+ return;
2771
+ }
2772
+ console.log(`E9 邮件标签(共 ${r.items.length} 个)`);
2773
+ if (r.items.length === 0) {
2774
+ console.log('(暂无标签)');
2775
+ return;
2776
+ }
2777
+ r.items.forEach((it, i) => console.log(`${i + 1}. ${it.name ?? '(无名)'}${it.color ? ` ${it.color}` : ''}${it.unreadCount ? ` 未读 ${it.unreadCount}` : ''}${it.totalCount ? ` 共 ${it.totalCount}` : ''} [id=${it.id}]`));
2778
+ return;
2779
+ }
2780
+ if (op === 'add') {
2781
+ const name = flagStr(args, 'name');
2782
+ if (!name) {
2783
+ console.error('email label add 需要 --name <标签名>');
2784
+ process.exit(1);
2785
+ }
2786
+ const color = flagStr(args, 'color') ?? '#000000';
2787
+ const send = !!args.flags.yes;
2788
+ if (!send) {
2789
+ console.log(BOX_TOP);
2790
+ console.log(` [dry-run] 未执行 · 新建标签 "${name}" (${color})`);
2791
+ console.log(' 确认后加 --yes 执行。');
2792
+ console.log(BOX_TOP);
2793
+ return;
2794
+ }
2795
+ const r = await addEmailLabel(cfg, { name, color });
2796
+ console.log(BOX_TOP);
2797
+ console.log(` 新建标签 "${name}" ${r.ok ? `✅ 成功 [id=${r.id}]` : '⚠ 状态异常'}`);
2798
+ console.log(BOX_TOP);
2799
+ return;
2800
+ }
2801
+ if (op === 'apply') {
2802
+ const ids = flagStr(args, 'id');
2803
+ const label = flagStr(args, 'label');
2804
+ if (!ids || !label) {
2805
+ console.error('email label apply 需要 --id <邮件ids> 和 --label <标签id>');
2806
+ process.exit(1);
2807
+ }
2808
+ const remove = flagBool(args, 'remove');
2809
+ const send = !!args.flags.yes;
2810
+ if (!send) {
2811
+ console.log(BOX_TOP);
2812
+ console.log(` [dry-run] 未执行 · ${remove ? '去掉' : '贴'}标签[${label}] ${ids}`);
2813
+ console.log(' 确认后加 --yes 执行。');
2814
+ console.log(BOX_TOP);
2815
+ return;
2816
+ }
2817
+ const r = await applyEmailLabel(cfg, ids, label, { remove });
2818
+ console.log(BOX_TOP);
2819
+ console.log(` ${remove ? '去' : '贴'}标签[${label}] ${ids} ${r.ok ? '✅ 成功' : '⚠ 状态异常'}`);
2820
+ if (args.flags.raw)
2821
+ console.log(JSON.stringify(r.raw, null, 2));
2822
+ console.log(BOX_TOP);
2823
+ return;
2824
+ }
2825
+ console.error(`email label 支持 list/add/apply,收到: ${op}`);
2826
+ process.exit(1);
2827
+ }
2828
+ /** email attachment upload/list。 */
2829
+ async function runEmailAttachment(args) {
2830
+ const cfg = loadConfig(flagStr(args, 'config'));
2831
+ const op = args._[2] ?? 'list';
2832
+ if (op === 'upload') {
2833
+ const file = flagStr(args, 'file');
2834
+ if (!file) {
2835
+ console.error('email attachment upload 需要 --file <本地路径>');
2836
+ process.exit(1);
2837
+ }
2838
+ const name = flagStr(args, 'name');
2839
+ const r = await uploadEmailAttachment(cfg, file, name ? { name } : {});
2840
+ if (args.flags.raw)
2841
+ console.log(JSON.stringify(r.raw, null, 2));
2842
+ console.log(BOX_TOP);
2843
+ console.log(` 上传附件 ${r.data?.filename ?? file}`);
2844
+ console.log(` ${r.ok ? `✅ fileid=${r.data?.id} ${r.data?.filesize ?? ''}` : '⚠ 上传失败'}`);
2845
+ if (r.data?.filelink)
2846
+ console.log(` filelink: ${r.data.filelink}`);
2847
+ console.log(BOX_TOP);
2848
+ return;
2849
+ }
2850
+ if (op === 'list') {
2851
+ const r = await getEmailAttachmentList(cfg, { filename: flagStr(args, 'filename'), subject: flagStr(args, 'subject') });
2852
+ if (args.flags.json) {
2853
+ console.log(JSON.stringify({ count: r.items.length, items: r.items }, null, 2));
2854
+ return;
2855
+ }
2856
+ console.log(`E9 邮件附件(共 ${r.items.length} 个)`);
2857
+ if (r.items.length === 0) {
2858
+ console.log('(暂无附件)');
2859
+ return;
2860
+ }
2861
+ r.items.forEach((it, i) => console.log(`${i + 1}. ${it.filename ?? '(无名)'}${it.size ? ` ${it.size}` : ''}${it.subject ? ` | ${it.subject}` : ''}${it.mailid ? ` [mailid=${it.mailid}]` : ''}`));
2862
+ return;
2863
+ }
2864
+ console.error(`email attachment 支持 upload/list,收到: ${op}`);
2865
+ process.exit(1);
2866
+ }
2867
+ /** email size 邮箱容量。 */
2868
+ async function runEmailSize(args) {
2869
+ const cfg = loadConfig(flagStr(args, 'config'));
2870
+ const r = await getEmailSize(cfg);
2871
+ if (args.flags.json) {
2872
+ console.log(JSON.stringify(r, null, 2));
2873
+ return;
2874
+ }
2875
+ console.log(BOX_TOP);
2876
+ console.log(' E9 邮箱容量');
2877
+ console.log(` 未读 ${r.unreadsize ?? 0} 封 · 总数 ${r.totalSize ?? 0} 封`);
2878
+ console.log(` 已用 ${r.occupyspace ?? 0} / 总 ${r.totalspace ?? 0}`);
2879
+ console.log(` 剩余 ${r.remainspace ?? 0}`);
2880
+ console.log(BOX_TOP);
2881
+ }
2882
+ /** email accounts 接收账户。 */
2883
+ async function runEmailAccounts(args) {
2884
+ const cfg = loadConfig(flagStr(args, 'config'));
2885
+ const r = await getEmailReciveAccounts(cfg);
2886
+ if (args.flags.json) {
2887
+ console.log(JSON.stringify({ count: r.items.length, items: r.items }, null, 2));
2888
+ return;
2889
+ }
2890
+ console.log(`E9 接收账户(共 ${r.items.length} 个)`);
2891
+ if (r.items.length === 0) {
2892
+ console.log('(无独立接收账户,使用主账户)');
2893
+ return;
2894
+ }
2895
+ r.items.forEach((it, i) => console.log(`${i + 1}. ${JSON.stringify(it)}`));
2896
+ }
2897
+ /**
2898
+
2899
+ /**
2900
+ * 渲染台账的菜单挂载点(菜单名索引):取与关键字匹配的菜单路径,无则首个;截断 32 字符。
2901
+ * 用户按门户菜单名找台账时,菜单名可能与台账名不符,这列把两者关联起来。
2902
+ */
2903
+ function fmtMenus(e, kw) {
2904
+ const menus = Array.isArray(e.menus) ? e.menus : [];
2905
+ if (!menus.length)
2906
+ return '';
2907
+ let pick = menus[0];
2908
+ if (kw) {
2909
+ const q = String(kw).toLowerCase();
2910
+ pick = menus.find((mu) => (mu.name || '').toLowerCase().includes(q) || (mu.path || '').toLowerCase().includes(q)) || menus[0];
2911
+ }
2912
+ const s = pick.path || pick.name || '';
2913
+ if (!s)
2914
+ return '';
2915
+ const t = s.length > 32 ? s.slice(0, 32) + '…' : s;
2916
+ return ` 菜单=${t}`;
2917
+ }
2918
+ /**
2919
+ * #7 统一名称搜索:用户只说名字、不知归属(台账/菜单/流程)时,一次搜三类,避免猜错域来回。
2920
+ * 台账:cube find(台账目录 list_name + 菜单挂载名)
2921
+ * 菜单:fetchMenuMounts 的 menuNodes(含非 cube 的 CRM/SPA 前台菜单,按节点名搜)
2922
+ * 流程:getCreateWfList(流程名/拼音)
2923
+ */
2924
+ async function runSearch(args) {
2925
+ const kw = flagStr(args, 'name') ?? args._.slice(1).join(' ');
2926
+ if (!kw) {
2927
+ console.error('用法: e9 search <名称关键字> (或 --name <关键字>)—— 同时搜台账/菜单/流程');
2928
+ process.exit(1);
2929
+ }
2930
+ const cfg = loadConfig(flagStr(args, 'config'));
2931
+ const kwl = kw.toLowerCase();
2932
+ const searchBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
2933
+ console.log(`搜索「${kw}」:台账 / 菜单 / 流程 三类\n`);
2934
+ let ledgerN = 0, menuN = 0, wfN = 0;
2935
+ // ① 台账
2936
+ try {
2937
+ const r = await listCubeMenus(cfg, {});
2938
+ const cands = findCubeByName(r.entries, kw);
2939
+ ledgerN = cands.length;
2940
+ console.log(`■ 台账(${ledgerN} 个,来源:${r.source})`);
2941
+ if (cands.length === 0)
2942
+ console.log(' (无)');
2943
+ for (const c of cands.slice(0, 10)) {
2944
+ const label = hyperlinkFor('cube-search', { ...c, customid: c.id }, searchBase, c.list_name, { menuUrl: c.menus?.[0]?.url });
2945
+ console.log(` customid=${String(c.id).padEnd(7)} ${label} [mode=${c.mode_id || '-'} app=${c.app_name || '-'}] (${c.matchNote})${fmtMenus(c, kw)}`);
2946
+ }
2947
+ if (cands.length > 10)
2948
+ console.log(` ... 还有 ${cands.length - 10} 个(e9 cube find --name ${kw} 看全部)`);
2949
+ }
2950
+ catch (e) {
2951
+ console.log(`■ 台账:查询失败 ${e.message}`);
2952
+ }
2953
+ // ② 菜单(含非 cube 的 CRM/SPA 前台菜单,解决"客户监控"这类不在台账目录的菜单)
2954
+ try {
2955
+ const mm = await fetchMenuMounts(cfg);
2956
+ const nodes = (mm.menuNodes || []).filter((n) => (n.name || '').toLowerCase().includes(kwl) || (n.path || '').toLowerCase().includes(kwl));
2957
+ menuN = nodes.length;
2958
+ console.log(`\n■ 菜单(${menuN} 个,含 CRM/SPA 前台菜单)`);
2959
+ if (nodes.length === 0)
2960
+ console.log(' (无)');
2961
+ for (const n of nodes.slice(0, 10))
2962
+ console.log(` ${hyperlinkFor('cube-search', { ...n, id: n.customid }, searchBase, n.name, { menuUrl: n.url })} 路径=${n.path}${n.customid ? ' (→台账 ' + n.customid + ')' : ''}`);
2963
+ if (nodes.length > 10)
2964
+ console.log(` ... 还有 ${nodes.length - 10} 个`);
2965
+ }
2966
+ catch (e) {
2967
+ console.log(`\n■ 菜单:查询失败 ${e.message}`);
2968
+ }
2969
+ // ③ 流程
2970
+ try {
2971
+ const r = await getCreateWfList(cfg, { tab: '1', keyword: kw });
2972
+ const flows = [];
2973
+ for (const cat of (r.categories || []))
2974
+ for (const w of (cat.workflows || []))
2975
+ flows.push({ id: w.id, name: w.name, cat: cat.name });
2976
+ wfN = flows.length;
2977
+ console.log(`\n■ 流程(${wfN} 个)`);
2978
+ if (flows.length === 0)
2979
+ console.log(' (无)');
2980
+ for (const w of flows.slice(0, 10))
2981
+ console.log(` [${w.id}] ${hyperlinkFor('create', w, searchBase, w.name)} (${w.cat})`);
2982
+ if (flows.length > 10)
2983
+ console.log(` ... 还有 ${flows.length - 10} 个`);
2984
+ }
2985
+ catch (e) {
2986
+ console.log(`\n■ 流程:查询失败 ${e.message}`);
2987
+ }
2988
+ console.log(`\n合计:台账 ${ledgerN} / 菜单 ${menuN} / 流程 ${wfN}`);
2989
+ }
2990
+ /**
2991
+ * 建模台账(cube):list/fields/export。接线自 cli-cube-接线.js。
2992
+ */
2993
+ /** 条件 label 未匹配时列候选,提示修正并终止(不静默放空结果)。 */
2994
+ function printConditionUnresolved(unresolved) {
2995
+ for (const u of unresolved) {
2996
+ const cands = (u.candidates ?? []).join('、');
2997
+ console.error(`⚠ 条件「${u.label}」未匹配到台账查询字段${cands ? `。相近字段候选: ${cands}` : ''}(可用 e9 cube fields 查看全部查询字段名)。`);
2998
+ }
2999
+ process.exit(1);
3000
+ }
3001
+
3002
+ async function runCube(args) {
3003
+ const sub = args._[1] ?? 'list';
3004
+ const cfg = loadConfig(flagStr(args, 'config'));
3005
+ // ---------- menus:列全部台账(目录发现 + 缓存) ----------
3006
+ if (sub === 'menus') {
3007
+ const r = await listCubeMenus(cfg, { refresh: flagBool(args, 'refresh'), noCache: flagBool(args, 'no-cache') });
3008
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3009
+ const linkedEntries = r.entries.map((entry) => withLink('cube-search', { ...entry, customid: entry.id }, cubeBase, { menuUrl: entry.menus?.[0]?.url ?? entry.url }));
3010
+ if (args.flags.json) {
3011
+ console.log(JSON.stringify({ source: r.source, count: r.count, fetchedAt: r.fetchedAt, fromCache: r.fromCache, total: r.total, entries: linkedEntries }, null, 2));
3012
+ return;
3013
+ }
3014
+ console.log(`E9 台账目录 · ${r.count} 个(来源:${r.source}${r.fromCache ? ' ·缓存' : ''}${r.fetchedAt ? ' ·' + r.fetchedAt.slice(0, 10) : ''})`);
3015
+ if (r.count === 0) {
3016
+ console.log(r.error ? `(${r.error})` : '(无台账)');
3017
+ return;
3018
+ }
3019
+ for (const e of linkedEntries)
3020
+ console.log(` customid=${String(e.id).padEnd(7)} ${e.url ? `[${escapeLinkText(e.list_name)}](${e.url})` : e.list_name} [mode=${e.mode_id || '-'} app=${e.app_name || '-'}]${!e.mode_id ? ' ·查询视图' : ''}${fmtMenus(e)}`);
3021
+ return;
3022
+ }
3023
+ // ---------- find:按名模糊匹配台账 ----------
3024
+ if (sub === 'find') {
3025
+ const kw = flagStr(args, 'name') ?? args._.slice(2).join(' ');
3026
+ if (!kw) {
3027
+ console.error('用法: e9 cube find <名称关键字> (或 --name <关键字>)');
3028
+ process.exit(1);
3029
+ }
3030
+ const r = await listCubeMenus(cfg, {});
3031
+ const cands = findCubeByName(r.entries, kw);
3032
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3033
+ const linkedCands = cands.map((entry) => withLink('cube-search', { ...entry, customid: entry.id }, cubeBase, { menuUrl: entry.menus?.[0]?.url ?? entry.url }));
3034
+ const detail = flagBool(args, 'tree-detail');
3035
+ if (args.flags.json) {
3036
+ console.log(JSON.stringify({ keyword: kw, source: r.source, matches: linkedCands }, null, 2));
3037
+ return;
3038
+ }
3039
+ if (cands.length === 0) {
3040
+ console.log(`未匹配到含「${kw}」的台账(来源:${r.source},共${r.count}个)。可用 e9 cube menus 查看全部。`);
3041
+ return;
3042
+ }
3043
+ console.log(`匹配「${kw}」的台账(${cands.length} 个,按相关度排序 ·来源:${r.source}):`);
3044
+ for (const c of linkedCands) {
3045
+ // ★ #8 多节点(树形菜单叶子)标注总数,避免"只看到1条"的误解
3046
+ const ncnt = Array.isArray(c.menus) && c.menus.length > 1 ? ` (共${c.menus.length}节点)` : '';
3047
+ console.log(` customid=${String(c.id).padEnd(7)} ${c.url ? `[${escapeLinkText(c.list_name)}](${c.url})` : c.list_name} [mode=${c.mode_id || '-'} app=${c.app_name || '-'}] (${c.matchNote})${!c.mode_id ? ' ·查询视图' : ''}${fmtMenus(c, kw)}${ncnt}`);
3048
+ // ★ #8 --tree-detail:展开该台账对应的全部菜单/树节点路径
3049
+ if (detail && Array.isArray(c.menus) && c.menus.length > 1) {
3050
+ for (const mu of c.menus)
3051
+ console.log(` - ${mu.path || mu.name}`);
3052
+ }
3053
+ }
3054
+ return;
3055
+ }
3056
+ const customid = flagStr(args, 'customid') ?? flagStr(args, 'id');
3057
+ if (!customid) {
3058
+ console.error('用法: e9 cube <list|fields|export|search-info|excel|count|group|card|browser|card-fields|create|edit|delete|print|import-template|import> --customid <id>\n e9 cube menus [--refresh] 列全部台账\n e9 cube find <名称> 按名找台账');
3059
+ process.exit(1);
3060
+ }
3061
+ const menuIds = flagStr(args, 'menu-ids') ?? '';
3062
+ if (sub === 'import-template') {
3063
+ const r = await downloadCubeImportTemplate(cfg, { customid, modeId: flagStr(args, 'mode-id'), outDir: flagStr(args, 'out-dir'), filename: flagStr(args, 'filename') });
3064
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
3065
+ else {
3066
+ console.log(`台账批量导入模板已下载到:${r.outPath}`);
3067
+ console.log(r.requiredFieldsKnown ? `必填字段:${r.requiredFields.join('、')}` : '必填字段未能从卡片接口读取,请保留模板中的必填提示,不能猜。');
3068
+ console.log('可选择:自己填写;把文件交给助手填写;或直接描述每行数据。填写时只允许改数据单元格。');
3069
+ }
3070
+ return;
3071
+ }
3072
+ if (sub === 'import') {
3073
+ const file = flagStr(args, 'file');
3074
+ if (!file) throw new Error('cube import 需要 --file <已填写模板路径>。');
3075
+ const r = await importCubeExcel(cfg, { customid, modeId: flagStr(args, 'mode-id'), file, yes: !!args.flags.yes, ignoreErrors: flagStr(args, 'ignore-errors') ?? '0' });
3076
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
3077
+ else if (!r.imported && r.requiresConfirmation) console.log(`将向台账 customid=${customid} 导入文件:${r.filePath}\n确认文件内容无误后加 --yes。`);
3078
+ else console.log(`${r.imported ? '✅ 台账导入完成' : '⚠ 台账导入失败'}${r.message ? `:${r.message}` : ''}${r.successCount != null ? `(成功 ${r.successCount} 条)` : ''}`);
3079
+ if (args.flags.yes && !r.imported) process.exitCode = 1;
3080
+ return;
3081
+ }
3082
+ // ---------- card-fields:卡片字段定义 ----------
3083
+ if (sub === 'card-fields') {
3084
+ const init = await getCubeInit(cfg, { customid });
3085
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3086
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3087
+ const r = await getCardFields(cfg, { modeId, formId, billid: flagStr(args, 'billid') });
3088
+ if (args.flags.json) {
3089
+ console.log(JSON.stringify({ customid, modeId, formId, layoutid: r.layoutid, fields: r.fields }, null, 2));
3090
+ return;
3091
+ }
3092
+ console.log(`E9 卡片字段 · ${init.name ?? customid} (modeId=${modeId} formId=${formId} layoutid=${r.layoutid})`);
3093
+ for (const f of r.fields)
3094
+ console.log(` field${f.id} ${f.label ?? ''} [${f.name ?? ''}] ht=${f.htmltype ?? '?'} ${f.isedit ? '可编辑' : '只读'}${f.ismandatory ? ' 必填' : ''}`);
3095
+ return;
3096
+ }
3097
+ // ---------- view:单条记录完整详情(P1-2) ----------
3098
+ if (sub === 'view') {
3099
+ const billid = flagStr(args, 'billid');
3100
+ if (!billid) {
3101
+ console.error('cube view 需要 --billid <记录id>');
3102
+ process.exit(1);
3103
+ }
3104
+ const init = await getCubeInit(cfg, { customid });
3105
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3106
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3107
+ // 查看态字段定义 + best-effort 当前值
3108
+ const cf = await getCardFields(cfg, { modeId, formId, billid });
3109
+ if (args.flags.json) {
3110
+ console.log(JSON.stringify({ customid, billid, modeId, formId, layoutid: cf.layoutid, fields: cf.fields }, null, 2));
3111
+ return;
3112
+ }
3113
+ console.log(`E9 台账记录详情 · ${init.name ?? customid} billid=${billid} (modeId=${modeId} formId=${formId})`);
3114
+ const hasVal = cf.fields.some((f) => f.value !== undefined && f.value !== null && f.value !== '');
3115
+ if (hasVal) {
3116
+ for (const f of cf.fields)
3117
+ console.log(` ${f.label ?? `field${f.id}`} [${f.htmltype ?? '?'}] = ${f.value || '(空)'}${f.ismandatory ? ' 必填' : ''}${f.isedit ? '' : ' 只读'}`);
3118
+ }
3119
+ else {
3120
+ // 查看态 mainFields 不带值时:回退 list 反查该条记录的行值
3121
+ let row = null;
3122
+ try {
3123
+ const fb = await fetchCubeRowsByIds(cfg, { customid, billids: [billid], modeId });
3124
+ row = fb.rows[0];
3125
+ }
3126
+ catch { /* 反查失败不阻断 */ }
3127
+ if (row && row.fields.length) {
3128
+ console.log('(查看态无值,回退列表行值)');
3129
+ for (const f of row.fields)
3130
+ if (f.value)
3131
+ console.log(` ${f.title ?? f.dataIndex} = ${f.value}`);
3132
+ }
3133
+ else {
3134
+ console.log('(未能读取该记录的值,请人工核对 billid)');
3135
+ }
3136
+ }
3137
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3138
+ const link = hyperlinkFor('cube', { id: billid }, cubeBase, '打开卡片', { modeId, formId, customid });
3139
+ console.log(`\n${link}`);
3140
+ return;
3141
+ }
3142
+ // ---------- create:新建台账记录 ----------
3143
+ if (sub === 'create') {
3144
+ const init = await getCubeInit(cfg, { customid });
3145
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3146
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3147
+ const vf = flagStr(args, 'values-file');
3148
+ const valuesRaw = flagStr(args, 'values') ?? (vf ? (await import('node:fs')).readFileSync(vf, 'utf8') : '');
3149
+ if (!valuesRaw) {
3150
+ console.error('cube create 需要 --values <json> 或 --values-file');
3151
+ process.exit(1);
3152
+ }
3153
+ const values = JSON.parse(valuesRaw);
3154
+ // 附件:--file fieldId=path[,fieldId=path]
3155
+ const fileStr = flagStr(args, 'file') ?? '';
3156
+ const files = fileStr ? fileStr.split(',').map((s) => { const i = s.indexOf('='); return { fieldId: s.slice(0, i), filePath: s.slice(i + 1) }; }).filter((f) => f.fieldId && f.filePath) : [];
3157
+ if (!args.flags.yes) {
3158
+ // P1-1:dry-run 跑真实联动解析,确认表与最终提交字段集一致
3159
+ const pv = await previewCubeCard(cfg, { modeId, formId, values, files });
3160
+ const cf = { layoutid: pv.layoutid, fields: pv.fields };
3161
+ console.log(`[dry-run] 新建台账记录 · ${init.name ?? customid}`);
3162
+ console.log(`modeId=${modeId} formId=${formId} layoutid=${cf.layoutid} 字段 ${cf.fields.length} 个\n`);
3163
+ // 字段确认表:字段 | 控件类型 | 必填 | 值/【待填】
3164
+ for (const f of cf.fields) {
3165
+ const val = pv.finalValues[f.id] ?? pv.finalValues[`field${f.id}`] ?? '';
3166
+ const valDisplay = val ? val : (f.ismandatory ? '【待填·必填】' : '(空)');
3167
+ console.log(` field${f.id} ${f.label ?? ''} [${f.htmltype ?? '?'}]${f.ismandatory ? ' 必填' : ''} = ${valDisplay}`);
3168
+ }
3169
+ if (pv.linked.length) {
3170
+ console.log('\n🔗 联动自动带出:');
3171
+ for (const lk of pv.linked)
3172
+ console.log(` ${lk.label} (field${lk.fieldId}) = ${lk.value} ← 由已填触发字段带出`);
3173
+ }
3174
+ if (pv.files.length)
3175
+ console.log('\n附件:', pv.files.map((f) => `field${f.fieldId}←${f.filePath}`).join(', '), '(--yes 时上传)');
3176
+ console.log('\n加 --yes 真提交(cookie 鉴权 + 完整链路 + 附件上传 + 全字段 JSONStr)。');
3177
+ return;
3178
+ }
3179
+ const res = await submitCubeCard(cfg, { modeId, formId, values, files });
3180
+ console.log(`${res.ok ? '✅ 新建成功' : '⚠ 提交异常'} billid=${res.billid ?? '?'} status=${res.raw?.status}`);
3181
+ if (!res.ok)
3182
+ console.log('响应:', JSON.stringify(res.raw).slice(0, 400));
3183
+ return;
3184
+ }
3185
+ // ---------- edit:编辑台账记录 ----------
3186
+ if (sub === 'edit') {
3187
+ const billid = flagStr(args, 'billid');
3188
+ if (!billid) {
3189
+ console.error('cube edit 需要 --billid <id> --values <json>');
3190
+ process.exit(1);
3191
+ }
3192
+ const init = await getCubeInit(cfg, { customid });
3193
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3194
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3195
+ const vf = flagStr(args, 'values-file');
3196
+ const valuesRaw = flagStr(args, 'values') ?? (vf ? (await import('node:fs')).readFileSync(vf, 'utf8') : '');
3197
+ if (!valuesRaw) {
3198
+ console.error('cube edit 需要 --values <json>');
3199
+ process.exit(1);
3200
+ }
3201
+ const values = JSON.parse(valuesRaw);
3202
+ const fileStr = flagStr(args, 'file') ?? '';
3203
+ const files = fileStr ? fileStr.split(',').map((s) => { const i = s.indexOf('='); return { fieldId: s.slice(0, i), filePath: s.slice(i + 1) }; }).filter((f) => f.fieldId && f.filePath) : [];
3204
+ if (!args.flags.yes) {
3205
+ // P0-2:dry-run 显示改前→改后对比(取查看态当前值,能力已具备,复用 card-fields 的 billid 查看态)
3206
+ let curFields = null;
3207
+ let rowFallback = null;
3208
+ try {
3209
+ curFields = await getCardFields(cfg, { modeId, formId, billid });
3210
+ }
3211
+ catch {
3212
+ curFields = null;
3213
+ }
3214
+ // 查看态不带值时:list 反查该条记录的行值作对比兜底
3215
+ if (!(curFields && curFields.fields.some((f) => f.value !== undefined && f.value !== null && f.value !== ''))) {
3216
+ try {
3217
+ const fb = await fetchCubeRowsByIds(cfg, { customid, billids: [billid], modeId });
3218
+ rowFallback = fb.rows[0];
3219
+ }
3220
+ catch { /* 反查失败不阻断 */ }
3221
+ }
3222
+ console.log(`[dry-run] 编辑台账记录 · billid=${billid}${rowFallback ? ` 当前记录: ${rowFallback.name || '(无标题)'}` : ''}`);
3223
+ if (curFields && curFields.fields.some((f) => f.value !== undefined && f.value !== null && f.value !== '')) {
3224
+ console.log('字段变更对比(仅列有变动的字段;未列出的保持原值):');
3225
+ let diffCount = 0;
3226
+ for (const f of curFields.fields) {
3227
+ const newVal = values[f.id] ?? values[`field${f.id}`];
3228
+ if (newVal === undefined)
3229
+ continue;
3230
+ const oldVal = f.value ?? '';
3231
+ const changed = String(oldVal) !== String(newVal);
3232
+ if (changed) {
3233
+ diffCount++;
3234
+ console.log(` ${f.label ?? `field${f.id}`} (field${f.id}): ${oldVal || '(空)'} → ${newVal}`);
3235
+ }
3236
+ }
3237
+ if (diffCount === 0)
3238
+ console.log(' (传入值与当前值无差异,或字段均未匹配)');
3239
+ const unknownKeys = Object.keys(values).filter((k) => !curFields.fields.some((f) => String(f.id) === k.replace(/^field/, '')));
3240
+ if (unknownKeys.length)
3241
+ console.log(` ⚠ 以下 key 未匹配到卡片字段: ${unknownKeys.join(', ')}`);
3242
+ if (files.length)
3243
+ console.log('附件:', files.map((f) => `field${f.fieldId}←${f.filePath}`).join(', '), '(--yes 时上传)');
3244
+ }
3245
+ else {
3246
+ // 取不到原值(查看态不带值 / 无权限):降级展示传入值 + list 反查行值,并标注
3247
+ if (rowFallback && rowFallback.fields.length)
3248
+ console.log('当前记录行值(list 反查,查看态未带值):');
3249
+ for (const f of (rowFallback?.fields ?? []))
3250
+ if (f.value)
3251
+ console.log(` ${f.title ?? f.dataIndex} = ${f.value}`);
3252
+ console.log('JSONStr(⚠ 未能读取卡片字段原值,请人工核对):');
3253
+ console.log(JSON.stringify(values, null, 2));
3254
+ if (files.length)
3255
+ console.log('附件:', files.map((f) => `field${f.fieldId}←${f.filePath}`).join(', '));
3256
+ }
3257
+ console.log('\n加 --yes 真提交。');
3258
+ return;
3259
+ }
3260
+ const res = await submitCubeCard(cfg, { modeId, formId, billid, values, files });
3261
+ console.log(`${res.ok ? '✅ 编辑成功' : '⚠ 提交异常'} billid=${billid} status=${res.raw?.status}`);
3262
+ if (!res.ok)
3263
+ console.log('响应:', JSON.stringify(res.raw).slice(0, 400));
3264
+ return;
3265
+ }
3266
+ // ---------- delete:删除台账记录 ----------
3267
+ if (sub === 'delete') {
3268
+ const billids = flagStr(args, 'billids') ?? flagStr(args, 'billid');
3269
+ if (!billids) {
3270
+ console.error('cube delete 需要 --billids <id,...>');
3271
+ process.exit(1);
3272
+ }
3273
+ const init = await getCubeInit(cfg, { customid });
3274
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3275
+ if (!args.flags.yes) {
3276
+ // P0-1:反查记录名,输出确认表,避免黑盒删
3277
+ console.log(`[dry-run] 删除台账记录 · customid=${customid} modeId=${modeId}`);
3278
+ let rows = null;
3279
+ let missing = [];
3280
+ try {
3281
+ const fb = await fetchCubeRowsByIds(cfg, { customid, billids, modeId });
3282
+ rows = fb.rows;
3283
+ missing = fb.missing;
3284
+ }
3285
+ catch (e) {
3286
+ console.log(` (反查记录名失败: ${e.message})`);
3287
+ }
3288
+ if (rows && rows.length) {
3289
+ console.log(`将删除以下 ${rows.length} 条(⚠ 删除不可恢复):`);
3290
+ rows.forEach((row, i) => console.log(` ${i + 1}. ${row.name || '(无标题)'} [id=${row.id}]`));
3291
+ }
3292
+ else if (rows) {
3293
+ console.log('(未能反查到目标记录)');
3294
+ }
3295
+ if (missing && missing.length)
3296
+ console.log(` ⚠ 未找到/无权限的记录: ${missing.join(', ')}(请人工核对 billids)`);
3297
+ console.log('\n加 --yes 真删。');
3298
+ return;
3299
+ }
3300
+ const res = await deleteCubeData(cfg, { customid, billids, modeId });
3301
+ console.log(`${res.ok ? '✅ 删除成功' : '⚠ 删除异常'} ${res.message ?? ''}`);
3302
+ if (!res.ok)
3303
+ console.log('响应:', JSON.stringify(res.raw).slice(0, 400));
3304
+ return;
3305
+ }
3306
+ // ---------- print:打印台账记录为 PDF ----------
3307
+ if (sub === 'print') {
3308
+ const billid = flagStr(args, 'billid');
3309
+ if (!billid) {
3310
+ console.error('cube print 需要 --billid <记录id>');
3311
+ process.exit(1);
3312
+ }
3313
+ const init = await getCubeInit(cfg, { customid });
3314
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3315
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3316
+ console.log(`打开合同详情页 · customid=${customid} billid=${billid}(浏览器打开卡片,请在页面点「打印」按钮另存 PDF)...`);
3317
+ const res = await printCubePdf(cfg, { customid, billid, modeId, formId });
3318
+ console.log(`${res.ok ? '✅ 已打开详情页' : '⚠ 失败'} ${res.mode}`);
3319
+ return;
3320
+ }
3321
+ // ---------- search-info:字段下拉选项全表 ----------
3322
+ if (sub === 'search-info') {
3323
+ const r = await getSearchColumnInfo(cfg, { customid });
3324
+ // P2-2:合并 card-fields 的 fid→label 中文名(获取失败不阻断,回退数字 fid)
3325
+ let labelMap = {};
3326
+ try {
3327
+ const init = await getCubeInit(cfg, { customid });
3328
+ const modeId = flagStr(args, 'mode-id') ?? init.modeId;
3329
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3330
+ const cf = await getCardFields(cfg, { modeId, formId });
3331
+ for (const f of cf.fields)
3332
+ labelMap[String(f.id)] = f.label;
3333
+ }
3334
+ catch { /* label 获取失败不阻断 */ }
3335
+ if (args.flags.json) {
3336
+ // 向后兼容:保留 optionsMap 原结构,另加 labelMap(fid→中文名)供 AI 对应字段
3337
+ console.log(JSON.stringify({ customid, optionsMap: r.optionsMap, labelMap }, null, 2));
3338
+ return;
3339
+ }
3340
+ console.log(`E9 台账字段选项 · customid=${customid}`);
3341
+ for (const [fid, list] of Object.entries(r.optionsMap)) {
3342
+ const label = labelMap[fid] ? `${labelMap[fid]}(` : '';
3343
+ console.log(` 字段 ${label}${fid}${labelMap[fid] ? ')' : ''}:`);
3344
+ for (const o of list)
3345
+ if (o.showname)
3346
+ console.log(` ${o.key ?? ''} = ${o.showname}`);
3347
+ }
3348
+ return;
3349
+ }
3350
+ // ---------- excel:Excel 视图取数 ----------
3351
+ if (sub === 'excel') {
3352
+ const r = await listCubeExcel(cfg, { customid, menuIds, page: Number(flagStr(args, 'page') ?? '1') || 1 });
3353
+ if (args.flags.json) {
3354
+ console.log(JSON.stringify({ customid, name: r.name, total: r.total, columns: r.columns, items: r.items }, null, 2));
3355
+ return;
3356
+ }
3357
+ console.log(`E9 台账(Excel视图) · ${r.name ?? customid} 第 ${r.page} 页(每页 10 条) 共 ${r.total ?? '?'} 条`);
3358
+ for (const c of r.columns)
3359
+ console.log(` · ${c.title ?? c.dataIndex} [${c.dataIndex}] ${c.editable ? '(可编辑)' : ''}`);
3360
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3361
+ for (let i = 0; i < r.items.length; i++) {
3362
+ const it = r.items[i];
3363
+ // 卡片详情 URL(点开进 E9 台账卡片页)
3364
+ const nameLinked = hyperlinkFor('cube', it, cubeBase, it.name ?? '(无名称)', { modeId: r.modeId, formId: r.formId, customid: r.customid });
3365
+ console.log(`${i + 1}. ${nameLinked} [id=${it.id ?? ''}]`);
3366
+ const meta = it.fields.filter((f) => f.value).slice(0, 5).map((f) => `${f.title}=${f.value}`).join(' | ');
3367
+ if (meta)
3368
+ console.log(` ${meta}`);
3369
+ }
3370
+ // getDataAndTime 服务端固定 10 条/页(无视 pageSize 入参),直读服务端页天然连续
3371
+ const remaining = (r.total ?? 0) - r.page * 10;
3372
+ if (remaining > 0)
3373
+ console.log(`还有 ${remaining} 条,用 --page ${r.page + 1} 继续(每页 10 条)。`);
3374
+ return;
3375
+ }
3376
+ // ---------- count / group / card / browser ----------
3377
+ if (sub === 'count') {
3378
+ const r = await getCubeCountData(cfg, { customid, menuIds });
3379
+ if (args.flags.json) {
3380
+ console.log(JSON.stringify({ customid, countData: r.countData }, null, 2));
3381
+ return;
3382
+ }
3383
+ console.log(`E9 台账聚合统计 · customid=${customid}`);
3384
+ for (const c of r.countData)
3385
+ console.log(` ${JSON.stringify(c)}`);
3386
+ return;
3387
+ }
3388
+ if (sub === 'group') {
3389
+ const r = await getCubeGroupData(cfg, { customid, menuIds, groupValue: flagStr(args, 'group-value') });
3390
+ if (args.flags.json) {
3391
+ console.log(JSON.stringify({ customid, groupData: r.groupData }, null, 2));
3392
+ return;
3393
+ }
3394
+ console.log(`E9 台账分组统计 · customid=${customid}: ${JSON.stringify(r.groupData).slice(0, 600)}`);
3395
+ return;
3396
+ }
3397
+ if (sub === 'card') {
3398
+ const modeId = flagStr(args, 'mode-id') ?? flagStr(args, 'modeid');
3399
+ if (!modeId) {
3400
+ console.error('cube card 需要 --mode-id');
3401
+ process.exit(1);
3402
+ }
3403
+ const r = await getExpCardConfig(cfg, { customid, modeId });
3404
+ console.log(`E9 卡片导出配置 · 可导出:${r.canExp ? '是' : '否'} 导出名:${r.expCardName ?? ''}`);
3405
+ for (const b of r.batchExpList)
3406
+ console.log(` · ${b.showname} [key=${b.key}]`);
3407
+ return;
3408
+ }
3409
+ if (sub === 'browser') {
3410
+ const modeId = flagStr(args, 'mode-id');
3411
+ const fieldId = flagStr(args, 'field-id');
3412
+ const dt = flagStr(args, 'detail-type');
3413
+ if (!modeId || !fieldId || !dt) {
3414
+ console.error('cube browser 需要 --mode-id --field-id --detail-type(1人员/17多选/167部门/256 cube树)');
3415
+ process.exit(1);
3416
+ }
3417
+ // 256(cube 树浏览框):treeid = fielddbtype,未给则从 card-fields 自动取
3418
+ let fielddbtype = flagStr(args, 'field-db-type');
3419
+ if (dt === '256' && !fielddbtype) {
3420
+ const init = await getCubeInit(cfg, { customid });
3421
+ const formId = flagStr(args, 'form-id') ?? init.formId;
3422
+ const cf = await getCardFields(cfg, { modeId, formId });
3423
+ const fld = cf.fields.find((f) => String(f.id) === String(fieldId));
3424
+ fielddbtype = fld?.fielddbtype;
3425
+ }
3426
+ const r = await getModeBrowserOptions(cfg, { modeId, fieldId, detailtype: dt, fielddbtype, pageSize: Number(flagStr(args, 'page-size') ?? '10') || 10 });
3427
+ for (const it of r.items)
3428
+ console.log(` ${it.id} = ${it.name}${it.path ? ' (' + it.path + ')' : ''}`);
3429
+ return;
3430
+ }
3431
+ // ---------- fields ----------
3432
+ if (sub === 'fields') {
3433
+ const r = await getCubeCondition(cfg, { customid });
3434
+ if (args.flags.json) {
3435
+ console.log(JSON.stringify({ customid, items: r.items }, null, 2));
3436
+ return;
3437
+ }
3438
+ console.log(`E9 建模查询条件 · customid=${customid}`);
3439
+ if (r.items.length === 0) {
3440
+ console.log('(无查询条件)');
3441
+ return;
3442
+ }
3443
+ for (const it of r.items) {
3444
+ console.log(` ${it.label}(domkey=${(it.domkey || []).join(',')},类型=${it.type ?? '?'})${it.isQuickSearch ? ' [快捷搜索]' : ''}`);
3445
+ for (const o of it.options)
3446
+ console.log(` ${o.key} = ${o.showname}`);
3447
+ }
3448
+ console.log('\n注:以上为查询条件字段,不含必填标记和完整控件类型。');
3449
+ console.log('新建/编辑请用 `cube card-fields --customid <id>` 查看卡片字段定义(含必填/可编辑/完整控件类型)。');
3450
+ return;
3451
+ }
3452
+ // ---------- export ----------
3453
+ if (sub === 'export') {
3454
+ const filename = flagStr(args, 'filename');
3455
+ // P2-1:透传筛选条件(--condition/--keyword),中文 label 自动映射 domkey
3456
+ const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
3457
+ if (condRes.unresolved.length)
3458
+ printConditionUnresolved(condRes.unresolved);
3459
+ const r = await exportCubeExcel(cfg, { customid, filename, yes: !!args.flags.yes, outDir: flagStr(args, 'out-dir'), modeCustomidPlain: !!args.flags.plain, conditionValues: condRes.conditionValues, keyword: flagStr(args, 'keyword') ?? '' });
3460
+ if (args.flags.json) {
3461
+ console.log(JSON.stringify({ customid, dataKey: r.dataKey, filename: r.filename, downloaded: r.downloaded, outPath: r.outPath, size: r.size, sourceTotal: r.sourceTotal, validation: r.validation, expBody: r.expBody }, null, 2));
3462
+ return;
3463
+ }
3464
+ if (!r.downloaded) {
3465
+ console.log(`E9 建模导出 · dry-run 文件名=${r.filename}`);
3466
+ console.log(JSON.stringify(r.expBody, null, 2));
3467
+ console.log('\n加 --yes 真导出。');
3468
+ return;
3469
+ }
3470
+ console.log(`导出完成并通过结构校验: ${r.outPath}(${r.size} 字节,${r.validation.rowCount} 行,${r.validation.cellCount} 个单元格)`);
3471
+ return;
3472
+ }
3473
+ // ---------- list ----------
3474
+ if (sub !== 'list') {
3475
+ console.error(`cube 子命令: list/fields/export/view/search-info/excel/count/group/card/browser/card-fields/create/edit/delete/print/menus/find/import-template/import,收到: ${sub}`);
3476
+ process.exit(1);
3477
+ }
3478
+ const condRes = await resolveCubeCondition(cfg, { customid, condition: flagStr(args, 'condition') ?? '' });
3479
+ if (condRes.unresolved.length)
3480
+ printConditionUnresolved(condRes.unresolved);
3481
+ const conditionValues = condRes.conditionValues;
3482
+ const r = await listCube(cfg, { customid, page: Number(flagStr(args, 'page') ?? '1') || 1, keyword: flagStr(args, 'keyword') ?? '', conditionValues });
3483
+ if (args.flags.json) {
3484
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3485
+ console.log(JSON.stringify({ customid: r.customid, name: r.name, modeId: r.modeId, formId: r.formId, dataKey: r.dataKey, page: r.page, total: r.total, count: r.count, columns: r.columns.map((c) => ({ dataIndex: c.dataIndex, title: c.title })), items: r.items.map((it) => withLink('cube', { id: it.id, name: it.name, fields: it.fields }, cubeBase, { modeId: r.modeId, formId: r.formId, customid: r.customid })) }, null, 2));
3486
+ return;
3487
+ }
3488
+ console.log(`E9 建模台账 · ${r.name ?? 'customid=' + r.customid} [第 ${r.page} 页${r.total != null ? ` / 共 ${r.total}` : ''}] 当前 ${r.count} 条`);
3489
+ if (r.count === 0) {
3490
+ console.log('(无数据)');
3491
+ return;
3492
+ }
3493
+ // 固定列头:模型据此观察全部字段,挑选 ≤5 个展示列后逐行对齐渲染(空值显式显示(无数据))。
3494
+ const headerCols = (r.columns || []).filter((c) => c.dataIndex && c.dataIndex !== 'randomFieldId');
3495
+ if (headerCols.length)
3496
+ console.log(`字段(${headerCols.length}): ${headerCols.map((c) => c.title ?? c.dataIndex).join(' | ')}`);
3497
+ const cubeBase = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3498
+ for (let i = 0; i < r.items.length; i++) {
3499
+ const it = r.items[i];
3500
+ // 卡片详情 URL(点开进 E9 台账卡片页)
3501
+ const nameLinked = hyperlinkFor('cube', it, cubeBase, it.name ?? '(无名称)', { modeId: r.modeId, formId: r.formId, customid: r.customid });
3502
+ console.log(`${i + 1}. ${nameLinked} [id=${it.id ?? ''}]`);
3503
+ // 逐行按全量 columns 对齐输出(空值显式标(无数据)),不再 filter 非空 + slice 5 ——
3504
+ // 避免“每行展示的字段集合不同、同一位置含义不一致”的合并列错觉。
3505
+ const meta = it.fields.map((f) => `${f.title}=${f.value || '(无数据)'}`).join(' | ');
3506
+ if (meta)
3507
+ console.log(` ${meta}`);
3508
+ }
3509
+ }
3510
+ function printTodoItems(items, base, { indent = '', max = 10 } = {}) {
3511
+ const visible = items.slice(0, max);
3512
+ for (let i = 0; i < visible.length; i++) {
3513
+ const it = visible[i];
3514
+ const head = it.title ?? '(无标题)';
3515
+ const meta = [it.creator, it.currentOperator ? `未操作者=${it.currentOperator}` : null, it.node, it.createdate].filter(Boolean).join(' | ');
3516
+ const headLinked = hyperlinkFor('todo', it, base, head);
3517
+ console.log(`${indent}${i + 1}. ${headLinked}${it.level ? ` (${it.level})` : ''}`);
3518
+ if (meta)
3519
+ console.log(`${indent} ${meta}`);
3520
+ }
3521
+ if (items.length > visible.length)
3522
+ console.log(`${indent}还有 ${items.length - visible.length} 条本页结果未显示,可使用 --page 查看后续内容。`);
3523
+ }
3524
+
3525
+ async function runTodoBatch(args) {
3526
+ const sub = args._[1];
3527
+ const cfg = loadConfig(flagStr(args, 'config'));
3528
+ const requestIds = parseIdList(flagStr(args, 'request-ids'), 'requestIds');
3529
+ const remark = flagStr(args, 'remark');
3530
+ let preview;
3531
+ if (sub === 'batch-submit') {
3532
+ const userIds = parseIdList(flagStr(args, 'user-ids'), 'userIds');
3533
+ if (requestIds.length !== userIds.length)
3534
+ throw new Error('request-ids 与 user-ids 数量必须一致。');
3535
+ preview = { operation: '批量提交', requestIds, userIds, remark: remark ?? 'OK' };
3536
+ }
3537
+ else if (sub === 'batch-forward') {
3538
+ const receiver = parseIdList(flagStr(args, 'receiver'), 'receiver');
3539
+ preview = { operation: '批量转发', requestIds, receiver, remark: remark ?? '转发' };
3540
+ }
3541
+ else {
3542
+ // 批量关注:dry-run 也先查关注类型(只读),供用户确认加入哪个关注类型
3543
+ const attentionTypes = await getAttentionTypes(cfg, requestIds);
3544
+ const attentionId = flagStr(args, 'attention-id') ?? attentionTypes[0]?.id;
3545
+ preview = { operation: '批量关注', requestIds, attentionId, attentionTypes: attentionTypes.map((t) => ({ id: t.id, showname: t.showname })) };
3546
+ }
3547
+ if (!args.flags.yes) {
3548
+ console.log(BOX_TOP);
3549
+ console.log(` ${preview.operation}预览(共 ${requestIds.length} 条)`);
3550
+ console.log(DIVIDER);
3551
+ console.log(JSON.stringify(preview, null, 2));
3552
+ console.log('\n 确认无误后,请使用相同命令加 --yes 执行。');
3553
+ return;
3554
+ }
3555
+ let result;
3556
+ if (sub === 'batch-submit')
3557
+ result = await batchSubmit(cfg, { requestIds, userIds: preview.userIds, remark: preview.remark });
3558
+ else if (sub === 'batch-forward')
3559
+ result = await batchForward(cfg, { requestIds, receiver: preview.receiver, remark: preview.remark });
3560
+ else
3561
+ result = await batchAttention(cfg, { requestIds, attentionId: preview.attentionId });
3562
+ const body = result.result?.json ?? {};
3563
+ const ok = result.result?.ok && body.success !== false && body.status !== false;
3564
+ console.log(`${ok ? '✅' : '⚠'} ${preview.operation}已请求处理(${requestIds.length} 条)。`);
3565
+ console.log(JSON.stringify(body, null, 2));
3566
+ }
3567
+
3568
+ async function runPageOpen(args) {
3569
+ const cfg = loadConfig(flagStr(args, 'config'));
3570
+ const page = flagStr(args, 'page') ?? 'center';
3571
+ const url = pageUrl(cfg, page, flagStr(args, 'request-id'));
3572
+ if (args.flags.json) {
3573
+ console.log(JSON.stringify({ page, url }, null, 2));
3574
+ return;
3575
+ }
3576
+ console.log(`打开“${page}”页面:`);
3577
+ console.log(url);
3578
+ console.log(`可选页面:${availablePages().join('、')}`);
3579
+ }
3580
+ async function runReport(args) {
3581
+ const sub = args._[1] ?? 'collect';
3582
+ if (sub !== 'collect')
3583
+ throw new Error('report 只支持 collect。');
3584
+ const report = await collectQueryableBusiness(loadConfig(flagStr(args, 'config')), flagStr(args, 'date') ?? todayStr());
3585
+ if (args.flags.json) {
3586
+ console.log(JSON.stringify(report, null, 2));
3587
+ return;
3588
+ }
3589
+ console.log(report.scope);
3590
+ console.log(`环境:${report.baseUrl};日期:${report.date}`);
3591
+ for (const [name, items] of Object.entries(report.sources))
3592
+ console.log(`- ${name}:${items.length} 条`);
3593
+ for (const note of report.notes ?? [])
3594
+ console.log(`- ${note}`);
3595
+ if (report.failures.length)
3596
+ console.log(`- 未查询成功的来源:${report.failures.map((item) => item.index).join(', ')}`);
3597
+ }
3598
+
3599
+ async function main() {
3600
+ const args = parseArgs(process.argv.slice(2));
3601
+ if (args.flags.help || args.flags.h) {
3602
+ console.log(usage());
3603
+ return;
3604
+ }
3605
+ const cmd = args._[0] ?? 'todo';
3606
+ if (cmd === 'version') {
3607
+ const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
3608
+ console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.0.0', buildHash }, null, 2));
3609
+ return;
3610
+ }
3611
+ if (cmd === 'config') {
3612
+ runConfig(args);
3613
+ return;
3614
+ }
3615
+ if (cmd === 'auth') {
3616
+ await runAuth(args);
3617
+ return;
3618
+ }
3619
+ if (cmd === 'create') {
3620
+ await runCreate(args);
3621
+ return;
3622
+ }
3623
+ if (cmd === 'page-open') {
3624
+ await runPageOpen(args);
3625
+ return;
3626
+ }
3627
+ if (cmd === 'report') {
3628
+ await runReport(args);
3629
+ return;
3630
+ }
3631
+ if (cmd === 'blog') {
3632
+ await runBlog(args);
3633
+ return;
3634
+ }
3635
+ if (cmd === 'email') {
3636
+ await runEmail(args);
3637
+ return;
3638
+ }
3639
+ if (cmd === 'workplan') {
3640
+ await runWorkplan(args);
3641
+ return;
3642
+ }
3643
+ if (cmd === 'odoc') {
3644
+ await runOdoc(args);
3645
+ return;
3646
+ }
3647
+ if (cmd === 'kq') {
3648
+ await runKq(args);
3649
+ return;
3650
+ }
3651
+ if (cmd === 'search') {
3652
+ await runSearch(args);
3653
+ return;
3654
+ }
3655
+ if (cmd === 'cube') {
3656
+ await runCube(args);
3657
+ return;
3658
+ }
3659
+ if (cmd !== 'todo') {
3660
+ console.error(`未知命令: ${cmd}\n\n${usage()}`);
3661
+ process.exit(1);
3662
+ }
3663
+ const sub = args._[1] ?? 'list';
3664
+ if (sub === 'summary') {
3665
+ await runTodoSummary(args);
3666
+ return;
3667
+ }
3668
+ if (sub === 'submit') {
3669
+ await runTodoSubmit(args);
3670
+ return;
3671
+ }
3672
+ if (sub === 'retract') {
3673
+ await runTodoRetract(args);
3674
+ return;
3675
+ }
3676
+ if (sub === 'reject') {
3677
+ await runTodoReject(args);
3678
+ return;
3679
+ }
3680
+ if (sub === 'transfer') {
3681
+ await runTodoTransfer(args);
3682
+ return;
3683
+ }
3684
+ if (sub === 'forward') {
3685
+ await runTodoForward(args);
3686
+ return;
3687
+ }
3688
+ if (sub === 'circulate') {
3689
+ await runTodoCirculate(args);
3690
+ return;
3691
+ }
3692
+ if (sub === 'cc') {
3693
+ await runTodoCc(args);
3694
+ return;
3695
+ }
3696
+ if (sub === 'ask-idea') {
3697
+ await runTodoAskIdea(args);
3698
+ return;
3699
+ }
3700
+ if (sub === 'batch-submit' || sub === 'batch-forward' || sub === 'batch-attention') {
3701
+ await runTodoBatch(args);
3702
+ return;
3703
+ }
3704
+ // P1-1: done/mine 子命令(按 category 分流,viewcondition 由 workflow.js 默认)
3705
+ const category = (sub === 'done') ? 'done' : (sub === 'mine') ? 'mine' : 'todo';
3706
+ if (sub !== 'list' && sub !== 'done' && sub !== 'mine') {
3707
+ console.error(`todo 暂只支持 list / done / mine / summary / submit / retract / reject / transfer / forward / circulate / cc / ask-idea 子命令,收到: ${sub}`);
3708
+ process.exit(1);
3709
+ }
3710
+ const cfg = loadConfig(flagStr(args, 'config'));
3711
+ let savedQueryFlags = {};
3712
+ let tab = flagStr(args, 'tab');
3713
+ let key = '', viewcondition = undefined, name = sub;
3714
+ if (category === 'todo') {
3715
+ const r = resolveViewcondition(cfg, tab);
3716
+ key = r.key;
3717
+ viewcondition = r.viewcondition;
3718
+ // 默认 tab=5 是“待处理”,不是 OA 的“全部”;无论旧配置如何命名,都用准确口径展示。
3719
+ name = r.viewcondition === '5' ? '待处理' : r.name;
3720
+ }
3721
+ let creatorFilter = flagStr(args, 'creator');
3722
+ let workflowFilter = flagStr(args, 'workflow');
3723
+ const resumeQuery = flagStr(args, 'resume-query');
3724
+ if (resumeQuery) {
3725
+ const saved = loadCandidateQuery(resumeQuery);
3726
+ savedQueryFlags = saved.options?.flags ?? {};
3727
+ tab = flagStr(args, 'tab') ?? (typeof savedQueryFlags.tab === 'string' ? savedQueryFlags.tab : tab);
3728
+ if (category === 'todo') {
3729
+ const resolved = resolveViewcondition(cfg, tab);
3730
+ key = resolved.key;
3731
+ viewcondition = resolved.viewcondition;
3732
+ name = resolved.viewcondition === '5' ? '待处理' : resolved.name;
3733
+ }
3734
+ creatorFilter = flagStr(args, 'creator') ?? (typeof savedQueryFlags.creator === 'string' ? savedQueryFlags.creator : creatorFilter);
3735
+ const candidate = Number(flagStr(args, 'candidate'));
3736
+ if (!Number.isInteger(candidate) || candidate < 1 || candidate > saved.candidates.length)
3737
+ throw new Error(`请用 --candidate 选择 1 到 ${saved.candidates.length} 的候选流程。`);
3738
+ workflowFilter = String(saved.candidates[candidate - 1].id);
3739
+ }
3740
+ else if (workflowFilter && !/^\d+(\s*,\s*\d+)*$/.test(workflowFilter)) {
3741
+ const candidates = await findCreateWfByName(cfg, workflowFilter);
3742
+ if (candidates.length === 0)
3743
+ throw new Error(`未找到名称包含“${workflowFilter}”的流程。`);
3744
+ if (candidates.length > 1) {
3745
+ const contextId = saveCandidateQuery({ workflow: workflowFilter, candidates, options: { flags: args.flags, sub } });
3746
+ console.log(`找到 ${candidates.length} 个相似流程,请选择:`);
3747
+ candidates.forEach((item, index) => console.log(`${index + 1}. ${item.name} [${item.id}]`));
3748
+ console.log(`\n选定后继续:e9 todo ${sub} --resume-query ${contextId} --candidate <序号>`);
3749
+ return;
3750
+ }
3751
+ workflowFilter = String(candidates[0].id);
3752
+ }
3753
+ const effectiveCreatorName = flagStr(args, 'creator-name') ?? (typeof savedQueryFlags['creator-name'] === 'string' ? savedQueryFlags['creator-name'] : undefined);
3754
+ if (!creatorFilter && effectiveCreatorName) {
3755
+ const people = await getAtAllList(cfg, { name: effectiveCreatorName });
3756
+ const exact = people.items.filter((person) => String(person.name) === effectiveCreatorName);
3757
+ if (exact.length !== 1) {
3758
+ console.log(`“${effectiveCreatorName}”匹配到 ${people.items.length} 位人员,请改用 --creator <人员 ID>:`);
3759
+ people.items.forEach((person, index) => console.log(`${index + 1}. ${person.name} [${person.uid}]${person.name === effectiveCreatorName ? ' ★精确匹配' : ''}`));
3760
+ return;
3761
+ }
3762
+ creatorFilter = String(exact[0].uid);
3763
+ }
3764
+ const queryFlag = (key) => {
3765
+ const current = flagStr(args, key);
3766
+ if (current !== undefined)
3767
+ return current;
3768
+ const saved = savedQueryFlags[key];
3769
+ return typeof saved === 'string' ? saved : undefined;
3770
+ };
3771
+ const queryBool = (key) => args.flags[key] === true || savedQueryFlags[key] === true;
3772
+ let workflowTypeFilter = queryFlag('workflow-type');
3773
+ if (workflowTypeFilter && !/^\d+$/.test(workflowTypeFilter)) {
3774
+ const resolvedType = await resolveWorkflowType(cfg, workflowTypeFilter, { category, viewcondition });
3775
+ workflowTypeFilter = resolvedType.id;
3776
+ }
3777
+ let excludeCreatorId;
3778
+ if (queryBool('exclude-current-user')) {
3779
+ const currentUser = await getCurrentUser(cfg);
3780
+ if (!currentUser.userid)
3781
+ throw new Error('未能读取当前用户 ID,无法安全执行“不是我发起”筛选。');
3782
+ excludeCreatorId = currentUser.userid;
3783
+ }
3784
+ const listOpts = {
3785
+ category,
3786
+ viewcondition,
3787
+ subCategory: queryFlag('sub-category'),
3788
+ page: Number(queryFlag('page') ?? '1') || 1,
3789
+ keyword: queryFlag('keyword') ?? '',
3790
+ sort: queryFlag('sort'),
3791
+ workflow: workflowFilter,
3792
+ creator: creatorFilter,
3793
+ creatorDept: queryFlag('creator-dept'),
3794
+ creatorSubcom: queryFlag('creator-subcom'),
3795
+ createTime: queryFlag('create-time'),
3796
+ receiveTime: queryFlag('receive-time'),
3797
+ operateTime: queryFlag('operate-time'),
3798
+ requestLevel: queryFlag('request-level'),
3799
+ requestMark: queryFlag('request-mark'),
3800
+ workflowType: workflowTypeFilter,
3801
+ nodeId: queryFlag('node-id'),
3802
+ flowStatus: queryFlag('flow-status'),
3803
+ totalOnly: queryBool('total-only'),
3804
+ debug: queryBool('debug'),
3805
+ excludeCreatorId,
3806
+ };
3807
+ // 默认概览优先展示“我需要做什么”,而不是把转发和关注与待处理混在数千条列表里。
3808
+ const shouldGroup = category === 'todo' && !listOpts.subCategory && !tab && !listOpts.totalOnly && !queryBool('ungrouped') && !queryBool('raw');
3809
+ if (shouldGroup) {
3810
+ const grouped = await getSmartTodoGroups(cfg, listOpts);
3811
+ const groupedSnapshot = {
3812
+ actionTotal: grouped.actionTotal,
3813
+ categoryTotals: grouped.categoryTotals,
3814
+ categoryTotalSum: grouped.categoryTotalSum,
3815
+ page: grouped.page,
3816
+ groups: grouped.groups.map((group) => ({
3817
+ key: group.key,
3818
+ title: group.title,
3819
+ total: group.result.total,
3820
+ items: group.items,
3821
+ raw: group.result.raw,
3822
+ })),
3823
+ };
3824
+ const savedGroupedResult = persistSearchResult(groupedSnapshot, 'E9 待办智能概览');
3825
+ if (args.flags.json) {
3826
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3827
+ console.log(JSON.stringify({ page: grouped.page, actionTotal: grouped.actionTotal, categoryTotals: grouped.categoryTotals, groups: grouped.groups.map((g) => ({ key: g.key, title: g.title, total: g.result.total, items: g.items.map((item) => withLink('todo', item, base)) })) }, null, 2));
3828
+ return;
3829
+ }
3830
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3831
+ console.log('E9 待办智能概览(按处理方式分组,避免重复提醒)');
3832
+ for (const group of grouped.groups) {
3833
+ console.log(`\n### ${group.title}(共 ${group.result.total ?? '?'} 条,第 ${group.result.page} 页显示前 10 条)`);
3834
+ if (group.items.length)
3835
+ printTodoItems(group.items, base);
3836
+ else
3837
+ console.log('(本页无数据)');
3838
+ }
3839
+ if (savedGroupedResult)
3840
+ console.log(`完整结果已保存到 ${savedGroupedResult.jsonPath}`);
3841
+ return;
3842
+ }
3843
+ const result = await getWfList(cfg, listOpts);
3844
+ const savedResult = persistSearchResult(result, `E9 ${name ?? key} 查询`);
3845
+ if (args.flags.raw) {
3846
+ console.error('===== RAW getWfListParams =====');
3847
+ console.error(JSON.stringify(result.raw.getWfListParams, null, 2));
3848
+ console.error('===== RAW splitPageKey =====');
3849
+ console.error(JSON.stringify(result.raw.splitPageKey, null, 2));
3850
+ console.error('===== RAW datas =====');
3851
+ console.error(JSON.stringify(result.raw.datas, null, 2));
3852
+ console.error('===== RAW counts =====');
3853
+ console.error(JSON.stringify(result.raw.counts, null, 2));
3854
+ console.error('===== 解析结果 =====');
3855
+ }
3856
+ if (args.flags.json) {
3857
+ console.log(JSON.stringify({
3858
+ tab: key,
3859
+ tabName: name,
3860
+ page: result.page,
3861
+ pageSize: result.pageSize,
3862
+ total: result.total,
3863
+ count: result.items.length,
3864
+ dataKey: result.dataKey,
3865
+ columns: result.columns.map((c) => ({ dataIndex: c.dataIndex, title: c.title })),
3866
+ items: result.items.map((it) => ({
3867
+ ...linkMetaFor('todo', it, endpointFor(cfg).baseUrl),
3868
+ requestId: it.requestId,
3869
+ title: it.title,
3870
+ creator: it.creator,
3871
+ creatorId: it.creatorId,
3872
+ currentOperator: it.currentOperator,
3873
+ node: it.node,
3874
+ createdate: it.createdate,
3875
+ receivedate: it.receivedate,
3876
+ status: it.status,
3877
+ level: it.level,
3878
+ requestmark: it.requestmark,
3879
+ })),
3880
+ sourceTotal: result.sourceTotal,
3881
+ excludedCreatorId: result.excludedCreatorId,
3882
+ fetchedPages: result.fetchedPages,
3883
+ }, null, 2));
3884
+ return;
3885
+ }
3886
+ if (listOpts.totalOnly) {
3887
+ console.log(`E9 待办 [${name ?? key}]:共 ${result.total ?? '?'} 条`);
3888
+ return;
3889
+ }
3890
+ // 人类可读输出
3891
+ console.log(`E9 待办 [${name ?? key}] 第 ${result.page} 页 共 ${result.total ?? '?'} 条`);
3892
+ if (result.items.length === 0) {
3893
+ console.log('(无数据)');
3894
+ if (args.flags.raw) {
3895
+ console.error('提示:如果 total 显示为 ? 或无数据,请用 --raw 查看真实响应后反馈。');
3896
+ }
3897
+ return;
3898
+ }
3899
+ const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
3900
+ printTodoItems(result.items, base);
3901
+ if (savedResult)
3902
+ console.log(`完整结果已保存到 ${savedResult.jsonPath}`);
3903
+ }
3904
+ async function runTodoSummary(args) {
3905
+ const cfg = loadConfig(flagStr(args, 'config'));
3906
+ const requestId = flagStr(args, 'requestid');
3907
+ if (!requestId) {
3908
+ console.error('todo summary 需要 --requestid <请求id>');
3909
+ process.exit(1);
3910
+ }
3911
+ const s = await getFlowSummary(cfg, requestId);
3912
+ if (args.flags.json) {
3913
+ console.log(JSON.stringify({ requestId: s.requestId, workflowName: s.workflowName, workflowId: s.workflowId, nodeId: s.nodeId, verifyRight: s.verifyRight, fields: s.fields, logs: s.logs }, null, 2));
3914
+ return;
3915
+ }
3916
+ console.log(BOX_TOP);
3917
+ console.log(` 流程摘要 · ${s.workflowName ?? '(未知流程)'} (requestid=${s.requestId})`);
3918
+ console.log(` workflowid=${s.workflowId ?? '?'} nodeid=${s.nodeId ?? '?'} verifyRight=${s.verifyRight ? '✅' : '❌'}`);
3919
+ console.log(DIVIDER);
3920
+ if (s.fields.length === 0) {
3921
+ console.log(' 表单字段:无数据');
3922
+ }
3923
+ else {
3924
+ console.log(' 表单内容:');
3925
+ for (const f of s.fields) {
3926
+ console.log(` ▸ ${f.label ?? f.id}${f.name ? ` (${f.name})` : ''} = ${f.value ?? ''}`);
3927
+ }
3928
+ }
3929
+ if (s.logs.length > 0) {
3930
+ console.log(DIVIDER);
3931
+ console.log(' 流转历史:');
3932
+ for (const log of s.logs) {
3933
+ const who = log.displayname || log.receiveUser || '?';
3934
+ const when = `${log.log_operatedate ?? ''} ${log.log_operatetime ?? ''}`.trim();
3935
+ const op = log.operationname || '';
3936
+ const node = log.log_nodename || '';
3937
+ const remark = log.remark || log.remarkContent || '';
3938
+ console.log(` ${who} ${op} ${node} ${when}${remark ? ` 「${remark}」` : ''}`);
3939
+ }
3940
+ }
3941
+ else {
3942
+ console.log(' 流转历史:暂无');
3943
+ }
3944
+ console.log(BOX_TOP);
3945
+ }
3946
+ async function runTodoSubmit(args) {
3947
+ const cfg = loadConfig(flagStr(args, 'config'));
3948
+ const requestId = flagStr(args, 'requestid');
3949
+ if (!requestId) {
3950
+ console.error('todo submit 需要 --requestid <请求id>');
3951
+ process.exit(1);
3952
+ }
3953
+ const remark = flagStr(args, 'remark') ?? 'OK';
3954
+ const send = !!args.flags.yes;
3955
+ if (!send) {
3956
+ console.log(BOX_TOP);
3957
+ console.log(` [dry-run] 提交待办 requestid=${requestId}`);
3958
+ console.log(` 签字意见 = ${remark}`);
3959
+ console.log(' 确认无误后加 --yes 真正提交。');
3960
+ console.log(BOX_TOP);
3961
+ return;
3962
+ }
3963
+ const res = await submitTodo(cfg, { requestId, remark });
3964
+ console.log(BOX_TOP);
3965
+ const j = res.json;
3966
+ const ok = j?.data?.type === 'SUCCESS' || j?.type === 'SUCCESS';
3967
+ console.log(` ${ok ? '✅ 提交成功' : '⚠ 提交异常'} status=${res.status}`);
3968
+ if (j)
3969
+ console.log('响应:', JSON.stringify(j).slice(0, 400));
3970
+ else
3971
+ console.log('响应(非JSON):', (res.text ?? '').slice(0, 300));
3972
+ console.log(BOX_TOP);
3973
+ }
3974
+ async function runTodoRetract(args) {
3975
+ const cfg = loadConfig(flagStr(args, 'config'));
3976
+ const requestId = flagStr(args, 'requestid');
3977
+ if (!requestId) {
3978
+ console.error('todo retract 需要 --requestid <请求id>');
3979
+ process.exit(1);
3980
+ }
3981
+ const send = !!args.flags.yes;
3982
+ if (!send) {
3983
+ console.log(BOX_TOP);
3984
+ console.log(` [dry-run] 撤回流程 requestid=${requestId}`);
3985
+ console.log(' 确认无误后加 --yes 真正撤回。');
3986
+ console.log(BOX_TOP);
3987
+ return;
3988
+ }
3989
+ const res = await retractRequest(cfg, requestId);
3990
+ console.log(BOX_TOP);
3991
+ console.log(` ${res.verified ? '✅ 撤回成功且已核验' : '⚠ 撤回结果未确认'}${res.response ? ` status=${res.response.status}` : ''}`);
3992
+ console.log(` ${res.message ?? ''}`);
3993
+ if (!res.verified)
3994
+ console.log(` 请在 OA 中核对流程状态:${buildRequestFormUrl(endpointFor(cfg).baseUrl, requestId)}`);
3995
+ console.log(BOX_TOP);
3996
+ }
3997
+ function runConfig(args) {
3998
+ const sub = args._[1];
3999
+ if (sub === 'set') {
4000
+ const field = args._[2];
4001
+ const value = args._[3];
4002
+ if (!field || value === undefined) {
4003
+ console.error('用法: e9 config set <field> <value> (field: baseUrl/cookie/username/menuIds/...)');
4004
+ console.error(' 首次使用: e9 config set baseUrl http://你的E9地址:端口');
4005
+ process.exit(1);
4006
+ }
4007
+ setConfigField(field, String(value), flagStr(args, 'config'));
4008
+ return;
4009
+ }
4010
+ if (sub === 'init') {
4011
+ console.log('E9 首次配置:');
4012
+ console.log(' 1) e9 config set baseUrl http://你的E9地址:端口');
4013
+ console.log(' 2) e9 auth token-login (有头浏览器登录 E9 + 点确认授权,拿 aiAgentToken,cookie 自动写回)');
4014
+ console.log(' 3) e9 blog check / e9 todo list / e9 create list 验证');
4015
+ return;
4016
+ }
4017
+ printConfig(args);
4018
+ }
4019
+ function printConfig(args) {
4020
+ let cfg;
4021
+ try {
4022
+ cfg = loadConfig(flagStr(args, 'config'));
4023
+ }
4024
+ catch (e) {
4025
+ console.log(`配置加载失败:${e.message}`);
4026
+ return;
4027
+ }
4028
+ const tabs = cfg.tabs ?? {};
4029
+ console.log(`配置文件: ${cfg._configPath}`);
4030
+ console.log(`baseUrl: ${cfg.baseUrl}`);
4031
+ console.log(`cookie: ${cfg.cookie ? `已配置 (${cfg.cookie.length} 字符)` : '未配置'}`);
4032
+ console.log(`defaultTab: ${cfg.defaultTab ?? '(未设)'}`);
4033
+ console.log('tabs:');
4034
+ for (const [k, t] of Object.entries(tabs)) {
4035
+ console.log(` ${k.padEnd(10)} viewcondition=${t.viewcondition} ${t.name ?? ''}`);
4036
+ }
4037
+ }
4038
+ main().catch((e) => {
4039
+ console.error(`错误: ${e instanceof Error ? e.message : String(e)}`);
4040
+ process.exit(1);
4041
+ });