DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins

dsh-connect

Connect

All-in-one DeepSeek Harness (DSH) chat bridge: one plugin, one config block — Feishu/Lark, Telegram, DingTalk, and Web mirror adapters plus the web-settings pane, all behind a channels[] selector

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add dsh-connect@1.0.17
READMECompatibilityVersions

Compatibility and provenance

Connect is published as dsh-connect and currently resolves to version 1.0.17. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
npm
Registry updated
11/4/2026

Versions

1.0.17stable
10/28/2026
1.0.16stable
10/17/2026
1.0.15stable
10/10/2026
Show 13 more versionsCollapse versions
1.0.14stable
10/9/2026
1.0.5stable
10/6/2026
1.0.2stable
10/4/2026
1.0.0stable
10/4/2026
0.9.2stable
10/2/2026
0.9.0stable
9/21/2026
0.8.0stable
9/3/2026
0.7.2stable
8/28/2026
0.7.1stable
8/26/2026
0.7.0stable
8/25/2026
0.6.7stable
8/24/2026
0.6.6stable
8/20/2026
0.6.4stable
8/20/2026

Related plugins

Loading related plugins…

Latest
1.0.17
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
4.5 MB
Files
224
Surface
web
License
MIT
Source
npm
GitHub
★ 0
Weekly downloads
950
Security scan
✓ v1.0.17 scan passed
Last push
10/29/2026
View source ↗Project homepage ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in integrations-communication.

Acp App@deepseek-ai/dsh-acp-appThe dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-baseRemote Web Ui@linxin666/dsh-remote-web-uiScan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and rRemote Bridge@linxin666/dsh-remote-bridgeDSH remote bridge: the host half serves protocol v1 over one exact /ios/v1 WebSocket route — session roster, history baseline, prompting, live turn increments with tool activity, interrupt, and in-process agent approval — to a device paired by a one-shot Im@xmanrui/dsh-im把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.

README

dsh-connect

English | 中文

The all-in-one plugin for connecting DeepSeek Harness (DSH) agents to chat platforms (Feishu / Lark, Telegram, DingTalk, and the Web mirror; more to come): session binding, agent driving, streaming reply bridging, interactive menu cards, local commands, and the web-settings stack.

One install, one config block: the core connect service, every channel adapter (feishu / telegram / dingtalk / web), and the web-settings stack all live in this single package. Enable the channels you use via the channels selector. The former split packages (dsh-connect-feishu, dsh-connect-telegram, dsh-connect-dingtalk, dsh-connect-web) and the dsh-connect-all bundle no longer exist.

What it looks like

The point of the plugin is the conversation, so that is what is shown first.

A question, answered from the chat. When the agent needs a decision it posts a card with the options as buttons. Tapping one — or replying with its number or text — answers the same question, and every earlier question stays answered above it:

two question cards in a Feishu conversation, each with tappable option buttons, alongside a progress card reporting 5 minutes elapsed and 37 steps completed

A long task reporting progress. The same conversation, further along: the reminder has advanced from 5 minutes to 15, and a second question follows the first answer. This is the behaviour that matters most on a long run — the card keeps reporting instead of freezing on its first message:

a Feishu conversation showing a progress reminder at 15 minutes elapsed, a question card with three option buttons, and the acknowledgement that the previous answer was received

Configuring it without typing config. /settings opens a button menu — model, reasoning effort, notification level, progress interval, language, and a config overview. Nothing here needs a YAML edit, and the same controls exist per chat, so one conversation can be tuned without changing the profile:

a Feishu conversation showing /settings opening a button menu with options for model, reasoning effort, notification settings, progress reminder, language and config overview; above it, an unrecognised command sent while a task was running received the "previous request is still processing" hint instead of being silently queued

These are Chinese-language screenshots taken in Feishu, because that is where the project is actually used. The UI strings exist in English too (language: en).

The settings pane is configured separately — the full walkthrough, with its screenshots, is further down.

Why this exists

DSH's own Web GUI is excellent, and nothing here replaces it. What it cannot do is reach you. An agent that runs for twenty minutes on a long refactor has no way to tap you on the shoulder while you are in a meeting, on a phone, or away from the machine — and you have no way to answer it when it stops to ask a question.

dsh-connect closes that loop. The agent keeps working in a real DSH session on the real host; you get a native chat client as the control surface:

  • Start work from anywhere, on any device. Feishu, Telegram and DingTalk all have mature mobile and desktop clients. You are not tunneling into a web UI or keeping a tab open; you message the bot the way you message a colleague.
  • Watch it work, or don't. The chat card streams the reasoning and tool calls live, so a long turn is legible instead of a spinner. When you would rather not read it, drop the chat to 输出重要节点 or 只输出结果 and get the answer once, whole, at the end.
  • Answer the agent where the question appears. When the agent needs a decision (ask_user_question) or permission for a risky action, the question arrives as buttons in the same conversation. Answering from chat is a first-class path, not a fallback — and the Web GUI stays fully functional, so whoever answers first wins.
  • Keep the audit trail in the conversation. Every task, result and approval is a message you can scroll, search and forward, in a tool your team already has.

Two properties make this usable rather than a demo:

  1. It is the real agent, not a chat wrapper. Sessions live in DSH and are bound 1:1 to chats; the same session is simultaneously visible in the Web GUI as a mirror. Switch between the two freely — the transcript, working directory and context are the same object, not a copy.
  2. The quiet levels are genuinely quiet. Only full sends repeating status lines. The two quieter levels send discrete events only — this is asserted by tests, because "notifications you cannot turn off" is the failure mode that makes people uninstall a bot.

Who it is for. Anyone running DSH who wants to operate agents from a chat platform — solo operators running a bot for their own workspaces, and small teams sharing one bot in group chats behind an allowlist.

Overview

dsh-connect binds a chat conversation to a DSH agent session and drives it end to end:

  • Session binding & routing — one chat ⇄ one agent session, persisted in a bindings.json route store; sessions can be created, resumed, switched, cleared and mirrored to the DSH Web GUI.
  • Streaming replies — the model's live deltas are bridged into the channel's native streaming (Feishu typewriter cards): the thinking hint opens the reasoning phase, reasoning streams live with readable paragraph breaks, tool calls appear as 🔧 progress lines, and a liveness heartbeat keeps the card moving even through long silent stretches (long first-token waits, heavy tool runs) so it never sits frozen on "Thinking…". The runner keeps two subscriptions, because 0.1.5-rc.2 split what used to be one: the durable session/event stream carries turns, tools and settlement, while the transient agent/assistant-stream frames carry the deltas — the assistant/chunk session event that used to carry both is gone.
  • Notification levels — per-chat control over how much of the process is streamed: 尽量输出过程 (full process) / 输出重要节点 (key milestones) / 只输出结果 (result only). Only full sends repeating status lines; the two quieter levels send discrete events instead (see the table below). Switch any time via the settings menu or /notify; the choice is persisted per chat and applies immediately. | Level | Thinking hint | Reasoning text | Tool-call lines | Liveness heartbeat | 5-min progress reminder | Final answer | |---|---|---|---|---|---|---| | 尽量输出过程 full | once | streamed live | streamed live | sent on a timer | sent, with the step count | streamed live | | 输出重要节点 important | once | not sent | not sent | not sent | sent, with the step count | sent once, whole, at task end | | 只输出结果 result (default) | not sent | not sent | not sent | not sent | sent, with the step count | sent once, whole, at task end |

The liveness heartbeat is repeating chatter ("still here"), so only full gets it. The 5-minute progress reminder is a status report you configured with progressTimeoutMs, so it runs at every level — set it to 0 to turn it off everywhere. Both edit the streaming card in place rather than posting a new bubble.

  • Automatic context compaction — optional, off by default. With autoCompact on, a session is compacted automatically at the end of a turn once context usage reaches autoCompactThresholdPct (default 80%), so a long unattended run does not stop on a full window. The task-end card states which rule is active. Toggle it per chat with /autocompact on|off|<1-99>.
  • Task-end stats — after every task a compact card reports the model used, input/output tokens, elapsed time and context-window usage, and suggests /compact when the context is getting full.
  • Interactive menus — button cards for status, tasks, history, goals, schedule, model/effort switching, workspace picking, language, and more (see the in-chat / commands).
  • Media handling — downloads user images/attachments, passes them to a vision-capable model (or a configured vision model) so text-only main models never stall on images.
  • Locking & queuing — a per-chat write lock is held by the chat side, and messages that arrive while another writer holds it are queued and drained on release. The lock is one-sided, and that is worth knowing before you rely on it: it is enforced only on the chat side. The Web GUI reads the mirrored session straight out of DSH's session store and never sends inbound messages through this plugin, so it is write-always — the Web side cannot be made to respect the lock from this repository, and a Web user editing the same session is not blocked by it. Running the same session from two writers at once is therefore possible, not prevented.
  • Web mirror (auto) — every Feishu conversation is automatically available in the DSH Web GUI as a mirrored session (disable with autoMirror: false).

Who is it for? Anyone running DSH who wants to operate agents from a chat platform — solo operators running a bot for their own workspaces, and small teams sharing a bot in group chats with an allowlist.

Compatibility

AspectValue
DSH version^0.2.0-rc.2 (peer @deepseek-ai/dsh-agent, dsh-llm, dsh-session)
Cordis^4.0.1
Node.js≥ 20 (ESM, NodeNext)
Last verified2026-10-10 against DSH 0.2.0-rc.2 on Windows — host load, Feishu WebSocket transport, the web-settings pane, and an end-to-end round trip against a live dsh host (the E2E suite's live leg)

Keep the peer range in step with the host. Upstream ships no changelog or migration guide, so a stale range is the only thing standing between this plugin and silent breakage: DSH 0.1.5-rc.2 deleted the Session.events accessor and the assistant/chunk event type outright, and every dsh-* package is versioned on the same line. When you upgrade DSH, bump peerDependencies (and devDependencies) for dsh-agent, dsh-llm and dsh-session together, re-run tsc, and re-run the test suite — a range that no longer overlaps the host version is the signal that the bridge needs another migration.

DSH refuses to load a plugin whose range does not cover it, so a stale range is at least loud: installing 0.9.0 on 0.2.0-rc.2 is rejected with "may cause crashes or data loss" before anything runs. 0.9.3 is the version that covers 0.2.0-rc.2; see Upgrading from 0.9.0.

The plugin runs on the DSH Host plane (process-level singleton services), not inside an agent preset.

Install / Uninstall

Plugin management is a thin wrapper over pnpm in the DSH profile:

# Install the single all-in-one plugin (core + all channel adapters + web-settings)
dsh plugin --profile web add dsh-connect

Upgrade

dsh plugin --profile web update dsh-connect

Disable — override the bundle-registered entry with disabled: true in the profile patch (see ~/.dsh/profiles/<profile>/cordis.patch.yml):

- id: connect
  name: dsh-connect
  disabled: true

Complete removal — uninstall the package and delete the data it created:

dsh plugin --profile web remove dsh-connect
# then remove the plugin data (see "Permissions & data" below):
rm -rf .dsh-connect            # binding route store (stateDir)
rm -f ~/.dsh/.dsh-connect/feishu-credentials.json

Installing in the Desktop app

The Desktop app carries its own reserved profile (desktop) and installs into it from its own UI, not from a terminal: dsh plugin --profile desktop … is refused with profile "desktop" is managed exclusively by the Electron application, because only the app's own carrier is allowed to manage that profile. In the app, the install surface is a sidebar panel named Plugins — the first entry in the sidebar's panel list — whose header button Add plugin takes a package name. The app then tells you when it applies: changes take effect on next start.

Settings is not where that lives. Settings has two read-only pages instead — Bundled plugins (the built-in list and their status) and Plugin list (session plugins / global plugins) — which is exactly where the Plugins page's own help line points: the built-in plugin list and their status are under Settings → Bundled plugins.

dsh-connect's own pane (Settings → dsh-connect) appears only once the plugin is actually installed and loaded, so a refused install leaves nothing to find there yet.

When the install resolves to an older version

Two pnpm 12 behaviours bite a fresh install, and both look like a lie: the Desktop plugin list offers the newest version, yet what lands on disk is the one before it — or the install ends in ERR_PNPM_IGNORED_BUILDS while still leaving the dependency in package.json.

  1. pnpm's built-in 24-hour release cooldown. minimumReleaseAge defaults to 1440 minutes and is non-strict: a bare add dsh-connect resolves to the newest release older than 24 hours. The version query the Desktop shows you is a read, not an install, so it is not subject to the policy — for one day after every publish the two legitimately disagree.
  2. An undecided build script. protobufjs, reached through @larksuiteoapi/node-sdk, has a build script pnpm 12 will not run until you decide about it. It reports that after writing the dependency, which is why the failure looks like a half-finished install.

Append this to the profile's pnpm-workspace.yaml ($DSH_HOME/profiles/web/pnpm-workspace.yaml; on Windows the Desktop app's profile is %USERPROFILE%\.dsh\profiles\desktop\pnpm-workspace.yaml), then install again:

minimumReleaseAgeExclude:
  - dsh-connect
allowBuilds:
  protobufjs: false

minimumReleaseAgeExclude exempts only this package; prefer it to minimumReleaseAge: 0, which turns the 24-hour supply-chain protection off for everything in the profile. allowBuilds is the spelling pnpm 12 accepts — onlyBuiltDependencies and ignoredBuiltDependencies no longer work and still fail with the same error.

The exemption cannot ship inside dsh-connect: a package cannot grant its own transitive build allowance from its own manifest, so the consuming profile is the only place it can live.

The two remedies are not interchangeable. Naming the version explicitly — dsh plugin --profile web add dsh-connect@<version> — does defeat the cooldown on its own, but it does nothing for the build script: that install still ends in ERR_PNPM_IGNORED_BUILDS, because allowBuilds is the only thing that decides protobufjs. If you would rather not edit the profile at all, the pin gets you the right version; it does not get you a clean exit.

Two more things worth knowing:

  • Uninstall before reinstalling. DSH rejects a second add of an installed plugin with already-installed, so remove it first.
  • Restart dsh (or the Desktop app) afterwards: host plugins load at process start.

Quick start

  1. Install the plugins (see above).

  2. Add the minimal config to ~/.dsh/profiles/<profile>/cordis.patch.yml — the shape is also in examples/minimal.config.json, and the fully commented version is examples/profile-cordis.patch.yml in the repository (that path is outside the published tarball, hence the absolute link). The plugin registers itself via its bundle manifest, so only override its config — do not insert it again (a duplicate id crashes dsh at boot):

    - id: connect
      name: dsh-connect
      # workDir: D:\your\workdir     # agent working directory (default: process cwd)
      config:
        channels: [feishu]            # which channels to activate; omit = all built-in
        # channelDefaults: { language: zh }   # keys applied to every channel that doesn't set its own
        feishu:
          appId: cli_xxxx
          appSecret: cli_secret_xxxx
          transport: websocket
          requireMention: true
          dmMode: open
    
  3. Start the host — dsh web (or dsh run). With no credentials configured, the feishu channel enters one-click onboarding: scan the QR / open the link from the log to authorize the bot.

  4. Send a message to the bot in Feishu. The bot replies with a streaming card; /help lists all commands; the conversation also appears in the DSH Web GUI automatically (auto-mirror).

A fully reproducible example is the examples/ folder plus the repository's Feishu setup manual (Feishu app creation, event subscriptions, publishing).

Questions and approvals in a conversation

When the agent needs a decision from you it stops and asks, and the asking happens in the chat — no switching to the Web GUI:

  • A question with options renders as a card with one button per option. One tap answers it and the card immediately moves on to the next question. Once every question is answered the card is replaced by a summary of each question and the answer given, and the buttons disappear — leaving them live made the card look like it was still waiting, and a tap on one did nothing at all, because a tap on a retired card is deliberately absorbed rather than reported as expired.
  • A question without options has no buttons to offer: the bot sends the question as a prompt and you reply in the chat. That message is the answer.
  • A tool approval (an action that needs your go-ahead) is a card too, with Allow once / Reject buttons. This one accepts only a tap — a plain chat message sent while an approval is waiting is not recorded as its result.

One chat holds one pending card at a time. A second request is handed back to the host's own path (the Web GUI) rather than fighting the first card for the same message — and so is a card that could not be delivered, or a request cancelled before you answered; in each case the chat is released, because a leaked pending entry would silently swallow your next message.

"This action is no longer active" means the card has expired — it auto-closes after 60 s idle, so ask again. A double tap, or a tap landing right after the previous question was answered, falls inside the card's redraw window and is ignored silently rather than misreported as expired.

Configuration

Configuration lives in the DSH profile patch (cordis.patch.yml) under the plugin's config:. dsh.shared.config.json in the project root (or its parent) can supply workspace/state defaults that take precedence for those keys.

dsh-connect (core)

KeyDefaultDescription
agentPresetroster defaultAgent preset id composed into each bound session. Resolution is best-effort: the configured id is tried, then standard (or the roster's first mountable row), and if neither composes the agent is built without a preset so the turn still runs. A stale id therefore degrades instead of failing every message — it is logged, not thrown.
workDirprocess cwdAbsolute working directory for each bound agent
workspaces[]Extra workspaces offered by the /dir picker
visionModelauto-detected{ provider, model } used to describe images when the main model can't see them
languagezhUser-facing message language: zh / en
allowUsers[]Fallback sender allowlist, used only by a channel that sets none of its own. Identifiers are channel-specific, so each channel has its own list on its card; a channel declaring an empty list allows everyone, overriding this
allowChats[]Fallback chat allowlist; a channel's own list wins. See allowUsers
<channel>.allowUsers[]That channel's sender allowlist, in that channel's own id format (Feishu ou_…, Telegram numeric). Empty = no restriction for this channel; absent = use the fallback above
<channel>.allowChats[]That channel's chat allowlist. Empty = every chat; absent = use the fallback
stateDir.dsh-connectDirectory holding the bindings.json route store (env DSH_CONNECT_STATE_DIR overrides)
autoMirrortrueAutomatically create a Web GUI mirror for every new session
streamHeartbeatMs

Shared (all channels)

KeyDefaultDescription
channelsall built-inWhich channels to activate: feishu / telegram / dingtalk / web. Omit to activate all built-in channels.
channelDefaults{}Keys applied to every channel that doesn't set its own (e.g. { language: "zh" }).
settingsStatePath<stateDir>/dsh-connect-settings.jsonWhere the web-settings pane mirrors non-secret config. Defaults to dsh-connect-settings.json inside stateDir, so it lands beside bindings.json and can never disagree with the stores; set it to override. The pane's authoritative store is this plugin's entry in the active profile patch (see User settings); this file is only the compatibility mirror the legacy /dsh-connect RPC reads and writes, and a pre-0.2 install's copy of it is read back once by the upgrade (see Upgrading from 0.9.0).

feishu (Feishu / Lark channel)

KeyDefaultDescription
appIdenv FEISHU_APP_IDFeishu custom app id (secret)
appSecretenv FEISHU_APP_SECRETFeishu custom app secret (secret)
transportwebsocketwebsocket = long connection (no public network); webhook needs public HTTPS
verificationToken—Webhook verification token (secret)
encryptKey—Webhook encrypt key (secret)
webhookPort9000HTTP port for the built-in webhook server when transport: "webhook"
webhookPath/URL path the Feishu event callback posts to (webhook transport)
requireMentiontrueGroups only respond when the bot is @mentioned
dmModeopenDM policy: open / allowlist / pair / disabled
languagezhUser-facing message language: zh / en

Environment variables

VariablePurpose
FEISHU_APP_ID / FEISHU_APP_SECRETFeishu credentials — preferred over putting secrets in config files
DSH_CONNECT_STATE_DIROverrides stateDir for the binding store
DSH_HOMEOverrides the ~/.dsh base for credential files

Sensitive items — appSecret, verificationToken, encryptKey, and feishu-credentials.json. Prefer environment variables or one-click onboarding; keep them out of version control.

Creating the app. Two one-click entry points (1.0.0): the button in the settings pane, and — from a terminal — starting the plugin with no credentials, which prints an onboarding link. Both create the app with its permissions and event preset and store the credentials in the DSH credential store; the pane additionally enables the channel and pins transport: websocket. The CLI path is skipped when onboarding: false or stdout is not a terminal, so a headless service never starts a flow nobody can answer. Telegram and DingTalk have no such API — the pane links to their official creation pages instead.

telegram (Telegram channel)

KeyDefaultDescription
botTokenenv TELEGRAM_BOT_TOKENTelegram bot token from @BotFather (secret)
requireMentiontrueGroups only respond when the bot is @mentioned (or replying to the bot's own message)
pollingTimeoutSeconds50getUpdates long-poll timeout in seconds
baseUrl—Optional Bot API base URL override (e.g. a local Bot API server)
languagezhUser-facing message language: zh / en

dingtalk (DingTalk channel)

KeyDefaultDescription
webhookUrlenv DINGTALK_WEBHOOK_URLGroup custom-robot webhook URL (proactive push)
secretenv DINGTALK_WEBHOOK_SECRETSigning secret (SEC…) only when signing is enabled
stream.clientId / stream.clientSecretenv DINGTALK_STREAM_CLIENT_ID / DINGTALK_STREAM_CLIENT_SECRETBidirectional stream-mode app credentials (secret, nested under stream)
stream.requireMentiontrueGroup replies need an @-mention (stream mode)
defaultAt—Default @-mentions merged into every push ({ mobiles, userIds, all })
languagezhUser-facing message language: zh / en

web (Web mirror channel)

KeyDefaultDescription
pollIntervalMs1000Mirror-session polling interval (ms)

Environment variables (FEISHU_*, TELEGRAM_*, DINGTALK_*, DSH_CONNECT_STATE_DIR, DSH_HOME) and the DSH credential store are the preferred way to supply per-channel secrets — the web settings pane writes them to the credential store and injectSecrets populates them on load.

Permissions & data

  • Files written
    • <stateDir>/bindings.json (default .dsh-connect/) — the chat ⇄ session route store (chat keys, session ids, mirror and lock state).
    • <stateDir>/dsh-connect-settings.json (default .dsh-connect/) — the non-secret compatibility mirror, see settingsStatePath.
    • <profile dir>/.dsh-connect-legacy-imported — a marker recording that the one-shot upgrade import ran. It sits beside the profile entry the import writes to (see Upgrading from 0.9.0), and its contents are a sentence saying where the settings came from; nothing is stored in it.
    • this plugin's entry in the active profile patch (profileContext.patchPath, cordis.patch.yml) — written through DSH's first-party settings service (atomic, file-locked, comment-preserving).
    • ~/.dsh/.dsh-connect/feishu-credentials.json — legacy, read-only. One-click onboarding used to save Feishu credentials here instead of the credential store, so a user who had just scanned the QR code still saw 未配置凭据 forever. Onboarding now writes to the credential store, and an existing install is backfilled from this file once on boot; after that it is never read or written again.
    • <workDir>/.dsh-connect-images/ — user images/attachments staged for the agent's tools.
    • DSH's own session logs and settings under ~/.dsh/ (sessions, settings, etc.).
  • Network
    • Feishu Open Platform: WebSocket long connection (or webhook over public HTTPS), plus HTTPS API calls (media download, cards).
    • LLM provider APIs used by DSH for the agent's model (e.g. DeepSeek), plus the optional vision model.
  • User data — message text and attachments flow through the bot to the agent session; they are stored in the DSH session log like any DSH conversation. The allowlists (allowUsers / allowChats) limit who can drive the bot.

The settings pane

dsh-connect adds its own page under Settings → dsh-connect, with a two-level navigation: a primary strip (General first, Bot channels second) over the channel view's own tab strip of collapsible cards. Each card is headed by a button, its low-frequency fields sit behind a second-level Advanced fold, and Save/status stay pinned to the bottom of the scroll region. Both strips are pinned — position: sticky at the top of the scroll region, so content scrolls under them rather than carrying them off-screen. Settings are one per row: the field grid is a single column, so the label/control pairs never reflow into two or three columns as the pane widens.

The pane opens on Bot channels, so the one-click Feishu button is zero clicks away; it sits at the very top of the Feishu card body, above App ID / App Secret. General is the ten values that used to be reachable only through chat commands — reply language, notification level, progress watchdog, working directory and extra workspaces, the allow-lists, the agent preset, mirror and heartbeat. They are editable here, but unlike channel settings they are read once when the plugin loads: every field says so, and a change takes effect after dsh restarts. A key that dsh.shared.config.json actually overrides carries a note saying the pane cannot win; the current model is shown read-only, because DSH owns it and switching it belongs inside DSH.

Every channel card is headed by two badges (1.0.5). The first is the credentials badge — whether the channel has the credentials it needs, which is what it has always meant. The second is access status, answering a different question: whether the bot is connected right now. It reads Connected / Connecting / Reconnecting (attempt N) / Idle / Running / Not running / Connection failed / Not enabled, and the card's issue bar below carries the adapter's own reason when something failed. Only Feishu reports a real transport state (LarkChannel.getConnectionStatus()); the other three have no probe to ask, so they report only what the runtime can evidence rather than inventing a "connected" nobody checked. When the host offers no status at all, the second badge is not rendered — an 「unknown」 placeholder would be a claim. The channels view polls while it is open, so a reconnect appears on its own; the poll merges the status keys only, which is what keeps it from overwriting edits you have not saved yet.

The captures below were taken before 1.0.5, so each channel header shows only the credentials badge; the access status badge described above renders beside it at runtime. The two-badge header is covered by the client-bundle tests rather than a screenshot.

Channels & credentialsGeneral
dsh-connect settings pane: the primary navigation above the channel tab strip, the Feishu card expanded with its credential fields, and folded channel cards each showing a credentials badgethe General view: four cards of settings, one per row, each noting that a change takes effect after dsh restarts

Further captures, one click each rather than inline so this section stays readable: Advanced fold · Feishu credentials · common defaults — English: overview · general · model row · advanced · defaults · Feishu · official entry.

Captured from a throwaway profile whose credentials are all placeholders. Nothing above contains a real secret — and it could not, because the host masks every stored value before it reaches the browser (see below).

Cards default to the channels you have enabled (the first one, if none are), and clicking a tab opens that card and scrolls it into view without closing the others — several open at once is a legitimate state. Collapsing a card unmounts its body rather than hiding it, which is safe because an unsaved secret you typed lives in the pane's own state, not in the card. Ticking a channel's enable box opens it too.

The Feishu card carries a Create and configure a Feishu bot in one click button (1.0.0), at the very top of the card body — above App ID / App Secret, so the automated path comes before the manual one instead of after it. The host runs Feishu's official OAuth 2.0 device-authorization flow: the button hands you a link to open in a browser, the app is created there with its permissions and message-receive event preset, the credentials go into the DSH credential store, and feishu is added to channels with transport: websocket — reachable without a public URL. Telegram and DingTalk have no equivalent API, so their cards show a link to the official creation page (@BotFather / open-dev.dingtalk.com) and an instruction to paste what it returns into the fields below. Nothing about those two is automated.

The result is a list of separate facts in the save bar, never a single "done": app created (with its appId) / credentials stored / channel enabled / whether the running channels reloaded in place / the event-subscription outcome / what is left for you in the vendor console. Credentials-stored and channel-not-reloaded are always two lines — a saved credential the live adapter has not picked up is a state you need to know about, and restarting dsh applies it. The subscription patch is best-effort by design: the SDK notes it may apply only to apps created in the developer console, and most configuration changes there need a published version before they take effect. This plugin never publishes on your behalf and never claims the subscription is live.

User settings

The web settings pane (dsh-connect under Settings) edits this plugin's entry in the active profile patch — the same ~/.dsh/profiles/<profile>/cordis.patch.yml you would edit by hand — through DSH's own first-party settings service. That service writes atomically under a file lock and preserves your comments, and the loader hot-reloads the result. Channel settings therefore take effect immediately; the general settings do not — workDir, language, notifyLevel, progressTimeoutMs, workspaces, the two allowlists, agentPreset and streamHeartbeatMs are read when the plugin loads, so they need a restart of dsh. Every one of them says so on its own row in the pane.

The fields the pane owns are declared volatile in the plugin's config schema. That declaration is what makes a save reconcile instead of remounting: the loader hands the plugin a live reference for each declared field, and a settings write commits them in place, so the running adapters pick the values up on their next message. Fields the pane does not own are simply not declared — which is the mechanism that keeps it from writing them.

Values resolve in three layers, most specific last:

  1. the schema defaults shipped with the plugin;
  2. the config the plugin is composed with (its inherited entry);
  3. this plugin's entry in the active profile patch — both what you hand-write there and what the pane saves.

A save is projected onto the declared fields only, so an undeclared key — a credential, settingsStatePath, something you added yourself — cannot reach the document even if a caller sends it. The converse is load-bearing too: the host resets a declared field that an update omits to its inherited value, so a save always writes the complete declared section. Undeclared keys you hand-wrote in that entry are preserved across a pane save, untouched.

Consequence worth knowing: pressing Save pins the channel list. channels is a declared field, so the section a save writes names every channel that was in the form at that moment — and there is no way to unpin it from the pane, because an omitted field does not clear but inherits. If you have saved at least once, a channel added by a newer release will not be enabled for you until you tick it and save again (or delete the channels key from ~/.dsh/profiles/<profile>/cordis.patch.yml by hand). The pane cannot tell the difference between "the user chose this list" and "the user has not looked since the list changed", and it will not guess on your behalf: a save that wrote only what you edited would reset every other field to its inherited value.

The pane never writes credentials. A profile patch is a plain document users are invited to paste into bug reports, so a secret you type into the pane goes to the DSH credential store (ctx.credentials) instead — which is also where one-click onboarding and the FEISHU_*-style environment variables put them. A secret you hand-wrote in the entry yourself is left where it is.

The legacy /dsh-connect HTTP RPC is retained for panel compatibility; it now reads and writes the same namespace, and mirrors non-secret config to settingsStatePath (see Shared) for older panels.

Seeing and masking stored values

The pane shows each stored credential as a read-only current value line under its input (not configured when nothing is stored), so you can confirm what you configured without retyping it. The masking happens on the host, in one shared table (src/settings/secret-disclosure.ts) that both the host and the pane read — the pane only decides whether to render the input as a password or a text field, so the display and the policy cannot drift apart.

FieldHow it is shown
appId, clientIdIn full. These are identifiers, not authenticators: they appear in every outbound API call and in the vendor console, so hiding them protects nothing.
appSecret, clientSecret, botToken, secretHead and tail only — a1b2…z9y8. Values too short to survive partial disclosure show a fixed •••••• instead.
webhookUrl (DingTalk)URL-aware. Origin, path and parameter names are kept and only the token's middle is masked, because DingTalk puts the token in the query string — masking the whole URL (https…bcde) would confirm nothing.
any other keyMasked by default.

Two properties hold regardless:

  • No usable secret crosses the wire. The value is masked before it leaves the host, so a browser tab — or a screenshot of one — never holds one.
  • A mask can never be written back. The preview is text beside the input, never the input's value. Inputs always start blank; blank means "leave the stored value alone", so saving the config alone writes no credential at all.

The pane's own strings (channel names, field labels, option text, status) all come from client/locale.mjs, which ships zh and en. A test asserts both languages cover exactly the same key set — the host resolves a missing key by silently falling back to the other language, so an untranslated string shows up as mixed-language text rather than as an error.

When a save has something to report

已保存 / Saved means the write landed, and nothing more. When there is something else to say, the save bar grows a list above the button — it is the only part of the pane that is always on screen, and a channel card can be folded or scrolled past, which is exactly when a bare "Saved" next to nothing else misleads.

LineWhat it means
· Credential state unknownThe credential store could not be read for that channel — usually a permission problem or a malformed store. This is not not configured: a secret may well be stored. Do not retype it until you have checked the store, which is why the badge on that channel's card turns amber rather than red.
· Channel failed to start: plus the adapter's own messageThe adapter's start() threw. The message is passed through verbatim, because it is the only part that names the credential or option actually at fault.
The credential was saved, but the running channels did not reload it — restart dsh to be sure.The save partially applied: the secret is in the credential store, but the reconcile that hands it to the running adapters failed, so the bot keeps using the old one until a restart.

Warnings accumulate across the several calls one Save makes (config first, then each channel's credentials); the error lines are re-derived from the world on every call. A warning is about the call that raised it, so it would otherwise be erased by the next channel's save — and the one that mattered is the one you would lose.

Credential groups

A channel counts as configured when any one of its credential groups is fully satisfied, and each group is satisfied only when all of its refs are set — all-of within a group, any-of across groups. A channel with no groups (web) is configured by definition and never shows a warning badge.

The grouping exists because a channel can have more than one mutually exclusive way to authenticate, and requiring all of them would wrongly report a working bot as unconfigured:

ChannelGroups
feishuapp id + app secret
telegrambot token
dingtalkwebhook URL + sign secret — or — stream client id + client secret
webnone

So a DingTalk bot using only webhook push (no stream credentials) is correctly reported as configured, as is one using only stream mode.

Upgrading from 0.9.0

Applies to the in-repo 0.9.1 as well — it was committed but never published to npm, so 0.9.0 is the version users are actually upgrading from.

DSH 0.2 keeps per-plugin settings in the profile patch, not in $DSH_HOME/settings.yaml, and it does not know the old document's dsh-connect: section — its own migration renames that file and imports the sections it recognises, leaving ours to be dropped with a warning. Without help, an upgrading user's channels keep working (their config is in the patch) but every pane-only choice silently reverts to its default the first time the pane opens.

So 0.9.2 imports it once, on the first boot after the upgrade:

  • It looks for the dsh-connect: section in $DSH_HOME/settings.yaml first, then in settings.yaml.imported (where the host's own migration renames the document, and which may have happened before or after this ran), and finally in this plugin's own dsh-connect-settings.json — the fallback store a user who never had a live settings peer would be carrying all their choices in.
  • The section is projected onto the fields the pane owns, so credentials cannot travel: they are in the credential store, and anything else in that file stays where it is.
  • It merges, it does not replace. The values in force are the base and the legacy values are layered on top, because the host resets a declared field that an update omits — an import carrying only per-channel keys would otherwise clear channels and switch every adapter off.
  • It writes through the same path a pane save uses, so the running adapters reconcile immediately — no restart.
  • Nothing is deleted or renamed. Unlike the host's own import, ours never writes to settings.yaml.
  • The outcome is recorded once in a marker named after the profile entry itself (profileContext.patchPath), so it lives at <profile dir>/.dsh-connect-legacy-imported — including the benign "there was nothing to import" case, so a later boot cannot re-apply the old values over edits you have made since. The anchor is the entry, not the state file it used to sit beside: the state path is yours to move (stateDir, DSH_CONNECT_STATE_DIR, settingsStatePath) and to delete, and either would have made the next boot believe the import had never run. A profile directory moves only if the profile itself does, which is the one case where re-importing is right.

The retry rule is narrower than "any failure is retried", and deliberately so. Three outcomes are final and get the marker: a document that is not there, a document with no dsh-connect: section, and a successful import. Everything else — a document that will not parse, one that is there but cannot be read (a permission, or a settings.yaml that is really a directory), a refused write, a host with no settings service — imports nothing, leaves both files alone, and writes no marker, so the next boot simply tries again once you have fixed the cause. That last one is why "not there" and "could not be read" are told apart: until 0.9.2 they were the same outcome, so a single unreadable document ended the migration for good, silently.

Each of those failures is one connect: … line naming the file and the reason; the troubleshooting table below lists them. A marker that cannot be written is reported too — without it every boot would re-run the migration and layer the legacy values back over your newer edits. If you never used the pane, none of this is visible.

Troubleshooting

Logs come from the DSH host logger (run dsh web in a terminal); plugin messages are prefixed connect: / connect-feishu:.

SymptomLikely cause / fix
Installing dsh-connect@0.9.0 on DSH 0.2.0-rc.2 is refused: "dsh-connect@0.9.0 与 DSH 0.2.0-rc.2 不兼容 … 运行它可能导致崩溃或数据丢失"Not a bug and not a warning to click past: DSH's compatibility gate rejects any plugin whose declared peer range does not cover the running host, and 0.9.0 predates the 0.2.0 line. Install 0.9.3 (or newer), whose peers require ^0.2.0-rc.2.
In the Desktop app you cannot find where to install a plugin — the entry seems to have gone after a restartThe install surface is the Plugins panel in the sidebar (first in the panel list), not Settings; Settings only lists plugins, read-only. See Installing in the Desktop app. If the panel opens but says this deployment runs without a manageable profile, the host did not expose its plugin manager, so the page is inert — restart the app. And Settings → dsh-connect can only exist once the plugin is installed and loaded, so after a refused install there is legitimately nothing to find.
The install reports the newest version but lands on the previous one, or ends in ERR_PNPM_IGNORED_BUILDSBoth are pnpm 12 behaviours rather than packaging faults: a bare add hits the 1440-minute minimumReleaseAge cooldown, and protobufjs's build script is undecided. Two lines in the profile's pnpm-workspace.yaml clear both — see When the install resolves to an older version.
connect-feishu: adapter init failed / start failedBad credentials, app not published, or network blocked. Check appId/appSecret, re-run onboarding, verify the bot is online in the Feishu console.
connect: resume of <id> failed, creating fresh sessionThe persisted session could not be resumed (missing workdir, persistence issue). Check workDir and ~/.dsh/sessions. The chat is told too, since 0.9.3: you get 「无法恢复上次的会话,已为你开启一个新会话继续」 with the reason, because the log is the one place a chat user never looks and a reply that arrives with no memory of the conversation looks like the bot forgetting rather than a session that moved. The old session is not lost — it is still in the session store and still openable in the Web GUI.

Rollback — reinstall a previous release (dsh plugin --profile web add dsh-connect@<version> after removing the current one), or git checkout the pinned commit in a source install.

Development

This is a pnpm workspace; dsh-connect is the single package under packages/:

packages/
  connect/          # this package — the all-in-one plugin
    src/            # core: runner, service, binding, commands, menus, chat keys …
    src/channels/   # channel adapters: feishu / telegram / dingtalk / web
    src/settings/   # web-settings stack: host RPC, credential store, disclosure policy
    client/         # web-settings frontend plugin + its pure, testable modules
    test/           # node:test suites (run-all.mjs imports every suite)
    docs/images/    # screenshots used by this README
    examples/       # minimal.config.json
pnpm install

# build & typecheck the package
pnpm --filter dsh-connect build
pnpm --filter dsh-connect typecheck

# unit tests (node:test) — run-all.mjs imports every suite in-process
pnpm test
# or run one suite
node packages/connect/test/unit.test.mjs

Structure — src/runner.ts owns the per-chat agent driver and the streaming bridge (applyStreamChunk is the pure, unit-tested chunk assembler); src/service.ts owns the adapter registry and routing; src/channels/ holds the feishu / telegram / dingtalk / web channel adapters; src/settings/ holds the web-settings stack (host RPC, credential store, disclosure policy); src/binding.ts is the route store.

Two things that used to live in src/ moved to client/ so they could be unit-tested without React: client/locale.mjs holds every user-visible pane string in zh and en (keep the key sets identical — the host resolves a missing key by silently rendering the other language, so an untranslated string shows up as half-English text, not as an error), and client/panel-state.mjs holds the card open/advanced rules. Both are plain ESM with no dependencies and are asserted directly in test/locale.test.mjs / test/panel-state.test.mjs.

Contributing — PRs welcome at github.com/IvanWu2015/dsh-connect. For user-facing pane strings, add the key to both zh and en in client/locale.mjs, then rebuild the bundle (node scripts/build-client.mjs) — test/client-bundle.test.mjs runs the built artifact and fails if it is stale. Release notes live in CHANGELOG.md; see docs/PUBLISHING.md for the release flow.

License & security

  • License: MIT (see LICENSE).
  • Security: report vulnerabilities privately — use the GitHub security advisory flow on the repository, or contact the maintainer via the email listed on the GitHub profile. Please do not open public issues for credential exposure. Treat appSecret / verificationToken / encryptKey / feishu-credentials.json as secrets: prefer environment variables, and never commit them.
60000
Liveness heartbeat interval (ms) for the streaming card; 0 disables it
autoCompactfalseCompact the session automatically at the end of a turn once context usage reaches autoCompactThresholdPct. Off by default: compaction rewrites the history. Per-chat override via /autocompact on|off
autoCompactThresholdPct80Context-window usage (the same percentage the task-end card reports) at which autoCompact fires; clamped to 1–99. Per-chat override via /autocompact <1-99>
notifyLevelresultDefault notification level: full (stream everything) / important (key milestones) / result (answer only, the default); per-chat override via settings menu or /notify
progressTimeoutMs300000Proactive progress-notice interval (ms): when a turn has sent no standalone card/text for this long, a status card reports the latest milestone; 0 disables; per-chat override via settings menu or /progress. Runs at every notification level — it is a status report you configured, not liveness chatter (which is the separate, full-only heartbeat). Its milestone is a tool-free step count. 0 disables it everywhere
connect: binding store writes to <file> are working again / … cannot persist bindings …The binding file could not be written, so existing chats will not be resumed after a restart and each will start a new session. Reported once when the write breaks and once when it recovers — on its own it is a disk or permission problem, but if you see it and the resume notice above, the two are the same cause.
connect: summary card could not be delivered … / connect: stats card could not be delivered …The turn finished but its result card did not reach the chat, so the streaming card stays on its last frame. Usually a bot-API or permission problem on the channel side; the answer itself is in the session and visible in the Web GUI.
A reminder was accepted but never firedFixed in 0.9.3: the store's write failure was swallowed, so 「已设置」 could be sent for a reminder that would not survive a restart. The confirmation now carries a second line saying so. Upgrade.
The bot answers every message with a raw agent-presets: preset "…" not found line, and nothing reaches the agentA stale agent-presets.default in $DSH_HOME/settings.yaml names an id no installed build ships. Fixed in 0.9.0, which retries standard and logs the decision rather than failing the turn; on an older build, set the key to a shipped id (standard).
Session-locked noticesAnother client (Feishu or Web) holds the write lock. Use /unlock or wait for the lock timeout.
Model switch in the Web GUI appears ignoredFixed in 0.9.0: the plugin no longer pins a static default model over the Web GUI's session selection. Upgrade, then restart dsh web.
[用户发送了图片,但下载失败…]Feishu im:resource permission is missing on the app; grant it and re-approve.
Streaming reply is one unbroken blobFixed in 0.9.0: block boundaries and the reasoning/answer split now insert blank lines (and reasoning soft breaks are expanded for Feishu cards). Upgrade, then restart dsh web.
Card frozen on "Thinking…" with no progress on a long taskFixed in 0.9.0: reasoning now streams live, tool calls show as 🔧 progress lines, and a liveness heartbeat updates the card during silent stretches. Upgrade, then restart dsh web.
The agent offers options / asks for tool approval and nothing appears in FeishuFixed in 0.9.2 (the in-repo 0.9.1 was never published): the bridge subscribed to a host service that does not exist, so every question fell silently back to the host. Upgrade, then restart dsh web.
Tapping a card button says it is no longer active, on a card that was just postedFixed in 0.9.2 (the in-repo 0.9.1 was never published): a tap landing while the card was being redrawn (a double tap, or one right after the previous question was answered) was misread as a stale action. Upgrade, then restart dsh web.
connect: the legacy settings at <path> could not be parsed …The pre-0.2 document has a YAML error, so the one-shot migration (Upgrading from 0.9.0) skipped it and left it in place. Fix the YAML and restart; nothing is imported until then, and no marker is written, so the retry is automatic.
connect: could not import the legacy dsh-connect settings from <path> …The migration found the section but DSH refused the write (usually a value that fails validation). The section is still in the file — fix the named field and restart.
connect: could not read the legacy settings candidate at <path> …The candidate is there but could not be read: a permission, a path that is really a directory, or a ~ in $DSH_HOME that was not expanded. This is the one failure that does not mark the migration done — nothing is imported, nothing is marked, and the next start retries on its own, so fixing the cause is all that is needed. The distinction from "not there" is the point: the two were the same outcome until 0.9.2, so a single EACCES ended the entire migration silently and permanently.
connect: could not write the one-shot import marker at <path> …The import itself succeeded; the marker that records it could not be written (usually a read-only home). Not harmless: with no marker every start re-runs the whole migration and layers the legacy values back over whatever you changed in the pane after upgrading — which shows up as settings reverting on their own. Fix the profile directory's write permission, or create the marker file by hand.
connect: the import marker at <path> could not be read …A marker exists but cannot be read. It is treated as already imported and reported: better to skip an import than to re-apply old values over your newer settings. Delete the marker and restart to trigger the import again.
Saving the pane fails with Configuration for "connect" is overridden by a home patch or command-line overlayThe pane writes the profile patch, but resolution layers bundle → profile → $DSH_HOME/cordis.patch.yml → --patch, so a value set in one of the last two wins over anything the pane saves and DSH refuses the write rather than let a save that could never take effect look successful. Edit the home patch (or drop the overlay) if you want the pane to own these settings.
The pane takes a save but the channel still reads Credentials missing, and no credentials status line ever appearsFixed in 1.0.4: the plugin's Config schema was a named export only, so the loader never attached it to the plugin object, and DSH refused every pane write with No configurable plugin entry "connect". The appId/appSecret you had just typed were dropped and the channel stayed unconfigured — and because the refusal happened before the credential write, nothing on screen said which of the two failed. 1.0.3 ships in this state and cannot save any setting at all; upgrade. On 1.0.4 the save bar names each half separately (settings vs. credential) and appends the host's own error code.
A channel setting you never touched is gone after saving the paneFixed in 1.0.5. A save rewrites the channel's whole section, and the section was projected through a per-field allowlist — so any declared key the pane did not render was reset, not preserved, the first time you saved anything. Feishu lost threadIsolation and onboarding, DingTalk lost stream.url and stream.requireMention. DingTalk's defaultAt was worse than lost: rendered as [object Object] and then written back over the real object as that string. The declared keys now each fall into exactly one of editable, secret or carried through, and a completeness test derives the check from the schema so a new key cannot be forgotten. Upgrade and re-set the values the pane dropped.
The Feishu card shows a credentials badge but you cannot tell whether the bot is actually connectedFixed in 1.0.5, which adds a second access status badge to every channel card. Before it, the header answered only 「are credentials configured」 — a channel with a valid appSecret and a dead socket looked exactly like a healthy one. Upgrade, then look for the badge beside the credentials one; the channels view also refreshes it on its own while open.
/send <path> pastes the path as text instead of sending the fileFixed in 1.0.5: the retry wrapper rebuilt the adapter as an object literal carrying a fixed set of members, and sendFile was not among them, so adapter.sendFile was always undefined on the wrapper the runner holds and every /send silently degraded to a text message. Upgrade.
Menu cards don't update / expireCards auto-close after 60 s idle by design; re-open the menu. Question and approval cards behave the same — see Questions and approvals in a conversation.
Progress notices stop, but /status still says a task is runningFixed in 1.0.7. Two faults in /status, not in the notices: the reported completion time was stamped with the clock at the moment you asked (so any old turn looked freshly finished), and a task that nothing was driving was reported as 「正在处理任务」. If you are on 1.0.7 and still see this, the task really was interrupted — the status now says so explicitly, and the progress notices live inside the turn that drives them, so a restart mid-task ends them by construction. Re-send the message to start a fresh turn.