Merge branch 'dev-1' into dev
This commit is contained in:
commit
f365d94077
@ -118,8 +118,8 @@ api:
|
|||||||
# ActionEnum 的枚举作为 key
|
# ActionEnum 的枚举作为 key
|
||||||
agents:
|
agents:
|
||||||
INTENT_RECOGNITION: # 意图识别
|
INTENT_RECOGNITION: # 意图识别
|
||||||
app-id: d3c98sp0gdon0fcf6l00
|
app-id: d5dn5ibp9adhq1b34lig
|
||||||
app-key: d3c98vp0gdon0fcf6n20
|
app-key: d6j5bcellh49on5tasvg
|
||||||
TOUCH_COORDINATES: # 意图识别
|
TOUCH_COORDINATES: # 意图识别
|
||||||
app-id: d5thge2cktmipk78h82g
|
app-id: d5thge2cktmipk78h82g
|
||||||
app-key: d1ebtabnjkflk4gmhikg
|
app-key: d1ebtabnjkflk4gmhikg
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.cmvr.llm.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
public interface LLMAiAgentPlatformService {
|
||||||
|
JSONObject query(String action, String text, String apiKey);
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.cmvr.llm.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
public interface LLMAiTtsService {
|
||||||
|
JSONObject play(String url, String text, String voice, String speed, String volume);
|
||||||
|
}
|
||||||
@ -0,0 +1,270 @@
|
|||||||
|
package com.cmvr.llm.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.common.exception.GlobalException;
|
||||||
|
import com.cmvr.common.utils.http.CallAPIUtil;
|
||||||
|
import com.cmvr.llm.config.APIProperties;
|
||||||
|
import com.cmvr.llm.util.LlmChatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商道大模型工作流调用服务
|
||||||
|
* 流程:
|
||||||
|
* 1. 调用工作流启动接口,获取 runId
|
||||||
|
* 2. 根据 runId 轮询查询接口
|
||||||
|
* 3. 解析结果
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WorkflowInvokeServiceCopy {
|
||||||
|
|
||||||
|
private final APIProperties apiProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID可以作为全局常量
|
||||||
|
*/
|
||||||
|
private static final String USER_ID = "18888888888";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认最大重试次数
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_MAX_RETRY = 40;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认轮询间隔(毫秒)
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_INTERVAL_MS = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一站式执行工作流(带自定义重试参数)
|
||||||
|
*
|
||||||
|
* @param appId 智能体应用ID
|
||||||
|
* @param appKey 智能体应用秘钥
|
||||||
|
* @param inputParams 输入参数(业务参数,例如图片、描述等)
|
||||||
|
* @param maxRetry 最大重试次数
|
||||||
|
* @param intervalMs 每次轮询间隔(毫秒)
|
||||||
|
* @return 最终结果 message
|
||||||
|
*/
|
||||||
|
public JSONObject executeWorkflow(String appId, String appKey,
|
||||||
|
Map<String, String> inputParams,
|
||||||
|
int maxRetry, long intervalMs) {
|
||||||
|
// 1. 启动工作流,获取 runId
|
||||||
|
String runId = startWorkflow(appId, appKey, inputParams);
|
||||||
|
|
||||||
|
// 2. 轮询获取结果
|
||||||
|
String responseBody = pollResult(runId, appId, appKey, maxRetry, intervalMs);
|
||||||
|
|
||||||
|
// 3. 解析
|
||||||
|
return parseResponse(responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LlmChatService llmChatService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一站式执行工作流(使用默认重试参数)
|
||||||
|
*
|
||||||
|
* @param appId 智能体应用ID
|
||||||
|
* @param appKey 智能体应用秘钥
|
||||||
|
* @param text 输入参数(业务参数,例如图片、描述等)
|
||||||
|
* @return 最终结果 message
|
||||||
|
*/
|
||||||
|
public String executeWorkflow(String appId, String appKey, String text) {
|
||||||
|
// 调用
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("Query", text);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String result = null;
|
||||||
|
try {
|
||||||
|
result = llmChatService.chat(
|
||||||
|
appKey, // apiKey (header用)
|
||||||
|
appId, // apiId (body的AppKey)
|
||||||
|
"user_123", // userId
|
||||||
|
body
|
||||||
|
);
|
||||||
|
System.out.println( result);
|
||||||
|
System.out.println("总耗时:" + (System.currentTimeMillis() - startTime));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 核心流程 -------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动工作流,返回 runId
|
||||||
|
*
|
||||||
|
* @param appId 智能体应用ID
|
||||||
|
* @param appKey 智能体应用秘钥
|
||||||
|
* @param inputParams 输入参数
|
||||||
|
* @return 工作流运行ID-runId
|
||||||
|
*/
|
||||||
|
private String startWorkflow(String appId, String appKey, Map<String, String> inputParams) {
|
||||||
|
Map<String, String> headers = buildHeaders(appKey);
|
||||||
|
Map<String, String> body = buildRequestBody(appKey, appId, inputParams);
|
||||||
|
|
||||||
|
String responseBody = CallAPIUtil.doPostJson(apiProperties.getWorkFlowUrl(), headers, body);
|
||||||
|
JSONObject jsonObject = JSON.parseObject(responseBody);
|
||||||
|
|
||||||
|
String runId = jsonObject.getString("runId");
|
||||||
|
if (StrUtil.isEmpty(runId)) {
|
||||||
|
throw new GlobalException("未返回 runId,响应:{}", responseBody);
|
||||||
|
}
|
||||||
|
return runId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询查询接口,直到获取到 end 节点
|
||||||
|
*
|
||||||
|
* @param runId 工作流运行ID
|
||||||
|
* @param appId 智能体应用ID
|
||||||
|
* @param appKey 智能体应用秘钥
|
||||||
|
* @param maxRetry 最大重试次数
|
||||||
|
* @param intervalMs 轮询间隔(毫秒)
|
||||||
|
* @return 最终响应 JSON 字符串
|
||||||
|
*/
|
||||||
|
private String pollResult(String runId, String appId, String appKey, int maxRetry, long intervalMs) {
|
||||||
|
Map<String, String> headers = buildHeaders(appKey);
|
||||||
|
Map<String, String> body = buildBaseBody(appKey, appId);
|
||||||
|
body.put("RunID", runId);
|
||||||
|
|
||||||
|
int failCount = 0;
|
||||||
|
while (true) {
|
||||||
|
String responseBody = CallAPIUtil.doPostJson(apiProperties.getQueryResultUrl(), headers, body);
|
||||||
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
String status = root.getString("status");
|
||||||
|
|
||||||
|
if ("success".equalsIgnoreCase(status)) {
|
||||||
|
// 校验并返回最终结果
|
||||||
|
parseResponse(responseBody);
|
||||||
|
return responseBody;
|
||||||
|
} else if ("processing".equalsIgnoreCase(status)) {
|
||||||
|
// 继续轮询
|
||||||
|
failCount++;
|
||||||
|
if (failCount > maxRetry) {
|
||||||
|
throw new GlobalException("轮询超时,达到最大重试次数,最后响应:{}", responseBody);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(intervalMs);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new GlobalException("轮询被中断");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他状态直接抛异常,不再重试
|
||||||
|
throw new GlobalException("工作流执行失败,status={},响应:{}", status, responseBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 结果解析 -------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从响应 JSON 中找到 nodeType=end 且 status=success 的节点
|
||||||
|
*/
|
||||||
|
private JSONObject findEndNode(String responseBody) {
|
||||||
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
JSONObject nodes = root.getJSONObject("nodes");
|
||||||
|
if (nodes == null) {
|
||||||
|
throw new GlobalException("响应中 nodes 为空:{}", responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> entry : nodes.entrySet()) {
|
||||||
|
JSONObject node = (JSONObject) entry.getValue();
|
||||||
|
if ("end".equalsIgnoreCase(node.getString("nodeType"))
|
||||||
|
&& "success".equalsIgnoreCase(node.getString("status"))) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new GlobalException("未找到 nodeType=end 且 status=success 的节点:{}", nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 end 节点的 output 内容
|
||||||
|
*
|
||||||
|
* @param endNode end 节点
|
||||||
|
* @return output JSON
|
||||||
|
*/
|
||||||
|
private JSONObject parseOutput(JSONObject endNode) {
|
||||||
|
String outputStr = endNode.getString("output");
|
||||||
|
if (StrUtil.isEmpty(outputStr)) {
|
||||||
|
throw new GlobalException("end 节点 output 为空:{}", endNode);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parseObject(outputStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new GlobalException("output 解析失败:{}", outputStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接卸
|
||||||
|
*
|
||||||
|
* @param responseBody 响应 JSON 字符串
|
||||||
|
* @return message
|
||||||
|
*/
|
||||||
|
private JSONObject parseResponse(String responseBody) {
|
||||||
|
JSONObject endNode = findEndNode(responseBody);
|
||||||
|
JSONObject output = parseOutput(endNode);
|
||||||
|
|
||||||
|
JSONObject nestedOutput = output.getJSONObject("output");
|
||||||
|
if (ObjUtil.isEmpty(nestedOutput)) {
|
||||||
|
throw new GlobalException("end node output 为空!");
|
||||||
|
}
|
||||||
|
return nestedOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 请求体拼装 -------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造请求头
|
||||||
|
*
|
||||||
|
* @param appKey 应用秘钥
|
||||||
|
* @return headers
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildHeaders(String appKey) {
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("Apikey", appKey);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造启动工作流的请求体
|
||||||
|
*
|
||||||
|
* @param appKey 应用秘钥
|
||||||
|
* @param appId 应用ID
|
||||||
|
* @param inputParams 输入参数
|
||||||
|
* @return body
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildRequestBody(String appKey, String appId, Map<String, String> inputParams) {
|
||||||
|
Map<String, String> body = buildBaseBody(appKey, appId);
|
||||||
|
body.put("InputData", JSON.toJSONString(inputParams));
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造通用请求体
|
||||||
|
*
|
||||||
|
* @param appKey 应用秘钥
|
||||||
|
* @param appId 应用ID
|
||||||
|
* @return body
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildBaseBody(String appKey, String appId) {
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("AppKey", appKey);
|
||||||
|
body.put("AppID", appId);
|
||||||
|
body.put("UserID", USER_ID);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.cmvr.llm.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||||
|
import com.cmvr.llm.util.LlmChatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LLMAiAgentPlatformServiceImpl implements LLMAiAgentPlatformService {
|
||||||
|
private final LlmChatService llmChatService;
|
||||||
|
@Override
|
||||||
|
public JSONObject query(String action, String text, String apiKey) {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("Query", text);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String result = null;
|
||||||
|
try {
|
||||||
|
result = llmChatService.chat(
|
||||||
|
apiKey, // apiKey (header用)
|
||||||
|
apiKey, // apiId (body的AppKey)
|
||||||
|
"user_123", // userId
|
||||||
|
body
|
||||||
|
);
|
||||||
|
System.out.println( result);
|
||||||
|
System.out.println("总耗时:" + (System.currentTimeMillis() - startTime));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new JSONObject().fluentPut("result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.cmvr.llm.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.common.utils.http.CallAPIUtil;
|
||||||
|
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||||
|
import com.cmvr.llm.service.LLMAiTtsService;
|
||||||
|
import com.cmvr.llm.util.LlmChatService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LLMAiTtsServiceImpl implements LLMAiTtsService {
|
||||||
|
@Override
|
||||||
|
public JSONObject play(String url, String text, String voice, String speed, String volume) {
|
||||||
|
// Map<String, Object> body = new HashMap<>();
|
||||||
|
// body.put("Query", text);
|
||||||
|
// long startTime = System.currentTimeMillis();
|
||||||
|
// String result = null;
|
||||||
|
// try {
|
||||||
|
// result = llmChatService.chat(
|
||||||
|
// apiKey, // apiKey (header用)
|
||||||
|
// apiKey, // apiId (body的AppKey)
|
||||||
|
// "user_123", // userId
|
||||||
|
// body
|
||||||
|
// );
|
||||||
|
// System.out.println( result);
|
||||||
|
// System.out.println("总耗时:" + (System.currentTimeMillis() - startTime));
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// throw new RuntimeException(e);
|
||||||
|
// }
|
||||||
|
// 通过post调用tts接口
|
||||||
|
String result = CallAPIUtil.doPostJson(url, new HashMap<>(), new JSONObject().fluentPut("text", text).fluentPut("voice", voice).fluentPut("speed", speed).fluentPut("volume", volume));
|
||||||
|
|
||||||
|
return new JSONObject().fluentPut("result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,9 +6,11 @@ import com.cmvr.llm.config.APIProperties;
|
|||||||
import com.cmvr.llm.config.AgentConfig;
|
import com.cmvr.llm.config.AgentConfig;
|
||||||
import com.cmvr.llm.service.LLMIntentRecognitionService;
|
import com.cmvr.llm.service.LLMIntentRecognitionService;
|
||||||
import com.cmvr.llm.service.WorkflowInvokeService;
|
import com.cmvr.llm.service.WorkflowInvokeService;
|
||||||
|
import com.cmvr.llm.util.LlmChatService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -17,12 +19,30 @@ public class LLMIntentRecognitionServiceImpl implements LLMIntentRecognitionServ
|
|||||||
|
|
||||||
private final APIProperties apiProperties;
|
private final APIProperties apiProperties;
|
||||||
private final WorkflowInvokeService workflowInvokeService;
|
private final WorkflowInvokeService workflowInvokeService;
|
||||||
|
private final LlmChatService llmChatService;
|
||||||
@Override
|
@Override
|
||||||
public JSONObject intentRecognition(String action, String text) {
|
public JSONObject intentRecognition(String action, String text) {
|
||||||
AgentConfig acg = apiProperties.getAgentConfig(action);
|
AgentConfig acg = apiProperties.getAgentConfig(action);
|
||||||
Map<String, String> inputParams = MapUtil.of("text", text);
|
// Map<String, String> inputParams = MapUtil.of("text", text);
|
||||||
// return new JSONObject().fluentPut("type", "4").fluentPut("message", "\n\n建议优先选择成都大熊猫繁育研究基地(成华区),该基地拥有顶流熊猫\\\"花花\\\"及完善的参观体系。门票需提前7天在\\\"成都熊猫基地电子票\\\"公众号预约,全票55元/人,建议选择清晨7:30前抵达南门入园(当前9月凉爽适宜户外观赏)。重点参观点为幼年大熊猫别墅(花花所在地)、月亮/太阳产房(活跃时段8:30-10:30)。若需避开人流,可前往都江堰熊猫谷(门票51元),该区域植被覆盖率高,游客密度较低,适合深度观赏熊猫自然行为。交通建议乘坐地铁3号线至熊猫大道站,换乘景区直通车(运营时间8:00-17:00)。需注意园区禁止投喂,建议携带轻便饮食及防滑鞋具。");
|
// return new JSONObject().fluentPut("type", "4").fluentPut("message", "\n\n建议优先选择成都大熊猫繁育研究基地(成华区),该基地拥有顶流熊猫\\\"花花\\\"及完善的参观体系。门票需提前7天在\\\"成都熊猫基地电子票\\\"公众号预约,全票55元/人,建议选择清晨7:30前抵达南门入园(当前9月凉爽适宜户外观赏)。重点参观点为幼年大熊猫别墅(花花所在地)、月亮/太阳产房(活跃时段8:30-10:30)。若需避开人流,可前往都江堰熊猫谷(门票51元),该区域植被覆盖率高,游客密度较低,适合深度观赏熊猫自然行为。交通建议乘坐地铁3号线至熊猫大道站,换乘景区直通车(运营时间8:00-17:00)。需注意园区禁止投喂,建议携带轻便饮食及防滑鞋具。");
|
||||||
return workflowInvokeService.executeWorkflow(acg.getAppId(), acg.getAppKey(), inputParams);
|
// return workflowInvokeService.executeWorkflow(acg.getAppId(), acg.getAppKey(), inputParams);
|
||||||
|
// 调用
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("Query", text);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String result = null;
|
||||||
|
try {
|
||||||
|
result = llmChatService.chat(
|
||||||
|
acg.getAppKey(), // apiKey (header用)
|
||||||
|
acg.getAppId(), // apiId (body的AppKey)
|
||||||
|
"user_123", // userId
|
||||||
|
body
|
||||||
|
);
|
||||||
|
System.out.println( result);
|
||||||
|
System.out.println("总耗时:" + (System.currentTimeMillis() - startTime));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new JSONObject().fluentPut("type", result).fluentPut("test2", new JSONObject().fluentPut("test2-2", "今天天气怎么样"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,339 @@
|
|||||||
|
package com.cmvr.llm.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM聊天服务 - 严格保持原接口参数
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class LlmChatService {
|
||||||
|
|
||||||
|
private static final String DEFAULT_BASE_URL = "https://aiagentplatform.cmft.com/api/proxy/api/v1";
|
||||||
|
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 会话缓存:key = apiKey + "|" + apiId + "|" + userId
|
||||||
|
private final ConcurrentHashMap<String, ConversationHolder> conversationCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final OkHttpClient httpClient;
|
||||||
|
|
||||||
|
public LlmChatService() {
|
||||||
|
this.httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心方法:同步调用,返回完整字符串结果
|
||||||
|
* 自动管理会话生命周期
|
||||||
|
*
|
||||||
|
* @param apiKey 对应原AppKey的header
|
||||||
|
* @param apiId 对应原AppKey的body字段
|
||||||
|
* @param userId 对应原UserID
|
||||||
|
* @param body 请求体,必须包含Query字段,可选Name等
|
||||||
|
* @return 完整AI回复字符串
|
||||||
|
*/
|
||||||
|
public String chat(String apiKey, String apiId, String userId, Map<String, Object> body) throws IOException {
|
||||||
|
String cacheKey = apiKey + "|" + apiId + "|" + userId;
|
||||||
|
|
||||||
|
// 获取或创建会话(严格保持原createConversation逻辑)
|
||||||
|
ConversationHolder holder = conversationCache.computeIfAbsent(cacheKey, k -> {
|
||||||
|
try {
|
||||||
|
String conversationId = createConversation(apiKey, apiId, userId);
|
||||||
|
System.out.println("创建新会话: " + conversationId);
|
||||||
|
return new ConversationHolder(conversationId, apiKey, apiId, userId);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("创建会话失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建请求体 - 严格保持原buildChatRequestBody逻辑
|
||||||
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
|
requestBody.put("AppKey", apiId); // 对应原apiKey参数
|
||||||
|
requestBody.put("AppConversationID", holder.conversationId);
|
||||||
|
requestBody.put("UserID", userId);
|
||||||
|
requestBody.put("ResponseMode", "streaming");
|
||||||
|
requestBody.put("PubAgentJump", true);
|
||||||
|
|
||||||
|
// 合并用户传入的body(包含Query等)
|
||||||
|
if (body != null) {
|
||||||
|
requestBody.putAll(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行流式请求并同步返回完整结果
|
||||||
|
return executeSseRequestSync(apiKey, requestBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会话 - 完全保持原逻辑
|
||||||
|
*/
|
||||||
|
private String createConversation(String apiKey, String apiId, String userId) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiId); // 对应原apiKey
|
||||||
|
body.put("UserID", userId);
|
||||||
|
// 如果调用方传了Name就用,否则自动生成
|
||||||
|
if (!body.containsKey("Name")) {
|
||||||
|
body.put("Name", "自动创建-" + System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(DEFAULT_BASE_URL + "/create_conversation")
|
||||||
|
.header("Apikey", apiKey) // 对应原apiKey
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("创建会话失败: " + response.code() + " - " + response.body().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
String respJson = response.body().string();
|
||||||
|
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
|
||||||
|
Map<String, Object> conversation = (Map<String, Object>) respMap.get("Conversation");
|
||||||
|
|
||||||
|
if (conversation == null) {
|
||||||
|
throw new IOException("返回数据格式错误,缺少 Conversation 字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
String conversationId = (String) conversation.get("AppConversationID");
|
||||||
|
if (conversationId == null || conversationId.isEmpty()) {
|
||||||
|
throw new IOException("返回数据格式错误,缺少 AppConversationID");
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行SSE请求 - 完全保持原executeSseRequest逻辑,改为同步返回
|
||||||
|
*/
|
||||||
|
private String executeSseRequestSync(String apiKey, Map<String, Object> requestBody) throws IOException {
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(DEFAULT_BASE_URL + "/chat_query")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "text/event-stream")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StringBuilder fullContent = new StringBuilder();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
AtomicReference<Throwable> errorRef = new AtomicReference<>();
|
||||||
|
AtomicReference<String> finalMessageIdRef = new AtomicReference<>("");
|
||||||
|
|
||||||
|
Call call = httpClient.newCall(request);
|
||||||
|
|
||||||
|
// 使用异步调用但阻塞等待结果
|
||||||
|
call.enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call call, IOException e) {
|
||||||
|
errorRef.set(e);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
|
||||||
|
errorRef.set(new IOException("SSE请求失败: " + response.code() + " - " + errorBody));
|
||||||
|
latch.countDown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder localContent = new StringBuilder();
|
||||||
|
String finalMessageId = "";
|
||||||
|
String currentEvent = "";
|
||||||
|
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
currentEvent = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("event:")) {
|
||||||
|
currentEvent = line.substring(6).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("data:")) {
|
||||||
|
String data = line.substring(5).trim();
|
||||||
|
|
||||||
|
// 处理双重 data: 前缀(data:data: {...})
|
||||||
|
if (data.startsWith("data:")) {
|
||||||
|
data = data.substring(5).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("[DONE]".equals(data)) {
|
||||||
|
fullContent.append(localContent);
|
||||||
|
finalMessageIdRef.set(finalMessageId);
|
||||||
|
latch.countDown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> jsonData = objectMapper.readValue(data, Map.class);
|
||||||
|
|
||||||
|
String messageId = extractString(jsonData, "id", "task_id", "message_id");
|
||||||
|
if (!messageId.isEmpty()) {
|
||||||
|
finalMessageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String event = (String) jsonData.getOrDefault("event", currentEvent);
|
||||||
|
|
||||||
|
if ("message".equals(event)) {
|
||||||
|
String answer = extractString(jsonData, "answer", "content", "chunk", "text");
|
||||||
|
if (!answer.isEmpty()) {
|
||||||
|
localContent.append(answer);
|
||||||
|
}
|
||||||
|
} else if ("message_start".equals(event)) {
|
||||||
|
System.out.println("消息开始,ID: " + finalMessageId);
|
||||||
|
} else if ("message_output_start".equals(event)) {
|
||||||
|
System.out.println("输出开始");
|
||||||
|
} else if ("message_end".equals(event) || "end".equals(event)) {
|
||||||
|
fullContent.append(localContent);
|
||||||
|
finalMessageIdRef.set(finalMessageId);
|
||||||
|
latch.countDown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("解析失败,原始数据: " + data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流正常结束
|
||||||
|
fullContent.append(localContent);
|
||||||
|
finalMessageIdRef.set(finalMessageId);
|
||||||
|
latch.countDown();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
errorRef.set(e);
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待流式传输完成
|
||||||
|
try {
|
||||||
|
boolean completed = latch.await(120, TimeUnit.SECONDS);
|
||||||
|
if (!completed) {
|
||||||
|
throw new IOException("请求超时");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("请求被中断", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorRef.get() != null) {
|
||||||
|
if (errorRef.get() instanceof IOException) {
|
||||||
|
throw (IOException) errorRef.get();
|
||||||
|
} else {
|
||||||
|
throw new IOException("请求异常", errorRef.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullContent.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完全保持原extractString方法
|
||||||
|
*/
|
||||||
|
private String extractString(Map<String, Object> map, String... keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Object value = map.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Boot停止时清理所有会话
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void close() {
|
||||||
|
System.out.println("开始清理会话,共 " + conversationCache.size() + " 个");
|
||||||
|
|
||||||
|
conversationCache.values().forEach(holder -> {
|
||||||
|
try {
|
||||||
|
deleteConversation(holder);
|
||||||
|
System.out.println("已删除会话: " + holder.conversationId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("删除会话失败: " + holder.conversationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conversationCache.clear();
|
||||||
|
httpClient.dispatcher().executorService().shutdown();
|
||||||
|
httpClient.connectionPool().evictAll();
|
||||||
|
System.out.println("会话清理完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话 - 完全保持原deleteConversation逻辑
|
||||||
|
*/
|
||||||
|
private void deleteConversation(ConversationHolder holder) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", holder.apiId);
|
||||||
|
body.put("ConversationID", holder.conversationId);
|
||||||
|
body.put("UserID", holder.userId);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(DEFAULT_BASE_URL + "/delete_conversation")
|
||||||
|
.header("Apikey", holder.apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("删除会话失败: " + response.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部会话持有类
|
||||||
|
*/
|
||||||
|
private static class ConversationHolder {
|
||||||
|
final String conversationId;
|
||||||
|
final String apiKey;
|
||||||
|
final String apiId;
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
ConversationHolder(String conversationId, String apiKey, String apiId, String userId) {
|
||||||
|
this.conversationId = conversationId;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.apiId = apiId;
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,629 @@
|
|||||||
|
package com.cmvr.llm.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import okhttp3.*;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 流式对话客户端(纯 OkHttp 实现,无额外依赖)
|
||||||
|
* 包含会话管理、消息管理、流式/阻塞对话
|
||||||
|
*/
|
||||||
|
public class SseChatClient {
|
||||||
|
|
||||||
|
private static final String DEFAULT_BASE_URL = "https://aiagentplatform.cmft.com/api/proxy/api/v1";
|
||||||
|
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private final String baseUrl;
|
||||||
|
private final String apiKey;
|
||||||
|
private final OkHttpClient httpClient;
|
||||||
|
|
||||||
|
// ==================== 数据模型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话信息
|
||||||
|
*/
|
||||||
|
public static class Conversation {
|
||||||
|
private String conversationId;
|
||||||
|
private String name;
|
||||||
|
private long createdAt;
|
||||||
|
|
||||||
|
public Conversation(String conversationId, String name, long createdAt) {
|
||||||
|
this.conversationId = conversationId;
|
||||||
|
this.name = name;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConversationId() { return conversationId; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public long getCreatedAt() { return createdAt; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Conversation{id='%s', name='%s'}", conversationId, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息信息
|
||||||
|
*/
|
||||||
|
public static class ChatMessage {
|
||||||
|
private String messageId;
|
||||||
|
private String queryId;
|
||||||
|
private String content;
|
||||||
|
private String role;
|
||||||
|
private long createdAt;
|
||||||
|
|
||||||
|
public ChatMessage(String messageId, String queryId, String content, String role, long createdAt) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.queryId = queryId;
|
||||||
|
this.content = content;
|
||||||
|
this.role = role;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessageId() { return messageId; }
|
||||||
|
public String getQueryId() { return queryId; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public long getCreatedAt() { return createdAt; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式消息回调接口
|
||||||
|
*/
|
||||||
|
public interface StreamCallback {
|
||||||
|
/**
|
||||||
|
* 收到消息块时触发(实现打字机效果)
|
||||||
|
* @param chunk 文本块
|
||||||
|
* @param messageId 消息ID
|
||||||
|
*/
|
||||||
|
void onMessage(String chunk, String messageId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式传输完成
|
||||||
|
* @param fullContent 完整内容
|
||||||
|
* @param messageId 最终消息ID
|
||||||
|
*/
|
||||||
|
default void onComplete(String fullContent, String messageId) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发生错误
|
||||||
|
* @param error 错误信息
|
||||||
|
*/
|
||||||
|
default void onError(Throwable error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 构造函数 ====================
|
||||||
|
|
||||||
|
public SseChatClient(String apiKey) {
|
||||||
|
this(DEFAULT_BASE_URL, apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SseChatClient(String baseUrl, String apiKey) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 会话管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新会话
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param name 会话名称(可选)
|
||||||
|
* @return 会话对象
|
||||||
|
*/
|
||||||
|
public Conversation createConversation(String userId, String name) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
body.put("Name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/create_conversation")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("创建会话失败: " + response.code() + " - " + response.body().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
String respJson = response.body().string();
|
||||||
|
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
|
||||||
|
|
||||||
|
// 修正:先取 Conversation 对象,再取 AppConversationID
|
||||||
|
Map<String, Object> conversation = (Map<String, Object>) respMap.get("Conversation");
|
||||||
|
if (conversation == null) {
|
||||||
|
throw new IOException("返回数据格式错误,缺少 Conversation 字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
String conversationId = (String) conversation.get("AppConversationID");
|
||||||
|
if (conversationId == null || conversationId.isEmpty()) {
|
||||||
|
throw new IOException("返回数据格式错误,缺少 AppConversationID");
|
||||||
|
}
|
||||||
|
String convName = (String) conversation.getOrDefault("ConversationName", "新会话");
|
||||||
|
long createdAt = ((Number) conversation.getOrDefault("CreatedAt", System.currentTimeMillis())).longValue();
|
||||||
|
|
||||||
|
return new Conversation(conversationId, convName, createdAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会话(自动生成名称)
|
||||||
|
*/
|
||||||
|
public Conversation createConversation(String userId) throws IOException {
|
||||||
|
return createConversation(userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话列表
|
||||||
|
*/
|
||||||
|
public List<Conversation> getConversationList(String userId, Integer limit, Integer offset) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
if (limit != null) body.put("Limit", limit);
|
||||||
|
if (offset != null) body.put("Offset", offset);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/get_conversation_list")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("获取会话列表失败: " + response.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
String respJson = response.body().string();
|
||||||
|
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
|
||||||
|
|
||||||
|
List<Conversation> conversations = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> list = (List<Map<String, Object>>) respMap.get("ConversationList");
|
||||||
|
|
||||||
|
if (list != null) {
|
||||||
|
for (Map<String, Object> item : list) {
|
||||||
|
conversations.add(new Conversation(
|
||||||
|
(String) item.get("ConversationID"),
|
||||||
|
(String) item.getOrDefault("Name", "未命名"),
|
||||||
|
((Number) item.getOrDefault("CreatedAt", 0L)).longValue()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*/
|
||||||
|
public void deleteConversation(String conversationId, String userId) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("ConversationID", conversationId);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/delete_conversation")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("删除会话失败: " + response.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息详情
|
||||||
|
*/
|
||||||
|
public ChatMessage getMessageInfo(String messageId, String userId) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("MessageID", messageId);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/get_message_info")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("获取消息失败: " + response.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
String respJson = response.body().string();
|
||||||
|
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
|
||||||
|
Map<String, Object> msgInfo = (Map<String, Object>) respMap.get("MessageInfo");
|
||||||
|
|
||||||
|
if (msgInfo == null) {
|
||||||
|
throw new IOException("消息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ChatMessage(
|
||||||
|
(String) msgInfo.get("MessageID"),
|
||||||
|
(String) msgInfo.get("QueryID"),
|
||||||
|
(String) msgInfo.get("Content"),
|
||||||
|
(String) msgInfo.get("Role"),
|
||||||
|
((Number) msgInfo.getOrDefault("CreatedAt", 0L)).longValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
*/
|
||||||
|
public void deleteMessage(String messageId, String queryId, String userId) throws IOException {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("MessageID", messageId);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
if (queryId != null && !queryId.isEmpty()) {
|
||||||
|
body.put("QueryID", queryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/delete_message")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("删除消息失败: " + response.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 对话功能 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式对话(自动创建会话)
|
||||||
|
* @return 创建的会话ID
|
||||||
|
*/
|
||||||
|
public String chatStreamingAuto(String query, String userId, StreamCallback callback) throws IOException {
|
||||||
|
Conversation conv = createConversation(userId, "自动创建-" + System.currentTimeMillis());
|
||||||
|
String conversationId = conv.getConversationId();
|
||||||
|
|
||||||
|
System.out.println("自动创建会话: " + conversationId);
|
||||||
|
|
||||||
|
chatStreaming(query, conversationId, userId, null, callback);
|
||||||
|
|
||||||
|
return conversationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式对话(使用已有会话)
|
||||||
|
*/
|
||||||
|
public void chatStreaming(String query, String conversationId, String userId, StreamCallback callback) {
|
||||||
|
chatStreaming(query, conversationId, userId, null, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式对话(支持文件上传)
|
||||||
|
*/
|
||||||
|
public void chatStreaming(String query, String conversationId, String userId,
|
||||||
|
List<Map<String, Object>> files, StreamCallback callback) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> requestBody = buildChatRequestBody(query, conversationId, userId, "streaming", files);
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/chat_query")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "text/event-stream")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
executeSseRequest(request, callback);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
callback.onError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞模式对话
|
||||||
|
*/
|
||||||
|
public String chatBlocking(String query, String conversationId, String userId) throws IOException {
|
||||||
|
return chatBlocking(query, conversationId, userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞模式对话(支持文件)
|
||||||
|
*/
|
||||||
|
public String chatBlocking(String query, String conversationId, String userId,
|
||||||
|
List<Map<String, Object>> files) throws IOException {
|
||||||
|
Map<String, Object> requestBody = buildChatRequestBody(query, conversationId, userId, "blocking", files);
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/chat_query")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(request).execute()) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("请求失败: " + response.code() + " - " + response.body().string());
|
||||||
|
}
|
||||||
|
return response.body().string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新生成回复(流式)
|
||||||
|
*/
|
||||||
|
public void regenerateStreaming(String conversationId, String messageId,
|
||||||
|
String userId, StreamCallback callback) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("AppConversationID", conversationId);
|
||||||
|
body.put("MessageID", messageId);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(body);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(baseUrl + "/query_again")
|
||||||
|
.header("Apikey", apiKey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "text/event-stream")
|
||||||
|
.post(RequestBody.create(jsonBody, JSON))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
executeSseRequest(request, callback);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
callback.onError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有辅助方法 ====================
|
||||||
|
|
||||||
|
private Map<String, Object> buildChatRequestBody(String query, String conversationId,
|
||||||
|
String userId, String mode,
|
||||||
|
List<Map<String, Object>> files) {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("Query", query);
|
||||||
|
body.put("AppConversationID", conversationId);
|
||||||
|
body.put("AppKey", apiKey);
|
||||||
|
body.put("ResponseMode", mode);
|
||||||
|
body.put("UserID", userId);
|
||||||
|
body.put("PubAgentJump", true);
|
||||||
|
|
||||||
|
if (files != null && !files.isEmpty()) {
|
||||||
|
Map<String, Object> queryExtends = new HashMap<>();
|
||||||
|
queryExtends.put("Files", files);
|
||||||
|
body.put("QueryExtends", queryExtends);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 SSE 请求(核心方法)
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 执行 SSE 请求(修正版)
|
||||||
|
* 处理实际格式:event:text + data:data: {...}
|
||||||
|
*/
|
||||||
|
private void executeSseRequest(Request request, StreamCallback callback) throws IOException {
|
||||||
|
Call call = httpClient.newCall(request);
|
||||||
|
Response response = call.execute();
|
||||||
|
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
|
||||||
|
throw new IOException("SSE请求失败: " + response.code() + " - " + errorBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder fullContent = new StringBuilder();
|
||||||
|
String finalMessageId = "";
|
||||||
|
String currentEvent = "";
|
||||||
|
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
// 空行表示一个事件结束
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
currentEvent = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 event 行
|
||||||
|
if (line.startsWith("event:")) {
|
||||||
|
currentEvent = line.substring(6).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 data 行(注意:实际是 data:data: {...} 格式)
|
||||||
|
if (line.startsWith("data:")) {
|
||||||
|
String data = line.substring(5).trim();
|
||||||
|
|
||||||
|
// 处理双重 data: 前缀(data:data: {...})
|
||||||
|
if (data.startsWith("data:")) {
|
||||||
|
data = data.substring(5).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束标记
|
||||||
|
if ("[DONE]".equals(data)) {
|
||||||
|
callback.onComplete(fullContent.toString(), finalMessageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> jsonData = objectMapper.readValue(data, Map.class);
|
||||||
|
|
||||||
|
// 提取消息ID
|
||||||
|
String messageId = extractString(jsonData, "id", "task_id", "message_id");
|
||||||
|
if (!messageId.isEmpty()) {
|
||||||
|
finalMessageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据事件类型处理
|
||||||
|
String event = (String) jsonData.getOrDefault("event", currentEvent);
|
||||||
|
|
||||||
|
if ("message".equals(event)) {
|
||||||
|
// 提取回答内容(在 answer 字段)
|
||||||
|
String answer = extractString(jsonData, "answer", "content", "chunk", "text");
|
||||||
|
if (!answer.isEmpty()) {
|
||||||
|
fullContent.append(answer);
|
||||||
|
callback.onMessage(answer, finalMessageId);
|
||||||
|
}
|
||||||
|
} else if ("message_start".equals(event)) {
|
||||||
|
// 消息开始,可忽略或记录
|
||||||
|
System.out.println("消息开始,ID: " + finalMessageId);
|
||||||
|
} else if ("message_output_start".equals(event)) {
|
||||||
|
// 输出开始,可忽略或记录
|
||||||
|
System.out.println("输出开始");
|
||||||
|
} else if ("message_end".equals(event) || "end".equals(event)) {
|
||||||
|
// 消息结束
|
||||||
|
callback.onComplete(fullContent.toString(), finalMessageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 其他事件类型可扩展...
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// JSON解析失败,打印调试
|
||||||
|
System.out.println("解析失败,原始数据: " + data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流正常结束(没有明确结束标记)
|
||||||
|
callback.onComplete(fullContent.toString(), finalMessageId);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
callback.onError(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Map 中提取字符串(尝试多个 key)
|
||||||
|
*/
|
||||||
|
private String extractString(Map<String, Object> map, String... keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
Object value = map.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭客户端
|
||||||
|
*/
|
||||||
|
public void close() {
|
||||||
|
httpClient.dispatcher().executorService().shutdown();
|
||||||
|
httpClient.connectionPool().evictAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 使用示例 ====================
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SseChatClient client = new SseChatClient("d6gl6n6llh49on40fqug");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String userId = "user_" + System.currentTimeMillis();
|
||||||
|
|
||||||
|
// ========== 示例1:标准流程 ==========
|
||||||
|
System.out.println("=== 步骤1:创建会话 ===");
|
||||||
|
Conversation conv = client.createConversation(userId, "测试会话");
|
||||||
|
System.out.println("会话创建成功: " + conv.getConversationId());
|
||||||
|
|
||||||
|
System.out.println("\n=== 步骤2:流式对话 ===");
|
||||||
|
final StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
final String[] lastMessageId = {""};
|
||||||
|
|
||||||
|
client.chatStreaming(
|
||||||
|
"给我讲一个关于人工智能的短故事",
|
||||||
|
conv.getConversationId(),
|
||||||
|
userId,
|
||||||
|
new StreamCallback() {
|
||||||
|
@Override
|
||||||
|
public void onMessage(String chunk, String messageId) {
|
||||||
|
System.out.print(chunk);
|
||||||
|
System.out.flush();
|
||||||
|
contentBuilder.append(chunk);
|
||||||
|
if (!messageId.isEmpty()) {
|
||||||
|
lastMessageId[0] = messageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete(String fullContent, String messageId) {
|
||||||
|
System.out.println("\n\n[对话完成] 消息ID: " + messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable error) {
|
||||||
|
System.err.println("\n[错误] " + error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
String string = contentBuilder.toString();
|
||||||
|
// Thread.sleep(15000);
|
||||||
|
// // 等待流式传输完成
|
||||||
|
// Thread.sleep(15000);
|
||||||
|
// String string = contentBuilder.toString();
|
||||||
|
|
||||||
|
// ========== 示例3:获取会话列表 ==========
|
||||||
|
System.out.println("\n=== 步骤4:获取会话列表 ===");
|
||||||
|
List<Conversation> conversations = client.getConversationList(userId, 10, 0);
|
||||||
|
System.out.println("共有 " + conversations.size() + " 个会话:");
|
||||||
|
for (Conversation c : conversations) {
|
||||||
|
System.out.println(" - " + c);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,5 +35,16 @@
|
|||||||
<artifactId>cmvr-iot-grpc-client</artifactId>
|
<artifactId>cmvr-iot-grpc-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js</artifactId>
|
||||||
|
<version>20.3.0</version> <!-- 最后一个支持 JDK 8 的版本 -->
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js-scriptengine</artifactId>
|
||||||
|
<version>20.3.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@ -24,6 +24,7 @@ public enum ActionEnum {
|
|||||||
SUB_END("NONE", "SUB_END", "子流程结束"),
|
SUB_END("NONE", "SUB_END", "子流程结束"),
|
||||||
SLEEP("NONE", "SLEEP", "延迟节点"),
|
SLEEP("NONE", "SLEEP", "延迟节点"),
|
||||||
HTTP("NONE", "HTTP", "HTTP调用"),
|
HTTP("NONE", "HTTP", "HTTP调用"),
|
||||||
|
CODE("NONE", "CODE", "代码执行"),
|
||||||
// AI评估
|
// AI评估
|
||||||
AI_EVALUATION_PRE("AE", "AI_EVALUATION_PRE", "AI评估预处理"),
|
AI_EVALUATION_PRE("AE", "AI_EVALUATION_PRE", "AI评估预处理"),
|
||||||
// AI评估
|
// AI评估
|
||||||
@ -66,6 +67,8 @@ public enum ActionEnum {
|
|||||||
TOUCH_COORDINATES("LLM", "TOUCH_COORDINATES", "获取触控坐标"),
|
TOUCH_COORDINATES("LLM", "TOUCH_COORDINATES", "获取触控坐标"),
|
||||||
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
INTENT_RECOGNITION("LLM", "INTENT_RECOGNITION", "意图识别"),
|
||||||
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
||||||
|
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
||||||
|
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
||||||
|
|
||||||
// 触控交互
|
// 触控交互
|
||||||
TI_PATH_SEARCH("EDGE", "TI_PATH_SEARCH", "路径搜索"),
|
TI_PATH_SEARCH("EDGE", "TI_PATH_SEARCH", "路径搜索"),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public enum NodeTypeEnum {
|
|||||||
END("end"),
|
END("end"),
|
||||||
SLEEP("sleep"),
|
SLEEP("sleep"),
|
||||||
HTTP("http"),
|
HTTP("http"),
|
||||||
|
CODE("code"),
|
||||||
SUB_END("subEnd"),
|
SUB_END("subEnd"),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@ -288,6 +288,8 @@ public class FlowModelBuilder {
|
|||||||
return NodeTypeEnum.SLEEP;
|
return NodeTypeEnum.SLEEP;
|
||||||
case "http":
|
case "http":
|
||||||
return NodeTypeEnum.HTTP;
|
return NodeTypeEnum.HTTP;
|
||||||
|
case "code":
|
||||||
|
return NodeTypeEnum.CODE;
|
||||||
default:
|
default:
|
||||||
return NodeTypeEnum.FUNCTION;
|
return NodeTypeEnum.FUNCTION;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.cmvr.test.flow.runtime.dispatcher;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
|
import com.cmvr.test.util.NashornJsExecutor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component("CODE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FlowCodeNodeHandler implements FlowNodeTypeHandler {
|
||||||
|
|
||||||
|
private final NashornJsExecutor nashornJsExecutor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) {
|
||||||
|
String nodeId = message.getNodeId();
|
||||||
|
JSONObject inputParams = message.getInputParams();
|
||||||
|
String code = inputParams.getString("code");
|
||||||
|
inputParams.remove("code");
|
||||||
|
log.info("code 节点 {} 开始执行", nodeId);
|
||||||
|
JSONObject execute = nashornJsExecutor.execute(code, inputParams);
|
||||||
|
log.info("code 节点 {} 执行完成", nodeId);
|
||||||
|
return TaskNodeExecuteResult.success(execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,12 +18,8 @@ public class FlowHttpNodeHandler implements FlowNodeTypeHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) {
|
public TaskNodeExecuteResult handle(TaskNodeExecuteMessage message) {
|
||||||
JSONObject output = new JSONObject();
|
try {
|
||||||
output.put("result", "主页");
|
|
||||||
return TaskNodeExecuteResult.success(output);
|
|
||||||
/* try {
|
|
||||||
JSONObject inputParams = message.getInputParams();
|
JSONObject inputParams = message.getInputParams();
|
||||||
|
|
||||||
// 1. 必填参数校验
|
// 1. 必填参数校验
|
||||||
String url = inputParams.getString("url");
|
String url = inputParams.getString("url");
|
||||||
if (url == null || url.isEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
@ -94,6 +90,6 @@ public class FlowHttpNodeHandler implements FlowNodeTypeHandler {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[FLOW][HTTP] 执行异常", e);
|
log.error("[FLOW][HTTP] 执行异常", e);
|
||||||
return TaskNodeExecuteResult.failure(e.getMessage());
|
return TaskNodeExecuteResult.failure(e.getMessage());
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.cmvr.test.flow.runtime.operator.llm;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.llm.service.LLMAiAgentPlatformService;
|
||||||
|
import com.cmvr.llm.service.LLMIntentRecognitionService;
|
||||||
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LLMAiAgentPlatformOperateService implements LLMOperateService {
|
||||||
|
|
||||||
|
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(ActionEnum action) {
|
||||||
|
return action.name().equals("AI_AGENT_PLATFORM");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) {
|
||||||
|
ActionEnum action = message.getAction();
|
||||||
|
JSONObject inputParams = message.getInputParams();
|
||||||
|
String text = inputParams.getString("text");
|
||||||
|
String apiKey = inputParams.getString("apiKey");
|
||||||
|
|
||||||
|
JSONObject output = llmAiAgentPlatformService.query(action.name(), text, apiKey);
|
||||||
|
|
||||||
|
return TaskNodeExecuteResult.success(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.cmvr.test.flow.runtime.operator.llm;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.cmvr.llm.service.LLMAiTtsService;
|
||||||
|
import com.cmvr.test.enums.ActionEnum;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||||
|
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LLMAiTtsOperateService implements LLMOperateService {
|
||||||
|
|
||||||
|
private final LLMAiTtsService llmAiTtsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(ActionEnum action) {
|
||||||
|
return action.name().equals("AI_TTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TaskNodeExecuteResult execute(TaskNodeExecuteMessage message) {
|
||||||
|
ActionEnum action = message.getAction();
|
||||||
|
JSONObject inputParams = message.getInputParams();
|
||||||
|
String text = inputParams.getString("text");
|
||||||
|
String speed = inputParams.getString("speed");
|
||||||
|
String volume = inputParams.getString("volume");
|
||||||
|
String voice = inputParams.getString("voice");
|
||||||
|
String url = inputParams.getString("url");
|
||||||
|
|
||||||
|
JSONObject output = llmAiTtsService.play(url, text, voice, speed, volume);
|
||||||
|
|
||||||
|
return TaskNodeExecuteResult.success(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,250 @@
|
|||||||
|
package com.cmvr.test.util;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import jdk.nashorn.api.scripting.ClassFilter;
|
||||||
|
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.script.ScriptEngine;
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NashornJsExecutor {
|
||||||
|
|
||||||
|
// 执行超时时间(秒)
|
||||||
|
private static final long TIMEOUT_SECONDS = 5;
|
||||||
|
// 最大返回结果大小(1MB)
|
||||||
|
private static final int MAX_RESULT_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final ScriptEngine engine;
|
||||||
|
|
||||||
|
// 危险代码模式
|
||||||
|
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
|
||||||
|
"(Java|Packages|java|javax|org|com|edu|net)\\s*\\.\\s*([a-zA-Z_$])|" +
|
||||||
|
"eval\\s*\\(|new\\s+Function\\s*\\(|java\\.lang\\.Runtime"
|
||||||
|
);
|
||||||
|
|
||||||
|
public NashornJsExecutor() {
|
||||||
|
// 创建线程池
|
||||||
|
this.executor = new ThreadPoolExecutor(
|
||||||
|
2, 10, 60L, TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(100),
|
||||||
|
new ThreadFactory() {
|
||||||
|
private int count = 0;
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r, "js-executor-" + (++count));
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建 Nashorn 引擎,禁止访问所有 Java 类
|
||||||
|
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
|
||||||
|
String[] strings = {"--language=es6"};
|
||||||
|
this.engine = factory.getScriptEngine(
|
||||||
|
strings, // 关键在这里
|
||||||
|
null,
|
||||||
|
new ClassFilter() {
|
||||||
|
@Override
|
||||||
|
public boolean exposeToScripts(String className) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 JS 代码
|
||||||
|
*
|
||||||
|
* @param jsCode JS 代码,必须包含 handler 函数
|
||||||
|
* @param params 传入的参数(Map 或 POJO)
|
||||||
|
* @return 执行结果 Map
|
||||||
|
*/
|
||||||
|
public JSONObject execute(String jsCode, Object params) {
|
||||||
|
// 1. 代码安全检查
|
||||||
|
validateCode(jsCode);
|
||||||
|
|
||||||
|
// 2. 提交到线程池执行(支持超时中断)
|
||||||
|
Future<JSONObject> future = executor.submit(new Callable<JSONObject>() {
|
||||||
|
@Override
|
||||||
|
public JSONObject call() throws Exception {
|
||||||
|
return doExecute(jsCode, params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new RuntimeException("脚本执行超时(限制 " + TIMEOUT_SECONDS + " 秒)");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException("执行被中断");
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
throw new RuntimeException(cause.getMessage(), cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际执行逻辑
|
||||||
|
*/
|
||||||
|
private JSONObject doExecute(String jsCode, Object params) throws ScriptException {
|
||||||
|
// 将参数转为 JSON 字符串
|
||||||
|
String jsonParams = toJson(params);
|
||||||
|
|
||||||
|
// 构建安全的执行环境
|
||||||
|
String wrappedScript = buildSafeScript(jsCode, jsonParams);
|
||||||
|
|
||||||
|
// 执行脚本
|
||||||
|
Object result = engine.eval(wrappedScript);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
throw new RuntimeException("脚本返回 null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resultJson = result.toString();
|
||||||
|
|
||||||
|
// 检查结果大小
|
||||||
|
if (resultJson.length() > MAX_RESULT_SIZE) {
|
||||||
|
throw new RuntimeException("返回结果超过 " + (MAX_RESULT_SIZE/1024/1024) + "MB 限制");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析为 Map
|
||||||
|
return fromJson(resultJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更简单的版本:不覆盖 eval,依赖其他安全措施
|
||||||
|
private String buildSafeScript(String userCode, String jsonParams) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.append("(function() {\n");
|
||||||
|
sb.append(" \"use strict\";\n");
|
||||||
|
|
||||||
|
// 只禁用 Java 访问
|
||||||
|
sb.append(" var Java = undefined;\n");
|
||||||
|
sb.append(" var Packages = undefined;\n");
|
||||||
|
sb.append(" var java = undefined;\n");
|
||||||
|
sb.append(" var javax = undefined;\n");
|
||||||
|
sb.append(" var com = undefined;\n");
|
||||||
|
sb.append(" var org = undefined;\n");
|
||||||
|
sb.append(" var edu = undefined;\n");
|
||||||
|
sb.append(" var net = undefined;\n");
|
||||||
|
|
||||||
|
sb.append(" var console = {log: function(){}, error: function(){}};\n");
|
||||||
|
sb.append(" \n");
|
||||||
|
sb.append(userCode).append("\n");
|
||||||
|
sb.append(" \n");
|
||||||
|
sb.append(" var params = JSON.parse('").append(escapeJson(jsonParams)).append("');\n");
|
||||||
|
sb.append(" var result = handler(params);\n");
|
||||||
|
sb.append(" return JSON.stringify(result);\n");
|
||||||
|
sb.append("})()");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码安全校验
|
||||||
|
*/
|
||||||
|
private void validateCode(String code) {
|
||||||
|
if (code == null || code.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("代码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含 handler 函数定义
|
||||||
|
if (!code.contains("function handler")) {
|
||||||
|
throw new IllegalArgumentException("代码必须包含 function handler(params) 定义");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黑名单检查
|
||||||
|
if (DANGEROUS_PATTERN.matcher(code).find()) {
|
||||||
|
throw new SecurityException("代码包含危险关键字,已被拦截");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 字符串转义(防止注入)
|
||||||
|
*/
|
||||||
|
private String escapeJson(String json) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (char c : json.toCharArray()) {
|
||||||
|
switch (c) {
|
||||||
|
case '\\':
|
||||||
|
sb.append("\\\\");
|
||||||
|
break;
|
||||||
|
case '\'':
|
||||||
|
sb.append("\\'");
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
sb.append("\\\"");
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
sb.append("\\n");
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
sb.append("\\r");
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
sb.append("\\t");
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
sb.append("\\b");
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
sb.append("\\f");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象转 JSON(使用 Fastjson)
|
||||||
|
*/
|
||||||
|
private String toJson(Object obj) {
|
||||||
|
try {
|
||||||
|
return com.alibaba.fastjson.JSON.toJSONString(obj);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("参数序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 转 Map
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private JSONObject fromJson(String json) {
|
||||||
|
try {
|
||||||
|
return JSONObject.parseObject(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("结果反序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
executor.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user