@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.2
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/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { C as debugLog } from "./sender-p-B14eLG.js";
|
|
2
|
+
import { a as getHomeDir, f as isWindows, l as getQQBotMediaPath, o as getQQBotDataDir } from "./target-parser-Y0prnrXD.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
//#region extensions/qqbot/src/engine/commands/builtin/state.ts
|
|
7
|
+
let _resolveVersion = () => "unknown";
|
|
8
|
+
let _approveRuntimeGetter = null;
|
|
9
|
+
let PLUGIN_VERSION = "unknown";
|
|
10
|
+
/**
|
|
11
|
+
* Initialize command dependencies from the EngineAdapters.commands port.
|
|
12
|
+
* Called once by the bridge layer during startup.
|
|
13
|
+
*/
|
|
14
|
+
function initSlashCommandDeps(port) {
|
|
15
|
+
_resolveVersion = port.resolveVersion;
|
|
16
|
+
PLUGIN_VERSION = port.pluginVersion;
|
|
17
|
+
_approveRuntimeGetter = port.approveRuntimeGetter ?? null;
|
|
18
|
+
}
|
|
19
|
+
function resolveRuntimeServiceVersion() {
|
|
20
|
+
return _resolveVersion();
|
|
21
|
+
}
|
|
22
|
+
function getPluginVersionString() {
|
|
23
|
+
return PLUGIN_VERSION;
|
|
24
|
+
}
|
|
25
|
+
function getFrameworkVersionString() {
|
|
26
|
+
return _resolveVersion();
|
|
27
|
+
}
|
|
28
|
+
function getApproveRuntimeGetter() {
|
|
29
|
+
return _approveRuntimeGetter;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-approve.ts
|
|
33
|
+
function registerApproveCommands(registry) {
|
|
34
|
+
registry.register({
|
|
35
|
+
name: "bot-approve",
|
|
36
|
+
description: "管理命令执行审批配置",
|
|
37
|
+
requireAuth: true,
|
|
38
|
+
c2cOnly: true,
|
|
39
|
+
usage: [
|
|
40
|
+
`/bot-approve 查看操作指引`,
|
|
41
|
+
`/bot-approve on 开启审批(白名单模式,推荐)`,
|
|
42
|
+
`/bot-approve off 关闭审批,命令直接执行`,
|
|
43
|
+
`/bot-approve always 始终审批,每次执行都需审批`,
|
|
44
|
+
`/bot-approve reset 恢复框架默认值`,
|
|
45
|
+
`/bot-approve status 查看当前审批配置`
|
|
46
|
+
].join("\n"),
|
|
47
|
+
handler: async (ctx) => {
|
|
48
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
49
|
+
let runtime;
|
|
50
|
+
try {
|
|
51
|
+
const getter = getApproveRuntimeGetter();
|
|
52
|
+
if (!getter) throw new Error("runtime not available");
|
|
53
|
+
runtime = getter();
|
|
54
|
+
} catch {
|
|
55
|
+
return [
|
|
56
|
+
`🔐 命令执行审批配置`,
|
|
57
|
+
``,
|
|
58
|
+
`❌ 当前环境不支持在线配置修改,请通过 CLI 手动配置:`,
|
|
59
|
+
``,
|
|
60
|
+
`\`\`\`shell`,
|
|
61
|
+
`# 开启审批(白名单模式)`,
|
|
62
|
+
`openclaw config set tools.exec.security allowlist`,
|
|
63
|
+
`openclaw config set tools.exec.ask on-miss`,
|
|
64
|
+
``,
|
|
65
|
+
`# 关闭审批`,
|
|
66
|
+
`openclaw config set tools.exec.security full`,
|
|
67
|
+
`openclaw config set tools.exec.ask off`,
|
|
68
|
+
`\`\`\``
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
const configApi = runtime.config;
|
|
72
|
+
const loadExecConfig = () => {
|
|
73
|
+
const exec = (configApi.current().tools ?? {}).exec ?? {};
|
|
74
|
+
return {
|
|
75
|
+
security: typeof exec.security === "string" ? exec.security : "deny",
|
|
76
|
+
ask: typeof exec.ask === "string" ? exec.ask : "on-miss"
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const writeExecConfig = async (security, ask) => {
|
|
80
|
+
const cfg = structuredClone(configApi.current());
|
|
81
|
+
const tools = cfg.tools ?? {};
|
|
82
|
+
const exec = tools.exec ?? {};
|
|
83
|
+
exec.security = security;
|
|
84
|
+
exec.ask = ask;
|
|
85
|
+
tools.exec = exec;
|
|
86
|
+
cfg.tools = tools;
|
|
87
|
+
await configApi.replaceConfigFile({
|
|
88
|
+
nextConfig: cfg,
|
|
89
|
+
afterWrite: { mode: "auto" }
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const formatStatus = (security, ask) => {
|
|
93
|
+
const secIcon = security === "full" ? "🟢" : security === "allowlist" ? "🟡" : "🔴";
|
|
94
|
+
const askIcon = ask === "off" ? "🟢" : ask === "always" ? "🔴" : "🟡";
|
|
95
|
+
return [
|
|
96
|
+
`🔐 当前审批配置`,
|
|
97
|
+
``,
|
|
98
|
+
`${secIcon} 安全模式 (security): **${security}**`,
|
|
99
|
+
`${askIcon} 审批模式 (ask): **${ask}**`,
|
|
100
|
+
``,
|
|
101
|
+
security === "deny" ? `⚠️ 当前为 deny 模式,所有命令执行被拒绝` : security === "full" && ask === "off" ? `✅ 所有命令无需审批直接执行` : security === "allowlist" && ask === "on-miss" ? `🛡️ 白名单命令直接执行,其余需审批` : ask === "always" ? `🔒 每次命令执行都需要人工审批` : `ℹ️ security=${security}, ask=${ask}`
|
|
102
|
+
].join("\n");
|
|
103
|
+
};
|
|
104
|
+
if (!arg) return [
|
|
105
|
+
`🔐 命令执行审批配置`,
|
|
106
|
+
``,
|
|
107
|
+
`<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
|
|
108
|
+
`<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
|
|
109
|
+
`<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
|
|
110
|
+
`<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
|
|
111
|
+
`<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`
|
|
112
|
+
].join("\n");
|
|
113
|
+
if (arg === "status") {
|
|
114
|
+
const { security, ask } = loadExecConfig();
|
|
115
|
+
return [
|
|
116
|
+
formatStatus(security, ask),
|
|
117
|
+
``,
|
|
118
|
+
`<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批`,
|
|
119
|
+
`<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
|
|
120
|
+
`<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
|
|
121
|
+
`<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
if (arg === "on") try {
|
|
125
|
+
await writeExecConfig("allowlist", "on-miss");
|
|
126
|
+
return [
|
|
127
|
+
`✅ 审批已开启`,
|
|
128
|
+
``,
|
|
129
|
+
`• security = allowlist(白名单模式)`,
|
|
130
|
+
`• ask = on-miss(未命中白名单时需审批)`,
|
|
131
|
+
``,
|
|
132
|
+
`已批准的命令自动加入白名单,下次直接执行。`
|
|
133
|
+
].join("\n");
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
136
|
+
}
|
|
137
|
+
if (arg === "off") try {
|
|
138
|
+
await writeExecConfig("full", "off");
|
|
139
|
+
return [
|
|
140
|
+
`✅ 审批已关闭`,
|
|
141
|
+
``,
|
|
142
|
+
`• security = full(允许所有命令)`,
|
|
143
|
+
`• ask = off(不需要审批)`,
|
|
144
|
+
``,
|
|
145
|
+
`⚠️ 所有命令将直接执行,不会弹出审批确认。`
|
|
146
|
+
].join("\n");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
149
|
+
}
|
|
150
|
+
if (arg === "always" || arg === "strict") try {
|
|
151
|
+
await writeExecConfig("allowlist", "always");
|
|
152
|
+
return [
|
|
153
|
+
`✅ 已切换为严格审批模式`,
|
|
154
|
+
``,
|
|
155
|
+
`• security = allowlist`,
|
|
156
|
+
`• ask = always(每次执行都需审批)`,
|
|
157
|
+
``,
|
|
158
|
+
`每个命令都会弹出审批按钮,需手动确认。`
|
|
159
|
+
].join("\n");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
162
|
+
}
|
|
163
|
+
if (arg === "reset") try {
|
|
164
|
+
const cfg = structuredClone(configApi.current());
|
|
165
|
+
const tools = cfg.tools ?? {};
|
|
166
|
+
const exec = tools.exec ?? {};
|
|
167
|
+
delete exec.security;
|
|
168
|
+
delete exec.ask;
|
|
169
|
+
if (Object.keys(exec).length === 0) delete tools.exec;
|
|
170
|
+
else tools.exec = exec;
|
|
171
|
+
if (Object.keys(tools).length === 0) delete cfg.tools;
|
|
172
|
+
else cfg.tools = tools;
|
|
173
|
+
await configApi.replaceConfigFile({
|
|
174
|
+
nextConfig: cfg,
|
|
175
|
+
afterWrite: { mode: "auto" }
|
|
176
|
+
});
|
|
177
|
+
return [
|
|
178
|
+
`✅ 审批配置已重置`,
|
|
179
|
+
``,
|
|
180
|
+
`已移除 tools.exec.security 和 tools.exec.ask`,
|
|
181
|
+
`框架将使用默认值(security=deny, ask=on-miss)`,
|
|
182
|
+
``,
|
|
183
|
+
`如需开启命令执行,请使用 /bot-approve on`
|
|
184
|
+
].join("\n");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
187
|
+
}
|
|
188
|
+
return [
|
|
189
|
+
`❌ 未知参数: ${arg}`,
|
|
190
|
+
``,
|
|
191
|
+
`可用选项: on | off | always | reset | status`,
|
|
192
|
+
`输入 /bot-approve ? 查看详细用法`
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-basic.ts
|
|
199
|
+
const QQBOT_PLUGIN_GITHUB_URL = "https://github.com/openclaw/openclaw/tree/main/extensions/qqbot";
|
|
200
|
+
const QQBOT_UPGRADE_GUIDE_URL = "https://q.qq.com/qqbot/openclaw/upgrade.html";
|
|
201
|
+
function registerBasicBotCommands(registry) {
|
|
202
|
+
registry.register({
|
|
203
|
+
name: "bot-help",
|
|
204
|
+
description: "查看所有内置命令",
|
|
205
|
+
usage: [
|
|
206
|
+
`/bot-help`,
|
|
207
|
+
``,
|
|
208
|
+
`查看所有可用的 QQBot 内置命令及其简要说明。`,
|
|
209
|
+
`在命令后追加 ? 可查看详细用法。`
|
|
210
|
+
].join("\n"),
|
|
211
|
+
handler: (ctx) => {
|
|
212
|
+
const isGroup = ctx.type === "group";
|
|
213
|
+
const lines = [`### QQBot 内置命令`, ``];
|
|
214
|
+
for (const [name, cmd] of registry.getAllCommands()) {
|
|
215
|
+
if (isGroup && cmd.c2cOnly) continue;
|
|
216
|
+
lines.push(`<qqbot-cmd-input text="/${name}" show="/${name}"/> ${cmd.description}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push(``, `> 插件版本 v${getPluginVersionString()}`);
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
registry.register({
|
|
223
|
+
name: "bot-me",
|
|
224
|
+
description: "查看当前发送者的账号ID",
|
|
225
|
+
c2cOnly: true,
|
|
226
|
+
usage: [
|
|
227
|
+
`/bot-me`,
|
|
228
|
+
``,
|
|
229
|
+
`显示当前发送者的账号ID`
|
|
230
|
+
].join("\n"),
|
|
231
|
+
handler: (ctx) => {
|
|
232
|
+
return `你的账号ID:\`${ctx.senderId}\``;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
registry.register({
|
|
236
|
+
name: "bot-ping",
|
|
237
|
+
description: "测试 OpenClaw 与 QQ 之间的网络延迟",
|
|
238
|
+
usage: [
|
|
239
|
+
`/bot-ping`,
|
|
240
|
+
``,
|
|
241
|
+
`测试当前 OpenClaw 宿主机与 QQ 服务器之间的网络延迟。`,
|
|
242
|
+
`返回网络传输耗时和插件处理耗时。`
|
|
243
|
+
].join("\n"),
|
|
244
|
+
handler: (ctx) => {
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const eventTime = new Date(ctx.eventTimestamp).getTime();
|
|
247
|
+
if (Number.isNaN(eventTime)) return `✅ pong!`;
|
|
248
|
+
const totalMs = now - eventTime;
|
|
249
|
+
const qqToPlugin = ctx.receivedAt - eventTime;
|
|
250
|
+
const pluginProcess = now - ctx.receivedAt;
|
|
251
|
+
return [
|
|
252
|
+
`✅ pong!`,
|
|
253
|
+
``,
|
|
254
|
+
`⏱ 延迟:${totalMs}ms`,
|
|
255
|
+
` ├ 网络传输:${qqToPlugin}ms`,
|
|
256
|
+
` └ 插件处理:${pluginProcess}ms`
|
|
257
|
+
].join("\n");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
registry.register({
|
|
261
|
+
name: "bot-version",
|
|
262
|
+
description: "查看 QQBot 插件版本和 OpenClaw 框架版本",
|
|
263
|
+
c2cOnly: true,
|
|
264
|
+
usage: [
|
|
265
|
+
`/bot-version`,
|
|
266
|
+
``,
|
|
267
|
+
`查看当前 QQBot 插件版本和 OpenClaw 框架版本。`
|
|
268
|
+
].join("\n"),
|
|
269
|
+
handler: async () => {
|
|
270
|
+
const frameworkVersion = resolveRuntimeServiceVersion();
|
|
271
|
+
const ver = getPluginVersionString();
|
|
272
|
+
return [
|
|
273
|
+
`🦞 OpenClaw 框架版本:${frameworkVersion}`,
|
|
274
|
+
`🤖 QQBot 插件版本:v${ver}`,
|
|
275
|
+
`🌟 官方 GitHub 仓库:[点击前往](${QQBOT_PLUGIN_GITHUB_URL})`
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
registry.register({
|
|
280
|
+
name: "bot-upgrade",
|
|
281
|
+
description: "查看 QQBot 升级指引",
|
|
282
|
+
c2cOnly: true,
|
|
283
|
+
usage: [
|
|
284
|
+
`/bot-upgrade`,
|
|
285
|
+
``,
|
|
286
|
+
`查看 QQBot 升级说明。`
|
|
287
|
+
].join("\n"),
|
|
288
|
+
handler: () => [`📘 QQBot 升级指引:`, `[点击查看升级说明](${QQBOT_UPGRADE_GUIDE_URL})`].join("\n")
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-clear-storage.ts
|
|
293
|
+
function scanDirectoryFiles(dirPath) {
|
|
294
|
+
const files = [];
|
|
295
|
+
if (!fs.existsSync(dirPath)) return files;
|
|
296
|
+
const walk = (dir) => {
|
|
297
|
+
let entries;
|
|
298
|
+
try {
|
|
299
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
const fullPath = path.join(dir, entry.name);
|
|
305
|
+
if (entry.isDirectory()) walk(fullPath);
|
|
306
|
+
else if (entry.isFile()) try {
|
|
307
|
+
const stat = fs.statSync(fullPath);
|
|
308
|
+
files.push({
|
|
309
|
+
filePath: fullPath,
|
|
310
|
+
size: stat.size
|
|
311
|
+
});
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
walk(dirPath);
|
|
316
|
+
files.sort((a, b) => b.size - a.size);
|
|
317
|
+
return files;
|
|
318
|
+
}
|
|
319
|
+
function formatBytes(bytes) {
|
|
320
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
321
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
322
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
323
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
324
|
+
}
|
|
325
|
+
function removeEmptyDirs(dirPath) {
|
|
326
|
+
if (!fs.existsSync(dirPath)) return;
|
|
327
|
+
let entries;
|
|
328
|
+
try {
|
|
329
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
330
|
+
} catch {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const entry of entries) if (entry.isDirectory()) removeEmptyDirs(path.join(dirPath, entry.name));
|
|
334
|
+
try {
|
|
335
|
+
if (fs.readdirSync(dirPath).length === 0) fs.rmdirSync(dirPath);
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
const CLEAR_STORAGE_MAX_DISPLAY = 10;
|
|
339
|
+
/**
|
|
340
|
+
* Resolve the canonical QQBot downloads directory.
|
|
341
|
+
*
|
|
342
|
+
* All inbound attachments and outbound fallback downloads are stored directly
|
|
343
|
+
* under `~/.openclaw/media/qqbot/downloads/` without appId subdivision.
|
|
344
|
+
* The clear-storage command therefore cleans the entire downloads root.
|
|
345
|
+
*/
|
|
346
|
+
function resolveQqbotDownloadsDir() {
|
|
347
|
+
return getQQBotMediaPath("downloads");
|
|
348
|
+
}
|
|
349
|
+
function registerClearStorageCommands(registry) {
|
|
350
|
+
registry.register({
|
|
351
|
+
name: "bot-clear-storage",
|
|
352
|
+
description: "清理通过 QQBot 对话产生的下载文件,释放主机磁盘空间",
|
|
353
|
+
requireAuth: true,
|
|
354
|
+
c2cOnly: true,
|
|
355
|
+
usage: [
|
|
356
|
+
`/bot-clear-storage`,
|
|
357
|
+
``,
|
|
358
|
+
`扫描 QQBot 下载目录下的所有文件并列出明细。`,
|
|
359
|
+
`确认后执行删除,释放主机磁盘空间。`,
|
|
360
|
+
``,
|
|
361
|
+
`/bot-clear-storage --force 确认执行清理`,
|
|
362
|
+
``,
|
|
363
|
+
`⚠️ 仅在私聊中可用。`
|
|
364
|
+
].join("\n"),
|
|
365
|
+
handler: (ctx) => {
|
|
366
|
+
const isForce = ctx.args.trim() === "--force";
|
|
367
|
+
const targetDir = resolveQqbotDownloadsDir();
|
|
368
|
+
const displayDir = `~/.openclaw/media/qqbot/downloads`;
|
|
369
|
+
if (!isForce) {
|
|
370
|
+
const files = scanDirectoryFiles(targetDir);
|
|
371
|
+
if (files.length === 0) return [
|
|
372
|
+
`✅ 当前没有需要清理的文件`,
|
|
373
|
+
``,
|
|
374
|
+
`目录 \`${displayDir}\` 为空或不存在。`
|
|
375
|
+
].join("\n");
|
|
376
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
377
|
+
const lines = [
|
|
378
|
+
`即将清理 \`${displayDir}\` 目录下所有文件,总共 ${files.length} 个文件,占用磁盘存储空间 ${formatBytes(totalSize)}。`,
|
|
379
|
+
``,
|
|
380
|
+
`目录文件概况:`
|
|
381
|
+
];
|
|
382
|
+
const displayFiles = files.slice(0, CLEAR_STORAGE_MAX_DISPLAY);
|
|
383
|
+
for (const f of displayFiles) {
|
|
384
|
+
const relativePath = path.relative(targetDir, f.filePath).replace(/\\/g, "/");
|
|
385
|
+
lines.push(`${relativePath} (${formatBytes(f.size)})`, ``, ``);
|
|
386
|
+
}
|
|
387
|
+
if (files.length > CLEAR_STORAGE_MAX_DISPLAY) lines.push(`...[合计:${files.length} 个文件(${formatBytes(totalSize)})]`, ``);
|
|
388
|
+
lines.push(``, `---`, ``, `确认清理后,上述保存在 OpenClaw 运行主机磁盘上的文件将永久删除,后续对话过程中 AI 无法再找回相关文件。`, `‼️ 点击指令确认删除`, `<qqbot-cmd-enter text="/bot-clear-storage --force" />`);
|
|
389
|
+
return lines.join("\n");
|
|
390
|
+
}
|
|
391
|
+
const files = scanDirectoryFiles(targetDir);
|
|
392
|
+
if (files.length === 0) return `✅ 目录已为空,无需清理`;
|
|
393
|
+
let deletedCount = 0;
|
|
394
|
+
let deletedSize = 0;
|
|
395
|
+
let failedCount = 0;
|
|
396
|
+
for (const f of files) try {
|
|
397
|
+
fs.unlinkSync(f.filePath);
|
|
398
|
+
deletedCount++;
|
|
399
|
+
deletedSize += f.size;
|
|
400
|
+
} catch {
|
|
401
|
+
failedCount++;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
removeEmptyDirs(targetDir);
|
|
405
|
+
} catch {}
|
|
406
|
+
if (failedCount === 0) return [
|
|
407
|
+
`✅ 清理成功`,
|
|
408
|
+
``,
|
|
409
|
+
`已删除 ${deletedCount} 个文件,释放 ${formatBytes(deletedSize)} 磁盘空间。`
|
|
410
|
+
].join("\n");
|
|
411
|
+
return [
|
|
412
|
+
`⚠️ 部分清理完成`,
|
|
413
|
+
``,
|
|
414
|
+
`已删除 ${deletedCount} 个文件(${formatBytes(deletedSize)}),${failedCount} 个文件删除失败。`
|
|
415
|
+
].join("\n");
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region extensions/qqbot/src/engine/commands/builtin/log-helpers.ts
|
|
421
|
+
/** Read user-configured log file paths from local config files. */
|
|
422
|
+
function getConfiguredLogFiles() {
|
|
423
|
+
const homeDir = getHomeDir();
|
|
424
|
+
const files = [];
|
|
425
|
+
for (const cli of [
|
|
426
|
+
"openclaw",
|
|
427
|
+
"clawdbot",
|
|
428
|
+
"moltbot"
|
|
429
|
+
]) try {
|
|
430
|
+
const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
|
|
431
|
+
if (!fs.existsSync(cfgPath)) continue;
|
|
432
|
+
const logFile = JSON.parse(fs.readFileSync(cfgPath, "utf8"))?.logging?.file;
|
|
433
|
+
if (logFile && typeof logFile === "string") files.push(path.resolve(logFile));
|
|
434
|
+
break;
|
|
435
|
+
} catch {}
|
|
436
|
+
return files;
|
|
437
|
+
}
|
|
438
|
+
/** Collect directories that may contain runtime logs across common install layouts. */
|
|
439
|
+
function collectCandidateLogDirs() {
|
|
440
|
+
const homeDir = getHomeDir();
|
|
441
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
442
|
+
const pushDir = (p) => {
|
|
443
|
+
if (!p) return;
|
|
444
|
+
const normalized = path.resolve(p);
|
|
445
|
+
dirs.add(normalized);
|
|
446
|
+
};
|
|
447
|
+
const pushStateDir = (stateDir) => {
|
|
448
|
+
if (!stateDir) return;
|
|
449
|
+
pushDir(stateDir);
|
|
450
|
+
pushDir(path.join(stateDir, "logs"));
|
|
451
|
+
};
|
|
452
|
+
for (const logFile of getConfiguredLogFiles()) pushDir(path.dirname(logFile));
|
|
453
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
454
|
+
if (!value) continue;
|
|
455
|
+
if (/STATE_DIR$/i.test(key) && /(OPENCLAW|CLAWDBOT|MOLTBOT)/i.test(key)) pushStateDir(value);
|
|
456
|
+
}
|
|
457
|
+
for (const name of [
|
|
458
|
+
".openclaw",
|
|
459
|
+
".clawdbot",
|
|
460
|
+
".moltbot",
|
|
461
|
+
"openclaw",
|
|
462
|
+
"clawdbot",
|
|
463
|
+
"moltbot"
|
|
464
|
+
]) {
|
|
465
|
+
pushDir(path.join(homeDir, name));
|
|
466
|
+
pushDir(path.join(homeDir, name, "logs"));
|
|
467
|
+
}
|
|
468
|
+
const searchRoots = new Set([
|
|
469
|
+
homeDir,
|
|
470
|
+
process.cwd(),
|
|
471
|
+
path.dirname(process.cwd())
|
|
472
|
+
]);
|
|
473
|
+
if (process.env.APPDATA) searchRoots.add(process.env.APPDATA);
|
|
474
|
+
if (process.env.LOCALAPPDATA) searchRoots.add(process.env.LOCALAPPDATA);
|
|
475
|
+
for (const root of searchRoots) try {
|
|
476
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (!entry.isDirectory()) continue;
|
|
479
|
+
if (!/(openclaw|clawdbot|moltbot)/i.test(entry.name)) continue;
|
|
480
|
+
const base = path.join(root, entry.name);
|
|
481
|
+
pushDir(base);
|
|
482
|
+
pushDir(path.join(base, "logs"));
|
|
483
|
+
}
|
|
484
|
+
} catch {}
|
|
485
|
+
if (!isWindows()) for (const name of [
|
|
486
|
+
"openclaw",
|
|
487
|
+
"clawdbot",
|
|
488
|
+
"moltbot"
|
|
489
|
+
]) pushDir(path.join("/var/log", name));
|
|
490
|
+
const tmpRoots = /* @__PURE__ */ new Set();
|
|
491
|
+
if (isWindows()) {
|
|
492
|
+
tmpRoots.add("C:\\tmp");
|
|
493
|
+
if (process.env.TEMP) tmpRoots.add(process.env.TEMP);
|
|
494
|
+
if (process.env.TMP) tmpRoots.add(process.env.TMP);
|
|
495
|
+
if (process.env.LOCALAPPDATA) tmpRoots.add(path.join(process.env.LOCALAPPDATA, "Temp"));
|
|
496
|
+
} else tmpRoots.add("/tmp");
|
|
497
|
+
for (const tmpRoot of tmpRoots) for (const name of [
|
|
498
|
+
"openclaw",
|
|
499
|
+
"clawdbot",
|
|
500
|
+
"moltbot"
|
|
501
|
+
]) pushDir(path.join(tmpRoot, name));
|
|
502
|
+
return Array.from(dirs);
|
|
503
|
+
}
|
|
504
|
+
function collectRecentLogFiles(logDirs) {
|
|
505
|
+
const candidates = [];
|
|
506
|
+
const dedupe = /* @__PURE__ */ new Set();
|
|
507
|
+
const pushFile = (filePath, sourceDir) => {
|
|
508
|
+
const normalized = path.resolve(filePath);
|
|
509
|
+
if (dedupe.has(normalized)) return;
|
|
510
|
+
try {
|
|
511
|
+
const stat = fs.statSync(normalized);
|
|
512
|
+
if (!stat.isFile()) return;
|
|
513
|
+
dedupe.add(normalized);
|
|
514
|
+
candidates.push({
|
|
515
|
+
filePath: normalized,
|
|
516
|
+
sourceDir,
|
|
517
|
+
mtimeMs: stat.mtimeMs
|
|
518
|
+
});
|
|
519
|
+
} catch {}
|
|
520
|
+
};
|
|
521
|
+
for (const logFile of getConfiguredLogFiles()) pushFile(logFile, path.dirname(logFile));
|
|
522
|
+
for (const dir of logDirs) {
|
|
523
|
+
pushFile(path.join(dir, "gateway.log"), dir);
|
|
524
|
+
pushFile(path.join(dir, "gateway.err.log"), dir);
|
|
525
|
+
pushFile(path.join(dir, "openclaw.log"), dir);
|
|
526
|
+
pushFile(path.join(dir, "clawdbot.log"), dir);
|
|
527
|
+
pushFile(path.join(dir, "moltbot.log"), dir);
|
|
528
|
+
try {
|
|
529
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
if (!entry.isFile()) continue;
|
|
532
|
+
if (!/\.(log|txt)$/i.test(entry.name)) continue;
|
|
533
|
+
if (!/(gateway|openclaw|clawdbot|moltbot)/i.test(entry.name)) continue;
|
|
534
|
+
pushFile(path.join(dir, entry.name), dir);
|
|
535
|
+
}
|
|
536
|
+
} catch {}
|
|
537
|
+
}
|
|
538
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
539
|
+
return candidates;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Read the last N lines of a file without loading the entire file into memory.
|
|
543
|
+
*/
|
|
544
|
+
function tailFileLines(filePath, maxLines) {
|
|
545
|
+
const fd = fs.openSync(filePath, "r");
|
|
546
|
+
try {
|
|
547
|
+
const fileSize = fs.fstatSync(fd).size;
|
|
548
|
+
if (fileSize === 0) return {
|
|
549
|
+
tail: [],
|
|
550
|
+
totalFileLines: 0
|
|
551
|
+
};
|
|
552
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
553
|
+
const chunks = [];
|
|
554
|
+
let bytesRead = 0;
|
|
555
|
+
let position = fileSize;
|
|
556
|
+
let newlineCount = 0;
|
|
557
|
+
while (position > 0 && newlineCount <= maxLines) {
|
|
558
|
+
const readSize = Math.min(CHUNK_SIZE, position);
|
|
559
|
+
position -= readSize;
|
|
560
|
+
const buf = Buffer.alloc(readSize);
|
|
561
|
+
fs.readSync(fd, buf, 0, readSize, position);
|
|
562
|
+
chunks.unshift(buf);
|
|
563
|
+
bytesRead += readSize;
|
|
564
|
+
for (let i = 0; i < readSize; i++) if (buf[i] === 10) newlineCount++;
|
|
565
|
+
}
|
|
566
|
+
const allLines = Buffer.concat(chunks).toString("utf8").split("\n");
|
|
567
|
+
const tail = allLines.slice(-maxLines);
|
|
568
|
+
let totalFileLines;
|
|
569
|
+
if (bytesRead >= fileSize) totalFileLines = allLines.length;
|
|
570
|
+
else {
|
|
571
|
+
const avgBytesPerLine = bytesRead / Math.max(allLines.length, 1);
|
|
572
|
+
totalFileLines = Math.round(fileSize / avgBytesPerLine);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
tail,
|
|
576
|
+
totalFileLines
|
|
577
|
+
};
|
|
578
|
+
} finally {
|
|
579
|
+
fs.closeSync(fd);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Build the /bot-logs result: collect recent log files, write them to a temp file.
|
|
584
|
+
*/
|
|
585
|
+
function buildBotLogsResult() {
|
|
586
|
+
const logDirs = collectCandidateLogDirs();
|
|
587
|
+
const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
|
|
588
|
+
if (recentFiles.length === 0) {
|
|
589
|
+
const existingDirs = logDirs.filter((d) => {
|
|
590
|
+
try {
|
|
591
|
+
return fs.existsSync(d);
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
const searched = existingDirs.length > 0 ? existingDirs.map((d) => ` • ${d}`).join("\n") : logDirs.slice(0, 6).map((d) => ` • ${d}`).join("\n") + (logDirs.length > 6 ? `\n …以及另外 ${logDirs.length - 6} 个路径` : "");
|
|
597
|
+
return [
|
|
598
|
+
`⚠️ 未找到日志文件`,
|
|
599
|
+
``,
|
|
600
|
+
`已搜索以下${existingDirs.length > 0 ? "存在的" : ""}路径:`,
|
|
601
|
+
searched,
|
|
602
|
+
``,
|
|
603
|
+
`💡 如果日志存放在自定义路径,请在配置中添加:`,
|
|
604
|
+
` "logging": { "file": "/path/to/your/logfile.log" }`
|
|
605
|
+
].join("\n");
|
|
606
|
+
}
|
|
607
|
+
const lines = [];
|
|
608
|
+
let totalIncluded = 0;
|
|
609
|
+
let totalOriginal = 0;
|
|
610
|
+
let truncatedCount = 0;
|
|
611
|
+
const MAX_LINES_PER_FILE = 1e3;
|
|
612
|
+
for (const logFile of recentFiles) try {
|
|
613
|
+
const { tail, totalFileLines } = tailFileLines(logFile.filePath, MAX_LINES_PER_FILE);
|
|
614
|
+
if (tail.length > 0) {
|
|
615
|
+
const fileName = path.basename(logFile.filePath);
|
|
616
|
+
lines.push(`\n========== ${fileName} (last ${tail.length} of ${totalFileLines} lines) ==========`);
|
|
617
|
+
lines.push(`from: ${logFile.sourceDir}`);
|
|
618
|
+
lines.push(...tail);
|
|
619
|
+
totalIncluded += tail.length;
|
|
620
|
+
totalOriginal += totalFileLines;
|
|
621
|
+
if (totalFileLines > MAX_LINES_PER_FILE) truncatedCount++;
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
lines.push(`[Failed to read ${path.basename(logFile.filePath)}]`);
|
|
625
|
+
}
|
|
626
|
+
if (lines.length === 0) return `⚠️ 找到了日志文件,但无法读取。请检查文件权限。`;
|
|
627
|
+
const tmpDir = getQQBotDataDir("downloads");
|
|
628
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
629
|
+
const tmpFile = path.join(tmpDir, `bot-logs-${timestamp}.txt`);
|
|
630
|
+
fs.writeFileSync(tmpFile, lines.join("\n"), "utf8");
|
|
631
|
+
const fileCount = recentFiles.length;
|
|
632
|
+
const topSources = Array.from(new Set(recentFiles.map((item) => item.sourceDir))).slice(0, 3);
|
|
633
|
+
let summaryText = `共 ${fileCount} 个日志文件,包含 ${totalIncluded} 行内容`;
|
|
634
|
+
if (truncatedCount > 0) summaryText += `(其中 ${truncatedCount} 个文件已截断为最后 ${MAX_LINES_PER_FILE} 行,总计原始 ${totalOriginal} 行)`;
|
|
635
|
+
return {
|
|
636
|
+
text: `📋 ${summaryText}\n📂 来源:${topSources.join(" | ")}`,
|
|
637
|
+
filePath: tmpFile
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-logs.ts
|
|
642
|
+
function registerLogCommands(registry) {
|
|
643
|
+
registry.register({
|
|
644
|
+
name: "bot-logs",
|
|
645
|
+
description: "导出本地日志文件",
|
|
646
|
+
requireAuth: true,
|
|
647
|
+
c2cOnly: true,
|
|
648
|
+
usage: [
|
|
649
|
+
`/bot-logs`,
|
|
650
|
+
``,
|
|
651
|
+
`导出最近的 OpenClaw 日志文件(最多 4 个文件)。`,
|
|
652
|
+
`每个文件只保留最后 1000 行,并作为附件返回。`
|
|
653
|
+
].join("\n"),
|
|
654
|
+
handler: () => {
|
|
655
|
+
return buildBotLogsResult();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-streaming.ts
|
|
661
|
+
function isStreamingConfigEnabled(streaming) {
|
|
662
|
+
if (streaming === true) return true;
|
|
663
|
+
if (streaming === false || streaming === void 0 || streaming === null) return false;
|
|
664
|
+
if (typeof streaming === "object") {
|
|
665
|
+
const o = streaming;
|
|
666
|
+
if (o.c2cStreamApi === true) return true;
|
|
667
|
+
if (o.mode === "off") return false;
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
function registerStreamingCommands(registry) {
|
|
673
|
+
registry.register({
|
|
674
|
+
name: "bot-streaming",
|
|
675
|
+
description: "一键开关流式消息",
|
|
676
|
+
c2cOnly: true,
|
|
677
|
+
usage: [
|
|
678
|
+
`/bot-streaming on 开启流式消息`,
|
|
679
|
+
`/bot-streaming off 关闭流式消息`,
|
|
680
|
+
`/bot-streaming 查看当前流式消息状态`,
|
|
681
|
+
``,
|
|
682
|
+
`开启后,AI 的回复会以流式形式逐步显示(打字机效果)。`,
|
|
683
|
+
`注意:仅 C2C(私聊)支持流式消息。`
|
|
684
|
+
].join("\n"),
|
|
685
|
+
handler: async (ctx) => {
|
|
686
|
+
const arg = ctx.args.trim().toLowerCase();
|
|
687
|
+
const currentOn = isStreamingConfigEnabled(ctx.accountConfig?.streaming);
|
|
688
|
+
if (!arg) return [
|
|
689
|
+
`📡 流式消息状态:${currentOn ? "✅ 已开启" : "❌ 已关闭"}`,
|
|
690
|
+
``,
|
|
691
|
+
`使用 <qqbot-cmd-input text="/bot-streaming on" show="/bot-streaming on"/> 开启`,
|
|
692
|
+
`使用 <qqbot-cmd-input text="/bot-streaming off" show="/bot-streaming off"/> 关闭`
|
|
693
|
+
].join("\n");
|
|
694
|
+
if (arg !== "on" && arg !== "off") return `❌ 参数错误,请使用 on 或 off\n\n示例:/bot-streaming on`;
|
|
695
|
+
const wantOn = arg === "on";
|
|
696
|
+
if (wantOn === currentOn) return `📡 流式消息已经是${wantOn ? "开启" : "关闭"}状态,无需操作`;
|
|
697
|
+
let runtime;
|
|
698
|
+
try {
|
|
699
|
+
const getter = getApproveRuntimeGetter();
|
|
700
|
+
if (!getter) throw new Error("runtime not available");
|
|
701
|
+
runtime = getter();
|
|
702
|
+
} catch {
|
|
703
|
+
const fwVer = resolveRuntimeServiceVersion();
|
|
704
|
+
const ver = getPluginVersionString();
|
|
705
|
+
return [
|
|
706
|
+
`❌ 当前版本不支持该指令`,
|
|
707
|
+
``,
|
|
708
|
+
`🦞框架版本:${fwVer}`,
|
|
709
|
+
`🤖QQBot 插件版本:v${ver}`,
|
|
710
|
+
``,
|
|
711
|
+
`可通过以下命令手动开启流式消息:`,
|
|
712
|
+
``,
|
|
713
|
+
`\`\`\`shell`,
|
|
714
|
+
`# 1. 开启流式消息`,
|
|
715
|
+
`openclaw config set channels.qqbot.streaming true`,
|
|
716
|
+
``,
|
|
717
|
+
`# 2. 重启网关使配置生效`,
|
|
718
|
+
`openclaw gateway restart`,
|
|
719
|
+
`\`\`\``
|
|
720
|
+
].join("\n");
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
const configApi = runtime.config;
|
|
724
|
+
const currentCfg = structuredClone(configApi.current());
|
|
725
|
+
const qqbot = (currentCfg.channels ?? {}).qqbot;
|
|
726
|
+
if (!qqbot) return `❌ 配置文件中未找到 qqbot 通道配置`;
|
|
727
|
+
const accountId = ctx.accountId;
|
|
728
|
+
const newVal = wantOn;
|
|
729
|
+
if (accountId !== "default") {
|
|
730
|
+
const nextAccounts = { ...qqbot.accounts ?? {} };
|
|
731
|
+
const acct = { ...nextAccounts[accountId] };
|
|
732
|
+
acct.streaming = newVal;
|
|
733
|
+
nextAccounts[accountId] = acct;
|
|
734
|
+
qqbot.accounts = nextAccounts;
|
|
735
|
+
} else {
|
|
736
|
+
qqbot.streaming = newVal;
|
|
737
|
+
const accs = qqbot.accounts;
|
|
738
|
+
if (accs?.default && typeof accs.default === "object") {
|
|
739
|
+
const nextAccs = { ...accs };
|
|
740
|
+
nextAccs.default = {
|
|
741
|
+
...accs.default,
|
|
742
|
+
streaming: newVal
|
|
743
|
+
};
|
|
744
|
+
qqbot.accounts = nextAccs;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
await configApi.replaceConfigFile({
|
|
748
|
+
nextConfig: currentCfg,
|
|
749
|
+
afterWrite: { mode: "auto" }
|
|
750
|
+
});
|
|
751
|
+
return [
|
|
752
|
+
`✅ 流式消息已${wantOn ? "开启" : "关闭"}`,
|
|
753
|
+
``,
|
|
754
|
+
wantOn ? `AI 的回复将以流式形式逐步显示(仅私聊生效)。` : `AI 的回复将恢复为完整发送。`
|
|
755
|
+
].join("\n");
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return `❌ 配置写入失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region extensions/qqbot/src/engine/commands/builtin/register-all.ts
|
|
764
|
+
/**
|
|
765
|
+
* Register all built-in slash commands on the shared registry instance.
|
|
766
|
+
*/
|
|
767
|
+
function registerBuiltinSlashCommands(registry) {
|
|
768
|
+
registerBasicBotCommands(registry);
|
|
769
|
+
registerLogCommands(registry);
|
|
770
|
+
registerClearStorageCommands(registry);
|
|
771
|
+
registerStreamingCommands(registry);
|
|
772
|
+
registerApproveCommands(registry);
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region extensions/qqbot/src/engine/commands/slash-commands.ts
|
|
776
|
+
/** Lowercase and trim a string. */
|
|
777
|
+
function lc(s) {
|
|
778
|
+
return (s ?? "").toLowerCase().trim();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Slash command registry.
|
|
782
|
+
*
|
|
783
|
+
* Maintains two maps:
|
|
784
|
+
* - `commands` — pre-dispatch commands (requireAuth: false)
|
|
785
|
+
* - `frameworkCommands` — auth-gated commands (requireAuth: true)
|
|
786
|
+
*/
|
|
787
|
+
var SlashCommandRegistry = class {
|
|
788
|
+
constructor() {
|
|
789
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
790
|
+
this.frameworkCommands = /* @__PURE__ */ new Map();
|
|
791
|
+
}
|
|
792
|
+
/** Register one command. */
|
|
793
|
+
register(cmd) {
|
|
794
|
+
const key = lc(cmd.name);
|
|
795
|
+
this.commands.set(key, cmd);
|
|
796
|
+
if (cmd.requireAuth) this.frameworkCommands.set(key, cmd);
|
|
797
|
+
}
|
|
798
|
+
/** Return all auth-gated commands for framework registration. */
|
|
799
|
+
getFrameworkCommands() {
|
|
800
|
+
return Array.from(this.frameworkCommands.values()).map((cmd) => ({
|
|
801
|
+
name: cmd.name,
|
|
802
|
+
description: cmd.description,
|
|
803
|
+
usage: cmd.usage,
|
|
804
|
+
handler: cmd.handler
|
|
805
|
+
}));
|
|
806
|
+
}
|
|
807
|
+
/** Return all pre-dispatch commands. */
|
|
808
|
+
getPreDispatchCommands() {
|
|
809
|
+
return this.commands;
|
|
810
|
+
}
|
|
811
|
+
/** Return all registered commands (both maps) for help listing. */
|
|
812
|
+
getAllCommands() {
|
|
813
|
+
const all = /* @__PURE__ */ new Map();
|
|
814
|
+
for (const [k, v] of this.commands) all.set(k, v);
|
|
815
|
+
for (const [k, v] of this.frameworkCommands) all.set(k, v);
|
|
816
|
+
return all;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Try to match and execute a pre-dispatch slash command.
|
|
820
|
+
*
|
|
821
|
+
* @returns A reply when matched, or null when the message should continue
|
|
822
|
+
* through normal routing.
|
|
823
|
+
*/
|
|
824
|
+
async matchSlashCommand(ctx, log) {
|
|
825
|
+
const content = ctx.rawContent.trim();
|
|
826
|
+
if (!content.startsWith("/")) return null;
|
|
827
|
+
const spaceIdx = content.indexOf(" ");
|
|
828
|
+
const cmdName = lc(spaceIdx === -1 ? content.slice(1) : content.slice(1, spaceIdx));
|
|
829
|
+
const args = spaceIdx === -1 ? "" : content.slice(spaceIdx + 1).trim();
|
|
830
|
+
const cmd = this.commands.get(cmdName);
|
|
831
|
+
if (!cmd) return null;
|
|
832
|
+
if (cmd.c2cOnly && ctx.type !== "c2c") return `💡 请在私聊中使用此指令`;
|
|
833
|
+
if (cmd.requireAuth && !ctx.commandAuthorized) {
|
|
834
|
+
log?.info?.(`[qqbot] Slash command /${cmd.name} rejected: sender ${ctx.senderId} is not authorized`);
|
|
835
|
+
return `⛔ 权限不足:请先在 channels.qqbot.${ctx.type === "group" || ctx.type === "guild" ? "groupAllowFrom" : "allowFrom"} 中配置明确的发送者列表后再使用 /${cmd.name}。`;
|
|
836
|
+
}
|
|
837
|
+
if (args === "?") {
|
|
838
|
+
if (cmd.usage) return `📖 /${cmd.name} 用法:\n\n${cmd.usage}`;
|
|
839
|
+
return `/${cmd.name} - ${cmd.description}`;
|
|
840
|
+
}
|
|
841
|
+
ctx.args = args;
|
|
842
|
+
return await cmd.handler(ctx);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region extensions/qqbot/src/engine/commands/slash-commands-impl.ts
|
|
847
|
+
const registry = new SlashCommandRegistry();
|
|
848
|
+
registerBuiltinSlashCommands(registry);
|
|
849
|
+
/**
|
|
850
|
+
* Initialize command dependencies from the EngineAdapters.commands port.
|
|
851
|
+
* Called once by the bridge layer during startup.
|
|
852
|
+
*/
|
|
853
|
+
function initCommands(port) {
|
|
854
|
+
initSlashCommandDeps(port);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Return all commands that require authorization, for registration with the
|
|
858
|
+
* framework via api.registerCommand() in registerFull().
|
|
859
|
+
*/
|
|
860
|
+
function getFrameworkCommands() {
|
|
861
|
+
return registry.getFrameworkCommands();
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Try to match and execute a plugin-level slash command.
|
|
865
|
+
*
|
|
866
|
+
* @returns A reply when matched, or null when the message should continue through normal routing.
|
|
867
|
+
*/
|
|
868
|
+
async function matchSlashCommand(ctx) {
|
|
869
|
+
return registry.matchSlashCommand(ctx, { info: debugLog });
|
|
870
|
+
}
|
|
871
|
+
/** Return the plugin version for external callers. */
|
|
872
|
+
function getPluginVersion() {
|
|
873
|
+
return getPluginVersionString();
|
|
874
|
+
}
|
|
875
|
+
/** Return the framework version for external callers. */
|
|
876
|
+
function getFrameworkVersion() {
|
|
877
|
+
return getFrameworkVersionString();
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region extensions/qqbot/src/engine/utils/request-context.ts
|
|
881
|
+
/**
|
|
882
|
+
* Request-level context using AsyncLocalStorage.
|
|
883
|
+
*
|
|
884
|
+
* Provides ambient context (accountId, target openid, chat type, etc.)
|
|
885
|
+
* throughout the request lifecycle without explicit parameter threading.
|
|
886
|
+
*
|
|
887
|
+
* Gateway establishes the scope around each inbound message via
|
|
888
|
+
* `runWithRequestContext()`; any async code within that scope (including
|
|
889
|
+
* AI agent calls and tool `execute` callbacks) can retrieve the current
|
|
890
|
+
* request via `getRequestContext()` without racing with concurrent
|
|
891
|
+
* inbound messages.
|
|
892
|
+
*
|
|
893
|
+
* This is a pure Node.js module with zero framework dependencies,
|
|
894
|
+
* making it trivially portable between the built-in and standalone
|
|
895
|
+
* versions of QQBot.
|
|
896
|
+
*/
|
|
897
|
+
const store = new AsyncLocalStorage();
|
|
898
|
+
/**
|
|
899
|
+
* Execute an async function with request-scoped context.
|
|
900
|
+
*
|
|
901
|
+
* All code running within `fn` (including nested async calls) can
|
|
902
|
+
* retrieve the context via `getRequestContext()`.
|
|
903
|
+
*
|
|
904
|
+
* @param ctx - The context to attach to this request.
|
|
905
|
+
* @param fn - The async function to run within the context.
|
|
906
|
+
* @returns The return value of `fn`.
|
|
907
|
+
*/
|
|
908
|
+
function runWithRequestContext(ctx, fn) {
|
|
909
|
+
return store.run(ctx, fn);
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Retrieve the current request context.
|
|
913
|
+
*
|
|
914
|
+
* Returns `undefined` when called outside of a `runWithRequestContext`
|
|
915
|
+
* scope.
|
|
916
|
+
*/
|
|
917
|
+
function getRequestContext() {
|
|
918
|
+
return store.getStore();
|
|
919
|
+
}
|
|
920
|
+
//#endregion
|
|
921
|
+
export { getPluginVersion as a, getFrameworkVersion as i, runWithRequestContext as n, initCommands as o, getFrameworkCommands as r, matchSlashCommand as s, getRequestContext as t };
|