@ganziliang/kb 0.1.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -3
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +414 -148
- package/dist/model.d.ts +15 -4
- package/dist/model.js +56 -7
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +10 -1
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +52 -0
- package/dist/ui.d.ts +88 -0
- package/dist/ui.js +171 -0
- package/package.json +4 -6
package/dist/cli.js
CHANGED
|
@@ -1,204 +1,470 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { Box, Text, render, useApp, useInput } from "ink";
|
|
5
2
|
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { Container, Editor, Loader, ProcessTerminal, SelectList, Spacer, TuiMainScreen, matchesKey, } from "@earendil-works/pi-tui";
|
|
6
4
|
import { ensureModelConfig, saveModels } from "./config.js";
|
|
7
|
-
import {
|
|
8
|
-
import { copyOriginal, defaultDataRoot, ensureKnowledgeBase, isImageFile, KnowledgeStore, readImage, readLocalFile,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { SAVE_KNOWLEDGE_TOOL, describeImage, fetchTransport } from "./model.js";
|
|
6
|
+
import { backupKnowledgeBase, copyOriginal, defaultDataRoot, deleteKnowledgeBase, ensureKnowledgeBase, isImageFile, KnowledgeStore, listKnowledgeBases, readImage, readLocalFile, restoreKnowledgeBase, writeNote, } from "./storage.js";
|
|
7
|
+
import { color, editorTheme, selectListTheme } from "./theme.js";
|
|
8
|
+
import { AssistantMessage, HelpPanel, ImageNotice, Notice, Rule, Splash, UserMessage } from "./ui.js";
|
|
9
|
+
const errText = (error) => (error instanceof Error ? error.message : String(error));
|
|
10
|
+
/**
|
|
11
|
+
* 显式录入命令,仅作为模型不可用时的兜底。
|
|
12
|
+
* 正常情况交给模型按语义判断(见 buildSystemPrompt),
|
|
13
|
+
* 因此这里只保留 / 开头的命令,不硬匹配「记住」这类自然语言词。
|
|
14
|
+
*/
|
|
15
|
+
const NOTE_PATTERN = /^\/(?:add|note)(?:\s+([\s\S]*))?$/;
|
|
16
|
+
/** 从录入内容里推导一个标题:优先取 Markdown 标题,否则取首个非空行。 */
|
|
17
|
+
function deriveTitle(content) {
|
|
18
|
+
const firstLine = content.split("\n").map((l) => l.trim()).find(Boolean) ?? "笔记";
|
|
19
|
+
const heading = firstLine.match(/^#{1,6}\s+(.+)$/);
|
|
20
|
+
const base = (heading ? heading[1] : firstLine).replace(/[#*`>_]/g, "").trim();
|
|
21
|
+
return base.slice(0, 40) || "���记";
|
|
22
|
+
}
|
|
23
|
+
/** 多行内容回显时只显示首行,避免刷屏。 */
|
|
24
|
+
function shortPreview(text) {
|
|
25
|
+
const lines = text.split("\n");
|
|
26
|
+
return lines.length > 1 ? `${lines[0]} …(共 ${lines.length} 行)` : text;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 把「回答」和「录入」放进同一轮对话:模型自己决定是回答还是调用工具,
|
|
30
|
+
* 因此判断意图不会带来额外的一次模型调用。
|
|
31
|
+
*/
|
|
32
|
+
function buildSystemPrompt(context) {
|
|
33
|
+
return [
|
|
34
|
+
"你是本地知识库助手。每一轮输入,先判断用户意图:",
|
|
35
|
+
"",
|
|
36
|
+
"A) 提问 —— 用户想从知识库获取信息。",
|
|
37
|
+
" 用「知识库内容」回答,并标注来源文件名与版本。",
|
|
38
|
+
" 内容不足以回答时明确说明没有找到,不要编造。",
|
|
39
|
+
"",
|
|
40
|
+
"B) 录入 —— 用户想把一段内容长期存进知识库。",
|
|
41
|
+
" 调用 save_knowledge 工具,title 用一句话概括,content 用规范 Markdown 并完整保留用户信息。",
|
|
42
|
+
" 此时不要回答内容本身,也不要反问确认。",
|
|
43
|
+
"",
|
|
44
|
+
"判断依据是用户表达的意图,不要依赖固定关键词。",
|
|
45
|
+
"",
|
|
46
|
+
"知识库内容:",
|
|
47
|
+
"---",
|
|
48
|
+
context,
|
|
49
|
+
"---",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
const HELP_ITEMS = [
|
|
53
|
+
["/help", "显示本帮助"],
|
|
54
|
+
["/clear", "清空对话上下文"],
|
|
55
|
+
["/sources", "列出知识来源与版本"],
|
|
56
|
+
["/add <内容>", "显式录入(通常直接说「记住…」即可)"],
|
|
57
|
+
["/model", "查看 / 切换模型"],
|
|
58
|
+
["/kb", "多知识库管理"],
|
|
59
|
+
["/backup", "备份当前知识库"],
|
|
60
|
+
["/restore", "��备份恢复"],
|
|
61
|
+
["/cleanup", "清理历史版本"],
|
|
62
|
+
["/quit", "退出"],
|
|
63
|
+
["", ""],
|
|
64
|
+
["导入 <路径>", "录入 md / txt / json / csv / xlsx / png / jpg"],
|
|
16
65
|
];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
const ROLE = { user: "❯", assistant: "⏺", ok: "✔", err: "✖", warn: "!", image: "★" };
|
|
67
|
+
export class KbApp {
|
|
68
|
+
root;
|
|
69
|
+
tui;
|
|
70
|
+
stream = new Container();
|
|
71
|
+
editor;
|
|
72
|
+
store;
|
|
73
|
+
base;
|
|
74
|
+
models;
|
|
75
|
+
modelIndex = 0;
|
|
76
|
+
messages = [];
|
|
77
|
+
turns = 0;
|
|
78
|
+
busy = false;
|
|
79
|
+
loader = null;
|
|
80
|
+
overlay = null;
|
|
81
|
+
closed = false;
|
|
82
|
+
constructor(root, base, models) {
|
|
83
|
+
this.root = root;
|
|
84
|
+
this.base = base;
|
|
85
|
+
this.models = models;
|
|
86
|
+
this.store = new KnowledgeStore(base.path);
|
|
87
|
+
this.tui = new TuiMainScreen(new ProcessTerminal());
|
|
88
|
+
this.editor = new Editor(this.tui, editorTheme, { paddingX: 1 });
|
|
89
|
+
this.editor.onSubmit = (text) => { void this.submit(text.trim()); };
|
|
90
|
+
}
|
|
91
|
+
// ---------- 启动 ----------
|
|
92
|
+
start() {
|
|
93
|
+
this.tui.addChild(new Splash({
|
|
94
|
+
base: this.base.name,
|
|
95
|
+
model: this.model?.model ?? "-",
|
|
96
|
+
dataPath: this.root,
|
|
97
|
+
sources: this.store.sources().length,
|
|
98
|
+
}));
|
|
99
|
+
this.tui.addChild(this.stream);
|
|
100
|
+
this.tui.addChild(new Spacer(1));
|
|
101
|
+
this.tui.addChild(new Rule());
|
|
102
|
+
this.tui.addChild(this.editor);
|
|
103
|
+
this.tui.setFocus(this.editor);
|
|
104
|
+
this.tui.addInputListener((data) => {
|
|
105
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
106
|
+
this.shutdown();
|
|
107
|
+
return { consume: true };
|
|
108
|
+
}
|
|
109
|
+
if (matchesKey(data, "escape") && this.tui.hasOverlay()) {
|
|
110
|
+
this.closeOverlay();
|
|
111
|
+
return { consume: true };
|
|
112
|
+
}
|
|
113
|
+
return {};
|
|
114
|
+
});
|
|
115
|
+
this.tui.start();
|
|
116
|
+
}
|
|
117
|
+
get model() { return this.models[this.modelIndex]; }
|
|
118
|
+
// ---------- 渲染原语 ----------
|
|
119
|
+
add(component) {
|
|
120
|
+
this.stream.addChild(component);
|
|
121
|
+
this.tui.requestRender();
|
|
122
|
+
}
|
|
123
|
+
setBusy(on, message = "") {
|
|
124
|
+
if (on) {
|
|
125
|
+
this.editor.disableSubmit = true;
|
|
126
|
+
if (this.loader) {
|
|
127
|
+
this.loader.setMessage(message);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.loader = new Loader(this.tui, color.accent, color.muted, message);
|
|
131
|
+
this.stream.addChild(this.loader);
|
|
132
|
+
this.loader.start();
|
|
133
|
+
}
|
|
33
134
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
135
|
+
else {
|
|
136
|
+
this.editor.disableSubmit = false;
|
|
137
|
+
if (this.loader) {
|
|
138
|
+
this.loader.stop();
|
|
139
|
+
this.stream.removeChild(this.loader);
|
|
140
|
+
this.loader = null;
|
|
141
|
+
}
|
|
38
142
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
143
|
+
this.tui.requestRender();
|
|
144
|
+
}
|
|
145
|
+
showOverlay(component, width) {
|
|
146
|
+
this.closeOverlay();
|
|
147
|
+
this.overlay = this.tui.showOverlay(component, { width, anchor: "center", margin: 1 });
|
|
148
|
+
}
|
|
149
|
+
closeOverlay() {
|
|
150
|
+
if (this.overlay) {
|
|
151
|
+
this.overlay.hide();
|
|
152
|
+
this.overlay = null;
|
|
42
153
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (value === "/quit" || value === "/exit") {
|
|
48
|
-
store.close();
|
|
49
|
-
exit();
|
|
154
|
+
this.tui.setFocus(this.editor);
|
|
155
|
+
}
|
|
156
|
+
shutdown() {
|
|
157
|
+
if (this.closed)
|
|
50
158
|
return;
|
|
159
|
+
this.closed = true;
|
|
160
|
+
try {
|
|
161
|
+
this.store.close();
|
|
51
162
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
163
|
+
catch { /* 已关闭 */ }
|
|
164
|
+
this.tui.stop();
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
// ---------- 输入入口 ----------
|
|
168
|
+
async submit(value) {
|
|
169
|
+
if (this.busy || !value)
|
|
170
|
+
return;
|
|
171
|
+
this.editor.setText("");
|
|
172
|
+
this.tui.requestRender();
|
|
173
|
+
const note = value.match(NOTE_PATTERN);
|
|
174
|
+
if (note) {
|
|
175
|
+
const content = (note[1] ?? "").trim();
|
|
176
|
+
if (!content) {
|
|
177
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/add <内容>;也可以直接说「记住 ……」,模型会自动识别为录入。"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.add(new UserMessage(shortPreview(`/add ${content}`)));
|
|
181
|
+
await this.addNote(content);
|
|
55
182
|
return;
|
|
56
183
|
}
|
|
57
|
-
if (value
|
|
58
|
-
|
|
184
|
+
if (value.startsWith("/")) {
|
|
185
|
+
await this.command(value);
|
|
59
186
|
return;
|
|
60
187
|
}
|
|
61
|
-
|
|
62
|
-
|
|
188
|
+
this.turns += 1;
|
|
189
|
+
this.add(new UserMessage(value));
|
|
190
|
+
await this.run(value);
|
|
191
|
+
}
|
|
192
|
+
async run(value) {
|
|
193
|
+
const pathMatch = value.match(/(?:录入|导入|整理|ingest|import)\s+(.+)$/i);
|
|
194
|
+
if (pathMatch) {
|
|
195
|
+
await this.ingest(pathMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
63
196
|
return;
|
|
64
197
|
}
|
|
65
|
-
|
|
66
|
-
|
|
198
|
+
await this.ask(value);
|
|
199
|
+
}
|
|
200
|
+
// ---------- 直接录入文本 ----------
|
|
201
|
+
async addNote(content, explicitTitle = "") {
|
|
202
|
+
const body = content.trim();
|
|
203
|
+
if (!body) {
|
|
204
|
+
this.add(new Notice(ROLE.warn, color.warn, "没有可录入的内容。"));
|
|
67
205
|
return;
|
|
68
206
|
}
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
207
|
+
try {
|
|
208
|
+
const title = explicitTitle.trim() || deriveTitle(body);
|
|
209
|
+
const file = writeNote(this.store.root, title, body);
|
|
210
|
+
const result = this.store.addOrUpdateSource(file, title, body);
|
|
211
|
+
this.add(new Notice(ROLE.ok, color.ok, `已录入「${result.title}」(v${result.version},${body.length} 字符)`));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
this.add(new Notice(ROLE.err, color.err, `录入失败:${errText(error)}`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ---------- 导入 ----------
|
|
218
|
+
async ingest(path) {
|
|
219
|
+
try {
|
|
220
|
+
this.setBusy(true, "读取文件…");
|
|
221
|
+
const source = readLocalFile(path);
|
|
222
|
+
if (!source.hash) {
|
|
223
|
+
this.add(new Notice(ROLE.err, color.err, `${source.title}:无法解析(${source.content})`));
|
|
224
|
+
return;
|
|
75
225
|
}
|
|
76
|
-
|
|
77
|
-
|
|
226
|
+
let content = source.content;
|
|
227
|
+
let note = "";
|
|
228
|
+
if (source.image) {
|
|
229
|
+
this.setBusy(true, "识别图片并生成描述…");
|
|
230
|
+
try {
|
|
231
|
+
const caption = await describeImage(this.model, fetchTransport, source.image);
|
|
232
|
+
if (caption) {
|
|
233
|
+
content = `${source.content}\n\n${caption}`;
|
|
234
|
+
note = ",已生成图片描述";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
note = ",图片描述生成失败(仅按文件名检索)";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const copied = copyOriginal(source.originalPath, this.store.root);
|
|
242
|
+
const result = this.store.addOrUpdateSource(copied, source.title, content);
|
|
243
|
+
this.add(new Notice(ROLE.ok, color.ok, `${result.isNew ? "已导入" : "已创建新版本"} ${result.title}(v${result.version})${note}`));
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.add(new Notice(ROLE.err, color.err, `导入失败:${errText(error)}`));
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
this.setBusy(false);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ---------- 问答 ----------
|
|
253
|
+
async ask(question) {
|
|
254
|
+
const config = this.model;
|
|
255
|
+
if (!config) {
|
|
256
|
+
this.add(new Notice(ROLE.err, color.err, "没有可用模型,请先配置模型。"));
|
|
78
257
|
return;
|
|
79
258
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
259
|
+
const started = Date.now();
|
|
260
|
+
try {
|
|
261
|
+
this.setBusy(true, "检索知识库…");
|
|
262
|
+
const results = this.store.search(question);
|
|
263
|
+
const context = results.length
|
|
264
|
+
? results.map((item) => `[${item.id}] ${item.title} (source: ${item.source}, v${item.version})\n${item.content}`).join("\n\n")
|
|
265
|
+
: "(没有检索到相关内容)";
|
|
266
|
+
const attached = [];
|
|
267
|
+
const questionBlocks = [{ type: "text", text: question }];
|
|
268
|
+
for (const item of results.filter((hit) => hit.originalPath && isImageFile(hit.originalPath)).slice(0, 4)) {
|
|
269
|
+
try {
|
|
270
|
+
const image = readImage(item.originalPath);
|
|
271
|
+
questionBlocks.push({ type: "text", text: `[附图] ${item.source} v${item.version}` });
|
|
272
|
+
questionBlocks.push({ type: "image", mediaType: image.mediaType, base64: image.base64 });
|
|
273
|
+
attached.push({ name: item.source, version: item.version });
|
|
274
|
+
}
|
|
275
|
+
catch { /* 跳过无法读取的图片 */ }
|
|
85
276
|
}
|
|
86
|
-
|
|
87
|
-
|
|
277
|
+
this.setBusy(true, attached.length ? `命中 ${results.length} 条,附图 ${attached.length} 张,正在处理…` : `检索到 ${results.length} 条,正在理解意图…`);
|
|
278
|
+
const reply = await fetchTransport.complete(config, [
|
|
279
|
+
{ role: "system", content: buildSystemPrompt(context) },
|
|
280
|
+
...this.messages,
|
|
281
|
+
{ role: "user", content: questionBlocks.length > 1 ? questionBlocks : question },
|
|
282
|
+
], [SAVE_KNOWLEDGE_TOOL]);
|
|
283
|
+
const save = reply.toolCalls?.find((call) => call.name === SAVE_KNOWLEDGE_TOOL.name);
|
|
284
|
+
if (save) {
|
|
285
|
+
this.setBusy(true, "正在录入知识库…");
|
|
286
|
+
await this.addNote(String(save.arguments.content ?? ""), String(save.arguments.title ?? ""));
|
|
88
287
|
return;
|
|
89
288
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
289
|
+
const answer = (reply.text ?? "").trim();
|
|
290
|
+
this.messages = [...this.messages, { role: "user", content: question }, { role: "assistant", content: answer }];
|
|
291
|
+
this.store.saveMessage("user", question);
|
|
292
|
+
this.store.saveMessage("assistant", answer);
|
|
293
|
+
for (const item of attached)
|
|
294
|
+
this.add(new ImageNotice(item.name, item.version));
|
|
295
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
296
|
+
const meta = `检索 ${results.length} 条 · 用时 ${seconds}s` + (attached.length ? ` · 附图 ${attached.length} 张` : "");
|
|
297
|
+
this.add(new AssistantMessage(answer || "(模型返回空回答)", meta));
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.add(new Notice(ROLE.err, color.err, `模型请求失败:${errText(error)}`));
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
this.setBusy(false);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ---------- 命令 ----------
|
|
307
|
+
async command(value) {
|
|
308
|
+
const [cmd, ...rest] = value.split(/\s+/);
|
|
309
|
+
const arg = rest.join(" ");
|
|
310
|
+
switch (cmd) {
|
|
311
|
+
case "/quit":
|
|
312
|
+
case "/exit":
|
|
313
|
+
this.shutdown();
|
|
93
314
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
315
|
+
case "/help":
|
|
316
|
+
this.showOverlay(new HelpPanel(HELP_ITEMS), 66);
|
|
317
|
+
return;
|
|
318
|
+
case "/clear":
|
|
319
|
+
this.stream.clear();
|
|
320
|
+
this.messages = [];
|
|
321
|
+
this.turns = 0;
|
|
322
|
+
this.add(new Notice(ROLE.ok, color.ok, "已清空对话上下文。"));
|
|
323
|
+
return;
|
|
324
|
+
case "/sources": return this.showSources();
|
|
325
|
+
case "/model":
|
|
326
|
+
case "/models":
|
|
327
|
+
if (!rest.length) {
|
|
328
|
+
this.showModelPicker();
|
|
99
329
|
return;
|
|
100
330
|
}
|
|
101
|
-
|
|
102
|
-
storeRef.current = new KnowledgeStore(selected.path);
|
|
103
|
-
setCurrentBase(selected);
|
|
104
|
-
setMessages([]);
|
|
105
|
-
setLines((old) => [...old, `Using ${selected.name}`]);
|
|
331
|
+
this.switchModel(Number(rest[0]));
|
|
106
332
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
333
|
+
case "/kb": return this.kbCommand(rest, arg);
|
|
334
|
+
case "/backup": {
|
|
335
|
+
const target = backupKnowledgeBase(this.base, arg || undefined);
|
|
336
|
+
this.add(new Notice(ROLE.ok, color.ok, `已备份到 ${target}`));
|
|
111
337
|
return;
|
|
112
338
|
}
|
|
113
|
-
|
|
114
|
-
|
|
339
|
+
case "/restore":
|
|
340
|
+
if (!arg) {
|
|
341
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/restore <备份目录>"));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
restoreKnowledgeBase(this.base, arg);
|
|
345
|
+
this.add(new Notice(ROLE.ok, color.ok, "已恢复。重启 kb 后重新打开数据库。"));
|
|
346
|
+
return;
|
|
347
|
+
case "/cleanup": return this.cleanup(rest);
|
|
348
|
+
default:
|
|
349
|
+
this.add(new Notice(ROLE.warn, color.warn, `未知命令 ${cmd},输入 /help 查看可用命令。`));
|
|
115
350
|
}
|
|
116
|
-
|
|
117
|
-
|
|
351
|
+
}
|
|
352
|
+
showSources() {
|
|
353
|
+
const sources = this.store.sources();
|
|
354
|
+
if (!sources.length) {
|
|
355
|
+
this.add(new Notice(ROLE.warn, color.warn, "知识库还是空的,用「导入 <文件路径>」录入内容。"));
|
|
118
356
|
return;
|
|
119
357
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
358
|
+
this.add(new Notice(ROLE.ok, color.ok, `共 ${sources.length} 个来源:`));
|
|
359
|
+
for (const source of sources) {
|
|
360
|
+
this.add(new Notice(" ", color.muted, `${String(source.name)} v${String(source.version)} ${String(source.updatedAt)}`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
showModelPicker() {
|
|
364
|
+
if (!this.models.length) {
|
|
365
|
+
this.add(new Notice(ROLE.err, color.err, "没有已配置的模型。"));
|
|
123
366
|
return;
|
|
124
367
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
368
|
+
const items = this.models.map((m, index) => ({
|
|
369
|
+
value: String(index),
|
|
370
|
+
label: `${m.provider}/${m.model}`,
|
|
371
|
+
description: `${m.api}${index === this.modelIndex ? " ← 当前" : ""}`,
|
|
372
|
+
}));
|
|
373
|
+
const list = new SelectList(items, Math.min(items.length, 10), selectListTheme);
|
|
374
|
+
list.onSelect = (item) => { this.switchModel(Number(item.value)); this.closeOverlay(); };
|
|
375
|
+
list.onCancel = () => { this.closeOverlay(); };
|
|
376
|
+
this.showOverlay(list, 68);
|
|
377
|
+
}
|
|
378
|
+
switchModel(index) {
|
|
379
|
+
const next = this.models[index];
|
|
380
|
+
if (!next) {
|
|
381
|
+
this.add(new Notice(ROLE.err, color.err, `没有编号为 ${index} 的模型。`));
|
|
128
382
|
return;
|
|
129
383
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
384
|
+
this.modelIndex = index;
|
|
385
|
+
this.models = [next, ...this.models.filter((_, i) => i !== index)];
|
|
386
|
+
saveModels(this.models);
|
|
387
|
+
this.add(new Notice(ROLE.ok, color.ok, `已切换到 ${next.provider}/${next.model}`));
|
|
388
|
+
}
|
|
389
|
+
kbCommand(rest, arg) {
|
|
390
|
+
const [sub, ...args] = rest;
|
|
391
|
+
if (!sub || sub === "list") {
|
|
392
|
+
const bases = listKnowledgeBases(this.root);
|
|
393
|
+
this.add(new Notice(ROLE.ok, color.ok, `共 ${bases.length} 个知识库:`));
|
|
394
|
+
for (const item of bases) {
|
|
395
|
+
this.add(new Notice(" ", color.muted, `${item.id}${item.id === this.base.id ? " ← 当前" : ""}`));
|
|
135
396
|
}
|
|
136
|
-
else
|
|
137
|
-
setLines((old) => [...old, "Usage: /cleanup <version-id> confirm"]);
|
|
138
397
|
return;
|
|
139
398
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const result = await ingestOrAsk(value, store, config[modelIndex], messages);
|
|
144
|
-
setMessages(result.messages);
|
|
145
|
-
store.saveMessage("user", value);
|
|
146
|
-
store.saveMessage("assistant", result.text);
|
|
147
|
-
setLines((old) => [...old, result.text]);
|
|
399
|
+
if (sub === "current") {
|
|
400
|
+
this.add(new Notice(ROLE.ok, color.ok, `${this.base.name}(${this.base.id})`));
|
|
401
|
+
return;
|
|
148
402
|
}
|
|
149
|
-
|
|
150
|
-
|
|
403
|
+
if (sub === "create") {
|
|
404
|
+
if (!args.length) {
|
|
405
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb create <名称>"));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const created = ensureKnowledgeBase(this.root, args.join(" "));
|
|
409
|
+
this.add(new Notice(ROLE.ok, color.ok, `已创建 ${created.name}(${created.id})`));
|
|
410
|
+
return;
|
|
151
411
|
}
|
|
152
|
-
|
|
153
|
-
|
|
412
|
+
if (sub === "use") {
|
|
413
|
+
const selected = listKnowledgeBases(this.root).find((item) => item.id === args[0] || item.name === args.join(" "));
|
|
414
|
+
if (!selected) {
|
|
415
|
+
this.add(new Notice(ROLE.err, color.err, `未找到知识库 ${arg}`));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.store.close();
|
|
419
|
+
this.store = new KnowledgeStore(selected.path);
|
|
420
|
+
this.base = selected;
|
|
421
|
+
this.messages = [];
|
|
422
|
+
this.turns = 0;
|
|
423
|
+
this.add(new Notice(ROLE.ok, color.ok, `已切换到知识库 ${selected.name}(${this.store.sources().length} 个来源)`));
|
|
424
|
+
return;
|
|
154
425
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (pathMatch) {
|
|
161
|
-
const source = readLocalFile(pathMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
162
|
-
if (!source.hash)
|
|
163
|
-
return { text: `${source.title}: unable to parse (${source.content})`, messages: previous };
|
|
164
|
-
let content = source.content;
|
|
165
|
-
let note = "";
|
|
166
|
-
if (source.image) {
|
|
426
|
+
if (sub === "delete") {
|
|
427
|
+
if (!args[0] || args[1] !== "confirm") {
|
|
428
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb delete <id> confirm"));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
167
431
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
content = `${source.content}\n\n${caption}`;
|
|
171
|
-
note = ",已生成图片描述";
|
|
172
|
-
}
|
|
432
|
+
deleteKnowledgeBase(this.root, args[0]);
|
|
433
|
+
this.add(new Notice(ROLE.ok, color.ok, `已删除 ${args[0]},备份已保留。`));
|
|
173
434
|
}
|
|
174
435
|
catch (error) {
|
|
175
|
-
|
|
436
|
+
this.add(new Notice(ROLE.err, color.err, errText(error)));
|
|
176
437
|
}
|
|
438
|
+
return;
|
|
177
439
|
}
|
|
178
|
-
|
|
179
|
-
const result = store.addOrUpdateSource(copied, source.title, content);
|
|
180
|
-
return { text: `${result.isNew ? "Imported" : "New version created for"} ${result.title} (v${result.version})${note}`, messages: previous };
|
|
440
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb list | /kb create <名称> | /kb use <id> | /kb current | /kb delete <id> confirm"));
|
|
181
441
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
442
|
+
cleanup(rest) {
|
|
443
|
+
if (!rest.length) {
|
|
444
|
+
const old = this.store.oldVersions();
|
|
445
|
+
if (!old.length) {
|
|
446
|
+
this.add(new Notice(ROLE.ok, color.ok, "没有历史版本。"));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
this.add(new Notice(ROLE.warn, color.warn, `历史版本 ${old.length} 个,用 /cleanup <版本ID> confirm 删除:`));
|
|
450
|
+
for (const item of old)
|
|
451
|
+
this.add(new Notice(" ", color.muted, `${String(item.id)} ${String(item.title)} v${String(item.version)}`));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const [id, confirmation] = rest;
|
|
455
|
+
if (confirmation !== "confirm") {
|
|
456
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/cleanup <版本ID> confirm"));
|
|
457
|
+
return;
|
|
191
458
|
}
|
|
192
|
-
|
|
459
|
+
this.store.removeVersions([id]);
|
|
460
|
+
this.add(new Notice(ROLE.ok, color.ok, `已删除历史版本 ${id}。`));
|
|
193
461
|
}
|
|
194
|
-
const agent = new Agent(store, modelConfig, fetchTransport);
|
|
195
|
-
return agent.answer(blocks.length > 1 ? blocks : prompt, previous);
|
|
196
462
|
}
|
|
197
463
|
export async function main() {
|
|
198
464
|
const root = defaultDataRoot();
|
|
199
465
|
mkdirSync(root, { recursive: true });
|
|
200
|
-
const knowledgeRoot = root;
|
|
201
466
|
const base = ensureKnowledgeBase(root, "default");
|
|
202
|
-
const
|
|
203
|
-
|
|
467
|
+
const models = await ensureModelConfig();
|
|
468
|
+
const app = new KbApp(root, base, models);
|
|
469
|
+
app.start();
|
|
204
470
|
}
|