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,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owner 身份确定性解析(spec §8)
|
|
3
|
-
*
|
|
4
|
-
* 输入 Agent 给出的 module,按「精确 key 查找,不做扩展词/模糊匹配/二次 LLM」解析:
|
|
5
|
-
* module → 功能模块人员配置表快照(config-sync 发布到 ${globalVar.module_owners})
|
|
6
|
-
*
|
|
7
|
-
* 快照由 config-sync 定时从 AI 表格「功能模块人员配置表」(模块-文本 + 人员-人员字段)
|
|
8
|
-
* 拉取,人员经 dws contact user get→search 链式解析出 userId/nick/openDingTalkId 后
|
|
9
|
-
* 才会入快照,因此本模块只做纯查找,不再回读文件、不再二次解析联系人。
|
|
10
|
-
*
|
|
11
|
-
* Owner 数组视为「主负责人 + 备选负责人」:取第一个 Owner 作为主负责人(群内只 @ 一人)。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type OwnerResolution =
|
|
15
|
-
| {
|
|
16
|
-
status: "resolved";
|
|
17
|
-
module: string;
|
|
18
|
-
ownerName: string;
|
|
19
|
-
ownerUserId: string;
|
|
20
|
-
ownerOpenDingTalkId?: string;
|
|
21
|
-
}
|
|
22
|
-
| {
|
|
23
|
-
status: "failed";
|
|
24
|
-
module: string;
|
|
25
|
-
reason: "missing_module" | "unknown_module" | "missing_contact" | "invalid_config";
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type OwnerContact = {
|
|
29
|
-
userId: string;
|
|
30
|
-
nick: string;
|
|
31
|
-
openDingTalkId: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type ModuleOwners = {
|
|
35
|
-
module: string;
|
|
36
|
-
owners: OwnerContact[];
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
40
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
41
|
-
? value as Record<string, unknown>
|
|
42
|
-
: null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function cellString(v: unknown): string {
|
|
46
|
-
return typeof v === "string" ? v.trim() : typeof v === "number" ? String(v) : "";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 模块人员快照标准化(config-sync 发布前与消费端共用):
|
|
51
|
-
* 丢弃缺 module 的行、缺 userId/nick/openDingTalkId 的人;按 module 排序保证快照稳定。
|
|
52
|
-
*/
|
|
53
|
-
export function normalizeModuleOwners(raw: unknown): ModuleOwners[] {
|
|
54
|
-
if (!Array.isArray(raw)) return [];
|
|
55
|
-
return raw.flatMap((value): ModuleOwners[] => {
|
|
56
|
-
const record = asRecord(value);
|
|
57
|
-
const module = cellString(record?.module);
|
|
58
|
-
if (!module) return [];
|
|
59
|
-
const owners = (Array.isArray(record?.owners) ? record.owners : [])
|
|
60
|
-
.flatMap((o): OwnerContact[] => {
|
|
61
|
-
const contact = asRecord(o);
|
|
62
|
-
const userId = cellString(contact?.userId);
|
|
63
|
-
const nick = cellString(contact?.nick);
|
|
64
|
-
const openDingTalkId = cellString(contact?.openDingTalkId);
|
|
65
|
-
return userId && nick && openDingTalkId ? [{ userId, nick, openDingTalkId }] : [];
|
|
66
|
-
});
|
|
67
|
-
return owners.length > 0 ? [{ module, owners }] : [];
|
|
68
|
-
}).sort((left, right) => (
|
|
69
|
-
left.module.localeCompare(right.module)
|
|
70
|
-
|| JSON.stringify(left).localeCompare(JSON.stringify(right))
|
|
71
|
-
));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function resolveOwner(snapshot: unknown, rawModule: string): OwnerResolution {
|
|
75
|
-
const module = String(rawModule ?? "").trim();
|
|
76
|
-
if (!module) return { status: "failed", module, reason: "missing_module" };
|
|
77
|
-
|
|
78
|
-
const moduleOwners = normalizeModuleOwners(snapshot);
|
|
79
|
-
if (moduleOwners.length === 0) return { status: "failed", module, reason: "invalid_config" };
|
|
80
|
-
|
|
81
|
-
// 精确 module 匹配(禁止扩展词/子串/模糊匹配)
|
|
82
|
-
const matched = moduleOwners.find((entry) => entry.module === module);
|
|
83
|
-
if (!matched) return { status: "failed", module, reason: "unknown_module" };
|
|
84
|
-
|
|
85
|
-
const owner = matched.owners[0];
|
|
86
|
-
if (!owner) return { status: "failed", module, reason: "missing_contact" };
|
|
87
|
-
return {
|
|
88
|
-
status: "resolved",
|
|
89
|
-
module,
|
|
90
|
-
ownerName: owner.nick,
|
|
91
|
-
ownerUserId: owner.userId,
|
|
92
|
-
ownerOpenDingTalkId: owner.openDingTalkId,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { isCapabilityEnabled, loadClassifierRules, loadReplyRules } from "./declarative-matcher.js";
|
|
5
|
-
import type { PreflightIssue } from "aitable-workflow-core";
|
|
6
|
-
import { SUPPORT_ACTIONS, KB_STATUS, KB_STATUS_VALUES } from "./contract.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* support-qa 实例 preflight(纯校验,零写盘)
|
|
10
|
-
*
|
|
11
|
-
* 由 core 的 preflight 扩展点在 `aitable-workflow config` 与 `run` / `setup` 启动期加载
|
|
12
|
-
* (约定:实例根 `lib/preflight.ts` 导出 `preflight(configDir)`)。
|
|
13
|
-
*
|
|
14
|
-
* 分档语义(core 按此决定停到什么程度):
|
|
15
|
-
* - fatal:实例级不可信 → 拒绝启动。profile.yml 结构错、persona 缺失/为空、
|
|
16
|
-
* persona 疑似放开通用闸门("闸门只准收紧不准放开"纪律的落点)。
|
|
17
|
-
* - error:可归因到单个 workflow → 只停该轨道,其余工作流照跑。
|
|
18
|
-
* - warn :L3 可选能力关不上 → 该能力优雅关闭,主环照跑。
|
|
19
|
-
*
|
|
20
|
-
* 为什么不再写盘:persona 早已由 core 读取 profile 的 `persona: { file }` 并注入每次
|
|
21
|
-
* coding.run 的 prompt(registry.resolvePersonaFile → RoleContext → agent-adapter),
|
|
22
|
-
* 曾经生成的 workspaces/<role>/persona.md 是第二条冗余通路,已删。
|
|
23
|
-
* 但 core 对 persona 读取失败只 warn + 跳过该角色(会静默降级成 persona=role-id 的
|
|
24
|
-
* 临时角色),因此「persona.file 存在且非空」必须由本文件按 fatal 拦住。
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const GROUP_QA_WORKFLOW = "group-qa";
|
|
28
|
-
const ADMIN_KB_WORKFLOW = "admin-kb-update";
|
|
29
|
-
|
|
30
|
-
/** persona 疑似放开通用闸门的特征:闸门只准收紧,不准放开 */
|
|
31
|
-
const PERSONA_GATE_RELAXING = /忽略(上述|以上|通用)|必须回复所有|无据也(可以|要)回答|不用静默/;
|
|
32
|
-
|
|
33
|
-
function readRawProfile(configDir: string, issues: PreflightIssue[]): Record<string, unknown> | null {
|
|
34
|
-
const file = path.resolve(configDir, "profile", "profile.yml");
|
|
35
|
-
if (!existsSync(file)) {
|
|
36
|
-
issues.push({ level: "fatal", message: `profile/profile.yml 不存在: ${file}` });
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const raw = (parseYaml(readFileSync(file, "utf8")) ?? {}) as Record<string, unknown>;
|
|
41
|
-
if (raw.schema_version !== 2) {
|
|
42
|
-
issues.push({
|
|
43
|
-
level: "fatal",
|
|
44
|
-
message: `profile.yml schema_version 必须为 2(当前: ${raw.schema_version ?? "缺失"})。请按迁移指引升级。`,
|
|
45
|
-
});
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return raw;
|
|
49
|
-
} catch (err) {
|
|
50
|
-
issues.push({
|
|
51
|
-
level: "fatal",
|
|
52
|
-
message: `profile/profile.yml 解析失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
53
|
-
});
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** collections 校验:trim、去空、必须是不含 .. 的相对路径、目录须存在 */
|
|
59
|
-
function validateCollections(configDir: string, raw: unknown, issues: PreflightIssue[]): void {
|
|
60
|
-
const list = Array.isArray(raw)
|
|
61
|
-
? raw.map((c) =>
|
|
62
|
-
typeof c === "string" ? c.trim()
|
|
63
|
-
: c && typeof c === "object" && "collection" in c ? String((c as { collection: string }).collection).trim()
|
|
64
|
-
: String(c ?? "").trim())
|
|
65
|
-
: [];
|
|
66
|
-
if (list.length === 0 || list.some((c) => !c)) {
|
|
67
|
-
issues.push({
|
|
68
|
-
level: "fatal",
|
|
69
|
-
message: "roles.support.knowledge_scope.kb_collections 缺失、为空或含空字符串(必填)",
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
for (const c of list.filter(Boolean)) {
|
|
73
|
-
if (path.isAbsolute(c) || c.split(/[\\/]/).includes("..")) {
|
|
74
|
-
issues.push({ level: "fatal", message: `kb_collections 必须是实例内的相对路径(不含 ..): ${c}` });
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!existsSync(path.resolve(configDir, c))) {
|
|
78
|
-
issues.push({ level: "fatal", message: `kb_collections 目录不存在: ${c}` });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* persona 校验(两项都是 fatal,缺一不可):
|
|
85
|
-
* 1. `{ file }` 引用的文件必须存在且内容非空——否则 core 会静默跳过该角色,
|
|
86
|
-
* Agent 拿到的 persona 退化成角色 id 字面量,答疑质量无声崩塌;
|
|
87
|
-
* 2. 内容不得含"放开通用闸门"的措辞。
|
|
88
|
-
*/
|
|
89
|
-
function validatePersona(configDir: string, supportRole: Record<string, unknown> | undefined, issues: PreflightIssue[]): void {
|
|
90
|
-
if (!supportRole) return;
|
|
91
|
-
const personaField = supportRole.persona;
|
|
92
|
-
let personaText = "";
|
|
93
|
-
|
|
94
|
-
if (typeof personaField === "string") {
|
|
95
|
-
personaText = personaField.trim();
|
|
96
|
-
if (!personaText) {
|
|
97
|
-
issues.push({ level: "fatal", message: "roles.support.persona 为空字符串" });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
} else if (personaField && typeof personaField === "object" && "file" in personaField) {
|
|
101
|
-
const fileRel = String((personaField as { file: string }).file ?? "").trim();
|
|
102
|
-
if (!fileRel) {
|
|
103
|
-
issues.push({ level: "fatal", message: "roles.support.persona.file 为空" });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const abs = path.resolve(configDir, "profile", fileRel);
|
|
107
|
-
if (!existsSync(abs)) {
|
|
108
|
-
issues.push({ level: "fatal", message: `roles.support.persona.file 引用的文件不存在: profile/${fileRel}` });
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
personaText = readFileSync(abs, "utf8").trim();
|
|
112
|
-
if (!personaText) {
|
|
113
|
-
issues.push({ level: "fatal", message: `roles.support.persona.file 文件内容为空: profile/${fileRel}` });
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
issues.push({ level: "fatal", message: "roles.support.persona 缺失(必填)" });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (PERSONA_GATE_RELAXING.test(personaText)) {
|
|
122
|
-
issues.push({
|
|
123
|
-
level: "fatal",
|
|
124
|
-
message: "persona 内容疑似放开通用闸门(只准收紧,不准放开),请修改后重试",
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 集合严格相等比较 */
|
|
130
|
-
function diffSets(actual: string[], expected: readonly string[]): { missing: string[]; extra: string[] } {
|
|
131
|
-
const a = new Set(actual);
|
|
132
|
-
const e = new Set(expected);
|
|
133
|
-
return { missing: expected.filter((v) => !a.has(v)), extra: actual.filter((v) => !e.has(v)) };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getIn(obj: unknown, ...keys: string[]): unknown {
|
|
137
|
-
let current = obj;
|
|
138
|
-
for (const key of keys) {
|
|
139
|
-
if (!current || typeof current !== "object") return undefined;
|
|
140
|
-
current = (current as Record<string, unknown>)[key];
|
|
141
|
-
}
|
|
142
|
-
return current;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* on_action policies 闭集校验:键必须 ⊆ SUPPORT_ACTIONS;group 只允许 answer / silent。
|
|
147
|
-
* group 只表达「群里说不说话」(轴1);Owner 指引由 Runtime 按「action 需要找人 + Owner
|
|
148
|
-
* 可解析」派生(轴2),不存在「通知 Owner」档位。
|
|
149
|
-
*/
|
|
150
|
-
function validateOnActionPolicies(groupQaWf: unknown, issues: PreflightIssue[]): void {
|
|
151
|
-
const onActionStep = getIn(groupQaWf, "steps", "on_action") as Record<string, unknown> | undefined;
|
|
152
|
-
const policiesRaw = onActionStep?.policies;
|
|
153
|
-
if (policiesRaw && typeof policiesRaw === "object") {
|
|
154
|
-
const { extra } = diffSets(Object.keys(policiesRaw), SUPPORT_ACTIONS);
|
|
155
|
-
if (extra.length > 0) {
|
|
156
|
-
issues.push({
|
|
157
|
-
level: "error",
|
|
158
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
159
|
-
message: `on_action.policies 含未知 action [${extra.join(",")}],合法值: [${SUPPORT_ACTIONS.join(",")}]`,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
for (const [actionName, cfg] of Object.entries(policiesRaw as Record<string, { group?: unknown }>)) {
|
|
163
|
-
const group = cfg && typeof cfg === "object" ? cfg.group : undefined;
|
|
164
|
-
if (group === undefined) continue;
|
|
165
|
-
if (group !== "answer" && group !== "silent") {
|
|
166
|
-
issues.push({
|
|
167
|
-
level: "error",
|
|
168
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
169
|
-
message: `on_action.policies.${actionName}.group 只能是 answer / silent(Owner 尾注不在 group 里配,由 Runtime 按 action 与 Owner 解析结果派生),实际: ${JSON.stringify(group)}`,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// 旧 on_silence 步配置仍存在 → 提醒迁移(拍平 action 后该步已改名 on_action)
|
|
175
|
-
if (getIn(groupQaWf, "steps", "on_silence")) {
|
|
176
|
-
issues.push({
|
|
177
|
-
level: "warn",
|
|
178
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
179
|
-
message: "workflows.group-qa.steps.on_silence 已废弃(拍平 action 后步为 on_action):请迁移为 steps.on_action.policies(track/group)",
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** group-qa 单选列 options 与 contract 常量的镜像强校验(双源分家会让路由静默错位) */
|
|
185
|
-
function validateGroupQaFieldOptions(configDir: string, issues: PreflightIssue[]): void {
|
|
186
|
-
const groupQaFile = path.resolve(configDir, "workflows", GROUP_QA_WORKFLOW, "workflow.yml");
|
|
187
|
-
if (!existsSync(groupQaFile)) return;
|
|
188
|
-
try {
|
|
189
|
-
const wf = (parseYaml(readFileSync(groupQaFile, "utf8")) ?? {}) as Record<string, any>;
|
|
190
|
-
const defs = Array.isArray(wf?.tracker?.field_definitions) ? wf.tracker.field_definitions : [];
|
|
191
|
-
for (const [fieldName, expected] of [
|
|
192
|
-
["处置动作", SUPPORT_ACTIONS],
|
|
193
|
-
["知识沉淀", KB_STATUS_VALUES],
|
|
194
|
-
] as const) {
|
|
195
|
-
const matched = defs.filter((d: any) => d?.name === fieldName);
|
|
196
|
-
if (matched.length !== 1) {
|
|
197
|
-
issues.push({
|
|
198
|
-
level: "error",
|
|
199
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
200
|
-
message: `field_definitions 中「${fieldName}」字段必须恰好 1 个,实际 ${matched.length} 个`,
|
|
201
|
-
});
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const options = Array.isArray(matched[0]?.options) ? matched[0].options.map((o: any) => String(o)) : [];
|
|
205
|
-
const { missing, extra } = diffSets(options, expected);
|
|
206
|
-
if (missing.length > 0 || extra.length > 0) {
|
|
207
|
-
issues.push({
|
|
208
|
-
level: "error",
|
|
209
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
210
|
-
message: `「${fieldName}」options 与 lib/contract.ts 不一致:缺[${missing.join(",")}] 多[${extra.join(",")}]`,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
} catch (err) {
|
|
215
|
-
issues.push({
|
|
216
|
-
level: "error",
|
|
217
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
218
|
-
message: `workflow.yml 解析失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** admin-kb-update 事件过滤锚点:锚错了知识更新轨道根本不拉单 */
|
|
224
|
-
function validateAdminKbFilter(workflows: Record<string, Record<string, unknown>>, issues: PreflightIssue[]): void {
|
|
225
|
-
const adminKbWf = workflows[ADMIN_KB_WORKFLOW];
|
|
226
|
-
if (!adminKbWf) return;
|
|
227
|
-
const esConfig = getIn(adminKbWf, "event_sources", "管理员更新监听") as Record<string, unknown> | undefined;
|
|
228
|
-
const filterEquals = typeof esConfig?.filter === "object" && esConfig.filter
|
|
229
|
-
? String((esConfig.filter as Record<string, unknown>).equals ?? "")
|
|
230
|
-
: "";
|
|
231
|
-
if (filterEquals && filterEquals !== KB_STATUS.READY_TO_PERSIST) {
|
|
232
|
-
issues.push({
|
|
233
|
-
level: "error",
|
|
234
|
-
workflowId: ADMIN_KB_WORKFLOW,
|
|
235
|
-
message: `事件过滤 filter.equals="${filterEquals}" 必须为 "${KB_STATUS.READY_TO_PERSIST}"(否则知识更新轨道不拉单)`,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** L3 可选能力预检(一律 warn:运行期缺失即优雅关闭,不阻断 L0 主环) */
|
|
241
|
-
function validateOptionalCapabilities(configDir: string, groupQaWf: unknown, issues: PreflightIssue[]): void {
|
|
242
|
-
const fastpathStep = getIn(groupQaWf, "steps", "fastpath") as Record<string, unknown> | undefined;
|
|
243
|
-
const fastpathRel = typeof fastpathStep?.rules === "string" ? fastpathStep.rules.trim() : "";
|
|
244
|
-
if (fastpathRel) {
|
|
245
|
-
const { issues: fpIssues } = loadReplyRules(configDir, fastpathRel);
|
|
246
|
-
for (const msg of fpIssues) {
|
|
247
|
-
issues.push({ level: "warn", workflowId: GROUP_QA_WORKFLOW, message: `fastpath: ${msg}` });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const troubleshootStep = getIn(groupQaWf, "steps", "troubleshoot") as Record<string, unknown> | undefined;
|
|
252
|
-
const skillsDirRel = typeof troubleshootStep?.skills_dir === "string" ? troubleshootStep.skills_dir.trim() : "";
|
|
253
|
-
if (skillsDirRel && !isCapabilityEnabled(configDir, skillsDirRel)) {
|
|
254
|
-
issues.push({
|
|
255
|
-
level: "warn",
|
|
256
|
-
workflowId: GROUP_QA_WORKFLOW,
|
|
257
|
-
message: `troubleshoot: skills_dir 目录不存在,整档将关闭: ${skillsDirRel}`,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
const classifiersRel = typeof troubleshootStep?.classifiers === "string" ? troubleshootStep.classifiers.trim() : "";
|
|
261
|
-
if (classifiersRel) {
|
|
262
|
-
const { issues: tsIssues } = loadClassifierRules(configDir, classifiersRel);
|
|
263
|
-
for (const msg of tsIssues) {
|
|
264
|
-
issues.push({ level: "warn", workflowId: GROUP_QA_WORKFLOW, message: `troubleshoot: ${msg}` });
|
|
265
|
-
}
|
|
266
|
-
// classifier.module 合法性不在此交叉校验:模块闭集来自功能模块人员配置表
|
|
267
|
-
// (config-sync 运行期快照),拼写不匹配时运行期降级并记 owner_resolution_failed。
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** core preflight 扩展点约定的导出 */
|
|
272
|
-
export function preflight(configDir: string): PreflightIssue[] {
|
|
273
|
-
const issues: PreflightIssue[] = [];
|
|
274
|
-
|
|
275
|
-
const raw = readRawProfile(configDir, issues);
|
|
276
|
-
if (!raw) return issues; // profile 读不了,后续校验都无意义
|
|
277
|
-
|
|
278
|
-
const roles = (raw.roles ?? {}) as Record<string, Record<string, unknown>>;
|
|
279
|
-
const workflows = (raw.workflows ?? {}) as Record<string, Record<string, unknown>>;
|
|
280
|
-
const supportRole = roles.support;
|
|
281
|
-
const groupQaWf = workflows[GROUP_QA_WORKFLOW];
|
|
282
|
-
|
|
283
|
-
const roleName = typeof supportRole?.name === "string" ? supportRole.name.trim() : "";
|
|
284
|
-
if (!roleName) issues.push({ level: "fatal", message: "roles.support.name 缺失(必填)" });
|
|
285
|
-
validateCollections(configDir, (supportRole?.knowledge_scope as Record<string, unknown> | undefined)?.kb_collections, issues);
|
|
286
|
-
validatePersona(configDir, supportRole, issues);
|
|
287
|
-
|
|
288
|
-
validateOnActionPolicies(groupQaWf, issues);
|
|
289
|
-
validateGroupQaFieldOptions(configDir, issues);
|
|
290
|
-
validateAdminKbFilter(workflows, issues);
|
|
291
|
-
validateOptionalCapabilities(configDir, groupQaWf, issues);
|
|
292
|
-
|
|
293
|
-
return issues;
|
|
294
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { SupportAction } from "./contract.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* support-qa 处置策略类型 + 默认值(拍平 action 模型)
|
|
5
|
-
*
|
|
6
|
-
* profile v2 后 loadProfile() 已废弃(由 core profile-loader 统一注入 ctx.config):
|
|
7
|
-
* on-action 步从 ctx.config.policies 读逐 action 覆写(profile.yml 的
|
|
8
|
-
* workflows.group-qa.steps.on_action.policies 段),在 recipe 内与默认值 spread 合并
|
|
9
|
-
* (与 dev 基线 on_silence 的读取方式一致);本文件仅保留类型与常量。
|
|
10
|
-
* group 合法值由 assemble preflight 闭集校验把关(answer/silent,无 notify_owner)。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 群内行为(轴1:只管群里说不说话):
|
|
15
|
-
* - `answer`:发送回答;
|
|
16
|
-
* - `silent`:群里不说话(留痕与通知字段不受影响)。
|
|
17
|
-
*
|
|
18
|
-
* 是否附 Owner 指引(轴2)**不由本字段控制**,由 Runtime 按
|
|
19
|
-
* 「action 是否需要找人 + Owner 能不能解析出来」派生(见 action-dispatch 的
|
|
20
|
-
* needsOwnerResolution)——两个轴正交,`answer` 也能带 Owner 尾注。
|
|
21
|
-
*/
|
|
22
|
-
export type GroupAction = "answer" | "silent";
|
|
23
|
-
|
|
24
|
-
export type ActionPolicy = {
|
|
25
|
-
/** 是否建行留痕(tracker materialize) */
|
|
26
|
-
track: boolean;
|
|
27
|
-
/** 群里做什么(不用裸字段 silence:group: silent 只表示群内不说话,留痕由 track 单独表达) */
|
|
28
|
-
group: GroupAction;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** profile.yml 里 policies 段的原始形状(逐 action 局部覆写) */
|
|
32
|
-
export type ActionPolicyOverrides = Partial<Record<SupportAction, Partial<ActionPolicy>>>;
|
|
33
|
-
|
|
34
|
-
/** 群内是否说话(轴1 的唯一判定点) */
|
|
35
|
-
export function speaksInGroup(policy: ActionPolicy): boolean {
|
|
36
|
-
return policy.group !== "silent";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Pattern 默认 action 处置(spec §7.1):可被 policies 逐项覆写(recipe 内 spread 合并) */
|
|
40
|
-
export const DEFAULT_ON_ACTION: Record<SupportAction, ActionPolicy> = {
|
|
41
|
-
reply: { track: true, group: "answer" },
|
|
42
|
-
// bug/feature 群内说话;Owner 尾注由 Runtime 派生附加(不在 group 里表达)
|
|
43
|
-
bug: { track: true, group: "answer" },
|
|
44
|
-
feature: { track: true, group: "answer" },
|
|
45
|
-
kb_gap: { track: true, group: "silent" },
|
|
46
|
-
no_question: { track: false, group: "silent" },
|
|
47
|
-
human_handled: { track: false, group: "silent" },
|
|
48
|
-
owner_mentioned: { track: true, group: "silent" },
|
|
49
|
-
safety: { track: true, group: "silent" },
|
|
50
|
-
agent_failure: { track: true, group: "silent" },
|
|
51
|
-
};
|