@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -15
- package/README.zh.md +3 -16
- package/dist/src/admin-resolver.d.ts +12 -6
- package/dist/src/admin-resolver.js +69 -34
- package/dist/src/api.d.ts +105 -1
- package/dist/src/api.js +164 -15
- package/dist/src/channel.js +13 -0
- package/dist/src/config.js +3 -10
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.js +450 -248
- package/dist/src/image-server.d.ts +27 -8
- package/dist/src/image-server.js +179 -71
- package/dist/src/inbound-attachments.d.ts +3 -1
- package/dist/src/inbound-attachments.js +28 -14
- package/dist/src/outbound-deliver.js +77 -148
- package/dist/src/outbound.d.ts +6 -4
- package/dist/src/outbound.js +266 -442
- package/dist/src/reply-dispatcher.js +4 -4
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/slash-commands.js +277 -32
- package/dist/src/startup-greeting.d.ts +5 -5
- package/dist/src/startup-greeting.js +32 -13
- package/dist/src/streaming.d.ts +244 -0
- package/dist/src/streaming.js +907 -0
- package/dist/src/tools/remind.js +11 -10
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +17 -1
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/audio-convert.d.ts +9 -0
- package/dist/src/utils/audio-convert.js +51 -0
- package/dist/src/utils/chunked-upload.d.ts +59 -0
- package/dist/src/utils/chunked-upload.js +289 -0
- package/dist/src/utils/file-utils.d.ts +7 -1
- package/dist/src/utils/file-utils.js +24 -2
- package/dist/src/utils/media-send.d.ts +147 -0
- package/dist/src/utils/media-send.js +434 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +5 -5
- package/node_modules/ws/lib/websocket.js +6 -6
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -22
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/upgrade-via-npm.ps1 +161 -6
- package/scripts/upgrade-via-npm.sh +311 -104
- package/scripts/upgrade-via-source.sh +117 -0
- package/skills/qqbot-media/SKILL.md +9 -5
- package/skills/qqbot-remind/SKILL.md +3 -3
- package/src/admin-resolver.ts +76 -35
- package/src/api.ts +284 -12
- package/src/channel.ts +12 -0
- package/src/config.ts +3 -10
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +277 -67
- package/src/image-server.ts +213 -77
- package/src/inbound-attachments.ts +32 -15
- package/src/outbound-deliver.ts +77 -157
- package/src/outbound.ts +304 -451
- package/src/reply-dispatcher.ts +4 -4
- package/src/request-context.ts +39 -0
- package/src/slash-commands.ts +303 -33
- package/src/startup-greeting.ts +35 -13
- package/src/streaming.ts +1096 -0
- package/src/tools/remind.ts +15 -11
- package/src/types.ts +111 -0
- package/src/update-checker.ts +2 -7
- package/src/utils/audio-convert.ts +56 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +28 -2
- package/src/utils/media-send.ts +563 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/clawdbot.plugin.json +0 -16
- package/dist/src/user-messages.d.ts +0 -8
- package/dist/src/user-messages.js +0 -8
- package/moltbot.plugin.json +0 -16
- package/scripts/upgrade-via-alt-pkg.sh +0 -307
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
- package/src/gateway.log +0 -43
- package/src/openclaw-2026-03-21.log +0 -3729
- package/src/user-messages.ts +0 -7
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { getAccessToken, sendC2CMessage, sendChannelMessage, sendGroupMessage, clearTokenCache, sendC2CImageMessage, sendGroupImageMessage, sendC2CVoiceMessage, sendGroupVoiceMessage, sendC2CVideoMessage, sendGroupVideoMessage, sendC2CFileMessage, sendGroupFileMessage } from "./api.js";
|
|
3
3
|
import { parseQQBotPayload, encodePayloadForCron, isCronReminderPayload, isMediaPayload } from "./utils/payload.js";
|
|
4
4
|
import { resolveTTSConfig, textToSilk, formatDuration } from "./utils/audio-convert.js";
|
|
5
|
-
import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize } from "./utils/file-utils.js";
|
|
5
|
+
import { checkFileSize, readFileAsync, fileExistsAsync, formatFileSize, getMaxUploadSize } from "./utils/file-utils.js";
|
|
6
6
|
import { getQQBotDataDir, normalizePath, sanitizeFileName } from "./utils/platform.js";
|
|
7
7
|
/**
|
|
8
8
|
* 带 token 过期重试的消息发送
|
|
@@ -121,7 +121,7 @@ async function handleImagePayload(ctx, payload) {
|
|
|
121
121
|
log?.error(`[qqbot:${account.accountId}] Image not found: ${imageUrl}`);
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
const imgSzCheck = checkFileSize(imageUrl);
|
|
124
|
+
const imgSzCheck = checkFileSize(imageUrl, getMaxUploadSize(1)); // IMAGE = 1
|
|
125
125
|
if (!imgSzCheck.ok) {
|
|
126
126
|
log?.error(`[qqbot:${account.accountId}] Image size check failed: ${imgSzCheck.error}`);
|
|
127
127
|
return;
|
|
@@ -230,7 +230,7 @@ async function handleVideoPayload(ctx, payload) {
|
|
|
230
230
|
if (!(await fileExistsAsync(videoPath))) {
|
|
231
231
|
throw new Error(`视频文件不存在: ${videoPath}`);
|
|
232
232
|
}
|
|
233
|
-
const vPaySzCheck = checkFileSize(videoPath);
|
|
233
|
+
const vPaySzCheck = checkFileSize(videoPath, getMaxUploadSize(2)); // VIDEO = 2
|
|
234
234
|
if (!vPaySzCheck.ok) {
|
|
235
235
|
throw new Error(vPaySzCheck.error);
|
|
236
236
|
}
|
|
@@ -285,7 +285,7 @@ async function handleFilePayload(ctx, payload) {
|
|
|
285
285
|
if (!(await fileExistsAsync(filePath))) {
|
|
286
286
|
throw new Error(`文件不存在: ${filePath}`);
|
|
287
287
|
}
|
|
288
|
-
const fPaySzCheck = checkFileSize(filePath);
|
|
288
|
+
const fPaySzCheck = checkFileSize(filePath, getMaxUploadSize(4)); // FILE = 4
|
|
289
289
|
if (!fPaySzCheck.ok) {
|
|
290
290
|
throw new Error(fPaySzCheck.error);
|
|
291
291
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RequestContext {
|
|
2
|
+
/** 投递目标地址,如 qqbot:c2c:xxx 或 qqbot:group:xxx */
|
|
3
|
+
target: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 在请求级作用域中执行回调。
|
|
7
|
+
* 作用域内所有同步/异步代码都能通过 getRequestContext() 获取上下文。
|
|
8
|
+
*/
|
|
9
|
+
export declare function runWithRequestContext<T>(ctx: RequestContext, fn: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* 获取当前请求的上下文,不存在时返回 undefined。
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRequestContext(): RequestContext | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* 获取当前请求的投递目标地址。
|
|
16
|
+
* 便捷方法,等价于 getRequestContext()?.target。
|
|
17
|
+
*/
|
|
18
|
+
export declare function getRequestTarget(): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求级上下文(基于 AsyncLocalStorage)
|
|
3
|
+
*
|
|
4
|
+
* 解决并发消息下工具获取当前会话信息的竞态问题。
|
|
5
|
+
* gateway 在处理每条入站消息时通过 runWithRequestContext() 建立作用域,
|
|
6
|
+
* 作用域内的所有异步代码(包括 AI agent 调用、tool execute)
|
|
7
|
+
* 都能通过 getRequestContext() 安全地拿到当前请求的上下文。
|
|
8
|
+
*/
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
const asyncLocalStorage = new AsyncLocalStorage();
|
|
11
|
+
/**
|
|
12
|
+
* 在请求级作用域中执行回调。
|
|
13
|
+
* 作用域内所有同步/异步代码都能通过 getRequestContext() 获取上下文。
|
|
14
|
+
*/
|
|
15
|
+
export function runWithRequestContext(ctx, fn) {
|
|
16
|
+
return asyncLocalStorage.run(ctx, fn);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 获取当前请求的上下文,不存在时返回 undefined。
|
|
20
|
+
*/
|
|
21
|
+
export function getRequestContext() {
|
|
22
|
+
return asyncLocalStorage.getStore();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 获取当前请求的投递目标地址。
|
|
26
|
+
* 便捷方法,等价于 getRequestContext()?.target。
|
|
27
|
+
*/
|
|
28
|
+
export function getRequestTarget() {
|
|
29
|
+
return asyncLocalStorage.getStore()?.target;
|
|
30
|
+
}
|
|
@@ -18,16 +18,9 @@ import { getUpdateInfo, checkVersionExists } from "./update-checker.js";
|
|
|
18
18
|
import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
|
|
19
19
|
import { saveCredentialBackup } from "./credential-backup.js";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { getPackageVersion } from "./utils/pkg-version.js";
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
22
|
-
|
|
23
|
-
let PLUGIN_VERSION = "unknown";
|
|
24
|
-
try {
|
|
25
|
-
const pkg = require("../package.json");
|
|
26
|
-
PLUGIN_VERSION = pkg.version ?? "unknown";
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// fallback
|
|
30
|
-
}
|
|
23
|
+
let PLUGIN_VERSION = getPackageVersion(import.meta.url);
|
|
31
24
|
// 获取 openclaw 框架版本(缓存结果,只执行一次)
|
|
32
25
|
let _frameworkVersion = null;
|
|
33
26
|
function getFrameworkVersion() {
|
|
@@ -249,16 +242,21 @@ registerCommand({
|
|
|
249
242
|
`列出所有可用的 QQBot 插件内置指令及其简要说明。`,
|
|
250
243
|
`使用 /指令名 ? 可查看某条指令的详细用法。`,
|
|
251
244
|
].join("\n"),
|
|
252
|
-
handler: () => {
|
|
245
|
+
handler: (ctx) => {
|
|
246
|
+
// 群聊场景排除仅限私聊的指令
|
|
247
|
+
const GROUP_EXCLUDED_COMMANDS = new Set(["bot-upgrade", "bot-clear-storage"]);
|
|
248
|
+
const isGroup = ctx.type === "group";
|
|
253
249
|
const lines = [`### QQBot插件内置调试指令`, ``];
|
|
254
250
|
for (const [name, cmd] of commands) {
|
|
251
|
+
if (isGroup && GROUP_EXCLUDED_COMMANDS.has(name))
|
|
252
|
+
continue;
|
|
255
253
|
lines.push(`<qqbot-cmd-input text="/${name}" show="/${name}"/> ${cmd.description}`);
|
|
256
254
|
}
|
|
257
255
|
lines.push(``, `> 插件版本 v${PLUGIN_VERSION}`);
|
|
258
256
|
return lines.join("\n");
|
|
259
257
|
},
|
|
260
258
|
});
|
|
261
|
-
const DEFAULT_UPGRADE_URL = "https://
|
|
259
|
+
const DEFAULT_UPGRADE_URL = "https://docs.qq.com/doc/DSGxOZk1oVnVKVkpq";
|
|
262
260
|
function saveUpgradeGreetingTarget(accountId, appId, openid) {
|
|
263
261
|
const safeAccountId = accountId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
264
262
|
const safeAppId = appId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -568,6 +566,38 @@ function findPowerShell() {
|
|
|
568
566
|
}
|
|
569
567
|
return null;
|
|
570
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* 将升级脚本复制到临时位置,避免升级过程中插件目录被清理后脚本丢失。
|
|
571
|
+
* 返回临时脚本路径,失败返回 null。
|
|
572
|
+
*/
|
|
573
|
+
function copyScriptToTemp(scriptPath) {
|
|
574
|
+
try {
|
|
575
|
+
const ext = path.extname(scriptPath);
|
|
576
|
+
const tmpDir = path.join(getHomeDir(), ".openclaw", ".qqbot-upgrade-tmp");
|
|
577
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
578
|
+
const tmpScript = path.join(tmpDir, `upgrade-via-npm${ext}`);
|
|
579
|
+
fs.copyFileSync(scriptPath, tmpScript);
|
|
580
|
+
if (!isWindows()) {
|
|
581
|
+
fs.chmodSync(tmpScript, 0o755);
|
|
582
|
+
}
|
|
583
|
+
return tmpScript;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 清理临时升级脚本目录
|
|
591
|
+
*/
|
|
592
|
+
function cleanupTempScript() {
|
|
593
|
+
try {
|
|
594
|
+
const tmpDir = path.join(getHomeDir(), ".openclaw", ".qqbot-upgrade-tmp");
|
|
595
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
// 非关键,静默忽略
|
|
599
|
+
}
|
|
600
|
+
}
|
|
571
601
|
/**
|
|
572
602
|
* 执行热更新:执行脚本(--no-restart) → 立即触发 gateway restart
|
|
573
603
|
*
|
|
@@ -577,11 +607,15 @@ function findPowerShell() {
|
|
|
577
607
|
* - 新进程启动时 getStartupGreeting() 检测到版本变更,自动通知管理员
|
|
578
608
|
*
|
|
579
609
|
* Windows 使用 PowerShell 执行 .ps1 脚本,Mac/Linux 使用 bash 执行 .sh 脚本。
|
|
610
|
+
*
|
|
611
|
+
* 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
|
|
580
612
|
*/
|
|
581
613
|
function fireHotUpgrade(targetVersion) {
|
|
582
|
-
const
|
|
583
|
-
if (!
|
|
614
|
+
const originalScriptPath = getUpgradeScriptPath();
|
|
615
|
+
if (!originalScriptPath)
|
|
584
616
|
return { ok: false, reason: "no-script" };
|
|
617
|
+
// 将脚本复制到临时位置,避免升级过程中脚本被删除
|
|
618
|
+
const scriptPath = copyScriptToTemp(originalScriptPath) || originalScriptPath;
|
|
585
619
|
const cli = findCli();
|
|
586
620
|
if (!cli)
|
|
587
621
|
return { ok: false, reason: "no-cli" };
|
|
@@ -608,7 +642,7 @@ function fireHotUpgrade(targetVersion) {
|
|
|
608
642
|
shell = bash;
|
|
609
643
|
shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
|
|
610
644
|
}
|
|
611
|
-
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
|
|
645
|
+
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath} (original: ${originalScriptPath}), cli=${cli}, target=${targetVersion || "latest"}`);
|
|
612
646
|
// 异步执行升级脚本
|
|
613
647
|
execFile(shell, shellArgs, {
|
|
614
648
|
timeout: 120_000,
|
|
@@ -621,6 +655,7 @@ function fireHotUpgrade(targetVersion) {
|
|
|
621
655
|
console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
|
|
622
656
|
if (_stderr)
|
|
623
657
|
console.error(`[qqbot] fireHotUpgrade: stderr: ${_stderr.slice(0, 2000)}`);
|
|
658
|
+
cleanupTempScript();
|
|
624
659
|
_upgrading = false;
|
|
625
660
|
return;
|
|
626
661
|
}
|
|
@@ -630,10 +665,13 @@ function fireHotUpgrade(targetVersion) {
|
|
|
630
665
|
const newVersion = versionMatch?.[1];
|
|
631
666
|
if (newVersion === "unknown") {
|
|
632
667
|
console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
|
|
668
|
+
cleanupTempScript();
|
|
633
669
|
_upgrading = false;
|
|
634
670
|
return;
|
|
635
671
|
}
|
|
636
672
|
console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
|
|
673
|
+
// 脚本执行成功,清理临时脚本副本
|
|
674
|
+
cleanupTempScript();
|
|
637
675
|
// 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
|
|
638
676
|
// 确保新进程启动时读到的是 npm source,不会被本地源码覆盖。
|
|
639
677
|
switchPluginSourceToNpm();
|
|
@@ -693,7 +731,11 @@ function fireHotUpgrade(targetVersion) {
|
|
|
693
731
|
/**
|
|
694
732
|
* /bot-upgrade — 统一升级入口
|
|
695
733
|
*
|
|
696
|
-
*
|
|
734
|
+
* upgradeMode 开关:
|
|
735
|
+
* - "doc"(默认):只展示升级指引文档,不执行热更新
|
|
736
|
+
* - "hot-reload":执行 npm 升级脚本进行热更新
|
|
737
|
+
*
|
|
738
|
+
* 热更新模式下的产品流程:
|
|
697
739
|
* /bot-upgrade — 展示版本信息+确认按钮(不直接升级)
|
|
698
740
|
* /bot-upgrade --latest — 确认升级到最新版本
|
|
699
741
|
* /bot-upgrade --version X — 升级到指定版本
|
|
@@ -702,21 +744,49 @@ function fireHotUpgrade(targetVersion) {
|
|
|
702
744
|
let _upgrading = false; // 升级锁
|
|
703
745
|
registerCommand({
|
|
704
746
|
name: "bot-upgrade",
|
|
705
|
-
description: "
|
|
747
|
+
description: "检查更新并查看升级指引",
|
|
706
748
|
usage: [
|
|
707
|
-
`/bot-upgrade
|
|
708
|
-
`/bot-upgrade --latest
|
|
709
|
-
`/bot-upgrade --version X
|
|
710
|
-
`/bot-upgrade --force
|
|
711
|
-
``,
|
|
712
|
-
`⚠️ 仅在私聊中可用。升级过程约 30~60 秒,期间服务短暂不可用。`,
|
|
713
|
-
``,
|
|
714
|
-
`环境要求:`,
|
|
715
|
-
` - 操作系统:macOS / Linux / Windows`,
|
|
716
|
-
` - OpenClaw 框架版本 ≥ ${UPGRADE_REQUIREMENTS.minFrameworkVersion}`,
|
|
717
|
-
` - Node.js ≥ v${UPGRADE_REQUIREMENTS.minNodeVersion}`,
|
|
749
|
+
`/bot-upgrade 检查是否有新版本`,
|
|
750
|
+
`/bot-upgrade --latest 确认升级到最新版本(需 upgradeMode=hot-reload)`,
|
|
751
|
+
`/bot-upgrade --version X 升级到指定版本(需 upgradeMode=hot-reload)`,
|
|
752
|
+
`/bot-upgrade --force 强制重新安装当前版本(需 upgradeMode=hot-reload)`,
|
|
718
753
|
].join("\n"),
|
|
719
754
|
handler: async (ctx) => {
|
|
755
|
+
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
756
|
+
const upgradeMode = ctx.accountConfig?.upgradeMode || "doc";
|
|
757
|
+
const args = ctx.args.trim();
|
|
758
|
+
const info = await getUpdateInfo();
|
|
759
|
+
const GITHUB_URL = "https://github.com/tencent-connect/openclaw-qqbot/";
|
|
760
|
+
// ── doc 模式(默认):只展示升级指引,不执行热更新 ──
|
|
761
|
+
if (upgradeMode !== "hot-reload") {
|
|
762
|
+
if (info.checkedAt === 0) {
|
|
763
|
+
return `⏳ 版本检查中,请稍后再试`;
|
|
764
|
+
}
|
|
765
|
+
if (info.error) {
|
|
766
|
+
return [
|
|
767
|
+
`❌ 主机网络访问异常,无法检查更新`,
|
|
768
|
+
``,
|
|
769
|
+
`查看升级指引:[点击查看](${url})`,
|
|
770
|
+
].join("\n");
|
|
771
|
+
}
|
|
772
|
+
if (!info.hasUpdate) {
|
|
773
|
+
return [
|
|
774
|
+
`✅ 当前已是最新版本 v${PLUGIN_VERSION}`,
|
|
775
|
+
``,
|
|
776
|
+
`项目地址:[GitHub](${GITHUB_URL})`,
|
|
777
|
+
].join("\n");
|
|
778
|
+
}
|
|
779
|
+
return [
|
|
780
|
+
`🆕 发现新版本`,
|
|
781
|
+
``,
|
|
782
|
+
`当前版本:**v${PLUGIN_VERSION}**`,
|
|
783
|
+
`最新版本:**v${info.latest}**`,
|
|
784
|
+
``,
|
|
785
|
+
`📖 升级指引:[点击查看](${url})`,
|
|
786
|
+
`🌟 官方 GitHub 仓库:[点击前往](${GITHUB_URL})`,
|
|
787
|
+
].join("\n");
|
|
788
|
+
}
|
|
789
|
+
// ── hot-reload 模式:执行热更新 ──
|
|
720
790
|
// 升级相关指令仅在私聊中可用
|
|
721
791
|
if (ctx.type !== "c2c") {
|
|
722
792
|
return `💡 请在私聊中使用此指令`;
|
|
@@ -725,9 +795,6 @@ registerCommand({
|
|
|
725
795
|
if (_upgrading) {
|
|
726
796
|
return `⏳ 正在升级中,请稍候...`;
|
|
727
797
|
}
|
|
728
|
-
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
729
|
-
const args = ctx.args.trim();
|
|
730
|
-
const info = await getUpdateInfo();
|
|
731
798
|
let isForce = false;
|
|
732
799
|
let isLatest = false;
|
|
733
800
|
let versionArg;
|
|
@@ -764,7 +831,6 @@ registerCommand({
|
|
|
764
831
|
continue;
|
|
765
832
|
}
|
|
766
833
|
}
|
|
767
|
-
const GITHUB_URL = "https://github.com/tencent-connect/openclaw-qqbot/";
|
|
768
834
|
// ── 无参数(也没有 --latest / --version / --force):只展示信息+确认按钮 ──
|
|
769
835
|
if (!versionArg && !isLatest && !isForce) {
|
|
770
836
|
if (info.checkedAt === 0) {
|
|
@@ -785,7 +851,7 @@ registerCommand({
|
|
|
785
851
|
];
|
|
786
852
|
return lines.join("\n");
|
|
787
853
|
}
|
|
788
|
-
// 有新版本:展示信息 +
|
|
854
|
+
// 有新版本:展示信息 + 确认按钮
|
|
789
855
|
return [
|
|
790
856
|
`🆕 发现新版本`,
|
|
791
857
|
``,
|
|
@@ -1145,6 +1211,185 @@ registerCommand({
|
|
|
1145
1211
|
};
|
|
1146
1212
|
},
|
|
1147
1213
|
});
|
|
1214
|
+
// ============ /bot-clear-storage ============
|
|
1215
|
+
/**
|
|
1216
|
+
* 扫描指定目录下的所有文件,递归统计。
|
|
1217
|
+
* 返回按文件大小降序排列的文件列表。
|
|
1218
|
+
*/
|
|
1219
|
+
function scanDirectoryFiles(dirPath) {
|
|
1220
|
+
const files = [];
|
|
1221
|
+
if (!fs.existsSync(dirPath))
|
|
1222
|
+
return files;
|
|
1223
|
+
const walk = (dir) => {
|
|
1224
|
+
let entries;
|
|
1225
|
+
try {
|
|
1226
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
for (const entry of entries) {
|
|
1232
|
+
const fullPath = path.join(dir, entry.name);
|
|
1233
|
+
if (entry.isDirectory()) {
|
|
1234
|
+
walk(fullPath);
|
|
1235
|
+
}
|
|
1236
|
+
else if (entry.isFile()) {
|
|
1237
|
+
try {
|
|
1238
|
+
const stat = fs.statSync(fullPath);
|
|
1239
|
+
files.push({ filePath: fullPath, size: stat.size });
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
// 跳过无法访问的文件
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
walk(dirPath);
|
|
1248
|
+
// 按大小降序排列
|
|
1249
|
+
files.sort((a, b) => b.size - a.size);
|
|
1250
|
+
return files;
|
|
1251
|
+
}
|
|
1252
|
+
/** 格式化文件大小为人类可读形式 */
|
|
1253
|
+
function formatBytes(bytes) {
|
|
1254
|
+
if (bytes < 1024)
|
|
1255
|
+
return `${bytes} B`;
|
|
1256
|
+
if (bytes < 1024 * 1024)
|
|
1257
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1258
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
1259
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1260
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* /bot-clear-storage — 清理通过 QQBot 对话产生的文件以及下载的资源
|
|
1264
|
+
*
|
|
1265
|
+
* 仅在私聊(c2c)中可用。
|
|
1266
|
+
* --force 执行时删除整个 appId 目录下的所有文件(不区分用户 openid)。
|
|
1267
|
+
*
|
|
1268
|
+
* 产品流程:
|
|
1269
|
+
* /bot-clear-storage — 扫描并列出当前 appId 下的文件,展示确认按钮
|
|
1270
|
+
* /bot-clear-storage --force — 确认执行删除
|
|
1271
|
+
*/
|
|
1272
|
+
registerCommand({
|
|
1273
|
+
name: "bot-clear-storage",
|
|
1274
|
+
description: "清理通过QQBot对话产生的文件以及下载的资源(保存在 OpenClaw 运行环境的主机上)",
|
|
1275
|
+
usage: [
|
|
1276
|
+
`/bot-clear-storage`,
|
|
1277
|
+
``,
|
|
1278
|
+
`扫描当前机器人产生的下载文件并列出明细。`,
|
|
1279
|
+
`确认后执行删除,释放主机磁盘空间。`,
|
|
1280
|
+
``,
|
|
1281
|
+
`/bot-clear-storage --force 确认执行清理`,
|
|
1282
|
+
``,
|
|
1283
|
+
`⚠️ 仅在私聊中可用。`,
|
|
1284
|
+
].join("\n"),
|
|
1285
|
+
handler: (ctx) => {
|
|
1286
|
+
const { appId, type } = ctx;
|
|
1287
|
+
// 仅私聊可用
|
|
1288
|
+
if (type !== "c2c") {
|
|
1289
|
+
return `💡 请在私聊中使用此指令`;
|
|
1290
|
+
}
|
|
1291
|
+
const isForce = ctx.args.trim() === "--force";
|
|
1292
|
+
// 删除粒度为 appId 目录(不区分用户 openid)
|
|
1293
|
+
// 路径: downloads/{appId}/
|
|
1294
|
+
const targetDir = path.join(getHomeDir(), ".openclaw", "media", "qqbot", "downloads", appId);
|
|
1295
|
+
const displayDir = `~/.openclaw/media/qqbot/downloads/${appId}`;
|
|
1296
|
+
if (!isForce) {
|
|
1297
|
+
// ── 第一步:扫描并展示文件列表 ──
|
|
1298
|
+
const files = scanDirectoryFiles(targetDir);
|
|
1299
|
+
if (files.length === 0) {
|
|
1300
|
+
return [
|
|
1301
|
+
`✅ 当前没有需要清理的文件`,
|
|
1302
|
+
``,
|
|
1303
|
+
`目录 \`${displayDir}\` 为空或不存在。`,
|
|
1304
|
+
].join("\n");
|
|
1305
|
+
}
|
|
1306
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
1307
|
+
const MAX_DISPLAY = 10;
|
|
1308
|
+
const lines = [
|
|
1309
|
+
`即将清理 \`${displayDir}\` 目录下所有文件,总共 ${files.length} 个文件,占用磁盘存储空间 ${formatBytes(totalSize)}。`,
|
|
1310
|
+
``,
|
|
1311
|
+
`目录文件概况:`,
|
|
1312
|
+
];
|
|
1313
|
+
// 展示前 MAX_DISPLAY 个(按大小降序)
|
|
1314
|
+
const displayFiles = files.slice(0, MAX_DISPLAY);
|
|
1315
|
+
for (const f of displayFiles) {
|
|
1316
|
+
const relativePath = path.relative(targetDir, f.filePath);
|
|
1317
|
+
// 在 Windows 上统一用 / 分隔显示
|
|
1318
|
+
const displayName = relativePath.replace(/\\/g, "/");
|
|
1319
|
+
lines.push(`${displayName} (${formatBytes(f.size)})`, ``, ``);
|
|
1320
|
+
}
|
|
1321
|
+
if (files.length > MAX_DISPLAY) {
|
|
1322
|
+
lines.push(`...[合计:${files.length} 个文件(${formatBytes(totalSize)})]`, ``);
|
|
1323
|
+
}
|
|
1324
|
+
lines.push(``, `---`, ``, `确认清理后,上述保存在 OpenClaw 运行主机磁盘上的文件将永久删除,后续对话过程中AI无法再找回相关文件。`, `‼️ 点击指令确认删除`, `<qqbot-cmd-enter text="/bot-clear-storage --force" />`);
|
|
1325
|
+
return lines.join("\n");
|
|
1326
|
+
}
|
|
1327
|
+
// ── 第二步:--force 执行删除 ──
|
|
1328
|
+
const files = scanDirectoryFiles(targetDir);
|
|
1329
|
+
if (files.length === 0) {
|
|
1330
|
+
return `✅ 目录已为空,无需清理`;
|
|
1331
|
+
}
|
|
1332
|
+
let deletedCount = 0;
|
|
1333
|
+
let deletedSize = 0;
|
|
1334
|
+
let failedCount = 0;
|
|
1335
|
+
for (const f of files) {
|
|
1336
|
+
try {
|
|
1337
|
+
fs.unlinkSync(f.filePath);
|
|
1338
|
+
deletedCount++;
|
|
1339
|
+
deletedSize += f.size;
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
failedCount++;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
// 尝试清理空目录(递归删除空子目录)
|
|
1346
|
+
try {
|
|
1347
|
+
removeEmptyDirs(targetDir);
|
|
1348
|
+
}
|
|
1349
|
+
catch {
|
|
1350
|
+
// 非关键,静默忽略
|
|
1351
|
+
}
|
|
1352
|
+
if (failedCount === 0) {
|
|
1353
|
+
return [
|
|
1354
|
+
`✅ 清理成功`,
|
|
1355
|
+
``,
|
|
1356
|
+
`已删除 ${deletedCount} 个文件,释放 ${formatBytes(deletedSize)} 磁盘空间。`,
|
|
1357
|
+
].join("\n");
|
|
1358
|
+
}
|
|
1359
|
+
return [
|
|
1360
|
+
`⚠️ 部分清理完成`,
|
|
1361
|
+
``,
|
|
1362
|
+
`已删除 ${deletedCount} 个文件(${formatBytes(deletedSize)}),${failedCount} 个文件删除失败。`,
|
|
1363
|
+
].join("\n");
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
/** 递归删除空目录(从叶子向上清理) */
|
|
1367
|
+
function removeEmptyDirs(dirPath) {
|
|
1368
|
+
if (!fs.existsSync(dirPath))
|
|
1369
|
+
return;
|
|
1370
|
+
let entries;
|
|
1371
|
+
try {
|
|
1372
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
for (const entry of entries) {
|
|
1378
|
+
if (entry.isDirectory()) {
|
|
1379
|
+
removeEmptyDirs(path.join(dirPath, entry.name));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// 重新读取,如果目录已空则删除
|
|
1383
|
+
try {
|
|
1384
|
+
const remaining = fs.readdirSync(dirPath);
|
|
1385
|
+
if (remaining.length === 0) {
|
|
1386
|
+
fs.rmdirSync(dirPath);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
// 目录可能正在被使用,跳过
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1148
1393
|
// ============ 匹配入口 ============
|
|
1149
1394
|
/**
|
|
1150
1395
|
* 尝试匹配并执行插件级斜杠指令
|
|
@@ -11,8 +11,8 @@ export type StartupMarkerData = {
|
|
|
11
11
|
lastFailureReason?: string;
|
|
12
12
|
lastFailureVersion?: string;
|
|
13
13
|
};
|
|
14
|
-
export declare function readStartupMarker(): StartupMarkerData;
|
|
15
|
-
export declare function writeStartupMarker(data: StartupMarkerData): void;
|
|
14
|
+
export declare function readStartupMarker(accountId: string, appId: string): StartupMarkerData;
|
|
15
|
+
export declare function writeStartupMarker(accountId: string, appId: string, data: StartupMarkerData): void;
|
|
16
16
|
/**
|
|
17
17
|
* 判断是否需要发送启动问候:
|
|
18
18
|
* - 首次启动(无 marker)→ "灵魂已上线"
|
|
@@ -20,11 +20,11 @@ export declare function writeStartupMarker(data: StartupMarkerData): void;
|
|
|
20
20
|
* - 同版本 → 不发送
|
|
21
21
|
* - 同版本近期失败 → 冷却期内不重试
|
|
22
22
|
*/
|
|
23
|
-
export declare function getStartupGreetingPlan(): {
|
|
23
|
+
export declare function getStartupGreetingPlan(accountId: string, appId: string): {
|
|
24
24
|
shouldSend: boolean;
|
|
25
25
|
greeting?: string;
|
|
26
26
|
version: string;
|
|
27
27
|
reason?: string;
|
|
28
28
|
};
|
|
29
|
-
export declare function markStartupGreetingSent(version: string): void;
|
|
30
|
-
export declare function markStartupGreetingFailed(version: string, reason: string): void;
|
|
29
|
+
export declare function markStartupGreetingSent(accountId: string, appId: string, version: string): void;
|
|
30
|
+
export declare function markStartupGreetingFailed(accountId: string, appId: string, version: string, reason: string): void;
|
|
@@ -5,29 +5,48 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { getQQBotDataDir } from "./utils/platform.js";
|
|
7
7
|
import { getPluginVersion } from "./slash-commands.js";
|
|
8
|
-
const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
9
8
|
const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
|
|
9
|
+
function safeName(id) {
|
|
10
|
+
return id.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
11
|
+
}
|
|
12
|
+
/** 按 accountId+appId 区分的 marker 文件路径 */
|
|
13
|
+
function getMarkerFile(accountId, appId) {
|
|
14
|
+
return path.join(getQQBotDataDir("data"), `startup-marker-${safeName(accountId)}-${safeName(appId)}.json`);
|
|
15
|
+
}
|
|
16
|
+
/** 旧版全局 marker 路径(兼容迁移) */
|
|
17
|
+
const LEGACY_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
|
|
10
18
|
export function getFirstLaunchGreetingText() {
|
|
11
19
|
return `Haha,我的'灵魂'已上线,随时等你吩咐。`;
|
|
12
20
|
}
|
|
13
21
|
export function getUpgradeGreetingText(version) {
|
|
14
22
|
return `🎉 QQBot 插件已更新至 v${version},在线等候你的吩咐。`;
|
|
15
23
|
}
|
|
16
|
-
export function readStartupMarker() {
|
|
24
|
+
export function readStartupMarker(accountId, appId) {
|
|
17
25
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
// 1. 新版 per-bot 路径优先
|
|
27
|
+
const file = getMarkerFile(accountId, appId);
|
|
28
|
+
if (fs.existsSync(file)) {
|
|
29
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
20
30
|
return data || {};
|
|
21
31
|
}
|
|
32
|
+
// 2. fallback 旧版全局 marker(兼容迁移)
|
|
33
|
+
if (fs.existsSync(LEGACY_MARKER_FILE)) {
|
|
34
|
+
const data = JSON.parse(fs.readFileSync(LEGACY_MARKER_FILE, "utf8"));
|
|
35
|
+
if (data) {
|
|
36
|
+
// 自动迁移:写到新路径
|
|
37
|
+
writeStartupMarker(accountId, appId, data);
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
22
41
|
}
|
|
23
42
|
catch {
|
|
24
43
|
// 文件损坏或不存在,视为无 marker
|
|
25
44
|
}
|
|
26
45
|
return {};
|
|
27
46
|
}
|
|
28
|
-
export function writeStartupMarker(data) {
|
|
47
|
+
export function writeStartupMarker(accountId, appId, data) {
|
|
29
48
|
try {
|
|
30
|
-
fs.writeFileSync(
|
|
49
|
+
fs.writeFileSync(getMarkerFile(accountId, appId), JSON.stringify(data) + "\n");
|
|
31
50
|
}
|
|
32
51
|
catch {
|
|
33
52
|
// ignore
|
|
@@ -40,9 +59,9 @@ export function writeStartupMarker(data) {
|
|
|
40
59
|
* - 同版本 → 不发送
|
|
41
60
|
* - 同版本近期失败 → 冷却期内不重试
|
|
42
61
|
*/
|
|
43
|
-
export function getStartupGreetingPlan() {
|
|
62
|
+
export function getStartupGreetingPlan(accountId, appId) {
|
|
44
63
|
const currentVersion = getPluginVersion();
|
|
45
|
-
const marker = readStartupMarker();
|
|
64
|
+
const marker = readStartupMarker(accountId, appId);
|
|
46
65
|
if (marker.version === currentVersion) {
|
|
47
66
|
return { shouldSend: false, version: currentVersion, reason: "same-version" };
|
|
48
67
|
}
|
|
@@ -58,18 +77,18 @@ export function getStartupGreetingPlan() {
|
|
|
58
77
|
: getUpgradeGreetingText(currentVersion);
|
|
59
78
|
return { shouldSend: true, greeting, version: currentVersion };
|
|
60
79
|
}
|
|
61
|
-
export function markStartupGreetingSent(version) {
|
|
62
|
-
writeStartupMarker({
|
|
80
|
+
export function markStartupGreetingSent(accountId, appId, version) {
|
|
81
|
+
writeStartupMarker(accountId, appId, {
|
|
63
82
|
version,
|
|
64
83
|
startedAt: new Date().toISOString(),
|
|
65
84
|
greetedAt: new Date().toISOString(),
|
|
66
85
|
});
|
|
67
86
|
}
|
|
68
|
-
export function markStartupGreetingFailed(version, reason) {
|
|
69
|
-
const marker = readStartupMarker();
|
|
87
|
+
export function markStartupGreetingFailed(accountId, appId, version, reason) {
|
|
88
|
+
const marker = readStartupMarker(accountId, appId);
|
|
70
89
|
// 同版本已有失败记录时,不覆盖 lastFailureAt,避免冷却期被无限续期
|
|
71
90
|
const shouldPreserveTimestamp = marker.lastFailureVersion === version && marker.lastFailureAt;
|
|
72
|
-
writeStartupMarker({
|
|
91
|
+
writeStartupMarker(accountId, appId, {
|
|
73
92
|
...marker,
|
|
74
93
|
lastFailureVersion: version,
|
|
75
94
|
lastFailureAt: shouldPreserveTimestamp ? marker.lastFailureAt : new Date().toISOString(),
|