Skip to content

Latest commit

 

History

History
51 lines (43 loc) · 15 KB

File metadata and controls

51 lines (43 loc) · 15 KB

MaiMBot 多租户改造核验(2025-12-06)

背景

  • 目标:核实 MULTI_TENANT_MIGRATION_STRATEGY.md 中提出的四项手段是否已经真正落地,并复核 Todo #6 风险地图里的模块。
  • 仓库:MaiMBot(运行时)与 maim_db(统一 DB 层)。检查基于 master 当前工作副本。

现状总结(2025-12-06 代码核查)

已完成的隔离/上下文改造

  • Chat stream ID 前缀src/chat/message_receive/chat_stream.py 通过 _hash_components 统一在 MD5 前拼接 tenant_id/agent_id,并在缺少上下文时抛出 RuntimeErrorgroup_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.py 605 行起),所有非基础类型属性访问都会根据当前上下文查询 _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 的缓存链路MoodManagerFrequencyControlManagerExpressionLearner/ReflectorJargonMinerManagerGlobalAnnouncementManager 等模块虽然以内存缓存或磁盘文件按 chat_id 分桶,但对应的 chat_id 现由 ChatStream._hash_components 注入 tenant_id/agent_idsrc/chat/message_receive/chat_stream.py 133-170 行),且异步任务通过 AsyncTask.run_for 进入 tenant_context_asyncsrc/manager/async_task_manager.py 24-80 行),因此它们的键空间和 ORM 访问都已经具备租户维度。
  • ChatStream 正式落地上下文与持久化字段src/chat/message_receive/chat_stream.py 现在在内存对象中显式持久化 tenant_id/agent_id,并在 _save_streamget_or_create_streamload_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_idheartflow.get_or_create_heartflow_chat 仅由 HeartFCMessageReceiver.process_message 调用,而该处理器由 message_handlertenant_context_async 中驱动;心流实例保存在内存 dict,key 为 ChatStream._hash_components 生成的 chat_id,不落盘也不访问数据库,因此天然具备租户隔离。
  • Dream 维护任务接入 AsyncTaskManagerDreamMaintenanceTasksrc/dream/dream_agent.py 700 行起)继承 AsyncTask,在 MainSystem._init_components 中通过 async_task_manager.add_task 注册(src/main.py 118-157 行)。AsyncTask.run_for 会先 tenant_context_async(tenant_id, agent_id),因此 run_dream_cycle_once/_pick_random_chat_idChatHistory 查询天然具备租户过滤,不再出现“Tenant context missing”或跨租户扫描。

仍未解决/需要优先关注的隔离风险

  • 其他后台/工具模块Hippo 摘要、ImageManager、WebUI 路由等在策略文档里列出的模块尚未改动,仍然缺失 (tenant, chat) namespacing 或 TenantModel 字段,文件/缓存依旧是共享目录,需要按照风险表持续整改。
  • 零散定时循环未并入 AsyncTaskManager:聊天流自动保存(ChatManager._auto_save_task)依旧在 ChatManager 内部通过 asyncio.create_task 启动,但该任务仅处理内存状态持久化,不依赖租户上下文,保留原实现即可。表情扫描则已迁入 AsyncTaskManager(见下文“风险地图复核结果”更新),不再属于风险项。

1. 四大策略落实情况

策略 预期 观察到的现状(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_managerfrequency_control_managerexpression 系列)已经因此自动具备租户维度,但磁盘目录仍共用 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 层风险已从本次核验范围中移除。

2. 风险地图复核结果

风险条目(来自 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 行

3. 建议的后续动作

  1. 先补齐基础能力:运行时配置代理依旧需要上下文,虽然 API-Server 入站消息已在 handler 中设置 tenant_context_async,但 WebUI、命令行脚本和后台任务入口仍未建立上下文;仍需同步落地 TenantChatManagerTenantModel schema 迁移,并在所有入口显式设置 tenant_context 以解锁代理的价值。
  2. 执行数据库迁移 & 回填:为 ChatStreams, Messages, ChatHistory, ThinkingBack, LLMUsage, Emoji, Expression, Jargon 等核心表添加 tenant_id 字段与索引,并规划历史数据 owner/回填策略。
  3. 缓存/文件分桶:按照风险表优先级对 Hippo cache、emoji/image/统计输出等仍共享磁盘状态的模块做命名空间拆分。没有租户目录的任务即使在 per-tenant 调度下也会串租。
  4. 按租户调度后台任务:在 AsyncTask 子类的入口显式读取 tenant_id 并把它传播到 DB/API 层,禁止“未找到活跃租户时回退为全局执行”。同时梳理任务副作用(如 local_storage、HTML 输出),拆分为 per-tenant 资源。
  5. 关闭 WebUI(当前决策):新分租户模式将不启用 WebUI;部署时请在服务配置中关闭 WebUI 接口并归档相关路由代码。若未来需要重新启用 WebUI,必须在路由层从 API Key 或请求头解析 tenant_id,并在进入 ORM 前调用 tenant_context 或显式 where 过滤。
  6. 验证链路:为每个模块建立“同群不同租户”测试用例,确保缓存、文件和数据库写入不会互相覆盖。

本报告聚焦代码层静态核查;未覆盖尚未提交的配置、部署脚本或运行时环境。如果需要进一步的迁移计划/验收 checklist,可在上述基础能力就绪后再生成新版本。