package vip.mate.tool.builtin;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import vip.mate.system.model.SystemSettingsDTO;
import vip.mate.system.service.SystemSettingService;
import vip.mate.task.AsyncTaskService;
import vip.mate.task.model.AsyncTaskInfo;
import vip.mate.tool.video.*;
import java.util.List;
import java.util.StringJoiner;
/**
* 视频生成工具 — Agent 可调用的 @Tool,提交异步视频生成任务
*
* 借鉴 OpenClaw 的 video-generate-tool.ts 设计,支持 action=generate/list/status,
* 以及 session 级重复提交防护。
*
* @author MateClaw Team
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VideoGenerateTool {
private final VideoGenerationService videoGenerationService;
private final VideoProviderRegistry providerRegistry;
private final SystemSettingService systemSettingService;
private final AsyncTaskService asyncTaskService;
@vip.mate.tool.ConcurrencyUnsafe("creates async tasks and persists generated artifacts; provider rate limits also forbid parallel calls")
@Tool(description = "视频生成工具,支持以下 action:\n"
+ "- generate(默认):生成视频。提供 prompt 描述视频内容,可选 aspectRatio/duration/imageUrl/model\n"
+ "- list:列出所有可用的视频 Provider 及其支持的模型和能力\n"
+ "- status:查看当前会话中正在进行的视频生成任务状态\n"
+ "视频生成是异步过程(1-5 分钟),完成后自动显示在对话中。")
public String video_generate(
@ToolParam(description = "操作类型: generate(生成视频)、list(列出可用 Provider)、status(查看任务状态),默认 generate", required = false) String action,
@ToolParam(description = "视频内容描述,尽量详细(generate 时必填)", required = false) String prompt,
@ToolParam(description = "画面比例: 16:9 / 9:16 / 1:1,默认 16:9", required = false) String aspectRatio,
@ToolParam(description = "视频时长(秒),如 5 或 10,默认 5", required = false) Integer duration,
@ToolParam(description = "参考图片 URL(图生视频模式)", required = false) String imageUrl,
@ToolParam(description = "指定模型名称(可选)", required = false) String model,
@ToolParam(description = "查询指定任务 ID 的状态(status 模式时使用)", required = false) String taskId,
// RFC-063r §2.5: ToolContext is auto-injected by Spring AI MethodToolCallback
// and explicitly skipped by JsonSchemaGenerator — never visible to the LLM.
@Nullable ToolContext ctx
) {
// 路由 action
String normalizedAction = (action == null || action.isBlank()) ? "generate" : action.trim().toLowerCase();
return switch (normalizedAction) {
case "list" -> handleListAction();
case "status" -> handleStatusAction(taskId, ctx);
default -> handleGenerateAction(prompt, aspectRatio, duration, imageUrl, model, ctx);
};
}
// ==================== action=list ====================
private String handleListAction() {
SystemSettingsDTO config = systemSettingService.getAllSettings();
List providers = providerRegistry.allSorted();
if (providers.isEmpty()) {
return "当前没有注册的视频生成 Provider。";
}
StringJoiner sb = new StringJoiner("\n\n");
sb.add("## 可用的视频生成 Provider\n");
for (VideoGenerationProvider p : providers) {
boolean available = p.isAvailable(config);
VideoProviderCapabilities caps = p.detailedCapabilities();
StringJoiner entry = new StringJoiner("\n");
entry.add("### " + p.label() + " (" + p.id() + ") " + (available ? "[已配置]" : "[未配置]"));
if (caps != null) {
entry.add("- 模式: " + caps.getModes());
if (caps.getModels() != null && !caps.getModels().isEmpty()) {
entry.add("- 模型: " + String.join(", ", caps.getModels()));
}
entry.add("- 画面比例: " + String.join(", ", caps.getAspectRatios()));
entry.add("- 支持时长: " + caps.getSupportedDurations() + " 秒");
}
sb.add(entry.toString());
}
return sb.toString();
}
// ==================== action=status ====================
private String handleStatusAction(String taskId, @Nullable ToolContext ctx) {
String conversationId = ToolExecutionContext.conversationId(ctx);
// 指定 taskId 查询
if (taskId != null && !taskId.isBlank()) {
AsyncTaskInfo info = videoGenerationService.checkTaskStatus(taskId);
if (info == null) {
return "未找到任务 ID: " + taskId;
}
return formatTaskStatus(info);
}
// 查询当前会话的所有活跃任务
if (conversationId == null) {
return "无法获取当前会话信息";
}
List activeTasks = asyncTaskService.listActiveTasks(conversationId);
if (activeTasks.isEmpty()) {
return "当前会话没有进行中的视频生成任务。";
}
StringJoiner sb = new StringJoiner("\n");
sb.add("当前会话有 " + activeTasks.size() + " 个进行中的任务:");
for (AsyncTaskInfo task : activeTasks) {
sb.add("- 任务 " + task.getTaskId() + ": " + formatTaskStatus(task));
}
return sb.toString();
}
// ==================== action=generate ====================
private String handleGenerateAction(String prompt, String aspectRatio, Integer duration,
String imageUrl, String model, @Nullable ToolContext ctx) {
String conversationId = ToolExecutionContext.conversationId(ctx);
String username = ToolExecutionContext.username(ctx);
if (conversationId == null || conversationId.isBlank()) {
return "错误:无法获取当前会话信息,请重试";
}
if (prompt == null || prompt.isBlank()) {
return "错误:prompt 为必填参数,请描述你想要生成的视频内容";
}
// Session 级重复提交防护(借鉴 OpenClaw 的 duplicateGuard)
List activeTasks = asyncTaskService.listActiveTasks(conversationId);
long videoTasks = activeTasks.stream()
.filter(t -> "video_generation".equals(t.getTaskType()))
.count();
if (videoTasks > 0) {
AsyncTaskInfo existing = activeTasks.stream()
.filter(t -> "video_generation".equals(t.getTaskType()))
.findFirst().orElse(null);
return "当前会话已有一个视频生成任务正在进行中(任务 ID: "
+ (existing != null ? existing.getTaskId() : "unknown")
+ ")。请等待完成后再提交新任务,或使用 action=status 查看进度。";
}
VideoGenerationRequest request = VideoGenerationRequest.builder()
.prompt(prompt)
.aspectRatio(aspectRatio)
.durationSeconds(duration)
.imageUrl(imageUrl)
.model(model)
.build();
VideoGenerationResult result = videoGenerationService.submitGeneration(
request, conversationId, username != null ? username : "system");
if (result.isSubmitted()) {
return result.getMessage();
} else {
return "视频生成失败:" + result.getMessage();
}
}
// ==================== 辅助方法 ====================
private String formatTaskStatus(AsyncTaskInfo info) {
return switch (info.getStatus()) {
case "pending" -> "排队中,请稍候...";
case "running" -> {
String progressStr = info.getProgress() != null && info.getProgress() > 0
? "(进度: " + info.getProgress() + "%)" : "";
yield "生成中" + progressStr + "(" + info.getProviderName() + ")";
}
case "succeeded" -> "已完成,视频已显示在对话中";
case "failed" -> "失败:" + (info.getErrorMessage() != null ? info.getErrorMessage() : "未知错误");
default -> "状态: " + info.getStatus();
};
}
}