package plugin // dependency.go 实现插件依赖解析器. // // 核心问题:插件 A 依赖插件 B,如果 B 未加载或被禁用,A 也必须被禁用. // 依赖关系可以形成有向无环图(DAG);如果有环,所有涉及的插件均报错. // // 设计决策: // - 使用 DFS + 灰集检测环(标准拓扑排序算法) // - VerifyAndDemote 用固定点迭代传播禁用状态(比一次 DFS 更清晰) // - 跨来源依赖被拒绝:项目级插件不能依赖用户级插件(避免隐式全局依赖) // // 来源判定: // // 当前以 Plugin.Source 字段区分("user" 或 "project"), // Source 在 LoadPlugin 时根据搜索路径自动填充. // // 升华改进(ELEVATED): 早期实现 无依赖解析(每个插件独立). // 我们增加依赖 DAG 是为了仓储/金融场景中的"插件包"需求: // 一个"仓储核心"插件可以依赖"barcode-scanner"和"inventory-db"插件, // 安装核心包自动拉取所有依赖,禁用依赖自动禁用上层. import "fmt" // Source 表示插件的来源级别. // 来源决定了依赖规则:只允许同级或向上(项目级→用户级不允许). // // 精妙之处(CLEVER): 用整数而非字符串,使来源比较可以用 >= 运算符. // SourceBuiltin=0 < SourceUser=1 < SourceProject=2,越大优先级越高. // 如果改用字符串比较,跨来源逻辑会更复杂. type Source int const ( // SourceBuiltin 内置插件(代码注册,无文件系统来源) SourceBuiltin Source = iota // SourceUser 用户级插件(~/.flyto/plugins/) SourceUser // SourceProject 项目级插件(/.flyto/plugins/) SourceProject ) // ResolveClosure 从给定的起点插件集合出发,通过 DFS 计算传递闭包. // 返回所有需要加载的插件(包括所有传递依赖),按拓扑顺序排列(依赖先于被依赖者). // // 参数: // - names: 用户请求加载的插件名称集合 // - registry: 所有已发现的插件,name → *Plugin // // 返回: // - ordered: 拓扑排序后的插件列表(可直接按序加载) // - errors: 依赖缺失或循环错误(每个受影响插件一条) // // 算法:三色 DFS // - 白(未访问):初始状态 // - 灰(访问中):当前 DFS 路径上,发现灰节点 = 有环 // - 黑(已完成):已排入结果 func ResolveClosure(names []string, registry map[string]*Plugin) ([]*Plugin, []PluginError) { var ( ordered []*Plugin errs []PluginError colorMap = make(map[string]color) // name → white/gray/black ) // 对每个请求的起点做 DFS;跳过已完成的节点(黑色). for _, name := range names { if colorMap[name] == black { continue } var path []string // 当前 DFS 路径,用于构建循环诊断消息 dfsVisit(name, registry, colorMap, &path, &ordered, &errs) } return ordered, errs } // color 是 DFS 三色标记. type color int const ( white color = iota // 未访问 gray // 访问中(在当前路径上) black // 已完成 ) // dfsVisit 对单个节点做 DFS,递归处理其所有依赖. func dfsVisit( name string, registry map[string]*Plugin, colorMap map[string]color, path *[]string, ordered *[]*Plugin, errs *[]PluginError, ) { switch colorMap[name] { case black: return // 已处理,跳过 case gray: // 发现环:当前路径形成了环. // 精妙之处(CLEVER): 记录完整环路径(不只是两端)方便诊断. // 例如 A→B→C→A,用户能看到整条环链而不只是"A和A冲突". cycleMsg := fmt.Sprintf("cycle detected: %v → %s", *path, name) *errs = append(*errs, PluginError{ Code: ErrDependencyCycle, PluginName: name, Message: cycleMsg, }) return } // 标记为灰色,压入路径 colorMap[name] = gray *path = append(*path, name) plugin, ok := registry[name] if !ok { // 依赖缺失:注册表中找不到该插件 *errs = append(*errs, PluginError{ Code: ErrDependencyMissing, PluginName: name, Message: fmt.Sprintf("plugin %q not found in registry", name), }) // 弹出路径,标黑(避免重复报同一节点的错误) *path = (*path)[:len(*path)-1] colorMap[name] = black return } // 先递归处理所有依赖(深度优先) // 精妙之处(CLEVER): 内置插件(RegisterBuiltin 注册)没有 Manifest, // 视为无依赖节点,直接跳过依赖递归. var deps []string if plugin.Manifest != nil { deps = plugin.Manifest.Dependencies } for _, dep := range deps { // 跨来源检查:项目级插件不允许依赖用户级插件 if dep != "" { depPlugin, depExists := registry[dep] if depExists && plugin.Source > depPlugin.Source && depPlugin.Source != SourceBuiltin { // 历史包袱(LEGACY): 当前只检测项目级→用户级跨源依赖. // 未来应支持跨组织依赖白名单,让企业内部共享插件库. *errs = append(*errs, PluginError{ Code: ErrDependencyCrossSource, PluginName: plugin.Name, Message: fmt.Sprintf( "plugin %q (project-level) cannot depend on %q (user-level)", plugin.Name, dep, ), }) continue } } dfsVisit(dep, registry, colorMap, path, ordered, errs) } // 所有依赖处理完后,将当前节点加入有序结果(后序,依赖在前) colorMap[name] = black *path = (*path)[:len(*path)-1] *ordered = append(*ordered, plugin) } // VerifyAndDemote 固定点迭代:如果某个插件的依赖被禁用或有错误, // 将该插件从 enabled 移入 disabled,并记录原因. // // 算法:固定点(fixed-point)迭代 - 反复扫描直到没有新的降级发生. // // 精妙之处(CLEVER): 为什么用固定点迭代而不是拓扑排序倒序遍历? // 因为输入 enabled 列表不保证已经拓扑排序.固定点迭代不依赖顺序, // 正确处理任意顺序的输入列表.最坏情况 O(n²),实际插件数量小(<100),可接受. // 如果改用拓扑排序倒序,需要先建图,实现更复杂,收益微小. func VerifyAndDemote(enabled []*Plugin, disabled []*Plugin, loadErrs []PluginError) ( newEnabled []*Plugin, newDisabled []*Plugin, warnings []PluginError, ) { // 构建禁用集和错误集(用于快速查找) disabledSet := buildNameSet(disabled) errorSet := buildErrorNameSet(loadErrs) current := make([]*Plugin, len(enabled)) copy(current, enabled) for { changed := false var stillEnabled []*Plugin for _, p := range current { demoted, reason := shouldDemote(p, disabledSet, errorSet) if demoted { disabledSet[p.Name] = struct{}{} newDisabled = append(newDisabled, p) warnings = append(warnings, PluginError{ Code: ErrDependencyMissing, PluginName: p.Name, Message: fmt.Sprintf("disabled because dependency %q is unavailable", reason), }) changed = true } else { stillEnabled = append(stillEnabled, p) } } current = stillEnabled if !changed { break // 固定点收敛 } } newEnabled = current return newEnabled, newDisabled, warnings } // shouldDemote 检查插件是否应该被降级(禁用). // 如果任意一个依赖在禁用集或错误集中,返回 (true, 依赖名). func shouldDemote(p *Plugin, disabledSet map[string]struct{}, errorSet map[string]struct{}) (bool, string) { if p.Manifest == nil { return false, "" } for _, dep := range p.Manifest.Dependencies { if _, inDisabled := disabledSet[dep]; inDisabled { return true, dep } if _, inError := errorSet[dep]; inError { return true, dep } } return false, "" } // FindReverseDependents 返回所有直接依赖 targetName 的插件名称列表. // 用于"禁用 X 会影响哪些插件"的诊断查询. func FindReverseDependents(targetName string, all []*Plugin) []string { var result []string for _, p := range all { if p.Manifest == nil { continue } for _, dep := range p.Manifest.Dependencies { if dep == targetName { result = append(result, p.Name) break } } } return result } // buildNameSet 将插件列表转为 name → struct{} 的集合. func buildNameSet(plugins []*Plugin) map[string]struct{} { s := make(map[string]struct{}, len(plugins)) for _, p := range plugins { s[p.Name] = struct{}{} } return s } // buildErrorNameSet 从错误列表中提取受影响的插件名称集合. func buildErrorNameSet(errs []PluginError) map[string]struct{} { s := make(map[string]struct{}, len(errs)) for _, e := range errs { s[e.PluginName] = struct{}{} } return s }