@aliyunrds/ctxdb 0.0.7 → 0.0.8-beta.2

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.
@@ -4,15 +4,16 @@ import {
4
4
  isConnectionError,
5
5
  resetCircuit,
6
6
  tripCircuit
7
- } from "../chunk-Q2EEP4CE.js";
7
+ } from "../chunk-52IAVJRK.js";
8
8
  import {
9
9
  CtxdbError,
10
10
  HttpClient,
11
11
  agentFromArgvWithFallback,
12
12
  debug,
13
+ isDebug,
13
14
  load,
14
15
  setDebug
15
- } from "../chunk-S45GOYUU.js";
16
+ } from "../chunk-BWPFGTF7.js";
16
17
 
17
18
  // src/lib/capture-orchestrator.ts
18
19
  import {
@@ -32,7 +33,7 @@ function getGitContext() {
32
33
  }).trim();
33
34
  const lines = raw.split("\n");
34
35
  if (lines.length < 2) return null;
35
- const project = lines[0].split("/").pop();
36
+ const project = lines[0].split(/[/\\]/).pop();
36
37
  if (!project) return null;
37
38
  const branch = lines[1];
38
39
  return { project, branch };
@@ -122,6 +123,48 @@ function toKbDetectionMessages(rows) {
122
123
  }
123
124
  return out;
124
125
  }
126
+ function selectTurnRows(rows) {
127
+ let turnStart = -1;
128
+ for (let ri = rows.length - 1; ri >= 0; ri--) {
129
+ if (hasExtractableUserText(rows[ri])) {
130
+ turnStart = ri;
131
+ while (turnStart > 0 && hasExtractableUserText(rows[turnStart - 1])) {
132
+ turnStart--;
133
+ }
134
+ break;
135
+ }
136
+ }
137
+ return turnStart >= 0 ? rows.slice(turnStart) : [];
138
+ }
139
+ function hasExtractableUserText(row) {
140
+ if (!row || typeof row !== "object") return false;
141
+ if (row.type === CODEX_RESPONSE_ITEM) {
142
+ const p = row.payload;
143
+ if (!p || typeof p !== "object") return false;
144
+ if (p.type !== "message" || p.role !== "user") return false;
145
+ const content2 = p.content;
146
+ if (!Array.isArray(content2)) return false;
147
+ return content2.some((block) => {
148
+ if (!block || typeof block !== "object") return false;
149
+ if (block.type !== "input_text") return false;
150
+ const text = block.text;
151
+ return typeof text === "string" && text.trim() && !isCodexScaffoldingText(text);
152
+ });
153
+ }
154
+ if (row.isMeta === true) return false;
155
+ const msg = row.message;
156
+ if (!msg || typeof msg !== "object") return false;
157
+ if (msg.role !== "user") return false;
158
+ const content = msg.content;
159
+ if (typeof content === "string") return Boolean(content.trim());
160
+ if (!Array.isArray(content)) return false;
161
+ return content.some((block) => {
162
+ if (!block || typeof block !== "object") return false;
163
+ if (block.type !== "text") return false;
164
+ const text = block.text;
165
+ return typeof text === "string" && Boolean(text.trim());
166
+ });
167
+ }
125
168
  function parseArgs(raw) {
126
169
  if (typeof raw !== "string") return {};
127
170
  try {
@@ -275,15 +318,10 @@ async function captureTurn(transcriptPath, cfg, client, agent = "default") {
275
318
  if (rows.length === 0) {
276
319
  return { captured: false, reason: "empty_transcript", messageCount: 0 };
277
320
  }
278
- const kb = isKnowledgeBaseUploadTurn(toKbDetectionMessages(rows));
279
- if (kb.detected) {
280
- return {
281
- captured: false,
282
- reason: `b3c_skip: ${kb.reason ?? "(no reason)"}`,
283
- messageCount: 0
284
- };
285
- }
321
+ const dbg = isDebug();
322
+ if (dbg) debug("capture", "transcript rows", summarizeTranscriptRows(rows));
286
323
  const parsed = toParsedMessages(rows);
324
+ if (dbg) debug("capture", "parsed messages", summarizeTextMessages(parsed));
287
325
  if (parsed.length === 0) {
288
326
  return {
289
327
  captured: false,
@@ -292,31 +330,39 @@ async function captureTurn(transcriptPath, cfg, client, agent = "default") {
292
330
  };
293
331
  }
294
332
  const turn = selectTurnMessages(parsed);
333
+ if (dbg) debug("capture", "selected turn", summarizeTextMessages(turn));
295
334
  if (turn.length === 0) {
296
335
  return { captured: false, reason: "empty_turn_slice", messageCount: 0 };
297
336
  }
298
337
  if (!turn.some((m) => m.role === "user")) {
299
338
  return { captured: false, reason: "no_user_in_turn", messageCount: 0 };
300
339
  }
340
+ const turnRows = selectTurnRows(rows);
341
+ if (dbg) debug("capture", "current turn rows", summarizeTranscriptRows(turnRows));
342
+ const kb = isKnowledgeBaseUploadTurn(toKbDetectionMessages(turnRows));
343
+ if (kb.detected) {
344
+ return {
345
+ captured: false,
346
+ reason: `b3c_skip: ${kb.reason ?? "(no reason)"}`,
347
+ messageCount: 0
348
+ };
349
+ }
301
350
  const rawMessages = turn.map((m) => ({ role: m.role, content: m.content }));
302
351
  const filtered = filterMessagesForExtraction(rawMessages);
352
+ if (dbg) debug("capture", "filtered messages", summarizeTextMessages(filtered));
303
353
  if (filtered.length === 0) {
304
354
  return { captured: false, reason: "all_filtered", messageCount: 0 };
305
355
  }
306
356
  const git = getGitContext();
307
357
  const skills = extractSkillSignals(rows);
308
- const bgParts = [];
309
- if (git) {
310
- bgParts.push(`This conversation took place in project "${git.project}", branch "${git.branch}".`);
311
- }
312
- if (skills.length > 0) {
313
- const descs = skills.map((s) => {
314
- const label = s.trigger === "command" ? "user-invoked" : "agent-invoked";
315
- return s.args ? `${s.skill} (${label}, args: "${s.args}")` : `${s.skill} (${label})`;
316
- });
317
- bgParts.push(`Skills used: ${descs.join(", ")}.`);
318
- }
319
- const messages = bgParts.length > 0 ? [{ role: "background", content: bgParts.join("\n") }, ...filtered] : filtered;
358
+ const bg = {};
359
+ if (git) bg.git = git;
360
+ if (skills.length > 0) bg.skills = skills.map(
361
+ ({ skill, trigger, args }) => args ? { skill, trigger, args } : { skill, trigger }
362
+ );
363
+ const hasBackground = Boolean(bg.git || bg.skills);
364
+ if (hasBackground) debug("capture", "background", bg);
365
+ const messages = hasBackground ? [{ role: "background", content: JSON.stringify(bg) }, ...filtered] : filtered;
320
366
  const payload = {
321
367
  messages,
322
368
  user_id: cfg.userId,
@@ -342,6 +388,104 @@ async function captureTurn(transcriptPath, cfg, client, agent = "default") {
342
388
  messageCount: filtered.length
343
389
  };
344
390
  }
391
+ var DEBUG_TAIL_LIMIT = 25;
392
+ var DEBUG_TEXT_LIMIT = 240;
393
+ var DEBUG_BLOCK_LIMIT = 8;
394
+ function summarizeTextMessages(messages) {
395
+ return {
396
+ count: messages.length,
397
+ roles: countRoles(messages),
398
+ tail: tailWithIndex(messages).map(({ index, item }) => ({
399
+ index,
400
+ parsedIndex: typeof item.index === "number" ? item.index : void 0,
401
+ role: item.role ?? "(unknown)",
402
+ content: summarizeText(item.content)
403
+ }))
404
+ };
405
+ }
406
+ function summarizeTranscriptRows(rows) {
407
+ return {
408
+ count: rows.length,
409
+ tail: tailWithIndex(rows).map(({ index, item: row }) => ({
410
+ index,
411
+ type: row.type,
412
+ role: rowRole(row),
413
+ isMeta: row.isMeta === true || void 0,
414
+ payloadType: row.payload?.type,
415
+ content: summarizeRowContent(row)
416
+ }))
417
+ };
418
+ }
419
+ function tailWithIndex(items) {
420
+ const start = Math.max(0, items.length - DEBUG_TAIL_LIMIT);
421
+ return items.slice(start).map((item, offset) => ({ index: start + offset, item }));
422
+ }
423
+ function countRoles(messages) {
424
+ const counts = {};
425
+ for (const msg of messages) {
426
+ const role = msg.role ?? "(unknown)";
427
+ counts[role] = (counts[role] ?? 0) + 1;
428
+ }
429
+ return counts;
430
+ }
431
+ function rowRole(row) {
432
+ const msgRole = row.message && typeof row.message === "object" ? row.message.role : void 0;
433
+ if (typeof msgRole === "string") return msgRole;
434
+ const payloadRole = row.payload && typeof row.payload === "object" ? row.payload.role : void 0;
435
+ return typeof payloadRole === "string" ? payloadRole : void 0;
436
+ }
437
+ function summarizeRowContent(row) {
438
+ if (row.type === "response_item") {
439
+ const p = row.payload;
440
+ if (!p || typeof p !== "object") return { kind: "missing" };
441
+ if (p.type === "function_call") {
442
+ return {
443
+ kind: "function_call",
444
+ name: p.name,
445
+ arguments: summarizeText(p.arguments)
446
+ };
447
+ }
448
+ return summarizeContent(p.content);
449
+ }
450
+ return summarizeContent(row.message?.content);
451
+ }
452
+ function summarizeContent(content) {
453
+ if (typeof content === "string") return summarizeText(content);
454
+ if (!Array.isArray(content)) {
455
+ return { kind: content === void 0 ? "missing" : typeof content };
456
+ }
457
+ return {
458
+ kind: "blocks",
459
+ count: content.length,
460
+ blocks: content.slice(0, DEBUG_BLOCK_LIMIT).map((block) => summarizeBlock(block)),
461
+ truncated: content.length > DEBUG_BLOCK_LIMIT || void 0
462
+ };
463
+ }
464
+ function summarizeBlock(block) {
465
+ if (!block || typeof block !== "object") return { kind: typeof block };
466
+ const b = block;
467
+ const summary = { type: b.type };
468
+ if (typeof b.name === "string") summary.name = b.name;
469
+ if (typeof b.text === "string") summary.text = summarizeText(b.text);
470
+ if (typeof b.thinking === "string") summary.thinking = summarizeText(b.thinking);
471
+ if (typeof b.content === "string") summary.content = summarizeText(b.content);
472
+ const input = b.input;
473
+ if (input && typeof input === "object" && !Array.isArray(input)) {
474
+ const i = input;
475
+ summary.inputKeys = Object.keys(i).sort();
476
+ if (typeof i.command === "string") summary.command = summarizeText(i.command);
477
+ }
478
+ return summary;
479
+ }
480
+ function summarizeText(text) {
481
+ if (typeof text !== "string") return { kind: text === void 0 ? "missing" : typeof text };
482
+ const normalized = text.replace(/\s+/g, " ").trim();
483
+ return {
484
+ kind: "text",
485
+ length: text.length,
486
+ snippet: normalized.length > DEBUG_TEXT_LIMIT ? `${normalized.slice(0, DEBUG_TEXT_LIMIT)}...` : normalized
487
+ };
488
+ }
345
489
 
346
490
  // src/hooks/stop.ts
347
491
  async function readStdinJson() {
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- listKnowledgeBases
4
- } from "../chunk-6S5RJYBC.js";
5
- import {
3
+ fetchKbCatalogBlock,
6
4
  recallTurn
7
- } from "../chunk-LIC44DR6.js";
5
+ } from "../chunk-73OY44GY.js";
6
+ import "../chunk-6S5RJYBC.js";
8
7
  import {
9
8
  isCircuitOpen
10
- } from "../chunk-Q2EEP4CE.js";
9
+ } from "../chunk-52IAVJRK.js";
11
10
  import {
12
11
  HttpClient,
13
12
  agentFromArgvWithFallback,
@@ -15,23 +14,31 @@ import {
15
14
  isComplete,
16
15
  load,
17
16
  setDebug
18
- } from "../chunk-S45GOYUU.js";
19
-
20
- // src/lib/kb-catalog.ts
21
- async function fetchKbCatalogBlock(client, agent) {
22
- const kbs = await listKnowledgeBases(client);
23
- const active = kbs.filter((kb) => kb.status === "active");
24
- if (active.length === 0) return "";
25
- const lines = active.map((kb) => `\xB7 ${kb.name}`);
26
- return [
27
- "<available-knowledge-bases>",
28
- `When you identify that relevant information may exist in the knowledge bases below, you MUST run \`ctxdb kb search "<query>" --kb=<name> --agent=${agent}\` with targeted keywords after initial analysis to supplement and correct your approach. Knowledge bases:`,
29
- ...lines,
30
- "</available-knowledge-bases>"
31
- ].join("\n");
32
- }
17
+ } from "../chunk-BWPFGTF7.js";
33
18
 
34
19
  // src/hooks/user-prompt-submit.ts
20
+ import { pathToFileURL } from "url";
21
+ async function composeUserPromptSubmit(cfg, agent, client, prompt) {
22
+ const [recall, kbBlock] = await Promise.all([
23
+ recallTurn(prompt, cfg, client, agent),
24
+ cfg.kbCatalogInjection === "user_prompt_submit" ? fetchKbCatalogBlock(client, agent).catch(() => "") : Promise.resolve("")
25
+ ]);
26
+ let ctx = recall.additionalContext || "";
27
+ if (kbBlock) ctx = ctx ? `${ctx}
28
+
29
+ ${kbBlock}` : kbBlock;
30
+ return { ctx, recall, kbBlock };
31
+ }
32
+ function formatUserPromptSubmitStdout(agent, ctx) {
33
+ if (agent === "codex") return ctx + "\n";
34
+ const out = {
35
+ hookSpecificOutput: {
36
+ hookEventName: "UserPromptSubmit",
37
+ additionalContext: ctx
38
+ }
39
+ };
40
+ return JSON.stringify(out) + "\n";
41
+ }
35
42
  async function readStdinJson() {
36
43
  let raw = "";
37
44
  for await (const chunk of process.stdin) raw += chunk;
@@ -69,36 +76,19 @@ async function main() {
69
76
  return 0;
70
77
  }
71
78
  const client = new HttpClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey, timeoutMs: 5e3 });
72
- const [result, kbBlock] = await Promise.all([
73
- recallTurn(prompt, cfg, client, agent),
74
- fetchKbCatalogBlock(client, agent).catch(() => "")
75
- ]);
76
- if (!result.ok && !result.additionalContext && !kbBlock) {
77
- const reason = result.reason ?? "no_context";
78
- debug("recall", `no result: ${reason}`, result);
79
- if (result.reason && result.reason.startsWith("http_error:")) {
80
- process.stderr.write(`ctxdb recall: ${result.reason}
79
+ const { ctx, recall, kbBlock } = await composeUserPromptSubmit(cfg, agent, client, prompt);
80
+ if (!recall.ok && !recall.additionalContext && !kbBlock) {
81
+ const reason = recall.reason ?? "no_context";
82
+ debug("recall", `no result: ${reason}`, recall);
83
+ if (recall.reason && recall.reason.startsWith("http_error:")) {
84
+ process.stderr.write(`ctxdb recall: ${recall.reason}
81
85
  `);
82
86
  }
83
87
  return 0;
84
88
  }
85
- let ctx = result.additionalContext || "";
86
- if (kbBlock) ctx = ctx ? `${ctx}
87
-
88
- ${kbBlock}` : kbBlock;
89
89
  if (!ctx) return 0;
90
90
  debug("recall", `ok, additionalContext length=${ctx.length}`);
91
- if (agent === "codex") {
92
- process.stdout.write(ctx + "\n");
93
- } else {
94
- const out = {
95
- hookSpecificOutput: {
96
- hookEventName: "UserPromptSubmit",
97
- additionalContext: ctx
98
- }
99
- };
100
- process.stdout.write(JSON.stringify(out) + "\n");
101
- }
91
+ process.stdout.write(formatUserPromptSubmitStdout(agent, ctx));
102
92
  return 0;
103
93
  } catch (err) {
104
94
  process.stderr.write(`ctxdb recall: unexpected error: ${err?.message ?? err}
@@ -106,4 +96,10 @@ ${kbBlock}` : kbBlock;
106
96
  return 0;
107
97
  }
108
98
  }
109
- main().then((code) => process.exit(code));
99
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
100
+ main().then((code) => process.exit(code));
101
+ }
102
+ export {
103
+ composeUserPromptSubmit,
104
+ formatUserPromptSubmitStdout
105
+ };
@@ -1,161 +1,191 @@
1
1
  ---
2
2
  name: contextdb-knowledge
3
- description: RDS contextdb 知识库操作全量参考。覆盖 kb create / search / upload-text / upload-file / list / documents-list / document-get 的完整参数和 JSON 输出结构。当用户提到"查知识库"、"查 KB"、"上传到 KB"、"灌进知识库"、"列出知识库"、"看文档"、"建知识库"等场景时使用。
3
+ description: contextdb 知识库高频操作(查询 / 浏览 KB / 上传文档)和命令速查。当用户提到"查知识库 / KB / 搜知识库 / KB chunks / 上传到 KB / 灌进知识库 / 列出知识库 / 列出 KB / 看文档 / 建知识库 / 建 KB"等场景时使用。配套 contextdb-memory 处理长期记忆。
4
4
  ---
5
5
 
6
- # contextdb Knowledge Base 操作参考
6
+ # contextdb Knowledge Base 操作
7
7
 
8
- ## 配置
8
+ ## 1. 什么时候用这个 skill
9
9
 
10
- - 配置文件:`~/.ctxdb/ctxdb.json`
11
- - 配置命令:`ctxdb setup --api-key=<key> --base-url=<url> [--user-id=<id>]`
12
- - 带 `--agent <name>` 时写入指定 agent 的配置段并安装 hooks + skill
13
- - 不带 `--agent` 时写入 `agents.default` 配置段(仅 CLI 使用)
14
- - 连接检查:`ctxdb ping`,状态查看:`ctxdb status`
10
+ 用户在做"查/灌/列知识库"的任何动作时进这个 skill。触发关键词:**查知识库 / 查 KB / 搜知识库 / 查 KB chunks / 上传到 KB / 灌进知识库 / 列出知识库 / 列出 KB / 看文档 / 建知识库**。
15
11
 
16
- ## kb search
12
+ - 用户想**找已有 KB 里的内容** → §2 recipe 1
13
+ - 用户**不知道有哪些 KB** → §2 recipe 2
14
+ - 用户想**了解某个 KB 收了哪些文档** → §2 recipe 3
15
+ - 用户要**写入/上传内容** → §2 recipe 4/5
17
16
 
18
- 搜索知识库。
17
+ 配套 skill:**contextdb-memory**(长期记忆操作)。两者用同一份 `~/.ctxdb/ctxdb.json` 配置和同一个 `--agent` 路由。
18
+
19
+ ## 2. 高频 recipes
20
+
21
+ > 命令里的 `--agent={{agent}}` 是当前 skill 安装目标。不要把示例改成 `qoder`;如果读到未替换的双花括号 agent 模板,先按 §6 判定真实 agent,再替换成 `qoder` / `qoderwork` / `codex` / `claude` / `default`。
22
+
23
+ ### Recipe 1:在已知 KB 中查内容(最常见)
24
+
25
+ **场景**:用户问"X KB 里关于 Y 的内容"、"在知识库 Z 查一下 W"。
19
26
 
20
27
  ```sh
21
- ctxdb kb search "<query>" [--kb=<name1,name2>] [--top-k=N] [--threshold=F] [--verbose] [--raw] [--agent=<name>]
28
+ ctxdb kb search "<query>" --kb=<kb-name> --top-k=6 --agent={{agent}}
22
29
  ```
23
30
 
24
- | 参数 | 说明 | 默认值 |
25
- |------|------|--------|
26
- | `<query>` | 搜索文本(必填) | |
27
- | `--kb` | 指定搜索的知识库名称,逗号分隔 | 搜索所有 KB |
28
- | `--top-k` | 返回 chunk 数上限 | 6 |
29
- | `--threshold` | 相关度阈值 | 0.4 |
30
- | `--verbose` | 每个 chunk 增加 `doc_name` / `kb_id` / `doc_id` / `tags` 字段 | false |
31
- | `--raw` | 服务端原始响应(13+ 字段,含 tokenizer 细节) | false |
32
- | `--agent` | 指定操作哪个 agent 的配置 | agents.default |
33
-
34
- **输出 JSON 结构**(默认):
35
-
36
- ```json
37
- {
38
- "chunks": [
39
- {
40
- "content": "chunk 文本内容",
41
- "score": 0.82
42
- }
43
- ]
44
- }
45
- ```
31
+ 返回 `chunks` 数组,按 `score` 倒序判断相关性;默认 threshold 0.4,空结果时见 §5 注意事项 4。
46
32
 
47
- `--verbose` 模式增加字段:
48
-
49
- ```json
50
- {
51
- "chunks": [
52
- {
53
- "content": "chunk 文本内容",
54
- "score": 0.82,
55
- "doc_name": "zircodb-overview",
56
- "kb_id": "kb-uuid",
57
- "doc_id": "doc-uuid",
58
- "tags": ["architecture"]
59
- }
60
- ]
61
- }
62
- ```
33
+ **何时不适用**:用户没指定 KB → 先走 recipe 2 列出来确认。
63
34
 
64
- ## kb create
35
+ ### Recipe 2:列出所有 KB(不知道有哪些)
65
36
 
66
- 创建知识库。
37
+ **场景**:用户问"有哪些知识库"、"列一下 KB",或本身在写 recipe 1 之前需要确认 KB 名。
67
38
 
68
39
  ```sh
69
- ctxdb kb create <kb-name> [--description=<desc>] [--agent=<name>]
40
+ ctxdb kb list --agent={{agent}}
70
41
  ```
71
42
 
72
- | 参数 | 说明 |
73
- |------|------|
74
- | `<kb-name>` | 知识库名称(必填) |
75
- | `--description` | 知识库描述 |
43
+ 返回 `knowledge_bases` 数组,每项含 `name` / `id` / `document_count`。跨多 KB 查 chunk 时把 `name` 拼起来:`--kb=a,b,c`。
76
44
 
77
- 上传前须先创建 KB。`upload-text` / `upload-file` 不会自动创建 KB,KB 不存在时会报错。
45
+ **何时不适用**:hook 已注入 `<available-knowledge-bases>` 块时,直接读那个块更省一次调用(见 §4)。
78
46
 
79
- ## kb upload-file
47
+ ### Recipe 3:列出某 KB 的文档清单
80
48
 
81
- 上传本地文件到知识库。
49
+ **场景**:用户问"X KB 收了哪些文档"、"X KB 里有几篇"。
82
50
 
83
51
  ```sh
84
- ctxdb kb upload-file <kb-name> <local-path> [--doc-name=<name>] [--file-path=<server-logical-path>] [--no-wait] [--agent=<name>]
52
+ ctxdb kb documents-list <kb-name> --agent={{agent}}
85
53
  ```
86
54
 
87
- | 参数 | 说明 |
88
- |------|------|
89
- | `<kb-name>` | 目标知识库名称(须已存在) |
90
- | `<local-path>` | 本机文件路径 |
91
- | `--doc-name` | 服务端文档名(默认取文件名) |
92
- | `--file-path` | 服务端逻辑路径(归档/分类用) |
93
- | `--no-wait` | 不等待 chunking 完成,立即返回 |
55
+ 返回 `documents` 数组,每项含 `name` / `id` / `size`。
94
56
 
95
- 支持格式:PDF、DOCX、MD、TXT。
57
+ **何时不适用**:用户要的是文档**正文**而不是清单 → 走 recipe 1 用 `kb search`(见 §5 注意事项 1)。
96
58
 
97
- ## kb upload-text
59
+ ### Recipe 4:灌一段文字进 KB
98
60
 
99
- 上传文本内容到知识库。
61
+ **场景**:用户说"把这段记进 X KB"、"上传这段文字到知识库 Y"。
100
62
 
101
63
  ```sh
102
- ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" [--file-path=<server-logical-path>] [--no-wait] [--agent=<name>]
64
+ ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent={{agent}}
103
65
  ```
104
66
 
105
- | 参数 | 说明 |
106
- |------|------|
107
- | `<kb-name>` | 目标知识库名称(须已存在) |
108
- | `<doc-name>` | 文档名称 |
109
- | `--text` | 文本内容(必填) |
110
- | `--file-path` | 服务端逻辑路径 |
111
- | `--no-wait` | 不等待 chunking 完成 |
67
+ 副作用操作。**用户没明示 KB 名前先 recipe 2 列出来确认**(见 §5 注意事项 3)。KB 不存在会直接报错,不自动创建。
68
+
69
+ **何时不适用**:内容是本地文件 recipe 5。
112
70
 
113
- ## kb list
71
+ ### Recipe 5:上传本地文件到 KB
114
72
 
115
- 列出所有知识库。
73
+ **场景**:用户说"把 X.pdf 上传到 Y KB"、"灌这个文件进知识库 Z"。
116
74
 
117
75
  ```sh
118
- ctxdb kb list [--agent=<name>]
76
+ ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
119
77
  ```
120
78
 
121
- 返回 `knowledge_bases` 数组,每项含知识库名称、ID、文档数等。
79
+ 支持格式:PDF / DOCX / MD / TXT。默认等待 chunking 完成;想立即返回加 `--no-wait`(见 §6)。
122
80
 
123
- ## kb documents-list
81
+ **何时不适用**:要上传的内容是字符串而非本地文件 recipe 4。
124
82
 
125
- 列出知识库中的文档。
83
+ ## 3. 命令速查
126
84
 
127
- ```sh
128
- ctxdb kb documents-list <kb-name> [--agent=<name>]
129
- ```
85
+ `--agent=<name>` 必填;详细解析链见 §6。其他 advanced flag 见 §6。
130
86
 
131
- ## kb document-get
87
+ | 子命令 | minimal signature |
88
+ |---|---|
89
+ | `kb search` | `ctxdb kb search "<query>" --agent=<name>` |
90
+ | `kb list` | `ctxdb kb list --agent=<name>` |
91
+ | `kb documents-list` | `ctxdb kb documents-list <kb-name> --agent=<name>` |
92
+ | `kb document-get` | `ctxdb kb document-get <kb-name> <doc-id> --agent=<name>` |
93
+ | `kb create` | `ctxdb kb create <kb-name> --agent=<name>` |
94
+ | `kb upload-text` | `ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent=<name>` |
95
+ | `kb upload-file` | `ctxdb kb upload-file <kb-name> <local-path> --agent=<name>` |
132
96
 
133
- 查看文档详情。
97
+ 所有命令 JSON 写 stdout、错误写 stderr 非零退出码。
134
98
 
135
- ```sh
136
- ctxdb kb document-get <kb-name> <doc-id> [--agent=<name>]
137
- ```
99
+ ## 4. Hooks 感知
100
+
101
+ **`<available-knowledge-bases>` 块**:UPS / SessionStart hook 注入到 system context;存在时直接列出 workspace 内所有 KB 名(省一次 `kb list` 调用)。可直接喂给 `--kb=<name>`。
102
+
103
+ **`<recalled-memories>` 块**:是 contextdb-memory 的产物,但 `kb search --knowledge` 会复用同样的 retrieval 路径。两块独立。
138
104
 
139
- ## 写操作注意事项
105
+ **B-3c 守卫**:含 `kb upload-text` / `kb upload-file` 的对话轮次,其内容**不会**被 autoCapture 写入记忆(防文档原文混进记忆桶)。
140
106
 
141
- `kb create`、`kb upload-text`、`kb upload-file` 是副作用操作(创建 KB / 写入文档)。执行前须明确目标 KB 名称:
107
+ **纯 CLI 模式**:对话里没有上面任何块 说明当前不在 hook 环境(agent 是裸 CLI 调用),所有信息只能主动调命令拿。
142
108
 
143
- - 用户指定了 KB 名称 → 直接使用
144
- - 用户未指定 → 先用 `kb list` 查看现有知识库,向用户确认目标 KB 后再操作
145
- - KB 不存在时 upload 命令会自动创建,需确认用户意图是新建还是写入已有 KB
109
+ ## 5. 注意事项
146
110
 
147
- ## --agent 参数
111
+ ### 注意事项 1:`kb document-get` 只返 metadata,**不返正文**
148
112
 
149
- 所有 kb 命令支持 `--agent <name>` 指定操作目标。知识库归属于 workspace,不同 agent 若共享同一 workspace 则访问相同的知识库。
113
+ - **现象**:想读文档内容,调 `kb document-get` 拿到的只有 `{id, name, size, upload_time, tags}`,没 `content` 字段
114
+ - **根因**:服务端当前只暴露元信息端点;获取正文需走 chunk 检索
115
+ - **正确做法**:用 `kb search "<关键词>" --kb=<name>` 拿 chunks,或 `kb documents-list <kb-name>` 后浏览返回的内嵌 chunk(如有)
150
116
 
151
- 不带 `--agent` 时的解析顺序:`CTXDB_AGENT` 环境变量 `agents.default` 配置段。
117
+ ### 注意事项 2:解析 JSON `jq`,**不要 inline `python3 -c`**
152
118
 
153
- ## Hooks 感知
119
+ - **现象**:在 shell 里写 `ctxdb kb list | python3 -c "import json,sys; ..."` 经常因为单/双引号嵌套 escape 失败
120
+ - **根因**:shell + python -c 的引号是两层独立 escape,组合时极容易漏
121
+ - **正确做法**:`ctxdb kb list --agent={{agent}} | jq '.knowledge_bases[].name'`;jq 表达式在单引号里不需要 escape
154
122
 
155
- **KB 目录注入**:如果对话中出现 `<available-knowledge-bases>` 块,表示 hooks 已启用,该块列出可用知识库名称,可直接用于 `--kb=<name>` 参数。
123
+ ### 注意事项 3:`upload-*` / `create` 是副作用,没明示 KB 名前必须先确认
156
124
 
157
- **B-3c 守卫**:包含 `kb upload-text` `kb upload-file` 操作的对话轮次,其内容不会被 autoCapture 写入记忆(防止文档原文混入记忆)。
125
+ - **现象**:用户只说"把这段记进知识库"没指定 KB,agent 直接用了某个猜的 KB 写到错的桶
126
+ - **根因**:写命令不会跟 agent 二次确认;KB 不存在时报错,但**KB 名拼错却恰好命中另一个真实 KB**时会静默写错
127
+ - **正确做法**:用户没明示 → 必须先 `kb list` 拿候选清单,跟用户确认目标后才执行
158
128
 
159
- 如果对话中没有 `<available-knowledge-bases>`,表示当前为纯 CLI 模式,用 `kb list` 查看可用知识库。
129
+ ### 注意事项 4:`kb search` 空结果时降阈值再试一次
160
130
 
161
- 所有命令输出 JSON stdout,错误输出到 stderr 并返回非零退出码。
131
+ - **现象**:query 明明跟内容相关,但 `chunks: []`
132
+ - **根因**:默认 `--threshold=0.4`,短 query / 关键词偏冷时打不到
133
+ - **正确做法**:降到 `--threshold=0.2` 或 `--threshold=0.3` 重试一次再判定真的没命中;仍空才放弃
134
+
135
+ ### 注意事项 5:`upload-*` 不会自动建 KB
136
+
137
+ - **现象**:`kb upload-text foo bar --text=...` 报错 "kb 'foo' not found"
138
+ - **根因**:上传命令只写已有 KB,**不会**为了你 implicit 建一个
139
+ - **正确做法**:先 `ctxdb kb create foo --agent={{agent}}`,再 upload
140
+
141
+ ## 6. 高级参数
142
+
143
+ ### `kb search` 全部 flag
144
+
145
+ | 参数 | 说明 | 默认值 |
146
+ |------|------|--------|
147
+ | `--kb=<name1,name2>` | 限定搜索的 KB,逗号分隔 | 搜所有 |
148
+ | `--top-k=N` | chunk 数上限 | 6 |
149
+ | `--threshold=F` | 相关度阈值 | 0.4 |
150
+ | `--verbose` | 每个 chunk 加 `doc_name` / `kb_id` / `doc_id` / `tags` | false |
151
+ | `--raw` | 服务端原始响应(13+ 字段,含 tokenizer 细节) | false |
152
+
153
+ ### `kb upload-file` / `kb upload-text` 全部 flag
154
+
155
+ | 参数 | 说明 |
156
+ |------|------|
157
+ | `--doc-name` | (`upload-file`)服务端文档名,默认取本地文件名 |
158
+ | `--file-path` | 服务端逻辑路径(归档/分类用) |
159
+ | `--no-wait` | 不等 chunking 完成立即返回 |
160
+
161
+ ### `kb create` 全部 flag
162
+
163
+ | 参数 | 说明 |
164
+ |------|------|
165
+ | `--description` | 知识库描述 |
166
+
167
+ ### `kb list` / `kb documents-list` / `kb document-get`
168
+
169
+ 无额外 flag(除通用参数)。
170
+
171
+ ### 通用参数
172
+
173
+ | 参数 | 说明 |
174
+ |------|------|
175
+ | `--agent=<name>` | **必填**。指定操作哪个 agent 的配置桶(`qoder` / `qoderwork` / `codex` / `claude` / `default`) |
176
+
177
+ `--agent` 解析顺序:
178
+ 1. 命令行显式 `--agent=<name>` → 用它
179
+ 2. 缺省 → 读 `CTXDB_AGENT` 环境变量
180
+ 3. env 也没有 → 落到 `"default"` 桶
181
+ 4. 新版本 `ctxdb setup --agent <qoder|qoderwork|codex|claude>` 会在 `agents.default` 缺失时复制当前 agent 配置作为兜底
182
+ 5. 若 `"default"` 桶仍不存在或未配置完整 → exit 2 "config incomplete for agent default"
183
+
184
+ ## 7. 配置
185
+
186
+ - 配置文件:`~/.ctxdb/ctxdb.json`,分 `agents.<name>` 段(`qoder` / `qoderwork` / `codex` / `claude` / `default`),互不复用
187
+ - 配置命令:`ctxdb setup --agent <name> --api-key=<key> --base-url=<url> [--user-id=<id>]`
188
+ - 带 `--agent` 时还会装 hooks + skill;若 `agents.default` 缺失,会复制当前 agent 配置作为 CLI 兜底
189
+ - `--agent default` 只写 `agents.default` 段(仅 CLI 用,不装 hooks / skill)
190
+ - 连接检查:`ctxdb ping --agent=<name>`
191
+ - 状态查看:`ctxdb status --agent=<name>`