local-knowledge-graph 1.5.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/.opencode/skill/kg-triples/SKILL.md +55 -0
- package/HELP.md +112 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +86 -0
- package/lib/agent.js +211 -0
- package/lib/ask.js +290 -0
- package/lib/db.js +613 -0
- package/lib/embeddings.js +166 -0
- package/lib/git.js +175 -0
- package/lib/importer.js +54 -0
- package/lib/inference.js +123 -0
- package/lib/paths.js +53 -0
- package/lib/rdf.js +64 -0
- package/lib/updater.js +144 -0
- package/lib/validator.js +146 -0
- package/lib/vectors.js +68 -0
- package/lib/viewer.js +33 -0
- package/lib/viewer_template.html +283 -0
- package/mcp/server.js +262 -0
- package/package.json +44 -0
- package/public/app.js +1761 -0
- package/public/index.html +238 -0
- package/public/style.css +374 -0
- package/public/vendor/OrbitControls.js +1045 -0
- package/public/vendor/three.min.js +6 -0
- package/server.js +625 -0
- package/tools/ego.js +122 -0
- package/tools/search.js +101 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kg-triples
|
|
3
|
+
description: 将文本、文章或文档科学系统地转化为RDF三元组(含属性)并融合进入本地知识图谱。当用户要求"文本转三元组"、"文档建图"、"知识抽取"、"把这篇文章导入知识图谱"、"抽取实体关系"等任务时使用本技能。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 文本/文档 → 三元组抽取技能
|
|
7
|
+
|
|
8
|
+
按五段流水线处理用户提供的文本或文档附件。所有产出以 kg-ops 协议输出,由本地程序做 RDF 校验与原子入库,禁止直接操作数据库文件。
|
|
9
|
+
|
|
10
|
+
## 第一步:预处理与分块
|
|
11
|
+
|
|
12
|
+
1. 获取文本:用户粘贴的正文或附件(PDF/Word 等先提取纯文本,保留章节结构)
|
|
13
|
+
2. 清洗:去掉页眉页脚、水印、乱码、参考书目噪音
|
|
14
|
+
3. 识别标题层级与文体:叙事文(历史/新闻)以事件为核心;说明文/百科以实体-属性为主;表格按"行=实体、列=属性键"映射;技术文档关注概念层级
|
|
15
|
+
4. 分块:按语义单元(小节/段落)切为 300-800 字的块,块间保留一句重叠;逐块处理,每块独立输出一个 kg-ops 代码块
|
|
16
|
+
|
|
17
|
+
## 第二步:逐块抽取
|
|
18
|
+
|
|
19
|
+
- 全量识别候选知识元素:人名、地名、机构、概念、事件、作品、数值、时间表达
|
|
20
|
+
- 实体四大类判定:物理可感的存在 → 物理实体;观念/事件/作品/领域 → 抽象实体;量值作为关系端点 → 数值实体;时间作为关系端点 → 时间实体
|
|
21
|
+
- 建模铁律:仅作描述说明的年份、日期、数量(成立年份、测量年份、海拔、人口)一律写入所属实体的 attributes,禁止为它们单独建节点
|
|
22
|
+
- 属性:扁平键值对,值为字符串/数值/布尔,禁止嵌套
|
|
23
|
+
- 关系:识别谓词(动词/系词/介词结构),对齐主语宾语,产出(主语, 谓词, 宾语)候选
|
|
24
|
+
|
|
25
|
+
## 第三步:规范化
|
|
26
|
+
|
|
27
|
+
- 命名全称优先、统一译名;同义实体合并为一个节点,原名称收进"别名"属性
|
|
28
|
+
- 同名歧义靠上下文与属性区分,必要时加限定符(如"李白(诗人)")
|
|
29
|
+
- 谓词使用规范词:位于/属于/发生于/参与/包含/创作/国籍/推动发展 等,同义谓词归一("坐落于"→"位于")
|
|
30
|
+
- 关系大类固定五选一:空间 / 互动 / 归属 / 时间 / 属性
|
|
31
|
+
- 数值单位统一换算、日期统一格式
|
|
32
|
+
|
|
33
|
+
## 第四步:融合(对齐已有图谱)
|
|
34
|
+
|
|
35
|
+
- 每块抽取前先对照提示词中的"当前图谱"摘要:已存在的实体直接用其真实 id 引用(source_id/target_id),禁止重复创建同名实体
|
|
36
|
+
- 本次新增的实体用 ref 占位符(如 "ref":"A"),同一块内后续关系用 source_ref/target_ref 引用
|
|
37
|
+
- 与已有知识矛盾的信息:创建/更新时在 attributes 中加"来源"键标注文档名与原文表述,并在说明中提示用户裁决;禁止擅自覆盖或删除已有知识
|
|
38
|
+
- 从文档新建的实体建议带"来源"属性记录出处文档名,保证可追溯
|
|
39
|
+
|
|
40
|
+
## 第五步:输出协议
|
|
41
|
+
|
|
42
|
+
- 先输出简短中文抽取报告:本块新建实体数、复用实体数、关系数、存疑项清单
|
|
43
|
+
- 每个处理块输出一个 kg-ops 代码块(一次回复可含多个代码块),格式:
|
|
44
|
+
|
|
45
|
+
````
|
|
46
|
+
```kg-ops
|
|
47
|
+
[
|
|
48
|
+
{"op":"add_entity","ref":"A","name":"实体名","category":"物理实体","attributes":{"来源":"xx文档"}},
|
|
49
|
+
{"op":"add_relation","source_ref":"A","target_id":3,"name":"位于","category":"空间"},
|
|
50
|
+
{"op":"update_entity","id":5,"attributes":{"别名":"某某"}}
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
- 底线原则:宁可漏抽并在说明中列出存疑项,也不硬造三元组;每条知识必须能回指原文位置
|
package/HELP.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# 本地知识图谱整合器 · 帮助
|
|
2
|
+
|
|
3
|
+
把文档与联网信息整理成可导航的 3D 知识图谱。全流程本地运行,图谱数据、图片、API Key 全部保存在本机数据目录(npm 安装模式在 `~/.local-knowledge-graph/`,Git 克隆模式在项目内 `data/`),代码仓库不包含任何用户数据。
|
|
4
|
+
|
|
5
|
+
## 查看当前版本
|
|
6
|
+
|
|
7
|
+
- 网页端:页头「**视图设置**」面板底部,或页头「**帮助**」面板的"版本与更新"区域,显示当前版本号
|
|
8
|
+
- 命令行:`node -e "console.log(require('./package.json').version)"`
|
|
9
|
+
|
|
10
|
+
## 检查更新 / 一键更新
|
|
11
|
+
|
|
12
|
+
1. 打开页头「**帮助**」面板(或「视图设置」面板底部)的"版本与更新"区域
|
|
13
|
+
2. 点击「**检查更新**」—— 应用会连接 GitHub 对比本地与远端版本,并显示最新发布说明
|
|
14
|
+
3. 若有新版本,点击「**一键更新**」—— 应用自动下载更新并重启服务,页面随后自动刷新
|
|
15
|
+
|
|
16
|
+
注意事项:
|
|
17
|
+
|
|
18
|
+
- 一键更新要求应用目录是本仓库的 Git 克隆(默认安装方式即是),且本地没有未发布的代码改动
|
|
19
|
+
- 更新只替换程序代码;`data/` 目录(图谱数据、图片、配置)完全不受影响
|
|
20
|
+
- 若自动重启失败,关闭窗口后重新运行启动脚本即可(`start.bat` / `start.command` / `bash start.sh`)
|
|
21
|
+
|
|
22
|
+
## 手动更新(备用方式)
|
|
23
|
+
|
|
24
|
+
npm 安装模式:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -g local-knowledge-graph@latest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Git 克隆模式:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd 项目目录
|
|
34
|
+
git pull --ff-only origin master
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
然后重新启动应用。
|
|
38
|
+
|
|
39
|
+
## 启动方式
|
|
40
|
+
|
|
41
|
+
**npm 安装(推荐)**:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm i -g local-knowledge-graph
|
|
45
|
+
kg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
数据保存在用户目录 `~/.local-knowledge-graph/`(Windows 在 `C:\Users\你\.local-knowledge-graph`),升级与卸载程序均不影响数据。也可 `npx local-knowledge-graph` 免安装试用。
|
|
49
|
+
|
|
50
|
+
**脚本启动(Git 克隆模式)**:
|
|
51
|
+
|
|
52
|
+
| 平台 | 方式 |
|
|
53
|
+
| ---- | ---- |
|
|
54
|
+
| Windows | 双击 `start.bat` |
|
|
55
|
+
| macOS | 双击 `start.command` |
|
|
56
|
+
| Linux / 命令行 | `bash start.sh` 或 `npm start` |
|
|
57
|
+
|
|
58
|
+
此模式数据存放在项目内 `data/` 目录。启动后浏览器访问 `http://localhost:3000`。依赖 Node.js >= 22.5;自然语言补全需要全局安装 `opencode` CLI(`npm i -g opencode-ai`),未安装时仅该功能降级,其余功能正常。
|
|
59
|
+
|
|
60
|
+
## API Key 配置(全部在界面上完成)
|
|
61
|
+
|
|
62
|
+
程序自身零内置密钥,所有需要 key 的能力都在界面上自由配置:
|
|
63
|
+
|
|
64
|
+
- **语义检索 / 智能提问综述**:检索页签"语义检索配置"填入 API Key(默认硅基流动 BAAI/bge-m3,可换任意 OpenAI 兼容接口的 Base URL 与模型)。Key 仅存本地数据目录的 settings.json,服务端永不回传真实值。不配置则自动退化为关键词检索,其余功能不受影响。
|
|
65
|
+
- **OpenCode(AI 对话建图 / 智能提问编译)**:使用 opencode CLI 自身的认证(`opencode auth login`),应用不经手密钥。未安装或未认证时仅这两个功能降级。
|
|
66
|
+
- 更换数据目录(`kg --data <目录>`)会使用该目录下的独立配置,互不串扰。
|
|
67
|
+
|
|
68
|
+
## 功能速览
|
|
69
|
+
|
|
70
|
+
- **AI 对话建图**:右侧输入自然语言指令(联网补全),或上传文档(md/txt/pdf/docx)批量抽取三元组
|
|
71
|
+
- **智能提问**:检索页签一句话找关系(LLM 编译检索计划,本地只读执行,可生成带引用的综述)
|
|
72
|
+
- **语义检索**:配置硅基流动 API Key 后支持语义+关键词混合检索(未配置时自动退化为关键词模式)
|
|
73
|
+
- **最短路径 / 中心子图 / 推理**:挖掘实体间关联链路、层级范围与隐性关系
|
|
74
|
+
- **撤销**:Ctrl+Z 或按钮,可连续撤销
|
|
75
|
+
- **保存点与回溯**:版本页签打保存点,随时回滚;每次保存点自动滚动备份到 `data/backups/`
|
|
76
|
+
- **图片绑定**:实体信息卡上传本地图片
|
|
77
|
+
- **导出**:RDF(Turtle)、整库 .db、单文件只读网页版
|
|
78
|
+
|
|
79
|
+
## 键盘快捷键
|
|
80
|
+
|
|
81
|
+
| 快捷键 | 功能 |
|
|
82
|
+
| ------ | ---- |
|
|
83
|
+
| Ctrl+F | 聚焦搜索框 |
|
|
84
|
+
| Ctrl+Z | 撤销最近操作 |
|
|
85
|
+
| Delete | 删除选中实体/关系 |
|
|
86
|
+
| Esc | 逐层关闭面板 / 取消选中 |
|
|
87
|
+
|
|
88
|
+
## 常见问题
|
|
89
|
+
|
|
90
|
+
**Q:打开页面是空白?**
|
|
91
|
+
用现代浏览器(Chrome/Edge/Safari 最新版);确认服务已启动且端口未被占用。
|
|
92
|
+
|
|
93
|
+
**Q:OpenCode 检测未就绪?**
|
|
94
|
+
运行 `npm i -g opencode-ai` 安装后重启应用;OpenCode 仅用于自然语言补全与智能提问的编译环节。
|
|
95
|
+
|
|
96
|
+
**Q:数据会上传到云端吗?**
|
|
97
|
+
图谱数据只存在本机。仅当你在"检索"页签配置了语义检索 API Key 时,检索向量化的文本片段才会发往你配置的模型服务;OpenCode 补全公开知识时按其自身机制联网。其余全部本地完成。
|
|
98
|
+
|
|
99
|
+
**Q:如何回滚某次误操作?**
|
|
100
|
+
优先用「撤销」(可连续);需要回到更早状态时在"版本"页签回溯到对应保存点。
|
|
101
|
+
|
|
102
|
+
## 版本历史摘要
|
|
103
|
+
|
|
104
|
+
- **v1.5.0**:npm 包化(`npm i -g local-knowledge-graph` + `kg` 一键安装启动);数据目录迁至用户目录(升级无损);应用内更新支持 npm 与 git 双模式
|
|
105
|
+
|
|
106
|
+
- **v1.4.0**:设置与帮助面板内置版本号、检查更新、一键更新;项目目录新增本帮助文档
|
|
107
|
+
- **v1.3.0**:智能关系检索(自然语言提问 → 检索计划 → 关联发现 → 综述)
|
|
108
|
+
- **v1.2.0**:连续撤销、最短路径查询、日志筛选、悬浮提示、键盘快捷键
|
|
109
|
+
- **v1.1.0**:向量库拆分、导入去重加固、AI 任务互斥、聚焦飞行、日志人话渲染、推理持久化、单文件网页导出、移动端适配、一键启动脚本
|
|
110
|
+
- **v1.0.0**:首版——三表数据模型、3D 可视化、RDF 校验、Git 保存点/回溯、文档导入、OWL 推理、MCP Server
|
|
111
|
+
|
|
112
|
+
完整发布说明见 [GitHub Releases](https://github.com/iamsamyiok/local-knowledge-graph/releases)。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iamsamyiok
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# 本地知识图谱整合器
|
|
2
|
+
|
|
3
|
+
全本地运行的个人知识图谱工具:3D 可视化、AI 文档导入、OWL 推理、向量混合检索、Git 级保存点、MCP Server。数据始终存放在本机 SQLite 中。
|
|
4
|
+
|
|
5
|
+
## 适用场景
|
|
6
|
+
|
|
7
|
+
- **个人知识管理**:读书笔记、学习摘录沉淀为可检索、可视化的关系网络,越用越密
|
|
8
|
+
- **研究与考证**:人物、事件、地点、时间线的梳理与追溯(历史考据、家谱、案件梳理、情报分析)
|
|
9
|
+
- **创作设定库**:小说、剧本、游戏世界观的角色关系、组织架构、地理设定统一管理
|
|
10
|
+
- **文档资产化**:批量导入 Markdown / PDF / Word,AI 自动抽取实体关系,零手工建库
|
|
11
|
+
- **离线分享**:单文件 HTML 查看器发给他人浏览器直接打开;整库 .db 一键交接
|
|
12
|
+
|
|
13
|
+
## 功能总览
|
|
14
|
+
|
|
15
|
+
| 模块 | 说明 |
|
|
16
|
+
|------|------|
|
|
17
|
+
| 3D 可视化 | three.js 力导向布局,实体按 4 大类区分形状/颜色,关系按 5 大类区分线型;支持中心层级视图(可调层数)、视图主题自定义、实体图片绑定与灯箱 |
|
|
18
|
+
| AI 文档导入 | 调用 OpenCode CLI 将 Markdown/PDF/Word 等文档抽取为三元组并自动导入(分块+保存点对齐,防重复导入) |
|
|
19
|
+
| OWL 推理 | 按 传递/对称/逆 三类规则动态推导隐性关系(不入库),可配置 data/ontology.json,前端一键叠加显示并可查看推导链 |
|
|
20
|
+
| 混合检索 | 语义向量(默认硅基流动 BAAI/bge-m3,可换任意 OpenAI 兼容接口)+ 关键词检索,RRF 融合排序 |
|
|
21
|
+
| 迷你 Cypher | 只读关系查询:`MATCH (a)-[r:互动]->(b) WHERE a.name contains 郑和 RETURN a.name, r.name LIMIT 10` |
|
|
22
|
+
| 数据资产 | 实体图片(本地存储,独立表)、RDF Turtle 导出、SQLite 整库导出/导入(自动备份保存点)、单文件只读网页另存 |
|
|
23
|
+
| 保存点 | 每次合规写入自动打 Git 保存点,违规写入自动回滚到上个合规版本;可一键回溯任意历史版本 |
|
|
24
|
+
| MCP Server | 零依赖 stdio JSON-RPC,10 个工具可直接接入 Claude Desktop / OpenCode 等支持 MCP 的客户端 |
|
|
25
|
+
| 双端适配 | 桌面端完整面板;手机端单行工具栏+抽屉面板+文件菜单 |
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
环境要求:Node.js >= 22.5(使用内置 node:sqlite),Windows / macOS / Linux 均可,无需数据库安装。
|
|
30
|
+
|
|
31
|
+
### npm 安装(推荐)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i -g local-knowledge-graph
|
|
35
|
+
kg
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
一条命令安装,一条命令启动(自动打开浏览器)。数据保存在用户目录 `~/.local-knowledge-graph/`,升级(`kg` 内一键更新或 `npm i -g local-knowledge-graph@latest`)不影响数据。也可以免安装试用:`npx local-knowledge-graph`。
|
|
39
|
+
|
|
40
|
+
启动参数:`kg --port 3000 --host 127.0.0.1 --data <目录> --no-open`(详见 `kg --help`)。
|
|
41
|
+
|
|
42
|
+
### 脚本启动(Git 克隆 / 开发模式)
|
|
43
|
+
|
|
44
|
+
| 平台 | 操作 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| Windows | 双击 `start.bat` |
|
|
47
|
+
| macOS | 双击 `start.command`(若提示无法打开:右键 → 打开;若提示无执行权限:终端执行一次 `chmod +x start.command start.sh`) |
|
|
48
|
+
| Linux | `./start.sh`(首次可能需 `chmod +x start.sh`) |
|
|
49
|
+
|
|
50
|
+
脚本自动完成:Node 版本检查 → 首次自动安装依赖(仅 express,几秒)→ 启动服务 → 自动打开浏览器 `http://localhost:3000`。此模式数据存放在项目内 `data/` 目录,应用内"一键更新"走 git 快进拉取。
|
|
51
|
+
|
|
52
|
+
### 手动启动
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 环境要求:Node.js >= 22.5(使用内置 node:sqlite)
|
|
56
|
+
npm install
|
|
57
|
+
|
|
58
|
+
# 启动(默认 http://localhost:3000)
|
|
59
|
+
npm start
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
AI 导入功能需要安装 [OpenCode CLI](https://opencode.ai) 并配置可用模型;其余功能(可视化/推理/检索配置/导出/MCP)无外部依赖。
|
|
63
|
+
|
|
64
|
+
## 目录结构
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
server.js Express 服务与全部 API
|
|
68
|
+
lib/db.js SQLite 三表核心(entities/relations/operation_logs)+ 图片表 + 向量表 + 迷你Cypher
|
|
69
|
+
lib/validator.js 手工/AI 共用的合规校验(违规即整体拒绝,库保持上个合规版本)
|
|
70
|
+
lib/agent.js OpenCode CLI 封装(普通问答与文档导入两种模式)
|
|
71
|
+
lib/git.js data/ 目录 Git 保存点(差量提交与回溯)
|
|
72
|
+
lib/inference.js OWL 推理引擎(虚拟推导,不写库)
|
|
73
|
+
lib/embeddings.js 向量构建/检索(data/settings.json 存配置,已被 gitignore)
|
|
74
|
+
lib/rdf.js RDF Turtle 导出
|
|
75
|
+
lib/viewer.js 单文件只读网页查看器生成
|
|
76
|
+
mcp/server.js MCP Server(stdio)
|
|
77
|
+
tools/ego.js 中心层级查询 CLI(只读)
|
|
78
|
+
public/index.html 单文件前端(无构建步骤)
|
|
79
|
+
data/ kg.db、ontology.json、settings.json、uploads/(gitignore,不出仓库)
|
|
80
|
+
vendor/ three.js 本地副本(全离线)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 数据模型
|
|
84
|
+
|
|
85
|
+
三张严格字段表 + 两张资产表:
|
|
86
|
+
|
|
87
|
+
- `entities(id, name, category, attributes, source, created_at)` — category 枚举:物理实体/抽象实体/数值实体/时间实体,attributes 为扁平 JSON
|
|
88
|
+
- `relations(id, source_id, target_id, name, category, created_at)` — category 枚举:归属/空间/时间/互动/属性
|
|
89
|
+
- `operation_logs(id, op_type, snapshot, source, created_at)` — 全部写入留痕,snapshot 为操作前后JSON快照
|
|
90
|
+
- `entity_images` / `entity_embeddings` — 图片与向量资产,不进入图谱保存点语义
|
|
91
|
+
|
|
92
|
+
## API 一览(http://localhost:3000/api)
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
GET /meta 版本与计数
|
|
96
|
+
GET/POST /entities 实体 CRUD(PUT/DELETE /entities/:id)
|
|
97
|
+
GET/POST /relations 关系 CRUD(PUT/DELETE /relations/:id)
|
|
98
|
+
GET /graph 全图(含图片计数)
|
|
99
|
+
GET /graph/ego?center=&depth= 中心层级子图(depth 省略或 0 = 全部层级)
|
|
100
|
+
GET /inference?center= 推理关系(含推导依据与规则)
|
|
101
|
+
GET/PUT /ontology 推理规则配置
|
|
102
|
+
GET/PUT /embeddings/settings 检索配置(密钥不回传)
|
|
103
|
+
POST /embeddings/build 构建全量实体向量
|
|
104
|
+
POST /search 混合检索 {query, top_k}
|
|
105
|
+
POST /cypher 迷你 Cypher {query}
|
|
106
|
+
GET/POST /entities/:id/images 图片绑定(DELETE /images/:imgId)
|
|
107
|
+
POST /agent OpenCode 问答+kg-ops 写入
|
|
108
|
+
POST /agent/doc 文档批量导入(分块+保存点对齐)
|
|
109
|
+
GET/POST /git/history|savepoint|restore 保存点
|
|
110
|
+
GET /export/rdf RDF Turtle
|
|
111
|
+
GET /export/db?name= SQLite 整库导出
|
|
112
|
+
POST /graph/import SQLite 整库导入(自动备份+校验)
|
|
113
|
+
GET /export/html 单文件只读网页
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## kg-ops 写入协议
|
|
117
|
+
|
|
118
|
+
AI 回答中输出 ```kg-ops 代码块(JSON 数组)即可写库,全部操作过校验、留日志、打保存点:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
[
|
|
122
|
+
{"op":"add_entity","ref":"A","name":"大雁塔","category":"物理实体","attributes":{"朝代":"唐"}},
|
|
123
|
+
{"op":"add_relation","source_ref":"A","target_id":58,"name":"位于","category":"空间"}
|
|
124
|
+
]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
支持操作:`add_entity / update_entity / delete_entity / add_relation / update_relation / delete_relation`。
|
|
128
|
+
|
|
129
|
+
## MCP 接入
|
|
130
|
+
|
|
131
|
+
`mcp/server.js` 零外部依赖,stdio 传输。以 Claude Desktop 为例(`claude_desktop_config.json`):
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"local-kg": {
|
|
137
|
+
"command": "node",
|
|
138
|
+
"args": ["/absolute/path/to/local-knowledge-graph/mcp/server.js"],
|
|
139
|
+
"env": { "KG_MCP_READONLY": "1" }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
工具列表:`kg_stats / kg_list_entities / kg_get_entity / kg_get_graph / kg_ego / kg_search / kg_cypher / kg_inference / kg_export_rdf / kg_apply_ops`。设 `KG_MCP_READONLY=1` 时拒绝全部写入。
|
|
146
|
+
|
|
147
|
+
## 向量检索配置
|
|
148
|
+
|
|
149
|
+
打开前端"检索"页签,填入硅基流动(或任意 OpenAI 兼容 /embeddings 接口)的 API Key 后保存,点击"构建全量向量"。配置仅存本机 `data/settings.json`(该目录已被 .gitignore 排除)。
|
|
150
|
+
|
|
151
|
+
## CLI:中心层级查询
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
node tools/ego.js "郑和" # 全部层级
|
|
155
|
+
node tools/ego.js "郑和" 2 # 2 层内
|
|
156
|
+
node tools/ego.js --json 3 郑和 # JSON 输出
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
退出码:0 成功 / 1 未找到 / 2 名称多义(附候选) / 3 参数错误。
|
|
160
|
+
|
|
161
|
+
## 隐私与安全
|
|
162
|
+
|
|
163
|
+
- 除 AI 导入/问答需联网调用 OpenCode 外,其余功能完全离线
|
|
164
|
+
- `data/`(数据库、密钥、图片)与 `node_modules/` 均不出仓库
|
|
165
|
+
- 前端密钥输入框不回传真实值,服务端只存本地文件
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// local-knowledge-graph 命令行入口
|
|
5
|
+
// 用法: kg [--port 3000] [--host 127.0.0.1] [--data <目录>] [--no-open] [-v|--version] [-h|--help]
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
function argValue(name, short) {
|
|
10
|
+
const i = args.indexOf(name);
|
|
11
|
+
const j = short ? args.indexOf(short) : -1;
|
|
12
|
+
const idx = i >= 0 ? i : j;
|
|
13
|
+
return idx >= 0 ? args[idx + 1] : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
17
|
+
console.log(`本地知识图谱整合器 (local-knowledge-graph)
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
kg [选项]
|
|
21
|
+
|
|
22
|
+
选项:
|
|
23
|
+
--port <n> 服务端口(默认 3000,环境变量 PORT 同效)
|
|
24
|
+
--host <addr> 监听地址(默认 127.0.0.1,局域网访问用 0.0.0.0,环境变量 KG_HOST 同效)
|
|
25
|
+
--data <dir> 数据目录(默认 ~/.local-knowledge-graph,环境变量 KG_DATA_DIR 同效)
|
|
26
|
+
--no-open 启动后不自动打开浏览器
|
|
27
|
+
-v, --version 显示版本
|
|
28
|
+
-h, --help 显示本帮助
|
|
29
|
+
|
|
30
|
+
数据说明: 图谱数据、图片、API Key 全部保存在本地数据目录,升级卸载均不影响。`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
35
|
+
console.log(require('../package.json').version);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { ensureLegacyMigration, isDevRepo } = require('../lib/paths');
|
|
40
|
+
|
|
41
|
+
const port = argValue('--port');
|
|
42
|
+
const host = argValue('--host');
|
|
43
|
+
const dataDir = argValue('--data');
|
|
44
|
+
if (port) process.env.PORT = port;
|
|
45
|
+
if (host) process.env.KG_HOST = host;
|
|
46
|
+
if (dataDir) process.env.KG_DATA_DIR = dataDir;
|
|
47
|
+
|
|
48
|
+
ensureLegacyMigration();
|
|
49
|
+
|
|
50
|
+
// 启动后轮询就绪,自动打开浏览器
|
|
51
|
+
if (!args.includes('--no-open')) {
|
|
52
|
+
const listenPort = Number(process.env.PORT || 3000);
|
|
53
|
+
const url = `http://localhost:${listenPort}`;
|
|
54
|
+
const started = Date.now();
|
|
55
|
+
const timer = setInterval(() => {
|
|
56
|
+
fetch(`${url}/api/version`, { signal: AbortSignal.timeout(1500) })
|
|
57
|
+
.then((r) => {
|
|
58
|
+
if (r.ok || Date.now() - started > 20000) {
|
|
59
|
+
clearInterval(timer);
|
|
60
|
+
if (r.ok) {
|
|
61
|
+
console.log(`已在浏览器打开 ${url}(如未弹出请手动访问)`);
|
|
62
|
+
openBrowser(url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch(() => { if (Date.now() - started > 25000) clearInterval(timer); });
|
|
67
|
+
}, 800);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function openBrowser(url) {
|
|
71
|
+
const { spawn } = require('child_process');
|
|
72
|
+
const cmds = process.platform === 'win32' ? [['cmd', ['/c', 'start', '', url]]]
|
|
73
|
+
: process.platform === 'darwin' ? [['open', [url]]]
|
|
74
|
+
: [['xdg-open', [url]]];
|
|
75
|
+
try {
|
|
76
|
+
const [cmd, cargs] = cmds[0];
|
|
77
|
+
spawn(cmd, cargs, { detached: true, stdio: 'ignore' }).unref();
|
|
78
|
+
} catch (_) { /* 打不开就让用户手动访问 */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
require('../server.js');
|
|
82
|
+
|
|
83
|
+
// 开发仓库模式提示数据位置(npm模式由 server bootstrap 迁移逻辑打印)
|
|
84
|
+
if (isDevRepo()) {
|
|
85
|
+
console.log('[开发模式] 检测到Git仓库,数据目录使用项目内 ./data');
|
|
86
|
+
}
|
package/lib/agent.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// OpenCode进程管理:后端默认拉起OpenCode进程,前端自然语言指令直接传递执行。
|
|
4
|
+
// OpenCode仅负责理解指令与联网补全,所有写库动作必须经本进程RDF校验后落库,禁止直连数据库。
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const { APP_ROOT, DATA_DIR } = require('./paths');
|
|
11
|
+
const SESSION_FILE = path.join(DATA_DIR, '.agent_session');
|
|
12
|
+
const TIMEOUT_MS = Number(process.env.AGENT_TIMEOUT_MS || 300000);
|
|
13
|
+
|
|
14
|
+
function readSession() {
|
|
15
|
+
try { return fs.readFileSync(SESSION_FILE, 'utf8').trim() || null; } catch (_) { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeSession(id) {
|
|
19
|
+
if (id) fs.writeFileSync(SESSION_FILE, id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 构建Agent提示词:注入图谱摘要、RDF规范、输出协议
|
|
23
|
+
function buildPrompt(instruction) {
|
|
24
|
+
const database = require('./db');
|
|
25
|
+
const graph = database.getGraph();
|
|
26
|
+
const entLines = graph.entities.map((e) => {
|
|
27
|
+
let attrs = {};
|
|
28
|
+
try { attrs = JSON.parse(e.attributes || '{}'); } catch (_) {}
|
|
29
|
+
const a = Object.keys(attrs).length ? ` 属性:${JSON.stringify(attrs)}` : '';
|
|
30
|
+
return `- id=${e.id} "${e.name}" [${e.category}]${a}`;
|
|
31
|
+
}).join('\n');
|
|
32
|
+
const relLines = graph.relations.map((r) => `- id=${r.id} e${r.source_id} --(${r.category}/${r.name})--> e${r.target_id}`).join('\n');
|
|
33
|
+
|
|
34
|
+
return `你是本地知识图谱的管理助手,通过OpenCode进程运行。你可以使用联网搜索补全公开信息,但必须遵守:
|
|
35
|
+
|
|
36
|
+
【铁律】
|
|
37
|
+
1. 本地图谱数据只增不删(除非用户明确要求删除/修改)。
|
|
38
|
+
2. 联网补全的信息必须经过你核对(交叉验证可信来源)后才允许写入;无法核实的不要写入,并在说明中告知用户原因。
|
|
39
|
+
3. 禁止向任何外部服务上传或泄露本地图谱的私有数据;联网时只查询公开信息。
|
|
40
|
+
|
|
41
|
+
【当前图谱】
|
|
42
|
+
实体 ${graph.entities.length} 个:
|
|
43
|
+
${entLines || '(空)'}
|
|
44
|
+
关系 ${graph.relations.length} 条:
|
|
45
|
+
${relLines || '(空)'}
|
|
46
|
+
|
|
47
|
+
【RDF规范(必须严格遵守)】
|
|
48
|
+
- 实体大类只能是: 物理实体 / 抽象实体 / 数值实体 / 时间实体
|
|
49
|
+
- 关系大类只能是: 空间 / 互动 / 归属 / 时间 / 属性
|
|
50
|
+
- 实体属性(attributes)必须是扁平JSON对象,值为字符串/数值/布尔,禁止嵌套对象或数组
|
|
51
|
+
- 关系的 source/target 只能引用已存在实体id,或用ref占位符引用本次新增的实体
|
|
52
|
+
- 建模准则:数值实体/时间实体仅在"数值或时间作为某条关系的端点"时才创建(如"事件—发生于→744年");仅作描述说明的年份、日期、数量(如成立年份、测量年份、海拔、人口)一律写入所属实体的attributes,禁止为它们单独建节点
|
|
53
|
+
|
|
54
|
+
【本地查询工具】
|
|
55
|
+
- 需要聚焦某个实体的关联范围(直接或间接关系)时,运行只读命令:node tools/ego.js "<实体名或id>" [层数]
|
|
56
|
+
输出该中心 N 层内的实体(按层级分组)与关系;层数省略表示全部层级。该命令仅本地读库,无需联网。
|
|
57
|
+
- 需要按关键词或关系模式检索时,运行只读命令:
|
|
58
|
+
node tools/search.js keyword "<关键词>" [数量](名称/属性关键词检索,附每个实体的关系列表)
|
|
59
|
+
node tools/search.js cypher "MATCH (a)-[r:互动]->(b) WHERE a.name contains 词 RETURN a,r,b LIMIT 20"(按关系类型/名称与实体条件检索)
|
|
60
|
+
建议先用 keyword 找到实体名,再用 cypher 按关系类型展开。
|
|
61
|
+
- 需要挖掘隐性/间接关系(如"大雁塔和长安有什么联系")时,运行只读命令:node -e "const{computeInferred}=require('./lib/inference');const db=require('./lib/db');const g=db.getGraph();console.log(JSON.stringify(computeInferred(g),null,1))"
|
|
62
|
+
该推理引擎按OWL传递/对称/逆规则推导库中未显式存储的关系(含推导依据)。也可访问 http://localhost:3000/api/inference 获取同样结果。回答时请区分"库中显式关系"与"推理得出的隐性关系"。
|
|
63
|
+
|
|
64
|
+
【输出协议】
|
|
65
|
+
第一步:用简洁中文说明你的分析与结论(若涉及联网信息,注明来源可信度)。
|
|
66
|
+
第二步:如需修改图谱,输出一个 \`\`\`kg-ops 代码块,内容为JSON数组(仅这一种操作格式),元素形如:
|
|
67
|
+
{"op":"add_entity","ref":"A","name":"实体名","category":"物理实体","attributes":{"键":"值"}}
|
|
68
|
+
{"op":"add_relation","source_ref":"A","target_id":3,"name":"关系名","category":"归属"}
|
|
69
|
+
{"op":"update_entity","id":5,"attributes":{"键":"新值"}}
|
|
70
|
+
{"op":"delete_entity","id":7}
|
|
71
|
+
{"op":"update_relation","id":2,"name":"新名"}
|
|
72
|
+
{"op":"delete_relation","id":4}
|
|
73
|
+
(source_id/target_id用数字id引用已有实体;本次新增的实体用ref占位符,如"ref":"A"供后续关系引用)
|
|
74
|
+
第三步:若无任何图谱修改,省略代码块。
|
|
75
|
+
|
|
76
|
+
【用户指令】
|
|
77
|
+
${instruction}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 从输出中提取全部 kg-ops 代码块(文档分块抽取时一次回复可含多个块)
|
|
81
|
+
function parseOps(output) {
|
|
82
|
+
const re = /```kg-ops\s*([\s\S]*?)```/g;
|
|
83
|
+
const ops = [];
|
|
84
|
+
let parse_error = null;
|
|
85
|
+
let m;
|
|
86
|
+
while ((m = re.exec(output))) {
|
|
87
|
+
try {
|
|
88
|
+
const arr = JSON.parse(m[1].trim());
|
|
89
|
+
if (Array.isArray(arr)) ops.push(...arr);
|
|
90
|
+
else parse_error = 'kg-ops块必须是JSON数组';
|
|
91
|
+
} catch (e) {
|
|
92
|
+
parse_error = `kg-ops块JSON解析失败: ${e.message}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const reply = output.replace(/```kg-ops\s*[\s\S]*?```/g, '').trim();
|
|
96
|
+
return { ops, reply, parse_error };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 解析OpenCode JSON事件流:提取文本回复、错误事件与会话id
|
|
100
|
+
function parseEventStream(stdout) {
|
|
101
|
+
const texts = [];
|
|
102
|
+
const errors = [];
|
|
103
|
+
let sessionIdFound = null;
|
|
104
|
+
for (const line of String(stdout).split('\n')) {
|
|
105
|
+
const t = line.trim();
|
|
106
|
+
if (!t.startsWith('{')) continue;
|
|
107
|
+
try {
|
|
108
|
+
const j = JSON.parse(t);
|
|
109
|
+
if (j.sessionID && !sessionIdFound) sessionIdFound = j.sessionID;
|
|
110
|
+
if (j.type === 'text' && j.part && j.part.text) texts.push(j.part.text);
|
|
111
|
+
const parts = j.parts || (j.message && j.message.parts) || [];
|
|
112
|
+
for (const p of parts) if (p.type === 'text' && p.text) texts.push(p.text);
|
|
113
|
+
if (j.type === 'error') {
|
|
114
|
+
const msg = (j.error && ((j.error.data && j.error.data.message) || j.error.message)) || '未知错误';
|
|
115
|
+
errors.push(`${msg}${j.error && j.error.data && j.error.data.ref ? `(ref:${j.error.data.ref})` : ''}`);
|
|
116
|
+
}
|
|
117
|
+
} catch (_) { /* 非JSON行,忽略 */ }
|
|
118
|
+
}
|
|
119
|
+
return { texts: texts.join('\n'), errors, sessionId: sessionIdFound };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 拉起OpenCode进程执行指令;服务端错误时自动清除会话并用全新会话重试一次
|
|
123
|
+
// files: 附件路径数组(经 opencode run -f 挂载)
|
|
124
|
+
async function runAgent(instruction, files = []) {
|
|
125
|
+
if (!instruction || !String(instruction).trim()) {
|
|
126
|
+
return { ok: false, error: '指令不能为空' };
|
|
127
|
+
}
|
|
128
|
+
let result = await attempt(readSession(), String(instruction), files);
|
|
129
|
+
if (!result.ok && result.retriable && readSession()) {
|
|
130
|
+
// 会话可能已失效或服务端状态异常,清除后用全新会话重试
|
|
131
|
+
clearSession();
|
|
132
|
+
const retry = await attempt(null, String(instruction), files);
|
|
133
|
+
if (retry.ok) return { ...retry, retried: true };
|
|
134
|
+
return { ...retry, retried: true, error: `重试仍失败: ${retry.error}` };
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 底层进程调用:拉起 opencode run --format json,收集事件流(attempt与runPlain共用)
|
|
140
|
+
// preArgs: 位于prompt之前的参数(如 --session);prompt必须在-f之前(-f是贪婪数组选项)
|
|
141
|
+
function runOpenCodeRaw(preArgs, prompt, files, timeoutMs) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const args = ['run', '--format', 'json', '--auto', ...preArgs, prompt];
|
|
144
|
+
for (const f of files || []) {
|
|
145
|
+
if (f && fs.existsSync(f)) args.push('-f', f);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let stdout = '', stderr = '', done = false;
|
|
149
|
+
let child;
|
|
150
|
+
try {
|
|
151
|
+
child = spawn('opencode', args, { cwd: APP_ROOT, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return resolve({ ok: false, error: '无法启动OpenCode进程: ' + e.message });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
if (!done) { done = true; try { child.kill('SIGKILL'); } catch (_) {} resolve({ ok: false, error: `OpenCode执行超时(${timeoutMs / 1000}s),已终止` }); }
|
|
158
|
+
}, timeoutMs);
|
|
159
|
+
|
|
160
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
161
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
162
|
+
child.on('error', (e) => {
|
|
163
|
+
if (done) return; done = true; clearTimeout(timer);
|
|
164
|
+
resolve({ ok: false, error: `OpenCode进程启动失败: ${e.message}(请确认 opencode CLI 已安装)` });
|
|
165
|
+
});
|
|
166
|
+
child.on('close', (code) => {
|
|
167
|
+
if (done) return; done = true; clearTimeout(timer);
|
|
168
|
+
|
|
169
|
+
if (code !== 0 && !stdout.trim()) {
|
|
170
|
+
return resolve({ ok: false, error: `OpenCode进程退出码${code}: ${(stderr || '无输出').slice(0, 500)}` });
|
|
171
|
+
}
|
|
172
|
+
const ev = parseEventStream(stdout);
|
|
173
|
+
// 出现error事件:服务端执行失败;保留已产生的部分回复供参考
|
|
174
|
+
if (ev.errors.length) {
|
|
175
|
+
return resolve({
|
|
176
|
+
ok: false,
|
|
177
|
+
retriable: true,
|
|
178
|
+
error: `OpenCode执行出错: ${ev.errors[0]}`,
|
|
179
|
+
partial_reply: ev.texts || null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
resolve({ ok: true, texts: ev.texts || stdout.trim(), sessionId: ev.sessionId || null });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function attempt(sessionId, instruction, files) {
|
|
188
|
+
const preArgs = sessionId ? ['--session', sessionId] : [];
|
|
189
|
+
return runOpenCodeRaw(preArgs, buildPrompt(instruction), files, TIMEOUT_MS).then((r) => {
|
|
190
|
+
if (!r.ok) return r;
|
|
191
|
+
if (r.sessionId) writeSession(r.sessionId);
|
|
192
|
+
const replyText = r.texts;
|
|
193
|
+
const parsed = parseOps(replyText);
|
|
194
|
+
return { ok: true, reply: parsed.reply || replyText, ops: parsed.ops, parse_error: parsed.parse_error || null, session: readSession() };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 纯文本问答模式(智能检索编译/综述用):无会话、无 kg-ops 解析,仅取回复文本
|
|
199
|
+
async function runPlain(prompt, timeoutMs = 20000) {
|
|
200
|
+
const p = String(prompt || '').trim();
|
|
201
|
+
if (!p) return { ok: false, error: 'prompt不能为空' };
|
|
202
|
+
const r = await runOpenCodeRaw([], p, [], timeoutMs);
|
|
203
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
204
|
+
return { ok: true, text: r.texts || '' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function clearSession() {
|
|
208
|
+
try { fs.unlinkSync(SESSION_FILE); } catch (_) { /* 文件不存在则忽略 */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { buildPrompt, runAgent, runPlain, parseOps, parseEventStream };
|