Files
cliproxy-plugin-commandcode/plugin/management.go
T
yuez 688a7f395f feat: derive monthly (billing period) usage window
- Add FetchUsageSummaryRaw calling /internal/usage/summary which returns
  billing-period totals (periodBasis=billing-period)
- Derive monthly window: used=totalMonthlyCredits, cap=totalMonthlyCredits
  + monthlyCredits remaining (≈ plan monthly total), remaining=monthlyCredits
- Add monthly card to quota resource page (HTML + JS rendering)
- Skip monthly card when summary unavailable
2026-09-04 14:13:08 +08:00

218 lines
6.0 KiB
Go

package plugin
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
)
// RegisterManagement handles management.register method.
func RegisterManagement() (ManagementRegistrationResponse, error) {
return ManagementRegistrationResponse{
Routes: []ManagementRoute{
{
Method: http.MethodGet,
Path: "/plugins/commandcode/usage",
Description: "Query Command Code credits and window limits usage",
},
{
Method: http.MethodPost,
Path: "/plugins/commandcode/usage",
Description: "Query Command Code credits and window limits usage with custom session_token",
},
},
Resources: []ResourceRoute{
{
Path: "/quota",
Menu: "Command Code 配额",
Description: "Command Code 用量与限额卡片",
},
},
}, nil
}
// HandleManagement processes management.handle requests for API routes and resource pages.
func HandleManagement(ctx context.Context, req ManagementRequest, cfg *PluginConfig) (ManagementResponse, error) {
method := strings.ToUpper(strings.TrimSpace(req.Method))
path := strings.TrimSpace(req.Path)
// 1. Serve Quota Resource Page
if method == http.MethodGet && (strings.HasSuffix(path, "/quota") || strings.HasSuffix(path, "/quota/")) {
return ManagementResponse{
StatusCode: http.StatusOK,
Headers: map[string][]string{
"Content-Type": {"text/html; charset=utf-8"},
},
Body: GetQuotaPageHTML(),
}, nil
}
// 2. Serve Usage API (GET / POST)
if strings.HasSuffix(path, "/plugins/commandcode/usage") || strings.HasSuffix(path, "/usage") {
switch method {
case http.MethodGet:
return handleGetUsage(ctx, req, cfg)
case http.MethodPost:
return handlePostUsage(ctx, req, cfg)
default:
return ManagementResponse{
StatusCode: http.StatusMethodNotAllowed,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: []byte(`{"ok":false,"error":"method not allowed"}`),
}, nil
}
}
// Unknown path
return ManagementResponse{
StatusCode: http.StatusNotFound,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: []byte(`{"ok":false,"error":"not found"}`),
}, nil
}
func handleGetUsage(ctx context.Context, req ManagementRequest, cfg *PluginConfig) (ManagementResponse, error) {
sessionToken := ""
apiBase := ""
// Check query params
if len(req.Query) > 0 {
if tokens, ok := req.Query["session_token"]; ok && len(tokens) > 0 {
sessionToken = tokens[0]
} else if tokens, ok := req.Query["token"]; ok && len(tokens) > 0 {
sessionToken = tokens[0]
}
if bases, ok := req.Query["api_base"]; ok && len(bases) > 0 {
apiBase = bases[0]
}
}
// Fallback to plugin config
if sessionToken == "" && cfg != nil {
sessionToken = cfg.GetSessionToken()
}
if apiBase == "" && cfg != nil {
apiBase = cfg.GetAPIBase()
}
return executeUsageQuery(ctx, apiBase, sessionToken, req.HostCallbackID)
}
func handlePostUsage(ctx context.Context, req ManagementRequest, cfg *PluginConfig) (ManagementResponse, error) {
var body struct {
SessionToken string `json:"session_token"`
Token string `json:"token"`
APIBase string `json:"api_base"`
}
if len(req.Body) > 0 {
_ = json.Unmarshal(req.Body, &body)
}
sessionToken := body.SessionToken
if sessionToken == "" {
sessionToken = body.Token
}
apiBase := body.APIBase
// Fallback to plugin config if body didn't specify
if sessionToken == "" && cfg != nil {
sessionToken = cfg.GetSessionToken()
}
if apiBase == "" && cfg != nil {
apiBase = cfg.GetAPIBase()
}
return executeUsageQuery(ctx, apiBase, sessionToken, req.HostCallbackID)
}
func executeUsageQuery(ctx context.Context, apiBase, sessionToken, hostCallbackID string) (ManagementResponse, error) {
if strings.TrimSpace(sessionToken) == "" {
resBytes, _ := json.Marshal(map[string]any{
"ok": false,
"error": "session_token is required. Configure session_token in plugin config, provide a credential file, or pass session_token in request",
})
return ManagementResponse{
StatusCode: http.StatusBadRequest,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: resBytes,
}, nil
}
raw, statusCode, errFetch := FetchCreditsRaw(ctx, apiBase, sessionToken, hostCallbackID)
if errFetch != nil {
resBytes, _ := json.Marshal(map[string]any{
"ok": false,
"status_code": statusCode,
"error": errFetch.Error(),
})
if statusCode == 0 || statusCode == http.StatusOK {
statusCode = http.StatusBadGateway
}
return ManagementResponse{
StatusCode: statusCode,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: resBytes,
}, nil
}
if statusCode != http.StatusOK {
resBytes, _ := json.Marshal(map[string]any{
"ok": false,
"status_code": statusCode,
"error": "upstream returned non-200 status",
"body": string(raw),
})
return ManagementResponse{
StatusCode: statusCode,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: resBytes,
}, nil
}
// Fetch billing-period (monthly) usage totals; non-fatal if unavailable.
var summary *UpstreamUsageSummaryResponse
if sumRaw, sumStatus, sumErr := FetchUsageSummaryRaw(ctx, apiBase, sessionToken, hostCallbackID); sumErr == nil && sumStatus == http.StatusOK {
var parsed UpstreamUsageSummaryResponse
if errSum := json.Unmarshal(sumRaw, &parsed); errSum == nil && parsed.TotalMonthlyCredits > 0 {
summary = &parsed
}
}
usage, errParse := ParseAndFormatUsage(raw, summary, time.Now().UTC())
if errParse != nil {
resBytes, _ := json.Marshal(map[string]any{
"ok": false,
"error": "failed to parse upstream usage: " + errParse.Error(),
})
return ManagementResponse{
StatusCode: http.StatusBadGateway,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: resBytes,
}, nil
}
resBytes, _ := json.Marshal(usage)
return ManagementResponse{
StatusCode: http.StatusOK,
Headers: map[string][]string{
"Content-Type": {"application/json"},
},
Body: resBytes,
}, nil
}