dsh-feishu
Drive an existing dsh (DeepSeek Harness) session from Feishu/Lark on your phone — dispatch work, watch progress live, answer questions, get the results. Outbound-only WebSocket: no open ports, no tunnels.
Requires dsh >= 0.1.5-rc.1 — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the latest/next dist-tags at runtime). The alpha line is no longer supported.
✨ Highlights
- Live round cards: one card per LLM round-trip — current state (🤔 thinking / 🔧 tool / ⏳ subagent), tool calls, and a growing tail of the in-flight message, refreshed every 5 seconds (pseudo-streaming)
- Quick actions on the round card: ⛔ 停止 while a turn runs, ▶️ 继续 once it ends — one tap instead of typing
- Approval cards: when the host's approval waterfall asks for a sandbox escalation, the phone gets a ✅ 允许一次 / ❌ 拒绝 card — an unattended run no longer stalls at the desk (the session's approval policy must be
ask; expiry fails closed) - Interactive ask-user cards: when the agent calls
ask_user_question, your phone gets an interactive card (dropdown / multi-select / text input + submit); the answer flows straight back. Pair it with ask-router for both desktop and phone prompting — first answer wins - Group chats: pull the bot into a Feishu group — @-mention it to dispatch work or run commands (allowlisted members only, silent to everyone else); in the group the bot is actively driving, images flow without a mention
- Image dispatch: send a picture in DM — it is downloaded, media-type sniffed, committed as a durable attachment and injected into the session as an image block (the session's model route must accept image input)
- Background push (
backgroundPush): completion cards for sessions the phone is not bound to — cron deliveries and subagent settlements (modecron), or every finished turn (modeall); off by default, the bot never messages unprompted unless told to - Interactive /model: pick a model on the phone, grouped by provider — bot-created sessions switch live
- Interactive pickers for the desktop selectors:
/think(reasoning effort),/permission(permission preset),/select-skill(skill activation) and/profile-switch(model profiles) render as one-tap selection cards on the phone - Interactive /resume: pick a session from the card (dropdown + enter), or just reply
/resume N - One-tap corrupt-log repair: if /resume hits a damaged session log (historical double-writer writes), the bot offers a repair card — it rebuilds the log in place (the original is kept as a backup) and re-enters the session
/newstarts a fresh session that inherits the previous one's working directory, model and reasoning effort- Phone dispatch: messages sent mid-turn default to steer (they join the running turn — course corrections land immediately)
- Remote stop:
/stopaborts anytime; non-allowlisted users are completely invisible to the bot - By-the-way questions:
/btwfires a side question alongside the running task and streams the answer into its own card — the main line never notices (parity with dsh-tui-pi's/btw, duplicated not shared)
🎬 Demo
/new starts a fresh session; dispatch work right from the phone:
https://github.com/user-attachments/assets/177e8839-523b-487e-b3d1-6d725cd8aba5
/resume interactive session picker + answering an ask-user card:
https://github.com/user-attachments/assets/c0d7092f-deda-4443-b75a-2bc93bd30d86
🚀 Install & Configure
Step 1: Create the Feishu app (web, ≈10 min)
Sign in at open.feishu.cn → create a Custom App (企业自建应用):
- Note the
App ID(starts withcli_) andApp Secret - "Add app capability" → Bot
- "Events & callbacks" → subscription mode Long connection; add events:
im.message.receive_v1(messages) andcard.action.trigger(card interactions — required by the ask cards and the /resume picker) - Permissions:
im:message:send_as_bot,im:message.p2p_msg:readonly,im:message.group_msg:readonly(group dispatch),im:message.resources:readonly(image download),im:message.reactions:write - Availability: add yourself → create a version and publish (events don't flow until you publish — the most common stumbling block)
Step 2: Install the plugin into your profile (≈2 min)
git clone git@github.com:fan56/dsh-feishu.git ~/github/dsh-feishu
cd ~/github/dsh-feishu && npm install && npm run link-closure
Edit ~/.dsh/profiles/<your-profile>/package.json:
{
"dsh": { "profile": { "bundles": [
// …existing bundles…
"@aiwayds/dsh-feishu" // ← add
]}},
"dependencies": {
// …existing deps…
"@aiwayds/dsh-feishu": "link:/path/to/dsh-feishu" // ← add
}
}
cd ~/.dsh/profiles/<your-profile> && pnpm install
Step 3: Credentials (≈1 min)
# ~/.dsh/.credentials.yaml (chmod 600; restart dsh after changing)
dsh-feishu-app-id: cli_xxxxxxxxxx
dsh-feishu-app-secret: xxxxxxxxxxxxxxxx
Step 4: Allowlist (≈1 min)
Only allowlisted Feishu users can use the bot — everyone else is invisible:
# ~/.dsh/cordis.patch.yml
- id: dsh-feishu
config:
operators:
- ou_xxxxxxxxxxxxxx # your open_id (admin console → member details)
Step 5: Recommended — add ask-router (multi-surface prompting)
npm install -g @aiwayds/dsh-ask-router
Add @aiwayds/dsh-ask-router to bundles, after dsh-base and before any UI
bundle. With it: phone cards and the desktop TUI panel prompt
simultaneously — first answer wins. Without it things still work — the
phone owns prompting when no other UI is present, otherwise the desktop UI
takes it.
Start & verify
dsh --profile <your-profile>
# the log line dsh-feishu: armed (1 operator(s), feishu) means success
DM the bot /help → you get the command list; /resume lists sessions; send
text to dispatch work.
🗑️ Uninstall
Remove the plugin from a profile:
dsh plugin --profile <name> remove @aiwayds/dsh-feishu
The host reconciles the profile automatically: the dsh.profile.bundles entry is spliced and the patch layer (the dsh-feishu insert with its config) is dropped.
What stays on disk (kept on purpose — deleting data is destructive; a reinstall reuses it):
~/.dsh/settings.yamldsh-feishu:section — bound session id, picker style, phone-model preference. Delete the section to reset the pairing.- Repair artifacts inside session dirs:
*.corrupt-bak*is the only pre-repair copy of a damaged session log — keep it;*.repaired.*is the rewritten log the repair produced. /tmp/dsh-feishu-bot.lockcan linger after a SIGKILL; the stale-pid check steals it on the next start, so no manual step is needed.
Plugin unload (reload, disable, process exit) settles pending phone-side flows: unanswered ask/approval/selection cards are patched to a terminal state and their host-side callers fail fast instead of hanging.
📱 Usage
| Command | What it does |
|---|---|
/resume |
Interactive session picker card (dropdown + enter; or reply /resume N), sorted by last update |
/new |
Start a brand-new session and bind to it (inherits cwd, model and reasoning effort) |
/stop |
Abort the running turn (queued messages survive) |
/btw <question> |
By-the-way side question while the main task runs: one tool-less model call over a recent-conversation snapshot, streamed into its own card — the main line never notices. Not kept in the session; idle main line refuses; --model provider/model overrides the route; bare /btw re-sends the last exchange (btwContextMessages config sizes the snapshot) |
/status |
Binding and run status |
/sub N |
Inspect the Nth subagent |
/model |
Interactive model picker (two steps: pick a provider → pick one of its models); live-switches bot-created sessions, otherwise saved as the phone default (applies to /new) |
/think |
Interactive reasoning-effort picker for the current model (adapter-provided efforts + provider default); live-switches bot-created sessions, otherwise saved as the phone default |
/permission |
Interactive permission-preset picker; the pick is replayed as /permission <name> through the dsh command registry |
/select-skill |
Interactive skill picker (user-invocable skills of the bound workspace); activation rides dsh's native /name skill gesture |
/profile-switch |
Interactive model-profile switcher over $DSH_HOME/model-profiles.json; applies the profile's provider/model/effort (agent frontmatter updates remain desktop-only) |
/feishu-plugin think on|off |
Toggle the reasoning tail in the activity section (default on) |
/settings /preset /theme /reload /hotkeys /model-sync /export /agents /subagents /profile-cfg /login /logout /skills |
Provided by the desktop dsh-tui-pi plugin (interactive panels) — the phone refuses them with a desktop pointer (and a phone-side stand-in hint where one exists, e.g. /skills → /select-skill) |
/goal /dcp |
Exist in the dsh runtime but not yet adapted — refused with a desktop pointer |
/session |
Mirrored to /status |
| Any image message | Downloaded and injected into the bound session as an image block (DM: directly; group: only while that group is the active dispatch surface). Requires the model route to accept image input |
| Any other text | Injected as a prompt into the bound session (steered into the running turn when one is live) |
Group usage: add the bot to a Feishu group, then @-mention it — @dsh 帮我跑一下测试 dispatches exactly like a DM; commands (/resume, /stop, …) work the same way after a mention. Only allowlisted members ever trigger the bot; everyone else is invisible. Cards land in the group while the dispatches keep coming from there; the binding itself stays the bot's single global one (one session at a time, whichever chat drove it last).
Typical flow:
Session running on your desktop → open Feishu on the train → /resume and pick it
→ keep going from the phone (auto-steer) → answer ask cards with a tap → /stop anytime
⚙️ Configuration (config: block)
| key | default | description |
|---|---|---|
operators |
[] |
open_id allowlist — required to arm the bot |
mode |
"on" |
"off" disables the plugin entirely |
domain |
"feishu" |
"feishu" (CN) or "lark" (international) |
statusIntervalMs |
5000 |
round-card refresh beat (pseudo-streaming), range [5000, 600000] |
bodySegmentChars |
3500 |
long-body segmentation threshold |
resumeListStyle |
"auto" |
/resume list: auto/table/list |
backgroundPush |
"off" |
Completion push for sessions the phone is not bound to, into the last active chat: off / cron (turns carrying a cron delivery or a subagent-settled notice) / all (every finished turn). Env override: DSH_FEISHU_BACKGROUND_PUSH |
appIdRef / appSecretRef |
DSH_FEISHU_APP_ID/SECRET |
credentials ref names |
Credential resolution order: plaintext in patch > DSH_FEISHU_APP_ID/SECRET env
vars > the credentials service.
🧩 Bundled skill
The plugin ships a bundled skill (dsh-feishu-config): ask the agent to set up or
configure the Feishu bot and the guide loads automatically — it checks the
prerequisites (Feishu app, credentials), collects the operators allowlist and
the backgroundPush mode via ask_user_question, writes the config: block
above for you, and walks the phone-side pairing. It also documents the full
config key table, the DSH_FEISHU_* env vars, and the runtime-state
(settings.yaml dsh-feishu: section) vs config distinction.
🧯 Troubleshooting
| Symptom | Fix |
|---|---|
Log: no operators configured — dormant |
Allowlist missing (Step 4) |
Log: no Lark credentials |
Credentials missing (Step 3); restart after changing |
Log: startup failed |
Wrong App ID/Secret, network blocked, or the app version isn't published |
| Bot ignores DMs | Your open_id isn't in the allowlist (non-allowlisted users are silently ignored) |
| Ask card taps do nothing | card.action.trigger isn't subscribed (Step 1.3) |
/resume N says expired |
The list lives 5 minutes — send /resume again |
Development
npm run check # tsc --noEmit
npm test # build + node --test (230+ pure-logic unit tests)
Boundaries
- Single-writer guarantee (host-native since dsh 0.1.5): cold-resuming a
session another process is driving is refused by the host's kernel write
lease (
SessionAlreadyOwnedError) instead of silently forking the log into interleaved seq numbers; same-process attach (shared agent instance) never opens a second write handle and behaves as before; a refused/resumedegrades into a READ-ONLY watch over the persisted log — the phone still receives every turn's final reply (poll-delayed, no streaming detail), and queued follow-ups take over automatically once the other process lets the session go - Group chats are mention-gated and share the bot's single global binding: one bound session at a time, whichever chat dispatched last receives the cards. A group image is accepted only from the chat that is currently the active dispatch surface
- Approval cards ride the host's
approval/requestwaterfall with the standard selector TTL (10 min); an expired or undeliverable approval fails closed asunavailable— never an implicit allow - After attaching, session history is not replayed; counters start from attach time when a turn is already running
- Never install ask-router into a web profile (the upstream apiproxy does not tolerate duplicate provider registrations)
License: MIT. Author fan56.
No comments yet. Be the first to write one.