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

+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, test } from 'bun:test';
import { harnessDraft, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft } from '../src/services/deepSeekHarnessCatalog';
describe('DSH catalog overrides', () => {
test('empty values inherit while explicit false and zero remain overrides', () => {
const draft = harnessDraft({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } }, 'provider');
expect(parseHarnessDraft(draft, 'provider', 'openai-completions')).toEqual({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } });
expect(parseHarnessDraft(updateHarnessDraft(draft, 'timeoutMs', ''), 'provider', 'openai-completions').timeoutMs).toBeUndefined();
expect(parseHarnessDraft({}, 'model', 'openai-completions')).toEqual({});
expect(parseHarnessDraft({ input: '[]' }, 'model', 'openai-completions')).toEqual({ input: [] });
});
test('reasoning levels keep wire values and only off allows null', () => {
expect(parseHarnessDraft({ reasoningEfforts: '{"off":null,"high":"custom-high"}' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: { off: null, high: 'custom-high' } });
expect(parseHarnessDraft({ reasoningEfforts: 'false' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: false });
expect(() => parseHarnessDraft({ reasoningEfforts: '{"high":null}' }, 'model', 'openai-completions')).toThrow();
expect(() => parseHarnessDraft({ reasoningEfforts: '{}' }, 'model', 'openai-completions')).toThrow();
});
test('invalid numbers, JSON, headers and incompatible protocol switches are rejected', () => {
for (const draft of [{ maxTokens: '-1' }, { input: '["audio"]' }, { contextWindow: '2.5' }, { 'compat.supportsStore': 'null' }, { 'compat.supportsTemperature': 'true' }]) {
expect(() => parseHarnessDraft(draft, 'model', 'openai-completions')).toThrow();
}
expect(() => parseHarnessDraft({ headers: '{bad json}' }, 'provider', 'openai-completions')).toThrow();
expect(() => parseHarnessDraft({ headers: '{"x-test":"a\\r\\nb"}' }, 'provider', 'openai-completions')).toThrow();
expect(() => parseHarnessDraft({ 'retryPolicy.backoff.initialDelayMs': '20000' }, 'provider', 'openai-completions')).toThrow();
});
test('nested groups round trip and follow current protocol', () => {
const profile = { api: 'anthropic-messages', compat: { supportsTemperature: false }, retryPolicy: { mode: 'always', backoff: { maxDelayMs: 60000 } }, thinkingBudgets: { high: 8192 } };
expect(parseHarnessDraft(harnessDraft(profile, 'provider'), 'provider', 'anthropic-messages')).toEqual(profile);
expect(sameHarnessDraft({ input: '["text"]', name: 'A' }, { name: 'A', input: '["text"]' })).toBe(true);
});
});
+84
View File
@@ -0,0 +1,84 @@
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const page = await browser.newPage();
const errors = [];
page.on('pageerror', e => errors.push(e.message));
const open = async (query = '') => {
await page.goto(`http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=deepseek-harness&reset-selections&${query}`);
await page.getByRole('button', { name: '配置模型', exact: true }).click();
await page.getByRole('option', { name: /gpt-one/ }).waitFor();
};
const dialog = () => page.getByRole('dialog', { name: 'DeepSeek Harness 模型配置' });
const save = async () => {
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await dialog().getByRole('status').filter({ hasText: /已保存/ }).waitFor();
};
try {
for (const mode of ['', 'embedded&']) {
await open(mode);
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
await dialog().getByRole('option', { name: /unknown-model/ }).click();
await dialog().getByText(/未提供(不代表不支持)/).waitFor();
await dialog().getByRole('combobox', { name: '输入类型', exact: true }).selectOption('["text","image"]');
await dialog().getByRole('option', { name: /gpt-two/ }).click();
await dialog().getByRole('spinbutton', { name: '输出上限(tokens)' }).fill('4096');
await dialog().getByRole('combobox', { name: '推理等级', exact: true }).selectOption('custom');
await dialog().getByRole('textbox', { name: 'high reasoning_effort' }).fill('custom-high');
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
await dialog().getByRole('combobox', { name: '默认输入类型', exact: true }).selectOption('["text","image"]');
await dialog().getByRole('spinbutton', { name: '请求超时(ms)' }).fill('60000');
await save();
const request = await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').at(-1).args.request);
assert.deepEqual(request.models.find(m => m.id === 'unknown-model').configuration.input, ['text','image']);
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.maxTokens, 4096);
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.reasoningEfforts.high, 'custom-high');
assert.equal(request.provider.timeoutMs, 60000);
assert.equal(await page.evaluate(() => window.fixtureCalls.some(c => c.cmd === 'update_agent_config')), false);
await dialog().getByRole('button', { name: '关闭', exact: true }).click();
assert.match(await page.locator('.agent-model-trigger').innerText(), /gpt-one/);
await page.evaluate(() => window.fixtureRemount());
await page.getByRole('button', { name: '配置模型', exact: true }).click();
await dialog().getByRole('option', { name: /unknown-model/ }).click();
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '["text","image"]');
await dialog().getByRole('button', { name: '恢复自动', exact: true }).click();
await save();
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('0');
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await dialog().getByRole('alert').filter({ hasText: /contextWindow/ }).waitFor();
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('64000');
await page.evaluate(() => window.fixtureFailHarnessSave = true);
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await dialog().getByRole('alert').filter({ hasText: /模拟模型配置保存失败/ }).waitFor();
assert.equal(await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).inputValue(), '64000');
await page.evaluate(() => { window.fixtureFailHarnessSave = false; window.fixtureStaleHarnessSave = true; });
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await dialog().getByRole('alert').filter({ hasText: /模型列表或配置已变化/ }).waitFor();
await page.evaluate(() => window.fixtureStaleHarnessSave = false);
await page.keyboard.press('Escape');
await page.getByRole('alertdialog').waitFor();
await page.getByRole('button', { name: '继续编辑', exact: true }).click();
await save();
}
const screenshots = path.join(os.tmpdir(), 'cpa-harness-catalog-ui');
fs.mkdirSync(screenshots, { recursive: true });
for (const width of [1280, 540]) for (const theme of ['light', 'dark']) {
await page.setViewportSize({ width, height: 900 });
await open(`theme=${theme}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-model.png`) });
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
assert.equal(await page.evaluate(() => document.querySelector('.codex-catalog-editor').scrollWidth > document.querySelector('.codex-catalog-editor').clientWidth), false);
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-provider.png`) });
}
assert.deepEqual(errors, []);
console.log('PASS: default model preserved, catalog overrides, API reset, reasoning, validation, retry, stale save, responsive themes');
console.log(screenshots);
} finally { await browser.close(); }
})().catch(e => { console.error(e); process.exit(1); });
+22 -1
View File
@@ -12,6 +12,15 @@ localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') ||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','zcode','kimi-code','grok-build','pi'];
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
let currentOauth=false; const currentMappings:Record<string,any>={};
let harnessProvider:Record<string,unknown>={};
const harnessConfigurations:Record<string,Record<string,unknown>>={};
let harnessRevision=1;
const harnessModels=[
{id:'gpt-one',defaults:{input:['text','image'],contextWindow:128000}},
{id:'gpt-two',defaults:{input:['text']}},
{id:'unknown-model',defaults:{}},
];
const harnessSnapshot=()=>({revision:String(harnessRevision),provider:harnessProvider,baseUrl:'http://127.0.0.1:8317/v1',defaultModel:currentModel,configured:!!currentModel,models:harnessModels.map(m=>({...m,configuration:harnessConfigurations[m.id]??{}}))});
let harnessStatus={running:params.has('running'),pid:params.has('running')?100:null as number|null,mode:params.has('running')?params.get('harness-mode')||'web':null as string|null};
let appliedCount=0;
const calls:any[]=[];(window as any).fixtureCalls=calls;
@@ -22,7 +31,10 @@ mockIPC(async (cmd,args:any) => {
if(cmd==='plugin:event|listen') return 1;
if(cmd==='plugin:event|unlisten'||cmd==='set_app_locale') return null;
if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&&currentOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null}));
if(cmd==='get_agent_models') return [{name:'gpt-one'},{name:'gpt-two'}];
if(cmd==='get_agent_models') {
if(args.client==='deepseek-harness')return harnessModels.map(m=>({name:m.id,inputModalities:m.defaults.input}));
return [{name:'gpt-one'},{name:'gpt-two'}];
}
if(cmd==='list_codex_sessions') {
const {offset,limit}=args.request;
const sessionIds:string[]=[...(window as any).fixtureSessionIds];
@@ -63,6 +75,15 @@ mockIPC(async (cmd,args:any) => {
if(cmd==='preview_agent_config_template') return {revision:'template1',files:['C:/test/.codex/config.toml','C:/test/.codex/auth.json','C:/test/.codex/models.json']};
if(cmd==='apply_agent_config_template') {currentModel=args.model;return {outcome:'updated'};}
if(cmd==='check_pi_provider_update') return {installedVersion:'1.0',latestVersion:params.has('update')?'1.1':'1.0',updateAvailable:params.has('update')};
if(cmd==='get_deepseek_harness_model_catalog_editor')return harnessSnapshot();
if(cmd==='save_deepseek_harness_model_catalog_editor') {
if((window as any).fixtureFailHarnessSave)throw new Error('模拟模型配置保存失败');
if((window as any).fixtureStaleHarnessSave)throw new Error('DSH_MODEL_CATALOG_CHANGED');
if((window as any).fixtureDeferHarnessSave)await new Promise<void>(resolve=>{(window as any).fixtureFinishHarnessSave=resolve;});
harnessProvider=args.request.provider;
for(const m of args.request.models)harnessConfigurations[m.id]=m.configuration;
harnessRevision++;return harnessSnapshot();
}
if(cmd==='get_codex_model_catalog_editor') return {models:[],hiddenModels:[],customizations:{}};
if(cmd==='check_codex_oauth_login')return null;
throw new Error('Unhandled fixture command: '+cmd);