This commit is contained in:
2026-03-11 00:42:46 +08:00
parent a125b5922f
commit a12cb5e75a
23 changed files with 1593 additions and 101 deletions

View File

@@ -3,11 +3,13 @@ package cn.iocoder.yudao.module.tik.dify.controller;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.tik.dify.service.DifyService;
import cn.iocoder.yudao.module.tik.dify.vo.DifyBenchmarkReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyConversationListRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyMessageListRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.ForecastRewriteReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.PromptAnalysisReqVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -51,6 +53,20 @@ public class AppDifyController {
.map(CommonResult::success);
}
@PostMapping(value = "/benchmark/analyze", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "对标分析(流式)")
public Flux<CommonResult<DifyChatRespVO>> benchmarkAnalyzeStream(@Valid @RequestBody DifyBenchmarkReqVO reqVO) {
return difyService.benchmarkAnalyzeStream(reqVO, getCurrentUserId())
.map(CommonResult::success);
}
@PostMapping(value = "/prompt/analysis", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "提示词分析(流式)")
public Flux<CommonResult<DifyChatRespVO>> promptAnalysisStream(@Valid @RequestBody PromptAnalysisReqVO reqVO) {
return difyService.promptAnalysisStream(reqVO, getCurrentUserId())
.map(CommonResult::success);
}
@GetMapping("/conversations")
@Operation(summary = "获取会话列表")
@Parameter(name = "agentId", description = "智能体ID", required = true)

View File

@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.tik.dify.service;
import cn.iocoder.yudao.module.tik.dify.vo.DifyBenchmarkReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.PromptAnalysisReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyConversationListRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyMessageListRespVO;
@@ -32,6 +34,24 @@ public interface DifyService {
*/
Flux<DifyChatRespVO> rewriteStream(ForecastRewriteReqVO reqVO, String userId);
/**
* 对标分析(流式)
*
* @param reqVO 请求参数
* @param userId 用户ID
* @return 流式响应
*/
Flux<DifyChatRespVO> benchmarkAnalyzeStream(DifyBenchmarkReqVO reqVO, String userId);
/**
* 提示词分析(流式)
*
* @param reqVO 请求参数
* @param userId 用户ID
* @return 流式响应
*/
Flux<DifyChatRespVO> promptAnalysisStream(PromptAnalysisReqVO reqVO, String userId);
/**
* 获取会话列表
*

View File

@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.tik.dify.service;
import cn.iocoder.yudao.module.tik.dify.client.DifyClient;
import cn.iocoder.yudao.module.tik.dify.vo.DifyBenchmarkReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.PromptAnalysisReqVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyChatRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyConversationListRespVO;
import cn.iocoder.yudao.module.tik.dify.vo.DifyMessageListRespVO;
@@ -181,10 +183,23 @@ public class DifyServiceImpl implements DifyService {
String difyUserId = "user-" + userId;
return Mono.fromCallable(() -> {
// 1. 获取智能体配置(通过 agentId 获取 systemPrompt
AiAgentDO agent = aiAgentService.getAiAgent(reqVO.getAgentId());
if (agent == null) {
throw new RuntimeException("智能体不存在");
// 1. 获取系统提示词
String systemPrompt;
if (reqVO.getCustomSystemPrompt() != null && !reqVO.getCustomSystemPrompt().isEmpty()) {
// 使用自定义系统提示词(用户风格)
systemPrompt = reqVO.getCustomSystemPrompt();
log.info("[rewriteStream] 使用自定义系统提示词,长度: {}", systemPrompt.length());
} else if (reqVO.getAgentId() != null) {
// 从智能体获取系统提示词
AiAgentDO agent = aiAgentService.getAiAgent(reqVO.getAgentId());
if (agent == null) {
throw new RuntimeException("智能体不存在");
}
systemPrompt = agent.getSystemPrompt();
log.info("[rewriteStream] 使用智能体系统提示词agentId: {}, 长度: {}",
reqVO.getAgentId(), systemPrompt != null ? systemPrompt.length() : 0);
} else {
throw new RuntimeException("必须提供 agentId 或 customSystemPrompt");
}
// 2. 根据 modelType 获取对应的积分配置
@@ -208,17 +223,14 @@ public class DifyServiceImpl implements DifyService {
);
pendingRecordId.set(recordId);
// 5. 构建 inputs 参数(使用 agent 的 systemPrompt
// 5. 构建 inputs 参数
Map<String, Object> inputs = new HashMap<>();
inputs.put("sysPrompt", agent.getSystemPrompt());
inputs.put("sysPrompt", systemPrompt);
inputs.put("userText", reqVO.getUserText());
inputs.put("level", reqVO.getLevel());
// 调试日志
log.info("[rewriteStream] 请求参数 - agentId: {}, modelType: {}", reqVO.getAgentId(), reqVO.getModelType());
log.info("[rewriteStream] Agent信息 - id: {}, name: {}, systemPrompt长度: {}",
agent.getId(), agent.getAgentName(),
agent.getSystemPrompt() != null ? agent.getSystemPrompt().length() : 0);
log.info("[rewriteStream] inputs参数 - userText长度: {}, level: {}",
reqVO.getUserText() != null ? reqVO.getUserText().length() : 0,
reqVO.getLevel());
@@ -324,6 +336,323 @@ public class DifyServiceImpl implements DifyService {
*/
private record ForecastRewriteContext(Map<String, Object> inputs, String apiKey, Integer consumePoints) {}
@Override
public Flux<DifyChatRespVO> benchmarkAnalyzeStream(DifyBenchmarkReqVO reqVO, String userId) {
// 用于存储预扣记录ID
AtomicLong pendingRecordId = new AtomicLong();
// 用于存储会话ID
AtomicReference<String> conversationIdRef = new AtomicReference<>("");
// 用于存储 token 使用信息
AtomicReference<DifyChatRespVO> tokenUsageRef = new AtomicReference<>();
// Dify 用户标识(固定格式)
String difyUserId = "user-" + userId;
return Mono.fromCallable(() -> {
// 1. 获取积分配置
AiServiceConfigDO config = pointsService.getConfig(
AiPlatformEnum.DIFY.getPlatform(),
AiModelTypeEnum.BENCHMARK_ANALYZE.getModelCode());
// 2. 预检积分
pointsService.checkPoints(userId, config.getConsumePoints());
// 3. 创建预扣记录(带 serviceCode
Long recordId = pointsService.createPendingDeduct(
userId,
config.getConsumePoints(),
"benchmark_analyze",
String.valueOf(reqVO.getVideoCount()),
config.getServiceCode()
);
pendingRecordId.set(recordId);
// 4. 构建对标分析提示词
String benchmarkPrompt = buildBenchmarkPrompt(reqVO.getContent(), reqVO.getVideoCount());
log.info("[benchmarkAnalyzeStream] 请求参数 - videoCount: {}", reqVO.getVideoCount());
log.info("[benchmarkAnalyzeStream] 提示词长度: {}", benchmarkPrompt.length());
log.info("[benchmarkAnalyzeStream] API配置 - apiKey前缀: {}..., consumePoints: {}",
config.getApiKey() != null && config.getApiKey().length() > 10
? config.getApiKey().substring(0, 10) + "***"
: "null",
config.getConsumePoints());
// 5. 返回调用参数
return new BenchmarkAnalyzeContext(benchmarkPrompt, config.getApiKey(), config.getConsumePoints());
})
.flatMapMany(context -> {
// 6. 调用 Dify 流式 API
return difyClient.chatStream(
context.apiKey(),
context.prompt(),
null, // systemPrompt 为空,使用 Dify 工作流中的配置
null, // conversationId 为空,新会话
difyUserId
)
.doOnNext(resp -> {
if (resp.getConversationId() != null) {
conversationIdRef.set(resp.getConversationId());
}
// 捕获 token 使用信息
if (resp.getTotalTokens() != null && resp.getTotalTokens() > 0) {
tokenUsageRef.set(resp);
}
})
// 7. 流结束时确认扣费(带 token
.doOnComplete(() -> {
if (pendingRecordId.get() > 0) {
try {
DifyChatRespVO tokenUsage = tokenUsageRef.get();
if (tokenUsage != null) {
pointsService.confirmPendingDeductWithTokens(
pendingRecordId.get(),
tokenUsage.getInputTokens(),
tokenUsage.getOutputTokens(),
tokenUsage.getTotalTokens()
);
log.info("[benchmarkAnalyzeStream] 流结束确认扣费带token记录ID: {}, tokens: {}",
pendingRecordId.get(), tokenUsage.getTotalTokens());
} else {
pointsService.confirmPendingDeduct(pendingRecordId.get());
log.info("[benchmarkAnalyzeStream] 流结束确认扣费无token记录ID: {}", pendingRecordId.get());
}
} catch (Exception e) {
log.error("[benchmarkAnalyzeStream] 确认扣费失败", e);
}
}
})
// 8. 流出错时取消预扣
.doOnError(e -> {
if (pendingRecordId.get() > 0) {
try {
pointsService.cancelPendingDeduct(pendingRecordId.get());
log.info("[benchmarkAnalyzeStream] 流出错取消预扣记录ID: {}", pendingRecordId.get());
} catch (Exception ex) {
log.error("[benchmarkAnalyzeStream] 取消预扣失败", ex);
}
}
})
// 9. 用户取消时确认扣费(已消费的部分)
.doOnCancel(() -> {
if (pendingRecordId.get() > 0) {
try {
// 用户主动取消,仍然扣费(按最低消费)
pointsService.confirmPendingDeduct(pendingRecordId.get());
log.info("[benchmarkAnalyzeStream] 用户取消确认扣费记录ID: {}", pendingRecordId.get());
} catch (Exception e) {
log.error("[benchmarkAnalyzeStream] 用户取消后扣费失败", e);
}
}
});
})
// 10. 在最后添加 done 事件
.concatWith(Mono.defer(() -> {
DifyChatRespVO tokenUsage = tokenUsageRef.get();
if (tokenUsage != null) {
return Mono.just(DifyChatRespVO.doneWithTokens(
conversationIdRef.get(), null,
tokenUsage.getInputTokens(),
tokenUsage.getOutputTokens(),
tokenUsage.getTotalTokens()
));
}
return Mono.just(DifyChatRespVO.done(conversationIdRef.get(), null));
}))
.onErrorResume(e -> {
log.error("[benchmarkAnalyzeStream] 对标分析异常", e);
return Flux.just(DifyChatRespVO.error(e.getMessage()));
});
}
/**
* 构建对标分析提示词
*/
private String buildBenchmarkPrompt(String content, Integer videoCount) {
return String.format("""
请分析以下 %d 个爆款视频的文案,提取出它们的共同特点和创作风格,生成一个综合的文案创作提示词。
文案内容:
%s
请从以下维度进行分析:
1. 文案结构和节奏
2. 常用的表达方式和句式
3. 情感调动技巧
4. 互动引导方式
5. 关键词和热词使用
请生成一个可以直接用于文案创作的提示词,格式要求:
- 清晰描述这种文案风格的核心特点
- 包含具体的写作指导
- 适合作为 AI 写作的 system prompt
""", videoCount, content);
}
/**
* 对标分析上下文
*/
private record BenchmarkAnalyzeContext(String prompt, String apiKey, Integer consumePoints) {}
@Override
public Flux<DifyChatRespVO> promptAnalysisStream(PromptAnalysisReqVO reqVO, String userId) {
// 用于存储预扣记录ID
AtomicLong pendingRecordId = new AtomicLong();
// 用于存储会话ID
AtomicReference<String> conversationIdRef = new AtomicReference<>("");
// 用于存储 token 使用信息
AtomicReference<DifyChatRespVO> tokenUsageRef = new AtomicReference<>();
// Dify 用户标识(固定格式)
String difyUserId = "user-" + userId;
return Mono.fromCallable(() -> {
// 1. 获取积分配置
AiServiceConfigDO config = pointsService.getConfig(
AiPlatformEnum.DIFY.getPlatform(),
AiModelTypeEnum.PROMPT_ANALYSIS.getModelCode());
// 2. 预检积分
pointsService.checkPoints(userId, config.getConsumePoints());
// 3. 创建预扣记录(带 serviceCode
Long recordId = pointsService.createPendingDeduct(
userId,
config.getConsumePoints(),
"prompt_analysis",
String.valueOf(reqVO.getVideoCount()),
config.getServiceCode()
);
pendingRecordId.set(recordId);
// 4. 构建提示词分析 prompt
String analysisPrompt = buildPromptAnalysisPrompt(reqVO.getContent(), reqVO.getVideoCount());
log.info("[promptAnalysisStream] 请求参数 - videoCount: {}", reqVO.getVideoCount());
log.info("[promptAnalysisStream] 提示词长度: {}", analysisPrompt.length());
log.info("[promptAnalysisStream] API配置 - apiKey前缀: {}..., consumePoints: {}",
config.getApiKey() != null && config.getApiKey().length() > 10
? config.getApiKey().substring(0, 10) + "***"
: "null",
config.getConsumePoints());
// 5. 返回调用参数
return new PromptAnalysisContext(analysisPrompt, config.getApiKey(), config.getConsumePoints());
})
.flatMapMany(context -> {
// 6. 调用 Dify 流式 API
return difyClient.chatStream(
context.apiKey(),
context.prompt(),
null, // systemPrompt 为空,使用 Dify 工作流中的配置
null, // conversationId 为空,新会话
difyUserId
)
.doOnNext(resp -> {
if (resp.getConversationId() != null) {
conversationIdRef.set(resp.getConversationId());
}
// 捕获 token 使用信息
if (resp.getTotalTokens() != null && resp.getTotalTokens() > 0) {
tokenUsageRef.set(resp);
}
})
// 7. 流结束时确认扣费(带 token
.doOnComplete(() -> {
if (pendingRecordId.get() > 0) {
try {
DifyChatRespVO tokenUsage = tokenUsageRef.get();
if (tokenUsage != null) {
pointsService.confirmPendingDeductWithTokens(
pendingRecordId.get(),
tokenUsage.getInputTokens(),
tokenUsage.getOutputTokens(),
tokenUsage.getTotalTokens()
);
log.info("[promptAnalysisStream] 流结束确认扣费带token记录ID: {}, tokens: {}",
pendingRecordId.get(), tokenUsage.getTotalTokens());
} else {
pointsService.confirmPendingDeduct(pendingRecordId.get());
log.info("[promptAnalysisStream] 流结束确认扣费无token记录ID: {}", pendingRecordId.get());
}
} catch (Exception e) {
log.error("[promptAnalysisStream] 确认扣费失败", e);
}
}
})
// 8. 流出错时取消预扣
.doOnError(e -> {
if (pendingRecordId.get() > 0) {
try {
pointsService.cancelPendingDeduct(pendingRecordId.get());
log.info("[promptAnalysisStream] 流出错取消预扣记录ID: {}", pendingRecordId.get());
} catch (Exception ex) {
log.error("[promptAnalysisStream] 取消预扣失败", ex);
}
}
})
// 9. 用户取消时确认扣费(已消费的部分)
.doOnCancel(() -> {
if (pendingRecordId.get() > 0) {
try {
// 用户主动取消,仍然扣费(按最低消费)
pointsService.confirmPendingDeduct(pendingRecordId.get());
log.info("[promptAnalysisStream] 用户取消确认扣费记录ID: {}", pendingRecordId.get());
} catch (Exception e) {
log.error("[promptAnalysisStream] 用户取消后扣费失败", e);
}
}
});
})
// 10. 在最后添加 done 事件
.concatWith(Mono.defer(() -> {
DifyChatRespVO tokenUsage = tokenUsageRef.get();
if (tokenUsage != null) {
return Mono.just(DifyChatRespVO.doneWithTokens(
conversationIdRef.get(), null,
tokenUsage.getInputTokens(),
tokenUsage.getOutputTokens(),
tokenUsage.getTotalTokens()
));
}
return Mono.just(DifyChatRespVO.done(conversationIdRef.get(), null));
}))
.onErrorResume(e -> {
log.error("[promptAnalysisStream] 提示词分析异常", e);
return Flux.just(DifyChatRespVO.error(e.getMessage()));
});
}
/**
* 构建提示词分析 prompt
*/
private String buildPromptAnalysisPrompt(String content, Integer videoCount) {
return String.format("""
请分析以下 %d 个爆款视频的文案,提取出它们的共同特点和创作风格,生成一个可以直接用于 AI 写作的系统提示词。
文案内容:
%s
请从以下维度进行深度分析:
1. 文案结构和节奏特点
2. 常用的表达方式和句式模板
3. 情感调动和共鸣技巧
4. 互动引导和行动号召
5. 关键词、热词和流行语使用
6. 开头和结尾的特点
输出要求:
- 生成一个完整的系统提示词System Prompt
- 提示词应该清晰描述这种文案风格的核心特点
- 包含具体的写作指导和格式要求
- 适合直接作为 AI 写作的系统指令
- 使用专业但易懂的语言
""", videoCount != null ? videoCount : 1, content);
}
/**
* 提示词分析上下文
*/
private record PromptAnalysisContext(String prompt, String apiKey, Integer consumePoints) {}
@Override
public DifyConversationListRespVO getConversations(Long agentId, String userId, String lastId, Integer limit) {
// 获取智能体配置

View File

@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.tik.dify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 对标分析请求 VO
*/
@Schema(description = "对标分析请求")
@Data
public class DifyBenchmarkReqVO {
@Schema(description = "合并后的文案内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "文案内容不能为空")
private String content;
@Schema(description = "视频数量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "视频数量不能为空")
private Integer videoCount;
}

View File

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.tik.dify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
@@ -12,8 +11,7 @@ import lombok.Data;
@Data
public class ForecastRewriteReqVO {
@Schema(description = "智能体ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "智能体ID不能为空")
@Schema(description = "智能体ID(使用用户风格时可为空)")
private Long agentId;
@Schema(description = "用户输入文案", requiredMode = Schema.RequiredMode.REQUIRED)
@@ -26,4 +24,7 @@ public class ForecastRewriteReqVO {
@Schema(description = "模型类型forecast_standard-标准版 forecast_pro-pro版", example = "forecast_standard")
private String modelType = "forecast_standard";
@Schema(description = "自定义系统提示词(使用用户风格时传入)")
private String customSystemPrompt;
}

View File

@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.tik.dify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* 提示词分析请求 VO
*/
@Schema(description = "提示词分析请求")
@Data
public class PromptAnalysisReqVO {
@Schema(description = "合并后的文案内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "文案内容不能为空")
private String content;
@Schema(description = "视频数量", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer videoCount;
}

View File

@@ -24,6 +24,12 @@ public enum AiModelTypeEnum implements ArrayValuable<String> {
FORECAST_STANDARD("forecast_standard", "文案改写-标准版", AiPlatformEnum.DIFY, "text"),
FORECAST_PRO("forecast_pro", "文案改写-Pro版", AiPlatformEnum.DIFY, "text"),
// ========== 对标分析 ==========
BENCHMARK_ANALYZE("benchmark_analyze", "对标分析", AiPlatformEnum.DIFY, "text"),
// ========== 提示词分析 ==========
PROMPT_ANALYSIS("prompt_analysis", "提示词分析", AiPlatformEnum.DIFY, "text"),
// ========== 数字人模型 ==========
DIGITAL_HUMAN_LATENTSYNC("latentsync", "LatentSync", AiPlatformEnum.DIGITAL_HUMAN, "video"),
DIGITAL_HUMAN_KLING("kling", "可灵", AiPlatformEnum.DIGITAL_HUMAN, "video"),

View File

@@ -107,5 +107,13 @@ public class AppUserPromptController {
PageResult<UserPromptDO> pageResult = userPromptService.getUserPromptPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, UserPromptRespVO.class));
}
@GetMapping("/list")
@Operation(summary = "获得用户提示词列表(简化版)")
public CommonResult<java.util.List<UserPromptRespVO>> getUserPromptList() {
// 只查询当前用户的提示词
java.util.List<UserPromptDO> list = userPromptService.getUserPromptListByUserId(getLoginUserId());
return success(BeanUtils.toBean(list, UserPromptRespVO.class));
}
}

View File

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptDO;
import cn.iocoder.yudao.module.tik.userprompt.vo.UserPromptPageReqVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 用户提示词 Mapper
*
@@ -30,4 +32,12 @@ public interface UserPromptMapper extends BaseMapperX<UserPromptDO> {
.orderByDesc(UserPromptDO::getId));
}
default List<UserPromptDO> selectList(Long userId) {
return selectList(new LambdaQueryWrapperX<UserPromptDO>()
.eq(UserPromptDO::getUserId, userId)
.eq(UserPromptDO::getStatus, 1)
.orderByDesc(UserPromptDO::getSort)
.orderByDesc(UserPromptDO::getId));
}
}

View File

@@ -60,4 +60,12 @@ public interface UserPromptService {
*/
PageResult<UserPromptDO> getUserPromptPage(UserPromptPageReqVO pageReqVO);
/**
* 根据用户ID获取用户提示词列表
*
* @param userId 用户ID
* @return 用户提示词列表
*/
List<UserPromptDO> getUserPromptListByUserId(Long userId);
}

View File

@@ -92,4 +92,9 @@ public class UserPromptServiceImpl implements UserPromptService {
public PageResult<UserPromptDO> getUserPromptPage(UserPromptPageReqVO pageReqVO) {
return userPromptMapper.selectPage(pageReqVO);
}
@Override
public List<UserPromptDO> getUserPromptListByUserId(Long userId) {
return userPromptMapper.selectList(userId);
}
}