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
|
@@ -26,6 +26,13 @@ import { EB_BASE, ebBusinessSuccess, extractEbRows, queryBody } from './factory.
|
|
|
26
26
|
* 失败语义与标准版(create.ts 的 runMandatoryConflictChecks)对齐:
|
|
27
27
|
* - 应用设置读取失败:非致命,按「开启检测 + 仅提醒」继续,并在 warnings 中如实说明;
|
|
28
28
|
* - 检测请求本身失败:视为冲突 → 拒绝建会(`conflict_check_failed`),宁可拦住不可漏检。
|
|
29
|
+
*
|
|
30
|
+
* 建会路径闸门(2026-10-15 补齐)—— 与冲突检测共用同一次 baseSet 读取:
|
|
31
|
+
* 文档一直要求「`only_flow_create=1` 或审批开启 → CLI 不直接建会」,但 EB 版 prepare
|
|
32
|
+
* 只在**文档层**要求、**代码层没有拦**,于是应用设置 `only_flow_create=1` 时
|
|
33
|
+
* `meeting.eb.meeting.create.prepare` 仍放行、`apply` 照样落库(真机复现:产生
|
|
34
|
+
* `request_id` 为空、绕过流程的会议)。现由 `assertEbCreatePathAllowed` 在 prepare 阶段
|
|
35
|
+
* 直接拒绝(`conflict_rejected`),读不到应用设置则保守拒绝(`conflict_check_failed`)。
|
|
29
36
|
*/
|
|
30
37
|
/** 候选会议的有效状态:1审批中 / 2未开始 / 8已结束 / 9进行中。 */
|
|
31
38
|
const ACTIVE_MT_STATUS = '1,2,8,9';
|
|
@@ -69,8 +76,11 @@ async function listAllEbRows(env, interfacePk, baseQuery, operation) {
|
|
|
69
76
|
}
|
|
70
77
|
return { rows, requests };
|
|
71
78
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
79
|
+
/**
|
|
80
|
+
* 读取应用设置(query_meeting_baseset,返回 1 条,多条取第一条):
|
|
81
|
+
* 同时取「冲突策略」与「建会路径开关」,一次请求供两处使用。
|
|
82
|
+
*/
|
|
83
|
+
export async function fetchEbAppSettings(env, operation) {
|
|
74
84
|
try {
|
|
75
85
|
const rows = await requestEbList(env, 'query_meeting_baseset', { pageNo: 1, pageSize: 5 }, operation);
|
|
76
86
|
const base = rows[0] ?? {};
|
|
@@ -79,6 +89,10 @@ export async function fetchEbConflictPolicy(env, operation) {
|
|
|
79
89
|
roomAction: String(base.room_conflict ?? ''),
|
|
80
90
|
memberChk: String(base.member_conflict_chk ?? ''),
|
|
81
91
|
memberAction: String(base.member_conflict ?? ''),
|
|
92
|
+
onlyFlowCreate: String(base.only_flow_create ?? ''),
|
|
93
|
+
openMtApprove: String(base.open_mt_approve ?? ''),
|
|
94
|
+
mtNewWf: String(base.mt_new_wf ?? ''),
|
|
95
|
+
mtRepeatNewWf: String(base.mt_repeat_new_wf ?? ''),
|
|
82
96
|
};
|
|
83
97
|
}
|
|
84
98
|
catch (error) {
|
|
@@ -87,10 +101,50 @@ export async function fetchEbConflictPolicy(env, operation) {
|
|
|
87
101
|
roomAction: '',
|
|
88
102
|
memberChk: '',
|
|
89
103
|
memberAction: '',
|
|
104
|
+
onlyFlowCreate: '',
|
|
105
|
+
openMtApprove: '',
|
|
106
|
+
mtNewWf: '',
|
|
107
|
+
mtRepeatNewWf: '',
|
|
90
108
|
error: errorText(error),
|
|
91
109
|
};
|
|
92
110
|
}
|
|
93
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* 建会路径闸门(2026-10-15 补齐,口径与标准版 `create.ts` 的 `assertCreatePathAllowed` 对齐)。
|
|
114
|
+
*
|
|
115
|
+
* 规则:
|
|
116
|
+
* - `only_flow_create=1`(只允许通过流程新建会议)→ 拒绝:应用明确不允许流程之外建会;
|
|
117
|
+
* - `open_mt_approve=1`(会议提交需审批)→ 同样拒绝:会议须由 `mt_new_wf` 流程/ESB 创建,
|
|
118
|
+
* 手动 `create_meeting` 会绕开审批并产生重复记录(references 实测曾出现 4 条同名会议);
|
|
119
|
+
* - 应用设置**读不到** → 无法判定是否允许直接建会 ⇒ 保守拒绝(宁可拦住,不可绕过管控);
|
|
120
|
+
* - 开关为空 / 非 `1` → 放行(交由后续冲突检测与用户确认)。
|
|
121
|
+
*
|
|
122
|
+
* 拒绝后 CLI 不代为发起流程(流程集成不在本模块范围),只把已确认的信息交回调用方,
|
|
123
|
+
* 由调用方转 `weaver-e10-workflow`。
|
|
124
|
+
*/
|
|
125
|
+
export function assertEbCreatePathAllowed(settings) {
|
|
126
|
+
if (settings.error) {
|
|
127
|
+
throw meetingConflictError('conflict_check_failed', `无法读取会议应用设置(query_meeting_baseset),无法判定是否允许直接建会(only_flow_create / open_mt_approve),已按保守策略拒绝:${settings.error}`);
|
|
128
|
+
}
|
|
129
|
+
const blocked = [];
|
|
130
|
+
if (settings.onlyFlowCreate === '1') {
|
|
131
|
+
blocked.push('only_flow_create=1(只允许通过流程新建会议)');
|
|
132
|
+
}
|
|
133
|
+
if (settings.openMtApprove === '1') {
|
|
134
|
+
blocked.push('open_mt_approve=1(会议提交需审批)');
|
|
135
|
+
}
|
|
136
|
+
if (blocked.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
throw meetingConflictError('conflict_rejected', `应用设置不允许直接建会:${blocked.join(';')}。该应用的会议必须经流程创建`
|
|
139
|
+
+ `(新建会议工作流 mt_new_wf=${settings.mtNewWf || '未配置'},周期会议工作流 mt_repeat_new_wf=${settings.mtRepeatNewWf || '未配置'})——`
|
|
140
|
+
+ 'CLI 不直接建会、也不代替流程建会(否则会产生流程之外、request_id 为空的会议,且与流程/ESB 自动建会重复)。'
|
|
141
|
+
+ '请改用 weaver-e10-workflow 发起流程建会;如需 CLI 直接建会,请管理员先在会议应用设置中关闭该开关。', {
|
|
142
|
+
onlyFlowCreate: settings.onlyFlowCreate,
|
|
143
|
+
openMtApprove: settings.openMtApprove,
|
|
144
|
+
mtNewWf: settings.mtNewWf,
|
|
145
|
+
mtRepeatNewWf: settings.mtRepeatNewWf,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
94
148
|
/** 会议室冲突:查占用视图(query_roomusage_list),时间重叠即冲突。 */
|
|
95
149
|
async function checkRoomConflict(roomId, begin, end, excludeMtId, env, operation) {
|
|
96
150
|
const { rows, requests } = await listAllEbRows(env, 'query_roomusage_list',
|
|
@@ -179,7 +233,8 @@ export async function runEbConflictCheck(input, env, operation, options = {}) {
|
|
|
179
233
|
const begin = assertDateTimeMinute(input.beginDatetime, 'begin_datetime');
|
|
180
234
|
const end = assertDateTimeMinute(input.endDatetime, 'end_datetime');
|
|
181
235
|
const warnings = [];
|
|
182
|
-
|
|
236
|
+
// 调用方(建会/变更 preflight)已读过应用设置时直接复用,避免为闸门与冲突各发一次请求。
|
|
237
|
+
const policy = options.settings ?? await fetchEbAppSettings(env, operation);
|
|
183
238
|
const outcome = { warnings, policy };
|
|
184
239
|
if (policy.error) {
|
|
185
240
|
warnings.push(`应用设置(query_meeting_baseset)读取失败,冲突策略按「开启检测 + 仅提醒」处理:${policy.error}`);
|
|
@@ -268,20 +323,31 @@ export async function runEbConflictCheck(input, env, operation, options = {}) {
|
|
|
268
323
|
return outcome;
|
|
269
324
|
}
|
|
270
325
|
/**
|
|
271
|
-
* 建会/变更 prepare 的前置钩子:EB
|
|
272
|
-
*
|
|
326
|
+
* 建会/变更 prepare 的前置钩子:EB 版补上**建会路径闸门**与强制冲突检测。
|
|
327
|
+
*
|
|
328
|
+
* 顺序(重要):
|
|
329
|
+
* 1. 建会(`create_meeting`)**先过路径闸门** —— 应用设置不允许直接建会时直接拒绝,
|
|
330
|
+
* 连冲突检测都不必做(结论已定局);读应用设置失败同样拒绝。
|
|
331
|
+
* 2. 冲突检测仅在拿到起止时间、且传了 address / hrm_members 时才发请求(无目标即无冲突)。
|
|
273
332
|
*/
|
|
274
333
|
export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env }) => {
|
|
275
|
-
|
|
334
|
+
const isCreate = endpoint.interfacePk === 'create_meeting';
|
|
335
|
+
const isChange = endpoint.interfacePk === 'change_meeting';
|
|
336
|
+
if (!isCreate && !isChange)
|
|
276
337
|
return [];
|
|
277
|
-
|
|
338
|
+
const operation = isChange ? 'meeting.eb.meeting.change.prepare' : 'meeting.eb.meeting.create.prepare';
|
|
339
|
+
// 载荷自 2026-09-21 起为结构化(mainTable + details),闸门与冲突检测都只关心主表字段。
|
|
278
340
|
const main = payload.mainTable;
|
|
279
|
-
const begin = main.begin_datetime;
|
|
280
|
-
const end = main.end_datetime;
|
|
281
|
-
if (typeof begin !== 'string' || typeof end !== 'string' || !begin || !end)
|
|
282
|
-
return [];
|
|
341
|
+
const begin = typeof main.begin_datetime === 'string' ? main.begin_datetime : '';
|
|
342
|
+
const end = typeof main.end_datetime === 'string' ? main.end_datetime : '';
|
|
283
343
|
const address = typeof main.address === 'string' && main.address ? main.address : undefined;
|
|
284
344
|
const hrmMembers = typeof main.hrm_members === 'string' && main.hrm_members ? main.hrm_members : undefined;
|
|
345
|
+
// 建会必读应用设置(闸门);变更只在确有冲突检测目标时才读(避免多一次请求)。
|
|
346
|
+
const settings = isCreate || address || hrmMembers ? await fetchEbAppSettings(env, operation) : undefined;
|
|
347
|
+
if (settings && isCreate)
|
|
348
|
+
assertEbCreatePathAllowed(settings);
|
|
349
|
+
if (!begin || !end)
|
|
350
|
+
return [];
|
|
285
351
|
if (!address && !hrmMembers)
|
|
286
352
|
return [];
|
|
287
353
|
const outcome = await runEbConflictCheck({
|
|
@@ -291,7 +357,7 @@ export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env })
|
|
|
291
357
|
...(hrmMembers ? { hrmMembers } : {}),
|
|
292
358
|
...(typeof main.org_members === 'string' && main.org_members ? { orgMembers: main.org_members } : {}),
|
|
293
359
|
...(typeof args.id === 'string' || typeof args.id === 'number' ? { excludeMtId: String(args.id) } : {}),
|
|
294
|
-
}, env,
|
|
360
|
+
}, env, operation, settings ? { settings } : {});
|
|
295
361
|
return outcome.warnings;
|
|
296
362
|
};
|
|
297
363
|
/** 只查不建:会议室冲突检测 operation。 */
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
|
|
5
6
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
6
7
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
7
8
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
@@ -143,7 +144,7 @@ export function createOkrHost(ctx, dependencies = {}) {
|
|
|
143
144
|
mapRuntimeNetworkError(error);
|
|
144
145
|
}
|
|
145
146
|
if (response.status === 401 || response.status === 302) {
|
|
146
|
-
throw okrAuthError('session_expired',
|
|
147
|
+
throw okrAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
147
148
|
}
|
|
148
149
|
if (response.parseError) {
|
|
149
150
|
throw okrNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
|
|
5
6
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
6
7
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
7
8
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
@@ -150,7 +151,7 @@ export function createPlanHost(ctx, dependencies = {}) {
|
|
|
150
151
|
mapRuntimeNetworkError(error);
|
|
151
152
|
}
|
|
152
153
|
if (response.status === 401 || response.status === 302) {
|
|
153
|
-
throw planAuthError('session_expired',
|
|
154
|
+
throw planAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
154
155
|
}
|
|
155
156
|
if (response.parseError) {
|
|
156
157
|
throw planNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
|
|
1
2
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
2
3
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
3
4
|
import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
@@ -133,7 +134,7 @@ export function createProjectHost(ctx, dependencies = {}) {
|
|
|
133
134
|
mapRuntimeNetworkError(error);
|
|
134
135
|
}
|
|
135
136
|
if (response.status === 401 || response.status === 302) {
|
|
136
|
-
throw projectAuthError('session_expired',
|
|
137
|
+
throw projectAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, {
|
|
137
138
|
httpStatus: response.status,
|
|
138
139
|
});
|
|
139
140
|
}
|
|
@@ -50,6 +50,11 @@ export const WORKFLOW_ENDPOINTS = {
|
|
|
50
50
|
/** 我发起总数 / 列表 */
|
|
51
51
|
MINE_TOTAL: '/api/workflow/list/total/getMineListTotal',
|
|
52
52
|
MINE_LIST: '/api/workflow/list/data/getMineListData',
|
|
53
|
+
/**
|
|
54
|
+
* 我发起——**工作流树**(`data.data[]` = 流程大类,`children` = 具体工作流,`id` 形如 `wf_<workflowId>`)。
|
|
55
|
+
* 入参与 `MINE_LIST` 一致、去掉分页参数。发起侧消歧用(2026-10-20)。
|
|
56
|
+
*/
|
|
57
|
+
MINE_TREE: '/api/workflow/list/tree/getMineListTree',
|
|
53
58
|
/** 全部总数 / 列表 */
|
|
54
59
|
ALL_TOTAL: '/api/workflow/list/total/getAllListTotal',
|
|
55
60
|
ALL_LIST: '/api/workflow/list/data/getAllListData',
|
|
@@ -138,6 +143,12 @@ export const FORM_ENDPOINTS = {
|
|
|
138
143
|
verificationConditionRule: '/api/bs/workflow/pathdef/nodeConformance/verificationConditionRule',
|
|
139
144
|
/** 提交已创建的流程(创建后页面重载拿到 view 页鉴权再提交,与真机同路径) */
|
|
140
145
|
flowSubmit: '/api/workflow/core/flow/submit',
|
|
146
|
+
/**
|
|
147
|
+
* 签字意见的默认样式(字体 / 字号 / 是否纯文本输入)。
|
|
148
|
+
*
|
|
149
|
+
* 提交前取一次,用来把意见内容包成服务端能正确渲染的富文本形态;取不到就退回原样式。
|
|
150
|
+
*/
|
|
151
|
+
signInputInfo: '/api/workflow/core/flowPage/loadSignInputInfo',
|
|
141
152
|
/** 租户级文件上传设置(单文件大小上限 + 格式黑白名单) */
|
|
142
153
|
tenantSetting: '/api/file/tenant/setting',
|
|
143
154
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
|
|
1
2
|
import { requireE10Session } from '../../internal/e10/context.js';
|
|
2
3
|
import { getActiveProfile } from '../../internal/e10/auth/session.js';
|
|
3
4
|
import { createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
|
|
@@ -118,9 +119,6 @@ export function extractApiBusinessError(payload) {
|
|
|
118
119
|
}
|
|
119
120
|
return '';
|
|
120
121
|
}
|
|
121
|
-
function loginExpiredMessage() {
|
|
122
|
-
return 'OA 登录状态或系统认证信息无效,本次任务已停止。请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录,随后重放原请求。';
|
|
123
|
-
}
|
|
124
122
|
export function createWorkflowHost(ctx, dependencies = {}) {
|
|
125
123
|
let session = null;
|
|
126
124
|
let sessionContext = null;
|
|
@@ -150,7 +148,7 @@ export function createWorkflowHost(ctx, dependencies = {}) {
|
|
|
150
148
|
return sessionContext;
|
|
151
149
|
}
|
|
152
150
|
async function run(request, responseType) {
|
|
153
|
-
const { method = 'POST', path, query, body, form, formData, operation, timeoutMs = DEFAULT_TIMEOUT_MS, } = request;
|
|
151
|
+
const { method = 'POST', path, query, headers, body, form, formData, operation, timeoutMs = DEFAULT_TIMEOUT_MS, } = request;
|
|
154
152
|
const runtime = createE10RequestRuntime(ctx, await ensureSession(), {
|
|
155
153
|
interceptors: dependencies.interceptors,
|
|
156
154
|
fetch: dependencies.fetch,
|
|
@@ -160,7 +158,7 @@ export function createWorkflowHost(ctx, dependencies = {}) {
|
|
|
160
158
|
});
|
|
161
159
|
let response;
|
|
162
160
|
try {
|
|
163
|
-
const base = { method, path, query, body, form, formData, operation, timeoutMs, module: 'workflow' };
|
|
161
|
+
const base = { method, path, query, headers, body, form, formData, operation, timeoutMs, module: 'workflow' };
|
|
164
162
|
response = responseType === 'binary'
|
|
165
163
|
? await runtime.requestBinary(base)
|
|
166
164
|
: await runtime.requestJson(base);
|
|
@@ -174,7 +172,7 @@ export function createWorkflowHost(ctx, dependencies = {}) {
|
|
|
174
172
|
}
|
|
175
173
|
if ('content' in response) {
|
|
176
174
|
if (isLoginExpired({}, response.status)) {
|
|
177
|
-
throw workflowAuthError('session_expired',
|
|
175
|
+
throw workflowAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
|
|
178
176
|
}
|
|
179
177
|
return {
|
|
180
178
|
status: response.status,
|
|
@@ -184,7 +182,7 @@ export function createWorkflowHost(ctx, dependencies = {}) {
|
|
|
184
182
|
};
|
|
185
183
|
}
|
|
186
184
|
if (isLoginExpired(response.value, response.status)) {
|
|
187
|
-
throw workflowAuthError('session_expired',
|
|
185
|
+
throw workflowAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, {
|
|
188
186
|
httpStatus: response.status,
|
|
189
187
|
details: { endpointKey: operation, response: response.value },
|
|
190
188
|
});
|
|
@@ -81,47 +81,17 @@ function failureEnvelope(error) {
|
|
|
81
81
|
*/
|
|
82
82
|
export { compactStdoutPaths };
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* 用户 2026-10-15 裁定:「xiaoe 下应该直接输出 `<browsertool>` 标签」。即该档的 stdout **就是**
|
|
87
|
-
* 那段标签块本身,**不再套** `{schemaVersion, ok, operation, data, warnings}` 信封 —— 于是
|
|
88
|
-
* `workflow-create.md` 里「返回内容以 `<browsertool>` 开头 ⇒ 原样输出」这句是**字面成立**的。
|
|
89
|
-
* 代价(用户已知悉并接受):这一档模型拿不到 `status` / `contextId`(本档不写服务端、句柄也未消费,
|
|
90
|
-
* 需要重发时由用户重新发起即可)。
|
|
91
|
-
*
|
|
92
|
-
* 两个条件必须同时成立,缺一就退回信封 —— 宁可不直出,也不让调用方拿到半截或空输出:
|
|
93
|
-
* - `data.status === 'BROWSER_FILL'`:小e 专用档,其它状态(含 `NEEDS_INPUT`)一律维持信封;
|
|
94
|
-
* - `data.finalMarkdown` 以 `<browsertool>` 开头:与文档口径同字面,渲染档万一变了也不会直出一个
|
|
95
|
-
* 看起来像标签、实际不是的东西。
|
|
96
|
-
*
|
|
97
|
-
* 必须走 `ctx.output.raw()` 而不是 `write(value, { text })`:后者在**全局 `--json`** 下仍会退化成
|
|
98
|
-
* JSON(`core/output.ts`:`useJson = globalOptions.json || options.json || !options.text`),
|
|
99
|
-
* 而本 skill 的文档一律带 `--json`。
|
|
100
|
-
*/
|
|
101
|
-
export function rawStdoutOf(result) {
|
|
102
|
-
const data = result.data;
|
|
103
|
-
if (!data || typeof data !== 'object')
|
|
104
|
-
return undefined;
|
|
105
|
-
if (data.status !== 'BROWSER_FILL')
|
|
106
|
-
return undefined;
|
|
107
|
-
const markdown = data.finalMarkdown;
|
|
108
|
-
if (typeof markdown !== 'string' || !markdown.startsWith('<browsertool>'))
|
|
109
|
-
return undefined;
|
|
110
|
-
return markdown;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* 把一次 operation 的结果打到 stdout —— **形态二选一**(判定见 `rawStdoutOf`)。
|
|
84
|
+
* 把一次 operation 的结果打到 stdout —— **只有一种形态:JSON 信封**。
|
|
114
85
|
*
|
|
115
86
|
* 独立成函数并**导出**,是为了让用例直接锁住这条接线(与 `compactStdoutPaths` 同一理由:
|
|
116
87
|
* 回归要在这里红,而不是等到真机上模型拿到一份"既不是 JSON、也不是标签块"的东西才发现)。
|
|
88
|
+
*
|
|
89
|
+
* ★ 2026-10-21 用户裁定(**改口径**):小e 的 `BROWSER_FILL` 档**不再直出** `<browsertool>` 标签块,
|
|
90
|
+
* 一律走 JSON 信封 —— 标签块放在 `data.finalMarkdown` 里,与 `createRequestDirect` 统一成一种结构,
|
|
91
|
+
* 于是 `durationMs` / `apiDurations` / `apiSlowest` / `authMs` 四个耗时键在这一档也可见。
|
|
92
|
+
* 原来的 `rawStdoutOf()` 直出通道随之删除(它当时只为"stdout 就是标签块"服务)。
|
|
117
93
|
*/
|
|
118
94
|
export function printWorkflowResult(ctx, operation, result) {
|
|
119
|
-
const raw = rawStdoutOf(result);
|
|
120
|
-
if (raw !== undefined) {
|
|
121
|
-
// 小e:stdout 就是标签块本身 —— 不套信封、不进 `prepare()` 的超线落盘(这段本来就不长)。
|
|
122
|
-
ctx.output.raw(raw);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
95
|
ctx.output.write(successEnvelope(operation, result), {
|
|
126
96
|
json: true,
|
|
127
97
|
compactPaths: compactStdoutPaths(operation),
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 智能填参:调 OA 的 `chatScene` 接口,让一个**独立智能体**把「用户意图 + 表单字段上下文」
|
|
3
|
+
* 映射成可直接喂给 `createRequest` 的 `data` 参数。
|
|
4
|
+
*
|
|
5
|
+
* ## 接口契约(来源:2026-10-19 真机 HAR,HTTP 200)
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* POST {baseUrl}/api/ai/application/chat/chatScene
|
|
9
|
+
* aiappuniquekey: workflow_form_filler
|
|
10
|
+
* Content-Type: application/json;charset=UTF-8
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* - body 里 `chatId`(19 位数字串)/ `taskId` / `userMessageId`(32 位无横线 hex)**每次随机**,
|
|
14
|
+
* 其余字段与值固定;
|
|
15
|
+
* - `messages[0].content` 是**字符串**(入参 JSON 序列化后的文本),**不是对象**;
|
|
16
|
+
* - 返回的 `data.chatMessage.content` 同样是**字符串**,要**二次 `JSON.parse`**;
|
|
17
|
+
* - **同步返回**(真机 `timeConsuming.all = 3592 ms`),不需要轮询 —— `pollingInterval` 在本场景不起作用。
|
|
18
|
+
*
|
|
19
|
+
* ## 失败口径
|
|
20
|
+
*
|
|
21
|
+
* 这个接口是**增强**而不是前置依赖:拿不到合法结果就返回 `ok:false`,由调用方原样退回旧口径
|
|
22
|
+
* (保留 `historyRequestData` / `systemFields`)。**任何异常都不向上抛**,绝不因为智能体故障
|
|
23
|
+
* 把整个发起流程打死。
|
|
24
|
+
*/
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
26
|
+
/** 智能体应用标识。注意是**下划线**形态(不是早期的 `workflow-form-filler`)。 */
|
|
27
|
+
export const CHATSCENE_APP_UNIQUE_KEY = 'workflow_form_filler';
|
|
28
|
+
/** 接口路径(`/api/` 前缀是 host 的强制要求)。 */
|
|
29
|
+
export const CHATSCENE_PATH = '/api/ai/application/chat/chatScene';
|
|
30
|
+
/**
|
|
31
|
+
* 单独超时:真机 3.6 s,但这是**外部大模型**调用,可能排队。
|
|
32
|
+
*
|
|
33
|
+
* ★ 2026-10-20 用户裁定:**15 s**(此前 30 s)。实测该智能体真出状况时服务端自身也要挂到 30 s 才回
|
|
34
|
+
* `InterruptedIOException`,等它没有意义 —— 反正拿不到可用结果就会降级,早 15 s 放弃能让整条
|
|
35
|
+
* `createForm` 少等一半。失败口径不变(仍静默降级、不打断发起流程)。
|
|
36
|
+
*/
|
|
37
|
+
const CHATSCENE_TIMEOUT_MS = 15_000;
|
|
38
|
+
/**
|
|
39
|
+
* 填写依据的后缀:智能体在每个字段值旁写 `<字段id>_reason`(实测还会写成 `_reason2`、`_reason3`),
|
|
40
|
+
* 这些键**必须**在进 `data` 之前剔掉。
|
|
41
|
+
*
|
|
42
|
+
* ★ 漏剔一个不是"多提交一个字段",而是**整张单被卡住**:`createRequest` 的 `bindInputFields`
|
|
43
|
+
* 会把 `main` / 明细行里的每个键拿去和字段 id / 名称匹配,匹配不到就记一条 `NOT_FOUND` 歧义,
|
|
44
|
+
* 而 `commandCreateRequest` 一见 `fieldAmbiguities.length` 就直接返回 `NEEDS_CONFIRMATION`。
|
|
45
|
+
*
|
|
46
|
+
* 依据本身**只用于调试参数是怎么生成出来的**,不进任何返回面(2026-10-20 用户裁定)。
|
|
47
|
+
*/
|
|
48
|
+
const REASON_KEY = /_reason\d*$/;
|
|
49
|
+
/**
|
|
50
|
+
* 19 位数字串,形态对齐雪花 ID(真机 `1327983839164596324`)。
|
|
51
|
+
*
|
|
52
|
+
* 用 `Date.now() * 1e6 + 随机 6 位`:时间戳保证单调整体、尾 6 位保证同毫秒内不撞。
|
|
53
|
+
* 必须走 `BigInt` —— `1.79e12 × 1e6 = 1.79e18` 已远超 `Number.MAX_SAFE_INTEGER`(9.0e15),
|
|
54
|
+
* 用 number 会丢精度、末尾几位变成 0。
|
|
55
|
+
*/
|
|
56
|
+
function randomChatId() {
|
|
57
|
+
const ts = BigInt(Date.now()) * 1000000n;
|
|
58
|
+
const seq = BigInt(Math.floor(Math.random() * 1_000_000));
|
|
59
|
+
return (ts + seq).toString();
|
|
60
|
+
}
|
|
61
|
+
/** 32 位无横线 hex(真机 `taskId` / `userMessageId` 的形态)。 */
|
|
62
|
+
function randomHex32() {
|
|
63
|
+
return randomUUID().replace(/-/g, '');
|
|
64
|
+
}
|
|
65
|
+
/** 本地时间 `YYYY-MM-DD HH:mm:ss`,供智能体把「今天 / 下周三 / 上个月」换算成绝对日期。 */
|
|
66
|
+
export function formatLocalNow(date = new Date()) {
|
|
67
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
68
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
69
|
+
+ ` ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 交给智能体的字段属性**白名单** —— 就是智能体入参契约里的那 11 个键,一个不多一个不少
|
|
73
|
+
* (2026-10-20 用户对齐真机入参)。为此要挡掉 `createForm` 侧那些**只给 CLI 自己用**的属性:
|
|
74
|
+
*
|
|
75
|
+
* | 属性 | 为什么不给智能体 |
|
|
76
|
+
* | --- | --- |
|
|
77
|
+
* | `initialValue` | 字段默认值:智能体只负责按用户意图产值,不需要(也不该被)默认值影响(2026-10-22 用户裁定去掉) |
|
|
78
|
+
* | `fileUploadParams` | 文件夹 / 大小格式限制 / 一次性签名,是给调 caizhilian 上传的模型用的;带上它会让智能体误以为要自己判上传限制 |
|
|
79
|
+
* | `browserType` / `browserModule` | 浏览通道标识,由 CLI 在 `createRequest` 解析值时用,与「意图 → 值」无关 |
|
|
80
|
+
* | `readOnly` / `disableEdit` / `folderDependsOnOption` | 是否可写、附件文件夹是否随选项变,都是 CLI 侧的门禁/联动细节 |
|
|
81
|
+
*
|
|
82
|
+
* 键序与契约一致(`componentKey` → 布尔标记 → `format` → `options`)。
|
|
83
|
+
*/
|
|
84
|
+
const AGENT_FIELD_KEYS = [
|
|
85
|
+
'id', 'name', 'subFormId', 'componentKey',
|
|
86
|
+
'isBrowser', 'isSelect', 'isMultiple', 'isEmployeeScope', 'isEinvoice',
|
|
87
|
+
'format', 'options',
|
|
88
|
+
];
|
|
89
|
+
function pickAgentFields(fields) {
|
|
90
|
+
const picked = [];
|
|
91
|
+
for (const field of fields) {
|
|
92
|
+
if (!field || typeof field !== 'object')
|
|
93
|
+
continue;
|
|
94
|
+
const item = {};
|
|
95
|
+
for (const key of AGENT_FIELD_KEYS) {
|
|
96
|
+
const value = field[key];
|
|
97
|
+
if (value === undefined || value === null)
|
|
98
|
+
continue;
|
|
99
|
+
item[key] = value;
|
|
100
|
+
}
|
|
101
|
+
picked.push(item);
|
|
102
|
+
}
|
|
103
|
+
return picked;
|
|
104
|
+
}
|
|
105
|
+
export function buildChatSceneInput(params) {
|
|
106
|
+
return {
|
|
107
|
+
userQuestion: params.userQuestion,
|
|
108
|
+
workflow: { id: String(params.workflow.id ?? ''), name: String(params.workflow.name ?? '') },
|
|
109
|
+
fields: pickAgentFields(params.fields || []),
|
|
110
|
+
historyRequestData: params.historyRequestData ?? null,
|
|
111
|
+
userMemory: String(params.userMemory ?? ''),
|
|
112
|
+
now: params.now || formatLocalNow(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** 摘掉一行里的所有依据键(`_reason` / `_reason2` …),只留真值;空值也不留。 */
|
|
116
|
+
function stripReasonKeys(source) {
|
|
117
|
+
const values = {};
|
|
118
|
+
for (const [key, value] of Object.entries(source)) {
|
|
119
|
+
if (REASON_KEY.test(key))
|
|
120
|
+
continue;
|
|
121
|
+
if (value === null || value === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
values[key] = value;
|
|
124
|
+
}
|
|
125
|
+
return values;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 把智能体返回的 `formData` 收敛成 `{main, details}`,并**摘掉所有依据键**。
|
|
129
|
+
* 结构不对就返回 null(=降级)。
|
|
130
|
+
*/
|
|
131
|
+
function normalizeFormData(raw) {
|
|
132
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
133
|
+
return null;
|
|
134
|
+
const mainRaw = raw.main;
|
|
135
|
+
const detailsRaw = raw.details;
|
|
136
|
+
const main = {};
|
|
137
|
+
if (mainRaw && typeof mainRaw === 'object' && !Array.isArray(mainRaw)) {
|
|
138
|
+
Object.assign(main, stripReasonKeys(mainRaw));
|
|
139
|
+
}
|
|
140
|
+
const details = {};
|
|
141
|
+
if (detailsRaw && typeof detailsRaw === 'object' && !Array.isArray(detailsRaw)) {
|
|
142
|
+
for (const [subFormId, rows] of Object.entries(detailsRaw)) {
|
|
143
|
+
if (!Array.isArray(rows))
|
|
144
|
+
continue;
|
|
145
|
+
const kept = [];
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
148
|
+
continue;
|
|
149
|
+
const values = stripReasonKeys(row);
|
|
150
|
+
// 整行只有依据键、一个值都没有 ⇒ 不产行(否则会多出一行空明细)
|
|
151
|
+
if (!Object.keys(values).length)
|
|
152
|
+
continue;
|
|
153
|
+
kept.push(values);
|
|
154
|
+
}
|
|
155
|
+
if (!kept.length)
|
|
156
|
+
continue;
|
|
157
|
+
details[subFormId] = kept;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 一个字段都没有 ⇒ 视为没拿到结果(否则会把整张空表单当成"已填好"交出去)
|
|
161
|
+
if (!Object.keys(main).length && !Object.keys(details).length)
|
|
162
|
+
return null;
|
|
163
|
+
return { data: { main, details } };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `systemData` 只收四个已知键、统一成字符串;缺键 / 非字符串一律当空串。
|
|
167
|
+
* 四个键的 `_reason` 变体本来就不在白名单里,直接不收。
|
|
168
|
+
*/
|
|
169
|
+
function normalizeSystemData(raw) {
|
|
170
|
+
const source = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
|
|
171
|
+
const systemData = {};
|
|
172
|
+
for (const key of ['requestName', 'requestLevel', 'secLevel', 'remark']) {
|
|
173
|
+
const value = source[key];
|
|
174
|
+
systemData[key] = (value === null || value === undefined) ? '' : String(value).trim();
|
|
175
|
+
}
|
|
176
|
+
return systemData;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 调 `chatScene` 并解析结果。**永不抛异常**。
|
|
180
|
+
*
|
|
181
|
+
* 判成功的三个条件(缺一即降级,对应需求「返回值 data.chatMessage.content 不存在或者为空则走原逻辑」):
|
|
182
|
+
* 1. 外层 `code === 200 && status === true`;
|
|
183
|
+
* 2. `data.chatMessage.content` 是**非空字符串**;
|
|
184
|
+
* 3. 该字符串能 parse 成 JSON,且里面有非空的 `formData`。
|
|
185
|
+
*/
|
|
186
|
+
export async function requestFormFill(env, input) {
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
const base = () => ({ ok: false, durationMs: Date.now() - startedAt });
|
|
189
|
+
try {
|
|
190
|
+
const body = {
|
|
191
|
+
appUniqueKey: CHATSCENE_APP_UNIQUE_KEY,
|
|
192
|
+
fromPersonalHome: false,
|
|
193
|
+
chatId: randomChatId(),
|
|
194
|
+
appConfigType: 3,
|
|
195
|
+
// ★ content 必须是**字符串**:整个入参 JSON 序列化一次
|
|
196
|
+
messages: [{ content: JSON.stringify(input), role: 'user' }],
|
|
197
|
+
isNeedContext: 1,
|
|
198
|
+
regenerate: false,
|
|
199
|
+
pollingInterval: 200,
|
|
200
|
+
globalParam: {},
|
|
201
|
+
sessionParam: {},
|
|
202
|
+
frontToolList: [],
|
|
203
|
+
searchType: 1,
|
|
204
|
+
userMessageId: randomHex32(),
|
|
205
|
+
assist: { fileList: [], quoteContent: '' },
|
|
206
|
+
taskId: randomHex32(),
|
|
207
|
+
clientType: 1,
|
|
208
|
+
};
|
|
209
|
+
const response = await env.host.workflowRequest({
|
|
210
|
+
method: 'POST',
|
|
211
|
+
path: CHATSCENE_PATH,
|
|
212
|
+
// 该接口靠这个头定位智能体应用;workflow 系列接口不需要它
|
|
213
|
+
headers: { aiappuniquekey: CHATSCENE_APP_UNIQUE_KEY },
|
|
214
|
+
body,
|
|
215
|
+
operation: 'workflow.createForm',
|
|
216
|
+
timeoutMs: CHATSCENE_TIMEOUT_MS,
|
|
217
|
+
// 失败要能自己读结论(也避免把 code/status 异常抛成业务错误中断 createForm)
|
|
218
|
+
tolerateBusinessError: true,
|
|
219
|
+
});
|
|
220
|
+
const root = (response && typeof response === 'object') ? response : {};
|
|
221
|
+
if (Number(root.code) !== 200 || root.status !== true) {
|
|
222
|
+
return { ...base(), reason: `chatScene 返回 code=${String(root.code)} status=${String(root.status)}` };
|
|
223
|
+
}
|
|
224
|
+
const content = root?.data?.chatMessage?.content;
|
|
225
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
226
|
+
return { ...base(), reason: 'chatScene 未返回 chatMessage.content' };
|
|
227
|
+
}
|
|
228
|
+
let parsed;
|
|
229
|
+
try {
|
|
230
|
+
parsed = JSON.parse(content);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return { ...base(), reason: 'chatMessage.content 不是合法 JSON' };
|
|
234
|
+
}
|
|
235
|
+
const normalized = normalizeFormData(parsed?.formData);
|
|
236
|
+
if (!normalized)
|
|
237
|
+
return { ...base(), reason: '返回值里没有可用的 formData' };
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
data: normalized.data,
|
|
241
|
+
systemData: normalizeSystemData(parsed?.systemData),
|
|
242
|
+
durationMs: Date.now() - startedAt,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
// 超时 / 网络 / HTTP 5xx / 登录态以外的任何问题,一律降级,不打断 createForm。
|
|
247
|
+
const message = (error && typeof error === 'object' && 'message' in error)
|
|
248
|
+
? String(error.message)
|
|
249
|
+
: String(error);
|
|
250
|
+
return { ...base(), reason: `chatScene 调用失败:${message}` };
|
|
251
|
+
}
|
|
252
|
+
}
|