mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
Fix custom usage script summaries (#3129)
Co-authored-by: Zihao Han <hanhan3344@users.noreply.github.com>
This commit is contained in:
@@ -176,18 +176,18 @@ Interval for automatically refreshing usage data (minutes):
|
||||
|
||||
## Extractor Return Format
|
||||
|
||||
The extractor function must return an object containing the following fields:
|
||||
The extractor function returns an object containing the following fields. All fields are optional:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `isValid` | boolean | No | Whether the account is valid, defaults to true |
|
||||
| `invalidMessage` | string | No | Message when invalid |
|
||||
| `remaining` | number | Yes | Remaining balance |
|
||||
| `unit` | string | Yes | Unit (e.g., USD, CNY, times) |
|
||||
| `remaining` | number | No | Remaining balance |
|
||||
| `unit` | string | No | Unit (e.g., USD, CNY, times) |
|
||||
| `planName` | string | No | Plan name (supports multi-plan) |
|
||||
| `total` | number | No | Total balance |
|
||||
| `used` | number | No | Used amount |
|
||||
| `extra` | object | No | Additional information |
|
||||
| `extra` | string | No | Additional display text |
|
||||
|
||||
## Test Script
|
||||
|
||||
|
||||
@@ -176,18 +176,18 @@ New API タイプの中継サービス専用に設計されています:
|
||||
|
||||
## エクストラクターの戻り値形式
|
||||
|
||||
エクストラクター関数は以下のフィールドを含むオブジェクトを返す必要があります:
|
||||
エクストラクター関数は以下のフィールドを含むオブジェクトを返します。すべてのフィールドは任意です:
|
||||
|
||||
| フィールド | 型 | 必須 | 説明 |
|
||||
|------|------|------|------|
|
||||
| `isValid` | boolean | いいえ | アカウントが有効かどうか、デフォルト true |
|
||||
| `invalidMessage` | string | いいえ | 無効時の通知メッセージ |
|
||||
| `remaining` | number | はい | 残額 |
|
||||
| `unit` | string | はい | 単位(例:USD、CNY、回) |
|
||||
| `remaining` | number | いいえ | 残額 |
|
||||
| `unit` | string | いいえ | 単位(例:USD、CNY、回) |
|
||||
| `planName` | string | いいえ | プラン名(複数プラン対応) |
|
||||
| `total` | number | いいえ | 総額 |
|
||||
| `used` | number | いいえ | 使用済み額 |
|
||||
| `extra` | object | いいえ | 追加情報 |
|
||||
| `extra` | string | いいえ | 追加表示テキスト |
|
||||
|
||||
## スクリプトのテスト
|
||||
|
||||
|
||||
@@ -176,18 +176,18 @@ CC Switch 提供三种预设模板:
|
||||
|
||||
## 提取器返回格式
|
||||
|
||||
提取器函数需要返回包含以下字段的对象:
|
||||
提取器函数返回包含以下字段的对象,所有字段均可选:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `isValid` | boolean | 否 | 账户是否有效,默认 true |
|
||||
| `invalidMessage` | string | 否 | 无效时的提示信息 |
|
||||
| `remaining` | number | 是 | 剩余额度 |
|
||||
| `unit` | string | 是 | 单位(如 USD、CNY、次) |
|
||||
| `remaining` | number | 否 | 剩余额度 |
|
||||
| `unit` | string | 否 | 单位(如 USD、CNY、次) |
|
||||
| `planName` | string | 否 | 套餐名称(支持多套餐) |
|
||||
| `total` | number | 否 | 总额度 |
|
||||
| `used` | number | 否 | 已使用额度 |
|
||||
| `extra` | object | 否 | 额外信息 |
|
||||
| `extra` | string | 否 | 额外展示文本 |
|
||||
|
||||
## 测试脚本
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ pub async fn execute_usage_script(
|
||||
|
||||
// 2. 验证 base_url 的安全性(仅当提供了 base_url 时)
|
||||
// 自定义模板模式下,用户可能不使用模板变量,而是直接在脚本中写完整 URL
|
||||
if !base_url.is_empty() {
|
||||
if should_validate_base_url(base_url, is_custom_template) {
|
||||
validate_base_url(base_url)?;
|
||||
}
|
||||
|
||||
@@ -470,6 +470,10 @@ fn validate_base_url(base_url: &str) -> Result<(), AppError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_validate_base_url(base_url: &str, is_custom_template: bool) -> bool {
|
||||
!base_url.is_empty() && !is_custom_template
|
||||
}
|
||||
|
||||
/// 验证请求 URL 是否安全(HTTPS 强制 + 同源检查)
|
||||
fn validate_request_url(
|
||||
request_url: &str,
|
||||
@@ -580,6 +584,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_template_allows_http_lan_request_with_different_base_url() {
|
||||
assert!(
|
||||
!should_validate_base_url("http://10.37.192.156:8090/anthropic", true),
|
||||
"Custom scripts should not validate an unused provider base_url fallback"
|
||||
);
|
||||
|
||||
let result = validate_request_url(
|
||||
"http://10.37.192.156:18344/user/balance",
|
||||
"http://10.37.192.156:8090/anthropic",
|
||||
true,
|
||||
);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Custom usage scripts should be able to call an explicit HTTP quota endpoint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_comparison() {
|
||||
// 测试端口比较逻辑是否正确处理默认端口和显式端口
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
CODING_PLAN_PROVIDERS,
|
||||
detectCodingPlanProvider,
|
||||
} from "@/config/codingPlanProviders";
|
||||
import { formatUsageDataSummary } from "@/utils/usageDisplay";
|
||||
|
||||
interface UsageScriptModalProps {
|
||||
provider: Provider;
|
||||
@@ -394,10 +395,13 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const result = await subscriptionApi.getBalance(baseUrl, apiKey);
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
const summary = result.data
|
||||
.map((d) => {
|
||||
const name = d.planName ? `[${d.planName}] ` : "";
|
||||
return `${name}${t("usage.remaining")} ${d.remaining?.toFixed(2)} ${d.unit || ""}`;
|
||||
})
|
||||
.map((d) =>
|
||||
formatUsageDataSummary(d, {
|
||||
invalid: t("usage.invalid"),
|
||||
remaining: t("usage.remaining"),
|
||||
used: t("usage.used"),
|
||||
}),
|
||||
)
|
||||
.join(", ");
|
||||
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
|
||||
duration: 3000,
|
||||
@@ -493,10 +497,13 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
);
|
||||
if (result.success && result.data && result.data.length > 0) {
|
||||
const summary = result.data
|
||||
.map((plan: UsageData) => {
|
||||
const planInfo = plan.planName ? `[${plan.planName}]` : "";
|
||||
return `${planInfo} ${t("usage.remaining")} ${plan.remaining} ${plan.unit}`;
|
||||
})
|
||||
.map((plan: UsageData) =>
|
||||
formatUsageDataSummary(plan, {
|
||||
invalid: t("usage.invalid"),
|
||||
remaining: t("usage.remaining"),
|
||||
used: t("usage.used"),
|
||||
}),
|
||||
)
|
||||
.join(", ");
|
||||
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
|
||||
duration: 3000,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { UsageData } from "@/types";
|
||||
|
||||
interface UsageSummaryLabels {
|
||||
invalid: string;
|
||||
remaining: string;
|
||||
used: string;
|
||||
}
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
||||
}
|
||||
|
||||
function formatValue(value: number, unit?: string): string {
|
||||
if (!unit) {
|
||||
return formatNumber(value);
|
||||
}
|
||||
|
||||
return unit === "%"
|
||||
? `${formatNumber(value)}%`
|
||||
: `${formatNumber(value)} ${unit}`;
|
||||
}
|
||||
|
||||
function isNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function formatUsed(
|
||||
data: UsageData,
|
||||
labels: UsageSummaryLabels,
|
||||
): string | null {
|
||||
if (!isNumber(data.used)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNumber(data.total) && data.total > 0) {
|
||||
const usedPercent = (data.used / data.total) * 100;
|
||||
|
||||
if (data.unit === "%" && data.total === 100) {
|
||||
return `${labels.used} ${formatValue(data.used, "%")}`;
|
||||
}
|
||||
|
||||
return `${labels.used} ${formatNumber(usedPercent)}%`;
|
||||
}
|
||||
|
||||
return `${labels.used} ${formatValue(data.used, data.unit)}`;
|
||||
}
|
||||
|
||||
export function formatUsageDataSummary(
|
||||
data: UsageData,
|
||||
labels: UsageSummaryLabels,
|
||||
): string {
|
||||
const planPrefix = data.planName ? `[${data.planName}] ` : "";
|
||||
|
||||
if (data.isValid === false) {
|
||||
return `${planPrefix}${data.invalidMessage || labels.invalid}`;
|
||||
}
|
||||
|
||||
const parts = [
|
||||
formatUsed(data, labels),
|
||||
isNumber(data.remaining)
|
||||
? `${labels.remaining} ${formatValue(data.remaining, data.unit)}`
|
||||
: null,
|
||||
data.extra || null,
|
||||
].filter((part): part is string => Boolean(part));
|
||||
|
||||
return `${planPrefix}${parts.join(" / ") || labels.invalid}`;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatUsageDataSummary } from "@/utils/usageDisplay";
|
||||
|
||||
const labels = {
|
||||
invalid: "Invalid",
|
||||
remaining: "Remaining:",
|
||||
used: "Used:",
|
||||
};
|
||||
|
||||
describe("formatUsageDataSummary", () => {
|
||||
it("formats used percentage when remaining is omitted", () => {
|
||||
expect(
|
||||
formatUsageDataSummary(
|
||||
{
|
||||
planName: "Coco OpenRouter",
|
||||
used: 55,
|
||||
total: 100,
|
||||
unit: "%",
|
||||
},
|
||||
labels,
|
||||
),
|
||||
).toBe("[Coco OpenRouter] Used: 55%");
|
||||
});
|
||||
|
||||
it("formats remaining when present", () => {
|
||||
expect(
|
||||
formatUsageDataSummary(
|
||||
{
|
||||
planName: "Balance",
|
||||
remaining: 12.5,
|
||||
unit: "USD",
|
||||
},
|
||||
labels,
|
||||
),
|
||||
).toBe("[Balance] Remaining: 12.50 USD");
|
||||
});
|
||||
|
||||
it("formats invalid results without requiring quota fields", () => {
|
||||
expect(
|
||||
formatUsageDataSummary(
|
||||
{
|
||||
isValid: false,
|
||||
invalidMessage: "Unauthorized",
|
||||
},
|
||||
labels,
|
||||
),
|
||||
).toBe("Unauthorized");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user