import { ASRModel, Assistant, CallLog, InteractionDetail, KnowledgeBase, KnowledgeDocument, LLMModel, Tool, Voice, Workflow, WorkflowEdge, WorkflowNode } from '../types'; import { apiRequest } from './apiClient'; type AnyRecord = Record; const readField = (obj: AnyRecord, keys: string[], fallback: T): T => { for (const key of keys) { if (obj[key] !== undefined && obj[key] !== null) { return obj[key] as T; } } return fallback; }; const normalizeDateLabel = (value: string): string => { if (!value) return ''; return value.includes('T') ? value.replace('T', ' ').slice(0, 16) : value.slice(0, 16); }; const formatDuration = (seconds?: number | null): string => { if (!seconds || seconds <= 0) return '0s'; const m = Math.floor(seconds / 60); const s = seconds % 60; if (m === 0) return `${s}s`; return `${m}m ${s}s`; }; const mapAssistant = (raw: AnyRecord): Assistant => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), callCount: Number(readField(raw, ['callCount', 'call_count'], 0)), opener: readField(raw, ['opener'], ''), prompt: readField(raw, ['prompt'], ''), knowledgeBaseId: readField(raw, ['knowledgeBaseId', 'knowledge_base_id'], ''), language: readField(raw, ['language'], 'zh') as 'zh' | 'en', voice: readField(raw, ['voice'], ''), speed: Number(readField(raw, ['speed'], 1)), hotwords: readField(raw, ['hotwords'], []), tools: readField(raw, ['tools'], []), interruptionSensitivity: Number(readField(raw, ['interruptionSensitivity', 'interruption_sensitivity'], 500)), configMode: readField(raw, ['configMode', 'config_mode'], 'platform') as 'platform' | 'dify' | 'fastgpt' | 'none', apiUrl: readField(raw, ['apiUrl', 'api_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), }); const mapVoice = (raw: AnyRecord): Voice => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: ((): string => { const vendor = String(readField(raw, ['vendor'], '')); return vendor.toLowerCase() === 'siliconflow' ? '硅基流动' : vendor; })(), gender: readField(raw, ['gender'], ''), language: readField(raw, ['language'], ''), description: readField(raw, ['description'], ''), model: readField(raw, ['model'], ''), voiceKey: readField(raw, ['voiceKey', 'voice_key'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), speed: Number(readField(raw, ['speed'], 1)), gain: Number(readField(raw, ['gain'], 0)), pitch: Number(readField(raw, ['pitch'], 0)), enabled: Boolean(readField(raw, ['enabled'], true)), isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)), }); const mapASRModel = (raw: AnyRecord): ASRModel => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: readField(raw, ['vendor'], 'OpenAI Compatible'), language: readField(raw, ['language'], 'zh'), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), modelName: readField(raw, ['modelName', 'model_name'], ''), hotwords: readField(raw, ['hotwords'], []), enablePunctuation: Boolean(readField(raw, ['enablePunctuation', 'enable_punctuation'], true)), enableNormalization: Boolean(readField(raw, ['enableNormalization', 'enable_normalization'], true)), enabled: Boolean(readField(raw, ['enabled'], true)), }); const mapLLMModel = (raw: AnyRecord): LLMModel => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: readField(raw, ['vendor'], 'OpenAI Compatible'), type: readField(raw, ['type'], 'text'), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), modelName: readField(raw, ['modelName', 'model_name'], ''), temperature: Number(readField(raw, ['temperature'], 0.7)), contextLength: Number(readField(raw, ['contextLength', 'context_length'], 0)), enabled: Boolean(readField(raw, ['enabled'], true)), }); const mapTool = (raw: AnyRecord): Tool => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), description: readField(raw, ['description'], ''), category: readField(raw, ['category'], 'system') as 'system' | 'query', icon: readField(raw, ['icon'], 'Wrench'), isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)), enabled: Boolean(readField(raw, ['enabled'], true)), isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)), }); const mapWorkflowNode = (raw: AnyRecord): WorkflowNode => ({ name: readField(raw, ['name'], ''), type: readField(raw, ['type'], 'conversation') as 'conversation' | 'tool' | 'human' | 'end', isStart: readField(raw, ['isStart', 'is_start'], undefined), metadata: readField(raw, ['metadata'], { position: { x: 200, y: 200 } }), prompt: readField(raw, ['prompt'], ''), messagePlan: readField(raw, ['messagePlan', 'message_plan'], undefined), variableExtractionPlan: readField(raw, ['variableExtractionPlan', 'variable_extraction_plan'], undefined), tool: readField(raw, ['tool'], undefined), globalNodePlan: readField(raw, ['globalNodePlan', 'global_node_plan'], undefined), }); const mapWorkflowEdge = (raw: AnyRecord): WorkflowEdge => ({ from: readField(raw, ['from', 'from_'], ''), to: readField(raw, ['to'], ''), label: readField(raw, ['label'], undefined), }); const mapWorkflow = (raw: AnyRecord): Workflow => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), nodeCount: Number(readField(raw, ['nodeCount', 'node_count'], Array.isArray(raw.nodes) ? raw.nodes.length : 0)), createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')), updatedAt: normalizeDateLabel(readField(raw, ['updatedAt', 'updated_at'], '')), nodes: readField(raw, ['nodes'], []).map((node: AnyRecord) => mapWorkflowNode(node)), edges: readField(raw, ['edges'], []).map((edge: AnyRecord) => mapWorkflowEdge(edge)), globalPrompt: readField(raw, ['globalPrompt', 'global_prompt'], ''), }); const mapKnowledgeDocument = (raw: AnyRecord): KnowledgeDocument => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), size: readField(raw, ['size'], ''), uploadDate: normalizeDateLabel(readField(raw, ['uploadDate', 'upload_date', 'created_at'], '')), }); const mapKnowledgeBase = (raw: AnyRecord): KnowledgeBase => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), creator: 'Admin', createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')), documents: readField(raw, ['documents'], []).map((doc: AnyRecord) => mapKnowledgeDocument(doc)), }); const toHistoryRow = (raw: AnyRecord, assistantNameMap: Map): CallLog => { const assistantId = readField(raw, ['assistant_id', 'assistantId'], ''); const startTime = normalizeDateLabel(readField(raw, ['started_at', 'startTime'], '')); const type = readField(raw, ['type'], 'text'); return { id: String(readField(raw, ['id'], '')), source: readField(raw, ['source'], 'debug') as 'debug' | 'external', status: readField(raw, ['status'], 'connected') as 'connected' | 'missed', startTime, duration: formatDuration(readField(raw, ['duration_seconds', 'durationSeconds'], 0)), agentName: assistantNameMap.get(String(assistantId)) || String(assistantId || 'Unknown Assistant'), type: type === 'audio' || type === 'video' ? type : 'text', details: [], }; }; const toHistoryDetails = (raw: AnyRecord): InteractionDetail[] => { const transcripts = readField(raw, ['transcripts'], []); return transcripts.map((t: AnyRecord) => ({ role: readField(t, ['speaker'], 'human') === 'human' ? 'user' : 'assistant', content: readField(t, ['content'], ''), timestamp: `${Math.floor(Number(readField(t, ['startMs', 'start_ms'], 0)) / 1000)}s`, audioUrl: readField(t, ['audioUrl', 'audio_url'], undefined), })); }; export const fetchAssistants = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/assistants'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapAssistant(item)); }; export const createAssistant = async (data: Partial): Promise => { const payload = { name: data.name || 'New Assistant', opener: data.opener || '', prompt: data.prompt || '', knowledgeBaseId: data.knowledgeBaseId || '', language: data.language || 'zh', voice: data.voice || '', speed: data.speed ?? 1, hotwords: data.hotwords || [], tools: data.tools || [], interruptionSensitivity: data.interruptionSensitivity ?? 500, configMode: data.configMode || 'platform', apiUrl: data.apiUrl || '', apiKey: data.apiKey || '', }; const response = await apiRequest('/assistants', { method: 'POST', body: payload }); return mapAssistant(response); }; export const updateAssistant = async (id: string, data: Partial): Promise => { const payload = { name: data.name, opener: data.opener, prompt: data.prompt, knowledgeBaseId: data.knowledgeBaseId, language: data.language, voice: data.voice, speed: data.speed, hotwords: data.hotwords, tools: data.tools, interruptionSensitivity: data.interruptionSensitivity, configMode: data.configMode, apiUrl: data.apiUrl, apiKey: data.apiKey, }; const response = await apiRequest(`/assistants/${id}`, { method: 'PUT', body: payload }); return mapAssistant(response); }; export const deleteAssistant = async (id: string): Promise => { await apiRequest(`/assistants/${id}`, { method: 'DELETE' }); }; export const fetchVoices = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/voices'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapVoice(item)); }; export const createVoice = async (data: Partial): Promise => { const payload = { id: data.id || undefined, name: data.name || 'New Voice', vendor: data.vendor === '硅基流动' ? 'SiliconFlow' : (data.vendor || 'SiliconFlow'), gender: data.gender || 'Female', language: data.language || 'zh', description: data.description || '', model: data.model || undefined, voice_key: data.voiceKey || undefined, api_key: data.apiKey || undefined, base_url: data.baseUrl || undefined, speed: data.speed ?? 1, gain: data.gain ?? 0, pitch: data.pitch ?? 0, enabled: data.enabled ?? true, }; const response = await apiRequest('/voices', { method: 'POST', body: payload }); return mapVoice(response); }; export const updateVoice = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor === '硅基流动' ? 'SiliconFlow' : data.vendor, gender: data.gender, language: data.language, description: data.description, model: data.model, voice_key: data.voiceKey, api_key: data.apiKey, base_url: data.baseUrl, speed: data.speed, gain: data.gain, pitch: data.pitch, enabled: data.enabled, }; const response = await apiRequest(`/voices/${id}`, { method: 'PUT', body: payload }); return mapVoice(response); }; export const deleteVoice = async (id: string): Promise => { await apiRequest(`/voices/${id}`, { method: 'DELETE' }); }; export const previewVoice = async (id: string, text: string, speed?: number, apiKey?: string): Promise => { const response = await apiRequest<{ success: boolean; audio_url?: string; error?: string }>(`/voices/${id}/preview`, { method: 'POST', body: { text, speed, api_key: apiKey }, }); if (!response.success || !response.audio_url) { throw new Error(response.error || 'Preview failed'); } return response.audio_url; }; export const fetchASRModels = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/asr'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapASRModel(item)); }; export const createASRModel = async (data: Partial): Promise => { const payload = { id: data.id || undefined, name: data.name || 'New ASR Model', vendor: data.vendor || 'OpenAI Compatible', language: data.language || 'zh', base_url: data.baseUrl || 'https://api.siliconflow.cn/v1', api_key: data.apiKey || '', model_name: data.modelName || undefined, hotwords: data.hotwords || [], enable_punctuation: data.enablePunctuation ?? true, enable_normalization: data.enableNormalization ?? true, enabled: data.enabled ?? true, }; const response = await apiRequest('/asr', { method: 'POST', body: payload }); return mapASRModel(response); }; export const updateASRModel = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor, language: data.language, base_url: data.baseUrl, api_key: data.apiKey, model_name: data.modelName, hotwords: data.hotwords, enable_punctuation: data.enablePunctuation, enable_normalization: data.enableNormalization, enabled: data.enabled, }; const response = await apiRequest(`/asr/${id}`, { method: 'PUT', body: payload }); return mapASRModel(response); }; export const deleteASRModel = async (id: string): Promise => { await apiRequest(`/asr/${id}`, { method: 'DELETE' }); }; export type ASRPreviewResult = { success: boolean; transcript?: string; language?: string; confidence?: number; latency_ms?: number; message?: string; error?: string; }; export const previewASRModel = async ( id: string, file: File, options?: { language?: string; apiKey?: string } ): Promise => { const formData = new FormData(); formData.append('file', file); if (options?.language) { formData.append('language', options.language); } if (options?.apiKey) { formData.append('api_key', options.apiKey); } const base = (import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8100/api').replace(/\/+$/, ''); const url = `${base}/asr/${id}/preview`; const response = await fetch(url, { method: 'POST', body: formData, }); let data: ASRPreviewResult | null = null; try { data = await response.json(); } catch { data = null; } if (!response.ok) { const detail = (data as AnyRecord | null)?.error || (data as AnyRecord | null)?.detail || `Request failed: ${response.status}`; throw new Error(typeof detail === 'string' ? detail : `Request failed: ${response.status}`); } return data || { success: false, error: 'Invalid preview response' }; }; export const fetchLLMModels = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/llm'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapLLMModel(item)); }; export const createLLMModel = async (data: Partial): Promise => { const payload = { id: data.id || undefined, name: data.name || 'New LLM Model', vendor: data.vendor || 'OpenAI Compatible', type: data.type || 'text', base_url: data.baseUrl || '', api_key: data.apiKey || '', model_name: data.modelName || undefined, temperature: data.temperature, context_length: data.contextLength, enabled: data.enabled ?? true, }; const response = await apiRequest('/llm', { method: 'POST', body: payload }); return mapLLMModel(response); }; export const updateLLMModel = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor, type: data.type, base_url: data.baseUrl, api_key: data.apiKey, model_name: data.modelName, temperature: data.temperature, context_length: data.contextLength, enabled: data.enabled, }; const response = await apiRequest(`/llm/${id}`, { method: 'PUT', body: payload }); return mapLLMModel(response); }; export const deleteLLMModel = async (id: string): Promise => { await apiRequest(`/llm/${id}`, { method: 'DELETE' }); }; export type LLMPreviewResult = { success: boolean; reply?: string; usage?: Record; latency_ms?: number; error?: string; }; export const previewLLMModel = async ( id: string, payload: { message: string; system_prompt?: string; max_tokens?: number; temperature?: number; api_key?: string } ): Promise => { return apiRequest(`/llm/${id}/preview`, { method: 'POST', body: payload, }); }; export const fetchTools = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/tools/resources'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapTool(item)); }; export const createTool = async (data: Partial): Promise => { const payload = { id: data.id || undefined, name: data.name || 'New Tool', description: data.description || '', category: data.category || 'system', icon: data.icon || (data.category === 'query' ? 'Globe' : 'Terminal'), enabled: data.enabled ?? true, }; const response = await apiRequest('/tools/resources', { method: 'POST', body: payload }); return mapTool(response); }; export const updateTool = async (id: string, data: Partial): Promise => { const payload = { name: data.name, description: data.description, category: data.category, icon: data.icon, enabled: data.enabled, }; const response = await apiRequest(`/tools/resources/${id}`, { method: 'PUT', body: payload }); return mapTool(response); }; export const deleteTool = async (id: string): Promise => { await apiRequest(`/tools/resources/${id}`, { method: 'DELETE' }); }; export const fetchWorkflows = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/workflows'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapWorkflow(item)); }; export const fetchWorkflowById = async (id: string): Promise => { const list = await fetchWorkflows(); const workflow = list.find((item) => item.id === id); if (!workflow) { throw new Error('Workflow not found'); } return workflow; }; export const createWorkflow = async (data: Partial): Promise => { const payload = { name: data.name || '新工作流', nodeCount: data.nodeCount ?? data.nodes?.length ?? 0, createdAt: data.createdAt || '', updatedAt: data.updatedAt || '', globalPrompt: data.globalPrompt || '', nodes: data.nodes || [], edges: data.edges || [], }; const response = await apiRequest('/workflows', { method: 'POST', body: payload }); return mapWorkflow(response); }; export const updateWorkflow = async (id: string, data: Partial): Promise => { const payload = { name: data.name, nodeCount: data.nodeCount ?? data.nodes?.length, nodes: data.nodes, edges: data.edges, globalPrompt: data.globalPrompt, }; const response = await apiRequest(`/workflows/${id}`, { method: 'PUT', body: payload }); return mapWorkflow(response); }; export const deleteWorkflow = async (id: string): Promise => { await apiRequest(`/workflows/${id}`, { method: 'DELETE' }); }; export const fetchKnowledgeBases = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/knowledge/bases'); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapKnowledgeBase(item)); }; export const createKnowledgeBase = async (name: string): Promise => { const payload = { name, description: '', embeddingModel: 'text-embedding-3-small', chunkSize: 500, chunkOverlap: 50 }; const response = await apiRequest('/knowledge/bases', { method: 'POST', body: payload }); return mapKnowledgeBase(response); }; export const deleteKnowledgeBase = async (kbId: string): Promise => { await apiRequest(`/knowledge/bases/${kbId}`, { method: 'DELETE' }); }; export const uploadKnowledgeDocument = async (kbId: string, file: File): Promise => { const payload = { name: file.name, size: `${(file.size / 1024).toFixed(1)} KB`, fileType: file.type || 'txt', }; await apiRequest(`/knowledge/bases/${kbId}/documents`, { method: 'POST', body: payload }); }; export const deleteKnowledgeDocument = async (kbId: string, docId: string): Promise => { await apiRequest(`/knowledge/bases/${kbId}/documents/${docId}`, { method: 'DELETE' }); }; export const fetchHistory = async (): Promise => { const [historyResp, assistantsResp] = await Promise.all([ apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/history'), apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/assistants'), ]); const assistantList = Array.isArray(assistantsResp) ? assistantsResp : (assistantsResp.list || []); const assistantNameMap = new Map(assistantList.map((a: AnyRecord) => [String(readField(a, ['id'], '')), readField(a, ['name'], '')])); const historyList = Array.isArray(historyResp) ? historyResp : (historyResp.list || []); return historyList.map((item) => toHistoryRow(item, assistantNameMap)); }; export const fetchHistoryDetail = async (id: string, base: CallLog): Promise => { const response = await apiRequest(`/history/${id}`); const details = toHistoryDetails(response); const duration = formatDuration(readField(response, ['duration_seconds', 'durationSeconds'], 0)); const startTime = normalizeDateLabel(readField(response, ['started_at'], base.startTime)); return { ...base, startTime, duration, details, }; };