Fail-closed 的飞书(Lark)channel 桥:和机器人聊天即驱动 dsh agent turn。仅经官方 Python SDK 集成(精确锁版);白名单默认全拒、webhook 验签/时间窗/防重放、按 chat 粘性会话;中英双语文档。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:wz-heng/dsh-feishu-bridge
GitHub 来源的插件在安装时会在你的机器上执行构建脚本。请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
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),从另一个 agent harness 项目的成熟飞书桥移植而来,改接到dsh。 - 与
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
在飞书里给机器人发消息。未在白名单里的 open_id 发来的第一条消息会被静默拒绝并记录日志——这条日志就是你查到自己 open_id 的方式(见下方"获取你的 open_id")。
获取你的 open_id
先给机器人发一条消息(它不会回复——这是预期行为,fail-closed)。在服务端日志里找这样一行:
Feishu: rejecting message from unauthorized open_id=ou_xxxxxxxxxxxxxxxx (chat=oc_xxxx)
把这个 open_id 填进 FEISHU_ALLOWED_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 |
列出命令 |
配置参考
全部走环境变量。可选的 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_SESSION_ROOT |
未设置 | runtime 写 JSONL session 日志的目录。 |
DSH_WORKSPACE |
当前目录 | agent 工具操作的工作区。 |
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 |
(空 = 不限制) | 逗号分隔的群聊白名单。 |
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 来偷看它的回复。 - 用这个 bridge 的进程时用composition 实际需要的最小权限运行。内置默认的
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 流式。 - 不支持工具审批流程。 内置示例
dshcomposition 跑 Bash/编辑器工具时没有交互式审批提示,SDK 的高层SessionAPI 也没有暴露"服务端发起审批请求"的钩子(即便未来某个 composition 加了这个能力,也只有底层HarnessClient.next_request()/respond()才能接住)。批准/拒绝卡片的机制已移植并有单测覆盖(接口对等),但目前没有任何 session backend 会触发它。 - 会话仅在单个 bridge 进程内 sticky。 重启会起一个全新的
DeepSeekHarness子进程;通过共享session_root跨重启恢复并不是 SDK v0.1 文档承诺的行为,所以本桥不会在其之上搭建未经证实的持久化。聊天的 sticky session 指针和它的/quiet//verbose偏好都会在重启后重置。 - 仅支持文本消息——不支持语音/图片/文件附件,也不支持话题/子话题回复(一个聊天只有一个 sticky session,跨话题共享会悄悄串台)。
- 每个 bridge 进程只有一份模型配置——provider/model/cordis composition 是进程级的,不是按聊天区分的。没有
/agent式的重新绑定命令;如果需要第二份配置,跑第二个 bridge 进程(不同端口、不同飞书 app 或白名单)。
开发
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。
链接
同类插件
omdsh-dev/dsh-open-in-vscode★ 40
从 Web GUI 一键在 VS Code 中打开工作区目录。
omdsh-dev/dsh-notification★ 38
回合完成桌面通知,按结果分控 + 关键词过滤。
whyihaveyou/dsh-suite#plugin-notify★ 15
回合完成、错误或待审批时推送 IM webhook(飞书/企微/钉钉/Slack/Discord/自定义)与本地通知。
bobleer/dsh-acp-for-bitfun★ 9
BitFun 与 DSH 的 ACP 交互对接。
bill9109/dsh-web-ui-notify★ 9
桌面通知提醒。
openma-ai/deepseek-harness-acp★ 6
ACP profile 插件与独立 stdio server,可从 Zed 等 ACP 客户端使用完整 DSH agent,并共享 DSH 凭据与会话。