DSH Plugin Store
Back to home

TtTRz

dsh-wecom

WeCom AI Bot channel for DeepSeek Harness — every chat runs a persistent, preset-backed agent with real tools.

Stars
1
Language
TypeScript
Created
Aug 13, 2026
Updated
Aug 14, 2026
Other
GitHub repo

Introduction

dsh-wecom

Wire a WeCom AI Bot to DeepSeek Harness. Each single/group chat is backed by a real Harness agent with tools — not a bare chat loop.

What it solves

WeCom's AI Bot long connection only carries messages; deciding who answers them is left open. This plugin fills the gap:

  • One conversation = one agent, and the agent mounts a preset in its scoped setup (default standard). It therefore has the preset's tools (bash, read, edit, skills, …) and persona.
  • The session id is derived deterministically from sha256(namespace · scope · peer) — it never embeds the raw userid — and survives restarts through sessionPersistence.
  • Media arrives too: images are downloaded, decrypted with the official SDK, and attached when the model can view them; files and videos land in the agent's workspace so its tools can read them.

The loop

WeCom AI Bot
   │  WebSocket long connection (wss://openws.work.weixin.qq.com)
   │  auth body is exactly { bot_id, secret } — nothing else, per the official doc
   ▼
dsh-wecom (host plugin)
   │  msgid dedup → access policy → per-conversation queue → global concurrency cap
   │  create/resume agent (setup mounts the preset + a persistent instruction section)
   │  agent.followup(userMessage) → await agent.whenIdle()
   │  on timeout → agent.cancel(), no zombie turn left behind
   ▼
persistent per-conversation Harness agent (sessionPersistence)

Why not a bare agents.create

DecisionWhy
Preset mounted in setupA bare agent has no tools; mounting preset is what makes it capable
systemPrompt.section()A persistent instruction rendered every turn, not a one-shot inject
Timeout cancelsA slow turn is cancelled, so the next message is not stuck behind it
Group allowlist by chatidYou gate which groups the bot answers in, not which senders
maxConcurrentBounds how many turns run at once across all conversations

Install

dsh plugin --profile web add github:TtTRz/dsh-wecom

Local checkout:

dsh plugin --profile web add /absolute/path/to/dsh-wecom

Configure

The Secret lives in the Harness credential service (reference WECOM_BOT_SECRET); the Bot ID comes from WECOM_BOT_ID. Commit neither.

export WECOM_BOT_ID='your-bot-id'
# put the Secret into credential reference WECOM_BOT_SECRET (env works for development too)

To make it stick across restarts, write WECOM_BOT_ID into ~/.dsh/.env (the user .env layer the harness loads at boot) and WECOM_BOT_SECRET into ~/.dsh/.credentials.yaml. DSH_WECOM_CWD overrides the agent working directory.

Every WeCom conversation is grouped under a workspace created on cwd (title workspaceTitle, default WeCom), so chats stay out of the sidebar's "Ungrouped" bucket. Point DSH_WECOM_CWD at a dedicated directory to keep them separate from your own sessions. When the long connection dies (kicked, auth failure, or replaced by another client), the channel restarts itself after restartIntervalMs (default 10s) — no dead bot, no manual restarts.

Tune the mounted row in ~/.dsh/profiles/web/cordis.patch.yml:

- id: wecom-channel
  name: dsh-wecom
  config:
    botId: !!js process.env.WECOM_BOT_ID
    credentialName: WECOM_BOT_SECRET
    namespace: default
    cwd: !!js process.env.DSH_WECOM_CWD ?? process.cwd()
    preset: standard
    dmPolicy: open
    dmAllowlist: []
    groupPolicy: allowlist
    groupAllowlist: [wr_your_group_chatid]
    greeting: Hello, I am an assistant.
fielddefaultmeaning
presetstandardpreset mounted into each conversation agent
dmPolicy / groupPolicyopenopen / allowlist / disabled
dmAllowlist[]single-chat userid allowlist
groupAllowlist[]group-chat chatid allowlist
instructionsenterprise-chat guidanceinstruction section layered on the persona every turn
imageModeautoauto attaches images when the model can view them; always / never force it
maxConcurrent4global cap on concurrent turns
turnTimeoutMs300000per-turn timeout (cancels the turn)

Commands

commandwhat it does
/bot-pingconnectivity check
/bot-helplist commands
/bot-statussession status
/bot-cancelcancel the current generation
/bot-newstart a fresh conversation (history is kept, the next message opens a new session)

Verify

Once the log prints WeCom AI Bot authenticated, try /bot-ping and expect a pong back.

Status service

While running, the plugin publishes a host-wide wecomChannelStatus service so dashboards and UI plugins can render live health without touching channel internals:

const status = ctx.get('wecomChannelStatus') // { snapshot(): ChannelStatus }
const health = status.snapshot()
// { connected: boolean, stopping: boolean, conversations: number,
//   authenticatedAt: number | null, lastError: string | null }

snapshot() returns plain scalars only — safe to serialize into RPC or JSON.

Browser UI

The package ships a browser client half (declared via dsh.client and served by the web profile as /plugins/dsh-wecom/client.js — no frontend rebuild needed). It adds:

  • a WeCom action in the sidebar foot with a live connection dot, and
  • a floating status panel (connection, conversation count, authentication age, last error) that opens from the action and polls every five seconds.

Both consume GET /api/wecom/status, the JSON route the host half registers when a web server is present. The client half is built to CommonJS and wrapped by scripts/wrap-client.mjs into the factory form the web module loader runs.

Development

npm install --legacy-peer-deps
npm run check   # biome + typecheck + test + build

License

MIT