feat: 数据集版本管理
新增数据集版本创建、切换、激活与历史追溯能力,Mock 通过 localStorage 持久化版本状态并接入版本接口路由,预览页支持版本对比与内容回滚,列表补充版本入口,回归脚本扩充断言。
This commit is contained in:
@@ -1,22 +1,67 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
const root = process.cwd()
|
||||
const previewPath = path.join(root, 'src/views/dataset/DatasetPreviewView.vue')
|
||||
const apiPath = path.join(root, 'src/api/modules/dataset.ts')
|
||||
const adapterPath = path.join(root, 'src/mock/adapter.ts')
|
||||
const mockPath = path.join(root, 'src/mock/data.ts')
|
||||
const recordsPath = path.join(root, 'src/views/dataset/datasetRecords.ts')
|
||||
const versionsPath = path.join(root, 'src/mock/datasetVersions.ts')
|
||||
const source = fs.readFileSync(previewPath, 'utf8')
|
||||
const apiSource = fs.readFileSync(apiPath, 'utf8')
|
||||
const adapterSource = fs.readFileSync(adapterPath, 'utf8')
|
||||
const mockSource = fs.readFileSync(mockPath, 'utf8')
|
||||
const recordsSource = fs.readFileSync(recordsPath, 'utf8')
|
||||
const versionsSource = fs.readFileSync(versionsPath, 'utf8')
|
||||
const recordsModuleCode = ts.transpileModule(recordsSource, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
const { parseDatasetRecords, updateDatasetRecord } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(recordsModuleCode).toString('base64')}`
|
||||
)
|
||||
const versionsModuleCode = ts.transpileModule(versionsSource, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
const {
|
||||
activateDatasetVersion,
|
||||
appendDatasetVersion,
|
||||
createInitialVersionState,
|
||||
getActiveDatasetVersion,
|
||||
} = await import(`data:text/javascript;base64,${Buffer.from(versionsModuleCode).toString('base64')}`)
|
||||
|
||||
function expect(condition, message) {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
expect(source.includes('class="preview-workspace"'), '预览页应使用文件与内容双栏工作区')
|
||||
expect(source.includes('class="preview-workspace"'), '详情页应提供数据文件工作区')
|
||||
expect(!source.includes('class="file-pane"'), '单文件数据集详情不应展示文件选择侧栏')
|
||||
expect(source.includes('class="code-line"'), '内容查看器应提供逐行展示')
|
||||
expect(source.includes('class="line-number"'), '内容查看器应显示行号')
|
||||
expect(source.includes('previewLoading'), '切换文件时应提供独立加载状态')
|
||||
expect(source.includes('previewError'), '内容加载失败时应提供错误状态')
|
||||
expect(source.includes('aria-current'), '文件选中态应向辅助技术暴露')
|
||||
expect(source.includes('class="records-viewer"'), '详情页应将结构化文件展示为样本列表')
|
||||
expect(source.includes('class="record-table"'), '样本数据应使用企业级表格展示')
|
||||
expect(source.includes('fixed="right"'), '逐条编辑操作列应固定在表格右侧')
|
||||
expect(source.includes('v-for="fieldKey in tableFieldKeys"'), '企业表格应根据数据结构动态生成字段列')
|
||||
expect(source.includes('openRecordEditor(asDatasetRecord(row))'), '每条样本应提供独立编辑入口')
|
||||
expect(source.includes('暂存修改'), '单条编辑应先暂存到当前页面')
|
||||
expect(source.includes('hasPendingVersionChanges && isViewingActiveVersion'), '存在暂存修改时才应显示保存版本按钮')
|
||||
expect(source.includes('保存版本'), '文件工具栏应提供保存版本按钮')
|
||||
expect(source.includes('baseVersionContent'), '页面应区分已保存版本与待保存工作副本')
|
||||
expect(source.includes('class="version-control-bar"'), '详情页应提供独立版本控制区')
|
||||
expect(source.includes('设为当前版本'), '历史版本应支持显式切换为当前版本')
|
||||
expect(source.includes('历史版本(只读)'), '历史版本应明确展示只读状态')
|
||||
expect(source.includes('loadedVersionId.value'), '下载和展示应绑定正在查看的具体版本')
|
||||
expect(source.includes('versionRequestId'), '快速切换历史版本时应防止旧响应覆盖新内容')
|
||||
expect(source.includes('v-model:current-page="currentPage"'), '样本列表应支持分页浏览')
|
||||
expect(!source.includes('class="content-editor"'), '详情页不应继续提供整文件编辑器')
|
||||
expect(source.includes('saveRecord'), '逐条编辑器应提供单条保存动作')
|
||||
expect(source.includes('hasUnsavedChanges'), '在线编辑器应跟踪未保存修改')
|
||||
expect(source.includes('onBeforeRouteLeave'), '离开页面时应保护未保存修改')
|
||||
expect(source.includes("event.key.toLowerCase() === 's'"), '在线编辑器应支持快捷键保存')
|
||||
expect(source.includes('loadVersions(selectedFile)'), '版本内容加载失败后应支持重新加载')
|
||||
expect(!source.includes('handleDownloadAll'), '页面不应保留整包下载逻辑')
|
||||
expect(!source.includes('handleDelete'), '页面不应保留删除逻辑')
|
||||
expect(!source.includes('router.back()'), '页面不应保留页头返回逻辑')
|
||||
@@ -25,5 +70,53 @@ expect(mockSource.includes('mockDatasetPreviews'), 'Mock 数据应包含文件
|
||||
expect(mockSource.includes("'mock-jsonl'"), 'Mock 数据应为通用数据集提供模拟样本')
|
||||
expect(mockSource.includes("'mock-readme'"), 'Mock 数据应为通用数据集提供模拟说明')
|
||||
expect(mockSource.includes('.map((dataset)'), '所有 Mock 数据集都应自动补齐预览文件')
|
||||
expect(mockSource.includes(': [{ id: `dataset-${dataset.id}-samples`'), '每个 Mock 数据集应只对应一个数据文件')
|
||||
expect(apiSource.includes('createDatasetFileVersion'), '数据集 API 应提供创建新版本方法')
|
||||
expect(apiSource.includes('activateDatasetFileVersion'), '数据集 API 应提供切换当前版本方法')
|
||||
expect(apiSource.includes('getDatasetFileVersionContent'), '数据集 API 应支持读取历史版本内容')
|
||||
expect(apiSource.includes('expected_current_version_id'), '创建和激活版本应携带乐观锁版本指针')
|
||||
expect(!apiSource.includes('updateDatasetFileContent'), '数据编辑不应继续覆盖历史版本内容')
|
||||
expect(adapterSource.includes('mock:dataset-versions:'), 'Mock 版本历史应支持刷新持久化')
|
||||
expect(adapterSource.includes('当前版本已被其他用户更新'), 'Mock 应拒绝基于过期版本的并发覆盖')
|
||||
expect(adapterSource.includes('历史版本不可覆盖'), '旧的覆盖保存接口应明确拒绝修改历史版本')
|
||||
expect(adapterSource.includes('Mock 存储空间不足'), 'Mock 存储失败时应返回明确错误且保留编辑草稿')
|
||||
|
||||
const jsonl = [
|
||||
'{"instruction":"第一条","output":"A"}',
|
||||
'',
|
||||
'{"instruction":"第二条","output":["B","C"]}',
|
||||
].join('\n')
|
||||
const parsed = parseDatasetRecords(jsonl, 'samples.jsonl')
|
||||
expect(parsed.supported && parsed.records.length === 2, 'JSONL 应跳过空行并解析为逐条样本')
|
||||
expect(parsed.records[1].sourceIndex === 2, '样本应保留原文件物理行位置')
|
||||
const updated = updateDatasetRecord(jsonl, 'samples.jsonl', 2, {
|
||||
instruction: '第二条已修改',
|
||||
output: ['B', 'C'],
|
||||
})
|
||||
const beforeLines = jsonl.split('\n')
|
||||
const afterLines = updated.split('\n')
|
||||
expect(afterLines[0] === beforeLines[0] && afterLines[1] === '', '单条保存不得改写其他样本或空行')
|
||||
expect(JSON.parse(afterLines[2]).instruction === '第二条已修改', '单条保存应准确更新目标样本')
|
||||
expect(Array.isArray(JSON.parse(afterLines[2]).output), '单条保存不应丢失数组等非字符串字段类型')
|
||||
const invalid = parseDatasetRecords('{invalid json}', 'samples.jsonl')
|
||||
expect(invalid.records[0].kind === 'invalid' && invalid.records[0].error, '非法 JSON 行应被单独标记并给出错误')
|
||||
const repairedJson = updateDatasetRecord('{invalid json}', 'samples.json', 0, { repaired: true })
|
||||
expect(JSON.parse(repairedJson).repaired === true, '非法 JSON 文件应支持按单条方式修复')
|
||||
|
||||
const v1State = createInitialVersionState('A', '2026-07-13T00:00:00Z')
|
||||
const v2State = appendDatasetVersion(v1State, 'B', '2026-07-13T01:00:00Z', '修改第一条')
|
||||
expect(v2State !== v1State, '创建版本应返回新的不可变状态')
|
||||
expect(v1State.versions.length === 1 && v1State.versions[0].content === 'A', '创建新版本不得修改历史版本')
|
||||
expect(v2State.versions[1].version === 2 && v2State.active_version_id === 'v2', '新版本编号应递增并自动激活')
|
||||
const switchedToV1 = activateDatasetVersion(v2State, 'v1')
|
||||
expect(getActiveDatasetVersion(switchedToV1).content === 'A', '切换当前版本后应读取对应历史内容')
|
||||
expect(v2State.active_version_id === 'v2' && v2State.versions[1].content === 'B', '切换版本不得污染原状态或历史快照')
|
||||
let invalidVersionRejected = false
|
||||
try {
|
||||
activateDatasetVersion(v2State, 'missing')
|
||||
} catch {
|
||||
invalidVersionRejected = true
|
||||
}
|
||||
expect(invalidVersionRejected, '不存在的版本不得切换为当前版本')
|
||||
|
||||
console.log('dataset preview regression checks passed')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get, post, put, del } from '../request'
|
||||
import type { DatasetItem } from '@/types'
|
||||
import type { DatasetItem, DatasetVersion, DatasetVersionList } from '@/types'
|
||||
|
||||
/** 数据集列表 */
|
||||
export const getDatasetList = () => get<DatasetItem[]>('/dataset-manage')
|
||||
@@ -29,11 +29,58 @@ export const uploadDatasetFiles = (datasetId: string | number, files: File[]) =>
|
||||
|
||||
/** 预览数据集文件内容 */
|
||||
export const previewDatasetFile = (fileId: string | number) =>
|
||||
get<{ content: string }>(`/dataset-manage/preview/${fileId}`)
|
||||
get<{ content: string }>(`/dataset-manage/preview/${encodeURIComponent(fileId)}`)
|
||||
|
||||
/** 数据集文件版本列表 */
|
||||
export const getDatasetFileVersions = (fileId: string | number) =>
|
||||
get<DatasetVersionList>(`/dataset-manage/versions/${encodeURIComponent(fileId)}`)
|
||||
|
||||
/** 读取指定历史版本内容(不改变当前版本) */
|
||||
export const getDatasetFileVersionContent = (fileId: string | number, versionId: string) =>
|
||||
get<{ version: DatasetVersion; content: string }>(
|
||||
`/dataset-manage/versions/${encodeURIComponent(fileId)}/${encodeURIComponent(versionId)}`,
|
||||
)
|
||||
|
||||
/** 保存为新版本,并自动设为当前版本 */
|
||||
export const createDatasetFileVersion = (
|
||||
fileId: string | number,
|
||||
content: string,
|
||||
options: {
|
||||
description?: string
|
||||
baseVersionId: string
|
||||
expectedCurrentVersionId: string
|
||||
},
|
||||
) =>
|
||||
post<{ version: DatasetVersion; content: string }>(
|
||||
`/dataset-manage/versions/${encodeURIComponent(fileId)}`,
|
||||
{
|
||||
content,
|
||||
description: options.description || '在线编辑',
|
||||
base_version_id: options.baseVersionId,
|
||||
expected_current_version_id: options.expectedCurrentVersionId,
|
||||
},
|
||||
)
|
||||
|
||||
/** 切换当前使用的数据集版本 */
|
||||
export const activateDatasetFileVersion = (
|
||||
fileId: string | number,
|
||||
versionId: string,
|
||||
expectedCurrentVersionId: string,
|
||||
) =>
|
||||
put<{ version: DatasetVersion; content: string }>(
|
||||
`/dataset-manage/versions/${encodeURIComponent(fileId)}/active`,
|
||||
{ version_id: versionId, expected_current_version_id: expectedCurrentVersionId },
|
||||
)
|
||||
|
||||
/** 下载文件 URL */
|
||||
export const downloadFileUrl = (datasetId: string | number, fileId: string | number) =>
|
||||
`/api/dataset-manage/download/${datasetId}/${fileId}`
|
||||
export const downloadFileUrl = (
|
||||
datasetId: string | number,
|
||||
fileId: string | number,
|
||||
versionId?: string,
|
||||
) => {
|
||||
const baseUrl = `/api/dataset-manage/download/${datasetId}/${fileId}`
|
||||
return versionId ? `${baseUrl}?version_id=${encodeURIComponent(versionId)}` : baseUrl
|
||||
}
|
||||
|
||||
/** 打包下载数据集 URL */
|
||||
export const downloadDatasetUrl = (datasetId: string | number) =>
|
||||
|
||||
@@ -22,6 +22,13 @@ import {
|
||||
mockTrainingLogFiles,
|
||||
mockLogContent,
|
||||
} from './data'
|
||||
import {
|
||||
activateDatasetVersion,
|
||||
appendDatasetVersion,
|
||||
createInitialVersionState,
|
||||
getActiveDatasetVersion,
|
||||
} from './datasetVersions'
|
||||
import type { StoredDatasetVersion, StoredDatasetVersionState } from './datasetVersions'
|
||||
|
||||
/** 模拟网络延迟 */
|
||||
function delay(ms = 200): Promise<void> {
|
||||
@@ -57,6 +64,41 @@ function stripBaseURL(url: string): string {
|
||||
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) return state
|
||||
} 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
|
||||
}
|
||||
|
||||
/** 通过路径 + method 匹配 mock 响应 */
|
||||
async function handleMock(config: AxiosRequestConfig) {
|
||||
await delay(150) // 模拟网络延迟
|
||||
@@ -133,9 +175,77 @@ async function handleMock(config: AxiosRequestConfig) {
|
||||
m = url.match(/^\/dataset-manage\/preview\/([^/]+)$/)
|
||||
if (m && method === 'get') {
|
||||
const fileId = decodeURIComponent(m[1])
|
||||
const fallbackKey = fileId.endsWith('-readme') ? 'mock-readme' : 'mock-jsonl'
|
||||
const content = mockDatasetPreviews[fileId] ?? mockDatasetPreviews[fallbackKey]
|
||||
return ok({ content })
|
||||
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({
|
||||
versions: state.versions.map(versionMetadata).sort((a, b) => b.version - a.version),
|
||||
active_version_id: state.active_version_id,
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// ==================== 训练任务 ====================
|
||||
|
||||
@@ -30,10 +30,14 @@ export const mockHealth: HealthMetrics = {
|
||||
}
|
||||
|
||||
export const mockSystemInfo: SystemInfo = {
|
||||
timestamp: '2026-07-12T10:30:00+08:00',
|
||||
cpu: {
|
||||
percent: 32,
|
||||
cores: 8,
|
||||
percents: [25, 38, 42, 30, 28, 36, 40, 18],
|
||||
model: 'Intel Xeon Gold 6330',
|
||||
frequency_mhz: 2500,
|
||||
load_1m: 2.56,
|
||||
},
|
||||
memory: {
|
||||
used_gb: 9.2,
|
||||
@@ -46,20 +50,166 @@ export const mockSystemInfo: SystemInfo = {
|
||||
used_gb: 230,
|
||||
total_gb: 512,
|
||||
percent: 45,
|
||||
read_mb_s: 86.4,
|
||||
write_mb_s: 34.8,
|
||||
},
|
||||
gpu: [
|
||||
{ name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 32, power_w: 38, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 28, memory_used_gb: 22.5, memory_total_gb: 80, temperature: 52, power_w: 165, fan_speed: 32, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 73, memory_used_gb: 58.7, memory_total_gb: 80, temperature: 68, power_w: 320, fan_speed: 58, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 34, power_w: 42, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 46, memory_used_gb: 36.8, memory_total_gb: 80, temperature: 60, power_w: 210, fan_speed: 42, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 0, memory_used_gb: 0.0, memory_total_gb: 80, temperature: 33, power_w: 40, fan_speed: 0, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 85, memory_used_gb: 68.2, memory_total_gb: 80, temperature: 73, power_w: 365, fan_speed: 62, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{ name: 'NVIDIA A800', gpu_percent: 15, memory_used_gb: 12.0, memory_total_gb: 80, temperature: 45, power_w: 95, fan_speed: 22, clock_mhz: 1410, driver_version: '535.86.10' },
|
||||
{
|
||||
id: 0,
|
||||
uuid: 'GPU-MOCK-A800-00',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'idle',
|
||||
gpu_percent: 0,
|
||||
memory_used_gb: 0,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 0,
|
||||
temperature: 32,
|
||||
power_w: 38,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 0,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'GPU-MOCK-A800-01',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'busy',
|
||||
gpu_percent: 28,
|
||||
memory_used_gb: 22.5,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 28.1,
|
||||
temperature: 52,
|
||||
power_w: 165,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 32,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [
|
||||
{ pid: 18421, name: 'python', task_name: '指令微调任务', user: 'trainer', memory_used_gb: 18.6 },
|
||||
{ pid: 18503, name: 'python', task_name: '训练指标采集', user: 'trainer', memory_used_gb: 3.9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
uuid: 'GPU-MOCK-A800-02',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'busy',
|
||||
gpu_percent: 73,
|
||||
memory_used_gb: 58.7,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 73.4,
|
||||
temperature: 68,
|
||||
power_w: 320,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 58,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [
|
||||
{ pid: 20118, name: 'python', task_name: '大模型推理服务', user: 'inference', memory_used_gb: 56.4 },
|
||||
{ pid: 20207, name: 'python', task_name: '推理指标采集', user: 'inference', memory_used_gb: 2.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
uuid: 'GPU-MOCK-A800-03',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'idle',
|
||||
gpu_percent: 0,
|
||||
memory_used_gb: 0,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 0,
|
||||
temperature: 34,
|
||||
power_w: 42,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 0,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
uuid: 'GPU-MOCK-A800-04',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'busy',
|
||||
gpu_percent: 46,
|
||||
memory_used_gb: 36.8,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 46,
|
||||
temperature: 60,
|
||||
power_w: 210,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 42,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [
|
||||
{ pid: 22680, name: 'python', task_name: '批量评测任务', user: 'evaluation', memory_used_gb: 32 },
|
||||
{ pid: 22814, name: 'python', task_name: '模型裁判服务', user: 'evaluation', memory_used_gb: 4.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
uuid: 'GPU-MOCK-A800-05',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'idle',
|
||||
gpu_percent: 0,
|
||||
memory_used_gb: 0,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 0,
|
||||
temperature: 33,
|
||||
power_w: 40,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 0,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
uuid: 'GPU-MOCK-A800-06',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'warning',
|
||||
gpu_percent: 85,
|
||||
memory_used_gb: 68.2,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 85.3,
|
||||
temperature: 73,
|
||||
power_w: 365,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 62,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [
|
||||
{ pid: 24129, name: 'python', task_name: '全参数训练任务', user: 'trainer', memory_used_gb: 63.5 },
|
||||
{ pid: 24211, name: 'python', task_name: '训练检查点保存', user: 'trainer', memory_used_gb: 4.7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
uuid: 'GPU-MOCK-A800-07',
|
||||
name: 'NVIDIA A800',
|
||||
status: 'busy',
|
||||
gpu_percent: 15,
|
||||
memory_used_gb: 12,
|
||||
memory_total_gb: 80,
|
||||
memory_percent: 15,
|
||||
temperature: 45,
|
||||
power_w: 95,
|
||||
power_limit_w: 400,
|
||||
fan_speed: 22,
|
||||
clock_mhz: 1410,
|
||||
driver_version: '535.86.10',
|
||||
processes: [
|
||||
{ pid: 25367, name: 'python', task_name: '在线推理服务', user: 'inference', memory_used_gb: 12 },
|
||||
],
|
||||
},
|
||||
],
|
||||
network: {
|
||||
download_mb: 1024,
|
||||
upload_mb: 256,
|
||||
download_mb_s: 42.6,
|
||||
upload_mb_s: 12.4,
|
||||
},
|
||||
system: {
|
||||
uptime_seconds: 86400 * 3 + 3600 * 7 + 1800,
|
||||
@@ -112,8 +262,6 @@ export const mockDatasets: DatasetItem[] = [
|
||||
create_time: '2025-12-20T08:00:00Z',
|
||||
files: [
|
||||
{ id: 'finance-train-jsonl', name: 'finance_train.jsonl', size: '86.4 MB' },
|
||||
{ id: 'finance-validation-jsonl', name: 'finance_validation.jsonl', size: '21.8 MB' },
|
||||
{ id: 'dataset-readme', name: 'README.md', size: '4.2 KB' },
|
||||
],
|
||||
},
|
||||
{ id: 2, name: '法律文书-训练集', type: 'train', storage_type: 'local', source: 'upload', size: '256 MB', count: 15230, description: '法律文书数据集', create_time: '2025-12-25T10:30:00Z' },
|
||||
@@ -129,10 +277,7 @@ export const mockDatasets: DatasetItem[] = [
|
||||
...dataset,
|
||||
files: dataset.files?.length
|
||||
? dataset.files
|
||||
: [
|
||||
{ id: `dataset-${dataset.id}-samples`, name: 'dataset_samples.jsonl', size: dataset.size || '12.8 MB' },
|
||||
{ id: `dataset-${dataset.id}-readme`, name: 'README.md', size: '3.8 KB' },
|
||||
],
|
||||
: [{ id: `dataset-${dataset.id}-samples`, name: 'dataset_samples.jsonl', size: dataset.size || '12.8 MB' }],
|
||||
}))
|
||||
|
||||
export const mockDatasetPreviews: Record<string, string> = {
|
||||
|
||||
64
frontend/src/mock/datasetVersions.ts
Normal file
64
frontend/src/mock/datasetVersions.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export interface StoredDatasetVersion {
|
||||
id: string
|
||||
version: number
|
||||
create_time: string
|
||||
description?: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface StoredDatasetVersionState {
|
||||
versions: StoredDatasetVersion[]
|
||||
active_version_id: string
|
||||
}
|
||||
|
||||
export function createInitialVersionState(
|
||||
content: string,
|
||||
createTime = '2026-07-13T00:00:00Z',
|
||||
): StoredDatasetVersionState {
|
||||
const version: StoredDatasetVersion = {
|
||||
id: 'v1',
|
||||
version: 1,
|
||||
create_time: createTime,
|
||||
description: '初始版本',
|
||||
content,
|
||||
}
|
||||
return { versions: [version], active_version_id: version.id }
|
||||
}
|
||||
|
||||
/** 追加不可变版本快照,并自动把新版本设为当前版本。 */
|
||||
export function appendDatasetVersion(
|
||||
state: StoredDatasetVersionState,
|
||||
content: string,
|
||||
createTime: string,
|
||||
description = '在线编辑',
|
||||
): StoredDatasetVersionState {
|
||||
const nextNumber = Math.max(0, ...state.versions.map((item) => item.version)) + 1
|
||||
const version: StoredDatasetVersion = {
|
||||
id: `v${nextNumber}`,
|
||||
version: nextNumber,
|
||||
create_time: createTime,
|
||||
description,
|
||||
content,
|
||||
}
|
||||
return {
|
||||
versions: [...state.versions, version],
|
||||
active_version_id: version.id,
|
||||
}
|
||||
}
|
||||
|
||||
/** 仅移动当前版本指针,不修改任何历史快照。 */
|
||||
export function activateDatasetVersion(
|
||||
state: StoredDatasetVersionState,
|
||||
versionId: string,
|
||||
): StoredDatasetVersionState {
|
||||
if (!state.versions.some((item) => item.id === versionId)) {
|
||||
throw new Error('数据集版本不存在')
|
||||
}
|
||||
return { ...state, active_version_id: versionId }
|
||||
}
|
||||
|
||||
export function getActiveDatasetVersion(state: StoredDatasetVersionState) {
|
||||
const version = state.versions.find((item) => item.id === state.active_version_id)
|
||||
if (!version) throw new Error('当前数据集版本不存在')
|
||||
return version
|
||||
}
|
||||
@@ -118,7 +118,7 @@ onMounted(loadData)
|
||||
<template #actions="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" link size="small" @click="handlePreview(row)">
|
||||
<i class="fa fa-eye" style="margin-right: 4px" />预览
|
||||
<i class="fa fa-eye" style="margin-right: 4px" />详情
|
||||
</el-button>
|
||||
<el-button type="success" link size="small" @click="handleDownload(row)">
|
||||
<i class="fa fa-download" style="margin-right: 4px" />下载
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
121
frontend/src/views/dataset/datasetRecords.ts
Normal file
121
frontend/src/views/dataset/datasetRecords.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export type DatasetRecordKind = 'object' | 'primitive' | 'invalid'
|
||||
|
||||
export interface DatasetRecord {
|
||||
sourceIndex: number
|
||||
displayIndex: number
|
||||
raw: string
|
||||
kind: DatasetRecordKind
|
||||
value: unknown
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DatasetRecordResult {
|
||||
supported: boolean
|
||||
records: DatasetRecord[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function parseValue(raw: string, sourceIndex: number, displayIndex: number): DatasetRecord {
|
||||
try {
|
||||
const value: unknown = JSON.parse(raw)
|
||||
return {
|
||||
sourceIndex,
|
||||
displayIndex,
|
||||
raw,
|
||||
kind: isObjectRecord(value) ? 'object' : 'primitive',
|
||||
value,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
sourceIndex,
|
||||
displayIndex,
|
||||
raw,
|
||||
kind: 'invalid',
|
||||
value: raw,
|
||||
error: error instanceof Error ? error.message : 'JSON 格式不正确',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 将 JSONL 或 JSON 文件解析为可逐条编辑的样本,保留原始物理位置。 */
|
||||
export function parseDatasetRecords(content: string, fileName: string): DatasetRecordResult {
|
||||
const normalizedName = fileName.toLowerCase()
|
||||
if (normalizedName.endsWith('.jsonl')) {
|
||||
const records: DatasetRecord[] = []
|
||||
content.split('\n').forEach((line, sourceIndex) => {
|
||||
if (!line.trim()) return
|
||||
records.push(parseValue(line, sourceIndex, records.length + 1))
|
||||
})
|
||||
return { supported: true, records }
|
||||
}
|
||||
|
||||
if (normalizedName.endsWith('.json')) {
|
||||
try {
|
||||
const root: unknown = JSON.parse(content)
|
||||
const values = Array.isArray(root) ? root : [root]
|
||||
return {
|
||||
supported: true,
|
||||
records: values.map((value, sourceIndex) => ({
|
||||
sourceIndex,
|
||||
displayIndex: sourceIndex + 1,
|
||||
raw: JSON.stringify(value),
|
||||
kind: isObjectRecord(value) ? 'object' : 'primitive',
|
||||
value,
|
||||
})),
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
supported: true,
|
||||
records: [{
|
||||
sourceIndex: 0,
|
||||
displayIndex: 1,
|
||||
raw: content,
|
||||
kind: 'invalid',
|
||||
value: content,
|
||||
error: error instanceof Error ? error.message : 'JSON 格式不正确',
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { supported: false, records: [] }
|
||||
}
|
||||
|
||||
/** 仅替换目标样本;JSONL 中其他行(包括空行和原格式)保持不变。 */
|
||||
export function updateDatasetRecord(
|
||||
content: string,
|
||||
fileName: string,
|
||||
sourceIndex: number,
|
||||
nextValue: unknown,
|
||||
) {
|
||||
const normalizedName = fileName.toLowerCase()
|
||||
if (normalizedName.endsWith('.jsonl')) {
|
||||
const lines = content.split('\n')
|
||||
if (sourceIndex < 0 || sourceIndex >= lines.length) throw new Error('目标样本不存在')
|
||||
lines[sourceIndex] = JSON.stringify(nextValue)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
if (normalizedName.endsWith('.json')) {
|
||||
let root: unknown
|
||||
try {
|
||||
root = JSON.parse(content)
|
||||
} catch {
|
||||
if (sourceIndex !== 0) throw new Error('目标样本不存在')
|
||||
return JSON.stringify(nextValue, null, 2)
|
||||
}
|
||||
if (Array.isArray(root)) {
|
||||
if (sourceIndex < 0 || sourceIndex >= root.length) throw new Error('目标样本不存在')
|
||||
root[sourceIndex] = nextValue
|
||||
return JSON.stringify(root, null, 2)
|
||||
}
|
||||
if (sourceIndex !== 0) throw new Error('目标样本不存在')
|
||||
return JSON.stringify(nextValue, null, 2)
|
||||
}
|
||||
|
||||
throw new Error('当前文件不支持逐条编辑')
|
||||
}
|
||||
Reference in New Issue
Block a user