Tong G 1 month ago
parent
commit
45cea45bce

+ 10 - 0
app.log

@@ -1579,3 +1579,13 @@
 {"level":"info","timestamp":"2025-12-10T17:40:41.013+0800","caller":"logger/logger.go:104","message":"路由注册完成"}
 {"level":"info","timestamp":"2025-12-10T17:40:41.013+0800","caller":"logger/logger.go:104","message":"HTTP服务器设置完成","addr":"127.0.0.1","cors_enabled":true,"audit_enabled":true}
 {"level":"info","timestamp":"2025-12-10T17:40:41.013+0800","caller":"logger/logger.go:104","message":"HTTP服务器启动完成","IP:Port":"127.0.0.1:8080"}
+{"level":"info","timestamp":"2025-12-11T17:19:54.448+0800","caller":"logger/logger.go:104","message":"数据库连接池初始化完成"}
+{"level":"info","timestamp":"2025-12-11T17:19:54.448+0800","caller":"logger/logger.go:104","message":"应用初始化完成","config_path":"config.toml","audit_enabled":true,"storage_type":"db"}
+{"level":"info","timestamp":"2025-12-11T17:19:54.449+0800","caller":"logger/logger.go:104","message":"路由注册完成"}
+{"level":"info","timestamp":"2025-12-11T17:19:54.449+0800","caller":"logger/logger.go:104","message":"HTTP服务器设置完成","addr":"127.0.0.1","cors_enabled":true,"audit_enabled":true}
+{"level":"info","timestamp":"2025-12-11T17:19:54.449+0800","caller":"logger/logger.go:104","message":"HTTP服务器启动完成","IP:Port":"127.0.0.1:8080"}
+{"level":"info","timestamp":"2025-12-11T17:28:07.353+0800","caller":"logger/logger.go:104","message":"数据库连接池初始化完成"}
+{"level":"info","timestamp":"2025-12-11T17:28:07.353+0800","caller":"logger/logger.go:104","message":"应用初始化完成","config_path":"config.toml","audit_enabled":true,"storage_type":"db"}
+{"level":"info","timestamp":"2025-12-11T17:28:07.353+0800","caller":"logger/logger.go:104","message":"路由注册完成"}
+{"level":"info","timestamp":"2025-12-11T17:28:07.353+0800","caller":"logger/logger.go:104","message":"HTTP服务器设置完成","addr":"127.0.0.1","cors_enabled":true,"audit_enabled":true}
+{"level":"info","timestamp":"2025-12-11T17:28:07.353+0800","caller":"logger/logger.go:104","message":"HTTP服务器启动完成","IP:Port":"127.0.0.1:8080"}

BIN
dbview.exe


+ 0 - 37
dd.md

@@ -1,37 +0,0 @@
-现有代码与 DBeaver 风格目录层级的对比分析
-基于你现有的 meta.go 结构(SchemaMeta、ObjectMeta、ObjectChild)和之前实现的 DBeaver 风格 Node 设计,以下是详细对比。DBeaver 强调插件化、树状浏览和属性面板,而现有代码更注重简单的数据结构。
-
-1. 现有代码结构概述
-SchemaMeta:一级对象(如数据库),包含 Name、Type、Extra。
-ObjectMeta:二级对象(如表),包含 Name、Type、Extra。
-ObjectChild:三级对象(如列),包含 Name、Type、Comment、Extra。
-接口:SchemaReader 和 ObjectReader,提供 ListSchemas、ListObjects 等方法。
-特点:扁平化设计,易于分页查询,但层级关系需前端手动构建。
-2. DBeaver 风格结构概述
-Node:统一节点结构,支持递归子节点,包含 ID、Type、Metadata、Children。
-NodeProvider:插件化接口,支持 GetRoot、GetChildren、UpdateNode 等。
-Metadata:详细元数据,包括 Name、Description、DataType、CreatedAt 等。
-特点:树状设计,天然支持懒加载和属性面板,扩展性强。
-3. 详细对比
-方面	现有代码 (meta.go 风格)	DBeaver 风格 (Node 风格)
-结构复杂度	简单:分层结构体 (Schema/Object/Child)	中等:统一 Node + Metadata
-层级支持	固定 3 层 (Schema > Object > Child)	无限层级 (递归 Node)
-扩展性	依赖 Extra 字段,扩展有限	Metadata 字段丰富,易扩展属性
-查询效率	高效分页 (ListSchemas/ListObjects)	懒加载 + 分页,适合大数据
-前端集成	需要手动构建树状结构	天然树状,易渲染
-插件化	无插件支持,需手动适配不同数据库	NodeProvider 接口,支持多数据库插件
-属性面板	依赖 Extra,显示简单	Metadata 直接映射,属性丰富
-修改操作	无内置修改接口	支持 UpdateNode、RenameNode 等
-搜索支持	无	支持 SearchNodes
-内存占用	低 (扁平数据)	中等 (树状结构可能占用更多内存)
-维护成本	低 (结构固定)	中等 (需实现递归逻辑)
-4. 优缺点总结
-现有代码优点:轻量、查询快,适合简单场景;易于理解和维护。
-现有代码缺点:扩展性差,层级固定;不支持复杂操作(如修改、搜索)。
-DBeaver 风格优点:高度灵活,支持无限层级和插件;用户体验佳(类似 DBeaver 的树状浏览和属性面板)。
-DBeaver 风格缺点:实现复杂度高;递归查询可能影响性能。
-5. 建议
-如果项目简单且数据量小,保留现有代码,添加分页和搜索即可。
-如果需要更丰富的功能(如属性面板、多数据库支持),迁移到 DBeaver 风格。可以通过渐进式重构:先扩展现有结构,再引入 Node 概念。
-混合方案:保留 meta.go 作为数据模型,结合 Node 作为前端展示层。
-如果你提供更多具体需求,我可以给出更针对性的对比或迁移方案。

+ 9 - 0
go.mod

@@ -16,6 +16,8 @@ require (
 
 require (
 	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/bahlo/generic-list-go v0.2.0 // indirect
+	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/bytedance/sonic v1.13.3 // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
@@ -28,17 +30,24 @@ require (
 	github.com/go-playground/validator/v10 v10.26.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect
+	github.com/invopop/jsonschema v0.13.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mark3labs/mcp-go v0.43.2 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	github.com/spf13/cast v1.7.1 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.3.0 // indirect
+	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	go.uber.org/multierr v1.10.0 // indirect
 	golang.org/x/arch v0.18.0 // indirect
 	golang.org/x/crypto v0.39.0 // indirect

+ 19 - 0
go.sum

@@ -1,5 +1,9 @@
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
 github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -42,6 +46,9 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -54,6 +61,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
+github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -73,6 +84,10 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -87,6 +102,10 @@ 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.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
 github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=

+ 4 - 3
main.go

@@ -2,15 +2,16 @@ package main
 
 import (
 	"dbview/service"
-	"fmt"
 	"log"
 	"os"
 	"path/filepath"
+
+	"github.com/spf13/pflag"
 )
 
 func main() {
-	fmt.Println("DB View - MySQL v8 DBeaver Style Implementation")
-	fmt.Println("============================================")
+	// 解析命令行标志(在入口统一解析)
+	pflag.Parse()
 
 	// 获取配置路径
 	configPath := getConfigPath()

+ 14 - 14
service/internal/bootstrap/bootstrap.go

@@ -11,6 +11,7 @@ import (
 	"dbview/service/internal/common/manager/connection"
 	"dbview/service/internal/common/manager/storage"
 	"dbview/service/internal/common/manager/task"
+	"dbview/service/internal/common/mcp"
 	"dbview/service/internal/config"
 
 	"go.uber.org/zap"
@@ -24,6 +25,7 @@ type App struct {
 	StorageManager storage.StorageInterface // 使用 StorageInterface 接口
 	TaskManager    *task.Manager
 	ConnectionPool *connection.ConnectionPool
+	MCP            *mcp.Component
 	Context        context.Context
 	Cancel         context.CancelFunc
 }
@@ -71,11 +73,11 @@ func InitializeApp(configPath string) (*App, error) {
 	app.StorageManager = storageMgr
 
 	// 5. 注册数据库驱动
-	if err := registerDatabaseDrivers(); err != nil {
-		app.Logger.Error("注册数据库驱动失败", zap.Error(err))
-		cancel()
-		return nil, fmt.Errorf("注册数据库驱动失败,错误: %v", err)
-	}
+	// if err := registerDatabaseDrivers(); err != nil {
+	// 	app.Logger.Error("注册数据库驱动失败", zap.Error(err))
+	// 	cancel()
+	// 	return nil, fmt.Errorf("注册数据库驱动失败,错误: %v", err)
+	// }
 
 	// 6. 初始化连接池
 	connPool := initializeConnectionPool(ctx, appLogger)
@@ -85,11 +87,16 @@ func InitializeApp(configPath string) (*App, error) {
 	taskManager := initializeTaskManager(ctx)
 	app.TaskManager = taskManager
 
-	// 8. 记录启动信息
+	// 8. 初始化 MCP 组件(按配置开关)
+	mcpComponent := initializeMCP(ctx, cfg, appLogger, storageMgr, connPool, taskManager)
+	app.MCP = mcpComponent
+
+	// 9. 记录启动信息
 	app.Logger.Info("应用初始化完成",
 		zap.String("config_path", configPath),
 		zap.Bool("audit_enabled", cfg.Audit.Enabled),
 		zap.String("storage_type", cfg.Storage.Type),
+		zap.Bool("mcp_enabled", cfg.MCP.Enable),
 	)
 
 	return app, nil
@@ -144,17 +151,10 @@ func initializeStorage(cfg *config.AppConfig) (storage.StorageInterface, error)
 
 		return storageMgr, nil
 	default:
-		return nil, fmt.Errorf("不支持的存储类型: %s,可用类型: file, db", cfg.Storage.Type)
+		return nil, fmt.Errorf("不支持的存储类型: %s,可用类型:  db", cfg.Storage.Type)
 	}
 }
 
-// registerDatabaseDrivers 注册数据库驱动
-func registerDatabaseDrivers() error {
-	// 这里可以注册各种数据库驱动
-	// 目前暂时返回 nil,具体实现根据需要添加
-	return nil
-}
-
 // initializeConnectionPool 初始化连接池
 func initializeConnectionPool(ctx context.Context, log logger.Logger) *connection.ConnectionPool {
 	pool := connection.NewConnectionPool(ctx)

+ 11 - 0
service/internal/bootstrap/flags.go

@@ -0,0 +1,11 @@
+package bootstrap
+
+import "github.com/spf13/pflag"
+
+// MountStaticFiles 由命令行标志控制(注册但不解析)
+var MountStaticFiles bool
+
+func init() {
+	// 在此注册标志,但不要在包 init 中调用 Parse()
+	pflag.BoolVar(&MountStaticFiles, "mount-static", false, "是否挂载本地静态文件到 /static")
+}

+ 323 - 0
service/internal/bootstrap/mcp.go

@@ -0,0 +1,323 @@
+package bootstrap
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"time"
+
+	"dbview/service/internal/common/databases/meta"
+	"dbview/service/internal/common/logger"
+	"dbview/service/internal/common/manager/connection"
+	"dbview/service/internal/common/manager/storage"
+	"dbview/service/internal/common/manager/storage/types"
+	"dbview/service/internal/common/manager/task"
+	"dbview/service/internal/common/mcp"
+	"dbview/service/internal/config"
+	dq "dbview/service/internal/modules/data_query/service"
+
+	mcpgo "github.com/mark3labs/mcp-go/mcp"
+	"go.uber.org/zap"
+)
+
+// initializeMCP creates and starts the MCP component when enabled in config.
+// It registers a few useful tools backed by existing storage and query services.
+func initializeMCP(ctx context.Context, cfg *config.AppConfig, log logger.Logger, storageMgr storage.StorageInterface, pool *connection.ConnectionPool, taskMgr *task.Manager) *mcp.Component {
+	if cfg == nil || !cfg.MCP.Enable {
+		if log != nil {
+			log.Info("MCP 组件未启用,跳过初始化", zap.Bool("enabled", cfg != nil && cfg.MCP.Enable))
+		}
+		return nil
+	}
+
+	comp := mcp.NewComponent(mcp.Config{
+		Enable:        cfg.MCP.Enable,
+		ServerName:    cfg.MCP.ServerName,
+		ServerVersion: cfg.MCP.ServerVersion,
+	}, log)
+
+	// 基础工具
+	comp.RegisterHealthTool()
+	comp.RegisterEchoTool()
+
+	// 与当前业务相关的工具
+	registerConnectionTools(comp, storageMgr, log)
+	registerExecuteSQLTool(comp, pool, taskMgr, log)
+	registerAIChatTool(comp, cfg.AI, log)
+
+	go func() {
+		if err := comp.ServeStdio(ctx); err != nil && log != nil {
+			log.Error("MCP ServeStdio 启动失败", zap.Error(err))
+		}
+	}()
+
+	if log != nil {
+		log.Info("MCP 组件已启用", zap.String("server_name", cfg.MCP.ServerName), zap.String("version", cfg.MCP.ServerVersion))
+	}
+
+	return comp
+}
+
+// registerConnectionTools exposes a simple connection listing backed by storage.
+func registerConnectionTools(comp *mcp.Component, storageMgr storage.StorageInterface, log logger.Logger) {
+	if storageMgr == nil {
+		if log != nil {
+			log.Warn("未找到存储管理器,跳过 MCP 连接工具注册")
+		}
+		return
+	}
+
+	tool := mcpgo.NewTool("connections_list",
+		mcpgo.WithDescription("列出已存储的连接(不含敏感字段)"),
+	)
+
+	comp.Server().AddTool(tool, func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
+		conns, err := storageMgr.GetAllConnections()
+		if err != nil {
+			return nil, err
+		}
+
+		summaries := make([]connectionSummary, 0, len(conns))
+		for _, c := range conns {
+			summaries = append(summaries, summarizeConnection(c))
+		}
+
+		payload, err := json.MarshalIndent(summaries, "", "  ")
+		if err != nil {
+			return nil, err
+		}
+
+		return &mcpgo.CallToolResult{Content: []mcpgo.Content{mcpgo.TextContent{Text: string(payload)}}}, nil
+	})
+}
+
+// registerExecuteSQLTool exposes a synchronous SQL execution tool backed by the connection pool.
+func registerExecuteSQLTool(comp *mcp.Component, pool *connection.ConnectionPool, taskMgr *task.Manager, log logger.Logger) {
+	if pool == nil {
+		if log != nil {
+			log.Warn("未找到连接池,跳过 MCP SQL 执行工具注册")
+		}
+		return
+	}
+
+	dataSvc := dq.NewDataService(
+		dq.WithConnectionPool(pool),
+		dq.WithTaskManager(taskMgr),
+	)
+
+	tool := mcpgo.NewTool("execute_sql",
+		mcpgo.WithDescription("在指定连接上同步执行 SQL"),
+		mcpgo.WithString("connection_id", mcpgo.Description("连接ID,需已在连接池中就绪")),
+		mcpgo.WithString("sql", mcpgo.Description("要执行的 SQL 语句")),
+	)
+
+	comp.Server().AddTool(tool, func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
+		args, _ := req.Params.Arguments.(map[string]any)
+		connID, _ := args["connection_id"].(string)
+		sqlText, _ := args["sql"].(string)
+
+		if connID == "" || sqlText == "" {
+			return nil, fmt.Errorf("connection_id 与 sql 不能为空")
+		}
+
+		result, err := dataSvc.ExecuteSQL(ctx, connID, "", meta.ObjectPath{}, sqlText, nil, false, false)
+		if err != nil {
+			return nil, err
+		}
+
+		execRes, ok := result.(meta.ExecuteResult)
+		if !ok {
+			return nil, fmt.Errorf("unexpected execute result type %T", result)
+		}
+
+		payload, err := json.MarshalIndent(execRes, "", "  ")
+		if err != nil {
+			return nil, err
+		}
+
+		return &mcpgo.CallToolResult{Content: []mcpgo.Content{mcpgo.TextContent{Text: string(payload)}}}, nil
+	})
+}
+
+// registerAIChatTool exposes a simple AI chat tool backed by OpenAI HTTP API.
+func registerAIChatTool(comp *mcp.Component, aiCfg config.AIConfig, log logger.Logger) {
+	if !aiCfg.Enable {
+		if log != nil {
+			log.Info("AI 交互未启用,跳过 MCP ai_chat 注册")
+		}
+		return
+	}
+
+	if aiCfg.Provider != "openai" {
+		if log != nil {
+			log.Warn("AI Provider 未支持,跳过 MCP ai_chat 注册", zap.String("provider", aiCfg.Provider))
+		}
+		return
+	}
+
+	apiKeyEnv := aiCfg.APIKeyEnv
+	if apiKeyEnv == "" {
+		apiKeyEnv = "OPENAI_API_KEY"
+	}
+	apiKey := os.Getenv(apiKeyEnv)
+	if apiKey == "" {
+		if log != nil {
+			log.Warn("AI 交互已启用但未找到 API Key 环境变量", zap.String("env", apiKeyEnv))
+		}
+		return
+	}
+
+	defaultModel := aiCfg.Model
+	if defaultModel == "" {
+		defaultModel = "gpt-4o-mini"
+	}
+
+	tool := mcpgo.NewTool("ai_chat",
+		mcpgo.WithDescription("调用外部 AI 模型生成回复 (OpenAI)"),
+		mcpgo.WithString("prompt", mcpgo.Description("用户提示,必填")),
+		mcpgo.WithString("system", mcpgo.Description("系统指令,可选")),
+		mcpgo.WithString("model", mcpgo.Description("覆盖默认模型,可选")),
+	)
+
+	comp.Server().AddTool(tool, func(ctx context.Context, req mcpgo.CallToolRequest) (*mcpgo.CallToolResult, error) {
+		args, _ := req.Params.Arguments.(map[string]any)
+		prompt, _ := args["prompt"].(string)
+		if prompt == "" {
+			return nil, fmt.Errorf("prompt 不能为空")
+		}
+		systemPrompt, _ := args["system"].(string)
+		model := defaultModel
+		if override, ok := args["model"].(string); ok && override != "" {
+			model = override
+		}
+
+		callCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
+		defer cancel()
+
+		respText, err := callOpenAIChat(callCtx, apiKey, model, systemPrompt, prompt)
+		if err != nil {
+			return nil, err
+		}
+
+		return &mcpgo.CallToolResult{Content: []mcpgo.Content{mcpgo.TextContent{Text: respText}}}, nil
+	})
+
+	if log != nil {
+		log.Info("MCP ai_chat 工具已注册", zap.String("model", defaultModel), zap.String("provider", aiCfg.Provider))
+	}
+}
+
+// callOpenAIChat performs a minimal chat completion request.
+func callOpenAIChat(ctx context.Context, apiKey, model, systemPrompt, userPrompt string) (string, error) {
+	if apiKey == "" {
+		return "", fmt.Errorf("missing api key")
+	}
+
+	messages := []openAIMessage{}
+	if systemPrompt != "" {
+		messages = append(messages, openAIMessage{Role: "system", Content: systemPrompt})
+	}
+	messages = append(messages, openAIMessage{Role: "user", Content: userPrompt})
+
+	body := openAIChatRequest{Model: model, Messages: messages}
+	b, err := json.Marshal(body)
+	if err != nil {
+		return "", err
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader(b))
+	if err != nil {
+		return "", err
+	}
+	req.Header.Set("Authorization", "Bearer "+apiKey)
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	var out openAIChatResponse
+	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
+		return "", err
+	}
+
+	if resp.StatusCode >= 300 {
+		if out.Error != nil && out.Error.Message != "" {
+			return "", fmt.Errorf("openai error: %s", out.Error.Message)
+		}
+		return "", fmt.Errorf("openai request failed: status %d", resp.StatusCode)
+	}
+
+	if len(out.Choices) == 0 {
+		return "", fmt.Errorf("openai 返回为空")
+	}
+
+	return out.Choices[0].Message.Content, nil
+}
+
+type openAIChatRequest struct {
+	Model    string          `json:"model"`
+	Messages []openAIMessage `json:"messages"`
+}
+
+type openAIMessage struct {
+	Role    string `json:"role"`
+	Content string `json:"content"`
+}
+
+type openAIChatResponse struct {
+	Choices []struct {
+		Message openAIMessage `json:"message"`
+	} `json:"choices"`
+	Error *struct {
+		Message string `json:"message"`
+	} `json:"error,omitempty"`
+}
+
+// connectionSummary holds non-sensitive fields for MCP responses.
+type connectionSummary struct {
+	ID          string `json:"id"`
+	Name        string `json:"name"`
+	Description string `json:"description,omitempty"`
+	Kind        string `json:"kind"`
+	Type        string `json:"type,omitempty"`
+	Version     string `json:"version,omitempty"`
+	Server      string `json:"server,omitempty"`
+	Port        int    `json:"port,omitempty"`
+	Database    string `json:"database,omitempty"`
+	Color       string `json:"color,omitempty"`
+	AutoConnect bool   `json:"auto_connect"`
+}
+
+func summarizeConnection(c types.ConnectionWithDetails) connectionSummary {
+	summary := connectionSummary{
+		ID:          c.ID,
+		Name:        c.Name,
+		Description: c.Description,
+		Kind:        c.Kind,
+		Color:       c.Color,
+		AutoConnect: c.AutoConnect,
+	}
+
+	if c.DBDetail != nil {
+		summary.Type = c.DBDetail.Type
+		summary.Version = c.DBDetail.Version
+		summary.Server = c.DBDetail.Server
+		summary.Port = c.DBDetail.Port
+		summary.Database = c.DBDetail.DatabaseName
+	}
+
+	if c.ServerDetail != nil {
+		summary.Type = c.ServerDetail.Type
+		summary.Version = c.ServerDetail.Version
+		summary.Server = c.ServerDetail.Server
+		summary.Port = c.ServerDetail.Port
+	}
+
+	return summary
+}

+ 10 - 4
service/internal/bootstrap/server.go

@@ -7,11 +7,11 @@ import (
 	"strings"
 	"time"
 
+	routers "dbview/service/internal/modules"
+
 	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
 
-	routers "dbview/service/internal/modules"
-
 	"go.uber.org/zap"
 )
 
@@ -21,6 +21,8 @@ type Server struct {
 	engine *gin.Engine
 }
 
+// MountStaticFiles 由命令行标志控制(在 package flags.go 中注册,入口处调用 Parse)
+
 // NewServer 创建新的服务器实例
 func NewServer(app *App) *Server {
 	return &Server{
@@ -40,7 +42,11 @@ func (s *Server) Setup() {
 	s.registerRoutes()
 
 	// 2. 挂载本地静态文件(Vue打包的dist目录)
-	//s.mountStaticFiles()
+	if MountStaticFiles {
+		s.mountStaticFiles()
+	} else {
+		s.app.Logger.Info("静态文件挂载已禁用(运行时参数控制)", zap.Bool("mount_flag", MountStaticFiles))
+	}
 
 	s.app.Logger.Info("HTTP服务器设置完成",
 		zap.String("addr", s.app.Config.Server.Ip),
@@ -115,7 +121,7 @@ func (s *Server) setupMiddleware() {
 // registerRoutes 注册路由(现有逻辑不变)
 func (s *Server) registerRoutes() {
 	// 注册模块路由
-	routers.RegisterRoutes(s.engine, s.app.StorageManager, s.app.TaskManager, s.app.ConnectionPool)
+	routers.RegisterRoutes(s.engine, s.app.StorageManager, s.app.TaskManager, s.app.ConnectionPool, s.app.Config.AI)
 
 	// 基础健康检查路由
 	s.engine.GET("/health", func(c *gin.Context) {

+ 631 - 824
service/internal/common/databases/drivers/mysql/v8/meta.go

@@ -210,343 +210,9 @@ func (q *MySQLDriver) getTableIndexes(ctx context.Context, dbName, tableName, ob
 	return idxs, nil
 }
 
-// DeleteRootObjects:根据前端传入的 rootName(支持通配符 * 或 SQL %)、typeName(如 database/table/view)
-// 返回匹配到的对象列表与总数(注:默认不直接执行 DROP,仅列出匹配项)
-func (q *MySQLDriver) deleteRootObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error) {
-	var resp meta.ObjectOperationResponse
-	db := q.db
-	rootName := req.Object.Name
-	typeName := req.Object.Type
-	t := strings.ToLower(typeName)
-	// 处理通配符
-	pattern := rootName
-	if strings.Contains(pattern, "*") {
-		pattern = strings.ReplaceAll(pattern, "*", "%")
-	}
-	useLike := strings.Contains(pattern, "%") || strings.Contains(pattern, "_")
-
-	switch t {
-	case "database", "schema":
-		var rows *sql.Rows
-		var err error
-		if pattern == "" {
-			rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA`)
-		} else if useLike {
-			rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE ?`, pattern)
-		} else {
-			rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, pattern)
-		}
-		if err != nil {
-			return resp, fmt.Errorf("查询 schema 列表以供删除匹配失败:%w", err)
-		}
-		defer rows.Close()
-		var objs []meta.GenericObject
-		var total int64
-		for rows.Next() {
-			var name sql.NullString
-			var charset sql.NullString
-			if err := rows.Scan(&name, &charset); err != nil {
-				continue
-			}
-			total++
-			objs = append(objs, meta.GenericObject{
-				ID:       fmt.Sprintf("db-%s", name.String),
-				Name:     name.String,
-				Type:     "database",
-				ParentID: "",
-				DBType:   "mysql",
-				Attrs: map[string]string{
-					"charset": charset.String,
-				},
-			})
-		}
-		if err := rows.Err(); err != nil {
-			return resp, fmt.Errorf("遍历 schema 结果出错:%w", err)
-		}
-		resp.Affected = total
-		resp.ObjectID = ""
-		// 将匹配对象放入 Options 供前端显示(不能放在 Object 因为可能是多个)
-		respMap := map[string]interface{}{"matches": objs}
-		if respMapBytes, err := json.Marshal(respMap); err == nil {
-			// 当仅生成 SQL(不执行)时,把匹配对象的 JSON 放到 Sql 字段供前端查看;
-			// 若 Execute==true,则我们会尝试执行对应的 DROP 语句并在 Sql 中也返回执行的语句列表。
-			resp.Sql = string(respMapBytes)
-		}
-
-		// 如果请求不要求执行,直接返回列出匹配项
-		if !req.Execute {
-			return resp, nil
-		}
-
-		// Execute==true: 执行删除操作(注意:对 DDL 的事务语义依赖于具体数据库;MySQL 的 DROP 在多数情况下不可回滚)
-		tx, err := db.BeginTx(ctx, nil)
-		if err != nil {
-			return resp, fmt.Errorf("开始事务失败:%w", err)
-		}
-		var execCount int64
-		var execSQLs []string
-		for _, o := range objs {
-			// 对不同类型生成对应的 DROP 语句
-			switch o.Type {
-			case "database":
-				sqlStr := fmt.Sprintf("DROP DATABASE `%s`", o.Name)
-				if _, err := tx.ExecContext(ctx, sqlStr); err != nil {
-					_ = tx.Rollback()
-					return resp, fmt.Errorf("执行 SQL 失败:%s, err: %w", sqlStr, err)
-				}
-				execSQLs = append(execSQLs, sqlStr)
-				execCount++
-			default:
-				// 其他 root 类型目前不支持直接删除,记录并继续
-			}
-		}
-		if err := tx.Commit(); err != nil {
-			_ = tx.Rollback()
-			return resp, fmt.Errorf("提交事务失败:%w", err)
-		}
-		resp.Affected = execCount
-		if b, err := json.Marshal(execSQLs); err == nil {
-			resp.Sql = string(b)
-		}
-		return resp, nil
-
-	case "table", "view":
-		tableType := "BASE TABLE"
-		if t == "view" {
-			tableType = "VIEW"
-		}
-		var rows *sql.Rows
-		var err error
-		if pattern == "" {
-			rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ?`, tableType)
-		} else if useLike {
-			rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME LIKE ?`, tableType, pattern)
-		} else {
-			rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME = ?`, tableType, pattern)
-		}
-		if err != nil {
-			return resp, fmt.Errorf("查询表/视图以供删除匹配失败:%w", err)
-		}
-		defer rows.Close()
-		var objs []meta.GenericObject
-		var total int64
-		for rows.Next() {
-			var schema sql.NullString
-			var tname sql.NullString
-			var engine sql.NullString
-			var ctime sql.NullString
-			if err := rows.Scan(&schema, &tname, &engine, &ctime); err != nil {
-				continue
-			}
-			total++
-			pid := fmt.Sprintf("db-%s", schema.String)
-			id := fmt.Sprintf("%s.table-%s", pid, tname.String)
-			objs = append(objs, meta.GenericObject{
-				ID:       id,
-				Name:     tname.String,
-				Type:     t,
-				ParentID: pid,
-				DBType:   "mysql",
-				Attrs: map[string]string{
-					"engine":     engine.String,
-					"createTime": ctime.String,
-				},
-			})
-		}
-		if err := rows.Err(); err != nil {
-			return resp, fmt.Errorf("遍历表/视图结果出错:%w", err)
-		}
-		resp.Affected = total
-		matchMap := map[string]interface{}{"matches": objs}
-		if b, err := json.Marshal(matchMap); err == nil {
-			resp.Sql = string(b)
-		}
-		return resp, nil
-
-	default:
-		return resp, fmt.Errorf("不支持的 root 类型: %s", typeName)
-	}
-}
-
-// DeleteChildObjects:根据 parentID(如 db-xxx 或 conn.db-xxx)和 filter(type/name)返回匹配的子对象列表与总数
-func (q *MySQLDriver) deleteChildObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error) {
-	var resp meta.ObjectOperationResponse
-	parentID := req.Object.ParentID
-	filter := map[string]string{}
-	if req.Object.Name != "" {
-		filter["name"] = req.Object.Name
-	}
-	if req.Object.Type != "" {
-		filter["type"] = req.Object.Type
-	}
-	// reuse previous logic but adapt to return ObjectOperationResponse
-	parts := strings.Split(parentID, ".")
-	var dbPart string
-	if len(parts) >= 2 {
-		dbPart = parts[1]
-	} else {
-		dbPart = parentID
-	}
-	dbName := strings.TrimPrefix(dbPart, "db-")
-	objectType := strings.ToLower(filter["type"])
-	namePattern := filter["name"]
-	if strings.Contains(namePattern, "*") {
-		namePattern = strings.ReplaceAll(namePattern, "*", "%")
-	}
-	useLike := strings.Contains(namePattern, "%") || strings.Contains(namePattern, "_")
-
-	db := q.db
-	switch objectType {
-	case "index":
-		// 表级或库级索引
-		var rows *sql.Rows
-		var err error
-		if namePattern == "" {
-			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName)
-		} else if useLike {
-			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME LIKE ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern)
-		} else {
-			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern)
-		}
-		if err != nil {
-			return resp, fmt.Errorf("查询索引以供删除匹配失败:%w", err)
-		}
-		defer rows.Close()
-		var res []meta.GenericObject
-		var total int64
-		for rows.Next() {
-			var idx sql.NullString
-			var tname sql.NullString
-			var nonUnique sql.NullInt64
-			if err := rows.Scan(&idx, &tname, &nonUnique); err != nil {
-				continue
-			}
-			total++
-			id := fmt.Sprintf("%s.table-%s.index-%s", parentID, tname.String, idx.String)
-			res = append(res, meta.GenericObject{
-				ID:       id,
-				Name:     idx.String,
-				Type:     "index",
-				ParentID: fmt.Sprintf("%s.table-%s", parentID, tname.String),
-				DBType:   "mysql",
-				Attrs: map[string]string{
-					"table":     tname.String,
-					"nonUnique": fmt.Sprintf("%d", nonUnique.Int64),
-				},
-			})
-		}
-		if err := rows.Err(); err != nil {
-			return resp, fmt.Errorf("遍历索引结果出错:%w", err)
-		}
-		resp.Affected = total
-		m := map[string]interface{}{"matches": res}
-		if b, err := json.Marshal(m); err == nil {
-			resp.Sql = string(b)
-		}
-		return resp, nil
-
-	case "procedure", "proc":
-		var rows *sql.Rows
-		var err error
-		if namePattern == "" {
-			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE'`, dbName)
-		} else if useLike {
-			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME LIKE ?`, dbName, namePattern)
-		} else {
-			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ?`, dbName, namePattern)
-		}
-		if err != nil {
-			return resp, fmt.Errorf("查询存储过程以供删除匹配失败:%w", err)
-		}
-		defer rows.Close()
-		var res []meta.GenericObject
-		var total int64
-		for rows.Next() {
-			var rName sql.NullString
-			var def sql.NullString
-			var created sql.NullString
-			if err := rows.Scan(&rName, &def, &created); err != nil {
-				continue
-			}
-			total++
-			id := fmt.Sprintf("%s.proc-%s", parentID, rName.String)
-			res = append(res, meta.GenericObject{
-				ID:       id,
-				Name:     rName.String,
-				Type:     "procedure",
-				ParentID: parentID,
-				DBType:   "mysql",
-				Attrs: map[string]string{
-					"definition": def.String,
-					"created":    created.String,
-				},
-			})
-		}
-		if err := rows.Err(); err != nil {
-			return resp, fmt.Errorf("遍历存储过程结果出错:%w", err)
-		}
-		resp.Affected = total
-		m := map[string]interface{}{"matches": res}
-		if b, err := json.Marshal(m); err == nil {
-			resp.Sql = string(b)
-		}
-		return resp, nil
-
-	case "trigger":
-		var rows *sql.Rows
-		var err error
-		if namePattern == "" {
-			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ?`, dbName)
-		} else if useLike {
-			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME LIKE ?`, dbName, namePattern)
-		} else {
-			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME = ?`, dbName, namePattern)
-		}
-		if err != nil {
-			return resp, fmt.Errorf("查询触发器以供删除匹配失败:%w", err)
-		}
-		defer rows.Close()
-		var res []meta.GenericObject
-		var total int64
-		for rows.Next() {
-			var tName sql.NullString
-			var event sql.NullString
-			var objTable sql.NullString
-			var timing sql.NullString
-			var stmt sql.NullString
-			if err := rows.Scan(&tName, &event, &objTable, &timing, &stmt); err != nil {
-				continue
-			}
-			total++
-			id := fmt.Sprintf("%s.trigger-%s", parentID, tName.String)
-			res = append(res, meta.GenericObject{
-				ID:       id,
-				Name:     tName.String,
-				Type:     "trigger",
-				ParentID: parentID,
-				DBType:   "mysql",
-				Attrs: map[string]string{
-					"event":     event.String,
-					"table":     objTable.String,
-					"timing":    timing.String,
-					"statement": stmt.String,
-				},
-			})
-		}
-		if err := rows.Err(); err != nil {
-			return resp, fmt.Errorf("遍历触发器结果出错:%w", err)
-		}
-		resp.Affected = total
-		m := map[string]interface{}{"matches": res}
-		if b, err := json.Marshal(m); err == nil {
-			resp.Sql = string(b)
-		}
-		return resp, nil
+// deleteRootObjects 已移除,逻辑已内联到 ApplyChanges
 
-	default:
-		return resp, fmt.Errorf("不支持的 child 类型: %s", objectType)
-	}
-}
+// deleteChildObjects 已移除,逻辑已内联到 ApplyChanges
 
 // DescribeCreateTemplate 返回创建指定类型对象的表单模板(供前端渲染)
 // getDatabaseTemplateFields 返回数据库相关的模板字段
@@ -679,104 +345,42 @@ func (q *MySQLDriver) getCurrentTableInfo(ctx context.Context, dbName, tableName
 	}, nil
 }
 
-func (q *MySQLDriver) describeCreateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) {
-	// 从路径中提取对象类型和父名称
-	var objectType, parentName string
-	if len(path) > 0 {
-		lastEntry := path[len(path)-1]
-		objectType = strings.ToLower(lastEntry.Type)
-		// 对于创建操作,最后一个元素是待创建的对象类型
-		// 前面的元素构成父上下文
-		if len(path) > 1 {
-			parentEntry := path[len(path)-2] // 父对象是倒数第二个元素
-			parentName = parentEntry.Name
+// describeCreateTemplate 已内联到 GetObjectProperties,已删除以清理旧兼容层
+
+// GetMetadataInfo 返回数据库的元信息(关键字、字段类型、能力等)
+func (q *MySQLDriver) GetMetadataInfo(ctx context.Context) (meta.MetadataCapabilities, error) {
+	var caps meta.MetadataCapabilities
+
+	// 1) 尝试从 INFORMATION_SCHEMA.COLUMNS 获取字段类型
+	rows, err := q.db.QueryContext(ctx, "SELECT DISTINCT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS")
+	if err == nil {
+		defer rows.Close()
+		for rows.Next() {
+			var dt sql.NullString
+			if err := rows.Scan(&dt); err != nil {
+				continue
+			}
+			if dt.Valid && dt.String != "" {
+				caps.FieldTypes = append(caps.FieldTypes, strings.ToUpper(dt.String))
+			}
 		}
 	} else {
-		return meta.ObjectTemplate{}, fmt.Errorf("创建模板需要指定对象类型路径")
+		// 回退到一组常见类型
+		caps.FieldTypes = []string{"INT", "BIGINT", "VARCHAR", "TEXT", "DATETIME", "TIMESTAMP", "DATE", "CHAR", "FLOAT", "DOUBLE", "DECIMAL", "BOOLEAN"}
 	}
 
-	switch objectType {
-	case "database":
-		// 示例值均以字符串形式表示
-		ex := map[string]string{"databaseName": "mydb", "charset": "utf8mb4", "ifNotExists": "true"}
-		tpl := meta.ObjectTemplate{
-			Operation:  "create",
-			ObjectType: "database",
-			ParentHint: "",
-			Fields:     getDatabaseTemplateFields(false, nil),
-			Example:    ex,
-		}
-		return tpl, nil
-	case "table":
-		parentHint := "parentName should be the database name"
-		if parentName != "" {
-			parentHint = "database: " + parentName
-		}
-		// 将复杂的 columns 示例编码为 JSON 字符串以便前端展示/解析
-		colsExample := []map[string]interface{}{{"name": "id", "type": "INT", "nullable": false}}
-		colsB, _ := json.Marshal(colsExample)
-		ex := map[string]string{"tableName": "users", "columns": string(colsB), "engine": "InnoDB"}
-		tpl := meta.ObjectTemplate{
-			Operation:  "create",
-			ObjectType: "table",
-			ParentHint: parentHint,
-			Fields:     getTableTemplateFields(false, nil),
-			Example:    ex,
-		}
-		return tpl, nil
-	case "index":
-		parentHint := "parentName can be table name"
-		if parentName != "" {
-			parentHint = "table: " + parentName
-		}
-		ex := map[string]string{"indexName": "idx_users_email", "columns": "email", "unique": "true"}
-		tpl := meta.ObjectTemplate{
-			Operation:  "create",
-			ObjectType: "index",
-			ParentHint: parentHint,
-			Fields:     getIndexTemplateFields(false, nil),
-			Example:    ex,
-		}
-		return tpl, nil
-	default:
-		return meta.ObjectTemplate{}, fmt.Errorf("不支持的 create 类型: %s", objectType)
-	}
-}
-
-// GetMetadataInfo 返回数据库的元信息(关键字、字段类型、能力等)
-func (q *MySQLDriver) GetMetadataInfo(ctx context.Context) (meta.MetadataCapabilities, error) {
-	var caps meta.MetadataCapabilities
-
-	// 1) 尝试从 INFORMATION_SCHEMA.COLUMNS 获取字段类型
-	rows, err := q.db.QueryContext(ctx, "SELECT DISTINCT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS")
-	if err == nil {
-		defer rows.Close()
-		for rows.Next() {
-			var dt sql.NullString
-			if err := rows.Scan(&dt); err != nil {
-				continue
-			}
-			if dt.Valid && dt.String != "" {
-				caps.FieldTypes = append(caps.FieldTypes, strings.ToUpper(dt.String))
-			}
-		}
-	} else {
-		// 回退到一组常见类型
-		caps.FieldTypes = []string{"INT", "BIGINT", "VARCHAR", "TEXT", "DATETIME", "TIMESTAMP", "DATE", "CHAR", "FLOAT", "DOUBLE", "DECIMAL", "BOOLEAN"}
-	}
-
-	// 2) 尝试从 mysql.help_keyword 获取关键字(部分 MySQL 安装提供)
-	rows2, err2 := q.db.QueryContext(ctx, "SELECT DISTINCT word FROM mysql.help_keyword")
-	if err2 == nil {
-		defer rows2.Close()
-		for rows2.Next() {
-			var kw sql.NullString
-			if err := rows2.Scan(&kw); err != nil {
-				continue
-			}
-			if kw.Valid && kw.String != "" {
-				caps.Keywords = append(caps.Keywords, kw.String)
-			}
+	// 2) 尝试从 mysql.help_keyword 获取关键字(部分 MySQL 安装提供)
+	rows2, err2 := q.db.QueryContext(ctx, "SELECT DISTINCT word FROM mysql.help_keyword")
+	if err2 == nil {
+		defer rows2.Close()
+		for rows2.Next() {
+			var kw sql.NullString
+			if err := rows2.Scan(&kw); err != nil {
+				continue
+			}
+			if kw.Valid && kw.String != "" {
+				caps.Keywords = append(caps.Keywords, kw.String)
+			}
 		}
 	} else {
 		// 3) 尝试 INFORMATION_SCHEMA.KEYWORDS(部分版本可用)
@@ -828,167 +432,7 @@ func (q *MySQLDriver) GetMetadataInfo(ctx context.Context) (meta.MetadataCapabil
 	return caps, nil
 }
 
-// CreateObject 仅实现 preview(execute==false)路径:校验输入并生成 SQL,execute=true 尚未实现
-func (q *MySQLDriver) createObject(ctx context.Context, req meta.CreateObjectRequest) (meta.CreateObjectResponse, error) {
-	var resp meta.CreateObjectResponse
-	t := strings.ToLower(req.ObjectType)
-	// Basic validation
-	if req.ObjectType == "" {
-		return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: objectType")
-	}
-	switch t {
-	case "database":
-		props := req.Properties
-		nameI, ok := props["databaseName"]
-		if !ok {
-			return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: databaseName")
-		}
-		name, _ := nameI.(string)
-		if name == "" {
-			return meta.CreateObjectResponse{}, fmt.Errorf("databaseName 不能为空")
-		}
-		if req.Execute {
-			return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
-		}
-		charset := "utf8mb4"
-		if cs, ok := props["charset"].(string); ok && cs != "" {
-			charset = cs
-		}
-		ifNot := false
-		if v, ok := props["ifNotExists"].(bool); ok {
-			ifNot = v
-		}
-		sql := "CREATE DATABASE"
-		if ifNot {
-			sql += " IF NOT EXISTS"
-		}
-		sql = fmt.Sprintf("%s `%s` DEFAULT CHARACTER SET = %s;", sql, name, charset)
-		resp.GeneratedSQL = []string{sql}
-		return resp, nil
-
-	case "table":
-		props := req.Properties
-		tnameI, ok := props["tableName"]
-		if !ok {
-			return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: tableName")
-		}
-		tname, _ := tnameI.(string)
-		if tname == "" {
-			return meta.CreateObjectResponse{}, fmt.Errorf("tableName 不能为空")
-		}
-		colsI, ok := props["columns"]
-		if !ok {
-			return meta.CreateObjectResponse{}, fmt.Errorf("缺少必填字段: columns")
-		}
-		colsSlice, ok := colsI.([]interface{})
-		if !ok {
-			return meta.CreateObjectResponse{}, fmt.Errorf("columns 格式无效,期望为数组类型的列定义,例如 [{\"name\":\"id\",\"type\":\"INT\"}]")
-		}
-		if req.Execute {
-			return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
-		}
-		engine := "InnoDB"
-		if e, ok := props["engine"].(string); ok && e != "" {
-			engine = e
-		}
-		charset := "utf8mb4"
-		if cs, ok := props["charset"].(string); ok && cs != "" {
-			charset = cs
-		}
-		ifNot := false
-		if v, ok := props["ifNotExists"].(bool); ok {
-			ifNot = v
-		}
-
-		// build column definitions
-		var colDefs []string
-		for _, ci := range colsSlice {
-			m, ok := ci.(map[string]interface{})
-			if !ok {
-				continue
-			}
-			cname, _ := m["name"].(string)
-			ctype, _ := m["type"].(string)
-			if cname == "" || ctype == "" {
-				// skip invalid
-				continue
-			}
-			nullable := true
-			if n, ok := m["nullable"].(bool); ok {
-				nullable = n
-			}
-			autoInc := false
-			if a, ok := m["autoIncrement"].(bool); ok {
-				autoInc = a
-			}
-			defStr := ""
-			if d, ok := m["default"]; ok && d != nil {
-				// simple formatting: quote strings, otherwise use fmt.Sprint
-				switch v := d.(type) {
-				case string:
-					defStr = fmt.Sprintf(" DEFAULT '%s'", strings.ReplaceAll(v, "'", "\\'"))
-				default:
-					defStr = fmt.Sprintf(" DEFAULT %v", v)
-				}
-			}
-			col := fmt.Sprintf("`%s` %s", cname, ctype)
-			if !nullable {
-				col += " NOT NULL"
-			}
-			if autoInc {
-				col += " AUTO_INCREMENT"
-			}
-			col += defStr
-			colDefs = append(colDefs, col)
-		}
-		if len(colDefs) == 0 {
-			return meta.CreateObjectResponse{}, fmt.Errorf("未找到有效的列定义,请检查 properties.columns 字段,示例格式: [{\"name\":\"id\",\"type\":\"INT\"}]")
-		}
-		// build create statement (include parent/db if provided)
-		createStmt := "CREATE TABLE"
-		if ifNot {
-			createStmt += " IF NOT EXISTS"
-		}
-		// If parent provided, include database qualifier
-		if req.ParentName != "" {
-			createStmt = fmt.Sprintf("%s `%s`.`%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, req.ParentName, tname, strings.Join(colDefs, ", "), engine, charset)
-		} else {
-			createStmt = fmt.Sprintf("%s `%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, tname, strings.Join(colDefs, ", "), engine, charset)
-		}
-		resp.GeneratedSQL = []string{createStmt}
-		return resp, nil
-
-	case "index":
-		props := req.Properties
-		iname, _ := props["indexName"].(string)
-		cols, _ := props["columns"].(string)
-		unique := false
-		if u, ok := props["unique"].(bool); ok {
-			unique = u
-		}
-		if iname == "" || cols == "" {
-			return meta.CreateObjectResponse{}, fmt.Errorf("indexName 与 columns 为必填字段")
-		}
-		if req.Execute {
-			return meta.CreateObjectResponse{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
-		}
-		uq := ""
-		if unique {
-			uq = "UNIQUE "
-		}
-		// parentName ideally should be table name
-		tableRef := iname
-		if req.ParentName != "" {
-			tableRef = req.ParentName
-		}
-		stmt := fmt.Sprintf("CREATE %sINDEX `%s` ON `%s` (%s);", uq, iname, tableRef, cols)
-		resp.GeneratedSQL = []string{stmt}
-		return resp, nil
-
-	default:
-		return meta.CreateObjectResponse{}, fmt.Errorf("不支持的 create 类型: %s", req.ObjectType)
-	}
-}
+// createObject 已内联到 ApplyChanges,原 helper 已删除
 
 // QueryData 执行数据查询(实现 DataReader 接口)
 func (q *MySQLDriver) QueryData(ctx context.Context, path meta.ObjectPath, req meta.DataQueryRequest, includeLarge bool) (meta.QueryResult, error) {
@@ -1482,33 +926,77 @@ func getRowName(children []meta.DataMeta) string {
 	return fmt.Sprintf("%v", children[0].Value)
 }
 
-// DescribeUpdateTemplate 返回指定对象的修改模板,用于前端动态生成表单
-func (q *MySQLDriver) describeUpdateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) {
+// buildCreateTemplate 根据对象类型和父级名称构建创建模板(Create ObjectTemplate)
+func (q *MySQLDriver) buildCreateTemplate(ctx context.Context, objectType, parentName string) (meta.ObjectTemplate, error) {
+	switch objectType {
+	case "database":
+		ex := map[string]string{"databaseName": "mydb", "charset": "utf8mb4", "ifNotExists": "true"}
+		return meta.ObjectTemplate{
+			Operation:  "create",
+			ObjectType: "database",
+			ParentHint: "",
+			Fields:     getDatabaseTemplateFields(false, nil),
+			Example:    ex,
+		}, nil
+
+	case "table":
+		parentHint := "parentName should be the database name"
+		if parentName != "" {
+			parentHint = "database: " + parentName
+		}
+		colsExample := []map[string]interface{}{{"name": "id", "type": "INT", "nullable": false}}
+		colsB, _ := json.Marshal(colsExample)
+		ex := map[string]string{"tableName": "users", "columns": string(colsB), "engine": "InnoDB"}
+		return meta.ObjectTemplate{
+			Operation:  "create",
+			ObjectType: "table",
+			ParentHint: parentHint,
+			Fields:     getTableTemplateFields(false, nil),
+			Example:    ex,
+		}, nil
+
+	case "index":
+		parentHint := "parentName can be table name"
+		if parentName != "" {
+			parentHint = "table: " + parentName
+		}
+		ex := map[string]string{"indexName": "idx_users_email", "columns": "email", "unique": "true"}
+		return meta.ObjectTemplate{
+			Operation:  "create",
+			ObjectType: "index",
+			ParentHint: parentHint,
+			Fields:     getIndexTemplateFields(false, nil),
+			Example:    ex,
+		}, nil
+
+	default:
+		return meta.ObjectTemplate{}, fmt.Errorf("不支持的 create 类型: %s", objectType)
+	}
+}
+
+// buildUpdateTemplate 为指定路径构建 update 模板(读取当前值并转换为 ObjectTemplate)
+func (q *MySQLDriver) buildUpdateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) {
 	if len(path) == 0 {
 		return meta.ObjectTemplate{}, fmt.Errorf("路径不能为空")
 	}
-
 	lastEntry := path[len(path)-1]
 	objectType := strings.ToLower(lastEntry.Type)
 	objectName := lastEntry.Name
 
 	switch objectType {
 	case "database":
-		// 获取数据库的当前信息
 		currentValues, err := q.getCurrentDatabaseInfo(ctx, objectName)
 		if err != nil {
 			return meta.ObjectTemplate{}, err
 		}
-
-		tpl := meta.ObjectTemplate{
+		return meta.ObjectTemplate{
 			Operation:  "update",
 			ObjectType: "database",
 			ParentHint: "",
 			Fields:     getDatabaseTemplateFields(true, currentValues),
 			Current:    currentValues,
 			Example:    map[string]string{"charset": "utf8mb4"},
-		}
-		return tpl, nil
+		}, nil
 
 	case "table":
 		if len(path) < 2 {
@@ -1516,194 +1004,43 @@ func (q *MySQLDriver) describeUpdateTemplate(ctx context.Context, path meta.Obje
 		}
 		dbName := path[0].Name
 		tableName := objectName
-
-		// 获取表的当前信息
 		currentValues, err := q.getCurrentTableInfo(ctx, dbName, tableName)
 		if err != nil {
 			return meta.ObjectTemplate{}, err
 		}
-
-		tpl := meta.ObjectTemplate{
+		return meta.ObjectTemplate{
 			Operation:  "update",
 			ObjectType: "table",
 			ParentHint: "database: " + dbName,
 			Fields:     getTableTemplateFields(true, currentValues),
 			Current:    currentValues,
 			Example:    map[string]string{"engine": "InnoDB", "charset": "utf8mb4"},
-		}
-		return tpl, nil
+		}, nil
 
 	default:
 		return meta.ObjectTemplate{}, fmt.Errorf("不支持的 update 类型: %s", objectType)
 	}
 }
 
-// UpdateObject 执行对象的修改操作
-func (q *MySQLDriver) updateObject(ctx context.Context, req meta.UpdateObjectRequest) (meta.UpdateObjectResponse, error) {
-	if len(req.Path) == 0 {
-		return meta.UpdateObjectResponse{}, fmt.Errorf("路径不能为空")
-	}
-
-	lastEntry := req.Path[len(req.Path)-1]
-	objectType := strings.ToLower(lastEntry.Type)
-	objectName := lastEntry.Name
+// describeUpdateTemplate 已内联到 GetObjectProperties 并删除
 
-	var sqls []string
+// updateObject 已内联到 ApplyChanges 并删除
 
-	switch objectType {
-	case "database":
-		dbName := objectName
-		charset, charsetOk := req.Properties["charset"].(string)
-		collation, collationOk := req.Properties["collation"].(string)
+// describeDeleteTemplate 已内联到 GetObjectProperties / ApplyChanges,并已删除
 
-		if charsetOk && charset != "" {
-			sql := fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET %s", dbName, charset)
-			sqls = append(sqls, sql)
-		}
+// --- 适配器方法:实现新的 MetadataReader / ObjectManager 接口 ---
 
-		if collationOk && collation != "" {
-			sql := fmt.Sprintf("ALTER DATABASE `%s` COLLATE %s", dbName, collation)
-			sqls = append(sqls, sql)
-		}
-
-	case "table":
-		if len(req.Path) < 2 {
-			return meta.UpdateObjectResponse{}, fmt.Errorf("表路径需要包含数据库信息")
-		}
-		dbName := req.Path[0].Name
-		tableName := objectName
-
-		engine, engineOk := req.Properties["engine"].(string)
-		charset, charsetOk := req.Properties["charset"].(string)
-		collation, collationOk := req.Properties["collation"].(string)
-
-		if engineOk && engine != "" {
-			sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` ENGINE = %s", dbName, tableName, engine)
-			sqls = append(sqls, sql)
-		}
-
-		if charsetOk && charset != "" {
-			sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` CONVERT TO CHARACTER SET %s", dbName, tableName, charset)
-			sqls = append(sqls, sql)
-		}
-
-		if collationOk && collation != "" {
-			sql := fmt.Sprintf("ALTER TABLE `%s`.`%s` COLLATE %s", dbName, tableName, collation)
-			sqls = append(sqls, sql)
-		}
-
-	default:
-		return meta.UpdateObjectResponse{}, fmt.Errorf("不支持的 update 类型: %s", objectType)
-	}
-
-	if !req.Execute {
-		// 预览模式,只返回生成的SQL
-		return meta.UpdateObjectResponse{
-			GeneratedSQL: sqls,
-		}, nil
-	}
-
-	// 执行模式
-	for _, sql := range sqls {
-		if _, err := q.db.ExecContext(ctx, sql); err != nil {
-			return meta.UpdateObjectResponse{}, fmt.Errorf("执行SQL失败: %s, 错误: %v", sql, err)
-		}
-	}
-
-	return meta.UpdateObjectResponse{
-		GeneratedSQL: sqls,
-	}, nil
-}
-
-// DescribeDeleteTemplate 返回指定对象的删除模板,用于前端显示删除确认和影响预览
-func (q *MySQLDriver) describeDeleteTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error) {
-	if len(path) == 0 {
-		return meta.ObjectTemplate{}, fmt.Errorf("路径不能为空")
-	}
-
-	lastEntry := path[len(path)-1]
-	objectType := strings.ToLower(lastEntry.Type)
-	objectName := lastEntry.Name
-
-	switch objectType {
-	case "database":
-		dbName := objectName
-
-		// 查询数据库中的表数量
-		var tableCount int64
-		err := q.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?", dbName).Scan(&tableCount)
-		if err != nil {
-			return meta.ObjectTemplate{}, fmt.Errorf("获取数据库表数量失败: %w", err)
-		}
-
-		tpl := meta.ObjectTemplate{
-			Operation:  "delete",
-			ObjectType: "database",
-			ParentHint: "",
-			Current: map[string]string{
-				"databaseName": dbName,
-				"tableCount":   fmt.Sprintf("%d", tableCount),
-			},
-			Fields: []meta.TemplateField{
-				{Name: "databaseName", Label: "Database Name", Type: meta.FieldTypeString, Required: true, Current: dbName, Editable: &[]bool{false}[0]},
-				{Name: "tableCount", Label: "Tables Count", Type: meta.FieldTypeString, Required: false, Current: fmt.Sprintf("%d", tableCount), Editable: &[]bool{false}[0]},
-				{Name: "confirmDelete", Label: "Confirm Delete", Type: meta.FieldTypeBool, Required: true, Help: "删除数据库将同时删除所有表和数据,此操作不可恢复"},
-			},
-			Notes: "警告:删除数据库将永久删除所有表、数据和相关对象。此操作不可恢复。",
-		}
-		return tpl, nil
-
-	case "table":
-		if len(path) < 2 {
-			return meta.ObjectTemplate{}, fmt.Errorf("表路径需要包含数据库信息")
-		}
-		dbName := path[0].Name
-		tableName := objectName
-
-		// 查询表中的行数
-		var rowCount int64
-		query := fmt.Sprintf("SELECT COUNT(*) FROM `%s`.`%s`", dbName, tableName)
-		err := q.db.QueryRowContext(ctx, query).Scan(&rowCount)
-		if err != nil {
-			// 如果查询失败,可能表不存在或权限问题
-			rowCount = -1
-		}
-
-		tpl := meta.ObjectTemplate{
-			Operation:  "delete",
-			ObjectType: "table",
-			ParentHint: "database: " + dbName,
-			Current: map[string]string{
-				"tableName": tableName,
-				"rowCount":  fmt.Sprintf("%d", rowCount),
-			},
-			Fields: []meta.TemplateField{
-				{Name: "tableName", Label: "Table Name", Type: meta.FieldTypeString, Required: true, Current: tableName, Editable: &[]bool{false}[0]},
-				{Name: "rowCount", Label: "Row Count", Type: meta.FieldTypeString, Required: false, Current: fmt.Sprintf("%d", rowCount), Editable: &[]bool{false}[0]},
-				{Name: "confirmDelete", Label: "Confirm Delete", Type: meta.FieldTypeBool, Required: true, Help: "删除表将永久删除所有数据,此操作不可恢复"},
-			},
-			Notes: "警告:删除表将永久删除所有数据。此操作不可恢复。",
-		}
-		return tpl, nil
-
-	default:
-		return meta.ObjectTemplate{}, fmt.Errorf("不支持的 delete 类型: %s", objectType)
-	}
-}
-
-// --- 适配器方法:实现新的 MetadataReader / ObjectManager 接口 ---
-
-// GetStructureDefinition 返回 MySQL 的节点类型定义
-// 这些定义用于上层构建树状导航(例如数据库->表->列),包含类型标识、显示标签、图标与允许的子类型
-func (q *MySQLDriver) GetStructureDefinition(ctx context.Context) ([]meta.NodeTypeDefinition, error) {
-	defs := []meta.NodeTypeDefinition{
-		{Type: "database", Label: "Database", Icon: "database", ChildTypes: []string{"table", "view", "procedure", "trigger", "index"}, IsLeaf: false},
-		{Type: "table", Label: "Table", Icon: "table", ChildTypes: []string{"column", "index"}, IsLeaf: false},
-		{Type: "view", Label: "View", Icon: "eye", ChildTypes: []string{"column"}, IsLeaf: false},
-		{Type: "column", Label: "Column", Icon: "column", ChildTypes: nil, IsLeaf: true},
-	}
-	return defs, nil
-}
+// GetStructureDefinition 返回 MySQL 的节点类型定义
+// 这些定义用于上层构建树状导航(例如数据库->表->列),包含类型标识、显示标签、图标与允许的子类型
+func (q *MySQLDriver) GetStructureDefinition(ctx context.Context) ([]meta.NodeTypeDefinition, error) {
+	defs := []meta.NodeTypeDefinition{
+		{Type: "database", Label: "Database", Icon: "database", ChildTypes: []string{"table", "view", "procedure", "trigger", "index"}, IsLeaf: false},
+		{Type: "table", Label: "Table", Icon: "table", ChildTypes: []string{"column", "index"}, IsLeaf: false},
+		{Type: "view", Label: "View", Icon: "eye", ChildTypes: []string{"column"}, IsLeaf: false},
+		{Type: "column", Label: "Column", Icon: "column", ChildTypes: nil, IsLeaf: true},
+	}
+	return defs, nil
+}
 
 // 辅助函数:将内部通用对象 meta.GenericObject 转换为通用节点 meta.Node
 // - basePath: 如果提供,将在当前对象之前作为路径前缀
@@ -2160,12 +1497,28 @@ func (q *MySQLDriver) GetObjectProperties(ctx context.Context, path meta.ObjectP
 	var tpl meta.ObjectTemplate
 	var err error
 	if last.Name == "" {
-		tpl, err = q.describeCreateTemplate(ctx, path)
+		// create 模板
+		var objectType, parentName string
+		if len(path) > 0 {
+			lastEntry := path[len(path)-1]
+			objectType = strings.ToLower(lastEntry.Type)
+			if len(path) > 1 {
+				parentEntry := path[len(path)-2]
+				parentName = parentEntry.Name
+			}
+		} else {
+			return nil, nil, fmt.Errorf("创建模板需要指定对象类型路径")
+		}
+		tpl, err = q.buildCreateTemplate(ctx, objectType, parentName)
+		if err != nil {
+			return nil, nil, err
+		}
 	} else {
-		tpl, err = q.describeUpdateTemplate(ctx, path)
-	}
-	if err != nil {
-		return nil, nil, err
+		// update 模板
+		tpl, err = q.buildUpdateTemplate(ctx, path)
+		if err != nil {
+			return nil, nil, err
+		}
 	}
 	// 将 ObjectTemplate 中的 TemplateField 转换为通用的 PropertyDefinition,并收集 Current 值到 PropertyValues
 	var defs meta.PropertyDefinitions
@@ -2197,70 +1550,524 @@ func (q *MySQLDriver) GetObjectProperties(ctx context.Context, path meta.ObjectP
 	return defs, curr, nil
 }
 
-// ApplyChanges 实现对象的创建/更新/删除操作,内部委托到已有的 createObject/updateObject/delete* helper
-// - action: create/update/delete
-// - options.DryRun=true 时仅预览(不执行),否则执行对应操作
-func (q *MySQLDriver) ApplyChanges(ctx context.Context, action meta.ObjectAction, path meta.ObjectPath, changes meta.PropertyValues, options meta.ApplyOptions) (meta.ApplyResult, error) {
-	// 注意:DryRun=true 对应 Execute=false(旧接口中 Execute 控制是否执行 SQL)
-	execute := !options.DryRun
-	switch action {
-	case meta.ObjectAction("create"):
-		// 使用已有的 createObject API 生成/执行创建语句
-		req := meta.CreateObjectRequest{
-			ObjectType: path[len(path)-1].Type,
-			ParentName: "",
-			Properties: changes,
-			Execute:    execute,
+// applyCreate 处理 create 操作的预览与执行逻辑(从原 ApplyChanges 中抽出)
+func (q *MySQLDriver) applyCreate(ctx context.Context, path meta.ObjectPath, props meta.PropertyValues, execute bool) (meta.ApplyResult, error) {
+	t := strings.ToLower(path[len(path)-1].Type)
+	parentName := ""
+	if len(path) >= 2 {
+		parentName = path[len(path)-2].Name
+	}
+	var generated []string
+	switch t {
+	case "database":
+		nameI, ok := props["databaseName"]
+		if !ok {
+			return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: databaseName")
 		}
-		if len(path) >= 2 {
-			req.ParentName = path[len(path)-2].Name
+		name, _ := nameI.(string)
+		if name == "" {
+			return meta.ApplyResult{}, fmt.Errorf("databaseName 不能为空")
 		}
-		resp, err := q.createObject(ctx, req)
-		if err != nil {
-			return meta.ApplyResult{}, err
+		if execute {
+			return meta.ApplyResult{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
 		}
-		var script string
-		if len(resp.GeneratedSQL) > 0 {
-			script = strings.Join(resp.GeneratedSQL, "\n")
+		charset := "utf8mb4"
+		if cs, ok := props["charset"].(string); ok && cs != "" {
+			charset = cs
 		}
-		return meta.ApplyResult{Script: script, Message: "ok"}, nil
-	case meta.ObjectAction("update"):
-		ureq := meta.UpdateObjectRequest{
-			Path:       path,
-			Properties: changes,
-			Execute:    execute,
+		ifNot := false
+		if v, ok := props["ifNotExists"].(bool); ok {
+			ifNot = v
 		}
-		uresp, err := q.updateObject(ctx, ureq)
-		if err != nil {
-			return meta.ApplyResult{}, err
+		sqlStr := "CREATE DATABASE"
+		if ifNot {
+			sqlStr += " IF NOT EXISTS"
 		}
-		var script string
-		if len(uresp.GeneratedSQL) > 0 {
-			script = strings.Join(uresp.GeneratedSQL, "\n")
+		sqlStr = fmt.Sprintf("%s `%s` DEFAULT CHARACTER SET = %s;", sqlStr, name, charset)
+		generated = []string{sqlStr}
+
+	case "table":
+		tnameI, ok := props["tableName"]
+		if !ok {
+			return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: tableName")
 		}
-		return meta.ApplyResult{Script: script, Message: "ok"}, nil
-	case meta.ObjectAction("delete"):
-		// Build ObjectOperationRequest
-		if len(path) == 0 {
-			return meta.ApplyResult{}, fmt.Errorf("path required for delete")
-		}
-		last := path[len(path)-1]
-		obj := meta.GenericObject{Type: last.Type, Name: last.Name}
-		doreq := meta.ObjectOperationRequest{Object: obj, Execute: execute}
-		// If deleting root (database)
-		if len(path) == 1 {
-			resp, err := q.deleteRootObjects(ctx, doreq)
+		tname, _ := tnameI.(string)
+		if tname == "" {
+			return meta.ApplyResult{}, fmt.Errorf("tableName 不能为空")
+		}
+		colsI, ok := props["columns"]
+		if !ok {
+			return meta.ApplyResult{}, fmt.Errorf("缺少必填字段: columns")
+		}
+		colsSlice, ok := colsI.([]interface{})
+		if !ok {
+			return meta.ApplyResult{}, fmt.Errorf("columns 格式无效,期望为数组类型的列定义,例如 [{\"name\":\"id\",\"type\":\"INT\"}]")
+		}
+		if execute {
+			return meta.ApplyResult{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
+		}
+		engine := "InnoDB"
+		if e, ok := props["engine"].(string); ok && e != "" {
+			engine = e
+		}
+		charset := "utf8mb4"
+		if cs, ok := props["charset"].(string); ok && cs != "" {
+			charset = cs
+		}
+		ifNot := false
+		if v, ok := props["ifNotExists"].(bool); ok {
+			ifNot = v
+		}
+		var colDefs []string
+		for _, ci := range colsSlice {
+			m, ok := ci.(map[string]interface{})
+			if !ok {
+				continue
+			}
+			cname, _ := m["name"].(string)
+			ctype, _ := m["type"].(string)
+			if cname == "" || ctype == "" {
+				continue
+			}
+			nullable := true
+			if n, ok := m["nullable"].(bool); ok {
+				nullable = n
+			}
+			autoInc := false
+			if a, ok := m["autoIncrement"].(bool); ok {
+				autoInc = a
+			}
+			defStr := ""
+			if d, ok := m["default"]; ok && d != nil {
+				switch v := d.(type) {
+				case string:
+					defStr = fmt.Sprintf(" DEFAULT '%s'", strings.ReplaceAll(v, "'", "\\'"))
+				default:
+					defStr = fmt.Sprintf(" DEFAULT %v", v)
+				}
+			}
+			col := fmt.Sprintf("`%s` %s", cname, ctype)
+			if !nullable {
+				col += " NOT NULL"
+			}
+			if autoInc {
+				col += " AUTO_INCREMENT"
+			}
+			col += defStr
+			colDefs = append(colDefs, col)
+		}
+		if len(colDefs) == 0 {
+			return meta.ApplyResult{}, fmt.Errorf("未找到有效的列定义,请检查 properties.columns 字段,示例格式: [{\"name\":\"id\",\"type\":\"INT\"}]")
+		}
+		createStmt := "CREATE TABLE"
+		if ifNot {
+			createStmt += " IF NOT EXISTS"
+		}
+		if parentName != "" {
+			createStmt = fmt.Sprintf("%s `%s`.`%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, parentName, tname, strings.Join(colDefs, ", "), engine, charset)
+		} else {
+			createStmt = fmt.Sprintf("%s `%s` ( %s ) ENGINE=%s DEFAULT CHARSET=%s;", createStmt, tname, strings.Join(colDefs, ", "), engine, charset)
+		}
+		generated = []string{createStmt}
+
+	case "index":
+		iname, _ := props["indexName"].(string)
+		cols, _ := props["columns"].(string)
+		unique := false
+		if u, ok := props["unique"].(bool); ok {
+			unique = u
+		}
+		if iname == "" || cols == "" {
+			return meta.ApplyResult{}, fmt.Errorf("indexName 与 columns 为必填字段")
+		}
+		if execute {
+			return meta.ApplyResult{}, fmt.Errorf("execute 路径未实现(当前仅支持预览 execute=false)")
+		}
+		uq := ""
+		if unique {
+			uq = "UNIQUE "
+		}
+		tableRef := iname
+		if parentName != "" {
+			tableRef = parentName
+		}
+		stmt := fmt.Sprintf("CREATE %sINDEX `%s` ON `%s` (%s);", uq, iname, tableRef, cols)
+		generated = []string{stmt}
+
+	default:
+		return meta.ApplyResult{}, fmt.Errorf("不支持的 create 类型: %s", t)
+	}
+	var script string
+	if len(generated) > 0 {
+		script = strings.Join(generated, "\n")
+	}
+	return meta.ApplyResult{Script: script, Message: "ok"}, nil
+}
+
+// applyUpdate 处理 update 操作(生成 SQL 或执行)
+func (q *MySQLDriver) applyUpdate(ctx context.Context, path meta.ObjectPath, changes meta.PropertyValues, execute bool) (meta.ApplyResult, error) {
+	if len(path) == 0 {
+		return meta.ApplyResult{}, fmt.Errorf("path required for update")
+	}
+	last := path[len(path)-1]
+	objectType := strings.ToLower(last.Type)
+	objectName := last.Name
+	var sqls []string
+	switch objectType {
+	case "database":
+		charset, _ := changes["charset"].(string)
+		collation, _ := changes["collation"].(string)
+		if charset != "" {
+			sqls = append(sqls, fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET %s", objectName, charset))
+		}
+		if collation != "" {
+			sqls = append(sqls, fmt.Sprintf("ALTER DATABASE `%s` COLLATE %s", objectName, collation))
+		}
+
+	case "table":
+		if len(path) < 2 {
+			return meta.ApplyResult{}, fmt.Errorf("表路径需要包含数据库信息")
+		}
+		dbName := path[0].Name
+		tableName := objectName
+		engine, _ := changes["engine"].(string)
+		charset, _ := changes["charset"].(string)
+		collation, _ := changes["collation"].(string)
+		if engine != "" {
+			sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` ENGINE = %s", dbName, tableName, engine))
+		}
+		if charset != "" {
+			sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` CONVERT TO CHARACTER SET %s", dbName, tableName, charset))
+		}
+		if collation != "" {
+			sqls = append(sqls, fmt.Sprintf("ALTER TABLE `%s`.`%s` COLLATE %s", dbName, tableName, collation))
+		}
+
+	default:
+		return meta.ApplyResult{}, fmt.Errorf("不支持的 update 类型: %s", objectType)
+	}
+	if !execute {
+		return meta.ApplyResult{Script: strings.Join(sqls, "\n"), Message: "ok"}, nil
+	}
+	for _, s := range sqls {
+		if _, err := q.db.ExecContext(ctx, s); err != nil {
+			return meta.ApplyResult{}, fmt.Errorf("执行SQL失败: %s, 错误: %v", s, err)
+		}
+	}
+	return meta.ApplyResult{Script: strings.Join(sqls, "\n"), Message: "ok"}, nil
+}
+
+// applyDelete 处理 delete 操作(包括 root 与 child 删除逻辑)
+func (q *MySQLDriver) applyDelete(ctx context.Context, path meta.ObjectPath, execute bool) (meta.ApplyResult, error) {
+	if len(path) == 0 {
+		return meta.ApplyResult{}, fmt.Errorf("path required for delete")
+	}
+	last := path[len(path)-1]
+	obj := meta.GenericObject{Type: last.Type, Name: last.Name}
+	// root 删除(如数据库)
+	if len(path) == 1 {
+		db := q.db
+		rootName := obj.Name
+		typeName := obj.Type
+		t := strings.ToLower(typeName)
+		pattern := rootName
+		if strings.Contains(pattern, "*") {
+			pattern = strings.ReplaceAll(pattern, "*", "%")
+		}
+		useLike := strings.Contains(pattern, "%") || strings.Contains(pattern, "_")
+		switch t {
+		case "database", "schema":
+			var rows *sql.Rows
+			var err error
+			if pattern == "" {
+				rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA`)
+			} else if useLike {
+				rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME LIKE ?`, pattern)
+			} else {
+				rows, err = db.QueryContext(ctx, `SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, pattern)
+			}
+			if err != nil {
+				return meta.ApplyResult{}, fmt.Errorf("查询 schema 列表以供删除匹配失败:%w", err)
+			}
+			defer rows.Close()
+			var objs []meta.GenericObject
+			var total int64
+			for rows.Next() {
+				var name sql.NullString
+				var charset sql.NullString
+				if err := rows.Scan(&name, &charset); err != nil {
+					continue
+				}
+				total++
+				objs = append(objs, meta.GenericObject{
+					ID:       fmt.Sprintf("db-%s", name.String),
+					Name:     name.String,
+					Type:     "database",
+					ParentID: "",
+					DBType:   "mysql",
+					Attrs: map[string]string{
+						"charset": charset.String,
+					},
+				})
+			}
+			matchMap := map[string]interface{}{"matches": objs}
+			b, _ := json.Marshal(matchMap)
+			if !execute {
+				return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil
+			}
+			tx, err := db.BeginTx(ctx, nil)
+			if err != nil {
+				return meta.ApplyResult{}, fmt.Errorf("开始事务失败:%w", err)
+			}
+			var execCount int64
+			var execSQLs []string
+			for _, o := range objs {
+				switch o.Type {
+				case "database":
+					sqlStr := fmt.Sprintf("DROP DATABASE `%s`", o.Name)
+					if _, err := tx.ExecContext(ctx, sqlStr); err != nil {
+						_ = tx.Rollback()
+						return meta.ApplyResult{}, fmt.Errorf("执行 SQL 失败:%s, err: %w", sqlStr, err)
+					}
+					execSQLs = append(execSQLs, sqlStr)
+					execCount++
+				default:
+				}
+			}
+			if err := tx.Commit(); err != nil {
+				_ = tx.Rollback()
+				return meta.ApplyResult{}, fmt.Errorf("提交事务失败:%w", err)
+			}
+			if b, err := json.Marshal(execSQLs); err == nil {
+				return meta.ApplyResult{Script: string(b), AffectedRows: execCount}, nil
+			}
+			return meta.ApplyResult{Script: "", AffectedRows: execCount}, nil
+
+		case "table", "view":
+			tableType := "BASE TABLE"
+			if t == "view" {
+				tableType = "VIEW"
+			}
+			var rows *sql.Rows
+			var err error
+			if pattern == "" {
+				rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ?`, tableType)
+			} else if useLike {
+				rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME LIKE ?`, tableType, pattern)
+			} else {
+				rows, err = db.QueryContext(ctx, `SELECT TABLE_SCHEMA, TABLE_NAME, NULL AS ENGINE, CREATE_TIME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ? AND TABLE_NAME = ?`, tableType, pattern)
+			}
 			if err != nil {
-				return meta.ApplyResult{}, err
+				return meta.ApplyResult{}, fmt.Errorf("查询表/视图以供删除匹配失败:%w", err)
+			}
+			defer rows.Close()
+			var objs []meta.GenericObject
+			var total int64
+			for rows.Next() {
+				var schema sql.NullString
+				var tname sql.NullString
+				var engine sql.NullString
+				var ctime sql.NullString
+				if err := rows.Scan(&schema, &tname, &engine, &ctime); err != nil {
+					continue
+				}
+				total++
+				pid := fmt.Sprintf("db-%s", schema.String)
+				id := fmt.Sprintf("%s.table-%s", pid, tname.String)
+				objs = append(objs, meta.GenericObject{
+					ID:       id,
+					Name:     tname.String,
+					Type:     t,
+					ParentID: pid,
+					DBType:   "mysql",
+					Attrs: map[string]string{
+						"engine":     engine.String,
+						"createTime": ctime.String,
+					},
+				})
+			}
+			matchMap := map[string]interface{}{"matches": objs}
+			if b, err := json.Marshal(matchMap); err == nil {
+				return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil
 			}
-			return meta.ApplyResult{Script: resp.Sql, AffectedRows: resp.Affected}, nil
+			return meta.ApplyResult{Script: "", AffectedRows: total}, nil
+		default:
+			return meta.ApplyResult{}, fmt.Errorf("不支持的 root 类型: %s", typeName)
+		}
+	}
+
+	// child 删除逻辑
+	parentID := fmt.Sprintf("db-%s", path[len(path)-2].Name)
+	filter := map[string]string{}
+	if obj.Name != "" {
+		filter["name"] = obj.Name
+	}
+	if obj.Type != "" {
+		filter["type"] = obj.Type
+	}
+	parts := strings.Split(parentID, ".")
+	var dbPart string
+	if len(parts) >= 2 {
+		dbPart = parts[1]
+	} else {
+		dbPart = parentID
+	}
+	dbName := strings.TrimPrefix(dbPart, "db-")
+	objectType := strings.ToLower(filter["type"])
+	namePattern := filter["name"]
+	if strings.Contains(namePattern, "*") {
+		namePattern = strings.ReplaceAll(namePattern, "*", "%")
+	}
+	useLike := strings.Contains(namePattern, "%") || strings.Contains(namePattern, "_")
+	db := q.db
+	switch objectType {
+	case "index":
+		var rows *sql.Rows
+		var err error
+		if namePattern == "" {
+			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName)
+		} else if useLike {
+			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME LIKE ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern)
+		} else {
+			rows, err = db.QueryContext(ctx, `SELECT INDEX_NAME, TABLE_NAME, NON_UNIQUE FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME = ? GROUP BY INDEX_NAME, TABLE_NAME, NON_UNIQUE`, dbName, namePattern)
 		}
-		// else child
-		resp, err := q.deleteChildObjects(ctx, doreq)
 		if err != nil {
-			return meta.ApplyResult{}, err
+			return meta.ApplyResult{}, fmt.Errorf("查询索引以供删除匹配失败:%w", err)
 		}
-		return meta.ApplyResult{Script: resp.Sql, AffectedRows: resp.Affected}, nil
+		defer rows.Close()
+		var res []meta.GenericObject
+		var total int64
+		for rows.Next() {
+			var idx sql.NullString
+			var tname sql.NullString
+			var nonUnique sql.NullInt64
+			if err := rows.Scan(&idx, &tname, &nonUnique); err != nil {
+				continue
+			}
+			total++
+			id := fmt.Sprintf("%s.table-%s.index-%s", parentID, tname.String, idx.String)
+			res = append(res, meta.GenericObject{
+				ID:       id,
+				Name:     idx.String,
+				Type:     "index",
+				ParentID: fmt.Sprintf("%s.table-%s", parentID, tname.String),
+				DBType:   "mysql",
+				Attrs: map[string]string{
+					"table":     tname.String,
+					"nonUnique": fmt.Sprintf("%d", nonUnique.Int64),
+				},
+			})
+		}
+		if b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil {
+			return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil
+		}
+		return meta.ApplyResult{Script: "", AffectedRows: total}, nil
+	case "procedure", "proc":
+		var rows *sql.Rows
+		var err error
+		if namePattern == "" {
+			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE'`, dbName)
+		} else if useLike {
+			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME LIKE ?`, dbName, namePattern)
+		} else {
+			rows, err = db.QueryContext(ctx, `SELECT ROUTINE_NAME, ROUTINE_DEFINITION, CREATED FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' AND ROUTINE_NAME = ?`, dbName, namePattern)
+		}
+		if err != nil {
+			return meta.ApplyResult{}, fmt.Errorf("查询存储过程以供删除匹配失败:%w", err)
+		}
+		defer rows.Close()
+		var res []meta.GenericObject
+		var total int64
+		for rows.Next() {
+			var rName sql.NullString
+			var def sql.NullString
+			var created sql.NullString
+			if err := rows.Scan(&rName, &def, &created); err != nil {
+				continue
+			}
+			total++
+			id := fmt.Sprintf("%s.proc-%s", parentID, rName.String)
+			res = append(res, meta.GenericObject{
+				ID:       id,
+				Name:     rName.String,
+				Type:     "procedure",
+				ParentID: parentID,
+				DBType:   "mysql",
+				Attrs: map[string]string{
+					"definition": def.String,
+					"created":    created.String,
+				},
+			})
+		}
+		if b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil {
+			return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil
+		}
+		return meta.ApplyResult{Script: "", AffectedRows: total}, nil
+	case "trigger":
+		var rows *sql.Rows
+		var err error
+		if namePattern == "" {
+			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ?`, dbName)
+		} else if useLike {
+			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME LIKE ?`, dbName, namePattern)
+		} else {
+			rows, err = db.QueryContext(ctx, `SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME = ?`, dbName, namePattern)
+		}
+		if err != nil {
+			return meta.ApplyResult{}, fmt.Errorf("查询触发器以供删除匹配失败:%w", err)
+		}
+		defer rows.Close()
+		var res []meta.GenericObject
+		var total int64
+		for rows.Next() {
+			var tName sql.NullString
+			var event sql.NullString
+			var objTable sql.NullString
+			var timing sql.NullString
+			var stmt sql.NullString
+			if err := rows.Scan(&tName, &event, &objTable, &timing, &stmt); err != nil {
+				continue
+			}
+			total++
+			id := fmt.Sprintf("%s.trigger-%s", parentID, tName.String)
+			res = append(res, meta.GenericObject{
+				ID:       id,
+				Name:     tName.String,
+				Type:     "trigger",
+				ParentID: parentID,
+				DBType:   "mysql",
+				Attrs: map[string]string{
+					"event":     event.String,
+					"table":     objTable.String,
+					"timing":    timing.String,
+					"statement": stmt.String,
+				},
+			})
+		}
+		if b, err := json.Marshal(map[string]interface{}{"matches": res}); err == nil {
+			return meta.ApplyResult{Script: string(b), AffectedRows: total}, nil
+		}
+		return meta.ApplyResult{Script: "", AffectedRows: total}, nil
+	default:
+		return meta.ApplyResult{}, fmt.Errorf("不支持的 child 类型: %s", objectType)
+	}
+}
+
+// ApplyChanges 实现对象的创建/更新/删除操作,内部委托到已有的 createObject/updateObject/delete* helper
+// - action: create/update/delete
+// - options.DryRun=true 时仅预览(不执行),否则执行对应操作
+// 说明:为了兼容旧调用约定,`options.DryRun=true` 等价于旧接口中的 `Execute=false`。
+// 驱动实现应使用 `options.DryRun` 或 `meta.ApplyOptions` 中的明确字段决定是否真正执行 SQL,而不是依赖旧的 Execute 标志。
+func (q *MySQLDriver) ApplyChanges(ctx context.Context, action meta.ObjectAction, path meta.ObjectPath, changes meta.PropertyValues, options meta.ApplyOptions) (meta.ApplyResult, error) {
+	// 注意:DryRun=true 对应 Execute=false(旧接口中 Execute 控制是否执行 SQL)
+	execute := !options.DryRun
+	switch action {
+	case meta.ObjectAction("create"):
+		return q.applyCreate(ctx, path, changes, execute)
+	case meta.ObjectAction("update"):
+		return q.applyUpdate(ctx, path, changes, execute)
+	case meta.ObjectAction("delete"):
+		return q.applyDelete(ctx, path, execute)
 	default:
 		return meta.ApplyResult{}, fmt.Errorf("unsupported action: %s", action)
 	}

+ 4 - 16
service/internal/common/databases/interfaces.go

@@ -109,19 +109,7 @@ type DataExecutor interface {
 	ExecuteSQL(ctx context.Context, path meta.ObjectPath, sql string, params []interface{}, includeLarge bool) (meta.ExecuteResult, error)
 }
 
-// 以下为向后兼容的旧接口定义(保留,便于逐步迁移)
-type MetadataDeleter interface {
-	DescribeDeleteTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error)
-	DeleteRootObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error)
-	DeleteChildObjects(ctx context.Context, req meta.ObjectOperationRequest) (meta.ObjectOperationResponse, error)
-}
-
-type MetadataCreater interface {
-	DescribeCreateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error)
-	CreateObject(ctx context.Context, req meta.CreateObjectRequest) (meta.CreateObjectResponse, error)
-}
-
-type MetadataUpdater interface {
-	DescribeUpdateTemplate(ctx context.Context, path meta.ObjectPath) (meta.ObjectTemplate, error)
-	UpdateObject(ctx context.Context, req meta.UpdateObjectRequest) (meta.UpdateObjectResponse, error)
-}
+// 旧的基于 template/operation 的接口已移除,驱动应实现新接口:
+// - MetadataReader
+// - ObjectManager
+// 这有助于统一基于结构化路径(meta.ObjectPath)的元数据访问与对象管理模型。

+ 2 - 1
service/internal/common/databases/meta/meta.go

@@ -91,7 +91,8 @@ const (
 type ObjectTemplate struct {
 	ObjectType string `json:"objectType"`
 	// Operation 指示模板用途:"create" | "update" | "delete"。
-	// 保持向后兼容:旧实现若未设置该字段,仍按调用接口语义处理。
+	// 说明:为了兼容历史实现,如果旧驱动未设置该字段,调用方可根据上下文推断用途。
+	// 新实现/驱动应显式设置该字段以提高语义清晰度并减少歧义。
 	Operation string `json:"operation,omitempty"`
 
 	ParentHint string          `json:"parentHint,omitempty"` // 提示父上下文(例如需要传入 db 名称)

+ 0 - 427
service/internal/common/logger/DESIGN.md

@@ -1,427 +0,0 @@
-# 符合Go理念的日志系统设计
-
-## Go语言核心理念遵循
-
-### 1. 简单性 (Simplicity)
-- **最小API**: 只提供必要的方法,避免过度设计
-- **清晰的职责分离**: 每个组件职责单一
-- **直观的接口**: 接口设计符合Go习惯
-
-### 2. 可组合性 (Composition)
-- **接口组合**: 使用小接口,支持组合使用
-- **选项模式**: 使用函数选项模式进行配置
-- **中间件模式**: 支持HTTP中间件组合
-
-### 3. 并发安全 (Concurrency)
-- **goroutine安全**: 所有组件都是并发安全的
-- **上下文支持**: 集成context.Context
-- **优雅关闭**: 支持优雅关闭和资源清理
-
-### 4. 错误处理 (Error Handling)
-- **显式错误**: 遵循Go的显式错误处理
-- **错误包装**: 使用fmt.Errorf进行错误包装
-- **错误检查**: 提供便捷的错误检查方法
-
-### 5. 接口导向 (Interface-oriented)
-- **小接口**: 设计小而专注的接口
-- **依赖注入**: 通过接口进行依赖注入
-- **可测试性**: 接口设计便于单元测试
-
-## 重新设计的架构
-
-### 核心接口
-
-#### Logger 接口 - 简单而强大
-```go
-type Logger interface {
-    // 核心日志方法
-    Debug(msg string, fields ...zap.Field)
-    Info(msg string, fields ...zap.Field)
-    Warn(msg string, fields ...zap.Field)
-    Error(msg string, fields ...zap.Field)
-    Fatal(msg string, fields ...zap.Field)
-
-    // 便捷方法
-    With(fields ...zap.Field) Logger
-    Named(name string) Logger
-
-    // 同步方法
-    Sync() error
-}
-```
-
-#### AuditLogger 接口 - 审计专用
-```go
-type AuditLogger interface {
-    // 记录审计事件
-    Record(entry *AuditEntry) error
-
-    // 查询审计日志
-    Query(filter *AuditFilter) ([]*AuditEntry, error)
-
-    // 清理过期数据
-    Cleanup(retention time.Duration) error
-
-    // 同步方法
-    Sync() error
-}
-```
-
-### 选项模式配置
-
-#### Logger 配置选项
-```go
-type LoggerOption func(*loggerConfig)
-
-func WithLevel(level zapcore.Level) LoggerOption {
-    return func(c *loggerConfig) {
-        c.level = level
-    }
-}
-
-func WithOutputPaths(paths ...string) LoggerOption {
-    return func(c *loggerConfig) {
-        c.outputPaths = paths
-    }
-}
-
-func WithDevelopment(dev bool) LoggerOption {
-    return func(c *loggerConfig) {
-        c.development = dev
-    }
-}
-```
-
-#### Audit 配置选项
-```go
-type AuditOption func(*auditConfig)
-
-func WithDatabasePath(path string) AuditOption {
-    return func(c *auditConfig) {
-        c.dbPath = path
-    }
-}
-
-func WithRetention(days int) AuditOption {
-    return func(c *auditConfig) {
-        c.retentionDays = days
-    }
-}
-```
-
-### 构造函数设计
-
-#### Logger 构造函数
-```go
-func NewLogger(opts ...LoggerOption) (Logger, error) {
-    config := &loggerConfig{
-        level:       zapcore.InfoLevel,
-        development: false,
-        encoding:    "json",
-        outputPaths: []string{"stdout"},
-    }
-
-    for _, opt := range opts {
-        opt(config)
-    }
-
-    return newZapLogger(config)
-}
-```
-
-#### AuditLogger 构造函数
-```go
-func NewAuditLogger(opts ...AuditOption) (AuditLogger, error) {
-    config := &auditConfig{
-        enabled:       true,
-        dbPath:        "audit.db",
-        retentionDays: 90,
-        bufferSize:    1000,
-    }
-
-    for _, opt := range opts {
-        opt(config)
-    }
-
-    return newSQLiteAuditLogger(config)
-}
-```
-
-## 数据结构优化
-
-### AuditEntry - 符合Go习惯
-```go
-type AuditEntry struct {
-    ID        string    `json:"id"`
-    Timestamp time.Time `json:"timestamp"`
-    UserID    string    `json:"user_id,omitempty"`
-    Username  string    `json:"username,omitempty"`
-    Action    string    `json:"action"`
-    Resource  string    `json:"resource,omitempty"`
-    Details   string    `json:"details,omitempty"`
-    IP        string    `json:"ip,omitempty"`
-    UserAgent string    `json:"user_agent,omitempty"`
-    Success   bool      `json:"success"`
-    Error     string    `json:"error,omitempty"`
-}
-```
-
-### AuditFilter - 查询过滤器
-```go
-type AuditFilter struct {
-    UserID    string
-    Action    string
-    Resource  string
-    StartTime *time.Time
-    EndTime   *time.Time
-    Success   *bool
-    Limit     int
-    Offset    int
-}
-```
-
-## 符合Go习惯的使用方式
-
-### 基本使用
-```go
-// 创建日志器
-logger, err := NewLogger(
-    WithLevel(zapcore.DebugLevel),
-    WithDevelopment(true),
-    WithOutputPaths("stdout", "app.log"),
-)
-if err != nil {
-    panic(err)
-}
-defer logger.Sync()
-
-// 使用日志
-logger.Info("用户登录",
-    zap.String("user_id", "123"),
-    zap.String("ip", "192.168.1.1"))
-```
-
-### 审计使用
-```go
-// 创建审计日志器
-audit, err := NewAuditLogger(
-    WithDatabasePath("audit.db"),
-    WithRetention(90),
-)
-if err != nil {
-    panic(err)
-}
-defer audit.Sync()
-
-// 记录审计事件
-err = audit.Record(&AuditEntry{
-    UserID:   "123",
-    Username: "john_doe",
-    Action:   "login",
-    IP:       "192.168.1.1",
-    Success:  true,
-})
-```
-
-### 上下文使用
-```go
-// 创建带上下文的日志器
-userLogger := logger.Named("user_service").With(
-    zap.String("user_id", "123"),
-    zap.String("request_id", "req_001"),
-)
-
-// 在函数中使用
-func processUser(ctx context.Context, userID string) error {
-    logger := userLogger.With(zap.String("operation", "process_user"))
-
-    logger.Info("开始处理用户")
-    defer logger.Info("处理用户完成")
-
-    // 处理逻辑
-    if err := doSomething(); err != nil {
-        logger.Error("处理失败", zap.Error(err))
-        return err
-    }
-
-    return nil
-}
-```
-
-### HTTP中间件
-```go
-func AuditMiddleware(audit AuditLogger) gin.HandlerFunc {
-    return func(c *gin.Context) {
-        start := time.Now()
-
-        // 处理请求
-        c.Next()
-
-        // 记录审计
-        entry := &AuditEntry{
-            UserID:    getUserID(c),
-            Action:    c.Request.Method + " " + c.Request.URL.Path,
-            IP:        c.ClientIP(),
-            UserAgent: c.Request.UserAgent(),
-            Success:   c.Writer.Status() < 400,
-        }
-
-        if err := audit.Record(entry); err != nil {
-            // 审计失败不应该影响正常业务
-            c.Error(err)
-        }
-
-        // 记录访问日志
-        duration := time.Since(start)
-        logger.Info("HTTP请求",
-            zap.String("method", c.Request.Method),
-            zap.String("path", c.Request.URL.Path),
-            zap.Int("status", c.Writer.Status()),
-            zap.Duration("duration", duration),
-        )
-    }
-}
-```
-
-## 错误处理设计
-
-### 自定义错误类型
-```go
-type LoggerError struct {
-    Op  string // 操作
-    Err error  // 底层错误
-}
-
-func (e *LoggerError) Error() string {
-    return fmt.Sprintf("logger %s: %v", e.Op, e.Err)
-}
-
-func (e *LoggerError) Unwrap() error {
-    return e.Err
-}
-```
-
-### 便捷的错误检查
-```go
-// 检查是否为日志错误
-var loggerErr *LoggerError
-if errors.As(err, &loggerErr) {
-    // 处理日志相关错误
-}
-
-// 检查特定操作的错误
-if errors.Is(err, ErrAuditDisabled) {
-    // 审计功能被禁用
-}
-```
-
-## 测试设计
-
-### 单元测试
-```go
-func TestLogger(t *testing.T) {
-    logger, err := NewLogger(WithDevelopment(true))
-    require.NoError(t, err)
-    defer logger.Sync()
-
-    // 测试日志输出
-    logger.Info("测试消息", zap.String("key", "value"))
-
-    // 验证日志内容(通过钩子或缓冲区)
-}
-```
-
-### 集成测试
-```go
-func TestAuditLogger(t *testing.T) {
-    audit, err := NewAuditLogger(WithDatabasePath(":memory:"))
-    require.NoError(t, err)
-    defer audit.Sync()
-
-    // 插入测试数据
-    entry := &AuditEntry{
-        UserID:  "test_user",
-        Action:  "test_action",
-        Success: true,
-    }
-
-    err = audit.Record(entry)
-    require.NoError(t, err)
-
-    // 查询验证
-    entries, err := audit.Query(&AuditFilter{
-        UserID: "test_user",
-        Limit:  10,
-    })
-    require.NoError(t, err)
-    require.Len(t, entries, 1)
-}
-```
-
-## 性能优化
-
-### 对象池
-```go
-var auditEntryPool = sync.Pool{
-    New: func() interface{} {
-        return &AuditEntry{}
-    },
-}
-
-func getAuditEntry() *AuditEntry {
-    return auditEntryPool.Get().(*AuditEntry)
-}
-
-func putAuditEntry(entry *AuditEntry) {
-    // 重置对象
-    *entry = AuditEntry{}
-    auditEntryPool.Put(entry)
-}
-```
-
-### 异步写入
-```go
-type asyncAuditLogger struct {
-    AuditLogger
-    entries chan *AuditEntry
-    done    chan struct{}
-}
-
-func (a *asyncAuditLogger) Record(entry *AuditEntry) error {
-    select {
-    case a.entries <- entry:
-        return nil
-    case <-a.done:
-        return ErrLoggerClosed
-    default:
-        return ErrBufferFull
-    }
-}
-```
-
-## 文档和示例
-
-### 包文档
-```go
-// Package logger 提供基于Zap的高性能日志和审计功能
-//
-// 设计理念:
-//   - 简单性: 最小化API,专注于核心功能
-//   - 性能: 利用Zap的高性能特性
-//   - 可组合性: 支持中间件和选项模式
-//   - 并发安全: 所有操作都是goroutine安全的
-//
-// 基本使用:
-//
-//	logger, _ := logger.NewLogger(logger.WithDevelopment(true))
-//	logger.Info("应用启动")
-//	defer logger.Sync()
-//
-// 审计使用:
-//
-//	audit, _ := logger.NewAuditLogger(logger.WithRetention(90))
-//	audit.Record(&logger.AuditEntry{Action: "login", Success: true})
-package logger
-```
-
-这个重新设计的日志系统完全遵循Go语言的核心理念,提供了简单、高效、可组合的日志和审计解决方案。

+ 1 - 2
service/internal/common/logger/types.go

@@ -113,8 +113,7 @@ func FromContext(ctx context.Context) Logger {
 	if logger, ok := ctx.Value(loggerKey{}).(Logger); ok {
 		return logger
 	}
-	// Ensure the package default logger is initialized lazily to avoid
-	// returning nil when called from contexts without an injected logger.
+	// 确保包级默认 logger 使用懒惰初始化,避免在没有注入 logger 的 context 中返回 nil。
 	defaultOnce.Do(initDefaultLogger)
 	return defaultLogger
 }

+ 22 - 15
service/internal/common/manager/storage/db_storage/connection_operations.go

@@ -38,10 +38,17 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 		args = append(args, *req.Kind)
 	}
 
-	// Note: detail fields (type/version/server/port/username/password/database/connection_string/use_ssh_tunnel)
-	// moved to specialized detail tables (db_connections / server_connections).
-	// We do not update them in the base `connections` table; instead we perform conditional upserts
-	// on the corresponding detail tables after updating base fields.
+	// 说明:详细的连接信息字段(type/version/server/port/username/password/database/connection_string/use_ssh_tunnel)
+	// 已拆分到专门的详情表 `db_connections` 与 `server_connections` 中。
+	// 因此这些字段不会在基础 `connections` 表中直接更新;当请求包含任一详情字段时,
+	// 代码将在更新基础字段后,在一个事务内对相应的详情表执行有条件的 upsert(插入或替换)。
+	// upsert 的实现要点:
+	// - 先读取已有的详情行以保留未被请求覆盖的字段,避免将 NULL 或未提供的值错误覆盖。
+	// - 将可空列(如 password、connection_string、ssh_tunnel_connection_id、last_connected)映射到
+	//   本地可空变量并正确处理 `sql.NullString`,以免丢失已有值。
+	// - 对 last_connected 字段尝试多种常见时间格式解析,兼容历史数据的不同存储格式。
+	// - 最终使用 `INSERT OR REPLACE` 来保证详情行被创建或替换,从而完成 upsert。
+	// 这种策略允许部分字段更新(partial updates),并在事务内保证基础表与详情表的一致性。
 	if req.Color != nil {
 		setParts = append(setParts, "color = ?")
 		args = append(args, *req.Color)
@@ -83,11 +90,11 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 	}
 
 	// 如果有 detail 字段需要更新,则在事务中对 detail 表执行 upsert
-	// 构建 db detail upsert
+	// 构建 db 详情表的 upsert(插入或替换)操作
 	var dbUpsertNeeded bool
 	if req.Type != nil || req.Version != nil || req.Server != nil || req.Port != nil || req.Username != nil || req.Password != nil || req.Database != nil || req.ConnectionString != nil || req.UseSSHTunnel != nil {
 		dbUpsertNeeded = true
-		// fill with possible nil/defaults; we'll set actual values below
+		// 占位:预先标记需要对 db 详情表执行 upsert,实际要写入的字段值将在下文读取现有详情并合并后确定。
 	}
 
 	var serverUpsertNeeded bool
@@ -101,7 +108,7 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 			return nil, fmt.Errorf("开始事务失败,连接ID: %s,错误: %v", connID, err)
 		}
 
-		// Handle db_connections upsert
+		// 处理 `db_connections` 表的 upsert(插入或替换)
 		if dbUpsertNeeded {
 			// Read existing detail to preserve unchanged fields
 			var existing types.DBConnectionDetail
@@ -109,15 +116,15 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 			var connStrNull stdsql.NullString
 			var sshTunnelConnNull stdsql.NullString
 			var lastConnectedNull stdsql.NullString
-			var useSSHTunnelInt int
+			var useSSHTunnelNull stdsql.NullInt64
 
-			err := tx.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, connID).Scan(&existing.Type, &existing.Version, &existing.Server, &existing.Port, &existing.Username, &passwordNull, &existing.DatabaseName, &connStrNull, &useSSHTunnelInt, &sshTunnelConnNull, &lastConnectedNull)
+			err := tx.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, connID).Scan(&existing.Type, &existing.Version, &existing.Server, &existing.Port, &existing.Username, &passwordNull, &existing.DatabaseName, &connStrNull, &useSSHTunnelNull, &sshTunnelConnNull, &lastConnectedNull)
 			if err != nil && err.Error() != "sql: no rows in result set" {
 				tx.Rollback()
 				return nil, fmt.Errorf("读取 db_connections 失败,连接ID: %s,错误: %v", connID, err)
 			}
 
-			// map nullable columns to existing struct
+			// 将可空列映射到本地结构体字段,正确处理 `sql.NullString`,以保留已有值或使用空字符串
 			if passwordNull.Valid {
 				existing.Password = passwordNull.String
 			} else {
@@ -133,7 +140,7 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 			} else {
 				existing.SSHTunnelConnection = ""
 			}
-			existing.UseSSHTunnel = useSSHTunnelInt != 0
+			existing.UseSSHTunnel = useSSHTunnelNull.Valid && useSSHTunnelNull.Int64 != 0
 			if lastConnectedNull.Valid {
 				if t, err := time.Parse(time.RFC3339Nano, lastConnectedNull.String); err == nil {
 					existing.LastConnected = t
@@ -144,7 +151,7 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 				}
 			}
 
-			// decide values (prefer explicit Type/Version over DBType/DBVersion)
+			// 确定最终写入的值(优先使用请求中显式提供的 Type/Version 等字段,否则保留已有值)
 			typ := existing.Type
 			if req.Type != nil {
 				typ = *req.Type
@@ -182,8 +189,8 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 				useSSHTunnel = *req.UseSSHTunnel
 			}
 
-			// Use INSERT OR REPLACE to upsert the detail row
-			useSSHTunnelInt = 0
+			// 使用 `INSERT OR REPLACE` 完成 upsert(插入或替换)操作,保证详情行被创建或更新
+			useSSHTunnelInt := 0
 			if useSSHTunnel {
 				useSSHTunnelInt = 1
 			}
@@ -195,7 +202,7 @@ func (sm *StorageManager) UpdateConnection(connID string, req *types.UpdateConne
 			}
 		}
 
-		// Handle server_connections upsert
+		// 处理 `server_connections` 表的 upsert(插入或替换)
 		if serverUpsertNeeded {
 			var existing types.ServerConnectionDetail
 			err := tx.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, connID).Scan(&existing.Type, &existing.Version, &existing.Server, &existing.Port, &existing.Username, &existing.AuthType, &existing.PrivateKey, &existing.UseSudo)

+ 21 - 18
service/internal/common/manager/storage/db_storage/manager.go

@@ -405,7 +405,8 @@ func (sm *StorageManager) CreateConnection(groupID, name, description, kind, typ
 	var connDetail *types.DBConnectionDetail
 	var serverDetail *types.ServerConnectionDetail
 
-	if kind == "database" {
+	switch kind {
+	case "database":
 		insertDBSQL := `INSERT INTO db_connections (connection_id, type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
 		useSSHTunnelInt := 0
 		if useSSHTunnel {
@@ -429,7 +430,7 @@ func (sm *StorageManager) CreateConnection(groupID, name, description, kind, typ
 			SSHTunnelConnection: "",
 			LastConnected:       now,
 		}
-	} else if kind == "server" {
+	case "server":
 		insertServerSQL := `INSERT INTO server_connections (connection_id, type, version, server, port, username, auth_type, private_key, use_sudo) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
 		_, err = tx.Exec(insertServerSQL, id, typ, version, server, port, username, "", "", 0)
 		if err != nil {
@@ -560,15 +561,16 @@ func (sm *StorageManager) GetConnection(connID string) (*types.ConnectionWithDet
 	}
 
 	// Load detail according to explicit kind (no compatibility fallbacks)
-	if kind == "database" {
+	switch kind {
+	case "database":
 		var detail types.DBConnectionDetail
 		var lastConnectedNull sql.NullString
-		var useSSHTunnelInt int
+		var useSSHTunnelNull sql.NullInt64
 		// 使用 sql.NullString 来接收可能为 NULL 的字符串列
 		var passwordNull sql.NullString
 		var connStrNull sql.NullString
 		var sshTunnelConnNull sql.NullString
-		err = sm.db.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, id).Scan(&detail.Type, &detail.Version, &detail.Server, &detail.Port, &detail.Username, &passwordNull, &detail.DatabaseName, &connStrNull, &useSSHTunnelInt, &sshTunnelConnNull, &lastConnectedNull)
+		err = sm.db.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, id).Scan(&detail.Type, &detail.Version, &detail.Server, &detail.Port, &detail.Username, &passwordNull, &detail.DatabaseName, &connStrNull, &useSSHTunnelNull, &sshTunnelConnNull, &lastConnectedNull)
 		if err != nil {
 			return nil, fmt.Errorf("查询 db_connections 失败,连接ID: %s,错误: %v", id, err)
 		}
@@ -589,7 +591,7 @@ func (sm *StorageManager) GetConnection(connID string) (*types.ConnectionWithDet
 			detail.SSHTunnelConnection = ""
 		}
 		detail.ConnectionID = id
-		detail.UseSSHTunnel = useSSHTunnelInt != 0
+		detail.UseSSHTunnel = useSSHTunnelNull.Valid && useSSHTunnelNull.Int64 != 0
 		if lastConnectedNull.Valid {
 			if t, err := time.Parse(time.RFC3339Nano, lastConnectedNull.String); err == nil {
 				detail.LastConnected = t
@@ -600,13 +602,13 @@ func (sm *StorageManager) GetConnection(connID string) (*types.ConnectionWithDet
 			}
 		}
 		conn.DBDetail = &detail
-	} else if kind == "server" {
+	case "server":
 		var sdetail types.ServerConnectionDetail
-		var useSudoInt int
+		var useSudoNull sql.NullInt64
 		// 使用 sql.NullString 来接收可能为 NULL 的字符串列
 		var authTypeNull sql.NullString
 		var privateKeyNull sql.NullString
-		err = sm.db.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, id).Scan(&sdetail.Type, &sdetail.Version, &sdetail.Server, &sdetail.Port, &sdetail.Username, &authTypeNull, &privateKeyNull, &useSudoInt)
+		err = sm.db.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, id).Scan(&sdetail.Type, &sdetail.Version, &sdetail.Server, &sdetail.Port, &sdetail.Username, &authTypeNull, &privateKeyNull, &useSudoNull)
 		if err != nil {
 			return nil, fmt.Errorf("查询 server_connections 失败,连接ID: %s,错误: %v", id, err)
 		}
@@ -621,7 +623,7 @@ func (sm *StorageManager) GetConnection(connID string) (*types.ConnectionWithDet
 			sdetail.PrivateKey = ""
 		}
 		sdetail.ConnectionID = id
-		sdetail.UseSudo = useSudoInt != 0
+		sdetail.UseSudo = useSudoNull.Valid && useSudoNull.Int64 != 0
 		conn.ServerDetail = &sdetail
 	}
 
@@ -718,14 +720,15 @@ func (sm *StorageManager) getConnection(connID string) (*types.ConnectionWithDet
 	}
 
 	// Load detail according to explicit kind (no lock)
-	if kind == "database" {
+	switch kind {
+	case "database":
 		var detail types.DBConnectionDetail
 		var lastConnectedNull sql.NullString
-		var useSSHTunnelInt int
+		var useSSHTunnelNull sql.NullInt64
 		var passwordNull sql.NullString
 		var connStrNull sql.NullString
 		var sshTunnelConnNull sql.NullString
-		err = sm.db.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, id).Scan(&detail.Type, &detail.Version, &detail.Server, &detail.Port, &detail.Username, &passwordNull, &detail.DatabaseName, &connStrNull, &useSSHTunnelInt, &sshTunnelConnNull, &lastConnectedNull)
+		err = sm.db.QueryRow(`SELECT type, version, server, port, username, password, database_name, connection_string, use_ssh_tunnel, ssh_tunnel_connection_id, last_connected FROM db_connections WHERE connection_id = ?`, id).Scan(&detail.Type, &detail.Version, &detail.Server, &detail.Port, &detail.Username, &passwordNull, &detail.DatabaseName, &connStrNull, &useSSHTunnelNull, &sshTunnelConnNull, &lastConnectedNull)
 		if err != nil {
 			return nil, fmt.Errorf("查询 db_connections 失败,连接ID: %s,错误: %v", id, err)
 		}
@@ -745,7 +748,7 @@ func (sm *StorageManager) getConnection(connID string) (*types.ConnectionWithDet
 			detail.SSHTunnelConnection = ""
 		}
 		detail.ConnectionID = id
-		detail.UseSSHTunnel = useSSHTunnelInt != 0
+		detail.UseSSHTunnel = useSSHTunnelNull.Valid && useSSHTunnelNull.Int64 != 0
 		if lastConnectedNull.Valid {
 			if t, err := time.Parse(time.RFC3339Nano, lastConnectedNull.String); err == nil {
 				detail.LastConnected = t
@@ -756,12 +759,12 @@ func (sm *StorageManager) getConnection(connID string) (*types.ConnectionWithDet
 			}
 		}
 		conn.DBDetail = &detail
-	} else if kind == "server" {
+	case "server":
 		var sdetail types.ServerConnectionDetail
-		var useSudoInt int
+		var useSudoNull sql.NullInt64
 		var authTypeNull sql.NullString
 		var privateKeyNull sql.NullString
-		err = sm.db.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, id).Scan(&sdetail.Type, &sdetail.Version, &sdetail.Server, &sdetail.Port, &sdetail.Username, &authTypeNull, &privateKeyNull, &useSudoInt)
+		err = sm.db.QueryRow(`SELECT type, version, server, port, username, auth_type, private_key, use_sudo FROM server_connections WHERE connection_id = ?`, id).Scan(&sdetail.Type, &sdetail.Version, &sdetail.Server, &sdetail.Port, &sdetail.Username, &authTypeNull, &privateKeyNull, &useSudoNull)
 		if err != nil {
 			return nil, fmt.Errorf("查询 server_connections 失败,连接ID: %s,错误: %v", id, err)
 		}
@@ -776,7 +779,7 @@ func (sm *StorageManager) getConnection(connID string) (*types.ConnectionWithDet
 			sdetail.PrivateKey = ""
 		}
 		sdetail.ConnectionID = id
-		sdetail.UseSudo = useSudoInt != 0
+		sdetail.UseSudo = useSudoNull.Valid && useSudoNull.Int64 != 0
 		conn.ServerDetail = &sdetail
 	}
 

+ 0 - 4
service/internal/common/manager/storage/db_storage/sql_scripts.go

@@ -1,4 +0,0 @@
-package db_storage
-
-// This file has been intentionally emptied after moving implementations to `scripts.go`.
-// Keeping this file avoids accidental deletes in some workflows.

+ 2 - 1
service/internal/common/manager/storage/interface.go

@@ -46,7 +46,8 @@ type StorageInterface interface {
 	CreateDefaultConfig() error
 }
 
-// 类型别名,保持向后兼容
+// 类型别名:当前保留以减少迁移成本,但新代码应直接引用 `types` 包中的类型。
+// 未来会逐步移除这些别名以消除隐式依赖。
 type ConnectionGroup = types.ConnectionGroup
 type Script = types.Script
 type ScriptGroup = types.ScriptGroup

+ 100 - 0
service/internal/common/mcp/component.go

@@ -0,0 +1,100 @@
+package mcp
+
+import (
+	"context"
+
+	"dbview/service/internal/common/logger"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+	"go.uber.org/zap"
+)
+
+// Config MCP 组件配置(可按需扩展,例如新增 SSE/WS 等传输层参数)。
+// 目前仅提供名称/版本控制与开关。
+// 若未来需要从全局配置加载,可在 bootstrap 层读取后传入。
+type Config struct {
+	Enable        bool   // 是否启用 MCP 服务
+	ServerName    string // MCP Server 名称(展示给客户端)
+	ServerVersion string // MCP Server 版本
+}
+
+// DefaultConfig 提供一个安全的默认值(关闭状态)。
+func DefaultConfig() Config {
+	return Config{
+		Enable:        false,
+		ServerName:    "dbview-mcp",
+		ServerVersion: "0.1.0",
+	}
+}
+
+// Component 封装 mcp-go Server,便于在项目内统一注册工具、启动 Stdio 服务。
+type Component struct {
+	cfg    Config
+	server *server.MCPServer
+	logger logger.Logger
+}
+
+// NewComponent 创建 MCP 组件;仅注册 Server 与 Logger,不启动。
+func NewComponent(cfg Config, logger logger.Logger) *Component {
+	if cfg.ServerName == "" {
+		cfg.ServerName = "dbview-mcp"
+	}
+	if cfg.ServerVersion == "" {
+		cfg.ServerVersion = "0.1.0"
+	}
+	return &Component{
+		cfg:    cfg,
+		server: server.NewMCPServer(cfg.ServerName, cfg.ServerVersion),
+		logger: logger,
+	}
+}
+
+// RegisterHealthTool 注册一个内置健康检查工具,便于客户端验证 MCP 连接。
+func (c *Component) RegisterHealthTool() {
+	healthTool := mcp.NewTool("health",
+		mcp.WithDescription("返回 MCP 服务健康状态"),
+	)
+
+	c.server.AddTool(healthTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		return &mcp.CallToolResult{
+			Content: []mcp.Content{mcp.TextContent{Text: "ok"}},
+		}, nil
+	})
+}
+
+// RegisterEchoTool 注册一个示例工具,可用于调试与演示。
+// 参数:message(string,可选),返回与输入一致的文本。
+func (c *Component) RegisterEchoTool() {
+	echoTool := mcp.NewTool("echo",
+		mcp.WithDescription("回显传入的 message 文本"),
+		mcp.WithString("message", mcp.Description("要回显的文本")),
+	)
+
+	c.server.AddTool(echoTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		args, _ := req.Params.Arguments.(map[string]any)
+		msg, _ := args["message"].(string)
+		if msg == "" {
+			msg = "(empty)"
+		}
+		return &mcp.CallToolResult{
+			Content: []mcp.Content{mcp.TextContent{Text: msg}},
+		}, nil
+	})
+}
+
+// Server 返回底层 MCPServer,便于外部注册自定义工具。
+func (c *Component) Server() *server.MCPServer {
+	return c.server
+}
+
+// ServeStdio 启动基于 stdio 的 MCP 服务(常用于被 IDE/CLI 以子进程方式拉起)。
+func (c *Component) ServeStdio(ctx context.Context) error {
+	if !c.cfg.Enable {
+		c.logger.Warn("MCP 组件未启用,跳过 ServeStdio")
+		return nil
+	}
+	c.logger.Info("启动 MCP Stdio 服务", zap.String("server", c.cfg.ServerName), zap.String("version", c.cfg.ServerVersion))
+	// mcp-go 当前 Stdio Server 不接受外部 Context;如需退出,可由调用方中断进程。
+	return server.ServeStdio(c.server)
+}

+ 12 - 0
service/internal/config/config.example.toml

@@ -38,3 +38,15 @@ sql_base_dir = "./DBconfig/sqlfiles"
 database_path = "./DBconfig/storage.db"
 # 向后兼容的数据文件路径
 data_file = "./DBconfig/data.toml"
+
+[mcp]
+enable = false
+server_name = "dbview-mcp"
+server_version = "0.1.0"
+
+[ai]
+enable = false
+provider = "openai"
+model = "gpt-4o-mini"
+# 从该环境变量读取 API Key
+api_key_env = "OPENAI_API_KEY"

+ 63 - 2
service/internal/config/config.go

@@ -15,6 +15,8 @@ type AppConfig struct {
 	Log     LogConfig     `toml:"log"`
 	Audit   AuditConfig   `toml:"audit"`
 	Storage StorageConfig `toml:"storage"`
+	MCP     MCPConfig     `toml:"mcp"`
+	AI      AIConfig      `toml:"ai"`
 }
 
 // ServerConfig 服务器配置
@@ -53,7 +55,22 @@ type StorageConfig struct {
 	DatabasePath string `toml:"database_path"` // 数据库文件路径 (用于数据库存储)
 
 	// 通用配置
-	DataFile string `toml:"data_file"` // 数据文件路径 (向后兼容)
+	DataFile string `toml:"data_file"` // 数据文件路径(兼容旧配置 key `data_file`,推荐使用 `config_path`)
+}
+
+// MCPConfig MCP 组件配置
+type MCPConfig struct {
+	Enable        bool   `toml:"enable"`
+	ServerName    string `toml:"server_name"`
+	ServerVersion string `toml:"server_version"`
+}
+
+// AIConfig AI 交互配置
+type AIConfig struct {
+	Enable    bool   `toml:"enable"`
+	Provider  string `toml:"provider"`    // 目前支持 openai
+	Model     string `toml:"model"`       // 默认模型
+	APIKeyEnv string `toml:"api_key_env"` // 从哪个环境变量读取 API Key
 }
 
 // LoadConfig 加载配置文件
@@ -108,6 +125,17 @@ func CreateDefaultConfig(configPath string) error {
 			DatabasePath: "./DBconfig/storage.db",
 			DataFile:     "./DBconfig/data.toml",
 		},
+		MCP: MCPConfig{
+			Enable:        false,
+			ServerName:    "dbview-mcp",
+			ServerVersion: "0.1.0",
+		},
+		AI: AIConfig{
+			Enable:    false,
+			Provider:  "openai",
+			Model:     "gpt-4o-mini",
+			APIKeyEnv: "OPENAI_API_KEY",
+		},
 	}
 
 	// 确保配置目录存在
@@ -191,7 +219,26 @@ func (cfg *AppConfig) SetDefaults() {
 		cfg.Storage.DatabasePath = "./DBconfig/storage.db"
 	}
 	if cfg.Storage.DataFile == "" {
-		cfg.Storage.DataFile = "./DBconfig/data.toml" // 向后兼容
+		cfg.Storage.DataFile = "./DBconfig/data.toml" // 兼容旧配置 key `data_file`(推荐使用 `config_path`)
+	}
+
+	// MCP 默认值
+	if cfg.MCP.ServerName == "" {
+		cfg.MCP.ServerName = "dbview-mcp"
+	}
+	if cfg.MCP.ServerVersion == "" {
+		cfg.MCP.ServerVersion = "0.1.0"
+	}
+
+	// AI 默认值
+	if cfg.AI.Provider == "" {
+		cfg.AI.Provider = "openai"
+	}
+	if cfg.AI.Model == "" {
+		cfg.AI.Model = "gpt-4o-mini"
+	}
+	if cfg.AI.APIKeyEnv == "" {
+		cfg.AI.APIKeyEnv = "OPENAI_API_KEY"
 	}
 }
 
@@ -275,6 +322,20 @@ func (cfg *AppConfig) Validate() error {
 		return fmt.Errorf("invalid storage type: %s", cfg.Storage.Type)
 	}
 
+	// 验证 AI 配置(仅在启用时检查)
+	if cfg.AI.Enable {
+		validProviders := map[string]bool{"openai": true}
+		if !validProviders[cfg.AI.Provider] {
+			return fmt.Errorf("invalid ai provider: %s", cfg.AI.Provider)
+		}
+		if cfg.AI.Model == "" {
+			return fmt.Errorf("ai model required when ai enabled")
+		}
+		if cfg.AI.APIKeyEnv == "" {
+			return fmt.Errorf("ai api_key_env required when ai enabled")
+		}
+	}
+
 	return nil
 }
 

+ 82 - 0
service/internal/modules/mcp_ai/API.md

@@ -0,0 +1,82 @@
+# AI Chat API
+
+简要说明:提供基于外部大模型(当前支持 OpenAI)的简单对话接口。默认路由前缀 `/ai`,统一使用 JSON 请求/响应,并沿用 `service/internal/common/response.Response` 包装。
+
+## 前置条件
+- 配置:`config.toml`
+  ```toml
+  [mcp]
+  enable = true  # 仅启用 MCP 时会自动注册 ai_chat 工具;HTTP 接口与 MCP 各自独立
+
+  [ai]
+  enable = true
+  provider = "openai"
+  model = "gpt-4o-mini"
+  api_key_env = "OPENAI_API_KEY"
+  ```
+- 环境变量:启动进程前导出对应的 Key,例如:`export OPENAI_API_KEY=sk-...`
+- 构建/运行:`go build ./...` 或 `go run ./`
+
+## 通用响应格式
+```json
+{
+  "code": 0,          // 0=成功,非0=失败
+  "msg": "ok",       // 简短提示
+  "data": {},         // 成功时返回的业务数据
+  "error": ""        // 失败时的底层错误信息
+}
+```
+
+## 接口一览
+| 方法 | 路径 | 功能 |
+| ---- | ---- | ---- |
+| POST | /ai/chat | 发送用户提示词,获取模型回复 |
+
+## POST /ai/chat
+- 功能:调用外部 AI 模型生成文本回复。
+- Content-Type: `application/json`
+
+### 请求体
+| 字段 | 类型 | 必填 | 说明 |
+| ---- | ---- | :--: | ---- |
+| prompt | string | 是 | 用户输入内容 |
+| system | string | 否 | 系统指令/角色设定 |
+| model | string | 否 | 覆盖默认模型(不填则使用配置中的 `ai.model`) |
+
+### 成功响应
+`data` 字段为:
+```json
+{
+  "text": "模型生成的回复文本"
+}
+```
+完整示例:
+```json
+{
+  "code": 0,
+  "msg": "ok",
+  "data": {"text": "SQL 索引用于加速查询..."}
+}
+```
+
+### 失败示例
+```json
+{
+  "code": 2,
+  "msg": "AI 调用失败",
+  "error": "openai request failed: status 401"
+}
+```
+
+### cURL 示例
+```bash
+curl -X POST http://127.0.0.1:8080/ai/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "prompt": "解释 SQL 索引的作用",
+    "system": "你是数据库助手"
+  }'
+```
+
+## MCP ai_chat(补充说明)
+当 `[mcp].enable=true` 时,MCP 服务会注册工具 `ai_chat`(与 HTTP 路由独立)。参数含义与 HTTP 相同,客户端通过 MCP 协议的 `tools/call` 请求调用。

+ 15 - 0
service/internal/modules/mcp_ai/api/types.go

@@ -0,0 +1,15 @@
+package api
+
+// ChatRequest AI 聊天请求
+// prompt 为必填,system/model 可选。
+type ChatRequest struct {
+	Prompt string `json:"prompt" binding:"required"`
+	System string `json:"system,omitempty"`
+	Model  string `json:"model,omitempty"`
+}
+
+// ChatResponse AI 聊天响应
+// text 为生成的回复内容。
+type ChatResponse struct {
+	Text string `json:"text"`
+}

+ 43 - 0
service/internal/modules/mcp_ai/handler/handler.go

@@ -0,0 +1,43 @@
+package handler
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"dbview/service/internal/common/response"
+	"dbview/service/internal/modules/mcp_ai/api"
+	"dbview/service/internal/modules/mcp_ai/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+// Handler 提供 AI 聊天接口的 HTTP 处理器。
+type Handler struct {
+	svc *service.AIService
+}
+
+// NewHandler 创建 Handler。
+func NewHandler(svc *service.AIService) *Handler {
+	return &Handler{svc: svc}
+}
+
+// Chat 提供 POST /ai/chat 接口。
+func (h *Handler) Chat(c *gin.Context) {
+	var req api.ChatRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(http.StatusBadRequest, response.Response{Code: 1, Msg: "参数错误", Error: err.Error()})
+		return
+	}
+
+	ctx, cancel := context.WithTimeout(c.Request.Context(), 45*time.Second)
+	defer cancel()
+
+	text, err := h.svc.Chat(ctx, req.Prompt, req.System, req.Model)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, response.Response{Code: 2, Msg: "AI 调用失败", Error: err.Error()})
+		return
+	}
+
+	c.JSON(http.StatusOK, response.Response{Code: 0, Msg: "ok", Data: api.ChatResponse{Text: text}})
+}

+ 21 - 0
service/internal/modules/mcp_ai/route.go

@@ -0,0 +1,21 @@
+package mcp_ai
+
+import (
+	"dbview/service/internal/modules/mcp_ai/handler"
+	svc "dbview/service/internal/modules/mcp_ai/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+// RegisterRoutes 注册 AI 聊天相关路由。
+func RegisterRoutes(r *gin.Engine, svcObj *svc.AIService) {
+	if svcObj == nil {
+		return
+	}
+
+	h := handler.NewHandler(svcObj)
+	grp := r.Group("/ai")
+	{
+		grp.POST("/chat", h.Chat)
+	}
+}

+ 120 - 0
service/internal/modules/mcp_ai/service/service.go

@@ -0,0 +1,120 @@
+package service
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+
+	"dbview/service/internal/config"
+)
+
+// AIService 提供 AI 聊天能力(当前支持 OpenAI)。
+type AIService struct {
+	cfg config.AIConfig
+	hc  *http.Client
+}
+
+// NewAIService 创建 AIService,需传入配置。
+func NewAIService(cfg config.AIConfig) *AIService {
+	return &AIService{cfg: cfg, hc: http.DefaultClient}
+}
+
+// Chat 调用外部模型生成回复。
+func (s *AIService) Chat(ctx context.Context, prompt, system, modelOverride string) (string, error) {
+	if !s.cfg.Enable {
+		return "", fmt.Errorf("ai 功能未启用")
+	}
+
+	if s.cfg.Provider != "openai" {
+		return "", fmt.Errorf("ai provider 未支持: %s", s.cfg.Provider)
+	}
+
+	apiKeyEnv := s.cfg.APIKeyEnv
+	if apiKeyEnv == "" {
+		apiKeyEnv = "OPENAI_API_KEY"
+	}
+	apiKey := os.Getenv(apiKeyEnv)
+	if apiKey == "" {
+		return "", fmt.Errorf("未找到 AI API Key 环境变量 %s", apiKeyEnv)
+	}
+
+	model := s.cfg.Model
+	if model == "" {
+		model = "gpt-4o-mini"
+	}
+	if modelOverride != "" {
+		model = modelOverride
+	}
+
+	return callOpenAIChat(ctx, s.hc, apiKey, model, system, prompt)
+}
+
+// callOpenAIChat performs a minimal chat completion request.
+func callOpenAIChat(ctx context.Context, hc *http.Client, apiKey, model, systemPrompt, userPrompt string) (string, error) {
+	messages := []openAIMessage{}
+	if systemPrompt != "" {
+		messages = append(messages, openAIMessage{Role: "system", Content: systemPrompt})
+	}
+	messages = append(messages, openAIMessage{Role: "user", Content: userPrompt})
+
+	body := openAIChatRequest{Model: model, Messages: messages}
+	b, err := json.Marshal(body)
+	if err != nil {
+		return "", err
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader(b))
+	if err != nil {
+		return "", err
+	}
+	req.Header.Set("Authorization", "Bearer "+apiKey)
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := hc.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	var out openAIChatResponse
+	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
+		return "", err
+	}
+
+	if resp.StatusCode >= 300 {
+		if out.Error != nil && out.Error.Message != "" {
+			return "", fmt.Errorf("openai error: %s", out.Error.Message)
+		}
+		return "", fmt.Errorf("openai request failed: status %d", resp.StatusCode)
+	}
+
+	if len(out.Choices) == 0 {
+		return "", fmt.Errorf("openai 返回为空")
+	}
+
+	return out.Choices[0].Message.Content, nil
+}
+
+// 请求/响应结构
+
+type openAIChatRequest struct {
+	Model    string          `json:"model"`
+	Messages []openAIMessage `json:"messages"`
+}
+
+type openAIMessage struct {
+	Role    string `json:"role"`
+	Content string `json:"content"`
+}
+
+type openAIChatResponse struct {
+	Choices []struct {
+		Message openAIMessage `json:"message"`
+	} `json:"choices"`
+	Error *struct {
+		Message string `json:"message"`
+	} `json:"error,omitempty"`
+}

+ 9 - 1
service/internal/modules/routers.go

@@ -8,17 +8,21 @@ import (
 	storageRouter "dbview/service/internal/modules/db_conn_storage"
 	storageService "dbview/service/internal/modules/db_conn_storage/service"
 
+	aiChat "dbview/service/internal/modules/mcp_ai"
+	aiSvc "dbview/service/internal/modules/mcp_ai/service"
+
 	connpool "dbview/service/internal/modules/connection_pool"
 
 	"dbview/service/internal/common/manager/connection"
 	storagemgr "dbview/service/internal/common/manager/storage"
 	"dbview/service/internal/common/manager/task"
+	"dbview/service/internal/config"
 
 	"github.com/gin-gonic/gin"
 )
 
 // RegisterRoutes 注册所有模块路由(不依赖 bootstrap 包,避免导入环)
-func RegisterRoutes(engine *gin.Engine, storageManager interface{}, taskManager *task.Manager, connPool *connection.ConnectionPool) {
+func RegisterRoutes(engine *gin.Engine, storageManager interface{}, taskManager *task.Manager, connPool *connection.ConnectionPool, aiCfg config.AIConfig) {
 	// 1) 存储模块(db_conn_storage)
 
 	if storageManager != nil {
@@ -58,6 +62,10 @@ func RegisterRoutes(engine *gin.Engine, storageManager interface{}, taskManager
 	}
 	dataQuery.RegisterRoutes(engine, dataSvc)
 
+	// 4) AI Chat 模块(通过 OpenAI/AI 配置)
+	aiSvcInst := aiSvc.NewAIService(aiCfg)
+	aiChat.RegisterRoutes(engine, aiSvcInst)
+
 	// 3) SQL 编辑器模块(sql_editor)
 	// var sqlSvc *sqlService.SQLEditorService
 	// if taskManager != nil {