111 lines
2.6 KiB
Go
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()
|
|
}
|