- 补入库:server/internal/db/dump.go、redis 分页/扫描预算测试、opsxdb-mcp main_test、 packages/opsxdb-mcp package-lock、docs 审计与对比文档等 37 个新文件 - 含结构扁平化进行中的 80 处修改与 155 处旧结构删除(server/ src/ src-tauri/ 为当前有效源) - 构建产物(bin/target/dist)与签名私钥仍按规则排除
270 lines
7.4 KiB
Go
270 lines
7.4 KiB
Go
package aitools
|
|
|
|
import (
|
|
"context"
|
|
"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 "", ""
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(ctx, 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",
|
|
)
|
|
}
|
|
// 检查常见安装路径,不执行探测命令。
|
|
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
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func npmGlobalPackageVersion(npmBin, pkg string) string {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(ctx, 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
|
|
}
|