feat: add dynamic Robot configuration choices
This commit is contained in:
@@ -4,6 +4,7 @@ import { AiHardware } from '@/pages/AiHardware';
|
||||
import {
|
||||
AiHardwareApiError,
|
||||
type AiHardwareAgentConfiguration,
|
||||
type AiHardwareConfigurationCatalog,
|
||||
type AiHardwareOverview,
|
||||
} from '@/lib/ai-hardware';
|
||||
|
||||
@@ -13,6 +14,7 @@ const api = vi.hoisted(() => ({
|
||||
createAiHardwareAgent: vi.fn(),
|
||||
bindAiHardwareDevice: vi.fn(),
|
||||
getAiHardwareAgentConfiguration: vi.fn(),
|
||||
getAiHardwareConfigurationCatalog: vi.fn(),
|
||||
updateAiHardwareAgentConfiguration: vi.fn(),
|
||||
getAiHardwareAssignment: vi.fn(),
|
||||
updateAiHardwareAssignment: vi.fn(),
|
||||
@@ -51,6 +53,24 @@ const configuration: AiHardwareAgentConfiguration = {
|
||||
tts_volume: 10, tts_rate: 5, tts_pitch: 0, mem_model_id: null, intent_model_id: null,
|
||||
chat_history_conf: 1,
|
||||
};
|
||||
const catalog: AiHardwareConfigurationCatalog = {
|
||||
schema_version: 1,
|
||||
models: [
|
||||
['ASR', 'asr-old', '旧语音识别'], ['ASR', 'asr-new', '新语音识别'],
|
||||
['VAD', 'vad-old', '旧活动检测'], ['VAD', 'vad-new', '新活动检测'],
|
||||
['LLM', 'llm-old', '旧语言模型'], ['LLM', 'llm-new', '新语言模型'],
|
||||
['LLM', 'slm-new', '轻量语言模型'], ['VLLM', 'vllm-old', '旧视觉模型'],
|
||||
['TTS', 'tts-old', '旧语音合成'], ['TTS', 'tts-new', '新语音合成'],
|
||||
['Memory', 'mem-old', '旧记忆'], ['Memory', 'mem-new', '新记忆'],
|
||||
['Intent', 'intent-old', '旧意图'], ['Intent', 'intent-new', '新意图'],
|
||||
].map(([model_type, model_id, model_name]) => ({
|
||||
model_type, model_id, model_name, supports_function_call: null,
|
||||
})) as AiHardwareConfigurationCatalog['models'],
|
||||
voices: [{
|
||||
tts_model_id: 'tts-new', voice_id: 'voice-new', voice_name: '龙小夏',
|
||||
languages: ['zh-CN', 'en-US'], is_clone: false,
|
||||
}],
|
||||
};
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
@@ -70,6 +90,7 @@ describe('AI hardware page', () => {
|
||||
api.getAiHardwareOverview.mockResolvedValue(activeOverview);
|
||||
api.recoverAiHardwareCredential.mockResolvedValue({ status: 'active', agents: [], devices: [] });
|
||||
api.getAiHardwareAgentConfiguration.mockResolvedValue({ data: configuration, revision: 4 });
|
||||
api.getAiHardwareConfigurationCatalog.mockResolvedValue(catalog);
|
||||
api.createAiHardwareAgent.mockResolvedValue(agentOne);
|
||||
api.bindAiHardwareDevice.mockResolvedValue(device);
|
||||
api.updateAiHardwareAgentConfiguration.mockResolvedValue({ data: configuration, revision: 5 });
|
||||
@@ -149,8 +170,8 @@ describe('AI hardware page', () => {
|
||||
await openConfigurationEditor();
|
||||
fireEvent.change(screen.getByLabelText('名称'), { target: { value: '新的助手' } });
|
||||
fireEvent.change(screen.getByLabelText('系统提示'), { target: { value: '' } });
|
||||
fireEvent.change(screen.getByLabelText('TTS 语音 ID'), { target: { value: '' } });
|
||||
fireEvent.change(screen.getByLabelText('音量 (-100–100)'), { target: { value: '20' } });
|
||||
fireEvent.change(screen.getByLabelText('声音音色'), { target: { value: '' } });
|
||||
fireEvent.change(screen.getByLabelText('音量'), { target: { value: '20' } });
|
||||
fireEvent.change(screen.getByLabelText('聊天记录'), { target: { value: '2' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
@@ -237,7 +258,6 @@ describe('AI hardware page', () => {
|
||||
});
|
||||
render(<AiHardware />);
|
||||
await openConfigurationEditor();
|
||||
fireEvent.click(screen.getByText('高级设置'));
|
||||
|
||||
const values: Record<string, string> = {
|
||||
asr_model_id: '', vad_model_id: 'vad-new', llm_model_id: '', slm_model_id: 'slm-new',
|
||||
@@ -260,6 +280,61 @@ describe('AI hardware page', () => {
|
||||
expect(Object.values(update)).not.toContain(null);
|
||||
});
|
||||
|
||||
it('uses dynamic model, language, and voice choices instead of free-form IDs', async () => {
|
||||
api.getAiHardwareAgentConfiguration.mockResolvedValueOnce({
|
||||
data: { ...configuration, tts_model_id: 'tts-old', tts_voice_id: 'legacy-voice' },
|
||||
revision: 4,
|
||||
});
|
||||
render(<AiHardware />);
|
||||
await openConfigurationEditor();
|
||||
|
||||
expect(api.getAiHardwareConfigurationCatalog).toHaveBeenCalledWith('tts-old');
|
||||
expect(screen.getByLabelText('语音合成 (TTS)')).toHaveValue('tts-old');
|
||||
expect(screen.getByRole('option', { name: '旧语音合成' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('option', { name: '当前值(目录中不可用):legacy-voice' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('语音合成 (TTS)'), { target: { value: 'tts-new' } });
|
||||
await waitFor(() => expect(api.getAiHardwareConfigurationCatalog).toHaveBeenLastCalledWith('tts-new'));
|
||||
expect(screen.getByRole('option', { name: '龙小夏' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option', { name: 'zh-CN' }).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('uses bounded sliders while preserving nullable provider defaults', async () => {
|
||||
render(<AiHardware />);
|
||||
await openConfigurationEditor();
|
||||
|
||||
const volume = screen.getByLabelText('音量');
|
||||
expect(volume).toHaveAttribute('type', 'range');
|
||||
expect(volume).toHaveAttribute('min', '-100');
|
||||
expect(volume).toHaveAttribute('max', '100');
|
||||
expect(screen.getByRole('button', { name: '音调使用默认值' })).toHaveClass('min-h-10');
|
||||
fireEvent.change(volume, { target: { value: '24' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '音调使用默认值' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||||
|
||||
await waitFor(() => expect(api.updateAiHardwareAgentConfiguration).toHaveBeenCalled());
|
||||
expect(api.updateAiHardwareAgentConfiguration.mock.calls[0][2]).toMatchObject({
|
||||
tts_volume: 24,
|
||||
clear_fields: expect.arrayContaining(['tts_pitch']),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps current values when the catalog is unavailable and offers a safe retry', async () => {
|
||||
api.getAiHardwareConfigurationCatalog
|
||||
.mockRejectedValueOnce(new Error('provider token=secret'))
|
||||
.mockResolvedValueOnce(catalog);
|
||||
render(<AiHardware />);
|
||||
await openConfigurationEditor();
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('当前配置会保持不变');
|
||||
expect(screen.getByRole('option', { name: '当前值(目录中不可用):voice-a' })).toBeInTheDocument();
|
||||
expect(screen.queryByText(/provider token|secret/)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '重新加载选项' })).toHaveClass('min-h-10');
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新加载选项' }));
|
||||
await waitFor(() => expect(api.getAiHardwareConfigurationCatalog).toHaveBeenCalledTimes(2));
|
||||
expect(await screen.findByRole('option', { name: '旧语音合成' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['AI_HARDWARE_AUTH_REQUIRED', '请先登录'],
|
||||
['ai_hardware_unconfigured', '尚未配置'],
|
||||
|
||||
Reference in New Issue
Block a user