Files
cliproxy-plugin-commandcode/plugin/quota_page.go
T
yuez 61c50280b1 feat: add OpenCode Go usage query with aggregated /all endpoint, bump v0.3.0
- New provider: OpenCode Go (GET https://opencode.ai/zen/go/v1/usage, Bearer auth)
- Extract shared transport doUpstreamRequest (host.http.do first, net/http fallback)
- New management routes: GET/POST /plugins/commandcode/opencode/usage, GET/POST /plugins/commandcode/all
- /all aggregates both providers sequentially with partial-failure semantics
  (>=1 success -> 200, all-local-missing -> 400, all-upstream-failure -> 502)
- QuotaCard UI: tabs (Command Code / OpenCode Go / All), OpenCode window cards,
  version badge v0.3.0, OpenCode API key test override in settings drawer
- Config: opencode_api_key / opencode_api_base (ConfigFields 2 -> 4)
- Tests: route-order regression, /all partial failure & misclassification guards,
  ParseOpenCodeUsage edge cases, host/http transport paths
2026-09-18 09:38:40 +08:00

1860 lines
63 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import (
"encoding/base64"
)
// QuotaPageHTML contains the full standalone HTML/CSS/JS single-file QuotaCard UI.
const QuotaPageHTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>用量配额 - Command Code + OpenCode Go - CLIProxyAPI</title>
<style>
:root {
--bg-page: #f8fafc;
--bg-card: #ffffff;
--bg-subtle: #f1f5f9;
--border-color: #e2e8f0;
--text-main: #0f172a;
--text-muted: #64748b;
--text-dim: #94a3b8;
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-subtle: #eff6ff;
--success: #10b981;
--success-subtle: #ecfdf5;
--warning: #f59e0b;
--warning-subtle: #fffbeb;
--danger: #ef4444;
--danger-subtle: #fef2f2;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
}
/* Auto mode fallback: when no explicit data-theme is set, follow OS dark preference */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]):not([data-theme="white"]) {
--bg-page: #0b0f19;
--bg-card: #151d30;
--bg-subtle: #1e293b;
--border-color: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--text-dim: #64748b;
--primary: #60a5fa;
--primary-hover: #3b82f6;
--primary-subtle: rgba(59, 130, 246, 0.12);
--success: #34d399;
--success-subtle: rgba(16, 185, 129, 0.12);
--warning: #fbbf24;
--warning-subtle: rgba(245, 158, 11, 0.12);
--danger: #f87171;
--danger-subtle: rgba(239, 68, 68, 0.12);
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
}
}
/* Explicit dark theme from Management Center */
:root[data-theme="dark"] {
--bg-page: #0b0f19;
--bg-card: #151d30;
--bg-subtle: #1e293b;
--border-color: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--text-dim: #64748b;
--primary: #60a5fa;
--primary-hover: #3b82f6;
--primary-subtle: rgba(59, 130, 246, 0.12);
--success: #34d399;
--success-subtle: rgba(16, 185, 129, 0.12);
--warning: #fbbf24;
--warning-subtle: rgba(245, 158, 11, 0.12);
--danger: #f87171;
--danger-subtle: rgba(239, 68, 68, 0.12);
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
}
/* Explicit white / light theme from Management Center: overrides OS dark preference */
:root[data-theme="white"],
:root[data-theme="light"] {
--bg-page: #f8fafc;
--bg-card: #ffffff;
--bg-subtle: #f1f5f9;
--border-color: #e2e8f0;
--text-main: #0f172a;
--text-muted: #64748b;
--text-dim: #94a3b8;
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-subtle: #eff6ff;
--success: #10b981;
--success-subtle: #ecfdf5;
--warning: #f59e0b;
--warning-subtle: #fffbeb;
--danger: #ef4444;
--danger-subtle: #fef2f2;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-page);
color: var(--text-main);
min-height: 100vh;
padding: 24px 16px;
display: flex;
justify-content: center;
align-items: flex-start;
line-height: 1.5;
}
.container {
width: 100%;
max-width: 860px;
margin: 0 auto;
}
.header-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px 24px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.brand-section {
display: flex;
align-items: center;
gap: 14px;
}
.brand-icon {
width: 44px;
height: 44px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
color: white;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
}
.brand-icon svg {
width: 26px;
height: 26px;
}
.brand-title {
font-size: 20px;
font-weight: 700;
color: var(--text-main);
display: flex;
align-items: center;
gap: 8px;
}
.version-tag {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 9999px;
background: var(--primary-subtle);
color: var(--primary);
border: 1px solid var(--primary);
}
.plan-tag {
font-size: 11px;
font-weight: 700;
padding: 2px 10px;
border-radius: 9999px;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.15));
color: #8b5cf6;
border: 1px solid rgba(139, 92, 246, 0.35);
display: inline-flex;
align-items: center;
gap: 4px;
letter-spacing: 0.3px;
}
:root[data-theme="dark"] .plan-tag {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.2), rgba(96, 165, 250, 0.2));
color: #c084fc;
border-color: rgba(167, 139, 250, 0.45);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]):not([data-theme="white"]) .plan-tag {
background: linear-gradient(135deg, rgba(167, 139, 250, 0.2), rgba(96, 165, 250, 0.2));
color: #c084fc;
border-color: rgba(167, 139, 250, 0.45);
}
}
.brand-subtitle {
font-size: 13px;
color: var(--text-muted);
}
.action-group {
display: flex;
align-items: center;
gap: 10px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
border-radius: 9999px;
background: var(--bg-subtle);
color: var(--text-muted);
border: 1px solid var(--border-color);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-dim);
}
.status-badge.online .status-dot { background: var(--success); }
.status-badge.online { background: var(--success-subtle); color: var(--success); border-color: rgba(16, 185, 129, 0.3); }
.status-badge.warning .status-dot { background: var(--warning); }
.status-badge.warning { background: var(--warning-subtle); color: var(--warning); border-color: rgba(245, 158, 11, 0.3); }
.status-badge.exceeded .status-dot { background: var(--danger); animation: pulse 1.5s infinite; }
.status-badge.exceeded { background: var(--danger-subtle); color: var(--danger); border-color: rgba(239, 68, 68, 0.3); }
@keyframes pulse {
0% { transform: scale(0.95); opacity: 0.8; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.8; }
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
padding: 8px 14px;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--border-color);
background: var(--bg-card);
color: var(--text-main);
outline: none;
}
.btn:hover {
background: var(--bg-subtle);
border-color: var(--primary);
}
.btn-primary {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.btn-primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn svg {
width: 15px;
height: 15px;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Error / Warning Alert Banner */
.alert {
display: none;
padding: 14px 18px;
border-radius: var(--radius-md);
margin-bottom: 20px;
font-size: 13px;
align-items: center;
gap: 10px;
border: 1px solid transparent;
}
.alert.show {
display: flex;
}
.alert-danger {
background: var(--danger-subtle);
color: var(--danger);
border-color: rgba(239, 68, 68, 0.3);
}
.alert-warning {
background: var(--warning-subtle);
color: var(--warning);
border-color: rgba(245, 158, 11, 0.3);
}
/* Drawer / Settings */
.settings-drawer {
display: none;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.settings-drawer.open {
display: block;
}
.drawer-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
@media (max-width: 640px) {
.form-grid {
grid-template-columns: 1fr;
}
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
}
.form-control {
background: var(--bg-subtle);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 13px;
color: var(--text-main);
outline: none;
font-family: monospace;
}
.form-control:focus {
border-color: var(--primary);
}
/* Metrics Overview Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 700px) {
.metrics-grid {
grid-template-columns: 1fr;
}
}
.metric-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.metric-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.metric-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.metric-title {
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
}
.metric-icon {
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
background: var(--bg-subtle);
color: var(--text-muted);
}
.metric-value {
font-size: 28px;
font-weight: 800;
color: var(--text-main);
font-feature-settings: "tnum";
}
.metric-desc {
font-size: 12px;
color: var(--text-dim);
}
/* Window Limit Quota Cards */
.quota-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
}
@media (max-width: 768px) {
.quota-section {
grid-template-columns: 1fr;
}
}
.quota-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 18px;
position: relative;
overflow: hidden;
}
.quota-card.is-exceeded {
border-color: rgba(239, 68, 68, 0.4);
}
.quota-card.is-exceeded::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--danger);
}
.quota-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.quota-tag {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 3px 8px;
border-radius: 4px;
background: var(--primary-subtle);
color: var(--primary);
}
.quota-name {
font-size: 18px;
font-weight: 700;
color: var(--text-main);
margin-top: 4px;
}
.quota-percent-badge {
font-size: 15px;
font-weight: 800;
padding: 4px 10px;
border-radius: var(--radius-md);
background: var(--bg-subtle);
color: var(--text-main);
}
.quota-stats-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 13px;
color: var(--text-muted);
}
.quota-usage-num {
font-size: 24px;
font-weight: 800;
color: var(--text-main);
font-feature-settings: "tnum";
}
.quota-cap-num {
font-size: 14px;
color: var(--text-muted);
font-weight: 500;
}
/* Progress bar */
.progress-track {
width: 100%;
height: 12px;
background: var(--bg-subtle);
border-radius: 9999px;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 100%;
border-radius: 9999px;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
width: 0%;
background: linear-gradient(90deg, #10b981, #06b6d4);
}
.progress-bar.warning {
background: linear-gradient(90deg, #f59e0b, #fbbf24);
}
.progress-bar.danger {
background: linear-gradient(90deg, #ef4444, #f87171);
}
/* Reset countdown */
.reset-box {
background: var(--bg-subtle);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.reset-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
font-weight: 600;
}
.reset-label svg {
width: 14px;
height: 14px;
}
.countdown-timer {
font-size: 13px;
font-weight: 700;
color: var(--text-main);
font-family: monospace;
letter-spacing: 0.5px;
}
/* Tab Bar (Command Code | OpenCode Go | All) */
.tab-bar {
display: flex;
gap: 6px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 6px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.tab-btn {
flex: 1;
border: none;
background: transparent;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
padding: 8px 12px;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.tab-btn:hover {
color: var(--text-main);
background: var(--bg-subtle);
}
.tab-btn.active {
background: var(--primary);
color: #fff;
}
.tab-section {
display: none;
}
.tab-section.active {
display: block;
}
/* OpenCode card status chip (reuses .status-badge variants) */
.oc-status-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
/* All tab: two provider cards side by side */
.all-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
}
@media (max-width: 768px) {
.all-grid {
grid-template-columns: 1fr;
}
}
.all-provider-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 12px;
}
.all-provider-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.all-provider-name {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
}
.all-provider-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.all-summary-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
font-size: 13px;
color: var(--text-muted);
}
.all-summary-value {
font-weight: 700;
color: var(--text-main);
font-feature-settings: "tnum";
}
.all-grid .progress-track {
height: 8px;
}
/* Per-tab provider failure error cards */
.error-card {
display: none;
background: var(--danger-subtle);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 20px;
flex-direction: column;
gap: 8px;
}
.error-card.show {
display: flex;
}
.error-card-title {
color: var(--danger);
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.error-card-msg {
color: var(--danger);
font-size: 13px;
word-break: break-all;
}
.form-hint {
font-size: 11px;
color: var(--text-dim);
}
/* Footer */
.footer-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 12px 8px;
font-size: 12px;
color: var(--text-dim);
}
.footer-links {
display: flex;
gap: 16px;
}
.footer-links a {
color: var(--text-muted);
text-decoration: none;
}
.footer-links a:hover {
color: var(--primary);
}
</style>
</head>
<body>
<div class="container">
<!-- Header Card -->
<div class="header-card">
<div class="brand-section">
<div class="brand-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</div>
<div>
<div class="brand-title">
用量配额 - Command Code + OpenCode Go
<span class="version-tag">v0.3.0</span>
<span id="planBadge" class="plan-tag" style="display:none;">Plan: -</span>
</div>
<div class="brand-subtitle">CLIProxyAPI 实时限额与用量监控 (Command Code + OpenCode Go)</div>
</div>
</div>
<div class="action-group">
<div id="statusBadge" class="status-badge">
<span class="status-dot"></span>
<span id="statusText">正在检查...</span>
</div>
<button id="btnSettings" class="btn" title="配置选项">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<button id="btnRefresh" class="btn btn-primary">
<svg id="refreshIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
刷新
</button>
</div>
</div>
<!-- Tab Bar -->
<div id="tabBar" class="tab-bar">
<button type="button" class="tab-btn active" data-tab="commandcode">Command Code</button>
<button type="button" class="tab-btn" data-tab="opencode">OpenCode Go</button>
<button type="button" class="tab-btn" data-tab="all">All</button>
</div>
<!-- Alert Message -->
<div id="alertBox" class="alert alert-danger">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<span id="alertMsg">加载失败</span>
</div>
<!-- Settings Drawer -->
<div id="settingsDrawer" class="settings-drawer">
<div class="drawer-title">
<span>诊断与访问配置</span>
<button id="btnCloseSettings" class="btn" style="padding:4px 8px;">关闭</button>
</div>
<div class="form-grid">
<div class="form-group">
<label for="inputMgmtKey">CLIProxyAPI 管理密钥 (Management Key)</label>
<input type="password" id="inputMgmtKey" class="form-control" placeholder="留空则自动从浏览器 localStorage 探测" />
</div>
<div class="form-group">
<label for="inputSessionToken">Command Code 会话 Token (Session Token 测试)</label>
<input type="password" id="inputSessionToken" class="form-control" placeholder="覆盖测试: __Secure-commandcode_prod_.session_token" />
</div>
<div class="form-group">
<label for="inputOpenCodeKey">OpenCode Go API Key (测试覆盖)</label>
<input type="password" id="inputOpenCodeKey" class="form-control" placeholder="覆盖测试: sk-..." />
<span class="form-hint">仅测试覆盖用:值不持久化,仅当次请求生效</span>
</div>
</div>
<div style="margin-top: 14px; display: flex; justify-content: flex-end; gap: 10px;">
<button id="btnSaveConfig" class="btn btn-primary">保存并重新获取用量</button>
</div>
</div>
<!-- Tab: Command Code -->
<div id="sectionCommandcode" class="tab-section active">
<div id="ccErrorCard" class="error-card">
<div class="error-card-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<span>Command Code 查询失败</span>
</div>
<div id="ccErrorMsg" class="error-card-msg">-</div>
</div>
<div id="ccContent">
<!-- Overview Metrics -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">月度额度 (Monthly Credits)</span>
<div class="metric-icon">💰</div>
</div>
<div id="valMonthlyCredits" class="metric-value">-</div>
<div class="metric-desc">当前周期 Command Code 基础配额</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">开源奖励额度 (Open Source)</span>
<div class="metric-icon">🎁</div>
</div>
<div id="valOpensourceCredits" class="metric-value">-</div>
<div class="metric-desc">开源贡献者赠送与奖励额度</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">可用额度总计 (Total Credits)</span>
<div class="metric-icon">✨</div>
</div>
<div id="valTotalCredits" class="metric-value">-</div>
<div class="metric-desc">月度额度与奖励额度汇总</div>
</div>
</div>
<!-- Double Window Limits -->
<div class="quota-section">
<!-- Monthly Window -->
<div id="cardMonthly" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag" style="color:#f59e0b; background:rgba(245,158,11,0.12)">账单周期</span>
<div class="quota-name">月度额度 (Monthly Window)</div>
</div>
<div id="badgeMonthly" class="quota-percent-badge">- %</div>
</div>
<div class="quota-stats-row">
<div>
<span id="usedMonthly" class="quota-usage-num">-</span>
<span id="capMonthly" class="quota-cap-num">/ -</span>
</div>
<div>剩余可用: <strong id="remainMonthly">-</strong></div>
</div>
<div class="progress-track">
<div id="barMonthly" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>账单周期重置</span>
</div>
<div id="timerMonthly" class="countdown-timer">--:--:--</div>
</div>
</div>
<!-- 5-Hour Window -->
<div id="cardFiveHour" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag">短期滑动窗口</span>
<div class="quota-name">5 小时限制 (5-Hour Window)</div>
</div>
<div id="badgeFiveHour" class="quota-percent-badge">- %</div>
</div>
<div class="quota-stats-row">
<div>
<span id="usedFiveHour" class="quota-usage-num">-</span>
<span id="capFiveHour" class="quota-cap-num">/ -</span>
</div>
<div>剩余可用: <strong id="remainFiveHour">-</strong></div>
</div>
<div class="progress-track">
<div id="barFiveHour" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>重置倒计时</span>
</div>
<div id="timerFiveHour" class="countdown-timer">--:--:--</div>
</div>
</div>
<!-- Weekly Window -->
<div id="cardWeekly" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag" style="color:#8b5cf6; background:rgba(139,92,246,0.12)">周度窗口</span>
<div class="quota-name">每周限制 (Weekly Window)</div>
</div>
<div id="badgeWeekly" class="quota-percent-badge">- %</div>
</div>
<div class="quota-stats-row">
<div>
<span id="usedWeekly" class="quota-usage-num">-</span>
<span id="capWeekly" class="quota-cap-num">/ -</span>
</div>
<div>剩余可用: <strong id="remainWeekly">-</strong></div>
</div>
<div class="progress-track">
<div id="barWeekly" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>重置倒计时</span>
</div>
<div id="timerWeekly" class="countdown-timer">--:--:--</div>
</div>
</div>
</div>
</div>
</div>
<!-- /Tab: Command Code -->
<!-- Tab: OpenCode Go -->
<div id="sectionOpencode" class="tab-section">
<div id="ocErrorCard" class="error-card">
<div class="error-card-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<span>OpenCode Go 查询失败</span>
</div>
<div id="ocErrorMsg" class="error-card-msg">-</div>
</div>
<div id="ocContent">
<div class="metrics-grid">
<!-- Rolling 5h Window -->
<div id="ocCardRolling" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag">短期滑动窗口</span>
<div class="quota-name">Rolling 5h (5 小时窗口)</div>
</div>
<div class="oc-status-wrap">
<span id="ocStatusRolling" class="status-badge online"><span class="status-dot"></span><span id="ocStatusTextRolling">-</span></span>
<div id="ocBadgeRolling" class="quota-percent-badge">- %</div>
</div>
</div>
<div class="progress-track">
<div id="ocBarRolling" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>重置倒计时</span>
</div>
<div id="ocTimerRolling" class="countdown-timer">-</div>
</div>
</div>
<!-- Weekly Window -->
<div id="ocCardWeekly" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag" style="color:#8b5cf6; background:rgba(139,92,246,0.12)">周度窗口</span>
<div class="quota-name">Weekly (每周限制)</div>
</div>
<div class="oc-status-wrap">
<span id="ocStatusWeekly" class="status-badge online"><span class="status-dot"></span><span id="ocStatusTextWeekly">-</span></span>
<div id="ocBadgeWeekly" class="quota-percent-badge">- %</div>
</div>
</div>
<div class="progress-track">
<div id="ocBarWeekly" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>重置倒计时</span>
</div>
<div id="ocTimerWeekly" class="countdown-timer">-</div>
</div>
</div>
<!-- Monthly Window -->
<div id="ocCardMonthly" class="quota-card">
<div class="quota-card-header">
<div>
<span class="quota-tag" style="color:#f59e0b; background:rgba(245,158,11,0.12)">月度窗口</span>
<div class="quota-name">Monthly (月度窗口)</div>
</div>
<div class="oc-status-wrap">
<span id="ocStatusMonthly" class="status-badge online"><span class="status-dot"></span><span id="ocStatusTextMonthly">-</span></span>
<div id="ocBadgeMonthly" class="quota-percent-badge">- %</div>
</div>
</div>
<div class="progress-track">
<div id="ocBarMonthly" class="progress-bar"></div>
</div>
<div class="reset-box">
<div class="reset-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
<span>重置倒计时</span>
</div>
<div id="ocTimerMonthly" class="countdown-timer">-</div>
</div>
</div>
</div>
</div>
</div>
<!-- /Tab: OpenCode Go -->
<!-- Tab: All -->
<div id="sectionAll" class="tab-section">
<div class="all-grid">
<div class="all-provider-card">
<div class="all-provider-head">
<span class="all-provider-name">Command Code</span>
<span id="allBadgeCommandcode" class="status-badge"><span class="status-dot"></span><span id="allBadgeTextCommandcode">-</span></span>
</div>
<div id="allBodyCommandcode" class="all-provider-body">尚未加载</div>
</div>
<div class="all-provider-card">
<div class="all-provider-head">
<span class="all-provider-name">OpenCode Go</span>
<span id="allBadgeOpencode" class="status-badge"><span class="status-dot"></span><span id="allBadgeTextOpencode">-</span></span>
</div>
<div id="allBodyOpencode" class="all-provider-body">尚未加载</div>
</div>
</div>
</div>
<!-- /Tab: All -->
<!-- Footer Status -->
<div class="footer-bar">
<div>最后同步时间: <span id="lastUpdated">-</span></div>
<div class="footer-links">
<span id="authInfo">Provider: commandcode + opencode_go</span>
</div>
</div>
</div>
<script>
(function () {
const ALL_ENDPOINT = "/v0/management/plugins/commandcode/all";
// Elements
const btnRefresh = document.getElementById("btnRefresh");
const refreshIcon = document.getElementById("refreshIcon");
const btnSettings = document.getElementById("btnSettings");
const btnCloseSettings = document.getElementById("btnCloseSettings");
const btnSaveConfig = document.getElementById("btnSaveConfig");
const settingsDrawer = document.getElementById("settingsDrawer");
const inputMgmtKey = document.getElementById("inputMgmtKey");
const inputSessionToken = document.getElementById("inputSessionToken");
const inputOpenCodeKey = document.getElementById("inputOpenCodeKey");
const alertBox = document.getElementById("alertBox");
const alertMsg = document.getElementById("alertMsg");
const statusBadge = document.getElementById("statusBadge");
const statusText = document.getElementById("statusText");
const planBadge = document.getElementById("planBadge");
// Tab sections and per-provider error cards
const ccContent = document.getElementById("ccContent");
const ccErrorCard = document.getElementById("ccErrorCard");
const ccErrorMsg = document.getElementById("ccErrorMsg");
const ocContent = document.getElementById("ocContent");
const ocErrorCard = document.getElementById("ocErrorCard");
const ocErrorMsg = document.getElementById("ocErrorMsg");
// All tab elements
const allBodyCommandcode = document.getElementById("allBodyCommandcode");
const allBadgeCommandcode = document.getElementById("allBadgeCommandcode");
const allBadgeTextCommandcode = document.getElementById("allBadgeTextCommandcode");
const allBodyOpencode = document.getElementById("allBodyOpencode");
const allBadgeOpencode = document.getElementById("allBadgeOpencode");
const allBadgeTextOpencode = document.getElementById("allBadgeTextOpencode");
const valMonthlyCredits = document.getElementById("valMonthlyCredits");
const valOpensourceCredits = document.getElementById("valOpensourceCredits");
const valTotalCredits = document.getElementById("valTotalCredits");
const cardMonthly = document.getElementById("cardMonthly");
const badgeMonthly = document.getElementById("badgeMonthly");
const usedMonthly = document.getElementById("usedMonthly");
const capMonthly = document.getElementById("capMonthly");
const remainMonthly = document.getElementById("remainMonthly");
const barMonthly = document.getElementById("barMonthly");
const timerMonthly = document.getElementById("timerMonthly");
const cardFiveHour = document.getElementById("cardFiveHour");
const badgeFiveHour = document.getElementById("badgeFiveHour");
const usedFiveHour = document.getElementById("usedFiveHour");
const capFiveHour = document.getElementById("capFiveHour");
const remainFiveHour = document.getElementById("remainFiveHour");
const barFiveHour = document.getElementById("barFiveHour");
const timerFiveHour = document.getElementById("timerFiveHour");
const cardWeekly = document.getElementById("cardWeekly");
const badgeWeekly = document.getElementById("badgeWeekly");
const usedWeekly = document.getElementById("usedWeekly");
const capWeekly = document.getElementById("capWeekly");
const remainWeekly = document.getElementById("remainWeekly");
const barWeekly = document.getElementById("barWeekly");
const timerWeekly = document.getElementById("timerWeekly");
const lastUpdated = document.getElementById("lastUpdated");
// OpenCode Go card elements
const ocCardRolling = document.getElementById("ocCardRolling");
const ocBadgeRolling = document.getElementById("ocBadgeRolling");
const ocBarRolling = document.getElementById("ocBarRolling");
const ocTimerRolling = document.getElementById("ocTimerRolling");
const ocStatusRolling = document.getElementById("ocStatusRolling");
const ocStatusTextRolling = document.getElementById("ocStatusTextRolling");
const ocCardWeekly = document.getElementById("ocCardWeekly");
const ocBadgeWeekly = document.getElementById("ocBadgeWeekly");
const ocBarWeekly = document.getElementById("ocBarWeekly");
const ocTimerWeekly = document.getElementById("ocTimerWeekly");
const ocStatusWeekly = document.getElementById("ocStatusWeekly");
const ocStatusTextWeekly = document.getElementById("ocStatusTextWeekly");
const ocCardMonthly = document.getElementById("ocCardMonthly");
const ocBadgeMonthly = document.getElementById("ocBadgeMonthly");
const ocBarMonthly = document.getElementById("ocBarMonthly");
const ocTimerMonthly = document.getElementById("ocTimerMonthly");
const ocStatusMonthly = document.getElementById("ocStatusMonthly");
const ocStatusTextMonthly = document.getElementById("ocStatusTextMonthly");
const ocTimerEls = { rolling: ocTimerRolling, weekly: ocTimerWeekly, monthly: ocTimerMonthly };
let monthlyTargetTime = null;
let fiveHourTargetTime = null;
let weeklyTargetTime = null;
let timerInterval = null;
// Per-provider render state (drives tab badge aggregation)
const providerState = {
commandcode: { level: "unknown", err: null, data: null },
opencode: { level: "unknown", err: null, data: null }
};
let activeTab = "commandcode";
let ocTargets = { rolling: null, weekly: null, monthly: null };
function getStoredManagementKey() {
if (inputMgmtKey.value.trim()) {
return inputMgmtKey.value.trim();
}
const keys = ["management_key", "managementKey", "cpa_management_key", "token"];
for (const k of keys) {
const val = localStorage.getItem(k);
if (val && val.trim()) {
return val.trim();
}
}
return "";
}
function showAlert(message, isWarning = false) {
alertMsg.textContent = message;
alertBox.className = isWarning ? "alert alert-warning show" : "alert alert-danger show";
}
function hideAlert() {
alertBox.className = "alert";
}
// 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) {
if (!targetDate) return "--:--:--";
const now = new Date().getTime();
const target = targetDate.getTime();
const diff = target - now;
if (diff <= 0) return "已就绪 (可重置)";
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (days > 0) {
return days + "天 " + pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
}
return pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
}
function pad(n) {
return n < 10 ? "0" + n : n;
}
function esc(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// Returns a clamped number in [0,100], or null when missing/invalid
function clampPercent(v) {
if (v === null || v === undefined || v === "") return null;
const n = Number(v);
if (!isFinite(n)) return null;
return Math.min(100, Math.max(0, n));
}
// Unknown status values never error: only explicit exceeded flag,
// status "exceeded", or percent >= 100 count as exceeded
function levelOf(pct, exceeded, status) {
if (exceeded || status === "exceeded" || (pct !== null && pct >= 100)) return "exceeded";
if (pct !== null && pct >= 80) return "warning";
return "online";
}
const LEVEL_RANK = { unknown: -1, online: 0, warning: 1, error: 2, exceeded: 3 };
const BADGE_TEXT = {
unknown: "正在检查...",
online: "正常运行 (Normal)",
warning: "配额紧张 (Warning)",
exceeded: "已达限额 (Exceeded)",
error: "查询错误"
};
function badgeVisualClass(level) {
// error reuses the danger/exceeded look
return level === "error" ? "exceeded" : level === "unknown" ? "" : level;
}
function updateGlobalBadge() {
let p;
if (activeTab === "commandcode") {
p = providerState.commandcode;
} else if (activeTab === "opencode") {
p = providerState.opencode;
} else {
const a = providerState.commandcode;
const b = providerState.opencode;
p = LEVEL_RANK[a.level] >= LEVEL_RANK[b.level] ? a : b;
}
const visual = badgeVisualClass(p.level);
statusBadge.className = visual ? "status-badge " + visual : "status-badge";
statusText.textContent = BADGE_TEXT[p.level] || "正在检查...";
}
function setProviderStatus(provider, level) {
providerState[provider].level = level;
providerState[provider].err = null;
updateGlobalBadge();
}
function summaryRow(label, value) {
return '<div class="all-summary-row"><span class="all-summary-label">' + label + '</span><span class="all-summary-value">' + value + '</span></div>';
}
function miniBar(pct, level) {
const cls = level === "exceeded" ? " danger" : level === "warning" ? " warning" : "";
const w = pct === null ? 0 : pct;
return '<div class="progress-track"><div class="progress-bar' + cls + '" style="width:' + w + '%"></div></div>';
}
function showProviderError(provider, msg) {
providerState[provider].level = "error";
providerState[provider].err = msg;
providerState[provider].data = null;
if (provider === "commandcode") {
ccContent.style.display = "none";
ccErrorMsg.textContent = msg;
ccErrorCard.classList.add("show");
} else {
ocContent.style.display = "none";
ocErrorMsg.textContent = msg;
ocErrorCard.classList.add("show");
}
updateGlobalBadge();
}
function updateTimers() {
if (monthlyTargetTime) {
timerMonthly.textContent = formatCountdown(monthlyTargetTime);
}
if (fiveHourTargetTime) {
timerFiveHour.textContent = formatCountdown(fiveHourTargetTime);
}
if (weeklyTargetTime) {
timerWeekly.textContent = formatCountdown(weeklyTargetTime);
}
for (const key in ocTargets) {
if (ocTargets[key]) {
ocTimerEls[key].textContent = formatCountdown(ocTargets[key]);
}
}
}
function renderUsage(data) {
providerState.commandcode.err = null;
providerState.commandcode.data = data;
ccContent.style.display = "";
ccErrorCard.classList.remove("show");
const credits = data.credits || (data.data && data.data.credits) || {};
const limits = data.window_limits || (data.data && data.data.window_limits) || {};
// Plan
const plan = data.plan || (data.data && data.data.plan);
if (plan) {
const planName = plan.name || (typeof plan === "string" ? plan : "Unknown");
planBadge.textContent = "Plan: " + planName;
planBadge.style.display = "inline-flex";
} else {
planBadge.style.display = "none";
}
// 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 = 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" : "");
cardMonthly.className = "quota-card" + (monthly.exceeded ? " is-exceeded" : "");
monthlyTargetTime = null;
timerMonthly.textContent = "账单周期";
// Five Hour
const fiveHour = limits.five_hour || {};
const pFive = Math.min(100, Math.max(0, fiveHour.percentage || 0));
badgeFiveHour.textContent = pFive.toFixed(1) + "%";
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" : "");
cardFiveHour.className = "quota-card" + (fiveHour.exceeded ? " is-exceeded" : "");
if (fiveHour.reset_at) {
fiveHourTargetTime = new Date(fiveHour.reset_at);
} else if (fiveHour.reset_in_seconds) {
fiveHourTargetTime = new Date(Date.now() + fiveHour.reset_in_seconds * 1000);
} else {
fiveHourTargetTime = null;
}
// Weekly
const weekly = limits.weekly || {};
const pWeek = Math.min(100, Math.max(0, weekly.percentage || 0));
badgeWeekly.textContent = pWeek.toFixed(1) + "%";
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" : "");
cardWeekly.className = "quota-card" + (weekly.exceeded ? " is-exceeded" : "");
if (weekly.reset_at) {
weeklyTargetTime = new Date(weekly.reset_at);
} else if (weekly.reset_in_seconds) {
weeklyTargetTime = new Date(Date.now() + weekly.reset_in_seconds * 1000);
} else {
weeklyTargetTime = null;
}
// Per-provider status; the header badge is aggregated in updateGlobalBadge()
let ccLevel;
if (fiveHour.exceeded || weekly.exceeded || monthly.exceeded) {
ccLevel = "exceeded";
} else if (pFive >= 80 || pWeek >= 80 || pMonth >= 80) {
ccLevel = "warning";
} else {
ccLevel = "online";
}
setProviderStatus("commandcode", ccLevel);
updateTimers();
}
function renderOpencode(data) {
providerState.opencode.err = null;
providerState.opencode.data = data;
ocContent.style.display = "";
ocErrorCard.classList.remove("show");
const windows = data.windows || {};
const defs = [
{ key: "rolling", card: ocCardRolling, badge: ocBadgeRolling, bar: ocBarRolling, timer: ocTimerRolling, chip: ocStatusRolling, chipText: ocStatusTextRolling, w: windows.rolling },
{ key: "weekly", card: ocCardWeekly, badge: ocBadgeWeekly, bar: ocBarWeekly, timer: ocTimerWeekly, chip: ocStatusWeekly, chipText: ocStatusTextWeekly, w: windows.weekly },
{ key: "monthly", card: ocCardMonthly, badge: ocBadgeMonthly, bar: ocBarMonthly, timer: ocTimerMonthly, chip: ocStatusMonthly, chipText: ocStatusTextMonthly, w: windows.monthly }
];
let worst = "online";
const rank = { online: 0, warning: 1, exceeded: 2 };
defs.forEach(function (d) {
const w = d.w || {};
const pct = clampPercent(w.percent);
const level = levelOf(pct, w.exceeded, w.status);
if (rank[level] > rank[worst]) worst = level;
d.badge.textContent = pct === null ? "- %" : pct.toFixed(1) + " %";
d.bar.style.width = (pct === null ? 0 : pct) + "%";
d.bar.className = "progress-bar" + (level === "exceeded" ? " danger" : level === "warning" ? " warning" : "");
d.card.className = "quota-card" + (level === "exceeded" ? " is-exceeded" : "");
d.chip.className = "status-badge " + level;
d.chipText.textContent = level === "exceeded" ? "超限" : level === "warning" ? "紧张" : "正常";
// reset_at 优先;缺失/不可解析时回退 reset_in_seconds;都没有则显示 "-"
let target = null;
if (w.reset_at) {
const t = new Date(w.reset_at);
if (!isNaN(t.getTime())) target = t;
}
if (!target && w.reset_in_seconds > 0) {
target = new Date(Date.now() + Number(w.reset_in_seconds) * 1000);
}
ocTargets[d.key] = target;
if (!target) d.timer.textContent = "-";
});
setProviderStatus("opencode", worst);
}
function renderAllTab() {
const cc = providerState.commandcode;
const oc = providerState.opencode;
// Command Code summary card
if (cc.err) {
allBadgeCommandcode.className = "status-badge exceeded";
allBadgeTextCommandcode.textContent = "查询错误";
allBodyCommandcode.innerHTML = '<div class="error-card-title">Command Code 查询失败</div><div class="error-card-msg">' + esc(cc.err) + '</div>';
} else if (cc.data) {
const data = cc.data;
const credits = data.credits || (data.data && data.data.credits) || {};
const limits = data.window_limits || (data.data && data.data.window_limits) || {};
const plan = data.plan || (data.data && data.data.plan);
const planName = plan ? (plan.name || (typeof plan === "string" ? plan : "Unknown")) : "-";
const windowsDef = [
{ name: "Monthly", o: limits.monthly || {} },
{ name: "5-Hour", o: limits.five_hour || {} },
{ name: "Weekly", o: limits.weekly || {} }
];
let worstName = "-";
let worstPct = null;
let worstLevel = "online";
windowsDef.forEach(function (d) {
const pct = clampPercent(d.o.percentage);
const level = levelOf(pct, d.o.exceeded, "");
if (worstPct === null || (pct !== null && pct > worstPct) || level === "exceeded") {
worstName = d.name;
worstPct = pct === null ? 0 : pct;
worstLevel = level;
}
});
allBadgeCommandcode.className = "status-badge " + badgeVisualClass(cc.level);
allBadgeTextCommandcode.textContent = BADGE_TEXT[cc.level] || "-";
allBodyCommandcode.innerHTML =
summaryRow("Plan", esc(planName)) +
summaryRow("Total Credits", formatUSD(credits.total_credits)) +
summaryRow("Monthly Credits", formatUSD(credits.monthly_credits)) +
summaryRow("最差窗口", esc(worstName) + " " + (worstPct === null ? "-" : worstPct.toFixed(1) + "%")) +
miniBar(worstPct, worstLevel);
} else {
allBadgeCommandcode.className = "status-badge";
allBadgeTextCommandcode.textContent = "尚未加载";
allBodyCommandcode.innerHTML = '<div class="all-summary-value">尚未加载</div>';
}
// OpenCode Go summary card
if (oc.err) {
allBadgeOpencode.className = "status-badge exceeded";
allBadgeTextOpencode.textContent = "查询错误";
allBodyOpencode.innerHTML = '<div class="error-card-title">OpenCode Go 查询失败</div><div class="error-card-msg">' + esc(oc.err) + '</div>';
} else if (oc.data) {
const data = oc.data;
const windows = data.windows || {};
const defs = [
{ name: "Rolling 5h", w: windows.rolling },
{ name: "Weekly", w: windows.weekly },
{ name: "Monthly", w: windows.monthly }
];
let html = "";
defs.forEach(function (d) {
const win = d.w || {};
const pct = clampPercent(win.percent);
const level = levelOf(pct, win.exceeded, win.status);
html += summaryRow(d.name, pct === null ? "-" : pct.toFixed(1) + "%") + miniBar(pct, level);
});
allBadgeOpencode.className = "status-badge " + badgeVisualClass(oc.level);
allBadgeTextOpencode.textContent = BADGE_TEXT[oc.level] || "-";
allBodyOpencode.innerHTML = html;
} else {
allBadgeOpencode.className = "status-badge";
allBadgeTextOpencode.textContent = "尚未加载";
allBodyOpencode.innerHTML = '<div class="all-summary-value">尚未加载</div>';
}
}
function setActiveTab(tab, updateHash) {
activeTab = tab;
const tabBtns = document.querySelectorAll(".tab-btn");
for (let i = 0; i < tabBtns.length; i++) {
tabBtns[i].classList.toggle("active", tabBtns[i].getAttribute("data-tab") === tab);
}
document.getElementById("sectionCommandcode").classList.toggle("active", tab === "commandcode");
document.getElementById("sectionOpencode").classList.toggle("active", tab === "opencode");
document.getElementById("sectionAll").classList.toggle("active", tab === "all");
updateGlobalBadge();
if (updateHash) {
if (tab === "commandcode") {
history.replaceState(null, "", location.pathname + location.search);
} else {
location.hash = tab;
}
}
}
async function fetchUsage() {
refreshIcon.classList.add("spin");
btnRefresh.disabled = true;
const mgmtKey = getStoredManagementKey();
const overrideToken = inputSessionToken.value.trim();
const overrideOpenCodeKey = inputOpenCodeKey.value.trim();
const headers = {
"Accept": "application/json",
"Content-Type": "application/json"
};
if (mgmtKey) {
headers["Authorization"] = "Bearer " + mgmtKey;
headers["X-Management-Key"] = mgmtKey;
}
// 整页只发一次 /all 请求,一次拿两个 provider;
// 覆盖凭据仅在填写时才进 body,不持久化
const bodyObj = {};
if (overrideToken) {
bodyObj.session_token = overrideToken;
}
if (overrideOpenCodeKey) {
bodyObj.opencode_api_key = overrideOpenCodeKey;
}
try {
const res = await fetch(ALL_ENDPOINT, {
method: "POST",
headers: headers,
body: JSON.stringify(bodyObj)
});
if (res.status === 401 || res.status === 403) {
settingsDrawer.classList.add("open");
showAlert("需要 CLIProxyAPI 管理密钥 (401/403)。请在上方输入框填入 Management Key 并保存。", true);
statusBadge.className = "status-badge warning";
statusText.textContent = "未授权";
return;
}
const json = await res.json();
if (!res.ok) {
const msg = (json && (json.error || json.message)) || "获取配额失败 (HTTP " + res.status + ")";
showProviderError("commandcode", msg);
showProviderError("opencode", msg);
showAlert(msg);
return;
}
// 部分失败不阻塞:缺失/失败 provider 在各自 tab 内渲染错误卡片
let anyOk = false;
if (json.commandcode && json.commandcode.ok !== false) {
renderUsage(json.commandcode);
anyOk = true;
} else {
showProviderError("commandcode", (json.errors && json.errors.commandcode) || "Command Code 查询失败");
}
if (json.opencode && json.opencode.ok !== false) {
renderOpencode(json.opencode);
anyOk = true;
} else {
showProviderError("opencode", (json.errors && json.errors.opencode) || "OpenCode Go 查询失败");
}
if (anyOk) {
hideAlert();
} else {
showAlert("所有数据源查询失败,请检查配置或凭据。");
}
const updatedStr = json.updated_at ||
(json.commandcode && json.commandcode.updated_at) ||
(json.opencode && json.opencode.updated_at);
lastUpdated.textContent = updatedStr ? new Date(updatedStr).toLocaleString() : new Date().toLocaleString();
renderAllTab();
updateTimers();
} catch (err) {
showProviderError("commandcode", "网络或同源请求错误: " + err.message);
showProviderError("opencode", "网络或同源请求错误: " + err.message);
showAlert("网络或同源请求错误: " + err.message);
} finally {
refreshIcon.classList.remove("spin");
btnRefresh.disabled = false;
}
}
// Event Listeners
btnRefresh.addEventListener("click", fetchUsage);
btnSettings.addEventListener("click", () => {
settingsDrawer.classList.toggle("open");
});
btnCloseSettings.addEventListener("click", () => {
settingsDrawer.classList.remove("open");
});
btnSaveConfig.addEventListener("click", () => {
const key = inputMgmtKey.value.trim();
if (key) {
localStorage.setItem("management_key", key);
}
fetchUsage();
});
// Tab switching + hash persistence (#opencode / #all)
document.querySelectorAll(".tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
setActiveTab(btn.getAttribute("data-tab"), true);
});
});
// Init on load
const initKey = localStorage.getItem("management_key") || localStorage.getItem("cpa_management_key");
if (initKey) {
inputMgmtKey.value = initKey;
}
function syncTheme() {
let themeSetting = null;
// 1. Try reading from parent window (if same-origin iframe)
try {
if (window.parent && window.parent !== window && window.parent.document) {
const parentTheme = window.parent.document.documentElement.getAttribute("data-theme");
if (parentTheme) {
themeSetting = parentTheme;
}
}
} catch (e) {}
// 2. Try reading from localStorage['cli-proxy-theme']
if (!themeSetting) {
try {
const stored = localStorage.getItem("cli-proxy-theme");
if (stored) {
const parsed = JSON.parse(stored);
if (parsed && parsed.state) {
if (parsed.state.theme) {
themeSetting = parsed.state.theme;
}
if (parsed.state.resolvedTheme && themeSetting === "auto") {
themeSetting = parsed.state.resolvedTheme;
}
}
}
} catch (e) {}
}
// 3. Fallback to own html attribute
if (!themeSetting) {
themeSetting = document.documentElement.getAttribute("data-theme");
}
// Apply theme to documentElement
const docEl = document.documentElement;
if (themeSetting === "dark") {
docEl.setAttribute("data-theme", "dark");
} else if (themeSetting === "white" || themeSetting === "light") {
docEl.setAttribute("data-theme", themeSetting);
} else {
// auto or unset: check system preference
const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
if (isDark) {
docEl.setAttribute("data-theme", "dark");
} else {
docEl.setAttribute("data-theme", "light");
}
}
}
// Initialize theme and sync listeners
syncTheme();
window.addEventListener("storage", (e) => {
if (e.key === "cli-proxy-theme") {
syncTheme();
}
});
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", syncTheme);
}
setInterval(syncTheme, 2000);
if (!timerInterval) {
timerInterval = setInterval(updateTimers, 1000);
}
// Restore tab from location.hash, then initial fetch
const initHash = location.hash.replace(/^#/, "");
if (initHash === "opencode" || initHash === "all") {
setActiveTab(initHash, false);
} else {
setActiveTab("commandcode", false);
}
// Initial fetch
fetchUsage();
})();
</script>
</body>
</html>
`
// GetQuotaPageHTML returns the raw HTML bytes.
func GetQuotaPageHTML() []byte {
return []byte(QuotaPageHTML)
}
// GetQuotaPageBase64 returns the base64-encoded Quota page HTML.
func GetQuotaPageBase64() string {
return base64.StdEncoding.EncodeToString([]byte(QuotaPageHTML))
}