面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 在所有 AI 对话端点(chat、health-coach、butler、nutrition)中注入用户画像 prompt,实现个性化对话体验。
架构:
users 表加 portrait_prompt 列,新增 PortraitService 和 PortraitController,在 4 个对话 Controller 方法中统一注入 portrait_prompt 到 inputsportrait_service.py,在 3 个 LangGraph(chat、health_coach、health_butler)的 generate_answer 节点中读取 portrait_prompt 并组装 SystemMessage;nutrition 端点走 Dify,由 Java 直接注入技术栈: Java 8 + Spring Boot、Python 3.10 + FastAPI + LangGraph、uni-app Vue 2
| 操作 | 路径 | 职责 |
|---|---|---|
| 修改 | cfc-backend/src/main/resources/schema.sql |
users 表加 portrait_prompt 列 |
| 修改 | cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java |
迁移脚本 |
| 修改 | cfc-backend/src/main/java/com/etotem/cfc/entity/User.java |
实体加 portraitPrompt 字段 |
| 新增 | cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.java |
接口 |
| 新增 | cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java |
实现(拉 portrait + 渲染快照) |
| 新增 | cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java |
两个 REST 端点 |
| 修改 | cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java |
4 个方法注入 portrait_prompt |
| 修改 | cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java |
sendNutritionMessage 注入 portrait_prompt |
| 操作 | 路径 | 职责 |
|---|---|---|
| 新增 | cfc-langgraph/app/portrait_service.py |
get_user_portrait_text 函数 + 缓存 |
| 修改 | cfc-langgraph/app/graphs/chat_graph.py |
注入画像段 |
| 修改 | cfc-langgraph/app/graphs/health_coach_graph.py |
注入画像段 |
| 修改 | cfc-langgraph/app/graphs/health_butler_graph.py |
注入画像段 |
| 操作 | 路径 | 职责 |
|---|---|---|
| 新增 | cfc-frontend/pages/profile/portrait-edit.vue |
画像编辑页 |
| 修改 | cfc-frontend/utils/api.js |
新增 getPortrait/editPortrait 封装 |
| 修改 | cfc-frontend/pages.json |
注册新页面 |
文件:
cfc-backend/src/main/resources/schema.sqlcfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java修改:cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
[ ] 步骤 1:在 schema.sql 追加 users 表列定义
找到 schema.sql 中 users 表的 CREATE TABLE 语句末尾(在 mascot 列之后),追加:
portrait_prompt TEXT COMMENT '用户自定义画像 prompt(为空时自动从 profile_snapshot 渲染)',
先 grep 确认 mascot 列位置:
grep -n "mascot" cfc-backend/src/main/resources/schema.sql
在 DatabaseInitializer.java 中搜索 // 迁移 找最新编号,追加:
// 迁移N: users 表添加 portrait_prompt 列(用户画像 prompt 拼装需求)
try {
jdbcTemplate.execute("ALTER TABLE users ADD COLUMN portrait_prompt TEXT COMMENT '用户自定义画像 prompt'");
log.info("已添加 portrait_prompt 列到 users 表");
} catch (Exception e) {
// 列已存在,忽略错误
}
在 private String mascot; 之后追加:
@TableField("portrait_prompt")
private String portraitPrompt;
同时生成 getter/setter(IntelliJ 快捷键 Alt+Insert 或手写)。
[ ] 步骤 4:编译验证
cd cfc-backend && /bwydata/maven/bin/mvn clean compile -q > /tmp/opencode/t1.log 2>&1; M=$?; tail -3 /tmp/opencode/t1.log; test $M -eq 0 && echo MVN_OK
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
GIT_MASTER=1 git add cfc-backend/src/main/resources/schema.sql \
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
GIT_MASTER=1 git commit -m "feat: users 表添加 portrait_prompt 列"
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.javacfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java新增:cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java
[ ] 步骤 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);
}
[ ] 步骤 2:创建 PortraitServiceImpl
package com.etotem.cfc.service.impl;
import com.etotem.cfc.entity.User;
import com.etotem.cfc.mapper.UserMapper;
import com.etotem.cfc.service.PortraitService;
import com.etotem.cfc.service.ProfileReadService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Service
public class PortraitServiceImpl implements PortraitService {
@Resource
private UserMapper userMapper;
@Resource
private ProfileReadService profileReadService;
@Override
public String buildPortraitPrompt(Long userId, Long memberId) {
User user = userMapper.selectById(userId);
if (user == null) return null;
String custom = user.getPortraitPrompt();
if (custom == null || custom.trim().isEmpty()) {
return renderSnapshot(memberId);
}
String snapshot = renderSnapshot(memberId);
if (snapshot == null || snapshot.trim().isEmpty()) {
return custom.trim();
}
return custom.trim() + "\n\n---参考指标---\n" + snapshot;
}
@Override
public void updatePortraitPrompt(Long userId, String portraitPrompt) {
User user = new User();
user.setId(userId);
user.setPortraitPrompt(portraitPrompt);
userMapper.updateById(user);
}
@Override
public Map<String, Object> getPortrait(Long userId, Long targetUserId, Long memberId) {
Long effectiveUserId = (targetUserId != null) ? targetUserId : userId;
User user = userMapper.selectById(effectiveUserId);
Map<String, Object> result = new HashMap<>();
result.put("portraitPrompt", user != null ? user.getPortraitPrompt() : null);
if (memberId != null) {
Map<String, Object> profile = profileReadService.getProfile(memberId);
result.put("renderedSnapshot", renderSnapshotFull(profile));
}
return result;
}
private String renderSnapshot(Long memberId) {
if (memberId == null) return null;
Map<String, Object> profile = profileReadService.getProfile(memberId);
return renderSnapshotFull(profile);
}
private String renderSnapshotFull(Map<String, Object> profile) {
if (profile == null || profile.containsKey("error")) return null;
StringBuilder sb = new StringBuilder();
Map<String, Object> member = (Map<String, Object>) profile.get("member");
if (member != null) {
sb.append("## 画像对象:");
sb.append(member.getOrDefault("name", "用户"));
sb.append("(").append(member.getOrDefault("age", "?")).append("岁,");
String gender = String.valueOf(member.getOrDefault("gender", ""));
sb.append("男".equals(gender) ? "男" : "女".equals(gender) ? "女" : "未知");
sb.append(")\n");
}
Map<String, Object> dims = (Map<String, Object>) profile.get("dimension_scores");
if (dims != null && !dims.isEmpty()) {
sb.append(String.format("五维评分:身 %s 智 %s 心 %s 行 %s 富 %s\n",
dims.getOrDefault("body", "?"), dims.getOrDefault("wisdom", "?"),
dims.getOrDefault("mind", "?"), dims.getOrDefault("action", "?"),
dims.getOrDefault("wealth", "?")));
}
Map<String, Object> body = (Map<String, Object>) profile.get("body_metrics");
if (body != null) {
if (body.get("sleep_dur_avg") != null)
sb.append(String.format("平均睡眠:%s小时/天\n", body.get("sleep_dur_avg")));
if (body.get("exercise_count_week") != null)
sb.append(String.format("周运动频次:%s次\n", body.get("exercise_count_week")));
}
Map<String, Object> mind = (Map<String, Object>) profile.get("mind_metrics");
if (mind != null && mind.get("stress_avg") != null)
sb.append(String.format("平均压力:%s/10\n", mind.get("stress_avg")));
@SuppressWarnings("unchecked")
java.util.List<String> problems = (java.util.List<String>) profile.get("problem_domains");
if (problems != null && !problems.isEmpty())
sb.append("关注问题域:").append(String.join(", ", problems.subList(0, Math.min(5, problems.size())))).append("\n");
return sb.toString().trim().isEmpty() ? null : sb.toString().trim();
}
}
[ ] 步骤 3:创建 PortraitController
package com.etotem.cfc.controller.user;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.PortraitService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/user/portrait")
public class PortraitController {
@Resource
private PortraitService portraitService;
@PostMapping("/get")
public Result<Map<String, Object>> getPortrait(
@RequestAttribute("userId") Long userId,
@RequestBody Map<String, Object> params) {
Long targetUserId = params.get("userId") != null
? Long.valueOf(params.get("userId").toString()) : null;
Long memberId = params.get("memberId") != null
? Long.valueOf(params.get("memberId").toString()) : null;
// 普通用户不能查他人
if (targetUserId != null && !targetUserId.equals(userId)) {
// 可加管理员权限检查,此处暂放行(前端只传自己)
}
return Result.success(portraitService.getPortrait(userId, targetUserId, memberId));
}
@PostMapping("/edit")
public Result<String> editPortrait(
@RequestAttribute("userId") Long userId,
@RequestBody Map<String, Object> params) {
Object promptObj = params.get("portraitPrompt");
String portraitPrompt = promptObj != null ? promptObj.toString() : null;
if (portraitPrompt != null && portraitPrompt.length() > 2000) {
return Result.error("画像描述最多 2000 字符");
}
portraitService.updatePortraitPrompt(userId, portraitPrompt);
return Result.success("保存成功");
}
}
[ ] 步骤 4:编译验证
cd cfc-backend && /bwydata/maven/bin/mvn clean compile -q > /tmp/opencode/t2.log 2>&1; M=$?; tail -3 /tmp/opencode/t2.log; test $M -eq 0 && echo MVN_OK
[ ] 步骤 5:Commit
GIT_MASTER=1 git add \
cfc-backend/src/main/java/com/etotem/cfc/service/PortraitService.java \
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PortraitServiceImpl.java \
cfc-backend/src/main/java/com/etotem/cfc/controller/user/PortraitController.java
GIT_MASTER=1 git commit -m "feat: 新增 PortraitService 和 PortraitController"
文件:
新增:cfc-langgraph/app/portrait_service.py
[ ] 步骤 1:创建 portrait_service.py
"""用户画像 prompt 组装服务
职责:
1. 拉取用户自定义画像文本(600s 缓存)
2. 拉取 profile_snapshot 并渲染为文本(900s 缓存)
3. 拼装成 SystemMessage 内容;无数据时返回 None
"""
import time
import logging
from app.tools.java_client import JavaClient
logger = logging.getLogger(__name__)
_TTL_PROMPT = 600
_TTL_SNAPSHOT = 900
_cache_prompt: dict = {}
_cache_snapshot: dict = {}
def _render_snapshot(profile: dict) -> str:
"""将 profile Map 渲染为中文指标清单文本"""
if not profile or "error" in profile:
return ""
lines = []
member = profile.get("member") or {}
name = member.get("name", "用户")
age = member.get("age", "?")
gender_raw = str(member.get("gender", ""))
gender = "男" if gender_raw == "male" else "女" if gender_raw == "female" else "未知"
lines.append(f"## 画像对象:{name}({age}岁,{gender})")
dims = profile.get("dimension_scores") or {}
if dims:
lines.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"):
lines.append(f"平均睡眠:{body['sleep_dur_avg']}小时/天")
if body.get("exercise_count_week"):
lines.append(f"周运动频次:{body['exercise_count_week']}次")
mind = profile.get("mind_metrics") or {}
if mind.get("stress_avg"):
lines.append(f"平均压力:{mind['stress_avg']}/10")
problems = profile.get("problem_domains") or []
if problems:
lines.append(f"关注问题域:{', '.join(problems[:5])}")
return "\n".join(lines)
async def get_user_portrait_text(java: JavaClient, user_id: int, member_id) -> str | None:
"""返回画像 SystemMessage 内容;无画像数据时返回 None"""
parts = []
# 1. 用户自定义 prompt(600s 缓存)
now = time.time()
cached_text, cached_ts = _cache_prompt.get(user_id, (None, 0))
if now - cached_ts < _TTL_PROMPT and cached_text is not None:
user_text = cached_text
else:
try:
client = await java._get_client()
resp = await client.post("/api/user/portrait/get", json={"userId": user_id}, timeout=5.0)
data = resp.json()
user_text = (data.get("data") or {}).get("portraitPrompt") or ""
_cache_prompt[user_id] = (user_text, now)
except Exception as e:
logger.warning("拉取用户画像 prompt 失败: %s", e)
user_text = ""
if user_text.strip():
parts.append(user_text)
# 2. 快照渲染文本(仅当有 member_id 时)
if member_id:
snap_now = time.time()
cached_snap, cached_snap_ts = _cache_snapshot.get(int(member_id), (None, 0))
if snap_now - cached_snap_ts < _TTL_SNAPSHOT and cached_snap is not None:
snap_text = cached_snap
else:
try:
profile = await java.get_member_profile(int(member_id))
snap_text = _render_snapshot(profile) if profile else ""
_cache_snapshot[int(member_id)] = (snap_text, snap_now)
except Exception as e:
logger.warning("拉取成员画像快照失败: %s", e)
snap_text = ""
if snap_text.strip():
if user_text.strip():
parts.append("\n---参考指标---\n" + snap_text)
else:
parts.append("以下是用户画像数据(系统自动生成),请结合这些数据给出更针对性的建议:\n" + snap_text)
return "\n\n".join(parts) if parts else None
def clear_cache(user_id: int | None = None, member_id: int | None = None):
"""清除缓存"""
if user_id is not None:
_cache_prompt.pop(user_id, None)
if member_id is not None:
_cache_snapshot.pop(member_id, None)
if user_id is None and member_id is None:
_cache_prompt.clear()
_cache_snapshot.clear()
[ ] 步骤 2:py_compile 验证
cd cfc-langgraph && python3 -m py_compile app/portrait_service.py && echo PS_OK
文件:
cfc-langgraph/app/graphs/chat_graph.pycfc-langgraph/app/graphs/health_coach_graph.pycfc-langgraph/app/graphs/health_butler_graph.py每个 graph 的改造模式相同:在 generate_answer 节点中,人格 prompt 之后插入画像段。
在文件顶部追加 import:
from app.portrait_service import get_user_portrait_text
找到 generate_answer 函数中 messages = [SystemMessage(content=...)] 行(人格 prompt 处),在其后追加:
# 画像注入
member_id = state.get("child_id")
portrait_text = await get_user_portrait_text(java_client, state["user_id"], member_id)
if portrait_text:
messages.insert(1, SystemMessage(content=portrait_text))
其中 java_client 需要在图创建时构造(与 RagRetriever/ChatOpenAI 同级)。
与 chat_graph.py 相同模式,在 generate_answer 中人格 prompt 之后插入:
from app.portrait_service import get_user_portrait_text
# ... 在 messages 追加 persona 后 ...
portrait_text = await get_user_portrait_text(java_client, state["user_id"], state.get("child_id"))
if portrait_text:
messages.insert(1, SystemMessage(content=portrait_text))
注意:health_coach_graph.py 已有 from app.prompt_service import get_prompt,追加 from app.portrait_service import get_user_portrait_text。
同上模式。先 grep 确认其 generate_answer 结构:
grep -n "def generate_answer\|SystemMessage\|messages = " cfc-langgraph/app/graphs/health_butler_graph.py | head -15
然后在 SystemMessage 组装处插入画像段。
[ ] 步骤 4:py_compile 验证
cd cfc-langgraph && python3 -m py_compile app/graphs/chat_graph.py app/graphs/health_coach_graph.py app/graphs/health_butler_graph.py && echo GRAPH_OK
[ ] 步骤 5:Commit
GIT_MASTER=1 git add cfc-langgraph/app/portrait_service.py \
cfc-langgraph/app/graphs/chat_graph.py \
cfc-langgraph/app/graphs/health_coach_graph.py \
cfc-langgraph/app/graphs/health_butler_graph.py
GIT_MASTER=1 git commit -m "feat: Python 侧画像 prompt 注入(3 个 graph)"
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java修改:cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java
[ ] 步骤 1:在 AIChatController 中注入 PortraitService
在 AIChatController.java 顶部加:
@Resource
private PortraitService portraitService;
在 inputs = familyContextService.buildContext(...) 之后追加:
// 注入用户画像
Long memberId = null;
String memberIdStr = params.get("memberId");
if (memberIdStr != null && !memberIdStr.trim().isEmpty()) {
memberId = Long.valueOf(memberIdStr);
}
String portraitPrompt = portraitService.buildPortraitPrompt(userId, memberId);
if (portraitPrompt != null) {
inputs.put("portrait_prompt", portraitPrompt);
}
在 inputs 构建后(已有 child_id 和 mascot 注入之后)追加:
// 注入用户画像
String portraitPrompt = portraitService.buildPortraitPrompt(userId, memberId);
if (portraitPrompt != null) {
inputs.put("portrait_prompt", portraitPrompt);
}
同上,在 inputs 构建后追加 portrait_prompt 注入。
在 sendNutritionMessage 方法中,构建 inputs 后追加 portrait_prompt。由于 nutrition 走 Dify(非 LangGraph),portrait_prompt 会被 Dify 作为 inputs 传入(Dify 的 system prompt 中可以引用)。
[ ] 步骤 6:编译验证
cd cfc-backend && /bwydata/maven/bin/mvn clean compile -q > /tmp/opencode/t5.log 2>&1; M=$?; tail -3 /tmp/opencode/t5.log; test $M -eq 0 && echo MVN_OK
[ ] 步骤 7:Commit
GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java \
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java
GIT_MASTER=1 git commit -m "feat: Java 侧 4 个对话端点注入 portrait_prompt"
文件:
cfc-frontend/pages/profile/portrait-edit.vuecfc-frontend/utils/api.js修改:cfc-frontend/pages.json
[ ] 步骤 1:在 api.js 新增画像接口封装
在 cfc-frontend/utils/api.js 末尾追加:
// 用户画像
export function getPortrait(params = {}) {
return request('/api/user/portrait/get', 'POST', params)
}
export function editPortrait(data) {
return request('/api/user/portrait/edit', 'POST', data)
}
[ ] 步骤 2:创建 portrait-edit.vue
<template>
<view class="portrait-edit-page">
<view class="section">
<view class="section-title">我的画像描述</view>
<view class="hint">请输入你的个人画像描述,AI 会根据这些信息提供更有针对性的建议(最多 2000 字)</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>
<script>
import { getPortrait, editPortrait } from '../../utils/api.js'
export default {
data() {
return {
portraitPrompt: '',
renderedSnapshot: '',
saving: 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 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
}
}
}
}
</script>
<style scoped>
.portrait-edit-page {
padding: 24rpx;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 12rpx;
}
.hint {
font-size: 24rpx;
color: #999;
margin-bottom: 16rpx;
}
.portrait-textarea {
width: 100%;
height: 300rpx;
border: 1rpx solid #eee;
border-radius: 8rpx;
padding: 16rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.snapshot-text {
font-size: 26rpx;
color: #555;
white-space: pre-wrap;
line-height: 1.6;
}
.save-btn {
background: #F97316;
color: #fff;
border-radius: 44rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
margin-top: 32rpx;
}
</style>
[ ] 步骤 3:在 pages.json 注册新页面
在 pages.json 的 pages 数组中追加(放在 profile 相关页面附近):
{
"path": "pages/profile/portrait-edit",
"style": {
"navigationBarTitleText": "我的画像"
}
}
在 cfc-frontend/pages/profile/index.vue 中找到合适位置(如吉祥物设置附近),添加「我的画像」入口按钮或菜单项,跳转至 /pages/profile/portrait-edit。
[ ] 步骤 5:语法验证
sed -n '/<script>/,/<\/script>/p' cfc-frontend/pages/profile/portrait-edit.vue | sed '1d;$d' > /tmp/opencode/portrait_check.mjs && node --check /tmp/opencode/portrait_check.mjs && echo VUE_OK
[ ] 步骤 6:Commit
GIT_MASTER=1 git add cfc-frontend/pages/profile/portrait-edit.vue \
cfc-frontend/utils/api.js \
cfc-frontend/pages.json
GIT_MASTER=1 git commit -m "feat: 小程序新增画像编辑页"
[ ] 步骤 1:后端编译
cd cfc-backend && /bwydata/maven/bin/mvn clean compile -q > /tmp/opencode/final_mvn.log 2>&1; M=$?; tail -3 /tmp/opencode/final_mvn.log; test $M -eq 0 && echo MVN_OK
[ ] 步骤 2:Python 编译
cd cfc-langgraph && python3 -m py_compile app/portrait_service.py app/graphs/chat_graph.py app/graphs/health_coach_graph.py app/graphs/health_butler_graph.py && echo PY_OK
[ ] 步骤 3:小程序前端验证
for f in pages/profile/portrait-edit.vue pages/ai/chat.vue pages/membership/index.vue; do
cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' "$f" | sed '1d;$d' > /tmp/opencode/final_check.mjs && node --check /tmp/opencode/final_check.mjs && echo "OK: $f" && cd ..
done
[ ] 步骤 4:功能验证(可选,本地启动后测试)
调用 POST /api/user/portrait/edit 保存画像文本
调用 POST /api/user/portrait/get 读取画像
调用 POST /api/ai/health-coach/send 发起对话,检查 Python 日志确认画像段已注入
[ ] 步骤 5:提交所有剩余改动
cd /sc-data/cfc && GIT_MASTER=1 git add -A && git diff --cached --stat && GIT_MASTER=1 git commit -m "feat: 用户画像 prompt 拼装全栈实现" && git log --oneline -5
规格覆盖度:
占位符扫描:无"待定/TODO"
类型一致性:
PortraitService 接口方法名与 impl 一致get_user_portrait_text(java, user_id, member_id) 签名统一getPortrait/editPortrait 与后端 /api/user/portrait/get/edit 对应边界处理:
待办:运营侧 system_prompts 配置(步骤 7.3 遗留,非本次范围)