Просмотр исходного кода

feat: Java 侧画像 AI 生成网关 + AIService + AIBehaviorController

E2E Test Bot 1 месяц назад
Родитель
Сommit
ef5e6d2cc1

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIBehaviorController.java

@@ -0,0 +1,26 @@
+package com.etotem.cfc.controller.ai;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.FamilyContextService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/ai/behavior")
+public class AIBehaviorController {
+    @Resource
+    private FamilyContextService familyContextService;
+
+    @PostMapping("/summary")
+    public Result<Map<String, Object>> getBehaviorSummary(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long memberId = params.get("member_id") != null
+                ? Long.valueOf(params.get("member_id").toString()) : null;
+        Map<String, Object> context = familyContextService.buildContext(userId);
+        if (memberId != null) context.put("memberId", memberId);
+        return Result.success(context);
+    }
+}

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java

@@ -63,6 +63,9 @@ public class AIService {
     @Resource
     private AiUserFactMapper aiUserFactMapper;
 
+    @Resource
+    private FamilyContextService familyContextService;
+
     @Value("${dify.callback-secret:}")
     private String callbackSecret;
 
@@ -583,4 +586,18 @@ public class AIService {
             log.warn("保存关键事实失败: {}", e.getMessage());
         }
     }
+
+    /**
+     * 生成用户画像(通过AiGateway调用LangGraph)
+     */
+    public String generateUserPortrait(Long userId, Long memberId, String previousPortrait) {
+        try {
+            Map<String, Object> context = familyContextService.buildContext(userId);
+            if (memberId != null) context.put("memberId", memberId);
+            return aiGateway.generatePortrait(userId, memberId, previousPortrait, context);
+        } catch (Exception e) {
+            log.error("AI 生成画像失败: userId={}, memberId={}", userId, memberId, e);
+            return null;
+        }
+    }
 }

+ 30 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -385,6 +385,36 @@ public class AiGateway {
         return headers;
     }
 
+    /**
+     * 生成用户画像文本(调用 LangGraph /api/portrait/generate)
+     * @return 画像文本;失败返回 null(调用方决定 fallback)
+     */
+    public String generatePortrait(Long userId, Long memberId, String previousPortrait, Map<String, Object> context) {
+        if (!enabled || isCircuitOpen()) return null;
+        try {
+            ObjectNode body = objectMapper.createObjectNode();
+            body.put("user_id", userId);
+            if (memberId != null) body.put("member_id", memberId);
+            if (previousPortrait != null) body.put("previous_portrait", previousPortrait);
+            if (context != null) {
+                context.forEach((k, v) -> body.put(k, v != null ? v.toString() : ""));
+            }
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/portrait/generate";
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                JsonNode root = objectMapper.readTree(response.getBody());
+                consecutiveFailures.set(0);
+                return root.has("portrait_text") ? root.get("portrait_text").asText() : null;
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway generatePortrait 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
+
     /**
      * 识别图片中的食材
      */

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java

@@ -2,8 +2,11 @@ package com.etotem.cfc.service.impl;
 
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.AIService;
 import com.etotem.cfc.service.PortraitService;
 import com.etotem.cfc.service.ProfileReadService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -13,10 +16,14 @@ import java.util.Map;
 @Service
 public class PortraitServiceImpl implements PortraitService {
 
+    private static final Logger log = LoggerFactory.getLogger(PortraitServiceImpl.class);
+
     @Resource
     private UserMapper userMapper;
     @Resource
     private ProfileReadService profileReadService;
+    @Resource
+    private AIService aiService;
 
     @Override
     public String buildPortraitPrompt(Long userId, Long memberId) {
@@ -41,6 +48,36 @@ public class PortraitServiceImpl implements PortraitService {
         userMapper.updateById(user);
     }
 
+    @Override
+    public String generatePortrait(Long userId, Long memberId) {
+        try {
+            String result = aiService.generateUserPortrait(userId, memberId, null);
+            if (result != null && !result.trim().isEmpty()) {
+                updatePortraitPrompt(userId, result);
+                return result;
+            }
+        } catch (Exception e) {
+            log.warn("AI 生成画像失败,回退到快照: userId={}, memberId={}", userId, memberId, e);
+        }
+        return renderSnapshot(memberId);
+    }
+
+    @Override
+    public String regeneratePortrait(Long userId, Long memberId) {
+        User user = userMapper.selectById(userId);
+        String currentPortrait = user != null ? user.getPortraitPrompt() : null;
+        try {
+            String result = aiService.generateUserPortrait(userId, memberId, currentPortrait);
+            if (result != null && !result.trim().isEmpty()) {
+                updatePortraitPrompt(userId, result);
+                return result;
+            }
+        } catch (Exception e) {
+            log.warn("AI 重画画像失败,使用旧画像: userId={}, memberId={}", userId, memberId, e);
+        }
+        return currentPortrait;
+    }
+
     @Override
     public Map<String, Object> getPortrait(Long userId, Long targetUserId, Long memberId) {
         Long effectiveUserId = (targetUserId != null) ? targetUserId : userId;