DSH Plugins Marketplace

DSH Plugins

Plugins

/

Notifications & Integrations

/

dsh-im-global

d

dsh-im-global

Manifest valid

Telegram, Discord, Slack, Mattermost, Zulip and Matrix channels in one plugin, with per-conversation sessions that survive restarts, a Settings page that adds, tests and saves channels without YAML, a

UI (client)hasBundlePatch

dsh-im-global

Global IM channels for DeepSeek Harness — Telegram, Discord, Slack, Mattermost, Zulip and Matrix in one plugin, with per-conversation sessions that survive restarts.

⚠️ Security: closed by default. A channel with no allowlist configured rejects every message (fail-closed) and shows as locked until you add the sender's user ids. See Security: the allowlist is fail-closed before connecting a bot.

It shares its shape with the DingTalk / QQ / WeChat / Feishu / WeCom gateway (@lijian-ui/dsh-im-gateway): one plugin, a channels: [...] list, one adapter per platform.

Why this exists

dsh-telegram proved the model but is a single-platform channel owned by a third party: a single hard-coded language, one platform per package, and no way to add a second one without a second copy of the same session plumbing.

This package factors that plumbing out:

lib/index.js            plugin: config, channel lifecycle, routing, commands
lib/core/bindings.js    conversation -> session (durable, restart-safe)
lib/core/reply.js       turn -> message delivery (streaming, platform-agnostic)
lib/core/commands.js    /new /status /help parsing
lib/adapters/telegram.js Bot API long polling
lib/adapters/discord.js Gateway WebSocket + REST
lib/adapters/slack.js   Socket Mode + Web API
lib/adapters/mattermost.js WebSocket + REST (self-hosted)
lib/adapters/zulip.js   event-queue long polling
lib/adapters/matrix.js  client-server /sync long polling

Adding a platform is one adapter file. Nothing in core/ changes.

Platforms

typeinboundneeds a public URL?credentials
telegramBot API long pollingnobot token (@BotFather)
discordGateway WebSocketnobot token
slackSocket Mode WebSocketnobot token xoxb-… + app token xapp-…
matrix/sync long pollingnoaccess token (+ homeserver URL)
mattermostWebSocket + RESTnopersonal access token + base URL
zulipevent-queue long pollingno<bot email>:<api key> + base URL

WhatsApp / Teams / LINE / Messenger are not covered: they only deliver events to a public HTTPS callback, which is a different hosting shape rather than another adapter.

Install

From the plugin market

Add this index under Plugins → Discover → Manage index sources:

https://raw.githubusercontent.com/monkey-sking/dsh-im-global/main/index.json

From a local checkout

plugin_manager({ action: "install_bundle", target: "/Users/you/.local/share/dsh-im-global" })

The bundle inserts one row, im-global, into the profile composition.

Configure (two ways)

Settings page (no YAML, no restart)

Enable the bundle once, then open Settings → Chat channels. The page manages the channel list: add/remove a channel, pick its type, name the credential, set the server URL, test the credential against the platform, and save. Saving restarts the affected channels in place — the process is not restarted.

Credentials are never edited there. The page only stores the credential name; the secret stays in the credential store.

Saving a token directly (builds without a credentials page)

Each channel card also has a Token(可选,直接保存) field. Paste the token and press 保存 token — the plugin writes it to ~/.dsh/dsh-im-global/secrets.json (mode 0600) and resolves it as a fallback after ctx.credentials and the environment variable of the same name. Useful when the host has no credentials UI: the channel then starts without touching the host credential store.

cordis.patch.yml

## Configure below documents the file-based route, which stays the floor: a value pinned there is used until the settings page saves a channel list of its own.

Configure

Tokens live in ctx.credentials, never in the config file. Create them first — via the credentials settings page or the host's credential store — under the names the channel references:

platformdefault credentialwhat to paste
TelegramTELEGRAM_BOT_TOKENBotFather → /newbot → the token it prints
DiscordDISCORD_BOT_TOKENBot → Token (Developer Portal → Bot → Reset Token)
SlackSLACK_BOT_TOKENOAuth & Permissions → Bot User OAuth Token (xoxb-…)
SlackSLACK_APP_TOKENBasic Information → App-Level Tokens (xapp-…, scope connections:write)
MatrixMATRIX_ACCESS_TOKENany account's access token
MattermostMATTERMOST_TOKENProfile → Security → Personal Access Tokens
ZulipZULIP_BOT_TOKEN<bot email>:<api key> from Settings → Bots

Then edit the im-global row in the profile's cordis.patch.yml:

- id: im-global
  config:
    channels:
      - id: telegram-main
        type: telegram
        tokenRef: TELEGRAM_BOT_TOKEN
      - id: discord-main
        type: discord
        tokenRef: DISCORD_BOT_TOKEN
        cwd: /Users/you/dev/workspace
      - id: slack-main
        type: slack
        tokenRef: SLACK_BOT_TOKEN
        appTokenRef: SLACK_APP_TOKEN
      - id: matrix-main
        type: matrix
        tokenRef: MATRIX_ACCESS_TOKEN
        homeserverUrl: https://matrix.org

Settings storage

The page writes ~/.dsh/dsh-im-global/settings.json (atomic temp-file + rename). When that file exists its channels list wins over the composition's; delete it to fall back to cordis.patch.yml.

Security: the allowlist is fail-closed

A chat bridge turns anyone who can reach the bot into an agent driver, so this plugin is closed by default. A channel with no allowlist configured accepts nothing — not even the owner — and shows as locked on the Settings page with every inbound message dropped and logged.

To open a channel, put the sender's platform user ids on the list, either in the Settings page (允许的用户 id field, comma separated) or in the composition:

- id: im-global
  config:
    allowedUsers: [123456789]          # plugin-level default for every channel
    channels:
      - id: telegram-main
        type: telegram
        tokenRef: TELEGRAM_BOT_TOKEN
        allowedUsers: [987654321]      # per-channel list beats the plugin-level

A per-channel allowedUsers wins; otherwise the plugin-level allowedUsers applies; if neither is set the channel stays locked. How to find a user id: Telegram = the numeric id a bot sees, Discord = the numeric snowflake, Slack = the member id, Matrix = the full @user:server, Mattermost/Zulip = the numeric user id. The bot reads them from the userId it logs per inbound message.

This is the only gate between the platform and your agent. Treat it like a firewall rule: configure it before exposing a bot, and keep it populated as you add users.

Telegram specifics

  • Create the bot with @BotFather and copy the token.

  • Only one process may long-poll a token. A second getUpdates gets 409 Conflict, so disable any other Telegram channel on the same bot before starting this one (the profile patch does that with - id: telegram / disabled: true).

  • Coming from another Telegram plugin? Carry the conversation over:

    node scripts/migrate-telegram-bindings.mjs telegram-main
    

    It copies the old chat -> session mapping, so the same chat resumes the same Harness session instead of starting from zero.

Mattermost / Zulip specifics

  • Both are self-hosted, so baseUrl is required (https://chat.example.com).
  • Mattermost needs a personal access token and the socket authenticates with it; the adapter pings every 30 s because the socket goes quiet otherwise.
  • Zulip's event queue expires on its own schedule; BAD_EVENT_QUEUE_ID is handled by re-registering rather than surfacing an error.
  • Zulip replies go to the same stream and topic; a DM replies to the sender.

Discord specifics

  • Enable the Message Content privileged intent (Bot → Privileged Gateway Intents). Without it the bot receives every message with empty content.
  • Invite the bot with bot scope and the Send Messages / Read Message History / Attach Files permissions.
  • Use !new in chat: a leading / is reserved by Discord's own slash-command UI, so /new may be swallowed by the client. !new, !status, !help work everywhere; /… is accepted too in DMs and other platforms.

Slack specifics

  • Enable Socket Mode, create an app-level token with connections:write.
  • Event subscriptions: message.channels, message.im (and message.groups for private channels).
  • Reinstall the app after changing scopes or the bot will silently stop receiving events.

Matrix specifics

  • Any account works; the bot answers in any room it can read.
  • The adapter carries since forward, so a restart does not replay history.

In chat

commandeffect
!new / /newrotate to a new session (the old one stays readable in the Web UI)
!statussession id, state, working directory
!helpcommand list

Plain messages go to the agent. Images are attached for the model when the platform lets the adapter download them.

Design notes

  • Session identity is platform identity. The session id is derived from type:conversationId (discord:1234…), so a restart resumes the same conversation instead of starting a blank one.
  • Create-collision falls back to resume. If the log is on disk but the existence probe missed it, the create path retries as a resume instead of failing every message — the exact bug that broke the Telegram channel.
  • One preview message per turn. Streaming edits one message in place on a 1.6 s cadence rather than posting a message per token.
  • Bots never answer themselves. Adapters drop bot authors, webhooks and their own user id before routing.
  • Platform-correct markdown. Tables become aligned monospace blocks, headings become bold, mass mentions are neutralized — a raw pipe table on a phone reads as corruption.

Quoted messages

A reply that quotes an earlier message is passed to the model as a short prefix, so "这个怎么改" has a referent:

[引用 你 的消息]
> 配置文件在 ~/.dsh/ 下面
这个怎么改
PlatformQuoted content on the wireStatus
Telegramreply_to_message (full message)✅ inline
Discordreferenced_message (full message)✅ inline
Slacknot inlined; thread_ts → conversations.replies✅ thread parent (1 call, cached 2 min)
Matrixevent id only; m.in_reply_to → /context✅ exact quoted event (cached 2 min)
Mattermostroot_id → GET /posts/{id}✅ thread root (cached 2 min)
Zulipmarkdown blockquote in the body✅ parsed inline, no extra call

Telegram, Discord and Zulip need no extra request. Slack, Matrix and Mattermost fetch the referenced message once and cache it for two minutes, because their protocols only carry an id.

Interactive questions

When the agent asks (ask_user_question), the question is answered in the chat instead of only in the Web UI: the harness exposes questions through the user-questions/request waterfall, and this channel claims the ones belonging to its own sessions (anything else falls through to the Web UI).

Platform support for interactive answers:

PlatformNative controls it offersIn this plugin
TelegramInline keyboards (callback buttons, URL buttons, web_app). Rows ≤8, ≤100 buttons✅ Buttons — callback_query + answerCallbackQuery, keyboard collapsed after the answer
DiscordMessage components: buttons (≤5/row, ≤25/message), select menus, modals. Replies need an interaction ACK within 3 s⏳ Text fallback (1) … 2) …)
SlackBlock Kit actions: buttons (≤25/block), select menus, overflow, modals. Arrives over Socket Mode — no public URL⏳ Text fallback
MattermostInteractive message attachments (actions), but the click posts to an integration URL⏳ Text fallback
MatrixNo buttons; emoji reactions or plain text⏳ Text fallback
ZulipNo buttons; reactions and plain text⏳ Text fallback

The text fallback works everywhere and needs nothing from the platform: the question is posted numbered, and a reply of 2 (or the option label) answers it. While a question is open it owns the next message — typing 2 answers instead of starting a new turn.

Approvals (user-approval) are a separate service and are not routed to chat yet; a tool that needs approval still surfaces in the Web UI.

Reliability notes

Four failure modes are handled explicitly, because each one looked like "the bot is broken" from the chat side:

  • Archived session. The kernel rejects every step of an archived session (archived-session-gate), which surfaces as a blocked turn: the message is accepted and answered with silence. The channel detects that, unarchives the session, and falls back to a fresh session only if unarchiving is refused.
  • Long tool-using turns. This harness publishes finished step messages rather than raw deltas, so the streaming preview is driven from both. Without that a multi-step turn shows nothing until it ends.
  • Plugin reloads. The disposer is registered synchronously and marks the instance dead before stopping adapters. An async effect hands cordis its disposer only after startup resolves, so a reload during startup left the old poller alive: two pollers on one bot token fight over getUpdates (409 Conflict) and silently eat each other's messages.
  • Missing model wiring. agentOptions alone leaves the agent without a provider/model at prompt assembly time; installModelSelection couples it to both assembly and request routing. Without it a turn ends in milliseconds.

Test

node --test test/*.test.js

Comments

Loading…

Similar plugins

dsh-relay

by nicecx

DSH plugin: relay approval & question requests to any chat channel — iMessage, Email, WeChat built-in; Telegram/DingTalk/Feishu/Slack via a common adapter contract (src/channels/types.js)

Tools & CapabilitiesNotifications & IntegrationsManifest valid

★ 3

↓ 174/wk

MIT

JavaScript

Sep 7, 2026

dsh plugin --profile web add dsh-relay

by THEWOLFWALKER

Unified notification and remote-control plugin for DeepSeek Harness (DSH): one zero-dependency notify() API across 27 channels, with phone-friendly approvals/questions, six inbound control channels, a

Notifications & IntegrationsManifest valid

★ 60

MIT

JavaScript

Oct 17, 2026

dsh plugin --profile web add dsh-notifier

by ZZJQ678

全局提示词配置:在对话视图新增一个标签页,写进去的内容会加在每个会话系统提示词的最前面,保存后下一条消息即生效。

Development & InfrastructureSessions & MessagesManifest valid

★ 0

MIT

JavaScript

Sep 18, 2026

dsh plugin --profile web add dsh-global-context

辅助补丁 — DeepSeek Harness 插件:任意会话把文件投递到微信、cron 定时任务、活动呼吸灯、完成提示音、一键重启

Notifications & IntegrationsManifest valid

★ 0

MIT

JavaScript

dsh plugin --profile web add dsh-helper

by LLYlab

A permanent DSH plugin that bundles a developer workbench, an in-session conversation tree, and a plugin manager: build and run an LVAL project, browse its code, and keep per-program version snapshots

Security & AuditDevelopment & InfrastructureManifest valid

★ 1

MIT

JavaScript

Oct 24, 2026

dsh plugin --profile web add dsh-essential-tools

by 534119219

Multi-channel push for DeepSeek Harness: Server酱, PushPlus, Bark, DingTalk, WeCom, Telegram, Feishu, ntfy, custom webhooks and more, configurable from the settings panel and callable by other plugins

Notifications & IntegrationsManifest valid

★ 7

MIT

JavaScript

Aug 16, 2026

dsh plugin --profile web add chicheng-push