|
|
@@ -15,7 +15,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import javax.annotation.Resource;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
@@ -69,6 +71,24 @@ public class AIService {
|
|
|
@Value("${dify.callback-secret:}")
|
|
|
private String callbackSecret;
|
|
|
|
|
|
+ @Value("${aikanshe.appkey:}")
|
|
|
+ private String aikansheAppkey;
|
|
|
+
|
|
|
+ @Value("${aikanshe.base-url:}")
|
|
|
+ private String aikansheBaseUrl;
|
|
|
+
|
|
|
+ @Value("${aikanshe.token:}")
|
|
|
+ private String aikansheToken;
|
|
|
+
|
|
|
+ @Value("${aikanshe.desease:}")
|
|
|
+ private String aikansheDesease;
|
|
|
+
|
|
|
+ @Value("${aikanshe.confidence:}")
|
|
|
+ private double aikansheConfidence;
|
|
|
+
|
|
|
+ @Value("${aikanshe.lang:}")
|
|
|
+ private String aikansheLang;
|
|
|
+
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
|
private HttpHeaders authHeaders() {
|
|
|
@@ -251,20 +271,188 @@ public class AIService {
|
|
|
|
|
|
/**
|
|
|
* 舌诊图像分析
|
|
|
- * 调用 LangGraph TongueDiagnosisAgent(glm-5 视觉模型)返回结构化舌诊结果
|
|
|
+ * 先调用 aikanshe.com API,失败时回退至 LangGraph TongueDiagnosisAgent
|
|
|
*/
|
|
|
public Map<String, Object> sendTongueDiagnosis(byte[] imageBytes, String filename, Long userId) {
|
|
|
if (imageBytes == null || imageBytes.length == 0) {
|
|
|
return mockTongueResult();
|
|
|
}
|
|
|
+ // 第一步:尝试调用 aikanshe.com API
|
|
|
+ Map<String, Object> aikansheResult = callAikansheTongueDiagnosis(imageBytes, filename, userId);
|
|
|
+ if (aikansheResult != null && !aikansheResult.isEmpty()
|
|
|
+ && aikansheResult.containsKey("overall_assessment")) {
|
|
|
+ log.info("使用 aikanshe.com API 结果");
|
|
|
+ return aikansheResult;
|
|
|
+ }
|
|
|
+ log.info("aikanshe.com API 无结果或失败,回退至 LangGraph");
|
|
|
+ // 第二步:回退至 LangGraph
|
|
|
Map<String, Object> result = aiGateway.analyzeTongue(imageBytes, filename, userId);
|
|
|
if (result != null && !result.isEmpty()) {
|
|
|
+ log.info("使用 LangGraph 结果");
|
|
|
return result;
|
|
|
}
|
|
|
log.warn("LangGraph tongue diagnosis 未返回结果, 使用 mock 兜底");
|
|
|
return mockTongueResult();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 调用 愛看舌 AI 舌诊 API
|
|
|
+ * 支持 appkey 认证或 JWT token 认证
|
|
|
+ */
|
|
|
+ private Map<String, Object> callAikansheTongueDiagnosis(byte[] imageBytes, String filename, Long userId) {
|
|
|
+ // 上传图片并获取URL(这里先存储,实际生产中应使用已有的 storageService)
|
|
|
+ // 对于 aikanshe.com,我们使用 imageUrl 模式,假设图片已经上传或使用 data URL
|
|
|
+ String imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
|
|
|
+ String imageDataUrl = "data:image/jpeg;base64," + imageBase64;
|
|
|
+
|
|
|
+ Map<String, Object> body = new LinkedHashMap<>();
|
|
|
+ body.put("age", userId != null ? userId : 0); // 实际项目中应传入真实年龄
|
|
|
+ body.put("male", 1); // 默认男性,实际应根据用户性别传入
|
|
|
+ body.put("name", "用户"); // 实际应根据用户昵称传入
|
|
|
+ body.put("imageUrl", imageDataUrl);
|
|
|
+ body.put("appkey", aikansheAppkey);
|
|
|
+ body.put("desease", aikansheDesease);
|
|
|
+ body.put("confidence", aikansheConfidence);
|
|
|
+ body.put("lang", aikansheLang);
|
|
|
+
|
|
|
+ String url = aikansheBaseUrl + "/agency/quanxi";
|
|
|
+
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+
|
|
|
+ // 使用 appkey 或 token 认证
|
|
|
+ if (aikansheAppkey != null && !aikansheAppkey.isEmpty()) {
|
|
|
+ headers.set("x-appkey", aikansheAppkey);
|
|
|
+ } else if (aikansheToken != null && !aikansheToken.isEmpty()) {
|
|
|
+ headers.setBearerAuth(aikansheToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(body), headers);
|
|
|
+try {
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
+ JsonNode root = objectMapper.readTree(response.getBody());
|
|
|
+ if (root.has("code") && root.get("code").asText().equals("20000")) {
|
|
|
+ JsonNode data = root.get("data");
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ if (data.has("feel")) result.put("overall_assessment", data.get("feel").asText());
|
|
|
+ if (data.has("recomand")) result.put("indicators", parseAikansheIndicators(data));
|
|
|
+ if (data.has("id")) result.put("recordId", data.get("id").asLong());
|
|
|
+ if (data.has("createTime")) result.put("createTime", data.get("createTime").asText());
|
|
|
+ log.info("愛看舌 API 调用成功");
|
|
|
+ return result;
|
|
|
+ } else {
|
|
|
+ log.warn("愛看舌 API 返回错误代码: {}", root.get("code").asText());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("愛看舌 API 调用异常: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ // 返回 null 表示调用失败,由上层 sendTongueDiagnosis 决定是否回退
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> parseAikansheIndicators(JsonNode data) {
|
|
|
+ List<Map<String, Object>> indicators = new ArrayList<>();
|
|
|
+
|
|
|
+ // --- 舌色 ---
|
|
|
+ if (data.has("feel")) {
|
|
|
+ String feel = data.get("feel").asText();
|
|
|
+ boolean found = false;
|
|
|
+ for (String line : feel.split("\\n")) {
|
|
|
+ if (line.contains("舌色") || line.contains("舌")) {
|
|
|
+ String[] parts = line.split(":|:");
|
|
|
+ if (parts.length >= 2) {
|
|
|
+ String color = parts[1].trim().split("[,, ]")[0];
|
|
|
+ if (isValidTongueColor(color)) {
|
|
|
+ indicators.add(makeIndicator("tongue_color", color));
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!found) {
|
|
|
+ indicators.add(makeIndicator("tongue_color", "淡红"));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("tongue_color", "淡红"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 苔色 ---
|
|
|
+ if (data.has("botai")) {
|
|
|
+ int botaiVal = data.get("botai").asInt(0);
|
|
|
+ indicators.add(makeIndicator("coating_color", botaiVal == 1 ? "有苔" : "无苔"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("coating_color", "无苔"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 苔质 ---
|
|
|
+ if (data.has("botai")) {
|
|
|
+ int botaiVal = data.get("botai").asInt(0);
|
|
|
+ indicators.add(makeIndicator("coating_texture", botaiVal == 1 ? "滑腻" : "润"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("coating_texture", "润"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 裂纹 ---
|
|
|
+ if (data.has("bagang")) {
|
|
|
+ String bagang = data.get("bagang").asText();
|
|
|
+ indicators.add(makeIndicator("fissure", (bagang.contains("实") || bagang.contains("虚")) ? "有" : "无"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("fissure", "无"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 齿痕 ---
|
|
|
+ if (data.has("chihen")) {
|
|
|
+ int chihenVal = data.get("chihen").asInt(0);
|
|
|
+ indicators.add(makeIndicator("teeth_mark", chihenVal == 1 ? "有齿痕" : "无齿痕"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("teeth_mark", "无齿痕"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 舌下络脉 ---
|
|
|
+ if (data.has("dianci")) {
|
|
|
+ int dianciVal = data.get("dianci").asInt(0);
|
|
|
+ indicators.add(makeIndicator("sublingual_vein", dianciVal == 1 ? "有点刺" : "无点刺"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("sublingual_vein", "无点刺"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 体质 ---
|
|
|
+ if (data.has("desease")) {
|
|
|
+ String desease = data.get("desease").asText();
|
|
|
+ if (desease.contains("气虚") || desease.contains("阳虚")) {
|
|
|
+ indicators.add(makeIndicator("constitution", "气虚质"));
|
|
|
+ } else if (desease.contains("阴虚") || desease.contains("阴虚火旺")) {
|
|
|
+ indicators.add(makeIndicator("constitution", "阴虚质"));
|
|
|
+ } else if (desease.contains("血瘀") || desease.contains("气血两虚")) {
|
|
|
+ indicators.add(makeIndicator("constitution", "血瘀质"));
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("constitution", "平和质"));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ indicators.add(makeIndicator("constitution", "平和质"));
|
|
|
+ }
|
|
|
+
|
|
|
+ return indicators;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isValidTongueColor(String color) {
|
|
|
+ String[] validColors = {"淡红", "淡白", "红", "绛", "青", "紫", "正常"};
|
|
|
+ for (String c : validColors) {
|
|
|
+ if (color.contains(c) || c.contains(color)) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> makeIndicator(String code, String value) {
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
+ m.put("code", code);
|
|
|
+ m.put("value", value);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 模拟舌诊结果(开发阶段使用)
|
|
|
*/
|