[English](./README.md)

# pi-qq-integration — 中文版

在 **QQ 中操控 pi**。安装此扩展后，pi 启动时会自动加载扩展并**默认自动连接** QQ Bot（可在配置中关闭）。连接后即可通过 QQ 向 pi 发消息、查看 session 列表、浏览历史对话；也可随时用 `/qq-connect`、`/qq-disconnect` 手动控制连接。

---

## 安装

```bash
pi install npm:pi-qq-integration
```

---

## 快速开始

### 1. 注册 QQ Bot

在 [QQ 开放平台](https://q.qq.com) 创建一个机器人应用，获取 **AppID** 和 **AppSecret**。

### 2. 创建配置文件

创建 `~/.pi/agent/qq-integration-config.json`：

```json
{
  "appId": "你的 AppID",
  "appSecret": "你的 AppSecret"
}
```

### 3. 启动 pi

```bash
pi
```

扩展加载后，pi 会话启动时会**自动连接** QQ Bot（默认行为）。如需关闭自动连接，在配置文件加 `"autoConnect": false` 后重启 pi，再用 `/qq-connect` 手动连接；断开用 `/qq-disconnect`。

现在在 QQ 中给机器人发消息，就能和 pi 对话了。

---

## 配置项（qq-integration-config.json）

配置文件路径：`~/.pi/agent/qq-integration-config.json`（位于 pi 的 agent 数据目录，与扩展代码目录无关）。

### 顶层字段

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `appId` | string | ✅ | — | QQ 开放平台机器人应用的 AppID |
| `appSecret` | string | ✅ | — | QQ 开放平台机器人应用的 AppSecret（**敏感，勿提交 git**） |
| `instanceId` | string | ❌ | `PID` | 多实例下本实例的唯一 ID（默认取进程 PID，用于 `#to <PID>` 切换与消息署名） |
| `role` | `"auto" \| "leader" \| "follower"` | ❌ | `"auto"` | 多实例角色：`auto` 由文件锁自动选举；`leader` 强制持有 QQ 连接；`follower` 强制经 IPC 接入 leader |
| `autoConnect` | boolean | ❌ | `true` | pi 启动时是否自动连接 QQ Bot；设为 `false` 则需手动 `/qq-connect` |
| `allowedUsers` | string[] | ❌ | — | 允许向 pi 发 prompt 的 c2c 用户 openid 白名单。未配置则**放行所有**私聊消息（会输出安全告警）。强烈建议配置，以防远程提示词注入 |
| `allowedGroups` | string[] | ❌ | — | 允许向 pi 发 prompt 的群 openid 白名单。未配置则放行所有 @机器人消息 |

### `settings` 字段（转发设置）

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `forwardDesktopMessages` | boolean | `false` | 桌面端（pi 终端）输入的消息是否转发到 QQ |
| `forwardToolCalls` | boolean | `false` | 工具调用**及其结果**是否转发到 QQ（与 `lastMessageOnly` 互斥；开启一个会自动关闭另一个，配置文件与 `#settings` 均强制） |
| `lastMessageOnly` | boolean | `false` | 只转发整次 agent 运行的**最后一条** assistant 回复（与 `forwardToolCalls` 互斥；开启一个会自动关闭另一个） |
| `defaultSession` | object \| undefined | `undefined` | 默认 QQ 转发目标。收到 QQ 消息时会**自动更新**为该消息来源会话；也可由 `/qq-target` 或 QQ `#target` 设置 |

> `settings` 内的字段既可在配置文件里静态写死，也可在 QQ 内用 `#settings` 命令动态调整并持久化。`#settings` 命令对前两个开关使用了简写别名：`forwardMessages` 对应 `forwardDesktopMessages`，`forwardTools` 对应 `forwardToolCalls`。

### 完整示例

```json
{
  "appId": "你的 AppID",
  "appSecret": "你的 AppSecret",
  "autoConnect": true,
  "role": "auto",
  "settings": {
    "forwardDesktopMessages": false,
    "forwardToolCalls": false,
    "lastMessageOnly": false
  }
}
```

### 环境变量

| 变量 | 说明 |
|------|------|
| `QQ_INTEGRATION_DATA_DIR` | 覆盖数据目录（默认 `~/.pi/agent`） |
| `QQ_API_BASE` | 覆盖 QQ API 域名（默认 `https://api.sgroup.qq.com`） |
| `QQ_TOKEN_API` | 覆盖 Token API 地址（默认 `https://bots.qq.com/app/getAppAccessToken`） |

### 多实例

同时运行多个 pi 实例时，用文件锁选举唯一的 **leader** 持有 QQ 连接；其余实例作为 **follower** 经本地 IPC 把 QQ 收发委托给 leader（macOS/Linux 用 Unix socket，Windows 用命名管道）。

- `role: "auto"`（默认）：谁先抢到锁谁是 leader，其余自动成为 follower。
- `role: "leader"` / `"follower"`：强制角色。`follower` 即使 leader 宕机也**不会**尝试接管成为 leader。
- `instanceId`：默认即进程 PID，同时作为实例署名与 `#to <PID>` 定向路由的标识；仅在需要固定 ID 时手动设置。

**消息署名** — 所有发往 QQ 的消息都会带一段引用块署名，标明消息来自哪个实例：

```
> 【session名-3863】

<消息内容>
```

（session 名为当前 pi session 名；未命名时署名为 `> 【PID】`。）该署名同时是引用消息路由的兜底依据（见下）。

**引用消息定向路由** — 在 QQ 中**引用（回复）某条消息**时，消息会自动路由回发送被引用消息的那个实例（基于 `ref_idx` 映射，60 分钟 TTL；未命中时按被引用内容中的署名唯一匹配兜底）。也就是说，**想给某个特定实例发消息，直接引用它之前发的消息回复即可**。

---

## 架构

```
QQ 用户
  │
  ├─ 发消息 → QQ Bot 服务器 → WebSocket
  │                                │
  │                     ┌──────────▼──────────┐
  │                     │  pi-qq-integration  │
  │                     │  ws-client.ts       │
  │                     │    ↕ WebSocket      │
  │                     │  command-handler.ts │
  │                     │    ↕ #cmd 解析      │
  │                     │  index.ts           │
  │                     │    ↕ sendUserMessage│
  │                     └──────────┬──────────┘
  │                                │
  │                     ┌──────────▼──────────┐
  │                     │      pi 引擎         │
  │                     │   处理 prompt 并回复   │
  │                     └──────────┬──────────┘
  │                                │
  └─────── REST API ←──── 回复内容
```

两个独立通道：
- **WebSocket** — 接收 QQ 消息（长连接，带心跳和断线重连）
- **REST API** — 发送回复到 QQ，按会话类型 POST 到对应端点：`/v2/users/{openid}/messages`（c2c）、`/v2/groups/{group_openid}/messages`（群）、`/channels/{channel_id}/messages`（频道）

---

## pi Slash 命令

| 命令 | 说明 |
|------|------|
| `/qq-connect` | 手动连接 QQ Bot |
| `/qq-disconnect` | 断开 QQ Bot 连接 |
| `/qq-status` | 查看连接状态概览（角色、锁、WebSocket、Token） |
| `/qq-diagnose` | 查看详细诊断信息 |
| `/qq-logs` | 查看最近 30 条日志 |
| `/qq-logs-path` | 查看日志文件路径 |
| `/qq-logs-clear` | 清空日志文件 |
| `/qq-target` | 设置/查看默认 QQ 转发目标 |

---

## QQ 命令

在 QQ 中给机器人发送的消息，如果不以 `#` 开头，会直接作为 prompt 发给 pi。

| 命令 | 说明 |
|------|------|
| `#help` | 显示帮助 |
| `#sessions [页码]` | 跨项目列出全部 session，每页 10 条，最近使用在前 |
| `#history [N]` | 查看当前实例 session 的最近 N 条消息（默认 5） |
| `#target` | 将当前 QQ 会话设为默认转发目标 |
| `#settings` | 查看/修改转发设置（`#setting` 为别名） |
| `#instances` | 列出在线实例（ID、角色、认领会话的名字/最近消息摘要） |
| `#to <PID/名称> [内容]` | 查看当前绑定实例 / 切换会话到指定实例 / 向指定实例定向发送内容 |
| `#create <序号/名称>` | 创建新实例并复用指定 session |
| `#create new [--dir <目录>]` | 创建全新 session 的新实例（可指定工作目录）|
| `#close <PID> [PID...]` | 关闭实例（支持空格分隔多个 PID） |

### 桌面端消息转发

开启桌面端转发（`#settings forwardMessages on`）后，桌面端输入的消息会同步转发到 QQ。目标按优先级选择：

1. 最近一条 QQ 消息来源的会话（收到 QQ 消息时会自动更新 `defaultSession`）
2. 手动设置的默认目标（`/qq-target` 或 QQ `#target`）——仅在尚未收到任何 QQ 消息时生效

> **注：** 从 QQ 转发进 pi 的消息会带来源标签前缀（私聊 `[QQ]`、群聊 `[QQ群]`）。该前缀也用于识别并跳过桌面端回响，避免转发回环。

```bash
/qq-target c2c <用户openid> [备注]        # 私聊（备注可选）
/qq-target group <群openid> [备注]         # 群聊
/qq-target channel <频道id> [备注]         # 频道
/qq-target                                 # 查看当前目标（别名：show）
/qq-target clear                           # 清除
```

### `#settings` 示例

```
你: #settings
Bot: ## ⚙️ QQ Bot 设置
     | 选项 | 状态 | 说明 |
     | forwardMessages | ❌ 关 | 桌面端消息转发到 QQ |
     | forwardTools | ✅ 开 | 工具调用转发到 QQ |
     | lastMessageOnly | ❌ 关 | 只转发整次回复的最后一条 assistant 回复 |

你: #settings forwardTools on
Bot: ✅ **工具调用转发** 已开启，同时 `lastMessageOnly` 已自动关闭。

你: #settings lastMessageOnly on
Bot: ✅ **只转发最后一条回复** 已开启，assistant 整次运行仅发送一条最终回复；`forwardTools` 已自动关闭。
```

---

## 文件结构

```
pi-qq-integration/
├── index.ts              # 入口：初始化、事件、slash 命令
├── constants.ts          # 集中常量（路径、URL、超时值）
├── config.ts             # 配置文件读写（原子写入）
├── auth.ts               # Token 管理 + 自动刷新
├── lock.ts               # 文件锁（O_EXCL 原子创建）
├── ws-client.ts          # WebSocket 客户端
├── api-client.ts         # REST API 客户端
├── ipc.ts                # IPC（leader-follower 委派；Unix socket / Windows 命名管道）
├── registry.ts           # 实例注册表（原子写入）
├── routing.ts            # 引用消息路由纯函数（ref_idx + 署名兜底）
├── validation.ts         # session/sessionKey 校验、参考名清洗
├── session-manager.ts    # Session 浏览
├── command-handler.ts    # #命令解析
├── logger.ts             # 文件日志（自动截断）
├── types.ts              # 类型定义
└── package.json
```

---

## 多实例细节

```
~/.pi/agent/
├── qq-integration.lock          # 文件锁（O_EXCL 原子创建）
│   └─ JSON: { pid, startedAt, heartbeatAt } （心跳每 30 秒更新）
└── qq-integration/
    ├── registry.json             # 实例注册表（原子写入）
    └── instances/
        └── <pid>.sock            # IPC Unix socket（leader；Windows 为命名管道）
```

- 第一个实例获取锁 → 成为 leader → 连接 QQ Bot
- 后续实例检测到锁 → 成为 follower → 通过 IPC 连接 leader
- leader 崩溃或退出后 PID 失效 → follower 在重连循环中自动接管锁升级为 leader（故障转移）；`role: follower` 强制跟随时不升级
- leader 宕机期间 follower **静默**重试（仅写日志，不刷 UI 提示），连接恢复时才通知

---

## 日志

所有调试日志写入 `~/.pi/agent/qq-integration.log`。使用 `/qq-logs` 查看最近 30 条，`/qq-logs-path` 查看路径。日志文件达到 5 MB 自动截断。

---

## 注意事项

1. **Token 安全** — Token 有效期约 2 小时，自动刷新。连续 3 次刷新失败后自动断开并通知。
2. **消息频率** — 主动消息每月每用户/群限 4 条（QQ 官方平台限制），被动回复较宽松。
3. **Session 管理** — 用 `#create` 创建新实例（复用 session 或全新开始），`#sessions` 列出全部 session，`#close` 关闭实例。实例内不再支持会话切换（用 `#create` + `#to` 替代）。
4. **设置持久化** — `#settings` 变更保存到配置文件，`/reload` 不丢失。
5. **群聊消息** — 仅接收 @机器人的消息。
6. **配置文件** — 含 AppSecret，勿提交 git。

---

## 开发

```bash
cd ~/.pi/agent/extensions/pi-qq-integration
npm install          # 安装依赖
npm run build        # 编译 TypeScript
npm run typecheck    # 仅类型检查
npm test             # 运行测试套件（node:test，无额外开发依赖）
# 编辑代码后在 pi 中 /reload 热重载
```

**依赖** — 唯一的运行时第三方依赖是 [`ws`](https://www.npmjs.com/package/ws)（WebSocket 客户端，用于 `ws-client.ts`），其余全部使用 Node.js 内置模块。

---

## 贡献者

<a href="https://github.com/Star-233"><img src="https://github.com/Star-233.png?size=100" width="100" height="100" alt="Star-233" /></a>
<a href="https://github.com/illusionlie"><img src="https://github.com/illusionlie.png?size=100" width="100" height="100" alt="illusionlie" /></a>

感谢 [@illusionlie](https://github.com/illusionlie) 报告 Windows IPC bug (#1) 并提交修复 PR (#2)。
