diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..00ad40f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,166 @@ +# AGENTS 指南(MyTool) + +本文件面向在本仓库内工作的自动化 coding agents,目标是快速、安全地完成任务并保持项目一致性。 + +## 1. 仓库概览 +- `backend/`:Spring Boot 2.7 + Java 8。 +- `frontend/`:Vue 3 + Vite + TypeScript + Element Plus。 +- `docker/`:单容器部署脚本与镜像构建。 +- 生产环境为同源部署:后端托管前端构建产物。 + +## 2. Cursor / Copilot 规则检查 +- 已检查 `.cursor/rules/`、`.cursorrules`、`.github/copilot-instructions.md`。 +- 当前仓库未发现上述规则文件。 +- 结论:请以本 `AGENTS.md` 作为主要执行规范。 + +## 3. 构建 / 运行 / Lint / 测试命令 + +### 3.1 前端(在 `frontend/` 目录执行) +安装依赖: +```bash +npm ci +``` +本地开发: +```bash +npm run dev +``` +生产构建: +```bash +npm run build +``` +预览构建: +```bash +npm run preview +``` +Lint(当前为占位脚本): +```bash +npm run lint +``` +说明:当前 `lint` 实际输出 `no linter configured yet`,不是强校验。 + +### 3.2 后端(在 `backend/` 目录执行) +编译: +```bash +mvn clean compile +``` +打包: +```bash +mvn clean package +``` +运行服务: +```bash +mvn spring-boot:run +``` +运行全部测试: +```bash +mvn test +``` +运行单个测试类(重点): +```bash +mvn -Dtest=SomeServiceTest test +``` +运行单个测试方法(重点): +```bash +mvn -Dtest=SomeServiceTest#shouldHandleEdgeCase test +``` +跳过测试打包(Docker 构建同策略): +```bash +mvn clean package -DskipTests +``` + +### 3.3 Docker(在仓库根目录或 `docker/`) +启动: +```bash +cd docker && docker compose up -d --build +``` +日志: +```bash +cd docker && docker compose logs -f +``` +停止: +```bash +cd docker && docker compose down +``` + +## 4. 测试现状 +- `backend/src/test` 已有基础测试(如 `FileTransferUtilsTest`、`DedupServiceInternalTest`、`ConvertServiceInternalTest`、`TraditionalFilterServiceTest`、`ProgressStoreTest`)。 +- 前端未配置 Vitest/Jest,暂无 `*.spec.ts` / `*.test.ts`。 +- 新增功能时,优先补后端单测,至少覆盖核心服务分支。 +- 若引入前端测试框架,需同步更新 `package.json` 与本文件命令。 + +## 5. 后端代码规范(Java / Spring) + +### 5.1 分层与职责 +- 根包固定为 `com.music`。 +- 目录分层遵循 `controller` / `service` / `dto` / `config` / `common` / `exception`。 +- Controller 负责参数校验与编排,不写重业务逻辑。 +- Service 负责核心业务流程,异常要转为可读的业务失败信息。 + +### 5.2 命名规范 +- 类名:`PascalCase`(如 `ConvertService`、`GlobalExceptionHandler`)。 +- 方法/字段:`camelCase`。 +- 常量:`UPPER_SNAKE_CASE`(如 `LOSSLESS_EXTENSIONS`)。 +- DTO 命名:`XxxRequest` / `XxxResponse`。 + +### 5.3 导入、格式、语言特性 +- 使用显式 import,避免 `*` 通配符导入。 +- 4 空格缩进,K&R 花括号风格,保持与现有代码一致。 +- 保持 Java 8 兼容,不引入高版本语法。 +- 文件编码 UTF-8。 + +### 5.4 参数校验与类型 +- 接口入参使用 DTO,并配合 `@Valid`、`@NotBlank` 等注解。 +- 业务模式值(如 `copy`/`move`)做显式校验,错误走业务异常。 +- 路径、文件系统相关逻辑先校验再执行,避免抛裸异常到接口层。 + +### 5.5 错误处理与返回结构 +- 统一返回 `Result`:成功 `Result.success(...)`,失败 `Result.failure(...)`。 +- 业务异常使用 `BusinessException(code, message)`。 +- 全局异常由 `GlobalExceptionHandler` 兜底处理。 +- 不向前端暴露堆栈或底层实现细节,错误信息保持可读。 +- 异步任务失败需通过进度消息体现失败状态与原因。 + +## 6. 前端代码规范(Vue 3 + TypeScript) + +### 6.1 组织与分层 +- 页面主逻辑放在 `src/components/*Tab.vue`。 +- 请求封装放在 `src/api/*.ts`,组件内不要直接拼 axios 细节。 +- WebSocket 逻辑集中在 `src/composables/useWebSocket.ts`。 +- 全局入口保持在 `src/main.ts`,避免散落初始化逻辑。 + +### 6.2 命名与类型 +- 组件文件使用 `PascalCase.vue`。 +- 组合式函数使用 `useXxx`。 +- TS 接口和类型用 `PascalCase`。 +- 变量和函数用 `camelCase`。 +- 避免 `any`;优先显式接口、联合类型或 `unknown` + 类型收窄。 + +### 6.3 编码风格 +- 使用 ESM 导入,默认相对路径。 +- 保持单引号、分号、2 空格缩进。 +- 默认使用 ` - diff --git a/frontend/src/components/ConvertTab.vue b/frontend/src/components/ConvertTab.vue index 9f14ec8..4a1fa52 100644 --- a/frontend/src/components/ConvertTab.vue +++ b/frontend/src/components/ConvertTab.vue @@ -5,10 +5,7 @@ @@ -186,40 +183,7 @@
@@ -229,24 +193,7 @@
-
-
-
待转码
-
{{ progress.total }}
-
-
-
已处理
-
{{ progress.processed }}
-
-
-
成功
-
{{ progress.success }}
-
-
-
失败
-
{{ progress.failed }}
-
-
+
@@ -296,9 +243,7 @@ import { VideoPlay, Refresh, DataLine, - Connection, CircleCheck, - Loading, Warning, Document, InfoFilled, @@ -308,6 +253,8 @@ import { startConvert } from '../api/convert'; import { getProgress } from '../api/progress'; import { useWebSocket, type ProgressMessage } from '../composables/useWebSocket'; import { getConfig } from '../api/config'; +import TaskCardHeader, { type CardHeaderStatus } from './common/TaskCardHeader.vue'; +import TaskStatsGrid, { type StatItem } from './common/TaskStatsGrid.vue'; interface Progress { taskId: string | null; @@ -351,6 +298,23 @@ const percentage = computed(() => { return Math.round((progress.processed / progress.total) * 100); }); +const connectionStatus = computed(() => { + if (!progress.taskId) { + return null; + } + if (progress.completed) { + return 'completed'; + } + return wsConnected.value ? 'connected' : 'connecting'; +}); + +const progressStats = computed(() => [ + { label: '待转码', value: progress.total }, + { label: '已处理', value: progress.processed, tone: 'success' }, + { label: '成功', value: progress.success, tone: 'success' }, + { label: '失败', value: progress.failed, tone: 'failed' } +]); + let wsDisconnect: (() => void) | null = null; let stopWatchingConnected: (() => void) | null = null; let stopWatchingError: (() => void) | null = null; @@ -618,150 +582,7 @@ onUnmounted(() => { diff --git a/frontend/src/components/DedupTab.vue b/frontend/src/components/DedupTab.vue index 4b92739..081cd6d 100644 --- a/frontend/src/components/DedupTab.vue +++ b/frontend/src/components/DedupTab.vue @@ -5,10 +5,7 @@ @@ -311,40 +308,7 @@
@@ -354,26 +318,7 @@
-
-
-
扫描文件数
-
{{ progress.scanned }}
-
-
-
重复组数量
-
{{ progress.duplicateGroups }}
-
-
-
移动/复制文件数
-
{{ progress.moved }}
-
-
-
完成状态
-
- {{ progress.completed ? '已完成' : '进行中' }} -
-
-
+
@@ -416,9 +361,7 @@ import { VideoPlay, Refresh, DataLine, - Connection, CircleCheck, - Loading, Document, InfoFilled, ArrowDown, @@ -430,6 +373,8 @@ import { startDedup } from '../api/dedup'; import { getProgress } from '../api/progress'; import { useWebSocket, type ProgressMessage } from '../composables/useWebSocket'; import { getConfig } from '../api/config'; +import TaskCardHeader, { type CardHeaderStatus } from './common/TaskCardHeader.vue'; +import TaskStatsGrid, { type StatItem } from './common/TaskStatsGrid.vue'; const form = reactive({ libraryDir: '', @@ -470,9 +415,39 @@ const percentage = computed(() => { return Math.round((scannedProcessed.value / scannedTotal.value) * 100); }); +const connectionStatus = computed(() => { + if (!progress.taskId) { + return null; + } + if (progress.completed) { + return 'completed'; + } + return wsConnected.value ? 'connected' : 'connecting'; +}); + +const progressStats = computed(() => [ + { label: '扫描文件数', value: progress.scanned }, + { label: '重复组数量', value: progress.duplicateGroups, tone: 'success' }, + { label: '移动/复制文件数', value: progress.moved, tone: 'success' }, + { label: '完成状态', value: progress.completed ? '已完成' : '进行中', tone: progress.completed ? 'success' : 'default' } +]); + let wsDisconnect: (() => void) | null = null; +let connectedWatchStop: (() => void) | null = null; let pollTimer: number | null = null; +function cleanupRealtime() { + if (connectedWatchStop) { + connectedWatchStop(); + connectedWatchStop = null; + } + if (wsDisconnect) { + wsDisconnect(); + wsDisconnect = null; + } + wsConnected.value = false; +} + function startPolling(taskId: string) { stopPolling(); pollTimer = window.setInterval(async () => { @@ -507,8 +482,8 @@ function handleProgressMessage(msg: ProgressMessage) { scannedProcessed.value = msg.processed; progress.scanned = msg.total; - progress.duplicateGroups = msg.success; - progress.moved = msg.failed; + progress.duplicateGroups = msg.duplicateGroups ?? msg.success; + progress.moved = msg.movedFiles ?? msg.failed; progress.completed = msg.completed; progress.message = msg.message ?? ''; } @@ -516,16 +491,12 @@ function handleProgressMessage(msg: ProgressMessage) { watch( () => progress.taskId, (newTaskId) => { - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } - wsConnected.value = false; + cleanupRealtime(); if (newTaskId) { const { connect, disconnect, connected } = useWebSocket(newTaskId, handleProgressMessage); wsDisconnect = disconnect; - watch(connected, (val) => (wsConnected.value = val), { immediate: true }); + connectedWatchStop = watch(connected, (val) => (wsConnected.value = val), { immediate: true }); connect(); startPolling(newTaskId); } @@ -579,10 +550,7 @@ watch( if (done) { submitting.value = false; stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); } } ); @@ -593,10 +561,7 @@ onMounted(() => { onUnmounted(() => { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); }); async function loadDefaultPaths() { @@ -613,10 +578,7 @@ async function loadDefaultPaths() { function reset() { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); form.useMd5 = false; form.useMetadata = true; form.mode = 'move'; @@ -629,130 +591,13 @@ function reset() { scannedTotal.value = 0; scannedProcessed.value = 0; submitting.value = false; - wsConnected.value = false; // 重新加载默认路径 loadDefaultPaths(); } - diff --git a/frontend/src/components/MergeTab.vue b/frontend/src/components/MergeTab.vue index b4f412b..c002755 100644 --- a/frontend/src/components/MergeTab.vue +++ b/frontend/src/components/MergeTab.vue @@ -5,10 +5,7 @@ @@ -220,40 +217,7 @@
@@ -263,26 +227,7 @@
-
-
-
已合并专辑数
-
{{ progress.albums }}
-
-
-
已合并曲目数
-
{{ progress.tracks }}
-
-
-
升级替换文件数
-
{{ progress.upgraded }}
-
-
-
完成状态
-
- {{ progress.completed ? '已完成' : '进行中' }} -
-
-
+
@@ -323,9 +268,7 @@ import { VideoPlay, Refresh, DataLine, - Connection, CircleCheck, - Loading, Document, InfoFilled, TrendCharts, @@ -339,6 +282,8 @@ import { startMerge } from '../api/merge'; import { getProgress } from '../api/progress'; import { useWebSocket, type ProgressMessage } from '../composables/useWebSocket'; import { getConfig } from '../api/config'; +import TaskCardHeader, { type CardHeaderStatus } from './common/TaskCardHeader.vue'; +import TaskStatsGrid, { type StatItem } from './common/TaskStatsGrid.vue'; const form = reactive({ srcDir: '', @@ -376,9 +321,39 @@ const percentage = computed(() => { return Math.round((processedFiles.value / totalFiles.value) * 100); }); +const connectionStatus = computed(() => { + if (!progress.taskId) { + return null; + } + if (progress.completed) { + return 'completed'; + } + return wsConnected.value ? 'connected' : 'connecting'; +}); + +const progressStats = computed(() => [ + { label: '已合并专辑数', value: progress.albums, tone: 'success' }, + { label: '已合并曲目数', value: progress.tracks, tone: 'success' }, + { label: '升级替换文件数', value: progress.upgraded }, + { label: '完成状态', value: progress.completed ? '已完成' : '进行中', tone: progress.completed ? 'success' : 'default' } +]); + let wsDisconnect: (() => void) | null = null; +let connectedWatchStop: (() => void) | null = null; let pollTimer: number | null = null; +function cleanupRealtime() { + if (connectedWatchStop) { + connectedWatchStop(); + connectedWatchStop = null; + } + if (wsDisconnect) { + wsDisconnect(); + wsDisconnect = null; + } + wsConnected.value = false; +} + function startPolling(taskId: string) { stopPolling(); pollTimer = window.setInterval(async () => { @@ -409,44 +384,25 @@ function handleProgressMessage(msg: ProgressMessage) { } // 字段映射:见后端 LibraryMergeService 注释 - // success 字段存储专辑数,failed 字段存储曲目数 totalFiles.value = msg.total; processedFiles.value = msg.processed; - progress.albums = msg.success; - progress.tracks = msg.failed; + progress.albums = msg.albumsMerged ?? msg.success; + progress.tracks = msg.tracksMerged ?? msg.failed; + progress.upgraded = msg.upgradedFiles ?? progress.upgraded; progress.completed = msg.completed; progress.message = msg.message ?? ''; - - // 从消息中提取升级数量(如果消息中包含) - if (msg.message) { - // 匹配 "升级: 5" 或 "升级:5" 或 "(升级: 5)" 等格式 - const upgradeMatch = msg.message.match(/升级[::]\s*(\d+)/); - if (upgradeMatch) { - progress.upgraded = parseInt(upgradeMatch[1], 10); - } else { - // 如果没有找到,尝试从完成消息中提取 - const finalMatch = msg.message.match(/升级[::]\s*(\d+)/); - if (finalMatch) { - progress.upgraded = parseInt(finalMatch[1], 10); - } - } - } } watch( () => progress.taskId, (newTaskId) => { - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } - wsConnected.value = false; + cleanupRealtime(); if (newTaskId) { const { connect, disconnect, connected } = useWebSocket(newTaskId, handleProgressMessage); wsDisconnect = disconnect; - watch(connected, (val) => (wsConnected.value = val), { immediate: true }); + connectedWatchStop = watch(connected, (val) => (wsConnected.value = val), { immediate: true }); connect(); startPolling(newTaskId); } @@ -494,10 +450,7 @@ watch( if (done) { submitting.value = false; stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); } } ); @@ -508,10 +461,7 @@ onMounted(() => { onUnmounted(() => { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); }); async function loadDefaultPaths() { @@ -528,10 +478,7 @@ async function loadDefaultPaths() { function reset() { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); form.smartUpgrade = true; form.keepBackup = false; progress.taskId = null; @@ -543,130 +490,13 @@ function reset() { totalFiles.value = 0; processedFiles.value = 0; submitting.value = false; - wsConnected.value = false; // 重新加载默认路径 loadDefaultPaths(); } - diff --git a/frontend/src/components/RenameTab.vue b/frontend/src/components/RenameTab.vue index 62ba563..fcd814f 100644 --- a/frontend/src/components/RenameTab.vue +++ b/frontend/src/components/RenameTab.vue @@ -5,10 +5,7 @@ @@ -208,40 +205,7 @@
@@ -250,24 +214,7 @@
-
-
-
扫描文件数
-
{{ progress.total }}
-
-
-
已处理
-
{{ progress.processed }}
-
-
-
整理成功
-
{{ progress.organized }}
-
-
-
需人工修复
-
{{ progress.manualFix }}
-
-
+
{ return Math.round((progress.processed / progress.total) * 100); }); +const connectionStatus = computed(() => { + if (!progress.taskId) { + return null; + } + if (progress.completed) { + return 'completed'; + } + return wsConnected.value ? 'connected' : 'connecting'; +}); + +const progressStats = computed(() => [ + { label: '扫描文件数', value: progress.total }, + { label: '已处理', value: progress.processed }, + { label: '整理成功', value: progress.organized, tone: 'success' }, + { label: '需人工修复', value: progress.manualFix, tone: progress.manualFix > 0 ? 'warning' : 'default' } +]); + let wsDisconnect: (() => void) | null = null; +let connectedWatchStop: (() => void) | null = null; let pollTimer: number | null = null; +function cleanupRealtime() { + if (connectedWatchStop) { + connectedWatchStop(); + connectedWatchStop = null; + } + if (wsDisconnect) { + wsDisconnect(); + wsDisconnect = null; + } + wsConnected.value = false; +} + function startPolling(taskId: string) { stopPolling(); pollTimer = window.setInterval(async () => { @@ -393,8 +370,8 @@ function handleProgressMessage(msg: ProgressMessage) { progress.total = msg.total; progress.processed = msg.processed; - progress.organized = msg.success; - progress.manualFix = msg.failed; + progress.organized = msg.organizedFiles ?? msg.success; + progress.manualFix = msg.manualFixFiles ?? msg.failed; progress.currentFile = msg.currentFile ?? ''; progress.message = msg.message ?? ''; progress.completed = msg.completed; @@ -412,16 +389,12 @@ function handleProgressMessage(msg: ProgressMessage) { watch( () => progress.taskId, (newTaskId) => { - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } - wsConnected.value = false; + cleanupRealtime(); if (newTaskId) { const { connect, disconnect, connected } = useWebSocket(newTaskId, handleProgressMessage); wsDisconnect = disconnect; - watch(connected, (val) => (wsConnected.value = val), { immediate: true }); + connectedWatchStop = watch(connected, (val) => (wsConnected.value = val), { immediate: true }); connect(); startPolling(newTaskId); } @@ -476,10 +449,7 @@ watch( if (done) { submitting.value = false; stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); } } ); @@ -490,10 +460,7 @@ onMounted(() => { onUnmounted(() => { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); }); async function loadDefaultPaths() { @@ -510,10 +477,7 @@ async function loadDefaultPaths() { function reset() { stopPolling(); - if (wsDisconnect) { - wsDisconnect(); - wsDisconnect = null; - } + cleanupRealtime(); form.mode = 'strict'; form.extractCover = true; form.extractLyrics = true; @@ -527,62 +491,13 @@ function reset() { progress.message = ''; progress.completed = false; submitting.value = false; - wsConnected.value = false; // 重新加载默认路径 loadDefaultPaths(); } diff --git a/frontend/src/components/SettingsTab.vue b/frontend/src/components/SettingsTab.vue index 2087e7d..5af78be 100644 --- a/frontend/src/components/SettingsTab.vue +++ b/frontend/src/components/SettingsTab.vue @@ -1,10 +1,7 @@