partners 是一层 IM 渠道:它接到了哪些地方

2026-08-10

看到一个项目里有 partners 这么个包,第一反应通常是「这里面装着一套 partner 的运行逻辑」。DeepTutor 这个包不是。包的 docstring 第一句就把边界划掉了,剩下的事就好读多了。

以下行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,文件名、类属性名和常量名是稳的,照着搜即可。

一、这个包里没有 partner 引擎

deeptutor/partners/__init__.py:1-9 的包 docstring 写明:这里托管的是 channel(IM)层——聊天平台集成、把渠道与 agent 运行时解耦的消息总线,以及配置 schema。紧接着的 :5-8 补了一句更关键的:agent 运行时本身在 deeptutor.services.partners,复用 chat capability 的 agent loop(ChatOrchestratorAgenticChatPipeline),没有独立的 partner 引擎

这句话决定了你读这个包的姿势。12252 行、30 个 .py 文件(deeptutor/partners 全量统计,2026-08-10),全是「怎么把消息收进来、怎么把回复发出去、按什么配置发」,不包含「怎么想」。所以当你排查「partner 回答得不对劲」这类问题时,deeptutor/partners/ 大概率不是现场;而排查「消息根本没进来」「回复发不出去」「某个平台加不上」,现场就在这里。

顺带一个容易看漏的细节:这个包自带独立版本号,__init__.py:11 写的是 __version__ = "2.0.0",与项目本身的 1.5.11 不是同一套编号。我们没有核实这个包版本号在哪里被消费,这里只记录它存在且与项目版本不同。

API 那一侧的定位也对得上:deeptutor/api/routers/partners.py:1-8 的 router docstring 把 partner 描述为「由 chat agent loop 驱动的 IM 连接伙伴」。挂载点是 /api/v1/partners,32 条路由,而且它是唯一整体挂 require_admin 的业务 router(deeptutor/api/main.py:464-466)——这一条与本篇直接相关:partner 的增删起停不是普通用户能碰的。api 层的整体装配另有一篇专门讲,这里不展开。

二、16 个渠道模块,逐个对上号

deeptutor/partners/channels/ 下除 base.pymanager.pyregistry.py__init__.py 之外,我们数出来是 16 个渠道模块。每个模块都在类里声明了 namedisplay_name 两个属性,行数差距很大:

模块name / display_name行数类属性位置
weixin.pyweixin / “WeChat”1564:147-148
feishu.pyfeishu / “Feishu”1342:278-279
mochat.pymochat / “Mochat”1062:280-281
telegram.pytelegram / “Telegram”1060:220-221
zulip.pyzulip / “Zulip”844:66-67
matrix.pymatrix / “Matrix”843:208-209
msteams.pymsteams / “Microsoft Teams”836:111-112
napcat.pynapcat / “QQ (NapCat)“582:64-65
dingtalk.pydingtalk / “DingTalk”527:127-128
discord.pydiscord / “Discord”505:52-53
email.pyemail / “Email”454:67-68
mattermost.pymattermost / “Mattermost”438:68-69
wecom.pywecom / “WeCom”381:50-51
slack.pyslack / “Slack”321:54-55
whatsapp.pywhatsapp / “WhatsApp”189:35-36
qq.pyqq / “QQ”186:67-68

这张表怎么读:行数不代表功能强弱,但它是个有用的先验——1564 行的 weixin.py 和 186 行的 qq.py 显然不在同一个复杂度量级上,接入前先打开看一眼各自处理了什么,比照着 README 的平台清单排期靠谱。至于每个平台的鉴权流程、媒体上传、消息格式转换,我们只读了 name / display_name / 配置模型和少量入口,没有逐行核实,这里不做任何平台之间的能力对比。

两个模块值得单独点名,因为它们涉及本机之外的东西:

  • whatsapp.py 走 Node.js bridge。 默认 bridge_url = "ws://localhost:3001",注释写明这个 bridge 用 @whiskeysockets/baileys 处理 WhatsApp Web 协议,Python 与 Node 之间用 WebSocket 通信(whatsapp.py:18-33)。也就是说这条渠道要另起一个本机 Node 进程,它不在 Python 包的依赖树里。
  • qq.py 基于 botpy SDK。 intents 为 public_messages=True, direct_message=True,并显式关掉了 botpy 自带的文件日志(qq.py:29-46)。

另外,渠道基类里的语音转写走 Groq Whisper,没有 key 时返回空字符串(base.py:47-58;provider 实现在 deeptutor/partners/transcription.py,61 行)。这一条要如实说明:启用语音消息意味着音频会离开本机送到第三方服务,是否使用请结合你自己的合规要求判断。同理,每接一个 IM 渠道就意味着要在本机配置该平台的凭据,这些都是敏感数据。

三、渠道是怎么被发现的:零导入扫描 + 插件入口

registry.py 这个文件不长,但它决定了「你的部署里到底有几个渠道可选」。

发现机制是 pkgutil.iter_modules 扫描包目录,零导入——只看模块名不 import 模块,排除集合写死为 _INTERNAL = {"base", "manager", "registry"}registry.py:14-25)。外部渠道插件另走 entry_points,group 名是 deeptutor.partners.channels;规则是内置优先,插件不能遮蔽同名内置registry.py:40-5154-5880-84)。

真正有排查价值的是 discover_all_with_errors()registry.py:63-78):它会把加载失败的内置渠道名连同 import 错误消息一起返回,注释里说明这么做是为了让「为什么少了 X」可诊断,而不是静默丢弃。

这一条直接对应一个常见现象:渠道模块在仓库里躺着,不等于你的环境里能用。文件存在是 16 个,能不能 import 成功取决于对应的第三方 SDK 装没装。而依赖是按 extra 分组的:pyproject.toml:131-156.[partners] extra 里列了 telegram、wecom、lark(feishu)、dingtalk、slack、qq-botpy、socketio、zulip、websockets、aiohttp、PyJWT、qrcode 等;Matrix 渠道被拆到独立的 matrix / matrix-e2e extra(pyproject.toml:179-191)。emailmochatmattermostmsteamswhatsappnapcatweixin 这几个渠道没有以自身命名的 SDK 包出现在 extra 里(其中 napcat / msteams / weixin 各有一条带指名注释的通用依赖,见 pyproject.toml:150-155),不能据此断言它们在 extra 里完全没有依赖覆盖。README 用的限定说法是「depending on installed extras and configured credentials」(README.md:511),但没有给出「哪个渠道属于哪个 extra」的映射表。

所以「某个渠道没出现」的判定动作很明确:先看它是不是被 discover_all_with_errors() 归到了失败集合里、错误消息是什么,而不是先去翻配置文件。如果它压根不在失败集合里,那就不是依赖缺失这个原因,得往别处找。

四、本篇最反直觉的一处:allow_from 为空 = 全部拒绝

base.py:116-124is_allowed() 只有几行,语义是这样的:

  • allow_from全部拒绝,并打一条 warning
  • allow_from 里含 "*" → 全部放行
  • 其余情况 → 按名单精确匹配

反直觉在第一条。按大多数人配白名单的经验,「没填」通常等于「不限制」;这里恰恰相反,没填就是一个都不放。这个方向本身是 fail-closed 的取值,对一个会接到公开 IM 平台上的组件来说是说得通的,但它带来的现象很具体:渠道能启动、日志里没有报错级别的异常,消息就是没反应

判定动作:去日志里找 is_allowed() 打出的那条 warning(base.py:116-124)。找到它,说明是这个原因;找不到它,说明消息压根没走到访问控制这一步,那就不是这个原因,得回去看渠道有没有真的收到入站消息。

同一个文件里还有第二处「且」的逻辑,容易被当成开关:supports_streaming 是个 property,要求 config 里 streaming 为真 并且子类真正覆写了 send_deltabase.py:105-114)。也就是说,配置里把 streaming 打开,如果这个渠道的实现没覆写 send_delta,它照样不是流式。BaseChannel 是 ABC,抽象方法只有 start() / stop() / send() 三个(base.py:1960-88),send_delta() 是可选覆写(base.py:90-103)——所以「哪些渠道支持流式」这件事,答案在各渠道模块里,不在配置文件里。我们没有逐个核实 16 个模块谁覆写了 send_delta,你要用的话自己 grep 一遍这个方法名即可。

五、总线、重试与落盘位置

消息总线 MessageBus 的实现比想象中朴素:两个 asyncio.Queue,一个 inbound 一个 outbound(deeptutor/partners/bus/queue.py:17-18)。会话的切分键是 InboundMessage.session_key,取值为 session_key_overridef"{channel}:{chat_id}"deeptutor/partners/bus/events.py:21-24)。这一行值得记住:默认情况下,同一个平台同一个会话 id 就是同一条会话,跨平台不会串。

出站失败的退避是写死的常量 _SEND_RETRY_DELAYS = (1, 2, 4)deeptutor/partners/channels/manager.py:23),schema 里的默认值是 send_max_retries: int = 3deeptutor/partners/config/schema.py:50)。这两个数是代码里的默认配置,不是「你会不会丢消息」的保证——退避序列是三档,最大重试次数默认也是 3,两处是否总是一一对应,我们没有读 manager.py 的重试循环实现,不做推断。

配置 schema 这一层有两个约定要知道:渠道配置基类用 Pydantic 且启用了 camelCase 别名生成(alias_generator=to_camel, populate_by_name=True),ChannelsConfig 设了 extra="allow"schema.py:1247);DeliveryOverrides 里两个开关 send_progress / send_tool_hints 默认都是 Trueschema.py:15-25)。extra="allow" 意味着你在配置里写错一个键名不会被拒,它会被原样收下——排查「我明明配了但没生效」时,先怀疑键名拼写和 camelCase 转换。

落盘位置是本篇第二处需要留神的设计。partner 的数据树锚定在 admin workspace 的 data/partners/,而不是当前用户的 path service;deeptutor/partners/config/paths.py:10-16 的注释解释说,这是因为 partner 运行在合成 scope 里、它的 workspace 就在这棵树下。每个 partner 的目录结构是 data/partners/{id}/(config / sessions / workspace)、.../workspace.../sessions.../media[/<channel>]paths.py:34-53)。

实际含义:你按「当前登录用户的目录」去找 partner 的会话和媒体文件,会找错地方。它不在 data/users/<uid>/ 下面。多用户与授权那一套机制另有一篇专门讲,本篇只交代 partner 数据落在哪棵树上。

六、README 与代码对不上的两处

按纪律,这里只陈述差异、标明位置,不推断原因、不评价:

  1. README 的渠道平台清单少列两个已存在的模块。 README.md:511 写的是可连接 Feishu、Telegram、Slack、Discord、DingTalk、QQ/NapCat、WeCom、WhatsApp、Zulip、Mattermost、Matrix、Mochat 和 Microsoft Teams,未提及代码中同样存在的 emailchannels/email.py:67-68,454 行)与 weixindisplay_name 为 “WeChat”,channels/weixin.py:147-148,1564 行)。代码侧我们数出来是 16 个模块。补充一句:README 全文除 :511 之外,WeChat 只出现在社群二维码与徽章语境(README.md:39README.md:188),没有被当作 partner 渠道介绍。
  2. README 版本记事写的是「15 channels」。 README.md:102 的 v1.4.3 记事称 Partners 上了 15 个渠道;我们在 HEAD(456f9c2,版本 1.5.11)数到的是 16 个模块。该记事是历史条目,README 正文没有给出更新后的数字。

另外 AGENTS.md 的 Key Files 表里没有出现 deeptutor/partners/(表在 AGENTS.md:101-118,api 侧只列了 deeptutor/api/routers/unified_ws.py 一个文件)。以我们实读的仓库状态为准。说完就停。

七、你可以照着核的六步

  1. 打开 deeptutor/partners/__init__.py:1-11,确认这层只是 channel 层、agent 运行时在 deeptutor.services.partners,以及包自带的 __version__ = "2.0.0"
  2. ls deeptutor/partners/channels/*.py,去掉 base / manager / registry / __init__ 之后数一遍,对照本文表格的 16 行。
  3. 打开 channels/base.py:116-124,确认 allow_from 为空是拒绝而不是放行,并记下那条 warning 的措辞。
  4. 打开 channels/base.py:105-114,确认 supports_streaming 是「配置为真」与「子类覆写 send_delta」的与逻辑,再 grep 一遍 send_delta 看哪些模块覆写了。
  5. 打开 channels/registry.py:63-78,确认 discover_all_with_errors() 会回传失败渠道名与 import 错误——这是排查「少了某个渠道」的第一现场。
  6. 打开 partners/config/paths.py:10-16:34-53,确认 partner 的数据树在 data/partners/{id}/ 而不在当前用户目录下。

需要说明的边界:本文只读了 partners 包的 docstring、channels/base.pychannels/registry.py、各渠道模块的类属性行与 config/ 下的 schema 与 paths;deeptutor/services/partners/ 下的 partner 运行时管理器、workspace 供给、凭据脱敏等实现不在本次核对范围,只从 import 与 docstring 间接引用过。partners.py 这个 router 我们也只提取了路由装饰器与挂载信息,处理函数体未逐一阅读,因此「某个端点具体做什么」多数未核实。文中出现的所有默认值都是源码中的默认配置,不构成对实际运行结果的保证。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。