dsh
OpenPetsHQ/openpets将 DeepSeek Harness 的生命周期状态、错误与审批请求,桥接到本地运行的 OpenPets 桌面伙伴。
一个插件接入 Telegram、Discord、Slack、Mattermost、Zulip 与 Matrix,按对话隔离会话、重启后仍能续上;带设置页可增删与测试通道、无需改 YAML,白名单 fail-closed——未配置前拒绝所有发送者。
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.
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 pollingAdding a platform is one adapter file. Nothing in core/ changes.
type · inbound · needs a public URL? · credentials telegram · Bot API long polling · no · bot token (@BotFather) discord · Gateway WebSocket · no · bot token slack · Socket Mode WebSocket · no · bot token xoxb-… + app token xapp-… matrix · /sync long polling · no · access token (+ homeserver URL) mattermost · WebSocket + REST · no · personal access token + base URL zulip · event-queue long polling · no · : + 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.
Add this index under Plugins → Discover → Manage index sources:
https://raw.githubusercontent.com/monkey-sking/dsh-im-global/main/index.jsonplugin_manager({ action: "install_bundle", target: "/Users/you/.local/share/dsh-im-global" })The bundle inserts one row, im-global, into the profile composition.
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.
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.
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:
platform · default credential · what to paste Telegram · TELEGRAM_BOT_TOKEN · BotFather → /newbot → the token it prints Discord · DISCORD_BOT_TOKEN · Bot → Token (Developer Portal → Bot → Reset Token) Slack · SLACK_BOT_TOKEN · OAuth & Permissions → Bot User OAuth Token (xoxb-…) Slack · SLACK_APP_TOKEN · Basic Information → App-Level Tokens (xapp-…, scope connections:write) Matrix · MATRIX_ACCESS_TOKEN · any account's access token Mattermost · MATTERMOST_TOKEN · Profile → Security → Personal Access Tokens Zulip · ZULIP_BOT_TOKEN · : 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.orgThe 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.
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-levelA 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.
The gate covers both ways a chat can drive the agent:
the choice is emitted. A press from an off-list sender is refused with "你不在允许名单上,无法回答" and never reaches the question broker — otherwise anyone in a group could answer for the owner just by seeing the keyboard.
getUpdates gets409 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).
node scripts/migrate-telegram-bindings.mjs telegram-mainIt copies the old chat -> session mapping, so the same chat resumes the same Harness session instead of starting from zero.
baseUrl is required (https://chat.example.com).it; the adapter pings every 30 s because the socket goes quiet otherwise.
BAD_EVENT_QUEUE_ID ishandled by re-registering rather than surfacing an error.
Intents). Without it the bot receives every message with empty content.
bot scope and the *Send Messages* / *Read MessageHistory* / *Attach Files* permissions.
!new in chat: a leading / is reserved by Discord's own slash-commandUI, so /new may be swallowed by the client. !new, !status, !help work everywhere; /… is accepted too in DMs and other platforms.
connections:write.message.channels, message.im (and message.groupsfor private channels).
receiving events.
since forward, so a restart does not replay history.command · effect !new / /new · rotate to a new session (the old one stays readable in the Web UI) !status · session id, state, working directory !help · command list
Plain messages go to the agent. Images are attached for the model when the platform lets the adapter download them.
type:conversationId (discord:1234…), so a restart resumes the same conversation instead of starting a blank one.
existence probe missed it, the create path retries as a resume instead of failing every message — the exact bug that broke the Telegram channel.