From f63e6b92bd33fa8f087269686354f596fddac41e Mon Sep 17 00:00:00 2001 From: shenaowei <450702724@qq.com> Date: Mon, 23 Feb 2026 23:36:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tikhup/controller/TikHupController.java | 109 +---- .../tik/tikhup/service/TikHupService.java | 26 +- .../tik/tikhup/service/TikHupServiceImpl.java | 382 ++++++++++-------- 3 files changed, 245 insertions(+), 272 deletions(-) diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java index e4e9e41899..d6a74ff7bf 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/controller/TikHupController.java @@ -3,21 +3,12 @@ package cn.iocoder.yudao.module.tik.tikhup.controller; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.tik.tikhup.mapper.TikPromptMapper; import cn.iocoder.yudao.module.tik.tikhup.service.TikHupService; -import com.alibaba.dashscope.app.Application; -import com.alibaba.dashscope.app.ApplicationParam; -import com.alibaba.dashscope.app.ApplicationResult; -import com.alibaba.dashscope.app.FlowStreamMode; -import com.alibaba.dashscope.utils.Constants; -import com.alibaba.dashscope.utils.JsonUtils; -import com.alibaba.fastjson.JSON; -import io.reactivex.Flowable; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; import java.util.List; import java.util.Map; @@ -30,118 +21,50 @@ import java.util.Map; public class TikHupController { private final TikHupService tikHupService; - private final TikPromptMapper tikPromptMapper; - @GetMapping("/fetch_user_post_videos") - @Operation( - summary = "获取用户主页作品数据", - description = "通过TikHup API获取指定用户的TikTok作品数据,包括视频列表、用户信息等" - ) - public Object fetch_user_post_videos( - @RequestParam String type, - @RequestParam String sec_user_id, - @RequestParam int max_cursor, - @RequestParam int count) { - return tikHupService.fetch_user_post_videos(type, sec_user_id,max_cursor, count); - } - - @PostMapping("/postTikHup") - @Operation( - summary = "获取用户主页作品数据", - description = "通过TikHup API获取指定用户的TikTok作品数据,包括视频列表、用户信息等" - ) - public Object postTikHup(@RequestParam String type,@RequestParam String methodType,@RequestParam String urlParams,@RequestParam String paramType) { - return tikHupService.postTikHup(type, methodType,urlParams,paramType); + @Operation(summary = "获取用户主页作品数据", description = "通过TikHup API获取指定用户的TikTok作品数据") + public Object postTikHup(@RequestParam String type, + @RequestParam String methodType, + @RequestParam String urlParams, + @RequestParam String paramType) { + return tikHupService.postTikHup(type, methodType, urlParams, paramType); } - @PostMapping("/videoToCharacters") @Operation(summary = "音频转文字", description = "音频转文字接口") - public Object videoToCharacters(@RequestBody Map fileLinkMap) { + public Object videoToCharacters(@RequestBody Map fileLinkMap) { String fileLink = (String) fileLinkMap.get("fileLink"); return tikHupService.videoToCharacters(fileLink); } @PostMapping("/videoToCharacters2") - @Operation(summary = "音频转文字", description = "音频转文字接口") - public Object videoToCharacters2(@RequestBody Map fileLinkMap) { + @Operation(summary = "批量音频转文字", description = "批量音频转文字接口") + public Object videoToCharacters2(@RequestBody Map fileLinkMap) { + @SuppressWarnings("unchecked") List fileLinkList = (List) fileLinkMap.get("fileLinkList"); return tikHupService.videoToCharacters2(fileLinkList); } - - @GetMapping("/getPromptTypeList") @Operation(summary = "获取提示词类型列表", description = "获取提示词类型列表") - public Object getPromptTypeList(){ + public Object getPromptTypeList() { return CommonResult.success(tikPromptMapper.getTikPromptVOList()); } - @PostMapping("/deepseekAnalysis") @Operation(summary = "deepseek文案重写", description = "deepseek文案重写") - public Object deepseekAnalysis(@RequestBody Map contentMap){ + public Object deepseekAnalysis(@RequestBody Map contentMap) { String content = (String) contentMap.get("content"); String promptType = (String) contentMap.get("promptType"); - return tikHupService.deepseekAnalysis(promptType,content); + return tikHupService.deepseekAnalysis(promptType, content); } @PostMapping("/callWorkflow") - @Operation(summary = "调用阿里工作流", description = "调用阿里工作流 文档地址:https://help.aliyun.com/zh/model-studio/invoke-workflow-application?mode=pure#8f4b4d2ce9fry") - public Flux callWorkflow(@RequestBody Map params){ - Constants.baseHttpApiUrl = "https://dashscope.aliyuncs.com/api/v1"; - ApplicationParam param = ApplicationParam.builder() - // 若没有配置环境变量,可用百炼API Key将下行替换为:.apiKey("sk-xxx")。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。 - .apiKey("sk-10c746f8cb8640738f8d6b71af699003") - .appId("bb746b74bfb0439e841cb309dc9f945c") - .flowStreamMode(FlowStreamMode.MESSAGE_FORMAT) - .bizParams(JsonUtils.parse(JSON.toJSONString(params))) - .prompt((String) params.get("audio_prompt")) - .hasThoughts(false) - .incrementalOutput(true) - .build(); - Application application = new Application(); - try{ - //ApplicationResult result = application.call(param); - //return CommonResult.success(result.getOutput().getText()); - Flowable flowableResult = application.streamCall(param); - // 关键:RxJava Flowable 转 WebFlux Flux,指定非阻塞线程调度 - // 4.2 将 RxJava Flowable 转换为 WebFlux Flux(响应式转换,无阻塞) - return Flux.from(flowableResult) - // 关键:切换到非阻塞线程执行(避免阻塞 WebFlux 事件循环) - .subscribeOn(Schedulers.boundedElastic()) - // 4.3 处理每一条流式数据:提取 thoughts 字段(按你的需求,也可提取 text) - .map(applicationResult -> { - // 校验结果非 null(阿里 SDK 可能返回空结果) - if (applicationResult == null || applicationResult.getOutput() == null) { - log.warn("收到空的流式结果"); - return "空结果"; - } - return applicationResult.getOutput().getWorkflowMessage().getMessage().getContent(); - }) - // 【新增】设置超时时间:30分钟内没有数据则超时 - .timeout(java.time.Duration.ofMinutes(30)) - // 【新增】超时后返回错误信息 - .onErrorResume(java.util.concurrent.TimeoutException.class, e -> { - log.warn("流式输出超时(30分钟无数据),连接已断开"); - return Flux.just("[TIMEOUT]流式输出超时,请检查网络或重新请求"); - }) - // 【新增】流式输出完成后,发送结束标记 - .concatWith(Flux.just("[DONE]")) - // 4.4 流处理异常捕获(如网络中断、阿里 SDK 内部错误) - .onErrorMap(e -> { - log.error("流式处理异常", e); // 打印完整异常栈,便于排查 - return new RuntimeException("流式处理错误:" + e.getMessage()); - }) - // 4.5 异常时返回默认提示(前端不会断连,而是接收错误信息) - .onErrorReturn("错误:流式处理中断:" + "请重试或联系管理员"); - }catch (Exception e){ - // 捕获 SDK 调用时的同步异常(如初始化失败、网络连接超时) - log.error("调用阿里 SDK 失败", e); - return Flux.just("错误:SDK 调用失败:" + e.getMessage()); - } + @Operation(summary = "调用阿里工作流", description = "调用阿里工作流进行流式处理") + public Flux callWorkflow(@RequestBody Map params) { + return tikHupService.callWorkflow(params); } } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java index c8170a5fb6..376e8ee2a1 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupService.java @@ -1,20 +1,13 @@ package cn.iocoder.yudao.module.tik.tikhup.service; +import reactor.core.publisher.Flux; + import java.util.List; +import java.util.Map; public interface TikHupService { - /** - * 获取用户主页作品数据 - * @param sec_user_id 类型 - * @param sec_user_id 用户 - * @param count 查询总数 - * @return Response - */ - Object fetch_user_post_videos(String type,String sec_user_id,int max_cursor, int count); - - - Object postTikHup(String type,String methodType,String urlParams,String paramType); + Object postTikHup(String type, String methodType, String urlParams, String paramType); /** * 音频转文字 @@ -31,11 +24,18 @@ public interface TikHupService { Object videoToCharacters2(List fileLinkList); /** - * + * deepseek 文案改写 * @param promptType 提示词类型 * @param content 提示词内容 * @return deepseek改写后的内容 */ - Object deepseekAnalysis(String promptType,String content); + Object deepseekAnalysis(String promptType, String content); + + /** + * 调用阿里云工作流 + * @param params 业务参数 + * @return 流式响应 + */ + Flux callWorkflow(Map params); } diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java index db680ab4da..ac5621d50d 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/tikhup/service/TikHupServiceImpl.java @@ -8,85 +8,62 @@ import cn.iocoder.yudao.module.tik.tikhup.mapper.TikPromptMapper; import cn.iocoder.yudao.module.tik.tikhup.mapper.TikTokenMapper; import cn.iocoder.yudao.module.tik.tikhup.vo.TikPromptVO; import cn.iocoder.yudao.module.tik.tikhup.vo.TikTokenVO; +import com.alibaba.dashscope.app.Application; +import com.alibaba.dashscope.app.ApplicationParam; +import com.alibaba.dashscope.app.ApplicationResult; +import com.alibaba.dashscope.app.FlowStreamMode; import com.alibaba.dashscope.audio.asr.transcription.Transcription; import com.alibaba.dashscope.audio.asr.transcription.TranscriptionParam; import com.alibaba.dashscope.audio.asr.transcription.TranscriptionQueryParam; import com.alibaba.dashscope.audio.asr.transcription.TranscriptionResult; +import com.alibaba.dashscope.utils.Constants; +import com.alibaba.dashscope.utils.JsonUtils; import com.alibaba.fastjson.JSON; import com.google.gson.GsonBuilder; import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; +import io.reactivex.Flowable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; +import java.time.Duration; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; @Slf4j @Service @RequiredArgsConstructor public class TikHupServiceImpl implements TikHupService { - /** TikHub 平台标识 */ + // 平台和模型标识 private static final String PLATFORM_TIKHUB = "tikhub"; - /** TikHub 数据抓取模型类型 */ - private static final String MODEL_TYPE_FETCH = "fetch"; + private static final String MODEL_TYPE_CRAWLER = "crawler"; + + // API 配置(建议后续迁移到配置文件) + private static final String ALIYUN_API_URL = "https://dashscope.aliyuncs.com/api/v1"; + private static final String ALIYUN_API_KEY = "sk-10c746f8cb8640738f8d6b71af699003"; + private static final String ALIYUN_APP_ID = "bb746b74bfb0439e841cb309dc9f945c"; + private static final String DEEPSEEK_API_KEY = "sk-7f666f993b144d279ae375a015e4de56"; + private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions"; + + // 阿里云语音识别配置 + private final String appKey = "sldJ4XSpYp3rKALZ"; + private final String accessKeyId = "LTAI5tPV9Ag3csf41GZjaLTA"; + private final String accessKeySecret = "kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs"; + + // 流式处理配置 + private static final Duration STREAM_TIMEOUT = Duration.ofMinutes(30); private final TikTokenMapper tikTokenMapper; - private final TikPromptMapper tikPromptMapper; - private final PointsService pointsService; - @Override - public Object fetch_user_post_videos(String type,String sec_user_id, int max_cursor, int count){ - // 1. 获取当前用户ID - Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); - String userId = loginUserId != null ? loginUserId.toString() : "1"; - - // 2. 获取积分配置并预检 - AiModelConfigDO config = null; - try { - config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_TYPE_FETCH); - pointsService.checkPoints(userId, config.getConsumePoints()); - } catch (Exception e) { - log.error("[fetch_user_post_videos] 积分预检失败: {}", e.getMessage()); - return CommonResult.error(400, e.getMessage()); - } - - String url = StringUtils.EMPTY; - if("xiaohongshu".equals(type)){ - url = "https://api.tikhub.io/api/v1/xiaohongshu/app/get_user_info"; - } else if("tik-web".equals(type)){ - url = "https://api.tikhub.io/api/v1/douyin/web/fetch_user_post_videos"; - } else if("tik-app".equals(type)){ - url = "https://api.tikhub.io/api/v1/douyin/app/v3/fetch_user_post_videos"; - } - TikTokenVO tikTokenVO = tikTokenMapper.getPlatformToken(url); - String Authorization = tikTokenVO.getPlatformToken(); - try{ - Unirest.setTimeouts(0, 0); - HttpResponse response = Unirest.get(url+"?sec_user_id="+sec_user_id+"&max_cursor="+max_cursor+"&count="+count) - .header("Authorization", "Bearer "+Authorization) - .asString(); - if(response.getStatus() == 200){ - // 3. API 调用成功,扣减积分 - try { - pointsService.deductPoints(userId, config.getConsumePoints(), "tikhub_fetch", type); - log.info("[fetch_user_post_videos] 用户 {} 扣减 {} 积分", userId, config.getConsumePoints()); - } catch (Exception e) { - log.error("[fetch_user_post_videos] 积分扣减失败: {}", e.getMessage()); - } - return JSON.parseObject(response.getBody()); - } - }catch (Exception e){ - log.error("fetch_user_post_videos接口调用异常"); - } - return new HashMap<>(); - } - @Override public Object postTikHup(String type, String methodType, String urlParams, String paramType) { // 1. 参数校验 @@ -102,182 +79,255 @@ public class TikHupServiceImpl implements TikHupService { // 2. 获取当前用户ID并预检积分 Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); String userId = loginUserId != null ? loginUserId.toString() : "1"; - AiModelConfigDO config = null; + AiModelConfigDO config; try { - config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_TYPE_FETCH); + config = pointsService.getConfig(PLATFORM_TIKHUB, MODEL_TYPE_CRAWLER); pointsService.checkPoints(userId, config.getConsumePoints()); } catch (Exception e) { log.error("[postTikHup] 积分预检失败: {}", e.getMessage()); return CommonResult.error(400, e.getMessage()); } - // 3. 获取接口配置信息 + // 3. 从 muye_ai_model_config 获取 apiKey + String authorization = config.getApiKey(); + if (StringUtils.isBlank(authorization)) { + log.error("postTikHup: TikHub 配置的 apiKey 为空"); + return CommonResult.error(500, "接口配置错误:apiKey 为空"); + } + + // 4. 获取接口 URL TikTokenVO tikTokenVO = tikTokenMapper.getInterfaceUrl(type); if (tikTokenVO == null) { - log.error("postTikHup: 未找到接口类型 {} 的配置信息", type); - return CommonResult.error(404, "未找到接口类型 " + type + " 的配置信息"); + log.error("postTikHup: 未找到接口类型 {} 的 URL 配置", type); + return CommonResult.error(404, "未找到接口类型 " + type + " 的 URL 配置"); } - - String authorization = tikTokenVO.getPlatformToken(); String url = tikTokenVO.getPlatformUrl(); - - if (StringUtils.isBlank(authorization)) { - log.error("postTikHup: 接口类型 {} 的 token 为空", type); - return CommonResult.error(500, "接口配置错误:token 为空"); - } if (StringUtils.isBlank(url)) { log.error("postTikHup: 接口类型 {} 的 URL 为空", type); return CommonResult.error(500, "接口配置错误:URL 为空"); } - // 4. 统一转换为小写进行比较(兼容大小写) + // 5. 统一转换为小写进行比较 String methodTypeLower = methodType.toLowerCase(); String paramTypeLower = paramType != null ? paramType.toLowerCase() : ""; try { Unirest.setTimeouts(0, 0); - HttpResponse response; + HttpResponse response = executeRequest(url, urlParams, authorization, methodTypeLower, paramTypeLower); - // 5. 根据请求方法和参数类型构建请求 - if ("post".equals(methodTypeLower) && "json".equals(paramTypeLower)) { - // POST + JSON: 将 urlParams 作为 JSON body - log.debug("postTikHup: POST JSON 请求, URL: {}, Body: {}", url, urlParams); - response = Unirest.post(url) - .header("Authorization", "Bearer " + authorization) - .header("Content-Type", "application/json") - .body(urlParams) - .asString(); - } else if ("post".equals(methodTypeLower)) { - // POST + 表单参数: 将 urlParams 作为 URL 查询参数 - log.debug("postTikHup: POST 表单请求, URL: {}?{}", url, urlParams); - response = Unirest.post(url + "?" + urlParams) - .header("Authorization", "Bearer " + authorization) - .asString(); - } else { - // GET 或其他方法: 将 urlParams 作为 URL 查询参数 - // 处理 URL 拼接:如果 URL 已包含查询参数,使用 & 连接,否则使用 ? 连接 - String finalUrl = url; - if (urlParams != null && !urlParams.trim().isEmpty()) { - if (url.contains("?")) { - // URL 已包含查询参数,使用 & 连接 - finalUrl = url + "&" + urlParams; - } else { - // URL 不包含查询参数,使用 ? 连接 - finalUrl = url + "?" + urlParams; - } - } - log.info("postTikHup: GET 请求, 原始URL: {}, 参数: {}, 最终URL: {}", url, urlParams, finalUrl); - response = Unirest.get(finalUrl) - .header("Authorization", "Bearer " + authorization) - .asString(); - } - - // 6. 检查响应状态码 - int statusCode = response.getStatus(); - String responseBody = response.getBody(); - - if (statusCode == 200) { - if (StringUtils.isNotBlank(responseBody)) { - // 7. API 调用成功,扣减积分 - try { - pointsService.deductPoints(userId, config.getConsumePoints(), "tikhub_fetch", type); - log.info("[postTikHup] 用户 {} 扣减 {} 积分", userId, config.getConsumePoints()); - } catch (Exception e) { - log.error("[postTikHup] 积分扣减失败: {}", e.getMessage()); - } - // 尝试解析为 JSON,如果失败则直接返回字符串 - try { - return JSON.parseObject(responseBody); - } catch (Exception e) { - // 如果不是 JSON,直接返回字符串 - return responseBody; - } - } else { - log.warn("postTikHup: 接口返回空响应, URL: {}", url); - return CommonResult.error(500, "接口返回空响应"); - } - } else { - log.error("postTikHup: 接口调用失败, URL: {}, 状态码: {}, 响应: {}", url, statusCode, responseBody); - return CommonResult.error(statusCode, "接口调用失败: " + (StringUtils.isNotBlank(responseBody) ? responseBody : "HTTP " + statusCode)); - } + // 6. 处理响应 + return handleResponse(response, url, userId, config.getConsumePoints(), type); } catch (Exception e) { log.error("postTikHup: 接口调用异常, URL: {}, 错误信息: {}", url, e.getMessage(), e); return CommonResult.error(500, "接口调用异常: " + e.getMessage()); } } - private final String appKey = "sldJ4XSpYp3rKALZ "; - private final String accessKeyId = "LTAI5tPV9Ag3csf41GZjaLTA"; - private final String accessKeySecret = "kDqlGeJTKw6tJtFYiaY8vQTFuVIQDs"; - private final String deepseekApiKey="sk-7f666f993b144d279ae375a015e4de56"; + private HttpResponse executeRequest(String url, String urlParams, String authorization, + String methodTypeLower, String paramTypeLower) throws Exception { + if ("post".equals(methodTypeLower) && "json".equals(paramTypeLower)) { + log.debug("postTikHup: POST JSON 请求, URL: {}, Body: {}", url, urlParams); + return Unirest.post(url) + .header("Authorization", "Bearer " + authorization) + .header("Content-Type", "application/json") + .body(urlParams) + .asString(); + } else if ("post".equals(methodTypeLower)) { + log.debug("postTikHup: POST 表单请求, URL: {}?{}", url, urlParams); + return Unirest.post(url + "?" + urlParams) + .header("Authorization", "Bearer " + authorization) + .asString(); + } else { + String finalUrl = buildUrlWithParams(url, urlParams); + log.info("postTikHup: GET 请求, 最终URL: {}", finalUrl); + return Unirest.get(finalUrl) + .header("Authorization", "Bearer " + authorization) + .asString(); + } + } + + private String buildUrlWithParams(String url, String urlParams) { + if (StringUtils.isBlank(urlParams)) { + return url; + } + return url.contains("?") ? url + "&" + urlParams : url + "?" + urlParams; + } + + private Object handleResponse(HttpResponse response, String url, String userId, + Integer consumePoints, String type) { + int statusCode = response.getStatus(); + String responseBody = response.getBody(); + + if (statusCode != 200) { + log.error("postTikHup: 接口调用失败, URL: {}, 状态码: {}, 响应: {}", url, statusCode, responseBody); + return CommonResult.error(statusCode, + "接口调用失败: " + (StringUtils.isNotBlank(responseBody) ? responseBody : "HTTP " + statusCode)); + } + + if (StringUtils.isBlank(responseBody)) { + log.warn("postTikHup: 接口返回空响应, URL: {}", url); + return CommonResult.error(500, "接口返回空响应"); + } + + // 扣减积分 + deductPointsSafely(userId, consumePoints, type); + + // 尝试解析 JSON,失败则返回原始字符串 + try { + return JSON.parseObject(responseBody); + } catch (Exception e) { + return responseBody; + } + } + + private void deductPointsSafely(String userId, Integer consumePoints, String type) { + try { + pointsService.deductPoints(userId, consumePoints, "tikhub_fetch", type); + log.info("[postTikHup] 用户 {} 扣减 {} 积分", userId, consumePoints); + } catch (Exception e) { + log.error("[postTikHup] 积分扣减失败: {}", e.getMessage()); + } + } @Override - public Object videoToCharacters(String fileLink){ - log.info("[videoToCharacters][开始识别,文件链接({})]", fileLink); - TikFileTransCharacters tikFileTransCharacters = new TikFileTransCharacters(accessKeyId, accessKeySecret); - String taskId = tikFileTransCharacters.submitFileTransRequest(appKey, fileLink); + public Object videoToCharacters(String fileLink) { + log.info("[videoToCharacters] 开始识别,文件链接: {}", fileLink); + TikFileTransCharacters transCharacters = new TikFileTransCharacters(accessKeyId, accessKeySecret); + String taskId = transCharacters.submitFileTransRequest(appKey, fileLink); + if (taskId == null) { - log.error("[videoToCharacters][提交识别请求失败,taskId为null,fileLink({})]", fileLink); - return CommonResult.error(500,"录音文件识别请求失败!"); + log.error("[videoToCharacters] 提交识别请求失败,taskId为null,fileLink: {}", fileLink); + return CommonResult.error(500, "录音文件识别请求失败!"); } - log.info("[videoToCharacters][提交识别请求成功,taskId({})]", taskId); - String transResult = tikFileTransCharacters.getFileTransResult(taskId); + + log.info("[videoToCharacters] 提交识别请求成功,taskId: {}", taskId); + String transResult = transCharacters.getFileTransResult(taskId); + if (transResult == null) { - log.error("[videoToCharacters][识别结果查询失败,taskId({}),transResult为null]", taskId); - return CommonResult.error(501,"录音文件识别请求失败!"); + log.error("[videoToCharacters] 识别结果查询失败,taskId: {}", taskId); + return CommonResult.error(501, "录音文件识别请求失败!"); } - log.info("[videoToCharacters][识别成功,taskId({}),结果长度({})]", taskId, transResult.length()); + + log.info("[videoToCharacters] 识别成功,taskId: {},结果长度: {}", taskId, transResult.length()); return CommonResult.success(transResult); } - private final String apiKey = "sk-10c746f8cb8640738f8d6b71af699003"; - @Override - public Object videoToCharacters2(List fileLinkList){ - log.info("[videoToCharacters2][开始识别,文件数量({}),文件URL({})]", + public Object videoToCharacters2(List fileLinkList) { + log.info("[videoToCharacters2] 开始识别,文件数量: {},文件URL: {}", fileLinkList != null ? fileLinkList.size() : 0, fileLinkList); + TranscriptionParam param = TranscriptionParam.builder() - .apiKey(apiKey) + .apiKey(ALIYUN_API_KEY) .model("paraformer-v1") .parameter("language_hints", new String[]{"zh", "en"}) .fileUrls(fileLinkList) .build(); + try { Transcription transcription = new Transcription(); TranscriptionResult result = transcription.asyncCall(param); - log.info("[videoToCharacters2][提交转写请求成功,TaskId({})]", result.getTaskId()); - result = transcription.wait( - TranscriptionQueryParam.FromTranscriptionParam(param, result.getTaskId())); + log.info("[videoToCharacters2] 提交转写请求成功,TaskId: {}", result.getTaskId()); + + result = transcription.wait(TranscriptionQueryParam.FromTranscriptionParam(param, result.getTaskId())); String outputJson = new GsonBuilder().setPrettyPrinting().create().toJson(result.getOutput()); - log.info("[videoToCharacters2][识别成功,TaskId({}),结果长度({})]", + + log.info("[videoToCharacters2] 识别成功,TaskId: {},结果长度: {}", result.getTaskId(), outputJson != null ? outputJson.length() : 0); return CommonResult.success(outputJson); } catch (Exception e) { - log.error("[videoToCharacters2][识别失败,文件URL({}),异常({})]", fileLinkList, e.getMessage(), e); - return CommonResult.error(500,"录音文件识别请求失败!"); + log.error("[videoToCharacters2] 识别失败,文件URL: {},异常: {}", fileLinkList, e.getMessage(), e); + return CommonResult.error(500, "录音文件识别请求失败!"); } } + @Override + public Object deepseekAnalysis(String promptType, String content) { + TikPromptVO tikPromptVO = tikPromptMapper.getTikPromptVO(promptType); + if (tikPromptVO == null) { + log.error("[deepseekAnalysis] 未找到提示词类型: {}", promptType); + return CommonResult.error(404, "未找到提示词类型"); + } - public Object deepseekAnalysis(String promptType,String content){ - Unirest.setTimeouts(0, 0); - try{ - TikPromptVO tikPromptVO = tikPromptMapper.getTikPromptVO(promptType); - HttpResponse response = Unirest.post("https://api.deepseek.com/chat/completions") + try { + String requestBody = buildDeepseekRequestBody(tikPromptVO.getPrompt(), content); + HttpResponse response = Unirest.post(DEEPSEEK_API_URL) .header("Content-Type", "application/json") .header("Accept", "application/json") - .header("Authorization", "Bearer " + deepseekApiKey) - .body("{\n \"messages\": [\n {\n \"content\": \""+tikPromptVO.getPrompt()+"\n\n\n原文如下:\n"+content+"\",\n \"role\": \"system\"\n },\n {\n \"content\": \"Hi\",\n \"role\": \"user\"\n }\n ],\n \"model\": \"deepseek-chat\",\n \"frequency_penalty\": 0,\n \"max_tokens\": 8000,\n \"presence_penalty\": 0,\n \"response_format\": {\n \"type\": \"text\"\n },\n \"stop\": null,\n \"stream\": false,\n \"stream_options\": null,\n \"temperature\": 1,\n \"top_p\": 1,\n \"tools\": null,\n \"tool_choice\": \"none\",\n \"logprobs\": false,\n \"top_logprobs\": null\n}") + .header("Authorization", "Bearer " + DEEPSEEK_API_KEY) + .body(requestBody) .asString(); - if(response.getStatus() == 200){ + + if (response.getStatus() == 200) { return response.getBody(); } - }catch (Exception e){ - log.error(e.getMessage()); - return CommonResult.error(500,"文案改写失败!"); + log.error("[deepseekAnalysis] 调用失败,状态码: {}", response.getStatus()); + return CommonResult.error(500, "文案改写失败!"); + } catch (Exception e) { + log.error("[deepseekAnalysis] 异常: {}", e.getMessage(), e); + return CommonResult.error(500, "文案改写失败!"); } - return null; + } + private String buildDeepseekRequestBody(String prompt, String content) { + Map requestBody = new HashMap<>(); + requestBody.put("model", "deepseek-chat"); + requestBody.put("frequency_penalty", 0); + requestBody.put("max_tokens", 8000); + requestBody.put("presence_penalty", 0); + requestBody.put("stream", false); + requestBody.put("temperature", 1); + requestBody.put("top_p", 1); + requestBody.put("logprobs", false); + + List> messages = List.of( + Map.of("role", "system", "content", prompt + "\n\n\n原文如下:\n" + content), + Map.of("role", "user", "content", "Hi") + ); + requestBody.put("messages", messages); + + return JSON.toJSONString(requestBody); + } + + @Override + public Flux callWorkflow(Map params) { + Constants.baseHttpApiUrl = ALIYUN_API_URL; + + ApplicationParam param = ApplicationParam.builder() + .apiKey(ALIYUN_API_KEY) + .appId(ALIYUN_APP_ID) + .flowStreamMode(FlowStreamMode.MESSAGE_FORMAT) + .bizParams(JsonUtils.parse(JSON.toJSONString(params))) + .prompt((String) params.get("audio_prompt")) + .hasThoughts(false) + .incrementalOutput(true) + .build(); + + Application application = new Application(); + try { + Flowable flowableResult = application.streamCall(param); + return Flux.from(flowableResult) + .subscribeOn(Schedulers.boundedElastic()) + .map(this::extractWorkflowContent) + .timeout(STREAM_TIMEOUT) + .onErrorResume(TimeoutException.class, e -> { + log.warn("流式输出超时(30分钟无数据),连接已断开"); + return Flux.just("[TIMEOUT]流式输出超时,请检查网络或重新请求"); + }) + .concatWith(Flux.just("[DONE]")) + .onErrorReturn("错误:流式处理中断,请重试或联系管理员"); + } catch (Exception e) { + log.error("调用阿里 SDK 失败", e); + return Flux.just("错误:SDK 调用失败:" + e.getMessage()); + } + } + + private String extractWorkflowContent(ApplicationResult result) { + if (result == null || result.getOutput() == null) { + log.warn("收到空的流式结果"); + return "空结果"; + } + return result.getOutput().getWorkflowMessage().getMessage().getContent(); } }