Files
CLIProxyAPI/internal/pluginhost/snapshot.go
Luis Pater b192f6550c feat(management): add plugin quota and declarative probe endpoints
- Add endpoints to list quota providers and fetch or reset credential quotas via plugins.
- Support declarative metadata quota probes with token substitution and response mapping.
- Clear core routing quota state when provider quota reset succeeds.

Closes: #5752
2026-09-12 19:07:43 +08:00

177 lines
4.5 KiB
Go

package pluginhost
import (
"sort"
"strings"
"sync"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
)
type capabilityRecord struct {
id string
path string
version string
priority int
meta pluginapi.Metadata
plugin pluginapi.Plugin
}
type Snapshot struct {
enabled bool
records []capabilityRecord
quotaSupportedProvidersMu sync.RWMutex
quotaSupportedProviders map[string][]string
}
// RegisteredPluginInfo describes a plugin that is active in the current runtime snapshot.
type RegisteredPluginInfo struct {
ID string
Priority int
Metadata pluginapi.Metadata
SupportsOAuth bool
OAuthProvider string
SupportsQuota bool
QuotaProvider string
Menus []RegisteredPluginMenu
}
// RegisteredPluginMenu describes a plugin-owned resource menu entry.
type RegisteredPluginMenu struct {
Path string
Menu string
Description string
}
func emptySnapshot() *Snapshot {
return &Snapshot{
quotaSupportedProviders: make(map[string][]string),
}
}
func (h *Host) activeRecords() []capabilityRecord {
return h.activeRecordsFromSnapshot(h.Snapshot())
}
func (h *Host) activeRecordsFromSnapshot(snap *Snapshot) []capabilityRecord {
if snap == nil || len(snap.records) == 0 {
return nil
}
out := make([]capabilityRecord, 0, len(snap.records))
for _, record := range snap.records {
if h.recordCurrent(record) {
out = append(out, record)
}
}
return out
}
// RegisteredPlugins returns a stable copy of plugin metadata in the current runtime snapshot.
func (h *Host) RegisteredPlugins() []RegisteredPluginInfo {
records := h.activeRecords()
if len(records) == 0 {
return nil
}
menusByPlugin := h.registeredPluginMenus()
out := make([]RegisteredPluginInfo, 0, len(records))
for _, record := range records {
authProvider := record.plugin.Capabilities.AuthProvider
oauthProvider := ""
if authProvider != nil && !h.isPluginFused(record.id) {
if identifier, okIdentifier := h.callAuthProviderIdentifier(record.id, authProvider); okIdentifier {
oauthProvider = identifier
}
}
quotaProvider := record.plugin.Capabilities.QuotaProvider
quotaIdentifier := ""
if quotaProvider != nil && !h.isPluginFused(record.id) {
if identifier, okIdentifier := h.callQuotaIdentifier(record.id, quotaProvider); okIdentifier {
quotaIdentifier = identifier
}
}
out = append(out, RegisteredPluginInfo{
ID: record.id,
Priority: record.priority,
Metadata: clonePluginMetadata(record.meta),
SupportsOAuth: authProvider != nil,
OAuthProvider: oauthProvider,
SupportsQuota: quotaProvider != nil,
QuotaProvider: quotaIdentifier,
Menus: menusByPlugin[record.id],
})
}
return out
}
// PluginRegistered reports whether a plugin is active in the current runtime snapshot.
func (h *Host) PluginRegistered(id string) bool {
if h == nil {
return false
}
id = strings.TrimSpace(id)
if id == "" {
return false
}
for _, record := range h.activeRecords() {
if record.id == id {
return true
}
}
return false
}
func (h *Host) registeredPluginMenus() map[string][]RegisteredPluginMenu {
out := make(map[string][]RegisteredPluginMenu)
if h == nil {
return out
}
h.mu.Lock()
defer h.mu.Unlock()
for _, record := range h.resourceRoutes {
menu := strings.TrimSpace(record.route.Menu)
if menu == "" {
continue
}
out[record.pluginID] = append(out[record.pluginID], RegisteredPluginMenu{
Path: strings.TrimSpace(record.route.Path),
Menu: menu,
Description: strings.TrimSpace(record.route.Description),
})
}
for pluginID := range out {
sort.SliceStable(out[pluginID], func(i, j int) bool {
return out[pluginID][i].Path < out[pluginID][j].Path
})
}
return out
}
func sortRecords(records []capabilityRecord) {
sort.SliceStable(records, func(i, j int) bool {
if records[i].priority == records[j].priority {
return records[i].id < records[j].id
}
return records[i].priority > records[j].priority
})
}
func clonePluginMetadata(meta pluginapi.Metadata) pluginapi.Metadata {
if len(meta.ConfigFields) == 0 {
return meta
}
meta.ConfigFields = cloneConfigFields(meta.ConfigFields)
return meta
}
func cloneConfigFields(fields []pluginapi.ConfigField) []pluginapi.ConfigField {
if len(fields) == 0 {
return nil
}
out := make([]pluginapi.ConfigField, len(fields))
copy(out, fields)
for index := range out {
out[index].EnumValues = append([]string(nil), fields[index].EnumValues...)
}
return out
}