安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add github:wz-heng/dsh-feishu-bridge
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
截图
README
English | 中文
SDK 金丝雀每天对 deepseek-harness-sdk、lark-channel-sdk 的最新版本(而非本仓库锁定的版本)跑一遍全套测试,上游一旦引入破坏性变更,一天内就能发现。
DeepSeek Harness(dsh)的飞书(Lark)channel 桥:给飞书机器人发消息,触发一次 dsh agent turn,回复自动发回该聊天。
这是一个独立的社区项目,不由 DeepSeek 官方构建、维护或背书。 它完全通过 dsh 的公开 Python SDK(deepseek-harness-sdk)驱动——子进程边界,没有 fork/patch harness 本身的代码。
这是什么
- 一套生产级飞书机器人桥:fail-closed 白名单、一次性卡片 nonce、per-chat 输出详略、sticky session、
ws/webhook双 transport。 - 与
deepseek-harness-sdk对话的薄适配层集中在一个文件src/dsh_feishu_bridge/dsh_adapter.py,SDK 版本精确锁定——harness 目前是 v0.1 developer preview,版本间明示会有破坏性变更。
截图






5 分钟快速上手
git clone https://github.com/wz-heng/dsh-feishu-bridge.git
cd dsh-feishu-bridge
python3.12 -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
凭据一律走环境变量——绝不写进任何要提交的文件:
export DEEPSEEK_API_KEY=sk-your-key-here
# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1 # 仅在走代理时需要
export FEISHU_APP_ID=cli_xxxxxxxx
export FEISHU_APP_SECRET=xxxxxxxx
export FEISHU_TRANSPORT=ws # 或 "webhook"(需要一个公网可达的 URL)
# export FEISHU_VERIFICATION_TOKEN=xxxx # FEISHU_TRANSPORT=webhook 时必填
# export FEISHU_ENCRYPT_KEY=xxxx # FEISHU_TRANSPORT=webhook 时必填
# fail-closed 白名单——必填。不配置任何 id 时机器人对谁都不回复;
# 每条消息都会被拒绝,这是设计如此(见下方"安全姿态")。
export FEISHU_ALLOWED_OPEN_IDS=ou_xxxxxxxxxxxxxxxx
# export FEISHU_ALLOWED_CHAT_IDS=oc_xxxxxxxxxxxxxxxx # 可选的群聊白名单
运行:
python -m dsh_feishu_bridge
# 或: dsh-feishu-bridge
在飞书里给机器人发消息——在私聊里发 /pair <码>,用 bridge 启动时打印在控制台的一次性配对码(见下方"获取你的 open_id")。码对了,你的 open_id 立刻加入白名单,不用重启。不在白名单里的 open_id 发来的其他任何消息都会被静默拒绝。
获取你的 open_id
bridge 一启动就会在自己的控制台打印一个一次性配对码:
[dsh-feishu-bridge] Pairing code: 7K9XQPRT
Send '/pair 7K9XQPRT' to the bot in a PRIVATE chat to get on the allowlist. Valid for 900s or 5 wrong tries, whichever comes first.
给机器人发消息时要用私聊(1:1),不要用群聊——/pair <码> 在群聊里一律不受理,这样码就永远不用经过群聊。码对了,你的 open_id 立刻加入白名单(不用重启),并持久化到 FEISHU_PAIRING_STATE_PATH(默认 data/feishu_paired_open_ids.json,一个 {"open_ids": [...]} 文件),下次重启也还在。每个进程一个码、一次成功配对——想要新码就重启 bridge;超过 FEISHU_PAIRING_MAX_ATTEMPTS(默认 5)次错误尝试,或超过 FEISHU_PAIRING_TTL_SECONDS(默认 900 秒/15 分钟)有效期,这一轮配对会提前失效。想手动管理白名单,用 FEISHU_PAIRING=0 整体关掉这个功能。
备选方案:捞日志。 关闭配对功能后(或者你就是不想用它),给机器人发一条消息(它不会回复——这是预期行为,fail-closed),然后在服务端日志里找这样一行:
Feishu: rejecting message from unauthorized open_id=ou_xxxxxxxxxxxxxxxx (chat=oc_xxxx)
把这个 open_id 填进 FEISHU_ALLOWED_OPEN_IDS,重启即可。
撤销权限。 从 env 加白的 id,把它从 FEISHU_ALLOWED_OPEN_IDS 里删掉再重启即可撤销。通过 /pair 加白的 id 是独立存储的——从 FEISHU_PAIRING_STATE_PATH 的 open_ids 列表里删掉那一项(或直接删文件),再重启。
作为 dsh 插件安装
除了上面独立运行的方式,dsh plugin add 也可以把本仓库装进某个 dsh profile:插件是一层薄的 Node/cordis 壳(package.json、cordis.patch.yml、lib/),负责拉起并管理同一个未经改动的 Python 进程——不重实现、也不 patch 任何桥逻辑。
两步,顺序不能反——插件不会替你装任何 Python 依赖:
先按上面"快速上手"把 Python 侧装好:
git clone https://github.com/wz-heng/dsh-feishu-bridge.git cd dsh-feishu-bridge python3.12 -m venv .venv . .venv/bin/activate pip install -e .配置
FEISHU_APP_ID/FEISHU_APP_SECRET/FEISHU_ALLOWED_OPEN_IDS等——可以 export 到启动dsh的 shell 里,也可以写进本仓库根目录的.env文件(每行KEY=value;插件会直接读取它并合并进被拉起进程继承的环境变量,因为 Python 侧本身只读os.environ)。再把插件加进 profile:
dsh plugin --profile <name> add /path/to/dsh-feishu-bridge之后该 profile 每次启动,
dsh都会把这个桥当作受管子进程拉起:它会执行<repo>/.venv/bin/python -m dsh_feishu_bridge(仓库根目录没有.venv时回退到PATH上的python3),等待GET /health返回{"status": "ok"},并在 profile/插件 dispose 时发送SIGTERM,若 5 秒内未退出则升级为SIGKILL——和手动Ctrl-C独立进程时的干净退出行为一致,只是自动化了。每个 config 字段都是可选的(
host、port、pythonBin、startupTimeoutMs、env)——只要第一步做好了,且默认值(0.0.0.0:8788、仓库根.venv)符合你的环境,裸add就能直接工作。host/port会写入被拉起进程的DSH_FEISHU_BRIDGE_HOST/DSH_FEISHU_BRIDGE_PORT(见下方"配置参考")——它们真的会改变 Python 侧实际监听的地址,插件自己的健康检查也跟着同一个值走,两者不会不一致。如需覆盖,在你自己 profile 的cordis.patch.yml里改同一个 id,例如换解释器和端口:- insert: - id: feishu-bridge name: dsh-feishu-bridge config: pythonBin: /usr/local/bin/python3.12 port: 8799
这层壳是 v1:无构建步骤(lib/ 下是纯 ESM)、零 npm 依赖,且不会自动引导 Python 环境——目前已收录的、包装外部进程的 dsh 插件里没有这么做的先例,本仓库也就不自创一个。壳自己的测试在 tests-node/ 下(node --test tests-node/**/*.test.mjs),与 tests/ 下的 Python 测试套件相互独立。
命令
| 命令 | 作用 |
|---|---|
/new [名称] |
开始一个新会话 |
/sessions |
列出会话(点击切换) |
/switch <id> |
切换到某个已有会话 |
/current |
查看当前会话信息 |
/quiet |
只显示回复(默认) |
/verbose |
同时显示状态/结果行 |
/help |
列出命令 |
远程工具审批
给 agent 的工具执行加一道人工闸门:模型想跑的命令,没人在飞书上点头就不会执行。
设置 DSH_APPROVAL_MODE=1 开启后,agent 每次调用 bash 都会阻塞,直到有人在飞书上给会话属主聊天推送的卡片上点 同意 或 拒绝,并且超时 fail-closed(DSH_APPROVAL_TIMEOUT_SECONDS,默认 60 秒——卡片超时没人处理是拒绝,绝不会默认放行)。默认关闭,不影响现有部署。
这个能力不需要(也不会组合)沙盒化的 bash 执行器——审批模式是对工具执行的人工核准闸门,和文件系统隔离是两回事。如果两者都要,按你原本不开审批模式时的做法把 DSH_WORKSPACE 指向一次性目录/容器即可(见下方"安全姿态")。
实现上:审批模式会换用一份内置的 Cordis composition(src/dsh_feishu_bridge/approval_runtime/cordis.yml),把 bash 调用标记为需要审批,并通过一条仅回环(loopback-only)的 HTTP 回调把决策转发回本 bridge——绝不经过公网的 webhook/健康检查端口,也不会被这台机器以外的任何人触达。完整设计、以及为什么这条路今天走不通 dsh SDK 自己的 JSON-RPC 通道,见 docs/architecture.md "Remote tool approval" 一节。
配置参考
全部走环境变量。可选的 YAML 文件(路径通过 DSH_FEISHU_BRIDGE_CONFIG 或 --config 指定)可以配置非敏感项(白名单、model、provider)——见 examples/config.example.yaml。两者都设置时环境变量优先,凭据故意不从 YAML 文件读取。
| 环境变量 | 默认值 | 含义 |
|---|---|---|
DEEPSEEK_API_KEY |
— | 必填。和 SDK 自身读取的变量同名。 |
DEEPSEEK_BASE_URL |
— | 可选,用于 OpenAI 兼容代理。 |
DSH_PROVIDER |
deepseek-official |
Provider 路由(见 SDK 文档)。 |
DSH_MODEL |
deepseek-v4-flash |
模型 id。 |
DSH_MAX_TOKENS |
未设置 | 可选的单请求输出上限。 |
DSH_CORDIS |
未设置 | 自定义 Cordis composition 路径;不填则用内置默认。和 DSH_APPROVAL_MODE 互斥(该模式自带一份 composition——见"远程工具审批")。 |
DSH_SESSION_ROOT |
未设置 | runtime 写 JSONL session 日志的目录。 |
DSH_WORKSPACE |
当前目录 | agent 工具操作的工作区。 |
DSH_APPROVAL_MODE |
0 |
设为 1/true/yes/on 要求每次 bash 调用前先在飞书上点 同意/拒绝——见"远程工具审批"。 |
DSH_APPROVAL_TIMEOUT_SECONDS |
60 |
一张待处理审批卡等待多久后自动拒绝(fail-closed)。 |
FEISHU_APP_ID / FEISHU_APP_SECRET |
— | 必须同时配置,或都不配置。 |
FEISHU_TRANSPORT |
ws |
ws(无需公网 URL)或 webhook。 |
FEISHU_VERIFICATION_TOKEN |
— | FEISHU_TRANSPORT=webhook 时必填。 |
FEISHU_ENCRYPT_KEY |
未设置 | FEISHU_TRANSPORT=webhook 时必填——在飞书开发者后台为该事件订阅开启 "Encrypt Key" 并填入同样的值。用于校验每个请求的 X-Lark-Signature(见"安全姿态")。 |
FEISHU_DOMAIN |
https://open.feishu.cn |
Lark 国际版或走代理时修改。 |
FEISHU_ALLOWED_OPEN_IDS |
(空) | 逗号分隔。必填——为空则无人被授权。 |
FEISHU_ALLOWED_CHAT_IDS |
(空 = 不限制) | 逗号分隔的群聊白名单。 |
FEISHU_PAIRING |
1 |
0/false 整体关闭一次性配对码 onboarding——见"获取你的 open_id"。 |
FEISHU_PAIRING_TTL_SECONDS |
900 |
配对码从 bridge 启动起算的有效期。 |
FEISHU_PAIRING_MAX_ATTEMPTS |
5 |
锁定这一轮配对之前允许的错误尝试次数。 |
FEISHU_PAIRING_CODE_LENGTH |
8 |
配对码长度;至少 8(否则启动失败)。 |
FEISHU_PAIRING_STATE_PATH |
data/feishu_paired_open_ids.json |
已配对 open_id 的持久化位置({"open_ids": [...]})——从不包含 env 白名单,所以从 FEISHU_ALLOWED_OPEN_IDS 删掉一个 id 重启后依然生效撤销。 |
DSH_FEISHU_BRIDGE_HOST |
0.0.0.0 |
HTTP 服务绑定地址(健康检查 + webhook 路由)。 |
DSH_FEISHU_BRIDGE_PORT |
8788 |
HTTP 服务端口。 |
安全姿态
- 默认 fail-closed。 不配置
FEISHU_ALLOWED_OPEN_IDS意味着所有发送者都被拒绝——没有隐式的"允许所有人"。这是刻意设计:一个白名单为空的 agent 桥如果默认放行,会让租户里任何人都能触发任意 agent turn。 - webhook 模式必须同时配置 verification token 和 encrypt key。 缺任一项,webhook 路由根本不会注册——进程宁可拒绝以半配置状态启动,也不会默默接受未经验证的事件。encrypt key 不是可选项:verification token 只是请求体里携带的一个静态值,不是逐请求的签名,单靠它无法证明请求真的来自飞书。
- 每个 webhook 请求都在本 bridge 自己的边界上做签名、时间戳、重放校验——校验先于任何下游 SDK 处理。
X-Lark-Signature会按sha256(timestamp + nonce + encrypt_key + body)校验;timestamp 必须落在距"现在"5 分钟的窗口内;同一个(timestamp, nonce)组合如果已经出现过,会被当作重放拒绝。任一校验失败都直接返回401,请求不会到达消息处理逻辑。唯一刻意放行的例外是飞书控制台"保存请求网址"这一步的握手请求:此时订阅尚未确认,飞书根本不会为它签名,因此本 bridge 只校验FEISHU_VERIFICATION_TOKEN并直接回显 challenge——这与底层 SDK 原本就会做的那次(已强制要求的)校验完全等价。 - 卡片按钮(会话切换、工具审批)使用一次性、绑定身份的 nonce。 nonce 铸造时精确绑定某个 action + session(审批卡片还额外绑定具体的工具调用);二次点击、重放的 nonce、被篡改的卡片 value 都会被拒绝且不生效。
- 会话归创建它的聊天所有。
/sessions只列出(/switch也只接受)发起请求的聊天自己拥有的会话——即便两个聊天都在白名单里,也不能列出或劫持另一个聊天的会话 id 来偷看它的回复。工具审批的决策同样在服务端做这一层归属校验,不是只靠 nonce 的作用域。 - 审批模式的回调服务器只监听回环地址。 它绑定
127.0.0.1上一个独立的临时端口,和对外的 webhook/健康检查端口分开;这个地址只会写进 harness 子进程自己的环境变量,不会暴露给任何远端能触达的地方。 - 配对码只存在于控制台 stdout 上,和
.env已经在的同一个信任边界——能读控制台的人本来就能直接改白名单。它在启动时打印一次,之后再也不会出现在任何日志行里。校验用常数时间比较,码一次性使用、会过期(FEISHU_PAIRING_TTL_SECONDS)、错够FEISHU_PAIRING_MAX_ATTEMPTS次会锁定;/pair在群聊里一律静默不受理,陌生人从它这能得到的回复,除了"错了"或"现在不可用",不会透露任何更多信息。 - 请以 composition 实际需要的最小权限运行本 bridge 进程。内置默认的
dshcomposition(examples/jsonrpc-agent上游)用的是danger-full-access的 bash——请在一次性工作区/容器里跑,不要对着你在意的机器跑——不论是否同时开启审批模式(这是两个互相独立的控制手段,见"远程工具审批")。
限制(v1,刻意为之)
这些是由 deepseek-harness-sdk v0.1 当前实际能力决定的范围收窄,在此明确写出而非静默缺失:
- 不支持增量流式输出。
DeepSeekHarness.run()是同步调用,阻塞到该 turn idle 才返回;SDK 的on_notification钩子能在调用过程中拿到原始协议 notification,但其事件 schema 不属于 v0.1 的既定文档契约。所以 bridge 在 turn 开始时发一条状态行,turn 结束后发完整回复——不是某些桥那种逐 token 流式。 - 会话仅在单个 bridge 进程内 sticky。 重启会起一个全新的
DeepSeekHarness子进程;通过共享session_root跨重启恢复并不是 SDK v0.1 文档承诺的行为,所以本桥不会在其之上搭建未经证实的持久化。聊天的 sticky session 指针和它的/quiet//verbose偏好都会在重启后重置。 - 仅支持文本消息——不支持语音/图片/文件附件,也不支持话题/子话题回复(一个聊天只有一个 sticky session,跨话题共享会悄悄串台)。
- 每个 bridge 进程只有一份模型配置——provider/model/cordis composition 是进程级的,不是按聊天区分的。没有
/agent式的重新绑定命令;如果需要第二份配置,跑第二个 bridge 进程(不同端口、不同飞书 app 或白名单)。 - 同一时间只有一个配对码,且只能自己配对自己。
/pair只能把发送者自己的open_id加白——没有代人配对的办法,每个进程也永远只有一个码活着。第二个人要加入,得等(或者由运维触发)一次重启拿新码。
开发
pip install -e ".[dev]"
pytest # 快——不联网、不起子进程、不烧 API 配额
pytest -m real_sdk # 真机冒烟测试:需要 DEEPSEEK_API_KEY + runtime;否则自动跳过
测试套件把两端都 fake 掉了:一个可编排的 DshBackend 顶替真实 SDK(不起子进程、不烧配额),一个本地 FakeFeishuServer 顶替 open.feishu.cn,断言 bridge 实际发出的出站请求。见 tests/。
如果你的网络走代理(比如 Clash)且没有为 127.0.0.1/localhost 配置豁免,运行涉及 loopback 服务器的测试前先 export no_proxy=127.0.0.1,localhost——否则代理会吞掉 bridge 自己发往 fake server 的出站请求。bridge 本身在运行时已经对 loopback 域名强制 trust_env=False,所以这只影响测试进程。
dsh 插件壳(lib/,见上面"作为 dsh 插件安装")有自己独立的 JS 测试套件,不涉及 Python:
node --test tests-node/**/*.test.mjs
License
MIT——见 LICENSE。