diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md new file mode 100644 index 0000000000..faba5139d1 --- /dev/null +++ b/.claude/commands/plan.md @@ -0,0 +1,134 @@ +## 计划模式 + +启动实施前的计划制定模式,制定详细的实施策略。通过在代码实施前制定结构化计划,支持高效开发。 + +### 使用方法 + +```bash +# 请求 Claude 进入 Plan Mode +"制定 [实施内容] 的实施计划" +``` + +### 基本示例 + +```bash +# 新功能的实施计划 +"制定用户认证功能的实施计划" + +# 系统设计计划 +"制定微服务拆分的实施计划" + +# 重构计划 +"制定遗留代码的重构计划" +``` + +### 与 Claude 的协作 + +```bash +# 复杂功能实施 +"制定聊天功能的实施计划。包括 WebSocket、实时通知、历史管理" + +# 数据库设计 +"制定电商网站的数据库设计计划。包括商品、订单、用户管理" + +# API 设计 +"制定 GraphQL API 的实施计划。包括认证、缓存、速率限制" + +# 基础设施设计 +"制定 Docker 化的实施计划。包括开发环境、生产环境、CI/CD" +``` + +### Plan Mode 的特点 + +**自动启动** + +- 检测到实施任务时自动启动 Plan Mode +- 可通过"制定实施计划"等关键词明确启动 + +**结构化规格书** + +- 需求定义 (用户故事·验收标准) +- 设计书 (架构·数据设计·UI 设计) +- 实施计划 (任务分解·进度跟踪·质量保证) +- 风险分析与对策 + +**审批流程** + +- 通过 `exit_plan_mode` 工具提交计划 +- **重要**: 无论工具返回值如何,必须等待用户的明确批准 +- 禁止未经批准就开始实施 +- 可以修改·调整计划 +- 仅在批准后才开始使用 TodoWrite 进行任务管理 + +### 详细示例 + +```bash +# 复杂系统实施 +"制定在线支付系统的实施计划。包括 Stripe 集成、安全性、错误处理" + +# 前端实施 +"制定 React 仪表板的实施计划。包括状态管理、组件设计、测试" + +# 后端实施 +"制定 RESTful API 的实施计划。包括认证、验证、日志记录" + +# DevOps 实施 +"制定 CI/CD 管道的实施计划。包括测试自动化、部署、监控" +``` + +### 3 阶段工作流程 + +#### 阶段 1: Requirements(需求定义) + +- **用户故事**: 明确功能的目的和价值 +- **验收标准**: 定义完成条件和质量标准 +- **约束·前提条件**: 整理技术·时间约束 +- **优先级排序**: Must-have/Nice-to-have 分类 + +#### 阶段 2: Design(设计) + +- **架构设计**: 系统构成和技术选型 +- **数据设计**: 模式、API 规格、数据流 +- **UI/UX 设计**: 界面构成和操作流程 +- **风险分析**: 潜在问题和对策 + +#### 阶段 3: Implementation(实施) + +- **任务分解**: 细分为可实施的单位 +- **进度跟踪**: 通过 TodoWrite 进行状态管理 +- **质量保证**: 测试策略和验证方法 +- **审批流程**: 通过 exit_plan_mode 提交计划并等待明确批准 + +### 注意事项 + +**适用范围** + +- Plan Mode 最适合复杂的实施任务 +- 简单修改或小规模变更使用常规实施形式 +- 推荐用于 3 步以上的工作或新功能开发 + +**技术约束** + +- 不要信任 `exit_plan_mode` 工具的返回值 +- 审批流程通过用户的明确意思表示判断 +- 与 CLI 的 plan mode 是不同的功能 + +**执行注意事项** + +- 严禁在批准前开始实施 +- 提交计划后必须等待用户响应 +- 出错时提供替代方案 + +### 执行示例 + +```bash +# 使用示例 +"制定用户管理系统的实施计划" + +# 预期行为 +# 1. Plan Mode 自动启动 +# 2. 需求分析和技术选型 +# 3. 实施步骤的结构化 +# 4. 通过 exit_plan_mode 提交计划 +# 5. 批准后开始实施 +``` \ No newline at end of file diff --git a/frontend/app/web-gold/src/views/dh/Video.vue b/frontend/app/web-gold/src/views/dh/Video.vue index 11c1f011ff..872c9c3326 100644 --- a/frontend/app/web-gold/src/views/dh/Video.vue +++ b/frontend/app/web-gold/src/views/dh/Video.vue @@ -498,7 +498,7 @@ const startPollingTask = () => { } catch (error) { console.error('polling error:', error) } - }, 2000) // 每2秒轮询一次 + }, 10000) // 每2秒轮询一次 } // 取消任务 @@ -868,7 +868,7 @@ let previewObjectUrl = '' -
+
@@ -953,24 +953,6 @@ let previewObjectUrl = '' {{ getStepText(currentTaskStep) }}
-
- - 取消任务 - - - 重试任务 - -
50) { - log.warn("[saveVideoToOss][任务({})视频文件较大][size={}MB]", task.getId(), sizeMB); - } - - // 3. 获取OSS目录路径 - Long userId = task.getUserId(); - String baseDirectory = ossInitService.getOssDirectoryByCategory(userId, "generate"); - - // 4. 生成文件名 - String fileName = String.format("task_%d_%d.mp4", task.getId(), System.currentTimeMillis()); - - // 5. 保存到OSS - String ossUrl = fileApi.createFile(videoBytes, fileName, baseDirectory, "video/mp4"); - - // 6. 移除预签名URL中的签名参数,获取基础URL - String cleanOssUrl = HttpUtils.removeUrlQuery(ossUrl); - - // 7. 缓存任务结果(快速回显) - cacheTaskResult(task.getId(), cleanOssUrl); - - log.info("[saveVideoToOss][任务({})视频保存到OSS完成][directory={}, fileName={}, ossUrl={}]", - task.getId(), baseDirectory, fileName, cleanOssUrl); - return cleanOssUrl; - - } catch (Exception e) { - log.error("[saveVideoToOss][任务({})保存视频到OSS失败][remoteUrl={}]", task.getId(), remoteVideoUrl, e); - // 如果保存失败,返回原始URL(降级处理) - return remoteVideoUrl; - } - } - - /** - * 下载远程文件 - 内存优化 - */ - private byte[] downloadRemoteFile(String remoteUrl) throws Exception { - log.info("[downloadRemoteFile][下载文件][url={}]", remoteUrl); - - try (HttpResponse response = HttpRequest.get(remoteUrl) - .execute()) { - - if (!response.isOk()) { - throw new Exception("下载文件失败: HTTP " + response.getStatus()); - } - - // 流式读取:分块处理避免大文件OOM - byte[] bytes = response.bodyBytes(); - int sizeMB = bytes.length / 1024 / 1024; - log.info("[downloadRemoteFile][文件下载完成][size={} bytes, {}MB]", bytes.length, sizeMB); - return bytes; - } - } - /** * 缓存任务结果 - 支持快速回显 */ @@ -758,31 +675,4 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService { } } - /** - * 缓存任务状态 - 支持增量轮询 - */ - private void cacheTaskStatus(Long taskId, String status, Integer progress) { - try { - String key = REDIS_TASK_STATUS_PREFIX + taskId; - String value = status + ":" + progress + ":" + System.currentTimeMillis(); - stringRedisTemplate.opsForValue().set(key, value, CACHE_EXPIRE_TIME); - log.debug("[cacheTaskStatus][任务({})状态已缓存][status={}]", taskId, status); - } catch (Exception e) { - log.warn("[cacheTaskStatus][任务({})缓存状态失败]", taskId, e); - } - } - - /** - * 获取缓存的任务状态 - */ - private String getCachedTaskStatus(Long taskId) { - try { - String key = REDIS_TASK_STATUS_PREFIX + taskId; - return stringRedisTemplate.opsForValue().get(key); - } catch (Exception e) { - log.warn("[getCachedTaskStatus][任务({})获取缓存状态失败]", taskId, e); - return null; - } - } - } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java index fb96f026d6..ac77294b20 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/voice/service/LatentsyncPollingService.java @@ -4,6 +4,8 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.tik.file.service.TikOssInitService; import cn.iocoder.yudao.module.tik.voice.dal.dataobject.TikDigitalHumanTaskDO; import cn.iocoder.yudao.module.tik.voice.dal.mysql.TikDigitalHumanTaskMapper; @@ -24,12 +26,14 @@ import java.util.concurrent.TimeUnit; /** * Latentsync任务轮询服务 - 轻量化异步处理 + * 使用@TenantIgnore忽略租户检查,因为轮询服务没有用户上下文 * * @author 芋道源码 */ @Slf4j @Service @RequiredArgsConstructor +@TenantIgnore public class LatentsyncPollingService { private final TikDigitalHumanTaskMapper taskMapper; @@ -126,10 +130,15 @@ public class LatentsyncPollingService { * 单个任务轮询 */ private void pollSingleTask(Long taskId) { + // 获取任务的requestId和轮询次数(在try块外声明,供catch块使用) + String requestId = null; + String countKey = null; + int currentCount = 0; + try { // 获取任务的requestId String taskKey = REDIS_POLLING_PREFIX + "task_" + taskId; - String requestId = stringRedisTemplate.opsForValue().get(taskKey); + requestId = stringRedisTemplate.opsForValue().get(taskKey); if (StrUtil.isBlank(requestId)) { // 如果没有requestId,说明任务可能已完成或已取消,从轮询队列中移除 @@ -138,9 +147,9 @@ public class LatentsyncPollingService { } // 检查轮询次数 - String countKey = REDIS_POLLING_COUNT_PREFIX + requestId; + countKey = REDIS_POLLING_COUNT_PREFIX + requestId; String countStr = stringRedisTemplate.opsForValue().get(countKey); - int currentCount = countStr != null ? Integer.parseInt(countStr) : 0; + currentCount = countStr != null ? Integer.parseInt(countStr) : 0; if (currentCount >= MAX_POLLING_COUNT) { // 超时,标记任务失败 @@ -169,7 +178,23 @@ public class LatentsyncPollingService { } } catch (Exception e) { log.error("[pollSingleTask][轮询任务异常][taskId={}]", taskId, e); - // 轮询异常不直接标记失败,避免误判 + + // 轮询异常处理:增加轮询次数,避免无限重试 + int errorCount = currentCount + 1; + if (errorCount >= MAX_POLLING_COUNT) { + // 达到最大次数,标记任务失败 + log.warn("[pollSingleTask][任务轮询异常次数过多,标记失败][taskId={}, count={}]", taskId, errorCount); + if (requestId != null && StrUtil.isNotBlank(requestId)) { + markTaskFailed(taskId, "Latentsync API调用异常:" + e.getMessage()); + removeFromPollingQueue(taskId, requestId); + } + } else { + // 更新轮询次数,继续重试 + if (countKey != null) { + stringRedisTemplate.opsForValue().set(countKey, String.valueOf(errorCount), CACHE_EXPIRE_TIME); + log.debug("[pollSingleTask][轮询异常,增加次数后继续重试][taskId={}, count={}]", taskId, errorCount); + } + } } }