@ganziliang/kb 0.1.5 → 0.1.7
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 +33 -9
- package/dist/cli.js +30 -6
- package/dist/model.d.ts +21 -2
- package/dist/model.js +33 -4
- package/dist/storage.d.ts +16 -6
- package/dist/storage.js +31 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ganziliang/kb
|
|
2
2
|
|
|
3
|
-
本地知识库命令行工具。它会把本地 Markdown、文本、JSON、CSV 或 Excel 文件导入 SQLite
|
|
3
|
+
本地知识库命令行工具。它会把本地 Markdown、文本、JSON、CSV 或 Excel 文件导入 SQLite 知识库,然后通过关键词检索相关内容,并调用已配置的模型回答问题。同时支持导入图片:入库时自动生成中文描述参与检索,提问命中后把原图发给模型。
|
|
4
4
|
|
|
5
5
|
## 环境要求
|
|
6
6
|
|
|
@@ -22,20 +22,18 @@ npm install -g @ganziliang/kb
|
|
|
22
22
|
kb
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
`@ganziliang/kb` 已经声明依赖 `@ganziliang/kb-model-setup`。通过 npm 安装 `@ganziliang/kb` 时,npm 会自动安装该依赖,正常情况下不需要单独安装。
|
|
26
|
+
|
|
27
|
+
如果你需要单独安装或更新模型配置模块,可以执行:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
|
-
|
|
29
|
-
npm install
|
|
30
|
-
npm run build
|
|
31
|
-
node dist/entry.js
|
|
30
|
+
npm install -g @ganziliang/kb-model-setup
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
注意:`kb-model-setup` 是模型配置模块,不是启动 `kb` 的主命令。日常使用只需要运行:
|
|
35
34
|
|
|
36
35
|
```bash
|
|
37
|
-
|
|
38
|
-
npm run build
|
|
36
|
+
kb
|
|
39
37
|
```
|
|
40
38
|
|
|
41
39
|
## 首次启动和模型配置
|
|
@@ -51,6 +49,8 @@ kb
|
|
|
51
49
|
1. `api-stats` 页面地址或 `apiId`
|
|
52
50
|
2. API Key
|
|
53
51
|
|
|
52
|
+
`kb` 使用依赖包 `@ganziliang/kb-model-setup` 完成模型配置。该模块会查询智真 LLM Gateway 中当前账号可用的模型,并保存所选模型配置。
|
|
53
|
+
|
|
54
54
|
当前默认使用智真 LLM Gateway:
|
|
55
55
|
|
|
56
56
|
```text
|
|
@@ -75,6 +75,7 @@ Windows 通常对应:
|
|
|
75
75
|
C:\Users\<用户名>\.config\kb\config.json
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
|
|
78
79
|
也可以通过环境变量跳过首次交互配置:
|
|
79
80
|
|
|
80
81
|
```bash
|
|
@@ -115,6 +116,7 @@ kb
|
|
|
115
116
|
- JSON:`.json`
|
|
116
117
|
- CSV:`.csv`
|
|
117
118
|
- Excel:`.xlsx`、`.xls`
|
|
119
|
+
- 图片:`.png`、`.jpg`、`.jpeg`、`.gif`、`.webp`、`.bmp`(需要模型支持视觉)
|
|
118
120
|
|
|
119
121
|
导入命令格式:
|
|
120
122
|
|
|
@@ -137,10 +139,32 @@ import <文件路径>
|
|
|
137
139
|
导入 ./docs/payment.md
|
|
138
140
|
import D:\docs\api.txt
|
|
139
141
|
导入 "D:\资料\产品说明.xlsx"
|
|
142
|
+
导入 "D:\截图\支付报错.png"
|
|
140
143
|
```
|
|
141
144
|
|
|
142
145
|
导入后会复制一份原始文件,并建立版本记录。对同一个文件再次导入时,会创建新版本,不会直接覆盖旧版本。
|
|
143
146
|
|
|
147
|
+
### 图片知识
|
|
148
|
+
|
|
149
|
+
图片本身无法参与关键词检索,因此 `kb` 对图片使用双通道处理:
|
|
150
|
+
|
|
151
|
+
1. **入库时生成描述**:导入图片时会调用当前模型看图,生成一段中文描述(含图中所有文字、数字、表格与界面细节),写入知识库参与全文检索。
|
|
152
|
+
2. **提问时附原图**:当问题检索命中图片记录时,`kb` 会把原始图片一并发送给模型,因此模型看到的是真实图像,而不只是描述。
|
|
153
|
+
|
|
154
|
+
也就是说,导入一次图片后,既可以按内容关键词搜到它,也可以直接针对图片细节提问:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
导入 "D:\截图\支付报错.png"
|
|
158
|
+
|
|
159
|
+
支付报错的截图里,订单号和错误码分别是什么?
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
注意:
|
|
163
|
+
|
|
164
|
+
- 图片描述依赖当前模型具备视觉能力;模型不支持视觉时,图片仍会入库,但只能按文件名检索,并在导入结果中提示失败原因。
|
|
165
|
+
- 单次提问最多附带 4 张命中图片。
|
|
166
|
+
- 历史对话中不会重复携带图片数据,每轮提问都会按检索结果重新附图。
|
|
167
|
+
|
|
144
168
|
### 查询知识
|
|
145
169
|
|
|
146
170
|
直接输入问题即可:
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,8 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { Box, Text, render, useApp, useInput } from "ink";
|
|
5
5
|
import { mkdirSync } from "node:fs";
|
|
6
6
|
import { ensureModelConfig, saveModels } from "./config.js";
|
|
7
|
-
import { Agent, fetchTransport } from "./model.js";
|
|
8
|
-
import { copyOriginal, defaultDataRoot, ensureKnowledgeBase, KnowledgeStore, readLocalFile, listKnowledgeBases, deleteKnowledgeBase, backupKnowledgeBase, restoreKnowledgeBase } from "./storage.js";
|
|
7
|
+
import { Agent, describeImage, fetchTransport } from "./model.js";
|
|
8
|
+
import { copyOriginal, defaultDataRoot, ensureKnowledgeBase, isImageFile, KnowledgeStore, readImage, readLocalFile, listKnowledgeBases, deleteKnowledgeBase, backupKnowledgeBase, restoreKnowledgeBase } from "./storage.js";
|
|
9
9
|
const banner = [
|
|
10
10
|
" _ __ ____ ",
|
|
11
11
|
" | |/ // __ ) ",
|
|
@@ -55,7 +55,7 @@ function App({ config, root, knowledgeBase }) {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
if (value === "/help") {
|
|
58
|
-
setLines((old) => [...old, "/help /clear /sources /backup /restore /cleanup /model /models /kb /quit"]);
|
|
58
|
+
setLines((old) => [...old, "/help /clear /sources /backup /restore /cleanup /model /models /kb /quit", "导入:输入「导入 <文件路径>」,支持 .md/.txt/.json/.csv/.xlsx/.xls 及 .png/.jpg/.jpeg/.gif/.webp/.bmp", "图片入库时自动生成中文描述用于检索,提问命中后会把原图一并发给模型"]);
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
if (value === "/sources") {
|
|
@@ -161,14 +161,38 @@ async function ingestOrAsk(value, store, modelConfig, previous) {
|
|
|
161
161
|
const source = readLocalFile(pathMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
162
162
|
if (!source.hash)
|
|
163
163
|
return { text: `${source.title}: unable to parse (${source.content})`, messages: previous };
|
|
164
|
+
let content = source.content;
|
|
165
|
+
let note = "";
|
|
166
|
+
if (source.image) {
|
|
167
|
+
try {
|
|
168
|
+
const caption = await describeImage(modelConfig, fetchTransport, source.image);
|
|
169
|
+
if (caption) {
|
|
170
|
+
content = `${source.content}\n\n${caption}`;
|
|
171
|
+
note = ",已生成图片描述";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
note = `,图片描述生成失败(仅按文件名检索): ${error instanceof Error ? error.message : String(error)}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
164
178
|
const copied = copyOriginal(source.originalPath, store.root);
|
|
165
|
-
const result = store.addOrUpdateSource(copied, source.title,
|
|
166
|
-
return { text: `${result.isNew ? "Imported" : "New version created for"} ${result.title} (v${result.version})`, messages: previous };
|
|
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 };
|
|
167
181
|
}
|
|
168
182
|
const results = store.search(value);
|
|
169
183
|
const context = results.length ? results.map((item) => `[${item.id}] ${item.title} (source: ${item.source}, v${item.version})\n${item.content}`).join("\n\n") : "No matching knowledge was found.";
|
|
184
|
+
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: ${value}`;
|
|
185
|
+
const blocks = [{ type: "text", text: prompt }];
|
|
186
|
+
for (const item of results.filter((hit) => hit.originalPath && isImageFile(hit.originalPath)).slice(0, 4)) {
|
|
187
|
+
try {
|
|
188
|
+
const image = readImage(item.originalPath);
|
|
189
|
+
blocks.push({ type: "text", text: `[附图] ${item.source} v${item.version}` });
|
|
190
|
+
blocks.push({ type: "image", mediaType: image.mediaType, base64: image.base64 });
|
|
191
|
+
}
|
|
192
|
+
catch { /* 忽略无法读取的图片 */ }
|
|
193
|
+
}
|
|
170
194
|
const agent = new Agent(store, modelConfig, fetchTransport);
|
|
171
|
-
return agent.answer(
|
|
195
|
+
return agent.answer(blocks.length > 1 ? blocks : prompt, previous);
|
|
172
196
|
}
|
|
173
197
|
export async function main() {
|
|
174
198
|
const root = defaultDataRoot();
|
package/dist/model.d.ts
CHANGED
|
@@ -4,15 +4,26 @@ export type ToolCall = {
|
|
|
4
4
|
name: "search" | "read" | "write";
|
|
5
5
|
arguments: Record<string, unknown>;
|
|
6
6
|
};
|
|
7
|
+
export type ContentBlock = {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "image";
|
|
12
|
+
mediaType: string;
|
|
13
|
+
base64: string;
|
|
14
|
+
};
|
|
7
15
|
export type ModelMessage = {
|
|
8
16
|
role: "system" | "user" | "assistant" | "tool";
|
|
9
|
-
content: string;
|
|
17
|
+
content: string | ContentBlock[];
|
|
10
18
|
toolCallId?: string;
|
|
11
19
|
};
|
|
12
20
|
export type ModelReply = {
|
|
13
21
|
text?: string;
|
|
14
22
|
toolCalls?: ToolCall[];
|
|
15
23
|
};
|
|
24
|
+
export declare function toAnthropicContent(content: string | ContentBlock[]): unknown;
|
|
25
|
+
export declare function toOpenAIContent(content: string | ContentBlock[]): unknown;
|
|
26
|
+
export declare function contentToText(content: string | ContentBlock[]): string;
|
|
16
27
|
export type ModelTransport = {
|
|
17
28
|
complete(config: ModelConfig, messages: ModelMessage[]): Promise<ModelReply>;
|
|
18
29
|
};
|
|
@@ -22,10 +33,18 @@ export declare class Agent {
|
|
|
22
33
|
private readonly config;
|
|
23
34
|
private readonly transport;
|
|
24
35
|
constructor(store: KnowledgeStore, config: ModelConfig, transport?: ModelTransport);
|
|
25
|
-
answer(question: string, previous?: ModelMessage[]): Promise<{
|
|
36
|
+
answer(question: string | ContentBlock[], previous?: ModelMessage[]): Promise<{
|
|
26
37
|
text: string;
|
|
27
38
|
messages: ModelMessage[];
|
|
28
39
|
}>;
|
|
29
40
|
private execute;
|
|
30
41
|
}
|
|
31
42
|
export declare function formatResults(results: SearchResult[]): string;
|
|
43
|
+
type VisionTransport = {
|
|
44
|
+
complete: (config: ModelConfig, messages: ModelMessage[]) => Promise<ModelReply>;
|
|
45
|
+
};
|
|
46
|
+
export declare function describeImage(config: ModelConfig, transport: VisionTransport, image: {
|
|
47
|
+
mediaType: string;
|
|
48
|
+
base64: string;
|
|
49
|
+
}): Promise<string>;
|
|
50
|
+
export {};
|
package/dist/model.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
export function toAnthropicContent(content) {
|
|
2
|
+
if (typeof content === "string")
|
|
3
|
+
return content;
|
|
4
|
+
return content.map((block) => block.type === "text"
|
|
5
|
+
? { type: "text", text: block.text }
|
|
6
|
+
: { type: "image", source: { type: "base64", media_type: block.mediaType, data: block.base64 } });
|
|
7
|
+
}
|
|
8
|
+
export function toOpenAIContent(content) {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return content;
|
|
11
|
+
return content.map((block) => block.type === "text"
|
|
12
|
+
? { type: "input_text", text: block.text }
|
|
13
|
+
: { type: "input_image", image_url: `data:${block.mediaType};base64,${block.base64}` });
|
|
14
|
+
}
|
|
15
|
+
export function contentToText(content) {
|
|
16
|
+
return typeof content === "string" ? content : content.map((block) => block.type === "text" ? block.text : `[image:${block.mediaType}]`).join("\n");
|
|
17
|
+
}
|
|
18
|
+
// 历史消息里的图片 base64 不重复携带:每轮都会重新检索并附图,避免上下文膨胀。
|
|
19
|
+
function compactHistory(messages) {
|
|
20
|
+
return messages.map((message) => typeof message.content === "string" ? message : { ...message, content: contentToText(message.content) });
|
|
21
|
+
}
|
|
1
22
|
function endpoint(config) {
|
|
2
23
|
return `${config.baseURL.replace(/\/$/, "")}${config.api === "anthropic-messages" ? "/v1/messages" : "/v1/responses"}`;
|
|
3
24
|
}
|
|
@@ -14,8 +35,8 @@ export const fetchTransport = {
|
|
|
14
35
|
method: "POST",
|
|
15
36
|
headers,
|
|
16
37
|
body: JSON.stringify(config.api === "anthropic-messages"
|
|
17
|
-
? { model: config.model, max_tokens: 4096, messages: messages.filter((m) => m.role !== "system"), system: messages.find((m) => m.role === "system")?.content }
|
|
18
|
-
: { model: config.model, input: messages.map((m) => ({ role: m.role, content: m.content })) }),
|
|
38
|
+
? { model: config.model, max_tokens: 4096, messages: messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: toAnthropicContent(m.content) })), system: messages.find((m) => m.role === "system")?.content }
|
|
39
|
+
: { model: config.model, input: messages.map((m) => ({ role: m.role, content: toOpenAIContent(m.content) })) }),
|
|
19
40
|
});
|
|
20
41
|
if (!response.ok)
|
|
21
42
|
throw new Error(`Model request failed (${response.status}): ${await response.text()}`);
|
|
@@ -38,13 +59,13 @@ export class Agent {
|
|
|
38
59
|
const messages = [...previous, { role: "user", content: question }];
|
|
39
60
|
const first = await this.transport.complete(this.config, messages);
|
|
40
61
|
if (!first.toolCalls?.length)
|
|
41
|
-
return { text: first.text ?? "", messages: [...messages, { role: "assistant", content: first.text ?? "" }] };
|
|
62
|
+
return { text: first.text ?? "", messages: [...compactHistory(messages), { role: "assistant", content: first.text ?? "" }] };
|
|
42
63
|
for (const call of first.toolCalls) {
|
|
43
64
|
const result = this.execute(call);
|
|
44
65
|
messages.push({ role: "tool", content: JSON.stringify(result), toolCallId: call.name });
|
|
45
66
|
}
|
|
46
67
|
const final = await this.transport.complete(this.config, messages);
|
|
47
|
-
return { text: final.text ?? "", messages: [...messages, { role: "assistant", content: final.text ?? "" }] };
|
|
68
|
+
return { text: final.text ?? "", messages: [...compactHistory(messages), { role: "assistant", content: final.text ?? "" }] };
|
|
48
69
|
}
|
|
49
70
|
execute(call) {
|
|
50
71
|
if (call.name === "search")
|
|
@@ -57,3 +78,11 @@ export class Agent {
|
|
|
57
78
|
export function formatResults(results) {
|
|
58
79
|
return results.map((result) => `[${result.id}] ${result.title} (source: ${result.source}, v${result.version})\n${result.content}`).join("\n\n");
|
|
59
80
|
}
|
|
81
|
+
const IMAGE_QUESTION = "\u8bf7\u7528\u4e2d\u6587\u8be6\u7ec6\u63cf\u8ff0\u8fd9\u5f20\u56fe\u7247\u7684\u5185\u5bb9\uff0c\u5199\u6e05\u6240\u6709\u53ef\u89c1\u7684\u6587\u5b57\u3001\u6570\u5b57\u3001\u8868\u683c\u3001\u754c\u9762\u5143\u7d20\u548c\u5173\u952e\u7ec6\u8282\uff0c\u4ee5\u53ca\u56fe\u7247\u6574\u4f53\u5728\u8bb2\u4ec0\u4e48\u3002\u53ea\u8f93\u51fa\u63cf\u8ff0\u6b63\u6587\uff0c\u4e0d\u8981\u5ba2\u5957\u8bdd\u3002";
|
|
82
|
+
export async function describeImage(config, transport, image) {
|
|
83
|
+
const reply = await transport.complete(config, [{
|
|
84
|
+
role: "user",
|
|
85
|
+
content: [{ type: "text", text: IMAGE_QUESTION }, { type: "image", mediaType: image.mediaType, base64: image.base64 }],
|
|
86
|
+
}]);
|
|
87
|
+
return (reply.text ?? "").trim();
|
|
88
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -11,7 +11,22 @@ export type SearchResult = {
|
|
|
11
11
|
source: string;
|
|
12
12
|
version: number;
|
|
13
13
|
updatedAt: string;
|
|
14
|
+
originalPath?: string;
|
|
14
15
|
};
|
|
16
|
+
export declare const IMAGE_MEDIA_TYPES: Record<string, string>;
|
|
17
|
+
export type ImageData = {
|
|
18
|
+
mediaType: string;
|
|
19
|
+
base64: string;
|
|
20
|
+
};
|
|
21
|
+
export type LocalFile = {
|
|
22
|
+
title: string;
|
|
23
|
+
content: string;
|
|
24
|
+
originalPath: string;
|
|
25
|
+
hash: string;
|
|
26
|
+
image?: ImageData;
|
|
27
|
+
};
|
|
28
|
+
export declare function isImageFile(filePath: string): boolean;
|
|
29
|
+
export declare function readImage(filePath: string): ImageData;
|
|
15
30
|
export declare function tokenize(text: string): string;
|
|
16
31
|
export declare class KnowledgeStore {
|
|
17
32
|
readonly root: string;
|
|
@@ -37,12 +52,7 @@ type KnowledgeStoreResult = {
|
|
|
37
52
|
title: string;
|
|
38
53
|
isNew: boolean;
|
|
39
54
|
};
|
|
40
|
-
export declare function readLocalFile(filePath: string):
|
|
41
|
-
title: string;
|
|
42
|
-
content: string;
|
|
43
|
-
originalPath: string;
|
|
44
|
-
hash: string;
|
|
45
|
-
};
|
|
55
|
+
export declare function readLocalFile(filePath: string): LocalFile;
|
|
46
56
|
export declare function copyOriginal(filePath: string, root: string): string;
|
|
47
57
|
export declare function defaultDataRoot(): string;
|
|
48
58
|
export declare function knowledgeBasePath(root: string, id: string): string;
|
package/dist/storage.js
CHANGED
|
@@ -3,6 +3,24 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { join, resolve, extname, basename } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import * as XLSX from "xlsx";
|
|
6
|
+
export const IMAGE_MEDIA_TYPES = {
|
|
7
|
+
".png": "image/png",
|
|
8
|
+
".jpg": "image/jpeg",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".webp": "image/webp",
|
|
12
|
+
".bmp": "image/bmp",
|
|
13
|
+
};
|
|
14
|
+
export function isImageFile(filePath) {
|
|
15
|
+
return extname(filePath).toLowerCase() in IMAGE_MEDIA_TYPES;
|
|
16
|
+
}
|
|
17
|
+
export function readImage(filePath) {
|
|
18
|
+
const ext = extname(filePath).toLowerCase();
|
|
19
|
+
const mediaType = IMAGE_MEDIA_TYPES[ext];
|
|
20
|
+
if (!mediaType)
|
|
21
|
+
throw new Error(`Unsupported image format: ${ext || "unknown"}`);
|
|
22
|
+
return { mediaType, base64: readFileSync(filePath).toString("base64") };
|
|
23
|
+
}
|
|
6
24
|
export function tokenize(text) {
|
|
7
25
|
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
|
|
8
26
|
const words = [...segmenter.segment(text)]
|
|
@@ -48,10 +66,10 @@ export class KnowledgeStore {
|
|
|
48
66
|
const match = tokenize(query).split(/\s+/).filter(Boolean).map((part) => `"${part.replaceAll('"', '""')}"`).join(" OR ");
|
|
49
67
|
if (!match)
|
|
50
68
|
return [];
|
|
51
|
-
return this.db.prepare(`SELECT k.id, k.title, k.content, s.name AS source, k.version, k.updated_at AS updatedAt FROM knowledge_fts f JOIN knowledge k ON k.id = f.knowledge_id JOIN sources s ON s.id = k.source_id WHERE f.knowledge_fts MATCH ? AND k.current = 1 ORDER BY rank LIMIT ?`).all(match, limit);
|
|
69
|
+
return this.db.prepare(`SELECT k.id, k.title, k.content, s.name AS source, k.version, k.updated_at AS updatedAt, s.original_path AS originalPath FROM knowledge_fts f JOIN knowledge k ON k.id = f.knowledge_id JOIN sources s ON s.id = k.source_id WHERE f.knowledge_fts MATCH ? AND k.current = 1 ORDER BY rank LIMIT ?`).all(match, limit);
|
|
52
70
|
}
|
|
53
71
|
read(id) {
|
|
54
|
-
return this.db.prepare("SELECT k.id,k.title,k.content,s.name AS source,k.version,k.updated_at AS updatedAt FROM knowledge k JOIN sources s ON s.id=k.source_id WHERE k.id=?").get(id);
|
|
72
|
+
return this.db.prepare("SELECT k.id,k.title,k.content,s.name AS source,k.version,k.updated_at AS updatedAt,s.original_path AS originalPath FROM knowledge k JOIN sources s ON s.id=k.source_id WHERE k.id=?").get(id);
|
|
55
73
|
}
|
|
56
74
|
sources() { return this.db.prepare("SELECT s.name,s.original_path AS path,MAX(k.updated_at) AS updatedAt,MAX(k.version) AS version FROM sources s JOIN knowledge k ON k.source_id=s.id AND k.current=1 GROUP BY s.id ORDER BY updatedAt DESC").all(); }
|
|
57
75
|
oldVersions() { return this.db.prepare("SELECT k.id,k.title,k.version,s.name AS source FROM knowledge k JOIN sources s ON s.id=k.source_id WHERE k.current=0 ORDER BY s.name,k.version").all(); }
|
|
@@ -66,6 +84,17 @@ export function readLocalFile(filePath) {
|
|
|
66
84
|
const originalPath = resolve(filePath);
|
|
67
85
|
const ext = extname(originalPath).toLowerCase();
|
|
68
86
|
let content;
|
|
87
|
+
if (IMAGE_MEDIA_TYPES[ext]) {
|
|
88
|
+
const image = readImage(originalPath);
|
|
89
|
+
const size = statSync(originalPath).size;
|
|
90
|
+
return {
|
|
91
|
+
title: basename(originalPath),
|
|
92
|
+
content: `[\u56fe\u7247] ${basename(originalPath)} (${image.mediaType}, ${size} bytes)`,
|
|
93
|
+
originalPath,
|
|
94
|
+
hash: createHash("sha256").update(image.base64).digest("hex"),
|
|
95
|
+
image,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
69
98
|
if ([".md", ".txt", ".json", ".csv"].includes(ext))
|
|
70
99
|
content = readFileSync(originalPath, "utf8");
|
|
71
100
|
else if (ext === ".xlsx" || ext === ".xls") {
|