Browse Source

chore: auto bump version and changelog [skip ci]

iwt 3 tuần trước cách đây
mục cha
commit
7e50d03496

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-5bcc6cb26d08218a8b2e64c5ae4d17d6a279eec6
+6d2131d1687d2caaf99eb96492caf60b77768b93

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1202",
+  "version": "1.0.1203",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1202",
+      "version": "1.0.1203",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1203",
+  "version": "1.0.1204",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.1204 (2026-08-23)
+
+### 新功能
+- 添加续费页和闯关进度页
+
+
 ## v1.0.1203 (2026-08-23)
 
 ### 文档

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1203
+> 当前版本: v1.0.1204
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.1204 (2026-08-23)
+
+### 新功能
+- 添加续费页和闯关进度页
+
+
 ## v1.0.1203 (2026-08-23)
 
 ### 文档

+ 1325 - 0
docs/superpowers/plans/2026-08-23-coach-butler.md

@@ -0,0 +1,1325 @@
+# AI健康教练人格分化与管家自助选择 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 复用吉祥物管线为健康教练提供浠宝/福宝双人格话术;L2 家庭可自助浏览并绑定/更换真人管家;管理端补齐分配管理与停接开关。
+
+**架构:** 后端在现有 butler 体系上增量扩展(1 列迁移 + 1 新实体 + ButlerService 方法 + 2 个控制器);AI 人格经 `/health-coach/send` 补注入 `coach_id`,经 AiGateway context 透传至 LangGraph 按 prompt_key 三级回退路由;小程序新增选择页三态视图;Web 管理端新增分配管理视图与开关列。
+
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus(Java 8)/ FastAPI + LangGraph(Python)/ uni-app Vue 2 Options API / Vue 2 + Element UI
+
+**规格:** `docs/superpowers/specs/2026-08-23-coach-butler-design.md`(已批准,commit 8cc0a758)
+
+**⚠️ 本仓库验证约定(覆盖技能默认 TDD 流程):**
+- 后端唯一验证方式:`cd cfc-backend && mvn clean compile`(EXIT 0 即通过,无测试基建)
+- 小程序前端验证:提取改动 `.vue` 的 `<script>` 块存临时文件后 `node --check`;**禁止**运行整包 build
+- 所有 git 命令带 `GIT_MASTER=1` 前缀;commit message 用中文 SEMANTIC 风格(feat:/fix:/docs:)
+- 每个任务结束必须 commit(单文件或强耦合文件组一个 commit)
+
+---
+
+## 文件结构总览
+
+| # | 文件 | 动作 | 职责 |
+|---|------|------|------|
+| 1 | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 修改 | 迁移261:butler_profiles.accepting |
+| 2 | `cfc-backend/src/main/resources/schema.sql` | 修改 | 同步 accepting 列定义 |
+| 3 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java` | 修改 | 加 accepting 字段 |
+| 4 | `cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java` | 创建 | butler_assignments 表实体(当前全库无引用,本功能为首写入方) |
+| 5 | `cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java` | 创建 | BaseMapper |
+| 6 | `cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java` | 修改 | 家庭端三方法 + 管理端三方法 |
+| 7 | `cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java` | 创建 | 家庭端 3 端点 |
+| 8 | `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java` | 修改 | 管理端 3 端点 |
+| 9 | `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` | 修改 | /health-coach/send 注入 coach_id |
+| 10 | `cfc-langgraph/app/graphs/health_coach_graph.py` | 修改 | generate_answer prompt 路由 |
+| 11 | `cfc-frontend/utils/api.js` | 修改 | 3 个接口封装 |
+| 12 | `cfc-frontend/pages/butler/select.vue` | 创建 | 管家选择页(三态) |
+| 13 | `cfc-frontend/pages.json` | 修改 | 注册 select 页 |
+| 14 | `cfc-frontend/pages/ai/chat.vue` | 修改 | 欢迎语带教练名 |
+| 15 | `cfc-frontend/pages/membership/index.vue` | 修改 | 我的管家入口卡片 |
+| 16 | `cfc-web/src/api/butler.js` | 修改 | 3 个封装 |
+| 17 | `cfc-web/src/views/admin/ButlerAssignments.vue` | 创建 | 分配管理视图 |
+| 18 | `cfc-web/src/views/admin/review/ButlerAudit.vue` | 修改 | 停接开关列 |
+| 19 | `docs/superpowers/api/API_REFERENCE.md` | 修改 | 登记 6 个新接口 |
+
+---
+
+### 任务 1:DB 迁移 —— butler_profiles.accepting 停接开关列
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(迁移260 块之后,约 :9143 起)
+- 修改:`cfc-backend/src/main/resources/schema.sql`(butler_profiles CREATE TABLE,约 :2646 commission_rate_l2 行后)
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java`(commissionRateL2 字段后,约 :40)
+
+- [ ] **步骤 1.1:DatabaseInitializer 添加迁移 261**
+
+在 `// 迁移260: 创建圈子评论表` 整块结束后、下一个方法或空行处插入:
+
+```java
+		// 迁移261: butler_profiles 添加停接新单开关(管家自助选择功能)
+		ensureColumn("butler_profiles", "accepting",
+				"TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单'");
+```
+
+注意保持与相邻代码相同的缩进风格(该文件使用 Tab)。
+
+- [ ] **步骤 1.2:schema.sql 同步**
+
+在 `CREATE TABLE IF NOT EXISTS butler_profiles (` 块内,`commission_rate_l2 INT DEFAULT 5000,` 行之后插入一行:
+
+```sql
+    accepting TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单',
+```
+
+- [ ] **步骤 1.3:实体加字段**
+
+在 `ButlerProfile.java` 的 `private Integer commissionRateL2;` 之后加:
+
+```java
+    /**
+     * 是否接受新家庭绑定: 0=停接/1=接单
+     */
+    private Integer accepting;
+```
+
+- [ ] **步骤 1.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 1.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
+  cfc-backend/src/main/resources/schema.sql \
+  cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerProfile.java
+GIT_MASTER=1 git commit -m "feat: butler_profiles 新增accepting停接开关列"
+```
+
+---
+
+### 任务 2:新建 ButlerAssignment 实体与 Mapper
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java`
+
+表结构参照(schema.sql 已有,勿改):`id, family_id(UNIQUE uk_family_assignment), butler_user_id, subscription_id, level, assigned_at`
+
+- [ ] **步骤 2.1:确认 Mapper 风格**
+
+运行:`cat cfc-backend/src/main/java/com/etom/cfc/mapper/ButlerProfileMapper.java 2>/dev/null || cat cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerProfileMapper.java`
+观察:是否有 `@Mapper` 注解、是否继承 `BaseMapper<T>`。新 Mapper 与其保持完全一致的风格。
+
+- [ ] **步骤 2.2:创建实体**
+
+`entity/ButlerAssignment.java`:
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 会员-管家分配记录(家庭自助选择/换绑写入)
+ */
+@Data
+@TableName("butler_assignments")
+public class ButlerAssignment {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 家庭 ID(uk_family_assignment 唯一)
+     */
+    private Long familyId;
+
+    /**
+     * 管家用户 ID
+     */
+    private Long butlerUserId;
+
+    /**
+     * 关联订阅 ID
+     */
+    private Long subscriptionId;
+
+    /**
+     * 订阅级别 L1/L2
+     */
+    private String level;
+
+    private Date assignedAt;
+}
+```
+
+若项目实体不使用 Lombok `@Data`(对照 ButlerProfile.java 是否有 getter/setter 手写),则改为手写各字段的 getter/setter,去掉 `@Data`。
+
+- [ ] **步骤 2.3:创建 Mapper**
+
+`mapper/ButlerAssignmentMapper.java`(按步骤 2.1 观察到的风格调整注解):
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+
+public interface ButlerAssignmentMapper extends BaseMapper<ButlerAssignment> {
+}
+```
+
+- [ ] **步骤 2.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerAssignment.java \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerAssignmentMapper.java
+GIT_MASTER=1 git commit -m "feat: 新增butler_assignments实体与Mapper"
+```
+
+---
+
+### 任务 3:ButlerService 家庭端方法(浏览/查询/绑定)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java`
+
+已有依赖:`@Resource UserMapper userMapper; @Resource ButlerProfileMapper butlerProfileMapper; @Resource UserService userService;`
+需要新增依赖:`ButlerAssignmentMapper`、`MemberSubscriptionService`(Bean 名与类名首字母小写一致,符合注入规范)。
+
+- [ ] **步骤 3.1:添加依赖注入与 import**
+
+import 区新增:
+
+```java
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.etotem.cfc.entity.ButlerAssignment;
+import com.etotem.cfc.mapper.ButlerAssignmentMapper;
+import com.etotem.cfc.service.MemberSubscriptionService;
+import com.etotem.cfc.entity.MemberSubscription;
+```
+
+类内字段区新增:
+
+```java
+    @Resource
+    private ButlerAssignmentMapper butlerAssignmentMapper;
+
+    @Resource
+    private MemberSubscriptionService memberSubscriptionService;
+```
+
+- [ ] **步骤 3.2:追加三个家庭端方法**
+
+在类末尾(`assignMemberToButler(Long familyId, ...)` 方法 `}` 之后、类结束 `}` 之前)追加:
+
+```java
+    // ==================== 家庭端自助选择 ====================
+
+    /**
+     * 可选管家列表(浏览开放,不校验会员等级)
+     * 条件:approved + tier=L2 + accepting=1 + 有余量;返回脱敏档案
+     */
+    public Result<List<Map<String, Object>>> getAvailableButlers() {
+        LambdaQueryWrapper<ButlerProfile> query = new LambdaQueryWrapper<>();
+        query.eq(ButlerProfile::getStatus, "approved")
+             .eq(ButlerProfile::getTier, "L2")
+             .eq(ButlerProfile::getAccepting, 1)
+             .apply("member_count < max_members")
+             .orderByAsc(ButlerProfile::getCreatedAt);
+        SortUtil.applySort(query);
+        List<ButlerProfile> profiles = butlerProfileMapper.selectList(query);
+
+        List<Long> userIds = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            userIds.add(p.getUserId());
+        }
+        Map<Long, User> userMap = new HashMap<>();
+        if (!userIds.isEmpty()) {
+            for (User u : userMapper.selectBatchIds(userIds)) {
+                userMap.put(u.getId(), u);
+            }
+        }
+
+        List<Map<String, Object>> result = new java.util.ArrayList<>();
+        for (ButlerProfile p : profiles) {
+            User u = userMap.get(p.getUserId());
+            Map<String, Object> item = new HashMap<>();
+            item.put("butlerUserId", p.getUserId());
+            item.put("nickname", u != null ? u.getNickname() : "管家");
+            item.put("avatar", u != null ? u.getAvatar() : "");
+            item.put("description", p.getDescription());
+            item.put("certificates", p.getCertificates());
+            item.put("remainingSlots", (p.getMaxMembers() == null ? 0 : p.getMaxMembers())
+                    - (p.getMemberCount() == null ? 0 : p.getMemberCount()));
+            result.add(item);
+        }
+        return Result.success(result);
+    }
+
+    /**
+     * 当前家庭的管家绑定信息(无绑定时 data 为 null)
+     */
+    public Result<Map<String, Object>> getMyButler(Long userId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        ButlerAssignment assignment = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (assignment == null) {
+            return Result.success(null);
+        }
+        return Result.success(buildButlerSummary(assignment));
+    }
+
+    /**
+     * 自助绑定/更换管家:L2 校验 + 条件扣容 + INSERT/UPDATE 双路径
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Map<String, Object>> selectButler(Long userId, Long butlerUserId) {
+        Long familyId = requireFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到家庭信息");
+        }
+        // ① L2 有效订阅校验
+        MemberSubscription sub = memberSubscriptionService.getActiveSubscription(familyId);
+        boolean active = sub != null && sub.getExpireTime() != null
+                && sub.getExpireTime().after(new Date());
+        if (!active || !"L2".equals(sub.getLevel())) {
+            return Result.error("该功能面向久久一生(L2)会员家庭");
+        }
+        // ② 目标管家校验
+        ButlerProfile butler = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, butlerUserId)
+                        .eq(ButlerProfile::getStatus, "approved")
+                        .eq(ButlerProfile::getTier, "L2")
+                        .eq(ButlerProfile::getAccepting, 1)
+                        .last("LIMIT 1"));
+        if (butler == null) {
+            return Result.error("该管家不可选择");
+        }
+        // ③ 条件原子扣容(防并发超卖)
+        LambdaUpdateWrapper<ButlerProfile> incr = new LambdaUpdateWrapper<>();
+        incr.eq(ButlerProfile::getId, butler.getId())
+            .apply("member_count < max_members")
+            .setSql("member_count = member_count + 1");
+        int affected = butlerProfileMapper.update(null, incr);
+        if (affected == 0) {
+            return Result.error("该管家名额已满");
+        }
+        // ④ assignments INSERT/UPDATE 双路径(激活自动分配只加计数不建行)
+        ButlerAssignment existing = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        if (existing != null) {
+            // 旧管家计数回退
+            LambdaUpdateWrapper<ButlerProfile> decr = new LambdaUpdateWrapper<>();
+            decr.eq(ButlerProfile::getUserId, existing.getButlerUserId())
+                .setSql("member_count = GREATEST(member_count - 1, 0)");
+            butlerProfileMapper.update(null, decr);
+
+            existing.setButlerUserId(butlerUserId);
+            existing.setLevel(sub.getLevel());
+            existing.setAssignedAt(new Date());
+            butlerAssignmentMapper.updateById(existing);
+        } else {
+            ButlerAssignment na = new ButlerAssignment();
+            na.setFamilyId(familyId);
+            na.setButlerUserId(butlerUserId);
+            na.setSubscriptionId(sub.getId());
+            na.setLevel(sub.getLevel());
+            na.setAssignedAt(new Date());
+            butlerAssignmentMapper.insert(na);
+        }
+        log.info("Family {} selected butler {}, level={}", familyId, butlerUserId, sub.getLevel());
+
+        ButlerAssignment saved = butlerAssignmentMapper.selectOne(
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .eq(ButlerAssignment::getFamilyId, familyId)
+                        .last("LIMIT 1"));
+        return Result.success(buildButlerSummary(saved));
+    }
+
+    /**
+     * userId -> familyId(与 MembershipController 同源:users.family_id)
+     */
+    private Long requireFamilyId(Long userId) {
+        User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
+    /**
+     * 组装绑定摘要(脱敏:不含联系方式)
+     */
+    private Map<String, Object> buildButlerSummary(ButlerAssignment assignment) {
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("butlerUserId", assignment.getButlerUserId());
+        summary.put("level", assignment.getLevel());
+        summary.put("assignedAt", assignment.getAssignedAt());
+        User bu = userMapper.selectById(assignment.getButlerUserId());
+        summary.put("nickname", bu != null ? bu.getNickname() : "管家");
+        summary.put("avatar", bu != null ? bu.getAvatar() : "");
+        ButlerProfile bp = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, assignment.getButlerUserId())
+                        .last("LIMIT 1"));
+        summary.put("description", bp != null ? bp.getDescription() : "");
+        return summary;
+    }
+```
+
+- [ ] **步骤 3.3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。若报 `getAccepting()` 不存在,检查任务 1 的实体字段;若报 `MemberSubscription` import 找不到,确认包路径 `com.etotem.cfc.entity.MemberSubscription`。
+
+- [ ] **步骤 3.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
+GIT_MASTER=1 git commit -m "feat: ButlerService新增管家浏览/查询/自助绑定方法"
+```
+
+---
+
+### 任务 4:FamilyButlerSelectController 家庭端接口
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java`
+
+Bean 名 `familyButlerSelectController` 与既有 `butlerController` 无冲突。禁止 Controller 直连 Mapper——全部走 ButlerService(任务 3 已封装)。
+
+- [ ] **步骤 4.1:创建控制器**
+
+```java
+package com.etotem.cfc.controller.butler;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ButlerService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭端管家自助选择(仅 L2 绑定受限,浏览开放)
+ */
+@RestController
+@RequestMapping("/api/butler")
+public class FamilyButlerSelectController {
+
+    @Resource
+    private ButlerService butlerService;
+
+    @ApiOperation("可选管家列表(浏览开放)")
+    @PostMapping("/available-list")
+    public Result<List<Map<String, Object>>> availableList() {
+        return butlerService.getAvailableButlers();
+    }
+
+    @ApiOperation("我家的管家绑定信息")
+    @PostMapping("/my-butler")
+    public Result<Map<String, Object>> myButler(@RequestAttribute("userId") Long userId) {
+        return butlerService.getMyButler(userId);
+    }
+
+    @ApiOperation("绑定/更换管家(仅L2)")
+    @PostMapping("/select")
+    public Result<Map<String, Object>> select(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Object butlerUserIdObj = params.get("butlerUserId");
+        if (butlerUserIdObj == null || butlerUserIdObj.toString().trim().isEmpty()) {
+            return Result.error("缺少 butlerUserId 参数");
+        }
+        Long butlerUserId = Long.valueOf(butlerUserIdObj.toString());
+        return butlerService.selectButler(userId, butlerUserId);
+    }
+}
+```
+
+注意:`Result` 的实际包路径以 `ButlerService.java` 现有 import 为准(`com.etotem.cfc.common.Result`)。若 ButlerService 中三方法返回的 `Result` 泛型与此不一致,以 ButlerService 定义为准同步控制器签名。
+
+- [ ] **步骤 4.2:路由冲突检查**
+
+运行:`grep -rn '"/api/butler' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u`
+预期:新增的 `/available-list`、`/my-butler`、`/select` 不与任何既有路径重复。
+
+- [ ] **步骤 4.3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/controller/butler/FamilyButlerSelectController.java
+GIT_MASTER=1 git commit -m "feat: 家庭端管家自助选择接口"
+```
+
+---
+
+### 任务 5:管理端接口(分配列表/强制解绑/停接开关)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java`
+
+- [ ] **步骤 5.1:确认管理员鉴权模式**
+
+运行:`sed -n '1,60p' cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java`
+记录该控制器如何校验管理员(通常 `@RequestAttribute("role")` + `"admin".equals(role)` 判断)。后续端点完全复制该守卫模式。
+
+- [ ] **步骤 5.2:ButlerService 追加管理端方法**
+
+在任务 3 追加代码之后继续追加(import 额外需要 `com.baomidou.mybatisplus.extension.plugins.pagination.Page`,该文件头部已 import):
+
+```java
+    // ==================== 管理端分配管理 ====================
+
+    /**
+     * 分配关系分页列表
+     */
+    public Result<Map<String, Object>> listAssignments(Integer pageNum, Integer pageSize) {
+        int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
+        Page<ButlerAssignment> page = butlerAssignmentMapper.selectPage(new Page<>(pn, ps),
+                new LambdaQueryWrapper<ButlerAssignment>()
+                        .orderByDesc(ButlerAssignment::getAssignedAt));
+
+        List<Map<String, Object>> rows = new java.util.ArrayList<>();
+        for (ButlerAssignment a : page.getRecords()) {
+            Map<String, Object> row = new HashMap<>();
+            row.put("id", a.getId());
+            row.put("familyId", a.getFamilyId());
+            row.put("butlerUserId", a.getButlerUserId());
+            row.put("level", a.getLevel());
+            row.put("assignedAt", a.getAssignedAt());
+            User bu = userMapper.selectById(a.getButlerUserId());
+            row.put("butlerNickname", bu != null ? bu.getNickname() : "");
+            rows.add(row);
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("total", page.getTotal());
+        data.put("records", rows);
+        return Result.success(data);
+    }
+
+    /**
+     * 强制解绑(物理删除分配行 + 回退管家计数)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Void> unassign(Long assignmentId) {
+        ButlerAssignment assignment = butlerAssignmentMapper.selectById(assignmentId);
+        if (assignment == null) {
+            return Result.error("分配记录不存在");
+        }
+        butlerAssignmentMapper.deleteById(assignmentId);
+
+        LambdaUpdateWrapper<ButlerProfile> decr = new LambdaUpdateWrapper<>();
+        decr.eq(ButlerProfile::getUserId, assignment.getButlerUserId())
+            .setSql("member_count = GREATEST(member_count - 1, 0)");
+        butlerProfileMapper.update(null, decr);
+
+        log.info("Admin unassigned assignmentId={}, family={}, butler={}",
+                assignmentId, assignment.getFamilyId(), assignment.getButlerUserId());
+        return Result.success(null);
+    }
+
+    /**
+     * 管家停接/恢复接单开关(不影响存量绑定)
+     */
+    public Result<Void> toggleAccepting(Long butlerUserId, Integer accepting) {
+        if (accepting == null || (accepting != 0 && accepting != 1)) {
+            return Result.error("accepting 取值只能为 0 或 1");
+        }
+        ButlerProfile profile = butlerProfileMapper.selectOne(
+                new LambdaQueryWrapper<ButlerProfile>()
+                        .eq(ButlerProfile::getUserId, butlerUserId)
+                        .last("LIMIT 1"));
+        if (profile == null) {
+            return Result.error("管家不存在");
+        }
+        profile.setAccepting(accepting);
+        profile.setUpdatedAt(new Date());
+        butlerProfileMapper.updateById(profile);
+        return Result.success(null);
+    }
+```
+
+- [ ] **步骤 5.3:AdminButlerController 追加端点**
+
+在该控制器最后一个方法之后追加(守卫代码从步骤 5.1 观察到的现有方法中逐字复制;下例假设标准模式):
+
+```java
+    @Operation(summary = "分配关系分页列表")
+    @PostMapping("/assignments")
+    public Result<Map<String, Object>> assignments(@RequestBody Map<String, Object> params) {
+        // ← 此处粘贴步骤 5.1 观察到的管理员守卫代码(与现有方法一致)
+        Integer pageNum = params.get("pageNum") != null ? Integer.valueOf(params.get("pageNum").toString()) : 1;
+        Integer pageSize = params.get("pageSize") != null ? Integer.valueOf(params.get("pageSize").toString()) : 10;
+        return butlerService.listAssignments(pageNum, pageSize);
+    }
+
+    @Operation(summary = "强制解绑")
+    @PostMapping("/unassign")
+    public Result<Void> unassign(@RequestBody Map<String, Object> params) {
+        // ← 同上粘贴守卫代码
+        if (params.get("assignmentId") == null) {
+            return Result.error("缺少 assignmentId");
+        }
+        return butlerService.unassign(Long.valueOf(params.get("assignmentId").toString()));
+    }
+
+    @Operation(summary = "管家停接开关")
+    @PostMapping("/toggle-accepting")
+    public Result<Void> toggleAccepting(@RequestBody Map<String, Object> params) {
+        // ← 同上粘贴守卫代码
+        if (params.get("butlerUserId") == null || params.get("accepting") == null) {
+            return Result.error("缺少参数");
+        }
+        return butlerService.toggleAccepting(
+                Long.valueOf(params.get("butlerUserId").toString()),
+                Integer.valueOf(params.get("accepting").toString()));
+    }
+```
+
+同时确认控制器中已存在 `@Resource private ButlerService butlerService;`(AdminButlerController 现有 assign-member 功能应已注入;若无则补上,字段名必须为 `butlerService`)。`@Operation` 注解包路径以该文件现有用法为准。
+
+- [ ] **步骤 5.4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java \
+  cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
+GIT_MASTER=1 git commit -m "feat: 管理端分配管理与停接开关接口"
+```
+
+---
+
+### 任务 6:/health-coach/send 注入教练人格
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`(sendHealthCoachMessage 方法内,约 :420-447)
+
+现状:该方法只放 `child_id`,缺 mascot 注入(/chat/send :76-109 已有完整范式)。
+
+- [ ] **步骤 6.1:定位并插入注入代码**
+
+找到 `sendHealthCoachMessage` 内这一段:
+
+```java
+        Map<String, Object> inputs = new LinkedHashMap<>();
+        if (memberId != null) {
+            inputs.put("child_id", memberId);
+        }
+        Map<String, Object> resp = aiGateway.chat(query, userId, conversationId, inputs);
+```
+
+在 `inputs.put("child_id", memberId);` 的 `}` 之后、`Map<String, Object> resp = ...` 之前插入:
+
+```java
+        // 注入教练人格(对齐 /chat/send 的 mascot 范式;coach_id 用于 LangGraph prompt 路由)
+        com.etotem.cfc.entity.User hcUser = userService.getUserInfo(userId);
+        String hcMascotCode = hcUser != null ? hcUser.getMascot() : null;
+        if (hcMascotCode != null && !hcMascotCode.isEmpty()) {
+            com.etotem.cfc.enums.MascotEnum hcMascot = com.etotem.cfc.enums.MascotEnum.fromCode(hcMascotCode);
+            if (hcMascot != null) {
+                inputs.put("coach_id", hcMascot.getCode());
+                inputs.put("mascot_name", hcMascot.name());
+                inputs.put("mascot_gender", hcMascot.gender());
+                inputs.put("mascot_persona", hcMascot.persona());
+            }
+        } else {
+            inputs.put("coach_id", "");
+            inputs.put("mascot_name", "健康教练");
+            inputs.put("mascot_gender", "");
+            inputs.put("mascot_persona", "");
+        }
+```
+
+说明:变量名用 `hcUser/hcMascot/hcMascotCode` 防止与本方法其他局部变量冲突;`mascot.name()` 保持与 /chat/send 既有行为一致(不修无关行为);AiGateway 会将 String 类型键值放入 body.context 透传(AiGateway.java:162-168)。
+
+- [ ] **步骤 6.2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS(若 `userService` 未注入报错,检查类头 `@Resource private UserService userService;` 是否存在——/chat/send 已在使用,必然存在)
+
+- [ ] **步骤 6.3:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
+GIT_MASTER=1 git commit -m "feat: 健康教练对话注入教练人格标识"
+```
+
+---
+
+### 任务 7:LangGraph 人格 prompt 路由
+
+**文件:**
+- 修改:`cfc-langgraph/app/graphs/health_coach_graph.py`(generate_answer 函数开头,约 :87-90)
+
+- [ ] **步骤 7.1:改写 generate_answer 开头**
+
+将现有:
+
+```python
+    async def generate_answer(state: HealthCoachState) -> dict:
+        """带健康知识检索的 LLM 生成"""
+        messages = [SystemMessage(content=await get_prompt("health_coach") or DEFAULT_COACH_PROMPT)]
+```
+
+替换为:
+
+```python
+    async def generate_answer(state: HealthCoachState) -> dict:
+        """带健康知识检索的 LLM 生成"""
+        # 人格路由:context.coach_id (xibao/fubao) -> 专属prompt,三级回退保证可用性
+        _ctx = state.get("context") or {}
+        _coach_id = _ctx.get("coach_id")
+        if _coach_id not in ("xibao", "fubao"):
+            _coach_id = None
+        _persona_prompt = await get_prompt(f"health_coach_{_coach_id}") if _coach_id else None
+        _base_prompt = await get_prompt("health_coach")
+        messages = [SystemMessage(content=_persona_prompt or _base_prompt or DEFAULT_COACH_PROMPT)]
+```
+
+函数其余部分(ctx 家庭上下文、检索、save_memory 等)一律不动。
+
+- [ ] **步骤 7.2:语法验证**
+
+运行:`cd cfc-langgraph && python3 -m py_compile app/graphs/health_coach_graph.py && echo OK`
+预期:输出 `OK`
+
+- [ ] **步骤 7.3:运营配置两段提示词(人工/管理端操作,非代码)**
+
+登录 Web 管理端「系统提示词管理」(SystemPromptManagement 页),新增两条记录:
+
+| promptKey | 内容要点 |
+|-----------|----------|
+| `health_coach_xibao` | 浠宝·温暖知心风:倾听共情先行、温和提问引导、鼓励性语言、建议委婉分层给出;保留默认 prompt 的就医红线与中文要求 |
+| `health_coach_fubao` | 福宝·活泼行动派:语气轻快直接、优先给可执行行动清单、打卡式督促口吻、建议具体到频次;保留就医红线与中文要求 |
+
+两段均以现有 `health_coach` 默认提示词(DEFAULT_COACH_PROMPT 同款结构:核心原则/职责/回答结构/原则四段)为骨架改写语气即可。未配置时三级回退自动生效,不阻塞上线。
+
+- [ ] **步骤 7.4:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-langgraph/app/graphs/health_coach_graph.py
+GIT_MASTER=1 git commit -m "feat: 健康教练graph按coach_id路由人格prompt"
+```
+
+---
+
+### 任务 8:小程序 API 封装 + 管家选择页 + 路由注册
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`(文件尾部追加)
+- 创建:`cfc-frontend/pages/butler/select.vue`
+- 修改:`cfc-frontend/pages.json`(butler 子包,约 :628-638)
+
+- [ ] **步骤 8.1:api.js 追加封装**
+
+在 `utils/api.js` 末尾追加:
+
+```javascript
+// ── 管家自助选择 ──
+export function getAvailableButlers() {
+  return request('/api/butler/available-list', 'POST', {})
+}
+export function getMyButler() {
+  return request('/api/butler/my-butler', 'POST', {})
+}
+export function selectButler(butlerUserId) {
+  return request('/api/butler/select', 'POST', { butlerUserId })
+}
+```
+
+- [ ] **步骤 8.2:pages.json 注册**
+
+将 butler 子包改为:
+
+```json
+    {
+      "root": "pages/butler",
+      "pages": [
+        {
+          "path": "apply",
+          "style": {
+            "navigationBarTitleText": "管家入驻"
+          }
+        },
+        {
+          "path": "select",
+          "style": {
+            "navigationBarTitleText": "选择管家"
+          }
+        }
+      ]
+    },
+```
+
+- [ ] **步骤 8.3:创建 select.vue(完整页面)**
+
+`pages/butler/select.vue`(Vue2 Options API;无可选链;flexbox;`:key` 用方法;时间用字符串截取格式化):
+
+```vue
+<template>
+  <view class="select-page">
+    <!-- 顶部状态区 -->
+    <view class="status-card" v-if="pageState === 'bound'">
+      <view class="current-label">我的管家</view>
+      <view class="current-row">
+        <image class="current-avatar" :src="myButler.avatar || '/static/logo.png'" mode="aspectFill" />
+        <view class="current-info">
+          <text class="current-name">{{ myButler.nickname }}</text>
+          <text class="current-date">{{ formatDate(myButler.assignedAt) }} 绑定</text>
+        </view>
+      </view>
+      <view class="current-desc">{{ myButler.description }}</view>
+      <button class="btn-change" @click="scrollToList">更换管家</button>
+    </view>
+    <view class="upgrade-banner" v-if="pageState === 'locked'" @click="goUpgrade">
+      <text class="upgrade-text">久久一生(L2)会员专享,升级后可选择专属管家</text>
+      <text class="upgrade-arrow">›</text>
+    </view>
+
+    <!-- 管家列表 -->
+    <view class="list-section" id="butlerList">
+      <view class="section-title">可选管家</view>
+      <view class="butler-card" v-for="(item, idx) in butlers" :key="getItemKey(idx)"
+        :class="{ active: myButler && myButler.butlerUserId === item.butlerUserId }">
+        <image class="card-avatar" :src="item.avatar || '/static/logo.png'" mode="aspectFill" />
+        <view class="card-body">
+          <view class="card-top">
+            <text class="card-name">{{ item.nickname }}</text>
+            <text class="card-slots">剩余名额 {{ item.remainingSlots }}</text>
+          </view>
+          <text class="card-desc">{{ item.description || '专业家庭健康管家' }}</text>
+        </view>
+        <button class="btn-select"
+          :disabled="pageState === 'locked' || item.remainingSlots <= 0"
+          @click="onSelect(item)">
+          {{ myButler && myButler.butlerUserId === item.butlerUserId ? '当前' : '选择' }}
+        </button>
+      </view>
+      <view class="empty-tip" v-if="butlers.length === 0 && !loading">暂无可选管家</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getAvailableButlers, getMyButler, selectButler } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      pageState: 'browse', // browse=可浏览未绑定 / bound=已绑定 / locked=非L2
+      lockReason: '',
+      myButler: null,
+      butlers: [],
+      keySeq: 0
+    }
+  },
+  onLoad() {
+    this.initPage()
+  },
+  onShow() {
+    if (this.myButler || this.pageState === 'bound') {
+      this.refreshMyButler()
+    }
+  },
+  methods: {
+    getItemKey(idx) {
+      var item = this.butlers[idx]
+      return item && item.butlerUserId ? 'b_' + item.butlerUserId : 'idx_' + idx
+    },
+    formatDate(iso) {
+      if (!iso) return ''
+      return iso.substring(0, 19).replace('T', ' ').substring(0, 16)
+    },
+    initPage() {
+      var that = this
+      that.loading = true
+      Promise.all([getAvailableButlers(), getMyButler()])
+        .then(function (resArr) {
+          var listRes = resArr[0]
+          var myRes = resArr[1]
+          if (listRes && listRes.code === 200) {
+            that.butlers = listRes.data || []
+          }
+          if (myRes && myRes.code === 200 && myRes.data) {
+            that.myButler = myRes.data
+            that.pageState = 'bound'
+          }
+        })
+        .catch(function () {
+          uni.showToast({ title: '加载失败', icon: 'none' })
+        })
+        .then(function () { that.loading = false })
+    },
+    refreshMyButler() {
+      var that = this
+      getMyButler().then(function (res) {
+        if (res && res.code === 200) {
+          that.myButler = res.data
+          that.pageState = res.data ? 'bound' : 'browse'
+        }
+      })
+    },
+    markLocked(msg) {
+      this.pageState = 'locked'
+      this.lockReason = msg || ''
+    },
+    onSelect(item) {
+      var that = this
+      if (that.myButler && that.myButler.butlerUserId === item.butlerUserId) return
+      uni.showModal({
+        title: '确认选择',
+        content: '确定选择「' + item.nickname + '」作为您的专属管家?',
+        success: function (mr) {
+          if (!mr.confirm) return
+          selectButler(item.butlerUserId).then(function (res) {
+            if (res && res.code === 200) {
+              uni.showToast({ title: '绑定成功', icon: 'success' })
+              that.initPage()
+            } else if (res && res.message && res.message.indexOf('久久一生') !== -1) {
+              that.markLocked(res.message)
+              uni.showModal({
+                title: '会员权益',
+                content: res.message,
+                showCancel: false,
+                success: function () { that.goUpgrade() }
+              })
+            } else {
+              uni.showToast({ title: (res && res.message) || '绑定失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+    scrollToList() {
+      uni.pageScrollTo({ selector: '#butlerList', duration: 300 })
+    },
+    goUpgrade() {
+      uni.navigateTo({ url: '/pages/membership/upgrade' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.select-page { min-height: 100vh; background: #f5fafе; padding: 24rpx; box-sizing: border-box; display: flex; flex-direction: column; }
+.status-card { background: #fff; border-radius: 20rpx; padding: 32rpx; margin-bottom: 24rpx; }
+.current-label { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
+.current-row { display: flex; align-items: center; }
+.current-avatar { width: 96rpx; height: 96rpx; border-radius: 50%; margin-right: 20rpx; }
+.current-info { display: flex; flex-direction: column; }
+.current-name { font-size: 34rpx; font-weight: bold; color: #333; }
+.current-date { font-size: 22rpx; color: #999; margin-top: 6rpx; }
+.current-desc { font-size: 26rpx; color: #666; margin-top: 20rpx; line-height: 1.6; }
+.btn-change { margin-top: 24rpx; background: #4a9bd7; color: #fff; font-size: 28rpx; border-radius: 40rpx; }
+.upgrade-banner { display: flex; justify-content: space-between; align-items: center; background: #fff7ed; border: 1rpx solid #ffb56b; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; }
+.upgrade-text { font-size: 26rpx; color: #d4770a; flex: 1; }
+.upgrade-arrow { font-size: 36rpx; color: #d4770a; margin-left: 12rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; color: #333; margin: 8rpx 0 20rpx; }
+.butler-card { display: flex; align-items: center; background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx; }
+.butler-card.active { border: 2rpx solid #4a9bd7; }
+.card-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; margin-right: 20rpx; flex-shrink: 0; }
+.card-body { flex: 1; min-width: 0; }
+.card-top { display: flex; align-items: center; justify-content: space-between; }
+.card-name { font-size: 30rpx; font-weight: bold; color: #333; }
+.card-slots { font-size: 22rpx; color: #10b981; }
+.card-desc { display: block; font-size: 24rpx; color: #888; margin-top: 8rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.btn-select { width: 120rpx; height: 60rpx; line-height: 60rpx; padding: 0; font-size: 26rpx; background: #4a9bd7; color: #fff; border-radius: 30rpx; flex-shrink: 0; margin-left: 16rpx; }
+.btn-select[disabled] { background: #ccc; color: #fff; }
+.empty-tip { text-align: center; color: #999; font-size: 26rpx; padding: 60rpx 0; }
+</style>
+```
+
+⚠️ 注意上面样式第一行的 `background: #f5fafе;` —— 其中 `е` 必须是半角英文字母 e(此处若粘贴出现异常字符请手动改正为 `#f5fafe`)。
+
+- [ ] **步骤 8.4:语法验证**
+
+运行:
+```bash
+cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' pages/butler/select.vue | sed '1d;$d' > /tmp/opencode/select_check.js && node --check /tmp/opencode/select_check.js && echo SYNTAX_OK
+```
+预期:`SYNTAX_OK`
+
+- [ ] **步骤 8.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-frontend/utils/api.js cfc-frontend/pages/butler/select.vue cfc-frontend/pages.json
+GIT_MASTER=1 git commit -m "feat: 小程序管家选择页与接口封装"
+```
+
+---
+
+### 任务 9:chat.vue 欢迎语 + 会员中心入口卡片
+
+**文件:**
+- 修改:`cfc-frontend/pages/ai/chat.vue`(welcome-title,:41)
+- 修改:`cfc-frontend/pages/membership/index.vue`(入口卡片 + 方法)
+
+- [ ] **步骤 9.1:chat.vue 欢迎语带教练名**
+
+将 :41 行整行:
+
+```html
+          <text class="welcome-title">{{ isNutrition ? '你好!我是营养助手' : (isHealthCoach ? '你好!我是健康教练' : (isButler ? '你好!我是健康管家' : ('你好!我是' + (mascotName || '家庭助手')))) }}</text>
+```
+
+替换为:
+
+```html
+          <text class="welcome-title">{{ isNutrition ? '你好!我是营养助手' : (isHealthCoach ? ('你好!我是' + (mascotName || '健康教练')) : (isButler ? '你好!我是健康管家' : ('你好!我是' + (mascotName || '家庭助手')))) }}</text>
+```
+
+- [ ] **步骤 9.2:membership/index.vue 定位插入点**
+
+运行:`grep -n "class=\"\(benefits\|cards\|banner\|section\)\|<view class=\"benefit\|权益" cfc-frontend/pages/membership/index.vue | head`
+选择一个主内容区容器(如权益卡片区之前),在其后插入下方卡片。若该页有现成"特权入口"横向卡片区,跟随其结构放置。
+
+- [ ] **步骤 9.3:插入「我的管家」卡片(模板)**
+
+```html
+    <!-- 我的管家入口 -->
+    <view class="butler-entry" @click="goButlerSelect">
+      <view class="butler-entry-icon">🤵</view>
+      <view class="butler-entry-main">
+        <text class="butler-entry-title">{{ butlerEntryTitle }}</text>
+        <text class="butler-entry-sub">{{ butlerEntrySub }}</text>
+      </view>
+      <text class="butler-entry-arrow">›</text>
+    </view>
+```
+
+- [ ] **步骤 9.4:membership/index.vue 脚本接线**
+
+data 中追加:
+
+```javascript
+      myButlerInfo: null,
+      isL2Active: false,
+```
+
+methods 中追加(并确认该页已引入 `getMyButler`——若无则在顶部 api 引入行加入 `getMyButler`):
+
+```javascript
+    goButlerSelect() {
+      uni.navigateTo({ url: '/pages/butler/select' })
+    },
+    loadButlerEntry() {
+      var that = this
+      getMyButler().then(function (res) {
+        if (res && res.code === 200 && res.data) {
+          that.myButlerInfo = res.data
+        }
+      }).catch(function () {})
+    },
+```
+
+computed 中追加:
+
+```javascript
+    butlerEntryTitle() {
+      return this.myButlerInfo ? ('我的管家 · ' + this.myButlerInfo.nickname) : '我的管家'
+    },
+    butlerEntrySub() {
+      return this.myButlerInfo ? '点击查看或更换' : '久久一生会员可自助选择专属管家'
+    },
+```
+
+onLoad/onShow 中调用一次 `this.loadButlerEntry()`。
+
+样式追加:
+
+```css
+.butler-entry { display: flex; align-items: center; background: #fff; border-radius: 16rpx; padding: 28rpx; margin: 20rpx 0; }
+.butler-entry-icon { font-size: 48rpx; margin-right: 20rpx; }
+.butler-entry-main { flex: 1; display: flex; flex-direction: column; }
+.butler-entry-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.butler-entry-sub { font-size: 24rpx; color: #999; margin-top: 6rpx; }
+.butler-entry-arrow { font-size: 40rpx; color: #ccc; }
+```
+
+- [ ] **步骤 9.5:语法验证**
+
+运行:
+```bash
+cd cfc-frontend && sed -n '/<script>/,/<\/script>/p' pages/ai/chat.vue | sed '1d;$d' > /tmp/opencode/chat_check.js && node --check /tmp/opencode/chat_check.js && sed -n '/<script>/,/<\/script>/p' pages/membership/index.vue | sed '1d;$d' > /tmp/opencode/memb_check.js && node --check /tmp/opencode/memb_check.js && echo BOTH_OK
+```
+预期:`BOTH_OK`
+
+- [ ] **步骤 9.6:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-frontend/pages/ai/chat.vue cfc-frontend/pages/membership/index.vue
+GIT_MASTER=1 git commit -m "feat: 健康教练欢迎语个性化与我的管家入口"
+```
+
+---
+
+### 任务 10:Web 管理端(封装 + 分配管理视图 + 开关列 + 路由权限)
+
+**文件:**
+- 修改:`cfc-web/src/api/butler.js`(尾部追加)
+- 创建:`cfc-web/src/views/admin/ButlerAssignments.vue`
+- 修改:`cfc-web/src/views/admin/review/ButlerAudit.vue`(开关列)
+- 修改:`cfc-web/src/router/index.js` 及权限配置(定位后照抄邻居条目)
+
+- [ ] **步骤 10.1:api/butler.js 追加封装**
+
+```javascript
+// 分配关系列表
+export function getButlerAssignments(params) {
+  return request({
+    url: '/api/admin/butler/assignments',
+    method: 'post',
+    data: params
+  })
+}
+
+// 强制解绑
+export function unassignButler(data) {
+  return request({
+    url: '/api/admin/butler/unassign',
+    method: 'post',
+    data: data
+  })
+}
+
+// 停接开关
+export function toggleButlerAccepting(data) {
+  return request({
+    url: '/api/admin/butler/toggle-accepting',
+    method: 'post',
+    data: data
+  })
+}
+```
+
+- [ ] **步骤 10.2:创建 ButlerAssignments.vue(完整视图)**
+
+`views/admin/ButlerAssignments.vue`:
+
+```vue
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <div slot="header">
+        <span>管家分配管理</span>
+      </div>
+      <el-table v-loading="loading" :data="records" border stripe>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="familyId" label="家庭ID" width="100" />
+        <el-table-column prop="butlerNickname" label="管家" min-width="140" />
+        <el-table-column prop="butlerUserId" label="管家用户ID" width="110" />
+        <el-table-column prop="level" label="订阅级别" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.level === 'L2' ? 'warning' : 'info'" size="small">{{ row.level }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="绑定时间" width="170">
+          <template slot-scope="{ row }">{{ formatTime(row.assignedAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button size="mini" type="danger" @click="onUnassign(row)">强制解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        style="margin-top: 16px; text-align: right;"
+        layout="total, prev, pager, next"
+        :total="total"
+        :page-size="pageSize"
+        :current-page.sync="pageNum"
+        @current-change="loadData"
+      />
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getButlerAssignments, unassignButler } from '@/api/butler'
+
+export default {
+  name: 'ButlerAssignments',
+  data() {
+    return {
+      loading: false,
+      records: [],
+      total: 0,
+      pageNum: 1,
+      pageSize: 10
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    formatTime(iso) {
+      if (!iso) return '-'
+      return iso.substring(0, 19).replace('T', ' ')
+    },
+    loadData() {
+      this.loading = true
+      getButlerAssignments({ pageNum: this.pageNum, pageSize: this.pageSize })
+        .then(res => {
+          const data = res.data || {}
+          this.records = data.records || []
+          this.total = data.total || 0
+        })
+        .finally(() => { this.loading = false })
+    },
+    onUnassign(row) {
+      this.$confirm(
+        `确定解除家庭 ${row.familyId} 与管家「${row.butlerNickname}」的绑定?`,
+        '强制解绑',
+        { type: 'warning' }
+      ).then(() => unassignButler({ assignmentId: row.id }))
+        .then(res => {
+          if (res.code === 200) {
+            this.$message.success('已解绑')
+            this.loadData()
+          } else {
+            this.$message.error(res.message || '操作失败')
+          }
+        }).catch(() => {})
+    }
+  }
+}
+</script>
+```
+
+响应包装取值方式(res.data / res.code)先对照 `api/butler.js` 现有消费方(如 ButlerAudit.vue 中 `getButlerList(...).then(...)` 的取值层级)并保持一致,若全局拦截器已剥壳则同步调整。
+
+- [ ] **步骤 10.3:ButlerAudit.vue 加停接开关列**
+
+运行:`grep -n "el-table-column" cfc-web/src/views/admin/review/ButlerAudit.vue | tail -5`
+找到表格最后一列的位置,其后插入(prop 名以该表格数据字段为准,若列表项含 `userId` 则如下):
+
+```html
+        <el-table-column label="接单状态" width="110">
+          <template slot-scope="{ row }">
+            <el-switch
+              :value="row.accepting !== 0"
+              active-color="#13ce66"
+              @change="onToggleAccepting(row)" />
+            <div v-if="row.accepting === 0" style="font-size: 12px; color: #f56c6c;">已停接</div>
+          </template>
+        </el-table-column>
+```
+
+methods 中追加(并顶部 import `toggleButlerAccepting`):
+
+```javascript
+    async onToggleAccepting(row) {
+      const next = row.accepting === 0 ? 1 : 0
+      try {
+        const res = await toggleButlerAccepting({ butlerUserId: row.userId, accepting: next })
+        if (res.code === 200) {
+          this.$set(row, 'accepting', next)
+          this.$message.success(next === 1 ? '已恢复接单' : '已停接新单')
+        } else {
+          this.$message.error(res.message || '操作失败')
+        }
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+```
+
+若管家列表接口未返回 `accepting` 字段,需回查后端 list 接口的组装逻辑把 `profile.getAccepting()` 放进行数据(在 AdminButlerController/相关 Service 中搜索现有 list 的组装点,加一行 `row.put("accepting", p.getAccepting())` 同级代码),并在本计划内完成该最小改动后再进入下一任务。
+
+- [ ] **步骤 10.4:路由与权限注册(定位式)**
+
+运行:`grep -rn "ButlerAudit" cfc-web/src/router/ cfc-web/src/permissions.js 2>/dev/null ; grep -rn "path: '/admin" cfc-web/src/router/index.js | head -8`
+- 若找到 ButlerAudit 的注册行:紧随其后照抄一条 ButlerAssignments 路由(component 改为 `() => import('@/views/admin/ButlerAssignments.vue')`,name/path/meta 结构逐字段镜像邻居条目)。
+- 若 router 中搜不到但页面可访问(动态注册机制):在 `src/router/index.js` 里找任一 admin 视图条目作为模板,同样镜像新增。
+- 若存在独立权限清单文件(步骤命令会暴露路径):为新路由追加同名权限码。
+
+- [ ] **步骤 10.5:Commit**
+
+```bash
+GIT_MASTER=1 git add cfc-web/src/api/butler.js cfc-web/src/views/admin/ButlerAssignments.vue \
+  cfc-web/src/views/admin/review/ButlerAudit.vue cfc-web/src/router/index.js
+GIT_MASTER=1 git commit -m "feat: 管理端分配管理与停接开关页面"
+```
+
+---
+
+### 任务 11:API 文档登记 + 全量验收
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 11.1:登记 6 个新接口**
+
+运行:`grep -n "api/butler" docs/superpowers/api/API_REFERENCE.md | head -3` 定位管家章节,按该文档既有表格格式追加:
+
+| 接口 | 说明 | 权限 |
+|------|------|------|
+| `POST /api/butler/available-list` | 可选管家列表(浏览开放,脱敏) | 登录 |
+| `POST /api/butler/my-butler` | 我家当前管家绑定信息 | 登录 |
+| `POST /api/butler/select` | 绑定/更换管家(仅L2有效订阅) | 登录 |
+| `POST /api/admin/butler/assignments` | 分配关系分页列表 | 管理员 |
+| `POST /api/admin/butler/unassign` | 强制解绑 | 管理员 |
+| `POST /api/admin/butler/toggle-accepting` | 管家停接新单开关 | 管理员 |
+
+另在 AI 章节给 `/api/ai/health-coach/send` 的请求体补充一行说明:新增可选字段 `coach_id` 由服务端自动注入(客户端无需传)。
+
+- [ ] **步骤 11.2:最终验证清单(对照规格 §十一 验收标准)**
+
+```bash
+cd cfc-backend && mvn clean compile          # EXIT 0
+cd ../cfc-langgraph && python3 -m py_compile app/graphs/health_coach_graph.py && echo LG_OK
+for f in pages/butler/select.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.js && node --check /tmp/opencode/final_check.js && echo "OK: $f" && cd ..
+done
+```
+预期:BUILD SUCCESS / LG_OK / 三行 OK。
+
+- [ ] **步骤 11.3:Commit**
+
+```bash
+GIT_MASTER=1 git add docs/superpowers/api/API_REFERENCE.md
+GIT_MASTER=1 git commit -m "docs: 登记管家自选与健康教练接口文档"
+```
+
+---
+
+## 自检记录(编写者已完成)
+
+1. **规格覆盖度**:§1 数据模型→任务1;§2 家庭端接口→任务3/4;§2 AI端→任务6;§2 管理端→任务5;§3 LangGraph→任务7;§4 前端→任务8/9;§5 Web→任务10;§6 边界(条件扣容/GREATEST回退/双路径/三级回退/停接双拦截/物理删除)→任务3/5/7 对应实现;§十一 验收→任务11.2。无遗漏。
+2. **占位符扫描**:全文无"待定/TODO/类似任务N";两处"定位式步骤"(5.1/10.4)均为给出确切命令+两种已知形态的完整代码,属可执行指令而非缺口。
+3. **类型一致性**:`getAvailableButlers/getMyButler/selectButler/listAssignments/unassign/toggleAccepting` 在服务与控制器两侧签名一致;`accepting` 字段名贯穿迁移/schema/实体/接口/前端;`butlerUserId` 前后端统一驼峰;`coach_id` Java inputs 键与 Python `ctx.get("coach_id")` 一致。