Files
Cyrene_v2/pkg/plugins/sdk/origin.go
T
AskaEth 86aef7b454 feat(capability): 工具按上游 MCP server 分节(来源章 + 呈现隔离)
主人 2026-10-24:「能否将每个 MCP 服务器所承载的插件和工具按上游(来源)服务器分类
并与其他插件相对隔离?」

归属信息此前后端根本没有(GET /admin/plugins 每条只有 name/kind/description/两个
布尔),而 `mcp:` 是每台 server 一条的 KindMCP 条目的注册键,**不是**工具的命名空间
(工具名由 server 原样给出,连前缀都没有)⇒ 只能靠注册时的元数据,不能解析名字。

后端(唯一汇点盖章):
- 新增 pkg/plugins/sdk/origin.go:Origin{Kind,Server,Transport} + 可选接口
  OriginAware(没实现 = 宿主内置,老工具零改动)+ OriginOf(零值视为无来源)。
- registry.Capability 加 Origin 字段;RegisterTool 盖章(覆盖热装与 .env
  MCP_SERVERS_JSON 两条接入路径的合流处),ReplaceTool **同样重新盖章**(它逐字段
  重建条目,漏了就是「热替换把来源抹掉」)。
- mcp.mcpTool 实现 Origin()(server 名与传输都在 cfg 上),transport 用新的
  transportNameOf 报**实际会用的**形态(stdio/sse/http),与 newTransport 同一推断。
- 网关 PluginSummary 加可选 origin(omitempty):零值 ⇒ 键缺席,不是空对象。
- 卸载守卫:对单把 MCP 工具或 `mcp:<server>` 登记条目直接 400
  (plugin_uninstall_scope)并指向「MCP 服务器」页 —— 原来会绕过插件管理器裸删,
  server 的 names 账本不同步、重连后工具复活。判据是来源章,不是名字前缀。

前端(呈现分组):
- admin/modules/pluginOrigin.ts:纯函数按 origin.server 分节(按 server 名排序),
  transport 未报告时标题省略括号,缺 server 名不冒充内置。
- Plugins.tsx:工具段两级(`来自 fs(stdio)· N 把工具` + 宿主内置),MCP 工具行去掉
  「卸载」改为指向 MCP 页;`kind:"mcp"` 登记条目与未知 kind(iot)各有段如实显示,
  不再静默丢弃(design/09 §5.2.1 红线③)。

零影响(有断言):tool_policies 表/策略写路径未改;ToolSchemas 不发来源(测试断言
模型面既无 origin 键也无 server 名);停用 server ⇒ 条目连来源一起消失,无孤儿。

测试:registry 盖章/保章(两条均已实测变异变红)/内置零值/改策略不清章;mcp 注册
路径盖章 + transport 推断表;网关读面缺席与存在、热装整链、卸载守卫;前端 esbuild+cjs
逻辑测试 12 项。验收:go build/vet/test 全绿,frontend `npm run build` 通过。
2026-10-24 20:21:22 +08:00

54 lines
2.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sdk
// Origin 描述一个能力的**上游来源**:它是谁提供的,而不是它叫什么。
//
// 它存在的理由是「归属不能从名字里解析」:MCP server 的工具名由 server 自己给,
// 宿主不在上面加任何前缀(同名的两把工具在注册表里至多一个 owner,见
// internal/capability/mcp/registry.go),因此「这把工具来自哪台 server」只能由
// **注册那一刻的元数据**回答,不能靠字符串推断(改名、重名、前缀碰撞都会错)。
//
// 宿主内置能力(plugins/tools/**、web_search、read_file ……)**不实现** OriginAware,
// 它们的来源是「宿主自己」,不需要也不应该编一个章出来 —— 管理层面上「没有 origin
// 键」正是「宿主内置」的表达。
type Origin struct {
// Kind 是来源种类。目前只有 OriginKindMCP;空串 = 无来源。
Kind string `json:"kind"`
// Server 是提供该能力的上游 server 名(= plugins.json / MCP_SERVERS_JSON 里的
// name),不是工具名。
Server string `json:"server,omitempty"`
// Transport 是该 server 的传输形态(stdio / sse / http),来自配置而不是
// 工具的自我描述。
Transport string `json:"transport,omitempty"`
}
// OriginKindMCP 表示该能力由一台外部 MCP server 提供。
const OriginKindMCP = "mcp"
// OriginAware 由**自带来源**的能力实现:宿主在注册时据此给能力条目盖章。
//
// 它是可选接口 —— 没实现的一律按「宿主内置 / 来源未知」处理,因此既有的工具
// 实现零改动。实现方只需回报自己的来源,不参与任何路由或权限判定。
type OriginAware interface {
// Origin 返回该能力的上游来源;返回零值 Origin 表示「无可归属的来源」。
Origin() Origin
}
// IsZero 报告 o 是否是「没有来源」的零值。
func (o Origin) IsZero() bool {
return o.Kind == "" && o.Server == "" && o.Transport == ""
}
// OriginOf 返回 v 自报的来源。v 没实现 OriginAware,或自报的是零值(= 无来源)时,
// ok 为 false —— 调用方据此**省略**来源字段,而不是下发一个空对象。
func OriginOf(v any) (Origin, bool) {
oa, ok := v.(OriginAware)
if !ok {
return Origin{}, false
}
o := oa.Origin()
if o.IsZero() {
return Origin{}, false
}
return o, true
}