aitable-workflow-cli 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +1 -6
  2. package/dist/{chunk-IGB6WDLR.js → chunk-UNPV6DTX.js} +1 -1
  3. package/dist/cli.js +47 -47
  4. package/dist/config-set-FBOQBRSJ.js +13 -0
  5. package/dist/{config-ui-server-CORDAOYI.js → config-ui-server-IUSACAOT.js} +1 -1
  6. package/package.json +3 -3
  7. package/dist/config-set-EGLLIMVJ.js +0 -13
  8. package/templates/support-qa/.env.example +0 -16
  9. package/templates/support-qa/aitable-workflow.config.yml +0 -46
  10. package/templates/support-qa/coding-agents.config.json +0 -18
  11. package/templates/support-qa/lib/__tests__/declarative-matcher.test.ts +0 -240
  12. package/templates/support-qa/lib/__tests__/dingtalk-group-membership.test.ts +0 -144
  13. package/templates/support-qa/lib/__tests__/memory-events.test.ts +0 -90
  14. package/templates/support-qa/lib/__tests__/sandbox-agent.test.ts +0 -382
  15. package/templates/support-qa/lib/__tests__/thread-adapter.test.ts +0 -242
  16. package/templates/support-qa/lib/__tests__/thread-observe-step.test.ts +0 -391
  17. package/templates/support-qa/lib/__tests__/thread-observer-handler.test.ts +0 -274
  18. package/templates/support-qa/lib/action-dispatch.ts +0 -262
  19. package/templates/support-qa/lib/contract.ts +0 -253
  20. package/templates/support-qa/lib/declarative-matcher.ts +0 -398
  21. package/templates/support-qa/lib/dingtalk-group-membership.ts +0 -158
  22. package/templates/support-qa/lib/guards.ts +0 -18
  23. package/templates/support-qa/lib/memory-events.ts +0 -113
  24. package/templates/support-qa/lib/owner-resolver.ts +0 -94
  25. package/templates/support-qa/lib/preflight.ts +0 -294
  26. package/templates/support-qa/lib/profile.ts +0 -51
  27. package/templates/support-qa/lib/sandbox-agent.ts +0 -653
  28. package/templates/support-qa/lib/thread-adapter.ts +0 -281
  29. package/templates/support-qa/lib/thread-digest.ts +0 -144
  30. package/templates/support-qa/npmrc +0 -1
  31. package/templates/support-qa/package.json +0 -27
  32. package/templates/support-qa/profiles/aitable/profile/escalation/contacts.json +0 -37
  33. package/templates/support-qa/profiles/aitable/profile/escalation/owner-map.json +0 -18
  34. package/templates/support-qa/profiles/aitable/profile/fastpath/known-answers.yml +0 -561
  35. package/templates/support-qa/profiles/aitable/profile/profile.yml +0 -219
  36. package/templates/support-qa/profiles/aitable/profile/prompts/kb-editor.md +0 -3
  37. package/templates/support-qa/profiles/aitable/profile/prompts/persona.md +0 -49
  38. package/templates/support-qa/profiles/aitable/profile/prompts/troubleshooter.md +0 -5
  39. package/templates/support-qa/profiles/aitable/profile/troubleshoot/classifiers.yml +0 -36
  40. package/templates/support-qa/profiles/aitable/skills/troubleshooting/SKILL.md +0 -58
  41. package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-import-troubleshooter/SKILL.md +0 -485
  42. package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-openapi-troubleshooter/SKILL.md +0 -183
  43. package/templates/support-qa/profiles/aitable/skills/troubleshooting/import-openapi-e2e-troubleshooter/SKILL.md +0 -459
  44. package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-datasource-troubleshooter/SKILL.md +0 -1286
  45. package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-field-troubleshooter/SKILL.md +0 -673
  46. package/templates/support-qa/profiles/aitable/skills/troubleshooting/spreadsheet-datasync-troubleshooter/SKILL.md +0 -322
  47. package/templates/support-qa/profiles/default/profile/fastpath/known-answers.yml +0 -32
  48. package/templates/support-qa/profiles/default/profile/profile.yml +0 -217
  49. package/templates/support-qa/profiles/default/profile/troubleshoot/classifiers.yml +0 -37
  50. package/templates/support-qa/profiles/default/wiki/support//345/270/270/350/247/201/351/227/256/351/242/230.md +0 -10
  51. package/templates/support-qa/tsconfig.json +0 -16
  52. package/templates/support-qa/vitest.config.ts +0 -9
  53. package/templates/support-qa/workflows/admin-kb-update/recipes/kb-refine.recipe.ts +0 -137
  54. package/templates/support-qa/workflows/admin-kb-update/recipes/kb_sync_handler.recipe.ts +0 -119
  55. package/templates/support-qa/workflows/admin-kb-update/workflow.yml +0 -96
  56. package/templates/support-qa/workflows/config-sync/recipes/config_sync.recipe.ts +0 -668
  57. package/templates/support-qa/workflows/config-sync/recipes/config_sync_handler.recipe.ts +0 -16
  58. package/templates/support-qa/workflows/config-sync/workflow.yml +0 -62
  59. package/templates/support-qa/workflows/daily-review/recipes/daily_review.recipe.ts +0 -602
  60. package/templates/support-qa/workflows/daily-review/recipes/daily_review_handler.recipe.ts +0 -26
  61. package/templates/support-qa/workflows/daily-review/workflow.yml +0 -65
  62. package/templates/support-qa/workflows/group-qa/recipes/answer.recipe.ts +0 -231
  63. package/templates/support-qa/workflows/group-qa/recipes/conversation_handler.recipe.ts +0 -394
  64. package/templates/support-qa/workflows/group-qa/recipes/fastpath.recipe.ts +0 -72
  65. package/templates/support-qa/workflows/group-qa/recipes/on-action.recipe.ts +0 -225
  66. package/templates/support-qa/workflows/group-qa/recipes/reply-to-group.recipe.ts +0 -202
  67. package/templates/support-qa/workflows/group-qa/recipes/troubleshoot.recipe.ts +0 -273
  68. package/templates/support-qa/workflows/group-qa/workflow.yml +0 -268
  69. package/templates/support-qa/workflows/thread-observer/README.md +0 -96
  70. package/templates/support-qa/workflows/thread-observer/recipes/thread_observe.recipe.ts +0 -415
  71. package/templates/support-qa/workflows/thread-observer/recipes/thread_observer_handler.recipe.ts +0 -213
  72. package/templates/support-qa/workflows/thread-observer/workflow.yml +0 -185
  73. package/templates/support-qa/workspaces/daily-review/AGENTS.md +0 -233
  74. package/templates/support-qa/workspaces/kb-editor/AGENTS.md +0 -32
  75. package/templates/support-qa/workspaces/support/AGENTS.md +0 -156
  76. package/templates/support-qa/workspaces/thread-observer/AGENTS.md +0 -156
  77. package/templates/support-qa/workspaces/troubleshooter/AGENTS.md +0 -71
@@ -1,225 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import nodePath from "node:path";
3
- import { z } from "zod";
4
- import { resolveGlobalVarRef, type RecipeDefinition, type RecipeContext, type RecipeResult } from "aitable-workflow-core";
5
- import { resolveGroupMention } from "../../../lib/dingtalk-group-membership.js";
6
- import { SUPPORT_ACTIONS, type SupportAction, KB_STATUS } from "../../../lib/contract.js";
7
- import { DEFAULT_ON_ACTION, type ActionPolicy, type ActionPolicyOverrides } from "../../../lib/profile.js";
8
- import { resolveOwner, type OwnerResolution } from "../../../lib/owner-resolver.js";
9
- import { buildActionDispatch, needsOwnerResolution, shouldRecordOwnerFailure } from "../../../lib/action-dispatch.js";
10
- import { SUPPORT_MEMORY_KINDS, SUPPORT_MEMORY_ROLES, supportEntry, supportScope } from "../../../lib/memory-events.js";
11
-
12
- type GroupConfig = { groupId: string; outbound?: string; corpId?: string };
13
-
14
- function loadGroupConfig(ctx: RecipeContext, source: unknown, conversationId: string): GroupConfig | null {
15
- const list = resolveGlobalVarRef(source, ctx.getGlobalVar) as GroupConfig[] | undefined;
16
- if (!Array.isArray(list)) return null;
17
- return list.find((group) => group.groupId === conversationId) ?? null;
18
- }
19
-
20
- /**
21
- * 决策后处置分发(support-qa 尾前步,取代 on-silence——所有最终处置的统一分发器,spec §10)
22
- *
23
- * 职责:
24
- * 1. 按 action 查 policies(profile v2:ctx.config.policies,来自 profile.yml 的
25
- * workflows.group-qa.steps.on_action.policies 段)得出 track/group;
26
- * 2. track=false → tracker:false(整行 discard);
27
- * 3. 轴2 Owner 解析(needsOwnerResolution):bug/feature/kb_gap 确定性解析 module→Owner→userid
28
- * (数据源:config-sync 从功能模块人员配置表发布的 ${globalVar.module_owners} 快照),
29
- * **与 group 档位无关**(静默时 Owner 仍需进通知字段);解析失败按
30
- * shouldRecordOwnerFailure 过滤后记 owner_resolution_failed;
31
- * 4. 轴1 群内可见性 + 轴2 Owner 尾注由 buildActionDispatch 正交合成 final_reply
32
- * (group=answer 也可带 Owner 尾注;silent 则群内不说话);
33
- * 5. 为 bug/feature/kb_gap 生成渠道无关的自动化通知字段(标题/内容/接收人 userid);
34
- * kb_gap 额外置「知识沉淀=待补充」进知识收件箱。
35
- *
36
- * 不负责:不调用群发送接口、不选路 Session/Webhook、不调 LLM、不发送 TODO/IM
37
- * (通知由 AI 表格自动化读取结构化字段按需发起)。
38
- */
39
-
40
- const recipe: RecipeDefinition = {
41
- meta: {
42
- id: "on-action",
43
- name: "决策后处置",
44
- role: "support",
45
- description: "按 action 查 profile on_action 策略:留痕(track)、群行为(group)、Owner 解析与通知字段合成",
46
- version: "1.0.0",
47
- requiresAgent: false,
48
- timeoutMs: 60_000,
49
- config: z.object({
50
- policies: z.record(z.string(), z.object({
51
- track: z.boolean().optional().describe("是否建行留痕"),
52
- group: z.enum(["answer", "silent"]).optional().describe("群内行为(轴1):answer 发回答 / silent 不说话;Owner 尾注由轴2派生不在此配"),
53
- })).optional().describe("按 action 的处置策略覆写;省略即用 DEFAULT_ON_ACTION"),
54
- module_owners: z
55
- .union([z.string(), z.array(z.unknown())])
56
- .optional()
57
- .describe("功能模块人员配置来源(二选一):静态数组 [{module, owners:[{userId, nick, openDingTalkId}]}],或引用 ${globalVar.module_owners}(config-sync 从 AI 表格发布)。缺省取后者。"),
58
- group_configs: z
59
- .union([z.string(), z.array(z.unknown())])
60
- .optional()
61
- .describe("群配置来源;Webhook 群只有 corpId 非空时才允许真实 @ 人。缺省取 ${globalVar.group_configs}"),
62
- }),
63
- },
64
-
65
- async execute(ctx: RecipeContext): Promise<RecipeResult> {
66
- const { input, toolkit, log } = ctx;
67
- const rawAction = String(input.action ?? "").trim();
68
- const reply = String(input.reply ?? "");
69
- const module = String(input.module ?? "").trim();
70
- const reason = String(input.reason ?? "");
71
- const content = String(input.content ?? "");
72
- const senderName = String(input.senderName ?? "").trim();
73
- const conversationId = String(input.conversationId ?? "").trim();
74
-
75
- if (!rawAction) {
76
- // 上游未产出 action(防御)→ 不表态透传
77
- return { status: "success", output: {}, tracker: false, logs: ["无 action,透传"] };
78
- }
79
- if (!(SUPPORT_ACTIONS as readonly string[]).includes(rawAction)) {
80
- // 非法输出统一降级 agent_failure(spec §5.3)
81
- log(`未知 action ${rawAction},按 agent_failure 处理`);
82
- }
83
- const action = ((SUPPORT_ACTIONS as readonly string[]).includes(rawAction) ? rawAction : "agent_failure") as SupportAction;
84
-
85
- // 策略合并(与 dev 基线 on_silence 同构):默认值 + ctx.config.policies 逐 action spread 覆写;
86
- // group 合法值(answer/silent)由 assemble preflight 闭集校验把关,运行时不再归一
87
- const overrides = ctx.config.policies as ActionPolicyOverrides | undefined;
88
- const policy: ActionPolicy = { ...DEFAULT_ON_ACTION[action], ...overrides?.[action] };
89
-
90
- // Owner 解析(轴2,单点判定 needsOwnerResolution):只看 action 是否需要找人,
91
- // **与 group 档位无关**——群里说不说话由 policy.group 定,是否附 Owner 指引由
92
- // 解析结果派生(见 action-dispatch);静默时 Owner 仍需进通知字段供自动化找人。
93
- let owner: OwnerResolution | null = null;
94
- let ownerMentionUserId = "";
95
- if (needsOwnerResolution(action)) {
96
- // 不套 String():resolveGlobalVarRef 对非 ref 值原样透传,静态数组直填走同一条路;
97
- // 强转会把数组压成 "[object Object]",下游 Array.isArray 判假 → 永远解析不出 Owner。
98
- const ownersSource = ctx.config.module_owners ?? "${globalVar.module_owners}";
99
- owner = resolveOwner(resolveGlobalVarRef(ownersSource, ctx.getGlobalVar), module);
100
- if (owner.status === "resolved") {
101
- const groupsSource = ctx.config.group_configs ?? "${globalVar.group_configs}";
102
- const group = loadGroupConfig(ctx, groupsSource, conversationId);
103
- const webhookCorpId = group?.outbound === "webhook" ? String(group.corpId ?? "").trim() : "";
104
- if (group?.outbound === "webhook" && !webhookCorpId) {
105
- // Webhook 的 userId 属于企业命名空间。群归属企业未知时不能猜测当前 DWS
106
- // 默认组织,否则可能把无效 userId 作为裸 `@userId` 发进跨企业群。
107
- log(`Webhook 群 ${conversationId || "(空)"} 未配置 corpId,Owner 降级为花名文本`);
108
- } else {
109
- // 能否真实 @:校验 Owner 在不在当前群;Webhook 群通过 `--profile <corpId>` 固定认证组织。
110
- // 任何失败降级为 mode=name → 下游只渲染花名,不写假 @。
111
- const mention = await resolveGroupMention(
112
- conversationId,
113
- { userId: owner.ownerUserId, openDingTalkId: owner.ownerOpenDingTalkId },
114
- webhookCorpId ? { corpId: webhookCorpId } : {},
115
- );
116
- if (mention.mode === "at") ownerMentionUserId = mention.userId;
117
- }
118
- }
119
- // 失败事件按 shouldRecordOwnerFailure 过滤:bug/feature 恒记;
120
- // kb_gap 仅「给了 module 却查不到人」才记(否则大量无 module 的常态 kb_gap 会刷满事件)。
121
- if (owner.status !== "resolved" && shouldRecordOwnerFailure(action, module)) {
122
- log(`Owner 解析失败(${owner.reason}):module=${module || "(空)"},本次不附 Owner 指引(正文照发)`);
123
- if (conversationId) {
124
- void toolkit.memory.append(
125
- supportScope(conversationId),
126
- supportEntry({
127
- kind: SUPPORT_MEMORY_KINDS.ownerResolutionFailed,
128
- content: content.slice(0, 200),
129
- sender: "agent",
130
- role: SUPPORT_MEMORY_ROLES.agent,
131
- conversationId,
132
- summary: `Owner 解析失败(${owner.reason}): action=${action} module=${module || "(空)"}`,
133
- meta: { action, module, reason: owner.reason },
134
- }),
135
- ).catch((err) => log(`memory.append(owner_resolution_failed) 失败(不影响主流程): ${err}`));
136
- }
137
- }
138
- }
139
-
140
- const dispatch = buildActionDispatch({
141
- action,
142
- policy,
143
- reply,
144
- module,
145
- reason,
146
- content,
147
- senderName,
148
- conversationId,
149
- owner,
150
- ownerMentionUserId,
151
- });
152
- if (dispatch.agentReplyMissing) {
153
- log(`agent_reply_missing: ${action} 缺少 Agent 回复主体,使用内部兜底文案`);
154
- }
155
-
156
- if (!policy.track) {
157
- log(`处置(${action}) → 不留痕(track=false),group=${conversationId || "(未知)"}`);
158
- // logs 会被 step-engine 的 no-op info 日志抬出去(静默处置唯一的运营可见点),
159
- // 必须带群 ID + 理由摘要,否则「没收到」与「收到但判定不答」无法区分。
160
- return {
161
- status: "success",
162
- output: {},
163
- tracker: false,
164
- logs: [`处置 ${action}:不留痕,group=${conversationId || "(未知)"},理由=${reason.slice(0, 120) || "(空)"}`],
165
- };
166
- }
167
-
168
- const output: Record<string, unknown> = {
169
- final_reply: dispatch.finalReply,
170
- notification_title: dispatch.notificationTitle,
171
- notification_content: dispatch.notificationContent,
172
- notification_target_user_id: dispatch.notificationTargetUserId,
173
- mention_user_id: dispatch.mentionUserId,
174
- owner_resolution_status: dispatch.ownerResolutionStatus,
175
- ...(action === "kb_gap" ? { kb_status: KB_STATUS.PENDING_SUPPLEMENT } : {}),
176
- };
177
-
178
- // kb_gap:将群消息中的图片上传到「群消息补充」附件字段,供管理员在知识收件箱中查看原始截图
179
- if (action === "kb_gap") {
180
- const imagePaths = extractLocalImagePaths(content);
181
- if (imagePaths.length > 0) {
182
- const fileTokens: Array<{ fileToken: string }> = [];
183
- for (const imgPath of imagePaths) {
184
- try {
185
- const fileName = nodePath.basename(imgPath);
186
- const result = await toolkit.table.uploadAttachmentToCurrentTable(imgPath, fileName);
187
- fileTokens.push({ fileToken: result.fileToken });
188
- } catch (err) {
189
- log(`图片上传失败(${imgPath}),跳过: ${err}`);
190
- }
191
- }
192
- if (fileTokens.length > 0) {
193
- output.image_attachments = fileTokens;
194
- log(`kb_gap 图片上传完成: ${fileTokens.length}/${imagePaths.length} 张`);
195
- }
196
- }
197
- }
198
-
199
- log(`处置(${action}) → 留痕(track=true),group=${policy.group}${action === "kb_gap" ? ",进入知识收件箱" : ""}${dispatch.resolvedOwnerUserId ? `,Owner=${dispatch.resolvedOwnerName}(${dispatch.resolvedOwnerUserId})` : ""}`);
200
- return {
201
- status: "success",
202
- output,
203
- logs: [
204
- `处置 ${action}:留痕,group=${policy.group}`,
205
- dispatch.ownerResolutionStatus === "failed" ? "owner_resolution_failed" : "",
206
- dispatch.agentReplyMissing ? "agent_reply_missing" : "",
207
- ].filter(Boolean),
208
- };
209
- },
210
- };
211
-
212
- const IMAGE_PATH_RE = /!\[图片\]\(([^)]+)\)/g;
213
-
214
- function extractLocalImagePaths(content: string): string[] {
215
- const paths: string[] = [];
216
- for (const match of content.matchAll(IMAGE_PATH_RE)) {
217
- const p = match[1]!;
218
- if (p.startsWith("/") && p.includes(".aitable/media/") && !p.includes("..") && existsSync(p)) {
219
- paths.push(p);
220
- }
221
- }
222
- return paths;
223
- }
224
-
225
- export default recipe;
@@ -1,202 +0,0 @@
1
- import { z } from "zod";
2
- import type { RecipeDefinition, RecipeContext, RecipeResult, ReplyToConversationOptions } from "aitable-workflow-core";
3
- import { createFingerprintWindow, resolveGlobalVarRef } from "aitable-workflow-core";
4
- import { SUPPORT_MEMORY_KINDS, SUPPORT_MEMORY_ROLES, questionKey, supportEntry, supportScope } from "../../../lib/memory-events.js";
5
- import { echoWindow } from "../../../lib/guards.js";
6
- import { needsOwnerResolution } from "../../../lib/action-dispatch.js";
7
- import type { SupportAction } from "../../../lib/contract.js";
8
-
9
- const sendFailureWindow = createFingerprintWindow({
10
- key: "send_failure_marks",
11
- ttlMs: 10 * 60 * 1000,
12
- capacity: 50,
13
- maxPartLength: [300, 300, 200],
14
- });
15
-
16
- /** 群配置(config-sync 发布到 globalVar 的形状):出站选路与凭据引用据此传给 replyToConversation。 */
17
- type GroupConfig = { groupId: string; corpId?: string; inbound?: string; outbound?: string; credRef?: string };
18
-
19
- /**
20
- * 群配置来源二选一:静态数组直填,或 "${globalVar.group_configs}" 引用(config-sync 活读)。
21
- * 两种形态都直接交给 resolveGlobalVarRef —— 它对非 ref 值原样透传,静态数组走同一条路。
22
- * 调用侧不要套 String():数组会被压成 "[object Object]",这里 Array.isArray 判假,
23
- * 于是每群都取不到配置、webhook 分支永远进不去、发送落到 via=skipped。
24
- */
25
- function loadGroupConfig(ctx: RecipeContext, source: unknown, conversationId: string): GroupConfig | null {
26
- const list = resolveGlobalVarRef(source, ctx.getGlobalVar) as GroupConfig[] | undefined;
27
- if (!Array.isArray(list)) return null;
28
- return list.find((g) => g.groupId === conversationId) ?? null;
29
- }
30
-
31
- /** 兼容恢复中的旧 WorkItem:禁止把历史 Owner userId 作为裸 @ 占位符发进未知企业群。 */
32
- function downgradeOwnerPlaceholder(content: string, userId: string): string {
33
- const placeholder = `@${userId.trim()}`;
34
- return placeholder === "@" ? content : content.split(placeholder).join("**相关负责人**");
35
- }
36
-
37
- /**
38
- * 回复群消息(support-qa L0)
39
- *
40
- * 通过 messaging toolkit 的 replyToConversation 自动选路(session sessionWebhook → webhook 降级),
41
- * recipe 不感知传输细节。发送可靠性不变量(生产验证过):
42
- * - via=skipped 按失败处理(无可用通道 ≠ 已发送),记 send_failed;
43
- * 其中 configError(如 credRef 未命中 webhook_creds 命名集)返回 retryable=false,
44
- * 由调度层阻塞待人工改配置,不空转重试;
45
- * - 发送成功才记对应事件:升级找人(bug/feature/kb_gap,与 needsOwnerResolution 同源)
46
- * → owner_escalation_sent(不计为技术回答),其余 → agent_reply_sent(answered 统计口径);
47
- * 发送成功同时回写 reply_status=已回复;
48
- * - 发送成功后发布内容指纹(echo-guard,防 webhook 回复回流自激)。
49
- *
50
- * Owner @人(spec §11,与尾注渲染同源):needsOwnerResolution(action) 且有 mention_user_id
51
- * 时追加 mentionUserIds=[Owner userid]——
52
- * 尾注里提了谁就 @谁,不在本 recipe 里养第二份 action 清单;
53
- * 花名纯文本风格下不传,沿用默认 @提问者行为,避免抬头出现数字工号。
54
- */
55
- const recipe: RecipeDefinition = {
56
- meta: {
57
- id: "reply-to-group",
58
- name: "回复群消息",
59
- role: "support",
60
- description: "replyToConversation 自动选路发送;skipped 按失败处理;成功后发布回声指纹",
61
- version: "1.0.0",
62
- requiresAgent: false,
63
- timeoutMs: 30_000,
64
- config: z.object({
65
- messaging: z.object({}).optional(),
66
- group_configs: z
67
- .union([z.string(), z.array(z.unknown())])
68
- .optional()
69
- .describe("群配置来源(二选一):静态数组 [{groupId, outbound, credRef}],或引用 ${globalVar.group_configs}(config-sync 从 AI 表格发布)。缺省取后者。按群路由出站方式与凭据引用"),
70
- }),
71
- },
72
-
73
- async execute(ctx: RecipeContext): Promise<RecipeResult> {
74
- const { input, toolkit, log } = ctx;
75
- const answer = String(input.answer ?? "");
76
- const conversationId = String(input.conversationId ?? "");
77
- const action = String(input.action ?? "").trim();
78
- const mentionUserId = String(input.mention_user_id ?? "").trim();
79
- const groupsSource = ctx.config.group_configs ?? "${globalVar.group_configs}";
80
- // 关联键:让发送事实/失败事件能回指原始提问(复盘配对首次响应);缺 content 时不写脏字段
81
- const question = questionKey(String(input.content ?? ""));
82
- const questionMeta = question ? { question } : {};
83
-
84
- if (!answer.trim()) { log("无回答内容(静默),跳过"); return { status: "success", output: { reply_status: "静默" }, tracker: false, logs: ["跳过:无回答(静默)"] }; }
85
- if (!conversationId) { log("缺少群ID,跳过"); return { status: "success", output: {}, tracker: false, logs: ["跳过:群ID 为空"] }; }
86
-
87
- const isEscalation = needsOwnerResolution(action as SupportAction);
88
- const group = loadGroupConfig(ctx, groupsSource, conversationId);
89
- const options: ReplyToConversationOptions = {};
90
- let outboundAnswer = answer;
91
- if (group?.outbound === "webhook") {
92
- options.preferWebhook = true;
93
- if (group.credRef) options.credRef = group.credRef;
94
- if (!String(group.corpId ?? "").trim()) {
95
- // 当前群配置是发送时事实源:即使 WorkItem 带着旧 eventContext / mention_user_id,
96
- // corpId 缺失时也必须关闭所有 Webhook @ 并清除正文里的历史数字占位符。
97
- options.mentionSender = false;
98
- outboundAnswer = downgradeOwnerPlaceholder(outboundAnswer, mentionUserId);
99
- log(`Webhook 群 ${conversationId} 未配置 corpId,本次发送禁用 @ 人`);
100
- }
101
- }
102
- // Owner @人(与尾注渲染同源):只有 on_action 已确认可在当前群真实 @
103
- // 时 mention_user_id 才非空;否则保持缺省,仅 @提问者。
104
- const webhookMentionAllowed = group?.outbound !== "webhook" || Boolean(String(group.corpId ?? "").trim());
105
- if (isEscalation && mentionUserId && webhookMentionAllowed) {
106
- options.mentionUserIds = [mentionUserId];
107
- }
108
- const ownerMentioned = Boolean(options.mentionUserIds?.length);
109
-
110
- try {
111
- const res = await toolkit.messaging.replyToConversation(conversationId, outboundAnswer, options);
112
- const via = res.via;
113
- const reason = res.reason;
114
- if (via === "skipped") {
115
- log(`未能发送到群 ${conversationId} (via=skipped${reason ? `, reason=${reason}` : ""})`);
116
- const skipReason = reason ?? "无可用出站通道";
117
- // 重试风暴去重:同群+同待发内容+同原因在窗口内只留痕一次,重试只写日志
118
- if (sendFailureWindow.mark(ctx.getGlobalVar, ctx.setGlobalVar, conversationId, outboundAnswer, `skipped:${skipReason}`)) {
119
- try {
120
- await toolkit.memory.append(
121
- supportScope(conversationId),
122
- supportEntry({
123
- kind: SUPPORT_MEMORY_KINDS.sendFailed,
124
- content: outboundAnswer,
125
- sender: "agent",
126
- role: SUPPORT_MEMORY_ROLES.agent,
127
- conversationId,
128
- summary: `发送失败(via=skipped): ${skipReason}`,
129
- meta: { sendStatus: "failed", via, ...questionMeta, ...(reason ? { reason } : {}) },
130
- }),
131
- );
132
- } catch (memErr) {
133
- log(`memory.append(send_failed) 失败(不影响主流程): ${memErr instanceof Error ? memErr.message : String(memErr)}`);
134
- }
135
- } else {
136
- log("同一发送失败已在近期留痕,本次重试不重复写短期记忆");
137
- }
138
- return {
139
- status: "failed",
140
- output: {},
141
- error: `发送群消息失败(via=skipped): ${skipReason}`,
142
- // 配置错误(如 credRef 未命中 webhook_creds)改配置才能修,重试只会空转
143
- ...(res.configError ? { retryable: false } : {}),
144
- logs: [`未发送(via=skipped):${conversationId}`],
145
- };
146
- }
147
- log(`已发送到群 ${conversationId} (via=${via}${reason ? `, reason=${reason}` : ""})`);
148
- // 回声防护:发布出站内容指纹(conversation_handler 据此识别回流消息)
149
- try {
150
- echoWindow.mark(ctx.getGlobalVar, ctx.setGlobalVar, outboundAnswer);
151
- } catch (fpErr) {
152
- log(`发布回声指纹失败(不影响主流程): ${fpErr instanceof Error ? fpErr.message : String(fpErr)}`);
153
- }
154
- try {
155
- await toolkit.memory.append(
156
- supportScope(conversationId),
157
- supportEntry({
158
- kind: isEscalation
159
- ? SUPPORT_MEMORY_KINDS.ownerEscalationSent
160
- : SUPPORT_MEMORY_KINDS.agentReplySent,
161
- content: outboundAnswer,
162
- sender: "agent",
163
- role: SUPPORT_MEMORY_ROLES.agent,
164
- conversationId,
165
- summary: `已发送(via=${via}${ownerMentioned ? `, @Owner=${mentionUserId}` : ""}): ${outboundAnswer.slice(0, 80)}`,
166
- meta: { sendStatus: "sent", via, ...questionMeta, ...(action ? { action } : {}), ...(ownerMentioned ? { ownerUserId: mentionUserId } : {}) },
167
- }),
168
- );
169
- } catch (memErr) {
170
- log(`memory.append(reply_sent) 失败(不影响主流程): ${memErr instanceof Error ? memErr.message : String(memErr)}`);
171
- }
172
- return { status: "success", output: { reply_status: "已回复" }, logs: [`已发送(via=${via}):${conversationId}`] };
173
- } catch (error) {
174
- const msg = error instanceof Error ? error.message : String(error);
175
- log(`发送失败: ${msg}`);
176
- // 与 skipped 分支同源的重试风暴去重(异常原因不同则各记一次)
177
- if (sendFailureWindow.mark(ctx.getGlobalVar, ctx.setGlobalVar, conversationId, outboundAnswer, `error:${msg}`)) {
178
- try {
179
- await toolkit.memory.append(
180
- supportScope(conversationId),
181
- supportEntry({
182
- kind: SUPPORT_MEMORY_KINDS.sendFailed,
183
- content: outboundAnswer,
184
- sender: "agent",
185
- role: SUPPORT_MEMORY_ROLES.agent,
186
- conversationId,
187
- summary: `发送失败: ${msg}`,
188
- meta: { sendStatus: "failed", error: msg, ...questionMeta },
189
- }),
190
- );
191
- } catch (memErr) {
192
- log(`memory.append(send_failed) 失败(不影响主流程): ${memErr instanceof Error ? memErr.message : String(memErr)}`);
193
- }
194
- } else {
195
- log("同一发送失败已在近期留痕,本次重试不重复写短期记忆");
196
- }
197
- return { status: "failed", output: {}, error: `发送群消息失败: ${msg}`, logs: [`发送失败: ${msg}`] };
198
- }
199
- },
200
- };
201
-
202
- export default recipe;