@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.
- package/dist/{chunk-Q2EEP4CE.js → chunk-52IAVJRK.js} +1 -1
- package/dist/{chunk-LIC44DR6.js → chunk-73OY44GY.js} +34 -3
- package/dist/{chunk-S45GOYUU.js → chunk-BWPFGTF7.js} +25 -8
- package/dist/cli/main.js +41 -22
- package/dist/hooks/session-start.js +64 -32
- package/dist/hooks/stop.js +167 -23
- package/dist/hooks/user-prompt-submit.js +41 -45
- package/dist/setup/skills/contextdb-knowledge/SKILL.md +135 -105
- package/dist/setup/skills/contextdb-memory/SKILL.md +137 -95
- package/package.json +2 -3
package/dist/hooks/stop.js
CHANGED
|
@@ -4,15 +4,16 @@ import {
|
|
|
4
4
|
isConnectionError,
|
|
5
5
|
resetCircuit,
|
|
6
6
|
tripCircuit
|
|
7
|
-
} from "../chunk-
|
|
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-
|
|
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(
|
|
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
|
|
279
|
-
if (
|
|
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
|
|
309
|
-
if (git)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
4
|
-
} from "../chunk-6S5RJYBC.js";
|
|
5
|
-
import {
|
|
3
|
+
fetchKbCatalogBlock,
|
|
6
4
|
recallTurn
|
|
7
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-73OY44GY.js";
|
|
6
|
+
import "../chunk-6S5RJYBC.js";
|
|
8
7
|
import {
|
|
9
8
|
isCircuitOpen
|
|
10
|
-
} from "../chunk-
|
|
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-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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>"
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
### Recipe 2:列出所有 KB(不知道有哪些)
|
|
65
36
|
|
|
66
|
-
|
|
37
|
+
**场景**:用户问"有哪些知识库"、"列一下 KB",或本身在写 recipe 1 之前需要确认 KB 名。
|
|
67
38
|
|
|
68
39
|
```sh
|
|
69
|
-
ctxdb kb
|
|
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
|
-
|
|
45
|
+
**何时不适用**:hook 已注入 `<available-knowledge-bases>` 块时,直接读那个块更省一次调用(见 §4)。
|
|
78
46
|
|
|
79
|
-
|
|
47
|
+
### Recipe 3:列出某 KB 的文档清单
|
|
80
48
|
|
|
81
|
-
|
|
49
|
+
**场景**:用户问"X KB 收了哪些文档"、"X KB 里有几篇"。
|
|
82
50
|
|
|
83
51
|
```sh
|
|
84
|
-
ctxdb kb
|
|
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
|
-
|
|
57
|
+
**何时不适用**:用户要的是文档**正文**而不是清单 → 走 recipe 1 用 `kb search`(见 §5 注意事项 1)。
|
|
96
58
|
|
|
97
|
-
|
|
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>"
|
|
64
|
+
ctxdb kb upload-text <kb-name> <doc-name> --text="<body>" --agent={{agent}}
|
|
103
65
|
```
|
|
104
66
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
71
|
+
### Recipe 5:上传本地文件到 KB
|
|
114
72
|
|
|
115
|
-
|
|
73
|
+
**场景**:用户说"把 X.pdf 上传到 Y KB"、"灌这个文件进知识库 Z"。
|
|
116
74
|
|
|
117
75
|
```sh
|
|
118
|
-
ctxdb kb
|
|
76
|
+
ctxdb kb upload-file <kb-name> <local-path> --agent={{agent}}
|
|
119
77
|
```
|
|
120
78
|
|
|
121
|
-
|
|
79
|
+
支持格式:PDF / DOCX / MD / TXT。默认等待 chunking 完成;想立即返回加 `--no-wait`(见 §6)。
|
|
122
80
|
|
|
123
|
-
|
|
81
|
+
**何时不适用**:要上传的内容是字符串而非本地文件 → recipe 4。
|
|
124
82
|
|
|
125
|
-
|
|
83
|
+
## 3. 命令速查
|
|
126
84
|
|
|
127
|
-
|
|
128
|
-
ctxdb kb documents-list <kb-name> [--agent=<name>]
|
|
129
|
-
```
|
|
85
|
+
`--agent=<name>` 必填;详细解析链见 §6。其他 advanced flag 见 §6。
|
|
130
86
|
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
107
|
+
**纯 CLI 模式**:对话里没有上面任何块 → 说明当前不在 hook 环境(agent 是裸 CLI 调用),所有信息只能主动调命令拿。
|
|
142
108
|
|
|
143
|
-
|
|
144
|
-
- 用户未指定 → 先用 `kb list` 查看现有知识库,向用户确认目标 KB 后再操作
|
|
145
|
-
- KB 不存在时 upload 命令会自动创建,需确认用户意图是新建还是写入已有 KB
|
|
109
|
+
## 5. 注意事项
|
|
146
110
|
|
|
147
|
-
|
|
111
|
+
### 注意事项 1:`kb document-get` 只返 metadata,**不返正文**
|
|
148
112
|
|
|
149
|
-
|
|
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
|
-
|
|
117
|
+
### 注意事项 2:解析 JSON 用 `jq`,**不要 inline `python3 -c`**
|
|
152
118
|
|
|
153
|
-
|
|
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
|
-
|
|
123
|
+
### 注意事项 3:`upload-*` / `create` 是副作用,没明示 KB 名前必须先确认
|
|
156
124
|
|
|
157
|
-
|
|
125
|
+
- **现象**:用户只说"把这段记进知识库"没指定 KB,agent 直接用了某个猜的 KB 名 → 写到错的桶
|
|
126
|
+
- **根因**:写命令不会跟 agent 二次确认;KB 不存在时报错,但**KB 名拼错却恰好命中另一个真实 KB**时会静默写错
|
|
127
|
+
- **正确做法**:用户没明示 → 必须先 `kb list` 拿候选清单,跟用户确认目标后才执行
|
|
158
128
|
|
|
159
|
-
|
|
129
|
+
### 注意事项 4:`kb search` 空结果时降阈值再试一次
|
|
160
130
|
|
|
161
|
-
|
|
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>`
|