DeepSeek Harness plugin

dsh-fschannel

Feishu/Lark bot bridge that binds a web session to a Feishu chat so messages flow both ways.

Jump to install

Source facts

Repository
cershuang/dsh-fschannel
Latest update
Aug 15, 2026
Category
Remote & Mobile
GitHub stars
2
Format
bundle
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/cershuang/dsh-fschannel
Plugin: dsh-fschannel
Author: cershuang

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.en.mdSource · read only
README language

dsh-fschannel

English | 中文

Connect a DeepSeek Harness Web session to a Feishu bot: choose "Connect Feishu" when creating a new session, then send the bot a message in Feishu — the message is delivered into that DSH session for the agent to process, and replies flow back to the Feishu chat. Web and Feishu can drive the same session at the same time.

The transport layer uses the official Feishu @larksuite/channel SDK — WebSocket long connection, no public callback URL required.

---

Features

Session ↔ Feishu chat bridge

  • One-to-one binding (one session ↔ one chat, replaced on either side), persisted in $DSH_HOME/feishu-bindings.json
  • Pending-bind mechanism: a session marked as pending is bound to the chat of your next message to the bot (FIFO)
  • Agents are resumed automatically when offline; bindings survive restarts

Messaging

  • Streaming cards (default output: 'stream'): the card opens the moment a turn starts (showing "正在处理…"/processing), replies type out character by character; tool calls show a "正在调用工具:{name}" reference line; when the turn ends the card keeps the final result, automatically re-laid-out as a structured card (paragraphs/tables/code blocks rendered from markdown; empty or failed turns stay on the card). Falls back to plain messages without card permission
  • Embedded card images: markdown image references (![alt](path)) in the final card that point to a local image file inside the session workspace are uploaded and embedded as card img elements — no remote links or broken images in the card; remote URLs and unreadable paths never appear (their alt text is kept)
  • output: 'plain' switches to "one markdown message per step"
  • Queueing: SDK-level dedup (30s) + per-chat serialized delivery; when the agent is busy it replies with the queue position
  • Reactions: inbound messages get 👍, the turn completes to ✅, fails to ☹️ (configurable emoji and toggles; only applies to your own messages, Web-driven turns never get swapped)
  • Image staging & recognition: Feishu images are staged first, then delivered with your NEXT text message as file paths plus a recognition instruction — the agent loads the vision-tools skill and recognizes each image with vision tools (vision_glance/vision_ocr etc.), answering combined with the text (requires @anionex/dsh-vision-toolkit installed and its runtime ready; images live in the session workspace .dsh-fschannel-images/)
  • Session image gallery: the "Feishu images" button in the session header opens a grid of that session's images (click to view the original). Image metadata is managed by the plugin itself (feishu-bindings.json) and is no longer written into the session log — the old feishu/image event type was unknown to the harness and made the whole session unloadable; since v0.1.5.3 the plugin repairs historical logs at boot (marks those events with the harness-accepted ignorable flag) and re-indexes historical images
  • Bot sends images: after the agent generates an image during a turn (charts, crops, code-rendered PNGs etc.), it can call the send_feishu_image tool to deliver the image to the bound Feishu chat (optionally with a caption); the sent image also enters that session's Web gallery. The image path must stay inside the session workspace (traversal blocked); PNG/JPEG/WebP/GIF supported; sendImageTool: false disables the tool
  • Bot sends files: the agent can call the send_feishu_file tool to deliver any generated file (reports, exports, logs; ≤ 30 MiB) to the bound Feishu chat, with an optional display file name (defaults to the original basename) and caption; sendFileTool: false disables the tool
  • Groups only answer when the bot is @-mentioned by default (requireMention); unbound chats get a guidance hint (can be disabled)

Model control from Feishu

  • Mentioning "调整/切换/查看 模型/effort" pops a button card showing the current model and reasoning level with one-click switching
  • /model — same card (query current model & effort)
  • /model list — model directory (including per-model effort levels)
  • /model use <provider>/<model> — switch model (applies from the next message)
  • /model effort <off|high|max> — switch reasoning effort
  • /status — session & bot status; /stop — stop the current turn; /help — command list
  • Switches go through the host apiProxy (same channel as the Web UI) and are session-persistent: replayed automatically after a restart

Connecting Feishu (when creating a session)

  • Session-header chip: unbound → "连接飞书" (Connect Feishu); pending → "send the bot a message to bind"; bound → chat name shown (click to disconnect)
  • Settings → 飞书机器人 (Feishu Bot) tab (dedicated page): status, output mode, "auto-connect new sessions" toggle, "new session and connect" button, binding/pending management, command reference

---

Installation

Prerequisites: Node 22.19+, dsh CLI and pnpm installed (npm i -g @deepseek-ai/dsh pnpm).

# 1. Prepare the environment (.env holds path config only; credentials go to
#    the settings page after startup)
cp example.env .env      # adjust FSCHANNEL_REPO / FSCHANNEL_ENV_FILE as needed

# 2. Build the client (run after every source change)
npm install && npm run build

# 3. Install into the web profile (file: copies the package into the pnpm
#    store and installs its dependencies; the repo path can also be set in
#    FSCHANNEL_REPO in .env)
dsh plugin --profile web add file:<plugin-repo-path>

# 4. Restart dsh web to apply
dsh web

> On first install pnpm may report ERR_PNPM_IGNORED_BUILDS (protobufjs) due to build-script policy. protobufjs is a production transitive dependency (@larksuite/channel@larksuiteoapi/node-sdk → protobufjs) that ships an install script this plugin does not need. Add the following to $DSH_HOME/profiles/<profile>/pnpm-workspace.yaml — that file lives in the profile directory, not in this repo, so you have to create or extend it yourself: > > ``yaml > allowBuilds: > protobufjs: false > ``

---

Configuration

The feishu-bot entry (from the bundled cordis.patch.yml, applied automatically as a bundle layer):

FieldDefaultDescription
envFile<cwd>/.envPath-config file (FSCHANNEL_* keys); no longer carries credentials
appId / appSecretcredential storeDirect entry-config override; the settings page "连接凭据" saves into the DSH credential store
requireMentiontrueGroups answer only when @-mentioned; single-member groups (1 user + bot) skip the @ check
outputstreamstream = streaming typewriter cards (plain fallback on failure); plain = one message per step
modelCardTriggerstrueAuto-pop the button card when model/effort is mentioned
queueAcktrueReply with the queue position when the agent is busy
ackInboundfalseReply "received, processing…" even when idle
reactInboundtrueReaction feedback on inbound messages (👍→✅/☹️)
reactReceived / reactDone / reactErrorTHUMBSUP / DONE / SADEmoji per stage (Feishu standard emoji_type, customizable)
holdImagestrueStage Feishu images and recognize them with the next text
holdHinttrueReply "received N images…" to image-only messages
maxHeldImages10Staged-image cap per chat
maxHeldImageBytes10 MiBPer-image size cap
holdTtlMs0Staging expiry (ms, 0 = keep forever)
imageDir<cwd>/.dsh-fschannel-imagesStaging directory (must be inside the session workspace)
hintUnboundtrueGuidance reply to unbound chats
hintTextbuilt-inCustom guidance text
bindingsFile$DSH_HOME/feishu-bindings.jsonBinding persistence path

Credential precedence: plugin config appId/appSecret > credential service (shell-exported env > DSH credential store $DSH_HOME/.credentials.yaml > project .env > ~/.dsh/.env) > plugin envFile. Recommended: fill in appId/appSecret on the settings page "飞书机器人 → 连接凭据", saved to the credential store (appId shown masked, secret never echoed; don't put credentials in .env).

Path config (in .env, not committed): FSCHANNEL_REPO (plugin repo root, used by scripts/restarts), FSCHANNEL_ENV_FILE (.env's own path, defaults to the working directory), FSCHANNEL_BINDINGS_FILE (binding data file, defaults to $DSH_HOME/feishu-bindings.json). cordis.patch.yml's envFile resolution: FSCHANNEL_ENV_FILE env var (exported by scripts/restart-dsh.ps1 from .env) → <cwd>/.env.

---

Usage

Bind a session to Feishu

1. Create a session in the Web UI (or open an existing one) 2. Click "连接飞书" (Connect Feishu) next to the session title (pending state), or: - Settings → Feishu Bot tab: enable "auto-connect new sessions" → every new session becomes pending automatically - Settings → Feishu Bot tab: "new session and connect" creates and marks pending in one step 3. Send the bot a direct message (or @ it in a group) → that chat binds to this session 4. Chat messages enter the session for processing: replies stream back, Web and Feishu interoperate; the session-header chip shows connected (click to disconnect)

> The "绑定管理" (bindings management) section on the settings page lets you manage: bound sessions can be "断开" (detached); pending sessions can be "取消待绑定" (cancelled) (e.g. sessions created but never used).

Feishu-side commands and cards

| Input | Effect | |---|---| | "调整模型" "切换模型" "把 effort 调一下" etc. | Pops the button card (model Pro/Flash + effort off/high/max one-click switch) | | /model | Pops the same card | | /model list | Lists the model directory | | /model use <provider>/<model> | Switches model | | /model effort <off\|high\|max> | Switches reasoning effort | | /status | Session & bot status | | /stop | Stops the current turn | | /help | Command list |

---

Notes

  • File changes & restart: the plugin source lives in the plugin repo (local path in FSCHANNEL_REPO in .env); after code changes run npm run build (when the client changed) → dsh plugin --profile web add file:<plugin-repo-path> (re-copy into the store) → restart dsh web.
  • Config is read once at startup: changes to cordis.patch.yml need a restart.
  • Credential safety: credentials live in $DSH_HOME/.credentials.yaml (0600, dsh credential store), never echoed by the settings page; .env holds path config only and is safe to commit (the repo .env is still gitignored; example.env is safe to commit).
  • HTTP API is loopback-only: /feishu/* accepts only 127.0.0.1 (same origin as the Web GUI).
  • Streaming cards need card capability: without card permission the SDK falls back to plain messages; output: 'plain' disables cards entirely.
  • Images are staged & recognized, files are not forwarded: images are staged (.dsh-fschannel-images/) and recognized by the agent via vision-tools with the next text; other file types are normalized to text hints; streaming cards carry text and tool-activity lines only.
  • Multi-step turns: one turn (including multiple tool calls) accumulates on the same streaming card.
  • One chat ↔ one session: re-binding replaces the old binding (the old session is auto-unbound).
  • Unbound chats: default is a guidance hint; no session is auto-created.
  • Trigger matching: the model card only triggers on intent phrases like "调整/切换/查看 模型|effort"; the occasional word "模型" in a message does not misfire.
  • Reaction permission: reactions depend on the im:message scope (the same one used to receive messages); failures are logged only and never break send/receive.
  • Final card layout: at turn end the streaming card is replaced by a structured card (tables rendered with the card v2 table component); if the platform rejects structured cards it degrades to a plain markdown card — the final result is always visible.

Relationship with dsh upgrades (update immunity)

LayerLocationAfter a dsh upgrade
Plugin itselfplugin repo (git repo, path in FSCHANNEL_REPO in .env)retained
Registration entrybundled cordis.patch.yml (bundle layer, reconciled by dsh plugin)retained
Binding data$DSH_HOME/feishu-bindings.jsonretained

No internal dsh package files are modified; no re-patching is needed after an upgrade.

---

Development

npm test             # the gate: lint + build + all 10 offline smokes (run before committing)
npm run lint         # static check only (no-undef — the only thing that catches a missing import)
npm run build        # esbuild builds the client bundle (lib/client.js)
npm run watch        # rebuild on change

# Code changes do NOT take effect until the plugin is reinstalled: dsh COPIES it
# into ~/.dsh/profiles/web/node_modules/dsh-fschannel, so editing repo files alone
# does nothing. pnpm also keys its file: store entry on (path, version), so a lone
# `add` reuses the stale entry — remove + add is required.
# scripts/restart-dsh.ps1 chains all of this and compares lib/*.js hashes before
# launching, failing instead of logging UP. By hand it is:
#   npm run build
#   dsh plugin --profile web remove dsh-fschannel
#   dsh plugin --profile web add file:<plugin-repo-path>

# Individual suites (npm test runs them all):
node scripts/smoke-test.mjs     # server-side smoke (env/bindings/persistence)
node scripts/smoke-env-example.mjs      # example.env must not carry credentials
node scripts/smoke-locales.mjs  # zh/en dictionary key and placeholder parity
node scripts/smoke-client.mjs   # client bundle smoke
node scripts/smoke-settings-render.mjs  # settings-page render smoke (bindings table + layout)
node scripts/smoke-cards.mjs    # model card & trigger tests
node scripts/smoke-render.mjs   # markdown segmentation and result-card rendering
node scripts/smoke-stream.mjs   # streaming card buffering and failure fallback
node scripts/smoke-images.mjs   # image validation, staging and note composition
node scripts/smoke-repair.mjs   # session-log repair (feishu/image ignorable + seq conflicts)

# Need a real environment; excluded from npm test:
node scripts/audit-sessions.mjs # audit all session logs for seq continuity (needs a real $DSH_HOME)
node scripts/integration-test.mjs  # integration test (real Feishu connection + mock apiProxy)

---

Structure

  • lib/index.js — host plugin: transport, inbound/outbound bridge, command channel, card actions, /feishu HTTP API
  • lib/cards.js — model/reasoning settings button card (build, triggers, action parsing)
  • lib/stream.js — streaming cards (buffering + failure fallback)
  • lib/bindings.js — binding store (atomic JSON writes, per-session model route)
  • lib/env.js — .env parsing (path config) + layered credential resolution (credential service > envFile)
  • lib/images.js — image type table, validation, hold buffer and note composition
  • lib/render.js — markdown segmentation and final result-card rendering
  • lib/repair.js — historical session-log repair (zstd frame decoding, foreign events, seq conflicts)
  • lib/locales.js — Feishu-facing copy (zh/en, follows the host locale)
  • lib/send-image-tool.jssend_feishu_image tool (agent sends generated images to the bound chat)
  • lib/send-file-tool.jssend_feishu_file tool (agent sends generated files to the bound chat)
  • src/client/index.jsxlib/client.js — browser side: session-header chip + settings page

---

Known limitations

  • One streaming card per turn; card failures fall back to plain messages
  • Bindings are read once at startup; config changes need a restart
  • Images enter the model as recognition description text (raw bytes never enter the model)
  • Feishu messages arriving while the harness is down are not replayed (no transport cursor)
  • The server locale is one process-wide value (whichever client reported last). Settings-page errors are unaffected — the host returns error codes and the client translates them — but Feishu-facing copy follows the last browser to report; a chat has no locale of its own, so this is a deliberate single-tenant trade-off
  • POST /feishu/repair-logs is an operations-only endpoint: nothing in the UI calls it, it is rate-limited to one pass a minute, and the pass is fully synchronous — it blocks the entire harness process (every agent, the web server, the Feishu transport) while it runs

---

License

MIT License (see [LICENSE](LICENSE) in the repo root):

Copyright (c) 2026 CersHuang

Any modification or derivative work (including code and documentation) must retain the above copyright and license notices.

---

Appendix: creating and configuring the Feishu app (reference flow)

> The flow below walks through the full self-built app configuration, including two known pitfalls (personal accounts cannot create apps; configuring the long connection before publishing fails). The console UI may vary slightly between versions.

Step 0: confirm the account can create a self-built app

1. Open https://open.feishu.cn/ in a browser and sign in with your Feishu account 2. Go to the developer console and click "create an enterprise self-built app" 3. If blocked (personal plan not supported / no enterprise): in the Feishu client → avatar → settings → "upgrade to a team", create a free team with only yourself, then redo step 1

Step 1: create the app and grab credentials

1. Create an enterprise self-built app with any name (e.g. dsh-bot) and an avatar 2. Left sidebar "Credentials & Basic Info" → note the App ID (cli_ prefix) and App Secret (hidden initially; click "reset" or "view" to get the full value) 3. Save them on the settings page: start dsh web, open Settings → "飞书机器人" → "连接凭据", fill in App ID and App Secret (saved to the DSH credential store; appId shown masked, secret never echoed). > Historically they lived in .env (FEISHU_APP_ID=... / FEISHU_APP_SECRET=...); since v0.1.5.1 .env no longer carries credentials — always use the settings page/credential store.

Step 2: grant permissions

Left sidebar "Permission Management" → search and enable these 6 scopes:

ScopePurpose
im:messageReceive direct messages sent to the bot
im:message:send_as_botSend messages as the app (replies, cards)
im:message.group_at_msg:readonlyReceive group messages that @ the bot
im:chatFetch chat info (chat name shown after binding)
im:chat.members:bot_accessFetch chat members (single-member-group @ exemption, chat name after binding)
cardkit:card:writeSend/update interactive cards (streaming cards, model settings card)

Step 2.5: restrict availability (security boundary, required)

Left sidebar "Version Management & Release" → when creating a version, under "availability" select only your own member — never "all members". This is the only barrier preventing others from taking control of your sessions.

Step 3: enable the bot capability

Left sidebar "App Capabilities" → add the "bot" capability. Without this, direct messages never trigger any event.

Step 4: publish the app (must be done BEFORE configuring the long connection)

1. Left sidebar "Version Management & Release" → "create version" → fill in version number and update notes → "apply for release" 2. Since you're the team admin, approve it yourself in the admin console

> ⚠️ Known pitfall: without a published version, saving the long-connection config on the event subscription page errors with "the app has no long connection established". Publish first, then configure the long connection.

Step 5: configure event subscription (long connection)

Left sidebar "Events & Callbacks":

1. "Event config" → subscription mode