Skip to content
dsh.fish
Bundle

@kriskwok/dsh-feishu-gateway

DeepSeek Harness-native Feishu (Lark) gateway: chat with the DSH agent from Feishu via long connection, with persistent sessions, /new, Markdown replies, live streaming progress cards, native Typing reactions, click-to-answer approval / Q&A cards, and proactive push.

Source
kriskwok
stars
2 stars
License
MIT
Updated
Updated 13 days ago

Readme

# dsh-feishu-gateway

[![npm version](https://img.shields.io/npm/v/@kriskwok/dsh-feishu-gateway)](https://www.npmjs.com/package/@kriskwok/dsh-feishu-gateway)
[![License: MIT](https://img.shields.io/npm/l/@kriskwok/dsh-feishu-gateway)](LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/kriskwok/dsh-feishu-gateway)](https://github.com/kriskwok/dsh-feishu-gateway)

English | [中文](README.zh.md)

Chat with your **DeepSeek Harness (DSH)** agent from **Feishu (Lark)**.

A DSH plugin bundle that mounts a Feishu long-connection listener; every Feishu
message is routed to a **stable DSH session** (resumed via the `agents` service,
so multi-turn chats stay in the same session), and the agent's answer is
replied as a Markdown-rich post message. It also supports `/new` to start a
fresh session, a native **Typing reaction** while the answer is being produced,
**live streaming progress cards** for long tasks, **click-to-answer cards** for
permission approvals and the model's `ask_user_question` tool, and proactive
push.

## Features

- 💬 **Full conversation** — Feishu private chat, or group @bot → DSH agent → reply. In groups, every topic/thread is its own independent DSH session, and all messages in that topic stay in the same session until `/new`.
- 🔁 **Persistent sessions** — each Feishu conversation (or group topic) maps to one DSH
  session (`agents.resume` / `agents.create`); `/new` (or "另起会话" / "新会话" /
  "重新开始" / "换个话题") starts a fresh one, including inside a group topic.
- ⌨️ **Native Typing indicator** — while the agent works, the bot adds a
  `Typing` reaction to your message (like [hermes-agent's Feishu gateway](https://github.com/NousResearch/hermes-agent));
  it stays until the answer is done, and is swapped for a `CrossMark` reaction
  on failure. No more "thinking…" hint text by default.
- 🎞 **Streaming progress** — long tasks report continuously: a live interactive
  card streams the agent's **thinking, tool calls, and answer draft** as they
  happen (`reporting.mode: 'stream'`, default). Set `reporting.mode: 'final'`
  to only receive the final result.
- 🃏 **Click-to-answer cards** — permission approvals (`approval/request`, e.g.
  sandbox escalation) and the model's `ask_user_question` tool render as Feishu
  interactive cards: click **✅ 允许一次 / 🚫 拒绝** or an option button to answer.
  The click response instantly replaces the card with a decided state (buttons
  removed, result shown) plus a toast confirmation.
- ✍️ **Markdown replies** — plain rich-text (`post`) messages with the `md`
  tag: bold, inline code, lists and links render natively, no cards needed
- 🧩 **Web-only interactive fences degrade gracefully** — model-emitted
  `dsh-ui` interactive UI fences (e.g. from dsh-genui) only render in the Web
  UI; on Feishu they are auto-downgraded to a one-line readable hint (title
  extracted, "view in Web UI"), never a raw JSON code block
- 🤖 **Full agent capability** — the DSH agent runs with its own model and
  tools (bash, files, subagents…), fully autonomous
- 📨 **Proactive push** — optional admin HTTP API (`/api/push`) to push text /
  Markdown / cards to any user or group
- 🔌 **No public network required** — Feishu long connection, no webhook URL
- 🗂 **Persistence** — Feishu↔DSH session mapping survives restarts

## Requirements

- DeepSeek Harness installed and built (`dsh` CLI), with `DEEPSEEK_API_KEY`
  configured (the agent's model is used as-is)
- A Feishu open-platform **self-built app** with the bot capability enabled
  (see below)

## Feishu app setup

1. [Feishu Open Platform](https://open.feishu.cn/app) → create a
   **self-built app**.
2. Enable the **bot** capability.
3. Grant permissions: `im:message`, `im:message:send_as_bot` (+
   `im:message:send_as_bot:readonly` to read content). Publish a version.
4. Under **Events & callbacks**, choose **long connection** and subscribe to
   **`im.message.receive_v1`** (no public URL needed). The same long
   connection also delivers **card button clicks** (`card.action.trigger`,
   used by the approval / Q&A cards) — no webhook URL required.
5. In Feishu, search the app name and add the bot as a contact.

> The `Typing` reaction indicator and card buttons need the bot to be able to
> interact with messages in the chat (`im:message`). If the reaction API is
> denied, the gateway automatically falls back to the `hintText` message.

## Installation (as a DSH plugin)

Prerequisite: this package is published on npm and `dsh` is on your PATH.

The recommended setup **mounts the gateway into the web profile**: it runs in
the same process as the DSH Web UI, so starting the Web UI also starts the
Feishu gateway, and both share the same DSH agent. A standalone profile is also
supported (see "Alternative" at the end).

### Option 1 (recommended): mount into the web profile

The web profile is DSH's default GUI profile (`dsh --profile web`).

1. Edit `~/.dsh/profiles/web/package.json` to add the dependency and bundle:

```json
{
  "name": "dsh-profile-web",
  "private": true,
  "dependencies": {
    "@kriskwok/dsh-feishu-gateway": "^0.2.0"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "@deepseek-ai/dsh-web-app",
        "@kriskwok/dsh-feishu-gateway"
      ]
    }
  }
}
```

2. Install dependencies in the web profile directory:

```bash
cd ~/.dsh/profiles/web && pnpm install
```

3. Edit `~/.dsh/profiles/web/cordis.patch.yml` and fill in your Feishu app
   credentials:

```yaml
- id: feishu-gateway
  config:
    feishu:
      appId: cli_xxxxxxxxxxxxxxxx
      appSecret: xxxxxxxxxxxxxxxxxxxxxxxx
    http:
      port: 3100      # optional admin API
      token: your-token
```

4. Start (or restart) the web profile:

```bash
dsh --profile web
```

> You can also use the one-shot script from this repository:
> `./scripts/create-profile.sh` (mounts into the web profile by default;
> `--standalone` creates a standalone feishu profile instead).

### Alternative: standalone feishu profile

To run the gateway without the Web UI, use a standalone profile:

```bash
mkdir -p ~/.dsh/profiles/feishu && cd ~/.dsh/profiles/feishu

cat > package.json <<'EOF'
{
  "name": "dsh-profile-feishu",
  "private": true,
  "dependencies": {
    "@kriskwok/dsh-feishu-gateway": "^0.2.0"
  },
  "dsh": {
    "profile": {
      "bundles": ["@deepseek-ai/dsh-base", "@kriskwok/dsh-feishu-gateway"]
    }
  }
}
EOF

cat > pnpm-workspace.yaml <<'EOF'
packages:
  - .
nodeLinker: hoisted
autoInstallPeers: false
EOF

pnpm install
# then create ~/.dsh/profiles/feishu/cordis.patch.yml with your app credentials
dsh --profile feishu
```

## Configuration

All settings live in the `feishu-gateway` namespace (profile patch row or
`~/.dsh/settings.yaml`):

| Field | Default | Description |
|---|---|---|
| `feishu.appId` | — | Feishu app id (required) |
| `feishu.appSecret` | — | Feishu app secret (required) |
| `feishu.domain` | `feishu` | `feishu` (CN) or `lark` (international) |
| `feishu.botOpenId` | `` | Optional; @-detection works without it |
| `feishu.replyMode` | `at` | Group policy: `at` (reply only when @-mentioned) or `all` (reply to every message). Each group topic/thread is its own DSH session. |
| `workspace` | `/root/Documents/DSH-Workspace` | Agent working directory (the session is also auto-attached to the matching DSH Workspace, so it groups under that Workspace in the Web UI instead of "Ungrouped"). Both private and group-topic sessions are anchored here. |
| `hintText` | `爸爸,我正在努力处理中……` | Fallback "processing" text (only when the Typing reaction is disabled/unavailable) |
| `reporting.mode` | `stream` | `stream` = live streaming progress card; `final` = only the final answer |
| `reporting.typingReaction` | `true` | Show the native Feishu `Typing` reaction while working |
| `reporting.showReasoning` | `true` | Stream the model's reasoning in the card |
| `reporting.showToolCalls` | `true` | Stream tool-call activity in the card |
| `reporting.patchIntervalMs` | `1100` | Min interval between card patches; Feishu limits single-message updates to ~1/s (error 230020), and the stream backs off adaptively on failure |
| `reporting.maxBodyChars` | `900` | Max rendered card body length |
| `reporting.failureReaction` | `CrossMark` | Reaction added on failure (after removing `Typing`) |
| `reporting.cardTitleStreaming` | `🤖 DSH 处理中…` | Custom title of the streaming card while working (yellow header) |
| `reporting.cardTitleDone` | `🤖 DSH 处理完成` | Custom title of the streaming card when complete (green header) |
| `interactions.approvalCards` | `true` | Answer permission approvals with clickable cards |
| `interactions.userQuestionsCards` | `true` | Answer `ask_user_question` with clickable cards |
| `interactions.approvalCardDispose` | `update` | After an approval-card click: `update` instantly replaces the card with a decided state (buttons removed, result shown) + toast; `recall` recalls the card message (falls back to `update` when recall fails — note Feishu leaves a "撤回了一条消息" placeholder) |
| `newSessionPatterns` | `/new` + Chinese phrases | Regexes that reset the session |
| `sessionsFile` | `data/dsh-feishu-sessions.json` | Session mapping persistence |
| `http.port` | `0` | Admin API port (`0` disables) |
| `http.token` | `` | Admin API bearer token |

> **Q&A cards in the web profile**: `ask_user_question` answers go through the
> single `ctx.userQuestions` provider slot. The gateway never claims that slot
> (stealing it makes the Web UI's apiProxy host fail to start with
> `DUPLICATE_PROVIDER`); it wraps `service.ask` at the service boundary
> instead: sessions owned by a Feishu conversation are answered with Feishu
> cards, while every other session continues through the registered UI
> provider. Permission-approval cards work from Feishu in every setup. In a
> standalone feishu profile, both `ask_user_question` and approvals are
> answered from Feishu cards.

## Session coexistence & self-healing

- **Preset composition (tools!)** — in preset-roster deployments (e.g. the web
  profile), Feishu agents are composed from the deployment's agent preset
  (`meta.agentPreset` + preset `mount`), so the model gets its tools instead of
  treating tool calls as plain text.
- **Web UI coexistence** — sessions have a single live owner. When the Web UI
  opens a session, the Feishu side takes over the *running* agent via
  `agents.get()` and drives the same session instead of failing with
  "while it is live" / "already exists"; both surfaces share one conversation.
- **Wedged-session self-heal** — if a crashed process leaves a session
  permanently conflicted ("already exists"), the gateway mints a fresh session
  id, re-points the Feishu conversation at it, and continues chatting.

## Admin HTTP API (optional)

Enable by setting `http.port`. Endpoints:

- `GET /health` — status
- `POST /api/push` — proactive push
  `{ "receive_id": "ou_xxx", "receive_id_type": "open_id", "msg_type": "text", "content": "{\"text\":\"hi\"}" }`
- `GET /api/sessions` — Feishu↔DSH session mapping overview

## Development

```bash
pnpm install
pnpm build     # tsc → lib/
pnpm test      # offline self-tests
```

> Note: `@deepseek-ai/*` packages are provided by the DSH host at runtime; for
> local type-checking they are symlinked from your deepseek-harness checkout
> (see the publish checklist).

## License

MIT

Install

dsh plugin --profile web add github:kriskwok/dsh-feishu-gateway

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source