21 Commits

Author SHA1 Message Date
sliverp
0c419d5e6d feat: 支持 qqimg 标签解析本地图片发送 2026-02-06 18:14:26 +08:00
sliverp
a62df11bc7 refactor: 调整日志输出格式 2026-02-06 18:01:30 +08:00
sliverp
8ccb231fda chore: 更新版本号至 1.4.1 2026-02-06 16:23:07 +08:00
sliverp
251a2f0e99 fix: 默认启用 QQ 机器人 markdown 支持 2026-02-06 16:16:25 +08:00
sliverp
1205ecc86e 111 2026-02-06 15:58:13 +08:00
sliverp
8cc6e034bf chore: 更新版本号至 1.3.14 2026-02-06 12:00:36 +08:00
sliverp
8b4704f649 feat: 添加命令授权状态计算 2026-02-05 21:11:00 +08:00
sliverp
0a6b0fe73a chore: 添加调试日志 2026-02-05 20:55:10 +08:00
sliverp
611b4f4323 refactor: 移除消息目标解析逻辑并设置默认权限 2026-02-05 20:35:12 +08:00
sliverp
0c22a89c6a feat: 支持 QQ Bot allowFrom 命令授权配置 2026-02-05 20:25:37 +08:00
sliverp
6c0b8058ff fix: 修复Openclaw命令处理逻辑 2026-02-05 19:42:01 +08:00
sliverp
15ea067d8d feat: 添加 CLI 插件管理工具 2026-02-05 17:05:33 +08:00
sliverp
98c7a598d8 docs: 更新 README 格式和内容 2026-02-05 11:24:58 +08:00
Bijin
4fefa8f15b Update README.md 2026-02-04 18:11:58 +08:00
Bijin
31926bac28 Update README.md 2026-02-04 16:58:47 +08:00
Bijin
7db7b7e2d6 Update README.md 2026-02-04 16:57:37 +08:00
sliverp
d6c55d61f4 docs: 更新定时提醒使用说明 2026-02-04 11:51:38 +08:00
sliverp
eda0579750 docs: 更新定时提醒命令使用说明 2026-02-04 11:50:05 +08:00
sliverp
24200f1a5f refactor: 移除 URL 点号处理逻辑 2026-02-04 11:08:14 +08:00
Bijin
484db3f90c Update README.md 2026-02-04 10:27:30 +08:00
Bijin
efcf627213 Merge pull request #32 from sliverp/main-merge
Main merge
2026-02-04 10:25:41 +08:00
8 changed files with 770 additions and 1050 deletions

261
README.md
View File

@@ -1,101 +1,96 @@
<div align="center">
# QQ Bot Channel Plugin for Moltbot
QQ 开放平台Bot API 的 Moltbot 渠道插件,支持 C2C 私聊、群聊 @消息、频道消息。
QQ 开放平台 Bot API 的 Moltbot 渠道插件,支持 C2C 私聊、群聊 @消息、频道消息。
## 功能特性
[![npm version](https://img.shields.io/badge/npm-v1.3.7-blue)](https://www.npmjs.com/package/@sliverp/qqbot)
[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
[![Platform](https://img.shields.io/badge/platform-Moltbot-orange)](https://github.com/sliverp/moltbot)
[![Node.js](https://img.shields.io/badge/Node.js->=18-339933)](https://nodejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6)](https://www.typescriptlang.org/)
- **多场景支持**C2C 单聊、QQ 群 @消息、频道公开消息、频道私信
- **自动重连**WebSocket 断连后自动重连,支持 Session Resume
- **消息去重**:自动管理 `msg_seq`,支持对同一消息多次回复
- **系统提示词**:可配置自定义系统提示词注入到 AI 请求
- **错误提示**AI 无响应时自动提示用户检查配置
</div>
## 使用示例:
<img width="952" height="582" alt="image" src="https://github.com/user-attachments/assets/a16d582b-708c-473e-b3a2-e0c4c503a0c8" />
---
## 版本更新
**使用`openclaw plugins list`来产看插件版本,建议使用最新版的版本**
<img width="902" height="248" alt="Clipboard_Screenshot_1769739939" src="https://github.com/user-attachments/assets/d6f37458-900c-4de9-8fdc-f8e6bf5c7ee5" />
## ⭐ Star 趋势
### 1.3.0(即将更新)
- 支持图片收发等功能
<img width="924" height="428" alt="Clipboard_Screenshot_1770112572" src="https://github.com/user-attachments/assets/80f38ae9-dc40-4545-ad17-e7e254064cf4" />
<!-- 在这里放置 Star 数量变化图 -->
- 支持定时任务到时后主动推送
<img width="930" height="288" alt="Clipboard_Screenshot_1770112539" src="https://github.com/user-attachments/assets/9674cda0-91e9-4860-8dcc-bc50007862a2" />
---
## 📸 使用示例
### 1.2.5-2026.02.02
- 解除URL发送限制现在可以直接在私聊发送URL
<img width="886" height="276" alt="Clipboard_Screenshot_1770092858" src="https://github.com/user-attachments/assets/c660949e-28a5-4e5f-abc2-77f0a2c67bad" />
- 更新Bot正在输入中状态
<img width="740" height="212" alt="Clipboard_Screenshot_1770091969" src="https://github.com/user-attachments/assets/47835c4b-ccd2-4782-aaa6-b873cb58f7d7" />
- 提供主动推送能力目前AI还不知道怎么调用主动推送相关完整Skill能力将在后续版本更新
- 优化一些已知问题
- 优化未收到未收到大模型响应时的提示信息
<img width="600" alt="使用示例" src="https://github.com/user-attachments/assets/6f1704ab-584b-497e-8937-96f84ce2958f" />
---
### 1.2.2-2026.01.31
- 支持发送文件
- 支持openclaw、moltbot命令行
- 修复[health]检查提示: [health] refresh failed: Cannot read properties of undefined (reading 'appId')的问题(不影响使用)
- 修复文件发送后clawdbot无法读取的问题
## ✨ 功能特性
### 1.2.1
- 解决了长时间使用会断联的问题
- 解决了频繁重连的问题
- 增加了大模型调用失败后的提示消息
- 🔒 **多场景支持** - C2C 私聊、群聊 @消息、频道消息、频道私信
- 🖼️ **富媒体消息** - 支持图片收发、文件发送
- **定时推送** - 支持定时任务到时后主动推送
- 🔗 **URL 无限制** - 私聊可直接发送 URL
- ⌨️ **输入状态** - Bot 正在输入中状态提示
- 🔄 **热更新** - 支持 npm 方式安装和热更新
- 📝 **Markdown** - 支持 Markdown 格式(即将更新)
---
### 1.1.0
- 解决了一些url会被拦截的问题
- 解决了多轮消息会发送失败的问题
- 修复了部分图片无法接受的问题
- 增加支持onboard的方式配置AppId 和 AppSecret
## 📦 安装
### 方式一:腾讯云 Lighthouse 镜像(最简单)
## 安装
[![Lighthouse](https://img.shields.io/badge/腾讯云-Lighthouse_镜像-00A4FF)](https://cloud.tencent.com/product/lighthouse)
在插件目录下执行:
直接使用预装好的腾讯云 Lighthouse 镜像,开箱即用,无需手动安装配置。
### 方式二npm 安装(推荐)
```bash
openclaw plugins install @sliverp/qqbot@1.3.7
```
### 方式三:源码安装
```bash
git clone https://github.com/sliverp/qqbot.git && cd qqbot
clawdbot plugins install . # 这一步会有点久,需要安装一些依赖。稍微耐心等待一下,尤其是小内存机器
clawdbot plugins install .
```
## 配置
> 💡 安装过程需要一些时间,尤其是小内存机器,请耐心等待
---
## ⚙️ 配置
### 1. 获取 QQ 机器人凭证
1. 访问 [QQ 开放平台](https://q.qq.com/)
2. 创建机器人应用
3. 获取 `AppID``AppSecret`ClientSecret
4. Token 格式`AppID:AppSecret`,例如 `102146862:Xjv7JVhu7KXkxANbp3HVjxCRgvAPeuAQ`
4. Token 格式`AppID:AppSecret`
### 2. 添加配置
#### 方式一:交互式配置
**交互式配置:**
```bash
clawdbot channels add
# 选择 qqbot按提示输入 Token
```
#### 方式二:命令行配置
**命令行配置:**
```bash
clawdbot channels add --channel qqbot --token "AppID:AppSecret"
```
示例:
```bash
clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
```
### 3. 手动编辑配置(可选)
也可以直接编辑 `~/.clawdbot/clawdbot.json`
编辑 `~/.clawdbot/clawdbot.json`
```json
{
@@ -103,27 +98,28 @@ clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
"qqbot": {
"enabled": true,
"appId": "你的AppID",
"clientSecret": "你的AppSecret",
"systemPrompt": "你是一个友好的助手"
"clientSecret": "你的AppSecret"
}
}
}
```
---
## 配置项说明
## 📋 配置项说明
| 配置项 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `appId` | string | | QQ 机器人 AppID |
| `clientSecret` | string | * | AppSecret`clientSecretFile` 二选一 |
| `clientSecretFile` | string | * | AppSecret 文件路径 |
| `enabled` | boolean | | 是否启用,默认 `true` |
| `name` | string | | 账户显示名称 |
| `systemPrompt` | string | | 自定义系统提示词 |
| `appId` | string | | QQ 机器人 AppID |
| `clientSecret` | string | * | AppSecret`clientSecretFile` 二选一 |
| `clientSecretFile` | string | * | AppSecret 文件路径 |
| `enabled` | boolean | | 是否启用,默认 `true` |
| `name` | string | | 账户显示名称 |
| `systemPrompt` | string | | 自定义系统提示词 |
## 支持的消息类型
---
## 📨 支持的消息类型
| 事件类型 | 说明 | Intent |
|----------|------|--------|
@@ -132,17 +128,17 @@ clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
| `AT_MESSAGE_CREATE` | 频道 @机器人消息 | `1 << 30` |
| `DIRECT_MESSAGE_CREATE` | 频道私信 | `1 << 12` |
## 使用
---
### 启动
## 🚀 使用
### 启动服务
后台启动
```bash
# 后台启动
clawdbot gateway restart
```
前台启动, 方便试试查看日志
```bash
# 前台启动查看日志
clawdbot gateway --port 18789 --verbose
```
@@ -153,23 +149,35 @@ clawdbot onboard
# 选择 QQ Bot 进行交互式配置
```
## 注意事项
---
1. **群消息**:需要在群内 @机器人 才能触发回复
2. **沙箱模式**:新创建的机器人默认在沙箱模式,需要添加测试用户
## ⚠️ 注意事项
## 升级
- **群消息**:需要在群内 @机器人 才能触发回复
- **沙箱模式**:新创建的机器人默认在沙箱模式,需要添加测试用户
如果需要升级插件,先运行升级脚本清理旧版本:
---
## 🔄 升级
### npm 热更新
```bash
npx -y @sliverp/qqbot@1.3.7 upgrade
```
> 热更新后无需重新配置 AppId 和 AppSecret
### 源码热更新
```bash
git clone https://github.com/sliverp/qqbot.git && cd qqbot
# 运行升级脚本(清理旧版本和配置)
# 运行升级脚本
bash ./scripts/upgrade.sh
# 重新安装插件
clawdbot plugins install . # 这一步会有点久,需要安装一些依赖。稍微耐心等待一下,尤其是小内存机器
# 重新安装
clawdbot plugins install .
# 重新配置
clawdbot channels add --channel qqbot --token "AppID:AppSecret"
@@ -178,49 +186,82 @@ clawdbot channels add --channel qqbot --token "AppID:AppSecret"
clawdbot gateway restart
```
升级脚本会自动
- 删除 `~/.clawdbot/extensions/qqbot` 目录
- 清理 `clawdbot.json` 中的 qqbot 相关配置
升级脚本会自动清理旧版本和配置。
## 开发
---
```bash
# 安装依赖
npm install
# 编译
npm run build
# 监听模式
npm run dev
```
---
## 文件结构
## 📚 版本历史
```
qqbot/
├── index.ts # 入口文件
├── src/
│ ├── api.ts # QQ Bot API 封装
│ ├── channel.ts # Channel Plugin 定义
│ ├── config.ts # 配置解析
│ ├── gateway.ts # WebSocket 网关
│ ├── onboarding.ts # CLI 配置向导
│ ├── outbound.ts # 出站消息处理
│ ├── runtime.ts # 运行时状态
│ └── types.ts # 类型定义
├── scripts/
│ └── upgrade.sh # 升级脚本
├── package.json
└── tsconfig.json
```
<details>
<summary><b>v1.4.0(即将更新)</b></summary>
## 相关链接
- 支持 Markdown 格式
</details>
<details>
<summary><b>v1.3.0 - 2026.02.03</b></summary>
- ✨ 支持图片收发等功能
- ✨ 支持定时任务到时后主动推送
- ✨ 支持使用 npm 等方式安装和升级
- 🐛 优化一些已知问题
</details>
<details>
<summary><b>v1.2.5 - 2026.02.02</b></summary>
- ✨ 解除 URL 发送限制
- ✨ 更新 Bot 正在输入中状态
- ✨ 提供主动推送能力
- 🐛 优化一些已知问题
</details>
<details>
<summary><b>v1.2.2 - 2026.01.31</b></summary>
- ✨ 支持发送文件
- ✨ 支持 openclaw、moltbot 命令行
- 🐛 修复 health 检查提示问题
- 🐛 修复文件发送后 clawdbot 无法读取的问题
</details>
<details>
<summary><b>v1.2.1</b></summary>
- 🐛 解决长时间使用会断联的问题
- 🐛 解决频繁重连的问题
- ✨ 增加大模型调用失败后的提示消息
</details>
<details>
<summary><b>v1.1.0</b></summary>
- 🐛 解决 URL 被拦截的问题
- 🐛 解决多轮消息发送失败的问题
- 🐛 修复部分图片无法接收的问题
- ✨ 增加支持 onboard 配置方式
</details>
---
## 🔗 相关链接
- [QQ 机器人官方文档](https://bot.q.qq.com/wiki/)
- [QQ 开放平台](https://q.qq.com/)
- [API v2 文档](https://bot.q.qq.com/wiki/develop/api-v2/)
## License
---
## 📄 License
MIT

227
bin/qqbot-cli.js Normal file
View File

@@ -0,0 +1,227 @@
#!/usr/bin/env node
/**
* QQBot CLI - 用于升级和管理 QQBot 插件
*
* 用法:
* npx @sliverp/qqbot upgrade # 升级插件
* npx @sliverp/qqbot install # 安装插件
*/
import { execSync } from 'child_process';
import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
import { homedir } from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 获取包的根目录
const PKG_ROOT = join(__dirname, '..');
const args = process.argv.slice(2);
const command = args[0];
// 检测使用的是 clawdbot 还是 openclaw
function detectInstallation() {
const home = homedir();
if (existsSync(join(home, '.openclaw'))) {
return 'openclaw';
}
if (existsSync(join(home, '.clawdbot'))) {
return 'clawdbot';
}
return null;
}
// 清理旧版本插件,返回旧的 qqbot 配置
function cleanupInstallation(appName) {
const home = homedir();
const appDir = join(home, `.${appName}`);
const configFile = join(appDir, `${appName}.json`);
const extensionDir = join(appDir, 'extensions', 'qqbot');
let oldQqbotConfig = null;
console.log(`\n>>> 处理 ${appName} 安装...`);
// 1. 先读取旧的 qqbot 配置
if (existsSync(configFile)) {
try {
const config = JSON.parse(readFileSync(configFile, 'utf8'));
if (config.channels?.qqbot) {
oldQqbotConfig = { ...config.channels.qqbot };
console.log('已保存旧的 qqbot 配置');
}
} catch (err) {
console.error('读取配置文件失败:', err.message);
}
}
// 2. 删除旧的扩展目录
if (existsSync(extensionDir)) {
console.log(`删除旧版本插件: ${extensionDir}`);
rmSync(extensionDir, { recursive: true, force: true });
} else {
console.log('未找到旧版本插件目录,跳过删除');
}
// 3. 清理配置文件中的 qqbot 相关字段
if (existsSync(configFile)) {
console.log('清理配置文件中的 qqbot 字段...');
try {
const config = JSON.parse(readFileSync(configFile, 'utf8'));
// 删除 channels.qqbot
if (config.channels?.qqbot) {
delete config.channels.qqbot;
console.log(' - 已删除 channels.qqbot');
}
// 删除 plugins.entries.qqbot
if (config.plugins?.entries?.qqbot) {
delete config.plugins.entries.qqbot;
console.log(' - 已删除 plugins.entries.qqbot');
}
// 删除 plugins.installs.qqbot
if (config.plugins?.installs?.qqbot) {
delete config.plugins.installs.qqbot;
console.log(' - 已删除 plugins.installs.qqbot');
}
writeFileSync(configFile, JSON.stringify(config, null, 2));
console.log('配置文件已更新');
} catch (err) {
console.error('清理配置文件失败:', err.message);
}
} else {
console.log(`未找到配置文件: ${configFile}`);
}
return oldQqbotConfig;
}
// 执行命令并继承 stdio
function runCommand(cmd, args = []) {
try {
execSync([cmd, ...args].join(' '), { stdio: 'inherit' });
return true;
} catch (err) {
return false;
}
}
// 升级命令
function upgrade() {
console.log('=== QQBot 插件升级脚本 ===');
let foundInstallation = null;
let savedConfig = null;
const home = homedir();
// 检查 openclaw
if (existsSync(join(home, '.openclaw'))) {
savedConfig = cleanupInstallation('openclaw');
foundInstallation = 'openclaw';
}
// 检查 clawdbot
if (existsSync(join(home, '.clawdbot'))) {
const clawdbotConfig = cleanupInstallation('clawdbot');
if (!savedConfig) savedConfig = clawdbotConfig;
foundInstallation = 'clawdbot';
}
if (!foundInstallation) {
console.log('\n未找到 clawdbot 或 openclaw 安装目录');
console.log('请确认已安装 clawdbot 或 openclaw');
process.exit(1);
}
console.log('\n=== 清理完成 ===');
// 自动安装插件
console.log('\n[1/2] 安装新版本插件...');
runCommand(foundInstallation, ['plugins', 'install', '@sliverp/qqbot']);
// 自动配置通道(使用保存的 appId 和 clientSecret
console.log('\n[2/2] 配置机器人通道...');
if (savedConfig?.appId && savedConfig?.clientSecret) {
const token = `${savedConfig.appId}:${savedConfig.clientSecret}`;
console.log(`使用已保存的配置: appId=${savedConfig.appId}`);
runCommand(foundInstallation, ['channels', 'add', '--channel', 'qqbot', '--token', `"${token}"`]);
// 恢复其他配置项(如 markdownSupport
if (savedConfig.markdownSupport !== undefined) {
runCommand(foundInstallation, ['config', 'set', 'channels.qqbot.markdownSupport', String(savedConfig.markdownSupport)]);
}
} else {
console.log('未找到已保存的 qqbot 配置,请手动配置:');
console.log(` ${foundInstallation} channels add --channel qqbot --token "AppID:AppSecret"`);
return;
}
console.log('\n=== 升级完成 ===');
console.log(`\n可以运行以下命令前台运行启动机器人:`);
console.log(` ${foundInstallation} gateway stop && ${foundInstallation} gateway --port 18789 --verbose`);
}
// 安装命令
function install() {
console.log('=== QQBot 插件安装 ===');
const cmd = detectInstallation();
if (!cmd) {
console.log('未找到 clawdbot 或 openclaw 安装');
console.log('请先安装 openclaw 或 clawdbot');
process.exit(1);
}
console.log(`\n使用 ${cmd} 安装插件...`);
runCommand(cmd, ['plugins', 'install', '@sliverp/qqbot']);
console.log('\n=== 安装完成 ===');
console.log('\n请配置机器人通道:');
console.log(` ${cmd} channels add --channel qqbot --token "AppID:AppSecret"`);
}
// 显示帮助
function showHelp() {
console.log(`
QQBot CLI - QQ机器人插件管理工具
用法:
npx @sliverp/qqbot <命令>
命令:
upgrade 清理旧版本插件(升级前执行)
install 安装插件到 openclaw/clawdbot
示例:
npx @sliverp/qqbot upgrade
npx @sliverp/qqbot install
`);
}
// 主入口
switch (command) {
case 'upgrade':
upgrade();
break;
case 'install':
install();
break;
case '-h':
case '--help':
case 'help':
showHelp();
break;
default:
if (command) {
console.log(`未知命令: ${command}`);
}
showHelp();
process.exit(command ? 1 : 0);
}

1153
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,22 @@
{
"name": "qqbot",
"version": "1.3.0",
"name": "@sliverp/qqbot",
"version": "1.4.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"qqbot": "./bin/qqbot-cli.js"
},
"files": [
"dist",
"bin",
"src",
"index.ts",
"tsconfig.json",
"openclaw.plugin.json",
"clawdbot.plugin.json",
"moltbot.plugin.json"
],
"clawdbot": {
"extensions": ["./index.ts"]
},
@@ -33,5 +46,6 @@
"clawdbot": "*",
"moltbot": "*",
"openclaw": "*"
}
},
"homepage": "https://github.com/sliverp/qqbot"
}

View File

@@ -120,6 +120,20 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
configured: Boolean(account?.appId && account?.clientSecret),
tokenSource: account?.secretSource,
}),
// 关键:解析 allowFrom 配置,用于命令授权
resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => {
const account = resolveQQBotAccount(cfg, accountId);
const allowFrom = account.config?.allowFrom ?? [];
console.log(`[qqbot] resolveAllowFrom: accountId=${accountId}, allowFrom=${JSON.stringify(allowFrom)}`);
return allowFrom.map((entry: string | number) => String(entry));
},
// 格式化 allowFrom 条目(移除 qqbot: 前缀,统一大写)
formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> }) =>
allowFrom
.map((entry: string | number) => String(entry).trim())
.filter(Boolean)
.map((entry: string) => entry.replace(/^qqbot:/i, ""))
.map((entry: string) => entry.toUpperCase()), // QQ openid 是大写的
},
setup: {
// 新增:规范化账户 ID
@@ -159,18 +173,6 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
});
},
},
// 新增:消息目标解析
messaging: {
normalizeTarget: (target) => {
// 支持格式: qqbot:openid, qqbot:group:xxx, openid, group:xxx
const normalized = target.replace(/^qqbot:/i, "");
return { ok: true, to: normalized };
},
targetResolver: {
looksLikeId: (id) => /^[A-F0-9]{32}$/i.test(id) || id.startsWith("group:") || id.startsWith("channel:"),
hint: "<openid> or group:<groupOpenid>",
},
},
outbound: {
deliveryMode: "direct",
chunker: chunkText,

View File

@@ -76,7 +76,7 @@ export function resolveQQBotAccount(
allowFrom: qqbot?.allowFrom,
systemPrompt: qqbot?.systemPrompt,
imageServerBaseUrl: qqbot?.imageServerBaseUrl,
markdownSupport: qqbot?.markdownSupport,
markdownSupport: qqbot?.markdownSupport ?? true,
};
appId = qqbot?.appId ?? "";
} else {
@@ -128,11 +128,16 @@ export function applyQQBotAccountConfig(
const next = { ...cfg };
if (accountId === DEFAULT_ACCOUNT_ID) {
// 如果没有设置过 allowFrom默认设置为 ["*"]
const existingConfig = (next.channels?.qqbot as QQBotChannelConfig) || {};
const allowFrom = existingConfig.allowFrom ?? ["*"];
next.channels = {
...next.channels,
qqbot: {
...(next.channels?.qqbot as Record<string, unknown> || {}),
enabled: true,
allowFrom,
...(input.appId ? { appId: input.appId } : {}),
...(input.clientSecret
? { clientSecret: input.clientSecret }
@@ -144,6 +149,10 @@ export function applyQQBotAccountConfig(
},
};
} else {
// 如果没有设置过 allowFrom默认设置为 ["*"]
const existingAccountConfig = (next.channels?.qqbot as QQBotChannelConfig)?.accounts?.[accountId] || {};
const allowFrom = existingAccountConfig.allowFrom ?? ["*"];
next.channels = {
...next.channels,
qqbot: {
@@ -154,6 +163,7 @@ export function applyQQBotAccountConfig(
[accountId]: {
...((next.channels?.qqbot as QQBotChannelConfig)?.accounts?.[accountId] || {}),
enabled: true,
allowFrom,
...(input.appId ? { appId: input.appId } : {}),
...(input.clientSecret
? { clientSecret: input.clientSecret }

View File

@@ -2,7 +2,7 @@ import WebSocket from "ws";
import path from "node:path";
import * as fs from "node:fs";
import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent, GroupMessageEvent } from "./types.js";
import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, sendC2CImageMessage, sendGroupImageMessage, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh } from "./api.js";
import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, sendC2CImageMessage, sendGroupImageMessage, initApiConfig, startBackgroundTokenRefresh, stopBackgroundTokenRefresh, sendC2CInputNotify } from "./api.js";
import { loadSession, saveSession, clearSession, type SessionState } from "./session-store.js";
import { recordKnownUser, flushKnownUsers } from "./known-users.js";
import { getQQBotRuntime } from "./runtime.js";
@@ -401,7 +401,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
groupOpenid?: string;
attachments?: Array<{ content_type: string; url: string; filename?: string }>;
}) => {
log?.info(`[qqbot:${account.accountId}] Processing message from ${event.senderId}: ${event.content}`);
log?.info(`[qqbot:${account.accountId}] Received Event: ${{event}}`);
if (event.attachments?.length) {
log?.info(`[qqbot:${account.accountId}] Attachments: ${event.attachments.length}`);
}
@@ -412,6 +412,13 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
direction: "inbound",
});
try{
await sendC2CInputNotify(accessToken, event.senderId, event.messageId, 60);
log?.info(`[qqbot:${account.accountId}] Sent input notify to ${event.senderId}`);
}catch(err){
log?.error(`[qqbot:${account.accountId}] sendC2CInputNotify error: ${err}`);
}
const isGroup = event.type === "guild" || event.type === "group";
const peerId = event.type === "guild" ? `channel:${event.channelId}`
: event.type === "group" ? `group:${event.groupOpenid}`
@@ -445,13 +452,14 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
- 群组 group_openid: ${event.groupOpenid}` : ""}
【定时提醒能力】
你可以帮助用户设置定时提醒。使用 openclaw cron 命令:
你可以帮助用户设置定时提醒。使用exec工具来来运行bash命令 openclaw cron 命令而不是直接使用 "cron" 工具
示例5分钟后提醒用户喝水
\`\`\`bash
openclaw cron add \\
--name "提醒喝水-${event.senderName || "用户"}" \\
--at "5m" \\
--session isolated \\
--message "💧 该喝水啦!" \\
--deliver \\
--channel qqbot \\
@@ -468,6 +476,7 @@ openclaw cron add \\
- \`--tz "Asia/Shanghai"\`: 周期任务务必设置时区
- \`--delete-after-run\`: 一次性任务必须添加此参数
- \`--message\`: 消息内容(必填,不能为空!这是定时提醒触发时直接发送给用户的内容)
- \`--session isolated\` 独立会话任务
⚠️ 重要注意事项:
1. --at 参数格式:相对时间用 \`5m\`\`1h\` 等(不要加 + 号!);绝对时间用完整 ISO 格式
@@ -490,7 +499,8 @@ openclaw cron add \\
⚠️ 注意:
- 必须使用 <qqimg>路径</qqimg> 格式
- 本地路径必须是绝对路径,支持 png、jpg、jpeg、gif、webp 格式
- 图片文件/URL 必须有效,否则发送失败`;
- 图片文件/URL 必须有效,否则发送失败
- Markdown格式下也必须使用该方式发送图片`;
const systemPrompts = [builtinPrompt];
if (account.systemPrompt) {
@@ -566,10 +576,15 @@ openclaw cron add \\
}
const userContent = event.content + attachmentInfo;
const messageBody = `【系统提示】\n${systemPrompts.join("\n")}\n\n【用户输入】\n${userContent}`;
let messageBody = `【系统提示】\n${systemPrompts.join("\n")}\n\n【用户输入】\n${userContent}`;
if(userContent.startsWith("/")){ // 保留Openclaw原始命令
messageBody = userContent
}
log?.info(`[qqbot:${account.accountId}] messageBody: ${messageBody}`);
const body = pluginRuntime.channel.reply.formatInboundEnvelope({
channel: "QQBot",
channel: "qqbot",
from: event.senderName ?? event.senderId,
timestamp: new Date(event.timestamp).getTime(),
body: messageBody,
@@ -588,6 +603,14 @@ openclaw cron add \\
: `qqbot:c2c:${event.senderId}`;
const toAddress = fromAddress;
// 计算命令授权状态
// allowFrom: ["*"] 表示允许所有人,否则检查 senderId 是否在 allowFrom 列表中
const allowFromList = account.config?.allowFrom ?? [];
const allowAll = allowFromList.length === 0 || allowFromList.some((entry: string) => entry === "*");
const commandAuthorized = allowAll || allowFromList.some((entry: string) =>
entry.toUpperCase() === event.senderId.toUpperCase()
);
const ctxPayload = pluginRuntime.channel.reply.finalizeInboundContext({
Body: body,
RawBody: event.content,
@@ -608,6 +631,7 @@ openclaw cron add \\
QQChannelId: event.channelId,
QQGuildId: event.guildId,
QQGroupOpenid: event.groupOpenid,
CommandAuthorized: commandAuthorized,
});
// 发送消息的辅助函数,带 token 过期重试
@@ -808,7 +832,7 @@ openclaw cron add \\
log?.info(`[qqbot:${account.accountId}] Sent image via <qqimg> tag: ${imagePath.slice(0, 60)}...`);
} catch (err) {
log?.error(`[qqbot:${account.accountId}] Failed to send image from <qqimg>: ${err}`);
await sendErrorMessage(`发送图片失败: ${err}`);
await sendErrorMessage(`图片发送失败,图片似乎不存在哦,图片路径:${imagePath}`);
}
}
}
@@ -1055,6 +1079,68 @@ openclaw cron add \\
// 这些标记可能被 AI 错误地学习并输出
textWithoutImages = filterInternalMarkers(textWithoutImages);
// ============ 提取并处理 <qqimg> 标签中的本地图片 ============
// 在所有发送逻辑前,先检测 <qqimg> 标签,提取本地图片并通过富媒体 API 发送
// 然后从文本中移除这些标签
const qqimgRegexInMarkdown = /<qqimg>([^<>]+)<\/(?:qqimg|img)>/gi;
const qqimgMatchesInText = [...textWithoutImages.matchAll(qqimgRegexInMarkdown)];
const localImagesFromQqimg: string[] = []; // 存储需要通过富媒体 API 发送的本地图片
if (qqimgMatchesInText.length > 0) {
log?.info(`[qqbot:${account.accountId}] Found ${qqimgMatchesInText.length} <qqimg> tag(s) in text, processing...`);
for (const match of qqimgMatchesInText) {
const imagePath = match[1]?.trim();
if (!imagePath) continue;
// 从文本中移除 <qqimg> 标签
textWithoutImages = textWithoutImages.replace(match[0], "");
// 判断是本地文件还是 URL
const isLocalPath = imagePath.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(imagePath);
const isHttpUrl = imagePath.startsWith("http://") || imagePath.startsWith("https://");
if (isLocalPath) {
// 本地文件:转换为 Base64 Data URL
if (!fs.existsSync(imagePath)) {
log?.error(`[qqbot:${account.accountId}] <qqimg> Image file not found: ${imagePath}`);
continue;
}
try {
const fileBuffer = fs.readFileSync(imagePath);
const base64Data = fileBuffer.toString("base64");
const ext = path.extname(imagePath).toLowerCase();
const mimeTypes: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
};
const mimeType = mimeTypes[ext];
if (!mimeType) {
log?.error(`[qqbot:${account.accountId}] <qqimg> Unsupported image format: ${ext}`);
continue;
}
const imageUrl = `data:${mimeType};base64,${base64Data}`;
localImagesFromQqimg.push(imageUrl);
log?.info(`[qqbot:${account.accountId}] <qqimg> Converted local image to Base64 (size: ${fileBuffer.length} bytes): ${imagePath}`);
} catch (readErr) {
log?.error(`[qqbot:${account.accountId}] <qqimg> Failed to read image file: ${readErr}`);
}
} else if (isHttpUrl) {
// HTTP URL添加到图片列表
imageUrls.push(imagePath);
log?.info(`[qqbot:${account.accountId}] <qqimg> Added HTTP URL to imageUrls: ${imagePath}`);
}
}
// 清理多余的空行
textWithoutImages = textWithoutImages.replace(/\n{3,}/g, "\n\n").trim();
}
// 根据模式处理图片
if (useMarkdown) {
// ============ Markdown 模式 ============
@@ -1064,7 +1150,7 @@ openclaw cron add \\
// 分离图片:公网 URL vs Base64/本地文件
const httpImageUrls: string[] = []; // 公网 URL用于 Markdown 嵌入
const base64ImageUrls: string[] = []; // Base64用于富媒体 API
const base64ImageUrls: string[] = [...localImagesFromQqimg]; // Base64用于富媒体 API(包含从 <qqimg> 提取的本地图片)
for (const url of imageUrls) {
if (url.startsWith("data:image/")) {
@@ -1185,12 +1271,7 @@ openclaw cron add \\
for (const match of bareUrlMatches) {
textWithoutImages = textWithoutImages.replace(match[0], "").trim();
}
// 处理文本中的 URL 点号(防止被 QQ 解析为链接)
if (textWithoutImages) {
textWithoutImages = textWithoutImages.replace(/([a-zA-Z0-9])\.([a-zA-Z0-9])/g, "$1_$2");
}
try {
// 发送图片(通过富媒体 API
for (const imageUrl of imageUrls) {
@@ -1253,7 +1334,9 @@ openclaw cron add \\
}
},
},
replyOptions: {},
replyOptions: {
disableBlockStreaming: false,
},
});
// 等待分发完成或超时

View File

@@ -21,6 +21,7 @@ interface QQBotChannelConfig {
clientSecretFile?: string;
name?: string;
imageServerBaseUrl?: string;
allowFrom?: string[];
accounts?: Record<string, {
enabled?: boolean;
appId?: string;
@@ -28,6 +29,7 @@ interface QQBotChannelConfig {
clientSecretFile?: string;
name?: string;
imageServerBaseUrl?: string;
allowFrom?: string[];
}>;
}
@@ -137,6 +139,7 @@ statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecr
qqbot: {
...(next.channels?.qqbot as Record<string, unknown> || {}),
enabled: true,
allowFrom: resolvedAccount.config?.allowFrom ?? ["*"],
},
},
};
@@ -200,6 +203,9 @@ statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecr
).trim();
}
// 默认允许所有人执行命令(用户无感知)
const allowFrom: string[] = resolvedAccount.config?.allowFrom ?? ["*"];
// 应用配置
if (appId && clientSecret) {
if (accountId === DEFAULT_ACCOUNT_ID) {
@@ -212,6 +218,7 @@ statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecr
enabled: true,
appId,
clientSecret,
allowFrom,
},
},
};
@@ -230,6 +237,7 @@ statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecr
enabled: true,
appId,
clientSecret,
allowFrom,
},
},
},