mirror of
https://github.com/zgs225/cliproxy-plugin-commandcode.git
synced 2026-09-26 03:32:50 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2b8d89ba6 | ||
|
|
9d1884cf20 |
@@ -3,6 +3,7 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -74,8 +75,8 @@ func TestHandleManagement_QuotaResource(t *testing.T) {
|
|||||||
if !strings.Contains(bodyStr, "用量配额") {
|
if !strings.Contains(bodyStr, "用量配额") {
|
||||||
t.Errorf("Body does not contain expected menu text 用量配额")
|
t.Errorf("Body does not contain expected menu text 用量配额")
|
||||||
}
|
}
|
||||||
if !strings.Contains(bodyStr, "v0.4.3") {
|
if !strings.Contains(bodyStr, "v0.4.5") {
|
||||||
t.Errorf("Body does not contain version badge v0.4.3")
|
t.Errorf("Body does not contain version badge v0.4.5")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +176,24 @@ const mockOpencodeUsageJSON = `{"usage":{
|
|||||||
"monthly": {"status":"ok","percent":23,"resetsAt":"2026-10-14T09:13:49.000Z"}
|
"monthly": {"status":"ok","percent":23,"resetsAt":"2026-10-14T09:13:49.000Z"}
|
||||||
}}`
|
}}`
|
||||||
|
|
||||||
|
// mockOpencodeUsageJSONFuture returns the same usage envelope but with reset
|
||||||
|
// timestamps relative to now. The hardcoded dates in mockOpencodeUsageJSON
|
||||||
|
// eventually fall into the past (weekly 2026-09-21 did), which makes
|
||||||
|
// weekly.ResetInSeconds = 0 and breaks the `want > 0` assertion in
|
||||||
|
// TestHandleManagement_OpencodeUsageRoute. Use this fixture for tests that
|
||||||
|
// assert positive reset_in_seconds.
|
||||||
|
func mockOpencodeUsageJSONFuture() string {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
ts := func(d time.Duration) string {
|
||||||
|
return now.Add(d).Format("2006-01-02T15:04:05.000Z")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"usage":{
|
||||||
|
"rolling": {"status":"ok","percent":4, "resetsAt":"%s"},
|
||||||
|
"weekly": {"status":"ok","percent":46,"resetsAt":"%s"},
|
||||||
|
"monthly": {"status":"ok","percent":23,"resetsAt":"%s"}
|
||||||
|
}}`, ts(2*time.Hour), ts(72*time.Hour), ts(30*24*time.Hour))
|
||||||
|
}
|
||||||
|
|
||||||
// Verifies that /plugins/commandcode/opencode/usage is matched by the dedicated
|
// Verifies that /plugins/commandcode/opencode/usage is matched by the dedicated
|
||||||
// OpenCode handler and NOT swallowed by the generic "/usage" suffix match
|
// OpenCode handler and NOT swallowed by the generic "/usage" suffix match
|
||||||
// (which would route it to the Command Code handler).
|
// (which would route it to the Command Code handler).
|
||||||
@@ -194,7 +213,7 @@ func TestHandleManagement_OpencodeUsageRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
sawAuthHeader = true
|
sawAuthHeader = true
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_, _ = w.Write([]byte(mockOpencodeUsageJSON))
|
_, _ = w.Write([]byte(mockOpencodeUsageJSONFuture()))
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
PluginID = "commandcode"
|
PluginID = "commandcode"
|
||||||
PluginName = "commandcode"
|
PluginName = "commandcode"
|
||||||
PluginVersion = "0.4.3"
|
PluginVersion = "0.4.5"
|
||||||
PluginAuthor = "zgs225"
|
PluginAuthor = "zgs225"
|
||||||
PluginRepo = "https://github.com/zgs225/cliproxy-plugin-commandcode"
|
PluginRepo = "https://github.com/zgs225/cliproxy-plugin-commandcode"
|
||||||
PluginLogo = "https://raw.githubusercontent.com/zgs225/cliproxy-plugin-commandcode/main/assets/logo.svg"
|
PluginLogo = "https://raw.githubusercontent.com/zgs225/cliproxy-plugin-commandcode/main/assets/logo.svg"
|
||||||
|
|||||||
+38
-5
@@ -918,7 +918,7 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
|||||||
<div>
|
<div>
|
||||||
<div class="brand-title">
|
<div class="brand-title">
|
||||||
用量配额
|
用量配额
|
||||||
<span class="version-tag">v0.4.3</span>
|
<span class="version-tag">v0.4.5</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1234,6 +1234,8 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
|||||||
const lastUpdated = document.getElementById("lastUpdated");
|
const lastUpdated = document.getElementById("lastUpdated");
|
||||||
|
|
||||||
let fiveHourTargetTime = null;
|
let fiveHourTargetTime = null;
|
||||||
|
let monthlyTargetTime = null;
|
||||||
|
|
||||||
let weeklyTargetTime = null;
|
let weeklyTargetTime = null;
|
||||||
let timerInterval = null;
|
let timerInterval = null;
|
||||||
|
|
||||||
@@ -1625,12 +1627,43 @@ const QuotaPageHTML = `<!DOCTYPE html>
|
|||||||
});
|
});
|
||||||
allBadgeCommandcode.className = "status-badge " + badgeVisualClass(cc.level);
|
allBadgeCommandcode.className = "status-badge " + badgeVisualClass(cc.level);
|
||||||
allBadgeTextCommandcode.textContent = BADGE_TEXT[cc.level] || "-";
|
allBadgeTextCommandcode.textContent = BADGE_TEXT[cc.level] || "-";
|
||||||
|
// Plan + 三个限额窗口(百分比/进度条/重置时间),行结构与 OpenCode 卡片一致,
|
||||||
|
// 倒计时走 data-reset-at/data-reset-secs 属性,由 updateTimers() 统一刷新
|
||||||
|
const ccWins = [
|
||||||
|
{ name: "Rolling 5h", o: limits.five_hour || {} },
|
||||||
|
{ name: "Weekly", o: limits.weekly || {} },
|
||||||
|
{ name: "Monthly", o: limits.monthly || {} }
|
||||||
|
];
|
||||||
|
let ccRows = "";
|
||||||
|
ccWins.forEach(function (d) {
|
||||||
|
const w = d.o;
|
||||||
|
const pct = clampPercent(w.percentage);
|
||||||
|
const level = levelOf(pct, w.exceeded, "");
|
||||||
|
let resetAt = "";
|
||||||
|
let resetSecs = "";
|
||||||
|
if (w.reset_at) {
|
||||||
|
const t = new Date(w.reset_at);
|
||||||
|
if (!isNaN(t.getTime())) resetAt = t.toISOString();
|
||||||
|
}
|
||||||
|
if (!resetAt && w.reset_in_seconds > 0) {
|
||||||
|
resetSecs = String(w.reset_in_seconds);
|
||||||
|
}
|
||||||
|
const resetAttr = resetAt
|
||||||
|
? " data-reset-at=\"" + esc(resetAt) + "\""
|
||||||
|
: (resetSecs ? " data-reset-secs=\"" + esc(resetSecs) + "\"" : "");
|
||||||
|
const pctText = pct === null ? "-" : Math.round(pct) + "%";
|
||||||
|
const pctCls = level === "exceeded" ? " bad" : pct !== null && pct >= 80 ? " warn" : "";
|
||||||
|
const barCls = level === "exceeded" ? " danger" : level === "warning" ? " warning" : "";
|
||||||
|
ccRows += '<div class="oc-win-row">' +
|
||||||
|
'<span class="oc-win-name">' + d.name + '</span>' +
|
||||||
|
'<div class="progress-track oc-win-bar"><div class="progress-bar' + barCls + '" style="width:' + (pct === null ? 0 : pct) + '%"></div></div>' +
|
||||||
|
'<span class="oc-win-pct' + pctCls + '">' + pctText + '</span>' +
|
||||||
|
'<span class="oc-win-reset countdown-timer"' + resetAttr + '>-</span>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
allBodyCommandcode.innerHTML =
|
allBodyCommandcode.innerHTML =
|
||||||
summaryRow("Plan", esc(planName)) +
|
summaryRow("Plan", esc(planName)) +
|
||||||
summaryRow("Total Credits", formatUSD(credits.total_credits)) +
|
'<div class="oc-key-body">' + ccRows + '</div>';
|
||||||
summaryRow("Monthly Credits", formatUSD(credits.monthly_credits)) +
|
|
||||||
summaryRow("最差窗口", esc(worstName) + " " + (worstPct === null ? "-" : worstPct.toFixed(1) + "%")) +
|
|
||||||
miniBar(worstPct, worstLevel);
|
|
||||||
} else {
|
} else {
|
||||||
allBadgeCommandcode.className = "status-badge";
|
allBadgeCommandcode.className = "status-badge";
|
||||||
allBadgeTextCommandcode.textContent = "尚未加载";
|
allBadgeTextCommandcode.textContent = "尚未加载";
|
||||||
|
|||||||
@@ -21,3 +21,197 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log("pagecheck: embedded JS syntax OK");
|
console.log("pagecheck: embedded JS syntax OK");
|
||||||
|
|
||||||
|
// --- Undeclared-identifier audit (guards against ReferenceErrors like the
|
||||||
|
// v0.4.4 `monthlyTargetTime is not defined` bug: a bare identifier read in
|
||||||
|
// updateTimers() that was never declared and only existed as a global
|
||||||
|
// property accidentally created by renderUsage()).
|
||||||
|
//
|
||||||
|
// Approach (deliberately simple/grep-style, no DOM execution):
|
||||||
|
// 1. strip comments and string literals
|
||||||
|
// 2. collect every var/let/const/function declaration name + function/
|
||||||
|
// callback/catch parameter
|
||||||
|
// 3. flag candidates: bare assignment targets (x =, x +=, x++, ...),
|
||||||
|
// bare if()/while() condition identifiers, and for-loop init identifiers
|
||||||
|
// 4. whitelist known globals; anything left is reported and fails the check
|
||||||
|
|
||||||
|
const whitelist = new Set([
|
||||||
|
// browser builtins referenced by the page
|
||||||
|
"document", "window", "localStorage", "sessionStorage", "fetch",
|
||||||
|
"setInterval", "setTimeout", "clearInterval", "clearTimeout", "console",
|
||||||
|
"alert", "Date", "Math", "Number", "String", "Boolean", "Array",
|
||||||
|
"Object", "JSON", "parseInt", "parseFloat", "isNaN", "Promise", "Error",
|
||||||
|
"escape", "unescape", "navigator", "location", "history", "URL",
|
||||||
|
"URLSearchParams", "FormData", "Headers", "Request", "Response",
|
||||||
|
"Intl", "Map", "Set", "AbortController", "requestAnimationFrame",
|
||||||
|
"cancelAnimationFrame", "structuredClone", "globalThis", "arguments",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function stripLiterals(source) {
|
||||||
|
// Remove comments, string literals, and regex literals; replace with
|
||||||
|
// harmless placeholders so identifier scanning never sees string content.
|
||||||
|
// Template literals are NOT fully discarded: their ${...} interpolations
|
||||||
|
// are kept as "( ... )" so identifiers inside them stay visible.
|
||||||
|
//
|
||||||
|
// A "/" only starts a regex literal when it appears in expression
|
||||||
|
// position (previous significant token is an operator/open bracket or a
|
||||||
|
// keyword such as return/typeof). Otherwise it is division.
|
||||||
|
let out = "";
|
||||||
|
let i = 0;
|
||||||
|
const n = source.length;
|
||||||
|
// Stack of lexer contexts. Each entry: { type: "expr", depth: number } or
|
||||||
|
// { type: "tmpl" }. The initial code runs in a never-ending expr context.
|
||||||
|
const stack = [{ type: "expr", depth: Infinity }];
|
||||||
|
// Last significant (non-whitespace) emitted char + last identifier word,
|
||||||
|
// used for the regex-vs-division heuristic.
|
||||||
|
let lastSig = "";
|
||||||
|
let lastWord = "";
|
||||||
|
const KEYWORDS_BEFORE_REGEX = new Set([
|
||||||
|
"return", "typeof", "instanceof", "in", "of", "new", "delete", "void",
|
||||||
|
"do", "else", "case", "throw", "await", "yield",
|
||||||
|
]);
|
||||||
|
const emit = (text) => {
|
||||||
|
for (const ch of text) {
|
||||||
|
if (/\s/.test(ch)) continue;
|
||||||
|
if (/[A-Za-z0-9_$]/.test(ch)) {
|
||||||
|
lastWord = /[A-Za-z0-9_$]/.test(lastSig) ? lastWord + ch : ch;
|
||||||
|
} else {
|
||||||
|
lastWord = "";
|
||||||
|
}
|
||||||
|
lastSig = ch;
|
||||||
|
}
|
||||||
|
out += text;
|
||||||
|
};
|
||||||
|
const regexAllowed = () =>
|
||||||
|
lastSig === "" ||
|
||||||
|
"(,=:[!&|?+-*/%<>~^;{".includes(lastSig) ||
|
||||||
|
KEYWORDS_BEFORE_REGEX.has(lastWord);
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
const ctx = stack[stack.length - 1];
|
||||||
|
const c = source[i];
|
||||||
|
const next = source[i + 1];
|
||||||
|
|
||||||
|
if (ctx.type === "tmpl") {
|
||||||
|
// Inside template literal text: skip until ` or ${ ... }
|
||||||
|
if (c === "\\") { i += 2; continue; }
|
||||||
|
if (c === "`") { i++; stack.pop(); emit(" "); continue; }
|
||||||
|
if (c === "$" && next === "{") {
|
||||||
|
i += 2;
|
||||||
|
stack.push({ type: "expr", depth: 0 });
|
||||||
|
emit(" ( ");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// code context (top-level or template ${...} expression)
|
||||||
|
if (c === "/" && next === "/") {
|
||||||
|
while (i < n && source[i] !== "\n") i++;
|
||||||
|
} else if (c === "/" && next === "*") {
|
||||||
|
i += 2;
|
||||||
|
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
||||||
|
i += 2;
|
||||||
|
} else if (c === "/" && regexAllowed()) {
|
||||||
|
// regex literal: skip to unescaped closing / (not inside [...])
|
||||||
|
i++;
|
||||||
|
let inClass = false;
|
||||||
|
while (i < n) {
|
||||||
|
if (source[i] === "\\") { i += 2; continue; }
|
||||||
|
if (source[i] === "[") { inClass = true; i++; continue; }
|
||||||
|
if (source[i] === "]") { inClass = false; i++; continue; }
|
||||||
|
if (source[i] === "/" && !inClass) { i++; break; }
|
||||||
|
if (source[i] === "\n") break; // malformed; bail out safely
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
while (i < n && /[a-z]/i.test(source[i])) i++; // flags
|
||||||
|
emit(" / ");
|
||||||
|
} else if (c === '"' || c === "'") {
|
||||||
|
const quote = c;
|
||||||
|
i++;
|
||||||
|
while (i < n) {
|
||||||
|
if (source[i] === "\\") { i += 2; continue; }
|
||||||
|
if (source[i] === quote) { i++; break; }
|
||||||
|
if (source[i] === "\n") break;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
emit(" " + quote + quote + " ");
|
||||||
|
} else if (c === "`") {
|
||||||
|
i++;
|
||||||
|
stack.push({ type: "tmpl" });
|
||||||
|
emit(" ");
|
||||||
|
} else {
|
||||||
|
if (c === "{") ctx.depth++;
|
||||||
|
if (c === "}") {
|
||||||
|
if (ctx.depth === 0) {
|
||||||
|
// closes a template interpolation: back into template text
|
||||||
|
stack.pop();
|
||||||
|
i++;
|
||||||
|
emit(" ) ");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ctx.depth--;
|
||||||
|
}
|
||||||
|
emit(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDeclarations(clean) {
|
||||||
|
const declared = new Set();
|
||||||
|
const addParamList = (raw) => {
|
||||||
|
for (const p of raw.split(",")) {
|
||||||
|
const name = p.trim().split(/[\s=]/)[0].replace(/^\.\.\./, "");
|
||||||
|
if (/^[A-Za-z_$][\w$]*$/.test(name)) declared.add(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const m of clean.matchAll(/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)/g))
|
||||||
|
declared.add(m[1]);
|
||||||
|
// function declarations/expressions: name + params
|
||||||
|
for (const m of clean.matchAll(/\bfunction\s*([A-Za-z_$][\w$]*)?\s*\(([^()]*)\)/g)) {
|
||||||
|
if (m[1]) declared.add(m[1]);
|
||||||
|
addParamList(m[2]);
|
||||||
|
}
|
||||||
|
// arrow functions: (a, b) => and a =>
|
||||||
|
for (const m of clean.matchAll(/\(\s*([^()]*?)\s*\)\s*=>/g)) addParamList(m[1]);
|
||||||
|
for (const m of clean.matchAll(/(?<![\w$.(])\b([A-Za-z_$][\w$]*)\s*=>/g)) declared.add(m[1]);
|
||||||
|
// catch (e) and destructuring catch
|
||||||
|
for (const m of clean.matchAll(/\bcatch\s*\(?\s*\{?\s*([A-Za-z_$][\w$]*)/g)) declared.add(m[1]);
|
||||||
|
return declared;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clean = stripLiterals(js);
|
||||||
|
const declared = collectDeclarations(clean);
|
||||||
|
const flagged = new Set();
|
||||||
|
|
||||||
|
// 1. bare assignment targets / updates: x =, x +=, x++, x--, x ??=
|
||||||
|
for (const m of clean.matchAll(/(?:^|[{};\n])\s*([A-Za-z_$][\w$]*)\s*(?:=[^=>]|[+*\/%-]?=[^=]|\+\+|\-\-)/gm)) {
|
||||||
|
const name = m[1];
|
||||||
|
if (!declared.has(name) && !whitelist.has(name)) flagged.add(name);
|
||||||
|
}
|
||||||
|
// 2. bare if()/while() condition identifiers
|
||||||
|
for (const m of clean.matchAll(/\b(?:if|while)\s*\(\s*(!*)\s*([A-Za-z_$][\w$]*)\s*(?:\)|&&|\|\||\?)/g)) {
|
||||||
|
const name = m[2];
|
||||||
|
if (!declared.has(name) && !whitelist.has(name)) flagged.add(name);
|
||||||
|
}
|
||||||
|
// 3. for-loop init without let/var: for (i = 0; ...)
|
||||||
|
for (const m of clean.matchAll(/\bfor\s*\(\s*([A-Za-z_$][\w$]*)\s*=[^=]/g)) {
|
||||||
|
const name = m[1];
|
||||||
|
if (!declared.has(name) && !whitelist.has(name)) flagged.add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagged.size > 0) {
|
||||||
|
console.error("pagecheck: undeclared identifier(s) referenced in embedded JS:");
|
||||||
|
for (const name of [...flagged].sort()) console.error(" - " + name);
|
||||||
|
console.error(
|
||||||
|
"pagecheck: fix by declaring with let/const (see v0.4.5 monthlyTargetTime bug); " +
|
||||||
|
"if this is a false positive, extend scripts/pagecheck.js"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"pagecheck: undeclared-identifier scan OK (" + declared.size + " declarations checked)"
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user