@linkmind_claw/openclaw-faq-kb 3.0.0 → 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 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +653 -388
- 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 +143 -47
- 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/linkmind_claw-openclaw-faq-kb-2.0.3.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw Plugin —
|
|
2
|
+
* OpenClaw Plugin entry — FAQ Knowledge Base
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 5 tools + 5 slash commands:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* /linkmind_create — 创建知识库并训练(Admin / Editor)
|
|
12
|
-
* /linkmind_share — 管理 KB 权限与可见性(Admin / Editor)
|
|
13
|
-
* /linkmind_query — 知识库问答
|
|
14
|
-
* /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
|
|
15
11
|
*/
|
|
16
12
|
import { Type } from "@sinclair/typebox";
|
|
17
13
|
import { FaqKbApiClient } from "./lib/api-client.js";
|
|
@@ -25,168 +21,81 @@ function definePluginEntry(entry) {
|
|
|
25
21
|
// ── Export ─────────────────────────────────────────────────────────────
|
|
26
22
|
export default definePluginEntry({
|
|
27
23
|
id: "faq-knowledge-base",
|
|
28
|
-
name: "
|
|
29
|
-
description: "
|
|
24
|
+
name: "FAQ Knowledge Base",
|
|
25
|
+
description: "FAQ 知识库插件:社媒用户自动注册、权限管理、一键建库训练、智能问答",
|
|
30
26
|
register(api) {
|
|
31
27
|
const cfg = (api.pluginConfig ?? {});
|
|
32
28
|
const baseUrl = cfg.apiBaseUrl || "http://67.212.146.21:8999";
|
|
33
29
|
const pollInterval = cfg.pollIntervalMs ?? 8_000;
|
|
34
30
|
const pipelineTimeout = cfg.pipelineTimeoutMs ?? 20 * 60 * 1000;
|
|
31
|
+
const defaultBotId = cfg.defaultBotId ?? "default";
|
|
35
32
|
const client = new FaqKbApiClient({
|
|
36
33
|
baseUrl,
|
|
37
34
|
staticToken: cfg.apiToken || undefined,
|
|
38
35
|
});
|
|
39
|
-
|
|
40
|
-
async function getOwnerStatus() {
|
|
41
|
-
if (cachedOwnerStatus && cachedOwnerStatus.claimed && Date.now() - cachedOwnerStatus.checkedAt < 60_000) {
|
|
42
|
-
return { claimed: true, owner_telegram_id: null };
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
const res = await client.get("/owner/status");
|
|
46
|
-
const data = res.data ?? res;
|
|
47
|
-
cachedOwnerStatus = { claimed: !!data.claimed, checkedAt: Date.now() };
|
|
48
|
-
return data;
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return { claimed: false, owner_telegram_id: null };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function ensureAuth(ctx) {
|
|
55
|
-
if (!ctx.senderId || !ctx.channel) {
|
|
56
|
-
return "无法获取你的用户信息,请在社媒渠道中使用。";
|
|
57
|
-
}
|
|
58
|
-
const status = await getOwnerStatus();
|
|
59
|
-
if (!status.claimed) {
|
|
60
|
-
const secret = status.claim_secret;
|
|
61
|
-
if (secret) {
|
|
62
|
-
return [
|
|
63
|
-
"🎉 欢迎使用 LinkMind!此服务器尚未绑定管理员。",
|
|
64
|
-
"你是第一个使用此 Bot 的用户,可以直接激活管理员身份!",
|
|
65
|
-
"",
|
|
66
|
-
"请发送以下命令完成绑定:",
|
|
67
|
-
` /linkmind_claim ${secret}`,
|
|
68
|
-
"",
|
|
69
|
-
"绑定后你将拥有完整的知识库管理权限。",
|
|
70
|
-
].join("\n");
|
|
71
|
-
}
|
|
72
|
-
return [
|
|
73
|
-
"此服务器尚未绑定管理员。",
|
|
74
|
-
"如果你是部署者,请使用以下命令完成绑定:",
|
|
75
|
-
" /linkmind_claim <部署时生成的密钥>",
|
|
76
|
-
"密钥可在服务器启动日志中找到。",
|
|
77
|
-
].join("\n");
|
|
78
|
-
}
|
|
79
|
-
client.setSender(ctx.senderId);
|
|
80
|
-
if (client.hasUserToken(ctx.senderId)) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
await client.oauthLogin({
|
|
85
|
-
channel_type: ctx.channel,
|
|
86
|
-
channel_user_id: ctx.senderId,
|
|
87
|
-
display_name: "",
|
|
88
|
-
});
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
catch (e) {
|
|
92
|
-
return `⚠️ 自动登录失败: ${e.message}`;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
async function requireAdmin(ctx) {
|
|
96
|
-
const authErr = await ensureAuth(ctx);
|
|
97
|
-
if (authErr)
|
|
98
|
-
return authErr;
|
|
99
|
-
try {
|
|
100
|
-
const res = await client.get("/oauth/me");
|
|
101
|
-
const data = res.data ?? res;
|
|
102
|
-
if (!data.is_admin) {
|
|
103
|
-
const status = await getOwnerStatus();
|
|
104
|
-
if (!status.claimed && status.claim_secret) {
|
|
105
|
-
return [
|
|
106
|
-
"此服务器尚未绑定管理员。",
|
|
107
|
-
"请先完成绑定:",
|
|
108
|
-
` /linkmind_claim ${status.claim_secret}`,
|
|
109
|
-
].join("\n");
|
|
110
|
-
}
|
|
111
|
-
return "此操作仅限 Admin。";
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return "无法验证身份,请稍后再试。";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async function requireEditor(ctx) {
|
|
120
|
-
const authErr = await ensureAuth(ctx);
|
|
121
|
-
if (authErr)
|
|
122
|
-
return authErr;
|
|
123
|
-
try {
|
|
124
|
-
const res = await client.get("/oauth/me");
|
|
125
|
-
const data = res.data ?? res;
|
|
126
|
-
if (!data.is_editor) {
|
|
127
|
-
const status = await getOwnerStatus();
|
|
128
|
-
if (!status.claimed && status.claim_secret) {
|
|
129
|
-
return [
|
|
130
|
-
"此服务器尚未绑定管理员。",
|
|
131
|
-
"请先完成绑定:",
|
|
132
|
-
` /linkmind_claim ${status.claim_secret}`,
|
|
133
|
-
].join("\n");
|
|
134
|
-
}
|
|
135
|
-
return "此操作需要管理员权限。如需查询知识库请使用 /linkmind_query";
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return "无法验证身份,请稍后再试。";
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// ── Tools ──────────────────────────────────────────────────────────
|
|
36
|
+
// ── Tool 1: Account Register ─────────────────────────────────────
|
|
144
37
|
api.registerTool({
|
|
145
|
-
name: "
|
|
146
|
-
label: "
|
|
38
|
+
name: "faq_account_register",
|
|
39
|
+
label: "社媒用户注册/登录",
|
|
147
40
|
description: [
|
|
148
|
-
"
|
|
149
|
-
"
|
|
41
|
+
"社媒渠道用户自动注册或登录。",
|
|
42
|
+
"首次调用为指定渠道用户创建账号,后续调用返回已有身份。",
|
|
150
43
|
"支持 Telegram / Discord / Slack / Web 等渠道。",
|
|
44
|
+
"新用户默认角色为 viewer(只能查询),Owner 可通过 faq_account_manage 提升权限。",
|
|
45
|
+
"注册后 API Key 自动缓存,后续所有操作使用该用户身份鉴权。",
|
|
151
46
|
].join(" "),
|
|
152
47
|
parameters: AccountRegisterParams,
|
|
153
48
|
execute: (_id, params) => executeAccountRegister(client, params),
|
|
154
49
|
});
|
|
50
|
+
// ── Tool 2: Account Manage ───────────────────────────────────────
|
|
155
51
|
api.registerTool({
|
|
156
|
-
name: "
|
|
157
|
-
label: "
|
|
52
|
+
name: "faq_account_manage",
|
|
53
|
+
label: "账号权限管理",
|
|
158
54
|
description: [
|
|
159
|
-
"
|
|
160
|
-
"grant_role
|
|
55
|
+
"管理社媒渠道用户的权限和角色。需要 admin 权限。",
|
|
56
|
+
"grant_role: 给指定用户授权角色和知识库访问范围;",
|
|
57
|
+
"list_users: 列出所有已注册的渠道用户;",
|
|
58
|
+
"my_info: 查看当前用户的身份、角色和配额信息。",
|
|
161
59
|
].join(" "),
|
|
162
60
|
parameters: AccountManageParams,
|
|
163
61
|
execute: (_id, params) => executeAccountManage(client, params),
|
|
164
62
|
});
|
|
63
|
+
// ── Tool 3: Create & Train ───────────────────────────────────────
|
|
165
64
|
api.registerTool({
|
|
166
|
-
name: "
|
|
65
|
+
name: "faq_kb_create_and_train",
|
|
167
66
|
label: "创建知识库并训练",
|
|
168
67
|
description: [
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
68
|
+
"创建一个新的 FAQ 知识库并自动完成全部训练流程。需要 editor 以上权限。",
|
|
69
|
+
"内部执行: 创建知识库 → 上传文档 → AI 提取 FAQ → 构建知识树 → 建索引。",
|
|
70
|
+
"训练完成后知识库即可用于问答(faq_kb_query)。",
|
|
71
|
+
"支持 PDF / DOCX / DOC / TXT / MD 格式,通过 file_url 传入文件地址。",
|
|
72
|
+
"训练过程通常需要 2-10 分钟,取决于文档大小。",
|
|
172
73
|
].join(" "),
|
|
173
74
|
parameters: CreateAndTrainParams,
|
|
174
75
|
execute: (_id, params) => executeCreateAndTrain(client, params, pollInterval, pipelineTimeout),
|
|
175
76
|
});
|
|
77
|
+
// ── Tool 4: Query ────────────────────────────────────────────────
|
|
176
78
|
api.registerTool({
|
|
177
|
-
name: "
|
|
79
|
+
name: "faq_kb_query",
|
|
178
80
|
label: "知识库问答",
|
|
179
81
|
description: [
|
|
180
|
-
"
|
|
181
|
-
"
|
|
82
|
+
"向指定知识库提问并获取 AI 智能回答。需要 viewer 以上权限。",
|
|
83
|
+
"知识库必须已经训练完成(is_active=true)。",
|
|
84
|
+
"设置 enable_polish=true 可让 LLM 综合多个 FAQ 生成更自然的回答。",
|
|
85
|
+
"返回包含答案、置信度、来源引用和日志 ID。",
|
|
182
86
|
].join(" "),
|
|
183
87
|
parameters: QueryParams,
|
|
184
88
|
execute: (_id, params) => executeQuery(client, params),
|
|
185
89
|
});
|
|
90
|
+
// ── Tool 5: List KB ──────────────────────────────────────────────
|
|
186
91
|
api.registerTool({
|
|
187
|
-
name: "
|
|
92
|
+
name: "faq_kb_list",
|
|
188
93
|
label: "查看知识库列表",
|
|
189
|
-
description:
|
|
94
|
+
description: [
|
|
95
|
+
"列出当前可访问的所有知识库。需要 viewer 以上权限。",
|
|
96
|
+
"返回每个知识库的 ID、名称、标签和训练状态。",
|
|
97
|
+
"支持按名称和标签筛选,支持分页。",
|
|
98
|
+
].join(" "),
|
|
190
99
|
parameters: Type.Object({
|
|
191
100
|
keyword: Type.Optional(Type.String({ description: "按名称搜索(可选)" })),
|
|
192
101
|
tag_name: Type.Optional(Type.String({ description: "按标签筛选(可选)" })),
|
|
@@ -206,195 +115,95 @@ export default definePluginEntry({
|
|
|
206
115
|
const total = res.total ?? items.length;
|
|
207
116
|
const lines = [`知识库列表(共 ${total} 个):`];
|
|
208
117
|
for (const kb of items) {
|
|
209
|
-
const
|
|
118
|
+
const s = kb.is_active ? "就绪" : "未训练";
|
|
210
119
|
const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
|
|
211
|
-
lines.push(` ${kb.id} ${kb.name}${tag} (${
|
|
120
|
+
lines.push(` ${kb.id} ${kb.name}${tag} (${s})`);
|
|
212
121
|
}
|
|
213
122
|
if (items.length === 0)
|
|
214
123
|
lines.push(" (无知识库)");
|
|
215
124
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
216
125
|
},
|
|
217
126
|
});
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
handler: async (ctx) => {
|
|
223
|
-
const status = await getOwnerStatus();
|
|
224
|
-
let userFlags = { is_admin: false, is_editor: false };
|
|
225
|
-
if (status.claimed && ctx.senderId && ctx.channel) {
|
|
226
|
-
client.setSender(ctx.senderId);
|
|
227
|
-
if (!client.hasUserToken(ctx.senderId)) {
|
|
228
|
-
try {
|
|
229
|
-
await client.oauthLogin({
|
|
230
|
-
channel_type: ctx.channel,
|
|
231
|
-
channel_user_id: ctx.senderId,
|
|
232
|
-
display_name: "",
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch { /* ignore */ }
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const me = await client.get("/oauth/me");
|
|
239
|
-
const d = me.data ?? me;
|
|
240
|
-
userFlags = { is_admin: !!d.is_admin, is_editor: !!d.is_editor };
|
|
241
|
-
}
|
|
242
|
-
catch { /* ignore */ }
|
|
243
|
-
}
|
|
244
|
-
const lines = [
|
|
245
|
-
"LinkMind 智能知识库",
|
|
246
|
-
"━━━━━━━━━━━━━━━━━━",
|
|
247
|
-
];
|
|
248
|
-
if (!status.claimed) {
|
|
249
|
-
const secret = status.claim_secret;
|
|
250
|
-
if (secret) {
|
|
251
|
-
lines.push("", "此服务器尚未绑定管理员,你可以直接激活!", "", "请发送以下命令:", ` /linkmind_claim ${secret}`, "", "绑定后即可创建和管理知识库。");
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
lines.push("", "首次使用?请先绑定管理员身份:", " /linkmind_claim <密钥>", "", "密钥会在首次对话时自动展示。");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else if (userFlags.is_admin) {
|
|
258
|
-
lines.push("", "你是 Admin,可用命令:", "", " /linkmind_role <用户ID> <editor|user>", " 设置用户角色(提升/降级)", "", " /linkmind_create <名称> <文档URL>", " 创建知识库并自动训练", "", " /linkmind_share <KB名> public|private", " 设置知识库公开/私有", " /linkmind_share <KB名> <用户ID> viewer|revoke", " 授予/撤销查看权限", "", " /linkmind_list — 查看所有知识库", " /linkmind_query <KB名> <问题> — 提问", " /linkmind_me — 查看身份信息");
|
|
259
|
-
}
|
|
260
|
-
else if (userFlags.is_editor) {
|
|
261
|
-
lines.push("", "你是管理员(Editor),可用命令:", "", " /linkmind_create <名称> <文档URL>", " 创建知识库并自动训练", "", " /linkmind_share <KB名> public|private", " 设置知识库公开/私有", " /linkmind_share <KB名> <用户ID> viewer|revoke", " 授予/撤销查看权限", "", " /linkmind_list — 查看所有知识库", " /linkmind_query <KB名> <问题> — 提问", " /linkmind_me — 查看身份信息");
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
lines.push("", "可用命令:", "", " /linkmind_list — 查看可访问的知识库", " /linkmind_query <KB名> <问题> — 向知识库提问", " /linkmind_me — 查看身份信息");
|
|
265
|
-
}
|
|
266
|
-
return { text: lines.join("\n") };
|
|
267
|
-
},
|
|
268
|
-
});
|
|
127
|
+
// ══════════════════════════════════════════════════════════════════
|
|
128
|
+
// Slash Commands — 5 个,和 5 个 Tools 一一对应
|
|
129
|
+
// ══════════════════════════════════════════════════════════════════
|
|
130
|
+
// 1. /faq_register [channel_user_id] — 注册/登录
|
|
269
131
|
api.registerCommand({
|
|
270
|
-
name: "
|
|
271
|
-
description: "
|
|
132
|
+
name: "faq_register",
|
|
133
|
+
description: "注册/登录: /faq_register 或 /faq_register <渠道用户ID>",
|
|
272
134
|
acceptsArgs: true,
|
|
273
135
|
handler: async (ctx) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
text: [
|
|
281
|
-
"用法: /linkmind_claim <密钥>",
|
|
282
|
-
"",
|
|
283
|
-
"密钥在首次对话时自动展示,或可在服务器启动日志中查看。",
|
|
284
|
-
"绑定后你将成为此服务器的管理员,拥有创建/管理知识库的权限。",
|
|
285
|
-
].join("\n"),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
client.setSender(ctx.senderId);
|
|
289
|
-
if (!client.hasUserToken(ctx.senderId)) {
|
|
290
|
-
try {
|
|
291
|
-
await client.oauthLogin({
|
|
292
|
-
channel_type: ctx.channel,
|
|
293
|
-
channel_user_id: ctx.senderId,
|
|
294
|
-
display_name: "",
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
catch (e) {
|
|
298
|
-
return { text: `⚠️ 登录失败: ${e.message}` };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
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";
|
|
301
141
|
try {
|
|
302
|
-
const res = await client.post("/
|
|
303
|
-
|
|
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",
|
|
304
148
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
catch (e) {
|
|
316
|
-
return { text: `❌ 绑定失败: ${e.message}` };
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
api.registerCommand({
|
|
321
|
-
name: "linkmind_me",
|
|
322
|
-
description: "查看我的身份信息",
|
|
323
|
-
handler: async (ctx) => {
|
|
324
|
-
const authErr = await ensureAuth(ctx);
|
|
325
|
-
if (authErr)
|
|
326
|
-
return { text: authErr };
|
|
327
|
-
try {
|
|
328
|
-
const res = await client.get("/oauth/me");
|
|
329
|
-
const d = res.data ?? res;
|
|
330
|
-
let roleLabel = d.role ?? "user";
|
|
331
|
-
if (d.is_admin)
|
|
332
|
-
roleLabel = "Admin";
|
|
333
|
-
else if (d.is_editor)
|
|
334
|
-
roleLabel = "Editor (管理员)";
|
|
335
|
-
else
|
|
336
|
-
roleLabel = "普通用户";
|
|
337
|
-
const lines = [
|
|
338
|
-
`✅ 你已登录`,
|
|
339
|
-
` 用户 ID: ${d.user_id ?? d.id ?? "—"}`,
|
|
340
|
-
` 名称: ${d.name ?? "—"}`,
|
|
341
|
-
` 权限等级: ${roleLabel}`,
|
|
342
|
-
` 渠道: ${d.channel_type ?? "—"}`,
|
|
343
|
-
];
|
|
344
|
-
return { text: lines.join("\n") };
|
|
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);
|
|
154
|
+
}
|
|
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 进行问答。` };
|
|
157
|
+
}
|
|
158
|
+
return { text: `欢迎回来!\n 用户: ${d.name}\n 角色: ${d.role}\n ID: ${d.user_id}` };
|
|
345
159
|
}
|
|
346
160
|
catch (e) {
|
|
347
|
-
return { text:
|
|
161
|
+
return { text: `注册失败: ${e.message}` };
|
|
348
162
|
}
|
|
349
163
|
},
|
|
350
164
|
});
|
|
165
|
+
// 2. /faq_grant <channel_user_id> <role> — 授权
|
|
351
166
|
api.registerCommand({
|
|
352
|
-
name: "
|
|
353
|
-
description: "
|
|
167
|
+
name: "faq_grant",
|
|
168
|
+
description: "授权: /faq_grant <渠道用户ID> <角色>",
|
|
354
169
|
acceptsArgs: true,
|
|
170
|
+
requireAuth: true,
|
|
355
171
|
handler: async (ctx) => {
|
|
356
|
-
const authErr = await requireAdmin(ctx);
|
|
357
|
-
if (authErr)
|
|
358
|
-
return { text: authErr };
|
|
359
172
|
const parts = (ctx.args ?? "").trim().split(/\s+/);
|
|
360
173
|
if (parts.length < 2) {
|
|
361
|
-
return {
|
|
362
|
-
text: [
|
|
363
|
-
"用法: /linkmind_role <用户ID> <角色>",
|
|
364
|
-
"",
|
|
365
|
-
"角色选项:",
|
|
366
|
-
" editor — 提升为管理员(可管理所有知识库)",
|
|
367
|
-
" user — 降为普通用户",
|
|
368
|
-
"",
|
|
369
|
-
"示例: /linkmind_role 1d52227f-6ac editor",
|
|
370
|
-
].join("\n"),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
const [targetUserId, role] = parts;
|
|
374
|
-
if (role !== "editor" && role !== "user") {
|
|
375
|
-
return { text: "角色只能是 editor 或 user" };
|
|
174
|
+
return { text: "用法: /faq_grant <渠道用户ID> <角色>\n角色: viewer / editor / reviewer / admin\n示例: /faq_grant 123456789 editor" };
|
|
376
175
|
}
|
|
176
|
+
const [targetId, role] = parts;
|
|
177
|
+
const channelType = ctx.channel.includes("telegram") ? "telegram"
|
|
178
|
+
: ctx.channel.includes("discord") ? "discord"
|
|
179
|
+
: "web";
|
|
377
180
|
try {
|
|
378
|
-
const res = await client.
|
|
379
|
-
|
|
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,
|
|
186
|
+
});
|
|
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}` };
|
|
380
191
|
}
|
|
381
192
|
catch (e) {
|
|
382
|
-
return { text:
|
|
193
|
+
return { text: `授权失败: ${e.message}` };
|
|
383
194
|
}
|
|
384
195
|
},
|
|
385
196
|
});
|
|
197
|
+
// 3. /faq_create <kb_name> <file_url>
|
|
386
198
|
api.registerCommand({
|
|
387
|
-
name: "
|
|
388
|
-
description: "创建知识库并训练: /
|
|
199
|
+
name: "faq_create",
|
|
200
|
+
description: "创建知识库并训练: /faq_create <名称> <文档URL>",
|
|
389
201
|
acceptsArgs: true,
|
|
390
202
|
handler: async (ctx) => {
|
|
391
|
-
const authErr = await requireEditor(ctx);
|
|
392
|
-
if (authErr)
|
|
393
|
-
return { text: authErr };
|
|
394
203
|
const args = (ctx.args ?? "").trim();
|
|
395
204
|
const spaceIdx = args.indexOf(" ");
|
|
396
205
|
if (!args || spaceIdx === -1) {
|
|
397
|
-
return { text: "用法: /
|
|
206
|
+
return { text: "用法: /faq_create <知识库名称> <文档URL>\n示例: /faq_create 客服知识库 https://example.com/doc.pdf\n支持 PDF/DOCX/DOC/TXT/MD" };
|
|
398
207
|
}
|
|
399
208
|
const kbName = args.slice(0, spaceIdx).trim();
|
|
400
209
|
const fileUrl = args.slice(spaceIdx + 1).trim();
|
|
@@ -410,160 +219,616 @@ export default definePluginEntry({
|
|
|
410
219
|
name: kbName, description: "", file_path: fileUrl,
|
|
411
220
|
});
|
|
412
221
|
if (!pipeRes.success) {
|
|
413
|
-
return { text:
|
|
222
|
+
return { text: `启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
|
|
414
223
|
}
|
|
415
|
-
return { text:
|
|
224
|
+
return { text: `知识库创建成功,训练已启动!\n 名称: ${kbName}\n KB ID: ${kbId}\n 任务 ID: ${pipeRes.data.task_id}\n\n训练需要 2-10 分钟,完成后可用 /faq_query 提问。` };
|
|
416
225
|
}
|
|
417
226
|
catch (e) {
|
|
418
|
-
return { text:
|
|
227
|
+
return { text: `创建失败: ${e.message}` };
|
|
419
228
|
}
|
|
420
229
|
},
|
|
421
230
|
});
|
|
231
|
+
// 4. /faq_query <kb_id> <question>
|
|
422
232
|
api.registerCommand({
|
|
423
|
-
name: "
|
|
424
|
-
description: "知识库问答: /
|
|
233
|
+
name: "faq_query",
|
|
234
|
+
description: "知识库问答: /faq_query <kb_id> <问题>",
|
|
425
235
|
acceptsArgs: true,
|
|
426
236
|
handler: async (ctx) => {
|
|
427
|
-
const authErr = await ensureAuth(ctx);
|
|
428
|
-
if (authErr)
|
|
429
|
-
return { text: authErr };
|
|
430
237
|
const args = (ctx.args ?? "").trim();
|
|
431
238
|
const spaceIdx = args.indexOf(" ");
|
|
432
239
|
if (!args || spaceIdx === -1) {
|
|
433
|
-
return { text: "用法: /
|
|
240
|
+
return { text: "用法: /faq_query <kb_id> <问题>\n示例: /faq_query abc123 什么是退款政策?" };
|
|
434
241
|
}
|
|
435
|
-
const
|
|
242
|
+
const kbId = args.slice(0, spaceIdx).trim();
|
|
436
243
|
const question = args.slice(spaceIdx + 1).trim();
|
|
437
244
|
if (!question)
|
|
438
245
|
return { text: "请提供问题" };
|
|
439
246
|
try {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
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)}%`);
|
|
445
258
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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(", ")}`);
|
|
450
325
|
}
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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}`);
|
|
456
369
|
}
|
|
457
|
-
|
|
370
|
+
lines.push("\n使用 /linkmind_graph <kb_id> <实体名> 查看该实体的关系和关联FAQ");
|
|
371
|
+
return { text: lines.join("\n") };
|
|
458
372
|
}
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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}" 相关的关系` };
|
|
380
|
+
}
|
|
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);
|
|
468
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
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (rels.length > 8)
|
|
401
|
+
lines.push(` ... 还有 ${rels.length - 8} 条`);
|
|
402
|
+
}
|
|
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
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (_) { }
|
|
469
422
|
return { text: lines.join("\n") };
|
|
470
423
|
}
|
|
471
424
|
catch (e) {
|
|
472
|
-
return { text:
|
|
425
|
+
return { text: `查询失败: ${e.message}` };
|
|
473
426
|
}
|
|
474
427
|
},
|
|
475
428
|
});
|
|
429
|
+
// 8. /linkmind_graph_edit <kb_id> <action> <args> — 编辑知识图谱
|
|
476
430
|
api.registerCommand({
|
|
477
|
-
name: "
|
|
478
|
-
description: "
|
|
431
|
+
name: "linkmind_graph_edit",
|
|
432
|
+
description: "编辑图谱: /linkmind_graph_edit <kb_id> <action> <args>",
|
|
479
433
|
acceptsArgs: true,
|
|
480
434
|
handler: async (ctx) => {
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
return {
|
|
484
|
-
|
|
435
|
+
const args = (ctx.args ?? "").trim();
|
|
436
|
+
if (!args) {
|
|
437
|
+
return {
|
|
438
|
+
text: [
|
|
439
|
+
"知识图谱编辑命令:",
|
|
440
|
+
"",
|
|
441
|
+
"添加实体:",
|
|
442
|
+
" /linkmind_graph_edit <kb_id> add_entity <名称> <类型>",
|
|
443
|
+
" 示例: /linkmind_graph_edit kb_xxx add_entity Z7T model",
|
|
444
|
+
"",
|
|
445
|
+
"添加同义词:",
|
|
446
|
+
" /linkmind_graph_edit <kb_id> add_synonym <实体名> <同义词>",
|
|
447
|
+
" 示例: /linkmind_graph_edit kb_xxx add_synonym 隔烟屏 挡烟板",
|
|
448
|
+
"",
|
|
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",
|
|
464
|
+
].join("\n"),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const parts = args.split(/\s+/);
|
|
485
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();
|
|
473
|
+
try {
|
|
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) {
|
|
486
601
|
return {
|
|
487
602
|
text: [
|
|
488
|
-
"
|
|
489
|
-
"
|
|
490
|
-
"
|
|
491
|
-
" /
|
|
492
|
-
"
|
|
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 板子=导烟板 盖子=油杯盖",
|
|
493
614
|
"",
|
|
494
|
-
"
|
|
495
|
-
" /
|
|
496
|
-
"
|
|
615
|
+
"搜索:",
|
|
616
|
+
" /linkmind_term <kb_id> search <关键词>",
|
|
617
|
+
"",
|
|
618
|
+
"删除:",
|
|
619
|
+
" /linkmind_term <kb_id> del <口语>",
|
|
620
|
+
"",
|
|
621
|
+
"导出:",
|
|
622
|
+
" /linkmind_term <kb_id> export",
|
|
497
623
|
].join("\n"),
|
|
498
624
|
};
|
|
499
625
|
}
|
|
500
|
-
const
|
|
626
|
+
const parts = args.split(/\s+/);
|
|
627
|
+
const kbId = parts[0];
|
|
628
|
+
const action = (parts[1] || "").toLowerCase();
|
|
501
629
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
if (
|
|
508
|
-
kbId
|
|
509
|
-
|
|
510
|
-
|
|
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") };
|
|
511
644
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
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}` };
|
|
517
657
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
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}` };
|
|
520
679
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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") };
|
|
525
695
|
}
|
|
526
|
-
if (action
|
|
527
|
-
|
|
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}` };
|
|
528
707
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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` };
|
|
536
717
|
}
|
|
537
718
|
catch (e) {
|
|
538
|
-
return { text:
|
|
719
|
+
return { text: `操作失败: ${e.message}` };
|
|
539
720
|
}
|
|
540
721
|
},
|
|
541
722
|
});
|
|
723
|
+
// 10. /linkmind_graph_view <kb_id> <overview|unmapped|around>
|
|
542
724
|
api.registerCommand({
|
|
543
|
-
name: "
|
|
544
|
-
description: "
|
|
725
|
+
name: "linkmind_graph_view",
|
|
726
|
+
description: "图谱浏览: /linkmind_graph_view <kb_id> <overview|unmapped|around>",
|
|
727
|
+
acceptsArgs: true,
|
|
545
728
|
handler: async (ctx) => {
|
|
546
|
-
const
|
|
547
|
-
if (
|
|
548
|
-
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();
|
|
549
743
|
try {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
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") };
|
|
559
773
|
}
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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` };
|
|
564
829
|
}
|
|
565
830
|
catch (e) {
|
|
566
|
-
return { text:
|
|
831
|
+
return { text: `失败: ${e.message}` };
|
|
567
832
|
}
|
|
568
833
|
},
|
|
569
834
|
});
|