feat(agents): add configurable DeepSeek Harness model catalog

- Separate default model selection from catalog configuration
- Use API metadata with per-model and provider overrides
- Expose documented capability, reasoning, protocol, and request settings
- Preserve customizations with conflict detection and transactional rollback
This commit is contained in:
lzt404 committed 2026-09-11 16:25:14 +08:00
1 parent 145b585c3f
commit 4d8d549e04
28 files changed
+2047 -20

No files matched your search

+21 -3
View File
@@ -63,6 +63,7 @@ import type { ModelOption } from '../services/modelService';
import { getCurrentLocale, translate, useI18n } from '../i18n';
import { CodexSessionsPanel } from './CodexSessionsPanel';
import { CodexModelCatalogDialog } from './CodexModelCatalogDialog';
import { DeepSeekHarnessCatalogDialog } from './DeepSeekHarnessCatalogDialog';
import { AgentConfigBackupDialog } from './AgentConfigBackupDialog';
import { AgentConfigManagementPanel, AgentConfigurationFeedback, AgentRunControls } from './AgentControls';
@@ -753,6 +754,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false);
const [codexCatalogDialogOpen, setCodexCatalogDialogOpen] = useState(false);
const [harnessCatalogDialogOpen, setHarnessCatalogDialogOpen] = useState(false);
const [launchDirectory, setLaunchDirectory] = useState('');
const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState<AgentLaunchTarget | null>(null);
const [launchDirectoryError, setLaunchDirectoryError] = useState('');
@@ -1326,6 +1328,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
claudeModelMappingsDirtyRef.current[selected] = false;
}
await reloadStatusesAfterAction();
if (isDeepSeekHarnessClient) await loadModels('deepseek-harness');
setOauthConfigurationDraft(null);
setConfigurationNotice(t(result.outcome === 'unchanged' ? 'agents.backup.unchanged' : 'agents.backup.updated'));
onConfigurationApplied?.();
@@ -1686,6 +1689,13 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
</button>
) : null;
const harnessCatalogButton = isDeepSeekHarnessClient ? (
<button type="button" className="secondary-button agent-codex-catalog-button"
onClick={() => setHarnessCatalogDialogOpen(true)} disabled={busy || configurationWriteBlocked}>
<SlidersHorizontal size={16} />{t('agents.harness.button')}
</button>
) : null;
return (
<section className={`page management-page agents-page${embedded ? ' agents-page-embedded' : ''}`}>
<header className="management-header">
@@ -1796,7 +1806,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
{selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ? <p className="agent-inline-message warning">{t('agents.backup.mappingRequired')}</p> : null}
<div className="agent-minimal-field">
<label htmlFor="embedded-agent-model">{t('agents.useModel')}</label>
<label htmlFor="embedded-agent-model">{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</label>
<AgentModelPicker
models={isClaudeModelMappingClient ? claudeMappingModels : models}
value={isClaudeModelMappingClient ? claudeModelMappingsDraft.sonnet : selectedModel}
@@ -1806,9 +1816,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
onChange={selectEmbeddedModel}
onRefresh={refreshModels}
/>
{codexCatalogButton}
{codexCatalogButton}{harnessCatalogButton}
</div>
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
<div className="agent-minimal-actions">
<button
type="button"
@@ -1894,7 +1906,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
{!isClaudeModelMappingClient ? (
<section className="agent-core-setting-section agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong></div>
<div><strong>{t(isDeepSeekHarnessClient ? 'agents.harness.defaultModel' : 'agents.useModel')}</strong></div>
</div>
<AgentModelPicker
models={models}
@@ -1914,6 +1926,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
{modelHint}
</span>
) : null}
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
{harnessCatalogButton}
{selected === 'codex' ? (
<div className="agent-codex-options">
<div className="agent-auth-method">
@@ -2140,6 +2154,10 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
setConfigurationNotice(t('agents.backup.restored'));
}} /> : null}
{harnessCatalogDialogOpen ? (
<DeepSeekHarnessCatalogDialog onClose={() => setHarnessCatalogDialogOpen(false)}
onSaved={async () => { await loadModels('deepseek-harness', selectedModel); await loadStatuses(true); }} />
) : null}
{codexCatalogDialogOpen ? (
<CodexModelCatalogDialog
onClose={() => setCodexCatalogDialogOpen(false)}
+169
View File
@@ -0,0 +1,169 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { LoaderCircle, RefreshCw, RotateCcw, Search, Settings2, X } from 'lucide-react';
import { useI18n } from '../i18n';
import type { MessageKey } from '../i18n/resources';
import {
harnessDraft, harnessReasoningLevels, harnessSchema, isHarnessRecord, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft,
type HarnessDraft, type HarnessEditorSnapshot, type HarnessProfile,
} from '../services/deepSeekHarnessCatalog';
function HarnessFields({ group, draft, defaults = {}, prefix = '', api, onChange }: {
group: string; draft: HarnessDraft; defaults?: HarnessProfile; prefix?: string; api: string;
onChange: (next: HarnessDraft) => void;
}) {
const { t } = useI18n();
const label = (name: string) => t(`agents.harness.field.${name}` as MessageKey);
return <div className="codex-catalog-form harness-catalog-form">{harnessSchema[group].map(field => {
const key = `${prefix}${field.name}`;
const value = draft[key] ?? '';
const supplied = defaults[field.name] ?? field.default;
const automatic = `${t('agents.harness.auto')}${supplied === undefined ? '' : ` (${typeof supplied === 'string' ? supplied : JSON.stringify(supplied)})`}`;
const set = (next: string) => onChange(updateHarnessDraft(draft, key, next));
if (field.apis && !field.apis.includes(api) && !value) return null;
if (field.group) {
const customized = Object.keys(draft).some(k => k.startsWith(`${key}.`));
return <details className="harness-catalog-group" key={key}>
<summary>{label(field.name)}{customized ? ` · ${t('agents.catalog.customized')}` : ''}</summary>
<p className="codex-catalog-hint">{t(`agents.harness.hint.${field.name}` as MessageKey)}</p>
<HarnessFields group={field.group} draft={draft} defaults={isHarnessRecord(supplied) ? supplied : {}} prefix={`${key}.`} api={api} onChange={onChange} />
{customized ? <button type="button" className="secondary-button" onClick={() => onChange(Object.fromEntries(Object.entries(draft).filter(([k]) => !k.startsWith(`${key}.`))))}>{t('agents.harness.resetGroup')}</button> : null}
</details>;
}
if (field.kind === 'reasoning') {
let mapping: HarnessProfile = {};
try { const parsed: unknown = JSON.parse(value || '{}'); if (isHarnessRecord(parsed)) mapping = parsed; } catch {}
return <div className="harness-reasoning wide" key={key}>
<label><span>{label(field.name)}</span><select aria-label={label(field.name)} value={!value ? '' : value === 'false' ? 'false' : 'custom'}
onChange={e => set(e.currentTarget.value === 'custom' ? JSON.stringify({ off: null, low: 'low', medium: 'medium', high: 'high' }) : e.currentTarget.value)}>
<option value="">{automatic}</option><option value="false">{t('agents.harness.reasoningDisabled')}</option><option value="custom">{t('agents.harness.reasoningCustom')}</option>
</select></label>
{value && value !== 'false' ? <div className="harness-reasoning-levels">{harnessReasoningLevels.map(level => <div key={level}>
<label><input type="checkbox" checked={Object.prototype.hasOwnProperty.call(mapping, level)} onChange={e => {
const next = { ...mapping }; if (e.currentTarget.checked) next[level] = level === 'off' ? null : level; else delete next[level]; set(JSON.stringify(next));
}} />{level}</label>
{Object.prototype.hasOwnProperty.call(mapping, level) ? <input aria-label={`${level} reasoning_effort`} value={String(mapping[level] ?? '')} placeholder={level === 'off' ? t('agents.harness.noWireValue') : level}
onChange={e => set(JSON.stringify({ ...mapping, [level]: level === 'off' && !e.currentTarget.value ? null : e.currentTarget.value }))} /> : null}
</div>)}</div> : null}
<small>{t('agents.harness.hint.reasoningEfforts')}</small>
</div>;
}
const jsonField = ['headers', 'kwargs', 'strings'].includes(field.kind);
return <label key={key} className={jsonField ? 'wide' : undefined}>
<span title={field.name}>{label(field.name)}{Object.prototype.hasOwnProperty.call(draft, key) ? <small className="harness-override">{t('agents.catalog.customized')}</small> : null}</span>
{['enum', 'boolean', 'modalities'].includes(field.kind) ? <select aria-label={label(field.name)} value={value} onChange={e => set(e.currentTarget.value)}>
<option value="">{automatic}</option>
{field.kind === 'boolean' ? <><option value="true">{t('agents.harness.yes')}</option><option value="false">{t('agents.harness.no')}</option></> : null}
{field.kind === 'enum' ? field.values!.map(v => <option key={v} value={v}>{v}</option>) : null}
{field.kind === 'modalities' ? <>{field.name === 'input' ? <option value="[]">{t('agents.harness.providerInput')}</option> : null}<option value='["text"]'>{t('agents.harness.textOnly')}</option><option value='["text","image"]'>{t('agents.harness.textImage')}</option><option value='["image"]'>{t('agents.catalog.image')}</option></> : null}
</select> : jsonField ? <textarea aria-label={label(field.name)} rows={4} spellCheck={false} value={value} placeholder={JSON.stringify(supplied ?? field.example, null, 2) ?? automatic} onChange={e => set(e.currentTarget.value)} />
: <input aria-label={label(field.name)} type={['integer', 'number'].includes(field.kind) ? 'number' : 'text'} min={field.min ?? field.exclusiveMin} max={field.max} step={field.kind === 'integer' ? 1 : 'any'} value={value} placeholder={automatic} onChange={e => set(e.currentTarget.value)} />}
{['input', 'defaultInput', 'maxTokens', 'defaultMaxTokens', 'api', 'headers'].includes(field.name) ? <small>{t(`agents.harness.hint.${field.name}` as MessageKey)}</small> : null}
</label>;
})}</div>;
}
export function DeepSeekHarnessCatalogDialog({ onClose, onSaved }: { onClose: () => void; onSaved: () => void | Promise<void> }) {
const { t } = useI18n();
const [snapshot, setSnapshot] = useState<HarnessEditorSnapshot | null>(null);
const [drafts, setDrafts] = useState<Record<string, HarnessDraft>>({});
const [provider, setProvider] = useState<HarnessDraft>({});
const [selected, setSelected] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [confirmation, setConfirmation] = useState<'close' | 'reload' | null>(null);
const dialogRef = useRef<HTMLElement>(null);
const install = (next: HarnessEditorSnapshot) => {
setSnapshot(next);
setDrafts(Object.fromEntries(next.models.map(m => [m.id, harnessDraft(m.configuration, 'model')])));
setProvider(harnessDraft(next.provider, 'provider'));
};
const load = useCallback(async () => {
setLoading(true); setError(''); setNotice('');
try {
const next = await invoke<HarnessEditorSnapshot>('get_deepseek_harness_model_catalog_editor');
install(next);
setSelected(current => current && next.models.some(m => m.id === current) ? current : next.defaultModel ?? next.models[0]?.id ?? null);
} catch (e) { setError(String(e)); }
finally { setLoading(false); }
}, []);
useEffect(() => { void load(); }, [load]);
const dirty = !!snapshot && (!sameHarnessDraft(provider, harnessDraft(snapshot.provider, 'provider'))
|| snapshot.models.some(m => !sameHarnessDraft(drafts[m.id] ?? {}, harnessDraft(m.configuration, 'model'))));
const close = () => { if (!saving) { if (dirty) setConfirmation('close'); else onClose(); } };
useEffect(() => {
const previous = document.activeElement as HTMLElement | null;
dialogRef.current?.querySelector<HTMLElement>('button')?.focus();
return () => previous?.focus();
}, []);
useEffect(() => {
const handleKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') { event.stopPropagation(); if (confirmation) setConfirmation(null); else close(); }
if (event.key === 'Tab') {
const scope = confirmation ? dialogRef.current?.querySelector('[role="alertdialog"]') : dialogRef.current;
const targets = [...(scope?.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), summary') ?? [])].filter(el => el.getClientRects().length);
const first = targets[0], last = targets[targets.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
}
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
});
const active = snapshot?.models.find(m => m.id === selected);
const api = provider.api || 'openai-completions';
const changed = () => { setNotice(''); setError(''); };
const save = async () => {
if (!snapshot || saving || loading) return;
let request;
try {
let configuration;
try { configuration = parseHarnessDraft(provider, 'provider', api); }
catch (e) { setSelected(null); throw e; }
request = { revision: snapshot.revision, provider: configuration, models: snapshot.models.map(m => {
try { return { ...m, configuration: parseHarnessDraft(drafts[m.id] ?? {}, 'model', api) }; }
catch (e) { setSelected(m.id); throw e; }
}) };
} catch (e) { setError(t('agents.harness.invalid', { field: String(e).replace(/^Error: /, '') })); return; }
setSaving(true); setError(''); setNotice('');
try {
const next = await invoke<HarnessEditorSnapshot>('save_deepseek_harness_model_catalog_editor', { request });
install(next); setNotice(t(next.configured ? 'agents.harness.saved' : 'agents.harness.savedDraft'));
await onSaved();
} catch (e) { setError(String(e).includes('DSH_MODEL_CATALOG_CHANGED') ? t('agents.harness.changed') : String(e)); }
finally { setSaving(false); }
};
return <div className="config-dialog-backdrop codex-catalog-backdrop" onMouseDown={e => { if (e.target === e.currentTarget) close(); }}>
<section ref={dialogRef} className="config-dialog codex-catalog-dialog harness-catalog-dialog" role="dialog" aria-modal="true" aria-labelledby="harness-catalog-title">
<header className="config-dialog-heading codex-catalog-heading"><div><h2 id="harness-catalog-title">{t('agents.harness.title')}</h2><p>{t('agents.harness.subtitle')}</p></div><button className="icon-button quiet" onClick={close} disabled={saving} aria-label={t('common.close')}><X size={18} /></button></header>
<div className="codex-catalog-body">
<aside className="codex-catalog-sidebar">
<button className={`secondary-button harness-provider-button${selected === null ? ' active' : ''}`} onClick={() => setSelected(null)}><Settings2 size={16} />{t('agents.harness.provider')}</button>
<div className="codex-catalog-search"><Search size={15} /><input aria-label={t('agents.catalog.search')} value={search} placeholder={t('agents.catalog.search')} onChange={e => setSearch(e.currentTarget.value)} /></div>
<div className="codex-catalog-models" role="listbox" aria-label={t('agents.harness.title')}>
{loading ? <div className="codex-catalog-state"><LoaderCircle className="spin" size={18} />{t('agents.catalog.loading')}</div> : snapshot?.models.filter(m => `${m.id} ${drafts[m.id]?.name ?? m.defaults.name ?? ''}`.toLowerCase().includes(search.toLowerCase())).map(m => <button role="option" aria-selected={selected === m.id} className={selected === m.id ? 'active' : ''} key={m.id} onClick={() => setSelected(m.id)}>
<strong>{drafts[m.id]?.name || String(m.defaults.name ?? m.id)}</strong><span>{m.id}</span><small>{t(Object.keys(drafts[m.id] ?? {}).length ? 'agents.catalog.customized' : 'agents.harness.apiSource')}{m.id === snapshot.defaultModel ? ` · ${t('agents.harness.defaultModel')}` : ''}</small>
</button>)}
{!loading && snapshot?.models.length === 0 ? <div className="codex-catalog-state">{t('agents.catalog.empty')}</div> : null}
</div>
<button className="secondary-button codex-catalog-reload" disabled={loading || saving} onClick={() => dirty ? setConfirmation('reload') : void load()}><RefreshCw size={15} />{t('agents.catalog.reload')}</button>
</aside>
<main className="codex-catalog-editor"><fieldset disabled={loading || saving} className="harness-editor-fieldset">
{snapshot && (active || selected === null) ? <>
<div className="codex-catalog-model-heading"><div><h3>{active ? active.id : t('agents.harness.provider')}</h3><span>{t('agents.harness.inheritHint')}</span></div><button className="secondary-button" onClick={() => { if (active) setDrafts(current => ({ ...current, [active.id]: {} })); else setProvider({}); changed(); }}><RotateCcw size={14} />{t('agents.harness.reset')}</button></div>
{active ? <p className="codex-catalog-hint" role="note">{t('agents.harness.apiInput', { value: Array.isArray(active.defaults.input) ? active.defaults.input.join(', ') : t('agents.harness.unknown') })}</p> : <p className="codex-catalog-hint">{t('agents.harness.managedConnection', { url: api === 'anthropic-messages' ? snapshot.baseUrl.replace(/\/v1$/, '') : snapshot.baseUrl })}</p>}
<HarnessFields key={active?.id ?? 'provider'} group={active ? 'model' : 'provider'} draft={active ? drafts[active.id] ?? {} : provider} defaults={active?.defaults} api={api} onChange={next => { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} />
</> : null}
</fieldset></main>
</div>
<footer className="codex-catalog-footer"><div aria-live="polite">{error ? <span className="agent-inline-message error" role="alert">{error}</span> : notice ? <span role="status">{notice}</span> : <span>{t(dirty ? 'agents.catalog.unsaved' : 'agents.harness.saveHint')}</span>}</div><div>
<button className="secondary-button" disabled={loading || saving || !snapshot} onClick={() => { setDrafts(Object.fromEntries(snapshot!.models.map(m => [m.id, {}]))); setProvider({}); changed(); }}>{t('agents.catalog.resetAll')}</button>
<button className="secondary-button" disabled={saving} onClick={close}>{t('common.cancel')}</button><button className="primary-button" disabled={!dirty || loading || saving} onClick={() => void save()}>{saving ? <LoaderCircle size={16} className="spin" /> : null}{t(saving ? 'common.saving' : 'common.save')}</button>
</div></footer>
{confirmation ? <div className="codex-catalog-confirm"><div role="alertdialog" aria-modal="true" aria-label={t('agents.catalog.unsaved')}><strong>{t('agents.catalog.unsaved')}</strong><span>{t('agents.catalog.discardHint')}</span><div><button autoFocus className="secondary-button" onClick={() => setConfirmation(null)}>{t('agents.catalog.keepEditing')}</button><button className="danger-button" onClick={() => { if (confirmation === 'close') onClose(); else { setConfirmation(null); void load(); } }}>{t('agents.catalog.discard')}</button></div></div></div> : null}
</section>
</div>;
}