fix(dsh): persist managed runtime settings (#626)

This commit is contained in:
matevip 2026-09-01 01:54:51 -04:00
parent 631b8ab02a
commit 7e2dc55b5d
3 changed files with 107 additions and 5 deletions

View File

@ -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')
})
})

View File

@ -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<keyof DshConfigForm, string>
const FORM_FIELDS = Object.keys(MANAGED_KEYS) as Array<keyof DshConfigForm>
export function createEmptyDshConfigForm(): DshConfigForm {
return {
executable_path: '',
cordis_config_path: '',
working_directory: '',
base_url: '',
model_name: '',
api_key: '',
}
}
export function managedConfigToForm(
managed: Record<string, string | null | undefined>,
): 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<string, string> {
return Object.fromEntries(
FORM_FIELDS.map(field => [MANAGED_KEYS[field], form[field]]),
)
}

View File

@ -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<any>({ state: 'NOT_INSTALLED', installed: false, enabled: false, config: {}, artifactManifestConfigured: false })
const form = reactive<Record<string, string>>({ 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<string, string>)[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<any>, 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 进程测试通过') }