/** * Axios Mock Adapter * 拦截所有 API 请求并返回 mock 数据 * 通过 URL + method 路由到对应的 mock 响应 */ import type { AxiosAdapter, AxiosInstance, AxiosRequestConfig } from 'axios' import { mockHealth, mockSystemInfo, mockModels, mockTrainedModels, mockLocalModels, mockDatasets, mockDatasetPreviews, mockFineTuneList, mockCompareList, mockEvalList, mockEvalDetails, mockDimensions, mockLogFiles, mockTrainingLogFiles, mockLogContent, mockTrainingLogContents, } from './data' import { activateDatasetVersion, appendDatasetVersion, createInitialVersionState, deleteDatasetVersion, DatasetVersionMutationError, getActiveDatasetVersion, normalizeDatasetVersionState, } from './datasetVersions' import type { StoredDatasetVersion, StoredDatasetVersionState } from './datasetVersions' import { authenticateMockUser, createMockUser, deleteMockUser, listMockUsers, updateMockUserAccess, UserMutationError, } from './users' /** 模拟网络延迟 */ function delay(ms = 200): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } /** 构造 Axios 风格的成功响应 */ function ok(data: any, config: AxiosRequestConfig = {}) { return { data: { code: 0, message: 'ok', data }, status: 200, statusText: 'OK', headers: {}, config, } } /** 构造失败响应 */ function fail(message: string, code = 500, config: AxiosRequestConfig = {}) { return { data: { code, message }, status: code, statusText: message, headers: {}, config, } } /** 简易 URL 解析(去掉 baseURL 前缀) */ function stripBaseURL(url: string): string { // url 可能带查询参数 const [path] = url.split('?') return path } function datasetFallbackContent(fileId: string) { const fallbackKey = fileId.endsWith('-readme') ? 'mock-readme' : 'mock-jsonl' return mockDatasetPreviews[fileId] ?? mockDatasetPreviews[fallbackKey] ?? '' } function versionStorageKey(fileId: string) { return `mock:dataset-versions:${fileId}` } function persistVersionState(fileId: string, state: StoredDatasetVersionState) { localStorage.setItem(versionStorageKey(fileId), JSON.stringify(state)) } function getVersionState(fileId: string): StoredDatasetVersionState { const persisted = localStorage.getItem(versionStorageKey(fileId)) if (persisted) { try { const state = JSON.parse(persisted) as StoredDatasetVersionState if (state.versions?.length && state.active_version_id) { const normalizedState = normalizeDatasetVersionState(state) if (state.next_version_number !== normalizedState.next_version_number) { persistVersionState(fileId, normalizedState) } return normalizedState } } catch { // 版本数据损坏时回退到初始版本 } } const legacyContent = localStorage.getItem(`mock:dataset-file:${fileId}`) const state = createInitialVersionState(legacyContent ?? datasetFallbackContent(fileId)) persistVersionState(fileId, state) return state } function versionMetadata(version: StoredDatasetVersion) { const { content: _content, ...metadata } = version return metadata } function versionListResponse(state: StoredDatasetVersionState) { return { versions: state.versions.map(versionMetadata).sort((a, b) => b.version - a.version), active_version_id: state.active_version_id, next_version_number: state.next_version_number, } } /** 通过路径 + method 匹配 mock 响应 */ async function handleMock(config: AxiosRequestConfig) { await delay(150) // 模拟网络延迟 const url = stripBaseURL(config.url || '') const method = (config.method || 'get').toLowerCase() const params = config.params || {} const body = typeof config.data === 'string' ? safeJSON(config.data) : config.data || {} // ==================== 认证 ==================== if (url === '/login' && method === 'post') { try { return ok(authenticateMockUser(String(body.username || ''), String(body.password || ''))) } catch (error) { if (error instanceof UserMutationError) return fail(error.message, error.status) return fail('登录失败', 500) } } if (url === '/web-log' && method === 'post') { return ok({ received: true }) } // ==================== 系统监控 ==================== if (url === '/health' && method === 'get') return ok(mockHealth) if (url === '/system-info' && method === 'get') return ok(mockSystemInfo) // ==================== 用户设置 ==================== if (url === '/users' && method === 'get') return ok(listMockUsers()) if (url === '/users' && method === 'post') { try { return ok(createMockUser(body)) } catch (error) { if (error instanceof UserMutationError) return fail(error.message, error.status) return fail('创建用户失败', 500) } } let userMatch = url.match(/^\/users\/([^/]+)$/) if (userMatch && method === 'put') { try { return ok(updateMockUserAccess(decodeURIComponent(userMatch[1]), body)) } catch (error) { if (error instanceof UserMutationError) return fail(error.message, error.status) return fail('更新用户失败', 500) } } if (userMatch && method === 'delete') { try { return ok(deleteMockUser( decodeURIComponent(userMatch[1]), String(params.current_username || ''), )) } catch (error) { if (error instanceof UserMutationError) return fail(error.message, error.status) return fail('删除用户失败', 500) } } // ==================== 模型管理 ==================== if (url === '/model-manage' && method === 'get') return ok(mockModels) if (url === '/model-manage/local-models' && method === 'get') return ok(mockLocalModels) if (url === '/model-manage/trained-models' && method === 'get') return ok(mockTrainedModels) if (url === '/model-manage/merge' && method === 'post') { return ok({ merged: true, path: '/data/saves/' + body.model_name + '-merged' }) } // 模型详情 / 编辑 / 删除 / 用途更新 let m = url.match(/^\/model-manage\/([^/]+)$/) if (m && method === 'get') { const id = m[1] const found = mockModels.find((x) => String(x.id) === id || x.name === id) return found ? ok(found) : fail('模型不存在', 404) } m = url.match(/^\/model-manage\/([^/]+)$/) if (m && (method === 'put' || method === 'delete')) { return ok({ id: m[1], ...body }) } m = url.match(/^\/model-manage\/name\/([^/]+)$/) if (m && method === 'get') { const name = decodeURIComponent(m[1]) const found = mockModels.find((x) => x.name === name) return found ? ok(found) : fail('模型不存在', 404) } m = url.match(/^\/model-manage\/trained-models\/([^/]+)$/) if (m && method === 'delete') { return ok({ deleted: m[1] }) } m = url.match(/^\/model-manage\/([^/]+)\/purpose$/) if (m && method === 'put') { return ok({ id: m[1], purpose: body.purpose }) } // ==================== 数据集 ==================== if (url === '/dataset-manage' && method === 'get') return ok(mockDatasets) if (url === '/dataset-manage' && method === 'post') { const newId = Math.max(...mockDatasets.map((d) => Number(d.id))) + 1 return ok({ id: newId }) } m = url.match(/^\/dataset-manage\/([^/]+)$/) if (m && method === 'get') { const datasetId = m[1] const found = mockDatasets.find((x) => String(x.id) === datasetId) return found ? ok(found) : fail('数据集不存在', 404) } if (m && (method === 'put' || method === 'delete')) { return ok({ id: m[1] }) } m = url.match(/^\/dataset-manage\/upload\/([^/]+)$/) if (m && method === 'post') return ok({ uploaded: true }) m = url.match(/^\/dataset-manage\/preview\/([^/]+)$/) if (m && method === 'get') { const fileId = decodeURIComponent(m[1]) return ok({ content: getActiveDatasetVersion(getVersionState(fileId)).content }) } if (m && method === 'put') return fail('历史版本不可覆盖,请创建新版本', 405) m = url.match(/^\/dataset-manage\/versions\/([^/]+)$/) if (m && method === 'get') { const fileId = decodeURIComponent(m[1]) const state = getVersionState(fileId) return ok(versionListResponse(state)) } if (m && method === 'post') { const fileId = decodeURIComponent(m[1]) if (typeof body.content !== 'string') return fail('文件内容格式不正确', 400) const currentState = getVersionState(fileId) if (body.expected_current_version_id !== currentState.active_version_id) { return fail('当前版本已被其他用户更新,请刷新后重试', 409) } if (body.base_version_id !== currentState.active_version_id) { return fail('只能基于当前版本创建新版本', 409) } if (body.content === getActiveDatasetVersion(currentState).content) { return fail('数据内容没有变化,无需创建新版本', 400) } const nextState = appendDatasetVersion( currentState, body.content, new Date().toISOString(), typeof body.description === 'string' ? body.description : '在线编辑', ) try { persistVersionState(fileId, nextState) } catch { return fail('文件内容过大,浏览器 Mock 存储空间不足', 413) } const version = getActiveDatasetVersion(nextState) return ok({ version: versionMetadata(version), content: version.content }) } m = url.match(/^\/dataset-manage\/versions\/([^/]+)\/active$/) if (m && method === 'put') { const fileId = decodeURIComponent(m[1]) const currentState = getVersionState(fileId) if (body.expected_current_version_id !== currentState.active_version_id) { return fail('当前版本已被其他用户更新,请刷新后重试', 409) } let nextState: StoredDatasetVersionState try { nextState = activateDatasetVersion(currentState, String(body.version_id || '')) } catch (error) { return fail(error instanceof Error ? error.message : '版本不存在', 404) } try { persistVersionState(fileId, nextState) } catch { return fail('浏览器 Mock 存储空间不足', 413) } const version = getActiveDatasetVersion(nextState) return ok({ version: versionMetadata(version), content: version.content }) } m = url.match(/^\/dataset-manage\/versions\/([^/]+)\/([^/]+)$/) if (m && method === 'get') { const fileId = decodeURIComponent(m[1]) const versionId = decodeURIComponent(m[2]) const version = getVersionState(fileId).versions.find((item) => item.id === versionId) return version ? ok({ version: versionMetadata(version), content: version.content }) : fail('数据集版本不存在', 404) } if (m && method === 'delete') { const fileId = decodeURIComponent(m[1]) const versionId = decodeURIComponent(m[2]) const currentState = getVersionState(fileId) if (params.expected_current_version_id !== currentState.active_version_id) { return fail('当前版本已被其他用户更新,请刷新后重试', 409) } let nextState: StoredDatasetVersionState try { nextState = deleteDatasetVersion(currentState, versionId) } catch (error) { if (error instanceof DatasetVersionMutationError) { return fail(error.message, error.status) } return fail('删除版本失败', 500) } try { persistVersionState(fileId, nextState) } catch { return fail('浏览器 Mock 存储空间不足', 413) } return ok(versionListResponse(nextState)) } // ==================== 训练任务 ==================== if (url === '/fine-tune' && method === 'get') return ok(mockFineTuneList) if (url === '/fine-tune' && method === 'post') { return ok({ id: Math.floor(Math.random() * 10000) + 100 }) } if (url === '/fine-tune/start' && method === 'post') { return ok({ started: true }) } if (url === '/fine-tune/check-name' && method === 'get') { return ok({ exists: false }) } m = url.match(/^\/fine-tune\/progress\/([^/]+)$/) if (m && method === 'get') { const taskId = m[1] const task = mockFineTuneList.find((t) => String(t.id) === taskId) if (!task) return fail('任务不存在', 404) if (task.status === 'running') { return ok({ status: task.status, progress: task.progress ?? 0, step: `${Math.floor((task.progress ?? 0) * 10)}/1000`, speed: '1.23s/it', eta: '00:23:45', }) } return ok({ status: task.status, progress: task.progress ?? 0 }) } m = url.match(/^\/fine-tune\/([^/]+)$/) if (m && method === 'get') { const taskId = m[1] const found = mockFineTuneList.find((x) => String(x.id) === taskId) return found ? ok(found) : fail('任务不存在', 404) } m = url.match(/^\/fine-tune\/stop\/([^/]+)$/) if (m && method === 'post') return ok({ stopped: true }) m = url.match(/^\/fine-tune\/([^/]+)$/) if (m && (method === 'put' || method === 'delete')) { return ok({ id: m[1] }) } if (url === '/fine-tune/tensorboard/start' && method === 'post') { return ok({ url: 'http://10.10.10.77:6006' }) } // ==================== 模型推理/对比 ==================== if (url === '/model-compare' && method === 'get') return ok(mockCompareList) if (url === '/model-compare' && method === 'post') { return ok({ id: Math.floor(Math.random() * 10000) + 100 }) } m = url.match(/^\/model-compare\/([^/]+)$/) if (m && method === 'get') { const compareId = m[1] const found = mockCompareList.find((x) => String(x.id) === compareId) return found ? ok(found) : fail('任务不存在', 404) } if (m && method === 'delete') return ok({ deleted: m[1] }) m = url.match(/^\/model-compare\/([^/]+)\/load-status$/) if (m && method === 'get') { return ok({ all_ready: true, loaded_models: [] }) } if (m && method === 'post') return ok({ updated: true }) m = url.match(/^\/model-compare\/([^/]+)\/load$/) if (m && method === 'post') return ok({ loading: true }) m = url.match(/^\/model-compare\/([^/]+)\/unload$/) if (m && method === 'post') return ok({ stopped: true }) m = url.match(/^\/model-compare\/([^/]+)\/start-model$/) if (m && method === 'post') { return ok({ pid: 12345 + Math.floor(Math.random() * 100), port: 18000 + Math.floor(Math.random() * 1000) }) } if (url === '/model-compare/all/stop-all' && method === 'post') return ok({ stopped: true }) if (url === '/model-compare/stop-by-pid' && method === 'post') return ok({ stopped: true }) if (url === '/model-compare/chat-with-port' && method === 'post') { // 模拟推理回答(用于对比结果页) const userQ = body?.messages?.find((m: any) => m.role === 'user')?.content || '你的问题' const answer = `这是一个针对「${userQ.slice(0, 30)}」的模拟回答。\n\n在真实环境中,对比结果页会通过端口代理调用对应模型服务,返回该模型的真实推理结果。\n\n模型参数量、温度、Top-p 等参数都会影响输出内容。\n\n- 模型:${body?.model_name || '未知'}\n- 端口:${body?.port || '-'}\n- 参数:temperature=${body?.temperature ?? 0.7}, max_tokens=${body?.max_tokens ?? 2048}` return ok({ response: answer, content: answer }) } if (url === '/model-compare/stream-chat' && method === 'post') { // 模拟流式对话(前端 useStreamChat 会按块读取) const userQ = body?.user_question || '' const sysPrompt = body?.system_prompt || '' const answer = `${sysPrompt ? '【系统提示】' + sysPrompt.slice(0, 50) + '\n\n' : ''}关于「${userQ}」的回答:\n\n这是一段**模拟流式输出**。在真实部署中,后端会通过 SSE/WebSocket 逐字推送 token。\n\n## 模型参数\n- 温度(temperature):${body?.temperature ?? 0.7}\n- 最大长度(max_tokens):${body?.max_tokens ?? 2048}\n\n## 思考过程\n让我先分析这个问题...\n- 识别用户意图\n- 检索相关知识\n- 生成回答\n\n回答已生成。以上为前端 Mock 演示内容。` return ok({ response: answer }) } if (url === '/model-compare/test-stream' && method === 'post') { return ok({ response: '测试流式输出' }) } if (url === '/model-chat/batch' && method === 'post') return ok({ responses: [] }) if (url === '/model-chat/local/chat' && method === 'post') return ok({ response: '本地模型回复' }) if (url === '/model-chat/local/preload' && method === 'post') return ok({ loaded: true }) if (url === '/model-chat/trained/preload' && method === 'post') return ok({ loaded: true }) // ==================== 模型评测 ==================== if (url === '/model-eval' && method === 'get') return ok(mockEvalList) if (url === '/model-eval' && method === 'delete') return ok({ deleted: true }) if (url === '/model-eval/start' && method === 'post') return ok({ task_id: Math.floor(Math.random() * 1000) + 1 }) m = url.match(/^\/model-eval\/([^/]+)$/) const evalTaskId = m?.[1] if (evalTaskId && method === 'get') { const found = mockEvalDetails.find((item) => String(item.id) === evalTaskId) return found ? ok(found) : fail('评测任务不存在', 404) } if (url === '/dimension' && method === 'get') return ok(mockDimensions) if (url === '/dimension' && method === 'post') { return ok({ id: Math.floor(Math.random() * 1000) + 100 }) } m = url.match(/^\/dimension\/([^/]+)$/) if (m && method === 'get') { const found = mockDimensions.find((x) => String(x.id) === m[1]) return found ? ok(found) : fail('维度不存在', 404) } if (m && (method === 'put' || method === 'delete')) { return ok({ id: m[1] }) } // ==================== 日志 ==================== if (url === '/log-files' && method === 'get') return ok(mockLogFiles) if (url === '/log-content' && method === 'get') return ok(mockLogContent) if (url === '/training-log-files' && method === 'get') return ok(mockTrainingLogFiles) if (url === '/training-log-content' && method === 'get') { const file = String(params.file || '') return ok(mockTrainingLogContents[file] || { file, size: '0 KB', content: `[Mock] 未找到训练日志内容:${file}`, }) } // 未匹配的请求 → 兜底返回空成功(避免阻断 UI) console.warn('[Mock] 未匹配路由:', method.toUpperCase(), url, params) return ok({ mocked: true, url, method, params, body }) } function safeJSON(str: string) { try { return JSON.parse(str) } catch { return {} } } /** 给 axios instance 安装 mock adapter */ export function installMockAdapter(instance: AxiosInstance) { const adapter = async (config: AxiosRequestConfig) => { try { const response = await handleMock(config) return response } catch (error: unknown) { return fail(error instanceof Error ? error.message : 'Mock 错误', 500, config) } } instance.defaults.adapter = adapter as AxiosAdapter }