mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-15 03:57:30 +08:00
fix
This commit is contained in:
@@ -138,13 +138,13 @@ export function ProviderRuntimeToolbar({
|
||||
<option value="all">{t('providersRuntimeAll')}</option>
|
||||
</SelectField>
|
||||
<TextField dense value={newProxyName} onChange={(e) => onNewProxyNameChange(e.target.value)} placeholder={t('configNewProviderName')} className="min-w-[220px] !w-[220px] xl:!w-[280px] bg-zinc-900/70 border-zinc-700" />
|
||||
<Button onClick={onRefreshRuntime} size="xs" radius="lg" variant="neutral" gap="2" noShrink>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button onClick={onAddProxy} variant="primary" size="xs" radius="lg" gap="2" noShrink>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('add')}
|
||||
</Button>
|
||||
<Button onClick={onRefreshRuntime} size="xs" radius="lg" variant="neutral" gap="2" noShrink>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -409,6 +409,14 @@ export function ProviderProxyCard({
|
||||
const oauthProvider = String(proxy?.oauth?.provider || '');
|
||||
const [runtimeOpen, setRuntimeOpen] = React.useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showOAuth) {
|
||||
onLoadOAuthAccounts();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showOAuth]);
|
||||
|
||||
const oauthAccountCount = Array.isArray(oauthAccounts) ? oauthAccounts.length : 0;
|
||||
const runtimeErrors = Array.isArray(runtimeItem?.recent_errors) ? runtimeItem.recent_errors : [];
|
||||
const lastQuotaError = runtimeErrors.find((item: any) => String(item?.reason || '').trim() === 'quota') || null;
|
||||
@@ -419,13 +427,13 @@ export function ProviderProxyCard({
|
||||
: lastQuotaError
|
||||
? {
|
||||
label: t('providersQuotaLimited'),
|
||||
tone: 'border-amber-500/30 bg-amber-500/10 text-amber-200',
|
||||
tone: 'ui-pill ui-pill-warning',
|
||||
detail: ti('providersQuotaLimitedDetail', { when: String(lastQuotaError?.when || '-') }),
|
||||
}
|
||||
: oauthAccounts.some((account) => String(account?.cooldown_until || '').trim())
|
||||
? {
|
||||
label: t('providersQuotaCooldown'),
|
||||
tone: 'border-orange-500/30 bg-orange-500/10 text-orange-200',
|
||||
tone: 'ui-pill ui-pill-warning',
|
||||
detail: oauthAccounts
|
||||
.map((account) => String(account?.cooldown_until || '').trim())
|
||||
.find(Boolean) || '-',
|
||||
@@ -433,21 +441,21 @@ export function ProviderProxyCard({
|
||||
: oauthAccounts.some((account) => Number(account?.health_score || 100) < 60)
|
||||
? {
|
||||
label: t('providersQuotaHealthLow'),
|
||||
tone: 'border-rose-500/30 bg-rose-500/10 text-rose-200',
|
||||
tone: 'ui-pill ui-pill-danger',
|
||||
detail: ti('providersQuotaHealthLowDetail', { score: Math.min(...oauthAccounts.map((account) => Number(account?.health_score || 100))) }),
|
||||
}
|
||||
: connected
|
||||
? {
|
||||
label: t('providersQuotaHealthy'),
|
||||
tone: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200',
|
||||
tone: 'ui-pill ui-pill-success',
|
||||
detail: t('providersQuotaHealthyDetail'),
|
||||
}
|
||||
: {
|
||||
label: t('providersOAuthDisconnected'),
|
||||
tone: 'border-zinc-700 bg-zinc-900/50 text-zinc-300',
|
||||
tone: 'ui-pill ui-pill-neutral',
|
||||
detail: t('providersQuotaNoAccountDetail'),
|
||||
};
|
||||
const quotaTone = quotaState?.tone || 'border-zinc-700 bg-zinc-900/50 text-zinc-300';
|
||||
const quotaTone = quotaState?.tone || 'ui-pill ui-pill-neutral';
|
||||
const oauthStatusText = oauthAccountsLoading
|
||||
? t('providersOAuthLoading')
|
||||
: connected
|
||||
@@ -702,11 +710,9 @@ export function ProviderProxyCard({
|
||||
<RefreshCw className={`w-4 h-4${oauthAccountsLoading ? ' animate-spin' : ''}`} />
|
||||
</FixedButton>
|
||||
</div>
|
||||
<div className={`rounded-xl border px-3 py-2 text-[11px] ${oauthAccountsLoading
|
||||
? 'border-sky-500/25 bg-sky-500/10 text-sky-100'
|
||||
: connected
|
||||
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
|
||||
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
|
||||
<div className={`mt-4 rounded-xl border px-4 py-3 text-sm ${connected
|
||||
? 'ui-pill ui-pill-success'
|
||||
: 'ui-pill ui-pill-neutral'
|
||||
}`}>
|
||||
{oauthAccountsLoading
|
||||
? t('providersOAuthLoadingHelp')
|
||||
@@ -715,8 +721,8 @@ export function ProviderProxyCard({
|
||||
: t('providersOAuthEmptyHelp')}
|
||||
</div>
|
||||
<div className={`hidden rounded-xl border px-3 py-2 text-[11px] ${connected
|
||||
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
|
||||
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
|
||||
? 'ui-pill ui-pill-success'
|
||||
: 'ui-pill ui-pill-neutral'
|
||||
}`}>
|
||||
{connected
|
||||
? ti('providersAutoLoadedCount', { count: oauthAccountCount, primary: oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || '-' })
|
||||
@@ -734,10 +740,10 @@ export function ProviderProxyCard({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-zinc-200 truncate">{account?.email || account?.account_id || account?.credential_file}</div>
|
||||
<div className={`shrink-0 rounded-full border px-2 py-0.5 text-[10px] ${String(account?.cooldown_until || '').trim()
|
||||
? 'border-orange-500/30 bg-orange-500/10 text-orange-200'
|
||||
? 'ui-pill ui-pill-warning'
|
||||
: Number(account?.health_score || 100) < 60
|
||||
? 'border-rose-500/30 bg-rose-500/10 text-rose-200'
|
||||
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
|
||||
? 'ui-pill ui-pill-danger'
|
||||
: 'ui-pill ui-pill-success'
|
||||
}`}>
|
||||
{String(account?.cooldown_until || '').trim() ? t('providersAccountCooldown') : Number(account?.health_score || 100) < 60 ? t('providersAccountLimited') : t('providersAccountOnline')}
|
||||
</div>
|
||||
|
||||
@@ -298,6 +298,9 @@ const resources = {
|
||||
providersIntroBefore: 'Select a provider tab, then set auth to ',
|
||||
providersIntroMiddle: ' or ',
|
||||
providersIntroAfter: '. The OAuth fields, login link flow, callback paste step, and account list appear in that provider card.',
|
||||
providersDefaultProvider: 'Default Provider',
|
||||
providersDefaultProviderHelp: 'Changing this updates the global primary model, clears global fallbacks, and syncs every subagent runtime provider to the selected provider after you save.',
|
||||
providersDefaultProviderModelRequired: 'Provider "{{name}}" has no model configured yet. Add a model before making it the default provider.',
|
||||
providersQwenLabelTitle: 'Qwen Account Label',
|
||||
providersQwenLabelMessage: 'Qwen OAuth may not return an email. Enter an email or alias to identify this account.',
|
||||
providersQwenImportLabelMessage: 'Enter an email or alias for this imported Qwen account.',
|
||||
@@ -1146,6 +1149,9 @@ const resources = {
|
||||
providersIntroBefore: '先选择一个 provider 标签,再把认证模式切到 ',
|
||||
providersIntroMiddle: ' 或 ',
|
||||
providersIntroAfter: '。对应 provider 卡片里会出现 OAuth 字段、登录链接流程、回调地址回填和账号列表。',
|
||||
providersDefaultProvider: '默认供应商',
|
||||
providersDefaultProviderHelp: '切换后会更新全局主模型、清空全局回退列表,并把所有子代理的 runtime provider 同步成这个供应商。保存配置后生效。',
|
||||
providersDefaultProviderModelRequired: 'provider "{{name}}" 还没有配置模型,先添加模型后才能设为默认供应商。',
|
||||
providersQwenLabelTitle: 'Qwen 账号标识',
|
||||
providersQwenLabelMessage: 'Qwen OAuth 可能不会返回邮箱,请输入一个邮箱或别名来标识该账号。',
|
||||
providersQwenImportLabelMessage: '请为导入的 Qwen 账号输入邮箱或别名。',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RefreshCw, Save } from 'lucide-react';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import { useUI } from '../context/UIContext';
|
||||
import { Button, FixedButton } from '../components/ui/Button';
|
||||
import { SelectField } from '../components/ui/FormControls';
|
||||
import PageHeader from '../components/layout/PageHeader';
|
||||
import { ConfigDiffModal } from '../components/config/ConfigPageChrome';
|
||||
import { ProviderProxyCard, ProviderRuntimeDrawer, ProviderRuntimeSummary, ProviderRuntimeToolbar } from '../components/config/ProviderConfigSection';
|
||||
@@ -13,6 +14,22 @@ import { useConfigRuntimeView } from '../components/config/useConfigRuntimeView'
|
||||
import { useConfigSaveAction } from '../components/config/useConfigSaveAction';
|
||||
import { cloneJSON } from '../utils/object';
|
||||
|
||||
function parseProviderModelRef(raw: unknown) {
|
||||
const trimmed = String(raw || '').trim();
|
||||
if (!trimmed) return { provider: '', model: '' };
|
||||
const idx = trimmed.indexOf('/');
|
||||
if (idx <= 0) return { provider: '', model: trimmed };
|
||||
return {
|
||||
provider: trimmed.slice(0, idx).trim(),
|
||||
model: trimmed.slice(idx + 1).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function firstProviderModel(provider: any) {
|
||||
const models = Array.isArray(provider?.models) ? provider.models : [];
|
||||
return String(models.find((item: any) => String(item || '').trim()) || '').trim();
|
||||
}
|
||||
|
||||
const Providers: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const ui = useUI();
|
||||
@@ -56,6 +73,11 @@ const Providers: React.FC = () => {
|
||||
() => providerEntries.find(([name]) => name === activeProviderName) || null,
|
||||
[providerEntries, activeProviderName],
|
||||
);
|
||||
const defaultProviderName = useMemo(() => {
|
||||
const { provider } = parseProviderModelRef((cfg as any)?.agents?.defaults?.model?.primary);
|
||||
if (provider && providerEntries.some(([name]) => name === provider)) return provider;
|
||||
return providerEntries[0]?.[0] || '';
|
||||
}, [cfg, providerEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
latestProviderRuntimeRef.current = Array.isArray(providerRuntimeItems) ? providerRuntimeItems : [];
|
||||
@@ -182,6 +204,38 @@ const Providers: React.FC = () => {
|
||||
ui,
|
||||
});
|
||||
|
||||
const applyDefaultProvider = useCallback((providerName: string) => {
|
||||
const trimmed = String(providerName || '').trim();
|
||||
if (!trimmed) return;
|
||||
const providerEntry = providerEntries.find(([name]) => name === trimmed);
|
||||
if (!providerEntry) return;
|
||||
const [, providerConfig] = providerEntry;
|
||||
const currentPrimary = parseProviderModelRef((cfg as any)?.agents?.defaults?.model?.primary);
|
||||
const model = currentPrimary.provider === trimmed && currentPrimary.model
|
||||
? currentPrimary.model
|
||||
: firstProviderModel(providerConfig);
|
||||
if (!model) {
|
||||
void ui.notify({ title: t('requestFailed'), message: t('providersDefaultProviderModelRequired', { name: trimmed }) });
|
||||
return;
|
||||
}
|
||||
setCfg((value) => {
|
||||
const next = cloneJSON(value || {});
|
||||
if (!next.agents || typeof next.agents !== 'object') next.agents = {};
|
||||
if (!next.agents.defaults || typeof next.agents.defaults !== 'object') next.agents.defaults = {};
|
||||
if (!next.agents.defaults.model || typeof next.agents.defaults.model !== 'object') next.agents.defaults.model = {};
|
||||
next.agents.defaults.model.primary = `${trimmed}/${model}`;
|
||||
next.agents.defaults.model.fallbacks = [];
|
||||
if (!next.agents.subagents || typeof next.agents.subagents !== 'object') return next;
|
||||
Object.entries(next.agents.subagents).forEach(([, rawAgent]) => {
|
||||
const agent = rawAgent as any;
|
||||
if (!agent || typeof agent !== 'object') return;
|
||||
if (!agent.runtime || typeof agent.runtime !== 'object') agent.runtime = {};
|
||||
agent.runtime.provider = trimmed;
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}, [cfg, providerEntries, setCfg, 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} />
|
||||
@@ -229,6 +283,29 @@ const Providers: React.FC = () => {
|
||||
{t('providersIntroAfter')}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/50 p-4 md:p-5">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-semibold text-zinc-100">{t('providersDefaultProvider')}</div>
|
||||
<div className="text-xs text-zinc-400">{t('providersDefaultProviderHelp')}</div>
|
||||
</div>
|
||||
<div className="w-full lg:w-[320px]">
|
||||
<SelectField
|
||||
dense
|
||||
value={defaultProviderName}
|
||||
onChange={(e) => applyDefaultProvider(e.target.value)}
|
||||
className="w-full bg-zinc-900/70 border-zinc-700"
|
||||
>
|
||||
{providerEntries.map(([name, provider]) => (
|
||||
<option key={`default-provider-${name}`} value={name}>
|
||||
{name} · {String(provider?.auth || 'bearer')}
|
||||
</option>
|
||||
))}
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{providerEntries.length > 0 ? (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
Reference in New Issue
Block a user