@nsyan/db 1.2.1 → 1.3.1
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 +3 -3
- package/docs/USAGE.md +36 -9
- package/index.ts +164 -130
- package/package.json +7 -4
- package/src/config.ts +18 -3
- package/src/core/index.ts +1 -1
- package/src/core/scan/tree.ts +34 -0
- package/src/core/scan/validate.ts +119 -0
- package/src/core/types.ts +19 -5
- package/src/dialects/dm.ts +5 -2
- package/src/dialects/elasticsearch.ts +16 -6
- package/src/dialects/hive.ts +3 -1
- package/src/dialects/mysql.ts +4 -2
- package/src/dialects/neo4j.ts +2 -2
- package/src/dialects/oracle.ts +4 -2
- package/src/dialects/redis.ts +8 -3
- package/src/dialects/spark.ts +2 -1
- package/src/core/scan/candidates.ts +0 -294
- package/src/core/scan/parsers.ts +0 -149
- package/src/core/scan/placeholders.ts +0 -24
- package/src/core/scan/scoring.ts +0 -25
- package/src/core/scan/spring.ts +0 -99
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- **10 种数据库 · 6 家族**:PostgreSQL · MySQL · Oracle · 达梦 · Redis · Elasticsearch · MongoDB · Neo4j · Hive · Spark
|
|
17
17
|
- **AI 工具**:查询 / 表结构 / 连接清单 / 扫描建连,读多写少场景的 token 友好输出
|
|
18
|
-
-
|
|
18
|
+
- **扫描建连**:AI 驱动——工具返回项目文件树,会话 AI 自行定位并阅读配置文件(任意语言/格式),提取候选后弹框确认建连;防幻觉校验(url 必须能被方言解析)
|
|
19
19
|
- **安全模型**:只读模式、写确认 + 执行理由、连接级强制只读、家族白名单、管理命令恒拒
|
|
20
20
|
- **审计**(可选):写操作按天落盘,含理由与完整 SQL
|
|
21
21
|
- **零构建**:TypeScript 由 pi/tsx 直接加载,core/dialects 零 pi 运行时依赖
|
|
@@ -31,7 +31,7 @@ pi install ./packages/db # 本仓库开发者
|
|
|
31
31
|
|
|
32
32
|
1. `/db add` 粘贴连接串一键建连(支持 `postgresql://` · `jdbc:*` · `redis://` · `mongodb+srv://` · `bolt://`/`neo4j://` · `http://host:9200`)
|
|
33
33
|
2. 直接让 AI 查询:*「查一下订单表最近 10 条」*
|
|
34
|
-
3.
|
|
34
|
+
3. 扫描建连:在会话说「连一下这个项目的数据库」或「/db scan 配置pg」——AI 读取项目配置文件(任意语言)提取连接,会话内弹框确认后建连;被拒候选按原因修正重提
|
|
35
35
|
|
|
36
36
|
## 🗄️ 支持的数据库
|
|
37
37
|
|
|
@@ -73,7 +73,7 @@ pi install ./packages/db # 本仓库开发者
|
|
|
73
73
|
| 连接级强制只读 | 标记的连接(如生产库)无视全局开关,永远只读 |
|
|
74
74
|
| 家族白名单 | Redis/ES/Mongo 只读白名单;`DROP`、管理 DDL、服务端 JS、`CALL dbms.*` 恒拒 |
|
|
75
75
|
| 审计日志 | 默认关;`/db config` 开启后写操作按天落盘(完整 SQL,0600) |
|
|
76
|
-
| 凭据 | 配置文件 `0600
|
|
76
|
+
| 凭据 | 配置文件 `0600`;扫描场景密码经确认框补录(v1.3 起随 AI 阅读进会话,用户知情接受) |
|
|
77
77
|
|
|
78
78
|
## ⚙️ 设置
|
|
79
79
|
|
package/docs/USAGE.md
CHANGED
|
@@ -46,18 +46,46 @@
|
|
|
46
46
|
- `强制只读` 的连接**无视全局只读开关**,永远只接受查询;AI 侧写入会被拒并提示原因
|
|
47
47
|
- 典型用法:全局允许写(开发库随便改),生产库连接强制只读
|
|
48
48
|
|
|
49
|
-
## 🔎
|
|
49
|
+
## 🔎 扫描建连(会话 AI 驱动,v1.3.0 重构)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
扫描建连已从"正则匹配"重构为"**会话 AI 提取**":插件不再用文件名模式/键名正则猜测配置(v1.2 及以前的 Spring 键映射、baomidou dynamic-datasource 解析、占位符 `.env` 回退等提取代码已删除),改为把项目文件树交给会话模型,由 AI 自行定位并阅读配置文件——**语言无关**(Java/Python/Go/TS/Rust 的 yml/toml/env/ini/json/硬编码均可识别),`${POSTGRES-IP:10.2.12.50}` 这类占位符、Nacos 导出、K8s manifest 等长尾格式天然理解。
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
**用法**(在会话对话框直接说):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
连一下这个项目的数据库 # 全类型扫描
|
|
57
|
+
/db scan 配置pg # 只提取 PostgreSQL(类型词中英文均可)
|
|
58
|
+
/db scan 只看 redis 和 neo4j # 多类型
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
**流程**:
|
|
62
|
+
|
|
63
|
+
1. AI 调用 `scan_project_configs` 获取项目文件树(path 强制限定当前工作目录子树内,越界拒绝)
|
|
64
|
+
2. AI 从树中挑出可能含连接配置的文件,用自带读文件工具阅读,提取候选(类型/host/端口/库名/账号/密码/连接串)
|
|
65
|
+
3. AI 调用 `db_scan_save` 提交候选——工具逐个弹确认框:确认 → 补录缺字段 → **测试连接通过才写盘**;同名连接三选一(覆盖/改名/跳过)
|
|
66
|
+
4. 被拒绝的候选(url 与类型矛盾等)AI 会按返回原因修正后重提
|
|
67
|
+
|
|
68
|
+
**防幻觉校验**(确定性规则,AI 不可绕过):dialectId 必须在支持列表内;带 `url` 的候选必须能被对应方言 `parseUrl` 解析(解析成功后以 URL 为准);host 必填、port 范围 1~65535。
|
|
69
|
+
|
|
70
|
+
**结果状态**:✅ 可直接建 / ✏️ 待补字段(确认框中补录)/ ⏭️ 同名已存在。
|
|
71
|
+
|
|
72
|
+
### 配置中心场景(Nacos / Apollo / Spring Cloud Config)
|
|
73
|
+
|
|
74
|
+
当连接配置不在本地文件、而在配置中心时(特征:`bootstrap.yml` 含 `spring.cloud.nacos.config` 或类似的配置中心指向),AI 会自动走"两跳"提取:
|
|
75
|
+
|
|
76
|
+
1. 从 `bootstrap.yml` 读取配置中心地址与凭据(server-addr / username / password / namespace / group)
|
|
77
|
+
2. 用会话自带的命令工具调 Open API 拉取配置原文——Nacos 为例:
|
|
78
|
+
```bash
|
|
79
|
+
# 登录拿 accessToken
|
|
80
|
+
curl -X POST "http://<server>/nacos/v1/auth/login" -d "username=<u>&password=<p>"
|
|
81
|
+
# 拉取配置(tenant = namespace 的 UUID,group/file-extension 与 bootstrap 对应)
|
|
82
|
+
curl "http://<server>/nacos/v1/cs/configs?dataId=<服务名>.yaml&group=dev&tenant=<namespace>&accessToken=<token>"
|
|
83
|
+
```
|
|
84
|
+
3. 从返回的 YAML 原文提取连接候选 → `db_scan_save` 提交(校验/确认/测试连接照常)
|
|
85
|
+
|
|
86
|
+
本地 profile 文件(如 `application-dev.yml`)与配置中心共有的项目,两边都会被提取,重复的连接靠同名查重(⏭️ exists 状态)与确认框兜底。
|
|
87
|
+
|
|
88
|
+
**隐私行为变化**(v1.3.0,用户知情接受):配置文件原文(含密码)会随 AI 阅读进入会话上下文;v1.2 及以前"密码只在终端补录、不进模型上下文"的承诺不再适用于扫描场景(查询/连接管理不受影响)。
|
|
61
89
|
|
|
62
90
|
## 🧾 各家族 sql 形态详解
|
|
63
91
|
|
|
@@ -122,7 +150,6 @@ SHOW INDEXES
|
|
|
122
150
|
- **结果拍平**:Node 渲染为 `:Label {属性}`,Relationship 为 `-(TYPE)-> {属性}`,Path 为 `<path:n>`;无返回记录的写语句回显变更计数(创建节点 n,设置属性 m)
|
|
123
151
|
- **limit 封顶**:客户端截断对齐关系型方言(不做 Cypher LIMIT 注入,任意语句尾部加 LIMIT 不总合法)
|
|
124
152
|
- **连接串**:`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
153
|
|
|
127
154
|
## ✍️ 写操作:理由与审计
|
|
128
155
|
|
package/index.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { registry } from "./src/dialects/index.js";
|
|
5
5
|
import { decide, effectiveReadonly, writeRequiresReason } from "./src/core/policy.js";
|
|
6
6
|
import { appendAuditLog } from "./src/core/audit.js";
|
|
7
|
-
import {
|
|
7
|
+
import { collectTree } from "./src/core/scan/tree.js";
|
|
8
|
+
import { validateCandidates } from "./src/core/scan/validate.js";
|
|
8
9
|
import type { Candidate, ConnConfig, DbTypeId } from "./src/core/types.js";
|
|
9
10
|
import {
|
|
10
11
|
loadConfigs, saveConfigs, loadPluginConfig, savePluginConfig, getConfigSummary,
|
|
@@ -49,13 +50,21 @@ function buildDisplayList(configs: ConnConfig[]): { list: string[]; map: Map<str
|
|
|
49
50
|
// ── 构建「可用数据库」提示(注入系统提示,让 AI 知道有哪些连接) ────
|
|
50
51
|
// 效率项(Spec §6):每连接只注一行 `名称[家族] + 一行语义`,细节压进系统提示
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
+
// 各家族一行语义:让 AI 一眼知道该类型的 sql 参数填什么(覆盖全部十种类型)
|
|
53
54
|
function familyHint(c: ConnConfig): string {
|
|
54
55
|
switch (c.type) {
|
|
55
56
|
case "postgresql":
|
|
56
57
|
case "mysql":
|
|
57
58
|
case "oracle":
|
|
59
|
+
case "dm":
|
|
58
60
|
return "关系型,sql 参数填 SQL";
|
|
61
|
+
case "redis":
|
|
62
|
+
return "Redis 键值库,sql 参数填单条 Redis 命令(如 GET key / SCAN 0 MATCH user:* COUNT 100;KEYS 已禁用,扫描请用 SCAN)";
|
|
63
|
+
case "elasticsearch":
|
|
64
|
+
return "Elasticsearch 搜索库,sql 参数填 DSL JSON(如 {\"query\":{\"match_all\":{}}};仅支持读端点 _search/_count/_mget)";
|
|
65
|
+
case "hive":
|
|
66
|
+
case "spark":
|
|
67
|
+
return "大数据 SQL 引擎,sql 参数填 SQL(如 SELECT * FROM 库.表 LIMIT 10)";
|
|
59
68
|
case "mongodb":
|
|
60
69
|
return "MongoDB 文档库,sql 参数填 JSON 命令信封(如 {\"find\":\"users\",\"filter\":{}};读命令 find/count/distinct/aggregate)";
|
|
61
70
|
case "neo4j":
|
|
@@ -68,8 +77,12 @@ function familyHint(c: ConnConfig): string {
|
|
|
68
77
|
// 系统提示触发词文案(Spec §8.5:AI 侧入口;建连写盘前必须经用户确认)
|
|
69
78
|
const SCAN_TRIGGER_HINT = [
|
|
70
79
|
"[项目扫描建连]",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
80
|
+
"当用户说“连一下这个项目的数据库”“帮我把这项目的库配上”“/db scan”等时,走两步 AI 扫描流程:",
|
|
81
|
+
"第 1 步:调用 scan_project_configs 获取项目文件树(path 必须在当前工作目录子树内,越界拒绝);",
|
|
82
|
+
"第 2 步:从树中自行判断哪些文件可能包含数据库连接配置(任意语言:Spring/Py/Go/TS/Rust 的 yml/toml/env/ini/json 等都算),用自带读文件工具阅读它们,",
|
|
83
|
+
" 从中提取连接候选(类型/host/端口/库名/账号/密码),用户说“只要 pg”这类话时只提取对应类型;",
|
|
84
|
+
"第 3 步:调用 db_scan_save 提交候选——工具会弹确认框,用户逐个确认并补录密码后才写盘,绝不静默建连。",
|
|
85
|
+
"注意:配置文件中的密码会随文件内容进入会话上下文(用户已接受);候选提交后由工具校验(url 必须能被方言解析,否则被拒),被拒时根据原因修正后重提。",
|
|
73
86
|
].join("\n");
|
|
74
87
|
|
|
75
88
|
function buildDbListHint(configs: ConnConfig[], cfg: PluginConfig): string {
|
|
@@ -113,22 +126,15 @@ function buildDbListHint(configs: ConnConfig[], cfg: PluginConfig): string {
|
|
|
113
126
|
].join("\n");
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
// scan
|
|
129
|
+
// scan 候选状态图标(✅ 可直接建 / ✏️ 待补字段 / ⏭️ 同名已存在)
|
|
117
130
|
const SCAN_GLYPH: Record<Candidate["status"], string> = {
|
|
118
|
-
ready: "✅", incomplete: "✏️",
|
|
131
|
+
ready: "✅", incomplete: "✏️", exists: "⏭️",
|
|
119
132
|
};
|
|
120
133
|
|
|
121
|
-
// 密码掩码(输出层红线):真实密码不进模型上下文;jasypt ENC( 密文保留(非明文,可用信号)
|
|
122
|
-
function maskCandidate(c: Candidate): Candidate {
|
|
123
|
-
const p = { ...c.partial };
|
|
124
|
-
if (typeof p.password === "string" && p.password && !p.password.startsWith("ENC(")) p.password = "***";
|
|
125
|
-
return { ...c, partial: p };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
134
|
function candidateLine(c: Candidate): string {
|
|
129
135
|
const p = c.partial;
|
|
130
136
|
const miss = c.missing.length ? `,缺: ${c.missing.join("/")}` : "";
|
|
131
|
-
return `${SCAN_GLYPH[c.status]} [${c.status}] ${p.name} (${c.dialectId}) ${p.host ?? ""}:${p.port ?? ""}${p.database ? "/" + p.database : ""}${miss} <- ${c.source
|
|
137
|
+
return `${SCAN_GLYPH[c.status]} [${c.status}] ${p.name} (${c.dialectId}) ${p.host ?? ""}:${p.port ?? ""}${p.database ? "/" + p.database : ""}${miss} <- ${c.source}`;
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
// ── 导出扩展 ──────────────────────────────────────
|
|
@@ -136,88 +142,63 @@ function candidateLine(c: Candidate): string {
|
|
|
136
142
|
// scan 向导核心(由 registerCommand 内的 scanWizard 调用;需要 ctx.ui)
|
|
137
143
|
|
|
138
144
|
export default function (pi: ExtensionAPI) {
|
|
139
|
-
// ──
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (candidates.length === 0) {
|
|
151
|
-
ctx.ui.notify(`在 ${scanPath} 未扫出数据库连接候选`, "info");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 1. 分组展示
|
|
156
|
-
ctx.ui.notify(
|
|
157
|
-
`扫描到 ${candidates.length} 个连接候选:\n${candidates.map(candidateLine).join("\n")}\n\n接下来逐个确认,不会静默建连。`,
|
|
158
|
-
"info",
|
|
145
|
+
// ── AI 候选落地(v1.3.0:scan_project_configs 返回文件树 → 会话模型提取 → db_scan_save 提交)───
|
|
146
|
+
// 硬性原则不变:绝不静默建连(每个候选写盘前 confirm);绝不静默覆盖(同名三选一);
|
|
147
|
+
// 缺字段在确认框交互中补录;testConnection 通过才保存。
|
|
148
|
+
const confirmAndSaveCandidate = async (ctx: any, cand: Candidate): Promise<"saved" | "skipped" | "failed"> => {
|
|
149
|
+
const p = cand.partial;
|
|
150
|
+
let name = p.name!;
|
|
151
|
+
const ok = await ctx.ui.confirm(
|
|
152
|
+
"AI 扫描建连",
|
|
153
|
+
`添加 ${name} (${cand.dialectId})?\n${p.host ?? ""}:${p.port ?? ""}${p.database ? "/" + p.database : ""}\n来源: ${cand.source}`,
|
|
159
154
|
);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let name = cand.partial.name!;
|
|
174
|
-
if (loadConfigs().some((x) => x.name === name)) {
|
|
175
|
-
const how = await ctx.ui.select(`同名配置已存在: ${name}`, ["覆盖", "改名", "跳过"]);
|
|
176
|
-
if (!how || how === "跳过") continue;
|
|
177
|
-
if (how === "改名") {
|
|
178
|
-
name = (await ctx.ui.input("新名称", name))?.trim() || name;
|
|
179
|
-
// 改名后仍需查重:新名若也撞车,退回三选一(绝不静默覆盖)
|
|
180
|
-
while (loadConfigs().some((x) => x.name === name)) {
|
|
181
|
-
const again = await ctx.ui.select(`新名称 ${name} 也已存在`, ["覆盖", "再改一次", "跳过"]);
|
|
182
|
-
if (!again || again === "跳过") { name = ""; break; }
|
|
183
|
-
if (again === "再改一次") {
|
|
184
|
-
name = (await ctx.ui.input("新名称", name))?.trim() || name;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
break; // 覆盖
|
|
155
|
+
if (!ok) return "skipped";
|
|
156
|
+
|
|
157
|
+
if (loadConfigs().some((x) => x.name === name)) {
|
|
158
|
+
const how = await ctx.ui.select(`同名配置已存在: ${name}`, ["覆盖", "改名", "跳过"]);
|
|
159
|
+
if (!how || how === "跳过") return "skipped";
|
|
160
|
+
if (how === "改名") {
|
|
161
|
+
name = (await ctx.ui.input("新名称", name))?.trim() || name;
|
|
162
|
+
while (loadConfigs().some((x) => x.name === name)) {
|
|
163
|
+
const again = await ctx.ui.select(`新名称 ${name} 也已存在`, ["覆盖", "再改一次", "跳过"]);
|
|
164
|
+
if (!again || again === "跳过") { name = ""; break; }
|
|
165
|
+
if (again === "再改一次") {
|
|
166
|
+
name = (await ctx.ui.input("新名称", name))?.trim() || name;
|
|
167
|
+
continue;
|
|
188
168
|
}
|
|
189
|
-
|
|
169
|
+
break; // 覆盖
|
|
190
170
|
}
|
|
171
|
+
if (name === "") return "skipped";
|
|
191
172
|
}
|
|
173
|
+
}
|
|
192
174
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
175
|
+
// 缺字段追问(TUI 通道补录)
|
|
176
|
+
const bag: Partial<ConnConfig> = { ...p, name };
|
|
177
|
+
for (const f of cand.missing) {
|
|
178
|
+
const v = (await ctx.ui.input(`补充 ${f}(${name})`, ""))?.trim();
|
|
179
|
+
if (v !== undefined && v !== "") {
|
|
180
|
+
(bag as Record<string, unknown>)[f] = f === "port" || f === "dbIndex" ? parseInt(v, 10) : v;
|
|
200
181
|
}
|
|
182
|
+
}
|
|
201
183
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
184
|
+
const dialect = registry.get(cand.dialectId);
|
|
185
|
+
if (!dialect) return "failed";
|
|
186
|
+
const conn: ConnConfig = { id: randomUUID(), createdAt: new Date().toISOString(), type: cand.dialectId, ...bag } as ConnConfig;
|
|
205
187
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
ctx.ui.notify(`连接成功 (${result.version}${result.warning ? ",警告: " + result.warning : ""}, ${result.latency})`, "success");
|
|
214
|
-
const all = loadConfigs();
|
|
215
|
-
const idx = all.findIndex((x) => x.name === name);
|
|
216
|
-
if (idx >= 0) all[idx] = conn; else all.push(conn);
|
|
217
|
-
saveConfigs(all);
|
|
218
|
-
recordTestResult(name, result);
|
|
219
|
-
ctx.ui.notify(`配置已保存: ${name}`, "success");
|
|
188
|
+
ctx.ui.notify(`正在测试 ${dialect.label} 连接...`, "info");
|
|
189
|
+
const result = await dialect.testConnection(toRuntimeConfig(conn, dialect.defaultPort));
|
|
190
|
+
if (!result.success) {
|
|
191
|
+
ctx.ui.notify(`连接失败: ${result.error}\n未保存。请检查网络/账号后重试,或用 /db add 手动添加。`, "error");
|
|
192
|
+
return "failed";
|
|
220
193
|
}
|
|
194
|
+
ctx.ui.notify(`连接成功 (${result.version ?? ""}${result.warning ? ",警告: " + result.warning : ""}, ${result.latency})`, "success");
|
|
195
|
+
const all = loadConfigs();
|
|
196
|
+
const idx = all.findIndex((x) => x.name === name);
|
|
197
|
+
if (idx >= 0) all[idx] = conn; else all.push(conn);
|
|
198
|
+
saveConfigs(all);
|
|
199
|
+
recordTestResult(name, result);
|
|
200
|
+
ctx.ui.notify(`配置已保存: ${name}`, "success");
|
|
201
|
+
return "saved";
|
|
221
202
|
};
|
|
222
203
|
|
|
223
204
|
// ── 添加数据库连接(P0:一键连接串 / 家族分支逐步表单,Spec §7/§11.1)───
|
|
@@ -682,7 +663,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
682
663
|
const val = await ctx.ui.select(`选择 ${field.label}`, field.options);
|
|
683
664
|
if (!val) continue;
|
|
684
665
|
if (field.valueMap) {
|
|
685
|
-
(newCfg as any)[field.key] = field.valueMap[val];
|
|
666
|
+
(newCfg as any)[field.key] = (field.valueMap as Record<string, unknown>)[val];
|
|
686
667
|
} else {
|
|
687
668
|
(newCfg as any)[field.key] = val === "是";
|
|
688
669
|
}
|
|
@@ -726,7 +707,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
726
707
|
};
|
|
727
708
|
});
|
|
728
709
|
|
|
729
|
-
// ── 注册
|
|
710
|
+
// ── 注册 6 个工具(给 LLM 调用) ──────────────────
|
|
711
|
+
// 注:pi 的 AgentToolResult.details 为必填字段(registerTool 的 TDetails 默认 unknown)。
|
|
712
|
+
// 本插件工具没有结构化详情,故在返回对象里统一显式写 details: undefined——与"完全不提供该字段"
|
|
713
|
+
// 在运行时等价:宿主读取走可选链(result.details?.x)、合并判定用 `!== undefined`,
|
|
714
|
+
// 且 JSON 序列化会跳过 undefined 值。
|
|
730
715
|
|
|
731
716
|
// 解析工具 database 参数:缺省走默认连接(Spec §11.1);无默认则报错指引
|
|
732
717
|
function resolveTargetDb(name: string | undefined): { config?: ConnConfig; error?: string } {
|
|
@@ -754,7 +739,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
739
|
promptSnippet: "执行 SQL 语句。先根据系统提示中的当前数据库工具执行策略判断是否允许写操作;写操作必须在 reason 参数说明动机与影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\"),否则会被拒绝。database 参数取系统提示「可用数据库」列表中的名称(缺省走默认连接)。使用 list_tables 查看表结构后再编写 SQL。",
|
|
755
740
|
parameters: Type.Object({
|
|
756
741
|
database: Type.Optional(Type.String({ description: "数据库连接名称(取系统提示「可用数据库」列表中的名称;缺省走默认连接)" })),
|
|
757
|
-
sql: Type.String({ description: "语句;关系型填 SQL,MongoDB 填 JSON 命令信封,Redis 填命令,ES 填 DSL,Neo4j 填 Cypher" })
|
|
742
|
+
sql: Type.String({ description: "语句;关系型填 SQL,MongoDB 填 JSON 命令信封,Redis 填命令,ES 填 DSL,Neo4j 填 Cypher" }),
|
|
758
743
|
reason: Type.Optional(Type.String({ description: "执行理由,写操作必填:动机+影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\")。读操作无需填写" })),
|
|
759
744
|
}),
|
|
760
745
|
async execute(_toolCallId: string, params: { database?: string; sql: string; reason?: string }, _signal: any, _onUpdate?: any, ctx?: any) {
|
|
@@ -762,7 +747,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
762
747
|
|
|
763
748
|
const target = resolveTargetDb(params.database);
|
|
764
749
|
if (target.error || !target.config) {
|
|
765
|
-
return {
|
|
750
|
+
return { details: undefined,
|
|
766
751
|
content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
|
|
767
752
|
};
|
|
768
753
|
}
|
|
@@ -775,7 +760,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
775
760
|
// verdict = dialect.isAllowed(sql, readonly) → decide → deny/confirm/run
|
|
776
761
|
const dialect = registry.get(config.type);
|
|
777
762
|
if (!dialect) {
|
|
778
|
-
return {
|
|
763
|
+
return { details: undefined,
|
|
779
764
|
content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
|
|
780
765
|
};
|
|
781
766
|
}
|
|
@@ -785,19 +770,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
785
770
|
const note = !cfg.ai_readonly && config.forceReadonly === true
|
|
786
771
|
? `(连接 ${config.name} 已设置强制只读)`
|
|
787
772
|
: "";
|
|
788
|
-
return {
|
|
773
|
+
return { details: undefined,
|
|
789
774
|
content: [{ type: "text" as const, text: (verdict.reason ?? "该操作不被允许。如需修改,请执行 /db config 更改配置。") + note }],
|
|
790
775
|
};
|
|
791
776
|
}
|
|
792
777
|
// 写操作强制附执行理由(v1.1 UX 共识 Q1/Q3:与确认策略解耦;缺 reason 拒绝并引导 AI 补充)
|
|
793
778
|
if (writeRequiresReason(verdict, params.reason)) {
|
|
794
|
-
return {
|
|
779
|
+
return { details: undefined,
|
|
795
780
|
content: [{ type: "text" as const, text: "写操作必须附执行理由:请在 reason 参数中说明动机与影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\"),补充后重试。" }],
|
|
796
781
|
};
|
|
797
782
|
}
|
|
798
783
|
if (action === "confirm") {
|
|
799
784
|
if (!ctx?.hasUI) {
|
|
800
|
-
return {
|
|
785
|
+
return { details: undefined,
|
|
801
786
|
content: [{ type: "text" as const, text: "当前环境无法弹出确认对话框,已取消 SQL 执行。请在有界面的环境中操作。" }],
|
|
802
787
|
};
|
|
803
788
|
}
|
|
@@ -806,7 +791,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
806
791
|
`理由: ${params.reason}\n\n${verdict.summary ?? ""}\n\n数据库: ${config.name}\n\nSQL:\n${params.sql}`,
|
|
807
792
|
);
|
|
808
793
|
if (!ok) {
|
|
809
|
-
return {
|
|
794
|
+
return { details: undefined,
|
|
810
795
|
content: [{ type: "text" as const, text: "用户取消了 SQL 执行。" }],
|
|
811
796
|
};
|
|
812
797
|
}
|
|
@@ -820,7 +805,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
820
805
|
);
|
|
821
806
|
|
|
822
807
|
if (!result.success) {
|
|
823
|
-
return {
|
|
808
|
+
return { details: undefined,
|
|
824
809
|
content: [{ type: "text" as const, text: `查询失败: ${result.error}` }],
|
|
825
810
|
};
|
|
826
811
|
}
|
|
@@ -868,7 +853,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
868
853
|
text += `\n执行理由: ${params.reason}`;
|
|
869
854
|
}
|
|
870
855
|
|
|
871
|
-
return { content: [{ type: "text" as const, text }] };
|
|
856
|
+
return { details: undefined, content: [{ type: "text" as const, text }] };
|
|
872
857
|
},
|
|
873
858
|
});
|
|
874
859
|
|
|
@@ -885,7 +870,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
885
870
|
async execute(_toolCallId: string, params: { database?: string; pattern?: string }, _signal: any) {
|
|
886
871
|
const target = resolveTargetDb(params.database);
|
|
887
872
|
if (target.error || !target.config) {
|
|
888
|
-
return {
|
|
873
|
+
return { details: undefined,
|
|
889
874
|
content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
|
|
890
875
|
};
|
|
891
876
|
}
|
|
@@ -893,14 +878,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
893
878
|
|
|
894
879
|
const dialect = registry.get(config.type);
|
|
895
880
|
if (!dialect) {
|
|
896
|
-
return {
|
|
881
|
+
return { details: undefined,
|
|
897
882
|
content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
|
|
898
883
|
};
|
|
899
884
|
}
|
|
900
885
|
const result = await dialect.listTables(toRuntimeConfig(config, dialect.defaultPort), params.pattern);
|
|
901
886
|
|
|
902
887
|
if (!result.success || !result.tables) {
|
|
903
|
-
return {
|
|
888
|
+
return { details: undefined,
|
|
904
889
|
content: [{ type: "text" as const, text: `获取表列表失败: ${result.error}` }],
|
|
905
890
|
};
|
|
906
891
|
}
|
|
@@ -912,7 +897,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
912
897
|
return `${schema}${t.name} (${t.type})${desc}`;
|
|
913
898
|
});
|
|
914
899
|
|
|
915
|
-
return {
|
|
900
|
+
return { details: undefined,
|
|
916
901
|
content: [{ type: "text" as const, text: `数据库 "${config.name}"${params.pattern ? `(pattern: ${params.pattern})` : ""} 共 ${result.count} 张表:\n${lines.join("\n")}` }],
|
|
917
902
|
};
|
|
918
903
|
},
|
|
@@ -931,7 +916,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
931
916
|
async execute(_toolCallId: string, params: { database?: string; table: string }, _signal: any) {
|
|
932
917
|
const target = resolveTargetDb(params.database);
|
|
933
918
|
if (target.error || !target.config) {
|
|
934
|
-
return {
|
|
919
|
+
return { details: undefined,
|
|
935
920
|
content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
|
|
936
921
|
};
|
|
937
922
|
}
|
|
@@ -939,14 +924,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
939
924
|
|
|
940
925
|
const dialect = registry.get(config.type);
|
|
941
926
|
if (!dialect) {
|
|
942
|
-
return {
|
|
927
|
+
return { details: undefined,
|
|
943
928
|
content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
|
|
944
929
|
};
|
|
945
930
|
}
|
|
946
931
|
const result = await dialect.describeTable(toRuntimeConfig(config, dialect.defaultPort), params.table);
|
|
947
932
|
|
|
948
933
|
if (!result.success || !result.columns) {
|
|
949
|
-
return {
|
|
934
|
+
return { details: undefined,
|
|
950
935
|
content: [{ type: "text" as const, text: `获取表结构失败: ${result.error}` }],
|
|
951
936
|
};
|
|
952
937
|
}
|
|
@@ -962,44 +947,94 @@ export default function (pi: ExtensionAPI) {
|
|
|
962
947
|
);
|
|
963
948
|
}
|
|
964
949
|
|
|
965
|
-
return {
|
|
950
|
+
return { details: undefined,
|
|
966
951
|
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
967
952
|
};
|
|
968
953
|
},
|
|
969
954
|
});
|
|
970
955
|
|
|
971
|
-
// 工具 4: scan_project_configs(
|
|
956
|
+
// 工具 4: scan_project_configs(v1.3.0:返回项目文件树,提取交给会话模型)
|
|
972
957
|
pi.registerTool({
|
|
973
958
|
name: "scan_project_configs",
|
|
974
959
|
label: "扫描项目数据库配置",
|
|
975
|
-
description: "
|
|
976
|
-
promptSnippet: "
|
|
960
|
+
description: "获取项目文件树,供 AI 自行定位并阅读数据库连接配置文件(任意语言:Spring/Py/Go/TS/Rust 的 yml/toml/env/ini/json 等均覆盖)。本身不做提取;提取结果用 db_scan_save 提交。",
|
|
961
|
+
promptSnippet: "当用户说「连一下这个项目的数据库」「帮我把这项目的库配上」「/db scan 配置pg」等时调用第 1 步。返回项目文件树后:自己判断哪些文件可能含数据库连接配置(用户指了类型就只找该类型),用自带读文件工具阅读,提取连接候选(类型/host/端口/库名/账号/密码),然后调用 db_scan_save 提交。path 必须在当前工作目录子树内。",
|
|
977
962
|
parameters: Type.Object({
|
|
978
963
|
path: Type.Optional(Type.String({ description: "扫描根目录,缺省为当前工作目录;强制限定在当前工作目录子树内,越界拒绝" })),
|
|
979
964
|
}),
|
|
980
965
|
async execute(_toolCallId: string, params: { path?: string }, _signal: any) {
|
|
981
966
|
try {
|
|
982
|
-
const
|
|
983
|
-
if (candidates.length === 0) {
|
|
984
|
-
return {
|
|
985
|
-
content: [{ type: "text" as const, text: `在 ${params.path ?? "当前目录"} 未扫出数据库连接候选。可建议用户用 /db add 手动添加。` }],
|
|
986
|
-
};
|
|
987
|
-
}
|
|
967
|
+
const tree = collectTree(params.path ?? ".");
|
|
988
968
|
const text = [
|
|
989
|
-
|
|
990
|
-
|
|
969
|
+
`项目根目录: ${tree.root}(文件总数 ${tree.total}${tree.truncated ? ",已截断展示前 " + tree.lines.length + " 条" : ""})`,
|
|
970
|
+
"",
|
|
971
|
+
"文件树(相对路径,浅层优先):",
|
|
972
|
+
...tree.lines,
|
|
991
973
|
"",
|
|
992
|
-
"
|
|
974
|
+
"下一步(必须执行):",
|
|
975
|
+
"1. 从文件树中判断哪些文件可能包含数据库连接配置——任意语言/格式都算:Spring application*.yml、Python settings/config.py、Go config.yaml、Node .env、Rust .env+config.toml、docker-compose、K8s manifest 等;",
|
|
976
|
+
"2. 用你的读文件工具阅读这些文件;若发现 bootstrap.yml/application.yml 指向配置中心(nacos/apollo/spring-cloud-config),用配置中心地址与凭据拉取远端配置后再提取——例如 Nacos:先用 POST {server}/nacos/v1/auth/login(form: username/password)拿 accessToken,再 GET {server}/nacos/v1/cs/configs?dataId=<服务名>.yaml&group=<group>&tenant=<namespace-id>&accessToken=<token>,返回的 YAML 原文同样提取候选;",
|
|
977
|
+
"3. 从本地/远端配置中提取连接候选:dialectId(postgresql/mysql/oracle/dm/redis/elasticsearch/mongodb/neo4j/hive/spark)、host、port、database、username、password、url(如有完整连接串)、source(来源文件或 dataId);用户指定了类型(如「配置pg」)就只提取该类型;",
|
|
978
|
+
"4. 调用 db_scan_save 提交候选数组。带 url 的候选必须能被对应方言解析,编造的 url 会被拒绝;",
|
|
979
|
+
"5. 工具会弹确认框让用户逐个确认并补录密码,把工具返回的保存结果汇报给用户。",
|
|
993
980
|
].join("\n");
|
|
994
|
-
return { content: [{ type: "text" as const, text }] };
|
|
981
|
+
return { details: undefined, content: [{ type: "text" as const, text }] };
|
|
995
982
|
} catch (err) {
|
|
996
|
-
return {
|
|
983
|
+
return { details: undefined,
|
|
997
984
|
content: [{ type: "text" as const, text: `扫描失败: ${err instanceof Error ? err.message : String(err)}` }],
|
|
998
985
|
};
|
|
999
986
|
}
|
|
1000
987
|
},
|
|
1001
988
|
});
|
|
1002
989
|
|
|
990
|
+
// 工具 4b: db_scan_save(v1.3.0:AI 提交提取的候选,弹框确认后写盘——会话内闭环)
|
|
991
|
+
pi.registerTool({
|
|
992
|
+
name: "db_scan_save",
|
|
993
|
+
label: "保存扫描到的数据库连接",
|
|
994
|
+
description: "接收 AI 从项目配置提取的数据库连接候选数组,逐个弹确认框(用户确认+补录缺字段+测试连接)后写盘。绝不静默建连;url 与类型矛盾的候选会被拒绝(防幻觉)。",
|
|
995
|
+
promptSnippet: "在 scan_project_configs 之后调用。candidates 传候选数组,每项含 dialectId/host/port/database/username/password/url/source;被拒绝的候选根据返回的 rejected 原因修正后可重提;把每个候选的保存结果(已保存/已跳过/失败)汇报给用户。",
|
|
996
|
+
parameters: Type.Object({
|
|
997
|
+
candidates: Type.Array(Type.Object({
|
|
998
|
+
dialectId: Type.String({ description: "数据库类型:postgresql/mysql/oracle/dm/redis/elasticsearch/mongodb/neo4j/hive/spark" }),
|
|
999
|
+
host: Type.Optional(Type.String({ description: "主机(无 url 时必填)" })),
|
|
1000
|
+
port: Type.Optional(Type.Union([Type.Number(), Type.String()], { description: "端口" })),
|
|
1001
|
+
database: Type.Optional(Type.String({ description: "库名/schema(按类型语义)" })),
|
|
1002
|
+
username: Type.Optional(Type.String({ description: "账号" })),
|
|
1003
|
+
password: Type.Optional(Type.String({ description: "密码(缺失时确认框会引导用户补录)" })),
|
|
1004
|
+
url: Type.Optional(Type.String({ description: "完整连接串(可选,提供时必须能被方言解析,否则整条拒绝)" })),
|
|
1005
|
+
name: Type.Optional(Type.String({ description: "连接名称(缺省自动生成)" })),
|
|
1006
|
+
source: Type.Optional(Type.String({ description: "来源描述,如配置文件相对路径" })),
|
|
1007
|
+
}), { description: "连接候选数组" }),
|
|
1008
|
+
}),
|
|
1009
|
+
async execute(_toolCallId: string, params: { candidates: unknown[] }, _signal: any, _onUpdate?: any, ctx?: any) {
|
|
1010
|
+
if (!ctx?.ui) {
|
|
1011
|
+
return { details: undefined, content: [{ type: "text" as const, text: "db_scan_save 需要交互式会话(ctx.ui 不可用)。请引导用户在终端执行 /db add 手动添加。" }] };
|
|
1012
|
+
}
|
|
1013
|
+
const { candidates, rejected } = validateCandidates(params.candidates, new Set(loadConfigs().map((c) => c.name)));
|
|
1014
|
+
const lines: string[] = [];
|
|
1015
|
+
if (rejected.length > 0) {
|
|
1016
|
+
lines.push(`⛔ ${rejected.length} 个候选被拒绝(疑似编造或字段矛盾,请修正后重提):`, ...rejected.map((r) => " - " + r), "");
|
|
1017
|
+
}
|
|
1018
|
+
if (candidates.length === 0) {
|
|
1019
|
+
lines.push(rejected.length > 0 ? "没有可保存的候选。" : "未收到有效候选(candidates 需为数组,每项含 dialectId+host)。",
|
|
1020
|
+
"提示:从配置文件原文提取后重试;确认方言取值在支持列表内。");
|
|
1021
|
+
return { details: undefined, content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
1022
|
+
}
|
|
1023
|
+
lines.push(`收到 ${candidates.length} 个连接候选,逐个确认:`, ...candidates.map(candidateLine), "");
|
|
1024
|
+
const results: string[] = [];
|
|
1025
|
+
for (const cand of candidates) {
|
|
1026
|
+
try {
|
|
1027
|
+
const r = await confirmAndSaveCandidate(ctx, cand);
|
|
1028
|
+
results.push(`${SCAN_GLYPH[cand.status]} ${cand.partial.name} (${cand.dialectId}): ${r === "saved" ? "已保存" : r === "skipped" ? "用户跳过" : "连接失败未保存"}`);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
results.push(`❌ ${cand.partial.name} (${cand.dialectId}): 异常 ${err instanceof Error ? err.message : String(err)}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
lines.push("", "保存结果:", ...results.map((r) => " - " + r));
|
|
1034
|
+
return { details: undefined, content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1003
1038
|
// 工具 5: db_connections(v1.1 UX 共识 Q5:AI 自查连接清单与环境标签,识别生产库)
|
|
1004
1039
|
pi.registerTool({
|
|
1005
1040
|
name: "db_connections",
|
|
@@ -1010,7 +1045,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1010
1045
|
async execute() {
|
|
1011
1046
|
const configs = loadConfigs();
|
|
1012
1047
|
if (configs.length === 0) {
|
|
1013
|
-
return {
|
|
1048
|
+
return { details: undefined,
|
|
1014
1049
|
content: [{ type: "text" as const, text: "尚无数据库连接。请引导用户在终端执行 /db add 或 /db scan 建连。" }],
|
|
1015
1050
|
};
|
|
1016
1051
|
}
|
|
@@ -1023,7 +1058,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1023
1058
|
const url = registry.get(c.type)?.displayUrl(c) ?? "";
|
|
1024
1059
|
return `- ${c.name} [${shortTypeLabel(c.type)}]${marks ? " " + marks : ""} - ${fullTypeLabel(c.type)}${c.database ? " · 库: " + c.database : ""} · ${url} · ${test}${used ? " · " + used : ""}${c.description ? " · " + c.description : ""}`;
|
|
1025
1060
|
});
|
|
1026
|
-
return {
|
|
1061
|
+
return { details: undefined,
|
|
1027
1062
|
content: [{ type: "text" as const, text: `共 ${configs.length} 个连接:\n${lines.join("\n")}\n\n标注 [prod·强制只读] 的连接永远只读(即使全局允许写操作)。` }],
|
|
1028
1063
|
};
|
|
1029
1064
|
},
|
|
@@ -1043,7 +1078,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1043
1078
|
"⚡ 切换默认",
|
|
1044
1079
|
"✏️ 编辑连接",
|
|
1045
1080
|
"➕ 新增连接",
|
|
1046
|
-
"🔎
|
|
1081
|
+
"🔎 扫描建连(会话 AI)",
|
|
1047
1082
|
"🗑️ 删除连接",
|
|
1048
1083
|
"⚙️ 设置",
|
|
1049
1084
|
];
|
|
@@ -1076,8 +1111,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1076
1111
|
if (config) await editDbConfig(ctx, config);
|
|
1077
1112
|
} else if (navChoice === "➕ 新增连接") {
|
|
1078
1113
|
await addDbConfig(ctx);
|
|
1079
|
-
} else if (navChoice === "🔎
|
|
1080
|
-
|
|
1114
|
+
} else if (navChoice === "🔎 扫描建连(会话 AI)") {
|
|
1115
|
+
ctx.ui.notify("扫描建连已升级为会话 AI 驱动:请在对话框直接说「连一下这个项目的数据库」或「/db scan 配置pg」,\nAI 会读取项目配置文件并提取连接,在会话内弹框确认后建连。", "info");
|
|
1081
1116
|
} else if (navChoice === "🗑️ 删除连接") {
|
|
1082
1117
|
await deleteDbConfig(ctx, configs);
|
|
1083
1118
|
}
|
|
@@ -1090,10 +1125,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1090
1125
|
if (config) await editDbConfig(ctx, config);
|
|
1091
1126
|
} else if (sub === "rm" || sub === "remove" || sub === "del" || sub === "delete" || sub === "d") {
|
|
1092
1127
|
await deleteDbConfig(ctx, configs);
|
|
1093
|
-
} else if (sub === "scan"
|
|
1094
|
-
// /db scan [
|
|
1095
|
-
|
|
1096
|
-
await scanWizard(ctx, scanPath);
|
|
1128
|
+
} else if (sub === "scan") {
|
|
1129
|
+
// /db scan [自然语言]:提取已迁移到会话 AI(正则引擎已废弃),终端仅引导
|
|
1130
|
+
ctx.ui.notify("扫描建连已升级为会话 AI 驱动:请在对话框直接说「连一下这个项目的数据库」或「/db scan 配置pg」。\nAI 会读取项目配置文件(任意语言)提取连接,在会话内弹框确认后建连。", "info");
|
|
1097
1131
|
} else if (sub === "ls" || sub === "list") {
|
|
1098
1132
|
showDbList(ctx, configs);
|
|
1099
1133
|
} else {
|
|
@@ -1103,7 +1137,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1103
1137
|
" edit 编辑连接\n" +
|
|
1104
1138
|
" rm 删除连接\n" +
|
|
1105
1139
|
" ls 列出所有连接\n" +
|
|
1106
|
-
" scan
|
|
1140
|
+
" scan 扫描已迁移到会话 AI:在对话框说「连一下这个项目的数据库」\n" +
|
|
1107
1141
|
" config 查看/修改插件设置\n" +
|
|
1108
1142
|
" 默认 打开管理菜单(查看/编辑/新增/扫描/删除/设置)",
|
|
1109
1143
|
"info"
|