feat: 功能优化
This commit is contained in:
134
.claude/commands/plan.md
Normal file
134
.claude/commands/plan.md
Normal file
@@ -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. 批准后开始实施
|
||||
```
|
||||
@@ -498,7 +498,7 @@ const startPollingTask = () => {
|
||||
} catch (error) {
|
||||
console.error('polling error:', error)
|
||||
}
|
||||
}, 2000) // 每2秒轮询一次
|
||||
}, 10000) // 每2秒轮询一次
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
@@ -868,7 +868,7 @@ let previewObjectUrl = ''
|
||||
</div>
|
||||
|
||||
<!-- 语音合成 -->
|
||||
<div class="tts-actions">
|
||||
<!-- <div class="tts-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@@ -899,7 +899,7 @@ let previewObjectUrl = ''
|
||||
<div v-else class="synth-audio-hint">
|
||||
先生成语音,再上传视频,即可开始混剪
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 视频上传 -->
|
||||
<div class="video-section">
|
||||
@@ -953,24 +953,6 @@ let previewObjectUrl = ''
|
||||
<span class="status-value">{{ getStepText(currentTaskStep) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a-button
|
||||
v-if="currentTaskStatus === 'PROCESSING'"
|
||||
danger
|
||||
block
|
||||
@click="handleCancelTask"
|
||||
>
|
||||
取消任务
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="currentTaskStatus === 'FAILED' || currentTaskStatus === 'CANCELED'"
|
||||
type="primary"
|
||||
block
|
||||
@click="handleRetryTask"
|
||||
>
|
||||
重试任务
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-progress
|
||||
|
||||
@@ -70,7 +70,6 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
* Redis缓存键前缀
|
||||
*/
|
||||
private static final String REDIS_TASK_RESULT_PREFIX = "digital_human:task:result:";
|
||||
private static final String REDIS_TASK_STATUS_PREFIX = "digital_human:task:status:";
|
||||
|
||||
/**
|
||||
* 缓存过期时间(24小时)
|
||||
@@ -429,7 +428,7 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
updateTaskProgress(taskId, DigitalHumanTaskStepEnum.SYNTHESIZE_VOICE, "语音合成完成");
|
||||
|
||||
// 步骤3:口型同步(异步提交,不等待完成)
|
||||
String syncedVideoUrl = syncLip(task, audioUrl);
|
||||
syncLip(task, audioUrl);
|
||||
updateTaskProgress(taskId, DigitalHumanTaskStepEnum.SYNC_LIP, "口型同步任务已提交,等待处理");
|
||||
|
||||
// Latentsync是异步处理,这里不调用generateVideo
|
||||
@@ -584,21 +583,6 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
return task.getVideoUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成视频
|
||||
*/
|
||||
private String generateVideo(TikDigitalHumanTaskDO task, String syncedVideoUrl) throws Exception {
|
||||
log.info("[generateVideo][任务({})开始生成视频]", task.getId());
|
||||
|
||||
// TODO: 这里可以添加视频后处理逻辑,比如添加字幕、特效等
|
||||
|
||||
// 保存同步后的视频到OSS
|
||||
String resultVideoUrl = saveVideoToOss(task, syncedVideoUrl);
|
||||
|
||||
log.info("[generateVideo][任务({})视频生成完成]", task.getId());
|
||||
return resultVideoUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
*/
|
||||
@@ -635,9 +619,6 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
|
||||
taskMapper.updateById(updateObj);
|
||||
|
||||
// 缓存状态(支持增量轮询)
|
||||
cacheTaskStatus(taskId, status, progress);
|
||||
|
||||
log.info("[updateTaskStatus][任务({})状态更新: status={}, progress={}%]", taskId, status, progress);
|
||||
}
|
||||
|
||||
@@ -667,70 +648,6 @@ public class DigitalHumanTaskServiceImpl implements DigitalHumanTaskService {
|
||||
updateTaskStatus(taskId, "PROCESSING", step.getStep(), step.getProgress(), message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存视频到OSS - 流式处理优化内存
|
||||
*/
|
||||
private String saveVideoToOss(TikDigitalHumanTaskDO task, String remoteVideoUrl) throws Exception {
|
||||
log.info("[saveVideoToOss][任务({})开始下载并保存视频到OSS][remoteUrl={}]", task.getId(), remoteVideoUrl);
|
||||
|
||||
try {
|
||||
// 1. 下载远程视频文件(流式处理避免OOM)
|
||||
byte[] videoBytes = downloadRemoteFile(remoteVideoUrl);
|
||||
|
||||
// 2. 内存检查:超过50MB记录警告
|
||||
int sizeMB = videoBytes.length / 1024 / 1024;
|
||||
if (sizeMB > 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user