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
@@ -0,0 +1,208 @@
1
+ import { constants, createPublicKey, publicEncrypt } from 'node:crypto';
2
+
3
+ // Intentionally independent of auth/config/token: no persistence, registration, or logging.
4
+ const messages = {
5
+ PASSWORD_INVALID_INPUT: '密码登录输入或平台地址无效', PASSWORD_NETWORK_ERROR: '密码登录网络请求失败',
6
+ PASSWORD_INVALID_URL: '密码登录平台地址无效', PASSWORD_TLS_REQUIRED: '密码登录仅支持HTTP或HTTPS地址',
7
+ PASSWORD_HTTP_CONSENT_REQUIRED: '当前HTTP站点尚未启用密码登录兼容设置',
8
+ PASSWORD_INVALID_USERNAME: '密码登录用户名无效', PASSWORD_INVALID_PASSWORD: '密码登录密码输入无效', PASSWORD_INVALID_OPTIONS: '密码登录选项无效',
9
+ PASSWORD_CAPTCHA_REQUIRED: '平台要求图片验证码,请提供人工验证码处理', PASSWORD_CAPTCHA_REJECTED: '原生图片验证码未通过,未自动重试',
10
+ PASSWORD_INPUT_UNAVAILABLE: '无法取得人工验证输入', PASSWORD_INPUT_INVALID: '人工验证输入格式无效', PASSWORD_CAPTCHA_INVALID: '人工验证码输入无效',
11
+ PASSWORD_TIMEOUT: '密码登录请求超时', PASSWORD_CANCELLED: '密码登录已取消',
12
+ PASSWORD_REDIRECT_REJECTED: '密码登录不允许重定向', PASSWORD_RESPONSE_TOO_LARGE: '密码登录响应超过安全上限',
13
+ PASSWORD_RESPONSE_INVALID: '密码登录响应格式无效', PASSWORD_HTTP_ERROR: '密码登录服务器请求失败',
14
+ PASSWORD_REQUIREMENTS_UNKNOWN: '无法确认账号登录要求,已停止', PASSWORD_CHALLENGE_UNSUPPORTED: '账号要求额外验证,请使用原生登录处理',
15
+ PASSWORD_RSA_INVALID: '无法确认原生加密参数,已停止', PASSWORD_RSA_CAPACITY: '登录输入超过原生加密容量,已停止',
16
+ PASSWORD_LOGIN_REJECTED: '原生密码认证未通过,未自动重试', PASSWORD_COOKIE_UNVERIFIED: '未取得可验证的新登录会话',
17
+ PASSWORD_CHANGE_REQUIRED: '平台要求修改密码,请先在原生界面处理', PASSWORD_POLICY_UNKNOWN: '无法确认平台密码策略,已停止',
18
+ PASSWORD_POLICY_UNSUPPORTED: '平台已启用弱密码限制,当前安全接口不支持验证,请使用原生登录',
19
+ PASSWORD_IDENTITY_MISMATCH: '无法核实唯一当前登录身份,已停止', FORCED_OFFLINE: '当前会话已被强制下线',
20
+ };
21
+ function fail(code) { const e = new Error(messages[code]); e.code = code; throw e; }
22
+ function object(v) { return v && typeof v === 'object' && !Array.isArray(v); }
23
+ function off(v) { return v === false || v === 0 || v === '0'; }
24
+ function on(v) { return v === true || v === 1 || v === '1'; }
25
+ function bit(v) { return off(v) || on(v); }
26
+ function pathMatches(path, scope) { return path === scope || path.startsWith(scope.endsWith('/') ? scope : scope + '/'); }
27
+ class CookieJar {
28
+ constructor() { this.items = new Map(); }
29
+ receive(headers, url) {
30
+ const lines = typeof headers.getSetCookie === 'function' ? headers.getSetCookie() : (headers.get('set-cookie') || '').split(/,(?=\s*[^;,\s]+=)/);
31
+ for (const line of lines) {
32
+ if (!line || /[\r\n]/.test(line)) continue;
33
+ const [pair, ...rest] = line.split(';').map(s=>s.trim()); const eq=pair.indexOf('='); if(eq<1) continue;
34
+ const name=pair.slice(0,eq), value=pair.slice(eq+1);
35
+ if (!/^[!#$%&'*+.^_`|~\w-]+$/.test(name) || /[^\x21-\x7e]|[;,]/.test(value)) continue;
36
+ const attrs=Object.fromEntries(rest.map(s=>{const i=s.indexOf('='); return i<0?[s.toLowerCase(),true]:[s.slice(0,i).toLowerCase(),s.slice(i+1)];}));
37
+ const domain = typeof attrs.domain==='string'?attrs.domain.replace(/^\./,'').toLowerCase():url.hostname;
38
+ // Exact host only is deliberately stricter than browser domain matching. No public-suffix scope.
39
+ if (domain!==url.hostname || (attrs.secure && url.protocol!=='https:')) continue;
40
+ const path=typeof attrs.path==='string' && attrs.path.startsWith('/')?attrs.path:url.pathname.slice(0,url.pathname.lastIndexOf('/')+1).replace(/\/$/,'')||'/';
41
+ let expiry=Infinity;
42
+ if (attrs['max-age']!==undefined) { if(!/^-?\d+$/.test(String(attrs['max-age']))) continue; expiry=Date.now()+Number(attrs['max-age'])*1000; }
43
+ else if(attrs.expires!==undefined) {expiry=Date.parse(attrs.expires);if(!Number.isFinite(expiry))continue;}
44
+ if(name.startsWith('__Secure-') && (!attrs.secure || url.protocol!=='https:'))continue;
45
+ if(name.startsWith('__Host-') && (!attrs.secure || attrs.domain || path!=='/'))continue;
46
+ const key=`${domain}\n${path}\n${name}`;
47
+ if(expiry<=Date.now())this.items.delete(key);else this.items.set(key,{name,value,domain,path,secure:!!attrs.secure,expiry});
48
+ }
49
+ }
50
+ applicable(url) { return [...this.items.values()].filter(c=>c.expiry>Date.now() && c.domain===url.hostname && (!c.secure||url.protocol==='https:') && pathMatches(url.pathname,c.path)).sort((a,b)=>b.path.length-a.path.length); }
51
+ header(url) { return this.applicable(url).map(c=>`${c.name}=${c.value}`).join('; '); }
52
+ exported(base) {
53
+ const api=new URL(base+'/api/hrm/login/getAccountList'), business=new URL(base+'/wui/index.html');
54
+ return this.applicable(business).filter(c=>pathMatches(api.pathname,c.path)).map(c=>`${c.name}=${c.value}`).join('; ');
55
+ }
56
+ }
57
+ function requirements(data) {
58
+ const s=data?.loginSetting;
59
+ if(!object(s)||!bit(s.openRSA)||!bit(s.hasValidateCode)||!bit(s.showDynamicPwd)||!['0','1','2','3','4','5','6'].includes(String(s.userUsbType)))fail('PASSWORD_REQUIREMENTS_UNKNOWN');
60
+ if(!off(s.showDynamicPwd)||String(s.userUsbType)!=='0')fail('PASSWORD_CHALLENGE_UNSUPPORTED');
61
+ // These keys are the complete native GetLoginFromCmd contract. Unknown policy keys fail closed.
62
+ const known=new Set(['userUsbType','hasMultiLang','hasValidateCode','validateCodeKey','numValidateWrong','validateType','showDynamicPwd','needforgotpassword','openRSA']);
63
+ if(Object.keys(s).some(k=>!known.has(k)))fail('PASSWORD_REQUIREMENTS_UNKNOWN');
64
+ const names=data.formItems?.map?.(i=>object(i)?i.name:null);
65
+ if(!Array.isArray(names)||names.length<2||names.length>3||!names.includes('loginid')||!names.includes('userpassword')||names.some(n=>!['loginid','userpassword','validatecode'].includes(n))||new Set(names).size!==names.length||(!on(s.hasValidateCode)&&names.includes('validatecode')))fail('PASSWORD_CHALLENGE_UNSUPPORTED');
66
+ if(Object.keys(data).some(k=>!['loginSetting','formItems','qrcode','message','status'].includes(k)))fail('PASSWORD_CHALLENGE_UNSUPPORTED');
67
+ if(data.status!==undefined && String(data.status)!=='1')fail('PASSWORD_REQUIREMENTS_UNKNOWN');
68
+ return on(s.openRSA);
69
+ }
70
+ function encrypt(value,rsa) {
71
+ try {
72
+ if(!object(rsa)||typeof rsa.rsa_pub!=='string'||typeof rsa.rsa_code!=='string'||!rsa.rsa_code||rsa.rsa_code.length>256||typeof rsa.rsa_flag!=='string'||!rsa.rsa_flag||rsa.rsa_flag.length>128)fail('PASSWORD_RSA_INVALID');
73
+ const key=createPublicKey(rsa.rsa_pub.includes('BEGIN')?rsa.rsa_pub:`-----BEGIN PUBLIC KEY-----\n${rsa.rsa_pub}\n-----END PUBLIC KEY-----`);
74
+ const bits=key.asymmetricKeyDetails?.modulusLength;
75
+ if(key.asymmetricKeyType!=='rsa'||!bits||bits<1024||bits>8192)fail('PASSWORD_RSA_INVALID');
76
+ // Native scripts split at 240 JS chars. Never reproduce their unsafe multi-block ambiguity.
77
+ const bytes=Buffer.from(value+rsa.rsa_code,'utf8');
78
+ if(value.length>240 || bytes.length>Math.ceil(bits/8)-11)fail('PASSWORD_RSA_CAPACITY');
79
+ return publicEncrypt({key,padding:constants.RSA_PKCS1_PADDING},bytes).toString('base64')+rsa.rsa_flag;
80
+ } catch(e) {if(messages[e?.code])throw e;fail('PASSWORD_RSA_INVALID');}
81
+ }
82
+
83
+ /** Validate before collecting credentials; an HTTP grant is exact to scheme/host/port. */
84
+ export function assertPasswordLoginTransport(cfg) {
85
+ try {
86
+ const u=new URL(cfg?.baseUrl);
87
+ if(!['http:','https:'].includes(u.protocol))fail('PASSWORD_TLS_REQUIRED');
88
+ if(u.username||u.password||u.search||u.hash||/%2f|%5c/i.test(u.pathname))fail('PASSWORD_INVALID_URL');
89
+ const loopback=u.hostname==='localhost'||u.hostname==='[::1]'||/^127(?:\.\d{1,3}){3}$/.test(u.hostname);
90
+ if(u.protocol==='http:'&&!loopback&&cfg?.httpPasswordLoginOrigin!==u.origin)fail('PASSWORD_HTTP_CONSENT_REQUIRED');
91
+ return u.href.replace(/\/+$/,'');
92
+ } catch(e) {if(messages[e?.code])throw e;fail('PASSWORD_INVALID_URL');}
93
+ }
94
+
95
+ /** Native HTTP authentication. No persistence, custom token registration, or logging.
96
+ * onCaptcha({image:Buffer,contentType,expiresAt?},{signal}) resolves to one human answer.
97
+ * The callback must honor signal to release input/file resources when cancelled.
98
+ * Only JPEG/PNG/GIF <=512KiB are delivered; no server expiry is invented.
99
+ * Callback wait is excluded from the network budget and bounded to 5 minutes.
100
+ * Network injection never changes origin consent, redirect or platform policy checks.
101
+ */
102
+ export async function passwordLogin(cfg, options = {}) {
103
+ try { if(!object(options))fail('PASSWORD_INVALID_OPTIONS');return await login(cfg,options); }
104
+ catch(e) { const code=Object.hasOwn(messages,e?.code)?e.code:'PASSWORD_NETWORK_ERROR';const safe=new Error(messages[code]);safe.code=code;throw safe; }
105
+ }
106
+ async function login(cfg,{username,password,fetchImpl=globalThis.fetch,timeoutMs=8000,totalTimeoutMs=45000,maxResponseBytes=1024*1024,signal,onCaptcha}={}) {
107
+ // Error objects are reconstructed below so no raw server/network/crypto text escapes.
108
+ const base=assertPasswordLoginTransport(cfg);
109
+ if(typeof username!=='string'||!username.trim()||username!==username.trim()||username.length>1024||/[\x00-\x1f\x7f]/.test(username))fail('PASSWORD_INVALID_USERNAME');
110
+ if(typeof password!=='string'||!password||password.length>16384||/[\x00\r\n]/.test(password))fail('PASSWORD_INVALID_PASSWORD');
111
+ if(typeof fetchImpl!=='function'||(onCaptcha!==undefined&&typeof onCaptcha!=='function')||(signal!==undefined&&!(signal instanceof AbortSignal))||![timeoutMs,totalTimeoutMs,maxResponseBytes].every(v=>Number.isSafeInteger(v)&&v>0)||timeoutMs>30000||totalTimeoutMs>120000||maxResponseBytes>4*1024*1024)fail('PASSWORD_INVALID_OPTIONS');
112
+ const jar=new CookieJar();let deadline=Date.now()+totalTimeoutMs;
113
+ async function request(path,method='GET',params,cookieOverride,{image=false,allow405=false}={}) {
114
+ if(signal?.aborted)fail('PASSWORD_CANCELLED');
115
+ const remaining=Math.min(timeoutMs,deadline-Date.now());if(remaining<=0)fail('PASSWORD_TIMEOUT');
116
+ const controller=new AbortController();let timeout;let abort;
117
+ const stop=new Promise((_,reject)=>{const rejectCode=code=>{controller.abort(); const e=new Error(messages[code]);e.code=code;reject(e);};timeout=setTimeout(()=>rejectCode('PASSWORD_TIMEOUT'),remaining);abort=()=>rejectCode('PASSWORD_CANCELLED');signal?.addEventListener('abort',abort,{once:true});});
118
+ try {
119
+ return await Promise.race([stop,(async()=>{
120
+ const url=new URL(base+path); const res=await fetchImpl(url,{method,redirect:'manual',credentials:'omit',signal:controller.signal,headers:{Accept:'*/*','User-Agent':'BDIP-Office-Assistant','X-Requested-With':'XMLHttpRequest',Cookie:cookieOverride ?? jar.header(url),...(method==='POST'?{'Content-Type':'application/x-www-form-urlencoded; charset=utf-8'}:{})},...(method==='POST'?{body:new URLSearchParams(params).toString()}:{})});
121
+ if(controller.signal.aborted)fail(signal?.aborted?'PASSWORD_CANCELLED':'PASSWORD_TIMEOUT');
122
+ if(res.redirected || (res.status>=300&&res.status<400)){void res.body?.cancel().catch(()=>{});fail('PASSWORD_REDIRECT_REJECTED');}
123
+ if(allow405&&res.status===405){void res.body?.cancel().catch(()=>{});return null;}
124
+ const cap=image?Math.min(maxResponseBytes,512*1024):maxResponseBytes;
125
+ const length=res.headers.get('content-length'); if(length && Number(length)>cap){void res.body?.cancel().catch(()=>{});fail('PASSWORD_RESPONSE_TOO_LARGE');}
126
+ if(!res.body?.getReader)fail('PASSWORD_RESPONSE_INVALID');
127
+ const reader=res.body.getReader();const chunks=[];let count=0;
128
+ try { while(true){const {done,value}=await reader.read();if(done)break;count+=value.byteLength;if(count>cap){void reader.cancel().catch(()=>{});fail('PASSWORD_RESPONSE_TOO_LARGE');}chunks.push(Buffer.from(value));} } finally {reader.releaseLock();}
129
+ if(controller.signal.aborted)fail(signal?.aborted?'PASSWORD_CANCELLED':'PASSWORD_TIMEOUT');
130
+ if(image){
131
+ if(res.status!==200)fail('PASSWORD_HTTP_ERROR');
132
+ const bytes=Buffer.concat(chunks),type=(res.headers.get('content-type')||'').split(';')[0].trim().toLowerCase();
133
+ const valid=(type==='image/jpeg'&&bytes.length>=4&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255&&bytes.at(-2)===255&&bytes.at(-1)===217)||(type==='image/png'&&bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))||(type==='image/gif'&&bytes.length>=13&&['GIF87a','GIF89a'].includes(bytes.subarray(0,6).toString('ascii')));
134
+ if(!valid)fail('PASSWORD_RESPONSE_INVALID');jar.receive(res.headers,url);return {image:bytes,contentType:type};
135
+ }
136
+ let data;try{data=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{fail('PASSWORD_RESPONSE_INVALID');}if(!object(data))fail('PASSWORD_RESPONSE_INVALID');
137
+ if(['003','004','FORCED_OFFLINE'].some(c=>[data.code,data.errorCode,data.msgcode].some(v=>String(v)===c)))fail('FORCED_OFFLINE');
138
+ if(res.status!==200)fail('PASSWORD_HTTP_ERROR');
139
+ jar.receive(res.headers,url);return data;
140
+ })()]);
141
+ } finally {clearTimeout(timeout);signal?.removeEventListener('abort',abort);}
142
+ }
143
+ const bootstrap=await request('/api/hrm/login/getLoginForm','POST',{loginid:'',langid:'7'});
144
+ const rsaEnabled=requirements(bootstrap);
145
+ const rsaInfo=()=>request('/rsa/weaver.rsa.GetRsaInfo?ts='+Date.now());
146
+ let loginid=username;
147
+ if(rsaEnabled)loginid=encrypt(username,await rsaInfo());
148
+ const account=await request('/api/hrm/login/getLoginForm','POST',{loginid,langid:'7'});
149
+ if(requirements(account)!==rsaEnabled)fail('PASSWORD_REQUIREMENTS_UNKNOWN');
150
+ let validatecode='',validateCodeKey='';
151
+ if(on(account.loginSetting.hasValidateCode)){
152
+ if(!onCaptcha)fail('PASSWORD_CAPTCHA_REQUIRED');
153
+ validateCodeKey=account.loginSetting.validateCodeKey;
154
+ if(typeof validateCodeKey!=='string'||!validateCodeKey||validateCodeKey.length>256||/[\x00-\x20\x7f]/.test(validateCodeKey))fail('PASSWORD_REQUIREMENTS_UNKNOWN');
155
+ if(account.loginSetting.validateType!==undefined&&!off(account.loginSetting.validateType))fail('PASSWORD_CHALLENGE_UNSUPPORTED');
156
+ // Observed native public image request: seriesnum_ + validateCodeKey, never key.
157
+ const challenge=await request('/weaver/weaver.file.MakeValidateCode?'+new URLSearchParams({seriesnum_:String(Date.now()),validateCodeKey}), 'GET',undefined,undefined,{image:true});
158
+ const start=Date.now(),captchaController=new AbortController();let timer,abort;
159
+ try{
160
+ const stop=new Promise((_,reject)=>{const rejectCode=code=>{const e=new Error(messages[code]);e.code=code;reject(e);captchaController.abort();};timer=setTimeout(()=>rejectCode('PASSWORD_TIMEOUT'),300000);abort=()=>rejectCode('PASSWORD_CANCELLED');signal?.addEventListener('abort',abort,{once:true});if(signal?.aborted)abort();});
161
+ validatecode=await Promise.race([stop,Promise.resolve().then(()=>onCaptcha(challenge,{signal:captchaController.signal}))]);
162
+ }catch(e){if(['PASSWORD_CANCELLED','PASSWORD_TIMEOUT','PASSWORD_INPUT_UNAVAILABLE','PASSWORD_INPUT_INVALID','PASSWORD_CAPTCHA_INVALID'].includes(e?.code))throw e;fail('PASSWORD_INPUT_UNAVAILABLE');}
163
+ finally{clearTimeout(timer);signal?.removeEventListener('abort',abort);captchaController.abort();deadline+=Date.now()-start;}
164
+ if(typeof validatecode!=='string'||!validatecode.trim()||validatecode!==validatecode.trim()||validatecode.length>128||/[\x00-\x20\x7f]/.test(validatecode))fail('PASSWORD_CAPTCHA_INVALID');
165
+ }
166
+ let userpassword=password;
167
+ if(rsaEnabled){const rsa=await rsaInfo();loginid=encrypt(username,rsa);userpassword=encrypt(password,rsa);}
168
+ const before=jar.exported(base);
169
+ const result=await request('/api/hrm/login/checkLogin','POST',{loginid,userpassword,islanguid:'7',logintype:'1',validatecode,validateCodeKey,dynamicPassword:'',tokenAuthKey:'',messages:'',isie:'false'});
170
+ if(String(result.msgcode)==='52')fail('PASSWORD_CAPTCHA_REJECTED');
171
+ const uid=String(result.userid??'');
172
+ if(String(result.loginstatus)!=='true'||String(result.msgcode)!=='0'||! /^[1-9]\d*$/.test(uid))fail('PASSWORD_LOGIN_REJECTED');
173
+ if((result.authUrl!==undefined&&result.authUrl!=='')||(result.qysflag!==undefined&&String(result.qysflag)!=='0')||(result.isRemoveSession!==undefined&&!off(result.isRemoveSession)))fail('PASSWORD_CHALLENGE_UNSUPPORTED');
174
+ const fresh=jar.exported(base);if(!fresh||fresh===before)fail('PASSWORD_COOKIE_UNVERIFIED');
175
+ const policy=await request('/api/hrm/login/remindLogin','POST',{logintype:'1'});
176
+ if(String(policy.status)!=='1'||(policy.isUpPswd!==undefined&&!['true','false'].includes(String(policy.isUpPswd))))fail('PASSWORD_POLICY_UNKNOWN');
177
+ if(String(policy.isUpPswd)==='true')fail('PASSWORD_CHANGE_REQUIRED');
178
+ if(policy.passwdReminder!==undefined&&!bit(policy.passwdReminder))fail('PASSWORD_POLICY_UNKNOWN');
179
+ if(on(policy.passwdReminder)){if(!bit(policy.canpass))fail('PASSWORD_POLICY_UNKNOWN');if(!on(policy.canpass))fail('PASSWORD_CHANGE_REQUIRED');}
180
+ // Native online script uses POST {password:C}; legacy backend may be GET only. Metadata
181
+ // GET fallback is metadata only: no secret or password-strength proof.
182
+ // Never put plaintext/ciphertext in a URL. On fallback only explicitly
183
+ // disabled prohibition can proceed; enabled/unknown policy requires native UI.
184
+ let weak=await request('/api/hrm/password/isWeakPassword','POST',{password:userpassword},undefined,{allow405:true});
185
+ const metadataOnly=weak===null;
186
+ if(metadataOnly)weak=await request('/api/hrm/password/isWeakPassword');
187
+ if((weak.status!==undefined&&String(weak.status)!=='1')||!bit(weak.weakPasswordDisable)||(!metadataOnly&&typeof weak.isWeakPassword!=='boolean'))fail('PASSWORD_POLICY_UNKNOWN');
188
+ if(on(weak.weakPasswordDisable)&&(metadataOnly||weak.isWeakPassword))fail('PASSWORD_POLICY_UNSUPPORTED');
189
+ const identity=await request('/api/hrm/login/getAccountList');
190
+ const rows=identity?.data?.accountlist;
191
+ const current=Array.isArray(rows)?rows.filter(r=>object(r)&&String(r.iscurrent)==='1'):[];
192
+ if(identity.status!==undefined&&String(identity.status)!=='1')fail('PASSWORD_IDENTITY_MISMATCH');
193
+ if(current.length!==1||String(current[0].userid)!==uid)fail('PASSWORD_IDENTITY_MISMATCH');
194
+ const cookie=jar.exported(base);if(!cookie)fail('PASSWORD_COOKIE_UNVERIFIED');
195
+ // Prove the exact portable candidate, not a richer API-path jar header that
196
+ // might contain the only authenticating cookie. One final read; never loop.
197
+ const finalIdentity=await request('/api/hrm/login/getAccountList','GET',undefined,cookie);
198
+ const finalRows=finalIdentity?.data?.accountlist;
199
+ const finalCurrent=Array.isArray(finalRows)?finalRows.filter(r=>object(r)&&String(r.iscurrent)==='1'):[];
200
+ if((finalIdentity.status!==undefined&&String(finalIdentity.status)!=='1')||finalCurrent.length!==1||String(finalCurrent[0].userid)!==uid)fail('PASSWORD_IDENTITY_MISMATCH');
201
+ // Even benign rotation changes the candidate and needs a new proof. Stop
202
+ // conservatively rather than return credentials that were not actually sent.
203
+ if(jar.exported(base)!==cookie)fail('PASSWORD_COOKIE_UNVERIFIED');
204
+ // Native GetAccountListCmd returns userid/username/iscurrent, not canonical loginid.
205
+ // UID is bound to the single fresh password-authenticated session above. Do not
206
+ // mislabel a display name or user-supplied alias as a verified canonical login ID.
207
+ return {cookie,userId:uid,loginId:null,mode:'password-http'};
208
+ }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ const stateDir = () => path.resolve(process.cwd(), 'state');
6
+ const queryDir = () => path.join(stateDir(), 'query_contexts');
7
+
8
+ export function saveCandidateQuery({ workflow, candidates, options }) {
9
+ fs.mkdirSync(queryDir(), { recursive: true });
10
+ const id = randomUUID();
11
+ fs.writeFileSync(path.join(queryDir(), `${id}.json`), JSON.stringify({ id, workflow, candidates, options, createdAt: new Date().toISOString() }, null, 2), 'utf8');
12
+ return id;
13
+ }
14
+ export function loadCandidateQuery(id) {
15
+ if (!/^[a-zA-Z0-9-]+$/.test(String(id)))
16
+ throw new Error('resume-query 格式无效。');
17
+ const file = path.join(queryDir(), `${id}.json`);
18
+ if (!fs.existsSync(file))
19
+ throw new Error('查询上下文不存在或已被清理,请重新搜索流程名称。');
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ }
22
+ export function persistSearchResult(result, title = '待办查询') {
23
+ const json = JSON.stringify(result, null, 2);
24
+ if (Buffer.byteLength(json, 'utf8') <= 8192)
25
+ return null;
26
+ fs.mkdirSync(stateDir(), { recursive: true });
27
+ const jsonPath = path.join(stateDir(), 'last_search.json');
28
+ const mdPath = path.join(stateDir(), 'last_search.md');
29
+ fs.writeFileSync(jsonPath, json, 'utf8');
30
+ const items = Array.isArray(result.items)
31
+ ? result.items
32
+ : (Array.isArray(result.groups) ? result.groups.flatMap((group) => group.items ?? []) : []);
33
+ const lines = [`# ${title}`, '', `共 ${result.total ?? items.length} 条,以下为前 10 条:`, ''];
34
+ for (const item of items.slice(0, 10))
35
+ lines.push(`- ${item.title ?? '(无标题)'}(${item.requestId ?? ''})`);
36
+ fs.writeFileSync(mdPath, lines.join('\n'), 'utf8');
37
+ return { jsonPath, mdPath };
38
+ }
package/dist/render.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 人类可读输出渲染:框线表格、分隔线。
3
+ * 集中框线宽度,避免散落的字面量重复。
4
+ */
5
+ export const BOX_TOP = '═'.repeat(50); // 上下封口
6
+ export const DIVIDER = '─'.repeat(53); // 区内分隔
7
+ /**
8
+ * 打印一个标题框:顶线 + title + 可选正文行 + 底线。
9
+ * title/lines 已含前导空格等格式,由调用方决定对齐。
10
+ */
11
+ export function box(title, lines = []) {
12
+ console.log(BOX_TOP);
13
+ if (title)
14
+ console.log(title);
15
+ for (const l of lines)
16
+ console.log(l);
17
+ console.log(BOX_TOP);
18
+ }
19
+ export function divider() {
20
+ console.log(DIVIDER);
21
+ }
package/dist/report.js ADDED
@@ -0,0 +1,50 @@
1
+ import { endpointFor } from './config.js';
2
+ import { getWfList } from './workflow.js';
3
+ import { getMyCalendar } from './workplan.js';
4
+ import { getEmailList } from './email.js';
5
+ import { getButtons } from './kq.js';
6
+ import { getMyBlogList, isBlogRecalled } from './blog.js';
7
+
8
+ function source(module, id, time, data) {
9
+ return { module, id: String(id ?? ''), time: String(time ?? ''), data };
10
+ }
11
+ function onDate(value, date) {
12
+ return String(value ?? '').slice(0, 10) === date;
13
+ }
14
+
15
+ /** 只汇总当前 BDIP5 环境可回溯且状态可确认的数据;不把聊天内容、浏览行为或撤回微博当作事实。 */
16
+ export async function collectQueryableBusiness(cfg, date) {
17
+ const results = await Promise.allSettled([
18
+ getWfList(cfg, { category: 'done', page: 1, operateTime: date }),
19
+ getWfList(cfg, { category: 'mine', page: 1, createTime: date }),
20
+ getMyCalendar(cfg, { selectDate: date }),
21
+ getEmailList(cfg, { folder: 'inbox', current: 1, startdate: date, enddate: date }),
22
+ getEmailList(cfg, { folder: 'sent', current: 1, startdate: date, enddate: date }),
23
+ getButtons(cfg),
24
+ getMyBlogList(cfg, date, 1),
25
+ ]);
26
+ const value = (index, fallback) => results[index].status === 'fulfilled' ? results[index].value : fallback;
27
+ const failed = results.map((result, index) => result.status === 'rejected' ? ({ index, message: String(result.reason?.message ?? result.reason) }) : null).filter(Boolean);
28
+ const done = value(0, { items: [] }).items.filter((item) => onDate(item.operatedate ?? item.operateTime, date)).map((item) => source('已办流程', item.requestId, item.operatedate ?? item.operateTime, { title: item.title }));
29
+ const mine = value(1, { items: [] }).items.filter((item) => onDate(item.createdate ?? item.createTime, date)).map((item) => source('我发起的流程', item.requestId, item.createdate ?? item.createTime, { title: item.title }));
30
+ const calendar = value(2, { items: [] }).items.filter((item) => onDate(item.beginDateTime, date)).map((item) => source('日程', item.id, item.beginDateTime, { name: item.name, status: item.status }));
31
+ const inbox = value(3, { items: [] }).items.filter((item) => onDate(item.date, date)).map((item) => source('收件箱', item.id, item.date, { subject: item.subject, from: item.from }));
32
+ const sent = value(4, { items: [] }).items.filter((item) => onDate(item.date, date)).map((item) => source('已发送邮件', item.id, item.date, { subject: item.subject, to: item.to }));
33
+ const attendance = value(5, { timeline: [] }).timeline.map((item) => source('考勤', `${item.date}:${item.type}`, item.signTime, { type: item.type, signTime: item.signTime, status: item.kqstatus }));
34
+ // 微博:列表接口返回 isReCalled(已抓包验证,0=正常发布,1/2=已撤回)。
35
+ // 仅纳入正常发布(isReCalled=0)的微博,已撤回微博剔除并记入 notes。
36
+ const blogToday = value(6, { items: [] }).items.filter((it) => onDate(it.workdate, date) || onDate(it.createdate, date));
37
+ const recalledBlogs = blogToday.filter((it) => isBlogRecalled(it));
38
+ const blogs = blogToday.filter((it) => !isBlogRecalled(it)).map((item) => source('微博', item.id, item.workdate, { content: item.content?.slice(0, 60) }));
39
+ const notes = [];
40
+ if (recalledBlogs.length > 0)
41
+ notes.push(`微博:${recalledBlogs.length} 条已撤回(isReCalled≠0)未纳入汇总`);
42
+ return {
43
+ scope: '当前 BDIP5 环境可查询业务数据汇总(不含操作审计、页面浏览及其他不可查询行为)',
44
+ baseUrl: endpointFor(cfg).baseUrl,
45
+ date,
46
+ sources: { done, mine, calendar, inbox, sent, attendance, blogs },
47
+ notes,
48
+ failures: failed,
49
+ };
50
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig } from './config.js';
4
+ import { SessionHeartbeat } from './session.js';
5
+ import { keepalivePaths } from './keepalive.js';
6
+
7
+ async function run() {
8
+ const [configPath, statePath, leaseId] = process.argv.slice(2);
9
+ const cfg = loadConfig(configPath);
10
+ const paths = keepalivePaths(cfg);
11
+ if (path.resolve(statePath ?? '') !== paths.state || !/^[a-f0-9]{48}$/.test(leaseId ?? '')) throw new Error('Invalid lease');
12
+ const read = () => { try { return JSON.parse(fs.readFileSync(paths.state, 'utf8')); } catch { return null; } };
13
+ const initial = read();
14
+ if (initial?.leaseId !== leaseId || initial.status !== 'starting' || initial.expiresAt <= Date.now()) return;
15
+ let finished = false, watch, heartbeat, failures = 0;
16
+ const write = patch => {
17
+ const current = read();
18
+ if (current?.leaseId !== leaseId) return false;
19
+ fs.writeFileSync(paths.state, JSON.stringify({ ...current, ...patch, pid: process.pid }), { mode: 0o600 });
20
+ return true;
21
+ };
22
+ const finish = (status, message) => {
23
+ if (finished) return;
24
+ finished = true;
25
+ if (watch) clearInterval(watch);
26
+ heartbeat?.stop();
27
+ write({ status, message, active: false, lastSeen: Date.now(), endedAt: Date.now() });
28
+ process.off('SIGINT', interrupt); process.off('SIGTERM', interrupt);
29
+ };
30
+ const interrupt = () => finish('stopped', '已停止保活');
31
+ heartbeat = new SessionHeartbeat(cfg, { unref: false, onState: state => {
32
+ if (finished) return;
33
+ failures = state.authenticated ? 0 : failures + 1;
34
+ if (state.authenticated) {
35
+ write({ status: 'running', lastSeen: Date.now(), userId: state.userId, loginId: state.loginId,
36
+ mode: state.mode, idleTimeoutSeconds: state.idleTimeoutSeconds, message: state.message ?? null, failures: 0 });
37
+ } else {
38
+ write({ lastSeen: Date.now(), failures, message: state.message, code: state.code });
39
+ if (['FORCED_OFFLINE', 'SESSION_EXPIRED', 'NO_COOKIE', 'PROFILE_CHANGED'].includes(state.code) || failures >= 3) finish('failed', state.message);
40
+ }
41
+ } });
42
+ process.once('SIGINT', interrupt); process.once('SIGTERM', interrupt);
43
+ // Observe cancellation even while the first network check is still pending.
44
+ watch = setInterval(() => {
45
+ const current = read();
46
+ if (current?.leaseId !== leaseId) finish('stopped', '保活租约已被替换');
47
+ else if (current.stopRequested) finish('stopped', '已按请求停止保活');
48
+ else if (Date.now() >= initial.expiresAt) finish('expired', '保活已达到设定截止时间');
49
+ }, 250);
50
+ await heartbeat.start();
51
+ if (finished) heartbeat.stop();
52
+ else if (!heartbeat.state?.authenticated) finish('failed', heartbeat.state?.message ?? '当前用户检查失败');
53
+ }
54
+ run().catch(() => { process.exitCode = 1; });