@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.js CHANGED
@@ -1,18 +1,13 @@
1
1
  /**
2
- * OpenClaw Plugin — LinkMind Knowledge Base
2
+ * OpenClaw Plugin entry FAQ Knowledge Base
3
3
  *
4
- * 一个 OpenClaw 实例 = 一个管理员(Owner)
5
- * 管理员通过 /linkmind_claim 绑定身份,拥有全部管理权限
6
- * 其他用户只能查询被开放的知识库
4
+ * 5 tools + 5 slash commands:
7
5
  *
8
- * Slash Commands:
9
- * /linkmind_help 使用帮助
10
- * /linkmind_claim 绑定管理员身份
11
- * /linkmind_me 查看我的身份信息
12
- * /linkmind_create 创建知识库并训练(仅 Owner)
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: "LinkMind Knowledge Base",
30
- description: "LinkMind 智能知识库:管理员绑定、一键上传文档训练、智能问答",
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
- let cachedOwnerStatus = null;
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: "linkmind_account_register",
123
- label: "用户注册/登录",
38
+ name: "faq_account_register",
39
+ label: "社媒用户注册/登录",
124
40
  description: [
125
- "社媒用户 OAuth 注册或登录。",
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: "linkmind_account_manage",
134
- label: "权限管理",
52
+ name: "faq_account_manage",
53
+ label: "账号权限管理",
135
54
  description: [
136
- "管理员权限操作工具。",
137
- "grant_role / list_users / my_info",
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: "linkmind_kb_create_and_train",
65
+ name: "faq_kb_create_and_train",
144
66
  label: "创建知识库并训练",
145
67
  description: [
146
- "创建 FAQ 知识库并自动完成全部训练流程。",
147
- "支持 PDF / DOCX / DOC / TXT / MD 格式。",
148
- "训练过程通常需要 2-10 分钟。",
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: "linkmind_kb_query",
79
+ name: "faq_kb_query",
155
80
  label: "知识库问答",
156
81
  description: [
157
- "向知识库提问并获取 AI 智能回答。",
158
- "可以用知识库名称(kb_name)或 ID(kb_id)指定,推荐用名称。",
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: "linkmind_kb_list",
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 st = kb.is_active ? "就绪" : "未训练";
118
+ const s = kb.is_active ? "就绪" : "未训练";
187
119
  const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
188
- lines.push(` ${kb.id} ${kb.name}${tag} (${st})`);
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
- // ── Slash Commands ─────────────────────────────────────────────────
127
+ // ══════════════════════════════════════════════════════════════════
128
+ // Slash Commands — 5 个,和 5 个 Tools 一一对应
129
+ // ══════════════════════════════════════════════════════════════════
130
+ // 1. /faq_register [channel_user_id] — 注册/登录
196
131
  api.registerCommand({
197
- name: "linkmind_help",
198
- description: "LinkMind 使用帮助",
132
+ name: "faq_register",
133
+ description: "注册/登录: /faq_register 或 /faq_register <渠道用户ID>",
134
+ acceptsArgs: true,
199
135
  handler: async (ctx) => {
200
- const status = await getOwnerStatus();
201
- let isOwner = false;
202
- if (status.claimed && ctx.senderId && ctx.channel) {
203
- client.setSender(ctx.senderId);
204
- if (!client.hasUserToken(ctx.senderId)) {
205
- try {
206
- await client.oauthLogin({
207
- channel_type: ctx.channel,
208
- channel_user_id: ctx.senderId,
209
- display_name: "",
210
- });
211
- }
212
- catch { /* ignore */ }
213
- }
214
- try {
215
- const me = await client.get("/oauth/me");
216
- isOwner = !!(me.data ?? me).is_owner;
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
- else {
230
- lines.push("", "首次使用?请先绑定管理员身份:", " /linkmind_claim <密钥>", "", "密钥在服务器启动日志中生成。", "绑定后即可创建和管理知识库。");
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
- else if (isOwner) {
234
- lines.push("", "你是管理员,可用命令:", "", " /linkmind_create <名称> <文档URL>", " 创建知识库并自动训练", " 支持 PDF / DOCX / DOC / TXT / MD", "", " /linkmind_list", " 查看所有知识库", "", " /linkmind_query <知识库名称> <问题>", " 向知识库提问", "", " /linkmind_share <KB名> <用户ID> <角色>", " 分享知识库给其他用户", " 角色: viewer / editor / revoke", "", " /linkmind_me", " 查看你的身份信息");
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: "linkmind_claim",
244
- description: "绑定管理员身份: /linkmind_claim <密钥>",
167
+ name: "faq_grant",
168
+ description: "授权: /faq_grant <渠道用户ID> <角色>",
245
169
  acceptsArgs: true,
170
+ requireAuth: true,
246
171
  handler: async (ctx) => {
247
- if (!ctx.senderId || !ctx.channel) {
248
- return { text: "无法获取你的用户信息,请在社媒渠道中使用。" };
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("/owner/claim", {
276
- claim_secret: secret,
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
- cachedOwnerStatus = { claimed: true, checkedAt: Date.now() };
279
- return {
280
- text: [
281
- `✅ ${res.message || "绑定成功!"}`,
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: `❌ 获取信息失败: ${e.message}` };
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: "linkmind_create",
320
- description: "创建知识库并训练(仅管理员): /linkmind_create <名称> <文档URL>",
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: "用法: /linkmind_create <知识库名称> <文档URL>\n示例: /linkmind_create 客服知识库 https://example.com/doc.pdf\n支持 PDF/DOCX/DOC/TXT/MD" };
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: `❌ 启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
222
+ return { text: `启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
346
223
  }
347
- return { text: `🚀 知识库创建成功,训练已启动!\n 名称: ${kbName}\n KB ID: ${kbId}\n 任务 ID: ${pipeRes.data.task_id}\n\n训练需要 2-10 分钟,完成后可用 /linkmind_query 提问。` };
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: `❌ 创建失败: ${e.message}` };
227
+ return { text: `创建失败: ${e.message}` };
351
228
  }
352
229
  },
353
230
  });
231
+ // 4. /faq_query <kb_id> <question>
354
232
  api.registerCommand({
355
- name: "linkmind_query",
356
- description: "知识库问答: /linkmind_query <知识库名称> <问题>",
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: "用法: /linkmind_query <知识库名称> <问题>\n示例: /linkmind_query 客服知识库 什么是退款政策?" };
240
+ return { text: "用法: /faq_query <kb_id> <问题>\n示例: /faq_query abc123 什么是退款政策?" };
366
241
  }
367
- const nameOrId = args.slice(0, spaceIdx).trim();
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
- let res;
373
- try {
374
- res = await client.post("/knowledge_bases/query_by_name", {
375
- kb_name: nameOrId, question, enable_polish: false,
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
- catch {
379
- res = await client.post(`/knowledge_bases/${nameOrId}/query`, {
380
- question, enable_polish: false,
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
- if (!res.success) {
384
- const candidates = res.data?.candidates;
385
- if (candidates?.length) {
386
- const list = candidates.map((c) => ` • ${c.name} (${c.id})`).join("\n");
387
- return { text: `${res.message}\n\n可选:\n${list}` };
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
- return { text: `❌ ${res.message}` };
400
+ if (rels.length > 8)
401
+ lines.push(` ... 还有 ${rels.length - 8} 条`);
390
402
  }
391
- const d = res.data;
392
- const lines = [];
393
- if (d.kb_name)
394
- lines.push(`📚 ${d.kb_name}`);
395
- lines.push(`💡 ${d.answer || "(无答案)"}`);
396
- if (d.faq_data?.question)
397
- lines.push(`📌 匹配: ${d.faq_data.question}`);
398
- if (d.source_nodes?.[0]?.score != null) {
399
- lines.push(`📊 置信度: ${(d.source_nodes[0].score * 100).toFixed(1)}%`);
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: `❌ 问答失败: ${e.message}` };
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: "linkmind_share",
410
- description: "管理知识库权限(仅管理员): /linkmind_share <KB名称> <用户ID> <角色>",
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 authErr = await requireOwner(ctx);
414
- if (authErr)
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
- "用法: /linkmind_share <KB名称或ID> <目标用户ID> <角色>",
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
- " viewer 只读(可查询)",
424
- " editor 可编辑(可创建/修改 FAQ)",
425
- " revoke — 撤销权限",
445
+ "添加同义词:",
446
+ " /linkmind_graph_edit <kb_id> add_synonym <实体名> <同义词>",
447
+ " 示例: /linkmind_graph_edit kb_xxx add_synonym 隔烟屏 挡烟板",
426
448
  "",
427
- "示例:",
428
- " /linkmind_share 客服知识库 123456789 viewer",
429
- " /linkmind_share 客服知识库 123456789 revoke",
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 [kbNameOrId, targetUserId, action] = parts;
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
- let kbId = kbNameOrId;
436
- try {
437
- const listRes = await client.get(`/knowledge_bases?keyword=${encodeURIComponent(kbNameOrId)}&page_size=0`);
438
- const items = listRes.items ?? listRes.data?.items ?? [];
439
- const exact = items.find((kb) => kb.name === kbNameOrId);
440
- if (exact)
441
- kbId = exact.id;
442
- else if (items.length === 1)
443
- kbId = items[0].id;
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
- catch { /* use as-is */ }
446
- if (action === "revoke") {
447
- const res = await client.del(`/knowledge_bases/${kbId}/share/${targetUserId}`);
448
- return { text: `✅ ${res.message || "已撤销权限"}` };
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
- const res = await client.post(`/knowledge_bases/${kbId}/share`, {
451
- target_channel_user_id: targetUserId,
452
- target_channel_type: ctx.channel || "telegram",
453
- channel_bot_id: "default",
454
- role: action,
455
- });
456
- return { text: `✅ ${res.message || `已将 ${targetUserId} 设为 ${action}`}` };
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: `❌ 操作失败: ${e.message}` };
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: "linkmind_list",
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 authErr = await ensureAuth(ctx);
468
- if (authErr)
469
- return { text: authErr };
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
- const res = await client.get("/knowledge_bases?page=1&page_size=0");
472
- const items = res.items ?? res.data?.items ?? [];
473
- const total = res.total ?? items.length;
474
- const lines = [`📚 知识库(共 ${total} 个):`];
475
- for (const kb of items) {
476
- const st = kb.is_active ? "✅ 就绪" : "⏳ 未训练";
477
- const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
478
- const role = kb.my_role ? ` (${kb.my_role})` : "";
479
- lines.push(` • ${kb.name}${tag}${role} ${st}\n ID: ${kb.id}`);
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 (items.length === 0)
482
- lines.push(" (暂无可访问的知识库)");
483
- lines.push("", "输入 /linkmind_help 查看更多命令");
484
- return { text: lines.join("\n") };
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: `❌ ${e.message}` };
831
+ return { text: `失败: ${e.message}` };
488
832
  }
489
833
  },
490
834
  });