Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/changelog/1.6.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 1.6.1

## 中文

### ✨ 新增

- **`/stop` 紧急命令支持**(PR #134 by [@jerryliang122](https://github.com/jerryliang122)):用户发送 `/stop` 时,命令会立即执行,不进入消息队列。同时清空该用户所有待处理的排队消息,实现即时中断 AI 对话的能力。
- **Windows 升级脚本**(PR #130 by [@anyangmvp](https://github.com/anyangmvp)):新增 `scripts/upgrade.bat`,为 Windows 用户提供与 `upgrade.sh` 等价的一键升级脚本,自动清理旧插件目录和配置文件中的 qqbot 字段,并输出重新安装指引。

---

## English

### ⬆️ Improvements

- **Improved cron task execution accuracy**: Enhanced the execution precision of scheduled tasks (cron skill) for more accurate trigger timing.

### ✨ New

- **`/stop` urgent command support** (PR #134 by [@jerryliang122](https://github.com/jerryliang122)): When a user sends `/stop`, the command is executed immediately without entering the message queue. All pending queued messages for that user are also cleared, enabling instant interruption of ongoing AI conversations.
- **Windows upgrade script** (PR #130 by [@anyangmvp](https://github.com/anyangmvp)): Added `scripts/upgrade.bat` as a Windows equivalent to `upgrade.sh`. It automatically cleans up the old plugin directory and removes qqbot-related fields from the config file, then prints reinstall instructions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sliverp/qqbot",
"version": "1.6.0",
"version": "1.6.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
66 changes: 66 additions & 0 deletions scripts/upgrade.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
@echo off
setlocal enabledelayedexpansion

echo === QQBot Upgrade Script ===

set "foundInstallation="

set "clawdbotDir=%USERPROFILE%\.clawdbot"
if exist "%clawdbotDir%\" (
call :CleanupInstallation clawdbot
set "foundInstallation=clawdbot"
)

set "openclawDir=%USERPROFILE%\.openclaw"
if exist "%openclawDir%\" (
call :CleanupInstallation openclaw
set "foundInstallation=openclaw"
)

if "%foundInstallation%"=="" (
echo clawdbot or openclaw not found
exit /b 1
)

set "cmd=%foundInstallation%"

echo.
echo === Cleanup Complete ===
echo.
echo Run these commands to reinstall:
for %%I in ("%~dp0..") do set "qqbotDir=%%~fI"
echo cd %qqbotDir%
echo %cmd% plugins install .
echo %cmd% channels add --channel qqbot --token "AppID:AppSecret"
echo %cmd% gateway restart
exit /b 0

:CleanupInstallation
set "AppName=%~1"
set "appDir=%USERPROFILE%\.%AppName%"
set "configFile=%appDir%\%AppName%.json"
set "extensionDir=%appDir%\extensions\qqbot"

echo.
echo ^>^>^> Processing %AppName% installation...

if exist "%extensionDir%\" (
echo Deleting old plugin: %extensionDir%
rd /s /q "%extensionDir%" 2>nul || (
echo Warning: Could not delete %extensionDir% ^(permission denied^)
echo Please delete it manually if needed
)
) else (
echo Old plugin directory not found, skipping
)

if exist "%configFile%" (
echo Cleaning qqbot fields from config...
set "configPath=%configFile:\=/%"
node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('!configPath!','utf8'));if(c.channels&&c.channels.qqbot){delete c.channels.qqbot;console.log(' - deleted channels.qqbot');}if(c.plugins&&c.plugins.entries&&c.plugins.entries.qqbot){delete c.plugins.entries.qqbot;console.log(' - deleted plugins.entries.qqbot');}if(c.plugins&&c.plugins.installs&&c.plugins.installs.qqbot){delete c.plugins.installs.qqbot;console.log(' - deleted plugins.installs.qqbot');}if(c.plugins&&c.plugins.allow&&Array.isArray(c.plugins.allow)){const i=c.plugins.allow.indexOf('qqbot');if(i^!==-1){c.plugins.allow.splice(i,1);console.log(' - deleted qqbot from plugins.allow array');}}fs.writeFileSync('!configPath!',JSON.stringify(c,null,2));console.log('Config file updated');" || (
echo Warning: Node.js error
)
) else (
echo Config file not found: %configFile%
)
exit /b 0
92 changes: 92 additions & 0 deletions skills/qqbot-cron/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,98 @@ metadata: {"openclaw":{"emoji":"⏰","requires":{"config":["channels.qqbot"]}}}

---

## 🛠️ 两种调用方式

### 方式一:cron 工具调用(推荐)

如果你的环境中有 `cron` 工具,使用 JSON 参数直接调用:

```javascript
cron({
"action": "add",
"job": {
"name": "喝水提醒",
"schedule": { "kind": "at", "atMs": 1770734300000 },
"sessionTarget": "isolated",
"wakeMode": "now",
"deleteAfterRun": true,
"payload": {
"kind": "agentTurn",
"message": "暖心提醒内容",
"deliver": true,
"channel": "qqbot",
"to": "qqbot:c2c:0DEBF031..."
}
}
})
```

### 方式二:exec 工具执行 CLI 命令(备用)

⚠️ **如果没有 cron 工具,使用 exec 工具调用 openclaw CLI 命令。**

**重要区别:CLI 参数名和 JSON API 不一样!**

| JSON API 字段 | CLI 参数 |
|--------------|----------|
| `action: "add"` | `openclaw cron add` |
| `job.schedule.kind: "at"` | `--at "30s"` 或 `--at "2026-03-14T10:00:00Z"` |
| `job.schedule.kind: "cron"` | `--cron "0 8 * * *" --tz "Asia/Shanghai"` |
| `job.schedule.atMs` | `--at "30s"`(相对时间)或 ISO 时间字符串 |
| `job.sessionTarget: "isolated"` | `--session isolated` |
| `job.wakeMode: "now"` | `--wake now` |
| `job.deleteAfterRun: true` | `--delete-after-run` |
| `job.payload.kind: "agentTurn"` | `--message "..."` |
| `job.payload.message` | `--message "..."` |
| `job.payload.deliver: true` | `--announce` |
| `job.payload.channel: "qqbot"` | `--channel qqbot` |
| `job.payload.to` | `--to "qqbot:c2c:..."` |

**CLI 命令示例(一次性提醒):**

```bash
exec({
command: 'openclaw cron add --name "喝水提醒" --at "30s" --session isolated --wake now --delete-after-run --announce --channel qqbot --to "qqbot:c2c:0DEBF031A9738F49D0194257976D7BAE" --message "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:该喝水了。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"'
})
```

**CLI 命令示例(周期提醒):**

```bash
exec({
command: 'openclaw cron add --name "打卡提醒" --cron "0 8 * * *" --tz "Asia/Shanghai" --session isolated --wake now --announce --channel qqbot --to "qqbot:c2c:0DEBF031A9738F49D0194257976D7BAE" --message "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:该打卡了。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"'
})
```

**查询提醒(CLI):**

```bash
exec({ command: 'openclaw cron list --json' })
```

**删除提醒(CLI):**

```bash
exec({ command: 'openclaw cron rm <job-id>' })
```

**⚠️ CLI 时间格式特殊说明:**

- `--at` 参数支持:
- 相对时间:`"30s"`, `"5m"`, `"1h"`(推荐用于短时间提醒)
- ISO 时间:`"2026-03-14T10:00:00Z"`
- ❌ 不支持:毫秒时间戳(如 `1770734300000`)

- JSON API 的 `schedule.atMs` 必须是毫秒时间戳,需要自己计算
- CLI 的 `--at` 可以直接用 `"30s"` 这样的相对时间,更方便

**如何选择?**
1. 优先使用 `cron` 工具(如果可用)
2. 如果 `cron` 工具不可用,用 `exec` 执行 CLI 命令
3. 可以通过 `openclaw cron add --help` 查看完整参数列表

---

## 🤖 AI 决策指南

### 时间确认规则
Expand Down
30 changes: 30 additions & 0 deletions src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,10 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
// ============ 按用户并发的消息队列(同用户串行,跨用户并行) ============
// 每个用户有独立队列,同一用户的消息串行处理(保持时序),
// 不同用户的消息并行处理(互不阻塞)。

// 紧急命令列表:这些命令会立即执行,不进入队列
const URGENT_COMMANDS = ["/stop"];

const userQueues = new Map<string, QueuedMessage[]>(); // peerId → 消息队列
const activeUsers = new Set<string>(); // 正在处理中的用户
let messagesProcessed = 0;
Expand All @@ -499,6 +503,32 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {

const enqueueMessage = (msg: QueuedMessage): void => {
const peerId = getMessagePeerId(msg);
const content = (msg.content ?? "").trim().toLowerCase();

// 检测是否为紧急命令
const isUrgentCommand = URGENT_COMMANDS.some(cmd => content.startsWith(cmd.toLowerCase()));

if (isUrgentCommand) {
log?.info(`[qqbot:${account.accountId}] Urgent command detected: ${content.slice(0, 20)}, executing immediately`);

// 清空该用户队列中所有待处理消息
const queue = userQueues.get(peerId);
if (queue) {
const droppedCount = queue.length;
queue.length = 0; // 清空队列
totalEnqueued = Math.max(0, totalEnqueued - droppedCount);
log?.info(`[qqbot:${account.accountId}] Dropped ${droppedCount} queued messages for ${peerId} due to urgent command`);
}

// 立即异步执行紧急命令,不等待
if (handleMessageFnRef) {
handleMessageFnRef(msg).catch(err => {
log?.error(`[qqbot:${account.accountId}] Urgent command error: ${err}`);
});
}
return;
}

let queue = userQueues.get(peerId);
if (!queue) {
queue = [];
Expand Down
Loading