diff --git a/feasibility_analysis.md b/feasibility_analysis.md
index 8148716..aa76675 100644
--- a/feasibility_analysis.md
+++ b/feasibility_analysis.md
@@ -4,7 +4,7 @@
可行性分析用于判断 HyperSql 在当前技术条件、开发时间、经济成本和实际使用场景下是否能够顺利完成。
-HyperSql 是一个基于 Java 24、JavaFX、Maven 和 SQLite 的轻量级数据库图形化管理工具,主要面向学生和 SQLite 初学者。目前项目已经完成了核心数据库浏览、表管理、数据行编辑和 SQLite 支持范围内的表结构编辑功能,后续将继续扩展备份恢复和 AI 辅助 SQL 生成。
+HyperSql 是一个基于 Java 24、JavaFX、Maven 和 SQLite 的轻量级数据库图形化管理工具,主要面向学生和 SQLite 初学者。目前项目已经完成了核心数据库浏览、表管理、数据行编辑、SQLite 支持范围内的表结构编辑、数据库备份恢复和 AI 辅助 SQL 生成功能。
本项目从以下四个方面进行分析:
@@ -20,11 +20,13 @@ HyperSql 采用以下技术栈:
| 技术 | 作用 | 当前使用情况 |
|---|---|---|
| Java 24 | 主要开发语言 | 已用于项目主要代码开发 |
-| JavaFX | 构建图形化用户界面 | 已实现主界面、表格、菜单、工具栏、创建表对话框、数据行编辑和结构编辑操作入口 |
-| Maven | 项目构建与依赖管理 | 已完成项目构建配置 |
+| JavaFX | 构建图形化用户界面 | 已实现主界面、表格、菜单、工具栏、创建表对话框、数据行编辑、结构编辑、备份恢复入口和 AI 设置/生成入口 |
+| Maven | 项目构建与依赖管理 | 已完成项目构建配置,并管理 JavaFX、SQLite JDBC、Jackson 等依赖 |
| SQLite | 本地轻量级数据库 | 作为系统管理对象,并通过原生 ALTER TABLE 支持部分表结构编辑 |
-| SQLite JDBC | Java 程序访问 SQLite 数据库 | 已实现数据库连接、元数据读取、SQL 执行、数据行写入和表结构变更 |
-| AI API | 根据表结构和用户需求生成 SQL | 作为后续扩展功能,当前尚未实现 |
+| SQLite JDBC | Java 程序访问 SQLite 数据库 | 已实现数据库连接、元数据读取、SQL 执行、数据行写入、表结构变更、备份前检查点等操作 |
+| JDK HttpClient | 调用 AI API | 已用于请求 OpenAI 兼容接口和 Anthropic Claude Messages API |
+| Jackson | JSON 构造与解析 | 已用于构造 AI 请求体和解析 AI 返回结果 |
+| AI API | 根据表结构和用户需求生成 SQL | 已支持 OpenAI 兼容接口和 Anthropic Claude,API Key 由用户在 UI 中输入并仅保存在本次运行内存中 |
### 技术可行性说明
@@ -33,16 +35,16 @@ HyperSql 采用以下技术栈:
- 当前项目已经通过 Maven 编译验证,说明技术环境可正常运行。
2. **JavaFX 能满足 GUI 需求**
- - JavaFX 提供菜单栏、按钮、标签、文本框、表格、分页按钮、弹窗和 FXML 等组件。
- - 当前项目已经实现左侧表/视图列表,右侧表数据、表结构和 SQL 执行区域,并提供数据行新增/删除按钮、可编辑单元格和表结构编辑按钮,能够满足数据库管理工具的基本界面需求。
+ - JavaFX 提供菜单栏、按钮、标签、文本框、密码输入框、下拉框、表格、分页按钮、弹窗和 FXML 等组件。
+ - 当前项目已经实现左侧表/视图列表,右侧表数据、表结构和 SQL 执行区域,并提供数据行新增/删除按钮、可编辑单元格、表结构编辑按钮、备份恢复菜单和 AI SQL 生成入口。
3. **SQLite 集成难度低**
- SQLite 是文件型数据库,不需要单独部署数据库服务器,适合课程项目和本地数据库管理。
- - 当前项目已经支持打开、新建、关闭和删除 SQLite 数据库文件。
+ - 当前项目已经支持打开、新建、关闭、删除、备份和恢复 SQLite 数据库文件。
4. **SQLite JDBC 访问方式成熟**
- 通过 JDBC 可以完成数据库连接、查询、执行 SQL、读取表结构等操作。
- - 当前项目已经实现表/视图列表读取、字段信息读取、表数据分页查询、SQL 执行、数据行插入/更新/删除和部分表结构变更。
+ - 当前项目已经实现表/视图列表读取、字段信息读取、表数据分页查询、SQL 执行、数据行插入/更新/删除、部分表结构变更和备份恢复前后的连接管理。
5. **表管理和结构编辑功能可以通过 SQL 和 JavaFX UI 实现**
- 当前项目已经通过独立创建表对话框实现了表名输入、字段定义、字段类型选择、主键/非空设置、默认值填写和 SQL 预览。
@@ -53,20 +55,26 @@ HyperSql 采用以下技术栈:
- 当前项目已经通过主键或 SQLite `rowid` 定位数据行,使用参数化 SQL 完成新增、修改和删除。
- SQL 返回结果只对可识别的单表查询启用编辑,复杂查询保持只读,降低误操作风险。
-7. **AI SQL 生成功能技术上可行,但需要控制范围**
- - 后续可以读取当前数据库表结构,结合用户自然语言描述构造提示词,然后调用 AI API 生成 SQL。
- - 该功能涉及 API Key 管理、网络请求、调用成本和生成结果校验,因此适合作为扩展功能。
+7. **数据库备份恢复功能技术上可行**
+ - SQLite 数据库本质上是本地文件,因此可以通过文件复制完成备份。
+ - 当前项目在备份前执行检查点操作,并在恢复前关闭数据库连接,使用临时文件替换目标数据库,恢复后重新打开数据库并刷新界面。
+ - 恢复操作前会弹出确认提示,避免误覆盖当前数据库。
+
+8. **AI SQL 生成功能技术上可行**
+ - 当前项目已经能够读取数据库表和视图结构,并将结构信息与用户自然语言需求组合成提示词。
+ - 系统通过 JDK HttpClient 调用 OpenAI 兼容接口或 Anthropic Claude Messages API,并用 Jackson 处理 JSON 请求和响应。
+ - API Key 只保存在运行内存中,不写入本地文件;AI 生成的 SQL 只填入 SQL 输入框,不自动执行,降低安全风险。
### 技术可行性结论
-HyperSql 所采用的核心技术已经在当前项目中得到验证。数据库连接、表结构读取、表数据分页显示、SQL 执行、数据库文件管理、表管理、数据行编辑和 SQLite 安全结构编辑功能均已实现,因此项目核心功能在技术上可行。AI SQL 生成属于后续扩展,技术上可实现,但需要在安全性和调用成本方面进行控制。
+HyperSql 所采用的核心技术已经在当前项目中得到验证。数据库连接、表结构读取、表数据分页显示、SQL 执行、数据库文件管理、表管理、数据行编辑、SQLite 安全结构编辑、备份恢复和 AI SQL 生成均已实现,因此项目核心功能和主要扩展功能在技术上可行。
## 3. 经济可行性
HyperSql 的开发成本较低,主要体现在以下方面:
1. **开发工具成本低**
- - Java、JavaFX、Maven、SQLite 和 SQLite JDBC 均可免费使用。
+ - Java、JavaFX、Maven、SQLite、SQLite JDBC 和 Jackson 均可免费使用。
- 可使用 IntelliJ IDEA Community、VS Code 等免费开发工具。
2. **运行环境成本低**
@@ -78,12 +86,13 @@ HyperSql 的开发成本较低,主要体现在以下方面:
- 当前已完成的功能不涉及商业授权费用。
4. **AI API 成本可控**
- - AI SQL 生成尚未实现,可以作为后续扩展功能。
- - 若后续实现,可限制调用次数,或只在演示场景使用少量 API 调用,避免高额费用。
+ - AI SQL 生成功能由用户自行输入 API Key,系统本身不内置付费密钥。
+ - 该功能只在用户主动点击生成时调用 API,不会自动后台调用。
+ - 生成 SQL 时只发送数据库结构和用户需求,不发送表数据,减少请求内容规模。
### 经济可行性结论
-HyperSql 不需要额外硬件和商业软件投入,当前已实现功能均基于免费技术完成,整体开发与运行成本较低,经济上可行。
+HyperSql 不需要额外硬件和商业软件投入,当前主要功能均基于免费技术完成。AI 功能可能产生的调用费用由用户配置的 API 服务决定,并且只在用户主动使用时发生,因此整体开发与运行成本较低,经济上可行。
## 4. 时间可行性
@@ -104,21 +113,22 @@ HyperSql 采用增量迭代模型,可以按照功能优先级逐步完成,
| 交互修复 | 修复选择表不同步、刷新表列表保留当前表、SQL 结果区启动空状态 | 已完成 |
| 数据行编辑 | 表数据界面新增、修改、删除数据行;可编辑单表 SQL 结果 | 已完成 |
| 表结构安全编辑 | 重命名表、新增字段、重命名字段 | 已完成 |
-| 备份恢复 | 数据库备份和恢复 | 待实现 |
-| AI SQL 生成 | 根据表结构和自然语言生成 SQL | 待实现 |
+| 备份恢复 | 数据库备份和从备份恢复 | 已完成 |
+| 界面优化 | 整理菜单和工具栏,降低界面拥挤程度 | 已完成 |
+| AI SQL 生成 | 根据表结构和自然语言生成 SQL,支持 OpenAI 兼容接口和 Anthropic Claude | 已完成 |
| 测试优化 | 功能测试、界面优化、最终演示准备 | 持续进行 |
### 时间可行性说明
1. 当前项目已经完成数据库管理工具的核心可运行版本,可以满足基本演示要求。
2. 数据行编辑和 SQLite 支持范围内的表结构编辑已经完成,软件实用性进一步提高。
-3. 后续开发可以优先完成备份恢复,因为它与数据库管理工具的数据安全性直接相关。
-4. AI SQL 生成可以作为亮点功能,根据剩余时间决定实现深度。
-5. 采用增量迭代方式后,即使扩展功能未全部完成,系统仍然具备可运行、可演示的核心功能。
+3. 备份恢复功能已经完成,增强了数据库文件管理和数据安全能力。
+4. AI SQL 生成功能已经完成基础版本,可以作为课程项目亮点功能展示。
+5. 后续主要工作集中在测试、演示数据准备、界面细节优化和文档完善,风险相对较低。
### 时间可行性结论
-从当前进展看,HyperSql 的核心功能和主要编辑功能已经完成,课程周期内完成一个可演示版本具有较高可行性。后续需要合理控制扩展功能范围,避免 AI 调用和复杂数据库结构重建功能占用过多开发时间。
+从当前进展看,HyperSql 的核心功能和主要扩展功能已经完成,课程周期内形成可运行、可演示版本具有较高可行性。后续只需继续进行测试和演示准备即可。
## 5. 操作可行性
@@ -139,34 +149,41 @@ HyperSql 面向学生和 SQLite 初学者,软件操作流程较为简单。
↓
在 SQLite 支持范围内编辑表结构
↓
-在 SQL 执行区输入并执行 SQL
+根据需要备份或恢复数据库
↓
-根据需要创建表、删除表或刷新表列表
+在 SQL 执行区输入 SQL 或使用 AI 生成 SQL
+ ↓
+检查 SQL 后手动执行
```
### 操作可行性说明
1. **界面符合用户习惯**
- 采用类似数据库管理工具的布局:左侧显示表/视图列表,右侧显示表数据、表结构和 SQL 执行区域。
+ - 菜单和工具栏经过整理,高频操作更明显,危险或低频操作放在菜单中,界面更清晰。
2. **降低数据库操作门槛**
- - 用户可以通过图形界面打开数据库、浏览表结构、查看表数据、新增修改删除数据行、创建表、删除表和进行部分表结构编辑,不必完全依赖命令行工具。
+ - 用户可以通过图形界面打开数据库、浏览表结构、查看表数据、新增修改删除数据行、创建表、删除表、进行部分表结构编辑、备份恢复数据库,不必完全依赖命令行工具。
3. **分页降低操作压力**
- 表数据和 SQL 查询结果都使用分页显示,每页 100 行,避免一次显示大量数据导致界面卡顿或不便查看。
4. **错误提示清晰**
- - 当未连接数据库、数据库连接失败、SQL 执行错误、创建表失败或删除表失败时,系统会给出提示信息。
+ - 当未连接数据库、数据库连接失败、SQL 执行错误、创建表失败、删除表失败、备份恢复失败或 AI 调用失败时,系统会给出提示信息。
5. **危险操作有确认流程**
- - 删除数据库文件、删除表和删除数据行都属于破坏性操作,系统在执行前会弹出确认提示。
+ - 删除数据库文件、删除表、删除数据行、恢复数据库和结构变更等操作都属于破坏性操作,系统在执行前会弹出确认提示。
-6. **当前限制清晰**
- - 当前版本已经支持创建表、删除普通表、数据行编辑和 SQLite 原生支持的部分结构编辑,但尚未实现数据库备份恢复和 AI SQL 生成。
+6. **AI 生成 SQL 使用方式安全**
+ - 用户在 UI 中设置 API Key、Base URL 和模型。
+ - API Key 只在本次运行中保存。
+ - 系统只将数据库结构和用户需求发送给 AI,不发送表数据。
+ - AI 生成的 SQL 只填入 SQL 输入框,不自动执行,用户可以检查后再手动运行。
+ - 如果生成内容包含可能修改数据或结构的关键词,系统会提示用户谨慎检查。
### 操作可行性结论
-HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常见数据浏览、数据编辑和部分结构编辑操作,适合学生和 SQLite 初学者使用。随着后续备份恢复功能完善,软件的数据安全性会进一步提高。
+HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常见数据浏览、数据编辑、部分结构编辑、备份恢复和 AI 辅助 SQL 生成操作,适合学生和 SQLite 初学者使用。
## 6. 可行性分析总结
@@ -174,11 +191,11 @@ HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常
| 分析方面 | 结论 |
|---|---|
-| 技术可行性 | JavaFX、SQLite JDBC 和 Maven 已经支撑当前核心功能、数据编辑和结构编辑实现,AI API 可作为后续扩展 |
-| 经济可行性 | 开发工具和数据库免费,当前功能无额外运行成本 |
-| 时间可行性 | 核心功能和主要编辑功能已经完成,后续扩展可按优先级继续迭代 |
-| 操作可行性 | 面向学生和初学者,界面简单直观,危险操作有确认提示 |
+| 技术可行性 | JavaFX、SQLite JDBC、Maven、JDK HttpClient 和 Jackson 已经支撑当前核心功能、数据编辑、结构编辑、备份恢复和 AI SQL 生成实现 |
+| 经济可行性 | 开发工具和数据库免费,当前功能无额外固定运行成本,AI 调用成本由用户自行配置和控制 |
+| 时间可行性 | 核心功能和主要扩展功能已经完成,后续重点是测试和演示准备 |
+| 操作可行性 | 面向学生和初学者,界面简单直观,危险操作有确认提示,AI 生成 SQL 不自动执行 |
### 总体结论
-HyperSql 的技术方案成熟,开发成本低,核心功能和主要编辑功能已经形成可运行版本,适合作为软件工程课程大作业项目继续完善。后续应优先补充数据库备份恢复功能,再根据时间实现 AI 辅助 SQL 生成。
\ No newline at end of file
+HyperSql 的技术方案成熟,开发成本低,核心功能和主要扩展功能已经形成可运行版本,适合作为软件工程课程大作业项目继续完善和演示。当前项目已经具备数据库浏览、SQL 执行、数据编辑、结构编辑、备份恢复和 AI 辅助 SQL 生成等功能,整体可行性较高。
diff --git a/pom.xml b/pom.xml
index 3520dee..c3ea66a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,11 @@
slf4j-simple
2.0.17
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.18.3
+
diff --git a/requirements_analysis.md b/requirements_analysis.md
index 09c1a1c..0e2afa9 100644
--- a/requirements_analysis.md
+++ b/requirements_analysis.md
@@ -11,10 +11,11 @@ HyperSql 是一个面向学生和 SQLite 初学者的轻量级数据库图形化
3. 数据库表结构和表数据需要以更直观的方式展示。
4. 大表或较多查询结果需要分页显示,避免界面卡顿和阅读困难。
5. 创建数据库、创建表、删除表等常见操作需要简洁清晰的图形界面。
-6. 删除数据库文件、删除表和删除数据行等危险操作需要确认流程,降低误操作风险。
+6. 删除数据库文件、删除表、删除数据行、恢复数据库等危险操作需要确认流程,降低误操作风险。
7. 表结构修改需要尊重 SQLite 原生能力,只提供安全可控的结构编辑入口。
+8. 用户希望通过 AI 根据当前数据库结构和自然语言需求辅助生成 SQL,但生成结果不应自动执行。
-当前版本已经完成了数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、SQL 结果分页、创建表、删除表、数据行编辑和 SQLite 支持范围内的表结构编辑等核心功能。数据库备份恢复和 AI 辅助 SQL 生成作为后续扩展功能继续开发。
+当前版本已经完成了数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、SQL 结果分页、创建表、删除表、数据行编辑、SQLite 支持范围内的表结构编辑、数据库备份恢复和 AI 辅助 SQL 生成等核心与扩展功能。
## 2. 用户需求分析
@@ -39,7 +40,11 @@ HyperSql 是一个面向学生和 SQLite 初学者的轻量级数据库图形化
9. 用户通过图形界面新增、修改或删除表中的数据行。
10. 用户在 SQLite 支持范围内重命名表、新增字段或重命名字段。
11. 用户刷新表列表,并希望保持当前选中的表不变。
-12. 后续版本中,用户可以备份恢复数据库,或使用 AI 辅助生成 SQL。
+12. 用户将当前数据库备份为另一个文件。
+13. 用户从备份文件恢复当前数据库,并在恢复前得到确认提示。
+14. 用户在 UI 中配置 AI Provider、API Key、Base URL 和模型。
+15. 用户输入自然语言需求,由 AI 根据当前数据库结构生成 SQL。
+16. 用户检查 AI 生成的 SQL,然后手动点击执行。
## 3. 功能性需求
@@ -56,8 +61,9 @@ HyperSql 的功能性需求主要包括以下模块:
| 表结构安全编辑 | 支持重命名表、新增字段、重命名字段 | 已实现 |
| 状态与错误提示 | 显示连接状态、执行结果和错误信息 | 已实现 |
| 数据编辑 | 支持新增、修改、删除表数据行,并支持可识别单表 SQL 结果编辑 | 已实现 |
-| 备份与恢复 | 支持数据库文件备份和从备份文件恢复 | 待实现 |
-| AI SQL 生成 | 根据表结构和用户自然语言需求生成 SQL | 待实现 |
+| 备份与恢复 | 支持数据库文件备份和从备份文件恢复 | 已实现 |
+| AI SQL 生成 | 根据表结构和用户自然语言需求生成 SQL,支持 OpenAI 兼容接口和 Anthropic Claude | 已实现 |
+| UI 优化 | 整理菜单、工具栏和侧边栏操作入口,降低界面拥挤程度 | 已实现 |
## 4. 核心功能需求说明
@@ -160,36 +166,53 @@ HyperSql 的功能性需求主要包括以下模块:
### 5.2 数据库备份与恢复
-- 系统后续应支持将当前数据库复制为备份文件。
-- 系统后续应支持用户选择备份文件进行恢复。
+- 系统应支持将当前数据库复制为备份文件。
+- 系统应支持用户选择备份文件进行恢复。
- 系统应在恢复前提示用户确认,避免覆盖当前数据库。
+- 系统应避免将当前数据库文件本身作为备份目标或恢复来源。
+- 系统应在恢复前关闭当前数据库连接,恢复后重新打开数据库并刷新界面。
- 系统应显示备份或恢复的执行结果。
-当前实现状态:待实现。
+当前实现状态:已实现。
### 5.3 AI 辅助 SQL 生成
-- 系统后续应读取当前数据库的表结构信息。
-- 系统后续应根据表结构自动生成 AI 提示词。
+- 系统应读取当前数据库的表结构和视图结构信息。
+- 系统应根据表结构自动生成 AI 提示词。
- 用户可以输入自然语言需求。
+- 系统支持 OpenAI 兼容接口和 Anthropic Claude。
+- 用户应能在 UI 中设置 Provider、API Key、Base URL、模型和超时时间。
+- API Key 只应保存在本次运行内存中,不应写入本地配置文件。
- 系统调用 AI API 生成 SQL 语句。
- 用户可以检查并确认生成的 SQL 后再执行。
+- AI 生成的 SQL 不应直接自动执行,应由用户确认后手动执行。
+- 系统不应向 AI 发送表数据,只发送数据库结构和用户需求。
- 若 AI 调用失败,系统应显示错误提示。
-- AI 生成的 SQL 不应直接自动执行,应由用户确认后执行。
+- 若生成 SQL 包含可能修改数据或结构的关键词,系统应提示用户谨慎检查。
-当前实现状态:待实现。
+当前实现状态:已实现。
+
+### 5.4 界面整理优化
+
+- 系统菜单应包含完整功能入口。
+- 工具栏应保留高频操作,避免按钮过多导致界面拥挤。
+- 表相关操作应放在侧边栏附近,方便用户针对当前表操作。
+- 危险或低频操作应主要放在菜单中,减少误触。
+
+当前实现状态:已实现。
## 6. 非功能性需求
| 需求类型 | 说明 | 当前体现 |
|---|---|---|
-| 易用性 | 界面清晰,操作流程简单,适合初学者使用 | 已采用菜单、工具栏、表格、创建表对话框、数据行编辑按钮和结构编辑按钮 |
-| 可靠性 | 数据库连接、SQL 执行、数据编辑和表操作失败时应有错误提示 | 已实现基础错误提示 |
-| 安全性 | 删除数据库、删除表、删除数据行和结构变更等操作前应确认 | 已实现确认流程 |
+| 易用性 | 界面清晰,操作流程简单,适合初学者使用 | 已采用菜单、工具栏、侧边栏、表格、创建表对话框、数据行编辑按钮、结构编辑按钮和 AI 生成入口 |
+| 可靠性 | 数据库连接、SQL 执行、数据编辑、表操作、备份恢复和 AI 调用失败时应有错误提示 | 已实现基础错误提示 |
+| 安全性 | 删除数据库、删除表、删除数据行、恢复数据库和结构变更等操作前应确认;AI 生成 SQL 不自动执行 | 已实现确认流程和 AI 生成后手动执行机制 |
| 性能 | 对常见小型 SQLite 数据库能够快速打开和查询 | 表数据采用数据库侧分页,降低大表加载压力 |
-| 可维护性 | 采用模块化设计,便于后续扩展和维护 | 已划分数据库连接、元数据读取、SQL 执行、行数据服务、结构编辑服务、工具类和控制器 |
+| 隐私性 | AI 功能不应发送表数据,API Key 不应持久化到本地文件 | AI 提示只包含表/视图结构和用户需求,API Key 仅保存在运行内存中 |
+| 可维护性 | 采用模块化设计,便于后续扩展和维护 | 已划分数据库连接、元数据读取、SQL 执行、行数据服务、结构编辑服务、备份恢复逻辑、AI 客户端和工具类 |
| 兼容性 | 支持常见 SQLite 数据库文件,适配主流桌面系统 | 基于 JavaFX 和 SQLite JDBC,具备跨平台基础 |
-| 可扩展性 | 后续可扩展备份恢复和 AI SQL 生成 | 已保留 SQL 执行、元数据读取、行数据服务和结构编辑服务基础能力 |
+| 可扩展性 | 后续可扩展更多 AI Provider、SQL 历史记录和界面美化 | 已提供 AI Provider 适配层和独立 AI SQL 生成服务 |
## 7. 需求优先级
@@ -202,8 +225,9 @@ HyperSql 的功能性需求主要包括以下模块:
| 中 | 创建表、删除表、删除数据库文件 | 已实现 |
| 中 | 数据行新增、修改、删除 | 已实现 |
| 中 | SQLite 支持范围内的表结构编辑 | 已实现 |
-| 中 | 数据库备份与恢复 | 待实现 |
-| 低 | AI 辅助 SQL 生成、SQL 历史记录、界面进一步美化 | 待实现 |
+| 中 | 数据库备份与恢复 | 已实现 |
+| 低 | AI 辅助 SQL 生成、界面进一步美化 | 已实现 |
+| 低 | SQL 历史记录、更多数据库类型支持 | 后续可扩展 |
### 优先级说明
@@ -213,23 +237,24 @@ HyperSql 的功能性需求主要包括以下模块:
2. **中优先级需求**
- 提升软件实用性和数据安全性。
- - 当前已经完成创建表、删除表、删除数据库文件、数据行编辑和表结构安全编辑,后续应继续完成备份恢复。
+ - 当前已经完成创建表、删除表、删除数据库文件、数据行编辑、表结构安全编辑和备份恢复。
3. **低优先级需求**
- 作为项目亮点和扩展功能。
- - 可根据开发进度调整实现程度。
+ - 当前已经完成 AI 辅助 SQL 生成和基础界面整理,后续可继续扩展 SQL 历史记录或更多界面美化。
## 8. 当前版本限制
当前 HyperSql 仍存在以下限制:
-1. 还没有实现数据库备份与恢复功能。
-2. 还没有实现 AI 辅助 SQL 生成功能。
-3. SQL 查询结果分页属于客户端分页,查询结果会先全部读取到内存中,不适合特别大的查询结果。
-4. SQL 结果编辑只支持可识别的单表查询,JOIN、聚合、表达式等复杂结果保持只读。
-5. 表结构编辑只支持 SQLite 原生安全支持的重命名表、新增字段和重命名字段,不支持需要重建表的复杂结构修改。
-6. 当前删除表功能只支持删除普通表,不支持删除视图。
-7. 当前主要面向 SQLite,不支持 MySQL、PostgreSQL 等远程数据库。
+1. SQL 查询结果分页属于客户端分页,查询结果会先全部读取到内存中,不适合特别大的查询结果。
+2. SQL 结果编辑只支持可识别的单表查询,JOIN、聚合、表达式等复杂结果保持只读。
+3. 表结构编辑只支持 SQLite 原生安全支持的重命名表、新增字段和重命名字段,不支持需要重建表的复杂结构修改。
+4. 当前删除表功能只支持删除普通表,不支持删除视图。
+5. 当前主要面向 SQLite,不支持 MySQL、PostgreSQL 等远程数据库。
+6. AI 生成 SQL 的质量取决于用户配置的模型和 API 服务,生成结果仍需要用户检查后再执行。
+7. API Key 只在本次运行中保存,关闭软件后需要重新输入。
+8. 当前未实现 SQL 历史记录功能。
## 9. 需求分析总结
@@ -246,6 +271,9 @@ HyperSql 的功能性需求主要包括以下模块:
7. 删除普通表。
8. 数据行新增、修改和删除。
9. SQLite 支持范围内的表结构安全编辑。
-10. 状态提示、错误提示和危险操作确认。
+10. 数据库备份和从备份恢复。
+11. AI 辅助 SQL 生成。
+12. 状态提示、错误提示和危险操作确认。
+13. 菜单、工具栏和侧边栏入口整理。
-后续开发应优先完成数据库备份恢复,使系统从“可浏览、可执行 SQL、可编辑数据、可管理表结构”进一步发展为“可完整管理 SQLite 数据库”的工具。在时间允许的情况下,再实现 AI 辅助 SQL 生成作为项目亮点。
\ No newline at end of file
+当前 HyperSql 已经从“可浏览、可执行 SQL、可编辑数据、可管理表结构”进一步发展为“可较完整管理 SQLite 数据库,并提供 AI 辅助 SQL 生成”的轻量级数据库管理工具。后续可以围绕测试、演示体验、SQL 历史记录和更细致的界面美化继续迭代。
diff --git a/software_process_model.md b/software_process_model.md
index 1f1cc98..3990966 100644
--- a/software_process_model.md
+++ b/software_process_model.md
@@ -6,7 +6,7 @@
增量迭代模型是指将软件系统划分为多个相对独立的功能增量,每个增量都经过需求分析、设计、编码、测试和反馈优化等过程,最终逐步形成完整的软件系统。
-HyperSql 是一个基于 Java 24、JavaFX 和 Maven 的轻量级 SQLite 图形化管理工具。项目开发过程中,先完成数据库连接、表结构浏览、表数据查看和 SQL 执行等核心功能,再逐步增加数据库文件管理、表管理、分页显示、数据行编辑和表结构安全编辑等增强功能,最后根据时间继续扩展备份恢复和 AI 辅助 SQL 生成。
+HyperSql 是一个基于 Java 24、JavaFX 和 Maven 的轻量级 SQLite 图形化管理工具。项目开发过程中,先完成数据库连接、表结构浏览、表数据查看和 SQL 执行等核心功能,再逐步增加数据库文件管理、表管理、分页显示、数据行编辑、表结构安全编辑、备份恢复、界面优化和 AI 辅助 SQL 生成等增强功能。
## 2. 选择增量迭代模型的原因
@@ -20,6 +20,7 @@ HyperSql 选择增量迭代模型,主要有以下原因:
3. **降低开发风险**
- 先完成基础功能,再开发复杂功能,可以避免一开始实现过多内容导致项目失控。
+ - 对 SQLite 表结构编辑和 AI SQL 生成这类有安全边界的功能,可以在后续迭代中逐步控制范围。
4. **便于阶段性展示**
- 每个迭代版本都能形成可运行的软件,方便课程汇报、阶段检查和最终演示。
@@ -38,12 +39,13 @@ HyperSql 选择增量迭代模型,主要有以下原因:
| V1.3 | 分页显示优化 | 表数据按每页 100 行分页显示;SQL 查询结果区按每页 100 行分页显示 | 已完成 |
| V1.4 | 数据库文件管理增强 | 新建 SQLite 数据库文件、删除当前数据库文件 | 已完成 |
| V1.5 | 表管理功能 | 通过简洁 UI 创建表;删除选中普通表;删除前进行确认 | 已完成 |
-| V1.6 | 交互与缺陷修复 | 修复表/视图选择时数据与结构不同步问题;刷新表列表时保留当前选中表 | 已完成 |
+| V1.6 | 交互与缺陷修复 | 修复表/视图选择时数据与结构不同步问题;刷新表列表时保留当前选中表;SQL 结果区启动时保持空状态 | 已完成 |
| V1.7 | 数据行增删改查 | 在表数据界面新增、修改、删除数据行;在可识别单表 SQL 结果中支持编辑 | 已完成 |
| V1.8 | 表结构安全编辑 | 支持 SQLite 原生安全的重命名表、新增字段、重命名字段 | 已完成 |
-| V1.9 | 备份与恢复 | 数据库文件备份、从备份恢复数据库 | 待实现 |
-| V2.0 | AI 辅助 SQL 生成 | 根据表结构和用户自然语言需求调用 AI API 生成 SQL | 待实现 |
-| V2.1 | 测试与优化 | 完善异常处理、界面优化、系统测试和最终演示准备 | 持续进行 |
+| V1.9 | 备份与恢复 | 数据库文件备份、从备份恢复数据库,恢复前进行确认并重新打开数据库 | 已完成 |
+| V2.0 | 界面整理优化 | 整理菜单和工具栏,将高频操作保留在工具栏,将危险或低频操作放入菜单 | 已完成 |
+| V2.1 | AI 辅助 SQL 生成 | 根据表结构和用户自然语言需求调用 OpenAI 兼容接口或 Anthropic Claude 生成 SQL | 已完成 |
+| V2.2 | 测试与优化 | 编译验证、启动验证、功能回归和最终演示准备 | 持续进行 |
## 4. 单次迭代流程
@@ -69,15 +71,18 @@ HyperSql 选择增量迭代模型,主要有以下原因:
1. **需求细化**
- 明确本次迭代要完成的功能和边界。例如创建表功能要求有简洁易用的 UI,而不是只让用户手写 SQL。
+ - 对 AI SQL 生成明确了 API Key 只在本次运行中保存,同时支持 OpenAI 兼容接口和 Anthropic Claude。
2. **界面与功能设计**
- 设计 JavaFX 界面布局和功能交互流程。例如主界面采用左侧表/视图列表、右侧表数据/表结构/SQL 执行区域的布局。
+ - 对菜单和工具栏进行整理,保留常用操作入口,减少界面拥挤。
3. **编码实现**
- 使用 Java 24、JavaFX、SQLite JDBC 和 Maven 完成功能开发。
+ - 使用 JDK 自带 HttpClient 和 Jackson 实现 AI API 调用与 JSON 解析。
4. **功能测试**
- - 测试数据库连接、表结构读取、分页显示、SQL 执行、创建数据库、删除数据库、创建表和删除表等功能是否正确。
+ - 测试数据库连接、表结构读取、分页显示、SQL 执行、创建数据库、删除数据库、创建表、删除表、备份恢复、AI 设置和 AI SQL 生成等功能是否正确。
5. **问题修复**
- 根据测试结果修复异常、界面错误和逻辑问题。例如修复刷新表列表后错误切换到第一张表的问题。
@@ -107,11 +112,14 @@ HyperSql 选择增量迭代模型,主要有以下原因:
5. **逐步扩展编辑能力**
- 在只读浏览功能完成后,继续实现了数据行新增、修改、删除,并基于 SQLite 原生 `ALTER TABLE` 能力实现了重命名表、新增字段、重命名字段。
-6. **保留后续扩展空间**
- - 数据库备份恢复和 AI SQL 生成尚未完成,后续可作为新的增量继续实现。
+6. **完善数据安全能力**
+ - 在数据库文件管理基础上增加备份和恢复功能,恢复前进行确认,并在恢复后重新打开数据库,降低数据丢失风险。
+
+7. **增加智能辅助能力**
+ - 在 SQL 执行功能稳定后,增加 AI 辅助 SQL 生成。系统只发送数据库结构,不发送表数据;生成的 SQL 只填入输入框,不自动执行,由用户检查后手动运行。
## 6. 总结
-HyperSql 采用增量迭代模型进行开发是合适的。当前项目已经完成了 SQLite 数据库管理工具的核心可运行版本,包括数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行分页显示、创建表、删除表、数据行增删改查和 SQLite 支持范围内的表结构安全编辑等功能。
+HyperSql 采用增量迭代模型进行开发是合适的。当前项目已经完成了 SQLite 数据库管理工具的核心可运行版本,包括数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行分页显示、创建表、删除表、数据行增删改查、SQLite 支持范围内的表结构安全编辑、数据库备份恢复和 AI 辅助 SQL 生成等功能。
-后续开发应继续沿用增量迭代方式,优先完善数据库备份恢复等实用功能,再根据课程进度实现 AI 辅助 SQL 生成等扩展功能。
\ No newline at end of file
+后续开发可继续沿用增量迭代方式,重点进行功能测试、异常场景验证、界面细节优化和最终课程演示材料整理。
diff --git a/src/main/java/com/hypersql/ai/AiGenerationException.java b/src/main/java/com/hypersql/ai/AiGenerationException.java
new file mode 100644
index 0000000..8e9b253
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/AiGenerationException.java
@@ -0,0 +1,11 @@
+package com.hypersql.ai;
+
+public class AiGenerationException extends Exception {
+ public AiGenerationException(String message) {
+ super(message);
+ }
+
+ public AiGenerationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/AiProvider.java b/src/main/java/com/hypersql/ai/AiProvider.java
new file mode 100644
index 0000000..0cccf41
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/AiProvider.java
@@ -0,0 +1,17 @@
+package com.hypersql.ai;
+
+public enum AiProvider {
+ OPENAI_COMPATIBLE("OpenAI 兼容"),
+ ANTHROPIC("Anthropic Claude");
+
+ private final String displayName;
+
+ AiProvider(String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/AiSettings.java b/src/main/java/com/hypersql/ai/AiSettings.java
new file mode 100644
index 0000000..9897c3c
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/AiSettings.java
@@ -0,0 +1,29 @@
+package com.hypersql.ai;
+
+public record AiSettings(
+ AiProvider provider,
+ String apiKey,
+ String baseUrl,
+ String model,
+ int timeoutSeconds
+) {
+ public AiSettings {
+ if (provider == null) {
+ throw new IllegalArgumentException("请选择 AI Provider。");
+ }
+ apiKey = normalize(apiKey);
+ baseUrl = normalize(baseUrl);
+ model = normalize(model);
+ if (timeoutSeconds <= 0) {
+ throw new IllegalArgumentException("超时时间必须大于 0 秒。");
+ }
+ }
+
+ public boolean hasApiKey() {
+ return apiKey != null && !apiKey.isBlank();
+ }
+
+ private static String normalize(String value) {
+ return value == null ? "" : value.trim();
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/AiSqlGenerationService.java b/src/main/java/com/hypersql/ai/AiSqlGenerationService.java
new file mode 100644
index 0000000..225256b
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/AiSqlGenerationService.java
@@ -0,0 +1,73 @@
+package com.hypersql.ai;
+
+import com.hypersql.db.SchemaDescriptionService;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+public final class AiSqlGenerationService {
+ private final SchemaDescriptionService schemaDescriptionService = new SchemaDescriptionService();
+ private final LlmClient openAiClient = new OpenAiCompatibleClient();
+ private final LlmClient anthropicClient = new AnthropicMessagesClient();
+ private final GeneratedSqlSanitizer sanitizer = new GeneratedSqlSanitizer();
+
+ public String generateSql(Connection connection, AiSettings settings, String naturalLanguagePrompt) throws AiGenerationException {
+ validate(settings, naturalLanguagePrompt);
+ try {
+ String schemaDescription = schemaDescriptionService.describe(connection);
+ String rawSql = client(settings.provider()).generateSql(systemPrompt(), userPrompt(schemaDescription, naturalLanguagePrompt), settings);
+ return sanitizer.sanitize(rawSql);
+ } catch (SQLException e) {
+ throw new AiGenerationException("读取数据库结构失败。", e);
+ }
+ }
+
+ public boolean containsDestructiveKeyword(String sql) {
+ return sanitizer.containsDestructiveKeyword(sql);
+ }
+
+ private void validate(AiSettings settings, String naturalLanguagePrompt) throws AiGenerationException {
+ if (settings == null || !settings.hasApiKey()) {
+ throw new AiGenerationException("请先在 AI 设置中输入 API Key。");
+ }
+ if (settings.baseUrl().isBlank()) {
+ throw new AiGenerationException("请先设置 AI Base URL。");
+ }
+ if (settings.model().isBlank()) {
+ throw new AiGenerationException("请先设置 AI Model。");
+ }
+ if (naturalLanguagePrompt == null || naturalLanguagePrompt.isBlank()) {
+ throw new AiGenerationException("请输入自然语言需求。");
+ }
+ }
+
+ private LlmClient client(AiProvider provider) {
+ return switch (provider) {
+ case OPENAI_COMPATIBLE -> openAiClient;
+ case ANTHROPIC -> anthropicClient;
+ };
+ }
+
+ private String systemPrompt() {
+ return """
+ 你是 SQLite SQL 生成助手。
+ 只能根据提供的数据库结构生成 SQLite SQL。
+ 只返回一条 SQL 语句。
+ 不要使用 Markdown 代码块。
+ 不要解释。
+ 除非用户明确要求修改数据或结构,否则优先生成 SELECT 查询。
+ """;
+ }
+
+ private String userPrompt(String schemaDescription, String naturalLanguagePrompt) {
+ return """
+ 当前数据库结构如下:
+ %s
+
+ 用户需求:
+ %s
+
+ 请根据上述结构生成一条 SQLite SQL。生成结果会展示给用户检查,不会自动执行。
+ """.formatted(schemaDescription, naturalLanguagePrompt.trim());
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/AnthropicMessagesClient.java b/src/main/java/com/hypersql/ai/AnthropicMessagesClient.java
new file mode 100644
index 0000000..b305773
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/AnthropicMessagesClient.java
@@ -0,0 +1,96 @@
+package com.hypersql.ai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+public final class AnthropicMessagesClient implements LlmClient {
+ private static final int MAX_TOKENS = 1200;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException {
+ try {
+ String requestBody = buildRequestBody(systemPrompt, userPrompt, settings);
+ HttpRequest request = HttpRequest.newBuilder(endpoint(settings.baseUrl()))
+ .timeout(Duration.ofSeconds(settings.timeoutSeconds()))
+ .header("x-api-key", settings.apiKey())
+ .header("anthropic-version", "2023-06-01")
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ HttpResponse response = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(settings.timeoutSeconds()))
+ .build()
+ .send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
+ throw new AiGenerationException("AI 调用失败,HTTP 状态码:" + response.statusCode());
+ }
+ return parseResponse(response.body());
+ } catch (IOException e) {
+ throw new AiGenerationException("AI 返回内容解析失败。", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AiGenerationException("AI 调用已中断。", e);
+ } catch (IllegalArgumentException e) {
+ throw new AiGenerationException(e.getMessage(), e);
+ }
+ }
+
+ private String buildRequestBody(String systemPrompt, String userPrompt, AiSettings settings) throws IOException {
+ ObjectNode root = objectMapper.createObjectNode();
+ root.put("model", settings.model());
+ root.put("max_tokens", MAX_TOKENS);
+ root.put("system", systemPrompt);
+ ArrayNode messages = root.putArray("messages");
+ messages.addObject()
+ .put("role", "user")
+ .put("content", userPrompt);
+ return objectMapper.writeValueAsString(root);
+ }
+
+ private String parseResponse(String responseBody) throws IOException, AiGenerationException {
+ JsonNode root = objectMapper.readTree(responseBody);
+ JsonNode content = root.path("content");
+ StringBuilder text = new StringBuilder();
+ if (content.isArray()) {
+ for (JsonNode item : content) {
+ if ("text".equals(item.path("type").asText()) && item.path("text").isTextual()) {
+ text.append(item.path("text").asText()).append('\n');
+ }
+ }
+ }
+ String result = text.toString().trim();
+ if (result.isBlank()) {
+ throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
+ }
+ return result;
+ }
+
+ private URI endpoint(String baseUrl) {
+ String normalized = trimTrailingSlash(baseUrl);
+ if (normalized.endsWith("/v1")) {
+ normalized += "/messages";
+ } else if (!normalized.endsWith("/v1/messages")) {
+ normalized += "/v1/messages";
+ }
+ return URI.create(normalized);
+ }
+
+ private String trimTrailingSlash(String value) {
+ String result = value == null ? "" : value.trim();
+ while (result.endsWith("/")) {
+ result = result.substring(0, result.length() - 1);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/GeneratedSqlSanitizer.java b/src/main/java/com/hypersql/ai/GeneratedSqlSanitizer.java
new file mode 100644
index 0000000..3313ec2
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/GeneratedSqlSanitizer.java
@@ -0,0 +1,67 @@
+package com.hypersql.ai;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class GeneratedSqlSanitizer {
+ private static final Pattern FENCED_BLOCK = Pattern.compile("(?is)```(?:sql)?\\s*(.*?)\\s*```");
+ private static final Pattern SQL_START = Pattern.compile("(?is)\\b(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|REPLACE|PRAGMA)\\b");
+ private static final Pattern DESTRUCTIVE_KEYWORD = Pattern.compile("(?is)\\b(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|REPLACE)\\b");
+
+ public String sanitize(String rawContent) throws AiGenerationException {
+ if (rawContent == null || rawContent.isBlank()) {
+ throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
+ }
+
+ String text = extractCodeBlock(rawContent.trim()).trim();
+ Matcher startMatcher = SQL_START.matcher(text);
+ if (startMatcher.find()) {
+ text = text.substring(startMatcher.start()).trim();
+ }
+ text = firstStatement(text).trim();
+ if (text.isBlank()) {
+ throw new AiGenerationException("AI 生成的 SQL 为空。");
+ }
+ return text;
+ }
+
+ public boolean containsDestructiveKeyword(String sql) {
+ if (sql == null) {
+ return false;
+ }
+ return DESTRUCTIVE_KEYWORD.matcher(sql.toUpperCase(Locale.ROOT)).find();
+ }
+
+ private String extractCodeBlock(String text) {
+ Matcher matcher = FENCED_BLOCK.matcher(text);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return text;
+ }
+
+ private String firstStatement(String text) {
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ for (int i = 0; i < text.length(); i++) {
+ char current = text.charAt(i);
+ if (current == '\'' && !inDoubleQuote) {
+ if (inSingleQuote && i + 1 < text.length() && text.charAt(i + 1) == '\'') {
+ i++;
+ } else {
+ inSingleQuote = !inSingleQuote;
+ }
+ } else if (current == '"' && !inSingleQuote) {
+ if (inDoubleQuote && i + 1 < text.length() && text.charAt(i + 1) == '"') {
+ i++;
+ } else {
+ inDoubleQuote = !inDoubleQuote;
+ }
+ } else if (current == ';' && !inSingleQuote && !inDoubleQuote) {
+ return text.substring(0, i + 1);
+ }
+ }
+ return text;
+ }
+}
diff --git a/src/main/java/com/hypersql/ai/LlmClient.java b/src/main/java/com/hypersql/ai/LlmClient.java
new file mode 100644
index 0000000..3926a80
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/LlmClient.java
@@ -0,0 +1,5 @@
+package com.hypersql.ai;
+
+public interface LlmClient {
+ String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException;
+}
diff --git a/src/main/java/com/hypersql/ai/OpenAiCompatibleClient.java b/src/main/java/com/hypersql/ai/OpenAiCompatibleClient.java
new file mode 100644
index 0000000..2d3534b
--- /dev/null
+++ b/src/main/java/com/hypersql/ai/OpenAiCompatibleClient.java
@@ -0,0 +1,84 @@
+package com.hypersql.ai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+public final class OpenAiCompatibleClient implements LlmClient {
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException {
+ try {
+ String requestBody = buildRequestBody(systemPrompt, userPrompt, settings);
+ HttpRequest request = HttpRequest.newBuilder(endpoint(settings.baseUrl()))
+ .timeout(Duration.ofSeconds(settings.timeoutSeconds()))
+ .header("Authorization", "Bearer " + settings.apiKey())
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ HttpResponse response = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(settings.timeoutSeconds()))
+ .build()
+ .send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
+ throw new AiGenerationException("AI 调用失败,HTTP 状态码:" + response.statusCode());
+ }
+ return parseResponse(response.body());
+ } catch (IOException e) {
+ throw new AiGenerationException("AI 返回内容解析失败。", e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new AiGenerationException("AI 调用已中断。", e);
+ } catch (IllegalArgumentException e) {
+ throw new AiGenerationException(e.getMessage(), e);
+ }
+ }
+
+ private String buildRequestBody(String systemPrompt, String userPrompt, AiSettings settings) throws IOException {
+ ObjectNode root = objectMapper.createObjectNode();
+ root.put("model", settings.model());
+ ArrayNode messages = root.putArray("messages");
+ messages.addObject()
+ .put("role", "system")
+ .put("content", systemPrompt);
+ messages.addObject()
+ .put("role", "user")
+ .put("content", userPrompt);
+ return objectMapper.writeValueAsString(root);
+ }
+
+ private String parseResponse(String responseBody) throws IOException, AiGenerationException {
+ JsonNode root = objectMapper.readTree(responseBody);
+ JsonNode content = root.path("choices").path(0).path("message").path("content");
+ if (!content.isTextual() || content.asText().isBlank()) {
+ throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
+ }
+ return content.asText();
+ }
+
+ private URI endpoint(String baseUrl) {
+ String normalized = trimTrailingSlash(baseUrl);
+ if (!normalized.endsWith("/chat/completions")) {
+ normalized += "/chat/completions";
+ }
+ return URI.create(normalized);
+ }
+
+ private String trimTrailingSlash(String value) {
+ String result = value == null ? "" : value.trim();
+ while (result.endsWith("/")) {
+ result = result.substring(0, result.length() - 1);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/hypersql/controller/MainController.java b/src/main/java/com/hypersql/controller/MainController.java
index cd4e859..8864d7f 100644
--- a/src/main/java/com/hypersql/controller/MainController.java
+++ b/src/main/java/com/hypersql/controller/MainController.java
@@ -1,5 +1,9 @@
package com.hypersql.controller;
+import com.hypersql.ai.AiGenerationException;
+import com.hypersql.ai.AiProvider;
+import com.hypersql.ai.AiSettings;
+import com.hypersql.ai.AiSqlGenerationService;
import com.hypersql.db.DatabaseConnectionManager;
import com.hypersql.db.DatabaseMetadataService;
import com.hypersql.db.SqlExecutionService;
@@ -17,6 +21,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
+import javafx.concurrent.Task;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Scene;
@@ -26,6 +31,7 @@ import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
+import javafx.scene.control.PasswordField;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
@@ -43,8 +49,10 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.SQLException;
+import java.sql.Statement;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -57,6 +65,7 @@ import java.util.stream.Collectors;
public class MainController {
private static final int TABLE_PAGE_SIZE = 100;
private static final int SQL_PAGE_SIZE = 100;
+ private static final int DEFAULT_AI_TIMEOUT_SECONDS = 30;
private static final List ALLOWED_COLUMN_TYPES = List.of(
"INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC", "BOOLEAN", "DATE", "DATETIME"
);
@@ -134,12 +143,14 @@ public class MainController {
private final SqlExecutionService sqlExecutionService = new SqlExecutionService();
private final RowDataService rowDataService = new RowDataService();
private final SchemaEditService schemaEditService = new SchemaEditService();
+ private final AiSqlGenerationService aiSqlGenerationService = new AiSqlGenerationService();
private TableInfo currentTable;
private long currentTableTotalRows;
private long currentTablePageIndex;
private EditableQueryResult currentTableResult;
private QueryResult currentSqlResult;
private EditableQueryResult currentEditableSqlResult;
+ private AiSettings aiSettings;
private String currentSqlText;
private long currentSqlPageIndex;
private boolean selectingTableProgrammatically;
@@ -168,10 +179,7 @@ public class MainController {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("新建 SQLite 数据库");
fileChooser.setInitialFileName("new-database.db");
- fileChooser.getExtensionFilters().addAll(
- new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
- new FileChooser.ExtensionFilter("所有文件", "*.*")
- );
+ addSqliteExtensionFilters(fileChooser);
File selectedFile = fileChooser.showSaveDialog(currentWindow());
if (selectedFile == null) {
@@ -202,10 +210,7 @@ public class MainController {
private void handleOpenDatabase() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("打开 SQLite 数据库");
- fileChooser.getExtensionFilters().addAll(
- new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
- new FileChooser.ExtensionFilter("所有文件", "*.*")
- );
+ addSqliteExtensionFilters(fileChooser);
File selectedFile = fileChooser.showOpenDialog(currentWindow());
if (selectedFile == null) {
@@ -224,6 +229,102 @@ public class MainController {
}
}
+ @FXML
+ private void handleBackupCurrentDatabase() {
+ Optional currentDatabasePath = currentDatabasePathOrAlert();
+ if (currentDatabasePath.isEmpty()) {
+ return;
+ }
+
+ Path sourcePath = currentDatabasePath.get();
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("备份 SQLite 数据库");
+ fileChooser.setInitialFileName(sourcePath.getFileName() + ".backup.db");
+ addSqliteExtensionFilters(fileChooser);
+
+ File selectedFile = fileChooser.showSaveDialog(currentWindow());
+ if (selectedFile == null) {
+ status("已取消备份数据库");
+ return;
+ }
+
+ Path backupPath = selectedFile.toPath().toAbsolutePath().normalize();
+ try {
+ if (isSamePathOrFile(sourcePath, backupPath)) {
+ DialogUtils.showError("备份数据库失败", "备份目标不能是当前数据库文件本身。");
+ status("备份数据库失败");
+ return;
+ }
+ checkpointCurrentDatabase();
+ Files.copy(sourcePath, backupPath, StandardCopyOption.REPLACE_EXISTING);
+ DialogUtils.showInfo("备份数据库成功", "已备份数据库:\n" + sourcePath + "\n\n备份文件:\n" + backupPath);
+ status("已备份数据库");
+ } catch (IOException | SQLException e) {
+ DialogUtils.showError("备份数据库失败", e.getMessage());
+ status("备份数据库失败");
+ }
+ }
+
+ @FXML
+ private void handleRestoreCurrentDatabase() {
+ Optional currentDatabasePath = currentDatabasePathOrAlert();
+ if (currentDatabasePath.isEmpty()) {
+ return;
+ }
+
+ Path targetPath = currentDatabasePath.get();
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("选择数据库备份文件");
+ addSqliteExtensionFilters(fileChooser);
+
+ File selectedFile = fileChooser.showOpenDialog(currentWindow());
+ if (selectedFile == null) {
+ status("已取消还原数据库");
+ return;
+ }
+
+ Path backupPath = selectedFile.toPath().toAbsolutePath().normalize();
+ try {
+ if (!Files.isRegularFile(backupPath) || !Files.isReadable(backupPath)) {
+ DialogUtils.showError("还原数据库失败", "备份文件不存在或不可读:\n" + backupPath);
+ status("还原数据库失败");
+ return;
+ }
+ if (isSamePathOrFile(targetPath, backupPath)) {
+ DialogUtils.showError("还原数据库失败", "备份文件不能是当前数据库文件本身。");
+ status("还原数据库失败");
+ return;
+ }
+ } catch (IOException e) {
+ DialogUtils.showError("还原数据库失败", e.getMessage());
+ status("还原数据库失败");
+ return;
+ }
+
+ boolean confirmed = DialogUtils.confirm(
+ "确认还原数据库",
+ "将关闭当前连接,并用以下备份文件覆盖当前数据库:\n\n"
+ + backupPath
+ + "\n\n当前数据库:\n"
+ + targetPath
+ + "\n\n此操作不可撤销,是否继续?"
+ );
+ if (!confirmed) {
+ status("已取消还原数据库");
+ return;
+ }
+
+ connectionManager.close();
+ try {
+ restoreFromBackup(backupPath, targetPath);
+ reopenDatabaseAndRefresh(targetPath);
+ DialogUtils.showInfo("还原数据库成功", "已从备份还原数据库:\n" + backupPath + "\n\n当前数据库:\n" + targetPath);
+ status("已还原数据库");
+ } catch (IOException | SQLException e) {
+ handleRestoreFailure(targetPath, e);
+ }
+ }
+
@FXML
private void handleDeleteCurrentDatabase() {
Optional currentDatabasePath = connectionManager.getCurrentDatabasePath();
@@ -469,6 +570,50 @@ public class MainController {
}
}
+ @FXML
+ private void handleAiSettings() {
+ Optional settings = showAiSettingsDialog();
+ if (settings.isEmpty()) {
+ return;
+ }
+ aiSettings = settings.get();
+ status("AI 设置已更新");
+ }
+
+ @FXML
+ private void handleGenerateSql() {
+ if (!ensureConnected()) {
+ return;
+ }
+ if (aiSettings == null || !aiSettings.hasApiKey()) {
+ DialogUtils.showError("未设置 AI", "请先在“执行 > AI 设置”中输入 API Key。");
+ status("未设置 AI");
+ return;
+ }
+
+ Optional prompt = showAiPromptDialog();
+ if (prompt.isEmpty()) {
+ return;
+ }
+
+ AiSettings settingsSnapshot = aiSettings;
+ Connection connection = connection();
+ String naturalLanguagePrompt = prompt.get();
+ Task task = new Task<>() {
+ @Override
+ protected String call() throws AiGenerationException {
+ return aiSqlGenerationService.generateSql(connection, settingsSnapshot, naturalLanguagePrompt);
+ }
+ };
+ sqlMessageLabel.setText("AI 正在生成 SQL...");
+ status("AI 正在生成 SQL");
+ task.setOnSucceeded(_ -> applyGeneratedSql(task.getValue()));
+ task.setOnFailed(_ -> handleAiGenerationFailure(task.getException()));
+ Thread thread = new Thread(task, "hypersql-ai-sql-generation");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
@FXML
private void handleExecuteSql() {
if (!ensureConnected()) {
@@ -836,6 +981,139 @@ public class MainController {
}
}
+ private Optional showAiSettingsDialog() {
+ Dialog dialog = new Dialog<>();
+ dialog.setTitle("AI 设置");
+ dialog.initOwner(currentWindow());
+ dialog.getDialogPane().getButtonTypes().addAll(javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL);
+
+ ComboBox providerComboBox = new ComboBox<>(FXCollections.observableArrayList(AiProvider.values()));
+ PasswordField apiKeyField = new PasswordField();
+ TextField baseUrlField = new TextField();
+ TextField modelField = new TextField();
+ TextField timeoutField = new TextField(String.valueOf(DEFAULT_AI_TIMEOUT_SECONDS));
+
+ AiSettings currentSettings = aiSettings;
+ if (currentSettings == null) {
+ providerComboBox.getSelectionModel().select(AiProvider.OPENAI_COMPATIBLE);
+ baseUrlField.setText(defaultBaseUrl(AiProvider.OPENAI_COMPATIBLE));
+ modelField.setText("gpt-4o-mini");
+ } else {
+ providerComboBox.getSelectionModel().select(currentSettings.provider());
+ apiKeyField.setText(currentSettings.apiKey());
+ baseUrlField.setText(currentSettings.baseUrl());
+ modelField.setText(currentSettings.model());
+ timeoutField.setText(String.valueOf(currentSettings.timeoutSeconds()));
+ }
+
+ providerComboBox.valueProperty().addListener((_, _, provider) -> {
+ if (provider == null) {
+ return;
+ }
+ baseUrlField.setText(defaultBaseUrl(provider));
+ modelField.setText(defaultModel(provider));
+ });
+
+ GridPane gridPane = new GridPane();
+ gridPane.setHgap(8);
+ gridPane.setVgap(8);
+ gridPane.add(new Label("Provider"), 0, 0);
+ gridPane.add(providerComboBox, 1, 0);
+ gridPane.add(new Label("API Key"), 0, 1);
+ gridPane.add(apiKeyField, 1, 1);
+ gridPane.add(new Label("Base URL"), 0, 2);
+ gridPane.add(baseUrlField, 1, 2);
+ gridPane.add(new Label("Model"), 0, 3);
+ gridPane.add(modelField, 1, 3);
+ gridPane.add(new Label("超时秒数"), 0, 4);
+ gridPane.add(timeoutField, 1, 4);
+ dialog.getDialogPane().setContent(gridPane);
+
+ dialog.setResultConverter(buttonType -> {
+ if (!javafx.scene.control.ButtonType.OK.equals(buttonType)) {
+ return null;
+ }
+ try {
+ int timeoutSeconds = Integer.parseInt(timeoutField.getText().trim());
+ return new AiSettings(
+ providerComboBox.getValue(),
+ apiKeyField.getText(),
+ baseUrlField.getText(),
+ modelField.getText(),
+ timeoutSeconds
+ );
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("超时时间必须是数字。");
+ }
+ });
+ try {
+ return dialog.showAndWait();
+ } catch (IllegalArgumentException e) {
+ DialogUtils.showError("AI 设置无效", e.getMessage());
+ return Optional.empty();
+ }
+ }
+
+ private Optional showAiPromptDialog() {
+ Dialog dialog = new Dialog<>();
+ dialog.setTitle("AI 生成 SQL");
+ dialog.initOwner(currentWindow());
+ dialog.getDialogPane().getButtonTypes().addAll(javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL);
+
+ TextArea promptArea = new TextArea();
+ promptArea.setPromptText("例如:查询每个用户的订单数量");
+ promptArea.setPrefRowCount(6);
+ promptArea.setWrapText(true);
+ dialog.getDialogPane().setContent(promptArea);
+ dialog.setResultConverter(buttonType -> {
+ if (!javafx.scene.control.ButtonType.OK.equals(buttonType)) {
+ return null;
+ }
+ return promptArea.getText() == null ? "" : promptArea.getText().trim();
+ });
+ Optional result = dialog.showAndWait();
+ if (result.isPresent() && result.get().isBlank()) {
+ DialogUtils.showError("需求不能为空", "请输入希望 AI 生成的 SQL 需求。");
+ return Optional.empty();
+ }
+ return result;
+ }
+
+ private void applyGeneratedSql(String generatedSql) {
+ sqlTextArea.setText(generatedSql);
+ mainTabPane.getSelectionModel().select(2);
+ String message = "AI 已生成 SQL,请检查后再手动执行。";
+ if (aiSqlGenerationService.containsDestructiveKeyword(generatedSql)) {
+ message += " 检测到可能修改数据或结构的关键词,请特别谨慎。";
+ }
+ sqlMessageLabel.setText(message);
+ status("AI 已生成 SQL");
+ }
+
+ private void handleAiGenerationFailure(Throwable throwable) {
+ String message = throwable == null ? "AI 生成 SQL 失败。" : throwable.getMessage();
+ if (message == null || message.isBlank()) {
+ message = "AI 生成 SQL 失败。";
+ }
+ sqlMessageLabel.setText(message);
+ DialogUtils.showError("AI 生成 SQL 失败", message);
+ status("AI 生成 SQL 失败");
+ }
+
+ private String defaultBaseUrl(AiProvider provider) {
+ return switch (provider) {
+ case OPENAI_COMPATIBLE -> "https://api.openai.com/v1";
+ case ANTHROPIC -> "https://api.anthropic.com";
+ };
+ }
+
+ private String defaultModel(AiProvider provider) {
+ return switch (provider) {
+ case OPENAI_COMPATIBLE -> "gpt-4o-mini";
+ case ANTHROPIC -> "claude-opus-4-7";
+ };
+ }
+
private Optional