Files
YG_FT/frontend/src/mock/adapter.ts
caoxiaozhu e09c6e81df feat: 用户与权限管理
新增 PermissionCode 权限类型与用户/权限 API,路由注册用户设置/创建/权限页与无权访问页并接入权限守卫,AppSidebar 按权限过滤菜单并新增用户设置入口,auth store 与 mock 适配器同步支持用户管理与新登录认证,回归脚本与 npm 脚本注册。
2026-07-14 16:10:50 +08:00

483 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<void> {
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
}