weaver-work-cli 0.1.25 → 0.1.28
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 +35 -30
- package/dist/cmd/root.js +1 -0
- package/dist/cmd/setup/index.js +1 -1
- package/dist/core/runtime-agent.js +19 -0
- package/dist/internal/e10/auth/commands.js +48 -33
- package/dist/internal/e10/auth/messages.js +3 -0
- package/dist/internal/e10/auth/session.js +8 -0
- package/dist/internal/e10/auth/xiaoe.js +62 -43
- package/dist/internal/e10/auth-recovery.js +148 -0
- package/dist/internal/e10/client.js +47 -15
- package/dist/internal/e10/context.js +9 -31
- package/dist/internal/e10/index.js +1 -0
- package/dist/internal/e10/request-runtime.js +51 -9
- package/dist/shortcuts/archive/host.js +3 -2
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive-oa/constants.js +66 -0
- package/dist/shortcuts/archive-oa/continuation.js +70 -0
- package/dist/shortcuts/archive-oa/errors.js +53 -0
- package/dist/shortcuts/archive-oa/host.js +158 -0
- package/dist/shortcuts/archive-oa/index.js +225 -0
- package/dist/shortcuts/archive-oa/manifest.js +62 -0
- package/dist/shortcuts/archive-oa/operations/borrow-car.js +159 -0
- package/dist/shortcuts/archive-oa/operations/borrow-url.js +108 -0
- package/dist/shortcuts/archive-oa/operations/confirm.js +48 -0
- package/dist/shortcuts/archive-oa/operations/contract.js +120 -0
- package/dist/shortcuts/archive-oa/operations/locate.js +50 -0
- package/dist/shortcuts/archive-oa/operations/registry.js +40 -0
- package/dist/shortcuts/archive-oa/operations/shared.js +181 -0
- package/dist/shortcuts/archive-oa/operations/types.js +45 -0
- package/dist/shortcuts/archive-oa/operations.js +13 -0
- package/dist/shortcuts/asset/host.js +7 -6
- package/dist/shortcuts/blog/host.js +4 -3
- package/dist/shortcuts/caizhilian/host.js +3 -2
- package/dist/shortcuts/caizhilian/manifest.js +1 -1
- package/dist/shortcuts/calendar/host.js +2 -1
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +6 -3
- package/dist/shortcuts/fna/manifest.js +10 -5
- package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
- package/dist/shortcuts/fna/operations/registry.js +2 -0
- package/dist/shortcuts/hrm/host.js +2 -1
- package/dist/shortcuts/im/host.js +3 -2
- package/dist/shortcuts/index.js +2 -0
- package/dist/shortcuts/invoice/host.js +9 -7
- package/dist/shortcuts/invoice/manifest.js +38 -47
- package/dist/shortcuts/invoice/operations/issuing-qys.js +328 -0
- package/dist/shortcuts/invoice/operations/registry.js +2 -3
- package/dist/shortcuts/jiuchuanhui/host.js +2 -1
- package/dist/shortcuts/mail/host.js +2 -1
- package/dist/shortcuts/meeting/host.js +2 -1
- package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
- package/dist/shortcuts/okr/host.js +2 -1
- package/dist/shortcuts/plan/host.js +2 -1
- package/dist/shortcuts/project/host.js +2 -1
- package/dist/shortcuts/workflow/endpoints.js +11 -0
- package/dist/shortcuts/workflow/host.js +5 -7
- package/dist/shortcuts/workflow/index.js +6 -36
- package/dist/shortcuts/workflow/operations/chatscene.js +252 -0
- package/dist/shortcuts/workflow/operations/form.js +1047 -191
- package/dist/shortcuts/workflow/operations/read.js +137 -8
- package/dist/shortcuts/workflow/operations/sign-input.js +106 -0
- package/dist/shortcuts/workflow/operations/write.js +9 -1
- package/dist/shortcuts/workflow/stdout-compact.js +16 -1
- package/docs/SKILL.md +1 -1
- package/docs/_catalog.md +1 -1
- package/docs/agent-invoice.md +1 -1
- package/docs/archive-oa.md +79 -0
- package/docs/caizhilian.md +1 -1
- package/docs/e10-auth.md +7 -6
- package/docs/fna.md +7 -3
- package/docs/invoice.md +4 -6
- package/package.json +1 -1
- package/skill-template/business-info.json +20 -1
- package/skill-template/domains/archive-oa.md +19 -0
- package/skill-template/domains/caizhilian.md +2 -1
- package/skill-template/domains/qiyecheng.md +1 -0
- package/skill-template/domains/yepiaotong.md +8 -4
- package/skill-template/skill-template.md +1 -0
- package/skills/weaver-e10-archive-oa/SKILL.md +159 -0
- package/skills/weaver-e10-archive-oa/references/archive-locate.md +49 -0
- package/skills/weaver-e10-archive-oa/references/borrow-car.md +76 -0
- package/skills/weaver-e10-archive-oa/references/borrow-flow.md +53 -0
- package/skills/weaver-e10-archive-oa/references/contract-search.md +63 -0
- package/skills/weaver-e10-archive-oa/references/disabled-capabilities.md +29 -0
- package/skills/weaver-e10-archive-oa/references/qys-contract-search.md +47 -0
- package/skills/weaver-e10-archive-oa/references/source-manifest.json +400 -0
- package/skills/weaver-e10-blog/SKILL.md +6 -1
- package/skills/weaver-e10-blog/references/blog-query.md +3 -1
- package/skills/weaver-e10-builder/SKILL.md +22 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +47 -16
- package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
- package/skills/weaver-e10-caizhilian/references/agent-entry.md +12 -4
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
- package/skills/weaver-e10-caizhilian/references/collaboration.md +5 -3
- package/skills/weaver-e10-caizhilian/references/document.md +33 -11
- package/skills/weaver-e10-caizhilian/references/file-collection.md +57 -54
- package/skills/weaver-e10-caizhilian/references/file-upload.md +5 -6
- package/skills/weaver-e10-caizhilian/references/files.md +9 -3
- package/skills/weaver-e10-caizhilian/references/folder.md +48 -16
- package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
- package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
- package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +17 -54
- package/skills/weaver-e10-caizhilian/references/setting-backup.md +16 -63
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +24 -64
- package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage.md +143 -548
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
- package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
- package/skills/weaver-e10-caizhilian/references/withheld.md +21 -45
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
- package/skills/weaver-e10-calendar/SKILL.md +6 -1
- package/skills/weaver-e10-esb/SKILL.md +8 -0
- package/skills/weaver-e10-esb/references/input-format.md +3 -1
- package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +6 -1
- package/skills/weaver-e10-hrm/references/employee.md +1 -1
- package/skills/weaver-e10-hrm/references/org.md +1 -1
- package/skills/weaver-e10-hrm/references/position.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/SKILL.md +6 -1
- package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
- package/skills/weaver-e10-jucailin/references/salary.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +6 -1
- package/skills/weaver-e10-meeting/SKILL.md +11 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
- package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
- package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
- package/skills/weaver-e10-okr/SKILL.md +6 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
- package/skills/weaver-e10-plan/SKILL.md +6 -1
- package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
- package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
- package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
- package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
- package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
- package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
- package/skills/weaver-e10-shared/SKILL.md +53 -28
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +22 -20
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +13 -13
- package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-workflow/references/source-manifest.json +33 -24
- package/skills/weaver-e10-workflow/references/workflow-create.md +41 -26
- package/skills/weaver-e10-yepiaotong/SKILL.md +23 -18
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +87 -45
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +152 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +210 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +59 -36
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +63 -512
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +205 -346
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -5
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +231 -344
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +5 -5
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +201 -162
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +121 -145
- package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
- package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
- package/dist/shortcuts/invoice/operations/issuing.js +0 -198
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +0 -107
- package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +0 -119
|
@@ -20,11 +20,13 @@ import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CONTINUATION, SCHE
|
|
|
20
20
|
import { asBool, responseData, toArray, withLeadingMeta, stdoutBytesOf, stdoutCharsOf, STDOUT_SAFE_BYTES, parseNameWithId, resolveMemoryAppId, } from './shared.js';
|
|
21
21
|
import { workflowValidationError, workflowApiError, workflowPartialError, workflowPolicyError, workflowClarificationError, WorkflowCliError, } from '../errors.js';
|
|
22
22
|
import { FORM_ENDPOINTS, WORKFLOW_ENDPOINTS, AJAX_QUERY, formApiUrl } from '../endpoints.js';
|
|
23
|
+
import { resolveSignRemark } from './sign-input.js';
|
|
23
24
|
import { compactStdoutPaths } from '../stdout-compact.js';
|
|
24
25
|
import { continuationReplayError } from '../continuation.js';
|
|
25
26
|
import { writeStateFileBestEffort } from '../state.js';
|
|
26
|
-
import { buildHistoryRequestData, fetchHistoryListSnapshot, filterHistoryRequestData } from './read.js';
|
|
27
|
+
import { buildHistoryRequestData, fetchHistoryListSnapshot, filterHistoryRequestData, pickUniqueMineWorkflow } from './read.js';
|
|
27
28
|
import { hasXiaoEEnv } from '../../../internal/e10/auth/xiaoe.js';
|
|
29
|
+
import { buildChatSceneInput, formatLocalNow, requestFormFill } from './chatscene.js';
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// 常量(与 Python 源逐条对齐)
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
@@ -1217,6 +1219,86 @@ function collectHiddenMultiContentFieldIds(comps) {
|
|
|
1217
1219
|
}
|
|
1218
1220
|
return hiddenFieldIds;
|
|
1219
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* 页面根网格(Sheet1)上**被隐藏的单元格**里的字段(2026-10-20 修复)。
|
|
1224
|
+
*
|
|
1225
|
+
* `pageLayouts[0].config.workbook.Sheet1.cells[]` 里 `cell.tag.attrs.hide === true`
|
|
1226
|
+
* ⇒ 该单元格(`col:row`)在表单上不显示,格子里的字段用户看不到。
|
|
1227
|
+
*
|
|
1228
|
+
* 寻址必须**沿容器链往上走**(与隐藏行 / 隐藏列 / 多内容格同一套):组件的 `config.excel.col`/`row`
|
|
1229
|
+
* 是**相对其父容器**的坐标,只有 `excel.parent === 'ROOT'` 那一层才是 Sheet1 的行列号。
|
|
1230
|
+
* 真机反例(总部费用报销,「报销人直接上级」`fieldId 100003720000076596`):字段自身的坐标是
|
|
1231
|
+
* `col:0,row:0`(相对它所在的 `MultiContent` 容器),真正被藏起来的是 Sheet1 上 `col:3,row:4`
|
|
1232
|
+
* 那个格子里的**容器本体**——只比字段自己的坐标就永远不可能命中,值于是漏进「已填内容」。
|
|
1233
|
+
*
|
|
1234
|
+
* 之前这里的实现只拿 comp 自己的 `excel.col:row` 与 Sheet1 的隐藏格直接比对,组合控件(尤其是
|
|
1235
|
+
* `MultiContent`)内部的字段一概漏判;改成沿链上溯后,容器被藏 ⇒ 它内部所有字段一并判隐藏。
|
|
1236
|
+
*
|
|
1237
|
+
* 查不到 `cells` / 没有隐藏格时**不判隐藏**——fail-open,与其它隐藏口径同一降级原则。
|
|
1238
|
+
*/
|
|
1239
|
+
function collectHiddenSheetCellFieldIds(page0, comps) {
|
|
1240
|
+
const hiddenFieldIds = new Set();
|
|
1241
|
+
const sheet1 = page0?.config?.workbook?.Sheet1;
|
|
1242
|
+
const cells = sheet1 && Array.isArray(sheet1.cells) ? sheet1.cells : [];
|
|
1243
|
+
if (!cells.length)
|
|
1244
|
+
return hiddenFieldIds;
|
|
1245
|
+
/** `col:row`(两边同口径归一成数字,避免 `"3"` 与 `3` 对不上);坐标不可解析时返回 null。 */
|
|
1246
|
+
const posOf = (col, row) => {
|
|
1247
|
+
const c = Number(col);
|
|
1248
|
+
const r = Number(row);
|
|
1249
|
+
if (!Number.isFinite(c) || !Number.isFinite(r))
|
|
1250
|
+
return null;
|
|
1251
|
+
return `${c}:${r}`;
|
|
1252
|
+
};
|
|
1253
|
+
const hidePositions = new Set();
|
|
1254
|
+
for (const cell of cells) {
|
|
1255
|
+
if (!cell || typeof cell !== 'object')
|
|
1256
|
+
continue;
|
|
1257
|
+
const attrs = cell.tag?.attrs;
|
|
1258
|
+
if (!attrs || typeof attrs !== 'object' || !attrs.hide)
|
|
1259
|
+
continue;
|
|
1260
|
+
const pos = posOf(cell.col, cell.row);
|
|
1261
|
+
if (pos)
|
|
1262
|
+
hidePositions.add(pos);
|
|
1263
|
+
}
|
|
1264
|
+
if (!hidePositions.size)
|
|
1265
|
+
return hiddenFieldIds;
|
|
1266
|
+
// nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能找回父容器自己的坐标。
|
|
1267
|
+
const compByNodeId = new Map();
|
|
1268
|
+
for (const comp of toArray(comps)) {
|
|
1269
|
+
if (!comp || typeof comp !== 'object')
|
|
1270
|
+
continue;
|
|
1271
|
+
const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
|
|
1272
|
+
if (nodeId)
|
|
1273
|
+
compByNodeId.set(nodeId, comp);
|
|
1274
|
+
}
|
|
1275
|
+
for (const comp of toArray(comps)) {
|
|
1276
|
+
if (!comp || typeof comp !== 'object')
|
|
1277
|
+
continue;
|
|
1278
|
+
const fieldId = String(comp.config?.fieldId ?? '').trim();
|
|
1279
|
+
if (!fieldId)
|
|
1280
|
+
continue;
|
|
1281
|
+
let cursor = comp;
|
|
1282
|
+
while (cursor) {
|
|
1283
|
+
const excel = cursor.config?.excel;
|
|
1284
|
+
if (!excel || typeof excel !== 'object')
|
|
1285
|
+
break;
|
|
1286
|
+
const parentId = String(excel.parent ?? '').trim();
|
|
1287
|
+
if (!parentId || parentId === 'ROOT') {
|
|
1288
|
+
// 到根网格这一层才是 Sheet1 的真实行列号(容器内的坐标是相对的,比不得)。
|
|
1289
|
+
const pos = posOf(excel.col, excel.row);
|
|
1290
|
+
if (pos && hidePositions.has(pos))
|
|
1291
|
+
hiddenFieldIds.add(fieldId);
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
const owner = compByNodeId.get(parentId);
|
|
1295
|
+
if (!owner)
|
|
1296
|
+
break;
|
|
1297
|
+
cursor = owner;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return hiddenFieldIds;
|
|
1301
|
+
}
|
|
1220
1302
|
function parseLayoutMeta(layoutData) {
|
|
1221
1303
|
const hiddenCellIds = new Set();
|
|
1222
1304
|
const compsFieldIds = new Set();
|
|
@@ -1226,44 +1308,6 @@ function parseLayoutMeta(layoutData) {
|
|
|
1226
1308
|
if (Array.isArray(pageLayouts) && pageLayouts.length)
|
|
1227
1309
|
page0 = pageLayouts[0];
|
|
1228
1310
|
if (page0 && typeof page0 === 'object') {
|
|
1229
|
-
const wb = page0.config && page0.config.workbook ? page0.config.workbook : {};
|
|
1230
|
-
const sheet1 = wb.Sheet1;
|
|
1231
|
-
const cells = sheet1 ? sheet1.cells : null;
|
|
1232
|
-
const hidePositions = new Set();
|
|
1233
|
-
if (Array.isArray(cells)) {
|
|
1234
|
-
for (const cell of cells) {
|
|
1235
|
-
if (!cell || typeof cell !== 'object')
|
|
1236
|
-
continue;
|
|
1237
|
-
const attrs = cell.tag && cell.tag.attrs ? cell.tag.attrs : {};
|
|
1238
|
-
if (attrs.hide) {
|
|
1239
|
-
try {
|
|
1240
|
-
hidePositions.add(`${cell.col}:${cell.row}`);
|
|
1241
|
-
}
|
|
1242
|
-
catch {
|
|
1243
|
-
/* ignore */
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
if (hidePositions.size) {
|
|
1249
|
-
for (const comp of page0.comps || []) {
|
|
1250
|
-
if (!comp || typeof comp !== 'object')
|
|
1251
|
-
continue;
|
|
1252
|
-
const cfg = comp.config || {};
|
|
1253
|
-
const fid = String(cfg.fieldId ?? '').trim();
|
|
1254
|
-
if (!fid)
|
|
1255
|
-
continue;
|
|
1256
|
-
const excel = cfg.excel || {};
|
|
1257
|
-
try {
|
|
1258
|
-
const pos = `${excel.col}:${excel.row}`;
|
|
1259
|
-
if (hidePositions.has(pos))
|
|
1260
|
-
hiddenCellIds.add(fid);
|
|
1261
|
-
}
|
|
1262
|
-
catch {
|
|
1263
|
-
/* ignore */
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
1311
|
for (const comp of page0.comps || []) {
|
|
1268
1312
|
if (!comp || typeof comp !== 'object')
|
|
1269
1313
|
continue;
|
|
@@ -1280,6 +1324,9 @@ function parseLayoutMeta(layoutData) {
|
|
|
1280
1324
|
// 行隐藏 / 单元格隐藏 / 明细表隐藏列 / 多内容单元格隐藏**同义**(都是"用户在表单上看不到"),
|
|
1281
1325
|
// 并成一份 `hiddenCellIds`:下游 `visibleFields`/`initialValue`/`missingRequiredFields`/「已填内容」
|
|
1282
1326
|
// /联动提示全部只认这一份,且随句柄以 `hiddenCellFieldIds` 传给 `createRequest`,两边口径天然一致。
|
|
1327
|
+
// 单元格隐藏**必须沿容器链上溯**(2026-10-20 修复):容器的 `excel` 坐标才是 Sheet1 上的位置。
|
|
1328
|
+
for (const fid of collectHiddenSheetCellFieldIds(page0, page0.comps))
|
|
1329
|
+
hiddenCellIds.add(fid);
|
|
1283
1330
|
for (const fid of collectHiddenRowFieldIds(page0, page0.comps))
|
|
1284
1331
|
hiddenCellIds.add(fid);
|
|
1285
1332
|
for (const fid of collectHiddenDetailColumnFieldIds(page0, page0.comps))
|
|
@@ -1349,17 +1396,29 @@ function requiredInfoWarningText(context) {
|
|
|
1349
1396
|
// 发起侧工作流解析(原 createList 的职责;候选池 = 当前用户「可发起的工作流」)
|
|
1350
1397
|
// ---------------------------------------------------------------------------
|
|
1351
1398
|
/**
|
|
1352
|
-
*
|
|
1399
|
+
* 名称关键词的**填充词**(按长词优先依次脱壳)。
|
|
1400
|
+
*
|
|
1401
|
+
* 用户裁定:「去掉一些词:工作流、流程」(2026-09-23);「现在要**额外去掉申请**关键字」
|
|
1402
|
+
* (2026-10-21)。顺序有讲究:`工作流程` 必须排在 `工作流` / `流程` 之前
|
|
1403
|
+
* (`工作流` 与 `流程` 在 `流` 处重叠,先剥短的会漏判)。
|
|
1404
|
+
*/
|
|
1405
|
+
const CREATE_KEYWORD_FILLERS = ['工作流程', '工作流', '流程', '申请'];
|
|
1406
|
+
/**
|
|
1407
|
+
* 把一个「纯名称」切成多个名称关键词(2026-09-23 用户裁定 + 2026-10-21 追加净词)。
|
|
1353
1408
|
*
|
|
1354
1409
|
* 单个格内可用逗号分隔多个关键词(中英文逗号都认),也可用数组多元素表达;命中结果做**并集**,
|
|
1355
|
-
* 目的是**提高候选召回** ——
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1410
|
+
* 目的是**提高候选召回** —— 用户首次要发起某个流程时,多给几个说法更容易命中他真正要发起的那个。
|
|
1411
|
+
*
|
|
1412
|
+
* 每个关键词带**两副面孔**(2026-10-21 用户裁定):
|
|
1413
|
+
* - `raw` = 原样(`请假流程`、`请假申请`);
|
|
1414
|
+
* - `core` = 去掉填充词后的净词(`请假流程` → `请假`、`请假申请` → `请假`、`总部请假` → `总部请假`)。
|
|
1415
|
+
*
|
|
1416
|
+
* 两者**都参与本地模糊过滤**(候选取并集),于是「请假申请」既按它自己、也按「请假」去找,
|
|
1417
|
+
* 能命中「请假申请单」这类名字;净词与原词相同的词天然只算一次。
|
|
1418
|
+
*
|
|
1419
|
+
* **只丢弃"整格全是填充词"的关键词**(`流程` / `工作流` / `申请` …):它们不代表任何业务含义,
|
|
1420
|
+
* 留着会命中一大批无关流程。`报销申请` 这类**含填充词但本身有效**的词不会被丢 ——
|
|
1421
|
+
* 它的净词 `报销` 才是干活的那个。
|
|
1363
1422
|
*/
|
|
1364
1423
|
function splitCreateKeywords(raw) {
|
|
1365
1424
|
const out = [];
|
|
@@ -1368,15 +1427,18 @@ function splitCreateKeywords(raw) {
|
|
|
1368
1427
|
const token = piece.trim();
|
|
1369
1428
|
if (!token)
|
|
1370
1429
|
continue;
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1430
|
+
let core = token;
|
|
1431
|
+
for (const filler of CREATE_KEYWORD_FILLERS)
|
|
1432
|
+
core = core.split(filler).join('');
|
|
1433
|
+
core = core.trim();
|
|
1434
|
+
// 整格都是填充词 ⇒ 丢弃(留着会让 `流程` 命中「请假流程」「合同流程」等一大批)
|
|
1435
|
+
if (!core)
|
|
1374
1436
|
continue;
|
|
1375
1437
|
const key = token.toLowerCase();
|
|
1376
1438
|
if (seen.has(key))
|
|
1377
1439
|
continue;
|
|
1378
1440
|
seen.add(key);
|
|
1379
|
-
out.push(token);
|
|
1441
|
+
out.push({ raw: token, core });
|
|
1380
1442
|
}
|
|
1381
1443
|
return out;
|
|
1382
1444
|
}
|
|
@@ -1390,10 +1452,17 @@ function splitCreateKeywords(raw) {
|
|
|
1390
1452
|
* - **0 命中 → 澄清**:不能把「没听懂你的条件」伪装成「没有这条流程」,否则模型会如实转述成
|
|
1391
1453
|
* "没有该工作流",用户拿不到可操作的信息;
|
|
1392
1454
|
* - 1 命中 → 直接用;
|
|
1393
|
-
* -
|
|
1394
|
-
*
|
|
1455
|
+
* - 多命中时,**全名一致必须唯一**才直接用(2026-10-21 用户二次裁定,撤销同日"按词序先到先得"):
|
|
1456
|
+
* 连"净词 + 原词"一起算,若与候选全名一致的说法只指向**同一个**候选 ⇒ 用它;若指向**多个不同**
|
|
1457
|
+
* 候选(如 `费用报销,报销流程审批` 恰好各自对上一条)⇒ 这是用户给的条件本身有歧义,仍回来澄清;
|
|
1458
|
+
* - 多命中且没有任何全名一致的 → 澄清并列出候选(`display` 即 `名称(ID)`,用户选定后原样回传)。
|
|
1459
|
+
* 这一档在澄清**之前**先做一次消歧(2026-10-20 用户裁定):把候选拿去查「我发起的」工作流树,
|
|
1460
|
+
* 用户近一年只发起过其中**一个**就直接用它(`from:'mineRecentUnique'`);否则/失败则照旧澄清。
|
|
1461
|
+
* **只有这一档会消歧**:0 命中、以及"只给了填充词"那两档都不查(前者本就没候选可比,
|
|
1462
|
+
* 后者在发任何远端请求前就澄清了)。
|
|
1395
1463
|
*
|
|
1396
|
-
* 名称格支持**多关键词**(`"报销,报销流程"`,见 `splitCreateKeywords
|
|
1464
|
+
* 名称格支持**多关键词**(`"报销,报销流程"`,见 `splitCreateKeywords`):每个关键词同时贡献
|
|
1465
|
+
* **净词**与**原词**(`请假申请` → `请假` + `请假申请`),两组词各自命中后**并集去重**,
|
|
1397
1466
|
* 再走上面同一套判据。**只发一次** `getNewFlowListData` 而不是每个关键词并行各发一次 —— 该接口
|
|
1398
1467
|
* 请求体固定为 `{wfpBelongTypeCode, loadOfsFlow, clientCtrl}`、**没有任何关键词入参**(V11.3 Python
|
|
1399
1468
|
* 基线 `e10_workflow_create.py:395-436` 同样是取全量后本地过滤;真机单笔 ≈1,649 ms / 228,929 B),
|
|
@@ -1416,8 +1485,17 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
|
|
|
1416
1485
|
// 一个关键词都没解析出来(如只传了 `流程`)时**连可发起列表都不去拉**
|
|
1417
1486
|
// (该接口真机单笔 ≈1,649 ms / 228,929 B),与"完全不传 workflow"同一口径。
|
|
1418
1487
|
const explicitIds = new Set();
|
|
1419
|
-
|
|
1420
|
-
const
|
|
1488
|
+
/** 参与**本地模糊过滤**的词:每个关键词先净词、再原词(同一词只留一个)。 */
|
|
1489
|
+
const filterTerms = [];
|
|
1490
|
+
/**
|
|
1491
|
+
* 参与**全名精确匹配**的词集合:每个关键词先净词、再原词(如 `请假申请` → `请假` + `请假申请`)。
|
|
1492
|
+
*
|
|
1493
|
+
* 只做集合比对、**不看先后次序**(2026-10-21 二次裁定:全名一致必须唯一才直接用,不允许按词序
|
|
1494
|
+
* 先到先得)。
|
|
1495
|
+
*/
|
|
1496
|
+
const exactTerms = [];
|
|
1497
|
+
const filterSeen = new Set();
|
|
1498
|
+
const exactSeen = new Set();
|
|
1421
1499
|
for (const term of rawTerms) {
|
|
1422
1500
|
const parsed = parseNameWithId(term);
|
|
1423
1501
|
// 纯数字同样按 ID 处理(`parseNameWithId` 只认 "名称(数字)" 形态)
|
|
@@ -1427,14 +1505,29 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
|
|
|
1427
1505
|
continue;
|
|
1428
1506
|
}
|
|
1429
1507
|
for (const keyword of splitCreateKeywords(parsed.name)) {
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1508
|
+
const coreKey = keyword.core.toLowerCase();
|
|
1509
|
+
const rawKey = keyword.raw.toLowerCase();
|
|
1510
|
+
// 模糊过滤:**单字净词不参与**(`申请单` 剥成 `单` 会把整张清单拉进来);
|
|
1511
|
+
// 原词永远参与,所以"只说得出一个怪名字"时召回不受影响。
|
|
1512
|
+
if (keyword.core.length >= 2 && !filterSeen.has(coreKey)) {
|
|
1513
|
+
filterSeen.add(coreKey);
|
|
1514
|
+
filterTerms.push(keyword.core);
|
|
1515
|
+
}
|
|
1516
|
+
if (!filterSeen.has(rawKey)) {
|
|
1517
|
+
filterSeen.add(rawKey);
|
|
1518
|
+
filterTerms.push(keyword.raw);
|
|
1519
|
+
}
|
|
1520
|
+
if (!exactSeen.has(coreKey)) {
|
|
1521
|
+
exactSeen.add(coreKey);
|
|
1522
|
+
exactTerms.push(keyword.core);
|
|
1523
|
+
}
|
|
1524
|
+
if (!exactSeen.has(rawKey)) {
|
|
1525
|
+
exactSeen.add(rawKey);
|
|
1526
|
+
exactTerms.push(keyword.raw);
|
|
1527
|
+
}
|
|
1435
1528
|
}
|
|
1436
1529
|
}
|
|
1437
|
-
if (!explicitIds.size && !
|
|
1530
|
+
if (!explicitIds.size && !filterTerms.length) {
|
|
1438
1531
|
throw workflowClarificationError('workflow', queryText, '要发起哪个流程?请提供该流程的完整名称(或其它可用于检索的关键词)后再继续。');
|
|
1439
1532
|
}
|
|
1440
1533
|
const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, {
|
|
@@ -1455,7 +1548,7 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
|
|
|
1455
1548
|
}
|
|
1456
1549
|
}
|
|
1457
1550
|
// 名称关键词:逐个取命中,**并集去重**(多给几个说法只扩大候选面,不改变下面的命中判据)
|
|
1458
|
-
for (const keyword of
|
|
1551
|
+
for (const keyword of filterTerms) {
|
|
1459
1552
|
for (const item of simplifyWorkflows(data, keyword)) {
|
|
1460
1553
|
if (seen.has(String(item.id)))
|
|
1461
1554
|
continue;
|
|
@@ -1470,13 +1563,30 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
|
|
|
1470
1563
|
return { id, name: String(only.workflowname ?? ''), source: explicitIds.has(id) ? 'workflowId' : 'single' };
|
|
1471
1564
|
}
|
|
1472
1565
|
if (picked.length > 1) {
|
|
1473
|
-
|
|
1566
|
+
// ★ 2026-10-21 用户二次裁定:**退回「全名命中必须唯一」**——同日早先那版"按词序循环、第一个
|
|
1567
|
+
// 全名一致的就用"已撤销。理由:多个说法各自全名命中**不同**候选时,用户给的条件本身就有歧义
|
|
1568
|
+
// (两条互相竞争的精确证据),按词序替用户先到先得等于擅自替他裁决,宁可回来问一句。
|
|
1569
|
+
//
|
|
1570
|
+
// 仍算"唯一"、可直接用的两档:
|
|
1571
|
+
// - 只有一个候选与某个说法全名一致,其余说法只是陪跑(命中的是别的、或没命中);
|
|
1572
|
+
// - 多个说法全名命中的是**同一个**候选(去重后仍是一条)。
|
|
1573
|
+
// "说法"包含每个关键词的**净词**与**原词**(见 `exactTerms`),两者都参与比对。
|
|
1574
|
+
//
|
|
1575
|
+
// 某个说法命中**多条同名候选**(不同 id、名字一字不差)同样算不唯一 ⇒ 一并澄清。
|
|
1576
|
+
const wanted = new Set(exactTerms.map((t) => t.trim().toLowerCase()).filter(Boolean));
|
|
1474
1577
|
const exact = picked.filter((item) => wanted.has(String(item.workflowname ?? '').trim().toLowerCase()));
|
|
1475
|
-
// 多命中时有唯一全名精确匹配 → 直接选中(与查询侧 exact_candidates 优选同一判据)
|
|
1476
1578
|
if (exact.length === 1) {
|
|
1477
1579
|
return { id: String(exact[0].id), name: String(exact[0].workflowname ?? ''), source: 'exactName' };
|
|
1478
1580
|
}
|
|
1581
|
+
// 澄清候选:能对上的那几条优先(比全量 picked 精确),一条都对不上才退回全量。
|
|
1479
1582
|
const list = exact.length ? exact : picked;
|
|
1583
|
+
// ★ 2026-10-20 用户裁定:澄清前先按「用户近一年实际发起过哪些」消歧。
|
|
1584
|
+
// 用户只发起过其中某一个 ⇒ 直接用,不再问一遍(`from:mineRecentUnique` 标明这是推断的,
|
|
1585
|
+
// 不是用户明说的)。查不到 / 不唯一 / 请求失败 ⇒ 一律回落下面原样的多候选澄清。
|
|
1586
|
+
const inferred = await pickUniqueMineWorkflow(list.map((item) => ({ id: String(item.id), name: String(item.workflowname ?? '') })), env);
|
|
1587
|
+
if (inferred) {
|
|
1588
|
+
return { id: inferred.id, name: inferred.name, source: 'single', from: 'mineRecentUnique' };
|
|
1589
|
+
}
|
|
1480
1590
|
// `clarificationQuestion` 是**用户口径**:它会被 `note` 要求「原样交给用户确认」,因此
|
|
1481
1591
|
// 只能写"发生了什么 + 要用户做什么"。**禁止**把本机判定依据(如"其中没有与所写名称完全
|
|
1482
1592
|
// 一致的")写进这句——那属于 CLI 内部实现,外传会让用户在确认卡片上看到与己无关的判据
|
|
@@ -1496,9 +1606,9 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
|
|
|
1496
1606
|
*
|
|
1497
1607
|
* 背景(2026-09-23 实测 + 用户裁定):`POST …memoryPrompt` 带 `workflowId` 时返回的是**全集** ——
|
|
1498
1608
|
* `--- user.md ---` / `--- memory.md ---`(全局画像 + 全局别名映射)+ `--- memory_<workflowId>.md ---`
|
|
1499
|
-
*
|
|
1609
|
+
* (该工作流维度)。于是这条命令取回的记忆与入口处读过的无参 `workflow.memoryPrompt`(纯全局)
|
|
1500
1610
|
* **内容重叠**:真机实测同一租户 `promptText` 2,467 字符 = 全局 543 + 工作流维度 1,924,
|
|
1501
|
-
*
|
|
1611
|
+
* 同一段全局记忆会在上下文里出现两次。而这里要的是「**该工作流的**发起偏好」,
|
|
1502
1612
|
* 全局那份已由入口读取覆盖,不应重复下发。
|
|
1503
1613
|
*
|
|
1504
1614
|
* 故只保留标记 `--- memory_<workflowId>.md ---` 及其**之后**的内容:
|
|
@@ -1521,16 +1631,17 @@ function extractWorkflowMemorySection(promptText, workflowId) {
|
|
|
1521
1631
|
return text.slice(at).trim();
|
|
1522
1632
|
}
|
|
1523
1633
|
/**
|
|
1524
|
-
*
|
|
1634
|
+
* 取回用户在这条工作流上的记忆(`buildAgentMemoryPrompt`,带 `workflowId`)。
|
|
1525
1635
|
*
|
|
1526
|
-
*
|
|
1527
|
-
* 覆盖,这里经 `extractWorkflowMemorySection`
|
|
1636
|
+
* 返回值**只含该工作流维度的那一段**(全局画像 / 全局别名由入口的无参
|
|
1637
|
+
* `workflow.memoryPrompt` 覆盖,这里经 `extractWorkflowMemorySection` 裁掉,
|
|
1638
|
+
* 避免同一段全局记忆在上下文里出现两次)。
|
|
1528
1639
|
*
|
|
1529
|
-
* best-effort
|
|
1530
|
-
*
|
|
1640
|
+
* best-effort:读不到(没开通记忆 / 404 / 接口故障)一律返回空串 —— 对模型而言
|
|
1641
|
+
* 「没有记忆」和「读取失败」的处理完全一样(按新用户处理、不要重试),不需要再分档。
|
|
1642
|
+
* 绝不阻断表单载入(与 `workflow.memoryPrompt` 同口径)。
|
|
1531
1643
|
*/
|
|
1532
|
-
async function
|
|
1533
|
-
const result = { available: false, workflowId, promptText: '' };
|
|
1644
|
+
async function fetchWorkflowMemory(workflowId, env) {
|
|
1534
1645
|
try {
|
|
1535
1646
|
const response = await env.host.workflowRequest({
|
|
1536
1647
|
method: 'POST',
|
|
@@ -1540,38 +1651,21 @@ async function fetchLaunchPreferences(workflowId, env) {
|
|
|
1540
1651
|
});
|
|
1541
1652
|
// 该接口原始返回是 {code,msg,status,data:"<markdown>",fail},业务成功标志是 status/fail
|
|
1542
1653
|
// **不是 success**——这里**不能**用 responseData 解包(会把 data 里的 markdown 当主体,
|
|
1543
|
-
//
|
|
1544
|
-
// 只认 `data
|
|
1545
|
-
// 通用提示,回退它会让"没配记忆"的环境被读成有一条内容是"接口返回成功"
|
|
1654
|
+
// 导致正文恒空)。与 workflow.memoryPrompt 同源。
|
|
1655
|
+
// 只认 `data`(字符串)作为正文:**不回退 `msg`**——`msg` 里是「接口返回成功」这类
|
|
1656
|
+
// 通用提示,回退它会让"没配记忆"的环境被读成有一条内容是"接口返回成功"的记忆。
|
|
1546
1657
|
const body = (response && typeof response === 'object' ? response : {});
|
|
1547
|
-
const rawPromptText = typeof body.data === 'string' ? body.data.trim() : '';
|
|
1548
|
-
const promptText = extractWorkflowMemorySection(rawPromptText, workflowId);
|
|
1549
1658
|
const businessOk = body.fail !== true && body.status !== false && body.success !== false;
|
|
1550
|
-
|
|
1551
|
-
|
|
1659
|
+
if (!businessOk)
|
|
1660
|
+
return '';
|
|
1661
|
+
const rawPromptText = typeof body.data === 'string' ? body.data.trim() : '';
|
|
1662
|
+
return extractWorkflowMemorySection(rawPromptText, workflowId);
|
|
1552
1663
|
}
|
|
1553
|
-
catch
|
|
1554
|
-
// 404 =
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1557
|
-
if (!isNotFoundError(error)) {
|
|
1558
|
-
result.note = '该工作流的记忆偏好读取失败,按新用户处理,不要重试。';
|
|
1559
|
-
}
|
|
1664
|
+
catch {
|
|
1665
|
+
// 404 = 该工作流还没有记忆条目(未开通记忆)、其它错误 = 接口不可用;
|
|
1666
|
+
// 两者都按「没有记忆」处理,既不加提示也不阻断任务。
|
|
1667
|
+
return '';
|
|
1560
1668
|
}
|
|
1561
|
-
return result;
|
|
1562
|
-
}
|
|
1563
|
-
/**
|
|
1564
|
-
* 是否是「资源不存在」类错误(HTTP 404)。
|
|
1565
|
-
*
|
|
1566
|
-
* 真机上该错误由 host 统一抛成 `workflowApiError('http_error', 'OA 接口 HTTP 404', {code:404, details:{status:404}})`,
|
|
1567
|
-
* 故同时认 `code` 与 `details.status` 两处。不依赖类实例判断,避免把跨包包装过的错误漏掉。
|
|
1568
|
-
*/
|
|
1569
|
-
function isNotFoundError(error) {
|
|
1570
|
-
const record = (error && typeof error === 'object') ? error : {};
|
|
1571
|
-
if (Number(record.code) === 404)
|
|
1572
|
-
return true;
|
|
1573
|
-
const details = record.details;
|
|
1574
|
-
return Boolean(details && typeof details === 'object' && Number(details.status) === 404);
|
|
1575
1669
|
}
|
|
1576
1670
|
// ---------------------------------------------------------------------------
|
|
1577
1671
|
// createForm:解析工作流 + loadBase/loadFrame/loadMinor/formLayout + 字段输出
|
|
@@ -1643,23 +1737,105 @@ function singleExplicitWorkflowId(rawWorkflow) {
|
|
|
1643
1737
|
const parsed = parseNameWithId(terms[0]);
|
|
1644
1738
|
return parsed.id ?? (/^[0-9]+$/.test(terms[0]) ? terms[0] : '');
|
|
1645
1739
|
}
|
|
1646
|
-
/** 表单载入 +
|
|
1740
|
+
/** 表单载入 + 该工作流的记忆:两者只共享 `workflowId`,可以并行。 */
|
|
1647
1741
|
async function loadCreateFormPack(workflowId, env) {
|
|
1648
|
-
const [{ data },
|
|
1742
|
+
const [{ data }, workflowMemory] = await Promise.all([
|
|
1649
1743
|
buildCreateFormData(workflowId, env),
|
|
1650
1744
|
// 记忆读取是 best-effort(内部全 catch),不会把表单载入的错误顶掉。
|
|
1651
|
-
|
|
1745
|
+
fetchWorkflowMemory(workflowId, env),
|
|
1652
1746
|
]);
|
|
1653
|
-
return { data,
|
|
1747
|
+
return { data, workflowMemory };
|
|
1654
1748
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1749
|
+
/**
|
|
1750
|
+
* 智能填参:把「用户意图 + 表单字段上下文」交给独立智能体(`chatScene`),换回可直接使用的 `data`。
|
|
1751
|
+
*
|
|
1752
|
+
* ★ **2026-10-23 起本函数没有调用方**(用户裁定:`createForm` 不再接 `userQuestion`、内部不再调
|
|
1753
|
+
* `chatScene`,发起流程改回「`createForm` 取数 → 模型自己拼 `data` → `createRequest`」)。
|
|
1754
|
+
* 连同 `chatscene.ts` 的 `requestFormFill` 一起**保留不删**,只是暂时不接回链路;要恢复时把
|
|
1755
|
+
* `prepareCreateFormPayload` 里那句调用加回来即可。
|
|
1756
|
+
*
|
|
1757
|
+
* ## 什么时候调(恢复时生效)
|
|
1758
|
+
*
|
|
1759
|
+
* 两个条件**同时**成立才调,缺一就原样返回(= 老行为):
|
|
1760
|
+
* 1. 调用方给了 `userQuestion`(非空)—— 空串时智能体没有可依据的意图,接口也回不出有效结果,
|
|
1761
|
+
* 所以仍以它为前置(2026-10-20 用户裁定);
|
|
1762
|
+
* 2. 拿到了 `baseUrl`(真连上了 OA)。
|
|
1763
|
+
*
|
|
1764
|
+
* **不看小e 档**(2026-10-20 用户裁定):以前这一档被跳过,理由是"填单发生在宿主页面、算了也没人用";
|
|
1765
|
+
* 现在照走 —— 走通时 `createForm` 直接把算好的 `data` 交给模型,页面填单同样能用上。
|
|
1766
|
+
*
|
|
1767
|
+
* ## 失败怎么处理
|
|
1768
|
+
*
|
|
1769
|
+
* `requestFormFill` 内部吞掉所有异常并返回 `ok:false`,这里**直接不当回事**:返回
|
|
1770
|
+
* `applied:false`,调用方照旧回 `historyRequestData` / `systemFields`。智能体挂掉不能把
|
|
1771
|
+
* 发起流程打死 —— 这是本功能唯一的硬约束。
|
|
1772
|
+
*
|
|
1773
|
+
* ## 传什么记忆
|
|
1774
|
+
*
|
|
1775
|
+
* `userMemory` 用该工作流的记忆(`workflowMemory`)。它是
|
|
1776
|
+
* **一段 Markdown 文本**,不是键值对象(2026-10-19 用户裁定)。全局画像由入口处的
|
|
1777
|
+
* `workflow.memoryPrompt` 覆盖,这里不重复取。
|
|
1778
|
+
*/
|
|
1779
|
+
async function resolveIntentFill(pack, userQuestion, baseUrl, env) {
|
|
1780
|
+
const question = String(userQuestion ?? '').trim();
|
|
1781
|
+
// ★ 2026-10-20 用户裁定(口径修正):前置**只有两条** ——「`userQuestion` 非空」+「连上了 OA」。
|
|
1782
|
+
// `userQuestion` 为空时智能体没有可依据的意图、接口也回不出有效结果,故照旧不调;
|
|
1783
|
+
// **小e 档不再豁免**(此前 `hasXiaoEEnv` 直接关掉这条链路,现已按用户要求去掉)。
|
|
1784
|
+
// 走通与否仍由接口结果决定:回不出可用的 `data.chatMessage.content`(JSON)就 `applied:false`。
|
|
1785
|
+
if (!question || !baseUrl)
|
|
1786
|
+
return { applied: false };
|
|
1787
|
+
const fields = Array.isArray(pack.data.fields) ? pack.data.fields : [];
|
|
1788
|
+
if (!fields.length)
|
|
1789
|
+
return { applied: false };
|
|
1790
|
+
const input = buildChatSceneInput({
|
|
1791
|
+
userQuestion: question,
|
|
1792
|
+
workflow: {
|
|
1793
|
+
id: String(pack.data.workflow?.id ?? ''),
|
|
1794
|
+
name: String(pack.data.workflow?.name ?? ''),
|
|
1795
|
+
},
|
|
1796
|
+
fields,
|
|
1797
|
+
historyRequestData: pack.data.historyRequestData ?? null,
|
|
1798
|
+
userMemory: String(pack.workflowMemory ?? ''),
|
|
1799
|
+
now: formatLocalNow(),
|
|
1800
|
+
});
|
|
1801
|
+
const outcome = await requestFormFill(env, input);
|
|
1802
|
+
if (!outcome.ok || !outcome.data)
|
|
1803
|
+
return { applied: false };
|
|
1804
|
+
return {
|
|
1805
|
+
applied: true,
|
|
1806
|
+
data: outcome.data,
|
|
1807
|
+
// 智能体的标题规则:有默认值时它只会给空串(沿用默认);给了值才是用户明确要求的标题
|
|
1808
|
+
requestName: String(outcome.systemData?.requestName ?? '').trim(),
|
|
1809
|
+
// 供 `createRequestDirect` 把「紧急程度 / 密级 / 签字意见」一并透传给 `createRequest`;
|
|
1810
|
+
// `createForm` 自己不消费这几个键(返回面一个字不变)。
|
|
1811
|
+
systemData: outcome.systemData ?? {},
|
|
1812
|
+
// ★ 2026-10-19 用户裁定:把「生成表单参数」这段的耗时单独带出去(只在真走通 chatScene 时才有)。
|
|
1813
|
+
// 口径 = `requestFormFill` 内部这一趟(发请求 → 拿回 content → parse → 归一化),
|
|
1814
|
+
// 不含本函数上游的会话初始化与表单取数 —— 那些已由 `durationMs` 计入,两者是「整条命令」与
|
|
1815
|
+
// 「其中智能体这一段」的关系,不是并列的两笔开销。
|
|
1816
|
+
durationMs: outcome.durationMs,
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* `createForm` 的**取数阶段**(原 `commandCreateForm` 前半段,逐行搬过来,行为一字不改)。
|
|
1821
|
+
*
|
|
1822
|
+
* 抽出来的唯一目的:`createRequestDirect` 需要「未经压缩、未经落盘」的原始 payload ——
|
|
1823
|
+
* `commandCreateForm` 的输出可能因为超线只剩一句"结果已落盘"的提示,取不到键。
|
|
1824
|
+
*
|
|
1825
|
+
* 2026-10-23 用户裁定:本函数**不再**调 `chatScene`(`createForm` 不再接 `userQuestion`)。
|
|
1826
|
+
* 现在返回的就是纯取数结果:`fields` + `historyRequestData` + `systemFields` + 记忆,
|
|
1827
|
+
* 由模型自己拼 `data` 后交给 `createRequest`。
|
|
1828
|
+
*/
|
|
1829
|
+
async function prepareCreateFormPayload(args, env) {
|
|
1660
1830
|
// 会话是**惰性**加载的(`ensureSession` 只在首次调用时填充)。本命令随后会并发发起多个请求,
|
|
1661
1831
|
// 先在这里显式取一次,避免并发首调各自走一遍会话初始化。
|
|
1832
|
+
//
|
|
1833
|
+
// 顺带计时:登录取凭据(小e 环境下是 `tokens.py` 换 eteamId + `teamsCheck` 换登录人这一整套)
|
|
1834
|
+
// 走的是 auth 层自己的 fetch,不经 `host.workflowRequest` ⇒ 天然落在 `apiDurations` 之外。
|
|
1835
|
+
// 这一段的耗时单独带出去(`authMs`),否则「总耗时 − 各接口」会凭空多出几百毫秒对不上账。
|
|
1836
|
+
const authStartedAt = Date.now();
|
|
1662
1837
|
await env.host.getSession();
|
|
1838
|
+
const authMs = Date.now() - authStartedAt;
|
|
1663
1839
|
let target;
|
|
1664
1840
|
let pack;
|
|
1665
1841
|
const explicitId = singleExplicitWorkflowId(args.workflow);
|
|
@@ -1691,22 +1867,70 @@ async function commandCreateForm(args, env) {
|
|
|
1691
1867
|
// 完整**(pretty JSON,未紧凑),所以模型按提示读回来拿到的是与 stdout 同口径的全量内容。
|
|
1692
1868
|
//
|
|
1693
1869
|
// 显式标注为 `Record<string, any>`:`pack.data` 是带索引签名的宽松对象,而对象字面量里又显式写了
|
|
1694
|
-
// `workflow
|
|
1870
|
+
// `workflow`,TS 的推断会**丢掉索引签名**、只留这个显式键。
|
|
1695
1871
|
const fullPayload = {
|
|
1696
1872
|
...pack.data,
|
|
1697
|
-
workflow: {
|
|
1698
|
-
|
|
1873
|
+
workflow: {
|
|
1874
|
+
...(pack.data.workflow || {}),
|
|
1875
|
+
selectionSource: target.source,
|
|
1876
|
+
// ★ 2026-10-20 用户裁定:`from` **只在**「多候选按近一年发起记录推断出来」这一种场景出现,
|
|
1877
|
+
// 其余场景一个字都不多(模型据此知道这个工作流是推断的,而不是用户明说的)。
|
|
1878
|
+
...(target.from ? { from: target.from } : {}),
|
|
1879
|
+
},
|
|
1699
1880
|
};
|
|
1700
|
-
|
|
1881
|
+
// 该工作流的记忆:**一段 Markdown 原文**,读不到就不出这个键(模型见到"没有这个键"就按新用户处理)。
|
|
1882
|
+
if (pack.workflowMemory)
|
|
1883
|
+
fullPayload.workflowMemory = pack.workflowMemory;
|
|
1884
|
+
// ★ 2026-10-23 用户裁定:`createForm` **不再调 `chatScene`**(入参里的 `userQuestion` 也一并取消)。
|
|
1885
|
+
//
|
|
1886
|
+
// 起因:智能填参(2026-10-19 引入)让 `createForm` 在返回前把意图算成 `data`,成功时还会删掉
|
|
1887
|
+
// `historyRequestData` / `systemFields` —— 而现在的流程是「`createForm` 只负责取数(字段 + 历史 +
|
|
1888
|
+
// 系统字段 + 记忆)→ 模型自己拼 `data` → 调 `createRequest`」。中间再跑一次模型既多一笔开销,
|
|
1889
|
+
// 又会让返回面在两套结构之间切换。
|
|
1890
|
+
//
|
|
1891
|
+
// `resolveIntentFill` / `chatscene.ts` 的 `requestFormFill` **保留不删**(万一要恢复这条路),
|
|
1892
|
+
// 只是这里不再调用 —— 因此 `fillDurationMs`(智能体那一段的耗时)也不会再出现。
|
|
1893
|
+
return { payload: fullPayload, authMs };
|
|
1894
|
+
}
|
|
1895
|
+
async function commandCreateForm(args, env) {
|
|
1896
|
+
// ★ 2026-10-16 用户裁定:返回值里固定带上**本命令自身的耗时**(毫秒)。
|
|
1897
|
+
// 起点取在函数入口(含下面那次会话惰性初始化),终点取在结果产出之后 —— 即"从命令开始执行到
|
|
1898
|
+
// 执行完成"。取值不做开关、不进 skill 文档(用户明确要求"直接默认加上")。
|
|
1899
|
+
const startedAt = Date.now();
|
|
1900
|
+
// ★ 2026-10-21 用户裁定:耗时口径与 `createRequestDirect` **完全一致** —— 除总耗时外再给
|
|
1901
|
+
// 「每个接口多久」(`apiDurations`)与「登录取凭据多久」(`authMs`)。所以这里也把 host 包一层
|
|
1902
|
+
// 逐接口计时,整条命令的请求都会被记上。
|
|
1903
|
+
const spans = [];
|
|
1904
|
+
const timedEnv = { ...env, host: withApiTiming(env.host, spans) };
|
|
1905
|
+
const { payload: fullPayload, authMs } = await prepareCreateFormPayload(args, timedEnv);
|
|
1906
|
+
// 取数阶段已全部结束 ⇒ 这里拿到的是完整的一份;它既是判线要预留的键,也是最终返回的值。
|
|
1907
|
+
const { durations: apiDurations, slowest: apiSlowest } = summarizeApiSpans(spans);
|
|
1908
|
+
// 判线的位数预留:`durationMs` / `apiDurations` / `apiSlowest` / `authMs` 一定会有。
|
|
1909
|
+
// `apiDurations` 此刻已经求出,直接按真值占(不必用哨兵估)。
|
|
1910
|
+
const reserve = {
|
|
1911
|
+
durationMs: DURATION_RESERVE_SENTINEL,
|
|
1912
|
+
apiDurations,
|
|
1913
|
+
apiSlowest,
|
|
1914
|
+
authMs: DURATION_RESERVE_SENTINEL,
|
|
1915
|
+
};
|
|
1916
|
+
const compacted = await compactFormOutput(fullPayload, timedEnv, 'last_create_form.json', [], {
|
|
1701
1917
|
safeChars: CREATE_FORM_STDOUT_SAFE_CHARS,
|
|
1702
1918
|
operation: 'workflow.createForm',
|
|
1703
|
-
// 判线要量"最终打出去的那份文本",而 `durationMs`
|
|
1704
|
-
//
|
|
1705
|
-
|
|
1919
|
+
// 判线要量"最终打出去的那份文本",而 `durationMs` 的真值只能在压缩**之后**求
|
|
1920
|
+
// (它统计的正是这段时间),故此处按位数用哨兵值先把它算进判线 —— 高估只让边界上的表单提前落盘
|
|
1921
|
+
// (安全方向)。
|
|
1922
|
+
reserve,
|
|
1706
1923
|
});
|
|
1707
1924
|
// 耗时并在 `data` 里(与 `outputChars` / `persistedResult` 这些运行期属性同处);**两条分支都带上**
|
|
1708
1925
|
// —— 超线落盘那一档 `data` 只剩一句提示,并进去它才不会恰好在"大表单"时消失。
|
|
1709
|
-
|
|
1926
|
+
const result = {
|
|
1927
|
+
...compacted,
|
|
1928
|
+
durationMs: Date.now() - startedAt,
|
|
1929
|
+
apiDurations,
|
|
1930
|
+
apiSlowest,
|
|
1931
|
+
authMs,
|
|
1932
|
+
};
|
|
1933
|
+
return workflowSuccess('workflow.createForm', result);
|
|
1710
1934
|
}
|
|
1711
1935
|
// ---------------------------------------------------------------------------
|
|
1712
1936
|
// createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
|
|
@@ -1735,26 +1959,36 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1735
1959
|
// 结果对象再判定 —— 不能让它们的失败经由 Promise.all 把整个命令带崩(后者语义与串行版一致)。
|
|
1736
1960
|
// `loadBaseParam` / `loadFrameParam` 也改成"先收着、后判定":目的是让 base 的权限结论
|
|
1737
1961
|
// **优先于** frame 的泛化错误浮出(理由见下面的判定顺序)。
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1962
|
+
//
|
|
1963
|
+
// 2026-10-22(用户裁定;非小e 行为不变):
|
|
1964
|
+
// ① 小e 环境**不发** `loadMinorInfo`:这一档不做系统字段(流程标题/紧急程度/密级)的必填校验,
|
|
1965
|
+
// 它的 `permissions` / 选项清单用不上,省掉真机 429 ms 的一趟。`tenant/setting` 仍照发
|
|
1966
|
+
// (`fileUploadParams` 的 sizeLimit 从它来)。
|
|
1967
|
+
// ② `getFormLayout` **挂到 base+frame 之后立刻发**,与 minor/tenant 并行:它只用 runtime 与
|
|
1968
|
+
// frame(见 `buildLayoutRequest`),原先要等四个都回来才发,白等最慢的那一个。
|
|
1969
|
+
const isXiaoE = hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
|
|
1970
|
+
// `tolerateBusinessError`:loadBase 用 `status:false` + `commonParam.authInfo` 表达
|
|
1971
|
+
// 「不存在 / 无发起权限」,走带业务过滤的通道只会拿到"接口返回状态异常,code=200"。
|
|
1972
|
+
const basePromise = requestJson(env, 'POST', FORM_ENDPOINTS.loadBase, query, body, { tolerateBusinessError: true })
|
|
1973
|
+
.then((value) => ({ ok: true, value }))
|
|
1974
|
+
.catch((error) => ({ ok: false, error }));
|
|
1975
|
+
const framePromise = requestJson(env, 'POST', FORM_ENDPOINTS.loadFrame, query, body)
|
|
1976
|
+
.then((value) => ({ ok: true, value }))
|
|
1977
|
+
.catch((error) => ({ ok: false, error }));
|
|
1978
|
+
// 小e:不发请求,直接给一个"没有 minorInfo"的结果,下游静默降级(不产生告警文本)。
|
|
1979
|
+
const minorPromise = isXiaoE
|
|
1980
|
+
? Promise.resolve({ ok: false, value: null })
|
|
1981
|
+
: requestJson(env, 'POST', FORM_ENDPOINTS.loadMinor, query, { workflowId, isCreate: 1, isMobile: 0 })
|
|
1751
1982
|
.then((value) => ({ ok: true, value }))
|
|
1752
|
-
.catch(() => ({ ok: false, value: null }))
|
|
1753
|
-
])
|
|
1983
|
+
.catch(() => ({ ok: false, value: null }));
|
|
1984
|
+
const tenantPromise = requestJson(env, 'POST', FORM_ENDPOINTS.tenantSetting, undefined, { setType: [] })
|
|
1985
|
+
.then((value) => ({ ok: true, value }))
|
|
1986
|
+
.catch(() => ({ ok: false, value: null }));
|
|
1754
1987
|
// 「为什么进不去这张表单」必须**先于**其它接口的错误判定:
|
|
1755
1988
|
// ID 不存在 / 无发起权限时,真机上 `loadFrameParam`、`loadMinorInfo` 会返回
|
|
1756
1989
|
// `code:500「无流程查看权限」`——那是**后果**不是**原因**。若让它先外溢,模型只能对着
|
|
1757
1990
|
// "无流程查看权限"猜,说不出到底是「ID 不存在」还是「存在但没权限」。
|
|
1991
|
+
const [baseOutcome, frameOutcome] = await Promise.all([basePromise, framePromise]);
|
|
1758
1992
|
if (baseOutcome.ok) {
|
|
1759
1993
|
const blocked = readCreateFormBlock(baseOutcome.value);
|
|
1760
1994
|
if (blocked)
|
|
@@ -1764,10 +1998,25 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1764
1998
|
throw baseOutcome.error;
|
|
1765
1999
|
if (!frameOutcome.ok)
|
|
1766
2000
|
throw frameOutcome.error;
|
|
2001
|
+
const base = baseOutcome.value;
|
|
2002
|
+
const frameData = frameOutcome.value;
|
|
2003
|
+
const runtime = parseRuntime(base, workflowId);
|
|
2004
|
+
const frame = parseFrame(frameData, runtime.node_id);
|
|
2005
|
+
// 表单布局只依赖上面这两个结果 —— 拿到就发,不陪 minor/tenant 一起等。
|
|
2006
|
+
const layoutPromise = requestJson(env, 'POST', formApiUrl(FORM_ENDPOINTS.formLayout, frame.relate_type), { formId: frame.form_id, ...AJAX_QUERY }, buildLayoutRequest(workflowId, runtime, frame))
|
|
2007
|
+
.then((value) => ({ ok: true, value }))
|
|
2008
|
+
.catch((error) => ({ ok: false, error }));
|
|
2009
|
+
const [layoutOutcome, minorOutcome, tenantOutcome] = await Promise.all([layoutPromise, minorPromise, tenantPromise]);
|
|
2010
|
+
if (!layoutOutcome.ok)
|
|
2011
|
+
throw layoutOutcome.error;
|
|
1767
2012
|
const minorWarningText = 'loadMinorInfo 获取系统字段权限失败,跳过系统字段(流程标题/紧急程度/密级)必填校验';
|
|
1768
2013
|
let minorInfo = {};
|
|
1769
2014
|
let minorWarning = null;
|
|
1770
|
-
if (
|
|
2015
|
+
if (isXiaoE) {
|
|
2016
|
+
// 小e:本来就没发这个请求 ⇒ 按"没有系统字段权限"静默降级,不把告警塞给模型。
|
|
2017
|
+
minorWarning = null;
|
|
2018
|
+
}
|
|
2019
|
+
else if (minorOutcome.ok) {
|
|
1771
2020
|
try {
|
|
1772
2021
|
minorInfo = parseMinorInfo(minorOutcome.value);
|
|
1773
2022
|
}
|
|
@@ -1779,10 +2028,6 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1779
2028
|
else {
|
|
1780
2029
|
minorWarning = minorWarningText;
|
|
1781
2030
|
}
|
|
1782
|
-
const base = baseOutcome.value;
|
|
1783
|
-
const frameData = frameOutcome.value;
|
|
1784
|
-
const runtime = parseRuntime(base, workflowId);
|
|
1785
|
-
const frame = parseFrame(frameData, runtime.node_id);
|
|
1786
2031
|
// tenantSetting 取不到时按 {} 处理(与串行版的 catch 分支同口径)。
|
|
1787
2032
|
let tenantSetting = {};
|
|
1788
2033
|
if (tenantOutcome.ok) {
|
|
@@ -1793,7 +2038,7 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1793
2038
|
tenantSetting = {};
|
|
1794
2039
|
}
|
|
1795
2040
|
}
|
|
1796
|
-
const layoutResp =
|
|
2041
|
+
const layoutResp = layoutOutcome.value;
|
|
1797
2042
|
const layoutData = layoutResp;
|
|
1798
2043
|
if (!layoutData || typeof layoutData !== 'object')
|
|
1799
2044
|
throw workflowApiError('format_error', 'getFormLayout 未返回有效 data');
|
|
@@ -1856,7 +2101,7 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1856
2101
|
* 小e 环境下两处都不跑(`createRequest` 同档也跳过字段联动与显示属性联动,由浏览器页面自己算)。
|
|
1857
2102
|
* 非小e 环境行为不变。
|
|
1858
2103
|
*/
|
|
1859
|
-
if (!
|
|
2104
|
+
if (!isXiaoE && linkageActions.length) {
|
|
1860
2105
|
const loadEntries = _entriesFromDefaults(defaults);
|
|
1861
2106
|
try {
|
|
1862
2107
|
onLoadLinkage = await runFieldLinkage(env, buildLinkageContext({ runtime, frame, layout: { form_id: frame.form_id }, linkageActions }), loadEntries, new Set(['onLoadForm']), defaults);
|
|
@@ -2201,12 +2446,18 @@ function bindInputFields(main, details, fields) {
|
|
|
2201
2446
|
if (field)
|
|
2202
2447
|
entries.push({ field, value: main[key] });
|
|
2203
2448
|
}
|
|
2204
|
-
|
|
2449
|
+
// ★ 2026-10-26 修复:`rowId` 必须**每行唯一**,服务端就是按 `(subForm.id, rowId)` 分行的
|
|
2450
|
+
// (同一行所有字段共用一个 rowId,不同行 rowId 不同)。原来用
|
|
2451
|
+
// `Date.now() * 100000 + ...` 得到 ≈1.79e17,已越过 `Number.MAX_SAFE_INTEGER`(9.0e15),
|
|
2452
|
+
// double 在该量级的精度间距是 32 ⇒ `+1/+2/+3` 全被舍回同一个数,4 行明细塌成 1 行。
|
|
2453
|
+
// 改用 BigInt 生成,保持同一套 18 位数值形态但不再丢精度。
|
|
2454
|
+
const rowSeed = BigInt(Date.now()) * 100000n
|
|
2455
|
+
+ BigInt(Math.floor(Math.random() * 70000) + 10000);
|
|
2205
2456
|
let rowOffset = 0;
|
|
2206
2457
|
for (const subFormId of Object.keys(details)) {
|
|
2207
2458
|
const rows = details[subFormId];
|
|
2208
2459
|
rows.forEach((row, index) => {
|
|
2209
|
-
const rowId = String(rowSeed + rowOffset);
|
|
2460
|
+
const rowId = String(rowSeed + BigInt(rowOffset));
|
|
2210
2461
|
rowOffset += 1;
|
|
2211
2462
|
const rowMeta = { dataIndex: index + 1, rowId, subForm: { id: subFormId } };
|
|
2212
2463
|
for (const key of Object.keys(row)) {
|
|
@@ -2372,6 +2623,23 @@ function splitValueAliases(raw) {
|
|
|
2372
2623
|
}
|
|
2373
2624
|
return out;
|
|
2374
2625
|
}
|
|
2626
|
+
/**
|
|
2627
|
+
* 一个格子要送去查候选的「说法」清单——**按字段是单选还是多选分叉**:
|
|
2628
|
+
*
|
|
2629
|
+
* - **单选**:整串与各说法一并登记,由 `resolveBrowserAliases` 的「整串优先」先试整串
|
|
2630
|
+
* (名称本身自带逗号时不会被拆错);
|
|
2631
|
+
* - **多选**:逗号是**值分隔符**,只拆不拼——整串既不做「整串优先」,也**不进候选池**。
|
|
2632
|
+
* 否则「张三,李四」会被整串当成一个名称,恰好命中就静默吞成一个值(真机问题:
|
|
2633
|
+
* 多选浏览字段只写进去第一个对象),而且还会白花一次 complete / nameToId 往返。
|
|
2634
|
+
*
|
|
2635
|
+
* 多选里若单个对象名真的自带逗号,由 `名称(ID)` 显式指定(`resolveBrowserAliases` 认它)。
|
|
2636
|
+
*/
|
|
2637
|
+
function lookupTermsOf(field, raw) {
|
|
2638
|
+
const aliases = splitValueAliases(raw);
|
|
2639
|
+
if (field && field.isMultiple)
|
|
2640
|
+
return aliases.length ? aliases : [raw];
|
|
2641
|
+
return aliases.length > 1 ? [raw, ...aliases] : [raw];
|
|
2642
|
+
}
|
|
2375
2643
|
/** 浏览按钮字段解析不出来的统一出口。`candidates` **只含关键词真实命中**,绝不用首页枚举冒充。 */
|
|
2376
2644
|
function browserAmbiguity(field, input, hits) {
|
|
2377
2645
|
return new ValueAmbiguity({
|
|
@@ -2421,8 +2689,11 @@ function resolveBrowserAliases(field, raw, lookup) {
|
|
|
2421
2689
|
return out;
|
|
2422
2690
|
};
|
|
2423
2691
|
const aliases = splitValueAliases(raw);
|
|
2424
|
-
//
|
|
2425
|
-
|
|
2692
|
+
// 整串与各说法**并行**登记:整串命中(名称自带逗号)与切段命中进同一个池子判定。
|
|
2693
|
+
// 多选字段只登记切段结果(见 lookupTermsOf)。
|
|
2694
|
+
const terms = lookupTermsOf(field, raw);
|
|
2695
|
+
/** 「整串优先」只对**单选**成立:多选里逗号是值分隔符,整串本身不是候选名称。 */
|
|
2696
|
+
const wholeTermFirst = !(field && field.isMultiple) && aliases.length > 1;
|
|
2426
2697
|
const pool = [];
|
|
2427
2698
|
/** 每个词各自「字面同名」的候选(显式 ID 也算字面命中,它本来就是精确值)。 */
|
|
2428
2699
|
const literalByTerm = new Map();
|
|
@@ -2435,8 +2706,8 @@ function resolveBrowserAliases(field, raw, lookup) {
|
|
|
2435
2706
|
literalByTerm.set(term, candidates.filter((c) => String(c?.name ?? '') === term));
|
|
2436
2707
|
}
|
|
2437
2708
|
const poolHits = dedupe(pool);
|
|
2438
|
-
// ①
|
|
2439
|
-
if (
|
|
2709
|
+
// ① **整串优先**(仅单选):原串本身就是一个名称 ⇒ 用它、不拆(名称自带逗号也不会被拆错);真重名则澄清
|
|
2710
|
+
if (wholeTermFirst) {
|
|
2440
2711
|
const wholeHits = dedupe(literalByTerm.get(raw) || []);
|
|
2441
2712
|
if (wholeHits.length === 1)
|
|
2442
2713
|
return wholeHits[0];
|
|
@@ -2582,6 +2853,7 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2582
2853
|
if (!item || typeof item !== 'object') {
|
|
2583
2854
|
throw new ValueAmbiguity({ kind: 'EMPLOYEE_SCOPE', fieldId: field.id, fieldName: field.name, reason: '人员范围字段的值必须是对象数组,每项含 name 和 type', resolution: '格式如 [{"name":"袁湘云","type":"user"}]' });
|
|
2584
2855
|
}
|
|
2856
|
+
// 入参键名:`id`/`name` 是现行口径(智能体侧),`optionId`/`content` 是历史写法,两套都收。
|
|
2585
2857
|
const optionId = String(item.optionId ?? item.id ?? '').trim();
|
|
2586
2858
|
const content = String(item.content ?? item.name ?? '').trim();
|
|
2587
2859
|
const scopeType = String(item.type ?? '').trim();
|
|
@@ -2593,7 +2865,7 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2593
2865
|
continue;
|
|
2594
2866
|
}
|
|
2595
2867
|
if (!content || !scopeType) {
|
|
2596
|
-
throw new ValueAmbiguity({ kind: 'EMPLOYEE_SCOPE', fieldId: field.id, fieldName: field.name, input: item, reason: '缺少名称或类型', resolution: '每项必须含 name
|
|
2868
|
+
throw new ValueAmbiguity({ kind: 'EMPLOYEE_SCOPE', fieldId: field.id, fieldName: field.name, input: item, reason: '缺少名称或类型', resolution: '每项必须含 name 与 type(user/dept/subcompany/group/role/position/external/all/allExternal)' });
|
|
2597
2869
|
}
|
|
2598
2870
|
// 名称/类型齐了却没拿到 ID:说明名称→ID 解析没成功(预解析失败或未走预解析)。
|
|
2599
2871
|
// **绝不静默丢弃**——空 optionId 会让 hasDetailValue 判为无值,整条值从保存体里消失,
|
|
@@ -2679,6 +2951,18 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2679
2951
|
const key = `${field.id}_${rowIndex}_${term}`;
|
|
2680
2952
|
return { mapped: mappings[key] || [], completed: browserHints[key] || [] };
|
|
2681
2953
|
};
|
|
2954
|
+
if (field.isMultiple) {
|
|
2955
|
+
// 多选:一个格子里的逗号是**值分隔符**,逐说法各解析出一条 `dataOptions`
|
|
2956
|
+
// (整串不当名称,见 lookupTermsOf)。某个说法有歧义时整条值都不落地,
|
|
2957
|
+
// 由该说法自己的候选澄清——已解析出的兄弟值**不丢**,因为澄清后会整条重发。
|
|
2958
|
+
for (const term of lookupTermsOf(field, raw)) {
|
|
2959
|
+
const pickedOne = resolveBrowserAliases(field, term, lookup);
|
|
2960
|
+
if (options.some((o) => o.optionId === pickedOne.id))
|
|
2961
|
+
continue;
|
|
2962
|
+
options.push({ optionId: pickedOne.id, content: pickedOne.name });
|
|
2963
|
+
}
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2682
2966
|
const picked = resolveBrowserAliases(field, raw, lookup);
|
|
2683
2967
|
options.push({ optionId: picked.id, content: picked.name });
|
|
2684
2968
|
}
|
|
@@ -2751,6 +3035,9 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2751
3035
|
* `{"optionId":"<解析出的ID>","content":"刘艳","type":"user","optionObj":<接口原始对象>}`,
|
|
2752
3036
|
* 交给 `serializeValue` 的"已解析"分支直接透传。解析不到(0 命中/多命中)时把歧义挂到该条目上,
|
|
2753
3037
|
* 由主循环原样报给模型(带候选),**不允许退化成一个空 optionId 被静默丢掉**。
|
|
3038
|
+
*
|
|
3039
|
+
* ★ 2026-10-22 用户裁定:智能体入参统一成 `{"type":…,"id":…,"name":…}`(不再用 `optionId`/`content`)。
|
|
3040
|
+
* 入参侧三个键名都收(`id`/`optionId`、`name`/`content`),内部与服务端仍用 `optionId`/`content`。
|
|
2754
3041
|
*/
|
|
2755
3042
|
async function preResolveEmployeeScopeEntries(env, entries) {
|
|
2756
3043
|
const failures = new Map();
|
|
@@ -2788,7 +3075,7 @@ async function preResolveEmployeeScopeEntries(env, entries) {
|
|
|
2788
3075
|
reason: selected.length ? 'MULTIPLE_MATCHES' : 'NOT_FOUND',
|
|
2789
3076
|
candidates: selected.map((c) => ({ id: c.id, name: c.name })),
|
|
2790
3077
|
resolution: selected.length
|
|
2791
|
-
? `把 candidates 原样交给用户确认;用户选定后由你取该项的 id / name 拼出 {"
|
|
3078
|
+
? `把 candidates 原样交给用户确认;用户选定后由你取该项的 id / name 拼出 {"id":…,"name":…,"type":"${scopeType}"} 重发同一条命令(不要让用户提供 ID)。`
|
|
2792
3079
|
: '该名称在系统里没有匹配。请让用户给出该对象的完整名称,或其它可用于检索的关键词(不要向用户索取 ID)。',
|
|
2793
3080
|
};
|
|
2794
3081
|
break;
|
|
@@ -2897,8 +3184,8 @@ async function resolveEntryValues(env, entries, formId) {
|
|
|
2897
3184
|
continue;
|
|
2898
3185
|
// 一个格子可写多个「说法」(逗号分隔):**整串与各说法一起登记**——这样"整串优先"要用的
|
|
2899
3186
|
// 整串候选、以及各说法各自的候选,都在同一批里拿到(nameToId 一把批量发,未命中的再并行补)。
|
|
2900
|
-
|
|
2901
|
-
const terms =
|
|
3187
|
+
// 多选字段只登记切段结果(逗号是值分隔符,见 lookupTermsOf)。
|
|
3188
|
+
const terms = lookupTermsOf(field, raw);
|
|
2902
3189
|
// 候选来源二选一:控件自带 `browserModule`+`browserType` ⇒ 只走 complete(下一步补查),
|
|
2903
3190
|
// **不进 nameToId 批次**;否则只走 nameToId。这样池子里永远只有一个来源,不会互相污染。
|
|
2904
3191
|
const hasBrowserChannel = Boolean(field.browserModule && field.browserType);
|
|
@@ -3191,6 +3478,147 @@ function _detailValueIds(detail) {
|
|
|
3191
3478
|
}
|
|
3192
3479
|
return ids;
|
|
3193
3480
|
}
|
|
3481
|
+
/**
|
|
3482
|
+
* 把一个值拆成「可比 token」集合:`id` / `optionId` / `name` / `content`,以及去掉尾部
|
|
3483
|
+
* `(id)` / `(id)` 后缀的纯文本。
|
|
3484
|
+
*
|
|
3485
|
+
* 为什么要拆:入参是**原始串**(`"银行(个人报销)(7)"`、`"20260999"`),基线是 **detail**
|
|
3486
|
+
* (`dataOptions:[{optionId:'7',content:'银行(个人报销)'}]`、`{content:'20260999'}`),
|
|
3487
|
+
* 直接比 `_entryValueIds` 与 `_detailValueIds` 会得到 `["银行(个人报销)(7)"]` vs
|
|
3488
|
+
* `["银行(个人报销)","7"]` 这种必不相等的形状。取交集判等才不会误判成"变了"。
|
|
3489
|
+
*/
|
|
3490
|
+
function _pushComparableToken(tokens, raw) {
|
|
3491
|
+
const text = String(raw ?? '').trim();
|
|
3492
|
+
if (!text)
|
|
3493
|
+
return;
|
|
3494
|
+
tokens.add(text);
|
|
3495
|
+
const matched = text.match(/^(.*?)\s*[((]\s*([^()()]*?)\s*[))]$/);
|
|
3496
|
+
if (matched) {
|
|
3497
|
+
// 尾部括号里是 id 时(`银行(个人报销)(7)`、`泛微上海(100504500000311504)`)也要能对上
|
|
3498
|
+
if (matched[1].trim())
|
|
3499
|
+
tokens.add(matched[1].trim());
|
|
3500
|
+
if (matched[2].trim())
|
|
3501
|
+
tokens.add(matched[2].trim());
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
function _entryComparableTokens(source) {
|
|
3505
|
+
const tokens = new Set();
|
|
3506
|
+
for (const item of listValues(source)) {
|
|
3507
|
+
if (item && typeof item === 'object') {
|
|
3508
|
+
for (const key of ['id', 'optionId', 'value', 'name', 'content']) {
|
|
3509
|
+
if (item[key] !== undefined && item[key] !== null)
|
|
3510
|
+
_pushComparableToken(tokens, item[key]);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
else {
|
|
3514
|
+
_pushComparableToken(tokens, item);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
return tokens;
|
|
3518
|
+
}
|
|
3519
|
+
/** 基线 detail 的可比 token(与 `_entryComparableTokens` 同一套口径)。 */
|
|
3520
|
+
function _baselineComparableTokens(detail) {
|
|
3521
|
+
const tokens = new Set();
|
|
3522
|
+
if (detail && typeof detail === 'object') {
|
|
3523
|
+
if (detail.content !== undefined && detail.content !== null)
|
|
3524
|
+
_pushComparableToken(tokens, detail.content);
|
|
3525
|
+
if (detail.dataText && typeof detail.dataText === 'object' && detail.dataText.content !== undefined) {
|
|
3526
|
+
_pushComparableToken(tokens, detail.dataText.content);
|
|
3527
|
+
}
|
|
3528
|
+
for (const option of detail.dataOptions || []) {
|
|
3529
|
+
if (!option || typeof option !== 'object')
|
|
3530
|
+
continue;
|
|
3531
|
+
if (option.optionId !== undefined && option.optionId !== null)
|
|
3532
|
+
_pushComparableToken(tokens, option.optionId);
|
|
3533
|
+
if (option.content !== undefined && option.content !== null)
|
|
3534
|
+
_pushComparableToken(tokens, option.content);
|
|
3535
|
+
if (option.name !== undefined && option.name !== null)
|
|
3536
|
+
_pushComparableToken(tokens, option.name);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return tokens;
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* 入参这条值是否**与基线相同**(即"用户没动过")。
|
|
3543
|
+
*
|
|
3544
|
+
* 判松不判严(任一 token 命中即算相同):漏判"变了"只会少触发一次联动(页面语义本来就是
|
|
3545
|
+
* 没变不触发);误判"没变"则会把用户真实修改的字段跳过,风险更大,所以这里宁可宽。
|
|
3546
|
+
* 基线里没有该字段 / 基线没值时一律视为"变了"(首次填写必须触发)。
|
|
3547
|
+
*/
|
|
3548
|
+
function _sameAsBaseline(entry, detail) {
|
|
3549
|
+
const base = _baselineComparableTokens(detail);
|
|
3550
|
+
if (!base.size)
|
|
3551
|
+
return false;
|
|
3552
|
+
const incoming = _entryComparableTokens(entry.value);
|
|
3553
|
+
if (!incoming.size)
|
|
3554
|
+
return false;
|
|
3555
|
+
for (const token of incoming) {
|
|
3556
|
+
if (base.has(token))
|
|
3557
|
+
return true;
|
|
3558
|
+
}
|
|
3559
|
+
return false;
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* 「值真的变了的字段 id」——`changeField` 类联动的触发依据(2026-10-20 用户裁定)。
|
|
3563
|
+
*
|
|
3564
|
+
* 旧口径把**入参里出现的字段**一律当成变更字段(模拟页面 onchange),于是"把表单默认值原样
|
|
3565
|
+
* 回传"会伪造出一次"用户修改":真机坑是 `合同号=20260999`(= 该字段 `initialValue`)被
|
|
3566
|
+
* `createForm.data` 带出来、又被整份回传给 `createRequest`,触发合同号的 changeField 联动,
|
|
3567
|
+
* 服务端按合同重算 `相关客户` 等 7 个字段把 `相关客户` 置空 ⇒ 必填缺失。
|
|
3568
|
+
*
|
|
3569
|
+
* 现在的口径:**只有值与基线不同才算变更**。
|
|
3570
|
+
* - 发起流程:基线 = 表单默认值(`context.defaults`)
|
|
3571
|
+
* - 编辑流程:基线 = 编辑前的字段值(`context.currentDetails`,来自 edit-load 的 `dataDetails`)
|
|
3572
|
+
*
|
|
3573
|
+
* 明细行按 `(subFormId, dataIndex)` 定位基线;明细默认值常常没有行号,此时回落到"同字段任一基线"
|
|
3574
|
+
* (见 `byField`)——宁可判"没变",也不要让无行号的默认值把每一行都判成变更。
|
|
3575
|
+
*/
|
|
3576
|
+
function _changedFieldIds(entries, baseline) {
|
|
3577
|
+
const byRow = new Map();
|
|
3578
|
+
const byField = new Map();
|
|
3579
|
+
const pushTo = (map, key, item) => {
|
|
3580
|
+
const bucket = map.get(key);
|
|
3581
|
+
if (bucket)
|
|
3582
|
+
bucket.push(item);
|
|
3583
|
+
else
|
|
3584
|
+
map.set(key, [item]);
|
|
3585
|
+
};
|
|
3586
|
+
for (const detail of baseline || []) {
|
|
3587
|
+
if (!detail || typeof detail !== 'object')
|
|
3588
|
+
continue;
|
|
3589
|
+
const fieldId = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
3590
|
+
if (!fieldId)
|
|
3591
|
+
continue;
|
|
3592
|
+
const subFormId = String((detail.subForm && detail.subForm.id) ?? '').trim();
|
|
3593
|
+
const hasRow = detail.dataIndex !== undefined && detail.dataIndex !== null;
|
|
3594
|
+
const dataIndex = hasRow ? String(detail.dataIndex) : '';
|
|
3595
|
+
pushTo(byField, fieldId, detail);
|
|
3596
|
+
if (hasRow)
|
|
3597
|
+
pushTo(byRow, `${subFormId}\u0000${fieldId}\u0000${dataIndex}`, detail);
|
|
3598
|
+
}
|
|
3599
|
+
const changed = [];
|
|
3600
|
+
const seen = new Set();
|
|
3601
|
+
for (const entry of entries) {
|
|
3602
|
+
if (!entry.field || typeof entry.field !== 'object')
|
|
3603
|
+
continue;
|
|
3604
|
+
const fieldId = String(entry.field.id ?? '').trim();
|
|
3605
|
+
if (!fieldId || seen.has(fieldId))
|
|
3606
|
+
continue;
|
|
3607
|
+
const row = (entry.row && typeof entry.row === 'object') ? entry.row : null;
|
|
3608
|
+
const subFormId = row ? String((row.subForm && row.subForm.id) ?? '').trim() : String(entry.field.subFormId ?? '').trim();
|
|
3609
|
+
const hasRow = Boolean(row) && row.dataIndex !== undefined && row.dataIndex !== null;
|
|
3610
|
+
const candidates = hasRow
|
|
3611
|
+
? (byRow.get(`${subFormId}\u0000${fieldId}\u0000${String(row.dataIndex)}`) || [])
|
|
3612
|
+
: (byField.get(fieldId) || []);
|
|
3613
|
+
const fallback = hasRow ? (byField.get(fieldId) || []) : [];
|
|
3614
|
+
const unchanged = [...candidates, ...fallback].some((detail) => _sameAsBaseline(entry, detail));
|
|
3615
|
+
if (unchanged)
|
|
3616
|
+
continue;
|
|
3617
|
+
seen.add(fieldId);
|
|
3618
|
+
changed.push(fieldId);
|
|
3619
|
+
}
|
|
3620
|
+
return changed;
|
|
3621
|
+
}
|
|
3194
3622
|
function _linkageOptions(sourceContent) {
|
|
3195
3623
|
if (!sourceContent || typeof sourceContent !== 'string' || !sourceContent.trim())
|
|
3196
3624
|
return [];
|
|
@@ -3315,9 +3743,10 @@ function _mainValueMap(details) {
|
|
|
3315
3743
|
}
|
|
3316
3744
|
return result;
|
|
3317
3745
|
}
|
|
3318
|
-
async function runFieldLinkage(env, ctx, entries, actionTypes, allDetails, isEdit = false) {
|
|
3319
|
-
|
|
3320
|
-
|
|
3746
|
+
async function runFieldLinkage(env, ctx, entries, actionTypes, allDetails, isEdit = false, baseline) {
|
|
3747
|
+
// 只有「值与基线(发起=表单默认值 / 编辑=编辑前值)不同」的字段才算变更字段,
|
|
3748
|
+
// 见 `_changedFieldIds` 的注释(2026-10-20 用户裁定:值没变就不该触发 changeField 联动)。
|
|
3749
|
+
const changedIds = _changedFieldIds(entries, baseline);
|
|
3321
3750
|
const runDataList = _buildLinkageRunData(ctx.linkageActions, changedIds, actionTypes);
|
|
3322
3751
|
const neededIds = _neededLinkageFieldIds(ctx.linkageActions, runDataList);
|
|
3323
3752
|
const dataList = [];
|
|
@@ -4018,7 +4447,8 @@ function mainFilledRows(fields, values, hiddenIds, disabledIds, compsFieldIds) {
|
|
|
4018
4447
|
* 表格口径**,只换结论句与末段(那边要说清"还没发起 + 还差哪几项");`NEEDS_CONFIRMATION` 不给
|
|
4019
4448
|
* 文本(要先解决 `ambiguities` 再重发)。
|
|
4020
4449
|
*
|
|
4021
|
-
* 内容分四段:结论(`message
|
|
4450
|
+
* 内容分四段:结论(`message`,命令已写好的中文;**只用来渲染,渲染后这个键不再返回**)→ 编号/链接
|
|
4451
|
+
* → 主表「已填内容」两列表
|
|
4022
4452
|
* (系统字段如流程标题在前,业务字段按表单布局顺序随后;行由 `mainFilledRows` + `systemFilledRows`
|
|
4023
4453
|
* 算好传进来,命令侧只有这一处口径)→ 各明细表(Excel 列形态)→ 提交失败原因 / 草稿缺项提醒(有才出现)。
|
|
4024
4454
|
*/
|
|
@@ -4262,6 +4692,47 @@ function xiaoEUrlParamValue(value) {
|
|
|
4262
4692
|
* 明细字段(`preview` 里带 `dataIndex`)按**行号升序**取值、用 `;` 连接;主表字段就是它自己的值。
|
|
4263
4693
|
* 值为空(没解析出内容)的字段整个省略,不生成空参数。
|
|
4264
4694
|
*/
|
|
4695
|
+
/**
|
|
4696
|
+
* 小e 档交给浏览器页面的字段值:只留**可见且可编辑**的那些(2026-10-21 用户裁定)。
|
|
4697
|
+
*
|
|
4698
|
+
* 依据是 `computeLinkageVisibility` 用句柄里本地 `viewLinkages` 求出来的隐藏 / 禁用,再加上字段
|
|
4699
|
+
* 自身的只读、禁填、初始隐藏(`display === false`)与布局隐藏(`hiddenCellFieldIds`)。
|
|
4700
|
+
*
|
|
4701
|
+
* 过滤的**只有值**,不改页面结构:这些字段在表单页里本来就填不进去(页面自己会拦),把值塞过去
|
|
4702
|
+
* 只会让页面报错或让用户看到"填了但没生效"。
|
|
4703
|
+
*/
|
|
4704
|
+
function editablePreviewForXiaoE(preview, fields, visibility) {
|
|
4705
|
+
const fieldById = new Map();
|
|
4706
|
+
for (const field of toArray(fields)) {
|
|
4707
|
+
const id = String(field?.id ?? '').trim();
|
|
4708
|
+
if (id)
|
|
4709
|
+
fieldById.set(id, field);
|
|
4710
|
+
}
|
|
4711
|
+
const kept = [];
|
|
4712
|
+
for (const item of toArray(preview)) {
|
|
4713
|
+
const fieldId = String(item?.fieldId ?? '').trim();
|
|
4714
|
+
if (!fieldId)
|
|
4715
|
+
continue;
|
|
4716
|
+
const field = fieldById.get(fieldId) || {};
|
|
4717
|
+
if (field.readOnly || field.disableEdit || field.display === false)
|
|
4718
|
+
continue;
|
|
4719
|
+
if (visibility.hiddenFieldIds.has(fieldId))
|
|
4720
|
+
continue;
|
|
4721
|
+
const subFormId = String(field.subFormId ?? '').trim();
|
|
4722
|
+
if (subFormId && visibility.hiddenSubforms.has(subFormId))
|
|
4723
|
+
continue;
|
|
4724
|
+
// 行键口径与 `computeLinkageVisibility` 完全一致('main' 或 `<subFormId>:<dataIndex>`,
|
|
4725
|
+
// 明细行没给行号时按第 1 行算)。
|
|
4726
|
+
const hasRow = item?.dataIndex !== undefined && item?.dataIndex !== null;
|
|
4727
|
+
const rowKey = rowKeyFor(subFormId, hasRow ? item.dataIndex : undefined);
|
|
4728
|
+
if (visibility.rowHidden[rowKey]?.has(fieldId))
|
|
4729
|
+
continue;
|
|
4730
|
+
if (visibility.rowDisabled[rowKey]?.has(fieldId))
|
|
4731
|
+
continue;
|
|
4732
|
+
kept.push(item);
|
|
4733
|
+
}
|
|
4734
|
+
return kept;
|
|
4735
|
+
}
|
|
4265
4736
|
function xiaoECustomParams(preview) {
|
|
4266
4737
|
const ordered = [];
|
|
4267
4738
|
const mainValues = new Map();
|
|
@@ -4336,9 +4807,111 @@ function renderXiaoEFillFormMarkdown(params) {
|
|
|
4336
4807
|
};
|
|
4337
4808
|
return `<browsertool>\n${JSON.stringify(payload, null, 2)}\n</browsertool>`;
|
|
4338
4809
|
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4810
|
+
/**
|
|
4811
|
+
* 把命令耗时并回 `data` 顶层 —— `createRequest` 与 `createRequestDirect` **共用这一份口径**
|
|
4812
|
+
* (2026-10-21 用户裁定:`createRequest` 也要给耗时,且与 `createRequestDirect` 一致):
|
|
4813
|
+
* - `durationMs` = 命令入口 → 结果产出;
|
|
4814
|
+
* - `apiDurations` = 每个接口(并行批次合成一个 key,取墙钟、不叠加);
|
|
4815
|
+
* - `apiSlowest` = 每个**真并行**批里谁最慢(`[{ batch, name, ms, items }]`,`ms` 是该接口自身耗时,
|
|
4816
|
+
* `items` 是批内每个成员的 `{ name, at, ms }`,按 `ms` 降序);
|
|
4817
|
+
* - `authMs` = 登录取凭据(首次会话解析)的墙钟 —— 这一段走 auth 层自己的 fetch、不经
|
|
4818
|
+
* `host.workflowRequest`,`apiDurations` 统计不到,单独给一个数才能对上账。
|
|
4819
|
+
*
|
|
4820
|
+
* `data` 不是普通对象时原样返回(被替换成字符串 / 数组的返回面一个字都不动)。
|
|
4821
|
+
*/
|
|
4822
|
+
function stampCommandTimings(envelope, timing) {
|
|
4823
|
+
const data = envelope?.data;
|
|
4824
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
4825
|
+
return envelope;
|
|
4826
|
+
const mutable = data;
|
|
4827
|
+
const { durations, slowest } = summarizeApiSpans(timing.spans);
|
|
4828
|
+
mutable.durationMs = Date.now() - timing.startedAt;
|
|
4829
|
+
mutable.apiDurations = durations;
|
|
4830
|
+
mutable.apiSlowest = slowest;
|
|
4831
|
+
mutable.authMs = timing.authMs;
|
|
4832
|
+
return envelope;
|
|
4833
|
+
}
|
|
4834
|
+
/**
|
|
4835
|
+
* 取第一个**非空**字符串。
|
|
4836
|
+
*
|
|
4837
|
+
* 系统字段的兜底链原来是 `a ?? b ?? c`,但 `??` 挡不住空串:`systemFields.defaults.requestName`
|
|
4838
|
+
* 的构造是 `runtime.default_request_name || ''`,一旦 OA 没给默认标题,这条链就停在 `''` 上、
|
|
4839
|
+
* 后面的 `context.workflow.name` / `'新建流程'` 全被短路掉。提示词改成「用户没说就不传 key」之后,
|
|
4840
|
+
* 这条链路会真的走到(以前是智能体拿默认值补的),所以按"跳过空串"取值。
|
|
4841
|
+
*/
|
|
4842
|
+
function firstNonEmptyString(values) {
|
|
4843
|
+
for (const value of values) {
|
|
4844
|
+
const text = String(value ?? '').trim();
|
|
4845
|
+
if (text)
|
|
4846
|
+
return text;
|
|
4847
|
+
}
|
|
4848
|
+
return '';
|
|
4849
|
+
}
|
|
4850
|
+
/**
|
|
4851
|
+
* 系统字段「文本口径」归一(2026-10-22 用户裁定):提示词里紧急程度 / 密级已改成**文本字段**,
|
|
4852
|
+
* 智能体给的是用户原话(「紧急」「机密」「加急」…),而服务端要的是 `levelOptions[].code` /
|
|
4853
|
+
* `secLevelOptions[].id`。这里做一次「原话 → 编码」的映射:
|
|
4854
|
+
*
|
|
4855
|
+
* 1. 原样命中编码(`code` / `id`)⇒ 用编码;
|
|
4856
|
+
* 2. 命中选项**名称**(忽略空白与大小写)⇒ 换成编码;
|
|
4857
|
+
* 3. 兜底「宽松包含」(用户说「加急」、选项名「紧急加急」)⇒ 换成编码;
|
|
4858
|
+
* 4. 三层都命中不了 ⇒ **原样透传**:不静默丢掉用户说的内容,也不编造编码。
|
|
4859
|
+
*
|
|
4860
|
+
* 选项清单来自 `loadMinorInfo`;取不到(小e 档、老句柄)时清单为空 ⇒ 全部走第 4 条,
|
|
4861
|
+
* 行为与改造前完全一致(改造前就是原样透传)。
|
|
4862
|
+
*/
|
|
4863
|
+
function normalizeSystemChoice(value, options, codeKey) {
|
|
4864
|
+
const text = String(value ?? '').trim();
|
|
4865
|
+
if (!text)
|
|
4866
|
+
return '';
|
|
4867
|
+
const list = (Array.isArray(options) ? options : []).filter((item) => item && typeof item === 'object');
|
|
4868
|
+
const norm = (raw) => String(raw ?? '').replace(/\s+/gu, '').toLowerCase();
|
|
4869
|
+
const target = norm(text);
|
|
4870
|
+
if (!target)
|
|
4871
|
+
return '';
|
|
4872
|
+
const codeOf = (item) => String(item?.[codeKey] ?? '').trim();
|
|
4873
|
+
for (const item of list) {
|
|
4874
|
+
const code = codeOf(item);
|
|
4875
|
+
if (code && norm(code) === target)
|
|
4876
|
+
return code;
|
|
4877
|
+
}
|
|
4878
|
+
for (const item of list) {
|
|
4879
|
+
const code = codeOf(item);
|
|
4880
|
+
if (code && norm(item?.name) === target)
|
|
4881
|
+
return code;
|
|
4882
|
+
}
|
|
4883
|
+
for (const item of list) {
|
|
4884
|
+
const name = norm(item?.name);
|
|
4885
|
+
const code = codeOf(item);
|
|
4886
|
+
if (name && code && (name.includes(target) || target.includes(name)))
|
|
4887
|
+
return code;
|
|
4888
|
+
}
|
|
4889
|
+
return text;
|
|
4890
|
+
}
|
|
4891
|
+
/**
|
|
4892
|
+
* `createRequest` 的耗时外壳:建计时通道 + 量登录耗时 + 把三个耗时键并回结果。
|
|
4893
|
+
*
|
|
4894
|
+
* 参数明显不合法(`action` 不对 / 没 `context`)时**不先登录** —— 那两种情况内层会直接抛
|
|
4895
|
+
* 参数错,为一个注定失败的调用白走一趟统一登录不值当;真正的报错措辞仍由内层给出。
|
|
4896
|
+
*/
|
|
4897
|
+
async function commandCreateRequest(args, env, options = {}) {
|
|
4898
|
+
const startedAt = Date.now();
|
|
4899
|
+
const spans = options.apiSpans ?? [];
|
|
4900
|
+
// `createRequestDirect` 已经把 host 包过一层(它的片段数组覆盖到表单载入那一段)⇒ 这里不能再包,
|
|
4901
|
+
// 否则每个请求会被记两遍(key 里会出现重名)。
|
|
4902
|
+
const timedEnv = options.apiSpans ? env : { ...env, host: withApiTiming(env.host, spans) };
|
|
4903
|
+
const action = String(args.action ?? '').trim();
|
|
4904
|
+
let authMs = 0;
|
|
4905
|
+
if ((action === 'draft' || action === 'submit') && String(args.context ?? '')) {
|
|
4906
|
+
// 会话是惰性的:内层的 `verifyContextLease` 本来也会取一次,这里只是把它**提前并单独计时**,
|
|
4907
|
+
// 拿到的仍是同一份缓存,不额外多发请求。
|
|
4908
|
+
const authStartedAt = Date.now();
|
|
4909
|
+
await timedEnv.host.getSession();
|
|
4910
|
+
authMs = Date.now() - authStartedAt;
|
|
4911
|
+
}
|
|
4912
|
+
const envelope = await runCreateRequest(args, timedEnv, options);
|
|
4913
|
+
return stampCommandTimings(envelope, { startedAt, spans, authMs });
|
|
4914
|
+
}
|
|
4342
4915
|
/**
|
|
4343
4916
|
* `workflow.createRequest` —— **一条命令**完成「字段解析 → 联动 → 必填校验 → 保存 → 发起」。
|
|
4344
4917
|
*
|
|
@@ -4350,7 +4923,7 @@ function renderXiaoEFillFormMarkdown(params) {
|
|
|
4350
4923
|
* 返回**不消费**句柄,于是模型补齐字段后可以带**同一个 context** 重发,不必回头重跑 createForm;
|
|
4351
4924
|
* 一旦消费,就只能按句柄状态分流(`ALREADY_APPLIED` / 结果不确定 / 并发占用)。
|
|
4352
4925
|
*/
|
|
4353
|
-
async function
|
|
4926
|
+
async function runCreateRequest(args, env, options = {}) {
|
|
4354
4927
|
const action = String(args.action ?? '').trim();
|
|
4355
4928
|
if (action !== 'draft' && action !== 'submit') {
|
|
4356
4929
|
throw workflowValidationError('action_required', 'action 必填,且只能是 draft(仅存草稿)或 submit(提交审批)');
|
|
@@ -4395,17 +4968,22 @@ async function commandCreateRequest(args, env) {
|
|
|
4395
4968
|
* (`buildCreateFormData`,那边的联动结果要用来算字段的必填 / 可编辑清单);这里两者都不跑 ——
|
|
4396
4969
|
* 本档既不做必填判定、也不发写请求。非小e 环境行为不变。
|
|
4397
4970
|
*/
|
|
4398
|
-
const isXiaoE = hasXiaoEEnv(env.ctx.env);
|
|
4971
|
+
const isXiaoE = hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
|
|
4399
4972
|
const linkageDetails = isXiaoE
|
|
4400
4973
|
? []
|
|
4401
|
-
: await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false);
|
|
4974
|
+
: await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false, context.defaults || []);
|
|
4402
4975
|
// 明细默认值无行号,而判缺失/写载荷都按行号索引 ⇒ 按用户已建行补齐缺口(V12.1.35,
|
|
4403
4976
|
// 详见 fillDetailDefaultsByRow 注释)。放在联动之后合并,联动带出的值仍然优先。
|
|
4404
4977
|
const merged2 = fillDetailDefaultsByRow(mergeLinkageDetails(merged, linkageDetails), context.defaults || [], userDetailRows(entries));
|
|
4405
4978
|
const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
|
|
4406
4979
|
const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
|
|
4407
4980
|
const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = isXiaoE
|
|
4408
|
-
?
|
|
4981
|
+
? (options.xiaoEViewLinkageFilter
|
|
4982
|
+
// 2026-10-21 用户裁定(`createRequestDirect` 这一档):小e 档**照跑显示属性联动**,只是不跑字段联动
|
|
4983
|
+
// —— 它要据此把页面本来就填不进去的字段(隐藏 / 禁用)从要填的值里剔掉。
|
|
4984
|
+
// 第三参传 `[]`(本次没有联动带出的值)时,求值只用表单默认值 + 本次入参 + 系统字段值。
|
|
4985
|
+
? computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], [], context.systemFieldValues, fields)
|
|
4986
|
+
: emptyLinkageVisibility())
|
|
4409
4987
|
: computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues, fields);
|
|
4410
4988
|
const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
|
|
4411
4989
|
if (initialHidden.size) {
|
|
@@ -4420,8 +4998,13 @@ async function commandCreateRequest(args, env) {
|
|
|
4420
4998
|
const missing = missingRequiredFields(fields, entries, merged2, hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled, hiddenFieldIds, compsFieldIds, layoutSubformIds);
|
|
4421
4999
|
const sysPermissions = (context.systemFields && context.systemFields.permissions) || {};
|
|
4422
5000
|
const sysDefaults = (context.systemFields && context.systemFields.defaults) || {};
|
|
4423
|
-
|
|
4424
|
-
|
|
5001
|
+
// 选项清单的键名随版本变过一次:`createForm` 句柄里是 snake_case,别处可能给 camelCase,两种都收
|
|
5002
|
+
// (与下方 `systemFilledRows` 同一口径)。
|
|
5003
|
+
const levelOptions = (context.systemFields && (context.systemFields.level_options || context.systemFields.levelOptions)) || [];
|
|
5004
|
+
const secLevelOptions = (context.systemFields && (context.systemFields.sec_level_options || context.systemFields.secLevelOptions)) || [];
|
|
5005
|
+
// 紧急程度 / 密级:提示词里是文本字段 ⇒ 入参可能是用户原话,先归一成编码(取不到选项就原样透传)。
|
|
5006
|
+
const requestLevelArg = normalizeSystemChoice(args.requestLevel, levelOptions, 'code');
|
|
5007
|
+
const secLevelArg = normalizeSystemChoice(args.secLevel, secLevelOptions, 'id');
|
|
4425
5008
|
const requestNameValue = String(args.requestName ?? sysDefaults.requestName ?? '').trim();
|
|
4426
5009
|
const requestLevelValue = requestLevelArg || String(sysDefaults.requestLevel ?? '').trim();
|
|
4427
5010
|
const secLevelValue = secLevelArg || String(sysDefaults.secLevel ?? '').trim();
|
|
@@ -4487,8 +5070,8 @@ async function commandCreateRequest(args, env) {
|
|
|
4487
5070
|
secLevel: secLevelValue,
|
|
4488
5071
|
permissions: sysPermissions,
|
|
4489
5072
|
// 老句柄可能没有这两个选项清单(键名也随版本变过一次),取不到就退化成"不列系统字段"。
|
|
4490
|
-
levelOptions
|
|
4491
|
-
secLevelOptions
|
|
5073
|
+
levelOptions,
|
|
5074
|
+
secLevelOptions,
|
|
4492
5075
|
});
|
|
4493
5076
|
if (valueAmbiguities.length) {
|
|
4494
5077
|
return workflowSuccess('workflow.createRequest', {
|
|
@@ -4518,17 +5101,16 @@ async function commandCreateRequest(args, env) {
|
|
|
4518
5101
|
* 渲染档(2026-10-15 用户裁定):`finalMarkdown` **整体就是一段 `<browsertool>` 标签块**;本轮回复正文
|
|
4519
5102
|
* = 这段内容本身(从 `<browsertool>` 到 `</browsertool>`),原样输出、**不要包在代码块里**。
|
|
4520
5103
|
*
|
|
4521
|
-
* **stdout
|
|
4522
|
-
*
|
|
4523
|
-
* `
|
|
4524
|
-
*
|
|
4525
|
-
*
|
|
5104
|
+
* **stdout 形态**(2026-10-21 用户裁定,**改口径**):这一档同样**走 JSON 信封**,标签块放在
|
|
5105
|
+
* `data.finalMarkdown` 里 —— 与 `createRequestDirect` 统一,模型侧只有一条读法;`durationMs` /
|
|
5106
|
+
* `apiDurations` / `apiSlowest` / `authMs` 四个耗时键也因此在这一档可见。原来的「stdout 直接就是标签块、不套
|
|
5107
|
+
* 信封」已作废(`shortcuts/workflow/index.ts` 的直出通道随之删除)。
|
|
5108
|
+
* 所以下面的 `message` 要明确告诉模型:从 `data.finalMarkdown` 取内容、原样作为本轮回复正文。
|
|
4526
5109
|
*
|
|
4527
|
-
*
|
|
4528
|
-
*
|
|
4529
|
-
*
|
|
4530
|
-
*
|
|
4531
|
-
* `isXiaoE`(同一判据,只求值一次)。
|
|
5110
|
+
* 判据:桌面客户端可由 Agent 根据宿主明确上下文传入全局参数 `--runtime-agent xiaoe`;
|
|
5111
|
+
* 传入环境中存在任意 `XIAOE_*` 变量时也直接判定为小e环境。
|
|
5112
|
+
* 历史会话和本地文件本身不参与宿主身份判断。
|
|
5113
|
+
* 本处复用上面算好的 `isXiaoE`(同一判据,只求值一次)。
|
|
4532
5114
|
*
|
|
4533
5115
|
* 本档**不跑字段联动、也不跑显示属性联动**(2026-10-16 用户裁定,见上方 `isXiaoE` 的注释):
|
|
4534
5116
|
* 所以这里既没有「字段联动带出」的提示,`hiddenSubforms` / `rowExtraRequired` / `rowDisabled`
|
|
@@ -4538,16 +5120,22 @@ async function commandCreateRequest(args, env) {
|
|
|
4538
5120
|
// `src` 要的是绝对地址:句柄里带走的那份 baseUrl 为准(与句柄同源,避免"换了域名后旧句柄拼出别的站"),
|
|
4539
5121
|
// 旧版本句柄可能没带 ⇒ 回落到当前会话的 baseUrl。
|
|
4540
5122
|
const xiaoEBaseUrl = String(context.baseUrl ?? '') || (await getSessionContext(env)).baseUrl;
|
|
5123
|
+
// 交给页面的值:`createRequestDirect` 这一档先按显示属性联动过滤掉不可编辑字段(见上方 options 注释),
|
|
5124
|
+
// 其余入口(`createRequest`)原样传 `preview`。
|
|
5125
|
+
const fillPreview = options.xiaoEViewLinkageFilter
|
|
5126
|
+
? editablePreviewForXiaoE(preview, fields, { hiddenSubforms, rowHidden, rowDisabled, hiddenFieldIds })
|
|
5127
|
+
: preview;
|
|
4541
5128
|
return workflowSuccess('workflow.createRequest', {
|
|
4542
5129
|
status: 'BROWSER_FILL',
|
|
4543
5130
|
contextId: token,
|
|
4544
|
-
|
|
5131
|
+
// 本档只有一种输出形态(JSON 信封),指引也就只有一条:从 `data.finalMarkdown` 取内容。
|
|
5132
|
+
message: '当前是小e环境:本次不向服务端写入,改由浏览器页面完成填单。本档输出的是 JSON 信封(额外带 durationMs / apiDurations / authMs 耗时),`data.finalMarkdown` 就是那段 <browsertool> 标签块(其中的换行在 JSON 里以 \\n 转义)。把 `data.finalMarkdown` 的内容原样作为本轮回复正文:从 <browsertool> 到 </browsertool>、换行还原成真实换行,不要包在代码块里、不要加任何前后说明;JSON 信封本身不要回给用户。',
|
|
4545
5133
|
workflow: context.workflow,
|
|
4546
5134
|
finalMarkdown: renderXiaoEFillFormMarkdown({
|
|
4547
5135
|
baseUrl: xiaoEBaseUrl,
|
|
4548
5136
|
title: String(context.workflow?.name ?? ''),
|
|
4549
5137
|
workflowId: String(context.workflow?.id ?? context.runtime?.workflow_id ?? ''),
|
|
4550
|
-
customParams: xiaoECustomParams(
|
|
5138
|
+
customParams: xiaoECustomParams(fillPreview),
|
|
4551
5139
|
}),
|
|
4552
5140
|
});
|
|
4553
5141
|
}
|
|
@@ -4557,11 +5145,18 @@ async function commandCreateRequest(args, env) {
|
|
|
4557
5145
|
// 同时给一份可直接原样粘贴的渲染档(V12.1.102):结论 + 主表「已填内容」两列表(含流程标题与
|
|
4558
5146
|
// 表单默认值,2026-10-14 起)+ 明细 Excel 列 + 待补必填项清单。模型不必自己拼这些表格,也不必再
|
|
4559
5147
|
// 遍历 `resolvedValues` 拼待补清单;因此这里**不再返回** `resolvedValues`(渲染素材只在命令内部用)。
|
|
4560
|
-
const pendingRequestName =
|
|
5148
|
+
const pendingRequestName = firstNonEmptyString([
|
|
5149
|
+
args.requestName,
|
|
5150
|
+
sysDefaults.requestName,
|
|
5151
|
+
context.runtime.default_request_name,
|
|
5152
|
+
]);
|
|
5153
|
+
// ★ 2026-10-20 用户裁定:**有 `finalMarkdown` 的状态不再返回 `message`**。
|
|
5154
|
+
// 这条路一定有渲染档(`renderCreatePendingMarkdown` 至少输出结论句 + 待补清单),
|
|
5155
|
+
// 而 `message` 说的是同一件事、口气还是"对模型下指令"("带同一 context 重发 action=submit"),
|
|
5156
|
+
// 模型看到就会在 `finalMarkdown` 之外多输出一段、顺带把内部机制漏给用户。
|
|
4561
5157
|
return workflowSuccess('workflow.createRequest', {
|
|
4562
5158
|
status: 'NEEDS_INPUT',
|
|
4563
5159
|
contextId: token,
|
|
4564
|
-
message: '缺少必填字段,本次未发起流程。请把下面这些字段一次问齐后,带同一 context 重发 action=submit。',
|
|
4565
5160
|
missingFields: missingAll,
|
|
4566
5161
|
finalMarkdown: renderCreatePendingMarkdown({ missingFields: missingAll, linkageResult },
|
|
4567
5162
|
// 标题行按"当前还没填到标题"的口径取(可能为空 ⇒ 不出现该行),与落地档的 `requestName` 兜底分开。
|
|
@@ -4575,7 +5170,13 @@ async function commandCreateRequest(args, env) {
|
|
|
4575
5170
|
...folderPayload,
|
|
4576
5171
|
});
|
|
4577
5172
|
}
|
|
4578
|
-
const requestName =
|
|
5173
|
+
const requestName = firstNonEmptyString([
|
|
5174
|
+
args.requestName,
|
|
5175
|
+
sysDefaults.requestName,
|
|
5176
|
+
context.runtime.default_request_name,
|
|
5177
|
+
context.workflow.name,
|
|
5178
|
+
'新建流程',
|
|
5179
|
+
]);
|
|
4579
5180
|
if (!requestName)
|
|
4580
5181
|
throw workflowValidationError('input_invalid', 'request_name 不能为空');
|
|
4581
5182
|
const inputFieldIds = [...new Set(entries.map((e) => String((e.field && e.field.id) ?? '')).filter(Boolean))];
|
|
@@ -4779,9 +5380,234 @@ async function commandCreateRequest(args, env) {
|
|
|
4779
5380
|
// 标题行用**本次真正落库的** `requestName`(与 `prepared.requestName` 同源),不再是"可能为空"的口径。
|
|
4780
5381
|
if (['CREATED', 'CREATED_SUBMITTED', 'CREATED_SUBMIT_FAILED'].includes(String(result.status))) {
|
|
4781
5382
|
result.finalMarkdown = renderCreateResultMarkdown(result, [...filledTitleRows(requestName), ...filledFieldRows]);
|
|
5383
|
+
// ★ 渲染档的**结论句就是 `message`**(`renderCreateResultMarkdown` 第一行取的它),所以只能在
|
|
5384
|
+
// 渲染之后再摘键——不是不用它,是**不单独返回它**:返回面里 `message` 与 `finalMarkdown` 说的
|
|
5385
|
+
// 是同一句话,模型只会挑一个输出,留着就是给自己制造"两个声音"(2026-10-20 用户裁定)。
|
|
5386
|
+
delete result.message;
|
|
4782
5387
|
}
|
|
4783
5388
|
return workflowSuccess('workflow.createRequest', result);
|
|
4784
5389
|
}
|
|
5390
|
+
// ---------------------------------------------------------------------------
|
|
5391
|
+
// 接口耗时(`workflow.createRequestDirect`)
|
|
5392
|
+
// ---------------------------------------------------------------------------
|
|
5393
|
+
/**
|
|
5394
|
+
* 接口**简称**:`path` → 模型看得懂的短名。
|
|
5395
|
+
*
|
|
5396
|
+
* 只登记这条命令真会打到的接口(解析工作流 / 载入表单 / 联动 / 写库 / 校验 / 浏览按钮);
|
|
5397
|
+
* 没登记的按路径最后一段兜底,不会因为漏登记就把计时丢掉。
|
|
5398
|
+
*/
|
|
5399
|
+
const API_SHORT_NAMES = {
|
|
5400
|
+
[FORM_ENDPOINTS.workflowCreateList]: 'newFlowList',
|
|
5401
|
+
[FORM_ENDPOINTS.loadBase]: 'loadBase',
|
|
5402
|
+
[FORM_ENDPOINTS.loadFrame]: 'loadFrame',
|
|
5403
|
+
[FORM_ENDPOINTS.loadMinor]: 'loadMinor',
|
|
5404
|
+
[FORM_ENDPOINTS.formLayout]: 'formLayout',
|
|
5405
|
+
[FORM_ENDPOINTS.tenantSetting]: 'tenantSetting',
|
|
5406
|
+
[FORM_ENDPOINTS.fdLinkage]: 'fdLinkage',
|
|
5407
|
+
[FORM_ENDPOINTS.save]: 'saveFormData',
|
|
5408
|
+
[FORM_ENDPOINTS.create]: 'flowCreate',
|
|
5409
|
+
[FORM_ENDPOINTS.flowSubmit]: 'flowSubmit',
|
|
5410
|
+
[FORM_ENDPOINTS.signInputInfo]: 'signInputInfo',
|
|
5411
|
+
[FORM_ENDPOINTS.submit]: 'submitRequest',
|
|
5412
|
+
[FORM_ENDPOINTS.nameToId]: 'relateName2Id',
|
|
5413
|
+
[FORM_ENDPOINTS.verifyNodeFieldByList]: 'verifyNodeField',
|
|
5414
|
+
[FORM_ENDPOINTS.conformanceSet]: 'conformanceSet',
|
|
5415
|
+
[FORM_ENDPOINTS.verificationRequestData]: 'verificationRequestData',
|
|
5416
|
+
[FORM_ENDPOINTS.verificationConditionRule]: 'verificationConditionRule',
|
|
5417
|
+
[WORKFLOW_ENDPOINTS.MINE_TREE]: 'mineTree',
|
|
5418
|
+
[WORKFLOW_ENDPOINTS.MEMORY_PROMPT]: 'memoryPrompt',
|
|
5419
|
+
[WORKFLOW_ENDPOINTS.WORKFLOW_PATH_BROWSER]: 'workflowPathBrowser',
|
|
5420
|
+
[WORKFLOW_ENDPOINTS.RESOURCE_BROWSER]: 'resourceBrowser',
|
|
5421
|
+
[WORKFLOW_ENDPOINTS.HRM_RESOURCE_BROWSER]: 'hrmResourceBrowser',
|
|
5422
|
+
[WORKFLOW_ENDPOINTS.DEPT_BROWSER]: 'deptBrowser',
|
|
5423
|
+
[WORKFLOW_ENDPOINTS.SUBCOMPANY_BROWSER]: 'subcompanyBrowser',
|
|
5424
|
+
[WORKFLOW_ENDPOINTS.GROUP_BROWSER]: 'groupBrowser',
|
|
5425
|
+
[WORKFLOW_ENDPOINTS.ROLE_BROWSER]: 'roleBrowser',
|
|
5426
|
+
[WORKFLOW_ENDPOINTS.POSITION_BROWSER]: 'positionBrowser',
|
|
5427
|
+
[WORKFLOW_ENDPOINTS.NODE_BROWSER]: 'nodeBrowser',
|
|
5428
|
+
[WORKFLOW_ENDPOINTS.FIELD_OPTION_BROWSER]: 'fieldOptionBrowser',
|
|
5429
|
+
[WORKFLOW_ENDPOINTS.REQUEST_DETAIL]: 'getWorkflowRequest',
|
|
5430
|
+
// 智能填参(非 workflow 前缀,靠 `aiappuniquekey` 头定位智能体应用,见 chatscene.ts)
|
|
5431
|
+
'/api/ai/application/chat/chatScene': 'chatScene',
|
|
5432
|
+
};
|
|
5433
|
+
function apiShortName(path) {
|
|
5434
|
+
const text = String(path ?? '');
|
|
5435
|
+
const known = API_SHORT_NAMES[text];
|
|
5436
|
+
if (known)
|
|
5437
|
+
return known;
|
|
5438
|
+
const segments = text.split('/').filter(Boolean);
|
|
5439
|
+
return segments.length ? segments[segments.length - 1] : text || 'unknown';
|
|
5440
|
+
}
|
|
5441
|
+
/**
|
|
5442
|
+
* 给 host 套一层**逐接口计时**(只包请求,不改返回、不吞异常)。
|
|
5443
|
+
*
|
|
5444
|
+
* `finally` 里落片段:请求失败(超时 / 业务错误 / 抛错)同样要记 —— 恰恰是这类请求最需要看到耗时。
|
|
5445
|
+
*/
|
|
5446
|
+
function withApiTiming(host, spans) {
|
|
5447
|
+
const record = async (path, run) => {
|
|
5448
|
+
const start = Date.now();
|
|
5449
|
+
try {
|
|
5450
|
+
return await run();
|
|
5451
|
+
}
|
|
5452
|
+
finally {
|
|
5453
|
+
spans.push({ name: apiShortName(path), start, end: Date.now() });
|
|
5454
|
+
}
|
|
5455
|
+
};
|
|
5456
|
+
return {
|
|
5457
|
+
...host,
|
|
5458
|
+
workflowRequest: (request) => record(request?.path, () => host.workflowRequest(request)),
|
|
5459
|
+
workflowBinaryRequest: (request) => record(request?.path, () => host.workflowBinaryRequest(request)),
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
/**
|
|
5463
|
+
* 把逐次请求的片段收成「一条命令里每个接口花了多久」。
|
|
5464
|
+
*
|
|
5465
|
+
* 口径(2026-10-21 用户裁定):
|
|
5466
|
+
* - **并行**(时间区间重叠)的接口合成**一个 key**:各简称用 `_` 拼起来(多个时按字母序,保证同一批
|
|
5467
|
+
* 接口每次输出同一个 key),**值取这一批的墙钟耗时**(最早开始 → 最晚结束),**不是**把批内每个
|
|
5468
|
+
* 接口的耗时相加;
|
|
5469
|
+
* - 串行接口各自一个 key,值就是它自己的耗时;
|
|
5470
|
+
* - 同一个 key 出现多次(同一接口被调了两轮)按累加计 —— 那确实是两笔独立开销。
|
|
5471
|
+
*
|
|
5472
|
+
* 重叠判定用「区间扫描」而不是推断代码里的并发结构:只要两个请求在时间上真重叠就算一批,
|
|
5473
|
+
* 批量接口内部再起并发也照样能反映出来。
|
|
5474
|
+
*
|
|
5475
|
+
* ★ 2026-10-22 用户裁定:**并行批还要记「谁最慢」**(`apiSlowest`)。批的墙钟只说明"这一批花了多久",
|
|
5476
|
+
* 看不出是哪个接口把它拖住的;而优化恰恰只对最慢的那个有用。判定口径:
|
|
5477
|
+
* - 只对**真并行**(批内 ≥ 2 个请求)的批产出一条,单一请求的批不产 —— 那种情况下"谁最慢"就是它自己,
|
|
5478
|
+
* 记了只是噪声;
|
|
5479
|
+
* - 批内**全是同一个接口**时也不产(如 5 个并发的 `getWorkflowRequest` 取历史详情):请求都一样,
|
|
5480
|
+
* "谁最慢"没有区分意义,只有批的墙钟有用(2026-10-22 用户第二轮裁定);
|
|
5481
|
+
* - `ms` 取该接口**自身**耗时(`end - start`),不是批墙钟;
|
|
5482
|
+
* - 每条另带 `items`:批内**每个**成员的 `{name, at, ms}`(`at` 相对批起点),按 `ms` 降序。
|
|
5483
|
+
* 2026-10-26 用户要求:只看最慢项判断不出「优化它值不值」——若批内多个接口耗时接近,
|
|
5484
|
+
* 砍掉最慢那个的收益有限,得看整条关键路径的分布;
|
|
5485
|
+
* - 顺序=批的时间顺序,与 `apiDurations` 的 key 一一对应。
|
|
5486
|
+
*/
|
|
5487
|
+
function summarizeApiSpans(spans) {
|
|
5488
|
+
const sorted = [...spans].sort((a, b) => (a.start - b.start) || (a.end - b.end));
|
|
5489
|
+
const groups = [];
|
|
5490
|
+
let current = [];
|
|
5491
|
+
let reach = Number.NEGATIVE_INFINITY;
|
|
5492
|
+
for (const span of sorted) {
|
|
5493
|
+
// 端点相接(`start === reach`)按串行算:前一个已经返回,后一个才开始。
|
|
5494
|
+
if (current.length && span.start < reach) {
|
|
5495
|
+
current.push(span);
|
|
5496
|
+
reach = Math.max(reach, span.end);
|
|
5497
|
+
}
|
|
5498
|
+
else {
|
|
5499
|
+
if (current.length)
|
|
5500
|
+
groups.push(current);
|
|
5501
|
+
current = [span];
|
|
5502
|
+
reach = span.end;
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
if (current.length)
|
|
5506
|
+
groups.push(current);
|
|
5507
|
+
const durations = {};
|
|
5508
|
+
const slowest = [];
|
|
5509
|
+
for (const group of groups) {
|
|
5510
|
+
const names = group.map((item) => item.name);
|
|
5511
|
+
const key = names.length === 1 ? names[0] : [...names].sort().join('_');
|
|
5512
|
+
const cost = Math.max(0, Math.round(Math.max(...group.map((item) => item.end)) - Math.min(...group.map((item) => item.start))));
|
|
5513
|
+
durations[key] = (durations[key] ?? 0) + cost;
|
|
5514
|
+
// 批内全是同一个接口 ⇒ 不产最慢项("5 个 getWorkflowRequest 里谁最慢"没有区分意义)。
|
|
5515
|
+
if (group.length > 1 && new Set(names).size > 1) {
|
|
5516
|
+
const origin = Math.min(...group.map((item) => item.start));
|
|
5517
|
+
// 批内逐个成员都留一条(`at` 相对批起点,`ms` 自身耗时),按耗时降序;并列取先开始的
|
|
5518
|
+
// (`at` 升序),保证同一份输入每次输出一致。`name`/`ms` 就取这一项。
|
|
5519
|
+
const items = group
|
|
5520
|
+
.map((item) => ({
|
|
5521
|
+
name: item.name,
|
|
5522
|
+
at: Math.max(0, Math.round(item.start - origin)),
|
|
5523
|
+
ms: Math.max(0, Math.round(item.end - item.start)),
|
|
5524
|
+
}))
|
|
5525
|
+
.sort((a, b) => (b.ms - a.ms) || (a.at - b.at));
|
|
5526
|
+
slowest.push({ batch: key, name: items[0].name, ms: items[0].ms, items });
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return { durations, slowest };
|
|
5530
|
+
}
|
|
5531
|
+
/**
|
|
5532
|
+
* `workflow.createRequestDirect` —— **取数命令**(2026-10-22 用户裁定,语义已变)。
|
|
5533
|
+
*
|
|
5534
|
+
* 原先是「载入表单 + 智能填参 + 发起」一步到位;现在**只返回填参原料**,由调用方(桌面端 agent
|
|
5535
|
+
* 自己的模型)拼好 `data` 再调 `createRequest`。理由:那一侧本身就是模型,再往 CLI 里跑一趟
|
|
5536
|
+
* `chatScene` 只是白白多一次模型调用与一轮上下文。
|
|
5537
|
+
*
|
|
5538
|
+
* ## 五条口径
|
|
5539
|
+
* 1. **不调 `chatScene`**:它复用的取数段(`prepareCreateFormPayload`)自 2026-10-23 起就不再碰
|
|
5540
|
+
* 这条链路(`createForm` 已不接 `userQuestion`),本命令无需再做什么就能保证「一个请求都不发」。
|
|
5541
|
+
* 2. **不发起、不写库**:不调用 `commandCreateRequest`,一条 `saveFormData` / `flow/create` 都不会发。
|
|
5542
|
+
* 这条命令的 `risk` 因此从 `high-risk-write` 降为 `read`。
|
|
5543
|
+
* 3. **只返回填参原料**:`fields`(含 `initialValue`)、`historyRequestData`、`userMemory`
|
|
5544
|
+
* (=该工作流的记忆 `workflowMemory`),外加 `status` / `workflow` 两个元信息。
|
|
5545
|
+
* 4. **不返回系统字段**:`systemFields` / `systemFieldWarning` 整块不出现(标题/紧急程度/密级的默认值
|
|
5546
|
+
* 与选项清单都不给)。
|
|
5547
|
+
* 5. **输出降级**(按 20,000 **字符**,与 `createForm` 同一个宿主口径):
|
|
5548
|
+
* 全量超线 ⇒ `historyRequestData` 留空数组(键还在);仍超 ⇒ `userMemory` 留空串(键还在);
|
|
5549
|
+
* **`fields` 自己也超线时不再裁**(宁大不丢)—— 它是唯一不可省的,砍了这条命令就没有意义。
|
|
5550
|
+
*
|
|
5551
|
+
* ## 耗时
|
|
5552
|
+
* 与 `createForm` 完全同口径,全部收在 `timing` 一个桶里:`durationMs`(整条命令)/
|
|
5553
|
+
* `apiDurations`(按并行批合并的接口耗时)/ `apiSlowest`(每个真并行批的最慢项,`items` 里带批内
|
|
5554
|
+
* 每个接口各自的耗时)/
|
|
5555
|
+
* `authMs`(会话解析)。本命令不会出现 `fillDurationMs` —— 那一项统计的是 `chatScene`,不再有这一段。
|
|
5556
|
+
*/
|
|
5557
|
+
async function commandCreateRequestDirect(args, env) {
|
|
5558
|
+
const startedAt = Date.now();
|
|
5559
|
+
// 计时从**命令入口**开始算(含会话解析、工作流消歧、表单载入)。
|
|
5560
|
+
const spans = [];
|
|
5561
|
+
const timedEnv = { ...env, host: withApiTiming(env.host, spans) };
|
|
5562
|
+
// 取数段本身已经不碰 `chatScene`(`createForm` 自 2026-10-23 起不再接 `userQuestion`),
|
|
5563
|
+
// 这里不必再置空任何入参。
|
|
5564
|
+
const { payload, authMs } = await prepareCreateFormPayload(args, timedEnv);
|
|
5565
|
+
const contextId = String(payload.contextId ?? '');
|
|
5566
|
+
if (!contextId) {
|
|
5567
|
+
// `prepareCreateFormPayload` 正常返回时 contextId 必然存在;这里只用来判断
|
|
5568
|
+
// "表单到底载入成功没有"(句柄本身不再交给调用方)。
|
|
5569
|
+
throw workflowValidationError('context_missing', '表单载入未产出可用句柄,请重试');
|
|
5570
|
+
}
|
|
5571
|
+
const { durations: apiDurations, slowest: apiSlowest } = summarizeApiSpans(spans);
|
|
5572
|
+
// 返回面只留「模型真正要用的」:状态 + 工作流 + 填参三样 + 一个耗时桶。
|
|
5573
|
+
// 句柄类(contextId/requestId/requestName)、`form`、`continuation`、`requiredInfoWarning` 全部不出
|
|
5574
|
+
// —— 本命令不发起,这些字段没有下游(2026-10-22 用户裁定)。`contextId` 仍在上面的守卫里用于
|
|
5575
|
+
// 判断"表单到底载入成功没有",只是不再交出去。
|
|
5576
|
+
const data = {
|
|
5577
|
+
status: payload.status,
|
|
5578
|
+
workflow: payload.workflow,
|
|
5579
|
+
fields: payload.fields,
|
|
5580
|
+
historyRequestData: payload.historyRequestData,
|
|
5581
|
+
userMemory: String(payload.workflowMemory ?? ''),
|
|
5582
|
+
};
|
|
5583
|
+
// 判线必须把「稍后才求得出」的耗时键先按位数占住(与 `createForm` 同一个哨兵口径,宁可高估),
|
|
5584
|
+
// 否则边界上的表单会被判成"没超"、然后在宿主那儿被真截断。
|
|
5585
|
+
const reserve = {
|
|
5586
|
+
timing: {
|
|
5587
|
+
durationMs: DURATION_RESERVE_SENTINEL,
|
|
5588
|
+
apiDurations,
|
|
5589
|
+
apiSlowest,
|
|
5590
|
+
authMs: DURATION_RESERVE_SENTINEL,
|
|
5591
|
+
},
|
|
5592
|
+
};
|
|
5593
|
+
const overLine = () => finalStdoutChars({ ...data, ...reserve }, 'workflow.createRequestDirect') > CREATE_FORM_STDOUT_SAFE_CHARS;
|
|
5594
|
+
// 两级降级:先砍历史(留空 key)→ 再砍记忆(留空 key)。`fields` 不参与裁剪。
|
|
5595
|
+
if (overLine()) {
|
|
5596
|
+
data.historyRequestData = [];
|
|
5597
|
+
if (overLine())
|
|
5598
|
+
data.userMemory = '';
|
|
5599
|
+
}
|
|
5600
|
+
return workflowSuccess('workflow.createRequestDirect', {
|
|
5601
|
+
...data,
|
|
5602
|
+
// 全部耗时口径收进一个桶:总耗时、逐接口耗时、并行批最慢项、登录耗时。
|
|
5603
|
+
timing: {
|
|
5604
|
+
durationMs: Date.now() - startedAt,
|
|
5605
|
+
apiDurations,
|
|
5606
|
+
apiSlowest,
|
|
5607
|
+
authMs,
|
|
5608
|
+
},
|
|
5609
|
+
});
|
|
5610
|
+
}
|
|
4785
5611
|
function formatRemark(value) {
|
|
4786
5612
|
const text = String(value ?? '').trim();
|
|
4787
5613
|
if (!text)
|
|
@@ -4900,10 +5726,17 @@ function buildCreateBody(context, prepared, saved, src) {
|
|
|
4900
5726
|
* - 多带 `operResultType`/`userSpecifiedNodeId`/`needAutoOpen`/`autoMaxDisplay`/`testParam`/
|
|
4901
5727
|
* `seconedSubmit`(字段名本来就少一个 n,照抄不改)。
|
|
4902
5728
|
*/
|
|
4903
|
-
function buildSubmitBody(context, prepared, view
|
|
5729
|
+
function buildSubmitBody(context, prepared, view,
|
|
5730
|
+
/**
|
|
5731
|
+
* 已经按默认样式包好的签字意见(见 `sign-input.ts`)。
|
|
5732
|
+
*
|
|
5733
|
+
* 不传则退回 `formatRemark` 的老形态 —— 只有"确实要提交、且内容非空"的那一档才会传,
|
|
5734
|
+
* 空意见与降级路径仍是 `_weaverMte_`,与历史完全一致。
|
|
5735
|
+
*/
|
|
5736
|
+
wrappedRemark) {
|
|
4904
5737
|
const runtime = context.runtime;
|
|
4905
5738
|
const requestId = String(runtime.request_id ?? '');
|
|
4906
|
-
const remark = formatRemark(prepared.remark);
|
|
5739
|
+
const remark = wrappedRemark ?? formatRemark(prepared.remark);
|
|
4907
5740
|
return {
|
|
4908
5741
|
operResultType: 'SUCCESS_RELOAD_CURRENT_PAGE',
|
|
4909
5742
|
userSpecifiedNodeId: '0',
|
|
@@ -5151,13 +5984,18 @@ async function loadCreateSubmitContext(env, context, requestLevel) {
|
|
|
5151
5984
|
* 连接中断同样按"提交结果不确定"如实回报;**认证错误照规则原样抛**,不降级成数据结论。
|
|
5152
5985
|
*/
|
|
5153
5986
|
async function submitCreatedRequest(env, context, prepared, view) {
|
|
5987
|
+
// 提交前先按租户配置把签字意见包成带默认字体/字号的形态(发起流程 ⇒ `isCreate:1` + `workflowId`)。
|
|
5988
|
+
// 取不到样式时 `resolveSignRemark` 内部已退回老形态,不会阻断提交。
|
|
5989
|
+
const signRemark = await resolveSignRemark(env, prepared.remark, {
|
|
5990
|
+
workflowId: String(context.workflow?.id ?? ''),
|
|
5991
|
+
});
|
|
5154
5992
|
let response;
|
|
5155
5993
|
try {
|
|
5156
5994
|
response = await env.host.workflowRequest({
|
|
5157
5995
|
method: 'POST',
|
|
5158
5996
|
path: FORM_ENDPOINTS.flowSubmit,
|
|
5159
5997
|
query: { requestId: String(context.runtime.request_id ?? '') },
|
|
5160
|
-
body: buildSubmitBody(context, prepared, view),
|
|
5998
|
+
body: buildSubmitBody(context, prepared, view, signRemark),
|
|
5161
5999
|
operation: 'workflow.createRequest',
|
|
5162
6000
|
});
|
|
5163
6001
|
}
|
|
@@ -5648,7 +6486,7 @@ async function commandEditPrepare(args, env) {
|
|
|
5648
6486
|
});
|
|
5649
6487
|
}
|
|
5650
6488
|
const merged = mergeDetails(context.currentDetails || [], details);
|
|
5651
|
-
const linkageDetails = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, entries, null, merged, true);
|
|
6489
|
+
const linkageDetails = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, entries, null, merged, true, context.currentDetails || []);
|
|
5652
6490
|
const merged2 = mergeLinkageDetails(merged, linkageDetails);
|
|
5653
6491
|
const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
|
|
5654
6492
|
const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
|
|
@@ -5822,7 +6660,7 @@ const workflowCreateForm = {
|
|
|
5822
6660
|
resource: 'workflow',
|
|
5823
6661
|
method: 'createForm',
|
|
5824
6662
|
action: 'workflow.createForm',
|
|
5825
|
-
summary: '按工作流参数解析可发起的工作流并载入发起表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出 context 句柄 + 字段清单 + fileUploadParams +
|
|
6663
|
+
summary: '按工作流参数解析可发起的工作流并载入发起表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出 context 句柄 + 字段清单 + fileUploadParams + 该工作流的发起偏好。只取数、不填参(不再调智能体):模型据此自己拼 data 后交给 createRequest',
|
|
5826
6664
|
capability: 'create',
|
|
5827
6665
|
risk: 'read',
|
|
5828
6666
|
requiresConfirmation: false,
|
|
@@ -5830,9 +6668,8 @@ const workflowCreateForm = {
|
|
|
5830
6668
|
workflow: {
|
|
5831
6669
|
type: 'array',
|
|
5832
6670
|
items: { type: 'string' },
|
|
5833
|
-
description: '工作流条件:支持 "名称(ID)"、名称(
|
|
6671
|
+
description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用中英文逗号分隔,如 "请假,请假申请")、纯数字 ID;按当前用户**可发起**的工作流模糊匹配;未能唯一确定目标时返回澄清(候选见 candidates),确认后由你按「名称(ID)」重发(只问用户完整名称或其它可用于检索的关键词,不要向用户索取 ID)',
|
|
5834
6672
|
},
|
|
5835
|
-
relatesTo: { type: 'string', description: '可选关联对象 id(仅透传,不影响表单初始化)' },
|
|
5836
6673
|
}, { required: ['workflow'] }),
|
|
5837
6674
|
handler: (args, env) => commandCreateForm(args, env),
|
|
5838
6675
|
};
|
|
@@ -5854,13 +6691,31 @@ const workflowCreateRequest = {
|
|
|
5854
6691
|
},
|
|
5855
6692
|
action: { type: 'string', enum: ['draft', 'submit'], description: 'draft=仅保存草稿(必填缺失不阻断,缺失项随结果作为提醒返回);submit=提交审批(仍有必填缺失时不调用创建接口,改为一次性回报全部缺失项)' },
|
|
5856
6693
|
requestName: { type: 'string', description: '流程标题(form 返回 requestName 非空时直接用其值;为空时由模型按内容生成)' },
|
|
5857
|
-
requestLevel: { type: 'string', description: '
|
|
5858
|
-
secLevel: { type: 'string', description: '
|
|
6694
|
+
requestLevel: { type: 'string', description: '紧急程度:用户原话即可(如「紧急」「普通」),CLI 按选项名归一成编码;说不出就不传,走表单默认值' },
|
|
6695
|
+
secLevel: { type: 'string', description: '密级:用户原话即可(如「机密」「公开」),CLI 按选项名归一成密级 id;说不出就不传,走表单默认值' },
|
|
5859
6696
|
remark: { type: 'string', description: '签字意见' },
|
|
5860
6697
|
verificationConfirmed: { type: 'boolean', description: '节点字段校验需用户确认时显式确认继续提交' },
|
|
5861
6698
|
}, { required: ['context', 'data', 'action'] }),
|
|
5862
6699
|
handler: (args, env) => commandCreateRequest(args, env),
|
|
5863
6700
|
};
|
|
6701
|
+
const workflowCreateRequestDirect = {
|
|
6702
|
+
name: 'workflow.createRequestDirect',
|
|
6703
|
+
resource: 'workflow',
|
|
6704
|
+
method: 'createRequestDirect',
|
|
6705
|
+
action: 'workflow.createRequestDirect',
|
|
6706
|
+
summary: '取数(不发起、不调智能体、不写库):载入发起表单后直接返回 fields(含 initialValue)、historyRequestData、userMemory,供你拼好 data 后再调 createRequest。不返回系统字段(标题/紧急程度/密级的默认值与选项),也不返回句柄、form、continuation。输出超过 2 万字符时依次降级:先让 historyRequestData 变成空数组,仍超再让 userMemory 变成空串(键都保留)。耗时全部收在 timing 一个键里:{durationMs, apiDurations, apiSlowest, authMs}',
|
|
6707
|
+
capability: 'read',
|
|
6708
|
+
risk: 'read',
|
|
6709
|
+
requiresConfirmation: false,
|
|
6710
|
+
inputSchema: schemaOf({
|
|
6711
|
+
workflow: {
|
|
6712
|
+
type: 'array',
|
|
6713
|
+
items: { type: 'string' },
|
|
6714
|
+
description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用英文逗号分隔)、纯数字 ID;语义与 createForm 的 workflow 完全一致(含澄清规则)',
|
|
6715
|
+
},
|
|
6716
|
+
}, { required: ['workflow'] }),
|
|
6717
|
+
handler: (args, env) => commandCreateRequestDirect(args, env),
|
|
6718
|
+
};
|
|
5864
6719
|
const workflowEditLoad = {
|
|
5865
6720
|
name: 'workflow.editLoad',
|
|
5866
6721
|
resource: 'workflow',
|
|
@@ -5910,6 +6765,7 @@ const workflowEditApply = {
|
|
|
5910
6765
|
export const workflowFormOperations = [
|
|
5911
6766
|
workflowCreateForm,
|
|
5912
6767
|
workflowCreateRequest,
|
|
6768
|
+
workflowCreateRequestDirect,
|
|
5913
6769
|
workflowEditLoad,
|
|
5914
6770
|
workflowEditPrepare,
|
|
5915
6771
|
workflowEditApply,
|