@ryantest/openclaw-qqbot 1.6.7-beta.4 → 1.6.7-beta.6
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 +1 -1
- package/README.zh.md +1 -1
- package/dist/src/commands/bot-clear-storage.d.ts +1 -0
- package/dist/src/commands/bot-clear-storage.js +157 -0
- package/dist/src/commands/bot-help.d.ts +1 -0
- package/dist/src/commands/bot-help.js +24 -0
- package/dist/src/commands/bot-logs.d.ts +1 -0
- package/dist/src/commands/bot-logs.js +232 -0
- package/dist/src/commands/bot-ping.d.ts +1 -0
- package/dist/src/commands/bot-ping.js +29 -0
- package/dist/src/commands/bot-upgrade.d.ts +1 -0
- package/dist/src/commands/bot-upgrade.js +252 -0
- package/dist/src/commands/bot-version.d.ts +1 -0
- package/dist/src/commands/bot-version.js +33 -0
- package/dist/src/slash-commands.d.ts +21 -6
- package/dist/src/slash-commands.js +35 -1455
- package/dist/src/upgrade-utils.d.ts +74 -0
- package/dist/src/upgrade-utils.js +634 -0
- package/package.json +1 -1
- package/scripts/upgrade-via-npm.sh +23 -7
- package/src/commands/bot-clear-storage.ts +176 -0
- package/src/commands/bot-help.ts +26 -0
- package/src/commands/bot-logs.ts +234 -0
- package/src/commands/bot-ping.ts +31 -0
- package/src/commands/bot-upgrade.ts +281 -0
- package/src/commands/bot-version.ts +32 -0
- package/src/slash-commands.ts +33 -1522
- package/src/upgrade-utils.ts +673 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
**Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
|
|
12
12
|
|
|
13
|
-
### 🚀 Current Version: `v1.6.
|
|
13
|
+
### 🚀 Current Version: `v1.6.7`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](https://bot.q.qq.com/wiki/)
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
**让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
|
|
11
11
|
|
|
12
|
-
### 🚀 当前版本: `v1.6.
|
|
12
|
+
### 🚀 当前版本: `v1.6.7`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](https://bot.q.qq.com/wiki/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { getHomeDir } from "../utils/platform.js";
|
|
4
|
+
import { registerCommand } from "../slash-commands.js";
|
|
5
|
+
function scanDirectoryFiles(dirPath) {
|
|
6
|
+
const files = [];
|
|
7
|
+
if (!fs.existsSync(dirPath))
|
|
8
|
+
return files;
|
|
9
|
+
const walk = (dir) => {
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
walk(fullPath);
|
|
21
|
+
}
|
|
22
|
+
else if (entry.isFile()) {
|
|
23
|
+
try {
|
|
24
|
+
const stat = fs.statSync(fullPath);
|
|
25
|
+
files.push({ filePath: fullPath, size: stat.size });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// 跳过无法访问的文件
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
walk(dirPath);
|
|
34
|
+
files.sort((a, b) => b.size - a.size);
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
function formatBytes(bytes) {
|
|
38
|
+
if (bytes < 1024)
|
|
39
|
+
return `${bytes} B`;
|
|
40
|
+
if (bytes < 1024 * 1024)
|
|
41
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
42
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
43
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
44
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
45
|
+
}
|
|
46
|
+
function removeEmptyDirs(dirPath) {
|
|
47
|
+
if (!fs.existsSync(dirPath))
|
|
48
|
+
return;
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
removeEmptyDirs(path.join(dirPath, entry.name));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const remaining = fs.readdirSync(dirPath);
|
|
63
|
+
if (remaining.length === 0) {
|
|
64
|
+
fs.rmdirSync(dirPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// 目录可能正在被使用,跳过
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
registerCommand({
|
|
72
|
+
name: "bot-clear-storage",
|
|
73
|
+
description: "清理通过QQBot对话产生的文件以及下载的资源(保存在 OpenClaw 运行环境的主机上)",
|
|
74
|
+
usage: [
|
|
75
|
+
`/bot-clear-storage`,
|
|
76
|
+
``,
|
|
77
|
+
`扫描当前机器人产生的下载文件并列出明细。`,
|
|
78
|
+
`确认后执行删除,释放主机磁盘空间。`,
|
|
79
|
+
``,
|
|
80
|
+
`/bot-clear-storage --force 确认执行清理`,
|
|
81
|
+
``,
|
|
82
|
+
`⚠️ 仅在私聊中可用。`,
|
|
83
|
+
].join("\n"),
|
|
84
|
+
handler: (ctx) => {
|
|
85
|
+
const { appId, type } = ctx;
|
|
86
|
+
if (type !== "c2c") {
|
|
87
|
+
return `💡 请在私聊中使用此指令`;
|
|
88
|
+
}
|
|
89
|
+
const isForce = ctx.args.trim() === "--force";
|
|
90
|
+
const targetDir = path.join(getHomeDir(), ".openclaw", "media", "qqbot", "downloads", appId);
|
|
91
|
+
const displayDir = `~/.openclaw/media/qqbot/downloads/${appId}`;
|
|
92
|
+
if (!isForce) {
|
|
93
|
+
const files = scanDirectoryFiles(targetDir);
|
|
94
|
+
if (files.length === 0) {
|
|
95
|
+
return [
|
|
96
|
+
`✅ 当前没有需要清理的文件`,
|
|
97
|
+
``,
|
|
98
|
+
`目录 \`${displayDir}\` 为空或不存在。`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
102
|
+
const MAX_DISPLAY = 10;
|
|
103
|
+
const lines = [
|
|
104
|
+
`即将清理 \`${displayDir}\` 目录下所有文件,总共 ${files.length} 个文件,占用磁盘存储空间 ${formatBytes(totalSize)}。`,
|
|
105
|
+
``,
|
|
106
|
+
`目录文件概况:`,
|
|
107
|
+
];
|
|
108
|
+
const displayFiles = files.slice(0, MAX_DISPLAY);
|
|
109
|
+
for (const f of displayFiles) {
|
|
110
|
+
const relativePath = path.relative(targetDir, f.filePath);
|
|
111
|
+
const displayName = relativePath.replace(/\\/g, "/");
|
|
112
|
+
lines.push(`${displayName} (${formatBytes(f.size)})`, ``, ``);
|
|
113
|
+
}
|
|
114
|
+
if (files.length > MAX_DISPLAY) {
|
|
115
|
+
lines.push(`...[合计:${files.length} 个文件(${formatBytes(totalSize)})]`, ``);
|
|
116
|
+
}
|
|
117
|
+
lines.push(``, `---`, ``, `确认清理后,上述保存在 OpenClaw 运行主机磁盘上的文件将永久删除,后续对话过程中AI无法再找回相关文件。`, `‼️ 点击指令确认删除`, `<qqbot-cmd-enter text="/bot-clear-storage --force" />`);
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
// ── --force 执行删除 ──
|
|
121
|
+
const files = scanDirectoryFiles(targetDir);
|
|
122
|
+
if (files.length === 0) {
|
|
123
|
+
return `✅ 目录已为空,无需清理`;
|
|
124
|
+
}
|
|
125
|
+
let deletedCount = 0;
|
|
126
|
+
let deletedSize = 0;
|
|
127
|
+
let failedCount = 0;
|
|
128
|
+
for (const f of files) {
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(f.filePath);
|
|
131
|
+
deletedCount++;
|
|
132
|
+
deletedSize += f.size;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
failedCount++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
removeEmptyDirs(targetDir);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// 非关键,静默忽略
|
|
143
|
+
}
|
|
144
|
+
if (failedCount === 0) {
|
|
145
|
+
return [
|
|
146
|
+
`✅ 清理成功`,
|
|
147
|
+
``,
|
|
148
|
+
`已删除 ${deletedCount} 个文件,释放 ${formatBytes(deletedSize)} 磁盘空间。`,
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
return [
|
|
152
|
+
`⚠️ 部分清理完成`,
|
|
153
|
+
``,
|
|
154
|
+
`已删除 ${deletedCount} 个文件(${formatBytes(deletedSize)}),${failedCount} 个文件删除失败。`,
|
|
155
|
+
].join("\n");
|
|
156
|
+
},
|
|
157
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { registerCommand, getCommands, getPluginVersion } from "../slash-commands.js";
|
|
2
|
+
registerCommand({
|
|
3
|
+
name: "bot-help",
|
|
4
|
+
description: "查看所有指令以及用途",
|
|
5
|
+
usage: [
|
|
6
|
+
`/bot-help`,
|
|
7
|
+
``,
|
|
8
|
+
`列出所有可用的 QQBot 插件内置指令及其简要说明。`,
|
|
9
|
+
`使用 /指令名 ? 可查看某条指令的详细用法。`,
|
|
10
|
+
].join("\n"),
|
|
11
|
+
handler: (ctx) => {
|
|
12
|
+
const GROUP_EXCLUDED_COMMANDS = new Set(["bot-upgrade", "bot-clear-storage"]);
|
|
13
|
+
const isGroup = ctx.type === "group";
|
|
14
|
+
const PLUGIN_VERSION = getPluginVersion();
|
|
15
|
+
const lines = [`### QQBot插件内置调试指令`, ``];
|
|
16
|
+
for (const [name, cmd] of getCommands()) {
|
|
17
|
+
if (isGroup && GROUP_EXCLUDED_COMMANDS.has(name))
|
|
18
|
+
continue;
|
|
19
|
+
lines.push(`<qqbot-cmd-input text="/${name}" show="/${name}"/> ${cmd.description}`);
|
|
20
|
+
}
|
|
21
|
+
lines.push(``, `> 插件版本 v${PLUGIN_VERSION}`);
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { getHomeDir, getQQBotDataDir, isWindows } from "../utils/platform.js";
|
|
4
|
+
import { registerCommand } from "../slash-commands.js";
|
|
5
|
+
/**
|
|
6
|
+
* 从 openclaw.json / clawdbot.json / moltbot.json 的 logging.file 配置中
|
|
7
|
+
* 提取用户自定义的日志文件路径。
|
|
8
|
+
*/
|
|
9
|
+
function getConfiguredLogFiles() {
|
|
10
|
+
const homeDir = getHomeDir();
|
|
11
|
+
const files = [];
|
|
12
|
+
for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
|
|
13
|
+
try {
|
|
14
|
+
const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
|
|
15
|
+
if (!fs.existsSync(cfgPath))
|
|
16
|
+
continue;
|
|
17
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
18
|
+
const logFile = cfg?.logging?.file;
|
|
19
|
+
if (logFile && typeof logFile === "string") {
|
|
20
|
+
files.push(path.resolve(logFile));
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// ignore
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
function collectCandidateLogDirs() {
|
|
31
|
+
const homeDir = getHomeDir();
|
|
32
|
+
const dirs = new Set();
|
|
33
|
+
const pushDir = (p) => {
|
|
34
|
+
if (!p)
|
|
35
|
+
return;
|
|
36
|
+
const normalized = path.resolve(p);
|
|
37
|
+
dirs.add(normalized);
|
|
38
|
+
};
|
|
39
|
+
const pushStateDir = (stateDir) => {
|
|
40
|
+
if (!stateDir)
|
|
41
|
+
return;
|
|
42
|
+
pushDir(stateDir);
|
|
43
|
+
pushDir(path.join(stateDir, "logs"));
|
|
44
|
+
};
|
|
45
|
+
for (const logFile of getConfiguredLogFiles()) {
|
|
46
|
+
pushDir(path.dirname(logFile));
|
|
47
|
+
}
|
|
48
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
49
|
+
if (!value)
|
|
50
|
+
continue;
|
|
51
|
+
if (/STATE_DIR$/i.test(key) && /(OPENCLAW|CLAWDBOT|MOLTBOT)/i.test(key)) {
|
|
52
|
+
pushStateDir(value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const name of [".openclaw", ".clawdbot", ".moltbot", "openclaw", "clawdbot", "moltbot"]) {
|
|
56
|
+
pushDir(path.join(homeDir, name));
|
|
57
|
+
pushDir(path.join(homeDir, name, "logs"));
|
|
58
|
+
}
|
|
59
|
+
const searchRoots = new Set([
|
|
60
|
+
homeDir,
|
|
61
|
+
process.cwd(),
|
|
62
|
+
path.dirname(process.cwd()),
|
|
63
|
+
]);
|
|
64
|
+
if (process.env.APPDATA)
|
|
65
|
+
searchRoots.add(process.env.APPDATA);
|
|
66
|
+
if (process.env.LOCALAPPDATA)
|
|
67
|
+
searchRoots.add(process.env.LOCALAPPDATA);
|
|
68
|
+
for (const root of searchRoots) {
|
|
69
|
+
try {
|
|
70
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
if (!/(openclaw|clawdbot|moltbot)/i.test(entry.name))
|
|
75
|
+
continue;
|
|
76
|
+
const base = path.join(root, entry.name);
|
|
77
|
+
pushDir(base);
|
|
78
|
+
pushDir(path.join(base, "logs"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// 无权限或不存在,跳过
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!isWindows()) {
|
|
86
|
+
for (const name of ["openclaw", "clawdbot", "moltbot"]) {
|
|
87
|
+
pushDir(path.join("/var/log", name));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const tmpRoots = new Set();
|
|
91
|
+
if (isWindows()) {
|
|
92
|
+
tmpRoots.add("C:\\tmp");
|
|
93
|
+
if (process.env.TEMP)
|
|
94
|
+
tmpRoots.add(process.env.TEMP);
|
|
95
|
+
if (process.env.TMP)
|
|
96
|
+
tmpRoots.add(process.env.TMP);
|
|
97
|
+
if (process.env.LOCALAPPDATA)
|
|
98
|
+
tmpRoots.add(path.join(process.env.LOCALAPPDATA, "Temp"));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
tmpRoots.add("/tmp");
|
|
102
|
+
}
|
|
103
|
+
for (const tmpRoot of tmpRoots) {
|
|
104
|
+
for (const name of ["openclaw", "clawdbot", "moltbot"]) {
|
|
105
|
+
pushDir(path.join(tmpRoot, name));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Array.from(dirs);
|
|
109
|
+
}
|
|
110
|
+
function collectRecentLogFiles(logDirs) {
|
|
111
|
+
const candidates = [];
|
|
112
|
+
const dedupe = new Set();
|
|
113
|
+
const pushFile = (filePath, sourceDir) => {
|
|
114
|
+
const normalized = path.resolve(filePath);
|
|
115
|
+
if (dedupe.has(normalized))
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
const stat = fs.statSync(normalized);
|
|
119
|
+
if (!stat.isFile())
|
|
120
|
+
return;
|
|
121
|
+
dedupe.add(normalized);
|
|
122
|
+
candidates.push({ filePath: normalized, sourceDir, mtimeMs: stat.mtimeMs });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// 文件不存在或无权限
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
for (const logFile of getConfiguredLogFiles()) {
|
|
129
|
+
pushFile(logFile, path.dirname(logFile));
|
|
130
|
+
}
|
|
131
|
+
for (const dir of logDirs) {
|
|
132
|
+
pushFile(path.join(dir, "gateway.log"), dir);
|
|
133
|
+
pushFile(path.join(dir, "gateway.err.log"), dir);
|
|
134
|
+
pushFile(path.join(dir, "openclaw.log"), dir);
|
|
135
|
+
pushFile(path.join(dir, "clawdbot.log"), dir);
|
|
136
|
+
pushFile(path.join(dir, "moltbot.log"), dir);
|
|
137
|
+
try {
|
|
138
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isFile())
|
|
141
|
+
continue;
|
|
142
|
+
if (!/\.(log|txt)$/i.test(entry.name))
|
|
143
|
+
continue;
|
|
144
|
+
if (!/(gateway|openclaw|clawdbot|moltbot)/i.test(entry.name))
|
|
145
|
+
continue;
|
|
146
|
+
pushFile(path.join(dir, entry.name), dir);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// 无权限或不存在,跳过
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
154
|
+
return candidates;
|
|
155
|
+
}
|
|
156
|
+
registerCommand({
|
|
157
|
+
name: "bot-logs",
|
|
158
|
+
description: "导出本地日志文件",
|
|
159
|
+
usage: [
|
|
160
|
+
`/bot-logs`,
|
|
161
|
+
``,
|
|
162
|
+
`导出最近的 OpenClaw 日志文件(最多 4 个)。`,
|
|
163
|
+
`每个文件最多保留最后 1000 行,以文件形式返回。`,
|
|
164
|
+
].join("\n"),
|
|
165
|
+
handler: () => {
|
|
166
|
+
const logDirs = collectCandidateLogDirs();
|
|
167
|
+
const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
|
|
168
|
+
if (recentFiles.length === 0) {
|
|
169
|
+
const existingDirs = logDirs.filter(d => { try {
|
|
170
|
+
return fs.existsSync(d);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
} });
|
|
175
|
+
const searched = existingDirs.length > 0
|
|
176
|
+
? existingDirs.map(d => ` • ${d}`).join("\n")
|
|
177
|
+
: logDirs.slice(0, 6).map(d => ` • ${d}`).join("\n") + (logDirs.length > 6 ? `\n …及其他 ${logDirs.length - 6} 个路径` : "");
|
|
178
|
+
return [
|
|
179
|
+
`⚠️ 未找到日志文件`,
|
|
180
|
+
``,
|
|
181
|
+
`已搜索以下${existingDirs.length > 0 ? "已存在的" : ""}路径:`,
|
|
182
|
+
searched,
|
|
183
|
+
``,
|
|
184
|
+
`💡 如果日志在自定义路径,请在配置文件中添加:`,
|
|
185
|
+
` "logging": { "file": "/path/to/your/logfile.log" }`,
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
188
|
+
const lines = [];
|
|
189
|
+
let totalIncluded = 0;
|
|
190
|
+
let totalOriginal = 0;
|
|
191
|
+
let truncatedCount = 0;
|
|
192
|
+
const MAX_LINES_PER_FILE = 1000;
|
|
193
|
+
for (const logFile of recentFiles) {
|
|
194
|
+
try {
|
|
195
|
+
const content = fs.readFileSync(logFile.filePath, "utf8");
|
|
196
|
+
const allLines = content.split("\n");
|
|
197
|
+
const totalFileLines = allLines.length;
|
|
198
|
+
const tail = allLines.slice(-MAX_LINES_PER_FILE);
|
|
199
|
+
if (tail.length > 0) {
|
|
200
|
+
const fileName = path.basename(logFile.filePath);
|
|
201
|
+
lines.push(`\n========== ${fileName} (last ${tail.length} of ${totalFileLines} lines) ==========`);
|
|
202
|
+
lines.push(`from: ${logFile.sourceDir}`);
|
|
203
|
+
lines.push(...tail);
|
|
204
|
+
totalIncluded += tail.length;
|
|
205
|
+
totalOriginal += totalFileLines;
|
|
206
|
+
if (totalFileLines > MAX_LINES_PER_FILE)
|
|
207
|
+
truncatedCount++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
lines.push(`[读取 ${path.basename(logFile.filePath)} 失败]`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (lines.length === 0) {
|
|
215
|
+
return `⚠️ 找到日志文件但读取失败,请检查文件权限`;
|
|
216
|
+
}
|
|
217
|
+
const tmpDir = getQQBotDataDir("downloads");
|
|
218
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
219
|
+
const tmpFile = path.join(tmpDir, `bot-logs-${timestamp}.txt`);
|
|
220
|
+
fs.writeFileSync(tmpFile, lines.join("\n"), "utf8");
|
|
221
|
+
const fileCount = recentFiles.length;
|
|
222
|
+
const topSources = Array.from(new Set(recentFiles.map(item => item.sourceDir))).slice(0, 3);
|
|
223
|
+
let summaryText = `${fileCount} 个日志文件,共 ${totalIncluded} 行`;
|
|
224
|
+
if (truncatedCount > 0) {
|
|
225
|
+
summaryText += `(${truncatedCount} 个文件因过长仅保留最后 ${MAX_LINES_PER_FILE} 行,原始共 ${totalOriginal} 行)`;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
text: `📋 ${summaryText}\n📂 来源:${topSources.join(" | ")}`,
|
|
229
|
+
filePath: tmpFile,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { registerCommand } from "../slash-commands.js";
|
|
2
|
+
registerCommand({
|
|
3
|
+
name: "bot-ping",
|
|
4
|
+
description: "测试当前 openclaw 与 QQ 连接的网络延迟",
|
|
5
|
+
usage: [
|
|
6
|
+
`/bot-ping`,
|
|
7
|
+
``,
|
|
8
|
+
`测试 OpenClaw 主机与 QQ 服务器之间的网络延迟。`,
|
|
9
|
+
`返回网络传输耗时和插件处理耗时。`,
|
|
10
|
+
].join("\n"),
|
|
11
|
+
handler: (ctx) => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const eventTime = new Date(ctx.eventTimestamp).getTime();
|
|
14
|
+
if (isNaN(eventTime)) {
|
|
15
|
+
return `✅ pong!`;
|
|
16
|
+
}
|
|
17
|
+
const totalMs = now - eventTime;
|
|
18
|
+
const qqToPlugin = ctx.receivedAt - eventTime;
|
|
19
|
+
const pluginProcess = now - ctx.receivedAt;
|
|
20
|
+
const lines = [
|
|
21
|
+
`✅ pong!`,
|
|
22
|
+
``,
|
|
23
|
+
`⏱ 延迟: ${totalMs}ms`,
|
|
24
|
+
` ├ 网络传输: ${qqToPlugin}ms`,
|
|
25
|
+
` └ 插件处理: ${pluginProcess}ms`,
|
|
26
|
+
];
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|