chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进

This commit is contained in:
Logan.Li committed 2026-09-14 22:10:22 +08:00
1 parent f71eef0eba
commit ae6093c9b3
163 files changed
+4911 -2494

No files matched your search

+97 -84
View File
@@ -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>
}