@ganziliang/kb 0.1.7 → 0.2.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 +63 -2
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +336 -150
- 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/README.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
本地知识库命令行工具。它会把本地 Markdown、文本、JSON、CSV 或 Excel 文件导入 SQLite 知识库,然后通过关键词检索相关内容,并调用已配置的模型回答问题。同时支持导入图片:入库时自动生成中文描述参与检索,提问命中后把原图发给模型。
|
|
4
4
|
|
|
5
|
+
界面基于 [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui) 构建,采用差分渲染:Markdown 回答排版、带边框的编辑器、加载动画与命令浮层。
|
|
6
|
+
|
|
7
|
+
> **0.2.0 提示**:这是界面重写版本,Node.js 要求提升到 `>=22.19.0`。存储格式与 `~/.kb` 数据目录未变,旧知识库可直接使用。详见[常见问题](#从-01x-升级到-020-后无法启动)。
|
|
8
|
+
|
|
5
9
|
## 环境要求
|
|
6
10
|
|
|
7
|
-
- Node.js `>=22.
|
|
11
|
+
- Node.js `>=22.19.0`(当前 UI 依赖的终端渲染库要求,低于此版本无法启动)
|
|
8
12
|
- 如果使用模型问答,需要可用的模型 API Key
|
|
9
13
|
- 首次启动需要配置模型
|
|
10
14
|
|
|
@@ -105,7 +109,48 @@ kb
|
|
|
105
109
|
|
|
106
110
|
## 交互操作
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
启动后界面分为三部分:顶部的启动屏(logo 与环境信息)、中间的对话流、底部的输入框。
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
██╗ ██╗ ██████╗
|
|
116
|
+
██║ ██╔╝ ██╔══██╗
|
|
117
|
+
█████╔╝ ██████╔╝
|
|
118
|
+
██╔═██╗ ██╔══██╗
|
|
119
|
+
██║ ██╗ ██████╔╝
|
|
120
|
+
╚═╝ ╚═╝ ╚═════╝
|
|
121
|
+
|
|
122
|
+
知识库 · Knowledge Base
|
|
123
|
+
|
|
124
|
+
知识库 default
|
|
125
|
+
模型 deepseek-flash
|
|
126
|
+
来源 1
|
|
127
|
+
|
|
128
|
+
输入问题开始,或键入 /help 查看命令
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
对话流中不同类型的消息有各自的标记:
|
|
132
|
+
|
|
133
|
+
| 标记 | 含义 |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `❯` | 你的提问 |
|
|
136
|
+
| `⏺` | 模型回答(Markdown 排版,附检索统计与耗时) |
|
|
137
|
+
| `✔` | 成功(导入完成、命令结果) |
|
|
138
|
+
| `★` | 本次提问附带的原图 |
|
|
139
|
+
| `✖` | 错误 |
|
|
140
|
+
| `!` | 用法提示 |
|
|
141
|
+
|
|
142
|
+
快捷键:
|
|
143
|
+
|
|
144
|
+
| 按键 | 作用 |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `Enter` | 发送 |
|
|
147
|
+
| `Esc` | 关闭浮层 |
|
|
148
|
+
| `Ctrl+C` | 退出 |
|
|
149
|
+
| `Ctrl+A` / `Ctrl+E` | 行首 / 行尾 |
|
|
150
|
+
| `Ctrl+U` / `Ctrl+K` | 删除到行首 / 行尾 |
|
|
151
|
+
| `Ctrl+W` | 删除前一个单词 |
|
|
152
|
+
|
|
153
|
+
输入框支持多行编辑与撤销:`Ctrl+Z` 撤销,`Ctrl+Y` 重做。
|
|
109
154
|
|
|
110
155
|
### 导入文件
|
|
111
156
|
|
|
@@ -257,6 +302,22 @@ npm test
|
|
|
257
302
|
|
|
258
303
|
## 常见问题
|
|
259
304
|
|
|
305
|
+
### 从 0.1.x 升级到 0.2.0 后无法启动
|
|
306
|
+
|
|
307
|
+
0.2.0 换用了新的终端渲染库,Node.js 要求从 `>=22.5.0` 提升到 `>=22.19.0`。
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
node -v # 低于 v22.19.0 会启动失败
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
升级 Node 后重装即可:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm i -g @ganziliang/kb@latest
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
0.2.0 是界面重写,存储格式和 `~/.kb` 数据目录没有变化,原有知识库和模型配置可以直接沿用。
|
|
320
|
+
|
|
260
321
|
### 运行 `kb` 找不到命令
|
|
261
322
|
|
|
262
323
|
确认 npm 全局 bin 目录已经加入 PATH,或者使用源码方式运行:
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { ensureModelConfig } from "./config.js";
|
|
3
|
+
import { type KnowledgeBase } from "./storage.js";
|
|
4
|
+
type ModelConfig = Awaited<ReturnType<typeof ensureModelConfig>>[number];
|
|
5
|
+
export declare class KbApp {
|
|
6
|
+
private readonly root;
|
|
7
|
+
private readonly tui;
|
|
8
|
+
private readonly stream;
|
|
9
|
+
private readonly editor;
|
|
10
|
+
private store;
|
|
11
|
+
private base;
|
|
12
|
+
private models;
|
|
13
|
+
private modelIndex;
|
|
14
|
+
private messages;
|
|
15
|
+
private turns;
|
|
16
|
+
private busy;
|
|
17
|
+
private loader;
|
|
18
|
+
private overlay;
|
|
19
|
+
private closed;
|
|
20
|
+
constructor(root: string, base: KnowledgeBase, models: ModelConfig[]);
|
|
21
|
+
start(): void;
|
|
22
|
+
private get model();
|
|
23
|
+
private add;
|
|
24
|
+
private setBusy;
|
|
25
|
+
private showOverlay;
|
|
26
|
+
private closeOverlay;
|
|
27
|
+
private shutdown;
|
|
28
|
+
private submit;
|
|
29
|
+
private run;
|
|
30
|
+
private ingest;
|
|
31
|
+
private ask;
|
|
32
|
+
private command;
|
|
33
|
+
private showSources;
|
|
34
|
+
private showModelPicker;
|
|
35
|
+
private switchModel;
|
|
36
|
+
private kbCommand;
|
|
37
|
+
private cleanup;
|
|
38
|
+
}
|
|
2
39
|
export declare function main(): Promise<void>;
|
|
40
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,204 +1,390 @@
|
|
|
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
5
|
import { Agent, describeImage, fetchTransport } from "./model.js";
|
|
8
|
-
import { copyOriginal, defaultDataRoot, ensureKnowledgeBase, isImageFile, KnowledgeStore,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
6
|
+
import { backupKnowledgeBase, copyOriginal, defaultDataRoot, deleteKnowledgeBase, ensureKnowledgeBase, isImageFile, KnowledgeStore, listKnowledgeBases, readImage, readLocalFile, restoreKnowledgeBase, } 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
|
+
const HELP_ITEMS = [
|
|
11
|
+
["/help", "显示本帮助"],
|
|
12
|
+
["/clear", "清空对话上下文"],
|
|
13
|
+
["/sources", "列出知识来源与版本"],
|
|
14
|
+
["/model", "查看 / 切换模型"],
|
|
15
|
+
["/kb", "多知识库管理"],
|
|
16
|
+
["/backup", "备份当前知识库"],
|
|
17
|
+
["/restore", "��备份恢复"],
|
|
18
|
+
["/cleanup", "清理历史版本"],
|
|
19
|
+
["/quit", "退出"],
|
|
20
|
+
["", ""],
|
|
21
|
+
["导入 <路径>", "录入 md / txt / json / csv / xlsx / png / jpg"],
|
|
16
22
|
];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const ROLE = { user: "❯", assistant: "⏺", ok: "✔", err: "✖", warn: "!", image: "★" };
|
|
24
|
+
export class KbApp {
|
|
25
|
+
root;
|
|
26
|
+
tui;
|
|
27
|
+
stream = new Container();
|
|
28
|
+
editor;
|
|
29
|
+
store;
|
|
30
|
+
base;
|
|
31
|
+
models;
|
|
32
|
+
modelIndex = 0;
|
|
33
|
+
messages = [];
|
|
34
|
+
turns = 0;
|
|
35
|
+
busy = false;
|
|
36
|
+
loader = null;
|
|
37
|
+
overlay = null;
|
|
38
|
+
closed = false;
|
|
39
|
+
constructor(root, base, models) {
|
|
40
|
+
this.root = root;
|
|
41
|
+
this.base = base;
|
|
42
|
+
this.models = models;
|
|
43
|
+
this.store = new KnowledgeStore(base.path);
|
|
44
|
+
this.tui = new TuiMainScreen(new ProcessTerminal());
|
|
45
|
+
this.editor = new Editor(this.tui, editorTheme, { paddingX: 1 });
|
|
46
|
+
this.editor.onSubmit = (text) => { void this.submit(text.trim()); };
|
|
47
|
+
}
|
|
48
|
+
// ---------- 启动 ----------
|
|
49
|
+
start() {
|
|
50
|
+
this.tui.addChild(new Splash({
|
|
51
|
+
base: this.base.name,
|
|
52
|
+
model: this.model?.model ?? "-",
|
|
53
|
+
dataPath: this.root,
|
|
54
|
+
sources: this.store.sources().length,
|
|
55
|
+
}));
|
|
56
|
+
this.tui.addChild(this.stream);
|
|
57
|
+
this.tui.addChild(new Spacer(1));
|
|
58
|
+
this.tui.addChild(new Rule());
|
|
59
|
+
this.tui.addChild(this.editor);
|
|
60
|
+
this.tui.setFocus(this.editor);
|
|
61
|
+
this.tui.addInputListener((data) => {
|
|
62
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
63
|
+
this.shutdown();
|
|
64
|
+
return { consume: true };
|
|
65
|
+
}
|
|
66
|
+
if (matchesKey(data, "escape") && this.tui.hasOverlay()) {
|
|
67
|
+
this.closeOverlay();
|
|
68
|
+
return { consume: true };
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
});
|
|
72
|
+
this.tui.start();
|
|
73
|
+
}
|
|
74
|
+
get model() { return this.models[this.modelIndex]; }
|
|
75
|
+
// ---------- 渲染原语 ----------
|
|
76
|
+
add(component) {
|
|
77
|
+
this.stream.addChild(component);
|
|
78
|
+
this.tui.requestRender();
|
|
79
|
+
}
|
|
80
|
+
setBusy(on, message = "") {
|
|
81
|
+
if (on) {
|
|
82
|
+
this.editor.disableSubmit = true;
|
|
83
|
+
if (this.loader) {
|
|
84
|
+
this.loader.setMessage(message);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.loader = new Loader(this.tui, color.accent, color.muted, message);
|
|
88
|
+
this.stream.addChild(this.loader);
|
|
89
|
+
this.loader.start();
|
|
90
|
+
}
|
|
33
91
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
92
|
+
else {
|
|
93
|
+
this.editor.disableSubmit = false;
|
|
94
|
+
if (this.loader) {
|
|
95
|
+
this.loader.stop();
|
|
96
|
+
this.stream.removeChild(this.loader);
|
|
97
|
+
this.loader = null;
|
|
98
|
+
}
|
|
38
99
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
100
|
+
this.tui.requestRender();
|
|
101
|
+
}
|
|
102
|
+
showOverlay(component, width) {
|
|
103
|
+
this.closeOverlay();
|
|
104
|
+
this.overlay = this.tui.showOverlay(component, { width, anchor: "center", margin: 1 });
|
|
105
|
+
}
|
|
106
|
+
closeOverlay() {
|
|
107
|
+
if (this.overlay) {
|
|
108
|
+
this.overlay.hide();
|
|
109
|
+
this.overlay = null;
|
|
42
110
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (value === "/quit" || value === "/exit") {
|
|
48
|
-
store.close();
|
|
49
|
-
exit();
|
|
111
|
+
this.tui.setFocus(this.editor);
|
|
112
|
+
}
|
|
113
|
+
shutdown() {
|
|
114
|
+
if (this.closed)
|
|
50
115
|
return;
|
|
116
|
+
this.closed = true;
|
|
117
|
+
try {
|
|
118
|
+
this.store.close();
|
|
51
119
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
120
|
+
catch { /* 已关闭 */ }
|
|
121
|
+
this.tui.stop();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
// ---------- 输入入口 ----------
|
|
125
|
+
async submit(value) {
|
|
126
|
+
if (this.busy || !value)
|
|
55
127
|
return;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
128
|
+
this.editor.setText("");
|
|
129
|
+
this.tui.requestRender();
|
|
130
|
+
if (value.startsWith("/")) {
|
|
131
|
+
await this.command(value);
|
|
59
132
|
return;
|
|
60
133
|
}
|
|
61
|
-
|
|
62
|
-
|
|
134
|
+
this.turns += 1;
|
|
135
|
+
this.add(new UserMessage(value));
|
|
136
|
+
await this.run(value);
|
|
137
|
+
}
|
|
138
|
+
async run(value) {
|
|
139
|
+
const pathMatch = value.match(/(?:录入|导入|整理|ingest|import)\s+(.+)$/i);
|
|
140
|
+
if (pathMatch) {
|
|
141
|
+
await this.ingest(pathMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
63
142
|
return;
|
|
64
143
|
}
|
|
65
|
-
|
|
66
|
-
|
|
144
|
+
await this.ask(value);
|
|
145
|
+
}
|
|
146
|
+
// ---------- 导入 ----------
|
|
147
|
+
async ingest(path) {
|
|
148
|
+
try {
|
|
149
|
+
this.setBusy(true, "读取文件…");
|
|
150
|
+
const source = readLocalFile(path);
|
|
151
|
+
if (!source.hash) {
|
|
152
|
+
this.add(new Notice(ROLE.err, color.err, `${source.title}:无法解析(${source.content})`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let content = source.content;
|
|
156
|
+
let note = "";
|
|
157
|
+
if (source.image) {
|
|
158
|
+
this.setBusy(true, "识别图片并生成描述…");
|
|
159
|
+
try {
|
|
160
|
+
const caption = await describeImage(this.model, fetchTransport, source.image);
|
|
161
|
+
if (caption) {
|
|
162
|
+
content = `${source.content}\n\n${caption}`;
|
|
163
|
+
note = ",已生成图片描述";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
note = ",图片描述生成失败(仅按文件名检索)";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const copied = copyOriginal(source.originalPath, this.store.root);
|
|
171
|
+
const result = this.store.addOrUpdateSource(copied, source.title, content);
|
|
172
|
+
this.add(new Notice(ROLE.ok, color.ok, `${result.isNew ? "已导入" : "已创建新版本"} ${result.title}(v${result.version})${note}`));
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.add(new Notice(ROLE.err, color.err, `导入失败:${errText(error)}`));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
this.setBusy(false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ---------- 问答 ----------
|
|
182
|
+
async ask(question) {
|
|
183
|
+
const config = this.model;
|
|
184
|
+
if (!config) {
|
|
185
|
+
this.add(new Notice(ROLE.err, color.err, "没有可用模型,请先配置模型。"));
|
|
67
186
|
return;
|
|
68
187
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
188
|
+
const started = Date.now();
|
|
189
|
+
try {
|
|
190
|
+
this.setBusy(true, "检索知识库…");
|
|
191
|
+
const results = this.store.search(question);
|
|
192
|
+
const context = results.length
|
|
193
|
+
? results.map((item) => `[${item.id}] ${item.title} (source: ${item.source}, v${item.version})\n${item.content}`).join("\n\n")
|
|
194
|
+
: "No matching knowledge was found.";
|
|
195
|
+
const prompt = `Answer only from the local knowledge below. If it is insufficient, say no knowledge was found. Include source filename and version.\n\n${context}\n\nQuestion: ${question}`;
|
|
196
|
+
const blocks = [{ type: "text", text: prompt }];
|
|
197
|
+
const attached = [];
|
|
198
|
+
for (const item of results.filter((hit) => hit.originalPath && isImageFile(hit.originalPath)).slice(0, 4)) {
|
|
199
|
+
try {
|
|
200
|
+
const image = readImage(item.originalPath);
|
|
201
|
+
blocks.push({ type: "text", text: `[附图] ${item.source} v${item.version}` });
|
|
202
|
+
blocks.push({ type: "image", mediaType: image.mediaType, base64: image.base64 });
|
|
203
|
+
attached.push({ name: item.source, version: item.version });
|
|
204
|
+
}
|
|
205
|
+
catch { /* 跳过无法读取的图片 */ }
|
|
75
206
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
207
|
+
this.setBusy(true, attached.length ? `命中 ${results.length} 条,附图 ${attached.length} 张,正在生成回答…` : `命中 ${results.length} 条,正在生成回答…`);
|
|
208
|
+
const agent = new Agent(this.store, config, fetchTransport);
|
|
209
|
+
const answer = await agent.answer(blocks.length > 1 ? blocks : prompt, this.messages);
|
|
210
|
+
this.messages = answer.messages;
|
|
211
|
+
this.store.saveMessage("user", question);
|
|
212
|
+
this.store.saveMessage("assistant", answer.text);
|
|
213
|
+
for (const item of attached)
|
|
214
|
+
this.add(new ImageNotice(item.name, item.version));
|
|
215
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
216
|
+
const meta = `检索 ${results.length} 条 · 命中 ${results.length} 条 · ${seconds}s` + (attached.length ? ` · 附图 ${attached.length} 张` : "");
|
|
217
|
+
this.add(new AssistantMessage(answer.text || "(模型返回空回答)", meta));
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
this.add(new Notice(ROLE.err, color.err, `模型请求失败:${errText(error)}`));
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this.setBusy(false);
|
|
79
224
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
225
|
+
}
|
|
226
|
+
// ---------- 命令 ----------
|
|
227
|
+
async command(value) {
|
|
228
|
+
const [cmd, ...rest] = value.split(/\s+/);
|
|
229
|
+
const arg = rest.join(" ");
|
|
230
|
+
switch (cmd) {
|
|
231
|
+
case "/quit":
|
|
232
|
+
case "/exit":
|
|
233
|
+
this.shutdown();
|
|
84
234
|
return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
setLines((old) => [...old, `${currentBase.name} (${currentBase.id})`]);
|
|
235
|
+
case "/help":
|
|
236
|
+
this.showOverlay(new HelpPanel(HELP_ITEMS), 66);
|
|
88
237
|
return;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
238
|
+
case "/clear":
|
|
239
|
+
this.stream.clear();
|
|
240
|
+
this.messages = [];
|
|
241
|
+
this.turns = 0;
|
|
242
|
+
this.add(new Notice(ROLE.ok, color.ok, "已清空对话上下文。"));
|
|
93
243
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!
|
|
98
|
-
|
|
244
|
+
case "/sources": return this.showSources();
|
|
245
|
+
case "/model":
|
|
246
|
+
case "/models":
|
|
247
|
+
if (!rest.length) {
|
|
248
|
+
this.showModelPicker();
|
|
99
249
|
return;
|
|
100
250
|
}
|
|
101
|
-
|
|
102
|
-
storeRef.current = new KnowledgeStore(selected.path);
|
|
103
|
-
setCurrentBase(selected);
|
|
104
|
-
setMessages([]);
|
|
105
|
-
setLines((old) => [...old, `Using ${selected.name}`]);
|
|
251
|
+
this.switchModel(Number(rest[0]));
|
|
106
252
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
253
|
+
case "/kb": return this.kbCommand(rest, arg);
|
|
254
|
+
case "/backup": {
|
|
255
|
+
const target = backupKnowledgeBase(this.base, arg || undefined);
|
|
256
|
+
this.add(new Notice(ROLE.ok, color.ok, `已备份到 ${target}`));
|
|
111
257
|
return;
|
|
112
258
|
}
|
|
113
|
-
|
|
114
|
-
|
|
259
|
+
case "/restore":
|
|
260
|
+
if (!arg) {
|
|
261
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/restore <备份目录>"));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
restoreKnowledgeBase(this.base, arg);
|
|
265
|
+
this.add(new Notice(ROLE.ok, color.ok, "已恢复。重启 kb 后重新打开数据库。"));
|
|
266
|
+
return;
|
|
267
|
+
case "/cleanup": return this.cleanup(rest);
|
|
268
|
+
default:
|
|
269
|
+
this.add(new Notice(ROLE.warn, color.warn, `未知命令 ${cmd},输入 /help 查看可用命令。`));
|
|
115
270
|
}
|
|
116
|
-
|
|
117
|
-
|
|
271
|
+
}
|
|
272
|
+
showSources() {
|
|
273
|
+
const sources = this.store.sources();
|
|
274
|
+
if (!sources.length) {
|
|
275
|
+
this.add(new Notice(ROLE.warn, color.warn, "知识库还是空的,用「导入 <文件路径>」录入内容。"));
|
|
118
276
|
return;
|
|
119
277
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
278
|
+
this.add(new Notice(ROLE.ok, color.ok, `共 ${sources.length} 个来源:`));
|
|
279
|
+
for (const source of sources) {
|
|
280
|
+
this.add(new Notice(" ", color.muted, `${String(source.name)} v${String(source.version)} ${String(source.updatedAt)}`));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
showModelPicker() {
|
|
284
|
+
if (!this.models.length) {
|
|
285
|
+
this.add(new Notice(ROLE.err, color.err, "没有已配置的模型。"));
|
|
123
286
|
return;
|
|
124
287
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
288
|
+
const items = this.models.map((m, index) => ({
|
|
289
|
+
value: String(index),
|
|
290
|
+
label: `${m.provider}/${m.model}`,
|
|
291
|
+
description: `${m.api}${index === this.modelIndex ? " ← 当前" : ""}`,
|
|
292
|
+
}));
|
|
293
|
+
const list = new SelectList(items, Math.min(items.length, 10), selectListTheme);
|
|
294
|
+
list.onSelect = (item) => { this.switchModel(Number(item.value)); this.closeOverlay(); };
|
|
295
|
+
list.onCancel = () => { this.closeOverlay(); };
|
|
296
|
+
this.showOverlay(list, 68);
|
|
297
|
+
}
|
|
298
|
+
switchModel(index) {
|
|
299
|
+
const next = this.models[index];
|
|
300
|
+
if (!next) {
|
|
301
|
+
this.add(new Notice(ROLE.err, color.err, `没有编号为 ${index} 的模型。`));
|
|
128
302
|
return;
|
|
129
303
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
304
|
+
this.modelIndex = index;
|
|
305
|
+
this.models = [next, ...this.models.filter((_, i) => i !== index)];
|
|
306
|
+
saveModels(this.models);
|
|
307
|
+
this.add(new Notice(ROLE.ok, color.ok, `已切换到 ${next.provider}/${next.model}`));
|
|
308
|
+
}
|
|
309
|
+
kbCommand(rest, arg) {
|
|
310
|
+
const [sub, ...args] = rest;
|
|
311
|
+
if (!sub || sub === "list") {
|
|
312
|
+
const bases = listKnowledgeBases(this.root);
|
|
313
|
+
this.add(new Notice(ROLE.ok, color.ok, `共 ${bases.length} 个知识库:`));
|
|
314
|
+
for (const item of bases) {
|
|
315
|
+
this.add(new Notice(" ", color.muted, `${item.id}${item.id === this.base.id ? " ← 当前" : ""}`));
|
|
135
316
|
}
|
|
136
|
-
else
|
|
137
|
-
setLines((old) => [...old, "Usage: /cleanup <version-id> confirm"]);
|
|
138
317
|
return;
|
|
139
318
|
}
|
|
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]);
|
|
319
|
+
if (sub === "current") {
|
|
320
|
+
this.add(new Notice(ROLE.ok, color.ok, `${this.base.name}(${this.base.id})`));
|
|
321
|
+
return;
|
|
148
322
|
}
|
|
149
|
-
|
|
150
|
-
|
|
323
|
+
if (sub === "create") {
|
|
324
|
+
if (!args.length) {
|
|
325
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb create <名称>"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const created = ensureKnowledgeBase(this.root, args.join(" "));
|
|
329
|
+
this.add(new Notice(ROLE.ok, color.ok, `已创建 ${created.name}(${created.id})`));
|
|
330
|
+
return;
|
|
151
331
|
}
|
|
152
|
-
|
|
153
|
-
|
|
332
|
+
if (sub === "use") {
|
|
333
|
+
const selected = listKnowledgeBases(this.root).find((item) => item.id === args[0] || item.name === args.join(" "));
|
|
334
|
+
if (!selected) {
|
|
335
|
+
this.add(new Notice(ROLE.err, color.err, `未找到知识库 ${arg}`));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.store.close();
|
|
339
|
+
this.store = new KnowledgeStore(selected.path);
|
|
340
|
+
this.base = selected;
|
|
341
|
+
this.messages = [];
|
|
342
|
+
this.turns = 0;
|
|
343
|
+
this.add(new Notice(ROLE.ok, color.ok, `已切换到知识库 ${selected.name}(${this.store.sources().length} 个来源)`));
|
|
344
|
+
return;
|
|
154
345
|
}
|
|
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) {
|
|
346
|
+
if (sub === "delete") {
|
|
347
|
+
if (!args[0] || args[1] !== "confirm") {
|
|
348
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb delete <id> confirm"));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
167
351
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
content = `${source.content}\n\n${caption}`;
|
|
171
|
-
note = ",已生成图片描述";
|
|
172
|
-
}
|
|
352
|
+
deleteKnowledgeBase(this.root, args[0]);
|
|
353
|
+
this.add(new Notice(ROLE.ok, color.ok, `已删除 ${args[0]},备份已保留。`));
|
|
173
354
|
}
|
|
174
355
|
catch (error) {
|
|
175
|
-
|
|
356
|
+
this.add(new Notice(ROLE.err, color.err, errText(error)));
|
|
176
357
|
}
|
|
358
|
+
return;
|
|
177
359
|
}
|
|
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 };
|
|
360
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/kb list | /kb create <名称> | /kb use <id> | /kb current | /kb delete <id> confirm"));
|
|
181
361
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
362
|
+
cleanup(rest) {
|
|
363
|
+
if (!rest.length) {
|
|
364
|
+
const old = this.store.oldVersions();
|
|
365
|
+
if (!old.length) {
|
|
366
|
+
this.add(new Notice(ROLE.ok, color.ok, "没有历史版本。"));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.add(new Notice(ROLE.warn, color.warn, `历史版本 ${old.length} 个,用 /cleanup <版本ID> confirm 删除:`));
|
|
370
|
+
for (const item of old)
|
|
371
|
+
this.add(new Notice(" ", color.muted, `${String(item.id)} ${String(item.title)} v${String(item.version)}`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const [id, confirmation] = rest;
|
|
375
|
+
if (confirmation !== "confirm") {
|
|
376
|
+
this.add(new Notice(ROLE.warn, color.warn, "用法:/cleanup <版本ID> confirm"));
|
|
377
|
+
return;
|
|
191
378
|
}
|
|
192
|
-
|
|
379
|
+
this.store.removeVersions([id]);
|
|
380
|
+
this.add(new Notice(ROLE.ok, color.ok, `已删除历史版本 ${id}。`));
|
|
193
381
|
}
|
|
194
|
-
const agent = new Agent(store, modelConfig, fetchTransport);
|
|
195
|
-
return agent.answer(blocks.length > 1 ? blocks : prompt, previous);
|
|
196
382
|
}
|
|
197
383
|
export async function main() {
|
|
198
384
|
const root = defaultDataRoot();
|
|
199
385
|
mkdirSync(root, { recursive: true });
|
|
200
|
-
const knowledgeRoot = root;
|
|
201
386
|
const base = ensureKnowledgeBase(root, "default");
|
|
202
|
-
const
|
|
203
|
-
|
|
387
|
+
const models = await ensureModelConfig();
|
|
388
|
+
const app = new KbApp(root, base, models);
|
|
389
|
+
app.start();
|
|
204
390
|
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
|
|
2
|
+
export declare const fg: (r: number, g: number, b: number) => (s: string) => string;
|
|
3
|
+
export declare const bg: (r: number, g: number, b: number) => (s: string) => string;
|
|
4
|
+
export declare const bold: (s: string) => string;
|
|
5
|
+
export declare const dim: (s: string) => string;
|
|
6
|
+
export declare const italic: (s: string) => string;
|
|
7
|
+
export declare const underline: (s: string) => string;
|
|
8
|
+
export declare const strike: (s: string) => string;
|
|
9
|
+
/** 全局配色:slate/sky/teal 冷色调,深浅终端下都有足够对比度。 */
|
|
10
|
+
export declare const color: {
|
|
11
|
+
accent: (s: string) => string;
|
|
12
|
+
accent2: (s: string) => string;
|
|
13
|
+
user: (s: string) => string;
|
|
14
|
+
text: (s: string) => string;
|
|
15
|
+
muted: (s: string) => string;
|
|
16
|
+
dim: (s: string) => string;
|
|
17
|
+
border: (s: string) => string;
|
|
18
|
+
ok: (s: string) => string;
|
|
19
|
+
warn: (s: string) => string;
|
|
20
|
+
err: (s: string) => string;
|
|
21
|
+
chip: (s: string) => string;
|
|
22
|
+
userBg: (s: string) => string;
|
|
23
|
+
};
|
|
24
|
+
export declare const markdownTheme: MarkdownTheme;
|
|
25
|
+
export declare const selectListTheme: SelectListTheme;
|
|
26
|
+
export declare const editorTheme: EditorTheme;
|
|
27
|
+
export declare const imageTheme: ImageTheme;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const fg = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
2
|
+
export const bg = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
|
|
3
|
+
export const bold = (s) => `\x1b[1m${s}\x1b[22m`;
|
|
4
|
+
export const dim = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
5
|
+
export const italic = (s) => `\x1b[3m${s}\x1b[23m`;
|
|
6
|
+
export const underline = (s) => `\x1b[4m${s}\x1b[24m`;
|
|
7
|
+
export const strike = (s) => `\x1b[9m${s}\x1b[29m`;
|
|
8
|
+
/** 全局配色:slate/sky/teal 冷色调,深浅终端下都有足够对比度。 */
|
|
9
|
+
export const color = {
|
|
10
|
+
accent: fg(56, 189, 248),
|
|
11
|
+
accent2: fg(45, 212, 191),
|
|
12
|
+
user: fg(226, 232, 240),
|
|
13
|
+
text: fg(203, 213, 225),
|
|
14
|
+
muted: fg(100, 116, 139),
|
|
15
|
+
dim: fg(71, 85, 105),
|
|
16
|
+
border: fg(51, 65, 85),
|
|
17
|
+
ok: fg(74, 222, 128),
|
|
18
|
+
warn: fg(251, 191, 36),
|
|
19
|
+
err: fg(248, 113, 113),
|
|
20
|
+
chip: bg(30, 41, 59),
|
|
21
|
+
userBg: bg(15, 23, 42),
|
|
22
|
+
};
|
|
23
|
+
export const markdownTheme = {
|
|
24
|
+
heading: (t) => bold(color.accent2(t)),
|
|
25
|
+
link: (t) => color.accent(t),
|
|
26
|
+
linkUrl: (t) => dim(color.muted(t)),
|
|
27
|
+
code: (t) => color.warn(t),
|
|
28
|
+
codeBlock: (t) => color.text(t),
|
|
29
|
+
codeBlockBorder: (t) => color.border(t),
|
|
30
|
+
quote: (t) => dim(color.muted(t)),
|
|
31
|
+
quoteBorder: (t) => color.accent2(t),
|
|
32
|
+
hr: (t) => color.border(t),
|
|
33
|
+
listBullet: (t) => color.accent(t),
|
|
34
|
+
bold: (t) => bold(color.user(t)),
|
|
35
|
+
italic: (t) => italic(color.text(t)),
|
|
36
|
+
strikethrough: (t) => strike(color.text(t)),
|
|
37
|
+
underline: (t) => underline(color.text(t)),
|
|
38
|
+
};
|
|
39
|
+
export const selectListTheme = {
|
|
40
|
+
selectedPrefix: (t) => color.accent(bold(t)),
|
|
41
|
+
selectedText: (t) => bold(color.user(t)),
|
|
42
|
+
description: (t) => color.muted(t),
|
|
43
|
+
scrollInfo: (t) => dim(color.muted(t)),
|
|
44
|
+
noMatch: (t) => dim(color.muted(t)),
|
|
45
|
+
};
|
|
46
|
+
export const editorTheme = {
|
|
47
|
+
borderColor: (t) => color.border(t),
|
|
48
|
+
selectList: selectListTheme,
|
|
49
|
+
};
|
|
50
|
+
export const imageTheme = {
|
|
51
|
+
fallbackColor: (t) => color.muted(t),
|
|
52
|
+
};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { dim, selectListTheme } from "./theme.js";
|
|
3
|
+
/** 把文本按可见宽度换行;保证每行都不超过 width。 */
|
|
4
|
+
export declare function wrap(text: string, width: number): string[];
|
|
5
|
+
/** 通用多行文本组件,按渲染宽度截断,保证永不超宽。 */
|
|
6
|
+
export declare class Lines implements Component {
|
|
7
|
+
private lines;
|
|
8
|
+
constructor(lines?: string[]);
|
|
9
|
+
setLines(lines: string[]): void;
|
|
10
|
+
invalidate(): void;
|
|
11
|
+
render(width: number): string[];
|
|
12
|
+
}
|
|
13
|
+
/** 启动屏:logo + 环境信息。 */
|
|
14
|
+
export declare class Splash implements Component {
|
|
15
|
+
private info;
|
|
16
|
+
constructor(info: {
|
|
17
|
+
base: string;
|
|
18
|
+
model: string;
|
|
19
|
+
dataPath: string;
|
|
20
|
+
sources: number;
|
|
21
|
+
});
|
|
22
|
+
invalidate(): void;
|
|
23
|
+
render(width: number): string[];
|
|
24
|
+
}
|
|
25
|
+
/** 顶部状态条:带圆角边框的一行摘要。 */ export declare class StatusBar implements Component {
|
|
26
|
+
private info;
|
|
27
|
+
constructor(info: {
|
|
28
|
+
base: string;
|
|
29
|
+
model: string;
|
|
30
|
+
sources: number;
|
|
31
|
+
turns: number;
|
|
32
|
+
});
|
|
33
|
+
setInfo(info: {
|
|
34
|
+
base: string;
|
|
35
|
+
model: string;
|
|
36
|
+
sources: number;
|
|
37
|
+
turns: number;
|
|
38
|
+
}): void;
|
|
39
|
+
invalidate(): void;
|
|
40
|
+
render(width: number): string[];
|
|
41
|
+
}
|
|
42
|
+
/** 用户提问。 */
|
|
43
|
+
export declare class UserMessage implements Component {
|
|
44
|
+
private text;
|
|
45
|
+
constructor(text: string);
|
|
46
|
+
invalidate(): void;
|
|
47
|
+
render(width: number): string[];
|
|
48
|
+
}
|
|
49
|
+
/** 助手回答:图标 + Markdown 正文 + 元信息脚注。 */
|
|
50
|
+
export declare class AssistantMessage implements Component {
|
|
51
|
+
private meta;
|
|
52
|
+
private md;
|
|
53
|
+
constructor(text: string, meta?: string);
|
|
54
|
+
setMeta(meta: string): void;
|
|
55
|
+
invalidate(): void;
|
|
56
|
+
render(width: number): string[];
|
|
57
|
+
}
|
|
58
|
+
/** 单行提示(成功 / 信息 / 警告 / 错误)。 */
|
|
59
|
+
export declare class Notice implements Component {
|
|
60
|
+
private icon;
|
|
61
|
+
private style;
|
|
62
|
+
private text;
|
|
63
|
+
constructor(icon: string, style: (s: string) => string, text: string);
|
|
64
|
+
invalidate(): void;
|
|
65
|
+
render(width: number): string[];
|
|
66
|
+
}
|
|
67
|
+
/** 检索命中的附图标记。 */
|
|
68
|
+
export declare class ImageNotice implements Component {
|
|
69
|
+
private name;
|
|
70
|
+
private version;
|
|
71
|
+
constructor(name: string, version: number);
|
|
72
|
+
invalidate(): void;
|
|
73
|
+
render(width: number): string[];
|
|
74
|
+
}
|
|
75
|
+
/** 水平分隔线。 */
|
|
76
|
+
export declare class Rule implements Component {
|
|
77
|
+
invalidate(): void;
|
|
78
|
+
render(width: number): string[];
|
|
79
|
+
}
|
|
80
|
+
/** 帮助面板(作为 overlay 内容)。 */
|
|
81
|
+
export declare class HelpPanel implements Component {
|
|
82
|
+
private items;
|
|
83
|
+
constructor(items: Array<[string, string]>);
|
|
84
|
+
invalidate(): void;
|
|
85
|
+
render(width: number): string[];
|
|
86
|
+
}
|
|
87
|
+
/** 供 SelectList 复用的主题。 */
|
|
88
|
+
export { selectListTheme, dim };
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import { bold, color, dim, markdownTheme, selectListTheme } from "./theme.js";
|
|
3
|
+
/** 把文本按可见宽度换行;保证每行都不超过 width。 */
|
|
4
|
+
export function wrap(text, width) {
|
|
5
|
+
if (width <= 0)
|
|
6
|
+
return [""];
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const raw of text.split("\n")) {
|
|
9
|
+
if (raw === "") {
|
|
10
|
+
out.push("");
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
for (const piece of wrapTextWithAnsi(raw, width))
|
|
14
|
+
out.push(piece);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
|
|
19
|
+
/** 统一裁剪:保证组件输出永不超过给定宽度(pi-tui 会因超宽报错)。 */
|
|
20
|
+
const clip = (lines, width) => lines.map((l) => truncateToWidth(l, Math.max(0, width)));
|
|
21
|
+
/** 通用多行文本组件,按渲染宽度截断,保证永不超宽。 */
|
|
22
|
+
export class Lines {
|
|
23
|
+
lines = [];
|
|
24
|
+
constructor(lines = []) { this.lines = lines; }
|
|
25
|
+
setLines(lines) { this.lines = lines; }
|
|
26
|
+
invalidate() { }
|
|
27
|
+
render(width) {
|
|
28
|
+
return this.lines.map((l) => truncateToWidth(l, Math.max(0, width)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** 启动屏:logo + 环境信息。 */
|
|
32
|
+
export class Splash {
|
|
33
|
+
info;
|
|
34
|
+
constructor(info) {
|
|
35
|
+
this.info = info;
|
|
36
|
+
}
|
|
37
|
+
invalidate() { }
|
|
38
|
+
render(width) {
|
|
39
|
+
const logo = ["██╗ ██╗ ██████╗", "██║ ██╔╝ ██╔══██╗", "█████╔╝ ██████╔╝", "██╔═██╗ ██╔══██╗", "██║ ██╗ ██████╔╝", "╚═╝ ╚═╝ ╚═════╝ "];
|
|
40
|
+
const field = (k, v, val) => ` ${color.muted(pad(k, 8))}${val(v)}`;
|
|
41
|
+
const lines = [
|
|
42
|
+
"",
|
|
43
|
+
...logo.map((l) => " " + color.accent(bold(l))),
|
|
44
|
+
"",
|
|
45
|
+
" " + color.text(bold("知识库")) + color.border(" · ") + color.muted("Knowledge Base"),
|
|
46
|
+
"",
|
|
47
|
+
field("知识库", this.info.base, color.user),
|
|
48
|
+
field("模型", this.info.model, color.user),
|
|
49
|
+
field("来源", String(this.info.sources), color.user),
|
|
50
|
+
field("数据", this.info.dataPath, (s) => color.dim(s)),
|
|
51
|
+
"",
|
|
52
|
+
" " + color.dim("输入问题开始,或键入 ") + color.accent("/help") + color.dim(" 查看命令"),
|
|
53
|
+
"",
|
|
54
|
+
];
|
|
55
|
+
return lines.map((l) => truncateToWidth(l, Math.max(0, width)));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 顶部状态条:带圆角边框的一行摘要。 */ export class StatusBar {
|
|
59
|
+
info;
|
|
60
|
+
constructor(info) {
|
|
61
|
+
this.info = info;
|
|
62
|
+
}
|
|
63
|
+
setInfo(info) { this.info = info; }
|
|
64
|
+
invalidate() { }
|
|
65
|
+
render(width) {
|
|
66
|
+
const w = Math.max(0, width);
|
|
67
|
+
const inner = Math.max(0, w - 4);
|
|
68
|
+
const label = " KB ";
|
|
69
|
+
const top = color.border("╭─") + color.accent(bold(label)) + color.border("─".repeat(Math.max(0, w - 3 - visibleWidth(label))) + "╮");
|
|
70
|
+
const bot = color.border("╰" + "─".repeat(Math.max(0, w - 2)) + "╯");
|
|
71
|
+
const stat = (k, v) => `${color.muted(k)} ${color.user(v)}`;
|
|
72
|
+
const content = " " + [
|
|
73
|
+
stat("知识库", this.info.base),
|
|
74
|
+
stat("模型", this.info.model),
|
|
75
|
+
stat("来源", String(this.info.sources)),
|
|
76
|
+
stat("会话", String(this.info.turns)),
|
|
77
|
+
].join(color.border(" · "));
|
|
78
|
+
const row = color.border("│") + " " + pad(truncateToWidth(content, inner), inner) + " " + color.border("│");
|
|
79
|
+
return clip([top, row, bot], w);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** 用户提问。 */
|
|
83
|
+
export class UserMessage {
|
|
84
|
+
text;
|
|
85
|
+
constructor(text) {
|
|
86
|
+
this.text = text;
|
|
87
|
+
}
|
|
88
|
+
invalidate() { }
|
|
89
|
+
render(width) {
|
|
90
|
+
const prefix = ` ${color.accent2(bold("❯"))} `;
|
|
91
|
+
const body = wrap(this.text, Math.max(1, width - 4));
|
|
92
|
+
return body.map((l, i) => (i === 0 ? prefix + color.user(l) : " " + color.user(l)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** 助手回答:图标 + Markdown 正文 + 元信息脚注。 */
|
|
96
|
+
export class AssistantMessage {
|
|
97
|
+
meta;
|
|
98
|
+
md;
|
|
99
|
+
constructor(text, meta = "") {
|
|
100
|
+
this.meta = meta;
|
|
101
|
+
this.md = new Markdown(text, 0, 0, markdownTheme);
|
|
102
|
+
}
|
|
103
|
+
setMeta(meta) { this.meta = meta; }
|
|
104
|
+
invalidate() { this.md.invalidate(); }
|
|
105
|
+
render(width) {
|
|
106
|
+
const indent = " ";
|
|
107
|
+
const bodyWidth = Math.max(10, width - indent.length);
|
|
108
|
+
const lines = [` ${color.ok(bold("⏺"))}`, ""];
|
|
109
|
+
for (const l of this.md.render(bodyWidth))
|
|
110
|
+
lines.push(indent + l.replace(/\s+$/, ""));
|
|
111
|
+
if (this.meta)
|
|
112
|
+
lines.push("", `${indent}${color.dim("·")} ${color.muted(this.meta)}`);
|
|
113
|
+
return lines.map((l) => truncateToWidth(l, Math.max(0, width)));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** 单行提示(成功 / 信息 / 警告 / 错误)。 */
|
|
117
|
+
export class Notice {
|
|
118
|
+
icon;
|
|
119
|
+
style;
|
|
120
|
+
text;
|
|
121
|
+
constructor(icon, style, text) {
|
|
122
|
+
this.icon = icon;
|
|
123
|
+
this.style = style;
|
|
124
|
+
this.text = text;
|
|
125
|
+
}
|
|
126
|
+
invalidate() { }
|
|
127
|
+
render(width) {
|
|
128
|
+
const prefix = ` ${this.style(this.icon)} `;
|
|
129
|
+
return wrap(this.text, Math.max(1, width - 4)).map((l, i) => (i === 0 ? prefix + color.text(l) : " " + color.text(l)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 检索命中的附图标记。 */
|
|
133
|
+
export class ImageNotice {
|
|
134
|
+
name;
|
|
135
|
+
version;
|
|
136
|
+
constructor(name, version) {
|
|
137
|
+
this.name = name;
|
|
138
|
+
this.version = version;
|
|
139
|
+
}
|
|
140
|
+
invalidate() { }
|
|
141
|
+
render(width) {
|
|
142
|
+
const line = ` ${color.accent2("★")} ${color.muted("附图")} ${color.chip(color.text(` ${this.name} `))} ${color.dim("v" + this.version)}`;
|
|
143
|
+
return [truncateToWidth(line, Math.max(0, width))];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** 水平分隔线。 */
|
|
147
|
+
export class Rule {
|
|
148
|
+
invalidate() { }
|
|
149
|
+
render(width) {
|
|
150
|
+
return [color.dim("─".repeat(Math.max(0, width)))];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** 帮助面板(作为 overlay 内容)。 */
|
|
154
|
+
export class HelpPanel {
|
|
155
|
+
items;
|
|
156
|
+
constructor(items) { this.items = items; }
|
|
157
|
+
invalidate() { }
|
|
158
|
+
render(width) {
|
|
159
|
+
const w = Math.min(Math.max(0, width), 72);
|
|
160
|
+
const inner = Math.max(0, w - 4);
|
|
161
|
+
const title = " 命令 ";
|
|
162
|
+
const keyWidth = Math.max(10, ...this.items.map(([k]) => visibleWidth(k))) + 2;
|
|
163
|
+
const top = color.border("╭─") + color.accent(bold(title)) + color.border("─".repeat(Math.max(0, w - 3 - visibleWidth(title))) + "╮");
|
|
164
|
+
const bot = color.border("╰" + "─".repeat(Math.max(0, w - 2)) + "╯");
|
|
165
|
+
const row = (c) => color.border("│") + " " + pad(truncateToWidth(c, inner), inner) + " " + color.border("│");
|
|
166
|
+
const body = this.items.map(([k, d]) => row(k ? " " + color.accent(k) + " ".repeat(Math.max(1, keyWidth - visibleWidth(k))) + color.muted(d) : ""));
|
|
167
|
+
return clip([top, row(""), ...body, row(""), bot], w);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** 供 SelectList 复用的主题。 */
|
|
171
|
+
export { selectListTheme, dim };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ganziliang/kb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local knowledge base agent CLI",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Local knowledge base agent CLI with a pi-tui interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kb": "dist/entry.js"
|
|
@@ -17,17 +17,15 @@
|
|
|
17
17
|
"test": "npm run build && node --test"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=22.
|
|
20
|
+
"node": ">=22.19.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
23
24
|
"@ganziliang/kb-model-setup": "^0.1.2",
|
|
24
|
-
"ink": "^5.1.0",
|
|
25
|
-
"react": "^18.3.1",
|
|
26
25
|
"xlsx": "^0.18.5"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
28
|
"@types/node": "^22.10.0",
|
|
30
|
-
"@types/react": "^18.3.12",
|
|
31
29
|
"typescript": "^5.7.2"
|
|
32
30
|
}
|
|
33
31
|
}
|