For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 用户画像由 AI 根据系统行为数据 + 五维指标自动生成,用户可编辑,提供「重画」功能(参考旧画像 + 考虑变化因素)
Architecture:
POST /api/user/portrait/generate、POST /api/user/portrait/regenerate,调用 LangGraph portrait 生成/重画 graphportrait_generation_graph.py(LLM 分析行为+五维指标生成画像),app/api/portrait.py FastAPI 端点portrait-edit.vue 增加「生成画像」「重画」按钮、加载态、结果展示Tech Stack: Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / Python 3.10 + FastAPI + LangGraph + OpenAI / uni-app Vue 2
@PostMapping,禁止 @GetMapping/@PutMapping/@DeleteMappingResult<T> (code/message/data)JwtInterceptor 拦截 /api/**@Resource,字段名必须与 Bean Name 一致@TableName + @TableId(type = IdType.AUTO)?.、CSS Grid、:key 表达式、new Date(string)(用 parseDate())mvn clean compile / python3 -m py_compile 为唯一验证方式| 文件 | 类型 | 说明 |
|---|---|---|
service/PortraitService.java |
修改 | 新增 generatePortrait、regeneratePortrait 接口方法 |
service/impl/PortraitServiceImpl.java |
修改 | 实现调用 AiGateway / portrait 生成逻辑 |
controller/user/PortraitController.java |
修改 | 新增 /generate、/regenerate 端点 |
service/AIService.java |
修改 | 新增 generateUserPortrait 方法(走 AiGateway 调用 LangGraph) |
| 文件 | 类型 | 说明 |
|---|---|---|
graphs/portrait_generation_graph.py |
新增 | AI 生成/重画用户画像的 LangGraph |
api/portrait.py |
新增 | FastAPI 路由:POST /api/portrait/generate、POST /api/portrait/regenerate |
main.py |
修改 | 注册 portrait.router |
tools/java_client.py |
修改 | 新增 get_behavior_summary(user_id, member_id) 获取行为数据 |
| 文件 | 类型 | 说明 |
|---|---|---|
pages/profile/portrait-edit.vue |
修改 | 增加「生成画像」「重画」按钮、加载态、错误处理 |
utils/api.js |
修改 | 新增 generatePortrait、regeneratePortrait 封装 |
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.javaInterfaces:
Produces: 新增 generatePortrait(Long userId, Long memberId)、regeneratePortrait(Long userId, Long memberId) 方法签名
[ ] Step 1: 扩展 PortraitService 接口
package com.etotem.cfc.service;
import java.util.Map;
public interface PortraitService {
/** 获取用户画像文本(自定义 prompt + 快照渲染) */
String buildPortraitPrompt(Long userId, Long memberId);
/** 更新用户自定义画像 prompt */
void updatePortraitPrompt(Long userId, String portraitPrompt);
/** 读取用户画像(含渲染快照,用于前端展示) */
Map<String, Object> getPortrait(Long userId, Long targetUserId, Long memberId);
/** AI 生成用户画像(分析行为+五维指标) */
String generatePortrait(Long userId, Long memberId);
/** AI 重画用户画像(参考旧画像+变化因素) */
String regeneratePortrait(Long userId, Long memberId);
}
[ ] Step 2: 编译验证
cd cfc-backend && mvn clean compile -q
[ ] Step 3: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.java
git commit -m "feat: PortraitService 新增 generatePortrait/regeneratePortrait 接口"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.javaInterfaces:
AIService.generateUserPortrait(userId, memberId, previousPortrait) 返回 AI 生成的画像文本Produces: generatePortrait、regeneratePortrait 实现
[ ] Step 1: 注入 AIService
@Resource
private AIService aiService;
[ ] Step 2: 实现 generatePortrait
@Override
public String generatePortrait(Long userId, Long memberId) {
// 调用 AIService 生成画像
String generated = aiService.generateUserPortrait(userId, memberId, null);
if (generated != null && !generated.trim().isEmpty()) {
// 保存到用户表
updatePortraitPrompt(userId, generated.trim());
return generated.trim();
}
// 失败时降级为快照渲染
return renderSnapshot(memberId);
}
[ ] Step 3: 实现 regeneratePortrait
@Override
public String regeneratePortrait(Long userId, Long memberId) {
// 1. 获取当前画像
User user = userMapper.selectById(userId);
String currentPortrait = user != null ? user.getPortraitPrompt() : null;
// 2. 调用 AIService 重画(传入旧画像作为参考)
String regenerated = aiService.generateUserPortrait(userId, memberId, currentPortrait);
if (regenerated != null && !regenerated.trim().isEmpty()) {
updatePortraitPrompt(userId, regenerated.trim());
return regenerated.trim();
}
// 失败保持原画像
return currentPortrait;
}
[ ] Step 4: 编译验证
cd cfc-backend && mvn clean compile -q
[ ] Step 5: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java
git commit -m "feat: PortraitServiceImpl 实现 generatePortrait/regeneratePortrait"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java (接口)cfc-backend/src/main/java/com/etotem/cfc/service/impl/AIServiceImpl.java (实现)Interfaces:
Produces: generateUserPortrait(Long userId, Long memberId, String previousPortrait) → String
[ ] Step 1: 扩展 AIService 接口
/**
* AI 生成用户画像
* @param userId 用户ID
* @param memberId 成员ID(可空,为空时仅基于用户自身行为)
* @param previousPortrait 旧画像文本(重画时传入,首次生成为 null)
* @return 生成的画像文本,失败返回 null
*/
String generateUserPortrait(Long userId, Long memberId, String previousPortrait);
[ ] Step 2: 实现 AIServiceImpl.generateUserPortrait
@Override
public String generateUserPortrait(Long userId, Long memberId, String previousPortrait) {
try {
// 1. 获取用户行为摘要 + 五维画像
Map<String, Object> context = familyContextService.buildContext(userId);
if (memberId != null) {
context.put("memberId", memberId);
}
// 2. 构建输入
Map<String, Object> inputs = new HashMap<>();
inputs.put("user_id", userId);
inputs.put("member_id", memberId);
inputs.put("previous_portrait", previousPortrait);
inputs.put("behavior_summary", context.get("behaviorSummary"));
inputs.put("profile_snapshot", context.get("profileSnapshot"));
// 3. 调用 LangGraph portrait 生成 graph
// 端点:POST /api/portrait/generate (Python 侧)
Map<String, Object> resp = aiGateway.chat("", userId, null, inputs);
// aiGateway.chat 会路由到 portrait graph
// 这里需要专门的方法,或者扩展 aiGateway
// 临时方案:直接 HTTP 调用 Python 服务
// TODO: 在 AiGateway 增加专用方法
String result = callPortraitGraph(userId, memberId, previousPortrait, context);
return result;
} catch (Exception e) {
log.error("AI 生成画像失败: userId={}, memberId={}", userId, memberId, e);
return null;
}
}
注:
AiGateway目前通过chat方法统一路由。需要在AiGateway增加generatePortrait专用方法,或扩展chat支持portrait类型路由。
Files:
cfc-backend/src/main/java/com/etotem/cfc/gateway/AiGateway.javaModify: cfc-backend/src/main/java/com/etotem/cfc/gateway/impl/AiGatewayImpl.java
// AiGateway.java
String generatePortrait(Long userId, Long memberId, String previousPortrait, Map<String, Object> context);
// AiGatewayImpl.java
@Override
public String generatePortrait(Long userId, Long memberId, String previousPortrait, Map<String, Object> context) {
Map<String, Object> inputs = new HashMap<>();
inputs.put("user_id", userId);
inputs.put("member_id", memberId);
inputs.put("previous_portrait", previousPortrait);
inputs.put("behavior_summary", context.get("behaviorSummary"));
inputs.put("profile_snapshot", context.get("profileSnapshot"));
// 专用 graph 类型标识
inputs.put("__graph_type", "portrait_generation");
Map<String, Object> resp = webClient.post()
.uri(pythonBaseUrl + "/api/portrait/generate")
.bodyValue(inputs)
.retrieve()
.bodyToMono(Map.class)
.block();
// 解析返回的 portrait_text
return resp != null ? (String) resp.get("portrait_text") : null;
}
[ ] Step 4: 编译验证
cd cfc-backend && mvn clean compile -q
[ ] Step 5: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java \
cfc-backend/src/main/java/com/etotem/cfc/service/impl/AIServiceImpl.java \
cfc-backend/src/main/java/com/etotem/cfc/gateway/AiGateway.java \
cfc-backend/src/main/java/com/etotem/cfc/gateway/impl/AiGatewayImpl.java
git commit -m "feat: AIService + AiGateway 新增 portrait 生成调用"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.javaInterfaces:
Consumes: PortraitService.generatePortrait、regeneratePortrait
[ ] Step 1: 新增 /generate 端点
@PostMapping("/generate")
public Result<Map<String, Object>> generatePortrait(
@RequestAttribute("userId") Long userId,
@RequestBody Map<String, Object> params) {
Long memberId = params.get("memberId") != null
? Long.valueOf(params.get("memberId").toString()) : null;
String portrait = portraitService.generatePortrait(userId, memberId);
Map<String, Object> result = new HashMap<>();
result.put("portraitPrompt", portrait);
// 同时返回快照供前端展示
if (memberId != null) {
Map<String, Object> profile = profileReadService.getProfile(memberId);
result.put("renderedSnapshot", renderSnapshotFull(profile));
}
return Result.success(result);
}
[ ] Step 2: 新增 /regenerate 端点
@PostMapping("/regenerate")
public Result<Map<String, Object>> regeneratePortrait(
@RequestAttribute("userId") Long userId,
@RequestBody Map<String, Object> params) {
Long memberId = params.get("memberId") != null
? Long.valueOf(params.get("memberId").toString()) : null;
String portrait = portraitService.regeneratePortrait(userId, memberId);
Map<String, Object> result = new HashMap<>();
result.put("portraitPrompt", portrait);
if (memberId != null) {
Map<String, Object> profile = profileReadService.getProfile(memberId);
result.put("renderedSnapshot", renderSnapshotFull(profile));
}
return Result.success(result);
}
[ ] Step 3: 复用 renderSnapshotFull(已存在于 impl 中,需提取为工具方法或在 controller 内复制)
[ ] Step 4: 编译验证
cd cfc-backend && mvn clean compile -q
[ ] Step 5: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java
git commit -m "feat: PortraitController 新增 /generate /regenerate 端点"
Files:
Modify: cfc-langgraph/app/tools/java_client.py
[ ] Step 1: 新增 get_behavior_summary 方法
async def get_behavior_summary(self, user_id: int, member_id: int | None) -> dict:
"""获取用户行为摘要(任务完成率、挑战参与、积分流水、健康报告等)"""
client = await self._get_client()
body = {"user_id": str(user_id)}
if member_id:
body["member_id"] = member_id
resp = await client.post("/api/ai/behavior/summary", json=body)
data = resp.json()
if data.get("code") == 200:
return data.get("data", {})
return {}
[ ] Step 2: py_compile 验证
cd cfc-langgraph && python3 -m py_compile app/tools/java_client.py
[ ] Step 3: Commit
git add cfc-langgraph/app/tools/java_client.py
git commit -m "feat: JavaClient 新增 get_behavior_summary 获取行为数据"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java 或新增 AIBehaviorController.java复用
AIChatController的familyContextService.buildContext()逻辑,提取行为摘要
[ ] Step 1: 新增 AIBehaviorController
@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);
}
// 提取行为相关字段
Map<String, Object> summary = new HashMap<>();
summary.put("behaviorSummary", context.get("behaviorSummary"));
summary.put("profileSnapshot", context.get("profileSnapshot"));
summary.put("recentTasks", context.get("recentTasks"));
summary.put("recentChallenges", context.get("recentChallenges"));
summary.put("pointsHistory", context.get("pointsHistory"));
summary.put("healthReports", context.get("healthReports"));
return Result.success(summary);
}
}
[ ] Step 2: 编译验证
cd cfc-backend && mvn clean compile -q
[ ] Step 3: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIBehaviorController.java
git commit -m "feat: 新增 AIBehaviorController /behavior/summary 端点"
Files:
cfc-langgraph/app/graphs/portrait_generation_graph.pyInterfaces:
PortraitGenerationState { user_id, member_id, previous_portrait, behavior_summary, profile_snapshot, portrait_text }Returns: { "portrait_text": "生成的画像文本" }
[ ] Step 1: 创建 graph 文件
"""用户画像生成/重画 LangGraph
输入:
- user_id: 用户ID
- member_id: 成员ID(可空)
- previous_portrait: 旧画像文本(重画时传入,首次生成为 None)
- behavior_summary: 行为摘要(任务完成率、挑战、积分、健康报告等)
- profile_snapshot: 五维画像快照(dimension_scores、body_metrics、mind_metrics、problem_domains)
输出:
- portrait_text: 生成的画像文本(中文,自然语言描述)
"""
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from app.tools.java_client import JavaClient
from app.config import settings
import logging
logger = logging.getLogger(__name__)
class PortraitGenerationState(TypedDict):
user_id: int
member_id: int | None
previous_portrait: str | None
behavior_summary: dict | None
profile_snapshot: dict | None
portrait_text: str | None
SYSTEM_PROMPT = """你是「浠艾福」家庭成长平台的 AI 画像分析师。
任务:根据用户的行为数据、五维能量指标、健康画像,生成一段自然语言的「用户画像」文本。
画像用途:作为后续所有 AI 对话(健康教练、管家、营养师、通用聊天)的 SystemMessage 注入,帮助 AI 更懂用户,给出更个性化的建议。
生成要求:
1. 语言自然、温暖、具体,像一位了解家庭的成长顾问写的观察笔记
2. 必须包含:核心特质、行为模式、关注点、潜在需求
3. 长度:200-500 字,不要过长
4. 不要列举原始数据(如"身 7.2 智 6.5"),要转化为洞察(如"身体维度相对突出,但智力维度有提升空间")
5. 如有 previous_portrait,重画时要体现"变化"与"延续":哪些特质稳定,哪些有新变化,原因可能是什么
输出格式:纯文本,无 JSON,无标记。"""
def create_portrait_generation_graph():
llm = ChatOpenAI(
model=settings.llm_model,
api_key=settings.llm_api_key,
base_url=settings.llm_base_url,
temperature=0.7, # 稍高温度增加多样性
)
java = JavaClient()
builder = StateGraph(PortraitGenerationState)
async def fetch_data(state: PortraitGenerationState) -> dict:
"""并行拉取行为摘要 + 画像快照"""
user_id = state["user_id"]
member_id = state.get("member_id")
# 并行获取
behavior_task = java.get_behavior_summary(user_id, member_id)
profile_task = java.get_member_profile(member_id) if member_id else None
behavior = await behavior_task
profile = await profile_task if profile_task else {}
return {
"behavior_summary": behavior,
"profile_snapshot": profile,
}
async def generate_portrait(state: PortraitGenerationState) -> dict:
"""LLM 生成画像"""
previous = state.get("previous_portrait")
behavior = state.get("behavior_summary") or {}
profile = state.get("profile_snapshot") or {}
# 构建用户画像上下文
context_parts = []
if profile:
member = profile.get("member") or {}
name = member.get("name", "用户")
age = member.get("age")
gender = member.get("gender")
context_parts.append(f"用户:{name},{age}岁,{gender}")
dims = profile.get("dimension_scores") or {}
if dims:
context_parts.append(
f"五维评分:身 {dims.get('body','?')} 智 {dims.get('wisdom','?')} "
f"心 {dims.get('mind','?')} 行 {dims.get('action','?')} 富 {dims.get('wealth','?')}"
)
body = profile.get("body_metrics") or {}
if body.get("sleep_dur_avg"):
context_parts.append(f"平均睡眠:{body['sleep_dur_avg']}h/天")
if body.get("exercise_count_week"):
context_parts.append(f"周运动:{body['exercise_count_week']}次")
mind = profile.get("mind_metrics") or {}
if mind.get("stress_avg"):
context_parts.append(f"平均压力:{mind['stress_avg']}/10")
problems = profile.get("problem_domains") or []
if problems:
context_parts.append(f"关注问题:{', '.join(problems[:5])}")
if behavior:
context_parts.append("行为摘要:" + str(behavior))
context_text = "\n".join(context_parts)
# 构建 HumanMessage
if previous:
human_content = f"""【重画任务】请基于以下最新数据,重新生成用户画像。
【原画像(参考)】:
{previous}
【最新数据】:
{context_text}
要求:保留原画像中仍有效的核心特质,更新已变化的部分,体现"变化与延续"。"""
else:
human_content = f"""【生成任务】请基于以下数据,生成用户画像。
【数据】:
{context_text}"""
messages = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=human_content),
]
response = await llm.ainvoke(messages)
portrait_text = response.content.strip()
logger.info("生成画像完成: user_id=%d, member_id=%s, len=%d",
state["user_id"], state.get("member_id"), len(portrait_text))
return {"portrait_text": portrait_text}
builder.add_node("fetch_data", fetch_data)
builder.add_node("generate_portrait", generate_portrait)
builder.add_edge(START, "fetch_data")
builder.add_edge("fetch_data", "generate_portrait")
builder.add_edge("generate_portrait", END)
return builder.compile()
# 单例
_portrait_graph = None
def get_portrait_graph():
global _portrait_graph
if _portrait_graph is None:
_portrait_graph = create_portrait_generation_graph()
return _portrait_graph
[ ] Step 2: py_compile 验证
cd cfc-langgraph && python3 -m py_compile app/graphs/portrait_generation_graph.py
[ ] Step 3: Commit
git add cfc-langgraph/app/graphs/portrait_generation_graph.py
git commit -m "feat: 新增 portrait_generation_graph.py AI 生成/重画用户画像"
Files:
cfc-langgraph/app/api/portrait.pycfc-langgraph/app/main.pyInterfaces:
POST /api/portrait/generate → { "portrait_text": "..." }POST /api/portrait/regenerate → { "portrait_text": "..." }(同一实现,区别在于是否传 previous_portrait)
[ ] Step 1: 创建 api/portrait.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.graphs.portrait_generation_graph import get_portrait_graph
import logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/portrait", tags=["portrait"])
class PortraitGenerateRequest(BaseModel):
user_id: int
member_id: int | None = None
previous_portrait: str | None = None
behavior_summary: dict | None = None
profile_snapshot: dict | None = None
class PortraitGenerateResponse(BaseModel):
portrait_text: str
@router.post("/generate", response_model=PortraitGenerateResponse)
async def generate_portrait(req: PortraitGenerateRequest):
"""AI 生成用户画像(首次生成)"""
try:
graph = get_portrait_graph()
state = {
"user_id": req.user_id,
"member_id": req.member_id,
"previous_portrait": None,
"behavior_summary": req.behavior_summary,
"profile_snapshot": req.profile_snapshot,
"portrait_text": None,
}
result = await graph.ainvoke(state)
return PortraitGenerateResponse(portrait_text=result["portrait_text"])
except Exception as e:
logger.exception("画像生成失败")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/regenerate", response_model=PortraitGenerateResponse)
async def regenerate_portrait(req: PortraitGenerateRequest):
"""AI 重画用户画像(参考旧画像)"""
try:
graph = get_portrait_graph()
state = {
"user_id": req.user_id,
"member_id": req.member_id,
"previous_portrait": req.previous_portrait,
"behavior_summary": req.behavior_summary,
"profile_snapshot": req.profile_snapshot,
"portrait_text": None,
}
result = await graph.ainvoke(state)
return PortraitGenerateResponse(portrait_text=result["portrait_text"])
except Exception as e:
logger.exception("画像重画失败")
raise HTTPException(status_code=500, detail=str(e))
[ ] Step 2: 注册路由到 main.py
# main.py 第 30 行附近
from app.api import portrait
...
app.include_router(portrait.router)
[ ] Step 3: py_compile 验证
cd cfc-langgraph && python3 -m py_compile app/api/portrait.py app/graphs/portrait_generation_graph.py
[ ] Step 4: Commit
git add cfc-langgraph/app/api/portrait.py cfc-langgraph/app/main.py
git commit -m "feat: Python 侧新增 portrait 生成/重画 API 端点"
Files:
Modify: cfc-frontend/utils/api.js
[ ] Step 1: 新增封装
// 用户画像
export function getPortrait(params = {}) {
return request('/api/user/portrait/get', 'POST', params)
}
export function editPortrait(data) {
return request('/api/user/portrait/edit', 'POST', data)
}
export function generatePortrait(params = {}) {
return request('/api/user/portrait/generate', 'POST', params)
}
export function regeneratePortrait(params = {}) {
return request('/api/user/portrait/regenerate', 'POST', params)
}
[ ] Step 2: 语法检查
node --check <(sed -n '/<script>/,/<\/script>/p' cfc-frontend/utils/api.js | sed '1d;$d')
[ ] Step 3: Commit
git add cfc-frontend/utils/api.js
git commit -m "feat: api.js 新增 generatePortrait/regeneratePortrait 封装"
Files:
Modify: cfc-frontend/pages/profile/portrait-edit.vue
[ ] Step 1: 升级 template(增加生成/重画按钮、加载态)
<template>
<view class="portrait-edit-page">
<view class="section">
<view class="section-title">我的画像描述</view>
<view class="hint">请输入你的个人画像描述,AI 会根据这些信息提供更有针对性的建议(最多 2000 字)</view>
<!-- 操作按钮行 -->
<view class="action-row">
<button class="action-btn" :loading="generating" @click="onGenerate" v-if="!portraitPrompt">
🪄 AI 生成画像
</button>
<button class="action-btn secondary" :loading="regenerating" @click="onRegenerate" v-if="portraitPrompt">
🔄 重画画像
</button>
</view>
<textarea
class="portrait-textarea"
:value="portraitPrompt"
placeholder="例如:我家孩子 8 岁,偏瘦,挑食,容易积食,睡眠不足..."
maxlength="2000"
@input="onInput"
></textarea>
<view class="char-count">{{ portraitPrompt.length }}/2000</view>
</view>
<view class="section" v-if="renderedSnapshot">
<view class="section-title">参考指标(自动生成)</view>
<view class="snapshot-text">{{ renderedSnapshot }}</view>
</view>
<button class="save-btn" :loading="saving" @click="onSave">保存</button>
</view>
</template>
[ ] Step 2: 升级 script(新增 generate/regenerate 方法)
import { getPortrait, editPortrait, generatePortrait, regeneratePortrait } from '../../utils/api.js'
export default {
data() {
return {
portraitPrompt: '',
renderedSnapshot: '',
saving: false,
generating: false,
regenerating: false
}
},
onLoad() {
this.loadPortrait()
},
methods: {
async loadPortrait() {
try {
const res = await getPortrait({ memberId: this.$store.state.currentMemberId })
if (res.code === 200 && res.data) {
this.portraitPrompt = res.data.portraitPrompt || ''
this.renderedSnapshot = res.data.renderedSnapshot || ''
}
} catch (e) {
console.error('加载画像失败', e)
}
},
onInput(e) {
this.portraitPrompt = e.detail.value
},
async onGenerate() {
this.generating = true
try {
const memberId = this.$store.state.currentMemberId
const res = await generatePortrait({ memberId })
if (res.code === 200 && res.data) {
this.portraitPrompt = res.data.portraitPrompt || ''
this.renderedSnapshot = res.data.renderedSnapshot || ''
uni.showToast({ title: '生成成功', icon: 'success' })
} else {
uni.showToast({ title: res.message || '生成失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '生成失败', icon: 'none' })
} finally {
this.generating = false
}
},
async onRegenerate() {
this.regenerating = true
try {
const memberId = this.$store.state.currentMemberId
const res = await regeneratePortrait({
memberId,
previousPortrait: this.portraitPrompt // 传入当前画像作为参考
})
if (res.code === 200 && res.data) {
this.portraitPrompt = res.data.portraitPrompt || ''
this.renderedSnapshot = res.data.renderedSnapshot || ''
uni.showToast({ title: '重画成功', icon: 'success' })
} else {
uni.showToast({ title: res.message || '重画失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '重画失败', icon: 'none' })
} finally {
this.regenerating = false
}
},
async onSave() {
this.saving = true
try {
const res = await editPortrait({ portraitPrompt: this.portraitPrompt })
if (res.code === 200) {
uni.showToast({ title: '保存成功', icon: 'success' })
this.loadPortrait()
} else {
uni.showToast({ title: res.message || '保存失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
this.saving = false
}
}
}
}
[ ] Step 3: 增加样式
.action-row {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
.action-btn {
flex: 1;
height: 72rpx;
border-radius: 36rpx;
font-size: 26rpx;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}
.action-btn.secondary {
background: linear-gradient(135deg, #F97316, #F59E0B);
}
[ ] Step 4: 语法检查
python -c "import re; f=open(r'D:\cfc\cfc-frontend\pages\profile\portrait-edit.vue','r',encoding='utf-8'); c=f.read(); f.close(); m=re.search(r'<script>(.*?)</script>',c,re.DOTALL); open(r'D:\tmp\portrait_check.mjs','w',encoding='utf-8').write(m.group(1))"
node --check D:\tmp\portrait_check.mjs
[ ] Step 5: Commit
git add cfc-frontend/pages/profile/portrait-edit.vue
git commit -m "feat: portrait-edit.vue 新增 AI 生成/重画按钮与逻辑"
[ ] Step 1: 后端编译
cd cfc-backend && mvn clean compile -q
[ ] Step 2: Python 编译
cd cfc-langgraph && python3 -m py_compile app/api/portrait.py app/graphs/portrait_generation_graph.py app/tools/java_client.py
[ ] Step 3: 前端语法检查
# portrait-edit.vue
python -c "import re; f=open(r'D:\cfc\cfc-frontend\pages\profile\portrait-edit.vue','r',encoding='utf-8'); c=f.read(); f.close(); m=re.search(r'<script>(.*?)</script>',c,re.DOTALL); open(r'D:\tmp\pe.mjs','w',encoding='utf-8').write(m.group(1))"
node --check D:\tmp\pe.mjs
# api.js
node --check <(sed -n '/<script>/,/<\/script>/p' cfc-frontend/utils/api.js | sed '1d;$d')
[ ] Step 4: 本地联调(需启动后端 9082 + LangGraph 9000)
POST /api/user/portrait/generate → 验证返回 portraitPromptPOST /api/user/portrait/regenerate → 验证返回新画像[ ] Step 5: 提交剩余改动
git add -A
git commit -m "feat: 用户画像 AI 生成/重画全栈实现"
git push
PortraitService 接口 → impl → controller → AIService → AiGatewayPortraitGenerateRequest Pydantic 模型与 Java 请求体对应generatePortrait({memberId})、regeneratePortrait({memberId, previousPortrait}) 与后端端点对应renderSnapshot),保留原画像Plan 完整,保存至 docs/superpowers/plans/2026-09-13-user-portrait-ai-generation.md。
Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?