feat(tray): show coding-plan usage for Kimi / Zhipu / MiniMax

dc04165f surfaced tray usage badges for Claude/Codex/Gemini official
OAuth only. Chinese coding-plan providers already expose 5h + weekly
windows through coding_plan::get_coding_plan_quota, but two gaps kept
the tray from rendering them.

- format_script_summary read only data.first(), truncating the tier-
  flattened UsageResult to a single window. Detect plan_name matching
  TIER_FIVE_HOUR / TIER_WEEKLY_LIMIT and emit the "🟢 h12% w80%" layout
  used by format_subscription_summary; worst utilization drives the
  emoji. Copilot / balance / custom scripts keep the legacy single-
  bucket output via fallback.

- usage_script previously required manual activation through
  UsageScriptModal. Auto-inject meta.usage_script on Claude provider
  creation when ANTHROPIC_BASE_URL matches a known coding plan, so the
  tray lights up without the user opening the modal. Does not overwrite
  existing usage_script on update.

Extract the URL route table out of UsageScriptModal into a shared
codingPlanProviders module so the modal, the creation hook, and the
Rust coding_plan::detect_provider mirror all agree on one list.
Add TIER_WEEKLY_LIMIT alongside TIER_FIVE_HOUR and a createUsageScript()
factory to collapse the duplicated default fields across four call
sites and drop the remaining stringly-typed tier names.
This commit is contained in:
Jason
2026-04-23 17:34:39 +08:00
parent cdcc423122
commit 4737158439
6 changed files with 267 additions and 62 deletions
+14 -53
View File
@@ -3,7 +3,7 @@ import { Play, Wand2, Eye, EyeOff, Save } from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { Provider, UsageScript, UsageData } from "@/types";
import { Provider, UsageScript, UsageData, createUsageScript } from "@/types";
import { usageApi, settingsApi, type AppId } from "@/lib/api";
import { copilotGetUsage, copilotGetUsageForAccount } from "@/lib/api/copilot";
import { useSettingsQuery } from "@/lib/query";
@@ -21,6 +21,10 @@ import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { cn } from "@/lib/utils";
import { TEMPLATE_TYPES, PROVIDER_TYPES } from "@/config/constants";
import {
CODING_PLAN_PROVIDERS,
detectCodingPlanProvider,
} from "@/config/codingPlanProviders";
interface UsageScriptModalProps {
provider: Provider;
@@ -114,21 +118,6 @@ const TEMPLATE_NAME_KEYS: Record<string, string> = {
[TEMPLATE_TYPES.BALANCE]: "usageScript.templateBalance",
};
/** Coding Plan 供应商选项 */
const TOKEN_PLAN_PROVIDERS = [
{ id: "kimi", label: "Kimi For Coding", pattern: /api\.kimi\.com\/coding/i },
{
id: "zhipu",
label: "Zhipu GLM (智谱)",
pattern: /bigmodel\.cn|api\.z\.ai/i,
},
{
id: "minimax",
label: "MiniMax",
pattern: /api\.minimaxi?\.com|api\.minimax\.io/i,
},
] as const;
/** 官方余额查询供应商检测 */
const BALANCE_PROVIDERS = [
{ id: "deepseek", label: "DeepSeek", pattern: /api\.deepseek\.com/i },
@@ -148,15 +137,6 @@ function detectBalanceProvider(baseUrl: string | undefined): boolean {
return BALANCE_PROVIDERS.some((bp) => bp.pattern.test(baseUrl));
}
/** 根据 Base URL 自动检测 Coding Plan 供应商 */
function detectTokenPlanProvider(baseUrl: string | undefined): string | null {
if (!baseUrl) return null;
for (const cp of TOKEN_PLAN_PROVIDERS) {
if (cp.pattern.test(baseUrl)) return cp.id;
}
return null;
}
const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
provider,
appId,
@@ -237,43 +217,24 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
return {
...savedScript,
codingPlanProvider:
detectTokenPlanProvider(providerCredentials.baseUrl) || "kimi",
detectCodingPlanProvider(providerCredentials.baseUrl) || "kimi",
};
}
return savedScript;
}
// 新配置:如果 URL 匹配 Coding Plan,自动初始化
const autoDetected = detectTokenPlanProvider(providerCredentials.baseUrl);
const autoDetected = detectCodingPlanProvider(providerCredentials.baseUrl);
if (autoDetected) {
return {
enabled: false,
language: "javascript" as const,
code: "",
timeout: 10,
autoQueryInterval: 5,
codingPlanProvider: autoDetected,
};
return createUsageScript({ codingPlanProvider: autoDetected });
}
// 新配置:如果 URL 匹配官方余额查询供应商,自动初始化
if (detectBalanceProvider(providerCredentials.baseUrl)) {
return {
enabled: false,
language: "javascript" as const,
code: "",
timeout: 10,
autoQueryInterval: 5,
};
return createUsageScript();
}
return {
enabled: false,
language: "javascript" as const,
return createUsageScript({
code: PRESET_TEMPLATES[TEMPLATE_TYPES.GENERAL],
timeout: 10,
autoQueryInterval: 5,
};
});
});
const [testing, setTesting] = useState(false);
@@ -346,7 +307,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
return TEMPLATE_TYPES.GENERAL;
}
// 新配置:如果 URL 匹配 Coding Plan 供应商,自动选择 Coding Plan 模板
if (detectTokenPlanProvider(providerCredentials.baseUrl)) {
if (detectCodingPlanProvider(providerCredentials.baseUrl)) {
return TEMPLATE_TYPES.TOKEN_PLAN;
}
// 新配置:如果 URL 匹配官方余额查询供应商,自动选择 Balance 模板
@@ -623,7 +584,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
});
} else if (presetName === TEMPLATE_TYPES.TOKEN_PLAN) {
// Coding Plan 模板不需要脚本,使用 Rust 原生查询
const autoDetected = detectTokenPlanProvider(
const autoDetected = detectCodingPlanProvider(
providerCredentials.baseUrl,
);
setScript({
@@ -862,7 +823,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
{t("usageScript.tokenPlanHint")}
</p>
<div className="flex gap-2 flex-wrap">
{TOKEN_PLAN_PROVIDERS.map((cp) => (
{CODING_PLAN_PROVIDERS.map((cp) => (
<Button
key={cp.id}
type="button"