mirror of
https://github.com/zgs225/cliproxy-plugin-commandcode.git
synced 2026-09-26 11:42:48 +08:00
Compare commits
2
Commits
4adda19b72
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
016929f9a7 | ||
|
|
680a40ebc8 |
@@ -4,7 +4,7 @@
|
||||
[](https://help.router-for.me/plugin/development.html)
|
||||
[](LICENSE)
|
||||
|
||||
[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) 动态 C ABI 插件,用于提供 **Command Code** 凭据认证、上游配额与窗口限额查询、以及嵌入式配额监控仪表盘卡片(QuotaCard)。
|
||||
[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) 动态 C ABI 插件,用于提供 **Command Code** 上游配额与窗口限额查询、以及嵌入式配额监控仪表盘卡片(QuotaCard)。
|
||||
|
||||
---
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
- [构建插件](#构建插件)
|
||||
- [安装与目录结构](#安装与目录结构)
|
||||
- [宿主配置 (`config.yaml`)](#宿主配置-configyaml)
|
||||
- [凭据文件配置](#凭据文件配置)
|
||||
- [管理端点与资源页](#管理端点与资源页)
|
||||
- [1. 浏览器资源页 (`QuotaCard`)](#1-浏览器资源页-quotacard)
|
||||
- [2. 管理 API: 查询用量 (`GET`)](#2-管理-api-查询用量-get)
|
||||
@@ -32,12 +31,12 @@
|
||||
1. **标准 C ABI 兼容**:
|
||||
- 导出 `cliproxy_plugin_init`、`cliproxyPluginCall`、`cliproxyPluginFree`、`cliproxyPluginShutdown`。
|
||||
- 遵照 CLIProxyAPI 官方 JSON Envelope 规范(`ok`, `result`, `error`)。
|
||||
2. **双核心能力声明**:
|
||||
- `auth_provider`: 参与凭据识别、加载、解析与刷新。
|
||||
- `management_api`: 注册插件自有的管理端点与浏览器资源页面。
|
||||
3. **凭据自动解析 (`auth.parse`)**:
|
||||
- 自动识别 `commandcode-*.json` 凭据文件、`type: "commandcode"` 配置或包含 `session_token` / Cookie 的凭据。
|
||||
- 提取并规范化 `__Secure-commandcode_prod_.session_token`,存入宿主持久化凭据库。
|
||||
2. **纯粹的管理监控能力 (`management_api`)**:
|
||||
- 注册插件自有的用量管理端点与浏览器嵌入式仪表盘资源页面。
|
||||
- 无多余的 OAuth 提供商注册,不污染 CLIProxyAPI 后台的 OAuth 授权列表。
|
||||
3. **Session Token 灵活提取与支持**:
|
||||
- 支持在 `config.yaml` 配置或在配额页面上直接输入。
|
||||
- 支持纯 token 或完整 Cookie 字符串(自动提取 `__Secure-commandcode_prod_.session_token`)。
|
||||
4. **精确用量与双滑动窗口限额解析**:
|
||||
- 上游接口:`GET https://api.commandcode.ai/internal/billing/credits`。
|
||||
- 请求优先走宿主提供的 `host.http.do` 回调(复用宿主代理、日志与鉴权管道),离线或未注入宿主时自动无缝降级至 Go 标准 `net/http`。
|
||||
@@ -55,16 +54,15 @@
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ CLIProxyAPI │
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Auth Management │ │ Management Center │ │
|
||||
│ │ (reads auths/*.json) │ │ (/v0/management) │ │
|
||||
│ └───────────┬─────────────┘ └──────────┬──────────┘ │
|
||||
│ │ C ABI │ C ABI │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ Management Center │ │
|
||||
│ │ (/v0/management) │ │
|
||||
│ └──────────┬──────────┘ │
|
||||
│ │ C ABI │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ cliproxy-plugin-commandcode.dylib/.so │ │
|
||||
│ │ │ │
|
||||
│ │ • auth.identifier / auth.parse │ │
|
||||
│ │ • management.register / management.handle │ │
|
||||
│ │ • Usage Parser & Window Limits Formatter │ │
|
||||
│ │ • Embedded Single-file HTML/CSS/JS QuotaCard │ │
|
||||
@@ -126,34 +124,10 @@ plugins:
|
||||
commandcode:
|
||||
enabled: true
|
||||
priority: 1
|
||||
session_token: "YOUR_COMMANDCODE_SESSION_TOKEN"
|
||||
session_token: "YOUR_COMMANDCODE_SESSION_TOKEN" # 支持纯 token 或完整 Cookie 字符串
|
||||
api_base: "https://api.commandcode.ai" # 可选,默认为官方接口
|
||||
```
|
||||
|
||||
### 凭据文件配置
|
||||
|
||||
除了在 `config.yaml` 中全局配置,你也可以在 CLIProxyAPI 的 `auths/` 凭据目录下创建凭据文件(如 `auths/commandcode-main.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "commandcode",
|
||||
"session_token": "YOUR_COMMANDCODE_SESSION_TOKEN",
|
||||
"email": "user@example.com",
|
||||
"label": "Command Code Pro"
|
||||
}
|
||||
```
|
||||
|
||||
或者直接放入浏览器 Cookie:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "commandcode",
|
||||
"cookie": "__Secure-commandcode_prod_.session_token=YOUR_COMMANDCODE_SESSION_TOKEN; Path=/;"
|
||||
}
|
||||
```
|
||||
|
||||
插件的 `auth.parse` 会自动拦截并完成凭据加载。
|
||||
|
||||
---
|
||||
|
||||
## 管理端点与资源页
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
7830fab0eb763247602fd4815549a13eeb25841afbe2d3a047d25b9f4b9cf86d commandcode_0.2.2_linux_amd64.zip
|
||||
Binary file not shown.
-129
@@ -1,31 +1,12 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var cookieRegex = regexp.MustCompile(`(?:^|;\s*)__Secure-commandcode_prod_\.session_token=([^;]+)`)
|
||||
|
||||
// RawAuthContent represents possible structures inside a commandcode credential JSON file.
|
||||
type RawAuthContent struct {
|
||||
Type string `json:"type"`
|
||||
Provider string `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
SessionToken string `json:"session_token"`
|
||||
CommandCodeSession string `json:"commandcode_session_token"`
|
||||
Cookie string `json:"cookie"`
|
||||
Token string `json:"token"`
|
||||
UpstreamBase string `json:"api_base"`
|
||||
}
|
||||
|
||||
// ExtractSessionToken extracts the clean session token from a raw string or cookie string.
|
||||
func ExtractSessionToken(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
@@ -54,113 +35,3 @@ func FormatSessionCookie(token string) string {
|
||||
return "__Secure-commandcode_prod_.session_token=" + clean
|
||||
}
|
||||
|
||||
// ParseAuth handles auth.parse requests for Command Code credentials.
|
||||
func ParseAuth(req AuthParseRequest) (AuthParseResponse, error) {
|
||||
lowerFileName := strings.ToLower(req.FileName)
|
||||
isCommandCodeFile := strings.HasPrefix(lowerFileName, "commandcode") && strings.HasSuffix(lowerFileName, ".json")
|
||||
isCommandCodeProvider := strings.EqualFold(req.Provider, PluginID)
|
||||
|
||||
var content RawAuthContent
|
||||
var rawMap map[string]any
|
||||
if len(req.RawJSON) > 0 {
|
||||
if err := json.Unmarshal(req.RawJSON, &content); err == nil {
|
||||
_ = json.Unmarshal(req.RawJSON, &rawMap)
|
||||
}
|
||||
}
|
||||
|
||||
isExplicitCommandCode := strings.EqualFold(content.Type, PluginID) ||
|
||||
strings.EqualFold(content.Provider, PluginID) ||
|
||||
content.SessionToken != "" ||
|
||||
content.CommandCodeSession != "" ||
|
||||
strings.Contains(content.Cookie, "__Secure-commandcode_prod_.session_token")
|
||||
|
||||
if !isCommandCodeFile && !isCommandCodeProvider && !isExplicitCommandCode {
|
||||
return AuthParseResponse{Handled: false}, nil
|
||||
}
|
||||
|
||||
// Extract session token
|
||||
token := content.SessionToken
|
||||
if token == "" {
|
||||
token = content.CommandCodeSession
|
||||
}
|
||||
if token == "" && content.Cookie != "" {
|
||||
token = ExtractSessionToken(content.Cookie)
|
||||
}
|
||||
if token == "" && (isCommandCodeFile || isCommandCodeProvider || isExplicitCommandCode) {
|
||||
token = content.Token
|
||||
}
|
||||
token = ExtractSessionToken(token)
|
||||
|
||||
// Determine ID
|
||||
authID := content.ID
|
||||
if authID == "" && req.FileName != "" {
|
||||
base := filepath.Base(req.FileName)
|
||||
authID = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
if authID == "" {
|
||||
authID = "commandcode-default"
|
||||
}
|
||||
|
||||
// Determine Label
|
||||
label := content.Label
|
||||
if label == "" {
|
||||
label = content.Name
|
||||
}
|
||||
if label == "" && content.Email != "" {
|
||||
label = fmt.Sprintf("Command Code (%s)", content.Email)
|
||||
}
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("Command Code (%s)", authID)
|
||||
}
|
||||
|
||||
// Build clean StorageJSON
|
||||
storageMap := map[string]any{
|
||||
"type": PluginID,
|
||||
"provider": PluginID,
|
||||
"session_token": token,
|
||||
}
|
||||
if content.Email != "" {
|
||||
storageMap["email"] = content.Email
|
||||
}
|
||||
if content.Label != "" {
|
||||
storageMap["label"] = content.Label
|
||||
}
|
||||
if content.UpstreamBase != "" {
|
||||
storageMap["api_base"] = content.UpstreamBase
|
||||
}
|
||||
for k, v := range rawMap {
|
||||
if _, exists := storageMap[k]; !exists {
|
||||
storageMap[k] = v
|
||||
}
|
||||
}
|
||||
storageJSON, _ := json.Marshal(storageMap)
|
||||
|
||||
metadata := map[string]any{
|
||||
"type": PluginID,
|
||||
"session_token": token,
|
||||
}
|
||||
if content.Email != "" {
|
||||
metadata["email"] = content.Email
|
||||
}
|
||||
|
||||
attributes := map[string]string{
|
||||
"provider": PluginID,
|
||||
}
|
||||
|
||||
authData := AuthData{
|
||||
Provider: PluginID,
|
||||
ID: authID,
|
||||
FileName: req.FileName,
|
||||
Label: label,
|
||||
Disabled: false,
|
||||
StorageJSON: storageJSON,
|
||||
Metadata: metadata,
|
||||
Attributes: attributes,
|
||||
NextRefreshAfter: time.Now().Add(24 * time.Hour).UTC(),
|
||||
}
|
||||
|
||||
return AuthParseResponse{
|
||||
Handled: true,
|
||||
Auth: authData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -60,107 +59,3 @@ func TestFormatSessionCookie(t *testing.T) {
|
||||
t.Errorf("FormatSessionCookie() from cookie = %q, want %q", gotCookie, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuth_ExplicitJSON(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"type": "commandcode",
|
||||
"session_token": "test-session-token-xyz",
|
||||
"email": "user@example.com",
|
||||
"label": "My Command Code Auth"
|
||||
}`)
|
||||
|
||||
resp, err := ParseAuth(AuthParseRequest{
|
||||
FileName: "custom.json",
|
||||
RawJSON: raw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAuth error: %v", err)
|
||||
}
|
||||
if !resp.Handled {
|
||||
t.Fatal("expected Handled=true for explicit commandcode type")
|
||||
}
|
||||
|
||||
auth := resp.Auth
|
||||
if auth.Provider != PluginID {
|
||||
t.Errorf("Provider = %q, want %q", auth.Provider, PluginID)
|
||||
}
|
||||
if auth.ID != "custom" {
|
||||
t.Errorf("ID = %q, want %q", auth.ID, "custom")
|
||||
}
|
||||
if auth.Label != "My Command Code Auth" {
|
||||
t.Errorf("Label = %q, want %q", auth.Label, "My Command Code Auth")
|
||||
}
|
||||
|
||||
var storage map[string]any
|
||||
if err := json.Unmarshal(auth.StorageJSON, &storage); err != nil {
|
||||
t.Fatalf("failed to unmarshal StorageJSON: %v", err)
|
||||
}
|
||||
if storage["session_token"] != "test-session-token-xyz" {
|
||||
t.Errorf("StorageJSON session_token = %v, want test-session-token-xyz", storage["session_token"])
|
||||
}
|
||||
|
||||
if auth.Metadata["session_token"] != "test-session-token-xyz" {
|
||||
t.Errorf("Metadata session_token = %v, want test-session-token-xyz", auth.Metadata["session_token"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuth_FileNameMatch(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"token": "tok_987654"
|
||||
}`)
|
||||
|
||||
resp, err := ParseAuth(AuthParseRequest{
|
||||
FileName: "commandcode-work.json",
|
||||
RawJSON: raw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAuth error: %v", err)
|
||||
}
|
||||
if !resp.Handled {
|
||||
t.Fatal("expected Handled=true for commandcode-*.json filename")
|
||||
}
|
||||
if resp.Auth.ID != "commandcode-work" {
|
||||
t.Errorf("ID = %q, want commandcode-work", resp.Auth.ID)
|
||||
}
|
||||
if resp.Auth.Metadata["session_token"] != "tok_987654" {
|
||||
t.Errorf("session_token = %v, want tok_987654", resp.Auth.Metadata["session_token"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuth_CookieFormat(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"cookie": "__Secure-commandcode_prod_.session_token=cookie_tok_456; Path=/"
|
||||
}`)
|
||||
|
||||
resp, err := ParseAuth(AuthParseRequest{
|
||||
FileName: "any.json",
|
||||
RawJSON: raw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAuth error: %v", err)
|
||||
}
|
||||
if !resp.Handled {
|
||||
t.Fatal("expected Handled=true for cookie with __Secure-commandcode_prod_.session_token")
|
||||
}
|
||||
if resp.Auth.Metadata["session_token"] != "cookie_tok_456" {
|
||||
t.Errorf("session_token = %v, want cookie_tok_456", resp.Auth.Metadata["session_token"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAuth_UnrelatedFile(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"type": "openai",
|
||||
"api_key": "sk-123456"
|
||||
}`)
|
||||
|
||||
resp, err := ParseAuth(AuthParseRequest{
|
||||
FileName: "openai-test.json",
|
||||
RawJSON: raw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAuth error: %v", err)
|
||||
}
|
||||
if resp.Handled {
|
||||
t.Fatal("expected Handled=false for unrelated credential file")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-75
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -14,7 +13,7 @@ import (
|
||||
const (
|
||||
PluginID = "commandcode"
|
||||
PluginName = "commandcode"
|
||||
PluginVersion = "0.1.0"
|
||||
PluginVersion = "0.2.2"
|
||||
PluginAuthor = "zgs225"
|
||||
PluginRepo = "https://github.com/zgs225/cliproxy-plugin-commandcode"
|
||||
PluginLogo = "https://raw.githubusercontent.com/zgs225/cliproxy-plugin-commandcode/main/assets/logo.svg"
|
||||
@@ -113,17 +112,6 @@ func (p *Plugin) HandleMethod(method string, requestBytes []byte) ([]byte, error
|
||||
case "plugin.quiesce", "plugin.shutdown":
|
||||
return OkEnvelope(map[string]any{"shutdown": true})
|
||||
|
||||
case "auth.identifier":
|
||||
return OkEnvelope(IdentifierResponse{Identifier: PluginID})
|
||||
case "auth.parse":
|
||||
return p.handleAuthParse(requestBytes)
|
||||
case "auth.login.start":
|
||||
return p.handleAuthLoginStart()
|
||||
case "auth.login.poll":
|
||||
return p.handleAuthLoginPoll()
|
||||
case "auth.refresh":
|
||||
return p.handleAuthRefresh(requestBytes)
|
||||
|
||||
case "management.register":
|
||||
return p.handleManagementRegister()
|
||||
case "management.handle":
|
||||
@@ -163,7 +151,6 @@ func (p *Plugin) handleRegister(raw []byte) ([]byte, error) {
|
||||
},
|
||||
},
|
||||
Capabilities: RegistrationCapability{
|
||||
AuthProvider: true,
|
||||
ManagementAPI: true,
|
||||
},
|
||||
})
|
||||
@@ -179,67 +166,6 @@ func (p *Plugin) handleReconfigure(raw []byte) ([]byte, error) {
|
||||
return p.handleRegister(raw)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthParse(raw []byte) ([]byte, error) {
|
||||
var req AuthParseRequest
|
||||
if len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, &req); err != nil {
|
||||
return ErrorEnvelope("invalid_request", "failed to parse AuthParseRequest: "+err.Error()), nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := ParseAuth(req)
|
||||
if err != nil {
|
||||
return ErrorEnvelope("auth_parse_error", err.Error()), nil
|
||||
}
|
||||
|
||||
// Cache token in config if config doesn't have one yet
|
||||
if resp.Handled && resp.Auth.Metadata != nil {
|
||||
if tok, ok := resp.Auth.Metadata["session_token"].(string); ok && tok != "" {
|
||||
if p.config.GetSessionToken() == "" {
|
||||
p.config.SetSessionToken(tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OkEnvelope(resp)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthLoginStart() ([]byte, error) {
|
||||
return OkEnvelope(map[string]any{
|
||||
"Provider": PluginID,
|
||||
"URL": "https://commandcode.ai",
|
||||
"State": "manual",
|
||||
"ExpiresAt": time.Now().Add(5 * time.Minute).UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthLoginPoll() ([]byte, error) {
|
||||
return OkEnvelope(map[string]any{
|
||||
"Status": "error",
|
||||
"Message": "Command Code interactive login is not supported; please configure session_token or provide a commandcode-*.json credential file",
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAuthRefresh(raw []byte) ([]byte, error) {
|
||||
var req AuthRefreshRequest
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &req)
|
||||
}
|
||||
|
||||
authData := AuthData{
|
||||
Provider: PluginID,
|
||||
ID: req.AuthID,
|
||||
StorageJSON: req.StorageJSON,
|
||||
Metadata: req.Metadata,
|
||||
Attributes: req.Attributes,
|
||||
NextRefreshAfter: time.Now().Add(24 * time.Hour).UTC(),
|
||||
}
|
||||
return OkEnvelope(AuthRefreshResponse{
|
||||
Auth: authData,
|
||||
NextRefreshAfter: authData.NextRefreshAfter,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) handleManagementRegister() ([]byte, error) {
|
||||
resp, err := RegisterManagement()
|
||||
if err != nil {
|
||||
|
||||
+8
-10
@@ -39,8 +39,8 @@ api_base: "https://custom-api.commandcode.ai"
|
||||
if reg.Metadata.Version != PluginVersion {
|
||||
t.Errorf("Metadata.Version = %q, want %q", reg.Metadata.Version, PluginVersion)
|
||||
}
|
||||
if !reg.Capabilities.AuthProvider {
|
||||
t.Errorf("Capabilities.AuthProvider = false, want true")
|
||||
if reg.Capabilities.AuthProvider {
|
||||
t.Errorf("Capabilities.AuthProvider = true, want false")
|
||||
}
|
||||
if !reg.Capabilities.ManagementAPI {
|
||||
t.Errorf("Capabilities.ManagementAPI = false, want true")
|
||||
@@ -84,7 +84,7 @@ session_token: "new-token-abc"
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginAuthIdentifier(t *testing.T) {
|
||||
func TestPluginAuthIdentifier_NotHandled(t *testing.T) {
|
||||
p := NewPlugin()
|
||||
raw, err := p.HandleMethod("auth.identifier", nil)
|
||||
if err != nil {
|
||||
@@ -92,16 +92,14 @@ func TestPluginAuthIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
var env Envelope
|
||||
if err := json.Unmarshal(raw, &env); err != nil || !env.OK {
|
||||
if err := json.Unmarshal(raw, &env); err != nil {
|
||||
t.Fatalf("envelope error: %+v", env)
|
||||
}
|
||||
|
||||
var idResp IdentifierResponse
|
||||
if err := json.Unmarshal(env.Result, &idResp); err != nil {
|
||||
t.Fatalf("unmarshal idResp error: %v", err)
|
||||
if env.OK {
|
||||
t.Fatal("expected env.OK=false for auth.identifier")
|
||||
}
|
||||
if idResp.Identifier != PluginID {
|
||||
t.Errorf("Identifier = %q, want %q", idResp.Identifier, PluginID)
|
||||
if env.Error == nil || env.Error.Code != "unknown_method" {
|
||||
t.Errorf("Error = %+v, want code=unknown_method", env.Error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-16
@@ -663,7 +663,7 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
<div>
|
||||
<div class="brand-title">
|
||||
Command Code 配额
|
||||
<span class="version-tag">v0.2.0</span>
|
||||
<span class="version-tag">v0.2.2</span>
|
||||
<span id="planBadge" class="plan-tag" style="display:none;">Plan: -</span>
|
||||
</div>
|
||||
<div class="brand-subtitle">CLIProxyAPI 实时限额与 Credits 用量监控</div>
|
||||
@@ -932,9 +932,15 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
alertBox.className = "alert";
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined || isNaN(num)) return "0";
|
||||
return Number(num).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
||||
// Command Code 额度按美元(USD)计价:金额统一加 $ 前缀,按美元格式输出(千分位 + 两位小数)
|
||||
function formatUSD(num) {
|
||||
if (num === null || num === undefined || isNaN(num)) return "$0.00";
|
||||
return Number(num).toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function formatCountdown(targetDate) {
|
||||
@@ -987,17 +993,17 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
}
|
||||
|
||||
// Credits
|
||||
valMonthlyCredits.textContent = formatNumber(credits.monthly_credits);
|
||||
valOpensourceCredits.textContent = formatNumber(credits.opensource_monthly_credits);
|
||||
valTotalCredits.textContent = formatNumber(credits.total_credits);
|
||||
valMonthlyCredits.textContent = formatUSD(credits.monthly_credits);
|
||||
valOpensourceCredits.textContent = formatUSD(credits.opensource_monthly_credits);
|
||||
valTotalCredits.textContent = formatUSD(credits.total_credits);
|
||||
|
||||
// Monthly (billing period)
|
||||
const monthly = limits.monthly || {};
|
||||
const pMonth = Math.min(100, Math.max(0, monthly.percentage || 0));
|
||||
badgeMonthly.textContent = pMonth.toFixed(1) + "%";
|
||||
usedMonthly.textContent = formatNumber(monthly.used);
|
||||
capMonthly.textContent = "/ " + formatNumber(monthly.cap);
|
||||
remainMonthly.textContent = formatNumber(monthly.remaining);
|
||||
usedMonthly.textContent = formatUSD(monthly.used);
|
||||
capMonthly.textContent = "/ " + formatUSD(monthly.cap);
|
||||
remainMonthly.textContent = formatUSD(monthly.remaining);
|
||||
barMonthly.style.width = pMonth + "%";
|
||||
|
||||
barMonthly.className = "progress-bar" + (pMonth >= 90 || monthly.exceeded ? " danger" : pMonth >= 70 ? " warning" : "");
|
||||
@@ -1009,9 +1015,9 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
const fiveHour = limits.five_hour || {};
|
||||
const pFive = Math.min(100, Math.max(0, fiveHour.percentage || 0));
|
||||
badgeFiveHour.textContent = pFive.toFixed(1) + "%";
|
||||
usedFiveHour.textContent = formatNumber(fiveHour.used);
|
||||
capFiveHour.textContent = "/ " + formatNumber(fiveHour.cap);
|
||||
remainFiveHour.textContent = formatNumber(fiveHour.remaining);
|
||||
usedFiveHour.textContent = formatUSD(fiveHour.used);
|
||||
capFiveHour.textContent = "/ " + formatUSD(fiveHour.cap);
|
||||
remainFiveHour.textContent = formatUSD(fiveHour.remaining);
|
||||
barFiveHour.style.width = pFive + "%";
|
||||
|
||||
barFiveHour.className = "progress-bar" + (pFive >= 90 || fiveHour.exceeded ? " danger" : pFive >= 70 ? " warning" : "");
|
||||
@@ -1029,9 +1035,9 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
||||
const weekly = limits.weekly || {};
|
||||
const pWeek = Math.min(100, Math.max(0, weekly.percentage || 0));
|
||||
badgeWeekly.textContent = pWeek.toFixed(1) + "%";
|
||||
usedWeekly.textContent = formatNumber(weekly.used);
|
||||
capWeekly.textContent = "/ " + formatNumber(weekly.cap);
|
||||
remainWeekly.textContent = formatNumber(weekly.remaining);
|
||||
usedWeekly.textContent = formatUSD(weekly.used);
|
||||
capWeekly.textContent = "/ " + formatUSD(weekly.cap);
|
||||
remainWeekly.textContent = formatUSD(weekly.remaining);
|
||||
barWeekly.style.width = pWeek + "%";
|
||||
|
||||
barWeekly.className = "progress-bar" + (pWeek >= 90 || weekly.exceeded ? " danger" : pWeek >= 70 ? " warning" : "");
|
||||
|
||||
+2
-53
@@ -55,59 +55,8 @@ type ConfigField struct {
|
||||
|
||||
// RegistrationCapability declares the capabilities implemented by this plugin.
|
||||
type RegistrationCapability struct {
|
||||
AuthProvider bool `json:"auth_provider"`
|
||||
ManagementAPI bool `json:"management_api"`
|
||||
}
|
||||
|
||||
// IdentifierResponse is returned by auth.identifier.
|
||||
type IdentifierResponse struct {
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
|
||||
// AuthData describes a credential record.
|
||||
type AuthData struct {
|
||||
Provider string `json:"Provider"`
|
||||
ID string `json:"ID"`
|
||||
FileName string `json:"FileName"`
|
||||
Label string `json:"Label"`
|
||||
Prefix string `json:"Prefix,omitempty"`
|
||||
ProxyURL string `json:"ProxyURL,omitempty"`
|
||||
Disabled bool `json:"Disabled,omitempty"`
|
||||
StorageJSON []byte `json:"StorageJSON"`
|
||||
Metadata map[string]any `json:"Metadata,omitempty"`
|
||||
Attributes map[string]string `json:"Attributes,omitempty"`
|
||||
NextRefreshAfter time.Time `json:"NextRefreshAfter,omitempty"`
|
||||
}
|
||||
|
||||
// AuthParseRequest is passed to auth.parse.
|
||||
type AuthParseRequest struct {
|
||||
Provider string `json:"Provider"`
|
||||
Path string `json:"Path"`
|
||||
FileName string `json:"FileName"`
|
||||
RawJSON []byte `json:"RawJSON"`
|
||||
Host map[string]any `json:"Host,omitempty"`
|
||||
}
|
||||
|
||||
// AuthParseResponse is returned by auth.parse.
|
||||
type AuthParseResponse struct {
|
||||
Handled bool `json:"Handled"`
|
||||
Auth AuthData `json:"Auth"`
|
||||
Auths []AuthData `json:"Auths,omitempty"`
|
||||
}
|
||||
|
||||
// AuthRefreshRequest is passed to auth.refresh.
|
||||
type AuthRefreshRequest struct {
|
||||
AuthID string `json:"AuthID"`
|
||||
AuthProvider string `json:"AuthProvider"`
|
||||
StorageJSON []byte `json:"StorageJSON"`
|
||||
Metadata map[string]any `json:"Metadata,omitempty"`
|
||||
Attributes map[string]string `json:"Attributes,omitempty"`
|
||||
}
|
||||
|
||||
// AuthRefreshResponse is returned by auth.refresh.
|
||||
type AuthRefreshResponse struct {
|
||||
Auth AuthData `json:"Auth"`
|
||||
NextRefreshAfter time.Time `json:"NextRefreshAfter,omitempty"`
|
||||
AuthProvider bool `json:"auth_provider,omitempty"`
|
||||
ManagementAPI bool `json:"management_api,omitempty"`
|
||||
}
|
||||
|
||||
// ManagementRegistrationResponse is returned by management.register.
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ func fetchUpstream(ctx context.Context, apiBase, endpoint, sessionToken, hostCal
|
||||
Headers: map[string][]string{
|
||||
"Cookie": {cookieValue},
|
||||
"Accept": {"application/json"},
|
||||
"User-Agent": {"cliproxy-plugin-commandcode/0.1.0"},
|
||||
"User-Agent": {fmt.Sprintf("cliproxy-plugin-commandcode/%s", PluginVersion)},
|
||||
},
|
||||
HostCallbackID: hostCallbackID,
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func fetchUpstream(ctx context.Context, apiBase, endpoint, sessionToken, hostCal
|
||||
}
|
||||
httpReq.Header.Set("Cookie", cookieValue)
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header.Set("User-Agent", "cliproxy-plugin-commandcode/0.1.0")
|
||||
httpReq.Header.Set("User-Agent", fmt.Sprintf("cliproxy-plugin-commandcode/%s", PluginVersion))
|
||||
|
||||
res, errDo := defaultHTTPClient.Do(httpReq)
|
||||
if errDo != nil {
|
||||
|
||||
Reference in New Issue
Block a user