chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进
This commit is contained in:
1 parent
f71eef0eba
commit
ae6093c9b3
163 files changed
+4911
-2494
No files matched your search
@@ -200,12 +200,12 @@ export default function AiProviderSettings() {
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
aria-label={t('ai.presets')}
|
||||
value={presetId}
|
||||
onChange={(event) => setPresetId(event.target.value)}
|
||||
className="ui-input-sm min-w-52 flex-1"
|
||||
className="ui-select-pill"
|
||||
>
|
||||
{AI_PROVIDER_FAMILY_ORDER.map((familyId) => (
|
||||
<optgroup key={familyId} label={t(`ai.family.${familyId}`)}>
|
||||
@@ -228,11 +228,11 @@ export default function AiProviderSettings() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[400px] grid-cols-[168px_minmax(0,1fr)] gap-6">
|
||||
<aside className="space-y-0.5">
|
||||
<div className="px-2 pb-2 text-[11px] font-medium text-txt-faint">{t('ai.configured')}</div>
|
||||
<div className="grid min-h-[22rem] grid-cols-[12rem_minmax(0,1fr)] gap-5">
|
||||
<aside className="border-dark-700/45 bg-dark-800/50 h-fit overflow-hidden rounded-2xl border py-2">
|
||||
<div className="px-3 pb-1.5 text-[11px] font-medium text-txt-faint">{t('ai.configured')}</div>
|
||||
{store.providers.length === 0 ? (
|
||||
<p className="px-2 py-6 text-xs leading-relaxed text-txt-faint">{t('ai.configuredHint')}</p>
|
||||
<p className="px-3 py-5 text-xs leading-relaxed text-txt-faint">{t('ai.configuredHint')}</p>
|
||||
) : (
|
||||
store.providers.map((provider) => {
|
||||
const model = provider.models.find((item) => item.id === provider.defaultModelId)
|
||||
@@ -241,7 +241,7 @@ export default function AiProviderSettings() {
|
||||
key={provider.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(provider.id)}
|
||||
className={`w-full rounded-lg px-2.5 py-2 text-left transition-colors ${
|
||||
className={`w-full px-3 py-2 text-left transition-colors ${
|
||||
selectedId === provider.id
|
||||
? 'bg-accent-blue/12 text-accent-blue'
|
||||
: 'hover:bg-dark-700/50 text-content-secondary'
|
||||
@@ -260,34 +260,40 @@ export default function AiProviderSettings() {
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0">
|
||||
<main className="min-w-0 max-w-lg">
|
||||
{!draft ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-txt-faint">{t('ai.emptyHint')}</div>
|
||||
<div className="border-dark-600/80 flex h-40 items-center justify-center rounded-2xl border border-dashed text-sm text-txt-faint">
|
||||
{t('ai.emptyHint')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-xl space-y-3.5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label={t('ai.name')}>
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(event) => updateDraft({ name: event.target.value })}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('ai.model')}>
|
||||
{manualModel || draft.models.length === 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="border-dark-700/45 bg-dark-800/50 divide-dark-700/35 divide-y overflow-hidden rounded-2xl border">
|
||||
<FormRow label={t('ai.name')}>
|
||||
<CompactField>
|
||||
<input
|
||||
aria-label={t('ai.model')}
|
||||
value={currentModel?.model || ''}
|
||||
onChange={(event) => updateManualModel(event.target.value)}
|
||||
placeholder="deepseek-v4-pro"
|
||||
className="ui-input-sm font-mono"
|
||||
value={draft.name}
|
||||
onChange={(event) => updateDraft({ name: event.target.value })}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</CompactField>
|
||||
</FormRow>
|
||||
<FormRow label={t('ai.model')}>
|
||||
{manualModel || draft.models.length === 0 ? (
|
||||
<CompactField>
|
||||
<input
|
||||
aria-label={t('ai.model')}
|
||||
value={currentModel?.model || ''}
|
||||
onChange={(event) => updateManualModel(event.target.value)}
|
||||
placeholder="deepseek-v4-pro"
|
||||
className="ui-input-sm font-mono"
|
||||
/>
|
||||
</CompactField>
|
||||
) : (
|
||||
<select
|
||||
aria-label={t('ai.model')}
|
||||
value={draft.defaultModelId}
|
||||
onChange={(event) => chooseModel(event.target.value)}
|
||||
className="ui-input-sm"
|
||||
className="ui-select-pill"
|
||||
>
|
||||
{draft.models.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
@@ -297,100 +303,103 @@ export default function AiProviderSettings() {
|
||||
<option value="__manual">{t('ai.otherModels')}</option>
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
</FormRow>
|
||||
{manualModel && draft.models.length > 0 && (
|
||||
<div className="flex justify-end px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-accent-blue hover:underline"
|
||||
onClick={() => setManualModel(false)}
|
||||
>
|
||||
{t('ai.backToList')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<FormRow label={draft.authStyle === 'none' ? t('ai.apiKeyOptional') : t('ai.apiKey')}>
|
||||
<CompactField wide>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={apiKey}
|
||||
disabled={draft.authStyle === 'none'}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder={
|
||||
draft.hasApiKey
|
||||
? t('ai.savedKeepBlank')
|
||||
: draft.authStyle === 'none'
|
||||
? t('ai.notRequired')
|
||||
: 'sk-...'
|
||||
}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</CompactField>
|
||||
</FormRow>
|
||||
{(needsEndpoint || advancedOpen) && (
|
||||
<FormRow label={t('ai.endpoint')}>
|
||||
<CompactField wide>
|
||||
<input
|
||||
aria-label={t('ai.endpoint')}
|
||||
value={currentModel?.baseUrl || draft.baseUrl}
|
||||
onChange={(event) => updateDraft({ baseUrl: event.target.value })}
|
||||
placeholder="https://api.example.com/v1"
|
||||
className="ui-input-sm font-mono"
|
||||
disabled={!!currentModel?.baseUrl && !needsEndpoint}
|
||||
/>
|
||||
</CompactField>
|
||||
</FormRow>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{manualModel && draft.models.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-accent-blue hover:underline"
|
||||
onClick={() => setManualModel(false)}
|
||||
>
|
||||
{t('ai.backToList')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Field label={draft.authStyle === 'none' ? t('ai.apiKeyOptional') : 'API Key'}>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={apiKey}
|
||||
disabled={draft.authStyle === 'none'}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder={
|
||||
draft.hasApiKey
|
||||
? t('ai.savedKeepBlank')
|
||||
: draft.authStyle === 'none'
|
||||
? t('ai.notRequired')
|
||||
: 'sk-...'
|
||||
}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{(needsEndpoint || advancedOpen) && (
|
||||
<Field label="Endpoint">
|
||||
<input
|
||||
aria-label="Endpoint"
|
||||
value={currentModel?.baseUrl || draft.baseUrl}
|
||||
onChange={(event) => updateDraft({ baseUrl: event.target.value })}
|
||||
placeholder="https://api.example.com/v1"
|
||||
className="ui-input-sm font-mono"
|
||||
disabled={!!currentModel?.baseUrl && !needsEndpoint}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAdvancedOpen((open) => !open)}
|
||||
className="flex items-center gap-1 text-xs text-txt-faint hover:text-txt-muted"
|
||||
className="flex items-center gap-1 px-0.5 text-xs text-txt-faint hover:text-txt-muted"
|
||||
>
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${advancedOpen ? 'rotate-180' : ''}`} />
|
||||
{t('ai.advanced')}
|
||||
</button>
|
||||
|
||||
{advancedOpen && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label={t('ai.protocol')}>
|
||||
<div className="border-dark-700/45 bg-dark-800/50 divide-dark-700/35 divide-y overflow-hidden rounded-2xl border">
|
||||
<FormRow label={t('ai.protocol')}>
|
||||
<select
|
||||
value={draft.protocol}
|
||||
onChange={(event) =>
|
||||
updateDraft({ protocol: event.target.value as AiProviderConfig['protocol'] })
|
||||
}
|
||||
className="ui-input-sm"
|
||||
className="ui-select-pill"
|
||||
>
|
||||
<option value="openai-chat">OpenAI Chat</option>
|
||||
<option value="openai-responses">OpenAI Responses</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t('ai.auth')}>
|
||||
</FormRow>
|
||||
<FormRow label={t('ai.auth')}>
|
||||
<select
|
||||
value={draft.authStyle}
|
||||
onChange={(event) =>
|
||||
updateDraft({ authStyle: event.target.value as AiProviderConfig['authStyle'] })
|
||||
}
|
||||
className="ui-input-sm"
|
||||
className="ui-select-pill"
|
||||
>
|
||||
<option value="bearer">Bearer</option>
|
||||
<option value="x-api-key">x-api-key</option>
|
||||
<option value="both">Bearer + x-api-key</option>
|
||||
<option value="none">{t('ai.authNone')}</option>
|
||||
</select>
|
||||
</Field>
|
||||
<label className="col-span-2 flex items-center gap-2 text-xs text-txt-muted">
|
||||
</FormRow>
|
||||
<FormRow label={t('ai.allowHttp')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.allowInsecureHttp}
|
||||
onChange={(event) => updateDraft({ allowInsecureHttp: event.target.checked })}
|
||||
className="h-3.5 w-3.5 accent-blue-500"
|
||||
/>
|
||||
{t('ai.allowHttp')}
|
||||
</label>
|
||||
</FormRow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 pt-1">
|
||||
<div className="flex flex-wrap items-center gap-2 pt-0.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@@ -450,11 +459,15 @@ export default function AiProviderSettings() {
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
function FormRow({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-[11px] text-txt-muted">{label}</span>
|
||||
{children}
|
||||
<label className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1.5 px-4 py-3">
|
||||
<span className="min-w-[6.5rem] text-sm text-content-primary">{label}</span>
|
||||
<div className="ml-auto flex-shrink-0">{children}</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactField({ children, wide = false }: { children: React.ReactNode; wide?: boolean }) {
|
||||
return <div className={wide ? 'w-[17.5rem] max-w-full' : 'w-[12.5rem] max-w-full'}>{children}</div>
|
||||
}
|
||||
Reference in new issue
Block a user