package permission // AI 分类器工厂 -- 多模型提供商适配. // // 不同 AI 提供商有不同的能力(cache_control, structured output, grounding), // 工厂方法为每个提供商创建针对性优化的分类器实现. // // 升华改进(ELEVATED): 工厂方法针对特定模型优化. // 不同 AI 提供商有不同的能力(cache_control, structured output, grounding), // 分类器利用各家独特优势. // 替代方案:统一纯文本 prompt + regex 解析(通用但浪费各家特有能力). import ( "context" "strings" "sync" "git.flytoex.net/yuanwei/flyto-agent/pkg/config" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // ClassifierFactory 是 AI 分类器的工厂函数类型. type ClassifierFactory func(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier // classifierFactoriesMu 保护 classifierFactories 的读写并发访问. // // 精妙之处(CLEVER): 注册时写锁,查找时读锁-- // 热路径(NewClassifierForProvider)只需读锁,不会阻塞并发查找. // 写锁仅在 RegisterClassifierFactory(插件热加载场景)和 init() 时获取. var classifierFactoriesMu sync.RWMutex // classifierFactories 注册的分类器工厂. // // 升华改进(ELEVATED): 使用 RWMutex 保护并发读写,支持插件热加载场景-- // 原方案:裸 map,并发注册(如插件系统在多 goroutine 中调用 RegisterClassifierFactory) // 会触发 go test -race 数据竞争,生产中可能产生 map concurrent write panic. // 替代方案:sync.Map(适合读多写极少场景,但类型断言代码更繁琐). var classifierFactories = map[string]ClassifierFactory{ "anthropic": NewAnthropicClassifier, "openai": NewOpenAIClassifier, "google": NewGoogleClassifier, "generic": NewGenericClassifier, } // RegisterClassifierFactory 注册自定义的分类器工厂. // 并发安全,可在插件热加载场景中从多 goroutine 调用. func RegisterClassifierFactory(provider string, factory ClassifierFactory) { classifierFactoriesMu.Lock() defer classifierFactoriesMu.Unlock() classifierFactories[provider] = factory } // NewClassifierForProvider 根据提供商名称创建分类器. // 如果提供商未注册,使用 generic 兜底.并发安全(读锁). func NewClassifierForProvider(providerName string, provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier { classifierFactoriesMu.RLock() factory, ok := classifierFactories[strings.ToLower(providerName)] if !ok { factory = classifierFactories["generic"] } classifierFactoriesMu.RUnlock() return factory(provider, stage1Model, stage2Model) } // DetectProvider 根据模型 ID 推断提供商. // // 精妙之处(CLEVER): 通过模型 ID 前缀自动检测提供商, // 用户不需要显式配置 provider 字段. // claude-* → anthropic, gpt-* → openai, gemini-* → google. func DetectProvider(modelID string) string { modelLower := strings.ToLower(modelID) switch { case strings.HasPrefix(modelLower, "claude"): return "anthropic" case strings.HasPrefix(modelLower, "gpt"), strings.HasPrefix(modelLower, "o1"), strings.HasPrefix(modelLower, "o3"): return "openai" case strings.HasPrefix(modelLower, "gemini"): return "google" default: return "generic" } } // NewClassifierFromRegistry 使用 ModelRegistry 和注入的 ModelProvider 创建分类器. // 从注册表中获取 RoleFast 和 RoleMain 的模型 ID,通过模型 ID 自动检测提供商类型. // // 升华改进(ELEVATED): 不硬编码模型 ID,完全通过 ModelRegistry 解耦. // 用户切换模型只需修改角色映射,分类器自动适配. // 此前版本通过 legacyClientProvider 桥接 api.Client-- // 那个路径将 Anthropic HTTP 客户端泄漏给所有分类器,OpenAI/Gemini 无法使用. // 现在直接接受 flyto.ModelProvider 接口,provider 由调用方注入,分类器完全 Provider 无关. // 替代方案(原方案): apiKey+baseURL 参数 + legacyClientProvider 桥接-- // 调用方仍需提供 Anthropic 凭据,即使实际 provider 是 OpenAI. func NewClassifierFromRegistry(registry *config.ModelRegistry, provider flyto.ModelProvider) SecurityClassifier { stage1Model := registry.GetRole(config.RoleFast) stage2Model := registry.GetRole(config.RoleMain) if stage1Model == "" || stage2Model == "" { return nil } if provider == nil { return nil } providerName := DetectProvider(stage1Model) return NewClassifierForProvider(providerName, provider, stage1Model, stage2Model) } // --- 各提供商的分类器实现 --- // NewAnthropicClassifier 创建 Anthropic 特化的分类器. // // Anthropic 实现特点: // - prompt caching(分类器系统提示跨调用复用) // - 标准 AIClassifier(XML 输出天然适合 Anthropic 模型) // - cache_control: ephemeral 标记可在上层配置 func NewAnthropicClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier { // 精妙之处(CLEVER): Anthropic 模型天然擅长 XML 格式输出, // 所以 Stage 2 的 // 格式 // 在 Claude 上的解析成功率接近 100%.不需要额外的结构化输出配置. return NewAIClassifier(provider, stage1Model, stage2Model) } // NewOpenAIClassifier 创建 OpenAI 特化的分类器. // // ⚠️ 占位实现(PLACEHOLDER):当前与 NewAnthropicClassifier 完全相同. // // OpenAI 的 API 协议(JSON body schema,响应格式,SSE 事件名)与 Anthropic 不兼容, // 通过同一个 api.Client 调用 OpenAI 模型时 SSE 解析会失败或产生错误结果. // 调用此工厂的代码将获得一个"表面可用但实际行为不正确"的分类器-- // 请勿在生产环境中将 provider 设为 "openai",直到专用适配层完成. // // 技术债记录(P1-6):未来需实现 OpenAIClassifier,差异点: // - API endpoint: /v1/chat/completions(非 /v1/messages) // - Auth header: Authorization: Bearer(非 x-api-key) // - 响应 JSON 结构: choices[].message.content(非 content[].text) // - SSE event 格式: data: {delta: {content: ...}}(非 content_block_delta) // - 推荐用 JSON mode(response_format: {type: "json_object"})取代 XML 解析 // // 历史包袱(LEGACY): 占位实现返回了与 Anthropic 相同的 AIClassifier, // 使调用方误以为切换 provider 字段即可无缝切换供应商. func NewOpenAIClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier { return newUnimplementedClassifier("openai", NewAIClassifier(provider, stage1Model, stage2Model)) } // NewGoogleClassifier 创建 Google 特化的分类器. // // ⚠️ 占位实现(PLACEHOLDER):当前与 NewAnthropicClassifier 完全相同. // // Google AI(Gemini)的 API 协议与 Anthropic 差异更大(REST-only,safety_settings 参数, // grounding 功能,不同的 SSE 格式),通过 api.Client 调用时会产生请求/解析错误. // 请勿在生产环境中将 provider 设为 "google",直到专用适配层完成. // // 技术债记录(P1-6):未来需实现 GoogleClassifier,差异点: // - Endpoint: generativelanguage.googleapis.com // - Auth: ?key= 或 Bearer token // - 请求格式: generateContent(与 /v1/messages 完全不同) // - 响应格式: candidates[].content.parts[].text // - 推荐利用 grounding 特性增强分类准确性 // // 历史包袱(LEGACY): 与 NewOpenAIClassifier 同样的占位问题. func NewGoogleClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier { return newUnimplementedClassifier("google", NewAIClassifier(provider, stage1Model, stage2Model)) } // unimplementedClassifierWrapper 包装一个占位 SecurityClassifier,在每次调用时注入警告信息. // // 精妙之处(CLEVER): 透明代理模式--包装而非 nil 返回,保证调用方不会因 nil dereference panic, // 同时通过 Classify 方法返回的 ClassifyResult.Reason 字段让调用方感知到"占位实现"状态. // 替代方案 A:直接返回 nil(调用方 panic,强制感知但体验极差). // 替代方案 B:只写注释(调用方完全无运行时感知,静默产生错误结果). type unimplementedClassifierWrapper struct { provider string delegate SecurityClassifier } func newUnimplementedClassifier(provider string, delegate SecurityClassifier) SecurityClassifier { return &unimplementedClassifierWrapper{provider: provider, delegate: delegate} } // Classify 执行分类,并在 Reason 字段中附加占位实现警告. // 调用方可通过检查 ClassifyResult.Reason 是否含 "placeholder" 来感知此状态. func (u *unimplementedClassifierWrapper) Classify(ctx context.Context, req *ClassifyRequest) (*ClassifyResult, error) { result, err := u.delegate.Classify(ctx, req) if err != nil { return nil, err } // 注入占位警告--让上层审计日志和调试者能看到"这里用了占位实现". // 精妙之处(CLEVER): 追加而非覆盖 Reason--保留原始决策原因,警告作为后缀, // 方便调用方分别提取决策原因和实现状态. placeholder := "[WARNING: " + u.provider + " classifier is a placeholder; API format mismatch may cause incorrect results]" if result.Reason != "" { result.Reason = result.Reason + " " + placeholder } else { result.Reason = placeholder } return result, nil } // NewGenericClassifier 创建通用兜底分类器. // // Generic 实现(兜底): // - 纯文本 prompt // - regex 解析 ALLOW/BLOCK // - 不依赖任何提供商特有功能 func NewGenericClassifier(provider flyto.ModelProvider, stage1Model, stage2Model string) SecurityClassifier { return NewAIClassifier(provider, stage1Model, stage2Model) }