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.
- package/README.md +1 -6
- package/dist/{chunk-IGB6WDLR.js → chunk-UNPV6DTX.js} +1 -1
- package/dist/cli.js +47 -47
- package/dist/config-set-FBOQBRSJ.js +13 -0
- package/dist/{config-ui-server-CORDAOYI.js → config-ui-server-IUSACAOT.js} +1 -1
- package/package.json +3 -3
- package/dist/config-set-EGLLIMVJ.js +0 -13
- package/templates/support-qa/.env.example +0 -16
- package/templates/support-qa/aitable-workflow.config.yml +0 -46
- package/templates/support-qa/coding-agents.config.json +0 -18
- package/templates/support-qa/lib/__tests__/declarative-matcher.test.ts +0 -240
- package/templates/support-qa/lib/__tests__/dingtalk-group-membership.test.ts +0 -144
- package/templates/support-qa/lib/__tests__/memory-events.test.ts +0 -90
- package/templates/support-qa/lib/__tests__/sandbox-agent.test.ts +0 -382
- package/templates/support-qa/lib/__tests__/thread-adapter.test.ts +0 -242
- package/templates/support-qa/lib/__tests__/thread-observe-step.test.ts +0 -391
- package/templates/support-qa/lib/__tests__/thread-observer-handler.test.ts +0 -274
- package/templates/support-qa/lib/action-dispatch.ts +0 -262
- package/templates/support-qa/lib/contract.ts +0 -253
- package/templates/support-qa/lib/declarative-matcher.ts +0 -398
- package/templates/support-qa/lib/dingtalk-group-membership.ts +0 -158
- package/templates/support-qa/lib/guards.ts +0 -18
- package/templates/support-qa/lib/memory-events.ts +0 -113
- package/templates/support-qa/lib/owner-resolver.ts +0 -94
- package/templates/support-qa/lib/preflight.ts +0 -294
- package/templates/support-qa/lib/profile.ts +0 -51
- package/templates/support-qa/lib/sandbox-agent.ts +0 -653
- package/templates/support-qa/lib/thread-adapter.ts +0 -281
- package/templates/support-qa/lib/thread-digest.ts +0 -144
- package/templates/support-qa/npmrc +0 -1
- package/templates/support-qa/package.json +0 -27
- package/templates/support-qa/profiles/aitable/profile/escalation/contacts.json +0 -37
- package/templates/support-qa/profiles/aitable/profile/escalation/owner-map.json +0 -18
- package/templates/support-qa/profiles/aitable/profile/fastpath/known-answers.yml +0 -561
- package/templates/support-qa/profiles/aitable/profile/profile.yml +0 -219
- package/templates/support-qa/profiles/aitable/profile/prompts/kb-editor.md +0 -3
- package/templates/support-qa/profiles/aitable/profile/prompts/persona.md +0 -49
- package/templates/support-qa/profiles/aitable/profile/prompts/troubleshooter.md +0 -5
- package/templates/support-qa/profiles/aitable/profile/troubleshoot/classifiers.yml +0 -36
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/SKILL.md +0 -58
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-import-troubleshooter/SKILL.md +0 -485
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-openapi-troubleshooter/SKILL.md +0 -183
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/import-openapi-e2e-troubleshooter/SKILL.md +0 -459
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-datasource-troubleshooter/SKILL.md +0 -1286
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-field-troubleshooter/SKILL.md +0 -673
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/spreadsheet-datasync-troubleshooter/SKILL.md +0 -322
- package/templates/support-qa/profiles/default/profile/fastpath/known-answers.yml +0 -32
- package/templates/support-qa/profiles/default/profile/profile.yml +0 -217
- package/templates/support-qa/profiles/default/profile/troubleshoot/classifiers.yml +0 -37
- package/templates/support-qa/profiles/default/wiki/support//345/270/270/350/247/201/351/227/256/351/242/230.md +0 -10
- package/templates/support-qa/tsconfig.json +0 -16
- package/templates/support-qa/vitest.config.ts +0 -9
- package/templates/support-qa/workflows/admin-kb-update/recipes/kb-refine.recipe.ts +0 -137
- package/templates/support-qa/workflows/admin-kb-update/recipes/kb_sync_handler.recipe.ts +0 -119
- package/templates/support-qa/workflows/admin-kb-update/workflow.yml +0 -96
- package/templates/support-qa/workflows/config-sync/recipes/config_sync.recipe.ts +0 -668
- package/templates/support-qa/workflows/config-sync/recipes/config_sync_handler.recipe.ts +0 -16
- package/templates/support-qa/workflows/config-sync/workflow.yml +0 -62
- package/templates/support-qa/workflows/daily-review/recipes/daily_review.recipe.ts +0 -602
- package/templates/support-qa/workflows/daily-review/recipes/daily_review_handler.recipe.ts +0 -26
- package/templates/support-qa/workflows/daily-review/workflow.yml +0 -65
- package/templates/support-qa/workflows/group-qa/recipes/answer.recipe.ts +0 -231
- package/templates/support-qa/workflows/group-qa/recipes/conversation_handler.recipe.ts +0 -394
- package/templates/support-qa/workflows/group-qa/recipes/fastpath.recipe.ts +0 -72
- package/templates/support-qa/workflows/group-qa/recipes/on-action.recipe.ts +0 -225
- package/templates/support-qa/workflows/group-qa/recipes/reply-to-group.recipe.ts +0 -202
- package/templates/support-qa/workflows/group-qa/recipes/troubleshoot.recipe.ts +0 -273
- package/templates/support-qa/workflows/group-qa/workflow.yml +0 -268
- package/templates/support-qa/workflows/thread-observer/README.md +0 -96
- package/templates/support-qa/workflows/thread-observer/recipes/thread_observe.recipe.ts +0 -415
- package/templates/support-qa/workflows/thread-observer/recipes/thread_observer_handler.recipe.ts +0 -213
- package/templates/support-qa/workflows/thread-observer/workflow.yml +0 -185
- package/templates/support-qa/workspaces/daily-review/AGENTS.md +0 -233
- package/templates/support-qa/workspaces/kb-editor/AGENTS.md +0 -32
- package/templates/support-qa/workspaces/support/AGENTS.md +0 -156
- package/templates/support-qa/workspaces/thread-observer/AGENTS.md +0 -156
- package/templates/support-qa/workspaces/troubleshooter/AGENTS.md +0 -71
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type NormalizedMessage,
|
|
3
|
-
type ThreadRoleRoster,
|
|
4
|
-
type ThreadStoreOptions,
|
|
5
|
-
} from "aitable-workflow-core";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* thread observer 场景适配层 —— DingTalk payload / profile 名单 → core thread 契约。
|
|
9
|
-
*
|
|
10
|
-
* 为什么这一层在模板而不在 core:core 不知道 support-qa 的 `globalVar.group_configs`
|
|
11
|
-
* 长什么样、群名从哪来、谁算答疑人员。core 只吃 {@link NormalizedMessage} 与
|
|
12
|
-
* {@link ThreadRoleRoster},把「怎么从这个客户的配置里算出这两样」留给模板。
|
|
13
|
-
*
|
|
14
|
-
* 两个实测坑,改动前先看:
|
|
15
|
-
*
|
|
16
|
-
* ① **必须逐条拆 `payload.messages`,不能用 `payload.content`。**
|
|
17
|
-
* `dingtalk_message` 源默认 `aggregate_by_sender=true`,一个 event 可能含同一人多条消息,
|
|
18
|
-
* `payload.content` 是它们拼成的多行文本。用拼接结果当一条会把每条自己的
|
|
19
|
-
* `openMessageId` 与 `quotedMessageId` 全丢掉 —— 而这两个正是归并与溯源的地基。
|
|
20
|
-
* (现有 group-qa 的 conversation_handler 读的就是 `payload.content` + 不存在的
|
|
21
|
-
* `payload.messageId`,所以它拿不到稳定的消息身份,这也是本试点要独立建表的原因。)
|
|
22
|
-
*
|
|
23
|
-
* ② **群名不能指望事件源给。**
|
|
24
|
-
* `dingtalk_message` 走的是逐群 `dws chat message list`,响应里没有群名(只有
|
|
25
|
-
* `dingtalk_user_message` 的 list-all 在会话层给 `title` → `payload.conversationTitle`)。
|
|
26
|
-
* 因此群名从 `globalVar.group_configs` 的 `groupName` 按 groupId 反查,取不到就留空串
|
|
27
|
-
* —— 留空串而不是 undefined,避免下游写表时该列被整列跳过。
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/** 身份名单:先按 openDingTalkId 精确匹配,匹配不到再用昵称兜底。 */
|
|
31
|
-
export interface IdentitySets {
|
|
32
|
-
ids: Set<string>;
|
|
33
|
-
nicks: Set<string>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const EMPTY_IDENTITY: IdentitySets = { ids: new Set(), nicks: new Set() };
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 解析名单。兼容三种形状:`["花名"]` / `[{nick, openDingTalkId}]` / `[{botNick}]`。
|
|
40
|
-
*
|
|
41
|
-
* 非数组(含 `undefined`)一律得空集合——`${globalVar.X}` 在首轮 config-sync 发布前解为
|
|
42
|
-
* `undefined`,这里必须能降级,不能抛。
|
|
43
|
-
*/
|
|
44
|
-
export function parseIdentities(list: unknown): IdentitySets {
|
|
45
|
-
const ids = new Set<string>();
|
|
46
|
-
const nicks = new Set<string>();
|
|
47
|
-
for (const e of Array.isArray(list) ? list : []) {
|
|
48
|
-
if (typeof e === "string") {
|
|
49
|
-
const nick = e.trim();
|
|
50
|
-
if (nick) nicks.add(nick);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (!e || typeof e !== "object") continue;
|
|
54
|
-
const rec = e as Record<string, unknown>;
|
|
55
|
-
for (const key of ["nick", "name", "botNick"]) {
|
|
56
|
-
const nick = String(rec[key] ?? "").trim();
|
|
57
|
-
if (nick) nicks.add(nick);
|
|
58
|
-
}
|
|
59
|
-
const openId = String(rec.openDingTalkId ?? "").trim();
|
|
60
|
-
if (openId) ids.add(openId);
|
|
61
|
-
}
|
|
62
|
-
return { ids, nicks };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 从功能模块人员配置里取**模块名字典**(不取人),拼成一行递给分析 agent。
|
|
67
|
-
*
|
|
68
|
-
* 为何复用 `module_owners` 而不另配一份字典:模块口径必须与答疑链路同源,否则
|
|
69
|
-
* observer 算出的 `结论模块` 与主链的 `模块` 对不上,后续按模块聚合就失效。两份配置
|
|
70
|
-
* 必然漂移。
|
|
71
|
-
*
|
|
72
|
-
* 兼容两种形状:`[{module, owners}]` 与 `{模块名: [...]}`。取不到时返回空串,
|
|
73
|
-
* agent 侧按 AGENTS.md 的约定省略 module 字段(而不是乱造一个)。
|
|
74
|
-
*/
|
|
75
|
-
export function parseModuleTaxonomy(raw: unknown): string {
|
|
76
|
-
const names = new Set<string>();
|
|
77
|
-
if (Array.isArray(raw)) {
|
|
78
|
-
for (const e of raw) {
|
|
79
|
-
if (typeof e === "string") {
|
|
80
|
-
const n = e.trim();
|
|
81
|
-
if (n) names.add(n);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (!e || typeof e !== "object") continue;
|
|
85
|
-
const n = String((e as Record<string, unknown>).module ?? "").trim();
|
|
86
|
-
if (n) names.add(n);
|
|
87
|
-
}
|
|
88
|
-
} else if (raw && typeof raw === "object") {
|
|
89
|
-
for (const key of Object.keys(raw as Record<string, unknown>)) {
|
|
90
|
-
const n = key.trim();
|
|
91
|
-
if (n) names.add(n);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return [...names].join(" / ");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** groupId → 群名。群配置里没有该群或没填群名时不进 map(下游降级为空串)。 */
|
|
98
|
-
export function parseGroupNames(list: unknown): Map<string, string> {
|
|
99
|
-
const out = new Map<string, string>();
|
|
100
|
-
for (const g of Array.isArray(list) ? list : []) {
|
|
101
|
-
if (!g || typeof g !== "object") continue;
|
|
102
|
-
const rec = g as Record<string, unknown>;
|
|
103
|
-
const gid = String(rec.groupId ?? "").trim();
|
|
104
|
-
const name = String(rec.groupName ?? "").trim();
|
|
105
|
-
if (gid && name) out.set(gid, name);
|
|
106
|
-
}
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function matchesIdentity(sets: IdentitySets, name: string, openId: string): boolean {
|
|
111
|
-
if (openId && sets.ids.has(openId)) return true;
|
|
112
|
-
return !!name && sets.nicks.has(name);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** 客观身份类别。**不是 thread 里的 role** —— 谁是本 thread 的 asker 要等 thread 成型才知道。 */
|
|
116
|
-
export type MessageIdentity = "bot" | "answerer" | "user";
|
|
117
|
-
|
|
118
|
-
export function resolveIdentity(bots: IdentitySets, answerers: IdentitySets, name: string, openId: string): MessageIdentity {
|
|
119
|
-
if (matchesIdentity(bots, name, openId)) return "bot";
|
|
120
|
-
if (matchesIdentity(answerers, name, openId)) return "answerer";
|
|
121
|
-
return "user";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 角色判定状态。写进观察表供人工一眼看出「SLA 算不出来是名单没配,还是真没人回」。
|
|
126
|
-
*
|
|
127
|
-
* `missing_bots` 优先于 `missing_answerers` 报:机器人名单缺失会让机器人消息被当成用户
|
|
128
|
-
* 提问,污染面更大。
|
|
129
|
-
*/
|
|
130
|
-
export type RosterStatus = "ok" | "missing_bots" | "missing_answerers";
|
|
131
|
-
|
|
132
|
-
export function rosterStatus(bots: IdentitySets, answerers: IdentitySets): RosterStatus {
|
|
133
|
-
if (bots.ids.size === 0 && bots.nicks.size === 0) return "missing_bots";
|
|
134
|
-
if (answerers.ids.size === 0 && answerers.nicks.size === 0) return "missing_answerers";
|
|
135
|
-
return "ok";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function toRoster(bots: IdentitySets, answerers: IdentitySets): ThreadRoleRoster {
|
|
139
|
-
return {
|
|
140
|
-
answererIds: [...answerers.ids],
|
|
141
|
-
answererNames: [...answerers.nicks],
|
|
142
|
-
botNames: [...bots.nicks],
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** 从 handler_config / step.config 解析出的名单与群名,一次算好供本轮复用。 */
|
|
147
|
-
export interface ObserverRosters {
|
|
148
|
-
bots: IdentitySets;
|
|
149
|
-
answerers: IdentitySets;
|
|
150
|
-
groupNames: Map<string, string>;
|
|
151
|
-
status: RosterStatus;
|
|
152
|
-
roster: ThreadRoleRoster;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 活读名单。**每次事件都要重算**,不能在 create() 时快照:名单来自 config-sync 发布的
|
|
157
|
-
* globalVar,首轮发布前为空;快照下来的话名单后来发布也不生效,SLA 会永远算不出来。
|
|
158
|
-
*
|
|
159
|
-
* 调用方可自行加短 TTL 缓存(见 handler),但缓存期必须远小于 config-sync 周期。
|
|
160
|
-
*/
|
|
161
|
-
export function loadObserverRosters(input: {
|
|
162
|
-
bots: unknown;
|
|
163
|
-
answerers: unknown;
|
|
164
|
-
/** 仅用于反查群名。收尾阶段群名已写入表,可省。 */
|
|
165
|
-
groupConfigs?: unknown;
|
|
166
|
-
}): ObserverRosters {
|
|
167
|
-
const bots = parseIdentities(input.bots);
|
|
168
|
-
const answerers = parseIdentities(input.answerers);
|
|
169
|
-
return {
|
|
170
|
-
bots,
|
|
171
|
-
answerers,
|
|
172
|
-
groupNames: parseGroupNames(input.groupConfigs),
|
|
173
|
-
status: rosterStatus(bots, answerers),
|
|
174
|
-
roster: toRoster(bots, answerers),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** 图片标记,用于把纯图片消息与空消息区分开。 */
|
|
179
|
-
const IMAGE_MARK = "![图片]";
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* 把一个 `message_received` 事件拆成逐条 {@link NormalizedMessage}。
|
|
183
|
-
*
|
|
184
|
-
* 丢弃规则(两条,都是刻意的):
|
|
185
|
-
* - **无 openMessageId 一律丢**:既无法归并也无法溯源,记了只会弄脏日志重放;
|
|
186
|
-
* - 既无正文又无图片的丢(文件/系统消息)。
|
|
187
|
-
*
|
|
188
|
-
* `role` 只填 `bot` 或 `unknown`:真正的 asker/answerer 由 core 在收敛时按名单派生。
|
|
189
|
-
* 这里填 `bot` 是必需的 —— core 靠它避免把机器人登记成提问人锚点。
|
|
190
|
-
*/
|
|
191
|
-
export function normalizeEventMessages(
|
|
192
|
-
payload: Record<string, unknown>,
|
|
193
|
-
rosters: Pick<ObserverRosters, "bots" | "answerers" | "groupNames">,
|
|
194
|
-
): Array<{ message: NormalizedMessage; identity: MessageIdentity }> {
|
|
195
|
-
const conversationId = String(payload.conversationId ?? "").trim();
|
|
196
|
-
if (!conversationId) return [];
|
|
197
|
-
// 群名优先用事件源给的(list-all 源有),否则从群配置反查;都没有则空串
|
|
198
|
-
const groupName =
|
|
199
|
-
String(payload.conversationTitle ?? "").trim() || rosters.groupNames.get(conversationId) || "";
|
|
200
|
-
|
|
201
|
-
const raw = Array.isArray(payload.messages) ? payload.messages : [];
|
|
202
|
-
const out: Array<{ message: NormalizedMessage; identity: MessageIdentity }> = [];
|
|
203
|
-
for (const item of raw) {
|
|
204
|
-
if (!item || typeof item !== "object") continue;
|
|
205
|
-
const rec = item as Record<string, unknown>;
|
|
206
|
-
const openMessageId = String(rec.messageId ?? rec.openMessageId ?? "").trim();
|
|
207
|
-
if (!openMessageId) continue;
|
|
208
|
-
|
|
209
|
-
const content = String(rec.content ?? "");
|
|
210
|
-
const images = Array.isArray(rec.images) ? rec.images.map(String) : [];
|
|
211
|
-
if (!content.trim() && images.length === 0 && !content.includes(IMAGE_MARK)) continue;
|
|
212
|
-
|
|
213
|
-
// senderName 兜底顺序对齐两个源:逐群轮询给 senderName,list-all 只给 sender
|
|
214
|
-
const senderName = String(rec.senderName ?? rec.sender ?? payload.senderName ?? "").trim();
|
|
215
|
-
const senderOpenDingTalkId = String(rec.senderOpenDingTalkId ?? "").trim();
|
|
216
|
-
const identity = resolveIdentity(rosters.bots, rosters.answerers, senderName, senderOpenDingTalkId);
|
|
217
|
-
|
|
218
|
-
out.push({
|
|
219
|
-
identity,
|
|
220
|
-
message: {
|
|
221
|
-
openMessageId,
|
|
222
|
-
conversationId,
|
|
223
|
-
groupName,
|
|
224
|
-
senderName,
|
|
225
|
-
senderOpenDingTalkId,
|
|
226
|
-
createTime: parseCreateTime(rec.createTime),
|
|
227
|
-
content,
|
|
228
|
-
images,
|
|
229
|
-
...(typeof rec.quotedMessageId === "string" && rec.quotedMessageId.trim()
|
|
230
|
-
? { quotedMessageId: rec.quotedMessageId.trim() }
|
|
231
|
-
: {}),
|
|
232
|
-
role: identity === "bot" ? "bot" : "unknown",
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
// 归并信号依赖「上一条」,乱序会把追问判成新 thread。源已排序,这里再兜一次底。
|
|
237
|
-
out.sort((a, b) => a.message.createTime - b.message.createTime);
|
|
238
|
-
return out;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* 事件里的时间统一成 epoch ms。
|
|
243
|
-
*
|
|
244
|
-
* 源已把时间归一为带时区的 ISO(无时区的墙上时间按 Asia/Shanghai 解释),所以这里直接
|
|
245
|
-
* `Date.parse` 即可。解析不出来时回落「当下」而不是 0:0 会让这条消息在时间排序里跑到
|
|
246
|
-
* 所有 thread 之前,把归并搅乱。
|
|
247
|
-
*/
|
|
248
|
-
function parseCreateTime(raw: unknown): number {
|
|
249
|
-
const str = typeof raw === "string" ? raw.trim() : "";
|
|
250
|
-
if (str) {
|
|
251
|
-
const ms = Date.parse(str);
|
|
252
|
-
if (Number.isFinite(ms)) return ms;
|
|
253
|
-
}
|
|
254
|
-
return Date.now();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 从 handler_config 取归并策略开关。
|
|
259
|
-
*
|
|
260
|
-
* 只暴露少量开关,不把 core 的全部 option 都摊给用户:R0 需要调的就是「收敛多久」
|
|
261
|
-
* 与「哪些归并信号打开」。启发式与 mention 在 core 里默认关,这里也保持关 —— 想开必须
|
|
262
|
-
* 在 profile 里显式写,且开了就会在复核池里看到 low 置信样本。
|
|
263
|
-
*/
|
|
264
|
-
export function parseThreadOptions(config: Record<string, unknown>): ThreadStoreOptions {
|
|
265
|
-
const num = (v: unknown): number | undefined => {
|
|
266
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
267
|
-
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
268
|
-
};
|
|
269
|
-
const idleMin = num(config.thread_idle_close_min);
|
|
270
|
-
const maxOpenHour = num(config.thread_max_open_hour);
|
|
271
|
-
const patterns = Array.isArray(config.thread_address_patterns)
|
|
272
|
-
? config.thread_address_patterns.map(String).filter((s) => s.includes("{name}"))
|
|
273
|
-
: [];
|
|
274
|
-
return {
|
|
275
|
-
...(idleMin ? { idleCloseMs: idleMin * 60_000 } : {}),
|
|
276
|
-
...(maxOpenHour ? { maxOpenMs: maxOpenHour * 60 * 60_000 } : {}),
|
|
277
|
-
...(config.thread_enable_mention_merge === true ? { enableMentionMerge: true } : {}),
|
|
278
|
-
...(config.thread_enable_heuristic_merge === true ? { enableHeuristicMerge: true } : {}),
|
|
279
|
-
...(patterns.length > 0 ? { addressPatterns: patterns } : {}),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* thread 抽取结果 —— **模板侧业务层**,刻意不放进 core。
|
|
5
|
-
*
|
|
6
|
-
* core 的 thread 模块只回答「哪些消息属于同一个问题」;「这个问题是知识缺口还是需求」、
|
|
7
|
-
* 「落到表里哪一列」是业务口径,换个客户就会变。把它放进 core 就等于让 core 认识
|
|
8
|
-
* `kb_gap`、`需求`、`结论模块` 这些只对本场景成立的概念。
|
|
9
|
-
*
|
|
10
|
-
* 与观察表 `归并置信度` 的关系:**两者必须分开看**。
|
|
11
|
-
* - `归并置信度` 来自 core 的确定性规则,说的是「threadId 归得对不对」;
|
|
12
|
-
* - 本文件的 `confidence` 来自 LLM,说的是「结论/分类抽得对不对」。
|
|
13
|
-
*
|
|
14
|
-
* 混成一列的后果是复核时分不清该改 ThreadStore 策略还是该改 prompt。因此 digest 的低置信
|
|
15
|
-
* 只**追加**到 `归并疑点` 与 `复核状态`,绝不覆盖 `归并置信度`。
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** 问题类型(英文枚举是 LLM 的输出契约,落表时映射成中文,见 {@link typeToColumn})。 */
|
|
19
|
-
export const THREAD_TYPES = ["kb_gap", "requirement", "bug", "answered", "chitchat"] as const;
|
|
20
|
-
export type ThreadType = (typeof THREAD_TYPES)[number];
|
|
21
|
-
|
|
22
|
-
export const THREAD_CONFIDENCES = ["high", "medium", "low"] as const;
|
|
23
|
-
export type ThreadDigestConfidence = (typeof THREAD_CONFIDENCES)[number];
|
|
24
|
-
|
|
25
|
-
export interface ThreadDigest {
|
|
26
|
-
type: ThreadType;
|
|
27
|
-
module?: string;
|
|
28
|
-
/** 检索友好的归一问法 */
|
|
29
|
-
standardQuestion: string;
|
|
30
|
-
/** 本次对话的最终结论(已被推翻的说法不进这里) */
|
|
31
|
-
conclusion: string;
|
|
32
|
-
/** 对先前错误理解的纠正,按发生顺序 */
|
|
33
|
-
amendments?: string[];
|
|
34
|
-
/** 结论所依据的消息的 openMessageId,供人工 review 回群核对 */
|
|
35
|
-
evidenceMessageIds?: string[];
|
|
36
|
-
confidence: ThreadDigestConfidence;
|
|
37
|
-
/** 为何不确定(confidence 非 high 时应给) */
|
|
38
|
-
confidenceReason?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* LLM 输出的边界校验。抽取结果是**外部不可信输入**,必须过这道 schema。
|
|
43
|
-
*
|
|
44
|
-
* `kb_gap / requirement / bug` 要求 conclusion 非空:这三类是要人跟进的,结论为空说明抽取
|
|
45
|
-
* 失败,放过去只会在表里留一行没内容的记录,让复核的人白读一遍。
|
|
46
|
-
*
|
|
47
|
-
* `confidence 非 high 时要求 confidenceReason`:「我不确定」不带理由对复核没有价值,
|
|
48
|
-
* 反而让人重新读一遍 thread。
|
|
49
|
-
*/
|
|
50
|
-
export const threadDigestSchema = z
|
|
51
|
-
.object({
|
|
52
|
-
type: z.enum(THREAD_TYPES),
|
|
53
|
-
module: z.string().trim().min(1).optional(),
|
|
54
|
-
standardQuestion: z.string().trim().min(1),
|
|
55
|
-
conclusion: z.string().trim(),
|
|
56
|
-
amendments: z.array(z.string().trim().min(1)).optional(),
|
|
57
|
-
evidenceMessageIds: z.array(z.string().trim().min(1)).optional(),
|
|
58
|
-
confidence: z.enum(THREAD_CONFIDENCES),
|
|
59
|
-
confidenceReason: z.string().trim().min(1).optional(),
|
|
60
|
-
})
|
|
61
|
-
.superRefine((v, ctx) => {
|
|
62
|
-
const needsConclusion = v.type === "kb_gap" || v.type === "requirement" || v.type === "bug";
|
|
63
|
-
if (needsConclusion && !v.conclusion.trim()) {
|
|
64
|
-
ctx.addIssue({ code: "custom", message: `type=${v.type} 时 conclusion 必填` });
|
|
65
|
-
}
|
|
66
|
-
if (v.confidence !== "high" && !v.confidenceReason) {
|
|
67
|
-
ctx.addIssue({ code: "custom", message: `confidence=${v.confidence} 时 confidenceReason 必填` });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/** 解析并校验抽取结果;失败返回 null 由调用方决定降级(**不要**抛到工作流外)。 */
|
|
72
|
-
export function parseThreadDigest(raw: unknown): ThreadDigest | null {
|
|
73
|
-
const checked = threadDigestSchema.safeParse(raw);
|
|
74
|
-
if (!checked.success) return null;
|
|
75
|
-
const v = checked.data;
|
|
76
|
-
return {
|
|
77
|
-
type: v.type,
|
|
78
|
-
...(v.module ? { module: v.module } : {}),
|
|
79
|
-
standardQuestion: v.standardQuestion,
|
|
80
|
-
conclusion: v.conclusion,
|
|
81
|
-
...(v.amendments?.length ? { amendments: v.amendments } : {}),
|
|
82
|
-
...(v.evidenceMessageIds?.length ? { evidenceMessageIds: v.evidenceMessageIds } : {}),
|
|
83
|
-
confidence: v.confidence,
|
|
84
|
-
...(v.confidenceReason ? { confidenceReason: v.confidenceReason } : {}),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 拼出落入「本次结论」列的文本。
|
|
90
|
-
*
|
|
91
|
-
* 为何 amendments 不单独建列、但也不丢:实测存在「第二个解答者引用中间某条消息做
|
|
92
|
-
* 纠正」的分叉形态,这类 thread 同时含错误理解与正确结论。若输出契约只给单个答案字段,
|
|
93
|
-
* 模型可能选中**被推翻的那个**。因此契约上分两段让模型先把两者区分开,落表时再合成
|
|
94
|
-
* 一列,既保留契约好处又不给观察表加列。
|
|
95
|
-
*/
|
|
96
|
-
export function renderConclusion(digest: ThreadDigest): string {
|
|
97
|
-
const parts = [digest.conclusion.trim()].filter(Boolean);
|
|
98
|
-
if (digest.amendments?.length) {
|
|
99
|
-
parts.push(`【过程更正】${digest.amendments.map((a) => `- ${a}`).join("\n")}`);
|
|
100
|
-
}
|
|
101
|
-
if (digest.evidenceMessageIds?.length) {
|
|
102
|
-
parts.push(`【证据】${digest.evidenceMessageIds.join(", ")}`);
|
|
103
|
-
}
|
|
104
|
-
return parts.join("\n\n");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 类型 → 观察表 `问题类型` 列的选项值。
|
|
109
|
-
*
|
|
110
|
-
* 列里用中文是为了让复核的人不用记英文枚举;`kb_gap` 保留原样,与既有答疑口径的
|
|
111
|
-
* `处置动作` 对齐,便于横向比对两条链路对同一个问题的判断。
|
|
112
|
-
*/
|
|
113
|
-
const TYPE_TO_COLUMN: Record<ThreadType, string> = {
|
|
114
|
-
kb_gap: "kb_gap",
|
|
115
|
-
requirement: "需求",
|
|
116
|
-
bug: "bug",
|
|
117
|
-
answered: "已答",
|
|
118
|
-
chitchat: "闲聊",
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export function typeToColumn(type: ThreadType): string {
|
|
122
|
-
return TYPE_TO_COLUMN[type];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 从 LLM 响应中提取 JSON 对象。
|
|
127
|
-
*
|
|
128
|
-
* 必须兼容 ```json 围栏与裸 JSON 两种形态:同一个模型在不同轮次会给不同包装,只认一种
|
|
129
|
-
* 会把好结果当解析失败丢掉、白烧一次重试额度。
|
|
130
|
-
*/
|
|
131
|
-
export function extractJsonObject(raw: string): Record<string, unknown> | null {
|
|
132
|
-
const trimmed = raw.trim();
|
|
133
|
-
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
134
|
-
const jsonStr = fenced?.[1]?.trim() ?? trimmed;
|
|
135
|
-
const start = jsonStr.indexOf("{");
|
|
136
|
-
const end = jsonStr.lastIndexOf("}");
|
|
137
|
-
if (start < 0 || end < 0) return null;
|
|
138
|
-
try {
|
|
139
|
-
const obj = JSON.parse(jsonStr.slice(start, end + 1));
|
|
140
|
-
return obj && typeof obj === "object" ? (obj as Record<string, unknown>) : null;
|
|
141
|
-
} catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
registry=https://registry.npmjs.org/
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "support-qa-app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "通用群答疑助手(由 aitable-workflow init --template support-qa 生成)。业务差异全部在 profile/profile.yml。",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"run": "aitable-workflow run",
|
|
9
|
-
"setup": "aitable-workflow setup",
|
|
10
|
-
"test": "vitest run",
|
|
11
|
-
"typecheck": "tsc --noEmit"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"aitable-workflow-core": "^0.1.5",
|
|
15
|
-
"@ali/aone-remote-agent": "^4.1.0",
|
|
16
|
-
"@ali/aone-sandbox": "^1.0.10",
|
|
17
|
-
"fflate": "^0.8.2",
|
|
18
|
-
"tsx": "^4.20.6",
|
|
19
|
-
"yaml": "^2.8.1",
|
|
20
|
-
"zod": "^4.1.13"
|
|
21
|
-
},
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"@types/node": "^24.10.1",
|
|
24
|
-
"typescript": "^5.9.3",
|
|
25
|
-
"vitest": "^3.0.0"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"学澜": {
|
|
3
|
-
"userId": "314321",
|
|
4
|
-
"openDingTalkId": "DHqTPjhJwIC6iPVkvTqSiiiSdiSv65f4yxiScR"
|
|
5
|
-
},
|
|
6
|
-
"龙衔": {
|
|
7
|
-
"userId": "132984",
|
|
8
|
-
"openDingTalkId": "DHqTPjhJwIC7l00iSIGLaJhlWGuSudnciiC"
|
|
9
|
-
},
|
|
10
|
-
"饶超": {
|
|
11
|
-
"userId": "442922",
|
|
12
|
-
"openDingTalkId": "DWe5nTPjuTWZOWo5W1x0Vj2GYZnCxN64H"
|
|
13
|
-
},
|
|
14
|
-
"羽承": {
|
|
15
|
-
"userId": "329849",
|
|
16
|
-
"openDingTalkId": "D4NgPmiSwO21ROWo5W1x0Vj2GYZnCxN64H"
|
|
17
|
-
},
|
|
18
|
-
"荣权": {
|
|
19
|
-
"userId": "475007",
|
|
20
|
-
"openDingTalkId": "DtyBiiC76t0EXuHZs1SjvTJ7NFHliioS5CU"
|
|
21
|
-
},
|
|
22
|
-
"桦楠": {
|
|
23
|
-
"userId": "239198",
|
|
24
|
-
"openDingTalkId": "Dnv9vDZLISpHfF7vfnAiiHNbNFHliioS5CU"
|
|
25
|
-
},
|
|
26
|
-
"蓝锋": {
|
|
27
|
-
"userId": "308709",
|
|
28
|
-
"openDingTalkId": "DHqTPjhJwIC5qSGeYbV6RE2r6tk7GINOa"
|
|
29
|
-
},
|
|
30
|
-
"枫与": {
|
|
31
|
-
"userId": "450841",
|
|
32
|
-
"openDingTalkId": "DHqTPjhJwIC6ANlW0Es0sZtfpXstQ2BoC"
|
|
33
|
-
},
|
|
34
|
-
"脉昆": {
|
|
35
|
-
"userId": "13669584"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"字段类型与高级字段": ["学澜"],
|
|
3
|
-
"自动化": ["龙衔"],
|
|
4
|
-
"表单": ["饶超"],
|
|
5
|
-
"仪表盘和应用模式": ["脉昆"],
|
|
6
|
-
"商业化与版本权益": ["羽承", "荣权"],
|
|
7
|
-
"高级权限": ["饶超"],
|
|
8
|
-
"侧栏插件和脚本插件": ["桦楠"],
|
|
9
|
-
"数据源": ["桦楠"],
|
|
10
|
-
"导入 excel": ["桦楠"],
|
|
11
|
-
"AI表格导出": ["枫与"],
|
|
12
|
-
"AI表格数据源": ["桦楠"],
|
|
13
|
-
"AI字段": ["桦楠"],
|
|
14
|
-
"功能实践玩法": ["羽承"],
|
|
15
|
-
"查找引用 & 表格加载等性能问题": ["蓝锋"],
|
|
16
|
-
"数据不显示 & 数据丢失等问题": ["蓝锋"],
|
|
17
|
-
"公式": ["蓝锋"]
|
|
18
|
-
}
|