mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(codex): expose official routing and restore the built-in provider
Let users switch between the built-in OpenAI provider and third-party Codex providers directly from the provider panel while takeover mode remains active. Centralize the frontend capability predicate so only the fixed codex-official seed receives native-login routing support, and keep copied UUID-based official entries clearly marked as unsupported. Add an idempotent backend command that recreates the deleted official seed, wire it into the add-provider flow, refresh localized guidance, and add mutation and provider-action regression coverage.
This commit is contained in:
@@ -31,6 +31,7 @@ interface AddProviderDialogProps {
|
||||
providerKey?: string;
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
@@ -116,6 +117,7 @@ export function AddProviderDialog({
|
||||
providerKey?: string;
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
} = {
|
||||
name: values.name.trim(),
|
||||
notes: values.notes?.trim() || undefined,
|
||||
@@ -137,6 +139,14 @@ export function AddProviderDialog({
|
||||
preset?.category === "official";
|
||||
}
|
||||
|
||||
if (appId === "codex" && values.presetId) {
|
||||
const presetIndex = parseInt(values.presetId.replace("codex-", ""));
|
||||
const preset = codexProviderPresets[presetIndex];
|
||||
providerData.ensureCodexOfficialSeed =
|
||||
values.presetCategory === "official" &&
|
||||
preset?.category === "official";
|
||||
}
|
||||
|
||||
// OpenCode/OpenClaw: pass providerKey for ID generation
|
||||
if (
|
||||
(appId === "opencode" || appId === "openclaw" || appId === "hermes") &&
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
isCodexAnthropicWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { supportsOfficialProxyTakeover } from "@/utils/providerCapabilities";
|
||||
import { useProviderHealth } from "@/lib/query/failover";
|
||||
import { useUsageQuery } from "@/lib/query/queries";
|
||||
|
||||
@@ -208,8 +209,14 @@ export function ProviderCard({
|
||||
// 并不兑现(绕过 UI 即可切换)→ 属虚保护,却以误伤 category 缺失的自定义供应商为代价。
|
||||
// 3) 预设导入的官方一定带 category="official",category 缺失的「真官方」现实中≈不存在。
|
||||
// 真官方就该有显式 category;手动新建官方应引导标注,而不是靠空字段猜。
|
||||
const supportsOfficialRouting = supportsOfficialProxyTakeover(
|
||||
appId,
|
||||
provider,
|
||||
);
|
||||
const isOfficialBlockedByProxy =
|
||||
isProxyTakeover && provider.category === "official";
|
||||
isProxyTakeover &&
|
||||
provider.category === "official" &&
|
||||
!supportsOfficialRouting;
|
||||
const isCopilot =
|
||||
provider.meta?.providerType === PROVIDER_TYPES.GITHUB_COPILOT ||
|
||||
provider.meta?.usage_script?.templateType === "github_copilot";
|
||||
@@ -405,14 +412,28 @@ export function ProviderCard({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{appId === "codex" && provider.category === "official" && (
|
||||
<span className="inline-flex items-center rounded-md bg-slate-200 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700 dark:bg-slate-700/60 dark:text-slate-200">
|
||||
{t("codex.noRoutingSupport", {
|
||||
defaultValue: "不支持路由",
|
||||
})}
|
||||
{appId === "codex" && supportsOfficialRouting && (
|
||||
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">
|
||||
{isProxyTakeover
|
||||
? t("codex.officialRouting", {
|
||||
defaultValue: "官方账号路由",
|
||||
})
|
||||
: t("codex.nativeLogin", {
|
||||
defaultValue: "Codex 登录",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{appId === "codex" &&
|
||||
provider.category === "official" &&
|
||||
!supportsOfficialRouting && (
|
||||
<span className="inline-flex items-center rounded-md bg-slate-200 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700 dark:bg-slate-700/60 dark:text-slate-200">
|
||||
{t("codex.noRoutingSupport", {
|
||||
defaultValue: "不支持路由",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isProxyRunning && isInFailoverQueue && health && (
|
||||
<ProviderHealthBadge
|
||||
consecutiveFailures={health.consecutive_failures}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
isCodexAnthropicWireApi,
|
||||
isCodexChatWireApi,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { supportsOfficialProxyTakeover } from "@/utils/providerCapabilities";
|
||||
|
||||
/**
|
||||
* Hook for managing provider actions (add, update, delete, switch)
|
||||
@@ -78,6 +79,7 @@ export function useProviderActions(
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const enhanced = injectCodingPlanUsageScript(activeApp, provider);
|
||||
@@ -236,8 +238,17 @@ export function useProviderActions(
|
||||
);
|
||||
}
|
||||
|
||||
// Block official providers when proxy takeover is active
|
||||
if (isProxyTakeover && provider.category === "official") {
|
||||
// The built-in Codex official provider can reuse Codex's native ChatGPT
|
||||
// login through local routing. Other official providers remain blocked.
|
||||
const officialSupportsTakeover = supportsOfficialProxyTakeover(
|
||||
activeApp,
|
||||
provider,
|
||||
);
|
||||
if (
|
||||
isProxyTakeover &&
|
||||
provider.category === "official" &&
|
||||
!officialSupportsTakeover
|
||||
) {
|
||||
toast.error(
|
||||
t("notifications.officialBlockedByProxy", {
|
||||
defaultValue:
|
||||
|
||||
@@ -181,7 +181,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "Needs Routing",
|
||||
"noRoutingSupport": "No Routing Support"
|
||||
"noRoutingSupport": "No Routing Support",
|
||||
"officialRouting": "Official Account Routing",
|
||||
"nativeLogin": "Codex Sign-in"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
"officialNotice": "Claude Desktop official providers use the built-in 1P sign-in. No API key or endpoint URL is required.",
|
||||
@@ -678,8 +680,8 @@
|
||||
"skipClaudeOnboarding": "Skip Claude Code first-run confirmation",
|
||||
"skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation",
|
||||
"codexAuth": "Codex App Enhancements",
|
||||
"preserveCodexOfficialAuthOnSwitch": "Keep official login when switching third-party providers",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "When enabled, you can use the Codex app's official plugins, mobile remote control, and other features while using third-party APIs.",
|
||||
"preserveCodexOfficialAuthOnSwitch": "Keep official login for direct switches",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "Controls third-party switches when local routing is off. Takeover routing always preserves the Codex official login.",
|
||||
"unifyCodexSessionHistory": "Unified Codex session history",
|
||||
"unifyCodexSessionHistoryDescription": "When enabled, the official subscription runs under the shared \"custom\" provider id so official and third-party sessions appear in one history list, optionally migrating existing official sessions in (backed up first). When turning it off, the migrated sessions can be restored from backup. Note: resuming an old session across providers may fail because its encrypted_content reasoning can only be decrypted by the backend that created it.",
|
||||
"unifyCodexHistoryRestoreCompleted": "Official session history restored from backup ({{files}} session files, {{rows}} index rows)",
|
||||
|
||||
@@ -181,7 +181,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "ルーティングが必要",
|
||||
"noRoutingSupport": "ルーティング非対応"
|
||||
"noRoutingSupport": "ルーティング非対応",
|
||||
"officialRouting": "公式アカウントルーティング",
|
||||
"nativeLogin": "Codex ログイン"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
"officialNotice": "Claude Desktop の公式プロバイダーは内蔵の 1P サインインを使用します。API キーやエンドポイントの設定は不要です。",
|
||||
@@ -678,8 +680,8 @@
|
||||
"skipClaudeOnboarding": "Claude Code の初回確認をスキップ",
|
||||
"skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします",
|
||||
"codexAuth": "Codex アプリ拡張",
|
||||
"preserveCodexOfficialAuthOnSwitch": "サードパーティ切替時に公式ログインを保持",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "オンにすると、サードパーティ API を使用する際に Codex アプリの公式プラグインやスマホからのリモート操作などの機能を利用できます。",
|
||||
"preserveCodexOfficialAuthOnSwitch": "直接切替時に公式ログインを保持",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "ローカルルーティングが無効な場合のサードパーティ切替を制御します。ルーティング接管中は常に Codex の公式ログインを保持します。",
|
||||
"unifyCodexSessionHistory": "Codex セッション履歴を統一",
|
||||
"unifyCodexSessionHistoryDescription": "オンにすると、公式サブスクリプションも共有の custom プロバイダー ID で動作し、公式とサードパーティのセッションが同じ履歴リストに表示されます。既存の公式セッションの移行も選択できます(移行前に自動バックアップ)。オフにする際はバックアップから復元できます。注意:プロバイダーをまたいで古いセッションを再開すると、encrypted_content の推論内容を相手のバックエンドが復号できず、再開に失敗する場合があります。",
|
||||
"unifyCodexHistoryRestoreCompleted": "バックアップから公式セッション履歴を復元しました(セッションファイル {{files}} 件、インデックス {{rows}} 行)",
|
||||
|
||||
@@ -181,7 +181,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "需要路由",
|
||||
"noRoutingSupport": "不支援路由"
|
||||
"noRoutingSupport": "不支援路由",
|
||||
"officialRouting": "官方帳號路由",
|
||||
"nativeLogin": "Codex 登入"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
"officialNotice": "Claude Desktop 官方供應商使用應用程式內建的 1P 登入,無需設定 API Key 和 API 位址。",
|
||||
@@ -678,8 +680,8 @@
|
||||
"skipClaudeOnboarding": "跳過 Claude Code 初次安裝確認",
|
||||
"skipClaudeOnboardingDescription": "開啟後跳過 Claude Code 初次安裝確認",
|
||||
"codexAuth": "Codex 應用增強",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切換第三方時保留官方登入",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "開啟後可以在使用第三方 API 的時候使用 Codex 應用的官方外掛、手機遠端操作等功能",
|
||||
"preserveCodexOfficialAuthOnSwitch": "非接管切換時保留官方登入",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "控制未開啟本機路由時切換第三方供應商是否保留 Codex 官方登入;路由接管期間一律保留",
|
||||
"unifyCodexSessionHistory": "統一 Codex 會話歷史",
|
||||
"unifyCodexSessionHistoryDescription": "開啟後,官方訂閱將以共享的 custom 供應商標識執行,官方與第三方會話出現在同一歷史清單中,並可選擇把現有官方會話一併遷入(遷移前自動備份)。關閉開關時可按備份恢復遷入的會話。注意:跨供應商繼續舊會話時,對方後端可能無法解密會話中的 encrypted_content 推理內容,導致繼續失敗",
|
||||
"unifyCodexHistoryRestoreCompleted": "已按備份還原官方會話歷史({{files}} 個會話檔案、{{rows}} 條索引記錄)",
|
||||
|
||||
@@ -181,7 +181,9 @@
|
||||
},
|
||||
"codex": {
|
||||
"needsRouting": "需要路由",
|
||||
"noRoutingSupport": "不支持路由"
|
||||
"noRoutingSupport": "不支持路由",
|
||||
"officialRouting": "官方账号路由",
|
||||
"nativeLogin": "Codex 登录"
|
||||
},
|
||||
"claudeDesktop": {
|
||||
"officialNotice": "Claude Desktop 官方供应商使用应用内置的 1P 登录,无需配置 API Key 和接口地址。",
|
||||
@@ -678,8 +680,8 @@
|
||||
"skipClaudeOnboarding": "跳过 Claude Code 初次安装确认",
|
||||
"skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认",
|
||||
"codexAuth": "Codex 应用增强",
|
||||
"preserveCodexOfficialAuthOnSwitch": "切换第三方时保留官方登录",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "开启后可以在使用第三方 API 的时候使用 Codex 应用的官方插件、手机远程操作等功能",
|
||||
"preserveCodexOfficialAuthOnSwitch": "非接管切换时保留官方登录",
|
||||
"preserveCodexOfficialAuthOnSwitchDescription": "控制未开启路由接管时切换第三方供应商是否保留 Codex 官方登录;路由接管期间始终保留",
|
||||
"unifyCodexSessionHistory": "统一 Codex 会话历史",
|
||||
"unifyCodexSessionHistoryDescription": "开启后,官方订阅将以共享的 custom 供应商标识运行,官方与第三方会话出现在同一历史列表中,并可选择把现有官方会话一并迁入(迁移前自动备份)。关闭开关时可按备份恢复迁入的会话。注意:跨供应商继续旧会话时,对方后端可能无法解密会话中的 encrypted_content 推理内容,导致继续失败",
|
||||
"unifyCodexHistoryRestoreCompleted": "已按备份还原官方会话历史({{files}} 个会话文件、{{rows}} 条索引记录)",
|
||||
|
||||
@@ -103,6 +103,10 @@ export const providersApi = {
|
||||
return await invoke("ensure_claude_desktop_official_provider");
|
||||
},
|
||||
|
||||
async ensureCodexOfficialProvider(): Promise<boolean> {
|
||||
return await invoke("ensure_codex_official_provider");
|
||||
},
|
||||
|
||||
async getClaudeDesktopStatus(): Promise<ClaudeDesktopStatus> {
|
||||
return await invoke("get_claude_desktop_status");
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { generateUUID } from "@/utils/uuid";
|
||||
import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
import { invalidateHermesProviderCaches } from "@/hooks/useHermes";
|
||||
import { usageKeys } from "@/lib/query/usage";
|
||||
import { CODEX_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities";
|
||||
|
||||
export const useAddProviderMutation = (appId: AppId) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -21,12 +22,14 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
providerKey?: string;
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
providerKey: _providerKey,
|
||||
addToLive,
|
||||
ensureClaudeDesktopOfficialSeed,
|
||||
ensureCodexOfficialSeed,
|
||||
...rest
|
||||
} = providerInput;
|
||||
|
||||
@@ -40,6 +43,16 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
return officialProvider;
|
||||
}
|
||||
|
||||
if (appId === "codex" && ensureCodexOfficialSeed) {
|
||||
await providersApi.ensureCodexOfficialProvider();
|
||||
const providers = await providersApi.getAll(appId);
|
||||
const officialProvider = providers[CODEX_OFFICIAL_PROVIDER_ID];
|
||||
if (!officialProvider) {
|
||||
throw new Error("Codex official provider was not created");
|
||||
}
|
||||
return officialProvider;
|
||||
}
|
||||
|
||||
let id: string;
|
||||
|
||||
if (appId === "opencode" || appId === "openclaw" || appId === "hermes") {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppId } from "@/lib/api";
|
||||
import type { Provider } from "@/types";
|
||||
|
||||
export const CODEX_OFFICIAL_PROVIDER_ID = "codex-official";
|
||||
|
||||
/** Keep the UI capability rule aligned with the Rust takeover policy. */
|
||||
export function supportsOfficialProxyTakeover(
|
||||
appId: AppId,
|
||||
provider: Pick<Provider, "id" | "category">,
|
||||
): boolean {
|
||||
return (
|
||||
appId === "codex" &&
|
||||
provider.id === CODEX_OFFICIAL_PROVIDER_ID &&
|
||||
provider.category === "official"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user