feat(mcp): 设置 → MCP 子页 + 策略 API + 官网/文档接入
借鉴 DBX:安装检测(Node/npm/包/Go 二进制)、只读与 allow_dangerous、 连接白名单、多客户端配置片段;GET/PUT /api/ai/mcp-policy、/mcp-install; 文档 /docs/mcp 与落地页 AI/MCP 块;对照 docs/16 勾选设置项、列仍未完清单。
This commit is contained in:
1 parent
5416e86379
commit
1513f4bc2d
20 files changed
+1567
-98
No files matched your search
@@ -0,0 +1,275 @@
|
||||
package aitools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const npmPackageName = "@rlwj-lab/opsxdb-mcp"
|
||||
|
||||
// MCPInstallInfo 设置 → MCP 页的安装/环境检测结果。
|
||||
type MCPInstallInfo struct {
|
||||
Status string `json:"status"` // ready | not_installed | partial
|
||||
Installed bool `json:"installed"`
|
||||
CurrentVersion string `json:"currentVersion,omitempty"`
|
||||
LatestVersion string `json:"latestVersion,omitempty"`
|
||||
NodeVersion string `json:"nodeVersion,omitempty"`
|
||||
NpmAvailable bool `json:"npmAvailable"`
|
||||
NpmVersion string `json:"npmVersion,omitempty"`
|
||||
NpmPackageInstalled bool `json:"npmPackageInstalled"`
|
||||
NpmPackageVersion string `json:"npmPackageVersion,omitempty"`
|
||||
GoBinaryFound bool `json:"goBinaryFound"`
|
||||
GoBinaryPath string `json:"goBinaryPath,omitempty"`
|
||||
InstallCommand string `json:"installCommand"`
|
||||
NpxCommand string `json:"npxCommand"`
|
||||
PolicyPath string `json:"policyPath,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
// ProbeMCPInstall 检测 Node/npm、全局 npm 包、Go 二进制与 registry 最新版。
|
||||
func ProbeMCPInstall() MCPInstallInfo {
|
||||
info := MCPInstallInfo{
|
||||
Status: "not_installed",
|
||||
InstallCommand: "npm install -g " + npmPackageName + "@latest",
|
||||
NpxCommand: "npx -y " + npmPackageName,
|
||||
}
|
||||
if p, err := PolicyPath(); err == nil {
|
||||
info.PolicyPath = p
|
||||
}
|
||||
|
||||
node, nodeVer := findToolVersion("node", "--version")
|
||||
npm, npmVer := findToolVersion("npm", "--version")
|
||||
info.NodeVersion = strings.TrimPrefix(strings.TrimSpace(nodeVer), "v")
|
||||
if node != "" && info.NodeVersion != "" {
|
||||
info.NodeVersion = "v" + strings.TrimPrefix(info.NodeVersion, "v")
|
||||
}
|
||||
info.NpmAvailable = npm != ""
|
||||
info.NpmVersion = strings.TrimSpace(npmVer)
|
||||
|
||||
if bin := resolveGoMCPBinary(); bin != "" {
|
||||
info.GoBinaryFound = true
|
||||
info.GoBinaryPath = bin
|
||||
}
|
||||
|
||||
if npm != "" {
|
||||
ver := npmGlobalPackageVersion(npm, npmPackageName)
|
||||
if ver != "" {
|
||||
info.NpmPackageInstalled = true
|
||||
info.NpmPackageVersion = ver
|
||||
info.CurrentVersion = ver
|
||||
}
|
||||
}
|
||||
if info.CurrentVersion == "" && info.GoBinaryFound {
|
||||
info.CurrentVersion = "native"
|
||||
}
|
||||
|
||||
info.LatestVersion = fetchLatestNpmVersion(npmPackageName)
|
||||
|
||||
switch {
|
||||
case info.GoBinaryFound && (info.NpmPackageInstalled || info.NpmAvailable):
|
||||
info.Status = "ready"
|
||||
info.Installed = true
|
||||
if !info.NpmPackageInstalled {
|
||||
info.Hint = "已找到 Go 二进制;建议再 npm i -g,或用 npx + OPSXDB_MCP_BIN。"
|
||||
}
|
||||
case info.GoBinaryFound:
|
||||
info.Status = "partial"
|
||||
info.Installed = true
|
||||
info.Hint = "已找到 Go 二进制,但未检测到 Node/npm。客户端可用绝对路径启动二进制。"
|
||||
case info.NpmPackageInstalled:
|
||||
info.Status = "partial"
|
||||
info.Installed = false
|
||||
info.Hint = "npm 包已装,但未找到 Go 二进制。请 go build 后设置 OPSXDB_MCP_BIN。"
|
||||
case info.NpmAvailable:
|
||||
info.Status = "not_installed"
|
||||
info.Hint = "检测到 Node/npm,尚未安装 MCP 包或 Go 二进制。"
|
||||
default:
|
||||
info.Status = "not_installed"
|
||||
info.Hint = "未检测到 Node.js。请安装 Node 18+,或直接用 Go 二进制绝对路径配置客户端。"
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func findToolVersion(name string, versionArg string) (bin, version string) {
|
||||
bin = lookPathAugmented(name)
|
||||
if bin == "" {
|
||||
return "", ""
|
||||
}
|
||||
out, err := exec.Command(bin, versionArg).CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, ""
|
||||
}
|
||||
return bin, strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func lookPathAugmented(name string) string {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
var extras []string
|
||||
if home != "" {
|
||||
nvm := filepath.Join(home, ".nvm", "versions", "node")
|
||||
if ents, err := os.ReadDir(nvm); err == nil {
|
||||
for _, e := range ents {
|
||||
if e.IsDir() {
|
||||
extras = append(extras, filepath.Join(nvm, e.Name(), "bin"))
|
||||
}
|
||||
}
|
||||
}
|
||||
extras = append(extras,
|
||||
filepath.Join(home, ".local", "bin"),
|
||||
filepath.Join(home, "bin"),
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
)
|
||||
}
|
||||
pathEnv := os.Getenv("PATH")
|
||||
aug := strings.Join(extras, string(os.PathListSeparator))
|
||||
if pathEnv != "" {
|
||||
aug = aug + string(os.PathListSeparator) + pathEnv
|
||||
}
|
||||
cmd := exec.Command(name, "--version")
|
||||
cmd.Env = append(os.Environ(), "PATH="+aug)
|
||||
// Prefer LookPath with augmented PATH
|
||||
for _, dir := range extras {
|
||||
cand := filepath.Join(dir, name)
|
||||
if runtime.GOOS == "windows" {
|
||||
cand += ".exe"
|
||||
}
|
||||
if st, err := os.Stat(cand); err == nil && !st.IsDir() {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p
|
||||
}
|
||||
_ = cmd
|
||||
return ""
|
||||
}
|
||||
|
||||
func npmGlobalPackageVersion(npmBin, pkg string) string {
|
||||
out, err := exec.Command(npmBin, "list", "-g", pkg, "--depth=0", "--json").CombinedOutput()
|
||||
if err != nil && len(out) == 0 {
|
||||
return ""
|
||||
}
|
||||
var raw struct {
|
||||
Dependencies map[string]struct {
|
||||
Version string `json:"version"`
|
||||
} `json:"dependencies"`
|
||||
}
|
||||
if json.Unmarshal(out, &raw) != nil {
|
||||
return ""
|
||||
}
|
||||
if d, ok := raw.Dependencies[pkg]; ok {
|
||||
return d.Version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fetchLatestNpmVersion(pkg string) string {
|
||||
client := &http.Client{Timeout: 4 * time.Second}
|
||||
url := "https://registry.npmjs.org/" + pkg + "/latest"
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return ""
|
||||
}
|
||||
var raw struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if json.NewDecoder(res.Body).Decode(&raw) != nil {
|
||||
return ""
|
||||
}
|
||||
return raw.Version
|
||||
}
|
||||
|
||||
func resolveGoMCPBinary() string {
|
||||
if env := strings.TrimSpace(os.Getenv("OPSXDB_MCP_BIN")); env != "" {
|
||||
if isLikelyNativeBinary(env) {
|
||||
return env
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath("opsxdb-mcp"); err == nil && isLikelyNativeBinary(p) {
|
||||
return p
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
cands := []string{}
|
||||
if home != "" {
|
||||
cands = append(cands,
|
||||
filepath.Join(home, "devops", "opsxdb", "bin", "opsxdb-mcp"),
|
||||
filepath.Join(home, ".opsxdb", "bin", "opsxdb-mcp"),
|
||||
filepath.Join(home, ".local", "bin", "opsxdb-mcp"),
|
||||
)
|
||||
}
|
||||
// Relative to running server binary (src-tauri/binaries or server/)
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
cands = append(cands,
|
||||
filepath.Join(dir, "opsxdb-mcp"),
|
||||
filepath.Join(dir, "..", "opsxdb-mcp"),
|
||||
filepath.Join(dir, "..", "..", "bin", "opsxdb-mcp"),
|
||||
filepath.Join(dir, "..", "..", "..", "bin", "opsxdb-mcp"),
|
||||
)
|
||||
}
|
||||
cands = append(cands, "/usr/local/bin/opsxdb-mcp", "/opt/homebrew/bin/opsxdb-mcp")
|
||||
for _, c := range cands {
|
||||
if isLikelyNativeBinary(c) {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isLikelyNativeBinary(path string) bool {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil || st.IsDir() {
|
||||
return false
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, 8)
|
||||
n, _ := f.Read(buf)
|
||||
if n < 2 {
|
||||
return false
|
||||
}
|
||||
// shebang → script
|
||||
if buf[0] == '#' && buf[1] == '!' {
|
||||
return false
|
||||
}
|
||||
if n >= 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F' {
|
||||
return true
|
||||
}
|
||||
if buf[0] == 'M' && buf[1] == 'Z' {
|
||||
return true
|
||||
}
|
||||
if n >= 4 {
|
||||
be := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
|
||||
le := uint32(buf[3])<<24 | uint32(buf[2])<<16 | uint32(buf[1])<<8 | uint32(buf[0])
|
||||
macho := map[uint32]bool{
|
||||
0xfeedfacf: true, 0xcffaedfe: true, 0xfeedface: true, 0xcefaedfe: true,
|
||||
0xcafebabe: true, 0xbebafeca: true, 0xcafebabf: true, 0xbfbafeca: true,
|
||||
}
|
||||
if macho[be] || macho[le] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user