dsh-feishu-gateway
English | 中文
Chat with your DeepSeek Harness (DSH) agent from Feishu (Lark).
A DSH plugin bundle that mounts a Feishu long-connection listener; every Feishu
message is routed to a stable DSH session (resumed via the agents service,
so multi-turn chats stay in the same session), and the agent's answer is
replied as a Markdown-rich post message. It also supports /new to start a
fresh session, a native Typing reaction while the answer is being produced,
live streaming progress cards for long tasks, click-to-answer cards for
permission approvals and the model's ask_user_question tool, and proactive
push.
Features
- 💬 Full conversation — Feishu private chat, or group @bot → DSH agent → reply. In groups, every topic/thread is its own independent DSH session, and all messages in that topic stay in the same session until
/new.
- 🔁 Persistent sessions — each Feishu conversation (or group topic) maps to one DSH
session (
agents.resume / agents.create); /new (or "另起会话" / "新会话" /
"重新开始" / "换个话题") starts a fresh one, including inside a group topic.
- ⌨️ Native Typing indicator — while the agent works, the bot adds a
Typing reaction to your message (like hermes-agent's Feishu gateway);
it stays until the answer is done, and is swapped for a CrossMark reaction
on failure. No more "thinking…" hint text by default.
- 🎞 Streaming progress — long tasks report continuously: a live interactive
card streams the agent's thinking, tool calls, and answer draft as they
happen (
reporting.mode: 'stream', default). Set reporting.mode: 'final'
to only receive the final result.
- 🃏 Click-to-answer cards — permission approvals (
approval/request, e.g.
sandbox escalation) and the model's ask_user_question tool render as Feishu
interactive cards: click ✅ 允许一次 / 🚫 拒绝 or an option button to answer.
The click response instantly replaces the card with a decided state (buttons
removed, result shown) plus a toast confirmation.
- ✍️ Markdown replies — plain rich-text (
post) messages with the md
tag: bold, inline code, lists and links render natively, no cards needed
- 🧩 Web-only interactive fences degrade gracefully — model-emitted
dsh-ui interactive UI fences (e.g. from dsh-genui) only render in the Web
UI; on Feishu they are auto-downgraded to a one-line readable hint (title
extracted, "view in Web UI"), never a raw JSON code block
- 🤖 Full agent capability — the DSH agent runs with its own model and
tools (bash, files, subagents…), fully autonomous
- 📨 Proactive push — optional admin HTTP API (
/api/push) to push text /
Markdown / cards to any user or group
- 🔌 No public network required — Feishu long connection, no webhook URL
- 🗂 Persistence — Feishu↔DSH session mapping survives restarts
Requirements
- DeepSeek Harness installed and built (
dsh CLI), with DEEPSEEK_API_KEY
configured (the agent's model is used as-is)
- A Feishu open-platform self-built app with the bot capability enabled
(see below)
Feishu app setup
- Feishu Open Platform → create a
self-built app.
- Enable the bot capability.
- Grant permissions:
im:message, im:message:send_as_bot (+
im:message:send_as_bot:readonly to read content). Publish a version.
- Under Events & callbacks, choose long connection and subscribe to
im.message.receive_v1 (no public URL needed). The same long
connection also delivers card button clicks (card.action.trigger,
used by the approval / Q&A cards) — no webhook URL required.
- In Feishu, search the app name and add the bot as a contact.
The Typing reaction indicator and card buttons need the bot to be able to
interact with messages in the chat (im:message). If the reaction API is
denied, the gateway automatically falls back to the hintText message.
Installation (as a DSH plugin)
Prerequisite: this package is published on npm and dsh is on your PATH.
The recommended setup mounts the gateway into the web profile: it runs in
the same process as the DSH Web UI, so starting the Web UI also starts the
Feishu gateway, and both share the same DSH agent. A standalone profile is also
supported (see "Alternative" at the end).
Option 1 (recommended): mount into the web profile
The web profile is DSH's default GUI profile (dsh --profile web).
- Edit
~/.dsh/profiles/web/package.json to add the dependency and bundle:
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {
"@kriskwok/dsh-feishu-gateway": "^0.2.0"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"@kriskwok/dsh-feishu-gateway"
]
}
}
}
- Install dependencies in the web profile directory:
cd ~/.dsh/profiles/web && pnpm install
- Edit
~/.dsh/profiles/web/cordis.patch.yml and fill in your Feishu app
credentials:
- id: feishu-gateway
config:
feishu:
appId: cli_xxxxxxxxxxxxxxxx
appSecret: xxxxxxxxxxxxxxxxxxxxxxxx
http:
port: 3100 # optional admin API
token: your-token
- Start (or restart) the web profile:
dsh --profile web
You can also use the one-shot script from this repository:
./scripts/create-profile.sh (mounts into the web profile by default;
--standalone creates a standalone feishu profile instead).
Alternative: standalone feishu profile
To run the gateway without the Web UI, use a standalone profile:
mkdir -p ~/.dsh/profiles/feishu && cd ~/.dsh/profiles/feishu
cat > package.json <<'EOF'
{
"name": "dsh-profile-feishu",
"private": true,
"dependencies": {
"@kriskwok/dsh-feishu-gateway": "^0.2.0"
},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@kriskwok/dsh-feishu-gateway"]
}
}
}
EOF
cat > pnpm-workspace.yaml <<'EOF'
packages:
- .
nodeLinker: hoisted
autoInstallPeers: false
EOF
pnpm install
# then create ~/.dsh/profiles/feishu/cordis.patch.yml with your app credentials
dsh --profile feishu
Configuration
All settings live in the feishu-gateway namespace (profile patch row or
~/.dsh/settings.yaml):
| Field | Default | Description |
|---|
feishu.appId | — | Feishu app id (required) |
feishu.appSecret | — | Feishu app secret (required) |
feishu.domain | feishu | feishu (CN) or lark (international) |
feishu.botOpenId | `` | Optional; @-detection works without it |
feishu.replyMode | at | Group policy: at (reply only when @-mentioned) or all (reply to every message). Each group topic/thread is its own DSH session. |
workspace | /root/Documents/DSH-Workspace | Agent working directory (the session is also auto-attached to the matching DSH Workspace, so it groups under that Workspace in the Web UI instead of "Ungrouped"). Both private and group-topic sessions are anchored here. |
hintText | 爸爸,我正在努力处理中…… | Fallback "processing" text (only when the Typing reaction is disabled/unavailable) |
reporting.mode | stream | stream = live streaming progress card; final = only the final answer |
reporting.typingReaction | true | Show the native Feishu Typing reaction while working |
reporting.showReasoning | true | Stream the model's reasoning in the card |
reporting.showToolCalls | true | Stream tool-call activity in the card |
reporting.patchIntervalMs | 1100 | Min interval between card patches; Feishu limits single-message updates to ~1/s (error 230020), and the stream backs off adaptively on failure |
reporting.maxBodyChars | 900 | Max rendered card body length |
reporting.failureReaction | CrossMark |
Q&A cards in the web profile: ask_user_question answers go through the
single ctx.userQuestions provider slot. The gateway never claims that slot
(stealing it makes the Web UI's apiProxy host fail to start with
DUPLICATE_PROVIDER); it wraps service.ask at the service boundary
instead: sessions owned by a Feishu conversation are answered with Feishu
cards, while every other session continues through the registered UI
provider. Permission-approval cards work from Feishu in every setup. In a
standalone feishu profile, both ask_user_question and approvals are
answered from Feishu cards.
Session coexistence & self-healing
- Preset composition (tools!) — in preset-roster deployments (e.g. the web
profile), Feishu agents are composed from the deployment's agent preset
(
meta.agentPreset + preset mount), so the model gets its tools instead of
treating tool calls as plain text.
- Web UI coexistence — sessions have a single live owner. When the Web UI
opens a session, the Feishu side takes over the running agent via
agents.get() and drives the same session instead of failing with
"while it is live" / "already exists"; both surfaces share one conversation.
- Wedged-session self-heal — if a crashed process leaves a session
permanently conflicted ("already exists"), the gateway mints a fresh session
id, re-points the Feishu conversation at it, and continues chatting.
Admin HTTP API (optional)
Enable by setting http.port. Endpoints:
GET /health — status
POST /api/push — proactive push
{ "receive_id": "ou_xxx", "receive_id_type": "open_id", "msg_type": "text", "content": "{\"text\":\"hi\"}" }
GET /api/sessions — Feishu↔DSH session mapping overview
Development
pnpm install
pnpm build # tsc → lib/
pnpm test # offline self-tests
Note: @deepseek-ai/* packages are provided by the DSH host at runtime; for
local type-checking they are symlinked from your deepseek-harness checkout
(see the publish checklist).
License
MIT