bdip-office-assistant-cli 0.0.0-stage → 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/README.md +37 -2
- package/dist/accounts.js +30 -0
- package/dist/args.js +36 -0
- package/dist/auth-token.js +778 -0
- package/dist/auth.js +290 -0
- package/dist/blog.js +562 -0
- package/dist/browser-links.js +223 -0
- package/dist/captcha-display.js +30 -0
- package/dist/cli.js +5479 -0
- package/dist/client.js +254 -0
- package/dist/config.js +170 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1771 -0
- package/dist/email.js +841 -0
- package/dist/field-preparation.js +185 -0
- package/dist/fileutil.js +20 -0
- package/dist/hrm.js +478 -0
- package/dist/keepalive.js +99 -0
- package/dist/kq.js +280 -0
- package/dist/links.js +34 -0
- package/dist/nodemap.js +242 -0
- package/dist/odoc.js +199 -0
- package/dist/office-tools.js +138 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +177 -0
- package/dist/password-input.js +84 -0
- package/dist/password-login.js +208 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/session-worker.js +54 -0
- package/dist/session.js +280 -0
- package/dist/submissionplan.js +45 -0
- package/dist/submissionstate.js +81 -0
- package/dist/subordinate.js +458 -0
- package/dist/supervise.js +152 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +172 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +325 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfagent.js +538 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +169 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +558 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +78 -0
- package/dist/wfuser.js +96 -0
- package/dist/workflow-runtime.js +151 -0
- package/dist/workflow.js +570 -0
- package/dist/workplan.js +431 -0
- package/package.json +28 -6
package/dist/odoc.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDIP5 公文(odoc)模块。
|
|
3
|
+
*
|
|
4
|
+
* 抓包+实跑确认的接口(测试环境):
|
|
5
|
+
* POST /api/odoc/odocCustomPage/getCustomPageList 公文待办/待阅列表入口(拿 dataKey,customListId/customTabId 定列表与 tab)
|
|
6
|
+
* POST /api/ec/dev/table/datas 取列+行(同待办 table 链路第 2 步)
|
|
7
|
+
* POST /api/ec/dev/table/counts 取总数(同待办 table 链路第 3 步)
|
|
8
|
+
* POST /api/odoc/odocTodoList/getTodoCount 待办/待阅计数(发文/收文/签报,按 officalType 分)
|
|
9
|
+
* POST /api/odoc/odocCenter/createOfficalList 发文/收文流程列表(officalType=1 发文拟稿 / 2 收文登记,结构同 create 列表)
|
|
10
|
+
* POST /api/odoc/odocCenter/getCenterInfo 公文中心首页两个新建入口(发文拟稿/收文登记)
|
|
11
|
+
* POST /api/odoc/odocCenter/getCountByOdocType 按公文种类计数
|
|
12
|
+
* POST /api/odoc/odocCenter/getCountByTopicType 按主题分类计数
|
|
13
|
+
*
|
|
14
|
+
* 走当前 baseUrl(单一环境,所有功能同地址),aiAgentToken 鉴权(e9pc 不支持 token 时回退 cookie)。
|
|
15
|
+
*/
|
|
16
|
+
import { createApiFromProfile } from './client.js';
|
|
17
|
+
import { pickByPaths, stripHtml, extractDataKey, extractColumns, extractRows, extractTotal } from './parse.js';
|
|
18
|
+
const DATERANGE_EMPTY = '0,,'; // type=0(不过滤), from 空, to 空
|
|
19
|
+
/**
|
|
20
|
+
* 公文待办/待阅列表。3 步 table 链路(POST,form-urlencoded),同待办/workplan-search:
|
|
21
|
+
* 1. /api/odoc/odocCustomPage/getCustomPageList(过滤项 + customListId/customTabId)→ dataKey(sessionkey 字段)
|
|
22
|
+
* 2. /api/ec/dev/table/datas(dataKey + current + sortParams=[])→ columns + 行
|
|
23
|
+
* 3. /api/ec/dev/table/counts(dataKey)→ 总数(失败不阻断)
|
|
24
|
+
* 行字段:原值 + `<字段>span` 渲染值(去 HTML)。
|
|
25
|
+
*/
|
|
26
|
+
export async function listOdocTodo(cfg, opts = {}) {
|
|
27
|
+
const api = await createApiFromProfile(cfg);
|
|
28
|
+
const filters = {
|
|
29
|
+
btnType: opts.btnType ?? '1',
|
|
30
|
+
readstatus: opts.readStatus ?? '',
|
|
31
|
+
odocSubject: opts.odocSubject ?? '',
|
|
32
|
+
sendnode: '',
|
|
33
|
+
topictype: opts.topicType ?? '',
|
|
34
|
+
secretlevel: opts.secretLevel ?? '',
|
|
35
|
+
send_unit: opts.sendUnit ?? '',
|
|
36
|
+
issueduserid: opts.issuedUserid ?? '',
|
|
37
|
+
writtendateselect: opts.writtenDateSelect ?? DATERANGE_EMPTY,
|
|
38
|
+
ownerid: opts.ownerId ?? '',
|
|
39
|
+
createdateselect: opts.createDateSelect ?? DATERANGE_EMPTY,
|
|
40
|
+
lastmodifydateselect: opts.lastModifyDateSelect ?? DATERANGE_EMPTY,
|
|
41
|
+
dealinguserid: opts.dealingUserid ?? '',
|
|
42
|
+
undealeduserid: opts.undealedUserid ?? '',
|
|
43
|
+
docabstract: opts.docAbstract ?? '',
|
|
44
|
+
opendateselect: opts.openDateSelect ?? DATERANGE_EMPTY,
|
|
45
|
+
deadlineselect: opts.deadlineSelect ?? DATERANGE_EMPTY,
|
|
46
|
+
receivenum: opts.receiveNum ?? '',
|
|
47
|
+
receivedateselect: opts.receiveDateSelect ?? DATERANGE_EMPTY,
|
|
48
|
+
workflowid: opts.workflowId ?? '',
|
|
49
|
+
requestname: opts.requestName ?? '',
|
|
50
|
+
dispatchnum: opts.dispatchNum ?? '',
|
|
51
|
+
urgencydegree: opts.urgencyDegree ?? '',
|
|
52
|
+
receive_unit: opts.receiveUnit ?? '',
|
|
53
|
+
issueddateselect: opts.issuedDateSelect ?? DATERANGE_EMPTY,
|
|
54
|
+
departmentid: opts.departmentId ?? '',
|
|
55
|
+
odoctype: opts.odocType ?? '',
|
|
56
|
+
customListId: opts.customListId ?? '-12',
|
|
57
|
+
customTabId: opts.customTabId ?? '-24',
|
|
58
|
+
};
|
|
59
|
+
const r1 = await api.post('/api/odoc/odocCustomPage/getCustomPageList', filters);
|
|
60
|
+
const dataKey = extractDataKey(r1.json);
|
|
61
|
+
if (!dataKey) {
|
|
62
|
+
throw new Error(`odoc list 未拿到 dataKey,getCustomPageList 响应: ${(r1.text ?? '').slice(0, 200)}`);
|
|
63
|
+
}
|
|
64
|
+
const page = opts.page ?? 1;
|
|
65
|
+
const r2 = await api.post('/api/ec/dev/table/datas', {
|
|
66
|
+
dataKey,
|
|
67
|
+
current: String(page),
|
|
68
|
+
sortParams: '[]',
|
|
69
|
+
// table/datas 认 pageSize(同待办链路):显式 10 条/页,避免服务端默认 30 条/页导致
|
|
70
|
+
// --page N 按 30 条跳页、本页第 11~30 条看不到的错位。
|
|
71
|
+
pageSize: opts.pageSize ?? '10',
|
|
72
|
+
});
|
|
73
|
+
const columns = extractColumns(r2.json);
|
|
74
|
+
const rows = extractRows(r2.json);
|
|
75
|
+
const items = rows.map((r) => normalizeOdocRow(r)).filter((it) => it.requestId || it.requestName);
|
|
76
|
+
const pageSize = pickByPaths(r2.json, ['pageSize', 'data.pageSize']);
|
|
77
|
+
let total = null;
|
|
78
|
+
let countsResp = null;
|
|
79
|
+
try {
|
|
80
|
+
const r3 = await api.post('/api/ec/dev/table/counts', { dataKey });
|
|
81
|
+
countsResp = r3.json;
|
|
82
|
+
total = extractTotal(r3.json);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// counts 失败不阻断
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
dataKey,
|
|
89
|
+
page,
|
|
90
|
+
total,
|
|
91
|
+
pageSize: typeof pageSize === 'number' ? pageSize : null,
|
|
92
|
+
columns,
|
|
93
|
+
items,
|
|
94
|
+
raw: { getCustomPageList: r1.json, datas: r2.json, counts: countsResp },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** 解析公文待办表行(原值 + span 渲染值)。 */
|
|
98
|
+
export function normalizeOdocRow(row) {
|
|
99
|
+
if (!row || typeof row !== 'object')
|
|
100
|
+
return {};
|
|
101
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
102
|
+
const spanOr = (dataIndex, extraKeys = []) => stripHtml(get([`${dataIndex}span`, dataIndex, ...extraKeys]));
|
|
103
|
+
return {
|
|
104
|
+
requestId: stringify(get(['requestid', 'randomField2'])),
|
|
105
|
+
requestName: stripHtml(get(['requestnamespan'])) || stringify(get(['requestname'])),
|
|
106
|
+
odocType: stringify(get(['odoctype'])),
|
|
107
|
+
odocTypeName: stripHtml(get(['odoctypespan'])),
|
|
108
|
+
issuedNum: stringify(get(['issued_num', 'issued_numspan'])),
|
|
109
|
+
receiveNum: stripHtml(get(['receivenumspan'])) || stringify(get(['receivenum'])),
|
|
110
|
+
topicType: stringify(get(['topictype'])),
|
|
111
|
+
urgencyDegree: stringify(get(['urgencydegree'])),
|
|
112
|
+
secretLevel: stripHtml(get(['secretlevelspan'])) || stringify(get(['secretlevel'])),
|
|
113
|
+
issuedUnit: stringify(get(['issued_unit'])),
|
|
114
|
+
receiveUnit: stripHtml(get(['receiveunitspan'])) || stringify(get(['receiveunit'])),
|
|
115
|
+
issuedUserid: stripHtml(get(['issued_useridspan'])) || stringify(get(['issued_userid'])),
|
|
116
|
+
writtenDate: stripHtml(get(['written_datespan'])) || stringify(get(['written_date'])),
|
|
117
|
+
issuedDate: stringify(get(['issued_date'])),
|
|
118
|
+
createrId: stripHtml(get(['createridspan'])) || stringify(get(['createrid', 'creater'])),
|
|
119
|
+
createDate: stringify(get(['createdate'])),
|
|
120
|
+
sendDepartment: stringify(get(['senddepartment'])),
|
|
121
|
+
lastOperateDate: stringify(get(['lastoperatedate'])),
|
|
122
|
+
// ⚠️ BDIP5 公文列表这两列的**字段名与内容是反的**(2026-10-14 实测 wf=552 发文):
|
|
123
|
+
// currentnodeidspan → "拟稿"(公文环节名,是标签不是 id)
|
|
124
|
+
// currentnodespan → "2868"(真正的**节点 id**,用 getNodeInfo 可解析出节点名「拟稿发起」)
|
|
125
|
+
// 故不再沿用误导性的原名,改为语义正确的 stageName / node;nodeName 由 nodemap 回填。
|
|
126
|
+
stageName: stripHtml(get(['currentnodeidspan'])) || stringify(get(['currentnodeid'])),
|
|
127
|
+
node: stripHtml(get(['currentnodespan'])) || stringify(get(['currentnode'])),
|
|
128
|
+
nodeName: undefined,
|
|
129
|
+
zbr: stringify(get(['zbr'])),
|
|
130
|
+
wbr: stringify(get(['wbr'])),
|
|
131
|
+
docAbstract: stripHtml(get(['docabstractspan'])) || stringify(get(['docabstract'])),
|
|
132
|
+
deadline: stripHtml(get(['deadlinespan'])) || stringify(get(['deadline'])),
|
|
133
|
+
openDate: stripHtml(get(['opendatespan'])) || stringify(get(['opendate'])),
|
|
134
|
+
receiveDate: stripHtml(get(['receivedatespan'])) || stringify(get(['receivedate'])),
|
|
135
|
+
operateDate: stripHtml(get(['operatedatespan'])) || stringify(get(['operatedate'])),
|
|
136
|
+
workflowId: stringify(get(['workflowid', 'randomField0'])),
|
|
137
|
+
raw: row,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 公文待办/待阅计数。POST /api/odoc/odocTodoList/getTodoCount。
|
|
142
|
+
* - view=false(默认):待办计数(iscomplete=0)
|
|
143
|
+
* - view=true:待阅计数(isview=0)
|
|
144
|
+
* 返回 datalist:[{officalType, title, count}](发文/收文/签报)。
|
|
145
|
+
*/
|
|
146
|
+
export async function getOdocTodoCount(cfg, opts = {}) {
|
|
147
|
+
const api = await createApiFromProfile(cfg);
|
|
148
|
+
const body = opts.view
|
|
149
|
+
? { isview: '0', isNew: 'false' }
|
|
150
|
+
: { iscomplete: '0', isNew: 'false' };
|
|
151
|
+
const r = await api.post('/api/odoc/odocTodoList/getTodoCount', body);
|
|
152
|
+
const j = r.json || {};
|
|
153
|
+
const arr = pickByPaths(j, ['datalist', 'data.datalist']) ?? [];
|
|
154
|
+
const items = (Array.isArray(arr) ? arr : [])
|
|
155
|
+
.map((d) => ({
|
|
156
|
+
officalType: stringify(d?.officalType) ?? '',
|
|
157
|
+
title: stringify(d?.title) ?? '',
|
|
158
|
+
count: typeof d?.count === 'number' ? d.count : Number(d?.count ?? 0),
|
|
159
|
+
}))
|
|
160
|
+
.filter((it) => it.title);
|
|
161
|
+
return { view: !!opts.view, items, raw: j };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 发文/收文流程列表。POST /api/odoc/odocCenter/createOfficalList。
|
|
165
|
+
* - officalType=1(默认):发文拟稿
|
|
166
|
+
* - officalType=2:收文登记
|
|
167
|
+
* 返回 datas[{typeName, id, wfCount, wfbeans[]}],结构同 create 列表,
|
|
168
|
+
* 故"新建公文"可复用 create start(wfstart.ts)的发起链路。
|
|
169
|
+
*/
|
|
170
|
+
export async function listOdocCreate(cfg, opts = {}) {
|
|
171
|
+
const api = await createApiFromProfile(cfg);
|
|
172
|
+
const officalType = opts.officalType ?? '1';
|
|
173
|
+
const r = await api.post('/api/odoc/odocCenter/createOfficalList', { officalType });
|
|
174
|
+
const j = r.json || {};
|
|
175
|
+
const arr = pickByPaths(j, ['datas', 'data.datas']) ?? [];
|
|
176
|
+
const toplabel = stringify(pickByPaths(j, ['createwflabels.toplabel']));
|
|
177
|
+
const categories = (Array.isArray(arr) ? arr : [])
|
|
178
|
+
.map((d) => ({
|
|
179
|
+
id: stringify(d?.id),
|
|
180
|
+
name: stringify(d?.typeName),
|
|
181
|
+
wfCount: typeof d?.wfCount === 'number' ? d.wfCount : Number(d?.wfCount ?? 0),
|
|
182
|
+
workflows: (Array.isArray(d?.wfbeans) ? d.wfbeans : [])
|
|
183
|
+
.map((w) => ({
|
|
184
|
+
id: stringify(w?.id),
|
|
185
|
+
name: stringify(w?.name),
|
|
186
|
+
typeId: stringify(w?.typeId),
|
|
187
|
+
spell: stringify(w?.spell),
|
|
188
|
+
letter: stringify(w?.letter),
|
|
189
|
+
}))
|
|
190
|
+
.filter((w) => w.id || w.name),
|
|
191
|
+
}))
|
|
192
|
+
.filter((c) => c.workflows.length > 0);
|
|
193
|
+
return { officalType, toplabel, categories, raw: j };
|
|
194
|
+
}
|
|
195
|
+
function stringify(v) {
|
|
196
|
+
if (v === undefined || v === null || v === '')
|
|
197
|
+
return undefined;
|
|
198
|
+
return String(v);
|
|
199
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
5
|
+
|
|
6
|
+
export async function accounts(api) {
|
|
7
|
+
const r = await api.get('/api/hrm/login/getAccountList', {__random__: Date.now()}, {forceCookie: true});
|
|
8
|
+
const rows = r.json?.data?.accountlist;
|
|
9
|
+
if (!Array.isArray(rows) || !rows.length) throw Error('未取得关联账号,请检查登录状态');
|
|
10
|
+
const items = rows.map(a => ({id: String(a.userid), name: a.username,
|
|
11
|
+
division: a.subcompanyname || '所属分部未返回(请核实)', department: a.deptname || '',
|
|
12
|
+
current: String(a.iscurrent) === '1'}));
|
|
13
|
+
if (items.filter(a => a.current).length !== 1) throw Error('未能确认当前账号');
|
|
14
|
+
return items;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function shift(api, id) {
|
|
18
|
+
const before = await accounts(api);
|
|
19
|
+
if (!before.some(a => a.id === id)) throw Error('目标不在关联账号列表中');
|
|
20
|
+
if (!before.find(a => a.id === id).current) {
|
|
21
|
+
await api.post('/api/hrm/login/identityShift', {shiftid: id}, {forceCookie: true});
|
|
22
|
+
}
|
|
23
|
+
const after = await accounts(api);
|
|
24
|
+
if (!after.find(a => a.id === id)?.current) throw Error('切换未验证成功,停止后续业务操作');
|
|
25
|
+
return after;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
29
|
+
const [command, ...args] = argv;
|
|
30
|
+
if (!command || command === 'help') {
|
|
31
|
+
console.log('accounts | after-login | switch <账号ID> | default-set <账号ID> | default-clear | draft-preview <流程ID> <值文件> <标题> <计划文件> [明细文件] | draft-save <计划文件> | draft-verify <计划文件> | draft-submit <计划文件> --confirm | draft-update-preview <流程ID> <值文件> <标题> <新计划文件> <明细文件或-> <原requestId> | browser-options <流程ID> <字段ID> [上下文文件] | adapter-register <流程ID> <脚本文件> <映射文件>');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const dist = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const load = name => import(pathToFileURL(path.join(dist, name + '.js')).href);
|
|
36
|
+
const {loadConfig} = await load('config');
|
|
37
|
+
const {createApiFromProfile} = await load('client');
|
|
38
|
+
const cfg = loadConfig(), api = await createApiFromProfile(cfg);
|
|
39
|
+
const list = await accounts(api);
|
|
40
|
+
const accountId = list.find(a => a.current).id;
|
|
41
|
+
// 独立偏好文件,避免工具写配置时丢失;按环境和关联账号组隔离。
|
|
42
|
+
const prefFile = path.join(path.dirname(cfg._configPath), 'office-project-defaults.json');
|
|
43
|
+
const scope = createHash('sha256').update(cfg.baseUrl + '\n' + list.map(a => a.id).sort().join(',')).digest('hex');
|
|
44
|
+
const prefs = fs.existsSync(prefFile) ? JSON.parse(fs.readFileSync(prefFile, 'utf8')) : {};
|
|
45
|
+
if (['accounts', 'after-login', 'switch', 'default-set', 'default-clear'].includes(command)) {
|
|
46
|
+
let result = list;
|
|
47
|
+
if (command === 'default-set') {
|
|
48
|
+
if (!list.some(a => a.id === args[0])) throw Error('默认账号必须属于当前关联账号列表');
|
|
49
|
+
prefs[scope] = args[0];
|
|
50
|
+
fs.writeFileSync(prefFile, JSON.stringify(prefs, null, 2));
|
|
51
|
+
}
|
|
52
|
+
if (command === 'default-clear') {
|
|
53
|
+
delete prefs[scope];
|
|
54
|
+
fs.writeFileSync(prefFile, JSON.stringify(prefs, null, 2));
|
|
55
|
+
}
|
|
56
|
+
if (command === 'switch') result = await shift(api, args[0]);
|
|
57
|
+
if (command === 'after-login' && prefs[scope]) result = await shift(api, prefs[scope]);
|
|
58
|
+
console.log(JSON.stringify({accounts: result, defaultAccountId: prefs[scope] || null,
|
|
59
|
+
prompt: prefs[scope] ? '已核验默认项目;是否需要切换其他项目?可以更改或取消默认项目。' : '是否切换项目(次账号)?可以设置默认项目,以后登录自动切换。'}, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const runtime = await load('workflow-runtime');
|
|
63
|
+
const preparation = await load('field-preparation');
|
|
64
|
+
const getForm = async (workflowId, requestId) => {
|
|
65
|
+
const r = await api.post('/api/workflow/reqform/loadForm', {workflowid:workflowId, iscreate:requestId?'0':'1', requestid:String(requestId??'-1')}, {forceCookie:true});
|
|
66
|
+
if(r.json?.params?.verifyRight!==true || String(r.json.params.currentUserid)!==accountId) throw Error('表单权限或账号不符');
|
|
67
|
+
return r.json;
|
|
68
|
+
};
|
|
69
|
+
if (command === 'browser-options') {
|
|
70
|
+
const [workflowId,fieldId,contextFile]=args;
|
|
71
|
+
const context=contextFile?JSON.parse(fs.readFileSync(contextFile,'utf8')):{};
|
|
72
|
+
const form=await getForm(workflowId,context.requestId);
|
|
73
|
+
const adapter=await runtime.loadAdapter(cfg,workflowId,form);
|
|
74
|
+
if(adapter?.browserContext) Object.assign(context,await adapter.browserContext({form,fieldId,context,mapping:adapter.mapping}));
|
|
75
|
+
console.log(JSON.stringify(await runtime.queryBrowser(api,form,fieldId,context),null,2));return;
|
|
76
|
+
}
|
|
77
|
+
if (command === 'adapter-register') {
|
|
78
|
+
const [workflowId,source,mappingFile]=args;
|
|
79
|
+
runtime.registerAdapter(cfg,workflowId,await getForm(workflowId),source,JSON.parse(fs.readFileSync(mappingFile,'utf8')));
|
|
80
|
+
console.log('已登记本机适配;不随升级包分发');return;
|
|
81
|
+
}
|
|
82
|
+
if (command === 'draft-preview' || command === 'draft-update-preview') {
|
|
83
|
+
const [workflowId, valuesFile, title, planFile, detailsFile, requestId] = args;
|
|
84
|
+
if(!planFile || !title || fs.existsSync(planFile)) throw Error('参数不全或计划已存在');
|
|
85
|
+
if(command==='draft-update-preview' && !requestId)throw Error('更新原稿必须提供requestId');
|
|
86
|
+
const form=await getForm(workflowId,requestId);
|
|
87
|
+
const plan={workflowId,requestName:title,values:JSON.parse(fs.readFileSync(valuesFile,'utf8')),details:detailsFile&&detailsFile!=='-'?JSON.parse(fs.readFileSync(detailsFile,'utf8')):[],baseUrl:cfg.baseUrl,accountId,requestId};
|
|
88
|
+
if(plan.values.values) { const input=plan.values; plan.values=input.values;plan.browserNames=input.browserNames??{};plan.detailBrowserNames=input.detailBrowserNames??{}; }
|
|
89
|
+
const prepared=await preparation.prepareFields(cfg,plan,{api,form});
|
|
90
|
+
plan.expected=prepared.expected;
|
|
91
|
+
plan.prepared={...prepared,body:Object.fromEntries(Object.entries(prepared.body).filter(([k])=>!/token|signature|UUID/i.test(k)))};
|
|
92
|
+
fs.writeFileSync(planFile,JSON.stringify(plan,null,2),{flag:'wx'});
|
|
93
|
+
console.log(JSON.stringify({state:'local-preview-only',...plan},null,2));return;
|
|
94
|
+
}
|
|
95
|
+
if(!['draft-save','draft-verify','draft-submit'].includes(command))throw Error('未知命令');
|
|
96
|
+
const planFile=args[0],receiptFile=planFile+'.receipt.json';
|
|
97
|
+
const plan=JSON.parse(fs.readFileSync(planFile,'utf8'));
|
|
98
|
+
if(plan.baseUrl!==cfg.baseUrl || plan.accountId!==accountId || !plan.expected)throw Error('计划环境/账号不符或旧版计划,请重新预览,不重复新建');
|
|
99
|
+
let receipt=fs.existsSync(receiptFile)?JSON.parse(fs.readFileSync(receiptFile,'utf8')):null;
|
|
100
|
+
const action=command==='draft-submit'?'submit':'save';
|
|
101
|
+
if(command==='draft-submit' && args[1]!=='--confirm')throw Error('正式提交需在完整预览后明确确认,使用 --confirm');
|
|
102
|
+
if(command==='draft-save'||command==='draft-submit') {
|
|
103
|
+
if(receipt)throw Error('已有保存回执,禁止重复执行,请先核验');
|
|
104
|
+
const form=await getForm(plan.workflowId,plan.requestId);
|
|
105
|
+
if(!plan.prepared)throw Error('旧计划缺少统一准备快照,请重新预览');
|
|
106
|
+
const prepared=plan.prepared;
|
|
107
|
+
await preparation.checkAdapter(cfg,plan,form);
|
|
108
|
+
const tables=await runtime.detailTables(api,form,plan.workflowId,plan.requestId);
|
|
109
|
+
preparation.checkBaseline(prepared,form,tables);
|
|
110
|
+
const body=preparation.actionBody(prepared,form,action);
|
|
111
|
+
const menu=await api.post('/api/workflow/reqform/rightMenu',{...form.params,...form.submitParams,isaffirmance:'0',isaffirmanceSrc:''},{forceCookie:true});
|
|
112
|
+
if(!menu.json?.rightMenus?.some(m=>m.systemMenuType===(action==='save'?'SAVE':'SUBMIT')))throw Error('没有保存权限');
|
|
113
|
+
receipt={state:'executing',action,requestId:plan.requestId};fs.writeFileSync(receiptFile,JSON.stringify(receipt),{flag:'wx'});
|
|
114
|
+
const saved=await api.post('/api/workflow/reqform/requestOperation',body,{forceCookie:true});
|
|
115
|
+
Object.assign(receipt,preparation.classifyWrite(saved,plan.requestId));
|
|
116
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2));
|
|
117
|
+
if(receipt.state==='failed')throw Error('保存明确失败:'+JSON.stringify(receipt.messageInfo));
|
|
118
|
+
if(receipt.state==='uncertain')throw Error('保存结果不明,请核验,禁止重复新建');
|
|
119
|
+
}
|
|
120
|
+
if(!receipt?.requestId)throw Error('结果不明,禁止重复新建,请到我的请求核验');
|
|
121
|
+
const form=await getForm(plan.workflowId,receipt.requestId);
|
|
122
|
+
const tables=await runtime.detailTables(api,form,plan.workflowId,receipt.requestId);
|
|
123
|
+
if(receipt.state==='failed')throw Error('上次保存明确失败,不能将原稿回读冒充保存成功');
|
|
124
|
+
const problems=preparation.verifyAction(plan.expected,form,tables,receipt.action??'save');
|
|
125
|
+
const current=(await accounts(api)).find(a=>a.current);
|
|
126
|
+
if(current.id!==plan.accountId)problems.push('账号变化');
|
|
127
|
+
receipt.state=problems.length?'verification-required':receipt.action==='submit'?'submitted-verified':'draft-verified';receipt.problems=problems;
|
|
128
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2));
|
|
129
|
+
const {buildRequestFormUrl}=await load('wfcreate');
|
|
130
|
+
// The business receipt is already durable; link issuance must never replay the write.
|
|
131
|
+
const {resolveBrowserLink} = await load('browser-links');
|
|
132
|
+
const link = await resolveBrowserLink(cfg, buildRequestFormUrl(cfg.baseUrl,receipt.requestId,plan.workflowId));
|
|
133
|
+
console.log(JSON.stringify({...receipt,...link},null,2));
|
|
134
|
+
if(problems.length)process.exitCode=1;
|
|
135
|
+
}
|
|
136
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
137
|
+
main().catch(e => {console.error(e.message); process.exitCode = 1;});
|
|
138
|
+
}
|
package/dist/pageopen.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { endpointFor } from './config.js';
|
|
2
|
+
|
|
3
|
+
const PAGE_MAP = {
|
|
4
|
+
center: '/wui/index.html#/main/workflow/index',
|
|
5
|
+
todo: '/wui/index.html#/main/workflow/doing',
|
|
6
|
+
done: '/wui/index.html#/main/workflow/done',
|
|
7
|
+
mine: '/wui/index.html#/main/workflow/mine',
|
|
8
|
+
draft: '/wui/index.html#/main/workflow/draft',
|
|
9
|
+
all: '/wui/index.html#/main/workflow/all',
|
|
10
|
+
newflow: '/spa/workflow/static4form/index.html#/main/workflow/req?iscreate=1',
|
|
11
|
+
monitor: '/wui/index.html#/main/workflow/monitor',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function pageUrl(cfg, page, requestId) {
|
|
15
|
+
const base = endpointFor(cfg).baseUrl.replace(/\/+$/, '');
|
|
16
|
+
if (page === 'view') {
|
|
17
|
+
if (!requestId)
|
|
18
|
+
throw new Error('打开流程详情需要 --request-id <请求 ID>。');
|
|
19
|
+
return `${base}/spa/workflow/static4form/index.html#/main/workflow/req?iscreate=0&requestid=${encodeURIComponent(requestId)}`;
|
|
20
|
+
}
|
|
21
|
+
const path = PAGE_MAP[page];
|
|
22
|
+
if (!path)
|
|
23
|
+
throw new Error(`不支持的页面“${page}”。可选:${Object.keys(PAGE_MAP).join('、')}、view。`);
|
|
24
|
+
return base + path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function availablePages() {
|
|
28
|
+
return [...Object.keys(PAGE_MAP), 'view'];
|
|
29
|
+
}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDIP5 ec/dev/table/datas 响应解析。
|
|
3
|
+
*
|
|
4
|
+
* 实跑确认的结构:
|
|
5
|
+
* - splitPageKey 响应里 dataKey 在 `sessionkey` 字段(小写)。
|
|
6
|
+
* - datas 响应顶层含 `columns`(列定义,dataIndex/title/dbField)和 `datas`(行数组,50 行/页)。
|
|
7
|
+
* - 每行同时有原始字段(如 creater="1" 是 id)和 `<field>span` 渲染字段(含可读名称 / HTML)。
|
|
8
|
+
* 可读值优先取 span 字段并去 HTML 标签。
|
|
9
|
+
* - counts 响应 `{"count":N,"status":true}`。
|
|
10
|
+
*/
|
|
11
|
+
/** 从一组候选路径里取第一个非空值,路径用点号分隔、支持下标。 */
|
|
12
|
+
export function pickByPaths(obj, paths) {
|
|
13
|
+
if (obj == null)
|
|
14
|
+
return undefined;
|
|
15
|
+
for (const p of paths) {
|
|
16
|
+
const v = getPath(obj, p);
|
|
17
|
+
if (v !== undefined && v !== null && v !== '')
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function getPath(obj, path) {
|
|
23
|
+
const tokens = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter((t) => t.length > 0);
|
|
24
|
+
let cur = obj;
|
|
25
|
+
for (const t of tokens) {
|
|
26
|
+
if (cur == null)
|
|
27
|
+
return undefined;
|
|
28
|
+
cur = cur[t];
|
|
29
|
+
}
|
|
30
|
+
return cur;
|
|
31
|
+
}
|
|
32
|
+
/** 从 getWfListParams / splitPageKey 响应里提取 dataKey(sessionkey)。 */
|
|
33
|
+
export function extractDataKey(resp) {
|
|
34
|
+
const v = pickByPaths(resp, [
|
|
35
|
+
'sessionkey',
|
|
36
|
+
'data.sessionkey',
|
|
37
|
+
'sessionKey',
|
|
38
|
+
'data.sessionKey',
|
|
39
|
+
'dataKey',
|
|
40
|
+
'data.dataKey',
|
|
41
|
+
'data',
|
|
42
|
+
]);
|
|
43
|
+
if (typeof v === 'string' && v.trim())
|
|
44
|
+
return v.trim();
|
|
45
|
+
return scanForDataKey(resp);
|
|
46
|
+
}
|
|
47
|
+
function scanForDataKey(obj) {
|
|
48
|
+
if (obj == null)
|
|
49
|
+
return null;
|
|
50
|
+
if (typeof obj === 'string') {
|
|
51
|
+
if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}_[A-F0-9]{32}$/.test(obj)) {
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(obj)) {
|
|
57
|
+
for (const it of obj) {
|
|
58
|
+
const r = scanForDataKey(it);
|
|
59
|
+
if (r)
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (typeof obj === 'object') {
|
|
65
|
+
for (const v of Object.values(obj)) {
|
|
66
|
+
const r = scanForDataKey(v);
|
|
67
|
+
if (r)
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** 提取列定义(可用于把行字段映射到中文标题)。 */
|
|
74
|
+
export function extractColumns(datasResp) {
|
|
75
|
+
const cols = pickByPaths(datasResp, ['columns', 'data.columns', 'result.columns']);
|
|
76
|
+
if (!Array.isArray(cols))
|
|
77
|
+
return [];
|
|
78
|
+
return cols.map((c) => ({ dataIndex: c?.dataIndex, title: c?.title, dbField: c?.dbField })).filter((c) => c.dataIndex);
|
|
79
|
+
}
|
|
80
|
+
/** 从 datas 响应里提取行数组。 */
|
|
81
|
+
export function extractRows(datasResp) {
|
|
82
|
+
const arr = pickByPaths(datasResp, ['datas', 'data.datas', 'rows', 'data.rows', 'data', 'list']);
|
|
83
|
+
if (Array.isArray(arr))
|
|
84
|
+
return arr;
|
|
85
|
+
if (arr && typeof arr === 'object' && !Array.isArray(arr)) {
|
|
86
|
+
const vals = Object.values(arr).filter((v) => v && typeof v === 'object');
|
|
87
|
+
if (vals.length)
|
|
88
|
+
return vals;
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/** 从 counts 或 datas 响应里提取总数。 */
|
|
93
|
+
export function extractTotal(resp) {
|
|
94
|
+
const v = pickByPaths(resp, ['count', 'data.count', 'total', 'data.total', 'totalCount', 'data.totalCount', 'result.count']);
|
|
95
|
+
if (typeof v === 'number')
|
|
96
|
+
return v;
|
|
97
|
+
if (typeof v === 'string' && /^\d+$/.test(v))
|
|
98
|
+
return parseInt(v, 10);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** 去掉 HTML 标签与常见实体,得到纯文本。 */
|
|
102
|
+
export function stripHtml(s) {
|
|
103
|
+
if (s == null)
|
|
104
|
+
return '';
|
|
105
|
+
let str = String(s);
|
|
106
|
+
str = str.replace(/<[^>]*>/g, '');
|
|
107
|
+
str = str
|
|
108
|
+
.replace(/ /g, ' ')
|
|
109
|
+
.replace(/&/g, '&')
|
|
110
|
+
.replace(/</g, '<')
|
|
111
|
+
.replace(/>/g, '>')
|
|
112
|
+
.replace(/"/g, '"')
|
|
113
|
+
.replace(/'/g, "'");
|
|
114
|
+
return str.trim();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 从 requestnamespan 提取可见标题。
|
|
118
|
+
* OA 把标题放进第一个 `<a>`(模板前缀如「[退回]」也在 `<a>` 内),把「我的关注」等状态标签
|
|
119
|
+
* 以独立 `<div class='wf_attention_div_btn'>` 追加在 `<a>` 之后——整体去标签会把标签文字并进标题
|
|
120
|
+
* (实测「我的关注」页签的标题被渲染成「采购审批4-... 我的关注」)。
|
|
121
|
+
* 故优先只取第一个 `<a>` 的文本;无 `<a>` 时回退整体去标签。
|
|
122
|
+
*/
|
|
123
|
+
function extractTitleFromNamespan(html) {
|
|
124
|
+
const s = String(html ?? '');
|
|
125
|
+
if (!s)
|
|
126
|
+
return '';
|
|
127
|
+
const m = s.match(/<a\b[^>]*>([\s\S]*?)<\/a>/i);
|
|
128
|
+
return stripHtml(m ? m[1] : s);
|
|
129
|
+
}
|
|
130
|
+
const LEVEL_LABEL = {
|
|
131
|
+
'0': '一般',
|
|
132
|
+
'1': '重要',
|
|
133
|
+
'2': '紧急',
|
|
134
|
+
'3': '紧急',
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* 把一行数据映射成标准待办项。
|
|
138
|
+
* 可读值优先取 `<field>span`(去 HTML),其次原始字段。
|
|
139
|
+
*/
|
|
140
|
+
export function normalizeRow(row, columns) {
|
|
141
|
+
if (!row || typeof row !== 'object')
|
|
142
|
+
return {};
|
|
143
|
+
void columns; // 列定义目前仅用于 extractColumns;字段名已稳定,直接按已知 dataIndex 取
|
|
144
|
+
const get = (keys) => pickByPaths(row, keys);
|
|
145
|
+
const spanOr = (dataIndex, extraKeys = []) => stripHtml(get([`${dataIndex}span`, dataIndex, ...extraKeys]));
|
|
146
|
+
// requestnamespan 是 OA 已按管理员模板渲染的可见标题;原始 requestname 仅作回退。
|
|
147
|
+
const rawTitle = stringify(get(['requestname', 'requestName']));
|
|
148
|
+
const title = extractTitleFromNamespan(get(['requestnamespan'])) || rawTitle;
|
|
149
|
+
const levelRaw = stringify(get(['requestlevel', 'level'])) ?? '';
|
|
150
|
+
const level = levelRaw ? (LEVEL_LABEL[levelRaw] ?? levelRaw) : undefined;
|
|
151
|
+
const creatorIdRaw = get(['createrid', 'creatorid', 'creatorId', 'creater']);
|
|
152
|
+
const creatorId = /^\d+$/.test(String(creatorIdRaw ?? '').trim()) ? String(creatorIdRaw).trim() : undefined;
|
|
153
|
+
return {
|
|
154
|
+
requestId: stringify(get(['requestid', 'requestId', 'id', 'randomField2', 'randomFieldId'])),
|
|
155
|
+
title,
|
|
156
|
+
rawTitle,
|
|
157
|
+
creator: spanOr('creater', ['createrName', 'creator', 'createName']),
|
|
158
|
+
creatorId,
|
|
159
|
+
createdate: spanOr('createdate', ['createdatespan', 'createDate']),
|
|
160
|
+
receivedate: spanOr('receivedate', ['receivedate', 'recieveDate']),
|
|
161
|
+
// 注意:服务端流程列表**只有节点 id**(currentnodeidspan 实测也是 id 本身,整行无节点名),
|
|
162
|
+
// 节点名由 nodemap.annotateNodeNames 按流程节点表回填到 nodeName。
|
|
163
|
+
node: stripHtml(get(['currentnodeidspan', 'currentnodespan'])) || stringify(get(['currentnodeid', 'nodeName', 'nodename'])),
|
|
164
|
+
nodeName: undefined,
|
|
165
|
+
workflowId: stringify(get(['workflowid', 'workflowId'])),
|
|
166
|
+
status: stripHtml(get(['statusspan'])) || stringify(get(['status'])),
|
|
167
|
+
level,
|
|
168
|
+
requestmark: stringify(get(['requestmark'])),
|
|
169
|
+
currentOperator: spanOr('unoperators', ['unoperatorspan', 'currentoperator', 'operator']),
|
|
170
|
+
raw: row,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function stringify(v) {
|
|
174
|
+
if (v === undefined || v === null || v === '')
|
|
175
|
+
return undefined;
|
|
176
|
+
return String(v);
|
|
177
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Separate input boundary: never accept credentials in argv, files or configuration.
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
export const PASSWORD_RISK_NOTICE = '临时兼容登录需要处理账号密码;通过聊天输入可能进入平台记录、模型上下文或日志。程序不保存密码,但不能消除平台侧风险。请单独征得用户明确同意,不要使用管理员账号。';
|
|
4
|
+
function failure(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
5
|
+
export function assertNoCredentialArguments(args) {
|
|
6
|
+
const flags = args?.flags ?? {};
|
|
7
|
+
if (Object.keys(flags).some(key => /password|passwd|credentials|username|loginid|secret/i.test(key)
|
|
8
|
+
&& !['allow-insecure-password-input', 'credentials-stdin'].includes(key))) {
|
|
9
|
+
throw failure('PASSWORD_ARGUMENT_FORBIDDEN', '不接受命令行、配置或文件传入的账号密码;请使用独立进程 stdin。');
|
|
10
|
+
}
|
|
11
|
+
if ((args?._?.length ?? 0) > 2) throw failure('PASSWORD_ARGUMENT_FORBIDDEN', '登录命令不接受附加位置参数。');
|
|
12
|
+
}
|
|
13
|
+
/** One JSON line from a distinct process stdin; no shell command interpolation. */
|
|
14
|
+
export async function readPasswordCredentials(input = process.stdin, options = {}) {
|
|
15
|
+
return readInputLine(input, options, false);
|
|
16
|
+
}
|
|
17
|
+
export async function readCaptchaAnswer(input = process.stdin, options = {}) {
|
|
18
|
+
return readInputLine(input, { timeoutMs: 300000, maxBytes: 1024, ...options }, true);
|
|
19
|
+
}
|
|
20
|
+
async function readInputLine(input, { timeoutMs = 60000, maxBytes = 8192, signal } = {}, captcha) {
|
|
21
|
+
if (signal?.aborted) throw failure('PASSWORD_CANCELLED', '输入已取消。');
|
|
22
|
+
if (!input || input.isTTY === true || input.destroyed || input.readableEnded || typeof input.on !== 'function') {
|
|
23
|
+
throw failure('PASSWORD_INPUT_UNAVAILABLE', '当前工具没有可用的独立 stdin 输入;请勿把密码拼入命令或临时文件。');
|
|
24
|
+
}
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let text = '', bytes = 0, settled = false, timer;
|
|
27
|
+
const decoder = new StringDecoder('utf8');
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
input.removeListener('data', data);
|
|
31
|
+
input.removeListener('end', end);
|
|
32
|
+
input.removeListener('error', failed);
|
|
33
|
+
signal?.removeEventListener('abort', cancelled);
|
|
34
|
+
input.pause?.();
|
|
35
|
+
// An open tool pipe must remain reusable for captcha, but not keep a
|
|
36
|
+
// completed CLI alive. The next read has its own referenced timeout.
|
|
37
|
+
input.unref?.();
|
|
38
|
+
text = '';
|
|
39
|
+
};
|
|
40
|
+
const finish = error => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
if (error) { cleanup(); reject(error); return; }
|
|
44
|
+
try {
|
|
45
|
+
const value = JSON.parse(text);
|
|
46
|
+
if (captcha) {
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
48
|
+
|| Object.keys(value).length !== 1 || typeof value.captcha !== 'string'
|
|
49
|
+
|| !/^[A-Za-z0-9]{1,16}$/.test(value.captcha)) throw new Error('invalid');
|
|
50
|
+
const answer = value.captcha;
|
|
51
|
+
cleanup(); resolve(answer); return;
|
|
52
|
+
}
|
|
53
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
54
|
+
|| Object.keys(value).some(k => !['username', 'password'].includes(k))
|
|
55
|
+
|| typeof value.username !== 'string' || !value.username.trim()
|
|
56
|
+
|| typeof value.password !== 'string' || !value.password
|
|
57
|
+
|| value.username.length > 256 || value.password.length > 1024
|
|
58
|
+
|| /[\u0000-\u001f\u007f]/.test(value.username + value.password)) {
|
|
59
|
+
throw new Error('invalid');
|
|
60
|
+
}
|
|
61
|
+
const credentials = { username: value.username, password: value.password };
|
|
62
|
+
cleanup(); resolve(credentials);
|
|
63
|
+
} catch { cleanup(); reject(failure('PASSWORD_INPUT_INVALID', captcha ? '下一行必须是仅含 captcha 字符串的 JSON;未输出输入内容。' : 'stdin 必须是一行仅含 username 和 password 字符串的 JSON;未输出输入内容。')); }
|
|
64
|
+
};
|
|
65
|
+
const data = chunk => {
|
|
66
|
+
bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
|
|
67
|
+
if (bytes > maxBytes) return finish(failure('PASSWORD_INPUT_INVALID', '凭据输入过长,已停止;未输出输入内容。'));
|
|
68
|
+
text += Buffer.isBuffer(chunk) ? decoder.write(chunk) : String(chunk);
|
|
69
|
+
if (text.includes('\n')) {
|
|
70
|
+
const [line, ...rest] = text.split('\n');
|
|
71
|
+
if (rest.join('\n').trim()) return finish(failure('PASSWORD_INPUT_INVALID', '只接受一行凭据 JSON。'));
|
|
72
|
+
text = line.replace(/\r$/, ''); finish();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const end = () => finish();
|
|
76
|
+
const failed = () => finish(failure('PASSWORD_INPUT_UNAVAILABLE', '凭据输入通道中断;未输出输入内容。'));
|
|
77
|
+
const cancelled = () => finish(failure('PASSWORD_CANCELLED', '输入已取消。'));
|
|
78
|
+
signal?.addEventListener('abort', cancelled, { once: true });
|
|
79
|
+
input.ref?.();
|
|
80
|
+
input.on('data', data); input.on('end', end); input.on('error', failed);
|
|
81
|
+
timer = setTimeout(() => finish(failure('PASSWORD_INPUT_UNAVAILABLE', '等待独立 stdin 输入超时;请勿改用聊天命令或临时文件拼接密码。')), timeoutMs);
|
|
82
|
+
input.resume?.();
|
|
83
|
+
});
|
|
84
|
+
}
|