# dsh-qqbot-dafeiyu A QQ Bot IM plugin for [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (dsh), driving the dsh agent loop with the QQ messaging platform as the frontend protocol. [中文文档](./README.md) | English ## Architecture ``` QQ User → QQ WebSocket → dsh-im-qqbot → ctx.agents → dsh agent loop → LLM ↑ │ └── session/event ──────────┘ (assistant reply → QQ sendMarkdown) ``` ## Installation ### Method 1: Manual ```bash # Add to a profile dsh plugin --profile qqbot add github:cheesewoo/dsh-qqbot-dafeiyu # Start dsh --profile qqbot ``` On first launch, the plugin detects missing credentials and automatically starts the QR flow: a QR code is printed in the terminal → scan it with the QQ mobile app → credentials are saved to the profile, so subsequent launches require no re-scan. ### Method 2: Local path ```bash # Build cd /path/to/dsh-qqbot pnpm install && pnpm build # Add to a profile (local path) dsh plugin --profile qqbot add /path/to/dsh-qqbot # Start export QQBOT_APPID="yourAppID" QQBOT_SECRET="yourAppSecret" dsh --profile qqbot ``` ### Method 3: --patch dev mode ```bash cd /path/to/deepseek-harness export QQBOT_APPID="yourAppID" QQBOT_SECRET="yourAppSecret" pnpm dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml ``` ## Configuration | Config | Type | Default | Description | |------|------|--------|------| | `appId` | string | **required** | QQ Bot AppID (or via `QQBOT_APPID` env var) | | `appSecret` | string | **required** | QQ Bot AppSecret (or via `QQBOT_SECRET` env var) | | `provider` | string | `deepseek-official` | LLM provider name | | `model` | string | `deepseek-chat` | Model name | | `preset` | string | - | Agent preset id | | `cwd` | string | `process.cwd()` | Agent working directory | | `requireMention` | boolean | `true` | Whether group messages require @bot to trigger | | `groupPrompt` | string | - | Extra system prompt for group chats | | `directPrompt` | string | - | Extra system prompt for direct chats | | `textChunkLimit` | number | `4500` | Max chars per message | | `sessionIdleTimeout` | number | `1800000` | Session idle timeout (ms), default 30 min | | `access.c2cMode` | `open` \| `allowlist` \| `disabled` | **`allowlist`** | Direct-chat access mode (`disabled` = ignore direct chats) | | `access.c2cAllow` | string[] | `[]` | Direct-chat allowlist (sender openids) | | `access.groupMode` | `open` \| `allowlist` \| `disabled` | **`allowlist`** | Group access mode (`disabled` = ignore group chats) | | `access.groupAllow` | string[] | `[]` | Group allowlist (group openids) | | `debug` | boolean | `false` | Debug mode | ## Access control (security) The default is **allowlist mode with an empty list — which denies everyone**: a freshly installed bot answers nobody until you add the openids you trust. | Mode | Behaviour | |------|------| | `allowlist` (default) | Only openids on the list; an empty list **denies everyone** | | `open` | Answers everyone / every group — **anyone who can reach the bot can use it**; a warning is logged at startup | | `disabled` | Turn that scope off entirely (direct or group) | ```yaml access: c2cMode: allowlist c2cAllow: ['sender-openid'] groupMode: allowlist groupAllow: ['group-openid'] ``` ⚠️ With `open`, IM messages drive the agent directly, and the agent carries tools (including shell and file access) — that exposes host capabilities to anyone who can DM or @-mention the bot. Use `allowlist` (or `disabled`) before exposing it. An empty list is never treated as "allow everyone"; it allows nobody. ## Built-in Commands | Command | Description | |------|------| | `/bot-reset` | Reset the current session (clear context) | | `/bot-model` | View or switch model | | `/bot-status` | View current session status | | `/bot-help` | View all commands | ## Core Modules ``` src/ ├── index.ts # Cordis plugin entry (async apply) ├── config.ts # Config schema ├── types.ts # Global types ├── setup.ts # Credential binding (QR) ├── transport/ # Transport layer │ ├── inbound.ts # QQ inbound message → agent.followup() │ ├── outbound.ts # session/event → QQ sendMarkdown │ ├── outbound-buffer.ts # Streaming buffer │ └── chunker.ts # Markdown chunking ├── session/ # Session management │ ├── session-manager.ts # QQ peer → Agent mapping │ └── idle-evictor.ts # Idle eviction ├── model/ # Model routing │ ├── model-resolver.ts # Route resolution │ ├── prefs-store.ts # Per-peer preference persistence │ └── settings-reader.ts # settings.yaml read-only ├── shared/ # Shared utilities │ ├── utils.ts # Common helpers │ ├── scope.ts # scope/peer extraction │ └── send-helper.ts # Chunked send ├── commands/ # Slash commands └── typings/ # External module declarations ``` ## Session Routing sessionKey: `qqbot:${appId}:${scope}:${peerId}`, with the SessionId derived deterministically via SHA-256 so sessions survive restarts. Resolution strategy: in-process reuse → persisted resume → fresh create. ## Design Principles - **Pure Cordis plugin** — follows the dsh "Plugins, not loop changes" principle - **Declarative dependencies** — `inject = ['agents']`, no direct coupling to other plugins - **Session isolation** — one independent Agent per QQ direct user / group - **Preset support** — mount presets (toolkits, prompts, etc.) via the `agent-presets` service - **Idle eviction** — auto-dispose Agents on timeout to prevent memory leaks - **Markdown output** — replies sent as Markdown with code-block/table-aware chunking ## Local Development ```bash # Install dependencies pnpm install # Build pnpm build # Dev mode (watch) pnpm dev # Debug via --patch (inside the dsh monorepo) cd /path/to/deepseek-harness export QQBOT_APPID="xxx" QQBOT_SECRET="xxx" pnpm dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml ``` ## License [MIT](./LICENSE)