Compare commits

...

956 Commits

Author SHA1 Message Date
Syngnat
fdfdebf24e feat(sidebar): 表新建查询展开全部字段而非 select *
- 新建查询时通过 DBGetColumns 拉取字段并生成 SELECT 列表
- 元数据不可用时回退 SELECT *,消息队列仍保持 LIMIT 预览
- 覆盖 V2/旧版右键菜单与表总览入口,补充模板单测

Refs #710
2026-07-25 09:25:04 +08:00
Syngnat
365c72ad22 🐛 fix(tabs/theme): 修复标签顶条与标题栏主题不一致 (#715)
## 背景

V2 工作区的活动 Tab
顶部仍显示一条绿色强调线,与当前圆角标签样式不协调。同时,自定义标题栏使用独立的固定背景,切换内置或自定义主题后无法与侧栏表面颜色保持一致。

## 变更点

- 移除 V2 主工作区活动 Tab 顶部的 inset 强调条
- 保留活动 Tab 的选中背景、8px 圆角及原有交互状态
- V2 自定义标题栏改为复用侧栏的 `--gn-bg-panel-2` 主题表面
- 补充活动 Tab 和标题栏主题来源的回归断言

## 影响范围

- 仅影响 V2 主工作区 Tab 和应用自定义标题栏的视觉表现
- legacy 标题栏继续使用原有 `bgMain`,透明度与模糊逻辑不变
- Modal、Drawer 等其他 Ant Tabs 的强调条不受影响
- 不修改标签尺寸、拖拽、关闭、窗口控制或主题配置逻辑
- 无后端、数据库、依赖或配置变更

## 验证

- `npm test -- --run src/components/TabManager.adaptive-width.test.ts
src/App.sidebar-collapse.test.ts`:12/12 通过
- `npx tsc --noEmit --pretty false`:通过
- Playwright 运行时验证:标题栏与侧栏背景一致,主题 token 更新后同步变化
- Playwright 活动 Tab 样式验证:`box-shadow: none`、顶部伪元素隐藏、圆角保持 8px
- `git diff --check origin/dev...HEAD`:通过
2026-07-24 22:44:44 +08:00
Syngnat
17a3ea91a1 🐛 fix(ci): 加固 driver-agent revision 校验避免 gaussdb 空值失败
- metadata 探测增加重试、stdout/stderr 诊断与 UPX 解压回退
- UPX 冒烟失败恢复未压缩产物后仍做 metadata 自检
- gaussdb revision 指纹补齐 postgres_impl 依赖
2026-07-24 22:43:57 +08:00
Syngnat
4c0f0a8fdd 🔧 fix(ui): 清理控制台 Form 与 Modal 弃用警告
- 关闭消息发送弹窗时不再对已卸载 Form 调用 resetFields
- 缩放修复路径中 ResetWebViewZoom 不可用时最多告警一次
- 将 Drawer/Modal 的 destroyOnClose 统一迁移为 destroyOnHidden
2026-07-24 22:31:33 +08:00
AutumnNazi
a2dc8db513 🐛 fix(tabs/theme): 修复标签顶条与标题栏主题不一致
- 移除 V2 工作区活动标签顶部强调条,保留圆角与选中背景
- 让 V2 自定义标题栏跟随侧栏主题表面,同时保持 legacy 背景逻辑
- 补充标签与标题栏主题回归断言
2026-07-24 21:49:19 +08:00
Syngnat
4400fd67c0 🔧 fix(sidebar): 修复 resize 测试类型断言导致镜像构建失败
- 将 FakeHTMLElement 断言改为 unknown 中转,消除 tsc TS2352
- 恢复 image CI 中 npm run build 可通过
2026-07-24 21:42:14 +08:00
Syngnat
2a7ef109fd 🐛 fix(ssh): 修复非 id_rsa 私钥无法浏览选择与读取
- 修正 OpenFileDialog 默认目录,支持无扩展名密钥路径
- macOS 下取消错误文件名过滤,显示隐藏项以选择 ~/.ssh 密钥
- 私钥读/解析失败返回明确错误,示例改为 id_ed25519
- 补充选钥目录解析与私钥读取失败回归测试

Refs #711
2026-07-24 21:31:21 +08:00
Syngnat
e6f39d34d3 Merge remote-tracking branch 'origin/dev' into dev 2026-07-24 21:30:30 +08:00
Syngnat
ce9fdbebc7 🐛 fix(about): 移除关于页非必要滚动条 (#714)
## 背景

Issue #702 中,安装版在“已是最新版本”且页面内容可以完整展示时,关于页右侧仍会出现滚动条。原因是内容根节点额外的 18px
底部留白造成了临界溢出。

## 变更点

- 移除关于页内容根节点冗余的底部留白
- 保留详情容器原有的 `overflowY: auto`,仅在内容真实溢出时显示滚动条
- 补充设置中心布局回归断言,防止非必要底部留白再次引入

## 影响范围

- 仅影响设置中心“关于 GoNavi”页面的垂直布局
- 已是最新版本且内容可完整展示时不再出现滚动条
- 有可用更新并显示“本次更新包”等额外信息时,内容溢出仍可正常滚动
- 不修改更新检查、下载、安装或自动检查间隔逻辑
- 无后端、数据库、依赖或配置变更

## 验证

- `npm test -- --run App.settings-center.test.ts`:22/22 通过
- `npx tsc --noEmit --pretty false`:通过
- Playwright 运行时验证:最新安装版内容 591px / 容器 599px,无溢出;有更新时内容 624px / 容器
599px,保留 25px 可滚动区域
- `git diff --check origin/dev...HEAD`:通过

Closes #702
2026-07-24 21:29:46 +08:00
Syngnat
2f427d3baf 🐛 fix(tabs/query-editor): 优化标签圆角并修复搜索提示遮挡 (#713)
## 背景

Issue #695 希望统一右侧工作区标签的圆角样式。同时,查询页打开 Monaco
搜索后,顶部按钮的悬浮提示会被编辑器区域裁剪,影响操作提示阅读。

## 变更点

- 将 V2 工作区标签四角统一为 8px 圆角
- 使用尊重圆角边界的 inset 激活色,避免顶部绿色条遮住圆角或形成悬浮短条
- Monaco 查找组件可见时放开编辑器区域的浮层裁剪,保持搜索按钮及查找框位置不变
- 补充标签圆角和搜索提示显示的回归测试

## 影响范围

- 仅影响 V2 主工作区标签样式及查询编辑器查找浮层
- 不改变标签尺寸、间距、拖拽和关闭逻辑
- 不改变 Monaco 查找框位置,也不恢复顶部额外留白
- 无后端、数据库、依赖或配置变更

## 验证

- `npx vitest run src/components/TabManager.adaptive-width.test.ts`(7/7
通过)
- `npx vitest run src/components/QueryEditor.external-sql-save.test.tsx
-t "shows Monaco find button hovers without moving the find widget"`(1/1
通过)
- `npx tsc --noEmit`
- `npm run build`
- Playwright 实际页面验证标签圆角、激活色和 Monaco 搜索按钮 Tooltip

Closes #695
2026-07-24 21:29:42 +08:00
AutumnNazi
a6c83d9164 🐛 fix(about): 移除关于页非必要滚动条
- 移除关于页冗余底部留白,内容完整时不再触发滚动
- 保留内容溢出时的滚动能力并补充布局回归断言
2026-07-24 20:53:19 +08:00
AutumnNazi
82a0a02c56 🐛 fix(tabs/query-editor): 优化标签圆角并修复搜索提示遮挡
- 将 V2 工作区标签统一为四角圆角,并让激活色沿圆角自然显示
- 放开 Monaco 查找组件可见时的浮层裁剪,不改变按钮位置和顶部间距
- 补充标签圆角与查找提示显示的回归测试
2026-07-24 20:23:46 +08:00
Syngnat
cbca312a50 🐛 fix(sidebar): 将表置顶星标改为只读状态指示
- 仅为已置顶表展示非交互实心星标
- 保留右键菜单中的置顶与取消置顶入口
- 补充侧边栏置顶指示器回归测试
2026-07-24 20:20:23 +08:00
Syngnat
0c9e807674 ️ perf(sidebar): 消除左侧树拖拽改宽时的卡顿
- 拖拽与松手提交期间标记 data-sidebar-resizing,关闭 Sider 默认 transition
- 避免 Ant Design transition:all 带动工作台/DataGrid 连续 reflow
- 补充 resize 标记生命周期与 CSS 约束回归测试
2026-07-24 19:50:58 +08:00
Syngnat
e1bc65b788 feat(connection): 支持分组快捷新建与环境预设 (#712)
## 背景

Issue #698
希望在连接分组的右键菜单中直接新建连接,减少先创建再手动移动分组的操作。后续需求还需要为连接和分组增加环境预设,使生产、测试、开发和本地环境在侧栏与工作台中可辨识。

## 变更点

- 在 V2 与旧版侧栏的分组右键菜单中增加“新建连接”,保存成功后自动归入触发操作的分组
- 为连接和分组增加生产、测试、开发、本地四种环境预设,默认使用本地环境
- 在分组图标和连接相关 Tab 底部展示对应环境颜色,并由直接所属分组优先决定展示环境
- 将连接环境类型持久化到 connections.json,并支持连接包导入导出;旧数据或非法值兼容回落为本地环境
- 补充多语言文案、Wails 类型绑定和前后端回归测试

## 影响范围

- 影响新旧侧栏分组菜单、连接编辑弹窗、连接分组弹窗和连接相关 Tab 的环境标识
- 分组预设只覆盖其直属连接的展示环境,不会批量改写连接自身保存的预设类型
- environmentType 为可选兼容字段,不需要数据库迁移;旧连接和旧连接包可继续加载
- 未新增依赖,未修改构建链路;回滚时可直接回退本提交

## 验证

- npm run build
- npx tsc --noEmit
- 相关 Vitest:13 项通过(分组新建连接、连接/分组环境、Tab 环境标识、Store 持久化)
- Go 定向测试通过(连接持久化、旧数据兼容、连接包导入导出)
- go vet ./internal/app ./internal/connection
- 6 个 i18n JSON 文件解析通过,npm run i18n:scan 正常完成
- 完整组合测试中仍有 origin/dev 已存在的 3 条 TabManager 源码字符串断言失败及 1 条无关 Store
超时;本次新增用例均已单独通过

Closes #698
2026-07-24 19:16:30 +08:00
AutumnNazi
66531b6abd feat(connection): 支持分组快捷新建与环境预设
- 在新旧侧栏分组右键菜单中增加新建连接,并在保存后自动归入目标分组
- 为连接与分组增加生产、测试、开发和本地环境预设及颜色标识
- 持久化连接环境元数据,兼容旧数据与连接包导入导出
- 补充多语言文案及前后端回归测试
2026-07-24 18:31:40 +08:00
Syngnat
b7636f5fdd Merge remote-tracking branch 'origin/dev' into dev 2026-07-24 18:13:22 +08:00
Syngnat
10db415d7a 🐛 fix(datagrid): 修复筛选字段选择失效
- 将 Select 下拉选项识别为交互目标
- 避免页内查找焦点逻辑中断字段选择
- 补充下拉选项交互目标回归测试
2026-07-24 18:12:47 +08:00
Syngnat
b40214e65c 🐛 fix(kingbase): 修复无模式前缀查询失败
- 缓存连接初始化时探测出的 search_path
- 新建固定会话后先同步 schema 搜索路径
- 补充金仓会话初始化顺序回归测试
2026-07-24 18:12:25 +08:00
Syngnat
55b24b2d02 🐛 fix(table-designer): fix table sorting and column options (#705) (#709)
## Summary

- sort table names in natural numeric order in both the table overview
and sidebar
- preserve MySQL column default values, including explicit empty-string
defaults and supported expressions
- add per-column default, character set, collation, and comment editing
- generate complete MySQL column definitions while filtering
`DEFAULT_GENERATED`
- keep column-level character set behavior scoped to MySQL

## Testing

- `npm --prefix frontend test --
src/components/TableDesignerSqlPreview.test.tsx
src/utils/tableOverviewFilter.test.ts src/utils/columnDefinition.test.ts
src/components/tableDesignerSchemaSql.test.ts` (58 passed)
- sidebar natural-sort focused test (passed)
- `npm --prefix frontend run build` (passed; existing chunk-size warning
only)
- `go test ./internal/db ./internal/connection` (passed)
- i18n JSON validation, Go formatting check, and `git show --check`
(passed)

## Validation notes

- The full sidebar test file still has 5 unrelated source/CSS sentinel
failures. The same 5 failures reproduce against the pre-sidebar-change
baseline.
- Repository-wide frontend and Go suites contain existing
environment/baseline failures unrelated to this change.
- Docker was unavailable, so a disposable MySQL round-trip test could
not be run.
- The development page was reachable, but Playwright is not installed in
the repository, so automated UI interaction screenshots were not
produced.

## Known limitation

MySQL `SHOW FULL COLUMNS` cannot reliably distinguish no `DEFAULT`
clause from an explicit `DEFAULT NULL`. This change preserves non-null
defaults and explicit empty-string defaults without introducing a full
`SHOW CREATE TABLE` parser.

Closes #705
2026-07-24 18:03:33 +08:00
mango
2a7fcb4781 🐛 fix(table-designer): fix table sorting and column options (#705) 2026-07-24 17:55:22 +08:00
Syngnat
7398873435 Merge pull request #708 from virgosgod-maker/fix/data-viewer-refresh-total 2026-07-24 17:36:57 +08:00
Syngnat
016f0197eb 🔁 sync(pr): 合并 PR #704 最新来源分支 2026-07-24 17:22:37 +08:00
Syngnat
ef625ec911 🐛 fix(datagrid): 修复空结果横向滚动条位置异常
- 实测表格 DOM 溢出与轨道宽度,轻微溢出时仍将滚动条固定到底部
- 空结果时关闭数据预览面板并清理聚焦状态,避免残留布局占位
- 扩展性能复现页支持零行数据并补充布局与预览面板回归测试
2026-07-24 17:13:39 +08:00
mango
43c6f5e391 🐛 fix(data-viewer): 刷新表数据时重新统计总数
Fixes Syngnat/GoNavi#707
2026-07-24 16:19:15 +08:00
virgosgod-maker
2b2cc231af Merge branch 'Syngnat:dev' into dev 2026-07-24 15:50:08 +08:00
Syngnat
72432b639e Merge pull request #704 from virgosgod-maker/dev
 feat(query-editor): 美化 SQL 支持仅格式化选中内容
2026-07-24 15:22:10 +08:00
Syngnat
d520c9b806 ️ perf(sidebar): 优化大型资源树折叠动画
- 延迟隐藏资源树内容,使侧栏宽度收起动画保持连续
- 稳定 Sidebar 属性并保留已加载节点,避免大量对象触发重复渲染
- 覆盖 V2、旧版与减少动态效果场景,补充折叠回归测试
- 关联问题:#697
2026-07-24 15:16:15 +08:00
mango
2307a539a8 feat(query-editor): 美化 SQL 支持仅格式化选中内容
有非空选区时只美化选中 SQL,无选区时仍美化全文。
2026-07-24 14:56:31 +08:00
Syngnat
1e4ece74a7 feat(sidebar): 优化窄宽度连接头部展示
- 移除连接头部状态徽章,释放连接名与库名显示空间
- 窄宽度时收起新建查询文字并保持操作按钮可用
- 连接名或库名截断时悬浮同时展示完整信息
- 补充响应式布局、主题对比度与悬浮提示回归测试
2026-07-24 14:46:24 +08:00
Syngnat
3bb0866633 🐛 fix(driver-agent): 修复 Windows 测试连接释放失败
- 优先等待驱动代理自然退出,超时后再执行强制终止
- 进程已退出时忽略 Windows 终止权限竞态,避免连接成功被误判为失败
- 统一可选驱动与 MySQL agent 清理流程并补充回归测试
2026-07-24 14:27:09 +08:00
Syngnat
c76daa0727 🐛 fix(update): 修复更新完成提示被弹窗遮挡
- 新增静态反馈专用层级,确保消息提示高于嵌套弹窗和退出确认框
- 调整 Ant Design 静态反馈的全局层级配置,保持普通弹出层顺序不变
- 补充反馈层级顺序与全局配置回归测试
2026-07-24 14:15:29 +08:00
Syngnat
e8eb6366c3 feat(tabs): 默认启用双行标签展示
- 主行默认仅展示对象名,副行依次展示对象类型、连接名和数据库
- 调整设置项顺序并默认关闭 Schema 与 Host/IP
- 升级持久化版本并仅迁移未自定义的旧默认配置
- 补充默认快照、标签文案和配置迁移回归测试
2026-07-24 14:00:49 +08:00
Syngnat
340e7837a3 feat(tabs): 支持工作区标签宽度自适应
- 根据工作区可用宽度和标签数量动态分配标签宽度
- 使用 ResizeObserver 响应布局变化并限制可读宽度范围
- 通过 CSS 变量同步标签宽度并补充边界回归测试
2026-07-24 14:00:18 +08:00
Syngnat
aee4da47db 🐛 fix(sidebar): 调整折叠状态下的展开按钮位置
- 将 V2 侧栏展开按钮从标题栏移至固定工具栏顶部
- 保持展开与折叠后的焦点回迁和无障碍属性
- 补充固定栏按钮顺序、交互和样式回归测试
2026-07-24 13:59:43 +08:00
Syngnat
300eb3d48e Merge pull request #699 from AutumnNazi/fix/redis-key-detail-actions-layout 2026-07-24 13:26:14 +08:00
Syngnat
090e51c0ef Merge pull request #701 from AutumnNazi/feature/sidebar-recent-query-actions 2026-07-24 13:05:51 +08:00
AutumnNazi
0d27d2deaa feat(sidebar): 支持删除与清空近期查询
- 为每条近期查询增加删除入口,并在标题区提供清空操作
- 通过持久化可见性标记隐藏近期记录,保留完整 SQL 执行日志
- 补齐六语言文案及交互、订阅和状态持久化测试
2026-07-24 12:35:56 +08:00
Syngnat
9275e93421 🐛 fix(query-editor): 修复 Oracle 长脚本执行卡死
- 线性屏蔽 SQL 注释和字符串,避免 PL/SQL 定义检测发生正则灾难性回溯
- 仅为顶层 SELECT 解析结果表定位,避免匿名块内部查询触发元数据加载
- 新增长注释匿名块与多结果回归,验证 Oracle 过程和 SQLPlus 分隔符兼容性
2026-07-24 12:22:37 +08:00
Syngnat
e43fc6e1a1 🐛 fix(ci): 修复 Windows 更新 MSI 测试夹具
- 为多实例确认和单实例直启用例显式固定当前 MSI 安装模式
- 在测试清理阶段恢复安装模式解析器,避免全局注入污染后续用例
- 避免临时安装目录被误判为 Portable 后提前触发包兼容性失败
2026-07-24 12:00:40 +08:00
Syngnat
562afd1406 Merge remote-tracking branch 'origin/dev' into dev 2026-07-24 11:43:34 +08:00
Syngnat
d3909c7dff feat(saved-query): 支持独立 SQL 文件与自定义存储目录
- 将已存查询内容从 JSON 拆分为独立 .sql 文件,并采用可读名称和数字后缀处理冲突
- 自动迁移旧版内联 SQL 与摘要文件名,保留外部编辑并在失败时回滚
- 支持选择、迁移、打开及恢复默认存储目录,补充数据根切换串行保护
- 新增已存查询右键在文件夹中打开,并同步处理查询重命名后的文件路径
- 补齐 Wails、浏览器模拟、六语言文案及后端和前端回归测试
2026-07-24 11:29:04 +08:00
github-actions[bot]
1be0fac702 chore(docs): refresh star history chart [skip ci] 2026-07-24 03:27:30 +00:00
github-actions[bot]
5e5cbe3577 chore(docs): refresh star history chart [skip ci] 2026-07-24 03:20:01 +00:00
AutumnNazi
427d22d9e7 🎨 style(redis): 优化 Key 详情操作按钮布局
- 将 Key 名称、类型与 TTL/长度、操作按钮拆分为独立行
- 移除操作按钮组外层灰色容器并保留窄宽度换行
- 增加 Key 详情布局交互回归测试
2026-07-24 11:19:17 +08:00
Syngnat
4bc131264b Merge remote-tracking branch 'origin/dev' into dev 2026-07-24 10:50:21 +08:00
github-actions[bot]
ee62171d84 chore(docs): refresh star history chart [skip ci] 2026-07-24 02:47:15 +00:00
Syngnat
bbcafe63f4 🐛 fix(update): 修复 Windows 在线更新多实例确认流程
- 安装前获取维护锁并枚举同目录进程,仅检测到其他实例时触发关闭确认
- 改用 Windows 原生系统模态对话框并先收起下载进度窗口,避免确认提示被遮挡
- 单实例直接进入安装,取消确认或退出检查中止后恢复下载完成状态
- 多语言确认文案展示实际实例数量,补齐前后端与发布工作流回归测试
2026-07-24 10:19:09 +08:00
Syngnat
20c322627e 🔧 chore(ci): Star History 仅在 star 增长≥50 时提交
- 增加 last-star-count 基线文件与增长阈值闸门
- 未达阈值时跳过图表渲染与 commit,避免频繁刷历史
- workflow_dispatch 仍支持强制刷新
2026-07-24 10:08:52 +08:00
github-actions[bot]
9a80ae3a7b chore(docs): refresh star history chart [skip ci] 2026-07-24 01:11:49 +00:00
github-actions[bot]
b5585631ce chore(docs): refresh star history chart [skip ci] 2026-07-24 00:55:04 +00:00
github-actions[bot]
43cf2b4a15 chore(docs): refresh star history chart [skip ci] 2026-07-24 00:38:59 +00:00
github-actions[bot]
74c9002c87 chore(docs): refresh star history chart [skip ci] 2026-07-24 00:09:39 +00:00
github-actions[bot]
2e4c257fdc chore(docs): refresh star history chart [skip ci] 2026-07-23 17:05:51 +00:00
Syngnat
7579bc6eab feat(datagrid): 精简结果工具栏并支持快捷页内查找
- 将结果视图操作改为图标按钮并补充悬浮提示
- 移除冗余状态信息并支持 Ctrl/Cmd+F 打开当前页查找
- 完善查找导航、关闭和自适应布局,避免抢占 SQL 编辑器快捷键
2026-07-23 23:13:56 +08:00
Syngnat
86facaaf8c 📝 docs(readme): 添加华龙赞助致谢并接入 CI 刷新 Star History
- 中英文 README 新增华龙中转站赞助商板块与共建额度说明(链至 #671)
- 收录赞助商 logo 资源 assets/sponsors
- 使用 star-history-action + GITHUB_TOKEN 定时/点 Star 刷新增长曲线
- 补充本地生成脚本 tools/generate_star_history_svg.py 作为备用
2026-07-23 21:59:56 +08:00
Syngnat
b595c5fdfc 🔁 sync(datagrid): 合并尾页按钮布局调整 (#686)
- 将尾页按钮图标移动到文字右侧,统一分页操作阅读顺序
- 保留 #685 的独立合并记录与两个 PR 的原始提交关系
- 限定合并结果仅修改分页按钮组件
2026-07-23 17:16:52 +08:00
Syngnat
74c2fe6039 🔁 sync(datagrid): 合并跳页输入框居中修复 (#685)
- 调整跳页数字输入框内部布局与行高,保持内容垂直居中
- 排除锁文件、错误 package 校验值与生成绑定空白噪音
- 保留当前分支已完成的侧栏样式与绑定文件内容
2026-07-23 17:16:21 +08:00
Syngnat
61f01b78d0 feat(sidebar): 支持仅折叠资源树并保留固定栏
- 折叠 V2 侧栏时仅隐藏资源树,按界面缩放保留固定操作栏
- 将 SQL 分析与 SQL 审计入口迁移至固定栏并优化小窗口滚动布局
- 增加标题栏展开入口与双向焦点回切,补充侧栏回归测试
2026-07-23 17:12:19 +08:00
Syngnat
a08520f4f3 🔧 chore(frontend): 同步 package 校验文件 2026-07-23 16:41:36 +08:00
Syngnat
d618ef381e feat(data-transfer): 完善数据库 SQL 备份与恢复
- 数据导入工作台新增数据库模式,支持 SQL 文件选择、目标库、进度、取消与重试
- 新增受连接保护约束的 ImportDatabaseSQL 接口并复用流式执行链路
- SQL 导出支持切换数据库上下文,兼顾原库备份和跨库导入
- 调整侧栏与表概览入口,备份先进入工作台确认配置再执行
- 同步 Wails 绑定、Web 运行时限制、六语文案和回归测试
2026-07-23 16:40:44 +08:00
Syngnat
1f17b5cfad 🎨 style(settings/ai): 统一设置中心与 AI 配置交互
- 移除设置导航、详情页和 AI 配置列表的冗余分隔线
- 梳理外观字号项顺序并精简日志目录重复标题
- 恢复安全与上下文选项语义色,统一供应商操作按钮
- 强化 MCP 页签、客户端选中态和折叠交互
- 拆分推荐流程与内置工具视图并补齐六语回归测试
2026-07-23 16:36:21 +08:00
Syngnat
d8c4352b66 🐛 fix(query-editor/data-grid): 修复查询结果预览与保存状态
- 修正保存队列、草稿同步及保存结果校验的生命周期位置
- 查询完成后通过本地请求切回表格数据预览
- 支持 SQLite、ClickHouse、MongoDB 及空结果集重复执行
- 避免共享 DDL 展示状态覆盖新查询结果
- 补充数据预览、DDL 状态恢复和保存状态回归测试
2026-07-23 16:34:09 +08:00
Syngnat
ba28c626fd 🐛 fix(query-editor): 修正 SQL 保存代码错位
- 恢复保存队列与组件挂载状态的声明位置
- 修正草稿同步、保存完成判断和结果关闭逻辑结构
- 恢复保存查询回归用例并补齐异步等待
2026-07-23 16:31:41 +08:00
Syngnat
3c24068190 🎨 style(data-grid/query-editor): 统一工具栏图标交互
- 将 V2 数据查看、查询结果和 SQL 编辑器操作改为纯图标按钮
- 统一工具栏操作尺寸,并保留本地化 Tooltip 与无障碍标签
- 使用状态图标精简 SQL 提交模式和提交时机选择器
- 修复事务下拉选项截断及悬浮提示与菜单重叠问题
- 补充图标布局、下拉交互和旧版兼容测试
2026-07-23 16:17:56 +08:00
Syngnat
84d6007be9 ️ perf(driver/release): 优化驱动镜像下载与发布链路
- 将各平台驱动拆分为独立 ZIP,并为 DuckDB 同包携带运行库
- 仅生成 ZIP 下载候选,按 Gatewaysentry 与 GitHub 短测速结果调整优先级
- 扩展发布索引的归档摘要与条目元数据,强化跨平台资产校验
- 禁止裸驱动与 CI 总包进入 VPS 镜像,并校验载荷摘要和索引一致性
- 显式控制驱动镜像发布,自动清理失败暂存、过期目录和旧版本
2026-07-23 16:10:08 +08:00
Syngnat
dd9a56a70d 🐛 fix(query-editor): 修复 SQL 保存状态与退出检查竞态
- 使用最新编辑器内容维护保存查询草稿基线
- 等待保存查询加载并在退出前刷新恢复状态
- 保留保存期间新增编辑,避免旧请求覆盖最新 SQL
- 串行处理重复保存并阻止卸载后的过期回写
- 增加保存、重启恢复和退出检查竞态测试
2026-07-23 15:29:23 +08:00
Syngnat
830403b04b 🐛 fix(update): 避免旧镜像遮蔽开发版更新 2026-07-23 14:45:31 +08:00
Syngnat
1dcc866846 🐛 fix(update): 修复重启更新时退出确认弹窗被遮挡
- 新增应用退出确认专用层级并动态高于当前设置子弹窗
- 未保存 SQL 与关闭其他实例确认框统一使用前景层级
- 补充更新退出链路与弹层层级回归测试
2026-07-23 14:44:09 +08:00
Syngnat
95992d739a 🎨 style(sql-audit): 调整审计筛选操作按钮布局
- 将日期范围固定在筛选栏左侧
- 将刷新与重置筛选操作组靠右对齐
- 完善响应式布局并补充样式回归测试
2026-07-23 14:27:01 +08:00
Syngnat
e1e65f9698 🐛 fix(release): 修复镜像容量检查参数冲突
- 使用 POSIX df 输出读取 Gatewaysentry 可用空间
- 校验远端容量结果后再换算为字节
- 增加发布 action 容量命令回归测试
2026-07-23 13:47:38 +08:00
Syngnat
83952abad4 🔧 chore(release): 将下载镜像迁移至 Gatewaysentry
- 将 stable 与 dev 的应用和驱动镜像改为 SSH/rsync 发布到 Gatewaysentry
- 新增发布载荷校验、原子切换、版本清理及 Nginx 部署工具
- Windows 发布保留原始 EXE 并增加普通 Portable ZIP,移除 Windows UPX
- 删除 Cloudflare R2 上传保留脚本并统一镜像探测命名
2026-07-23 13:08:26 +08:00
Syngnat
3ac390f0dc 🐛 fix(update): 支持 Windows 便携版 ZIP 自动更新
- 将便携版更新资产切换为普通 ZIP,并兼容复用已下载包
- 解压后映射版本化目标文件名,保留旧版 EXE 更新兼容
- 将内嵌 PowerShell 更新脚本策略收紧为 RemoteSigned
- 补充 ZIP、目标路径与启动参数回归测试
2026-07-23 13:07:03 +08:00
Syngnat
57108d5b71 feat(sidebar): 支持左侧树整体折叠与展开
- 新增侧栏整体折叠状态并保留原有展开宽度
- 将 V2 折叠按钮固定到资源树标题栏,折叠后从窗口标题栏展开
- 联动搜索入口、AI 面板和日志调整线布局
- 补充多语言文案、按钮边框样式和回归测试
2026-07-23 12:27:55 +08:00
Syngnat
97cffa7809 feat(settings): 支持自定义并持久化日志目录
- 将日志目录配置整合到数据目录设置,支持选择、打开、恢复默认与保存
- 持久化日志目录并保留环境变量优先级,目录调整后重启生效
- 通过跨进程锁和原子写入保护数据目录与日志目录配置
- 移除重复标题并互斥目录修改操作,补齐六语言文案
- 补充桌面接口、浏览器模拟及并发回归测试
2026-07-23 11:40:17 +08:00
Syngnat
5a5fbf04fe feat(settings): 拆分 SQL 编辑器与查询结果字号
- 新增 SQL 编辑器独立字号与跟随全局配置
- 将查询编辑器和 DDL 预览接入 SQL 专用排版
- 迁移旧版耦合字号设置并保留原有视觉大小
- 更新六语种文案并补充持久化与组件测试
2026-07-23 11:24:44 +08:00
Syngnat
175c7a908c 🎨 style(titlebar): 增大 Windows 窗口按钮间距
- 将 Windows 自绘窗口控制按钮间距从 4px 调整为 8px
- 保持按钮尺寸、圆角、交互及其他平台样式不变
- 更新间距回归测试并完成浏览器几何验证
2026-07-23 11:19:55 +08:00
Syngnat
516e929169 🐛 fix(about): 移除关于页品牌图标白底
- 恢复 10 套关于页专用透明品牌资源
- 保持选择器、Dock 与应用图标继续使用方形 WebP
- 补充资源透明像素和默认回退逻辑回归测试
2026-07-23 11:08:54 +08:00
Kang-Yang
7c72c3d2af 🐛 fix(datagrid): 将尾页按钮图标调整到右侧 2026-07-23 10:42:57 +08:00
Syngnat
0554b45371 feat(data-grid): 增加单元格 JSON 格式化入口
- 将格式化操作移至单元格编辑器字段栏,避免短窗口裁切
- 仅在可编辑 JSON 单元格中显示图标按钮与提示
- 补充格式化草稿及保存结果的交互回归测试
2026-07-23 10:31:04 +08:00
Kang-Yang
4f0d3abeea 🐛 fix(datagrid): 修复跳页输入框数字垂直未居中 2026-07-23 10:27:00 +08:00
Syngnat
93b2bfef69 🐛 fix(settings): 修正更新通道说明与控件对齐
- 按 latest 和 dev 通道显示对应的稳定版或开发版说明
- 统一更新通道与检查间隔控件宽度及右侧对齐
- 补充六种语言文案和设置中心回归测试
2026-07-23 10:23:14 +08:00
Syngnat
5e71ad0090 🎨 style(titlebar): 调整 Windows 窗口按钮间距
- 为 Windows 自绘标题栏控制组增加 4px 间距
- 保持其他平台及按钮尺寸、圆角和交互不变
- 新增平台选择器与间距接线回归测试
2026-07-23 10:14:52 +08:00
Syngnat
e4ca74d935 🐛 fix(sidebar): 为表库删除增加五秒确认
- 统一侧边栏与表概览的 DROP 表库确认流程
- 倒计时期间禁用危险按钮并阻止提前执行,关闭时清理计时器
- 补齐六语言文案及倒计时边界与入口接线测试
2026-07-23 10:05:08 +08:00
Syngnat
430e27fb1e 🎨 style(driver-manager): 精简驱动卡片重复信息
- 移除卡片标题中的内部驱动类型,仅保留展示名和状态徽章
- 删除更新原因区域重复的重装标题,保留具体 revision 说明
- 覆盖普通弹窗、嵌入设置页和多语言重装提示
2026-07-23 09:36:58 +08:00
Syngnat
8c0e685429 🐛 fix(dameng): 修复 SQL 编辑器增删改执行失败
- 使用真实驱动事务执行达梦 DML,并支持显式提交与回滚
- 为 optional driver-agent 增加事务生命周期协议,仅向达梦暴露托管事务能力
- 同步驱动 revision,并补充驱动、代理与工厂回归测试
2026-07-23 09:36:22 +08:00
Syngnat
f2d7e3febd 🐛 fix(sql-audit): 修复审计分页被表格遮挡
- 表格滚动高度改为跟随面板容器,移除视口高度估算
- 补齐 Ant Table 高度与 flex 收缩链,限制表体滚动区域
- 新增分页布局回归测试,确保数据撑满时控件保持可用
2026-07-23 09:16:21 +08:00
Syngnat
9b47316211 🐛 fix(postgres): 修复序列展示与默认值插入 (#681)
- 接入 PostgreSQL 兼容数据库序列元数据并展示序列分组
- 保留字段默认值与生成列信息,插入空值时交由数据库生成
- 兼容 identity 与 pg_catalog.nextval,并支持 DEFAULT VALUES 插入
- 补充序列加载、生成列提交和 PostgreSQL 元数据回归测试
2026-07-23 09:11:41 +08:00
Syngnat
3e02c81471 🔧 chore(deps/bindings): 同步直接依赖与原生窗口类型
- 将代码直接使用的 golang.org/x/sync 标记为直接依赖
- 为 Wails OperationResult 绑定补充 applied 可选字段
- 在构造阶段同步反序列化 applied 以保持前后端类型一致
2026-07-23 08:47:38 +08:00
Syngnat
7e129fca35 test(query-editor): 补全文本转 SQL 组件测试模拟
- 补充 Input.TextArea 测试组件以支持文本转 SQL 弹窗渲染
- 增加图标模拟并保持结果拖拽用例可独立执行
2026-07-23 08:46:14 +08:00
Syngnat
818b75b691 🐛 fix(overlay): 修复浮窗与弹层层级遮挡
- 统一分离窗口、弹出层和弹窗的层级常量
- 将浮动窗口通过 Portal 挂载到 body 并隔离 AntD 弹层
- 补充右键菜单和分离窗口层级回归测试
2026-07-23 08:45:48 +08:00
Syngnat
5efe6e7947 Merge pull request #682 from AutumnNazi/fix/query-editor-find-blank-space 2026-07-23 08:23:50 +08:00
Syngnat
836b8258f7 ♻️ refactor(settings): 重构设置中心布局与交互规范
- 扁平化设置中心与 AI/MCP 信息层级,移除冗余卡片容器
- 统一字体、按钮、控件密度和嵌入式设置样式
- 完善键盘导航、焦点恢复、状态保留和多语言回归测试
2026-07-22 23:32:39 +08:00
Syngnat
caf450d4f0 🐛 fix(import): 修复导入进度与任务停止
- 修正 SQL 文件执行进度提前完成及终态竞态
- 支持停止导入并保留已提交的部分统计
- 完善取消审计、多语言提示与回归测试

Refs #677
Refs #678
2026-07-22 22:36:19 +08:00
Syngnat
e11706f9a9 feat(release): 接入 R2 镜像并限制发布保留量
- 应用与驱动更新优先使用 R2,并保留 GitHub 回退
- stable/dev 各保留当前版本并实施 9GB 发布预算
- 增加指针校验、安全清理和发布回归测试
2026-07-22 21:43:57 +08:00
Syngnat
e30c2d8c62 🐛 fix(ui): 修复拖拽与缩放残留导致的交互锁死
- 统一管理 pointer capture、全局监听器与选择状态生命周期
- 在失焦、捕获丢失、按键释放和组件卸载时自动清理交互状态
- 覆盖 AI、查询结果、工作台与结果比对浮窗拖拽缩放
- 修复 Redis、表设计、通用弹窗、数据网格和侧栏的残留监听
- 增加异常释放、重复终止和卸载清理等交互回归测试
2026-07-22 18:56:54 +08:00
Syngnat
c19fcdd183 🐛 fix(native-window): 修复 AI 子窗重开、设置遮挡与关闭恢复
- 单次快捷键可重新唤起停驻 AI 子窗,并过滤重复键盘事件
- 打开 AI 设置前原子隐藏子窗,避免设置窗口被遮挡
- 用可见性版本阻止延迟隐藏或设置事件覆盖新焦点
- 拒绝复用终态子进程,并恢复失败的附加与关闭操作
- 串行展示、隐藏和终态退出,消除关闭门闩并发竞态
- 增加原生窗口生命周期、结果回滚和焦点恢复回归测试
2026-07-22 18:45:55 +08:00
Syngnat
75749d2465 feat(table-overview): 新增紧凑表格视图并持久化视图偏好 2026-07-22 17:56:33 +08:00
Syngnat
0ea807c3cb feat(query-editor): 新增自动换行并持久化编辑器设置 2026-07-22 17:53:12 +08:00
Syngnat
0fb9857378 🐛 fix(query-editor): 修复查找框全选被编辑器快捷键拦截
- 在可编辑目标内放行 Ctrl/Cmd+A 原生全选
- 保留文档级快捷键对 Monaco 编辑器的兜底全选
- 补充查找输入框与源码范围回归测试
2026-07-22 17:45:51 +08:00
AutumnNazi
9706fa571a 🐛 fix(query-editor): 移除搜索框顶部空白区域
- 禁用 Monaco 查找组件的顶部预留空间,保持浮层显示
- 删除 V2 查询编辑器中查找组件的额外 24px 顶部补偿
- 更新查找配置与样式回归测试
2026-07-22 17:44:25 +08:00
Syngnat
c067ad8a62 🐛 fix(shortcuts): 迁移侧边栏搜索默认快捷键
- 将旧版 Meta/Ctrl+F 一次性迁移为各平台 Meta/Ctrl+K
- 升级持久化版本并避免启动刷新重新读回旧配置
- 保留禁用状态、自定义组合及版本 18 后的 F 配置
- 补充 macOS、Windows/Linux 与旧结构回归测试
2026-07-22 16:55:44 +08:00
Syngnat
8ca2c23d18 🐛 fix(ci): 补全前端 statSync 类型声明
- 补齐 node:fs 测试 shim 的 statSync 返回类型
- 修复干净 CI 环境下前端 TypeScript 构建失败
2026-07-22 16:53:49 +08:00
Syngnat
bf060f0082 feat(about): 关于页自动检查更新开关 + 自定义检查间隔 2026-07-22 15:40:46 +08:00
Syngnat
93bb164004 🐛 fix(modal): 统一浮层层级并修复设置窗口遮挡
- 统一主窗口、原生分离窗口和静态 API 的 Ant Design 浮层基线
- 提升更新、导出、安全更新与连接代理子弹窗层级
- 将业务 Modal 接入统一封装并修复命令面板 stacking context
- 增加浮层策略、设置流程和 Modal 导入守卫测试
2026-07-22 15:37:20 +08:00
virgosgod-maker
d4d3ac1f34 Merge branch 'Syngnat:dev' into dev 2026-07-22 15:26:24 +08:00
mango
d774aa741e feat(about): 关于页自动检查更新开关 + 自定义检查间隔
- 新增 autoCheckForUpdates 开关(默认开启)
- 新增 autoCheckForUpdatesIntervalMinutes(默认30分钟,可选15/30/60/120/360/720/1440)
- hook timer 按开关和间隔动态注册
- 关于页版本与更新卡片增加 Switch + Select 控件
- 6语种 i18n 文案
- store / hook / settings-center 测试补充
2026-07-22 15:24:55 +08:00
Syngnat
82af1e365e feat(data-grid): 支持只读单元格查看完整内容
- 只读查询结果和不可写字段支持双击打开只读 Monaco 查看器
- 保留 MongoDB 原始值与待提交值,并兼容空别名和空值
- 隔离数据源、权限与虚拟编辑会话,阻止旧回调误保存
- 补齐六种语言文案及 DataGrid 回归测试
2026-07-22 14:42:16 +08:00
Syngnat
ece61d097f feat(data-grid): 支持只读查询结果拖选复制
- 允许聚合与只读结果拖选单元格并复制 TSV
- 增加选区工具栏操作和 Ctrl/Cmd+C 快捷键
- 隔离后台结果页与编辑操作,避免快捷键抢占

Fixes #673
2026-07-22 12:30:30 +08:00
Syngnat
b272c2e39c ️ perf(jvm-monitor): 回收停止的监控会话 2026-07-22 09:36:57 +08:00
Syngnat
23d95f2771 ️ perf(store): 限制表访问频次状态增长 2026-07-22 09:35:41 +08:00
Syngnat
78880cfce5 test(i18n): 更新性能重构后的源码守卫 2026-07-22 09:06:02 +08:00
Syngnat
89b65cfb2d ️ perf(result-diff): 主动回收过期比对会话 2026-07-22 09:00:33 +08:00
Syngnat
5424f22677 ️ perf(native-window): 稳定结果同步回调避免全量序列化 2026-07-22 08:54:24 +08:00
Syngnat
92dd0cc650 ️ perf(startup): 延迟加载结果窗口数据网格 2026-07-22 08:53:56 +08:00
Syngnat
1fe1e51791 ️ perf(redis-monitor): 暂停后台页签轮询 2026-07-22 08:45:11 +08:00
Syngnat
62b7ea9aa9 ️ perf(startup): 延迟加载 AI 重型模块 2026-07-22 07:52:04 +08:00
Syngnat
375b472143 ️ perf(db-cache): 防止释放竞态复活数据库连接 2026-07-22 07:47:34 +08:00
Syngnat
c6ca44aa44 ️ perf(workbench): 隔离 SQL 输入引发的全局重绘 2026-07-22 07:28:02 +08:00
Syngnat
d864a83527 ️ perf(native-window): 避免热路径序列化 AI 上下文 2026-07-22 01:51:33 +08:00
Syngnat
ea7c05f28f ️ perf(query-completion): 限制大元数据候选构建 2026-07-22 01:47:14 +08:00
Syngnat
f9b685f5e7 ️ perf(build): 启用生产依赖分块配置 2026-07-22 01:41:07 +08:00
Syngnat
2015676ece ️ perf(er-diagram): 限制元数据缓存长期增长 2026-07-22 01:30:27 +08:00
Syngnat
813e9cde2c ️ perf(driver-agent): 限制长期运行错误日志缓存 2026-07-22 01:29:15 +08:00
Syngnat
cd55228e1e ️ perf(store): 合并持久化写入并复用状态投影 2026-07-22 01:14:26 +08:00
Syngnat
074f695e15 ️ perf(window): 降低空闲状态原生轮询频率 2026-07-22 01:10:56 +08:00
Syngnat
f64c4a4b98 ️ perf(db-cache): 合并并发冷连接并缩短缓存锁
按连接缓存键合并并发建连,避免同一数据源瞬间创建重复物理连接。

将慢速 Close 移出全局缓存锁,并保持每个调用的连接保活策略,补充并发回归测试。
2026-07-22 00:49:18 +08:00
Syngnat
7c7ed0ca81 ️ perf(ai-chat): 隔离流式消息重绘并复用工具结果索引
将消息行按引用和相关工具结果精确 memo,避免流式输出重绘全部历史消息。

复用单次工具结果索引,并为最新流式消息增加 O(1) 定位热路径与 500 条消息回归测试。
2026-07-22 00:45:27 +08:00
Syngnat
26e980917f ️ perf(workbench): 隔离后台页签日志订阅
仅让活跃查询编辑器订阅 SQL 日志,并在侧栏命令搜索关闭时保持稳定空快照。

补充隐藏页签重绘、激活切换和最近日志顺序回归测试。
2026-07-22 00:34:40 +08:00
Syngnat
76e74a02fd ️ perf(data-grid): 合并当前页查找扫描
将匹配坐标与命中统计合并为单次单元格遍历,避免重复格式化整页数据。

使用延迟值调度大结果集查找,同时保留清空输入时立即移除高亮的行为。
2026-07-22 00:20:17 +08:00
Syngnat
c0b1b0873e ️ perf(query-draft): 将大草稿持久化移出输入热路径
将防抖后的序列化与 localStorage 写入转移到空闲任务,并合并重复调度。

保留 pagehide、beforeunload 与显式同步刷新,补充大草稿和调度边界回归测试。
2026-07-22 00:13:00 +08:00
Syngnat
419480800c ️ perf(logger): 批量同步日志以消除全局 I/O 阻塞
- 移除每条日志在全局锁内执行的磁盘同步
- 使用脏标记和单槽队列合并后台刷盘请求
- 保证错误日志快速触发同步并在关闭时完整落盘
- 增加慢磁盘、并发关闭、空闲刷盘与日志顺序回归测试
2026-07-22 00:05:07 +08:00
Syngnat
7916538a19 ️ perf(query-editor): 避免后台查询页重复加载元数据
- 仅允许活动查询页拉取数据库列表和自动补全元数据
- 标签切换后忽略旧数据库列表请求的延迟回包
- 增加多查询页切换回归测试,限制后台 RPC 扇出
2026-07-21 23:54:58 +08:00
Syngnat
fda0733f11 ️ perf(native-window): 优化 AI 独立窗口启动与交互
- 关闭时隐藏保活,重开复用现有进程并通过可见性版本消除竞态
- 拆分 AI 与工作台资源,使用启动状态白名单减少序列化开销
- 修复设置中心层级、Windows 前台切换及独立窗口关闭交互
- 补齐状态同步、断线重放与窗口生命周期回归测试
2026-07-21 23:40:17 +08:00
Syngnat
62a045b39d 🐛 fix(native-window): 修复 Intel macOS 构建类型转换错误
将 Cocoa BOOL 桥接结果统一转换为整数值。

兼容 darwin/amd64 与 darwin/arm64 的 cgo 类型映射。
2026-07-21 19:45:33 +08:00
Syngnat
5c3535c85d feat(native-window): 支持 macOS Dock 管理独立窗口
- 在主程序 Dock 菜单动态列出并聚焦已就绪的独立窗口
- 在 Wails 启动前固定子进程为 Accessory,消除临时 Dock 图标闪现
- 按多显示器可见区域修正越界窗口并回写实际坐标
- 将自定义 AI 面板快捷键同步到独立窗口并转交主窗口处理
- 补齐 Go、Objective-C、前端及 Wails 绑定回归测试
2026-07-21 18:52:54 +08:00
Syngnat
d7b60891c5 feat(mcp): 支持 OpenCode 客户端接入
- 识别用户目录与 XDG 路径下的 OpenCode JSON/JSONC 配置
- 保留注释、自定义字段与符号链接并原子写入 MCP 配置
- 增加 CLI 检测、安装状态、自动接入及旧路径修复
- 在客户端选择、浏览器模拟、AI 检查提示和六语言文案中接入 OpenCode
- 引入 hujson、补充许可声明及前后端回归测试
2026-07-21 18:52:19 +08:00
Syngnat
3c8692b133 🐛 fix(connection/elasticsearch): 修复索引加载超时与测试按钮卡顿
- 使用 CAT Indices 轻量枚举索引并兼容 ES 7.3 参数
- CAT 失败时在统一超时预算内回退 Alias API
- 分离连接测试与保存状态并后台加载数据库列表
- 隔离过期测试请求,避免弹窗重开后状态污染
- 补充索引回退、超时预算与按钮状态回归测试
2026-07-21 18:41:58 +08:00
Syngnat
446707698f feat(table-copy): 新增整表复制与自动副本命名
- 在表概览及新旧侧栏右键菜单接入复制整表入口与确认、进度和刷新反馈
- 按 source_copyN 原子创建目标表并复制列、索引、默认值及全部数据
- 处理 PostgreSQL 生成列、identity/serial 序列校准与失败清理
- 限定安全数据源与连接保护策略,阻止分区表、RLS 及引用型存储引擎
- 加固 MySQL/PostgreSQL 元数据标识符处理并补齐六语种文案
- 增加后端、能力矩阵、菜单接线和国际化回归测试
2026-07-21 13:27:24 +08:00
Syngnat
49e566e01e 📝 docs(readme): 将体积表述从 ~10MB 更新为 ~30MB 量级
与当前发行包体量及仓库 About 描述对齐。
2026-07-21 12:29:27 +08:00
Syngnat
37e196d122 📝 docs(readme): 将反馈联系方式写入 README 并下线 #536 置顶依赖
新增「联系与反馈」章节(GitHub Issues + 微信),便于用户直接在文档中找到交流入口。
2026-07-21 12:19:47 +08:00
Syngnat
af2922a937 📝 docs(readme): 补充 Windows 内网 WebView2 Runtime 安装与排查
覆盖在线 Bootstrapper、离线 Standalone 静默安装、安装校验与 Web Server 绕过,并关联 #672。
2026-07-21 12:13:18 +08:00
Syngnat
8c4014c931 feat(ci): 评论「我想做」自动认领 Issue
在 Issue 下评论认领关键词时,优先写入 Assignees;无写权限则标记 claimed 并记录认领状态,避免双人撞车。
2026-07-21 11:59:23 +08:00
Syngnat
60d172254d 🐛 fix(update): 安装前确认并安全关闭当前安装的全部实例
- 安装更新前先确认关闭当前安装目录下的 GoNavi 实例,并继续执行未保存 SQL 退出保护
- 按可执行文件路径枚举并复核进程身份,优雅关闭超时后再强制终止,避免 PID 复用误杀
- 通过全局维护事件阻止更新期间启动新实例,并由 PowerShell 更新器完成跨权限无空窗交接
- 同步更新前端交互、多语言提示、Wails 绑定及 Windows 发布资产校验
2026-07-21 10:55:08 +08:00
Syngnat
2597d087ad feat(windows): 支持 MSI 主程序严格单实例启动
- 仅对带 MSI 安装标识的 Windows 主程序启用单实例门禁,Portable 与特殊运行模式保持原行为
- 重复启动通过命名事件唤醒首个窗口,并兼容 Wails 运行时尚未就绪的激活请求
- 新增并发抢占、窗口激活与安装模式测试,覆盖同一安装仅保留一个主实例
2026-07-21 10:49:52 +08:00
Syngnat
b75fd37b05 🐛 fix(update): 修复下载进度弹窗层级
- 隐藏下载进度时销毁旧弹窗 Portal,重新查看时恢复到设置中心上层
- 保留后台下载进度与完成状态,支持隐藏后反复查看
- 补充弹窗挂载策略及更新状态回归测试
2026-07-21 09:01:22 +08:00
Syngnat
0f474e4918 🐛 fix(ci): 修复标题栏资源测试的类型错误
- 为 PNG 资源读取显式指定 base64 编码
- 恢复前端及 Docker 镜像构建的 TypeScript 校验
2026-07-20 22:19:57 +08:00
Syngnat
118da179f6 🐛 fix(titlebar): 修复暗色主题标题栏图标白底
- 为默认品牌图标增加透明紧凑标题栏素材
- 增加独立标题栏资源解析并保留其他品牌图标现有展示
- 移除标题栏图标强制白色背景
- 补充默认图标、未知图标回退及透明素材测试
2026-07-20 20:23:18 +08:00
Syngnat
b2819db495 feat(shortcuts): 支持快捷键关闭当前标签页
- 新增可自定义的关闭当前标签页动作,macOS 默认 ⌘W,Windows/Linux 默认 Ctrl+W
- 根据最近交互区域路由工作区或结果标签关闭,并隔离分离窗口
- 关闭日志标签时隐藏整个结果区,避免连续按键误关工作区
- 沿用未保存 SQL 与导入任务等工作区关闭保护
- 隔离 IME、快捷键录制、弹窗、抽屉与上下文菜单
- 补充多语言文案及快捷键路由回归测试
2026-07-20 20:22:35 +08:00
Syngnat
db74e01447 📝 docs(spec): 设计标签关闭快捷键路由 2026-07-20 16:37:30 +08:00
Syngnat
049ec7bdf1 feat(data-workbench): 直达批量操作并新增数据导入工作台
- 批量表和批量库入口无需预选目标即可进入统一工作台
- 将对象选择、导出、清空和删除操作迁入可持续查看的任务界面
- 新增侧栏数据导入入口并复用预览、字段映射和进度流程
- 保护运行中的导入标签并同步复用工作台的连接上下文
- 补齐多语言文案及批量操作、导入链路回归测试
2026-07-20 15:34:20 +08:00
Syngnat
21e0921b3b feat(data-sync/export): 支持同步与备份后台工作台
- 将数据同步及各级 SQL 备份统一迁入稳定工作区
- 全局保留任务状态、进度和结构化日志,关闭后可重新接回
- 支持从当前任务及历史记录打开 SQL 恢复工作台
- 锁定运行中配置并禁止后台任务分离到独立窗口
2026-07-20 13:43:58 +08:00
Syngnat
19d962d9ba feat(data-export): 增强备份进度与原子写入
- 为整库与 Schema 备份补齐逐对象进度和最终文件路径
- 通过临时文件同步落盘和原子替换保护已有完整备份
- 增加任务进度及失败保留回归测试
2026-07-20 13:43:20 +08:00
Syngnat
89e35b087a 🐛 fix(sqlserver): 修复存储过程执行消息丢失
- 按消息协议仅在 MsgNext 时扫描结果集,避免空结果边界提前耗尽消息
- 保留连续 DONE 边界后的 PRINT 输出与纯消息结果
- 更新 SQL Server driver-agent revision 并补充回归测试
2026-07-20 13:40:53 +08:00
Syngnat
258f5bdd64 🐛 fix(tab-manager): 修复工作区标签批量关闭菜单失效
- 将右键菜单项和下拉菜单排除在标签拖拽触发区域外
- 避免 dnd-kit 指针捕获吞掉关闭其他页及左右侧关闭操作
- 补充菜单项不触发拖拽的指针回归测试
2026-07-20 13:40:29 +08:00
Syngnat
02eec5aee3 🐛 fix(query-editor): 修复 SQL 执行与结果标签交互
- 空白光标回退执行编辑器内全部有效 SQL
- 修复活动结果标签必须切换日志后才能关闭的问题
- 阻止关闭按钮和菜单指针误触结果标签分离拖拽
- 补充空白行、尾部空白和结果标签关闭回归
2026-07-20 13:40:03 +08:00
Syngnat
63ffa12958 🐛 fix(connection): 修复多数据源连接与浏览异常
- 修复 Milvus 表概览元数据查询与 SELECT 预览路由异常
- ClickHouse 非标端口 Native 握手超时后继续 HTTP 自动探测
- Elasticsearch 连接测试与索引枚举解耦,避免健康服务误判
- 更新可选驱动 revision 并补充回归测试
2026-07-20 10:55:19 +08:00
Syngnat
cb5f6a668f feat(datagrid): 支持按单元格选区删除记录
- 统一单元格选区与复选框行的删除目标和计数,按记录行去重处理
- 保留复选框优先级,新增行直接移除,已有记录进入待提交删除
- 隔离页内查找高亮,并在刷新、数据重载或上下文切换时清理旧选区
- 补充选区删除、刷新保护、查找隔离和新增行回归测试
2026-07-20 10:21:01 +08:00
Syngnat
ae0eb03c21 🐛 fix(datagrid): 修复 macOS 触摸板横向滚动回跳
- 持续横向滚动期间守护最新视觉偏移,避免虚拟列表异步回写旧位置
- 增加可取消的逐帧视觉守卫并完善交互与卸载清理
- 补充守卫时序及 DataGrid 滚动接线回归测试
2026-07-20 09:43:36 +08:00
Syngnat
54c06ed113 feat(context-menu): 增强字段与数据库名称复制交互
数据表列头支持按元数据复制字段注释;数据库节点旧版与 V2 菜单支持复制数据库名;补齐六语言文案与回归测试。

Fixes #547
2026-07-19 18:17:14 +08:00
Syngnat
ea9ccb3648 🐛 fix(release): 修复自动更新日志缺少贡献者署名
- 基于关联 PR 与提交元数据解析外部贡献者账号
- 生成 GitHub 原生用户提及以展示贡献者头像名单
- 增加 API 熔断、版本历史告警与 CI 回归测试
2026-07-19 17:39:36 +08:00
Syngnat
c4f24a6245 🐛 fix(data-export): 自动补齐导出文件格式后缀
- 为 CSV、XLSX、JSON、Markdown、HTML 和 SQL 保存对话框绑定对应过滤器并统一补齐扩展名
- 覆盖单表、查询、批量 SQL、数据库、Schema 与 SQL 审计导出路径
- 防止补后缀后的目标绕过覆盖确认,并补充真实落盘、多语言与安全回归测试
2026-07-19 14:27:00 +08:00
Syngnat
453bed439d 🐛 fix(data-export): 修复查询重放 SQL 导出缺少目标表
- 导出工作台执行 SQL 查询重放时传递已知目标表
- 保持 MariaDB 方言与列元数据由后端统一处理
- 补充当前页 SQL 导出的真实组件回归测试
2026-07-19 14:02:03 +08:00
Syngnat
2428fe6cc5 🐛 fix(tdengine): 修复查询结果仅显示执行日志
- TDengine 只读查询跳过不支持的多结果集探测
- 保证可选驱动代理通过普通查询返回完整行列数据
- 补充应用层与驱动层回归测试

Fixes #610
2026-07-19 13:32:52 +08:00
Syngnat
94ad5ae2f6 feat(connection): 支持自定义 SQL 探活
- 新增连接级自定义探活 SQL 配置与多语言表单校验
- 限制单条 SELECT/WITH 并使用可取消的超时查询
- 隔离陈旧探活策略,避免配置错误驱逐健康连接

Fixes #611
2026-07-19 12:29:21 +08:00
Syngnat
82c62fd55c feat(query-result): 新增结果页快速查看表 DDL
- 识别单表查询并在 Legacy 与 V2 结果视图提供 DDL 入口
- 按数据库方言独立解析并传递 DDL 目标至浮动与独立窗口
- 避免多表和歧义查询误展示入口并补充回归测试

Fixes #537
2026-07-19 10:55:13 +08:00
Syngnat
3e9335d6ad 🐛 fix(ci): 修复草稿发布分页结果解析 [skip ci]
- 直接读取 Release workflow 与 release 列表响应
- 避免 paginate 映射回调产生空元素导致校验异常
- 保持目标提交和完整资产校验不变
2026-07-19 01:06:47 +08:00
Syngnat
b6cfeccfe4 🔧 ci(release): 支持发布分支触发草稿发布 [skip ci]
- 允许 publish/v* 临时分支触发受校验的发布流程
- 从分支名安全解析目标版本并复用完整资产检查
- 保留 workflow_dispatch 作为具备 Actions 权限时的入口
2026-07-19 01:04:55 +08:00
Syngnat
0fb986868b 🔧 ci(release): 新增草稿发布安全校验 [skip ci]
- 校验目标标签对应的 Release workflow 已成功完成
- 确认 13 项核心安装与更新资产均已上传
- 使用最小 GitHub 权限手动发布草稿并设为最新版
2026-07-19 01:03:37 +08:00
Syngnat
e404fa3ac0 🐛 fix(data-grid): 兼容无 navigator 的服务端渲染
- 通过 globalThis 安全注入浏览器 navigator,避免 Node 20 渲染期抛错
- 补充无 navigator 环境下的 DataGrid 服务端渲染回归测试
- 保持浏览器剪贴板行为不变
2026-07-19 00:22:02 +08:00
Syngnat
7913811549 ️ perf(data-grid): 优化大数据量横纵向滚动性能
- 为固定行高结果集启用常数时间纵向窗口计算和横向列虚拟化
- 合并横向滚动预览与空闲提交,减少滚动期间重复渲染
- 固化 rc-table 与 rc-virtual-list 补丁并同步 Wails、Docker 和 CI 安装链路
- 补充滚动边界、页内查找及调度回归测试
2026-07-19 00:10:25 +08:00
Syngnat
af49d15563 🐛 fix(table-metadata): 修复统计字段被误识别为表名 (#666)
## 背景

DBGetTables 会同时返回 Table、Rows、Data_length 和 Index_length 等字段。部分前端入口通过
Object.values(row)[0] 获取表名,Go map 序列化后的字段顺序变化时,行数或存储大小会被误当成表名。

## 变更内容

- 新增共享表元数据解析器,优先读取明确的表名字段
- 兼容 Table、table_name、tableName、name、MySQL Tables_in_*、字符串行和旧单字段响应
- 多字段响应缺少明确表名时不再猜测首个字段
- 统一 AI 关联上下文、AI 数据库工具、查询补全、ER 图、侧边栏、全库搜索、数据同步、批量导出、导出工作台和表概览的解析逻辑
- 补充统计字段顺序、兼容格式、空值和重复名称的回归测试

## 影响范围

仅调整前端对 DBGetTables 返回值的表名解析,不修改后端 API、数据库查询或表统计逻辑。

## 验证

- 12 个相关 Vitest 文件,共 62 条测试通过
- npm run build 通过,8669 个模块完成生产构建
- git diff --check 通过
2026-07-18 21:49:58 +08:00
AutumnNazi
6e58240c38 🐛 fix(table-metadata): 修复统计字段被误识别为表名
- 新增共享表元数据解析器,优先读取明确的表名字段
- 统一 AI、查询补全、侧边栏、导出和表概览等调用路径
- 补充字段顺序、兼容格式及去重行为的回归测试
2026-07-18 21:03:25 +08:00
Syngnat
3aea725f7b feat(import-export): 支持 SQL 导出前删除同名对象
- 新增默认关闭的 DROP IF EXISTS SQL 导出选项和数据删除提示
- 覆盖数据库、Schema、单表、批量导出及工作台入口
- 按方言生成逆序 DROP 前导段并保持纯数据与旧 API 兼容

Fixes #514
2026-07-18 18:20:30 +08:00
Syngnat
fd093b3325 feat(clickhouse): 支持自定义连接兼容 JDBC DSN
- 解析 ClickHouse 与 JDBC 风格 DSN 并映射协议、TLS、认证及数据库参数
- 统一连接缓存、DDL、数据同步与导出 driver-agent 生命周期
- 保持 DSN 加密存储并补充前后端回归测试和多语言提示

Fixes #383
2026-07-18 17:48:19 +08:00
Syngnat
0136b87311 🐛 fix(proxy): 避免全局代理影响数据库连接
移除数据库和 Redis 连接对全局代理的隐式继承

保留连接自身代理与 HTTP 隧道配置并修正缓存释放链路

同步六种语言的代理作用域说明并增加回归测试

Fixes #347
2026-07-18 16:43:39 +08:00
Syngnat
aeecffb517 feat(table-designer): 补齐 MySQL 空间字段类型
- 补充 Geometry、Point、MultiPoint 等八种 MySQL 空间类型候选
- 保留自定义字段类型表达式在建表和改表 SQL 中的原样输出
- 增加空间类型候选及 POINT SRID 属性回归测试

Fixes #341
2026-07-18 16:15:33 +08:00
Syngnat
5f5b28ced3 🐛 fix(schema): 修复 PostgreSQL 模式备份恢复与重命名
- 模式备份脚本写入安全转义的 CREATE SCHEMA IF NOT EXISTS
- 支持仅大小写变化的 PostgreSQL 模式重命名
- 补充备份头顺序、非 PostgreSQL 隔离和模式 DDL 回归测试

Fixes #480
2026-07-18 15:51:48 +08:00
Syngnat
15fba1d89f feat(import-export): 优化数据表导入导出能力
- 修复表格格式导出时空值显示为 null 的问题
- 支持按列导出并保持字段顺序
- 支持导入列与数据库字段映射及冲突校验
- 隔离并发导入任务进度并完善元数据回退

Fixes #646
2026-07-18 15:17:54 +08:00
Syngnat
f08bcb5e10 🐛 fix(monaco-editor): 修复 WebKit 输入法组合输入时异常滚动
- 仅在原生 WebKit 环境稳定 Monaco 输入法组合期间的文本区滚动位置
- 在组合结束、失焦和编辑器销毁时及时停止并清理监听器
- 补充 WebKit、Chromium 隔离及销毁清理回归测试
2026-07-18 14:00:19 +08:00
Syngnat
7017db418b 🐛 fix(updater): 修复 MSI 更新后任务栏图标失效与桌面快捷方式重建
- 自动迁移引用失效 MSI 缓存图标的旧任务栏固定项并刷新 Shell
- 在在线更新前备份桌面快捷方式状态,成功或失败后安全恢复
- 将桌面快捷方式拆分为条件安装组件并补充 Windows 回归测试
2026-07-18 13:48:35 +08:00
Syngnat
6cd2835066 feat(ai): 支持 Codex 与 Claude 订阅登录
- 新增基于本机官方 CLI 登录状态的 Codex 与 Claude 订阅 Provider
- 隔离 API Key、工具与项目配置,完善登录校验和错误处理
- 补充设置界面、国际化及前后端回归测试

Fixes #651
2026-07-18 11:57:25 +08:00
Syngnat
07a4fe1fc9 🐛 fix(query-editor): 修复 MySQL 关键字需美化后才着色
- 根据当前连接方言为 MySQL 系查询启用 Monaco MySQL 语法
- 将补全、悬浮、斜杠命令和代码片段同步注册到 MySQL 语言
- 增加方言映射和首屏着色回归测试

Fixes #612
2026-07-18 10:32:51 +08:00
Syngnat
1ee8f7f980 🐛 fix(ci): 修复 Docker 冒烟测试端口冲突
- Web Server 冒烟测试使用随机宿主端口
- 动态解析映射端口并复用健康检查与鉴权验证
2026-07-18 09:51:19 +08:00
Syngnat
d0a6a97f71 🐛 fix(sql-file): 修复整库备份导入失败
Fixes #662
2026-07-17 23:37:53 +08:00
Syngnat
e24942083a 🐛 fix(sidebar): 顶部新建查询绑定选中数据库
Fixes #665
2026-07-17 22:40:36 +08:00
Syngnat
e7857bb2e4 🐛 fix(ci): 修复跨 Runner 驱动版本误判
使用构建阶段 SHA 绑定 provenance 校验驱动发布资产。

保留原生二进制元数据和 legacy fallback 校验,并固定发布任务 Go 环境。

Fixes #622
2026-07-17 21:50:15 +08:00
Syngnat
8f52cbc319 🐛 fix(query-editor): 修复选区输入首字符丢失
- 为 Monaco 可打印输入兜底补充非空选区替换恢复
- 区分原生成功、仅删除选区和完全丢失,避免重复插入
- 覆盖连续输入、重选文本和模型事件光标竞态
- 新增 &lt; 单次替换为 < 的回归测试
2026-07-17 17:39:02 +08:00
Syngnat
ca8be18531 🐛 fix(ci): 校验驱动代理发布产物版本来源
- 原生平台执行 metadata 校验真实 driver type 与 revision
- 跨平台构建生成与二进制 SHA256 绑定的 provenance
- 复用旧资产时要求清单 SHA 与当前源码 revision 同时匹配
- 将 provenance 接入开发版和正式版发布聚合流程
- 发布链变更触发全平台 driver-agent 重建
2026-07-17 17:07:37 +08:00
Syngnat
54f18cdd2f 🐛 fix(driver-agent): 阻止运行不匹配的驱动代理
- 连接前复用安装阶段的严格 revision 与 metadata 校验
- 对旧版或无法验证的 agent 返回明确重装错误而非继续运行
- 覆盖 stale、metadata 失败、当前版本和自定义驱动边界
- 隔离 SQL 审计测试对本机已安装 agent 的依赖
2026-07-17 17:06:44 +08:00
Syngnat
0ca8b1960c 🐛 fix(milvus): 兼容不支持数据库枚举的服务
- 数据库列表接口不可用时验证当前数据库的集合枚举能力
- 验证成功后回退到已配置数据库并继续加载集合
- 补充测试连接成功但侧栏展开失败的回归用例
2026-07-17 17:06:08 +08:00
Syngnat
3f4b247faa feat(native-window): 完善原生独立窗口与跨屏拖拽
- 支持全部工作台标签与 AI 对话在多显示器中原生独立展示
- 修复拖拽指针捕获,恢复关闭、右键菜单和跨窗口释放识别
- 拆分子窗启动入口并按需加载语言与工作台内容
- 内容与 Monaco 完成可见绘制后再提交迁移,消除白屏和加载闪烁
- 加固结果编辑、宿主同步及关闭、超时和焦点竞态
2026-07-17 16:06:24 +08:00
Syngnat
a86f5c6078 🐛 fix(data-grid): 保留独立窗口完整结果快照
- 数据变更回传不再受界面隐藏列过滤
- 同步编辑与删除后的完整结果行数据
- 补充独立窗口隐藏列快照回归测试
2026-07-17 16:05:24 +08:00
Syngnat
38a18b9b8e 🐛 fix(ai): 稳定流式会话终止与窗口迁移
- 跟踪并等待全部流生产者退出,避免重叠请求残留
- 窗口迁移期间阻止新请求并保留最终流式消息
- 锁定会话切换与重试操作,状态落稳后再解除发送态
- 同步终止握手绑定并补充回归测试
2026-07-17 16:04:33 +08:00
Syngnat
f2d622b53f 🐛 fix(ai): 规范 OpenAI system 消息顺序
Fixes #663
2026-07-16 22:39:33 +08:00
Syngnat
fa87c2ee9f Merge pull request #658 from AutumnNazi/fix/msi-shortcut-icon 2026-07-16 21:16:02 +08:00
Syngnat
43b33ca42a Merge pull request #664 from AutumnNazi/fix/middle-click-close-tab 2026-07-16 21:11:36 +08:00
Syngnat
fa9d21be8b feat(native-window): 支持标签页拖出为原生独立窗口
- 支持 SQL、数据和结果标签拖出到跨显示器原生窗口
- 通过受认证 loopback bridge 复用主进程后端与状态
- 支持子窗口继续拆窗、聚焦、关闭、还原与异常退出恢复
- 补充多窗口协议、状态同步和跨平台回归测试
2026-07-16 20:25:08 +08:00
Syngnat
d3dee295d6 🐛 fix(query-results): 修复 SQLite 与 SQL Server 查询结果空页
- SQLite 只读查询优先普通 Query,并安全降级不支持的多结果集接口
- SQL Server 在结果集边界前恢复未扫描数据,保持 data/affectedRows 顺序
- 普通与事务查询过滤状态空壳,修正结果展示与审计返回行数
- 更新 SQL Server driver-agent revision 并补充 SQLite、MSSQL 回归测试
2026-07-16 20:24:39 +08:00
AutumnNazi
35427d08a8 feat(tab): 支持鼠标中键关闭标签页
- 在标签标题上识别鼠标中键并阻止默认滚动行为
- 复用现有标签关闭流程,保留 SQL 文件未保存确认
- 修正停靠标签关闭范围断言并增加中键交互回归测试
2026-07-16 20:19:34 +08:00
Syngnat
193d68398b 🔧 chore(license): 补齐开源许可与发布声明
- 新增项目版权及上游 NOTICE
- 让桌面、容器、驱动发布包携带法律文档
- 补充 Wails 和前端许可证元数据
- 增加发布资产回归校验
2026-07-16 17:24:16 +08:00
Syngnat
d14ac65db9 🐛 fix(connection): 修复多数据源连接打开异常
- 修复 Milvus 等数据源类型被错误回退为 MySQL
- 为 ClickHouse 22.8 增加旧版 JSON HTTP 兼容回退
- 使用轻量别名接口验证 Elasticsearch 索引枚举
- 更新可选驱动 revision 并补充回归测试
2026-07-16 17:23:16 +08:00
Syngnat
c90dcce859 🐛 fix(driver-agent): 修复驱动重装循环与旧资产复用
- 在线重装与本地导入统一使用 staging 并严格校验 agent revision
- 旧候选失败时继续回退,激活异常时恢复原驱动与运行文件
- 修复公共驱动源码变更被误判为单一驱动的问题并补齐 Trino
- 显式传播依赖枚举失败,避免生成部分 revision 指纹
- 将驱动发布契约回归接入开发版与正式版构建流程
2026-07-16 15:57:49 +08:00
Syngnat
dfa6836580 🐛 fix(sidebar): 修复元数据展示开关未过滤嵌套对象
- 修复子节点数量不变时误复用未过滤父节点的问题
- 保留标签、连接、数据库与 Schema 包装层中的深层过滤结果
- 补充多层侧栏树对象显隐回归测试
2026-07-16 15:56:15 +08:00
AutumnNazi
8f4bd9a3a6 🐛 fix(installer): 修复 MSI 更新后快捷方式图标失效
- 让开始菜单和桌面快捷方式直接使用 GoNavi.exe 内嵌图标
- 避免 Major Upgrade 后 Shell 缓存继续引用已删除的 MSI 图标资源
- 增加 Windows 安装产物回归测试,锁定快捷方式图标来源
2026-07-16 14:38:20 +08:00
Syngnat
0080cb33ac 🐛 fix(table-overview): 修复未知存储大小误显示为零
将 Oracle、达梦和默认方言不支持的存储统计改为 NULL 占位。

补充 Oracle 与默认方言回归测试,保留 SQLite 真实零值的 0 B 展示。
2026-07-16 13:10:29 +08:00
AutumnNazi
2400823ca4 🐛 fix(sqlite): 修复表概览行数与大小统计
- 通过 driver-agent 为 SQLite 表列表补充精确行数与存储占用
- 使用 dbstat 区分数据页和索引页并支持相对大小展示
- 区分未知统计与真实零值并补充前后端回归测试
2026-07-16 12:44:18 +08:00
Syngnat
695fad283c 🐛 fix(ci): 补全前端 Node 文件系统类型声明
- 为测试环境的 node:fs shim 增加 readdirSync 返回类型
- 避免本机上级 @types/node 掩盖隔离构建错误
- 恢复 Dev Build 与 Web Server Docker 镜像前端编译
2026-07-16 12:11:29 +08:00
Syngnat
6de7807f01 🐛 fix(query-editor): 修复 Oracle 同名同义词补全
- 合并 PR #654,并按 OWNER 与名称保留同义词元数据
- 无限定名优先登录用户,缺失时回退 PUBLIC,显式 schema 精确匹配
- 保持 GetTables 仅返回物理表,避免影响导出、ER 与 ROWID 判断
- 补充 owner 解析、PUBLIC 回退及物理表查询回归测试
2026-07-16 11:49:15 +08:00
Syngnat
3de7e1bb40 feat(settings): 将工具中心整合至设置中心
- 侧栏移除独立工具入口并统一使用设置齿轮
- 设置中心扩展为六组导航并复用原有工具内页
- 统一安全更新、连接配置、AI 设置与工具页面的返回路径
- 清理快捷键捕获、连接密码弹窗和 AI 修复上下文等瞬态状态
- 更新设置、侧栏、SQL 审计与国际化回归测试
2026-07-16 11:17:59 +08:00
Syngnat
ac4b520295 🐛 fix(brand): 统一图标资源并适配 macOS Dock
- 将 40 个重复 PNG 变体收敛为 10 个 WebP 品牌资源
- 品牌选择器、关于页、favicon 与 Dock 统一复用所选图标
- 按 824px 安全区和 184px 标准圆角合成 macOS Dock 图标
- 补充资源映射、品牌展示和 Dock 合成回归测试
2026-07-16 11:16:47 +08:00
Syngnat
823fd54ef0 📝 docs(readme): 替换透明背景品牌 Logo
- 中英文 README 统一引用仓库内品牌 Logo
- 使用带透明通道的 512px 图片移除底色
- 保持 GitHub 页面与仓库预览直接可用
2026-07-16 11:14:48 +08:00
半袭纸鸢
5413089db0 Merge 4df1dac2c2 into 5d91deaade 2026-07-16 11:04:02 +08:00
autumn
4df1dac2c2 feat(data-grid): 支持查询结果导出 INSERT SQL
- 导出弹窗新增 INSERT SQL 格式并支持选中行、当前页和全部结果
- 单表查询自动映射真实表名与字段类型,连表结果使用带方言引号的 <table_name> 占位符
- 复用流式导出与批量 INSERT 生成逻辑,兼容 PostgreSQL 布尔字面量
- 补充前后端回归测试,覆盖单表、连表、别名和占位表场景
2026-07-16 10:58:00 +08:00
Syngnat
5d91deaade 🔧 chore(frontend): 同步 package 校验文件
- 将 package.json.md5 同步为当前 frontend/package.json 的真实 MD5
- 消除工作台更新遗留的过期校验值
- 不改动依赖声明或自动生成的 TS 绑定
2026-07-16 08:44:19 +08:00
Syngnat
b781a1197e 📝 docs(readme): 修复 GitHub 截图展示 2026-07-16 08:29:14 +08:00
Syngnat
176b100274 🐛 fix(app): 修复品牌图标与 SQLite 查询展示
- 修复 Windows、macOS 与关于页的品牌图标展示及资源裁切问题
- 补齐 SQLite 表记录数与 SELECT 查询结果渲染
- 增加品牌图标和 SQLite 回归测试
2026-07-15 22:53:47 +08:00
Syngnat
d87e314880 🎨 style(brand): 吉祥物品牌图标可选,并同步 macOS Dock
新增 10 款品牌吉祥物资源与设置页选择器;标题栏/关于页/favicon 跟随切换,
macOS 通过原生 API 更新 Dock 图标(满铺浅蓝 Dock 专用图)。
同步默认 appicon 与 README 品牌展示。
2026-07-15 19:55:20 +08:00
yx755141
9a693ec4b1 fix: GetTables 增加 UNION ALL all_synonyms
imp_basicinfo 名下只有同义词没有实际表,仅查 all_tables 返回空。
增加 all_synonyms 后 sbdev 输入 imp_basicinfo. 时能提示 AC01/AC02。
2026-07-15 17:29:42 +08:00
yx755141
1bf8271562 fix: 去掉OWNER=USER限制,ALL_SYNONYMS本身已权限过滤 2026-07-15 16:58:07 +08:00
tianqijiuyun-latiao
8a5209c084 ️ perf(data-grid): 优化大数据量横向滚动性能 2026-07-15 15:26:53 +08:00
tianqijiuyun-latiao
89c25e3019 🐛 fix(updater): 阻止 Windows 多实例并发安装更新 2026-07-15 15:26:53 +08:00
辣条
c8e7495f50 查询同义词,查询所有有权限的,不止当前租户 (#653) 2026-07-15 14:25:27 +08:00
yx755141
1b4e47d709 查询同义词,查询所有有权限的,不止当前租户 2026-07-15 14:20:57 +08:00
Syngnat
6548f939a2 🎨 style(brand): 替换 GoNavi 品牌 Logo 为 orbit-core
- 更新仓库根与前端 public 的 logo.svg 为轨道核心标识
- 同步 build/appicon.png(1024)及 macOS icns、Windows ico
- 视觉语义:核心圆为数据中枢,双轨道为跨库导航,卫星点为连接节点
2026-07-15 13:58:44 +08:00
tianqijiuyun-latiao
1458b57c0d Merge branch 'refs/heads/dev' into feature/20260602_connection_driver_i18n 2026-07-15 13:32:04 +08:00
tianqijiuyun-latiao
1c1adb8331 🐛 fix(redis): 防止命名空间行误触批量选择 2026-07-15 13:31:48 +08:00
tianqijiuyun-latiao
6f65f9e7cc 🐛 fix(sidebar): 修复 OceanBase Oracle 只读对象加载 2026-07-15 13:17:40 +08:00
Syngnat
5b8e7b538c 🐛 fix(query-editor): 修复视图与同义词补全
- 跨用户视图补全避免重复 owner 前缀
- 按结果集字段名去重视图列候选
- 加载 Oracle 私有及 PUBLIC 同义词并解析目标字段
- 补充跨用户视图与同义词回归测试
2026-07-15 11:37:36 +08:00
Syngnat
a0bb19b8bb feat(sidebar): 支持自定义对象分类显示
新增侧栏对象分类开关与仅显示表快捷设置
配置持久化并即时应用到所有连接
补充筛选、持久化与国际化回归测试
2026-07-14 21:53:57 +08:00
Syngnat
3fa4f8e900 🐛 fix(query-editor): 修复 OceanBase 视图查询与输入丢字
- 仅对元数据确认的 OceanBase Oracle 基表注入 ROWID
- 视图和同义词查询改为只读,避免触发 OBE-01445
- 在 SQL 补全中加入普通视图和物化视图候选
- 修复 beforeinput 已拦截时首个可打印字符被跳过的问题
- 补充视图、同义词和输入回归测试
2026-07-14 17:11:07 +08:00
Syngnat
8a1c1842ee feat(saved-query): 支持多级已存查询分组
- 新增已存 SQL 分组的持久化模型与后端管理接口
- 支持创建、编辑、嵌套、排序、移动和删除分组
- 在侧栏树和菜单中提供分组管理与未分组查询视图
- 同步 Wails 绑定、六种语言文案及前后端回归测试
2026-07-14 17:09:27 +08:00
Syngnat
7886cdc613 🐛 fix(sidebar): 排除已归属连接的分组选项
- 创建分组时仅展示未归属其他分组的连接
- 编辑分组时保留当前分组已有成员
- 无可选连接时显示明确空状态并补充多语言文案
- 增加连接筛选回归测试
2026-07-14 17:06:35 +08:00
Syngnat
454b6e0236 🐛 fix(dameng): 修复自增字段识别
通过达梦系统列元数据识别 IDENTITY 与 AUTO_INCREMENT。\n\n系统表不可访问时保留基础字段查询,并补充调用链回归测试。
2026-07-14 14:17:30 +08:00
Syngnat
c1e07dddaa feat(sidebar): 支持多层 Host 分组
- 支持分组父子层级与 Host/子分组混排排序
- 兼容旧分组配置并清理循环引用和重复归属
- 增加子分组创建、父分组选项及层级拖拽
- 删除分组时原位提升直属 Host 和子分组
2026-07-14 13:43:54 +08:00
Syngnat
1b3137d947 🐛 fix(transaction): 修复匿名SQL块自动提交
- 区分匿名 BEGIN...END 块与显式事务控制语句
- 统一前后端方言判定并挂起 Oracle、达梦和 SQL Server 块内 DML
- 补充编辑器、事务控制器及 SQL Server 回滚链路回归测试
2026-07-14 12:22:07 +08:00
Syngnat
72423e80a8 🐛 fix(datagrid): 修复 macOS 横向滚动拖拽抖动
- 拖动期间延迟虚拟表异步视觉重绘,减少重复样式写入
- 使用滚动空闲窗口保护外部原生滑块,避免偏移回写
- 松开拖拽后统一对齐表头、表体和滑块
- 补充横向滚动同步回归断言
2026-07-14 11:46:25 +08:00
Syngnat
5fd12d5dd8 🐛 fix(update): 同步 Windows Portable 更新版本
- 版本化 Portable 更新后切换到新包文件名并清理旧文件
- 下载完成进度采用后端实际返回的版本与资产
- 补充前端竞态和 Windows PowerShell 集成回归测试
2026-07-14 10:02:36 +08:00
Syngnat
c9ddd8919e feat(workbench): 丰富主页快捷资源与工作入口 2026-07-13 22:52:01 +08:00
Syngnat
b22a0894b8 🐛 fix(update): 修复隐藏下载进度后弹窗重开 2026-07-13 21:54:43 +08:00
Syngnat
e17e21b105 feat(workbench): 支持 Schema 筛选和历史快捷打开 2026-07-13 21:54:10 +08:00
Syngnat
57172f1296 feat(mcp-http): 持久化服务开关并支持启动恢复
保存启用偏好及运行参数,重启自动恢复;分离运行态与启用意图,补齐失败重试、并发收敛和回归测试。
2026-07-13 20:23:54 +08:00
Syngnat
2d5f246486 🐛 fix(kafka): 修复主题预览偏移量越界
- 使用绝对 offset 定位 Kafka 分区消息
- 避免 earliest offset 非零时重复叠加
- 增加直连预览读取的回归测试
2026-07-13 17:52:26 +08:00
Syngnat
0077c6dba7 ️ perf(sqlserver): 延长空闲连接复用窗口
- 将 SQL Server 单条空闲连接保留时间由 30 秒延长至 30 分钟
- 保持最大连接数和 30 分钟生命周期轮换策略不变
- Oracle、OceanBase 与其他数据库继续沿用原连接池策略
- 增加空闲窗口与连接复用边界回归测试
2026-07-13 17:30:23 +08:00
Syngnat
1423ac0902 🐛 fix(sql-editor): 修复退出保存重复生成查询副本
- 退出保存临时查询时复用原查询页签 ID
- 重启后通过稳定 ID 识别已经保存的同一条查询
- 增加连续退出不重复生成历史副本的回归测试
2026-07-13 17:30:02 +08:00
Syngnat
6958299ec2 🐛 fix(oceanbase): 修复 Oracle 模式单元格事务提交
- 单元格变更提交与预览使用所选 schema 限定目标表
- 对限定表名按分段方式引用,避免将 schema 与表名整体转义
- 为全列定位条件中的日期和时间值补充显式 Oracle 转换
- 增加 OceanBase Oracle 事务提交与 SQL 预览回归测试
2026-07-13 17:29:41 +08:00
Syngnat
6329345e73 🐛 fix(query-results): 修复分页统计与字段排序交互
- 为分页栏显示明确的首页和尾页入口,并接入真实的总数统计与取消回调
- 支持精确总数查询并隔离取消请求和过期响应,翻页时保留已统计总数
- 完整结果采用本地稳定排序,分页结果按数据库方言重新查询第一页
- 修复单元格编辑模式阻断列头排序箭头并补充回归测试
2026-07-13 17:29:15 +08:00
Syngnat
ef0845a9c5 🐛 fix(tabs): 修复短连接标签占用固定间距
- 将连接标签由固定 flex 基准改为按内容宽度收缩
- 保留 92px 最大宽度以避免长连接名称撑开标签
- 增加短连接名称间距回归测试
2026-07-13 17:28:33 +08:00
Syngnat
509962a773 feat(pagination): 增加首尾跳转并恢复查询翻页
- 自动限行结果保持未知总数语义并恢复后续页探测
- V2 与旧版分页增加首页和尾页快捷按钮
- 未知总页数时禁用尾页避免跳转到推算页
- 补齐六种语言文案及分页边界回归测试
2026-07-13 12:56:35 +08:00
Syngnat
b9e890379e feat(ai): 支持 OpenAI Responses 协议
- 设置中心为 OpenAI 预设增加 Chat 与 Responses 协议选择
- 实现 Responses API 同步、流式、推理摘要及工具调用适配
- 支持会话状态回放与上游能力不兼容时的自动降级
- 完善地址、模型列表和思考强度识别并补充回归测试
2026-07-13 12:55:58 +08:00
Syngnat
933e601816 🐛 fix(snippet): 保留代码片段尾部空白
- 持久化时完整保留代码片段正文中的尾部空格与换行
- 继续拒绝仅包含空白字符的无效片段
- 补充保存、存储及重新加载后的正文一致性测试
2026-07-13 12:55:22 +08:00
Syngnat
b23e768973 🐛 fix(query-editor): 修复 Oracle 序列查询自动限行报错
- 识别可执行 SQL 中的 NEXTVAL 与 CURRVAL 序列伪列
- Oracle、达梦及 OceanBase Oracle 序列查询跳过 ROWNUM 子查询包装
- 排除字符串和注释中的同名文本误判
- 补充自动限行与真实执行链路回归测试
2026-07-13 12:54:43 +08:00
Syngnat
4bee3cefa6 ️ perf(query-editor): 优化输入与选区交互流畅度
- 为 Monaco 编辑器及语言服务接入独立 Worker
- 限制大规模对象元数据与列装饰扫描范围
- 仅在按下导航修饰键时更新悬停状态
- 避免同一数据库上下文重复重建装饰并补充回归测试
2026-07-13 12:54:02 +08:00
Syngnat
1ae2b74279 🐛 fix(sql-parser): 修复尾随注释导致事务与只读判定异常
- 过滤分号后的纯注释语句并保留数据库可执行版本注释
- 前后端按数据库方言统一处理双横线、井号与块注释
- 修复事务选择、只读保护、SQL 审计及 AI 风险分析误判
- 补充流式 SQL、事务执行与方言解析回归测试
2026-07-13 12:52:56 +08:00
Syngnat
00473e7ac0 🐛 fix(theme): 提升连接状态与 SQL 关键字辨识度
- 为亮暗主题和内置预设增加独立连接状态语义色
- 强化 Host、数据库状态点及双行页签字段对比度
- 为 Monaco SQL 关键字补充高对比加粗规则
- 增加基础主题与内置预设的对比度回归测试
2026-07-13 09:54:23 +08:00
Syngnat
7bdce19d01 feat(release): 发布 Windows Portable 与 MSI 双产物
- Windows AMD64 与 ARM64 同时生成 Portable EXE 和 MSI 安装包
- 统一 stable 与 dev 的 MSI 产品身份并支持通道切换升级
- 发布清单同时包含两类资产及对应 SHA256 校验信息
- 在 Windows workflow 中执行双模式更新与 WiX 契约测试
2026-07-13 09:25:21 +08:00
Syngnat
431f740476 feat(update-ui): 区分 MSI 与 Portable 更新交互
- 接收后端安装方式与包类型并隔离同版本更新缓存
- 按 MSI 与 Portable 动态展示下载、安装和重启状态
- 保留未保存 SQL 的退出保护并统一两处安装入口
- 补齐六语言文案及前端更新交互测试
2026-07-13 09:24:44 +08:00
Syngnat
6e2e3afc85 feat(update): 支持 Windows MSI 与 Portable 分流更新
- 根据当前安装标记精确选择 MSI 或 Portable 更新包
- MSI 更新下载到用户缓存并通过 msiexec 保留原目录安装重启
- 下载与安装前校验包类型并隔离不同安装方式的缓存
- 补充安装模式、脚本执行和更新清理测试
2026-07-13 09:24:13 +08:00
Syngnat
4d90ed776f 🐛 fix(sql-audit): 修复固定操作列悬停内容穿透
- 覆盖审计表格固定操作列的不透明背景
- 使用不透明混合色保留行悬停反馈
- 增加固定列内容穿透回归测试
2026-07-12 16:28:17 +08:00
Syngnat
7c8e8d8dd3 feat(sql-audit): 新增 SQL 审计中心并完善事务追踪
- 新增脱敏审计存储、筛选、保留策略、完整性校验与 JSON/CSV 导出
- 覆盖查询编辑器、事务、导入同步、对象操作、AI/MCP 与 Web 运行时入口
- 完善事务完整语句日志、Windows 快捷键映射及 SQL 分析布局
- 补充多语言、健康状态、数据目录迁移和回归测试
2026-07-12 15:24:27 +08:00
Syngnat
0b33b3a683 🐛 fix(query-editor): 修复 SQL Server 多查询冗余影响行数结果
- 将 SQL Server 成对返回的数据结果与 affectedRows 归属同一语句
- 保留 DML 影响行数与存储过程真实多结果集
- 补充后端正反用例及 V2 结果标签回归测试
2026-07-12 09:37:29 +08:00
Syngnat
ef5e344c4f feat(theme): 新增自定义主题与多套低刺激预设
- 支持导入、管理和持久化自定义 CSS 主题
- 新增六套内置主题并提供低刺激舒适暗色
- 完善 V2 主题令牌、多语言、可访问性与回归测试
2026-07-11 23:50:24 +08:00
Syngnat
034e968888 feat(sql-analysis): 完善慢 SQL 监控与执行计划诊断
- 完善慢查询采集、聚合、迁移与跨进程持久化

- 加固只读诊断边界并补齐多数据库执行计划解析

- 优化慢 SQL 面板、计划图、响应式布局与交互体验

- 补充后端、前端及 SQL Server 会话回归测试
2026-07-11 16:25:07 +08:00
Syngnat
5c80f6e9a2 ️ perf(sidebar): 优化左侧树搜索响应
延迟筛选配置持久化,避免输入期间触发全局状态更新。

稳定树标题渲染回调,减少可见节点重复协调。

补充搜索持久化与标题渲染回归测试。
2026-07-11 14:33:15 +08:00
Syngnat
cf979f436c feat(data-grid): 支持表头整列选择
- 单元格编辑模式下点击可写列表头选中整列数据

- 保留排序、筛选、外键跳转和列宽调整交互

- 补充表头整列选择回归测试

Fixes #622
2026-07-10 22:37:39 +08:00
Syngnat
328c9dbb28 feat(redis): 支持 List 单个值删除
- 为 Redis List 数据行增加删除操作与确认提示

- 新增 RedisListRemove RPC,使用 LREM 删除一个匹配元素

- 补充 Redis 命令、前端交互和本地化回归测试

Fixes #639
2026-07-10 22:21:08 +08:00
Syngnat
9870959ca6 🐛 fix(ui): 修复旧版编辑器布局并迁移默认界面至 V2
- 修复旧版 SQL 编辑器在弹性布局中的遮挡问题

- 将既有用户的界面偏好一次性迁移至 V2 并保留后续手动选择

- 覆盖持久化迁移与旧版编辑器布局回归测试
2026-07-10 22:00:05 +08:00
Syngnat
1ac9103735 🐛 fix(data-grid): 补全数据编辑事务语句 2026-07-10 21:21:42 +08:00
Syngnat
69be54c1cf 🐛 fix(web): 补齐浏览器端连接导入导出 2026-07-10 20:57:44 +08:00
Syngnat
689467ec91 🐛 fix(datagrid): 修复结果横向滚动条拖拽卡顿
- 合并同一帧内的外部横向滚动位置,避免每个原生事件触发布局写入
- 拖拽期间禁止虚拟表异步偏移反写外部滑块,松开后统一对齐
- 覆盖丢失指针捕获、窗口失焦、结果集隐藏和卸载时的同步状态清理
- 补充横向滚动同步回归断言
2026-07-10 18:10:27 +08:00
Syngnat
be09808d1f 🐛 fix(query-editor): 修复快捷执行与 Windows 输入丢字
- 快捷执行仅由 Monaco 编辑器处理,保留 Shift+Home 反向选区和光标位置
- 缓冲并恢复 Windows 下丢失的可打印输入和延迟 IME 提交
- 降低 AI 行内补全预检与元数据刷新造成的编辑器输入抖动
- 补充快捷键、输入回退和 AI 补全回归测试
2026-07-10 18:09:56 +08:00
Syngnat
18f40753dd feat(milvus): 新增 Milvus 向量数据库支持
- 内置 REST v2 驱动,支持集合浏览、向量检索和实体写入
- 支持认证、TLS、SSH 代理及多数据库连接切换
- 增加连接配置、图标、多语言文案和使用说明
- 补齐 JSON 查询路由与前后端回归测试
2026-07-10 15:37:54 +08:00
Syngnat
b23c1ba932 🐛 fix(query-results): 修复复杂查询固定列失效
为无物理表引用的结果集生成独立固定列作用域。

右键固定和取消固定不再依赖 tableName,保留只读编辑边界。

覆盖多行单表和关联查询的固定列回归。
2026-07-10 14:46:38 +08:00
Syngnat
e1fb5b12d4 🐛 fix(update): 修复 Windows 在线更新中文路径失效
- 将 Windows 更新脚本统一为 PowerShell,并通过 Unicode 环境变量传递中文与特殊字符路径
- 保留当前可执行文件路径,补充替换重试、启动健康检查、失败回滚和严格 ZIP 选择
- 更新前复用未保存 SQL 确认流程,并延长退出兜底时间
- 新增 Windows 原生集成测试并接入 dev/release 构建
2026-07-10 13:31:34 +08:00
Syngnat
06bfbc4a84 🐛 fix(window): 修复 Windows 后台可见时输入卡顿
- Windows 默认关闭透明 WebView、窗口透明与 Acrylic 背景
- 保留 macOS 透明效果并补充跨平台窗口视觉配置回归测试
- 同步更新多语言性能模式提示与开发文档
2026-07-10 11:16:34 +08:00
Syngnat
8d186fc066 feat(web-auth): 支持六位密码与环境变量托管
- 密码策略统一为至少六个可见字符,覆盖初始化与设置中心改密
- 支持 GONAVI_WEB_PASSWORD 启动同步并保留既有 2FA 与会话配置
- 设置中心识别环境托管状态并禁用临时改密
- Compose、env 示例及双语文档补充密码配置和容器重建说明
- 增加认证持久化、环境覆盖、前端状态与 Docker 登录回归校验
2026-07-10 10:34:03 +08:00
Syngnat
0a9aec5929 🐛 fix(docker): 修复容器驱动重装提示不消失
- Web 与 MCP 镜像编译前按目标平台生成驱动 revision
- 校验生成步骤位于源码复制后和 Go 编译前
- Docker 镜像工作流增加运行时 Dockerfile 回归检查
2026-07-10 10:04:38 +08:00
Syngnat
155ba0f1f7 🐛 fix(mcp): 修复在线更新后外部客户端连接失效
- 启动时自动迁移 Claude Code 与 Codex 中失效的 GoNavi MCP 路径
- 兼容 Windows 版本化 EXE 改名及旧进程占用导致的残留路径
- 限制迁移范围,保留仍有效的稳定路径与自定义命令
- 补充路径迁移和误覆盖边界回归测试
2026-07-10 09:44:26 +08:00
Syngnat
52592ffd6c 🐛 fix(query-editor): 修复 Oracle 查询自动限行语法
- 将 Oracle/Dameng 查询编辑器自动限行从 FETCH FIRST 改为 ROWNUM 兼容写法

- 识别 ROWNUM 自动封顶 SQL,避免结果分页继续基于截断 SQL 查询

- 补充自动限行与结果分页回归测试,覆盖 Oracle/Dameng 分支
2026-07-09 17:37:09 +08:00
Syngnat
92aedde121 🐛 fix(db): 优化 SQLServer 首次查询耗时
- 为 SQLServer 连接池保留 1 条空闲连接,减少反复冷连接开销

- 复用 Oracle/OceanBase 的默认空闲连接常量,其他默认数据库池策略保持不变

- 新增连接池行为测试,覆盖 SQLServer 复用与默认池关闭 idle 连接
2026-07-09 17:18:12 +08:00
Syngnat
b2df94a3ba 🐛 fix(window): 修复 mac 窗口恢复越界 (#643)
## 问题描述

macOS 多屏环境下,普通窗口恢复/修正位置时可能被定位到当前屏幕外,尤其是在外接屏位于主屏左侧或上方、浏览器
`screen.availLeft/availTop` 出现全局偏移时更容易触发。

## 复现条件说明

该问题不是所有 macOS 用户都能稳定复现,更依赖特定显示器布局和窗口状态。高概率触发条件包括:

- 使用 macOS 多屏。
- 外接屏位于主屏左侧或上方,导致 `screen.availLeft` / `screen.availTop` 为负数或非零。
- GoNavi 处于普通窗口态,而不是最大化或全屏。
- 应用保存过窗口位置,并在重启、切屏、拔插显示器、focus/resize 后触发窗口 bounds 恢复或修正。

单屏 mac、外接屏仅位于主屏右侧、始终最大化/全屏打开,或未保存过异常窗口位置的用户通常不容易触发。

## 根本原因

Wails 的 `WindowSetPosition(x, y)` 在 macOS 上接收的是当前 monitor
可见区域内的局部坐标,而不是全局桌面坐标。原逻辑把 `window.screen.availLeft/availTop`
作为可见区起点传入恢复算法,导致保存的局部坐标和恢复时的全局偏移混用。

## 修复方案

- 新增 `resolveWailsWindowVisibleViewport`,集中处理 Wails 主窗口恢复使用的可见区域。
- macOS 下将可见区原点固定为当前 monitor 局部 `(0, 0)`,避免把浏览器 screen 全局偏移继续传给 Wails。
- 补充单元测试覆盖负 `availLeft` 的外接屏场景。
- 更新 App 窗口恢复源码断言测试。

## 变更文件

| 文件 | 修改说明 | 变更行数 |
|---|---|---|
| `frontend/src/App.tsx` | macOS 下使用 Wails 局部坐标解析窗口可见区 | +7 -7 |
| `frontend/src/utils/wailsWindowViewport.ts` | 新增 Wails 窗口可见区解析工具 | +52
|
| `frontend/src/utils/wailsWindowViewport.test.ts` | 覆盖 macOS 局部原点和
fallback 场景 | +43 |
| `frontend/src/App.tool-center.test.ts` | 同步窗口恢复源码断言 | +2 -1 |

## 合并注意事项

- **影响功能点**:桌面端普通窗口启动恢复、focus/resize 后窗口 bounds 修正。
- **验证方式**:
- `npm test -- --run src/utils/wailsWindowViewport.test.ts
src/utils/windowRestoreBounds.test.ts
src/utils/windowStartupLayout.test.ts src/App.tool-center.test.ts`
  - `npm run build`
- **回归风险**:低。变更集中在窗口可见区解析;非 macOS 平台保留原 `screen.availLeft/availTop` 处理。
- **回滚方式**:revert 此 PR 即可。
2026-07-09 16:31:44 +08:00
Syngnat
19572a3cc9 feature(query-result): 支持SQL执行结果差异比对 2026-07-09 16:28:59 +08:00
Syngnat
8d7e7a6ddf feat(query-editor): 结果页列元数据跨库加载并实心化保存按钮
- 结果集携带 metadataDbName/TableName,列类型注释从真实库表加载
- 只读查询也解析 tableRef,避免结果页丢元数据
- 选择列底色与固定列对齐;保存按钮改为蓝色实心主按钮样式
- 独立结果窗/还原路径同步元数据字段
2026-07-09 15:37:27 +08:00
Syngnat
af9f490172 feat(datagrid): 完善左固定列体验并统一选中主题色
- 虚拟表横滚:表头 sticky + 表体 translateX 补偿,勾选/行号/钉住列同步固定
- 支持列头钉住左侧、钉住列显示图标;序号列可拖宽且不参与排序
- 行选中与单元格选区统一 V2 绿色,固定列底色对齐 --gn-bg-panel
- 查询结果页启用列头筛选,并在客户端按条件过滤当前结果集
2026-07-09 15:37:20 +08:00
DurianPankek
099443fddc 🐛 fix(window): 修复 mac 窗口恢复越界 2026-07-09 15:25:48 +08:00
Syngnat
947277bdec 🐛 fix(query-editor): 修复限定名超链接并支持跨库/schema 解析
- 两段名兼容 MySQL 库.表与 PG/SQL Server 的 schema.表
- 三段名支持 database.schema.object,常见 schema 不当成库拉取
- SQL 变更时按引用库防抖补拉元数据并刷新装饰
- 修复元数据 key 提前占死导致取消后不再拉取、超链接全灭
- 补充限定名解析与收集逻辑单测
2026-07-09 14:23:09 +08:00
Syngnat
b1bd4bb5ec feat(datagrid): 数据预览与 SQL 结果支持行号显示及设置开关
- 新增 appearance.showDataTableRowNumber,默认开启,主题设置可切换
- 数据预览与查询结果表统一跟随该偏好显示行号列
- 行号列收窄为固定窄宽,少列时多余宽度只加到数据列避免撑宽
- 补充 i18n 与相关单测
2026-07-09 14:23:09 +08:00
Syngnat
d210b3f39c 🐛 fix(window): 加固 Windows 冷启动半窗,原生最大化并兜底铺满工作区
- Windows 启动 WindowStartState 改为 Maximised,默认尺寸提升到 1440×900
- 无记忆/历史 1024×768/覆盖率过低的普通窗记忆改为启动最大化
- Maximise 多次失败时铺满工作区兜底,避免残留浮动半窗
- 用户刻意拉大的普通窗仍先 Unmaximise 再恢复尺寸位置
2026-07-09 14:02:53 +08:00
Syngnat
901b436107 🐛 fix(connection-package): 导出/导入连接包携带 Redis DB 别名
- 连接包 payload 与导出选项支持 redisDbAliases(连接 ID → db 序号 → 别名)
- 导入返回 ConnectionPackageImportResult,一并恢复别名映射
- 前端导出时注入 appearance.redisDbAliases,导入后 merge 回 appearance
- 补充别名往返测试与 mergeRedisDbAliases 单元测试
2026-07-09 13:57:32 +08:00
Syngnat
097826508a 🐛 fix(update): 修复 dev→latest 安装后丢包且仍停在旧版的问题
- 安装成功且 relaunch 发出后再删除下载包,失败保留 latest 包便于手动安装
- macOS open 使用路径形式,避免 -a 传完整路径导致重启失败
- 明确覆盖「当前运行的 .app」(桌面 dev 包),避免误以为只更新了 Applications
- Windows 同样先启动目标再删 SOURCE,防止重启失败时只剩旧包
2026-07-09 13:45:17 +08:00
Syngnat
cdfbdd5dd5 feat(update): 下载进度到 100% 后由用户点击「重启应用更新」
- 取消 macOS 下载完成后自动安装/退出,Win/Mac 统一交互
- 进度弹窗在 100% 显示就绪提示,主按钮改为「重启应用更新」
- 关于页安装入口同步文案;点击后才执行安装脚本并重启
- 补充多语言与前端回归测试
2026-07-09 13:38:07 +08:00
Syngnat
d31bb978f2 🐛 fix(update/macos): 加固在线更新自动安装脚本与进程隔离
- 安装脚本 Setsid 脱离主进程,避免 Quit 后脚本被连带结束
- 等待宿主退出增加 120s 超时,防止无限挂起
- 支持 dmg/zip 解包定位 .app,稳健解析 Contents/MacOS 可执行文件
- 不再 rm -rf 脚本所在 STAGED 目录,降低安装中途自删风险
- 替换失败明确走管理员权限回退,增强 relaunch(open -n -a / nohup)
- 补充 macOS 更新脚本单测
2026-07-09 13:35:41 +08:00
Syngnat
d067e2c00a feat(update): dev 通道同步生成 latest-dev.json 静态清单
- dev-build 发版流程上传 latest-dev.json 到 dev-latest Release
- 客户端 dev 通道优先读取 releases/download/dev-latest/latest-dev.json
- build-release 本地打包按版本自动选择 latest / latest-dev
- SHA256SUMS 生成时跳过清单文件本身
2026-07-09 12:38:18 +08:00
Syngnat
3beab030f4 feat(update): 检查更新优先静态 latest.json,避免用户撞 GitHub API 限流
- 客户端优先下载 releases/latest/download/latest.json,再回退 REST API
- 成功结果写入磁盘缓存,限流/断网时可 stale 回退
- 静默检查增加节流;限流文案改为用户无需配置 Token
- CI 与 build-release 发版时生成 latest.json 清单
- 补充静态清单/缓存/节流单测与生成脚本测试
2026-07-09 12:35:12 +08:00
Syngnat
566339f09f 🐛 fix(window): 加固 Windows 启动最大化与窗口尺寸记忆落盘
- windowState/windowBounds 与启动偏好一致,立即写入本地持久化
- 启动最大化首次立即执行并加重试,成功后固化 maximized 状态
- 仅在 hydration 完成后恢复,避免默认 1024×768 半窗被误记
- 补充 store 与启动恢复相关测试断言
2026-07-09 12:35:12 +08:00
Syngnat
dec9b8317c ♻️ refactor(settings/ai): 聊天入口打开 AI 设置改走设置中心
- 从 AI 聊天面板打开设置时进入设置中心服务-AI 页
- 移除独立 AISettingsModal 挂载,复用 AISettingsContent
- 安全更新修复跳转同步走设置中心,关闭后可回到修复流程
- 更新设置中心与工具中心相关源码级测试断言
2026-07-09 12:19:22 +08:00
Syngnat
c77d338ab1 feat(ai-chat): 支持侧栏/独立窗默认打开、双向切换与尺寸记忆
- 新增 AI 独立浮动窗,标题栏可拖拽缩放并与侧栏双向切换
- 设置中可配置默认打开方式(侧栏/独立窗),偏好持久化
- 记忆独立窗上次位置与尺寸,再次打开自动恢复
- 优化输入区默认高度与底部模型/思考/用量工具条布局
- 去掉独立窗外层重复标题栏,由面板 header 统一承载操作
2026-07-09 12:18:51 +08:00
Syngnat
bc3f76dd73 feat(ai-chat): 思考强度移入聊天面板并按次请求下发
- 面板底部新增思考强度选择,按服务商 profile 动态选项
- ChatSendOptions 支持 thinkingIntensity,仅本次发送生效
- 从供应商表单移除思考强度配置,避免与面板控制重复
- 补齐多语言文案与相关前后端测试
2026-07-09 12:17:40 +08:00
Syngnat
4fcd7ff61f feat(ai): 支持 MCP 少量查数与分服务商思考强度,修复 DeepSeek Anthropic
- MCP HTTP 默认可注册 execute_sql,行数默认 50/上限 200,设置页可切换仅结构模式
- 思考强度按 OpenAI/Anthropic/DeepSeek/Gemini 体系映射档位与请求参数
- 自定义端点 DeepSeek Anthropic 自动纠正 base 路径并回传 thinking 块
- 补充前后端回归测试与多语言文案
2026-07-09 11:34:38 +08:00
Syngnat
f930ffe153 🐛 fix(docker/web-server): 修复前端构建缺失 shared/i18n 导致 CI 失败
- frontend 阶段改为 monorepo 布局 WORKDIR /src/frontend
- COPY shared/ 到 /src/shared,恢复 catalog/messages/translate 相对路径解析
- dist 产物路径同步为 /src/frontend/dist
- 新增 Dockerfile 布局回归校验脚本,避免再次漏拷 shared
2026-07-09 11:16:30 +08:00
Syngnat
26c22379ae 🐛 fix(window/theme/updater/query-editor): 修复 Windows 冷启动半窗与主题/更新/补全体验
- 窗口:最大化恢复加重试与启动宽限,无记忆尺寸时 Windows 居中大窗,冷启动主动 WebView2 settle
- 主题:v2 设置中心主题外观改扁平自定义控件,legacy 保持侧栏卡片布局
- 更新:启动发现更新改开设置中心关于页,下载失败支持 assets API 回退并硬化检查链路
- 补全:suggest 列表字段名优先完整显示,元数据空间不足时再省略
- 测试:补充 window/theme/updater/补全相关回归断言
2026-07-09 11:13:42 +08:00
Syngnat
4c9bff5e8c feat(web-server): 支持 Docker 部署浏览器 Web UI
- 新增 Dockerfile.web-server:前端构建 + 纯 Go 主程序 web-server 模式
- 提供 Compose/env 示例与本地源码构建 override
- CI 增加 gonavi-web-server 镜像构建、冒烟与 GHCR 发布
- 文档补充 Web 容器部署、数据卷与安全暴露说明
2026-07-09 09:56:56 +08:00
Syngnat
8808a5aa77 🐛 fix(dameng): 修复设计表加载字段因 comment 保留字别名失败
- GetColumns/GetAllColumns 将 AS comment 改为 AS col_comment
- 达梦 COMMENT 为保留字,AS comment 会触发 Error -2007 语法分析出错
- 结果映射优先读取 COL_COMMENT,兼容 COMMENT/COMMENTS
- 补充单测锁定禁止使用 AS comment 别名
2026-07-09 09:48:52 +08:00
Syngnat
6fb6674803 feat(tab/window): 完善Tab拖出独立窗口跟手预览与入场动效
- 结果Tab支持拖出拆分,拖拽过程抑制文本选中
- 抽取 DetachDragPreview:进度条/跟手预览/阈值高亮
- 工作区主Tab拖出复用同一套预览动效,松手位置跟手
- 浮动窗增加 scale 入场动画,增强拆出反馈
2026-07-09 09:44:53 +08:00
Syngnat
c1cf19ef67 🐛 fix(sidebar/kingbase): 修复函数列表重复叠加导致越刷越多
- 元数据 fallback:完整目录查询首次成功后停止,避免多条 SQL 结果叠加
- 函数去重:按 schema+name+type 大小写不敏感去重,折叠同名 overload
- 树节点 key 区分 func/proc,避免同名函数/过程 key 冲突引发虚拟列表刷屏
- 补充 Kingbase/MySQL 函数元数据加载单测锁定回归
2026-07-09 09:20:33 +08:00
Syngnat
de868a2763 feat(tab/window): 支持主工作区与结果Tab拆出为应用内浮动窗口并还原
- 新增 detached 会话态:主工作区窗口与查询结果窗口独立管理
- 主Tab支持右键拆出与垂直拖出,浮动窗可拖拽缩放并还原/关闭
- 结果Tab右键拆出为浮动结果窗,支持还原回源查询结果区
- 抽离 WorkbenchTabContent,QueryEditor 结果会话缓存避免拆出丢失
- 补充六语言文案与 detach/store 相关单元测试
2026-07-09 09:08:50 +08:00
Syngnat
5fc33102a9 🐛 fix(updater): 修复关于页正式版信息兜底
- 为关于页补充固定项目元数据兜底
- 过滤 Unknown/未知 等无效版本与作者信息
- 增加正式版 AppInfo 缺失时的回归测试
2026-07-08 21:37:10 +08:00
Syngnat
f74ad02b9d 🐛 fix(query-editor): 限制 macOS 搜索仅响应 Cmd+F
- 拦截 Monaco 内置 Cmd+E find-with-selection 行为
- 保留默认 Cmd+E 当前语句选择逻辑,禁用或改键时不触发搜索
- 增加 SQL 编辑器快捷键回归测试并加固 Monaco 测试包装
2026-07-08 20:30:31 +08:00
Syngnat
dca1e8a251 feat(updater): 优化关于页更新信息展示
- 关于页直接进入 GoNavi 内容详情,减少二次点击
- 将更新通道改为分段选择器并优化描述布局
- 保留后端发布发布时间元数据并在版本信息中展示
2026-07-08 20:30:05 +08:00
Syngnat
22fab86010 🐛 fix(ui): 修复数据表按钮与侧栏滚动条显示
- 修复 v2 分页“统计总数”按钮被压缩为图标宽度的问题

- 首次打开时隐藏树主体横向溢出,保留独立横向滚动条占位

- 将自定义字段显示里的全局隐藏列文案改为多语言翻译

- 增加数据表按钮和侧栏横向滚动条布局回归断言
2026-07-08 17:37:21 +08:00
Syngnat
da895ea502 feat(settings): 统一设置中心内页与代理交互
- 将主题设置、AI 设置和关于页纳入设置中心详情页,补齐返回和取消交互

- 重构全局代理设置为草稿应用模式,新增常用预设、URL 测试入口和测试结果展示

- 按参考样式重做关于页版本与项目入口布局,并收紧顶部品牌卡尺寸

- 提取 AISettingsContent 供设置中心内嵌复用,控制详情页内部滚动边界

- 补齐多语言文案和设置中心静态回归断言
2026-07-08 17:37:05 +08:00
Syngnat
b94168957e 🐛 fix(update/proxy): 稳定更新检查与代理运行时
- 更新检查前恢复持久化全局代理,避免重启后代理状态未进入运行时

- 将 DNS、EOF 和临时网络异常转为本地化错误,并对可重试网络错误做短重试

- 优先读取 GitHub asset digest,缺失时再回退 SHA256SUMS 校验文件

- 新增全局代理连接测试后端接口,支持草稿代理、已保存密码复用和清除密码意图

- 更新 Wails 绑定、浏览器 mock 与代理草稿序列化测试
2026-07-08 17:36:42 +08:00
Syngnat
c792a30932 🐛 fix(query-result): 修复SQL执行后结果面板崩溃
- 提前解析当前激活结果页 key
- 避免构造结果 tab 时读取未初始化变量
- 修复 MySQL SELECT 成功返回后编辑器闪退问题
2026-07-08 14:16:36 +08:00
Syngnat
dd01d1c3f9 fix(update): auto-install macOS updates after download 2026-07-08 13:49:00 +08:00
Syngnat
23c895e839 test(update): auto install macOS package after download 2026-07-08 13:43:13 +08:00
Syngnat
7a5960e737 fix(update): auto install macOS package after download 2026-07-08 13:41:45 +08:00
Syngnat
5c6110a57e feat(query-result): support global hidden columns 2026-07-08 12:29:58 +08:00
Syngnat
17843ffd71 chore: remove temporary file 2026-07-08 12:26:21 +08:00
Syngnat
7296ab0425 tmp stop 2026-07-08 12:26:05 +08:00
Syngnat
47664023ee chore: remove temporary file 2026-07-08 12:25:57 +08:00
Syngnat
33918c6837 no 2026-07-08 12:25:42 +08:00
Syngnat
66a6052f88 chore: remove temporary file 2026-07-08 12:25:35 +08:00
Syngnat
31b5b838a2 chore: tmp create pr 2026-07-08 12:25:23 +08:00
Syngnat
575a3d88b9 chore: remove temporary PR trigger file 2026-07-08 12:25:07 +08:00
Syngnat
bef811ce13 tmp 2026-07-08 12:24:53 +08:00
Syngnat
dee5620fc9 test(data-grid): cover global hidden column helpers 2026-07-08 12:24:27 +08:00
Syngnat
b1174861d4 feat(query-result): apply global hidden columns 2026-07-08 12:24:12 +08:00
Syngnat
a1f296ee08 feat(data-grid): expose global hidden column configuration 2026-07-08 12:20:54 +08:00
Syngnat
3e34795ef5 feat(data-grid): add global hidden column storage helpers 2026-07-08 12:19:47 +08:00
Syngnat
f73d8dcb97 fix(oceanbase): tolerate Oracle sequence metadata differences 2026-07-08 12:18:44 +08:00
Syngnat
82ad393a05 perf(oceanbase): use FETCH FIRST for Oracle auto limit 2026-07-08 12:18:32 +08:00
Syngnat
257406120d fix(editor): recover dropped printable Monaco input 2026-07-08 12:18:20 +08:00
Syngnat
9ba3216fa8 test(oceanbase): cover Oracle sequence metadata fallbacks 2026-07-08 12:05:48 +08:00
Syngnat
fca3f0cffb fix(oceanbase): make Oracle sequence metadata queries tolerant 2026-07-08 12:05:14 +08:00
Syngnat
5712b39cf9 test(oceanbase): expect Oracle auto limit FETCH FIRST 2026-07-08 11:52:06 +08:00
Syngnat
07dc1436af perf(oceanbase): use FETCH FIRST for Oracle auto limit 2026-07-08 11:51:05 +08:00
Syngnat
9e52397732 fix(editor): recover dropped printable Monaco input 2026-07-08 11:35:00 +08:00
Syngnat
20ff4f3687 fix(query-result): show capped result counts as exact totals 2026-07-08 11:32:01 +08:00
Syngnat
73ce3039ce fix(oceanbase): improve query limit and Oracle object links 2026-07-08 11:31:29 +08:00
Syngnat
c27a038fc5 test(query-result): auto limit cap has exact page total 2026-07-08 11:08:03 +08:00
Syngnat
c3c4efc6ec fix(query-result): avoid not-counted summary for auto limit cap 2026-07-08 11:07:31 +08:00
Syngnat
61296bc855 chore: remove OceanBase follow-up note from focused fix 2026-07-08 10:44:43 +08:00
Syngnat
78da0076cf chore: remove temporary OceanBase follow-up note 2026-07-08 10:44:34 +08:00
Syngnat
279b38fec3 docs(oceanbase): note first implementation batch 2026-07-08 10:44:06 +08:00
Syngnat
0e348fcca4 test(query-editor): allow literal 100-row limit label 2026-07-08 10:43:46 +08:00
Syngnat
7d79fae84d fix(query-editor): use literal label for 100-row option 2026-07-08 10:43:02 +08:00
Syngnat
f642e0325d chore: remove temporary max rows i18n note 2026-07-08 10:40:20 +08:00
Syngnat
43a408f1c9 docs(query-editor): note 100-row option i18n key 2026-07-08 10:40:05 +08:00
Syngnat
b97b5cf6e2 docs(oceanbase): track remaining follow-up issues 2026-07-08 10:39:55 +08:00
Syngnat
6c269a99cf fix(oceanbase): support multi-part Oracle table links 2026-07-08 10:39:32 +08:00
Syngnat
24841f2dd9 test(query-editor): require 100-row limit label key 2026-07-08 10:37:10 +08:00
Syngnat
c2e60c1e52 feat(query-editor): add 100-row limit option 2026-07-08 10:36:38 +08:00
Syngnat
7a87d43bdc chore: remove temporary macOS CI annotation cleanup workflow 2026-07-08 09:43:21 +08:00
Syngnat
5fa1c90674 chore: trigger macOS CI annotation cleanup workflow 2026-07-08 09:42:18 +08:00
Syngnat
c79225ef27 chore: add temporary macOS CI annotation cleanup workflow 2026-07-08 09:41:44 +08:00
Syngnat
0bd8fdea1f chore: remove temporary macOS CI annotation cleanup workflow 2026-07-08 09:40:30 +08:00
Syngnat
18dbd043ae chore: temporarily apply macOS CI annotation cleanup 2026-07-08 09:39:28 +08:00
Syngnat
a7535dec2b fix(update): repair Go syntax in Windows update cleanup 2026-07-08 08:46:42 +08:00
Syngnat
dbd4b7f763 fix(update): repair Go multiline boolean expression 2026-07-08 08:44:15 +08:00
Syngnat
68eacbbcf7 fix(update): preserve latest Windows portable filename 2026-07-07 22:47:05 +08:00
Syngnat
cecefce84d fix(update): keep script builder test compatibility 2026-07-07 21:35:02 +08:00
Syngnat
2406c1ff71 fix(update): relaunch versioned portable exe with new filename 2026-07-07 21:28:21 +08:00
Syngnat
a4a0ac25cf fix(update): clean Windows update packages and logs 2026-07-07 20:54:13 +08:00
Syngnat
fbb23f0b9a fix(update): import logger for cleanup launcher 2026-07-07 20:50:57 +08:00
Syngnat
016e2bc70e chore(update): remove unused cleanup imports 2026-07-07 20:46:14 +08:00
Syngnat
070c2328e0 test(update): cover Windows cleanup helpers 2026-07-07 20:43:40 +08:00
Syngnat
c117da42e4 fix(update): clean staged Windows packages and logs 2026-07-07 20:42:15 +08:00
Syngnat
950856d86f 合并拉取请求 #627
fix: improve OceanBase Oracle performance, navigation, and count UX
2026-07-07 17:51:42 +08:00
Syngnat
93a3240862 fix(query-editor): fallback ctrl-click for OB Oracle schema tables 2026-07-07 17:47:45 +08:00
Syngnat
1116a3736b fix(data-grid): bridge pagination count action to toolbar 2026-07-07 17:45:37 +08:00
Syngnat
19af57e46b chore: remove temporary patch workflow 2026-07-07 17:36:48 +08:00
Syngnat
401c33c505 chore: apply OB Oracle performance navigation count patches 2026-07-07 17:33:59 +08:00
Syngnat
8c968c1a87 feat(data-grid): expose total count action in pagination 2026-07-07 17:31:53 +08:00
Syngnat
4fee58999d perf(db): keep one idle Oracle-style connection 2026-07-07 17:29:17 +08:00
Syngnat
bd0752df96 test(oceanbase): keep Oracle default timeout unset 2026-07-07 17:28:57 +08:00
Syngnat
47a15a6674 fix(oceanbase): stop inflating Oracle query timeout by default 2026-07-07 17:28:10 +08:00
Syngnat
2a64bad76e fix(oceanbase): reduce Oracle long-query bad connection failures 2026-07-07 16:56:05 +08:00
Syngnat
7ee8698165 test(query-editor): cover bad connection timeout semantics 2026-07-07 16:50:57 +08:00
Syngnat
9ae35e5fec fix(query-editor): classify driver bad connection as timeout 2026-07-07 16:49:39 +08:00
Syngnat
b618927454 test(oceanbase): cover Oracle query timeout default 2026-07-07 16:48:27 +08:00
Syngnat
e8ad34d2f0 fix(oceanbase): extend default Oracle query timeout 2026-07-07 16:47:50 +08:00
Syngnat
007fdc9a5d fix(oceanbase): set current schema for Oracle tenant queries 2026-07-07 15:44:14 +08:00
Syngnat
61b62fe5af test(oceanbase): cover Oracle current schema init 2026-07-07 15:41:22 +08:00
Syngnat
d574e6b396 fix(oceanbase): set Oracle current schema for selected tenant 2026-07-07 15:40:36 +08:00
Syngnat
f798b183e6 fix(query-editor): prevent AI inline completion from trapping cursor 2026-07-07 15:14:30 +08:00
Syngnat
e8ef4b824a fix(query-editor): prevent AI inline completion from trapping cursor 2026-07-07 15:10:45 +08:00
Syngnat
d87f8bf36a ♻️ perf(query-editor): 消除大库元数据下补全路径的全量扫描
延续上一轮 AI 补全卡顿修复:AI 元数据预热把整库列灌入共享缓存后,仍有三处热点会
阻塞主线程——buildQueryEditorAiContext 每次请求全量重建合并 Map、普通补全
findPreloadedColumns/relevantColumns 对全量列逐条正则扫描、ghost 可见期间
scroll/layout 每帧全量重渲染 overlay。改为:AI 上下文按依赖引用相等缓存(并保持
数组身份稳定使下游索引跨请求复用)、共享列按表名/标识符建 WeakMap 索引、
重定位用 rAF 合并。
2026-07-06 22:25:39 +08:00
Syngnat
50b4169148 🐛 fix(query-editor): 修复大库下SQL AI补全阻塞主线程导致全局卡顿
根因:全库列元数据加载后,每次内联补全在主线程对全部列做 O(列数×表数) 正则匹配,
且补全上下文被重复构建两次,8万列规模下单次请求耗时约900ms;同时 table_name 意图
每次补全都真实查库。改为按表名末段建索引(WeakMap按请求缓存)、复用已收敛上下文、
warmup 成功后会话内缓存,单次请求耗时降至约12ms。
2026-07-06 20:29:23 +08:00
Syngnat
4a63a175e4 🐛 fix(update): Windows在线更新改为覆盖当前exe并自动重启 2026-07-06 19:36:43 +08:00
Syngnat
2508b7935a 🐛 fix(update): 修复Windows在线更新复用旧安装包 2026-07-06 17:40:45 +08:00
Syngnat
22697baa37 🐛 fix(query-editor): 修复SQL AI补全误触普通补全 2026-07-06 17:26:41 +08:00
Syngnat
2fc587fd31 🐛 fix(frontend): 修复开发启动前端构建报错
- 修正 SQL 草稿快照定时器句柄类型兼容浏览器与 Node 环境
- 删除连接类型目录里重复的 trino 分支避免构建告警
- 配合可用 GOPROXY 验证 wails dev 已能完成前端编译与打包
2026-07-06 17:12:46 +08:00
Syngnat
2a3bd5bd97 🐛 fix(db): 修复Oracle同义词字段注释回填
- 元数据查不到列定义时补查 ALL_SYNONYMS 解析真实表
- 对解析出的 owner 和 table 继续回查列注释与主键信息
- 新增 SBDEV 指向 DEV.PERSON_INFO 的字段注释回归测试
2026-07-06 16:59:46 +08:00
Syngnat
ac68729734 🐛 fix(query-editor): 修复Oracle匿名块事务托管缺失
- 区分 BEGIN 事务控制语句与 Oracle 匿名块起始
- 让包含 DML 的 Oracle like 匿名块进入 SQL 编辑器托管事务
- 补充匿名块进入事务与回滚收口的后端回归测试
2026-07-06 16:59:17 +08:00
Syngnat
19c10f2369 🐛 fix(db): 修复 OceanBase Oracle 空结果并收敛提示
- OceanBase Oracle 只读查询优先走普通 Query 路径
- 补齐普通执行与托管事务两条回归测试
- 调整多语句回退提示避免误导为协议不支持
- 同步前端注释与多语言文案
2026-07-06 15:38:42 +08:00
Syngnat
8af8dd1ae0 🐛 fix(update): 修正 Windows 在线更新落盘与重启逻辑
- Windows 更新包直接下载到当前 EXE 目录
- staging 脚本目录移到临时目录避免暴露隐藏目录
- 安装时直接拉起新下载 EXE 并保留 zip 兼容分支
- 补充 Windows 更新路径回归测试
2026-07-06 15:37:56 +08:00
Syngnat
258fccd56f 🐛 fix(query-editor): 修复 OceanBase 执行误用登录Schema
- 保持 OceanBase Oracle 默认登录 schema 解析不变
- 未限定表名改写时增加登录 schema 到所选 schema 的回退候选
- 补充 SBDEVREAD/SBDEV 与 ORCLPDB1 场景回归测试

Fixes #615
2026-07-06 10:30:00 +08:00
Syngnat
93dc696e90 🐛 fix(db): 修复达梦DDL缺失字段注释 Fixes #615
- 补齐达梦列元数据查询中的字段注释读取
- 保留原始 CREATE TABLE 结果并追加 COMMENT ON COLUMN 语句
- 修正达梦列定义映射时 COMMENT 丢失的问题
- 新增达梦 DDL 注释补全回归测试
2026-07-06 08:56:41 +08:00
Syngnat
00d70d2934 🐛 fix(update): 修复 Windows 更新安装卡住并将安装包落到应用目录
- Windows 安装前预检当前安装目录写权限,避免脚本启动后主进程先退出
- 已下载更新补充打开安装目录入口,便于手动执行安装包
- 更新工作区优先改为当前应用运行目录,并补充回归测试与多语言文案
2026-07-05 20:06:51 +08:00
Syngnat
2f39767211 🐛 fix(update): 修复下载进度标题显示内部键并消除通道切换竞态
- 下载进度状态拆分 key 与 version 字段,弹窗标题恢复显示纯版本号
- SetUpdateChannel 将检查、持久化与缓存清理并入同一临界区,消除下载窗口期竞态
- 通道配置文件改为临时文件加重命名的原子写入
- 移除 useAppUpdateManager 未使用的 isMacRuntime 参数
2026-07-05 17:13:28 +08:00
Syngnat
5beb4c3ce4 feat(data-grid): 无主键表支持全列匹配编辑并移除只读限制
- 新增 all-columns 全列匹配行定位策略,无主键/唯一索引时不再强制只读
- 元数据加载失败、索引查询失败等误报场景全部降级为全列匹配编辑
- 修复 resolveQueryLocatorPlan 同步解析异常导致 SQL 结果页签不出现的问题
- 行安全由后端影响行数校验兜底,非单行命中即回滚整个事务
- 同步更新六语言文案与相关测试用例
2026-07-05 16:00:25 +08:00
Syngnat
8280f342a8 🐛 fix(mcp-server): 修复 arm64 镜像误装 amd64 二进制
- 移除 TARGETOS/TARGETARCH 的默认值,BuildKit 仅在无值声明时注入实际目标平台
- 原默认值 amd64 使 arm64 镜像内二进制架构错误,运行时报 exec format error
2026-07-05 12:13:56 +08:00
Syngnat
e5baf9ead6 feat(update): 支持 latest 与 dev 更新通道切换
- 新增更新通道持久化与按通道检查 GitHub Release
- 关于页支持切换通道并隔离本地更新缓存状态
- 补充多语言文案、Wails 绑定与前后端测试覆盖
2026-07-05 12:08:45 +08:00
Syngnat
06c4222aff ️ perf(ci): Docker 镜像改为原生 runner 分平台构建
- 构建矩阵拆分为 2 镜像 × 2 平台,arm64 使用 ubuntu-24.04-arm 原生 runner,移除 QEMU 模拟
- 各平台独立冒烟测试后以 digest 推送,merge job 合成多平台 manifest 并打标签
- 构建缓存 scope 按镜像-平台划分,避免跨平台缓存混用
2026-07-05 12:03:57 +08:00
Syngnat
f57f9913d5 🐛 fix(ci): 修复 build-env 镜像 npm 符号链接被 COPY 解引用
- 移除对 npm/npx/corepack 的直接 COPY,改为拷贝 node_modules 后重建符号链接
- COPY 会解引用符号链接,导致脚本内相对路径 require 解析失败
2026-07-05 11:40:14 +08:00
Syngnat
579891a53a 🐛 fix(ci): 修复 build-env 镜像 login shell 下 Go 工具链缺失
- 新增 /etc/profile.d 脚本,在 login shell 重置 PATH 后恢复 Go 工具链路径
- 修正 ENV PATH 中无效的 /root/go/bin 为 GOPATH 实际路径 /go/bin
2026-07-05 11:19:56 +08:00
Syngnat
061f78b897 🐛 fix(ci): 修复 mcp-server 本地 replace 模块分层构建
- 在 go mod download 前补拷贝 third_party/highgo-pq 与 third_party/go-irisnative 的 go.mod 依赖信息\n- 保留 Docker layer 缓存,同时兼容本地 replace 模块的依赖解析\n
2026-07-05 11:00:23 +08:00
Syngnat
6671915cf7 🐛 fix(ci): 修复 Docker 镜像构建基础环境
- 将 mcp-server 构建镜像升级到 Go 1.25,匹配当前 go.mod 版本要求\n- 调整 build-env 的 Node 资产复制方式,避免覆盖 Go 工具链\n- 显式补齐 /usr/local/go/bin 到 PATH,修复 smoke test 中 go 不可用\n
2026-07-05 10:57:07 +08:00
Syngnat
0cfcfa77d3 feat(mcp-server): 新增容器化部署支持
- 新增 MCP Server 的 Dockerfile、Compose 环境示例与 GHCR 镜像流水线\n- 补充 Podman Quadlet、Kubernetes Kustomize 与 Helm Chart 部署样例\n- 完善 MCP Server 独立 README,补充本地与远端 Agent 接入说明\n\nFixes #618
2026-07-04 21:57:21 +08:00
Syngnat
9e6a56a77b 📝 docs(readme): 补充 AI MCP 与数据源能力说明
- 补全中英文主 README 的 AI 与 MCP 工作流说明\n- 新增安全边界说明,覆盖 schema-only 与 allowMutating 约束\n- 补齐向量库、消息队列、国产数据库及时序库等数据源清单\n\nRefs #618
2026-07-04 21:56:10 +08:00
Syngnat
714c6fc553 feat(web-server): 新增浏览器访问认证与初始化流程
- 新增 web-server 运行模式与浏览器端运行时桥接
- 支持管理员密码、会话策略、Google Authenticator 与恢复码
- 设置中心补充浏览器访问认证状态与改密入口
- 优化初始化向导,未启用 2FA 时跳过验证器步骤

Refs #618
2026-07-04 21:50:52 +08:00
Syngnat
d7e15f5eb0 feat(release): 补齐 Linux arm64 构建与驱动发布链路
- 增加 dev/release 工作流 Linux arm64 构建矩阵与驱动分流
- 补齐在线更新资产识别及 driver release manifest/asset 校验
- 兼容 Windows 本地 bash/python 解析并补充对应测试
2026-07-04 10:15:13 +08:00
Syngnat
b388b08226 feat(sql-file-execution): 外部SQL执行切换工作台并优化大文件链路
- 前端将运行外部SQL改为工作台Tab并复用导出式进度视图

- 后端新增仅选择文件元数据接口并优化流式读取与批量拼接

- 补充执行Runner、国际化文案和定向测试
2026-07-03 22:11:49 +08:00
Syngnat
3bf42a169c 🐛 fix(sidebar): 修复侧边栏拖拽顺序重启后丢失
- 保留启动恢复阶段未回填连接时的 sidebarRootOrder
- 避免 hydration 过早裁剪连接根节点排序 token
- 补充重启后连接回填场景回归测试
2026-07-03 21:39:40 +08:00
Syngnat
29ee21e9d7 feat(query-editor): 增强 SQL 编辑器 AI 内联补全与独立模型配置
- 新增独立内联补全模型配置与服务端透传
- 优化 SQL 编辑器 Alt+\\ 触发、ghost 延续与对象位补全
- 引入基于已存查询和执行日志的 SQL 记忆补全
- 补充前后端本地化、快捷键与补全回归测试
2026-07-03 20:44:33 +08:00
Syngnat
5eafd2aa3c feat(query-editor): 支持 AI 行内补全与文本生成 SQL
- 新增 SQL 编辑器 AI 辅助服务,接入已配置供应商并生成行内补全

- 增加 Text-to-SQL 弹窗与插入、替换选区、替换全文应用模式

- 补齐多语言文案和定向测试覆盖
2026-07-02 21:36:44 +08:00
Syngnat
b0f077ffbb 🐛 fix(table-designer): 优化结构设计器元数据加载性能
- 字段元数据优先加载并拆分各页签 loading 状态

- 收窄 Oracle 与达梦约束字典查询范围

- 补充结构设计器与字典查询回归测试
2026-07-02 21:15:40 +08:00
Syngnat
0fe3060cf1 🎨 style(query-editor): 优化补全候选元数据显示
- 候选行未选中时也显示元数据,避免只有当前选中项可见
- 将字段名与元数据改为紧凑排列,减少候选列表空间浪费
- 增加表备注、字段类型备注、当前库隔离和候选样式回归断言
2026-07-02 17:50:56 +08:00
Syngnat
c2eaf27c20 🐛 fix(query-editor): 修复补全元数据来源与库范围
- 限定普通表名补全只使用当前选中库,避免其他库表污染候选列表
- 表名补全补充表备注,并在懒加载时回填共享表元数据
- 字段补全详情统一展示表名、类型和备注,文档区补充库表上下文
2026-07-02 17:50:48 +08:00
Syngnat
275d490ca7 🐛 fix(sidebar): 修复 Oracle 表元数据显示缺失
- Oracle/Dameng 表元数据查询移除 ALL_SEGMENTS 依赖
- 使用 ALL_TABLES.BLOCKS 轻量估算表大小,避免权限或大库聚合失败
- 保留表注释、行数、创建时间和最后 DDL 时间展示
- 补充 Oracle-like 元数据 SQL 回归断言
2026-07-02 15:21:45 +08:00
Syngnat
0707d60f0b 🐛 fix(sidebar): 放宽左侧树拖拽宽度上限
- 统一侧栏宽度常量,最大宽度放宽到 960px
- 拖拽时按窗口宽度预留主工作区空间
- 同步 v2 与 legacy 侧栏 CSS 最大宽度
- 持久化侧栏宽度时允许保存更宽配置并补充测试
2026-07-02 15:20:59 +08:00
Syngnat
9b5e785d67 feat(sidebar): 支持表元数据拖拽排序
- 设置中心元数据项支持拖拽调整展示顺序
- 新增 sidebarTableMetadataFieldOrder 持久化并按顺序渲染
- 保持表注释旧配置迁移与元数据开关兼容
- 补充排序、持久化和侧栏渲染回归测试
2026-07-02 15:19:44 +08:00
Syngnat
69d9f41085 feat(connection): 支持生产保护默认折叠
- 为连接配置分区卡片增加可折叠渲染能力

- 生产连接保护默认收起,仅保留标题说明和当前策略标签

- 补充中英文折叠态与展开态回归测试
2026-07-02 14:12:47 +08:00
Syngnat
ffb4d16950 🐛 fix(definition-viewer): 修复对象修改保留完整定义
- 统一对象查看页定义展示,补齐可复制的 CREATE OR REPLACE DDL

- 打开对象修改前清理同标签旧草稿,避免旧草稿覆盖完整注释

- 补充 Oracle 函数、过程和包定义的回归测试
2026-07-02 14:12:22 +08:00
Syngnat
68d4081fcf feat(sidebar): 支持表元数据显示配置
- 新增侧栏表元数据字段配置并兼容旧版表备注开关
- 设置中心新增侧栏表元数据入口并移除树上重复显示开关
- 批量补齐表大小、创建时间、修改时间等元数据加载与渲染
- 统一表元数据格式化逻辑并修复 V2 左树横向滚动裁切
- 补充设置中心、store、侧栏元数据相关回归测试与多语言文案
2026-07-02 12:54:43 +08:00
Syngnat
92eb4266da 🐛 fix(query-editor): 统一对象查看与修改字号
- object-edit 查询态补齐与查看页一致的 Monaco 字号和行高配置
- 统一对象查看与修改的行号宽度,避免视觉尺寸不一致
- 补充 QueryEditor 与 DefinitionViewer Monaco 选项回归测试
2026-07-02 10:47:08 +08:00
Syngnat
b220dd051f 🐛 fix(definition-viewer): 复用对象修改原标签页
- 查看页对象修改直接复用当前标签切换到 object-edit 编辑态
- 侧边栏视图和函数编辑入口统一补齐 queryMode 标记
- 补充 DefinitionViewer 与 store 回归测试锁定标签复用行为
2026-07-02 10:45:59 +08:00
Syngnat
e466e87fcc 🐛 fix(sidebar): 修正树节点加载与展开时序 2026-07-02 09:40:09 +08:00
Syngnat
0428a30071 🐛 fix(data-viewer): 延迟对象设计页的数据预览加载 2026-07-02 09:39:52 +08:00
Syngnat
faaf7169da 🐛 fix(query-editor): 修复 SQL 片段补全与插入交互
- 插入 SQL 片段后主动关闭片段弹窗,保持右键插入行为一致
- 提升 Monaco 补全详情区最小高度,放大片段说明展示区域
- 列补全解析改为基于当前语句引用上下文,修复当前语句前半段补全丢列问题
- 为 SQL 草稿持久化补齐 window 定时器兜底,避免测试环境 clearTimeout 缺失
- 更新 SQL 片段弹窗关闭和补全说明高度相关回归测试
2026-07-01 18:11:41 +08:00
Syngnat
04a61a50dc 🐛 fix(workbench): 统一 v2 工具中心与工作区主题
- 显式传递 uiVersion 构建 overlay theme,避免设置中心和工具中心混用旧版黄蓝色
- 统一 v2 模式下 Ant Design 主色和选中态为绿色语义
- 移除工具中心详情头部重复的返回上一步按钮
- 修复侧边树对象筛选反复切换时的 rc-tree 复用漂移
- 调整 v2 左侧树横向滚动条与首页空态布局,释放工作区展示空间
- 补充工具中心、主题 token 和侧边树 remount 的回归测试
2026-07-01 18:11:08 +08:00
Syngnat
749e543e37 🐛 fix(query-editor): 修复 SQL 片段插入与弹窗交互
- 在 QueryEditor 右键菜单中稳定注册插入 SQL 片段动作,并随语言切换刷新文案,避免入口偶发消失
- SQL 片段选择器改为统一的可拖拽可缩放弹窗,优先直连 Monaco snippetController 插入并保留回退链路
- 选中片段后保持弹窗打开以便连续插入,同时补充入口注册、延迟 onMount 与多语言文案回归测试
2026-07-01 11:03:48 +08:00
Syngnat
d94335dc62 feat(query-editor): 支持自定义新建查询默认 SQL
- 在 appearance 持久化设置中新增 newQuerySqlTemplate,支持默认回退与空白模板
- 新建空白查询标签页按用户模板初始化,不影响外部 SQL、保存查询和只读标签
- 在工具中心补充模板编辑与恢复默认入口,并完善 store、编辑器和 i18n 回归测试
2026-07-01 09:44:22 +08:00
Syngnat
e135956eba 🐛 fix(definition-viewer): 修复对象定义页滚动行号重叠
- 在 DefinitionViewer 只读 Monaco 实例上显式关闭 stickyScroll,避免滚动后出现悬浮代码行与行号叠层
- 同步关闭 TriggerViewer 的 stickyScroll,统一对象定义类只读查看器滚动表现
- 新增对象定义查看器 Monaco 选项回归测试,锁定只读定义页必须禁用 stickyScroll
2026-07-01 09:17:54 +08:00
Syngnat
66b14766a3 🐛 fix(query-editor): 修复补全范围与光标执行
- 仅按当前语句与光标前范围构建别名和表引用,避免跨语句泄露其他库表字段补全
- 在新语句起始关键字场景优先提升 SQL 关键字候选,避免关键字被字段建议压到后面
- Ctrl/Cmd+Enter 无选中内容时回退执行光标所在语句,不再误报没有可选择的 SQL 语句
- 补充补全排序、列范围和光标末尾执行的回归测试
2026-07-01 09:17:33 +08:00
Syngnat
923e789211 🐛 fix(elasticsearch): 修复 ES SQL 末尾分号导致空结果 Fixes #605
- 清理 parseESSQL 解析出的 WHERE 和 ORDER BY 子句尾部分号
- 避免 range 条件把数值误当成字符串传给 Elasticsearch
- 补充带分号的 ES SQL 端到端与解析回归测试
2026-06-30 22:28:17 +08:00
Syngnat
52eacdd9df 🐛 fix(sidebar): 修复表分组折叠后二次展开消失 Fixes #606
- 限制侧边栏折叠时的子树释放范围,只清理可重新懒加载的 connection/database 节点

- 保留 tables object-group 的已加载子节点,避免二次点击展开后表列表被清空

- 补充针对大表分组折叠场景的回归测试
2026-06-30 21:46:13 +08:00
Syngnat
a9bae6bb3f 🐛 fix(explorer): 补齐 Redis 部分导入与库节点加载态
- 新增 Redis 导入预览与按 key 勾选导入能力

- 后端支持导入文件预读、选中 key 过滤与本地化错误提示

- 打开数据库节点时同步展示加载态并补齐前后端回归测试

Fixes #607

Fixes #609
2026-06-30 21:17:00 +08:00
Syngnat
b0d8ccbf23 🐛 fix(definition-viewer): 修复对象定义页行号重影 2026-06-30 20:31:44 +08:00
Syngnat
4b18075314 🐛 fix(db): 修复达梦读查询结果空页
- 统一按解析后的驱动类型判断只读查询结果策略

- 达梦及 dm/dm8 自定义驱动只读查询优先走普通 Query

- 补充事务执行路径与达梦结果集回归测试
2026-06-30 20:06:57 +08:00
Syngnat
784d7d60e2 🐛 fix(query-editor): 修复Monaco搜索框与悬浮提示遮挡
- 移除 QueryEditor 中临时的 find widget DOM 监听与 offset zone 逻辑
- 为 V2 查询编辑器的 Monaco stage 增加搜索框可见态顶部安全区并放开必要溢出
- 调整分栏高度计算与回归测试,确保搜索框间距只作用于 V2 query-editor
2026-06-30 17:25:48 +08:00
Syngnat
ece1119c31 feat(query-editor): 增强行级快捷键并优化搜索框
- 接管 Cmd/Ctrl+E,选择当前行并复制,避免落到宿主搜索
- 新增 Cmd/Ctrl+D 复制当前行到下一行,并同步快捷键冲突校验
- 增加 macOS SQL 菜单桥接,确保原生 Cmd+E 回落到编辑器
- 保持 Monaco 内置搜索并下移,避免遮挡 SQL 第一行
- 补齐多语言文案与 QueryEditor/shortcut/main 回归测试
2026-06-30 15:25:01 +08:00
Syngnat
b9ffcd2959 🐛 fix(sidebar): 支持调节V2左侧导航按钮大小 2026-06-29 21:25:13 +08:00
Syngnat
d900bda756 test(sqlserver): 补强索引预览与DDL回归覆盖
Fixes #379
2026-06-29 20:47:01 +08:00
Syngnat
8ac435dc6d 🐛 fix(sidebar): 修复达梦表分组计数丢失
Fixes #412
2026-06-29 20:43:15 +08:00
Syngnat
99942e37d6 feat(database-ui): 补充表概览元数据并调整数据库菜单
Fixes #395
2026-06-29 20:35:59 +08:00
Syngnat
d2a535e6ff 🐛 fix(mysql): 补充 PolarDB-X 数据库列表回退查询
Fixes #455
2026-06-29 20:25:30 +08:00
Syngnat
61c2e524b4 feat(redis-viewer): 新增加载全部 Key 入口
Fixes #603
2026-06-29 20:17:01 +08:00
Syngnat
b434247838 🐛 fix(ddl): 补齐 kingbase 表DDL索引输出
- 在 SHOW CREATE TABLE 不可用时,fallback 额外拉取索引元数据并生成 CREATE INDEX 语句\n- 跳过与主键重复的索引,避免在 fallback DDL 中重复输出主键索引\n- 补充 kingbase DDL fallback 的索引回归测试\n\nFixes #601
2026-06-29 20:09:01 +08:00
Syngnat
b3f69ac401 🐛 fix(sidebar): 调整暗黑模式横向滚动条样式
- 为 v2 侧栏树横向虚拟滚动条补充暗色轨道与边界样式\n- 保持滚动条尺寸与 thumb 交互不变,仅修正暗黑模式视觉协调性\n- 补充侧栏主题样式断言覆盖\n\nFixes #602
2026-06-29 20:04:17 +08:00
Syngnat
93006d8423 feat(query-editor): 新增编辑器内搜索入口
- 在查询编辑器工具栏新增搜索入口并直连 Monaco 查找\n- 接管 Ctrl/Cmd+F,优先打开编辑器内搜索而不是浏览器默认查找\n- 补充多语言文案与静态测试覆盖\n\nFixes #539
2026-06-29 20:00:27 +08:00
Syngnat
a7fdab5bb2 feat(theme): 新增跟随系统主题模式
- 增加主题偏好持久化并兼容旧版主题状态恢复
- 监听系统深浅色变化并同步窗口与应用主题
- 补齐主题设置文案与持久化相关测试

Fixes #407
2026-06-29 19:52:22 +08:00
Syngnat
bedd1eceb5 🐛 fix(query-editor): 修复异常退出后SQL草稿丢失
- 为查询页签增加 crash-recovery 草稿快照并按 160ms 落盘
- 启动恢复时补齐缺失的 query tabs 与连接上下文
- 保存或关闭查询后清理临时草稿并补充恢复回归测试
2026-06-29 19:21:37 +08:00
Syngnat
dac9e1f571 🐛 fix(sidebar): 恢复批量库表危险操作
- 恢复批量表和批量库按钮到原批量操作弹窗\n- 补回批量删除表和删除库的危险确认与执行逻辑\n- 删除表仅处理 table 对象,避免视图误走 DropTable\n- 补齐多语言文案与入口/删除回归测试
2026-06-29 17:09:01 +08:00
Syngnat
aed87665f4 🐛 fix(query-results): 区分执行消息内容区域
- 为执行消息正文滚动区增加独立边框和浅背景
- 保留标题与复制按钮在顶部区域,正文内容单独成框
- 补充消息块样式断言,覆盖边框、背景和内边距
2026-06-29 15:18:35 +08:00
Syngnat
04bbab3d7e 🐛 fix(query-results): 修复金仓带模式表主键识别
- PG 类数据库保留 schema.table 作为查询结果表名和元数据表名
- 元数据连接继续使用当前数据库,避免把 schema 误当 database
- 补充金仓列、索引和前端可编辑定位回归测试
2026-06-29 15:18:13 +08:00
Syngnat
4798d3e8ec 🐛 fix(db): 修复金仓读查询结果空页
- 调整 PG 类数据库只读查询路径,优先使用普通 Query 返回真实结果集
- 避免金仓原生多结果集返回空行列时吞掉可展示数据
- 补充金仓读查询与原生空结果回退测试覆盖
2026-06-29 15:17:47 +08:00
Syngnat
c940e930bc feat(connection): 支持导入Navicat NCX连接 2026-06-29 14:16:17 +08:00
Syngnat
8280306f82 🔧 chore(wails): 同步数据库对象前端绑定 2026-06-29 13:45:34 +08:00
Syngnat
59e12b0867 🐛 fix(ui): 修复危险主按钮文字对比度 2026-06-29 13:38:07 +08:00
Syngnat
ded531a3c2 🐛 fix(sql-editor): 修复新建查询关闭应用未提示保存 2026-06-29 13:02:23 +08:00
Syngnat
3a471bc1aa 🐛 fix(mcp): 修复执行SQL结果不可见
- execute_sql 文本 Content 输出可读的 SQL 执行摘要和 Markdown 结果表\n- 保留 structuredContent 结构化结果,兼容机器解析客户端\n- 新增 in-memory MCP 调用回归测试,覆盖 select 1 可见结果
2026-06-29 12:32:07 +08:00
Syngnat
ea4f88a20d feat(sql-editor): 关闭应用时提示保存未保存SQL
- 关闭应用前拦截 Wails 退出事件,前端弹出确认退出、保存退出和取消三按钮\n- 保存退出支持外部 SQL 文件和已保存查询,未命名临时查询保留草稿恢复语义\n- 补充退出保护后端测试、前端保存目标测试和多语言文案
2026-06-29 11:16:55 +08:00
Syngnat
8e857b9aee 🐛 fix(query-results): 修复多结果集回退空结果页 2026-06-29 11:09:40 +08:00
Syngnat
f55a332ead feat(mcp): 扩展数据库对象结构读取工具
- 新增 get_views/get_objects 工具并让 get_tables 附带视图列表
- 后端统一返回表、视图、触发器、函数、过程、序列、包、事件和队列类对象
- 补充 MCP 服务测试、对象解析测试、远程 schema-only 文档和多语言文案
2026-06-29 10:25:37 +08:00
Syngnat
893a0f70fb 🐛 fix(query-results): 修复原生空结果集导致结果页缺失
- 后端对只读查询的原生多结果集空列空行结果增加逐条执行回退
- 前端执行成功后显式合并结果集并激活最新结果 tab
- 新增空原生结果集对象的回归测试,避免 SELECT 成功但结果页缺失
2026-06-29 10:23:58 +08:00
Syngnat
fdb05fb8d3 🐛 fix(i18n): 修复系统语言跟随不实时更新
Refs #335
2026-06-28 20:15:56 +08:00
Syngnat
730d4bb1ab 🐛 fix(tabs): 修复对象编辑页关闭后未回到来源标签
- 为 SQL 超链接打开的对象标签记录来源标签

- 关闭当前对象标签时优先回到来源标签

- 补充表对象与对象编辑标签关闭回退测试
2026-06-28 20:03:15 +08:00
Syngnat
fee703e879 🐛 fix(sqlserver): 修复消息循环缺失时吞掉查询结果
- 在 SQL Server 消息循环未产出结果集时兜底扫描 rows

- 保留 SELECT 返回的行数据和空结果列元数据

- 补充 SQL Server fallback 扫描回归测试
2026-06-28 16:41:12 +08:00
Syngnat
4f2f7003c8 ️ perf(ai-chat): 降低流式思考输出渲染开销
- 合并 AI 流式 token 刷新,减少高频状态写入

- 避免纯流式更新重排会话列表,并收窄当前会话订阅

- 补充 thinking 合并刷新和会话列表稳定性回归测试
2026-06-28 16:03:22 +08:00
Syngnat
97540206b3 feat(query-editor): 新增 SQL 美化快捷键 2026-06-28 12:08:40 +08:00
Syngnat
793e078676 🐛 fix(ci): 修复 Doris driver-agent revision 校验失败
- 统一 release/dev 构建流程中的 Doris/diros 驱动归一
- 保持 Doris 资产名为 doris,构建 tag 使用 gonavi_diros_driver
- 优化 driver-agent revision 校验失败日志,避免输出 JSON 解析 traceback
2026-06-28 08:35:05 +08:00
Syngnat
7453600ee5 feat(data-grid): 支持数据视图列头筛选 #490
- 列头筛选复用工具栏筛选状态,应用后同步条件但不自动打开筛选面板

- 修复列头筛选应用、清除和操作符下拉交互

- 补充筛选状态同步、主键查询和列头交互回归测试
2026-06-27 22:58:10 +08:00
Syngnat
6c53ef4eff feat(query-editor): 记住查询编辑区结果区比例
- 持久化查询编辑器与结果面板的分割比例
- 新开查询 Tab 时按已保存比例恢复编辑区高度
- 补充分割比例计算与 QueryEditor 回归测试

Fixes #538
2026-06-27 18:21:24 +08:00
Syngnat
60eb696859 🐛 fix(sidebar): 修复事件定义编辑入口 #540
- 事件右键编辑定义改为打开 object-edit 查询

- 拉取 CREATE EVENT 作为可编辑 SQL,避免只显示 SHOW CREATE EVENT

- 补充事件定义编辑与菜单入口回归测试

Fixes #540
2026-06-27 17:58:54 +08:00
Syngnat
e456925c23 feat(sidebar): 优化表备注悬浮信息展示
- 读取不同数据源表备注并写入左侧表节点
- 支持表分组菜单切换备注显示
- 表节点悬浮复用 Tab 信息卡并移除原生双提示

Fixes #569
2026-06-27 17:43:11 +08:00
Syngnat
038ecc8b70 🐛 fix(export): 兼容换行 SELECT 查询导出
- SELECT/WITH 只读判断支持关键字后换行
- 覆盖多表 JOIN 查询结果导出场景

Fixes #583
2026-06-27 16:41:35 +08:00
Syngnat
3bfcd15dc0 🐛 fix(data-grid): 允许未知总数结果跳页
- 未知总数分页保留跳页控件
- 跳页输入不再被当前估算页数截断

Fixes #584
2026-06-27 16:41:12 +08:00
Syngnat
6abdb8684f 🐛 fix(sidebar): 折叠海量对象树时释放子节点
- 折叠海量数据库/对象分组时清理子树缓存和后代展开态
- 避免虚拟树在大批量函数、过程等对象折叠后残留渲染

Fixes #587
2026-06-27 11:37:55 +08:00
Syngnat
080ae0986a 🐛 fix(window): 适配分辨率变化后的窗口边界
- 运行期检测普通窗口是否超出当前可用屏幕并自动收回
- 启动恢复时同步裁剪超过当前屏幕的窗口尺寸

Fixes #594
2026-06-27 11:17:42 +08:00
Syngnat
bfb61c8449 feat(redis): 优化数据库别名与计数展示
- Redis DB 标题不再重复拼接 key 数量
- 将别名单独渲染为浅色文本并支持设置后即时刷新

Fixes #593
2026-06-27 10:58:24 +08:00
Syngnat
23d7511f55 🐛 fix(sidebar): 单击打开数据库对象节点
- 支持视图、函数、触发器等对象单击直接打开定义 tab
- 覆盖查询日志打开后 CK/SQLite 查询结果仍切回数据结果

Fixes #595
2026-06-27 10:51:50 +08:00
Syngnat
2d73bcc6de 🐛 fix(query-editor): 跳过TDengine托管事务
Fixes #592
2026-06-27 10:41:28 +08:00
Syngnat
a24f4a2bc1 🐛 fix(query-editor): 限制快捷键仅执行选中SQL
Fixes #596
2026-06-27 10:36:46 +08:00
Syngnat
bf51003c66 🐛 fix(query-results): 修复执行消息面板布局异常 2026-06-27 10:21:39 +08:00
Syngnat
2df9dce78b 🐛 fix(query-results): 优化SQL执行消息展示
- 执行消息拆分标题栏和滚动正文区域,避免 SQL 文本被遮挡

- 消息正文关闭自动换行并保留原始空白,支持横向滚动查看完整 SQL

- 补充 SQLServer 消息展示和结果面板结构回归断言
2026-06-26 17:29:03 +08:00
Syngnat
3ed45fab41 🐛 fix(sidebar): 修复表默认打开行为与左树滚动
- 工具设置新增双击表名行为开关,默认打开表数据,可切换到对象设计

- 左侧树、表概览卡片视图和列表视图统一按开关打开数据页内对象设计

- 调整 V2 左侧树横向滚动条布局,使滚动条保持在底部可见
2026-06-26 17:28:49 +08:00
Syngnat
7c9cf95698 🐛 fix(query-editor): 修复对象跳转与存储过程补全
- 对象超链接支持表跳转到数据页内对象设计,并兼容 Ctrl/Cmd 修饰键状态丢失场景

- MySQL 补全增加 CALL 关键字和存储过程/函数名称提示,支持 CALL routine 跳转编辑

- DataGrid 支持按 tab 请求初始切换到对象设计视图,补充相关回归测试
2026-06-26 17:28:09 +08:00
Syngnat
af2c358822 🐛 fix(sidebar): 修复左侧树长名称滚动仍截断
- 调整侧栏树节点内容宽度,避免标题层继续被 ellipsis 截断

- 同步 V2 树标题和标签样式,横向滚动后可查看完整名称

- 补充样式回归测试,覆盖节点内容、标题和标签完整宽度
2026-06-26 15:43:43 +08:00
Syngnat
1226e544ec 🐛 fix(data-grid): 修复文本模式字段注释显示
- 文本记录视图接入列元数据与字段显示开关
- 字段名下方同步显示类型和注释,保持与表格列头设置一致
- DataGridShell 将查询结果列元数据传入文本视图
- 补充文本模式显示和隐藏字段元数据回归测试
2026-06-26 10:08:04 +08:00
Syngnat
faa7ed1ae2 🐛 fix(query-editor): 修复选择器与事务提示浮层
- host 和数据库下拉选项关闭原生 title,避免和自定义 Tooltip 重复显示
- 事务模式说明默认向上弹出,并保留边界自动避让
- 移除下拉打开时强制隐藏事务说明的状态控制
- 补充工具栏和事务设置回归断言
2026-06-26 10:06:56 +08:00
Syngnat
22cdeb677b 🐛 fix(query-editor): 修复对象超链接误定位左树 2026-06-25 22:29:09 +08:00
Syngnat
b210d078d0 test(data-grid): 补充 DDL 视图回归覆盖
覆盖切表常驻、侧栏布局继承和加载态保持

覆盖活动 DDL 入口隐藏重开与侧栏拖拽预览

覆盖 DDL 文本选择时保持横向视角的交互行为
2026-06-25 21:36:27 +08:00
Syngnat
ce30de20b6 🐛 fix(data-grid): 修复 DDL 视图常驻与侧栏交互
记忆 DDL 底部/侧栏布局并支持切表常驻刷新

修复侧栏关闭、拖拽预览和首次加载布局抖动

优化 DDL 行号 gutter,并保留文本选择时的横向视角
2026-06-25 21:36:14 +08:00
Syngnat
8dde4c3c6d 🐛 fix(tool-center): 移除内嵌工具重复标题头
- 工具中心详情页统一承载入口标题和说明
- 数据同步、表结构比对和数据比对嵌入时隐藏内部 hero
- 数据目录、快捷键、连接包和安全更新嵌入时隐藏内部 Modal 标题
- 补充工具中心全 pane 标题归属回归断言
2026-06-25 18:42:53 +08:00
Syngnat
bdb60a656a 🐛 fix(sidebar): 修复 V2 当前连接新建查询入口
- 在当前连接头部增加更明显的新建查询按钮
- 使用文档图标区分连接创建入口,避免与加号按钮混淆
- 通过连接树节点上下文触发新建查询,修复点击无响应
- 补充 V2 侧边栏入口位置与图标回归断言
2026-06-25 18:42:31 +08:00
Syngnat
8a56367d02 🐛 fix(query-editor): 优化工具栏长名称提示与片段弹窗布局
- SQL 编辑器连接和数据库选择器支持悬浮延迟显示完整名称
- SQL 片段管理嵌入工具中心时隐藏重复标题
- 缩小片段操作按钮并拆分滚动区域,避免底部内容被遮挡
- 补充工具栏和片段弹窗源码级回归断言
2026-06-25 18:42:11 +08:00
Syngnat
aebe9bab54 🐛 fix(sql-log): 统一 V2 SQL 日志入口
- V2 左侧入口改为打开当前工作区内嵌 SQL 日志,legacy 继续使用底部全局面板
- 表数据页新增 sqlLog 视图并复用嵌入式 LogPanel,避免无 SQL 编辑器时无法查看日志
- 移除 V2 侧栏底部重复 SQL 日志按钮,保留慢查询入口并补充回归测试
2026-06-25 17:39:57 +08:00
Syngnat
9ab31a7614 🐛 fix(oracle): 修复存储过程斜杠分隔执行截断
- 支持 SQL*Plus 斜杠分隔符后的可选分号,避免 Oracle 过程执行出空语句
- 光标落在过程异常尾部或斜杠分隔行时,仍选择完整 PL/SQL 定义执行
- 补充前端语句选择、QueryEditor 执行和后端 split/DBQueryMulti 回归测试
2026-06-25 17:37:32 +08:00
Syngnat
f6556f25d5 🐛 fix(oracle): 修复过程CASE分割导致执行截断 2026-06-25 13:52:20 +08:00
Syngnat
16a8a763f4 🐛 fix(oracle): 接入对象跳转并修复过程修改执行
- SQL 编辑器补齐 Oracle 序列和存储包元数据、hover 提示与 Ctrl/Cmd 点击跳转
- 对象编辑 SQL 保留 SQLPlus 斜杠分隔符,避免生成 /; 导致 ORA-00900
- 补充导航、对象编辑执行和多语言目录回归测试
2026-06-25 11:36:24 +08:00
Syngnat
4b1cd1b727 🐛 fix(oracle): 修复触发器脚本显示为空
- 触发器列表查询补齐 OWNER、TABLE_NAME、TRIGGER_BODY 等 Oracle 元数据字段
- 优先使用 DBMS_METADATA.GET_DDL 返回完整 CREATE TRIGGER 脚本
- 在 DDL 不可用时基于 USER_TRIGGERS/ALL_TRIGGERS 重建可编辑触发器语句
- 补充 Oracle 触发器 DDL 获取与回退重建回归测试
2026-06-25 10:50:07 +08:00
Syngnat
37ccaf7743 🐛 fix(oracle): 修复存储包定义截断与斜杠误执行
- 对象树支持序列和存储包双击及右键查看定义
- Oracle package 同时读取 PACKAGE 与 PACKAGE BODY 并生成可编辑 SQL
- SQL 拆分器跳过 SQL*Plus 独立斜杠和注释分隔符
- 补充前后端 PL/SQL 拆分与定义查看回归测试
2026-06-25 10:12:28 +08:00
Syngnat
06a984f39d 🐛 fix(elasticsearch): 修复 ES SQL 分号结尾查询无结果
- 兼容 SELECT FROM 索引名后直接接分号的查询语句
- 避免解析失败后退回全索引 query_string 导致空结果
- 补充 ES SQL 转 _search 的回归测试

Fixes #590
2026-06-24 23:36:37 +08:00
Syngnat
6d1c034052 🐛 fix(sidebar): 修复长表名横向滚动截断
- 取消 V2 对象树等宽标签自截断,支持横向滚动展示完整名称

- 提高对象树横向滚动宽度上限并补充回归测试
2026-06-24 22:42:04 +08:00
Syngnat
672d05d124 🐛 fix(sidebar): 恢复连接加载中的转圈状态
- 扩展连接状态为 loading/success/error

- 在连接根节点加载开始时显示 pending,成功后才置绿

- 同步 V2 active host 与 legacy Badge 状态显示并补测试
2026-06-24 22:31:13 +08:00
Syngnat
322d0a7cb8 feat(sidebar): 支持 Oracle 序列和存储包对象树
- 新增 Oracle/Dameng 序列与存储包元数据加载

- 同步对象树节点、V2 筛选、搜索、复制和拖拽支持

- 补充多语言文案和侧边栏回归测试
2026-06-24 22:26:39 +08:00
Syngnat
5725e78931 🐛 fix(mcp): 修复外部 MCP 读取连接不完整
- 启动时自动同步旧本地连接到后端安全仓库

- 启动 HTTP MCP 或安装外部 MCP 前执行连接同步

- 增加安全配置同步回归测试

Fixes #591
2026-06-24 21:37:52 +08:00
Syngnat
69b6072e37 🐛 fix(query-editor): 修复 OceanBase Oracle 查询默认 schema 误判
- 优先使用当前选中的 Oracle-like schema 解析未限定表名

- 为 OceanBase Oracle 只读账号查询补齐业务 schema 限定

- 增加回归用例覆盖登录用户与选中 schema 不一致场景
2026-06-24 21:25:22 +08:00
Syngnat
1a9d417c0a 🐛 fix(query-editor): 修复过程脚本斜杠分隔符误执行
- 前端语句选择跳过独立 SQL*Plus 斜杠分隔符

- 后端 SQL 拆分和流式文件执行保持过程体完整

- 增加 Oracle 过程脚本执行回归测试
2026-06-24 21:10:59 +08:00
Syngnat
8a552c4cb3 🐛 fix(query-editor): 优化事务下拉显示与浮层交互
- 自动提交延迟选项改为多语言完整文案,避免短标签语义不清
- 收窄并校准 V2 工具栏事务下拉宽度,兼顾不截断与紧凑布局
- 事务模式 Select 展开时自动隐藏 DBeaver 参考 Tooltip,避免浮层互相遮挡
- 补充事务设置行为测试和布局守护测试
2026-06-24 17:50:57 +08:00
Syngnat
9da9a36cf3 🐛 fix(snippet): 修复SQL片段入口与弹窗布局
- 将 SQL 编辑器代码片段管理入口改为打开工具中心 SQL 片段面板
- 关闭旧独立片段弹窗入口,避免同一功能出现两个入口形态
- 限制片段管理弹窗内容区高度并固定底部操作行,避免按钮被语法参考内容挤出
2026-06-24 17:49:48 +08:00
Syngnat
d08ab62f92 🐛 fix(data-grid): 修复刷新后未提交修改被清空 2026-06-24 15:56:07 +08:00
辣条
1cb112bcdf 1. 收口多语言能力 2. 修复查询编辑器、侧边栏、DDL 展示等业务问题 (#589)
## 主要改动

### 1. 多语言能力收口
- 收口 6 种语言下的业务文案与交互文案
- 覆盖连接驱动、查询编辑器、导出流程、部分 AI 工具面板等场景
- 补齐多语言回归测试,减少语言切换后文案残留中文或不同步的问题

### 2. 查询编辑器相关修复
- 修复新建查询标签页在语言切换后标题不同步的问题
- 修复查询结果集不展示、总数长期停留在“正在统计中”的问题
- 修复有限分页场景下仍可继续翻到不存在页码并展示异常数据的问题
- 修复 SQL 编辑器挂起事务内执行后续查询时,无法读到未提交修改的问题
- 修复 SQL Server 单条查询尾随 `affectedRows` 结果被误渲染为额外结果页签的问题
- 补齐 SQL Server 多结果集归属与过滤逻辑,避免冗余结果干扰展示

### 3. 侧边栏与视图相关修复
- 修复数据库连接下视图节点展开/滚动后渲染错乱、内容铺满侧栏的问题
- 修复收起视图栏后页面仍残留视图内容的问题
- 修复视图元数据重复导致的侧边栏树结构异常问题
- 补齐相关测试桩与回归测试

### 4. Oracle / DDL 相关修复
- 修复 Oracle 建表 DDL 追加注释时缺少语句结束分号与空行分隔的问题
- 避免 `CREATE TABLE` 与 `COMMENT ON TABLE/COLUMN` 粘连,提升复制后可直接执行的稳定性

### 5. 其他界面修复
- 修复语言设置面板选项显示截断问题
- 收口部分 dev 合并后的多语言遗漏与提示文案不一致问题

## 验证情况

已完成的本地验证包括:

- 前端定向测试
- `npm test -- QueryEditor.external-sql-save.test.tsx
sqlEditorTransaction.test.ts`
- `npm test -- QueryEditor.results-and-drop.test.tsx
QueryEditor.external-sql-save.test.tsx sqlEditorTransaction.test.ts
useSqlEditorTransactionController.test.tsx`
- `npm test -- QueryEditor.results-and-drop.test.tsx ddlFormat.test.ts`

- 后端定向测试
- `go test ./internal/app -run
'TestDBQueryMultiTransactionalKeepsDMLTransactionOpenUntilCommit|TestDBQueryMultiInTransactionReusesPendingManagedSessionForReadQueries'`
- `go test ./internal/db -run 'Test.*GetCreateStatement|Test.*Oracle.*'`

## 已知说明

- `internal/app` 下部分 SQL Server 相关定向测试在当前机器上受“SQL Server 纯 Go
驱动未启用”环境限制,失败原因为本地驱动依赖,不是本次改动引入的问题
- 本 PR 改动面较大,建议合并前重点关注:
  - 多语言切换后的查询编辑器与侧边栏行为
  - SQL Server 结果集展示
  - Oracle DDL 预览与复制执行
  - 挂起事务中的查询结果一致性

## 风险点

- 查询编辑器结果集合并、分页、事务复用逻辑改动较集中
- 多语言文案收口涉及范围较广,建议合并后做一次核心路径冒烟验证
2026-06-24 14:15:31 +08:00
Syngnat
2fb4851d1f 🐛 fix(query-editor): 补齐SQL Server冗余结果集过滤
- 为 SQL Server 原生多结果集缺失 statementIndex 的场景补齐结果归属
- 修复单条 SELECT 查询尾随 affectedRows 结果被误渲染为额外结果页签的问题
- 补充前后端回归测试并保持存储过程多结果集展示不变
2026-06-24 13:58:12 +08:00
Syngnat
32c3ba017e 🐛 fix(oracle): 修复DDL注释拼接缺少分号与换行
- 为 Oracle 建表 DDL 追加注释前补齐语句结束符并保留空行分隔
- 修复 COMMENT ON TABLE/COLUMN 与 CREATE TABLE 粘连导致复制后无法直接执行的问题
- 补充后端 Oracle 建表语句与前端 DDL 格式化回归测试
2026-06-24 13:57:49 +08:00
Syngnat
d09b968cc0 🐛 fix(query-editor): 补齐SQL Server冗余结果集过滤
- 为 SQL Server 原生多结果集缺失 statementIndex 的场景补齐结果归属
- 修复单条 SELECT 查询尾随 affectedRows 结果被误渲染为额外结果页签的问题
- 补充前后端回归测试并保持存储过程多结果集展示不变
2026-06-24 11:57:19 +08:00
Syngnat
90d84da849 🐛 fix(oracle): 修复DDL注释拼接缺少分号与换行
- 为 Oracle 建表 DDL 追加注释前补齐语句结束符并保留空行分隔
- 修复 COMMENT ON TABLE/COLUMN 与 CREATE TABLE 粘连导致复制后无法直接执行的问题
- 补充后端 Oracle 建表语句与前端 DDL 格式化回归测试
2026-06-24 11:40:18 +08:00
tianqijiuyun-latiao
d2c4160c6d 🐛 fix(query-editor): 修复挂起事务内查询结果不一致 2026-06-24 11:37:34 +08:00
tianqijiuyun-latiao
2b4beae9df test(i18n): 校准多语言守卫测试源码引用位置 2026-06-24 11:17:43 +08:00
tianqijiuyun-latiao
7dab6f2e33 🐛 fix(query-editor): 修复结果集阻塞与未知总数分页误导
- 为查询结果定位元数据探测增加软超时降级,避免租户元数据卡死阻塞主查询结果渲染
- 将未知总数分页切换为顺序翻页模式,并修正文案仅在真实统计时显示正在统计中
- 补充查询结果与分页回归测试,覆盖元数据超时和 legacy 未知总数分页场景
2026-06-24 10:47:37 +08:00
tianqijiuyun-latiao
1aab48783d 🐛 fix(query-tab): 修复新建查询标签页语言切换不同步
- 提取共享的新建查询默认标题识别逻辑
- 按当前语言重算默认查询和数据库作用域查询标题
- 补充跨语言标题回归测试
2026-06-24 10:26:05 +08:00
tianqijiuyun-latiao
5089e6b294 test(sidebar): 补齐视图元数据加载测试桩返回字段 2026-06-24 10:17:05 +08:00
tianqijiuyun-latiao
3be08cb6cd 🐛 fix(sidebar): 修复视图元数据重复导致侧边栏树渲染错乱
- MySQL 视图元数据缺失 schema 时回填当前数据库
- 避免多条回退查询生成重复视图节点
- 补充侧边栏视图去重回归测试
2026-06-24 10:14:22 +08:00
tianqijiuyun-latiao
6c4833dcd1 🐛 fix(settings): 修复语言设置面板选项显示截断 2026-06-24 10:07:52 +08:00
tianqijiuyun-latiao
ce17629a8d Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n 2026-06-24 08:08:53 +08:00
Syngnat
5e15c4dd2d 🐛 fix(query-editor): 隐藏 SQL Server 冗余影响行数结果集 2026-06-24 00:22:31 +08:00
tianqijiuyun-latiao
c04d82ee30 Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n
# Conflicts:
#	frontend/src/components/QueryEditor.results-and-drop.test.tsx
2026-06-23 23:58:25 +08:00
tianqijiuyun-latiao
7962aea008 🐛 test(query-editor): 修复 SQL Server 消息结果断言 2026-06-23 23:44:06 +08:00
Syngnat
5493b62bb9 🐛 fix(query-editor): 修复 SQL Server 结果消息缩进并校正回归测试 2026-06-23 23:42:30 +08:00
tianqijiuyun-latiao
86c39ac36f Merge branch 'dev' into feature/20260602_connection_driver_i18n 2026-06-23 23:34:16 +08:00
Syngnat
6bf05c9ed7 🐛 fix(update): 修复桌面端更新安装与重启流程
- macOS 下载完成后改为直接走安装更新链路,不再只打开安装目录
- Windows 更新脚本在重启时显式带上目标工作目录,提升安装后拉起稳定性
- 删除失效的 winget 发布工作流,并补充前端与脚本回归测试

Fixes #585
2026-06-23 22:18:07 +08:00
Syngnat
a2c1b4a7d8 🐛 fix(query-editor): 修复外部SQL快捷保存失效
- 放宽活跃 QueryEditor 在文档级快捷键目标下的保存触发条件
- 修复桌面端 Ctrl/Cmd+S 事件落到 document 时未真正写盘的问题
- 保持普通查询保存行为不变,并补充外部 SQL 文件快捷保存回归测试
2026-06-23 20:18:06 +08:00
Syngnat
07b3b908f9 🐛 fix(mongodb): 修复编辑态字符串类型丢失
- 为 MongoDB 编辑态补充字段名驱动的保守类型推断
- 统一 DataGrid 基准数据为类型化值,覆盖 JSON、行编辑和单元格编辑
- 保持 pMid 等普通字符串字段不被误判为 ObjectId
- 补充 Mongo helper、DataViewer 主键定位与 DataGrid 提交回归测试
2026-06-23 19:48:43 +08:00
tianqijiuyun-latiao
71989af586 🐛 fix(i18n): 收口 dev 合并后的业务提示多语言遗漏 2026-06-23 18:26:20 +08:00
Syngnat
adacf0b5c5 feat(connection): 支持生产连接多项保护策略
- 新增数据编辑、结构编辑、脚本执行和数据导入四类连接级保护配置
- 升级生产连接保护弹窗为多选卡片,并修复选项对齐与勾选态显示
- 按保护类型收口 QueryEditor、DataGrid、表设计、导入与同步目标入口
- 后端统一拦截 SQL 或 Mongo 写操作、结果编辑、结构变更和导入写入
- AI 本地工具与 RPC 执行链路透传连接保护配置并复用后端守卫
- 补充多语言文案、定向测试与需求追踪记录
2026-06-23 17:42:54 +08:00
tianqijiuyun-latiao
09ae3d74c4 🐛 fix(i18n): 合并 dev 后补齐只读保护多语言 2026-06-23 17:05:35 +08:00
tianqijiuyun-latiao
839fdd7d66 Merge branch 'dev' into feature/20260602_connection_driver_i18n
# Conflicts:
#	shared/i18n/de-DE.json
#	shared/i18n/en-US.json
#	shared/i18n/ja-JP.json
#	shared/i18n/ru-RU.json
#	shared/i18n/zh-CN.json
#	shared/i18n/zh-TW.json
2026-06-23 16:23:43 +08:00
Syngnat
b0a9a995fb feat(connection): 新增生产连接只读保护 2026-06-23 15:33:11 +08:00
Syngnat
3205d131c9 🐛 fix(query-editor): 修复消息结果前缀与复制全选交互
- 统一清洗 SQL Server 消息前缀并覆盖结果刷新与分页回填链路
- 将消息结果区改为只读文本区,补充一键复制入口
- 放行编辑器外可编辑区域的 cmd/ctrl+a,避免消息内容全选被抢占
- 补充结果面板交互与国际化回归,确保构建通过
2026-06-23 14:43:13 +08:00
tianqijiuyun-latiao
52d193b2e8 fix(i18n): 收口 dev 合并后的驱动与 QueryEditor 本地化 2026-06-23 14:04:25 +08:00
tianqijiuyun-latiao
d9e52c734a Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n 2026-06-23 13:14:43 +08:00
tianqijiuyun-latiao
3ce85617da feat(i18n): 收口导出前端多语言 2026-06-23 13:14:22 +08:00
tianqijiuyun-latiao
0ba984b277 Merge remote-tracking branch 'origin/dev' into feature/20260602_connection_driver_i18n
# Conflicts:
#	frontend/src/App.tsx
#	frontend/src/components/AISettingsModal.tsx
#	frontend/src/components/ConnectionModal.edit-password.test.tsx
#	frontend/src/components/ConnectionModal.tsx
#	frontend/src/components/DataSyncModal.i18n.test.ts
#	frontend/src/components/DataSyncModal.tsx
#	frontend/src/components/QueryEditor.external-sql-save.test.tsx
#	frontend/src/components/QueryEditor.tsx
#	frontend/src/components/Sidebar.locate-toolbar.test.tsx
#	frontend/src/components/Sidebar.tsx
#	frontend/src/components/SnippetSettingsModal.tsx
#	frontend/src/components/TableOverview.tsx
#	frontend/src/components/ai/AIChatHeader.test.tsx
#	frontend/src/components/ai/AISettingsProvidersSection.tsx
#	frontend/src/components/ai/aiChatPayloadDispatch.ts
#	frontend/src/components/ai/aiChatReadiness.ts
#	frontend/src/components/ai/aiSettingsModalConfig.tsx
#	frontend/src/components/ai/messageBubble/AIMessageCodeBlock.tsx
#	frontend/src/components/sidebarV2Utils.ts
#	frontend/src/i18n/catalog.test.ts
#	frontend/src/utils/connectionTypeCatalog.test.ts
#	frontend/src/utils/connectionTypeCatalog.ts
#	frontend/src/utils/tabDisplay.ts
#	internal/ai/provider/custom.go
#	internal/ai/service/service.go
#	internal/app/methods_driver.go
#	internal/app/methods_file.go
#	internal/db/custom_impl.go
#	internal/db/iris_impl.go
#	internal/db/mariadb_impl.go
#	internal/db/sqlserver_impl.go
#	shared/i18n/de-DE.json
#	shared/i18n/en-US.json
#	shared/i18n/ja-JP.json
#	shared/i18n/ru-RU.json
#	shared/i18n/zh-CN.json
#	shared/i18n/zh-TW.json
2026-06-23 12:41:27 +08:00
Syngnat
8da8cc7f91 🐛 fix(mongodb): 修复 DataGrid 编辑后 BSON 类型丢失
- 为 MongoDB 结果展示、单元格编辑和行编辑接入类型感知格式化与解析
- 支持 ObjectId、日期、Int32、Int64、Double、Decimal128、UUID 等常见类型保真
- 统一 v1/v2 驱动查询结果的 Extended JSON 输出与 ApplyChanges BSON 恢复
- 补充前端提交链路与后端类型转换回归测试
2026-06-23 12:14:27 +08:00
Syngnat
bc63311003 🐛 fix(oracle): 修复普通查询重复列自动别名缺失
- 在 QueryEditor 查询计划阶段识别显式列与 alias.* 的重复列冲突
- Oracle 执行前自动为冲突显式列补充 _1 风格唯一别名
- 让 locator 与后续追加表达式复用改写后的可执行 SQL
- 补充普通查询重复列自动别名的 Oracle 回归测试
2026-06-23 10:46:35 +08:00
Syngnat
3a00ae1f44 🔧 chore(wails): 同步数据同步 targetSchema TS 绑定
- 为 sync 请求模型补充 targetSchema 字段映射
- 同步自动生成的 frontend wailsjs models.ts 绑定
- 更新 package.json.md5 生成校验文件
2026-06-23 09:55:41 +08:00
Syngnat
e8cad189be 🐛 fix(sqlserver): 修复普通查询结果被原生多结果集吃空
- 对只读 SQL 的原生多结果集空返回增加顺序回退兜底
- 避免 optional driver-agent 成功返回空结果时前端只剩日志无结果集
- 补充 SQLServer 读查询空结果回退回归测试
2026-06-23 09:46:44 +08:00
Syngnat
495a985ae1 🐛 fix(sqlserver): 修复可选驱动查询消息透传缺失
- 为 optional-driver-agent 的 query 和 queryMulti 响应补充 messages 字段
- 在可选驱动 DB 客户端透传 SQL Server 查询提示信息与多结果集
- 补充 agent 与数据库层回归测试并更新 driver agent revision
2026-06-23 08:48:42 +08:00
Syngnat
8f1e6cf379 ️ perf(frontend): 优化长时运行下的搜索与缓存占用
- 为 V2 cmd+k 搜索预建索引并限制初始/宽泛结果数量
- 清理冷数据库树和 DataViewer 长生命周期快照缓存
- 收紧运行时 SQL 日志预算并在 hydration 时压缩旧缓存
2026-06-22 22:36:39 +08:00
Matt Van Horn
05e8dab710 Merge 0ba2bfe645 into 4999fd544d 2026-06-22 00:33:23 -07:00
Matt Van Horn
0ba2bfe645 fix(datasource): ClickHouse 22.8 HTTP 握手兼容 displayName 缺失
clickhouse-go 在 HTTP 握手阶段执行 SELECT displayName(), version(),
revision(), timezone(),而 ClickHouse 22.8 没有 displayName() 函数,
返回 Code 46 UNKNOWN_FUNCTION,导致即便已移除 client_protocol_version
的兼容重试路径仍然连接失败。

扩展现有 HTTP 兼容脚手架:新增 displayName Code 46 检测,复用同一条
兼容重试分支;兼容模式下的 RoundTripper 改写握手探测请求体,将
displayName() 替换为各版本通用的 hostName(),其余请求体原样放行。

Refs #479
2026-06-22 00:19:49 -07:00
tianqijiuyun-latiao
f282da3bcb feat(i18n): 收口多语言功能业务代码 2026-06-22 15:12:42 +08:00
tianqijiuyun-latiao
eba689754c test(ai): 对齐多语言 fallback 测试基线 2026-06-22 13:46:42 +08:00
tianqijiuyun-latiao
d13c153f5e feat(i18n): 收口数据库驱动多语言代码
- 提交 internal/db 多驱动用户可见错误与状态文案多语言化

- 补齐数据库驱动多语言测试与六语言 catalog

- 修复 frontend i18n catalog 的 4 个失效 guard
2026-06-22 10:09:45 +08:00
Syngnat
4999fd544d 🐛 fix(data-sync): 完善多种目标库的 schema 同步链路
- 扩展数据同步目标端 schema 选择与元数据加载,覆盖 SQL Server、IRIS、DuckDB 等独立 schema 场景
- 修正同步链路中的目标表 schema 归一化与 query/apply 表名解析,避免落到错误模式
- 补充前后端回归测试与多语言文案,覆盖 schema 选择、别名识别和结果预览路径

Fixes #571
2026-06-21 22:46:57 +08:00
Syngnat
36233ba9aa Merge pull request #580 from feat/573-redis-db-alias
feat(redis): per-database aliases in the sidebar
2026-06-21 16:42:36 +08:00
Matt Van Horn
4408bce159 feat(redis): support per-database alias in the Redis viewer tree 2026-06-21 00:44:22 -07:00
Syngnat
e7b8e78f9c 🐛 fix(query-editor): 修复当前语句快捷选择在 CRLF 文本下错位
- 统一当前语句选择与执行路径的归一化 offset/position 换算
- 避免 Windows CRLF 文本下 SQL 语句选区错位
- 补充 QueryEditor 当前语句选择回归测试

Fixes #575
2026-06-21 15:08:34 +08:00
Syngnat
29e7e365f1 📝 docs(readme): 补充 Trino 联邦查询支持说明
- 在中英文 README 的支持数据源列表中补充 Trino\n- 明确 Trino 作为跨多数据源联邦查询入口\n\nFixes #577
2026-06-21 14:11:36 +08:00
Syngnat
8ea7ecc477 feat(trino): 新增 Trino 可选驱动接入并补齐查询支持
- 后端新增 Trino 数据库实现与 optional driver-agent provider
- 前端补齐 catalog.schema 连接配置、URI 解析与能力开关
- SQL 编辑器对 Trino 禁用托管事务并补充前后端测试
2026-06-21 13:54:42 +08:00
Syngnat
99b75378c3 feat(data-grid): 完善 ER 图多层关系展开与字段浏览
- 支持按层扩展关联关系并重置为一层视图
- 支持节点字段展开收起与全部字段切换
- 补充 ER 图模型、Hook 与界面回归测试
2026-06-21 11:37:45 +08:00
Syngnat
5f56859898 🐛 fix(query-editor): 兜底 SQL 编辑器中文输入首次不上屏
- 补充 Monaco IME 组合输入提交兜底
- 统一拦截候选键避免快捷键链路抢占
- 增加 QueryEditor 中文输入回归测试

Fixes #578
2026-06-21 11:30:17 +08:00
Syngnat
7f2445a6f5 🐛 fix(monaco-editor): 禁用 EditContext 修复中文输入异常
- 在 MonacoEditor 中统一关闭 editContext
- 修复单引号场景下中文输入首次上屏异常
- 补充编辑器选项回归断言

Close #578
2026-06-20 17:05:38 +08:00
Syngnat
b3a54b3ff8 feat(data-grid): 新增完整 ER 图视图并收口导出回归
- 新增完整 ER 图建模、布局与交互 hook
- 在元数据视图接入 ER 图并补齐样式与多语言文案
- 修正查询结果导出测试交互与 Wails 导出 mock
2026-06-20 15:45:58 +08:00
Syngnat
c8c8497a2f feat(query-editor): 收敛 SQL 分析工作台与结果区日志体验
- 新增 SQL 分析工作台,统一承载慢 SQL 和 SQL 诊断视图
- 将 SQL 执行日志收进结果区首个日志标签并在失败时展示错误摘要
- 调整侧边栏入口、标签展示、多语言文案与定向前端测试覆盖
2026-06-20 14:09:58 +08:00
Syngnat
04019135a0 🐛 fix(sql-diagnose): 兼容新版 MySQL JSON EXPLAIN 解析
- 兼容 query_plan 包装和新版 JSON V2 执行计划结构
- 补齐 covering index、table scan 等节点类型与统计归一化
- 增加新版 MySQL EXPLAIN 解析测试并修正 TotalCost 汇总逻辑
2026-06-20 14:08:02 +08:00
Syngnat
e7935db84b feat(db-connection): 新增连接后台定时探活保活能力
- 为连接配置新增 keepAlive 开关与探活间隔
- 在后端缓存连接上增加后台定时 Ping 调度与失效剔除
- 补充前端表单、Wails 模型映射与定向测试覆盖

Close #579
2026-06-20 11:24:44 +08:00
Syngnat
0f67305100 ♻️ refactor(codebase): 拆分大文件并收敛模块职责
- 拆分 ConnectionModal、DataGrid、QueryEditor、App 等前端大文件

- 抽出 driver assets、v2 workbench 样式和 source-as-test 读取辅助

- 更新相关测试以覆盖拆分后的源码位置
2026-06-19 23:10:00 +08:00
Syngnat
6179c3fbd9 ♻️ refactor(sidebar): 拆分动作与搜索逻辑 2026-06-19 18:46:08 +08:00
Syngnat
13705f9098 ♻️ refactor(sidebar): 抽出 V2 右键菜单逻辑 2026-06-19 18:08:54 +08:00
Syngnat
1dea343aa2 ♻️ refactor(sidebar): 抽出 V2 树标题渲染 2026-06-19 18:03:28 +08:00
Syngnat
293a8ff3e9 ♻️ refactor(sidebar): 抽出实体操作弹窗 2026-06-19 17:56:12 +08:00
Syngnat
6e422aea33 ♻️ refactor(sidebar): 抽出树节点加载器 2026-06-19 17:32:45 +08:00
Syngnat
39e52469f2 ♻️ refactor(sidebar): 抽出批量操作弹窗 2026-06-19 17:23:53 +08:00
Syngnat
3ff5141184 ♻️ refactor(sidebar): 抽出外部 SQL 文件流程 2026-06-19 17:13:24 +08:00
Syngnat
db31513c0b ♻️ refactor(sidebar): 抽出批量导出状态逻辑 2026-06-19 16:55:43 +08:00
Syngnat
5da2c7ff1a ♻️ refactor(sidebar): 抽出元数据加载工具 2026-06-19 16:45:15 +08:00
Syngnat
d109f2891f ♻️ refactor(sidebar): 复用 V2 侧栏工具函数 2026-06-19 16:33:16 +08:00
Syngnat
f946951580 ♻️ refactor(sidebar): 迁出 legacy 节点菜单构建逻辑 2026-06-19 16:21:36 +08:00
Syngnat
f6ebfc2e44 ♻️ refactor(sidebar): 清理 V2 rail 残留死代码
- 删除未被调用的 rail connection button/group 渲染闭包

- 移除对应的折叠、拖拽状态和 badge/host 辅助函数

- 保留当前可达的 active connection 状态展示和树拖拽排序逻辑
2026-06-19 15:16:31 +08:00
Syngnat
540dbc2a28 ♻️ refactor(sidebar): 抽出 Command Search 面板组件
- 新建 SidebarSearchPanel,承接 V2 命令搜索弹层的行、分组和空状态渲染

- Sidebar.tsx 保留搜索状态、过滤和执行逻辑,仅通过 typed props 传入子组件

- 保持 @ 对象搜索、? AI 提问、同步过滤与键盘导航行为不变
2026-06-19 14:55:21 +08:00
Syngnat
2c8128724e ♻️ refactor(sidebar): 抽出 ConnectionRail 为独立子组件
- 新建 sidebar/SidebarConnectionRail.tsx:V2 连接栏(新建分组/批量导出/SQL 文件/定位/AI/工具/设置 7 个按钮)
- Props 用聚合对象(labels + handlers + canLocateActiveTab)避免 18+ 个独立 props drilling
- Sidebar.tsx 删除 renderV2ConnectionRail(-93 行),加 v2ConnectionRailProps 构造 + <SidebarConnectionRail /> 调用
- Sidebar.tsx 从 10187 减至 10122 行
2026-06-19 14:29:57 +08:00
Syngnat
528f3c51a0 ♻️ refactor(sidebar): 迁出命令搜索相关类型与 shouldLoadSidebarNodeOnExpand
- 迁出 V2CommandSearchMode/V2CommandSearchQuery 类型与 parseV2CommandSearchQuery(支持 @/?前缀切换 object/ai 模式)
- 迁出 shouldLoadSidebarNodeOnExpand(节点懒加载判定)
- SidebarNodeLike 加可选 key 字段以适配测试用法
- Sidebar.tsx 从 10235 减至 10187 行,sidebarHelpers.ts 增至 215 行
2026-06-19 14:19:06 +08:00
Syngnat
a4d94624cd ♻️ refactor(sidebar): 继续抽离 resolveV2ObjectGroupTitle 等 2 个工具函数
- 新增 SidebarNodeLike 结构化类型,替代 Pick<TreeNode, ...> 解除对 Sidebar 内部类型的依赖
- 迁出 resolveV2ObjectGroupTitle(对象分组标题本地化)与 resolveSidebarTableNameForCopy(节点表名提取)
- Sidebar.tsx 从 10243 减至 10235 行
2026-06-19 14:15:35 +08:00
Syngnat
87bd16c4ba ♻️ refactor(sidebar): 抽离独立工具函数到 sidebarHelpers 模块
- 新建 sidebar/sidebarHelpers.ts:迁出 6 个无内部类型依赖的纯函数(formatSidebarRowCount/hasSidebarLazyChildren/getV2RailConnectionGroupBadgeText 等)+ V2ExplorerFilter 类型 + V2_RAIL_UNGROUPED 常量
- Sidebar.tsx 通过 import + re-export 双向引用,外部测试文件的 `from './Sidebar'` 保持兼容
- 文件规模:Sidebar.tsx 从 10275 减至 10243 行,建立 sidebar/ 子目录作为后续拆分的目标归宿
2026-06-19 14:11:54 +08:00
Syngnat
dc54d24d2b feat(sidebar): 新增 Sidebar 慢 SQL 历史浮动入口
- 新建 SlowQueryRailButton:独立小组件,从 store 直接读 tabs/connections 解析激活连接
- 挂载方式:浮动在 Sidebar 右下角,不修改 Sidebar.tsx 内部代码(避免改 10275 行的大文件)
- 体验优化:无激活连接时按钮自动禁用并 tooltip 提示;与 Ctrl+Shift+L 快捷键路径并存
- bundle 保持:SlowQueryPanel 继续走 lazy 加载独立 chunk,不进入主 bundle
2026-06-19 13:56:15 +08:00
Syngnat
8457f6c4b7 feat(shortcuts): 把 SQL 诊断与慢查询快捷键注册到管理系统并加菜单入口
- 注册到系统:diagnoseQuery 与 showSlowQueries 加入 ShortcutAction,可在快捷键管理面板自定义
- 修复冲突:原硬编码 Ctrl+Shift+D 与 toggleTheme 冲突、Ctrl+Shift+H 与 toggleLogPanel 冲突;改为 Ctrl+Shift+P / Ctrl+Shift+L
- 菜单入口:QueryEditor 的"更多"下拉加 SQL 诊断、慢 SQL 历史两个菜单项,附带快捷键提示
- i18n 同步:6 种语言补齐 label/description
2026-06-19 13:43:12 +08:00
Syngnat
a2d83744b5 feat(explain): 扩展索引建议规则引擎至 15 条
- 新增规则:LIKE 前缀通配、函数包裹列、笛卡尔积风险、OR 条件无索引、大 OFFSET 分页、SELECT * + JOIN 模式
- 阈值常量:large_offset(10000)、cartesian_product(100000)、wide_table(20 列)
- 测试覆盖:新增 6 个用例验证规则触发与抑制(含边界场景)
2026-06-19 13:43:01 +08:00
Syngnat
946450874f 🔧 chore(wails): 同步 GetSlowQueries 与 ClearSlowQueries 的 TS 绑定
- Wails 工具自动重新生成,把手动插入的绑定重排到字母序正确位置
- 同步 package.json.md5 构建指纹
2026-06-19 13:25:32 +08:00
Syngnat
577a417292 feat(explain-ui): 新增慢 SQL 历史面板与 Ctrl+Shift+H 快捷键
- 面板组件:SlowQueryPanel 含 TopN 列表 + 耗时/扫描行数/时间三种排序 + 清空操作
- 入口接入:QueryEditor 加 Ctrl+Shift+H 快捷键,点击条目回填 SQL 到编辑器
- Wails 绑定:手动同步 GetSlowQueries 与 ClearSlowQueries 到 App.js/App.d.ts
- 颜色提示:耗时 >5s 红色、>1s 橙色,便于一眼识别重查询
2026-06-19 13:23:02 +08:00
Syngnat
a74065bdbb feat(explain): 新增慢 SQL 历史存储与 DBQueryMulti 执行埋点
- 存储层:JSONL 单连接单文件,5MB 自动滚动,TopN 排序 + SQL 指纹去重
- 执行埋点:DBQueryMulti 用 named return + defer 异步记录,成功后自动写入历史
- 阈值过滤:默认 500ms 以下查询跳过记录,避免历史爆炸
- 查询入口:GetSlowQueries 按 duration/rowsRead/recent 排序,ClearSlowQueries 支持清空
- SQL 指纹:字面量/大小写归一化后 sha256,同模板不同参数视为同一查询
- 测试覆盖:新增 13 个单元测试覆盖存储/滚动/排序/去重/指纹
2026-06-19 13:17:39 +08:00
Syngnat
0c320234fd feat(explain-ui): 将诊断工作台接入 QueryEditor 并支持快捷键触发
- 快捷键绑定:QueryEditor 监听 Ctrl+Shift+D(Mac 为 Cmd+Shift+D)打开诊断面板
- 配置解析:从 currentConnectionId 复用 SavedConnection 模式解析 ConnectionConfig
- lazy 加载:ExplainWorkbench 通过 React.lazy + Suspense 隔离,避免 reactflow 进入主 bundle
- 端到端联通:用户在编辑器写 SQL 后按快捷键即可触发 DiagnoseQuery 并可视化结果
2026-06-19 13:11:20 +08:00
Syngnat
f5ae2e51f9 feat(explain-ui): 新增执行计划图渲染组件与索引建议侧栏
- 依赖引入:新增 reactflow + dagre 用于执行计划 DAG 自动布局
- 构建配置:vite manualChunks 拆分 reactflow/dagre/recharts 独立 chunk,便于按需加载
- 类型镜像:utils/explainTypes.ts 镜像后端 ExplainResult/Node/Stats/IndexSuggestion,含颜色与格式化 helper
- 图渲染:ExplainGraph 自定义节点按 opType 着色 + 警告 flag 边框高亮 + dagre TB 布局
- 侧栏组件:ExplainSidebar 含统计条、节点详情、索引建议按 severity 排序
- 主容器:ExplainWorkbench 含 Modal + 执行计划/原文双 tab,调用 DiagnoseQuery 端到端联通
2026-06-19 13:04:49 +08:00
Syngnat
8e24e40fdd feat(explain): 补齐 Oracle/SQLServer/ClickHouse 解析器与索引建议规则引擎
- 方言解析:新增 Oracle DBMS_XPLAN 表格、SQLServer SHOWPLAN_XML、ClickHouse EXPLAIN JSON 解析器
- 规则引擎:新增 10 条跨方言规则(全表扫描、缺索引 JOIN、filesort、估算偏差、缓冲命中、Nested Loop 高扇出等)
- 入口接入:DiagnoseQuery 返回的 Suggestions 自动填充规则匹配结果
- 容错增强:SQLServer strip 默认命名空间与 XML 声明;Oracle 表格列与独立 Predicate 段双源融合
- 测试覆盖:新增 27 个用例覆盖三方言解析与规则触发场景
2026-06-19 12:45:15 +08:00
Syngnat
85648b1e5a 🔧 chore(wails): 同步 DiagnoseQuery 自动生成的 TS 绑定
- 重新生成 App.d.ts / App.js:暴露 DiagnoseQuery(connectionConfig, dbName, sql) 入口
- 更新 models.ts:新增 connection.ExplainResult / ExplainNode / ExplainStats 等类型
- 同步 Service.d.ts / Service.js / package.json.md5(构建缓存指纹)
2026-06-19 12:31:42 +08:00
Syngnat
b997788437 feat(explain): 新增 SQL 诊断工作台后端 EXPLAIN 基建
- 数据结构:新增 ExplainResult/Node/Stats/IndexSuggestion/DiagnoseReport 等归一化模型,跨方言通用
- 接口扩展:Database 接口新增 ExplainExecer 可选能力,支持驱动自带 EXPLAIN 实现
- 核心入口:DiagnoseQuery 支持 SELECT/WITH 白名单校验、方言调度、原生与 fallback 两条执行路径
- 方言适配:buildExplainQuery 覆盖 MySQL/PostgreSQL/SQLite/Oracle/SQLServer/ClickHouse 7 大主流
- 解析器:MySQL FORMAT=JSON 含表格 fallback、PostgreSQL ANALYZE BUFFERS JSON、SQLite EQP 层级解析
- 测试覆盖:新增 27 个单元测试覆盖 SQL 构造与三方言解析器
2026-06-19 12:30:56 +08:00
Syngnat
542bafe6c4 🔧 chore(gitignore): 忽略 optional-driver-agent 构建产物 2026-06-19 12:07:33 +08:00
Syngnat
98965a56e1 🐛 fix(memory): 修复大数据量导出导致进程内存飙升至 16G 的问题
- GC 策略:主进程与 driver-agent 启动时收紧 SetGCPercent 至 50
- 周期回收:scan_rows 与 callStreamQuery 每 5 万行触发 runtime.GC
- 自适应限流:driver-agent 引入 GOMEMLIMIT 自适应策略,2GB 起步按 1GB 步长抬升至 8GB 上限
- 批次调优:流式批次由 256 行缩减至 64 行,降低 JSON 编解码瞬时峰值
2026-06-19 12:05:02 +08:00
Syngnat
21c427bc39 🐛 fix(connection): 优化多数据源连接数占用
- 测试连接改为隔离连接,成功后立即关闭并避免写入全局缓存
- 新增通用 SQL 连接池配置,限制网络型数据源空闲连接长期占用
- Redis 测试连接改为临时客户端并立即释放
- MySQL 连接数超限时释放同实例缓存连接并重试
- 补充连接释放、缓存重试和连接池参数回归测试
2026-06-18 20:29:19 +08:00
Syngnat
6b67bb24b4 feat(tool-center): 优化工具中心分组交互与通用弹窗
- 重构工具中心为侧边分组导航,固定弹窗高度并支持内部滚动
- 新增通用可拖拽可缩放 Modal,统一主要弹窗打开体验
- 为工具中心内嵌入口补充返回上一步交互与底部操作区
- 补充多语言文案和工具中心/Modal/i18n 回归测试
2026-06-18 20:28:47 +08:00
Syngnat
06dd9507ee feat(ai): 补齐 Cursor 与 CodeBuddy 会话态聊天链路
- 新增 SessionChatProvider 接口,补齐非流式对话的会话态复用能力
- 为 Cursor Agent 和 CodeBuddy CLI 同步实现流式与非流式会话续接及状态持久化
- CustomProvider 补充会话态透传,统一 custom provider 的会话复用行为
- Service 新增 AIChatSendInSession,聊天主链路非流式回退改走带 session 的发送接口
- 保留原 AIChatSend 无状态语义,避免标题生成和记忆压缩污染主会话上下文
- 补充前后端定向测试,覆盖会话恢复、续接发送和前端回退分流
2026-06-18 13:35:08 +08:00
Syngnat
b588235b62 feat(ai): 接入 Cursor Cloud Agents API
- 新增 cursor-agent provider,支持创建 agent、轮询 run 状态和 SSE 流式响应
- 接入 AITestProvider 与 AIListModels,支持 Cursor 官方 /v1/models 连通性测试和模型发现
- 在 AI 设置中新增 Cursor 供应商预设,固定 cursor-agent 协议并补齐默认端点配置
- 调整 provider readiness 与 insights 规则,允许 Cursor 未显式选模型时走官方默认模型
- 补充后端 provider/service 测试和前端 preset、表单、readiness 相关用例

Close #576
2026-06-18 12:35:58 +08:00
Syngnat
f457f6aaca feat(ai): 接入 CodeBuddy CLI 并兼容官方登录态
- 新增 CodeBuddy CLI provider,支持 codebuddy/cbc 命令调用与输出解析
- 将 Base URL、API Key/Auth Token、自定义请求头映射到 CodeBuddy CLI 环境变量
- 扩展 custom provider 路由与测试链路,兼容空 Base URL 和 CLI 默认模型选择
- AI 设置新增 CodeBuddy 预设,并补齐 preset 回显识别与匹配逻辑
- 修正就绪态、模型列表与表单校验,允许留空凭证直接复用本机已登录账号
- 补充前后端定向测试并覆盖 CodeBuddy 配置展示文案

Close #574
2026-06-18 12:06:58 +08:00
Syngnat
c8fe90cbee ️ perf(import-export): 降低 OceanBase 导出链路内存占用
- 为 optional driver-agent 补齐 streamQuery 分片协议,避免大结果集整批缓冲到内存
- 在 OceanBase 整表导出和查询结果导出前强校验 driver-agent revision,旧版代理直接拦截并提示重装
- 为 driver-agent 增加大查询和流式导出完成后的 GC/FreeOSMemory 回收逻辑
- 补充导出前校验、流式分片消费和 agent 内存回收的定向测试
- 更新 driver-agent revisions 以匹配新的流式导出协议
2026-06-18 11:32:08 +08:00
Syngnat
6bd87fa568 🐛 fix(export-workbench): 补齐整表导出百分比进度
- 为整表导出链路补充 COUNT(*) 预统计,并把总行数写回首个导出进度事件
- 兼容解析多种总数返回类型,避免后端已知总量时前端仍降级为不定进度条
- 补充后端导出总数解析与前端 runner 状态切换回归测试
2026-06-18 10:58:01 +08:00
Syngnat
293fc6e0fe 🐛 fix(data-grid): 修复字段元数据偶发缺失
- 为字段元数据提取补充可用性判断并在空类型/空备注时自动重试
- 刷新结果集时同步清理字段、外键和唯一键缓存并强制补拉元数据
- 补充 DataGrid 头部元数据回归测试,覆盖首次空结果重试与刷新重载场景
2026-06-18 10:57:51 +08:00
Syngnat
2a8ae05363 🐛 fix(export-workbench): 修正未知总数进度展示并优化 XLSX 收尾阶段
- 修正总行数为 0 时仍被当作已知总数的问题,避免导出进度百分比失真
- 调整导出进度条判定逻辑,未知总数时改为展示实时写入进度
- 统一 Sidebar、TableOverview 和导出工作台的预计行数口径,仅在总数大于 0 时视为已知
- 优化 XLSX 收尾阶段的 ZIP 压缩策略和拷贝缓冲,降低 Windows 大文件导出封装耗时
- 细化 finalizing 阶段文案,明确显示 XLSX 正在封装压缩
- 补充导出进度状态与零总数场景的回归测试
2026-06-18 10:15:00 +08:00
Syngnat
9613f6b624 🐛 fix(frontend): 预构建本地化依赖避免开发启动代理失败 2026-06-18 09:58:06 +08:00
Syngnat
ee78b9b57c ️ perf(import-export): 降低大文件导入导出内存占用
- xlsx 导出改为临时 sheet 加 zip 直写,避免整包缓冲到内存
- xlsx 导入改为 zip xml 流式解析,并将 shared strings 落到临时文件
- 大任务完成后按行数和文件大小阈值触发内存回收
- 补充导入导出流式链路的测试与基准覆盖
2026-06-18 09:21:01 +08:00
Syngnat
5ce4dddd7a ️ perf(ci): 减少 driver-agent 检测与平台 diff 的重复计算
- driver_agents job 一次性产出各平台 driver 集合,build 矩阵直接消费结果\n- detect 阶段补充 setup-go 与缓存,降低 revision 检测的冷启动成本\n- diff-driver-agent-revisions 支持候选 drivers 过滤,并行生成 base/head revisions\n- 补充脚本测试,覆盖候选 driver 过滤与无关 driver 忽略场景
2026-06-17 17:44:40 +08:00
Syngnat
e67285fde1 ️ perf(import): 重构导入链路并支持流式批量写入
- 后端新增流式导入流水线,避免预览和导入阶段整文件驻留内存\n- 导入执行优先复用 BatchApplier 按批提交,并在批量失败时回退单行定位错误\n- 导入进度事件兼容未预扫总行数场景,沿用预览总数稳定展示进度\n- 补充导入预览、批量回退和前端进度展示的最小回归测试
2026-06-17 17:26:57 +08:00
Syngnat
4e31d47936 feat(export-workbench): 支持批量导出工作台并优化 SQL 导出性能
- 侧边栏批量表/批量库入口改为直接打开导出工作台,统一导出配置与进度视图
- 导出工作台新增 batch-tables / batch-databases 模式,支持连接、数据库、对象选择与独立历史记录键
- 连接、数据库、对象下拉项补齐完整名展示与悬浮提示,避免长名称被截断后不可识别
- 后端新增批量对象/批量库导出 WithOptions 链路,统一返回输出文件/目录与进度信息
- SQL dump 数据导出改为按方言批量写入,MySQL/PG 等使用多值 VALUES,Oracle/达梦使用 INSERT ALL
- 补充导出工作台与 SQL dump 的回归测试和 benchmark,覆盖批量模式与批量写入语义
2026-06-17 16:50:05 +08:00
Syngnat
954d126a8f test(sidebar): 适配多语言后的 locate-toolbar 测试 2026-06-17 15:36:58 +08:00
Syngnat
5b31ab7435 feat(export-workbench): 新增导出工作台与进度历史
- 新增表级导出工作台标签页,统一承载导出范围、格式和 XLSX sheet 行数配置
- 结果集、表概览、侧栏和右键菜单统一接入导出工作台与带进度的导出入口
- 导出进度改为事件驱动,未知总数时展示不定进度和实时已写入行数
- 持久化每张表的导出历史并复用同一导出标签,重启后仍可查看最近任务
- 调整导出页签标题、状态胶囊和历史列表,补充工作台与状态流测试覆盖
2026-06-17 14:40:49 +08:00
Syngnat
b3c321be67 ️ perf(export): 重构大结果集导出链路并支持流式写入
- 新增 ExportFileOptions 统一承载导出格式、进度任务和 XLSX sheet 行数上限
- 查询导出改为流式写入文件,避免一次性缓存整批结果导致高内存占用
- 增加值数组快速路径并复用扫描与写入缓冲,减少逐行 map 分配开销
- 为 ClickHouse、自定义驱动、达梦、SQLServer 和 TDengine 补齐 StreamQuery 支持
- 导出时间字符串仅在形似时间时再解析,避免普通文本被误判改写
- 补充 XLSX 分 sheet、流式导出和基准测试覆盖
2026-06-17 14:24:35 +08:00
辣条
d7ad83f0d5 完善多模块多语言 (#572)
- 补齐前后端多模块多语言文案、共享词典与 i18n guard,减少硬编码展示文案。
- 覆盖 QueryEditor、Sidebar 外部 SQL、安全更新、DataGrid、Redis、AI/工具入口等重点场景。
2026-06-17 13:56:01 +08:00
tianqijiuyun-latiao
3006429a9a 🔧 chore(merge): 合并最新 dev 并解决 DataGrid 冲突 2026-06-17 13:52:50 +08:00
tianqijiuyun-latiao
9364c48ef0 feat(i18n): 完善多模块多语言适配与发版验证
扩展前后端多语言文案与共享词典。增加多模块 i18n 回归测试与 guard。收口外部 SQL 菜单和弹窗多语言文案。
2026-06-17 13:17:33 +08:00
Syngnat
3e140c1bc6 🐛 fix(ai-safety): 修正完全模式执行口径与本地工具失败判定
- 修正完全模式下 DML 与过程调用的安全提示和限制说明
- 区分连接探针失败与可恢复 SQL 执行错误,避免数据探针被误终止
- 修复本地 execute_sql 写语句结果返回 affectedRows
- 补充 AI 安全、本地工具执行与 SQL 限制回归测试
2026-06-17 09:49:59 +08:00
Syngnat
7ff3e00759 🐛 fix(query-editor): 修复外部 SQL 标签状态与 OceanBase 查询改写
- 修复外部 SQL 文件删除后标签残留及关闭异常
- 修复 OceanBase Oracle 查询注入隐藏 ROWID 时的表名改写
- 修复小写表名执行时的精确引用并保留日志中的原始 SQL
- 补充查询编辑器相关回归测试
2026-06-17 09:49:39 +08:00
Syngnat
0632c5242c 🐛 fix(oceanbase/data-grid): 修复 Oracle 时间字段显示编辑与结果视图异常
- 修复 OceanBase Oracle DATE 与 TIMESTAMP 的解码、展示和编辑精度丢失问题
- 修复查询结果与数据视图的行号显示、分页页数和日期列展示口径
- 打通 Oracle 与 OceanBase 会话执行链路的扫描方言透传
- 补齐 DBQuery、DataGrid temporal 和 OceanBase 结果链路回归测试
2026-06-17 09:49:15 +08:00
tianqijiuyun-latiao
76b0163bd3 Merge branch 'dev' into feature/20260602_connection_driver_i18n
# Conflicts:
#	frontend/package.json.md5
#	frontend/src/App.tsx
#	frontend/src/components/AIChatPanel.message-boundary.test.tsx
#	frontend/src/components/AIChatPanel.tsx
#	frontend/src/components/AISettingsModal.tsx
#	frontend/src/components/ConnectionModal.tsx
#	frontend/src/components/DataGrid.ddl.test.tsx
#	frontend/src/components/DataGrid.layout.test.tsx
#	frontend/src/components/DataGrid.tsx
#	frontend/src/components/DataGridColumnTitle.test.tsx
#	frontend/src/components/DataGridLegacyCellContextMenu.tsx
#	frontend/src/components/DataGridSecondaryActions.tsx
#	frontend/src/components/DataGridToolbarFrame.tsx
#	frontend/src/components/DataSyncModal.tsx
#	frontend/src/components/DataViewer.tsx
#	frontend/src/components/DefinitionViewer.tsx
#	frontend/src/components/DriverManagerModal.tsx
#	frontend/src/components/QueryEditor.external-sql-save.test.tsx
#	frontend/src/components/QueryEditor.tsx
#	frontend/src/components/RedisViewer.tsx
#	frontend/src/components/Sidebar.locate-toolbar.test.tsx
#	frontend/src/components/Sidebar.tsx
#	frontend/src/components/TabManager.hover.test.tsx
#	frontend/src/components/TabManager.tsx
#	frontend/src/components/TableDesigner.tsx
#	frontend/src/components/V2TableContextMenu.tsx
#	frontend/src/components/ai/AIChatHeader.tsx
#	frontend/src/components/ai/AIHistoryDrawer.tsx
#	frontend/src/main.tsx
#	frontend/src/store.ts
#	frontend/src/utils/aiComposerNotice.test.ts
#	frontend/src/utils/aiComposerNotice.ts
#	frontend/src/utils/connectionModalPresentation.ts
#	frontend/src/utils/driverImportGuidance.ts
#	frontend/src/utils/externalSqlTree.test.ts
#	frontend/src/utils/externalSqlTree.ts
#	frontend/src/utils/sqlDialect.ts
#	internal/ai/service/service.go
2026-06-16 18:35:11 +08:00
Syngnat
6421662f5d 🐛 fix(oracle): 修复裸表查询结果字段元数据缺失
- Oracle-like 裸表查询默认使用连接用户名作为 schema 加载字段元数据
- 修复未写 schema 时结果表头不显示字段类型和注释的问题
- 保持显式 schema.table 查询优先级不变
- 补充查询结果表引用解析和 QueryEditor 回归测试
2026-06-16 14:05:28 +08:00
Syngnat
54195e0591 🐛 fix(sqlserver): 修复对象 SQL 定义获取失败
- SQL Server 对象定义改为通过 sys.all_sql_modules 按库、schema、对象名精确查询
- 增加 sp_helptext 兼容候选,支持拼接多行 Text 返回完整定义
- 统一修复视图、函数/存储过程、触发器定义查看与对象修改入口
- 补充 SQL Server 对象定义查询和组件回归测试
2026-06-16 12:54:39 +08:00
tianqijiuyun-latiao
5fc29a6fd3 feat(i18n): 推进多语言剩余切片闭环
- 补齐 DataGrid、DataViewer、DefinitionViewer、JVM 等模块多语言文案与回归测试
- 收口 JVM 前后端展示、诊断、监控和资源呈现相关多语言路径
- 更新六语言共享词典并保留 raw 边界
2026-06-16 12:40:33 +08:00
Syngnat
f41a15c7b8 feat(data-sync/oceanbase): 拆分比对入口并修复 OceanBase Oracle 连接
- 数据同步:新增表结构比对、数据比对两个独立工具入口
- 比对模式:为 DataSyncModal 增加只读入口展示与模式化文案
- OceanBase:Oracle 租户改用 OB Oracle 专用 MySQL-wire 连接路径
- 连接表单:允许 OceanBase Oracle Service Name 留空,仅 TNS 场景需要填写
- 驱动提示:revision 不匹配提示收敛到驱动管理,不再在普通数据源入口弹出
- 测试覆盖:补充数据比对入口、OceanBase Oracle、driver-agent 提示边界测试
2026-06-16 12:15:16 +08:00
Syngnat
938bc53966 🐛 fix(mysql): 修复 DATE 字段显示为 datetime
- 查询扫描链路透传数据库方言,区分 MySQL 与 Oracle DATE 语义
- MySQL/MariaDB/自定义 mysql 驱动的 DATE/NEWDATE 只展示 YYYY-MM-DD
- 保留 DATETIME/TIMESTAMP 和 Oracle DATE 的时间信息
- 补充值规整与扫描链路回归测试
Close #565
2026-06-16 09:25:16 +08:00
Syngnat
093b3cae1f 🐛 fix(postgres): 修复删除数据库误判当前连接占用
- PostgreSQL 类数据库 DROP DATABASE 自动切换到维护库执行
- 避免前端传入目标库名时被误判为当前连接正在使用
- 同步修复 ALTER DATABASE RENAME 的同类误判
- 补充 PostgreSQL 删除和重命名数据库回归测试
Close #567
2026-06-16 09:07:19 +08:00
Syngnat
0816702084 🐛 fix(external-sql): 修复外部 SQL 文件丢失后标签无法关闭
- 后端读取 SQL 文件失败时返回 file_not_found 结构化错误码
- 前端识别文件被删除或移动的场景,允许用户确认关闭标签
- 保留权限、网络盘异常等非缺失错误的关闭拦截,避免误丢草稿
- 补充前后端测试覆盖缺失文件识别与标签关闭提示
Close #566
2026-06-16 08:48:43 +08:00
Syngnat
c70eb7157f test(oceanbase): 脱敏 Oracle 租户连接测试数据
- 将测试中的内网地址、账号和服务名替换为虚构 fixture
- 统一 OceanBase Oracle 与 SSH 跳板机测试常量
- 保留连接预探测与 SSH 诊断断言语义
2026-06-16 08:37:18 +08:00
Syngnat
23f95d7dc8 feat(query-editor): 支持还原 SQL 美化前内容
- 美化 SQL 前保存最近一次原始内容快照
- 在格式设置菜单提供还原上次美化入口
- 持久化查询标签页的美化恢复快照
- 补充编辑器与状态恢复回归测试
2026-06-16 07:10:04 +08:00
Syngnat
682017ba96 🐛 fix(oceanbase): 修复 Oracle 租户 SSH 预探测超时
- 拆分 OceanBase Oracle 预探测的拨号超时与握手读取超时
- SSH 跳板机场景下使用完整连接超时,避免内网目标被误判不可达
- 保留 MySQL handshake 短读取超时,避免 TNS 端口测试连接变慢
- 补充 SSH 预探测超时与短读取行为回归测试
2026-06-15 17:56:34 +08:00
Syngnat
891c8c1200 🐛 fix(sql-snippet): 修复片段管理编辑与按钮布局
- 支持自定义 SQL 片段按 id 更新,避免修改时重复新增
- 将片段语法说明改为可编辑并随片段持久化
- 将保存、删除、重置、关闭按钮统一收敛到底部操作栏
- 调整操作按钮为大号尺寸并增加最小宽度和底部间距
- 补充片段编辑、布局结构和持久化回归测试
2026-06-15 17:28:48 +08:00
Syngnat
a611c1c04b 🐛 fix(oceanbase): 修复 Oracle 租户跳板机连接预探测失败
- 修复 OceanBase Oracle 预探测未走 SSH 隧道导致内网 IP 被本机直连误判不可达的问题
- 预探测阶段复用完整连接配置,支持通过 SSH 跳板机访问目标地址
- 区分本机 TCP 不可达与 SSH 跳板机访问失败,优化错误提示
- 保留 OBClient 与 TNS 双路径路由逻辑,避免协议判断回退
- 补充 OceanBase Oracle SSH 预探测与网络失败回归测试
2026-06-15 16:13:15 +08:00
tianqijiuyun-latiao
558966a129 feat(i18n): 推进六语言多语言体系与扫描门禁
- 新增共享六语言词典、前端 i18n 运行时与语言设置入口

- 推进连接、驱动、数据网格、查询、AI、Redis、表设计等模块文案本地化

- 补充 raw 边界、SQL/驱动/更新场景测试与 i18n 扫描工具
2026-06-15 14:35:08 +08:00
Syngnat
2f354d2267 feat(saved-query): 新增已存查询独立查看入口
- 侧边栏新增“全部已存查询”根节点,不依赖连接实例或加载数据库
- 按连接、数据库和未匹配状态分组展示后端已加载查询
- 使用独立树节点 key,避免与数据库节点下的同一查询冲突
- 重命名和删除按真实 query id 同步更新所有展示副本
- 补充独立入口分组结构测试,覆盖已匹配和未匹配查询
2026-06-15 14:12:39 +08:00
Syngnat
eca9601ab0 feat(saved-query): 支持已存查询后端持久化与连接重绑
- 后端新增 saved_queries.json 仓库,保存、导入、删除和重绑统一走 Wails 方法
- 启动时导入旧 lite-db-storage 中的 savedQueries 和连接快照,成功后清理旧字段
- 新增连接指纹匹配,唯一强匹配自动重绑,歧义场景保留为未匹配
- 侧边栏新增未匹配已存查询分组,并支持手动绑定到目标连接
- 前端保存、重命名、删除查询改为后端持久化,并补充浏览器 mock
- 补充后端与前端迁移回归测试
2026-06-15 12:20:57 +08:00
Syngnat
0b9f0448c8 ️ perf(database): 优化查询元数据加载和连接释放
- 查询编辑器仅预取当前库及 SQL 显式引用库的元数据
- 断开侧边栏连接时主动释放同实例后端缓存连接
- 完善 Redis 连接释放和 Wails 前端绑定
- 修复 SQL Server 存储过程消息结果显示
- 调整查询工具栏布局并补充回归测试
Close #541
2026-06-15 07:21:00 +08:00
Syngnat
675aae16e9 🐛 fix(query-editor): 修复事务提交按钮缺少悬浮反馈
- 调整提交按钮 hover 和 focus 态的背景与阴影样式
- 同步增强提交计数徽标的悬浮视觉反馈
- 补充提交按钮悬浮样式回归测试
2026-06-14 22:56:21 +08:00
Syngnat
9b0e7937f9 🐛 fix(query-editor): 修复小窗口下 SQL 编辑器工具栏换行问题
- 调整 v2 工具栏为单行布局并启用横向滚动
- 保持选择区、事务区和操作区在窄窗口下不换行
- 补充 SQL 编辑器工具栏布局回归测试
2026-06-14 22:47:42 +08:00
Syngnat
03eb26d999 🐛 fix(query-editor): 修复切换查询页后 SQL 美化方言误判
- 同步查询页的 connectionId 与 dbName 状态
- 美化时按活动连接和 tab 上下文兜底选择格式化方言
- 补充 PostgreSQL ::date 场景回归测试
Close #494
2026-06-14 21:52:14 +08:00
Syngnat
f2ffeeaf45 🐛 fix(sql-editor): 修复存储过程与返回结果写语句的结果识别
- 补齐 SQL 分类逻辑,识别 SQL Server 裸存储过程调用、RETURNING/OUTPUT、SELECT INTO 及消息块场景
- 调整多语句执行与批量写入分支,避免返回行或服务端消息被 Exec 路径吞掉
- 为 PostgreSQL、OpenGauss、Kingbase、HighGo 补充 notice 回传能力并增加回归测试
2026-06-14 21:37:02 +08:00
Syngnat
d7632e29a6 🐛 fix(query-editor): 优化事务工具栏排版并修复 Wails 开发页加载
- 将手动事务提交回滚按钮并入主工具栏,移除重复入口和未提交文案
- 调整 v2 查询工具栏顺序,收起结果区入口并统一更多、AI、设置交互
- 修正 Wails dev 地址为 127.0.0.1,避免 index.html not found
2026-06-14 18:49:22 +08:00
Syngnat
a750266e1c 🐛 fix(sqlserver): 修复托管事务下 UPDATE 误报执行失败
- 统一处理 SQL Server Exec 路径的 RowsAffected 返回
- 兼容 BEGIN/COMMIT/ROLLBACK/SAVE 等事务控制语句无影响行数场景
- 补充 SQL Server 事务控制语句与 DML 的回归测试
2026-06-14 18:03:06 +08:00
Syngnat
5f892d29c8 feat(schema): 支持模式编辑删除及按模式导出备份
- 新增 PostgreSQL 系模式重命名与删除能力
- 侧栏模式节点补充右键菜单、编辑弹窗和删除确认
- 导出表结构与备份表数据支持按模式过滤表和视图
- 同步补充 Wails 绑定与前后端定向测试
Close #526
2026-06-14 17:48:29 +08:00
Syngnat
f3e11961dc 🐛 fix(tdengine): 修复旧版 TDengine 元数据查询与驱动版本选择异常
- 放开 TDengine 已安装驱动的历史版本切换入口
- 兼容低版本 SHOW TABLES FROM 语法差异
- 修复表概览加载时报 [0x2600] syntax error near
- 新增后端兼容与前端交互回归测试
- Close #531
2026-06-14 17:22:02 +08:00
Syngnat
9e224d0067 🐛 fix(query-editor): 修复跨库查询字段补全缺失
- 统一 QueryEditor 中库表标识符与表引用解析规则
- 修复 MySQL 反引号及中划线库名场景下的 WHERE 字段补全
- 新增跨库字段补全回归测试
Close #533
2026-06-14 16:59:34 +08:00
Syngnat
3da3a3fb13 🐛 fix(mysql): 兼容 MyCAT 场景下数据库列表解析逻辑
- 扩展数据库名字段识别,兼容 SCHEMA、database_name 等返回列名
- 按驱动返回列顺序兜底提取单列结果,避免非标准列名导致误判为空
- 补充 MyCAT 风格回归测试,覆盖 SHOW DATABASES 与当前库回退逻辑
Close #552
2026-06-14 16:36:42 +08:00
Syngnat
70b469d349 🐛 fix(query-editor): 修复 SQL 美化未按数据库方言选择格式化器
- 为 QueryEditor 美化入口按当前连接类型动态选择 sql-formatter language
- 让 postgres、kingbase、highgo、vastbase、opengauss、gaussdb 使用 postgresql 方言
- 让 oracle、dameng 使用 plsql,避免固定 mysql 方言导致语法解析失败
- 补充 PostgreSQL 窗口函数与类型转换 SQL 的美化回归测试

Close #561
2026-06-14 16:07:52 +08:00
Syngnat
5310ec7c44 feat(query-editor): 为 Postgres 兼容方言补全增加标识符自动引用
- 在 SQL 编辑器补全中识别需要保留大小写的对象名
- 自动为大写表名和字段名插入双引号标识符
- 保持 MySQL 等其它方言现有补全行为不变
- 补充 QueryEditor 相关测试覆盖

Close #562
2026-06-14 15:54:00 +08:00
Syngnat
6cb5998cd6 feat(datagrid): 为表格编辑增加单元格级撤销能力
- 在 V2 与旧版单元格菜单中增加撤销当前修改入口
- 复用现有改单保存链路回退单元格值与脏标记
- 修复刷新后本地改单状态未完全清理的问题
- 补充相关布局与菜单回归测试

Close #563
2026-06-14 15:41:26 +08:00
Syngnat
6bbe5ad30d 🐛 fix(database-icons): 替换数据源官方图标并修正透明底展示
- 为 OceanBase、GaussDB、GoldenDB、Kafka、RocketMQ 等数据源替换官方品牌资源
- 扩展 DatabaseIcons 对 svg/png/ico 的统一加载与底色边框控制
- 补充图标资源断言测试并移除未使用的字母块 fallback
2026-06-14 14:43:03 +08:00
Syngnat
cd6d034d6c 🐛 fix(icons): 替换 RabbitMQ 为官方标识 2026-06-14 13:53:08 +08:00
Syngnat
df3a49becf feat(icons): 补齐新增数据源品牌图标资源 2026-06-14 13:07:57 +08:00
Syngnat
ca9eb65fdd 🐛 fix(connection-modal): 修复 MQTT 无认证用户名误判必填 2026-06-14 12:59:05 +08:00
Syngnat
8d5a24992a 🐛 fix(sql-editor): 修复事务执行会话与工具栏布局交互 2026-06-14 12:40:31 +08:00
Syngnat
7a85c30752 feat(rocketmq): 新增 RocketMQ 数据源连接与测试发消息支持 2026-06-14 12:19:43 +08:00
Syngnat
0fa8afd517 feat(mqtt): 新增 MQTT 数据源连接与测试发消息支持 2026-06-14 11:38:05 +08:00
Syngnat
d805f288ae feat(rabbitmq): 新增 RabbitMQ 数据源连接与测试发消息支持
- 新增 RabbitMQ 管理 API 数据源实现,支持 vhost、queue、exchange 浏览与队列预览

- 统一消息发送弹窗,支持 Kafka Topic 与 RabbitMQ Queue 的测试发送命令生成

- 补齐连接表单、能力矩阵、SQL 方言、图标与前后端回归测试覆盖
2026-06-14 10:49:11 +08:00
Syngnat
12fbc7ecf4 feat(goldendb): 新增 GoldenDB 数据库连接支持
Refs #477
2026-06-13 21:42:18 +08:00
Syngnat
0ff17dc27c feat(kafka): 新增 Kafka 数据源连接支持
Refs #387
2026-06-13 21:11:08 +08:00
Syngnat
d2f68acae8 feat(gaussdb): 新增 GaussDB 数据库连接支持
Refs #497
2026-06-13 19:34:52 +08:00
Syngnat
f3dfffb8d1 feat(iotdb): 新增 Apache IoTDB 时序库连接支持
Refs #546
2026-06-13 18:23:56 +08:00
Syngnat
c805b16fcd feat(qdrant): 新增 Qdrant 向量库连接支持
- 后端新增 Qdrant REST 连接、collection 元数据、scroll/search 查询与 upsert/delete/payload 更新

- 前端新增 Qdrant 类型、连接配置、图标、方言和能力矩阵

- 测试覆盖 mock REST、真实服务 smoke 和前端配置

Refs #555
2026-06-13 17:03:20 +08:00
Syngnat
56126e22f2 feat(chroma): 新增 Chroma 向量库连接支持
- 后端新增 Chroma REST 连接、元数据浏览、JSON/SELECT 查询与 upsert/delete 写入

- 前端新增 Chroma 类型、连接配置、图标、方言和能力矩阵

- 测试覆盖 v1/v2 兼容、真实服务 smoke 和前端配置

Refs #560
2026-06-13 16:47:25 +08:00
Syngnat
d3836da9cb 🐛 fix(table-designer): 修复触发器编辑测试在 CI 中解析失败
- 移除测试中的 __dirname 依赖
- 改用 import.meta.url 定位 TableDesigner 源文件
- 修复 Ubuntu 前端构建阶段 TypeScript 编译错误
2026-06-12 18:07:03 +08:00
Syngnat
ae2b27c4b4 🐛 fix(table-designer): 优化表设计触发器修改入口
- 修改触发器从固定弹窗改为对象编辑 SQL 标签页
- 生成删除旧触发器和创建新触发器脚本,便于执行前审查
- 抽出触发器编辑 SQL 构造工具,统一 TriggerViewer 与 TableDesigner 逻辑
- 保留新增触发器原弹窗路径,降低行为变更范围
- 新增触发器编辑入口与 SQL 构造回归测试
Refs #557
2026-06-12 17:39:34 +08:00
Syngnat
8519748512 🐛 fix(redis): 修复超过16个数据库无法展示
- 后端改为通过 CONFIG GET databases 动态获取 Redis 数据库数量
- 放宽单机和 Sentinel 模式的 RedisDB 索引限制,支持 db16 及以上
- 前端连接配置和持久化不再裁剪高编号 Redis 数据库
- 连接测试成功后按服务端返回的真实数据库列表展示可选 DB
- 增加 Redis db31 展示、切换、保存和 URI 解析回归测试
Refs #558
2026-06-12 17:22:09 +08:00
Syngnat
70134cd77f 🐛 fix(ci/driver): 修复可选驱动构建脚本语法错误
- 删除 dev/release workflow 中多余的 fi
- 修复 optional driver 构建阶段 bash 解析失败
- 覆盖全平台驱动资产 CI 的语法校验问题
2026-06-12 17:20:27 +08:00
Syngnat
356baa1e38 🐛 fix(ci/driver): 禁用 Windows AMD 驱动 UPX 压缩
- 在驱动产物压缩脚本中跳过 windows/amd64 平台
- 保留 Linux 驱动产物 UPX 压缩与 metadata 自检逻辑
- 补充 Windows amd64 跳过压缩且文件不变的脚本测试
2026-06-12 17:00:09 +08:00
Syngnat
5fcc04a200 🐛 fix(sql-editor): 修复补全提示下连续输入光标跳转
- 调整 SQL 补全触发策略,避免普通字母输入高频触发 provider
- 支持 Monaco 补全取消 token,丢弃连续输入时的过期异步请求
- 将 SQL snippet provider 纳入统一 disposable 管理,避免重复注册残留
- 补充 QueryEditor 回归测试覆盖补全触发、取消和释放链路
Refs #504
2026-06-12 16:25:23 +08:00
Syngnat
77a306beb2 🐛 fix(window): 修复外接显示器恢复后字体模糊
- 恢复策略:Windows 最小化恢复时不再依赖 viewport drift 才触发修复
- 渲染刷新:普通窗口执行 1px 尺寸 nudge,强制 WebView2/DWM 重建渲染 surface
- 体验保护:最大化窗口继续保留 zoom reset + resize,避免可见重复最大化动画
- 测试覆盖:补充 restore 无 drift 场景与自动修复路径断言
Refs #495
2026-06-12 15:58:30 +08:00
Syngnat
f5ee61f589 🐛 fix(ci/driver): 修复驱动 manifest 跨平台校验误判
- 修复 validate-driver-release-manifest 复用同一 revision 文件路径的问题
- 按平台生成后立即解析 revision 快照,避免全部平台读到最后一次生成结果
- 补充 darwin/linux/windows 跨平台 manifest 校验回归测试
- 验证线上 dev manifest 可通过 89 个资产 revision 校验
2026-06-12 15:57:04 +08:00
Syngnat
c99cdb5d0d 🐛 fix(driver-manager/mongodb): 恢复旧版 MongoDB 连接兼容
- 默认版本:将 MongoDB 推荐 driver-agent 切到 1.17.9
- 安装策略:v1 兼容驱动优先尝试发布资产,不再强制源码构建
- 状态提示:已安装 2.x 时提示重装以兼容 MongoDB 4.0
- 前端交互:重装驱动优先选择推荐兼容版本
- 测试覆盖:补充版本选择、下载候选和重装传参回归
2026-06-12 15:18:57 +08:00
Syngnat
c3a3387ee3 feat(ai): 优化 AI 对话体验与 MCP 接入配置
- AI 请求:增强 OpenAI 兼容接口降级逻辑,文本模型自动省略图片并在 400 场景重试
- MCP 接入:支持自定义 HTTP 服务监听地址、端口和 Authorization Bearer Token
- MCP 生命周期:停止服务后保留授权信息,并将主动关闭子进程视为正常停止
- 交互优化:移除 AI 对话导出入口,支持关闭常驻状态提示并收敛设置弹窗 toast 宽度
- UI 调整:优化 AI 输入框边框、聚焦态和 Authorization 运行中只读可查看体验
- 测试覆盖:补充 OpenAI 降级、MCP HTTP、AI Header 和设置面板相关用例
2026-06-12 14:51:37 +08:00
Syngnat
c189125aa4 🐛 fix(redis): 修复多节点连接编辑回填主节点
- 编辑 Redis Sentinel/Cluster 配置时优先保留已保存 host:port 作为主节点

- 合并 hosts 附加节点并去重,避免主节点被附加节点替换

- 补充连接弹窗 Redis 多节点回填回归断言
2026-06-12 13:04:56 +08:00
Syngnat
4cc8ab6482 🐛 fix(driver-manager/sql-editor): 优化驱动代理更新提示和事务提交控件
- 调整 driver-agent revision 为提示性校验,允许旧代理继续安装使用并保留需重装提示

- 精简 SQL 编辑器 DML 事务模式与自动提交档位展示

- 补充旧 revision 安装回归和事务控件断言
2026-06-12 12:57:47 +08:00
Syngnat
d1aa06d537 feat(ai): 支持聊天附件解析并优化数据库对象操作 2026-06-12 12:30:28 +08:00
Syngnat
d5688d31f6 🐛 fix(driver): 允许旧驱动代理继续连接并保留重装提示 2026-06-12 11:41:26 +08:00
Syngnat
ff8bf20680 🐛 fix(ai): 复用待响应气泡承载发送失败状态 2026-06-12 11:21:05 +08:00
Syngnat
5061ec081a 🐛 fix(driver): 修复驱动代理 revision 误触发重装并优化事务提示 2026-06-12 11:07:41 +08:00
Syngnat
e83c9f5553 feat(ai): 增强 Redis 拓扑诊断能力
- 增加 Redis Sentinel/Cluster 状态分级、阻断原因、后端适配器和脱敏 URI 示例

- 区分配置拓扑与后端实际拓扑,修正多节点单机配置的 ClusterClient 诊断

- 补充 AI 工具目录流程与 Redis 哨兵/集群提示文案
2026-06-12 10:05:01 +08:00
Syngnat
8eb4bf3954 ♻️ refactor(redis): 抽离 Redis 连接 URI 与拓扑装配逻辑 2026-06-12 09:43:52 +08:00
Syngnat
3d91079020 Merge pull request #556 from Lang-Qiu/auto-pr/type_annotations-1781208363 2026-06-12 09:22:11 +08:00
Syngnat
ab74270cd4 🧹 chore(types): 规范 Monaco 语言包类型声明 2026-06-12 09:20:37 +08:00
Syngnat
b815e7b296 feat(mcp): 增强新增服务参数逐项提示
- 新增 MCP 参数逐项说明,覆盖未知参数、位置参数和常见运行时参数

- 对敏感参数值做脱敏展示,避免提示区泄露 token 或 api key

- 将逐项说明拆分到独立 util,并接入 inspect_mcp_draft 诊断输出
2026-06-12 09:09:47 +08:00
Syngnat
fce50b513c 🐛 fix(sql-editor): 修复 Oracle 事务结束并补充 Redis 拓扑提示
- SQL 编辑器:Oracle 托管事务优先使用 transaction provider 完成提交和回滚

- Redis:拆分 Key 浏览工具栏并展示 Cluster/Sentinel 拓扑上下文

- 测试:补充 Oracle 事务结束和 Redis 拓扑头部回归用例
2026-06-12 08:48:08 +08:00
Syngnat
781a80e03f feat(ai): 增强代码热点拆分诊断 2026-06-12 08:24:13 +08:00
Syngnat
1058da653d feat(ai): 增强 MCP 草稿参数诊断 2026-06-12 07:57:38 +08:00
Syngnat
0573155285 🐛 fix(sql-editor): 防止事务重复提交误报失败 2026-06-12 07:45:32 +08:00
Syngnat
3427a8844a feat(mcp): 增强新增服务业务参数提示 2026-06-12 07:44:51 +08:00
Syngnat
e353fda7a2 ♻️ refactor(connection): 统一连接驱动类型归一化 2026-06-12 06:58:41 +08:00
Syngnat
f956991bda ♻️ refactor(connection): 拆分连接类型能力配置 2026-06-12 04:26:27 +08:00
Syngnat
0bc6941df6 🐛 fix(oracle): 修复表格编辑事务提交失败 2026-06-12 04:18:29 +08:00
Syngnat
c64b1fbb72 ♻️ refactor(connection): 拆分连接类型目录配置
- 抽出连接类型分组、默认端口和提示文案到独立 catalog

- ConnectionModal 仅负责渲染数据源图标和选择流程

- 补充 catalog 单元测试并更新连接弹窗源码快照测试
2026-06-12 04:07:02 +08:00
Auto PR Bot
72143f6a8f Improve code quality: auto-pr/type_annotations-1781208363 2026-06-12 04:06:04 +08:00
Syngnat
d5d4d4fabc feat(ai): 新增 Redis 拓扑诊断探针
- 新增 inspect_redis_topology 内置工具,输出 Redis 单机、Sentinel、Cluster 脱敏拓扑与风险提示

- 接入本地工具执行链、工具目录、系统引导和工具调用状态文案

- 补充工具注册、目录渲染、执行器和拓扑规则测试
2026-06-12 03:55:26 +08:00
Syngnat
03e08bec32 🐛 fix(redis): 修复 Sentinel 切换数据库配置丢失
- 切换 Redis DB 时复用完整 Connect 逻辑,保留 Sentinel、TLS、SSH 等连接参数

- 补充 Sentinel 切 DB 与 Redis RPC 配置字段回归测试
2026-06-12 03:42:12 +08:00
Syngnat
233894f027 feat(mcp): 增强启动命令参数拆分提示
- 识别 command 字段误填整行 MCP 启动命令

- 提供一键拆分 command 和 args 的表单操作

- 补充参数提示工具与组件回归测试
2026-06-12 03:30:17 +08:00
Syngnat
8f0bd61c14 ♻️ refactor(connection): 拆分 MongoDB 连接配置区
- 将 MongoDB 拓扑、SRV、策略和副本集表单拆入独立组件

- 更新连接弹窗源码断言覆盖拆分后的 Mongo 字段

- 已通过前端定向测试、build 和浏览器 Mongo 表单冒烟
2026-06-12 02:58:05 +08:00
Syngnat
8a0dc3a7d3 🐛 fix(transaction): 修复 Oracle 托管事务提交回滚失败
- Oracle 托管事务改为固定物理连接执行 COMMIT/ROLLBACK

- SQL 编辑器事务按归一化方言判断 Oracle 兼容协议

- 补充 Oracle 与 OceanBase Oracle 事务回归测试
2026-06-12 02:51:01 +08:00
Syngnat
453e13c88d feat(ai): 新增代码热点诊断探针
- 注册 inspect_codebase_hotspots 内置工具并接入本地执行器

- 在工具目录、系统提示和斜杠菜单中暴露大文件治理入口

- 增加工具注册、执行器、目录和斜杠菜单测试
2026-06-12 02:30:09 +08:00
Syngnat
4daaa22cba feat(mcp): 支持一键补齐缺失启动参数
- 在 MCP 参数提示中按当前 command 生成缺失必填参数

- 支持一键追加 docker、npx、python 等常见启动参数

- 增加组件交互测试覆盖参数补齐行为
2026-06-12 02:16:26 +08:00
Syngnat
97f062773b ♻️ refactor(connection): 拆分 Redis 连接配置区
- 抽离 Redis 单机、集群、哨兵配置区到独立组件

- 保留 Redis 密码、Sentinel 密钥和 DB 范围设置行为

- 同步更新连接弹窗源码级回归检查
2026-06-12 02:05:10 +08:00
Syngnat
fb73769063 🐛 fix(oracle): 修复 SQL 编辑器事务提交失败
- Oracle DML 托管事务改用固定连接隐式事务

- 提交和回滚通过 COMMIT/ROLLBACK 结束事务

- 覆盖提交、回滚和执行失败回滚场景
2026-06-12 01:42:14 +08:00
Syngnat
4cac8ef3c9 feat(mcp): 优化新增服务模板入口
- 模板入口移入一行命令快速新增面板

- 增加启动命令预览,降低 command 和 args 拆分成本

- 移除设置页重复模板区块并补充交互测试
2026-06-12 01:41:31 +08:00
Syngnat
156fce531c feat(redis): 支持 Redis Sentinel 连接模式 2026-06-12 01:04:43 +08:00
Syngnat
480edbe501 feat(ai): 增强 AI 诊断快捷命令 2026-06-11 23:03:34 +08:00
Syngnat
81fab81d1b 🐛 fix(sql): 修复 Oracle 托管事务提交回滚失败 2026-06-11 22:49:34 +08:00
Syngnat
cba8ff394c feat(ai): 优化内置工具目录检索与参数提示
- 为内置工具目录增加关键词搜索和结果计数

- 参数提示补充类型、默认值、枚举和示例信息

- 补充目录渲染和参数摘要提取测试
2026-06-11 22:29:37 +08:00
Syngnat
6f4e80c749 feat(mcp): 新增运行期失败诊断探针
- 解析 gonavi.log 中 MCP 启动、发现和调用失败信号

- 结合已保存 MCP 服务与工具发现状态输出原因和 nextActions

- 补充系统引导、工具目录、状态标签和回归测试
2026-06-11 22:01:26 +08:00
Syngnat
a9eed57cf7 feat(mcp): 增强环境变量用途提示
- 新增 MCP 环境变量 key 识别与风险提示

- 在新增 MCP 表单展示 env 用途、占位值和 Docker 边界提醒

- 在 inspect_mcp_draft 输出脱敏 envHints 供 AI 解释参数
2026-06-11 21:34:04 +08:00
Syngnat
890d693102 feat(ai): 增强上游请求 payload 结构诊断
- 新增 inspect_ai_upstream_logs 的 payload 结构摘要,识别模型、消息角色、工具数量和请求选项

- 补充 includePayloadSummary 参数提示和系统引导

- 补充上游日志探针回归测试
2026-06-11 21:16:39 +08:00
Syngnat
9038fe1bdf feat(ai): 增强 MCP 草稿校验输出
- 抽取 MCP 草稿 seed 构建逻辑供 UI 和内置工具复用

- inspect_mcp_draft 返回脱敏 suggestedServerSeed

- 同步 slash 命令、系统指导和回归测试
2026-06-11 20:53:43 +08:00
Syngnat
ca1c8559cf 🐛 fix(query-editor): 修复 Oracle 事务提交回滚失败
- 分离 Oracle 托管事务与单次查询执行上下文

- 避免查询结束后取消 BeginTx 上下文导致事务提前回滚

- 补充 sql.ErrTxDone 回归测试覆盖 Oracle 提交路径
2026-06-11 20:38:50 +08:00
Syngnat
bd2bd49e6d feat(ai): 支持 MCP 一行命令快速新增
- 新增 MCP 完整启动命令快速解析入口

- 将解析结果转换为可编辑服务草稿

- 补充快速新增与设置页渲染测试
2026-06-11 20:04:25 +08:00
Syngnat
c1d27448bc ♻️ refactor(ai): 拆分 MCP 客户端接入面板
- 将客户端选择和状态详情拆为独立组件

- 保留本机安装、远程桥接和快速配置展示行为

- 补充选择区和状态区渲染测试
2026-06-11 19:47:11 +08:00
Syngnat
e6d2685521 ♻️ refactor(ai): 拆分远程 MCP 快速配置面板
- 将 OpenClaw/Hermans 远程接入说明抽成独立展示组件

- 保留 schema-only、Bearer Token 和云端配置说明

- 补充远程快速配置渲染测试
2026-06-11 19:28:38 +08:00
Syngnat
8006844b9f 🐛 fix(datagrid): 修复 Oracle DATE 编辑丢失时间
- Oracle-like DATE 字段按日期时间编辑,保留时分秒
- 普通 date 字段维持纯日期编辑行为
- 补充时间编辑器回归测试
2026-06-11 18:27:44 +08:00
Syngnat
5d4989f68f feat(ai): 增加 MCP HTTP 服务与 Docker 配置诊断
- AI 设置页新增 GoNavi MCP HTTP 服务开关与状态展示
- 后端新增 HTTP MCP 子进程生命周期管理和鉴权配置
- 增加 Docker MCP 配置诊断工具与参数提示校验
2026-06-11 18:27:13 +08:00
Syngnat
b7e50118f0 Merge remote-tracking branch 'origin/pr-554' into dev 2026-06-11 18:12:59 +08:00
Arthur
5fc0a6504a fix(ui)旧版UI左侧列表无法通过鼠标扩大问题修复 2026-06-11 16:04:04 +08:00
Syngnat
c9053bccc5 feat(ai): 完善 MCP Docker 启动参数指引
- 新增 Docker MCP 启动模板和参数顺序提示

- 校验 docker run、-i 和镜像名等易漏参数

- 同步 MCP 设置页说明、空状态和单元测试
2026-06-11 15:45:52 +08:00
Syngnat
e4672062f8 🐛 fix(query-editor): 支持 Oracle SQL 编辑器托管事务
- 新增 driver transaction 执行器,支持不适合文本 BEGIN 的数据库

- Oracle SQL 编辑器 DML 托管事务改用 database/sql Tx 提交和回滚

- 补充 Oracle 托管事务提交和失败回滚回归测试
2026-06-11 15:45:13 +08:00
Syngnat
06583abad9 feat(query-editor): 完善片段说明事务状态和结果分页
- SQL 片段新增可编辑语法说明并用于补全详情

- 事务模式改为中文展示并显示未提交变更语句数

- 查询结果支持分页翻页与重新查询全部导出
2026-06-11 14:55:35 +08:00
Syngnat
ce568362c6 ♻️ refactor(sidebar): 拆分侧边栏核心工具函数 2026-06-11 14:15:30 +08:00
Syngnat
ed67a72b68 ♻️ refactor(ai): 拆分 AI 本地检查工具执行器 2026-06-11 13:47:17 +08:00
Syngnat
7850f49429 ♻️ refactor(ai): 拆分 AI 内置检查工具元数据模块 2026-06-11 13:37:48 +08:00
Syngnat
440172aae8 feat(ai): 记录 Claude CLI 上游请求入参日志
- 统一 Claude CLI 聊天请求写入 AI 上游请求日志

- 记录脱敏后的 prompt、参数、模型和工具名称

- 补充 CLI 上游日志解析与脱敏测试
2026-06-11 13:04:59 +08:00
Syngnat
19989e4c26 feat(mcp): 增强新增服务参数填写提示 2026-06-11 12:37:02 +08:00
Syngnat
4265d7cfa9 feat(ai): 新增上游请求日志自查工具 2026-06-11 12:00:17 +08:00
Syngnat
2d562ccfd6 feat(ai): 记录上游请求入参与会话日志
- 增加 OpenAI、Anthropic、Gemini 上游请求体与响应状态日志

- 为 AIChatSend 和 AIChatStream 补充会话生命周期日志

- 增加请求日志脱敏与 URL 密钥过滤测试
2026-06-11 11:07:22 +08:00
Syngnat
74a422a5e2 🐛 fix(sync): 修复 Oracle 同步连接与 MySQL 备份导出异常
- 分离 Oracle/OceanBase Oracle 同步连接 Service Name 与选中 schema

- 兼容旧同步请求中 database 被 schema 覆盖的情况

- 规范 MySQL/MariaDB SHOW CREATE TABLE 标识符引用

Refs #549

Refs #518
2026-06-11 10:24:48 +08:00
Syngnat
450d1d66b4 feat(ai): 完善远程 MCP 结构模式与面板稳定性
- MCP HTTP 支持 schema-only 模式,远程配置默认不暴露 execute_sql

- OpenClaw/Hermans 向导补充安全边界与结构模式命令

- 拆分 AI 面板错误边界和 Linux CJK 字体提示组件
2026-06-11 09:26:54 +08:00
Syngnat
4a944ad23f feat(ai): 完善远程 MCP 指引与排障体验 2026-06-11 08:31:20 +08:00
Syngnat
26fb650e04 feat(ai): 增强 MCP 远程接入与上下文诊断 2026-06-11 07:29:04 +08:00
Syngnat
d3278bb4c4 feat(mcp): 强化远程 Agent 接入配置指引 2026-06-11 01:11:59 +08:00
Syngnat
b438881a50 🐛 fix(ci): 修复跨仓库驱动发布鉴权失败 2026-06-11 00:22:15 +08:00
Syngnat
b11b662071 feat(ai): 完善工具目录与远程 MCP 接入指引
- 新增 inspect_ai_tool_catalog 工具,返回内置探针流程、参数提示和 MCP 工具摘要

- 拆分 AI 内置工具目录配置,降低 AIBuiltinToolsCatalog 体积

- 补充 OpenClaw/Hermans 远程 MCP Streamable HTTP 配置说明

- 增加 Linux CJK 字体缺失检测与 Ubuntu 安装提示
2026-06-10 23:52:19 +08:00
Syngnat
11156c941c feat(mcp): 增加远程 Agent 接入检查工具
- 新增 inspect_mcp_remote_access 内置工具,输出 Streamable HTTP MCP、Bearer Token、隧道和安全边界指引

- 注册工具目录与工具元信息,覆盖 OpenClaw/Hermans 云端 Agent 访问 Windows GoNavi 场景

- 补充远程 MCP 快照和本地工具执行器测试,清理 SQL 编辑器冗余补全常量
2026-06-10 22:19:24 +08:00
Syngnat
0394667680 feat(mcp): 支持远程 Agent 接入和 HTTP 模式 2026-06-10 21:28:00 +08:00
Syngnat
856a5158e4 ♻️ refactor(query-editor): 抽离 SQL 事务控制逻辑 2026-06-10 20:36:25 +08:00
Syngnat
fb00f47031 feat(query-editor): 对齐 DBeaver 风格事务提交模式 2026-06-10 20:24:45 +08:00
Syngnat
55a52bb0f3 feat(ai): 增加 MCP 工具 arguments 示例 2026-06-10 20:07:19 +08:00
Syngnat
630044b740 feat(ai): 显示 MCP 工具参数摘要 2026-06-10 19:58:18 +08:00
Syngnat
69f51f8ec8 ♻️ refactor(query-editor): 拆分 SQL 事务提交设置控件 2026-06-10 19:47:33 +08:00
Syngnat
ab053ef7d1 ♻️ refactor(query-editor): 拆分 SQL 事务工具栏组件
- 抽离 SQL 编辑器待提交事务提示与提交回滚按钮

- 保持 QueryEditor 事务状态与回调逻辑不变

- 同步组件结构测试并验证构建
2026-06-10 19:22:47 +08:00
Syngnat
89639e36bc 🐛 fix(query-editor): 修正 SQL 编辑器 DML 事务识别
- 统一前后端 DML 与数据修改 CTE 的受管事务判断

- 保留数据修改 CTE 返回行并补充事务回归测试

- 明确 SQL 编辑器事务提交策略文案
2026-06-10 19:13:54 +08:00
Syngnat
cf8f9be8dc feat(ai): 新增 SQL 编辑器事务状态探针
- 新增 inspect_sql_editor_transaction 内置探针,返回提交模式、待提交事务和当前 SQL 页签事务语义

- 将 SQL 编辑器待提交事务状态登记到 store,供 AI 只读诊断使用

- 增加 /tx 斜杠菜单、工具目录、系统引导和回归测试
2026-06-10 18:53:24 +08:00
Syngnat
156631c263 feat(ai): 完善 MCP 新增字段填写提示
- 增加 command、args、env、timeout 的应填与勿填对照

- 抽取 MCP 字段指南卡片复用速查与表单说明

- 补充 AI 设置 MCP 区域渲染测试
2026-06-10 18:19:10 +08:00
Syngnat
d8da8d6abf 🐛 fix(editor): 修正 SQL 编辑器 DML 事务提交语义
- SQL 编辑器 DML 固定进入托管事务

- 区分 WITH SELECT 和 WITH DML 的事务判定

- 调整提交方式文案并补充前后端回归测试
2026-06-10 18:05:46 +08:00
Syngnat
61d71cf1d0 feat(editor): 支持 SQL 编辑器增删改事务提交
- 为 SQL 编辑器 DML 新增后端托管事务会话和提交回滚接口

- 增加手动提交与自动提交设置,并显示待提交状态

- 补充前后端事务执行、提交、回滚和自动提交测试
2026-06-10 17:18:34 +08:00
Syngnat
7eb086cade feat(ai): 新增 MCP 草稿校验探针
- 新增 inspect_mcp_draft 内置工具,支持完整命令和分字段草稿校验

- 补充 MCP 新增指引、斜杠菜单和工具目录流程

- 增加工具注册、执行器和系统提示相关测试
2026-06-10 17:17:37 +08:00
Syngnat
c9d0bce153 🐛 fix(workbench): 修复结果区切换与对象设计字段定义
- 修复手动隐藏结果区后快捷键无法再次显示的问题

- 补全 Dameng/Oracle-like 字段长度、精度和可空标识

- 增加 SQL 编辑器、字段元数据和对象设计回归测试
2026-06-10 15:51:03 +08:00
Syngnat
1d1d8d21cd feat(ai): 新增 MCP 工具参数探针 2026-06-10 15:19:23 +08:00
Syngnat
c4153202ba feat(editor): 完善 SQL 编辑与数据编辑交互
- 结果区状态按 SQL Tab 独立保存,快捷键可恢复手动隐藏面板

- 对象设计保留完整字段类型和可空信息,完善兼容驱动 DDL 元数据

- 数据编辑新增手动/自动提交设置和自动提交倒计时

- 修复 schema 视图定位时找不到左侧树节点的问题
2026-06-10 14:27:40 +08:00
Syngnat
8ddd8a726d feat(ai): 新增 AI 消息流诊断探针
- 新增 inspect_ai_message_flow 内置工具

- 识别连续 assistant 气泡、空消息和未闭环工具调用

- 同步工具目录、系统引导、执行状态文案和回归测试
2026-06-10 12:59:09 +08:00
Syngnat
e16082af9a 🐛 fix(sidebar): 修复 GDB 兼容库视图定位失败
- 增加 MySQL 兼容视图元数据查询回退

- 统一编辑器和左侧树的视图元数据查询

- 放宽视图分组下缺失节点类型时的可视定位兜底
2026-06-10 12:14:27 +08:00
Syngnat
2c7962f5d3 feat(ai): 完善 MCP 新增参数指引
- 新增 npx MCP 服务模板和启动预览

- 补充 command/args 表单说明与内置指引快照

- 覆盖 MCP 参数草稿、指引和 AI 配置检查测试
2026-06-10 12:13:45 +08:00
Syngnat
d6f552d539 🐛 fix(sidebar): 修复国产兼容库视图定位失败
- 统一 Sidebar 与 SQL 编辑器的元数据方言解析

- 兼容 GDB/GoldenDB/GreatDB 等 MySQL 兼容驱动的视图元数据

- 放宽左侧树视图定位对 objectType 节点的识别
2026-06-10 11:07:50 +08:00
Syngnat
8f86c4419b ♻️ refactor(ai-tests): 拆分连接与本地资产探针测试
- 将连接能力、外部 SQL、本地查询资产探针测试拆入独立文件

- 继续缩减 aiLocalToolExecutor.test.ts,降低单文件维护成本

- 通过定向测试和生产构建验证
2026-06-10 10:39:27 +08:00
Syngnat
b4affbc1d5 ♻️ refactor(ai-tests): 拆分 AI 配置探针测试
- 将 AI runtime、provider、safety、MCP 与 guidance 探针测试移入独立文件

- 缩减 aiLocalToolExecutor.test.ts 体积,保持执行器行为不变

- 通过定向测试和生产构建验证
2026-06-10 10:00:37 +08:00
Syngnat
0834d8cb3d feat(ai): 将渲染异常纳入应用健康总览
- 在 inspect_app_health 汇总最近一次 AI 回复气泡渲染异常

- 同步内置工具目录和系统探针引导

- 补充应用健康和本地工具执行器测试
2026-06-10 09:24:07 +08:00
Syngnat
d0b9a7f50c feat(ai): 增强 MCP 配置诊断能力
- 复用新增 MCP 表单校验逻辑,向 inspect_mcp_setup 输出服务配置问题

- 将 MCP 配置问题汇总到 AI 设置体检,补充后续处理建议

- 补充 MCP 快照、AI 体检和本地工具执行器测试
2026-06-10 08:33:06 +08:00
Syngnat
7df524e9ef feat(mcp): 增加 MCP 服务配置实时校验
- 新增 MCP 服务草稿校验工具,识别空命令、整行命令、无效环境变量和异常超时

- 在 MCP 服务表单展示配置检查结果,并阻止明显无效配置直接测试或保存

- 补充 MCP 参数校验和卡片渲染回归测试
2026-06-10 02:01:33 +08:00
Syngnat
cb90a4ad01 🐛 fix(sidebar): 修复视图定位慢加载误报
- 兼容 table-prefixed key 的视图兜底定位

- 区分对象树仍在加载与真实未找到状态

- 补充 sidebarLocate 与 Sidebar 定位回归测试
2026-06-10 01:13:29 +08:00
Syngnat
01d8fe44ce feat(ai-tools): 新增 AI 应用健康总览探针 2026-06-10 00:41:48 +08:00
Syngnat
73e93e955c 🐛 fix(sidebar): 修复视图缺失元数据时定位失败 2026-06-09 23:51:36 +08:00
Syngnat
c742b4d61e 🐛 fix(sidebar): 修复视图定位误报未找到
- 视图定位失败时增加表分支可视节点兜底

- 命令搜索对象定位传入精确树节点 key

- 补充国产库视图与元数据缺失场景回归测试
2026-06-09 23:26:15 +08:00
Syngnat
ce06bea744 ♻️ refactor(ai-tools): 拆分 SQL 风险探针执行器
- 将 inspect_sql_risk 执行逻辑从聚合探针执行器中拆出

- 将 SQL 风险工具调用测试迁移到独立测试文件

- 保持本地工具调用行为不变并降低后续扩展成本
2026-06-09 22:31:30 +08:00
Syngnat
48de0b83c4 feat(ai-tools): 新增 SQL 风险预检并优化视图定位
- 新增 inspect_sql_risk 内置工具,识别多语句、写入、DDL、无 WHERE 和安全策略风险

- 在 AI 设置内置工具目录和系统提示中补充 SQL 风险预检链路

- 修复同名视图定位时优先当前数据库 schema 的匹配逻辑
2026-06-09 21:56:28 +08:00
Syngnat
f0afff68c4 ♻️ refactor(ai-chat): 拆分本地工具调用链
- 抽出 useAIChatLocalTools 承载工具执行、熔断和回灌模型逻辑

- 补齐重试消息的工具上下文依赖,避免配置变更后使用旧闭包

- 增加 hook 行为测试并同步 MCP 指南断言
2026-06-09 21:18:39 +08:00
Syngnat
67e0cc752b feat(ai-mcp): 增加 MCP 参数填写速查
- 在 MCP 设置页常驻展示 command / args / env / timeout 字段说明
- 复用现有字段指南,补充示例和填写边界
- 增加无服务场景下的渲染断言
2026-06-09 20:31:59 +08:00
Syngnat
d78c4481f0 🐛 fix(sidebar): 兼容国产库视图定位
- 兼容 MySQL 协议国产库返回的 SYSTEM VIEW / BASE VIEW 类型
- 同步 SQL 编辑器与左侧树的视图元数据识别逻辑
- 增加节点元数据缺失时的唯一可视标识兜底定位
2026-06-09 20:31:24 +08:00
Syngnat
75b60f94d2 🐛 fix(sidebar): 修复视图定位误报未找到 2026-06-09 19:38:41 +08:00
Syngnat
c17d867aa6 🐛 fix(sidebar): 兜底定位表分支中的视图节点 2026-06-09 18:45:30 +08:00
Syngnat
327a78f1cb feat(ai-mcp): 支持 Windows 完整命令自动拆分 2026-06-09 18:44:39 +08:00
Syngnat
af51ead948 🐛 fix(sidebar): 修复视图裸名定位失败 2026-06-09 17:51:43 +08:00
Syngnat
9fab48e64f ♻️ refactor(ai-chat): 拆分会话标题生成逻辑
- 将自动标题生成从 AIChatPanel 抽到独立 hook

- 补充标题生成、清洗和稳定回调测试
2026-06-09 17:24:26 +08:00
Syngnat
acfa112415 🐛 fix(ai-chat): 修复流式回复分裂为多个气泡
- 持久化流式回复状态,避免重渲染后丢失当前 assistant 消息

- 补充回归测试覆盖 chunk 追加到同一气泡
2026-06-09 16:45:39 +08:00
Syngnat
79094d4f3b feat(ai-mcp): 补充 MCP 服务配置排错指引 2026-06-09 16:21:03 +08:00
Syngnat
da7559426c ♻️ refactor(ai-mcp): 拆分外部客户端安装状态逻辑 2026-06-09 15:39:30 +08:00
Syngnat
c8f11d7258 ♻️ refactor(query-editor): 拆分 SQL 结果区渲染组件 2026-06-09 14:57:29 +08:00
Syngnat
c45961f027 🐛 fix(db): 保留多写语句结果并修复 MySQL 字符集参数
- 多条写语句改为逐条返回 affectedRows,避免只显示最后一条结果

- 为写语句结果补充 statementIndex,保持语句与结果映射

- 保留 MySQL charset fallback 逗号,避免驱动解析成 %2C
2026-06-09 14:13:35 +08:00
Syngnat
a6105f4807 feat(query-editor): 支持结果区默认隐藏和快捷键切换
- 默认隐藏 SQL 结果区,执行成功或失败后自动展开

- 增加结果区显示/隐藏按钮和 Win/Mac 快捷键提示

- 在结果表工具栏提供隐藏入口并补充前端回归测试
2026-06-09 14:13:06 +08:00
Syngnat
77b58baff7 ♻️ refactor(sidebar): 拆分 V2 侧栏工具逻辑 2026-06-09 13:39:11 +08:00
Syngnat
8543f1dc65 ♻️ refactor(ai-message): 拆分 Markdown 代码块渲染逻辑 2026-06-09 13:08:01 +08:00
Syngnat
17a3d72852 feat(ai-mcp): 优化外部客户端安装选择状态 2026-06-09 12:11:03 +08:00
Syngnat
b5ba49ff8f ♻️ refactor(ai-tools): 拆分数据库工具执行器逻辑 2026-06-09 11:19:15 +08:00
Syngnat
83972d29b7 ♻️ refactor(ai-chat): 拆分输入区动作按钮组件 2026-06-09 10:43:32 +08:00
Syngnat
58a9adad34 ♻️ refactor(ai-context): 拆分系统上下文探针提示模块 2026-06-09 10:06:19 +08:00
Syngnat
7d1e066997 feat(ai-tools): 新增连接失败诊断探针并接入快捷命令
- 新增基于 gonavi.log 的连接失败总结探针与结构化根因分类\n- 接入 slash 命令、内置工具目录、状态文案和系统提示\n- 补齐本地执行、insight 解析、指令筛选和注册链路测试
2026-06-09 09:45:24 +08:00
Syngnat
9be10beadc 🐛 fix(ai-mcp): 澄清外部客户端接入文案并统一状态语义
- 将外部客户端安装区统一表述为接入流程\n- 按客户端状态调整主按钮文案与重复操作禁用态\n- 同步 MCP 设置导航描述与相关测试断言
2026-06-09 09:23:39 +08:00
Syngnat
8529fbd9e2 ♻️ refactor(ai-mcp): 拆分 MCP 服务卡片并收敛表单逻辑 2026-06-09 08:49:00 +08:00
Syngnat
86095b5bf1 feat(ai-chat): 新增渲染异常探针并拆分聊天面板逻辑 2026-06-09 08:29:02 +08:00
Syngnat
c081d23cc4 ♻️ refactor(ai-settings): 拆分 AI 设置预设与服务桥接配置 2026-06-09 08:04:06 +08:00
Syngnat
6841e69008 feat(ai-tools): 新增日志与快捷键探针并完善 MCP 配置引导 2026-06-09 07:46:38 +08:00
Syngnat
ee5623d290 feat(ai-mcp): 增强 MCP 新增指引与内置工具提示 2026-06-09 06:33:28 +08:00
Syngnat
ae3e08d5f6 🐛 fix(ai-mcp): 强化外部客户端接入说明与单选安装状态 2026-06-09 05:47:55 +08:00
Syngnat
15e0766bbb feat(ai-chat): 新增诊断类 slash 命令并拆分输入区状态 2026-06-09 05:29:06 +08:00
Syngnat
25fb3502e1 feat(ai-tools): 新增AI配置体检探针并拆分本地快照执行器
- 新增 inspect_ai_setup_health 统一诊断供应商、聊天前置、MCP 接入和提示词技能状态
- 拆分 AI 配置类本地快照执行器与共享运行时类型,收缩 aiSnapshotInspectionToolExecutor 体积
- 补充内置工具目录、系统提示链路、定向测试与构建验证
2026-06-09 04:56:30 +08:00
Syngnat
6e7b8ceb39 feat(ai-tools): 新增最近 SQL 活动总结探针
- 新增 inspect_recent_sql_activity 总结最近 SQL 活动的读写与报错分布
- 抽离 SQL 日志洞察模块并复用 recent logs 快照逻辑
- 补齐工具目录、系统提示、状态文案与测试覆盖
2026-06-09 04:22:31 +08:00
Syngnat
b4f46aeecd feat(ai-tools): 新增外部 SQL 文件探针 2026-06-09 03:51:04 +08:00
Syngnat
4162a6491b feat(ai-tools): 新增外部 SQL 目录探针
- 新增外部 SQL 目录快照构建与本地工具执行入口
- 补充内置工具目录、系统提示和状态文案
- 覆盖 AI 面板、工具注册与探针链路测试
2026-06-09 03:26:04 +08:00
Syngnat
f7648413ed ♻️ refactor(ai-chat): 拆分流式消息订阅 Hook 2026-06-09 03:05:00 +08:00
Syngnat
9dde59a6c7 🐛 fix(ai-mcp): 明确外部客户端接入语义与状态提示 2026-06-09 03:01:25 +08:00
Syngnat
f7c20f6d79 ♻️ refactor(ai-chat): 抽离聊天消息分发助手 2026-06-09 02:21:52 +08:00
Syngnat
747cabe447 feat(ai-tools): 新增 AI 历史会话探针 2026-06-09 02:11:35 +08:00
Syngnat
b723b16671 🐛 fix(ai-mcp): 统一外部客户端安装状态文案 2026-06-09 02:07:30 +08:00
Syngnat
bffad0c3a3 ♻️ refactor(ai-chat): 抽离运行时资源加载与设置同步
- 新增 useAIChatRuntimeResources 管理供应商、模型、MCP 工具和 Skills 加载
- 收拢 AI 设置事件监听与模型列表刷新逻辑,减少面板内部副作用堆叠
- 保持 AI 面板行为不变,并通过定向测试、构建和真实页面路径复验
2026-06-09 01:44:20 +08:00
Syngnat
0a48f70643 ♻️ refactor(ai-chat): 拆分面板会话视图与派生状态逻辑
- 抽离 AIChatPanelConversationView 承载欢迎态、历史态和洞察态渲染
- 下沉连接推断、上下文统计和会话裁剪等派生逻辑到独立模块
- 补充守卫测试并验证 AI 面板定向测试、构建和真实页面切换
2026-06-09 01:31:42 +08:00
Syngnat
0a229e8156 feat(ai-tools): 新增能力探针并优化 MCP 接入指引
- 新增 inspect_connection_capabilities 内置探针与工具目录入口\n- 优化 MCP 外部客户端接入状态表达和重复写入保护\n- 同步调整 AI 设置相关测试与系统提示
2026-06-09 00:59:25 +08:00
Syngnat
f7ed6f8e61 ♻️ refactor(ai-tools): 拆分本地工具执行链路
- 抽离 AI 本地工具默认运行时封装

- 拆分数据库工具执行器并保留现有返回协议

- 收敛 aiLocalToolExecutor 为工具路由与结果包装
2026-06-09 00:16:57 +08:00
Syngnat
20e817b203 feat(ai-tools): 新增安全边界探针并拆分工具注册 2026-06-08 23:50:15 +08:00
Syngnat
1284c8c8ef ♻️ refactor(ai-settings): 抽离 MCP 客户端安装状态逻辑 2026-06-08 23:18:12 +08:00
Syngnat
7ac9b89b7d feat(ai-mcp): 补充新增服务字段速查与超时捷径 2026-06-08 22:56:56 +08:00
Syngnat
c0e29a08ba feat(ai-settings): 优化 MCP 新手引导并新增连接盘点探针 2026-06-08 22:42:12 +08:00
Syngnat
cc788d1b25 feat(ai-mcp): 增强完整命令拆分预览 2026-06-08 22:04:24 +08:00
Syngnat
7fa23e72c0 feat(ai-chat): 增强发送前状态提示并新增就绪探针 2026-06-08 21:47:10 +08:00
Syngnat
5ce5d03d69 feat(ai-tools): 新增供应商与模型配置探针 2026-06-08 20:59:15 +08:00
Syngnat
4ac6a9e798 feat(ai-tools): 新增 AI 提示与技能配置探针 2026-06-08 20:41:45 +08:00
Syngnat
472686e8ff feat(ai-tools): 新增 MCP 配置探针并拆分本地执行器 2026-06-08 20:15:29 +08:00
Syngnat
dc38602d32 feat(ai-tools): 新增 AI 运行时探针 2026-06-08 19:50:39 +08:00
Syngnat
5c867fd121 feat(ai-mcp): 补充外部客户端命令检测状态 2026-06-08 19:33:05 +08:00
Syngnat
d7879d9ef0 feat(ai-tools): 新增历史查询与片段探针
- 新增 inspect_saved_queries 与 inspect_sql_snippets 内置工具
- 拆出本地 SQL 资产快照 helper,并补齐执行器与测试覆盖
- 补充工具目录展示、系统提示和执行状态文案
2026-06-08 19:11:23 +08:00
Syngnat
8aa2932878 🐛 fix(ci): 修复 DuckDB Windows 可选驱动构建的 CGO 参数污染 2026-06-08 18:53:55 +08:00
Syngnat
f19ff5fdd5 feat(ai-tools): 新增当前连接探针并拆分 AIChatPanel 运行时模块 2026-06-08 18:53:12 +08:00
Syngnat
7cce1ce30a 🐛 fix(ci): 修正 DuckDB Windows 动态链接参数
- 统一 Windows DuckDB 动态库构建使用的 CGO LDFLAGS
- 为 workflow 中的 DuckDB 库目录补充 GCC 友好的路径格式
- 补充 DuckDB Windows 动态链接参数的回归测试
2026-06-08 18:20:54 +08:00
Syngnat
92e9b0ef75 🐛 fix(ai-mcp): 优化外部客户端默认选中逻辑
- 抽离 MCP 客户端状态归一化与命令格式化工具
- 优先默认选中已接入但需要更新的外部客户端
- 补充状态选择与启动命令格式化测试
2026-06-08 18:15:08 +08:00
Syngnat
a54a357e4b 🐛 fix(ci): 修复 DuckDB Windows 导入库生成链路
- 改为从 duckdb.dll 生成 MinGW 可用的导入库文件
- 同步修复 dev/release workflow 与本机源码构建的 DuckDB Windows 依赖准备逻辑
- 新增导入库生成命令与 buildutil 单测
2026-06-08 17:59:58 +08:00
Syngnat
2e5c3473e1 feat(ai-chat): 新增上下文探针并拆分输入区组件
- 新增 inspect_ai_context 内置工具与系统提示引导
- 拆分 AIChatInput 的上下文、附件、提示条和斜杠命令模块
- 补充工具目录、执行器与输入区相关测试
2026-06-08 17:58:52 +08:00
Syngnat
5f3d1d9880 ♻️ refactor(ai-chat): 抽离系统上下文提示构建逻辑 2026-06-08 17:25:50 +08:00
Syngnat
a575fb49db 🐛 fix(ci): 修复DuckDB动态库同路径复制问题 2026-06-08 17:14:27 +08:00
Syngnat
53b4fcb842 feat(ai-mcp): 补强MCP参数填写引导 2026-06-08 17:06:58 +08:00
Syngnat
2c95009d1f feat(ai-tools): 新增工作区页签探针并拆分界面洞察模块 2026-06-08 17:05:29 +08:00
Syngnat
5b8bbd672e feat(ai-mcp): 优化外部客户端接入引导与状态识别
- 调整 Claude Code 和 Codex 的安装文案与状态标签,明确是把 GoNavi 暴露给外部客户端使用
- 优化 MCP 客户端默认选择逻辑,优先聚焦未接入或需更新的目标并避免刷新后乱跳
- 同步补齐前端 mock、后端状态文案和定向测试,确保安装区交互与状态展示一致
2026-06-08 16:29:53 +08:00
Syngnat
54f1f6970c 🐛 fix(ci): 增强DuckDB库下载容错
- 为 Windows DuckDB 动态库下载增加 curl 重试和超时控制
- 增加 zip 校验与 duckdb.dll/duckdb.lib 存在性检查
- 下载失败时清理临时目录后重试,避免空文件继续参与链接
- 同步修复 dev-build 与 release 工作流的相同脆弱点
2026-06-08 16:14:05 +08:00
Syngnat
8a1e65640e feat(ai-tools): 新增最近SQL日志探针
- 新增 inspect_recent_sql_logs 内置工具用于回看最近 SQL 执行历史
- 接入本地工具执行链,支持按成功或失败状态筛选日志
- 更新 AI 设置内置工具目录、流程说明和工具状态文案
- 完成 vitest、生产构建与预览页内置工具目录验证
2026-06-08 16:10:09 +08:00
Syngnat
0312dfbb16 ♻️ refactor(ai-chat): 拆分洞察与历史模式视图
- 提取 AI 面板的自动洞察与内联历史为独立展示组件
- 保持会话切换与发送主链留在 AIChatPanel 中
- 补充模式内容与消息边界相关回归测试
- 完成 vitest、生产构建与本地预览切换验证
2026-06-08 15:50:40 +08:00
Syngnat
02afeba564 🐛 fix(ci): 为driver-agent压缩增加metadata自检回退
- 在 UPX 压缩脚本里增加 driver-agent metadata 烟测
- 压缩后若 metadata 探测失败则自动恢复原始二进制
- 避免单个驱动因 UPX 压缩后失效导致整个平台构建失败
- 补充压缩脚本的 smoke test 覆盖恢复路径
2026-06-08 09:49:38 +08:00
Syngnat
1a2462ef17 🐛 fix(ai-settings): 修正MCP环境变量录入反馈
- 抽离环境变量草稿解析工具,区分有效项和无效行
- 保留用户原始输入,避免无效行被静默吞掉
- 在 MCP 服务卡片中显示识别数量与无效行提示
- 补充环境变量解析与卡片提示测试
2026-06-08 09:29:40 +08:00
Syngnat
c76b634739 ♻️ refactor(ai-settings): 拆分供应商配置视图组件
- 外提供应商列表与编辑表单组件,收敛 AISettingsModal 体积

- 保留 API Key 掩码、预设卡片切换与返回编辑流

- 补充定向测试、前端构建和真实预览验证
2026-06-08 09:14:55 +08:00
Syngnat
fc88e21811 feat(ai-settings): 补充全库结构快照并拆分AI设置侧栏
- 新增 inspect_database_bundle 内置工具与状态文案

- 拆分 AI 设置导航、安全控制和上下文卡片组件

- 补充定向测试、前端构建和真实预览验证
2026-06-08 09:00:59 +08:00
Syngnat
e1cebb1c9a feat(ai-settings): 优化MCP录入引导并补充结构快照工具
- 新增完整命令自动拆分与提示词设置分区,降低 MCP 配置门槛

- 新增 inspect_table_bundle 内置工具并补充状态文案

- 补齐定向测试、前端构建与预览验证
2026-06-08 08:24:27 +08:00
Syngnat
7d7b775fe0 feat(ai-settings): 优化MCP安装引导并补充表样例预览工具
- 拆分 Skills 区块并补充 MCP 启动模板\n- 改造 Claude/Codex 安装目标选择与状态展示\n- 新增 preview_table_rows 内置工具与目录说明
2026-06-08 08:00:43 +08:00
Syngnat
6c53fb14a6 feat(ai-mcp): 补全跨表字段探针并拆分 MCP 设置区块
- 新增 get_all_columns 内置工具和 MCP server 只读探针
- MCP 服务表单增加实际启动命令预览并补强参数提示
- 抽离 AISettingsMCPSection 并补齐前后端测试
2026-06-08 07:29:52 +08:00
Syngnat
aad0f447c0 🐛 fix(ci): 为前端产物下载增加重试保护
- 为 build 阶段的 frontend-dist 下载增加失败清理和一次自动重试
- 将前端产物固定下载到 frontend-artifact 目录并在解压前校验 tar 文件存在
- 同步加固 dev 与 release 工作流,降低单个 runner 下载抖动导致的误报失败
2026-06-08 06:54:00 +08:00
Syngnat
9d3c77755d ♻️ refactor(ai-message): 拆分消息气泡渲染并补齐展示测试
- 抽离 AIMessageMarkdown 与 AIMessageStatusBlocks,拆分代码块、思考态和探针状态渲染职责
- 优化探针结果查找链路,使用 Map 减少消息渲染时的重复扫描
- 新增消息气泡与 Markdown 代码块测试,并完成 build 与浏览器验证
2026-06-08 06:45:40 +08:00
Syngnat
67dd178166 ♻️ refactor(ai-input): 拆分上下文弹窗并完善命令反馈
- 抽离 AI 输入区的上下文选表弹窗和斜杠菜单,降低 AIChatInput 体积与重复逻辑
- 合并图片粘贴与 slash 状态处理,减少 V1/V2 两套输入分支的重复代码
- 新增 /命令无匹配空状态提示,并补做测试、构建与真实页面验证
2026-06-07 23:19:38 +08:00
Syngnat
802385464d ♻️ refactor(ai-chat): 抽离本地工具执行器并补齐测试
- 将 AIChatPanel 中本地工具执行 switch 抽成独立 helper,降低主面板耦合度
- 为表结构、SQL 安全拦截和工具结果映射补充独立单测
- 保持 AI 面板交互不变,补做构建与浏览器冒烟验证
2026-06-07 22:40:07 +08:00
Syngnat
eff2f7f63a feat(ai-mcp): 补全结构探针并优化客户端接入体验
- 新增 get_indexes、get_foreign_keys、get_triggers 内置工具与 MCP Server 对应实现
- 拆分 AI 设置中的 MCP 接入面板和服务卡片,补充参数提示与客户端状态展示
- 补齐前后端测试与真实页面验证,降低 AI 设置区域的臃肿度
2026-06-07 22:06:24 +08:00
Syngnat
7039eae9c7 🐛 fix(ai-chat): 统一历史模式会话排序
- 面板内历史模式按最近更新时间优先展示会话

- 保持与历史侧栏一致的最近会话浏览体验

- 增加静态测试防止排序逻辑回退
2026-06-07 21:10:39 +08:00
Syngnat
fbed6580fa feat(ai-chat): 增加提示动作并完善历史侧栏体验
- 缺少供应商或模型时在提示区提供可执行入口

- 历史侧栏按更新时间排序并在重新打开时重置搜索

- 替换 Drawer 废弃属性并补充定向测试
2026-06-07 21:05:09 +08:00
Syngnat
7cdd2bd6c0 🐛 fix(ci): 补齐 MCP SQL 检查实现文件
- 将 internal/app/sql_inspect.go 纳入版本控制

- 让 Wails 绑定生成和 mcpserver 依赖的 SQLInspection 类型重新闭合
2026-06-07 20:36:42 +08:00
Syngnat
651c452fe1 🐛 fix(ci): 让 Go 版本跟随 go.mod
- 将 dev-build 的两处 setup-go 改为读取 go.mod

- 同步修正 release workflow,避免构建 Go 版本再次漂移
2026-06-07 20:32:18 +08:00
Syngnat
5b843ee25b feat(ai-mcp): 完善外部客户端安装链路并收紧 SQL 安全控制
- 新增 GoNavi MCP stdio server 与 Claude/Codex 用户级安装入口

- 增加安装状态检测、刷新复制能力和浏览器联调 mock

- 外部 execute_sql 对齐 GoNavi safetyLevel 并补充前端/后端验证
2026-06-07 20:27:50 +08:00
Syngnat
f7d71c6c5c feat(ai): 增加用户提示词与 MCP/Skills 扩展能力
- 支持用户级自定义提示词加载保存与聊天会话注入

- 新增 MCP 服务配置、工具发现与按次调用链路

- 增加 Skills 配置、作用域注入与前后端类型同步

- 补充配置存储与前端行为测试并更新依赖
2026-06-07 17:56:45 +08:00
Syngnat
dda8bbb6e3 🐛 fix(mysql): 修复 GDB 连接参数不兼容导致的握手失败
- 优化 MySQL 兼容 DSN 默认参数
- 在连接验证阶段增加 multiStatements 兼容回退
- 补充相关单元测试覆盖

Refs #543
2026-06-07 14:50:42 +08:00
Syngnat
6932abe674 🐛 fix(ddl): 修复 DDL 格式化方言类型导致的前端构建失败 2026-06-07 13:51:30 +08:00
Syngnat
79c5bfb3d4 feat(connection-import): 支持导入 Navicat NCX 连接与密码
- 新增 Navicat NCX XML 解析与 v1/v2 密码解密能力
- 接入后端连接导入链路并补充导入失败提示
- 前端补充 NCX 格式识别、缺失密码提示与定向测试
2026-06-07 13:38:53 +08:00
Syngnat
ace6e18da8 🐛 fix(sql-editor): 移除对象信息未命中误报提示
- 取消对象信息未命中时的提示弹窗
- 保留已识别对象的 hover 与超链接行为
- 补充未命中静默回归测试
2026-06-07 12:12:02 +08:00
Syngnat
a5b27820cb feat(ddl): 为 DDL 视图增加按方言格式化展示能力
- 新增通用 DDL 格式化工具
- DataGrid 查看 DDL 时按数据源方言输出可读 SQL
- 覆盖 DuckDB DDL 展示与工具层测试
2026-06-05 22:21:40 +08:00
Syngnat
d2189e1442 🐛 fix(duckdb): 修复 DuckDB 查询误用连接超时导致中断
- 新增 DuckDB 查询上下文策略,避免将连接超时直接作为查询执行超时
- 调整 DBQueryWithCancel、DBQueryMulti、DBQueryIsolated 统一走查询上下文工厂
- 补充 DuckDB 查询不继承连接超时与网络型数据库保留 deadline 的回归测试
2026-06-05 21:35:03 +08:00
Syngnat
36a80951a0 🐛 fix(ci/driver): 修复驱动发布错配与 dev 驱动下载命中旧资产
- 发布链路新增 driver release 资产自检,校验已发布二进制与 manifest revision/sha256 一致
- dev-build 与 release 工作流在复用已发布驱动前先校验实际资产,发现旧二进制混入时强制全量重建
- driver manifest 新增 sha256 字段,避免仅凭 source commit 与 manifest 一致就误判发布有效
- 驱动变更检测与全量重建判定纳入 release asset 校验脚本变更,确保链路修复提交会触发自愈重建
- 驱动下载优先使用 GitHub release asset API 地址,并对资产接口按 octet-stream 下载,降低 dev-latest 同名资产命中旧缓存的风险
- 保持 DuckDB 无主键编辑修复不回退,并通过 internal/db 与前端相关回归测试
2026-06-05 20:00:27 +08:00
Syngnat
2ea88c03d3 🐛 fix(ci): 修复 Windows revision 哈希构建失败 2026-06-05 19:10:22 +08:00
Syngnat
5d9499e4dd 🐛 fix(ci): 修复驱动 revision 更新后复用旧构建缓存 2026-06-05 18:56:15 +08:00
Syngnat
fe479778d1 🐛 fix(ci): 修复驱动发布链路变更漏掉全量重建 2026-06-05 18:17:16 +08:00
Syngnat
81eaeb5d5f 🐛 fix(ci): 修复 dev 驱动全量重建基线未透传
- 将 compare_base 从 driver 检测阶段透传到 build job
- 强制全量重建时不再回退到旧 source_commit 做平台 diff
- 避免 dev driver release 成功发布但仍夹带旧 standalone 资产
2026-06-05 17:22:09 +08:00
Syngnat
ea53430d70 🐛 fix(ci): 修复驱动总包打包与 manifest 生成失败
- 抽离 driver release 打包脚本,避免内联脚本删掉已生成 manifest
- 修复 release-assets 场景下 tools 相对路径错误,统一通过仓库根脚本打包
- manifest 改为按源码和目标平台重算 revision,不再执行跨平台 driver 二进制
- 补充 driver release 打包与 manifest 生成回归测试
2026-06-05 16:53:01 +08:00
Syngnat
be26970761 🐛 fix(ci): 修复脏 driver release 资产导致 revision 错配 2026-06-05 16:10:05 +08:00
Syngnat
f7dd90a5d1 🐛 fix(ci): 修复 DuckDB 变更后驱动重建漏判
- 新增 diff-driver-agent-revisions 脚本,直接比对 base/head 在各平台实际生成出的 driver revision 差异
- dev/release workflow 的驱动检测改为源码归因结果叠加各平台 revision diff union,避免只改 DuckDB 时漏重建真实已变更的 driver assets
- 将 CI Go 版本固定到 1.24.3,减少 runner patch 漂移导致的 revision 指纹抖动
- 补充 DuckDB 变更命中与前端变更不误触发的 revision diff 回归测试
2026-06-05 15:03:44 +08:00
Syngnat
2254b76232 🐛 fix(duckdb): 修复无主键结果无法安全编辑
- 为 DuckDB 查询结果和表预览补充隐藏 rowid 定位列,允许无主键表安全提交修改
- DataGrid 提交变更时仅将 rowid 用作定位条件,避免把隐藏定位列写回业务字段
- DuckDB ApplyChanges 对 duckdb-rowid 改用未加引号的 rowid 条件,修复更新和删除失效
- 补充前后端回归测试,覆盖 QueryEditor、DataViewer、rowLocator 与 ApplyChanges 链路
2026-06-05 14:05:18 +08:00
Syngnat
ea1737ab8d 🐛 fix(ci): 修复工作流改动漏触发驱动重建 2026-06-05 12:36:39 +08:00
Syngnat
01bee3d8e4 🐛 fix(ci): 修复 driver revision 跨平台错配
- 改为在 dev/release 主程序构建前按当前平台全量生成 driver-agent revision 指纹
- 避免只重算部分驱动时混入其他平台旧 revision,导致最新 dev 应用误判全量 driver mismatch
- 为 windows/amd64 始终准备 DuckDB CGO toolchain,保证全量 revision 生成可执行
- 补充 revision 平台相关回归脚本,覆盖 darwin/arm64 与 windows/amd64 生成差异
2026-06-05 11:59:30 +08:00
Syngnat
805ab8b3d8 🐛 fix(table-designer): 修复 DuckDB 表设计主键保存失效
- 为 DuckDB 表结构变更补充 ADD PRIMARY KEY 预览 SQL
- 保存前拦截已有主键表的主键替换与删除,避免假成功
- 补充 DuckDB 主键变更判定与 schema SQL 回归测试
2026-06-05 11:25:44 +08:00
Syngnat
6742495c6f 🐛 fix(frontend/ci): 修复对象修改卡顿与 Windows ARM 驱动校验失败
- QueryEditor 为对象修改标签增加 object-edit 轻量模式,跳过重型元数据抓取和对象装饰扫描
- DefinitionViewer 与 TriggerViewer 打开的对象修改标签统一透传 queryMode,避免重新进入普通查询链路
- TriggerViewer 补全 MySQL/Oracle 类触发器 DDL 重建逻辑,修复对象修改打开语法不完整
- 补充对象修改与触发器 DDL 回归测试,覆盖轻量模式和元数据补全场景
- verify-driver-agent-revisions 脚本改为跨架构校验,避免在 x64 runner 直接执行 windows/arm64 二进制
- 新增 Windows ARM CI 校验追踪文档,保留架构校验与 host-native probe 证据
2026-06-05 10:34:18 +08:00
Syngnat
3964ecbe88 🐛 fix(ci): 修复 DuckDB Windows 驱动打包变量未定义
- 修复 dev workflow 中 DuckDB 专属驱动包生成阶段的变量作用域问题
- 修复 release workflow 中同样的 DuckDB zip 打包变量传递错误
- 避免 Windows 全量驱动构建时因 DUCKDB_ZIP_PATH 未定义导致 CI 失败
- 保持 DuckDB driver zip 与 dll 打包流程不变,仅修正 shell 环境变量用法
2026-06-05 09:38:47 +08:00
Syngnat
ebdf7b5a6c 🐛 fix(ci): 修复 dev CI 漏掉驱动补偿构建
- dev 构建改为优先使用已发布 driver release 对应源码提交作为检测基线
- 避免驱动相关提交的 CI 失败后,后续仅修 workflow 时被误判为无需重建驱动
- 当已发布驱动基线无法解析时,保守回退为全量驱动构建
- 补充驱动变更检测补偿场景回归测试
- 新增 driver release source 解析测试,覆盖 release body 与 target_commitish 场景
2026-06-05 09:10:49 +08:00
Syngnat
26ce75a547 🐛 fix(ci): 修复驱动工作流 heredoc 缩进
- 修复 dev-build 中 DuckDB Windows 驱动包 Python heredoc 缩进
- 同步修复 release workflow 同类缩进问题
- 避免 GitHub Actions 将 Python 代码误解析为 YAML 导致工作流无效
2026-06-05 08:40:28 +08:00
Syngnat
53811969c5 🐛 fix(driver-manager): 修复驱动 revision 错配与安装卡住
- 修复 revision 生成文件变更未触发 driver-agent 构建的问题
- 检测脚本自身变更时保守触发全量 driver-agent 构建
- 调整 dev 构建驱动安装策略为发布包优先、源码构建兜底
- 为驱动总包下载和源码构建增加超时边界
- 为驱动管理安装流程增加前端看门狗并补充回归测试
2026-06-05 08:34:38 +08:00
Syngnat
2438899ff5 🐛 fix(driver-manager): 修复驱动安装交互与 DuckDB Windows 发布链路
- 修复单驱动安装期间右侧目录操作被错误禁用的问题
- 调整 DuckDB Windows 优先下载专属 zip 并兼容带 query 的签名链接
- 补齐本地构建与 CI 发布的 duckdb-driver.zip 产物及回归测试
2026-06-05 07:15:16 +08:00
Syngnat
a718c41d5d test(app): 补强 DuckDB 定义刷新与主键回归
- 补充 DuckDB 对象修改链路的真实 DDL 刷新回归测试

- 为 app 层连接门禁增加可替换测试 seam,避免 fake metadata 测试被驱动校验拦截

- 修正 DuckDB metadata 测试的串行与断言稳定性
2026-06-04 22:25:08 +08:00
Syngnat
274c32ebdd 🐛 fix(frontend): 修复 DuckDB 对象编辑与安全修改回归
- 修复 DuckDB qualified table 在查询结果页丢失 schema 导致无法识别主键的问题

- 打开对象修改前强制刷新最新定义,并避免切换对象失败时沿用旧定义

- 为 DuckDB 元数据链路补充前后端回归测试,并给 app 层真实 runtime 测试增加环境门槛
2026-06-04 22:00:55 +08:00
Syngnat
a664f1a869 🐛 fix(driver): 精简驱动安装失败日志
驱动代理 revision 校验失败时不再在内外层重复拼接同一下载来源。

统一安装尝试错误格式,并折叠完全相同的失败条目。

补充 URL 前缀去重和安装尝试错误去重回归测试。
2026-06-04 17:02:46 +08:00
Syngnat
455813e53c 🐛 fix(driver): 修复驱动安装进度回退
驱动安装进度按单次会话做单调归一,避免旧下载事件覆盖新进度。

有单文件预编译资产时跳过驱动总包兜底,减少进度回退和安装失败面。

补充前端进度状态机与后端总包兜底回归测试。
2026-06-04 16:57:00 +08:00
Syngnat
30d1c080a0 🐛 fix(sql-editor): 补全对象定义片段修改模板
- 视图定义以 VIEW 片段开头时转换为 CREATE OR REPLACE VIEW,避免重复拼接 VIEW

- 触发器定义以 TRIGGER 或触发时机片段开头时自动补 CREATE OR REPLACE TRIGGER

- 增加视图和触发器对象修改模板回归测试
2026-06-04 16:18:58 +08:00
Syngnat
f7217583a3 🐛 fix(sql-editor): 修复对象跳转卡死与过程模板缺失CREATE
- 跳过大 SQL 的全局对象装饰扫描,避免 Ctrl/Cmd 点击对象时读取整篇编辑器文本

- 存储过程和函数源码片段缺少 CREATE 时自动补 CREATE OR REPLACE

- 增加过程修改模板与大 SQL 对象跳转回归测试
2026-06-04 16:05:40 +08:00
Syngnat
8f7c790700 🐛 fix(sql-editor): 修复存储过程定义执行截断 2026-06-04 15:46:09 +08:00
Syngnat
4cef232271 🐛 fix(sql-editor): 修复大段INSERT脚本导致全局卡顿
- 对 SQL 字符串字面量和注释做屏蔽,避免对象高亮/hover 扫描 values 中的大量测试数据

- 复用候选 token 收集结果刷新对象装饰,减少无效对象解析

- 补充 INSERT 脚本候选收集和字符串内 hover 的回归测试
2026-06-04 15:10:26 +08:00
Syngnat
5b602bff75 🐛 fix(duckdb): 修复唯一索引元数据安全编辑定位
- DuckDB 显式唯一索引表达式返回字符串包裹标识符时,统一归一化为真实列名

- 补充 DuckDB 主键、唯一约束、显式唯一索引的真实驱动回归测试

- 将 duckdb_metadata.go 纳入 DuckDB driver-agent revision 计算,确保重装驱动后加载新元数据逻辑
2026-06-04 13:52:05 +08:00
Syngnat
4ad1d15781 🐛 fix(driver/sidebar): 修复驱动代理 revision 校验与外部SQL目录重复显示
- driver:下载或总包中的 driver-agent revision 过旧时跳过该候选并继续 fallback

- driver:新增发布资产 revision 校验脚本并接入 dev/release CI

- sidebar:修复 v2 表/视图等筛选下重复显示外部 SQL 目录

- test:补充 driver-agent fallback 与侧栏筛选回归测试
2026-06-04 13:37:09 +08:00
Syngnat
f25a449e20 feat(sql-editor): 增加对象定义修改入口
- 视图、事件、函数/存储过程定义页增加对象修改按钮

- 触发器定义页增加对象修改按钮,并在定义片段场景提示补全 CREATE TRIGGER

- 对象修改入口统一打开 query 标签,复用现有 SQL 执行与连接上下文

- 新增定义页对象修改入口组件回归测试
2026-06-04 11:42:15 +08:00
Syngnat
9d39440438 🐛 fix(sql-editor): 修复对象超链接侧栏定位与样式
- 侧栏定位 fallback 统一按限定名解析,兼容 schema.object 与对象名、大小写差异

- 补充视图、函数、触发器超链接定位事件与树节点匹配回归测试

- 将 SQL 编辑器超链接改为蓝色实线下划线,并补充暗色主题样式断言
2026-06-04 11:41:54 +08:00
Syngnat
02faa4586b 🐛 fix(metadata): 修复多数据源主键唯一索引识别
- 统一 PG-like 数据源字段和索引元数据查询,支持 search_path 可见表

- 兼容 snake_case、布尔别名和字符串唯一索引标记

- 修复 DuckDB main/memory 路径解析,避免误判外部 catalog

- 补充前后端回归测试,覆盖可编辑结果定位和元数据重试路径
2026-06-04 10:49:16 +08:00
Syngnat
9acb1c69f7 feat(sql-editor): 增加SQL错误中文语义提示
- 新增 SQL 执行错误语义化规则,覆盖语法、对象、字段、约束和连接类错误

- 执行失败和刷新失败展示中文语义、处理建议与原始错误

- 补充工具函数与 QueryEditor 回归测试,确保英文报错可读化
2026-06-04 10:48:17 +08:00
Syngnat
a9d515f160 🐛 fix(sql-editor): 修复表名悬停元数据重复展示
- 去除 Cmd/Ctrl 导航高亮 decoration 上的 hoverMessage

- 统一由 SQL hover provider 输出对象元数据

- 补充回归断言,确保表元数据只展示一次
2026-06-04 10:47:30 +08:00
Syngnat
37a094c351 🐛 fix(duckdb): 修复元数据兼容与在线安装回退 2026-06-04 08:27:25 +08:00
Syngnat
f5166ac3fc 🐛 fix(sql-editor): 修复结果消息展示与数据目录迁移稳定性 2026-06-04 07:09:42 +08:00
Syngnat
23ac30086f 🐛 fix(tdengine): 修复低版本驱动连接与表元数据兼容问题
- 修复 TDengine 历史驱动源码构建未按所选版本切换依赖的问题
- 为 DESCRIBE 与 SHOW CREATE 增加旧版本语法降级,避免表详情加载报错
- 为表概览补充 TDengine 专用查询分支,避免误查 information_schema
- 补充 TDengine 兼容性与驱动构建回归测试

Refs #531
2026-06-03 21:33:15 +08:00
Syngnat
c6f6f76489 Merge pull request #530 from origin/pr-530 2026-06-03 20:41:49 +08:00
Syngnat
cbe4645bc6 🐛 fix(store): 修复启动最大化设置未及时持久化的问题
- 为 startupFullscreen 增加即时持久化补写
- 避免用户修改设置后立即退出导致下次启动仍按旧窗口状态恢复
- 补充回归测试覆盖立即重启场景
2026-06-03 20:22:38 +08:00
Syngnat
82cac0b12e 🐛 fix(sql-editor): 修复对象元数据与跳转交互异常
- 修复 SQL 元数据 hover provider 多实例重复注册导致内容重复显示
- 修复侧栏对象拖入编辑器后 Monaco 原生拖拽虚线残留
- 修复跨库拖拽对象丢失来源库导致后续无法跳转
- 兼容 macOS Cmd 点击时 Monaco 未提供 leftButton 的事件结构
- 补充 hover 去重、拖拽插入、对象跳转和 Cmd 点击回归测试
2026-06-03 19:52:32 +08:00
Syngnat
1ae44941dd 🐛 fix(sql-editor): 修复脚本执行拆分与元数据只读提示
- Oracle 匿名块:识别 BEGIN/DECLARE...END 块,避免按内部分号错误拆分
- 执行路径:PL/SQL 块跳过批量写入路径,保持单条语句语义
- SQL 文件:同步修复流式 SQL 文件拆分逻辑
- 查询结果:系统元数据表保持只读但不再弹业务表主键提示
- 测试覆盖:补充前后端拆分、执行和 information_schema 回归用例
2026-06-03 17:11:05 +08:00
Syngnat
4b23c013d9 feat(designer): 将对象设计整合进数据视图并统一设计表交互样式 2026-06-03 15:27:54 +08:00
lixiaodong
cf9b7d9d10 feat(elasticsearch): 生产级 ES 驱动 — 写入支持、分页、多版本兼容
- go-elasticsearch/v8 官方 SDK,支持 ES 6.x/7.x/8.x
- SQL → ES DSL 转换、DevTools 查询、JSON DSL
- _bulk API 批量写入(INSERT/UPDATE/DELETE)
- 别名写入、精确分页、COUNT 统计
- API Key / SSH / SSL 连接支持
- 表设计器只读适配、ES 品牌图标
- 30+ 单元测试全部通过
2026-06-03 10:15:38 +08:00
Syngnat
eeaf3c658b 🐛 fix(duckdb): 修复唯一索引识别与多库对象解析
- 合并 DuckDB 约束与索引元数据,恢复唯一索引表的可编辑判定
- 修复 attach 多库场景下 catalog/schema/table 定位混乱问题
- 统一前后端 qualified name 解析,支持带点和带引号对象名
- 补充 DuckDB 元数据与编辑链路回归测试
2026-06-02 21:12:59 +08:00
Syngnat
8fba42adbf 🐛 fix(sql-editor): 修复对象超链接定位并支持侧栏拖拽插入
- 修复视图、触发器、过程超链接打开后的左树定位失败
- 修正触发器对象内容不显示及错误提示文案
- 支持左侧对象名拖拽插入 SQL 编辑器
2026-06-02 20:29:42 +08:00
Syngnat
dd8af73887 🐛 fix(sidebar): 修复 v2 搜索关闭交互异常
- 关闭命令搜索前提交同步筛选值,避免输入框清空覆盖侧栏筛选

- 限制弹窗打开期间才同步命令输入到侧栏持久筛选

- 增加全局 ESC 关闭监听,修复焦点离开弹窗后无法关闭

- 补充回归测试覆盖筛选保留和全局 ESC 关闭规则
2026-06-02 18:22:07 +08:00
Syngnat
3eb9fd0acb 🐛 fix(sidebar): 修复 v2 侧栏横向滚动条位置 2026-06-02 18:06:31 +08:00
Syngnat
c315ea9c96 feat(elasticsearch): 补齐新建连接入口
- 前端连接弹窗新增 Elasticsearch 入口、默认端口、URI 示例和默认索引配置

- 补齐 Elasticsearch 图标、数据源能力、SQL dialect 和只读查询策略

- 后端驱动管理注册 Elasticsearch 版本、模块路径、构建标签和默认安装入口

- 增加连接展示、能力识别和驱动定义测试覆盖
2026-06-02 15:31:00 +08:00
Syngnat
864ad8a371 ♻️ refactor(elasticsearch): 改用轻量 REST 客户端
- 使用标准库 HTTP 客户端实现 ES ping、索引、mapping 和查询请求

- 保留代理、TLS、超时和 BasicAuth 配置能力

- 移除 go-elasticsearch SDK 及间接依赖,降低 dev 构建下载风险

- 更新 Elasticsearch 后端单测适配 REST 客户端
2026-06-02 15:30:32 +08:00
Syngnat
05d1bc22c6 🐛 fix(driver-agent): 修复 ES 驱动代理 revision 同步
- 补齐 Elasticsearch 在 revision 生成、变更检测和发布补齐脚本中的注册

- 生成 Elasticsearch 真实 driver-agent revision,避免 src-local 占位通过

- 本地测试版本改用 dev-latest driver-agent,避免退到旧 latest 包

- 增加 revision 测试覆盖,禁止可选驱动使用本地占位 revision
2026-06-02 15:30:02 +08:00
Syngnat
e4a8c53079 🐛 fix(window): 修复 Windows 恢复窗口后字体模糊
- Windows 从任务栏恢复窗口时直接触发 WebView2 zoom reset
- restore 场景不再依赖 viewport ratio drift 判断
- 覆盖最大化和非最大化窗口的恢复修复路径
- 保留最大化窗口零动画修复,避免二次最大化抖动
- 补充窗口恢复策略和 App 自动修复路径测试
2026-06-02 14:04:00 +08:00
Syngnat
c72542c92c feat(ai): upgrade MiniMax default model to M3 2026-06-02 13:42:48 +08:00
Syngnat
7f83954714 feat(elasticsearch): 新增 Elasticsearch 驱动支持 2026-06-02 13:42:22 +08:00
Syngnat
8c88017703 ️ perf(sidebar): 优化 v2 命令搜索输入和结果展示
- 修复中文输入法组合输入时按 Enter 误关闭搜索弹窗
- 限制搜索弹窗关闭方式为 ESC 或有效结果确认
- 移除关键词搜索下已加载表结果的固定条数截断
- 同步筛选开启时使用 deferred 值和防抖持久化,降低输入卡顿
- 补充命令搜索 Enter 判定和表匹配完整性测试
2026-06-02 13:40:48 +08:00
Syngnat
3a2db112f3 feat(sidebar): 增强 v2 侧栏搜索持久筛选
- 新增 v2 侧栏搜索模式配置,支持新版命令搜索和旧版侧栏筛选切换
- 命令搜索面板增加同步筛选开关和重置筛选按钮
- 侧栏顶部支持展示并清空已同步筛选词
- 补充 appearance 持久化字段清洗和回归测试
2026-06-02 13:40:20 +08:00
octo-patch
f6877ecfef feat(ai): upgrade MiniMax default model to M3
Add MiniMax-M3 to the MiniMax (Anthropic-compatible) provider model
list and set it as the new default. Retain MiniMax-M2.7 and
MiniMax-M2.7-highspeed for compatibility, and remove the deprecated
M2.5 / M2.5-highspeed / M2.1 / M2.1-highspeed / M2 entries.

Updates the matching backend static-model unit test and the frontend
provider preset (default model + model list).
2026-06-02 12:51:10 +08:00
lixiaodong
17ebc50b83 feat(elasticsearch): 新增 Elasticsearch 驱动支持
- 新增 ElasticsearchDB 实现 Database 接口全部 13 个方法
- 支持 HTTP/HTTPS 连接、Basic Auth、SSH 隧道、代理、SSL 回退
- 查询支持 JSON DSL 和 query_string 两种模式
- 元数据浏览:索引列表、mapping 字段、settings、aliases
- 完整单元测试覆盖(httptest mock,33+ 子测试)
- 注册为可选 Go 驱动,遵循现有 driver-agent 架构

Closes #521
2026-06-02 12:35:46 +08:00
Syngnat
d800f1ce84 🐛 fix(window): 修复 macOS 标题栏双击进入全屏
- 将标题栏双击与 macOS 原生全屏路径解耦

- 双击标题栏时仅执行窗口最大化或还原

- 保留快捷键触发 macOS 原生全屏的能力

- 增加工具栏测试断言覆盖双击和快捷键路径差异
2026-06-02 12:32:32 +08:00
Syngnat
c000bc3c69 🐛 fix(data-grid): 修复底部状态栏窄屏信息被隐藏
- 状态栏外层改为横向滚动,避免 AI 面板开启或小屏时功能被遮挡

- main/right 区域保持 max-content 宽度,避免分页和操作区被压缩

- 移除窄屏下隐藏行数和未提交状态的媒体规则

- 补充布局测试断言,防止状态数字再次被隐藏
2026-06-02 12:32:19 +08:00
Syngnat
c277791ef3 🐛 fix(data-grid): 修复单元格编辑自动首字母大写
- 编辑修复:普通内联单元格编辑输入框增加 noAutoCapInputProps

- 虚拟表格:同步为虚拟行内编辑输入框关闭自动大写

- 输入体验:禁用 autoCapitalize、autoCorrect 和 spellCheck,避免 true 被提示为 True

- 回归测试:新增 DataGrid 布局断言,覆盖两个内联编辑入口
2026-06-02 12:02:28 +08:00
Syngnat
7612657ded 💄 style(query-editor): 调整 v2 查询工具栏布局样式
- 为查询工具栏控件增加 v2 专用 class,移除 v2 下 inline 固定宽度依赖

- 使用内容宽度约束选择区,避免最大行数后出现多余空白

- 覆盖 Ant Design Button.Group 负 margin 和伪元素合并效果

- 增加 CSS 静态断言覆盖对齐、间距和响应式布局
2026-06-02 11:54:06 +08:00
Syngnat
e421662576 🐛 fix(ui): 修复数据表头和侧栏滚动显示
- 修复虚拟数据表横向滚动后表头与数据列错位

- 修复亮色主题字段元数据悬浮提示可读性

- 优化 v2 侧栏外部 SQL 菜单和定位入口文案

- 使用 rc-tree 横向滚动宽度估算并加粗侧栏树滚动条
2026-06-02 11:17:22 +08:00
Syngnat
2afddf497b 🐛 fix(query-editor): 优化 SQL 补全和结果页交互
- 修复新建查询页输入后表名补全失效,支持当前库懒加载与模糊匹配

- 限制长 SQL 实时装饰和持久化草稿,降低输入卡顿

- 执行相同格式化 SQL 时复用结果页并聚焦对应结果标签

- 查询结果标签增加右键关闭菜单并优化标签样式和选中文字行为
2026-06-02 11:16:52 +08:00
Syngnat
c405eb08b5 feat(tabs): 支持标签展示配置并提示保存 SQL 文件
- 新增标签展示元素配置,支持单行、双行布局和元素排序

- 在设置面板提供标签展示入口并持久化用户配置

- 标签右键菜单增加标签设置入口并优化悬浮信息展示

- 关闭外部 SQL 文件标签前检测未保存草稿并支持保存后关闭
2026-06-02 11:16:25 +08:00
Syngnat
e6dd986115 feat(external-sql): 完善外部 SQL 目录文件管理
- 新增外部 SQL 文件的新建、重命名、删除和目录管理接口

- 后端限制 SQL 目录只加载 .sql 文件并补充目录操作测试

- 前端补齐 Wails 类型、浏览器 mock 和外部 SQL 树过滤逻辑

- 支持从外部 SQL 文件标签定位到侧栏目录节点
2026-06-02 11:15:30 +08:00
Syngnat
bf3e21f15c 🐛 fix(datagrid): 修复 DDL 测试图标重复 mock
- 移除 DataGrid DDL 测试中重复的 AimOutlined mock 属性

- 修复 TypeScript TS1117 编译错误

- 验证 DDL 测试与前端构建通过
2026-06-01 12:11:17 +08:00
Syngnat
09139c2553 feat(datagrid): 增加分页跳页并适配窄屏
- 分页条新增跳页输入与提交按钮,支持回车和点击跳转

- 跳页页码自动限制在有效页码范围内,避免越界分页请求

- 为 v2 状态栏增加容器级响应式规则,适配 AI 面板打开后的窄宽场景

- 分页区域增加横向滚动兜底,避免小尺寸屏幕下控件被挤压变形

- 补充 DataGrid 布局回归测试,覆盖跳页控件和窄屏样式规则
2026-06-01 12:05:25 +08:00
Syngnat
b85e7491a9 feat(shortcuts): 新增标签页切换快捷键
- 新增切换到下一个标签页动作,默认 Ctrl+Tab
- 新增切换到上一个标签页动作,默认 Ctrl+Shift+Tab
- 接入全局快捷键处理,按当前标签顺序首尾循环切换
- 补充快捷键默认值与全局执行链路测试

Refs #399
2026-06-01 12:05:25 +08:00
Syngnat
2fee3d1389 🐛 fix(shortcuts): 调整新建查询默认快捷键
- 将新建查询页默认快捷键改为 macOS Cmd+N、Windows Ctrl+N
- 将新建数据源默认快捷键顺延为 Cmd/Ctrl+Shift+N
- 补充默认快捷键唯一性校验,避免动作默认撞键
2026-06-01 12:05:25 +08:00
Syngnat
999efa5947 🐛 fix(shortcuts): 同步侧边栏搜索快捷键提示
- 侧边栏 v2 搜索入口改为读取用户快捷键配置
- 修复搜索入口固定显示默认 ⌘K 的问题
- 按 macOS 语义使用 Cmd+F 作为查找类快捷键
- 移除快捷键描述中的硬编码默认组合
- 补充快捷键展示与平台冲突判断测试
2026-06-01 12:04:08 +08:00
Syngnat
35b7fdf96b 🐛 fix(ui): 修复暗色主题确认弹窗文字可读性
- 为 v2 主题下的 Modal.confirm 标题和内容补充前景色

- 修复删除表确认弹窗在暗色主题下文字不可读的问题

- 新增确认弹窗主题回归测试
2026-06-01 11:59:36 +08:00
Syngnat
5ffaa4361e 🐛 fix(metadata): 修复 Oracle 字段元数据显示缺失
- Oracle 元数据查询为字段名、类型、默认值、注释等列补齐稳定别名

- 新增字段定义归一化工具,兼容 name/Name/COLUMN_NAME 等返回形态

- 修复 DataGrid、DataViewer、QueryEditor、TableDesigner 对字段元数据的读取

- 补充 Oracle 字段注释、表头元数据和主键定位回归测试
2026-06-01 11:59:36 +08:00
Syngnat
63db9fecb3 feat(query-editor): 支持查询重命名导出与保存快捷键
- 支持已保存查询重命名并同步当前标签标题

- 新增 SQL 文件导出接口、Wails 绑定和浏览器 mock

- 补充 Ctrl/Cmd+S 保存查询与 Ctrl+, 快捷键入口修复

- 覆盖 SQL 编辑器保存、导出和快捷键回归测试
2026-05-31 22:32:48 +08:00
Syngnat
e687ae2819 feat(sidebar): 优化对象菜单与旧版布局交互
- 为已存查询右键菜单补充重命名能力并同步已打开标签

- 优化 v2 侧栏与表概览右键菜单定位,避免底部遮挡

- 精简旧版数据视图工具栏布局并统一快捷键显示

- 补充侧栏与表概览菜单回归测试
2026-05-31 22:31:47 +08:00
Syngnat
4cfa4bc63f 🐛 fix(data-grid): 修复数据视图交互与右键菜单问题
- 修复当前页查找高亮、清空与 ESC 取消行为

- 优化单元格编辑器尺寸与选中状态取消逻辑

- 收敛工具栏重复操作并修复右键菜单遮挡

- 补充数据网格布局与右键菜单测试覆盖
2026-05-31 22:30:54 +08:00
Syngnat
73f3e2cf73 feat(query-editor): 增强 SQL 编辑器对象悬浮与快捷查看能力
- 美化 SQL 改为写入 Monaco undo 栈,支持 Ctrl+Z 回退到格式化前

- 新增表名字段名库名语义着色,并在元数据加载后自动刷新装饰

- 支持鼠标悬浮和 Ctrl/Cmd+Q 查看对象信息,兼容 Ctrl/Cmd 点击跳转提示

- 补充 QueryEditor 定向测试覆盖对象 hover、快捷查看和撤销行为

Refs #506
2026-05-31 15:30:09 +08:00
Syngnat
6f132db328 🐛 fix(iris): 修复 InterSystems IRIS 连接后表元数据为空
- 兼容 IRIS INFORMATION_SCHEMA 返回的紧凑列名格式
- 修复表、列、索引元数据读取时字段取值为空的问题
- 保持系统 schema 过滤逻辑,避免误展示内置对象
- 补充 IRIS metadata 回归测试覆盖表列表与列索引解析
- Refs #505
2026-05-31 14:18:40 +08:00
Syngnat
b8053ff368 🐛 fix(data-grid): 修复筛选应用后横向滚动导致字段值错位
- 数据刷新后重置滚动恢复标记,允许重新同步横向偏移
- 虚拟表格恢复滚动时统一走 applyVirtualHorizontalOffset,避免表头与单元格错位
- 补充 DataGrid 横向滚动恢复回归断言
- Refs #508
2026-05-31 14:01:24 +08:00
Syngnat
9ba457c91f 💄 style(query-editor): 补充对象跳转提示样式
- 为 QueryEditor Ctrl/Cmd 对象跳转提示补充虚线下划线样式
- 保持跳转命中时的可点击反馈与测试断言一致
2026-05-31 13:41:16 +08:00
Syngnat
255e484dcf feat(sidebar): 同步标签上下文并补充对象树统计信息
- 切换和关闭标签时同步 activeContext,避免新建查询误用 host 或数据库
- 侧边栏表节点展示行数统计,数据库节点展示表数量
- 旧版 sidebar 工具栏改为稳定五列布局,v1 不再混入 v2 置顶分组
- 补充 sidebar 与 store 回归测试
2026-05-31 13:34:21 +08:00
Syngnat
e5fb03bbcd 🐛 fix(data-grid): 修复当前页查找高亮残留并压缩旧版结果工具栏
- 当前页查找改为即时响应,清空或按 Esc 后立即取消高亮
- 查找渲染版本元数据改为可透传,避免高亮状态残留
- 旧版结果工具栏调整为紧凑单行布局并移除重复分页信息
- JSON 和文本视图隐藏当前页查找入口
2026-05-31 13:33:46 +08:00
Syngnat
bea16b72df feat(data-grid): 优化字段跳转列匹配与回车定位
- 提取字段跳转目标解析逻辑,优先精确匹配再回退模糊匹配
- 旧版跳转列保留补全下拉,选中或回车即可定位
- 移除旧版多余跳转按钮,统一为输入即补全的交互
- 补充 DataGrid 与 dataGridFind 定向回归覆盖
- Refs #509
2026-05-31 13:32:50 +08:00
Syngnat
bea938bc34 ♻️ refactor(query-editor): 移除新建查询页冗余工作区头部
Refs #502
2026-05-30 22:55:40 +08:00
Syngnat
b516acb173 🐛 fix(query-editor): 修复连续按 Ctrl/Cmd 时对象跳转失效 2026-05-30 22:52:53 +08:00
Syngnat
ee96125385 feat(query-editor): 扩展 SQL 编辑器对象跳转到视图触发器和存储过程
- 为 QueryEditor 补充视图、物化视图、触发器和函数元数据解析
- 支持 Ctrl/Cmd 点击打开对应对象定义页并同步当前 host/db 上下文
- 扩展 sidebarLocate 对触发器和函数的定位能力
- 补充 QueryEditor 与 sidebarLocate 定向测试覆盖
2026-05-30 21:44:42 +08:00
Syngnat
6934285d83 🐛 fix(tab-manager): 精简标签冗余信息并优化悬浮卡触发与对齐
- 去掉重复连接后缀、圆点和表类型图标,收敛标签信息密度
- 调整悬浮信息布局为对齐网格,提升字段可读性
- 延长悬浮卡触发时间,减少切换 Tab 时的误触发
2026-05-30 17:27:40 +08:00
Syngnat
5a52b141ed 🐛 fix(ai-panel): 隔离面板与消息级渲染异常避免整块白屏
- 为 AI 面板保留本地错误边界与重新加载兜底
- 为单条消息增加渲染隔离,异常消息不再拖垮整段对话
- 补充面板与消息渲染错误上下文,便于后续定位
2026-05-30 17:26:52 +08:00
Syngnat
fdcbadf918 🐛 fix(connection-modal): 支持编辑态回填已保存密码并保持默认遮罩
- 编辑连接前主动拉取可编辑配置,恢复主密码与 SSH 等已保存密钥
- 支持 AI 供应商编辑态回填 API Key,并保持默认遮罩展示
- 修正 AI 设置长错误提示换行展示,避免测试连接报错被裁切

Refs #489
2026-05-30 17:25:58 +08:00
Syngnat
ebda018e13 🐛 fix(sidebar): 统一新版侧边栏数据库图标尺寸 2026-05-29 16:43:44 +08:00
Syngnat
f653a6eb79 test(font): 补齐字体配置与新版排版回归校验
- 增加字体配置能力相关断言,覆盖字体变量发布、字体列表加载与搜索匹配入口
- 新增 Monaco 排版测试,校验新版界面下代码编辑器和数据编辑器的字体与字号回归
- 保持快捷操作与外观设置的关键文案、结构和字体落点可回归验证
2026-05-29 14:44:39 +08:00
Syngnat
307bcc95d1 🐛 fix(ui): 统一新版界面字体并调整左侧快捷操作布局
- 统一新版界面字体变量在侧边栏、AI 面板、日志、DDL、Redis 与数据视图中的落地使用
- 调整 v2 左侧 rail 布局,将新建组、批量操作表、批量操作库、运行外部 SQL 文件、定位当前打开表迁移到顶部主操作区
- 收敛新版侧边栏树节点、连接信息、字段表头与字段描述的字体与字重表现
- 让 Monaco 编辑器、数据预览和代码展示区域跟随新的 UI/Mono 字体配置
- Refs #510
2026-05-29 14:43:32 +08:00
Syngnat
a7f8ce36df feat(font): 新增系统字体枚举与全局字体配置能力
- 新增 Go 侧已安装字体扫描接口,支持前端读取系统真实字体列表
- 接入 Wails 字体查询导出,补齐 App.d.ts 与 App.js 调用声明
- 新增字体选项构建与匹配工具,区分 UI 字体与等宽字体候选
- 外观设置支持按平台加载字体列表,并支持搜索匹配与默认字体回退
- Store 增加自定义 UI 字体与代码字体配置,持久化全局字体选择
2026-05-29 14:41:56 +08:00
Syngnat
f5f5bbf5eb 🐛 fix(sidebar): 修复新版左侧分组与 Host 拖拽排序
- 新增 sidebarRootOrder 持久化左侧根节点顺序
- 支持分组与未分组 Host 在新版左侧根层混排
- 统一 v2 rail 与树视图拖拽写回根层排序
- 拖拽期间抑制误选中与 Host 误切换
- 补充 Sidebar 与 store 拖拽排序回归测试
2026-05-29 08:39:25 +08:00
1667 changed files with 481340 additions and 41561 deletions

18
.containerignore Normal file
View File

@@ -0,0 +1,18 @@
.git
.github
.idea
.ace-tool
.playwright-mcp
.superpowers
.worktrees
bin
build
dist
frontend/dist
node_modules
frontend/node_modules
tools/__pycache__
GoNavi-Wails
GoNavi-Wails.exe
optional-driver-agent
optional-driver-agent.exe

18
.dockerignore Normal file
View File

@@ -0,0 +1,18 @@
.git
.github
.idea
.ace-tool
.playwright-mcp
.superpowers
.worktrees
bin
build
dist
frontend/dist
node_modules
frontend/node_modules
tools/__pycache__
GoNavi-Wails
GoNavi-Wails.exe
optional-driver-agent
optional-driver-agent.exe

View File

@@ -0,0 +1,213 @@
name: Publish VPS download mirror
description: Verify, upload, and atomically publish a GoNavi release to the VPS mirror
inputs:
channel:
description: stable or dev
required: true
app-tag:
description: Physical application release directory name
required: true
app-dir:
description: Directory containing application release assets
required: true
app-manifest:
description: Application latest manifest path
required: true
driver-enabled:
description: Whether this deployment must publish driver assets
required: true
driver-tag:
description: Physical driver release directory name
required: false
default: ''
driver-dir:
description: Directory containing driver release assets
required: false
default: ''
driver-version-index:
description: Versioned driver index path
required: false
default: ''
driver-latest-index:
description: Mutable latest driver index path
required: false
default: ''
ssh-host:
description: Mirror origin SSH host
required: true
ssh-port:
description: Mirror origin SSH port
required: true
ssh-user:
description: Unprivileged mirror deployment user
required: true
ssh-private-key:
description: SSH private key for the deployment user
required: true
ssh-known-hosts:
description: Pinned known_hosts entry for the mirror origin
required: true
runs:
using: composite
steps:
- name: Prepare, upload, and commit mirror payload
shell: bash
env:
MIRROR_CHANNEL: ${{ inputs.channel }}
MIRROR_APP_TAG: ${{ inputs.app-tag }}
MIRROR_APP_DIR: ${{ inputs.app-dir }}
MIRROR_APP_MANIFEST: ${{ inputs.app-manifest }}
MIRROR_DRIVER_ENABLED: ${{ inputs.driver-enabled }}
MIRROR_DRIVER_TAG: ${{ inputs.driver-tag }}
MIRROR_DRIVER_DIR: ${{ inputs.driver-dir }}
MIRROR_DRIVER_VERSION_INDEX: ${{ inputs.driver-version-index }}
MIRROR_DRIVER_LATEST_INDEX: ${{ inputs.driver-latest-index }}
MIRROR_SSH_HOST: ${{ inputs.ssh-host }}
MIRROR_SSH_PORT: ${{ inputs.ssh-port }}
MIRROR_SSH_USER: ${{ inputs.ssh-user }}
MIRROR_SSH_PRIVATE_KEY: ${{ inputs.ssh-private-key }}
MIRROR_SSH_KNOWN_HOSTS: ${{ inputs.ssh-known-hosts }}
run: |
set -euo pipefail
mirror_root="/srv/gonavi-downloads"
[[ "${MIRROR_CHANNEL}" == "stable" || "${MIRROR_CHANNEL}" == "dev" ]] || {
echo "Invalid mirror channel: ${MIRROR_CHANNEL}" >&2
exit 1
}
[[ "${GITHUB_RUN_ID:-}" =~ ^[0-9]+$ && "${GITHUB_RUN_ATTEMPT:-}" =~ ^[0-9]+$ ]] || {
echo "Invalid GitHub run identity" >&2
exit 1
}
deployment_id="${MIRROR_CHANNEL}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
stage_dir="${RUNNER_TEMP}/gonavi-vps-mirror-${deployment_id}"
ssh_dir="${RUNNER_TEMP}/gonavi-vps-ssh-${deployment_id}"
remote_stage="${mirror_root}/.incoming/${deployment_id}"
effective_driver_tag=""
remote=""
remote_cleanup_enabled=false
ssh_options=()
cleanup() {
local exit_code=$?
local cleanup_remote_command=""
trap - EXIT
set +e
if [[ "${remote_cleanup_enabled}" == true && -n "${remote}" ]]; then
printf -v cleanup_remote_command 'rm -rf -- %q' "${remote_stage}"
ssh "${ssh_options[@]}" "${remote}" "${cleanup_remote_command}" >/dev/null 2>&1 || true
fi
rm -rf -- "${ssh_dir}" "${stage_dir}"
exit "${exit_code}"
}
trap cleanup EXIT
[[ "${MIRROR_SSH_PORT}" =~ ^[0-9]+$ ]] || {
echo "Invalid mirror SSH port" >&2
exit 1
}
mkdir -p "${ssh_dir}"
chmod 0700 "${ssh_dir}"
printf '%s\n' "${MIRROR_SSH_PRIVATE_KEY}" > "${ssh_dir}/id_ed25519"
printf '%s\n' "${MIRROR_SSH_KNOWN_HOSTS}" > "${ssh_dir}/known_hosts"
chmod 0600 "${ssh_dir}/id_ed25519" "${ssh_dir}/known_hosts"
ssh-keygen -y -f "${ssh_dir}/id_ed25519" >/dev/null
prepare_args=(
--channel "${MIRROR_CHANNEL}"
--app-tag "${MIRROR_APP_TAG}"
--app-dir "${MIRROR_APP_DIR}"
--app-manifest "${MIRROR_APP_MANIFEST}"
--output "${stage_dir}"
)
case "${MIRROR_DRIVER_ENABLED}" in
true)
[[ -n "${MIRROR_DRIVER_TAG}" ]] || {
echo "Driver mirror publication is enabled but driver-tag is empty" >&2
exit 1
}
[[ -n "${MIRROR_DRIVER_DIR}" && -d "${MIRROR_DRIVER_DIR}" ]] || {
echo "Driver mirror publication is enabled but driver-dir is missing" >&2
exit 1
}
[[ -n "$(find "${MIRROR_DRIVER_DIR}" -maxdepth 1 -type f -print -quit)" ]] || {
echo "Driver mirror publication is enabled but driver-dir is empty" >&2
exit 1
}
[[ -n "${MIRROR_DRIVER_VERSION_INDEX}" && -s "${MIRROR_DRIVER_VERSION_INDEX}" ]] || {
echo "Driver mirror publication is enabled but driver-version-index is missing or empty" >&2
exit 1
}
[[ -n "${MIRROR_DRIVER_LATEST_INDEX}" && -s "${MIRROR_DRIVER_LATEST_INDEX}" ]] || {
echo "Driver mirror publication is enabled but driver-latest-index is missing or empty" >&2
exit 1
}
effective_driver_tag="${MIRROR_DRIVER_TAG}"
prepare_args+=(
--driver-tag "${effective_driver_tag}"
--driver-dir "${MIRROR_DRIVER_DIR}"
--driver-version-index "${MIRROR_DRIVER_VERSION_INDEX}"
--driver-latest-index "${MIRROR_DRIVER_LATEST_INDEX}"
)
;;
false)
echo "Driver mirror publication explicitly disabled"
;;
*)
echo "Invalid driver-enabled value: ${MIRROR_DRIVER_ENABLED}" >&2
exit 1
;;
esac
python3 "${GITHUB_WORKSPACE}/tools/prepare-vps-release-payload.py" "${prepare_args[@]}"
cp "${GITHUB_WORKSPACE}/tools/vps-release-commit.sh" "${stage_dir}/vps-release-commit.sh"
chmod 0755 "${stage_dir}/vps-release-commit.sh"
ssh_options=(
-i "${ssh_dir}/id_ed25519"
-p "${MIRROR_SSH_PORT}"
-o BatchMode=yes
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o "UserKnownHostsFile=${ssh_dir}/known_hosts"
)
remote="${MIRROR_SSH_USER}@${MIRROR_SSH_HOST}"
printf -v stale_cleanup_script '%s\n' \
'set -euo pipefail' \
"root=$(printf '%q' "${mirror_root}")" \
'marker="$(cat "${root}/.gonavi-mirror-root" 2>/dev/null || true)"' \
'[[ "${marker}" == "gonavi-download-mirror-v1" ]] || { echo "mirror root marker is missing or invalid" >&2; exit 1; }' \
'incoming="${root}/.incoming"' \
'mkdir -p "${incoming}"' \
'find "${incoming}" -mindepth 1 -maxdepth 1 -type d -mmin +1440 -exec rm -rf -- {} +'
printf -v stale_cleanup_command 'bash -c %q' "${stale_cleanup_script}"
ssh "${ssh_options[@]}" "${remote}" "${stale_cleanup_command}"
payload_bytes="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["payloadBytes"])' "${stage_dir}/deployment.json")"
available_kib="$(ssh "${ssh_options[@]}" "${remote}" "LC_ALL=C df -Pk '${mirror_root}' | awk 'NR == 2 { print \$4 }'")"
[[ "${available_kib}" =~ ^[0-9]+$ ]] || {
echo "Mirror origin returned an invalid free space value: ${available_kib:-<empty>}" >&2
exit 1
}
available_bytes="$((available_kib * 1024))"
required_bytes="$((payload_bytes + 2000000000))"
if (( available_bytes < required_bytes )); then
echo "Mirror origin has insufficient free space: available=${available_bytes} required=${required_bytes}" >&2
exit 1
fi
remote_cleanup_enabled=true
ssh "${ssh_options[@]}" "${remote}" "mkdir -p '${remote_stage}'"
rsync -rlt --delete --partial \
--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
-e "ssh -i ${ssh_dir}/id_ed25519 -p ${MIRROR_SSH_PORT} -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${ssh_dir}/known_hosts" \
"${stage_dir}/" "${remote}:${remote_stage}/"
printf -v remote_command 'bash %q %q %q %q %q %q' \
"${remote_stage}/vps-release-commit.sh" \
"${mirror_root}" \
"${remote_stage}" \
"${MIRROR_CHANNEL}" \
"${MIRROR_APP_TAG}" \
"${effective_driver_tag}"
ssh "${ssh_options[@]}" "${remote}" "${remote_command}"

File diff suppressed because it is too large Load Diff

260
.github/workflows/docker-images.yml vendored Normal file
View File

@@ -0,0 +1,260 @@
name: Docker Images
on:
push:
branches:
- dev
tags:
- 'v*'
concurrency:
group: docker-images-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
jobs:
build:
name: Build ${{ matrix.image_name }} (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
image_name:
- gonavi-mcp-server
- gonavi-web-server
- gonavi-build-env
platform:
- linux/amd64
- linux/arm64
include:
- image_name: gonavi-mcp-server
dockerfile: Dockerfile.mcp-server
description: GoNavi MCP Server container image
- image_name: gonavi-web-server
dockerfile: Dockerfile.web-server
description: GoNavi Web Server (browser UI) container image
- image_name: gonavi-build-env
dockerfile: Dockerfile.build-env
description: GoNavi Linux build environment image
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Validate runtime Dockerfiles
if: matrix.image_name == 'gonavi-web-server'
shell: bash
run: |
set -euo pipefail
bash tools/validate-web-server-dockerfile.test.sh
env_file="$(mktemp)"
trap 'rm -f "$env_file"' EXIT
printf 'GONAVI_HOST_DATA_ROOT=%s\nGONAVI_WEB_PASSWORD=123456\n' "${RUNNER_TEMP}/gonavi-data" >"$env_file"
rendered="$(docker compose --env-file "$env_file" -f docker-compose.web-server.yml config)"
grep -Eq "GONAVI_WEB_PASSWORD: ['\"]?123456['\"]?$" <<<"$rendered"
- name: Prepare build variables
id: prep
shell: bash
run: |
set -euo pipefail
echo "owner_lc=$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
echo "platform_pair=$(printf '%s' "${{ matrix.platform }}" | tr '/' '-')" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ steps.prep.outputs.owner_lc }}/${{ matrix.image_name }}
labels: |
org.opencontainers.image.title=${{ matrix.image_name }}
org.opencontainers.image.description=${{ matrix.description }}
org.opencontainers.image.licenses=Apache-2.0
- name: Build smoke image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
load: true
tags: codex-smoke/${{ matrix.image_name }}:local
cache-from: type=gha,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
- name: Smoke test MCP Server image
if: matrix.image_name == 'gonavi-mcp-server'
shell: bash
run: |
set -euo pipefail
cid="$(docker run -d -p 8765:8765 -e GONAVI_MCP_HTTP_TOKEN=smoke-token codex-smoke/${{ matrix.image_name }}:local http)"
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
for _ in 1 2 3 4 5 6 7 8 9 10; do
if curl --fail --silent http://127.0.0.1:8765/healthz >/dev/null; then
exit 0
fi
sleep 1
done
docker logs "$cid"
echo "MCP Server image smoke test failed" >&2
exit 1
- name: Smoke test Web Server image
if: matrix.image_name == 'gonavi-web-server'
shell: bash
run: |
set -euo pipefail
cookie_jar="$(mktemp)"
cid=""
trap 'rm -f "$cookie_jar"; if [[ -n "$cid" ]]; then docker rm -f "$cid" >/dev/null 2>&1 || true; fi' EXIT
cid="$(docker run -d -p 127.0.0.1::34116 -e GONAVI_WEB_ADDR=0.0.0.0:34116 -e GONAVI_WEB_PASSWORD=123456 codex-smoke/${{ matrix.image_name }}:local web-server)"
host_port="$(docker port "$cid" 34116/tcp | awk -F: 'NR == 1 { print $NF }')"
if [[ ! "$host_port" =~ ^[0-9]+$ ]]; then
docker logs "$cid"
echo "Failed to resolve the Web Server smoke-test port" >&2
exit 1
fi
base_url="http://127.0.0.1:${host_port}"
ready=false
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
if curl --fail --silent "${base_url}/__gonavi/healthz" >/dev/null; then
ready=true
break
fi
sleep 1
done
if [[ "$ready" != "true" ]]; then
docker logs "$cid"
echo "Web Server image smoke test failed" >&2
exit 1
fi
status="$(curl --fail --silent --show-error "${base_url}/__gonavi/auth/status")"
grep -Fq '"configured":true' <<<"$status"
login="$(curl --fail --silent --show-error -c "$cookie_jar" -H 'Content-Type: application/json' -d '{"password":"123456","code":""}' "${base_url}/__gonavi/auth/login")"
grep -Fq '"success":true' <<<"$login"
app_status="$(curl --silent --show-error -o /dev/null -w '%{http_code}' -b "$cookie_jar" "${base_url}/")"
[[ "$app_status" == "200" ]]
- name: Smoke test build environment image
if: matrix.image_name == 'gonavi-build-env'
shell: bash
run: |
set -euo pipefail
docker run --rm codex-smoke/${{ matrix.image_name }}:local \
bash -lc 'go version && node -v && npm -v && wails version'
- name: Push by digest
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ steps.prep.outputs.owner_lc }}/${{ matrix.image_name }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
- name: Export digest
shell: bash
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.push.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Publish ${{ matrix.image_name }}
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
include:
- image_name: gonavi-mcp-server
description: GoNavi MCP Server container image
- image_name: gonavi-web-server
description: GoNavi Web Server (browser UI) container image
- image_name: gonavi-build-env
description: GoNavi Linux build environment image
steps:
- name: Normalize image namespace
id: vars
shell: bash
run: |
set -euo pipefail
echo "owner_lc=$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-${{ matrix.image_name }}-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}
flavor: |
latest=false
tags: |
type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }}
type=sha,format=short,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }}
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
labels: |
org.opencontainers.image.title=${{ matrix.image_name }}
org.opencontainers.image.description=${{ matrix.description }}
org.opencontainers.image.licenses=Apache-2.0
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
shell: bash
run: |
set -euo pipefail
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}@sha256:%s ' *)
- name: Inspect image
shell: bash
run: |
set -euo pipefail
docker buildx imagetools inspect "ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}:${{ steps.meta.outputs.version }}"

293
.github/workflows/issue-claim.yml vendored Normal file
View File

@@ -0,0 +1,293 @@
# 评论认领:在 Issue 下评论「我想做」等关键词后,自动指派/标记认领人。
# 说明:
# - 仓库协作者write 及以上)可成功写入 Assignees
# - 外部贡献者若无写权限GitHub 不允许设为 assignee此时会打 claimed 标签并回复确认
# - 支持取消认领:取消认领 / /unclaim
name: Issue Claim
on:
issue_comment:
types: [created]
permissions:
issues: write
contents: read
jobs:
claim:
name: Auto claim or unclaim
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Process claim / unclaim comment
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const comment = context.payload.comment;
const actor = comment.user.login;
const actorType = comment.user.type;
// 忽略机器人自己的评论,避免循环
if (actorType === 'Bot') {
core.info('Skip bot comment');
return;
}
const raw = (comment.body || '').replace(/\r\n/g, '\n').trim();
if (!raw) {
return;
}
// 仅认领短指令:整段或首行匹配,避免长讨论误触发
const firstLine = raw.split('\n').map((l) => l.trim()).find(Boolean) || '';
const normalize = (s) =>
s
.replace(/^[@/]/g, '')
.replace(/[!。..、,\s]+$/g, '')
.trim()
.toLowerCase();
const claimPhrases = new Set([
'我想做',
'我来做',
'我来',
'认领',
'我认领',
'claim',
'i claim',
'i want this',
'i will take this',
'assign me',
]);
const unclaimPhrases = new Set([
'取消认领',
'不认领了',
'放弃认领',
'unclaim',
'release',
]);
const nFull = normalize(raw);
const nFirst = normalize(firstLine);
// 允许 /claim /unclaim 形式
const slash = firstLine.match(/^\/(claim|unclaim)\b/i);
const isClaim =
claimPhrases.has(nFull) ||
claimPhrases.has(nFirst) ||
(slash && slash[1].toLowerCase() === 'claim');
const isUnclaim =
unclaimPhrases.has(nFull) ||
unclaimPhrases.has(nFirst) ||
(slash && slash[1].toLowerCase() === 'unclaim');
if (!isClaim && !isUnclaim) {
core.info('Comment is not a claim/unclaim command');
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = issue.number;
const CLAIMED_LABEL = 'claimed';
async function ensureClaimedLabel() {
try {
await github.rest.issues.getLabel({ owner, repo, name: CLAIMED_LABEL });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({
owner,
repo,
name: CLAIMED_LABEL,
color: '0E8A16',
description: '已被社区认领,进行中',
});
}
}
async function ensureLabels(names) {
const current = new Set((issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name)));
const toAdd = names.filter((n) => !current.has(n));
if (toAdd.length) {
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: toAdd });
}
}
async function removeLabels(names) {
for (const name of names) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number, name });
} catch (e) {
if (e.status !== 404) throw e;
}
}
}
async function tryAssign(login) {
try {
await github.rest.issues.addAssignees({
owner,
repo,
issue_number,
assignees: [login],
});
return { ok: true };
} catch (e) {
const msg = e.message || String(e);
core.warning(`assign failed for ${login}: ${msg}`);
return { ok: false, message: msg };
}
}
async function tryUnassign(login) {
try {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number,
assignees: [login],
});
return { ok: true };
} catch (e) {
core.warning(`unassign failed for ${login}: ${e.message || e}`);
return { ok: false };
}
}
// 从 issue 正文底部的认领锚点解析当前认领人
const CLAIM_MARKER_RE =
/<!--\s*gonavi-claim\s*:\s*([A-Za-z0-9-]+)\s*-->/;
const body = issue.body || '';
const markerMatch = body.match(CLAIM_MARKER_RE);
const markerClaimer = markerMatch ? markerMatch[1] : null;
const assignees = (issue.assignees || []).map((a) => a.login);
const currentClaimer = assignees[0] || markerClaimer || null;
const hasClaimedLabel = (issue.labels || []).some(
(l) => (typeof l === 'string' ? l : l.name) === CLAIMED_LABEL
);
if (isUnclaim) {
if (!currentClaimer && !hasClaimedLabel) {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: ` @${actor} 当前无人认领,无需取消。`,
});
return;
}
if (currentClaimer && currentClaimer.toLowerCase() !== actor.toLowerCase()) {
// 仅认领人本人可取消;维护者可强制取消
const isMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(
context.payload.comment.author_association
);
if (!isMaintainer) {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: `⚠️ @${actor} 本 Issue 当前由 @${currentClaimer} 认领。请对方评论 \`取消认领\`,或联系维护者。`,
});
return;
}
}
const who = currentClaimer || actor;
await tryUnassign(who);
await removeLabels([CLAIMED_LABEL]);
let newBody = body;
if (CLAIM_MARKER_RE.test(newBody)) {
newBody = newBody
.replace(/\n?<!--\s*gonavi-claim\s*:\s*[A-Za-z0-9-]+\s*-->\s*/g, '\n')
.replace(/\n?### 🔐 认领状态[\s\S]*?(?=\n## |\n# |$)/, '\n')
.trimEnd();
await github.rest.issues.update({ owner, repo, issue_number, body: newBody });
}
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: `🔓 @${actor} 已取消认领${who !== actor ? `(原认领人 @${who}` : ''}。其他人可评论 \`我想做\` 重新认领。`,
});
return;
}
// —— claim ——
if (currentClaimer && currentClaimer.toLowerCase() !== actor.toLowerCase()) {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: `⚠️ @${actor} 本 Issue 已由 @${currentClaimer} 认领。\n\n- 若对方长期无进展,可评论提醒,或由维护者 \`取消认领\` 后重开\n- 你也可以先在本线程说明想做的方向,维护者可协助拆分子任务`,
});
return;
}
if (currentClaimer && currentClaimer.toLowerCase() === actor.toLowerCase()) {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: `✅ @${actor} 你已经是本 Issue 的认领人,无需重复认领。开干吧 💪`,
});
return;
}
await ensureClaimedLabel();
const assignResult = await tryAssign(actor);
await ensureLabels([CLAIMED_LABEL]);
// 在正文写入可机读认领锚点assignee 失败时仍可防双认领)
const stamp = new Date().toISOString().slice(0, 10);
const claimBlock =
`\n\n### 🔐 认领状态\n\n` +
`- 认领人:@${actor}\n` +
`- 认领日期:${stamp}\n` +
`- 取消方式:评论 \`取消认领\` 或 \`/unclaim\`\n` +
`<!-- gonavi-claim: ${actor} -->\n`;
let newBody = body;
if (CLAIM_MARKER_RE.test(newBody)) {
newBody = newBody.replace(
CLAIM_MARKER_RE,
`<!-- gonavi-claim: ${actor} -->`
);
if (!/### 🔐 认领状态/.test(newBody)) {
newBody = newBody.trimEnd() + claimBlock;
}
} else {
newBody = newBody.trimEnd() + claimBlock;
}
await github.rest.issues.update({ owner, repo, issue_number, body: newBody });
if (assignResult.ok) {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body:
`✅ @${actor} 已认领本 Issue并已加入 **Assignees**。\n\n` +
`建议下一步:\n` +
`1. 从 \`dev\` 拉分支:\`fix/*\` 或 \`feature/*\`\n` +
`2. 完成后向 **\`dev\`** 提 PR\n` +
`3. 若暂无法继续,请评论 \`取消认领\`\n\n` +
`贡献指南https://github.com/Syngnat/GoNavi/blob/dev/CONTRIBUTING.zh-CN.md`,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body:
`✅ @${actor} 已认领本 Issue已标记 \`claimed\`)。\n\n` +
`> 说明GitHub 仅允许将**仓库协作者**设为正式 Assignee。你目前可能还没有写权限因此无法写入 Assignees 字段;**社区认领仍然有效**,其他人再评论「我想做」会被拦截。\n\n` +
`建议下一步:\n` +
`1. Fork 仓库,从 \`dev\` 拉分支开发\n` +
`2. 向 **\`dev\`** 提 PR并在 PR 描述里写 \`Fixes #${issue_number}\`\n` +
`3. 若暂无法继续,请评论 \`取消认领\`\n\n` +
`贡献指南https://github.com/Syngnat/GoNavi/blob/dev/CONTRIBUTING.zh-CN.md`,
});
}

339
.github/workflows/publish-release.yml vendored Normal file
View File

@@ -0,0 +1,339 @@
name: Publish Draft Release
on:
push:
branches:
- 'publish/v*'
workflow_dispatch:
inputs:
tag:
description: Release tag to publish (for example, v0.8.7)
required: true
type: string
permissions:
actions: read
contents: write
concurrency:
# Stable and dev publications share the same mirror targets.
group: gonavi-release-publication
cancel-in-progress: false
jobs:
publish:
name: Validate, mirror, and publish release
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
steps:
- name: Validate release
id: validate
uses: actions/github-script@v8
with:
script: |
const requestedTag = String(process.env.RELEASE_TAG || '').trim();
const tag = requestedTag.startsWith('publish/')
? requestedTag.slice('publish/'.length)
: requestedTag;
if (!/^v\d+\.\d+\.\d+$/.test(tag)) {
core.setFailed(`Invalid release tag: ${tag}`);
return;
}
const { owner, repo } = context.repo;
const tagRef = await github.rest.git.getRef({
owner,
repo,
ref: `tags/${tag}`,
});
let commitSha = tagRef.data.object.sha;
if (tagRef.data.object.type === 'tag') {
const annotatedTag = await github.rest.git.getTag({
owner,
repo,
tag_sha: tagRef.data.object.sha,
});
commitSha = annotatedTag.data.object.sha;
}
const workflowRuns = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'release.yml',
event: 'push',
branch: tag,
per_page: 100,
});
const runs = workflowRuns.data.workflow_runs;
const successfulRun = runs.find((run) => (
run.head_sha === commitSha
&& run.status === 'completed'
&& run.conclusion === 'success'
));
if (!successfulRun) {
core.setFailed(`No successful Release workflow found for ${tag} at ${commitSha}`);
return;
}
const releaseList = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
const releases = releaseList.data;
const release = releases.find((item) => item.tag_name === tag);
if (!release) {
core.setFailed(`Release not found for ${tag}`);
return;
}
let latestRelease = null;
try {
latestRelease = (await github.rest.repos.getLatestRelease({ owner, repo })).data;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
const semverParts = (value) => value.slice(1).split('.').map(Number);
const compareSemver = (left, right) => {
const a = semverParts(left);
const b = semverParts(right);
for (let index = 0; index < 3; index += 1) {
if (a[index] !== b[index]) {
return a[index] - b[index];
}
}
return 0;
};
let shouldPublish = release.draft;
if (release.draft) {
const latestTag = String(latestRelease?.tag_name || '').trim();
if (latestTag && /^v\d+\.\d+\.\d+$/.test(latestTag) && compareSemver(tag, latestTag) <= 0) {
core.setFailed(`Refusing to publish ${tag} over current latest ${latestTag}`);
return;
}
} else if (!latestRelease || latestRelease.id !== release.id) {
core.setFailed(`Published release ${tag} is not the current GitHub latest release`);
return;
} else {
shouldPublish = false;
}
const version = tag.slice(1);
const expectedAssets = [
'LICENSE',
'NOTICE',
'SHA256SUMS',
'latest.json',
`GoNavi-${version}-Linux-Amd64-WebKit41.tar.gz`,
`GoNavi-${version}-Linux-Amd64.tar.gz`,
`GoNavi-${version}-Linux-Arm64.tar.gz`,
`GoNavi-${version}-MacOS-Amd64.dmg`,
`GoNavi-${version}-MacOS-Arm64.dmg`,
`GoNavi-${version}-Windows-Amd64-Installer.msi`,
`GoNavi-${version}-Windows-Amd64-Portable.exe`,
`GoNavi-${version}-Windows-Amd64-Portable.zip`,
`GoNavi-${version}-Windows-Arm64-Installer.msi`,
`GoNavi-${version}-Windows-Arm64-Portable.exe`,
`GoNavi-${version}-Windows-Arm64-Portable.zip`,
];
const assetsByName = new Map(release.assets.map((asset) => [asset.name, asset]));
const missingAssets = expectedAssets.filter((name) => {
const asset = assetsByName.get(name);
return !asset || asset.state !== 'uploaded' || asset.size <= 0;
});
if (missingAssets.length > 0) {
core.setFailed(`Release assets are incomplete: ${missingAssets.join(', ')}`);
return;
}
core.setOutput('tag', tag);
core.setOutput('release_id', String(release.id));
core.setOutput('should_publish', String(shouldPublish));
core.notice(`Validated ${release.draft ? 'draft' : 'published'} release ${tag} and its assets`);
- name: Checkout release tooling
uses: actions/checkout@v5
- name: Prepare and verify stable mirror payload
id: mirror_payload
env:
GH_TOKEN: ${{ github.token }}
DRIVER_RELEASE_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }}
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${DRIVER_RELEASE_TOKEN}" ]]; then
echo "DriverAgents release credentials are not configured" >&2
exit 1
fi
app_dir="${RUNNER_TEMP}/gonavi-release"
driver_dir="${RUNNER_TEMP}/driver-release"
mkdir -p "${app_dir}" "${driver_dir}"
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --dir "${app_dir}"
driver_release_json="${RUNNER_TEMP}/driver-release.json"
driver_status="$(curl --silent --show-error \
--output "${driver_release_json}" \
--write-out '%{http_code}' \
--header "Authorization: Bearer ${DRIVER_RELEASE_TOKEN}" \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/Syngnat/GoNavi-DriverAgents/releases/tags/${RELEASE_TAG}")"
has_driver_release=false
case "${driver_status}" in
200)
jq -e --arg tag "${RELEASE_TAG}" \
'.tag_name == $tag and .draft == false' "${driver_release_json}" >/dev/null
GH_TOKEN="${DRIVER_RELEASE_TOKEN}" gh release download "${RELEASE_TAG}" \
--repo Syngnat/GoNavi-DriverAgents --dir "${driver_dir}"
has_driver_release=true
;;
404)
echo "No DriverAgents release for ${RELEASE_TAG}; skipping driver mirror"
;;
*)
echo "DriverAgents release lookup failed with HTTP ${driver_status}" >&2
exit 1
;;
esac
echo "has_driver_release=${has_driver_release}" >> "$GITHUB_OUTPUT"
jq -e \
--arg tag "${RELEASE_TAG}" \
--arg mirror_base "https://download.syngnat.top/gonavi/releases/download" \
--arg github_base "https://github.com/${GITHUB_REPOSITORY}/releases/download" \
'
.tagName == $tag
and (.assets | type == "array" and length > 0)
and all(.assets[];
(.url | type == "string" and startswith($mirror_base + "/" + $tag + "/"))
and (.apiUrl | type == "string" and startswith($github_base + "/" + $tag + "/"))
and (.sha256 | type == "string" and test("^[0-9a-fA-F]{64}$"))
)
' "${app_dir}/latest.json" >/dev/null
if [[ "${has_driver_release}" == true ]]; then
jq -e '.assets | type == "object" and length > 0' \
"${driver_dir}/GoNavi-DriverAgents-Index.json" >/dev/null
[[ -s "${driver_dir}/SHA256SUMS" ]] || {
echo "DriverAgents SHA256SUMS is missing" >&2
exit 1
}
(cd "${driver_dir}" && sha256sum --check SHA256SUMS)
jq --arg tag "${RELEASE_TAG}" \
'.tagName = $tag' \
"${driver_dir}/GoNavi-DriverAgents-Index.json" \
> "${RUNNER_TEMP}/driver-latest-index.json"
fi
while IFS=$'\t' read -r name expected_size expected_sha; do
[[ "${name}" == "$(basename "${name}")" ]] || {
echo "Invalid app asset name in latest.json: ${name}" >&2
exit 1
}
file="${app_dir}/${name}"
[[ -f "${file}" ]] || {
echo "App asset referenced by latest.json is missing: ${name}" >&2
exit 1
}
actual_size="$(stat -c '%s' "${file}")"
actual_sha="$(sha256sum "${file}" | awk '{print $1}')"
if [[ "${actual_size}" != "${expected_size}" || "${actual_sha}" != "${expected_sha,,}" ]]; then
echo "App asset does not match latest.json: ${name}" >&2
exit 1
fi
done < <(jq -r '.assets[] | [.name, (.size | tostring), .sha256] | @tsv' "${app_dir}/latest.json")
if [[ "${has_driver_release}" == true ]]; then
while IFS=$'\t' read -r name expected_size; do
[[ "${name}" == "$(basename "${name}")" ]] || {
echo "Invalid driver asset name in index: ${name}" >&2
exit 1
}
file="${driver_dir}/${name}"
[[ -f "${file}" ]] || {
echo "Driver asset referenced by index is missing: ${name}" >&2
exit 1
}
actual_size="$(stat -c '%s' "${file}")"
if [[ "${actual_size}" != "${expected_size}" ]]; then
echo "Driver asset size does not match index: ${name}" >&2
exit 1
fi
done < <(jq -r '.assets | to_entries[] | [.key, (.value | tostring)] | @tsv' \
"${driver_dir}/GoNavi-DriverAgents-Index.json")
fi
- name: Publish validated draft
if: steps.validate.outputs.should_publish == 'true'
uses: actions/github-script@v8
env:
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
RELEASE_ID: ${{ steps.validate.outputs.release_id }}
with:
script: |
const tag = String(process.env.RELEASE_TAG || '').trim();
const releaseId = Number(process.env.RELEASE_ID || 0);
if (!/^v\d+\.\d+\.\d+$/.test(tag) || !Number.isSafeInteger(releaseId) || releaseId <= 0) {
core.setFailed(`Invalid validated release: tag=${tag} id=${releaseId}`);
return;
}
const { owner, repo } = context.repo;
const published = await github.rest.repos.updateRelease({
owner,
repo,
release_id: releaseId,
draft: false,
prerelease: false,
make_latest: 'true',
});
if (published.data.draft || !published.data.published_at) {
core.setFailed(`Release ${tag} was not published successfully`);
return;
}
core.notice(`Published ${tag}: ${published.data.html_url}`);
- name: Verify GitHub release is published and latest
uses: actions/github-script@v8
env:
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
RELEASE_ID: ${{ steps.validate.outputs.release_id }}
with:
script: |
const tag = String(process.env.RELEASE_TAG || '').trim();
const releaseId = Number(process.env.RELEASE_ID || 0);
const { owner, repo } = context.repo;
const [releaseResult, latestResult] = await Promise.all([
github.rest.repos.getRelease({ owner, repo, release_id: releaseId }),
github.rest.repos.getLatestRelease({ owner, repo }),
]);
const release = releaseResult.data;
const latest = latestResult.data;
if (release.draft || !release.published_at || release.tag_name !== tag || latest.id !== release.id) {
core.setFailed(`Release ${tag} is not the current published GitHub latest release`);
return;
}
core.notice(`Verified GitHub latest release ${tag}`);
- name: Mirror stable release to Gatewaysentry
uses: ./.github/actions/publish-vps-mirror
with:
channel: stable
app-tag: ${{ steps.validate.outputs.tag }}
app-dir: ${{ runner.temp }}/gonavi-release
app-manifest: ${{ runner.temp }}/gonavi-release/latest.json
driver-enabled: ${{ steps.mirror_payload.outputs.has_driver_release }}
driver-tag: ${{ steps.validate.outputs.tag }}
driver-dir: ${{ runner.temp }}/driver-release
driver-version-index: ${{ runner.temp }}/driver-release/GoNavi-DriverAgents-Index.json
driver-latest-index: ${{ runner.temp }}/driver-latest-index.json
ssh-host: ${{ secrets.MIRROR_SSH_HOST }}
ssh-port: ${{ secrets.MIRROR_SSH_PORT }}
ssh-user: ${{ secrets.MIRROR_SSH_USER }}
ssh-private-key: ${{ secrets.MIRROR_SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ secrets.MIRROR_SSH_KNOWN_HOSTS }}

View File

@@ -1,22 +0,0 @@
name: Publish to WinGet
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
required: true
description: 'Tag of release you want to publish'
type: string
jobs:
publish:
runs-on: windows-2025-vs2026
steps:
- uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: Syngnat.GoNavi
installers-regex: 'GoNavi-windows-(amd64|arm64)\.exe$'
release-tag: ${{ inputs.release_tag || github.ref_name }}
token: ${{ secrets.WINGET_TOKEN }}

File diff suppressed because it is too large Load Diff

183
.github/workflows/star-history.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
# After GitHub restricted public stargazers API (2026-06-30), live api.star-history.com
# embeds need a collaborator token. Official guidance for README:
# render with a token that can read THIS repo, do not put a raw PAT in the public file.
#
# This workflow uses the repo GITHUB_TOKEN + star-history's chart renderer (via
# narayann7/star-history-action, which star-history maintainers point to when you
# prefer not to hand a fine-grained token to the public star-history API).
# Ref: https://github.com/marketplace/actions/star-history-action
# https://www.star-history.com/blog/github-stargazer-api-restriction
#
# Commit throttle: only refresh + commit when stargazers_count has grown by
# STAR_GROWTH_THRESHOLD (default 50) since the last committed baseline
# (assets/star-history/last-star-count.txt). workflow_dispatch always refreshes.
name: Star History
on:
schedule:
- cron: "0 */6 * * *"
watch:
types: [started]
workflow_dispatch:
permissions:
contents: write
concurrency:
group: star-history
cancel-in-progress: true
env:
STAR_GROWTH_THRESHOLD: "50"
STAR_BASELINE_FILE: assets/star-history/last-star-count.txt
jobs:
star-history:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Decide whether to refresh
id: gate
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
current="$(gh api "repos/${{ github.repository }}" --jq '.stargazers_count')"
if [[ ! "$current" =~ ^[0-9]+$ ]]; then
echo "Unexpected stargazers_count: $current" >&2
exit 1
fi
seed_baseline=false
if [[ -f "$STAR_BASELINE_FILE" ]]; then
last="$(tr -d '[:space:]' < "$STAR_BASELINE_FILE" || true)"
else
last=""
fi
if [[ ! "$last" =~ ^[0-9]+$ ]]; then
# No baseline in repo yet: record current count and skip chart refresh
# until the next +N growth (avoids a burst commit on throttle rollout).
mkdir -p "$(dirname "$STAR_BASELINE_FILE")"
printf '%s\n' "$current" > "$STAR_BASELINE_FILE"
last="$current"
seed_baseline=true
echo "Seeded baseline at $current (no prior file)"
fi
delta=$((current - last))
force=false
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
force=true
fi
should_refresh=false
if [[ "$force" == "true" ]]; then
should_refresh=true
reason="manual workflow_dispatch"
elif (( delta >= STAR_GROWTH_THRESHOLD )); then
should_refresh=true
reason="star growth ${delta} >= ${STAR_GROWTH_THRESHOLD} (last=${last}, current=${current})"
else
reason="star growth ${delta} < ${STAR_GROWTH_THRESHOLD} (last=${last}, current=${current})"
fi
# Seeding still needs a tiny commit so the baseline survives checkouts.
should_commit=false
if [[ "$should_refresh" == "true" || "$seed_baseline" == "true" ]]; then
should_commit=true
fi
{
echo "current=$current"
echo "last=$last"
echo "delta=$delta"
echo "should_refresh=$should_refresh"
echo "seed_baseline=$seed_baseline"
echo "should_commit=$should_commit"
} >> "$GITHUB_OUTPUT"
echo "Gate: should_refresh=$should_refresh seed_baseline=$seed_baseline — $reason"
- name: Render star history chart
if: steps.gate.outputs.should_refresh == 'true'
uses: narayann7/star-history-action@v1
with:
repos: ${{ github.repository }}
token: ${{ github.token }}
type: Date
themes: light,dark
output-dir: assets/star-history
update-readme: true
readme: README.md
readme-format: picture
commit: false
- name: Sync chart block into README.zh-CN.md
if: steps.gate.outputs.should_refresh == 'true'
run: |
set -euo pipefail
python3 <<'PY'
from pathlib import Path
import re
start = "<!-- star-history:start -->"
end = "<!-- star-history:end -->"
def extract_block(text: str) -> str:
m = re.search(
re.escape(start) + r"(.*?)" + re.escape(end),
text,
flags=re.S,
)
if not m:
raise SystemExit("star-history markers missing in README.md")
return m.group(0)
def replace_block(text: str, block: str) -> str:
if start not in text or end not in text:
raise SystemExit("star-history markers missing in README.zh-CN.md")
return re.sub(
re.escape(start) + r".*?" + re.escape(end),
block,
text,
count=1,
flags=re.S,
)
en = Path("README.md").read_text(encoding="utf-8")
zh_path = Path("README.zh-CN.md")
zh = zh_path.read_text(encoding="utf-8")
block = extract_block(en)
zh_path.write_text(replace_block(zh, block), encoding="utf-8")
print("synced star-history block into README.zh-CN.md")
PY
- name: Update baseline star count
if: steps.gate.outputs.should_refresh == 'true'
run: |
set -euo pipefail
mkdir -p "$(dirname "$STAR_BASELINE_FILE")"
printf '%s\n' "${{ steps.gate.outputs.current }}" > "$STAR_BASELINE_FILE"
echo "Updated baseline -> ${{ steps.gate.outputs.current }}"
- name: Commit if changed
if: steps.gate.outputs.should_commit == 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if [[ "${{ steps.gate.outputs.should_refresh }}" == "true" ]]; then
git add assets/star-history README.md README.zh-CN.md
msg="chore(docs): refresh star history chart [skip ci]"
else
git add "$STAR_BASELINE_FILE"
msg="chore(docs): seed star history baseline [skip ci]"
fi
if git diff --staged --quiet; then
echo "No star-history changes"
exit 0
fi
git commit -m "$msg"
git push

7
.gitignore vendored
View File

@@ -10,12 +10,19 @@ build/bin/
# wails / node artifacts (按需)
node_modules/
frontend/wailsjs/tsconfig.json
__pycache__/
*.py[cod]
dist/
.DS_Store
.gemini-clipboard
.env
docker.*.env
GoNavi-Wails
GoNavi-Wails.exe
optional-driver-agent
optional-driver-agent.exe
.ace-tool/
.superpowers/
.claude/

106
AI_EXTENSIONS_ROADMAP.md Normal file
View File

@@ -0,0 +1,106 @@
# AI 扩展能力路线
当前 GoNavi 的 AI 链路是:
1. 前端 `AIChatPanel` 组装 system messages。
2. 前端声明本地固定工具 `LOCAL_TOOLS`
3. 后端 `aiservice.Service` 只负责 Provider 配置、安全级别与模型转发。
这套结构已经足够承接“用户级提示词”,但要继续承接 MCP 和 Skills需要先把“提示词 / 工具 / 技能”三层职责拆开。
## 1. 用户级自定义提示词
已落地的方向:
- 配置存储在 `ai_config.json``userPromptSettings`
-`AISettingsModal` 提供编辑入口。
-`AIChatPanel` 在运行时追加为 system message。
建议长期保持 4 个层级:
- `global`: 所有 AI 会话统一追加。
- `database`: 数据库 / SQL 场景追加。
- `jvm`: JVM 资源浏览与分析场景追加。
- `jvmDiagnostic`: JVM 诊断命令规划场景追加。
这样既能满足“个人习惯”定制,也不会把所有场景揉成一条超长总提示词。
## 2. MCP 能力开放
目标不是把 MCP 做成新的聊天面板,而是把它变成“外部工具源”。
建议后续拆成三层:
1. `tool registry`
- 统一收口内置工具、本地扩展工具、MCP 工具。
- 对模型只暴露统一的 `tools[]`
2. `mcp server config`
- 保存 server 名称、transport、启动命令或 URL、超时、启用状态。
- 由后端维护生命周期与连通性。
3. `mcp runtime bridge`
- 负责 `list tools / call tool / errors / timeout / auth`
### MCP 是否需要单独 GitHub 仓库
不需要把“GoNavi 对 MCP 的支持”单独拆仓库。
更合理的边界是:
- `GoNavi 主仓库`
- 维护 MCP client、配置、UI、工具注册和运行时桥接。
- `单独仓库(可选)`
- 只在你要发布一个可复用的 MCP Server 时才有价值。
- 例如 `gonavi-mcp-sql-tools``gonavi-mcp-jvm-agent` 这类独立 server。
结论:
- “客户端支持 MCP” 不需要新仓库。
- “某个独立 MCP Server” 是否拆仓库,取决于它要不要单独发布、复用或部署。
## 3. Skills 设计
Skills 不建议直接等同于“另一种提示词”。
更合适的定义是:
- `skill manifest`
- 名称、说明、适用场景、是否默认启用。
- `skill prompt`
- 该技能追加的 system prompt / few-shot / 输出约束。
- `skill tool requirements`
- 该技能依赖哪些内置工具或 MCP 工具。
- `skill shortcuts`
- 可选地给欢迎卡片、斜杠命令或快速动作提供入口。
一个 Skill 本质上应该是“提示词 + 工具依赖 + 使用入口”的组合,而不是单独一段文案。
### Skills 是否需要单独 GitHub 仓库
第一阶段不需要。
建议顺序:
1. 先在 GoNavi 主仓库内把 Skills manifest/runtime 跑通。
2. 等格式稳定后再考虑增加“本地目录导入”或“Git 仓库导入”。
只有当你明确要做下面两件事时,独立仓库才值得:
- 把 Skills 当作社区共享资产分发。
- 让不同团队独立维护自己的 skill pack。
## 建议的下一步实现顺序
1. 抽出统一 `ToolRegistry`,让 `LOCAL_TOOLS` 不再硬编码在聊天面板内部。
2. 在 AI 设置中新增 `MCP Servers` 配置页。
3. 后端先支持最小 transport
- `stdio`
- `http/sse`(如果后续确认需要)
4. 在 AI 设置中新增 `Skills` 配置页。
5. 让 Skill 以 manifest 形式声明:
- `id`
- `name`
- `description`
- `systemPrompt`
- `requiredTools`
- `scopes`
6. 再决定是否增加“从 Git 仓库同步 MCP/Skills 包”的分发能力。

58
Dockerfile.build-env Normal file
View File

@@ -0,0 +1,58 @@
# syntax=docker/dockerfile:1.7
ARG GO_IMAGE=golang:1.25-bookworm
ARG NODE_IMAGE=node:20-bookworm
FROM ${NODE_IMAGE} AS node
FROM ${GO_IMAGE}
ARG WEBKIT_API=4.0
ARG WAILS_VERSION=v2.11.0
ENV DEBIAN_FRONTEND=noninteractive
ENV PATH=/go/bin:/usr/local/go/bin:${PATH}
# Debian's /etc/profile resets PATH in login shells (bash -l), dropping the Go
# toolchain; restore it via profile.d which is sourced after that reset.
RUN printf 'export PATH=/go/bin:/usr/local/go/bin:$PATH\n' > /etc/profile.d/10-gonavi-build-env.sh
# Copy only Node.js runtime assets so the Go toolchain from the base image
# stays intact for smoke tests and interactive build shells.
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
# npm/npx/corepack are symlinks in the node image; COPY dereferences them into
# standalone scripts whose relative requires break, so recreate the symlinks.
RUN ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
&& ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
build-essential \
ca-certificates \
curl \
file \
git \
make \
pkg-config \
python3 \
unzip \
zip \
libgtk-3-dev \
&& if [ "${WEBKIT_API}" = "4.1" ]; then \
apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libsoup-3.0-dev; \
else \
apt-get install -y --no-install-recommends libwebkit2gtk-4.0-dev; \
fi \
&& go install github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION} \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/share/doc/gonavi
COPY LICENSE NOTICE /usr/share/doc/gonavi/
WORKDIR /workspace
CMD ["bash"]

49
Dockerfile.mcp-server Normal file
View File

@@ -0,0 +1,49 @@
# syntax=docker/dockerfile:1.7
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
# Declared without defaults so BuildKit injects the real target platform;
# a default like TARGETARCH=amd64 would win and ship amd64 binaries to arm64.
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
COPY third_party/highgo-pq/go.mod ./third_party/highgo-pq/go.mod
COPY third_party/go-irisnative/go.mod third_party/go-irisnative/go.sum ./third_party/go-irisnative/
RUN go mod download
COPY . .
RUN ./tools/generate-driver-agent-revisions.sh --platform "${TARGETOS}/${TARGETARCH}"
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w" -o /out/gonavi-mcp-server ./cmd/gonavi-mcp-server
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --create-home --home-dir /var/lib/gonavi --uid 10001 gonavi \
&& mkdir -p /data /var/lib/gonavi/logs /usr/share/doc/gonavi \
&& chown -R gonavi:gonavi /data /var/lib/gonavi
COPY --from=builder /out/gonavi-mcp-server /usr/local/bin/gonavi-mcp-server
COPY --from=builder /src/LICENSE /usr/share/doc/gonavi/LICENSE
COPY --from=builder /src/NOTICE /usr/share/doc/gonavi/NOTICE
ENV HOME=/var/lib/gonavi \
GONAVI_DATA_ROOT=/data \
GONAVI_LOG_DIR=/var/lib/gonavi/logs \
GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765 \
GONAVI_MCP_HTTP_PATH=/mcp \
GONAVI_MCP_SCHEMA_ONLY=true
VOLUME ["/data"]
EXPOSE 8765
USER gonavi
ENTRYPOINT ["/usr/local/bin/gonavi-mcp-server"]
CMD ["http"]

65
Dockerfile.web-server Normal file
View File

@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1.7
#
# GoNavi Web Server浏览器访问版镜像。
# 复用主程序 web-server 模式 + 嵌入 frontend/dist不依赖桌面 WebView/CGO。
FROM --platform=$BUILDPLATFORM node:20-bookworm AS frontend
# 使用 monorepo 布局frontend 源码通过相对路径 ../../../shared/i18n 引用仓库 shared/
# (见 frontend/src/i18n/catalog.ts、shared/i18n/messages.ts、shared/i18n/translate.ts
# 仅 COPY frontend/ 会导致 tsc 报 Cannot find module '../../../shared/i18n/*'。
WORKDIR /src/frontend
COPY frontend/package.json frontend/package-lock.json ./
COPY frontend/patches ./patches
RUN npm ci --no-audit --no-fund
COPY frontend/ ./
COPY shared/ /src/shared/
# wailsjs 绑定已入库;容器内只构建静态前端
RUN npm run build
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod go.sum ./
COPY third_party/highgo-pq/go.mod ./third_party/highgo-pq/go.mod
COPY third_party/go-irisnative/go.mod third_party/go-irisnative/go.sum ./third_party/go-irisnative/
RUN go mod download
COPY . .
RUN ./tools/generate-driver-agent-revisions.sh --platform "${TARGETOS}/${TARGETARCH}"
COPY --from=frontend /src/frontend/dist ./frontend/dist
# 纯 Go 构建主程序web-server 模式不需要 Wails WebView / CGO
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath -ldflags="-s -w" -o /out/gonavi .
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --create-home --home-dir /var/lib/gonavi --uid 10001 gonavi \
&& mkdir -p /data /var/lib/gonavi/logs /usr/share/doc/gonavi \
&& chown -R gonavi:gonavi /data /var/lib/gonavi
COPY --from=builder /out/gonavi /usr/local/bin/gonavi
COPY --from=builder /src/LICENSE /usr/share/doc/gonavi/LICENSE
COPY --from=builder /src/NOTICE /usr/share/doc/gonavi/NOTICE
ENV HOME=/var/lib/gonavi \
GONAVI_DATA_ROOT=/data \
GONAVI_LOG_DIR=/var/lib/gonavi/logs \
GONAVI_WEB_ADDR=0.0.0.0:34116
VOLUME ["/data"]
EXPOSE 34116
USER gonavi
# 未通过环境变量配置密码时,首次访问 /setup 初始化;认证状态落在数据目录 web_auth.json
ENTRYPOINT ["/usr/local/bin/gonavi"]
CMD ["web-server"]

212
NOTICE Normal file
View File

@@ -0,0 +1,212 @@
GoNavi
Copyright 2026 Syngnat
This product includes software developed by the following projects.
Apache RocketMQ
Copyright 2016-2022 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
Apache IoTDB
Copyright 2018-2020 The Apache Software Foundation.
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
============================================================================
IoTDB project uses 4 Chinese Patents:
* 201711384490X
* 201810111712.9
* 201711322631.5
* 201711319331.1
According to the Apache 2.0 License. The owner of the patents, Tsinghua University,
grant the users the right to the use of patent under the requirement of Apache 2.0 License.
============================================================================
Apache Thrift
Copyright (C) 2006 - 2019, The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
go-yaml
Copyright 2011-2016 Canonical Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Eclipse Paho MQTT Go
This content is produced and maintained by the Eclipse Paho project.
* Project home: https://www.eclipse.org/paho/
Note that a separate mqtt v5 client (https://github.com/eclipse/paho.golang) also
exists. It is a full rewrite and deliberately incompatible with this library.
Eclipse Mosquitto trademarks of the Eclipse Foundation. Eclipse, and the
Eclipse Logo are registered trademarks of the Eclipse Foundation.
Paho is a trademark of the Eclipse Foundation. Eclipse, and the Eclipse Logo are
registered trademarks of the Eclipse Foundation.
All content is the property of the respective authors or their employers.
For more information regarding authorship of content, please consult the
listed source code repository logs.
This program and the accompanying materials are made available under the terms of the
Eclipse Public License v2.0 and Eclipse Distribution License v1.0 which accompany this
distribution.
The Eclipse Public License is available at
https://www.eclipse.org/legal/epl-2.0/
and the Eclipse Distribution License is available at
http://www.eclipse.org/org/documents/edl-v10.php.
For an explanation of what dual-licensing means to you, see:
https://www.eclipse.org/legal/eplfaq.php#DUALLIC
SPDX-License-Identifier: EPL-2.0 or BSD-3-Clause
The project maintains the following source code repository:
* https://github.com/eclipse/paho.mqtt.golang
This project makes use of the following third-party projects:
Go Programming Language and Standard Library
* License: BSD-style license (https://golang.org/LICENSE)
Go Networking
* License: BSD 3-Clause style license and patent grant.
* Project: https://cs.opensource.google/go/x/net
Go Sync
* License: BSD 3-Clause style license and patent grant.
* Project: https://cs.opensource.google/go/x/sync/
Gorilla Websockets v1.4.2
* License: BSD 2-Clause "Simplified" License
* Project: https://github.com/gorilla/websocket
Content may contain encryption software. The country in which you are currently
may have restrictions on the import, possession, and use, and/or re-export to
another country. BEFORE using any encryption software, please check the country's
laws, regulations and policies concerning the import, possession, and use, and/or
re-export of encryption software, to see if this is permitted.
Vendored HighGo pq-sm3 (derived from lib/pq)
Copyright (c) 2011-2013, 'pq' Contributors
Portions Copyright (C) 2011 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The vendored SCRAM implementation includes the following BSD 3-Clause notice:
Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Tailscale hujson
Project: https://github.com/tailscale/hujson
Copyright (c) 2019 Tailscale Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Vendored InterSystems IRIS native driver
Copyright (c) 2025 Dmitry Maslennikov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

568
README.md
View File

@@ -1,42 +1,187 @@
# GoNavi - A Modern Lightweight Database Client
<p align="center">
<img src="assets/readme-logo.png" width="128" alt="GoNavi Logo" />
</p>
[![Go Version](https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi)](https://go.dev/)
[![Wails Version](https://img.shields.io/badge/Wails-v2-red)](https://wails.io)
[![React Version](https://img.shields.io/badge/React-v18-blue)](https://reactjs.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?label=Build)](https://github.com/Syngnat/GoNavi/actions)
[![Stars](https://img.shields.io/github/stars/Syngnat/GoNavi?style=social)](https://github.com/Syngnat/GoNavi/stargazers)
[![Downloads](https://img.shields.io/github/downloads/Syngnat/GoNavi/total?color=blue&label=downloads)](https://github.com/Syngnat/GoNavi/releases)
<h1 align="center">GoNavi</h1>
**Language**: English | [简体中文](README.zh-CN.md)
<p align="center">
<b>Navigate every data source — native speed, agent-ready, zero Electron bloat.</b>
</p>
GoNavi is a modern, high-performance, cross-platform database client built with **Wails (Go)** and **React**.
It delivers native-like responsiveness with low resource usage.
<p align="center">
A high-performance cross-platform database client built with
<a href="https://wails.io">Wails</a> (Go) + <a href="https://react.dev">React</a>.
Desktop-first. MCP-ready. ~30MB class binaries.
</p>
Compared with many Electron-based clients, GoNavi is typically smaller in binary size (around 10MB class), starts faster, and uses less memory.
<p align="center">
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/v/release/Syngnat/GoNavi?style=for-the-badge&color=8B5CF6" alt="Release" /></a>
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/downloads/Syngnat/GoNavi/total?style=for-the-badge&color=6366F1&label=downloads" alt="Downloads" /></a>
<a href="https://github.com/Syngnat/GoNavi/stargazers"><img src="https://img.shields.io/github/stars/Syngnat/GoNavi?style=for-the-badge&color=F59E0B" alt="Stars" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-22C55E?style=for-the-badge" alt="License" /></a>
</p>
<p align="center">
<a href="https://go.dev"><img src="https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi?style=flat-square&logo=go&logoColor=white&label=Go" alt="Go" /></a>
<a href="https://wails.io"><img src="https://img.shields.io/badge/Wails-v2-red?style=flat-square" alt="Wails" /></a>
<a href="https://reactjs.org"><img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white" alt="React" /></a>
<a href="https://github.com/Syngnat/GoNavi/actions"><img src="https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?style=flat-square&label=Build" alt="Build" /></a>
</p>
<p align="center">
<b>Language</b>: English · <a href="README.zh-CN.md">简体中文</a>
&nbsp;·&nbsp;
<a href="https://github.com/Syngnat/GoNavi/releases"><b>⬇ Download</b></a>
·
<a href="#-quick-start"><b>⚡ Quick Start</b></a>
·
<a href="#-key-features"><b>✨ Features</b></a>
·
<a href="#-mcp--agents"><b>🤖 MCP</b></a>
</p>
<p align="center">
<sub>Sponsored by
<a href="#-sponsors"><b>华龙中转站 / Hualong Transfer Station</b></a></sub>
</p>
---
## Project Overview
## Why GoNavi?
GoNavi is designed for developers and DBAs who need a unified desktop experience across multiple databases.
Most database GUIs are Electron shells with megabytes of tax. GoNavi takes a different path:
- **Native-performance architecture**: Wails (Go + WebView) with lightweight runtime overhead.
- **Large dataset usability**: virtualized rendering and optimized DataGrid workflows for high-volume tables.
- **Unified connectivity**: URI build/parse, SSH tunnel, proxy support, and on-demand driver activation.
- **Production-oriented workflow**: SQL editor, object management, batch export/backup, sync tools, execution logs, and update checks.
| | Typical Electron client | **GoNavi** |
|---|---|---|
| Runtime | Chromium + Node | **Go + native WebView** |
| Binary size | Hundreds of MB | **~30MB class** |
| Startup | Heavy | **Fast** |
| Memory | High baseline | **Lean** |
| AI / Agents | Bolt-on or absent | **First-class MCP + multi-provider AI** |
| Data sources | Mostly RDBMS | **SQL · Cache · Vector · MQ · Search · Time-series · Domestic DBs** |
## Supported Data Sources
> **One cockpit for MySQL, Postgres, Redis, Kafka, Milvus, OceanBase, ClickHouse…**
> Query, edit, audit, sync — and hand structured context to coding agents without leaking passwords off-host.
> `Built-in`: available out of the box.
> `Optional driver agent`: install/enable via Driver Manager first.
---
## At a Glance
```text
┌──────────────────────────────────────────────────────────────────────┐
│ GoNavi Workbench │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ Connections │ │ Monaco SQL + AI │ │ Virtualized DataGrid │ │
│ │ SSH / Proxy │ │ Schema context │ │ Batch edit · Export │ │
│ │ Drivers │ │ Slash commands │ │ Txn submit / rollback │ │
│ └─────────────┘ └────────┬─────────┘ └─────────────────────────┘ │
│ │ │
│ ┌─────────────▼─────────────┐ │
│ │ Go core · Audit · Sync │ │
│ │ MCP HTTP · Web Server │ │
│ └───────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
### Product screenshots
Each image is a **full GoNavi application window**, scaled proportionally for README display.
<p align="center">
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/01-home-workbench.png" alt="GoNavi full window — connections, queries, and workbench" width="560" />
&nbsp;
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/04-ai-assistant.png" alt="GoNavi full window — AI assistant with schema context" width="560" />
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/06-new-connection.png" alt="GoNavi full window — new connection data-source selector" width="560" />
</p>
<p align="center"><sub>Real desktop captures · full window</sub></p>
---
## ✨ Key Features
<table>
<tr>
<td width="50%" valign="top">
### 🤖 AI that knows your schema
- OpenAI · Gemini · Claude · custom OpenAI-compatible APIs
- Attach live table schemas as context
- Slash commands: generate SQL, explain, optimize, review
- **MCP**: install into Claude Code / Codex, or Streamable HTTP for remote agents
- Secrets stay on the GoNavi host — agents get tools, not raw passwords
</td>
<td width="50%" valign="top">
### ⚡ Built for large data
- Virtualized DataGrid for heavy result sets
- In-place cell edit · batch CRUD · transaction submit/rollback
- Large-field popup editor · smart read/write modes
- Export: CSV · XLSX · JSON · Markdown
- Monaco editor with context-aware completion
</td>
</tr>
<tr>
<td width="50%" valign="top">
### 🔌 Connectivity without drama
- URI generate / parse
- SSH tunnel · proxy
- Connection JSON import / export
- On-demand optional driver agents
- Custom Driver + DSN extensibility
</td>
<td width="50%" valign="top">
### 🛡️ Observable & shippable
- SQL execution logs with timing
- Audit center (redacted by default, retention, export)
- Desktop + experimental **Web Server** mode
- Docker / K8s / Helm / Podman packaging
- Auto update checks · multi-arch releases
</td>
</tr>
</table>
### 🧩 Stack
`Go 1.24` · `Wails v2` · `React 18` · `TypeScript` · `Vite` · `Ant Design 5` · `Zustand` · `Monaco`
---
## 🗄 Supported Data Sources
> **Built-in** — ready out of the box · **Optional agent** — install via Driver Manager
| | |
|---|---|
| **Built-in** | MySQL · GoldenDB · PostgreSQL · Oracle · Redis · Chroma · Qdrant · Milvus · RocketMQ · MQTT · Kafka · RabbitMQ |
| **Optional** | MariaDB · Doris · StarRocks · Sphinx · SQL Server · SQLite · DuckDB · OceanBase · Dameng · Kingbase · HighGo · Vastbase · OpenGauss · GaussDB · IRIS · MongoDB · TDengine · IoTDB · ClickHouse · Trino · Elasticsearch · Custom Driver/DSN |
<details>
<summary><b>Full capability matrix</b></summary>
| Category | Data Source | Driver Mode | Typical Capabilities |
|---|---|---|---|
| Relational | MySQL | Built-in | Schema browsing, SQL query, data editing, export/backup |
| Domestic DB | GoldenDB | Built-in | MySQL-compatible query workflow and distributed transaction scenarios |
| Relational | PostgreSQL | Built-in | Schema browsing, SQL query, data editing, object management |
| Relational | Oracle | Built-in | Query execution, object browsing, data editing |
| Cache | Redis | Built-in | Key browsing, command execution, encoding/view switch |
| Vector Database | Chroma | Built-in | Collection browsing, vector retrieval, metadata filtering |
| Vector Database | Qdrant | Built-in | Collection browsing, vector search, payload filtering |
| Vector Database | Milvus | Built-in | Collection browsing, vector search, scalar filtering |
| Message Queue | RocketMQ | Built-in | Topic browsing, consumer-group inspection, message-oriented workflow |
| Message Queue | MQTT | Built-in | Broker and topic-filter workflow with QoS-aware connection settings |
| Message Queue | Kafka | Built-in | Topic browsing, broker metadata, consumer-group workflow |
| Message Queue | RabbitMQ | Built-in | Queue/exchange browsing, virtual host inspection, management API workflow |
| Relational | MariaDB | Optional driver agent | Querying, object management, data editing |
| Relational | Doris | Optional driver agent | Querying, object browsing, SQL execution |
| Columnar Analytics | StarRocks | Optional driver agent | Querying, object browsing, SQL execution |
@@ -44,161 +189,234 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
| Relational | SQL Server | Optional driver agent | Schema browsing, SQL query, object management |
| File-based | SQLite | Optional driver agent | Local DB browsing, editing, export |
| File-based | DuckDB | Optional driver agent | Large-table query, pagination, file-DB workflow |
| Domestic DB | OceanBase | Optional driver agent | MySQL / Oracle tenant access, object browsing, query workflow |
| Domestic DB | Dameng | Optional driver agent | Querying, object browsing, data editing |
| Domestic DB | Kingbase | Optional driver agent | Querying, object browsing, data editing |
| Domestic DB | HighGo | Optional driver agent | Querying, object browsing, data editing |
| Domestic DB | Vastbase | Optional driver agent | Querying, object browsing, data editing |
| Domestic DB | OpenGauss | Optional driver agent | PostgreSQL-like schema browsing, SQL query, object management |
| Domestic DB | GaussDB | Optional driver agent | PostgreSQL-like schema browsing, SQL query, object management |
| Multi-model | InterSystems IRIS | Optional driver agent | Namespace browsing, SQL query, object management |
| Document | MongoDB | Optional driver agent | Document query, collection browsing, connection management |
| Time-series | TDengine | Optional driver agent | Time-series schema browsing and querying |
| Time-series | Apache IoTDB | Optional driver agent | Storage group / device / timeseries browsing and querying |
| Columnar Analytics | ClickHouse | Optional driver agent | Analytical query, object browsing, SQL execution |
| Federated Query | Trino | Optional driver agent | Cross-source SQL via multiple catalogs, `catalog.schema` browsing, SQL execution |
| Search | Elasticsearch | Optional driver agent | Index browsing, mapping inspection, JSON DSL / query_string search |
| Extensibility | Custom Driver/DSN | Custom | Extend to more data sources via Driver + DSN |
<h2 align="center">📸 Screenshots</h2>
<div align="center">
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/0eefe07f-2836-44fa-9ddf-a0d2124b90e2" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/6765e539-83ea-4cd6-9c9e-f42790fa05b5" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/60e3d187-171a-4248-94e0-c6b08736e235" />
<br />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/7a478602-0f08-4b30-8f6a-879f4a60ae32" />
<img width="14%" alt="image" src="https://github.com/user-attachments/assets/6442ca7d-ce9e-46d9-aecd-405ba88f5a5e" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/bc17895e-02a4-4cc5-b471-c3803cf25a2b" />
</div>
</details>
---
## Key Features
### AI Assistant (New)
- **Multi-provider Support**: OpenAI, Google Gemini, Anthropic Claude, and custom API support.
- **Context-Aware Chat**: Attach table schemas to the AI context for accurate SQL generation and assistance.
- **Slash Commands**: Quick commands for generating SQL, explaining queries, optimizing performance, and reviewing schema designs.
### Performance
- **Smooth interaction under load**: optimized table interaction (including column resize workflow on large datasets).
- **Virtualized rendering**: keeps large result sets responsive.
### Data Management (DataGrid)
- In-place cell editing.
- Batch insert/update/delete with transaction-oriented submit/rollback.
- Large-field popup editor.
- Context actions (set NULL, copy/export, etc.).
- Smart read/write mode switching based on query context.
- Export formats: CSV, Excel (XLSX), JSON, Markdown.
### SQL Editor
- Monaco Editor core.
- Context-aware completion for databases/tables/columns.
- Multi-tab query workflow.
### Batch Export / Backup
- Database-level and table-level batch export/backup.
- Scope-aware operation flow to reduce mistakes.
### Connectivity
- URI generation/parsing.
- SSH tunnel support.
- Proxy support.
- Config import/export (JSON).
- Optional driver management and activation.
### Redis Tools
- Multi-view value rendering (auto/raw text/UTF-8/hex).
- Built-in command execution panel.
### Observability and Update
- SQL execution logs with timing information.
- Startup/scheduled/manual update checks.
### UI/UX
- Ant Design 5 based interface.
- Light/Dark themes.
- Flexible sidebar and layout behavior.
---
## Tech Stack
- **Backend**: Go 1.24 + Wails v2
- **Frontend**: React 18 + TypeScript + Vite
- **UI**: Ant Design 5
- **State Management**: Zustand
- **Editor**: Monaco Editor
---
## Installation and Run
## 🚀 Quick Start
### Prerequisites
- [Go](https://go.dev/dl/) 1.21+
- [Node.js](https://nodejs.org/) 18+
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
- [Wails CLI](https://wails.io/docs/gettingstarted/installation)
### Development Mode
```bash
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
```
```shell
# Clone
### Develop
```bash
git clone https://github.com/Syngnat/GoNavi.git
cd GoNavi
# Start development with hot reload
wails dev
# Faster local startup when exported Go method signatures are unchanged
node tools/wails-fast-dev.mjs
# Refresh Wails JS bindings after changing exported Go method signatures
node tools/wails-fast-dev.mjs --refresh-bindings
# Windows PowerShell low-memory visual mode: disables transparent WebView/Acrylic backdrop
$env:GONAVI_LOW_MEMORY_MODE="1"; node tools/wails-fast-dev.mjs
wails dev # full hot reload
node tools/wails-fast-dev.mjs # faster when Go exports unchanged
node tools/wails-fast-dev.mjs --refresh-bindings # after Go export signature changes
```
### Build
```bash
# Build for current platform
wails build
# Clean build (recommended before release)
wails build -clean
wails build -clean # clean build before release
```
Artifacts are generated in `build/bin`.
Artifacts `build/bin`.
### Cross-Platform Release (GitHub Actions)
### Prefer a binary?
The repository includes a release workflow.
Push a `v*` tag to trigger automated build and release.
Release notes are generated automatically from merged pull requests and categorized by `.github/release.yaml`.
Target artifacts include:
- macOS (AMD64 / ARM64)
- Windows (AMD64)
- Linux (AMD64, WebKitGTK 4.0 and 4.1 variants)
Grab the latest build from **[Releases](https://github.com/Syngnat/GoNavi/releases)**
(macOS AMD64/ARM64 · Windows AMD64 · Linux WebKitGTK 4.0/4.1).
---
## Troubleshooting
## 🌐 Web Server (Experimental)
### macOS: "App is damaged and cant be opened"
Same Go backend + React UI over HTTP — **not** a containerized Wails window.
Without Apple notarization, Gatekeeper may block startup.
```powershell
go build .
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
```
1. Move `GoNavi.app` to **Applications**.
2. Open **Terminal**.
3. Run:
- First visit → `/setup` (admin password; optional Google Authenticator)
- Bridge: `window.go.*` / `window.runtime.*` → HTTP / SSE
- Sessions, recovery codes, login rate limits
#### Docker / Podman
```bash
cp docker.web-server.env.example docker.web-server.env
# set GONAVI_HOST_DATA_ROOT (absolute path)
# optional: GONAVI_WEB_PASSWORD (min 6 chars)
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
```
Open `http://127.0.0.1:34116`. Mount the active data root at `/data`
(`connections.json`, `daily_secrets.json`, optional `drivers/`).
Auth state → `web_auth.json`.
> **Do not expose an unhardened web entry to the public internet.** Use reverse proxy + HTTPS in production.
Health: `GET /__gonavi/healthz`
Local source build: add `-f docker-compose.web-server.local.yml --build`.
After changing env passwords:
```bash
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
```
(`docker restart` does **not** reload env files.)
---
## 🤖 MCP & Agents
Ship schema tools to agents without shipping your vault:
```bash
cp docker.mcp-server.env.example docker.mcp-server.env
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
```
| Surface | Entry |
|---|---|
| MCP container | `docker-compose.mcp-server.yml``ghcr.io/syngnat/gonavi-mcp-server` |
| Web UI container | `docker-compose.web-server.yml``ghcr.io/syngnat/gonavi-web-server` |
| Podman / Quadlet | [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server) |
| Kubernetes | [deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server) |
| Helm | [deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server) |
| Build-only image | `Dockerfile.build-env``ghcr.io/syngnat/gonavi-build-env` |
Safety defaults: remote `schema-only` omits `execute_sql`; mutating SQL requires explicit `allowMutating=true`.
Details: [cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md).
### Linux build environment only
```bash
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
```
---
## 📦 Release Pipeline
Push a `v*` tag → GitHub Actions builds multi-arch releases.
Notes auto-generated from merged PRs via `.github/release.yaml`.
---
## 🛠 Troubleshooting
<details>
<summary><b>Windows: missing Microsoft Edge WebView2 Runtime (common on intranet images)</b></summary>
The GoNavi desktop app on Windows depends on the **Microsoft Edge WebView2 Runtime** (a system component, not full Chrome).
Some intranet / thin / Server / LTSC images ship without it. Symptoms:
- Process exits immediately, blank/white window
- Errors about missing WebView2 / WebView2 Runtime
- Installer blocked by AV or group policy
### 1. Check whether Runtime is installed
In **PowerShell**:
```powershell
# Common Evergreen install path (64-bit Windows)
Test-Path "${env:ProgramFiles(x86)}\Microsoft\EdgeWebView\Application"
# Registry (a `pv` version usually means installed)
Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" `
-ErrorAction SilentlyContinue |
Select-Object pv, name
```
If the path is missing and registry has no `pv`, install the Runtime.
### 2. Online install (simplest)
1. Open the official download page:
[Microsoft Edge WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)
2. Download the **Evergreen Bootstrapper** (small; needs network during install)
3. Run as Administrator, then **restart GoNavi**
### 3. Offline / intranet: Standalone installer (recommended for IT)
The Bootstrapper fails on fully air-gapped machines. Use the **Evergreen Standalone Installer** instead:
1. On a machine with internet, download the matching architecture package, e.g.:
- `MicrosoftEdgeWebView2RuntimeInstallerX64.exe` (most 64-bit PCs)
- `…X86.exe` / `…ARM64.exe` as needed
2. Copy the installer into the intranet (USB, software center, share)
3. Install **as Administrator** on the target PC:
```powershell
# Interactive
.\MicrosoftEdgeWebView2RuntimeInstallerX64.exe
# Silent (batch / SCCM friendly)
.\MicrosoftEdgeWebView2RuntimeInstallerX64.exe /silent /install
```
4. Start GoNavi again. If it still fails, sign out or reboot Windows once.
### 4. Group policy / locked-down PCs
- Needs local admin, or IT push via SCCM / software center
- Ensure policies do not block Edge/WebView2 install or updates
- Enterprises may pin a [Fixed Version](https://developer.microsoft.com/microsoft-edge/webview2/) Runtime (most users should prefer Evergreen)
### 5. Temporary workaround: Web Server mode
If the desktop WebView cannot be installed yet, run the experimental Web Server and use a normal browser:
```powershell
.\GoNavi.exe web-server --addr 127.0.0.1:34116
```
Open `http://127.0.0.1:34116`. See the **Web Server** section above.
Do **not** expose an unhardened Web endpoint to the public internet.
Tracker / discussion: [#672](https://github.com/Syngnat/GoNavi/issues/672).
</details>
<details>
<summary><b>macOS: “App is damaged and cant be opened”</b></summary>
Without Apple notarization, Gatekeeper may block the app:
```bash
sudo xattr -rd com.apple.quarantine /Applications/GoNavi.app
```
Or right-click the app in Finder and choose **Open** with Control key flow.
Or right-click → Open (Control-click flow). Move the app to **Applications** first.
### Linux: missing `libwebkit2gtk` / `libjavascriptcoregtk`
</details>
GoNavi depends on WebKitGTK runtime libraries.
<details>
<summary><b>Linux: missing WebKitGTK</b></summary>
```bash
# Debian 13 / Ubuntu 24.04+
@@ -210,34 +428,92 @@ sudo apt-get update
sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18
```
If you use Linux artifacts with the `-WebKit41` suffix, prefer Debian 13 / Ubuntu 24.04+.
Artifacts with `-WebKit41` prefer Debian 13 / Ubuntu 24.04+.
</details>
<details>
<summary><b>Linux: Chinese glyphs as tofu boxes</b></summary>
```bash
sudo apt-get update
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
fc-cache -fv
```
</details>
---
## Contributing
## 💖 Sponsors
Issues and pull requests are welcome.
<p align="center">
<a href="https://api-fast.hualong.online/">
<img src="assets/sponsors/hualong-mark.png" width="120" alt="华龙中转站 Hualong Transfer Station" />
</a>
</p>
For the full workflow, branch model, and maintainer sync rules, see:
<p align="center">
<b><a href="https://api-fast.hualong.online/">华龙中转站 · Hualong Transfer Station</a></b><br/>
<sub>AI API Gateway · multi-model routing · domestic high-speed endpoint</sub>
</p>
Special thanks to **[华龙中转站 (Hualong Transfer Station)](https://api-fast.hualong.online/)** for sponsoring GoNavi open-source development.
| | |
|---|---|
| **What they are** | An **AI API Gateway** that unifies multi-model access behind an OpenAI-compatible surface — so you can ship LLM features without fighting provider fragmentation and network friction. |
| **What you get** | Multi-model routing · OpenAI-compatible API · **domestic high-speed direct endpoint** · practical for indie devs and open-source projects that need reliable LLM access |
| **Why we recommend them** | GoNavi is AI/MCP-first. Stable model access is core infrastructure for coding agents, schema-aware assistants, and day-to-day development. |
**Looking for a solid LLM relay?**
👉 [https://api-fast.hualong.online/](https://api-fast.hualong.online/) — console, docs, and the domestic high-speed endpoint.
> Hualong also sponsored a **shared 50B token pool** for GoNavi **co-maintainers**, used via **API Key** (not 50B per person). Rules and how to apply: [Issue #671](https://github.com/Syngnat/GoNavi/issues/671).
---
## 🤝 Contributing
Issues and PRs welcome. Branch from **`dev`**, PR against **`dev`**.
- [CONTRIBUTING.md](CONTRIBUTING.md)
- Call for contributors (incl. sponsored token pool): [Issue #671](https://github.com/Syngnat/GoNavi/issues/671)
External contributors should branch from `dev` and open pull requests against `dev`.
---
## Star History
<a href="https://www.star-history.com/?repos=Syngnat%2FGoNavi&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&legend=top-left" />
</picture>
</a>
## 💬 Contact & Feedback
| Channel | Notes |
|---|---|
| **GitHub Issues** | Preferred for bugs, feature requests, and docs — easier to track and reproduce |
| **WeChat** | `ygf1140302783` (note “GoNavi”) — maintainer can invite you to the community group for discussion |
> For technical reports, include version, OS, and repro steps when possible. Windows blank/crash on intranet images: [WebView2 guide](https://github.com/Syngnat/GoNavi/issues/672).
---
## ⭐ Star History
<!-- star-history:start -->
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/star-history/star-history-dark.svg">
<img alt="Star history" src="assets/star-history/star-history-light.svg">
</picture>
<!-- star-history:end -->
---
## Links
- [华龙中转站 · Hualong Transfer Station](https://api-fast.hualong.online/) — AI API Gateway (GoNavi sponsor)
- [linux.do](https://linux.do/)
- [AIBook](https://aibook.ren/)
## License
Licensed under [Apache-2.0](LICENSE).
[Apache-2.0](LICENSE)
<p align="center">
<sub>Built for people who live in SQL, schemas, and agent loops.</sub>
</p>

View File

@@ -1,41 +1,186 @@
# GoNavi - 现代化轻量级数据库客户端
<p align="center">
<img src="assets/readme-logo.png" width="128" alt="GoNavi Logo" />
</p>
[![Go Version](https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi)](https://go.dev/)
[![Wails Version](https://img.shields.io/badge/Wails-v2-red)](https://wails.io)
[![React Version](https://img.shields.io/badge/React-v18-blue)](https://reactjs.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?label=Build)](https://github.com/Syngnat/GoNavi/actions)
[![Stars](https://img.shields.io/github/stars/Syngnat/GoNavi?style=social)](https://github.com/Syngnat/GoNavi/stargazers)
[![Downloads](https://img.shields.io/github/downloads/Syngnat/GoNavi/total?color=blue&label=downloads)](https://github.com/Syngnat/GoNavi/releases)
<h1 align="center">GoNavi</h1>
**语言**: [English](README.md) | 简体中文
<p align="center">
<b>一套客户端,航行全部数据源 — 原生性能 · Agent 就绪 · 告别 Electron 膨胀。</b>
</p>
GoNavi 是基于 **Wails (Go)****React** 构建的跨平台数据库管理工具,强调原生性能、低资源占用与多数据源统一工作流。
<p align="center">
基于 <a href="https://wails.io">Wails</a>Go+ <a href="https://react.dev">React</a> 的跨平台数据库工作台。
桌面优先MCP 原生,二进制约 <b>30MB</b> 量级。
</p>
相比常见 Electron 客户端GoNavi 在体积、启动速度和内存占用上更轻量。
<p align="center">
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/v/release/Syngnat/GoNavi?style=for-the-badge&color=8B5CF6" alt="Release" /></a>
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/downloads/Syngnat/GoNavi/total?style=for-the-badge&color=6366F1&label=downloads" alt="Downloads" /></a>
<a href="https://github.com/Syngnat/GoNavi/stargazers"><img src="https://img.shields.io/github/stars/Syngnat/GoNavi?style=for-the-badge&color=F59E0B" alt="Stars" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-22C55E?style=for-the-badge" alt="License" /></a>
</p>
<p align="center">
<a href="https://go.dev"><img src="https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi?style=flat-square&logo=go&logoColor=white&label=Go" alt="Go" /></a>
<a href="https://wails.io"><img src="https://img.shields.io/badge/Wails-v2-red?style=flat-square" alt="Wails" /></a>
<a href="https://reactjs.org"><img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white" alt="React" /></a>
<a href="https://github.com/Syngnat/GoNavi/actions"><img src="https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?style=flat-square&label=Build" alt="Build" /></a>
</p>
<p align="center">
<b>语言</b><a href="README.md">English</a> · 简体中文
&nbsp;·&nbsp;
<a href="https://github.com/Syngnat/GoNavi/releases"><b>⬇ 下载</b></a>
·
<a href="#-快速开始"><b>⚡ 快速开始</b></a>
·
<a href="#-核心能力"><b>✨ 特性</b></a>
·
<a href="#-mcp--agents"><b>🤖 MCP</b></a>
</p>
<p align="center">
<sub>赞助商 ·
<a href="#-赞助商"><b>华龙中转站</b></a></sub>
</p>
---
## 项目简介
## 为什么是 GoNavi
GoNavi 面向开发者与 DBA核心目标是让数据库操作在桌面端做到“快、稳、统一”。
市面上大量数据库 GUI 仍是 Electron 壳体积大、启动慢、内存高。GoNavi 换了一条路:
- **原生性能架构**WailsGo + WebView降低运行时开销。
- **大数据可用性**:虚拟滚动 + DataGrid 交互优化,提升大结果集可操作性。
- **统一连接能力**:支持 URI 生成/解析、SSH 隧道、代理、驱动按需安装。
- **工程化能力完整**:覆盖 SQL 编辑、对象管理、批量导出/备份、数据同步、执行日志、在线更新。
| | 常见 Electron 客户端 | **GoNavi** |
|---|---|---|
| 运行时 | Chromium + Node | **Go + 系统 WebView** |
| 体积 | 动辄数百 MB | **约 30MB 量级** |
| 启动 | 偏重 | **更快** |
| 内存 | 基线高 | **更轻** |
| AI / Agent | 外挂或缺失 | **MCP + 多模型一等公民** |
| 数据源 | 以 RDBMS 为主 | **SQL · 缓存 · 向量 · 消息 · 搜索 · 时序 · 国产库** |
## 支持的数据源
> **MySQL、Postgres、Redis、Kafka、Milvus、OceanBase、ClickHouse… 一个工作台打通。**
> 查询、编辑、审计、同步;把结构化上下文交给编码 Agent密码仍留在本机。
> `内置`:主程序开箱即用。
> `可选驱动代理`:需在驱动管理中安装启用后可用。
---
## 一眼看懂
```text
┌──────────────────────────────────────────────────────────────────────┐
│ GoNavi 工作台 │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ 连接管理 │ │ Monaco SQL + AI │ │ 虚拟滚动 DataGrid │ │
│ │ SSH / 代理 │ │ 表结构上下文 │ │ 批量编辑 · 导出 │ │
│ │ 驱动代理 │ │ 快捷指令 │ │ 事务提交 / 回滚 │ │
│ └─────────────┘ └────────┬─────────┘ └─────────────────────────┘ │
│ │ │
│ ┌─────────────▼─────────────┐ │
│ │ Go 核心 · 审计 · 同步 │ │
│ │ MCP HTTP · Web Server │ │
│ └───────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
### 产品截图
每张均为 **完整 GoNavi 应用窗口**,再通过 README 宽度控件等比例缩小展示。
<p align="center">
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/01-home-workbench.png" alt="GoNavi 全窗口 — 连接、查询与工作台" width="560" />
&nbsp;
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/04-ai-assistant.png" alt="GoNavi 全窗口 — 带表结构上下文的 AI 助手" width="560" />
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/06-new-connection.png" alt="GoNavi 全窗口 — 新建连接的数据源选择器" width="560" />
</p>
<p align="center"><sub>真实桌面全窗口截图</sub></p>
---
## ✨ 核心能力
<table>
<tr>
<td width="50%" valign="top">
### 🤖 懂表结构的 AI
- OpenAI · Gemini · Claude · 自定义 OpenAI 兼容 API
- 将当前库表结构注入对话上下文
- 快捷指令:生成 SQL、解释、优化、表设计评审
- **MCP**:一键装到 Claude Code / Codex或 Streamable HTTP 给远端 Agent
- 连接与密码留在运行 GoNavi 的主机上
</td>
<td width="50%" valign="top">
### ⚡ 大数据仍流畅
- 虚拟滚动 DataGrid扛住大结果集
- 单元格编辑 · 批量增删改 · 事务提交/回滚
- 大字段弹窗 · 按查询智能切换读写
- 导出CSV · XLSX · JSON · Markdown
- Monaco + 库/表/字段上下文补全
</td>
</tr>
<tr>
<td width="50%" valign="top">
### 🔌 连接与驱动
- URI 生成 / 解析
- SSH 隧道 · 代理
- 连接配置 JSON 导入导出
- 可选驱动代理按需安装
- Custom Driver + DSN 扩展
</td>
<td width="50%" valign="top">
### 🛡️ 可观测 & 可交付
- SQL 执行日志(含耗时)
- 审计中心(默认脱敏、保留策略、导出)
- 桌面端 + 实验性 **Web Server**
- Docker / K8s / Helm / Podman
- 更新检查 · 多架构发布
</td>
</tr>
</table>
### 🧩 技术栈
`Go 1.24` · `Wails v2` · `React 18` · `TypeScript` · `Vite` · `Ant Design 5` · `Zustand` · `Monaco`
---
## 🗄 支持的数据源
> **内置**:开箱即用 · **可选驱动代理**:在驱动管理中安装启用
| | |
|---|---|
| **内置** | MySQL · GoldenDB · PostgreSQL · Oracle · Redis · Chroma · Qdrant · Milvus · RocketMQ · MQTT · Kafka · RabbitMQ |
| **可选** | MariaDB · Doris · StarRocks · Sphinx · SQL Server · SQLite · DuckDB · OceanBase · 达梦 · 人大金仓 · 瀚高 · 海量 · openGauss · GaussDB · IRIS · MongoDB · TDengine · IoTDB · ClickHouse · Trino · Elasticsearch · Custom Driver/DSN |
<details>
<summary><b>完整能力矩阵</b></summary>
| 类别 | 数据源 | 驱动模式 | 典型能力 |
|---|---|---|---|
| 关系型 | MySQL | 内置 | 库表浏览、SQL 查询、数据编辑、导出/备份 |
| 国产数据库 | GoldenDB | 内置 | MySQL 兼容查询工作流、分布式事务场景 |
| 关系型 | PostgreSQL | 内置 | 库表浏览、SQL 查询、数据编辑、对象管理 |
| 关系型 | Oracle | 内置 | 连接查询、对象浏览、数据编辑 |
| 缓存 | Redis | 内置 | Key 浏览、命令执行、编码/视图切换 |
| 向量数据库 | Chroma | 内置 | Collection 浏览、向量检索、元数据过滤 |
| 向量数据库 | Qdrant | 内置 | Collection 浏览、向量搜索、Payload 过滤 |
| 向量数据库 | Milvus | 内置 | Collection 浏览、向量搜索、标量过滤 |
| 消息队列 | RocketMQ | 内置 | Topic 浏览、消费组检查、消息型工作流 |
| 消息队列 | MQTT | 内置 | Broker / Topic Filter 工作流与 QoS 连接配置 |
| 消息队列 | Kafka | 内置 | Topic 浏览、Broker 元数据、消费组工作流 |
| 消息队列 | RabbitMQ | 内置 | Queue / Exchange 浏览、Virtual Host 检查、Management API 工作流 |
| 关系型 | MariaDB | 可选驱动代理 | 连接查询、对象管理、数据编辑 |
| 关系型 | Doris | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
| 列式分析 | StarRocks | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
@@ -43,147 +188,235 @@ GoNavi 面向开发者与 DBA核心目标是让数据库操作在桌面端做
| 关系型 | SQL Server | 可选驱动代理 | 库表浏览、SQL 查询、对象管理 |
| 文件型 | SQLite | 可选驱动代理 | 本地文件库浏览、编辑、导出 |
| 文件型 | DuckDB | 可选驱动代理 | 大表查询、分页浏览、文件库管理 |
| 国产数据库 | OceanBase | 可选驱动代理 | MySQL / Oracle 租户接入、对象浏览、查询工作流 |
| 国产数据库 | Dameng | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
| 国产数据库 | Kingbase | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
| 国产数据库 | HighGo | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
| 国产数据库 | Vastbase | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
| 国产数据库 | OpenGauss | 可选驱动代理 | 类 PostgreSQL 的库表浏览、SQL 查询、对象管理 |
| 国产数据库 | GaussDB | 可选驱动代理 | 类 PostgreSQL 的库表浏览、SQL 查询、对象管理 |
| 多模型数据库 | InterSystems IRIS | 可选驱动代理 | Namespace 浏览、SQL 查询、对象管理 |
| 文档型 | MongoDB | 可选驱动代理 | 文档查询、集合浏览、连接管理 |
| 时序 | TDengine | 可选驱动代理 | 时序库表浏览、查询分析 |
| 时序 | Apache IoTDB | 可选驱动代理 | Storage Group / Device / Timeseries 浏览与查询 |
| 列式分析 | ClickHouse | 可选驱动代理 | 分析查询、对象浏览、SQL 执行 |
| 联邦查询 | Trino | 可选驱动代理 | 跨多数据源联邦 SQL、`catalog.schema` 浏览、SQL 执行 |
| 搜索 | Elasticsearch | 可选驱动代理 | 索引浏览、Mapping 检查、JSON DSL / query_string 查询 |
| 扩展接入 | Custom Driver/DSN | 自定义 | 通过 Driver + DSN 接入更多数据源 |
<h2 align="center">📸 项目截图</h2>
<div align="center">
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/0eefe07f-2836-44fa-9ddf-a0d2124b90e2" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/6765e539-83ea-4cd6-9c9e-f42790fa05b5" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/60e3d187-171a-4248-94e0-c6b08736e235" />
<br />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/7a478602-0f08-4b30-8f6a-879f4a60ae32" />
<img width="14%" alt="image" src="https://github.com/user-attachments/assets/6442ca7d-ce9e-46d9-aecd-405ba88f5a5e" />
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/bc17895e-02a4-4cc5-b471-c3803cf25a2b" />
</div>
</details>
---
## 核心特性
### AI 智能助手 (New)
- **多模型服务商支持**:内置跨平台接入 OpenAI, Google Gemini, Anthropic Claude同时支持任意自定义兼容 OpenAI 格式的 API。
- **关联表结构上下文**:原生支持将当前数据库表结构直接提取作为上下文发送给 AI让 SQL 生成、分析变得更精准。
- **快捷指令**:内置多种快捷对话指(如一键生成 SQL、解释执行逻辑、分析性能优化、表字段代码评审等
### 性能与交互
- 大数据场景下保持流畅交互(含 DataGrid 列宽拖拽、批量编辑流程优化)。
- 虚拟滚动渲染,降低大结果集卡顿风险。
### 数据管理DataGrid
- 单元格所见即所得编辑。
- 批量新增/修改/删除,支持事务提交与回滚。
- 大字段弹窗编辑。
- 右键上下文操作NULL、复制、导出等
- 根据查询上下文智能切换读写模式。
- 支持 CSV / XLSX / JSON / Markdown 导出。
### SQL 编辑器
- 基于 Monaco Editor。
- 上下文补全(数据库/表/字段)。
- 多标签查询工作流。
### 连接与驱动
- URI 生成与解析。
- SSH 隧道、代理支持。
- 连接配置 JSON 导入/导出。
- 可选驱动安装与启用管理。
### Redis 工具
- 自动/原始文本/UTF-8/十六进制等视图模式。
- 内置命令执行面板。
### 可观测性与更新
- SQL 执行日志(含耗时)。
- 启动/定时/手动更新检查。
### UI 体验
- Ant Design 5 体系。
- 深色/浅色主题切换。
- 灵活布局与侧边栏行为。
---
## 技术栈
- **后端**: Go 1.24 + Wails v2
- **前端**: React 18 + TypeScript + Vite
- **UI 框架**: Ant Design 5
- **状态管理**: Zustand
- **编辑器**: Monaco Editor
---
## 安装与运行
## 🚀 快速开始
### 前置要求
- [Go](https://go.dev/dl/) 1.21+
- [Node.js](https://nodejs.org/) 18+
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
- [Wails CLI](https://wails.io/docs/gettingstarted/installation)
### 开发模式
```bash
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
```
```shell
# 克隆项目
### 开发
```bash
git clone https://github.com/Syngnat/GoNavi.git
cd GoNavi
# 启动开发(热重载
wails dev
# 本地快速启动:未修改 Go 导出方法签名时使用
node tools/wails-fast-dev.mjs
# 修改 Go 导出方法签名后刷新 Wails JS 绑定
node tools/wails-fast-dev.mjs --refresh-bindings
# Windows PowerShell 低内存视觉模式:关闭透明 WebView 和 Acrylic 背景
$env:GONAVI_LOW_MEMORY_MODE="1"; node tools/wails-fast-dev.mjs
wails dev # 完整热重载
node tools/wails-fast-dev.mjs # Go 导出签名未变时更快
node tools/wails-fast-dev.mjs --refresh-bindings # 修改导出方法签名后刷新绑定
```
### 编译构建
### 构建
```bash
# 构建当前平台
wails build
# 清理后构建(发布前推荐)
wails build -clean
wails build -clean # 发布前推荐
```
构建产物位于 `build/bin`
产物位于 `build/bin`
### 跨平台发布GitHub Actions
### 只想直接用?
仓库内置发布流水线,推送 `v*` Tag 可自动构建并发布 Release。
Release 更新说明会基于已合并 Pull Request 自动生成,并按 `.github/release.yaml` 分类
支持目标:
- macOS (AMD64 / ARM64)
- Windows (AMD64)
- Linux (AMD64含 WebKitGTK 4.0 / 4.1 变体)
前往 **[Releases](https://github.com/Syngnat/GoNavi/releases)** 下载
macOS AMD64/ARM64 · Windows AMD64 · Linux WebKitGTK 4.0/4.1
---
## 常见问题
## 🌐 Web Server实验中
### macOS 提示“应用已损坏,无法打开”
同一套 Go 后端 + React 前端提供浏览器访问(**不是**把 Wails 桌面窗口容器化)。
在未进行 Apple Notarization 时Gatekeeper 可能拦截应用。
```powershell
go build .
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
```
- 首次访问进入 `/setup`(管理员密码;可选 Google Authenticator
- 桥接:`window.go.*` / `window.runtime.*` → HTTP / SSE
- Session、恢复码、登录限流
#### Docker / Podman
```bash
cp docker.web-server.env.example docker.web-server.env
# 设置 GONAVI_HOST_DATA_ROOT 为绝对路径
# 可选GONAVI_WEB_PASSWORD至少 6 位)
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
```
打开 `http://127.0.0.1:34116`。将活动数据目录挂载到 `/data`
`connections.json``daily_secrets.json`,可选 `drivers/`)。
认证状态写入 `web_auth.json`
> **不要把未加固的 Web 入口直接暴露到公网。** 生产环境请配合反向代理与 HTTPS。
健康检查:`GET /__gonavi/healthz`
本地源码构建:叠加 `-f docker-compose.web-server.local.yml --build`
修改 env 密码后需重建容器:
```bash
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
```
`docker restart` **不会**重新加载 env 文件。)
---
## 🤖 MCP & Agents
把结构工具交给 Agent而不是把密钥交出去
```bash
cp docker.mcp-server.env.example docker.mcp-server.env
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
```
| 形态 | 入口 |
|---|---|
| MCP 容器 | `docker-compose.mcp-server.yml``ghcr.io/syngnat/gonavi-mcp-server` |
| Web UI 容器 | `docker-compose.web-server.yml``ghcr.io/syngnat/gonavi-web-server` |
| Podman / Quadlet | [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server) |
| Kubernetes | [deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server) |
| Helm | [deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server) |
| 仅构建镜像 | `Dockerfile.build-env``ghcr.io/syngnat/gonavi-build-env` |
安全默认:远端 `schema-only` 不暴露 `execute_sql`;变更类 SQL 需显式 `allowMutating=true`
详见:[cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md)。
### 仅 Linux 构建环境
```bash
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
```
---
## 📦 发布流水线
推送 `v*` Tag → GitHub Actions 自动多架构构建。
Release 说明按 `.github/release.yaml` 从已合并 PR 生成。
---
## 🛠 常见问题
<details>
<summary><b>Windows缺少 Microsoft Edge WebView2 运行时(内网常见)</b></summary>
GoNavi 桌面版在 Windows 上依赖 **Microsoft Edge WebView2 Runtime**(系统级组件,不是完整 Chrome
部分内网 / 精简镜像 / Windows Server / LTSC 机器未预装,会出现:
- 双击后窗口一闪即关、白屏、空白窗口
- 提示找不到 WebView2 / WebView2 Runtime
- 杀软或组策略拦截了运行时安装
### 1. 先确认是否已安装
**PowerShell** 中执行:
```powershell
# Evergreen Runtime 常见安装目录64 位系统)
Test-Path "${env:ProgramFiles(x86)}\Microsoft\EdgeWebView\Application"
# 注册表(有显示名称通常表示已安装)
Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" `
-ErrorAction SilentlyContinue |
Select-Object pv, name
```
若路径不存在且注册表无 `pv` 版本号,需要安装运行时。
### 2. 有外网:在线安装(最简单)
1. 打开微软官方下载页:
[Microsoft Edge WebView2](https://developer.microsoft.com/microsoft-edge/webview2/)
2. 下载 **Evergreen Bootstrapper**(体积小,安装时联网拉完整运行时)
3. 右键「以管理员身份运行」安装完成后,**重新打开 GoNavi**
### 3. 内网 / 离线:独立安装包(推荐企业分发)
Bootstrapper 在完全断网环境会失败。请改用 **Evergreen Standalone Installer**
1. 在一台能上网的电脑打开同一下载页,下载对应架构的独立安装包,例如:
- `MicrosoftEdgeWebView2RuntimeInstallerX64.exe`(绝大多数 64 位 Windows
- `…X86.exe` / `…ARM64.exe`(按机器架构选择)
2. 将安装包拷贝到内网U 盘、软件中心、文件共享均可)
3. 在目标机 **管理员权限** 安装:
```powershell
# 交互安装
.\MicrosoftEdgeWebView2RuntimeInstallerX64.exe
# 静默安装(便于 IT 批量推送)
.\MicrosoftEdgeWebView2RuntimeInstallerX64.exe /silent /install
```
4. 安装完成后重新启动 GoNavi。若仍异常注销/重启一次 Windows 再试。
### 4. 组策略 / 权限受限时
- 需要本地管理员或由 IT 通过 SCCM / 软件中心推送 Standalone 安装包
- 确认未禁用 Edge / WebView2 相关更新与安装策略
- 企业可固定使用 [Fixed Version](https://developer.microsoft.com/microsoft-edge/webview2/) 运行时并由管理员统一维护(一般用户优先 Evergreen
### 5. 临时绕过Web Server 模式(仍需能跑 GoNavi 后端)
若桌面 WebView 短期无法装上,可在本机用浏览器访问实验性 Web Server**不是**把桌面窗口容器化):
```powershell
# 以发布包中的可执行文件为例
.\GoNavi.exe web-server --addr 127.0.0.1:34116
```
浏览器打开 `http://127.0.0.1:34116`。详见上文「Web Server」章节。
注意:未加固的 Web 入口不要直接暴露到公网。
更多说明与反馈入口见 Issue[#672](https://github.com/Syngnat/GoNavi/issues/672)。
</details>
<details>
<summary><b>macOS提示「应用已损坏无法打开」</b></summary>
未做 Apple Notarization 时Gatekeeper 可能拦截:
```bash
sudo xattr -rd com.apple.quarantine /Applications/GoNavi.app
```
### Linux 缺少 `libwebkit2gtk` / `libjavascriptcoregtk`
也可在 Finder 中右键打开。建议先移到「应用程序」。
</details>
<details>
<summary><b>Linux缺少 WebKitGTK</b></summary>
```bash
# Debian 13 / Ubuntu 24.04+
@@ -195,33 +428,92 @@ sudo apt-get update
sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18
```
`-WebKit41` 后缀的产物更适配 Debian 13 / Ubuntu 24.04+。
</details>
<details>
<summary><b>Linux中文显示为方框</b></summary>
```bash
sudo apt-get update
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
fc-cache -fv
```
</details>
---
## 贡献指南
## 💖 赞助商
欢迎提交 Issue 与 Pull Request。
<p align="center">
<a href="https://api-fast.hualong.online/">
<img src="assets/sponsors/hualong-mark.png" width="120" alt="华龙中转站" />
</a>
</p>
完整流程、分支模型与维护者同步规则请查看:
<p align="center">
<b><a href="https://api-fast.hualong.online/">华龙中转站 · Hualong Transfer Station</a></b><br/>
<sub>AI API Gateway · 多模型统一接入 · 国内直连高速端点</sub>
</p>
特别感谢 **[华龙中转站](https://api-fast.hualong.online/)** 赞助 GoNavi 开源开发。
| | |
|---|---|
| **他们是谁** | **AI API 中转 / Gateway**:以 OpenAI 兼容接口统一多模型接入,帮你少踩「渠道碎片化 + 网络不稳定」的坑。 |
| **核心能力** | 多模型统一路由 · OpenAI 兼容 · **国内直连高速端点** · 适合个人开发者与开源项目快速接通 LLM |
| **为什么推荐** | GoNavi 自身是 AI / MCP 一等公民产品;稳定、好用的模型通道,是写代码、做 Agent、联调多 Provider 的基础设施。 |
**需要靠谱的 LLM 中转?**
👉 [https://api-fast.hualong.online/](https://api-fast.hualong.online/) — 控制台、文档与国内高速端点。
> 华龙另向本仓库赞助了 **共享 500 亿50Btoken** 额度,供 GoNavi **共建者** 以 **API Key** 方式协作开发使用(非整库人均 50B。申请与规则见共建入口[Issue #671](https://github.com/Syngnat/GoNavi/issues/671)。
---
## 🤝 贡献
欢迎 Issue 与 PR。请从 **`dev`** 拉分支,并向 **`dev`** 提 PR。
- [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md)
- 共建邀请(含赞助 token 池说明):[Issue #671](https://github.com/Syngnat/GoNavi/issues/671)
外部贡献者应从 `dev` 拉出分支,并统一向 `dev` 发起 Pull Request。
---
## Star History (Star 增长趋势)
## 💬 联系与反馈
<a href="https://www.star-history.com/?repos=Syngnat%2FGoNavi&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&legend=top-left" />
</picture>
</a>
| 渠道 | 说明 |
|---|---|
| **GitHub Issues** | Bug、功能建议、文档问题优先在此提交便于跟踪与复现 |
| **微信** | `ygf1140302783`备注「GoNavi」— 可拉交流群,适合使用讨论与快速沟通 |
> 技术问题尽量附版本号、系统与复现步骤;内网 Windows 启动白屏/闪退见 [WebView2 说明](https://github.com/Syngnat/GoNavi/issues/672)。
---
## ⭐ Star History
<!-- star-history:start -->
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/star-history/star-history-dark.svg">
<img alt="Star history" src="assets/star-history/star-history-light.svg">
</picture>
<!-- star-history:end -->
---
## 友情链接
- [华龙中转站](https://api-fast.hualong.online/) — AI API GatewayGoNavi 赞助商)
- [linux.do](https://linux.do/)
- [AI全书](https://aibook.ren/)
## 开源协议
本项目采用 [Apache-2.0 协议](LICENSE)
[Apache-2.0](LICENSE)
<p align="center">
<sub>为泡在 SQL、表结构与 Agent 循环里的人而造。</sub>
</p>

BIN
assets/readme-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1 @@
144ec2c9ab4c0a69c453fea4e33549fdd8342757b406b614f300f9819122fd01

View File

@@ -0,0 +1 @@
1778

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -5,7 +5,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss iris mongodb tdengine clickhouse)
DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino)
DEFAULT_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64)
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
@@ -42,7 +42,9 @@ normalize_driver() {
case "$name" in
doris|diros) echo "doris" ;;
open_gauss|open-gauss) echo "opengauss" ;;
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|iris|mongodb|tdengine|clickhouse)
gaussdb|gauss_db|gauss-db) echo "gaussdb" ;;
elasticsearch|elastic) echo "elasticsearch" ;;
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino)
echo "$name"
;;
*)
@@ -144,6 +146,40 @@ PY
fi
}
zip_duckdb_windows_package() {
local bundle_stage_dir="$1"
local zip_path="$2"
rm -f "$zip_path"
if command -v python3 >/dev/null 2>&1; then
BUNDLE_STAGE_DIR="$bundle_stage_dir" BUNDLE_ZIP_PATH="$zip_path" python3 - <<'PY'
import os
import zipfile
stage_dir = os.environ["BUNDLE_STAGE_DIR"]
zip_path = os.environ["BUNDLE_ZIP_PATH"]
entries = [
("Windows/duckdb-driver-agent-windows-amd64.exe", os.path.join(stage_dir, "Windows", "duckdb-driver-agent-windows-amd64.exe")),
("Windows/duckdb.dll", os.path.join(stage_dir, "Windows", "duckdb.dll")),
]
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for arcname, src in entries:
if not os.path.isfile(src):
raise FileNotFoundError(src)
zf.write(src, arcname)
PY
elif command -v zip >/dev/null 2>&1; then
(
cd "$bundle_stage_dir"
zip -qry "$zip_path" "Windows/duckdb-driver-agent-windows-amd64.exe" "Windows/duckdb.dll"
)
else
echo "❌ 未找到 python3 或 zip无法生成 DuckDB Windows 专属驱动包。"
exit 1
fi
}
prepare_duckdb_windows_library() {
local cache_root="$1"
local lib_dir="$cache_root/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
@@ -201,6 +237,7 @@ driver_csv=""
target_platform=""
out_root="dist/driver-agents"
bundle_name="GoNavi-DriverAgents.zip"
duckdb_windows_zip_name="duckdb-driver.zip"
strict_mode="false"
upx_mode="${GONAVI_DRIVER_AGENT_UPX:-auto}"
@@ -402,6 +439,14 @@ fi
zip_bundle "$bundle_zip_path" "$bundle_stage_dir"
duckdb_asset_path="$out_root_abs/windows-amd64/duckdb-driver-agent-windows-amd64.exe"
duckdb_dll_path="$out_root_abs/windows-amd64/$DUCKDB_WINDOWS_SUPPORT_DLL"
if [[ -f "$duckdb_asset_path" && -f "$duckdb_dll_path" ]]; then
duckdb_zip_path="$out_root_abs/windows-amd64/$duckdb_windows_zip_name"
zip_duckdb_windows_package "$bundle_stage_dir" "$duckdb_zip_path"
built_assets+=("Windows/$duckdb_windows_zip_name")
fi
echo ""
echo "✅ 构建完成"
echo " 单文件输出根目录:$out_root_abs"

View File

@@ -163,7 +163,7 @@ package_macos_release() {
echo -e "${GREEN}🍎 正在构建 macOS (${platform})...${NC}"
generate_driver_agent_revisions "darwin/${platform}"
wails build -platform "darwin/${platform}" -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform "darwin/${platform}" -clean -ldflags "$LDFLAGS"
if [ $? -ne 0 ]; then
echo -e "${RED} ❌ macOS ${platform} 构建失败。${NC}"
record_build_failure "macOS ${platform}"
@@ -213,11 +213,10 @@ package_macos_release "amd64" "mac-amd64"
echo -e "${GREEN}🪟 正在构建 Windows (amd64)...${NC}"
if command -v x86_64-w64-mingw32-gcc &> /dev/null; then
generate_driver_agent_revisions "windows/amd64"
wails build -platform windows/amd64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform windows/amd64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_EXE="$DIST_DIR/${APP_NAME}-${VERSION}-windows-amd64.exe"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}.exe" "$TARGET_EXE"
try_compress_binary_with_upx "$TARGET_EXE" "Windows amd64 可执行文件"
echo " ✅ 已生成 ${APP_NAME}-${VERSION}-windows-amd64.exe"
else
echo -e "${RED} ❌ Windows amd64 构建失败。${NC}"
@@ -231,11 +230,10 @@ fi
echo -e "${GREEN}🪟 正在构建 Windows (arm64)...${NC}"
if command -v aarch64-w64-mingw32-gcc &> /dev/null; then
generate_driver_agent_revisions "windows/arm64"
wails build -platform windows/arm64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform windows/arm64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_EXE="$DIST_DIR/${APP_NAME}-${VERSION}-windows-arm64.exe"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}.exe" "$TARGET_EXE"
echo -e "${YELLOW} ⚠️ 当前 UPX 不支持 win64/arm64跳过 Windows arm64 压缩。${NC}"
echo " ✅ 已生成 ${APP_NAME}-${VERSION}-windows-arm64.exe"
else
echo -e "${RED} ❌ Windows arm64 构建失败。${NC}"
@@ -255,7 +253,7 @@ CURRENT_ARCH=$(uname -m)
if [ "$CURRENT_OS" = "Linux" ] && [ "$CURRENT_ARCH" = "x86_64" ]; then
# 本机 Linux amd64直接构建
generate_driver_agent_revisions "linux/amd64"
wails build -platform linux/amd64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform linux/amd64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_LINUX_BIN="$DIST_DIR/${APP_NAME}-${VERSION}-linux-amd64"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}" "$TARGET_LINUX_BIN"
@@ -277,7 +275,7 @@ elif command -v x86_64-linux-gnu-gcc &> /dev/null; then
export CXX=x86_64-linux-gnu-g++
export CGO_ENABLED=1
generate_driver_agent_revisions "linux/amd64"
wails build -platform linux/amd64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform linux/amd64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_LINUX_BIN="$DIST_DIR/${APP_NAME}-${VERSION}-linux-amd64"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}" "$TARGET_LINUX_BIN"
@@ -303,7 +301,7 @@ echo -e "${GREEN}🐧 正在构建 Linux (arm64)...${NC}"
if [ "$CURRENT_OS" = "Linux" ] && [ "$CURRENT_ARCH" = "aarch64" ]; then
# 本机 Linux arm64直接构建
generate_driver_agent_revisions "linux/arm64"
wails build -platform linux/arm64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform linux/arm64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_LINUX_BIN="$DIST_DIR/${APP_NAME}-${VERSION}-linux-arm64"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}" "$TARGET_LINUX_BIN"
@@ -324,7 +322,7 @@ elif command -v aarch64-linux-gnu-gcc &> /dev/null; then
export CXX=aarch64-linux-gnu-g++
export CGO_ENABLED=1
generate_driver_agent_revisions "linux/arm64"
wails build -platform linux/arm64 -clean -ldflags "$LDFLAGS"
wails build -trimpath -platform linux/arm64 -clean -ldflags "$LDFLAGS"
if [ $? -eq 0 ]; then
TARGET_LINUX_BIN="$DIST_DIR/${APP_NAME}-${VERSION}-linux-arm64"
mv "$BUILD_BIN_DIR/${DEFAULT_BINARY_NAME}" "$TARGET_LINUX_BIN"
@@ -355,6 +353,9 @@ if command -v sha256sum &> /dev/null; then
: > SHA256SUMS
for f in *; do
[ -f "$f" ] || continue
case "$f" in
SHA256SUMS|latest.json|latest-dev.json) continue ;;
esac
sha256sum "$f" >> SHA256SUMS
done
cd ..
@@ -363,6 +364,9 @@ elif command -v shasum &> /dev/null; then
: > SHA256SUMS
for f in *; do
[ -f "$f" ] || continue
case "$f" in
SHA256SUMS|latest.json|latest-dev.json) continue ;;
esac
shasum -a 256 "$f" >> SHA256SUMS
done
cd ..
@@ -370,6 +374,35 @@ else
echo -e "${YELLOW} ⚠️ 未找到 sha256sum/shasum跳过校验文件生成。${NC}"
fi
echo -e "${GREEN}📄 生成静态更新清单...${NC}"
if command -v python3 &> /dev/null; then
# 正式版latest.jsondev/test 版本latest-dev.json对应客户端 dev 通道)
case "$VERSION" in
dev-*|*-dev*|*dev*|*test*)
TAG_FOR_MANIFEST="dev-latest"
CHANNEL_FOR_MANIFEST="dev"
OUT_MANIFEST="$DIST_DIR/latest-dev.json"
;;
*)
TAG_FOR_MANIFEST="v${VERSION#v}"
CHANNEL_FOR_MANIFEST="latest"
OUT_MANIFEST="$DIST_DIR/latest.json"
;;
esac
if python3 tools/generate-update-latest-manifest.py \
--assets-dir "$DIST_DIR" \
--version "$VERSION" \
--tag "$TAG_FOR_MANIFEST" \
--channel "$CHANNEL_FOR_MANIFEST" \
--output "$OUT_MANIFEST"; then
echo -e "${GREEN} ✅ 已生成 $OUT_MANIFEST (channel=${CHANNEL_FOR_MANIFEST})${NC}"
else
echo -e "${YELLOW} ⚠️ 生成更新清单失败(不影响本地产物,正式/dev 发版由 CI 生成)${NC}"
fi
else
echo -e "${YELLOW} ⚠️ 未找到 python3跳过更新清单${NC}"
fi
echo ""
if [ "${#BUILD_FAILURES[@]}" -gt 0 ]; then
echo -e "${RED}❌ 构建未完全成功,失败平台:${BUILD_FAILURES[*]}${NC}"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 85 KiB

100
build/windows/installer.wxs Normal file
View File

@@ -0,0 +1,100 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="$(var.ProductName)"
Manufacturer="Syngnat"
Language="1033"
Version="$(var.ProductVersion)"
ProductCode="*"
UpgradeCode="$(var.UpgradeCode)"
Scope="perMachine"
InstallerVersion="500"
Compressed="yes">
<MajorUpgrade AllowDowngrades="yes"
Schedule="afterInstallInitialize" />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<Icon Id="GoNaviIcon" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="GoNaviIcon" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/Syngnat/GoNavi" />
<Property Id="INSTALLDESKTOPSHORTCUT" Value="1" Secure="yes" />
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseRtf)" />
<Feature Id="MainFeature" Title="$(var.ProductName)" Level="1">
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="DesktopShortcutComponent" />
</Feature>
<Component Id="MainExecutable"
Directory="INSTALLFOLDER"
Guid="$(var.ComponentGuid)"
Bitness="always64">
<File Id="GoNaviExe"
Source="$(var.SourceExe)"
Name="GoNavi.exe"
KeyPath="yes">
<!-- Keep shortcut icons bound to the stable executable path. Referencing the MSI Icon table
can leave Windows shell caches pointing at the removed product cache after a major upgrade. -->
<Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder"
Name="$(var.ProductName)"
WorkingDirectory="INSTALLFOLDER"
Advertise="no" />
</File>
<File Id="GoNaviMsiInstallMarker"
Source="$(var.InstallMarker)"
Name=".gonavi-msi-install" />
<File Id="GoNaviLicense"
Source="$(var.LicenseFile)"
Name="LICENSE.txt" />
<File Id="GoNaviNotice"
Source="$(var.NoticeFile)"
Name="NOTICE.txt" />
<RegistryValue Root="HKLM"
Key="Software\Syngnat\$(var.RegistryKeyName)"
Name="InstallType"
Value="MSI"
Type="string"
KeyPath="no" />
<RegistryValue Root="HKLM"
Key="Software\Syngnat\$(var.RegistryKeyName)"
Name="InstallPath"
Value="[INSTALLFOLDER]GoNavi.exe"
Type="string"
KeyPath="no" />
</Component>
<!-- Online updates pass the user's pre-update desktop shortcut state. Keeping this resource
in its own component lets a major upgrade remove an unwanted old shortcut without
recreating it, while first-time and manual installs retain the existing default. -->
<Component Id="DesktopShortcutComponent"
Directory="DesktopFolder"
Guid="$(var.DesktopShortcutComponentGuid)"
Bitness="always64"
Condition="INSTALLDESKTOPSHORTCUT = 1">
<Shortcut Id="DesktopShortcut"
Name="$(var.ProductName)"
Target="[#GoNaviExe]"
WorkingDirectory="INSTALLFOLDER"
Advertise="no" />
<RegistryValue Root="HKLM"
Key="Software\Syngnat\$(var.RegistryKeyName)"
Name="DesktopShortcutInstalled"
Value="1"
Type="integer"
KeyPath="yes" />
</Component>
<ui:WixUI Id="WixUI_InstallDir"
InstallDirectory="INSTALLFOLDER"
ExtendedPathValidation="yes" />
</Package>
<Fragment>
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="$(var.InstallFolderName)" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<StandardDirectory Id="DesktopFolder" />
</Fragment>
</Wix>

View File

@@ -0,0 +1,381 @@
# GoNavi MCP Server
`gonavi-mcp-server` 会把 GoNavi 已保存连接背后的数据库能力通过 MCP 暴露给外部客户端。本机客户端默认使用 `stdio`;云端 Agent 可使用显式开启的 Streamable HTTP 模式。
## 当前提供的 tools
- `get_connections`
- 返回 GoNavi 已保存连接的 `id/name/type/target/defaultDatabase` 等摘要信息
- `get_databases`
- 入参:`connectionId`
- `get_tables`
- 入参:`connectionId`、可选 `dbName`
- 返回表列表,并在 `views` 字段附带视图列表,兼容旧客户端只调用 `get_tables` 的场景
- `get_views`
- 入参:`connectionId`、可选 `dbName`
- 返回视图列表
- `get_objects`
- 入参:`connectionId`、可选 `dbName`、可选 `objectTypes`
- 返回表、视图、触发器、函数、过程、序列、包、事件,以及消息队列类 `topic/queue/exchange` 等对象清单
- `get_columns`
- 入参:`connectionId`、可选 `dbName``tableName`
- `get_table_ddl`
- 入参:`connectionId`、可选 `dbName``tableName`
- `execute_sql`
- 入参:`connectionId`、可选 `dbName``sql`
- 默认只允许只读 SQL
- 如果 SQL 包含 DDL/DML必须显式传 `allowMutating=true`
- `maxRowsPerResult` 用来限制单个结果集返回的行数,默认 `200`
远程 Agent 只需要结构元数据时,启动 HTTP 模式请加 `--schema-only`。该模式不注册 `execute_sql`,只保留连接摘要、对象清单、表/视图、字段、索引、外键、触发器和 DDL 工具。
## 运行方式
开发态直接运行:
```powershell
go run ./cmd/gonavi-mcp-server
```
显式运行本机 `stdio`
```powershell
go run ./cmd/gonavi-mcp-server stdio
```
也可以先编译:
```powershell
go build -o .\bin\gonavi-mcp-server.exe .\cmd\gonavi-mcp-server
```
## Docker / Podman / Compose
当前容器化支持仅覆盖 `gonavi-mcp-server`,不包含 Wails 桌面 GUI。
当前支持矩阵:
- Docker Desktop / Linux 服务器 / NAS直接使用 Compose 或 `docker run`
- Podman / Quadlet使用 `deploy/podman/gonavi-mcp-server`
- Kubernetes使用 `deploy/k8s/gonavi-mcp-server`
- Helm使用 `deploy/helm/gonavi-mcp-server`
- 仅构建环境:使用仓库根目录 `Dockerfile.build-env`
- 桌面 GUI 浏览器访问版:当前不提供,此仓库主应用仍是 Wails 桌面程序,不是现成的 Web 服务
仓库根目录已提供以下文件:
- `Dockerfile.mcp-server`
- `docker-compose.mcp-server.yml`
- `docker.mcp-server.env.example`
- `deploy/podman/gonavi-mcp-server/*`
推荐流程:
```bash
cp docker.mcp-server.env.example docker.mcp-server.env
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
```
默认 Compose 会拉取 GHCR 预构建镜像。如果你要基于当前工作区源码本地构建,再叠加:
```bash
docker compose --env-file docker.mcp-server.env \
-f docker-compose.mcp-server.yml \
-f docker-compose.mcp-server.local.yml \
up -d --build
```
其中 `GONAVI_HOST_DATA_ROOT` 必须指向 GoNavi 当前活动数据目录。该目录内至少应包含:
- `connections.json`
- `daily_secrets.json`
- `drivers/`(如果目标连接依赖可选 driver agent
容器内默认会设置:
- `GONAVI_DATA_ROOT=/data`
- `GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765`
- `GONAVI_MCP_HTTP_PATH=/mcp`
`GONAVI_DATA_ROOT` 会覆盖默认活动数据目录解析逻辑,避免宿主机路径与容器内路径不一致时依赖 `storage_root.json` 的绝对路径。
如果你只想手动构建镜像:
```bash
docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local .
docker run --rm -p 8765:8765 \
-e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \
-e GONAVI_MCP_SCHEMA_ONLY=true \
-e GONAVI_DATA_ROOT=/data \
-v /absolute/path/to/gonavi-data:/data \
gonavi-mcp-server:local http
```
如果你直接使用已发布镜像:
```bash
docker run --rm -p 8765:8765 \
-e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \
-e GONAVI_MCP_SCHEMA_ONLY=true \
-e GONAVI_DATA_ROOT=/data \
-v /absolute/path/to/gonavi-data:/data \
ghcr.io/syngnat/gonavi-mcp-server:latest http
```
### Podman
仓库内还提供了 Podman 原生部署样例:
- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example`
- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container`
- `deploy/podman/gonavi-mcp-server/README.md`
直接运行已发布镜像:
```bash
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env
podman run -d --name gonavi-mcp-server --replace \
-p 8765:8765 \
--env-file ./gonavi-mcp-server.env \
-v /absolute/path/to/gonavi-data:/data:Z \
ghcr.io/syngnat/gonavi-mcp-server:latest http
```
如果你要基于当前源码本地构建:
```bash
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
podman run -d --name gonavi-mcp-server --replace \
-p 8765:8765 \
--env-file ./gonavi-mcp-server.env \
-v /absolute/path/to/gonavi-data:/data:Z \
localhost/gonavi-mcp-server:local http
```
其中:
- `gonavi-mcp-server.env``deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example` 初始化
- `:Z` 适用于开启 SELinux 的宿主机;未启用 SELinux 可去掉
- 更适合长期运行的方式见 [deploy/podman/gonavi-mcp-server/README.md](../../deploy/podman/gonavi-mcp-server/README.md) 中的 Quadlet 示例
`podman compose` 本身依赖外部 compose provider所以仓库对 Podman 的主支持路径是 `podman run` 与 Quadlet而不是假设所有环境都能直接复用 Compose。
## Kubernetes
仓库内置了最小 K8s 示例:
- `deploy/k8s/gonavi-mcp-server/kustomization.yaml`
- `deploy/k8s/gonavi-mcp-server/base/deployment.yaml`
- `deploy/k8s/gonavi-mcp-server/base/service.yaml`
- `deploy/k8s/gonavi-mcp-server/README.md`
- `deploy/k8s/gonavi-mcp-server/overlays/*`
推荐先从现有 GoNavi 数据目录生成 Secret
```bash
kubectl create namespace gonavi
kubectl -n gonavi create secret generic gonavi-mcp-server-data \
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
kubectl apply -k deploy/k8s/gonavi-mcp-server
```
如果需要 NAS hostPath、可选 driver agent PVC、Ingress或两者组合可直接使用 `overlays/nas-hostpath``overlays/drivers-pvc``overlays/ingress``overlays/ingress-with-drivers-pvc`
更完整的说明见 [deploy/k8s/gonavi-mcp-server/README.md](../../deploy/k8s/gonavi-mcp-server/README.md)。
## Helm
如果你希望把镜像、Secret、Ingress、hostPath / PVC 挂载做成参数化部署,而不是维护多份 Kustomize overlay可直接使用
- `deploy/helm/gonavi-mcp-server`
快速安装:
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace
```
Chart 详细说明见 [deploy/helm/gonavi-mcp-server/README.md](../../deploy/helm/gonavi-mcp-server/README.md)。
## Docker / Podman Build Environment
如果你的目标不是运行 MCP而是给 Linux 服务器 / NAS / CI 准备一套可重复的 Wails 构建环境,可直接使用仓库根目录的 `Dockerfile.build-env`
```bash
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
```
如果你使用 Podman也可以直接执行
```bash
podman build -f Dockerfile.build-env -t localhost/gonavi-build-env:local .
podman run --rm -it -v "$PWD:/workspace" -w /workspace localhost/gonavi-build-env:local bash
```
镜像内已预装 Go、Node、Wails CLI、GTK3 与 WebKitGTK 开发依赖,适合执行:
```bash
wails build
```
这个镜像默认安装 WebKitGTK 4.0 构建依赖,适合作为通用 Linux / NAS 构建环境。镜像基座支持多架构,`amd64` / `arm64` 会跟随容器平台。
预构建镜像会发布到 GHCR
- `ghcr.io/syngnat/gonavi-mcp-server:latest`
- `ghcr.io/syngnat/gonavi-build-env:latest`
它只负责构建 Linux 产物,不会把 Wails 主程序变成浏览器版服务。
远程 Agent 使用 Streamable HTTP 时必须设置 bearer token
```powershell
$env:GONAVI_MCP_HTTP_TOKEN = "<随机token>"
go run ./cmd/gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --schema-only
```
安装包主程序也支持同样模式:
```powershell
& "C:\Program Files\GoNavi\GoNavi.exe" mcp-server http --addr 127.0.0.1:8765 --path /mcp --token "<随机token>" --schema-only
```
默认建议只监听 `127.0.0.1`,再通过 SSH 隧道、反向代理或内网网关暴露给云端 Agent。不要在没有 TLS、防火墙和鉴权的情况下直接监听公网地址。
无图形界面或需要把配置交给云端 Agent 时,可直接生成 OpenClaw / Hermans 等远程 MCP 配置:
```powershell
& "C:\Program Files\GoNavi\GoNavi.exe" mcp-server remote-config --client openclaw --url "https://<你的域名或隧道地址>/mcp" --token "<随机token>" --schema-only
```
独立 server 开发态也支持同样能力:
```powershell
go run ./cmd/gonavi-mcp-server remote-config --client hermans --url "https://<你的域名或隧道地址>/mcp" --token "<随机token>" --schema-only
```
## Claude Code / Codex / OpenClaw / Hermans
正式安装包场景,推荐直接在 GoNavi 里使用“AI 设置 -> MCP 服务 -> 安装到 Claude Code / 安装到 Codex”。
它会自动把当前安装的 `GoNavi.exe` 写入 Claude Code 的用户级 `~/.claude.json`,命令形态类似:
```json
{
"mcpServers": {
"gonavi": {
"type": "stdio",
"command": "C:\\Program Files\\GoNavi\\GoNavi.exe",
"args": ["mcp-server"],
"env": {}
}
}
}
```
这样用户不需要自己找本机 `gonavi-mcp-server.exe` 路径,安装包本体就能直接作为 MCP 入口。
Codex 当前使用 `~/.codex/config.toml`GoNavi 会写入类似下面这段:
```toml
[mcp_servers.gonavi]
command = 'C:\Program Files\GoNavi\GoNavi.exe'
args = ['mcp-server']
startup_timeout_sec = 60
```
仓库开发态如果要在本机 `Claude Code CLI` 里稳定使用这个 MCP仍然推荐走仓库内包装脚本
```powershell
.\tools\claude-gonavi-mcp.ps1 -p "必须调用 gonavi MCP 的 get_connections 工具"
```
或者:
```cmd
tools\claude-gonavi-mcp.cmd -p "必须调用 gonavi MCP 的 get_connections 工具"
```
这个脚本会先构建 `bin\gonavi-mcp-server.exe`,再通过 `--mcp-config``--strict-mcp-config` 把 GoNavi MCP 单独注入当前 Claude 会话,避免默认混合 MCP 加载时序导致的首轮工具未挂载问题。
OpenClaw、Hermans 这类部署在云端或远端 Linux 的 Agent不能直接使用 Windows 本机的 `stdio` 命令。GoNavi 的连接信息和数据库密码仍应留在 Windows 本机,由 GoNavi MCP 读取保存连接和系统凭据;远端 Agent 只拿到 MCP tools 和 `connectionId`
推荐接入形态:
1. Windows 本机运行 GoNavi并保持能访问已保存的数据库连接。
2. 在 Windows 本机启动 `GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token> --schema-only`
3. 通过 SSH 隧道、反向代理或内网网关把 `http://127.0.0.1:8765/mcp` 暴露为云端 Agent 可访问的 HTTPS 地址。
4. 在 OpenClaw / Hermans 中添加远程 MCP Servertransport 选择 Streamable HTTPURL 指向 `/mcp` 地址,并设置请求头 `Authorization: Bearer <随机token>`
5. 先调用 `get_connections` 获取 `connectionId`,再调用 `get_databases``get_objects``get_tables``get_views``get_columns``get_table_ddl` 等工具读取结构。
如果目标 Agent 支持 `mcpServers` JSON可按下面的通用片段配置
```json
{
"mcpServers": {
"gonavi": {
"type": "streamable-http",
"url": "https://<你的域名或隧道地址>/mcp",
"headers": {
"Authorization": "Bearer <随机token>"
}
}
}
}
```
不要把数据库 `host/user/password` 写入云端 Agent 的配置文件。默认 `--schema-only` 不暴露 `execute_sql`;如果你明确需要远程执行 SQL可以去掉该参数此时 `execute_sql` 仍受 GoNavi AI 安全设置控制,写操作必须显式传 `allowMutating=true`
## MCP 客户端配置示例
开发态:
```json
{
"mcpServers": {
"gonavi": {
"command": "go",
"args": ["run", "./cmd/gonavi-mcp-server"]
}
}
}
```
Windows 独立 server 编译产物(开发态):
```json
{
"mcpServers": {
"gonavi": {
"command": "D:\\Work\\CodeRepos\\GoNavi\\bin\\gonavi-mcp-server.exe",
"args": []
}
}
}
```
Windows 已安装 GoNavi推荐给最终用户
```json
{
"mcpServers": {
"gonavi": {
"type": "stdio",
"command": "C:\\Program Files\\GoNavi\\GoNavi.exe",
"args": ["mcp-server"],
"env": {}
}
}
}
```
## 使用说明
- 先调用 `get_connections`,拿到 `connectionId`
- 之后所有数据库工具都只传 `connectionId`,由 GoNavi 服务端内部解析保存连接和密钥
- 如果 `dbName` 为空,会优先使用该保存连接里的默认数据库
- Server 会读取 GoNavi 当前活动数据目录里的连接配置,并通过系统 keyring/凭据管理器解析密文
- 如果本机凭据存储不可用,依赖密钥的连接会返回对应错误

View File

@@ -0,0 +1,42 @@
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"GoNavi-Wails/internal/mcpserver"
)
func main() {
ctx := context.Background()
err := run(ctx, os.Args[1:])
if err != nil {
log.Printf("GoNavi MCP Server 退出: %v", err)
}
}
func run(ctx context.Context, args []string) error {
if len(args) == 0 {
return mcpserver.RunAppStdioServer(ctx)
}
mode := strings.ToLower(strings.TrimSpace(args[0]))
switch mode {
case "stdio", "--stdio":
return mcpserver.RunAppStdioServer(ctx)
case "http", "--http", "streamable-http", "--streamable-http":
options, err := mcpserver.ParseHTTPServerOptions(args[1:])
if err != nil {
return err
}
log.Printf("GoNavi MCP Streamable HTTP Server 启动addr=%s path=%s schemaOnly=%v", options.Addr, options.Path, options.SchemaOnly)
return mcpserver.RunAppStreamableHTTPServer(ctx, options)
case "remote-config", "--remote-config":
return mcpserver.WriteRemoteMCPClientConfig(os.Stdout, args[1:])
default:
return fmt.Errorf("未知 MCP server 模式: %s支持 stdio/http/remote-config", args[0])
}
}

View File

@@ -0,0 +1,27 @@
package main
import (
"flag"
"fmt"
"os"
"GoNavi-Wails/internal/buildutil"
)
func main() {
var (
dllPath string
dlltoolPath string
outputLib string
)
flag.StringVar(&dllPath, "dll", "", "Path to the source DLL")
flag.StringVar(&dlltoolPath, "dlltool", "", "Optional path to dlltool executable")
flag.StringVar(&outputLib, "output-lib", "", "Output import library path")
flag.Parse()
if err := buildutil.GenerateWindowsImportLibraryFromDLL(dllPath, dlltoolPath, outputLib); err != nil {
fmt.Fprintf(os.Stderr, "generate mingw import library failed: %v\n", err)
os.Exit(1)
}
}

View File

@@ -7,7 +7,11 @@ import (
"fmt"
"os"
"reflect"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync/atomic"
"time"
"GoNavi-Wails/internal/connection"
@@ -17,6 +21,7 @@ import (
type agentRequest struct {
ID int64 `json:"id"`
Method string `json:"method"`
SessionID string `json:"sessionId,omitempty"`
Config *connection.ConnectionConfig `json:"config,omitempty"`
Query string `json:"query,omitempty"`
TimeoutMs int64 `json:"timeoutMs,omitempty"`
@@ -31,46 +36,99 @@ type agentResponse struct {
Error string `json:"error,omitempty"`
Data interface{} `json:"data,omitempty"`
Fields []string `json:"fields,omitempty"`
Messages []string `json:"messages,omitempty"`
ChunkType string `json:"chunkType,omitempty"`
RowsAffected int64 `json:"rowsAffected,omitempty"`
}
const (
agentMethodConnect = "connect"
agentMethodClose = "close"
agentMethodMetadata = "metadata"
agentMethodPing = "ping"
agentMethodQuery = "query"
agentMethodExec = "exec"
agentMethodGetDatabases = "getDatabases"
agentMethodGetTables = "getTables"
agentMethodGetCreateStmt = "getCreateStatement"
agentMethodGetColumns = "getColumns"
agentMethodGetAllColumns = "getAllColumns"
agentMethodGetIndexes = "getIndexes"
agentMethodGetForeignKey = "getForeignKeys"
agentMethodGetTriggers = "getTriggers"
agentMethodApplyChanges = "applyChanges"
agentMethodConnect = "connect"
agentMethodClose = "close"
agentMethodMetadata = "metadata"
agentMethodPing = "ping"
agentMethodOpenSession = "openSession"
agentMethodCloseSession = "closeSession"
agentMethodOpenTransaction = "openTransaction"
agentMethodCommitTransaction = "commitTransaction"
agentMethodRollbackTransaction = "rollbackTransaction"
agentMethodQuery = "query"
agentMethodQueryMulti = "queryMulti"
agentMethodStreamQuery = "streamQuery"
agentMethodExec = "exec"
agentMethodGetDatabases = "getDatabases"
agentMethodGetTables = "getTables"
agentMethodGetCreateStmt = "getCreateStatement"
agentMethodGetColumns = "getColumns"
agentMethodGetAllColumns = "getAllColumns"
agentMethodGetIndexes = "getIndexes"
agentMethodGetForeignKey = "getForeignKeys"
agentMethodGetTriggers = "getTriggers"
agentMethodApplyChanges = "applyChanges"
)
const legacyClickHouseDefaultTimeout = 2 * time.Hour
var (
agentDriverType string
agentDatabaseFactory func() db.Database
const (
agentChunkColumns = "columns"
agentChunkRows = "rows"
agentChunkDone = "done"
// agentStreamBatchSize 控制 driver-agent 向主进程发送 row chunk 的批次大小。
// 调小到 64单批 JSON 编码 + 主进程解码的瞬时内存峰值降为原来的 1/4
// 代价是 IPC 次数变为 4 倍,但每批仅一次 stdin/stdout 行读写,整体影响可忽略。
// 重要:减小批次不能根除内存峰值,仍需配合 SetGCPercent + 周期 GC见 main
agentStreamBatchSize = 64
agentMemoryTrimRowsThreshold = 100000
agentMemoryTrimMinInterval = 3 * time.Second
)
var (
agentDriverType string
agentDatabaseFactory func() db.Database
agentMemoryTrimRunning atomic.Bool
agentMemoryTrimLastAt atomic.Int64
runAgentMemoryTrimAsync = func(fn func()) {
go fn()
}
agentMemoryTrimFn = func() {
runtime.GC()
debug.FreeOSMemory()
}
)
type agentRuntime struct {
inst db.Database
sessions map[string]db.StatementExecer
nextSessionID int64
}
func main() {
if agentDatabaseFactory == nil || strings.TrimSpace(agentDriverType) == "" {
fmt.Fprintf(os.Stderr, "未配置驱动代理 provider请使用 gonavi_<driver>_driver 标签构建\n")
os.Exit(2)
}
// driver-agent 是独立进程,主进程无法控制其 GC 行为。
// 大结果集88W+ 行)通过 JSON-lines 跨进程传输时,每行有 5-8 倍内存副本;
// Go 默认 GOGC=100 + Windows MADV_FREE 不归还 RSS会导致 driver-agent 进程
// 内存峰值达到数据总量的 10+ 倍(用户实测 88W 普通业务表撑到 8G+)。
//
// GC 策略组合:
// - SetGCPercent(50):堆增长 50% 即触发 GC比默认 100 更早收敛
// - InitMemorySoftLimit起始 2GB运行时由 MaybeGrowMemoryLimit 自适应抬升到最多 8GB
// (起步保守 + 按需扩张,避免静态 2GB 限制在大表场景触发 GC 硬模式降速 15-25%
//
// 代价CPU 开销增加约 5-10%。导出场景是 I/O 密集型,可忽略。
debug.SetGCPercent(50)
db.InitMemorySoftLimit(db.MemorySoftLimitInitialBytes)
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 0, 16<<10), 8<<20)
writer := bufio.NewWriter(os.Stdout)
defer writer.Flush()
var inst db.Database
runtimeState := &agentRuntime{
sessions: make(map[string]db.StatementExecer),
}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
@@ -87,23 +145,32 @@ func main() {
continue
}
resp := handleRequest(&inst, req)
if strings.TrimSpace(req.Method) == agentMethodStreamQuery {
if err := handleStreamRequest(runtimeState, req, writer); err != nil {
fmt.Fprintf(os.Stderr, "写入流式响应失败:%v\n", err)
break
}
continue
}
resp := handleRequest(runtimeState, req)
if err := writeResponse(writer, resp); err != nil {
fmt.Fprintf(os.Stderr, "写入响应失败:%v\n", err)
break
}
if strings.TrimSpace(req.Method) == agentMethodQuery {
maybeReleaseAgentMemory("query-response", countAgentResponseRows(resp.Data))
}
}
if inst != nil {
_ = inst.Close()
}
runtimeState.close()
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "读取请求失败:%v\n", err)
}
}
func handleRequest(inst *db.Database, req agentRequest) agentResponse {
func handleRequest(runtimeState *agentRuntime, req agentRequest) agentResponse {
resp := agentResponse{ID: req.ID, Success: true}
method := strings.TrimSpace(req.Method)
@@ -112,9 +179,7 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
if req.Config == nil {
return fail(resp, "连接配置为空")
}
if *inst != nil {
_ = (*inst).Close()
}
runtimeState.close()
next := agentDatabaseFactory()
if next == nil {
return fail(resp, "驱动代理初始化失败")
@@ -122,14 +187,13 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
if err := next.Connect(*req.Config); err != nil {
return fail(resp, err.Error())
}
*inst = next
runtimeState.inst = next
return resp
case agentMethodClose:
if *inst != nil {
if err := (*inst).Close(); err != nil {
if runtimeState.inst != nil {
if err := runtimeState.close(); err != nil {
return fail(resp, err.Error())
}
*inst = nil
}
return resp
case agentMethodMetadata:
@@ -139,74 +203,177 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
"protocolSchema": "json-lines-v1",
}
return resp
case agentMethodOpenSession:
if runtimeState.inst == nil {
return fail(resp, "connection not open")
}
provider, ok := runtimeState.inst.(db.SessionExecerProvider)
if !ok {
return fail(resp, fmt.Sprintf("当前数据源(%s不支持 SQL 编辑器托管事务", strings.TrimSpace(agentDriverType)))
}
openCtx := context.Background()
var cancel context.CancelFunc
if req.TimeoutMs > 0 {
openCtx, cancel = context.WithTimeout(context.Background(), time.Duration(req.TimeoutMs)*time.Millisecond)
defer cancel()
}
session, err := provider.OpenSessionExecer(openCtx)
if err != nil {
return fail(resp, err.Error())
}
sessionID := runtimeState.nextID()
runtimeState.sessions[sessionID] = session
resp.Data = sessionID
return resp
case agentMethodOpenTransaction:
if runtimeState.inst == nil {
return fail(resp, "connection not open")
}
provider, ok := runtimeState.inst.(db.TransactionExecerProvider)
if !ok {
return fail(resp, fmt.Sprintf("当前数据源(%s不支持 SQL 编辑器托管事务", strings.TrimSpace(agentDriverType)))
}
// The transaction must outlive this request and be finished by a later RPC.
transaction, err := provider.OpenTransactionExecer(context.Background())
if err != nil {
return fail(resp, err.Error())
}
sessionID := runtimeState.nextID()
runtimeState.sessions[sessionID] = transaction
resp.Data = sessionID
return resp
case agentMethodCloseSession:
if err := runtimeState.closeSession(req.SessionID); err != nil {
return fail(resp, err.Error())
}
return resp
}
if *inst == nil {
if runtimeState.inst == nil {
return fail(resp, "connection not open")
}
if session, ok, err := runtimeState.session(req.SessionID); err != nil {
return fail(resp, err.Error())
} else if ok {
switch method {
case agentMethodQuery:
data, fields, messages, err := queryStatementWithMessagesOptionalTimeout(session, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
resp.Fields = fields
resp.Messages = messages
case agentMethodQueryMulti:
data, messages, supported, err := queryMultiStatementWithMessagesOptionalTimeout(session, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
if !supported {
return fail(resp, "当前事务会话不支持多结果集查询")
}
resp.Data = data
resp.Messages = messages
case agentMethodExec:
affected, err := execStatementWithOptionalTimeout(session, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
resp.RowsAffected = affected
case agentMethodCommitTransaction, agentMethodRollbackTransaction:
transaction, ok := session.(db.TransactionExecer)
if !ok {
return fail(resp, "当前会话不是托管事务")
}
var err error
if method == agentMethodCommitTransaction {
err = transaction.Commit()
} else {
err = transaction.Rollback()
}
if err != nil {
return fail(resp, err.Error())
}
default:
return fail(resp, "当前事务会话不支持该方法")
}
return resp
}
switch method {
case agentMethodPing:
if err := (*inst).Ping(); err != nil {
if err := runtimeState.inst.Ping(); err != nil {
return fail(resp, err.Error())
}
case agentMethodQuery:
data, fields, err := queryWithOptionalTimeout(*inst, req.Query, req.TimeoutMs)
data, fields, messages, err := queryWithMessagesOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
resp.Fields = fields
resp.Messages = messages
case agentMethodQueryMulti:
data, messages, supported, err := queryMultiWithMessagesOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
if !supported {
return fail(resp, "当前驱动不支持原生多结果集查询")
}
resp.Data = data
resp.Messages = messages
case agentMethodExec:
affected, err := execWithOptionalTimeout(*inst, req.Query, req.TimeoutMs)
affected, err := execWithOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
if err != nil {
return fail(resp, err.Error())
}
resp.RowsAffected = affected
case agentMethodGetDatabases:
data, err := (*inst).GetDatabases()
data, err := runtimeState.inst.GetDatabases()
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetTables:
data, err := (*inst).GetTables(req.DBName)
data, err := runtimeState.inst.GetTables(req.DBName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetCreateStmt:
data, err := (*inst).GetCreateStatement(req.DBName, req.TableName)
data, err := runtimeState.inst.GetCreateStatement(req.DBName, req.TableName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetColumns:
data, err := (*inst).GetColumns(req.DBName, req.TableName)
data, err := runtimeState.inst.GetColumns(req.DBName, req.TableName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetAllColumns:
data, err := (*inst).GetAllColumns(req.DBName)
data, err := runtimeState.inst.GetAllColumns(req.DBName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetIndexes:
data, err := (*inst).GetIndexes(req.DBName, req.TableName)
data, err := runtimeState.inst.GetIndexes(req.DBName, req.TableName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetForeignKey:
data, err := (*inst).GetForeignKeys(req.DBName, req.TableName)
data, err := runtimeState.inst.GetForeignKeys(req.DBName, req.TableName)
if err != nil {
return fail(resp, err.Error())
}
resp.Data = data
case agentMethodGetTriggers:
data, err := (*inst).GetTriggers(req.DBName, req.TableName)
data, err := runtimeState.inst.GetTriggers(req.DBName, req.TableName)
if err != nil {
return fail(resp, err.Error())
}
@@ -215,7 +382,7 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
if req.Changes == nil {
return fail(resp, "变更集为空")
}
applier, ok := (*inst).(interface {
applier, ok := runtimeState.inst.(interface {
ApplyChanges(tableName string, changes connection.ChangeSet) error
})
if !ok {
@@ -231,6 +398,169 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
return resp
}
type agentStreamResponseWriter struct {
writer *bufio.Writer
requestID int64
columns []string
rows [][]interface{}
rowCount int64
}
func newAgentStreamResponseWriter(writer *bufio.Writer, requestID int64) *agentStreamResponseWriter {
return &agentStreamResponseWriter{
writer: writer,
requestID: requestID,
}
}
func (w *agentStreamResponseWriter) SetColumns(columns []string) error {
w.columns = append([]string(nil), columns...)
return writeResponse(w.writer, agentResponse{
ID: w.requestID,
Success: true,
ChunkType: agentChunkColumns,
Fields: w.columns,
})
}
func (w *agentStreamResponseWriter) ConsumeRow(row map[string]interface{}) error {
if len(w.columns) == 0 {
return fmt.Errorf("流式查询缺少列定义")
}
values := make([]interface{}, len(w.columns))
for idx, column := range w.columns {
values[idx] = row[column]
}
return w.ConsumeRowValues(values)
}
func (w *agentStreamResponseWriter) ConsumeRowValues(values []interface{}) error {
row := append([]interface{}(nil), values...)
w.rows = append(w.rows, row)
w.rowCount++
if len(w.rows) < agentStreamBatchSize {
return nil
}
return w.flushRows()
}
func (w *agentStreamResponseWriter) flushRows() error {
if len(w.rows) == 0 {
return nil
}
rows := w.rows
w.rows = nil
return writeResponse(w.writer, agentResponse{
ID: w.requestID,
Success: true,
ChunkType: agentChunkRows,
Data: rows,
})
}
func (w *agentStreamResponseWriter) finish() error {
return w.flushRows()
}
func handleStreamRequest(runtimeState *agentRuntime, req agentRequest, writer *bufio.Writer) error {
resp := agentResponse{ID: req.ID, Success: true}
if runtimeState.inst == nil {
return writeResponse(writer, fail(resp, "connection not open"))
}
streamWriter := newAgentStreamResponseWriter(writer, req.ID)
if session, ok, err := runtimeState.session(req.SessionID); err != nil {
return writeResponse(writer, fail(resp, err.Error()))
} else if ok {
if err := streamStatementWithOptionalTimeout(session, req.Query, req.TimeoutMs, streamWriter); err != nil {
_ = streamWriter.finish()
return writeResponse(writer, fail(resp, err.Error()))
}
if err := streamWriter.finish(); err != nil {
return err
}
if err := writeResponse(writer, agentResponse{ID: req.ID, Success: true, ChunkType: agentChunkDone}); err != nil {
return err
}
maybeReleaseAgentMemory("stream-query-session", streamWriter.rowCount)
return nil
}
if err := streamDatabaseWithOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs, streamWriter); err != nil {
_ = streamWriter.finish()
return writeResponse(writer, fail(resp, err.Error()))
}
if err := streamWriter.finish(); err != nil {
return err
}
if err := writeResponse(writer, agentResponse{ID: req.ID, Success: true, ChunkType: agentChunkDone}); err != nil {
return err
}
maybeReleaseAgentMemory("stream-query-db", streamWriter.rowCount)
return nil
}
func (r *agentRuntime) nextID() string {
r.ensureSessionMap()
r.nextSessionID++
return "session-" + strconv.FormatInt(r.nextSessionID, 10)
}
func (r *agentRuntime) session(sessionID string) (db.StatementExecer, bool, error) {
r.ensureSessionMap()
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return nil, false, nil
}
session, ok := r.sessions[sessionID]
if !ok || session == nil {
return nil, false, fmt.Errorf("事务会话不存在或已结束")
}
return session, true, nil
}
func (r *agentRuntime) closeSession(sessionID string) error {
r.ensureSessionMap()
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return fmt.Errorf("事务会话 ID 不能为空")
}
session, ok := r.sessions[sessionID]
if ok {
delete(r.sessions, sessionID)
}
if !ok || session == nil {
return fmt.Errorf("事务会话不存在或已结束")
}
return session.Close()
}
func (r *agentRuntime) close() error {
var closeErr error
r.ensureSessionMap()
for sessionID, session := range r.sessions {
delete(r.sessions, sessionID)
if session != nil {
if err := session.Close(); err != nil && closeErr == nil {
closeErr = err
}
}
}
if r.inst != nil {
if err := r.inst.Close(); err != nil && closeErr == nil {
closeErr = err
}
r.inst = nil
}
return closeErr
}
func (r *agentRuntime) ensureSessionMap() {
if r.sessions == nil {
r.sessions = make(map[string]db.StatementExecer)
}
}
func writeResponse(writer *bufio.Writer, resp agentResponse) error {
// 对响应数据做统一 JSON 安全归一化:
// 将 map[any]any如 duckdb.Map递归转换为 map[string]any避免序列化失败导致代理进程退出。
@@ -301,25 +631,234 @@ func normalizeAgentResponseData(v interface{}) interface{} {
}
}
func queryWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
type agentQueryRunner interface {
Query(string) ([]map[string]interface{}, []string, error)
}
type agentQueryContextRunner interface {
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
}
type agentQueryMessageRunner interface {
QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error)
}
type agentQueryMessageContextRunner interface {
QueryContextWithMessages(context.Context, string) ([]map[string]interface{}, []string, []string, error)
}
type agentMultiResultMessageRunner interface {
QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error)
}
type agentMultiResultMessageContextRunner interface {
QueryMultiContextWithMessages(context.Context, string) ([]connection.ResultSetData, []string, error)
}
type agentMultiResultRunner interface {
QueryMulti(query string) ([]connection.ResultSetData, error)
}
type agentMultiResultContextRunner interface {
QueryMultiContext(context.Context, string) ([]connection.ResultSetData, error)
}
type agentExecRunner interface {
Exec(string) (int64, error)
}
type agentExecContextRunner interface {
ExecContext(context.Context, string) (int64, error)
}
func queryWithMessagesOptionalTimeout(inst agentQueryRunner, query string, timeoutMs int64) ([]map[string]interface{}, []string, []string, error) {
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
}
if effectiveTimeoutMs <= 0 {
return inst.Query(query)
if q, ok := inst.(agentQueryMessageRunner); ok {
return q.QueryWithMessages(query)
}
data, fields, err := inst.Query(query)
return data, fields, nil, err
}
if q, ok := inst.(interface {
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
}); ok {
if q, ok := inst.(agentQueryMessageContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
return q.QueryContext(ctx, query)
return q.QueryContextWithMessages(ctx, query)
}
return inst.Query(query)
if q, ok := inst.(agentQueryContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
data, fields, err := q.QueryContext(ctx, query)
return data, fields, nil, err
}
if q, ok := inst.(agentQueryMessageRunner); ok {
return q.QueryWithMessages(query)
}
data, fields, err := inst.Query(query)
return data, fields, nil, err
}
func execWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) (int64, error) {
func queryWithOptionalTimeout(inst agentQueryRunner, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
data, fields, _, err := queryWithMessagesOptionalTimeout(inst, query, timeoutMs)
return data, fields, err
}
func queryStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
queryRunner, ok := inst.(agentQueryRunner)
if !ok {
return nil, nil, fmt.Errorf("当前事务会话不支持查询语句")
}
return queryWithOptionalTimeout(queryRunner, query, timeoutMs)
}
func queryStatementWithMessagesOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]map[string]interface{}, []string, []string, error) {
queryRunner, ok := inst.(agentQueryRunner)
if !ok {
return nil, nil, nil, fmt.Errorf("当前事务会话不支持查询语句")
}
return queryWithMessagesOptionalTimeout(queryRunner, query, timeoutMs)
}
func queryMultiWithMessagesOptionalTimeout(inst db.Database, query string, timeoutMs int64) ([]connection.ResultSetData, []string, bool, error) {
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
}
if effectiveTimeoutMs > 0 {
if q, ok := inst.(agentMultiResultMessageContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
data, messages, err := q.QueryMultiContextWithMessages(ctx, query)
return data, messages, true, err
}
if q, ok := inst.(agentMultiResultContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
data, err := q.QueryMultiContext(ctx, query)
return data, nil, true, err
}
}
if q, ok := inst.(agentMultiResultMessageRunner); ok {
data, messages, err := q.QueryMultiWithMessages(query)
return data, messages, true, err
}
if q, ok := inst.(agentMultiResultRunner); ok {
data, err := q.QueryMulti(query)
return data, nil, true, err
}
return nil, nil, false, nil
}
func queryMultiStatementWithMessagesOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]connection.ResultSetData, []string, bool, error) {
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
}
if effectiveTimeoutMs > 0 {
if q, ok := inst.(agentMultiResultMessageContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
data, messages, err := q.QueryMultiContextWithMessages(ctx, query)
return data, messages, true, err
}
if q, ok := inst.(agentMultiResultContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
data, err := q.QueryMultiContext(ctx, query)
return data, nil, true, err
}
}
if q, ok := inst.(agentMultiResultMessageRunner); ok {
data, messages, err := q.QueryMultiWithMessages(query)
return data, messages, true, err
}
if q, ok := inst.(agentMultiResultRunner); ok {
data, err := q.QueryMulti(query)
return data, nil, true, err
}
return nil, nil, false, nil
}
func streamWithOptionalTimeout(inst db.StreamQueryExecer, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
}
if effectiveTimeoutMs <= 0 {
return inst.StreamQuery(query, consumer)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
return inst.StreamQueryContext(ctx, query, consumer)
}
func streamBufferedQueryResult(fields []string, data []map[string]interface{}, consumer db.QueryStreamConsumer) error {
if err := consumer.SetColumns(fields); err != nil {
return err
}
if valueConsumer, ok := consumer.(db.QueryStreamValueConsumer); ok {
for _, row := range data {
values := make([]interface{}, len(fields))
for idx, field := range fields {
values[idx] = row[field]
}
if err := valueConsumer.ConsumeRowValues(values); err != nil {
return err
}
}
return nil
}
for _, row := range data {
if err := consumer.ConsumeRow(row); err != nil {
return err
}
}
return nil
}
func streamStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
if streamer, ok := inst.(db.StreamQueryExecer); ok {
return streamWithOptionalTimeout(streamer, query, timeoutMs, consumer)
}
data, fields, err := queryStatementWithOptionalTimeout(inst, query, timeoutMs)
if err != nil {
return err
}
return streamBufferedQueryResult(fields, data, consumer)
}
func streamDatabaseWithOptionalTimeout(inst db.Database, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
if streamer, ok := inst.(db.StreamQueryExecer); ok {
return streamWithOptionalTimeout(streamer, query, timeoutMs, consumer)
}
if provider, ok := inst.(db.SessionExecerProvider); ok {
openCtx := context.Background()
var cancel context.CancelFunc
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
}
if effectiveTimeoutMs > 0 {
openCtx, cancel = context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
}
session, err := provider.OpenSessionExecer(openCtx)
if err == nil {
defer session.Close()
return streamStatementWithOptionalTimeout(session, query, timeoutMs, consumer)
}
}
data, fields, err := queryWithOptionalTimeout(inst, query, timeoutMs)
if err != nil {
return err
}
return streamBufferedQueryResult(fields, data, consumer)
}
func execWithOptionalTimeout(inst agentExecRunner, query string, timeoutMs int64) (int64, error) {
effectiveTimeoutMs := timeoutMs
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
@@ -327,12 +866,52 @@ func execWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) (i
if effectiveTimeoutMs <= 0 {
return inst.Exec(query)
}
if e, ok := inst.(interface {
ExecContext(context.Context, string) (int64, error)
}); ok {
if e, ok := inst.(agentExecContextRunner); ok {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
defer cancel()
return e.ExecContext(ctx, query)
}
return inst.Exec(query)
}
func execStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) (int64, error) {
return execWithOptionalTimeout(inst, query, timeoutMs)
}
func countAgentResponseRows(data interface{}) int64 {
rows, ok := data.([]map[string]interface{})
if !ok {
return 0
}
return int64(len(rows))
}
func maybeReleaseAgentMemory(reason string, rows int64) {
if rows < agentMemoryTrimRowsThreshold {
return
}
if !agentMemoryTrimRunning.CompareAndSwap(false, true) {
return
}
runAgentMemoryTrimAsync(func() {
defer agentMemoryTrimRunning.Store(false)
if delay := nextAgentMemoryTrimDelay(); delay > 0 {
time.Sleep(delay)
}
agentMemoryTrimFn()
agentMemoryTrimLastAt.Store(time.Now().UnixNano())
})
}
func nextAgentMemoryTrimDelay() time.Duration {
lastUnixNano := agentMemoryTrimLastAt.Load()
if lastUnixNano <= 0 {
return 0
}
elapsed := time.Since(time.Unix(0, lastUnixNano))
if elapsed >= agentMemoryTrimMinInterval {
return 0
}
return agentMemoryTrimMinInterval - elapsed
}

View File

@@ -6,6 +6,7 @@ import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
@@ -77,8 +78,8 @@ func TestHandleRequestMetadataReportsAgentRevision(t *testing.T) {
agentDriverType = "clickhouse"
agentDatabaseFactory = func() db.Database { return nil }
var inst db.Database
resp := handleRequest(&inst, agentRequest{ID: 7, Method: agentMethodMetadata})
runtimeState := &agentRuntime{sessions: make(map[string]db.StatementExecer)}
resp := handleRequest(runtimeState, agentRequest{ID: 7, Method: agentMethodMetadata})
if !resp.Success {
t.Fatalf("metadata request failed: %s", resp.Error)
}
@@ -100,6 +101,9 @@ type fakeAgentTimeoutDB struct {
execCalled bool
execContextCalled bool
deadlineSet bool
queryMessages []string
multiResults []connection.ResultSetData
multiMessages []string
}
func (f *fakeAgentTimeoutDB) Connect(config connection.ConnectionConfig) error { return nil }
@@ -116,6 +120,14 @@ func (f *fakeAgentTimeoutDB) QueryContext(ctx context.Context, query string) ([]
}
return []map[string]interface{}{{"ok": 1}}, []string{"ok"}, nil
}
func (f *fakeAgentTimeoutDB) QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error) {
data, fields, err := f.QueryContext(context.Background(), query)
return data, fields, append([]string(nil), f.queryMessages...), err
}
func (f *fakeAgentTimeoutDB) QueryContextWithMessages(ctx context.Context, query string) ([]map[string]interface{}, []string, []string, error) {
data, fields, err := f.QueryContext(ctx, query)
return data, fields, append([]string(nil), f.queryMessages...), err
}
func (f *fakeAgentTimeoutDB) Exec(query string) (int64, error) {
f.execCalled = true
return 0, errors.New("exec should not be called")
@@ -149,6 +161,147 @@ func (f *fakeAgentTimeoutDB) GetForeignKeys(dbName, tableName string) ([]connect
func (f *fakeAgentTimeoutDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
return nil, nil
}
func (f *fakeAgentTimeoutDB) QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error) {
return append([]connection.ResultSetData(nil), f.multiResults...), append([]string(nil), f.multiMessages...), nil
}
func (f *fakeAgentTimeoutDB) QueryMultiContextWithMessages(ctx context.Context, query string) ([]connection.ResultSetData, []string, error) {
if _, ok := ctx.Deadline(); ok {
f.deadlineSet = true
}
return f.QueryMultiWithMessages(query)
}
type fakeAgentSessionDB struct {
fakeAgentTimeoutDB
session *fakeAgentStatementSession
}
type fakeAgentTransactionDB struct {
fakeAgentTimeoutDB
transaction *fakeAgentTransactionSession
}
func (f *fakeAgentTransactionDB) OpenTransactionExecer(context.Context) (db.TransactionExecer, error) {
f.transaction = &fakeAgentTransactionSession{}
return f.transaction, nil
}
type fakeAgentTransactionSession struct {
fakeAgentStatementSession
commitCalls int
rollbackCalls int
}
func (f *fakeAgentTransactionSession) Commit() error {
f.commitCalls++
return nil
}
func (f *fakeAgentTransactionSession) Rollback() error {
f.rollbackCalls++
return nil
}
func (f *fakeAgentSessionDB) OpenSessionExecer(ctx context.Context) (db.StatementExecer, error) {
f.session = &fakeAgentStatementSession{}
return f.session, nil
}
type fakeAgentStatementSession struct {
queryCalls int
execCalls int
closed bool
messages []string
}
func (f *fakeAgentStatementSession) Query(query string) ([]map[string]interface{}, []string, error) {
return f.QueryContext(context.Background(), query)
}
func (f *fakeAgentStatementSession) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
f.queryCalls++
return []map[string]interface{}{{"session_ok": 1}}, []string{"session_ok"}, nil
}
func (f *fakeAgentStatementSession) QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error) {
data, fields, err := f.QueryContext(context.Background(), query)
return data, fields, append([]string(nil), f.messages...), err
}
func (f *fakeAgentStatementSession) QueryContextWithMessages(ctx context.Context, query string) ([]map[string]interface{}, []string, []string, error) {
data, fields, err := f.QueryContext(ctx, query)
return data, fields, append([]string(nil), f.messages...), err
}
func (f *fakeAgentStatementSession) Exec(query string) (int64, error) {
return f.ExecContext(context.Background(), query)
}
func (f *fakeAgentStatementSession) ExecContext(ctx context.Context, query string) (int64, error) {
f.execCalls++
return 9, nil
}
func (f *fakeAgentStatementSession) Close() error {
f.closed = true
return nil
}
type fakeAgentStreamSession struct {
closed bool
streamCalls int
deadlineSet bool
}
func (f *fakeAgentStreamSession) Exec(query string) (int64, error) {
return 0, nil
}
func (f *fakeAgentStreamSession) ExecContext(ctx context.Context, query string) (int64, error) {
return 0, nil
}
func (f *fakeAgentStreamSession) Close() error {
f.closed = true
return nil
}
func (f *fakeAgentStreamSession) StreamQuery(query string, consumer db.QueryStreamConsumer) error {
return f.StreamQueryContext(context.Background(), query, consumer)
}
func (f *fakeAgentStreamSession) StreamQueryContext(ctx context.Context, query string, consumer db.QueryStreamConsumer) error {
f.streamCalls++
if _, ok := ctx.Deadline(); ok {
f.deadlineSet = true
}
if err := consumer.SetColumns([]string{"id", "name"}); err != nil {
return err
}
if valueConsumer, ok := consumer.(db.QueryStreamValueConsumer); ok {
if err := valueConsumer.ConsumeRowValues([]interface{}{1, "alice"}); err != nil {
return err
}
if err := valueConsumer.ConsumeRowValues([]interface{}{2, "bob"}); err != nil {
return err
}
return nil
}
if err := consumer.ConsumeRow(map[string]interface{}{"id": 1, "name": "alice"}); err != nil {
return err
}
return consumer.ConsumeRow(map[string]interface{}{"id": 2, "name": "bob"})
}
type fakeAgentSessionStreamDB struct {
fakeAgentTimeoutDB
session *fakeAgentStreamSession
openCalls int
}
func (f *fakeAgentSessionStreamDB) OpenSessionExecer(ctx context.Context) (db.StatementExecer, error) {
f.openCalls++
f.session = &fakeAgentStreamSession{}
return f.session, nil
}
func TestQueryWithOptionalTimeout_UsesQueryContext(t *testing.T) {
fake := &fakeAgentTimeoutDB{}
@@ -198,3 +351,343 @@ func TestQueryWithOptionalTimeout_ClickHouseLegacyModeUsesQueryContext(t *testin
t.Fatalf("clickhouse legacy query 调用路径异常QueryContext=%v Query=%v", fake.queryContextCalled, fake.queryCalled)
}
}
func TestHandleRequest_QueryIncludesServerMessages(t *testing.T) {
old := agentDriverType
defer func() { agentDriverType = old }()
agentDriverType = "sqlserver"
fake := &fakeAgentTimeoutDB{
queryMessages: []string{"PRINT sql line 1", "PRINT sql line 2"},
}
runtimeState := &agentRuntime{inst: fake, sessions: make(map[string]db.StatementExecer)}
resp := handleRequest(runtimeState, agentRequest{
ID: 11,
Method: agentMethodQuery,
Query: "exec dbo.p_get_select",
TimeoutMs: int64((2 * time.Second).Milliseconds()),
})
if !resp.Success {
t.Fatalf("query request failed: %s", resp.Error)
}
if len(resp.Messages) != 2 || resp.Messages[0] != "PRINT sql line 1" {
t.Fatalf("expected query messages to be preserved, got %#v", resp.Messages)
}
}
func TestHandleRequest_QueryMultiIncludesResultSetsAndMessages(t *testing.T) {
old := agentDriverType
defer func() { agentDriverType = old }()
agentDriverType = "sqlserver"
fake := &fakeAgentTimeoutDB{
multiResults: []connection.ResultSetData{
{
StatementIndex: 1,
Rows: []map[string]interface{}{{"name": "master"}},
Columns: []string{"name"},
},
{
StatementIndex: 1,
Rows: []map[string]interface{}{},
Columns: []string{},
Messages: []string{"PRINT generated sql"},
},
},
multiMessages: []string{"batch top-level message"},
}
runtimeState := &agentRuntime{inst: fake, sessions: make(map[string]db.StatementExecer)}
resp := handleRequest(runtimeState, agentRequest{
ID: 12,
Method: agentMethodQueryMulti,
Query: "exec dbo.p_get_select",
TimeoutMs: int64((2 * time.Second).Milliseconds()),
})
if !resp.Success {
t.Fatalf("queryMulti request failed: %s", resp.Error)
}
if len(resp.Messages) != 1 || resp.Messages[0] != "batch top-level message" {
t.Fatalf("expected top-level messages to be preserved, got %#v", resp.Messages)
}
resultSets, ok := resp.Data.([]connection.ResultSetData)
if !ok {
t.Fatalf("expected []connection.ResultSetData, got %T", resp.Data)
}
if len(resultSets) != 2 {
t.Fatalf("expected 2 result sets, got %#v", resultSets)
}
if len(resultSets[1].Messages) != 1 || resultSets[1].Messages[0] != "PRINT generated sql" {
t.Fatalf("expected message-only result set to be preserved, got %#v", resultSets[1])
}
}
func TestHandleRequest_UsesPinnedSessionForSessionScopedQueryAndExec(t *testing.T) {
old := agentDriverType
defer func() { agentDriverType = old }()
agentDriverType = "sqlserver"
fake := &fakeAgentSessionDB{}
runtimeState := &agentRuntime{
inst: fake,
sessions: make(map[string]db.StatementExecer),
}
openResp := handleRequest(runtimeState, agentRequest{ID: 1, Method: agentMethodOpenSession})
if !openResp.Success {
t.Fatalf("openSession failed: %s", openResp.Error)
}
sessionID, ok := openResp.Data.(string)
if !ok || strings.TrimSpace(sessionID) == "" {
t.Fatalf("unexpected session id payload: %#v", openResp.Data)
}
if fake.session == nil {
t.Fatal("expected OpenSessionExecer to create a pinned session")
}
queryResp := handleRequest(runtimeState, agentRequest{
ID: 2,
Method: agentMethodQuery,
SessionID: sessionID,
Query: "SELECT 1",
})
if !queryResp.Success {
t.Fatalf("session query failed: %s", queryResp.Error)
}
if len(queryResp.Messages) != 0 {
t.Fatalf("expected empty default session messages, got %#v", queryResp.Messages)
}
if fake.queryCalled || fake.queryContextCalled {
t.Fatalf("expected session query to bypass database-level query path, got Query=%v QueryContext=%v", fake.queryCalled, fake.queryContextCalled)
}
if fake.session.queryCalls != 1 {
t.Fatalf("expected pinned session queryCalls=1, got %d", fake.session.queryCalls)
}
execResp := handleRequest(runtimeState, agentRequest{
ID: 3,
Method: agentMethodExec,
SessionID: sessionID,
Query: "UPDATE t SET v = 1",
})
if !execResp.Success {
t.Fatalf("session exec failed: %s", execResp.Error)
}
if fake.execCalled || fake.execContextCalled {
t.Fatalf("expected session exec to bypass database-level exec path, got Exec=%v ExecContext=%v", fake.execCalled, fake.execContextCalled)
}
if fake.session.execCalls != 1 {
t.Fatalf("expected pinned session execCalls=1, got %d", fake.session.execCalls)
}
closeResp := handleRequest(runtimeState, agentRequest{
ID: 4,
Method: agentMethodCloseSession,
SessionID: sessionID,
})
if !closeResp.Success {
t.Fatalf("closeSession failed: %s", closeResp.Error)
}
if !fake.session.closed {
t.Fatal("expected pinned session to close")
}
}
func TestHandleRequest_UsesManagedTransactionSession(t *testing.T) {
for _, tc := range []struct {
name string
finishMethod string
wantCommits int
wantRollbacks int
}{
{name: "commit", finishMethod: "commitTransaction", wantCommits: 1},
{name: "rollback", finishMethod: "rollbackTransaction", wantRollbacks: 1},
} {
t.Run(tc.name, func(t *testing.T) {
fake := &fakeAgentTransactionDB{}
runtimeState := &agentRuntime{
inst: fake,
sessions: make(map[string]db.StatementExecer),
}
openResp := handleRequest(runtimeState, agentRequest{ID: 1, Method: "openTransaction"})
if !openResp.Success {
t.Fatalf("openTransaction failed: %s", openResp.Error)
}
sessionID, ok := openResp.Data.(string)
if !ok || strings.TrimSpace(sessionID) == "" {
t.Fatalf("unexpected transaction id payload: %#v", openResp.Data)
}
execResp := handleRequest(runtimeState, agentRequest{
ID: 2,
Method: agentMethodExec,
SessionID: sessionID,
Query: "UPDATE t SET v = 1",
})
if !execResp.Success {
t.Fatalf("transaction exec failed: %s", execResp.Error)
}
finishResp := handleRequest(runtimeState, agentRequest{
ID: 3,
Method: tc.finishMethod,
SessionID: sessionID,
})
if !finishResp.Success {
t.Fatalf("%s failed: %s", tc.finishMethod, finishResp.Error)
}
closeResp := handleRequest(runtimeState, agentRequest{
ID: 4,
Method: agentMethodCloseSession,
SessionID: sessionID,
})
if !closeResp.Success {
t.Fatalf("closeSession failed: %s", closeResp.Error)
}
if fake.transaction == nil || !fake.transaction.closed {
t.Fatal("expected managed transaction session to close")
}
if fake.transaction.commitCalls != tc.wantCommits || fake.transaction.rollbackCalls != tc.wantRollbacks {
t.Fatalf(
"unexpected finish calls: commit=%d rollback=%d",
fake.transaction.commitCalls,
fake.transaction.rollbackCalls,
)
}
})
}
}
func TestHandleStreamRequest_UsesSessionStreamerAndWritesChunks(t *testing.T) {
old := agentDriverType
originalAsync := runAgentMemoryTrimAsync
originalTrim := agentMemoryTrimFn
originalLastAt := agentMemoryTrimLastAt.Load()
defer func() { agentDriverType = old }()
defer func() {
runAgentMemoryTrimAsync = originalAsync
agentMemoryTrimFn = originalTrim
agentMemoryTrimRunning.Store(false)
agentMemoryTrimLastAt.Store(originalLastAt)
}()
agentDriverType = "oceanbase"
agentMemoryTrimRunning.Store(false)
agentMemoryTrimLastAt.Store(0)
fake := &fakeAgentSessionStreamDB{}
runtimeState := &agentRuntime{
inst: fake,
sessions: make(map[string]db.StatementExecer),
}
trimmed := 0
runAgentMemoryTrimAsync = func(fn func()) {
fn()
}
agentMemoryTrimFn = func() {
trimmed++
}
var out bytes.Buffer
writer := bufio.NewWriter(&out)
if err := handleStreamRequest(runtimeState, agentRequest{
ID: 9,
Method: agentMethodStreamQuery,
Query: "SELECT * FROM person_info",
TimeoutMs: int64((2 * time.Second).Milliseconds()),
}, writer); err != nil {
t.Fatalf("handleStreamRequest 返回错误: %v", err)
}
if fake.openCalls != 1 {
t.Fatalf("expected OpenSessionExecer called once, got %d", fake.openCalls)
}
if fake.session == nil || fake.session.streamCalls != 1 {
t.Fatalf("expected session streamer used once, session=%#v", fake.session)
}
if !fake.session.deadlineSet {
t.Fatal("expected stream query context deadline to be set")
}
if !fake.session.closed {
t.Fatal("expected session to close after streaming")
}
if fake.queryCalled || fake.queryContextCalled {
t.Fatalf("unexpected fallback query path, Query=%v QueryContext=%v", fake.queryCalled, fake.queryContextCalled)
}
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
if len(lines) != 3 {
t.Fatalf("expected 3 stream responses, got %d: %q", len(lines), out.String())
}
var columnsResp struct {
Success bool `json:"success"`
ChunkType string `json:"chunkType"`
Fields []string `json:"fields"`
}
if err := json.Unmarshal([]byte(lines[0]), &columnsResp); err != nil {
t.Fatalf("decode columns response failed: %v", err)
}
if !columnsResp.Success || columnsResp.ChunkType != agentChunkColumns || len(columnsResp.Fields) != 2 {
t.Fatalf("unexpected columns response: %#v", columnsResp)
}
var rowsResp struct {
Success bool `json:"success"`
ChunkType string `json:"chunkType"`
Data [][]interface{} `json:"data"`
}
if err := json.Unmarshal([]byte(lines[1]), &rowsResp); err != nil {
t.Fatalf("decode rows response failed: %v", err)
}
if !rowsResp.Success || rowsResp.ChunkType != agentChunkRows || len(rowsResp.Data) != 2 {
t.Fatalf("unexpected rows response: %#v", rowsResp)
}
if got := rowsResp.Data[1][1]; got != "bob" {
t.Fatalf("unexpected streamed row payload: %v", rowsResp.Data)
}
var doneResp struct {
Success bool `json:"success"`
ChunkType string `json:"chunkType"`
}
if err := json.Unmarshal([]byte(lines[2]), &doneResp); err != nil {
t.Fatalf("decode done response failed: %v", err)
}
if !doneResp.Success || doneResp.ChunkType != agentChunkDone {
t.Fatalf("unexpected done response: %#v", doneResp)
}
if trimmed != 0 {
t.Fatalf("小流式任务不应触发内存回收got=%d", trimmed)
}
}
func TestMaybeReleaseAgentMemory_TriggersTrimForLargeJobs(t *testing.T) {
originalAsync := runAgentMemoryTrimAsync
originalTrim := agentMemoryTrimFn
originalLastAt := agentMemoryTrimLastAt.Load()
t.Cleanup(func() {
runAgentMemoryTrimAsync = originalAsync
agentMemoryTrimFn = originalTrim
agentMemoryTrimRunning.Store(false)
agentMemoryTrimLastAt.Store(originalLastAt)
})
agentMemoryTrimRunning.Store(false)
agentMemoryTrimLastAt.Store(0)
triggered := 0
runAgentMemoryTrimAsync = func(fn func()) {
fn()
}
agentMemoryTrimFn = func() {
triggered++
}
maybeReleaseAgentMemory("test-large-query", agentMemoryTrimRowsThreshold)
if triggered != 1 {
t.Fatalf("大查询完成后应触发一次内存回收got=%d", triggered)
}
}

View File

@@ -0,0 +1,12 @@
//go:build gonavi_elasticsearch_driver
package main
import "GoNavi-Wails/internal/db"
func init() {
agentDriverType = "elasticsearch"
agentDatabaseFactory = func() db.Database {
return &db.ElasticsearchDB{}
}
}

View File

@@ -0,0 +1,12 @@
//go:build gonavi_gaussdb_driver
package main
import "GoNavi-Wails/internal/db"
func init() {
agentDriverType = "gaussdb"
agentDatabaseFactory = func() db.Database {
return &db.GaussDB{}
}
}

View File

@@ -0,0 +1,12 @@
//go:build gonavi_iotdb_driver
package main
import "GoNavi-Wails/internal/db"
func init() {
agentDriverType = "iotdb"
agentDatabaseFactory = func() db.Database {
return &db.IoTDBDB{}
}
}

View File

@@ -0,0 +1,12 @@
//go:build gonavi_trino_driver
package main
import "GoNavi-Wails/internal/db"
func init() {
agentDriverType = "trino"
agentDatabaseFactory = func() db.Database {
return &db.TrinoDB{}
}
}

View File

@@ -0,0 +1,87 @@
server {
listen 80;
listen [::]:80;
server_name download.syngnat.top;
root /srv/gonavi-downloads;
access_log /var/log/nginx/gonavi-download-access.log;
error_log /var/log/nginx/gonavi-download-error.log warn;
client_max_body_size 1m;
autoindex off;
etag on;
max_ranges 8;
location = /health.txt {
try_files $uri =404;
default_type text/plain;
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ ^/(?:gonavi|drivers)/(?:dev/)?releases/latest/ {
try_files $uri =404;
limit_except GET { deny all; }
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ ^/(?:gonavi|drivers)/(?:dev/)?releases/download/ {
try_files $uri =404;
limit_except GET { deny all; }
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header X-Content-Type-Options "nosniff" always;
}
location / {
add_header Cache-Control "no-store" always;
return 404;
}
}
server {
listen 127.0.0.1:9443 ssl;
listen [::1]:9443 ssl;
server_name download.syngnat.top;
ssl_certificate /etc/nginx/certs/download.syngnat.top.crt;
ssl_certificate_key /etc/nginx/certs/download.syngnat.top.key;
ssl_protocols TLSv1.2 TLSv1.3;
root /srv/gonavi-downloads;
access_log /var/log/nginx/gonavi-download-access.log;
error_log /var/log/nginx/gonavi-download-error.log warn;
client_max_body_size 1m;
autoindex off;
etag on;
max_ranges 8;
location = /health.txt {
try_files $uri =404;
default_type text/plain;
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ ^/(?:gonavi|drivers)/(?:dev/)?releases/latest/ {
try_files $uri =404;
limit_except GET { deny all; }
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header X-Content-Type-Options "nosniff" always;
}
location ~ ^/(?:gonavi|drivers)/(?:dev/)?releases/download/ {
try_files $uri =404;
limit_except GET { deny all; }
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header X-Content-Type-Options "nosniff" always;
}
location / {
add_header Cache-Control "no-store" always;
return 404;
}
}

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$(id -u)" -ne 0 ]]; then
echo "this installer must run as root" >&2
exit 1
fi
if [[ $# -ne 1 || ! -f "$1" ]]; then
echo "usage: $0 PATH_TO_NGINX_CONFIG" >&2
exit 2
fi
nginx_source="$1"
mirror_user="gonavi-deploy"
mirror_root="/srv/gonavi-downloads"
certificate="/etc/nginx/certs/download.syngnat.top.crt"
private_key="/etc/nginx/certs/download.syngnat.top.key"
site_available="/etc/nginx/sites-available/download.syngnat.top.conf"
site_enabled="/etc/nginx/sites-enabled/download.syngnat.top.conf"
if ! id "${mirror_user}" >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash "${mirror_user}"
fi
install -d -m 0755 -o "${mirror_user}" -g www-data "${mirror_root}"
install -d -m 0755 -o "${mirror_user}" -g www-data "${mirror_root}/.incoming"
printf '%s\n' 'gonavi-download-mirror-v1' > "${mirror_root}/.gonavi-mirror-root"
printf '%s\n' 'GoNavi download mirror' > "${mirror_root}/health.txt"
chown "${mirror_user}:www-data" \
"${mirror_root}/.gonavi-mirror-root" \
"${mirror_root}/health.txt"
chmod 0644 "${mirror_root}/.gonavi-mirror-root" "${mirror_root}/health.txt"
install -d -m 0755 /etc/nginx/certs
if [[ ! -s "${certificate}" || ! -s "${private_key}" ]]; then
openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \
-subj '/CN=download.syngnat.top' \
-addext 'subjectAltName=DNS:download.syngnat.top' \
-keyout "${private_key}" \
-out "${certificate}"
fi
chmod 0600 "${private_key}"
chmod 0644 "${certificate}"
install -m 0644 "${nginx_source}" "${site_available}"
ln -sfn "${site_available}" "${site_enabled}"
nginx -t
systemctl reload nginx
echo "Gatewaysentry download mirror origin installed"

View File

@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""Seed a VPS mirror from the currently published public mirror."""
from __future__ import annotations
import argparse
import concurrent.futures
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
TAG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
USER_AGENT = "GoNavi-VPS-Mirror-Seeder/1"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--source-base-url", required=True)
parser.add_argument("--mirror-root", type=Path, required=True)
parser.add_argument("--prepare-script", type=Path, required=True)
parser.add_argument("--commit-script", type=Path, required=True)
parser.add_argument("--workers", type=int, default=4)
return parser.parse_args()
def fetch_json(url: str) -> tuple[dict[str, Any], bytes]:
separator = "&" if "?" in url else "?"
request = urllib.request.Request(
f"{url}{separator}seed={int(time.time())}",
headers={"User-Agent": USER_AGENT, "Cache-Control": "no-cache"},
)
with urllib.request.urlopen(request, timeout=60) as response:
raw = response.read()
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError(f"expected JSON object from {url}")
return value, raw
def validate_tag(value: Any, label: str) -> str:
if not isinstance(value, str) or not TAG_RE.fullmatch(value):
raise ValueError(f"invalid {label}: {value!r}")
return value
def validate_name(value: Any) -> str:
if not isinstance(value, str) or not value or Path(value).name != value:
raise ValueError(f"invalid asset name: {value!r}")
if value in {".", ".."} or "/" in value or "\\" in value:
raise ValueError(f"invalid asset name: {value!r}")
return value
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def download_file(
url: str,
destination: Path,
expected_size: int,
expected_sha256: str | None,
) -> int:
destination.parent.mkdir(parents=True, exist_ok=True)
partial = destination.with_name(destination.name + ".part")
if destination.is_file() and destination.stat().st_size == expected_size:
if expected_sha256 is None or sha256_file(destination) == expected_sha256:
return expected_size
for attempt in range(1, 4):
offset = partial.stat().st_size if partial.exists() else 0
if offset > expected_size:
partial.unlink()
offset = 0
headers = {"User-Agent": USER_AGENT}
if offset:
headers["Range"] = f"bytes={offset}-"
request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request, timeout=120) as response:
append = offset > 0 and response.status == 206
mode = "ab" if append else "wb"
with partial.open(mode) as output:
shutil.copyfileobj(response, output, length=1024 * 1024)
except (OSError, urllib.error.URLError) as exc:
if attempt == 3:
raise RuntimeError(f"download failed after 3 attempts: {url}: {exc}") from exc
time.sleep(attempt * 2)
continue
if partial.stat().st_size != expected_size:
if attempt == 3:
raise ValueError(
f"download size mismatch for {destination.name}: "
f"expected {expected_size}, got {partial.stat().st_size}"
)
time.sleep(attempt * 2)
continue
if expected_sha256 is not None and sha256_file(partial) != expected_sha256:
partial.unlink()
if attempt == 3:
raise ValueError(f"download sha256 mismatch for {destination.name}")
continue
partial.replace(destination)
os.chmod(destination, 0o644)
return expected_size
raise AssertionError("unreachable")
def download_many(
items: list[tuple[str, Path, int, str | None]],
workers: int,
label: str,
) -> None:
completed_files = 0
completed_bytes = 0
lock = threading.Lock()
def run(item: tuple[str, Path, int, str | None]) -> None:
nonlocal completed_files, completed_bytes
size = download_file(*item)
with lock:
completed_files += 1
completed_bytes += size
if completed_files == len(items) or completed_files % 10 == 0:
print(
f"{label}: {completed_files}/{len(items)} files, "
f"{completed_bytes / (1024 ** 3):.2f} GiB",
flush=True,
)
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(run, item) for item in items]
for future in concurrent.futures.as_completed(futures):
future.result()
def seed_channel(
source_base_url: str,
mirror_root: Path,
prepare_script: Path,
commit_script: Path,
workers: int,
channel: str,
) -> None:
if channel == "stable":
app_latest_path = "gonavi/releases/latest/latest.json"
driver_latest_path = "drivers/releases/latest/GoNavi-DriverAgents-Index.json"
app_download_root = "gonavi/releases/download"
driver_download_root = "drivers/releases/download"
else:
app_latest_path = "gonavi/dev/releases/latest/latest-dev.json"
driver_latest_path = "drivers/dev/releases/latest/GoNavi-DriverAgents-Index.json"
app_download_root = "gonavi/dev/releases/download"
driver_download_root = "drivers/dev/releases/download"
app_manifest, app_manifest_raw = fetch_json(f"{source_base_url}/{app_latest_path}")
driver_index, driver_index_raw = fetch_json(f"{source_base_url}/{driver_latest_path}")
if channel == "stable":
app_tag = validate_tag(app_manifest.get("tagName"), "stable app tag")
driver_tag = validate_tag(driver_index.get("tagName"), "stable driver tag")
else:
app_tag = validate_tag(app_manifest.get("version"), "dev app tag")
driver_tag = validate_tag(driver_index.get("mirrorTagName"), "dev driver tag")
source_dir = Path(tempfile.mkdtemp(prefix=f".seed-{channel}-", dir=mirror_root))
app_dir = source_dir / "app"
driver_dir = source_dir / "driver"
app_dir.mkdir()
driver_dir.mkdir()
app_manifest_path = app_dir / Path(app_latest_path).name
driver_index_path = driver_dir / "GoNavi-DriverAgents-Index.json"
app_manifest_path.write_bytes(app_manifest_raw)
driver_index_path.write_bytes(driver_index_raw)
app_assets = app_manifest.get("assets")
if not isinstance(app_assets, list) or not app_assets:
raise ValueError(f"{channel} app manifest has no assets")
app_items: list[tuple[str, Path, int, str | None]] = []
for entry in app_assets:
if not isinstance(entry, dict):
raise ValueError("app asset entry must be an object")
name = validate_name(entry.get("name"))
size = entry.get("size")
checksum = entry.get("sha256")
if not isinstance(size, int) or size < 0:
raise ValueError(f"invalid app asset size for {name}")
if not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-fA-F]{64}", checksum):
raise ValueError(f"invalid app asset sha256 for {name}")
url = f"{source_base_url}/{app_download_root}/{app_tag}/{urllib.parse.quote(name)}"
app_items.append((url, app_dir / name, size, checksum.lower()))
driver_assets = driver_index.get("assets")
if not isinstance(driver_assets, dict) or not driver_assets:
raise ValueError(f"{channel} driver index has no assets")
driver_items: list[tuple[str, Path, int, str | None]] = []
for raw_name, size in sorted(driver_assets.items()):
name = validate_name(raw_name)
if not isinstance(size, int) or size < 0:
raise ValueError(f"invalid driver asset size for {name}")
url = f"{source_base_url}/{driver_download_root}/{driver_tag}/{urllib.parse.quote(name)}"
driver_items.append((url, driver_dir / name, size, None))
print(f"Seeding {channel} app {app_tag}", flush=True)
download_many(app_items, workers, f"{channel} app")
print(f"Seeding {channel} drivers {driver_tag}", flush=True)
download_many(driver_items, workers, f"{channel} drivers")
deployment_id = f"seed-{channel}-{int(time.time())}"
staging_dir = mirror_root / ".incoming" / deployment_id
subprocess.run(
[
sys.executable,
str(prepare_script),
"--channel",
channel,
"--app-tag",
app_tag,
"--app-dir",
str(app_dir),
"--app-manifest",
str(app_manifest_path),
"--driver-tag",
driver_tag,
"--driver-dir",
str(driver_dir),
"--driver-version-index",
str(driver_index_path),
"--driver-latest-index",
str(driver_index_path),
"--output",
str(staging_dir),
],
check=True,
)
subprocess.run(
[
"bash",
str(commit_script),
str(mirror_root),
str(staging_dir),
channel,
app_tag,
driver_tag,
],
check=True,
)
shutil.rmtree(source_dir)
def main() -> int:
args = parse_args()
if args.workers < 1 or args.workers > 8:
print("error: --workers must be between 1 and 8", file=sys.stderr)
return 2
source_base_url = args.source_base_url.rstrip("/")
try:
for channel in ("stable", "dev"):
seed_channel(
source_base_url,
args.mirror_root,
args.prepare_script,
args.commit_script,
args.workers,
channel,
)
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,7 @@
apiVersion: v2
name: gonavi-mcp-server
description: Helm chart for deploying GoNavi MCP Server
type: application
version: 0.1.0
appVersion: "latest"
kubeVersion: ">=1.24.0-0"

View File

@@ -0,0 +1,94 @@
# GoNavi MCP Server Helm Chart
这个 Chart 用来部署 `gonavi-mcp-server`,覆盖当前仓库 K8s 示例里的几类常见场景:
- 基于现有 Secret 直接部署
- 通过 Helm values 内联创建测试用 Secret
- 启用 Ingress
- 追加 `drivers/` PVC
- NAS / k3s 单机节点直接挂宿主机数据目录
Chart 目录:
- `Chart.yaml`
- `values.yaml`
- `values-examples/*.yaml`
- `templates/*.yaml`
## 1. 默认安装
默认值假设你已经有一个现成 Secret
- 名称:`gonavi-mcp-server-data`
- 至少包含:
- `connections.json`
- `daily_secrets.json`
- `GONAVI_MCP_HTTP_TOKEN`
安装命令:
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace
```
## 2. 直接创建测试用 Secret
如果只是本地验证模板或快速试跑,可以使用示例 values
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
-n gonavi --create-namespace \
-f deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml
```
这个示例只适合最小联调,不适合真实生产连接数据。
## 3. 常用示例
启用 Ingress
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
-n gonavi --create-namespace \
-f deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml
```
挂载 drivers PVC
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
-n gonavi --create-namespace \
-f deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml
```
同时启用 Ingress 与 drivers PVC
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
-n gonavi --create-namespace \
-f deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml
```
NAS / k3s hostPath
```bash
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
-n gonavi --create-namespace \
-f deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml
```
## 4. 关键参数
- `image.repository` / `image.tag`
- `secret.create`
- `secret.name`
- `data.mode=secret|hostPath|pvc`
- `drivers.mode=none|pvc|hostPath`
- `ingress.enabled`
## 5. 约束
- `data.mode=secret`Secret 必须能提供 `connections.json``daily_secrets.json`
- `data.mode=hostPath` 时,宿主机目录需要包含 GoNavi 活动数据目录内容
- `drivers.mode!=none` 时,会在 `/data/drivers` 追加独立挂载
- Chart 默认只部署 MCP Server不部署桌面 GUI

View File

@@ -0,0 +1,49 @@
{{- define "gonavi-mcp-server.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "gonavi-mcp-server.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := include "gonavi-mcp-server.name" . -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "gonavi-mcp-server.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}}
{{- end -}}
{{- define "gonavi-mcp-server.labels" -}}
helm.sh/chart: {{ include "gonavi-mcp-server.chart" . }}
app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "gonavi-mcp-server.selectorLabels" -}}
app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "gonavi-mcp-server.namespace" -}}
{{- default .Release.Namespace .Values.namespaceOverride -}}
{{- end -}}
{{- define "gonavi-mcp-server.secretName" -}}
{{- if .Values.secret.name -}}
{{- .Values.secret.name -}}
{{- else -}}
{{- include "gonavi-mcp-server.fullname" . -}}
{{- end -}}
{{- end -}}
{{- define "gonavi-mcp-server.serviceName" -}}
{{- include "gonavi-mcp-server.fullname" . -}}
{{- end -}}

View File

@@ -0,0 +1,130 @@
{{- $dataMode := .Values.data.mode -}}
{{- $driversMode := .Values.drivers.mode -}}
{{- if not (or (eq $dataMode "secret") (eq $dataMode "hostPath") (eq $dataMode "pvc")) -}}
{{- fail "data.mode must be one of: secret, hostPath, pvc" -}}
{{- end -}}
{{- if not (or (eq $driversMode "none") (eq $driversMode "pvc") (eq $driversMode "hostPath")) -}}
{{- fail "drivers.mode must be one of: none, pvc, hostPath" -}}
{{- end -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "gonavi-mcp-server.fullname" . }}
namespace: {{ include "gonavi-mcp-server.namespace" . }}
labels:
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: gonavi-mcp-server
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- http
{{- range .Values.mcp.extraArgs }}
- {{ . | quote }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: GONAVI_DATA_ROOT
value: {{ .Values.mcp.dataRoot | quote }}
- name: GONAVI_LOG_DIR
value: {{ .Values.mcp.logDir | quote }}
- name: GONAVI_MCP_HTTP_ADDR
value: {{ .Values.mcp.httpAddr | quote }}
- name: GONAVI_MCP_HTTP_PATH
value: {{ .Values.mcp.httpPath | quote }}
- name: GONAVI_MCP_SCHEMA_ONLY
value: {{ ternary "true" "false" .Values.mcp.schemaOnly | quote }}
- name: GONAVI_MCP_HTTP_TOKEN
valueFrom:
secretKeyRef:
name: {{ include "gonavi-mcp-server.secretName" . }}
key: {{ .Values.secret.tokenKey }}
{{- with .Values.mcp.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: gonavi-data
mountPath: {{ .Values.data.mountPath | quote }}
readOnly: true
{{- if ne $driversMode "none" }}
- name: gonavi-drivers
mountPath: {{ .Values.drivers.mountPath | quote }}
readOnly: true
{{- end }}
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 20
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
volumes:
- name: gonavi-data
{{- if eq $dataMode "secret" }}
projected:
sources:
- secret:
name: {{ include "gonavi-mcp-server.secretName" . }}
items:
- key: {{ .Values.secret.connectionsJsonKey }}
path: connections.json
- key: {{ .Values.secret.dailySecretsJsonKey }}
path: daily_secrets.json
{{- else if eq $dataMode "hostPath" }}
hostPath:
path: {{ required "data.hostPath.path is required when data.mode=hostPath" .Values.data.hostPath.path | quote }}
type: {{ default "Directory" .Values.data.hostPath.type | quote }}
{{- else if eq $dataMode "pvc" }}
persistentVolumeClaim:
claimName: {{ required "data.pvc.claimName is required when data.mode=pvc" .Values.data.pvc.claimName | quote }}
{{- end }}
{{- if eq $driversMode "pvc" }}
- name: gonavi-drivers
persistentVolumeClaim:
claimName: {{ required "drivers.pvc.claimName is required when drivers.mode=pvc" .Values.drivers.pvc.claimName | quote }}
{{- else if eq $driversMode "hostPath" }}
- name: gonavi-drivers
hostPath:
path: {{ required "drivers.hostPath.path is required when drivers.mode=hostPath" .Values.drivers.hostPath.path | quote }}
type: {{ default "Directory" .Values.drivers.hostPath.type | quote }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,34 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "gonavi-mcp-server.fullname" . }}
namespace: {{ include "gonavi-mcp-server.namespace" . }}
labels:
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className | quote }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path | quote }}
pathType: {{ .pathType | quote }}
backend:
service:
name: {{ include "gonavi-mcp-server.serviceName" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,27 @@
{{- if .Values.secret.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "gonavi-mcp-server.secretName" . }}
namespace: {{ include "gonavi-mcp-server.namespace" . }}
labels:
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.secret.tokenKey }}: {{ required "secret.stringData.token is required when secret.create=true" .Values.secret.stringData.token | quote }}
{{- if eq .Values.data.mode "secret" }}
{{ .Values.secret.connectionsJsonKey }}: |
{{ required "secret.stringData.connectionsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.connectionsJson | nindent 4 }}
{{ .Values.secret.dailySecretsJsonKey }}: |
{{ required "secret.stringData.dailySecretsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.dailySecretsJson | nindent 4 }}
{{- else }}
{{- if .Values.secret.stringData.connectionsJson }}
{{ .Values.secret.connectionsJsonKey }}: |
{{ .Values.secret.stringData.connectionsJson | nindent 4 }}
{{- end }}
{{- if .Values.secret.stringData.dailySecretsJson }}
{{ .Values.secret.dailySecretsJsonKey }}: |
{{ .Values.secret.stringData.dailySecretsJson | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "gonavi-mcp-server.serviceName" . }}
namespace: {{ include "gonavi-mcp-server.namespace" . }}
labels:
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP

View File

@@ -0,0 +1,4 @@
drivers:
mode: pvc
pvc:
claimName: gonavi-drivers

View File

@@ -0,0 +1,19 @@
ingress:
enabled: true
className: nginx
hosts:
- host: gonavi-mcp.example.com
paths:
- path: /mcp
pathType: Prefix
- path: /healthz
pathType: Prefix
tls:
- secretName: gonavi-mcp-server-tls
hosts:
- gonavi-mcp.example.com
drivers:
mode: pvc
pvc:
claimName: gonavi-drivers

View File

@@ -0,0 +1,14 @@
ingress:
enabled: true
className: nginx
hosts:
- host: gonavi-mcp.example.com
paths:
- path: /mcp
pathType: Prefix
- path: /healthz
pathType: Prefix
tls:
- secretName: gonavi-mcp-server-tls
hosts:
- gonavi-mcp.example.com

View File

@@ -0,0 +1,13 @@
secret:
create: true
stringData:
token: replace-with-a-random-token
connectionsJson: |
{
"connections": []
}
dailySecretsJson: |
{
"schemaVersion": 1,
"connections": {}
}

View File

@@ -0,0 +1,10 @@
data:
mode: hostPath
hostPath:
path: /volume1/docker/gonavi/data
type: Directory
secret:
create: true
stringData:
token: replace-with-a-random-token

View File

@@ -0,0 +1,99 @@
nameOverride: ""
fullnameOverride: ""
namespaceOverride: ""
replicaCount: 1
image:
repository: ghcr.io/syngnat/gonavi-mcp-server
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8765
annotations: {}
mcp:
dataRoot: /data
logDir: /var/lib/gonavi/logs
httpAddr: 0.0.0.0:8765
httpPath: /mcp
schemaOnly: true
extraArgs: []
extraEnv: []
secret:
create: false
name: gonavi-mcp-server-data
tokenKey: GONAVI_MCP_HTTP_TOKEN
connectionsJsonKey: connections.json
dailySecretsJsonKey: daily_secrets.json
stringData:
token: ""
connectionsJson: ""
dailySecretsJson: ""
data:
mode: secret
mountPath: /data
hostPath:
path: /volume1/docker/gonavi/data
type: Directory
pvc:
claimName: ""
drivers:
mode: none
mountPath: /data/drivers
pvc:
claimName: gonavi-drivers
hostPath:
path: /volume1/docker/gonavi/drivers
type: Directory
ingress:
enabled: false
className: nginx
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
hosts:
- host: gonavi-mcp.example.com
paths:
- path: /mcp
pathType: Prefix
- path: /healthz
pathType: Prefix
tls:
- secretName: gonavi-mcp-server-tls
hosts:
- gonavi-mcp.example.com
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,151 @@
# GoNavi MCP Server Kubernetes 示例
这个目录提供 `gonavi-mcp-server` 的最小 K8s 部署清单,适合:
- Linux 服务器上的单集群部署
- NAS 自带 K8s / k3s / k8s-lite 环境
- 需要把 GoNavi MCP 通过 Ingress / Gateway 暴露给远端 Agent 的场景
如果你希望通过 values 参数统一控制镜像、Secret、Ingress、PVC/hostPath而不是维护多份 overlay请直接使用 [deploy/helm/gonavi-mcp-server](../../helm/gonavi-mcp-server)。
目录结构:
- `kustomization.yaml`:基础部署入口
- `base/kustomization.yaml`:基础资源集合
- `overlays/nas-hostpath`:直接挂 NAS / 单机节点上的 GoNavi 数据目录
- `base/deployment.yaml` / `base/service.yaml`:基础资源
- `overlays/drivers-pvc`:为 `/data/drivers` 增加 PVC 挂载
- `overlays/ingress`:增加 Ingress 暴露 `/mcp`
- `overlays/ingress-with-drivers-pvc`:同时启用 Ingress 和 drivers PVC
## 前提
容器仍然依赖 GoNavi 的活动数据目录。至少要准备:
- `connections.json`
- `daily_secrets.json`
如果目标连接依赖可选 driver agent还需要额外挂载 `/data/drivers`
`base/deployment.yaml` 默认镜像指向:
```text
ghcr.io/syngnat/gonavi-mcp-server:latest
```
如果你要跟随 `dev` 分支,可以改成:
```text
ghcr.io/syngnat/gonavi-mcp-server:dev-latest
```
如果你要使用自建镜像,再按下面方式覆盖 `image`
- 在当前节点可访问的 Docker / Podman / containerd 环境中构建并导入这个镜像
- 把镜像推送到你的私有仓库,并同步修改 `base/deployment.yaml` 里的 `image`
例如:
```bash
docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local .
# or
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
```
## 1. 创建 Secret
推荐直接从现有 GoNavi 数据文件生成:
```bash
kubectl create secret generic gonavi-mcp-server-data \
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
```
如需单独 namespace请先执行
```bash
kubectl create namespace gonavi
kubectl -n gonavi create secret generic gonavi-mcp-server-data \
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
```
## 2. 应用清单
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server
```
如果使用独立 namespace请先把 YAML 里的 `namespace` 改成你的目标值,或自行用 `kustomize` / Helm 做二次封装。
清单内已经带了基础安全上下文和默认资源配额;如果你的连接数量、对象规模或并发访问更高,建议按实际负载调整 `resources`
### 常用 Overlay
仅基础部署:
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server
```
需要可选 driver agent
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc
```
NAS / k3s 单机节点直接挂宿主机目录:
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath
```
需要对外暴露 `/mcp`
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress
```
同时需要 Ingress 与 drivers PVC
```bash
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc
```
应用 overlay 前,记得先改里面的占位值,例如:
- `overlays/ingress/ingress.yaml` 里的域名、TLS Secret、`ingressClassName`
- `overlays/drivers-pvc/patch-deployment.yaml` 里的 PVC 名称 `gonavi-drivers`
- `overlays/nas-hostpath/patch-deployment.yaml` 里的宿主机目录 `/volume1/docker/gonavi/data`
`nas-hostpath` overlay 会直接把整个 GoNavi 数据目录挂到 `/data`,因此 `connections.json``daily_secrets.json``drivers/` 都从宿主机目录读取;此时仍建议保留 `GONAVI_MCP_HTTP_TOKEN` 的 Secret 注入,不要把 token 直接硬编码进 Deployment。
## 3. 校验
```bash
kubectl get pods -n gonavi
kubectl get svc -n gonavi
kubectl port-forward -n gonavi svc/gonavi-mcp-server 8765:8765
curl -H "Authorization: Bearer replace-with-a-random-token" http://127.0.0.1:8765/healthz
```
`/healthz` 返回 `ok` 说明 Pod 已对外提供 HTTP 服务。
## 4. 暴露给远端 Agent
- 集群内使用:直接访问 `http://gonavi-mcp-server.gonavi.svc.cluster.local:8765/mcp`
- 集群外使用:通过 Ingress / Gateway / 反向代理暴露 `/mcp`
- 远端 Agent 请求头:`Authorization: Bearer <你的 token>`
## 5. drivers 挂载
当前示例默认只挂载 `connections.json``daily_secrets.json`。如果保存连接里包含 Dameng、ClickHouse、DuckDB 等依赖 driver agent 的数据源,请在 Deployment 中额外补一个卷,把宿主机或 PVC 中的 `drivers/` 目录挂载到:
```text
/data/drivers
```
否则对应连接会因为缺少 driver agent 无法使用。

View File

@@ -0,0 +1,89 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gonavi-mcp-server
namespace: gonavi
labels:
app.kubernetes.io/name: gonavi-mcp-server
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: gonavi-mcp-server
template:
metadata:
labels:
app.kubernetes.io/name: gonavi-mcp-server
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: gonavi-mcp-server
# 如需跟随 dev 分支,可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest
image: ghcr.io/syngnat/gonavi-mcp-server:latest
imagePullPolicy: IfNotPresent
args:
- http
ports:
- name: http
containerPort: 8765
protocol: TCP
env:
- name: GONAVI_DATA_ROOT
value: /data
- name: GONAVI_LOG_DIR
value: /var/lib/gonavi/logs
- name: GONAVI_MCP_HTTP_ADDR
value: 0.0.0.0:8765
- name: GONAVI_MCP_HTTP_PATH
value: /mcp
- name: GONAVI_MCP_SCHEMA_ONLY
value: "true"
- name: GONAVI_MCP_HTTP_TOKEN
valueFrom:
secretKeyRef:
name: gonavi-mcp-server-data
key: GONAVI_MCP_HTTP_TOKEN
volumeMounts:
- name: gonavi-data
mountPath: /data
readOnly: true
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 20
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumes:
- name: gonavi-data
projected:
sources:
- secret:
name: gonavi-mcp-server-data
items:
- key: connections.json
path: connections.json
- key: daily_secrets.json
path: daily_secrets.json

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: gonavi-mcp-server
namespace: gonavi
labels:
app.kubernetes.io/name: gonavi-mcp-server
spec:
selector:
app.kubernetes.io/name: gonavi-mcp-server
ports:
- name: http
port: 8765
targetPort: http
protocol: TCP

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- base

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-deployment.yaml

View File

@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gonavi-mcp-server
namespace: gonavi
spec:
template:
spec:
containers:
- name: gonavi-mcp-server
volumeMounts:
- name: gonavi-drivers
mountPath: /data/drivers
readOnly: true
volumes:
- name: gonavi-drivers
persistentVolumeClaim:
claimName: gonavi-drivers

View File

@@ -0,0 +1,33 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gonavi-mcp-server
namespace: gonavi
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
tls:
- hosts:
- gonavi-mcp.example.com
secretName: gonavi-mcp-server-tls
rules:
- host: gonavi-mcp.example.com
http:
paths:
- path: /mcp
pathType: Prefix
backend:
service:
name: gonavi-mcp-server
port:
number: 8765
- path: /healthz
pathType: Prefix
backend:
service:
name: gonavi-mcp-server
port:
number: 8765

View File

@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- ingress.yaml
patches:
- path: patch-deployment.yaml

View File

@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gonavi-mcp-server
namespace: gonavi
spec:
template:
spec:
containers:
- name: gonavi-mcp-server
volumeMounts:
- name: gonavi-drivers
mountPath: /data/drivers
readOnly: true
volumes:
- name: gonavi-drivers
persistentVolumeClaim:
claimName: gonavi-drivers

View File

@@ -0,0 +1,33 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gonavi-mcp-server
namespace: gonavi
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
tls:
- hosts:
- gonavi-mcp.example.com
secretName: gonavi-mcp-server-tls
rules:
- host: gonavi-mcp.example.com
http:
paths:
- path: /mcp
pathType: Prefix
backend:
service:
name: gonavi-mcp-server
port:
number: 8765
- path: /healthz
pathType: Prefix
backend:
service:
name: gonavi-mcp-server
port:
number: 8765

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- ingress.yaml

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-deployment.yaml

View File

@@ -0,0 +1,14 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gonavi-mcp-server
namespace: gonavi
spec:
template:
spec:
volumes:
- $patch: replace
name: gonavi-data
hostPath:
path: /volume1/docker/gonavi/data
type: Directory

View File

@@ -0,0 +1,101 @@
# GoNavi MCP Server Podman 示例
这个目录提供 `gonavi-mcp-server` 的 Podman 原生部署入口,覆盖两类常见场景:
- 直接用 `podman run` 在 Linux 服务器 / NAS 上启动
- 用 rootless Quadlet + systemd 做常驻服务
目录内容:
- `gonavi-mcp-server.env.example`:给 `podman run` / Quadlet 共用的容器环境变量示例
- `gonavi-mcp-server.container`Quadlet 示例
前提仍然一样:宿主机 GoNavi 活动数据目录内至少应包含:
- `connections.json`
- `daily_secrets.json`
如果目标连接依赖可选 driver agent还要保证同一数据目录下已有 `drivers/`
## 1. 直接运行已发布镜像
```bash
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env
```
`GONAVI_MCP_HTTP_TOKEN` 改成随机值后运行:
```bash
podman run -d --name gonavi-mcp-server --replace \
-p 8765:8765 \
--env-file ./gonavi-mcp-server.env \
-v /absolute/path/to/gonavi-data:/data:Z \
ghcr.io/syngnat/gonavi-mcp-server:latest http
```
查看状态:
```bash
podman ps
podman logs -f gonavi-mcp-server
```
如果你的宿主机没有启用 SELinux可把挂载参数末尾的 `:Z` 去掉;如果启用了 SELinux建议保留它。
## 2. 本地源码构建镜像
仓库根目录已经提供 `.containerignore`Podman 会优先读取它来裁剪构建上下文。
```bash
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
podman run -d --name gonavi-mcp-server --replace \
-p 8765:8765 \
--env-file ./gonavi-mcp-server.env \
-v /absolute/path/to/gonavi-data:/data:Z \
localhost/gonavi-mcp-server:local http
```
## 3. Rootless Quadlet 常驻服务
Podman Quadlet 适合 Linux 服务器 / NAS 上做 rootless 常驻服务。
把文件放到用户级目录:
```bash
mkdir -p ~/.config/containers/systemd
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container ~/.config/containers/systemd/
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ~/.config/containers/systemd/gonavi-mcp-server.env
```
然后修改两处:
- `~/.config/containers/systemd/gonavi-mcp-server.container` 里的 `Volume=/absolute/path/to/gonavi-data:/data:Z`
- `~/.config/containers/systemd/gonavi-mcp-server.env` 里的 `GONAVI_MCP_HTTP_TOKEN`
启动服务:
```bash
systemctl --user daemon-reload
systemctl --user enable --now gonavi-mcp-server.service
systemctl --user status gonavi-mcp-server.service
journalctl --user -u gonavi-mcp-server.service -f
```
如果你希望用户退出登录后服务仍然保持运行,再执行:
```bash
loginctl enable-linger "$USER"
```
Rootless Quadlet 默认搜索路径是 `~/.config/containers/systemd/`;如果你要做系统级 rootful 部署,可改放到 `/etc/containers/systemd/`
## 4. 关于 Compose
Podman 的 `podman compose` 依赖外部 compose provider。也就是说是否能直接复用仓库根目录的 `docker-compose.mcp-server.yml`,取决于你的 Podman 环境是否已经安装并配置了对应 provider。
因此,这个仓库对 Podman 的主支持路径是:
- `podman run`
- Quadlet推荐长期运行
如果你的环境已经有可用的 compose provider再去复用根目录 Compose 文件即可。

View File

@@ -0,0 +1,22 @@
[Unit]
Description=GoNavi MCP Server (rootless Podman)
Wants=network-online.target
After=network-online.target
[Container]
ContainerName=gonavi-mcp-server
Image=ghcr.io/syngnat/gonavi-mcp-server:latest
Pull=newer
EnvironmentFile=./gonavi-mcp-server.env
# 修改成你的 GoNavi 活动数据目录SELinux 主机建议保留 :Z
Volume=/absolute/path/to/gonavi-data:/data:Z
PublishPort=8765:8765
Exec=http
LogDriver=journald
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,15 @@
# 容器内 GoNavi 活动数据根目录。通常不需要修改Volume= 已把宿主机目录挂到 /data。
GONAVI_DATA_ROOT=/data
# 日志目录。默认与镜像内目录保持一致。
GONAVI_LOG_DIR=/var/lib/gonavi/logs
# MCP HTTP 监听地址与路径。
GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765
GONAVI_MCP_HTTP_PATH=/mcp
# 远程 MCP 客户端访问时必须携带的 Bearer Token
GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
# true = 只暴露结构查询工具,不注册 execute_sql
GONAVI_MCP_SCHEMA_ONLY=true

View File

@@ -0,0 +1,7 @@
services:
gonavi-mcp-server:
build:
context: .
dockerfile: Dockerfile.mcp-server
image: ${GONAVI_MCP_IMAGE_LOCAL:-gonavi-mcp-server:local}
pull_policy: never

View File

@@ -0,0 +1,20 @@
services:
gonavi-mcp-server:
image: ${GONAVI_MCP_IMAGE:-ghcr.io/syngnat/gonavi-mcp-server:latest}
pull_policy: ${GONAVI_MCP_PULL_POLICY:-missing}
container_name: gonavi-mcp-server
restart: unless-stopped
ports:
- "${GONAVI_MCP_HTTP_PORT:-8765}:8765"
environment:
GONAVI_DATA_ROOT: /data
GONAVI_LOG_DIR: /var/lib/gonavi/logs
GONAVI_MCP_HTTP_ADDR: 0.0.0.0:8765
GONAVI_MCP_HTTP_PATH: ${GONAVI_MCP_HTTP_PATH:-/mcp}
GONAVI_MCP_HTTP_TOKEN: ${GONAVI_MCP_HTTP_TOKEN}
GONAVI_MCP_SCHEMA_ONLY: ${GONAVI_MCP_SCHEMA_ONLY:-true}
volumes:
- type: bind
source: ${GONAVI_HOST_DATA_ROOT}
target: /data
command: ["http"]

View File

@@ -0,0 +1,7 @@
services:
gonavi-web-server:
build:
context: .
dockerfile: Dockerfile.web-server
image: ${GONAVI_WEB_IMAGE_LOCAL:-gonavi-web-server:local}
pull_policy: never

View File

@@ -0,0 +1,24 @@
services:
gonavi-web-server:
image: ${GONAVI_WEB_IMAGE:-ghcr.io/syngnat/gonavi-web-server:latest}
pull_policy: ${GONAVI_WEB_PULL_POLICY:-missing}
container_name: gonavi-web-server
restart: unless-stopped
ports:
- "${GONAVI_WEB_HTTP_PORT:-34116}:34116"
environment:
GONAVI_DATA_ROOT: /data
GONAVI_LOG_DIR: /var/lib/gonavi/logs
GONAVI_WEB_ADDR: 0.0.0.0:34116
GONAVI_WEB_PASSWORD: ${GONAVI_WEB_PASSWORD:-}
volumes:
- type: bind
source: ${GONAVI_HOST_DATA_ROOT}
target: /data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:34116/__gonavi/healthz"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
command: ["web-server"]

View File

@@ -0,0 +1,20 @@
# 宿主机 GoNavi 活动数据目录,目录内至少应包含 connections.json 与 daily_secrets.json
GONAVI_HOST_DATA_ROOT=/absolute/path/to/gonavi-data
# 预构建镜像地址。稳定版通常使用 latest跟随 dev 分支可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest
GONAVI_MCP_IMAGE=ghcr.io/syngnat/gonavi-mcp-server:latest
# 预构建镜像拉取策略always / missing / never
GONAVI_MCP_PULL_POLICY=missing
# 远程 MCP 客户端访问时必须携带的 Bearer Token
GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
# 宿主机暴露端口
GONAVI_MCP_HTTP_PORT=8765
# MCP HTTP 路径
GONAVI_MCP_HTTP_PATH=/mcp
# true = 只暴露结构查询工具,不注册 execute_sql
GONAVI_MCP_SCHEMA_ONLY=true

View File

@@ -0,0 +1,19 @@
# 宿主机 GoNavi 活动数据目录。
# 建议至少包含connections.json、daily_secrets.json
# 如需可选驱动代理,一并挂载 drivers/。
# Web 认证状态会写入 web_auth.json未设置环境密码时首次访问 /setup 初始化。
GONAVI_HOST_DATA_ROOT=/absolute/path/to/gonavi-data
# 预构建镜像地址。稳定版通常使用 latest跟随 dev 分支可改成 ghcr.io/syngnat/gonavi-web-server:dev-latest
GONAVI_WEB_IMAGE=ghcr.io/syngnat/gonavi-web-server:latest
# 预构建镜像拉取策略always / missing / never
GONAVI_WEB_PULL_POLICY=missing
# 宿主机暴露端口(映射到容器 34116
GONAVI_WEB_HTTP_PORT=34116
# Web 管理员密码(至少 6 位)。留空时首次访问 /setup 初始化;公网部署请使用更强密码。
# 设置或修改后请执行 docker compose up -d --force-recreate请限制本文件的读取权限。
# 新部署如需启用 2FA请先留空并通过 /setup 初始化,再设置为同一密码并重新创建容器。
GONAVI_WEB_PASSWORD=

View File

@@ -96,6 +96,12 @@
"version": "1.11.1",
"checksumPolicy": "off",
"downloadUrl": "builtin://activate/postgres"
},
"elasticsearch": {
"engine": "go",
"version": "8.19.0",
"checksumPolicy": "off",
"downloadUrl": "builtin://activate/elasticsearch"
}
}
}

View File

@@ -0,0 +1,355 @@
# Cmd/Ctrl+W 标签关闭快捷键设计
- 日期2026-07-20
- 状态:已确认,待实施
- 适用范围GoNavi 主窗口的工作区标签和 SQL 查询结果区标签
## 1. 背景
GoNavi 当前没有统一的 `Cmd+W` / `Ctrl+W` 应用内关闭语义。工作区标签由
`TabManager` 管理,查询结果标签由每个 `QueryEditor` 的本地状态管理,两套关闭
流程拥有不同的保护规则。若分别注册全局键盘监听,同一次按键可能同时关闭结果
标签和工作区标签。
本设计增加一个可配置动作 `closeActiveTab`,由唯一的全局入口根据用户最后一次
明确交互的区域,将关闭请求路由到结果区或工作区。
## 2. 目标
1. macOS 默认使用 `Meta+W`Windows/Linux 默认使用 `Ctrl+W`
2. 快捷键在设置中心可修改、启用或禁用。
3. 同一次按键最多执行一个关闭动作,结果区与工作区之间不允许穿透。
4. 复用现有关闭流程,保留未保存 SQL、运行中导入任务等保护。
5. 结果区日志标签保持常驻;日志标签活跃时,快捷键隐藏整个结果区。
6. 结果区被快捷键隐藏后,连续按键不得继续关闭工作区标签。
## 3. 非目标
1. 不新增“分别关闭结果标签”和“分别关闭工作区标签”两个可配置动作。
2. 不改变鼠标关闭按钮、中键关闭或右键批量关闭的产品语义。
3. 不关闭或清空 SQL 日志数据。
4. 不改变后台导出、同步任务在工作区标签关闭后的运行方式。
5. 不统一重构所有历史浮动窗口的关闭入口。
6. 不让主窗口快捷键关闭已经拆出的独立窗口标签或独立结果窗口。
## 4. 方案选择
### 4.1 采用方案:单一全局路由器
`App` 保持唯一的全局 `keydown` capture 监听入口,维护最后一次明确交互区域,
并向目标组件派发单一、明确的关闭命令。
采用该方案的原因:
- 键盘事件只匹配和消费一次,天然避免双重关闭。
- `TabManager``QueryEditor` 继续拥有各自的状态与保护逻辑。
- 路由状态可以独立测试,不需要把查询结果状态上移到全局 store。
- 后续自定义快捷键仍复用现有快捷键设置与冲突检测体系。
### 4.2 未采用方案
- 分区独立监听:工作区和结果区分别监听 `window.keydown`。该方案存在监听顺序、
冒泡阻断和同一 `window` 多监听器重复执行问题。
- 完全依赖 DOM 焦点Monaco、DataGrid、Ant Design Portal 和 `body` 级事件目标会
让焦点不足以可靠表达用户的区域意图。
## 5. 快捷键配置
### 5.1 动作定义
新增动作 `closeActiveTab`,纳入:
- `ShortcutAction`
- `SHORTCUT_ACTION_ORDER`
- `SHORTCUT_ACTION_META`
- `DEFAULT_SHORTCUT_OPTIONS`
- 快捷键设置界面和全部语言目录
动作允许在 Monaco、输入框和 DataGrid 等可编辑目标中触发,因为焦点位于编辑器
时仍应能关闭当前工作区标签。
默认绑定:
| 平台 | 组合键 | 默认状态 |
| --- | --- | --- |
| macOS | `Meta+W` | 启用 |
| Windows/Linux | `Ctrl+W` | 启用 |
GoNavi 正式接管该组合键,因此删除现有“浏览器关闭标签页”的 `Ctrl+W` 保留项;
其他浏览器、Monaco 和 DataGrid 保留项不变。
### 5.2 旧配置迁移
旧持久化配置中不存在 `closeActiveTab` 时,按平台分别迁移:
1. 若该平台没有任何已启用动作占用新默认组合键,补入启用的新动作。
2. 若已有动作占用 `Meta+W``Ctrl+W`,保留原动作及绑定,补入组合键相同但
`enabled: false` 的新动作。
3. 用户可在快捷键设置中修改冲突绑定并重新启用新动作。
4. “恢复默认值”和全新安装始终得到启用的默认绑定。
迁移只处理缺少新动作的旧配置,不覆盖用户已经保存的 `closeActiveTab` 设置。
禁用动作或把动作改绑到其他组合键后,默认 `Meta+W` / `Ctrl+W` 不再执行标签
关闭,但主窗口仍必须消费该平台原生关闭组合键,防止 WebView 把它解释为关闭应用
窗口或浏览器标签。
## 6. 路由状态模型
全局路由器维护 `CloseShortcutScope`
```ts
type CloseShortcutScope = 'workspace' | 'result' | 'blocked';
```
初始状态为 `workspace`
| 当前/触发条件 | 下一状态 | 含义 |
| --- | --- | --- |
| 用户点击或聚焦结果区 | `result` | 关闭请求只发送给当前查询结果区 |
| 用户点击或聚焦编辑器、工作区标签栏或侧边栏 | `workspace` | 关闭请求只发送给主工作区 |
| 快捷键在日志标签上隐藏结果区 | `blocked` | 后续重复按键只消费,不关闭任何标签 |
| `blocked` 后用户明确交互工作区 | `workspace` | 允许重新关闭工作区标签 |
| `blocked` 后用户明确交互重新显示的结果区 | `result` | 允许关闭结果标签或再次隐藏结果区 |
| 用户点击或聚焦独立结果窗口 | `blocked` | 独立窗口交互不得沿用后台工作区归属 |
SQL 执行后程序化展示结果、程序化切换结果标签、store 激活标签等行为不得修改
该状态。只有真实的 `pointerdown``focusin` 才能改变区域归属。
结果区根节点增加 `data-gonavi-close-shortcut-scope="result"`。路由器在 document
capture 阶段监听 `pointerdown``focusin`,通过该标记识别结果区;编辑器、
工作区标签栏和侧边栏归入工作区。
交互浮层与独立结果窗口使用稳定的
`data-gonavi-close-shortcut-guard="true"` 标记或统一 guard class。guard 包含当前可见
的模态框、抽屉、Dropdown、上下文菜单、Select/Picker 弹层、自建交互 Portal 和
`.gn-detached-result-window`。标记仅在浮层打开时处于 active 状态;若组件会保留
隐藏 DOM公共 helper 还必须检查元素的可见性。Tooltip、拖拽预览等不可获得输入
焦点的纯展示 Portal 不属于 guard。
普通浮层 guard 内的 `pointerdown``focusin` 不改变后台区域归属。关闭快捷键
来自 guard 内部时,只消费事件,不向工作区或结果区派发命令。
独立结果窗口是例外:其根节点同时标记
`data-gonavi-close-shortcut-scope="blocked"`。可信的 `pointerdown``focusin`
发生在窗口内部时,立即把路由 ref 写为 `blocked`。这覆盖标题栏拖动等不会夺取
后台 Monaco 焦点的交互;之后即使 `keydown.target` 仍是 Monaco也不得关闭主
工作区。只有用户再次明确交互主工作区或主结果区才恢复对应归属。
会遮挡后台交互的浮层另加
`data-gonavi-close-shortcut-blocks-background="true"`;只要此标记的节点当前可见,
即使键盘事件目标落在 `body`,也只消费、不关闭后台标签。模态框、抽屉、交互
Dropdown/上下文菜单、Select/Picker 弹层使用该标记。独立结果窗口只使用普通
guard不使用 background blocker避免窗口存在时永久禁用主工作区快捷键。
## 7. 键盘事件流
```text
全局 keydown capture
-> 不受 IME 过滤的底层组合键解析
-> 是否命中平台原生关闭键,或已启用的 closeActiveTab 绑定
-> 快捷键录制器优先接管
-> 命中平台原生关闭键时无条件 preventDefault
-> IME/guard 命中时只消费,不派发
-> 解析当前组合键的已启用动作 owner
其他动作 -> 交给原动作处理一次
closeActiveTab -> 读取 CloseShortcutScope 并路由
无 owner 且为平台原生关闭键 -> 消费为 no-op
```
事件处理约束:
1. `App` 是主窗口唯一的全局关闭快捷键入口。
2. 增加一个只做键位与修饰键比较、不会因 IME 状态返回 false 的底层 matcher。
它仅用于判断事件是否需要被应用接管;正常动作执行仍使用现有
`isShortcutMatch` 语义。
3. 路由器分别计算“平台原生关闭组合键”和“当前已启用动作绑定”的底层匹配,
同时按 `SHORTCUT_ACTION_ORDER` 解析当前组合键的已启用 owner。快捷键设置继续
保证同一平台不会新增多个启用 owner遇到历史重复数据时按现有动作顺序只取
第一个。
4. 快捷键录制器拥有优先权。录制期间全局路由器直接退出,由录制器负责
`preventDefault` 和保存绑定,不执行关闭动作。
5. 除录制器分支外,底层 matcher 一旦命中平台原生关闭组合键,必须立即
`preventDefault`,再判断 owner 和当前上下文。绑定 owner 存在但因可编辑目标、
当前标签类型或组件未挂载而无法执行时,动作可以 no-opWebView 默认关闭仍然
必须被抑制。
6. IME composing、全局 composition 状态或 keyCode/which 229 命中时,底层 matcher
仍能识别组合键并消费,但不得派发关闭。
7. 事件目标位于 guard 内,或存在可见的 background blocker 时,只消费、不关闭
后台标签。
8. 若 owner 是 `closeActiveTab`,才进入本设计的工作区/结果区路由。若 owner 是
其他历史动作,关闭路由不得抢占或改成 no-op全局动作继续由 App switch 执行,
查询编辑器动作继续交给其现有 owner并且整条链路只执行该动作一次。
9. 只有平台原生关闭键没有任何已启用 owner 时,才由保护分支消费为 no-op。
10. 关闭路由、IME/guard 保护和无 owner 的平台 no-op 在 `preventDefault` 后使用
`stopImmediatePropagation`,避免 WebView 或同一 `window` 上的其他监听器再次
处理。交给其他 owner 的事件不得在 owner 执行前被关闭路由
`stopImmediatePropagation`,但第 5 条的 `preventDefault` 仍已生效。
11. 一次事件只允许派发一个目标命令,不允许在目标无效时回退到另一区域。
12. `document.body``document.documentElement` 等文档级目标按已记录状态路由。
13. `closeActiveTab` 禁用或改绑,且没有其他已启用 owner 时,平台原生
`Meta+W` / `Ctrl+W` 仍被消费为 no-op自定义绑定只有在动作启用时才被接管。
14. 若真机验证发现 WebView 不向前端交付 `Cmd/Ctrl+W`,才增加 Wails 原生事件
fallbackfallback 必须汇入同一路由器并具备单次事件去重,不能形成第二套关闭
流程。
## 8. 工作区关闭契约
工作区命令由 `TabManager` 处理,目标必须是 `dockedActiveTabId`,不得使用全局
`activeTabId`。不存在主窗口活动标签时,命令为 no-op。
关闭必须复用:
```ts
closeTabsWithSQLFilePrompt(
[dockedActiveTabId],
() => closeTab(dockedActiveTabId),
);
```
由此保留:
- 外部 SQL 文件未保存时的保存、丢弃、取消确认。
- 外部 SQL 文件被移动或删除时的确认。
- 防止重复打开关闭确认。
- 运行中的 `data-import` 工作台关闭保护。
- store 已有的关闭后活动标签选择和关联资源清理。
`table-export``data-sync` 的任务状态独立于标签组件,仍允许关闭工作区标签并
在后台继续运行。
## 9. 结果区关闭契约
结果区命令只发送给主窗口当前活动的 `QueryEditor`,由它读取本地
`activeResultKey``resultSets`。组件提取一个共享的纯函数
`resolveEffectiveActiveResultKey`,供结果区渲染和快捷键命令共同使用,不能一方
读取原始 key、另一方使用视觉回退 key。
在支持嵌入 SQL 日志标签的结果区中,日志标签只要结果区可见就无条件存在,日志
数量为 0 时也显示;其 key 统一使用 `QUERY_EDITOR_SQL_LOG_TAB_KEY`
具体行为:
1. 当前键对应真实结果标签时,调用现有 `handleCloseResult`
2. 关闭后由现有逻辑选择相邻结果标签,不在路由器中重复计算。
3. 关闭最后一个真实结果标签后,活动状态明确切到日志 key结果区继续显示常驻
日志标签;即使历史状态中原始 key 为空,共享 resolver 也必须得到同一日志 key。
4. 当前键为日志标签时,不删除日志;隐藏整个结果区,并向路由器回报
`hidden`,使路由状态进入 `blocked`
5. 当前键缺失、过期或不属于真实结果与日志时,消费请求并回报 `ignored`,不得
回退关闭工作区。
6. 结果区已经隐藏时,结果命令为 no-op仍不得穿透到工作区。
结果命令需要一个明确的完成回报 `closed | hidden | ignored`。命令派发和回报必须
在当前 `keydown` 调用栈内同步完成,不使用 Promise 或等待 React render。可通过
同步 `dispatchEvent` 的可变 request detail 或等价的同步命令注册表返回 outcome。
路由状态以 ref/原子值为准,键盘监听不得读取可能滞后的 React state。收到
`hidden` 后,必须在监听器返回前写入 `scopeRef.current = 'blocked'``closed`
`ignored` 保持 `result`。因此即使同一 task 内连续派发两个 `keydown`,第二次也会
读到 `blocked`
结果命令同样读取 `resultSetsRef``activeResultKeyRef`。关闭动作在提交 React
state 更新的同时同步写入这两个 ref使同一 task 内的下一次命令能看到相邻结果
或日志 key而不是重复处理已经关闭的结果。
独立结果窗口不向主窗口发送工作区关闭命令。主窗口路由器也不控制独立结果窗口
的生命周期。
## 10. 错误与竞态处理
- 快速重复按键:工作区沿用关闭确认的互斥保护;结果区每次根据最新本地状态
处理。日志隐藏后立即切换为 `blocked`
- 结果标签关闭与查询完成并发:以命令处理时的最新 `activeResultKey` 为准;无效
key 返回 `ignored`
- 活动工作区标签在事件间切换:`TabManager` 在命令处理时读取最新
`dockedActiveTabId`
- 组件卸载:命令没有有效接收者时 no-op不向另一区域补发。
- 自定义绑定冲突:继续使用现有冲突提示;运行时不同时执行两个动作。
## 11. 测试设计
### 11.1 快捷键工具测试
- 新动作默认绑定、动作顺序、元信息和平台显示。
- 新动作允许在可编辑目标触发。
- `Ctrl+W` 不再报告浏览器保留冲突。
- 自定义绑定、禁用和恢复默认值。
- 动作禁用或改绑后,平台原生关闭组合键仍被消费但不关闭标签或窗口。
- 旧配置无冲突时自动启用新动作。
- 旧配置占用默认组合键时保留旧动作并禁用新动作,且按平台隔离。
- 迁移后按原生关闭组合键仍只执行原动作一次不执行关闭动作WebView 也不关闭。
- 历史 owner 因可编辑目标、非查询标签或组件未挂载而无法执行时,不执行任何动作,
但仍阻止 WebView 原生关闭。
- 所有语言包含新动作的 label/description。
### 11.2 路由器测试
- 初始状态关闭工作区,不触发结果区。
- 明确点击结果区后只关闭结果标签。
- 点击 Monaco、工作区标签栏或侧边栏后只关闭工作区标签。
- SQL 执行后程序化激活结果不抢占归属。
- 日志标签隐藏结果区并进入 `blocked`
- `blocked` 状态连续按键不关闭工作区;明确交互工作区后才恢复。
- 无效结果状态只消费,不穿透工作区。
- `body``documentElement` 目标使用最后明确区域。
- 快捷键录制期间不执行关闭。
- IME composing / keyCode 229 下,底层 matcher 仍消费默认或自定义关闭组合键,但
不执行关闭。
- 模态框、抽屉、下拉菜单和 Portal 活跃时不关闭后台标签。
- Tooltip 和拖拽预览等纯展示 Portal 不触发 guard。
- 保持后台 Monaco 焦点,初始状态为 `workspace` 时点击不可聚焦的独立结果窗口
标题栏,再按快捷键也不得关闭主工作区。
- 独立结果窗口仅仅存在时,不阻止主工作区内正常使用关闭快捷键。
- 可见 background blocker 存在且事件目标为 `body` 时也不得关闭后台标签。
- 同一个键盘事件最多派发一个关闭命令。
- `hidden` 回报后不等待重渲染,立即连续派发第二个事件仍保持 blocked。
### 11.3 工作区集成测试
- 使用 `dockedActiveTabId`,独立窗口处于全局 active 时也不关错标签。
- 外部 SQL 脏文件保留保存/丢弃/取消流程。
- 外部 SQL 文件丢失确认仍生效。
- 运行中数据导入工作台无法被快捷键关闭。
- 导出、同步工作台标签允许关闭,后台任务继续。
- 没有 docked 标签时 no-op。
### 11.4 结果区集成测试
- 关闭真实结果标签并正确选择相邻标签。
- 关闭最后一个真实结果后显示日志标签。
- 关闭最后一个真实结果后,命令和渲染共同解析为日志 key下一次快捷键隐藏
结果区。
- 日志标签不会被删除,结果区整体隐藏。
- 计数请求等现有清理逻辑仍由 `handleCloseResult` 执行。
- 结果区隐藏、active key 过期、组件卸载时均不关闭工作区。
- 独立结果窗口不会把命令路由回主工作区。
### 11.5 真机验收
- macOS 触摸板、Monaco、DataGrid 和普通面板焦点下验证 `Cmd+W`
- Windows/Linux 对应验证 `Ctrl+W`
- 验证 WebView 不执行原生窗口或浏览器标签关闭。
- 验证自定义组合键、完全禁用状态及 IME 组合输入;改绑或禁用后默认平台关闭键
仍是应用内 no-op。
## 12. 验收标准
1. 默认快捷键、自定义和禁用均按平台生效。
2. 结果区、工作区和 `blocked` 三种状态的路由符合本设计。
3. 一次按键不会同时关闭结果标签和工作区标签。
4. 日志标签始终保留;日志活跃时隐藏的是整个结果区。
5. 隐藏结果区后连续按键不会误关工作区标签。
6. 现有 SQL 文件和数据导入保护没有被绕过。
7. 旧用户配置不会因升级被静默覆盖或产生双动作。
8. 自动化测试和 macOS/Windows 真机验证通过。
## 13. 预计影响文件
- `frontend/src/utils/shortcuts.ts`
- `frontend/src/App.tsx`
- `frontend/src/components/TabManager.tsx`
- `frontend/src/components/QueryEditor.tsx`
- `frontend/src/components/QueryEditorResultsPanel.tsx`
- `frontend/src/i18n/catalog.ts` 及目录完整性测试
- 快捷键、App、TabManager、QueryEditor 相关测试文件

View File

@@ -0,0 +1,45 @@
# 需求进度追踪 - Oracle对象跳转与存储过程修改执行
## 1. 需求摘要
- 需求名称Oracle对象跳转与存储过程修改执行
- 提出日期2026-06-25
- 负责人Codex
- 目标SQL编辑器支持 Oracle 序列、存储包 Ctrl/Cmd 点击跳转;对象编辑执行 Oracle 存储过程时不再生成非法第二条语句。
- 非目标:不调整 Oracle 连接驱动、不改数据库 schema、不重构侧栏对象树。
## 2. 范围与验收
- 范围QueryEditor 对象元数据、hover/跳转逻辑、DefinitionViewer 对象编辑 SQL 生成、相关 i18n 和回归测试。
- 验收标准:序列/存储包在 SQL 编辑器可出现链接提示并打开定义页;带 SQLPlus `/` 的 Oracle PL/SQL 编辑 SQL 不生成 `/;`,执行时保留完整定义交给后端拆分。
- 依赖与约束:复用现有侧栏 sequence/package tab 类型;不混入 `frontend/wailsjs/go/models.ts` 既有未提交改动。
## 3. 里程碑与进度
- [x] 阶段 1需求澄清确认序列/存储包侧栏已可打开,但 SQL 编辑器跳转缺失;存储过程修改仍报 ORA-00900。
- [x] 阶段 2影响分析影响前端 QueryEditor、DefinitionViewer、i18n、测试后端拆分已有基础保护。
- [x] 阶段 3方案设计补齐元数据与跳转类型对象编辑 SQL 保留 SQLPlus `/` 语义。
- [x] 阶段 4实施计划先补导航闭环再修 PL/SQL 生成/执行,最后跑定向测试。
- [x] 阶段 5实现与自检已完成定向测试、QueryEditor 全文件测试、前端 build、后端 Oracle 拆分测试。
- [ ] 阶段 6评审与交付待提交推送。
- [ ] 阶段 7发布与观察发布后验证 Oracle 对象编辑与 SQL 编辑器跳转。
## 4. 变更清单
- 已完成:补齐 QueryEditor sequence/package 元数据、hover、点击打开定义页修复对象编辑 SQLPlus `/;`;执行路径兼容旧编辑页 `/;`
- 进行中:提交并推送。
- 待处理:发布后验证 Oracle 实库。
## 5. 风险与阻塞
- 风险:三段名称 `schema.package.proc``db.schema.table` 存在歧义。
- 阻塞:无。
- 缓解措施:仅当第一段不是可见数据库时,将三段名称按 Oracle schema-qualified sequence/package 解析。
## 6. 决策记录
- 决策 1序列和存储包复用侧栏现有 `sequence-def` / `package-def` tab 结构。
- 决策 2Oracle PL/SQL 定义执行时保留 SQLPlus `/` 分隔符,由后端拆分器去掉 delimiter避免前端用分号重组造成 ORA-00900。
## 7. 验证记录
- 验证项QueryEditor 导航回归测试、DefinitionViewer 对象编辑测试、SQL 语句拆分测试、i18n catalog 测试、前端 build、后端 Oracle 拆分测试。
- 结果:通过。
- 证据(日志/截图/链接):`npm test -- src/components/QueryEditor.external-sql-save.test.tsx``npm test -- src/components/DefinitionViewer.object-edit.test.tsx src/i18n/catalog.test.ts src/utils/sqlStatementSelection.test.ts``go test ./internal/app -run 'TestDBQueryMultiKeepsOracle|TestDBQueryMultiSkipsOracle'``npm run build`
## 8. 下一步
- 下一步行动:提交并推送到 dev。
- 负责人Codex

View File

@@ -0,0 +1,74 @@
# 需求进度追踪 - 生产连接只读保护
## 1. 需求摘要
- 需求名称:生产连接只读保护
- 提出日期2026-06-23
- 负责人Codex
- 目标:为 SQL 类数据库与 MongoDB 连接增加连接级只读保护启用后仅允许查询阻止写入、DDL、导入和同步目标操作
- 非目标:不为所有侧栏写操作都新增前端隐藏逻辑;不引入新的环境分级体系;不改造 JVM 只读能力
## 2. 范围与验收
- 范围:
- 连接配置模型、保存/回填与 RPC 序列化链路
- 连接弹窗只读开关、查询编辑器本地拦截、DataGrid 导入入口收口
- 后端 SQL/Mongo 查询判定与写操作统一守卫
- 验收标准:
- 支持的数据源出现“生产连接/只允许查询”开关
- 启用后普通查询仍可执行,非查询 SQL / Mongo 写命令被前后端阻止
- 导入、结构变更、清表、同步目标等关键写入口被后端拒绝
- 依赖与约束:
- 保持现有数据源能力判定与 QueryEditor 执行链路
- MongoDB 前端判定以保守拦截为主,最终正确性由后端守卫兜底
## 3. 里程碑与进度
- [x] 阶段 1需求澄清确认采用连接级 `readOnly` 布尔字段,不新建环境系统
- [x] 阶段 2影响分析梳理前端能力面板、查询执行、导入与后端写入口
- [x] 阶段 3方案设计确定“前端预拦截 + 后端最终守卫”双层保护
- [x] 阶段 4实施计划接入配置链路、能力判定、查询判定与写入口守卫
- [x] 阶段 5实现与自检补文案、测试与定向验证
- [x] 阶段 6评审与交付确认范围、风险、回滚点和验证命令
- [ ] 阶段 7发布与观察待体验验证
## 4. 变更清单
- 已完成:
- 新增连接级 `readOnly` 配置字段及前后端序列化支持
- 连接弹窗为 SQL 类数据库与 MongoDB 增加生产连接保护开关
- QueryEditor 增加本地非查询拦截DataGrid 导入入口在只读连接下禁用
- 后端为查询、DDL、导入、清表、同步等写入口增加统一只读守卫
- MongoDB 查询判定改为命令级白名单,不再把所有 JSON 命令都视为只读
- 补充前端/后端定向测试与需求追踪文档
- 进行中:
- 等待体验包验证连接弹窗、查询拦截和写操作拒绝文案
- 待处理:
- 如需进一步优化体验,再补侧栏对象级写菜单的前端隐藏/禁用
## 5. 风险与阻塞
- 风险:
- 前端 SQL/Mongo 只读判定是保守策略,边界命令可能仍需后端兜底
- 现有部分侧栏写菜单仍可能显示,但执行时会被后端拒绝
- 阻塞:
- 暂无
- 缓解措施:
- 关键写入口统一走后端守卫;前端只负责提前反馈与减少误操作
## 6. 决策记录
- 决策 1只对 SQL 类数据库和 MongoDB 支持连接级生产保护,其他数据源忽略 `readOnly`
- 决策 2采用顶层 `readOnly` 布尔字段,避免新增环境枚举和迁移成本
- 决策 3MongoDB 只读判定按命令白名单处理,防止把写命令误放行
## 7. 验证记录
- 验证项:
- 前端数据源能力判定、RPC 配置、连接配置测试
- 后端只读连接守卫与 SQL/Mongo 查询判定测试
- 结果:
- 通过
- 证据(日志/截图/链接):
- `go test ./internal/app -run 'Test(EnsureReadOnlyConnectionAllows|SupportsConnectionReadOnlyMode|IsReadOnlySQLQuery)'`
- `npm --prefix frontend test -- src/utils/dataSourceCapabilities.test.ts src/utils/connectionRpcConfig.test.ts src/components/connectionModal/connectionModalConfig.keepalive.test.ts`
## 8. 下一步
- 下一步行动:
- 用体验包回归验证生产连接下的查询、导入、建库删库、同步目标和 Mongo 命令拦截
- 如需更完整 UX再补侧栏写菜单的只读态收口
- 负责人:
- Codex

View File

@@ -25,6 +25,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/bootstrap.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,15 @@
"name": "gonavi-client",
"private": true,
"version": "0.6.5",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run"
"test": "vitest run",
"postinstall": "patch-package --error-on-fail",
"i18n:scan": "go run ../tools/i18n-scan --root .."
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
@@ -16,15 +19,19 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.6.0",
"@types/dagre": "^0.7.54",
"@types/react-syntax-highlighter": "^15.5.13",
"antd": "^5.12.0",
"clsx": "^2.1.0",
"dagre": "^0.8.5",
"fflate": "^0.8.3",
"mermaid": "^11.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-resizable": "^3.1.3",
"react-syntax-highlighter": "^16.1.1",
"reactflow": "^11.11.4",
"recharts": "^3.8.1",
"remark-gfm": "^4.0.1",
"sql-formatter": "^15.7.0",
@@ -38,6 +45,7 @@
"@types/react-test-renderer": "^18.0.7",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.1",
"patch-package": "^8.0.1",
"react-test-renderer": "^18.2.0",
"typescript": "^5.2.2",
"vite": "^5.0.8",

View File

@@ -1 +1 @@
d0464f9da25e9356e61652e638c99ffe
83b4b7d166ed9172beba2d6236382c0b

View File

@@ -0,0 +1,461 @@
diff --git a/node_modules/rc-table/es/VirtualTable/BodyGrid.js b/node_modules/rc-table/es/VirtualTable/BodyGrid.js
index 42dcde8..2cca743 100644
--- a/node_modules/rc-table/es/VirtualTable/BodyGrid.js
+++ b/node_modules/rc-table/es/VirtualTable/BodyGrid.js
@@ -23,6 +23,8 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
sticky = _useContext2.sticky,
scrollY = _useContext2.scrollY,
listItemHeight = _useContext2.listItemHeight,
+ listItemHeightFixed = _useContext2.listItemHeightFixed,
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual,
getComponent = _useContext2.getComponent,
onTablePropScroll = _useContext2.onScroll;
@@ -113,8 +115,14 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
var extraRender = function extraRender(info) {
+ // The column-window fast path explicitly requires rows without spans.
+ // Avoid scanning every column on each vertical window update.
+ if (listItemColumnVirtual) {
+ return null;
+ }
var start = info.start,
end = info.end,
getSize = info.getSize,
- offsetY = info.offsetY;
+ offsetY = info.offsetY,
+ offsetX = info.offsetX;
// Do nothing if no data
if (end < 0) {
@@ -198,6 +200,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
style: {
top: -offsetY + sizeInfo.top
},
+ offsetX: offsetX,
extra: true,
getHeight: getHeight
});
@@ -239,6 +242,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
className: tblPrefixCls,
height: scrollY,
itemHeight: listItemHeight || 24,
+ itemHeightFixed: listItemHeightFixed,
data: flattenData,
itemKey: function itemKey(item) {
return getRowKey(item.record);
@@ -262,6 +266,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
data: item,
rowKey: rowKey,
index: index,
+ offsetX: itemProps.offsetX,
style: itemProps.style
});
}));
diff --git a/node_modules/rc-table/es/VirtualTable/BodyLine.js b/node_modules/rc-table/es/VirtualTable/BodyLine.js
index c760819..1a6c396 100644
--- a/node_modules/rc-table/es/VirtualTable/BodyLine.js
+++ b/node_modules/rc-table/es/VirtualTable/BodyLine.js
@@ -2,7 +2,7 @@ import _extends from "@babel/runtime/helpers/esm/extends";
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
-var _excluded = ["data", "index", "className", "rowKey", "style", "extra", "getHeight"];
+var _excluded = ["data", "index", "className", "rowKey", "style", "offsetX", "extra", "getHeight"];
import { useContext } from '@rc-component/context';
import classNames from 'classnames';
import * as React from 'react';
@@ -10,7 +10,7 @@ import Cell from "../Cell";
import TableContext, { responseImmutable } from "../context/TableContext";
import useRowInfo from "../hooks/useRowInfo";
import VirtualCell from "./VirtualCell";
-import { StaticContext } from "./context";
+import { GridContext, StaticContext } from "./context";
import { computedExpandedClassName } from "../utils/expandUtil";
var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
var data = props.data,
@@ -18,20 +18,25 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
className = props.className,
rowKey = props.rowKey,
style = props.style,
+ offsetX = props.offsetX,
extra = props.extra,
getHeight = props.getHeight,
restProps = _objectWithoutProperties(props, _excluded);
var record = data.record,
indent = data.indent,
renderIndex = data.index;
- var _useContext = useContext(TableContext, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX']),
+ var _useContext = useContext(TableContext, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX', 'direction']),
scrollX = _useContext.scrollX,
flattenColumns = _useContext.flattenColumns,
prefixCls = _useContext.prefixCls,
fixColumn = _useContext.fixColumn,
- componentWidth = _useContext.componentWidth;
- var _useContext2 = useContext(StaticContext, ['getComponent']),
- getComponent = _useContext2.getComponent;
+ componentWidth = _useContext.componentWidth,
+ direction = _useContext.direction;
+ var _useContext2 = useContext(StaticContext, ['getComponent', 'listItemColumnVirtual']),
+ getComponent = _useContext2.getComponent,
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual;
+ var _useContext3 = useContext(GridContext, ['columnsOffset']),
+ columnsOffset = _useContext3.columnsOffset;
var rowInfo = useRowInfo(record, rowKey, index, indent);
var RowComponent = getComponent(['body', 'row'], 'div');
var cellComponent = getComponent(['body', 'cell'], 'div');
@@ -71,12 +76,7 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
rowStyle.position = 'absolute';
rowStyle.pointerEvents = 'none';
}
- var rowNode = /*#__PURE__*/React.createElement(RowComponent, _extends({}, rowProps, restProps, {
- "data-row-key": rowKey,
- ref: rowSupportExpand ? null : ref,
- className: classNames(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, _defineProperty({}, "".concat(prefixCls, "-row-extra"), extra)),
- style: _objectSpread(_objectSpread({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
- }), flattenColumns.map(function (column, colIndex) {
+ var renderVirtualCell = function renderVirtualCell(column, colIndex) {
return /*#__PURE__*/React.createElement(VirtualCell, {
key: colIndex,
component: cellComponent,
@@ -90,7 +90,60 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
inverse: extra,
getHeight: getHeight
});
- }));
+ };
+ var columnNodes;
+ if (listItemColumnVirtual && direction !== 'rtl' && !extra && columnsOffset && columnsOffset.length === flattenColumns.length) {
+ var mergedOffsetX = Math.max(0, offsetX || 0);
+ var viewportWidth = Math.max(0, componentWidth || 0);
+ var overscanWidth = Math.max(320, viewportWidth);
+ var visibleStart = Math.max(0, mergedOffsetX - overscanWidth);
+ var visibleEnd = mergedOffsetX + viewportWidth + overscanWidth;
+ var nodes = [];
+ var spacerStart = -1;
+ var spacerWidth = 0;
+ var flushSpacer = function flushSpacer() {
+ if (spacerWidth <= 0) return;
+ nodes.push(/*#__PURE__*/React.createElement(cellComponent, {
+ "aria-hidden": true,
+ className: "".concat(prefixCls, "-cell-virtual-spacer"),
+ key: "virtual-spacer-".concat(spacerStart),
+ style: {
+ border: 0,
+ flex: "0 0 ".concat(spacerWidth, "px"),
+ minWidth: 0,
+ padding: 0,
+ pointerEvents: 'none',
+ visibility: 'hidden',
+ width: "".concat(spacerWidth, "px")
+ }
+ }));
+ spacerStart = -1;
+ spacerWidth = 0;
+ };
+ flattenColumns.forEach(function (column, colIndex) {
+ var columnStart = colIndex > 0 ? columnsOffset[colIndex - 1] : 0;
+ var columnEnd = columnsOffset[colIndex] || columnStart;
+ var fixedColumn = column.fixed === true || column.fixed === 'left' || column.fixed === 'right';
+ var visibleColumn = fixedColumn || columnEnd >= visibleStart && columnStart <= visibleEnd;
+ if (visibleColumn) {
+ flushSpacer();
+ nodes.push(renderVirtualCell(column, colIndex));
+ } else {
+ if (spacerStart < 0) spacerStart = colIndex;
+ spacerWidth += Math.max(0, columnEnd - columnStart);
+ }
+ });
+ flushSpacer();
+ columnNodes = nodes;
+ } else {
+ columnNodes = flattenColumns.map(renderVirtualCell);
+ }
+ var rowNode = /*#__PURE__*/React.createElement(RowComponent, _extends({}, rowProps, restProps, {
+ "data-row-key": rowKey,
+ ref: rowSupportExpand ? null : ref,
+ className: classNames(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, _defineProperty({}, "".concat(prefixCls, "-row-extra"), extra)),
+ style: _objectSpread(_objectSpread({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
+ }), columnNodes);
if (rowSupportExpand) {
return /*#__PURE__*/React.createElement("div", {
ref: ref
diff --git a/node_modules/rc-table/es/VirtualTable/context.d.ts b/node_modules/rc-table/es/VirtualTable/context.d.ts
index e558165..ccd74fc 100644
--- a/node_modules/rc-table/es/VirtualTable/context.d.ts
+++ b/node_modules/rc-table/es/VirtualTable/context.d.ts
@@ -3,6 +3,8 @@ import type { GetComponent, TableSticky } from '../interface';
export interface StaticContextProps {
scrollY: number;
listItemHeight: number;
+ listItemHeightFixed?: boolean;
+ listItemColumnVirtual?: boolean;
sticky: boolean | TableSticky;
getComponent: GetComponent;
onScroll?: React.UIEventHandler<HTMLDivElement>;
diff --git a/node_modules/rc-table/es/VirtualTable/index.d.ts b/node_modules/rc-table/es/VirtualTable/index.d.ts
index 31fe8e0..c5097b2 100644
--- a/node_modules/rc-table/es/VirtualTable/index.d.ts
+++ b/node_modules/rc-table/es/VirtualTable/index.d.ts
@@ -8,6 +8,9 @@ export interface VirtualTableProps<RecordType> extends Omit<TableProps<RecordTyp
y: number;
};
listItemHeight?: number;
+ listItemHeightFixed?: boolean;
+ /** LTR-only fast path; requires body cells without colSpan/rowSpan. */
+ listItemColumnVirtual?: boolean;
}
export type ForwardGenericVirtualTable = (<RecordType>(props: TableProps<RecordType> & React.RefAttributes<Reference>) => React.ReactElement) & {
displayName?: string;
diff --git a/node_modules/rc-table/es/VirtualTable/index.js b/node_modules/rc-table/es/VirtualTable/index.js
index fb00e9b..a778aa6 100644
--- a/node_modules/rc-table/es/VirtualTable/index.js
+++ b/node_modules/rc-table/es/VirtualTable/index.js
@@ -27,6 +27,8 @@ function VirtualTable(props, ref) {
prefixCls = _props$prefixCls === void 0 ? DEFAULT_PREFIX : _props$prefixCls,
className = props.className,
listItemHeight = props.listItemHeight,
+ listItemHeightFixed = props.listItemHeightFixed,
+ listItemColumnVirtual = props.listItemColumnVirtual,
components = props.components,
onScroll = props.onScroll;
var _ref = scroll || {},
@@ -61,10 +63,12 @@ function VirtualTable(props, ref) {
sticky: sticky,
scrollY: scrollY,
listItemHeight: listItemHeight,
+ listItemHeightFixed: listItemHeightFixed,
+ listItemColumnVirtual: listItemColumnVirtual,
getComponent: getComponent,
onScroll: onInternalScroll
};
- }, [sticky, scrollY, listItemHeight, getComponent, onInternalScroll]);
+ }, [sticky, scrollY, listItemHeight, listItemHeightFixed, listItemColumnVirtual, getComponent, onInternalScroll]);
// ========================== Render ==========================
return /*#__PURE__*/React.createElement(StaticContext.Provider, {
diff --git a/node_modules/rc-table/lib/VirtualTable/BodyGrid.js b/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
index dee2539..b077789 100644
--- a/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
+++ b/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
@@ -33,6 +33,8 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
sticky = _useContext2.sticky,
scrollY = _useContext2.scrollY,
listItemHeight = _useContext2.listItemHeight,
+ listItemHeightFixed = _useContext2.listItemHeightFixed,
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual,
getComponent = _useContext2.getComponent,
onTablePropScroll = _useContext2.onScroll;
@@ -123,8 +125,14 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
var extraRender = function extraRender(info) {
+ // The column-window fast path explicitly requires rows without spans.
+ // Avoid scanning every column on each vertical window update.
+ if (listItemColumnVirtual) {
+ return null;
+ }
var start = info.start,
end = info.end,
getSize = info.getSize,
- offsetY = info.offsetY;
+ offsetY = info.offsetY,
+ offsetX = info.offsetX;
// Do nothing if no data
if (end < 0) {
@@ -208,6 +210,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
style: {
top: -offsetY + sizeInfo.top
},
+ offsetX: offsetX,
extra: true,
getHeight: getHeight
});
@@ -249,6 +252,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
className: tblPrefixCls,
height: scrollY,
itemHeight: listItemHeight || 24,
+ itemHeightFixed: listItemHeightFixed,
data: flattenData,
itemKey: function itemKey(item) {
return getRowKey(item.record);
@@ -272,6 +276,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
data: item,
rowKey: rowKey,
index: index,
+ offsetX: itemProps.offsetX,
style: itemProps.style
});
}));
diff --git a/node_modules/rc-table/lib/VirtualTable/BodyLine.js b/node_modules/rc-table/lib/VirtualTable/BodyLine.js
index e1275fb..d962380 100644
--- a/node_modules/rc-table/lib/VirtualTable/BodyLine.js
+++ b/node_modules/rc-table/lib/VirtualTable/BodyLine.js
@@ -19,7 +19,7 @@ var _useRowInfo = _interopRequireDefault(require("../hooks/useRowInfo"));
var _VirtualCell = _interopRequireDefault(require("./VirtualCell"));
var _context2 = require("./context");
var _expandUtil = require("../utils/expandUtil");
-var _excluded = ["data", "index", "className", "rowKey", "style", "extra", "getHeight"];
+var _excluded = ["data", "index", "className", "rowKey", "style", "offsetX", "extra", "getHeight"];
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
@@ -28,20 +28,25 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
className = props.className,
rowKey = props.rowKey,
style = props.style,
+ offsetX = props.offsetX,
extra = props.extra,
getHeight = props.getHeight,
restProps = (0, _objectWithoutProperties2.default)(props, _excluded);
var record = data.record,
indent = data.indent,
renderIndex = data.index;
- var _useContext = (0, _context.useContext)(_TableContext.default, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX']),
+ var _useContext = (0, _context.useContext)(_TableContext.default, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX', 'direction']),
scrollX = _useContext.scrollX,
flattenColumns = _useContext.flattenColumns,
prefixCls = _useContext.prefixCls,
fixColumn = _useContext.fixColumn,
- componentWidth = _useContext.componentWidth;
- var _useContext2 = (0, _context.useContext)(_context2.StaticContext, ['getComponent']),
- getComponent = _useContext2.getComponent;
+ componentWidth = _useContext.componentWidth,
+ direction = _useContext.direction;
+ var _useContext2 = (0, _context.useContext)(_context2.StaticContext, ['getComponent', 'listItemColumnVirtual']),
+ getComponent = _useContext2.getComponent,
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual;
+ var _useContext3 = (0, _context.useContext)(_context2.GridContext, ['columnsOffset']),
+ columnsOffset = _useContext3.columnsOffset;
var rowInfo = (0, _useRowInfo.default)(record, rowKey, index, indent);
var RowComponent = getComponent(['body', 'row'], 'div');
var cellComponent = getComponent(['body', 'cell'], 'div');
@@ -81,12 +86,7 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
rowStyle.position = 'absolute';
rowStyle.pointerEvents = 'none';
}
- var rowNode = /*#__PURE__*/React.createElement(RowComponent, (0, _extends2.default)({}, rowProps, restProps, {
- "data-row-key": rowKey,
- ref: rowSupportExpand ? null : ref,
- className: (0, _classnames.default)(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-row-extra"), extra)),
- style: (0, _objectSpread2.default)((0, _objectSpread2.default)({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
- }), flattenColumns.map(function (column, colIndex) {
+ var renderVirtualCell = function renderVirtualCell(column, colIndex) {
return /*#__PURE__*/React.createElement(_VirtualCell.default, {
key: colIndex,
component: cellComponent,
@@ -100,7 +100,60 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
inverse: extra,
getHeight: getHeight
});
- }));
+ };
+ var columnNodes;
+ if (listItemColumnVirtual && direction !== 'rtl' && !extra && columnsOffset && columnsOffset.length === flattenColumns.length) {
+ var mergedOffsetX = Math.max(0, offsetX || 0);
+ var viewportWidth = Math.max(0, componentWidth || 0);
+ var overscanWidth = Math.max(320, viewportWidth);
+ var visibleStart = Math.max(0, mergedOffsetX - overscanWidth);
+ var visibleEnd = mergedOffsetX + viewportWidth + overscanWidth;
+ var nodes = [];
+ var spacerStart = -1;
+ var spacerWidth = 0;
+ var flushSpacer = function flushSpacer() {
+ if (spacerWidth <= 0) return;
+ nodes.push(/*#__PURE__*/React.createElement(cellComponent, {
+ "aria-hidden": true,
+ className: "".concat(prefixCls, "-cell-virtual-spacer"),
+ key: "virtual-spacer-".concat(spacerStart),
+ style: {
+ border: 0,
+ flex: "0 0 ".concat(spacerWidth, "px"),
+ minWidth: 0,
+ padding: 0,
+ pointerEvents: 'none',
+ visibility: 'hidden',
+ width: "".concat(spacerWidth, "px")
+ }
+ }));
+ spacerStart = -1;
+ spacerWidth = 0;
+ };
+ flattenColumns.forEach(function (column, colIndex) {
+ var columnStart = colIndex > 0 ? columnsOffset[colIndex - 1] : 0;
+ var columnEnd = columnsOffset[colIndex] || columnStart;
+ var fixedColumn = column.fixed === true || column.fixed === 'left' || column.fixed === 'right';
+ var visibleColumn = fixedColumn || columnEnd >= visibleStart && columnStart <= visibleEnd;
+ if (visibleColumn) {
+ flushSpacer();
+ nodes.push(renderVirtualCell(column, colIndex));
+ } else {
+ if (spacerStart < 0) spacerStart = colIndex;
+ spacerWidth += Math.max(0, columnEnd - columnStart);
+ }
+ });
+ flushSpacer();
+ columnNodes = nodes;
+ } else {
+ columnNodes = flattenColumns.map(renderVirtualCell);
+ }
+ var rowNode = /*#__PURE__*/React.createElement(RowComponent, (0, _extends2.default)({}, rowProps, restProps, {
+ "data-row-key": rowKey,
+ ref: rowSupportExpand ? null : ref,
+ className: (0, _classnames.default)(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-row-extra"), extra)),
+ style: (0, _objectSpread2.default)((0, _objectSpread2.default)({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
+ }), columnNodes);
if (rowSupportExpand) {
return /*#__PURE__*/React.createElement("div", {
ref: ref
diff --git a/node_modules/rc-table/lib/VirtualTable/context.d.ts b/node_modules/rc-table/lib/VirtualTable/context.d.ts
index e558165..ccd74fc 100644
--- a/node_modules/rc-table/lib/VirtualTable/context.d.ts
+++ b/node_modules/rc-table/lib/VirtualTable/context.d.ts
@@ -3,6 +3,8 @@ import type { GetComponent, TableSticky } from '../interface';
export interface StaticContextProps {
scrollY: number;
listItemHeight: number;
+ listItemHeightFixed?: boolean;
+ listItemColumnVirtual?: boolean;
sticky: boolean | TableSticky;
getComponent: GetComponent;
onScroll?: React.UIEventHandler<HTMLDivElement>;
diff --git a/node_modules/rc-table/lib/VirtualTable/index.d.ts b/node_modules/rc-table/lib/VirtualTable/index.d.ts
index 31fe8e0..c5097b2 100644
--- a/node_modules/rc-table/lib/VirtualTable/index.d.ts
+++ b/node_modules/rc-table/lib/VirtualTable/index.d.ts
@@ -8,6 +8,9 @@ export interface VirtualTableProps<RecordType> extends Omit<TableProps<RecordTyp
y: number;
};
listItemHeight?: number;
+ listItemHeightFixed?: boolean;
+ /** LTR-only fast path; requires body cells without colSpan/rowSpan. */
+ listItemColumnVirtual?: boolean;
}
export type ForwardGenericVirtualTable = (<RecordType>(props: TableProps<RecordType> & React.RefAttributes<Reference>) => React.ReactElement) & {
displayName?: string;
diff --git a/node_modules/rc-table/lib/VirtualTable/index.js b/node_modules/rc-table/lib/VirtualTable/index.js
index 8e31a9b..24ab161 100644
--- a/node_modules/rc-table/lib/VirtualTable/index.js
+++ b/node_modules/rc-table/lib/VirtualTable/index.js
@@ -38,6 +38,8 @@ function VirtualTable(props, ref) {
prefixCls = _props$prefixCls === void 0 ? _Table.DEFAULT_PREFIX : _props$prefixCls,
className = props.className,
listItemHeight = props.listItemHeight,
+ listItemHeightFixed = props.listItemHeightFixed,
+ listItemColumnVirtual = props.listItemColumnVirtual,
components = props.components,
onScroll = props.onScroll;
var _ref = scroll || {},
@@ -72,10 +74,12 @@ function VirtualTable(props, ref) {
sticky: sticky,
scrollY: scrollY,
listItemHeight: listItemHeight,
+ listItemHeightFixed: listItemHeightFixed,
+ listItemColumnVirtual: listItemColumnVirtual,
getComponent: getComponent,
onScroll: onInternalScroll
};
- }, [sticky, scrollY, listItemHeight, getComponent, onInternalScroll]);
+ }, [sticky, scrollY, listItemHeight, listItemHeightFixed, listItemColumnVirtual, getComponent, onInternalScroll]);
// ========================== Render ==========================
return /*#__PURE__*/React.createElement(_context.StaticContext.Provider, {

View File

@@ -0,0 +1,336 @@
diff --git a/node_modules/rc-virtual-list/es/List.d.ts b/node_modules/rc-virtual-list/es/List.d.ts
index c517366..e414513 100644
--- a/node_modules/rc-virtual-list/es/List.d.ts
+++ b/node_modules/rc-virtual-list/es/List.d.ts
@@ -20,6 +20,8 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
data: T[];
height?: number;
itemHeight?: number;
+ /** Treat every item as exactly itemHeight and calculate the visible range in O(1). */
+ itemHeightFixed?: boolean;
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight?: boolean;
itemKey: React.Key | ((item: T) => React.Key);
diff --git a/node_modules/rc-virtual-list/es/List.js b/node_modules/rc-virtual-list/es/List.js
index 2926056..e917ba1 100644
--- a/node_modules/rc-virtual-list/es/List.js
+++ b/node_modules/rc-virtual-list/es/List.js
@@ -4,7 +4,7 @@ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
-var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
+var _excluded = ["prefixCls", "className", "height", "itemHeight", "itemHeightFixed", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer';
import { useEvent } from 'rc-util';
@@ -35,6 +35,7 @@ export function RawList(props, ref) {
className = props.className,
height = props.height,
itemHeight = props.itemHeight,
+ itemHeightFixed = props.itemHeightFixed,
_props$fullHeight = props.fullHeight,
fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
style = props.style,
@@ -65,7 +66,7 @@ export function RawList(props, ref) {
}, [itemKey]);
// ================================ Height ================================
- var _useHeights = useHeights(getKey, null, null),
+ var _useHeights = useHeights(getKey, null, null, itemHeightFixed),
_useHeights2 = _slicedToArray(_useHeights, 4),
setInstanceRef = _useHeights2[0],
collectHeight = _useHeights2[1],
@@ -74,11 +75,12 @@ export function RawList(props, ref) {
// ================================= MISC =================================
var useVirtual = !!(virtual !== false && height && itemHeight);
- var containerHeight = React.useMemo(function () {
+ var measuredContainerHeight = React.useMemo(function () {
return Object.values(heights.maps).reduce(function (total, curr) {
return total + curr;
}, 0);
}, [heights.id, heights.maps]);
+ var containerHeight = itemHeightFixed ? itemHeight * (data || EMPTY_DATA).length : measuredContainerHeight;
var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
var isRTL = direction === 'rtl';
var mergedClassName = classNames(prefixCls, _defineProperty({}, "".concat(prefixCls, "-rtl"), isRTL), className);
@@ -159,6 +161,28 @@ export function RawList(props, ref) {
offset: undefined
};
}
+ if (itemHeightFixed && itemHeight > 0) {
+ var fixedDataLen = mergedData.length;
+ var fixedScrollHeight = fixedDataLen * itemHeight;
+ if (fixedDataLen === 0) {
+ return {
+ scrollHeight: 0,
+ start: 0,
+ end: -1,
+ offset: 0
+ };
+ }
+ var fixedMaxScrollTop = Math.max(0, fixedScrollHeight - height);
+ var fixedOffsetTop = Math.max(0, Math.min(fixedMaxScrollTop, offsetTop));
+ var fixedStartIndex = Math.min(fixedDataLen - 1, Math.max(0, Math.ceil(fixedOffsetTop / itemHeight) - 1));
+ var fixedEndIndex = Math.min(fixedDataLen - 1, Math.floor((fixedOffsetTop + height) / itemHeight) + 1);
+ return {
+ scrollHeight: fixedScrollHeight,
+ start: fixedStartIndex,
+ end: fixedEndIndex,
+ offset: fixedStartIndex * itemHeight
+ };
+ }
var itemTop = 0;
var startIndex;
var startOffset;
@@ -201,7 +225,7 @@ export function RawList(props, ref) {
end: endIndex,
offset: startOffset
};
- }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
+ }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height, itemHeightFixed, itemHeight]),
scrollHeight = _React$useMemo.scrollHeight,
start = _React$useMemo.start,
end = _React$useMemo.end,
@@ -213,6 +237,10 @@ export function RawList(props, ref) {
// Which will make scroll jump.
// Let's sync scroll top to avoid jump
React.useLayoutEffect(function () {
+ if (itemHeightFixed) {
+ heights.resetRecord();
+ return;
+ }
var changedRecord = heights.getRecord();
if (changedRecord.size === 1) {
var recordKey = Array.from(changedRecord.keys())[0];
@@ -232,7 +260,7 @@ export function RawList(props, ref) {
}
}
heights.resetRecord();
- }, [scrollHeight]);
+ }, [scrollHeight, itemHeightFixed, heightUpdatedMark]);
// ================================= Size =================================
var _React$useState = React.useState({
diff --git a/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts b/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
--- a/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
+++ b/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
@@ -1,6 +1,6 @@
import type { GetKey } from '../interface';
import CacheMap from '../utils/CacheMap';
-export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void): [
+export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void, disabled?: boolean): [
setInstanceRef: (item: T, instance: HTMLElement) => void,
collectHeight: (sync?: boolean) => void,
cacheMap: CacheMap,
diff --git a/node_modules/rc-virtual-list/es/hooks/useHeights.js b/node_modules/rc-virtual-list/es/hooks/useHeights.js
index 0323aae..38e6e75 100644
--- a/node_modules/rc-virtual-list/es/hooks/useHeights.js
+++ b/node_modules/rc-virtual-list/es/hooks/useHeights.js
@@ -6,7 +6,7 @@ function parseNumber(value) {
var num = parseFloat(value);
return isNaN(num) ? 0 : num;
}
-export default function useHeights(getKey, onItemAdd, onItemRemove) {
+export default function useHeights(getKey, onItemAdd, onItemRemove, disabled) {
var _React$useState = React.useState(0),
_React$useState2 = _slicedToArray(_React$useState, 2),
updatedMark = _React$useState2[0],
@@ -14,12 +14,17 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
var instanceRef = useRef(new Map());
var heightsRef = useRef(new CacheMap());
var promiseIdRef = useRef(0);
+ var disabledRef = useRef(disabled);
+ disabledRef.current = disabled;
function cancelRaf() {
promiseIdRef.current += 1;
}
function collectHeight() {
var sync = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
cancelRaf();
+ if (disabledRef.current) {
+ return;
+ }
var doCollect = function doCollect() {
var changed = false;
instanceRef.current.forEach(function (element, key) {
@@ -79,5 +84,10 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
useEffect(function () {
return cancelRaf;
}, []);
+ useEffect(function () {
+ if (!disabled) {
+ collectHeight(true);
+ }
+ }, [disabled]);
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
}
diff --git a/node_modules/rc-virtual-list/lib/List.d.ts b/node_modules/rc-virtual-list/lib/List.d.ts
index c517366..e414513 100644
--- a/node_modules/rc-virtual-list/lib/List.d.ts
+++ b/node_modules/rc-virtual-list/lib/List.d.ts
@@ -20,6 +20,8 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
data: T[];
height?: number;
itemHeight?: number;
+ /** Treat every item as exactly itemHeight and calculate the visible range in O(1). */
+ itemHeightFixed?: boolean;
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight?: boolean;
itemKey: React.Key | ((item: T) => React.Key);
diff --git a/node_modules/rc-virtual-list/lib/List.js b/node_modules/rc-virtual-list/lib/List.js
index f0cb64d..d24a9ad 100644
--- a/node_modules/rc-virtual-list/lib/List.js
+++ b/node_modules/rc-virtual-list/lib/List.js
@@ -32,7 +32,7 @@ var _useScrollDrag = _interopRequireDefault(require("./hooks/useScrollDrag"));
var _useScrollTo = _interopRequireDefault(require("./hooks/useScrollTo"));
var _ScrollBar = _interopRequireDefault(require("./ScrollBar"));
var _scrollbarUtil = require("./utils/scrollbarUtil");
-var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
+var _excluded = ["prefixCls", "className", "height", "itemHeight", "itemHeightFixed", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
var EMPTY_DATA = [];
var ScrollStyle = {
overflowY: 'auto',
@@ -44,6 +44,7 @@ function RawList(props, ref) {
className = props.className,
height = props.height,
itemHeight = props.itemHeight,
+ itemHeightFixed = props.itemHeightFixed,
_props$fullHeight = props.fullHeight,
fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
style = props.style,
@@ -74,7 +75,7 @@ function RawList(props, ref) {
}, [itemKey]);
// ================================ Height ================================
- var _useHeights = (0, _useHeights3.default)(getKey, null, null),
+ var _useHeights = (0, _useHeights3.default)(getKey, null, null, itemHeightFixed),
_useHeights2 = (0, _slicedToArray2.default)(_useHeights, 4),
setInstanceRef = _useHeights2[0],
collectHeight = _useHeights2[1],
@@ -83,11 +84,12 @@ function RawList(props, ref) {
// ================================= MISC =================================
var useVirtual = !!(virtual !== false && height && itemHeight);
- var containerHeight = React.useMemo(function () {
+ var measuredContainerHeight = React.useMemo(function () {
return Object.values(heights.maps).reduce(function (total, curr) {
return total + curr;
}, 0);
}, [heights.id, heights.maps]);
+ var containerHeight = itemHeightFixed ? itemHeight * (data || EMPTY_DATA).length : measuredContainerHeight;
var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
var isRTL = direction === 'rtl';
var mergedClassName = (0, _classnames.default)(prefixCls, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-rtl"), isRTL), className);
@@ -168,6 +170,28 @@ function RawList(props, ref) {
offset: undefined
};
}
+ if (itemHeightFixed && itemHeight > 0) {
+ var fixedDataLen = mergedData.length;
+ var fixedScrollHeight = fixedDataLen * itemHeight;
+ if (fixedDataLen === 0) {
+ return {
+ scrollHeight: 0,
+ start: 0,
+ end: -1,
+ offset: 0
+ };
+ }
+ var fixedMaxScrollTop = Math.max(0, fixedScrollHeight - height);
+ var fixedOffsetTop = Math.max(0, Math.min(fixedMaxScrollTop, offsetTop));
+ var fixedStartIndex = Math.min(fixedDataLen - 1, Math.max(0, Math.ceil(fixedOffsetTop / itemHeight) - 1));
+ var fixedEndIndex = Math.min(fixedDataLen - 1, Math.floor((fixedOffsetTop + height) / itemHeight) + 1);
+ return {
+ scrollHeight: fixedScrollHeight,
+ start: fixedStartIndex,
+ end: fixedEndIndex,
+ offset: fixedStartIndex * itemHeight
+ };
+ }
var itemTop = 0;
var startIndex;
var startOffset;
@@ -210,7 +234,7 @@ function RawList(props, ref) {
end: endIndex,
offset: startOffset
};
- }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
+ }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height, itemHeightFixed, itemHeight]),
scrollHeight = _React$useMemo.scrollHeight,
start = _React$useMemo.start,
end = _React$useMemo.end,
@@ -222,6 +246,10 @@ function RawList(props, ref) {
// Which will make scroll jump.
// Let's sync scroll top to avoid jump
React.useLayoutEffect(function () {
+ if (itemHeightFixed) {
+ heights.resetRecord();
+ return;
+ }
var changedRecord = heights.getRecord();
if (changedRecord.size === 1) {
var recordKey = Array.from(changedRecord.keys())[0];
@@ -241,7 +269,7 @@ function RawList(props, ref) {
}
}
heights.resetRecord();
- }, [scrollHeight]);
+ }, [scrollHeight, itemHeightFixed, heightUpdatedMark]);
// ================================= Size =================================
var _React$useState = React.useState({
diff --git a/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts b/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
--- a/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
+++ b/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
@@ -1,6 +1,6 @@
import type { GetKey } from '../interface';
import CacheMap from '../utils/CacheMap';
-export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void): [
+export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void, disabled?: boolean): [
setInstanceRef: (item: T, instance: HTMLElement) => void,
collectHeight: (sync?: boolean) => void,
cacheMap: CacheMap,
diff --git a/node_modules/rc-virtual-list/lib/hooks/useHeights.js b/node_modules/rc-virtual-list/lib/hooks/useHeights.js
index 471ffc6..a6d2ce5 100644
--- a/node_modules/rc-virtual-list/lib/hooks/useHeights.js
+++ b/node_modules/rc-virtual-list/lib/hooks/useHeights.js
@@ -14,7 +14,7 @@ function parseNumber(value) {
var num = parseFloat(value);
return isNaN(num) ? 0 : num;
}
-function useHeights(getKey, onItemAdd, onItemRemove) {
+function useHeights(getKey, onItemAdd, onItemRemove, disabled) {
var _React$useState = React.useState(0),
_React$useState2 = (0, _slicedToArray2.default)(_React$useState, 2),
updatedMark = _React$useState2[0],
@@ -22,12 +22,17 @@ function useHeights(getKey, onItemAdd, onItemRemove) {
var instanceRef = (0, _react.useRef)(new Map());
var heightsRef = (0, _react.useRef)(new _CacheMap.default());
var promiseIdRef = (0, _react.useRef)(0);
+ var disabledRef = (0, _react.useRef)(disabled);
+ disabledRef.current = disabled;
function cancelRaf() {
promiseIdRef.current += 1;
}
function collectHeight() {
var sync = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
cancelRaf();
+ if (disabledRef.current) {
+ return;
+ }
var doCollect = function doCollect() {
var changed = false;
instanceRef.current.forEach(function (element, key) {
@@ -87,5 +92,10 @@ function useHeights(getKey, onItemAdd, onItemRemove) {
(0, _react.useEffect)(function () {
return cancelRaf;
}, []);
+ (0, _react.useEffect)(function () {
+ if (!disabled) {
+ collectHeight(true);
+ }
+ }, [disabled]);
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Some files were not shown because too many files have changed in this diff Show More