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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {definitions,detailTables,fingerprint,loadAdapter,buildDraft,verifyDraft} from './workflow-runtime.js';
|
|
1
|
+
import {definitions,detailTables,fingerprint,loadAdapter,buildDraft,verifyDraft,queryBrowser} from './workflow-runtime.js';
|
|
2
2
|
import {createApiFromProfile} from './client.js';
|
|
3
|
+
import {parseFormula,evaluateFormula,formulaDateFunctions} from './formula-runtime.js';
|
|
4
|
+
import {executeCalculations} from './linkage-calculations.js';
|
|
3
5
|
|
|
4
6
|
const clone=x=>structuredClone(x);
|
|
5
7
|
const cell=x=>x&&typeof x==='object'&&'value'in x?clone(x):{value:x??''};
|
|
@@ -37,17 +39,20 @@ export async function executeLinkages(api,form,tables,input,{maxSteps=100,covere
|
|
|
37
39
|
const f=clone(form),t=clone(tables),defs=definitions(f),cfg=f.linkageCfg??{};
|
|
38
40
|
const evidence={state:'preparing',coverage:[],rowChanges:[]};
|
|
39
41
|
// 未支持的配置必须明确由已验证适配承担,不能吞错继续。
|
|
40
|
-
|
|
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('、'));
|
|
41
45
|
const getCells=(table,row)=>table==='main'?f.maindata:t[table]?.rows[row];
|
|
42
46
|
const protectedValues=new Map();
|
|
43
47
|
const write=(table,row,id,value,source)=>{
|
|
44
48
|
const field=defs.find(x=>x.table===table&&x.id===String(id));if(!field)fail('unsupported','联动目标字段不存在:'+id);
|
|
45
49
|
const cells=getCells(table,row);if(!cells)fail('unsupported','联动行不存在');
|
|
46
|
-
const next=cell(value)
|
|
47
|
-
|
|
50
|
+
const next=cell(value);
|
|
51
|
+
// 联动可能先产生中间值;只在全部规则稳定后检查是否覆盖用户值。
|
|
48
52
|
if(String(cells['field'+id]?.value??'')===String(next.value)&&(!next.specialobj||equal(cells['field'+id]?.specialobj,next.specialobj)))return false;
|
|
49
53
|
cells['field'+id]=next;return true;
|
|
50
54
|
};
|
|
55
|
+
const applyUserInput=()=>{
|
|
51
56
|
for(let [raw,v]of Object.entries(input.values??{})){
|
|
52
57
|
const id=fieldKey(raw);if(Number(id)<0)continue;
|
|
53
58
|
const d=defs.find(x=>x.id===id&&x.table==='main');if(!d||![2,3].includes(Number(d.viewattr)))fail('conflict','禁止用户覆盖只读/隐藏字段:'+id);
|
|
@@ -59,16 +64,35 @@ export async function executeLinkages(api,form,tables,input,{maxSteps=100,covere
|
|
|
59
64
|
if(!input.requestId)t[mark].rows=dt.rows.map(()=>clone(t[mark].addRowDefValue??{}));
|
|
60
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);}));
|
|
61
66
|
}
|
|
67
|
+
};
|
|
68
|
+
// 新建表单先完成初始化带值,再应用用户填写;初始化规则不能反复覆盖用户明细。
|
|
69
|
+
let userApplied=!!input.requestId||!Object.keys(cfg.dataInputCfg??{}).length;
|
|
70
|
+
if(userApplied)applyUserInput();
|
|
62
71
|
const seen=new Map();let steps=0;
|
|
63
|
-
const formulas=cfg.formulaCfg?.formulas??[];
|
|
72
|
+
const formulas=cfg.formulaCfg?.formulas??[],dateFunctions=formulaDateFunctions(api);
|
|
64
73
|
for(let pass=0;pass<maxSteps;pass++){
|
|
65
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;
|
|
66
76
|
for(const rule of formulas){
|
|
67
77
|
const m=String(rule.formula??'').match(/^\[(main|detail_\d+)\.(-?\d+)\]\s*=\s*(.*)$/);if(!m)fail('unsupported','公式目标语法不支持');
|
|
68
78
|
const [,table,id,expression]=m;
|
|
69
79
|
const rows=table==='main'?[-1]:(t[table]?.rows??[]).map((_,i)=>i);
|
|
70
80
|
for(const row of rows){const read=ref=>{const [tab,fid]=ref.split('.');return getCells(tab,tab==='main'?-1:row)?.['field'+fid]?.value??'';};
|
|
71
|
-
const
|
|
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);
|
|
72
96
|
const sourceRef=expression.trim().match(/^\[(main|detail_\d+)\.(\d+)\]$/);
|
|
73
97
|
const next=sourceRef?getCells(sourceRef[1],sourceRef[1]==='main'?-1:row)?.['field'+sourceRef[2]]:{value};
|
|
74
98
|
// 双向互拷贝以非空输入为起点;未设置一端不能清掉另一端。
|
|
@@ -102,7 +126,7 @@ export async function executeLinkages(api,form,tables,input,{maxSteps=100,covere
|
|
|
102
126
|
for(const row of d.table==='main'?[-1]:(t[d.table]?.rows??[]).map((_,i)=>i))for(const rule of configs){
|
|
103
127
|
const key=`${rule.config}:${d.table}:${row}`;
|
|
104
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;}
|
|
105
|
-
const signature=JSON.stringify(values);if(seen.get(key)===signature)continue;
|
|
129
|
+
const signature=JSON.stringify([id,...(rule.relateFields??[])].map(fid=>[fid,values['field'+fid]??'']));if(seen.get(key)===signature)continue;
|
|
106
130
|
if(++steps>maxSteps)fail('nonconvergent','联动超限或循环不收敛');seen.set(key,signature);
|
|
107
131
|
const p=f.params,sp=f.submitParams;
|
|
108
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??'',
|
|
@@ -111,7 +135,7 @@ export async function executeLinkages(api,form,tables,input,{maxSteps=100,covere
|
|
|
111
135
|
for(const assigned of rule.otherParams?.assignfields??[]) {
|
|
112
136
|
const assignedDef=defs.find(x=>x.id===String(assigned));
|
|
113
137
|
const dependencies=assignedDef?.browserattr?.cusDependShip??[];
|
|
114
|
-
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)fail('unsupported','缺少自定义浏览框同行依赖');depend[dep.key]=c['field'+dep.fieldid]?.value??'';}body['customDepend_field'+assigned+(assignedDef.table==='main'?'':'_'+row)]=JSON.stringify(depend);}
|
|
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);}
|
|
115
139
|
}
|
|
116
140
|
const r=await api.post('/api/workflow/linkage/reqDataInputResult',body,{forceCookie:true});
|
|
117
141
|
const info=r.json?.['assignInfo_'+rule.config];if(!info||info.resultOutOfLimit||info.error||r.json?.status===false)fail('failed','数据联动失败/超限:'+rule.config);
|
|
@@ -136,10 +160,33 @@ export async function executeLinkages(api,form,tables,input,{maxSteps=100,covere
|
|
|
136
160
|
evidence.coverage.push({kind:'dataInput',config:String(rule.config),row,state:'executed'});
|
|
137
161
|
}
|
|
138
162
|
}
|
|
139
|
-
if(!changed){
|
|
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
|
+
}
|
|
140
169
|
}
|
|
141
170
|
fail('nonconvergent','联动循环未收敛');
|
|
142
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
|
+
}
|
|
143
190
|
export async function prepareFields(cfg,plan,{api,form,tables}={}) {
|
|
144
191
|
api??=await createApiFromProfile(cfg);
|
|
145
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;}
|
|
@@ -147,12 +194,17 @@ export async function prepareFields(cfg,plan,{api,form,tables}={}) {
|
|
|
147
194
|
tables??=await detailTables(api,form,plan.workflowId,plan.requestId);
|
|
148
195
|
const adapter=await loadAdapter(cfg,plan.workflowId,form);
|
|
149
196
|
if(adapter?.prepare)await adapter.prepare({plan,form,tables,mapping:adapter.mapping});
|
|
197
|
+
const personnel=await validatePersonAssignments(api,cfg,plan,form,tables);
|
|
150
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}];}
|
|
151
202
|
const prepared=buildDraft({...plan,serverResolved:true,values:{},details:[],browserNames:{},detailBrowserNames:{}},linked.form,linked.tables);
|
|
152
203
|
// buildDraft receives only server/proven state; user permissions were checked before execution.
|
|
153
204
|
prepared.expected.coverage=linked.evidence;
|
|
154
205
|
prepared.expected.adapterDigest=adapter?.scriptDigest??null;
|
|
155
206
|
prepared.expected.originalFingerprint=fingerprint(form);
|
|
207
|
+
prepared.expected.personnel=personnel;
|
|
156
208
|
return {...prepared,evidence:linked.evidence,baseline:baseline(form,tables)};
|
|
157
209
|
}
|
|
158
210
|
export async function checkAdapter(cfg,plan,form) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {parseFormula,evaluateFormula} from './formula-runtime.js';
|
|
2
|
+
|
|
3
|
+
export async function executeCalculations({api,form,tables,input,defs,getCells,write,seen,evidence,step}) {
|
|
4
|
+
const cfg=form.linkageCfg??{};let changed=false;
|
|
5
|
+
const read=(ref,row)=>{const [table,id]=ref.split('.');const v=getCells(table,table==='main'?-1:row)?.['field'+id]?.value??'';return v;};
|
|
6
|
+
for(const [trigger,rules]of Object.entries(cfg.rowCalCfg??{})) {
|
|
7
|
+
if(trigger==='rowCalAssignInfo')continue;
|
|
8
|
+
if(!Array.isArray(rules))throw Error('行规则格式未知');
|
|
9
|
+
for(const rule of rules){const d=defs.find(x=>x.id===String(rule.assignField));if(!d||d.table==='main')throw Error('行规则目标不是明细字段');
|
|
10
|
+
const expression=String(rule.config).replace(/detailfield_(\d+)/g,(_,id)=>`[${d.table}.${id}]`),tree=parseFormula(expression);
|
|
11
|
+
for(let row=0;row<(tables[d.table]?.rows.length??0);row++){
|
|
12
|
+
const value=await evaluateFormula(tree,ref=>read(ref,row));
|
|
13
|
+
changed=write(d.table,row,d.id,{value},'rowCal')||changed;
|
|
14
|
+
evidence.coverage.push({kind:'rowCal',config:d.id,row,state:'executed'});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for(const id of new Set([...Object.keys(cfg.mainCalCfg??{}),...Object.keys(cfg.colCalCfg??{})])) {
|
|
19
|
+
const d=defs.find(x=>x.id===id);if(!d||d.table==='main'||!tables[d.table])throw Error('列合计字段未知');
|
|
20
|
+
const rows=tables[d.table].rows;
|
|
21
|
+
const sum=rows.reduce((total,row)=>{const n=Number(row['field'+id]?.value??0);if(!Number.isFinite(n))throw Error('列合计包含非数字');return total+n;},0);
|
|
22
|
+
if(cfg.colCalCfg?.[id]){if(cfg.colCalCfg[id]!=='needsum')throw Error('未知列合计规则');(evidence.columnSums??={})[id]=sum;evidence.coverage.push({kind:'colCal',config:id,state:'executed'});}
|
|
23
|
+
const target=cfg.mainCalCfg?.[id];
|
|
24
|
+
if(target){const dest=defs.find(x=>x.id===String(target));if(!dest||dest.table!=='main')throw Error('明细合计赋值主表目标未知');
|
|
25
|
+
const value=!rows.length&&String(form.propFileCfg?.colRule_noRow_empty)!=='1'?'':sum;
|
|
26
|
+
changed=write('main',-1,dest.id,{value},'mainCal')||changed;evidence.coverage.push({kind:'mainCal',config:id,target:String(target),state:'executed'});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for(const [trigger,rules]of Object.entries(cfg.fieldDateCfg??{}))for(const rule of rules){
|
|
30
|
+
const d=defs.find(x=>x.id===String(rule.assignField));
|
|
31
|
+
if(!d&&/^\d+$/.test(String(rule.assignField))&&form.cellInfo?.fieldCellInfo&&!Object.hasOwn(form.cellInfo.fieldCellInfo,String(rule.assignField))){evidence.coverage.push({kind:'fieldDate',config:String(rule.config),target:String(rule.assignField),state:'not-in-layout'});continue;}
|
|
32
|
+
if(!d)throw Error('日期规则目标未知');
|
|
33
|
+
for(const row of d.table==='main'?[-1]:(tables[d.table]?.rows??[]).map((_,i)=>i)){
|
|
34
|
+
const values={};for(const id of rule.relateFields??[]){const dep=defs.find(x=>x.id===String(id));if(!dep)throw Error('日期依赖不存在');values['field'+id+(dep.table==='main'?'':'_'+row)]=getCells(dep.table,dep.table==='main'?-1:row)?.['field'+id]?.value??'';}
|
|
35
|
+
const key='date:'+rule.config+':'+row,signature=JSON.stringify(values);if(seen.get(key)===signature)continue;seen.set(key,signature);step();
|
|
36
|
+
const p=form.params;
|
|
37
|
+
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:'0',linkageid:String(rule.config),linkageUUID:form.submitParams?.linkageUUID??'',
|
|
38
|
+
['triFieldid_'+rule.config]:trigger,['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')};
|
|
39
|
+
const r=await api.post('/api/workflow/linkage/reqDateTimeResult',body,{forceCookie:true}),info=r.json?.['assignInfo_'+rule.config];
|
|
40
|
+
if(!info||info.error||info.resultOutOfLimit||r.json?.status===false)throw Error('日期联动失败:'+rule.config);
|
|
41
|
+
for(const k of Object.keys(info))if(!['changeValue','resultOutOfLimit','linkageSql'].includes(k))throw Error('日期联动响应尚未支持:'+k);
|
|
42
|
+
for(const [key,value]of Object.entries(info.changeValue??{})){
|
|
43
|
+
const m=key.match(/^field(\d+)(?:_(\d+))?$/),dest=m&&defs.find(x=>x.id===m[1]);if(!dest)throw Error('日期联动返回未知字段');
|
|
44
|
+
changed=write(dest.table,m[2]?Number(m[2]):dest.table==='main'?-1:row,dest.id,value,'date')||changed;
|
|
45
|
+
}
|
|
46
|
+
evidence.coverage.push({kind:'fieldDate',config:String(rule.config),row,state:'executed'});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
evidence.viewAttributes={};
|
|
50
|
+
for(const [ref,rules]of Object.entries(cfg.viewAttrCfg??{})){
|
|
51
|
+
const id=ref.split('_')[0],d=defs.find(x=>x.id===id);if(!d||!Array.isArray(rules.relateFields))throw Error('属性联动配置未知');
|
|
52
|
+
for(const row of d.table==='main'?[-1]:(tables[d.table]?.rows??[]).map((_,i)=>i)){
|
|
53
|
+
const value=String(read(d.table+'.'+id,row)),assign={};
|
|
54
|
+
for(const [condition,targets]of Object.entries(rules))if(condition!=='relateFields'){
|
|
55
|
+
const [op,expected]=condition.split('|');if(expected===undefined||!['0','1','2','3','4','5'].includes(op))throw Error('属性联动条件未知');
|
|
56
|
+
const match=op==='0'?expected.split(',').includes(value):op==='1'?!expected.split(',').includes(value):op==='2'?(','+value+',').includes(','+expected+','):op==='3'?!(','+value+',').includes(','+expected+','):op==='4'?value==='1':value!=='1';
|
|
57
|
+
if(match)Object.assign(assign,targets);
|
|
58
|
+
}
|
|
59
|
+
for(const target of rules.relateFields){
|
|
60
|
+
const tid=target.split('_')[0],dest=defs.find(x=>x.id===tid);if(!dest)throw Error('属性联动目标未知');
|
|
61
|
+
const code=assign[target],attr=code===undefined?Number(dest.viewattr):({'1':2,'2':3,'3':1,'4':Number(dest.viewattr),'5':4})[String(code)];
|
|
62
|
+
if(attr===undefined)throw Error('属性联动属性未知');evidence.viewAttributes[dest.table+':'+(dest.table==='main'?-1:row)+':'+tid]=attr;
|
|
63
|
+
}
|
|
64
|
+
evidence.coverage.push({kind:'viewAttr',config:ref,row,state:'executed'});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return changed;
|
|
68
|
+
}
|
package/dist/meetings.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const clone = v => JSON.parse(JSON.stringify(v));
|
|
4
|
+
const same = (a,b) => JSON.stringify(a) === JSON.stringify(b);
|
|
5
|
+
const text = v => v == null ? '' : String(v);
|
|
6
|
+
const minute = v => text(v).slice(0,16);
|
|
7
|
+
const rowKeys = ['coding','subject','hrmid01','hrmid02','begindate','enddate'];
|
|
8
|
+
const links = ['decisiontskids','decisiondoc','decisionprjids','decisionwfids','decisioncrmids','decisionatchids'];
|
|
9
|
+
async function call(api, route, body={}) {
|
|
10
|
+
const r=await api.post(route.startsWith('/api/')?route:'/api/meeting/'+route,body,{forceCookie:true});
|
|
11
|
+
const j=r?.json;
|
|
12
|
+
if(r.status!==200 || !j || j.status===false || j.api_status===false || j.retstatus===false || j.ret==='false') throw Error('会议接口失败:'+route);
|
|
13
|
+
return j;
|
|
14
|
+
}
|
|
15
|
+
function walk(v, fn) {
|
|
16
|
+
if(!v || typeof v!=='object')return;
|
|
17
|
+
if(Array.isArray(v)){v.forEach(x=>walk(x,fn));return;}
|
|
18
|
+
fn(v); Object.values(v).forEach(x=>{if(x&&typeof x==='object')walk(x,fn);});
|
|
19
|
+
}
|
|
20
|
+
export async function fields(api) {
|
|
21
|
+
const meta=await call(api,'field/getMeetingBaseFieldMobx');
|
|
22
|
+
if(String(meta.onlyFlowCreate)==='1' || String(meta.hideCreateEntry)==='1')throw Error('平台限制直接新建会议,请按平台指定入口办理');
|
|
23
|
+
const defaults={},schema={};
|
|
24
|
+
walk(meta.datas,f=>{if(!Array.isArray(f.domkey))return;f.domkey.forEach((k,i)=>{
|
|
25
|
+
const value=Array.isArray(f.value)?f.value[i]:f.value;
|
|
26
|
+
schema[k]={viewAttr:f.viewAttr};
|
|
27
|
+
if(value!=null && typeof value!=='object')defaults[k]=text(value);
|
|
28
|
+
});});
|
|
29
|
+
if(!schema.name)throw Error('未取得会议表单结构');
|
|
30
|
+
return {meta,defaults,schema};
|
|
31
|
+
}
|
|
32
|
+
export async function view(api,id) {
|
|
33
|
+
if(!/^\d+$/.test(text(id)))throw Error('需提供合法会议ID');
|
|
34
|
+
const body={meetingid:text(id)};
|
|
35
|
+
const base=await call(api,'base/getMeetingBaseInfo',body);
|
|
36
|
+
if(!base.data)throw Error('会议不存在或无查看权限');
|
|
37
|
+
const display=await call(api,'field/getViewMeetingField',body);
|
|
38
|
+
const decision=await call(api,'decision/getDecisionData',body);
|
|
39
|
+
if(!Array.isArray(decision.datas))throw Error('决议数据不完整');
|
|
40
|
+
const key=display.decision?.list?.sessionkey,tasks=[];
|
|
41
|
+
if(key){
|
|
42
|
+
const seen=new Set();
|
|
43
|
+
for(let page=1;;page++){
|
|
44
|
+
const j=await call(api,'/api/ec/dev/table/datas',{dataKey:key,current:text(page),pageSize:'50'});
|
|
45
|
+
if(!Array.isArray(j.datas))throw Error('任务分页数据异常');
|
|
46
|
+
if(!j.datas.length)break;
|
|
47
|
+
const signature=JSON.stringify(j.datas);
|
|
48
|
+
if(seen.has(signature)||page>10000)throw Error('任务分页未完成,不能报告完整数量');
|
|
49
|
+
seen.add(signature);tasks.push(...j.datas);
|
|
50
|
+
}
|
|
51
|
+
}else if(decision.datas.length)throw Error('未取得会议任务查询入口');
|
|
52
|
+
return {meetingid:text(id),base:base.data,display,decision,tasks,taskCount:tasks.length};
|
|
53
|
+
}
|
|
54
|
+
function snapshot(v) {
|
|
55
|
+
return {base:Object.fromEntries(['meetingname','begindate','enddate','begintime','endtime','isdecision'].map(k=>[k,v.base[k]])),decision:{decision:v.decision.decision,datas:v.decision.datas.map(r=>Object.fromEntries(rowKeys.map(k=>[k,text(r[k])]))),...Object.fromEntries(links.map(k=>[k,v.decision[k]??[]]))}};
|
|
56
|
+
}
|
|
57
|
+
function validateRows(rows) {
|
|
58
|
+
if(!Array.isArray(rows))throw Error('tasks必须是完整任务数组');
|
|
59
|
+
for(const r of rows){
|
|
60
|
+
for(const k of rowKeys)if(!text(r[k]).trim())throw Error('决议任务缺少'+k+';执行人、检查人未指定时先询问用户');
|
|
61
|
+
for(const k of ['hrmid01','hrmid02'])if(!/^\d+$/.test(text(r[k])))throw Error('执行人、检查人必须为已核实的账号ID');
|
|
62
|
+
for(const k of ['begindate','enddate'])if(!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(r[k]))throw Error('任务时间格式为YYYY-MM-DD HH:mm');
|
|
63
|
+
if(r.begindate>r.enddate)throw Error('任务开始时间晚于截止时间');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function decisionBody(id,d) {
|
|
67
|
+
const body={meetingid:text(id),decision:d.decision,decisionrows:text(d.datas.length)};
|
|
68
|
+
for(const k of links){
|
|
69
|
+
const values=d[k]??[];
|
|
70
|
+
if(!Array.isArray(values))throw Error('关联数据格式不支持:'+k);
|
|
71
|
+
body[k]=values.map(x=>{if(typeof x!=='object')return text(x);if(x?.id!=null)return text(x.id);throw Error('无法保留关联项:'+k);}).join(',');
|
|
72
|
+
}
|
|
73
|
+
d.datas.forEach((r,i)=>rowKeys.forEach(k=>{body[k+'_'+(i+1)]=text(r[k]);}));
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
76
|
+
async function conflicts(api,body) {
|
|
77
|
+
const custom=await call(api,'base/customMeetingChk',body);
|
|
78
|
+
const check=await call(api,'base/chkMeeting',body);
|
|
79
|
+
if(custom.chkstatus!==true || check.attribute?.cansub!==true || ['address','member','wpMember','service'].some(k=>check[k]?.chkstatus!==true))throw Error('会议冲突检查未通过,请核实时间、人员、会议室及服务');
|
|
80
|
+
}
|
|
81
|
+
function verifyCreate(v,body) {
|
|
82
|
+
for(const k of ['begindate','enddate','begintime','endtime'])if(text(v.base[k])!==body[k])throw Error('会议时间回读不一致:'+k);
|
|
83
|
+
if(v.base.meetingname!==body.name)throw Error('会议名称回读不一致');
|
|
84
|
+
const display={};walk(v.display.base,f=>{if(f.fieldname)display[f.fieldname]=f.fieldValue;});
|
|
85
|
+
for(const k of Object.keys(body)){
|
|
86
|
+
if(['name','begindate','enddate','begintime','endtime','method','isInterval','serviceitems','totalmember'].includes(k))continue;
|
|
87
|
+
if(k==='hrmmembers'){
|
|
88
|
+
if(!Array.isArray(display[k]) || !same(display[k].map(p=>text(p.id)).sort(),body[k].split(',').filter(Boolean).sort()))throw Error('参会人回读不一致');
|
|
89
|
+
}else if(k in display && typeof display[k]!=='object' && text(display[k])!==text(body[k]))throw Error('会议字段回读不一致:'+k);
|
|
90
|
+
}
|
|
91
|
+
if(text(v.base.address||'')!==text(body.address||''))throw Error('会议室回读不一致');
|
|
92
|
+
for(const k of ['desc_n','customizeAddress'])if(body[k] && text(display[k])!==body[k])throw Error('会议字段未完整回读:'+k);
|
|
93
|
+
}
|
|
94
|
+
function verifyDecision(v,plan) {
|
|
95
|
+
const expected=plan.expected;
|
|
96
|
+
if(text(v.base.isdecision)!==(plan.action==='decision-submit'?'2':'1') || v.decision.decision!==expected.decision)throw Error('决议正文或状态回读不一致');
|
|
97
|
+
if(v.decision.datas.length!==expected.datas.length || v.tasks.length!==expected.datas.length)throw Error('决议任务数量回读不一致');
|
|
98
|
+
expected.datas.forEach((r,i)=>{
|
|
99
|
+
for(const k of rowKeys)if(text(v.decision.datas[i][k])!==text(r[k]))throw Error('决议任务回读不一致:'+k);
|
|
100
|
+
const t=v.tasks[i];
|
|
101
|
+
for(const k of ['coding','subject','hrmid01','hrmid02'])if(text(t[k])!==text(r[k]))throw Error('会议任务回读不一致:'+k);
|
|
102
|
+
if(minute(t.begint??t.begindatespan)!==r.begindate || minute(t.endt??t.enddatespan)!==r.enddate)throw Error('会议任务时间回读不一致');
|
|
103
|
+
if(plan.action==='decision-submit' && (!t.id || text(t.meetingid)!==plan.meetingid))throw Error('已提交任务缺少会议关联');
|
|
104
|
+
});
|
|
105
|
+
for(const k of links)if(!same(v.decision[k]??[],expected[k]??[]))throw Error('决议关联项回读不一致:'+k);
|
|
106
|
+
}
|
|
107
|
+
export async function meetingMain(api,args,accountId,baseUrl) {
|
|
108
|
+
const [command,...rest]=args;
|
|
109
|
+
if(command==='fields')return fields(api);
|
|
110
|
+
if(command==='view'||command==='tasks'){const v=await view(api,rest[0]);return command==='tasks'?{meetingid:v.meetingid,count:v.taskCount,items:v.tasks}:v;}
|
|
111
|
+
if(command==='preview'){
|
|
112
|
+
const [action,inputFile,planFile]=rest;
|
|
113
|
+
if(!['create','decision-save','decision-submit'].includes(action)||!inputFile||!planFile)throw Error('用法:meeting preview <create|decision-save|decision-submit> <输入JSON> <新计划JSON>');
|
|
114
|
+
const input=JSON.parse(fs.readFileSync(inputFile,'utf8'));
|
|
115
|
+
const plan={action,accountId,baseUrl};
|
|
116
|
+
if(action==='create'){
|
|
117
|
+
const f=await fields(api);const values=input.values;
|
|
118
|
+
if(!values||typeof values!=='object'||Array.isArray(values))throw Error('缺少values');
|
|
119
|
+
for(const k of Object.keys(values)){
|
|
120
|
+
if(!['name','begindate','begintime','enddate','endtime','address','customizeAddress','hrmmembers','desc_n'].includes(k))throw Error('当前已验证的新建输入不支持:'+k+';其他字段保留平台默认值');
|
|
121
|
+
if(!f.schema[k] || typeof values[k]==='object')throw Error('不支持的会议字段:'+k);
|
|
122
|
+
if(String(f.schema[k].viewAttr)==='1' && text(values[k])!==text(f.defaults[k]))throw Error('只读会议字段不能覆盖:'+k);
|
|
123
|
+
}
|
|
124
|
+
const body={...f.defaults,...Object.fromEntries(Object.entries(values).map(([k,v])=>[k,text(v)])),method:'',isInterval:'',serviceitems:''};
|
|
125
|
+
for(const [k,s] of Object.entries(f.schema))if(String(s.viewAttr)==='3'&&!body[k])throw Error('会议必填字段缺少:'+k);
|
|
126
|
+
if(!/^\d+(,\d+)*$/.test(body.hrmmembers??''))throw Error('需核实参会人ID');
|
|
127
|
+
body.totalmember=text(new Set(body.hrmmembers.split(',')).size);
|
|
128
|
+
if(!body.address&&!body.customizeAddress && input.noRoom!==true)throw Error('未选择地点,请明确noRoom:true表示不预约会议室');
|
|
129
|
+
if(!/^\d{4}-\d{2}-\d{2}$/.test(body.begindate)||!/^\d{4}-\d{2}-\d{2}$/.test(body.enddate)||!/^\d{2}:\d{2}$/.test(body.begintime)||!/^\d{2}:\d{2}$/.test(body.endtime)||body.begindate+' '+body.begintime>body.enddate+' '+body.endtime)throw Error('会议日期时间不合法');
|
|
130
|
+
await conflicts(api,body);Object.assign(plan,{body,schema:f.schema});
|
|
131
|
+
}else{
|
|
132
|
+
const v=await view(api,input.meetingid);
|
|
133
|
+
if(text(v.base.isdecision)==='2')throw Error('决议已经提交,停止重复保存或提交');
|
|
134
|
+
const d=clone(v.decision);
|
|
135
|
+
if(Object.keys(input).some(k=>!['meetingid','decision','tasks'].includes(k)))throw Error('决议输入仅支持meetingid、decision、tasks');
|
|
136
|
+
if(input.decision!==undefined){if(typeof input.decision!=='string')throw Error('决议正文需为字符串');d.decision=input.decision;}
|
|
137
|
+
if(input.tasks!==undefined)d.datas=input.tasks;
|
|
138
|
+
validateRows(d.datas);
|
|
139
|
+
if(action==='decision-submit' && (text(v.base.isdecision)!=='1'||!same(d,v.decision)))throw Error('正式提交前必须保存并回读,重新按已保存内容预览');
|
|
140
|
+
Object.assign(plan,{meetingid:v.meetingid,baseline:snapshot(v),expected:d,body:decisionBody(v.meetingid,d)});
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(planFile,JSON.stringify(plan,null,2),{flag:'wx'});
|
|
143
|
+
return {state:'preview-only',...plan};
|
|
144
|
+
}
|
|
145
|
+
if(command!=='run'&&command!=='verify')throw Error('会议命令:fields | view <ID> | tasks <ID> | preview <动作> <输入JSON> <新计划JSON> | run <计划JSON> --confirm | verify <计划JSON>');
|
|
146
|
+
const planFile=rest[0],receiptFile=planFile+'.receipt.json';
|
|
147
|
+
const plan=JSON.parse(fs.readFileSync(planFile,'utf8'));
|
|
148
|
+
if(plan.accountId!==accountId||plan.baseUrl!==baseUrl)throw Error('会议计划环境或账号已变化,请重新预览');
|
|
149
|
+
if(!['create','decision-save','decision-submit'].includes(plan.action))throw Error('未知会议动作');
|
|
150
|
+
let receipt=fs.existsSync(receiptFile)?JSON.parse(fs.readFileSync(receiptFile,'utf8')):null;
|
|
151
|
+
if(command==='run'){
|
|
152
|
+
if(!rest.includes('--confirm'))throw Error('请先展示预览,取得用户确认后使用--confirm');
|
|
153
|
+
if(receipt)throw Error('已有执行回执,禁止重复写入,请verify核验');
|
|
154
|
+
if(plan.action==='create'){
|
|
155
|
+
const f=await fields(api);if(!same(f.schema,plan.schema))throw Error('会议表单结构变化,请重新预览');
|
|
156
|
+
await conflicts(api,plan.body);
|
|
157
|
+
}else if(!same(snapshot(await view(api,plan.meetingid)),plan.baseline))throw Error('决议已变化,请重新预览');
|
|
158
|
+
receipt={state:'uncertain',action:plan.action,meetingid:plan.meetingid};
|
|
159
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2),{flag:'wx'});
|
|
160
|
+
const result=await call(api,plan.action==='create'?'base/newMeeting':plan.action==='decision-save'?'decision/edit':'decision/submit',plan.body);
|
|
161
|
+
if(plan.action==='create')receipt.meetingid=text(result.meetingid);
|
|
162
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2));
|
|
163
|
+
if(plan.action==='create'?![true,'true'].includes(result.retstatus):result.status!==true)throw Error('写入未确认,请核验,禁止自动重试');
|
|
164
|
+
}
|
|
165
|
+
if(!receipt?.meetingid)throw Error('缺少会议ID,结果不明,请人工核查,不自动重建');
|
|
166
|
+
receipt.state='verification-required';
|
|
167
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2));
|
|
168
|
+
const v=await view(api,receipt.meetingid);
|
|
169
|
+
if(plan.action==='create')verifyCreate(v,plan.body);else verifyDecision(v,plan);
|
|
170
|
+
receipt.state='verified';receipt.taskCount=v.taskCount;receipt.isdecision=v.base.isdecision;
|
|
171
|
+
fs.writeFileSync(receiptFile,JSON.stringify(receipt,null,2));return receipt;
|
|
172
|
+
}
|
package/dist/office-tools.js
CHANGED
|
@@ -8,6 +8,7 @@ export async function accounts(api) {
|
|
|
8
8
|
const rows = r.json?.data?.accountlist;
|
|
9
9
|
if (!Array.isArray(rows) || !rows.length) throw Error('未取得关联账号,请检查登录状态');
|
|
10
10
|
const items = rows.map(a => ({id: String(a.userid), name: a.username,
|
|
11
|
+
divisionId: a.subcompanyid == null ? null : String(a.subcompanyid),
|
|
11
12
|
division: a.subcompanyname || '所属分部未返回(请核实)', department: a.deptname || '',
|
|
12
13
|
current: String(a.iscurrent) === '1'}));
|
|
13
14
|
if (items.filter(a => a.current).length !== 1) throw Error('未能确认当前账号');
|
|
@@ -26,9 +27,13 @@ export async function shift(api, id) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export async function main(argv = process.argv.slice(2)) {
|
|
29
|
-
const [command, ...
|
|
30
|
+
const [command, ...rawArgs] = argv;
|
|
31
|
+
const scopeIndex = rawArgs.indexOf('--person-scopes');
|
|
32
|
+
if (scopeIndex >= 0 && !rawArgs[scopeIndex + 1]) throw Error('--person-scopes 需要 JSON 文件路径');
|
|
33
|
+
const args = scopeIndex < 0 ? rawArgs : rawArgs.filter((_, i) => i !== scopeIndex && i !== scopeIndex + 1);
|
|
34
|
+
const personScopes = scopeIndex < 0 ? undefined : JSON.parse(fs.readFileSync(rawArgs[scopeIndex + 1], 'utf8'));
|
|
30
35
|
if (!command || command === 'help') {
|
|
31
|
-
console.log('accounts | after-login | switch <账号ID> | default-set <账号ID> | default-clear | draft-preview <流程ID> <值文件> <标题> <计划文件> [明细文件] | draft-save <计划文件> | draft-verify <计划文件> | draft-submit <计划文件> --confirm | draft-update-preview <流程ID> <值文件> <标题> <新计划文件> <明细文件或-> <原requestId> | browser-options <流程ID> <字段ID> [上下文文件] | adapter-register <流程ID> <脚本文件> <映射文件>');
|
|
36
|
+
console.log('meeting fields|view|tasks|preview|run|verify | doc directories|list|init|upload | accounts | after-login | switch <账号ID> | default-set <账号ID> | default-clear | draft-preview <流程ID> <值文件> <标题> <计划文件> [明细文件] [--person-scopes <JSON文件>] | draft-save <计划文件> | draft-verify <计划文件> | draft-submit <计划文件> --confirm | draft-update-preview <流程ID> <值文件> <标题> <新计划文件> <明细文件或-> <原requestId> | browser-options <流程ID> <字段ID> [上下文文件] | adapter-register <流程ID> <脚本文件> <映射文件>');
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
const dist = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -59,6 +64,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
59
64
|
prompt: prefs[scope] ? '已核验默认项目;是否需要切换其他项目?可以更改或取消默认项目。' : '是否切换项目(次账号)?可以设置默认项目,以后登录自动切换。'}, null, 2));
|
|
60
65
|
return;
|
|
61
66
|
}
|
|
67
|
+
if (command === 'meeting') {
|
|
68
|
+
const {meetingMain} = await load('meetings');
|
|
69
|
+
console.log(JSON.stringify(await meetingMain(api, args, accountId, cfg.baseUrl), null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === 'doc') {
|
|
73
|
+
const {documentMain} = await load('documents');
|
|
74
|
+
console.log(JSON.stringify(await documentMain(api, args, accountId, cfg), null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
62
77
|
const runtime = await load('workflow-runtime');
|
|
63
78
|
const preparation = await load('field-preparation');
|
|
64
79
|
const getForm = async (workflowId, requestId) => {
|
|
@@ -69,6 +84,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
69
84
|
if (command === 'browser-options') {
|
|
70
85
|
const [workflowId,fieldId,contextFile]=args;
|
|
71
86
|
const context=contextFile?JSON.parse(fs.readFileSync(contextFile,'utf8')):{};
|
|
87
|
+
context.baseUrl = cfg.baseUrl;
|
|
72
88
|
const form=await getForm(workflowId,context.requestId);
|
|
73
89
|
const adapter=await runtime.loadAdapter(cfg,workflowId,form);
|
|
74
90
|
if(adapter?.browserContext) Object.assign(context,await adapter.browserContext({form,fieldId,context,mapping:adapter.mapping}));
|
|
@@ -85,6 +101,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
85
101
|
if(command==='draft-update-preview' && !requestId)throw Error('更新原稿必须提供requestId');
|
|
86
102
|
const form=await getForm(workflowId,requestId);
|
|
87
103
|
const plan={workflowId,requestName:title,values:JSON.parse(fs.readFileSync(valuesFile,'utf8')),details:detailsFile&&detailsFile!=='-'?JSON.parse(fs.readFileSync(detailsFile,'utf8')):[],baseUrl:cfg.baseUrl,accountId,requestId};
|
|
104
|
+
if (personScopes) plan.personScopes = personScopes;
|
|
88
105
|
if(plan.values.values) { const input=plan.values; plan.values=input.values;plan.browserNames=input.browserNames??{};plan.detailBrowserNames=input.detailBrowserNames??{}; }
|
|
89
106
|
const prepared=await preparation.prepareFields(cfg,plan,{api,form});
|
|
90
107
|
plan.expected=prepared.expected;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {buildHrmCardUrl} from './hrm.js';
|
|
2
|
+
|
|
3
|
+
const clean = v => String(v ?? '').replace(/<[^>]*>/g,'').trim();
|
|
4
|
+
export async function currentPersonAccount(api) {
|
|
5
|
+
const r=await api.get('/api/hrm/login/getAccountList',{__random__:Date.now()},{forceCookie:true});
|
|
6
|
+
const rows=r.json?.data?.accountlist;
|
|
7
|
+
const current=Array.isArray(rows)?rows.filter(a=>String(a.iscurrent)==='1'):[];
|
|
8
|
+
if(current.length!==1 || !/^\d+$/.test(String(current[0].userid??'')) || !/^\d+$/.test(String(current[0].subcompanyid??'')))throw Error('无法核实当前账号及所属分部,停止选人');
|
|
9
|
+
const a=current[0];
|
|
10
|
+
return {id:String(a.userid),divisionId:String(a.subcompanyid),division:a.subcompanyname,name:a.username};
|
|
11
|
+
}
|
|
12
|
+
const conditionKey=c=>Array.isArray(c?.domkey)?c.domkey[0]:c?.domkey;
|
|
13
|
+
export function personSearchQuery(query,conditions,account,context={}) {
|
|
14
|
+
const q={...query};
|
|
15
|
+
const names=conditions.filter(c=>c.isQuickSearch&&c.conditionType==='INPUT');
|
|
16
|
+
const name=names.length===1?names[0]:null;
|
|
17
|
+
const division=conditions.find(c=>conditionKey(c)==='subcompanyid');
|
|
18
|
+
if(!name||!division)throw Error('未识别人员姓名/分部查询条件,需核实当前协议');
|
|
19
|
+
for(const c of conditions) {
|
|
20
|
+
const key=conditionKey(c);
|
|
21
|
+
const value=c.value??c.options?.filter(o=>o.selected).map(o=>o.key).join(',');
|
|
22
|
+
if(key&&value!==undefined&&value!==''&&q[key]===undefined)q[key]=value;
|
|
23
|
+
}
|
|
24
|
+
if(context.subcompanyId && String(context.subcompanyId)!==account.divisionId && context.crossDivision!==true)throw Error('跨分部查询需要用户明确指定范围');
|
|
25
|
+
const target=String(context.subcompanyId??account.divisionId);
|
|
26
|
+
if(!/^\d+$/.test(target))throw Error('分部ID格式错误');
|
|
27
|
+
const key=conditionKey(division),fixed=[q[key],q.subcompanyid1];
|
|
28
|
+
// 固定过滤只能收窄,不能被默认分部或跨分部要求覆盖。
|
|
29
|
+
if(fixed.some(v=>v!=null&&String(v)!==''&&!String(v).split(',').includes(target)))return {query:q,target,empty:true};
|
|
30
|
+
q[key]=target;
|
|
31
|
+
const nk=conditionKey(name);
|
|
32
|
+
if(context.search) {
|
|
33
|
+
if(q[nk] && String(q[nk])!==String(context.search))throw Error('姓名搜索与字段固定条件冲突');
|
|
34
|
+
q[nk]=String(context.search);
|
|
35
|
+
}
|
|
36
|
+
if(q.cmd&&q.cmd!=='search')throw Error('字段指定了其他人员协议,需适配');
|
|
37
|
+
q.cmd='search';
|
|
38
|
+
return {query:q,target,empty:false};
|
|
39
|
+
}
|
|
40
|
+
export function validPersonCandidates(rows,target,search,baseUrl) {
|
|
41
|
+
const selected=new Map();
|
|
42
|
+
for(const r of rows) {
|
|
43
|
+
const id=clean(r.id),divisionId=clean(r.subcompanyid??r.subcompanyid1),name=clean(r.lastnamespan??r.lastname);
|
|
44
|
+
if(!id||!divisionId)throw Error('人员结果缺少账号ID或分部ID,无法核验候选');
|
|
45
|
+
if(divisionId!==target || (search&&!name.includes(String(search))))continue;
|
|
46
|
+
selected.set(id,{id,name,department:clean(r.departmentname??r.departmentidspan),division:clean(r.subcompanyname??r.subcompanyid1span),divisionId,url:buildHrmCardUrl(baseUrl,id)});
|
|
47
|
+
}
|
|
48
|
+
return [...selected.values()];
|
|
49
|
+
}
|
|
50
|
+
export async function queryPersonBrowser(api,form,field,query,context={}) {
|
|
51
|
+
const before=await currentPersonAccount(api);
|
|
52
|
+
if(String(form.params.currentUserid)!==before.id)throw Error('表单身份与当前账号不符,请重新读取');
|
|
53
|
+
const path='/api/public/browser/';
|
|
54
|
+
const c=await api.get(path+'condition/'+field.detailtype,query,{forceCookie:true});
|
|
55
|
+
const conditions=c.json?.conditions;
|
|
56
|
+
if(!Array.isArray(conditions))throw Error('人员浏览条件查询失败,未证明表单可选性');
|
|
57
|
+
const scoped=personSearchQuery(query,conditions,before,context);
|
|
58
|
+
const rows=[],seen=new Set();let total=null,complete=scoped.empty;
|
|
59
|
+
const size=50;
|
|
60
|
+
for(let page=1;!complete&&page<=100;page++) {
|
|
61
|
+
const q={...scoped.query,current:String(page),pageSize:String(size),min:String((page-1)*size+1),max:String(page*size)};
|
|
62
|
+
const url=path+'data/'+field.detailtype;
|
|
63
|
+
const method=(url+'?'+new URLSearchParams(q)).length>6000?'post':'get';
|
|
64
|
+
const r=await api[method](url,q,{forceCookie:true}),j=r.json;
|
|
65
|
+
if(!r.ok||!j||j.status===false||j.status==='false'||!Array.isArray(j.datas)||j.count==null||!Number.isInteger(Number(j.count))||Number(j.count)<0)throw Error('人员浏览查询失败或分页总数缺失,未证明表单可选性');
|
|
66
|
+
if(total!==null&&total!==Number(j.count))throw Error('人员查询范围发生变化,请重查');
|
|
67
|
+
total=Number(j.count);
|
|
68
|
+
for(const row of j.datas){const id=String(row.id??'');if(seen.has(id))throw Error('人员分页重复,无法确认完整范围');seen.add(id);rows.push(row);}
|
|
69
|
+
if(rows.length>total)throw Error('人员分页条数与总数不符');
|
|
70
|
+
complete=rows.length>=total;
|
|
71
|
+
if(!complete&&!j.datas.length)throw Error('人员分页不完整');
|
|
72
|
+
}
|
|
73
|
+
if(!complete)throw Error('人员查询超出分页上限,不能据此唯一选人');
|
|
74
|
+
const after=await currentPersonAccount(api);
|
|
75
|
+
if(after.id!==before.id||after.divisionId!==before.divisionId)throw Error('选人期间账号/分部变化,候选已失效,请重查');
|
|
76
|
+
const items=validPersonCandidates(rows,scoped.target,context.search,context.baseUrl??'');
|
|
77
|
+
return {account:before,scope:{divisionId:scoped.target,crossDivision:context.crossDivision===true},conditions,
|
|
78
|
+
verification:'field-browser-api',complete:true,count:items.length,items,
|
|
79
|
+
resolution:items.length===1?'unique':items.length?'choose':'none',
|
|
80
|
+
note:'已核验当前字段浏览接口范围;不代替保存/提交时后端权限校验。零匹配不扩大范围。'};
|
|
81
|
+
}
|