@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.
Files changed (33) hide show
  1. package/dist/index.d.ts +7 -11
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +653 -388
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/api-client.d.ts +23 -39
  6. package/dist/lib/api-client.d.ts.map +1 -1
  7. package/dist/lib/api-client.js +54 -97
  8. package/dist/lib/api-client.js.map +1 -1
  9. package/dist/tools/account-manage.d.ts +5 -7
  10. package/dist/tools/account-manage.d.ts.map +1 -1
  11. package/dist/tools/account-manage.js +104 -70
  12. package/dist/tools/account-manage.js.map +1 -1
  13. package/dist/tools/account-register.d.ts +12 -5
  14. package/dist/tools/account-register.d.ts.map +1 -1
  15. package/dist/tools/account-register.js +51 -28
  16. package/dist/tools/account-register.js.map +1 -1
  17. package/dist/tools/query-kb.d.ts +4 -5
  18. package/dist/tools/query-kb.d.ts.map +1 -1
  19. package/dist/tools/query-kb.js +11 -37
  20. package/dist/tools/query-kb.js.map +1 -1
  21. package/package.json +3 -3
  22. package/skills/faq-kb/SKILL.md +143 -47
  23. package/linkmind_claw-openclaw-faq-kb-1.0.6.tgz +0 -0
  24. package/linkmind_claw-openclaw-faq-kb-1.1.0.tgz +0 -0
  25. package/linkmind_claw-openclaw-faq-kb-1.2.0.tgz +0 -0
  26. package/linkmind_claw-openclaw-faq-kb-1.3.1.tgz +0 -0
  27. package/linkmind_claw-openclaw-faq-kb-1.3.2.tgz +0 -0
  28. package/linkmind_claw-openclaw-faq-kb-1.3.3.tgz +0 -0
  29. package/linkmind_claw-openclaw-faq-kb-1.4.0.tgz +0 -0
  30. package/linkmind_claw-openclaw-faq-kb-2.0.0.tgz +0 -0
  31. package/linkmind_claw-openclaw-faq-kb-2.0.1.tgz +0 -0
  32. package/linkmind_claw-openclaw-faq-kb-2.0.2.tgz +0 -0
  33. 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 — LinkMind Knowledge Base
2
+ * OpenClaw Plugin entry FAQ Knowledge Base
3
3
  *
4
- * 四级权限: Admin > Editor(管理员) > Viewer(查看者) > 无权限
4
+ * 5 tools + 5 slash commands:
5
5
  *
6
- * Slash Commands:
7
- * /linkmind_help 使用帮助
8
- * /linkmind_claim 绑定 Admin 身份(首次部署)
9
- * /linkmind_me 查看我的身份信息
10
- * /linkmind_role 设置用户角色(仅 Admin)
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: "LinkMind Knowledge Base",
29
- description: "LinkMind 智能知识库:四级权限管理、一键文档训练、智能问答",
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
- let cachedOwnerStatus = null;
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: "linkmind_account_register",
146
- label: "用户注册/登录",
38
+ name: "faq_account_register",
39
+ label: "社媒用户注册/登录",
147
40
  description: [
148
- "社媒用户 OAuth 注册或登录。",
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: "linkmind_account_manage",
157
- label: "权限管理",
52
+ name: "faq_account_manage",
53
+ label: "账号权限管理",
158
54
  description: [
159
- "管理员权限操作工具。",
160
- "grant_role / list_users / my_info",
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: "linkmind_kb_create_and_train",
65
+ name: "faq_kb_create_and_train",
167
66
  label: "创建知识库并训练",
168
67
  description: [
169
- "创建 FAQ 知识库并自动完成全部训练流程。",
170
- "支持 PDF / DOCX / DOC / TXT / MD 格式。",
171
- "训练过程通常需要 2-10 分钟。",
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: "linkmind_kb_query",
79
+ name: "faq_kb_query",
178
80
  label: "知识库问答",
179
81
  description: [
180
- "向知识库提问并获取 AI 智能回答。",
181
- "可以用知识库名称(kb_name)或 ID(kb_id)指定,推荐用名称。",
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: "linkmind_kb_list",
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 st = kb.is_active ? "就绪" : "未训练";
118
+ const s = kb.is_active ? "就绪" : "未训练";
210
119
  const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
211
- lines.push(` ${kb.id} ${kb.name}${tag} (${st})`);
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
- // ── Slash Commands ─────────────────────────────────────────────────
219
- api.registerCommand({
220
- name: "linkmind_help",
221
- description: "LinkMind 使用帮助",
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: "linkmind_claim",
271
- description: "绑定管理员身份: /linkmind_claim <密钥>",
132
+ name: "faq_register",
133
+ description: "注册/登录: /faq_register 或 /faq_register <渠道用户ID>",
272
134
  acceptsArgs: true,
273
135
  handler: async (ctx) => {
274
- if (!ctx.senderId || !ctx.channel) {
275
- return { text: "无法获取你的用户信息,请在社媒渠道中使用。" };
276
- }
277
- const secret = (ctx.args ?? "").trim();
278
- if (!secret) {
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("/owner/claim", {
303
- claim_secret: secret,
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
- cachedOwnerStatus = { claimed: true, checkedAt: Date.now() };
306
- return {
307
- text: [
308
- `✅ ${res.message || "绑定成功!"}`,
309
- "",
310
- "你现在是此服务器的管理员。",
311
- "输入 /linkmind_help 查看可用命令。",
312
- ].join("\n"),
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: `❌ 获取信息失败: ${e.message}` };
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: "linkmind_role",
353
- description: "设置用户角色(仅 Admin): /linkmind_role <用户ID> <editor|user>",
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.put(`/users/${targetUserId}/role`, { role });
379
- return { text: `✅ ${res.message || `已将用户设为 ${role}`}` };
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: `❌ 设置失败: ${e.message}` };
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: "linkmind_create",
388
- description: "创建知识库并训练: /linkmind_create <名称> <文档URL>",
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: "用法: /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" };
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: `❌ 启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
222
+ return { text: `启动训练失败: ${pipeRes.message}\n知识库已创建 (ID: ${kbId})` };
414
223
  }
415
- 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 提问。` };
416
225
  }
417
226
  catch (e) {
418
- return { text: `❌ 创建失败: ${e.message}` };
227
+ return { text: `创建失败: ${e.message}` };
419
228
  }
420
229
  },
421
230
  });
231
+ // 4. /faq_query <kb_id> <question>
422
232
  api.registerCommand({
423
- name: "linkmind_query",
424
- description: "知识库问答: /linkmind_query <知识库名称> <问题>",
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: "用法: /linkmind_query <知识库名称> <问题>\n示例: /linkmind_query 客服知识库 什么是退款政策?" };
240
+ return { text: "用法: /faq_query <kb_id> <问题>\n示例: /faq_query abc123 什么是退款政策?" };
434
241
  }
435
- const nameOrId = args.slice(0, spaceIdx).trim();
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
- let res;
441
- try {
442
- res = await client.post("/knowledge_bases/query_by_name", {
443
- kb_name: nameOrId, question, enable_polish: false,
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
- catch {
447
- res = await client.post(`/knowledge_bases/${nameOrId}/query`, {
448
- question, enable_polish: false,
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 (!res.success) {
452
- const candidates = res.data?.candidates;
453
- if (candidates?.length) {
454
- const list = candidates.map((c) => ` • ${c.name} (${c.id})`).join("\n");
455
- return { text: `${res.message}\n\n可选:\n${list}` };
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
- return { text: `❌ ${res.message}` };
370
+ lines.push("\n使用 /linkmind_graph <kb_id> <实体名> 查看该实体的关系和关联FAQ");
371
+ return { text: lines.join("\n") };
458
372
  }
459
- const d = res.data;
460
- const lines = [];
461
- if (d.kb_name)
462
- lines.push(`📚 ${d.kb_name}`);
463
- lines.push(`💡 ${d.answer || "(无答案)"}`);
464
- if (d.faq_data?.question)
465
- lines.push(`📌 匹配: ${d.faq_data.question}`);
466
- if (d.source_nodes?.[0]?.score != null) {
467
- lines.push(`📊 置信度: ${(d.source_nodes[0].score * 100).toFixed(1)}%`);
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: `❌ 问答失败: ${e.message}` };
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: "linkmind_share",
478
- description: "管理知识库权限与可见性: /linkmind_share <KB名称> <用户ID|public|private> [viewer|revoke]",
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 authErr = await requireEditor(ctx);
482
- if (authErr)
483
- return { text: authErr };
484
- const parts = (ctx.args ?? "").trim().split(/\s+/);
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
- " /linkmind_share <KB名称> public — 设为公开",
490
- " /linkmind_share <KB名称> private — 设为私有",
491
- " /linkmind_share <KB名称> <用户ID> viewer — 授予查看权限",
492
- " /linkmind_share <KB名称> <用户ID> revoke — 撤销权限",
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
- " /linkmind_share 客服知识库 public",
496
- " /linkmind_share 客服知识库 123456789 viewer",
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 kbNameOrId = parts[0];
626
+ const parts = args.split(/\s+/);
627
+ const kbId = parts[0];
628
+ const action = (parts[1] || "").toLowerCase();
501
629
  try {
502
- let kbId = kbNameOrId;
503
- try {
504
- const listRes = await client.get(`/knowledge_bases?keyword=${encodeURIComponent(kbNameOrId)}&page_size=0`);
505
- const items = listRes.items ?? listRes.data?.items ?? [];
506
- const exact = items.find((kb) => kb.name === kbNameOrId);
507
- if (exact)
508
- kbId = exact.id;
509
- else if (items.length === 1)
510
- kbId = items[0].id;
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
- catch { /* use as-is */ }
513
- if (parts[1] === "public" || parts[1] === "private") {
514
- const isPublic = parts[1] === "public";
515
- const res = await client.put(`/knowledge_bases/${kbId}/visibility`, { is_public: isPublic });
516
- return { text: `✅ ${res.message || `知识库已设为${isPublic ? "公开" : "私有"}`}` };
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 (parts.length < 3) {
519
- return { text: "授权用户时需要指定角色: viewer 或 revoke" };
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
- const [, targetUserId, action] = parts;
522
- if (action === "revoke") {
523
- const res = await client.del(`/knowledge_bases/${kbId}/share/${targetUserId}`);
524
- return { text: `✅ ${res.message || "已撤销权限"}` };
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 !== "viewer") {
527
- return { text: "只能授予 viewer 权限。如需提升为管理员请使用 /linkmind_role" };
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
- const res = await client.post(`/knowledge_bases/${kbId}/share`, {
530
- target_channel_user_id: targetUserId,
531
- target_channel_type: ctx.channel || "telegram",
532
- channel_bot_id: "default",
533
- role: "viewer",
534
- });
535
- return { text: `✅ ${res.message || `已将 ${targetUserId} 设为查看者`}` };
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: `❌ 操作失败: ${e.message}` };
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: "linkmind_list",
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 authErr = await ensureAuth(ctx);
547
- if (authErr)
548
- 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();
549
743
  try {
550
- const res = await client.get("/knowledge_bases?page=1&page_size=0");
551
- const items = res.items ?? res.data?.items ?? [];
552
- const total = res.total ?? items.length;
553
- const lines = [`📚 知识库(共 ${total} 个):`];
554
- for (const kb of items) {
555
- const st = kb.is_active ? "✅ 就绪" : "⏳ 未训练";
556
- const tag = kb.tag_name ? ` [${kb.tag_name}]` : "";
557
- const role = kb.my_role ? ` (${kb.my_role})` : "";
558
- 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") };
559
773
  }
560
- if (items.length === 0)
561
- lines.push(" (暂无可访问的知识库)");
562
- lines.push("", "输入 /linkmind_help 查看更多命令");
563
- 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` };
564
829
  }
565
830
  catch (e) {
566
- return { text: `❌ ${e.message}` };
831
+ return { text: `失败: ${e.message}` };
567
832
  }
568
833
  },
569
834
  });