- 目标:核实
MULTI_TENANT_MIGRATION_STRATEGY.md中提出的四项手段是否已经真正落地,并复核 Todo #6 风险地图里的模块。 - 仓库:
MaiMBot(运行时)与maim_db(统一 DB 层)。检查基于master当前工作副本。
- Chat stream ID 前缀:
src/chat/message_receive/chat_stream.py通过_hash_components统一在 MD5 前拼接tenant_id/agent_id,并在缺少上下文时抛出RuntimeError,group_generator.py/private_generator.py也改为复用该 helper,确保 chat_id 的租户唯一性。 - API 消息入口注入上下文:
src/common/message/api.py:message_handler在解析 API Key 成功后会async with tenant_context_async(tenant_id, agent_id)再调用旧链路,并把标识写入message_info.additional_config与活跃上报,保证配置代理和 TenantModel 在 API Server 场景下可用。 - 配置代理钩子仍有效:
global_config/model_config依旧通过_RuntimeAwareConfig/_RuntimeAwareAPIAdapterConfig包装(src/config/config.py605 行起),所有非基础类型属性访问都会根据当前上下文查询_TenantConfigProvider的缓存,现阶段未发现新的直读配置绕过。 - ORM 层上下文拦截:
src/common/database/database_model.py在 SAAS 模式下让所有 Peewee 模型继承BaseModel(BusinessBaseModel),强制字段tenant_id并在select/create/update/delete中调用_require_ids(文件 1-120 行),因此只要入口设置了tenant_context,ORM 会自动追加租户过滤和写入。 - 依赖 chat_id 的缓存链路:
MoodManager、FrequencyControlManager、ExpressionLearner/Reflector、JargonMinerManager、GlobalAnnouncementManager等模块虽然以内存缓存或磁盘文件按chat_id分桶,但对应的chat_id现由ChatStream._hash_components注入tenant_id/agent_id(src/chat/message_receive/chat_stream.py133-170 行),且异步任务通过AsyncTask.run_for进入tenant_context_async(src/manager/async_task_manager.py24-80 行),因此它们的键空间和 ORM 访问都已经具备租户维度。 - ChatStream 正式落地上下文与持久化字段:
src/chat/message_receive/chat_stream.py现在在内存对象中显式持久化tenant_id/agent_id,并在_save_stream、get_or_create_stream、load_all_streams等数据库操作前使用tenant_context包裹(引用调用者上下文),确保ChatStreams表的tenant_id/agent_id字段被写入且 ORM 查询限定在当前租户。 - 记忆检索链路继承 API 上下文:
build_memory_retrieval_prompt仅由group_generator.py/private_generator.py在主回复链路中调用,而主回复流程自src/common/message/api.py:message_handler起即运行在tenant_context_async(tenant_id, agent_id)下,因此memory_retrieval.py内部对ThinkingBack的查询/清理天然具有租户上下文;未发现其他未包裹的调用入口。 - Heartflow/BrainChat 只依赖带租户前缀的
chat_id:heartflow.get_or_create_heartflow_chat仅由HeartFCMessageReceiver.process_message调用,而该处理器由message_handler在tenant_context_async中驱动;心流实例保存在内存 dict,key 为ChatStream._hash_components生成的chat_id,不落盘也不访问数据库,因此天然具备租户隔离。 - Dream 维护任务接入 AsyncTaskManager:
DreamMaintenanceTask(src/dream/dream_agent.py700 行起)继承AsyncTask,在MainSystem._init_components中通过async_task_manager.add_task注册(src/main.py118-157 行)。AsyncTask.run_for会先tenant_context_async(tenant_id, agent_id),因此run_dream_cycle_once/_pick_random_chat_id的ChatHistory查询天然具备租户过滤,不再出现“Tenant context missing”或跨租户扫描。
- 其他后台/工具模块:
Hippo摘要、ImageManager、WebUI 路由等在策略文档里列出的模块尚未改动,仍然缺失(tenant, chat)namespacing 或 TenantModel 字段,文件/缓存依旧是共享目录,需要按照风险表持续整改。 - 零散定时循环未并入 AsyncTaskManager:聊天流自动保存(
ChatManager._auto_save_task)依旧在ChatManager内部通过asyncio.create_task启动,但该任务仅处理内存状态持久化,不依赖租户上下文,保留原实现即可。表情扫描则已迁入 AsyncTaskManager(见下文“风险地图复核结果”更新),不再属于风险项。
| 策略 | 预期 | 观察到的现状(2025-12-06) | 结论 |
|---|---|---|---|
| 配置代理(GlobalConfigProxy / ModelConfigProxy) | global_config / model_config 在访问时通过租户上下文解析;模块不再在导入时固化配置。 |
src/config/config.py 仍按 _RuntimeAwareConfig/_RuntimeAwareAPIAdapterConfig 注入代理,并要求 tenant_context 提供 (tenant_id, agent_id);src/common/message/api.py:message_handler 现已在解析出 API Key → tenant/agent 后进入 tenant_context_async 再调用原处理流程,但 WebUI、测试脚本等入口依旧没有上下文,仍然可能触发“Missing tenant_id or agent_id” 异常。 |
实现但受限于上下文覆盖面 |
| chat_id 加租户前缀(TenantChatManager) | 所有缓存/文件命名改为 tenant:chat 或分租户目录。 |
ChatManager 现已在 _hash_components 中强制读取 tenant_id/agent_id 并把它们拼接进哈希(MaiMBot/src/chat/message_receive/chat_stream.py 133-164 行),旧入口在缺少上下文时会直接抛出 RuntimeError。依赖 chat_id 的缓存(如 mood_manager、frequency_control_manager、expression 系列)已经因此自动具备租户维度,但磁盘目录仍共用 data/*,需要继续拆分命名空间。 |
核心入口已实现,周边缓存未改 |
| ORM 层自动添加 tenant_id(TenantModel) | Peewee 模型统一继承带 tenant_id 的 BaseModel,且在请求/任务入口设置 tenant_context。 |
MaiMBot/src/common/database/database_model.py 的 BaseModel 仍要求上下文自动填充 tenant_id;现在 message_handler 会在拿到 API Key 解析结果后通过 tenant_context_async 包裹 handler.message_process,因此经 API-Server 进入的消息在多数分支上具备上下文。但 WebUI/HTTP 路由、离线脚本与 DB 迁移仍未提供 tenant_id 字段,ChatStreams.replace(...) 等写操作只要脱离消息链路仍会因 _require_ids 抛错。 |
实现但仅限 API 链路可用 |
| per-tenant AsyncTaskManager | 调度器为每个活跃租户/Agent 启动协程并在 tenant_context 下执行。 |
src/manager/async_task_manager.py 继续以 per_tenant=True 为默认并依赖 AsyncAgentActiveState.list_active(),message_handler 现在会在入站消息成功解析后调用 upsert_active_from_message 并设置上下文,因此后台任务能拿到更多活跃记录。但大量任务内部仍按全局缓存/文件操作,没有 (tenant, chat) 维度,导致即使 per-tenant 调度也会串租。 |
部分落地,缓存/存储仍未隔离 |
附注:全仓搜索
tenant_context(/tenant_context_async(现新增src/common/message/api.py:message_handler的包裹;注意:新分租户模式将不启用 WebUI,WebUI 路由不再作为运行时入口,因此相关的 HTTP 层风险已从本次核验范围中移除。
| 风险条目(来自 Todo #6) | 期望的缓解措施 | 当前代码状态 | 证据 |
|---|---|---|---|
src/hippo_memorizer/chat_history_summarizer.py |
HIPPO_CACHE_DIR 拆分/前缀化。 |
HIPPO_CACHE_DIR 固定为 data/hippo_memorizer,缓存文件命名为 <chat_id>.json。 |
文件 19-78 行 |
src/chat/utils/utils_image.py |
ImageManager/Images 表携带 tenant 信息,磁盘分目录。 |
ImageManager.IMAGE_DIR = "data",所有租户共用目录;数据库模型缺少 tenant_id。 |
文件 18-70 行 |
src/chat/emoji_system/emoji_manager.py |
表情扫描/注册任务在 per-tenant 上下文运行,并拆分磁盘目录。 | 已解决:EmojiManager 引入 run_maintenance_cycle,并新增 EmojiMaintenanceTask(AsyncTask),由 MainSystem._init_components 注册到 async_task_manager,AsyncTaskManager 将根据活跃租户上下文运行循环;表情目录此前已经借助 tenant_storage 拆分为 data/tenant_storage/<tenant>/<agent>/emoji*。 |
MaiMBot/src/chat/emoji_system/emoji_manager.py 600-750 行;src/main.py 110-150 行 |
| (WebUI 已禁用 — 路由相关风险已移除) | 新分租户模式不使用 WebUI;运行时入口不包含 HTTP 路由。 | 既然部署层选择不启用 WebUI,本次核验已从风险表中移除所有 WebUI 路由相关项。若将来重新启用 WebUI,请恢复对 webui/* 路由的专门核查,确保在路由层解析并设置 tenant_context 后再访问 ORM。 |
— |
src/chat/emoji_system/emoji_manager.py |
每个租户独立 emoji/emoji_registered 目录。 |
EMOJI_DIR = data/emoji,扫描/清理/盗表情任务共享目录与 emoji_objects。 |
文件 20-70、298-360 行 |
src/manager/async_task_manager.py |
任务运行前必须获取活跃租户上下文。 | AsyncTask.run_for 会进入 tenant_context_async,且消息入口现在会在成功解析 API Key 后 upsert_active_from_message,因此 per-tenant 任务能拿到更多上下文;但任务实现本身仍直接操作全局缓存/文件,无法真正隔离租户数据。 |
文件 1-140 行 |
- 先补齐基础能力:运行时配置代理依旧需要上下文,虽然 API-Server 入站消息已在 handler 中设置
tenant_context_async,但 WebUI、命令行脚本和后台任务入口仍未建立上下文;仍需同步落地TenantChatManager、TenantModelschema 迁移,并在所有入口显式设置tenant_context以解锁代理的价值。 - 执行数据库迁移 & 回填:为
ChatStreams,Messages,ChatHistory,ThinkingBack,LLMUsage,Emoji,Expression,Jargon等核心表添加tenant_id字段与索引,并规划历史数据 owner/回填策略。 - 缓存/文件分桶:按照风险表优先级对 Hippo cache、emoji/image/统计输出等仍共享磁盘状态的模块做命名空间拆分。没有租户目录的任务即使在 per-tenant 调度下也会串租。
- 按租户调度后台任务:在
AsyncTask子类的入口显式读取tenant_id并把它传播到 DB/API 层,禁止“未找到活跃租户时回退为全局执行”。同时梳理任务副作用(如local_storage、HTML 输出),拆分为 per-tenant 资源。 - 关闭 WebUI(当前决策):新分租户模式将不启用 WebUI;部署时请在服务配置中关闭 WebUI 接口并归档相关路由代码。若未来需要重新启用 WebUI,必须在路由层从 API Key 或请求头解析
tenant_id,并在进入 ORM 前调用tenant_context或显式 where 过滤。 - 验证链路:为每个模块建立“同群不同租户”测试用例,确保缓存、文件和数据库写入不会互相覆盖。
本报告聚焦代码层静态核查;未覆盖尚未提交的配置、部署脚本或运行时环境。如果需要进一步的迁移计划/验收 checklist,可在上述基础能力就绪后再生成新版本。