Back to home@0x0101010

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
GitHub repo

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; /stop is the hard interrupt
  • Mode switching: /mode switches 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)

KeyDefaultDescription
appIdrequiredFeishu App ID (FEISHU_APP_ID)
appSecretrequiredFeishu App Secret (FEISHU_APP_SECRET)
domainhttps://open.feishu.cnAPI domain; use https://open.larksuite.com for the international version
cwdprocess.cwd()Agent working directory
allowUsers[]open_id allowlist; empty = no restriction — set this in production
allowChats[]chat_id allowlist; empty = no restriction
sessionTimeoutMs1800000Idle session recycle timeout
defaultModestandardDefault agent preset for new sessions
modeAliasessee patchMode aliases (defaults include Chinese aliases)

Security note: when allowUsers/allowChats are 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)

CommandArgsBehavior
/newDispose 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.
/modeList 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 aliasSwitch 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.
/statusCurrent model (provider/model), active mode, chat id, agent status (running/idle), minutes since last activity, idle-recycle countdown, pending-approval indicator.
/stopHard-interrupt the running turn: cancels in-flight work and clears any queued inputs. If nothing is running, replies "agent idle".
/helpBuilt-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:

CommandBehavior
/permissionShow 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)
/planToggle plan mode: agent plans first, executes after you approve
/compactCompress session history to reclaim context
/goalSet/view a long-running task goal
/exportExport the session log
/feedbackRecord 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 /stop to 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):

  1. POST /callback/ws/endpoint (body carries AppID/AppSecret) returns a WS URL + PingInterval
  2. Binary frames = protobuf Frame: 1=SeqID 2=LogID 3=service 4=method(0 control/1 data) 5=headers 8=payload
  3. Heartbeat: send method=0 + headers[{type:ping}] every PingInterval
  4. Data frames have header type=event; payload is event JSON (core: im.message.receive_v1)
  5. 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