bdip-office-assistant-cli 1.0.0 → 1.0.1
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 +4 -4
- package/dist/auth-token.js +92 -87
- package/dist/auth.js +45 -4
- package/dist/browser-links.js +1 -0
- package/dist/cli.js +58 -4
- package/dist/documents.js +98 -0
- package/dist/field-preparation.js +61 -9
- package/dist/formula-runtime.js +55 -0
- package/dist/linkage-calculations.js +68 -0
- package/dist/meetings.js +172 -0
- package/dist/office-tools.js +19 -2
- package/dist/person-selection.js +81 -0
- package/dist/wfupload.js +14 -9
- package/dist/workflow-runtime.js +29 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 毕埃慕ai助手 CLI 1.0.1
|
|
2
2
|
|
|
3
|
-
BDIP5办公平台的命令行工具,由毕埃慕(上海)建筑数据技术股份有限公司提供。配合WorkBuddy
|
|
3
|
+
BDIP5办公平台的命令行工具,由毕埃慕(上海)建筑数据技术股份有限公司提供。配合WorkBuddy的“毕埃慕ai助手”技能使用,也可直接运行`bdip --help`查看命令。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
需要Node.js 18或更高版本及npm。在实际运行助手的环境安装;手机使用云电脑时需要安装在云电脑。
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
npm install -g bdip-office-assistant-cli@1.0.
|
|
10
|
+
npm install -g bdip-office-assistant-cli@1.0.1 --registry=https://registry.npmjs.org/
|
|
11
11
|
bdip version
|
|
12
12
|
bdip --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
当前配套技能版本为 1.0.1,CLI 版本为 1.0.1。安装时固定版本,两种交付包必须使用相同源码。CLI 版本不匹配时手机适配器会停止。
|
|
16
16
|
|
|
17
17
|
## 配置
|
|
18
18
|
|
package/dist/auth-token.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 链路:
|
|
5
5
|
* loginForCookie(有头 autoFill,失败转手动;轮询 loginidweaver,60s hash-portal 兜底)
|
|
6
|
+
* → 平台账号接口再次核验,关闭浏览器
|
|
6
7
|
* → saveCookieToProfile(存 cookie 供回退,写回 config.json 对应字段)
|
|
7
8
|
* → registerToken: GET /api/ai/agent/login/register 带 cookie → aiAgentToken(8h,加密存)
|
|
8
9
|
* 成功 → { token, expireTime, expireSeconds, cookieSavedAt, noToken:false }
|
|
9
10
|
* 404/异常 → { token:'', noToken:true }(回退 cookie)
|
|
10
|
-
* → promptAuthConfirm(有头,弹授权 overlay,用户点确认才 registerToken)
|
|
11
11
|
*
|
|
12
12
|
* token 文件:~/.bdip/tokens/<profileName>.json(AES-256-GCM 加密)。
|
|
13
13
|
* profileName 由当前 baseUrl 的 hostname+port 派生(一个环境一个 token 文件)。
|
|
@@ -344,7 +344,7 @@ export async function loginForCookie(baseUrl, username, password, opts = {}) {
|
|
|
344
344
|
// autoFill 用尽仍无 cookie → 原地提示手动登录(同一浏览器,避免二次 launchSystemBrowser)
|
|
345
345
|
if (autoFill && !fellBackToManual && Date.now() >= autoFillDeadline) {
|
|
346
346
|
fellBackToManual = true;
|
|
347
|
-
console.error(' [AUTH] autoFill 未拿到登录
|
|
347
|
+
console.error(' [AUTH] autoFill 未拿到登录 Cookie,请在已打开的浏览器手动登录(最长 600s)...');
|
|
348
348
|
}
|
|
349
349
|
if (!headless && !portaled && Date.now() - t0 > 60000) {
|
|
350
350
|
// hash-portal 兜底:hash 路由切 SPA portal 设 cookie
|
|
@@ -361,11 +361,10 @@ export async function loginForCookie(baseUrl, username, password, opts = {}) {
|
|
|
361
361
|
await sleep(200);
|
|
362
362
|
}
|
|
363
363
|
if (!authenticated) throw new Error('未验证到真实登录用户,不能只凭 loginidweaver Cookie 认定登录成功');
|
|
364
|
-
if (!
|
|
365
|
-
if (!await browserIdentityValid(context, baseUrl)) throw new Error('授权确认后登录已失效,请重新登录');
|
|
364
|
+
if (!await browserIdentityValid(context, baseUrl)) throw new Error('登录后账号核验失败,请重新登录');
|
|
366
365
|
cookies = await context.cookies(baseUrl + '/wui/index.html');
|
|
367
366
|
const cookieString = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
368
|
-
// Save only this OA's state after
|
|
367
|
+
// Save only this OA's state after login consent and identity verification.
|
|
369
368
|
try {
|
|
370
369
|
ensureDir(CONFIG_DIR);
|
|
371
370
|
const state = filterStorageState(await context.storageState(), baseUrl);
|
|
@@ -383,82 +382,6 @@ export async function loginForCookie(baseUrl, username, password, opts = {}) {
|
|
|
383
382
|
catch { /* 关闭失败忽略 */ }
|
|
384
383
|
}
|
|
385
384
|
}
|
|
386
|
-
export async function confirmAfterNavigation(page, confirm) {
|
|
387
|
-
for (let attempt = 0; ; attempt++) {
|
|
388
|
-
await page.waitForLoadState('networkidle', {timeout:10000}).catch(() => {});
|
|
389
|
-
try { await confirm(page); return; }
|
|
390
|
-
catch (error) {
|
|
391
|
-
if (attempt >= 2 || !/Execution context was destroyed|Cannot find context/.test(String(error?.message))) throw error;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
/** 登录后注入授权确认 overlay;用户点「确认授权」才放行。 */
|
|
396
|
-
async function promptAuthConfirm(page) {
|
|
397
|
-
const agent = getAgentInfo();
|
|
398
|
-
await page.evaluate((agentDisplay) => {
|
|
399
|
-
// 注入动画样式
|
|
400
|
-
const style = document.createElement('style');
|
|
401
|
-
style.textContent = `
|
|
402
|
-
@keyframes __e9_fade_in { from { opacity:0; } to { opacity:1; } }
|
|
403
|
-
@keyframes __e9_slide_up { from { opacity:0; transform:translateY(24px) scale(.96); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
404
|
-
`;
|
|
405
|
-
document.head.appendChild(style);
|
|
406
|
-
document.getElementById('__e9_auth_confirm__')?.remove();
|
|
407
|
-
window.__bdipExplicitConsent = false;
|
|
408
|
-
const mask = document.createElement('div');
|
|
409
|
-
mask.id = '__e9_auth_confirm__';
|
|
410
|
-
mask.style.cssText =
|
|
411
|
-
'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:2147483647;display:flex;align-items:center;justify-content:center;animation:__e9_fade_in .25s ease;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif;';
|
|
412
|
-
const card = document.createElement('div');
|
|
413
|
-
card.style.cssText =
|
|
414
|
-
'background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 12px 48px rgba(0,0,0,.3);width:400px;animation:__e9_slide_up .35s cubic-bezier(.16,1,.3,1);';
|
|
415
|
-
// 顶部渐变色条
|
|
416
|
-
const topBar = document.createElement('div');
|
|
417
|
-
topBar.style.cssText = 'height:5px;background:linear-gradient(90deg,#3b82f6,#6366f1,#8b5cf6);';
|
|
418
|
-
// 内容区
|
|
419
|
-
const body = document.createElement('div');
|
|
420
|
-
body.style.cssText = 'padding:36px 36px 32px;text-align:center;';
|
|
421
|
-
// SVG 盾牌图标
|
|
422
|
-
const icon = document.createElement('div');
|
|
423
|
-
icon.style.cssText = 'width:64px;height:64px;margin:0 auto 20px;';
|
|
424
|
-
icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" width="64" height="64">
|
|
425
|
-
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
426
|
-
<stop offset="0%" stop-color="#3b82f6"/><stop offset="100%" stop-color="#6366f1"/>
|
|
427
|
-
</linearGradient></defs>
|
|
428
|
-
<path d="M12 2L4 5v6c0 5 3.5 9.5 8 11 4.5-1.5 8-6 8-11V5l-8-3z" fill="url(#g)"/>
|
|
429
|
-
<path d="M9 12l2 2 4-4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
430
|
-
</svg>`;
|
|
431
|
-
const title = document.createElement('div');
|
|
432
|
-
title.style.cssText = 'font-size:17px;font-weight:600;color:#0f172a;margin-bottom:6px;letter-spacing:.3px;';
|
|
433
|
-
title.textContent = `【BDIP办公助手】请求授权访问BDIP系统,请确认。`;
|
|
434
|
-
const desc = document.createElement('div');
|
|
435
|
-
desc.style.cssText = 'font-size:12px;color:#94a3b8;line-height:1.6;margin-bottom:28px;';
|
|
436
|
-
desc.textContent = '确认后将以您的身份访问系统';
|
|
437
|
-
// 按钮区(只保留确认授权;关浏览器/超时不授权 → 走 cookie 回退)
|
|
438
|
-
const btnYes = document.createElement('button');
|
|
439
|
-
btnYes.id = '__e9_auth_yes__';
|
|
440
|
-
btnYes.style.cssText =
|
|
441
|
-
'width:100%;background:linear-gradient(135deg,#3b82f6,#6366f1);color:#fff;border:none;padding:12px 0;border-radius:8px;cursor:pointer;font-size:15px;font-weight:500;transition:transform .15s,box-shadow .15s;box-shadow:0 2px 8px rgba(59,130,246,.35);';
|
|
442
|
-
btnYes.textContent = '确认授权';
|
|
443
|
-
btnYes.onmouseenter = () => { btnYes.style.transform = 'translateY(-1px)'; btnYes.style.boxShadow = '0 4px 16px rgba(59,130,246,.45)'; };
|
|
444
|
-
btnYes.onmouseleave = () => { btnYes.style.transform = 'translateY(0)'; btnYes.style.boxShadow = '0 2px 8px rgba(59,130,246,.35)'; };
|
|
445
|
-
body.appendChild(icon);
|
|
446
|
-
body.appendChild(title);
|
|
447
|
-
body.appendChild(desc);
|
|
448
|
-
body.appendChild(btnYes);
|
|
449
|
-
card.appendChild(topBar);
|
|
450
|
-
card.appendChild(body);
|
|
451
|
-
mask.appendChild(card);
|
|
452
|
-
document.body.appendChild(mask);
|
|
453
|
-
btnYes.addEventListener('click', () => { window.__bdipExplicitConsent = true; mask.remove(); });
|
|
454
|
-
}, agent.display);
|
|
455
|
-
await page
|
|
456
|
-
.waitForFunction(() => window.__bdipExplicitConsent === true, null, { timeout: 180000 })
|
|
457
|
-
.catch((error) => {
|
|
458
|
-
if (/Execution context was destroyed|Cannot find context/.test(String(error?.message))) throw error;
|
|
459
|
-
throw new Error('授权确认超时(180s)、页面已变化或未点击');
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
385
|
/** 已知 agent 工具的 env 签名表(叶子层探测,按顺序匹配)。 */
|
|
463
386
|
const KNOWN_AGENT_TOOLS = [
|
|
464
387
|
// Claude Code:子进程注入 CLAUDECODE=1 / CLAUDE_CODE_SESSION_ID / CLAUDE_CODE_EXECPATH(已实跑确认)
|
|
@@ -471,7 +394,7 @@ const KNOWN_AGENT_TOOLS = [
|
|
|
471
394
|
{ keys: ['DUMATE', 'DU_MATE', 'DUMATE_SESSION_ID'], display: 'DuMate' },
|
|
472
395
|
];
|
|
473
396
|
/** 无法识别实际 agent 时的兜底展示名。 */
|
|
474
|
-
const FALLBACK_DISPLAY = '
|
|
397
|
+
const FALLBACK_DISPLAY = '毕埃慕ai助手';
|
|
475
398
|
function detectKnownAgent() {
|
|
476
399
|
for (const tool of KNOWN_AGENT_TOOLS) {
|
|
477
400
|
for (const k of tool.keys) {
|
|
@@ -511,12 +434,87 @@ export function getAgentInfo() {
|
|
|
511
434
|
framework = FALLBACK_DISPLAY;
|
|
512
435
|
return { framework: framework.toLowerCase(), display: framework };
|
|
513
436
|
}
|
|
437
|
+
const SKILL_AUTH_VERIFY_BASE = 'https://bim.bdip.com';
|
|
438
|
+
export const SKILL_AUTH_CONTACT = [
|
|
439
|
+
'如需申请或更新授权,请联系毕埃慕:',
|
|
440
|
+
' 400咨询热线:4000-835-957',
|
|
441
|
+
' 座机:021-35081131',
|
|
442
|
+
' 企业邮箱:bim@17bim.com',
|
|
443
|
+
].join('\n');
|
|
444
|
+
export function userFacingSkillAuthMessage(kind = 'no_auth') {
|
|
445
|
+
if (kind === 'expired') return `授权已过期。\n${SKILL_AUTH_CONTACT}`;
|
|
446
|
+
if (kind === 'verify_failed') return `授权校验未通过。\n${SKILL_AUTH_CONTACT}`;
|
|
447
|
+
if (kind === 'no_domain') return `尚未配置办公平台地址,无法校验授权。请先提供 OA 地址;若还没有授权,\n${SKILL_AUTH_CONTACT}`;
|
|
448
|
+
return `当前办公平台未获得有效授权。\n${SKILL_AUTH_CONTACT}`;
|
|
449
|
+
}
|
|
450
|
+
/** Authorization is bound to the configured OA host, including a non-default port. */
|
|
451
|
+
export function skillAuthDomain(baseUrl) {
|
|
452
|
+
const raw = String(baseUrl ?? '').trim();
|
|
453
|
+
if (!raw) return '';
|
|
454
|
+
try { return new URL(raw).host; }
|
|
455
|
+
catch { return raw.replace(/^https?:\/\//i, '').replace(/\/+$/, '').split('/')[0]; }
|
|
456
|
+
}
|
|
457
|
+
function authCodeError(kind, code, message) {
|
|
458
|
+
const error = new Error(message);
|
|
459
|
+
error.e9kind = kind;
|
|
460
|
+
error.code = code;
|
|
461
|
+
return error;
|
|
462
|
+
}
|
|
463
|
+
/** Check the configured OA domain without sending Cookie or login credentials. */
|
|
464
|
+
export async function verifySkillAuth(baseUrl) {
|
|
465
|
+
const domain = skillAuthDomain(baseUrl);
|
|
466
|
+
if (!domain) throw authCodeError('no-auth-domain', 'AI_SKILL_NO_AUTH', userFacingSkillAuthMessage('no_domain'));
|
|
467
|
+
const url = `${SKILL_AUTH_VERIFY_BASE}/bdipCloud/api/ai/aiSkillAuth/verifyByDomain?${new URLSearchParams({ domain })}`;
|
|
468
|
+
let response;
|
|
469
|
+
try {
|
|
470
|
+
response = await fetch(url, {
|
|
471
|
+
method: 'GET',
|
|
472
|
+
headers: {
|
|
473
|
+
Accept: 'application/json, text/plain, */*',
|
|
474
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
|
475
|
+
},
|
|
476
|
+
signal: AbortSignal.timeout(15000),
|
|
477
|
+
redirect: 'manual',
|
|
478
|
+
});
|
|
479
|
+
} catch {
|
|
480
|
+
throw authCodeError('network', 'AI_SKILL_AUTH_VERIFY_FAILED', '授权校验请求失败,请检查网络后重试。');
|
|
481
|
+
}
|
|
482
|
+
let json;
|
|
483
|
+
try { json = await response.json(); }
|
|
484
|
+
catch { throw authCodeError('auth-verify-invalid', 'AI_SKILL_AUTH_VERIFY_FAILED', userFacingSkillAuthMessage('verify_failed')); }
|
|
485
|
+
const code = Number(json?.code);
|
|
486
|
+
const msgKey = String(json?.msgKey ?? json?.msg_key ?? '');
|
|
487
|
+
const message = String(json?.message ?? json?.msg ?? '');
|
|
488
|
+
if (response.ok && (code === 200 || msgKey === 'OperOk'))
|
|
489
|
+
return { ok: true, code, msgKey: msgKey || 'OperOk', message, domain };
|
|
490
|
+
if (msgKey === 'ai_skill_auth_expired' || /授权已过期/.test(message))
|
|
491
|
+
throw authCodeError('auth-code-expired', 'AI_SKILL_AUTH_EXPIRED', userFacingSkillAuthMessage('expired'));
|
|
492
|
+
if (msgKey === 'ai_skill_no_auth' || /没有授权/.test(message))
|
|
493
|
+
throw authCodeError('no-auth-code', 'AI_SKILL_NO_AUTH', userFacingSkillAuthMessage('no_auth'));
|
|
494
|
+
throw authCodeError('auth-verify-failed', 'AI_SKILL_AUTH_VERIFY_FAILED', userFacingSkillAuthMessage('verify_failed'));
|
|
495
|
+
}
|
|
496
|
+
export async function ensureAgentAuth(baseUrl) {
|
|
497
|
+
const domain = skillAuthDomain(baseUrl);
|
|
498
|
+
console.error(' [AUTH] 登录前按域名校验授权...');
|
|
499
|
+
await verifySkillAuth(baseUrl);
|
|
500
|
+
console.error(' [AUTH] 域名授权有效。');
|
|
501
|
+
return { domain };
|
|
502
|
+
}
|
|
503
|
+
function looksAuthCodeRejected(status, json, text) {
|
|
504
|
+
const msgKey = String(json?.msgKey ?? json?.msg_key ?? '');
|
|
505
|
+
if (msgKey === 'ai_skill_no_auth' || msgKey === 'ai_skill_auth_expired') return true;
|
|
506
|
+
const msg = String(json?.msg ?? json?.message ?? text ?? '');
|
|
507
|
+
if (/授权码|authCode|auth.?code|没有授权|授权已过期|域名授权/i.test(msg)
|
|
508
|
+
&& /过期|无效|失效|不存在|错误|拒绝|invalid|expired|forbidden|denied|没有授权/i.test(msg)) return true;
|
|
509
|
+
if (status === 401 || status === 403) return /授权码|authCode|auth.?code|授权/i.test(msg);
|
|
510
|
+
return /AUTH_CODE|AUTHCODE|LICENSE|AI_SKILL/i.test(String(json?.errorCode ?? ''));
|
|
511
|
+
}
|
|
514
512
|
/** GET /api/ai/agent/login/register 用 cookie 换 token(仿 curl 头)。失败抛带 e9kind/e9status 的错,供上层大声诊断。 */
|
|
515
513
|
export async function registerToken(baseUrl, cookieString, cfg) {
|
|
516
514
|
const agent = getAgentInfo();
|
|
517
515
|
let resp;
|
|
518
516
|
try {
|
|
519
|
-
resp = await fetch(`${baseUrl}/api/ai/agent/login/register
|
|
517
|
+
resp = await fetch(`${baseUrl}/api/ai/agent/login/register?${new URLSearchParams({ aiAgentType: agent.display })}`, {
|
|
520
518
|
headers: {
|
|
521
519
|
Cookie: cookieString,
|
|
522
520
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
@@ -548,6 +546,11 @@ export async function registerToken(baseUrl, cookieString, cfg) {
|
|
|
548
546
|
throw err;
|
|
549
547
|
}
|
|
550
548
|
rejectForcedOffline(cfg, { authenticated: false, mode: 'cookie', code: authFailureCode(status, json) }, cookieString);
|
|
549
|
+
if (looksAuthCodeRejected(status, json, text)) {
|
|
550
|
+
if (String(json?.msgKey ?? '') === 'ai_skill_auth_expired' || /授权已过期/.test(String(json?.message ?? json?.msg ?? '')))
|
|
551
|
+
throw authCodeError('auth-code-expired', 'AI_SKILL_AUTH_EXPIRED', userFacingSkillAuthMessage('expired'));
|
|
552
|
+
throw authCodeError('no-auth-code', 'AI_SKILL_NO_AUTH', userFacingSkillAuthMessage('no_auth'));
|
|
553
|
+
}
|
|
551
554
|
if (!resp.ok || !json || json.status === false || json.errorCode) {
|
|
552
555
|
const code = /^[\w-]{1,64}$/.test(String(json?.errorCode ?? '')) ? String(json.errorCode) : 'UNKNOWN';
|
|
553
556
|
const err = new Error(`register 失败(HTTP ${status},code=${code})`);
|
|
@@ -635,6 +638,7 @@ export class AuthTokenSession {
|
|
|
635
638
|
}
|
|
636
639
|
/** acquireToken 真实主体(持有跨进程锁时执行);跳过续期/未拿到 token 均返回 null。 */
|
|
637
640
|
async _acquireTokenInner(reason = 'renew') {
|
|
641
|
+
await ensureAgentAuth(this.baseUrl);
|
|
638
642
|
// ② 跨进程锁:重读磁盘看别的进程是否正在续期(窗口内跳过)
|
|
639
643
|
this._reloadData();
|
|
640
644
|
const acquiringAt = this.data.acquiringAt || 0;
|
|
@@ -650,9 +654,9 @@ export class AuthTokenSession {
|
|
|
650
654
|
const username = this.cfg.username || '';
|
|
651
655
|
// ★ #15 文案区分:续期 vs 首登(loginForCookie 的"请在浏览器登录页输入账密"是共享等待语,这里先打场景横幅)
|
|
652
656
|
if (reason === 'first')
|
|
653
|
-
console.error('[AUTH]
|
|
657
|
+
console.error('[AUTH] 首次登录:浏览器登录并核验账号后申请 aiAgentToken。');
|
|
654
658
|
else
|
|
655
|
-
console.error('[AUTH] token
|
|
659
|
+
console.error('[AUTH] token 已过期:浏览器登录并核验账号后申请新 aiAgentToken。');
|
|
656
660
|
// autoFill 失败已在 loginForCookie 内原地转手动登录(同一浏览器会话),不再二次重开浏览器
|
|
657
661
|
const cookieString = await loginForCookie(this.baseUrl, username, username ? password : '', { headless: false });
|
|
658
662
|
this.saveCookieToProfile(cookieString);
|
|
@@ -669,7 +673,7 @@ export class AuthTokenSession {
|
|
|
669
673
|
return token;
|
|
670
674
|
}
|
|
671
675
|
catch (e) {
|
|
672
|
-
if (e.code === 'FORCED_OFFLINE') throw e;
|
|
676
|
+
if (e.code === 'FORCED_OFFLINE' || e.code === 'AI_SKILL_NO_AUTH' || e.code === 'AI_SKILL_AUTH_EXPIRED') throw e;
|
|
673
677
|
// 大声诊断:区分 404 不支持 / 网络不通 / 其它,不再含糊"系统可能不支持"
|
|
674
678
|
const kind = e?.e9kind || (e?.e9status === 404 ? 'unsupported' : 'other');
|
|
675
679
|
if (kind === 'unsupported')
|
|
@@ -693,6 +697,7 @@ export class AuthTokenSession {
|
|
|
693
697
|
}
|
|
694
698
|
/** Renew an expiring agent token with the live Cookie, without opening a browser. */
|
|
695
699
|
async renewWithCookie() {
|
|
700
|
+
await ensureAgentAuth(this.baseUrl);
|
|
696
701
|
const key = this.profileName || 'default';
|
|
697
702
|
if (_renewInflight.has(key)) return _renewInflight.get(key);
|
|
698
703
|
const work = async () => {
|
|
@@ -714,7 +719,7 @@ export class AuthTokenSession {
|
|
|
714
719
|
return token;
|
|
715
720
|
}
|
|
716
721
|
catch (error) {
|
|
717
|
-
if (error.code === 'FORCED_OFFLINE') throw error;
|
|
722
|
+
if (error.code === 'FORCED_OFFLINE' || error.code === 'AI_SKILL_NO_AUTH' || error.code === 'AI_SKILL_AUTH_EXPIRED') throw error;
|
|
718
723
|
this.data.cookieRenewFailedAt = Date.now();
|
|
719
724
|
this.data.acquiringAt = 0;
|
|
720
725
|
this.save();
|
package/dist/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ import { endpointFor, profileNameFromBaseUrl, updateConfigCookie } from './confi
|
|
|
4
4
|
import { passwordLogin, assertPasswordLoginTransport } from './password-login.js';
|
|
5
5
|
import { promptCaptcha } from './captcha-display.js';
|
|
6
6
|
import { PASSWORD_RISK_NOTICE, assertNoCredentialArguments, readPasswordCredentials } from './password-input.js';
|
|
7
|
-
import { AuthTokenSession, ensurePlaywright, canReach } from './auth-token.js';
|
|
7
|
+
import { AuthTokenSession, ensurePlaywright, canReach, loginForCookie, ensureAgentAuth, verifySkillAuth, skillAuthDomain } from './auth-token.js';
|
|
8
8
|
import { probeCookieSession, sessionOptions, refreshCookie, assertSessionNotForcedOffline, latchForcedOffline } from './session.js';
|
|
9
9
|
import { parseDuration, keepaliveStatus, stopKeepalive, startBackgroundKeepalive, runForegroundKeepalive } from './keepalive.js';
|
|
10
10
|
/** 登录前隐私确认(硬门槛):未持久化同意且未带 --consent 时拒绝执行登录/鉴权。
|
|
@@ -32,13 +32,44 @@ function loginError(code, message) { const error = new Error(message); error.cod
|
|
|
32
32
|
export function browserEnvironmentUnavailable(platform = process.platform, env = process.env) {
|
|
33
33
|
return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
34
34
|
}
|
|
35
|
+
/** Interactive browser login for sites without the aiAgentToken authorization page. */
|
|
36
|
+
export async function cmdCookieBrowserLogin(cfg, args, dependencies = {}) {
|
|
37
|
+
ensureLlmConsent(cfg, args);
|
|
38
|
+
refreshCookie(cfg);
|
|
39
|
+
assertSessionNotForcedOffline(cfg);
|
|
40
|
+
const ep = endpointFor(cfg);
|
|
41
|
+
if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080')
|
|
42
|
+
throw loginError('LOGIN_CONFIG_MISSING', '请先配置 BDIP 平台地址');
|
|
43
|
+
await (dependencies.verifyAuth ?? ensureAgentAuth)(ep.baseUrl);
|
|
44
|
+
const reachable = dependencies.reachable ?? canReach;
|
|
45
|
+
if (!await reachable(ep.baseUrl, 8000))
|
|
46
|
+
throw loginError('LOGIN_NETWORK_ERROR', '平台地址不可达,请检查地址与网络');
|
|
47
|
+
const ensureBrowser = dependencies.ensureBrowser ?? ensurePlaywright;
|
|
48
|
+
await ensureBrowser();
|
|
49
|
+
const login = dependencies.login ?? loginForCookie;
|
|
50
|
+
const expectedCookie = cfg.cookie ?? '';
|
|
51
|
+
console.log('请在打开的 BDIP 页面完成登录;验证成功后自动保存 Cookie。');
|
|
52
|
+
const cookie = await login(ep.baseUrl, '', '', {headless: false});
|
|
53
|
+
const candidate = {...cfg, cookie, _configPath: undefined};
|
|
54
|
+
const probe = dependencies.probe ?? probeCookieSession;
|
|
55
|
+
const verified = await probe(candidate, {refresh: false});
|
|
56
|
+
if (!verified.authenticated) throw loginError('IDENTITY_UNVERIFIED', '未验证到当前账号,未保存登录态');
|
|
57
|
+
if (!updateConfigCookie(cfg, candidate.cookie, expectedCookie))
|
|
58
|
+
throw loginError('PROFILE_CHANGED', '登录期间本地配置变化,未覆盖新登录态');
|
|
59
|
+
const session = AuthTokenSession.fromProfile(cfg);
|
|
60
|
+
session.data = {token: '', expireTime: '', expireSeconds: 0, cookieSavedAt: Date.now(), noToken: true,
|
|
61
|
+
userId: String(verified.userId), acquiringAt: 0, acquireFailedAt: 0};
|
|
62
|
+
session.save();
|
|
63
|
+
console.log(`✔ Cookie 登录已验证并保存;当前用户 ${verified.userId}。`);
|
|
64
|
+
return {mode: 'browser-cookie', userId: String(verified.userId)};
|
|
65
|
+
}
|
|
35
66
|
/** Test seams are explicit function dependencies, never user-controlled command flags. */
|
|
36
67
|
export async function cmdTokenLogin(cfg, args, dependencies = {}) {
|
|
37
68
|
assertNoCredentialArguments(args);
|
|
38
69
|
ensureLlmConsent(cfg, args);
|
|
39
70
|
const deps = { probe: probeCookieSession, reachable: canReach, ensureBrowser: ensurePlaywright,
|
|
40
71
|
browserUnavailable: browserEnvironmentUnavailable, getTokenSession: () => AuthTokenSession.fromProfile(cfg),
|
|
41
|
-
readCredentials: readPasswordCredentials, promptCaptcha, passwordLogin, ...dependencies };
|
|
72
|
+
readCredentials: readPasswordCredentials, promptCaptcha, passwordLogin, verifyAuth: ensureAgentAuth, ...dependencies };
|
|
42
73
|
const flags = args?.flags ?? {};
|
|
43
74
|
if (flags['allow-insecure-password-input'] !== undefined && flags['allow-insecure-password-input'] !== true
|
|
44
75
|
|| flags['credentials-stdin'] !== undefined && flags['credentials-stdin'] !== true) {
|
|
@@ -51,6 +82,7 @@ export async function cmdTokenLogin(cfg, args, dependencies = {}) {
|
|
|
51
82
|
console.error('✗ baseUrl 未配置,先跑 bdip config set baseUrl <你的E9地址>');
|
|
52
83
|
process.exit(1);
|
|
53
84
|
}
|
|
85
|
+
await deps.verifyAuth(ep.baseUrl);
|
|
54
86
|
if (cfg.cookie) {
|
|
55
87
|
let checkedCookie = cfg.cookie;
|
|
56
88
|
const existing = await deps.probe(cfg, { onCredential: cookie => { checkedCookie = cookie; } });
|
|
@@ -145,7 +177,7 @@ export async function cmdTokenLogin(cfg, args, dependencies = {}) {
|
|
|
145
177
|
// ── 第3步:开浏览器登录拿 token ──
|
|
146
178
|
console.log(`[3/3] 为环境 "${profileName}" (${cfg.username || '?'}@${ep.baseUrl}) 开浏览器登录拿 aiAgentToken ...`);
|
|
147
179
|
if (!cfg.username || !cfg.encPassword) {
|
|
148
|
-
console.log('⚠ 未配置 username/encPassword
|
|
180
|
+
console.log('⚠ 未配置 username/encPassword,将启动浏览器手动登录;账号核验成功后自动关闭窗口。');
|
|
149
181
|
console.log(' (在 .bdip/config.json 配置后可 autoFill 自动填)');
|
|
150
182
|
}
|
|
151
183
|
const session = deps.getTokenSession();
|
|
@@ -164,13 +196,14 @@ export async function cmdTokenLogin(cfg, args, dependencies = {}) {
|
|
|
164
196
|
* 手动导入 cookie 走 cookie 鉴权(无需浏览器,给无桌面/无 playwright 机器的后路)。
|
|
165
197
|
* 接受 --cookie <串> / --file <路径>,缺省交互式提示粘贴。
|
|
166
198
|
*/
|
|
167
|
-
export async function cmdCookieLogin(cfg, args) {
|
|
199
|
+
export async function cmdCookieLogin(cfg, args, dependencies = {}) {
|
|
168
200
|
ensureLlmConsent(cfg, args);
|
|
169
201
|
const ep = endpointFor(cfg);
|
|
170
202
|
if (!ep.baseUrl || ep.baseUrl.replace(/\/+$/, '') === 'http://localhost:8080') {
|
|
171
203
|
console.error('✗ baseUrl 未配置,先跑 bdip config set baseUrl <你的E9地址>');
|
|
172
204
|
process.exit(1);
|
|
173
205
|
}
|
|
206
|
+
await (dependencies.verifyAuth ?? ensureAgentAuth)(ep.baseUrl);
|
|
174
207
|
let cookie = '';
|
|
175
208
|
if (args?.flags && typeof args.flags.cookie === 'string' && args.flags.cookie)
|
|
176
209
|
cookie = args.flags.cookie;
|
|
@@ -206,6 +239,14 @@ export async function cmdCookieLogin(cfg, args) {
|
|
|
206
239
|
console.log(` 已验证当前用户:${verified.userId}${verified.loginId ? ` (${verified.loginId})` : ''};${verified.mode === 'extension' ? `空闲超时 ${verified.idleTimeoutSeconds} 秒` : verified.message}`);
|
|
207
240
|
console.log(' 业务命令运行期间会自动心跳;跨多次命令的长任务请运行 `bdip auth keepalive --duration 8h --background`。');
|
|
208
241
|
}
|
|
242
|
+
export async function cmdSkillAuthVerify(cfg) {
|
|
243
|
+
const baseUrl = endpointFor(cfg).baseUrl;
|
|
244
|
+
if (!baseUrl || baseUrl.replace(/\/+$/, '') === 'http://localhost:8080')
|
|
245
|
+
throw loginError('AI_SKILL_NO_AUTH', '尚未配置办公平台地址,请先运行 bdip config set baseUrl <地址>。');
|
|
246
|
+
const result = await verifySkillAuth(baseUrl);
|
|
247
|
+
console.log(`✔ 当前办公平台授权有效:${skillAuthDomain(baseUrl)}`);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
209
250
|
/** 读 stdin 第一条非空行(交互式粘贴 cookie)。5 分钟兜底超时。 */
|
|
210
251
|
function readCookieLine() {
|
|
211
252
|
return new Promise((resolve) => {
|
package/dist/browser-links.js
CHANGED
|
@@ -12,6 +12,7 @@ let warnedInvalidConfig = false;
|
|
|
12
12
|
const PAGE_PATHS = new Set([
|
|
13
13
|
'/wui/index.html', '/spa/workflow/static4form/index.html', '/spa/workflow/static4mobileform/index.html', '/spa/cube/index.html',
|
|
14
14
|
'/spa/email/static/index.html', '/spa/workplan/static/index.html', '/spa/hrm/index_mobx.html',
|
|
15
|
+
'/spa/document/index.jsp',
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
function plain(targetUrl, status, reason = null) {
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { getMyBlogList, getMyBlogAll, isBlogRecalled, createBlog, getBlogWorkInd
|
|
|
24
24
|
import { getCreateWfList, buildCreateWfUrl, buildRequestFormUrl, findCreateWfByName } from './wfcreate.js';
|
|
25
25
|
import { describeFormFields, VIEWATTR_LABEL } from './wfform.js';
|
|
26
26
|
import { getStartableFields, prepareCreateSubmission, submitCreateRequest, resolveWorkflowId, getOdocNextNodes, odocFlowNext } from './wfstart.js';
|
|
27
|
-
import { cmdTokenLogin, cmdCookieLogin, cmdAuthStatus, cmdKeepAlive } from './auth.js';
|
|
27
|
+
import { cmdTokenLogin, cmdCookieBrowserLogin, cmdCookieLogin, cmdSkillAuthVerify, cmdAuthStatus, cmdKeepAlive } from './auth.js';
|
|
28
28
|
import { resolveBrowserLink, resolveBrowserLinks, browserLinkNote } from './browser-links.js';
|
|
29
29
|
import { getEmailList, getEmailView, setMailReadStatus, resolveDateRange, sendEmail, setEmailStar, deleteEmails, moveEmails, getEmailLabels, addEmailLabel, applyEmailLabel, setEmailWaitdeal, recallEmail, uploadEmailAttachment, getEmailAttachmentList, getEmailSize, getEmailReciveAccounts } from './email.js';
|
|
30
30
|
import { searchWorkplan, createWorkplan, getMyCalendar, getAttentionData, getBaseData, getCalendarData, getSelectType, getWorkplanSearchCondition, viewWorkplan, deleteWorkplan, finishWorkplan, downloadWorkplanImportTemplate, importWorkplanExcel, exportWorkplanExcel } from './workplan.js';
|
|
@@ -38,6 +38,7 @@ import { getCurrentUser, getCurrentUserIsAdmin } from './wfuser.js';
|
|
|
38
38
|
import { getSubordinates, assertUserIdsInScope, assertNamesInScope, resolveNameToUid, isScopeEnforced } from './subordinate.js';
|
|
39
39
|
import { resolveHrmTab, getHrmTabs, getHrmConditions, resolveHrmCondition, hrmList, buildHrmCardUrl, findHrmOrgNode, applyResolvedPersonIds, expandHrmOrgTree } from './hrm.js';
|
|
40
40
|
import { parseArgs, flagStr, flagBool } from './args.js';
|
|
41
|
+
import { uploadFile as uploadWorkflowFile } from './wfupload.js';
|
|
41
42
|
import { todayStr } from './util.js';
|
|
42
43
|
import { BOX_TOP, DIVIDER } from './render.js';
|
|
43
44
|
function usage() {
|
|
@@ -49,6 +50,8 @@ function usage() {
|
|
|
49
50
|
' bdip link <url> [--json] 为当前账号生成浏览器免登录链接',
|
|
50
51
|
' bdip auth keepalive [--duration 8h] [--background] [--json]',
|
|
51
52
|
' bdip auth keepalive --status | --stop [--json]',
|
|
53
|
+
' bdip auth cookie-browser-login --consent 交互式网页登录并验证 Cookie,不申请 aiAgentToken',
|
|
54
|
+
' bdip auth skill-verify 校验当前办公平台域名授权',
|
|
52
55
|
' bdip account list [--json] 查看关联账号与当前身份',
|
|
53
56
|
' bdip account switch --id <账号ID> [--json] 切换并验证关联账号',
|
|
54
57
|
' bdip whoami 查看当前登录人(userid/用户名/是否系统管理员 isAdmin)',
|
|
@@ -104,6 +107,7 @@ function usage() {
|
|
|
104
107
|
' bdip create open --requestid <请求id> [--id <wf>] 生成已有请求表单 URL(处理/提交待办用,手动填+提交)',
|
|
105
108
|
' bdip create fields --id <流程id> 模拟打开发起表单,列出字段(可编辑/只读/隐藏/控件类型)',
|
|
106
109
|
' bdip create start [--id <id>|--name <名>] [--values <json>|--values-file <path>] [--detail-file <path>] [--requestname <名>] [--yes --plan <id>] 预览生成确认计划;提交复用确认字段',
|
|
110
|
+
' bdip create upload --id <流程id> --field-id <附件字段id> --file <本地路径> --receipt <新回执路径> --confirm 先上传附件取得文件ID,再纳入流程预览',
|
|
107
111
|
' bdip create flow --requestid <请求id> --workflowid <流程id> [--next-user <uid>] [--yes] 公文流转:取可流转候选→确认→推送下一节点(公文 create start 后续步骤)',
|
|
108
112
|
' bdip page-open --page todo|done|mine|center|newflow|view [--request-id <id>] 生成流程中心页面链接',
|
|
109
113
|
' bdip 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)',
|
|
@@ -627,8 +631,50 @@ async function runCreateVerify(args) {
|
|
|
627
631
|
console.log('请在 OA 中确认保留项;在确认前系统不会自动重试。');
|
|
628
632
|
}
|
|
629
633
|
|
|
634
|
+
async function runCreateUpload(args) {
|
|
635
|
+
const workflowId = flagStr(args, 'id');
|
|
636
|
+
const fieldId = flagStr(args, 'field-id');
|
|
637
|
+
const filePath = flagStr(args, 'file');
|
|
638
|
+
const receiptPath = flagStr(args, 'receipt');
|
|
639
|
+
if (!/^\d+$/.test(String(workflowId ?? '')) || !/^\d+$/.test(String(fieldId ?? '')) || !filePath || !receiptPath)
|
|
640
|
+
throw new Error('用法:bdip create upload --id <流程ID> --field-id <附件字段ID> --file <本地路径> --receipt <新回执路径> [--confirm]');
|
|
641
|
+
const cfg = loadConfig(flagStr(args, 'config'));
|
|
642
|
+
const form = await getStartableFields(cfg, workflowId);
|
|
643
|
+
const field = form.fields.find(item => String(item.id) === String(fieldId));
|
|
644
|
+
if (!form.verifyRight || !field || Number(field.htmltype) !== 4 || Number(field.viewattr) === 1 || Number(field.viewattr) === 4)
|
|
645
|
+
throw new Error('当前流程没有可上传的该主表附件字段,或字段不可编辑');
|
|
646
|
+
const accounts = await listAccounts(cfg);
|
|
647
|
+
const current = accounts.find(item => item.current);
|
|
648
|
+
if (!current || accounts.filter(item => item.current).length !== 1) throw new Error('当前账号未核验');
|
|
649
|
+
const stat = fs.statSync(filePath);
|
|
650
|
+
if (!stat.isFile() || stat.size === 0) throw new Error('待上传文件为空或不是普通文件');
|
|
651
|
+
if (!args.flags.confirm) {
|
|
652
|
+
console.log(`附件上传预览:流程 ${form.workflowName ?? workflowId};字段 ${field.label ?? fieldId};文件 ${filePath};账号 ${current.name}(${current.id})。确认后单独加 --confirm 上传,取得文件 ID 后再运行 create start 预览。`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const receipt = {state: 'uploading', baseUrl: cfg.baseUrl, workflowId: String(workflowId), fieldId: String(fieldId), filePath, accountId: current.id};
|
|
656
|
+
fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2), {flag: 'wx'});
|
|
657
|
+
try {
|
|
658
|
+
const result = await uploadWorkflowFile(cfg, {workflowId, fieldId, filePath, loginuserid: current.id});
|
|
659
|
+
if (!/^\d+$/.test(result.imagefileid)) throw new Error('上传响应缺少可用于表单的 imagefileid');
|
|
660
|
+
receipt.state = 'uploaded';
|
|
661
|
+
receipt.fileId = result.imagefileid;
|
|
662
|
+
receipt.uploadId = result.fileid;
|
|
663
|
+
fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2));
|
|
664
|
+
console.log(`附件已上传,尚未发起流程。将字段 ${fieldId} 的值设为 ${result.imagefileid},重新运行 create start 预览。回执:${receiptPath}`);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
receipt.state = 'verification-required';
|
|
667
|
+
fs.writeFileSync(receiptPath, JSON.stringify(receipt, null, 2));
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
630
672
|
async function runCreate(args) {
|
|
631
673
|
const sub = args._[1] ?? 'list';
|
|
674
|
+
if (sub === 'upload') {
|
|
675
|
+
await runCreateUpload(args);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
632
678
|
if (sub === 'workflows') {
|
|
633
679
|
await runCreateWorkflows(args);
|
|
634
680
|
return;
|
|
@@ -1246,6 +1292,10 @@ async function runCreateOpen(args) {
|
|
|
1246
1292
|
async function runAuth(args) {
|
|
1247
1293
|
const sub = args._[1] ?? 'status';
|
|
1248
1294
|
const cfg = loadConfig(flagStr(args, 'config'));
|
|
1295
|
+
if (sub === 'skill-verify') {
|
|
1296
|
+
await cmdSkillAuthVerify(cfg);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1249
1299
|
if (sub === 'keepalive') {
|
|
1250
1300
|
await cmdKeepAlive(cfg, args);
|
|
1251
1301
|
return;
|
|
@@ -1254,6 +1304,10 @@ async function runAuth(args) {
|
|
|
1254
1304
|
await cmdTokenLogin(cfg, args);
|
|
1255
1305
|
return;
|
|
1256
1306
|
}
|
|
1307
|
+
if (sub === 'cookie-browser-login') {
|
|
1308
|
+
await cmdCookieBrowserLogin(cfg, args);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1257
1311
|
if (sub === 'cookie-login') {
|
|
1258
1312
|
await cmdCookieLogin(cfg, args);
|
|
1259
1313
|
return;
|
|
@@ -1275,7 +1329,7 @@ async function runAuth(args) {
|
|
|
1275
1329
|
: '⚠ 已撤销隐私同意:后续登录需重新确认(--consent)。');
|
|
1276
1330
|
return;
|
|
1277
1331
|
}
|
|
1278
|
-
console.error(`auth 暂只支持 token-login / cookie-login / status / consent / keepalive,收到: ${sub}`);
|
|
1332
|
+
console.error(`auth 暂只支持 skill-verify / token-login / cookie-browser-login / cookie-login / status / consent / keepalive,收到: ${sub}`);
|
|
1279
1333
|
process.exit(1);
|
|
1280
1334
|
}
|
|
1281
1335
|
async function runBlog(args) {
|
|
@@ -4926,7 +4980,7 @@ async function main() {
|
|
|
4926
4980
|
}
|
|
4927
4981
|
if (cmd === 'version') {
|
|
4928
4982
|
const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
|
|
4929
|
-
console.log(JSON.stringify({ name: 'bdip-office-assistant-cli', version: '1.0.
|
|
4983
|
+
console.log(JSON.stringify({ name: 'bdip-office-assistant-cli', version: '1.0.1', buildHash }, null, 2));
|
|
4930
4984
|
return;
|
|
4931
4985
|
}
|
|
4932
4986
|
if (cmd === 'whoami') {
|
|
@@ -5446,7 +5500,7 @@ function runConfig(args) {
|
|
|
5446
5500
|
if (sub === 'init') {
|
|
5447
5501
|
console.log('BDIP5 首次配置:');
|
|
5448
5502
|
console.log(' 1) bdip config set baseUrl http://你的E9地址:端口');
|
|
5449
|
-
console.log(' 2) bdip auth token-login
|
|
5503
|
+
console.log(' 2) bdip auth token-login (浏览器登录 BDIP5,账号核验成功后保存 Cookie 并申请 aiAgentToken)');
|
|
5450
5504
|
console.log(' 3) bdip blog check / bdip todo list / bdip create list 验证');
|
|
5451
5505
|
return;
|
|
5452
5506
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {resolveBrowserLinks} from './browser-links.js';
|
|
4
|
+
|
|
5
|
+
export function directories(tree) {
|
|
6
|
+
const rows=[];
|
|
7
|
+
const walk=(value,parent='')=>{
|
|
8
|
+
if(Array.isArray(value)){for(const v of value)walk(v,parent);return;}
|
|
9
|
+
if(!value||typeof value!=='object')return;
|
|
10
|
+
const full=value.categoryid!=null&&value.name?[parent,value.name].filter(Boolean).join('/'):parent;
|
|
11
|
+
if(value.categoryid!=null&&value.name)rows.push({id:String(value.categoryid),name:value.name,path:full,hasRight:value.hasRight});
|
|
12
|
+
for(const [key,v]of Object.entries(value))if(v&&typeof v==='object')walk(v,full);
|
|
13
|
+
};
|
|
14
|
+
walk(tree);return rows;
|
|
15
|
+
}
|
|
16
|
+
export async function request(api,method,route,body) {
|
|
17
|
+
const multipart=body instanceof FormData;
|
|
18
|
+
const headers=await api.getHeaders(method==='POST'&&!multipart?'application/x-www-form-urlencoded; charset=utf-8':undefined,{forceCookie:true});
|
|
19
|
+
const response=await fetch(new URL(route,api.baseUrl),{method,headers,body:method==='GET'?undefined:multipart?body:new URLSearchParams(body),signal:AbortSignal.timeout(120000)});
|
|
20
|
+
if(!response.ok)throw Error('文档接口HTTP '+response.status);
|
|
21
|
+
const json=await response.json();
|
|
22
|
+
if(json.api_status===false||json.status===false)throw Error('文档接口失败');
|
|
23
|
+
return json;
|
|
24
|
+
}
|
|
25
|
+
const plain=s=>String(s??'').replace(/<[^>]*>/g,'');
|
|
26
|
+
export async function listDocuments(api,id) {
|
|
27
|
+
const init=await request(api,'GET','/api/doc/searchlist/list?'+new URLSearchParams({seccategory:id,secid:id}));
|
|
28
|
+
if(String(init.seccategoryid)!==String(id)||!init.sessionkey)throw Error('文档列表目录范围未验证');
|
|
29
|
+
const items=[],seen=new Set();
|
|
30
|
+
for(let page=1;page<=1000;page++){
|
|
31
|
+
const result=await request(api,'POST','/api/ec/dev/table/datas',{dataKey:init.sessionkey,current:String(page),pageSize:'50'});
|
|
32
|
+
if(!Array.isArray(result.datas))throw Error('文档列表响应缺失');
|
|
33
|
+
for(const row of result.datas){
|
|
34
|
+
if(String(row.seccategory)!==String(id)||!row.id||seen.has(String(row.id)))throw Error('文档分页重复或范围不符');
|
|
35
|
+
seen.add(String(row.id));
|
|
36
|
+
const browser=result.browser?.find(b=>String(b.linkvaluecolumn)===String(row.id));
|
|
37
|
+
const link=String(row.idspan??'').match(/(\/spa\/document\/[^"'<>\s]+)/)?.[1];
|
|
38
|
+
items.push({id:String(row.id),title:plain(row.docsubject??row.idspan),directoryId:String(row.seccategory),ownerId:String(row.ownerid),creatorId:String(row.doccreaterid),attachmentCount:Number(row.accessorycount),status:plain(browser?.list?.find(v=>v.t_text==='状态')?.t_column)||null,url:link?new URL(link,api.baseUrl).href:null});
|
|
39
|
+
}
|
|
40
|
+
// 服务端可能缩小pageSize;只在空页终止,不能把不足请求大小当作末页。
|
|
41
|
+
if(result.datas.length===0)return items;
|
|
42
|
+
}
|
|
43
|
+
throw Error('文档分页超过上限,不能声明完整');
|
|
44
|
+
}
|
|
45
|
+
export async function withDocumentLinks(cfg, rows) {
|
|
46
|
+
const links=await resolveBrowserLinks(cfg,rows.map(row=>row.url));
|
|
47
|
+
return rows.map((row,index)=>{
|
|
48
|
+
const link=links[index];
|
|
49
|
+
return {...row,...link};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function uploadDefaults(init,file) {
|
|
53
|
+
if(init.canEdit!==true)throw Error('目录不允许上传');
|
|
54
|
+
const size=fs.statSync(file).size;
|
|
55
|
+
const max=Number(init.maxUploadFileSize),min=Number(init.mixUploadFileSize);
|
|
56
|
+
if(!Number.isFinite(max)||!Number.isFinite(min))throw Error('目录大小规则未知');
|
|
57
|
+
if((max>0&&size>max*1024)||size<min*1024)throw Error('文件大小不符合目录限制');
|
|
58
|
+
const ext=path.extname(file).slice(1).toLowerCase(),allowed=String(init.uploadext??'').toLowerCase();
|
|
59
|
+
if(!['*.*','*'].includes(allowed)&&!allowed.split(/[;,\s]+/).map(x=>x.replace(/^\*?\./,'')).includes(ext))throw Error('扩展名不允许或规则未识别');
|
|
60
|
+
const defaults={};
|
|
61
|
+
const walk=values=>{for(const c of values??[]){if(c.items){walk(c.items);continue;}const key=c.domkey?.[0];if(!key)throw Error('未知文档初始化字段');const value=c.otherParams?.value??c.browserConditionParam?.replaceDatas?.map(x=>x.id).join(',')??c.value;if(value!=null&&value!=='')defaults[key]=String(value);else if(Number(c.viewAttr)===3)throw Error('缺少必填文档属性:'+key);}};
|
|
62
|
+
walk(init.conditions);
|
|
63
|
+
if(!defaults.ownerid)throw Error('未取得默认所有者');
|
|
64
|
+
return defaults;
|
|
65
|
+
}
|
|
66
|
+
export async function documentMain(api,args,accountId,cfg) {
|
|
67
|
+
const [action,id,file,receipt]=args;
|
|
68
|
+
if(action==='directories')return directories((await request(api,'GET','/api/doc/console/category/tree')).treeDatas).filter(r=>!id||r.name.includes(id)||r.path.includes(id));
|
|
69
|
+
if(!/^\d+$/.test(id??''))throw Error('请先按名查询并选择目录ID');
|
|
70
|
+
if(action==='list')return withDocumentLinks(cfg,await listDocuments(api,id));
|
|
71
|
+
if(action==='init')return request(api,'GET','/api/doc/console/multi/init?'+new URLSearchParams({secCategoryId:id}));
|
|
72
|
+
if(action==='upload'){
|
|
73
|
+
if(!file||!receipt||!args.includes('--confirm'))throw Error('上传需要文件、全新回执路径和--confirm;仅保存,不发布');
|
|
74
|
+
const init=await request(api,'GET','/api/doc/console/multi/init?'+new URLSearchParams({secCategoryId:id}));
|
|
75
|
+
const defaults=uploadDefaults(init,file);
|
|
76
|
+
const before=await listDocuments(api,id),beforeIds=new Set(before.map(r=>r.id));
|
|
77
|
+
const state={state:'prepared',directoryId:id,accountId,file:path.resolve(file),ownerId:defaults.ownerid};
|
|
78
|
+
fs.writeFileSync(receipt,JSON.stringify(state,null,2),{flag:'wx'});
|
|
79
|
+
const saveState=()=>fs.writeFileSync(receipt,JSON.stringify(state,null,2));
|
|
80
|
+
try{
|
|
81
|
+
state.state='uploading';saveState();
|
|
82
|
+
const form=new FormData();form.append('category','document');form.append('file',new Blob([fs.readFileSync(file)]),path.basename(file));
|
|
83
|
+
const up=await request(api,'POST','/api/doc/upload/uploadFile',form);
|
|
84
|
+
if(String(up.status)!=='1'||!up.data?.fileid)throw Error('附件上传未确认');
|
|
85
|
+
state.attachmentId=String(up.data.fileid);state.state='saving';saveState();
|
|
86
|
+
const identity=await request(api,'GET','/api/hrm/login/getAccountList');
|
|
87
|
+
const current=identity.data?.accountlist?.filter(a=>String(a.iscurrent)==='1');
|
|
88
|
+
if(current?.length!==1||String(current[0].userid)!==accountId)throw Error('上传期间账号变化,停止保存');
|
|
89
|
+
const saved=await request(api,'POST','/api/doc/console/multi/saveUpload',{...defaults,secCategoryId:id,seccategory:id,fileIds:state.attachmentId});
|
|
90
|
+
if(saved.api_status!==true)throw Error('文档保存未确认');
|
|
91
|
+
const title=path.parse(up.data.filename||path.basename(file)).name;
|
|
92
|
+
const matches=(await listDocuments(api,id)).filter(r=>!beforeIds.has(r.id)&&r.title===title&&r.ownerId===defaults.ownerid&&r.creatorId===accountId&&r.attachmentCount===1&&r.status==='草稿');
|
|
93
|
+
if(matches.length!==1)throw Error('保存后文档回查不唯一或属性不一致');
|
|
94
|
+
state.state='draft-verified';state.document=(await withDocumentLinks(cfg,matches))[0];saveState();return state;
|
|
95
|
+
}catch(error){state.state='uncertain';state.error=error.message;saveState();throw Error(error.message+';查阅回执并查询目录核验,禁止自动重复上传/保存');}
|
|
96
|
+
}
|
|
97
|
+
throw Error('文档命令:directories [名称] | list <目录ID> | init <目录ID> | upload <目录ID> <文件> <新回执路径> --confirm');
|
|
98
|
+
}
|