omp packages

@zxh126/pi-qqbot2.0.1

Bridge the Pi coding agent into QQ private chats, with remote tool-call approval

zxh126 Published 1 day ago MIT ~2.7K downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-qqbot

Bridge the Pi coding agent into QQ private (C2C) chats: send a message from QQ and let the agent work on the machine where pi runs — with remote approval before it executes dangerous tools.

English | 简体中文

Single-session design (v1): every QQ user shares one pi session. Multi-user / multi-session support is planned on top of the pi SDK.

Features

Feature Description
Chat-driven agent QQ private messages are fed into the current pi session; the assistant's answer is sent back to QQ (long replies are split automatically).
Message queue While a task is running, further QQ messages are queued instead of rejected; each one starts automatically as soon as the current run settles. /cancel is the exception — it acts immediately and clears the queue.
Remote approval Before pi runs bash / powershell / write / edit — or any tool annotated destructiveHint — the bot asks in QQ first.
Session-scoped "always allow" Reply 「以后都同意」 and every approval request in the current session is auto-approved, including the one already waiting.
Single-instance lock Only one pi process hosts the QQ gateway; the others stand by and take over automatically when the host exits.
Cancel a running task Send /cancel (or 「取消」/「停止」) and the current run is aborted immediately — the QQ reply, the pending tool calls, the half-written answer and the pending queue are all cleaned up.
Slash-command routing QQ messages starting with / are routed: pi extension commands, prompt templates and /skill:name are dispatched to pi; a set of pi built-ins (/reload, /new, /compact, /session, /name, /model, /thinking, /quit) is taken over; host-only commands are answered instead of being fed to the model.
Progress reports While a run is in progress the bot posts a short progress summary (turns, tool calls, the tool currently running, tokens) every progress.intervalMs (default 15 s). Ask at any time with /progress (aliases /p, 「进度」). /progress off pauses the automatic reports for the rest of the session, /progress on brings them back. If a report cannot be delivered the automatic reports stay quiet until you ask again.
Secret redaction Anything leaving the machine is scrubbed first — progress reports, approval prompts, queue previews and the log file: Authorization headers (incl. QQ bot tokens), sk-… / ghp_… / npm_… / AKIA… / JWTs / PEM blocks, token= / password= / --api-key values, URL user:pass@, and long high-entropy strings.
Multi-bot on one machine Put several QQ bots in one config (qqbots: [...], see docs/accounts.md). Each pi process, in config order, claims the first bot that is not online yet and then verifies its credentials (a wrong secret releases the lock instead of squatting the slot); a process that finds every bot taken stays idle and rescans every 5 s, taking over as soon as one goes offline. A bot whose owner is merely reloading is never snatched (a 20 s reservation is left behind). The bot name is logged, shown in /status and prepended to the first QQ reply; taking over from another process also tells you the new working directory.
Built-in commands /status, /queue, /progress (+ /progress off / on), /reset, /cancel, /help.

Install

# from npm
pi install npm:@zxh126/pi-qqbot

# or from a local checkout (dependencies are needed here)
npm install
pi install ./pi-qqbot

Then create ~/.pi/agent/pi-qqbot.json:

{
  "appId": "YOUR_APP_ID",
  "appSecret": "YOUR_APP_SECRET",
  "sandbox": true,
  "allowlist": [],
  "markdownReply": false,
  "streaming": false,
  "approve": {
    "tools": ["bash", "powershell", "write", "edit"],
    "remember": true
  },
  "queue": {
    "max": 20,
    "ack": true
  }
}
Field Default Description
appId / appSecret required Credentials of your QQ Open Platform bot
sandbox false Set true while the bot is not yet published (sandbox environment)
allowlist [] openid allowlist; empty means no restriction
markdownReply false QQ private chat does not support markdown, keep false
streaming false Reserved, streaming is not enabled in v1
approve.tools ["bash","powershell","write","edit"] Tools that require approval; tools annotated destructiveHint are always gated too
approve.remember true Remember the decision per tool inside the session (no more prompts for that tool)
commands see below Slash-command routing gates: passthrough, allowReload, allowQuit, allowSession
progress.enabled true Post periodic progress reports while a task is running (read at session start — restart or /new after changing; at runtime use /progress off)
progress.intervalMs 15000 Interval of those reports (env override: PI_QQBOT_PROGRESS_MS)
queue.max 20 How many messages may wait while a task runs; 0 disables queueing (busy messages are refused as before)
queue.ack true Reply to QQ when a queued message starts (the "queued" ack is always sent)

Credentials can also come from the QQ_BOT_APPID / QQ_BOT_APPSECRET environment variables (single-bot setup only).

Several bots (2.0)

{
  "qqbots": [
    { "name": "work", "appId": "1903340143", "appSecret": "…", "sandbox": true },
    { "name": "home", "appId": "1020123456", "appSecret": "…" }
  ],
  "queue": { "max": 20, "ack": true }   // still the global defaults, per-bot overrides allowed
}

Every pi process claims the first bot (in config order) that is not online; spare processes stand by and rescan every 5 s. After claiming, the credentials are verified by fetching an access_token: a bot with a wrong appId/appSecret releases its lock and is skipped instead of squatting the slot. PI_QQBOT_ACCOUNT=<name|appId> pins one bot instead of auto-claiming. Full details: docs/accounts.md.

Usage

Start pi, then message the bot in QQ; pi handles it and replies.

Command Description
/reset Clear the reply buffer, approval memory and session-wide auto-approve (does not switch sessions — send /new for that)
/status Show the bot this process serves (name + working directory), busy/idle state, queue length, auto-approve state, pending approval count and the progress-report mode
/queue Show the queue (aliases: /q); /queue clear empties it, /queue drop <id> removes one entry
/progress Show the current progress summary immediately (aliases: /p, 「进度」, 「当前进度」, 「进展」; plain-text questions like 「到哪了」「怎么样了」 work while busy). While you paused reports yourself it only replies once — use /progress on to resume; while reports are merely quota-quiet, asking resumes them
/progress off Pause automatic progress reports for the rest of the session (aliases: pause, stop, 暂停, 「暂停汇报」, 「关闭汇报」). Manual /progress still works, it just no longer resumes the timer
/progress on Resume automatic progress reports (aliases: resume, start, 恢复, 「恢复汇报」, 「继续汇报」)
/cancel Interrupt the task that is running right now and empty the queue (aliases: /stop, /abort, 「取消」, 「停止」, 「中止」, 「终止」, 「算了」, 「停」, 「停下」, 「停一下」, 「别跑了」). Note: a bare 「停」 interrupts the task; to silence progress reports say 「别汇报了」/「闭嘴」 or /progress off
/help Show help

Slash commands from QQ

Any message that starts with / is routed before it can become a chat prompt:

# Kind Behaviour
0 extension commands whose output only goes to ctx.ui.* (/usage, /cost, /mcp, /llama) /usage and /cost are bridged (see below) so the report comes back to QQ; /mcp and /llama need host-TUI dialogs and are answered with an explanation instead of being silently swallowed
1 pi extension commands, prompt templates, /skill:name dispatched to pi with expandPromptTemplates: true, so things like /usage, /cost or your own commands work from QQ
2 pi built-ins this bridge can take over re-implemented through the extension API (table below)
3 pi built-ins that only exist in the TUI answered with "run it on the host", never sent to the model
4 everything else (/tmp 里有什么, /etc 看看 …) ordinary chat
QQ command What happens Gate
/reload reloads extensions / skills / templates / themes — the bot restarts too and reconnects the gateway commands.allowReload (default false)
/new real new pi session (ctx.newSession()) commands.allowSession
/compact [instructions] compacts the context commands.allowSession
/session session id / cwd / model / thinking level / context usage commands.allowSession
/name [name] get or set the session name commands.allowSession
/model [provider/model] list available models or switch commands.allowSession
/thinking [level] get or set the thinking level commands.allowSession
/tree list branch points (numbered), answer with a number to jump there commands.allowSession
/fork list earlier user messages (numbered), answer with a number to fork from it commands.allowSession
/clone duplicate the current session at its current leaf commands.allowSession
/resume list the project's other sessions (numbered), answer with a number to switch commands.allowSession
/quit shuts the host pi down commands.allowQuit (default false)

Host-only (answered, never forwarded to the model): /settings, /hotkeys, /changelog, /trust, /login, /logout, /scoped-models, /llama, /import, /export, /share, /bug, /copy, /privacy.

TUI-only commands come back to QQ

Some extension commands never print into the conversation — they call ctx.ui.notify() (and pi.appendEntry()), which only renders on the host TUI/RPC front end. In QQ that showed up as "dispatched, then silence". The bridge closes that hole:

Command What happens
/usage [range], /cost [range] re-dispatched as /qq usage <range>: the bridge runs the original /usage command, then polls for the fresh customType: "usage-report" entry for up to 2 s (the append lands ~100 ms after sendUserMessage() resolves, so a single read always misses it), falling back to the tail of the session file, and renders it as text (requests, tokens, cache ratio, cost, per-model). Usage unchanged — keep sending /usage today.
/usage rate <value>, /usage rate auto really executed (they write ~/.pi/agent/pi-usage-report.json / refresh the cached rate); the bridge then reads those files back and reports the resulting rate, so an invalid value shows up as "unchanged"
/mcp, /llama answered: they need host-TUI selection dialogs / browser login, so nothing is dispatched

A bare /usage rate and /usage help are answered locally, without running the command. Coupling with pi-usage-report is limited to the usage-report entry type plus the two files it keeps in ~/.pi/agent/.

Guards:

  • /reload, /new, /compact, /quit, /tree, /fork, /clone and /resume refuse to run while a task is in progress — send /cancel first.
  • Prompt templates and /skill:name start an agent run, so while a task is in progress they are queued like ordinary messages (see Queueing while busy). Extension commands that manage their own LLM interaction still run immediately.
  • Selection commands (/tree, /fork, /resume) answer with a numbered list of at most 10 entries, scoped to the user who asked and valid for 10 minutes. The next bare number from that user executes the choice; anything else (or 「取消」) abandons it. A number without a pending list is ordinary chat.
  • /reload is off by default: it restarts the bot, and if the new code fails to load the bot stays down until someone fixes it on the host. Turn it on with commands.allowReload: true.
  • Why the indirection: pi only dispatches extension commands from a message (prompt() → _tryExecuteExtensionCommand), and reload() / newSession() are command-context APIs. So this extension registers /qq and forwards /reload → /qq reload. Built-in names are hard-coded here because pi does not export its built-in list; any unknown /word falls back to plain text.

Queueing while busy

pi's session runs one agent run at a time, so the bot serializes QQ input itself:

you: 帮我把测试跑一遍
bot: 收到,正在处理
you: 顺便看看 README 有没有错别字
bot: 已排队 #1,当前任务结束后自动执行。发 /queue 查看队列,/cancel 可中止当前任务并清空队列。
you: /queue
bot: 当前任务:处理中(发 /cancel 可中断)
     队列:1 条(上限 20)
       1. #1 · 12s 前 · 顺便看看 README 有没有错别字
     发 /queue clear 清空,/queue drop <编号> 删除一条。
bot: (第一轮结束后)开始执行队列 #1

Semantics:

  • Every message that would start a run — plain chat, prompt templates, /skill:name — is queued while a task is running, in arrival order (FIFO). Replies go to the QQ user who sent that message.
  • The next entry starts when pi has settled (agent_settled, no automatic continuation left), so a retry, a compaction retry or a follow-up turn is never cut in half.
  • The queue holds at most queue.max entries; beyond that the message is refused with a notice. queue.max: 0 restores the old "上一条消息还在处理中" behaviour.
  • If handing a message to pi fails (pi was still finishing the previous run), it is put back at the head of the queue and retried — messages are not lost. A run that already started is never retried, so a failure inside a run cannot execute the same message twice.
  • Control commands are not queued: approvals, /status, /queue, /reset, /help, the pi built-ins and /cancel all act immediately.
  • /cancel empties the queue as well, because "cancel" means "stop doing all of this". Use /queue drop <id> to remove a single entry instead.

Progress reports

While a run is in progress, a summary is pushed to QQ every progress.intervalMs (15 s by default):

[进度 02:15] · 第 3 轮 · 工具 6 次(bash 4 / read 2)· 失败 1
最近: bash "npm test" · 6.2s
正在: 分析测试输出… · 12.3k tok
  • Built purely from local event data (tool_execution_*, turn_end, message_end, message_update) — no model call, no tokens spent.
  • Tool arguments are redacted and truncated, so a command carrying a token never reaches QQ in clear.
  • Quota-aware: one QQ message buys only 4 passive replies, so a long run will eventually run out. The first failed send stops automatic reports for that run (no retry spam); send /progress (or 「进度」) and they resume.
  • Manual pause / resume: /progress off (or 「暂停汇报」) silences the reports for the rest of the session — it survives agent_settled and the next run, and asking for /progress no longer turns it back on. /progress on (or 「恢复汇报」) resumes. A new session (/new, restart) starts enabled again. /status shows which of the four modes you are in (on / paused-by-you / quota-quiet / disabled).
  • The final answer is protected: if it cannot be delivered (quota), it is cached and re-sent with a (上一条回复发送失败,补发如下) prefix as soon as you send the next message.
  • Timer is cleared on agent_settled / /cancel / session shutdown.
  • Set progress.enabled: false to turn the whole thing off (across sessions; read at session start).
Colloquial shortcuts (Chinese, matched only as a whole message)
Intent Say any of
Pause automatic reports 「暂停汇报进度」「暂停进度汇报」「别汇报了」「别说了」「闭嘴」「安静」「打住」「停止汇报」, or /progress off
Resume them 「恢复汇报」「恢复汇报进度」「接着说」「说下去」, or /progress on
Ask once 「汇报一下当前进度」「看一下进度」「说说进度」「进度如何」「进度怎么样」, or /progress
Interrupt the task 「停」「停下」「停一下」「停停」「停止」「取消」「别跑了」, or /cancel

A bare 「停」 interrupts the task; only phrases carrying a report noun (「停止汇报」) or an explicit silence word (「别说了」「闭嘴」) pause the reports. Sentences like 「帮我加个进度条」 are still forwarded to pi.

Cancelling a run

Long tasks started from QQ (installs, builds, a runaway agent loop) can be interrupted without going back to the machine:

you: /cancel
bot: 已取消当前任务,并终止了 1 个待审批请求,队列中的 2 条已清空。可以发送新消息继续。

What it does, in order: terminate every pending approval request, empty the queue, call ctx.abort() on the current run (the same path as pressing Escape in the TUI), drop the partially streamed answer so it is not sent to QQ, keep the busy flag until pi reports agent_settled (so messages arriving during the abort are queued instead of racing it), and reply with a confirmation. If agent_settled never arrives, an 8-second watchdog releases the busy flag and continues the queue. Replies that only look like cancelling — 「取消自动同意」, 「恢复审批」 — still go to the approval handler, so the auto-approve switch keeps working as before.

Anything else is forwarded to pi as a user message.

Remote approval

[审批 #1] bash: rm -rf node_modules
回复「同意 1」批准,「拒绝 1」拒绝,「停止」终止本轮。
要本会话内全部放行,回复「以后都同意」(可用「取消自动同意」关闭)。

Reply vocabulary (approval keywords are Chinese, with English aliases):

Intent Reply
Approve one request 同意 1 / allow 1 — a bare 同意 / allow applies to the oldest pending request
Reject one request 拒绝 1 / deny 1 — a bare 拒绝 / deny applies to the oldest pending request
Abort the pending requests and the running task 停止 / /cancel
Allow everything in this session 以后都同意, 一直同意, 全部放行, 别再问了, always allow
Back to per-request approval 取消自动同意, 恢复审批, 以后都不同意

Semantics:

  • Session scope only. The auto-approve switch lives in memory and lasts until /new, a new pi session, or one of the "back to per-request approval" replies. Nothing is persisted, nothing survives a restart.
  • The switch is an explicit user instruction, so it is not affected by approve.remember.
  • approve.remember: true additionally remembers the decision per tool for the session: after approving bash once, later bash calls are not asked again (/new clears it).
  • Requests time out after 10 minutes and are rejected with a notification. A timeout is not remembered as a rejection — the tool is asked again next time.
  • Approval text that arrives while no request is pending is forwarded to pi as a normal message.

Single-instance lock

The QQ gateway keeps only one active connection per bot. The extension uses a proper-lockfile lock at ~/.pi/agent/pi-qqbot-<appId>.lock:

  • the process that acquires the lock becomes the host and connects to QQ;
  • other processes stand by, retry every 5 seconds, and claim any bot of the list that is free;
  • when the host exits or crashes (30 second heartbeat timeout), a standby process takes over;
  • a host that is only rebuilding its session (/reload, /new, /fork, /resume) writes a 20 second reservation next to the lock, so nobody else grabs its bot in the gap;
  • the last owner (pid + working directory) is kept in ~/.pi/agent/pi-qqbot-<appId>.owner.json and is used to tell you that a new process took over, and from which directory.

Prerequisites

  1. Create a bot on the QQ Open Platform and copy its AppID / AppSecret.
  2. Before the bot is published, configure a sandbox C2C account and set sandbox to true.
  3. In production, add the server's public egress IP to the bot's allowlist.

Logs

Debug logs are written to ~/.pi/agent/pi-qqbot-<name>.log (one file per bot, so several pi processes on the same machine never interleave). A single-bot v1 config keeps writing pi-qqbot.log. Each line starts with a local timestamp that carries the UTC offset, e.g. [2026-10-24T07:39:09.762+08:00], so it lines up with file mtimes and process start times without mental arithmetic.

Changelog

See CHANGELOG.md — every release with what changed and why. The progress-report pause/resume (/progress off / on) landed in 1.5.0; multi-bot auto-claiming in 2.0.0.

License

MIT