# pi-pushplus-notify

Send a WeChat notification through [PushPlus](https://www.pushplus.plus/) when a pi task settles.

[English](README.en.md) | [Simplified Chinese](README.md)

## Who this is for

**This extension targets users in China.** It sends notifications through [PushPlus](https://www.pushplus.plus/), a China-based service that delivers to WeChat. You need a WeChat account and a PushPlus account to use it. If you do not use WeChat, this extension is not useful to you.

Two further consequences of that:

- PushPlus has **no OAuth or authorization flow**. There is no "scan to log in" integration, and this extension cannot validate anything for you beyond one live test send. You get your own token and paste it in once.
- The notification bodies this extension sends are written in **Chinese**, and the PushPlus account itself must pass China's real-name verification.

## Why

Long tasks (builds, test runs, batch refactors) leave you waiting without knowing when they finish or whether they succeeded. This extension pushes one WeChat message every time a pi task settles, with the project, duration, working directory, and a summary of the final reply — so you can walk away from the terminal.

Deliberately small:

- **Zero runtime dependencies.** No Python, no `curl`, only Node's built-in `fetch`.
- **The token stays local**, stored with file mode `0600`, and is sent nowhere except PushPlus.
- **Rate-limit protection is built in.** PushPlus penalties are harsh (see below), so the extension throttles itself and stops sending after it gets rate-limited.

## Requirements

- pi with its bundled Node.js **>= 22.19**
- A PushPlus account that has completed real-name verification (see [Rate limits and protection](#rate-limits-and-protection))
- A WeChat account following the PushPlus official account

## Install

```bash
pi install npm:pi-pushplus-notify
```

Restart pi after installing, or run `/reload` in an interactive session.

## Quick start: three steps, 30 seconds

1. **Get a token.** In the WeChat official account **「pushplus 推送加」**, simply reply with the text `token` and it returns the token value immediately. (Other ways to get one are [below](#getting-a-token).)
2. **Paste it.** Run `/pushplus` in pi and pick "configure token" from the menu, then paste the token.
3. **Check WeChat.** `setup` sends a **test message immediately** to prove the token really works. Receiving it means you are configured, and notifications are turned on automatically.

After that, every settled task sends a WeChat notification. The token is long-lived and does not expire, so pasting it once is enough.

> Prerequisite: the PushPlus account must have completed **real-name verification**, otherwise the API returns error code 905 and the test message never arrives.

## Commands

The only command is `/pushplus`. Arguments support Tab completion, and full-width letters (such as `ＯＮ`) are normalized automatically.

| Command | What it does |
| --- | --- |
| `/pushplus` | **Opens an interactive menu** (configure token / toggle notifications / send test / clear), so you do not have to remember arguments |
| `/pushplus status` | Shows whether a token is configured, where it comes from (environment variable or config file), whether notifications are on or off, and whether sending is currently suspended by a rate limit |
| `/pushplus setup` | Prompts for a token, sends one test message to validate it, then saves it and turns notifications on |
| `/pushplus on` / `/pushplus off` | Turn settled-task notifications on or off |
| `/pushplus test` | Sends another test message |
| `/pushplus reset` | Forgets the locally saved token and turns notifications off (also clears the local rate-limit suspension record) |

The menu items change with the current state: with no token configured it offers only "configure token"; once a token exists it offers send test / toggle notifications / replace token / clear token.

## Getting a token

Any of these gives you the same thing: an alphanumeric token.

1. **Simplest:** in the WeChat official account 「pushplus 推送加」, reply with the text `token` and it returns the token value directly. No browser needed.
2. **Web login:** scan the QR code with WeChat to log in at [pushplus.plus](https://www.pushplus.plus/), open the "one-to-one message" page, and use the one-click copy button.
3. **Safer for long-term use:** in pushplus.plus, go to Account → "Developer settings" and create an extra "message token". Such a token can be given an expiry date and deleted on its own — if it leaks, delete and recreate it without touching your main account.

## Rate limits and protection

PushPlus's limits for a verified free account are strict, and **exceeding them is punished heavily**:

- **5 requests per minute**
- **200 messages per day** on the WeChat channel
- The important trap: **failed requests count too** — an error still consumes quota
- **Identical content: at most 3 per hour**
- Penalty for exceeding: sending stops for the rest of the day, and up to 7 days in serious cases

The extension defends against this:

- **Sliding-window rate limiting.** It records the request times of the last 60 seconds and skips the send once 5 have been made, telling you explicitly: *"已达 PushPlus 每分钟 5 次上限，本次通知已跳过"* (already at PushPlus's 5-per-minute limit, this notification was skipped). It does not drop the message silently.
- **Same-content detection.** If the identical body has already been sent 3 times within an hour, the send is skipped with *"PushPlus 相同内容 1 小时最多 3 条，本次通知已跳过"* (PushPlus allows at most 3 identical messages per hour, this notification was skipped).
- **Error code 900** (account restricted) suspends sending automatically for **24 hours**, recorded as `blockedUntil` in the config file. Otherwise retrying would make the server-side penalty worse.
- **Error code 903** reports that the token is invalid and asks you to run `/pushplus setup` again.
- **Error code 905** reports that the account has not completed real-name verification.
- **Error code 888** reports insufficient PushPlus credits.
- **Error code 999** means PushPlus judged the request rate too high; the specific reason returned by the server is shown.
- **Sends are detached.** A slow network cannot stall the pi interface — the 15-second request timeout only happens in the background.

Notes on the manual commands: `/pushplus setup` and `/pushplus test` are triggered by you, and they consume quota too. `setup` is not subject to the sliding window; `test` checks it and refuses when the window is full. `test` also respects an active suspension, while `setup` with a *different* token lifts the suspension (retrying the *same* token keeps it, because that would only aggravate the server-side penalty). Both `setup` and `test` include a timestamp in the body so repeated manual runs do not trip the "identical content" rule.

## What gets sent

The WeChat message is a **summary**, not the full output; the full output always stays in your terminal.

- Fenced Markdown code blocks are removed and replaced with `[代码块已省略]`, so a whole diff never becomes the message body
- ANSI escape sequences are stripped
- If the body exceeds **1200 characters**, the beginning and the end are kept and an omission marker is inserted in between
- Titles are truncated to **100 characters**
- If the task produced no text at all, a fallback line is sent instead

A settled-task notification contains the status, the project name, the working directory, the elapsed seconds (when known), and the truncated final reply. The title is `Pi · <project> · 任务结束`.

## Configuration and storage

- Config file: `~/.pi/agent/pushplus-notify.json`, located through pi's `getAgentDir()`, written with mode `0600`
- Environment variable: `PUSHPLUS_TOKEN` acts only as an **initial source when the config file has no token**, which suits CI — the token never has to be written to disk
- Precedence: **a token saved by `/pushplus setup` always wins.** The environment variable only applies while the config file has no token, so setting the variable first and running `setup` later still overrides it
- Default behavior: once a token is configured, notifications are on
- You only need to configure the token; the switch state, rate-limit suspension time, and recent send record are maintained by the extension
- Writes use a temp file plus rename, so concurrent pi processes never read a half-written file
- `reset` removes the token, the switch state, the suspension, and the send logs, while **keeping any other fields you added to the file by hand**

## Troubleshooting

**`/pushplus setup` says the token is invalid**
The token was copied wrong. Fetch a new one — replying `token` in the official account is the easiest way. Make sure you do not include extra spaces or newlines.

**It says the account is not verified (code 905)**
PushPlus refuses to deliver for unverified accounts. Complete real-name verification at pushplus.plus and try again.

**Configuration succeeded, but no message arrives when a task finishes**
Check in order: run `/pushplus status` to see whether notifications were turned off (use `on`); check whether it reports a suspension ("已被 PushPlus 限流，暂停中" — if so, wait for the suspension to end); confirm WeChat follows the 「pushplus 推送加」 official account and has not muted it; confirm the account has not been stopped for exceeding the quota.

**I got rate-limited / "账号已被限流"**
The 5-per-minute or 200-per-day allowance is used up — usually because several tasks ran in quick succession, or the manual test was clicked too many times. On error 900 the extension suspends sending for 24 hours automatically; the simplest fix is to wait it out. Running `/pushplus reset` and `setup` again makes the extension try immediately, but **it does not lift PushPlus's server-side penalty**.

**"PushPlus 积分不足" (code 888)**
The account is out of credits. Top up or check your plan at pushplus.plus.

**Does the test message from `setup` consume quota?**
Yes. `setup` and `test` are real requests, and PushPlus counts all of them, including failed ones.

**Does it support anything besides PushPlus?**
No. This extension talks to PushPlus only and does not support other push providers.

## License

[MIT](LICENSE)
