@linkmind_claw/openclaw-faq-kb 2.0.3 → 3.1.0
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/dist/index.d.ts +7 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +659 -315
- package/dist/index.js.map +1 -1
- package/dist/lib/api-client.d.ts +23 -39
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +54 -97
- package/dist/lib/api-client.js.map +1 -1
- package/dist/tools/account-manage.d.ts +5 -7
- package/dist/tools/account-manage.d.ts.map +1 -1
- package/dist/tools/account-manage.js +104 -70
- package/dist/tools/account-manage.js.map +1 -1
- package/dist/tools/account-register.d.ts +12 -5
- package/dist/tools/account-register.d.ts.map +1 -1
- package/dist/tools/account-register.js +51 -28
- package/dist/tools/account-register.js.map +1 -1
- package/dist/tools/query-kb.d.ts +4 -5
- package/dist/tools/query-kb.d.ts.map +1 -1
- package/dist/tools/query-kb.js +11 -37
- package/dist/tools/query-kb.js.map +1 -1
- package/package.json +3 -3
- package/skills/faq-kb/SKILL.md +146 -42
- package/linkmind_claw-openclaw-faq-kb-1.0.6.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.1.0.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.2.0.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.3.1.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.3.2.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.3.3.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-1.4.0.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-2.0.0.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-2.0.1.tgz +0 -0
- package/linkmind_claw-openclaw-faq-kb-2.0.2.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw Plugin —
|
|
2
|
+
* OpenClaw Plugin entry — FAQ Knowledge Base
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 管理员通过 /linkmind_claim 绑定身份,拥有全部管理权限
|
|
6
|
-
* 其他用户只能查询被开放的知识库
|
|
4
|
+
* 5 tools + 5 slash commands:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* /linkmind_query — 知识库问答
|
|
14
|
-
* /linkmind_share — 管理 KB 权限(仅 Owner)
|
|
15
|
-
* /linkmind_list — 列出可访问的知识库
|
|
6
|
+
* 1. faq_account_register — channel user auto-register / login
|
|
7
|
+
* 2. faq_account_manage — grant roles, list users, view my info
|
|
8
|
+
* 3. faq_kb_create_and_train — one-click: create KB + auto-pipeline
|
|
9
|
+
* 4. faq_kb_query — intelligent Q&A
|
|
10
|
+
* 5. faq_kb_list — list accessible knowledge bases
|
|
16
11
|
*/
|
|
17
12
|
import { Type } from "@sinclair/typebox";
|
|
18
13
|
import { FaqKbApiClient } from "./lib/api-client.js";
|
|
@@ -26,144 +21,81 @@ function definePluginEntry(entry) {
|
|
|
26
21
|
// ── Export ─────────────────────────────────────────────────────────────
|
|
27
22
|
export default definePluginEntry({
|
|
28
23
|
id: "faq-knowledge-base",
|
|
29
|
-
name: "
|
|
30
|
-
description: "
|
|
24
|
+
name: "FAQ Knowledge Base",
|
|
25
|
+
description: "FAQ 知识库插件:社媒用户自动注册、权限管理、一键建库训练、智能问答",
|
|
31
26
|
register(api) {
|
|
32
27
|
const cfg = (api.pluginConfig ?? {});
|
|
33
28
|
const baseUrl = cfg.apiBaseUrl || "http://67.212.146.21:8999";
|
|
34
29
|
const pollInterval = cfg.pollIntervalMs ?? 8_000;
|
|
35
30
|
const pipelineTimeout = cfg.pipelineTimeoutMs ?? 20 * 60 * 1000;
|
|
31
|
+
const defaultBotId = cfg.defaultBotId ?? "default";
|
|
36
32
|
const client = new FaqKbApiClient({
|
|
37
33
|
baseUrl,
|
|
38
34
|
staticToken: cfg.apiToken || undefined,
|
|
39
35
|
});
|
|
40
|
-
|
|
41
|
-
async function getOwnerStatus() {
|
|
42
|
-
if (cachedOwnerStatus && cachedOwnerStatus.claimed && Date.now() - cachedOwnerStatus.checkedAt < 60_000) {
|
|
43
|
-
return { claimed: true, owner_telegram_id: null };
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
const res = await client.get("/owner/status");
|
|
47
|
-
const data = res.data ?? res;
|
|
48
|
-
cachedOwnerStatus = { claimed: !!data.claimed, checkedAt: Date.now() };
|
|
49
|
-
return data;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return { claimed: false, owner_telegram_id: null };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
async function ensureAuth(ctx) {
|
|
56
|
-
if (!ctx.senderId || !ctx.channel) {
|
|
57
|
-
return "无法获取你的用户信息,请在社媒渠道中使用。";
|
|
58
|
-
}
|
|
59
|
-
const status = await getOwnerStatus();
|
|
60
|
-
if (!status.claimed) {
|
|
61
|
-
const secret = status.claim_secret;
|
|
62
|
-
if (secret) {
|
|
63
|
-
return [
|
|
64
|
-
"🎉 欢迎使用 LinkMind!此服务器尚未绑定管理员。",
|
|
65
|
-
"你是第一个使用此 Bot 的用户,可以直接激活管理员身份!",
|
|
66
|
-
"",
|
|
67
|
-
"请发送以下命令完成绑定:",
|
|
68
|
-
` /linkmind_claim ${secret}`,
|
|
69
|
-
"",
|
|
70
|
-
"绑定后你将拥有完整的知识库管理权限。",
|
|
71
|
-
].join("\n");
|
|
72
|
-
}
|
|
73
|
-
return [
|
|
74
|
-
"此服务器尚未绑定管理员。",
|
|
75
|
-
"如果你是部署者,请使用以下命令完成绑定:",
|
|
76
|
-
" /linkmind_claim <部署时生成的密钥>",
|
|
77
|
-
"密钥可在服务器启动日志中找到。",
|
|
78
|
-
].join("\n");
|
|
79
|
-
}
|
|
80
|
-
client.setSender(ctx.senderId);
|
|
81
|
-
if (client.hasUserToken(ctx.senderId)) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
await client.oauthLogin({
|
|
86
|
-
channel_type: ctx.channel,
|
|
87
|
-
channel_user_id: ctx.senderId,
|
|
88
|
-
display_name: "",
|
|
89
|
-
});
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
return `⚠️ 自动登录失败: ${e.message}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function requireOwner(ctx) {
|
|
97
|
-
const authErr = await ensureAuth(ctx);
|
|
98
|
-
if (authErr)
|
|
99
|
-
return authErr;
|
|
100
|
-
try {
|
|
101
|
-
const res = await client.get("/oauth/me");
|
|
102
|
-
const data = res.data ?? res;
|
|
103
|
-
if (!data.is_owner) {
|
|
104
|
-
const status = await getOwnerStatus();
|
|
105
|
-
if (!status.claimed && status.claim_secret) {
|
|
106
|
-
return [
|
|
107
|
-
"此服务器尚未绑定管理员。",
|
|
108
|
-
"请先完成绑定:",
|
|
109
|
-
` /linkmind_claim ${status.claim_secret}`,
|
|
110
|
-
].join("\n");
|
|
111
|
-
}
|
|
112
|
-
return "此操作仅限管理员。如需查询知识库请使用 /linkmind_query";
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return "无法验证管理员身份,请稍后再试。";
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// ── Tools ──────────────────────────────────────────────────────────
|
|
36
|
+
// ── Tool 1: Account Register ─────────────────────────────────────
|
|
121
37
|
api.registerTool({
|
|
122
|
-
name: "
|
|
123
|
-
label: "
|
|
38
|
+
name: "faq_account_register",
|
|
39
|
+
label: "社媒用户注册/登录",
|
|
124
40
|
description: [
|
|
125
|
-
"
|
|
126
|
-
"
|
|
41
|
+
"社媒渠道用户自动注册或登录。",
|
|
42
|
+
"首次调用为指定渠道用户创建账号,后续调用返回已有身份。",
|
|
127
43
|
"支持 Telegram / Discord / Slack / Web 等渠道。",
|
|
44
|
+
"新用户默认角色为 viewer(只能查询),Owner 可通过 faq_account_manage 提升权限。",
|
|
45
|
+
"注册后 API Key 自动缓存,后续所有操作使用该用户身份鉴权。",
|
|
128
46
|
].join(" "),
|
|
129
47
|
parameters: AccountRegisterParams,
|
|
130
48
|
execute: (_id, params) => executeAccountRegister(client, params),
|
|
131
49
|
});
|
|
50
|
+
// ── Tool 2: Account Manage ───────────────────────────────────────
|
|
132
51
|
api.registerTool({
|
|
133
|
-
name: "
|
|
134
|
-
label: "
|
|
52
|
+
name: "faq_account_manage",
|
|
53
|
+
label: "账号权限管理",
|
|
135
54
|
description: [
|
|
136
|
-
"
|
|
137
|
-
"grant_role
|
|
55
|
+
"管理社媒渠道用户的权限和角色。需要 admin 权限。",
|
|
56
|
+
"grant_role: 给指定用户授权角色和知识库访问范围;",
|
|
57
|
+
"list_users: 列出所有已注册的渠道用户;",
|
|
58
|
+
"my_info: 查看当前用户的身份、角色和配额信息。",
|
|
138
59
|
].join(" "),
|
|
139
60
|
parameters: AccountManageParams,
|
|
140
61
|
execute: (_id, params) => executeAccountManage(client, params),
|
|
141
62
|
});
|
|
63
|
+
// ── Tool 3: Create & Train ───────────────────────────────────────
|
|
142
64
|
api.registerTool({
|
|
143
|
-
name: "
|
|
65
|
+
name: "faq_kb_create_and_train",
|
|
144
66
|
label: "创建知识库并训练",
|
|
145
67
|
description: [
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
68
|
+
"创建一个新的 FAQ 知识库并自动完成全部训练流程。需要 editor 以上权限。",
|
|
69
|
+
"内部执行: 创建知识库 → 上传文档 → AI 提取 FAQ → 构建知识树 → 建索引。",
|
|
70
|
+
"训练完成后知识库即可用于问答(faq_kb_query)。",
|
|
71
|
+
"支持 PDF / DOCX / DOC / TXT / MD 格式,通过 file_url 传入文件地址。",
|
|
72
|
+
"训练过程通常需要 2-10 分钟,取决于文档大小。",
|
|
149
73
|
].join(" "),
|
|
150
74
|
parameters: CreateAndTrainParams,
|
|
151
75
|
execute: (_id, params) => executeCreateAndTrain(client, params, pollInterval, pipelineTimeout),
|
|
152
76
|
});
|
|
77
|
+
// ── Tool 4: Query ────────────────────────────────────────────────
|
|
153
78
|
api.registerTool({
|
|
154
|
-
name: "
|
|
79
|
+
name: "faq_kb_query",
|
|
155
80
|
label: "知识库问答",
|
|
156
81
|
description: [
|
|
157
|
-
"
|
|
158
|
-
"
|
|
82
|
+
"向指定知识库提问并获取 AI 智能回答。需要 viewer 以上权限。",
|
|
83
|
+
"知识库必须已经训练完成(is_active=true)。",
|
|
84
|
+
"设置 enable_polish=true 可让 LLM 综合多个 FAQ 生成更自然的回答。",
|
|
85
|
+
"返回包含答案、置信度、来源引用和日志 ID。",
|
|
159
86
|
].join(" "),
|
|
160
87
|
parameters: QueryParams,
|
|
161
88
|
execute: (_id, params) => executeQuery(client, params),
|
|
162
89
|
});
|
|
90
|
+
// ── Tool 5: List KB ──────────────────────────────────────────────
|
|
163
91
|
api.registerTool({
|
|
164
|
-
name: "
|
|
92
|
+
name: "faq_kb_list",
|
|
165
93
|
label: "查看知识库列表",
|
|
166
|
-
description:
|
|
94
|
+
description: [
|
|
95
|
+
"列出当前可访问的所有知识库。需要 viewer 以上权限。",
|
|
96
|
+
"返回每个知识库的 ID、名称、标签和训练状态。",
|
|
97
|
+
"支持按名称和标签筛选,支持分页。",
|
|
98
|
+
].join(" "),
|
|
167
99
|
parameters: Type.Object({
|
|
168
100
|
keyword: Type.Optional(Type.String({ description: "按名称搜索(可选)" })),
|
|
169
101
|
tag_name: Type.Optional(Type.String({ description: "按标签筛选(可选)" })),
|
|
@@ -183,150 +115,95 @@ export default definePluginEntry({
|
|
|
183
115
|
const total = res.total ?? items.length;
|
|
184
116
|
const lines = [`知识库列表(共 ${total} 个):`];
|
|
185
117
|
for (const kb of items) {
|
|
186
|
-
const
|
|
118
|
+
const s = kb.is_active ? "就绪" : "未训练";
|
|
187
119
|
const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
|
|
188
|
-
lines.push(` ${kb.id} ${kb.name}${tag} (${
|
|
120
|
+
lines.push(` ${kb.id} ${kb.name}${tag} (${s})`);
|
|
189
121
|
}
|
|
190
122
|
if (items.length === 0)
|
|
191
123
|
lines.push(" (无知识库)");
|
|
192
124
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
193
125
|
},
|
|
194
126
|
});
|
|
195
|
-
//
|
|
127
|
+
// ══════════════════════════════════════════════════════════════════
|
|
128
|
+
// Slash Commands — 5 个,和 5 个 Tools 一一对应
|
|
129
|
+
// ══════════════════════════════════════════════════════════════════
|
|
130
|
+
// 1. /faq_register [channel_user_id] — 注册/登录
|
|
196
131
|
api.registerCommand({
|
|
197
|
-
name: "
|
|
198
|
-
description: "
|
|
132
|
+
name: "faq_register",
|
|
133
|
+
description: "注册/登录: /faq_register 或 /faq_register <渠道用户ID>",
|
|
134
|
+
acceptsArgs: true,
|
|
199
135
|
handler: async (ctx) => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
catch { /* ignore */ }
|
|
219
|
-
}
|
|
220
|
-
const lines = [
|
|
221
|
-
"LinkMind 智能知识库",
|
|
222
|
-
"━━━━━━━━━━━━━━━━━━",
|
|
223
|
-
];
|
|
224
|
-
if (!status.claimed) {
|
|
225
|
-
const secret = status.claim_secret;
|
|
226
|
-
if (secret) {
|
|
227
|
-
lines.push("", "🎉 此服务器尚未绑定管理员,你可以直接激活!", "", "请发送以下命令:", ` /linkmind_claim ${secret}`, "", "绑定后即可创建和管理知识库。");
|
|
136
|
+
const channelUserId = (ctx.args ?? "").trim() || ctx.senderId || "unknown";
|
|
137
|
+
const channelType = ctx.channel.includes("telegram") ? "telegram"
|
|
138
|
+
: ctx.channel.includes("discord") ? "discord"
|
|
139
|
+
: ctx.channel.includes("slack") ? "slack"
|
|
140
|
+
: "web";
|
|
141
|
+
try {
|
|
142
|
+
const res = await client.post("/auth/channel_register", {
|
|
143
|
+
channel_type: channelType,
|
|
144
|
+
channel_user_id: channelUserId,
|
|
145
|
+
channel_bot_id: defaultBotId,
|
|
146
|
+
display_name: "",
|
|
147
|
+
role: "viewer",
|
|
148
|
+
});
|
|
149
|
+
if (!res.success)
|
|
150
|
+
return { text: `注册失败: ${res.message}` };
|
|
151
|
+
const d = res.data;
|
|
152
|
+
if (d.api_key) {
|
|
153
|
+
client.storeUserApiKey({ channelType, channelUserId, channelBotId: defaultBotId }, d.api_key);
|
|
228
154
|
}
|
|
229
|
-
|
|
230
|
-
|
|
155
|
+
if (d.is_new) {
|
|
156
|
+
return { text: `注册成功!\n 用户: ${d.name}\n 角色: ${d.role}\n ID: ${d.user_id}\n\n你现在可以使用 /faq_list 查看知识库,/faq_query 进行问答。` };
|
|
231
157
|
}
|
|
158
|
+
return { text: `欢迎回来!\n 用户: ${d.name}\n 角色: ${d.role}\n ID: ${d.user_id}` };
|
|
232
159
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
lines.push("", "可用命令:", "", " /linkmind_list", " 查看你可访问的知识库", "", " /linkmind_query <知识库名称> <问题>", " 向知识库提问", "", " /linkmind_me", " 查看你的身份信息");
|
|
160
|
+
catch (e) {
|
|
161
|
+
return { text: `注册失败: ${e.message}` };
|
|
238
162
|
}
|
|
239
|
-
return { text: lines.join("\n") };
|
|
240
163
|
},
|
|
241
164
|
});
|
|
165
|
+
// 2. /faq_grant <channel_user_id> <role> — 授权
|
|
242
166
|
api.registerCommand({
|
|
243
|
-
name: "
|
|
244
|
-
description: "
|
|
167
|
+
name: "faq_grant",
|
|
168
|
+
description: "授权: /faq_grant <渠道用户ID> <角色>",
|
|
245
169
|
acceptsArgs: true,
|
|
170
|
+
requireAuth: true,
|
|
246
171
|
handler: async (ctx) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const secret = (ctx.args ?? "").trim();
|
|
251
|
-
if (!secret) {
|
|
252
|
-
return {
|
|
253
|
-
text: [
|
|
254
|
-
"用法: /linkmind_claim <密钥>",
|
|
255
|
-
"",
|
|
256
|
-
"密钥在首次对话时自动展示,或可在服务器启动日志中查看。",
|
|
257
|
-
"绑定后你将成为此服务器的管理员,拥有创建/管理知识库的权限。",
|
|
258
|
-
].join("\n"),
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
client.setSender(ctx.senderId);
|
|
262
|
-
if (!client.hasUserToken(ctx.senderId)) {
|
|
263
|
-
try {
|
|
264
|
-
await client.oauthLogin({
|
|
265
|
-
channel_type: ctx.channel,
|
|
266
|
-
channel_user_id: ctx.senderId,
|
|
267
|
-
display_name: "",
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
return { text: `⚠️ 登录失败: ${e.message}` };
|
|
272
|
-
}
|
|
172
|
+
const parts = (ctx.args ?? "").trim().split(/\s+/);
|
|
173
|
+
if (parts.length < 2) {
|
|
174
|
+
return { text: "用法: /faq_grant <渠道用户ID> <角色>\n角色: viewer / editor / reviewer / admin\n示例: /faq_grant 123456789 editor" };
|
|
273
175
|
}
|
|
176
|
+
const [targetId, role] = parts;
|
|
177
|
+
const channelType = ctx.channel.includes("telegram") ? "telegram"
|
|
178
|
+
: ctx.channel.includes("discord") ? "discord"
|
|
179
|
+
: "web";
|
|
274
180
|
try {
|
|
275
|
-
const res = await client.post("/
|
|
276
|
-
|
|
181
|
+
const res = await client.post("/auth/grant_role", {
|
|
182
|
+
target_channel_user_id: targetId,
|
|
183
|
+
target_channel_type: channelType,
|
|
184
|
+
channel_bot_id: defaultBotId,
|
|
185
|
+
role,
|
|
277
186
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"",
|
|
283
|
-
"你现在是此服务器的管理员。",
|
|
284
|
-
"输入 /linkmind_help 查看可用命令。",
|
|
285
|
-
].join("\n"),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
catch (e) {
|
|
289
|
-
return { text: `❌ 绑定失败: ${e.message}` };
|
|
290
|
-
}
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
api.registerCommand({
|
|
294
|
-
name: "linkmind_me",
|
|
295
|
-
description: "查看我的身份信息",
|
|
296
|
-
handler: async (ctx) => {
|
|
297
|
-
const authErr = await ensureAuth(ctx);
|
|
298
|
-
if (authErr)
|
|
299
|
-
return { text: authErr };
|
|
300
|
-
try {
|
|
301
|
-
const res = await client.get("/oauth/me");
|
|
302
|
-
const d = res.data ?? res;
|
|
303
|
-
const ownerTag = d.is_owner ? " (管理员)" : "";
|
|
304
|
-
const lines = [
|
|
305
|
-
`✅ 你已登录${ownerTag}`,
|
|
306
|
-
` 用户 ID: ${d.user_id ?? d.id ?? "—"}`,
|
|
307
|
-
` 名称: ${d.name ?? "—"}`,
|
|
308
|
-
` 角色: ${d.role ?? "—"}`,
|
|
309
|
-
` 渠道: ${d.channel_type ?? "—"}`,
|
|
310
|
-
];
|
|
311
|
-
return { text: lines.join("\n") };
|
|
187
|
+
if (!res.success)
|
|
188
|
+
return { text: `授权失败: ${res.message}` };
|
|
189
|
+
const d = res.data;
|
|
190
|
+
return { text: `授权成功!\n 用户: ${d.name}\n 角色变更: ${d.old_role} -> ${d.new_role}` };
|
|
312
191
|
}
|
|
313
192
|
catch (e) {
|
|
314
|
-
return { text:
|
|
193
|
+
return { text: `授权失败: ${e.message}` };
|
|
315
194
|
}
|
|
316
195
|
},
|
|
317
196
|
});
|
|
197
|
+
// 3. /faq_create <kb_name> <file_url>
|
|
318
198
|
api.registerCommand({
|
|
319
|
-
name: "
|
|
320
|
-
description: "
|
|
199
|
+
name: "faq_create",
|
|
200
|
+
description: "创建知识库并训练: /faq_create <名称> <文档URL>",
|
|
321
201
|
acceptsArgs: true,
|
|
322
202
|
handler: async (ctx) => {
|
|
323
|
-
const authErr = await requireOwner(ctx);
|
|
324
|
-
if (authErr)
|
|
325
|
-
return { text: authErr };
|
|
326
203
|
const args = (ctx.args ?? "").trim();
|
|
327
204
|
const spaceIdx = args.indexOf(" ");
|
|
328
205
|
if (!args || spaceIdx === -1) {
|
|
329
|
-
return { text: "用法: /
|
|
206
|
+
return { text: "用法: /faq_create <知识库名称> <文档URL>\n示例: /faq_create 客服知识库 https://example.com/doc.pdf\n支持 PDF/DOCX/DOC/TXT/MD" };
|
|
330
207
|
}
|
|
331
208
|
const kbName = args.slice(0, spaceIdx).trim();
|
|
332
209
|
const fileUrl = args.slice(spaceIdx + 1).trim();
|
|
@@ -342,149 +219,616 @@ export default definePluginEntry({
|
|
|
342
219
|
name: kbName, description: "", file_path: fileUrl,
|
|
343
220
|
});
|
|
344
221
|
if (!pipeRes.success) {
|
|
345
|
-
return { text:
|
|
222
|
+
return { text: `启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
|
|
346
223
|
}
|
|
347
|
-
return { text:
|
|
224
|
+
return { text: `知识库创建成功,训练已启动!\n 名称: ${kbName}\n KB ID: ${kbId}\n 任务 ID: ${pipeRes.data.task_id}\n\n训练需要 2-10 分钟,完成后可用 /faq_query 提问。` };
|
|
348
225
|
}
|
|
349
226
|
catch (e) {
|
|
350
|
-
return { text:
|
|
227
|
+
return { text: `创建失败: ${e.message}` };
|
|
351
228
|
}
|
|
352
229
|
},
|
|
353
230
|
});
|
|
231
|
+
// 4. /faq_query <kb_id> <question>
|
|
354
232
|
api.registerCommand({
|
|
355
|
-
name: "
|
|
356
|
-
description: "知识库问答: /
|
|
233
|
+
name: "faq_query",
|
|
234
|
+
description: "知识库问答: /faq_query <kb_id> <问题>",
|
|
357
235
|
acceptsArgs: true,
|
|
358
236
|
handler: async (ctx) => {
|
|
359
|
-
const authErr = await ensureAuth(ctx);
|
|
360
|
-
if (authErr)
|
|
361
|
-
return { text: authErr };
|
|
362
237
|
const args = (ctx.args ?? "").trim();
|
|
363
238
|
const spaceIdx = args.indexOf(" ");
|
|
364
239
|
if (!args || spaceIdx === -1) {
|
|
365
|
-
return { text: "用法: /
|
|
240
|
+
return { text: "用法: /faq_query <kb_id> <问题>\n示例: /faq_query abc123 什么是退款政策?" };
|
|
366
241
|
}
|
|
367
|
-
const
|
|
242
|
+
const kbId = args.slice(0, spaceIdx).trim();
|
|
368
243
|
const question = args.slice(spaceIdx + 1).trim();
|
|
369
244
|
if (!question)
|
|
370
245
|
return { text: "请提供问题" };
|
|
371
246
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
247
|
+
const res = await client.post(`/knowledge_bases/${kbId}/query`, {
|
|
248
|
+
question, enable_polish: false,
|
|
249
|
+
});
|
|
250
|
+
if (!res.success)
|
|
251
|
+
return { text: `查询失败: ${res.message}` };
|
|
252
|
+
const d = res.data;
|
|
253
|
+
const lines = [d.answer || "(无答案)"];
|
|
254
|
+
if (d.faq_data?.question)
|
|
255
|
+
lines.push(`匹配: ${d.faq_data.question}`);
|
|
256
|
+
if (d.source_nodes?.[0]?.score != null) {
|
|
257
|
+
lines.push(`置信度: ${(d.source_nodes[0].score * 100).toFixed(1)}%`);
|
|
377
258
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
259
|
+
return { text: lines.join("\n") };
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return { text: `问答失败: ${e.message}` };
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
// 5. /faq_list
|
|
267
|
+
api.registerCommand({
|
|
268
|
+
name: "faq_list",
|
|
269
|
+
description: "列出所有知识库",
|
|
270
|
+
handler: async () => {
|
|
271
|
+
try {
|
|
272
|
+
const res = await client.get("/knowledge_bases?page=1&page_size=0");
|
|
273
|
+
const items = res.items ?? res.data?.items ?? [];
|
|
274
|
+
const total = res.total ?? items.length;
|
|
275
|
+
const lines = [`知识库列表(共 ${total} 个):`];
|
|
276
|
+
for (const kb of items) {
|
|
277
|
+
const st = kb.is_active ? "就绪" : "未训练";
|
|
278
|
+
const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
|
|
279
|
+
lines.push(` ${kb.name}${tag} ${st}\n ID: ${kb.id}`);
|
|
280
|
+
}
|
|
281
|
+
if (items.length === 0)
|
|
282
|
+
lines.push(" (暂无知识库)");
|
|
283
|
+
return { text: lines.join("\n") };
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
return { text: `获取失败: ${e.message}` };
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
// 6. /linkmind_terms <kb_id> [pattern] — 查看/管理术语词典
|
|
291
|
+
api.registerCommand({
|
|
292
|
+
name: "linkmind_terms",
|
|
293
|
+
description: "术语词典: /linkmind_terms <kb_id> [搜索词]",
|
|
294
|
+
acceptsArgs: true,
|
|
295
|
+
handler: async (ctx) => {
|
|
296
|
+
const args = (ctx.args ?? "").trim();
|
|
297
|
+
if (!args) {
|
|
298
|
+
return { text: "用法:\n /linkmind_terms <kb_id> — 查看所有术语映射\n /linkmind_terms <kb_id> 异响 — 搜索包含\"异响\"的映射\n\n术语词典在建索引时自动从知识图谱生成,也可通过 API 手动管理。" };
|
|
299
|
+
}
|
|
300
|
+
const spaceIdx = args.indexOf(" ");
|
|
301
|
+
const kbId = spaceIdx > 0 ? args.slice(0, spaceIdx).trim() : args;
|
|
302
|
+
const searchTerm = spaceIdx > 0 ? args.slice(spaceIdx + 1).trim() : "";
|
|
303
|
+
try {
|
|
304
|
+
const res = await client.get(`/knowledge_bases/${kbId}/terminology?config_type=synonym`);
|
|
305
|
+
if (!res.success)
|
|
306
|
+
return { text: `查询失败: ${res.message}` };
|
|
307
|
+
let items = res.data?.items ?? [];
|
|
308
|
+
if (searchTerm) {
|
|
309
|
+
items = items.filter((i) => i.pattern.includes(searchTerm) || i.replacement.includes(searchTerm));
|
|
310
|
+
}
|
|
311
|
+
if (items.length === 0) {
|
|
312
|
+
return { text: searchTerm ? `未找到包含 "${searchTerm}" 的术语映射` : "该知识库暂无术语映射" };
|
|
313
|
+
}
|
|
314
|
+
// Group by pattern
|
|
315
|
+
const grouped = {};
|
|
316
|
+
for (const item of items) {
|
|
317
|
+
if (!grouped[item.pattern])
|
|
318
|
+
grouped[item.pattern] = [];
|
|
319
|
+
grouped[item.pattern].push(item.replacement);
|
|
320
|
+
}
|
|
321
|
+
const lines = [`术语词典(共 ${items.length} 条映射,${Object.keys(grouped).length} 个 pattern):`];
|
|
322
|
+
const entries = Object.entries(grouped).slice(0, 20);
|
|
323
|
+
for (const [pattern, replacements] of entries) {
|
|
324
|
+
lines.push(` "${pattern}" → ${replacements.join(", ")}`);
|
|
325
|
+
}
|
|
326
|
+
if (Object.keys(grouped).length > 20) {
|
|
327
|
+
lines.push(` ... 还有 ${Object.keys(grouped).length - 20} 个`);
|
|
328
|
+
}
|
|
329
|
+
return { text: lines.join("\n") };
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
return { text: `查询失败: ${e.message}` };
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
// 7. /linkmind_graph <kb_id> [entity_name] — 查看知识图谱
|
|
337
|
+
api.registerCommand({
|
|
338
|
+
name: "linkmind_graph",
|
|
339
|
+
description: "知识图谱: /linkmind_graph <kb_id> [实体名]",
|
|
340
|
+
acceptsArgs: true,
|
|
341
|
+
handler: async (ctx) => {
|
|
342
|
+
const args = (ctx.args ?? "").trim();
|
|
343
|
+
if (!args) {
|
|
344
|
+
return { text: "用法:\n /linkmind_graph <kb_id> — 查看图谱概览(实体/关系/FAQ映射统计)\n /linkmind_graph <kb_id> Z7T — 查看 Z7T 的关系和关联FAQ\n /linkmind_graph <kb_id> 止回阀 — 查看止回阀的关系和关联FAQ" };
|
|
345
|
+
}
|
|
346
|
+
const spaceIdx = args.indexOf(" ");
|
|
347
|
+
const kbId = spaceIdx > 0 ? args.slice(0, spaceIdx).trim() : args;
|
|
348
|
+
const entityName = spaceIdx > 0 ? args.slice(spaceIdx + 1).trim() : "";
|
|
349
|
+
try {
|
|
350
|
+
if (!entityName) {
|
|
351
|
+
// Show graph stats overview
|
|
352
|
+
const res = await client.get(`/knowledge_bases/${kbId}/graph/stats`);
|
|
353
|
+
if (!res.success)
|
|
354
|
+
return { text: `查询失败: ${res.message}` };
|
|
355
|
+
const d = res.data ?? {};
|
|
356
|
+
const lines = [`知识图谱概览:`];
|
|
357
|
+
lines.push(` 实体总数: ${d.total_entities ?? 0}`);
|
|
358
|
+
lines.push(` 关系总数: ${d.total_relations ?? 0}`);
|
|
359
|
+
lines.push(` FAQ-实体映射: ${d.faq_entity_mappings ?? 0}`);
|
|
360
|
+
lines.push(` 已映射FAQ: ${d.mapped_faqs ?? 0}`);
|
|
361
|
+
lines.push(` 已映射实体: ${d.mapped_entities ?? 0}`);
|
|
362
|
+
lines.push(`\n实体类型分布:`);
|
|
363
|
+
for (const [t, c] of Object.entries(d.entity_distribution ?? {})) {
|
|
364
|
+
lines.push(` ${t}: ${c}`);
|
|
365
|
+
}
|
|
366
|
+
lines.push(`\n关系类型分布:`);
|
|
367
|
+
for (const [t, c] of Object.entries(d.relation_distribution ?? {})) {
|
|
368
|
+
lines.push(` ${t}: ${c}`);
|
|
369
|
+
}
|
|
370
|
+
lines.push("\n使用 /linkmind_graph <kb_id> <实体名> 查看该实体的关系和关联FAQ");
|
|
371
|
+
return { text: lines.join("\n") };
|
|
372
|
+
}
|
|
373
|
+
// Show relations for specific entity
|
|
374
|
+
const res = await client.get(`/knowledge_bases/${kbId}/graph/relations?entity=${encodeURIComponent(entityName)}`);
|
|
375
|
+
if (!res.success)
|
|
376
|
+
return { text: `查询失败: ${res.message}` };
|
|
377
|
+
const items = res.data?.items ?? [];
|
|
378
|
+
if (items.length === 0) {
|
|
379
|
+
return { text: `未找到与 "${entityName}" 相关的关系` };
|
|
382
380
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
381
|
+
const lines = [`"${entityName}" 的知识图谱关系(${items.length} 条):`];
|
|
382
|
+
// Group by predicate
|
|
383
|
+
const byPred = {};
|
|
384
|
+
for (const item of items) {
|
|
385
|
+
const pred = item.predicate;
|
|
386
|
+
if (!byPred[pred])
|
|
387
|
+
byPred[pred] = [];
|
|
388
|
+
byPred[pred].push(item);
|
|
389
|
+
}
|
|
390
|
+
for (const [pred, rels] of Object.entries(byPred)) {
|
|
391
|
+
lines.push(`\n [${pred}]`);
|
|
392
|
+
for (const r of rels.slice(0, 8)) {
|
|
393
|
+
if (r.subject === entityName) {
|
|
394
|
+
lines.push(` → ${r.object}`);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
lines.push(` ← ${r.subject}`);
|
|
398
|
+
}
|
|
388
399
|
}
|
|
389
|
-
|
|
400
|
+
if (rels.length > 8)
|
|
401
|
+
lines.push(` ... 还有 ${rels.length - 8} 条`);
|
|
390
402
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
403
|
+
// Try to find linked FAQs via entity search
|
|
404
|
+
try {
|
|
405
|
+
const entRes = await client.get(`/knowledge_bases/${kbId}/graph/entities?page=1&size=200`);
|
|
406
|
+
if (entRes.success) {
|
|
407
|
+
const matchedEnt = (entRes.data?.entities ?? []).find((e) => e.name === entityName || (e.synonyms ?? []).includes(entityName));
|
|
408
|
+
if (matchedEnt) {
|
|
409
|
+
const faqRes = await client.get(`/knowledge_bases/${kbId}/graph/faq_links/${matchedEnt.id}`);
|
|
410
|
+
if (faqRes.success && faqRes.data?.linked_faqs?.length) {
|
|
411
|
+
lines.push(`\n关联FAQ(${faqRes.data.total} 条):`);
|
|
412
|
+
for (const f of faqRes.data.linked_faqs.slice(0, 5)) {
|
|
413
|
+
lines.push(` Q: ${f.question.slice(0, 60)} (${f.mention_location})`);
|
|
414
|
+
}
|
|
415
|
+
if (faqRes.data.total > 5)
|
|
416
|
+
lines.push(` ... 还有 ${faqRes.data.total - 5} 条`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
400
420
|
}
|
|
421
|
+
catch (_) { }
|
|
401
422
|
return { text: lines.join("\n") };
|
|
402
423
|
}
|
|
403
424
|
catch (e) {
|
|
404
|
-
return { text:
|
|
425
|
+
return { text: `查询失败: ${e.message}` };
|
|
405
426
|
}
|
|
406
427
|
},
|
|
407
428
|
});
|
|
429
|
+
// 8. /linkmind_graph_edit <kb_id> <action> <args> — 编辑知识图谱
|
|
408
430
|
api.registerCommand({
|
|
409
|
-
name: "
|
|
410
|
-
description: "
|
|
431
|
+
name: "linkmind_graph_edit",
|
|
432
|
+
description: "编辑图谱: /linkmind_graph_edit <kb_id> <action> <args>",
|
|
411
433
|
acceptsArgs: true,
|
|
412
434
|
handler: async (ctx) => {
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
415
|
-
return { text: authErr };
|
|
416
|
-
const parts = (ctx.args ?? "").trim().split(/\s+/);
|
|
417
|
-
if (parts.length < 3) {
|
|
435
|
+
const args = (ctx.args ?? "").trim();
|
|
436
|
+
if (!args) {
|
|
418
437
|
return {
|
|
419
438
|
text: [
|
|
420
|
-
"
|
|
439
|
+
"知识图谱编辑命令:",
|
|
440
|
+
"",
|
|
441
|
+
"添加实体:",
|
|
442
|
+
" /linkmind_graph_edit <kb_id> add_entity <名称> <类型>",
|
|
443
|
+
" 示例: /linkmind_graph_edit kb_xxx add_entity Z7T model",
|
|
421
444
|
"",
|
|
422
|
-
"
|
|
423
|
-
"
|
|
424
|
-
"
|
|
425
|
-
" revoke — 撤销权限",
|
|
445
|
+
"添加同义词:",
|
|
446
|
+
" /linkmind_graph_edit <kb_id> add_synonym <实体名> <同义词>",
|
|
447
|
+
" 示例: /linkmind_graph_edit kb_xxx add_synonym 隔烟屏 挡烟板",
|
|
426
448
|
"",
|
|
427
|
-
"
|
|
428
|
-
" /
|
|
429
|
-
" /
|
|
449
|
+
"添加关系:",
|
|
450
|
+
" /linkmind_graph_edit <kb_id> add_relation <主体> <关系> <客体>",
|
|
451
|
+
" 示例: /linkmind_graph_edit kb_xxx add_relation Z7T has_part 止回阀",
|
|
452
|
+
"",
|
|
453
|
+
"删除实体:",
|
|
454
|
+
" /linkmind_graph_edit <kb_id> del_entity <实体ID>",
|
|
455
|
+
"",
|
|
456
|
+
"删除关系:",
|
|
457
|
+
" /linkmind_graph_edit <kb_id> del_relation <关系ID>",
|
|
458
|
+
"",
|
|
459
|
+
"重建映射:",
|
|
460
|
+
" /linkmind_graph_edit <kb_id> rebuild",
|
|
461
|
+
"",
|
|
462
|
+
"查看/修改Schema:",
|
|
463
|
+
" /linkmind_graph_edit <kb_id> schema",
|
|
430
464
|
].join("\n"),
|
|
431
465
|
};
|
|
432
466
|
}
|
|
433
|
-
const
|
|
467
|
+
const parts = args.split(/\s+/);
|
|
468
|
+
if (parts.length < 2) {
|
|
469
|
+
return { text: "参数不足。用法: /linkmind_graph_edit <kb_id> <action> [args...]" };
|
|
470
|
+
}
|
|
471
|
+
const kbId = parts[0];
|
|
472
|
+
const action = parts[1].toLowerCase();
|
|
434
473
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
474
|
+
switch (action) {
|
|
475
|
+
case "add_entity": {
|
|
476
|
+
if (parts.length < 4)
|
|
477
|
+
return { text: "用法: /linkmind_graph_edit <kb_id> add_entity <名称> <类型>" };
|
|
478
|
+
const name = parts[2];
|
|
479
|
+
const entityType = parts[3];
|
|
480
|
+
const res = await client.post(`/knowledge_bases/${kbId}/graph/entities`, {
|
|
481
|
+
name,
|
|
482
|
+
entity_type: entityType,
|
|
483
|
+
synonyms: parts.length > 4 ? parts.slice(4) : [],
|
|
484
|
+
});
|
|
485
|
+
if (res.success) {
|
|
486
|
+
return { text: `✅ 实体已创建: ${name} (${entityType}) ID=${res.data?.id}\n级联更新已触发。` };
|
|
487
|
+
}
|
|
488
|
+
return { text: `❌ 创建失败: ${res.detail || res.message}` };
|
|
489
|
+
}
|
|
490
|
+
case "add_synonym": {
|
|
491
|
+
if (parts.length < 4)
|
|
492
|
+
return { text: "用法: /linkmind_graph_edit <kb_id> add_synonym <实体名> <同义词>" };
|
|
493
|
+
const entityName = parts[2];
|
|
494
|
+
const synonym = parts.slice(3).join(" ");
|
|
495
|
+
// Find entity by name
|
|
496
|
+
const searchRes = await client.get(`/knowledge_bases/${kbId}/graph/entities?search=${encodeURIComponent(entityName)}&size=50`);
|
|
497
|
+
if (!searchRes.success || !(searchRes.data?.entities?.length)) {
|
|
498
|
+
return { text: `未找到实体 "${entityName}"` };
|
|
499
|
+
}
|
|
500
|
+
const ent = searchRes.data.entities.find((e) => e.name === entityName);
|
|
501
|
+
if (!ent) {
|
|
502
|
+
return { text: `未找到精确匹配的实体 "${entityName}",已有: ${searchRes.data.entities.map((e) => e.name).join(", ")}` };
|
|
503
|
+
}
|
|
504
|
+
// Update synonyms
|
|
505
|
+
const existingSynonyms = ent.synonyms ?? [];
|
|
506
|
+
if (existingSynonyms.includes(synonym)) {
|
|
507
|
+
return { text: `"${synonym}" 已经是 "${entityName}" 的同义词` };
|
|
508
|
+
}
|
|
509
|
+
existingSynonyms.push(synonym);
|
|
510
|
+
const updateRes = await client.put(`/knowledge_bases/${kbId}/graph/entities/${ent.id}`, {
|
|
511
|
+
synonyms: existingSynonyms,
|
|
512
|
+
});
|
|
513
|
+
if (updateRes.success) {
|
|
514
|
+
return { text: `✅ 同义词已添加: "${entityName}" 的同义词现在包括 [${existingSynonyms.join(", ")}]\n级联更新已触发。` };
|
|
515
|
+
}
|
|
516
|
+
return { text: `❌ 更新失败: ${updateRes.detail || updateRes.message}` };
|
|
517
|
+
}
|
|
518
|
+
case "add_relation": {
|
|
519
|
+
if (parts.length < 5)
|
|
520
|
+
return { text: "用法: /linkmind_graph_edit <kb_id> add_relation <主体> <关系> <客体>" };
|
|
521
|
+
const subject = parts[2];
|
|
522
|
+
const predicate = parts[3];
|
|
523
|
+
const object = parts.slice(4).join(" ");
|
|
524
|
+
const res = await client.post(`/knowledge_bases/${kbId}/graph/relations`, {
|
|
525
|
+
subject,
|
|
526
|
+
predicate,
|
|
527
|
+
object,
|
|
528
|
+
});
|
|
529
|
+
if (res.success) {
|
|
530
|
+
return { text: `✅ 关系已创建: ${subject} --[${predicate}]--> ${object} ID=${res.data?.id}\n级联更新已触发。` };
|
|
531
|
+
}
|
|
532
|
+
return { text: `❌ 创建失败: ${res.detail || res.message}` };
|
|
533
|
+
}
|
|
534
|
+
case "del_entity": {
|
|
535
|
+
if (parts.length < 3)
|
|
536
|
+
return { text: "用法: /linkmind_graph_edit <kb_id> del_entity <实体ID>" };
|
|
537
|
+
const entityId = parts[2];
|
|
538
|
+
const res = await client.del(`/knowledge_bases/${kbId}/graph/entities/${entityId}`);
|
|
539
|
+
if (res.success) {
|
|
540
|
+
return { text: `✅ 实体 ${entityId} 已删除(关联关系和映射也已清理)` };
|
|
541
|
+
}
|
|
542
|
+
return { text: `❌ 删除失败: ${res.detail || res.message}` };
|
|
543
|
+
}
|
|
544
|
+
case "del_relation": {
|
|
545
|
+
if (parts.length < 3)
|
|
546
|
+
return { text: "用法: /linkmind_graph_edit <kb_id> del_relation <关系ID>" };
|
|
547
|
+
const relId = parts[2];
|
|
548
|
+
const res = await client.del(`/knowledge_bases/${kbId}/graph/relations/${relId}`);
|
|
549
|
+
if (res.success) {
|
|
550
|
+
return { text: `✅ 关系 ${relId} 已删除` };
|
|
551
|
+
}
|
|
552
|
+
return { text: `❌ 删除失败: ${res.detail || res.message}` };
|
|
553
|
+
}
|
|
554
|
+
case "rebuild": {
|
|
555
|
+
const res = await client.post(`/knowledge_bases/${kbId}/graph/rebuild_links`);
|
|
556
|
+
if (res.success) {
|
|
557
|
+
const d = res.data ?? {};
|
|
558
|
+
return { text: `✅ FAQ-实体映射已重建\n 映射总数: ${d.total_mappings ?? "?"}\n 涉及FAQ: ${d.mapped_faqs ?? "?"}\n 涉及实体: ${d.mapped_entities ?? "?"}` };
|
|
559
|
+
}
|
|
560
|
+
return { text: `❌ 重建失败: ${res.detail || res.message}` };
|
|
561
|
+
}
|
|
562
|
+
case "schema": {
|
|
563
|
+
const res = await client.get(`/knowledge_bases/${kbId}/graph/schema`);
|
|
564
|
+
if (!res.success)
|
|
565
|
+
return { text: `查询失败: ${res.message}` };
|
|
566
|
+
const s = res.data;
|
|
567
|
+
if (!s)
|
|
568
|
+
return { text: "该知识库尚未配置图谱Schema(使用默认家电模板)" };
|
|
569
|
+
const lines = [`图谱Schema (${s.template_name ?? "custom"}):`];
|
|
570
|
+
lines.push(`\n实体类型:`);
|
|
571
|
+
for (const et of (s.entity_types ?? [])) {
|
|
572
|
+
const ex = et.examples?.length ? ` (例: ${et.examples.join(", ")})` : "";
|
|
573
|
+
lines.push(` - ${et.type}: ${et.description}${ex}`);
|
|
574
|
+
}
|
|
575
|
+
lines.push(`\n关系类型:`);
|
|
576
|
+
for (const rt of (s.relation_types ?? [])) {
|
|
577
|
+
lines.push(` - ${rt.predicate}: ${rt.description}`);
|
|
578
|
+
}
|
|
579
|
+
if (s.model_pattern_regex) {
|
|
580
|
+
lines.push(`\n型号正则: ${s.model_pattern_regex}`);
|
|
581
|
+
}
|
|
582
|
+
return { text: lines.join("\n") };
|
|
583
|
+
}
|
|
584
|
+
default:
|
|
585
|
+
return { text: `未知操作: ${action}\n支持的操作: add_entity, add_synonym, add_relation, del_entity, del_relation, rebuild, schema` };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
return { text: `操作失败: ${e.message}` };
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
// 9. /linkmind_term <kb_id> [action] [args] — 术语词典管理(可编辑)
|
|
594
|
+
api.registerCommand({
|
|
595
|
+
name: "linkmind_term",
|
|
596
|
+
description: "术语词典管理: /linkmind_term <kb_id> [action]",
|
|
597
|
+
acceptsArgs: true,
|
|
598
|
+
handler: async (ctx) => {
|
|
599
|
+
const args = (ctx.args ?? "").trim();
|
|
600
|
+
if (!args) {
|
|
601
|
+
return {
|
|
602
|
+
text: [
|
|
603
|
+
"术语词典管理:",
|
|
604
|
+
"",
|
|
605
|
+
"查看词典:",
|
|
606
|
+
" /linkmind_term <kb_id>",
|
|
607
|
+
"",
|
|
608
|
+
"添加单条:",
|
|
609
|
+
" /linkmind_term <kb_id> add <口语> <术语>",
|
|
610
|
+
" 示例: /linkmind_term kb_xxx add 板子 导烟板",
|
|
611
|
+
"",
|
|
612
|
+
"批量添加 (空格分隔, =连接):",
|
|
613
|
+
" /linkmind_term <kb_id> add_batch 板子=导烟板 盖子=油杯盖",
|
|
614
|
+
"",
|
|
615
|
+
"搜索:",
|
|
616
|
+
" /linkmind_term <kb_id> search <关键词>",
|
|
617
|
+
"",
|
|
618
|
+
"删除:",
|
|
619
|
+
" /linkmind_term <kb_id> del <口语>",
|
|
620
|
+
"",
|
|
621
|
+
"导出:",
|
|
622
|
+
" /linkmind_term <kb_id> export",
|
|
623
|
+
].join("\n"),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const parts = args.split(/\s+/);
|
|
627
|
+
const kbId = parts[0];
|
|
628
|
+
const action = (parts[1] || "").toLowerCase();
|
|
629
|
+
try {
|
|
630
|
+
if (!action || action === "list") {
|
|
631
|
+
const res = await client.get(`/knowledge_bases/${kbId}/terminology/export?format=json`);
|
|
632
|
+
if (!res.success)
|
|
633
|
+
return { text: `查询失败: ${res.message}` };
|
|
634
|
+
const items = res.data?.items ?? [];
|
|
635
|
+
if (items.length === 0)
|
|
636
|
+
return { text: `知识库 ${kbId} 暂无术语词典` };
|
|
637
|
+
const lines = [`术语词典 (共 ${items.length} 条):`];
|
|
638
|
+
for (const item of items.slice(0, 15)) {
|
|
639
|
+
lines.push(` ${item.pattern} → ${item.replacement}`);
|
|
640
|
+
}
|
|
641
|
+
if (items.length > 15)
|
|
642
|
+
lines.push(` ... 还有 ${items.length - 15} 条`);
|
|
643
|
+
return { text: lines.join("\n") };
|
|
444
644
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
645
|
+
if (action === "add") {
|
|
646
|
+
if (parts.length < 4)
|
|
647
|
+
return { text: "用法: /linkmind_term <kb_id> add <口语> <术语>" };
|
|
648
|
+
const pattern = parts[2];
|
|
649
|
+
const replacement = parts.slice(3).join(" ");
|
|
650
|
+
const res = await client.post(`/knowledge_bases/${kbId}/terminology/batch`, {
|
|
651
|
+
items: [{ pattern, replacement }],
|
|
652
|
+
});
|
|
653
|
+
if (res.success) {
|
|
654
|
+
return { text: `✅ 已添加: ${pattern} → ${replacement}` };
|
|
655
|
+
}
|
|
656
|
+
return { text: `❌ 添加失败: ${res.detail || res.message}` };
|
|
449
657
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
658
|
+
if (action === "add_batch") {
|
|
659
|
+
const batchStr = parts.slice(2).join(" ");
|
|
660
|
+
const pairs = batchStr.split(/\s+/);
|
|
661
|
+
const items = [];
|
|
662
|
+
for (const pair of pairs) {
|
|
663
|
+
const eqIdx = pair.indexOf("=");
|
|
664
|
+
if (eqIdx > 0) {
|
|
665
|
+
const p = pair.slice(0, eqIdx).trim();
|
|
666
|
+
const r = pair.slice(eqIdx + 1).trim();
|
|
667
|
+
if (p && r)
|
|
668
|
+
items.push({ pattern: p, replacement: r });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (items.length === 0)
|
|
672
|
+
return { text: "格式: /linkmind_term <kb_id> add_batch 口语1=术语1 口语2=术语2" };
|
|
673
|
+
const res = await client.post(`/knowledge_bases/${kbId}/terminology/batch`, { items });
|
|
674
|
+
if (res.success) {
|
|
675
|
+
const d = res.data ?? {};
|
|
676
|
+
return { text: `✅ 批量导入: 新增 ${d.imported ?? 0}, 更新 ${d.updated ?? 0}` };
|
|
677
|
+
}
|
|
678
|
+
return { text: `❌ 导入失败: ${res.detail || res.message}` };
|
|
679
|
+
}
|
|
680
|
+
if (action === "search") {
|
|
681
|
+
if (parts.length < 3)
|
|
682
|
+
return { text: "用法: /linkmind_term <kb_id> search <关键词>" };
|
|
683
|
+
const keyword = parts.slice(2).join(" ");
|
|
684
|
+
const res = await client.get(`/knowledge_bases/${kbId}/terminology/export?format=json`);
|
|
685
|
+
if (!res.success)
|
|
686
|
+
return { text: `查询失败: ${res.message}` };
|
|
687
|
+
const items = (res.data?.items ?? []).filter((i) => i.pattern.includes(keyword) || i.replacement.includes(keyword));
|
|
688
|
+
if (items.length === 0)
|
|
689
|
+
return { text: `未找到包含 "${keyword}" 的术语` };
|
|
690
|
+
const lines = [`搜索结果 (${items.length} 条):`];
|
|
691
|
+
for (const item of items.slice(0, 20)) {
|
|
692
|
+
lines.push(` ${item.pattern} → ${item.replacement}`);
|
|
693
|
+
}
|
|
694
|
+
return { text: lines.join("\n") };
|
|
695
|
+
}
|
|
696
|
+
if (action === "del") {
|
|
697
|
+
if (parts.length < 3)
|
|
698
|
+
return { text: "用法: /linkmind_term <kb_id> del <口语>" };
|
|
699
|
+
const pattern = parts.slice(2).join(" ");
|
|
700
|
+
const res = await client.del(`/knowledge_bases/${kbId}/terminology/batch`, {
|
|
701
|
+
patterns: [pattern],
|
|
702
|
+
});
|
|
703
|
+
if (res.success) {
|
|
704
|
+
return { text: `✅ 已删除: ${pattern} (${res.data?.deleted ?? 0} 条)` };
|
|
705
|
+
}
|
|
706
|
+
return { text: `❌ 删除失败: ${res.detail || res.message}` };
|
|
707
|
+
}
|
|
708
|
+
if (action === "export") {
|
|
709
|
+
const res = await client.get(`/knowledge_bases/${kbId}/terminology/export?format=csv`);
|
|
710
|
+
if (!res.success)
|
|
711
|
+
return { text: `导出失败: ${res.message}` };
|
|
712
|
+
const csv = res.data?.csv ?? "";
|
|
713
|
+
const truncated = csv.length > 3000 ? csv.slice(0, 3000) + "\n... (truncated)" : csv;
|
|
714
|
+
return { text: `术语词典CSV (${res.data?.total ?? 0} 条):\n${truncated}` };
|
|
715
|
+
}
|
|
716
|
+
return { text: `未知操作: ${action}\n支持: list, add, add_batch, search, del, export` };
|
|
457
717
|
}
|
|
458
718
|
catch (e) {
|
|
459
|
-
return { text:
|
|
719
|
+
return { text: `操作失败: ${e.message}` };
|
|
460
720
|
}
|
|
461
721
|
},
|
|
462
722
|
});
|
|
723
|
+
// 10. /linkmind_graph_view <kb_id> <overview|unmapped|around>
|
|
463
724
|
api.registerCommand({
|
|
464
|
-
name: "
|
|
465
|
-
description: "
|
|
725
|
+
name: "linkmind_graph_view",
|
|
726
|
+
description: "图谱浏览: /linkmind_graph_view <kb_id> <overview|unmapped|around>",
|
|
727
|
+
acceptsArgs: true,
|
|
466
728
|
handler: async (ctx) => {
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
return {
|
|
729
|
+
const args = (ctx.args ?? "").trim();
|
|
730
|
+
if (!args) {
|
|
731
|
+
return {
|
|
732
|
+
text: [
|
|
733
|
+
"图谱浏览:",
|
|
734
|
+
" /linkmind_graph_view <kb_id> overview — 全景摘要+Top实体",
|
|
735
|
+
" /linkmind_graph_view <kb_id> unmapped — 未映射FAQ列表",
|
|
736
|
+
" /linkmind_graph_view <kb_id> around <实体名> — 一跳邻居",
|
|
737
|
+
].join("\n"),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const parts = args.split(/\s+/);
|
|
741
|
+
const kbId = parts[0];
|
|
742
|
+
const action = (parts[1] || "overview").toLowerCase();
|
|
470
743
|
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
lines.push(
|
|
744
|
+
if (action === "overview") {
|
|
745
|
+
const res = await client.get(`/knowledge_bases/${kbId}/graph/stats`);
|
|
746
|
+
if (!res.success)
|
|
747
|
+
return { text: `查询失败: ${res.message}` };
|
|
748
|
+
const d = res.data ?? {};
|
|
749
|
+
const lines = ["图谱全景摘要:"];
|
|
750
|
+
lines.push(` 实体: ${d.total_entities ?? 0} 关系: ${d.total_relations ?? 0}`);
|
|
751
|
+
lines.push(` FAQ映射: ${d.faq_entity_mappings ?? 0} (覆盖 ${d.mapped_faqs ?? 0} FAQs, ${d.mapped_entities ?? 0} 实体)`);
|
|
752
|
+
lines.push("\n类型分布:");
|
|
753
|
+
for (const [t, c] of Object.entries(d.entity_distribution ?? {})) {
|
|
754
|
+
lines.push(` ${t}: ${c}`);
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
const fullRes = await client.get(`/knowledge_bases/${kbId}/graph/full?max_nodes=500`);
|
|
758
|
+
if (fullRes.success) {
|
|
759
|
+
const sorted = (fullRes.data?.nodes ?? [])
|
|
760
|
+
.sort((a, b) => (b.faq_count ?? 0) - (a.faq_count ?? 0))
|
|
761
|
+
.slice(0, 10);
|
|
762
|
+
if (sorted.length) {
|
|
763
|
+
lines.push("\nTop 10 (关联FAQ最多):");
|
|
764
|
+
for (const n of sorted) {
|
|
765
|
+
const syn = n.synonyms?.length ? ` (${n.synonyms.slice(0, 3).join(",")})` : "";
|
|
766
|
+
lines.push(` ${n.name} [${n.type}] — ${n.faq_count} FAQs${syn}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (_) { }
|
|
772
|
+
return { text: lines.join("\n") };
|
|
480
773
|
}
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
774
|
+
if (action === "unmapped") {
|
|
775
|
+
const res = await client.get(`/knowledge_bases/${kbId}/graph/unmapped_faqs?page=1&size=10`);
|
|
776
|
+
if (!res.success)
|
|
777
|
+
return { text: `查询失败: ${res.message}` };
|
|
778
|
+
const d = res.data ?? {};
|
|
779
|
+
if (d.total === 0)
|
|
780
|
+
return { text: "所有FAQ都已关联到实体" };
|
|
781
|
+
const lines = [`未映射FAQ (共 ${d.total} 条):`];
|
|
782
|
+
for (const f of (d.unmapped_faqs ?? [])) {
|
|
783
|
+
lines.push(` Q: ${f.question.slice(0, 60)}`);
|
|
784
|
+
lines.push(` ID: ${f.faq_id}`);
|
|
785
|
+
}
|
|
786
|
+
return { text: lines.join("\n") };
|
|
787
|
+
}
|
|
788
|
+
if (action === "around") {
|
|
789
|
+
if (parts.length < 3)
|
|
790
|
+
return { text: "用法: /linkmind_graph_view <kb_id> around <实体名>" };
|
|
791
|
+
const entityName = parts.slice(2).join(" ");
|
|
792
|
+
const entRes = await client.get(`/knowledge_bases/${kbId}/graph/entities?search=${encodeURIComponent(entityName)}&size=50`);
|
|
793
|
+
if (!entRes.success)
|
|
794
|
+
return { text: `查询失败` };
|
|
795
|
+
const matched = (entRes.data?.entities ?? []).find((e) => e.name === entityName || (e.synonyms ?? []).includes(entityName));
|
|
796
|
+
if (!matched)
|
|
797
|
+
return { text: `未找到实体: ${entityName}` };
|
|
798
|
+
const nbRes = await client.get(`/knowledge_bases/${kbId}/graph/neighborhood/${matched.id}?hops=1`);
|
|
799
|
+
if (!nbRes.success)
|
|
800
|
+
return { text: `查询失败` };
|
|
801
|
+
const nb = nbRes.data ?? {};
|
|
802
|
+
const lines = [`"${entityName}" 的邻居 (${nb.nodes?.length ?? 0} 节点, ${nb.edges?.length ?? 0} 边):`];
|
|
803
|
+
const byPred = {};
|
|
804
|
+
for (const e of (nb.edges ?? [])) {
|
|
805
|
+
if (!byPred[e.predicate])
|
|
806
|
+
byPred[e.predicate] = [];
|
|
807
|
+
byPred[e.predicate].push(e);
|
|
808
|
+
}
|
|
809
|
+
for (const [pred, rels] of Object.entries(byPred)) {
|
|
810
|
+
lines.push(`\n [${pred}]`);
|
|
811
|
+
for (const r of rels.slice(0, 5)) {
|
|
812
|
+
if (r.source === entityName)
|
|
813
|
+
lines.push(` → ${r.target}`);
|
|
814
|
+
else
|
|
815
|
+
lines.push(` ← ${r.source}`);
|
|
816
|
+
}
|
|
817
|
+
if (rels.length > 5)
|
|
818
|
+
lines.push(` ... +${rels.length - 5}`);
|
|
819
|
+
}
|
|
820
|
+
if (nb.linked_faqs?.length) {
|
|
821
|
+
lines.push(`\n关联FAQ (${nb.linked_faqs.length} 条):`);
|
|
822
|
+
for (const f of nb.linked_faqs.slice(0, 5)) {
|
|
823
|
+
lines.push(` Q: ${f.question.slice(0, 50)}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return { text: lines.join("\n") };
|
|
827
|
+
}
|
|
828
|
+
return { text: `未知: ${action}\n支持: overview, unmapped, around` };
|
|
485
829
|
}
|
|
486
830
|
catch (e) {
|
|
487
|
-
return { text:
|
|
831
|
+
return { text: `失败: ${e.message}` };
|
|
488
832
|
}
|
|
489
833
|
},
|
|
490
834
|
});
|