mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(provider): add Zhipu team plan quota query support (#5128)
智谱团队套餐(Team Plan)的额度查询与个人版不同:同一 quota 端点加 `?type=2`,需额外 `bigmodel-organization` / `bigmodel-project` 请求头 (api_key + 组织 ID + 项目 ID 三者缺一不可),且仅存在于国内站 open.bigmodel.cn。参考 token-monitor/src/shared/zaiTeamLimits.js 实现。 - Backend (services/coding_plan.rs): 新增 query_zhipu_team(固定 CN 站、 ?type=2、org/project 头);抽出 zhipu_quota_from_body 与个人版共用解析; 入口 get_coding_plan_quota 靠显式 coding_plan_provider == "zhipu_team" 路由(base_url 与个人版智谱相同,detect_provider 无法区分)。新字段经 UsageScript、IPC 命令、后台查询路径(query_provider_usage_inner)透传。 - Frontend: UsageScriptModal 新增「Zhipu GLM Team」选项 + 组织/项目 ID 输入;模板切换时保留 team 字段;测试与保存逻辑按 team 传参。 - i18n: en/zh/zh-TW/ja 四个 locale 更新。 - Tests: 凭据校验/路由(缺任一凭据 → NotFound,标识大小写不敏感)+ 本地 server 验证 ?type=2 与 org/project 请求头形状。 Co-authored-by: XuZhanXin <1239576606@qq.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ import { formatUsageDataSummary } from "@/utils/usageDisplay";
|
||||
*/
|
||||
const VOLCENGINE_KEY_CONSOLE_URL =
|
||||
"https://console.volcengine.com/iam/keymanage";
|
||||
// 智谱团队套餐用量页(组织 ID / 项目 ID 在此页 URL 或管理后台可见)
|
||||
const ZHIPU_TEAM_USAGE_URL = "https://bigmodel.cn/coding-plan/team/usage-stats";
|
||||
|
||||
interface UsageScriptModalProps {
|
||||
provider: Provider;
|
||||
@@ -553,6 +555,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
// 另用账号 AK/SK 签名查询控制面用量。
|
||||
const isZenMux = script.codingPlanProvider === "zenmux";
|
||||
const isVolcengine = script.codingPlanProvider === "volcengine";
|
||||
const isZhipuTeam = script.codingPlanProvider === "zhipu_team";
|
||||
const baseUrl = isZenMux
|
||||
? (script.baseUrl ?? "")
|
||||
: (providerCredentials.baseUrl ?? "");
|
||||
@@ -565,6 +568,9 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
apiKey,
|
||||
isVolcengine ? script.accessKeyId : undefined,
|
||||
isVolcengine ? script.secretAccessKey : undefined,
|
||||
isZhipuTeam ? script.codingPlanProvider : undefined,
|
||||
isZhipuTeam ? script.teamOrganizationId : undefined,
|
||||
isZhipuTeam ? script.teamProjectId : undefined,
|
||||
);
|
||||
if (quota.success && quota.tiers.length > 0) {
|
||||
const summary = quota.tiers
|
||||
@@ -746,9 +752,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
providerCredentials.baseUrl,
|
||||
);
|
||||
const provider = script.codingPlanProvider || autoDetected || "kimi";
|
||||
// ZenMux 保留手填 baseUrl/apiKey;火山保留账号 AK/SK;其余清除。
|
||||
// ZenMux 保留手填 baseUrl/apiKey;火山保留账号 AK/SK;智谱团队保留组织/项目 ID;其余清除。
|
||||
const isZenMux = provider === "zenmux";
|
||||
const isVolcengine = provider === "volcengine";
|
||||
const isZhipuTeam = provider === "zhipu_team";
|
||||
setScript({
|
||||
...script,
|
||||
code: "",
|
||||
@@ -758,6 +765,10 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
userId: undefined,
|
||||
accessKeyId: isVolcengine ? script.accessKeyId : undefined,
|
||||
secretAccessKey: isVolcengine ? script.secretAccessKey : undefined,
|
||||
teamOrganizationId: isZhipuTeam
|
||||
? script.teamOrganizationId
|
||||
: undefined,
|
||||
teamProjectId: isZhipuTeam ? script.teamProjectId : undefined,
|
||||
codingPlanProvider: provider,
|
||||
});
|
||||
} else if (presetName === TEMPLATE_TYPES.BALANCE) {
|
||||
@@ -1359,6 +1370,76 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 智谱团队套餐:需组织 ID + 项目 ID(api_key 沿用供应商推理凭据) */}
|
||||
{selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN &&
|
||||
script.codingPlanProvider === "zhipu_team" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-foreground">
|
||||
{t("usageScript.credentialsConfig")}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
|
||||
{t("usageScript.zhipuTeamHint")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{t("usageScript.zhipuTeamConsoleLink")}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
settingsApi.openExternal(ZHIPU_TEAM_USAGE_URL)
|
||||
}
|
||||
className="inline-flex items-center gap-1 text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors break-all align-baseline underline-offset-2 hover:underline"
|
||||
>
|
||||
{ZHIPU_TEAM_USAGE_URL}
|
||||
<ExternalLink size={12} className="shrink-0" />
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage-zhipu-team-org">
|
||||
{t("usageScript.organizationId")}
|
||||
</Label>
|
||||
<Input
|
||||
id="usage-zhipu-team-org"
|
||||
type="text"
|
||||
value={script.teamOrganizationId || ""}
|
||||
onChange={(e) =>
|
||||
setScript({
|
||||
...script,
|
||||
teamOrganizationId: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("usageScript.organizationIdPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="border-white/10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="usage-zhipu-team-project">
|
||||
{t("usageScript.projectId")}
|
||||
</Label>
|
||||
<Input
|
||||
id="usage-zhipu-team-project"
|
||||
type="text"
|
||||
value={script.teamProjectId || ""}
|
||||
onChange={(e) =>
|
||||
setScript({
|
||||
...script,
|
||||
teamProjectId: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("usageScript.projectIdPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="border-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 通用配置(始终显示) */}
|
||||
<div className="grid gap-4 md:grid-cols-2 pt-4 border-t border-white/10">
|
||||
{/* 超时时间 */}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TEMPLATE_TYPES } from "@/config/constants";
|
||||
|
||||
export interface CodingPlanProviderEntry {
|
||||
/** 与后端 QuotaTier 的 `codingPlanProvider` 取值对齐 */
|
||||
id: "kimi" | "zhipu" | "minimax" | "zenmux" | "volcengine";
|
||||
id: "kimi" | "zhipu" | "zhipu_team" | "minimax" | "zenmux" | "volcengine";
|
||||
/** UsageScriptModal 下拉显示用 */
|
||||
label: string;
|
||||
/** base_url 匹配规则 */
|
||||
@@ -25,6 +25,16 @@ export const CODING_PLAN_PROVIDERS: readonly CodingPlanProviderEntry[] = [
|
||||
label: "Zhipu GLM (智谱)",
|
||||
pattern: /bigmodel\.cn|api\.z\.ai/i,
|
||||
},
|
||||
{
|
||||
// 智谱团队套餐(Team Plan)。base_url 与个人版智谱(open.bigmodel.cn)相同,
|
||||
// 无法靠 base_url 自动区分——靠显式 codingPlanProvider === "zhipu_team" 路由。
|
||||
// 个人版 zhipu 排在前面,detectCodingPlanProvider 首匹配仍命中个人版,
|
||||
// 故团队版永不被 injectCodingPlanUsageScript 自动注入(必须用户手动选)。
|
||||
// pattern 仅占位(下拉展示用),实际不参与自动检测。
|
||||
id: "zhipu_team",
|
||||
label: "Zhipu GLM Team (智谱团队)",
|
||||
pattern: /bigmodel\.cn/i,
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
label: "MiniMax",
|
||||
|
||||
@@ -1592,6 +1592,12 @@
|
||||
"secretAccessKey": "SecretAccessKey",
|
||||
"volcengineAkSkHint": "Volcengine usage query needs an account-level AccessKey ID / Secret (different from the inference API key). Create them in the Volcengine console via the top-right account menu → 'API Access Keys'.",
|
||||
"volcengineKeyConsoleLink": "Create keys at:",
|
||||
"zhipuTeamHint": "Zhipu team plan usage query needs the team API key (reuses this provider's credentials) + organization ID + project ID (sent as bigmodel-organization / bigmodel-project request headers). Find the organization and project IDs in the team admin console's usage page URL.",
|
||||
"zhipuTeamConsoleLink": "Team usage page:",
|
||||
"organizationId": "Organization ID",
|
||||
"organizationIdPlaceholder": "e.g. org-xxxxxx",
|
||||
"projectId": "Project ID",
|
||||
"projectIdPlaceholder": "e.g. proj-xxxxxx",
|
||||
"defaultPlan": "Default Plan",
|
||||
"queryFailedMessage": "Query failed",
|
||||
"queryScript": "Query script (JavaScript)",
|
||||
|
||||
@@ -1592,6 +1592,12 @@
|
||||
"secretAccessKey": "SecretAccessKey",
|
||||
"volcengineAkSkHint": "Volcengine の使用量照会にはアカウントレベルの AccessKey ID / Secret が必要です(推論 API キーとは別物)。Volcengine コンソール右上のアカウントメニュー →「API アクセスキー」で作成してください。",
|
||||
"volcengineKeyConsoleLink": "キー作成ページ:",
|
||||
"zhipuTeamHint": "智譜チームプランの使用量照会には、チーム API キー(このプロバイダーの認証情報を流用)+ 組織 ID + プロジェクト ID が必要です(リクエストヘッダー bigmodel-organization / bigmodel-project として送信)。組織 ID とプロジェクト ID はチーム管理コンソールの使用量ページの URL で確認できます。",
|
||||
"zhipuTeamConsoleLink": "チーム使用量ページ:",
|
||||
"organizationId": "組織 ID",
|
||||
"organizationIdPlaceholder": "例: org-xxxxxx",
|
||||
"projectId": "プロジェクト ID",
|
||||
"projectIdPlaceholder": "例: proj-xxxxxx",
|
||||
"defaultPlan": "デフォルトプラン",
|
||||
"queryFailedMessage": "照会に失敗しました",
|
||||
"queryScript": "照会スクリプト (JavaScript)",
|
||||
|
||||
@@ -1564,6 +1564,12 @@
|
||||
"secretAccessKey": "SecretAccessKey",
|
||||
"volcengineAkSkHint": "火山用量查詢需帳號級 AccessKey ID / Secret(與推理 API Key 不同)。請在火山引擎主控台右上角帳號選單 →「API存取金鑰」中建立。",
|
||||
"volcengineKeyConsoleLink": "金鑰建立網址:",
|
||||
"zhipuTeamHint": "智譜團隊套餐用量查詢需團隊 API Key(沿用本供應商憑據)+ 組織 ID + 專案 ID(作為請求標頭 bigmodel-organization / bigmodel-project)。組織與專案 ID 可在團隊管理後台用量頁的 URL 中找到。",
|
||||
"zhipuTeamConsoleLink": "團隊用量頁:",
|
||||
"organizationId": "組織 ID",
|
||||
"organizationIdPlaceholder": "例如:org-xxxxxx",
|
||||
"projectId": "專案 ID",
|
||||
"projectIdPlaceholder": "例如:proj-xxxxxx",
|
||||
"defaultPlan": "預設方案",
|
||||
"queryFailedMessage": "查詢失敗",
|
||||
"queryScript": "查詢腳本(JavaScript)",
|
||||
|
||||
@@ -1592,6 +1592,12 @@
|
||||
"secretAccessKey": "SecretAccessKey",
|
||||
"volcengineAkSkHint": "火山用量查询需账号级 AccessKey ID / Secret(与推理 API Key 不同)。请在火山引擎控制台右上角账号菜单 →「API访问密钥」中创建。",
|
||||
"volcengineKeyConsoleLink": "密钥创建地址:",
|
||||
"zhipuTeamHint": "智谱团队套餐用量查询需团队 API Key(沿用本供应商凭据)+ 组织 ID + 项目 ID(作为请求头 bigmodel-organization / bigmodel-project)。组织与项目 ID 可在团队管理后台用量页的 URL 中找到。",
|
||||
"zhipuTeamConsoleLink": "团队用量页:",
|
||||
"organizationId": "组织 ID",
|
||||
"organizationIdPlaceholder": "例如:org-xxxxxx",
|
||||
"projectId": "项目 ID",
|
||||
"projectIdPlaceholder": "例如:proj-xxxxxx",
|
||||
"defaultPlan": "默认套餐",
|
||||
"queryFailedMessage": "查询失败",
|
||||
"queryScript": "查询脚本(JavaScript)",
|
||||
|
||||
@@ -12,12 +12,19 @@ export const subscriptionApi = {
|
||||
// 火山方舟用账号 AK/SK 签名查询用量;其他供应商不传。
|
||||
accessKeyId?: string,
|
||||
secretAccessKey?: string,
|
||||
// 智谱团队版(zhipu_team)靠显式标识路由(base_url 与个人版相同无法区分)。
|
||||
codingPlanProvider?: string,
|
||||
teamOrganizationId?: string,
|
||||
teamProjectId?: string,
|
||||
): Promise<SubscriptionQuota> =>
|
||||
invoke("get_coding_plan_quota", {
|
||||
baseUrl,
|
||||
apiKey,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
codingPlanProvider,
|
||||
teamOrganizationId,
|
||||
teamProjectId,
|
||||
}),
|
||||
getBalance: (
|
||||
baseUrl: string,
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface UsageScript {
|
||||
userId?: string; // 用户ID(NewAPI 模板使用)
|
||||
accessKeyId?: string; // 火山方舟 AccessKey ID(用量查询签名用,与推理 Key 分离)
|
||||
secretAccessKey?: string; // 火山方舟 SecretAccessKey
|
||||
teamOrganizationId?: string; // 智谱团队套餐组织 ID(请求头 bigmodel-organization)
|
||||
teamProjectId?: string; // 智谱团队套餐项目 ID(请求头 bigmodel-project)
|
||||
codingPlanProvider?: string; // Coding Plan 供应商标识(如 "kimi", "zhipu", "minimax")
|
||||
autoQueryInterval?: number; // 自动查询间隔(单位:分钟,0 表示禁用)
|
||||
autoIntervalMinutes?: number; // 自动查询间隔(分钟)- 别名字段
|
||||
|
||||
Reference in New Issue
Block a user