feat(plugin): MCP 服务器管理页 + 按 server 独立启停与状态快照
design/09 §5.2.1 / §5.6.1:补上「看不见连没连上 / 暴露了哪些工具 / 为什么挂了」的洞。
- runtimeconfig:PluginConfig 新增 `enabled`(*bool,缺省=启用 ⇒ 既有 plugins.json
向后兼容);新增 RedactPluginSecrets(整把凭据 + 任意 ≥6 字符片段一并抹除)。
- mcp:新增 status.go 的只读状态读面(ToolSummaries:工具名 + 截断描述,取自能力
注册表本身,因此与模型可见的工具集合同源)。
- plugin 管理器:
· Install/Restore 尊重 enabled(停用=落盘不连接,重启后仍是停用,且一次都不连);
· SetEnabled 落盘 enabled(停用先落盘再下线;启用失败时 enabled 保持 false);
· 新增 Reconnect(对已停用者拒绝,不替用户启用);
· 新增 Statuses(三态:connected 缺席=未知,工具数/明细/时间只在知道时给出,
lastError 在记录那一刻即脱敏截断);
· 内置「MCP 服务器」页声明 + 7 个已声明动作(list/install/reconnect/enable/
disable/uninstall/transports),走既有通用动作端点(owner-admin + 审计);
· 护栏:热装插件的生命周期不改动内置提供者(mcp)的目录条目。
- gateway:已安装列表与 install 响应加 enabled,install 支持 config.enabled。
This commit is contained in:
1 parent
4ed5925296
commit
9654ce8861
14 files changed
+1969
-115
No files matched your search
@@ -186,3 +186,64 @@ func TestMaskedPlugin_RedactsKeyAndEnv(t *testing.T) {
|
||||
t.Fatalf("masked JSON leaks a secret: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginEnabled_BackwardCompatible 锁定 enabled 的向后兼容:既有 plugins.json
|
||||
// (没有 enabled 键)必须被当成**启用**,而显式 false 必须原样存活(含重启)。
|
||||
func TestPluginEnabled_BackwardCompatible(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "plugins.json")
|
||||
legacy := `[{"name":"calc","kind":"mcp","transport":"http","endpoint":"https://x"}]`
|
||||
if err := os.WriteFile(path, []byte(legacy), 0o600); err != nil {
|
||||
t.Fatalf("write legacy file: %v", err)
|
||||
}
|
||||
got, err := LoadPlugins(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPlugins: %v", err)
|
||||
}
|
||||
if len(got) != 1 || !got[0].IsEnabled() {
|
||||
t.Fatalf("老文件(无 enabled 键)应视为启用:%+v", got)
|
||||
}
|
||||
if got[0].Enabled != nil {
|
||||
t.Fatalf("缺失的 enabled 必须保持 nil(= 缺席),得到 %v", *got[0].Enabled)
|
||||
}
|
||||
|
||||
off := got[0].WithEnabled(false)
|
||||
if err := SavePlugins(path, []PluginConfig{off}); err != nil {
|
||||
t.Fatalf("SavePlugins: %v", err)
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read back: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"enabled": false`) {
|
||||
t.Fatalf("显式停用必须落盘:%s", raw)
|
||||
}
|
||||
back, err := LoadPlugins(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPlugins: %v", err)
|
||||
}
|
||||
if back[0].IsEnabled() {
|
||||
t.Fatalf("重启后停用态丢失:%+v", back[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedactPluginSecrets_ScrubsFragments 锁定「凭据任意片段也不许泄漏」。
|
||||
func TestRedactPluginSecrets_ScrubsFragments(t *testing.T) {
|
||||
t.Parallel()
|
||||
const key = "sk-live-0123456789abcdef"
|
||||
p := PluginConfig{APIKey: key, Env: map[string]string{"GH_TOKEN": "ghp_secret_value"}}
|
||||
for _, text := range []string{
|
||||
"bearer " + key,
|
||||
"echo " + key[8:20],
|
||||
"tail " + key[len(key)-6:],
|
||||
"env GH_TOKEN=ghp_secret_value",
|
||||
} {
|
||||
out := RedactPluginSecrets(p, text)
|
||||
if strings.Contains(out, key) || strings.Contains(out, "ghp_secret_value") {
|
||||
t.Fatalf("整把凭据仍在:%q", out)
|
||||
}
|
||||
if strings.Contains(out, key[8:20]) || strings.Contains(out, key[len(key)-6:]) {
|
||||
t.Fatalf("凭据片段仍在:%q", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user