Bundle
lark-agent-bridge
Feishu/Lark bridge for local coding agents — one group, one conversation, one pinned runtime. Bridges dsh, CLI agents (traex/codex), an IDE window, or a custom agent behind one gateway.
- Source
- bihangchi9-creator
- stars
- 33 stars
- License
- MIT
- Updated
- Updated 20 hours ago
Readme
# Lark Agent Bridge
<sub>npm package: `lark-agent-bridge` · git repo: `dsh-lark-bridge` (repo name kept; clone paths below are unchanged)</sub>
> A Feishu / Lark bridge for local coding agents — *one group, one conversation, one pinned runtime*. Bridges **dsh** (in-process plugin), **CLI** agents (`traex` / `codex`, spawned by a daemon), an **IDE** window (attached over a socket), or a **custom** agent — behind one gateway.
[中文 README](./README.zh.md)
Send a message in a Feishu chat, and a real coding agent — with its own tools, its own project directory, and its own persistent conversation — answers you right there. Each group chat is an isolated workspace pinned to exactly one runtime, so a team can run several projects (and several agents) in parallel, one per group.
---
## What it does
- **Feishu ⇄ your agent.** Inbound Feishu messages drive a live agent; the reply streams back onto a live-updating Feishu message. The agent is whichever runtime this chat is pinned to (dsh / CLI / IDE / custom).
- **Four host classes, one contract.** Every runtime implements the same `AgentAdapter`: dsh runs in-process as a plugin; CLI **spawns** `traex`/`codex`; IDE **attaches** a running window; custom loads your own module. They never silently replace each other.
- **One group, one conversation, one pinned runtime.** Every chat id maps to a stable directory (`<workspaceRoot>/<chatId>`) and one runtime chosen with `/agent`. Different groups never touch each other's files, and a message is never broadcast to several agents. If the pinned runtime is down (e.g. an IDE window closed), that chat fails closed instead of retargeting.
- **Persistent per-chat sessions.** A chat's conversation survives restarts (policy-fingerprint-gated resume-or-create; `/new` really clears it).
- **Files and images.** Send them to the bot and the bridge stores each message in an isolated `.attachments/<messageId>/` folder, then gives the paths to the agent. Limits: 5 attachments per message, images ≤10 MB, other files ≤20 MB; names are sanitized and stale files are swept after 7 days. Whether an image can actually be interpreted depends on the selected model's vision support.
- **Zero-config setup.** On first launch, if no credentials exist, a QR registration wizard runs — scan it in the Feishu app and it connects automatically. No portal spelunking.
- **Slash commands.** `/help`, `/new`, `/where`, `/models`, `/agent`, and `/whoami` manage each chat locally; owner-only `/agent`, `/model`, `/preset`, `/allow`, and `/disallow` change shared chat state. `/agent` pins this chat to one installed runtime and never silently retargets.
## Architecture in one picture
```
① Feishu Open Platform ← register a bot here (auto QR wizard does it for you)
│ gives: app_id + app_secret
▼
② lark-agent-bridge gateway ← holds the keys, opens a WebSocket to Feishu,
│ turns each message into one agent turn,
│ routes each chat to its pinned runtime
▼
③ the pinned runtime ← one of:
• dsh — in-process Cordis plugin (`dsh web`)
• CLI — daemon spawns traex / codex
• IDE — daemon attaches a running window over a socket
• custom — daemon loads your own AgentAdapter module
```
The bot **registration lives entirely on Feishu**. The gateway connects out to Feishu over a long-lived WebSocket (so no public IP or callback URL is needed). For **dsh** the gateway *is* the plugin loaded by `dsh web`; for **CLI / IDE / custom** it is a **separate daemon** (`node lib/daemon.js`), independent of any dsh host.
```bash
pnpm build
node lib/daemon.js # spawn traex/codex on PATH
LARK_BRIDGE_RUNTIME=traex node lib/daemon.js
LARK_BRIDGE_IDE_SOCKET=/tmp/ide.sock node lib/daemon.js
LARK_BRIDGE_CUSTOM_ADAPTER=./examples/custom-adapter.mjs node lib/daemon.js
```
CLI **spawns** the binary. IDE **attaches** a Unix-socket JSONL sidecar (window closed ⇒ that line dies). Custom loads an `AgentAdapter` module (see `examples/custom-adapter.mjs`). One group is still one conversation pinned with `/agent`; a dead line is not retargeted.
ByteDance-only overlay (SSO, bytecli, extra presets) lives in a local `internal/` directory that is gitignored. Do not publish it to this GitHub repo; ship it through the internal skill marketplace.
---
## Requirements
- A working **DeepSeek Harness (dsh)** checkout you can launch with `dsh web`.
- **Node.js** `^22.19.0 || >=24.0.0`.
- A **DeepSeek API key** (set `DEEPSEEK_API_KEY`, or configure it in your dsh credentials).
- A **Feishu account** to scan the QR code (the wizard creates the app for you).
## Install
> **Build first.** This repo ships TypeScript source; the compiled `lib/` is
> git-ignored, so a fresh clone has NO build output. The plugin's entry is
> `lib/index.js`, so installing without building gives dsh an empty package
> and **the host fails to load it**. `pnpm setup` builds for you; if you
> install manually, run `pnpm install && pnpm build` first.
### Option 1: one-command setup (recommended)
```bash
git clone https://github.com/bihangchi9-creator/dsh-lark-bridge.git
cd dsh-lark-bridge
pnpm setup # macOS / Linux (scripts/setup.sh) — builds, links, registers
pnpm setup:win # Windows (scripts/setup.ps1)
```
The script preflights your Node version, **builds the plugin (fails loudly if
the build fails)**, installs the access-tier presets, and registers both the
bridge and its `dsh-tool-lark-cli` dependency. When `dsh` is available it uses
the official `dsh plugin` command, which **initializes a missing `web` or
`headless` profile automatically** — no preliminary `dsh web` launch is
needed. After that, launch dsh directly with **no `--patch` flag**:
```bash
# macOS / Linux
DSH_PERMISSION_MODE=danger-full-access dsh web
# Windows PowerShell
$env:DSH_PERMISSION_MODE = "danger-full-access"; dsh web
```
> Different profile: `DSH_PROFILE=headless pnpm setup`; custom dsh home:
> `DSH_HOME=/path/.dsh pnpm setup` (both env vars work on Windows too). If no
> `dsh` command is available, setup can only use its manual fallback and
> therefore requires an already-initialized profile; it fails before building
> or copying presets, so it leaves no partial installation.
### Option 2: official `dsh plugin` command (build first!)
```bash
git clone https://github.com/bihangchi9-creator/dsh-lark-bridge.git
cd dsh-lark-bridge
pnpm install && pnpm build # REQUIRED — link installs pull lib/ from this dir
# then, from your dsh checkout:
dsh plugin --profile web add link:/path/to/dsh-lark-bridge
```
`dsh plugin` runs `pnpm add` in the profile directory and **auto-reconciles
`dsh.profile.bundles`**: a package that declares `dsh.bundle` joins the layer
stack automatically. Remove/update with the same family:
`dsh plugin --profile web remove lark-agent-bridge` /
`dsh plugin --profile web update lark-agent-bridge`.
> ⚠️ A `link:` install points the profile dependency at THIS directory. If you
> later move or delete it, the next `dsh web` cannot resolve the bundle and
> fails to boot. Keep the clone in place, or use Option 1.
### Option 3: manual install (source mode)
Because the dsh public npm graph is still partial, install from source alongside your dsh checkout.
```bash
# 1. Clone next to your dsh checkout; install & build
git clone https://github.com/bihangchi9-creator/dsh-lark-bridge.git
cd dsh-lark-bridge
pnpm install
pnpm build # compiles src/ -> lib/ (REQUIRED before the plugin can load)
```
Then register it as a dsh **bundle** (once it's in the profile, `dsh web` loads it automatically — no `--patch`):
```bash
# 2. Link it into the profile's node_modules (bundle resolution anchor)
# macOS / Linux:
mkdir -p ~/.dsh/profiles/web/node_modules
ln -s "$(pwd)" ~/.dsh/profiles/web/node_modules/dsh-lark-bridge
# Windows PowerShell (directory junction — no admin rights needed):
# New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-lark-bridge" -Target (Get-Location).Path
# 3. Append the package name to dsh.profile.bundles in ~/.dsh/profiles/web/package.json:
# "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "lark-agent-bridge"]
```
Launch dsh (the bundle loads the plugin automatically):
```bash
# from your dsh checkout
DSH_PERMISSION_MODE=danger-full-access dsh web
```
> `DSH_PERMISSION_MODE=danger-full-access` makes the agent's approval policy `never`. This is needed because Feishu users cannot click through a local approval prompt. Only use it in an environment you trust.
## Platform notes (Windows vs macOS/Linux)
| Item | macOS / Linux | Windows |
|---|---|---|
| One-command setup | `pnpm setup` (`scripts/setup.sh`) | `pnpm setup:win` (`scripts/setup.ps1`) |
| dsh home directory | `~/.dsh` (i.e. `$HOME/.dsh`) | `%USERPROFILE%\.dsh` |
| Directory link | `ln -s` (symlink) | `New-Item -ItemType Junction` (junction — **no admin rights needed**) |
| Env var syntax | `DSH_PERMISSION_MODE=danger-full-access dsh web` | PowerShell: `$env:DSH_PERMISSION_MODE="danger-full-access"; dsh web`; cmd: `set DSH_PERMISSION_MODE=danger-full-access && dsh web` |
| Registration URL file | `~/.dsh-lark-bridge/register-url.txt` | `%USERPROFILE%\.dsh-lark-bridge\register-url.txt` |
| Run as a background service | `launchd` (macOS) / `systemd` (Linux) | Task Scheduler (`schtasks`) |
| QR registration / build / chat commands | identical everywhere | identical everywhere |
> The two setup scripts behave identically and are idempotent: preflight → build → link → register bundle.
## First run: register your bot
**Every user registers their own Feishu bot** — you cannot share an `app_secret`, as that hands over control of your bot.
On the first launch with no credentials, the plugin prints a **QR code** in the terminal (and writes the raw URL to `~/.dsh-lark-bridge/register-url.txt` for backgrounded runs). Steps:
1. Open the **Feishu mobile app**, scan the QR code.
2. Confirm creating a self-built app on your phone.
3. The plugin receives the credentials, saves them to `~/.dsh-lark-bridge/credentials.json`, and connects automatically.
4. Add the bot to a group (or DM it) and start talking.
Prefer to do it manually / re-register / switch accounts? Run the standalone wizard:
```bash
pnpm register # or: npx dsh-lark-register
```
Already have credentials? Skip the wizard entirely by exporting them:
```bash
export LARK_APP_ID=cli_xxx
export LARK_APP_SECRET=yyy
export LARK_TENANT=feishu # or `lark` for larksuite.com
```
---
## Using it in a chat
| Command | What it does |
|---|---|
| *(any text)* | A prompt to this chat's agent |
| `/help` | Show help |
| `/new` | Start a fresh session (clears this chat's context) |
| `/where` | Show this chat's project directory |
| `/models` | List available providers/models |
| `/model [provider/model]` | (owner only) Show or switch the model for this chat |
| `/preset [workspace\|read-only\|full]` | (owner only) Show or switch this chat's access tier |
| `/agent [id]` | Show this chat's runtime; owner pins it (`dsh` today). A dead line is not retargeted |
| `/whoami` | Show identity, this chat's runtime, and authorization |
| `/allow` | (owner only, group chats) Authorize this chat to use the bot |
| `/disallow` | (owner only, group chats) Revoke this chat's authorization |
In a **group** chat, `@`-mention the bot to trigger it (unless mention is disabled). In a **DM**, just send a message.
## Configuration
Every field can come from the plugin `config:` block **or** an environment variable (env is the friendlier default).
| Config | Env var | Default | Meaning |
|---|---|---|---|
| `appId` | `LARK_APP_ID` | — | Feishu app id (`cli_...`) |
| `appSecret` | `LARK_APP_SECRET` | — | Feishu app secret |
| `tenant` | `LARK_TENANT` | `feishu` | `feishu` (feishu.cn) or `lark` (larksuite.com) |
| `provider` | `DSH_LARK_PROVIDER` | dsh default | LLM provider route |
| `model` | `DSH_LARK_MODEL` | dsh default | Model for created agents |
| `workspaceRoot` | `DSH_LARK_WORKSPACE_ROOT` | `~/dsh-lark-workspaces` | Root for per-chat folders |
| `allowDm` | `DSH_LARK_ALLOW_DM` | `true` | Respond in direct messages |
| `requireMention` | `DSH_LARK_REQUIRE_MENTION` | `true` | In groups, require an `@`-mention |
| `turnTimeoutMs` | `DSH_LARK_TURN_TIMEOUT_MS` | `600000` | Hard deadline for one agent turn; timeout disposes the stuck session |
| `allowedChats` | `DSH_LARK_ALLOWED_CHATS` | `[]` | Chat ids allowed to use the bot (comma-separated). **Empty = no group allowed (fail-closed)** |
| `allowedUsers` | `DSH_LARK_ALLOWED_USERS` | `[]` | User open_ids allowed to DM the bot (comma-separated). **Empty = only the owner may DM** |
| `accessMode` | `DSH_LARK_ACCESS_MODE` | `workspace` | Default access tier: `read-only`, `workspace`, or `full` |
| `extraPresets` | `DSH_LARK_EXTRA_PRESETS` | `{}` | Extra `id:preset-name` pairs |
| `ssoGatedPresets` | `DSH_LARK_SSO_GATED_PRESETS` | `[]` | Preset ids that must pass host SSO on switch and on every turn |
| `ssoCheckCmd` | `DSH_LARK_SSO_CHECK_CMD` | — | SSO status command, parsed as argv without a shell |
| `ssoOkMarker` | `DSH_LARK_SSO_OK_MARKER` | `Authenticated` | Required text in successful SSO output |
| `presetModels` | `DSH_LARK_PRESET_MODELS` | `{}` | `presetId:provider:model` routes |
Credentials are read in this order: inline config → environment variables → the file written by the registration wizard.
## Access control (security model)
The bot's **security boundary is exactly "who may send it a message"**: every
message becomes an agent turn with host-level permissions, so access is
deny-by-default:
- **The owner always passes.** The person who scanned the QR at registration
is the owner (open_id stored in `credentials.json`); older installs are
backfilled automatically at startup via the app-info API.
- **Groups:** only chat ids listed in `DSH_LARK_ALLOWED_CHATS` may use the bot.
- **DMs:** only open_ids listed in `DSH_LARK_ALLOWED_USERS` may use the bot
(owner always allowed).
- **Fail-closed:** with an unknown owner and empty allowlists, *every* message
is denied — the denial reply includes the chat id so you can configure it.
Example:
```bash
# Allow groups oc_xxx1, oc_xxx2 and let ou_friend DM the bot
export DSH_LARK_ALLOWED_CHATS="oc_xxx1,oc_xxx2"
export DSH_LARK_ALLOWED_USERS="ou_friend"
```
> Runtime owner resolution needs the `application-info` scope; the
> registration wizard captures the open_id directly, so usually nothing extra
> is needed. **Any deployment reachable by people outside your team should
> configure the allowlists.**
## Access tiers (blast radius)
Even after the access gate passes, what an agent may touch is tiered
(`DSH_LARK_ACCESS_MODE`, default `workspace`):
| Tier | Preset | What the agent can do |
|---|---|---|
| `read-only` | `lark-readonly` | search/read files only — no writes, no shell, no network |
| `workspace` (default) | `lark-workspace` | read/write/edit files; **no shell, no network, no subagents** (no arbitrary code execution). Ships with a `lark_cli` tool: Feishu operations (IM, docs, sheets, calendar, …) via the host's authenticated `lark-cli`, spawned as an argv array with a timeout and output cap — Feishu power without opening a shell |
| `full` | deployment default | everything the host offers (shell, network, subagents) |
Presets are dsh's *toolset compositions*: the host sandbox is identical for
every preset, so the enforceable difference between tiers is **which tools
exist**. The workspace tier removes the crown jewels of the attack surface —
arbitrary code execution, network egress, and delegation.
The setup scripts (`pnpm setup` / `pnpm setup:win`) install the presets into
the harness-home user root automatically. Manual install (discovery is
uncached):
```bash
mkdir -p ~/.dsh/.agent-presets
cp -r presets/lark-workspace presets/lark-readonly ~/.dsh/.agent-presets/
```
> Further host-level hardening: dsh's `workspace-write` permission preset
> (sandbox = writes inside the workspace, wider operations require approval)
> hard-bounds fs writes — but for remote users "wider" means "denied" (nobody
> can click the approval prompt), and it changes shell behavior, so verify it
> in your target deployment before enabling. The plugin-level tiers already
> remove shell/web/subagents, which is the highest value-per-risk change.
---
## Pairs well with lark-cli
If you already use [`lark-cli`](https://github.com/larksuite/cli) / the Lark skills to drive Feishu (docs, sheets, IM, calendar…), this plugin slots in beside it: keep using `lark-cli` for structured Feishu operations, and let **Lark Agent Bridge** be the conversational coding agent living in your group chats. You're very welcome to combine the two — for example, ask the agent in a group to draft something, then use `lark-cli` skills to push it into a Feishu doc.
## Troubleshooting
- **Bot says nothing / "(no output)"** — make sure a model is resolvable (dsh's default model service must be configured, or set `DSH_LARK_MODEL`).
- **"missing Feishu credentials"** — the wizard didn't complete; re-run `pnpm register` or export `LARK_APP_ID` / `LARK_APP_SECRET`.
- **QR not visible (backgrounded dsh)** — open the URL saved at `~/.dsh-lark-bridge/register-url.txt` in a browser.
- **Group messages ignored** — you must `@`-mention the bot, or set `DSH_LARK_REQUIRE_MENTION=false`.
## Credits
**Lark Agent Bridge** (npm `lark-agent-bridge`, repo `dsh-lark-bridge`) is a creative extension of [lark-coding-agent-bridge](https://github.com/zarazhangrui/lark-coding-agent-bridge) (originally `feishu-claude-code-bridge`) by [zarazhangrui](https://github.com/zarazhangrui), by way of [trae-to-lark](https://github.com/bihangchi9-creator/trae-to-lark). This project is a native DeepSeek Harness plugin reimplementation. All original work remains under its MIT license; see [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for the full copyright chain.
## License
[MIT](./LICENSE)
Install
dsh plugin --profile web add github:bihangchi9-creator/dsh-lark-bridge
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install lark-agent-bridge from the hub
- 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.