From 7e2dc55b5d064968edb27542c7acdd8f673c53da Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 1 Sep 2026 01:54:51 -0400 Subject: [PATCH] fix(dsh): persist managed runtime settings (#626) --- .../Dsh/__tests__/configMapping.test.ts | 52 +++++++++++++++++++ .../src/views/Settings/Dsh/configMapping.ts | 47 +++++++++++++++++ mateclaw-ui/src/views/Settings/Dsh/index.vue | 13 +++-- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 mateclaw-ui/src/views/Settings/Dsh/__tests__/configMapping.test.ts create mode 100644 mateclaw-ui/src/views/Settings/Dsh/configMapping.ts diff --git a/mateclaw-ui/src/views/Settings/Dsh/__tests__/configMapping.test.ts b/mateclaw-ui/src/views/Settings/Dsh/__tests__/configMapping.test.ts new file mode 100644 index 00000000..700a2c63 --- /dev/null +++ b/mateclaw-ui/src/views/Settings/Dsh/__tests__/configMapping.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { + createEmptyDshConfigForm, + formToManagedConfig, + managedConfigToForm, +} from '../configMapping' + +describe('DSH managed configuration mapping', () => { + it('maps canonical managed keys into the settings form', () => { + expect(managedConfigToForm({ + 'dsh.executable_path': '/opt/dsh/bin', + 'dsh.cordis_config_path': '/opt/dsh/cordis.yml', + 'dsh.working_directory': '/srv/workspace', + 'dsh.base_url': 'https://api.deepseek.com', + 'dsh.model_name': 'deepseek-chat', + 'dsh.api_key': 'plain-test-key', + })).toEqual({ + executable_path: '/opt/dsh/bin', + cordis_config_path: '/opt/dsh/cordis.yml', + working_directory: '/srv/workspace', + base_url: 'https://api.deepseek.com', + model_name: 'deepseek-chat', + api_key: 'plain-test-key', + }) + }) + + it('uses empty form values for missing managed keys and masked secrets', () => { + expect(managedConfigToForm({ 'dsh.api_key': '****abcd' })) + .toEqual(createEmptyDshConfigForm()) + }) + + it('maps every settings form field to the canonical backend key', () => { + const payload = formToManagedConfig({ + executable_path: '/opt/dsh/bin', + cordis_config_path: '/opt/dsh/cordis.yml', + working_directory: '/srv/workspace', + base_url: 'https://api.deepseek.com', + model_name: 'deepseek-chat', + api_key: '', + }) + + expect(payload).toEqual({ + 'dsh.executable_path': '/opt/dsh/bin', + 'dsh.cordis_config_path': '/opt/dsh/cordis.yml', + 'dsh.working_directory': '/srv/workspace', + 'dsh.base_url': 'https://api.deepseek.com', + 'dsh.model_name': 'deepseek-chat', + 'dsh.api_key': '', + }) + expect(payload).not.toHaveProperty('executable_path') + }) +}) diff --git a/mateclaw-ui/src/views/Settings/Dsh/configMapping.ts b/mateclaw-ui/src/views/Settings/Dsh/configMapping.ts new file mode 100644 index 00000000..09316f40 --- /dev/null +++ b/mateclaw-ui/src/views/Settings/Dsh/configMapping.ts @@ -0,0 +1,47 @@ +export interface DshConfigForm { + executable_path: string + cordis_config_path: string + working_directory: string + base_url: string + model_name: string + api_key: string +} + +const MANAGED_KEYS = { + executable_path: 'dsh.executable_path', + cordis_config_path: 'dsh.cordis_config_path', + working_directory: 'dsh.working_directory', + base_url: 'dsh.base_url', + model_name: 'dsh.model_name', + api_key: 'dsh.api_key', +} as const satisfies Record + +const FORM_FIELDS = Object.keys(MANAGED_KEYS) as Array + +export function createEmptyDshConfigForm(): DshConfigForm { + return { + executable_path: '', + cordis_config_path: '', + working_directory: '', + base_url: '', + model_name: '', + api_key: '', + } +} + +export function managedConfigToForm( + managed: Record, +): DshConfigForm { + const form = createEmptyDshConfigForm() + for (const field of FORM_FIELDS) { + form[field] = managed[MANAGED_KEYS[field]] || '' + } + if (form.api_key.startsWith('****')) form.api_key = '' + return form +} + +export function formToManagedConfig(form: DshConfigForm): Record { + return Object.fromEntries( + FORM_FIELDS.map(field => [MANAGED_KEYS[field], form[field]]), + ) +} diff --git a/mateclaw-ui/src/views/Settings/Dsh/index.vue b/mateclaw-ui/src/views/Settings/Dsh/index.vue index 49c2f3c0..692d0cad 100644 --- a/mateclaw-ui/src/views/Settings/Dsh/index.vue +++ b/mateclaw-ui/src/views/Settings/Dsh/index.vue @@ -56,12 +56,17 @@ import { computed, onMounted, reactive, ref } from 'vue' import { dshApi } from '@/api' import { mcToast } from '@/composables/useMcToast' +import { + createEmptyDshConfigForm, + formToManagedConfig, + managedConfigToForm, +} from './configMapping' const loading = ref(true) const busy = ref(false) const error = ref('') const status = reactive({ state: 'NOT_INSTALLED', installed: false, enabled: false, config: {}, artifactManifestConfigured: false }) -const form = reactive>({ executable_path: '', cordis_config_path: '', working_directory: '', base_url: '', model_name: '', api_key: '' }) +const form = reactive(createEmptyDshConfigForm()) const state = computed(() => String(status.state || 'NOT_INSTALLED')) const stateLabel = computed(() => ({ NOT_INSTALLED: '未安装', INSTALLING: '安装中', INSTALLED_UNCONFIGURED: '已安装待验证', CONFIG_INVALID: '配置不完整', CHECKING: '检测中', CHECK_FAILED: '检测失败', READY: '已就绪', ENABLED: '已启用' } as Record)[state.value] || state.value) const canEnable = computed(() => status.installed && status.config?.workingDirectory && state.value !== 'CONFIG_INVALID') @@ -75,14 +80,12 @@ const steps = computed(() => [ function applyResponse(response: any) { const data = response?.data ?? response Object.assign(status, data) - const managed = data?.managed || {} - for (const key of Object.keys(form)) form[key] = managed[key] || '' - if (form.api_key.startsWith('****')) form.api_key = '' + Object.assign(form, managedConfigToForm(data?.managed || {})) } async function load() { loading.value = true; error.value = ''; try { applyResponse(await dshApi.status()) } catch (e: any) { error.value = e?.message || '读取 DSH 状态失败' } finally { loading.value = false } } async function run(action: () => Promise, message: string) { busy.value = true; error.value = ''; try { applyResponse(await action()); mcToast.success(message) } catch (e: any) { error.value = e?.message || '操作失败'; mcToast.error(error.value) } finally { busy.value = false } } -function save() { return run(() => dshApi.saveConfig(form), 'DSH 配置已保存') } +function save() { return run(() => dshApi.saveConfig(formToManagedConfig(form)), 'DSH 配置已保存') } function verify() { return run(dshApi.verify, 'DSH 配置验证完成') } function install() { return run(dshApi.install, 'DSH 安装完成') } function testConnection() { return run(async () => { const response: any = await dshApi.testConnection(); const data = response?.data ?? response; if (data?.success === false) throw new Error(data.message || 'DSH 进程测试失败'); return response }, 'DSH 进程测试通过') }