mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 10:18:42 +00:00
Fix ChatGPT OAuth setup wizard flow (#289)
This commit is contained in:
1 parent
c1893c73d1
commit
43990c7e56
4 files changed
+69
-17
No files matched your search
@@ -18,6 +18,7 @@
|
||||
"provider": {
|
||||
"title": "Configure LLM Provider",
|
||||
"descriptionCli": "Connect using your local Claude CLI installation. No API key needed.",
|
||||
"descriptionOauth": "Sign in with your ChatGPT account to use your subscription through OAuth. No API key needed.",
|
||||
"description": "Connect to an AI provider to power your agents. You'll need an API key.",
|
||||
"providerType": "Provider Type",
|
||||
"providerTypeHint": "The LLM service you want to connect. OpenRouter is recommended for access to multiple models.",
|
||||
@@ -31,6 +32,7 @@
|
||||
"create": "Create Provider",
|
||||
"errors": {
|
||||
"apiKeyRequired": "API key is required",
|
||||
"oauthProviderNotFound": "OAuth authentication succeeded, but the provider could not be found. Please try again.",
|
||||
"failedCreate": "Failed to create provider"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"provider": {
|
||||
"title": "Cấu hình provider LLM",
|
||||
"descriptionCli": "Kết nối bằng Claude CLI cục bộ. Không cần API key.",
|
||||
"descriptionOauth": "Đăng nhập bằng tài khoản ChatGPT để dùng gói thuê bao qua OAuth. Không cần API key.",
|
||||
"description": "Kết nối với provider AI để cấp nguồn cho agent. Bạn cần có API key.",
|
||||
"providerType": "Loại provider",
|
||||
"providerTypeHint": "Dịch vụ LLM bạn muốn kết nối. Khuyến nghị dùng OpenRouter để truy cập nhiều model.",
|
||||
@@ -31,6 +32,7 @@
|
||||
"create": "Tạo provider",
|
||||
"errors": {
|
||||
"apiKeyRequired": "API key là bắt buộc",
|
||||
"oauthProviderNotFound": "Xác thực OAuth thành công nhưng không tìm thấy provider. Vui lòng thử lại.",
|
||||
"failedCreate": "Không thể tạo provider"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,12 @@ interface StartResponse {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function OAuthSection({ onSuccess }: { onSuccess: () => void }) {
|
||||
interface OAuthSectionProps {
|
||||
onSuccess: () => void;
|
||||
authenticatedActionLabel?: string;
|
||||
}
|
||||
|
||||
export function OAuthSection({ onSuccess, authenticatedActionLabel }: OAuthSectionProps) {
|
||||
const { t } = useTranslation("providers");
|
||||
const http = useHttp();
|
||||
const [status, setStatus] = useState<OAuthStatus | null>(null);
|
||||
@@ -176,9 +181,16 @@ export function OAuthSection({ onSuccess }: { onSuccess: () => void }) {
|
||||
{t("oauth.modelPrefixHint")} <code className="rounded bg-muted px-1 font-mono">openai-codex/</code>{" "}
|
||||
{t("oauth.modelPrefixExample")}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" onClick={handleLogout} className="gap-1.5">
|
||||
{t("oauth.removeToken")}
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{authenticatedActionLabel && (
|
||||
<Button size="sm" onClick={onSuccess}>
|
||||
{authenticatedActionLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={handleLogout} className="gap-1.5">
|
||||
{t("oauth.removeToken")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -6,6 +6,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { InfoTip } from "@/pages/setup/info-tip";
|
||||
import { useHttp } from "@/hooks/use-ws";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { PROVIDER_TYPES } from "@/constants/providers";
|
||||
import { useProviders } from "@/pages/providers/hooks/use-providers";
|
||||
import { CLISection } from "@/pages/providers/provider-cli-section";
|
||||
import { OAuthSection } from "@/pages/providers/provider-oauth-section";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import type { ProviderData, ProviderInput } from "@/types/provider";
|
||||
|
||||
@@ -26,6 +28,7 @@ interface StepProviderProps {
|
||||
|
||||
export function StepProvider({ onComplete, existingProvider }: StepProviderProps) {
|
||||
const { t } = useTranslation("setup");
|
||||
const http = useHttp();
|
||||
const { createProvider, updateProvider } = useProviders();
|
||||
|
||||
const isEditing = !!existingProvider;
|
||||
@@ -39,6 +42,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const isOAuth = providerType === "chatgpt_oauth";
|
||||
const isCLI = providerType === "claude_cli";
|
||||
// Local Ollama uses no API key — the server accepts any non-empty Bearer value internally
|
||||
const isOllama = providerType === "ollama";
|
||||
@@ -46,7 +50,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
const handleTypeChange = (value: string) => {
|
||||
setProviderType(value);
|
||||
const preset = PROVIDER_TYPES.find((t) => t.value === value);
|
||||
setName(slugify(value));
|
||||
setName(value === "chatgpt_oauth" ? "openai-codex" : slugify(value));
|
||||
setApiBase(preset?.apiBase || "");
|
||||
setApiKey("");
|
||||
setError("");
|
||||
@@ -59,7 +63,26 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
[providerType],
|
||||
);
|
||||
|
||||
const handleOAuthSuccess = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await http.get<{ providers: ProviderData[] }>("/v1/providers");
|
||||
const provider = res.providers?.find((p) => p.provider_type === "chatgpt_oauth" && p.name === "openai-codex");
|
||||
if (!provider) {
|
||||
setError(t("provider.errors.oauthProviderNotFound"));
|
||||
return;
|
||||
}
|
||||
onComplete(provider);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t("provider.errors.oauthProviderNotFound"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isOAuth) return;
|
||||
if (!isEditing && !isCLI && !isOllama && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
|
||||
setLoading(true);
|
||||
setError("");
|
||||
@@ -79,7 +102,7 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
name: name.trim(),
|
||||
provider_type: providerType,
|
||||
api_base: apiBase.trim() || undefined,
|
||||
api_key: isCLI || isOllama ? undefined : apiKey.trim(),
|
||||
api_key: isCLI || isOllama || isOAuth ? undefined : apiKey.trim(),
|
||||
enabled: true,
|
||||
}) as ProviderData;
|
||||
onComplete(provider);
|
||||
@@ -98,7 +121,9 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">{t("provider.title")}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isCLI
|
||||
{isOAuth
|
||||
? t("provider.descriptionOauth")
|
||||
: isCLI
|
||||
? t("provider.descriptionCli")
|
||||
: t("provider.description")}
|
||||
</p>
|
||||
@@ -124,11 +149,20 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
{t("provider.name")}
|
||||
<InfoTip text={t("provider.nameHint")} />
|
||||
</Label>
|
||||
<Input value={name} onChange={(e) => setName(slugify(e.target.value))} />
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(slugify(e.target.value))}
|
||||
disabled={isOAuth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCLI ? (
|
||||
{isOAuth ? (
|
||||
<OAuthSection
|
||||
onSuccess={handleOAuthSuccess}
|
||||
authenticatedActionLabel={t("model.continue")}
|
||||
/>
|
||||
) : isCLI ? (
|
||||
<CLISection open={true} />
|
||||
) : (
|
||||
<>
|
||||
@@ -161,13 +195,15 @@ export function StepProvider({ onComplete, existingProvider }: StepProviderProps
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit} disabled={loading || (!isEditing && !isCLI && !isOllama && !apiKey.trim())}>
|
||||
{loading
|
||||
? isEditing ? t("provider.updating", "Updating...") : t("provider.creating")
|
||||
: isEditing ? t("provider.update", "Update") : t("provider.create")}
|
||||
</Button>
|
||||
</div>
|
||||
{!isOAuth && (
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmit} disabled={loading || (!isEditing && !isCLI && !isOllama && !apiKey.trim())}>
|
||||
{loading
|
||||
? isEditing ? t("provider.updating", "Updating...") : t("provider.creating")
|
||||
: isEditing ? t("provider.update", "Update") : t("provider.create")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in new issue
Block a user