# 数字分身接管聊天功能 — 实现计划 > **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:** 实现数字分身接管主人在会会平台上的聊天,支持立即接管和延迟接管两种模式,优先使用知识库生成回复。 **Architecture:** 在数字分身应用后端新增 takeover 模块,包含消息监听器、接管决策器、回复执行器。通过轮询会会平台推送消息接口获取未读消息,调用现有分身聊天接口生成回复,通过盒子 IM 接口以主人身份发送。 **Tech Stack:** FastAPI (Python), SQLite, Redis, HTTPX, APScheduler, Vue 3 (frontend) ## 全局约束 - 数据库:SQLite (avatar.db),使用 `_try_add_columns` 模式添加新列 - 响应格式:统一使用 `ok()` / `fail()` from `responses.py` - 认证:`Authorization: Bearer ` header - 盒子 IM 接口前缀:`/api/im/box/*`(通过会会平台网关) - 签名机制:复用 `news_service.py` 中的 `_build_form` / `_make_sign` 模式 - YAGNI:本期只做单聊接管,群聊接管标记为后续扩展 - TDD:每个功能模块先写测试再实现 --- ## 文件清单 ### 新建文件 | 文件 | 职责 | |------|------| | `digital-avatar-app/backend/routers/takeover.py` | 接管配置 API + 内部处理接口 | | `digital-avatar-app/backend/services/takeover_service.py` | 接管核心逻辑(监听、决策、回复) | | `digital-avatar-app/backend/services/boxim_client.py` | 盒子 IM 客户端(获取凭证、发送消息) | | `digital-avatar-app/backend/requirements.txt` | 新增 redis、apscheduler 依赖 | | `digital-avatar-app/src/api/index.ts` | 新增 takeover 相关 API 函数 | | `digital-avatar-app/src/views/AuthorizationManage.vue` | 添加接管开关 UI | ### 修改文件 | 文件 | 修改内容 | |------|---------| | `digital-avatar-app/backend/models.py` | Authorization 表新增 takeover 字段 | | `digital-avatar-app/backend/database.py` | `_try_add_columns` 添加 takeover 列 | | `digital-avatar-app/backend/main.py` | 注册 takeover router,启动定时任务 | | `digital-avatar-app/backend/requirements.txt` | 添加 redis, apscheduler | --- ## 任务分解 ### Task 1: 扩展 Authorization 数据模型 **Files:** - Modify: `digital-avatar-app/backend/models.py` - Modify: `digital-avatar-app/backend/database.py` - Test: `digital-avatar-app/backend/tests/test_takeover_model.py` **Interfaces:** - 新增字段:`takeover_enabled`, `takeover_mode`, `takeover_delay_seconds` - `Authorization.to_dict()` 返回新增字段 - [ ] **Step 1: Write the failing test** ```python # digital-avatar-app/backend/tests/test_takeover_model.py from database import SessionLocal from models import Authorization def test_authorization_takeover_fields(): db = SessionLocal() try: auth = db.query(Authorization).first() assert auth is not None # 检查新字段存在且有默认值 assert hasattr(auth, 'takeover_enabled') assert hasattr(auth, 'takeover_mode') assert hasattr(auth, 'takeover_delay_seconds') assert auth.takeover_enabled == False assert auth.takeover_mode == 'immediate' assert auth.takeover_delay_seconds == 30 finally: db.close() def test_authorization_to_dict_includes_takeover(): db = SessionLocal() try: auth = db.query(Authorization).first() d = auth.to_dict() assert 'takeover_enabled' in d assert 'takeover_mode' in d assert 'takeover_delay_seconds' in d finally: db.close() ``` - [ ] **Step 2: Run test to verify it fails** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_model.py -v ``` Expected: FAIL with "no such column: takeover_enabled" - [ ] **Step 3: Update Authorization model** ```python # digital-avatar-app/backend/models.py # 在 Authorization 类中新增字段(在 created_at 之前) class Authorization(Base): # ... 现有字段 ... takeover_enabled = Column(Boolean, default=False) # 是否开启分身接管 takeover_mode = Column(String, default="immediate") # immediate | delayed takeover_delay_seconds = Column(Integer, default=30) # 延迟秒数 created_at = Column(DateTime, server_default=func.now()) def to_dict(self): return { # ... 现有字段 ... "takeoverEnabled": self.takeover_enabled, "takeoverMode": self.takeover_mode, "takeoverDelaySeconds": self.takeover_delay_seconds, "createdAt": _iso(self.created_at), } ``` - [ ] **Step 4: Add column migration** ```python # digital-avatar-app/backend/database.py # 在 _try_add_columns 调用中新增: _try_add_columns( # ... 现有列 ... ("authorizations", "takeover_enabled", "BOOLEAN DEFAULT 0"), ("authorizations", "takeover_mode", "VARCHAR DEFAULT 'immediate'"), ("authorizations", "takeover_delay_seconds", "INTEGER DEFAULT 30"), ) ``` - [ ] **Step 5: Run test to verify it passes** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_model.py -v ``` Expected: PASS - [ ] **Step 6: Commit** ```bash cd /Users/yqq/Works/AiProject/huihuiSquare git add digital-avatar-app/backend/models.py digital-avatar-app/backend/database.py digital-avatar-app/backend/tests/test_takeover_model.py git commit -m "feat: add takeover fields to Authorization model" ``` --- ### Task 2: 盒子 IM 客户端 **Files:** - Create: `digital-avatar-app/backend/services/boxim_client.py` - Test: `digital-avatar-app/backend/tests/test_boxim_client.py` **Interfaces:** - `BoxIMClient.get_credentials(user_id: str) -> dict` — 获取网易云信凭证 - `BoxIMClient.send_p2p_message(from_accid: str, to_accid: str, content: str) -> bool` — 发送单聊消息 **Dependencies:** - 会会平台签名机制(复用 news_service 中的模式) - HTTPX for async HTTP calls - 平台配置从环境变量读取 - [ ] **Step 1: Write the failing test** ```python # digital-avatar-app/backend/tests/test_boxim_client.py import pytest from unittest.mock import AsyncMock, patch from services.boxim_client import BoxIMClient @pytest.fixture def mock_config(): return { "HUIHUI_IM_BASE_URL": "http://192.168.1.200:60040", "HUIHUI_APP_ID": "test_app", "HUIHUI_ACCESS_ID": "test_access", "HUIHUI_ACCESS_SECRET": "test_secret", } @patch("services.boxim_client.httpx.AsyncClient") @pytest.mark.asyncio async def test_get_credentials(mock_client_class, mock_config): mock_response = AsyncMock() mock_response.json.return_value = {"code": 200, "data": {"accid": "user123", "token": "tok_xyz"}} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response client = BoxIMClient(mock_config) result = await client.get_credentials("user123") assert result["accid"] == "user123" assert result["token"] == "tok_xyz" @patch("services.boxim_client.httpx.AsyncClient") @pytest.mark.asyncio async def test_send_p2p_message_success(mock_client_class, mock_config): mock_response = AsyncMock() mock_response.json.return_value = {"code": 200} mock_client_class.return_value.__aenter__.return_value.post.return_value = mock_response client = BoxIMClient(mock_config) result = await client.send_p2p_message("owner_acc", "target_acc", "Hello") assert result is True ``` - [ ] **Step 2: Run test to verify it fails** ```bash cd digital-avatar-app/backend python -m pytest tests/test_boxim_client.py -v ``` Expected: FAIL with "ModuleNotFoundError: No module named 'services.boxim_client'" - [ ] **Step 3: Write minimal implementation** ```python # digital-avatar-app/backend/services/boxim_client.py """盒子 IM 客户端 — 封装网易云信 IM 接口调用""" import hashlib import random import string from datetime import datetime from typing import Optional import httpx class BoxIMClient: """盒子 IM 客户端,通过会会平台网关调用网易云信 IM""" def __init__(self, config: dict): self.base_url = config.get("HUIHUI_IM_BASE_URL", "http://192.168.1.200:60040") self.app_id = config.get("HUIHUI_APP_ID", "") self.access_id = config.get("HUIHUI_ACCESS_ID", "") self.access_secret = config.get("HUIHUI_ACCESS_SECRET", "") def _build_sign_params(self, extra: dict) -> dict: """构建带签名的请求参数""" nonce = "".join(random.choices(string.ascii_lowercase + string.digits, k=12)) timestamp = datetime.now().strftime("%Y%m%d%H%M%S") # 24小时制 params = { "appId": self.app_id, "accessId": self.access_id, "nonce": nonce, "timestamp": timestamp, **extra, } # 计算签名 keys = sorted(params.keys()) sign_parts = [] for k in keys: v = params.get(k) if v and v != "" and v != []: sign_parts.append(f"{k}={v}") sign_str = "&".join(sign_parts) + f"&accessSecret={self.access_secret}" signature = hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper() params["signature"] = signature params["signType"] = "MD5" params["signVersion"] = "1.0" return params async def get_credentials(self, user_id: str) -> Optional[dict]: """获取用户的网易云信 IM 凭证 (accid, token)""" params = self._build_sign_params({"userId": user_id}) async with httpx.AsyncClient(timeout=10) as client: r = await client.post( f"{self.base_url}/box/netease", params=params, ) data = r.json() if data.get("code") in (0, 200): return data.get("data", {}) return None async def send_p2p_message( self, from_accid: str, to_accid: str, content: str ) -> bool: """发送单聊消息(文本)""" params = self._build_sign_params({ "from": from_accid, "to": to_accid, "msgType": "text", "content": content, }) async with httpx.AsyncClient(timeout=10) as client: r = await client.post( f"{self.base_url}/box/message/send/p2p", params=params, ) data = r.json() return data.get("code") in (0, 200) ``` - [ ] **Step 4: Add test dependencies** ```bash cd digital-avatar-app/backend pip install pytest pytest-asyncio httpx ``` - [ ] **Step 5: Run test to verify it passes** ```bash cd digital-avatar-app/backend python -m pytest tests/test_boxim_client.py -v ``` Expected: PASS - [ ] **Step 6: Commit** ```bash git add digital-avatar-app/backend/services/boxim_client.py digital-avatar-app/backend/tests/test_boxim_client.py git commit -m "feat: add Box IM client for Netease Yunxin integration" ``` --- ### Task 3: 接管服务核心逻辑 **Files:** - Create: `digital-avatar-app/backend/services/takeover_service.py` - Test: `digital-avatar-app/backend/tests/test_takeover_service.py` **Interfaces:** - `TakeoverService.check_takeover_enabled(owner_huihui_id: str, from_user_id: str) -> Authorization | None` - `TakeoverService.generate_reply(avatar_id: str, message: str) -> str` - `TakeoverService.execute_takeover(auth: Authorization, message: dict) -> bool` - `TakeoverService.process_delayed_queue()` — 处理延迟队列中到期的消息 **Dependencies:** - `BoxIMClient` from Task 2 - Existing chat API: `POST /api/avatar/{id}/chat` - Redis for delayed queue - `Authorization` model from Task 1 - [ ] **Step 1: Write the failing test** ```python # digital-avatar-app/backend/tests/test_takeover_service.py import pytest from unittest.mock import AsyncMock, patch, MagicMock from services.takeover_service import TakeoverService @pytest.fixture def mock_db(): db = MagicMock() return db @pytest.fixture def mock_boxim(): client = AsyncMock() client.get_credentials.return_value = {"accid": "owner_acc", "token": "tok"} client.send_p2p_message.return_value = True return client @pytest.fixture def mock_auth(): auth = MagicMock() auth.takeover_enabled = True auth.takeover_mode = "immediate" auth.takeover_delay_seconds = 30 auth.avatar_id = "avatar_123" auth.target_id = "target_user_123" return auth def test_check_takeover_enabled_returns_auth_when_enabled(mock_db, mock_auth): mock_db.query.return_value.filter.return_value.filter.return_value.first.return_value = mock_auth service = TakeoverService(mock_db, None) result = service.check_takeover_enabled("owner_123", "target_123") assert result == mock_auth def test_check_takeover_enabled_returns_none_when_disabled(mock_db): disabled_auth = MagicMock() disabled_auth.takeover_enabled = False mock_db.query.return_value.filter.return_value.filter.return_value.first.return_value = disabled_auth service = TakeoverService(mock_db, None) result = service.check_takeover_enabled("owner_123", "target_123") assert result is None ``` - [ ] **Step 2: Run test to verify it fails** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_service.py -v ``` Expected: FAIL - [ ] **Step 3: Write minimal implementation** ```python # digital-avatar-app/backend/services/takeover_service.py """分身接管聊天服务 — 消息监听、决策、回复执行""" import json import logging from typing import Optional from sqlalchemy.orm import Session import httpx from models import Authorization, Avatar from services.boxim_client import BoxIMClient logger = logging.getLogger(__name__) class TakeoverService: """分身接管服务""" def __init__(self, db: Session, boxim_client: BoxIMClient, redis_client=None): self.db = db self.boxim = boxim_client self.redis = redis_client self._chat_api_base = "http://localhost:8000/api" # 分身应用自己的 API def check_takeover_enabled( self, owner_huihui_id: str, from_user_id: str ) -> Optional[Authorization]: """检查是否有接管授权""" auth = ( self.db.query(Authorization) .filter(Authorization.target_id == from_user_id) .filter(Authorization.takeover_enabled == True) .first() ) if auth and auth.takeover_enabled: return auth return None async def generate_reply(self, avatar_id: str, message: str) -> str: """调用分身聊天接口生成回复""" try: async with httpx.AsyncClient(timeout=30) as client: r = await client.post( f"{self._chat_api_base}/avatar/{avatar_id}/chat", json={"message": message, "history": []}, ) data = r.json() if data.get("code") in (0, 200): return data.get("data", {}).get("answer", "") logger.warning(f"分身聊天接口返回异常: {data}") return "" except Exception as e: logger.error(f"调用分身聊天接口失败: {e}") return "" async def execute_takeover(self, auth: Authorization, message: dict) -> bool: """执行接管:生成回复并以主人身份发送""" try: # 1. 获取主人的 IM 凭证 owner_huihui_id = auth.owner_id if hasattr(auth, 'owner_id') else "" credentials = await self.boxim.get_credentials(owner_huihui_id) if not credentials: logger.warning(f"无法获取主人 IM 凭证: {owner_huihui_id}") return False # 2. 生成回复 reply = await self.generate_reply(auth.avatar_id, message.get("content", "")) if not reply: logger.warning("分身未生成回复") return False # 3. 发送消息 success = await self.boxim.send_p2p_message( from_accid=credentials["accid"], to_accid=message.get("from_accid", ""), content=reply, ) if success: logger.info(f"分身接管回复成功: {reply[:50]}...") return success except Exception as e: logger.error(f"执行接管失败: {e}") return False def enqueue_delayed_message(self, auth: Authorization, message: dict): """将消息写入 Redis 延迟队列""" if not self.redis: logger.warning("Redis 未配置,降级为立即接管") return key = f"takeover:delayed:{auth.target_id}:{message.get('msg_id', '')}" value = json.dumps({ "avatar_id": auth.avatar_id, "from_accid": message.get("from_accid", ""), "content": message.get("content", ""), "owner_huihui_id": auth.owner_id if hasattr(auth, 'owner_id') else "", }) self.redis.setex(key, auth.takeover_delay_seconds + 10, value) logger.info(f"消息写入延迟队列: {key}") async def process_delayed_queue(self): """处理延迟队列中到期的消息""" if not self.redis: return # Redis 会自动过期,这里不需要主动处理 # 实际使用时可以用 Redis keyspace notifications 或定期扫描 pass ``` - [ ] **Step 4: Run test to verify it passes** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_service.py -v ``` Expected: PASS - [ ] **Step 5: Commit** ```bash git add digital-avatar-app/backend/services/takeover_service.py digital-avatar-app/backend/tests/test_takeover_service.py git commit -m "feat: add takeover service core logic" ``` --- ### Task 4: 接管配置 API **Files:** - Create: `digital-avatar-app/backend/routers/takeover.py` - Modify: `digital-avatar-app/backend/main.py` **Interfaces:** - `PUT /api/avatar/{avatar_id}/authorizations/takeover` — 更新接管配置 - 请求体:`{ authorization_id, takeover_enabled, takeover_mode, takeover_delay_seconds }` **Dependencies:** - `Authorization` model from Task 1 - `ok()` / `fail()` from `responses.py` - [ ] **Step 1: Write the failing test** ```python # digital-avatar-app/backend/tests/test_takeover_api.py from fastapi.testclient import TestClient from main import app from database import SessionLocal, Base, engine from models import Authorization, Avatar def setup_test_db(): Base.metadata.create_all(bind=engine) db = SessionLocal() avatar = Avatar(name="test", status="active", config={}) db.add(avatar) db.commit() auth = Authorization(avatar_id=avatar.id, target_id="user1", target_name="测试用户") db.add(auth) db.commit() return db, auth.id def test_update_takeover_config(): db, auth_id = setup_test_db() client = TestClient(app) response = client.put( f"/api/avatar/test_auth/avatar_id/authorizations/takeover", json={ "authorization_id": auth_id, "takeover_enabled": True, "takeover_mode": "delayed", "takeover_delay_seconds": 60, }, ) assert response.status_code == 200 data = response.json() assert data["code"] == 200 # 验证数据库已更新 auth = db.query(Authorization).filter(Authorization.id == auth_id).first() assert auth.takeover_enabled == True assert auth.takeover_mode == "delayed" assert auth.takeover_delay_seconds == 60 ``` - [ ] **Step 2: Run test to verify it fails** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_api.py -v ``` Expected: FAIL with 404 (endpoint not found) - [ ] **Step 3: Write minimal implementation** ```python # digital-avatar-app/backend/routers/takeover.py """分身接管配置 API""" from fastapi import APIRouter, Depends, Body from sqlalchemy.orm import Session from database import get_db from models import Authorization from responses import ok, fail router = APIRouter(tags=["分身接管"]) @router.put("/avatar/{avatar_id}/authorizations/takeover") def update_takeover_config( avatar_id: str, payload: dict = Body(...), db: Session = Depends(get_db), ): """更新分身接管配置""" auth_id = payload.get("authorization_id") if not auth_id: return fail("缺少 authorization_id", 400) auth = db.query(Authorization).filter(Authorization.id == auth_id).first() if not auth: return fail("授权不存在", 404) # 更新接管配置 if "takeover_enabled" in payload: auth.takeover_enabled = payload["takeover_enabled"] if "takeover_mode" in payload: mode = payload["takeover_mode"] if mode not in ("immediate", "delayed"): return fail("takeover_mode 必须是 immediate 或 delayed", 400) auth.takeover_mode = mode if "takeover_delay_seconds" in payload: delay = payload["takeover_delay_seconds"] if not isinstance(delay, int) or delay < 5: return fail("takeover_delay_seconds 必须 >= 5", 400) auth.takeover_delay_seconds = delay db.commit() return ok(auth.to_dict()) ``` - [ ] **Step 4: Register router in main.py** ```python # digital-avatar-app/backend/main.py # 在 import 部分添加: import routers.takeover # 在 router 注册部分添加: app.include_router(routers.takeover.router, prefix="/api") ``` - [ ] **Step 5: Run test to verify it passes** ```bash cd digital-avatar-app/backend python -m pytest tests/test_takeover_api.py -v ``` Expected: PASS - [ ] **Step 6: Commit** ```bash git add digital-avatar-app/backend/routers/takeover.py digital-avatar-app/backend/main.py digital-avatar-app/backend/tests/test_takeover_api.py git commit -m "feat: add takeover config API endpoint" ``` --- ### Task 5: 定时消息监听任务 **Files:** - Modify: `digital-avatar-app/backend/main.py` - Modify: `digital-avatar-app/backend/requirements.txt` **Interfaces:** - APScheduler 定时任务:每 10 秒轮询一次未读消息 - 调用 `TakeoverService.check_takeover_enabled` 判断是否接管 - 根据模式调用立即执行或延迟队列 **Dependencies:** - `TakeoverService` from Task 3 - `BoxIMClient` from Task 2 - `APScheduler` for scheduling - `redis` for delayed queue - [ ] **Step 1: Add dependencies** ``` # digital-avatar-app/backend/requirements.txt # 新增: redis>=5.0 apscheduler>=3.10 ``` - [ ] **Step 2: Add scheduler setup in main.py** ```python # digital-avatar-app/backend/main.py # 在 import 部分添加: from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger import redis as redis_lib import os # 在 on_startup 函数中添加: @app.on_event("startup") def on_startup(): init_db() seed() # 初始化 Redis(可选) redis_client = None redis_url = os.getenv("REDIS_URL", "") if redis_url: try: redis_client = redis_lib.from_url(redis_url) redis_client.ping() except Exception: logger.warning("Redis 连接失败,延迟接管将降级为立即接管") # 初始化 Box IM 客户端 boxim_config = { "HUIHUI_IM_BASE_URL": os.getenv("HUIHUI_IM_BASE_URL", "http://192.168.1.200:60040"), "HUIHUI_APP_ID": os.getenv("HUIHUI_APP_ID", ""), "HUIHUI_ACCESS_ID": os.getenv("HUIHUI_ACCESS_ID", ""), "HUIHUI_ACCESS_SECRET": os.getenv("HUIHUI_ACCESS_SECRET", ""), } boxim_client = BoxIMClient(boxim_config) # 初始化接管服务 from services.takeover_service import TakeoverService takeover_service = TakeoverService(SessionLocal(), boxim_client, redis_client) # 启动定时任务 scheduler = BackgroundScheduler() scheduler.add_job( takeover_service.poll_and_process_messages, trigger=IntervalTrigger(seconds=10), id="takeover_message_poll", ) scheduler.start() ``` - [ ] **Step 3: Add poll method to TakeoverService** ```python # digital-avatar-app/backend/services/takeover_service.py # 在 TakeoverService 类中新增: async def poll_and_process_messages(self): """定时轮询未读消息并处理""" # 这里简化处理,实际需要调用会会平台的消息推送接口 # 伪代码: # messages = await self.fetch_unread_messages() # for msg in messages: # await self.process_message(msg) pass async def process_message(self, message: dict): """处理单条消息""" owner_id = message.get("owner_huihui_id", "") from_id = message.get("from_accid", "") auth = self.check_takeover_enabled(owner_id, from_id) if not auth: return if auth.takeover_mode == "immediate": await self.execute_takeover(auth, message) else: self.enqueue_delayed_message(auth, message) ``` - [ ] **Step 4: Commit** ```bash git add digital-avatar-app/backend/main.py digital-avatar-app/backend/requirements.txt digital-avatar-app/backend/services/takeover_service.py git commit -m "feat: add scheduled message polling for takeover" ``` --- ### Task 6: 前端授权管理 UI 增强 **Files:** - Modify: `digital-avatar-app/src/views/AuthorizationManage.vue` - Modify: `digital-avatar-app/src/api/index.ts` **Interfaces:** - 授权卡片新增接管开关和配置 - 开关开启时显示模式选择和延迟秒数输入 - 调用 `PUT /api/avatar/{id}/authorizations/takeover` 保存配置 **Dependencies:** - Existing authorization API functions - Takeover config API from Task 4 - [ ] **Step 1: Add API function** ```typescript // digital-avatar-app/src/api/index.ts // 新增: export const updateTakeoverConfig = (avatarId: string, data: { authorizationId: string takeoverEnabled: boolean takeoverMode?: string takeoverDelaySeconds?: number }) => { return api.put(`/api/avatar/${avatarId}/authorizations/takeover`, data) } ``` - [ ] **Step 2: Update AuthorizationManage.vue** ```vue
分身接管聊天
``` - [ ] **Step 3: Add methods to AuthorizationManage.vue** ```vue ``` - [ ] **Step 4: Add styles** ```vue ``` - [ ] **Step 5: Commit** ```bash git add digital-avatar-app/src/views/AuthorizationManage.vue digital-avatar-app/src/api/index.ts git commit -m "feat: add takeover config UI to authorization management" ``` --- ### Task 7: 集成测试和部署 **Files:** - Modify: `docker-compose.yml` (digital-avatar-app) - Create: `digital-avatar-app/.env.example` **Interfaces:** - 添加 Redis 容器(可选,用于延迟队列) - 添加环境变量配置 - [ ] **Step 1: Add Redis to docker-compose** ```yaml # digital-avatar-app/docker-compose.yml services: avatar-backend: # ... 现有配置 ... environment: - REDIS_URL=redis://avatar-redis:6379 - HUIHUI_IM_BASE_URL=http://192.168.1.200:60040 - HUIHUI_APP_ID=${HUIHUI_APP_ID} - HUIHUI_ACCESS_ID=${HUIHUI_ACCESS_ID} - HUIHUI_ACCESS_SECRET=${HUIHUI_ACCESS_SECRET} depends_on: - avatar-redis avatar-redis: image: redis:7-alpine container_name: avatar-redis restart: unless-stopped volumes: - redis_data:/data networks: - avatar-net ``` - [ ] **Step 2: Create .env.example** ``` # digital-avatar-app/.env.example HUIHUI_IM_BASE_URL=http://192.168.1.200:60040 HUIHUI_APP_ID=your_app_id HUIHUI_ACCESS_ID=your_access_id HUIHUI_ACCESS_SECRET=your_access_secret REDIS_URL=redis://avatar-redis:6379 ``` - [ ] **Step 3: Manual testing checklist** ```markdown ## 测试清单 ### 功能测试 - [ ] 授权管理页面显示接管开关 - [ ] 开关开启后显示模式选择 - [ ] 延迟模式显示延迟秒数输入 - [ ] 保存配置后刷新页面配置保留 - [ ] 立即接管:模拟消息到达后分身立即回复 - [ ] 延迟接管:N 秒内主人回复则不分身接管 - [ ] 延迟接管:超时后分身自动回复 ### 边界测试 - [ ] 主人有多个分身时使用第一个 active 分身 - [ ] 知识库为空时使用 Qwen 兜底 - [ ] IM 接口异常时记录日志不崩溃 - [ ] Redis 不可用时降级为立即接管 ### 性能测试 - [ ] 轮询间隔 10 秒时 CPU 占用正常 - [ ] 延迟队列消息过期自动清理 ``` - [ ] **Step 4: Commit** ```bash git add digital-avatar-app/docker-compose.yml digital-avatar-app/.env.example git commit -m "feat: add Redis for takeover delayed queue and env config" ``` --- ## 执行顺序 1. Task 1 → Task 2 → Task 3 → Task 4 → Task 5 → Task 6 → Task 7 2. 每个 Task 完成后运行测试验证 3. Task 5 和 Task 6 可以并行(后端定时任务和前端 UI 无依赖) ## 风险与缓解 | 风险 | 缓解措施 | |------|---------| | 会会平台 IM 接口文档不完整 | 先做接口探索,用 Postman 手动验证 | | 盒子 IM 替换云信的 API 差异 | 保留云信客户端作为 fallback | | Redis 延迟队列复杂性 | 第一期可简化为内存队列,Redis 作为优化 | | 分身聊天接口响应慢 | 设置超时,失败时不阻塞轮询 |