86 Commits

Author SHA1 Message Date
c9058a27b7 Merge pull request 'ft_wyt' (#6) from ft_wyt into main
Reviewed-on: #6
2026-07-21 14:23:17 +08:00
wuyongtao
1e438164c1 fix: 更新 Dockerfile.backend 配置
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 12:45:47 +08:00
wuyongtao
f4864fafd0 docs: 更新 README.md
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 12:41:30 +08:00
wuyongtao
9798b34717 feat: 重构前端 dist 构建产物,更新 Docker 配置及文档
- 重新构建 frontend/dist(新版 hash 替换旧版)
- 更新 docker 前端 Dockerfile 及 docker-compose 配置
- 新增 docs/team-development-plan.md 团队开发计划文档
- 更新 README、系统开发计划等文档
- 更新 LoginView 组件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 12:36:33 +08:00
wuyongtao
284995d79c chore: 更新 Docker 部署配置及文档
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 11:36:43 +08:00
wuyongtao
e18a367abb feat: 添加前端构建产物 frontend/dist,更新 gitignore 规则
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 11:11:04 +08:00
wuyongtao
817d13c8f7 chore: 更新后端配置、Docker部署及前端API请求配置
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 11:06:34 +08:00
wuyongtao
a72b8f1e4b feat: 更新后端平台模块、数据库、Compute引擎及多项配置文档
- 更新 backend 平台 API、platform_store、session 数据库模块
- 新增 backend SQL 初始化脚本
- 更新 compute 引擎适配器及 README
- 更新 Docker 部署配置(app/compute)
- 更新前端入口、环境类型声明及 README
- 新增 docs/menu-functional-requirements.md 菜单功能需求文档
- 更新多项项目文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 10:55:44 +08:00
wuyongtao
bccd3bf448 feat: 更新后端配置、Docker部署、API模块及多项文档
- 更新后端 main.py、config.py 核心配置
- 更新 compute API 模块
- 更新 Docker 部署配置(app/compute docker-compose、nginx、环境变量)
- 更新前端 API 模块(dataset、model、request)及 vite 配置
- 更新多项项目文档(架构、部署、开发计划、日志等)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 10:09:36 +08:00
wuyongtao
a67ca2c19c feat: 添加平台管理、计算模块适配器及前端页面更新
- 新增 platform API 端点和存储
- 新增 llama_factory 适配器
- 新增前端 compute、guide、system 等视图页面
- 新增 echarts 插件和 mock 数据
- 更新 Docker 配置、后端配置及文档
- 更新前端路由、API、侧边栏等组件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 09:23:43 +08:00
wuyongtao
2c1e08a271 feat: 重构 Docker 配置结构,添加 compute 模块及新增文档
- 将 Dockerfile 和 docker-compose.yml 迁移至 docker/ 目录下统一管理
- 新增 compute 计算模块(API 入口、依赖配置)
- 新增 docker/app 和 docker/compute 部署配置
- 新增 demo-development-plan.md 演示开发计划文档
- 更新后端 API 设计、部署计划、架构需求等文档
- 更新 postgres 数据库 schema

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 14:59:31 +08:00
1468834116 Merge pull request 'ft_wyt' (#5) from ft_wyt into main
Reviewed-on: #5
2026-07-16 13:54:08 +08:00
wuyongtao
ba4059fe3b feat: 添加后端架构、计算模块及部署文档 2026-07-16 13:47:37 +08:00
wuyongtao
4050c120d5 feat: 添加 Docker 支持与项目文档
- 添加 Dockerfile, docker-compose.yml, .dockerignore, nginx 配置
- 添加后端 API 设计文档、平台架构需求文档、系统开发计划
- 添加 PostgreSQL schema 设计
- 更新 README.md 和 design-qa.md
2026-07-16 11:52:05 +08:00
156a952b47 Merge pull request 'dev' (#4) from dev into main
Reviewed-on: #4
2026-07-16 11:04:35 +08:00
caoxiaozhu
4173b53b1b refactor: 前端构建按需化与 Mock 懒加载
移除全量 Element Plus 与全局 VChart 注册,改为按需引入样式与组件内局部图表;Mock 适配器改为按 VITE_ENABLE_MOCK 环境变量懒加载,生产默认不拦截请求;ECharts 精简为看板所需图表,各视图与 stores 同步适配。
2026-07-16 11:03:54 +08:00
caoxiaozhu
ab9e87f948 refactor: 推理对比超时与打字机展示改进
抽取 withTimeout 替代 Promise.race 超时控制,对比结果新增打字机逐字渲染与清理,推理聊天参数与列表类型同步收敛。
2026-07-16 11:03:25 +08:00
caoxiaozhu
5a040366da refactor: usePolling 改为串行轮询
上一轮完成后才安排下一轮避免慢请求重叠,页面不可见时自动暂停、恢复后立即刷新,支持动态间隔与错误回调。
2026-07-16 11:03:15 +08:00
8789019db2 Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2026-07-16 10:18:12 +08:00
caoxiaozhu
3cb20a4a28 style: 登录时长图表标签回到柱条右侧
标签位置由 insideRight 改回 right、颜色改回灰色,避免短柱条内文字溢出。
2026-07-16 09:38:23 +08:00
caoxiaozhu
a6085a2612 chore: 页面表面回归与数据处理文案适配
页面表面回归选择器更新为 task-overview,任务概览补充表面背景变量,数据处理结果步骤标题文案精简。
2026-07-16 09:36:39 +08:00
caoxiaozhu
4899bc8779 style: Dashboard 看板表格行高与图表微调
训练任务表格行高加大提升可读性,登录时长图表留白与标签位置调整避免溢出,回归断言同步。
2026-07-16 09:36:39 +08:00
caoxiaozhu
e70538e64d feat: 顶部栏新增使用文档入口
AppHeader 增加使用文档按钮,新窗口打开 /guide 路由,补充响应式窄屏适配。
2026-07-16 09:36:39 +08:00
39a5390ecd Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-07-14 16:20:08 +08:00
caoxiaozhu
746d1118df style: 登录页高度与滚动行为修正
登录页主区域改为 height 100% 避免内部双滚动条,移动端恢复 min-height 并允许整体滚动。
2026-07-14 16:19:16 +08:00
caoxiaozhu
6e1197d321 docs: 更新视觉与权限设计走查记录
补充本期用户与权限、登录重构、看板优化等改动的视觉与交互走查记录。
2026-07-14 16:11:07 +08:00
caoxiaozhu
a70cf6addd refactor: Dashboard 看板布局与图表优化
DashboardView 调整看板卡片与栅格展示,ECharts 补充图表类型,主布局 dashboard 区域由内嵌滚动改为自适应可滚动以避免内容被裁剪,回归脚本适配。
2026-07-14 16:10:59 +08:00
caoxiaozhu
8b83b8facf refactor: 登录页重构为双栏视觉布局
左侧新增品牌展示区与平台能力介绍,右侧保留登录表单并优化可访问性属性,Element Plus 链接/文本按钮统一样式与焦点表现。
2026-07-14 16:10:55 +08:00
caoxiaozhu
e09c6e81df feat: 用户与权限管理
新增 PermissionCode 权限类型与用户/权限 API,路由注册用户设置/创建/权限页与无权访问页并接入权限守卫,AppSidebar 按权限过滤菜单并新增用户设置入口,auth store 与 mock 适配器同步支持用户管理与新登录认证,回归脚本与 npm 脚本注册。
2026-07-14 16:10:50 +08:00
caoxiaozhu
2bceb686c6 feat: 数据集版本管理支持删除历史版本
DatasetVersionList 新增 next_version_number,API 与 Mock 补充删除版本接口与并发控制,DatasetVersionBar 增加删除操作并对初始版本与最后版本禁用,DatasetPreviewView 接入删除流程,回归脚本与设计走查同步。
2026-07-14 14:47:12 +08:00
cd354f52e6 Merge pull request 'dev' (#1) from dev into main
Reviewed-on: #1
2026-07-14 11:26:14 +08:00
caoxiaozhu
acbd6d91b3 feat: 评测创建支持基础指标配置与启动参数类型化
新增 StartEvalPayload 与 BasicEvalMetricsConfig 类型约束启动评测接口,创建向导补充 BLEU、ROUGE、余弦相似度等基础指标配置,维度表单与规则步骤同步调整,回归脚本扩充断言。
2026-07-14 09:40:17 +08:00
caoxiaozhu
762d48d090 feat: 数据处理向导新增模型配置步骤
StepId 新增 model 步骤,DataProcessCreateView 增加模型选择与生成参数编排,各选项面板与样式适配新步骤流程,回归脚本补充断言。
2026-07-13 17:12:49 +08:00
caoxiaozhu
42f2efb449 refactor: 评测创建步骤导航条结构优化
步骤连接符独立为单独元素并随完成状态高亮,修复窄屏连接线显示。
2026-07-13 17:12:49 +08:00
caoxiaozhu
8c1a0d785c chore: 模型微调更名为模型训练
侧边栏菜单标签与路由标题统一由模型微调改为模型训练。
2026-07-13 16:43:37 +08:00
caoxiaozhu
66eac77c96 feat: 数据处理向导默认数据类型改为结构化
创建页 processType 默认值与草稿恢复兜底由非结构化调整为结构化,更贴合主流使用场景。
2026-07-13 16:41:42 +08:00
caoxiaozhu
d8ba048c6d chore: 调整数据治理菜单顺序
数据集管理移至数据处理之前,与实际使用优先级对齐。
2026-07-13 16:41:42 +08:00
caoxiaozhu
c9ddc3b652 refactor: 数据处理向导生成选项改为平铺布局
移除生成选项面板的高级设置折叠抽屉,温度、最大输出长度、JSON 模式等配置直接平铺展示,简化非结构化与任务设置步骤的嵌套结构,列表新建按钮文案精简。
2026-07-13 16:39:16 +08:00
caoxiaozhu
0917206191 refactor: 优化数据处理向导配置面板布局
数据集划分编辑器调整宽屏与窄屏布局,结构化预处理选项修正 flex 占满,非结构化智能预处理由开关改为复选卡片并补充可访问性标签。
2026-07-13 16:16:50 +08:00
caoxiaozhu
aea0d51b44 test: 新增统一回归运行器
新增 run-regressions.mjs 一键执行全部回归脚本,package.json 注册 npm test 入口与 test:fine-tune-create,page-surface 脚本适配统一运行。
2026-07-13 15:31:01 +08:00
caoxiaozhu
3fabd0c0eb refactor: 数据集预览组件化
拆分 DatasetVersionBar、DatasetRecordTable、DatasetRawPreview、DatasetRecordEditorDialog 子组件,DatasetPreviewView 聚焦编排,预览类型独立到 preview/types.ts,回归脚本适配。
2026-07-13 15:30:11 +08:00
caoxiaozhu
e212de1693 refactor: 训练日志组件化与 Mock 数据增强
拆分 TrainingTaskOverview 组件与 trainingLogModel 状态模型,TrainingLogView 大幅瘦身;Mock 新增按文件路由的训练日志内容与更真实的 GPU 进程占用数据,adapter 类型收敛为 AxiosAdapter,配套新增 mock 内容回归脚本。
2026-07-13 15:29:49 +08:00
caoxiaozhu
e580ec4791 refactor: 数据处理向导拆分组合式函数与子面板
提取 useDataProcessDraft、useDataProcessGeneration 与 dataProcessCreateState 管理向导状态,新增 StructuredOptionsPanel、UnstructuredOptionsPanel、DatasetSplitEditor 子面板组件,样式抽离为独立 scss,DataProcessCreateView 与 TaskSetupStep 大幅瘦身,回归脚本适配。
2026-07-13 15:28:48 +08:00
caoxiaozhu
735a8a71f5 refactor: 调优创建提取表单模型
将默认参数、命令构建、payload 构造逻辑抽离为 fineTuneFormModel.ts,新增 FineTuneStartPayload 类型约束启动训练接口,FineTuneCreateView 瘦身为视图层,列表微调,回归脚本适配。
2026-07-13 15:28:17 +08:00
caoxiaozhu
bff07b7a31 style: 表格操作按钮统一添加图标
评测列表与模型管理列表的详情、编辑、删除按钮补充 Font Awesome 图标,提升操作可辨识度。
2026-07-13 11:51:50 +08:00
caoxiaozhu
91a7c84d7c refactor: 评测列表移除内嵌维度管理
EvalView 移除维度列表加载/删除与相关 Tab 内容,聚焦评测任务展示;维度表单与规则步骤同步微调,回归脚本适配。
2026-07-13 11:47:46 +08:00
caoxiaozhu
5c469a1778 feat: 数据处理向导新增生成控制选项
提取 GenerationOptionsPanel 组件统一管理生成模型、温度、最大长度、JSON 模式与质量过滤配置,StructuredProcessOptions 与 UnstructuredProcessOptions 继承 GenerationControlOptions,CreateView、TaskSetupStep 及各子步骤同步接入,详情/列表小幅调整,回归脚本扩充生成选项断言。
2026-07-13 11:47:33 +08:00
caoxiaozhu
b64d730616 feat: 数据处理任务详情页
新增 DataProcessDetailView,列表查看详情入口跳转到详情页,路由注册 data-process/:id,配套回归脚本与 npm 脚本注册(含数据转换与数据处理详情两个测试脚本)。
2026-07-13 11:04:55 +08:00
caoxiaozhu
0f8f41057a feat: 新增数据类型转换页面
实现 DataConvertView 替换占位页,工具页 JSON 转 JSONL 入口跳转到该页面,路由指向真实视图,配套回归脚本。
2026-07-13 11:04:08 +08:00
caoxiaozhu
03d4c9fbd8 feat: 评测创建改为分步向导
任务配置与评测规则拆分为两个独立步骤,新增步骤导航条与下一步/返回流程,逐步校验任务配置后再进入规则设置,提交时仅校验规则项。
2026-07-13 10:31:40 +08:00
caoxiaozhu
6b48b8fac3 feat: 硬件看板展示 GPU 进程
HardwareView 增加显卡进程占用列表展示,配套回归脚本与 npm test:hardware 脚本注册。
2026-07-13 10:31:29 +08:00
caoxiaozhu
0d82930181 feat: 数据集版本管理
新增数据集版本创建、切换、激活与历史追溯能力,Mock 通过 localStorage 持久化版本状态并接入版本接口路由,预览页支持版本对比与内容回滚,列表补充版本入口,回归脚本扩充断言。
2026-07-13 10:31:20 +08:00
caoxiaozhu
4dde761348 feat: 调优创建支持模型量化配置
新增训练时量化(QLoRA 4/8bit)与训练后导出量化模型开关,支持 bitsandbytes、GPTQ、AWQ、GGUF 四种方法及对应位数、分组、导出格式配置,命令预览与提交参数同步接入,常量补充三类量化选项。
2026-07-13 10:30:52 +08:00
caoxiaozhu
08a567062d feat: 扩展类型定义支撑量化、版本与监控
FineTuneTask 新增量化参数字段(QLoRA 位数、量化方法、位数、分组、GGUF 格式),新增 DatasetVersion 与 DatasetVersionList 支撑数据集版本管理,新增 GpuProcess 完善硬件监控。
2026-07-13 10:30:32 +08:00
caoxiaozhu
862a7972cf refactor: 简化评测创建向导规则来源
移除已有维度/基线两种规则来源,评测创建仅保留新建维度流程并自动生成维度名称;维度名称字段回归独立创建页,表单校验改为返回精确布尔值并启用 scroll-to-error。
2026-07-12 22:49:05 +08:00
caoxiaozhu
687831870c feat: 看板细节优化与设计走查补充
DashboardView 调整窄屏布局与卡片展示,回归脚本补充断言,新增窄屏看板设计截图并更新视觉走查记录。
2026-07-12 15:40:16 +08:00
caoxiaozhu
71ce26fff6 feat: 评测模块新增详情页与创建向导
新增 EvalTaskDetail 与逐样本结果类型,Mock 与适配器补充评测详情接口,路由注册详情页并将维度创建并入向导;EvalCreateView 改为分步向导(任务配置、规则设置、维度表单),新增 EvalDetailView 展示综合得分与样本判定,列表补充详情入口,配套两份回归脚本。
2026-07-12 15:39:59 +08:00
caoxiaozhu
c0f5f4a30a refactor: 数据处理向导拆分源数据上传步骤
将任务设置中的文件上传与外来数据源拉取抽离为独立 SourceUploadStep 组件,TaskSetupStep 聚焦任务信息与处理配置,CreateView 同步接入新步骤与草稿同步,回归脚本补充上传步骤断言。
2026-07-12 15:39:43 +08:00
caoxiaozhu
fecaba040b style: 加深侧边栏激活项背景色
sidebar-active-bg 由 #f1f5f9 调整为 #e2e8f0,提升激活菜单项的视觉对比度。
2026-07-11 14:50:22 +08:00
caoxiaozhu
10b06c749f feat: 数据集列表展示任务来源ID
DatasetItem 新增 task_id 字段,任务来源数据集 Mock 补充对应任务 ID,列表任务 Tab 新增任务ID列,并将上传 Tab 文案与表单标签统一为上传方式。
2026-07-11 14:50:07 +08:00
caoxiaozhu
c5221e1624 feat: 推理对话支持生成参数配置
设置面板新增 Temperature、Top P、Max Tokens 滑块,参数随对话请求实时下发,替换原先硬编码默认值。
2026-07-11 14:49:53 +08:00
caoxiaozhu
cc113db1aa feat: 新增服务看板页面
实现 Dashboard 看板视图并将应用默认入口由调优改为看板,路由、侧边栏默认菜单与登录跳转同步调整;侧边栏增加导航失败高亮校正,布局与 ECharts 注册柱状图适配看板,补充两份回归脚本、设计走查记录与五张设计截图。
2026-07-11 14:49:37 +08:00
caoxiaozhu
ddf47eb8fe feat: 数据处理向导配置体系扩展
新增结构化与非结构化处理选项类型,TaskSetupStep 增加预处理、切分方法、数据集划分等配置 UI,previewModel 实现语义切分与受保护区间算法,CreateView 接入配置状态与草稿持久化并替换为 AppConfirmDialog,回归脚本扩充配置与弹窗断言。
2026-07-11 14:49:10 +08:00
caoxiaozhu
9e16f6358d feat: 新增 AppConfirmDialog 通用确认弹窗组件
基于 Teleport 的 Promise 式确认弹窗,支持键盘关闭、焦点管理、可访问性属性与响应式触控区域,供路由离开等场景替代 window.confirm。
2026-07-11 14:48:54 +08:00
caoxiaozhu
771c258929 refactor: 移除已训练模型的删除入口
清理已训练模型(合并权重)的删除处理函数、表格 delete-fn 绑定与行内删除按钮,避免误删合并产物。
2026-07-11 11:01:02 +08:00
caoxiaozhu
ba465b5016 feat: 调优创建支持 SFT 自动合并权重
FineTuneTask 类型新增 auto_merge 字段,SFT 训练表单新增自动合并权重开关并在创建/启动时随参提交,回归脚本补充 auto_merge 与模型对话框断言。
2026-07-11 11:00:39 +08:00
caoxiaozhu
ee50350dff feat: 数据处理向导支持外来数据源拉取
任务设置步骤新增 external 数据类型,支持 MySQL/PostgreSQL/MongoDB/REST API 配置、测试连接与拉取数据,生成步骤与草稿持久化同步适配,回归脚本放宽分页器数量断言。
2026-07-11 11:00:15 +08:00
caoxiaozhu
a2d65a1123 feat: 任务列表增加任务ID列
数据处理列表与调优列表统一新增任务ID列,并将数据处理示例数据 ID 更新为更贴近真实的长整型。
2026-07-11 10:39:54 +08:00
caoxiaozhu
1455ee7f97 feat: 增强数据集预览功能
重构数据集预览页支持多文件切换与内容高亮,Mock 新增预览数据集与按文件 ID 路由的内容返回,并补充回归测试脚本与 npm 脚本注册。
2026-07-11 10:39:26 +08:00
caoxiaozhu
bc3eddfe84 refactor: 精简评测页面头部操作
移除三个 Tab 下冗余的使用指南与刷新按钮及相关样式,新增任务详情入口占位。
2026-07-11 10:39:04 +08:00
caoxiaozhu
a438684575 refactor: 移除数据处理状态切换 2026-07-10 23:57:07 +08:00
caoxiaozhu
57c3947615 test: 覆盖数据处理任务列表布局 2026-07-10 23:56:06 +08:00
caoxiaozhu
73b2131dd4 docs: 添加数据处理状态切换移除计划 2026-07-10 23:55:16 +08:00
caoxiaozhu
f06b25c228 docs: 设计移除数据处理状态切换 2026-07-10 23:52:37 +08:00
caoxiaozhu
45b7206090 docs: 设计数据预览头部操作移除 2026-07-10 23:42:32 +08:00
caoxiaozhu
9bb26f3b24 feat: 添加数据任务 mock 数据 2026-07-10 17:13:23 +08:00
caoxiaozhu
79843a4b3b docs: 添加数据任务 mock 实施计划 2026-07-10 17:02:46 +08:00
caoxiaozhu
f5b1873ee5 docs: 设计数据任务 mock 数据 2026-07-10 17:01:12 +08:00
caoxiaozhu
89881a4307 readme 2026-07-10 16:52:39 +08:00
caoxiaozhu
acdad260d7 docs: 添加设计文档与视觉走查记录
视觉走查记录、设计方案规格(specs)、实施计划(plans)及配套截图资产,覆盖路由过渡、页面表层级、数据处理向导、训练日志重设计等改进项。
2026-07-10 16:47:29 +08:00
caoxiaozhu
e2eabe3525 test: 添加回归测试脚本
基于 Playwright 的 UI 回归脚本,覆盖返回导航、数据处理向导、调优创建、模型管理、页面表层级、训练日志布局六个场景。
2026-07-10 16:46:23 +08:00
caoxiaozhu
6f6609dae5 feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
2026-07-10 16:45:55 +08:00
caoxiaozhu
c1893cf82c feat: 实现通用组件
顶部栏、侧边栏、分页表格、Markdown 渲染、模型选择对话框、模型状态标签、页面卡片、占位视图等八个公共组件,及 Font Awesome 图标资源。
2026-07-10 16:45:25 +08:00
caoxiaozhu
ca9e05aa91 feat: 实现基础设施层
axios 请求封装及七个业务模块 API,Pinia 状态管理(auth/system/models/tools),Mock 适配器与数据,以及流式对话、轮询、倒计时组合式函数。
2026-07-10 16:45:06 +08:00
caoxiaozhu
8aa67003c8 feat: 搭建应用基础框架
包含应用入口、根组件、路由配置(含登录守卫)、主布局、全局样式、ECharts 插件、类型定义、常量映射表及静态资源。
2026-07-10 16:44:49 +08:00
caoxiaozhu
855308e5fa chore: 初始化前端项目脚手架与构建配置
Vue 3 + TypeScript + Vite 工程配置,包含 package.json、tsconfig、vite.config.ts 及 .gitignore(忽略 node_modules/dist/自动生成类型声明)。
2026-07-10 16:44:23 +08:00
554 changed files with 44172 additions and 2 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.gitignore
node_modules
frontend/node_modules
frontend/dist
frontend/.vite
npm-debug.log*
docker-compose*.yml
README.md
design-qa.md
docs

13
.gitignore vendored
View File

@@ -12,6 +12,10 @@ __pycache__/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
!frontend/dist/
!frontend/dist/**
node_modules/
*.tsbuildinfo
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
@@ -37,6 +41,15 @@ MANIFEST
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
# Runtime data and logs
runtime/
backend/runtime/
logs/
backend/logs/
*.db
*.sqlite
*.sqlite3
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/

163
README.md
View File

@@ -1,3 +1,162 @@
# YG_FT # YG_FT 模型微调平台
远光微调平台 YG_FT 是一个面向企业治理场景的模型微调平台,覆盖用户中心、多租户、项目隔离、数据集管理、模型管理、训练任务、评测、推理、审批流、审计留存、算力调度和训练引擎适配。
当前前端已有基础页面,后端与算力平台已按多人协作开发方式建立工程骨架,并开始实现正式系统主链路能力。当前代码和 SQL 均作为后续生产演进基线维护,不再以一次性演示或静态 Mock 为开发准则。
## 总体架构
```text
YG_FT/
frontend/ # 前端控制台
backend/ # FastAPI 应用平台后端
app/
api/v1/ # 对前端暴露的 REST API
core/ # 配置、日志、中间件、权限等基础能力
db/ # 数据库连接、迁移、事务工具
modules/ # 业务模块目录
schemas/ # Pydantic 入参/出参模型
services/ # 跨模块应用服务
workers/ # 后台任务入口
requirements.txt # 后端 Python 第三方依赖
compute/ # 算力平台与训练框架适配层
api/ # 内部 Compute API
agent/ # 单机多 GPU 调度与进程管理
engines/llama_factory/ # LLaMA-Factory 适配器
file_gateway/ # 本地文件上传、下载、导入、产物管理
docs/ # 需求、接口、数据库、开发计划和部署文档
docker/ # 容器化配置
```
## 平台分层
| 层级 | 职责 | 主要目录 |
| --- | --- | --- |
| 前端控制台 | 用户操作入口、任务看板、项目/模型/数据集/训练/审批/审计页面 | `frontend/` |
| 应用平台后端 | 用户中心、多租户、RBAC/ABAC、项目隔离、元数据、审批流、审计、API 编排 | `backend/` |
| 算力平台 | GPU 发现、资源锁定、训练进程管理、日志采集、产物归档、任务状态同步 | `compute/` |
| 训练引擎 | 当前固定接入 LLaMA-Factory预留其他训练平台适配标准 | `compute/engines/` |
| 数据层 | PostgreSQL、Redis、本地文件存储、日志归档 | `docs/postgres-schema.sql` |
## 当前开发基线
- 使用 FastAPI 提供统一 API 响应结构 `{ code, message, data }`
- 本地运行阶段统一使用 PostgreSQL后端启动时会在 PG 中初始化当前运行表和系统内置账号模型、数据集、算力节点、GPU、微调任务等业务数据必须通过页面、接口或正式导入流程产生。
- 支持登录、模型管理、数据集管理、微调任务创建/启动/停止/进度轮询。
- 支持训练日志、loss 指标、checkpoint 和训练产物接口;真实训练执行器接入前,联调状态机必须通过显式环境变量开启。
- 支持多算力节点、GPU、任务队列、资源副本和资源同步状态接口。
- 前端新增 `/compute` 算力节点页面展示节点地址、权重、标签、启用状态、GPU、队列和资源副本。
- `compute/engines/llama_factory/adapter.py` 提供 LLaMA-Factory 参数校验、命令生成和日志解析基础能力。
## 后端启动
```bash
cd backend
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 17861
```
默认接口前缀为 `/modelTF`,例如:
```text
GET /modelTF/health
POST /modelTF/login
GET /modelTF/model-manage
GET /modelTF/dataset-manage
GET /modelTF/fine-tune
GET /modelTF/compute/nodes
```
本地运行时默认 PostgreSQL 连接:
```text
DATABASE_URL=postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft
```
本地启动前需要确保 PostgreSQL 已监听 `localhost:15432`,并已创建 `yg_ft` 数据库和 `yg_ft` 用户。后端启动后会自动创建当前运行表并写入内置管理员账号,运行数据统一写入 PostgreSQL。
开发阶段内置登录账号:
| 角色 | 账号 | 密码 | 说明 |
| --- | --- | --- | --- |
| 超级管理员 | `admin` | `admin123` | 拥有当前全部页面权限 |
| 操作员 | `operator` | `operator123` | 拥有业务操作相关页面权限 |
以上账号仅用于本地开发和联调。生产环境初始化后应立即修改密码,或改为企业统一身份认证/管理员初始化流程。
## 前端启动
```bash
cd frontend
npm install
npm run dev
```
前端开发服务默认运行在 `http://localhost:16801`,并通过 Vite proxy 将 `/modelTF` 转发到 `http://localhost:17861`
## 算力服务启动
```bash
cd compute
uvicorn api.main:app --reload --port 19100
```
默认 `COMPUTE_MODE=real`。真实 GPU 接入时,在每台算力服务器上部署 Compute API、Agent、File Gateway 和 LLaMA-Factory应用平台通过 `compute_nodes.api_base_url``compute_nodes.file_gateway_url` 主动轮询。仅在隔离联调环境可显式设置 `COMPUTE_MODE=simulator``COMPUTE_EXECUTION_MODE=simulator`
## 日志
后端日志模块位于 `backend/app/core/logging.py`,说明文档见:
- `docs/backend-logging.md`
默认输出:
```text
logs/backend-YYYY-MM-DD.log
logs/error-YYYY-MM-DD.log
```
日志格式为 JSON Lines单个文件不超过 20MB只保留最近 10 天。
## 主要文档
- `docs/platform-architecture-requirements.md`:平台需求、功能模块、页面补全建议。
- `docs/menu-functional-requirements.md`:当前菜单、二级路由、规划菜单、功能需求、接口和数据库映射。
- `docs/backend-api-design.md`FastAPI 接口分组、参数定义、权限说明。
- `docs/postgres-schema.sql`PostgreSQL 数据库脚本,包含权限、用户中心、多租户、审批、审计等模型。
- `docs/system-development-plan.md`多人协作开发计划按前端、后端、DB、部署拆分。
- `docs/team-development-plan.md`3-4 人并行开发分工计划,按人员边界标注页面、接口、数据库和交付节奏。
- `docs/first-version-development-plan.md`当前系统主链路开发计划覆盖前端、后端、DB、Compute API、GPU 和 LLaMA-Factory 适配。
- `docs/backend-logging.md`:后端日志模块使用说明。
- `docs/deployment-plan.md`:后期部署方案,覆盖单机算力服务器部署与应用/算力分离部署。
- `docker/README.md`Docker 部署入口,包含应用服务器和算力服务器两套 Compose 使用方式。
## Docker 部署入口
应用服务器:
```bash
cd docker/app
cp .env.example .env
docker compose up -d
```
算力服务器:
```bash
cd docker/compute
cp .env.example .env
docker compose up -d
```
两套 Compose 均采用代码外挂方式运行,镜像只包含运行时环境和第三方依赖。项目根目录不再保留 `Dockerfile``docker-compose.yml`,部署时统一进入 `docker/app``docker/compute` 目录执行。
## 后续开发原则
- 接口实现优先遵循 `docs/backend-api-design.md`
- 数据库实现优先遵循 `docs/postgres-schema.sql`,后续通过 Alembic 迁移管理变更。
- 前端页面与后端接口、数据库表之间的映射以文档中的“对应页面/功能模块”为准。
- 训练引擎适配必须通过 `compute/engines/` 下的标准接口,不在应用平台后端直接拼接训练命令。
- 敏感信息不得写入日志,生产环境密钥通过环境变量或密钥管理系统注入。

9
backend/.env.example Normal file
View File

@@ -0,0 +1,9 @@
APP_NAME=YG Fine-Tune Platform API
APP_ENV=local
API_PREFIX=/api
LOG_LEVEL=INFO
LOG_DIR=./logs
LOG_FILE_PREFIX=backend
LOG_ERROR_FILE_PREFIX=error
LOG_MAX_BYTES=20971520
LOG_RETENTION_DAYS=10

65
backend/README.md Normal file
View File

@@ -0,0 +1,65 @@
# Backend Service
后端工程使用 FastAPI定位为模型微调平台的应用平台服务负责用户中心、多租户、权限隔离、项目、数据集、模型、训练任务、审批、审计和算力平台编排。
## 目录结构
```text
backend/
app/
main.py # FastAPI 应用入口
api/v1/ # 对前端暴露的 接口路由
core/ # 配置、日志、中间件、权限等基础能力
db/ # 数据库连接、迁移集成、事务工具
modules/ # 业务模块
auth/
tenant/
project/
model/
dataset/
data_process/
fine_tune/
eval/
inference/
approval/
audit/
compute_gateway/
file_gateway/
engine_registry/
retention/
system/
schemas/ # Pydantic 入参/出参模型
services/ # 跨模块应用服务
workers/ # 后台任务入口
requirements.txt # 后端第三方依赖
logs/ # 本地开发日志目录,生产环境建议挂载到独立日志盘
```
## 本地启动
```bash
cd backend
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload
```
健康检查:
```text
GET /modelTF/health
```
## 日志
日志模块位于 `app/core/logging.py`,使用说明见 `../docs/backend-logging.md`
默认日志文件:
```text
logs/backend-YYYY-MM-DD.log
logs/error-YYYY-MM-DD.log
```
文件日志为 JSON Lines 格式,单个文件不超过 20MB只保存最近 10 天,错误日志按 `ERROR` 级别独立拆分,便于 ELK/日志平台采集。

1
backend/app/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Application package."""

View File

@@ -0,0 +1 @@
"""API package."""

View File

@@ -0,0 +1 @@
"""Versioned API package."""

View File

@@ -0,0 +1 @@
"""API endpoint modules."""

View File

@@ -0,0 +1,14 @@
from fastapi import APIRouter
from app.core.logging import get_logger
from app.db.platform_store import get_platform_store
router = APIRouter()
logger = get_logger(__name__)
@router.get("/health")
async def health_check() -> dict[str, object]:
logger.info("health check requested")
return {"code": 0, "message": "ok", "data": get_platform_store().health_metrics()}

View File

@@ -0,0 +1,457 @@
from __future__ import annotations
from typing import Any
import uuid
from fastapi import APIRouter, Body, File, HTTPException, Query, UploadFile
from fastapi.responses import PlainTextResponse
from app.db.platform_store import get_platform_store
router = APIRouter()
def ok(data: Any = None, message: str = "ok") -> dict[str, Any]:
return {"code": 0, "message": message, "data": data}
def fail(status_code: int, message: str) -> HTTPException:
return HTTPException(status_code=status_code, detail={"code": status_code, "message": message, "data": None})
@router.post("/login")
async def login(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
user = get_platform_store().login(payload.get("username", ""), payload.get("password", ""))
if not user:
raise fail(401, "invalid username or password")
return ok({"token": f"platform-token-{user['id']}", "user": user})
@router.get("/me")
async def me() -> dict[str, Any]:
return ok(get_platform_store().users()[0])
@router.get("/dashboard/overview")
async def dashboard_overview() -> dict[str, Any]:
store = get_platform_store()
tasks = store.tasks()
return ok(
{
"models": len(store.models()),
"datasets": len(store.datasets()),
"fine_tune_tasks": len(tasks),
"running_tasks": len([t for t in tasks if t["status"] in {"syncing", "queued", "running"}]),
"compute_nodes": len(store.compute_nodes()),
"gpus": len(store.gpus()),
}
)
@router.get("/system-info")
async def system_info() -> dict[str, Any]:
return ok(get_platform_store().system_info())
@router.get("/users")
async def users() -> dict[str, Any]:
return ok(get_platform_store().users())
@router.post("/users")
async def create_user(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok(get_platform_store().create_user(payload))
@router.put("/users/{user_id}")
async def update_user(user_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_user(user_id, payload))
except KeyError:
raise fail(404, "user not found")
@router.delete("/users/{user_id}")
async def delete_user(user_id: str, current_username: str | None = Query(default=None)) -> dict[str, Any]:
try:
get_platform_store().delete_user(user_id)
return ok({"deleted": user_id, "current_username": current_username})
except KeyError:
raise fail(404, "user not found")
except ValueError as exc:
raise fail(400, str(exc))
@router.get("/model-manage/local-models")
async def local_models() -> dict[str, Any]:
models = [{"path": item.get("path") or "", "name": item["name"]} for item in get_platform_store().models()]
return ok({"models": models})
@router.get("/model-manage/trained-models")
async def trained_models() -> dict[str, Any]:
return ok({"models": get_platform_store().trained_models()})
@router.delete("/model-manage/trained-models/{model_id}")
async def delete_trained_model(model_id: str, type: str = Query(default="merged")) -> dict[str, Any]:
return ok({"deleted": model_id, "type": type})
@router.get("/model-manage/name/{name}")
async def model_by_name(name: str) -> dict[str, Any]:
try:
return ok(get_platform_store().model_by_name(name))
except KeyError:
raise fail(404, "model not found")
@router.get("/model-manage")
async def model_list() -> dict[str, Any]:
return ok(get_platform_store().models())
@router.post("/model-manage")
async def create_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok(get_platform_store().create_model(payload))
@router.get("/model-manage/{model_id}")
async def model_detail(model_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().model(model_id))
except KeyError:
raise fail(404, "model not found")
@router.put("/model-manage/{model_id}")
async def update_model(model_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_model(model_id, payload))
except KeyError:
raise fail(404, "model not found")
@router.put("/model-manage/{model_id}/purpose")
async def update_model_purpose(model_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_model(model_id, {"purpose": payload.get("purpose", "training")}))
except KeyError:
raise fail(404, "model not found")
@router.delete("/model-manage/{model_id}")
async def delete_model(model_id: str) -> dict[str, Any]:
get_platform_store().delete_model(model_id)
return ok({"deleted": model_id})
@router.post("/model-manage/merge")
async def merge_model(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok({"job_id": f"merge_{uuid.uuid4().hex[:12]}", "status": "queued", **payload})
@router.get("/dataset-manage/preview/{file_id}")
async def dataset_preview(file_id: str) -> dict[str, Any]:
try:
row = get_platform_store().dataset_file(file_id)
return ok({"content": row["content"]})
except KeyError:
raise fail(404, "dataset file not found")
@router.get("/dataset-manage/versions/{file_id}")
async def dataset_versions(file_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().file_versions(file_id))
except KeyError:
raise fail(404, "dataset file not found")
@router.get("/dataset-manage/versions/{file_id}/{version_id}")
async def dataset_version_content(file_id: str, version_id: str) -> dict[str, Any]:
try:
row = get_platform_store().dataset_file(file_id)
versions = get_platform_store().file_versions(file_id)["versions"]
version = next((item for item in versions if item["id"] == version_id), None)
if not version:
raise KeyError(version_id)
return ok({"version": version, "content": row["content"]})
except KeyError:
raise fail(404, "dataset version not found")
@router.post("/dataset-manage/versions/{file_id}")
async def create_dataset_version(file_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().create_file_version(file_id, payload))
except KeyError:
raise fail(404, "dataset file not found")
@router.put("/dataset-manage/versions/{file_id}/active")
async def activate_dataset_version(file_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().activate_file_version(file_id, payload["version_id"]))
except KeyError:
raise fail(404, "dataset version not found")
@router.delete("/dataset-manage/versions/{file_id}/{version_id}")
async def delete_dataset_version(file_id: str, version_id: str) -> dict[str, Any]:
return ok(get_platform_store().file_versions(file_id))
@router.post("/dataset-manage/upload/{dataset_id}")
async def upload_dataset_files(dataset_id: str, files: list[UploadFile] = File(default=[])) -> dict[str, Any]:
created: list[dict[str, Any]] = []
store = get_platform_store()
try:
store.dataset(dataset_id)
except KeyError:
raise fail(404, "dataset not found")
with store.connect() as conn:
for file in files:
raw = await file.read()
content = raw.decode("utf-8", errors="replace")
created.append(store.add_dataset_file(conn, dataset_id, file.filename or "upload.jsonl", content))
return ok({"files": created})
@router.get("/dataset-manage/download/{dataset_id}")
async def download_dataset(dataset_id: str) -> PlainTextResponse:
dataset = get_platform_store().dataset(dataset_id)
content = "\n".join([f"{file['name']}" for file in dataset.get("files", [])])
return PlainTextResponse(content, media_type="text/plain")
@router.get("/dataset-manage/download/{dataset_id}/{file_id}")
async def download_dataset_file(dataset_id: str, file_id: str, version_id: str | None = Query(default=None)) -> PlainTextResponse:
row = get_platform_store().dataset_file(file_id)
return PlainTextResponse(row["content"], media_type="text/plain")
@router.get("/dataset-manage")
async def dataset_list() -> dict[str, Any]:
return ok(get_platform_store().datasets())
@router.post("/dataset-manage")
async def create_dataset(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
dataset = get_platform_store().create_dataset(payload)
return ok({"id": dataset["id"]})
@router.get("/dataset-manage/{dataset_id}")
async def dataset_detail(dataset_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().dataset(dataset_id))
except KeyError:
raise fail(404, "dataset not found")
@router.put("/dataset-manage/{dataset_id}")
async def update_dataset(dataset_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_dataset(dataset_id, payload))
except KeyError:
raise fail(404, "dataset not found")
@router.delete("/dataset-manage/{dataset_id}")
async def delete_dataset(dataset_id: str) -> dict[str, Any]:
get_platform_store().delete_dataset(dataset_id)
return ok({"deleted": dataset_id})
@router.get("/fine-tune/check-name")
async def check_fine_tune_name(name: str = Query(...)) -> dict[str, Any]:
exists = any(task["name"] == name for task in get_platform_store().tasks())
return ok({"exists": exists})
@router.get("/fine-tune/progress/{task_id}")
async def fine_tune_progress(task_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().progress(task_id))
except KeyError:
raise fail(404, "fine tune task not found")
@router.post("/fine-tune/tensorboard/start")
async def tensorboard_start() -> dict[str, Any]:
return ok({"status": "running", "url": "http://localhost:6006"})
@router.get("/fine-tune")
async def fine_tune_list() -> dict[str, Any]:
return ok(get_platform_store().tasks())
@router.post("/fine-tune")
async def create_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
task = get_platform_store().create_task(payload)
return ok({"id": task["id"]})
except ValueError as exc:
raise fail(400, str(exc))
@router.post("/fine-tune/start")
async def start_fine_tune(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().start_task(payload))
except KeyError:
raise fail(404, "fine tune task not found")
except RuntimeError as exc:
raise fail(409, str(exc))
@router.get("/fine-tune/{task_id}")
async def fine_tune_detail(task_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().task(task_id))
except KeyError:
raise fail(404, "fine tune task not found")
@router.put("/fine-tune/{task_id}")
async def update_fine_tune(task_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_task(task_id, payload))
except KeyError:
raise fail(404, "fine tune task not found")
@router.post("/fine-tune/stop/{task_id}")
async def stop_fine_tune(task_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().stop_task(task_id))
except KeyError:
raise fail(404, "fine tune task not found")
@router.post("/fine-tune/{task_id}/stop")
async def stop_fine_tune_alt(task_id: str) -> dict[str, Any]:
return await stop_fine_tune(task_id)
@router.delete("/fine-tune/{task_id}")
async def delete_fine_tune(task_id: str) -> dict[str, Any]:
get_platform_store().delete_task(task_id)
return ok({"deleted": task_id})
@router.get("/fine-tune/{task_id}/overview")
async def fine_tune_overview(task_id: str) -> dict[str, Any]:
task = get_platform_store().task(task_id)
return ok({"task": task, "progress": get_platform_store().progress(task_id)})
@router.get("/fine-tune/{task_id}/checkpoints")
async def fine_tune_checkpoints(task_id: str) -> dict[str, Any]:
task = get_platform_store().task(task_id)
checkpoints = []
for step in [50, 100, 150]:
if task.get("progress", 0) >= min(100, step // 2):
checkpoints.append({"step": step, "path": f"/data/yg-ft/outputs/{task['name']}/checkpoint-{step}"})
return ok(checkpoints)
@router.get("/compute/nodes")
async def compute_nodes() -> dict[str, Any]:
return ok(get_platform_store().compute_nodes())
@router.post("/compute/nodes")
async def create_compute_node(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().create_compute_node(payload))
except KeyError as exc:
raise fail(400, f"missing field: {exc}")
@router.put("/compute/nodes/{node_id}")
async def update_compute_node(node_id: str, payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().update_compute_node(node_id, payload))
except KeyError:
raise fail(404, "compute node not found")
@router.post("/compute/nodes/{node_id}/test-connection")
async def test_compute_node(node_id: str) -> dict[str, Any]:
return ok({"node_id": node_id, "success": True, "latency_ms": 12})
@router.post("/compute/nodes/{node_id}/enable")
async def enable_compute_node(node_id: str) -> dict[str, Any]:
return ok(get_platform_store().update_compute_node(node_id, {"enabled": True, "scheduler_status": "online"}))
@router.post("/compute/nodes/{node_id}/disable")
async def disable_compute_node(node_id: str) -> dict[str, Any]:
return ok(get_platform_store().update_compute_node(node_id, {"enabled": False, "scheduler_status": "offline"}))
@router.post("/compute/nodes/{node_id}/drain")
async def drain_compute_node(node_id: str) -> dict[str, Any]:
return ok(get_platform_store().update_compute_node(node_id, {"scheduler_status": "draining"}))
@router.get("/compute/nodes/{node_id}/replicas")
async def compute_node_replicas(node_id: str) -> dict[str, Any]:
return ok(get_platform_store().replicas(node_id))
@router.get("/compute/gpus")
async def compute_gpus() -> dict[str, Any]:
return ok(get_platform_store().gpus())
@router.get("/compute/queue")
async def compute_queue() -> dict[str, Any]:
return ok(get_platform_store().queue())
@router.post("/internal/compute-sync/resources")
async def create_compute_sync(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
sync_id = get_platform_store().create_sync_job(payload.get("target_node_id", "node_01"), payload)
return ok(get_platform_store().sync_job(sync_id))
@router.get("/internal/compute-sync/resources/{sync_id}")
async def compute_sync_detail(sync_id: str) -> dict[str, Any]:
try:
return ok(get_platform_store().sync_job(sync_id))
except KeyError:
raise fail(404, "sync job not found")
@router.get("/training-log-files")
async def training_log_files() -> dict[str, Any]:
return ok(get_platform_store().training_log_files())
@router.get("/training-log-content")
async def training_log_content(file: str = Query(...)) -> dict[str, Any]:
try:
return ok(get_platform_store().training_log_content(file))
except KeyError:
raise fail(404, "training log not found")
@router.get("/log-files")
async def log_files(date: str | None = Query(default=None)) -> dict[str, Any]:
return ok(get_platform_store().log_files(date))
@router.get("/log-content")
async def log_content(file: str = Query(...)) -> dict[str, Any]:
return ok(get_platform_store().log_content(file))
@router.post("/web-log")
async def web_log(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok({"received": True, **payload})

View File

@@ -0,0 +1,9 @@
from fastapi import APIRouter
from app.api.v1.endpoints.platform import router as platform_router
from app.api.v1.endpoints.health import router as health_router
api_router = APIRouter()
api_router.include_router(health_router, tags=["health"])
api_router.include_router(platform_router, tags=["platform"])

View File

@@ -0,0 +1 @@
"""Core infrastructure modules."""

View File

@@ -0,0 +1,57 @@
from dataclasses import dataclass
from functools import lru_cache
import os
def _int_env(name: str, default: int) -> int:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return int(raw)
def _list_env(name: str, default: list[str]) -> list[str]:
raw = os.getenv(name)
if raw is None or raw.strip() == "":
return default
return [item.strip() for item in raw.split(",") if item.strip()]
@dataclass(frozen=True)
class Settings:
app_name: str = os.getenv("APP_NAME", "YG Fine-Tune Platform API")
app_env: str = os.getenv("APP_ENV", "local")
route_prefix: str = os.getenv("MODELTF_ROUTE_PREFIX", "/modelTF")
app_mode: str = os.getenv("APP_MODE", "local")
database_url: str = os.getenv("DATABASE_URL", "postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft")
cors_allow_origins: list[str] = None # type: ignore[assignment]
compute_mode: str = os.getenv("COMPUTE_MODE", "real")
compute_status_sync_mode: str = os.getenv("COMPUTE_STATUS_SYNC_MODE", "polling")
compute_poll_interval_seconds: int = _int_env("COMPUTE_POLL_INTERVAL_SECONDS", 3)
log_level: str = os.getenv("LOG_LEVEL", "INFO")
log_dir: str = os.getenv("LOG_DIR", "./logs")
log_file_prefix: str = os.getenv("LOG_FILE_PREFIX", "backend")
log_error_file_prefix: str = os.getenv("LOG_ERROR_FILE_PREFIX", "error")
log_max_bytes: int = _int_env("LOG_MAX_BYTES", 20 * 1024 * 1024)
log_retention_days: int = _int_env("LOG_RETENTION_DAYS", 10)
def __post_init__(self) -> None:
object.__setattr__(
self,
"cors_allow_origins",
_list_env(
"CORS_ALLOW_ORIGINS",
[
"http://localhost:16801",
"http://127.0.0.1:16801",
"http://localhost:17861",
"http://127.0.0.1:17861",
],
),
)
@lru_cache
def get_settings() -> Settings:
return Settings()

253
backend/app/core/logging.py Normal file
View File

@@ -0,0 +1,253 @@
from __future__ import annotations
from contextvars import ContextVar
from datetime import date, datetime, timedelta
import json
import logging
from logging import Handler, LogRecord
from pathlib import Path
import re
import time
from typing import Any
from uuid import uuid4
from fastapi import FastAPI, Request
from app.core.config import Settings, get_settings
request_id_var: ContextVar[str] = ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
def filter(self, record: LogRecord) -> bool:
record.request_id = request_id_var.get()
return True
class JsonLogFormatter(logging.Formatter):
"""Format one JSON object per line for ELK/Filebeat collection."""
def format(self, record: LogRecord) -> str:
payload: dict[str, Any] = {
"@timestamp": datetime.fromtimestamp(record.created).astimezone().isoformat(
timespec="milliseconds"
),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"file": record.pathname,
"line": record.lineno,
"process": record.process,
"thread": record.thread,
"thread_name": record.threadName,
"request_id": getattr(record, "request_id", "-"),
}
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
if record.stack_info:
payload["stack"] = self.formatStack(record.stack_info)
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
class DateSizeRotatingFileHandler(Handler):
"""Rotate log files by date and size while keeping date in every file name."""
def __init__(
self,
log_dir: str | Path,
file_prefix: str,
max_bytes: int,
retention_days: int,
encoding: str = "utf-8",
) -> None:
super().__init__()
self.log_dir = Path(log_dir)
self.file_prefix = file_prefix
self.max_bytes = max_bytes
self.retention_days = retention_days
self.encoding = encoding
self._current_date: date | None = None
self._stream: Any | None = None
self._current_path: Path | None = None
self.log_dir.mkdir(parents=True, exist_ok=True)
def emit(self, record: LogRecord) -> None:
try:
message = self.format(record) + self.terminator
encoded_size = len(message.encode(self.encoding))
self._ensure_stream()
if self._should_rotate(encoded_size):
self._rotate_by_size()
self._ensure_stream(force=True)
self._stream.write(message)
self.flush()
self._cleanup_expired_files()
except Exception:
self.handleError(record)
@property
def terminator(self) -> str:
return "\n"
def flush(self) -> None:
if self._stream and not self._stream.closed:
self._stream.flush()
def close(self) -> None:
try:
if self._stream and not self._stream.closed:
self._stream.close()
finally:
self._stream = None
super().close()
def _dated_path(self, target_date: date) -> Path:
return self.log_dir / f"{self.file_prefix}-{target_date.isoformat()}.log"
def _ensure_stream(self, force: bool = False) -> None:
today = date.today()
if not force and self._stream and self._current_date == today:
return
if self._stream and not self._stream.closed:
self._stream.close()
self._current_date = today
self._current_path = self._dated_path(today)
self._stream = self._current_path.open("a", encoding=self.encoding)
def _should_rotate(self, incoming_size: int) -> bool:
if not self._current_path or self.max_bytes <= 0:
return False
if not self._current_path.exists():
return False
return self._current_path.stat().st_size + incoming_size > self.max_bytes
def _rotate_by_size(self) -> None:
if not self._current_path or not self._current_path.exists():
return
if self._stream and not self._stream.closed:
self._stream.close()
self._stream = None
stem = self._current_path.stem
suffix = self._current_path.suffix
index = 1
while True:
rotated_path = self.log_dir / f"{stem}.{index}{suffix}"
if not rotated_path.exists():
self._current_path.rename(rotated_path)
return
index += 1
def _cleanup_expired_files(self) -> None:
if self.retention_days <= 0:
return
cutoff = date.today() - timedelta(days=self.retention_days - 1)
pattern = re.compile(
rf"^{re.escape(self.file_prefix)}-(\d{{4}}-\d{{2}}-\d{{2}})(?:\.\d+)?\.log$"
)
for path in self.log_dir.glob(f"{self.file_prefix}-*.log"):
match = pattern.match(path.name)
if not match:
continue
file_date = datetime.strptime(match.group(1), "%Y-%m-%d").date()
if file_date < cutoff:
path.unlink(missing_ok=True)
def configure_logging(settings: Settings | None = None) -> None:
settings = settings or get_settings()
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.setLevel(settings.log_level.upper())
console_formatter = logging.Formatter(
fmt=(
"%(asctime)s | %(levelname)s | pid=%(process)d | %(threadName)s | "
"request_id=%(request_id)s | %(name)s | %(pathname)s:%(lineno)d | %(message)s"
),
datefmt="%Y-%m-%d %H:%M:%S",
)
json_formatter = JsonLogFormatter()
request_filter = RequestIdFilter()
console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter)
console_handler.addFilter(request_filter)
file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_file_prefix,
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
)
file_handler.setFormatter(json_formatter)
file_handler.addFilter(request_filter)
error_file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_error_file_prefix,
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
)
error_file_handler.setLevel(logging.ERROR)
error_file_handler.setFormatter(json_formatter)
error_file_handler.addFilter(request_filter)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(error_file_handler)
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
logger = logging.getLogger(logger_name)
logger.handlers.clear()
logger.propagate = True
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def set_request_id(request_id: str) -> None:
request_id_var.set(request_id)
def setup_request_logging(app: FastAPI) -> None:
logger = get_logger("app.access")
@app.middleware("http")
async def request_logging_middleware(request: Request, call_next): # type: ignore[no-untyped-def]
request_id = request.headers.get("X-Request-ID") or str(uuid4())
token = request_id_var.set(request_id)
started_at = time.perf_counter()
try:
response = await call_next(request)
elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.info(
"request completed method=%s path=%s status_code=%s duration_ms=%.2f client=%s",
request.method,
request.url.path,
response.status_code,
elapsed_ms,
request.client.host if request.client else "-",
)
response.headers["X-Request-ID"] = request_id
return response
except Exception:
elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.exception(
"request failed method=%s path=%s duration_ms=%.2f client=%s",
request.method,
request.url.path,
elapsed_ms,
request.client.host if request.client else "-",
)
raise
finally:
request_id_var.reset(token)

View File

@@ -0,0 +1 @@
"""Database infrastructure package."""

File diff suppressed because it is too large Load Diff

40
backend/app/db/session.py Normal file
View File

@@ -0,0 +1,40 @@
from __future__ import annotations
import os
from collections.abc import Generator
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg://yg_ft:change_me@localhost:15432/yg_ft")
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
future=True,
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False, future=True)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
@contextmanager
def session_scope() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()

View File

@@ -0,0 +1,133 @@
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL,
permissions TEXT NOT NULL,
create_time TEXT NOT NULL,
last_login TEXT,
protected INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
purpose TEXT NOT NULL,
model_source TEXT NOT NULL,
description TEXT,
path TEXT,
api_url TEXT,
api_key TEXT,
online_model_name TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS trained_models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
train_methods TEXT NOT NULL,
base_model_path TEXT,
create_time TEXT NOT NULL,
merged INTEGER NOT NULL DEFAULT 0,
merging INTEGER NOT NULL DEFAULT 0,
merged_path TEXT
);
CREATE TABLE IF NOT EXISTS datasets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
storage_type TEXT NOT NULL,
source TEXT NOT NULL,
task_id TEXT,
size TEXT,
count INTEGER NOT NULL DEFAULT 0,
description TEXT,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS dataset_files (
id TEXT PRIMARY KEY,
dataset_id TEXT NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
name TEXT NOT NULL,
size TEXT,
content TEXT NOT NULL,
active_version_id TEXT NOT NULL,
versions TEXT NOT NULL,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS compute_nodes (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
api_base_url TEXT NOT NULL,
file_gateway_url TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
scheduler_status TEXT NOT NULL,
scheduler_weight INTEGER NOT NULL DEFAULT 100,
tags TEXT NOT NULL,
gpu_count INTEGER NOT NULL DEFAULT 0,
current_running_jobs INTEGER NOT NULL DEFAULT 0,
max_parallel_jobs INTEGER NOT NULL DEFAULT 2,
data_root TEXT NOT NULL,
model_root TEXT NOT NULL,
log_root TEXT NOT NULL,
last_health_check_at TEXT,
health_detail TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS gpus (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
gpu_index INTEGER NOT NULL,
uuid TEXT NOT NULL,
name TEXT NOT NULL,
memory_total_gb DOUBLE PRECISION NOT NULL,
power_limit_w DOUBLE PRECISION NOT NULL,
base_temperature INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS fine_tune_tasks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
payload TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
process_id INTEGER,
create_time TEXT NOT NULL,
start_time TEXT,
completed_at TEXT,
compute_node_id TEXT REFERENCES compute_nodes(id) ON DELETE SET NULL,
gpus TEXT NOT NULL,
sync_job_id TEXT
);
CREATE TABLE IF NOT EXISTS resource_replicas (
id TEXT PRIMARY KEY,
node_id TEXT NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
local_path TEXT NOT NULL,
status TEXT NOT NULL,
sync_status TEXT NOT NULL,
create_time TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS resource_sync_jobs (
id TEXT PRIMARY KEY,
target_node_id TEXT NOT NULL,
resources TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER NOT NULL DEFAULT 0,
create_time TEXT NOT NULL,
completed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_fine_tune_status ON fine_tune_tasks(status);
CREATE INDEX IF NOT EXISTS idx_dataset_files_dataset ON dataset_files(dataset_id);
CREATE INDEX IF NOT EXISTS idx_gpus_node ON gpus(node_id);
CREATE INDEX IF NOT EXISTS idx_replicas_resource ON resource_replicas(resource_type, resource_id);

26
backend/app/main.py Normal file
View File

@@ -0,0 +1,26 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.router import api_router
from app.core.config import get_settings
from app.core.logging import configure_logging, setup_request_logging
def create_app() -> FastAPI:
settings = get_settings()
configure_logging(settings)
app = FastAPI(title=settings.app_name)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_allow_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
setup_request_logging(app)
app.include_router(api_router, prefix=settings.route_prefix)
return app
app = create_app()

View File

@@ -0,0 +1,15 @@
# Backend Module Convention
每个业务模块建议保持一致结构:
```text
module_name/
__init__.py
router.py # FastAPI router
schemas.py # Pydantic request/response models
service.py # Business orchestration
repository.py # Database access
permissions.py # Optional resource permission checks
```
模块边界以 `docs/system-development-plan.md` 的页面模块开发工作包为准。

View File

@@ -0,0 +1 @@
"""Approval workflow module."""

View File

@@ -0,0 +1 @@
"""Audit log module."""

View File

@@ -0,0 +1 @@
"""Authentication and user session module."""

View File

@@ -0,0 +1 @@
"""Application-side compute platform gateway module."""

View File

@@ -0,0 +1 @@
"""Data processing module."""

View File

@@ -0,0 +1 @@
"""Dataset management module."""

View File

@@ -0,0 +1 @@
"""Training engine registry module."""

View File

@@ -0,0 +1 @@
"""Evaluation module."""

View File

@@ -0,0 +1 @@
"""Application-side file gateway module."""

View File

@@ -0,0 +1 @@
"""Fine-tuning task module."""

View File

@@ -0,0 +1 @@
"""Inference and compare module."""

View File

@@ -0,0 +1 @@
"""Model registry module."""

View File

@@ -0,0 +1 @@
"""Project workspace and member module."""

View File

@@ -0,0 +1 @@
"""Retention policy and cleanup module."""

View File

@@ -0,0 +1 @@
"""System health, metrics and logs module."""

View File

@@ -0,0 +1 @@
"""Tenant management module."""

View File

@@ -0,0 +1 @@
"""Shared schemas package."""

View File

@@ -0,0 +1 @@
"""Cross-module services package."""

View File

@@ -0,0 +1 @@
"""Background workers package."""

32
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[project]
name = "yg-ft-backend"
version = "0.1.0"
description = "Backend service for the model fine-tuning platform"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.111.0",
"uvicorn[standard]>=0.30.0",
"python-multipart>=0.0.9",
"pydantic>=2.7.0",
"sqlalchemy>=2.0.30",
"psycopg[binary]>=3.2.1",
"alembic>=1.13.1",
"redis>=5.0.4",
"httpx>=0.27.0",
"PyJWT>=2.8.0",
"passlib[bcrypt]>=1.7.4",
"python-dotenv>=1.0.1",
]
[project.optional-dependencies]
dev = [
"pytest>=8.2.0",
"ruff>=0.5.0",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.pytest.ini_options]
testpaths = ["tests"]

12
backend/requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
fastapi>=0.111.0
uvicorn[standard]>=0.30.0
python-multipart>=0.0.9
pydantic>=2.7.0
sqlalchemy>=2.0.30
psycopg[binary]>=3.2.1
alembic>=1.13.1
redis>=5.0.4
httpx>=0.27.0
PyJWT>=2.8.0
passlib[bcrypt]>=1.7.4
python-dotenv>=1.0.1

29
compute/README.md Normal file
View File

@@ -0,0 +1,29 @@
# Compute Platform
算力平台与应用平台分开部署,本目录用于后续实现单机多 GPU 调度、文件网关和训练引擎适配。
## 目录结构
```text
compute/
api/ # 只允许应用平台访问的内部 Compute API
agent/ # 单机 Agent负责 GPU、进程、工作区管理
engines/
llama_factory/ # LLaMA-Factory 训练引擎适配器
file_gateway/ # 本地磁盘上传、下载、预览、离线导入
tests/
```
## 开发职责
- GPU 发现、状态上报、锁定和释放。
- 本地磁盘工作区管理。
- 创建、停止、查询训练/评测/推理/合并任务。
- LLaMA-Factory 命令生成、日志解析、产物收集。
- 分片上传、短时下载、离线导入。
- 通过服务间 token 接受应用平台调用。
## 运行模式
- 默认 `COMPUTE_EXECUTION_MODE=real`Compute API 只暴露健康检查和接口契约;真实训练执行器完成前,创建作业会返回未实现错误。
- 仅隔离联调时可设置 `COMPUTE_EXECUTION_MODE=simulator`,启用内存状态机和合成 GPU/日志数据。该模式不得作为生产运行路径。

1
compute/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Compute platform package."""

View File

@@ -0,0 +1 @@
"""Compute agent package."""

1
compute/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Compute API package."""

211
compute/api/main.py Normal file
View File

@@ -0,0 +1,211 @@
from __future__ import annotations
import os
import math
import time
from pathlib import Path
from typing import Any
from fastapi import FastAPI, HTTPException
from compute.engines.llama_factory.adapter import build_command, parse_log_line
def create_app() -> FastAPI:
app = FastAPI(title="YG Fine-Tune Compute API")
jobs: dict[str, dict[str, Any]] = {}
route_prefix = os.getenv("MODELTF_ROUTE_PREFIX", "/modelTF").rstrip("/") or "/modelTF"
def now() -> float:
return time.time()
def host_id() -> str:
return os.getenv("COMPUTE_HOST_ID", "gpu-node-01")
def execution_mode() -> str:
return os.getenv("COMPUTE_EXECUTION_MODE", os.getenv("COMPUTE_MODE", "real")).lower()
def job_status(job: dict[str, Any]) -> dict[str, Any]:
if execution_mode() != "simulator":
return job
elapsed = max(0, int(now() - job["created_at"]))
if job["status"] not in {"stopped", "failed", "completed"}:
if elapsed < 5:
job["status"] = "queued"
job["progress"] = 12 + elapsed * 3
elif elapsed < 60:
job["status"] = "running"
job["progress"] = min(96, 25 + int((elapsed - 5) / 55 * 70))
else:
job["status"] = "completed"
job["progress"] = 100
job["logs"] = generate_logs(job)
return job
def generate_logs(job: dict[str, Any]) -> str:
progress = int(job.get("progress", 0) or 0)
points = max(1, min(80, progress))
lines = [
f"[INFO] compute_host_id={host_id()} job_id={job['id']} engine=llama_factory",
f"[INFO] command={' '.join(job['command'])}",
]
for step in range(1, points + 1):
if step % 4 != 0 and step != points:
continue
loss = max(0.11, 2.5 * math.exp(-step / 40))
grad_norm = 0.4 + (step % 5) * 0.04
lr = 0.0002 * max(0.05, 1 - step / 100)
epoch = round(step / points * 3, 4)
lines.append(
"{"
f"'loss': {loss:.4f}, 'grad_norm': {grad_norm:.4f}, "
f"'learning_rate': {lr:.8f}, 'epoch': {epoch:.4f}"
"}"
)
if job.get("status") == "completed":
lines.extend(
[
"***** train metrics *****",
"epoch = 3",
"train_loss = 0.1181",
"train_runtime = 1m 0s",
"***** train metrics end *****",
]
)
return "\n".join(lines)
def gpu_resources() -> list[dict[str, Any]]:
if execution_mode() != "simulator":
return []
active_jobs = [job_status(job) for job in jobs.values() if job["status"] in {"queued", "running"}]
gpus: list[dict[str, Any]] = []
for idx in range(4):
task = next((job for job in active_jobs if idx in job.get("gpus", [])), None)
busy = task is not None and task["status"] == "running"
reserved = task is not None and task["status"] == "queued"
gpus.append(
{
"id": idx,
"uuid": f"GPU-{host_id().upper()}-{idx}",
"name": os.getenv("COMPUTE_GPU_NAME", "NVIDIA A800-SXM4-80GB"),
"status": "busy" if busy else "reserved" if reserved else "idle",
"gpu_percent": 88 if busy else 25 if reserved else 4,
"memory_used_gb": 58 if busy else 12 if reserved else 2,
"memory_total_gb": 80,
"temperature": 61 if busy else 45 if reserved else 36,
"power_w": 215 if busy else 80 if reserved else 25,
"power_limit_w": 300,
"processes": [
{
"pid": task["pid"],
"name": "llamafactory-cli",
"task_name": task["name"],
"memory_used_gb": 58 if busy else 12,
}
]
if task
else [],
}
)
return gpus
@app.get(f"{route_prefix}/health")
async def health_check() -> dict[str, str]:
return {
"status": "ok",
"compute_host_id": os.getenv("COMPUTE_HOST_ID", "unknown"),
}
@app.get(f"{route_prefix}/v1/compute/health")
async def compute_health_check() -> dict[str, str | bool]:
data_root = Path(os.getenv("YG_FT_DATA_ROOT", "/data/yg-ft"))
llama_factory_home = Path(os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
return {
"status": "ok",
"compute_host_id": os.getenv("COMPUTE_HOST_ID", "unknown"),
"app_callback_enabled": os.getenv("ENABLE_APP_CALLBACK", "false").lower() == "true",
"data_root": str(data_root),
"data_root_exists": data_root.exists(),
"llama_factory_home": str(llama_factory_home),
"llama_factory_home_exists": llama_factory_home.exists(),
"execution_mode": execution_mode(),
}
@app.get(f"{route_prefix}/v1/compute/jobs")
async def list_jobs_alias() -> dict[str, list[dict[str, Any]]]:
return {"items": [job_status(job) for job in jobs.values()]}
@app.get(f"{route_prefix}/compute/resources/gpus")
async def list_gpus() -> dict[str, Any]:
return {"items": gpu_resources(), "compute_host_id": host_id()}
@app.post(f"{route_prefix}/compute/jobs")
async def create_job(payload: dict[str, Any]) -> dict[str, Any]:
try:
command = build_command(payload, os.getenv("LLAMA_FACTORY_HOME", "/app/LLaMA-Factory"))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
if execution_mode() != "simulator":
raise HTTPException(
status_code=501,
detail="real compute executor is not implemented yet; set COMPUTE_EXECUTION_MODE=simulator only for isolated development",
)
job_id = str(payload.get("id") or f"job_{int(now() * 1000)}")
job = {
"id": job_id,
"name": payload.get("name", job_id),
"status": "queued",
"progress": 10,
"pid": int(52000 + now() % 10000),
"gpus": payload.get("gpus") or [0],
"created_at": now(),
"command": command.command,
"work_dir": command.work_dir,
"artifacts": [],
"logs": "",
}
jobs[job_id] = job
return job_status(job)
@app.get(f"{route_prefix}/compute/jobs")
async def list_jobs() -> dict[str, Any]:
return {"items": [job_status(job) for job in jobs.values()]}
@app.get(f"{route_prefix}/compute/jobs/{{job_id}}")
async def get_job(job_id: str) -> dict[str, Any]:
job = jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="job not found")
return job_status(job)
@app.post(f"{route_prefix}/compute/jobs/{{job_id}}/stop")
async def stop_job(job_id: str) -> dict[str, Any]:
job = jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="job not found")
job["status"] = "stopped"
job["progress"] = min(job.get("progress", 0), 99)
return job
@app.get(f"{route_prefix}/compute/jobs/{{job_id}}/logs")
async def job_logs(job_id: str) -> dict[str, Any]:
job = jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="job not found")
job = job_status(job)
metrics = [parse_log_line(line) for line in job["logs"].splitlines()]
return {"job_id": job_id, "content": job["logs"], "metrics": [m for m in metrics if m]}
@app.post(f"{route_prefix}/compute/files/upload")
async def upload_file(payload: dict[str, Any]) -> dict[str, Any]:
file_id = str(payload.get("id") or f"file_{int(now() * 1000)}")
return {"id": file_id, "status": "available", "local_path": f"/data/yg-ft/uploads/{file_id}"}
@app.get(f"{route_prefix}/compute/files/{{file_id}}/download")
async def download_file(file_id: str) -> dict[str, Any]:
return {"id": file_id, "status": "ready", "download_url": f"{route_prefix}/compute/files/{file_id}/download"}
return app
app = create_app()

View File

@@ -0,0 +1 @@
"""Training engine adapters package."""

View File

@@ -0,0 +1 @@
"""LLaMA-Factory engine adapter package."""

View File

@@ -0,0 +1,80 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class LlamaFactoryCommand:
command: list[str]
work_dir: str
env: dict[str, str]
def validate_config(config: dict[str, Any]) -> list[str]:
errors: list[str] = []
if not config.get("base_model") and not config.get("model_name_or_path"):
errors.append("base_model or model_name_or_path is required")
if not config.get("dataset") and not config.get("dataset_dir"):
errors.append("dataset or dataset_dir is required")
learning_rate = float(config.get("learning_rate", 0.0002))
if learning_rate <= 0:
errors.append("learning_rate must be greater than zero")
epochs = int(config.get("n_epochs", config.get("num_train_epochs", 1)))
if epochs <= 0:
errors.append("n_epochs must be greater than zero")
return errors
def build_command(config: dict[str, Any], llama_factory_home: str = "/app/LLaMA-Factory") -> LlamaFactoryCommand:
errors = validate_config(config)
if errors:
raise ValueError("; ".join(errors))
model_path = config.get("base_model") or config.get("model_name_or_path")
dataset = config.get("dataset") or config.get("dataset_dir")
output_dir = config.get("output_dir") or f"/data/yg-ft/outputs/{config.get('name', 'training-job')}"
command = [
"llamafactory-cli",
"train",
"--stage",
str(config.get("stage", "sft")).lower(),
"--do_train",
"true",
"--model_name_or_path",
str(model_path),
"--dataset",
str(dataset),
"--template",
str(config.get("template", "qwen")),
"--finetuning_type",
str(config.get("train_method", config.get("finetuning_type", "lora"))),
"--output_dir",
str(output_dir),
"--per_device_train_batch_size",
str(config.get("batch_size", 2)),
"--learning_rate",
str(config.get("learning_rate", 0.0002)),
"--num_train_epochs",
str(config.get("n_epochs", 3)),
"--save_steps",
str(config.get("save_steps", 50)),
]
quantization_bit = int(config.get("quantization_bit", 0) or 0)
if quantization_bit in {4, 8}:
command.extend(["--quantization_bit", str(quantization_bit)])
return LlamaFactoryCommand(command=command, work_dir=str(Path(llama_factory_home)), env={})
def parse_log_line(line: str) -> dict[str, float] | None:
if "loss" not in line or "learning_rate" not in line:
return None
result: dict[str, float] = {}
for key in ["loss", "grad_norm", "learning_rate", "epoch"]:
match = re.search(rf"['\"]?{key}['\"]?\s*:\s*([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)", line)
if match:
result[key] = float(match.group(1))
return result or None

View File

@@ -0,0 +1 @@
"""Local file gateway package."""

5
compute/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi>=0.111.0
uvicorn[standard]>=0.30.0
pydantic>=2.7.0
python-dotenv>=1.0.1
httpx>=0.27.0

View File

@@ -0,0 +1 @@
"""Compute platform tests package."""

429
design-qa.md Normal file
View File

@@ -0,0 +1,429 @@
# Training Log Detail Design QA
## Evidence
- Source visual truth: `docs/superpowers/specs/assets/training-log-detail-option-2.png`
- Implementation screenshot: `docs/superpowers/specs/assets/training-log-detail-final-expanded-1440.png`
- Collapsed implementation screenshot with global surface: `docs/superpowers/specs/assets/training-log-detail-global-surface-1440-v2.png`
- Normalized full-view comparison: `docs/superpowers/specs/assets/training-log-detail-final-comparison-normalized.png`
- Focused parameter comparison: `docs/superpowers/specs/assets/training-log-detail-final-comparison-params.png`
- White-canvas reference: `docs/superpowers/specs/assets/page-white-canvas-reference.png`
- White-canvas implementation: `docs/superpowers/specs/assets/model-edit-white-page-canvas-final-1440.png`
- White-canvas normalized comparison: `docs/superpowers/specs/assets/page-white-canvas-comparison.png`
- Training-log white-canvas screenshot: `docs/superpowers/specs/assets/training-log-detail-white-page-canvas-1440.png`
- Self-surface list screenshot: `docs/superpowers/specs/assets/fine-tune-list-self-surface-final-1440.png`
- Default-canvas detail screenshot: `docs/superpowers/specs/assets/training-log-detail-default-canvas-final-1440.png`
- Reference/detail comparison: `docs/superpowers/specs/assets/page-surface-reference-detail-comparison.png`
- Create-page duplicate-surface evidence: `docs/superpowers/specs/assets/fine-tune-create-double-surface-before-1440.png`
- Create-page single-surface evidence: `docs/superpowers/specs/assets/fine-tune-create-single-surface-final-1440.png`
- Route-transition flash reference: `/var/folders/nk/yks07zp14wb4rv3jqq0pt_4h0000gn/T/codex-clipboard-ea775434-828f-4bca-9714-72887faa9af9.png`
- Route-transition final detail frame: `docs/superpowers/specs/assets/route-transition-detail-final.png`
- Viewport: 1440 × 1024; comparison content normalized to 1200 × 800 after removing the existing 240px sidebar and 60px header from the implementation capture.
- State: `finance-sft-001`, completed, mock data loaded, training parameters expanded.
## Full-view comparison
The implementation preserves the selected two-column hierarchy: task and dataset information occupy the wide left track, runtime facts use the narrow right rail, and training parameters continue as a full-width disclosure section. The selected mock omitted the product shell, so the comparison intentionally crops the existing sidebar and header rather than treating them as design drift.
The global product mode now uses two intentional surface modes. Form and detail routes render inside one `#ffffff` page canvas with 16px radius and 24px content padding. List routes that already own a white table/card surface render that surface directly on the `#f3f5f8` application background, avoiding a redundant white layer.
## Required fidelity surfaces
- Fonts and typography: Existing system font stack is preserved. Heading, label, value, and muted-copy hierarchy match the selected direction; output model uses body-level contrast after iteration 1, and all “未配置” values use `#64748b` on white after iteration 2.
- Spacing and layout rhythm: 24px main gap, 16px section gap, 12px surface radius, and light row separators match the selected composition. The existing application shell reduces usable content width, but normalized proportions remain aligned.
- Colors and tokens: Indigo accent, Slate text, success status, `#f3f5f8` page background, and white content surfaces are consistent with the current product.
- Image and icon fidelity: The screen contains no raster imagery. Existing Font Awesome icons are retained to match the repository's icon system; no placeholder, emoji, CSS drawing, or handcrafted SVG was introduced.
- Copy and content: Task name, status, model, date, duration, dataset metadata, storage, SFT, LoRA, and missing-value copy match the selected design and actual mock data.
## Interaction and responsive checks
- Parameter disclosure changed from `aria-expanded="false"` to `true` after activation, and the expanded content became visible.
- At 1000px viewport width, the overview changed to one column and the document had no horizontal overflow.
- At 700px viewport width, dataset metrics and parameter rows changed to one column and the document had no horizontal overflow.
- Browser console: no errors. One existing Element Plus `el-link` underline deprecation warning was emitted by the login flow and is unrelated to this page.
## Comparison history
### Iteration 1 — blocked
- [P2] The implementation added a visible “基础训练参数” heading that did not exist in the selected mock, creating extra vertical space.
- [P2] “暂未生成” was styled too faintly compared with the selected design.
Fixes:
- Removed the redundant visible base-parameter heading while retaining an accessible region label.
- Restored body-level contrast for “暂未生成”.
Post-fix evidence:
- `docs/superpowers/specs/assets/training-log-detail-final-comparison-normalized.png`
- `docs/superpowers/specs/assets/training-log-detail-final-comparison-params.png`
### Iteration 2 — blocked
- [P2] “未配置” values used `#94a3b8` on white, below WCAG AA contrast for 14px text.
Fix:
- Updated muted values to `#64748b`; the regression check now calculates the contrast ratio and requires at least 4.5:1.
Post-fix evidence:
- Browser computed color: `rgb(100, 116, 139)`.
- Browser console: no errors.
### Iteration 3 — passed
No actionable P0/P1/P2 differences remain. The retained P3 difference is that the generated mock does not include the real product sidebar/header; this is an intentional constraint because the existing shell is shared by every page.
### Iteration 4 — clarified global page canvas, passed
- [P1] The earlier interpretation left the route content directly on the gray layout background and only made individual cards white. The clarified reference requires a single white page canvas behind every route.
Fixes:
- Split the shell and page tokens into `--app-shell-bg: #f3f5f8` and `--app-page-bg: #ffffff`.
- Added one global `.page-canvas` around every route in `MainLayout.vue`.
- Added 16px outer gutter, 16px canvas radius, 24px canvas padding, and a subtle canvas shadow.
- Flattened a route-root `PageCard` to prevent a duplicate large card layer.
Post-fix evidence:
- `docs/superpowers/specs/assets/page-white-canvas-comparison.png`
- Browser computed canvas: white background, 16px radius, 24px padding; outer shell: `rgb(243, 245, 248)`.
- Both the model-edit page and training-log page render inside the same global white canvas without horizontal overflow.
### Iteration 5 — corrected list-page surface ownership, passed
- [P1] Applying the white page canvas to every route created a redundant layer on list pages because `DataTablePage`, model evaluation, and model management already provide their own white root card.
Fixes:
- Added explicit `pageSurface: 'self'` metadata to each self-surfaced list route: model tuning, model evaluation, model inference, model management, data processing, and dataset management.
- Added `.page-canvas.is-self-surface` to remove the outer canvas padding, radius, background, and shadow only for those routes.
- Preserved the default white canvas for training-log, create, edit, preview, chat, and result routes.
Post-fix evidence:
- `docs/superpowers/specs/assets/fine-tune-list-self-surface-final-1440.png`
- `docs/superpowers/specs/assets/training-log-detail-default-canvas-final-1440.png`
- `docs/superpowers/specs/assets/page-surface-reference-detail-comparison.png`
- Browser computed list state: transparent outer canvas, 0px padding/radius, no shadow; white 12px-radius list card on `rgb(243, 245, 248)` shell.
- Browser computed detail state: white outer canvas, 24px padding, 16px radius, subtle shadow.
- Both states have no horizontal overflow and no console errors at 1440 × 900.
### Iteration 6 — flattened wrapped root PageCard, passed
- [P1] The training-task creation route wraps its root `PageCard` in `.fine-tune-create`. The earlier selector only matched a `PageCard` directly under `.page-canvas`, so this page retained a second white background, 12px radius, and card shadow.
Fixes:
- Added an explicit `.page-card-host` marker to the training-task creation route root; the layout flattens only a directly rendered root `PageCard` or a `PageCard` inside that explicit host.
- Root `PageCard` now uses a transparent background, 0px radius, no shadow, and no bottom margin while preserving its header/body layout.
- Kept the selector excluded from `.is-self-surface`, so list cards retain their own white background, 12px radius, and shadow.
- Rejected a generic one-level descendant selector because it would also match the training-log parameter card.
Post-fix evidence:
- `docs/superpowers/specs/assets/fine-tune-create-double-surface-before-1440.png`
- `docs/superpowers/specs/assets/fine-tune-create-single-surface-final-1440.png`
- Browser computed create-page root card: transparent background, 0px radius, no shadow; outer canvas remains white with 24px padding.
- Browser computed list-page card remains white with 12px radius and subtle shadow on a transparent outer canvas.
- Browser computed training-log parameter card remains white with 12px radius and subtle shadow, confirming that internal business cards are not flattened.
- Both pages have no horizontal overflow; create-page console has no errors.
### Iteration 7 — removed page-level opacity transition, passed
- [P1] When navigating from a self-surface list to a default-canvas secondary page, `route.meta.pageSurface` changed immediately while the old list remained for the 150ms `out-in` leave animation. The result was a semi-transparent old list rendered inside the new white canvas.
Fixes:
- Removed the page-level Vue `transition` wrapper from `MainLayout.vue`.
- Removed the `.fade-enter-*` and `.fade-leave-*` opacity rules.
- Preserved local component animations such as dialogs, disclosures, and the selected-row batch bar.
Post-fix evidence:
- Source flash frame: `/var/folders/nk/yks07zp14wb4rv3jqq0pt_4h0000gn/T/codex-clipboard-ea775434-828f-4bca-9714-72887faa9af9.png`.
- Final detail frame: `docs/superpowers/specs/assets/route-transition-detail-final.png`.
- Immediate state after list → create: old list absent, route-root opacity `1`, zero `.fade-*` transition elements, correct default canvas.
- Immediate state after create → list: old create page absent, route-root opacity `1`, zero `.fade-*` transition elements, correct self-surface canvas.
- Immediate state after list → training log: old list absent, route-root opacity `1`, zero `.fade-*` transition elements, correct default canvas.
- All three paths had no horizontal overflow; browser console had no errors.
## Build evidence gap
The `type-check` script now uses project-reference mode (`vue-tsc -b --noEmit`) so it no longer reports a false pass. `npm run type-check` and `npm run build` remain blocked by pre-existing TypeScript errors in `src/mock/adapter.ts`, `FineTuneCreateView.vue`, and `FineTuneListView.vue`; no remaining error points to `TrainingLogView.vue` or the page-surface files. `npx vite build` succeeds, proving the updated UI bundles for production.
Design-QA final result: passed
final result: passed
---
# Service Dashboard Design QA
## Evidence
- Source visual truth: `docs/superpowers/specs/assets/service-dashboard-approved-1440.png`
- Browser-rendered implementation: `docs/superpowers/specs/assets/service-dashboard-implementation-1440.png`
- Viewport: 1440 × 1024
- State: authenticated `admin` user on `/dashboard`; service dashboard navigation active; 7-day chart visible; four training tasks visible.
## Full-view comparison
The implementation preserves the approved composition: the product shell stays intact, the service dashboard is the active navigation item, the platform-health summary spans the top, the grouped training chart occupies the wide middle track, service health occupies the narrow track, and the training-task table spans the bottom. The three bar series, dual axes, dates, values, service counts, task names, statuses, progress, accuracy, and actions match the selected mock.
A separate focused crop was not required because both source and implementation evidence are full-resolution desktop captures at a readable scale; the chart labels, axis units, service rows, and every task-table column are legible in the full-view comparison.
## Required fidelity surfaces
- Fonts and typography: the existing Inter/system/PingFang stack is retained. Heading, section title, metric, table header, and muted-copy weights and sizes match the selected direction.
- Spacing and layout rhythm: 24px page padding, 14px section gaps, 10px panel radii, light separators, and the wide-chart/narrow-status grid preserve the selected hierarchy. The implementation uses the repository's 240px sidebar and 60px header exactly.
- Colors and visual tokens: white page canvas, `#f3f5f8` shell, indigo `#4f46e5`, green `#10b981`, amber `#f59e0b`, red `#ef4444`, and slate text are aligned with the source and current product tokens.
- Image and icon fidelity: the page contains no decorative raster imagery. The supplied product logo is preserved and existing Font Awesome icons are used consistently; no emoji, handcrafted SVG, placeholder image, or CSS illustration was introduced.
- Copy and content: dashboard title, health summary, chart legend and units, service states, task names, task status, model names, progress, accuracy, timestamps, and action labels match the approved design.
## Interaction and runtime checks
- Login with the existing `admin` credentials navigated to `/dashboard`, confirming the requested default entry behavior.
- ECharts rendered one canvas; hovering 07/10 exposed the tooltip values: training count 18, GPU count 7, and average accuracy 91%.
- “查看全部任务” navigated to `/fine-tune` and browser back restored `/dashboard`.
- The first “查看详情” action navigated to `/training-log/103942` and browser back restored `/dashboard`.
- Browser console errors: none.
- `npm run test:default-dashboard`: passed.
- `npm run test:dashboard`: passed.
- `npx vite build`: passed.
## Comparison history
### Iteration 1 — passed
No actionable P0/P1/P2 differences remain. The only intentional product constraint is that the sidebar active background follows the repository's current neutral active token instead of the slightly bluer tint produced by ImageGen; location, contrast, label, and active-state clarity remain equivalent.
## Validation gap
`npm run type-check` remains blocked by pre-existing TypeScript errors in the mock adapter, dataset mock typing, data-process list, evaluation tabs, and fine-tune views. No reported error points to `DashboardView.vue`, the ECharts registration, router defaults, login redirect, or dashboard regression scripts. The direct Vite production build succeeds.
Design-QA final result: passed
final result: passed
## Compact dashboard revision
- User annotation reference: `/var/folders/nk/yks07zp14wb4rv3jqq0pt_4h0000gn/T/codex-clipboard-43f63327-063f-47da-9e96-31b53cebc49d.png`
- Revised implementation screenshot: `docs/superpowers/specs/assets/service-dashboard-compact-1440.png`
- Viewport: 1440 × 1024
- State: authenticated dashboard, compact layout, redundant title/action row removed.
### Iteration 2 — passed
The annotated header row containing the duplicate “服务看板” title, subtitle, and “查看告警” action was removed entirely. Section gaps, overview height, health icon, metric type, chart height, service rows, task heading, and task rows were reduced by roughly 10%15%. The result preserves chart labels, dual-axis readability, service-state text, task progress, accuracy, and all task actions while bringing the primary content closer to the top of the page.
- ECharts tooltip remains functional after the height reduction and reports all three 07/10 series values.
- The revised page contains no browser console errors.
- The full-resolution comparison makes the removed annotation target and the compact replacement legible; no focused crop is necessary.
- `npm run test:dashboard`, `npm run test:default-dashboard`, and `npx vite build` pass.
Design-QA final result: passed
final result: passed
## One-screen dashboard revision
- User annotation reference: `/var/folders/nk/yks07zp14wb4rv3jqq0pt_4h0000gn/T/codex-clipboard-3c166ffc-7243-4e57-94fc-48949599c4f1.png`
- Revised implementation screenshot: `docs/superpowers/specs/assets/service-dashboard-one-screen-1440x768.png`
- Viewport: 1440 × 768
- State: authenticated dashboard with the desktop low-height compact rules active.
### Iteration 3 — passed
The platform-status block was reduced again, including its container padding, inner gap, health icon, status copy, metric labels, and metric values. The chart, service rows, task rows, and page-canvas padding now use a dedicated `max-height: 900px` desktop mode. The dashboard page canvas is constrained to the available application viewport so the outer content area does not introduce a vertical scrollbar.
Browser measurements at 1440 × 768:
- document overflow: false
- layout-content overflow: false
- page-canvas overflow: false
- dashboard overflow: false
- task section bottom: 653px within the 768px viewport
- ECharts tooltip: passed with all three series present
- browser console errors: none
- `npm run test:dashboard`, `npm run test:default-dashboard`, and `npx vite build`: passed
Design-QA final result: passed
final result: passed
## Flexible middle-region revision
- User annotation reference: `/var/folders/nk/yks07zp14wb4rv3jqq0pt_4h0000gn/T/codex-clipboard-daf2bbae-baea-418d-9962-7d0e1a1c219b.png`
- Revised implementation screenshot: `docs/superpowers/specs/assets/service-dashboard-flex-middle-1440x900.png`
- Viewport: 1440 × 900
- State: authenticated dashboard with flexible middle-region growth.
### Iteration 4 — passed
The previous fixed-height middle row caused unused white space beneath the task table on taller screens. The dashboard now reserves compact intrinsic height for the platform summary and task table while allowing the chart/service row to consume all remaining viewport height. The ECharts canvas grows with that row, and the service-state rows distribute across the matching height.
Browser measurements:
- at 1440 × 768, chart height: 283px; no document, layout, canvas, or dashboard overflow
- at 1440 × 900, chart height: 415px; no document, layout, canvas, or dashboard overflow
- task table bottom at 1440 × 900: 868px within the 900px viewport
- ECharts tooltip: passed with all three series present
- browser console errors: none
- `npm run test:dashboard`, `npm run test:default-dashboard`, and `npx vite build`: passed
Design-QA final result: passed
final result: passed
## Narrow service-status revision
- User request: make the right-hand service-status panel slightly narrower.
- Revised implementation screenshot: `docs/superpowers/specs/assets/service-dashboard-narrow-service-1440x900.png`
- Viewports: 1440 × 900 and 1440 × 768
- State: authenticated dashboard with the service-status column reduced to approximately 30% of the middle row.
### Iteration 5 — passed
The middle grid now allocates `1.9fr` to the training chart and `0.82fr` to service status, with a 300px minimum width for the service panel. This gives the chart more horizontal space while keeping all service names, status badges, and instance counts fully visible.
Browser measurements:
- at 1440 × 900, chart width: 799px; service width: 345px; service share: 30.1%
- at 1440 × 768, chart width: 799px; service width: 345px
- clipped service cells: none at both tested viewports
- horizontal and vertical document overflow: none at both tested viewports
- browser console errors: none
- `npm run test:dashboard`, `npm run test:default-dashboard`, and `npx vite build`: passed
Design-QA final result: passed
final result: passed
## Taller training-task revision
- User request: increase the training-task region slightly and shorten the middle chart/service region.
- Source visual truth: `docs/superpowers/specs/assets/service-dashboard-narrow-service-1440x900.png` plus the current user annotation.
- Intended viewports: 1440 × 900 and 1440 × 768.
- State: authenticated dashboard with larger task heading and table rows.
### Iteration 6 — blocked
The task section now uses a taller heading and table rows in both standard and low-height desktop modes. Because the middle row is the only flexible region, the additional task height is taken directly from the chart/service row while preserving the one-screen layout contract in code.
Verification evidence:
- `npm run test:dashboard`: passed
- `npm run test:default-dashboard`: passed
- `npx vite build`: passed
- browser-rendered comparison: blocked because the in-app browser rejected the local preview URL under its URL security policy
- implementation screenshot: unavailable for this iteration
Design-QA final result: blocked
final result: blocked
---
# Dataset Version Actions Design QA
## Evidence
- Source visual truth: `/Users/caoxiaozhu/.codex/generated_images/019f5e2e-3bee-77f0-b285-89ef139db56c/exec-48a163b6-d4c9-4646-9199-135957c6e72e.png`
- Historical-version implementation: `/Users/caoxiaozhu/.codex/visualizations/2026/07/14/019f5e2e-3bee-77f0-b285-89ef139db56c/dataset-version-build/implementation-historical-version-final.png`
- Delete-confirm implementation: `/Users/caoxiaozhu/.codex/visualizations/2026/07/14/019f5e2e-3bee-77f0-b285-89ef139db56c/dataset-version-build/implementation-delete-confirm.png`
- Full-view comparison: `/Users/caoxiaozhu/.codex/visualizations/2026/07/14/019f5e2e-3bee-77f0-b285-89ef139db56c/dataset-version-build/comparison-full.png`
- Focused version-control comparison: `/Users/caoxiaozhu/.codex/visualizations/2026/07/14/019f5e2e-3bee-77f0-b285-89ef139db56c/dataset-version-build/comparison-version-controls-final.png`
- Viewport: 1316 × 768 browser window; source crop normalized for the focused comparison.
- State: authenticated dataset detail, V3 current, V2 selected as a read-only historical version.
## Full-view comparison
The existing product shell, dataset summary, version selector, read-only alert, file toolbar, and sample table remain unchanged. The former standalone primary action has been replaced by one compact rounded-square overflow button at the far right of the version-control row, matching the selected hierarchy.
## Focused comparison and required fidelity surfaces
- Fonts and typography: existing system/PingFang stack, 13px labels, 12px metadata, and Element Plus menu text are preserved.
- Spacing and layout rhythm: the 40px overflow trigger aligns with the version selector and leaves the central status copy flexible; the 168px menu provides 40px action rows.
- Colors and visual tokens: the existing indigo primary token is used for the activate icon; the delete item and confirmation action use the danger token.
- Image and icon fidelity: no new raster assets are needed. Existing Font Awesome ellipsis, check-circle, and trash icons match the repository's icon system.
- Copy and content: the menu contains exactly “设为当前版本” and “删除版本”, separated visually; the confirmation names V2 and explains that current V3 is unaffected.
## Interaction checks
- Created V2 and V3 through the real edit-and-save flow, then switched from current V3 to historical V2.
- Historical records became read-only and the overflow trigger appeared; current V3 showed no history-operation trigger.
- Opening the trigger exposed exactly two accessible menu items: “设为当前版本” and “删除版本”.
- Choosing “删除版本” opened the danger confirmation dialog; cancelling returned focus without deleting data.
- Actual deletion behavior, protected-version rejection, optimistic-lock handling, and non-reused version numbers are covered by `test:dataset-preview`.
## Comparison history
### Iteration 1 — blocked
- [P2] The overflow trigger was circular while the selected mock used a small rounded square.
- [P2] The menu did not explicitly lock its target width or primary-action icon color.
Fixes:
- Replaced the circular trigger with a 40px square and 10px radius.
- Set the menu minimum width to 168px, action height to 40px, and the activate icon to the product primary color.
### Iteration 2 — passed
No actionable P0/P1/P2 differences remain. The desktop capture API does not retain the transient popup layer in screenshots, so the open-menu labels were additionally verified through the accessibility tree; exact popup shadow rendering remains a non-blocking P3 capture gap.
final result: passed
---
# Login Page Responsive Design QA
## Evidence
- Source visual truth: `/Users/caoxiaozhu/.codex/generated_images/019f5f3f-f7e1-7e41-9605-ea307d9f09e6/exec-d376c603-0c47-45ad-86fd-3e99c1a95ec7.png`
- Implementation route: `http://localhost:6801/login`
- Implementation screenshot: unavailable because the in-app browser runtime could not initialize in this session.
- Intended desktop viewport: 1536 × 1024.
- Intended laptop viewports: 1366 × 768 and 1280 × 720.
- State: unauthenticated login page, default username and password populated.
## Static and automated evidence
- Added a 1440px width breakpoint that shifts the split from 58/42 to 55/45 and caps the form at 440px.
- Added a short-screen breakpoint for heights up to 820px that reduces title, form, input, footer, and panel spacing without hiding the left visual.
- Kept the single-column fallback at 900px and below.
- `regression-login-layout.mjs`: passed.
- `regression-default-dashboard.mjs`: passed.
- `vue-tsc -b --noEmit`: passed.
- Vite dev transform for `LoginView.vue`: HTTP 200.
- Production build: blocked by the pre-existing missing route component `UserPermissionView.vue`, outside the login-page change.
## Required fidelity surfaces
- Fonts and typography: code uses the existing product font stack with laptop-specific display-size reductions; visual comparison remains unavailable.
- Spacing and layout rhythm: dedicated width and height media queries are present; rendered measurements remain unavailable.
- Colors and visual tokens: existing indigo tokens and the selected dark-purple visual asset are preserved.
- Image quality and asset fidelity: the generated `login-hero-flow.png` is used directly; the official `logo.png` is reused for the brand lockup.
- Copy and content: platform title, supporting copy, form labels, actions, and footer match the selected design.
## Findings
- [P2] Browser-rendered laptop comparison unavailable
Location: login page at 1366 × 768 and 1280 × 720.
Evidence: the in-app browser runtime failed during initialization, so no implementation screenshot or side-by-side comparison could be captured.
Impact: static checks cannot prove that all visible spacing and crop details match the selected design at laptop sizes.
Fix: capture both laptop viewports in a working in-app browser session, compare them with the source, and resolve any remaining P0/P1/P2 differences.
## Comparison history
### Iteration 1 — blocked
- User reported that the initial implementation was optimized for large displays and did not compose well on laptop screens.
- Added explicit laptop-width and short-screen layout rules and passed targeted regression/type checks.
- Post-fix visual evidence remains unavailable because browser capture is blocked.
final result: blocked

267
docker/README.md Normal file
View File

@@ -0,0 +1,267 @@
# Docker 部署说明
本目录按应用服务器和算力服务器拆分 Dockerfile 与 Docker Compose 文件。Compose 文件不包含 `build:`,不会在 `docker compose up` 时自动构建业务镜像。所有业务镜像需要先通过手动 `docker build` 构建,再由 Compose 启动。
## 基础镜像
| 镜像 | 用途 |
| --- | --- |
| `python:3.12-slim` | 应用后端基础镜像,后端运行环境要求 Python 3.12 及以上 |
| `nginx:1.27-alpine` | 前端静态资源与 `/modelTF` 反向代理运行镜像 |
| `hiyouga/llamafactory:latest` | 算力服务基础镜像,基于 LLaMA-Factory 官方镜像扩展 Compute API |
| `postgres:16-alpine` | 开发阶段内置 PostgreSQL |
| `redis:7-alpine` | 开发阶段内置 Redis |
一键拉取基础镜像:
```bash
docker pull python:3.12-slim && \
docker pull nginx:1.27-alpine && \
docker pull hiyouga/llamafactory:latest && \
docker pull postgres:16-alpine && \
docker pull redis:7-alpine
```
Windows PowerShell
```powershell
$images = @(
"python:3.12-slim",
"nginx:1.27-alpine",
"hiyouga/llamafactory:latest",
"postgres:16-alpine",
"redis:7-alpine"
)
$images | ForEach-Object { docker pull $_ }
```
如果部署环境不能访问外网,需要提前在可联网环境执行上述拉取命令,再用 `docker save` / `docker load` 导出导入。
## 业务镜像
| 镜像 | Dockerfile | 构建命令 |
| --- | --- | --- |
| `yg-ft-backend-api:latest` | `docker/app/Dockerfile.backend` | `docker build -f docker/app/Dockerfile.backend -t yg-ft-backend-api:latest .` |
| `yg-ft-frontend-runtime:latest` | `docker/app/Dockerfile.frontend` | `docker build -f docker/app/Dockerfile.frontend -t yg-ft-frontend-runtime:latest .` |
| `yg-ft-compute-api:latest` | `docker/compute/Dockerfile.compute` | `docker build -f docker/compute/Dockerfile.compute -t yg-ft-compute-api:latest .` |
## 对外端口
所有宿主机对外端口统一使用 5 位端口。容器内部端口保持镜像默认端口,便于容器内服务和健康检查稳定。
| 服务 | 宿主机对外端口 | 容器内部端口 | 说明 |
| --- | --- | --- | --- |
| 前端 Nginx | `16801` | `80` | 前端页面入口 |
| 后端 API | `17861` | `8000` | FastAPI 服务 |
| PostgreSQL | `15432` | `5432` | 开发阶段内置数据库 |
| Redis | `16379` | `6379` | 开发阶段内置缓存 |
| Compute API | `19100` | `9100` | 算力服务器 API |
| File Gateway | `19101` | 后续服务端口 | 当前预留,后续拆出文件网关服务时使用 |
注意:`8000` 是后端容器内部端口,不作为宿主机对外访问端口。宿主机或浏览器应访问 `http://<app-server-ip>:17861/modelTF/health`;前端 Nginx 容器在 Docker 网络内部访问 `http://backend-api:8000/modelTF/...`
对应配置文件:
- `docker/app/.env.example`
- `FRONTEND_PORT=16801`
- `BACKEND_API_PORT=17861`
- `POSTGRES_PORT=15432`
- `REDIS_PORT=16379`
- `docker/compute/.env.example`
- `COMPUTE_API_PORT=19100`
- `FILE_GATEWAY_PORT=19101`
## 运行模式
- 应用侧默认 `COMPUTE_MODE=real`,任务状态必须由真实算力同步逻辑更新。
- 算力侧默认 `COMPUTE_EXECUTION_MODE=real`,真实执行器未完成前不会伪造训练作业。
- 仅隔离联调时可显式设置 `COMPUTE_MODE=simulator``COMPUTE_EXECUTION_MODE=simulator`,该模式不得用于测试环境、生产环境或生产升级基线。
## 应用服务器部署
应用服务器包含前端 Nginx、Backend API、PostgreSQL、Redis。
当前 Compose 内置 PostgreSQL 使用 `backend/app/db/sql/001_platform_runtime.sql` 初始化运行库。`docs/postgres-schema.sql` 是完整目标架构设计,不应直接挂载为当前运行库初始化脚本,否则会与当前后端代码的运行表结构不兼容。
首次部署:
```bash
cd <repo-root>
# 1. 使用当前 Windows/宿主机 npm 构建前端静态产物
cd frontend
npm ci
npm run build
cd ..
# 2. 手动构建业务镜像
docker build -f docker/app/Dockerfile.backend -t yg-ft-backend-api:latest .
docker build -f docker/app/Dockerfile.frontend -t yg-ft-frontend-runtime:latest .
# 3. 启动应用服务
cd docker/app
cp .env.example .env
docker compose up -d
```
后端镜像构建过程中会执行依赖导入自检,确认 `fastapi``uvicorn``psycopg``sqlalchemy``redis` 等运行依赖已安装。构建后也可以手动检查:
```bash
docker run --rm yg-ft-backend-api:latest python -c "import psycopg; print(psycopg.__version__)"
```
默认访问地址:
```text
http://<app-server-ip>:16801
```
应用侧代码和数据外挂:
```text
../../backend -> /app
../../frontend/dist -> /usr/share/nginx/html
../../runtime/app/logs/backend -> /opt/yg-ft/logs/backend
../../runtime/app/data -> /data/yg-ft
```
前端容器启动前必须确保 `../../frontend/dist/index.html` 已存在。若前端 Nginx 日志出现 `directory index of "/usr/share/nginx/html/" is forbidden``rewrite or internal redirection cycle while internally redirecting to "/index.html"`,通常表示当前执行 `docker compose` 的项目目录下没有构建好的 `frontend/dist`,或挂载路径不是同一份代码目录。
```bash
# 在执行 docker compose 的同一份代码目录中检查
cd <repo-root>/frontend
npm run build
test -f dist/index.html && ls -lh dist/index.html
cd ../docker/app
docker compose up -d --force-recreate frontend
docker compose logs --tail=80 frontend
```
如果使用 Windows npm 构建、WSL 中运行 Docker Compose需要确认 Windows 路径和 WSL 路径指向同一份仓库。例如在 `D:\...\YG_FT\frontend` 构建不会自动生成 `/mnt/d/wuyongtao/Code/YG_FT/frontend/dist` 下的产物,除非二者本就是同一个目录。
如果使用企业统一 PostgreSQL/Redis修改 `docker/app/.env`
```env
DATABASE_URL=postgresql+psycopg://<user>:<password>@<postgres-host>:15432/<db>
REDIS_URL=redis://<redis-host>:16379/0
USE_BUILTIN_POSTGRES=false
USE_BUILTIN_REDIS=false
```
生产环境如完全使用外部基础设施,可以删除或注释 Compose 中的 `postgres``redis` 服务及 `backend-api.depends_on` 中对应依赖。
## 算力服务器部署
算力服务器包含 Compute API、后续 Compute Agent、File Gateway、GPU runtime、本地训练数据目录和 LLaMA-Factory。`Dockerfile.compute` 基于 LLaMA-Factory 官方镜像:
```dockerfile
FROM hiyouga/llamafactory:latest
```
部署前需要安装:
- NVIDIA Driver
- NVIDIA Container Toolkit
- Docker Engine 和 Docker Compose Plugin
- 本地训练数据目录,默认 `/data/yg-ft`
首次部署:
```bash
cd <repo-root>
# 手动构建算力业务镜像
docker build -f docker/compute/Dockerfile.compute -t yg-ft-compute-api:latest .
# 启动算力服务
cd docker/compute
cp .env.example .env
docker compose up -d
```
健康检查:
```text
GET http://<compute-server-ip>:19100/modelTF/health
GET http://<compute-server-ip>:19100/modelTF/v1/compute/health
```
算力侧代码和数据外挂:
```text
../../compute -> /app/compute
${YG_FT_DATA_ROOT_HOST} -> /data/yg-ft
../../runtime/compute/logs -> /opt/yg-ft/logs/compute
../../runtime/compute/training-logs -> /opt/yg-ft/logs/training
```
## 应用与算力分离部署
应用服务器只需要主动访问算力服务器,不要求算力服务器回调应用服务器。
`docker/app/.env` 中配置:
```env
COMPUTE_API_BASE_URL=http://<compute-server-ip>:19100
FILE_GATEWAY_BASE_URL=http://<compute-server-ip>:19101
COMPUTE_SERVICE_TOKEN=change_me
COMPUTE_STATUS_SYNC_MODE=polling
COMPUTE_POLL_INTERVAL_SECONDS=10
COMPUTE_POLL_BATCH_SIZE=100
```
交互链路:
```text
Frontend
-> Backend API
-> Compute API
-> Compute Agent / LLaMA-Factory
-> 本地数据目录 / 模型目录 / 训练产物
<- Backend Worker 定时轮询 Compute API
```
## 多算力节点部署
多算力节点仍按“单机多 GPU 节点”部署。每台 GPU 服务器都独立部署一套 `docker/compute`
```text
gpu-node-01: docker/compute + /data/yg-ft + 19100/19101
gpu-node-02: docker/compute + /data/yg-ft + 19100/19101
gpu-node-03: docker/compute + /data/yg-ft + 19100/19101
```
节点之间默认不互访。应用平台主动访问每个节点的 Compute API/File Gateway并通过 `compute_nodes``resource_replicas``resource_sync_jobs` 统一调度和同步。
## 常用命令
重新构建应用镜像:
```bash
docker build -f docker/app/Dockerfile.backend -t yg-ft-backend-api:latest .
docker build -f docker/app/Dockerfile.frontend -t yg-ft-frontend-runtime:latest .
```
重新构建算力镜像:
```bash
docker build -f docker/compute/Dockerfile.compute -t yg-ft-compute-api:latest .
```
启动服务:
```bash
cd docker/app
docker compose up -d
cd ../compute
docker compose up -d
```
查看服务:
```bash
docker compose ps
docker compose logs -f
```

45
docker/app/.env.example Normal file
View File

@@ -0,0 +1,45 @@
APP_ENV=prod
APP_NAME=YG Fine-Tune Platform API
MODELTF_ROUTE_PREFIX=/modelTF
CORS_ALLOW_ORIGINS=http://localhost:16801,http://127.0.0.1:16801
FRONTEND_IMAGE=yg-ft-frontend-runtime:latest
BACKEND_API_IMAGE=yg-ft-backend-api:latest
# Five-digit host ports exposed outside the application server.
FRONTEND_PORT=16801
BACKEND_API_PORT=17861
POSTGRES_PORT=15432
REDIS_PORT=16379
POSTGRES_DB=yg_ft
POSTGRES_USER=yg_ft
POSTGRES_PASSWORD=change_me
DATABASE_URL=postgresql+psycopg://yg_ft:change_me@postgres:5432/yg_ft
REDIS_URL=redis://redis:6379/0
# Development uses the built-in PostgreSQL/Redis services in docker-compose.yml.
# For enterprise infrastructure, replace DATABASE_URL/REDIS_URL and remove or disable those services.
USE_BUILTIN_POSTGRES=true
USE_BUILTIN_REDIS=true
LOG_LEVEL=INFO
LOG_DIR=/opt/yg-ft/logs/backend
LOG_FILE_PREFIX=backend
LOG_ERROR_FILE_PREFIX=error
LOG_MAX_BYTES=20971520
LOG_RETENTION_DAYS=10
BACKEND_PROXY_PASS=http://backend-api:8000
# Split deployment: set these to the compute server address, for example http://10.10.20.31:19100.
COMPUTE_API_BASE_URL=http://compute-api:9100
COMPUTE_SERVICE_TOKEN=change_me
FILE_GATEWAY_BASE_URL=http://compute-api:9101
# The application side polls Compute API for job state to avoid opening reverse network access.
COMPUTE_MODE=real
COMPUTE_STATUS_SYNC_MODE=polling
COMPUTE_POLL_INTERVAL_SECONDS=10
COMPUTE_POLL_BATCH_SIZE=100

View File

@@ -0,0 +1,21 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY backend/requirements.txt /tmp/requirements.txt
RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& rm -f /tmp/requirements.txt
RUN python -c "import fastapi, uvicorn, psycopg, sqlalchemy, redis, jwt, passlib, httpx, alembic; print('backend dependency check ok')"
RUN mkdir -p /opt/yg-ft/logs/backend /data/yg-ft \
&& chmod -R 0775 /opt/yg-ft /data/yg-ft
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,9 @@
FROM nginx:1.27-alpine
RUN mkdir -p /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD test -f /usr/share/nginx/html/index.html && wget -qO- http://127.0.0.1/index.html >/dev/null || exit 1

View File

@@ -0,0 +1,123 @@
services:
frontend:
image: ${FRONTEND_IMAGE:-yg-ft-frontend-runtime:latest}
container_name: yg-ft-frontend
depends_on:
backend-api:
condition: service_started
ports:
- "${FRONTEND_PORT:-16801}:80"
environment:
BACKEND_PROXY_PASS: ${BACKEND_PROXY_PASS:-http://backend-api:8000}
volumes:
- ../../frontend/dist:/usr/share/nginx/html:ro
- ../nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
command:
- /bin/sh
- -c
- |
if [ ! -f /usr/share/nginx/html/index.html ]; then
echo "frontend dist is missing: build frontend first and ensure ../../frontend/dist is mounted";
ls -la /usr/share/nginx/html;
exit 1;
fi;
nginx -g 'daemon off;'
networks:
- yg-ft-app
restart: unless-stopped
backend-api:
image: ${BACKEND_API_IMAGE:-yg-ft-backend-api:latest}
container_name: yg-ft-backend-api
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
expose:
- "8000"
ports:
- "${BACKEND_API_PORT:-17861}:8000"
environment:
APP_ENV: ${APP_ENV:-prod}
APP_NAME: ${APP_NAME:-YG Fine-Tune Platform API}
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-http://localhost:16801,http://127.0.0.1:16801}
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://yg_ft:change_me@postgres:5432/yg_ft}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
USE_BUILTIN_POSTGRES: ${USE_BUILTIN_POSTGRES:-true}
USE_BUILTIN_REDIS: ${USE_BUILTIN_REDIS:-true}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
LOG_DIR: ${LOG_DIR:-/opt/yg-ft/logs/backend}
LOG_FILE_PREFIX: ${LOG_FILE_PREFIX:-backend}
LOG_ERROR_FILE_PREFIX: ${LOG_ERROR_FILE_PREFIX:-error}
LOG_MAX_BYTES: ${LOG_MAX_BYTES:-20971520}
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-10}
COMPUTE_API_BASE_URL: ${COMPUTE_API_BASE_URL:-http://compute-api:9100}
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
FILE_GATEWAY_BASE_URL: ${FILE_GATEWAY_BASE_URL:-http://compute-api:9101}
COMPUTE_MODE: ${COMPUTE_MODE:-real}
COMPUTE_STATUS_SYNC_MODE: ${COMPUTE_STATUS_SYNC_MODE:-polling}
COMPUTE_POLL_INTERVAL_SECONDS: ${COMPUTE_POLL_INTERVAL_SECONDS:-10}
COMPUTE_POLL_BATCH_SIZE: ${COMPUTE_POLL_BATCH_SIZE:-100}
PYTHONPATH: /app
volumes:
- ../../backend:/app:ro
- ../../runtime/app/logs/backend:/opt/yg-ft/logs/backend
- ../../runtime/app/data:/data/yg-ft
networks:
- yg-ft-app
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/modelTF/health', timeout=3).read()\""]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
postgres:
image: postgres:16-alpine
container_name: yg-ft-postgres
environment:
POSTGRES_DB: ${POSTGRES_DB:-yg_ft}
POSTGRES_USER: ${POSTGRES_USER:-yg_ft}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change_me}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- ../../backend/app/db/sql/001_platform_runtime.sql:/docker-entrypoint-initdb.d/001-platform-runtime.sql:ro
ports:
- "${POSTGRES_PORT:-15432}:5432"
networks:
- yg-ft-app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: yg-ft-redis
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
ports:
- "${REDIS_PORT:-16379}:6379"
networks:
- yg-ft-app
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
networks:
yg-ft-app:
name: yg-ft-app
volumes:
postgres_data:
redis_data:

View File

@@ -0,0 +1,23 @@
COMPUTE_ENV=prod
COMPUTE_HOST_ID=gpu-node-01
COMPUTE_EXECUTION_MODE=real
MODELTF_ROUTE_PREFIX=/modelTF
# Five-digit host ports exposed outside the compute server.
COMPUTE_API_PORT=19100
FILE_GATEWAY_PORT=19101
COMPUTE_API_IMAGE=yg-ft-compute-api:latest
# The application server actively polls Compute API; compute server does not need reverse access.
COMPUTE_SERVICE_TOKEN=change_me
ENABLE_APP_CALLBACK=false
# LLaMA-Factory is provided by the official hiyouga/llamafactory base image.
LLAMA_FACTORY_HOME=/app/LLaMA-Factory
YG_FT_DATA_ROOT=/data/yg-ft
YG_FT_DATA_ROOT_HOST=/data/yg-ft
LOG_DIR=/opt/yg-ft/logs/compute
CUDA_VISIBLE_DEVICES=all
NVIDIA_VISIBLE_DEVICES=all
NVIDIA_DRIVER_CAPABILITIES=compute,utility

View File

@@ -0,0 +1,27 @@
FROM hiyouga/llamafactory:latest
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
COPY compute/requirements.txt /tmp/requirements.txt
RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& rm -f /tmp/requirements.txt
RUN mkdir -p /opt/yg-ft/logs/compute /opt/yg-ft/logs/training /data/yg-ft /app/LLaMA-Factory \
&& chmod -R 0775 /opt/yg-ft /data/yg-ft /app/LLaMA-Factory
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 9100
CMD ["uvicorn", "compute.api.main:app", "--host", "0.0.0.0", "--port", "9100"]

View File

@@ -0,0 +1,39 @@
services:
compute-api:
image: ${COMPUTE_API_IMAGE:-yg-ft-compute-api:latest}
container_name: yg-ft-compute-api
gpus: all
ports:
- "${COMPUTE_API_PORT:-19100}:9100"
environment:
COMPUTE_ENV: ${COMPUTE_ENV:-prod}
COMPUTE_HOST_ID: ${COMPUTE_HOST_ID:-gpu-node-01}
COMPUTE_EXECUTION_MODE: ${COMPUTE_EXECUTION_MODE:-real}
MODELTF_ROUTE_PREFIX: ${MODELTF_ROUTE_PREFIX:-/modelTF}
COMPUTE_SERVICE_TOKEN: ${COMPUTE_SERVICE_TOKEN:-change_me}
ENABLE_APP_CALLBACK: ${ENABLE_APP_CALLBACK:-false}
LLAMA_FACTORY_HOME: ${LLAMA_FACTORY_HOME:-/app/LLaMA-Factory}
YG_FT_DATA_ROOT: ${YG_FT_DATA_ROOT:-/data/yg-ft}
LOG_DIR: ${LOG_DIR:-/opt/yg-ft/logs/compute}
CUDA_VISIBLE_DEVICES: ${CUDA_VISIBLE_DEVICES:-all}
NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}
NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-compute,utility}
PYTHONPATH: /app
volumes:
- ../../compute:/app/compute:ro
- ${YG_FT_DATA_ROOT_HOST:-/data/yg-ft}:${YG_FT_DATA_ROOT:-/data/yg-ft}
- ../../runtime/compute/logs:/opt/yg-ft/logs/compute
- ../../runtime/compute/training-logs:/opt/yg-ft/logs/training
networks:
- yg-ft-compute
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:9100/modelTF/health', timeout=3).read()\""]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
restart: unless-stopped
networks:
yg-ft-compute:
name: yg-ft-compute

View File

@@ -0,0 +1,30 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 200m;
location / {
try_files $uri $uri/ /index.html;
}
location /modelTF {
proxy_pass ${BACKEND_PROXY_PASS};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
try_files $uri =404;
expires 30d;
add_header Cache-Control "public, immutable";
}
}

1161
docs/backend-api-design.md Normal file

File diff suppressed because it is too large Load Diff

109
docs/backend-logging.md Normal file
View File

@@ -0,0 +1,109 @@
# 后端日志模块说明
本文档对应页面/功能模块:全平台通用能力、系统设置、审计中心、任务详情、训练任务日志、运维监控。
## 设计目标
- 后端服务统一使用 `backend/app/core/logging.py` 初始化日志。
- 日志文件按日期命名,单个文件超过 20MB 自动滚动。
- 日志只保留最近 10 天,过期文件自动清理。
- 业务日志使用 JSON Lines 格式,便于 Filebeat、Vector、Logstash、ELK、OpenSearch 等日志平台采集。
- `ERROR` 及以上日志独立写入错误日志文件,便于告警与问题定位。
- 日志字段必须包含代码文件、行号、函数、日志内容、请求 ID、进程和线程信息。
## 文件命名
默认日志目录由 `LOG_DIR` 控制,本地默认是 `./logs`
```text
logs/
backend-2026-07-16.log # INFO/ERROR 等全部应用日志JSON Lines
backend-2026-07-16.1.log # 当天主日志超过 20MB 后滚动产生
error-2026-07-16.log # ERROR/CRITICAL 错误日志JSON Lines
error-2026-07-16.1.log # 当天错误日志超过 20MB 后滚动产生
```
## 环境变量
```env
LOG_LEVEL=INFO
LOG_DIR=./logs
LOG_FILE_PREFIX=backend
LOG_ERROR_FILE_PREFIX=error
LOG_MAX_BYTES=20971520
LOG_RETENTION_DAYS=10
```
## JSON 字段
每一行都是一个完整 JSON 对象。
```json
{
"@timestamp": "2026-07-16T13:20:10.123",
"level": "INFO",
"logger": "app.access",
"message": "request completed method=GET path=/modelTF/health status_code=200 duration_ms=3.12 client=127.0.0.1",
"module": "logging",
"function": "request_logging_middleware",
"file": "D:\\AI\\codex-code\\YG_FT\\backend\\app\\core\\logging.py",
"line": 169,
"process": 1234,
"thread": 5678,
"thread_name": "MainThread",
"request_id": "6f9d1c3c-8be0-4c8d-a5b2-18f9d41f9a0c"
}
```
异常日志会额外包含:
```json
{
"exception": "Traceback ..."
}
```
## 使用方式
业务代码中不要直接 `print`,统一使用:
```python
from app.core.logging import get_logger
logger = get_logger(__name__)
logger.info("dataset uploaded dataset_id=%s", dataset_id)
logger.warning("gpu queue is busy project_id=%s", project_id)
logger.exception("training job failed job_id=%s", job_id)
```
`logger.exception(...)` 只能在 `except` 代码块中使用,它会自动写入堆栈信息,并同时进入主日志和错误日志。
## FastAPI 接入
应用入口 `backend/app/main.py` 已完成接入:
```python
settings = get_settings()
configure_logging(settings)
setup_request_logging(app)
```
请求日志会自动生成或透传 `X-Request-ID`,并在响应头中返回同一个请求 ID方便前端、后端、算力服务、日志平台串联排障。
## ELK/日志平台采集建议
- 采集路径:`/app/logs/*.log` 或生产环境挂载后的日志目录。
- 解析方式:按行读取,每行作为 JSON 文档解析。
- 索引建议:
- 主日志:`yg-ft-backend-*`
- 错误日志:`yg-ft-backend-error-*`
- 推荐保留字段:`@timestamp``level``logger``message``file``line``function``request_id``tenant_id``project_id``job_id`
- 业务开发后续应在关键模块日志中补充 `tenant_id``project_id``job_id` 等上下文字段,便于企业审计和问题定位。
## 注意事项
- 当前日志落本地磁盘,生产环境建议把日志目录挂载到独立数据盘。
- 日志文件保留 10 天是应用侧兜底策略,企业侧长期留存应由 ELK、对象存储或归档服务承担。
- 敏感字段如 token、密码、密钥、原始用户数据内容不得写入日志。
- 算力节点和应用节点分开部署时,建议两侧都采用 JSON Lines 格式,并使用统一 `request_id/job_id` 贯穿链路。

406
docs/deployment-plan.md Normal file
View File

@@ -0,0 +1,406 @@
# 模型微调平台后期部署方案
本文档对应页面/功能模块:系统设置、算力资源、训练任务、任务详情、模型管理、数据集管理、审批中心、审计中心、运维监控。
## 1. 部署目标
平台需要支持单机多 GPU 训练、本地磁盘文件存储、LLaMA-Factory 训练框架,并预留未来接入其他训练平台的能力。部署设计需要把“应用平台”和“算力平台”边界明确拆开:
- 应用平台:面向用户、权限、项目、模型、数据集、审批、审计、任务编排和 API。
- 算力平台:面向 GPU、训练进程、训练框架、本地工作目录、训练日志和产物。
- 训练框架:当前固定 LLaMA-Factory后续通过 Engine Adapter 标准接入其他框架。
结论:算力平台和训练框架应该部署在 GPU 算力服务器上。原因是训练框架需要直接访问 GPU、CUDA、驱动、模型权重、本地数据集切片、训练工作目录和训练进程。应用平台可以与算力平台同机部署也可以独立部署但不建议在无 GPU 的应用服务器上直接运行 LLaMA-Factory。
## 2. 服务清单
| 服务 | 部署位置 | 职责 |
| --- | --- | --- |
| Nginx | 应用服务器或算力服务器 | 前端静态资源、反向代理、TLS 终止 |
| Frontend | Nginx 静态目录 | 平台控制台 |
| Backend API | 应用服务器 | FastAPI 接口、鉴权、元数据、审批、审计、任务编排 |
| Backend Worker | 应用服务器 | 异步任务、状态同步、通知、审计归档 |
| PostgreSQL | 应用服务器或独立数据库服务器 | 业务元数据、权限、审批、审计 |
| Redis | 应用服务器或独立缓存服务器 | 队列、锁、短期状态、幂等控制 |
| Compute API | GPU 算力服务器 | 只对应用平台开放的内部算力接口 |
| Compute Agent | GPU 算力服务器 | GPU 发现、资源锁定、训练进程管理 |
| File Gateway | GPU 算力服务器 | 本地文件上传、下载、离线导入、产物访问 |
| LLaMA-Factory | GPU 算力服务器 | 实际训练、评测、合并、导出 |
| 日志采集 Agent | 两侧服务器 | 采集应用日志、训练日志、系统日志 |
## 3. 目录与存储规划
建议生产环境把文件、日志、数据库数据分盘挂载:
```text
/opt/yg-ft/
app/ # 应用服务代码
compute/ # 算力服务代码
config/ # 环境配置和服务配置
logs/
backend/ # 后端 JSON Lines 日志
compute/ # 算力服务日志
training/ # 训练过程日志
data/
datasets/ # 数据集文件
models/ # 基座模型、微调模型、导出模型
jobs/ # 训练任务工作目录
artifacts/ # 评测报告、adapter、checkpoint、导出包
```
本地文件存储建议按租户、项目、资源类型分区:
```text
/data/yg-ft/
tenants/{tenant_id}/
projects/{project_id}/
datasets/{dataset_id}/
models/{model_id}/
jobs/{job_id}/
```
## 4. 方案一:所有服务部署在算力服务器
### 4.1 适用场景
- 开发联调、单机试运行、资源受限的早期上线环境。
- 小团队共用一台单机多 GPU 服务器。
- 网络隔离要求不高,部署资源有限。
### 4.2 拓扑
```mermaid
flowchart LR
U["用户浏览器"] --> N["Nginx/Frontend"]
N --> B["Backend API"]
B --> DB["PostgreSQL"]
B --> R["Redis"]
B --> C["Compute API"]
C --> A["Compute Agent"]
A --> L["LLaMA-Factory"]
A --> G["GPU/CUDA"]
A --> FS["本地磁盘文件存储"]
```
### 4.3 部署方式
同一台 GPU 服务器部署:
- `frontend` 构建后由 Nginx 托管。
- `backend-api` 使用 Uvicorn/Gunicorn 或容器运行。
- `backend-worker` 独立进程运行。
- `postgres``redis` 可使用 Docker Compose 或系统服务。
- `compute-api``compute-agent``file-gateway` 与 LLaMA-Factory 在同机运行。
- 训练产物、数据集、模型和日志都放在本地数据盘。
### 4.4 优点
- 部署简单,路径共享容易。
- 上传数据、训练读取、产物归档都在本机完成I/O 链路短。
- 适合快速验证平台功能。
### 4.5 风险
- 应用服务、数据库、训练任务抢占同一台服务器资源。
- GPU 训练高负载可能影响 API 响应。
- 数据库与文件存储容灾能力弱。
- 安全边界不清晰,企业生产不推荐长期使用。
### 4.6 端口建议
| 服务 | 端口 | 暴露范围 |
| --- | --- | --- |
| Nginx | 80/443 | 用户网段 |
| Backend API | 17861 | 仅 Nginx、本机 |
| Compute API | 19100 | 仅 Backend API、本机 |
| File Gateway | 19101 | 仅 Backend API、本机 |
| PostgreSQL | 15432 | 本机或内网 |
| Redis | 16379 | 本机或内网 |
## 5. 方案二:应用服务与算力/训练服务独立部署
### 5.1 适用场景
- 企业生产环境。
- 有独立应用服务器、数据库服务器和 GPU 算力服务器。
- 需要清晰网络边界、权限边界和运维职责。
- 未来可能扩展多台 GPU 服务器或多种训练框架。
### 5.2 拓扑
```mermaid
flowchart LR
U["用户浏览器"] --> N["应用区 Nginx/Frontend"]
N --> B["应用区 Backend API"]
B --> DB["PostgreSQL"]
B --> R["Redis"]
B -- "内部 HTTPS/mTLS + 服务 Token" --> C["算力区 Compute API"]
C --> A["Compute Agent"]
A --> L["LLaMA-Factory"]
A --> G["GPU/CUDA"]
A --> FS["算力服务器本地磁盘"]
B -- "定时轮询任务状态/指标/产物索引" --> C
```
### 5.3 部署边界
应用服务器部署:
- Nginx。
- Frontend。
- Backend API。
- Backend Worker。
- PostgreSQL 或数据库连接。
- Redis 或队列连接。
- 审批、审计、系统配置、用户中心等应用能力。
GPU 算力服务器部署:
- Compute API。
- Compute Agent。
- File Gateway。
- LLaMA-Factory。
- CUDA、NVIDIA Driver、NCCL、PyTorch、训练依赖。
- 本地训练工作目录、模型目录、数据集缓存、产物目录。
### 5.4 互通方式
应用平台调用算力平台:
- 协议:内部 HTTPS REST后续可扩展 gRPC。
- 鉴权:服务间 Token生产建议 mTLS + IP 白名单。
- 幂等:训练任务提交使用 `Idempotency-Key``job_id`
- 状态同步:默认由应用平台定时轮询 Compute API拉取任务状态、指标摘要和产物索引。
- 回调策略:第一阶段关闭算力侧回调,避免算力服务器访问应用服务器,减少双向网络策略开通。
文件互通:
- 小文件:前端上传到 Backend API再由 Backend API 转发或同步到 File Gateway。
- 大文件Backend API 创建上传会话,前端通过受控地址分片上传到 File Gateway。
- 离线数据:管理员把数据放到算力服务器指定目录,应用平台登记离线导入任务。
- 产物下载:应用平台校验权限后,向 File Gateway 申请短期下载地址。
状态互通:
- Backend API 是业务状态的最终来源。
- Compute Agent 是训练进程状态的事实来源。
- Worker 定时对账,把 `queued/running/succeeded/failed/cancelled` 等状态同步回业务库。
### 5.5 优点
- 应用服务稳定性不受 GPU 训练高负载直接影响。
- 数据库和审计能力更适合纳入企业基础设施。
- 算力节点可以逐步扩展,不影响前端和应用后端。
- 安全边界更清晰,便于设置防火墙、堡垒机、服务账号和审计策略。
### 5.6 风险
- 文件传输链路比单机部署复杂。
- 需要处理跨服务器网络失败、轮询延迟、任务状态对账。
- 需要明确模型、数据集、产物在应用侧和算力侧的索引关系。
### 5.7 多算力节点部署约定
多算力节点阶段仍然按“单机多 GPU 节点”部署,每台 GPU 服务器都是一个独立算力节点。每个参与调度的节点都必须部署:
- Compute API。
- Compute Agent。
- File Gateway。
- LLaMA-Factory 宿主机目录和训练依赖。
- CUDA、NVIDIA Driver、NCCL、PyTorch。
- 本地数据盘 `/data/yg-ft`
- 本地日志和训练产物目录。
网络策略保持单向:
```text
应用服务器 -> 算力节点 A Compute API/File Gateway
应用服务器 -> 算力节点 B Compute API/File Gateway
应用服务器 -> 算力节点 C Compute API/File Gateway
```
默认不要求:
```text
算力节点 -> 应用服务器
算力节点 A -> 算力节点 B
```
多节点任务调度由应用平台统一完成。应用平台从 `compute_nodes` 读取节点地址、权重、标签、启用状态、维护状态和健康检查结果;从 `resource_replicas` 判断目标节点是否已有所需数据集/模型副本;缺失时创建 `resource_sync_jobs`,通过目标节点 File Gateway 同步资源。
调度策略:
- 默认自动调度按节点健康、标签、GPU 空闲、队列长度、节点权重和资源副本命中率排序。
- 支持管理员/高级用户手动指定节点或 GPU。
- `disabled` 节点不参与调度。
- `draining` 节点不接收新任务,但允许已有任务跑完。
- `maintenance/offline` 节点只允许查看和清理,不允许提交训练任务。
## 6. Compute API 接入标准
为预留其他训练平台,应用平台只依赖统一算力接口,不直接依赖 LLaMA-Factory 命令。
训练引擎适配器应提供:
- `validate_config(config)`:校验训练参数和模板。
- `build_command(job)`:生成训练命令或执行计划。
- `start(job)`:启动训练进程。
- `stop(job_id)`:停止训练进程。
- `status(job_id)`:查询训练状态。
- `collect_metrics(job_id)`:采集 loss、learning rate、epoch、step 等指标。
- `collect_artifacts(job_id)`:登记 checkpoint、adapter、导出模型、评测报告。
- `parse_log(line)`:解析训练日志。
第一版适配器:
```text
compute/engines/llama_factory/
```
后续其他框架:
```text
compute/engines/xtuner/
compute/engines/deepspeed_custom/
compute/engines/openrlhf/
```
## 7. 环境变量建议
应用平台:
```env
APP_ENV=prod
MODELTF_ROUTE_PREFIX=/modelTF
DATABASE_URL=postgresql+psycopg://yg_ft:***@postgres:5432/yg_ft
REDIS_URL=redis://redis:6379/0
LOG_DIR=/opt/yg-ft/logs/backend
COMPUTE_API_BASE_URL=https://compute.internal:19100
COMPUTE_SERVICE_TOKEN=***
FILE_GATEWAY_BASE_URL=https://compute.internal:19101
COMPUTE_STATUS_SYNC_MODE=polling
COMPUTE_POLL_INTERVAL_SECONDS=10
COMPUTE_POLL_BATCH_SIZE=100
```
算力平台:
```env
COMPUTE_ENV=prod
COMPUTE_HOST_ID=gpu-node-01
COMPUTE_API_PORT=19100
FILE_GATEWAY_PORT=19101
COMPUTE_SERVICE_TOKEN=***
ENABLE_APP_CALLBACK=false
LLAMA_FACTORY_HOME=/app/LLaMA-Factory
YG_FT_DATA_ROOT=/data/yg-ft
LOG_DIR=/opt/yg-ft/logs/compute
CUDA_VISIBLE_DEVICES=0,1,2,3
```
## 8. 日志与监控
应用平台:
- 采集 `backend-YYYY-MM-DD.log``error-YYYY-MM-DD.log`
-`request_id``tenant_id``project_id``job_id` 检索。
- ERROR 日志触发告警。
算力平台:
- 采集 Compute API 日志、Agent 日志、训练原始日志。
- 训练日志需要按 `job_id` 独立归档。
- 关键指标包括 GPU 利用率、显存、磁盘容量、训练队列长度、失败率。
## 9. 安全要求
- Compute API 不对公网开放。
- 应用平台和算力平台之间使用服务账号鉴权,生产建议 mTLS。
- File Gateway 下载地址必须短期有效,并绑定租户、项目、资源权限。
- 日志不得输出密码、Token、密钥、数据集原文敏感内容。
- 审计日志留存周期按租户或企业配置执行,应用日志短期留存,长期归档交给日志平台。
## 10. 部署检查清单
- PostgreSQL 已初始化当前运行脚本 `backend/app/db/sql/001_platform_runtime.sql``docs/postgres-schema.sql` 作为目标架构设计,后续通过迁移体系逐步收敛。
- Redis 可连通。
- 后端 `GET /modelTF/health` 正常。
- Compute API 健康检查正常。
- Compute Agent 能识别 GPU、显存、CUDA 版本。
- LLaMA-Factory 能在命令行完成最小训练作业。
- 应用平台能提交训练任务到 Compute API。
- 任务状态能从算力平台同步回应用平台。
- 数据集上传、离线导入、产物下载路径权限正确。
- 后端 JSON 日志可被日志平台解析。
- ERROR 日志能触发告警。
- 日志、数据集、模型、产物所在磁盘容量有监控和告警。
## 11. Docker Compose 文件规划
当前项目按应用服务器和算力服务器拆分了两套 Docker 部署文件,均采用代码外挂方式运行:
```text
docker/
app/
Dockerfile.backend # Backend API 运行时镜像,代码通过 volume 挂载到 /app
Dockerfile.frontend # Nginx 前端运行时镜像frontend/dist 通过 volume 挂载
docker-compose.yml # 应用服务器frontend、backend-api、postgres、redis
.env.example
compute/
Dockerfile.compute # CUDA + Python + Compute API 运行时镜像
docker-compose.yml # 算力服务器compute-api预留 agent/file gateway 拆分
.env.example
```
项目根目录不再保留 `Dockerfile``docker-compose.yml`,避免与拆分部署入口混淆。
应用服务器启动:
```bash
cd docker/app
cp .env.example .env
docker compose up -d
```
算力服务器启动:
```bash
cd docker/compute
cp .env.example .env
docker compose up -d
```
应用服务器与算力服务器独立部署时,需要在 `docker/app/.env` 中配置:
```env
COMPUTE_API_BASE_URL=http://<compute-server-ip>:19100
FILE_GATEWAY_BASE_URL=http://<compute-server-ip>:19101
COMPUTE_SERVICE_TOKEN=change_me
```
这些地址在当前 Docker 阶段通过环境变量动态配置。后续多算力节点阶段建议升级为数据库配置,由应用平台从 `compute_nodes` 表读取节点地址、权重、标签、健康状态和启用状态,并在“算力节点管理”页面维护。
多节点后,每台算力服务器各自进入 `docker/compute` 启动一套算力服务,并在应用平台中登记为一条 `compute_nodes` 记录:
```text
gpu-node-01 -> http://10.10.20.31:19100 / http://10.10.20.31:19101
gpu-node-02 -> http://10.10.20.32:19100 / http://10.10.20.32:19101
gpu-node-03 -> http://10.10.20.33:19100 / http://10.10.20.33:19101
```
算力服务器需要在 `docker/compute/.env` 中配置:
```env
ENABLE_APP_CALLBACK=false
COMPUTE_SERVICE_TOKEN=change_me
YG_FT_DATA_ROOT_HOST=/data/yg-ft
```
## 12. 仍需确认的问题
- 生产环境是否已有统一 ELK/OpenSearch、Filebeat/Vector 标准配置。
- PostgreSQL/Redis 开发阶段采用项目自带部署;生产阶段是否切换企业统一基础设施,以及对应 SLA 仍需确认。
- 是否需要 PostgreSQL 主备、备份恢复、审计日志长期归档的明确 SLA。
- 大文件上传是否需要断点续传、限速、病毒扫描或 DLP 检测。
- 应用服务器与算力服务器默认只开通应用侧主动访问算力侧;如后续需要实时回调,再单独评估双向网络策略。
- 多算力节点已按单机多 GPU 节点扩展设计;仍需确认是否需要节点组、租户绑定节点、同步限速和资源副本清理审批。

View File

@@ -0,0 +1,145 @@
# 当前系统主链路开发计划
> 说明:本计划描述当前正在开发的系统能力。代码、接口和 SQL 均按后续生产演进基线维护,不以一次性演示、静态 Mock 或样例数据作为开发准则。联调辅助能力必须显式配置启用,并不得成为默认运行路径。
## 1. 阶段目标
当前阶段需要完成模型微调平台的主链路工程基础:
```text
登录
-> 模型管理
-> 数据集管理
-> 创建微调任务
-> 调度算力节点与 GPU
-> 检查并同步模型/数据集资源
-> 启动训练任务
-> 轮询任务状态、GPU 占用、训练日志、loss 曲线
-> 训练完成后登记训练产物
```
该阶段是正式系统的第一批可运行能力,不再初始化业务样例数据。系统只允许初始化内置管理员/运维账号模型、数据集、算力节点、GPU、训练任务和资源副本必须通过页面、接口、算力 Agent 扫描或正式导入流程产生。
## 2. 运行模式
| 模式 | 说明 | 当前要求 |
| --- | --- | --- |
| `real` | 面向真实部署,等待 Compute API、Agent、File Gateway 和 LLaMA-Factory 执行器回写状态 | 默认模式 |
| `simulator` | 仅用于隔离联调,无真实 GPU 时临时推进任务状态、GPU 状态和训练日志 | 必须显式开启,不得用于生产基线 |
后端默认 `COMPUTE_MODE=real`。在该模式下,任务状态不再按时间自动推进,必须由后续真实算力同步逻辑更新。算力服务默认 `COMPUTE_EXECUTION_MODE=real`,真实训练执行器未完成前,创建训练作业会返回明确的未实现错误,避免误认为已经完成生产训练能力。
训练相关能力必须沉淀在 `compute/engines/` 适配层,不允许在应用平台后端直接拼接或执行训练命令。
## 3. 当前开发范围
### 3.1 应用平台后端
对应目录:
```text
backend/app/
api/v1/endpoints/platform.py
core/
db/
```
已建立能力:
- 统一 API 响应结构 `{ code, message, data }`
- PostgreSQL 运行表初始化,当前执行脚本位于 `backend/app/db/sql/001_platform_runtime.sql`
- 内置管理员账号初始化,业务数据不再自动写入样例记录。
- 登录、当前用户、用户列表与权限页面接口。
- 模型管理、训练产物列表、权重合并任务入口。
- 数据集管理、文件上传、预览、版本管理和下载。
- 微调任务创建、启动、停止、删除、进度查询、checkpoint 查询。
- 系统健康指标、系统信息、训练日志、系统日志接口。
- 算力节点、GPU、队列、资源副本、资源同步任务接口。
待继续开发:
- 接入正式 ORM/Repository/Service 分层和 Alembic 迁移。
- 将任务状态更新改为应用侧定时轮询 Compute API/File Gateway 后落库。
- 完成项目/模型/数据集级权限隔离校验。
- 完成审批流、审计留存、配额、资源申请和多租户上下文。
- 增加正式异常码、接口鉴权、中间件、幂等控制和分页规范。
### 3.2 算力平台服务
对应目录:
```text
compute/
api/main.py
agent/
engines/llama_factory/
file_gateway/
```
已建立能力:
- `/modelTF/health``/modelTF/v1/compute/health` 节点健康检查。
- LLaMA-Factory 参数校验、命令生成和训练日志指标解析。
- Compute API 作业、GPU、文件网关接口壳。
- 显式 `simulator` 模式下的内存状态机,用于隔离联调。
待继续开发:
- 真实 GPU 发现:接入 `nvidia-smi` 或 NVML。
- GPU 锁定与释放:一张 GPU 同一时间只分配给一个训练或推理任务。
- LLaMA-Factory 真实执行器:生成 YAML/命令、启动进程、停止进程、采集 PID。
- 训练日志采集:读取宿主机挂载日志文件,解析 loss、learning rate、epoch 等指标。
- Checkpoint/adapter/merged model 扫描与产物登记。
- File Gateway本地磁盘文件上传、下载、校验、导入和跨节点资源同步。
### 3.3 前端页面
已接入页面:
- `/login`:登录接口。
- `/model-manage`:模型列表、模型来源、训练产物。
- `/dataset``/dataset/:id/preview`:数据集列表、预览、版本。
- `/fine-tune``/fine-tune/create`:微调任务创建、启动、状态轮询。
- `/training-log/:id`:训练日志和 loss 曲线。
- `/hardware`:平台 GPU 与系统性能。
- `/compute`算力节点、GPU、队列、资源副本。
前端 Mock 默认关闭。仅在隔离前端开发时可设置 `VITE_ENABLE_MOCK=true`,真实联调和后续生产演进均以 `/modelTF` 后端接口为准。
待继续开发:
- 补齐多租户、项目管理、审批中心、审计中心、配额管理页面。
- 完成算力节点管理表单,包括节点地址、权重、标签、启用状态和健康检查结果。
- 完成模型/数据集导入页面,支持本地路径扫描和归属项目选择。
- 推理服务页面需接入真实后端任务接口,移除页面内本地假对话路径。
### 3.4 数据库
当前运行 SQL
```text
backend/app/db/sql/001_platform_runtime.sql
```
架构目标 SQL
```text
docs/postgres-schema.sql
```
当前运行 SQL 用于支持已开发接口落库;架构目标 SQL 包含用户中心、多租户、项目隔离、审批、审计、配额、评测等完整模型。后续需要通过 Alembic 将二者收敛为统一迁移体系,生产升级只走迁移脚本,不依赖手工改表。
## 4. 验收标准
- 启动后端必须连接 PostgreSQL不允许回退到 SQLite。
- 后端启动只初始化系统内置账号不初始化模型、数据集、算力节点、GPU、训练任务等业务样例数据。
- 前端默认请求真实 `/modelTF` 接口,除非显式设置 `VITE_ENABLE_MOCK=true`
- 默认 `real` 模式下任务状态不自动伪造完成,必须等待真实算力同步。
- 显式 `simulator` 模式只能用于隔离联调,部署文档必须标注不得用于生产。
- 登录后可以进入主界面,并可通过页面/API 创建真实业务记录。
- 代码、接口路由、配置项、数据库表名不得使用 `demo` 命名。
## 5. 后续开发计划
| 阶段 | 重点 | 说明 |
| --- | --- | --- |
| 阶段 1 | 数据库迁移体系 | 将当前运行 SQL 与架构 SQL 收敛到 Alembic 迁移 |
| 阶段 2 | 后端领域分层 | 拆分用户、模型、数据集、训练、算力、审计等模块 |
| 阶段 3 | 真实 Compute Agent | GPU 发现、资源锁定、进程管理、日志采集 |
| 阶段 4 | LLaMA-Factory 训练执行 | YAML/命令生成、进程启动/停止、checkpoint 和 adapter 扫描 |
| 阶段 5 | 企业治理 | 多租户、项目隔离、审批流、审计留存、配额和资源申请 |
| 阶段 6 | 多算力节点调度 | 基于 `compute_nodes`、标签、权重、资源副本和节点健康实现调度策略 |

View File

@@ -0,0 +1,98 @@
# 菜单与功能需求总览
> 本文根据当前前端侧边栏、路由、需求文档、接口文档、部署文档和 SQL 脚本整理。当前代码和 SQL 均按正式系统开发基线维护Mock、Simulator 只能作为显式联调能力,不作为默认开发准则。
## 1. 菜单分层
### 1.1 当前侧边栏菜单
| 一级分组 | 菜单 | 路由 | 权限码 | 当前状态 | 主要功能 |
| --- | --- | --- | --- | --- | --- |
| 服务看板 | 服务看板 | `/dashboard` | `dashboard` | 已有页面,接口需继续完善 | 总览指标、服务状态、训练统计、最近任务、健康入口 |
| 模型服务 | 模型训练 | `/fine-tune` | `fine-tune` | 已接入主链路 | 训练任务列表、创建训练、启动/停止、进度、训练日志、checkpoint |
| 模型服务 | 模型评测 | `/model-eval` | `model-eval` | 前端页面已有,后端待完整实现 | 评测任务、评测维度、样本评分、综合结果 |
| 模型服务 | 模型推理 | `/model-inference` | `model-inference` | 前端页面已有,后端待完整实现 | 推理任务、模型加载、单模型对话、模型对比入口 |
| 模型服务 | 模型管理 | `/model-manage` | `model-manage` | 已接入主链路 | 基座模型登记、本地/API 模型、训练产物、权重合并、模型导出 |
| 数据治理 | 数据集管理 | `/dataset` | `dataset` | 已接入主链路 | 数据集列表、上传、预览、在线编辑、版本、下载、删除审批入口 |
| 数据治理 | 数据处理 | `/data-process` | `data-process` | 前端页面已有,后端待完整实现 | 文档上传、切片预览、LLM 生成、结果编辑、发布数据集 |
| 其他工具 | 数据类型转换 | `/data-convert` | `data-convert` | 前端页面已有,后端待实现 | JSON/JSONL/Markdown 等格式转换任务 |
| 算力资源 | 算力节点 | `/compute` | `compute` | 已接入节点管理接口 | 节点地址、权重、标签、启用状态、GPU、队列、资源副本 |
| 系统设置 | 用户设置 | `/user-settings` | `user-settings` | 已接入基础用户接口 | 用户列表、创建用户、启停、页面权限 |
| 系统设置 | 平台性能 | `/hardware` | `hardware` | 已有接口,需接真实采集 | CPU、内存、磁盘、GPU、进程、网络监控 |
| 系统设置 | 查看日志 | `/logs` | `logs` | 已有接口,需接真实日志文件 | 后端日志、error 日志、训练日志索引、日志内容查看 |
### 1.2 当前二级和隐藏路由
| 页面 | 路由 | 归属菜单 | 说明 |
| --- | --- | --- | --- |
| 登录 | `/login` | 独立入口 | 登录后进入主界面 |
| 使用文档 | `/guide` | 独立入口 | 当前系统使用说明 |
| 创建训练任务 | `/fine-tune/create` | 模型训练 | 训练参数、模型/数据集/GPU 选择 |
| 训练日志 | `/training-log/:id` | 模型训练 | 日志、指标、checkpoint、任务概览 |
| 新建评测 | `/model-eval/create` | 模型评测 | 模型、数据集、维度、GPU 选择 |
| 评测详情 | `/model-eval/:id` | 模型评测 | 维度汇总、样本结果、人工复核预留 |
| 评测维度创建/编辑 | `/model-eval/dimension/create``/model-eval/dimension/:id/edit` | 模型评测 | 评测规则、Prompt、评分器配置 |
| 新建推理 | `/model-inference/create` | 模型推理 | 推理任务和模型加载配置 |
| 模型对话 | `/model-inference/chat/:id` | 模型推理 | 单模型对话 |
| 模型对比 | `/model-compare/chat/:id``/model-compare/result` | 模型推理 | 多模型对比和结果页 |
| 添加/编辑模型 | `/model-manage/create``/model-manage/:id/edit` | 模型管理 | 模型登记、用途、来源、路径/API 配置 |
| 合并权重 | `/model-manage/merge` | 模型管理 | LoRA/Adapter 合并任务 |
| 数据处理创建/详情 | `/data-process/create``/data-process/:id` | 数据处理 | 数据处理向导和任务详情 |
| 数据集创建/编辑/预览 | `/dataset/create``/dataset/:id/edit``/dataset/:id/preview` | 数据集管理 | 数据集元数据、文件、版本与内容 |
| 自定义工具 | `/tools``/tools/create``/tools/:id/edit` | 规划入口 | 路由存在,当前侧边栏未展示,后续可归入“其他工具” |
| 算力子页 | `/compute/gpus``/compute/queue``/compute/nodes` | 算力节点 | 当前可作为页签或深链 |
| 创建用户/权限设置 | `/user-settings/create``/user-settings/:id/permission` | 用户设置 | 用户创建和页面权限 |
| 无权限页 | `/permission-denied` | 系统页 | 路由守卫无权限跳转 |
### 1.3 企业治理待补菜单
| 建议菜单分组 | 菜单 | 建议路由 | 优先级 | 必要性 |
| --- | --- | --- | --- | --- |
| 组织与项目 | 租户管理 | `/tenants``/tenants/:id` | P0 | 多租户隔离、配额、留存策略入口 |
| 组织与项目 | 项目空间 | `/projects``/projects/:id``/projects/:id/members` | P0 | 项目级模型/数据集/任务隔离 |
| 组织与项目 | 资源授权 | `/projects/:id/permissions` 或资源详情弹窗 | P0 | 模型/数据集/任务级 ACL |
| 治理中心 | 审批中心 | `/approvals``/approvals/:id` | P0 | 删除、发布、导出、停止他人任务等高风险动作 |
| 治理中心 | 审批设置 | `/approval-settings` | P1 | 审批模板、审批人规则、超时策略 |
| 治理中心 | 审计中心 | `/audit-logs``/login-logs``/download-logs` | P1 | 操作审计、登录审计、下载审计、导出 |
| 运维中心 | 存储管理 | `/storage` | P1 | 本地磁盘占用、临时文件、checkpoint 清理、留存 |
| 运维中心 | 训练引擎管理 | `/training-engines` | P2 | LLaMA-Factory 和后续引擎能力 schema、健康检查 |
| 模型服务 | 模型服务治理 | `/model-services``/model-services/:id` | P1 | 测试/生产服务发布、调用统计、下线审批 |
## 2. 菜单对应接口和数据库
| 菜单/模块 | 主要接口 | 当前运行 SQL | 目标 SQL |
| --- | --- | --- | --- |
| 登录、用户设置 | `/modelTF/login``/modelTF/me``/modelTF/users` | `users` | `users``login_sessions``permissions``role_permissions``user_permission_overrides` |
| 服务看板 | `/modelTF/dashboard/overview``/modelTF/health` | 复用模型/数据集/任务/算力表 | `system_metric_snapshots``web_logs`、各业务表聚合 |
| 模型管理 | `/modelTF/model-manage``/modelTF/model-manage/trained-models``/modelTF/model-manage/merge` | `models``trained_models` | `models``trained_models``storage_objects``local_import_jobs``resource_acl` |
| 数据集管理 | `/modelTF/dataset-manage``/modelTF/dataset-manage/upload/{id}``/preview``/versions` | `datasets``dataset_files` | `datasets``dataset_files``dataset_file_versions``dataset_records``storage_objects` |
| 模型训练 | `/modelTF/fine-tune``/start``/progress``/checkpoints` | `fine_tune_tasks``trained_models` | `fine_tune_tasks``fine_tune_metrics``fine_tune_checkpoints``compute_jobs``gpu_allocations` |
| 训练日志 | `/modelTF/training-log-files``/modelTF/training-log-content` | 由任务表生成索引 | 日志文件元数据、`fine_tune_metrics``audit_logs` |
| 算力节点 | `/modelTF/compute/nodes``/compute/gpus``/compute/queue``/compute/nodes/{id}/replicas` | `compute_nodes``gpus``resource_replicas``resource_sync_jobs` | `compute_nodes``gpu_devices``compute_node_engines``compute_jobs``resource_replicas``resource_sync_jobs` |
| 平台性能 | `/modelTF/system-info``/modelTF/compute/gpus` | `gpus`、任务表 | `system_metric_snapshots``gpu_devices``compute_jobs` |
| 查看日志 | `/modelTF/log-files``/modelTF/log-content``/modelTF/web-log` | 文件日志 | `web_logs``audit_logs`,大日志进入日志平台 |
| 模型评测 | `/modelTF/model-eval``/modelTF/dimension` | 当前运行 SQL 未覆盖 | `eval_tasks``eval_dimensions``eval_sample_results``eval_dimension_summaries` |
| 模型推理/对比 | `/modelTF/model-compare``/modelTF/model-chat/*` | 当前运行 SQL 未覆盖 | `inference_tasks``inference_task_models``chat_sessions``chat_messages` |
| 数据处理 | `/modelTF/data-process/*` | 当前运行 SQL 未覆盖 | `data_process_tasks``data_process_source_files``data_process_preview_items``data_process_results` |
| 数据转换/自定义工具 | `/modelTF/data-convert/jobs``/modelTF/tools` | 当前运行 SQL 未覆盖 | `data_convert_jobs``custom_tools` |
| 租户/项目/资源授权 | `/modelTF/tenants``/modelTF/projects``/modelTF/resources/{type}/{id}/acl` | 当前运行 SQL 未覆盖 | `tenants``tenant_users``projects``project_members``resource_acl` |
| 审批/审计/留存/配额 | `/modelTF/approvals``/modelTF/audit-logs``/modelTF/retention-policies``/modelTF/quotas/usage` | 当前运行 SQL 未覆盖 | `approval_templates``approval_instances``approval_steps``audit_logs``retention_policies``quotas``quota_usage` |
## 3. 文档和脚本检查结论
| 对象 | 当前结论 | 本次补充 |
| --- | --- | --- |
| 需求文档 | `docs/platform-architecture-requirements.md``docs/system-development-plan.md` 已覆盖多租户、项目隔离、审批、审计、多算力节点、应用/算力分离部署;缺少一份按当前菜单组织的总览 | 新增本文作为菜单和功能需求总览 |
| 接口文档 | `docs/backend-api-design.md` 已统一 `/modelTF`,并已有页面/接口映射;需要明确引用菜单总览,避免开发只看接口不看页面入口 | 在接口文档增加菜单总览引用 |
| 开发计划 | `docs/system-development-plan.md` 已按工作包列出页面、接口和 DB需要把本文作为任务认领入口 | 在开发计划增加菜单总览引用 |
| 部署文档 | `docs/deployment-plan.md``docker/README.md` 已覆盖应用/算力分离、单机多 GPU、本地磁盘、真实模式默认、Docker 拆分 | 暂无新增部署配置要求 |
| 目标 SQL | `docs/postgres-schema.sql` 覆盖完整目标模型,包含用户、权限、多租户、项目、审批、审计、模型、数据集、训练、评测、推理、算力、存储、导入、服务治理 | 暂不需要新增目标表 |
| 当前运行 SQL | `backend/app/db/sql/001_platform_runtime.sql` 只覆盖已接入运行接口的最小表集 | 后续每实现一个 P0/P1 菜单模块,应同步补运行 SQL 或迁移脚本;不能再以样例数据补功能 |
## 4. 后续补充原则
- 新增侧边栏菜单时必须同步补齐路由、权限码、接口文档、DB 表/迁移、审计动作、部署依赖。
- 新增后端接口时,必须在 `docs/backend-api-design.md` 标注对应页面/功能模块。
- 新增表结构时,目标模型写入 `docs/postgres-schema.sql`,当前可执行落库写入 `backend/app/db/sql/` 或 Alembic 迁移。
- 与训练、评测、推理、数据处理相关的异步任务必须落库,不能依赖前端本地状态。
- 与算力相关的功能默认走真实模式Simulator 只能显式开启用于隔离联调。

View File

@@ -0,0 +1,772 @@
# 模型训练平台架构与功能需求设计
> 本文基于当前前端页面、已有接口/数据库设计,以及用户补充的 6 条约束进行补全。目标是把平台从“单前端原型 + 后端接口草案”扩展为企业可落地的模型训练平台方案。
## 1. 补充需求结论
### 1.1 已确认约束
1. 部署形态:单机多 GPU。
2. 文件存储:本地磁盘。
3. 训练框架:当前固定 LLaMA-Factory但要预留其他训练平台接入标准。
4. 权限粒度:需要到项目、模型、数据集级隔离。
5. 企业治理:需要多租户、审批流、审计留存周期。
6. 部署边界:算力平台与应用平台分开部署。
### 1.2 对整体设计的影响
- 不能只做页面级 RBAC需要引入“租户 -> 项目 -> 资源 -> 成员/角色/ACL”的资源权限模型。
- 单机多 GPU 不等于简单指定 GPU ID需要有 GPU 资源池、锁定、排队、抢占策略和异常释放机制。
- 本地磁盘存储与应用/算力分离存在天然冲突:文件不能只存应用服务器本地,也不能让应用直接读算力服务器目录。建议将训练文件、模型文件、日志文件统一落在算力节点本地磁盘,由算力平台提供文件网关 API应用平台只保存元数据和访问路径。
- LLaMA-Factory 应作为第一个训练引擎插件,而不是写死到业务流程里。后续接入其他平台时只需实现同一套训练引擎协议。
- 审批流、审计和保留策略必须从第一版进入数据模型和接口,否则后期补会牵动大量资源表。
## 2. 总体架构设计
### 2.1 逻辑分层
```mermaid
flowchart LR
U["用户浏览器"] --> APP["应用平台 Web / FastAPI"]
APP --> DB["PostgreSQL"]
APP --> REDIS["Redis / 任务队列"]
APP --> CPAPI["算力平台 API"]
CPAPI --> AGENT["算力节点 Agent"]
AGENT --> GPU["单机多 GPU"]
AGENT --> DISK["本地磁盘工作区"]
AGENT --> LF["LLaMA-Factory 引擎"]
```
### 2.2 应用平台职责
应用平台负责业务编排和企业治理,不直接执行训练命令:
- 用户、租户、项目、权限、审批、审计。
- 模型、数据集、任务、评测、推理任务元数据。
- 前端 API、OpenAPI、统一认证、统一响应。
- 训练/评测/数据处理任务创建、状态查询、审批校验。
- 与算力平台通信,下发任务、查询进度、拉取日志、下载产物。
建议部署组件:
- Nginx静态前端与反向代理。
- FastAPI业务 API。
- PostgreSQL业务元数据。
- Redis缓存、任务队列、SSE 状态缓存、分布式锁。
- Worker审批通知、审计归档、周期清理、异步导出。
### 2.3 算力平台职责
算力平台负责“实际占用 GPU 和磁盘”的事情:
- GPU 发现、资源上报、锁定、释放。
- 本地磁盘工作区管理。
- 数据集文件接收、校验、解压、版本目录管理。
- LLaMA-Factory 命令生成、执行、停止、日志采集。
- 训练产物、LoRA adapter、merged model、checkpoint 管理。
- 推理服务进程管理、端口分配、健康检查。
- 系统/GPU 监控指标采集。
建议部署组件:
- Compute API只暴露给应用平台访问。
- Compute Agent本机服务具备启动/停止进程权限。
- 本地文件网关:上传、下载、预览、断点续传、文件校验。
- Engine AdapterLLaMA-Factory 适配器,后续扩展其他训练引擎。
### 2.4 应用与算力平台通信
建议统一使用内部 HTTP/gRPC API并配置服务间认证
- 应用平台调用算力平台必须携带 `X-Service-Token` 或 mTLS 证书。
- 算力平台不信任前端用户身份,只信任应用平台下发的租户、项目、任务和资源上下文。
- 第一阶段不默认启用算力侧回调,应用平台通过定时轮询 Compute API 同步任务状态;如未来启用回调,必须带签名,避免伪造状态。
核心通信接口:
| 方向 | 接口 | 说明 |
| --- | --- | --- |
| 应用 -> 算力 | `POST /modelTF/compute/jobs` | 创建训练/评测/数据处理/推理任务 |
| 应用 -> 算力 | `POST /modelTF/compute/jobs/{id}/stop` | 停止任务 |
| 应用 -> 算力 | `GET /modelTF/compute/jobs/{id}` | 查询任务状态 |
| 应用 -> 算力 | `GET /modelTF/compute/jobs/{id}/logs` | 拉取日志 |
| 应用 -> 算力 | `GET /modelTF/compute/resources/gpus` | 查询 GPU 状态 |
| 应用 -> 算力 | `POST /modelTF/compute/files/upload` | 上传文件到算力本地磁盘 |
| 应用 -> 算力 | `GET /modelTF/compute/files/{object_id}/download` | 下载文件 |
| 应用 -> 算力 | `GET /modelTF/compute/jobs?status=running` | 定时轮询任务状态、指标、产物索引 |
## 3. 本地磁盘存储设计
### 3.1 存储根目录
由于算力和应用分离,建议文件主存储放在算力节点本地磁盘:
```text
/data/ft-platform/
tenants/{tenant_id}/
projects/{project_id}/
datasets/{dataset_id}/
models/base/{model_id}/
models/trained/{trained_model_id}/
jobs/{job_id}/
input/
output/
logs/
checkpoints/
tmp/
```
应用平台数据库保存:
- `storage_type=local`
- `storage_node_id`
- `relative_path`
- `checksum_sha256`
- `byte_size`
- `tenant_id/project_id/resource_id`
### 3.2 文件访问原则
- 前端不直接访问磁盘路径。
- 应用平台生成短时下载凭证,转发或重定向到算力文件网关。
- 预览内容只读取前 N 行或指定区间,避免大文件撑爆 API。
- 大文件上传必须支持分片上传、校验、断点续传。
- 删除操作采用软删除 + 延迟清理,等待审计留存和审批结果。
### 3.3 磁盘配额
配额应分三层:
- 租户配额:总容量、模型容量、数据容量、日志容量。
- 项目配额:可用容量、最大文件大小、最大任务产物保留数。
- 用户配额:可上传文件总量、并发任务产物占用。
超过配额时:
- 禁止新建任务或上传文件。
- 允许下载和清理。
- 提示可清理的 checkpoint、过期日志、失败任务临时目录。
## 4. 单机多 GPU 资源调度
第一版可以先以单个算力节点跑通主链路,但数据模型、接口和页面需要按多算力节点预留。多节点阶段仍然采用“每台 GPU 服务器 = 一个单机多 GPU 节点”的模式,不引入 Kubernetes。
### 4.1 GPU 资源模型
每张 GPU 需要记录:
- `gpu_index``uuid`、型号、显存、驱动版本。
- 当前利用率、显存占用、温度、功耗。
- 当前锁定任务、进程 PID、端口。
- 状态:`idle``reserved``running``draining``offline``error`
### 4.2 调度策略
第一版建议支持三种模式:
- 手动指定 GPU兼容当前前端选择 GPU 的模式。
- 自动选择 GPU按空闲显存、温度、任务队列选择。
- 项目配额调度:项目最多可占用 N 张 GPU避免单项目占满机器。
训练任务启动流程:
1. 应用平台校验权限、配额、审批状态。
2. 生成任务,状态为 `pending`
3. 算力平台尝试锁定 GPU。
4. 锁定成功后创建工作区并启动 LLaMA-Factory。
5. Agent 持续上报进度、日志、指标。
6. 任务完成后释放 GPU登记模型产物。
### 4.3 并发与排队
- 同一 GPU 同时只允许一个训练任务。
- 推理服务可与训练互斥,默认不允许混跑;如后续允许,需要显存保留策略。
- 数据处理如果只调用 API 模型,可以不占 GPU如果调用本地模型生成需要占用 GPU。
- 支持任务队列优先级:`low``normal``high``urgent`
- 高优任务是否可抢占低优任务,需要审批或管理员权限。
### 4.4 多算力节点升级策略
多算力节点阶段的推荐决策:
- 每个可执行训练任务的 GPU 节点都部署 `Compute API``Compute Agent``File Gateway`、LLaMA-Factory、CUDA/PyTorch 训练环境和本地数据盘。
- 应用服务器可以主动访问所有算力节点的 `Compute API/File Gateway`
- 算力节点之间默认不互相访问,不做节点间点对点同步;所有调度、状态同步和资源分发由应用平台统一编排。
- 长期坚持每台算力服务器本地磁盘,因此需要 `resource_replicas` 记录数据集、基座模型、checkpoint、adapter、导出模型在哪些节点已有本地副本。
- 调度前必须检查目标节点是否已有模型和数据集副本;缺失时由应用平台通过目标节点 File Gateway 创建同步任务,完成后再启动训练。
- 调度支持自动和手动两种模式:普通用户默认自动调度,管理员/高级用户可手动指定节点、GPU、标签或节点组。
多节点自动调度建议:
1. 过滤 `enabled = true``scheduler_status = online` 的节点。
2. 按训练引擎、GPU 型号、显存、节点标签、租户/项目配额过滤。
3. 优先选择已存在所需模型/数据集副本的节点,减少跨节点复制。
4. 同等条件下按空闲 GPU、队列长度、节点权重和最近健康检查排序。
5. `draining` 节点不接收新任务,但允许已有任务完成。
## 5. 训练引擎接入标准
### 5.1 引擎抽象
LLaMA-Factory 是第一实现,但业务系统只依赖统一训练引擎接口:
```text
TrainingEngine
validate_config(config)
prepare_workspace(job_context)
build_command(job_context)
start(job_context)
stop(job_id)
parse_progress(log_line)
collect_artifacts(job_id)
export_model(job_id, export_config)
```
### 5.2 引擎注册信息
每个训练引擎需要声明:
- 引擎编码:`llama_factory`
- 支持任务:`SFT``DPO``CPT`
- 支持方法:`lora``qlora``full`
- 支持模型模板:`qwen``llama3` 等。
- 支持数据格式Alpaca、ShareGPT、DPO pair、pretrain text。
- 支持量化和导出格式。
- 参数 schema。
- 命令模板或启动方式。
### 5.3 LLaMA-Factory 适配要求
LLaMA-Factory 适配器负责:
- 将平台训练参数转换为 YAML/CLI 参数。
- 根据项目工作区生成数据集配置。
- 自动设置 `CUDA_VISIBLE_DEVICES`
- 解析训练日志中的 loss、epoch、learning_rate、ETA。
- 收集 checkpoint、adapter、merged model、training_args、trainer_state。
- 支持训练停止和失败恢复。
### 5.4 后续接入其他平台的标准
其他训练平台只要实现以下契约即可接入:
- 输入:模型引用、数据集引用、训练配置、资源需求、输出目录。
- 输出:任务状态、进度、日志、指标、产物清单、失败原因。
- 生命周期:`prepare``start``running``stop``complete``cleanup`
- 安全:不能越权访问其他租户/项目目录。
- 可观测:必须输出结构化事件和日志。
## 6. 多租户与项目级隔离
### 6.1 租户模型
需要新增租户管理:
- 租户名称、编码、状态。
- 租户管理员。
- 租户资源配额GPU 并发数、磁盘容量、最大项目数。
- 租户审计策略、保留周期、审批策略。
### 6.2 项目空间
所有业务资源必须归属项目:
- 数据集。
- 模型。
- 训练任务。
- 评测任务。
- 推理任务。
- 数据处理任务。
- 自定义工具。
项目字段:
- 项目名称、描述、所属租户。
- 项目管理员、成员。
- 默认资源权限。
- GPU/磁盘/任务并发配额。
- 项目状态:启用、归档、禁用。
### 6.3 资源级权限
建议采用 RBAC + ACL 混合:
- RBAC 决定用户是否能访问模块,例如能否进入模型管理。
- 项目角色决定用户是否能管理项目内资源。
- 资源 ACL 处理特殊授权,例如某个数据集只给指定成员可读。
项目角色建议:
| 角色 | 权限 |
| --- | --- |
| Project Owner | 项目设置、成员、资源、审批策略全权限 |
| Project Maintainer | 创建/编辑模型、数据集、任务,可发起发布和删除 |
| Developer | 创建训练、评测、推理、数据处理任务 |
| Reviewer | 审批、复核、查看评测结果 |
| Viewer | 只读查看 |
资源权限建议:
- `read`:查看资源。
- `write`:编辑元数据和内容。
- `execute`:用于训练/评测/推理。
- `download`:下载文件和模型。
- `delete`:删除或申请删除。
- `manage_acl`:管理资源授权。
### 6.4 数据隔离要求
- API 查询必须默认带 `tenant_id` 和用户可访问项目范围。
- 数据库所有核心资源表增加 `tenant_id``project_id`
- 本地磁盘路径包含租户和项目 ID防止路径混用。
- 算力任务上下文必须携带租户/项目Agent 只允许访问对应工作区。
- 日志、审计、下载链接也必须按租户隔离。
## 7. 企业治理设计
### 7.1 审批流
建议第一版支持可配置审批模板:
| 场景 | 是否建议审批 | 原因 |
| --- | --- | --- |
| 删除数据集 | 是 | 数据不可逆风险高 |
| 删除模型 | 是 | 影响训练/推理依赖 |
| 模型发布为可推理服务 | 是 | 影响生产资源 |
| 停止他人训练任务 | 是或管理员直通 | 影响计算成本和他人工作 |
| 导出模型 | 可配置 | 涉及资产外流 |
| 下载敏感数据集 | 可配置 | 涉及数据安全 |
| 提高 GPU 配额 | 是 | 涉及资源竞争 |
审批流能力:
- 发起审批。
- 指定审批人/审批组。
- 多级审批。
- 通过、驳回、撤回、转交。
- 审批超时提醒。
- 审批结果回写原业务动作。
### 7.2 审计留存周期
建议支持租户级配置:
- 操作审计:默认 180 天,可配置 90/180/365/永久。
- 登录审计:默认 180 天。
- 训练日志:默认 90 天。
- 系统监控:原始采样默认 30 天,聚合指标保留 1 年。
- 模型产物:默认长期保留,删除需审批。
- 临时文件:默认 7 天清理。
- 失败任务工作区:默认 14 天清理。
审计不可被普通管理员物理删除,只能由系统归档任务按策略处理。
### 7.3 安全策略
需要补充:
- API Key 加密存储和脱敏展示。
- 外部数据源密码加密存储或不落库。
- 下载链接短时有效。
- 敏感操作二次确认。
- 审批通过后动作有效期,例如 24 小时内执行。
- IP 白名单和服务间 token。
- 操作审计记录 before/after 数据。
## 8. 需要补全的页面和功能
### 8.1 租户与项目页面
当前前端缺失,建议新增:
1. 租户管理页
- 租户列表、创建、禁用、配额设置、审计策略。
- 仅平台管理员可见。
2. 项目空间页
- 项目列表、创建项目、归档项目。
- 展示项目资源概览模型数、数据集数、任务数、磁盘占用、GPU 使用。
3. 项目成员页
- 添加/移除成员。
- 设置项目角色。
- 查看成员最近操作。
4. 项目资源权限页
- 模型/数据集/任务级授权。
- 支持按用户、用户组、项目角色授权。
### 8.2 用户中心补全
前端路由已有但页面文件缺失或未完成:
- `PermissionDeniedView.vue`
- `UserSettingsView.vue`
- `UserCreateView.vue`
- `UserPermissionView.vue`
建议功能:
- 用户列表、创建、禁用、重置密码。
- 角色管理。
- 页面权限管理。
- 用户所属租户/项目。
- 用户可用 GPU/磁盘配额查看。
- 登录记录和操作审计入口。
### 8.3 审批中心
新增页面:
- 我的申请。
- 待我审批。
- 已办审批。
- 审批详情。
- 审批模板配置。
审批详情需要展示:
- 申请人、申请时间、动作类型、目标资源。
- 变更前后信息。
- 风险提示。
- 审批记录。
- 通过/驳回意见。
### 8.4 算力资源中心
当前只有硬件监控页,建议扩展为算力资源中心:
- GPU 拓扑和状态。
- GPU 当前任务占用。
- GPU 锁定/释放记录。
- 队列中的任务。
- 资源配额:租户/项目/用户维度。
- 算力节点 Agent 状态。
- 算力节点新增/编辑、连接测试、启用/禁用、维护模式。
- 节点权重、标签、训练引擎版本、LLaMA-Factory 健康状态。
- 节点本地资源副本数据集、模型、checkpoint、adapter 和导出模型缓存。
- 训练引擎健康状态。
### 8.5 文件与存储管理
新增页面:
- 存储总览。
- 租户/项目磁盘占用。
- 大文件列表。
- 临时文件清理。
- Checkpoint 管理。
- 日志保留策略。
- 文件下载审计。
### 8.6 训练引擎管理
新增页面:
- 引擎列表。
- LLaMA-Factory 版本和路径。
- 引擎能力声明。
- 参数 schema 管理。
- 引擎健康检查。
- 引擎接入文档。
### 8.7 任务队列与运行控制
新增页面:
- 全局任务队列。
- 项目任务队列。
- 任务优先级调整。
- 任务重试。
- 任务停止审批。
- 失败任务诊断。
### 8.8 模型发布与服务治理
当前推理/对比页面已有基础能力,但缺少企业化发布能力:
- 模型发布申请。
- 推理服务实例配置。
- 端口、GPU、并发、超时、最大上下文限制。
- 服务启停记录。
- 调用统计。
- 服务下线审批。
## 9. 后端模块补全
### 9.1 新增核心模块
| 模块 | 职责 |
| --- | --- |
| tenant | 租户管理、租户配额、租户策略 |
| project | 项目空间、成员、项目角色 |
| resource_acl | 模型/数据集/任务级资源授权 |
| approval | 审批模板、审批实例、审批动作 |
| quota | GPU、磁盘、任务并发配额 |
| compute_gateway | 应用平台与算力平台通信 |
| file_gateway | 本地磁盘文件上传、下载、预览 |
| engine_registry | 训练引擎注册与能力发现 |
| retention | 审计、日志、临时文件保留策略 |
### 9.2 数据模型补充
在前一版 SQL 基础上,应新增或调整:
- `tenants`
- `tenant_users`
- `projects`
- `project_members`
- `resource_acl`
- `approval_templates`
- `approval_instances`
- `approval_steps`
- `quotas`
- `quota_usage`
- `compute_nodes`
- `gpu_devices`
- `gpu_allocations`
- `compute_jobs`
- `training_engines`
- `retention_policies`
核心资源表需要补充字段:
- `tenant_id`
- `project_id`
- `visibility`
- `owner_id`
- `approval_status`
- `storage_node_id`
需要调整的已有表:
- `models`
- `trained_models`
- `datasets`
- `dataset_files`
- `data_process_tasks`
- `fine_tune_tasks`
- `eval_tasks`
- `inference_tasks`
- `custom_tools`
- `audit_logs`
- `storage_objects`
### 9.3 接口补充
租户:
- `GET /modelTF/tenants`
- `POST /modelTF/tenants`
- `GET /modelTF/tenants/{id}`
- `PUT /modelTF/tenants/{id}`
- `PUT /modelTF/tenants/{id}/quota`
- `PUT /modelTF/tenants/{id}/retention-policy`
项目:
- `GET /modelTF/projects`
- `POST /modelTF/projects`
- `GET /modelTF/projects/{id}`
- `PUT /modelTF/projects/{id}`
- `POST /modelTF/projects/{id}/archive`
- `GET /modelTF/projects/{id}/members`
- `POST /modelTF/projects/{id}/members`
- `PUT /modelTF/projects/{id}/members/{user_id}`
- `DELETE /modelTF/projects/{id}/members/{user_id}`
资源授权:
- `GET /modelTF/resources/{resource_type}/{resource_id}/acl`
- `PUT /modelTF/resources/{resource_type}/{resource_id}/acl`
- `POST /modelTF/resources/{resource_type}/{resource_id}/share`
审批:
- `GET /modelTF/approvals`
- `POST /modelTF/approvals`
- `GET /modelTF/approvals/{id}`
- `POST /modelTF/approvals/{id}/approve`
- `POST /modelTF/approvals/{id}/reject`
- `POST /modelTF/approvals/{id}/cancel`
算力:
- `GET /modelTF/compute/nodes`
- `GET /modelTF/compute/gpus`
- `GET /modelTF/compute/queue`
- `POST /modelTF/compute/jobs/{id}/retry`
- `POST /modelTF/compute/jobs/{id}/priority`
训练引擎:
- `GET /modelTF/training-engines`
- `GET /modelTF/training-engines/{id}`
- `POST /modelTF/training-engines/{id}/health-check`
- `GET /modelTF/training-engines/{id}/schema`
## 10. 端到端业务流程
### 10.1 数据集上传
1. 用户进入项目空间。
2. 用户创建数据集。
3. 应用平台校验项目写权限和磁盘配额。
4. 前端上传文件到应用平台。
5. 应用平台转发到算力文件网关,保存到项目目录。
6. 算力平台返回文件元数据和 checksum。
7. 应用平台登记数据集文件版本。
8. 审计记录上传行为。
### 10.2 微调训练
1. 用户选择项目、模型、数据集、训练参数和 GPU。
2. 应用平台检查模型/数据集 `execute` 权限。
3. 检查项目 GPU 并发配额。
4. 如果策略要求审批,先创建审批单。
5. 审批通过后创建 compute job。
6. 算力平台锁定 GPU生成 LLaMA-Factory 配置,启动训练。
7. 训练日志和指标实时回传。
8. 完成后登记 trained model。
9. 如启用自动合并,进入合并任务。
10. 审计记录任务全生命周期。
### 10.3 模型发布
1. 用户选择训练产物。
2. 提交发布申请。
3. 审批通过后算力平台启动推理服务。
4. 分配端口和 GPU。
5. 应用平台登记服务实例。
6. 前端推理页面调用服务。
7. 监控调用量、延迟、错误率、GPU 占用。
## 11. 当前功能完整性评估
补充 6 条需求后,平台设计已经覆盖完整模型训练平台的主链路:
- 数据准备。
- 数据处理。
- 模型登记。
- 微调训练。
- 训练日志和指标。
- 模型合并和导出。
- 模型评测。
- 推理服务。
- 模型对比。
- 用户、权限、租户、项目隔离。
- 审批、审计、保留策略。
- 算力资源调度。
但如果目标是企业级生产平台,还建议继续确认以下缺口。
## 12. 仍需确认的问题
1. 本地磁盘是否在算力服务器上,应用服务器是否完全不保存训练文件?如果应用服务器也要保存上传临时文件,需要确认临时文件保留周期和容量。
2. 单机多 GPU 是否需要支持 MIG、GPU 分片或多进程共享,还是一张 GPU 同一时间只给一个任务?
3. 是否允许训练任务抢占?高优先级任务是否能停止低优先级任务?
4. 是否需要离线导入已有模型和已有数据集目录,还是所有文件都必须从平台上传?
5. 模型发布是否区分“测试服务”和“生产服务”?生产发布是否必须审批?
6. 是否需要数据集脱敏、敏感字段识别和数据质量评分作为内置流程?
7. 是否需要人工评测/人工复核结果沉淀为新数据集?
8. 是否需要训练任务失败后的断点续训?
9. 是否需要 checkpoint 自动清理策略,例如只保留最近 N 个或最好 N 个?
10. 是否需要对外提供标准 API 给其他系统调用训练、评测、推理能力?
11. 是否需要接入企业统一身份认证,例如 LDAP、OIDC、企业微信、钉钉
12. 是否需要成本核算:按租户/项目统计 GPU 小时、磁盘占用、模型调用量?
13. 多算力节点是否需要节点组、租户绑定节点或项目绑定节点策略?
14. 跨节点资源同步是否需要限速、同步窗口和管理员审批?
## 13. 推荐决策补充
`system-development-plan.md` 已对上述问题给出第一版建议,需求设计以以下决策为准。
1. 本地磁盘主存储放在算力服务器,应用服务器只保留上传临时文件。临时文件默认保留 24 小时,成功转发到算力文件网关后可立即进入清理队列。
2. 第一版不支持 MIG、GPU 分片和多任务共享同一张 GPU。一张 GPU 同一时间只分配给一个训练任务或一个推理服务。GPU 数据模型预留 `partition_type``parent_gpu_uuid``memory_total_mb`,便于后续扩展 MIG。
3. 第一版不做自动抢占。支持任务优先级和排队;停止他人任务需要审批或平台管理员权限。
4. 多算力节点仍按“单机多 GPU 节点”管理,每个节点独立部署算力服务和 LLaMA-Factory节点之间不互相访问由应用平台统一调度和资源同步。
5. 多节点调度默认自动选择节点,同时支持管理员/高级用户手动指定节点;调度优先考虑节点健康、标签、权重、空闲 GPU、队列长度和资源副本是否已存在。
6. 第一版必须支持离线导入已有模型和数据集目录。导入由算力 Agent 扫描、校验、登记,并归属指定租户和项目。
7. 模型发布区分测试服务和生产服务。测试服务项目内可启动并默认限流;生产服务必须审批。
8. 数据脱敏和数据质量评分作为数据处理模块的一等能力进入第一期,先实现规则版脱敏、格式校验、重复率、完整性、长度分布等指标。
9. 人工评测/复核作为第二期功能,但第一期需在数据库和页面入口预留人工复核状态与修订字段。
10. 第一版支持从 checkpoint 手动恢复训练,不做自动失败续训。失败任务可选择 checkpoint 重试。
11. 第一版必须支持 checkpoint 自动清理策略:默认保留最近 3 个、最优 2 个;已发布模型关联 checkpoint 不自动删除;失败任务 checkpoint 默认保留 14 天。
12. 第一版提供内部 API第二期再开放面向其他系统的标准 API、API Key、限流和 Webhook。
13. 第一版使用本地账号,预留 OIDC/LDAP 字段和认证 provider 抽象;第二期接入企业统一身份认证。
14. 第一版做 GPU 小时、磁盘占用、任务时长、推理调用量等用量统计;第二期再做成本单价和账单核算。
以上决策需要同步反映在接口文档、数据库 SQL、前端页面和部署方案中。第一版实现不再阻塞于这些问题的反复确认除非实际部署环境与假设明显冲突。
## 14. 页面功能模块映射
本节用于帮助前端、后端、DB 和测试人员理解需求对应到哪些页面与功能模块。页面路径以当前 Vue 路由和新增规划路由为准。
### 14.1 平台入口与全局能力
| 页面模块 | 路由/入口 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 登录页 | `/login` | 用户认证、本地账号、后续预留 OIDC/LDAP | 登录、会话创建、权限加载 |
| 主布局 | `/` | 全局项目上下文、权限控制 | 菜单、顶部状态、项目切换器、用户信息 |
| 无权限页 | `/permission-denied` | 页面权限和资源权限兜底 | 展示无权限原因、返回可访问页面 |
| 服务看板 | `/dashboard` | 平台运行总览 | 服务健康、任务概览、训练统计、用户操作分布 |
### 14.2 租户、项目与权限治理
| 页面模块 | 建议路由 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 租户管理 | `/tenants``/tenants/:id` | 多租户、租户配额、留存策略 | 租户列表、创建/禁用租户、配额、审计留存策略 |
| 项目空间 | `/projects``/projects/:id` | 项目级隔离、项目资源聚合 | 项目列表、项目概览、资源统计、项目归档 |
| 项目成员 | `/projects/:id/members` | 项目角色 | 添加成员、移除成员、设置 owner/maintainer/developer/reviewer/viewer |
| 资源授权 | `/projects/:id/permissions` 或资源详情弹窗 | 模型/数据集/任务级 ACL | 按用户、项目角色授权 read/write/execute/download/delete/manage_acl |
| 用户中心 | `/user-settings``/user-settings/create``/user-settings/:id/permission` | 用户、角色、页面权限 | 用户列表、创建用户、禁用、重置密码、分配页面权限和项目 |
### 14.3 数据链路
| 页面模块 | 路由/入口 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 数据集列表 | `/dataset` | 数据集管理、项目隔离 | 列表、搜索、下载、删除审批入口 |
| 数据集创建/编辑 | `/dataset/create``/dataset/:id/edit` | 上传、本地磁盘、文件网关 | 创建数据集、上传文件、离线导入、元数据编辑 |
| 数据集预览 | `/dataset/:id/preview` | 文件版本、在线编辑、乐观锁 | 文件预览、版本切换、保存新版本、下载 |
| 数据处理列表 | `/data-process` | 数据处理任务管理 | 任务列表、状态、输出数据集跳转、删除审批 |
| 数据处理创建向导 | `/data-process/create` | 清洗、切片、生成、质量评分、脱敏 | 任务配置、模型选择、源文件/外部源、预览切片、生成、结果编辑、发布数据集 |
| 数据处理详情 | `/data-process/:id` | 处理统计和结果追踪 | 运行信息、处理统计、失败原因、结果明细 |
| 数据转换 | `/data-convert` | JSON/JSONL 转换 | 上传 JSON、转换任务、下载结果 |
### 14.4 模型训练链路
| 页面模块 | 路由/入口 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 模型管理 | `/model-manage` | 模型登记、离线导入、资源 ACL | 基座模型/API 模型列表、用途变更、授权、删除审批 |
| 模型创建/编辑 | `/model-manage/create``/model-manage/:id/edit` | 本地模型/API 模型登记 | 选择本地路径、填写 API 模型信息、加密 API Key |
| 权重合并 | `/model-manage/merge` | LoRA 合并、产物管理 | 选择训练产物、合并权重、生成 merged model |
| 微调任务列表 | `/fine-tune` | 训练任务管理 | 任务列表、状态、进度、停止/删除审批 |
| 微调创建 | `/fine-tune/create` | LLaMA-Factory 训练配置、GPU 调度 | 选择模型/数据集/GPU、训练参数、量化导出、提交审批或启动 |
| 训练日志详情 | `/training-log/:id` | 日志、指标、checkpoint、恢复训练 | 日志 tail、loss 曲线、GPU 状态、checkpoint 列表、恢复/重试 |
| 训练引擎管理 | `/training-engines` | LLaMA-Factory 插件化和后续引擎接入 | 引擎列表、能力声明、schema、健康检查 |
### 14.5 评测、推理和发布
| 页面模块 | 路由/入口 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 评测列表 | `/model-eval` | 模型评测 | 评测任务列表、状态、分数、删除 |
| 评测创建 | `/model-eval/create` | 自动评测、LLM Judge、基础指标 | 选择模型/数据集/维度/GPU、配置指标、启动评测 |
| 评测详情 | `/model-eval/:id` | 样本级结果、人工复核预留 | 综合评价、维度汇总、样本评分、错误类型 |
| 评测维度 | `/model-eval/dimension/:id/edit` | 维度和评测 Prompt 管理 | 创建/编辑维度、评分范围、Prompt、启用状态 |
| 推理列表 | `/model-inference` | 测试推理服务 | 推理任务列表、加载/卸载、服务状态 |
| 推理创建 | `/model-inference/create` | 模型服务资源申请 | 选择模型、GPU、端口策略、并发参数 |
| 推理对话 | `/model-inference/chat/:id` | 模型对话 | 单模型流式对话、会话记录 |
| 模型对比 | `/model-compare/chat/:id``/model-compare/result` | 多模型对比 | 多模型加载、并行对话、对比结果 |
| 模型发布治理 | `/model-services``/model-services/:id` | 测试/生产服务、发布审批 | 测试服务启动、生产发布申请、调用统计、下线审批 |
### 14.6 算力、审批、审计和运维
| 页面模块 | 建议路由 | 对应需求 | 主要功能 |
| --- | --- | --- | --- |
| 算力资源中心 | `/compute``/compute/gpus` | 单机多 GPU、资源锁定、队列 | GPU 卡片、任务占用、节点状态、队列、优先级 |
| 存储管理 | `/storage` | 本地磁盘、配额、清理 | 租户/项目占用、大文件、临时文件、checkpoint 清理 |
| 审批中心 | `/approvals``/approvals/pending``/approvals/mine``/approvals/:id` | 审批流 | 我的申请、待我审批、审批详情、通过/驳回/撤回 |
| 审批模板 | `/approval-settings` | 审批策略配置 | 按动作配置审批人、超时、风险级别 |
| 审计中心 | `/audit-logs``/login-logs``/download-logs` | 操作审计和留存 | 操作审计、登录审计、下载审计、筛选导出 |
| 平台性能 | `/hardware` | 系统监控 | CPU、内存、磁盘、GPU、进程 |
| 系统日志 | `/logs` | 日志查看 | 系统日志、训练日志、tail/offset 查询 |

1496
docs/postgres-schema.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,458 @@
# Data Process Create Wizard Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`/data-process/create` 实现为固定四步向导,并在第二步提供“右侧选择切片、左侧自动定位并高亮原文”的可编辑对照体验。
**Architecture:** `DataProcessCreateView.vue` 只负责向导状态、步骤切换和跨步骤数据;每个步骤拆成独立 Vue 组件。源文定位和切片生成由纯 TypeScript 模块负责,第二步组件只消费偏移范围并同步滚动、高亮和编辑状态。现有 Vue 3、Element Plus、SCSS 和 Font Awesome 继续使用,不引入新依赖。
**Tech Stack:** Vue 3.5、TypeScript 5.7、Vite 6、Element Plus 2.9、SCSS、Node.js 回归脚本、`vue-tsc`
## Global Constraints
- 顶部固定四步:`创建任务``数据预览``开始生成``结果编辑与保存`
- 结构化和非结构化类型不得改变步骤数量。
- 第二步桌面端左侧约 58% 为只读源文件,右侧约 42% 为切片或记录列表及编辑器。
- 点击右侧条目时,左侧必须定位并高亮 `sourceStart``sourceEnd` 的原始范围。
- 编辑切片不得改写源文件;来源映射始终指向初始原文。
- 每一步只能有一个主操作,不得同时出现“下一步”和“开始生成”等竞争动作。
- 页面继续使用现有全局白色页面画布,不新增整页嵌套白卡。
- 不增加第三方依赖。
---
### Task 1: 建立四步向导回归测试
**Files:**
- Create: `frontend/scripts/regression-data-process-wizard.mjs`
- Modify: `frontend/package.json`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: Vue SFC 源码、`@vue/compiler-sfc``@vue/compiler-dom`
- Produces: `npm run test:data-process-wizard`,验证固定步骤、组件边界、对照定位标记和底部唯一主操作。
- [ ] **Step 1: 写入当前实现必然失败的结构回归检查**
```js
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseSfc } from '@vue/compiler-sfc'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const viewSource = await readFile(
path.resolve(scriptDir, '../src/views/data-process/DataProcessCreateView.vue'),
'utf8',
)
const previewSource = await readFile(
path.resolve(scriptDir, '../src/views/data-process/create/PreviewCompareStep.vue'),
'utf8',
)
assert.match(viewSource, /const WIZARD_STEPS = \[/)
for (const title of ['创建任务', '数据预览', '开始生成', '结果编辑与保存']) {
assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`)
}
assert.doesNotMatch(viewSource, /all\.filter|steps\s*=\s*computed/)
assert.match(previewSource, /class="source-viewer"/)
assert.match(previewSource, /class="preview-workspace"/)
assert.match(previewSource, /scrollIntoView/)
assert.match(previewSource, /sourceStart/)
assert.match(previewSource, /sourceEnd/)
const { descriptor } = parseSfc(viewSource)
assert.ok(descriptor.template?.content.includes('TaskSetupStep'))
assert.ok(descriptor.template?.content.includes('PreviewCompareStep'))
assert.ok(descriptor.template?.content.includes('GenerationStep'))
assert.ok(descriptor.template?.content.includes('ResultEditorStep'))
console.log('数据处理四步向导回归检查通过')
```
- [ ] **Step 2: 在 `package.json` 注册命令**
```json
{
"scripts": {
"test:data-process-wizard": "node scripts/regression-data-process-wizard.mjs"
}
}
```
- [ ] **Step 3: 运行测试并确认失败原因正确**
Run: `cd frontend && npm run test:data-process-wizard`
Expected: FAIL首先因 `PreviewCompareStep.vue` 不存在或固定步骤断言不成立而失败。
### Task 2: 建立向导类型、草稿状态和来源映射模型
**Files:**
- Create: `frontend/src/views/data-process/create/types.ts`
- Create: `frontend/src/views/data-process/create/previewModel.ts`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: 上传文件解析出的字符串。
- Produces: `ProcessType``StepId``PreviewItem``ResultItem``DataProcessDraft``buildPreviewItems(sourceText, processType)``sourceLines(sourceText)`
- [ ] **Step 1: 在回归脚本增加模型文件和关键字段断言**
```js
const typesSource = await readFile(
path.resolve(scriptDir, '../src/views/data-process/create/types.ts'),
'utf8',
)
const modelSource = await readFile(
path.resolve(scriptDir, '../src/views/data-process/create/previewModel.ts'),
'utf8',
)
for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedContent']) {
assert.ok(typesSource.includes(field), `PreviewItem 缺少字段:${field}`)
}
assert.match(modelSource, /export function buildPreviewItems/)
assert.match(modelSource, /export function sourceLines/)
```
- [ ] **Step 2: 定义稳定类型**
```ts
export type ProcessType = 'structured' | 'unstructured'
export type StepId = 'create' | 'preview' | 'generate' | 'results'
export interface PreviewItem {
id: string
originalContent: string
editedContent: string
sourceStart: number | null
sourceEnd: number | null
sourceStartLine: number | null
sourceEndLine: number | null
tokenCount: number
status: 'original' | 'modified' | 'manual' | 'invalid'
}
export interface ResultItem {
id: string
instruction: string
input: string
output: string
status: 'valid' | 'modified' | 'invalid'
error?: string
}
```
- [ ] **Step 3: 实现可重复的来源偏移生成**
```ts
export function buildPreviewItems(sourceText: string, processType: ProcessType): PreviewItem[] {
const lines = sourceText.split('\n')
const groupSize = processType === 'structured' ? 1 : 3
let cursor = 0
const ranges = lines.map((line, index) => {
const start = cursor
cursor += line.length + (index < lines.length - 1 ? 1 : 0)
return { line, lineNumber: index + 1, start, end: start + line.length }
})
const items: PreviewItem[] = []
for (let index = 0; index < ranges.length; index += groupSize) {
const group = ranges.slice(index, index + groupSize)
if (!group.length || group.every((item) => !item.line.trim())) continue
const content = group.map((item) => item.line).join('\n')
items.push({
id: `preview-${items.length + 1}`,
originalContent: content,
editedContent: content,
sourceStart: group[0].start,
sourceEnd: group[group.length - 1].end,
sourceStartLine: group[0].lineNumber,
sourceEndLine: group[group.length - 1].lineNumber,
tokenCount: Math.max(1, Math.ceil(content.length / 2)),
status: 'original',
})
}
return items
}
```
- [ ] **Step 4: 运行回归检查和类型检查**
Run: `cd frontend && npm run test:data-process-wizard && npm run type-check`
Expected: 回归测试继续因组件未完成而失败;`previewModel.ts``types.ts` 不产生 TypeScript 错误。
### Task 3: 实现向导壳层和第一步创建任务
**Files:**
- Create: `frontend/src/views/data-process/create/TaskSetupStep.vue`
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: `ProcessType`、Element Plus 表单与上传组件。
- Produces: `TaskSetupStep``v-model:name``v-model:description``v-model:processType``file-change``remove-file` 事件;父页面提供固定 `WIZARD_STEPS` 和统一底部操作。
- [ ] **Step 1: 将父页面步骤定义改为不可变四步**
```ts
const WIZARD_STEPS = [
{ id: 'create', title: '创建任务', desc: '填写任务信息与上传源数据' },
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
] as const
```
- [ ] **Step 2: 创建第一步组件,保留现有校验并改为视觉选择块**
```ts
const props = defineProps<{
name: string
description: string
processType: ProcessType
file: File | null
fileCount: number
}>()
const emit = defineEmits<{
'update:name': [value: string]
'update:description': [value: string]
'update:processType': [value: ProcessType]
'file-change': [file: UploadFile]
'remove-file': []
}>()
```
- [ ] **Step 3: 在父页面统一步骤导航和底部动作文案**
```ts
const primaryActionLabel = computed(() => ({
create: '继续:数据预览',
preview: '确认预览并继续',
generate: generation.progress === 100 ? '查看生成结果' : '开始生成',
results: '保存任务',
}[currentStepId.value]))
```
- [ ] **Step 4: 运行回归检查和类型检查**
Run: `cd frontend && npm run test:data-process-wizard && npm run type-check`
Expected: 回归测试因后续三个组件缺失而失败;第一步相关代码通过类型检查。
### Task 4: 实现左右源文件与切片同步预览
**Files:**
- Create: `frontend/src/views/data-process/create/PreviewCompareStep.vue`
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: `sourceText: string``items: PreviewItem[]``selectedId: string | null``processType: ProcessType`
- Produces: `update:selectedId``update:item-content``restore:item`;选中条目变化时调用 `scrollIntoView({ block: 'center' })`
- [ ] **Step 1: 增加源文范围与选中态的结构断言**
```js
for (const marker of [
'source-viewer',
'source-line',
'is-highlighted',
'preview-item',
'preview-editor',
'scrollIntoView',
]) {
assert.ok(previewSource.includes(marker), `第二步缺少结构:${marker}`)
}
```
- [ ] **Step 2: 通过行偏移判断高亮范围**
```ts
function isLineHighlighted(lineStart: number, lineEnd: number) {
if (!selectedItem.value || selectedItem.value.sourceStart == null || selectedItem.value.sourceEnd == null) {
return false
}
return lineEnd >= selectedItem.value.sourceStart
&& lineStart <= selectedItem.value.sourceEnd
}
```
- [ ] **Step 3: 选中切片后定位首个高亮行**
```ts
watch(selectedItem, async (item) => {
if (!item || item.sourceStart == null) return
await nextTick()
sourceViewerRef.value
?.querySelector<HTMLElement>(`[data-offset="${item.sourceStart}"]`)
?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
```
- [ ] **Step 4: 编辑时只更新 `editedContent` 和状态**
```ts
function updateContent(item: PreviewItem, value: string) {
emit('update:item-content', item.id, value)
}
```
父组件处理事件时不得修改 `sourceText``sourceStart``sourceEnd`
```ts
function updatePreviewContent(id: string, value: string) {
const item = draft.previewItems.find((entry) => entry.id === id)
if (!item) return
item.editedContent = value
item.status = value === item.originalContent ? 'original' : 'modified'
draft.dirty = true
}
```
- [ ] **Step 5: 完成搜索、仅看已修改、上一片、下一片和恢复原文**
Run: `cd frontend && npm run test:data-process-wizard && npm run type-check`
Expected: 第二步结构断言通过,类型检查通过;回归测试只因第三、四步组件缺失而失败。
### Task 5: 实现生成与结果编辑两个独立步骤
**Files:**
- Create: `frontend/src/views/data-process/create/GenerationStep.vue`
- Create: `frontend/src/views/data-process/create/ResultEditorStep.vue`
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: 任务摘要、预览条目、生成状态和结果条目。
- Produces: `start``stop``retry``update:result``restore:result``save` 事件。
- [ ] **Step 1: 生成步骤只保留摘要、进度与状态**
```ts
const emit = defineEmits<{
start: []
stop: []
retry: []
}>()
```
生成完成前底部唯一主操作为 `开始生成`;生成进行中为禁用的 `正在生成`;完成后变为 `查看生成结果`
- [ ] **Step 2: 清理并托管模拟生成计时器**
```ts
let generationTimer: ReturnType<typeof setInterval> | null = null
function stopGenerationTimer() {
if (generationTimer) clearInterval(generationTimer)
generationTimer = null
}
onBeforeUnmount(stopGenerationTimer)
```
- [ ] **Step 3: 将预览条目转换为结构化结果字段**
```ts
function createResults(items: PreviewItem[]): ResultItem[] {
return items.slice(0, 12).map((item, index) => ({
id: `result-${index + 1}`,
instruction: item.editedContent.split('\n')[0] || `数据条目 ${index + 1}`,
input: '',
output: item.editedContent.split('\n').slice(1).join('\n') || item.editedContent,
status: 'valid',
}))
}
```
- [ ] **Step 4: 使用左侧结果列表和右侧字段编辑器替代原始 JSON 文本框**
```ts
function validateResult(item: ResultItem) {
item.error = item.instruction.trim() && item.output.trim() ? undefined : '指令和输出不能为空'
item.status = item.error ? 'invalid' : 'modified'
}
```
- [ ] **Step 5: 运行回归、页面表面和类型检查**
Run: `cd frontend && npm run test:data-process-wizard && npm run test:page-surface && npm run type-check`
Expected: 三项检查全部 PASS。
### Task 6: 完成视觉实现、响应式和浏览器验收
**Files:**
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
- Modify: `frontend/src/views/data-process/create/TaskSetupStep.vue`
- Modify: `frontend/src/views/data-process/create/PreviewCompareStep.vue`
- Modify: `frontend/src/views/data-process/create/GenerationStep.vue`
- Modify: `frontend/src/views/data-process/create/ResultEditorStep.vue`
- Test: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: 已完成的四步组件和现有全局页面画布。
- Produces: 与确认修订稿一致的桌面布局,以及 900px 以下的上下布局。
- [ ] **Step 1: 落实单层页面、固定步骤和底部操作栏样式**
```scss
.wizard-footer {
position: sticky;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
background: rgba(255, 255, 255, 0.98);
border-top: 1px solid #eef0f5;
}
```
- [ ] **Step 2: 落实桌面左右对照和 900px 响应式**
```scss
.preview-workspace {
display: grid;
grid-template-columns: minmax(0, 58fr) minmax(380px, 42fr);
}
@media (max-width: 900px) {
.preview-workspace {
grid-template-columns: minmax(0, 1fr);
}
}
```
- [ ] **Step 3: 启动页面并逐步验证四步交互**
Run: `cd frontend && npm run dev -- --host 0.0.0.0 --port 16801`
Browser checks at `http://localhost:16801/data-process/create`:
1. 第一步上传文本并选择非结构化数据。
2. 第二步点击至少三个右侧切片,确认左侧滚动目标和高亮范围变化。
3. 修改一个切片并切换前后条目,确认修改状态和内容保留。
4. 完成生成并进入第四步,修改结果字段并保存。
5. 返回前一步,确认草稿和选中项未丢失。
6. 以 1440×1024 和 900px 窄屏分别截图,确认无横向溢出和底部遮挡。
- [ ] **Step 4: 执行完整验证**
Run: `cd frontend && npm run test:data-process-wizard && npm run test:page-surface && npm run type-check && npm run build`
Expected: 所有回归脚本、类型检查和生产构建全部 PASS。
## Self-Review Result
- 规格中的四步稳定语义由 Tasks 1、3、5 覆盖。
- 左右对照、来源映射、滚动高亮、编辑不改源文件由 Tasks 2、4 覆盖。
- 生成状态、计时器清理、结果字段校验由 Task 5 覆盖。
- 单层白底、响应式、路由转场连续性和最终验收由 Task 6 覆盖。
- 未引入新依赖;计划中所有类型和事件名在前置任务中已有定义。

View File

@@ -0,0 +1,117 @@
# 数据处理任务状态切换移除 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 移除数据处理任务列表左上角的三个状态切换按钮,并让表格始终展示全部任务。
**Architecture:** 保持现有 `DataTablePage` 结构不变,仅删除 `DataProcessListView.vue` 内部的页签状态、派生筛选数据、标题插槽和专用样式。新增一个轻量源码回归脚本,锁定“无状态切换组件且表格直接使用完整数据源”的行为。
**Tech Stack:** Vue 3、TypeScript、Element Plus、Node.js `assert`、Vue SFC parser
---
### Task 1: 增加状态切换移除回归检查
**Files:**
- Create: `frontend/scripts/regression-data-process-list.mjs`
- Modify: `frontend/package.json`
- Test: `frontend/scripts/regression-data-process-list.mjs`
- [ ] **Step 1: 编写失败的回归检查**
```js
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { parse as parseSfc } from '@vue/compiler-sfc'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const viewPath = path.resolve(scriptDir, '../src/views/data-process/DataProcessListView.vue')
const source = await readFile(viewPath, 'utf8')
const { descriptor, errors } = parseSfc(source, { filename: viewPath })
assert.equal(errors.length, 0, `数据处理任务列表模板无法解析:${errors[0]}`)
assert.ok(descriptor.template?.content.trim(), '数据处理任务列表缺少可渲染模板')
assert.match(source, /:data="dataList"/, '任务表格必须直接展示完整任务数据')
assert.doesNotMatch(source, /activeTab|filteredDataList/, '不应保留状态切换筛选逻辑')
assert.doesNotMatch(source, /全部任务|处理中|已完成/, '不应保留状态切换按钮文案')
assert.doesNotMatch(source, /capsule-tabs|capsule-tab-item/, '不应保留状态切换专用样式')
console.log('数据处理任务列表状态切换移除回归检查通过')
```
`frontend/package.json``scripts` 中增加:
```json
"test:data-process-list": "node scripts/regression-data-process-list.mjs"
```
- [ ] **Step 2: 运行检查并确认先失败**
Run: `npm run test:data-process-list`
Expected: FAIL错误指出任务表格尚未直接使用 `dataList`,或仍存在状态切换逻辑。
- [ ] **Step 3: 提交回归检查**
```bash
git add frontend/package.json frontend/scripts/regression-data-process-list.mjs
git commit -m "test: 覆盖数据处理任务列表布局"
```
### Task 2: 移除状态切换组件和筛选逻辑
**Files:**
- Modify: `frontend/src/views/data-process/DataProcessListView.vue`
- Test: `frontend/scripts/regression-data-process-list.mjs`
- [ ] **Step 1: 实现最小改动**
将脚本导入改为仅保留 `ref`
```ts
import { ref } from 'vue'
```
删除 `activeTab``filteredDataList`,并将表格数据源改为:
```vue
<DataTablePage
title=""
:data="dataList"
searchable
:search-fields="['name']"
create-text="新建数据处理任务"
create-to="/data-process/create"
row-key="id"
:page-size="10"
>
```
同时删除整个 `#title` 插槽以及 `.capsule-tabs``.capsule-tab-item` 样式,仅保留操作按钮样式。
- [ ] **Step 2: 运行目标回归检查**
Run: `npm run test:data-process-list`
Expected: PASS输出 `数据处理任务列表状态切换移除回归检查通过`
- [ ] **Step 3: 运行前端类型检查**
Run: `npm run type-check`
Expected: PASS退出码为 `0`
- [ ] **Step 4: 检查差异和格式**
Run: `git diff --check && git diff -- frontend/src/views/data-process/DataProcessListView.vue frontend/package.json frontend/scripts/regression-data-process-list.mjs`
Expected: `git diff --check` 无输出,差异仅包含状态切换移除及对应测试。
- [ ] **Step 5: 提交实现**
```bash
git add frontend/src/views/data-process/DataProcessListView.vue
git commit -m "refactor: 移除数据处理状态切换"
```

View File

@@ -0,0 +1,182 @@
# Dataset Task Mock Data Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在数据集管理页的“数据任务”页签展示 4 条任务产出 Mock 数据,并让“本地上传”与“数据任务”按来源稳定分流。
**Architecture:** 保持 `mockDatasets` 为唯一数据源,在 `DatasetItem` 上增加可选来源字段,并由列表页计算属性按来源过滤。使用一个无新增依赖的 Node 回归脚本锁定类型、Mock 数量、数据名称和页签过滤规则。
**Tech Stack:** Vue 3、TypeScript 5.7、Element Plus、Node.js 回归脚本、Vite 6
## Global Constraints
- `source` 只允许 `upload``task`,并保持可选以兼容暂未返回该字段的接口数据。
- 未携带 `source` 的数据归入“本地上传”。
- 现有 6 条 Mock 数据标记为 `upload`,新增 4 条 Mock 数据标记为 `task`
- 不新增依赖,不修改后端接口,不实现真实的数据任务关联。
- 搜索、分页、预览、下载和删除按钮保持现有行为。
---
## File Structure
- `frontend/scripts/regression-dataset-task-tab.mjs`静态回归检查验证来源类型、Mock 数据和页签过滤规则。
- `frontend/package.json`:注册 `test:dataset-task-tab` 命令。
- `frontend/src/types/index.ts`:定义 `DatasetSource` 并扩展 `DatasetItem`
- `frontend/src/mock/data.ts`:标记 6 条上传数据并新增 4 条任务数据。
- `frontend/src/views/dataset/DatasetListView.vue`:按 `source` 过滤两个页签。
### Task 1: 数据任务 Mock 数据与页签分流
**Files:**
- Create: `frontend/scripts/regression-dataset-task-tab.mjs`
- Modify: `frontend/package.json`
- Modify: `frontend/src/types/index.ts:58-78`
- Modify: `frontend/src/mock/data.ts:100-108`
- Modify: `frontend/src/views/dataset/DatasetListView.vue:16-23`
**Interfaces:**
- Consumes: `getDatasetList(): Promise<DatasetItem[]>` 与现有 `DataTablePage``data` 属性。
- Produces: `DatasetSource = 'upload' | 'task'``DatasetItem.source?: DatasetSource`,以及按来源过滤后的 `filteredDataList`
- [ ] **Step 1: 写入会失败的回归检查并注册命令**
创建 `frontend/scripts/regression-dataset-task-tab.mjs`
```js
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const typesPath = path.resolve(scriptDir, '../src/types/index.ts')
const dataPath = path.resolve(scriptDir, '../src/mock/data.ts')
const viewPath = path.resolve(scriptDir, '../src/views/dataset/DatasetListView.vue')
const [typesSource, dataSource, viewSource] = await Promise.all([
readFile(typesPath, 'utf8'),
readFile(dataPath, 'utf8'),
readFile(viewPath, 'utf8'),
])
assert.match(typesSource, /export type DatasetSource = 'upload' \| 'task'/)
assert.match(typesSource, /source\?: DatasetSource/)
assert.equal((dataSource.match(/source: 'upload'/g) || []).length, 6)
assert.equal((dataSource.match(/source: 'task'/g) || []).length, 4)
for (const name of [
'客服对话清洗集',
'通用指令构造集',
'用户反馈脱敏集',
'多轮对话增强集',
]) {
assert.ok(dataSource.includes(`name: '${name}'`), `缺少数据任务 Mock${name}`)
}
assert.match(viewSource, /item\.source === 'task'/)
assert.match(viewSource, /item\.source !== 'task'/)
assert.doesNotMatch(viewSource, /数据任务产生的数据集[\s\S]*?return \[\]/)
console.log('数据任务 Mock 数据与页签分流回归检查通过')
```
`frontend/package.json``scripts` 中加入:
```json
"test:dataset-task-tab": "node scripts/regression-dataset-task-tab.mjs"
```
- [ ] **Step 2: 运行回归检查并确认红灯**
Run: `cd frontend && npm run test:dataset-task-tab`
Expected: FAIL首个断言提示缺少 `DatasetSource`
- [ ] **Step 3: 增加来源类型**
`frontend/src/types/index.ts` 的数据集类型区加入并使用:
```ts
export type DatasetType = 'train' | 'test' | 'eval' | 'val' | 'other'
export type DatasetStorage = 'local' | 'cloud' | 'minio'
export type DatasetSource = 'upload' | 'task'
export interface DatasetItem {
id: number | string
name: string
type: DatasetType | string
storage_type: DatasetStorage | string
source?: DatasetSource
size?: string | number
count?: number
description?: string
create_time?: string
files?: DatasetFile[]
}
```
- [ ] **Step 4: 标记现有数据并添加 4 条任务数据**
`frontend/src/mock/data.ts``mockDatasets` 更新为:
```ts
export const mockDatasets: DatasetItem[] = [
{ id: 1, name: '金融问答-训练集', type: 'train', storage_type: 'local', source: 'upload', size: '128 MB', count: 8560, description: '金融领域问答对', create_time: '2025-12-20T08:00:00Z' },
{ id: 2, name: '法律文书-训练集', type: 'train', storage_type: 'local', source: 'upload', size: '256 MB', count: 15230, description: '法律文书数据集', create_time: '2025-12-25T10:30:00Z' },
{ id: 3, name: '客服对话-训练集', type: 'train', storage_type: 'minio', source: 'upload', size: '512 MB', count: 24500, description: '客服对话记录', create_time: '2026-01-05T14:20:00Z' },
{ id: 4, name: '金融评测集', type: 'eval', storage_type: 'local', source: 'upload', size: '32 MB', count: 1200, description: '金融领域评测', create_time: '2026-01-10T09:15:00Z' },
{ id: 5, name: '通用能力评测', type: 'eval', storage_type: 'local', source: 'upload', size: '64 MB', count: 3500, description: '通用能力评测数据集', create_time: '2026-01-12T11:30:00Z' },
{ id: 6, name: '医疗问答-训练集', type: 'train', storage_type: 'local', source: 'upload', size: '180 MB', count: 9800, description: '医疗问答对', create_time: '2026-02-01T15:00:00Z' },
{ id: 7, name: '客服对话清洗集', type: 'train', storage_type: 'minio', source: 'task', size: '96 MB', count: 18240, description: '由客服问答数据清洗任务生成', create_time: '2026-07-08T06:28:00Z' },
{ id: 8, name: '通用指令构造集', type: 'train', storage_type: 'local', source: 'task', size: '148 MB', count: 12600, description: '由指令微调数据构造任务生成', create_time: '2026-07-09T01:42:00Z' },
{ id: 9, name: '用户反馈脱敏集', type: 'test', storage_type: 'minio', source: 'task', size: '72 MB', count: 9340, description: '由敏感信息脱敏任务生成', create_time: '2026-07-09T09:18:00Z' },
{ id: 10, name: '多轮对话增强集', type: 'eval', storage_type: 'local', source: 'task', size: '41 MB', count: 2780, description: '由多轮对话拼接任务生成', create_time: '2026-07-10T02:06:00Z' },
]
```
- [ ] **Step 5: 实现两个页签的来源过滤**
`frontend/src/views/dataset/DatasetListView.vue``filteredDataList` 更新为:
```ts
const filteredDataList = computed(() => {
if (activeTab.value === 'task') {
return dataList.value.filter((item) => item.source === 'task')
}
return dataList.value.filter((item) => item.source !== 'task')
})
```
- [ ] **Step 6: 运行针对性回归检查并确认绿灯**
Run: `cd frontend && npm run test:dataset-task-tab`
Expected: PASS输出 `数据任务 Mock 数据与页签分流回归检查通过`
- [ ] **Step 7: 运行类型检查和生产构建**
Run: `cd frontend && npm run type-check`
Expected: PASS若仓库原有错误仍存在保存完整输出并确认本任务修改文件不在错误列表中。
Run: `cd frontend && npx vite build`
Expected: PASS并生成 `dist` 产物。
- [ ] **Step 8: 页面烟雾验证**
启动开发服务器后打开数据集管理页,验证“本地上传”总数为 6切换“数据任务”后总数为 4搜索“脱敏”只显示“用户反馈脱敏集”且预览、下载、删除按钮可见。
- [ ] **Step 9: 提交实现**
```bash
git add frontend/package.json \
frontend/scripts/regression-dataset-task-tab.mjs \
frontend/src/types/index.ts \
frontend/src/mock/data.ts \
frontend/src/views/dataset/DatasetListView.vue
git commit -m "feat: 添加数据任务 mock 数据"
```

View File

@@ -0,0 +1,126 @@
# 多文件预览下拉选择器 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在数据处理向导第二步中支持按文件切换原文和切片,同时保留现有双栏阅读空间。
**Architecture:** 为每个预览条目记录来源文件 ID父页面按当前文件筛选原文与条目。预览组件只负责可搜索下拉选择器和当前文件双栏对照不拼接不同文件的原文。
**Tech Stack:** Vue 3、TypeScript、Element Plus、SCSS、Node 回归脚本。
## Global Constraints
- 预览主区保持原文与切片的双栏比例,不新增常驻第三栏。
- 下拉选择器必须支持 100 个文件的名称筛选。
- 切换文件不得丢失其他文件已编辑的切片内容。
---
### Task 1: 锁定多文件来源映射
**Files:**
- Modify: `frontend/scripts/regression-data-process-wizard.mjs`
- Modify: `frontend/src/views/data-process/create/types.ts`
- Modify: `frontend/src/views/data-process/create/previewModel.ts`
**Interfaces:**
- Produces: `PreviewItem.sourceFileId: string`
- Produces: `buildPreviewItems(sourceText, processType, sourceFileId)` 为同一文件生成带文件归属的唯一条目。
- [ ] **Step 1: 写入失败断言**
```js
assert.match(typesSource, /sourceFileId/, 'PreviewItem 缺少来源文件标识')
assert.match(modelSource, /sourceFileId/, '切片生成没有写入来源文件标识')
```
- [ ] **Step 2: 运行失败断言**
Run: `npm run test:data-process-wizard`
Expected: FAIL提示缺少 `sourceFileId`
- [ ] **Step 3: 实现文件归属**
```ts
export interface PreviewItem {
sourceFileId: string
}
export function buildPreviewItems(sourceText: string, processType: ProcessType, sourceFileId: string) {
// 每个 item 写入 sourceFileId并以它构造稳定 ID。
}
```
- [ ] **Step 4: 再次运行回归脚本**
Run: `npm run test:data-process-wizard`
Expected: 新断言通过;仅保留已知的布局失败(如存在)。
### Task 2: 按文件驱动双栏预览
**Files:**
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
**Interfaces:**
- Consumes: `PreviewItem.sourceFileId`
- Produces: `activePreviewFile``activePreviewItems` 与当前文件选择状态。
- [ ] **Step 1: 为多文件下拉接线添加失败断言**
```js
assert.match(viewSource, /selectedPreviewFileId/, '父页面缺少当前预览文件状态')
assert.match(viewSource, /buildPreviewItems\(file\.content, processType\.value, String\(file\.uid\)\)/, '预览没有按文件分别生成')
```
- [ ] **Step 2: 运行失败断言**
Run: `npm run test:data-process-wizard`
Expected: FAIL提示缺少当前预览文件状态。
- [ ] **Step 3: 最小实现**
```ts
const activePreviewFile = computed(() => uploadedFiles.value.find((file) => String(file.uid) === selectedPreviewFileId.value))
const activePreviewItems = computed(() => previewItems.value.filter((item) => item.sourceFileId === selectedPreviewFileId.value))
```
- [ ] **Step 4: 运行回归脚本**
Run: `npm run test:data-process-wizard`
Expected: 父页面多文件断言通过。
### Task 3: 加入可搜索文件下拉框与布局修复
**Files:**
- Modify: `frontend/src/views/data-process/create/PreviewCompareStep.vue`
- Modify: `frontend/src/views/data-process/DataProcessCreateView.vue`
- Modify: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: `files``selectedFileId``items``sourceText`
- Produces: `update:selectedFileId` 事件。
- [ ] **Step 1: 添加失败断言**
```js
assert.match(previewSource, /filterable/, '文件选择器必须可搜索')
assert.match(previewSource, /当前文件/, '预览缺少当前文件切换器')
```
- [ ] **Step 2: 运行失败断言**
Run: `npm run test:data-process-wizard`
Expected: FAIL提示缺少可搜索的文件选择器。
- [ ] **Step 3: 实现下拉框与响应式样式**
```vue
<el-select filterable :model-value="selectedFileId" @update:model-value="emit('update:selectedFileId', $event)">
<el-option v-for="file in files" :key="file.id" :label="file.name" :value="file.id" />
</el-select>
```
- [ ] **Step 4: 将 `.wizard-content` 设为 `min-height: 0` 并运行验证**
Run: `npm run test:data-process-wizard && npm run type-check && npm run build`
Expected: 三个命令退出码均为 0。

View File

@@ -0,0 +1,94 @@
# Page Surface Classification Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 列表页直接使用自身白色卡片,表单和详情页继续使用主布局提供的白色圆角画布。
**Architecture:** 使用 Vue Router `meta.pageSurface` 做显式页面表面分类。主布局默认渲染白色画布,仅在 `pageSurface === 'self'` 时切换为透明、无内边距的承载容器。
**Tech Stack:** Vue 3、Vue Router 4、TypeScript、SCSS、Node.js 回归脚本
## Global Constraints
- 只给六个自带白色列表卡片的路由声明 `pageSurface: 'self'`
- 其他路由默认继续使用白色页面画布。
- 不新增依赖,不修改业务逻辑。
- 先写失败测试,再实现最小修复。
---
### Task 1: 路由级页面表面分类
**Files:**
- Modify: `frontend/scripts/regression-page-surface.mjs`
- Modify: `frontend/src/router/index.ts`
- Modify: `frontend/src/layouts/MainLayout.vue`
- Test: `frontend/scripts/regression-page-surface.mjs`
**Interfaces:**
- Consumes: Vue Router 当前路由对象的 `route.meta.pageSurface`
- Produces: `pageSurface: 'self'` 路由元数据和 `.page-canvas.is-self-surface` 布局状态。
- [ ] **Step 1: 写入失败回归测试**
`regression-page-surface.mjs` 中读取 `src/router/index.ts`,断言六个列表路由包含
`pageSurface: 'self'`;断言 `MainLayout` 使用 `useRoute()` 和动态类;断言状态样式为:
```scss
.page-canvas.is-self-surface {
padding: 0;
border-radius: 0;
background-color: transparent;
box-shadow: none;
}
```
- [ ] **Step 2: 运行测试并确认 RED**
Run: `npm run test:page-surface`
Expected: FAIL提示列表路由缺少 `pageSurface: 'self'` 或主布局缺少自表面状态。
- [ ] **Step 3: 写入最小实现**
在六个列表路由中加入:
```ts
meta: { title: '页面标题', pageSurface: 'self' },
```
`MainLayout.vue` 中使用:
```ts
const route = useRoute()
```
```vue
<div
class="page-canvas"
:class="{ 'is-self-surface': route.meta.pageSurface === 'self' }"
>
```
并加入透明承载容器样式。
- [ ] **Step 4: 运行专项测试并确认 GREEN**
Run: `npm run test:page-surface`
Expected: PASS输出“全局页面背景与内容表面回归检查通过”。
- [ ] **Step 5: 运行相关回归与生产构建**
Run: `npm run test:training-log-layout`
Expected: PASS。
Run: `npx vite build`
Expected: build exit code 0允许保留项目既有字体解析和 chunk size 警告。
- [ ] **Step 6: 浏览器视觉验证**
打开 `/fine-tune`,确认灰色背景上仅有列表自身白色卡片;打开
`/training-log/1`,确认白色圆角页面画布仍存在。两个页面均不得水平溢出,控制台不得新增错误。

View File

@@ -0,0 +1,77 @@
# 切片单面板编辑模式 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将数据预览右侧改为列表和编辑器互斥的单面板,并通过保存或取消控制切片内容写回。
**Architecture:** `PreviewCompareStep.vue` 保留切片筛选、分页和源文件定位,新增组件内编辑模式与临时草稿。父页面仅在收到保存事件时更新 `PreviewItem`,删除仍复用现有确认与删除事件。
**Tech Stack:** Vue 3 Composition API、TypeScript、Element Plus、Node 回归脚本。
## Global Constraints
- 列表行只展示编号、来源行号及编辑、删除图标操作。
- 编辑草稿未保存时不得更新 `PreviewItem.editedContent`
- 搜索、分页或切换文件时退出编辑模式并丢弃草稿。
- 不新增依赖。
---
### Task 1: 切片列表与编辑模式切换
**Files:**
- Modify: `frontend/src/views/data-process/create/PreviewCompareStep.vue`
- Modify: `frontend/scripts/regression-data-process-wizard.mjs`
**Interfaces:**
- Consumes: `PreviewItem``update:selectedId``update:item-content``remove:item`
- Produces: `openEditor(item)``closeEditor()``saveEditor()` 与列表/编辑互斥渲染。
- [ ] **Step 1: Write the failing test**
`regression-data-process-wizard.mjs` 断言组件存在 `editingItemId``editorDraft`,列表以图标按钮触发 `openEditor``remove:item`,编辑模式拥有 `保存修改``取消``返回列表`,并且列表不再含 `item-token``item-status``modifiedOnly`
- [ ] **Step 2: Run test to verify it fails**
Run: `npm run test:data-process-wizard`
Expected: FAIL提示缺少单面板编辑模式结构。
- [ ] **Step 3: Write minimal implementation**
在组件中增加以下状态和行为:
```ts
const editingItemId = ref<string | null>(null)
const editorDraft = ref('')
function openEditor(item: PreviewItem) {
editingItemId.value = item.id
editorDraft.value = item.editedContent
}
function closeEditor() {
editingItemId.value = null
editorDraft.value = ''
}
function saveEditor() {
if (!editingItem.value) return
emit('update:item-content', editingItem.value.id, editorDraft.value)
closeEditor()
}
```
列表模式只渲染编号、来源和两个无文字图标按钮;编辑模式在同一位置渲染正文输入框以及返回、取消、保存操作。搜索、翻页、文件切换调用 `closeEditor()`
- [ ] **Step 4: Run test to verify it passes**
Run: `npm run test:data-process-wizard`
Expected: `数据处理四步向导回归检查通过`
- [ ] **Step 5: Build and visually verify**
Run: `npx vite build`
Expected: Vite completes successfully. Open the second wizard step, verify the list has only the two icon operations and that cancel does not change the selected slice content while save returns to the list.

View File

@@ -0,0 +1,78 @@
# Route Transition Removal Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 移除主布局的页面级透明转场,消除列表与二级页面切换时的闪烁中间帧。
**Architecture:** `router-view` 直接渲染当前路由组件,不再包裹 Vue `transition`。页面表面仍由 `route.meta.pageSurface` 控制,因此内容和表面在同一轮渲染中同步更新。
**Tech Stack:** Vue 3、Vue Router 4、SCSS、Node.js 回归脚本
## Global Constraints
- 删除页面级透明度转场。
- 保留组件内部动画。
- 不改变路由表面分类、业务逻辑或数据加载流程。
- 先写失败测试,再实现最小修复。
---
### Task 1: 移除主布局页面级透明转场
**Files:**
- Modify: `frontend/scripts/regression-page-surface.mjs`
- Modify: `frontend/src/layouts/MainLayout.vue`
- Test: `frontend/scripts/regression-page-surface.mjs`
**Interfaces:**
- Consumes: `router-view` 提供的当前路由组件。
- Produces: 不带页面级透明度动画的同步路由内容渲染。
- [ ] **Step 1: 写入失败回归测试**
`regression-page-surface.mjs` 中断言主布局模板不包含页面级 `transition`,并断言主布局样式不包含 `.fade-enter-*``.fade-leave-*`
- [ ] **Step 2: 运行测试并确认 RED**
Run: `npm run test:page-surface`
Expected: FAIL提示主布局仍包含页面级透明转场。
- [ ] **Step 3: 写入最小实现**
将:
```vue
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
```
改为:
```vue
<component :is="Component" />
```
并删除主布局中的 `.fade-enter-active``.fade-leave-active``.fade-enter-from``.fade-leave-to` 样式。
- [ ] **Step 4: 运行专项回归并确认 GREEN**
Run: `npm run test:page-surface`
Expected: PASS输出“全局页面背景与内容表面回归检查通过”。
- [ ] **Step 5: 运行相关回归与构建**
Run: `npm run test:training-log-layout`
Expected: PASS。
Run: `npx vite build`
Expected: exit code 0允许项目既有字体解析和 chunk size 警告。
- [ ] **Step 6: 浏览器往返验证**
验证 `/fine-tune``/fine-tune/create``/fine-tune``/training-log/1` 以及二级页返回列表;页面内容与表面同步切换,无半透明旧页面、无水平溢出、无新增控制台错误。

View File

@@ -0,0 +1,202 @@
# 源数据上传紧凑文件列表 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将数据处理创建页的已上传文件从大卡片改为固定高度、可滚动且可逐项操作的紧凑列表。
**Architecture:** 只修改 `TaskSetupStep` 的模板和局部样式,继续消费现有的
`uploadedFiles` 属性并派发既有 `remove-file` 事件。使用同一组件内的标题栏和
滚动容器管理信息密度,不改变上传、格式限制或父组件数据流。
**Tech Stack:** Vue 3 `<script setup>`、TypeScript、Element Plus、SCSS、Node.js 回归脚本。
## Global Constraints
- 不新增依赖、组件、数据字段或父子组件事件。
- 上传区、格式限制、校验语义与逐项删除行为必须保持不变。
- 空状态保留大尺寸拖拽上传区与格式提示;存在至少一个文件时,改为列表标题右侧
的小型“继续上传”入口,且复用相同上传属性与文件变更事件。
- 列表默认显示最多 5 行;额外文件只能在列表内部纵向滚动。
- 文件名允许省略,但必须通过原生 `title` 保留完整文本。
- 校验状态必须同时显示图标和文字;删除按钮必须可见且可键盘操作。
- 当前工作区不是 Git 仓库,本计划不包含提交操作。
---
### Task 1: 紧凑上传文件列表
**Files:**
- Modify: `frontend/scripts/regression-data-process-wizard.mjs`
- Modify: `frontend/src/views/data-process/create/TaskSetupStep.vue:157-164,314-351`
**Interfaces:**
- Consumes: `uploadedFiles: { uid: string | number; name: string; size: number; count: number }[]`
`remove-file(uid)` 事件。
- Produces: `.uploaded-file-list-header``.uploaded-file-items` 和每条
`.uploaded-file` 紧凑行;不新增对外 TypeScript 接口。
- [ ] **Step 1: 添加会失败的页面结构回归断言**
`frontend/scripts/regression-data-process-wizard.mjs` 的第二步组件检查之后读取
`TaskSetupStep.vue`,并追加以下断言:
```js
const taskSetupPath = path.join(createDir, 'TaskSetupStep.vue')
const taskSetupSource = await readFile(taskSetupPath, 'utf8')
for (const marker of [
'uploaded-file-list-header',
'uploaded-file-items',
'已添加 {{ uploadedFiles.length }} 个文件',
':title="file.name"',
'aria-label="已上传文件列表"',
'继续上传',
'v-if="uploadedFiles.length === 0"',
]) {
assert.ok(taskSetupSource.includes(marker), `源数据文件列表缺少:${marker}`)
}
assert.match(taskSetupSource, /\.uploaded-file-items\s*\{[\s\S]*max-height:\s*240px/, '文件列表没有固定可见高度')
assert.match(taskSetupSource, /\.uploaded-file-items\s*\{[\s\S]*overflow-y:\s*auto/, '超出文件没有在列表内滚动')
assert.match(taskSetupSource, /compact-upload[\s\S]*:accept="uploadAccept"/, '继续上传没有复用格式限制')
assert.match(taskSetupSource, /compact-upload[\s\S]*on-change/, '继续上传没有复用文件变更事件')
```
- [ ] **Step 2: 运行回归脚本,确认新增断言失败**
Run: `npm --prefix frontend run test:data-process-wizard`
Expected: FAIL错误信息包含新增文件列表、列表可访问性或继续上传状态切换的缺失标记。
- [ ] **Step 3: 更新模板为带标题与滚动区域的紧凑列表**
`TaskSetupStep.vue` 中,将当前上传区改为两种互斥状态:空状态保留大尺寸拖拽
区和格式提示;存在文件时显示含继续上传入口的紧凑列表。两个上传入口均保留
`multiple``:accept="uploadAccept"``:auto-upload="false"` 和相同 `on-change` 事件。
文件图标、元信息、成功状态和原有删除事件必须保留:
```vue
<el-upload
v-if="uploadedFiles.length === 0"
class="upload-empty-state"
drag
multiple
:accept="uploadAccept"
:auto-upload="false"
:show-file-list="false"
:on-change="(file: UploadFile) => emit('file-change', file)"
>
<!-- 保留现有大尺寸上传引导和格式提示 -->
</el-upload>
<div v-else class="uploaded-file-list" aria-label="已上传文件列表">
<div class="uploaded-file-list-header">
<span>已添加 {{ uploadedFiles.length }} 个文件</span>
<el-upload
class="compact-upload"
multiple
:accept="uploadAccept"
:auto-upload="false"
:show-file-list="false"
:on-change="(file: UploadFile) => emit('file-change', file)"
>
<el-button size="small" type="primary">继续上传</el-button>
</el-upload>
</div>
<div class="uploaded-file-items">
<div v-for="file in uploadedFiles" :key="file.uid" class="uploaded-file">
<span class="file-icon"><i class="fa fa-file-text-o" /></span>
<div class="file-main">
<strong :title="file.name">{{ file.name }}</strong>
<span>{{ formatSize(file.size) }}<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template></span>
</div>
<span class="file-status"><i class="fa fa-check-circle" /> 校验通过</span>
<el-button link type="danger" @click="emit('remove-file', file.uid)">删除</el-button>
</div>
</div>
</div>
```
- [ ] **Step 4: 将现有大卡片样式改为紧凑行和内部滚动**
`TaskSetupStep.vue` 中替换现有 `.uploaded-file-list``.uploaded-file` 相关样式,
使容器、标题、滚动区和行高满足以下实现:
```scss
.uploaded-file-list {
margin-top: 20px;
overflow: hidden;
background: #fff;
border: 1px solid #dfe3ea;
border-radius: 8px;
}
.uploaded-file-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
color: #5f6878;
font-size: 12px;
background: #fbfcfe;
border-bottom: 1px solid #edf0f5;
}
.uploaded-file-items {
max-height: 240px;
overflow-y: auto;
}
.compact-upload {
flex: 0 0 auto;
}
.uploaded-file {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 8px 14px;
border-bottom: 1px solid #edf0f5;
&:last-child {
border-bottom: 0;
}
}
.file-icon {
width: 28px;
height: 28px;
font-size: 14px;
border-radius: 7px;
}
.file-main strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
保留 `.file-main` 的弹性填充与 `.file-status` 的成功色;为窄屏媒体查询加入更小的
行间距和可换行的状态文本,保证删除按钮不被文件名挤出。
- [ ] **Step 5: 运行回归脚本并执行类型检查**
Run: `npm --prefix frontend run test:data-process-wizard && npm --prefix frontend run type-check`
Expected: 两个命令均以退出码 `0` 完成,前者输出 `数据处理四步向导回归检查通过`
- [ ] **Step 6: 构建生产包,检查样式与模板编译**
Run: `npm --prefix frontend run build`
Expected: 退出码 `0`Vite 输出生产构建产物信息,无 Vue 模板或 SCSS 编译错误。
- [ ] **Step 7: 检查变更范围**
Run: `git diff -- frontend/src/views/data-process/create/TaskSetupStep.vue frontend/scripts/regression-data-process-wizard.mjs`
Expected: 当前目录不是 Git 仓库时,该命令会报告仓库缺失;改用
`diff -u <(git show HEAD:...) ...` 不可用,因此使用 `sed` 复查两个文件的目标区段,
确认没有修改上传、格式限制或父组件事件。

View File

@@ -0,0 +1,131 @@
# 源数据上传文件分页 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将紧凑源文件列表限制为每页最多 10 条,并在文件总数超过 10 时使用分页器切换文件。
**Architecture:** 分页状态只在 `TaskSetupStep` 内维护;父组件仍只提供完整的
`uploadedFiles` 数组和既有上传、删除事件。组件通过计算属性得到当前页文件,并在
文件数变化后跳转到新增文件所在的末页或将删除后的空页回退到有效页。
**Tech Stack:** Vue 3 `<script setup>`、TypeScript、Element Plus、SCSS、Node.js 回归脚本。
## Global Constraints
- 不新增依赖、组件、数据字段或父子组件事件。
- 每页固定显示最多 10 个文件;只有第 11 个文件出现分页器。
- 不再使用文件列表内部滚动承载额外文件;分页器承担跨页浏览。
- 新增文件自动切换到最后一页;删除后当前页不存在时回退到最后一个有效页。
- 空状态的大尺寸上传区、有文件时的小型继续上传入口、格式限制、校验状态和逐项
删除行为必须保持不变。
- 当前工作区不是 Git 仓库,本计划不包含提交操作。
---
### Task 1: 十条一页的文件列表
**Files:**
- Modify: `frontend/src/views/data-process/create/TaskSetupStep.vue:1-45,158-184,350-420`
- Modify: `frontend/scripts/regression-data-process-wizard.mjs:65-140`
**Interfaces:**
- Consumes: `uploadedFiles` 完整数组和既有 `file-change``remove-file` 事件。
- Produces: 组件内部的 `currentFilePage``pagedUploadedFiles` 与只在超出 10 条时显示的
Element Plus 分页器;不新增对外接口。
- [ ] **Step 1: 先添加分页回归断言**
`frontend/scripts/regression-data-process-wizard.mjs``TaskSetupStep.vue` 检查中追加:
```js
for (const marker of [
'const FILE_PAGE_SIZE = 10',
'const currentFilePage = ref(1)',
'const pagedUploadedFiles = computed',
'v-for="file in pagedUploadedFiles"',
'class="uploaded-file-pagination"',
':page-size="FILE_PAGE_SIZE"',
'uploadedFiles.length > FILE_PAGE_SIZE',
]) {
assert.ok(taskSetupSource.includes(marker), `文件分页缺少:${marker}`)
}
assert.match(taskSetupSource, /newLength > oldLength[\s\S]*currentFilePage\.value = totalPages/, '新增文件后没有跳到最后一页')
assert.match(taskSetupSource, /Math\.min\(currentFilePage\.value, totalPages\)/, '删除文件后没有回退到有效页')
assert.doesNotMatch(taskSetupSource, /\.uploaded-file-items\s*\{[\s\S]*overflow-y:\s*auto/, '文件列表仍依赖内部滚动')
```
- [ ] **Step 2: 运行回归脚本,确认新增断言失败**
Run: `npm --prefix frontend run test:data-process-wizard`
Expected: FAIL错误信息包含 `文件分页缺少const FILE_PAGE_SIZE = 10`
- [ ] **Step 3: 增加组件内分页状态与页码校正**
`TaskSetupStep.vue` 中将 Vue 导入扩展为 `computed, ref, watch`,并在
`uploadAccept` 之后添加:
```ts
const FILE_PAGE_SIZE = 10
const currentFilePage = ref(1)
const totalFilePages = computed(() => Math.max(1, Math.ceil(props.uploadedFiles.length / FILE_PAGE_SIZE)))
const pagedUploadedFiles = computed(() => {
const start = (currentFilePage.value - 1) * FILE_PAGE_SIZE
return props.uploadedFiles.slice(start, start + FILE_PAGE_SIZE)
})
watch(() => props.uploadedFiles.length, (newLength, oldLength) => {
const totalPages = Math.max(1, Math.ceil(newLength / FILE_PAGE_SIZE))
currentFilePage.value = newLength > oldLength
? totalPages
: Math.min(currentFilePage.value, totalPages)
})
```
- [ ] **Step 4: 使用当前页文件并添加紧凑分页器**
将文件行循环改为 `v-for="file in pagedUploadedFiles"`。在 `.uploaded-file-items`
之后、`</section>` 之前插入:
```vue
<el-pagination
v-if="uploadedFiles.length > FILE_PAGE_SIZE"
v-model:current-page="currentFilePage"
:page-size="FILE_PAGE_SIZE"
:total="uploadedFiles.length"
:pager-count="5"
small
background
layout="prev, pager, next"
class="uploaded-file-pagination"
/>
```
保留现有列表语义、继续上传入口、状态、删除按钮和 `aria-label`
- [ ] **Step 5: 移除列表滚动样式并添加分页器间距**
`.uploaded-file-items` 中的 `max-height``overflow-y` 删除,确保 10 条以内
由页面自然高度承载;追加:
```scss
.uploaded-file-pagination {
display: flex;
justify-content: flex-end;
padding: 10px 14px;
border-top: 1px solid #edf0f5;
}
```
在窄屏媒体查询中将分页器改为水平居中,避免与文件操作区争夺宽度。
- [ ] **Step 6: 验证回归与编译**
Run: `npm --prefix frontend run test:data-process-wizard`
Expected: 文件分页新增断言全部通过;脚本若非零,只能在既有父页面的
`.wizard-content` / `min-height: 0` 断言处终止。
Run: `cd frontend && npm exec vite build`
Expected: 退出码 `0`,无 Vue 模板或 SCSS 编译错误。

View File

@@ -0,0 +1,128 @@
# 训练日志详情页双栏改版 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将训练日志详情首屏改造成用户选定的双栏任务档案布局,同时保持原有数据与日志行为。
**Architecture:** 继续由 `TrainingLogView.vue` 负责数据加载与页面编排,仅重写首屏模板和 scoped SCSS。使用原生语义元素与 CSS Grid不新增依赖或全局组件避免影响其他详情页。
**Tech Stack:** Vue 3、TypeScript、Element Plus、SCSS、Node.js 原生断言脚本
## Global Constraints
- 保留所有 API 请求、5 秒轮询、指标解析、ECharts 和日志输出逻辑。
- 使用现有 Indigo/Slate 视觉 token不改全局主题。
- 灰色 `#f3f5f8` 只作为应用外壳留白;所有业务路由必须渲染在统一的白色圆角页面画布内。
- 缺失参数显示“未配置”,缺失输出模型显示“暂未生成”。
- `<= 1100px` 主体改单栏,折叠按钮具备 `aria-expanded`
- 不新增依赖。
---
### Task 1: 锁定布局与可访问性回归
**Files:**
- Create: `frontend/scripts/regression-training-log-layout.mjs`
- Modify: `frontend/package.json`
- Test: `frontend/scripts/regression-training-log-layout.mjs`
**Interfaces:**
- Consumes: `frontend/src/views/system/TrainingLogView.vue` 源文件。
- Produces: `npm run test:training-log-layout` 专项回归命令。
- [ ] **Step 1: 写入失败测试**
测试必须断言:双栏容器、任务/数据集/运行概况分组、语义化参数折叠按钮、`aria-expanded`、缺失值文案和 `1100px` 响应式断点存在;首屏旧 `el-descriptions` 结构已移除。
- [ ] **Step 2: 运行并确认 RED**
Run: `cd frontend && npm run test:training-log-layout`
Expected: FAIL提示缺少 `.overview-layout` 等新结构。
- [ ] **Step 3: 在 package scripts 暴露测试命令**
```json
"test:training-log-layout": "node scripts/regression-training-log-layout.mjs"
```
### Task 2: 实现双栏任务档案首屏
**Files:**
- Modify: `frontend/src/views/system/TrainingLogView.vue`
- Test: `frontend/scripts/regression-training-log-layout.mjs`
**Interfaces:**
- Consumes: 现有 `task``dataset``modelsStore`、枚举映射与 `paramsExpanded`
- Produces: `.overview-layout``.profile-section``.runtime-panel``.parameter-groups` 和语义化 `.params-toggle-button`
- [ ] **Step 1: 重写任务与数据集模板**
使用一个顶层 `PageCard` 承载任务头和双栏内容;左栏用定义列表表达任务信息,数据集使用名称/类型/描述加指标带;右栏使用纵向运行概况项。
- [ ] **Step 2: 重写参数分组模板**
训练参数和 LoRA 参数改为双列轻分隔行,所有缺失值用 `未配置`,折叠按钮绑定 `:aria-expanded="paramsExpanded"`
- [ ] **Step 3: 实现视觉与响应式 SCSS**
添加双栏、信息行、指标带、焦点态和 `@media (max-width: 1100px)` / `700px` 规则;不修改全局样式。
- [ ] **Step 4: 运行并确认 GREEN**
Run: `cd frontend && npm run test:training-log-layout`
Expected: PASS输出“训练日志详情布局回归检查通过”。
### Task 2.5: 统一页面背景与内容表面
**Files:**
- Modify: `frontend/src/styles/index.scss`
- Modify: `frontend/src/layouts/MainLayout.vue`
- Modify: `frontend/src/views/system/TrainingLogView.vue`
- Create: `frontend/scripts/regression-page-surface.mjs`
- [ ] **Step 1: 先写并运行背景层回归检查**
Run: `cd frontend && npm run test:page-surface`
Expected: 首次 FAIL提示缺少 `--app-shell-bg` 和全局 `.page-canvas`
- [ ] **Step 2: 添加全局背景与表面 token**
定义灰色外壳 `--app-shell-bg: #f3f5f8`、白色页面画布 `--app-page-bg: #ffffff` 和白色内容表面 `--app-surface-bg: #ffffff`。在 `MainLayout` 中用 `.page-canvas` 包裹所有业务路由,并消除直接根 `PageCard` 的重复阴影。
- [ ] **Step 3: 运行并确认 GREEN**
Run: `cd frontend && npm run test:page-surface`
Expected: PASS输出“全局页面背景与内容表面回归检查通过”。
### Task 3: 类型、构建与视觉 QA
**Files:**
- Create: `design-qa.md`
- Modify: `frontend/src/views/system/TrainingLogView.vue`(仅在 QA 发现 P0/P1/P2 时)
**Interfaces:**
- Consumes: 选定视觉稿、浏览器渲染截图。
- Produces: 通过的类型检查、生产构建和 `design-qa.md`
- [ ] **Step 1: 静态验证**
Run: `cd frontend && npm run type-check`
Expected: 项目引用模式能检查真实源码;若仓库原有错误仍存在,输出中不得包含本次修改的训练日志页、布局或全局样式。
Run: `cd frontend && npx vite build`
Expected: exit 0。
Run: `cd frontend && npm run build`
Expected: 在类型检查修复前仍会被仓库原有错误阻断,必须记录实际错误文件。
- [ ] **Step 2: 启动并检查页面**
启动 Vite打开 `/login`,使用 mock 账号进入 `/training-log/1`,在 1440px 宽视口检查双栏、折叠交互和控制台错误。
- [ ] **Step 3: 执行 Design QA**
将选定视觉稿与实现截图放在同一比较输入中,检查字体、间距、颜色、图标、文案和交互;修复所有 P0/P1/P2 后更新 `design-qa.md``final result: passed`
- [ ] **Step 4: 最终验证**
重新运行专项回归、类型检查和构建,并记录实际结果。

View File

@@ -0,0 +1,267 @@
# 前端视觉与样式系统重构设计
**日期**2026-07-09
**范围**`frontend/` Vue 3 + Element Plus + Pinia
**目标用户**:内部数据/算法工程师,桌面端浏览器使用
## 背景与动机
当前前端已用 Element Plus + 自定义 SCSS 变量搭起一套"深色侧边栏 + 浅色内容区"的现代 B 端布局,但实际页面观感"乱"且有功能性瑕疵,根因不是设计语言本身有问题,而是**设计系统没有真正落地**
- 颜色散落硬编码(`#1e293b``#475569``#909399``#1890ff``#67c23a`…)
- Element Plus 旧默认色与新主题色(`#4f46e5`)并存
- 用了 FontAwesome **4.7.0**2017 年停止维护),大量 `fa-cogs`/`fa-hdd-o` 这种短横线后缀命名
- 登录页(`LoginView.vue`)背景是纯色 `#dbe0f9`,与主应用玻璃拟态风格割裂
- `AppHeader.vue:189` 引用了主题里**不存在**的 `--warning-color` 变量,导致回退到默认值
- `index.scss` 内有 5+ 处 `!important`,说明覆盖策略不对
- 装饰性动画(按钮悬停 `translateY(-1px)``pulse` 动画、过度渐变)增加视觉噪音
本次重构目标是**收口样式系统**,让"主题改一处,全站生效",并消除明显观感问题。
## 范围与非范围
### 范围内
1. 统一颜色变量系统(消除硬编码、补齐缺失变量)
2. 图标库迁移FA4 → **Element Plus Icons**`@element-plus/icons-vue`,已与 Element Plus 同步)
3. 重新设计登录页,与主应用视觉一致
4. 修复 `AppHeader.vue``--warning-color` 缺失问题
5. 整理 `index.scss``!important` 覆盖,改用更高特异性选择器
6. 适度降低装饰性效果:移除按钮悬停位移、统一卡片阴影阶梯
### 非范围(本次不做)
- 布局结构改动(保留 260px 侧边栏 + 60px 顶栏)
- 深色模式切换
- 任何业务逻辑修改
- 移动端适配(当前项目无移动端需求)
- `web/` 静态版不动
## 方案选型
### 选定方案:方案 A — 克制重塑
保留现有"深色侧边栏 + 浅色内容区"骨架,做样式系统收口。
**未选 方案 B极简重做**改动面太广10+ 视图都要适配,回报/风险比差。
**未选 方案 C全深色科技感**Element Plus 暗色需要大量单独调校,工作量与当前诉求不匹配。
## 设计
### 1. 颜色变量系统(`src/styles/index.scss` 重构)
**目标**:所有颜色、阴影、圆角都走 CSS 变量;后续改主题只改 `:root` 即可。
**新增/补齐变量**`src/styles/index.scss` 顶部):
```scss
:root {
/* Element Plus 主色(已有) */
--el-color-primary: #4f46e5;
--el-color-primary-light-3: #818cf8;
--el-color-primary-light-5: #a5b4fc;
--el-color-primary-light-7: #c7d2fe;
--el-color-primary-light-8: #e0e7ff;
--el-color-primary-light-9: #eef2ff;
--el-color-primary-dark-2: #4338ca;
/* 状态色(已有) */
--el-color-success: #10b981;
--el-color-warning: #f59e0b; /* 修复:之前未显式定义 */
--el-color-danger: #ef4444;
--el-color-info: #64748b;
/* 项目别名(已有 + 新增) */
--primary-color: var(--el-color-primary);
--primary-hover: var(--el-color-primary-light-3);
--success-color: var(--el-color-success);
--warning-color: var(--el-color-warning); /* 新增:修复 AppHeader 引用 */
--danger-color: var(--el-color-danger);
/* 圆角(已有) */
--el-border-radius-base: 8px;
--el-border-radius-small: 6px;
--el-border-radius-round: 20px;
/* ========== 新增:项目语义化变量 ========== */
/* 文字 */
--text-primary: #1e293b; /* 替代硬编码 #1e293b */
--text-secondary: #475569; /* 替代 #475569 */
--text-tertiary: #64748b; /* 替代 #64748b */
--text-placeholder: #94a3b8; /* 替代 #94a3b8 */
/* 背景 */
--bg-page: #f8fafc; /* 替代 --el-bg-color-page */
--bg-card: #ffffff;
--bg-subtle: #f1f5f9; /* 替代硬编码 #f1f5f9 */
--bg-hover: #f8fafc; /* 替代硬编码 #f8fafc */
/* 边框 */
--border-subtle: #e2e8f0; /* 替代硬编码 #e2e8f0 */
--border-base: #f1f5f9; /* 替代硬编码 #f1f5f9 */
/* 侧边栏(已有) */
--sidebar-bg: #0f172a;
--sidebar-text: #94a3b8;
--sidebar-active-bg: rgba(79, 70, 229, 0.15);
--sidebar-active-text: #ffffff;
--sidebar-section-title: #64748b;
/* 阴影(统一阶梯) */
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 2px 8px rgba(15, 23, 42, 0.06); /* 替代 --card-shadow */
--shadow-lg: 0 8px 24px rgba(15, 23, 42, 0.08);
--shadow-dialog: 0 20px 40px rgba(15, 23, 42, 0.12);
/* 尺寸 */
--header-height: 60px;
--sidebar-width: 260px;
--content-padding-x: 32px;
--content-padding-y: 32px;
}
```
**改动原则**
- 组件 SCSS 内的硬编码颜色全部替换为对应变量
- 任何新增颜色需求都先在 `:root` 定义,再引用
- 不再使用 `!important`;如确需覆盖,提升特异性(如 `:deep(.el-button)`
### 2. 图标库迁移FA4 → Element Plus Icons
**为什么选 Element Plus Icons**
- 零新增依赖成本(仅在 `package.json` 增加一行)
- 与 Element Plus 组件视觉风格天然一致
- 树摇友好,按需引入
- SVG 渲染,可继承 `currentColor``font-size`,比 FA 字体图标更现代
**实施**
1. `npm i @element-plus/icons-vue`
2. `main.ts` 全局注册常用图标(按需在组件内 import 也可)
3. 替换规则(在每个 view/component 内做局部替换):
| FA4 类名 | Element Plus 图标 | 用在哪 |
|---------|------------------|--------|
| `fa-cogs` | `Setting` | 侧边栏"模型调优" |
| `fa-line-chart` | `TrendCharts` | 侧边栏"模型评测" |
| `fa-server` | `Monitor` | 侧边栏"模型推理" |
| `fa-cube` | `Box` | 侧边栏"模型管理"、Logo |
| `fa-file-text` | `Document` | 侧边栏"数据集" |
| `fa-wrench` | `Tools` | 侧边栏"其他工具" |
| `fa-bar-chart` | `DataAnalysis` | 侧边栏"平台性能" |
| `fa-search` | `Search` | 搜索框前缀 |
| `fa-plus` | `Plus` | 新建按钮 |
| `fa-trash` | `Delete` | 删除按钮 |
| `fa-arrow-left` | `ArrowLeft` | 返回按钮 |
| `fa-microchip` | `Cpu` | 性能监控 |
| `fa-database` | `Files` | 内存监控(数据文件堆) |
| `fa-hdd-o` | `Box` | 磁盘监控(存储箱) |
| `fa-sign-out` | `SwitchButton` | 退出登录 |
| `fa-angle-down` | `ArrowDown` | 下拉箭头 |
| `fa-user-circle-o` | `User` | 个人中心 |
| `fa-cog` | `Setting` | 系统设置 |
| `fa-stop-circle-o` | `VideoPause` | 停止训练 |
| `fa-pencil` | `Edit` | 编辑 |
| `fa-times` | `Close` | 关闭 |
| `fa-globe` | `Connection` | 网络流量 |
| `fa-info-circle` | `InfoFilled` | 系统信息 |
| `fa-user-o` | `User` | 登录页账号图标 |
| `fa-lock-o` | `Lock` | 登录页密码图标 |
| `fa-sign-in` | `Right` | 登录按钮 |
**注意点**
- 替换后调整图标尺寸Element Plus Icons 用 `size` prop 或 CSS `font-size`
- 部分 FA4 类名(如 `fa-file-text-o`)在 FA5/FA6 已无 `-o` 后缀
- `main.ts` 中删除 `import '@/assets/font-awesome/css/font-awesome.min.css'`
-`src/assets/font-awesome/` 整个目录(字体文件 100+KB
### 3. 登录页重新设计(`LoginView.vue`
**目标**:与主应用保持一致的视觉语言,背景改用渐变 + 玻璃卡片。
**设计**
- 背景:渐变 `linear-gradient(135deg, #4f46e5 0%, #818cf8 50%, #c7d2fe 100%)`
- 卡片:白色,圆角 16px阴影 `var(--shadow-dialog)`
- Logo 区域上下结构Logo 48px → 标题 24px → 副标题 14px 灰色)
- 表单:单项之间 20px 间距,输入框 44px 高,圆角 8px
- 按钮主色实心44px 高,全宽
- 选项行flex 布局,记住密码左、忘记密码右
- 整体居中,最大宽 420px
**关键 CSS 改动**
- 背景从 `#dbe0f9` 改为 `linear-gradient(135deg, #4f46e5 0%, #818cf8 50%, #c7d2fe 100%)`
- 卡片圆角从 12px 提升到 16px
- 阴影替换为 `var(--shadow-dialog)`
- 所有内联颜色改用变量
### 4. 修复 `AppHeader.vue` 的 `--warning-color` 缺失
**问题**`AppHeader.vue:189` 使用 `var(--warning-color)`,但 `index.scss` 里只定义了 `--danger-color``--success-color`,没有 `--warning-color`
**修复**:在 `:root` 中新增 `--warning-color: var(--el-color-warning);`(已在第 1 节列入)。
### 5. 整理 `index.scss` 的 `!important`
**现有 `!important` 用法**(来自 `index.scss`
| 选择器 | 用途 | 现状 |
|--------|------|------|
| `.el-card { border: none !important; ... }` | 去掉 EP 默认边框 | 提升为 `.app-container .el-card` 或全局加 `:where()` 重置 |
| `.el-tag { border: none !important; ... }` | 去掉 EP 默认边框 | 同上 |
| `.el-dialog { border-radius: 16px !important; ... }` | 圆角和阴影 | 同上 |
**改写策略**
- 用更高特异性选择器(如 `body .el-card`)替代 `!important`
- 或用 CSS Layer (`@layer reset, overrides, components`) 控制优先级
**本次采用**:直接提高特异性,避免引入 CSS Layer 复杂度(属于项目既定风格之外的引入,需要权衡)。
### 6. 降低装饰性效果
**调整清单**
- `index.scss` 移除 `.el-button:hover { transform: translateY(-1px); box-shadow: ...; }` — 改为仅改变背景色
- `AppHeader.vue` 移除 `pulse` 动画,改为静态红色
- 侧边栏菜单图标的 `transform: scale(1.1)` 悬停动画保留(这是细节点缀,不算噪音)
- 卡片悬停 `transform` 动效保留(仅在 DataTablePage 等列表卡片上,目前没有,不新增)
- 顶栏 `backdrop-filter: blur(12px)` 保留(玻璃感是这个项目的风格语言)
## 实施步骤(落地顺序)
按风险从低到高,便于逐步验证:
1. **步骤 1**:扩展 `src/styles/index.scss` 变量(新增,不删除旧的)
2. **步骤 2**:安装 `@element-plus/icons-vue`,在 `main.ts` 注册
3. **步骤 3**:逐文件替换图标(按表格顺序)
4. **步骤 4**:逐文件替换硬编码颜色为变量
5. **步骤 5**:重新设计 `LoginView.vue`
6. **步骤 6**:删除 `src/assets/font-awesome/` 目录和 `main.ts` 的 import
7. **步骤 7**:运行 `npm run type-check``npm run build` 验证
每步完成后截图(手动)确认未引入新问题。
## 验证标准
- [ ] `npm run type-check` 通过
- [ ] `npm run build` 通过
- [ ] 浏览器控制台无 CSS 警告(无未定义变量)
- [ ] 全站不再有 `#1890ff``#67c23a``#e6a23c``#f56c6c` 这类 EP 旧色硬编码grep 验证)
- [ ] 全站不再有 `class="fa fa-` 的引用grep 验证)
- [ ] 全站不再有 `--warning-color` 这类指向不存在变量的引用
- [ ] `grep -c "!important" src/styles/index.scss` 数量从当前值下降 50% 以上
- [ ] 登录页与主应用风格一致
## 风险与回退
- **风险 1**Element Plus Icons 不覆盖某些冷门 FA 图标(如 `fa-stop-circle-o` 训练停止)
- **应对**:在替换前先做一次图标对照表,确认每个 FA 类名都有 EP 对应项;没有则保留 FA4不删除 FA 资源),但推进用 EP 主流图标
- **风险 2**CSS 变量替换漏掉某处导致视觉回退
- **应对**:分阶段提交,每阶段都跑 build如有遗漏EP 会回退到自身默认色
- **风险 3**`!important` 整理引入新覆盖问题
- **应对**:整理前先 `git diff` 对比编译产物颜色变化,保留 `git stash` 能力
## 后续优化(不在本次范围)
- 抽出 Design Token 文件(`tokens.scss`)做 Figma → Code 同步
- 引入 UnoCSS 或 Windi CSS 做原子化样式(取决于团队接受度)
- 给所有列表页加 skeleton 加载态
- 暗色模式支持(需要先在 EP 主题层注册暗色)

View File

@@ -0,0 +1,194 @@
# 数据处理创建流程四步向导设计
日期2026-07-10
## 1. 目标
重构 `/data-process/create`,把当前职责混杂、步骤数量会变化的流程,调整为四个语义稳定、页面内容相互独立的步骤。用户在任一时刻只处理当前任务,同时始终知道所处阶段、前后关系和唯一主操作。
本次设计沿用现有平台视觉语言:浅灰应用背景、单层白色页面画布、靛蓝主色、克制边框与阴影、固定四步进度导航和底部操作栏。不得出现页面白底中再次嵌套整页白色大卡片的双层白底。
## 2. 固定流程
顶部步骤数量始终为四个,不随结构化或非结构化类型变化:
1. **创建任务**:填写任务信息、上传源文件并选择处理类型。
2. **数据预览**:检查系统解析结果;非结构化数据进入切片预览,结构化数据进入记录预览。
3. **开始生成**:确认摘要、启动生成并查看进度或错误。
4. **结果编辑与保存**:检查并修改生成结果,通过校验后保存任务。
步骤状态只有 `未开始``当前``已完成` 三种。用户返回已完成步骤时保留已经填写或编辑的内容,不因切换处理类型而静默清空任务名称、描述和上传文件。
## 3. 第一步:创建任务
页面只显示创建任务所需内容:
- 任务名称和可选描述。
- 结构化数据、非结构化数据两种处理类型。
- 单文件上传区、文件格式和大小约束。
- 上传后的基础识别结果:文件名、大小、格式、记录数或文本长度、校验状态。
底部操作:`取消``保存草稿`、唯一主操作 `继续:数据预览`
切换处理类型时仅更新后续预览和生成策略;如果已经产生预览结果,需要明确提示预览将重新计算,确认后才清除后续步骤数据。
## 4. 第二步:数据预览
### 4.1 页面职责
本页只负责核对源数据与系统预览结果的对应关系,并允许修改不合理的切片或记录内容。不得显示第一步的大量表单、第三步的生成进度或第四步的最终结果表格。
页面顶部仅保留一行来源摘要,例如:
`finance_qa.jsonl · 非结构化数据 · 8,560 个切片`
### 4.2 非结构化数据:源文件与切片对照
主体为同一白色画布中的左右分栏,使用竖向分隔线区分,不使用两个浮动大卡片:
- **左侧约 58%:源文件阅读器**
- 只读展示完整源文件,支持滚动和行号。
- 当前切片对应的原文使用浅靛蓝背景高亮,并在左边缘显示定位条。
- 顶部提示当前定位,例如 `已定位到切片 #002`
- 左侧原文始终保持源文件真实性,修改右侧切片不会直接改写源文件。
- **右侧约 42%:切片内容**
- 上方为切片列表,支持按编号或内容搜索、仅查看已修改项。
- 每个切片显示编号、来源行范围、Token 数和修改状态。
- 下方为当前切片的可编辑正文、字符数、Token 数和自动保存状态。
- 提供上一片、下一片和恢复原始内容。
### 4.3 对照定位交互
右侧切片是主选择源,左侧原文是定位反馈:
1. 用户点击右侧切片。
2. 系统读取该切片保存的原始来源范围 `sourceStart``sourceEnd`
3. 左侧阅读器自动滚动到目标范围,并将对应原文高亮。
4. 右侧切片进入选中态,左右使用一致的浅靛蓝语义色建立对应关系。
5. 用户切换上一片或下一片时执行相同的同步定位。
为了避免跳动,目标范围应滚动到阅读区中部;如果目标已完全可见,则只更新高亮,不重复滚动。
### 4.4 编辑与来源映射
- 编辑切片正文只改变待生成内容,来源映射仍指向最初的原文范围。
- 编辑后显示 `已修改,草稿已自动保存`,并允许恢复到系统初始切片内容。
- 修改后的文本与原文不再完全一致时,左侧仍高亮原始来源,右侧显示 `内容已调整`,避免误解为源文件被改写。
- 手动新增的切片没有原始来源,显示 `手动新增,无源文件定位`,左侧清除高亮但保留当前位置。
- 删除切片需要二次确认;删除只影响本次处理,不修改源文件。
- 找不到来源范围或范围越界时,右侧显示 `无法定位原文`,左侧不进行错误滚动,用户仍可编辑该切片。
### 4.5 结构化数据适配
第二步的整体布局和交互不变,但语义调整为记录对照:
- 左侧展示源文件中的表格行或原始 JSON 记录。
- 右侧展示系统解析后的单条记录及字段值。
- 点击右侧记录,左侧定位并高亮对应源行或 JSON 范围。
这样可以保持四步流程稳定,同时避免对结构化数据使用不准确的“切片”文案。
底部操作:`返回:创建任务``保存草稿`、唯一主操作 `确认预览并继续`
## 5. 第三步:开始生成
页面只显示:
- 任务名称、数据类型、文件、预览条目数和修改条目数的只读摘要。
- 唯一启动操作 `开始生成`
- 启动后的进度、当前阶段、耗时和状态日志。
- 失败时显示错误摘要与 `重试`;运行中提供 `停止生成`,停止前需要二次确认。
启动前不显示底部“下一步”与页面内“开始生成”两个竞争按钮。生成成功后,唯一主操作切换为 `查看生成结果`
## 6. 第四步:结果编辑与保存
结果不再使用一组原始 JSON 文本框堆叠展示,而采用结构化编辑列表:
- 桌面端左侧为结果条目列表,支持搜索、错误筛选和已修改筛选。
- 桌面端右侧为当前结果的字段编辑区;较窄窗口改为列表在上、编辑区在下。
- 每条结果显示校验状态,错误定位到具体字段。
- 支持上一条、下一条、恢复生成结果。
- 保存前执行完整校验;有阻断错误时禁止保存并聚焦首个错误。
底部操作:`返回:开始生成``保存草稿`、唯一主操作 `保存任务`
## 7. 状态与数据模型
向导需要维护统一草稿状态:
```ts
interface DataProcessDraft {
task: TaskInfo
sourceFile: SourceFileMeta
processingType: 'structured' | 'unstructured'
previewItems: PreviewItem[]
selectedPreviewId: string | null
generation: GenerationState
results: ResultItem[]
currentStep: 1 | 2 | 3 | 4
dirty: boolean
}
interface PreviewItem {
id: string
originalContent: string
editedContent: string
sourceStart: number | null
sourceEnd: number | null
sourceStartLine: number | null
sourceEndLine: number | null
tokenCount: number
status: 'original' | 'modified' | 'manual' | 'invalid'
}
```
源文件位置以字符偏移作为定位依据,行号用于展示。这样即使原文存在长行或不同换行符,也能稳定高亮准确范围。
## 8. 页面组件边界
- `DataProcessWizardShell`:固定四步导航、页面标题和底部操作栏。
- `TaskSetupStep`:任务信息、类型和文件上传。
- `PreviewStep`:数据预览容器,按处理类型选择预览器。
- `SourceViewer`:源文件渲染、滚动定位和范围高亮。
- `PreviewItemList`:切片或记录列表、搜索与选择。
- `PreviewItemEditor`:当前条目编辑、状态和恢复。
- `GenerationStep`:启动、进度、失败与重试。
- `ResultReviewStep`:生成结果列表、字段编辑和保存校验。
`SourceViewer` 不感知切片列表内部逻辑,只接收选中范围;`PreviewItemList` 不直接操纵滚动,只上报选中项。同步行为由 `PreviewStep` 统一协调。
## 9. 异常与离开保护
- 上传失败、解析失败、预览生成失败分别提供明确原因和重试入口。
- 预览数据加载时显示骨架或局部加载状态,不能让整个白色画布闪烁或短暂回退到列表页。
- 刷新、返回列表或关闭页面前,如果存在未持久化修改,必须提示保存草稿或放弃更改。
- 自动保存失败时保留本地编辑内容,并显示可重试状态。
- 生成过程中禁止无提示切换处理类型或替换源文件。
## 10. 响应式与可访问性
- 桌面端左右对照为默认布局,分栏宽度允许拖动但设置最小宽度。
- 较窄窗口下改为上下布局:源文件在上,切片在下;选中切片后仍自动定位高亮。
- 选中、高亮、修改和错误状态不能只依赖颜色,还需文字、图标或边缘标记。
- 键盘支持上下切换切片、聚焦编辑器、保存草稿;滚动定位后不抢走编辑器焦点。
## 11. 验收标准
1. 结构化和非结构化流程都始终显示四个固定步骤。
2. 每一步只包含该阶段必要内容,页面中不存在跨步骤主操作竞争。
3. 点击任一有来源映射的右侧切片后,左侧在一次交互内定位并高亮准确原文范围。
4. 已完全可见的范围不会产生多余滚动;不可定位时有明确反馈且不破坏编辑。
5. 修改切片不会改写左侧源文件,高亮仍指向原始来源。
6. 返回前一步再进入时,选中项、编辑内容、滚动上下文和草稿状态可恢复。
7. 生成开始前只存在一个明确启动操作;生成成功后才允许进入结果编辑。
8. 最终结果保存前执行字段级校验,阻断错误可准确定位。
9. 路由切换不出现列表页或灰色背景闪烁,白色页面画布保持连续。
10. 页面在常用桌面宽度下无横向溢出,底部操作栏不遮挡主体内容。
## 12. 视觉参考
- 当前流程截图:`docs/superpowers/specs/assets/data-process-create-audit/01-create-task.png`
- 当前动态步骤截图:`docs/superpowers/specs/assets/data-process-create-audit/02-unstructured-step-change.png`
- 最终左右对照修订稿:`/Users/caoxiaozhu/.codex/generated_images/019f4981-cb06-79f0-b387-1d685e74ceea/exec-c72de596-4c50-405b-8987-43d122e02b86.png`

View File

@@ -0,0 +1,27 @@
# 数据处理任务状态切换移除设计
## 目标
移除数据处理任务列表左上角的“全部任务”“处理中”“已完成”状态切换组件。
页面默认展示全部任务,用户仍可通过任务状态列查看每条任务的当前状态。
## 改动范围
- 删除 `DataProcessListView.vue` 中的状态切换按钮及标题插槽。
- 删除仅服务于该组件的 `activeTab` 状态和 `filteredDataList` 计算逻辑。
- 将任务表格的数据源直接绑定为完整的 `dataList`
- 删除仅服务于状态切换组件的胶囊样式。
## 保留行为
- 保留任务名称搜索。
- 保留任务状态列及状态标签。
- 保留新建任务、详情、删除和分页等现有行为。
- 不修改任务 Mock 数据及后端接口设计。
## 验证标准
- 页面不再出现“全部任务”“处理中”“已完成”三个切换按钮。
- 表格默认展示所有状态的任务。
- 页面不存在遗留的状态切换变量、筛选分支或专用样式。
- 前端类型检查和相关回归检查通过。

View File

@@ -0,0 +1,35 @@
# 数据预览详情页头部操作移除设计
## 目标
移除数据预览详情页右上角的“打包下载”“删除”“返回”三个按钮,
让该页面仅承担数据集文件与内容预览职责。
## 范围
- 删除 `DatasetPreviewView.vue` 的页头额外操作区域及其三个按钮。
- 删除仅被这三个按钮使用的整包下载、删除和路由返回逻辑及依赖。
- 保留文件列表中的单文件“预览”和“下载”操作。
- 保留数据集信息、文件列表与内容预览区域。
## 方案比较
1. 直接删除页面头部按钮和失效逻辑(采用):无隐藏交互与无用依赖,页面职责清晰。
2. 使用 CSS 隐藏按钮:按钮逻辑仍留在页面中,键盘和后续维护可能误暴露功能。
3. 增加运行时开关:为固定的展示要求引入无必要状态和分支。
## 实现设计
- 移除 `PageCard``extra` 插槽。
- 从组件中移除 `useRouter``ElMessage``ElMessageBox``deleteDataset`
`downloadDatasetUrl` 及对应处理函数。
- 保留 `handleDownloadFile``downloadFileUrl`,确保文件级下载仍可用。
- 新增回归检查,断言详情页不再含这三个页头按钮或其对应处理函数,
并断言文件级预览、下载操作仍存在。
## 验收标准
1. 数据预览详情页不显示“打包下载”“删除”“返回”按钮。
2. 文件列表中的“预览”和“下载”按钮继续显示并可用。
3. 页面不保留已删除头部操作的事件处理函数与未使用依赖。
4. 针对性回归检查和生产构建通过。

View File

@@ -0,0 +1,58 @@
# 数据集管理“数据任务”Mock 数据设计
## 目标
让数据集管理页的“数据任务”页签展示可用于界面演示的任务产出数据,
同时保持“本地上传”页签、数据集预览和下载入口的现有行为。
## 现状
- 数据集列表统一从 `mockDatasets` 获取。
- “本地上传”页签当前展示全部数据集。
- “数据任务”页签固定返回空数组,因此始终为空。
- 数据集详情、预览和下载均基于同一套 Mock 数据接口。
## 方案比较
1. 为数据集增加来源字段并统一过滤(采用)。单一数据源,页签语义清晰,
新增的任务数据可继续复用详情、预览和下载接口。
2. 在列表页内单独维护任务数据。改动较少,但会形成第二份数据源,详情接口
无法自然找到这些记录。
3. 从数据处理任务动态生成数据集。数据关联更强,但需要重构任务 Mock 数据,
超出本次只增加演示数据的范围。
## 数据设计
-`DatasetItem` 中增加可选来源字段 `source`,取值为 `upload``task`
- 现有 6 条 Mock 数据明确标记为 `upload`
- 新增 4 条来源为 `task` 的 Mock 数据,覆盖训练集、评测集等类型,包含名称、
存储位置、大小、条数、描述和创建时间。
- 新记录使用不重复的 ID并保留现有 Mock 适配器的统一查询路径。
## 页面行为
- “本地上传”仅展示 `source !== 'task'` 的数据,兼容后端暂未返回来源字段的情况。
- “数据任务”仅展示 `source === 'task'` 的数据。
- 搜索、分页、预览、下载和删除按钮保持现有行为。
- 切换页签后,公共列表组件会在数据变化时回到第一页并清空选中状态。
## 异常与边界
- 未携带 `source` 的真实接口数据默认归入“本地上传”,避免升级期间数据消失。
- 任务数据 ID 不与现有数据冲突,详情查询仍能返回对应记录。
- 本次不新增依赖,不修改后端接口,不实现真实的数据任务关联。
## 验证
- 先添加一个能验证来源字段和页签过滤规则的回归检查,再实现变更。
- 运行项目类型检查与生产构建;若存在仓库原有错误,需确认本次改动文件没有
引入新的错误。
- 启动页面后检查两个页签:本地上传显示 6 条,数据任务显示 4 条,任务数据可被
搜索且表格字段完整。
## 验收标准
1. “数据任务”页签展示 4 条假数据。
2. “本地上传”页签不混入任务生成的数据。
3. 两个页签的搜索、分页和操作列正常显示。
4. 新增类型定义与 Mock 数据通过针对性回归检查。

View File

@@ -0,0 +1,53 @@
# 页面表面分类设计
## 目标
修正主布局无条件套用白色页面画布的问题,使页面层级符合以下规则:
- 列表页已经拥有自己的白色卡片,因此直接放在灰色应用背景上。
- 表单页、详情页和对话页继续使用统一的白色圆角页面画布。
- 页面分类由路由显式声明,避免依赖路径字符串或 DOM 结构猜测。
## 根因
`MainLayout.vue` 当前无条件为所有子路由渲染 `.page-canvas`。同时,列表页通过
`DataTablePage``.eval-card-container``.manage-card-container` 自带白色卡片,
因此形成“灰色外层 → 白色页面画布 → 白色列表卡片”的重复层级。
## 设计
在列表路由的 `meta` 中增加 `pageSurface: 'self'`
- `/fine-tune`
- `/model-eval`
- `/model-inference`
- `/model-manage`
- `/data-process`
- `/dataset`
其他路由保持默认行为,不需要逐项声明,仍由主布局提供白色页面画布。
`MainLayout.vue` 根据 `route.meta.pageSurface``.page-canvas` 添加
`.is-self-surface` 状态类。该状态仅取消外层画布的白色背景、圆角、阴影和内边距,
保留列表页自身的卡片样式。
对于默认白色画布页面,如果路由直接输出 `PageCard`,可直接扁平化;如果由页面
根容器包裹主 `PageCard`,根容器必须显式声明 `.page-card-host`。只有这两类主卡片
改为透明、无圆角、无阴影,内部业务卡片不受影响,避免创建页出现“白色画布再套
白色卡片”,同时不误伤训练日志的参数卡片。
## 验收标准
1. 六个列表路由只显示自身的一层白色卡片,外部可见灰色页面背景。
2. `/training-log/:id`、创建页、编辑页等仍显示白色圆角页面画布。
3. 不根据 URL 文本或 DOM 自动判断页面类型。
4. 不改变列表、表单和详情页现有业务逻辑。
5. 桌面宽度下不产生水平滚动。
6. 创建训练任务等表单页只显示一层白色画布,根 `PageCard` 不形成第二层表面。
## 测试策略
- 静态回归脚本验证六个列表路由都声明 `pageSurface: 'self'`
- 静态回归脚本验证 `MainLayout` 根据路由 meta 设置状态类。
- 静态回归脚本验证 `.is-self-surface` 取消白底、内边距、圆角和阴影。
- 浏览器分别检查一个列表页和一个详情页。

View File

@@ -0,0 +1,25 @@
# 数据预览切片编辑模式
## 目标
第二步右侧区域不再把切片列表和正文编辑器上下堆叠。默认仅展示可搜索、可分页的切片列表;编辑时在同一右侧区域进入单独的编辑视图。
## 列表模式
- 每行显示切片编号与来源行号。
- 每行右侧提供铅笔和垃圾桶图标按钮,分别用于编辑与删除。
- 不显示 Token、修改状态和“仅看已修改”筛选。
- 保留按编号或正文搜索及每页 6 条分页。
## 编辑模式
- 点击铅笔后,右侧替换为当前切片的编辑界面,左侧源文件继续高亮对应行。
- 编辑使用组件内临时草稿;输入过程不更新父级预览数据。
- “保存修改”将草稿写回父级数据并返回列表。
- “取消”或“返回列表”丢弃未保存草稿并返回列表,用户可继续选择其他切片。
- 删除继续使用确认弹窗;确认后删除该条并返回列表。
## 边界与验证
- 翻页、搜索或切换文件时,应退出编辑模式并丢弃未保存草稿,避免将内容写到错误条目。
- 切片列表、编辑模式切换、保存和取消行为由向导回归脚本覆盖。

View File

@@ -0,0 +1,27 @@
# 二级页面路由转场设计
## 目标
消除列表页进入新建、详情、编辑等二级页面时的透明闪烁,使路由内容与页面表面模式在同一帧完成切换。
## 根因
`MainLayout.vue` 使用 `transition name="fade" mode="out-in"`。路由变化时,
`route.meta.pageSurface` 会立即切换外层画布模式,但旧页面仍执行 150ms 的离场淡出,
因此旧列表会短暂显示在新页面的白色画布中并处于半透明状态。
## 已选方案
移除主布局的页面级 `transition` 和对应 `.fade-*` 样式,让 `router-view` 组件立即替换。
列表、详情、新建和编辑页面的表面模式将与内容同步切换。
保留组件内部动效,包括弹窗、折叠、按钮状态、批量操作栏等局部交互动画。
## 验收标准
1. `MainLayout.vue` 不再使用页面级 `<transition>`
2. 主布局不再包含 `.fade-enter-*``.fade-leave-*` 透明度规则。
3. 列表进入新建、列表进入训练日志、二级页面返回列表时,不出现旧页面半透明中间帧。
4. 页面表面分类和根 `PageCard` 扁平化逻辑保持不变。
5. 页面无水平溢出,控制台无新增错误。

View File

@@ -0,0 +1,86 @@
# 源数据上传紧凑文件列表设计
日期2026-07-10
## 1. 目标
优化数据处理创建页“源数据上传”区域中已选文件的呈现方式。当前每个文件
占用一张独立的大卡片;文件数量增多时,页面会被文件清单拉长,用户也难以
快速定位和删除某一文件。
本次改动将已上传文件改为每页最多 10 条的紧凑列表,并让上传入口随文件状态切换:
空状态使用大尺寸拖拽上传区,已有文件时切换为列表标题右侧的小型继续上传按钮。
这样能保持文件状态和逐项删除能力,让大量文件的查看与操作集中在一个稳定区域内。
## 2. 方案选择
评估过三种方式:
1. 文件名标签流:占用最少,但长文件名、状态与删除操作会拥挤,不适合本页。
2. 折叠摘要:页面最短,但需要额外展开才能操作,增加一步操作成本。
3. 固定高度紧凑列表:每个文件是一行,状态和删除按钮始终可见;超过可见
行数后在列表内滚动。
采用第三种方案。它保留当前已有的逐项删除能力,同时解决页面高度随文件数
量线性增长的问题。
## 3. 布局与交互
空状态只显示现有的大尺寸拖拽上传区及格式提示,用于首次添加文件。
存在文件后,隐藏大尺寸拖拽上传区和其格式提示,在原位置显示文件列表:
- 列表顶部左侧显示 `已添加 N 个文件`,其中 `N` 为当前文件数;右侧显示
小型 `继续上传` 按钮。
- `继续上传` 复用原上传控件的多文件选择、格式限制、非自动上传和文件变更事件,
只改变触发入口的尺寸与位置。
- 每页最多显示 10 行;超过 10 个文件时,在列表底部显示分页器,不使用长列表
或内部滚动承载额外文件。
- 新增文件后自动切换到最后一页,确保用户能立刻看到刚添加的文件;删除当前页
的最后一个文件后,自动回退到仍有文件的最后一页。
- 单行高度约 48px使用细分隔线区分不再使用外框、左侧绿条和大尺寸图标。
- 每行从左至右依次为:文件类型图标、文件名、大小与条目数、校验状态、删除。
- 文件名占据可伸缩区域;空间不足时省略,保留原始 `title` 以便悬停查看全名。
- 删除按钮沿用红色文字链接,点击后继续使用当前的移除事件;不新增确认弹窗
或批量删除,以免改变既有操作语义。
- 校验状态沿用绿色图标和“校验通过”文案;后续若存在失败状态,可在同一位置
显示错误文案而不改变列表结构。
## 4. 视觉约束
- 延续页面的靛蓝文件图标、绿色成功状态和红色危险操作色;继续上传按钮使用现有
主色的紧凑次级按钮样式。
- 列表容器使用现有浅灰边框和 8px 圆角,背景保持白色;不新增大卡片层级。
- 仅当文件数超过 10 个时显示分页器;分页器使用紧凑的上一页、页码、下一页布局,
不提供页大小选择或跳页输入,避免增加不必要的操作。
- 在窄屏下,元信息可收缩,文件名仍保持最优先;删除按钮必须始终可见且可点按。
## 5. 组件边界与数据流
改动只在 `TaskSetupStep` 内完成:
- 继续使用现有 `uploadedFiles` 属性和 `remove-file` 事件。
- 不修改文件上传、格式限制、校验逻辑或父组件中的文件数据模型。
- 新增的文件总数来自 `uploadedFiles.length`,每行信息继续来自现有文件元数据。
## 6. 可访问性
- 删除按钮继续使用真实的 `el-button`,确保键盘可聚焦和激活。
- 文件名省略时通过原生 `title` 暴露完整名称。
- 校验状态同时保留图标和文字,不能只依靠颜色传达结果。
- 滚动列表不截断键盘焦点;聚焦到不可见的删除按钮时,浏览器应能将对应行带入
可见区域。
## 7. 验收标准
1. 尚未添加文件时,只显示大尺寸拖拽上传区与格式提示,不显示文件列表。
2. 上传至少 1 个文件后,大尺寸上传区与格式提示不再显示;文件列表标题右侧显示
可继续选择多个文件的小型上传按钮。
3. 上传 1 至 10 个文件时,所有文件在紧凑列表中直接可见,每行均可单独删除。
4. 上传超过 10 个文件时,列表一次只渲染当前 10 条,分页器可切换其他文件页;
页面高度不随总文件数继续增长。
5. 新增文件后自动显示最后一页;删除造成当前页没有文件时,自动回到有效页。
6. 每一行均展示文件名、大小、可选条目数、校验状态和删除操作。
7. 长文件名不会挤压删除按钮,且悬停时可获取完整名称。
8. 处理类型切换、格式限制、首次上传、继续上传和删除事件的现有行为不变。
9. 常用桌面宽度和窄屏下均无横向溢出,删除操作保持可访问。

View File

@@ -0,0 +1,68 @@
# 训练日志详情页双栏改版设计
**日期**2026-07-10
**页面**`frontend/src/views/system/TrainingLogView.vue`
**视觉基准**:用户选定的第 2 套方案(双栏任务档案型)
**视觉稿**`docs/superpowers/specs/assets/training-log-detail-option-2.png`
## 目标
把当前多组带边框 `el-descriptions` 表格改成更易扫描的任务档案页:左侧聚合任务信息与数据集,右侧集中展示运行概况,下方用轻量折叠区呈现训练参数。数据请求、日志刷新、训练曲线和原始日志行为保持不变。
## 选定布局
1. 页面顶部保留任务名与状态,并增加任务类型、创建时间等辅助上下文。
2. 首屏主体使用 `2fr / 0.9fr` 双栏:
- 左栏依次为“任务信息”和“训练数据集”;
- 右栏为“运行概况”,集中展示时间、时长、存储、训练方式和训练方法。
3. 数据集名称与描述优先,条数和大小放入同一条浅色指标带。
4. 训练参数与 LoRA 参数使用同一卡片内的两个分组;标签和值采用轻分隔行,不再使用描边表格。
5. 缺失值统一显示“未配置”,输出模型缺失显示“暂未生成”,避免大量孤立的 `-`
## 视觉规则
- 应用外壳使用 `--app-shell-bg: #f3f5f8`,灰色只作为页面四周的留白背景。
- 每个业务路由统一渲染在 `--app-page-bg: #ffffff` 的白色圆角页面画布中,所有页面内容都基于这层白底布局。
- 页面内部白色内容表面继续使用 `--app-surface-bg: #ffffff`;当路由根节点本身是 `PageCard` 时,移除其额外阴影和外边距,避免白底里再套一张重复大卡片。
- 沿用项目现有 Indigo `#4f46e5`、Slate 灰阶和成功色 `#10b981`
- 一级卡片使用现有 `PageCard` 和 12px 实际圆角;内部块使用 8px 圆角。
- 仅使用 `#e2e8f0` / `#f1f5f9` 轻分隔,不新增阴影层级和渐变。
- 标题 18px/600分组标题 16px/600正文值 14px辅助标签 1213px。
- 图标继续使用项目现有 Font Awesome 4避免引入依赖或扩大本次改造范围。
## 交互与响应式
- 参数折叠头改为原生 `button`,提供 `aria-expanded`、可见焦点和至少 44px 点击高度。
- 宽屏为双栏;`<= 1100px` 改为单栏;参数明细在窄屏下降为单列。
- 保持 5 秒自动刷新、图表、汇总和原始日志逻辑不变。
## 范围
### 修改
- `frontend/src/views/system/TrainingLogView.vue`
- `frontend/src/layouts/MainLayout.vue`
- `frontend/src/styles/index.scss`
- `frontend/package.json`
- `frontend/scripts/regression-training-log-layout.mjs`
- `frontend/scripts/regression-page-surface.mjs`
### 不修改
- API、Pinia、类型定义、路由和 mock 数据。
- 全局 `PageCard.vue`
- 训练曲线、训练汇总和日志输出的业务逻辑。
## 验收标准
- 首屏不再使用带边框的详情表格表达任务、数据集和训练参数。
- 双栏信息架构与选定视觉稿一致,缺失值文案可读。
- 参数折叠支持鼠标、键盘和辅助技术。
- 1100px 以下无双栏挤压或横向滚动。
- 两个专项回归脚本与 `npx vite build` 通过。
- `npm run type-check` / `npm run build` 使用项目引用模式执行;当前仓库原有的 mock 与微调页面类型错误需如实记录,且本次改动文件不得出现在错误列表中。
## 风险
- 生成视觉稿只覆盖首屏,图表区继续沿用现状;实现以首屏信息架构为高保真目标。
- 项目目录没有 Git 元数据,无法提交设计文档或代码提交;所有变更保持小范围、可直接审阅。

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Some files were not shown because too many files have changed in this diff Show More