
DSH Lark
☆ 0Lark/Feishu IM bot channel for DeepSeek Harness: chats drive agents, replies and approvals return as messages and cards | 飞书机器人 DSH 插件
Get this plugin
Review the source, then continue to the publisher.
dsh plugin add @dsh-contrib/dsh-lark-channel@latestAbout this plugin
Source snapshot 8/13/2026@dsh-contrib/dsh-lark-channel
English | 中文
A Lark/Feishu IM bot channel plugin for DeepSeek Harness. Each chat (direct message or group) drives its own DSH agent; assistant output streams back as a typewriter card, and host approval questions become interactive cards decided by button clicks.
Built from the standalone DSH plugin template: the repository is self-contained, builds against public cordis/schemastery, and reaches host services (agents, agentPresets, agentDefaultModel, settings, invariants, loader) through narrow local contracts in src/host.ts. Transport uses @larksuite/channel (WebSocket long connection; no public callback URL needed).
Behavior
- Inbound: each
messageevent is routed bychatId. The first message in a chat creates an agent (agents.create) whose session id islark-<chatId>-<uuid>; later messages becomeagent.followup()turns. Group messages are prefixed with the sender name so the model can tell voices apart. - Outbound (
output):stream(default) opens one streaming typewriter card per turn — text deltas stream token by token, tool activity appears inline, and each committed step corrects the card if the model's raw text carried protocol markup.markdowninstead sends one plain message per completed step, needs no card permissions, and is also the automatic fallback when the transport rejects a stream. Reasoning deltas never reach the chat in either mode; a turn that ends in an error is reported on its card (or as a⚠️message when the turn produced none). - Composition: each chat agent joins an agent preset (
preset, default the roster's own default) inside creationsetup. A deployment with a preset roster keeps every model-facing row on the agent plane, so an agent that joins nothing would reach the model with NO tools — and a model with no tools emits its native tool-call markup as plain text instead of calling anything. An unknown preset fails the creation and is reported to the chat rather than running a toolless session. - Approvals:
approval/requestquestions for agents owned by this plugin become an interactive card with 允许一次 / 拒绝 buttons; the click settles the host outcome (allowed-once/rejected), the card is rewritten with the decision, and a withdrawn question settlescancelled. Questions about other agents delegate to the next composed answerer. The listener is registered prepended, which is load-bearing when the Web app is composed alongside: its BFF claims every audited approval and never delegates, so in arrival order a chat-driven approval would surface in a browser nobody is watching while the chat waited forever. - Policy: group chats respond only when the bot is @-mentioned (
requireMention, default on). OptionalgroupAllowlist/dmAllowlistrestrict which chats and senders are processed. Dedup, per-chat serialization, and reconnection are handled by the transport SDK. - Lifecycle: every registration is owned by the plugin fiber. Disposal disconnects the transport, disposes every chat agent, and settles open approval cards as
cancelled.
Configuration
| Field | Default | Meaning |
|---|---|---|
appId, appSecret | first-boot QR onboarding | Lark/Feishu app credentials. Layering: entry config (e.g. env via !!js) under the user settings document; with neither present, boot logs a QR URL, scanning it creates the app through the official registerApp flow (event subscription included), and the credentials persist through the host settings service. |
domain | Feishu | Open-platform domain; set https://open.larksuite.com for Lark. |
cwd | host process cwd | Absolute workspace directory for chat-driven agents. |
provider, model | host agentDefaultModel | Model route for chat agents. |
preset | roster default | Agent preset chat agents join, when the deployment composes a roster. |
output | stream | stream (typewriter card per turn) or markdown (one plain message per step). |
showToolActivity | true | Show which tools ran, inline in the streaming card. Ignored by markdown, where each line would cost its own chat message. |
requireMention | true | In group chats, only respond when @-mentioned. |
groupAllowlist | [] | When non-empty, only these oc_… chat ids are processed. |
dmAllowlist | [] | When non-empty, DMs are limited to these ou_… sender ids. |
See cordis.patch.yml for the composed rows; credentials may use !!js process.env.… in the patch.
Install into a DSH profile
# from a checkout of this repository
pnpm install && pnpm run build
# add it to the profile you run (path, Git URL, or a published tarball)
dsh plugin --profile web add <repo-path-or-git-url>
export LARK_APP_ID=cli_…
export LARK_APP_SECRET=…
dsh web
The package manifest declares dsh.bundle.patch: ./cordis.patch.yml; installing it into a profile applies the patch rows over the profile composition.
Onboarding and end-to-end test
- Compose:
dsh plugin --profile web add <repo-path>, then verify the rows withdsh --profile web --dump-config(look for thelark-channelid). - Boot: run
dsh web; add a DeepSeek API key under Settings → Models on first use. With no credentials configured, the log prints a QR URL — scan it in Feishu to create the app (the official flow configures event subscription). The credentials persist in the host settings document and the channel connects without a restart. Deployments that prefer environment variables instead can runpnpm run register-apponce and exportLARK_APP_ID/LARK_APP_SECRET. - Drive: DM the bot (or @-mention it in a group). The first message creates the chat's agent and the reply streams into a card. Ask it to run a shell command to see the approval card, and click 允许一次.
To reset stored credentials, remove the lark-channel section from the settings document (its path prints via the host settings surface); the settings layer overrides entry-config values while present.
The invariant companion row is not part of the default patch — the shipped web profile composes no invariants service, and a row waiting on an absent service fails the whole tree at boot (cordis.patch.yml documents the diagnostic-composition row).
Development
pnpm install
pnpm run verify:self-contained
pnpm run typecheck
pnpm test
pnpm run build
Tests run against a fake transport port and a fake agents registry (tests/harness.ts); no Lark credentials are needed. The production transport is substituted through internals.createPort in src/runtime.ts.
Known Limitations and Deferred Work
- Inbound non-text content (images, files, audio) is passed through as the SDK's normalized text only; resources are not downloaded or attached to the agent.
- Chat agents live until plugin disposal; idle eviction and session resume across restarts are deferred.
- The model sees group messages as
sender: textsingle-user turns; there is no per-sender identity beyond the prefix. - Per-session model switching is not installed: chat agents route through
agentOptionsalone, so the host's scoped model-selection commands do not apply to them. - Tool activity shows the tool name only. Arguments can carry secrets or large payloads, so they stay off the chat.