dsh-feishu
Feishu (Lark) long-connection channel plugin for DeepSeek Harness (dsh) — zero npm dependencies
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 19, 2026
- Updated
- Aug 19, 2026
Introduction
dsh-feishu
中文 | English
A Feishu (Lark) long-connection channel plugin for DeepSeek Harness (dsh): bridges a dsh agent to a Feishu bot, so you can dispatch tasks, receive results, and approve permissions right in chat.
Zero npm dependencies — built on Node >= 22's native WebSocket plus a hand-written protobuf Frame codec (field numbers match the official oapi-sdk-go WS module). The whole plugin is a single index.js.
Features
- WebSocket long connection: uses Feishu's official WebSocket callback (
callback/ws/endpoint) — no public IP, no port forwarding, no callback server needed - One agent per chat: each conversation gets an isolated context; idle sessions are recycled automatically (default 30 min)
- Approvals as interactive cards: dsh approval requests become Feishu card buttons (✅ allow / ❌ deny); click or reply
y/n; the card updates in place once handled - Mid-turn steering: send a message while a task is running and it's injected into the current turn at the next step boundary — no interruption;
/stopis the hard interrupt - Mode switching:
/modeswitches agent presets (minimal / standard / PTC / creative), orthogonal to permission presets - Command bridge: all dsh built-in commands (
/permission/plan/compact/goal/export…) work in chat - Auto-reconnect (5s after close), heartbeat keepalive, event ack (prevents Feishu redelivery), seq-based dedup after resume
Requirements
- Node >= 22 (native WebSocket; developed on 23)
- A working dsh deployment (
npx @deepseek-ai/dsh) - A Feishu Open Platform app (a self-built enterprise app is fine) with:
- Events & Callbacks → long-connection (WebSocket) mode
- Bot capability enabled
- Scopes:
im:message(send/receive), etc.
Install
npx @deepseek-ai/dsh plugin --profile feishu add /path/to/dsh-feishu
Credentials never go into config files — they come from environment variables (put them in the profile's .env, or global ~/.dsh/.env):
FEISHU_APP_ID=cli_xxxxxxxx
FEISHU_APP_SECRET=xxxxxxxx
# optional: agent working directory
DSH_FEISHU_CWD=/your/workspace
Verify the composed config and start:
npx @deepseek-ai/dsh --profile feishu --dump-config # verify
npx @deepseek-ai/dsh --profile feishu # start
Configuration (cordis.patch.yml)
| Key | Default | Description |
|---|---|---|
appId | required | Feishu App ID (FEISHU_APP_ID) |
appSecret | required | Feishu App Secret (FEISHU_APP_SECRET) |
domain | https://open.feishu.cn | API domain; use https://open.larksuite.com for the international version |
cwd | process.cwd() | Agent working directory |
allowUsers | [] | open_id allowlist; empty = no restriction — set this in production |
allowChats | [] | chat_id allowlist; empty = no restriction |
sessionTimeoutMs | 1800000 | Idle session recycle timeout |
defaultMode | standard | Default agent preset for new sessions |
modeAliases | see patch | Mode aliases (defaults include Chinese aliases) |
Security note: when
allowUsers/allowChatsare empty, anyone who can talk to the bot can drive your agent (executing tasks on your machine). Configure them before sharing publicly.
Slash commands
Native commands (handled by this plugin)
| Command | Args | Behavior |
|---|---|---|
/new | — | Dispose the current session: context cleared, agent destroyed, next message gets a brand-new agent. Use after /mode on a session with history, or to start a fresh task. |
/mode | — | List all agent presets with descriptions and mark the current one (✅). Also shows whether any preset is broken (e.g. code needs a host runtime not present in the base bundle). |
/mode <preset> | id or alias | Switch the chat's agent preset: minimal (极简) / standard (标准) / code (ptc) / cordis (创造). Blank session → takes effect immediately. Session with history → the choice is recorded (📌) and applies after /new (presets lock at agent creation). Unknown names list the valid ids. |
/status | — | Current model (provider/model), active mode, chat id, agent status (running/idle), minutes since last activity, idle-recycle countdown, pending-approval indicator. |
/stop | — | Hard-interrupt the running turn: cancels in-flight work and clears any queued inputs. If nothing is running, replies "agent idle". |
/help | — | Built-in help text. |
Approval answers (not slash commands)
When an approval card is pending, a bare y = allow, n = deny (case-insensitive). Clicking the card buttons does the same; the card is updated in place either way.
Bridged dsh commands
Everything else starting with / is forwarded to dsh's command service, so all built-in commands work in chat. Common ones:
| Command | Behavior |
|---|---|
/permission | Show the current permission preset |
/permission <preset> | Switch: read-only (files read-only, no approval needed) / workspace-write (workspace writable, out-of-bounds needs approval — default) / danger-full-access (full access, no approval) |
/plan | Toggle plan mode: agent plans first, executes after you approve |
/compact | Compress session history to reclaim context |
/goal | Set/view a long-running task goal |
/export | Export the session log |
/feedback | Record feedback |
Unknown commands reply with the full list of what's available.
Plain messages (no slash)
- Task running → the message is steered into the current turn: acknowledged with
📌, takes effect at the next step boundary, task keeps running. Use/stopto abort instead. - Agent idle → starts a new turn as usual.
In group chats, @mention the bot to trigger any of the above.
Development
node test.mjs # offline unit tests: protobuf codec roundtrip + config validation
node debug.mjs # debug frame encoding bytes
test.mjs extracts the pure-function blocks from index.js and roundtrips them — no dsh startup, no real credentials needed.
Protocol notes
Feishu WebSocket long-connection protocol (from larksuite/oapi-sdk-go v3 ws module):
POST /callback/ws/endpoint(body carries AppID/AppSecret) returns a WS URL + PingInterval- Binary frames = protobuf Frame:
1=SeqID 2=LogID 3=service 4=method(0 control/1 data) 5=headers 8=payload - Heartbeat: send
method=0 + headers[{type:ping}]every PingInterval - Data frames have header
type=event; payload is event JSON (core:im.message.receive_v1) - Every event must be acked (original headers +
biz_rt+{"code":200}) or Feishu will redeliver
The codec is hand-written at the top of index.js (encodeFrame/decodeFrame) — no protobufjs dependency.
License
MIT