Skip to content

feat(partners): Partner 多用户隔离 — 按用户 scope 存储 partner 数据#566

Open
wedone wants to merge 5 commits into
HKUDS:devfrom
wedone:partner-multi-user-isolation
Open

feat(partners): Partner 多用户隔离 — 按用户 scope 存储 partner 数据#566
wedone wants to merge 5 commits into
HKUDS:devfrom
wedone:partner-multi-user-isolation

Conversation

@wedone

@wedone wedone commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description

本 PR 实现 Partner 多用户隔离——按用户 scope 存储 partner 数据。

问题背景:DeepTutor 启用多用户认证(multi-user)后,知识库、技能、笔记本等均按用户隔离,但 Partner 层遗漏了这一层隔离,出现两个问题:

  1. 普通用户无法创建 partner(partners router 硬编码为 _admin
  2. Channel 媒体/状态文件全局共享(不同用户的 partner 共用同一路径)

核心设计

模块 改动概要
路径层 partners/config/paths.py 引入 _base_dir_for_owner(owner_id) 替代 _base_dir();新增 get_partner_runtime_subdir;所有路径函数接受可选 owner_id 参数(避免 partner 运行时通过 get_current_user() 递归解析路径)
数据模型 services/partners/manager.py PartnerConfig 增加 owner_id 字段;save_config / load_config 读写 owner_id;_discover_all_partner_ids() 多用户扫描;list_partners / get_recent_active_partners 按当前用户过滤
运行时 services/partners/runtime.py owner_id 从 config 读取,修复空串 fallback 逻辑
API 注册 api/main.py partners router 从 _admin 降级为 _auth(仅需登录即可访问)
API 校验 api/routers/partners.py 新增 _check_partner_owner() 做细粒度 ownership 校验;各 /{partner_id} 端点加入校验;同名 partner 自动加后缀;admin 与普通用户完全隔离
Channel 基类 partners/channels/base.py 增加 self.partner_id: str = "" 属性,由 PartnerManager 注入
Channel 实现 channels/telegram.py / zulip.py / weixin.py / wecom.py / napcat.py / matrix.py / msteams.py / mochat.py 媒体/状态路径改为 get_partner_media_dir / get_partner_runtime_subdir 按 partner 隔离;MSTeams/MoChat 使用 @property 延迟初始化(避免 partner_id 注入前访问错误路径)

关键决策

  • Admin 与普通用户的 partner 完全隔离——admin 不能看到或操作普通用户的 partner,反之亦然
  • owner_id 为空串表示 admin owner(单机模式下 get_current_user() 返回 local_admin_user(),所有路径锚定 data/partners/,行为完全不变)
  • partner_id 在当前用户 scope 内唯一,创建时自动检测冲突并添加后缀;不同用户可创建同名 partner

向后兼容

  • 单机模式(AUTH_ENABLED=false):路径仍在 data/partners/ 下,行为完全不变
  • 已有 admin 创建的 partners:load_config() 自动补充 owner_id: ""(空串 = admin)并回写磁盘,admin 用户可继续操作

代码变更统计:22 个文件,519 行新增,154 行删除


Related Issues


Module(s) Affected

  • api
  • config
  • services
  • tests
  • Other: ...

Checklist

  • 我已阅读并遵循了 contribution guidelines
  • 我的代码遵循了项目的编码标准
  • 我已运行 ruff check --fix + ruff format 并修复了问题(msteams.py/mochat.py 添加 from pathlib import Path
  • 我已添加了相关测试或更新了现有测试
  • 如有必要,我已更新了文档
  • 我的变更不会引入新的安全漏洞

Additional Notes

(无额外事项)


提交记录

  • f5e309ab feat(partners): Partner 多用户隔离 — 按用户 scope 存储 partner 数据
  • 9b5ef4fd fix(partners): 修复 admin 越权看到普通用户 partner 的问题
  • 22368564 chore(partners): 添加 pathlib import + ruff format
  • 9795d318 fix(partners): 修复 load_config 迁移分支 owner_id 值错误及测试函数名不同步

wedone and others added 4 commits June 14, 2026 00:12
核心改动:
- 路径层:_base_dir() 改为 _base_dir_for_owner(owner_id),按 owner_id 解析到
  data/users/<uid>/partners/ 或 data/user/partners/,避免 get_current_user() 递归
- 数据模型:PartnerConfig 增加 owner_id 字段,save_config/load_config 读写
- Manager:_discover_all_partner_ids() 扫描所有用户目录,list_partners() 按用户过滤
- API:partners router 从 require_admin 降级为 require_auth + ownership 校验
- Channel:媒体/状态文件从全局共享改为按 partner 隔离(get_partner_media_dir/
  get_partner_runtime_subdir),BaseChannel 增加 partner_id 属性
- 迁移:无 owner_id 的旧 config 自动补充 owner_id: "local-admin"
- 同名 partner 自动加后缀避免全局冲突
- partner 不存在时返回 404 而非 403
- PartnerRunner.owner_id fallback 逻辑修复

向后兼容:单机模式(AUTH_ENABLED=false)行为不变
问题:admin 用户可以查看和操作所有普通用户的 partner,违反了多用户隔离预期。

修复:
- _check_partner_owner():admin 只允许操作 owner_id 为空的 partner
- partner_chat_ws():WebSocket 端点同理修复
- list_partners():admin 只看到 admin 自己的 partner(owner_id 为空)
- get_recent_active_partners():admin 同理过滤

内存循环使用 resolve_owner_for_partner()(基于磁盘位置)而非
config.owner_id,避免遗留迁移 partner(owner_id="local-admin")被误过滤。

Closes: #(issue)
- msteams.py / mochat.py 添加 from pathlib import Path(修复 F821)
- matrix.py import 格式调整
- manager.py / paths.py / workspace.py / partners.py ruff format 格式化
- ruff check 全部通过
- 测试:79 passed
- partner_exists() 传入 owner_id 参数,仅检查当前用户 scope 内冲突
- 不同用户可以创建同名 partner(路径已按 owner 隔离)
- 移除重复的 current_user/owner_id 获取代码
- 测试:79 passed
@wedone wedone force-pushed the partner-multi-user-isolation branch from e86e66a to b834d5d Compare June 13, 2026 16:56
1. load_config() 迁移分支将 owner_id 从 "local-admin" 改为 ""(空串)。
   "local-admin" 是非空串,会被 _base_dir_for_owner() 解析到
   data/users/local-admin/partners/,与老 partner 实际所在的
   data/partners/ 不一致,导致 save_config/start_partner/runtime
   路径发散(编辑写丢、运行时丢人设、产生幽灵目录)。
   空串 "" 才是 admin 的正确约定,与 _base_dir_for_owner() 的
   分支逻辑一致。

2. 三个测试文件的 monkeypatch 从旧函数名同步改为新函数名:
   - test_zulip_channel.py: get_media_dir → get_partner_media_dir
   - test_weixin_channel.py: get_runtime_subdir → get_partner_runtime_subdir
   - test_msteams_channel.py: get_runtime_subdir → get_partner_runtime_subdir
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant