mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 02:37:29 +08:00
310 lines
13 KiB
TypeScript
310 lines
13 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { RefreshCw, Save } from 'lucide-react';
|
|
import { useAppContext } from '../context/AppContext';
|
|
import { useUI } from '../context/UIContext';
|
|
import { Button, FixedButton } from '../components/Button';
|
|
import PageHeader from '../components/PageHeader';
|
|
import { ConfigDiffModal } from '../components/config/ConfigPageChrome';
|
|
import { ProviderProxyCard, ProviderRuntimeDrawer, ProviderRuntimeSummary, ProviderRuntimeToolbar } from '../components/config/ProviderConfigSection';
|
|
import { buildDiffRows, RuntimeWindow } from '../components/config/configUtils';
|
|
import { useConfigProviderActions } from '../components/config/useConfigProviderActions';
|
|
import { useConfigRuntimeView } from '../components/config/useConfigRuntimeView';
|
|
import { useConfigSaveAction } from '../components/config/useConfigSaveAction';
|
|
import { cloneJSON } from '../utils/object';
|
|
|
|
const Providers: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const ui = useUI();
|
|
const { cfg, setCfg, cfgRaw, loadConfig, q, setConfigEditing, providerRuntimeItems, setToken } = useAppContext();
|
|
const [newProxyName, setNewProxyName] = useState('');
|
|
const [runtimeAutoRefresh, setRuntimeAutoRefresh] = useState(true);
|
|
const [runtimeRefreshSec, setRuntimeRefreshSec] = useState(10);
|
|
const [runtimeWindow, setRuntimeWindow] = useState<RuntimeWindow>('24h');
|
|
const [runtimeDrawerProvider, setRuntimeDrawerProvider] = useState('');
|
|
const [selectedProviderTab, setSelectedProviderTab] = useState('');
|
|
const [showDiff, setShowDiff] = useState(false);
|
|
const [baseline, setBaseline] = useState<any>(null);
|
|
const oauthImportInputRef = useRef<HTMLInputElement | null>(null);
|
|
const latestProviderRuntimeRef = useRef<any[]>([]);
|
|
const [displayedProviderRuntimeItems, setDisplayedProviderRuntimeItems] = useState<any[]>([]);
|
|
const [oauthAccounts, setOAuthAccounts] = useState<Record<string, Array<any>>>({});
|
|
const [oauthAccountsLoading, setOAuthAccountsLoading] = useState<Record<string, boolean>>({});
|
|
const [oauthAccountsLoaded, setOAuthAccountsLoaded] = useState<Record<string, boolean>>({});
|
|
|
|
const providerEntries = useMemo(() => {
|
|
const providers = (((cfg as any)?.models || {}) as any)?.providers || {};
|
|
const entries: Array<[string, any]> = [];
|
|
if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
|
|
Object.entries(providers).forEach(([name, value]) => entries.push([name, value]));
|
|
}
|
|
return entries;
|
|
}, [cfg]);
|
|
|
|
const providerRuntimeMap = useMemo(() => {
|
|
const entries = Array.isArray(displayedProviderRuntimeItems) ? displayedProviderRuntimeItems : [];
|
|
return Object.fromEntries(entries.map((item: any) => [String(item?.name || ''), item]));
|
|
}, [displayedProviderRuntimeItems]);
|
|
|
|
const activeProviderName = useMemo(() => {
|
|
if (providerEntries.length === 0) return '';
|
|
if (providerEntries.some(([name]) => name === selectedProviderTab)) return selectedProviderTab;
|
|
return providerEntries[0]?.[0] || '';
|
|
}, [providerEntries, selectedProviderTab]);
|
|
|
|
const activeProviderEntry = useMemo(
|
|
() => providerEntries.find(([name]) => name === activeProviderName) || null,
|
|
[providerEntries, activeProviderName],
|
|
);
|
|
|
|
useEffect(() => {
|
|
latestProviderRuntimeRef.current = Array.isArray(providerRuntimeItems) ? providerRuntimeItems : [];
|
|
if (runtimeAutoRefresh && runtimeRefreshSec <= 1) {
|
|
setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current);
|
|
}
|
|
}, [providerRuntimeItems, runtimeAutoRefresh, runtimeRefreshSec]);
|
|
|
|
useEffect(() => {
|
|
if (!runtimeAutoRefresh) return;
|
|
setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current);
|
|
const timer = window.setInterval(() => {
|
|
setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current);
|
|
}, Math.max(1, runtimeRefreshSec) * 1000);
|
|
return () => window.clearInterval(timer);
|
|
}, [runtimeAutoRefresh, runtimeRefreshSec]);
|
|
|
|
useEffect(() => {
|
|
if (!activeProviderName && providerEntries.length > 0) {
|
|
setSelectedProviderTab(providerEntries[0][0]);
|
|
}
|
|
}, [activeProviderName, providerEntries]);
|
|
|
|
useEffect(() => {
|
|
if (baseline == null && cfg && Object.keys(cfg).length > 0) {
|
|
setBaseline(cloneJSON(cfg));
|
|
}
|
|
}, [cfg, baseline]);
|
|
|
|
const diffRows = useMemo(() => buildDiffRows(baseline, cfg, t('configRoot')), [baseline, cfg, t]);
|
|
const isDirty = useMemo(() => {
|
|
if (baseline == null) return false;
|
|
return JSON.stringify(baseline) !== JSON.stringify(cfg || {});
|
|
}, [baseline, cfg]);
|
|
|
|
useEffect(() => {
|
|
setConfigEditing(isDirty);
|
|
return () => setConfigEditing(false);
|
|
}, [isDirty, setConfigEditing]);
|
|
|
|
const { filterRuntimeEvents, renderRuntimeEventList, runtimeSectionOpen, toggleRuntimeSection } = useConfigRuntimeView(runtimeWindow);
|
|
const {
|
|
addProxy,
|
|
clearAPIKeyCooldown,
|
|
clearOAuthCooldown,
|
|
clearProviderHistory,
|
|
deleteOAuthAccount,
|
|
exportProviderHistory,
|
|
loadOAuthAccounts,
|
|
onOAuthImportChange,
|
|
refreshOAuthAccount,
|
|
refreshProviderRuntimeNow,
|
|
removeProxy,
|
|
startOAuthLogin,
|
|
triggerOAuthImport,
|
|
updateProxyField,
|
|
} = useConfigProviderActions({
|
|
inputRef: oauthImportInputRef,
|
|
loadConfig,
|
|
onProviderRuntimeRefreshed: () => setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current),
|
|
providerRuntimeMap,
|
|
q,
|
|
setCfg,
|
|
setNewProxyName,
|
|
setOAuthAccounts,
|
|
t,
|
|
ui,
|
|
});
|
|
|
|
const loadOAuthAccountsNow = useCallback(async (name: string) => {
|
|
if (!name) return;
|
|
setOAuthAccountsLoading((prev) => ({ ...prev, [name]: true }));
|
|
try {
|
|
await loadOAuthAccounts(name);
|
|
setOAuthAccountsLoaded((prev) => ({ ...prev, [name]: true }));
|
|
} finally {
|
|
setOAuthAccountsLoading((prev) => ({ ...prev, [name]: false }));
|
|
}
|
|
}, [loadOAuthAccounts]);
|
|
|
|
useEffect(() => {
|
|
providerEntries.forEach(([name, p]) => {
|
|
if (!['oauth', 'hybrid'].includes(String(p?.auth || ''))) return;
|
|
if (oauthAccountsLoaded[name] || oauthAccountsLoading[name]) return;
|
|
void loadOAuthAccountsNow(name);
|
|
});
|
|
}, [loadOAuthAccountsNow, oauthAccountsLoaded, oauthAccountsLoading, providerEntries]);
|
|
|
|
useEffect(() => {
|
|
if (!activeProviderEntry) return;
|
|
const [name, provider] = activeProviderEntry;
|
|
if (!['oauth', 'hybrid'].includes(String(provider?.auth || ''))) return;
|
|
if (oauthAccountsLoaded[name] || oauthAccountsLoading[name]) return;
|
|
void loadOAuthAccountsNow(name);
|
|
}, [activeProviderEntry, loadOAuthAccountsNow, oauthAccountsLoaded, oauthAccountsLoading]);
|
|
|
|
useEffect(() => {
|
|
const oauthProviderNames = new Set(
|
|
providerEntries
|
|
.filter(([, provider]) => ['oauth', 'hybrid'].includes(String(provider?.auth || '')))
|
|
.map(([name]) => name),
|
|
);
|
|
setOAuthAccountsLoaded((prev) => {
|
|
const next = Object.fromEntries(Object.entries(prev).filter(([name]) => oauthProviderNames.has(name)));
|
|
return Object.keys(next).length === Object.keys(prev).length ? prev : next;
|
|
});
|
|
setOAuthAccountsLoading((prev) => {
|
|
const next = Object.fromEntries(Object.entries(prev).filter(([name]) => oauthProviderNames.has(name)));
|
|
return Object.keys(next).length === Object.keys(prev).length ? prev : next;
|
|
});
|
|
}, [providerEntries]);
|
|
|
|
const { saveConfig } = useConfigSaveAction({
|
|
cfg,
|
|
cfgRaw,
|
|
loadConfig,
|
|
q,
|
|
setBaseline,
|
|
setConfigEditing,
|
|
setToken,
|
|
setShowDiff,
|
|
showRaw: false,
|
|
t,
|
|
ui,
|
|
});
|
|
|
|
return (
|
|
<div className="p-4 md:p-6 xl:p-8 w-full space-y-4 flex flex-col min-h-full">
|
|
<input ref={oauthImportInputRef} type="file" accept=".json,application/json" className="hidden" onChange={onOAuthImportChange} />
|
|
|
|
<PageHeader
|
|
title={t('providers')}
|
|
titleClassName="ui-text-primary"
|
|
actions={
|
|
<div className="flex items-center gap-2 flex-wrap justify-end">
|
|
<FixedButton onClick={async () => {
|
|
const reloaded = await loadConfig(true);
|
|
setBaseline(cloneJSON(reloaded ?? cfg));
|
|
}} label={t('reload')}>
|
|
<RefreshCw className="w-4 h-4" />
|
|
</FixedButton>
|
|
<Button onClick={() => setShowDiff(true)} size="sm">{t('configDiffPreview')}</Button>
|
|
<Button onClick={saveConfig} variant="primary" size="sm" radius="lg" gap="1">
|
|
<Save className="w-4 h-4" />
|
|
{t('saveChanges')}
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<div className="brand-card ui-border-subtle border rounded-[30px] p-4 md:p-6 space-y-4">
|
|
<ProviderRuntimeToolbar
|
|
newProxyName={newProxyName}
|
|
onAddProxy={() => addProxy(newProxyName)}
|
|
onNewProxyNameChange={setNewProxyName}
|
|
onRefreshRuntime={refreshProviderRuntimeNow}
|
|
onRuntimeAutoRefreshChange={setRuntimeAutoRefresh}
|
|
onRuntimeRefreshSecChange={setRuntimeRefreshSec}
|
|
onRuntimeWindowChange={setRuntimeWindow}
|
|
runtimeAutoRefresh={runtimeAutoRefresh}
|
|
runtimeRefreshSec={runtimeRefreshSec}
|
|
runtimeWindow={runtimeWindow}
|
|
t={t}
|
|
/>
|
|
|
|
<div className="rounded-xl border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-xs text-zinc-400">
|
|
{t('providersIntroBefore')}
|
|
<span className="font-mono text-zinc-200">oauth</span>
|
|
{t('providersIntroMiddle')}
|
|
<span className="font-mono text-zinc-200">hybrid</span>
|
|
{t('providersIntroAfter')}
|
|
</div>
|
|
|
|
{providerEntries.length > 0 ? (
|
|
<>
|
|
<div className="flex flex-wrap gap-2">
|
|
{providerEntries.map(([name, p]) => {
|
|
const auth = String(p?.auth || 'bearer');
|
|
const active = name === activeProviderName;
|
|
return (
|
|
<Button
|
|
key={`provider-tab-${name}`}
|
|
onClick={() => setSelectedProviderTab(name)}
|
|
variant={active ? 'primary' : 'neutral'}
|
|
size="sm"
|
|
radius="xl"
|
|
>
|
|
{name}
|
|
<span className="ml-1 text-[11px] opacity-80">{auth}</span>
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{activeProviderEntry && (
|
|
<ProviderProxyCard
|
|
key={activeProviderEntry[0]}
|
|
name={activeProviderEntry[0]}
|
|
oauthAccounts={oauthAccounts[activeProviderEntry[0]] || []}
|
|
oauthAccountsLoading={!!oauthAccountsLoading[activeProviderEntry[0]]}
|
|
onClearOAuthCooldown={(credentialFile) => clearOAuthCooldown(activeProviderEntry[0], credentialFile)}
|
|
onDeleteOAuthAccount={(credentialFile) => deleteOAuthAccount(activeProviderEntry[0], credentialFile)}
|
|
onFieldChange={(field, value) => updateProxyField(activeProviderEntry[0], field, value)}
|
|
onLoadOAuthAccounts={() => loadOAuthAccountsNow(activeProviderEntry[0])}
|
|
onRefreshOAuthAccount={(credentialFile) => refreshOAuthAccount(activeProviderEntry[0], credentialFile)}
|
|
onRemove={() => removeProxy(activeProviderEntry[0])}
|
|
onStartOAuthLogin={() => startOAuthLogin(activeProviderEntry[0], activeProviderEntry[1])}
|
|
onTriggerOAuthImport={() => triggerOAuthImport(activeProviderEntry[0], activeProviderEntry[1])}
|
|
proxy={activeProviderEntry[1]}
|
|
runtimeItem={providerRuntimeMap[activeProviderEntry[0]]}
|
|
runtimeSummary={providerRuntimeMap[activeProviderEntry[0]] ? (
|
|
<ProviderRuntimeSummary
|
|
item={providerRuntimeMap[activeProviderEntry[0]]}
|
|
name={activeProviderEntry[0]}
|
|
onClearApiCooldown={() => clearAPIKeyCooldown(activeProviderEntry[0])}
|
|
onClearHistory={() => clearProviderHistory(activeProviderEntry[0])}
|
|
onExportHistory={() => exportProviderHistory(activeProviderEntry[0])}
|
|
onOpenHistory={() => setRuntimeDrawerProvider(activeProviderEntry[0])}
|
|
renderRuntimeEventList={renderRuntimeEventList}
|
|
runtimeSectionOpen={(section) => runtimeSectionOpen(activeProviderEntry[0], section)}
|
|
toggleRuntimeSection={(section) => toggleRuntimeSection(activeProviderEntry[0], section)}
|
|
filterRuntimeEvents={filterRuntimeEvents}
|
|
/>
|
|
) : null}
|
|
t={t}
|
|
/>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="text-xs text-zinc-500">{t('configNoCustomProviders')}</div>
|
|
)}
|
|
</div>
|
|
|
|
{runtimeDrawerProvider && providerRuntimeMap[runtimeDrawerProvider] && (
|
|
<ProviderRuntimeDrawer
|
|
filterRuntimeEvents={filterRuntimeEvents}
|
|
item={providerRuntimeMap[runtimeDrawerProvider]}
|
|
name={runtimeDrawerProvider}
|
|
onClearHistory={() => clearProviderHistory(runtimeDrawerProvider)}
|
|
onClose={() => setRuntimeDrawerProvider('')}
|
|
onExportHistory={() => exportProviderHistory(runtimeDrawerProvider)}
|
|
renderRuntimeEventList={renderRuntimeEventList}
|
|
/>
|
|
)}
|
|
|
|
{showDiff && <ConfigDiffModal diffRows={diffRows} onClose={() => setShowDiff(false)} t={t} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Providers;
|