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,394 +0,0 @@
1
- import type {
2
- EventContext,
3
- EventHandlerRecipeDefinition,
4
- HandlerInitContext,
5
- HandlerInstance,
6
- } from "aitable-workflow-core";
7
- import {
8
- formatZonedDateTime,
9
- formatZonedHm,
10
- normalizeTimezone,
11
- resolveDingTalkStreamMessageEventContext,
12
- resolveDingTalkWebhookMentionEventContext,
13
- resolveGlobalVarRef,
14
- } from "aitable-workflow-core";
15
- import { z } from "zod";
16
- import { SUPPORT_MEMORY_KINDS, SUPPORT_MEMORY_ROLES, supportEntry, supportScope } from "../../../lib/memory-events.js";
17
- import { echoWindow } from "../../../lib/guards.js";
18
-
19
- /**
20
- * 会话消息处理器(support-qa L0)
21
- *
22
- * 事件源只产出统一的 message_received。本 handler 是长活单例:
23
- * - onEvent 纯同步(无长 await):回声过滤 → 摄入本轮消息 + 构建「近期对话上下文」→ pushEvent 建单。
24
- * - 内存里按 conversationId 维护近期消息环形缓冲(容量/TTL 限制,重启清空)。
25
- *
26
- * 三处 Pattern 级行为要点:
27
- * 1. 出站回声防护:reply 发布的内容指纹命中 → 按机器人消息处理(只记历史不建单),
28
- * 防 webhook 回复以真人名义回流造成自激(评测硬用例 G1)。
29
- * 2. 管理员/真人同事消息写入短期记忆(role=admin):学习通道(daily-review 提炼
30
- * 「提问→真人解答」)的原料,不建单不回复但必须留原料(勿删)。
31
- * 3. 历史消息带时间戳注入上下文:追问补位闸门(gates.standby)需要"@Owner 后已等多久"。
32
- *
33
- * 名单来源(均为 globalVar 引用,config-sync L2 装包后发布;未装包时为空集不影响 L0):
34
- * - conversation_ids → 群配置([{groupId, botNick, corpId}]):识别机器人自身消息,并为 Webhook @人指定群归属企业
35
- * - admins → 真人同事名单([{nick, openDingTalkId}] 或 ["花名"])
36
- */
37
-
38
- const HISTORY_CAPACITY = 20;
39
- const HISTORY_TTL_MS = 30 * 60 * 1000;
40
- const IMAGE_MARK = "![图片]";
41
- const CONFIG_CACHE_TTL_MS = 30 * 1000;
42
- const DEFAULT_AGGREGATE_WINDOW_MS = 5000;
43
-
44
- type HistoryEntry = { sender: string; openId: string; content: string; ts: number };
45
- type IdentitySets = { ids: Set<string>; nicks: Set<string> };
46
- type GroupRuntimeConfig = {
47
- identity: IdentitySets;
48
- outbound: string;
49
- corpId: string;
50
- };
51
- type PendingMessage = { content: string; senderName: string; senderOpenId: string; sendTime: string; ts: number; messageId: string; hasImage: boolean };
52
- type PendingBatch = {
53
- messages: PendingMessage[];
54
- conversationId: string;
55
- senderName: string;
56
- senderOpenDingTalkId: string;
57
- eventContext?: EventContext;
58
- timer: ReturnType<typeof setTimeout>;
59
- };
60
-
61
- function parseNicks(raw: unknown): Set<string> {
62
- const arr = Array.isArray(raw) ? raw.map(String) : String(raw ?? "").split(/[,,]/);
63
- return new Set(arr.map((s) => s.trim()).filter(Boolean));
64
- }
65
-
66
- /** 先按 openDingTalkId 精确匹配,匹配不到再用花名兜底 */
67
- function matchesIdentity(sets: IdentitySets, sender: string, openId: string): boolean {
68
- if (openId && sets.ids.has(openId)) return true;
69
- return !!sender && sets.nicks.has(sender);
70
- }
71
-
72
- function loadGroupBots(list: unknown): Map<string, GroupRuntimeConfig> {
73
- const map = new Map<string, GroupRuntimeConfig>();
74
- for (const g of Array.isArray(list) ? list : []) {
75
- if (!g || typeof g !== "object") continue;
76
- const rec = g as Record<string, unknown>;
77
- const gid = String(rec.groupId ?? "").trim();
78
- if (gid) {
79
- map.set(gid, {
80
- identity: { ids: new Set(), nicks: parseNicks(rec.botNick) },
81
- outbound: String(rec.outbound ?? "").trim(),
82
- corpId: String(rec.corpId ?? "").trim(),
83
- });
84
- }
85
- }
86
- return map;
87
- }
88
-
89
- function loadAdmins(list: unknown): IdentitySets {
90
- const ids = new Set<string>();
91
- const nicks = new Set<string>();
92
- for (const e of Array.isArray(list) ? list : []) {
93
- if (typeof e === "string") {
94
- const nick = e.trim();
95
- if (nick) nicks.add(nick);
96
- } else if (e && typeof e === "object") {
97
- const rec = e as Record<string, unknown>;
98
- const nick = String(rec.nick ?? "").trim();
99
- const openId = String(rec.openDingTalkId ?? "").trim();
100
- if (nick) nicks.add(nick);
101
- if (openId) ids.add(openId);
102
- }
103
- }
104
- return { ids, nicks };
105
- }
106
-
107
- function roleTag(sender: string, openId: string, botSets: IdentitySets, adminSets: IdentitySets): string {
108
- if (matchesIdentity(botSets, sender, openId)) return "(答疑机器人/即你自己之前的回复)";
109
- if (matchesIdentity(adminSets, sender, openId)) return "(真人同事)";
110
- return "";
111
- }
112
-
113
- /** 历史时间戳格式:闸门(追问补位)据此计算等待时长。按配置时区渲染,UTC 主机上不错位 */
114
- function formatTs(ts: number, timezone: string): string {
115
- return formatZonedHm(new Date(ts), timezone);
116
- }
117
-
118
- function ttlCached<T>(loader: () => T, ttlMs: number): () => T {
119
- let cache: T | null = null;
120
- let loadedAt = 0;
121
- return () => {
122
- const now = Date.now();
123
- if (cache === null || now - loadedAt > ttlMs) {
124
- cache = loader();
125
- loadedAt = now;
126
- }
127
- return cache;
128
- };
129
- }
130
-
131
- const recipe: EventHandlerRecipeDefinition = {
132
- meta: {
133
- id: "conversation_handler",
134
- name: "会话消息处理器",
135
- description: "message_received → 回声过滤 → 构建近期对话上下文 → 创建工单",
136
- config: z.object({
137
- conversation_ids: z
138
- .union([z.string(), z.array(z.unknown())])
139
- .optional()
140
- .describe("群配置来源(二选一):静态数组 [{groupId, botNick, corpId}],或引用 ${globalVar.group_configs}(config-sync 从 AI 表格发布)。缺省取后者。识别每群机器人身份,并为 Webhook @人指定群归属企业"),
141
- admins: z
142
- .union([z.string(), z.array(z.unknown())])
143
- .optional()
144
- .describe("真人同事名单来源(二选一):静态数组 [{nick, openDingTalkId}] 或 [\"花名\"],或引用 ${globalVar.admins}。缺省取后者。"),
145
- timezone: z
146
- .string()
147
- .optional()
148
- .describe("对话上下文 [hh:mm] 的渲染时区(IANA,缺省 Asia/Shanghai);UTC 主机上避免错位 8 小时"),
149
- aggregate_window_ms: z
150
- .number()
151
- .int()
152
- .min(0)
153
- .optional()
154
- .describe("同人同群消息聚合窗口(毫秒)。事件驱动源每条消息单独到达,该窗口内连续消息合并为一个工单。缺省 5000ms,设 0 关闭聚合。"),
155
- }),
156
- },
157
-
158
- create(ctx: HandlerInitContext): HandlerInstance {
159
- const histories = new Map<string, HistoryEntry[]>();
160
- const emptySets: IdentitySets = { ids: new Set(), nicks: new Set() };
161
- const timezone = normalizeTimezone(typeof ctx.config.timezone === "string" ? ctx.config.timezone : undefined);
162
- const aggregateWindowMs = typeof ctx.config.aggregate_window_ms === "number"
163
- ? ctx.config.aggregate_window_ms
164
- : DEFAULT_AGGREGATE_WINDOW_MS;
165
- // 两种形态都直接交给 resolveGlobalVarRef:ref 字符串被活读,静态数组原样透传。
166
- // 不要套 String():静态数组会被压成 "[object Object],..." 而后被 loader 的
167
- // Array.isArray 判假,静默退化成空名单 —— 服务照常启动、日志干净、就是不干活。
168
- const groupsSource = ctx.config.conversation_ids ?? "${globalVar.group_configs}";
169
- const adminsSource = ctx.config.admins ?? "${globalVar.admins}";
170
- const getAdmins = ttlCached(
171
- () => loadAdmins(resolveGlobalVarRef(adminsSource, ctx.getGlobalVar)),
172
- CONFIG_CACHE_TTL_MS,
173
- );
174
- const getGroupBots = ttlCached(
175
- () => loadGroupBots(resolveGlobalVarRef(groupsSource, ctx.getGlobalVar)),
176
- CONFIG_CACHE_TTL_MS,
177
- );
178
-
179
- const pending = new Map<string, PendingBatch>();
180
-
181
- function flushBatch(key: string): void {
182
- const batch = pending.get(key);
183
- if (!batch) return;
184
- pending.delete(key);
185
-
186
- const { messages, conversationId, senderName, senderOpenDingTalkId, eventContext } = batch;
187
- const mergedContent = messages.map((m) => m.content).join("\n");
188
- const lastMsg = messages[messages.length - 1]!;
189
-
190
- const adminSets = getAdmins();
191
- const botSets = getGroupBots().get(conversationId)?.identity ?? emptySets;
192
-
193
- // history 已在 onEvent 时实时写入,此处只读取构建上下文(batch 内消息已在 history 中)
194
- const prior = (histories.get(conversationId) ?? []).filter((e) => lastMsg.ts - e.ts <= HISTORY_TTL_MS);
195
- histories.set(conversationId, prior);
196
- // 上下文排除 batch 自身的消息:只取 batch 首条之前的历史作为「当前之前」的对话上下文
197
- const firstTs = messages[0]!.ts;
198
- const contextEntries = prior.filter((e) => e.ts < firstTs);
199
- const contextText =
200
- contextEntries.length > 0
201
- ? [
202
- "以下是群聊中近期的对话历史(帮助理解当前问题的上下文,如追问、反馈、是否已被同事接手;[hh:mm] 为发送时间,供判断等待时长):",
203
- "---",
204
- ...contextEntries.map((e, i) => `[历史消息 ${i + 1}][${formatTs(e.ts, timezone)}] ${e.sender}${roleTag(e.sender, e.openId, botSets, adminSets)}: ${e.content}`),
205
- "---",
206
- ].join("\n")
207
- : "";
208
-
209
- ctx.log(`聚合 flush: ${senderName}@${conversationId}, ${messages.length} 条消息合并`);
210
- ctx.pushEvent({
211
- action: "create_record",
212
- fields: {
213
- "消息内容": mergedContent,
214
- "发送人": senderName,
215
- "发送时间": formatZonedDateTime(lastMsg.ts, timezone),
216
- "群ID": conversationId,
217
- "对话上下文": contextText,
218
- },
219
- ...(eventContext ? { eventContext } : {}),
220
- });
221
-
222
- for (const m of messages) {
223
- void ctx.toolkit.memory.append(supportScope(conversationId), supportEntry({
224
- timestamp: m.sendTime,
225
- kind: SUPPORT_MEMORY_KINDS.userMessage,
226
- content: m.content,
227
- sender: m.senderName,
228
- role: SUPPORT_MEMORY_ROLES.user,
229
- conversationId,
230
- meta: { messageId: m.messageId, hasImage: m.hasImage },
231
- })).catch((err) => ctx.log(`memory.append(user_message) 失败(不影响主流程): ${err}`));
232
- }
233
- }
234
-
235
- return {
236
- onEvent(event) {
237
- const payload = event.payload;
238
- const content = String(payload.content ?? "");
239
- const hasImage = content.includes(IMAGE_MARK);
240
- if (!content.trim() && !hasImage) {
241
- ctx.log("消息内容为空且无图片,跳过(不 push)");
242
- return;
243
- }
244
-
245
- const senderName = String(payload.senderName ?? payload.sender ?? payload.firstSender ?? "");
246
- const senderOpenId = String(payload.senderOpenDingTalkId ?? "").trim();
247
- const messageId = String(payload.messageId ?? "");
248
- const sendTime = String(payload.firstSendTime ?? new Date().toISOString());
249
- const conversationId = String(payload.conversationId ?? "");
250
- const now = Date.parse(sendTime) || Date.now();
251
- const adminSets = getAdmins();
252
- const groupConfig = getGroupBots().get(conversationId);
253
- const botSets = groupConfig?.identity ?? emptySets;
254
- // 出站回声:webhook 回复以真人名义回流时身份匹配失效,用内容指纹兜底(G1)
255
- const isEcho = echoWindow.has(ctx.getGlobalVar, content);
256
- const isBot = isEcho || matchesIdentity(botSets, senderName, senderOpenId);
257
- const isAdmin = !isBot && matchesIdentity(adminSets, senderName, senderOpenId);
258
-
259
- // 1) 取该群历史,裁剪过期,构建「当前之前」的对话上下文(带角色标记 + 时间戳)
260
- const prior = (histories.get(conversationId) ?? []).filter((e) => now - e.ts <= HISTORY_TTL_MS);
261
- const contextText =
262
- prior.length > 0
263
- ? [
264
- "以下是群聊中近期的对话历史(帮助理解当前问题的上下文,如追问、反馈、是否已被同事接手;[hh:mm] 为发送时间,供判断等待时长):",
265
- "---",
266
- ...prior.map((e, i) => `[历史消息 ${i + 1}][${formatTs(e.ts, timezone)}] ${e.sender}${roleTag(e.sender, e.openId, botSets, adminSets)}: ${e.content}`),
267
- "---",
268
- ].join("\n")
269
- : "";
270
-
271
- // 2) 机器人/管理员消息立即入历史(用户消息由聚合 flush 或立即推送路径写入,避免重复)
272
- if (isBot || isAdmin) {
273
- prior.push({ sender: senderName, openId: senderOpenId, content, ts: now });
274
- while (prior.length > HISTORY_CAPACITY) prior.shift();
275
- histories.set(conversationId, prior);
276
- }
277
-
278
- // 机器人自己的消息(含回声)只记录进历史,不建单
279
- if (isBot) {
280
- ctx.log(`机器人消息${isEcho ? "(出站回声)" : ""},仅记录进历史不建单: ${senderName}`);
281
- return;
282
- }
283
-
284
- // 真人同事/管理员消息:不建单不回复,但必须写短期记忆(学习通道原料)
285
- if (isAdmin) {
286
- ctx.log(`真人同事消息,记历史+记忆不建单: ${senderName}`);
287
- void ctx.toolkit.memory.append(supportScope(conversationId), supportEntry({
288
- timestamp: sendTime,
289
- kind: SUPPORT_MEMORY_KINDS.adminMessage,
290
- content,
291
- sender: senderName,
292
- role: SUPPORT_MEMORY_ROLES.admin,
293
- conversationId,
294
- meta: { messageId, hasImage },
295
- })).catch((err) => ctx.log(`memory.append(admin_message) 失败(不影响主流程): ${err}`));
296
- return;
297
- }
298
-
299
- ctx.log(`处理群消息: 来自 ${senderName}${hasImage ? "(含图片)" : ""}, 历史 ${prior.length - 1} 条`);
300
-
301
- let eventContext: EventContext | undefined;
302
- const replyCtx = (payload.dingtalk ?? {}) as Record<string, unknown>;
303
- if (groupConfig?.outbound === "webhook") {
304
- eventContext = resolveDingTalkWebhookMentionEventContext({
305
- type: "dingtalk_webhook_mention",
306
- conversationId,
307
- senderOpenDingTalkId: senderOpenId,
308
- senderName,
309
- corpId: groupConfig.corpId,
310
- });
311
- } else if (replyCtx.credRef) {
312
- eventContext = resolveDingTalkStreamMessageEventContext({
313
- type: "dingtalk_stream_message",
314
- openMsgId: String(replyCtx.openMsgId ?? ""),
315
- conversationId,
316
- robotCode: String(replyCtx.robotCode ?? ""),
317
- credRef: String(replyCtx.credRef ?? ""),
318
- });
319
- }
320
-
321
- // 聚合:固定窗口,第一条消息定窗口 [now, now + aggregateWindowMs],窗口到期统一 flush
322
- if (aggregateWindowMs > 0) {
323
- const aggKey = `${conversationId}::${senderOpenId || senderName}`;
324
- const msg: PendingMessage = { content, senderName, senderOpenId, sendTime, ts: now, messageId, hasImage };
325
- // 立即写入 history,保证窗口期间同群其他人的上下文能看到这条消息
326
- prior.push({ sender: senderName, openId: senderOpenId, content, ts: now });
327
- while (prior.length > HISTORY_CAPACITY) prior.shift();
328
- histories.set(conversationId, prior);
329
-
330
- const existing = pending.get(aggKey);
331
- if (existing) {
332
- existing.messages.push(msg);
333
- if (eventContext) existing.eventContext = eventContext;
334
- ctx.log(`聚合: ${senderName}@${conversationId} 追加第 ${existing.messages.length} 条`);
335
- } else {
336
- if (eventContext?.type === "dingtalk_stream_message") {
337
- ctx.toolkit.messaging?.beginDingTalkProcessingFeedback?.(eventContext);
338
- }
339
- const timer = setTimeout(() => flushBatch(aggKey), aggregateWindowMs);
340
- pending.set(aggKey, {
341
- messages: [msg],
342
- conversationId,
343
- senderName,
344
- senderOpenDingTalkId: senderOpenId,
345
- eventContext,
346
- timer,
347
- });
348
- ctx.log(`聚合: ${senderName}@${conversationId} 开始 ${aggregateWindowMs}ms 窗口`);
349
- }
350
- return;
351
- }
352
-
353
- // aggregateWindowMs === 0: 不聚合,立即推送(兼容旧行为)
354
- prior.push({ sender: senderName, openId: senderOpenId, content, ts: now });
355
- while (prior.length > HISTORY_CAPACITY) prior.shift();
356
- histories.set(conversationId, prior);
357
-
358
- if (eventContext?.type === "dingtalk_stream_message") {
359
- ctx.toolkit.messaging?.beginDingTalkProcessingFeedback?.(eventContext);
360
- }
361
- ctx.pushEvent({
362
- action: "create_record",
363
- fields: {
364
- "消息内容": content,
365
- "发送人": senderName,
366
- "发送时间": formatZonedDateTime(now, timezone),
367
- "群ID": conversationId,
368
- "对话上下文": contextText,
369
- },
370
- ...(eventContext ? { eventContext } : {}),
371
- });
372
- void ctx.toolkit.memory.append(supportScope(conversationId), supportEntry({
373
- timestamp: sendTime,
374
- kind: SUPPORT_MEMORY_KINDS.userMessage,
375
- content,
376
- sender: senderName,
377
- role: SUPPORT_MEMORY_ROLES.user,
378
- conversationId,
379
- meta: { messageId, hasImage },
380
- })).catch((err) => ctx.log(`memory.append(user_message) 失败(不影响主流程): ${err}`));
381
- },
382
- stop() {
383
- for (const [key, batch] of pending) {
384
- clearTimeout(batch.timer);
385
- flushBatch(key);
386
- }
387
- pending.clear();
388
- histories.clear();
389
- },
390
- };
391
- },
392
- };
393
-
394
- export default recipe;
@@ -1,72 +0,0 @@
1
- import { z } from "zod";
2
- import { type RecipeDefinition, type RecipeContext, type RecipeResult } from "aitable-workflow-core";
3
- import { loadReplyRules, matchReplyRules } from "../../../lib/declarative-matcher.js";
4
-
5
- /**
6
- * fastpath 确定性秒回(support-qa L3,group-qa 首步,非 LLM)
7
- *
8
- * 按 profile/fastpath/known-answers.yml 的声明式规则(template/contains/regex + 安全兜底)
9
- * 单向匹配当前消息,命中即产出「已知错误」类别的固定话术,下游 answer 透传、reply_to_group 发送。
10
- *
11
- * 关闭/透传语义:
12
- * - profile.fastpath.rules 未配置或文件缺失 → 透传(本步不表态,tracker:false);
13
- * - 空消息 / 上游已定类 → 透传;
14
- * - 未命中任何规则 → 透传给 answer 走语义答疑(语义匹配不属于 fastpath)。
15
- *
16
- * 命中即建行:本步返回 output(含 answer)且不置 tracker:false,on_demand 下建行留痕
17
- * (已知错误秒回的建行语义)。
18
- */
19
- const recipe: RecipeDefinition = {
20
- meta: {
21
- id: "fastpath",
22
- name: "确定性秒回",
23
- role: "support",
24
- description: "声明式规则(template/contains/regex + 安全兜底)单向匹配秒回,非 LLM",
25
- version: "1.0.0",
26
- requiresAgent: false,
27
- timeoutMs: 10_000,
28
- config: z.object({
29
- rules: z.string().optional().describe("确定性秒回规则文件路径(相对 profile 目录);省略即关闭 fastpath"),
30
- }),
31
- },
32
-
33
- async execute(ctx: RecipeContext): Promise<RecipeResult> {
34
- const { input, log } = ctx;
35
- const content = String(input.content ?? "");
36
- const senderName = String(input.senderName ?? "").trim();
37
- const priorAction = String(input.action ?? "").trim();
38
-
39
- // 上游已产出 action(fastpath 通常是首步,此处为防御)→ 透传
40
- if (priorAction) {
41
- return { status: "success", output: {}, tracker: false, logs: [`透传:${priorAction}`] };
42
- }
43
- if (!content.trim()) {
44
- return { status: "success", output: {}, tracker: false, logs: ["空消息,透传"] };
45
- }
46
-
47
- const { config, issues } = loadReplyRules(ctx.configDir, ctx.config.rules as string | undefined);
48
- for (const i of issues) log(`[fastpath] ${i}`);
49
- if (!config) {
50
- // 未配置/文件缺失 → fastpath 关闭,透传 answer
51
- return { status: "success", output: {}, tracker: false, logs: ["fastpath 未启用,透传"] };
52
- }
53
-
54
- const match = matchReplyRules(config, content);
55
- if (!match) {
56
- log("未命中 fastpath 规则,交给 answer");
57
- return { status: "success", output: {}, tracker: false, logs: ["未命中 fastpath"] };
58
- }
59
-
60
- const greeting = senderName ? `Hi ${senderName},` : "";
61
- const answer = `${greeting}${match.reply}`;
62
- log(`命中 fastpath 规则 [${match.key}](${match.kind}),秒回`);
63
- return {
64
- status: "success",
65
- // action=reply:下游 on_action 按 answer 策略透传,reply_to_group 沿用 @提问者
66
- output: { action: "reply", answer },
67
- logs: [`fastpath 命中 ${match.key}(${match.kind})`],
68
- };
69
- },
70
- };
71
-
72
- export default recipe;