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,78 @@
1
+ /**
2
+ * BDIP5 表单文件上传(图片/附件)。
3
+ *
4
+ * 接口(用户抓包确认):POST /api/doc/upload/uploadFile2Doc
5
+ * query: category=,,<cat>&workflowid=<wf>&listType=<img|list>&uploadSrc=wfform&loginuserid=<uid>
6
+ * multipart: name=<filename>, isFirstUploadFile=9, fieldId=<表单字段id>,
7
+ * workflowId=<wf>, secretLevel=<>, ts=<时间戳>, file=<二进制>
8
+ * listType=img→图片字段;listType=list→附件字段。
9
+ * aiAgentToken 鉴权可用。
10
+ * 响应:data.imagefileid(上传后文件 id,填进表单字段的值)、filename、filelink、versionId、fileidCode 等。
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ const IMG_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg', 'tif', 'tiff']);
15
+ /** 判断文件按图片还是附件上传(按扩展名;可被 --list-type 覆盖)。 */
16
+ export function guessListType(filePath) {
17
+ const ext = (path.extname(filePath).slice(1) || '').toLowerCase();
18
+ return IMG_EXT.has(ext) ? 'img' : 'list';
19
+ }
20
+ /**
21
+ * 上传本地文件到表单字段。返回 imagefileid(填进 field<id> 的值)。
22
+ * listType: 'img'(图片字段) / 'list'(附件字段),不传按扩展名猜。
23
+ */
24
+ export async function uploadFile(cfg, opts) {
25
+ const { workflowId, fieldId, filePath } = opts;
26
+ const listType = opts.listType ?? guessListType(filePath);
27
+ const category = opts.category ?? '21';
28
+ const loginuserid = opts.loginuserid ?? '1';
29
+ if (!fs.existsSync(filePath))
30
+ throw new Error(`文件不存在: ${filePath}`);
31
+ const buf = fs.readFileSync(filePath);
32
+ const filename = path.basename(filePath);
33
+ const ext = (path.extname(filename).slice(1) || '').toLowerCase();
34
+ const mime = ext === 'png' ? 'image/png'
35
+ : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg'
36
+ : ext === 'gif' ? 'image/gif'
37
+ : 'application/octet-stream';
38
+ // 直接用 fetch + aiAgentToken(不走 E9Api,因 multipart 需自定义 body)
39
+ const sess = (await import('./auth-token.js')).AuthTokenSession.fromProfile(cfg);
40
+ const base = cfg.baseUrl.replace(/\/+$/, '');
41
+ const url = `${base}/api/doc/upload/uploadFile2Doc?category=,,${category}&workflowid=${workflowId}&listType=${listType}&wfTestStr=&uploadSrc=wfform&loginuserid=${loginuserid}`;
42
+ const fd = new FormData();
43
+ fd.append('name', filename);
44
+ fd.append('isFirstUploadFile', '9');
45
+ fd.append('fieldId', fieldId);
46
+ fd.append('workflowId', String(workflowId));
47
+ fd.append('secretLevel', '');
48
+ fd.append('ts', String(Date.now()));
49
+ fd.append('file', new Blob([buf], { type: mime }), filename);
50
+ const r = await fetch(url, {
51
+ method: 'POST',
52
+ headers: { aiAgentToken: sess.token },
53
+ body: fd,
54
+ signal: AbortSignal.timeout(60000),
55
+ });
56
+ const t = await r.text();
57
+ let j = null;
58
+ try {
59
+ j = JSON.parse(t);
60
+ }
61
+ catch { /* non-json */ }
62
+ if (!r.ok || !j?.data?.fileid) {
63
+ throw new Error(`上传失败 status=${r.status}: ${t.slice(0, 300)}`);
64
+ }
65
+ const d = j.data;
66
+ return {
67
+ fileid: String(d.fileid),
68
+ imagefileid: String(d.imagefileid ?? ''),
69
+ filename: d.filename,
70
+ filelink: d.filelink,
71
+ loadlink: d.loadlink,
72
+ versionId: d.versionId,
73
+ fileidCode: d.fileidCode,
74
+ isImg: d.isImg,
75
+ filesize: d.filesize,
76
+ raw: j,
77
+ };
78
+ }
package/dist/wfuser.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 当前用户信息(发起流程时,申请人/部门等字段默认填值用)。
3
+ *
4
+ * 接口(用户抓包确认):GET /api/hrm/login/getAccountList?__random__=<ts>
5
+ * aiAgentToken 鉴权。返回 data.{userid, username, deptid, deptname, subcompanyname, jobs}。
6
+ */
7
+ import { createApiFromProfile } from './client.js';
8
+ import { todayStr } from './util.js';
9
+ /**
10
+ * 取当前登录用户信息。GET /api/hrm/login/getAccountList?__random__=<ts>
11
+ * 走 E9Api(ensureValid + 双头 + 重试 + 失效逃生,见 client.js)——2026-09-20 e9pc 实测:
12
+ * 裸 fetch(aiAgentToken) 会 404(缺头/token 状态不同步),E9Api 通道正常返回 accountlist。
13
+ */
14
+ export async function getCurrentUser(cfg) {
15
+ const api = await createApiFromProfile(cfg);
16
+ const r = await api.get('/api/hrm/login/getAccountList', { __random__: Date.now() });
17
+ const j = r?.json ?? {};
18
+ const d = j?.data ?? {};
19
+ // accountlist 里 iscurrent=1 的是当前账号;data 顶层也有当前用户字段
20
+ const list = Array.isArray(d.accountlist) ? d.accountlist : [];
21
+ const cur = list.find((a) => String(a.iscurrent) === '1') ?? list[0] ?? d;
22
+ return {
23
+ userid: String(cur.userid ?? ''),
24
+ username: String(cur.username ?? ''),
25
+ deptid: String(cur.deptid ?? ''),
26
+ deptname: String(cur.deptname ?? ''),
27
+ subcompanyname: String(cur.subcompanyname ?? ''),
28
+ jobs: String(cur.jobs ?? ''),
29
+ };
30
+ }
31
+ /**
32
+ * 当前用户是否系统管理员。GET /api/ec/dev/app/getUserInfo?__random__=<ts> → isAdmin===true。
33
+ * 返回 { isAdmin: true|false|undefined }——undefined 表示无法确认(未取到 isAdmin 字段),
34
+ * 调用方按「非管理员」fail-closed 处理(越权防护默认不放行)。
35
+ * 鉴权:token 通道优先;token no-op(200 但无 isAdmin)且配了 cookie → 强制 cookie 头重发逃生。
36
+ */
37
+ export async function getCurrentUserIsAdmin(cfg) {
38
+ const api = await createApiFromProfile(cfg);
39
+ let r = await api.get('/api/ec/dev/app/getUserInfo', { __random__: Date.now() });
40
+ let isAdmin = pickIsAdmin(r?.json);
41
+ if (isAdmin === undefined && cfg.cookie) {
42
+ r = await api.get('/api/ec/dev/app/getUserInfo', { __random__: Date.now() }, { forceCookie: true });
43
+ isAdmin = pickIsAdmin(r?.json);
44
+ }
45
+ return { isAdmin, raw: r?.json ?? null };
46
+ }
47
+
48
+ /** 深度扫描找 isAdmin 字段(键名大小写不敏感,取值 true/'true'/1 → true)。 */
49
+ function pickIsAdmin(obj) {
50
+ if (obj == null)
51
+ return undefined;
52
+ if (typeof obj === 'object') {
53
+ for (const [k, v] of Object.entries(obj)) {
54
+ if (String(k).toLowerCase() === 'isadmin') {
55
+ return v === true || v === 'true' || v === '1' || v === 1;
56
+ }
57
+ if (v && typeof v === 'object') {
58
+ const r = pickIsAdmin(v);
59
+ if (r !== undefined)
60
+ return r;
61
+ }
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ // 人员类标签(→ userid 或 username)
68
+ const PERSON_LABEL = /申请人|发起人|续签人|经办人|经手人|创建人|人员|姓名|联系人|负责人|填报人/;
69
+ // 部门类标签(→ deptid 或 deptname)
70
+ const DEPT_LABEL = /申请部门|所属部门|部门|部门名称|单位|科室|组织/;
71
+ // 日期类标签(→ 今天)
72
+ const DATE_LABEL = /日期/;
73
+ // 时间类标签(结束时间/开始时间,不填默认值,避免时间字段误填日期)
74
+ const TIME_LABEL = /时间$/;
75
+ /**
76
+ * 按字段标签 + 类型,返回当前用户相关的默认值(无则 undefined)。
77
+ * htmltype: 3=浏览选择(给 id),1/2=文本(给名称)。
78
+ */
79
+ export function defaultForField(label, htmltype, user) {
80
+ if (!label)
81
+ return undefined;
82
+ if (PERSON_LABEL.test(label)) {
83
+ if (htmltype === 3)
84
+ return user.userid || undefined;
85
+ return user.username || undefined;
86
+ }
87
+ if (DEPT_LABEL.test(label)) {
88
+ if (htmltype === 3)
89
+ return user.deptid || undefined;
90
+ return user.deptname || undefined;
91
+ }
92
+ if (DATE_LABEL.test(label) && !TIME_LABEL.test(label)) {
93
+ return todayStr();
94
+ }
95
+ return undefined;
96
+ }
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {createHash} from 'node:crypto';
4
+ import {pathToFileURL} from 'node:url';
5
+ import {createApiFromProfile} from './client.js';
6
+ import {buildCreateSubmitBody} from './wfstart.js';
7
+ import {stableConfig} from './field-preparation.js';
8
+
9
+ const idOf = id => String(id).replace(/^field/, '');
10
+ export function definitions(form) {
11
+ return Object.entries(form.tableInfo ?? {}).flatMap(([table,t]) => Object.entries(t.fieldinfomap ?? {}).map(([id,f]) => ({...f,id:String(f.fieldid ?? id),table})));
12
+ }
13
+ export function fingerprint(form) {
14
+ const schema = definitions(form).map(f => [f.table,f.id,f.fieldname,f.fieldlabel,f.viewattr,f.htmltype,f.detailtype,f.fielddbtype,f.browserattr]).sort((a,b)=>String(a).localeCompare(String(b)));
15
+ const browsers = Object.entries(form.browserInfo ?? {}).map(([key,b]) => [key,b.relateFieldid,Object.keys(b.dataParams??{}).sort(),b.dataParams?.type,b.dataParams?.fielddbtype,b.conditionDataParams?.type]);
16
+ return createHash('sha256').update(JSON.stringify(['unified-linkage-2',schema,browsers,form.params?.formid,form.params?.layoutversion,stableConfig(form.linkageCfg??{})])).digest('hex');
17
+ }
18
+ export function adapterDirectory(cfg, workflowId) {
19
+ const env = createHash('sha256').update(cfg.baseUrl.replace(/\/+$/,'')).digest('hex').slice(0,24);
20
+ if (!/^\d+$/.test(String(workflowId))) throw Error('流程ID格式错误');
21
+ return path.join(path.dirname(cfg._configPath),'workflow-adapters',env,String(workflowId));
22
+ }
23
+ export function registerAdapter(cfg, workflowId, form, source, mapping) {
24
+ // 仅保存经用户/维护者审查的逻辑和映射;不保存本次业务值或服务器表单快照。
25
+ for (const [name,id] of Object.entries(mapping)) if (!definitions(form).some(f=>f.id===String(id))) throw Error('映射字段不在当前表单:'+name);
26
+ const dir=adapterDirectory(cfg,workflowId);fs.mkdirSync(dir,{recursive:true});
27
+ const hash=fingerprint(form), script=hash+'.mjs';
28
+ fs.copyFileSync(source,path.join(dir,script));
29
+ fs.writeFileSync(path.join(dir,'index.json'),JSON.stringify({fingerprint:hash,script,mapping,scriptDigest:createHash('sha256').update(fs.readFileSync(source)).digest('hex')},null,2));
30
+ }
31
+ export async function loadAdapter(cfg,workflowId,form) {
32
+ const dir=adapterDirectory(cfg,workflowId),index=path.join(dir,'index.json');
33
+ if (!fs.existsSync(index)) return null;
34
+ const item=JSON.parse(fs.readFileSync(index,'utf8'));
35
+ if (item.fingerprint!==fingerprint(form)) throw Error('表单结构已变化,本地适配失效;请重新适配');
36
+ if (item.script!==item.fingerprint+'.mjs') throw Error('适配索引无效');
37
+ const digest=createHash('sha256').update(fs.readFileSync(path.join(dir,item.script))).digest('hex');
38
+ if(item.scriptDigest!==digest)throw Error('适配逻辑已变化或旧索引缺少校验,请重新验证登记');
39
+ return {...await import(pathToFileURL(path.join(dir,item.script)).href+'?v='+digest),mapping:item.mapping,scriptDigest:digest};
40
+ }
41
+ export async function detailTables(api,form,workflowId,requestId) {
42
+ const result={};
43
+ for(const [mark,t] of Object.entries(form.tableInfo??{})) {
44
+ if(!/^detail_\d+$/.test(mark))continue;
45
+ const r=await api.post('/api/workflow/reqform/detailData',{iscreate:requestId?'0':'1',requestid:String(requestId??'-1'),workflowid:String(workflowId),detailmark:mark,
46
+ reqParams:JSON.stringify({...form.params,...form.submitParams}),f_weaver_belongto_userid:String(form.params.currentUserid),f_weaver_belongto_usertype:String(form.params.f_weaver_belongto_usertype??'0')},{forceCookie:true});
47
+ if(!r.json?.[mark])throw Error('未返回明细表 '+mark);
48
+ const d=r.json[mark];
49
+ result[mark]={...d,rows:Object.entries(d.rowDatas??{}).sort(([a],[b])=>Number(a.replace('row_',''))-Number(b.replace('row_',''))).map(([,row])=>row)};
50
+ }
51
+ return result;
52
+ }
53
+ export function browserQuery(form,fieldId,context={}) {
54
+ const f=definitions(form).find(x=>x.id===String(fieldId));if(!f)throw Error('浏览框字段不存在');
55
+ const b=form.browserInfo?.[`${f.detailtype}_${f.fielddbtype}`];
56
+ if(!b)throw Error('缺少浏览框配置,请读取网页配置后适配');
57
+ const p=form.params;
58
+ const query={...b.conditionDataParams,...b.completeParams,...b.dataParams,wfid:String(p.workflowid),billid:p.formid,isbill:p.isbill,
59
+ requestid:String(p.requestid??'-1'),fieldid:String(fieldId),viewtype:f.table==='main'?'0':'1',fromModule:'workflow',
60
+ f_weaver_belongto_userid:p.f_weaver_belongto_userid,f_weaver_belongto_usertype:p.f_weaver_belongto_usertype,
61
+ current:'1',pageSize:String(context.pageSize??10),min:'1',max:String(context.pageSize??10)};
62
+ for(const dep of f.browserattr?.cusDependShip??[]) {
63
+ const dependency=definitions(form).find(x=>x.id===String(dep.fieldid));
64
+ if(!dependency)throw Error('依赖字段不存在');
65
+ const values=dependency.table==='main'?context.main:context.row;
66
+ const cell=dependency.table==='main'?form.maindata?.['field'+dep.fieldid]:context.rowCells?.['field'+dep.fieldid];
67
+ const value=values?.[dep.fieldid]??values?.['field'+dep.fieldid]??cell?.value;
68
+ if(value===undefined)throw Error('缺少同一行/主表依赖值:'+dep.fieldid);
69
+ if(!dep.key||!b.dataParams?.currenttime)throw Error('缺少依赖字段key或服务器时间标识');
70
+ query[dep.key.toLowerCase()+'_'+b.dataParams.currenttime]=value;
71
+ }
72
+ return {field:f,query};
73
+ }
74
+ export async function queryBrowser(api,form,fieldId,context={}) {
75
+ const {field,query}=browserQuery(form,fieldId,context);
76
+ const condition=await api.get('/api/public/browser/condition/'+field.detailtype,query,{forceCookie:true});
77
+ const conditions=condition.json?.conditions??condition.json?.conditioninfo?.conditions;
78
+ if(context.search) {
79
+ const candidates=(conditions??[]).filter(c=>context.conditionTitle?c.label===context.conditionTitle||c.title===context.conditionTitle:true).filter(c=>c.domkey);
80
+ if(candidates.length!==1)throw Error('查询条件不唯一,请选择服务器返回的条件名称');
81
+ const key=Array.isArray(candidates[0].domkey)?candidates[0].domkey[0]:candidates[0].domkey;
82
+ query[key]=context.search;
83
+ }
84
+ const r=await api.get('/api/public/browser/data/'+field.detailtype,query,{forceCookie:true});
85
+ if(!r.json||r.json.status===false||r.json.msg==='登录信息超时')throw Error('关联查询失败');
86
+ return {conditions,result:r.json};
87
+ }
88
+ const strip=v=>String(v??'').replace(/<[^>]*>/g,'').trim();
89
+ const names=c=>(Array.isArray(c?.specialobj)?c.specialobj:[]).map(x=>strip(x.name)).join('##~~@@~~##');
90
+ export function isEntityBrowser(f) { return Number(f?.htmltype)===3 && ![2,19,20,402,403].includes(Number(f?.detailtype)); }
91
+ export function richText(value) { return String(value??'').replace(/<br\s*\/?\s*>/gi,'\n').replace(/<\/(?:p|div)>/gi,'\n').replace(/<(?:p|div)[^>]*>/gi,'').replace(/&nbsp;/g,' ').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/\r\n/g,'\n').replace(/\n$/,''); }
92
+ export function sameValue(a,b,field={}) {
93
+ if(Number(field.htmltype)===1 && [2,3,4,5].includes(Number(field.detailtype)) && a!=='' && b!=='' && Number.isFinite(Number(a))&&Number.isFinite(Number(b)))return Number(a)===Number(b);
94
+ if(Number(field.htmltype)===2&&String(field.fielddbtype??'').toLowerCase().includes('text'))return richText(a)===richText(b);
95
+ return String(a??'')===String(b??'');
96
+ }
97
+ export function buildDraft(plan,form,tables={}) {
98
+ if(form.params?.verifyRight!==true||String(form.params.currentUserid)!==plan.accountId)throw Error('账号或权限变化');
99
+ if(plan.requestId&&String(form.params.currentnodetype??form.params.nodetype)!=='0')throw Error('原请求已不在创建节点');
100
+ if(String(form.params.workflowtype)==='26'||form.params.isWorkflowDoc===true)throw Error('公文保存需单独验证');
101
+ const defs=definitions(form),main={};const expectedMain={},expectedDetails=[];
102
+ for(const f of defs.filter(f=>f.table==='main')){
103
+ const cell=form.maindata?.['field'+f.id];
104
+ if(cell && Number(f.id)>=0){main[f.id]=cell.value;expectedMain[f.id]={value:cell.value,name:names(cell)};}
105
+ }
106
+ const assign=(target,expected,values,fields)=>{
107
+ for(const [raw,value] of Object.entries(values??{})){
108
+ const id=idOf(raw),f=fields.find(x=>x.id===id);if(!f||![2,3].includes(Number(f.viewattr)))throw Error('字段不可由用户赋值:'+id);
109
+ target[id]=value;expected[id]={value,name:plan.browserNames?.[id]??''};
110
+ }
111
+ };
112
+ assign(main,expectedMain,plan.values,defs.filter(f=>f.table==='main'));
113
+ const requested=[...(plan.details??[])];
114
+ for(const [mark,source]of Object.entries(tables))if(!requested.some(d=>'detail_'+(Number(d.index)+1)===mark))requested.push({index:Number(mark.replace('detail_',''))-1,rows:source.rows.map(()=>({}))});
115
+ const details=requested.map(dt=>{
116
+ const mark='detail_'+(Number(dt.index)+1),source=tables[mark];if(!source)throw Error('缺少明细配置');
117
+ if(plan.requestId&&source.rows.length!==dt.rows.length)throw Error('原草稿明细行数变化;增删行需重新预览适配');
118
+ const expected=[];
119
+ const rows=dt.rows.map((row,i)=>{
120
+ const cells=plan.requestId||plan.serverResolved?source.rows[i]:source.addRowDefValue;
121
+ const values={},exp={};
122
+ for(const f of defs.filter(f=>f.table===mark)){
123
+ const c=cells?.['field'+f.id];
124
+ const actual=c??cells?.[f.id];
125
+ if(actual&&typeof actual==='object'&&'value'in actual){values[f.id]=actual.value;exp[f.id]={value:actual.value,name:names(actual)};}
126
+ }
127
+ assign(values,exp,row,defs.filter(f=>f.table===mark));
128
+ for(const [id,c]of Object.entries(exp))c.name=plan.detailBrowserNames?.[dt.index]?.[i]?.[id]??c.name;
129
+ expected.push({keyid:plan.requestId?source.rows[i].keyid:undefined,fields:exp});return values;
130
+ });expectedDetails.push({mark,index:dt.index,rows:expected});return {index:dt.index,rows};
131
+ });
132
+ for(const [id,c]of Object.entries(expectedMain))if(isEntityBrowser(defs.find(f=>f.id===id))&&c.value&&!c.name)throw Error('浏览框显示名缺失,请查询实际选项并确认:'+id);
133
+ for(const dt of expectedDetails)for(const row of dt.rows)for(const [id,c]of Object.entries(row.fields))if(isEntityBrowser(defs.find(f=>f.id===id))&&c.value&&!c.name)throw Error('明细浏览框显示名缺失,请查询实际选项并确认:'+id);
134
+ const body={...form.submitParams,...buildCreateSubmitBody(plan.workflowId,main,plan.requestName,form.params,form.submitParams,plan.browserNames,details),src:'save',isaffirmance:'0',isaffirmanceSrc:''};
135
+ for(const [id,c]of Object.entries(expectedMain))if(c.name)body['field'+id+'name']=c.name;
136
+ for(const dt of expectedDetails)dt.rows.forEach((r,i)=>{for(const [id,c]of Object.entries(r.fields))if(c.name)body[`field${id}_${i}name`]=c.name;});
137
+ if(plan.requestId){body.iscreate='0';body.requestid=String(plan.requestId);for(const dt of expectedDetails){body['submitdtlid'+dt.index]=dt.rows.map((r,i)=>{body[`dtl_id_${dt.index}_${i}`]=String(r.keyid??'');return i;}).join(',')+',';}}
138
+ return {body,expected:{main:expectedMain,details:expectedDetails,fingerprint:fingerprint(form),accountId:plan.accountId,requestName:plan.requestName}};
139
+ }
140
+ export function verifyDraft(expected,form,tables,{action='save'}={}) {
141
+ const problems=[],defs=definitions(form),p=form.params??{};
142
+ if(String(p.currentUserid)!==expected.accountId||String(p.creater)!==expected.accountId)problems.push('账号/创建人');
143
+ if(action==='save'&&String(p.currentnodetype??p.nodetype)!=='0')problems.push('非创建节点');
144
+ if(action==='submit'&&String(p.currentnodetype??p.nodetype)==='0')problems.push('提交后仍为创建节点');
145
+ if(fingerprint(form)!==expected.fingerprint)problems.push('结构变化');
146
+ if(String(p.requestname??form.maindata?.['field-1']?.value)!==expected.requestName)problems.push('标题');
147
+ const check=(fields,cells,table)=>{for(const [id,e]of Object.entries(fields)){const c=cells?.['field'+id];if(!c||!sameValue(c.value,e.value,defs.find(f=>f.id===id)))problems.push(table+':'+id);if(e.name&&names(c)!==strip(e.name))problems.push(table+':'+id+':显示名');}};
148
+ check(expected.main,form.maindata,'main');
149
+ for(const dt of expected.details){const rows=tables[dt.mark]?.rows;if(!rows||rows.length!==dt.rows.length){problems.push(dt.mark+':行数');continue;}dt.rows.forEach((r,i)=>{if(r.keyid&&String(rows[i].keyid)!==String(r.keyid))problems.push(dt.mark+':行标识');check(r.fields,rows[i],dt.mark+':'+i);});}
150
+ return problems;
151
+ }