feat: MCP 管理进后台 UI (v2.1) — 开关/Token 即时生效无需重启
- settings 表新增 mcp_enabled / mcp_token,鉴权中间件动态读库,后台改动即时生效(实测验证:关->503、开->200、换 token 旧401新200,均不重启) - 首次启动自动把 TONAV_MCP_TOKEN 环境变量迁移进数据库(兼容旧部署,systemd 现有 token 无感切换) - 后台新增 API:GET /admin/api/mcp/status(token 掩码)、POST /admin/api/mcp/token(重新生成+自动启用,仅一次明文展示)、POST /admin/api/mcp/enabled - 后台设置弹窗新增 MCP Server 区块:开关 checkbox、token 掩码展示、一键重新生成 - README 同步
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
### MCP Server(AI 集成)
|
### MCP Server(AI 集成)
|
||||||
- 🤖 **原生 MCP Server v2** — Streamable HTTP 双端点:`POST /mcp`(请求-响应) + `GET /mcp`(SSE 流式)
|
- 🤖 **原生 MCP Server v2** — Streamable HTTP 双端点:`POST /mcp`(请求-响应) + `GET /mcp`(SSE 流式)
|
||||||
- 📡 **服务端主动推送** — 服务/分类变更时通过 SSE 推送 `notifications/tools/list_changed`
|
- 📡 **服务端主动推送** — 服务/分类变更时通过 SSE 推送 `notifications/tools/list_changed`
|
||||||
- 🔑 **Bearer Token 鉴权** — 通过 `TONAV_MCP_TOKEN` 环境变量配置;**未设置时 MCP 端点禁用(返回 503),无默认兜底**
|
- 🔑 **Bearer Token 鉴权** — 开关与 Token 存于 settings 表,**后台 UI 管理,改动即时生效无需重启**(环境变量 TONAV_MCP_TOKEN 首次启动自动迁移进数据库)
|
||||||
- 🧰 **7 个工具** — 添加服务 / 创建分类 / 列出分类 / 列出服务 / 搜索服务 / 更新服务 / 删除服务
|
- 🧰 **7 个工具** — 添加服务 / 创建分类 / 列出分类 / 列出服务 / 搜索服务 / 更新服务 / 删除服务
|
||||||
- 🔎 **全文搜索** — `tonav_search_services` 独立搜索工具,按名称>标签>描述>URL 相关度排序
|
- 🔎 **全文搜索** — `tonav_search_services` 独立搜索工具,按名称>标签>描述>URL 相关度排序
|
||||||
- 📝 **智能添加** — 添加服务不传名称时自动抓取目标网页 `<title>` 与 meta description,图标默认取 favicon
|
- 📝 **智能添加** — 添加服务不传名称时自动抓取目标网页 `<title>` 与 meta description,图标默认取 favicon
|
||||||
@@ -118,7 +118,7 @@ docker compose up -d
|
|||||||
| `TONAV_PORT` | `9520` | 监听端口 |
|
| `TONAV_PORT` | `9520` | 监听端口 |
|
||||||
| `TONAV_DB` | `/app/data/tonav.db` | 数据库路径 |
|
| `TONAV_DB` | `/app/data/tonav.db` | 数据库路径 |
|
||||||
| `TONAV_SECRET` | 内置密钥 | Cookie 签名密钥(生产环境务必修改) |
|
| `TONAV_SECRET` | 内置密钥 | Cookie 签名密钥(生产环境务必修改) |
|
||||||
| `TONAV_MCP_TOKEN` | **未设置则 MCP 禁用** | MCP 接口鉴权 Token(必须配置,无默认值) |
|
| `TONAV_MCP_TOKEN` | 无 | **仅首启初始化用**:有值则迁移进数据库并自动启用 MCP;之后以后台设置为准 |
|
||||||
| `TZ` | `Asia/Shanghai` | 时区 |
|
| `TZ` | `Asia/Shanghai` | 时区 |
|
||||||
|
|
||||||
**数据持久化:**
|
**数据持久化:**
|
||||||
@@ -175,7 +175,7 @@ chmod +x tonav-go-ctl.sh
|
|||||||
| `TONAV_PORT` | `9520` | 监听端口 |
|
| `TONAV_PORT` | `9520` | 监听端口 |
|
||||||
| `TONAV_DB` | `tonav.db` | 数据库文件路径 |
|
| `TONAV_DB` | `tonav.db` | 数据库文件路径 |
|
||||||
| `TONAV_SECRET` | 内置密钥 | Cookie 签名密钥 |
|
| `TONAV_SECRET` | 内置密钥 | Cookie 签名密钥 |
|
||||||
| `TONAV_MCP_TOKEN` | **未设置则 MCP 禁用** | MCP 接口鉴权 Token(必须配置) |
|
| `TONAV_MCP_TOKEN` | 无 | 仅首启初始化用,之后以后台设置为准 |
|
||||||
|
|
||||||
### 默认账号
|
### 默认账号
|
||||||
|
|
||||||
@@ -210,6 +210,9 @@ chmod +x tonav-go-ctl.sh
|
|||||||
| GET | `/admin/api/backup/list` | 列出云端备份 |
|
| GET | `/admin/api/backup/list` | 列出云端备份 |
|
||||||
| DELETE | `/admin/api/backup/delete?name=xxx` | 删除云端备份 |
|
| DELETE | `/admin/api/backup/delete?name=xxx` | 删除云端备份 |
|
||||||
| POST | `/admin/api/backup/restore?name=xxx` | 恢复云端备份 |
|
| POST | `/admin/api/backup/restore?name=xxx` | 恢复云端备份 |
|
||||||
|
| GET | `/admin/api/mcp/status` | MCP 状态(Token 掩码) |
|
||||||
|
| POST | `/admin/api/mcp/token` | 重新生成 MCP Token(自动启用) |
|
||||||
|
| POST | `/admin/api/mcp/enabled` | 开启/关闭 MCP |
|
||||||
|
|
||||||
### MCP 接口(Bearer Token 鉴权)
|
### MCP 接口(Bearer Token 鉴权)
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
@@ -217,8 +220,9 @@ chmod +x tonav-go-ctl.sh
|
|||||||
| POST | `/mcp` | MCP Streamable HTTP 端点(JSON-RPC 2.0 请求-响应) |
|
| POST | `/mcp` | MCP Streamable HTTP 端点(JSON-RPC 2.0 请求-响应) |
|
||||||
| GET | `/mcp` | MCP SSE 流式端点(服务端主动推送通知) |
|
| GET | `/mcp` | MCP SSE 流式端点(服务端主动推送通知) |
|
||||||
|
|
||||||
- 鉴权:请求头 `Authorization: Bearer <TONAV_MCP_TOKEN>`,缺失或错误返回 401
|
- 鉴权:请求头 `Authorization: Bearer <token>`,缺失或错误返回 401
|
||||||
- **未配置 `TONAV_MCP_TOKEN` 时端点返回 503(MCP 禁用),启动日志会警告**
|
- **开关与 Token 在后台设置页管理(⚙️ 系统设置 → MCP Server),动态读数据库,改动即时生效无需重启**
|
||||||
|
- 开关关闭或未生成 Token 时端点返回 503
|
||||||
- POST 支持 `initialize` / `ping` / `tools/list` / `tools/call` 方法;通知类请求返回 202
|
- POST 支持 `initialize` / `ping` / `tools/list` / `tools/call` 方法;通知类请求返回 202
|
||||||
- POST 支持单条与批量 JSON-RPC 请求
|
- POST 支持单条与批量 JSON-RPC 请求
|
||||||
- GET 为 SSE 流:首个事件 `endpoint: /mcp`(握手),后续 `message` 事件推送 `notifications/tools/list_changed`(服务/分类增删时触发),每 30s keepalive
|
- GET 为 SSE 流:首个事件 `endpoint: /mcp`(握手),后续 `message` 事件推送 `notifications/tools/list_changed`(服务/分类增删时触发),每 30s keepalive
|
||||||
|
|||||||
+105
-24
@@ -1,6 +1,8 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
cryptoRand "crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,35 +18,54 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============ MCP (Model Context Protocol) v2 ============
|
// ============ MCP (Model Context Protocol) v2.1 ============
|
||||||
// 传输: Streamable HTTP (POST /mcp) + SSE (GET /mcp),JSON-RPC 2.0
|
// 传输: Streamable HTTP (POST /mcp) + SSE (GET /mcp),JSON-RPC 2.0
|
||||||
// 鉴权: Authorization: Bearer <TONAV_MCP_TOKEN>
|
// 鉴权: Authorization: Bearer <token>;开关与 Token 存于 settings 表(后台 UI 管理),动态读取即时生效
|
||||||
|
|
||||||
type mcpConfig struct {
|
// getMCPAuthState 从数据库读取 MCP 开关与 Token(每次请求动态读,改动即时生效)
|
||||||
Token string
|
func getMCPAuthState() (enabled bool, token string) {
|
||||||
|
var enabledSetting, tokenSetting models.Setting
|
||||||
|
database.DB.Where("key = ?", "mcp_enabled").First(&enabledSetting)
|
||||||
|
database.DB.Where("key = ?", "mcp_token").First(&tokenSetting)
|
||||||
|
return enabledSetting.Value == "true", tokenSetting.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
var mcpToken string
|
// SetupMCP 初始化 MCP 配置(启动时调用:把环境变量 Token 迁移进数据库作为初始值)
|
||||||
|
func SetupMCP() {
|
||||||
// LoadMCPConfig 读取 MCP 配置(环境变量 TONAV_MCP_TOKEN)
|
var s models.Setting
|
||||||
// v2: 未设置时返回错误,拒绝默认 token 兜底(防裸奔)
|
if err := database.DB.Where("key = ?", "mcp_token").First(&s).Error; err != nil {
|
||||||
func LoadMCPConfig() error {
|
// 数据库无 token:用环境变量值初始化(兼容旧部署),没有则留空(后台生成)
|
||||||
mcpToken = os.Getenv("TONAV_MCP_TOKEN")
|
database.DB.Save(&models.Setting{Key: "mcp_token", Value: os.Getenv("TONAV_MCP_TOKEN")})
|
||||||
if mcpToken == "" {
|
}
|
||||||
return fmt.Errorf("TONAV_MCP_TOKEN 未设置:MCP 端点已禁用。请设置环境变量后重启,或在 systemd 单元中配置 Environment=TONAV_MCP_TOKEN=<随机token>")
|
if err := database.DB.Where("key = ?", "mcp_enabled").First(&s).Error; err != nil {
|
||||||
|
var tok models.Setting
|
||||||
|
database.DB.Where("key = ?", "mcp_token").First(&tok)
|
||||||
|
v := "false"
|
||||||
|
if tok.Value != "" {
|
||||||
|
v = "true"
|
||||||
|
}
|
||||||
|
database.DB.Save(&models.Setting{Key: "mcp_enabled", Value: v})
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCPEnabled 报告 MCP 端点是否可用
|
// MCPAuthRequired MCP Bearer Token 鉴权(动态读数据库:后台改开关/Token 即时生效,无需重启)
|
||||||
func MCPEnabled() bool { return mcpToken != "" }
|
|
||||||
|
|
||||||
// MCPAuthRequired MCP Bearer Token 鉴权
|
|
||||||
func MCPAuthRequired() gin.HandlerFunc {
|
func MCPAuthRequired() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
enabled, token := getMCPAuthState()
|
||||||
|
if !enabled || token == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": gin.H{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "MCP 未启用:请在后台设置中开启 MCP Server 并生成 Token",
|
||||||
|
},
|
||||||
|
"id": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
auth := c.GetHeader("Authorization")
|
auth := c.GetHeader("Authorization")
|
||||||
token := strings.TrimPrefix(auth, "Bearer ")
|
tok := strings.TrimPrefix(auth, "Bearer ")
|
||||||
if token == "" || token != mcpToken {
|
if tok == "" || tok != token {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
@@ -653,8 +674,9 @@ func callTool(name string, args map[string]interface{}) (interface{}, error) {
|
|||||||
|
|
||||||
// MCPHandler 处理 POST /mcp (Streamable HTTP)
|
// MCPHandler 处理 POST /mcp (Streamable HTTP)
|
||||||
func MCPHandler(c *gin.Context) {
|
func MCPHandler(c *gin.Context) {
|
||||||
if mcpToken == "" {
|
enabled, token := getMCPAuthState()
|
||||||
c.JSON(http.StatusServiceUnavailable, mcpError(nil, -32000, "MCP 未启用:未配置 TONAV_MCP_TOKEN"))
|
if !enabled || token == "" {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, mcpError(nil, -32000, "MCP 未启用:请在后台设置中开启"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1024*1024))
|
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1024*1024))
|
||||||
@@ -755,9 +777,67 @@ func handleMCPRequest(req jsonRPCRequest) gin.H {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 后台 MCP 管理 API (v2.1) ============
|
||||||
|
|
||||||
|
// MCPStatus 后台查询 MCP 状态(Token 掩码显示)
|
||||||
|
func MCPStatus(c *gin.Context) {
|
||||||
|
enabled, token := getMCPAuthState()
|
||||||
|
masked := ""
|
||||||
|
if token != "" {
|
||||||
|
if len(token) > 10 {
|
||||||
|
masked = token[:6] + "…" + token[len(token)-4:]
|
||||||
|
} else {
|
||||||
|
masked = "****"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"enabled": enabled,
|
||||||
|
"has_token": token != "",
|
||||||
|
"token_masked": masked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCPRegenerateToken 生成新随机 Token 并自动启用 MCP,返回完整新 Token(仅此一次明文展示)
|
||||||
|
func MCPRegenerateToken(c *gin.Context) {
|
||||||
|
b := make([]byte, 24)
|
||||||
|
if _, err := cryptoRand.Read(b); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "生成失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := hex.EncodeToString(b)
|
||||||
|
database.DB.Save(&models.Setting{Key: "mcp_token", Value: token})
|
||||||
|
database.DB.Save(&models.Setting{Key: "mcp_enabled", Value: "true"})
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "token": token, "message": "已生成新 Token 并启用 MCP"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCPSetEnabled 开启/关闭 MCP(Token 为空时拒绝开启)
|
||||||
|
func MCPSetEnabled(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, Response{Success: false, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if input.Enabled {
|
||||||
|
_, token := getMCPAuthState()
|
||||||
|
if token == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "Token 为空,请先生成 Token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
database.DB.Save(&models.Setting{Key: "mcp_enabled", Value: strconv.FormatBool(input.Enabled)})
|
||||||
|
msg := "MCP 已关闭"
|
||||||
|
if input.Enabled {
|
||||||
|
msg = "MCP 已开启,即时生效"
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, Response{Success: true, Message: msg})
|
||||||
|
}
|
||||||
|
|
||||||
// GetMCPHeaderToken 供测试使用
|
// GetMCPHeaderToken 供测试使用
|
||||||
func GetMCPHeaderToken() string {
|
func GetMCPHeaderToken() string {
|
||||||
return mcpToken
|
_, token := getMCPAuthState()
|
||||||
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ SSE 端点 (GET /mcp, v2) ============
|
// ============ SSE 端点 (GET /mcp, v2) ============
|
||||||
@@ -788,8 +868,9 @@ func notifySSE(event string, data interface{}) {
|
|||||||
// MCPSSEHandler 处理 GET /mcp (SSE 流式端点)
|
// MCPSSEHandler 处理 GET /mcp (SSE 流式端点)
|
||||||
// 客户端可收到: endpoint(初始化握手)、message(服务端主动通知)等事件
|
// 客户端可收到: endpoint(初始化握手)、message(服务端主动通知)等事件
|
||||||
func MCPSSEHandler(c *gin.Context) {
|
func MCPSSEHandler(c *gin.Context) {
|
||||||
if mcpToken == "" {
|
enabled, token := getMCPAuthState()
|
||||||
c.JSON(http.StatusServiceUnavailable, mcpError(nil, -32000, "MCP 未启用:未配置 TONAV_MCP_TOKEN"))
|
if !enabled || token == "" {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, mcpError(nil, -32000, "MCP 未启用:请在后台设置中开启"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ func main() {
|
|||||||
database.Seed()
|
database.Seed()
|
||||||
handlers.StartHealthCheck()
|
handlers.StartHealthCheck()
|
||||||
handlers.StartAutoBackup()
|
handlers.StartAutoBackup()
|
||||||
if err := handlers.LoadMCPConfig(); err != nil {
|
handlers.SetupMCP()
|
||||||
log.Printf("⚠️ MCP 未启用: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
store := cookie.NewStore([]byte(cfg.SecretKey))
|
store := cookie.NewStore([]byte(cfg.SecretKey))
|
||||||
@@ -71,18 +69,18 @@ func main() {
|
|||||||
admin.GET("/api/backup/list", handlers.ListCloudBackups)
|
admin.GET("/api/backup/list", handlers.ListCloudBackups)
|
||||||
admin.DELETE("/api/backup/delete", handlers.DeleteCloudBackup)
|
admin.DELETE("/api/backup/delete", handlers.DeleteCloudBackup)
|
||||||
admin.POST("/api/backup/restore", handlers.RestoreCloudBackup)
|
admin.POST("/api/backup/restore", handlers.RestoreCloudBackup)
|
||||||
|
|
||||||
|
// MCP 管理(后台 UI)
|
||||||
|
admin.GET("/api/mcp/status", handlers.MCPStatus)
|
||||||
|
admin.POST("/api/mcp/token", handlers.MCPRegenerateToken)
|
||||||
|
admin.POST("/api/mcp/enabled", handlers.MCPSetEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCP (Model Context Protocol) 端点
|
// MCP (Model Context Protocol) 端点
|
||||||
// POST: 请求-响应模式;GET: SSE 流式(服务端主动推送 notifications/tools/list_changed)
|
// POST: 请求-响应模式;GET: SSE 流式(服务端主动推送 notifications/tools/list_changed)
|
||||||
if handlers.MCPEnabled() {
|
// 鉴权中间件动态读数据库:后台开关/Token 改动即时生效,无需重启
|
||||||
r.POST("/mcp", handlers.MCPAuthRequired(), handlers.MCPHandler)
|
r.POST("/mcp", handlers.MCPAuthRequired(), handlers.MCPHandler)
|
||||||
r.GET("/mcp", handlers.MCPAuthRequired(), handlers.MCPSSEHandler)
|
r.GET("/mcp", handlers.MCPAuthRequired(), handlers.MCPSSEHandler)
|
||||||
} else {
|
|
||||||
// 未配置 token 时仍注册端点,但返回 503 明确提示
|
|
||||||
r.POST("/mcp", handlers.MCPHandler)
|
|
||||||
r.GET("/mcp", handlers.MCPSSEHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("ToNav-go 启动在端口 %s...", cfg.Port)
|
log.Printf("ToNav-go 启动在端口 %s...", cfg.Port)
|
||||||
r.Run(":" + cfg.Port)
|
r.Run(":" + cfg.Port)
|
||||||
|
|||||||
@@ -129,6 +129,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:20px; padding:15px; background:#f9f9f9; border-radius:10px">
|
||||||
|
<h3 style="font-size:14px; margin-bottom:10px">🤖 MCP Server</h3>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px">
|
||||||
|
<input type="checkbox" id="cfg_mcp_enabled" style="width:16px; height:16px" onchange="toggleMCP()">
|
||||||
|
<label for="cfg_mcp_enabled" style="margin:0; font-size:13px">启用 MCP 接口(AI 可通过它管理导航页)</label>
|
||||||
|
</div>
|
||||||
|
<div id="mcpTokenRow" style="display:none">
|
||||||
|
<label style="font-size:12px; color:#666">Token</label>
|
||||||
|
<div style="display:flex; gap:8px; align-items:center">
|
||||||
|
<input type="text" id="cfg_mcp_token" class="input" readonly style="flex:1; background:#fff; color:#555">
|
||||||
|
<button class="btn btn-primary" onclick="regenMCPToken()" style="padding:8px 14px; white-space:nowrap">🔄 重新生成</button>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px; color:#999; margin-top:6px">
|
||||||
|
端点: <code>POST/GET /mcp</code>(Streamable HTTP + SSE)· 改动即时生效,无需重启
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" onclick="saveSettings()" style="width:100%; margin-bottom:15px">保存设置</button>
|
<button class="btn btn-primary" onclick="saveSettings()" style="width:100%; margin-bottom:15px">保存设置</button>
|
||||||
|
|
||||||
<div style="padding:15px; border:1px solid #eee; border-radius:10px">
|
<div style="padding:15px; border:1px solid #eee; border-radius:10px">
|
||||||
@@ -150,9 +168,52 @@
|
|||||||
document.getElementById('cfg_pass').value = data.webdav_password || '';
|
document.getElementById('cfg_pass').value = data.webdav_password || '';
|
||||||
document.getElementById('cfg_auto_backup').checked = data.auto_backup === 'true';
|
document.getElementById('cfg_auto_backup').checked = data.auto_backup === 'true';
|
||||||
document.getElementById('settingsModal').style.display = 'flex';
|
document.getElementById('settingsModal').style.display = 'flex';
|
||||||
|
loadMCPStatus();
|
||||||
loadCloudBackups();
|
loadCloudBackups();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadMCPStatus() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/admin/api/mcp/status');
|
||||||
|
const d = await resp.json();
|
||||||
|
const cb = document.getElementById('cfg_mcp_enabled');
|
||||||
|
const row = document.getElementById('mcpTokenRow');
|
||||||
|
cb.checked = d.enabled;
|
||||||
|
row.style.display = d.enabled ? 'block' : 'none';
|
||||||
|
document.getElementById('cfg_mcp_token').value = d.token_masked || '(未生成,点重新生成创建)';
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMCP() {
|
||||||
|
const enabled = document.getElementById('cfg_mcp_enabled').checked;
|
||||||
|
document.getElementById('mcpTokenRow').style.display = enabled ? 'block' : 'none';
|
||||||
|
fetch('/admin/api/mcp/enabled', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({enabled: enabled})
|
||||||
|
}).then(r => r.json()).then(res => {
|
||||||
|
if (!res.success) {
|
||||||
|
alert(res.message);
|
||||||
|
document.getElementById('cfg_mcp_enabled').checked = !enabled;
|
||||||
|
document.getElementById('mcpTokenRow').style.display = enabled ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenMCPToken() {
|
||||||
|
if (!confirm('确定重新生成 Token?\n\n旧 Token 将立即失效,已接入的 AI 客户端需要更新为新 Token。')) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/admin/api/mcp/token', {method: 'POST'});
|
||||||
|
const res = await resp.json();
|
||||||
|
if (res.success) {
|
||||||
|
alert('✅ 新 Token 已生成(仅此一次明文展示,请立即复制):\n\n' + res.token);
|
||||||
|
loadMCPStatus();
|
||||||
|
} else {
|
||||||
|
alert('❌ ' + res.message);
|
||||||
|
}
|
||||||
|
} catch(e) { alert('生成失败'); }
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCloudBackups() {
|
async function loadCloudBackups() {
|
||||||
const listDiv = document.getElementById('backupList');
|
const listDiv = document.getElementById('backupList');
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user