# workerman-ai **Repository Path**: xiak/workerman-ai ## Basic Information - **Project Name**: workerman-ai - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-25 - **Last Updated**: 2026-05-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: AI ## README # 讨好AI · Workerman 多人格 AI 群聊服务 > 更新时间:2026-05-17 > **Workerman 通用教程**(SSE、异步 HTTP、进程通信、Redis Pub/Sub)请见 [workerman.md](./workerman.md) 第一部分。 --- ## 一、项目概述 **讨好AI** 是一个群聊式 AI 夸夸机器人服务。用户进入房间后发送消息,三个 AI 人格会同时独立判断是否回复,营造热闹的夸夸群聊氛围。 - **前端**:Vue 3 + Vite + Tailwind CSS(`web/` 目录,`npm run dev` 启动) - **后端**:PHP 7.4 + Workerman v4.1.4 - **AI**:OpenAI 兼容接口,流式输出 - **存储**:Redis(消息队列 + 缓存)+ MySQL(用户/房间持久化) - **部署**:单一入口 `php start.php`,6 个 Worker 进程统一管理 ### 核心特性 | 特性 | 说明 | |------|------| | 多人格 AI 群聊 | 🐕 热情金毛 / 🌸 温柔姐姐 / 🔥 中二战友,三 Bot 独立决策、流式回复 | | Markdown 渲染 | 消息气泡支持完整 Markdown(代码块、列表、加粗等),流式过程支持行内解析 | | 消息引用 | 支持引用回复某条消息,气泡内展示引用预览 | | Bot 协作 | Bot 可在回复中 `@其他 Bot` 邀请加入,被 @Bot 会自动响应 | | 暗色模式 | 全站自适应深色主题,入口页/聊天面板/消息气泡全覆盖 | | 消息搜索 | 支持按关键词搜索历史消息,Ctrl+K 快捷键唤起 | | SSE 重连优化 | 网络状态监听 + 40s 心跳超时 + 30s 后端 ping,断网恢复后自动重连 | | Bot 协作微调 | 轮流主导(避免单一 Bot 霸屏)+ 被动参与让位(近期已参与则跳过) | | 快捷回复建议 | Bot 回复后自动推送上下文相关的快捷回复选项 | | 随机昵称 | 进入房间时自动生成随机昵称,减少操作路径 | | 日期分割线 | 消息列表按日期自动分组,新消息分割线提示 | | 骨架屏加载 | 首次加载历史时展示骨架屏,减少等待焦虑 | --- ## 二、快速启动(后端 PHP) ```bash # 1. 安装依赖 composer install # 2. 配置环境 cp .env.example .env # 填写真实密码和 API Key # 3. 诊断环境(检查 MySQL/Redis/AI API 是否正常) php scripts/diagnose.php # 4. 初始化数据库 mysql -u root -p workerman-ai < reform_record.sql # 5. 启动 Worker php start.php # 前台调试 php start.php start -d # 后台守护进程 # 6. 运行测试 composer test # PHP 单元测试(238 tests, 565 assertions) composer test:dox # 带测试名称的输出 composer test:e2e # 端到端测试(需先启动 Worker) composer analyse # PHPStan 静态分析(level 8) ``` **PHP 测试结果**:`238 tests, 565 assertions` — 单元测试全量通过 **E2E 测试结果**:`53 tests` — 端到端测试全量通过(含 SSE 流式/事件推送/编辑/撤回/公告/已读/搜索) --- ## 三、进程管理 ```bash php start.php # 前台调试模式 php start.php start -d # 后台守护进程 php start.php stop # 停止 php start.php restart # 重启 php start.php restart -d # 重启(守护进程) php start.php start --only=http # 仅启动 HTTP Worker php start.php start --only=sse # 仅启动 SSE Worker php start.php start --only=bot # 仅启动 Bot Worker ``` **运行时文件**(`runtime/` 目录): | 文件 | 说明 | |------|------| | `workerman.pid` | 主进程 PID | | `workerman.log` | Workerman 全局日志 | | `logs/bot_*.log` | 各 Bot Worker 日志 | | `logs/http.log` | HTTP Worker 日志 | | `logs/debug.log` | DEBUG 模式详细日志(需 `APP_DEBUG=true`) | | `logs/sse.log` | SSE Worker 日志 | --- ## 四、架构图 ``` 浏览器 ──HTTP GET /room/{id}/stream──▶ SseWorker (3004) │ ▼ TCP 总线(cmd=subscribe) TcpBusWorker (3003) │ ◀── TCP 总线(cmd=push)──┐ │ 浏览器 ──HTTP POST /room/{id}/message──▶ HttpWorker (3002) │ HttpWorker ──TcpBus.pushToRoom────────▶ TcpBusWorker ───────────────┘ HttpWorker ──TcpBus.broadcastBot──────▶ TcpBusWorker ──▶ BotWorkerImpl × 3 │ ▼ 消息队列(Redis List,向后兼容) Redis List: bot:{botId}:queue ──500ms lPop──▶ BotWorkerImpl × 3 HttpWorker ──syncRedis rPush──▶ Redis List: bot:{botId}:greet_queue ──500ms lPop──▶ BotWorkerImpl × 3(各自专属) SseWorker ──TCP 总线注册为 type=sse,onMessage 转发 SSE 消息给浏览器 TcpBusWorker (3003) ── Worker 间通信中枢(HTTP/SSE/Bot 连接注册 + 消息路由) ``` --- ## 五、进程模型 ``` Workerman 主进程(1个) ├── tcp_bus × 1 — TCP 总线,Worker 间通信中枢(端口 3003) ├── http × 1 — HTTP API,端口 3002 ├── sse × 1 — SSE 长连接,端口 3004 ├── bot:dog × 1 — AI Bot:🐕 热情金毛 ├── bot:gentle × 1 — AI Bot:🌸 温柔姐姐 └── bot:partner × 1 — AI Bot:🔥 中二战友 ``` **事件驱动**: - Bot 消息消费:TcpBus `broadcast_bot` 广播到所有 BotWorker,各 Bot 独立决策是否回复 - Bot 入群打招呼:TcpBus `broadcast_bot(is_greet=true)` 触发,各 Bot 自带随机延迟后问候 - SSE 推送:TcpBus `push` 命令,SseWorker subscribe 实时接收并转发浏览器 --- ## 六、目录结构 ``` app/ ├── events/ # 事件定义 │ ├── Event.php # 通用事件类(静态工厂方法生成各类事件) │ └── EventChannels.php # Redis 通道常量 ├── personas/ │ └── Personas.php # Bot 人格加载(从 prompts/ 目录读取) ├── controllers/ # HTTP 路由控制器(Phase 3 从 HttpWorker 拆分) │ ├── BaseController.php # 控制器基类(路由匹配 + 限流 + CORS) │ ├── RoomController.php # 房间相关接口(消息/历史/搜索/公告/反应) │ ├── UserController.php # 用户相关接口(画像/记忆) │ ├── UploadController.php # 文件上传 │ ├── CollectionController.php # 收藏管理 │ └── SystemController.php # 系统接口(监控指标) ├── services/ │ ├── RoomService.php # Redis 房间操作 │ ├── AiService.php # AI API 统一封装(workerman/http Client) │ ├── RoomDbService.php # MySQL 房间/成员操作 │ ├── UserService.php # MySQL 用户操作 │ ├── RateLimitService.php # 限流服务(内存 + Redis) │ ├── MemoryArchiveService.php # 记忆归档(AI 摘要) │ ├── BotAtmosphereService.php # Bot 气氛特效服务(标记提取/关键词匹配) │ └── DatabaseService.php # MySQL PDO 连接管理 ├── workers/ │ ├── WorkerInterface.php # Worker 接口规范 │ ├── BaseWorker.php # Worker 抽象基类(生产稳定性范式) │ ├── WorkerManager.php # 统一管理器(生命周期) │ ├── HttpWorker.php # HTTP API(3002) │ ├── SseWorker.php # SSE 长连接(3004) │ ├── BotWorkerImpl.php # Bot AI 处理(三阶段) │ └── TcpBusWorker.php # TCP 总线(3003) ├── helpers.php # 工具函数(loadEnv / debug_log) ├── RoomCoordinator.php # 房间协调器(创建/系统通知/入群顺序) └── config.php # 全局配置(从 .env 读取) prompts/ # AI 提示词模板(Markdown) ├── system.md # 基础系统提示词 + 决策规则 ├── bot_dog.md # 🐕 热情金毛人格 ├── bot_gentle.md # 🌸 温柔姐姐人格 ├── bot_partner.md # 🔥 中二战友人格 ├── greeting.md # 入群打招呼通用引导 ├── care.md # 主动关怀触发规则 ├── predict.md # 输入预测提示词 └── memory_extract.md # 记忆提取提示词 scripts/ └── diagnose.php # 环境诊断脚本 start.php # 唯一入口 web/ # Vue 3 + Vite 前端 ├── src/ │ ├── components/ # Vue 组件 │ ├── composables/ # 组合式逻辑(useApi / useSSE / useRoom) │ ├── stores/ # Pinia 状态管理 │ ├── config/ # 全局配置 │ │ └── bots.js # Bot 元数据(前后端共享定义的前端版本) │ └── __tests__/ # vitest 单元/集成测试 │ ├── setup.js # 全局 mock(EventSource / fetch / Notification) │ └── composables/ # useApi / useSSE / useRoom 测试 ├── package.json └── vitest.config.js # vitest + jsdom 配置 tests/ # PHP 单元测试(238 tests) ├── HttpApiTest.php # HTTP 接口测试 ├── HttpUploadApiTest.php # 上传接口测试 ├── HttpWorkerRateTest.php # 限流测试 ├── SseWorkerTest.php # SSE Worker 测试 ├── TcpBusWorkerTest.php # TCP 总线测试 ├── EventTest.php # Event 通用事件类测试 ├── EventChannelsTest.php # 事件通道测试 ├── RoomServiceTest.php # Redis 房间服务测试 ├── RoomDbServiceTest.php # MySQL 房间服务测试 ├── UserServiceTest.php # 用户服务测试 ├── AiServiceTest.php # AI 调用服务测试 ├── BotWorkerImplTest.php # Bot Worker 测试 ├── BotFlowTest.php # Bot 消息流 E2E 测试 ├── FullE2ETest.php # 完整流程 E2E 测试 ├── DecisionEngineTest.php # AI 决策引擎测试 ├── CollectionServiceTest.php # 收藏服务测试 ├── MemoryArchiveServiceTest.php # 记忆归档服务测试 ├── RateLimitServiceTest.php # 限流服务测试 ├── AchievementServiceTest.php # 成就服务测试 ├── UploadCleanupServiceTest.php # 上传清理服务测试 ├── BotAtmosphereServiceTest.php # 氛围特效服务测试 ├── XssFilterTest.php # XSS 过滤器测试 ├── JsonHelperTest.php # JSON 辅助测试 ├── ValidatorTest.php # 校验器测试 ├── LoggerServiceTest.php # 日志服务测试 ├── PersonasTest.php # Bot 人格加载测试 ├── RoomServiceTokenTest.php # Token 预算测试 └── E2ETest.php # E2E 测试基类 tests-e2e/ # PHP 端到端测试(需启动 Worker) ├── E2eTestCase.php # E2E 测试基类(HTTP + SSE 客户端) ├── SseFlowTest.php # SSE 流式端到端测试 ├── ChatFlowTest.php # 聊天流程端到端测试 ├── RoomFlowTest.php # 房间流程端到端测试 ├── ObservabilityApiTest.php # 可观测性 API 端到端测试 ├── SuggestionsTest.php # 建议功能端到端测试 └── AtmosphereAndPassiveTest.php # 氛围特效与被动事件端到端测试 ``` --- ## 七、配置(.env) ```bash cp .env.example .env # 复制模板,填写真实值 ``` | 变量 | 说明 | |------|------| | `APP_DEBUG` | `true` 开启 DEBUG 详细日志(写入 `runtime/logs/debug.log`) | | `BIND_ADDRESS` | 服务绑定地址(默认 `0.0.0.0`) | | `WORKER_PORT` | HTTP Worker 端口(默认 `3002`) | | `TCP_BUS_PORT` | TCP 总线端口(默认 `3003`) | | `SSE_PORT` | SSE Worker 端口(默认 `3004`) | | `TCP_BUS_HOST` | TCP 总线主机地址(默认 `192.168.80.103`) | | `MYSQL_HOST` / `MYSQL_PORT` | MySQL 地址和端口 | | `MYSQL_USER` / `MYSQL_PASSWORD` | MySQL 认证 | | `MYSQL_DATABASE` | 数据库名 | | `REDIS_HOST` / `REDIS_PORT` | Redis 地址和端口 | | `REDIS_PASSWORD` | Redis 密码 | | `REDIS_DB` | Redis 数据库编号 | | `NEWAPI_API_BASE` | AI 接口 Base URL(如 `https://api.openai.com/v1`) | | `NEWAPI_API_KEY` | AI 接口密钥 | | `AI_MODEL` | 模型名称 | | `API_BASE` | 前端 API 请求前缀(生产部署时注入到 HTML) | | `SSE_BASE` | SSE 服务地址(生产部署时注入到 HTML) | --- ## 八、本地前端开发(`npm run dev`) 前端使用 Vite 开发服务器(默认 `localhost:5173`),API 请求通过 **Vite Proxy** 转发到后端。 **1. 配置环境变量** ```bash cd web cp .env.example .env # 复制后按需修改 ``` `web/.env` 支持以下变量: | 变量 | 说明 | 默认值 | |------|------|--------| | `VITE_API_BASE` | 后端 API 地址(Vite proxy 目标) | `http://192.168.80.103:3002` | | `VITE_SSE_BASE` | SSE 服务地址(留空则回退到同域:3004) | `http://192.168.80.103:3004` | **若后端运行在其他机器**,只需修改 `web/.env`: ```bash VITE_API_BASE=http://192.168.80.103:3002 ``` **2. 启动开发服务器** ```bash npm install npm run dev ``` --- ## 九、Redis 数据结构 ### Pub/Sub 通道(实时消息,SseWorker subscribe 消费) | Key | 类型 | 说明 | |-----|------|------| | `room:pubsub` | PubSub | 房间所有消息事件(user/assistant/stream/system/announcement 等) | | `bot:thinking` | PubSub | Bot 流式思考事件(payload 含 bot_id + room_id) | | `bot:activity` | PubSub | Bot 活跃状态事件(payload 含 bot_id + room_id) | ### 存储 Key(List / String) | Key | 类型 | 说明 | |-----|------|------| | `room:{id}:history` | List | 房间聊天历史(最多 100 条,超出从头截断) | | `room:{id}:system` | List | 系统保留消息(`_keep=true`,永不截断) | | `bot:{botId}:queue` | List | Bot 专属消息队列(syncRedis lPop,每 500ms 轮询) | | `bot:{botId}:greet_queue` | List | Bot 入群打招呼命令(per-bot,无竞争) | | `room:{roomId}:bot:{botId}:session` | List | Bot 在指定房间的多轮对话历史(最多 20 条) | | `bot:{botId}:greeted:{roomId}` | String | 入群打招呼幂等标志(24h TTL) | --- ## 十、MySQL 数据库 ### users(用户) | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,自增 | | `username` | VARCHAR(50) | 用户名,唯一 | | `description` | VARCHAR(255) | 用户描述(可选) | | `created_at` | DATETIME | 创建时间 | ### rooms(房间) | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,自增 | | `room_id` | VARCHAR(64) | 房间号,唯一 | | `room_name` | VARCHAR(100) | 房间名称(可选) | | `password` | VARCHAR(128) | bcrypt 密码(可为空 = 公开房间) | | `creator` | VARCHAR(50) | 创建者用户名 | | `created_at` | DATETIME | 创建时间 | ### room_members(成员) | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,自增 | | `room_id` | VARCHAR(64) | 房间号 | | `username` | VARCHAR(50) | 成员用户名 | | `is_owner` | TINYINT(1) | 是否房主(0/1) | | `joined_at` | DATETIME | 加入时间 | --- ## 十一、HTTP 接口 服务地址:`http://0.0.0.0:3002` | 方法 | 路径 | 说明 | |------|------|------| | `GET` | `/` | 返回前端页面(Vue SPA) | | `GET` | `/uploads/{file}` | 访问已上传图片(防止路径遍历) | | `POST` | `/user` | 创建或获取用户 | | `POST` | `/room` | 创建房间(触发 Bot 入群打招呼) | | `POST` | `/room/{id}/join` | 加入房间(可选密码验证) | | `GET` | `/room/{id}/info` | 获取房间信息 + 成员 | | `POST` | `/room/{id}/message` | 用户发送消息(支持 `reply_to` 引用某条消息) | | `PATCH` | `/room/{id}/message/{msgId}/edit` | 编辑消息内容 | | `DELETE` | `/room/{id}/message/{msgId}` | 撤回消息(5分钟内本人消息) | | `POST` | `/room/{id}/rate` | 评价 Bot 回复(up/down) | | `POST` | `/room/{id}/react` | 添加/取消消息 emoji 反应 | | `GET` | `/room/{id}/announcement` | 获取房间公告 | | `PUT` | `/room/{id}/announcement` | 更新房间公告(仅房主) | | `GET` | `/room/{id}/history` | 获取房间历史(支持 before 分页) | | `GET` | `/room/{id}/search` | 搜索消息内容 | | `GET` | `/room/{id}/suggestions` | 获取 Bot 回复建议 | | `GET` | `/room/{id}/insights` | 获取房间洞察(懂用户报告) | | `GET` | `/user/{username}/memory` | 获取用户画像记忆 | | `POST` | `/user/{username}/memory` | 更新用户画像记忆 | | `GET` | `/user/{username}/memory-archives` | 获取归档记忆摘要 | | `GET` | `/user/{username}/bot-impressions` | 获取所有 Bot 对用户的印象 | | `POST` | `/upload/image` | 上传图片(最大 5MB,支持 jpeg/png/gif/webp) | | `GET` | `/metrics` | Prometheus 格式性能监控指标 | **SSE 连接**: - `GET http://IP:3002/room/{id}/stream?uid=xxx` — 消息流(user/assistant 等事件) - `GET http://IP:3004/room/{id}/stream` — SseWorker 直连(消息流) - `GET http://IP:3004/room/{id}/events` — SseWorker 直连(thinking/activity 事件) --- ## 十二、SSE 推送类型 SseWorker 同时维护两个 SSE 端点,事件来源如下: ### /stream 端点(`/room/{id}/stream`) 接收 TcpBus `push` 命令广播的房间事件,通过 `category` 和 `type` 字段区分: | category | type | 说明 | 推送通道 | 写 history? | |----------|------|------|---------|------------| | `stream` | `assistant` | Bot 回复流式吐字(chunk/done) | TcpBus `push` | ❌ | | — | `user` | 用户消息 | TcpBus `push` | ✅ | | — | `assistant` | Bot 最终回复(打招呼/评分感谢等) | TcpBus `push` | ✅ | | — | `system` | 系统通知 | TcpBus `push` | ❌ | | — | `edit` | 消息编辑 | TcpBus `push` | ✅ | | — | `recall` | 消息撤回 | TcpBus `push` | ❌ | | — | `read` | 消息已读回执 | TcpBus `push` | ❌ | | — | `reaction` | Emoji 反应 | TcpBus `push` | ❌ | | — | `suggestions` | Bot 回复建议 | TcpBus `push` | ❌ | | — | `announcement` | 房间公告更新 | TcpBus `push` | ❌ | | — | `bot_rated` | Bot 评分反馈 | TcpBus `push` | ✅ | ### /events 端点(`/room/{id}/events`) 接收 TcpBus `broadcast_sse` 命令广播的 Bot 状态事件,`category` 作为 SSE 事件名: | category | 说明 | payload 字段 | 推送通道 | |----------|------|------------|---------| | `thinking` | Bot 流式思考(status: start/chunk/done) | `status`, `content`, `bot_id`, `msg_id` | `bot:thinking` | | `activity` | Bot 活跃状态时间轴 | `bot_id`, `activity`, `content`, `reason` | `bot:activity` | `category=thinking` 的 payload 中,`status` 字段表示思考阶段: | status | 说明 | |--------|------| | `start` | Bot 开始思考 | | `chunk` | 思考内容增量(reasoning_content) | | `done` | 思考完成(reasoning_content 汇总) | ### 通用字段 | 字段 | 说明 | |------|------| | `type` | 事件类型 | | `role` | 角色:`user` / `assistant` | | `sender` | 发送者 ID(`bot_dog` / `user_xxx`) | | `sender_name` | 发送者显示名 | | `content` | 消息内容 | | `msg_id` | 消息唯一 ID(后端生成) | | `id` | 消息唯一 ID(与 `msg_id` 保持一致,前端消费优先使用) | | `ts` | Unix 时间戳 | ### activity 事件字段(category=activity) | 字段 | 说明 | |------|------| | `bot_id` | Bot ID(dog/gentle/partner) | | `activity` | 状态类型(见下表) | | `label` | 中文标签 | | `content` | 补充内容(如跳过原因) | | `reason` | 详细原因(可选) | **activity 状态类型**: | activity | 中文 | 触发时机 | |----------|------|---------| | `greeting` | 入群打招呼 | Bot 收到入群命令 | | `join` | 加入群聊 | Bot 入群完成 | | `msg_received` | 收到消息 | Bot 从队列 lPop 到消息 | | `thinking` | AI 思考中 | 开始调用 AI | | `decided` | 决定回复 | AI 判定需要回复 | | `skip` | 跳过(不回复) | AI 判定不回复 | | `reply` | 回复到群 | 回复发布到 history | | `idle` | 空闲 | 处理完成后 | | `care` | 主动关怀 | Bot 检测到用户长时间未发言,主动问候 | --- ## 十三、Bot 三阶段处理流程 ``` Timer 500ms → syncRedis lPop bot:{botId}:queue │ ▼ 1. askDecisionAsync() — AI 决策(是否回复) - 上一条消息是自己发的 → 跳过(推送 activity=skip) - AI 返回 NO → 跳过(推送 activity=skip) - AI 返回 YES → 继续(推送 activity=thinking) │ ▼ 2. streamThinkingAsync() — AI 流式思考 - 每个 chunk 推送 Redis PubSub bot:thinking(room_id 在 payload) - SseWorker subscribe 接收并转发到 /events 端点 │ ▼ 3. publishReply() — 写 history + SSE 推送 - 写 room:{id}:history(最多 100 条截断) - 推送 Redis PubSub room:pubsub(SseWorker subscribe → 前端聊天区) - 写 room:{roomId}:bot:{botId}:session(最多 20 条截断) - 推送 activity=reply ``` **决策规则**(内置于 `prompts/system.md`): - 用户表达负面情绪 → 应该回复 - 用户发出疑问或求助 → 应该回复 - 用户只是打招呼,且其他 Bot 已回应 → 可以不回复 - 上一条消息是自己发的 → 不回复(避免自言自语) --- ## 十四、Bot 入群打招呼 ``` POST /room(HttpWorker) │ ▼ RoomCoordinator.createRoom() ├─ MySQL: rooms 表插入 ├─ MySQL: room_members 表插入 ├─ dispatchGreetings() │ dog: delay = rand(0, 3)s → rPush bot:dog:greet_queue │ gentle: delay = rand(2, 6)s → rPush bot:gentle:greet_queue │ partner: delay = rand(5, 10)s → rPush bot:partner:greet_queue └─ pushWelcomeNotification() → Redis PubSub room:pubsub → SseWorker subscribe → 浏览器(system 类型) BotWorkerImpl Timer → syncRedis lPop bot:{botId}:greet_queue(各自专属,无竞争) → sleep(delay) → sendGreetingIfFirst() ├─ 检查 bot:{botId}:greeted:{roomId}(24h TTL,已打过 → 跳过) └─ 首次 → AI 非流式打招呼 → history + pubsub + activity=greeting ``` --- ## 十五、AI 人格与提示词模板 提示词统一存放于 `prompts/` 目录(Markdown 文件),由 `Personas.php` 动态加载。 | Bot ID | 名称 | 风格 | |--------|------|------| | `dog` | 🐕 热情金毛 | "汪!" 开头,大量 emoji,夸夸模式 | | `gentle` | 🌸 温柔姐姐 | 轻声细语,关注情绪,温暖克制 | | `partner` | 🔥 中二战友 | 热血斗志,⚡💪符号,打气鼓励 | **提示词模板文件**: | 文件 | 内容 | |------|------| | `prompts/system.md` | 基础系统提示词 + 决策规则(所有 Bot 共享) | | `prompts/bot_dog.md` | 🐕 热情金毛人格定义 | | `prompts/bot_gentle.md` | 🌸 温柔姐姐人格定义 | | `prompts/bot_partner.md` | 🔥 中二战友人格定义 | | `prompts/greeting.md` | 入群打招呼通用引导 | 修改提示词:直接编辑对应 Markdown 文件,无需改代码。 --- ## 十六、Worker 架构(BaseWorker 生产稳定性范式) 所有 Worker 统一继承 `BaseWorker` 抽象基类,实现 `WorkerInterface` 接口。 ### 类层级 ``` WorkerInterface(接口) └── BaseWorker(抽象基类:生命周期 / 进程保护 / 统一日志) ├── HttpWorker ├── SseWorker ├── BotWorkerImpl └── TcpBusWorker ``` ### WorkerInterface 规范 ```php interface WorkerInterface { public function getName(): string; // Worker 名称 public function getWorker(): \Workerman\Worker; // Workerman 对象 public static function onMasterInitialize(): void; // Master 初始化 public function setWorker(\Workerman\Worker $worker): void; // 注入 Worker 对象 } ``` ### BaseWorker 提供的统一能力 ```php abstract class BaseWorker implements WorkerInterface { // ── 进程生命周期(final,子类不可覆盖)── final public function onWorkerStart(int $workerId): void; // 框架层包装 final public function onWorkerStop(): void; // 框架层包装 // ── 业务生命周期(子类实现)── abstract protected function _onWorkerStart(int $workerId): void; protected function _onWorkerStop(): void; // ── 统一日志(格式固定)── final protected function log(string $msg, string $level = 'info', array $ctx = []): void; // ── 定时任务计数 ── final protected function tick(): void; } ``` **进程保护机制**: | 保护项 | 配置键 | 默认值 | 说明 | |--------|--------|--------|------| | 最大存活时间 | `max_existence_time` | 86400 秒 | 进程运行超期后发送 `SIGQUIT` 优雅退出,主进程自动拉起新进程 | | 最大定时任务次数 | `max_timer_count` | 0(不限制) | 每分钟检查 `_timerCount`,超过阈值优雅退出 | | 错峰退出 | — | workerId × 10 秒 | 防止所有 Worker 同时退出导致服务闪断 | | 致命错误捕获 | `register_shutdown_function` | — | 捕获 `E_ERROR / E_PARSE / E_CORE_ERROR` 等致命错误并记录 | **统一日志格式**: ``` 2026-05-11 20:35:11 [worker-ai:bot:dog:0] [p:12345] [n:0] [info] 消息内容 (pid=12345, worker_id=0) ``` ### 生命周期调用链 ``` WorkerManager::init() ├── onMasterInitialize() — Master 阶段,每个 Worker 类调用一次 ├── createWorkerGroup() — 为每个配置组创建 Worker + 子进程 │ ├── new WorkerClass($args) // 传入配置(含 max_existence_time / max_timer_count) │ ├── setWorker() → 注入 Workerman Worker │ ├── onWorkerStart() // BaseWorker 框架层:注册保护定时器 + 捕获异常 │ │ └── _onWorkerStart() // 子类业务逻辑 │ └── onWorkerStop() // BaseWorker 框架层:清理定时器 │ └── _onWorkerStop() // 子类业务逻辑 └── Worker::runAll() ``` --- ## 十七、端口一览 | 端口 | 协议 | 用途 | |------|------|------| | 3002 | HTTP | 浏览器 → HttpWorker(API + 静态文件) | | 3003 | TCP | 所有 Worker → TcpBusWorker(Worker 间通信) | | 3004 | HTTP | 浏览器 → SseWorker(SSE 长连接) | --- ## 十八、关键设计决策 ### per-bot Redis List + syncRedis 同步 lPop Bot 消息队列使用 per-bot List(`bot:{botId}:queue`),HTTP Worker 通过 syncRedis rPush 同步写入,Bot Worker 通过 syncRedis lPop 每 500ms 轮询。 **为什么不用 workerman/redis subscribe?** workerman/redis 的 `subscribe()` 在连接上设置 `_subscribe=true`,使得该连接的 `process()` 方法立即返回而不发送实际命令,导致同一连接上所有后续操作(包括 lPop)全部被静默吞掉。 **为什么不用 shared list + 多个 worker 竞争?** 共享 `bot:all:queue` 模式下,所有 Bot 竞争同一个 List,第一个 lPop 的 Bot 拿走消息,其余 Bot 收到空值(消息丢失)。改为 per-bot 队列后,每个 Bot 只消费自己专属 List,无竞争、无消息丢失。 ### 历史截断规则 - `room:{id}:history`:普通消息 + Bot 回复,最多 100 条,从头截断 - `room:{id}:system`:`_keep=true` 的系统提示词,永不截断 - `room:{roomId}:bot:{botId}:session`:Bot 在指定房间的对话,最多 20 条 --- ## 十九、Know-How(踩坑记录) ### 1. workerman/redis `subscribe()` 导致后续命令静默失败 **问题**:`BotWorkerImpl` 使用 workerman/redis `Client` 的 `subscribe()` 订阅 Redis Pub/Sub,之后同一连接上的 `lPop` 调用全部静默失效。 **原因**:`subscribe()` 设置 `_subscribe=true`,`process()` 立即返回不发送实际命令,事件循环被 suspend。 **解决**:Bot 消息传递改用 per-bot Redis List + PHP 原生 `\Redis` sync lPop,不依赖 workerman/redis Client 的 subscribe 模式。workerman/redis 仅用于 publish/subscribe SSE 频道(`publishToPubsub`)。 ### 2. HTTP Worker publish 阻塞事件循环 **问题**:`RoomService::publishToPubsub()` 使用 workerman/redis `publish()` 不传 callback,触发 EventLoop suspend。 **原因**:workerman/redis 在没有 callback 时会 suspend 事件循环直到收到响应,导致 HTTP 请求hang。 **解决**:所有 HTTP Worker 中的 Redis 写操作改用 PHP 原生 `\Redis` sync 命令(`rPush`、`setex` 等),不依赖 workerman/redis 的异步模式。Bot Worker 中的异步操作使用 workerman/redis + callback。 ### 3. PHP 7.4 箭头函数与 ARRAY_FILTER_USE_KEY 不兼容 **问题**:`array_filter($arr, fn($name) => ..., ARRAY_FILTER_USE_KEY)` 编译报错。 **原因**:箭头函数 `fn() =>` 不支持 `use` 闭包变量,也不兼容 `ARRAY_FILTER_USE_KEY`。 **解决**:使用匿名函数 `function($_, $name) use($arg) { return ...; }` + `ARRAY_FILTER_USE_BOTH`。 ### 4. Bot 入群打招呼共享队列竞争 **问题**:3 个 Bot 共享 `bot:greets:queue`,`lPop` 每次只返回一个 Bot 的命令,其余 Bot 漏掉问候命令。 **原因**:Redis List `lPop` 是原子的,每次只弹出一个元素。 **解决**:改为 per-bot greet queue(`bot:dog:greet_queue` 等),每个 Bot 只消费自己专属队列,无竞争。 ### 5. MySQL 密码特殊字符 **问题**:`.env` 中 `MYSQL_PASSWORD=WHyfrds@2025` 的 `@` 字符被当作环境变量分隔符处理。 **解决**:`helpers.php` 的 `loadEnv()` 使用 `explode('=', $line, 2)` 分割键值对(最多 2 段),`@` 不影响解析。推荐对含特殊字符的密码加双引号:`MYSQL_PASSWORD="WHyfrds@2025"`。 ### 6. AI API 调用统一封装 **问题**:多处代码直接同步调用 AI,阻塞事件循环: - `BotWorkerImpl` 使用 `curl_exec()` 同步调用 - `HttpWorker` 使用 `file_get_contents()` 同步调用(`callAiSync`) - `MemoryArchiveService` 使用 `file_get_contents()` 同步调用 **原因**:不同 AI 调用场景(非流式决策、流式思考、打招呼、输入预测、房间洞察、记忆归档)各自写一段同步请求代码,无法集中管理,且同步阻塞会卡住整个 Worker 进程。 **解决**:所有 AI 调用统一走 `App\services\AiService`: - **非流式**:`AiService::post()` → workerman\Http\Client(异步 I/O,共享连接池,不阻塞事件循环) - **流式**:`AiService::streamChat()` → workerman\Http\Client `progress` 回调(v2.2.9+ 支持逐 chunk 接收已解码的 SSE 数据) **历史演进**:早期流式使用 AsyncTcpConnection 手动拼接 HTTP + 解析 chunked transfer;workerman/http-client v2.2.9 新增 `progress` 回调后,统一迁移至 `AiService::streamChat()`,由 Client 处理底层连接和 chunked decode,代码更简洁可靠。 ### 7. Redis ping() 不能传 callback(Closure 序列化问题) **问题**:BotWorkerImpl 中 `$redis->ping(function () {})` 导致所有 Bot Worker 启动时崩溃退出:`Object of class Closure could not be converted to string`。 **原因**:workerman/redis 的 `encode()` 方法不支持 Closure 类型的 callback 参数,尝试将其序列化为字符串导致 Fatal Error。 **解决**:Redis ping() 不传 callback(同步模式):`$this->roomService->getRedis()->ping()`。 --- ## 二十、Flutter 客户端(`flutter/`) Flutter 重写版客户端,使用 Dart 构建跨平台界面。 ### 目录结构 ``` flutter/ ├── lib/ │ ├── main.dart │ ├── models/ # 数据模型(Room, Message, UserProfile, Collection 等) │ ├── screens/ # 页面(landing/room/collection/profile/search) │ └── services/ # 服务层(ApiService, SseService, PushService) ├── test/ │ ├── models/ # 模型测试(Message, RoomModels, MemoryArchive) │ └── services/ # 服务测试(ApiService, SseService) └── pubspec.yaml ``` ### 运行 ```bash cd flutter flutter pub get flutter run ``` ### 测试 ```bash cd flutter flutter test ``` ### 主要功能 - `ApiService`:Dio 封装,统一 HTTP 请求,含错误类型分类(network/timeout/rateLimit) - `SseService`:SSE 双频道(/stream + /events)管理,自动重连(指数退避,最大 30s) - `PushService`:FCM 推送通知,含 Token 注册、房间导航事件流