weaver-work-cli 0.1.7 → 0.1.20
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 +11 -11
- package/dist/cmd/action.js +76 -0
- package/dist/cmd/index.js +2 -0
- package/dist/cmd/refs/index.js +103 -0
- package/dist/cmd/register.js +31 -0
- package/dist/cmd/root.js +1 -0
- package/dist/cmd/setup/index.js +25 -5
- package/dist/cmd/skills/index.js +37 -5
- package/dist/core/connector.js +54 -0
- package/dist/core/context.js +2 -1
- package/dist/core/input.js +48 -0
- package/dist/core/output.js +298 -3
- package/dist/core/refs.js +432 -0
- package/dist/core/state.js +26 -0
- package/dist/internal/e10/auth/commands.js +2 -2
- package/dist/internal/e10/auth/session.js +14 -0
- package/dist/internal/e10/auth/xiaoe.js +116 -51
- package/dist/internal/e10/request-runtime.js +5 -0
- package/dist/internal/skills/install.js +148 -28
- package/dist/internal/skills/reader.js +12 -9
- package/dist/shortcuts/blog/continuation.js +74 -0
- package/dist/shortcuts/blog/errors.js +60 -0
- package/dist/shortcuts/blog/host.js +175 -0
- package/dist/shortcuts/blog/index.js +324 -0
- package/dist/shortcuts/blog/manifest.js +415 -0
- package/dist/shortcuts/blog/operations/comment.js +125 -0
- package/dist/shortcuts/blog/operations/comments.js +31 -0
- package/dist/shortcuts/blog/operations/edit.js +131 -0
- package/dist/shortcuts/blog/operations/export.js +46 -0
- package/dist/shortcuts/blog/operations/like.js +80 -0
- package/dist/shortcuts/blog/operations/publish.js +157 -0
- package/dist/shortcuts/blog/operations/query-user.js +58 -0
- package/dist/shortcuts/blog/operations/query.js +96 -0
- package/dist/shortcuts/blog/operations/registry.js +54 -0
- package/dist/shortcuts/blog/operations/remind.js +176 -0
- package/dist/shortcuts/blog/operations/shared.js +330 -0
- package/dist/shortcuts/blog/operations/types.js +16 -0
- package/dist/shortcuts/blog/operations.js +9 -0
- package/dist/shortcuts/caizhilian/continuation.js +70 -0
- package/dist/shortcuts/caizhilian/errors.js +53 -0
- package/dist/shortcuts/caizhilian/host.js +190 -0
- package/dist/shortcuts/caizhilian/index.js +186 -0
- package/dist/shortcuts/caizhilian/manifest.js +42 -0
- package/dist/shortcuts/caizhilian/operations/api.js +801 -0
- package/dist/shortcuts/caizhilian/operations/confirm.js +75 -0
- package/dist/shortcuts/caizhilian/operations/files.js +306 -0
- package/dist/shortcuts/caizhilian/operations/form.js +170 -0
- package/dist/shortcuts/caizhilian/operations/handlers/collab.js +611 -0
- package/dist/shortcuts/caizhilian/operations/handlers/document.js +890 -0
- package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +346 -0
- package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +192 -0
- package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +570 -0
- package/dist/shortcuts/caizhilian/operations/handlers/folder.js +969 -0
- package/dist/shortcuts/caizhilian/operations/handlers/search.js +318 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +294 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-backup.js +423 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-encrypt.js +238 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-storage.js +364 -0
- package/dist/shortcuts/caizhilian/operations/handlers/system.js +106 -0
- package/dist/shortcuts/caizhilian/operations/registry.js +151 -0
- package/dist/shortcuts/caizhilian/operations/shared.js +408 -0
- package/dist/shortcuts/caizhilian/operations/specs.js +623 -0
- package/dist/shortcuts/caizhilian/operations/types.js +57 -0
- package/dist/shortcuts/caizhilian/operations.js +21 -0
- package/dist/shortcuts/contract/continuation.js +70 -0
- package/dist/shortcuts/contract/errors.js +62 -0
- package/dist/shortcuts/contract/host.js +205 -0
- package/dist/shortcuts/contract/index.js +318 -0
- package/dist/shortcuts/contract/manifest.js +596 -0
- package/dist/shortcuts/contract/operations/clause-reference.js +78 -0
- package/dist/shortcuts/contract/operations/form-info.js +371 -0
- package/dist/shortcuts/contract/operations/read.js +118 -0
- package/dist/shortcuts/contract/operations/registry.js +43 -0
- package/dist/shortcuts/contract/operations/request.js +300 -0
- package/dist/shortcuts/contract/operations/shared.js +133 -0
- package/dist/shortcuts/contract/operations/subject.js +157 -0
- package/dist/shortcuts/contract/operations/template.js +81 -0
- package/dist/shortcuts/contract/operations/types.js +7 -0
- package/dist/shortcuts/contract/operations/workflow.js +52 -0
- package/dist/shortcuts/contract/operations/write.js +131 -0
- package/dist/shortcuts/contract/operations.js +8 -0
- package/dist/shortcuts/hrm/index.js +83 -2
- package/dist/shortcuts/hrm/operations/employee.js +160 -30
- package/dist/shortcuts/hrm/operations/org.js +17 -3
- package/dist/shortcuts/hrm/operations/position.js +18 -2
- package/dist/shortcuts/index.js +5 -1
- package/dist/shortcuts/invoice/index.js +74 -36
- package/dist/shortcuts/invoice/manifest.js +674 -4
- package/dist/shortcuts/invoice/markdown.js +172 -0
- package/dist/shortcuts/invoice/operations/add.js +3 -3
- package/dist/shortcuts/invoice/operations/delete.js +2 -2
- package/dist/shortcuts/invoice/operations/get.js +10 -2
- package/dist/shortcuts/invoice/operations/import.js +2 -2
- package/dist/shortcuts/invoice/operations/registry.js +8 -1
- package/dist/shortcuts/invoice/operations/reim-auto.js +719 -0
- package/dist/shortcuts/invoice/operations/reim.js +85 -2
- package/dist/shortcuts/invoice/operations/shared.js +75 -8
- package/dist/shortcuts/invoice/operations/update.js +2 -2
- package/dist/shortcuts/invoice/operations/upload-and-validate.js +93 -0
- package/dist/shortcuts/invoice/operations/validate-preview.js +2 -1
- package/dist/shortcuts/invoice/projection.js +328 -0
- package/dist/shortcuts/meeting/host.js +18 -1
- package/dist/shortcuts/meeting/manifest.js +30 -18
- package/dist/shortcuts/meeting/operations/eb/catalog.js +390 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +389 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +389 -0
- package/dist/shortcuts/meeting/operations/eb/index.js +22 -0
- package/dist/shortcuts/meeting/operations/env.js +8 -1
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/signs.js +216 -2
- package/dist/shortcuts/workflow/endpoints.js +2 -1
- package/dist/shortcuts/workflow/errors.js +8 -2
- package/dist/shortcuts/workflow/host.js +6 -3
- package/dist/shortcuts/workflow/operations/form.js +931 -533
- package/dist/shortcuts/workflow/operations/read.js +462 -81
- package/dist/shortcuts/workflow/operations/shared.js +44 -0
- package/dist/shortcuts/workflow/operations/write.js +760 -56
- package/dist/shortcuts/workflow/operations.js +36 -10
- package/docs/_catalog.md +6 -0
- package/docs/agent-invoice.md +5 -1
- package/docs/agent-io-channel-design.md +468 -0
- package/docs/agent-skill-install.md +9 -4
- package/docs/blog.md +123 -0
- package/docs/caizhilian.md +304 -0
- package/docs/contract.md +91 -0
- package/docs/e10-auth.md +2 -2
- package/docs/hrm.md +19 -9
- package/docs/invoice.md +5 -1
- package/docs/meeting.md +1 -1
- package/docs/operation-manual.md +15 -3
- package/docs/skill-review-2026-09-19.md +2 -2
- package/package.json +2 -2
- package/scripts/package-skill.mjs +33 -20
- package/skill-template/business-info.json +113 -14
- package/skill-template/domains/blog.md +24 -0
- package/skill-template/domains/caizhilian.md +22 -0
- package/skill-template/domains/jinchengda.md +23 -0
- package/skill-template/domains/meeting.md +1 -1
- package/skill-template/domains/workflow.md +1 -1
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +111 -0
- package/skills/weaver-e10-blog/references/blog-comment.md +51 -0
- package/skills/weaver-e10-blog/references/blog-comments.md +39 -0
- package/skills/weaver-e10-blog/references/blog-default-permission.md +38 -0
- package/skills/weaver-e10-blog/references/blog-disabled-capabilities.md +11 -0
- package/skills/weaver-e10-blog/references/blog-edit.md +51 -0
- package/skills/weaver-e10-blog/references/blog-export.md +42 -0
- package/skills/weaver-e10-blog/references/blog-like.md +50 -0
- package/skills/weaver-e10-blog/references/blog-publish.md +52 -0
- package/skills/weaver-e10-blog/references/blog-query-user.md +45 -0
- package/skills/weaver-e10-blog/references/blog-query.md +49 -0
- package/skills/weaver-e10-blog/references/blog-remind.md +51 -0
- package/skills/weaver-e10-blog/references/blog-resolve-users.md +38 -0
- package/skills/weaver-e10-blog/references/source-manifest.json +1540 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +216 -0
- package/skills/weaver-e10-caizhilian/references/agent-entry.md +106 -0
- package/skills/weaver-e10-caizhilian/references/collaboration.md +112 -0
- package/skills/weaver-e10-caizhilian/references/document.md +111 -0
- package/skills/weaver-e10-caizhilian/references/file-collection.md +136 -0
- package/skills/weaver-e10-caizhilian/references/file-upload.md +309 -0
- package/skills/weaver-e10-caizhilian/references/files.md +120 -0
- package/skills/weaver-e10-caizhilian/references/folder.md +118 -0
- package/skills/weaver-e10-caizhilian/references/org-browser.md +104 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +410 -0
- package/skills/weaver-e10-caizhilian/references/setting-backup.md +372 -0
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +399 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage.md +603 -0
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +5079 -0
- package/skills/weaver-e10-caizhilian/references/withheld.md +93 -0
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +120 -0
- package/skills/weaver-e10-calendar/SKILL.md +4 -3
- package/skills/weaver-e10-ebuilder-form/SKILL.md +4 -9
- package/skills/weaver-e10-ebuilder-form/references/context-routing.md +0 -9
- package/skills/weaver-e10-ebuilder-form/references/custom-api.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +1 -5
- package/skills/weaver-e10-esb/SKILL.md +2 -2
- package/skills/weaver-e10-esb/references/source-manifest.json +9 -9
- package/skills/weaver-e10-hrm/SKILL.md +54 -6
- package/skills/weaver-e10-hrm/references/employee.md +89 -15
- package/skills/weaver-e10-hrm/references/org.md +7 -4
- package/skills/weaver-e10-hrm/references/position.md +14 -7
- package/skills/weaver-e10-hrm/references/source-manifest.json +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +166 -0
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +82 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +69 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +78 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +27 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +71 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +94 -0
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +89 -0
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +77 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +66 -0
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +94 -0
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +259 -0
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -11
- package/skills/weaver-e10-jucailin/SKILL.md +4 -3
- package/skills/weaver-e10-mail/SKILL.md +4 -11
- package/skills/weaver-e10-meeting/SKILL.md +66 -32
- package/skills/weaver-e10-meeting/references/meeting-change.md +5 -2
- package/skills/weaver-e10-meeting/references/meeting-create.md +2 -1
- package/skills/weaver-e10-meeting/references/meeting-eb.md +114 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +9 -8
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +35 -7
- package/skills/weaver-e10-meeting/references/meeting-update.md +1 -1
- package/skills/weaver-e10-okr/SKILL.md +6 -11
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +3 -3
- package/skills/weaver-e10-plan/references/plan-collect-and-compose.md +1 -1
- package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +1 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +13 -13
- package/skills/weaver-e10-qiyecheng/SKILL.md +4 -10
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +9 -9
- package/skills/weaver-e10-shared/SKILL.md +36 -25
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +10 -31
- package/skills/weaver-e10-shared/references/json-output-contract.md +20 -11
- package/skills/weaver-e10-shijingran/SKILL.md +2 -1
- package/skills/weaver-e10-shijingran/references/source-manifest.json +9 -9
- package/skills/weaver-e10-skill-maker/SKILL.md +4 -10
- package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +5 -3
- package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +12 -27
- package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +17 -5
- package/skills/weaver-e10-skill-maker/references/source-update-detection.md +4 -27
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -16
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +7 -7
- package/skills/weaver-e10-workflow/SKILL.md +32 -37
- package/skills/weaver-e10-workflow/references/source-manifest.json +40 -77
- package/skills/weaver-e10-workflow/references/workflow-batch.md +90 -29
- package/skills/weaver-e10-workflow/references/workflow-create.md +79 -109
- package/skills/weaver-e10-workflow/references/workflow-memory.md +19 -9
- package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -2
- package/skills/weaver-e10-workflow/references/workflow-query.md +37 -20
- package/skills/weaver-e10-workflow/references/workflow-share.md +18 -2
- package/skills/weaver-e10-workflow/references/workflow-view.md +4 -4
- package/skills/weaver-e10-yepiaotong/SKILL.md +75 -39
- package/skills/weaver-e10-yepiaotong/references/invoice-detail.md +22 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +3 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +5 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-output-view.md +135 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +15 -3
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-summarize.py +217 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-upload-and-validate.md +88 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-validation-preview.md +7 -2
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +90 -10
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +1 -0
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +70 -49
- package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +119 -0
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +55 -36
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +4 -0
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +149 -0
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +125 -5
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +430 -63
- package/skills/weaver-e10-yimiaoban/SKILL.md +4 -12
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
- package/skills/weaver-e10-ziguanjia/SKILL.md +2 -1
- package/skills/weaver-e10-ziguanjia/references/ziguanjia-agent-entry.md +3 -3
- package/tools/weaver-skill-detector-1.0.13/SKILL.md +9 -9
- package/tools/weaver-skill-detector-1.0.13/references/checklist.md +3 -3
- package/tools/weaver-skill-detector-1.0.13/scripts/__pycache__/validate_skill.cpython-313.pyc +0 -0
- package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +98 -91
- package/scripts/sync-connector-skills.mjs +0 -172
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { WorkflowCliError, workflowConfirmationError, workflowPartialError, workflowValidationError, } from '../errors.js';
|
|
3
3
|
import { continuationReplayError } from '../continuation.js';
|
|
4
|
-
import { rethrowAuthOrNetworkError } from './read.js';
|
|
4
|
+
import { defaultTabIdOf, rethrowAuthOrNetworkError, runWorkflowSearch } from './read.js';
|
|
5
5
|
import { CONTINUATION_INPUT, SCHEMA_CATEGORY, SCHEMA_CONFIRM, SCHEMA_INDEX, SCHEMA_REQUEST_ID, schemaOf, workflowSuccess, } from './types.js';
|
|
6
|
-
import { asInt, asString, asStringArray, compact, responseData, toArray, resolveMemoryAppId } from './shared.js';
|
|
6
|
+
import { asInt, asString, asStringArray, compact, responseData, detailDataOf, toArray, resolveMemoryAppId } from './shared.js';
|
|
7
7
|
import { WORKFLOW_ENDPOINTS } from '../endpoints.js';
|
|
8
8
|
import { findIndexedItem, loadSearchIndex, } from '../state.js';
|
|
9
9
|
// ===========================================================================
|
|
@@ -195,9 +195,15 @@ async function resolveTargetViaDetail(env, requestId, action) {
|
|
|
195
195
|
catch {
|
|
196
196
|
throw workflowValidationError('request_detail_unavailable', `无法获取流程 ${requestId} 详情以校验操作权限,已停止操作(请通过列表序号操作)`, { requestId });
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
// ★ 层级与 `normalizeDetailResponse` 同源:`detailDataOf` 已处理"`responseData()` 解过一层 `data`"
|
|
199
|
+
// (旧写法在这里又取一次 `.data` ⇒ 恒 undefined ⇒ 门禁永远拿不到 `mapItem`)。
|
|
200
|
+
// ⚠️ 但要清楚:该详情响应里**本来就没有** `mapItem` / `canSubmitRejectTransfer`(真机探针实测,
|
|
201
|
+
// 这两个字段只出现在列表 `displayData` 与会话映射里,`otherInfo` 常为空对象)⇒ 本函数在
|
|
202
|
+
// "直传 requestId"场景下通常仍拿不到门禁信息,`allowed` 为 undefined ⇒ **不拦截**。
|
|
203
|
+
// 这与 V11.3 的行为一致(`_normalized` 里没有 `mapItem` ⇒ 降级为 `{requestId}` ⇒ 同样不拦),
|
|
204
|
+
// 属于"缺少门禁数据",不是"误放行"——修正层级只是让 fallback 逻辑按其本意生效。
|
|
205
|
+
const data = detailDataOf(detail);
|
|
206
|
+
const mapItem = (data.mapItem && typeof data.mapItem === 'object' ? data.mapItem : undefined) ?? data;
|
|
201
207
|
const isRemark = String(mapItem.isremark ?? mapItem.isRemark ?? '');
|
|
202
208
|
const allowed = mapItem.canSubmitRejectTransfer;
|
|
203
209
|
const isSpecialSubmit = ['agree', 'submit'].includes(String(action ?? '')) &&
|
|
@@ -299,6 +305,56 @@ async function withUserId(env, payload) {
|
|
|
299
305
|
const session = await env.host.getSession();
|
|
300
306
|
return { ...payload, userId: session.userId };
|
|
301
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* 批量提交请求体(2026-09-20 用户指定,对齐前端 `POST /api/workflow/core/flow/batchSubmit` 的形态)。
|
|
310
|
+
*
|
|
311
|
+
* 与旧端点(`/api/workflow/core/paService/v1/batchSubmit`,只吃 `requestIds` + `userId`)不同:
|
|
312
|
+
* - `batchSubmitListParams` 是一段 **JSON 字符串**,内含查询上下文(`params`)与查询条件(`searchParams`,
|
|
313
|
+
* `formDatas` 里各条件先给"只有 term 的空壳",真正的过滤由 `requestIds` 承担);
|
|
314
|
+
* - `requestIds` / `operateUserIds` 都是**逗号分隔串**;操作人取每条流程上的 `userid`(即当前操作者)。
|
|
315
|
+
*/
|
|
316
|
+
function buildBatchSubmitPayload(input) {
|
|
317
|
+
const termOnly = (extra = {}) => [{ term: 'in', ...extra }];
|
|
318
|
+
const searchParams = {
|
|
319
|
+
formDatas: {
|
|
320
|
+
workflowid: termOnly(), requestname: termOnly(), requestmark: termOnly(),
|
|
321
|
+
creator: termOnly({ hrmOpt: 'targetUser' }), personaltag: termOnly(), creatordept: termOnly(),
|
|
322
|
+
workflowtype: termOnly(), flowstatus: termOnly(), relateprj: termOnly(), creatorsubcom: termOnly(),
|
|
323
|
+
nodeid: termOnly(), relatedoc: termOnly(), relatehrm: termOnly(), relatecrm: termOnly(),
|
|
324
|
+
sourceUserid: termOnly({ hrmSourceOpt: 'targetUser' }), sysCode: termOnly(),
|
|
325
|
+
},
|
|
326
|
+
quickSearchDatas: {},
|
|
327
|
+
type: 'and',
|
|
328
|
+
initSearch: false,
|
|
329
|
+
};
|
|
330
|
+
const params = {
|
|
331
|
+
tabid: '0',
|
|
332
|
+
cusMenuId: '',
|
|
333
|
+
urlPageTitle: '',
|
|
334
|
+
outType: 0,
|
|
335
|
+
sortParams: [],
|
|
336
|
+
workflowId: '',
|
|
337
|
+
dimension: 'todo',
|
|
338
|
+
selectedSubId: '',
|
|
339
|
+
searchAdWorkflowid: '',
|
|
340
|
+
flowCenterParam: {},
|
|
341
|
+
flowSpareType: '1',
|
|
342
|
+
sdkModule: '',
|
|
343
|
+
queryId: '',
|
|
344
|
+
workflowid: '',
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
isMobile: false,
|
|
348
|
+
isAsync: input.isAsync,
|
|
349
|
+
isSelectAll: false,
|
|
350
|
+
conditionId: '',
|
|
351
|
+
batchSubmitListParams: JSON.stringify({ params, searchParams }),
|
|
352
|
+
requestIds: input.requestIds.join(','),
|
|
353
|
+
operateUserIds: input.operateUserIds.join(','),
|
|
354
|
+
remark: input.remark,
|
|
355
|
+
handWrittenSign: '',
|
|
356
|
+
};
|
|
357
|
+
}
|
|
302
358
|
// ---------------------------------------------------------------------------
|
|
303
359
|
// 节点字段校验(来源:run_node_field_verification L14294)
|
|
304
360
|
// 提交/批准/确认 triggerOperation=0,退回 triggerOperation=1。
|
|
@@ -1249,6 +1305,566 @@ const BATCH_COMMON_INPUT = schemaOf({
|
|
|
1249
1305
|
function batchWriteSchema(input) {
|
|
1250
1306
|
return schemaOf({ ...input.properties, confirm: SCHEMA_CONFIRM }, { required: ['confirm'] });
|
|
1251
1307
|
}
|
|
1308
|
+
// ===========================================================================
|
|
1309
|
+
// 3.0 范围驱动的批量执行(scopeCondition / action / exRequestIds)
|
|
1310
|
+
//
|
|
1311
|
+
// 背景:范围驱动("把这些待办都处理了"、没有准确 requestId)原先要由**模型**自己
|
|
1312
|
+
// 查一页 → 执行 → 再查 → 再执行,既烧轮次又容易出错(模型把多页 ID 先收集起来一次性提交 ⇒
|
|
1313
|
+
// 门禁判"不在最近一次查询结果里";或漏掉"流程被处理后从列表消失"导致重复/漏处理)。
|
|
1314
|
+
// 现在把循环下沉到 CLI:给 `scopeCondition`(与 workflow.search 同形)即可。
|
|
1315
|
+
//
|
|
1316
|
+
// 三条路径(三者互斥):
|
|
1317
|
+
// ① `requestIds` —— 只处理这些(原行为不变,门禁仍走快照)
|
|
1318
|
+
// ② `scopeCondition` + `action:"total"` —— 只查数量(不发写入),供用户确认
|
|
1319
|
+
// ③ `scopeCondition` + `action:"execute"` —— 内部循环:查一页 → 立即执行该页 → 排除集并入本次
|
|
1320
|
+
// 已处理与失败的 ID → 再查(始终从第 1 页 + exRequestId 排除,对"处理后消失"与
|
|
1321
|
+
// "处理后不消失"两类操作都成立)→ 直到取不到数据 / 触达上限 / 出现必须停止的错误。
|
|
1322
|
+
// ===========================================================================
|
|
1323
|
+
/** 范围驱动单次调用的上限:批次数与总条数,防止一次调用静默跑掉几百条写操作 */
|
|
1324
|
+
const MAX_SCOPE_BATCHES = 20;
|
|
1325
|
+
const MAX_SCOPE_ITEMS = 2000;
|
|
1326
|
+
const BATCH_SCOPE_INPUT_PROPERTIES = {
|
|
1327
|
+
action: {
|
|
1328
|
+
type: 'string',
|
|
1329
|
+
enum: ['total', 'execute'],
|
|
1330
|
+
description: '**给了 `scopeCondition` 时必填**(不给报 `batch_action_required`):`"total"` 只查条数(不发写入,用于先向用户确认)、`"execute"` 查询并执行',
|
|
1331
|
+
},
|
|
1332
|
+
scopeCondition: {
|
|
1333
|
+
type: 'object',
|
|
1334
|
+
description: '范围条件(与 workflow.search 入参同形:category/workflow/creator/时间条件/filterFields 等)。**与 requestIds 互斥**;给了它就不需要(也不要)先自己查 requestIds——循环由 CLI 内部完成。CLI 会自动补 `batchType`;**未显式给 subCategory/tabName/tabid 时自动按「全部」子分类取数**(给「待处理(7)」会漏掉抄送/传阅/转发类,导致范围恒 0 条)',
|
|
1335
|
+
},
|
|
1336
|
+
exRequestIds: {
|
|
1337
|
+
...BATCH_REQUEST_IDS_INPUT,
|
|
1338
|
+
description: '本次要**排除**的 requestId(数组或逗号分隔串)。用于续跑:上一次执行失败/未处理的 ID 传进来即可跳过',
|
|
1339
|
+
},
|
|
1340
|
+
};
|
|
1341
|
+
/** 从批量写入错误里取「逐条失败」的 ID;取不到返回空数组(表示整批失败、无法定位到条目) */
|
|
1342
|
+
function failedRequestIdsFromError(error) {
|
|
1343
|
+
const details = (error?.details ?? {});
|
|
1344
|
+
const candidates = [
|
|
1345
|
+
details.failedRequestIds, details.failedBatch, details.requestIds, details.blockedRequestIds,
|
|
1346
|
+
];
|
|
1347
|
+
for (const candidate of candidates) {
|
|
1348
|
+
const ids = asStringArray(candidate, 'failedRequestIds');
|
|
1349
|
+
if (ids && ids.length)
|
|
1350
|
+
return ids.map((id) => String(id).trim()).filter(Boolean);
|
|
1351
|
+
}
|
|
1352
|
+
return [];
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* 从**单批返回的信封数据**里提取逐条失败/跳过项(带原因)。
|
|
1356
|
+
*
|
|
1357
|
+
* 为什么必须做这一步(2026-09-20 修复的真实 bug):部分批量操作**部分失败时不抛错**,而是返回
|
|
1358
|
+
* `ok:true` + `status:'PARTIAL'`(转发/退回)或 `success:false`(催办),逐条原因就在信封里。
|
|
1359
|
+
* 驱动原先只按"内层没抛错"就 `succeededCount += 本页条数` ⇒ **把失败项计成成功、失败清单为空**。
|
|
1360
|
+
*
|
|
1361
|
+
* 各操作给的信息并不一致,按"有原因就用原因、没原因给可读兜底"处理:
|
|
1362
|
+
* - `batchForward` / `batchReject`:`failures:[{requestId, resultMessage}]`(有逐条原因)
|
|
1363
|
+
* - `batchSupervise`:`batches:[{requestIds, failNum, resultMessage}]`(**批次级**原因,摊到该批每条)
|
|
1364
|
+
* - `batchSubmit`:`skippedRequestIds` —— **CLI 自己输出的字段**(本地按 `isRemark` 判定的"不可批量
|
|
1365
|
+
* 提交"条目,见 batchSubmit 里的 blockedItems),**接口没有这个属性**,所以这里解析的是内层信封
|
|
1366
|
+
* 而不是服务端响应
|
|
1367
|
+
*/
|
|
1368
|
+
function itemFailuresFromEnvelope(data) {
|
|
1369
|
+
const collected = [];
|
|
1370
|
+
const push = (id, reason, fallback) => {
|
|
1371
|
+
const requestId = String(id ?? '').trim();
|
|
1372
|
+
if (!requestId)
|
|
1373
|
+
return;
|
|
1374
|
+
const text = String(reason ?? '').trim();
|
|
1375
|
+
collected.push({ requestId, reason: text || fallback });
|
|
1376
|
+
};
|
|
1377
|
+
for (const failure of Array.isArray(data?.failures) ? data?.failures : []) {
|
|
1378
|
+
if (!failure || typeof failure !== 'object')
|
|
1379
|
+
continue;
|
|
1380
|
+
push(failure.requestId, failure.resultMessage ?? failure.message ?? failure.reason, '操作未成功');
|
|
1381
|
+
}
|
|
1382
|
+
for (const batch of Array.isArray(data?.batches) ? data?.batches : []) {
|
|
1383
|
+
if (!batch || typeof batch !== 'object')
|
|
1384
|
+
continue;
|
|
1385
|
+
if ((asInt(batch.failNum) ?? 0) <= 0)
|
|
1386
|
+
continue;
|
|
1387
|
+
for (const id of asStringArray(batch.requestIds, 'requestIds') ?? []) {
|
|
1388
|
+
push(id, batch.resultMessage ?? batch.userMessage, '操作未成功');
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// `skippedRequestIds` 是 CLI 自家字段(服务端没有);范围驱动读内层信封时把它并入失败清单,避免假成功
|
|
1392
|
+
for (const id of asStringArray(data?.skippedRequestIds, 'skippedRequestIds') ?? []) {
|
|
1393
|
+
push(id, '', '该流程当前状态不可批量提交,已跳过');
|
|
1394
|
+
}
|
|
1395
|
+
// ④ 嵌套容器:`submitResponse.data.failRequest` 这类(真机 batchSubmit 的逐条失败就在这一层,
|
|
1396
|
+
// 转发/退回的逐条结果同样嵌在 `data.data.operateResult` 里)——复用同一套解析器,避免两处维护。
|
|
1397
|
+
const nested = findBatchResultContainer(data);
|
|
1398
|
+
if (nested) {
|
|
1399
|
+
for (const item of parseBatchWriteOutcome(nested, []).failed)
|
|
1400
|
+
push(item.requestId, item.reason, '操作未成功');
|
|
1401
|
+
}
|
|
1402
|
+
// 同一 requestId 可能同时出现在多个来源(逐条失败 / 跳过清单 / 嵌套容器)里:保留**带原因**的那条
|
|
1403
|
+
const byId = new Map();
|
|
1404
|
+
for (const item of collected) {
|
|
1405
|
+
const existing = byId.get(item.requestId);
|
|
1406
|
+
if (!existing || (existing.reason.length <= item.reason.length && existing.reason !== item.reason)) {
|
|
1407
|
+
byId.set(item.requestId, item);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return [...byId.values()];
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* 定位响应里的「逐条结果容器」。
|
|
1414
|
+
*
|
|
1415
|
+
* 不同批量接口的嵌套层数并不统一(真机实测:`batchSubmit` 的响应是
|
|
1416
|
+
* `{message, data:{successRequest, failRequest}}`,而转发/退回的逐条结果在 `data.data.operateResult`)
|
|
1417
|
+
* ⇒ 做**有界**深度搜索(≤3 层),只认那些"装逐条结果"的键。
|
|
1418
|
+
*/
|
|
1419
|
+
function findBatchResultContainer(node, depth = 0, seen = new Set()) {
|
|
1420
|
+
if (!node || typeof node !== 'object' || depth > 3 || seen.has(node))
|
|
1421
|
+
return null;
|
|
1422
|
+
seen.add(node);
|
|
1423
|
+
const record = node;
|
|
1424
|
+
// `failMsg` 是前端同款批量接口(/api/workflow/core/flow/batchSubmit 等)的失败映射键,
|
|
1425
|
+
// 与 `failRequest` 同形({流程ID: 原因}),一并认。
|
|
1426
|
+
const markers = ['successRequest', 'successRequestIds', 'failRequest', 'failMsg', 'submitResult', 'operateResult'];
|
|
1427
|
+
if (markers.some((key) => record[key] !== undefined))
|
|
1428
|
+
return record;
|
|
1429
|
+
for (const value of Object.values(record)) {
|
|
1430
|
+
const hit = findBatchResultContainer(value, depth + 1, seen);
|
|
1431
|
+
if (hit)
|
|
1432
|
+
return hit;
|
|
1433
|
+
}
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* 单个"逐条结果行"的判定(数组本身就是结果集时用)。
|
|
1438
|
+
*
|
|
1439
|
+
* 要求**同时**带 ID 与**布尔型/字符串布尔**的成功标记:这样才不会把查询结果数组误认成写结果
|
|
1440
|
+
* —— 查询记录里也有 `requestId`,而它的 `status` 是"经办人发起 至 部门领导审批"这类**描述字符串**。
|
|
1441
|
+
*/
|
|
1442
|
+
function isBatchOutcomeRow(row) {
|
|
1443
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
1444
|
+
return false;
|
|
1445
|
+
const record = row;
|
|
1446
|
+
const hasId = record.requestId !== undefined || record.sourceId !== undefined || record.id !== undefined;
|
|
1447
|
+
const resultLike = record.result === true || record.result === false
|
|
1448
|
+
|| record.result === 'true' || record.result === 'false';
|
|
1449
|
+
return hasId && (resultLike || typeof record.status === 'boolean');
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* 探测**数组本身就是逐条结果**的形态(`workflow.batchShare` 的 `addRequestShare2` 就是这样:
|
|
1453
|
+
* `data: [{sourceId, result:"true", msg}]`,既没有 `submitResult` 这类具名字段,`data` 也不是对象)。
|
|
1454
|
+
*
|
|
1455
|
+
* 与 `findBatchResultContainer` 的分工:后者找具名对象字段(`submitResult[]` / `failMsg{}`),
|
|
1456
|
+
* 本函数兜住"裸数组"。保持同样的有界深度(≤3),两种嵌套都能吃。
|
|
1457
|
+
*/
|
|
1458
|
+
function findBatchResultRows(node, depth = 0, seen = new Set()) {
|
|
1459
|
+
if (!node || typeof node !== 'object' || depth > 3 || seen.has(node))
|
|
1460
|
+
return null;
|
|
1461
|
+
seen.add(node);
|
|
1462
|
+
if (Array.isArray(node)) {
|
|
1463
|
+
if (node.length && node.every(isBatchOutcomeRow))
|
|
1464
|
+
return node;
|
|
1465
|
+
for (const item of node) {
|
|
1466
|
+
const hit = findBatchResultRows(item, depth + 1, seen);
|
|
1467
|
+
if (hit)
|
|
1468
|
+
return hit;
|
|
1469
|
+
}
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
for (const value of Object.values(node)) {
|
|
1473
|
+
const hit = findBatchResultRows(value, depth + 1, seen);
|
|
1474
|
+
if (hit)
|
|
1475
|
+
return hit;
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* 解析**一次**批量写入响应 → 「确认成功 / 逐条失败(带原因) / 无法确认」。
|
|
1481
|
+
*
|
|
1482
|
+
* 为什么必须有它(2026-09-20 真机实证的 P0):`batchSubmit` 原先是**按发出去的条数**计数、
|
|
1483
|
+
* `status` 恒 `COMPLETE`,从不看响应。实测把一条必然失败的流程提交,服务端返回
|
|
1484
|
+
* `successRequest: []` + `failRequest: {"<id>":"当前工作流未开启允许批量提交"}`,
|
|
1485
|
+
* CLI 却回 `status:"COMPLETE"` + `submittedCount:1` ⇒ **0 条成功被报成 1 条成功**。
|
|
1486
|
+
*
|
|
1487
|
+
* 口径(判据):**响应里有逐条结果就按它计数;一个逐条结果都没有时不能默认"全成功"**。
|
|
1488
|
+
*/
|
|
1489
|
+
function parseBatchWriteOutcome(response, requestedIds) {
|
|
1490
|
+
let flat = null;
|
|
1491
|
+
try {
|
|
1492
|
+
flat = responseData(response);
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
flat = null;
|
|
1496
|
+
}
|
|
1497
|
+
// 布尔契约(`doBatchRead`:`data === true` 成功 / `false` 失败)
|
|
1498
|
+
if (flat === true)
|
|
1499
|
+
return { succeeded: [...requestedIds], failed: [], unverified: false };
|
|
1500
|
+
if (flat === false) {
|
|
1501
|
+
return {
|
|
1502
|
+
succeeded: [],
|
|
1503
|
+
failed: requestedIds.map((requestId) => ({ requestId, reason: '服务端返回失败' })),
|
|
1504
|
+
unverified: false,
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const container = findBatchResultContainer(response);
|
|
1508
|
+
// 具名对象容器找不到时,再试「数组本身就是逐条结果」的形态(共享 addRequestShare2);
|
|
1509
|
+
// 两种都拿不到 ⇒ 无法确认(绝不默认成功)。
|
|
1510
|
+
const bareRows = container ? null : findBatchResultRows(response);
|
|
1511
|
+
if (!container && !bareRows)
|
|
1512
|
+
return { succeeded: [], failed: [], unverified: true };
|
|
1513
|
+
const failed = [];
|
|
1514
|
+
const succeeded = [];
|
|
1515
|
+
const failureIds = new Set();
|
|
1516
|
+
const pushFailure = (id, reason, name) => {
|
|
1517
|
+
const requestId = String(id ?? '').trim();
|
|
1518
|
+
if (!requestId)
|
|
1519
|
+
return;
|
|
1520
|
+
const title = String(name ?? '').trim();
|
|
1521
|
+
// 同一个 requestId 可能先由「只有 ID 的映射」(failMsg/failRequest)登记、后由「逐条结果数组」
|
|
1522
|
+
// (submitResult/operateResult)带来**标题** ⇒ 后到的补上标题,不要丢掉它。
|
|
1523
|
+
if (failureIds.has(requestId)) {
|
|
1524
|
+
if (!title)
|
|
1525
|
+
return;
|
|
1526
|
+
const existing = failed.find((item) => item.requestId === requestId);
|
|
1527
|
+
if (existing && !existing.name)
|
|
1528
|
+
existing.name = title;
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
failureIds.add(requestId);
|
|
1532
|
+
failed.push({
|
|
1533
|
+
requestId,
|
|
1534
|
+
reason: String(reason ?? '').trim() || '服务端未说明原因',
|
|
1535
|
+
...(title ? { name: title } : {}),
|
|
1536
|
+
});
|
|
1537
|
+
};
|
|
1538
|
+
// ① failRequest / failMsg:**{ requestId: 原因 } 映射**(两种键名都真机见过)
|
|
1539
|
+
for (const key of ['failRequest', 'failMsg']) {
|
|
1540
|
+
const failMap = container?.[key];
|
|
1541
|
+
if (failMap && typeof failMap === 'object' && !Array.isArray(failMap)) {
|
|
1542
|
+
for (const [id, reason] of Object.entries(failMap))
|
|
1543
|
+
pushFailure(id, reason);
|
|
1544
|
+
}
|
|
1545
|
+
if (Array.isArray(failMap)) {
|
|
1546
|
+
for (const item of failMap) {
|
|
1547
|
+
pushFailure(item?.requestId ?? item?.id, item?.resultMessage ?? item?.message ?? item?.reason);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// ② 逐条数组:具名的 submitResult / operateResult,或**数组本身就是逐条结果**(共享:根级 `data` 数组)
|
|
1552
|
+
const rowGroups = [];
|
|
1553
|
+
if (container) {
|
|
1554
|
+
for (const key of ['submitResult', 'operateResult']) {
|
|
1555
|
+
if (Array.isArray(container[key]))
|
|
1556
|
+
rowGroups.push(container[key]);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
else if (bareRows) {
|
|
1560
|
+
rowGroups.push(bareRows);
|
|
1561
|
+
}
|
|
1562
|
+
for (const rows of rowGroups) {
|
|
1563
|
+
for (const row of rows) {
|
|
1564
|
+
if (!row || typeof row !== 'object')
|
|
1565
|
+
continue;
|
|
1566
|
+
// 标题:前端同款接口在 row.requestName 与 row.data.requestNameTitle 都给了(真机样本)
|
|
1567
|
+
const rowName = row.requestName ?? row.requestNameTitle ?? row.data?.requestNameTitle ?? row.data?.requestName;
|
|
1568
|
+
// ID:共享形态是 `sourceId`;成功标记:共享给的是**字符串** `result:"true"`(不是布尔 status)
|
|
1569
|
+
const rowId = row.requestId ?? row.sourceId ?? row.id;
|
|
1570
|
+
const rowOk = row.status === true || row.result === true || row.result === 'true';
|
|
1571
|
+
// 非明确成功一律算失败(保守,绝不放行成"成功")
|
|
1572
|
+
if (rowOk)
|
|
1573
|
+
succeeded.push(String(rowId ?? '').trim());
|
|
1574
|
+
else
|
|
1575
|
+
pushFailure(rowId, row.resultMessage ?? row.msg ?? row.message ?? row.reason, rowName);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
// ③ successRequest / successRequestIds:元素可能是 id 或 {requestId}
|
|
1579
|
+
for (const key of ['successRequest', 'successRequestIds']) {
|
|
1580
|
+
const rows = container?.[key];
|
|
1581
|
+
if (!Array.isArray(rows))
|
|
1582
|
+
continue;
|
|
1583
|
+
for (const row of rows) {
|
|
1584
|
+
const id = typeof row === 'object' && row !== null
|
|
1585
|
+
? String(row.requestId ?? row.sourceId ?? row.id ?? '').trim()
|
|
1586
|
+
: String(row ?? '').trim();
|
|
1587
|
+
if (id)
|
|
1588
|
+
succeeded.push(id);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const confirmedSucceeded = succeeded.filter((id) => id && !failureIds.has(id));
|
|
1592
|
+
if (!confirmedSucceeded.length && !failed.length)
|
|
1593
|
+
return { succeeded: [], failed: [], unverified: true };
|
|
1594
|
+
// 只给了失败映射时,成功集合由「请求集合 − 失败集合」补出(实测 batchSubmit 正是这种)
|
|
1595
|
+
const succeededFinal = confirmedSucceeded.length
|
|
1596
|
+
? confirmedSucceeded
|
|
1597
|
+
: requestedIds.filter((id) => !failureIds.has(id));
|
|
1598
|
+
return { succeeded: succeededFinal, failed, unverified: false };
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* 给失败项补**流程标题**。
|
|
1602
|
+
*
|
|
1603
|
+
* 优先用服务端逐条结果里的名字(`requestName` / `data.requestNameTitle`);那里没有时,回退到
|
|
1604
|
+
* **最近一次查询结果的快照**(`search-index.json` 的 `flowName`,它取自原始记录的
|
|
1605
|
+
* `requestnameTitle`/`requestnametitle`——真机确认两个键名都存在且同值)。都取不到就只给 ID,
|
|
1606
|
+
* **不编造**。
|
|
1607
|
+
*/
|
|
1608
|
+
async function attachFailureTitles(failures, env) {
|
|
1609
|
+
if (!failures.length || failures.every((item) => item.name))
|
|
1610
|
+
return failures;
|
|
1611
|
+
const titleById = new Map();
|
|
1612
|
+
try {
|
|
1613
|
+
const snapshot = await loadSearchIndex(env.ctx);
|
|
1614
|
+
for (const item of collectAllIndexItems(snapshot)) {
|
|
1615
|
+
const requestId = String(item?.requestId ?? '').trim();
|
|
1616
|
+
const name = String(item?.flowName ?? '').trim();
|
|
1617
|
+
if (requestId && name)
|
|
1618
|
+
titleById.set(requestId, name);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1622
|
+
// 快照读不到(尚未查询过/被清理):只给 ID,不编造标题
|
|
1623
|
+
}
|
|
1624
|
+
return failures.map((item) => (item.name ? item : (() => {
|
|
1625
|
+
const name = titleById.get(item.requestId) ?? '';
|
|
1626
|
+
return name ? { ...item, name } : item;
|
|
1627
|
+
})()));
|
|
1628
|
+
}
|
|
1629
|
+
/** 读不到逐条结果时的统一提示("不确认"绝不等于"成功") */
|
|
1630
|
+
const UNVERIFIED_NOTICE = '服务端未返回逐条结果,本次**无法确认**这些流程是否已生效:'
|
|
1631
|
+
+ '不要按"已成功"上报,请用 workflow.search --category done --requestId <id> 只读核对(默认子分类为「全部」)。';
|
|
1632
|
+
/** 从一页查询结果里取 requestId:优先 `cards`;超线时 `cards` 被移出 stdout ⇒ 回退到刚刷新的索引快照 */
|
|
1633
|
+
async function scopePageRequestIds(data, env) {
|
|
1634
|
+
const cards = Array.isArray(data?.cards) ? data.cards : [];
|
|
1635
|
+
if (cards.length) {
|
|
1636
|
+
return cards.map((card) => String(card?.requestId ?? '').trim()).filter(Boolean);
|
|
1637
|
+
}
|
|
1638
|
+
try {
|
|
1639
|
+
const snapshot = await loadSearchIndex(env.ctx);
|
|
1640
|
+
return collectAllIndexItems(snapshot).map((item) => String(item?.requestId ?? '').trim()).filter(Boolean);
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
return [];
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* 范围驱动的批量执行入口。返回 `null` 表示"本次不是范围驱动",调用方按原逻辑(requestIds)继续。
|
|
1648
|
+
*/
|
|
1649
|
+
async function runScopeDrivenBatch(args, env, operationName, suffix, inner) {
|
|
1650
|
+
const scope = args.scopeCondition;
|
|
1651
|
+
const hasScope = !!scope && typeof scope === 'object' && !Array.isArray(scope) && Object.keys(scope).length > 0;
|
|
1652
|
+
if (!hasScope)
|
|
1653
|
+
return null;
|
|
1654
|
+
requireBatchConfirm(args, operationName);
|
|
1655
|
+
if (collectRequestIds(args).length) {
|
|
1656
|
+
throw workflowValidationError('batch_scope_conflict', '`requestIds` 与 `scopeCondition` 只能给一个:给了 `requestIds` 就只处理这些,给了 `scopeCondition` 才由 CLI 内部按范围取数');
|
|
1657
|
+
}
|
|
1658
|
+
// `action` 在范围驱动下**必填**(用户 2026-09-20 裁定):不给默认值,"先数条数再执行"这一步
|
|
1659
|
+
// 就不能靠"忘了写"绕过去——必须显式选 `total`(先数)或 `execute`(执行)。
|
|
1660
|
+
const action = (asString(args.action) ?? '').toLowerCase();
|
|
1661
|
+
if (!action) {
|
|
1662
|
+
throw workflowValidationError('batch_action_required', '给了 `scopeCondition` 时必须显式指定 `action`:先用 `"total"` 只查条数并请用户确认,确认后再用 `"execute"` 执行', { retryAllowed: false });
|
|
1663
|
+
}
|
|
1664
|
+
if (action !== 'total' && action !== 'execute') {
|
|
1665
|
+
throw workflowValidationError('batch_action_invalid', '`action` 只能是 `"total"` 或 `"execute"`');
|
|
1666
|
+
}
|
|
1667
|
+
const batchType = suffix.toLowerCase();
|
|
1668
|
+
const pageSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
|
|
1669
|
+
const scopeRaw = scope;
|
|
1670
|
+
const category = asString(scopeRaw.category) ?? asString(args.category) ?? 'todo';
|
|
1671
|
+
const scopeBase = { ...scopeRaw, batchType, category };
|
|
1672
|
+
// ★ 范围驱动默认「全部」子分类(用户 2026-09-20 裁定):scope 未显式给 subCategory/tabName/tabid 时,
|
|
1673
|
+
// 显式带上该大分类的「全部」tabid(todo→0)。理由见 defaultTabIdOf 注释:
|
|
1674
|
+
// legacy 下 todo 缺省子分类会落到「待处理(7)」,而批量过滤列表端点在该 tabid 下只返待处理
|
|
1675
|
+
// ⇒ 范围里全是抄送/传阅/转发类时恒 0 条,模型会把"取不到"报成"范围内没有数据"。
|
|
1676
|
+
const hasExplicitTab = ['subCategory', 'tabName', 'tabid'].some((key) => {
|
|
1677
|
+
const fromScope = scopeRaw[key];
|
|
1678
|
+
if (fromScope !== undefined && String(fromScope).trim() !== '')
|
|
1679
|
+
return true;
|
|
1680
|
+
const fromArgs = args[key];
|
|
1681
|
+
return fromArgs !== undefined && String(fromArgs).trim() !== '';
|
|
1682
|
+
});
|
|
1683
|
+
if (!hasExplicitTab)
|
|
1684
|
+
scopeBase.tabid = defaultTabIdOf(category);
|
|
1685
|
+
// —— ② 只查数量:不发任何写入(但要把"能不能真取到行"一并查清,避免 total 与 execute 说两套话)——
|
|
1686
|
+
if (action === 'total') {
|
|
1687
|
+
const totalData = await runWorkflowSearch({ ...scopeBase, totalOnly: true }, env);
|
|
1688
|
+
const scopeTotal = asInt(totalData?.total) ?? 0;
|
|
1689
|
+
// 列表探针(≤1 行):total 端点与 list 端点的口径可能不一致(真实事故:total 说 96、
|
|
1690
|
+
// list 取 0 行),所以"可执行条数"必须以**能取到行**为准,不能只信 total。
|
|
1691
|
+
let executable = false;
|
|
1692
|
+
if (scopeTotal > 0) {
|
|
1693
|
+
const probe = await runWorkflowSearch({ ...scopeBase, current: 1, pageSize: 1 }, env);
|
|
1694
|
+
executable = (await scopePageRequestIds(probe, env)).length > 0;
|
|
1695
|
+
}
|
|
1696
|
+
const reportTotal = executable ? scopeTotal : 0;
|
|
1697
|
+
// 返回只放**业务结果**:条数与可执行性。`pageSize`/预计批次数这类"内部怎么分批"的实现细节
|
|
1698
|
+
// 一律不回(2026-09-20 用户裁定:对普通用户没意义,模型还会顺手念给用户听)。
|
|
1699
|
+
return workflowSuccess(operationName, {
|
|
1700
|
+
status: 'SCOPE_TOTAL',
|
|
1701
|
+
action: 'total',
|
|
1702
|
+
total: reportTotal,
|
|
1703
|
+
scopeTotal,
|
|
1704
|
+
executable,
|
|
1705
|
+
batchType,
|
|
1706
|
+
scopeCondition: scope,
|
|
1707
|
+
message: executable
|
|
1708
|
+
? `该范围下可批量操作的流程共 ${reportTotal} 条。请先向用户确认条数,确认后再用 action="execute" 执行。`
|
|
1709
|
+
: (scopeTotal > 0
|
|
1710
|
+
? `该范围共有 ${scopeTotal} 条流程,但**当前没有一条能按本次操作取到**(可执行 0 条)——常见原因:这些流程是抄送/传阅/转发类(批量提交对它们等同置为已办,可改用 workflow.batchRead),或 scope 的子分类把数据收窄掉了。**这不代表范围内没有数据**。`
|
|
1711
|
+
: '该范围下没有任何流程,无需执行。'),
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
// —— ③ 内部循环执行 ——
|
|
1715
|
+
const excluded = new Set(collectRequestIds({ requestIds: args.exRequestIds }));
|
|
1716
|
+
const pageArgsBase = { ...args };
|
|
1717
|
+
for (const key of ['scopeCondition', 'action', 'exRequestIds', 'requestIds', 'confirm'])
|
|
1718
|
+
delete pageArgsBase[key];
|
|
1719
|
+
const failures = [];
|
|
1720
|
+
let lastBatchError = null;
|
|
1721
|
+
let succeededCount = 0;
|
|
1722
|
+
let stoppedReason = '';
|
|
1723
|
+
let scopeTotalWhenEmpty = 0;
|
|
1724
|
+
for (let index = 1; index <= MAX_SCOPE_BATCHES; index += 1) {
|
|
1725
|
+
// 始终从第 1 页取 + 用排除集排掉已处理/已失败的 ID —— 对"处理后消失"(提交/置已读)
|
|
1726
|
+
// 与"处理后不消失"(催办/共享/转发/退回)两类操作都成立,无需区分翻页策略。
|
|
1727
|
+
const pageData = await runWorkflowSearch({
|
|
1728
|
+
...scopeBase,
|
|
1729
|
+
current: 1,
|
|
1730
|
+
pageSize,
|
|
1731
|
+
...(excluded.size ? { exRequestId: [...excluded].join(',') } : {}),
|
|
1732
|
+
}, env);
|
|
1733
|
+
const pageIds = (await scopePageRequestIds(pageData, env)).filter((id) => id && !excluded.has(id));
|
|
1734
|
+
if (!pageIds.length) {
|
|
1735
|
+
// 区分「范围真的没数据」与「范围内有数据、但本次取不到可批量处理的项」——后者若报成
|
|
1736
|
+
// "该范围已取不到更多数据",模型会把它写成一条**假的数据结论**(真实事故:把 96 条报成"今天 0 条")。
|
|
1737
|
+
// 只在**第一轮就取空**时补一次只读总数查询:已经处理过条目的情况下取空=真的取完了,
|
|
1738
|
+
// 不必也不能当成"不适用"。正常路径零额外开销。
|
|
1739
|
+
if (index === 1 && !succeededCount && !failures.length) {
|
|
1740
|
+
try {
|
|
1741
|
+
const probeData = await runWorkflowSearch({ ...scopeBase, totalOnly: true }, env);
|
|
1742
|
+
scopeTotalWhenEmpty = asInt(probeData?.total) ?? 0;
|
|
1743
|
+
}
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
rethrowAuthOrNetworkError(error);
|
|
1746
|
+
scopeTotalWhenEmpty = 0;
|
|
1747
|
+
}
|
|
1748
|
+
stoppedReason = scopeTotalWhenEmpty > 0 ? 'scope_not_batch_eligible' : 'scope_exhausted';
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
stoppedReason = 'scope_exhausted';
|
|
1752
|
+
}
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
let envelope;
|
|
1756
|
+
try {
|
|
1757
|
+
envelope = await inner({ ...pageArgsBase, requestIds: pageIds, confirm: true });
|
|
1758
|
+
}
|
|
1759
|
+
catch (error) {
|
|
1760
|
+
// 认证/网络错误原样抛(不得降级成"没数据")
|
|
1761
|
+
rethrowAuthOrNetworkError(error);
|
|
1762
|
+
const itemFailed = failedRequestIdsFromError(error);
|
|
1763
|
+
const errorText = String(error?.message ?? error).trim().slice(0, 300);
|
|
1764
|
+
const toExclude = itemFailed.length ? itemFailed : pageIds;
|
|
1765
|
+
for (const id of toExclude) {
|
|
1766
|
+
excluded.add(id);
|
|
1767
|
+
// 抛错路径只有批次级原因(服务端响应在错误详情里):摊到该批每条,至少让用户知道"为什么没成功"
|
|
1768
|
+
failures.push({ requestId: id, reason: errorText || '批量写入未成功' });
|
|
1769
|
+
}
|
|
1770
|
+
lastBatchError = {
|
|
1771
|
+
subtype: String(error?.subtype ?? ''),
|
|
1772
|
+
message: errorText,
|
|
1773
|
+
};
|
|
1774
|
+
// 整批失败(定位不到逐条失败项)⇒ 立即停止:这类失败通常是口径/认证/门禁问题,
|
|
1775
|
+
// 继续跑只会把同一个错误重复 N 次。能定位到逐条失败项时才继续(继续时排除集里已有它们)。
|
|
1776
|
+
if (!itemFailed.length) {
|
|
1777
|
+
stoppedReason = 'batch_failed_stop';
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
if (succeededCount + failures.length >= MAX_SCOPE_ITEMS) {
|
|
1781
|
+
stoppedReason = 'max_items_reached';
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
// ★ 内层单批"部分失败"不一定抛错(转发/退回返回 PARTIAL+failures、催办返回 success=false、
|
|
1787
|
+
// 提交返回 skippedRequestIds)⇒ 必须解析信封,否则失败项会被计成成功、失败清单为空。
|
|
1788
|
+
const itemFailures = itemFailuresFromEnvelope((envelope.data ?? {}));
|
|
1789
|
+
const failedIdSet = new Set(itemFailures.map((item) => item.requestId));
|
|
1790
|
+
// 内层"无法确认"(服务端没给逐条结果,如 batchRead 的 resultUnverified)⇒ **不计入成功**:
|
|
1791
|
+
// 收进失败清单并如实标注原因,由模型只读核对后再决定是否重试("不确认"绝不等于"成功")。
|
|
1792
|
+
const innerData = (envelope.data ?? {});
|
|
1793
|
+
if (innerData.unverified === true || innerData.resultUnverified === true) {
|
|
1794
|
+
for (const id of pageIds) {
|
|
1795
|
+
if (failedIdSet.has(id))
|
|
1796
|
+
continue;
|
|
1797
|
+
failedIdSet.add(id); // 同步进"失败集合",使下面的成功计数同步扣除
|
|
1798
|
+
itemFailures.push({ requestId: id, reason: '服务端未返回逐条结果,无法确认是否生效(请只读核对)' });
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
// 已处理与已失败都进排除集:既不重复取数(否则原地打转),也不重复写入
|
|
1802
|
+
for (const id of pageIds)
|
|
1803
|
+
excluded.add(id);
|
|
1804
|
+
for (const item of itemFailures)
|
|
1805
|
+
failures.push(item);
|
|
1806
|
+
succeededCount += pageIds.length - pageIds.filter((id) => failedIdSet.has(id)).length;
|
|
1807
|
+
if (succeededCount + failures.length >= MAX_SCOPE_ITEMS) {
|
|
1808
|
+
stoppedReason = 'max_items_reached';
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (!stoppedReason)
|
|
1813
|
+
stoppedReason = 'max_batches_reached';
|
|
1814
|
+
const complete = stoppedReason === 'scope_exhausted';
|
|
1815
|
+
const notEligible = stoppedReason === 'scope_not_batch_eligible';
|
|
1816
|
+
const failuresWithTitles = await attachFailureTitles(failures, env);
|
|
1817
|
+
// 返回只放**业务结果**:处理条数、失败清单、未跑完的原因(诊断用)。
|
|
1818
|
+
// 不返回 `batches`/`batchSummaries`/`pageSize`——那是"内部怎么分批循环"的实现细节,
|
|
1819
|
+
// 对用户没有意义,还会被顺手念给用户听(2026-09-20 用户裁定)。
|
|
1820
|
+
const partialReasonText = {
|
|
1821
|
+
max_batches_reached: '已达单次调用的处理上限',
|
|
1822
|
+
max_items_reached: '已达单次调用的处理上限',
|
|
1823
|
+
batch_failed_stop: '其中一批整体失败,已停止',
|
|
1824
|
+
};
|
|
1825
|
+
return workflowSuccess(operationName, {
|
|
1826
|
+
status: complete ? 'COMPLETE' : 'PARTIAL',
|
|
1827
|
+
action: 'execute',
|
|
1828
|
+
batchType,
|
|
1829
|
+
scopeCondition: scope,
|
|
1830
|
+
succeededCount,
|
|
1831
|
+
failedCount: failuresWithTitles.length,
|
|
1832
|
+
...(failuresWithTitles.length ? { failures: failuresWithTitles } : {}),
|
|
1833
|
+
...(notEligible ? { scopeTotal: scopeTotalWhenEmpty } : {}),
|
|
1834
|
+
...(!complete && !notEligible && lastBatchError ? { lastError: lastBatchError } : {}),
|
|
1835
|
+
stoppedReason,
|
|
1836
|
+
message: complete
|
|
1837
|
+
? `已处理 ${succeededCount} 条${failuresWithTitles.length ? `、失败 ${failuresWithTitles.length} 条` : ''},该范围已处理完。`
|
|
1838
|
+
: (notEligible
|
|
1839
|
+
? `已处理 ${succeededCount} 条;该范围共有 ${scopeTotalWhenEmpty} 条流程,但**余下的都取不到**(本次操作对它们不适用)。`
|
|
1840
|
+
+ '常见原因:这些流程是抄送/传阅/转发类(例如批量提交对它们等同置为已办,可改用 workflow.batchRead)。'
|
|
1841
|
+
+ '**这不代表该范围内没有数据**——向用户汇报时必须说明"有多少条、为什么不适用",不要报成"0 条/没有数据"。'
|
|
1842
|
+
: `已处理 ${succeededCount} 条、失败 ${failuresWithTitles.length} 条,但**没有跑完**(${partialReasonText[stoppedReason] ?? stoppedReason})。`
|
|
1843
|
+
+ '不要把本次结果当成"全部处理完了";如需续跑,带上同一 `scopeCondition` 再执行一次(本次已处理的会被自动跳过);'
|
|
1844
|
+
+ '**要重试失败的那几条时,先只读核对它们是否其实已经生效**(workflow.search --category done --requestId <id>),确认没生效再用 requestIds 单独重试。'),
|
|
1845
|
+
notice: '汇报时只说"处理了多少条、失败了多少条(`failures` 里有逐条原因)"这类业务结果;'
|
|
1846
|
+
+ '**不要向用户提内部分批、循环、页数、批次数等实现细节**——那是 CLI 内部的事,对用户没有意义。'
|
|
1847
|
+
+ (failuresWithTitles.length ? '失败项要连**原因**一起说明(`failures[].reason`),不要只报个数。' : ''),
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
/** 给 6 个单命令批量操作统一挂上范围驱动(requestIds 路径完全不受影响)。 */
|
|
1851
|
+
function withBatchScopeDriver(definition, suffix) {
|
|
1852
|
+
const inner = definition.handler;
|
|
1853
|
+
const properties = {
|
|
1854
|
+
...(definition.inputSchema?.properties ?? {}),
|
|
1855
|
+
...BATCH_SCOPE_INPUT_PROPERTIES,
|
|
1856
|
+
};
|
|
1857
|
+
return {
|
|
1858
|
+
...definition,
|
|
1859
|
+
inputSchema: schemaOf(properties, { required: ['confirm'] }),
|
|
1860
|
+
handler: async (args, env) => {
|
|
1861
|
+
const scoped = await runScopeDrivenBatch(args, env, definition.name, suffix, (pageArgs) => inner(pageArgs, env));
|
|
1862
|
+
if (scoped)
|
|
1863
|
+
return scoped;
|
|
1864
|
+
return inner(args, env);
|
|
1865
|
+
},
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1252
1868
|
/**
|
|
1253
1869
|
* 单命令批量操作的**幂等键**:由「哪个操作、谁、在哪个租户、要写哪一批流程、写向哪些对象」决定,
|
|
1254
1870
|
* 与 `remark` / `batchSize` / `confirm` 无关(同一批同一对象换个意见再执行一次,仍然是重复执行)。
|
|
@@ -1383,26 +1999,60 @@ const workflowBatchSubmit = {
|
|
|
1383
1999
|
method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_READ, operation: 'workflow.batchSubmit', body: readPayload,
|
|
1384
2000
|
}), readPayload);
|
|
1385
2001
|
}
|
|
2002
|
+
// 前置置已办("不需提交"类)也要按服务端的逐条/布尔结果计数,不能默认成功
|
|
2003
|
+
let readCount = 0;
|
|
2004
|
+
let readUnverified = false;
|
|
2005
|
+
const readOutcome = readItems.length
|
|
2006
|
+
? parseBatchWriteOutcome(readResult, readItems.map((item) => item.requestId))
|
|
2007
|
+
: { succeeded: [], failed: [], unverified: false };
|
|
2008
|
+
readCount = readOutcome.succeeded.length;
|
|
2009
|
+
readUnverified = readOutcome.unverified;
|
|
1386
2010
|
let submitResult = null;
|
|
1387
|
-
|
|
2011
|
+
const submitFailures = [];
|
|
2012
|
+
const submitSucceededIds = [];
|
|
2013
|
+
let submitUnverified = false;
|
|
1388
2014
|
if (submitItems.length) {
|
|
1389
2015
|
// 按 batchSize 分批(契约:batchSize 1–100,CLI 自动分批循环)——
|
|
1390
2016
|
// 原先把所有 requestId 拼成一次请求,超出单批上限会被服务端拒绝
|
|
1391
2017
|
for (const batch of chunkArray(submitItems, batchSize)) {
|
|
1392
|
-
const
|
|
1393
|
-
|
|
2018
|
+
const payload = buildBatchSubmitPayload({
|
|
2019
|
+
requestIds: batch.map((it) => it.requestId),
|
|
2020
|
+
operateUserIds: [...new Set(batch.map((it) => String(it.userid || it.userMasterId || '')).filter(Boolean))],
|
|
2021
|
+
remark: asString(args.remark) ?? '',
|
|
2022
|
+
isAsync: args.async === true,
|
|
2023
|
+
});
|
|
1394
2024
|
submitResult = await runWrite('batchSubmit', env, () => env.host.workflowRequest({
|
|
1395
2025
|
method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_SUBMIT, operation: 'workflow.batchSubmit', body: payload,
|
|
1396
2026
|
}), payload);
|
|
1397
|
-
|
|
2027
|
+
// ★ 只认服务端逐条结果:`successRequest` 是成功清单、`failRequest` 是 {id: 原因} 映射
|
|
2028
|
+
const outcome = parseBatchWriteOutcome(submitResult, batch.map((it) => it.requestId));
|
|
2029
|
+
submitSucceededIds.push(...outcome.succeeded);
|
|
2030
|
+
submitFailures.push(...outcome.failed);
|
|
2031
|
+
if (outcome.unverified)
|
|
2032
|
+
submitUnverified = true;
|
|
1398
2033
|
}
|
|
1399
2034
|
}
|
|
2035
|
+
const failures = await attachFailureTitles([...readOutcome.failed, ...submitFailures], env);
|
|
2036
|
+
const unverified = readUnverified || submitUnverified;
|
|
1400
2037
|
const data = {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
2038
|
+
// 有逐条失败、或读不到逐条结果 ⇒ 都不是"全部成功"
|
|
2039
|
+
status: failures.length || unverified ? 'PARTIAL' : 'COMPLETE',
|
|
2040
|
+
readCount,
|
|
2041
|
+
submittedCount: submitSucceededIds.length,
|
|
2042
|
+
failedCount: failures.length,
|
|
1404
2043
|
batchRead: readResult,
|
|
1405
2044
|
submitResponse: submitResult,
|
|
2045
|
+
...(failures.length ? { failures } : {}),
|
|
2046
|
+
...(unverified
|
|
2047
|
+
? {
|
|
2048
|
+
unverified: true,
|
|
2049
|
+
unverifiedRequestIds: [
|
|
2050
|
+
...(readUnverified ? readItems.map((item) => item.requestId) : []),
|
|
2051
|
+
...(submitUnverified ? submitItems.map((item) => item.requestId) : []),
|
|
2052
|
+
],
|
|
2053
|
+
warnings: [UNVERIFIED_NOTICE],
|
|
2054
|
+
}
|
|
2055
|
+
: {}),
|
|
1406
2056
|
...(blockedItems.length
|
|
1407
2057
|
? {
|
|
1408
2058
|
skippedRequestIds: blockedItems.map((item) => item.requestId),
|
|
@@ -1572,6 +2222,7 @@ const workflowBatchSupervise = {
|
|
|
1572
2222
|
otherReminder: otherReminderIds,
|
|
1573
2223
|
});
|
|
1574
2224
|
const batches = chunkArray(requestIds, batchSize);
|
|
2225
|
+
let resultUnverified = false;
|
|
1575
2226
|
const result = {
|
|
1576
2227
|
success: true,
|
|
1577
2228
|
requestIdCount: requestIds.length,
|
|
@@ -1594,20 +2245,34 @@ const workflowBatchSupervise = {
|
|
|
1594
2245
|
const submitResult = await runWrite('batchSupervise', env, () => env.host.workflowRequest({
|
|
1595
2246
|
method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_SUPERVISE, operation: 'workflow.batchSupervise', body: payload,
|
|
1596
2247
|
}), payload);
|
|
2248
|
+
// 催办是**唯一**返回批次级计数的批量接口(真机样本:`data.data.{successNum, failNum, resultMessage}`,
|
|
2249
|
+
// 双层嵌套、**没有逐条 ID/标题**)——所以这里不走逐条解析,但**认不出这个结构时不能默认 0 条成功**。
|
|
1597
2250
|
const inner = submitResult?.data?.data ?? submitResult?.data ?? submitResult;
|
|
1598
|
-
const
|
|
1599
|
-
|
|
2251
|
+
const hasCounters = inner && typeof inner === 'object'
|
|
2252
|
+
&& (inner.successNum !== undefined || inner.failNum !== undefined);
|
|
2253
|
+
if (!hasCounters)
|
|
2254
|
+
resultUnverified = true;
|
|
2255
|
+
const successNum = hasCounters ? Number(inner?.successNum ?? 0) : 0;
|
|
2256
|
+
const failNum = hasCounters ? Number(inner?.failNum ?? 0) : 0;
|
|
1600
2257
|
result.successNum += successNum;
|
|
1601
2258
|
result.failNum += failNum;
|
|
1602
2259
|
result.batches.push({ batchIndex: i + 1, requestIds: batch, successNum, failNum, resultMessage: String(inner?.resultMessage ?? '') });
|
|
1603
2260
|
if (failNum > 0) {
|
|
1604
2261
|
result.success = false;
|
|
1605
2262
|
result.batchIndex = i + 1;
|
|
2263
|
+
result.status = 'PARTIAL';
|
|
2264
|
+
result.failedCount = result.failNum;
|
|
1606
2265
|
result.userMessage = `批量催办第 ${i + 1}/${batches.length} 批部分失败:成功 ${successNum} 条,失败 ${failNum} 条`;
|
|
1607
2266
|
await env.continuation.settle(key, result, 'workflow.batchSupervise');
|
|
1608
2267
|
return workflowSuccess('workflow.batchSupervise', result);
|
|
1609
2268
|
}
|
|
1610
2269
|
}
|
|
2270
|
+
result.status = result.failNum || resultUnverified ? 'PARTIAL' : 'COMPLETE';
|
|
2271
|
+
result.failedCount = result.failNum;
|
|
2272
|
+
if (resultUnverified) {
|
|
2273
|
+
result.unverified = true;
|
|
2274
|
+
result.warnings = [UNVERIFIED_NOTICE];
|
|
2275
|
+
}
|
|
1611
2276
|
result.userMessage = `已批量催办 ${result.successNum} 条流程。` + (result.failNum ? `失败 ${result.failNum} 条。` : '');
|
|
1612
2277
|
await env.continuation.settle(key, result, 'workflow.batchSupervise');
|
|
1613
2278
|
return workflowSuccess('workflow.batchSupervise', result);
|
|
@@ -1664,6 +2329,8 @@ const workflowBatchShare = {
|
|
|
1664
2329
|
maxSec,
|
|
1665
2330
|
});
|
|
1666
2331
|
const sharedIds = [];
|
|
2332
|
+
const shareFailures = [];
|
|
2333
|
+
let shareUnverified = false;
|
|
1667
2334
|
let lastResult = null;
|
|
1668
2335
|
for (const batch of chunkArray(sourceItems, batchSize)) {
|
|
1669
2336
|
const payload = {
|
|
@@ -1681,9 +2348,24 @@ const workflowBatchShare = {
|
|
|
1681
2348
|
method: 'POST', path: WORKFLOW_ENDPOINTS.SHARE_ADD, operation: 'workflow.batchShare', body: payload,
|
|
1682
2349
|
}), payload);
|
|
1683
2350
|
lastResult = response;
|
|
1684
|
-
|
|
2351
|
+
// ★ 共享的逐条结果在**根级数组**:`data: [{sourceId, result:"true", msg}]`(真机样本;
|
|
2352
|
+
// `result` 是字符串、ID 键名是 `sourceId`)——按它计数,不再按"发出去几条",
|
|
2353
|
+
// 否则一条都没共享成功也会报成功(与 batchSubmit 的 P0 同源)。
|
|
2354
|
+
const outcome = parseBatchWriteOutcome(response, batch.map((it) => String(it.requestId)));
|
|
2355
|
+
sharedIds.push(...outcome.succeeded);
|
|
2356
|
+
shareFailures.push(...outcome.failed);
|
|
2357
|
+
if (outcome.unverified)
|
|
2358
|
+
shareUnverified = true;
|
|
1685
2359
|
}
|
|
1686
|
-
const
|
|
2360
|
+
const failures = await attachFailureTitles(shareFailures, env);
|
|
2361
|
+
const data = {
|
|
2362
|
+
status: failures.length || shareUnverified ? 'PARTIAL' : 'COMPLETE',
|
|
2363
|
+
sharedCount: sharedIds.length,
|
|
2364
|
+
requestIds: sharedIds,
|
|
2365
|
+
...(failures.length ? { failedCount: failures.length, failures } : {}),
|
|
2366
|
+
...(shareUnverified ? { unverified: true, warnings: [UNVERIFIED_NOTICE] } : {}),
|
|
2367
|
+
response: lastResult,
|
|
2368
|
+
};
|
|
1687
2369
|
await env.continuation.settle(key, data, 'workflow.batchShare');
|
|
1688
2370
|
return workflowSuccess('workflow.batchShare', data);
|
|
1689
2371
|
},
|
|
@@ -1738,6 +2420,8 @@ const workflowBatchForward = {
|
|
|
1738
2420
|
});
|
|
1739
2421
|
const forwardedIds = [];
|
|
1740
2422
|
let lastResult = null;
|
|
2423
|
+
// 拿不到服务端逐条结果时置位(详见 UNVERIFIED_NOTICE:不确认 ≠ 成功)
|
|
2424
|
+
let forwardUnverified = false;
|
|
1741
2425
|
for (const batch of chunkArray(sourceItems, batchSize)) {
|
|
1742
2426
|
const payload = {
|
|
1743
2427
|
src: 'forward',
|
|
@@ -1767,25 +2451,34 @@ const workflowBatchForward = {
|
|
|
1767
2451
|
method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_FORWARD, operation: 'workflow.batchForward', body: payload,
|
|
1768
2452
|
}), payload);
|
|
1769
2453
|
lastResult = response;
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2454
|
+
// 逐条结果在 `data.submitResult[]`(真机样本:**一层** `body.data`,且**没有** `failMsg`)。
|
|
2455
|
+
// 交给统一解析器:有界深度探测(不写死层级)+ 认布尔/字符串结果 + 取 `requestName`/`requestNameTitle` 作标题。
|
|
2456
|
+
// 旧实现写死 `body.data.data.submitResult`,比真实结构**多解一层** ⇒ 成功也被判成"无法确认"。
|
|
2457
|
+
const outcome = parseBatchWriteOutcome(response, batch.map((it) => String(it.requestId)));
|
|
2458
|
+
forwardedIds.push(...outcome.succeeded);
|
|
2459
|
+
if (outcome.unverified)
|
|
2460
|
+
forwardUnverified = true;
|
|
2461
|
+
if (outcome.failed.length) {
|
|
2462
|
+
const failed = await attachFailureTitles(outcome.failed, env);
|
|
2463
|
+
const partial = {
|
|
2464
|
+
status: 'PARTIAL',
|
|
2465
|
+
success: false,
|
|
2466
|
+
forwardedCount: forwardedIds.length,
|
|
2467
|
+
failedCount: failed.length,
|
|
2468
|
+
failures: failed,
|
|
2469
|
+
response,
|
|
2470
|
+
};
|
|
2471
|
+
await env.continuation.settle(key, partial, 'workflow.batchForward');
|
|
2472
|
+
return workflowSuccess('workflow.batchForward', partial);
|
|
1785
2473
|
}
|
|
1786
|
-
forwardedIds.push(...batch.map((it) => it.requestId));
|
|
1787
2474
|
}
|
|
1788
|
-
const data = {
|
|
2475
|
+
const data = {
|
|
2476
|
+
status: forwardUnverified ? 'PARTIAL' : 'COMPLETE',
|
|
2477
|
+
forwardedCount: forwardedIds.length,
|
|
2478
|
+
requestIds: forwardedIds,
|
|
2479
|
+
response: lastResult,
|
|
2480
|
+
...(forwardUnverified ? { unverified: true, warnings: [UNVERIFIED_NOTICE] } : {}),
|
|
2481
|
+
};
|
|
1789
2482
|
await env.continuation.settle(key, data, 'workflow.batchForward');
|
|
1790
2483
|
return workflowSuccess('workflow.batchForward', data);
|
|
1791
2484
|
},
|
|
@@ -1823,6 +2516,7 @@ const workflowBatchReject = {
|
|
|
1823
2516
|
const key = await claimBatchWrite(env, 'workflow.batchReject', requestIds);
|
|
1824
2517
|
const rejectedIds = [];
|
|
1825
2518
|
let lastResult = null;
|
|
2519
|
+
let rejectUnverified = false;
|
|
1826
2520
|
for (const batch of chunkArray(sourceItems, batchSize)) {
|
|
1827
2521
|
const payload = {
|
|
1828
2522
|
isMobile: false,
|
|
@@ -1844,25 +2538,33 @@ const workflowBatchReject = {
|
|
|
1844
2538
|
method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_REJECT, operation: 'workflow.batchReject', body: payload,
|
|
1845
2539
|
}), payload);
|
|
1846
2540
|
lastResult = response;
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2541
|
+
// 逐条结果在 `data.operateResult[]` + `data.failMsg`(真机样本:同样是**一层** `body.data`,
|
|
2542
|
+
// 且退回比转发多一个 `failMsg` 映射)——统一解析器两种都认,还会补上标题。
|
|
2543
|
+
const outcome = parseBatchWriteOutcome(response, batch.map((it) => String(it.requestId)));
|
|
2544
|
+
rejectedIds.push(...outcome.succeeded);
|
|
2545
|
+
if (outcome.unverified)
|
|
2546
|
+
rejectUnverified = true;
|
|
2547
|
+
if (outcome.failed.length) {
|
|
2548
|
+
const failed = await attachFailureTitles(outcome.failed, env);
|
|
2549
|
+
const partial = {
|
|
2550
|
+
status: 'PARTIAL',
|
|
2551
|
+
success: false,
|
|
2552
|
+
rejectedCount: rejectedIds.length,
|
|
2553
|
+
failedCount: failed.length,
|
|
2554
|
+
failures: failed,
|
|
2555
|
+
response,
|
|
2556
|
+
};
|
|
2557
|
+
await env.continuation.settle(key, partial, 'workflow.batchReject');
|
|
2558
|
+
return workflowSuccess('workflow.batchReject', partial);
|
|
1862
2559
|
}
|
|
1863
|
-
rejectedIds.push(...batch.map((it) => it.requestId));
|
|
1864
2560
|
}
|
|
1865
|
-
const data = {
|
|
2561
|
+
const data = {
|
|
2562
|
+
status: rejectUnverified ? 'PARTIAL' : 'COMPLETE',
|
|
2563
|
+
rejectedCount: rejectedIds.length,
|
|
2564
|
+
requestIds: rejectedIds,
|
|
2565
|
+
response: lastResult,
|
|
2566
|
+
...(rejectUnverified ? { unverified: true, warnings: [UNVERIFIED_NOTICE] } : {}),
|
|
2567
|
+
};
|
|
1866
2568
|
await env.continuation.settle(key, data, 'workflow.batchReject');
|
|
1867
2569
|
return workflowSuccess('workflow.batchReject', data);
|
|
1868
2570
|
},
|
|
@@ -1981,11 +2683,13 @@ export const workflowWriteOperations = [
|
|
|
1981
2683
|
workflowOperateApply,
|
|
1982
2684
|
workflowSharePrepare,
|
|
1983
2685
|
workflowShareApply,
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2686
|
+
// 6 个单命令批量操作统一挂「范围驱动」(scopeCondition / action / exRequestIds);
|
|
2687
|
+
// 走 requestIds 时行为与以往完全一致。
|
|
2688
|
+
withBatchScopeDriver(workflowBatchSubmit, 'SUBMIT'),
|
|
2689
|
+
withBatchScopeDriver(workflowBatchRead, 'READ'),
|
|
2690
|
+
withBatchScopeDriver(workflowBatchSupervise, 'URGE'),
|
|
2691
|
+
withBatchScopeDriver(workflowBatchShare, 'SHARE'),
|
|
2692
|
+
withBatchScopeDriver(workflowBatchForward, 'FORWARD'),
|
|
2693
|
+
withBatchScopeDriver(workflowBatchReject, 'REJECT'),
|
|
1990
2694
|
workflowMemoryExtract,
|
|
1991
2695
|
];
|