bdip-office-assistant-cli 0.0.0-stage → 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 +37 -2
- package/dist/accounts.js +30 -0
- package/dist/args.js +36 -0
- package/dist/auth-token.js +783 -0
- package/dist/auth.js +331 -0
- package/dist/blog.js +562 -0
- package/dist/browser-links.js +224 -0
- package/dist/captcha-display.js +30 -0
- package/dist/cli.js +5533 -0
- package/dist/client.js +254 -0
- package/dist/config.js +170 -0
- package/dist/createcontext.js +41 -0
- package/dist/crypto.js +139 -0
- package/dist/cube.js +1771 -0
- package/dist/documents.js +98 -0
- package/dist/email.js +841 -0
- package/dist/field-preparation.js +237 -0
- package/dist/fileutil.js +20 -0
- package/dist/formula-runtime.js +55 -0
- package/dist/hrm.js +478 -0
- package/dist/keepalive.js +99 -0
- package/dist/kq.js +280 -0
- package/dist/linkage-calculations.js +68 -0
- package/dist/links.js +34 -0
- package/dist/meetings.js +172 -0
- package/dist/nodemap.js +242 -0
- package/dist/odoc.js +199 -0
- package/dist/office-tools.js +155 -0
- package/dist/pageopen.js +29 -0
- package/dist/parse.js +177 -0
- package/dist/password-input.js +84 -0
- package/dist/password-login.js +208 -0
- package/dist/person-selection.js +81 -0
- package/dist/querystate.js +38 -0
- package/dist/render.js +21 -0
- package/dist/report.js +50 -0
- package/dist/session-worker.js +54 -0
- package/dist/session.js +280 -0
- package/dist/submissionplan.js +45 -0
- package/dist/submissionstate.js +81 -0
- package/dist/subordinate.js +458 -0
- package/dist/supervise.js +152 -0
- package/dist/todoaskidea.js +54 -0
- package/dist/todobatch.js +172 -0
- package/dist/todocc.js +128 -0
- package/dist/todoforward.js +66 -0
- package/dist/todoreject.js +215 -0
- package/dist/todosubmit.js +325 -0
- package/dist/todotransfer.js +54 -0
- package/dist/util.js +25 -0
- package/dist/wfagent.js +538 -0
- package/dist/wfcreate.js +102 -0
- package/dist/wfform.js +169 -0
- package/dist/wfops.js +40 -0
- package/dist/wfreceiver.js +43 -0
- package/dist/wfstart.js +558 -0
- package/dist/wfsummary.js +89 -0
- package/dist/wfupload.js +83 -0
- package/dist/wfuser.js +96 -0
- package/dist/workflow-runtime.js +177 -0
- package/dist/workflow.js +570 -0
- package/dist/workplan.js +431 -0
- package/package.json +29 -6
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {definitions,detailTables,fingerprint,loadAdapter,buildDraft,verifyDraft,queryBrowser} from './workflow-runtime.js';
|
|
2
|
+
import {createApiFromProfile} from './client.js';
|
|
3
|
+
import {parseFormula,evaluateFormula,formulaDateFunctions} from './formula-runtime.js';
|
|
4
|
+
import {executeCalculations} from './linkage-calculations.js';
|
|
5
|
+
|
|
6
|
+
const clone=x=>structuredClone(x);
|
|
7
|
+
const cell=x=>x&&typeof x==='object'&&'value'in x?clone(x):{value:x??''};
|
|
8
|
+
const fieldKey=id=>String(id).replace(/^field/,'');
|
|
9
|
+
const equal=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
10
|
+
export function paragraphText(value,field) {
|
|
11
|
+
if(Number(field?.htmltype)!==2 || Number(field?.detailtype)!==2 || !/[\r\n]/.test(String(value)) || /<[^>]+>/.test(String(value))) return value;
|
|
12
|
+
const escape=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
13
|
+
return String(value).replace(/\r\n/g,'\n').split('\n').map(line=>'<p>'+escape(line)+'</p>').join('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fail=(state,message)=>{const error=Error(message);error.preparationState=state;throw error;};
|
|
17
|
+
export function stableConfig(value) {
|
|
18
|
+
if(Array.isArray(value))return value.map(stableConfig);
|
|
19
|
+
if(value&&typeof value==='object')return Object.fromEntries(Object.keys(value).sort().filter(k=>!['systemdatas','files','linkageUUID','currenttime','signatureSecretKey','signatureAttributesStr','formulakey'].includes(k)).map(k=>[k,stableConfig(value[k])]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
// 小语法解释器:字段引用、字符串、IF和相等判断;从不执行服务器JavaScript。
|
|
23
|
+
export function formulaValue(text,read) {
|
|
24
|
+
const tokens=text.match(/\[(?:main|detail_\d+)\.-?\d+\]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|==|!=|IF|[(),]|\s+|-?\d+(?:\.\d+)?/g)??[];
|
|
25
|
+
if(tokens.join('')!==text)fail('unsupported','不支持的公式语法');
|
|
26
|
+
const ts=tokens.filter(t=>!/^\s+$/.test(t));let i=0;
|
|
27
|
+
const primary=()=>{
|
|
28
|
+
const t=ts[i++];
|
|
29
|
+
if(t==='IF'){if(ts[i++]!=='(')throw Error('IF语法');const cond=expr();if(ts[i++]!==',')throw Error('IF语法');const yes=expr();if(ts[i++]!==',')throw Error('IF语法');const no=expr();if(ts[i++]!==')')throw Error('IF语法');return cond?yes:no;}
|
|
30
|
+
if(/^\[/.test(t??''))return read(t.slice(1,-1));
|
|
31
|
+
if(/^['"]/.test(t??''))return t.slice(1,-1);
|
|
32
|
+
if(/^-?\d/.test(t??''))return t;
|
|
33
|
+
fail('unsupported','无法解释公式');
|
|
34
|
+
};
|
|
35
|
+
const expr=()=>{let a=primary();if(ts[i]==='=='||ts[i]==='!='){const op=ts[i++],b=primary();a=op==='=='?String(a)===String(b):String(a)!==String(b);}return a;};
|
|
36
|
+
const result=expr();if(i!==ts.length)fail('unsupported','公式存在未处理内容');return result;
|
|
37
|
+
}
|
|
38
|
+
export async function executeLinkages(api,form,tables,input,{maxSteps=100,covered=[]}={}) {
|
|
39
|
+
const f=clone(form),t=clone(tables),defs=definitions(f),cfg=f.linkageCfg??{};
|
|
40
|
+
const evidence={state:'preparing',coverage:[],rowChanges:[]};
|
|
41
|
+
// 未支持的配置必须明确由已验证适配承担,不能吞错继续。
|
|
42
|
+
const supported=['formulaCfg','dataInputCfg','fieldSqlCfg','mainCalCfg','colCalCfg','rowCalCfg','fieldDateCfg','viewAttrCfg'];
|
|
43
|
+
const unsupported=Object.entries(cfg).filter(([kind,data])=>!supported.includes(kind)&&JSON.stringify(data)!=='{}'&&JSON.stringify(data)!=='[]'&&!covered.includes(kind)).map(([kind])=>kind);
|
|
44
|
+
if(unsupported.length)fail('unsupported','联动配置尚未支持:'+unsupported.join('、'));
|
|
45
|
+
const getCells=(table,row)=>table==='main'?f.maindata:t[table]?.rows[row];
|
|
46
|
+
const protectedValues=new Map();
|
|
47
|
+
const write=(table,row,id,value,source)=>{
|
|
48
|
+
const field=defs.find(x=>x.table===table&&x.id===String(id));if(!field)fail('unsupported','联动目标字段不存在:'+id);
|
|
49
|
+
const cells=getCells(table,row);if(!cells)fail('unsupported','联动行不存在');
|
|
50
|
+
const next=cell(value);
|
|
51
|
+
// 联动可能先产生中间值;只在全部规则稳定后检查是否覆盖用户值。
|
|
52
|
+
if(String(cells['field'+id]?.value??'')===String(next.value)&&(!next.specialobj||equal(cells['field'+id]?.specialobj,next.specialobj)))return false;
|
|
53
|
+
cells['field'+id]=next;return true;
|
|
54
|
+
};
|
|
55
|
+
const applyUserInput=()=>{
|
|
56
|
+
for(let [raw,v]of Object.entries(input.values??{})){
|
|
57
|
+
const id=fieldKey(raw);if(Number(id)<0)continue;
|
|
58
|
+
const d=defs.find(x=>x.id===id&&x.table==='main');if(!d||![2,3].includes(Number(d.viewattr)))fail('conflict','禁止用户覆盖只读/隐藏字段:'+id);
|
|
59
|
+
v=paragraphText(v,d);
|
|
60
|
+
write('main',-1,id,{value:v,specialobj:input.browserNames?.[id]?[{name:input.browserNames[id]}]:undefined});protectedValues.set(`main.${id}:-1`,v);
|
|
61
|
+
}
|
|
62
|
+
for(const dt of input.details??[]){const mark='detail_'+(Number(dt.index)+1);if(!t[mark])fail('unsupported','明细不存在');
|
|
63
|
+
if(input.requestId&&t[mark].rows.length!==dt.rows.length)fail('unsupported','用户增删明细需专门适配');
|
|
64
|
+
if(!input.requestId)t[mark].rows=dt.rows.map(()=>clone(t[mark].addRowDefValue??{}));
|
|
65
|
+
dt.rows.forEach((row,i)=>Object.entries(row).forEach(([raw,v])=>{const id=fieldKey(raw),d=defs.find(x=>x.id===id&&x.table===mark);if(!d||![2,3].includes(Number(d.viewattr)))fail('conflict','禁止用户覆盖只读明细:'+id);v=paragraphText(v,d);write(mark,i,id,{value:v,specialobj:input.detailBrowserNames?.[dt.index]?.[i]?.[id]?[{name:input.detailBrowserNames[dt.index][i][id]}]:undefined});protectedValues.set(`${mark}.${id}:${i}`,v);}));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// 新建表单先完成初始化带值,再应用用户填写;初始化规则不能反复覆盖用户明细。
|
|
69
|
+
let userApplied=!!input.requestId||!Object.keys(cfg.dataInputCfg??{}).length;
|
|
70
|
+
if(userApplied)applyUserInput();
|
|
71
|
+
const seen=new Map();let steps=0;
|
|
72
|
+
const formulas=cfg.formulaCfg?.formulas??[],dateFunctions=formulaDateFunctions(api);
|
|
73
|
+
for(let pass=0;pass<maxSteps;pass++){
|
|
74
|
+
let changed=false;
|
|
75
|
+
changed=await executeCalculations({api,form:f,tables:t,input,defs,getCells,write,seen,evidence,step:()=>{if(++steps>maxSteps)fail('nonconvergent','联动超限或循环不收敛');}})||changed;
|
|
76
|
+
for(const rule of formulas){
|
|
77
|
+
const m=String(rule.formula??'').match(/^\[(main|detail_\d+)\.(-?\d+)\]\s*=\s*(.*)$/);if(!m)fail('unsupported','公式目标语法不支持');
|
|
78
|
+
const [,table,id,expression]=m;
|
|
79
|
+
const rows=table==='main'?[-1]:(t[table]?.rows??[]).map((_,i)=>i);
|
|
80
|
+
for(const row of rows){const read=ref=>{const [tab,fid]=ref.split('.');return getCells(tab,tab==='main'?-1:row)?.['field'+fid]?.value??'';};
|
|
81
|
+
const sum=expression.trim().match(/^SUM\(\[(detail_\d+)\.(\d+)\]\)$/);
|
|
82
|
+
let value;
|
|
83
|
+
if(sum){
|
|
84
|
+
const source=t[sum[1]];
|
|
85
|
+
if(!source)fail('unsupported','SUM明细表不存在');
|
|
86
|
+
value=source.rows.reduce((total,r)=>{
|
|
87
|
+
const raw=r['field'+sum[2]]?.value??'';
|
|
88
|
+
const n=raw===''?0:Number(raw);
|
|
89
|
+
if(!Number.isFinite(n))fail('unsupported','SUM明细包含非数值');
|
|
90
|
+
return total+n;
|
|
91
|
+
},0);
|
|
92
|
+
}else value=await evaluateFormula(parseFormula(expression),ref=>{
|
|
93
|
+
const raw=read(ref),id=ref.split('.')[1],d=defs.find(x=>x.id===id);
|
|
94
|
+
return Number(d?.htmltype)===1&&[2,3,4,5].includes(Number(d.detailtype))?Number(raw||0):raw;
|
|
95
|
+
},dateFunctions);
|
|
96
|
+
const sourceRef=expression.trim().match(/^\[(main|detail_\d+)\.(\d+)\]$/);
|
|
97
|
+
const next=sourceRef?getCells(sourceRef[1],sourceRef[1]==='main'?-1:row)?.['field'+sourceRef[2]]:{value};
|
|
98
|
+
// 双向互拷贝以非空输入为起点;未设置一端不能清掉另一端。
|
|
99
|
+
if(sourceRef&&String(next?.value??'')===''&&String(getCells(table,row)?.['field'+id]?.value??'')!=='')continue;
|
|
100
|
+
changed=write(table,row,id,next??{value},'formula')||changed;
|
|
101
|
+
evidence.coverage.push({kind:'formula',target:table+'.'+id,row,state:'executed'});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for(const [trigger,rules]of Object.entries(cfg.fieldSqlCfg??{}))for(const rule of rules){
|
|
105
|
+
const targetIds=String(rule.assignField??'').split(',').filter(Boolean);
|
|
106
|
+
const targetDefs=targetIds.map(id=>defs.find(d=>d.id===id));
|
|
107
|
+
if(!targetDefs.length||targetDefs.some(d=>!d))fail('unsupported','SQL字段查询目标未知');
|
|
108
|
+
const table=targetDefs[0].table;
|
|
109
|
+
if(targetDefs.some(d=>d.table!==table))fail('unsupported','SQL字段查询跨表目标需适配');
|
|
110
|
+
for(const row of table==='main'?[-1]:(t[table]?.rows??[]).map((_,i)=>i)){
|
|
111
|
+
const params={};
|
|
112
|
+
for(const id of rule.relateFields??[]){const def=defs.find(d=>d.id===String(id));if(!def)fail('unsupported','SQL依赖字段不存在');params['field'+id+(def.table==='main'?'':'_'+row)]=getCells(def.table,def.table==='main'?-1:row)?.['field'+id]?.value??'';}
|
|
113
|
+
const key='sql:'+rule.config+':'+row,signature=JSON.stringify(params);if(seen.get(key)===signature)continue;
|
|
114
|
+
if(++steps>maxSteps)fail('nonconvergent','字段查询超限');seen.set(key,signature);
|
|
115
|
+
const p=f.params,body={...params,requestid:String(input.requestId??'-1'),workflowid:String(input.workflowId),nodeid:String(p.nodeid),formid:String(p.formid),isbill:String(p.isbill??'1'),triSource:'1',showAI:'1',linkageid:String(rule.config),linkageUUID:f.submitParams.linkageUUID??'',
|
|
116
|
+
['triFieldid_'+rule.config]:trigger==='emptyTriField'?'':trigger,['rowIndexStr_'+rule.config]:String(row),['triTableMark_'+rule.config]:table,f_weaver_belongto_userid:input.accountId,f_weaver_belongto_usertype:String(p.f_weaver_belongto_usertype??'0')};
|
|
117
|
+
const response=await api.post('/api/workflow/linkage/reqFieldSqlResult',body,{forceCookie:true});const info=response.json?.['assignInfo_'+rule.config];
|
|
118
|
+
if(!info||info.resultOutOfLimit||response.json?.status===false)fail('failed','字段查询失败:'+rule.config);
|
|
119
|
+
for(const key of Object.keys(info))if(!['changeValue','linkageSql','resultOutOfLimit'].includes(key))fail('unsupported','字段查询响应未支持:'+key);
|
|
120
|
+
for(const [raw,value]of Object.entries(info.changeValue??{})){const m=raw.match(/^field(\d+)(?:_(\d+))?$/);if(!m)fail('unsupported','SQL目标格式未知');const d=defs.find(d=>d.id===m[1]);changed=write(d?.table,m[2]?Number(m[2]):d?.table==='main'?-1:row,m[1],value,'server')||changed;}
|
|
121
|
+
evidence.coverage.push({kind:'fieldSql',config:String(rule.config),row,state:'executed'});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for(const [raw,configs]of Object.entries(cfg.dataInputCfg??{})){
|
|
125
|
+
const id=fieldKey(raw),d=defs.find(x=>x.id===id);if(!d||!Array.isArray(configs))fail('unsupported','数据联动配置无效');
|
|
126
|
+
for(const row of d.table==='main'?[-1]:(t[d.table]?.rows??[]).map((_,i)=>i))for(const rule of configs){
|
|
127
|
+
const key=`${rule.config}:${d.table}:${row}`;
|
|
128
|
+
const values={};for(const field of defs){const cells=getCells(field.table,field.table==='main'?-1:row);if(cells?.['field'+field.id])values['field'+field.id]=cells['field'+field.id].value;}
|
|
129
|
+
const signature=JSON.stringify([id,...(rule.relateFields??[])].map(fid=>[fid,values['field'+fid]??'']));if(seen.get(key)===signature)continue;
|
|
130
|
+
if(++steps>maxSteps)fail('nonconvergent','联动超限或循环不收敛');seen.set(key,signature);
|
|
131
|
+
const p=f.params,sp=f.submitParams;
|
|
132
|
+
const body={...values,requestid:String(input.requestId??'-1'),workflowid:String(input.workflowId),nodeid:String(p.nodeid),formid:String(p.formid),isbill:String(p.isbill??'1'),triSource:'1',showAI:'1',linkageid:String(rule.config),linkageUUID:sp.linkageUUID??'',
|
|
133
|
+
[`triFieldid_${rule.config}`]:id,[`rowIndexStr_${rule.config}`]:String(row),[`triTableMark_${rule.config}`]:d.table,f_weaver_belongto_userid:input.accountId,f_weaver_belongto_usertype:String(p.f_weaver_belongto_usertype??'0')};
|
|
134
|
+
if(row>=0)for(const [k,v]of Object.entries(values))body[k+'_'+row]=v;
|
|
135
|
+
for(const assigned of rule.otherParams?.assignfields??[]) {
|
|
136
|
+
const assignedDef=defs.find(x=>x.id===String(assigned));
|
|
137
|
+
const dependencies=assignedDef?.browserattr?.cusDependShip??[];
|
|
138
|
+
if(dependencies.length){const depend={};for(const dep of dependencies){const d=defs.find(x=>x.id===String(dep.fieldid));if(!d)fail('unsupported','自定义浏览框依赖不存在');const c=getCells(d.table,d.table==='main'?-1:row);if(!c&&row>=0)fail('unsupported','缺少自定义浏览框同行依赖');depend[dep.key]=c?.['field'+dep.fieldid]?.value??'';}body['customDepend_field'+assigned+(assignedDef.table==='main'?'':'_'+row)]=JSON.stringify(depend);}
|
|
139
|
+
}
|
|
140
|
+
const r=await api.post('/api/workflow/linkage/reqDataInputResult',body,{forceCookie:true});
|
|
141
|
+
const info=r.json?.['assignInfo_'+rule.config];if(!info||info.resultOutOfLimit||info.error||r.json?.status===false)fail('failed','数据联动失败/超限:'+rule.config);
|
|
142
|
+
for(const k of Object.keys(info))if(!['linkageSql','changeValue','addDetailRow','resultOutOfLimit'].includes(k))fail('unsupported','联动响应操作未支持:'+k);
|
|
143
|
+
for(const [target,v]of Object.entries(info.changeValue??{})){
|
|
144
|
+
const match=target.match(/^field(\d+)(?:_(\d+))?$/);if(!match)fail('unsupported','联动目标格式未知');const dest=defs.find(x=>x.id===match[1]);
|
|
145
|
+
changed=write(dest?.table,match[2]?Number(match[2]):dest?.table==='main'?-1:row,match[1],v,'server')||changed;
|
|
146
|
+
}
|
|
147
|
+
for(const [mark,rows]of Object.entries(info.addDetailRow??{})){
|
|
148
|
+
if(!Array.isArray(rows)||!t[mark])fail('unsupported','联动新增明细格式未知');
|
|
149
|
+
const existing=t[mark].rows,used=new Set();
|
|
150
|
+
const result=rows.map(r=>{const fields=Object.keys(r);const index=existing.findIndex((e,i)=>!used.has(i)&&fields.every(k=>String(e[k]?.value??'')===String(r[k]?.value??'')));if(index>=0)used.add(index);return {...clone(t[mark].addRowDefValue??{}),...(index>=0?existing[index]:{}),...r};});
|
|
151
|
+
if(String(rule.otherParams?.isdel)==='1'){
|
|
152
|
+
if(existing.some((e,i)=>e.keyid&&!used.has(i)))fail('unsupported','联动删除/替换已有明细需经验证的行删除适配');
|
|
153
|
+
if(!equal(existing,result)){t[mark].rows=result;changed=true;}
|
|
154
|
+
}else{
|
|
155
|
+
const additions=result.filter(r=>!existing.some(e=>Object.keys(r).filter(k=>k!=='keyid').every(k=>equal(e[k],r[k]))));
|
|
156
|
+
if(additions.length){existing.push(...additions);changed=true;}
|
|
157
|
+
}
|
|
158
|
+
evidence.rowChanges.push({config:String(rule.config),table:mark,rows:t[mark].rows.length,mode:String(rule.otherParams?.isdel)==='1'?'replace':'append'});
|
|
159
|
+
}
|
|
160
|
+
evidence.coverage.push({kind:'dataInput',config:String(rule.config),row,state:'executed'});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if(!changed){
|
|
164
|
+
if(!userApplied){applyUserInput();userApplied=true;continue;}
|
|
165
|
+
for(const [key,value]of protectedValues){const [ref,row]=key.split(':');const [table,id]=ref.split('.');if(String(getCells(table,Number(row))?.['field'+id]?.value??'')!==String(value))fail('conflict','联动明细覆盖用户值:'+id);const attr=evidence.viewAttributes?.[table+':'+row+':'+id];if(attr!==undefined&&![2,3].includes(attr))fail('conflict','用户字段在联动后不可编辑:'+id);}
|
|
166
|
+
for(const [key,attr]of Object.entries(evidence.viewAttributes??{}))if(attr===3){const [table,row,id]=key.split(':');if(String(getCells(table,Number(row))?.['field'+id]?.value??'')==='')fail('conflict','联动后必填字段为空:'+id);}
|
|
167
|
+
evidence.state='complete';return {form:f,tables:t,evidence};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
fail('nonconvergent','联动循环未收敛');
|
|
171
|
+
}
|
|
172
|
+
export async function validatePersonAssignments(api,cfg,plan,form,tables) {
|
|
173
|
+
const evidence=[];
|
|
174
|
+
const defs=definitions(form);
|
|
175
|
+
const check=async(values,table,rowIndex=-1)=>{
|
|
176
|
+
for(const [key,value] of Object.entries(values??{})) {
|
|
177
|
+
const id=fieldKey(key),f=defs.find(d=>d.id===id&&d.table===table);
|
|
178
|
+
if(Number(f?.htmltype)!==3||![1,17].includes(Number(f.detailtype))||String(value??'')==='')continue;
|
|
179
|
+
const ids=String(value).split(',').map(x=>x.trim()).filter(Boolean);
|
|
180
|
+
if(ids.some(x=>!/^\d+$/.test(x)))throw Error('人员字段必须使用已核验的账号ID');
|
|
181
|
+
const result=await queryBrowser(api,form,id,{main:plan.values,row:values,rowCells:tables[table]?.rows?.[rowIndex],baseUrl:cfg.baseUrl,...plan.personScopes?.[id]});
|
|
182
|
+
if(ids.some(id=>!result.items.some(c=>c.id===id)))throw Error('人员不在当前分部及表单可选范围内:字段'+id+';零结果不会扩大范围');
|
|
183
|
+
evidence.push({fieldId:id,rowIndex,account:result.account,scope:result.scope,ids,verification:result.verification});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
await check(plan.values,'main');
|
|
187
|
+
for(const dt of plan.details??[])for(let i=0;i<dt.rows.length;i++)await check(dt.rows[i],'detail_'+(Number(dt.index)+1),i);
|
|
188
|
+
return evidence;
|
|
189
|
+
}
|
|
190
|
+
export async function prepareFields(cfg,plan,{api,form,tables}={}) {
|
|
191
|
+
api??=await createApiFromProfile(cfg);
|
|
192
|
+
if(!form){const r=await api.post('/api/workflow/reqform/loadForm',{workflowid:String(plan.workflowId),requestid:String(plan.requestId??'-1'),iscreate:plan.requestId?'0':'1'},{forceCookie:true});form=r.json;}
|
|
193
|
+
if(form?.params?.verifyRight!==true||String(form.params.currentUserid)!==String(plan.accountId))throw Error('账号或表单权限不符');
|
|
194
|
+
tables??=await detailTables(api,form,plan.workflowId,plan.requestId);
|
|
195
|
+
const adapter=await loadAdapter(cfg,plan.workflowId,form);
|
|
196
|
+
if(adapter?.prepare)await adapter.prepare({plan,form,tables,mapping:adapter.mapping});
|
|
197
|
+
const personnel=await validatePersonAssignments(api,cfg,plan,form,tables);
|
|
198
|
+
const linked=await executeLinkages(api,form,tables,plan,{covered:adapter?.supportedLinkageKinds??[]});
|
|
199
|
+
// 中间联动可能清空再恢复同一ID;最终值已通过冲突检查,可恢复该用户选项的已确认显示名。
|
|
200
|
+
for(const [id,name]of Object.entries(plan.browserNames??{})){const c=linked.form.maindata?.['field'+id];if(c&&name&&(Object.hasOwn(plan.values??{},id)||Object.hasOwn(plan.values??{},'field'+id)))c.specialobj=[{id:String(c.value),name}];}
|
|
201
|
+
for(const [index,rows]of Object.entries(plan.detailBrowserNames??{}))for(const [row,names]of Object.entries(rows))for(const [id,name]of Object.entries(names)){const c=linked.tables['detail_'+(Number(index)+1)]?.rows[row]?.['field'+id],values=plan.details?.find(d=>String(d.index)===index)?.rows[row]??{};if(c&&name&&(Object.hasOwn(values,id)||Object.hasOwn(values,'field'+id)))c.specialobj=[{id:String(c.value),name}];}
|
|
202
|
+
const prepared=buildDraft({...plan,serverResolved:true,values:{},details:[],browserNames:{},detailBrowserNames:{}},linked.form,linked.tables);
|
|
203
|
+
// buildDraft receives only server/proven state; user permissions were checked before execution.
|
|
204
|
+
prepared.expected.coverage=linked.evidence;
|
|
205
|
+
prepared.expected.adapterDigest=adapter?.scriptDigest??null;
|
|
206
|
+
prepared.expected.originalFingerprint=fingerprint(form);
|
|
207
|
+
prepared.expected.personnel=personnel;
|
|
208
|
+
return {...prepared,evidence:linked.evidence,baseline:baseline(form,tables)};
|
|
209
|
+
}
|
|
210
|
+
export async function checkAdapter(cfg,plan,form) {
|
|
211
|
+
const adapter=await loadAdapter(cfg,plan.workflowId,form);
|
|
212
|
+
if((adapter?.scriptDigest??null)!==plan.prepared.expected.adapterDigest)throw Error('适配逻辑变化,请重新预览');
|
|
213
|
+
}
|
|
214
|
+
export function baseline(form,tables) {
|
|
215
|
+
const values=cells=>Object.fromEntries(Object.entries(cells??{}).filter(([k])=>/^field\d+$/.test(k)).map(([k,c])=>[k,{value:c?.value??'',specialobj:Array.isArray(c?.specialobj)?c.specialobj.map(x=>({id:x.id,name:x.name})):undefined}]));
|
|
216
|
+
return {main:values(form.maindata),details:Object.fromEntries(Object.entries(tables).map(([mark,t])=>[mark,t.rows.map(row=>({keyid:row.keyid,fields:values(row)}))]))};
|
|
217
|
+
}
|
|
218
|
+
export function checkBaseline(prepared,form,tables) {
|
|
219
|
+
if(!equal(prepared.baseline,baseline(form,tables)))throw Error('服务器默认值或原稿内容变化,请重新预览;不会静默重算');
|
|
220
|
+
}
|
|
221
|
+
export function classifyWrite(response,requestId) {
|
|
222
|
+
const data=response.json?.data??response.json??{},type=data.type;
|
|
223
|
+
return {responseType:type,requestId:data.resultInfo?.requestid??data.requestid??requestId,
|
|
224
|
+
state:type==='FAILD'||type==='FAILED'||data.status===false?'failed':type==='SUCCESS'?'written-unverified':'uncertain',messageInfo:data.messageInfo??response.json?.msg};
|
|
225
|
+
}
|
|
226
|
+
export function actionBody(prepared,form,action='save') {
|
|
227
|
+
if(!['save','submit'].includes(action)||prepared.expected?.coverage?.state!=='complete')throw Error('字段准备/联动覆盖未完成');
|
|
228
|
+
if(fingerprint(form)!==prepared.expected.originalFingerprint)throw Error('表单配置变化,请重新预览');
|
|
229
|
+
const business=Object.fromEntries(Object.entries(prepared.body).filter(([k])=>/^field|^nodesnum|^indexnum|^submitdtlid|^dtl_id_|^deldtlid|FieldUnEmptyCount$/.test(k)||['existChangeRange','requestname','requestid','iscreate'].includes(k)));
|
|
230
|
+
const body={...prepared.body,...form.submitParams,...business,signatureAttributesStr:String(form.params.signatureAttributesStr??''),signatureSecretKey:String(form.params.signatureSecretKey??''),src:action,isaffirmance:'0',isaffirmanceSrc:''};
|
|
231
|
+
if(String(form.params.currentUserid)!==prepared.expected.accountId)throw Error('执行账号已变化');
|
|
232
|
+
return body;
|
|
233
|
+
}
|
|
234
|
+
export function verifyAction(expected,form,tables,action='save') {
|
|
235
|
+
if(expected.coverage?.state!=='complete')return ['联动覆盖未完成'];
|
|
236
|
+
return verifyDraft(expected,form,tables,{action});
|
|
237
|
+
}
|
package/dist/fileutil.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** 下载文件不覆盖用户已有文件;重名时追加 (1)、(2)… */
|
|
5
|
+
export function nextAvailablePath(outDir, filename) {
|
|
6
|
+
const parsed = path.parse(filename);
|
|
7
|
+
let candidate = path.join(outDir, filename);
|
|
8
|
+
for (let index = 1; fs.existsSync(candidate); index++)
|
|
9
|
+
candidate = path.join(outDir, `${parsed.name} (${index})${parsed.ext}`);
|
|
10
|
+
return candidate;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 接受 OOXML zip 或传统 OLE Excel;拒绝 HTML 登录页、JSON 错误和空响应。 */
|
|
14
|
+
export function assertExcelLike(buf, label = 'Excel 文件') {
|
|
15
|
+
const isZip = Buffer.isBuffer(buf) && buf.length >= 64 && buf.readUInt32LE(0) === 0x04034b50;
|
|
16
|
+
const isOle = Buffer.isBuffer(buf) && buf.length >= 512 && buf.subarray(0, 8).equals(Buffer.from('D0CF11E0A1B11AE1', 'hex'));
|
|
17
|
+
if (!isZip && !isOle)
|
|
18
|
+
throw new Error(`${label}响应不是有效的 xls/xlsx,可能是登录页或错误页。`);
|
|
19
|
+
return { format: isZip ? 'xlsx' : 'xls', size: buf.length };
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 有限表达式语法;不执行表单JavaScript。日期/工作日查询沿用平台的只读接口。
|
|
2
|
+
export function parseFormula(text) {
|
|
3
|
+
const tokens=text.match(/\[(?:main|detail_\d+)\.-?\d+\]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\d+(?:\.\d+)?|[A-Za-z]+|==|!=|>=|<=|&&|\|\||[+*/%(),<>!\-]|\s+/g)??[];
|
|
4
|
+
if(tokens.join('')!==text)throw Error('不支持的公式语法:'+text);
|
|
5
|
+
const ts=tokens.filter(t=>!/^\s+$/.test(t));let i=0;
|
|
6
|
+
const precedence={'||':1,'&&':2,'==':3,'!=':3,'>':4,'<':4,'>=':4,'<=':4,'+':5,'-':5,'*':6,'/':6,'%':6};
|
|
7
|
+
const primary=()=>{
|
|
8
|
+
const t=ts[i++];
|
|
9
|
+
if(t==='('){const n=expr(1);if(ts[i++]!==')')throw Error('公式括号不匹配');return n;}
|
|
10
|
+
if(['-','+','!'].includes(t))return {unary:t,arg:primary()};
|
|
11
|
+
if(t?.startsWith('['))return {ref:t.slice(1,-1)};
|
|
12
|
+
if(/^['"]/.test(t??''))return {value:t.slice(1,-1).replace(/\\(['"\\])/g,'$1')};
|
|
13
|
+
if(/^\d/.test(t??''))return {value:Number(t)};
|
|
14
|
+
if(['IF','CurrDate','GetDateValue','CompareWorkDate'].includes(t)&&ts[i++]==='('){const args=[];if(ts[i]!==')'){do {args.push(expr(1));}while(ts[i]===','&&++i);}if(ts[i++]!==')')throw Error('函数参数语法错误');return {call:t,args};}
|
|
15
|
+
throw Error('不支持的公式函数或符号:'+t);
|
|
16
|
+
};
|
|
17
|
+
const expr=min=>{let left=primary();while((precedence[ts[i]]??0)>=min){const op=ts[i++],right=expr(precedence[op]+1);left={op,left,right};}return left;};
|
|
18
|
+
const tree=expr(1);if(i!==ts.length)throw Error('公式存在未处理内容');return tree;
|
|
19
|
+
}
|
|
20
|
+
export async function evaluateFormula(tree,read,functions={}) {
|
|
21
|
+
const run=async n=>{
|
|
22
|
+
if('value'in n)return n.value;
|
|
23
|
+
if(n.ref)return read(n.ref);
|
|
24
|
+
if(n.unary){const a=await run(n.arg);return n.unary==='!'?!a:n.unary==='-'?-Number(a):Number(a);}
|
|
25
|
+
if(n.call){
|
|
26
|
+
if(n.call==='IF'){if(n.args.length!==3)throw Error('IF需要三个参数');return await run(n.args[0])?run(n.args[1]):run(n.args[2]);}
|
|
27
|
+
if(!functions[n.call])throw Error('未提供日期函数:'+n.call);
|
|
28
|
+
return functions[n.call](...await Promise.all(n.args.map(run)));
|
|
29
|
+
}
|
|
30
|
+
const a=await run(n.left);
|
|
31
|
+
if(n.op==='&&')return a?run(n.right):a;
|
|
32
|
+
if(n.op==='||')return a?a:run(n.right);
|
|
33
|
+
const b=await run(n.right);let result;
|
|
34
|
+
switch(n.op){
|
|
35
|
+
case '==':return String(a)===String(b);case '!=':return String(a)!==String(b);
|
|
36
|
+
case '>':return Number(a)>Number(b);case '<':return Number(a)<Number(b);case '>=':return Number(a)>=Number(b);case '<=':return Number(a)<=Number(b);
|
|
37
|
+
case '+':result=Number(a)+Number(b);break;case '-':result=Number(a)-Number(b);break;case '*':result=Number(a)*Number(b);break;case '/':result=Number(a)/Number(b);break;case '%':result=Number(a)%Number(b);break;
|
|
38
|
+
}
|
|
39
|
+
if(!Number.isFinite(result))throw Error('公式计算无效或除零');return result;
|
|
40
|
+
};
|
|
41
|
+
return run(tree);
|
|
42
|
+
}
|
|
43
|
+
export function formulaDateFunctions(api) {
|
|
44
|
+
const cache=new Map();
|
|
45
|
+
const once=async(key,fetcher)=>{if(!cache.has(key))cache.set(key,fetcher());return cache.get(key);};
|
|
46
|
+
const dateParts=(value,part)=>{const m=String(value).match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!m)throw Error('不支持的日期值:'+value);const parts={Y:Number(m[1]),M:Number(m[2]),D:Number(m[3])};if(!(part in parts))throw Error('未支持的日期分量:'+part);return parts[part];};
|
|
47
|
+
return {
|
|
48
|
+
GetDateValue:dateParts,
|
|
49
|
+
CurrDate:async()=>once('today',async()=>{const r=await api.post('/api/workflow/formula/getCurrentDateTime',{type:'CurrDateTime'},{forceCookie:true});const ms=Number(r.json?.time);if(!Number.isFinite(ms)||ms<=0)throw Error('服务器日期查询失败');const d=new Date(ms);return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;}),
|
|
50
|
+
CompareWorkDate:async(beginDate,endDate,userid)=>{
|
|
51
|
+
dateParts(beginDate,'Y');dateParts(endDate,'Y');if(!/^\d+$/.test(String(userid)))throw Error('工作日计算缺少人员ID');
|
|
52
|
+
return once(JSON.stringify([beginDate,endDate,userid]),async()=>{const r=await api.post('/api/workflow/formula/compareWorkDate',{beginDate,endDate,userid:String(userid)},{forceCookie:true});const value=r.json?.workDays;if(value==null||value===''||!Number.isFinite(Number(value)))throw Error('平台工作日查询失败');return Number(value);});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|