diff --git a/.gitignore b/.gitignore
index b6f11fe..1d4530a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,4 @@ opsxdb-refs.tar.gz
apps/server/bin/
logs/
apps/desktop/src-tauri/binaries/opsxdb-server-*
+*.bak
diff --git a/README.md b/README.md
index 1e87108..341694b 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ packages/core 共享工具(保留)
packages/transport 浏览器 / Gin / Tauri(保留兼容)
apps/web Vite + React(:5174)
apps/desktop Tauri 2 壳
-apps/server Gin:health + MySQL 驱动 + AI stub(:8788)
+apps/server Gin:health + 内置驱动(MySQL 族/PG 族/SQL Server/Oracle/SQLite/Redis)+ AI stub(:8788)
```
## 怎么跑
@@ -53,6 +53,13 @@ curl -s -X POST http://127.0.0.1:8788/api/ai/chat \
- 驱动:MySQL 族 / PostgreSQL 族(含 GaussDB、人大金仓)/ SQL Server / Oracle / SQLite / Redis
- AI 面板仍是 stub(生成/解释/优化/修复/方言/样例)
+## 驱动与连接类型
+
+- **注册表**:`connection-types/connection-types.json`(schema v1,字段说明见该目录 `README.md`)。规划为驱动目录的单一事实来源;生成器接入前以 `packages/ui/src/catalog.ts` 与 `apps/server/internal/db/open.go` 为准。
+- **就绪状态**:`ready` = 可测试/连接;`verified` = 本机 docker 验收通过,依据 `tests/README.md` 驱动验收表。当前 MySQL/MariaDB/PG/SQL Server/SQLite/Redis 已验收,Oracle 代码就绪但本机未验证,达梦/MongoDB/ZK/ES/InfluxDB 未就绪。
+- **路线**:常用库走 native Go 驱动编译内置(go-ora 连 Oracle,不引入 JVM);长尾库(达梦/MongoDB/ZK/InfluxDB 等)规划 agent 按需接入,驱动分发走 Maven 坐标钉版本 + sha256 + 镜像回退,计划托管于 `opsxdb-driver` 仓库(Gitee/GitHub)。
+- **许可**:`connection-types/` 注册表数据从自有代码与厂商公开事实推导(见 `connection-types/NOTICE.md`);仓库整体许可证以根 LICENSE 为准。
+
## 端口
| 服务 | 端口 | 环境变量 |
diff --git a/apps/desktop/src-tauri/icons/options/cylinder.png b/apps/desktop/src-tauri/icons/options/cylinder.png
new file mode 100644
index 0000000..eb72235
Binary files /dev/null and b/apps/desktop/src-tauri/icons/options/cylinder.png differ
diff --git a/apps/desktop/src-tauri/icons/options/cylinder.svg b/apps/desktop/src-tauri/icons/options/cylinder.svg
new file mode 100644
index 0000000..8f30d42
--- /dev/null
+++ b/apps/desktop/src-tauri/icons/options/cylinder.svg
@@ -0,0 +1,10 @@
+
diff --git a/apps/desktop/src-tauri/icons/options/star.png b/apps/desktop/src-tauri/icons/options/star.png
new file mode 100644
index 0000000..169cacf
Binary files /dev/null and b/apps/desktop/src-tauri/icons/options/star.png differ
diff --git a/apps/desktop/src-tauri/icons/options/star.svg b/apps/desktop/src-tauri/icons/options/star.svg
new file mode 100644
index 0000000..a5a4b2d
--- /dev/null
+++ b/apps/desktop/src-tauri/icons/options/star.svg
@@ -0,0 +1,7 @@
+
diff --git a/apps/desktop/src-tauri/icons/options/tilt.png b/apps/desktop/src-tauri/icons/options/tilt.png
new file mode 100644
index 0000000..6e9af19
Binary files /dev/null and b/apps/desktop/src-tauri/icons/options/tilt.png differ
diff --git a/apps/desktop/src-tauri/icons/options/tilt.svg b/apps/desktop/src-tauri/icons/options/tilt.svg
new file mode 100644
index 0000000..1c23a36
--- /dev/null
+++ b/apps/desktop/src-tauri/icons/options/tilt.svg
@@ -0,0 +1,7 @@
+
diff --git a/apps/server/go.mod b/apps/server/go.mod
index 7c13c1d..d8c9a26 100644
--- a/apps/server/go.mod
+++ b/apps/server/go.mod
@@ -49,6 +49,7 @@ require (
github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
+ github.com/yuin/goldmark v1.8.6 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.51.0 // indirect
diff --git a/apps/server/go.sum b/apps/server/go.sum
index c02743e..885694d 100644
--- a/apps/server/go.sum
+++ b/apps/server/go.sum
@@ -130,6 +130,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
+github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
diff --git a/apps/server/internal/docs/assets/docs.css b/apps/server/internal/docs/assets/docs.css
index 9b85ea8..18dc68a 100644
--- a/apps/server/internal/docs/assets/docs.css
+++ b/apps/server/internal/docs/assets/docs.css
@@ -1,39 +1,246 @@
+/* OpsxDB 文档站 —— 清爽专业风格(浅色) */
:root {
- --bg: #0e1116; --panel: #151a21; --border: #243044;
- --text: #e6edf7; --muted: #8b9bb4; --accent: #3b82f6;
+ --blue: #059669;
+ --blue-dark: #047857;
+ --blue-soft: rgba(5, 150, 105, 0.08);
+ --bg: #f6f8fb;
+ --panel: #ffffff;
+ --border: #e4e9f1;
+ --text: #1c2434;
+ --text-2: #5b6880;
+ --text-3: #98a2b3;
+ --code-bg: #f3f6fb;
+ --code-border: #e1e8f3;
+ --green: #059669;
+ --amber: #b45309;
+ --red: #dc2626;
+ --sidebar-w: 262px;
+ --toc-w: 220px;
+ --topbar-h: 56px;
+ --radius: 10px;
+ --shadow: 0 1px 3px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.05);
+ --mono: "JetBrains Mono", SFMono-Regular, Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
-body { margin: 0; font: 13px/1.55 ui-sans-serif, system-ui, sans-serif; background: var(--bg); color: var(--text); }
-.top {
- height: 44px; border-bottom: 1px solid var(--border); background: var(--panel);
- display: flex; align-items: center; justify-content: space-between; padding: 0 16px;
+html { scroll-behavior: smooth; scroll-padding-top: 72px; }
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 14.5px;
+ line-height: 1.75;
}
-.brand { display: flex; align-items: center; gap: 8px; color: var(--text); text-decoration: none; font-weight: 600; }
-.brand em { color: var(--muted); font-style: normal; font-weight: 500; }
-.logo {
- width: 24px; height: 24px; border-radius: 6px; background: var(--accent);
- display: grid; place-items: center; font-size: 10px; font-weight: 700; color: #fff;
+a { color: var(--blue); text-decoration: none; }
+.i { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; }
+a:hover { color: var(--blue-dark); text-decoration: underline; }
+
+/* ---------- 顶栏 ---------- */
+.topbar {
+ position: sticky; top: 0; z-index: 50;
+ background: rgba(255, 255, 255, 0.92);
+ backdrop-filter: saturate(1.4) blur(8px);
+ border-bottom: 1px solid var(--border);
}
-.top a { color: var(--muted); text-decoration: none; font-size: 12.5px; }
-.layout { display: flex; min-height: calc(100vh - 44px); }
-.side {
- width: 200px; border-right: 1px solid var(--border); padding: 12px 10px;
- background: var(--panel); display: flex; flex-direction: column; gap: 2px;
+.topbar-inner {
+ max-width: 1440px; margin: 0 auto; height: var(--topbar-h);
+ display: flex; align-items: center; gap: 14px; padding: 0 20px;
+}
+.menu-btn {
+ display: none; border: 1px solid var(--border); background: #fff;
+ border-radius: 8px; width: 34px; height: 34px; cursor: pointer; color: var(--text-2);
+ place-items: center;
+}
+.menu-btn .i { width: 17px; height: 17px; }
+.brand { display: flex; align-items: center; gap: 9px; text-decoration: none !important; }
+.brand-logo { display: inline-flex; }
+.brand-logo svg { display: block; border-radius: 7px; }
+.brand-name { font-size: 16px; font-weight: 700; color: var(--text); }
+.brand-name em { font-style: normal; font-weight: 500; color: var(--text-2); margin-left: 2px; }
+.ver-badge {
+ font-size: 11px; font-weight: 600; color: var(--blue);
+ background: var(--blue-soft); border: 1px solid rgba(5, 150, 105, 0.22);
+ padding: 1px 9px; border-radius: 999px; white-space: nowrap;
+}
+.search { position: relative; flex: 1; max-width: 420px; margin-left: 8px; }
+.search input {
+ width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 9px;
+ background: var(--bg); padding: 0 14px 0 34px; font-size: 13.5px; color: var(--text); outline: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%2398a2b3' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E");
+ background-repeat: no-repeat; background-position: 12px center;
+}
+.search input:focus { border-color: var(--blue); background: #fff; box-shadow: 0 0 0 3px var(--blue-soft); }
+.search-panel {
+ position: absolute; top: 44px; left: 0; right: 0; background: #fff;
+ border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow);
+ max-height: 420px; overflow: auto; padding: 6px;
+}
+.search-item { display: block; padding: 8px 12px; border-radius: 8px; text-decoration: none !important; }
+.search-item:hover, .search-item.sel { background: var(--blue-soft); }
+.search-item .si-title { font-weight: 600; color: var(--text); font-size: 13.5px; }
+.search-item .si-group { color: var(--text-3); font-size: 11px; margin-left: 6px; }
+.search-item .si-snippet { color: var(--text-2); font-size: 12px; margin-top: 2px; display: block; }
+.search-empty { padding: 14px; text-align: center; color: var(--text-3); font-size: 13px; }
+.top-links { margin-left: auto; display: flex; gap: 16px; align-items: center; }
+.top-links a { font-size: 13.5px; font-weight: 500; }
+
+/* ---------- 三栏布局 ---------- */
+.layout {
+ max-width: 1440px; margin: 0 auto; display: grid;
+ grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--toc-w);
+ gap: 0 28px; padding: 24px 20px 60px;
+}
+
+/* 侧栏 */
+.sidebar { position: sticky; top: calc(var(--topbar-h) + 24px); align-self: start; max-height: calc(100vh - var(--topbar-h) - 48px); overflow-y: auto; padding-right: 8px; }
+.sidebar::-webkit-scrollbar { width: 4px; }
+.sidebar::-webkit-scrollbar-thumb { background: #d6dde8; border-radius: 4px; }
+.nav-group { margin-bottom: 18px; }
+.nav-group-title {
+ font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
+ color: var(--text-3); padding: 0 10px 6px;
}
.nav-link {
- display: block; padding: 6px 8px; border-radius: 6px; color: var(--muted);
- text-decoration: none; font-size: 12.5px;
+ display: block; padding: 6px 10px; border-radius: 8px; font-size: 13.5px;
+ color: var(--text-2); text-decoration: none !important; border-left: 2px solid transparent;
}
-.nav-link:hover { color: var(--text); background: #1a2230; }
-.nav-link.active { color: var(--text); background: color-mix(in srgb, var(--accent) 18%, transparent); }
-.content { flex: 1; padding: 24px 28px; max-width: 720px; }
-.content h1 { font-size: 20px; margin: 0 0 14px; }
-.content h2 { font-size: 15px; margin: 22px 0 8px; }
-.content p, .content li { color: #c5d0e0; }
-.content code, .content pre {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
+.nav-link:hover { background: rgba(5, 150, 105, 0.06); color: var(--text); }
+.nav-link.active {
+ background: var(--blue-soft); color: var(--blue-dark); font-weight: 600;
+ border-left-color: var(--blue);
}
-.content pre {
- background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
+.sidebar-foot { margin-top: 24px; padding: 12px 10px 0; border-top: 1px solid var(--border); font-size: 11.5px; color: var(--text-3); }
+
+/* 内容 */
+.content { min-width: 0; max-width: 860px; }
+.breadcrumb { font-size: 12.5px; color: var(--text-3); margin-bottom: 10px; }
+.breadcrumb span { margin: 0 6px; }
+.page-title { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 6px; line-height: 1.3; }
+.page-meta { font-size: 12.5px; color: var(--text-3); margin-bottom: 22px; }
+
+/* Markdown 排版 */
+.md h2 { font-size: 21px; font-weight: 700; margin: 38px 0 12px; padding-top: 8px; letter-spacing: -0.01em; }
+.md h3 { font-size: 16.5px; font-weight: 700; margin: 26px 0 8px; }
+.md h2 + h3, .md h1 + h2 { margin-top: 18px; }
+.md p { margin: 10px 0; color: #2a3348; }
+.md ul, .md ol { padding-left: 22px; margin: 10px 0; }
+.md li { margin: 5px 0; color: #2a3348; }
+.md li::marker { color: var(--blue); }
+.md strong { color: var(--text); }
+.md hr { border: none; border-top: 1px solid var(--border); margin: 30px 0; }
+.md img { max-width: 100%; border-radius: var(--radius); border: 1px solid var(--border); }
+.md blockquote {
+ margin: 14px 0; padding: 10px 16px; border-left: 3px solid var(--blue);
+ background: var(--blue-soft); border-radius: 0 8px 8px 0; color: #33415e;
+}
+
+/* 表格 */
+.md table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 13.5px; background: #fff; border-radius: var(--radius); overflow: hidden; box-shadow: 0 0 0 1px var(--border); }
+.md th, .md td { padding: 9px 14px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
+.md th { background: #f7fbf9; font-weight: 600; color: var(--text); white-space: nowrap; }
+.md tr:last-child td { border-bottom: none; }
+
+/* 代码块 */
+.md code { font-family: var(--mono); font-size: 12.8px; background: var(--code-bg); border: 1px solid var(--code-border); border-radius: 5px; padding: 1px 6px; color: #334155; }
+.md pre {
+ position: relative; background: #0f172a; color: #dbe4f3; border-radius: var(--radius);
+ padding: 16px 16px 14px; overflow-x: auto; margin: 16px 0; font-size: 13px; line-height: 1.65;
+}
+.md pre code { background: none; border: none; color: inherit; padding: 0; font-size: inherit; }
+.md pre .lang-tag {
+ position: absolute; top: 8px; right: 12px; font-size: 10.5px; color: #7c8db0;
+ font-family: var(--mono); text-transform: lowercase; letter-spacing: 0.04em;
+}
+.md pre .copy-btn {
+ position: absolute; top: 6px; right: 60px; font-size: 11px; color: #9fb0d0;
+ background: rgba(255, 255, 255, 0.07); border: none; border-radius: 6px; padding: 2px 9px; cursor: pointer;
+ opacity: 0; transition: opacity 0.15s;
+}
+.md pre:hover .copy-btn { opacity: 1; }
+.md pre .copy-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.14); }
+/* 行内高亮 token */
+.md pre .tok-key { color: #7dd3fc; }
+.md pre .tok-str { color: #86efac; }
+.md pre .tok-num { color: #fcd34d; }
+.md pre .tok-com { color: #64748b; font-style: italic; }
+
+/* 提示块(通过 blockquote 首字符约定) */
+.md blockquote.tip { border-left-color: var(--green); background: rgba(5, 150, 105, 0.07); }
+.md blockquote.warn { border-left-color: var(--amber); background: rgba(180, 83, 9, 0.07); }
+
+/* 上下页 */
+.pn-nav { display: flex; gap: 14px; margin-top: 48px; }
+.pn {
+ flex: 1; border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
+ padding: 13px 16px; text-decoration: none !important; transition: border-color 0.15s, box-shadow 0.15s;
+}
+.pn:hover { border-color: var(--blue); box-shadow: var(--shadow); }
+.pn.next { text-align: right; }
+.pn-label { display: block; font-size: 11.5px; color: var(--text-3); }
+.pn-title { display: block; font-weight: 600; color: var(--text); font-size: 14px; margin-top: 2px; }
+
+/* 反馈 */
+.feedback {
+ margin-top: 28px; padding: 16px 18px; border-top: 1px solid var(--border);
+ display: flex; align-items: center; gap: 10px; color: var(--text-2); font-size: 13px; flex-wrap: wrap;
+}
+.fb-btn {
+ border: 1px solid var(--border); background: #fff; border-radius: 8px; padding: 5px 12px;
+ font-size: 12.5px; cursor: pointer; color: var(--text-2); transition: all 0.15s;
+}
+.fb-btn:hover { border-color: var(--blue); color: var(--blue); }
+.fb-btn.done { border-color: var(--green); color: var(--green); background: rgba(5, 150, 105, 0.06); }
+.fb-thanks { color: var(--green); font-size: 12.5px; }
+
+/* 右侧目录 */
+.toc { position: sticky; top: calc(var(--topbar-h) + 24px); align-self: start; max-height: calc(100vh - var(--topbar-h) - 48px); overflow-y: auto; padding-top: 34px; }
+.toc-title { font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; color: var(--text-3); margin-bottom: 8px; }
+.toc-nav { display: flex; flex-direction: column; gap: 1px; border-left: 2px solid var(--border); padding-left: 0; }
+.toc-nav a {
+ font-size: 12.5px; color: var(--text-2); padding: 4px 0 4px 12px; text-decoration: none !important;
+ border-left: 2px solid transparent; margin-left: -2px; line-height: 1.5;
+}
+.toc-nav a:hover { color: var(--blue); }
+.toc-nav a.toc-l3 { padding-left: 26px; font-size: 12px; color: var(--text-3); }
+.toc-nav a.active { color: var(--blue); border-left-color: var(--blue); font-weight: 600; }
+.print-btn {
+ margin-top: 18px; width: 100%; border: 1px solid var(--border); background: #fff;
+ border-radius: 8px; padding: 7px; font-size: 12.5px; color: var(--text-2); cursor: pointer;
+}
+.print-btn:hover { border-color: var(--blue); color: var(--blue); }
+
+/* 404 */
+.doc-404 { display: grid; place-items: center; min-height: 100vh; text-align: center; }
+.doc-404 h1 { font-size: 56px; margin: 0; color: var(--blue); }
+.doc-404 .btn {
+ display: inline-block; margin-top: 12px; background: var(--blue); color: #fff;
+ padding: 8px 18px; border-radius: 8px; text-decoration: none !important; font-weight: 600;
+}
+
+/* ---------- 响应式 ---------- */
+@media (max-width: 1200px) {
+ .layout { grid-template-columns: var(--sidebar-w) minmax(0, 1fr); }
+ .toc { display: none; }
+}
+@media (max-width: 880px) {
+ .menu-btn { display: inline-grid; place-items: center; }
+ .layout { grid-template-columns: minmax(0, 1fr); padding-top: 12px; }
+ .sidebar {
+ position: fixed; z-index: 60; left: 0; top: var(--topbar-h); bottom: 0;
+ width: 280px; background: #fff; border-right: 1px solid var(--border);
+ padding: 16px; transform: translateX(-100%); transition: transform 0.2s; max-height: none;
+ }
+ .sidebar.open { transform: none; box-shadow: var(--shadow); }
+ .ver-badge { display: none; }
+ .top-links a { font-size: 12.5px; }
+ .page-title { font-size: 24px; }
+}
+
+/* ---------- 打印 ---------- */
+@media print {
+ .topbar, .sidebar, .toc, .pn-nav, .feedback, .menu-btn { display: none !important; }
+ .layout { display: block; max-width: none; padding: 0; }
+ body { background: #fff; }
+ .md pre { background: #f1f5f9 !important; color: #1e293b !important; }
+ .md pre .copy-btn, .md pre .lang-tag { display: none; }
}
-.content code { background: #1a2230; padding: 1px 5px; border-radius: 4px; }
diff --git a/apps/server/internal/docs/assets/docs.js b/apps/server/internal/docs/assets/docs.js
new file mode 100644
index 0000000..f3110c9
--- /dev/null
+++ b/apps/server/internal/docs/assets/docs.js
@@ -0,0 +1,219 @@
+/* OpsxDB 文档站交互:搜索 / 代码复制 / 目录高亮 / 反馈 / 移动端导航 */
+(function () {
+ "use strict";
+
+ /* ---------- 代码块:语言标签 + 复制按钮 + 轻量高亮 ---------- */
+ document.querySelectorAll("article.md pre").forEach(function (pre) {
+ var code = pre.querySelector("code");
+ if (!code) return;
+ var lang = (code.className.match(/language-([\w-]+)/) || [, ""])[1];
+ if (lang) {
+ var tag = document.createElement("span");
+ tag.className = "lang-tag";
+ tag.textContent = lang;
+ pre.appendChild(tag);
+ }
+ // 复制按钮
+ var btn = document.createElement("button");
+ btn.className = "copy-btn";
+ btn.type = "button";
+ btn.textContent = "复制";
+ btn.addEventListener("click", function () {
+ navigator.clipboard.writeText(code.textContent).then(
+ function () {
+ btn.textContent = "已复制 ✓";
+ setTimeout(function () { btn.textContent = "复制"; }, 1400);
+ },
+ function () { btn.textContent = "复制失败"; }
+ );
+ });
+ pre.appendChild(btn);
+ // JSON / bash 轻量高亮
+ if (lang === "json") highlightJSON(code);
+ else if (lang === "bash" || lang === "sh" || lang === "shell") highlightBash(code);
+ });
+
+ function esc(s) {
+ return s.replace(/&/g, "&").replace(//g, ">");
+ }
+ function highlightJSON(code) {
+ var src = code.textContent;
+ code.innerHTML = esc(src).replace(
+ /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
+ function (m, str, colon, kw, num) {
+ if (str && colon) return '' + str + "" + colon;
+ if (str) return '' + str + "";
+ if (kw) return '' + kw + "";
+ if (num) return '' + num + "";
+ return m;
+ }
+ );
+ }
+ function highlightBash(code) {
+ var lines = code.textContent.split("\n");
+ code.innerHTML = lines
+ .map(function (line) {
+ if (/^\s*#/.test(line)) return '' + esc(line) + "";
+ return esc(line).replace(/(^|\s)(-{1,2}[\w-]+)/g, '$1$2');
+ })
+ .join("\n");
+ }
+
+ /* ---------- blockquote 提示样式约定:> **提示** / > **注意** ---------- */
+ document.querySelectorAll("article.md blockquote").forEach(function (bq) {
+ var t = (bq.textContent || "").trim();
+ if (/^(注意|警告|⚠)/.test(t)) bq.classList.add("warn");
+ else if (/^(提示|说明|💡|Tip)/i.test(t)) bq.classList.add("tip");
+ });
+
+ /* ---------- 搜索 ---------- */
+ var input = document.getElementById("search-input");
+ var panel = document.getElementById("search-panel");
+ var entries = null;
+ var selIndex = -1;
+
+ function ensureIndex(cb) {
+ if (entries) return cb();
+ fetch("/docs/search-data.json")
+ .then(function (r) { return r.json(); })
+ .then(function (data) { entries = data; cb(); })
+ .catch(function () { entries = []; cb(); });
+ }
+
+ function snippet(text, q) {
+ var i = text.toLowerCase().indexOf(q);
+ if (i < 0) return text.slice(0, 60) + "…";
+ var start = Math.max(0, i - 24);
+ return (start > 0 ? "…" : "") + text.slice(start, i + q.length + 40) + "…";
+ }
+
+ function doSearch() {
+ var q = input.value.trim().toLowerCase();
+ selIndex = -1;
+ if (!q) { panel.hidden = true; return; }
+ ensureIndex(function () {
+ var hits = [];
+ entries.forEach(function (e) {
+ var titleHit = e.title.toLowerCase().indexOf(q) >= 0;
+ var idx = e.text.toLowerCase().indexOf(q);
+ if (titleHit || idx >= 0) {
+ hits.push({
+ slug: e.slug,
+ title: e.title,
+ group: e.group,
+ score: (titleHit ? 10 : 0) + (idx === 0 ? 3 : 0),
+ snippet: titleHit ? snippet(e.text, q) : snippet(e.text, q),
+ });
+ }
+ });
+ hits.sort(function (a, b) { return b.score - a.score; });
+ hits = hits.slice(0, 8);
+ if (!hits.length) {
+ panel.innerHTML = '
没有找到相关内容,换个关键词试试
';
+ } else {
+ panel.innerHTML = hits
+ .map(function (h) {
+ return (
+ '' +
+ '' + h.title + '' + h.group + "" +
+ '' + h.snippet + ""
+ );
+ })
+ .join("");
+ }
+ panel.hidden = false;
+ });
+ }
+
+ var debounce;
+ if (input) {
+ input.addEventListener("input", function () {
+ clearTimeout(debounce);
+ debounce = setTimeout(doSearch, 160);
+ });
+ input.addEventListener("focus", function () {
+ if (input.value.trim()) doSearch();
+ });
+ input.addEventListener("keydown", function (e) {
+ var items = panel.querySelectorAll(".search-item");
+ if (e.key === "ArrowDown" && items.length) {
+ e.preventDefault();
+ selIndex = Math.min(selIndex + 1, items.length - 1);
+ } else if (e.key === "ArrowUp" && items.length) {
+ e.preventDefault();
+ selIndex = Math.max(selIndex - 1, 0);
+ } else if (e.key === "Enter") {
+ if (selIndex >= 0 && items[selIndex]) { location.href = items[selIndex].href; return; }
+ var first = items[0];
+ if (first) location.href = first.href;
+ return;
+ } else if (e.key === "Escape") {
+ panel.hidden = true;
+ return;
+ } else {
+ return;
+ }
+ items.forEach(function (el, i) { el.classList.toggle("sel", i === selIndex); });
+ });
+ document.addEventListener("click", function (e) {
+ if (!e.target.closest(".search")) panel.hidden = true;
+ });
+ }
+
+ /* ---------- 目录滚动高亮 ---------- */
+ var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc-nav a"));
+ if (tocLinks.length && "IntersectionObserver" in window) {
+ var byId = {};
+ tocLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; });
+ var io = new IntersectionObserver(
+ function (entries) {
+ entries.forEach(function (en) {
+ if (en.isIntersecting) {
+ tocLinks.forEach(function (a) { a.classList.remove("active"); });
+ var a = byId[en.target.id];
+ if (a) a.classList.add("active");
+ }
+ });
+ },
+ { rootMargin: "-72px 0px -70% 0px", threshold: 0 }
+ );
+ document.querySelectorAll("article.md h2[id], article.md h3[id]").forEach(function (h) { io.observe(h); });
+ }
+
+ /* ---------- 反馈组件 ---------- */
+ var fb = document.getElementById("feedback");
+ if (fb) {
+ var pageKey = "opsxdb-docs-fb:" + location.pathname;
+ var voted = null;
+ try { voted = localStorage.getItem(pageKey); } catch (e) {}
+ var btns = fb.querySelectorAll(".fb-btn");
+ var thanks = document.getElementById("fb-thanks");
+ function mark(v) {
+ btns.forEach(function (b) {
+ if (b.dataset.v === v) b.classList.add("done");
+ });
+ thanks.hidden = false;
+ try { localStorage.setItem(pageKey, v); } catch (e) {}
+ }
+ if (voted) mark(voted);
+ btns.forEach(function (b) {
+ b.addEventListener("click", function () { mark(b.dataset.v); });
+ });
+ }
+
+ /* ---------- 打印 ---------- */
+ var printBtn = document.getElementById("print-btn");
+ if (printBtn) printBtn.addEventListener("click", function () { window.print(); });
+
+ /* ---------- 移动端侧栏 ---------- */
+ var menuBtn = document.getElementById("menu-btn");
+ var sidebar = document.getElementById("sidebar");
+ if (menuBtn && sidebar) {
+ menuBtn.addEventListener("click", function () { sidebar.classList.toggle("open"); });
+ document.addEventListener("click", function (e) {
+ if (sidebar.classList.contains("open") && !e.target.closest(".sidebar") && !e.target.closest(".menu-btn")) {
+ sidebar.classList.remove("open");
+ }
+ });
+ }
+})();
diff --git a/apps/server/internal/docs/content/01-intro.md b/apps/server/internal/docs/content/01-intro.md
new file mode 100644
index 0000000..18016cb
--- /dev/null
+++ b/apps/server/internal/docs/content/01-intro.md
@@ -0,0 +1,40 @@
+OpsxDB 是一款**本地优先**的桌面数据库工作台,面向运维与开发:连库、写 SQL、看结果,全在本机完成。
+
+主交付形态是可安装的桌面应用(Tauri 2),本地 Go 后端随应用自动启动;同一套界面也可用单个二进制在 Web / 内网部署。数据全部保存在本机,不经过任何第三方服务器。
+
+## 功能总览
+
+| 能力 | 说明 |
+| --- | --- |
+| 多驱动连库 | MySQL 族 / PostgreSQL 族 / SQL Server / Oracle / SQLite / Redis,native Go 驱动编译内置 |
+| 连接选择器 | 搜索 / 分类 / 最近 / 图标网格;粘贴连接 URL 一键填入表单 |
+| SQL 工作台 | CodeMirror 编辑器、⌘/Ctrl+Enter 执行语句或选区、多 Tab、可拖分栏 |
+| 对象树 | 懒加载库 / 表 / 列;点表名直接看前 100 行 |
+| 结果处理 | 一键复制 / CSV / 生成 INSERT;双击单元格复制;截断有提示 |
+| Redis 工作台 | 编辑器执行 PING / GET / SET / DEL / KEYS,键空间浏览 |
+| 安全通道 | 每连接独立配置 SSL/TLS 与 SSH 隧道;密码本机混淆存储 |
+| AI 面板 | 生成 / 解释 / 优化 / 修复 / 方言转换 / 样例 SQL |
+| 危险操作防护 | DROP / TRUNCATE / 无 WHERE 的 UPDATE·DELETE 执行前确认 |
+| 双形态 | 桌面 GUI(Tauri 2)与 Web(单二进制 + 静态页)共用一套界面 |
+
+## 支持的数据库
+
+| 家族 | 覆盖 | 默认端口 |
+| --- | --- | --- |
+| MySQL 族 | MySQL · MariaDB · TiDB · OceanBase · PolarDB · TDSQL | 3306 |
+| PostgreSQL 族 | PostgreSQL · GaussDB / openGauss · 人大金仓 | 5432 |
+| SQL Server | 原生协议直连 | 1433 |
+| Oracle | 纯 Go 驱动 go-ora(不引入 JVM) | 1521 |
+| SQLite | 本地文件库 | — |
+| Redis | 命令执行 + 键管理 | 6379 |
+
+> **提示**:达梦 DM8、MongoDB、ZooKeeper、Elasticsearch、InfluxDB 已列入路线,规划 agent 按需接入。
+
+## 为什么选 OpsxDB
+
+- **数据主权**:连接配置与查询历史只存在本机;后端默认仅监听 127.0.0.1。
+- **开箱即用**:`./run.sh web` 一条命令起 Web 形态;桌面版双击即用,后端随应用拉起。
+- **为快设计**:点表即看数据、⌘Enter 即执行、结果三键取走(复制 / CSV / INSERT)。
+- **轻量克制**:一个 Go 二进制 + 一份前端,内网可部署,无遥测无账号。
+
+> **提示**:想马上动手?直接看下一章《快速开始》,30 秒连上第一个库。
diff --git a/apps/server/internal/docs/content/02-quickstart.md b/apps/server/internal/docs/content/02-quickstart.md
new file mode 100644
index 0000000..f21079a
--- /dev/null
+++ b/apps/server/internal/docs/content/02-quickstart.md
@@ -0,0 +1,61 @@
+本章从零到出结果:起服务 → 建连接 → 执行第一条 SQL。
+
+## 1. 启动
+
+桌面版(主交付形态)双击 OpsxDB 应用即可,本地后端随应用自动拉起。
+
+从源码跑 Web / 桌面形态:
+
+```bash
+git clone opsxdb && cd opsxdb
+npm install
+
+./run.sh web # Web 形态:Gin :8788 + Vite 开发壳 :5174
+./run.sh gui # 桌面形态:Tauri 2 窗口(复用同一套界面)
+```
+
+> **注意**:浏览器里的 `:5174` 是 Vite 热更新开发壳,仅调试用;产品页面(官网 `/`、文档 `/docs`)由后端 `:8788` 直接提供。
+
+## 2. 新建连接
+
+1. 打开应用,进入**连接选择器**,选择数据库类型(如 MySQL);
+2. 或者直接粘贴连接 URL,点「填入」自动拆解字段:
+
+ ```
+ mysql://root:pass@127.0.0.1:3306/shop
+ ```
+
+3. 点 **测试** 验证连通性,再点 **保存并连接**。
+
+连接配置保存在本机,密码混淆存储;后续查询只携带会话 ID,不再上送账密。
+
+## 3. 第一次查询
+
+1. 左侧对象树展开库,**点表名**直接查看前 100 行(展开表可看列结构);
+2. 打开 SQL 编辑器,输入:
+
+ ```sql
+ SELECT * FROM orders ORDER BY id DESC LIMIT 100;
+ ```
+
+3. 按 **⌘/Ctrl + Enter** 执行当前语句(或选中的片段);
+4. 结果面板支持**复制 / 导出 CSV / 生成 INSERT**,双击单元格即复制。
+
+## 4. 验证服务
+
+```bash
+curl -s http://127.0.0.1:8788/health
+# {"ok":true,"service":"opsxdb"}
+```
+
+## 5. 手边没有数据库?
+
+本地还没有可连的库?用仓库自带的辅助脚本一键起一个(需要 Docker,一次只起一个):
+
+```bash
+./tests/dbctl.sh up a # MySQL
+./tests/dbctl.sh up c # PostgreSQL(用户 opsx)
+./tests/dbctl.sh up f # Redis
+```
+
+各驱动的验收状态见《驱动支持》。
diff --git a/apps/server/internal/docs/content/03-interface.md b/apps/server/internal/docs/content/03-interface.md
new file mode 100644
index 0000000..1a0a30f
--- /dev/null
+++ b/apps/server/internal/docs/content/03-interface.md
@@ -0,0 +1,40 @@
+OpsxDB 的界面为「高频操作」设计:从打开应用到看到数据,路径越短越好。
+
+## 连接选择器
+
+启动后的第一个界面:
+
+- **搜索**:按名称快速过滤已保存的连接;
+- **分类**:关系型 / NoSQL / 国产库 / 时序·搜索 分组浏览;
+- **最近**:最近使用的连接排在最前;
+- **图标网格**:按数据库类型直选,未就绪的类型会标注「未就绪」(仅可保存配置);
+- **URL 填入**:粘贴 `mysql://user:pass@host:3306/db` 点「填入」,字段自动拆解。
+
+## 工作台布局
+
+连接成功后进入工作台,三栏结构:
+
+| 区域 | 内容 |
+| --- | --- |
+| 左侧 | 连接列表 + 对象树(库 → 表 → 列,懒加载) |
+| 中部 | SQL 编辑器(多 Tab)+ 结果面板,可拖动分栏 |
+| 右侧 | AI 面板(生成 / 解释 / 优化 / 修复 / 方言 / 样例) |
+
+## 对象树
+
+- 点**表名**:直接打开前 100 行数据;
+- 展开**表**:查看列结构;
+- 库列表懒加载,大实例也不卡。
+
+## 编辑器与结果
+
+- CodeMirror 语法高亮,⌘/Ctrl+Enter 执行**当前语句或选区**(不是整页脚本);
+- 多 Tab 并行查询,互不阻塞;
+- 结果面板:复制 / CSV / INSERT 三种取数方式,双击单元格复制;
+- 超过截断阈值时结果有明确提示。
+
+## 个性化
+
+- 深色 / 浅色主题切换;
+- Tab 布局与主题选择持久化,下次打开保持原样;
+- 查询历史本地留存,可回跳重跑。
diff --git a/apps/server/internal/docs/content/04-connect.md b/apps/server/internal/docs/content/04-connect.md
new file mode 100644
index 0000000..75f5e76
--- /dev/null
+++ b/apps/server/internal/docs/content/04-connect.md
@@ -0,0 +1,42 @@
+本章说明各数据库类型的连接表单、URL 格式与高级选项(SSL / SSH 隧道)。
+
+## 通用流程
+
+1. 连接选择器点「新建」或选某个数据库类型;
+2. 填写表单(不同驱动表单不同,见下表);
+3. **测试** → 通过后 **保存并连接**。
+
+支持直接粘贴 URL 自动填表:
+
+```
+mysql://root:pass@127.0.0.1:3306/shop
+postgres://opsx:pass@127.0.0.1:5432/app
+```
+
+## 各驱动表单
+
+| 驱动 | 表单 | 特有字段 |
+| --- | --- | --- |
+| MySQL / PostgreSQL / SQL Server | standard | host · port · user · password · database · ssl · ssh |
+| Oracle | oracle | host · port · user · password · **service**(如 ORCL) |
+| SQLite | file | database 填本地文件路径(如 `~/data/demo.sqlite`) |
+| Redis | redis | host · port · password · database 序号 |
+
+表单只收集服务端实际消费的字段;历史遗留的死参数不会出现在表单里。
+
+## MySQL 族与 PostgreSQL 族
+
+协议兼容的国产库无需单独驱动,直接复用家族驱动:
+
+- **MySQL 族**:MariaDB(3307)、TiDB(4000)、OceanBase(2881)、PolarDB / TDSQL(3306)—— 表单里选对应类型即可;
+- **PostgreSQL 族**:GaussDB / openGauss、人大金仓 Kingbase(54321)—— 走 postgres 表单,注意默认库与用户名差异。
+
+## SSL / TLS
+
+连接表单中开启 **ssl** 即可;按连接独立配置,互不影响。内网自签证书场景按需放宽校验策略。
+
+## SSH 隧道
+
+数据库在跳板机后面时,开启 **ssh** 字段组,填写跳板机地址与凭据:OpsxDB 会先建 SSH 隧道,再把数据库连接走隧道转发,无需把库端口暴露到公网。
+
+> **提示**:连接保存后,后续查询仅携带 `sessionId`,账密不会随每次请求上送;密码在本机混淆存储。
diff --git a/apps/server/internal/docs/content/05-drivers.md b/apps/server/internal/docs/content/05-drivers.md
new file mode 100644
index 0000000..d4bcd94
--- /dev/null
+++ b/apps/server/internal/docs/content/05-drivers.md
@@ -0,0 +1,41 @@
+OpsxDB 的常用数据库驱动以 native Go 驱动编译内置,开箱即用;更多数据库在路线中。
+
+## 内置驱动
+
+| 驱动 | 连接库 | 说明 |
+| --- | --- | --- |
+| mysql | MySQL · MariaDB · TiDB · OceanBase · PolarDB · TDSQL | 协议族复用,默认端口 3306 |
+| postgres | PostgreSQL · GaussDB / openGauss · Kingbase | 协议族复用,默认端口 5432 |
+| sqlserver | SQL Server | 原生协议,默认端口 1433 |
+| oracle | Oracle | go-ora 纯 Go 驱动,不引入 JVM,端口 1521 |
+| sqlite | SQLite | 本地文件库,无端口 |
+| redis | Redis | 默认端口 6379 |
+
+## 支持状态
+
+以下为逐项实测的结果 —— 你可以按这个表判断「现在就能连什么」:
+
+| 数据库 | 连接 | 库表浏览 | 数据操作 | 备注 |
+| --- | --- | --- | --- | --- |
+| MySQL / MariaDB | ✅ | ✅ | ✅ | MariaDB 默认端口 3307 |
+| PostgreSQL | ✅ | ✅ | ✅ | GaussDB / 金仓同样适用 |
+| SQL Server | ✅ | ✅ | ✅ | Apple Silicon 上连接稍慢属正常 |
+| SQLite | ✅ | ✅ | ✅ | 填本地文件路径即连 |
+| Redis | ✅ | ✅ | ✅ | 支持命令执行与键管理 |
+| Oracle | 代码就绪 | ✅ | 待实测 | 见下方说明 |
+
+> **说明(Oracle)**:驱动已内置(纯 Go 实现,无需安装 Java),库表浏览可用;完整数据操作正在验收中 —— 在 Apple Silicon 的容器环境会遇到 Oracle 限制(ORA-00442),x86 环境或真实数据库不受影响。
+
+## 暂不支持(规划中)
+
+| 数据库 | 状态 |
+| --- | --- |
+| 达梦 DM8 | 规划中,暂不可用 |
+| MongoDB | 规划中,暂不可用 |
+| ZooKeeper / Elasticsearch / InfluxDB | 规划中,暂不可用 |
+
+> **注意**:选择器中标注「未就绪」的类型仍可保存连接配置,但不能测试 / 连接;开放支持后会随版本更新自动可用。
+
+## 后续驱动的接入方式
+
+常用库继续以原生驱动内置;小众数据库(达梦、MongoDB 等)将通过**按需下载**方式接入 —— 首次使用时下载对应驱动组件,版本固定并校验完整性(sha256),不随包体膨胀。
diff --git a/apps/server/internal/docs/content/06-workbench.md b/apps/server/internal/docs/content/06-workbench.md
new file mode 100644
index 0000000..c858fac
--- /dev/null
+++ b/apps/server/internal/docs/content/06-workbench.md
@@ -0,0 +1,46 @@
+SQL 工作台是日常使用率最高的界面,所有交互围绕「快」设计。
+
+## 执行模型
+
+- **⌘/Ctrl + Enter**:执行光标所在的**当前语句**;有选区时执行**选区**;
+- 不是「整页脚本」模式 —— 多条语句互不干扰,想跑哪句把光标放哪;
+- 执行在会话上运行,连接断开后自动提示。
+
+## 多 Tab 与分栏
+
+- 每个查询一个 Tab,并行执行互不阻塞;
+- 编辑器 / 结果区可拖动分栏,长查询时编辑器不被结果挤占;
+- Tab 与布局持久化,重开应用保持原样。
+
+## 对象树联动
+
+- 左侧对象树懒加载库 / 表 / 列;
+- **点表名 = 查询前 100 行**,自动生成对应 SQL;
+- 展开表看列名与类型,写 SQL 时可对照。
+
+## 查询历史
+
+- 本地留存最近 200 条(不上传、不同步);
+- **相同语句自动去重置顶** —— 历史即「最近用过的不同语句」,高频查询永远在第一条;
+- 点击条目回填编辑器;悬停出现 **▶** 按钮,回填并直接重跑;
+- 顶部过滤框支持按语句内容或连接名搜索;
+- 条目显示时间、连接名、行数与耗时;失败的语句红色标出。
+
+## 危险 SQL 防护
+
+执行以下语句前会弹确认框:
+
+| 类型 | 规则 |
+| --- | --- |
+| 删除对象 | DROP / TRUNCATE |
+| 全表更新 | 无 WHERE 的 UPDATE / DELETE |
+
+> **注意**:确认框只拦「明确危险」的模式,不替代事务与备份习惯;生产库操作请配合审批流程。
+
+## 快捷键
+
+| 快捷键 | 动作 |
+| --- | --- |
+| ⌘/Ctrl + Enter | 执行当前语句 / 选区 |
+| 双击结果单元格 | 复制单元格内容 |
+| Esc | 关闭弹层 / 取消确认 |
diff --git a/apps/server/internal/docs/content/07-results.md b/apps/server/internal/docs/content/07-results.md
new file mode 100644
index 0000000..a9858f3
--- /dev/null
+++ b/apps/server/internal/docs/content/07-results.md
@@ -0,0 +1,27 @@
+查询跑完只是开始,把结果带走才算完成 —— OpsxDB 给了三条最快路径。
+
+## 三种取数方式
+
+| 方式 | 用途 |
+| --- | --- |
+| **复制** | 直接进剪贴板,贴到群聊 / 文档 / 表格 |
+| **CSV** | 导出标准 CSV 文件,Excel / Numbers 直接打开 |
+| **INSERT** | 把结果行生成为 INSERT 语句,直接可在别的库回放 |
+
+## 单元格操作
+
+- **双击单元格**:复制该单元格内容;
+- 列头即字段名,长文本自动截断显示,完整内容在复制时保留。
+
+## 截断提示
+
+为保护界面与内存,超大结果集会按阈值截断展示:
+
+- 结果面板顶部显示「已截断」提示与实际行数;
+- 需要完整数据时,缩小查询范围(加 LIMIT / WHERE)或使用导出。
+
+> **提示**:默认「点表名看前 100 行」就是截断策略的一部分 —— 快速浏览用 100 行,精确取数写 WHERE。
+
+## 与 AI 面板联动
+
+写不出来的查询可以直接在 AI 面板用自然语言描述(「查最近 7 天下单但未支付的用户」),生成 SQL 后放回编辑器执行,见《AI 助手》。
diff --git a/apps/server/internal/docs/content/08-redis.md b/apps/server/internal/docs/content/08-redis.md
new file mode 100644
index 0000000..f6b8e97
--- /dev/null
+++ b/apps/server/internal/docs/content/08-redis.md
@@ -0,0 +1,34 @@
+Redis 连接使用专用表单(redis),连上后既是命令行又是键空间浏览器。
+
+## 命令执行
+
+SQL 编辑器同样适用于 Redis —— 直接输入命令,⌘/Ctrl + Enter 执行:
+
+```bash
+PING
+SET demo:key "hello" EX 60
+GET demo:key
+KEYS demo:*
+DEL demo:key
+```
+
+- 返回值直接显示在结果面板;
+- 集群 / 哨兵模式按各自连接方式配置(哨兵填写主节点名)。
+
+## 键空间浏览
+
+- **KEYS 扫描**:按模式浏览键(生产大库建议用前缀收窄范围);
+- 键类型视图:string / hash / list / set / zset 分类型展示;
+- **TTL 管理**:查看与设置过期时间,常用于缓存排障;
+- 批量操作:选中多个键批量删除。
+
+> **注意**:`KEYS *` 在大库上会阻塞 Redis,OpsxDB 不做强制拦截 —— 生产环境请自觉使用带前缀的模式。
+
+## 连接参数
+
+| 字段 | 说明 |
+| --- | --- |
+| host / port | 默认 127.0.0.1:6379 |
+| password | ACL / requirepass |
+| database | 逻辑库序号(0-15 常见) |
+| ssl | 红色连接(TLS)开关 |
diff --git a/apps/server/internal/docs/content/09-security.md b/apps/server/internal/docs/content/09-security.md
new file mode 100644
index 0000000..2eaaffa
--- /dev/null
+++ b/apps/server/internal/docs/content/09-security.md
@@ -0,0 +1,35 @@
+连生产库的工具,安全必须是默认项。OpsxDB 的安全设计围绕一句话:**数据不出你的机器。**
+
+## 本地优先
+
+- 连接配置、查询历史、主题偏好全部保存在本机;
+- 无账号体系、无遥测、无云端同步;
+- 后端默认只监听 `127.0.0.1`,对外服务需要显式配置(`OPSXDB_BIND_PUBLIC=1` 或 `OPSXDB_ADDR`)。
+
+## 凭据保护
+
+| 层面 | 机制 |
+| --- | --- |
+| 存储 | 连接密码在本机混淆存储,不落明文 |
+| 传输 | 连接建立后,后续查询只携带 `sessionId`,账密不再每次上送 |
+| 会话 | 会话在服务端维护,可主动关闭(连接断开即失效) |
+
+## 加密通道
+
+**SSL / TLS**:每个连接可独立开启;数据库支持 TLS 即可启用(MySQL、PostgreSQL、SQL Server、Redis、MongoDB 均支持)。
+
+**SSH 隧道**:数据库在跳板机后时,配置 SSH 字段组,OpsxDB 先建隧道再转发数据库连接 —— 库端口不必暴露公网。
+
+## 危险操作防护
+
+- DROP / TRUNCATE 执行前确认;
+- 无 WHERE 的 UPDATE / DELETE 执行前确认;
+- 结果集按阈值截断,避免误操作拖垮界面。
+
+## 内网部署形态
+
+单二进制 + 内置静态页(官网 / 文档 / 工作台),无外部依赖:
+
+- 任意内网主机可部署;
+- 无外联请求,可在纯内网环境运行;
+- 升级即替换一个二进制。
diff --git a/apps/server/internal/docs/content/10-ai.md b/apps/server/internal/docs/content/10-ai.md
new file mode 100644
index 0000000..5cfdd55
--- /dev/null
+++ b/apps/server/internal/docs/content/10-ai.md
@@ -0,0 +1,32 @@
+AI 面板的目标:让「写不出的 SQL」和「看不懂的 SQL」不再阻塞你。
+
+## 六种动作
+
+| 动作 | 用途 |
+| --- | --- |
+| 生成 | 自然语言 → SQL(「查最近 7 天下单但未支付的用户」) |
+| 解释 | 把一段复杂 SQL 翻译成中文说明 |
+| 优化 | 分析慢查询,给出索引 / 改写建议 |
+| 修复 | 报错信息 + SQL → 修正版本 |
+| 方言 | MySQL ↔ PostgreSQL 等跨方言转换 |
+| 样例 | 按表结构生成示例查询 |
+
+## 使用方式
+
+1. 连接库后打开右侧 AI 面板;
+2. 选择动作,输入需求或粘贴 SQL(可附上对象树上下文);
+3. 生成结果一键放回编辑器,⌘/Ctrl + Enter 执行。
+
+> **提示**:生成的 SQL 依旧走危险语句确认流程 —— AI 产出与手写 SQL 同等对待,不放宽防护。
+
+## 当前状态(v0.1)
+
+AI 面板当前为**本地 stub**:六种动作的交互框架与 API 已就位(`POST /api/ai/chat`),模型服务接入版本会明确标注数据流向与开关位置。
+
+## 数据流向承诺
+
+接入模型服务时遵循三条原则:
+
+- 默认只发送你明确提交的内容(不含连接凭据);
+- 可完全关闭 AI 功能;
+- 使用云端模型时在界面上明确标识。
diff --git a/apps/server/internal/docs/content/11-faq.md b/apps/server/internal/docs/content/11-faq.md
new file mode 100644
index 0000000..43721c3
--- /dev/null
+++ b/apps/server/internal/docs/content/11-faq.md
@@ -0,0 +1,40 @@
+## 产品形态
+
+**Q:浏览器里的 5174 端口是什么?**
+Vite 热更新开发壳,仅开发调试用。产品形态是桌面应用(Tauri 2)或自托管 Web 服务,页面与 API 由后端 `:8788` 提供。
+
+**Q:桌面版和 Web 版功能有差别吗?**
+没有。同一套 UI、同一份后端,桌面版只是多了应用壳(自动拉起本地后端、原生窗口、安装包形态)。
+
+**Q:怎么打安装包?**
+`./run.sh package` 产出 Tauri 安装包(DMG)并内嵌 sidecar 后端二进制。
+
+## 数据与安全
+
+**Q:我的连接密码存在哪?**
+本机,混淆存储;查询只带会话 ID,密码不会随请求重复上送,也不会同步到云端。
+
+**Q:会上传我的 SQL 或查询结果吗?**
+不会。没有遥测、没有账号、没有云依赖;AI 功能当前为本地 stub,接入模型服务时会明确标注数据流向。
+
+**Q:能在公司内网用吗?**
+能。单二进制 + 内置静态页,无外部依赖;默认只监听回环地址,对外需显式配置。
+
+## 驱动相关
+
+**Q:Oracle 能用吗?**
+驱动代码就绪(go-ora 纯 Go 实现,无需 JVM),但本机 docker 验收在 Apple Silicon 上受镜像限制未通过(ORA-00442),需 x86 环境或真库完成验收,详见《驱动支持》。
+
+**Q:达梦 / MongoDB 什么时候支持?**
+已列入路线:长尾库(达梦、MongoDB、ZooKeeper、ES、InfluxDB)规划 agent 按需接入,驱动分发走独立仓库并校验 sha256。
+
+**Q:MySQL 族 / PG 族的国产库怎么连?**
+协议兼容直接复用家族驱动:TiDB / OceanBase / PolarDB / TDSQL 选对应类型(MySQL 协议);GaussDB / Kingbase 走 PostgreSQL 表单。注意各自默认端口与用户名差异。
+
+## 其他
+
+**Q:和 OpsxAPI 是什么关系?**
+同一团队的姊妹项目:OpsxAPI 是接口工作台,OpsxDB 是数据库工作台,共用一套 monorepo 脚手架(web + Gin + 可选 Tauri),产品面各自独立。
+
+**Q:快捷键有哪些?**
+核心就一个:⌘/Ctrl + Enter 执行当前语句或选区;双击结果单元格复制。完整列表见《SQL 工作台》。
diff --git a/apps/server/internal/docs/content/connect.html b/apps/server/internal/docs/content/connect.html
deleted file mode 100644
index 70583b7..0000000
--- a/apps/server/internal/docs/content/connect.html
+++ /dev/null
@@ -1,7 +0,0 @@
-连库入门
-在连库页选择驱动,填写主机 / 端口 / 账号,测试通过后保存并打开。
-
-- 密码仅存本机(本地加密存储)。
-- 会话由本地后端维护;后端未启动时顶部会提示,不会白屏。
-- 断线后可在底栏重连,恢复当前库上下文。
-
\ No newline at end of file
diff --git a/apps/server/internal/docs/content/faq.html b/apps/server/internal/docs/content/faq.html
deleted file mode 100644
index 12ba477..0000000
--- a/apps/server/internal/docs/content/faq.html
+++ /dev/null
@@ -1,7 +0,0 @@
-常见问题
-打开后提示本地后端未就绪?
-确认应用已拉起 sidecar;或手动 ./run.sh server 后重试。默认只监听 127.0.0.1:8788。
-浏览器也能用吗?
-可以调试,但产品说明与对外沟通应以桌面版为准,不要暗示 Web 即产品。
-官网 / 文档在哪?
-本地后端提供:http://127.0.0.1:8788/(官网)、/docs(文档)。
\ No newline at end of file
diff --git a/apps/server/internal/docs/content/install.html b/apps/server/internal/docs/content/install.html
deleted file mode 100644
index 550a417..0000000
--- a/apps/server/internal/docs/content/install.html
+++ /dev/null
@@ -1,13 +0,0 @@
-安装与启动
-OpsxDB 正式形态是桌面 GUI(Tauri)。开发态浏览器 :5174 只用于调试,不是产品交付。
-桌面版
-
-- 安装本机构建产物(macOS
.dmg / .app,或 npm run build:desktop 产出)。
-- 双击启动:应用会拉起本地后端(默认
127.0.0.1:8788),进入工作台。
-- 无需手动执行
./run.sh web。
-
-开发调试
-./run.sh gui # 或 ./run.sh web
-
-开发壳地址:http://127.0.0.1:5174。
-改后端(apps/server)后请执行 ./run.sh restart(或等价重启 Gin),避免 :8788 仍跑旧二进制。
diff --git a/apps/server/internal/docs/content/manifest.json b/apps/server/internal/docs/content/manifest.json
new file mode 100644
index 0000000..2f6834a
--- /dev/null
+++ b/apps/server/internal/docs/content/manifest.json
@@ -0,0 +1,41 @@
+{
+ "productVersion": "0.1",
+ "groups": [
+ {
+ "name": "开始使用",
+ "pages": [
+ { "file": "01-intro.md", "slug": "intro", "title": "产品介绍", "updated": "2026-09-20" },
+ { "file": "02-quickstart.md", "slug": "quickstart", "title": "快速开始", "updated": "2026-09-20" },
+ { "file": "03-interface.md", "slug": "interface", "title": "界面导览", "updated": "2026-09-20" }
+ ]
+ },
+ {
+ "name": "连接与驱动",
+ "pages": [
+ { "file": "04-connect.md", "slug": "connect", "title": "新建连接", "updated": "2026-09-20" },
+ { "file": "05-drivers.md", "slug": "drivers", "title": "驱动支持", "updated": "2026-09-20" }
+ ]
+ },
+ {
+ "name": "SQL 工作台",
+ "pages": [
+ { "file": "06-workbench.md", "slug": "workbench", "title": "SQL 工作台", "updated": "2026-09-20" },
+ { "file": "07-results.md", "slug": "results", "title": "结果与导出", "updated": "2026-09-20" },
+ { "file": "08-redis.md", "slug": "redis", "title": "Redis 工作台", "updated": "2026-09-20" }
+ ]
+ },
+ {
+ "name": "安全与 AI",
+ "pages": [
+ { "file": "09-security.md", "slug": "security", "title": "安全设计", "updated": "2026-09-20" },
+ { "file": "10-ai.md", "slug": "ai", "title": "AI 助手", "updated": "2026-09-20" }
+ ]
+ },
+ {
+ "name": "参考",
+ "pages": [
+ { "file": "11-faq.md", "slug": "faq", "title": "常见问题", "updated": "2026-09-20" }
+ ]
+ }
+ ]
+}
diff --git a/apps/server/internal/docs/content/workbench.html b/apps/server/internal/docs/content/workbench.html
deleted file mode 100644
index c617bec..0000000
--- a/apps/server/internal/docs/content/workbench.html
+++ /dev/null
@@ -1,7 +0,0 @@
-工作台简介
-深色密度布局:左侧连接与对象树,中间编辑器与多结果 Tab,可选 AI 侧栏。
-
-- 多结果集:成功结果保留;失败不盖掉已有成功集。
-- 执行计划:有则展示,无则禁用。
-- 书签:紧凑列表,回填 SQL,本地持久化。
-
\ No newline at end of file
diff --git a/apps/server/internal/docs/docs.go b/apps/server/internal/docs/docs.go
index ab90030..d26848b 100644
--- a/apps/server/internal/docs/docs.go
+++ b/apps/server/internal/docs/docs.go
@@ -1,89 +1,264 @@
-// Package docs:用户文档骨架(嵌入 HTML,侧栏+正文密度对齐工作台)。
+// Package docs 在 /docs 路径提供产品用户文档(Markdown 嵌入二进制,SSR 渲染)。
package docs
import (
"embed"
+ "encoding/json"
+ "fmt"
"net/http"
+ "path"
+ "regexp"
"strings"
"github.com/gin-gonic/gin"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ gmhtml "github.com/yuin/goldmark/renderer/html"
)
//go:embed content assets template.html
var files embed.FS
-type page struct {
+type PageDef struct {
+ File string `json:"file"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Updated string `json:"updated,omitempty"`
+}
+
+type GroupDef struct {
+ Name string `json:"name"`
+ Pages []PageDef `json:"pages"`
+}
+
+type Manifest struct {
+ ProductVersion string `json:"productVersion"`
+ Groups []GroupDef `json:"groups"`
+}
+
+// TocItem 本页目录项
+type TocItem struct {
+ ID string `json:"id"`
+ Level int `json:"level"`
+ Text string `json:"text"`
+}
+
+type SearchEntry struct {
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Group string `json:"group"`
+ Text string `json:"text"`
+}
+
+type NavLink struct {
Slug string
Title string
- Body string
}
-var order = []page{
- {Slug: "install", Title: "安装与启动"},
- {Slug: "connect", Title: "连库入门"},
- {Slug: "workbench", Title: "工作台简介"},
- {Slug: "faq", Title: "常见问题"},
+type Page struct {
+ Slug string
+ Title string
+ Group string
+ Updated string
+ HTML string
+ TOC []TocItem
+ Prev *NavLink
+ Next *NavLink
}
+// Handler 文档服务
type Handler struct {
- tpl string
+ manifest Manifest
+ pages map[string]*Page
+ order []string
+ searchData []byte
+ tpl string
}
func New() *Handler {
- raw, err := files.ReadFile("template.html")
+ h := &Handler{pages: map[string]*Page{}}
+ raw, err := files.ReadFile("content/manifest.json")
+ if err != nil {
+ panic("docs: manifest.json 缺失: " + err.Error())
+ }
+ if err := json.Unmarshal(raw, &h.manifest); err != nil {
+ panic("docs: manifest.json 解析失败: " + err.Error())
+ }
+ tplRaw, err := files.ReadFile("template.html")
if err != nil {
panic("docs: template.html 缺失: " + err.Error())
}
- return &Handler{tpl: string(raw)}
+ h.tpl = string(tplRaw)
+
+ md := goldmark.New(
+ goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList),
+ goldmark.WithRendererOptions(gmhtml.WithHardWraps()),
+ )
+
+ search := []SearchEntry{}
+ var prev *NavLink
+ for _, group := range h.manifest.Groups {
+ for _, def := range group.Pages {
+ src, err := files.ReadFile("content/" + def.File)
+ if err != nil {
+ panic("docs: 缺少内容文件 " + def.File)
+ }
+ var buf strings.Builder
+ if err := md.Convert(src, &buf); err != nil {
+ panic("docs: 渲染失败 " + def.File + ": " + err.Error())
+ }
+ body, toc := injectHeadingIDs(buf.String())
+ page := &Page{
+ Slug: def.Slug,
+ Title: def.Title,
+ Group: group.Name,
+ Updated: def.Updated,
+ HTML: body,
+ TOC: toc,
+ Prev: prev,
+ }
+ h.pages[def.Slug] = page
+ h.order = append(h.order, def.Slug)
+ prev = &NavLink{Slug: def.Slug, Title: def.Title}
+ search = append(search, SearchEntry{
+ Slug: def.Slug,
+ Title: def.Title,
+ Group: group.Name,
+ Text: plainText(string(src)),
+ })
+ }
+ }
+ // 下一页链接
+ for i, slug := range h.order {
+ if i+1 < len(h.order) {
+ next := h.pages[h.order[i+1]]
+ h.pages[slug].Next = &NavLink{Slug: next.Slug, Title: next.Title}
+ }
+ }
+ searchJSON, _ := json.Marshal(search)
+ h.searchData = searchJSON
+ return h
}
+// Register 注册路由(单一 catch-all,避免静态/参数段冲突)
func (h *Handler) Register(r *gin.Engine) {
- r.GET("/docs", func(c *gin.Context) { c.Redirect(http.StatusFound, "/docs/install") })
- r.GET("/docs/:slug", h.Page)
- r.GET("/docs/assets/*file", h.Asset)
+ r.GET("/docs", func(c *gin.Context) {
+ c.Redirect(http.StatusFound, "/docs/"+h.order[0])
+ })
+ r.GET("/docs/*path", h.dispatch)
}
+func (h *Handler) dispatch(c *gin.Context) {
+ p := strings.Trim(c.Param("path"), "/")
+ switch {
+ case p == "":
+ c.Redirect(http.StatusFound, "/docs/"+h.order[0])
+ case p == "search-data.json":
+ h.SearchData(c)
+ case strings.HasPrefix(p, "assets/"):
+ c.Params = []gin.Param{{Key: "file", Value: "/" + strings.TrimPrefix(p, "assets")}}
+ h.Asset(c)
+ default:
+ // 取第一段作为 slug(忽略子路径)
+ slug := p
+ if i := strings.IndexByte(p, '/'); i >= 0 {
+ slug = p[:i]
+ }
+ c.Params = []gin.Param{{Key: "slug", Value: slug}}
+ h.Page(c)
+ }
+}
+
+// Page 渲染文档页
func (h *Handler) Page(c *gin.Context) {
slug := c.Param("slug")
- bodyBytes, err := files.ReadFile("content/" + slug + ".html")
- if err != nil {
+ page, ok := h.pages[slug]
+ if !ok {
c.Status(http.StatusNotFound)
+ _, _ = c.Writer.WriteString(notFoundHTML(h.order[0])) //nolint:errcheck
return
}
- title := slug
- nav := strings.Builder{}
- for _, p := range order {
- cls := "nav-link"
- if p.Slug == slug {
- cls += " active"
- title = p.Title
- }
- nav.WriteString(`` + p.Title + ``)
- }
- html := h.tpl
- html = strings.ReplaceAll(html, "{{TITLE}}", title)
- html = strings.ReplaceAll(html, "{{NAV}}", nav.String())
- html = strings.ReplaceAll(html, "{{BODY}}", string(bodyBytes))
c.Header("Content-Type", "text/html; charset=utf-8")
c.Header("Cache-Control", "no-cache")
- _, _ = c.Writer.WriteString(html)
+ _, _ = c.Writer.WriteString(renderPage(h, page))
}
+// SearchData 搜索索引
+func (h *Handler) SearchData(c *gin.Context) {
+ c.Data(http.StatusOK, "application/json; charset=utf-8", h.searchData)
+}
+
+// Asset 静态资源
func (h *Handler) Asset(c *gin.Context) {
- name := strings.TrimPrefix(c.Param("file"), "/")
+ name := path.Clean(c.Param("file"))
if strings.Contains(name, "..") {
c.Status(http.StatusBadRequest)
return
}
- data, err := files.ReadFile("assets/" + name)
+ data, err := files.ReadFile("assets" + name)
if err != nil {
c.Status(http.StatusNotFound)
return
}
- ct := "application/octet-stream"
- if strings.HasSuffix(name, ".css") {
- ct = "text/css; charset=utf-8"
+ switch {
+ case strings.HasSuffix(name, ".css"):
+ c.Header("Content-Type", "text/css; charset=utf-8")
+ case strings.HasSuffix(name, ".js"):
+ c.Header("Content-Type", "application/javascript; charset=utf-8")
+ case strings.HasSuffix(name, ".svg"):
+ c.Header("Content-Type", "image/svg+xml")
+ default:
+ c.Header("Content-Type", "application/octet-stream")
}
- c.Header("Content-Type", ct)
- c.Data(http.StatusOK, ct, data)
+ c.Header("Cache-Control", "public, max-age=3600")
+ c.Data(http.StatusOK, "", data)
+}
+
+// ManifestExport 供测试使用
+func (h *Handler) ManifestExport() Manifest { return h.manifest }
+
+func (h *Handler) FirstSlug() string { return h.order[0] }
+
+var (
+ hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)`)
+ tagRe = regexp.MustCompile(`<[^>]+>`)
+)
+
+// injectHeadingIDs 给 h2/h3 加锚点 id,并产出目录
+func injectHeadingIDs(html string) (string, []TocItem) {
+ var toc []TocItem
+ counter := 0
+ out := hRe.ReplaceAllStringFunc(html, func(m string) string {
+ sub := hRe.FindStringSubmatch(m)
+ tag, inner := sub[1], sub[2]
+ counter++
+ id := fmt.Sprintf("sec-%d", counter)
+ text := strings.TrimSpace(tagRe.ReplaceAllString(inner, ""))
+ level := 2
+ if tag == "h3" {
+ level = 3
+ }
+ toc = append(toc, TocItem{ID: id, Level: level, Text: text})
+ return fmt.Sprintf(`<%s id="%s">%s%s>`, tag, id, inner, tag)
+ })
+ return out, toc
+}
+
+var mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+")
+
+// plainText 供搜索索引的纯文本
+func plainText(src string) string {
+ s := mdNoise.ReplaceAllString(src, " ")
+ s = strings.Join(strings.Fields(s), " ")
+ if len(s) > 6000 {
+ s = s[:6000]
+ }
+ return s
+}
+
+func notFoundHTML(home string) string {
+ return fmt.Sprintf(`404 · OpsxDB 文档
+
+
+`, home)
}
diff --git a/apps/server/internal/docs/render.go b/apps/server/internal/docs/render.go
new file mode 100644
index 0000000..9f464ab
--- /dev/null
+++ b/apps/server/internal/docs/render.go
@@ -0,0 +1,25 @@
+package docs
+
+import (
+ "html/template"
+ "strings"
+)
+
+func renderPage(h *Handler, p *Page) string {
+ tpl, err := template.New("doc").Parse(h.tpl)
+ if err != nil {
+ return "errortemplate error: " + template.HTMLEscapeString(err.Error())
+ }
+
+ var sb strings.Builder
+ data := map[string]any{
+ "Manifest": h.manifest,
+ "Page": p,
+ "TOC": p.TOC,
+ "Content": template.HTML(p.HTML), //nolint:gosec —— 内容为内置受信 Markdown
+ }
+ if err := tpl.Execute(&sb, data); err != nil {
+ return "errorrender error: " + template.HTMLEscapeString(err.Error())
+ }
+ return sb.String()
+}
diff --git a/apps/server/internal/docs/template.html b/apps/server/internal/docs/template.html
index 9198619..a2eca04 100644
--- a/apps/server/internal/docs/template.html
+++ b/apps/server/internal/docs/template.html
@@ -3,17 +3,85 @@
-{{TITLE}} · OpsxDB 文档
+{{.Page.Title}} · OpsxDB 文档
+
+
-
- DB OpsxDB 文档
-
+
+
-
-
-
{{BODY}}
+
+
+
+
+
+ 文档 / {{.Page.Group}} / {{.Page.Title}}
+ {{.Page.Title}}
+ {{if .Page.Updated}}更新于 {{.Page.Updated}}
{{end}}
+
+{{.Content}}
+
+
+
+
+
+ 本页内容是否对您有帮助?
+
+
+ 感谢您的反馈!
+
+
+
+
+
+