Files
cpa-plugin/internal/plugin/app.go
T

111 lines
2.6 KiB
Go

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()
}