![dsh-feishu-bridge banner](docs/assets/dsh-feishu-bridge-banner.png) # dsh-feishu-bridge A bridge plugin that connects a **Feishu (Lark) bot** to **DeepSeek Harness (DSH)**: **Feishu / Lark ↔ DeepSeek Harness**. Message a bot in Feishu, and the message becomes one task for a DSH Agent; the Agent's **final answer** and **progress hints** (e.g. which tools it called) are sent back to Feishu — control DSH agents directly from Feishu. - **No public endpoint needed**: WebSocket long connection, runs locally - **Session context preserved**: one DSH Session per Feishu chat / thread - **Visible execution**: tool-call progress forwarded to Feishu in real time - **Control from Feishu**: `/reset`, `/workspace`, `/mode`, `/model`, `/effort`, `/stop`, and more ## Quick install ```sh # Install from GitHub (recommended) npx @deepseek-ai/dsh plugin --profile web add git+https://github.com/ailoushu666/dsh-feishu-bridge.git # Or install from a local checkout (development) npx @deepseek-ai/dsh plugin --profile web add "" ``` For the full steps see [Quick start (deploy into DSH)](#quick-start-deploy-into-dsh); Feishu console setup: [docs/feishu-setup.md](docs/feishu-setup.md). > 中文:[README.md](README.md) --- ## Features - Uses Feishu's official `@larksuiteoapi/node-sdk` over a **WebSocket long connection** — no public IP / domain / port forwarding required. - Reuses the same DSH Session per Feishu chat (or thread), so context is preserved; each thread is independent. - Replies are **linked to the triggering message**; replies inside a thread stay in that thread. - By default only handles **direct messages** and **group messages that @mention the bot**. - Visible progress: tool calls show as `🔧 调用工具 `, and **intermediate replies** between tool calls are forwarded too (`💬`), all sent through a throttled queue so Feishu rate limits never drop them; the final answer is replied to the original message separately. - Instant acknowledgement (“processing / queued”) for every message; turns exceeding `turnTimeoutMs` are auto-cancelled with a notice. - **Autonomous DSH turns are pushed back to Feishu**: when DSH wakes the Agent on its own (goal rounds / scheduled tasks / background jobs) and produces a result, Feishu receives it — no longer limited to one reply per message. - **Interactive questions in Feishu**: when the Agent needs your confirmation or choice (`ask_user_question`), the question and its options arrive as a `❓` list — reply with a **number (e.g. `1`), an option label, or free text** to answer, and the Agent continues; invalid replies (several numbers for a single choice, out-of-range numbers) are re-prompted instead of polluting the Agent. - A set of **Feishu control commands**: switch project folder, switch working mode, switch model & reasoning effort, stop a task, record feedback, manage a long-running goal, enter plan mode, and more. --- ## Supported commands Send them directly in Feishu (@mention the bot in groups, plain in DMs). Case-insensitive. | Command | Argument | Purpose | Interrupts the conversation? | | --- | --- | --- | --- | | `/reset` | — | Start a new conversation (the old one stays in the DSH sidebar). Aliases: `/new`, `/clear`, `重置`, `新会话`, `清空会话`, `重置会话` | Yes | | `/compact` | — | Compact context (summarize older history to save tokens). Aliases: `/压缩`, `压缩上下文`, `压缩会话` | No | | `/workspace` | `` | Switch the project folder (path must already exist) | Yes | | `/mode` (= `/permission`) | `` | Switch working mode: read-only / workspace-write / full access (a permission preset bundling sandbox + approval policy) | No (this session only) | | `/model` | `` | Switch model (unknown names are rejected) | No (next turn) | | `/effort` | `` | Set reasoning effort (only effort levels the model supports) | No (next turn) | | `/stop` | — | Immediately cancel the running task. Aliases: `/cancel`, `/halt` | — | | `/feedback` | `` | Record feedback about this session | No | | `/goal` | `[objective\|clear\|edit \|pause\|resume]` | Set / view / manage a long-running goal | No | | `/plan` | `[off\|description]` | Enter / leave plan mode (plan before acting) | No | | `/export` | — | Export the session log (web-only feature; the Feishu text channel cannot deliver files, so it points you to the web UI) | — | | `/session` | `[number\|full id]` | List non-archived sessions (with titles) without an argument; switch to a specific session with one | Yes (switches session) | | `/help` | — | List all commands | — | `/workspace`, `/mode`, `/model`, `/effort` without an argument show the current value or available options. For `/mode`, the short names `read` / `write` / `full` map to `read-only` / `workspace-write` / `danger-full-access`; full names are also accepted. --- ## Requirements - Node.js `^22.19.0` or `>= 24` (same as Harness). - A working DeepSeek Harness (`dsh web`). - A Feishu **self-built app** with: bot capability enabled, the `im.message.receive_v1` event subscribed over long connection, and the required permissions granted. See [docs/feishu-setup.md](docs/feishu-setup.md) for the full Feishu console steps. --- ## Quick start (deploy into DSH) ### Step 1: Prepare Feishu Follow [docs/feishu-setup.md](docs/feishu-setup.md): create the self-built app → enable the bot capability → grant permissions → subscribe `im.message.receive_v1` over long connection → publish and install the app. Note down the **App ID** and **App Secret**. ### Step 2: Install the plugin into the `web` profile ```sh npx @deepseek-ai/dsh plugin --profile web add "" ``` The plugin creates a **disabled by default** `feishu-bridge` instance in the bundle layer. ### Step 3: Store the secret and enable The **App ID** (not secret) and the **App Secret** (secret) are stored separately: 1. Put the **App Secret** into DSH's credential file `~/.dsh/.credentials.yaml` (Windows: `C:\Users\\.dsh\.credentials.yaml`), with a key matching `appSecretEnv` (default `FEISHU_APP_SECRET`): ```yaml FEISHU_APP_SECRET: ``` 2. Edit the profile patch `~/.dsh/profiles/web/cordis.patch.yml` (Windows: `C:\Users\\.dsh\profiles\web\cordis.patch.yml`) and override the same id to enable it: ```yaml - id: feishu-bridge disabled: false config: appId: cli_xxxxxxxxxxxxxxxx # App ID (not secret) appSecretEnv: FEISHU_APP_SECRET # reference name; value lives in ~/.dsh/.credentials.yaml domain: feishu # feishu (China) or lark (international) requireMention: true # group messages must @mention the bot dmMode: open # DMs: open / allowlist / disabled ``` > Do not `insert` a second instance with the same id — that raises `duplicate loader entry id: feishu-bridge`. ### Step 4: Start ```sh npx @deepseek-ai/dsh web ``` This line means the Feishu long connection is up: ```text feishu-bridge: WebSocket connected ``` ### Step 5: Verify - DM the bot; it replies with the final answer, and keeps context on follow-ups. - In a group: `@bot your question`. - Send `/help` for the full command list. --- ## Config & credential locations | What | File | | --- | --- | | Enable instance + plugin config | `~/.dsh/profiles/web/cordis.patch.yml` (Windows: `C:\Users\\.dsh\profiles\web\cordis.patch.yml`) | | App Secret (credential) | `~/.dsh/.credentials.yaml` (Windows: `C:\Users\\.dsh\.credentials.yaml`) | --- ## Configuration options | Option | Required | Default | Description | | --- | --- | --- | --- | | `appId` | Yes | — | Feishu App ID (not secret; plaintext) | | `appSecretEnv` | Yes | `FEISHU_APP_SECRET` | Credential reference name; the value lives in `~/.dsh/.credentials.yaml` | | `domain` | No | `feishu` | `feishu` (China) / `lark` (international) | | `requireMention` | No | `true` | Require @mention in groups | | `dmMode` | No | `open` | DM policy: `open` / `allowlist` / `disabled` | | `groupAllowlist` | No | `[]` | Group `chat_id` allowlist; empty = unrestricted | | `dmAllowlist` | No | `[]` | Allowed user `open_id`s when `dmMode: allowlist` | | `botOpenId` | No | — | Bot open_id for exact @mention detection; absent falls back to "mentions not empty" | | `provider` / `model` | No | Harness default | Model for the Feishu channel | | `reasoningEffort` | No | model default | Reasoning effort for the Feishu channel (e.g. `off`/`high`/`max`) | | `workspace` | No | first Workspace | Agent working directory | | `agentPreset` | No | default Preset | Preset for the Agent (tools / system prompt) | | `streamProgress` | No | `true` | Forward tool-call progress to Feishu | | `maxProgressMessages` | No | `0` (unlimited) | Max progress messages per turn (tool calls + intermediate replies); 0 = forward everything | | `processingNotice` | No | `true` | Send an instant “processing / queued” acknowledgement for normal messages | | `turnTimeoutMs` | No | `900000` (15 min) | Max duration of one turn in ms; auto-cancel with a notice on timeout | | `resetCommand` | No | `/reset` | The reset command | | `compactCommand` | No | `/compact` | The compact command | | `requireAdminForGroupReset` | No | `true` | Require group owner/admin for `/reset` (needs `im:chat:readonly`; set `false` to allow members) | | `errorMessage` | No | built-in Chinese text | Unified reply on error (max 500 chars) | Full example: ```yaml - id: feishu-bridge disabled: false config: appId: cli_xxxxxxxxxxxxxxxx appSecretEnv: FEISHU_APP_SECRET domain: feishu requireMention: true dmMode: open # optional: provider: deepseek-official model: deepseek-v4-flash reasoningEffort: high workspace: C:\Project\my-repo agentPreset: coding streamProgress: true maxProgressMessages: 10 ``` --- ## Feishu permissions The default setup (DM + group @mention + reply) needs these permissions; see [docs/feishu-setup.md](docs/feishu-setup.md) for details: | Permission | Purpose | Required | | --- | --- | --- | | `im:message.p2p_msg:readonly` | Receive DM messages | Yes | | `im:message.group_at_msg:readonly` | Receive group messages that @mention the bot | Yes | | `im:message:send_as_bot` | Send messages (reply) as the app | Yes | | `im:chat:readonly` | Read group info (owner/admin check) | Only when group `/reset` requires admin | Event subscription: use the **long connection** receive mode and subscribe **`im.message.receive_v1`**. --- ## Project structure ``` dsh-feishu-bridge/ ├── package.json # package metadata + dsh.bundle declaration ├── cordis.patch.yml # bundle patch: disabled-by-default instance ├── lib/ │ ├── index.js # plugin entry (name/inject/Config/apply + command dispatch) │ ├── config.js # config schema + validation │ ├── feishu.js # Feishu long connection + send/reply │ └── bridge.js # session mapping + agent driving + channel state + command execution ├── docs/ │ ├── technical.md # technical documentation │ └── feishu-setup.md # Feishu developer console guide └── .env.example # explains credential location (no .env needed in practice) ``` --- ## Security - The App Secret lives only in DSH's credential file `~/.dsh/.credentials.yaml`; the plugin never records it or writes it into the project. - Internal errors reply with a unified `errorMessage` only — stack traces and sensitive info never reach Feishu users. - Session IDs are derived via SHA-256 and never embed the raw `chat_id` / `thread_id`. - **Do not run multiple long-connection consumers for the same Feishu app** (e.g. both this bridge and OpenClaw's Feishu channel). Feishu distributes events randomly among the connections, so messages get "stolen" and the bot becomes flaky or silent. --- ## Known limitations (MVP) - Text messages only; images, rich text (post), files and cards are unsupported. - Replies are sent all-at-once (not streamed); progress covers "tool call started + intermediate replies", not tool results. - Progress messages, intermediate replies, autonomous-turn results and `❓` questions are all sent to the **chat root** — including for thread conversations (Feishu API limitation: only `reply` can stay inside a thread). - No persistent `chatId → sessionId` mapping: after a DSH restart, existing Feishu chats get a new Session (the old one stays on disk but is no longer reused). - Runtime switches of model / reasoning effort / project folder are **in-memory** and reset to config defaults after a DSH restart. - `/export` (ZIP download) is a DSH web-only feature; the Feishu text channel cannot deliver files. - Only one long-connection instance per Feishu app. --- ## Troubleshooting | Symptom | Check | | --- | --- | | Auth failure on startup | App ID / Secret belong to the same app; `FEISHU_APP_SECRET` is in `~/.dsh/.credentials.yaml` | | Connected but no messages | app published & installed; bot in the group; `im.message.receive_v1` subscribed; long-connection mode; permissions approved; group @mention | | Receives but can't reply | `im:message:send_as_bot` granted; check Feishu API errors in the terminal | | Connection keeps reconnecting | Feishu HTTPS/WebSocket reachable; not running multiple long-connection consumers | | Config change has no effect | stop and restart Harness (instances are created at profile startup) | | Group `/reset` says "owner/admin only" | grant `im:chat:readonly`, or set `requireAdminForGroupReset: false` | | Intermittent "no response" on consecutive messages | usually DeepSeek API rate/concurrency limits (TPM/RPM); retry later, or use `/effort off` to reduce reasoning cost | --- ## Uninstall ```sh npx @deepseek-ai/dsh plugin --profile web remove dsh-feishu-bridge ``` Then remove the leftover `feishu-bridge` config from `~/.dsh/profiles/web/cordis.patch.yml`. --- ## Docs - [Feishu developer console guide](docs/feishu-setup.md) - [Technical documentation](docs/technical.md) ## License [MIT](LICENSE)