mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
feat(grokbuild): add Grok Official provider with official-state import
Add a "Grok Official" preset and seed (grokbuild-official) whose empty config represents the official login state: no custom [model.*] tables are written, so Grok CLI falls back to its built-in xAI OAuth login and cc-switch never touches those credentials. Backend: - Seed entry in OFFICIAL_SEEDS plus ensure_grokbuild_official_provider command for on-demand repair (the one-shot master seeding flag is already set for existing databases). - Split validation into syntax-only (empty allowed) for live reads, writes and official snapshots, keeping the full custom-model shape check for non-official provider writes and imports. Backup/restore can now round-trip an official-state live file. - Manual import (command layer only) recognizes an official-state live config and imports it as the official entry set as current, matching the Codex official-login import outcome. Startup auto-import keeps rejecting official-state live so a deleted official entry is never resurrected on launch: startup import only captures real user data as "default" and never manufactures official entries. - Manual import also ensures the official entry before importing (claude-desktop precedent) and after a successful custom import, so first-time users end up with default + official like other apps. - Proxy takeover guards skip or reject official-state live configs in all three takeover paths, consistent with the official-provider takeover ban. Frontend: - Grok Official preset entry in the GrokBuild form: official category hides connection fields and passes the raw config through untouched. - Filter managed-OAuth presets out of the GrokBuild preset list; they were never wired for this app and produced keyless broken configs. Tests cover seed presence, official round-trip, ensure-after-deletion, and the four import scenarios including startup non-resurrection.
This commit is contained in:
@@ -20,6 +20,7 @@ import { geminiProviderPresets } from "@/config/geminiProviderPresets";
|
||||
import { claudeDesktopProviderPresets } from "@/config/claudeDesktopProviderPresets";
|
||||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||||
import { extractGrokBuildBaseUrl } from "@/utils/grokBuildConfig";
|
||||
import { GROKBUILD_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities";
|
||||
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
|
||||
@@ -33,6 +34,7 @@ interface AddProviderDialogProps {
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
ensureGrokBuildOfficialSeed?: boolean;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
}
|
||||
@@ -120,6 +122,7 @@ export function AddProviderDialog({
|
||||
suggestedDefaults?: OpenClawSuggestedDefaults;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
ensureGrokBuildOfficialSeed?: boolean;
|
||||
} = {
|
||||
name: values.name.trim(),
|
||||
notes: values.notes?.trim() || undefined,
|
||||
@@ -149,6 +152,12 @@ export function AddProviderDialog({
|
||||
preset?.category === "official";
|
||||
}
|
||||
|
||||
if (appId === "grokbuild" && values.presetId) {
|
||||
providerData.ensureGrokBuildOfficialSeed =
|
||||
values.presetCategory === "official" &&
|
||||
values.presetId === GROKBUILD_OFFICIAL_PROVIDER_ID;
|
||||
}
|
||||
|
||||
// OpenCode/OpenClaw: pass providerKey for ID generation
|
||||
if (
|
||||
(appId === "opencode" || appId === "openclaw" || appId === "hermes") &&
|
||||
|
||||
@@ -49,15 +49,41 @@ import {
|
||||
validateGrokBuildConfig,
|
||||
} from "@/utils/grokBuildConfig";
|
||||
import { resolveProviderIcon } from "@/utils/providerIcon";
|
||||
import { GROKBUILD_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities";
|
||||
|
||||
type GrokBuildProviderFormProps = Omit<ProviderFormProps, "appId">;
|
||||
|
||||
// 官方条目与后端 seed(providers_seed.rs 的 "Grok Official")对应:
|
||||
// 空 config = 不写自定义模型表,Grok CLI 回落到自带的 xAI OAuth 登录。
|
||||
// 预设 id 复用固定 provider id,AddProviderDialog 据此走 ensure seed 流程。
|
||||
const grokOfficialPreset: CodexProviderPreset = {
|
||||
name: "Grok Official",
|
||||
websiteUrl: "https://x.ai/grok",
|
||||
isOfficial: true,
|
||||
category: "official",
|
||||
auth: {},
|
||||
config: "",
|
||||
icon: "grok",
|
||||
iconColor: "currentColor",
|
||||
};
|
||||
|
||||
const grokPresetEntries: Array<{
|
||||
id: string;
|
||||
preset: CodexProviderPreset;
|
||||
}> = codexProviderPresets
|
||||
.map((preset, index) => ({ id: `grokbuild-${index}`, preset }))
|
||||
.filter(({ preset }) => preset.category !== "official" && !preset.isOfficial);
|
||||
}> = [
|
||||
{ id: GROKBUILD_OFFICIAL_PROVIDER_ID, preset: grokOfficialPreset },
|
||||
...codexProviderPresets
|
||||
.map((preset, index) => ({ id: `grokbuild-${index}`, preset }))
|
||||
.filter(
|
||||
// 托管 OAuth 预设(如 "xAI (Grok) OAuth")只在 Codex 表单接线;
|
||||
// Grok CLI 原生支持订阅登录,这里选中只会产出无 key 的坏配置,故排除。
|
||||
({ preset }) =>
|
||||
preset.category !== "official" &&
|
||||
!preset.isOfficial &&
|
||||
!preset.providerType &&
|
||||
!preset.requiresOAuth,
|
||||
),
|
||||
];
|
||||
|
||||
export const grokApiBackendFromApiFormat = (format: CodexApiFormat): string => {
|
||||
if (format === "openai_chat") return "chat_completions";
|
||||
@@ -233,6 +259,20 @@ export function GrokBuildProviderForm({
|
||||
return;
|
||||
}
|
||||
|
||||
if (presetId === GROKBUILD_OFFICIAL_PROVIDER_ID) {
|
||||
// 官方登录:无 API Key / 地址 / 模型表可填,提交走 ensure seed 流程
|
||||
form.setValue("name", grokOfficialPreset.name);
|
||||
form.setValue("websiteUrl", grokOfficialPreset.websiteUrl);
|
||||
form.setValue("icon", grokOfficialPreset.icon ?? "");
|
||||
form.setValue("iconColor", grokOfficialPreset.iconColor ?? "");
|
||||
setCategory("official");
|
||||
setIsPartner(false);
|
||||
setPartnerPromotionKey(undefined);
|
||||
setPresetEndpoints([]);
|
||||
setRawConfig("");
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = grokPresetEntries.find(
|
||||
(candidate) => candidate.id === presetId,
|
||||
);
|
||||
@@ -292,6 +332,24 @@ export function GrokBuildProviderForm({
|
||||
|
||||
const handleSubmit = async (values: ProviderFormData) => {
|
||||
const name = values.name.trim();
|
||||
|
||||
// 官方条目:config 快照原样透传(新增时为空),不做自定义模型字段校验,
|
||||
// 也不重建 config —— 新增走 ensure seed,编辑只允许改名称/图标等元信息。
|
||||
if (category === "official") {
|
||||
await onSubmit({
|
||||
...values,
|
||||
name,
|
||||
websiteUrl: values.websiteUrl?.trim() ?? "",
|
||||
notes: values.notes?.trim() ?? "",
|
||||
settingsConfig: JSON.stringify({ config: rawConfig }),
|
||||
presetId: selectedPresetId ?? undefined,
|
||||
presetCategory: "official",
|
||||
isPartner: false,
|
||||
meta: initialData?.meta,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedContextWindow = Number.parseInt(contextWindow, 10);
|
||||
const envKey = parseGrokBuildConfig(rawConfig).envKey?.trim();
|
||||
if (
|
||||
@@ -411,141 +469,145 @@ export function GrokBuildProviderForm({
|
||||
|
||||
<BasicFormFields form={form} />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-profile">
|
||||
{t("grokBuild.profile", { defaultValue: "客户端模型档位" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-profile"
|
||||
value={profile}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setProfile(value);
|
||||
syncStructuredConfig({ model: value });
|
||||
{category !== "official" && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-profile">
|
||||
{t("grokBuild.profile", { defaultValue: "客户端模型档位" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-profile"
|
||||
value={profile}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setProfile(value);
|
||||
syncStructuredConfig({ model: value });
|
||||
}}
|
||||
placeholder="grok-4.5"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-api-backend">
|
||||
{t("grokBuild.apiBackend", { defaultValue: "API Backend" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-api-backend"
|
||||
value={apiBackend}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setApiBackend(value);
|
||||
syncStructuredConfig({ apiBackend: value });
|
||||
}}
|
||||
placeholder="responses"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-context-window">
|
||||
{t("grokBuild.contextWindow", { defaultValue: "上下文窗口" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-context-window"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={contextWindow}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setContextWindow(value);
|
||||
syncStructuredConfig({
|
||||
contextWindow: Number.parseInt(value, 10),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<CodexFormFields
|
||||
appId="grokbuild"
|
||||
providerId={providerId}
|
||||
codexApiKey={apiKey}
|
||||
onApiKeyChange={(value) => {
|
||||
setApiKey(value);
|
||||
syncStructuredConfig({ apiKey: value });
|
||||
}}
|
||||
placeholder="grok-4.5"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-api-backend">
|
||||
{t("grokBuild.apiBackend", { defaultValue: "API Backend" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-api-backend"
|
||||
value={apiBackend}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setApiBackend(value);
|
||||
syncStructuredConfig({ apiBackend: value });
|
||||
category={category}
|
||||
shouldShowApiKeyLink={Boolean(websiteUrl)}
|
||||
websiteUrl={websiteUrl}
|
||||
isPartner={isPartner}
|
||||
partnerPromotionKey={partnerPromotionKey}
|
||||
shouldShowSpeedTest
|
||||
codexBaseUrl={baseUrl}
|
||||
onBaseUrlChange={(value) => {
|
||||
setBaseUrl(value);
|
||||
syncStructuredConfig({ baseUrl: value });
|
||||
}}
|
||||
placeholder="responses"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel htmlFor="grokbuild-context-window">
|
||||
{t("grokBuild.contextWindow", { defaultValue: "上下文窗口" })}
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="grokbuild-context-window"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={contextWindow}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setContextWindow(value);
|
||||
syncStructuredConfig({
|
||||
contextWindow: Number.parseInt(value, 10),
|
||||
});
|
||||
isFullUrl={isFullUrl}
|
||||
onFullUrlChange={setIsFullUrl}
|
||||
isEndpointModalOpen={isEndpointModalOpen}
|
||||
onEndpointModalToggle={setIsEndpointModalOpen}
|
||||
onCustomEndpointsChange={setDraftCustomEndpoints}
|
||||
autoSelect={endpointAutoSelect}
|
||||
onAutoSelectChange={setEndpointAutoSelect}
|
||||
codexModel={upstreamModel}
|
||||
onModelChange={(value) => {
|
||||
setUpstreamModel(value);
|
||||
syncStructuredConfig({ upstreamModel: value });
|
||||
}}
|
||||
apiFormat={apiFormat}
|
||||
onApiFormatChange={(value) => {
|
||||
const backend = grokApiBackendFromApiFormat(value);
|
||||
setApiFormat(value);
|
||||
setApiBackend(backend);
|
||||
syncStructuredConfig({ apiBackend: backend });
|
||||
}}
|
||||
anthropicAuthField={anthropicAuthField}
|
||||
onAnthropicAuthFieldChange={setAnthropicAuthField}
|
||||
impersonateClaudeCode={impersonateClaudeCode}
|
||||
onImpersonateClaudeCodeChange={setImpersonateClaudeCode}
|
||||
maxOutputTokens={maxOutputTokens}
|
||||
onMaxOutputTokensChange={setMaxOutputTokens}
|
||||
codexChatReasoning={codexChatReasoning}
|
||||
onCodexChatReasoningChange={setCodexChatReasoning}
|
||||
promptCacheRouting={promptCacheRouting}
|
||||
onPromptCacheRoutingChange={setPromptCacheRouting}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
customUserAgent={customUserAgent}
|
||||
onCustomUserAgentChange={setCustomUserAgent}
|
||||
localProxyHeadersOverride={headersOverride}
|
||||
onLocalProxyHeadersOverrideChange={setHeadersOverride}
|
||||
localProxyBodyOverride={bodyOverride}
|
||||
onLocalProxyBodyOverrideChange={setBodyOverride}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<CodexFormFields
|
||||
appId="grokbuild"
|
||||
providerId={providerId}
|
||||
codexApiKey={apiKey}
|
||||
onApiKeyChange={(value) => {
|
||||
setApiKey(value);
|
||||
syncStructuredConfig({ apiKey: value });
|
||||
}}
|
||||
category={category}
|
||||
shouldShowApiKeyLink={Boolean(websiteUrl)}
|
||||
websiteUrl={websiteUrl}
|
||||
isPartner={isPartner}
|
||||
partnerPromotionKey={partnerPromotionKey}
|
||||
shouldShowSpeedTest
|
||||
codexBaseUrl={baseUrl}
|
||||
onBaseUrlChange={(value) => {
|
||||
setBaseUrl(value);
|
||||
syncStructuredConfig({ baseUrl: value });
|
||||
}}
|
||||
isFullUrl={isFullUrl}
|
||||
onFullUrlChange={setIsFullUrl}
|
||||
isEndpointModalOpen={isEndpointModalOpen}
|
||||
onEndpointModalToggle={setIsEndpointModalOpen}
|
||||
onCustomEndpointsChange={setDraftCustomEndpoints}
|
||||
autoSelect={endpointAutoSelect}
|
||||
onAutoSelectChange={setEndpointAutoSelect}
|
||||
codexModel={upstreamModel}
|
||||
onModelChange={(value) => {
|
||||
setUpstreamModel(value);
|
||||
syncStructuredConfig({ upstreamModel: value });
|
||||
}}
|
||||
apiFormat={apiFormat}
|
||||
onApiFormatChange={(value) => {
|
||||
const backend = grokApiBackendFromApiFormat(value);
|
||||
setApiFormat(value);
|
||||
setApiBackend(backend);
|
||||
syncStructuredConfig({ apiBackend: backend });
|
||||
}}
|
||||
anthropicAuthField={anthropicAuthField}
|
||||
onAnthropicAuthFieldChange={setAnthropicAuthField}
|
||||
impersonateClaudeCode={impersonateClaudeCode}
|
||||
onImpersonateClaudeCodeChange={setImpersonateClaudeCode}
|
||||
maxOutputTokens={maxOutputTokens}
|
||||
onMaxOutputTokensChange={setMaxOutputTokens}
|
||||
codexChatReasoning={codexChatReasoning}
|
||||
onCodexChatReasoningChange={setCodexChatReasoning}
|
||||
promptCacheRouting={promptCacheRouting}
|
||||
onPromptCacheRoutingChange={setPromptCacheRouting}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
customUserAgent={customUserAgent}
|
||||
onCustomUserAgentChange={setCustomUserAgent}
|
||||
localProxyHeadersOverride={headersOverride}
|
||||
onLocalProxyHeadersOverrideChange={setHeadersOverride}
|
||||
localProxyBodyOverride={bodyOverride}
|
||||
onLocalProxyBodyOverrideChange={setBodyOverride}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="grokbuild-config-toml">
|
||||
{t("grokBuild.rawConfig", { defaultValue: "config.toml" })}
|
||||
</FormLabel>
|
||||
<JsonEditor
|
||||
value={rawConfig}
|
||||
onChange={handleRawConfigChange}
|
||||
placeholder=""
|
||||
darkMode={isDarkMode}
|
||||
rows={12}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
{rawConfigError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{t("grokBuild.invalidToml", {
|
||||
error: rawConfigError,
|
||||
defaultValue: `Invalid config.toml: ${rawConfigError}`,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="grokbuild-config-toml">
|
||||
{t("grokBuild.rawConfig", { defaultValue: "config.toml" })}
|
||||
</FormLabel>
|
||||
<JsonEditor
|
||||
value={rawConfig}
|
||||
onChange={handleRawConfigChange}
|
||||
placeholder=""
|
||||
darkMode={isDarkMode}
|
||||
rows={12}
|
||||
showValidation={false}
|
||||
language="javascript"
|
||||
/>
|
||||
{rawConfigError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{t("grokBuild.invalidToml", {
|
||||
error: rawConfigError,
|
||||
defaultValue: `Invalid config.toml: ${rawConfigError}`,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -84,6 +84,7 @@ export function useProviderActions(
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
ensureGrokBuildOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const enhanced = injectCodingPlanUsageScript(activeApp, provider);
|
||||
|
||||
@@ -107,6 +107,10 @@ export const providersApi = {
|
||||
return await invoke("ensure_codex_official_provider");
|
||||
},
|
||||
|
||||
async ensureGrokBuildOfficialProvider(): Promise<boolean> {
|
||||
return await invoke("ensure_grokbuild_official_provider");
|
||||
},
|
||||
|
||||
async getClaudeDesktopStatus(): Promise<ClaudeDesktopStatus> {
|
||||
return await invoke("get_claude_desktop_status");
|
||||
},
|
||||
|
||||
@@ -10,7 +10,10 @@ 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";
|
||||
import {
|
||||
CODEX_OFFICIAL_PROVIDER_ID,
|
||||
GROKBUILD_OFFICIAL_PROVIDER_ID,
|
||||
} from "@/utils/providerCapabilities";
|
||||
|
||||
export const useAddProviderMutation = (appId: AppId) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -23,6 +26,7 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
addToLive?: boolean;
|
||||
ensureClaudeDesktopOfficialSeed?: boolean;
|
||||
ensureCodexOfficialSeed?: boolean;
|
||||
ensureGrokBuildOfficialSeed?: boolean;
|
||||
},
|
||||
) => {
|
||||
const {
|
||||
@@ -30,6 +34,7 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
addToLive,
|
||||
ensureClaudeDesktopOfficialSeed,
|
||||
ensureCodexOfficialSeed,
|
||||
ensureGrokBuildOfficialSeed,
|
||||
...rest
|
||||
} = providerInput;
|
||||
|
||||
@@ -53,6 +58,16 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
return officialProvider;
|
||||
}
|
||||
|
||||
if (appId === "grokbuild" && ensureGrokBuildOfficialSeed) {
|
||||
await providersApi.ensureGrokBuildOfficialProvider();
|
||||
const providers = await providersApi.getAll(appId);
|
||||
const officialProvider = providers[GROKBUILD_OFFICIAL_PROVIDER_ID];
|
||||
if (!officialProvider) {
|
||||
throw new Error("Grok Build official provider was not created");
|
||||
}
|
||||
return officialProvider;
|
||||
}
|
||||
|
||||
let id: string;
|
||||
|
||||
if (appId === "opencode" || appId === "openclaw" || appId === "hermes") {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/utils/providerConfigUtils";
|
||||
|
||||
export const CODEX_OFFICIAL_PROVIDER_ID = "codex-official";
|
||||
export const GROKBUILD_OFFICIAL_PROVIDER_ID = "grokbuild-official";
|
||||
|
||||
/** Keep the UI capability rule aligned with the Rust takeover policy. */
|
||||
export function supportsOfficialProxyTakeover(
|
||||
|
||||
Reference in New Issue
Block a user