package plugin import ( "encoding/json" "fmt" "net/http" "sync" "sync/atomic" ) type App struct { config atomic.Pointer[Config] mu sync.Mutex closed bool seen atomic.Uint64 } func NewApp() *App { a := &App{} cfg := defaultConfig() a.config.Store(&cfg) return a } func (a *App) HandleMethod(method string, request []byte) (response []byte, err error) { defer func() { if recovered := recover(); recovered != nil { response = nil err = fmt.Errorf("插件处理 %s 时发生异常: %v", method, recovered) } }() switch method { case MethodPluginRegister, MethodPluginReconfigure: return a.configure(request) case MethodUsageHandle: return a.handleUsage(request) case MethodPluginShutdown: a.Shutdown() return OKEnvelope(struct{}{}) default: return ErrorEnvelope("unknown_method", "不支持的插件方法: "+method, http.StatusNotFound), nil } } func (a *App) configure(raw []byte) ([]byte, error) { var req LifecycleRequest if len(raw) > 0 { if err := json.Unmarshal(raw, &req); err != nil { return nil, fmt.Errorf("解析生命周期请求: %w", err) } } if req.SchemaVersion == 0 { req.SchemaVersion = 1 } negotiated := min(req.SchemaVersion, SchemaVersion) cfg, err := decodeConfig(req.ConfigYAML) if err != nil { return nil, err } a.mu.Lock() defer a.mu.Unlock() if a.closed { return nil, fmt.Errorf("插件已经关闭") } a.config.Store(&cfg) return OKEnvelope(registration(negotiated)) } func registration(schemaVersion uint32) Registration { return Registration{ SchemaVersion: schemaVersion, Metadata: Metadata{ Name: PluginName, Version: Version, Author: "cpa-ext", GitHubRepository: "https://git.pchuan.top/agent/cpa-plugin", ConfigFields: []ConfigField{ {Name: "enabled", Type: "boolean", Description: "启用 CPA 扩展。"}, {Name: "codex_only", Type: "boolean", Description: "只接收 Codex/OpenAI 模型的用量事件。"}, }, }, Capabilities: Capabilities{UsagePlugin: true}, } } func (a *App) handleUsage(raw []byte) ([]byte, error) { var record UsageRecord if err := json.Unmarshal(raw, &record); err != nil { return nil, fmt.Errorf("解析用量事件: %w", err) } cfg := a.config.Load() if cfg != nil && cfg.accepts(record) { // The first milestone only proves ingestion. Persistence and aggregation // belong in a separate package added behind this boundary. a.seen.Add(1) } return OKEnvelope(struct{}{}) } func (a *App) Seen() uint64 { return a.seen.Load() } func (a *App) Shutdown() { a.mu.Lock() a.closed = true a.mu.Unlock() }