@nsyan/db 1.1.0 → 1.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 CHANGED
@@ -4,171 +4,100 @@
4
4
 
5
5
  **AI 接入数据库插件 —— 方言化架构,原生 Node.js 实现**
6
6
 
7
- 支持关系型 / KV / 搜索 / 文档 / 大数据五大家族,共 9 种数据库的查询、表结构浏览与项目配置扫描建连。
8
-
9
- ![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)
7
+ [![npm](https://img.shields.io/npm/v/@nsyan/db?color=blue)](https://www.npmjs.com/package/@nsyan/db)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](../../LICENSE)
10
9
 
11
10
  </div>
12
11
 
12
+ 为 [pi](https://github.com/earendilofficial/pi) 提供 10 种数据库的查询、表结构浏览、扫描建连与安全管控:AI 通过 5 个工具读写数据库,用户通过 `/db` 菜单管理连接。
13
+
13
14
  ## ✨ 特性
14
15
 
15
- - **方言化架构**:`core/` + `dialects/` pi 运行时依赖,为 MCP server 复用铺路
16
- - **9 种数据库**:PostgreSQL · MySQL · Oracle · 达梦 DM8 · Redis · Elasticsearch · MongoDB · Hive · Spark(Thrift Server)
17
- - **五大家族**:关系型 / KV / 搜索 / 文档 / 大数据,各有一套共享基类
18
- - **安全强化**:
19
- - 多语句**逐条**检查,`SELECT 1; DROP TABLE x` 无法绕过
20
- - 注释剥离后再匹配,`/* c */ DELETE` 无法绕过
21
- - CTE-DML / `FOR UPDATE` 识别为写操作
22
- - Redis 命令白名单(只读模式放行读命令,`FLUSHALL/CONFIG/SHUTDOWN` 恒拒)
23
- - ES 端点白名单(只读模式放行读端点,`DELETE index` 恒拒)
24
- - MongoDB JSON 命令信封白名单(读命令白名单,`drop*`/`create*`/管理命令/服务端 JS `$where/$function/$accumulator` 恒拒,aggregate 含 `$out/$merge` 按写分类)
25
- - **代码扫描建连** `/db scan`:从 Spring 配置 / docker-compose / .env 自动抽取连接候选
26
- - **P0 体验**:一键连接串(自动识别家族)、默认连接(`database` 参数可选)、`list_tables` pattern 过滤、超 50 行结果自动导出 CSV/JSON
16
+ - **10 种数据库 · 6 家族**:PostgreSQL · MySQL · Oracle · 达梦 · Redis · Elasticsearch · MongoDB · Neo4j · Hive · Spark
17
+ - **AI 工具**:查询 / 表结构 / 连接清单 / 扫描建连,读多写少场景的 token 友好输出
18
+ - **扫描建连**:从 Spring / docker-compose / .env 自动抽取连接候选,终端确认后写入
19
+ - **安全模型**:只读模式、写确认 + 执行理由、连接级强制只读、家族白名单、管理命令恒拒
20
+ - **审计**(可选):写操作按天落盘,含理由与完整 SQL
21
+ - **零构建**:TypeScript pi/tsx 直接加载,core/dialects 零 pi 运行时依赖
27
22
 
28
23
  ## 📦 安装
29
24
 
30
- **npm 安装(推荐,任意机器可用):**
31
-
32
- ```bash
33
- pi install npm:@nsyan/db
34
- ```
35
-
36
- **本地源码安装(本仓库开发者):**
37
-
38
25
  ```bash
39
- # 在本仓库根目录执行
40
- pi install ./packages/db
26
+ pi install npm:@nsyan/db # npm 安装(推荐)
27
+ pi install ./packages/db # 本仓库开发者
41
28
  ```
42
29
 
43
- > 依赖 `pg` / `mysql2` / `oracledb` / `dmdb` / `ioredis` / `@elastic/elasticsearch` / `es7` / `mongodb` / `hive-driver` / `typebox`,`pi install` 自动安装。
44
-
45
30
  ## 🚀 快速开始
46
31
 
47
- ```bash
48
- /db add # 新增连接(一键粘贴连接串,或按家族逐步填写)
49
- /db ls # 列出所有连接
50
- /db scan # 扫描项目源码自动建连
51
- /db config # 查看/修改全局设置
52
- ```
53
-
54
- AI 侧接入后,直接用 `query_database` / `list_tables` / `describe_table` / `scan_project_configs` / `db_connections` 五个工具即可。
55
-
56
- ## 🗂️ 连接管理
57
-
58
- - **状态摘要列表**:连接选择器展示 `名称 [类型] ⭐默认 [prod·强制只读] · 上次使用 2 天前 · 测试 ✓45ms`,打开/编辑/删除/切默认共用
59
- - **⚡ 切换默认**:一级菜单直达(选中即切),无需进入连接动作菜单
60
- - **🧪 测试连接**:连接动作菜单内独立入口;**编辑保存后自动回测**,结果回显
61
- - **📋 从现有复制**:新增连接第三条路径,复制类型/账号/环境标签等全部设置,仅需改名
62
- - **环境标签 + 强制只读**:连接可标 `dev/test/prod`;`prod` 建连时主动建议开启**强制只读**——开启后该连接无视全局只读开关,永远只接受查询(AI 侧写入会被拒并提示原因)
63
- - **db_connections 工具**:AI 可自查连接清单与环境标签,说“在生产库查一下”时先识别哪条是 prod
64
- - 最近使用时间与测试结果自动回写,无需手工维护
32
+ 1. `/db add` 粘贴连接串一键建连(支持 `postgresql://` · `jdbc:*` · `redis://` · `mongodb+srv://` · `bolt://`/`neo4j://` · `http://host:9200`)
33
+ 2. 直接让 AI 查询:*「查一下订单表最近 10 条」*
34
+ 3. `/db scan` 从项目源码自动发现数据库连接(密码只在终端补录,不进模型上下文)
65
35
 
66
36
  ## 🗄️ 支持的数据库
67
37
 
68
- | 数据库 | 家族 | 驱动 | 支持版本 | 备注 |
69
- |--------|------|------|---------|------|
70
- | PostgreSQL | 关系型 | `pg` | 9.6 ~ 17 全系 | `statement_timeout` 全版本可用 |
71
- | MySQL | 关系型 | `mysql2` v3 | 5.7 / 8.0 / 8.4 | `max_execution_time` 需 ≥5.7.8,低版本自动忽略(客户端超时仍生效) |
72
- | Oracle | 关系型 | `oracledb`(Thin,零依赖) | ≥12.1 | Thin 模式硬性要求 DB ≥12.1,11g 及以下不支持(会给出版本原因指引);Thick 模式列入后续迭代 |
73
- | 达梦 DM | 关系型 | `dmdb` | DM8 全系;DM9 未验证 | 纯 JS 依赖,macOS/Linux/Windows 可用 |
74
- | Redis | KV | `ioredis` | 2.8 ~ 8.x | `SCAN` 需 ≥2.8;`MEMORY USAGE` 需 ≥4.0,低版本自动跳过;生产库一律 SCAN,禁 `KEYS` |
75
- | Elasticsearch | 搜索 | `@elastic/elasticsearch` v8 + v7(npm 别名 `es7`) | 7.x / 8.x / 更新大版本 | 连接时探测大版本分发对应客户端;未知大版本用最新客户端尝试并给出版本警告 |
76
- | MongoDB | 文档 | `mongodb`(官方驱动,纯 JS) | **已验证主流区 6.0 / 7.0 / 8.0**;4.2~5.x 可用未验证(4.x 已 EOL) | `query_database` 的 sql 参数填 JSON 命令信封;`mongodb://` 与 Atlas `mongodb+srv://` 双形态;`describe_table` 优先读 `$jsonSchema` validator,缺失时采样 ≤100 文档推断字段(非权威);无 limit 自动补 maxRows |
77
- | Hive | 大数据 | `hive-driver` | 目标 2.x4.x 未实测 | HS2 Thrift 协议;无 4.x 环境前声明仅支持 2/3 |
78
- | Spark(Thrift Server) | 大数据 | `hive-driver`(复用) | 2.x ~ 4.x | 与 Hive 同协议栈;Spark Connect(DataFrame/gRPC)不支持 |
79
-
80
- > 版本兼容采用**端点抽测**:每个支持项只实测「最老支持版 + 最新版」两个端点,中间版本声明兼容不实测。
81
-
82
- ## ⚙️ 新增连接
83
-
84
- `/db add` 首问选择「粘贴连接串(一键)」或「逐步填写」。连接串自动识别家族:
85
-
86
- ```
87
- postgresql://user:pass@host:5432/db → PostgreSQL
88
- jdbc:mysql://host:3306/db → MySQL
89
- jdbc:oracle:thin:@//host:1521/svc → Oracle(也支持 SID 形式)
90
- jdbc:dm://host:5236/schema → 达梦
91
- redis://:pass@host:6379/0 → Redis(含 rediss:// TLS)
92
- mongodb://user:pass@host:27017/db → MongoDB(含 +srv Atlas 形态,authSource/replicaSet 进 options)
93
- http://host:9200 → Elasticsearch
94
- jdbc:hive2://host:10000/db Hive / Spark
95
- ```
96
-
97
- 逐步填写按家族分支:Redis 无账号要求、需库号(dbIndex);MongoDB 账号可空(本地无认证常见);ES/其余 URL + 账号 + 密码。
38
+ | 数据库 | 家族 | 支持版本 |
39
+ |--------|------|---------|
40
+ | PostgreSQL | 关系型 | 9.6 ~ 17 |
41
+ | MySQL | 关系型 | 5.7 / 8.0 / 8.4 |
42
+ | Oracle | 关系型 | ≥12.1Thin 零依赖) |
43
+ | 达梦 DM | 关系型 | DM8 全系;DM9 未验证 |
44
+ | Redis | KV | 2.8 ~ 8.x |
45
+ | Elasticsearch | 搜索 | 7.x / 8.x+(自动探测大版本) |
46
+ | MongoDB | 文档 | 已验证 6.0 / 7.0 / 8.04.2+ 可用 |
47
+ | Neo4j | | 官方兼容矩阵 4.4 ~ 2025.x;已验证 4.4.29 community |
48
+ | Hive | 大数据 | 2.x / 3.x;4.x 未实测 |
49
+ | Spark(Thrift Server) | 大数据 | 2.x ~ 4.x |
50
+
51
+ > 版本采用**端点抽测**:只实测「最老支持版 + 最新版」,中间版本声明兼容。
52
+
53
+ ## 🧾 sql 参数形态
54
+
55
+ `query_database` 的 `sql` 参数按家族填对应形态:
56
+
57
+ | 家族 | 形态 | 示例 |
58
+ |------|------|------|
59
+ | KV · Redis | 空格分隔命令 | `GET key` / `SCAN 0 MATCH user:*` |
60
+ | 搜索 · ES | JSON DSL | `{"query": {"match_all": {}}}` |
61
+ | 文档 · MongoDB | JSON 命令信封 | `{"find": "users", "filter": {}}` |
62
+ | 图 · Neo4j | Cypher | `MATCH (n:Person) RETURN n LIMIT 10` |
63
+
64
+ 关系型直接填 SQL。各家族示例与实现要点 [docs/USAGE.md](./docs/USAGE.md)。
65
+
66
+ ## 🔐 安全模型
67
+
68
+ | 机制 | 行为 |
69
+ |------|------|
70
+ | AI 只读模式 | **默认开**;关闭后允许写 |
71
+ | 写确认 | 写操作弹框展示:理由 + 命令摘要 + SQL |
72
+ | 写执行理由 | AI 必须附 `reason`(动机+影响范围),缺失直接拒绝 |
73
+ | 连接级强制只读 | 标记的连接(如生产库)无视全局开关,永远只读 |
74
+ | 家族白名单 | Redis/ES/Mongo 只读白名单;`DROP`、管理 DDL、服务端 JS、`CALL dbms.*` 恒拒 |
75
+ | 审计日志 | 默认关;`/db config` 开启后写操作按天落盘(完整 SQL,0600) |
76
+ | 凭据 | 配置文件 `0600`;扫描场景密码掩码,不进模型上下文 |
98
77
 
99
- ## 🍃 MongoDB 命令信封
100
-
101
- MongoDB 走 `query_database`,`sql` 参数填 JSON 命令信封(`db.runCommand` 文档形态,单命令一次执行):
102
-
103
- ```json
104
- {"find": "users", "filter": {"age": {"$gt": 18}}, "sort": {"created_at": -1}}
105
- {"count": "users"}
106
- {"aggregate": "orders", "pipeline": [{"$group": {"_id": "$status", "n": {"$sum": 1}}}]}
107
- {"insert": "users", "documents": [{"name": "x"}]}
108
- ```
109
-
110
- - 未写 limit 的读命令自动补 `maxRows`,结果超限自动标 `truncated`
111
- - 返回文档拍平为列(顶层字段并集,封顶 50 列,嵌套转 JSON 字符串)
112
- - `list_tables` → `listCollections`;`describe_table` → `collStats` + 索引 + validator/采样字段推断
113
- - 硬限制:`drop*`/`create*` 等管理 DDL 与服务端 JS(`$where`/`$function`/`$accumulator`)恒拒;aggregate 含 `$out`/`$merge` 按写操作处理
114
-
115
- ## 🔎 代码扫描建连 `/db scan`
116
-
117
- 从项目源码自动抽取连接信息(Spring `application*.yml/properties`、`docker-compose.yml`、`.env`、通用 URL 正则):
118
-
119
- ```bash
120
- /db scan # 扫描当前工作目录
121
- /db scan ./backend # 扫描指定子目录(越界拒绝)
122
- ```
123
-
124
- 扫描结果按状态分组:✅ 可直接建 / ✏️ 待补字段 / 🔒 jasypt 加密(只标注不建)/ ⏭️ 同名已存在。逐个确认后才写入配置——**绝不静默建连、绝不静默覆盖**。占位符 `${KEY:default}` 取 default,`${KEY}` 依次查同目录 `.env` → 进程环境变量,缺省在终端追问。
125
-
126
- AI 侧说「连一下这个项目的数据库」等,会调用 `scan_project_configs` 展示**掩码后**的候选(不写盘),建连仍需你在终端确认,密码不进模型上下文。
127
-
128
- ## ⭐ 默认连接
129
-
130
- `/db` → 打开连接 → `⭐ 设为默认`。AI 调用工具时 `database` 参数可省略,缺省走默认连接;未设默认时给出指引。
131
-
132
- ## 💾 查询结果导出
133
-
134
- 查询结果超过 50 行时,完整结果自动导出到 `/tmp`(CSV + JSON 双格式),只返回文件路径。结果达到 `max_rows` 上限截断时有明确标注。
135
-
136
- ## 🔐 安全控制
78
+ ## ⚙️ 设置
137
79
 
138
- - **执行策略同步**:系统提示动态注入当前 AI 只读模式与执行确认设置
139
- - **逐语句检查**:多语句 SQL 逐条检查,注释剥离后再匹配
140
- - **硬限制**:DROP TABLE/DATABASE(关系型/大数据)、`FLUSHALL/FLUSHDB/CONFIG/SHUTDOWN`(Redis)、`DELETE <index>`(ES)、`drop*`/`create*`/管理命令/服务端 JS(MongoDB)
141
- - **只读白名单**:Redis 读命令白名单、ES 读端点白名单、MongoDB 读命令白名单(JS 执行恒拒)
142
- - **连接级强制只读**:标了 `forceReadonly` 的连接(如生产库)无视全局开关,永远只读
143
- - **写操作执行理由**:AI 发起写操作必须附 `reason`(动机+影响范围),确认框首行展示、随结果回显;缺失时直接拒绝并引导补充,与确认策略无关
144
- - **本地审计(默认关闭)**:`/db config → 审计日志` 开启后,写成功追加 `~/.pi/agent/db-audit/<YYYY-MM-DD>.jsonl`(按天分文件,含项目路径/连接/理由/**完整 SQL 不截断**,0600);审计失败不阻断主流程。清理示例:`find ~/.pi/agent/db-audit -name "*.jsonl" -mtime +90 -delete`。理由强校验不受此开关影响
145
- - **凭据降险**:连接配置含明文密码,保存时自动 `chmod 0600`(仅当前用户可读写);系统钥匙串(keychain)列入远期规划
146
- - **确认策略**:不确认 / 写操作确认 / 每次确认;无 UI 环境自动取消
147
- - **手工查询一致**:`/db` 菜单手工执行与 AI 工具使用相同的策略裁决
80
+ `/db config` 修改,每轮注入系统提示:
148
81
 
149
- ## ⚙️ 设置
82
+ | 设置项 | 默认值 |
83
+ |--------|--------|
84
+ | AI 只读模式 | 是 |
85
+ | 执行确认 | 写操作确认 |
86
+ | 最大行数 | 100 |
87
+ | 查询超时 | 30s |
88
+ | 审计日志 | 关 |
150
89
 
151
- 通过 `/db config` 修改,每轮注入系统提示:
90
+ ## 📚 详细文档
152
91
 
153
- | 设置项 | 默认值 | 说明 |
154
- |--------|--------|------|
155
- | AI 只读模式 | 是 | 开启时 AI 只能执行查询;关闭时允许写操作 |
156
- | 执行确认 | 写操作确认 | 不确认 / 写操作确认 / 每次都确认 |
157
- | 最大行数 | 100 | 查询返回的最大行数 |
158
- | 查询超时 | 30s | 单条语句超时(服务端设置失败时客户端兜底) |
92
+ 连接管理(三种建连路径 / 环境标签 / 自动回测)、扫描建连、各家族 sql 详解、审计配置 → **[docs/USAGE.md](./docs/USAGE.md)**
93
+ 更新记录 → [CHANGELOG](../../CHANGELOG.md)
159
94
 
160
95
  ## 📁 开发
161
96
 
162
97
  ```bash
163
- pnpm install
164
- cd packages/db
165
- NODE_ENV=development pnpm test # 运行全部单测(tsx + node:test)
98
+ pnpm install && cd packages/db && pnpm test # tsx + node:test,无构建产物
166
99
  ```
167
100
 
168
- ## 📜 更新记录
169
-
170
- 见仓库根目录 [CHANGELOG.md](../../CHANGELOG.md)。当前版本 **1.1.0**(2026-09-10):新增 MongoDB 支持(document 家族);连接管理体验升级(状态摘要/切换默认/从现有复制/环境标签+强制只读/db_connections 工具)。
171
-
172
101
  ## 📄 许可
173
102
 
174
103
  [MIT](./LICENSE)
package/docs/USAGE.md ADDED
@@ -0,0 +1,138 @@
1
+ # db 使用手册
2
+
3
+ 主 README 是能力速览;本页承接全部使用细节。遇到本文未覆盖的行为,以 `src/` 代码与测试为准。
4
+
5
+ ## 🔗 连接管理
6
+
7
+ ### 新增连接(/db add)
8
+
9
+ 首问三选一:
10
+
11
+ 1. **⚡ 粘贴连接串(一键)**——自动识别家族:
12
+
13
+ ```
14
+ postgresql://user:pass@host:5432/db → PostgreSQL
15
+ jdbc:mysql://host:3306/db → MySQL
16
+ jdbc:oracle:thin:@//host:1521/svc → Oracle(也支持 SID 形式)
17
+ jdbc:dm://host:5236/schema → 达梦
18
+ redis://:pass@host:6379/0 → Redis(含 rediss:// TLS)
19
+ mongodb://user:pass@host:27017/db → MongoDB(含 +srv Atlas 形态)
20
+ bolt://user:pass@host:7687/db → Neo4j(也认 neo4j://、bolt+s(ssc)://、neo4j+s(ssc)://)
21
+ http://host:9200 → Elasticsearch
22
+ jdbc:hive2://host:10000/db → Hive / Spark
23
+ ```
24
+
25
+ 2. **📝 逐步填写**——按家族分支:Redis 无账号要求、需库号(dbIndex);MongoDB 账号可空(本地无认证常见);ES/其余 URL + 账号 + 密码。
26
+
27
+ 3. **📋 从现有复制**——复制现有连接的全部设置(类型/账号/options/环境标签),仅需改名,可选立即编辑。
28
+
29
+ 所有路径在测试连接通过后,会追问「环境标签」(dev/test/prod,可跳过);选 prod 时主动建议开启**强制只读**。
30
+
31
+ ### 编辑与测试
32
+
33
+ - `/db edit` 编辑器支持修改名称/URL/账号/密码/环境标签/强制只读/说明
34
+ - **保存后自动回测**连接,结果即时回显并记录
35
+ - 连接动作菜单含独立「🧪 测试连接」,结果(版本/延迟)写入连接摘要
36
+
37
+ ### 状态摘要与默认连接
38
+
39
+ - 所有连接选择器展示:`名称 [类型] ⭐默认 [prod·强制只读] · 上次使用 2 天前 · 测试 ✓45ms`
40
+ - 最近使用时间与测试结果自动回写,无需手工维护
41
+ - 一级菜单「⚡ 切换默认」选中即切;AI 调用工具时 `database` 参数可省略,缺省走默认连接
42
+
43
+ ### 环境标签与强制只读
44
+
45
+ - 连接可标 `dev / test / prod`,列表与 AI 系统提示均可见
46
+ - `强制只读` 的连接**无视全局只读开关**,永远只接受查询;AI 侧写入会被拒并提示原因
47
+ - 典型用法:全局允许写(开发库随便改),生产库连接强制只读
48
+
49
+ ## 🔎 扫描建连(/db scan)
50
+
51
+ 从项目源码自动抽取连接信息(Spring `application*.yml/properties`、`docker-compose.yml`、`.env`、通用 URL 正则):
52
+
53
+ ```bash
54
+ /db scan # 扫描当前工作目录
55
+ /db scan ./backend # 扫描指定子目录(越界拒绝)
56
+ ```
57
+
58
+ - 结果状态:✅ 可直接建 / ✏️ 待补字段 / 🔒 jasypt 加密(只标注不建)/ ⏭️ 同名已存在
59
+ - **绝不静默建连、绝不静默覆盖**:逐个确认后才写入;占位符 `${KEY:default}` 取 default,`${KEY}` 依次查同目录 `.env` → 进程环境变量
60
+ - AI 侧说「连一下这个项目的数据库」会调用 `scan_project_configs` 展示**掩码后**候选,建连仍需终端确认
61
+
62
+ ## 🧾 各家族 sql 形态详解
63
+
64
+ ### Redis(KV)
65
+
66
+ `sql` 填空格分隔的命令,整体视为一条:
67
+
68
+ ```
69
+ GET mykey
70
+ SCAN 0 MATCH user:* COUNT 100 # 生产库一律 SCAN,禁 KEYS
71
+ HGETALL myhash
72
+ ```
73
+
74
+ 只读白名单:GET/HGETALL/LRANGE/ZRANGE/SCAN/INFO 等;`FLUSHALL/FLUSHDB/CONFIG/SHUTDOWN` 恒拒。`describe_table` 对 key 返回类型/长度/TTL/内存/编码/值预览。
75
+
76
+ ### Elasticsearch(搜索)
77
+
78
+ `sql` 填 JSON DSL(端点信封):
79
+
80
+ ```json
81
+ {"query": {"match_all": {}}}
82
+ {"count": {"query": {"match": {"status": "paid"}}}}
83
+ ```
84
+
85
+ 顶层 key 决定端点:`query/count/mget` → 读;`bulk/delete/update` → 写;未知 key 保守按写。`DELETE <index>` 字符串恒拒。
86
+
87
+ ### MongoDB(文档)
88
+
89
+ `sql` 填 JSON 命令信封(`db.runCommand` 文档形态,单命令一次执行):
90
+
91
+ ```json
92
+ {"find": "users", "filter": {"age": {"$gt": 18}}, "sort": {"created_at": -1}}
93
+ {"count": "users"}
94
+ {"aggregate": "orders", "pipeline": [{"$group": {"_id": "$status", "n": {"$sum": 1}}}]}
95
+ {"insert": "users", "documents": [{"name": "x"}]}
96
+ ```
97
+
98
+ 实现要点:
99
+
100
+ - **limit 注入**:未写 limit 的读命令自动补 `maxRows`(aggregate 自动追加 `$limit` 阶段),超限标 `truncated`
101
+ - **结果拍平**:返回文档拍平为列——顶层字段并集,封顶 50 列,嵌套转 JSON 字符串;`_id` 取 hex
102
+ - **list_tables** → `listCollections`;**describe_table** → `collStats` + 索引 + `$jsonSchema` validator(缺失时采样 ≤100 文档推断字段,非权威 schema)
103
+ - **硬限制**:`drop*`/`create*` 等管理 DDL、服务端 JS(`$where`/`$function`/`$accumulator`)恒拒;aggregate 含 `$out`/`$merge` 按写分类;未知命令保守按写
104
+ - 连接串:`mongodb://` 与 `mongodb+srv://`(SRV 默认 TLS);`authSource` 缺省 `admin`,`replicaSet`/`authMechanism` 等经 options 贯通
105
+
106
+ ### Neo4j(图)
107
+
108
+ `sql` 填 Cypher 原文,支持分号分隔多语句(逐条执行,返回最后一条结果):
109
+
110
+ ```cypher
111
+ MATCH (n:Person) RETURN n LIMIT 10
112
+ MATCH (n:Person)-[r:KNOWS]->(m) WHERE n.age > 18 RETURN n, r, m
113
+ CALL db.labels() YIELD label RETURN label
114
+ SHOW INDEXES
115
+ ```
116
+
117
+ 实现要点:
118
+
119
+ - **图结构即表**:`list_tables` 返回 node label(NODE LABEL)与关系类型(RELATIONSHIP,`rel:` 前缀);`describe_table` 目标填 label 名或 `rel:类型`
120
+ - **describe_table**:实体计数 + `SHOW INDEXES/CONSTRAINTS`(过滤目标)+ 采样 ≤100 推断属性键与类型分布(非权威 schema);唯一约束属性标主键;只依赖核心过程,**不依赖 APOC**
121
+ - **读写管控**:CREATE/MERGE/DELETE/DETACH/SET/REMOVE/DROP/FOREACH/LOAD CSV 任意深度出现即按写(`MATCH (n) DETACH DELETE n` 这类读外壳夹写拦得住);字符串字面量与注释内的写词不误判;`CALL dbms.*` 管理过程**恒拒**;未知 CALL 过程(含 apoc.*)保守按写
122
+ - **结果拍平**:Node 渲染为 `:Label {属性}`,Relationship 为 `-(TYPE)-> {属性}`,Path 为 `<path:n>`;无返回记录的写语句回显变更计数(创建节点 n,设置属性 m)
123
+ - **limit 封顶**:客户端截断对齐关系型方言(不做 Cypher LIMIT 注入,任意语句尾部加 LIMIT 不总合法)
124
+ - **连接串**:`bolt://`/`neo4j://`/`bolt+s://`/`neo4j+s://` 等;建连统一走 Bolt 直连(`bolt://`/`bolt+s://`)——单机社区版无路由服务,`neo4j://` 路由 scheme 会报 No routing servers available;URL 路径段 = 图数据库名(缺省 `neo4j`)
125
+ - **扫描建连**:Spring `spring.neo4j.uri`(Boot 3)/ `spring.data.neo4j.uri`(Boot 2)+ authentication 账号密码键、`.env` `NEO4J_URI`/`NEO4J_URL`/`BOLT_URL`、docker-compose `neo4j` 镜像(`NEO4J_AUTH`/`NEO4J_PASSWORD`)、通用 URL 正则
126
+
127
+ ## ✍️ 写操作:理由与审计
128
+
129
+ - AI 发起写操作必须附 `reason` 参数(动机 + 影响范围,如 *"将 status=2 的历史订单归档,预计影响 1.2 万行"*)
130
+ - 确认框**首行展示理由**,执行结果末尾回显;缺失时拒绝并引导补充(与确认策略无关)
131
+ - 开启审计(`/db config → 审计日志`)后,写成功追加 `~/.pi/agent/db-audit/<YYYY-MM-DD>.jsonl`:
132
+ - 一天一个文件;字段含时间/项目路径/连接/摘要/理由/**完整 SQL 不截断**
133
+ - 文件 `0600`;审计失败静默降级,不阻断主流程
134
+ - 清理示例:`find ~/.pi/agent/db-audit -name "*.jsonl" -mtime +90 -delete`
135
+
136
+ ## 💾 查询结果导出
137
+
138
+ 结果超过 50 行时自动导出 `/tmp`(CSV + JSON 双格式),只回文件路径;达到 `max_rows` 截断时有明确标注。
package/index.ts CHANGED
@@ -58,6 +58,8 @@ function familyHint(c: ConnConfig): string {
58
58
  return "关系型,sql 参数填 SQL";
59
59
  case "mongodb":
60
60
  return "MongoDB 文档库,sql 参数填 JSON 命令信封(如 {\"find\":\"users\",\"filter\":{}};读命令 find/count/distinct/aggregate)";
61
+ case "neo4j":
62
+ return "Neo4j 图数据库,sql 参数填 Cypher(如 MATCH (n:Person) RETURN n LIMIT 10;list_tables 列出 label 与关系类型,describe_table 目标填 label 名或 rel:类型)";
61
63
  default:
62
64
  return `${fullTypeLabel(c.type)},sql 参数填查询或命令`;
63
65
  }
@@ -264,11 +266,11 @@ export default function (pi: ExtensionAPI) {
264
266
  let dbIndex: number | undefined;
265
267
 
266
268
  if (mode === "⚡ 粘贴连接串(一键)") {
267
- const url = (await ctx.ui.input("连接串", "postgresql://user:pass@host:5432/db 或 jdbc:mysql://... 或 redis://:pass@host:6379/0 或 mongodb://user:pass@host:27017/db"))?.trim();
269
+ const url = (await ctx.ui.input("连接串", "postgresql://user:pass@host:5432/db 或 jdbc:mysql://... 或 redis://:pass@host:6379/0 或 mongodb://user:pass@host:27017/db 或 neo4j://user:pass@host:7687"))?.trim();
268
270
  if (!url) { ctx.ui.notify("连接串不能为空", "error"); return; }
269
271
  const pcs = parseConnectionString(url);
270
272
  if (!pcs) {
271
- ctx.ui.notify("连接串无法识别。支持: postgresql/mysql/oracle/dm/hive JDBC、redis(s)://、mongodb(srv)://、http(s)://host:9200", "error");
273
+ ctx.ui.notify("连接串无法识别。支持: postgresql/mysql/oracle/dm/hive JDBC、redis(s)://、mongodb(srv)://、neo4j/bolt(s)://、http(s)://host:9200", "error");
272
274
  return;
273
275
  }
274
276
  parsed = { dialectId: pcs.dialectId, host: pcs.host, port: pcs.port, database: pcs.database, options: pcs.options };
@@ -285,6 +287,7 @@ export default function (pi: ExtensionAPI) {
285
287
  " Oracle: jdbc:oracle:thin:@//host:port/service 或 @host:port:SID\n" +
286
288
  " Redis: redis://[:password@]host:port[/db]\n" +
287
289
  " MongoDB: mongodb://user:pass@host:27017/db 或 mongodb+srv://...\n" +
290
+ " Neo4j: neo4j://user:pass@host:7687/db 或 bolt://host:7687(路径段为图数据库名)\n" +
288
291
  " ES: http://host:9200", "error");
289
292
  return;
290
293
  }
@@ -299,6 +302,11 @@ export default function (pi: ExtensionAPI) {
299
302
  } else if (parsed.dialectId === "mongodb") {
300
303
  username = (await ctx.ui.input("账号(可空,本地无认证留空)", ""))?.trim() ?? "";
301
304
  password = (await ctx.ui.input("密码(可空)", ""))?.trim() ?? "";
305
+ } else if (parsed.dialectId === "neo4j") {
306
+ username = (await ctx.ui.input("账号", "neo4j"))?.trim() || "neo4j";
307
+ password = (await ctx.ui.input("密码", ""))?.trim() ?? "";
308
+ const dbInput = (await ctx.ui.input("图数据库名(缺省 neo4j)", parsed.database ?? "neo4j"))?.trim();
309
+ parsed.database = dbInput || parsed.database || "neo4j";
302
310
  } else {
303
311
  username = (await ctx.ui.input("账号", "root"))?.trim() || "root";
304
312
  password = (await ctx.ui.input("密码", ""))?.trim() ?? "";
@@ -742,11 +750,11 @@ export default function (pi: ExtensionAPI) {
742
750
  pi.registerTool({
743
751
  name: "query_database",
744
752
  label: "数据库查询",
745
- description: "执行 SQL 语句,支持关系型(PostgreSQL/MySQL/Oracle/达梦)/ Redis / Elasticsearch / MongoDB / Hive / Spark 九种数据库,返回执行结果。支持读和写,写操作受确认策略约束且必须附 reason 执行理由(动机+影响范围),用户确认框将展示该理由;是否允许写以及是否需确认,以系统提示中的当前数据库工具执行策略为准。DROP TABLE 始终禁止。MongoDB 的 sql 参数填 JSON 命令信封(db.runCommand 形态,如 {\"find\":\"users\",\"filter\":{}})。",
753
+ description: "执行语句,支持关系型(PostgreSQL/MySQL/Oracle/达梦)/ Redis / Elasticsearch / MongoDB / Neo4j / Hive / Spark 十种数据库,返回执行结果。支持读和写,写操作受确认策略约束且必须附 reason 执行理由(动机+影响范围),用户确认框将展示该理由;是否允许写以及是否需确认,以系统提示中的当前数据库工具执行策略为准。DROP TABLE 始终禁止。MongoDB 的 sql 参数填 JSON 命令信封(db.runCommand 形态,如 {\"find\":\"users\",\"filter\":{}});Neo4j 填 Cypher(如 MATCH (n:Person) RETURN n LIMIT 10)。",
746
754
  promptSnippet: "执行 SQL 语句。先根据系统提示中的当前数据库工具执行策略判断是否允许写操作;写操作必须在 reason 参数说明动机与影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\"),否则会被拒绝。database 参数取系统提示「可用数据库」列表中的名称(缺省走默认连接)。使用 list_tables 查看表结构后再编写 SQL。",
747
755
  parameters: Type.Object({
748
756
  database: Type.Optional(Type.String({ description: "数据库连接名称(取系统提示「可用数据库」列表中的名称;缺省走默认连接)" })),
749
- sql: Type.String({ description: "SQL 语句;MongoDB 填 JSON 命令信封,Redis 填命令,ES 填 DSL" }),
757
+ sql: Type.String({ description: "语句;关系型填 SQLMongoDB 填 JSON 命令信封,Redis 填命令,ES 填 DSL,Neo4j 填 Cypher" }),,
750
758
  reason: Type.Optional(Type.String({ description: "执行理由,写操作必填:动机+影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\")。读操作无需填写" })),
751
759
  }),
752
760
  async execute(_toolCallId: string, params: { database?: string; sql: string; reason?: string }, _signal: any, _onUpdate?: any, ctx?: any) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nsyan/db",
3
- "version": "1.1.0",
4
- "description": "AI 接入数据库扩展 —— 五大家族方言架构,支持 PostgreSQL/MySQL/Oracle/达梦/Redis/Elasticsearch/MongoDB/Hive/Spark 九种数据库,提供查询/表结构/扫描建连/连接清单工具给 LLM",
3
+ "version": "1.2.0",
4
+ "description": "AI 接入数据库扩展 —— 六大家族方言架构,支持 PostgreSQL/MySQL/Oracle/达梦/Redis/Elasticsearch/MongoDB/Neo4j/Hive/Spark 十种数据库,提供查询/表结构/扫描建连/连接清单工具给 LLM",
5
5
  "keywords": [
6
6
  "pi-extension",
7
7
  "pi-package",
@@ -15,6 +15,9 @@
15
15
  "elasticsearch",
16
16
  "mongodb",
17
17
  "mongo",
18
+ "neo4j",
19
+ "graph",
20
+ "cypher",
18
21
  "hive",
19
22
  "spark"
20
23
  ],
@@ -32,6 +35,7 @@
32
35
  "files": [
33
36
  "index.ts",
34
37
  "src/",
38
+ "docs/",
35
39
  "README.md"
36
40
  ],
37
41
  "exports": {
@@ -47,7 +51,9 @@
47
51
  "ioredis": "^6.0.0",
48
52
  "mongodb": "^6.21.0",
49
53
  "mysql2": "^3.23.1",
54
+ "neo4j-driver": "^5.28.3",
50
55
  "oracledb": "^7.0.1",
56
+ "neo4j-driver": "^5.28.3",
51
57
  "pg": "^8.22.0"
52
58
  },
53
59
  "devDependencies": {
package/src/config.ts CHANGED
@@ -276,9 +276,9 @@ export function toRuntimeConfig(c: ConnConfig, defaultPort: number): ConnConfig
276
276
 
277
277
  // 供 UI 层展示类型短标签(原 index.ts 内 5 处重复映射的收敛点之一)
278
278
  export function shortTypeLabel(type: DbTypeId): string {
279
- return ({ postgresql: "PG", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB" } as Record<string, string>)[type] ?? type;
279
+ return ({ postgresql: "PG", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB", neo4j: "Neo4j" } as Record<string, string>)[type] ?? type;
280
280
  }
281
281
 
282
282
  export function fullTypeLabel(type: DbTypeId): string {
283
- return ({ postgresql: "PostgreSQL", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB" } as Record<string, string>)[type] ?? type;
283
+ return ({ postgresql: "PostgreSQL", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB", neo4j: "Neo4j" } as Record<string, string>)[type] ?? type;
284
284
  }
@@ -42,6 +42,7 @@ const REQUIRED: Record<DbTypeId, string[]> = {
42
42
  spark: ["host", "port", "database", "username"],
43
43
  redis: ["host", "port", "password"],
44
44
  elasticsearch: ["host", "port"],
45
+ neo4j: ["host", "port", "username", "password"], // 工作库可选(缺省 neo4j;社区版默认开认证)
45
46
  mongodb: ["host", "port"], // 账号/工作库可选(本地无认证常见)
46
47
  };
47
48
 
@@ -82,6 +83,7 @@ function dialectFromImage(image: string): DbTypeId | null {
82
83
  if (/(^|\/)(mysql|mariadb)/.test(i)) return "mysql";
83
84
  if (/(^|\/)mongo/.test(i)) return "mongodb"; // mongo / mongodb 镜像(mongo-express 误报可忍变)
84
85
  if (/redis/.test(i)) return "redis";
86
+ if (/neo4j/.test(i)) return "neo4j";
85
87
  if (/elasticsearch/.test(i)) return "elasticsearch";
86
88
  if (/dm8|dameng/.test(i)) return "dm";
87
89
  if (/hive/.test(i)) return "hive";
@@ -166,7 +168,7 @@ export async function scanProject(
166
168
  if (base === ".env" || base.startsWith(".env.")) {
167
169
  const env = parseEnv(text);
168
170
  for (const [k, v] of Object.entries(env)) {
169
- if (/(^|_)(DATABASE_URL|DATASOURCE_URL|REDIS_URL|ELASTICSEARCH_URL|MONGODB_URI|MONGO_URL|DB_URL|JDBC_URL)$|_URL$/i.test(k)) {
171
+ if (/(^|_)(DATABASE_URL|DATASOURCE_URL|REDIS_URL|ELASTICSEARCH_URL|MONGODB_URI|MONGO_URL|NEO4J_URI|NEO4J_URL|BOLT_URL|DB_URL|JDBC_URL)$|_URL$/i.test(k)) {
170
172
  pushUrl(v, file, profile, weight);
171
173
  }
172
174
  }
@@ -181,8 +183,8 @@ export async function scanProject(
181
183
  const bag: FieldBag = {
182
184
  host: svc.name, // compose 网络内服务名即主机名
183
185
  port: hostPortOf(svc.ports),
184
- username: svc.env["POSTGRES_USER"] ?? svc.env["MYSQL_USER"] ?? svc.env["ES_USERNAME"] ?? svc.env["MONGO_INITDB_ROOT_USERNAME"],
185
- password: svc.env["POSTGRES_PASSWORD"] ?? svc.env["MYSQL_ROOT_PASSWORD"] ?? svc.env["MYSQL_PASSWORD"] ?? svc.env["REDIS_PASSWORD"] ?? svc.env["ELASTIC_PASSWORD"] ?? svc.env["MONGO_INITDB_ROOT_PASSWORD"],
186
+ username: svc.env["POSTGRES_USER"] ?? svc.env["MYSQL_USER"] ?? svc.env["ES_USERNAME"] ?? svc.env["MONGO_INITDB_ROOT_USERNAME"] ?? svc.env["NEO4J_AUTH"]?.split("/")[0],
187
+ password: svc.env["POSTGRES_PASSWORD"] ?? svc.env["MYSQL_ROOT_PASSWORD"] ?? svc.env["MYSQL_PASSWORD"] ?? svc.env["REDIS_PASSWORD"] ?? svc.env["ELASTIC_PASSWORD"] ?? svc.env["MONGO_INITDB_ROOT_PASSWORD"] ?? svc.env["NEO4J_PASSWORD"],
186
188
  database: svc.env["POSTGRES_DB"] ?? svc.env["MYSQL_DATABASE"] ?? svc.env["MONGO_INITDB_DATABASE"],
187
189
  };
188
190
  raws.push({ dialectId, bag, file, profile, confidence: weight });
@@ -138,7 +138,7 @@ export function parseCompose(text: string): ComposeService[] {
138
138
 
139
139
  // ── 通用 URL 正则(全文件扫描兜底)──────────────────
140
140
 
141
- const URL_RE = /(?:jdbc:(?:postgresql|mysql|oracle|dm|hive2)|rediss?|mongodb\+srv|mongodb|postgresql|mysql):\/\/[^\s"'<>`]+|https?:\/\/[^\s"'<>`]*:9200[^\s"'<>`]*/g;
141
+ const URL_RE = /(?:jdbc:(?:postgresql|mysql|oracle|dm|hive2)|rediss?|mongodb\+srv|mongodb|neo4j\+s(sc)?|neo4j|bolt\+s(sc)?|bolt|postgresql|mysql):\/\/[^\s"'<>`]+|https?:\/\/[^\s"'<>`]*:9200[^\s"'<>`]*/g;
142
142
 
143
143
  export function extractUrls(text: string): string[] {
144
144
  const out = new Set<string>();
@@ -1,7 +1,7 @@
1
1
  // scan/spring.ts —— Spring 专属:datasource / data.redis / elasticsearch / data.mongodb 键映射 + profile 分组
2
2
  import type { DbTypeId } from "../types.js";
3
3
 
4
- export type SpringGroup = "datasource" | "redis" | "es" | "mongo";
4
+ export type SpringGroup = "datasource" | "redis" | "es" | "mongo" | "neo4j";
5
5
 
6
6
  export interface RawDbConfig {
7
7
  group: SpringGroup;
@@ -47,12 +47,19 @@ const SPRING_KEYS: Array<[string, Field]> = [
47
47
  ["spring.data.elasticsearch.password", "password"],
48
48
  ["spring.data.mongodb.uri", "url"], // Spring Boot 2.x+(含 3.x)
49
49
  ["spring.mongodb.uri", "url"], // Spring Boot 1.x 旧前缀
50
+ ["spring.neo4j.uri", "url"], // Spring Boot 3.x
51
+ ["spring.data.neo4j.uri", "url"], // Spring Boot 2.x 旧前缀
52
+ ["spring.neo4j.authentication.username", "username"],
53
+ ["spring.neo4j.authentication.password", "password"],
54
+ ["spring.data.neo4j.username", "username"],
55
+ ["spring.data.neo4j.password", "password"],
50
56
  ];
51
57
 
52
58
  function groupOf(key: string): SpringGroup {
53
59
  if (key.includes("redis")) return "redis";
54
60
  if (key.includes("elasticsearch")) return "es";
55
61
  if (key.includes("mongodb")) return "mongo"; // 独立分组:避免与 datasource 的 url 字段互相覆盖
62
+ if (key.includes("neo4j")) return "neo4j";
56
63
  return "datasource";
57
64
  }
58
65
 
@@ -67,7 +74,23 @@ export function springKeysToRaw(dotted: Record<string, string>, file: string, we
67
74
  (raw as Record<string, unknown>)[field] = v;
68
75
  groups.set(g, raw);
69
76
  }
70
- return [...groups.values()];
77
+ // baomidou dynamic-datasource(多数据源):spring.datasource.dynamic.datasource.<name>.<field>
78
+ // 每个 <name> 独立成候选(与 Spec 单组 datasource 键互不覆盖)
79
+ const dynamic = new Map<string, RawDbConfig>();
80
+ const DYNAMIC_PREFIX = "spring.datasource.dynamic.datasource.";
81
+ for (const [key, value] of Object.entries(dotted)) {
82
+ if (!key.startsWith(DYNAMIC_PREFIX) || value === "") continue;
83
+ const rest = key.slice(DYNAMIC_PREFIX.length); // "<name>.<field>"
84
+ const dot = rest.indexOf(".");
85
+ if (dot <= 0) continue;
86
+ const name = rest.slice(0, dot);
87
+ const field = rest.slice(dot + 1);
88
+ if (field !== "url" && field !== "jdbc-url" && field !== "username" && field !== "password") continue;
89
+ const raw = dynamic.get(name) ?? { group: "datasource" as const, profile: profileOf(basename(file)), file, weight };
90
+ (raw as Record<string, unknown>)[field === "jdbc-url" ? "url" : field] = value;
91
+ dynamic.set(name, raw);
92
+ }
93
+ return [...groups.values(), ...dynamic.values()];
71
94
  }
72
95
 
73
96
  function basename(p: string): string {