mirror of
https://github.com/zgs225/cliproxy-plugin-commandcode.git
synced 2026-09-26 11:42:48 +08:00
- 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
218 lines
6.0 KiB
Go
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
|
|
}
|