mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
Add ChatGPT Web setup and doctor UI
This commit is contained in:
@@ -93,6 +93,7 @@ export default function AddApiKeyModal({
|
||||
const localProviderMetadata = getLocalProviderMetadata(provider);
|
||||
const isLocalSelfHostedProvider = !!localProviderMetadata;
|
||||
const isGooglePse = provider === "google-pse-search";
|
||||
const isChatGptWebCodex = provider === "chatgpt-web-codex";
|
||||
const webSessionCredential = getWebSessionCredentialRequirement(provider);
|
||||
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
|
||||
const isWebSessionCredential = !!webSessionCredential && webSessionCredential.kind !== "none";
|
||||
@@ -130,9 +131,16 @@ export default function AddApiKeyModal({
|
||||
ccCompatibleSummarizeThinking: false,
|
||||
passthroughModels: false,
|
||||
importFreeModelsOnly: false,
|
||||
tunnelId: "",
|
||||
runtimeKey: "",
|
||||
connectorName: "OmniRoute Codex",
|
||||
});
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [validationCapabilities, setValidationCapabilities] = useState<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
@@ -233,12 +241,18 @@ export default function AddApiKeyModal({
|
||||
baseUrl: formData.baseUrl.trim() || undefined,
|
||||
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
|
||||
cx: formData.cx.trim() || undefined,
|
||||
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
|
||||
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
|
||||
connectorName: isChatGptWebCodex ? formData.connectorName.trim() || undefined : undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const ok = !!data.valid;
|
||||
const unsupported = !!data.unsupported;
|
||||
setValidationResult(ok ? "success" : unsupported ? "unsupported" : "failed");
|
||||
setValidationCapabilities(
|
||||
ok && data.capabilities && typeof data.capabilities === "object" ? data.capabilities : null
|
||||
);
|
||||
// #5088: surface backend reason (e.g. TLS/EACCES) instead of bare "invalid".
|
||||
if (!ok && !unsupported && typeof data.error === "string" && data.error) {
|
||||
setSaveError(data.error);
|
||||
@@ -285,6 +299,7 @@ export default function AddApiKeyModal({
|
||||
let isValid = Boolean(isNoAuthWebSessionCredential && !credentialInput);
|
||||
let validationError: string | null = null;
|
||||
let isUnsupported = false; // #5565/#5567: no live validator → save anyway
|
||||
let validatedProviderSpecificData: Record<string, unknown> | undefined;
|
||||
if (!isValid) {
|
||||
try {
|
||||
setValidating(true);
|
||||
@@ -300,6 +315,11 @@ export default function AddApiKeyModal({
|
||||
baseUrl: formData.baseUrl.trim() || undefined,
|
||||
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
|
||||
cx: formData.cx.trim() || undefined,
|
||||
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
|
||||
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
|
||||
connectorName: isChatGptWebCodex
|
||||
? formData.connectorName.trim() || undefined
|
||||
: undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -308,6 +328,13 @@ export default function AddApiKeyModal({
|
||||
if (!isValid && data.error) {
|
||||
validationError = data.error;
|
||||
}
|
||||
if (
|
||||
isValid &&
|
||||
data.providerSpecificData &&
|
||||
typeof data.providerSpecificData === "object"
|
||||
) {
|
||||
validatedProviderSpecificData = data.providerSpecificData;
|
||||
}
|
||||
setValidationResult(isValid ? "success" : isUnsupported ? "unsupported" : "failed");
|
||||
} catch {
|
||||
setValidationResult("failed");
|
||||
@@ -339,14 +366,28 @@ export default function AddApiKeyModal({
|
||||
isCloudflare,
|
||||
isCcCompatible,
|
||||
});
|
||||
const mergedProviderSpecificData = {
|
||||
...(providerSpecificData || {}),
|
||||
...(validatedProviderSpecificData || {}),
|
||||
};
|
||||
|
||||
const encodedCredential = isChatGptWebCodex
|
||||
? JSON.stringify({
|
||||
version: 1,
|
||||
cookie: credentialInput.trim().replace(/^cookie\s*:\s*/i, ""),
|
||||
runtimeKey: formData.runtimeKey.trim(),
|
||||
})
|
||||
: credentialInput.trim();
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
apiKey: credentialInput.trim() || undefined,
|
||||
apiKey: encodedCredential || undefined,
|
||||
priority: formData.priority,
|
||||
testStatus: "active",
|
||||
defaultModel: isCompatible ? formData.defaultModel.trim() || undefined : undefined,
|
||||
providerSpecificData,
|
||||
providerSpecificData:
|
||||
Object.keys(mergedProviderSpecificData).length > 0
|
||||
? mergedProviderSpecificData
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const error = await onSave(payload);
|
||||
@@ -736,6 +777,59 @@ export default function AddApiKeyModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isChatGptWebCodex && (
|
||||
<div className="space-y-3 rounded-lg border border-border bg-surface/40 p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-main">Codex-Toolverbindung</p>
|
||||
<p className="mt-1 text-xs text-text-muted">
|
||||
Der Tunnel bleibt ausschließlich ausgehend. Lokale Tools werden weiterhin nur
|
||||
von Codex gemäß dessen Sandbox- und Freigaberichtlinie ausgeführt.
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
label="Tunnel-ID"
|
||||
value={formData.tunnelId}
|
||||
onChange={(e) => setFormData({ ...formData, tunnelId: e.target.value })}
|
||||
placeholder="tunnel_0123456789abcdef0123456789abcdef"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<Input
|
||||
label="Tunnel Runtime-Key"
|
||||
type="password"
|
||||
value={formData.runtimeKey}
|
||||
onChange={(e) => setFormData({ ...formData, runtimeKey: e.target.value })}
|
||||
placeholder="Runtime-Key"
|
||||
hint="Wird zusammen mit dem Cookie verschlüsselt gespeichert und nie in Logs ausgegeben."
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<Input
|
||||
label="ChatGPT-Custom-Connector"
|
||||
value={formData.connectorName}
|
||||
onChange={(e) => setFormData({ ...formData, connectorName: e.target.value })}
|
||||
placeholder="OmniRoute Codex"
|
||||
/>
|
||||
{validationCapabilities && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-text-muted">
|
||||
<div>Browser: bereit</div>
|
||||
<div>Storage-State: geprüft</div>
|
||||
<div>ChatGPT-Anmeldung: bestätigt</div>
|
||||
<div>Temporary Chat: bereit</div>
|
||||
<div>
|
||||
Pro:{" "}
|
||||
{validationCapabilities.proAvailable === true ? "verfügbar" : "nicht erkannt"}
|
||||
</div>
|
||||
<div>
|
||||
Toolmodus:{" "}
|
||||
{formData.tunnelId.trim() && formData.runtimeKey.trim()
|
||||
? "konfiguriert"
|
||||
: "global oder read-only"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isModal && (
|
||||
<Input
|
||||
label={providerText(t, "modalTokenSecretLabel", "Token Secret")}
|
||||
|
||||
@@ -143,12 +143,20 @@ export default function EditConnectionModal({
|
||||
passthroughModels: connectionProviderSpecificData?.passthroughModels === true,
|
||||
disableCooling: connectionProviderSpecificData?.disableCooling === true,
|
||||
importFreeModelsOnly: connectionProviderSpecificData?.importFreeModelsOnly === true,
|
||||
tunnelId: stringField(connectionProviderSpecificData?.tunnelId),
|
||||
runtimeKey: "",
|
||||
connectorName: stringField(connectionProviderSpecificData?.connectorName) || "OmniRoute Codex",
|
||||
m365Tier: normalizeM365TierValue(connectionProviderSpecificData?.tier) as M365TierValue,
|
||||
});
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [validatedProviderSpecificData, setValidatedProviderSpecificData] = useState<
|
||||
Record<string, unknown> | undefined
|
||||
>();
|
||||
const [doctorStatus, setDoctorStatus] = useState<Record<string, any> | null>(null);
|
||||
const [doctorLoading, setDoctorLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [extraApiKeys, setExtraApiKeys] = useState<string[]>([]);
|
||||
@@ -196,6 +204,7 @@ export default function EditConnectionModal({
|
||||
const localProviderMetadata = getLocalProviderMetadata(provider);
|
||||
const isLocalSelfHostedProvider = !!localProviderMetadata;
|
||||
const isGooglePse = provider === "google-pse-search";
|
||||
const isChatGptWebCodex = provider === "chatgpt-web-codex";
|
||||
const isM365TierCapable = isM365TierCapableProvider(provider);
|
||||
const webSessionCredential = getWebSessionCredentialRequirement(provider);
|
||||
const isNoAuthWebSessionCredential = webSessionCredential?.kind === "none";
|
||||
@@ -334,6 +343,10 @@ export default function EditConnectionModal({
|
||||
passthroughModels: connection?.providerSpecificData?.passthroughModels === true,
|
||||
disableCooling: connection?.providerSpecificData?.disableCooling === true,
|
||||
importFreeModelsOnly: connection?.providerSpecificData?.importFreeModelsOnly === true,
|
||||
tunnelId: stringField(connection.providerSpecificData?.tunnelId),
|
||||
runtimeKey: "",
|
||||
connectorName:
|
||||
stringField(connection.providerSpecificData?.connectorName) || "OmniRoute Codex",
|
||||
m365Tier: normalizeM365TierValue(connection.providerSpecificData?.tier) as M365TierValue,
|
||||
});
|
||||
const existing = connection.providerSpecificData?.extraApiKeys;
|
||||
@@ -359,6 +372,7 @@ export default function EditConnectionModal({
|
||||
);
|
||||
setTestResult(null);
|
||||
setValidationResult(null);
|
||||
setValidatedProviderSpecificData(undefined);
|
||||
setSaveError(null);
|
||||
}
|
||||
}, [
|
||||
@@ -422,10 +436,20 @@ export default function EditConnectionModal({
|
||||
baseUrl: formData.baseUrl.trim() || undefined,
|
||||
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
|
||||
cx: formData.cx.trim() || undefined,
|
||||
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
|
||||
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
|
||||
connectorName: isChatGptWebCodex ? formData.connectorName.trim() || undefined : undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data.valid ? "success" : "failed");
|
||||
if (
|
||||
data.valid &&
|
||||
data.providerSpecificData &&
|
||||
typeof data.providerSpecificData === "object"
|
||||
) {
|
||||
setValidatedProviderSpecificData(data.providerSpecificData);
|
||||
}
|
||||
} catch {
|
||||
setValidationResult("failed");
|
||||
} finally {
|
||||
@@ -502,7 +526,7 @@ export default function EditConnectionModal({
|
||||
}
|
||||
|
||||
if (!isOAuth && formData.apiKey) {
|
||||
updates.apiKey = formData.apiKey;
|
||||
let validationPsd = validatedProviderSpecificData;
|
||||
let isValid = validationResult === "success";
|
||||
if (!isValid) {
|
||||
try {
|
||||
@@ -519,11 +543,24 @@ export default function EditConnectionModal({
|
||||
baseUrl: formData.baseUrl.trim() || undefined,
|
||||
region: showsRegion ? formData.region.trim() || defaultRegion : undefined,
|
||||
cx: formData.cx.trim() || undefined,
|
||||
runtimeKey: isChatGptWebCodex ? formData.runtimeKey.trim() || undefined : undefined,
|
||||
tunnelId: isChatGptWebCodex ? formData.tunnelId.trim() || undefined : undefined,
|
||||
connectorName: isChatGptWebCodex
|
||||
? formData.connectorName.trim() || undefined
|
||||
: undefined,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
isValid = !!data.valid;
|
||||
setValidationResult(isValid ? "success" : "failed");
|
||||
if (
|
||||
isValid &&
|
||||
data.providerSpecificData &&
|
||||
typeof data.providerSpecificData === "object"
|
||||
) {
|
||||
setValidatedProviderSpecificData(data.providerSpecificData);
|
||||
validationPsd = data.providerSpecificData;
|
||||
}
|
||||
} catch {
|
||||
setValidationResult("failed");
|
||||
} finally {
|
||||
@@ -531,6 +568,13 @@ export default function EditConnectionModal({
|
||||
}
|
||||
}
|
||||
if (isValid) {
|
||||
updates.apiKey = isChatGptWebCodex
|
||||
? JSON.stringify({
|
||||
version: 1,
|
||||
cookie: formData.apiKey.trim().replace(/^cookie\s*:\s*/i, ""),
|
||||
...(formData.runtimeKey.trim() ? { runtimeKey: formData.runtimeKey.trim() } : {}),
|
||||
})
|
||||
: formData.apiKey;
|
||||
updates.testStatus = "active";
|
||||
updates.lastError = null;
|
||||
updates.lastErrorAt = null;
|
||||
@@ -543,6 +587,7 @@ export default function EditConnectionModal({
|
||||
if (!isOAuth) {
|
||||
updates.providerSpecificData = {
|
||||
...(connection.providerSpecificData || {}),
|
||||
...(validationPsd || {}),
|
||||
};
|
||||
assignEditApiKeyProviderSpecificData({
|
||||
provider,
|
||||
@@ -872,6 +917,83 @@ export default function EditConnectionModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isChatGptWebCodex && (
|
||||
<div className="space-y-3 rounded-lg border border-border bg-surface/40 p-3">
|
||||
<p className="text-sm font-medium text-text-main">Codex-Toolverbindung</p>
|
||||
<Input
|
||||
label="Tunnel-ID"
|
||||
value={formData.tunnelId}
|
||||
onChange={(event) => setFormData({ ...formData, tunnelId: event.target.value })}
|
||||
placeholder="tunnel_0123456789abcdef0123456789abcdef"
|
||||
/>
|
||||
<Input
|
||||
label="Neuer Tunnel Runtime-Key"
|
||||
type="password"
|
||||
value={formData.runtimeKey}
|
||||
onChange={(event) => setFormData({ ...formData, runtimeKey: event.target.value })}
|
||||
hint="Nur zusammen mit einem frischen Cookie eingeben. Der Wert wird verschlüsselt gespeichert."
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
label="ChatGPT-Custom-Connector"
|
||||
value={formData.connectorName}
|
||||
onChange={(event) =>
|
||||
setFormData({ ...formData, connectorName: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={doctorLoading || !connection.id}
|
||||
onClick={async () => {
|
||||
if (!connection.id) return;
|
||||
setDoctorLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/providers/${connection.id}/chatgpt-web-codex-doctor`
|
||||
);
|
||||
const payload = await response.json();
|
||||
setDoctorStatus(response.ok ? payload.status : { error: payload.error });
|
||||
} finally {
|
||||
setDoctorLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{doctorLoading ? "Status wird geprüft …" : "Doctor-Status prüfen"}
|
||||
</Button>
|
||||
{doctorStatus && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-text-muted">
|
||||
{[
|
||||
["Browser", doctorStatus.browser?.ready],
|
||||
["Storage-State", doctorStatus.storageState?.ready],
|
||||
["ChatGPT-Anmeldung", doctorStatus.login?.ready],
|
||||
["Temporary Chat", doctorStatus.temporaryChats?.ready],
|
||||
["Tunnel-Binary", doctorStatus.tunnelBinary?.ready],
|
||||
["Tunnel", doctorStatus.tunnel?.ready],
|
||||
["Connector", doctorStatus.connector?.ready],
|
||||
["Tool-Roundtrip", doctorStatus.toolRoundtrip?.ready],
|
||||
["Aktive Turns", doctorStatus.runtime?.activeTurns],
|
||||
["Wartende Turns", doctorStatus.runtime?.waitingTurns],
|
||||
].map(([label, ready]) => (
|
||||
<div key={String(label)}>
|
||||
{label}:{" "}
|
||||
{typeof ready === "number" ? ready : ready ? "bereit" : "nicht bereit"}
|
||||
</div>
|
||||
))}
|
||||
{doctorStatus.recovery?.interactiveLoginRequired && (
|
||||
<div className="col-span-2 text-warning">
|
||||
Interaktive Anmeldung erforderlich. Nutze den geschützten
|
||||
Browser-/VNC-Recovery-Pfad.
|
||||
</div>
|
||||
)}
|
||||
{doctorStatus.lastError && (
|
||||
<div className="col-span-2 break-words text-danger">
|
||||
Letzter Fehler: {String(doctorStatus.lastError)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isGooglePse && (
|
||||
<Input
|
||||
label={t("searchEngineIdLabel")}
|
||||
|
||||
@@ -32,6 +32,9 @@ type FormData = QuotaScrapingFieldValues &
|
||||
routingTags: string;
|
||||
tag?: string;
|
||||
validationModelId?: string;
|
||||
tunnelId: string;
|
||||
connectorName: string;
|
||||
runtimeKey?: string;
|
||||
};
|
||||
type ProviderSpecificData = Record<string, unknown>;
|
||||
|
||||
@@ -92,6 +95,10 @@ export function buildAddProviderSpecificData(options: {
|
||||
assignGlmTeamQuotaProviderData(isGlm, formData, data);
|
||||
} else if (isCloudflare && formData.accountId.trim()) data.accountId = formData.accountId.trim();
|
||||
if (isCcCompatible) assignCcCompatibleRequestDefaults(data, formData);
|
||||
if (provider === "chatgpt-web-codex") {
|
||||
if (formData.tunnelId.trim()) data.tunnelId = formData.tunnelId.trim();
|
||||
if (formData.connectorName.trim()) data.connectorName = formData.connectorName.trim();
|
||||
}
|
||||
return Object.keys(data).length > 0 ? data : undefined;
|
||||
}
|
||||
|
||||
@@ -147,4 +154,8 @@ export function assignEditApiKeyProviderSpecificData(options: {
|
||||
o.formData
|
||||
);
|
||||
}
|
||||
if (o.provider === "chatgpt-web-codex") {
|
||||
o.target.tunnelId = o.formData.tunnelId.trim() || undefined;
|
||||
o.target.connectorName = o.formData.connectorName.trim() || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@ const LOBE_PROVIDER_ALIASES = {
|
||||
"black-forest-labs": "Bfl",
|
||||
cerebras: "Cerebras",
|
||||
"chatgpt-web": "OpenAI",
|
||||
"chatgpt-web-codex": "OpenAI",
|
||||
claude: "ClaudeCode",
|
||||
"claude-web": "Claude",
|
||||
cline: "Cline",
|
||||
|
||||
Reference in New Issue
Block a user