weaver-e9-assistant-cli 1.1.12 → 1.1.13
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/dist/cli.js +51 -5
- package/dist/submissionstate.js +13 -0
- package/dist/todosubmit.js +72 -1
- package/dist/wfstart.js +81 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseIdList, batchSubmit, batchForward, batchAttention, getAttentionTyp
|
|
|
12
12
|
import { pageUrl, availablePages } from './pageopen.js';
|
|
13
13
|
import { buildBlogMyUrl, buildCubeCardUrl, buildCubeSearchUrl, normalizeE9Url } from './links.js';
|
|
14
14
|
import { createContext, loadContext, updateContext } from './createcontext.js';
|
|
15
|
-
import { loadSubmission, updateSubmission } from './submissionstate.js';
|
|
15
|
+
import { loadSubmission, updateSubmission, pickVerifiedRequest } from './submissionstate.js';
|
|
16
16
|
import { saveSubmissionPlan, loadSubmissionPlan } from './submissionplan.js';
|
|
17
17
|
import { saveCandidateQuery, loadCandidateQuery, persistSearchResult } from './querystate.js';
|
|
18
18
|
import { getFlowSummary } from './wfsummary.js';
|
|
@@ -272,6 +272,7 @@ function usage() {
|
|
|
272
272
|
' --values/--values-file 给值(key=字段id,值=文本/option id;中文用 --values-file);',
|
|
273
273
|
' --detail-file 给明细表行数据(JSON 数组 [{index, rows:[{fieldId:value}]}],index=明细表序号0起,每行多字段);',
|
|
274
274
|
' --file <字段id>=<本地路径> 上传图片/附件;明细行用 <字段id>_<行号>=<路径>(逗号分隔多个);默认 dry-run,--yes 真发',
|
|
275
|
+
' --yes --plan <plan_id> [--verify] 复现确认计划并真发;--verify 提交后同进程核验 requestid/当前节点',
|
|
275
276
|
' email list 收件箱邮件列表(单步 GET /api/email/list/allList)',
|
|
276
277
|
' --page 页码,默认 1',
|
|
277
278
|
' --subject 主题关键字过滤',
|
|
@@ -571,7 +572,7 @@ async function runContextCreate(args) {
|
|
|
571
572
|
const context = loadContext(id);
|
|
572
573
|
if (!args.flags.confirm || !args.flags.submit) {
|
|
573
574
|
const cfg = loadConfig(flagStr(args, 'config'));
|
|
574
|
-
const prepared = await prepareCreateSubmission(cfg, context.workflowId, context.values, context.requestName, context.details);
|
|
575
|
+
const prepared = await prepareCreateSubmission(cfg, context.workflowId, context.values, context.requestName, context.details, undefined, context.files ?? []);
|
|
575
576
|
printFormulaResults(prepared);
|
|
576
577
|
const plan = saveSubmissionPlan({ ...prepared, files: context.files });
|
|
577
578
|
updateContext(id, { planId: plan.id });
|
|
@@ -722,6 +723,38 @@ async function runCreate(args) {
|
|
|
722
723
|
}
|
|
723
724
|
}
|
|
724
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* 真实发起后同进程核验:查「我的请求」确认 requestid 与当前节点。
|
|
728
|
+
* 用一次列表查询(~1s)替代额外的一次工具往返 + 3.5s 的 todo summary。
|
|
729
|
+
*/
|
|
730
|
+
async function verifyCreatedInline(cfg, { requestName, requestId, workflowId, createDate, submissionId }) {
|
|
731
|
+
try {
|
|
732
|
+
const result = await getWfList(cfg, {
|
|
733
|
+
category: 'mine', page: 1, keyword: requestName,
|
|
734
|
+
createTime: createDate, workflow: workflowId,
|
|
735
|
+
});
|
|
736
|
+
const candidates = pickVerifiedRequest(result.items, { requestName, requestId });
|
|
737
|
+
if (candidates.length === 1) {
|
|
738
|
+
const item = candidates[0];
|
|
739
|
+
if (submissionId)
|
|
740
|
+
updateSubmission(submissionId, { status: 'created', requestId: item.requestId, verifiedAt: new Date().toISOString() });
|
|
741
|
+
console.log(` ✅ 核验通过 requestid=${item.requestId} 当前节点=${item.nodeName ?? item.node ?? '—'}`);
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
if (candidates.length === 0) {
|
|
745
|
+
console.log(` ⚠ 核验:我的请求列表暂未查到${submissionId ? `(可稍后 e9 create verify ${submissionId})` : ''}`);
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
console.log(` ⚠ 核验:查到 ${candidates.length} 条同名候选,请人工确认:`);
|
|
749
|
+
for (const item of candidates)
|
|
750
|
+
console.log(` · requestid=${item.requestId} ${item.nodeName ?? item.node ?? ''} ${item.createdate ?? ''}`);
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
console.log(` ⚠ 核验失败:${e.message}`);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
725
758
|
async function runCreateStart(args) {
|
|
726
759
|
const cfg = loadConfig(flagStr(args, 'config'));
|
|
727
760
|
const id = flagStr(args, 'id');
|
|
@@ -899,7 +932,7 @@ async function runCreateStart(args) {
|
|
|
899
932
|
console.log(` ▸ field${fl.fieldId} ← ${fl.filePath}`);
|
|
900
933
|
console.log(' 附件上传会产生新文件 ID,不能冻结到本次确认计划;请在无附件字段变更时单独确认。');
|
|
901
934
|
}
|
|
902
|
-
const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details, r);
|
|
935
|
+
const prepared = await prepareCreateSubmission(cfg, wfId, values, requestname, details, r, files);
|
|
903
936
|
printFormulaResults(prepared);
|
|
904
937
|
const plan = saveSubmissionPlan({ ...prepared, files });
|
|
905
938
|
if (details.length > 0) {
|
|
@@ -952,6 +985,15 @@ async function runCreateStart(args) {
|
|
|
952
985
|
console.log(` ⚠ 取流转候选失败:${e.message}`);
|
|
953
986
|
}
|
|
954
987
|
}
|
|
988
|
+
if (args.flags.verify) {
|
|
989
|
+
await verifyCreatedInline(cfg, {
|
|
990
|
+
requestName: plan.requestName,
|
|
991
|
+
requestId: rid ? String(rid) : '',
|
|
992
|
+
workflowId: wfId,
|
|
993
|
+
createDate: todayStr(),
|
|
994
|
+
submissionId: submitResult.submission.id,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
955
997
|
console.log(BOX_TOP);
|
|
956
998
|
if (j)
|
|
957
999
|
console.log('响应:', JSON.stringify(j).slice(0, 600));
|
|
@@ -5401,10 +5443,14 @@ async function main() {
|
|
|
5401
5443
|
console.log(usage());
|
|
5402
5444
|
return;
|
|
5403
5445
|
}
|
|
5404
|
-
|
|
5446
|
+
if (!args._[0]) {
|
|
5447
|
+
console.log(usage());
|
|
5448
|
+
return;
|
|
5449
|
+
}
|
|
5450
|
+
const cmd = args._[0];
|
|
5405
5451
|
if (cmd === 'version') {
|
|
5406
5452
|
const buildHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex').slice(0, 12);
|
|
5407
|
-
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.
|
|
5453
|
+
console.log(JSON.stringify({ name: 'weaver-e9-assistant-cli', version: '1.1.13', buildHash }, null, 2));
|
|
5408
5454
|
return;
|
|
5409
5455
|
}
|
|
5410
5456
|
if (cmd === 'whoami') {
|
package/dist/submissionstate.js
CHANGED
|
@@ -79,3 +79,16 @@ export function extractCreatedRequestId(response) {
|
|
|
79
79
|
const accepted = !!requestId && (type === 'SUCCESS' || type === 'ASYNC_SUBMIT');
|
|
80
80
|
return { accepted, requestId: requestId ? String(requestId) : '', type: String(type) };
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 从「我的请求」列表中挑选可用于核验的条目。
|
|
85
|
+
* - 有 requestId:只认 id 完全相等的(最可靠,不受同名影响);
|
|
86
|
+
* - 无 requestId(超时/状态不明):按标题完全匹配,交给调用方判断是否唯一。
|
|
87
|
+
*/
|
|
88
|
+
export function pickVerifiedRequest(items, { requestName = '', requestId = '' } = {}) {
|
|
89
|
+
const list = Array.isArray(items) ? items : [];
|
|
90
|
+
if (requestId) {
|
|
91
|
+
return list.filter((item) => String(item?.requestId ?? '') === String(requestId));
|
|
92
|
+
}
|
|
93
|
+
return list.filter((item) => item?.title === requestName || item?.rawTitle === requestName);
|
|
94
|
+
}
|
package/dist/todosubmit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createApiFromProfile } from './client.js';
|
|
2
2
|
import { getWfList } from './workflow.js';
|
|
3
3
|
import { todayStr, nowTimeStr } from './util.js';
|
|
4
|
-
import { findNonEditableFields } from './wfstart.js';
|
|
4
|
+
import { findNonEditableFields, resolveLinkage } from './wfstart.js';
|
|
5
5
|
import { getBrowserOptions, isCustomBrowserField, browserNameSeparator, normalizeSelectValue } from './wfform.js';
|
|
6
6
|
import { applyFormulas } from './formula.js';
|
|
7
7
|
import { uploadFile, fieldListType, uploadFieldValue } from './wfupload.js';
|
|
@@ -208,6 +208,60 @@ async function normalizeMainValues(cfg, workflowId, tableInfo, mainValues, brows
|
|
|
208
208
|
return overrides;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* 从联动结果中挑出可回填的主表字段:
|
|
213
|
+
* - 目标必须是主表字段(明细联动暂未覆盖);
|
|
214
|
+
* - 用户显式改过的字段不回填(用户值优先);
|
|
215
|
+
* - 只补空值,避免覆盖已有数据。
|
|
216
|
+
*/
|
|
217
|
+
export function pickLinkageTargets(changes, { mainValues = {}, triggers = {}, meta } = {}) {
|
|
218
|
+
const linked = {};
|
|
219
|
+
for (const [fid, cv] of Object.entries(changes ?? {})) {
|
|
220
|
+
const field = meta?.fieldById?.get(String(fid));
|
|
221
|
+
if (!field || isDetailTableKey(field.table))
|
|
222
|
+
continue;
|
|
223
|
+
if (Object.prototype.hasOwnProperty.call(triggers, fid))
|
|
224
|
+
continue;
|
|
225
|
+
if (mainValues[fid] !== undefined && mainValues[fid] !== null && mainValues[fid] !== '')
|
|
226
|
+
continue;
|
|
227
|
+
const value = cv?.value;
|
|
228
|
+
if (value === undefined || value === null || value === '')
|
|
229
|
+
continue;
|
|
230
|
+
linked[fid] = String(value);
|
|
231
|
+
}
|
|
232
|
+
return linked;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 待办字段联动(主表):只以本次改动的字段作为触发,结果只补空值、不覆盖用户显式值。
|
|
237
|
+
* 与发起路径共用 resolveLinkage(token/cookie 均可);明细行触发的联动暂未覆盖。
|
|
238
|
+
*/
|
|
239
|
+
async function applyTodoLinkage(cfg, wfId, j1, p, sp, tableInfo, meta, mainValues, userChangedIds, browserParams) {
|
|
240
|
+
const triggers = {};
|
|
241
|
+
for (const rawId of userChangedIds) {
|
|
242
|
+
const fid = normalizeFieldId(rawId);
|
|
243
|
+
if (!/^\d+$/.test(fid))
|
|
244
|
+
continue;
|
|
245
|
+
const value = mainValues[fid];
|
|
246
|
+
if (value === undefined || value === null || value === '')
|
|
247
|
+
continue;
|
|
248
|
+
triggers[fid] = String(value);
|
|
249
|
+
}
|
|
250
|
+
if (Object.keys(triggers).length === 0)
|
|
251
|
+
return { overrides: {}, applied: [] };
|
|
252
|
+
const changes = await resolveLinkage(cfg, wfId, triggers, {
|
|
253
|
+
formData: { raw: j1, params: p, submitParams: sp },
|
|
254
|
+
});
|
|
255
|
+
const linked = pickLinkageTargets(changes, { mainValues, triggers, meta });
|
|
256
|
+
const applied = Object.keys(linked);
|
|
257
|
+
if (applied.length === 0)
|
|
258
|
+
return { overrides: {}, applied: [] };
|
|
259
|
+
// 联动结果也要做浏览框/选择框规范化(显示名→值、补 field<id>name)
|
|
260
|
+
const overrides = await normalizeMainValues(cfg, wfId, tableInfo, linked, browserParams);
|
|
261
|
+
Object.assign(mainValues, linked);
|
|
262
|
+
return { overrides, applied };
|
|
263
|
+
}
|
|
264
|
+
|
|
211
265
|
/** 明细改动行的选择框 / 浏览框规范化(显示名→ID,并补 name)。 */
|
|
212
266
|
async function normalizeDetailPlan(cfg, workflowId, fieldById, plan, browserParams) {
|
|
213
267
|
const optionCache = new Map();
|
|
@@ -597,6 +651,21 @@ export async function prepareTodoSubmission(cfg, opts) {
|
|
|
597
651
|
}
|
|
598
652
|
}
|
|
599
653
|
|
|
654
|
+
// 字段联动(主表):本次改动的字段作触发,结果只补空值、不覆盖用户显式值
|
|
655
|
+
const userChangedIds = new Set();
|
|
656
|
+
if (plan) {
|
|
657
|
+
for (const k of plan.changedMainKeys)
|
|
658
|
+
userChangedIds.add(k);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
for (const k of Object.keys(opts.fieldValues ?? {})) {
|
|
662
|
+
if (!isDetailValueKey(k, meta.fieldById))
|
|
663
|
+
userChangedIds.add(k);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const linkage = await applyTodoLinkage(cfg, wfId, j1, p, sp, tableInfo, meta, mainValues, userChangedIds, browserParams);
|
|
667
|
+
Object.assign(bodyOverrides, linkage.overrides);
|
|
668
|
+
|
|
600
669
|
// 公式字段
|
|
601
670
|
const formula = { applied: [], skipped: [] };
|
|
602
671
|
if (plan) {
|
|
@@ -639,6 +708,8 @@ export async function prepareTodoSubmission(cfg, opts) {
|
|
|
639
708
|
if (item.table === 'main')
|
|
640
709
|
changedMain.add(`field${String(item.dest).split('.').pop()}`);
|
|
641
710
|
}
|
|
711
|
+
for (const fid of linkage.applied)
|
|
712
|
+
changedMain.add(`field${fid}`);
|
|
642
713
|
|
|
643
714
|
return {
|
|
644
715
|
api, requestId, remarkText, remarkHtml, j0, j1, p, sp, wfId, nodeId,
|
package/dist/wfstart.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createApiFromProfile } from './client.js';
|
|
2
2
|
import { describeFormFields, HTMLTYPE_LABEL, VIEWATTR_LABEL, fieldNonEditableReason, getBrowserOptions, browserNameSeparator, normalizeSelectValue } from './wfform.js';
|
|
3
|
-
import { AuthTokenSession } from './auth-token.js';
|
|
4
3
|
import { findCreateWfByName } from './wfcreate.js';
|
|
5
4
|
import { uploadFile, fieldListType, uploadFieldValue } from './wfupload.js';
|
|
6
5
|
import { getCurrentUser, defaultForField } from './wfuser.js';
|
|
@@ -204,6 +203,7 @@ export async function getStartableFields(cfg, workflowId) {
|
|
|
204
203
|
submitParams: f.submitParams,
|
|
205
204
|
params: f.params,
|
|
206
205
|
linkageCfg: f.raw?.linkageCfg,
|
|
206
|
+
fromCache, // 命中表单缓存时 submitParams 可能含过期短期 token,真提交前需刷新
|
|
207
207
|
selfId, // 当前用户 ID(供下游缓存/联动用)
|
|
208
208
|
};
|
|
209
209
|
}
|
|
@@ -636,7 +636,42 @@ export function findNonEditableFields(form, values, details = []) {
|
|
|
636
636
|
return bad;
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
|
|
639
|
+
/**
|
|
640
|
+
* 附件目标的字段权限校验(发起路径)。
|
|
641
|
+
* 附件值在提交时才上传,values/details 的门禁覆盖不到;这里按 `field<id>[_row]`
|
|
642
|
+
* 解析出字段 id,用与手动填值相同的判据(viewattr + isonlyshow/isedit/isview)校验。
|
|
643
|
+
*/
|
|
644
|
+
export function findNonEditableFileTargets(form, files) {
|
|
645
|
+
const byId = new Map();
|
|
646
|
+
for (const t of form?.tables ?? [])
|
|
647
|
+
for (const fld of t.fields ?? [])
|
|
648
|
+
byId.set(String(fld.id), fld);
|
|
649
|
+
const bad = [];
|
|
650
|
+
for (const fl of files ?? []) {
|
|
651
|
+
const raw = String(fl?.fieldId ?? '').trim().replace(/^field/, '');
|
|
652
|
+
const m = raw.match(/^(\d+)(?:_\d+)?$/);
|
|
653
|
+
if (!m)
|
|
654
|
+
continue; // 格式错误交给上传阶段报
|
|
655
|
+
const id = m[1];
|
|
656
|
+
const fld = byId.get(id);
|
|
657
|
+
if (!fld)
|
|
658
|
+
continue; // 未知字段交给服务端判定
|
|
659
|
+
const reason = fieldNonEditableReason(fld.raw ?? fld);
|
|
660
|
+
if (reason)
|
|
661
|
+
bad.push({ id, label: fld.label, reason, currentValue: fld.currentValue });
|
|
662
|
+
}
|
|
663
|
+
return bad;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** 附件目标含禁止编辑/不可见字段时直接拒绝(与手动填值同一硬规则)。 */
|
|
667
|
+
export function assertFilesEditable(form, files) {
|
|
668
|
+
const bad = findNonEditableFileTargets(form, files);
|
|
669
|
+
if (bad.length) {
|
|
670
|
+
throw new Error(`拒绝向禁止编辑的字段上传附件:${bad.map((b) => `${b.label ?? b.id}(${b.reason})`).join('、')}。只允许向当前节点可编辑的附件字段上传。`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function prepareCreateSubmission(cfg, workflowId, values, requestname, details = [], formData, files = []) {
|
|
640
675
|
const f = formData || await describeFormFields(cfg, workflowId);
|
|
641
676
|
if (f.verifyRight === false)
|
|
642
677
|
throw new Error('无权限(verifyRight:false),跑 e9 auth token-login');
|
|
@@ -644,6 +679,7 @@ export async function prepareCreateSubmission(cfg, workflowId, values, requestna
|
|
|
644
679
|
const badFields = findNonEditableFields(f, values, details);
|
|
645
680
|
if (badFields.length)
|
|
646
681
|
throw new Error(`拒绝填写禁止编辑的字段:${badFields.map((b) => `${b.label ?? b.id}(${b.reason}${b.currentValue ? `,已有默认值 ${b.currentValue} 自动带出` : ''})`).join('、')}。表单只允许填写「可编辑」字段,不得绕过字段权限自行填值。`);
|
|
682
|
+
assertFilesEditable(f, files);
|
|
647
683
|
const name = requestname || `${f.workflowName ?? '流程'}-系统管理员-${todayStr()}`;
|
|
648
684
|
const merged = { ...values };
|
|
649
685
|
const main = f.tables.find((t) => t.key === 'main');
|
|
@@ -875,6 +911,38 @@ const browserNames = await resolveBrowserNames(cfg, workflowId, browserFields, m
|
|
|
875
911
|
return { workflowId: String(workflowId), workflowName: f.workflowName, requestName: name, values: merged, details, browserNames, codeblockConflicts, codeblock: codeblockStatus, formulas, body };
|
|
876
912
|
}
|
|
877
913
|
|
|
914
|
+
/**
|
|
915
|
+
* 真实提交前刷新 loadForm 下发的短期签名参数。
|
|
916
|
+
*
|
|
917
|
+
* 表单缓存(30 天)里含 submitParams,其中 `*_addrequest_submit_token` 有服务端时效;
|
|
918
|
+
* 缓存命中时直接复用可能触发 E9「重复提交」。这里只在 fromCache 时补一次 loadForm,
|
|
919
|
+
* 缓存未命中时 describeFormFields 刚取过,不重复请求。
|
|
920
|
+
*/
|
|
921
|
+
export async function refreshSubmitParams(cfg, workflowId, formData) {
|
|
922
|
+
if (!formData?.fromCache)
|
|
923
|
+
return formData;
|
|
924
|
+
try {
|
|
925
|
+
const api = await createApiFromProfile(cfg);
|
|
926
|
+
const r = await api.post('/api/workflow/reqform/loadForm', { iscreate: '1', workflowid: String(workflowId) });
|
|
927
|
+
const j = r.json ?? {};
|
|
928
|
+
// 空响应/非 2xx(如鉴权失效)不视为刷新成功,继续用原缓存参数
|
|
929
|
+
if (!r || r.status >= 400 || (!j.params && !j.submitParams))
|
|
930
|
+
return formData;
|
|
931
|
+
return {
|
|
932
|
+
...formData,
|
|
933
|
+
params: j.params ?? formData.params,
|
|
934
|
+
submitParams: j.submitParams ?? formData.submitParams,
|
|
935
|
+
raw: j.linkageCfg ? { ...(formData.raw ?? {}), linkageCfg: j.linkageCfg } : formData.raw,
|
|
936
|
+
fromCache: false,
|
|
937
|
+
submitParamsRefreshedAt: new Date().toISOString(),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
// 刷新失败沿用缓存参数,最终由服务端判定;不阻断提交
|
|
942
|
+
return formData;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
878
946
|
/**
|
|
879
947
|
* 真正提交(新建流程)。若传入 prepared,则只用确认阶段冻结的业务字段、明细行与浏览框名称,
|
|
880
948
|
* 仅刷新本次表单必需的短期签名参数,绝不重新补默认值或执行联动。
|
|
@@ -882,20 +950,24 @@ const browserNames = await resolveBrowserNames(cfg, workflowId, browserFields, m
|
|
|
882
950
|
export async function submitCreateRequest(cfg, workflowId, values, requestname, files = [], details = [], prepared = undefined, opts = {}) {
|
|
883
951
|
let plan = prepared;
|
|
884
952
|
if (!plan) {
|
|
885
|
-
plan = await prepareCreateSubmission(cfg, workflowId, values, requestname, details, opts.formData);
|
|
953
|
+
plan = await prepareCreateSubmission(cfg, workflowId, values, requestname, details, opts.formData, files);
|
|
886
954
|
}
|
|
887
955
|
else {
|
|
888
956
|
plan = { ...prepared, values: { ...prepared.values } };
|
|
889
957
|
}
|
|
890
958
|
// 文件 ID 只能在真实提交时获得;其余已确认字段绝不重新解析或联动。
|
|
891
959
|
// ★ 表单缓存:优先用调用方传入的 formData(避免重复 describeFormFields ~2.3s)
|
|
892
|
-
|
|
960
|
+
// 缓存命中时 submitParams 可能是旧的短期 token,真提交前刷新,避免服务端「重复提交」。
|
|
961
|
+
const f = await refreshSubmitParams(cfg, workflowId, opts.formData || await describeFormFields(cfg, workflowId));
|
|
893
962
|
if (f.verifyRight === false)
|
|
894
963
|
throw new Error('无权限(verifyRight:false),跑 e9 auth token-login');
|
|
895
964
|
// 附件字段的上传目录在 fieldinfomap.fileattr.docCategory(如 ",,2"),必须与表单字段一致
|
|
896
965
|
const allFields = f.tables.flatMap((t) => t.fields ?? []);
|
|
897
966
|
const categoryFor = (fieldId) => allFields.find((x) => String(x.id) === String(fieldId))?.raw?.fileattr?.docCategory || undefined;
|
|
898
|
-
|
|
967
|
+
const fileList = plan.files ?? files;
|
|
968
|
+
// 附件也是「填值」,同样受当前节点字段权限约束
|
|
969
|
+
assertFilesEditable(f, fileList);
|
|
970
|
+
for (const fl of fileList) {
|
|
899
971
|
// 支持主表 fieldId 与明细行 fieldId_<row>(如 27799_0)
|
|
900
972
|
const m = String(fl.fieldId).match(/^(\d+)(?:_(\d+))?$/);
|
|
901
973
|
const baseId = m ? m[1] : String(fl.fieldId);
|
|
@@ -1112,8 +1184,7 @@ export async function resolveLinkage(cfg, workflowId, values, opts = {}) {
|
|
|
1112
1184
|
return {};
|
|
1113
1185
|
const p = f.params ?? {};
|
|
1114
1186
|
const sp = f.submitParams ?? {};
|
|
1115
|
-
const
|
|
1116
|
-
const sess = AuthTokenSession.fromProfile(cfg);
|
|
1187
|
+
const api = await createApiFromProfile(cfg);
|
|
1117
1188
|
const selfId = opts.selfId || '';
|
|
1118
1189
|
const changes = {};
|
|
1119
1190
|
for (const [triggerFieldId, configs] of Object.entries(linkageCfg.dataInputCfg)) {
|
|
@@ -1160,13 +1231,9 @@ export async function resolveLinkage(cfg, workflowId, values, opts = {}) {
|
|
|
1160
1231
|
'f_weaver_belongto_userid': String(p.f_weaver_belongto_userid ?? '1'),
|
|
1161
1232
|
'f_weaver_belongto_usertype': String(p.f_weaver_belongto_usertype ?? '0'),
|
|
1162
1233
|
});
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
body: body.toString(),
|
|
1167
|
-
signal: AbortSignal.timeout(15000),
|
|
1168
|
-
});
|
|
1169
|
-
const j = JSON.parse(await r.text());
|
|
1234
|
+
// 走 E9Api:token 优先、cookie 回退(cookie-only 环境也能触发联动)
|
|
1235
|
+
const r = await api.post('/api/workflow/linkage/reqDataInputResult', Object.fromEntries(body));
|
|
1236
|
+
const j = r?.json ?? {};
|
|
1170
1237
|
const assignInfo = j[`assignInfo_${configId}`];
|
|
1171
1238
|
if (assignInfo?.changeValue) {
|
|
1172
1239
|
// ★ 写入缓存
|