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.
Files changed (58) hide show
  1. package/README.md +37 -2
  2. package/dist/accounts.js +30 -0
  3. package/dist/args.js +36 -0
  4. package/dist/auth-token.js +778 -0
  5. package/dist/auth.js +290 -0
  6. package/dist/blog.js +562 -0
  7. package/dist/browser-links.js +223 -0
  8. package/dist/captcha-display.js +30 -0
  9. package/dist/cli.js +5479 -0
  10. package/dist/client.js +254 -0
  11. package/dist/config.js +170 -0
  12. package/dist/createcontext.js +41 -0
  13. package/dist/crypto.js +139 -0
  14. package/dist/cube.js +1771 -0
  15. package/dist/email.js +841 -0
  16. package/dist/field-preparation.js +185 -0
  17. package/dist/fileutil.js +20 -0
  18. package/dist/hrm.js +478 -0
  19. package/dist/keepalive.js +99 -0
  20. package/dist/kq.js +280 -0
  21. package/dist/links.js +34 -0
  22. package/dist/nodemap.js +242 -0
  23. package/dist/odoc.js +199 -0
  24. package/dist/office-tools.js +138 -0
  25. package/dist/pageopen.js +29 -0
  26. package/dist/parse.js +177 -0
  27. package/dist/password-input.js +84 -0
  28. package/dist/password-login.js +208 -0
  29. package/dist/querystate.js +38 -0
  30. package/dist/render.js +21 -0
  31. package/dist/report.js +50 -0
  32. package/dist/session-worker.js +54 -0
  33. package/dist/session.js +280 -0
  34. package/dist/submissionplan.js +45 -0
  35. package/dist/submissionstate.js +81 -0
  36. package/dist/subordinate.js +458 -0
  37. package/dist/supervise.js +152 -0
  38. package/dist/todoaskidea.js +54 -0
  39. package/dist/todobatch.js +172 -0
  40. package/dist/todocc.js +128 -0
  41. package/dist/todoforward.js +66 -0
  42. package/dist/todoreject.js +215 -0
  43. package/dist/todosubmit.js +325 -0
  44. package/dist/todotransfer.js +54 -0
  45. package/dist/util.js +25 -0
  46. package/dist/wfagent.js +538 -0
  47. package/dist/wfcreate.js +102 -0
  48. package/dist/wfform.js +169 -0
  49. package/dist/wfops.js +40 -0
  50. package/dist/wfreceiver.js +43 -0
  51. package/dist/wfstart.js +558 -0
  52. package/dist/wfsummary.js +89 -0
  53. package/dist/wfupload.js +78 -0
  54. package/dist/wfuser.js +96 -0
  55. package/dist/workflow-runtime.js +151 -0
  56. package/dist/workflow.js +570 -0
  57. package/dist/workplan.js +431 -0
  58. package/package.json +28 -6
package/dist/auth.js ADDED
@@ -0,0 +1,290 @@
1
+ import fs from 'node:fs';
2
+ import readline from 'node:readline';
3
+ import { endpointFor, profileNameFromBaseUrl, updateConfigCookie } from './config.js';
4
+ import { passwordLogin, assertPasswordLoginTransport } from './password-login.js';
5
+ import { promptCaptcha } from './captcha-display.js';
6
+ import { PASSWORD_RISK_NOTICE, assertNoCredentialArguments, readPasswordCredentials } from './password-input.js';
7
+ import { AuthTokenSession, ensurePlaywright, canReach } from './auth-token.js';
8
+ import { probeCookieSession, sessionOptions, refreshCookie, assertSessionNotForcedOffline, latchForcedOffline } from './session.js';
9
+ import { parseDuration, keepaliveStatus, stopKeepalive, startBackgroundKeepalive, runForegroundKeepalive } from './keepalive.js';
10
+ /** 登录前隐私确认(硬门槛):未持久化同意且未带 --consent 时拒绝执行登录/鉴权。
11
+ * 由 SKILL.md 行为规范保证:AI 必须先向用户展示选择框确认「信息会经过大模型处理」,
12
+ * 用户明确同意后才带 --consent 调用本命令;本函数作为 CLI 兜底,防 AI 跳过询问直接登录。 */
13
+ function ensureLlmConsent(cfg, args) {
14
+ const persisted = cfg.llmConsent === true || String(cfg.llmConsent).toLowerCase() === 'true';
15
+ if (persisted)
16
+ return;
17
+ if (args?.flags?.consent)
18
+ return;
19
+ console.error('⚠ 隐私确认:你发送或获取的信息(待办/台账/邮件/日报等)会经过大模型处理。是否同意?');
20
+ console.error(' 同意本次登录:在命令后加 --consent 重试;');
21
+ console.error(' 或一次性同意(写入配置,后续登录不再询问):bdip config set llmConsent true');
22
+ console.error(' 不同意则中止登录,不执行任何鉴权操作。');
23
+ process.exit(1);
24
+ }
25
+ /**
26
+ * 申请/续期当前环境的 aiAgentToken(单一端点)。
27
+ * 带 preflight:① baseUrl 可达(避免错地址白耗浏览器);② playwright 就位(缺则联网自动装模块+内核);
28
+ * ③ 开浏览器登录拿 token(开不出窗口由 loginForCookie 抛清晰错并引导 cookie-login)。
29
+ * 登录前需隐私确认:--consent 或 config llmConsent=true,见 ensureLlmConsent。
30
+ */
31
+ function loginError(code, message) { const error = new Error(message); error.code = code; return error; }
32
+ export function browserEnvironmentUnavailable(platform = process.platform, env = process.env) {
33
+ return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
34
+ }
35
+ /** Test seams are explicit function dependencies, never user-controlled command flags. */
36
+ export async function cmdTokenLogin(cfg, args, dependencies = {}) {
37
+ assertNoCredentialArguments(args);
38
+ ensureLlmConsent(cfg, args);
39
+ const deps = { probe: probeCookieSession, reachable: canReach, ensureBrowser: ensurePlaywright,
40
+ browserUnavailable: browserEnvironmentUnavailable, getTokenSession: () => AuthTokenSession.fromProfile(cfg),
41
+ readCredentials: readPasswordCredentials, promptCaptcha, passwordLogin, ...dependencies };
42
+ const flags = args?.flags ?? {};
43
+ if (flags['allow-insecure-password-input'] !== undefined && flags['allow-insecure-password-input'] !== true
44
+ || flags['credentials-stdin'] !== undefined && flags['credentials-stdin'] !== true) {
45
+ throw loginError('PASSWORD_INPUT_INVALID', '密码兼容选项必须是布尔开关,不得附带任何凭据值。');
46
+ }
47
+ refreshCookie(cfg);
48
+ assertSessionNotForcedOffline(cfg);
49
+ const ep = endpointFor(cfg);
50
+ if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080') {
51
+ console.error('✗ baseUrl 未配置,先跑 bdip config set baseUrl <你的E9地址>');
52
+ process.exit(1);
53
+ }
54
+ if (cfg.cookie) {
55
+ let checkedCookie = cfg.cookie;
56
+ const existing = await deps.probe(cfg, { onCredential: cookie => { checkedCookie = cookie; } });
57
+ if (existing.code === 'FORCED_OFFLINE') {
58
+ latchForcedOffline(cfg, existing, checkedCookie);
59
+ throw loginError('FORCED_OFFLINE', '当前会话被强制下线;已停止,未自动重新登录。');
60
+ }
61
+ if (existing.authenticated) {
62
+ console.log(`✔ 当前 Cookie 登录有效,已复用当前用户 ${existing.userId};未启动浏览器或请求密码。`);
63
+ return { mode: 'existing-cookie', userId: existing.userId };
64
+ }
65
+ if (!['SESSION_EXPIRED', 'NO_COOKIE', 'IDENTITY_UNVERIFIED'].includes(existing.code)) {
66
+ throw loginError(existing.code ?? 'SESSION_CHECK_FAILED', '无法确认现有会话状态;请检查网络和平台服务,未请求密码。');
67
+ }
68
+ }
69
+ const expectedCookie = cfg.cookie ?? '';
70
+ const fallback = async () => {
71
+ if (flags['allow-insecure-password-input'] !== true) {
72
+ console.error(PASSWORD_RISK_NOTICE);
73
+ throw loginError('MOBILE_PASSWORD_CONSENT_REQUIRED', '浏览器环境不可用。用户单独确认上述密码风险后,重试同命令并加 --allow-insecure-password-input --credentials-stdin;不要将密码写入命令或文件。');
74
+ }
75
+ if (flags['credentials-stdin'] !== true) {
76
+ throw loginError('PASSWORD_INPUT_UNAVAILABLE', '需要独立进程 stdin 输入;工具不支持时请停止,不得把密码拼入命令、脚本或临时文件。');
77
+ }
78
+ let credentials;
79
+ let verified;
80
+ try {
81
+ assertPasswordLoginTransport(cfg);
82
+ console.error('[AUTH] 已确认兼容方式;等待独立 stdin 的单行凭据 JSON,不会回显或保存密码。');
83
+ credentials = await deps.readCredentials();
84
+ verified = await deps.passwordLogin(cfg, { ...credentials, onCaptcha: (challenge, context) => deps.promptCaptcha(challenge, context) });
85
+ } catch (error) {
86
+ // Never propagate remote body, passwords, input lines or arbitrary transport errors.
87
+ const allowed = /^(?:PASSWORD_[A-Z_]+|LOGIN_[A-Z_]+|IDENTITY_UNVERIFIED|FORCED_OFFLINE|RSA_[A-Z_]+|HTTP_[A-Z_]+|SESSION_EXPIRED|AUTH_[A-Z_]+)$/;
88
+ const code = allowed.test(String(error?.code ?? '')) ? error.code : 'PASSWORD_LOGIN_FAILED';
89
+ if (code === 'FORCED_OFFLINE') latchForcedOffline(cfg, { code }, expectedCookie);
90
+ const guidance = {
91
+ PASSWORD_CHALLENGE_UNSUPPORTED: '账号要求动态口令、设备或未知验证,当前兼容方式不能处理;请勿跳过平台要求。',
92
+ PASSWORD_CAPTCHA_REQUIRED: '平台要求图片验证码,需要工具展示图片并保持同一进程 stdin 等待人工填写。',
93
+ PASSWORD_CAPTCHA_REJECTED: '平台拒绝了本次验证码;没有自动重试密码,请重新发起登录获取新图片。',
94
+ PASSWORD_INVALID_URL: '本地 baseUrl 格式无效;尚未提交密码,请用 bdip config 核对。',
95
+ PASSWORD_TLS_REQUIRED: '密码登录仅支持 HTTP/HTTPS;尚未提交密码,请核对 baseUrl 协议。',
96
+ PASSWORD_HTTP_CONSENT_REQUIRED: '尚未读取或提交密码。保留 HTTP 时,先按用户授权执行 bdip config set httpPasswordLoginOrigin <当前HTTP站点根地址>;该设置仅对同协议、主机及端口生效。',
97
+ PASSWORD_INVALID_USERNAME: '本地账号输入格式无效;尚未提交密码。',
98
+ PASSWORD_INVALID_PASSWORD: '本地密码输入格式无效;尚未提交密码。',
99
+ PASSWORD_INVALID_OPTIONS: '登录选项格式无效;尚未提交密码。',
100
+ PASSWORD_CHANGE_REQUIRED: '平台要求修改密码,请先按平台规定完成改密后再登录。',
101
+ PASSWORD_POLICY_UNSUPPORTED: '平台启用了弱密码限制,本期兼容方式无法安全完成策略检查。',
102
+ PASSWORD_POLICY_UNKNOWN: '无法安全确认平台密码策略,已停止;需要核对原生接口兼容性。',
103
+ PASSWORD_LOGIN_REJECTED: '账号密码认证未通过;请核实账号或平台要求,不要反复试错。',
104
+ PASSWORD_INPUT_UNAVAILABLE: '工具没有可用的独立 stdin 输入通道;不要把密码拼入命令或临时文件。',
105
+ };
106
+ throw loginError(code, `临时 HTTP 登录未完成;原登录配置未被新凭据覆盖,未自动重试。${guidance[code] ?? '请按错误代码处理认证要求。'}`);
107
+ } finally {
108
+ if (credentials) { credentials.password = ''; credentials.username = ''; }
109
+ }
110
+ if (!verified || typeof verified.cookie !== 'string' || !verified.cookie || !/^[1-9]\d*$/.test(String(verified.userId))) {
111
+ throw loginError('IDENTITY_UNVERIFIED', '未取得经验证的当前用户,未保存登录态。');
112
+ }
113
+ assertSessionNotForcedOffline(cfg);
114
+ const session = deps.getTokenSession();
115
+ if (!updateConfigCookie(cfg, verified.cookie, expectedCookie)) {
116
+ throw loginError('PROFILE_CHANGED', '登录期间本地平台或账号配置已更新;未覆盖新配置,请重新检查登录状态。');
117
+ }
118
+ session.data = { token: '', expireTime: '', expireSeconds: 0, cookieSavedAt: Date.now(), noToken: true,
119
+ userId: String(verified.userId) };
120
+ session.save();
121
+ console.log(`✔ 原生 HTTP 登录成功,已核验当前用户 ${verified.userId};仅保存 Cookie,未保存密码或注册 token。`);
122
+ console.log(' 长任务可使用 bdip auth keepalive --duration 8h --background;强制下线后不会自动重新登录。');
123
+ return { mode: 'password-http', userId: String(verified.userId) };
124
+ };
125
+ const profileName = profileNameFromBaseUrl(ep.baseUrl);
126
+ // ── preflight ①:baseUrl 可达 ──
127
+ console.log(`\n[1/3] 探测 baseUrl 可达:${ep.baseUrl}`);
128
+ const reachable = await deps.reachable(ep.baseUrl, 8000);
129
+ if (!reachable) {
130
+ throw loginError('LOGIN_NETWORK_ERROR', '平台地址不可达;请检查地址与网络,未启用密码兼容方式。');
131
+ }
132
+ console.log('✔ 地址可达');
133
+ if (deps.browserUnavailable()) return fallback();
134
+ // ── preflight ②:playwright 就位(缺则联网自动装模块+内核;装不上会清晰报错并引导 cookie-login)──
135
+ console.log('[2/3] 检查 playwright ...');
136
+ try {
137
+ await deps.ensureBrowser();
138
+ }
139
+ catch (e) {
140
+ if (e.code === 'BROWSER_ENV_UNAVAILABLE' || e.code === 'ERR_MODULE_NOT_FOUND') return fallback();
141
+ // Dependency download/network failures do not prove browser login is impossible.
142
+ throw loginError('BROWSER_SETUP_FAILED', '浏览器依赖准备失败;请检查依赖与网络,未要求密码输入。');
143
+ }
144
+ console.log('✔ playwright 就位');
145
+ // ── 第3步:开浏览器登录拿 token ──
146
+ console.log(`[3/3] 为环境 "${profileName}" (${cfg.username || '?'}@${ep.baseUrl}) 开浏览器登录拿 aiAgentToken ...`);
147
+ if (!cfg.username || !cfg.encPassword) {
148
+ console.log('⚠ 未配置 username/encPassword,将启动有头浏览器手动登录(输账密并点「确认授权」)');
149
+ console.log(' (在 .bdip/config.json 配置后可 autoFill 自动填)');
150
+ }
151
+ const session = deps.getTokenSession();
152
+ let token;
153
+ try { token = await session.acquireToken('first'); }
154
+ catch (error) {
155
+ if (error.code === 'BROWSER_ENV_UNAVAILABLE') return fallback();
156
+ throw error;
157
+ }
158
+ if (token)
159
+ console.log('✔ aiAgentToken 申请成功');
160
+ else
161
+ console.log('⚠ 未拿到 token(已切 cookie 鉴权,见上方诊断)。跑 `bdip todo list` 验证,或 `bdip auth cookie-login` 手动导入。');
162
+ }
163
+ /**
164
+ * 手动导入 cookie 走 cookie 鉴权(无需浏览器,给无桌面/无 playwright 机器的后路)。
165
+ * 接受 --cookie <串> / --file <路径>,缺省交互式提示粘贴。
166
+ */
167
+ export async function cmdCookieLogin(cfg, args) {
168
+ ensureLlmConsent(cfg, args);
169
+ const ep = endpointFor(cfg);
170
+ if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080') {
171
+ console.error('✗ baseUrl 未配置,先跑 bdip config set baseUrl <你的E9地址>');
172
+ process.exit(1);
173
+ }
174
+ let cookie = '';
175
+ if (args?.flags && typeof args.flags.cookie === 'string' && args.flags.cookie)
176
+ cookie = args.flags.cookie;
177
+ else if (args?.flags?.file) {
178
+ try {
179
+ cookie = fs.readFileSync(args.flags.file, 'utf8').trim();
180
+ }
181
+ catch (e) {
182
+ console.error(`✗ 读文件失败:${e.message}`);
183
+ process.exit(1);
184
+ }
185
+ }
186
+ if (!cookie) {
187
+ console.log('请粘贴整条 Cookie(含 loginidweaver=...),回车结束:');
188
+ console.log(' 获取:浏览器登录 OA → F12 → Network → 任一请求 → Request Headers → Cookie 整行复制');
189
+ cookie = await readCookieLine();
190
+ }
191
+ cookie = cookie.trim();
192
+ if (!/loginidweaver=/.test(cookie)) {
193
+ console.error('✗ cookie 中未找到 loginidweaver=,请确认复制的是登录后的整条 Cookie(未登录态没有它)。');
194
+ process.exit(1);
195
+ }
196
+ if (/[\r\n]/.test(cookie)) throw new Error('Cookie 应为单行请求头');
197
+ const candidate = { ...cfg, cookie, _configPath: undefined };
198
+ const verified = await probeCookieSession(candidate, { refresh: false });
199
+ if (!verified.authenticated) throw new Error(`Cookie 未导入:${verified.message};请从已登录浏览器重新获取。`);
200
+ cookie = candidate.cookie;
201
+ const session = AuthTokenSession.fromProfile(cfg);
202
+ session.saveCookieToProfile(cookie); // 只更新 Cookie,保留其他最新配置
203
+ session.data = { token: '', expireTime: '', expireSeconds: 0, cookieSavedAt: Date.now(), noToken: true };
204
+ session.save(); // 标 noToken:运行时走 cookie,不弹浏览器
205
+ console.log(`✔ 已导入 cookie 鉴权(${cookie.length} 字符),写入 ${cfg._configPath ?? '~/.bdip/config.json'}`);
206
+ console.log(` 已验证当前用户:${verified.userId}${verified.loginId ? ` (${verified.loginId})` : ''};${verified.mode === 'extension' ? `空闲超时 ${verified.idleTimeoutSeconds} 秒` : verified.message}`);
207
+ console.log(' 业务命令运行期间会自动心跳;跨多次命令的长任务请运行 `bdip auth keepalive --duration 8h --background`。');
208
+ }
209
+ /** 读 stdin 第一条非空行(交互式粘贴 cookie)。5 分钟兜底超时。 */
210
+ function readCookieLine() {
211
+ return new Promise((resolve) => {
212
+ let got = '';
213
+ let settled = false;
214
+ let timer;
215
+ const done = (v) => { if (!settled) {
216
+ settled = true;
217
+ clearTimeout(timer);
218
+ try {
219
+ rl.close();
220
+ }
221
+ catch { /* ignore */ }
222
+ resolve(v);
223
+ } };
224
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
225
+ rl.on('line', (line) => { const t = line.trim(); if (t)
226
+ done(t); });
227
+ rl.on('close', () => done(got));
228
+ timer = setTimeout(() => done(got), 300000);
229
+ });
230
+ }
231
+ /**
232
+ * 打印当前环境 / token 状态(单一端点)。加 --check 联网探活:baseUrl 可达 + 鉴权是否真有效。
233
+ * 不加 --check 行为不变(纯本地读,保护 cron)。
234
+ */
235
+ export async function cmdAuthStatus(cfg, args) {
236
+ const ep = endpointFor(cfg);
237
+ const profileName = profileNameFromBaseUrl(ep.baseUrl);
238
+ const session = AuthTokenSession.fromProfile(cfg);
239
+ let tokenLine = '❌ 未申请(跑 bdip auth token-login 或 bdip auth cookie-login)';
240
+ let extra = '';
241
+ if (session.data.token) {
242
+ const left = Math.round(((session.data.cookieSavedAt || 0) + (session.data.expireSeconds || 0) * 1000 - Date.now()) / 60000);
243
+ tokenLine = session.isExpired ? '⚠ 本地已过期' : `✅ 剩余约 ${left} 分钟`;
244
+ extra = `\n expireTime: ${session.data.expireTime || '?'}`;
245
+ }
246
+ else if (session.data.noToken) {
247
+ tokenLine = '⚠ noToken(走 cookie 鉴权:系统不支持 aiAgentToken 或已手动导入 cookie)';
248
+ }
249
+ console.log('════════════════════════════════════════════════════');
250
+ console.log(` BDIP5 认证状态 · ${profileName}`);
251
+ console.log('─────────────────────────────────────────────────────');
252
+ console.log(` 配置文件 ${cfg._configPath ?? '?'}`);
253
+ console.log(` baseUrl ${cfg.baseUrl}`);
254
+ console.log(` username ${cfg.username ?? '(未配置)'}`);
255
+ console.log(` encPwd ${cfg.encPassword ? '✅ 已配置(autoFill 可用)' : '❌ 未配置(手动登录)'}`);
256
+ console.log(` cookie ${cfg.cookie ? `已配置 (${cfg.cookie.length} 字符)` : '未配置'}`);
257
+ console.log(` aiAgentToken ${tokenLine}${extra}`);
258
+ const opts = sessionOptions(cfg);
259
+ console.log(` 运行期心跳 ${opts.enabled ? `每 ${opts.intervalMs / 1000} 秒` : '已禁用'}`);
260
+ const lease = keepaliveStatus(cfg);
261
+ console.log(` 后台保活 ${lease.active ? `运行中,截止 ${new Date(lease.expiresAt).toISOString()}` : lease.status}`);
262
+ if (args?.flags?.check) {
263
+ console.log('─────────────────────────────────────────────────────');
264
+ // Cookie-only identity probe: no browser, no token registration, no business mutation.
265
+ const state = await probeCookieSession(cfg);
266
+ console.log(` Cookie 验证 ${state.authenticated ? `✅ 当前用户 ${state.userId}${state.loginId ? ` (${state.loginId})` : ''}` : `⚠ ${state.message}`}`);
267
+ console.log(` 会话扩展 ${state.mode === 'extension' && state.authenticated ? `✅ 空闲超时 ${state.idleTimeoutSeconds} 秒` : '尚未验证可用'}`);
268
+ if (state.authenticated && state.mode === 'native') console.log(` 说明 ${state.message}`);
269
+ console.log(' token 通道 本地状态见上方;此检查不将本地 token 文件视为服务端有效证明。');
270
+ if (!state.authenticated) process.exitCode = 1;
271
+ }
272
+ console.log('════════════════════════════════════════════════════');
273
+ }
274
+
275
+ /** Explicit, bounded keepalive across short CLI invocations; never performs a login. */
276
+ export async function cmdKeepAlive(cfg, args) {
277
+ const flags = args?.flags ?? {};
278
+ if (flags.stop && flags.status) throw new Error('--stop 与 --status 只能选择一个');
279
+ const print = value => {
280
+ if (flags.json) console.log(JSON.stringify(value, null, 2));
281
+ else console.log(`保活状态:${value.status ?? (value.authenticated ? 'running' : value.code)}${value.userId ? `;当前用户 ${value.userId}` : ''}${value.expiresAt ? `;截止 ${new Date(value.expiresAt).toISOString()}` : ''}${value.message ? `;${value.message}` : ''}`);
282
+ };
283
+ if (flags.status) { print(keepaliveStatus(cfg)); return; }
284
+ if (flags.stop) { print(stopKeepalive(cfg)); return; }
285
+ const durationMs = parseDuration(flags.duration ?? '8h');
286
+ if (flags.background) { print(await startBackgroundKeepalive(cfg, durationMs)); return; }
287
+ const expiresAt = Date.now() + durationMs;
288
+ await runForegroundKeepalive(cfg, durationMs, state => print({ ...state, status: state.authenticated ? 'running' : 'failed', expiresAt }));
289
+ if (!flags.json) console.log('本次前台保活已结束。');
290
+ }