@gakr-gakr/qqbot 0.1.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.
Files changed (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,201 @@
1
+ import type { ApproveRuntimeGetter } from "../../adapter/commands.port.js";
2
+ import type { SlashCommandRegistry } from "../slash-commands.js";
3
+ import { getApproveRuntimeGetter } from "./state.js";
4
+
5
+ export function registerApproveCommands(registry: SlashCommandRegistry): void {
6
+ registry.register({
7
+ name: "bot-approve",
8
+ description: "管理命令执行审批配置",
9
+ requireAuth: true,
10
+ c2cOnly: true,
11
+ usage: [
12
+ `/bot-approve 查看操作指引`,
13
+ `/bot-approve on 开启审批(白名单模式,推荐)`,
14
+ `/bot-approve off 关闭审批,命令直接执行`,
15
+ `/bot-approve always 始终审批,每次执行都需审批`,
16
+ `/bot-approve reset 恢复框架默认值`,
17
+ `/bot-approve status 查看当前审批配置`,
18
+ ].join("\n"),
19
+ handler: async (ctx) => {
20
+ const arg = ctx.args.trim().toLowerCase();
21
+
22
+ let runtime: ReturnType<NonNullable<ApproveRuntimeGetter>>;
23
+ try {
24
+ const getter = getApproveRuntimeGetter();
25
+ if (!getter) {
26
+ throw new Error("runtime not available");
27
+ }
28
+ runtime = getter();
29
+ } catch {
30
+ return [
31
+ `🔐 命令执行审批配置`,
32
+ ``,
33
+ `❌ 当前环境不支持在线配置修改,请通过 CLI 手动配置:`,
34
+ ``,
35
+ `\`\`\`shell`,
36
+ `# 开启审批(白名单模式)`,
37
+ `autobot config set tools.exec.security allowlist`,
38
+ `autobot config set tools.exec.ask on-miss`,
39
+ ``,
40
+ `# 关闭审批`,
41
+ `autobot config set tools.exec.security full`,
42
+ `autobot config set tools.exec.ask off`,
43
+ `\`\`\``,
44
+ ].join("\n");
45
+ }
46
+
47
+ const configApi = runtime.config;
48
+
49
+ const loadExecConfig = () => {
50
+ const cfg = configApi.current();
51
+ const tools = ((cfg as Record<string, unknown>).tools ?? {}) as Record<string, unknown>;
52
+ const exec = (tools.exec ?? {}) as Record<string, unknown>;
53
+ const security = typeof exec.security === "string" ? exec.security : "deny";
54
+ const ask = typeof exec.ask === "string" ? exec.ask : "on-miss";
55
+ return { security, ask };
56
+ };
57
+
58
+ const writeExecConfig = async (security: string, ask: string) => {
59
+ const cfg = structuredClone(configApi.current() as Record<string, unknown>);
60
+ const tools = (cfg.tools ?? {}) as Record<string, unknown>;
61
+ const exec = (tools.exec ?? {}) as Record<string, unknown>;
62
+ exec.security = security;
63
+ exec.ask = ask;
64
+ tools.exec = exec;
65
+ cfg.tools = tools;
66
+ await configApi.replaceConfigFile({ nextConfig: cfg, afterWrite: { mode: "auto" } });
67
+ };
68
+
69
+ const formatStatus = (security: string, ask: string) => {
70
+ const secIcon = security === "full" ? "🟢" : security === "allowlist" ? "🟡" : "🔴";
71
+ const askIcon = ask === "off" ? "🟢" : ask === "always" ? "🔴" : "🟡";
72
+ return [
73
+ `🔐 当前审批配置`,
74
+ ``,
75
+ `${secIcon} 安全模式 (security): **${security}**`,
76
+ `${askIcon} 审批模式 (ask): **${ask}**`,
77
+ ``,
78
+ security === "deny"
79
+ ? `⚠️ 当前为 deny 模式,所有命令执行被拒绝`
80
+ : security === "full" && ask === "off"
81
+ ? `✅ 所有命令无需审批直接执行`
82
+ : security === "allowlist" && ask === "on-miss"
83
+ ? `🛡️ 白名单命令直接执行,其余需审批`
84
+ : ask === "always"
85
+ ? `🔒 每次命令执行都需要人工审批`
86
+ : `ℹ️ security=${security}, ask=${ask}`,
87
+ ].join("\n");
88
+ };
89
+
90
+ if (!arg) {
91
+ return [
92
+ `🔐 命令执行审批配置`,
93
+ ``,
94
+ `<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)`,
95
+ `<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批`,
96
+ `<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式`,
97
+ `<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认`,
98
+ `<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置`,
99
+ ].join("\n");
100
+ }
101
+
102
+ if (arg === "status") {
103
+ const { security, ask } = loadExecConfig();
104
+ return [
105
+ formatStatus(security, ask),
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
+ ].join("\n");
112
+ }
113
+
114
+ if (arg === "on") {
115
+ try {
116
+ await writeExecConfig("allowlist", "on-miss");
117
+ return [
118
+ `✅ 审批已开启`,
119
+ ``,
120
+ `• security = allowlist(白名单模式)`,
121
+ `• ask = on-miss(未命中白名单时需审批)`,
122
+ ``,
123
+ `已批准的命令自动加入白名单,下次直接执行。`,
124
+ ].join("\n");
125
+ } catch (err: unknown) {
126
+ return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
127
+ }
128
+ }
129
+
130
+ if (arg === "off") {
131
+ try {
132
+ await writeExecConfig("full", "off");
133
+ return [
134
+ `✅ 审批已关闭`,
135
+ ``,
136
+ `• security = full(允许所有命令)`,
137
+ `• ask = off(不需要审批)`,
138
+ ``,
139
+ `⚠️ 所有命令将直接执行,不会弹出审批确认。`,
140
+ ].join("\n");
141
+ } catch (err: unknown) {
142
+ return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
143
+ }
144
+ }
145
+
146
+ if (arg === "always" || arg === "strict") {
147
+ try {
148
+ await writeExecConfig("allowlist", "always");
149
+ return [
150
+ `✅ 已切换为严格审批模式`,
151
+ ``,
152
+ `• security = allowlist`,
153
+ `• ask = always(每次执行都需审批)`,
154
+ ``,
155
+ `每个命令都会弹出审批按钮,需手动确认。`,
156
+ ].join("\n");
157
+ } catch (err: unknown) {
158
+ return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
159
+ }
160
+ }
161
+
162
+ if (arg === "reset") {
163
+ try {
164
+ const cfg = structuredClone(configApi.current() as Record<string, unknown>);
165
+ const tools = (cfg.tools ?? {}) as Record<string, unknown>;
166
+ const exec = (tools.exec ?? {}) as Record<string, unknown>;
167
+ delete exec.security;
168
+ delete exec.ask;
169
+ if (Object.keys(exec).length === 0) {
170
+ delete tools.exec;
171
+ } else {
172
+ tools.exec = exec;
173
+ }
174
+ if (Object.keys(tools).length === 0) {
175
+ delete cfg.tools;
176
+ } else {
177
+ cfg.tools = tools;
178
+ }
179
+ await configApi.replaceConfigFile({ nextConfig: cfg, afterWrite: { mode: "auto" } });
180
+ return [
181
+ `✅ 审批配置已重置`,
182
+ ``,
183
+ `已移除 tools.exec.security 和 tools.exec.ask`,
184
+ `框架将使用默认值(security=deny, ask=on-miss)`,
185
+ ``,
186
+ `如需开启命令执行,请使用 /bot-approve on`,
187
+ ].join("\n");
188
+ } catch (err: unknown) {
189
+ return `❌ 配置更新失败: ${err instanceof Error ? err.message : String(err)}`;
190
+ }
191
+ }
192
+
193
+ return [
194
+ `❌ 未知参数: ${arg}`,
195
+ ``,
196
+ `可用选项: on | off | always | reset | status`,
197
+ `输入 /bot-approve ? 查看详细用法`,
198
+ ].join("\n");
199
+ },
200
+ });
201
+ }
@@ -0,0 +1,95 @@
1
+ import type { SlashCommandRegistry } from "../slash-commands.js";
2
+ import { getPluginVersionString, resolveRuntimeServiceVersion } from "./state.js";
3
+
4
+ const QQBOT_PLUGIN_GITHUB_URL = "https://github.com/autobot/autobot/tree/main/extensions/qqbot";
5
+ const QQBOT_UPGRADE_GUIDE_URL = "https://q.qq.com/qqbot/autobot/upgrade.html";
6
+
7
+ export function registerBasicBotCommands(registry: SlashCommandRegistry): void {
8
+ registry.register({
9
+ name: "bot-help",
10
+ description: "查看所有内置命令",
11
+ usage: [
12
+ `/bot-help`,
13
+ ``,
14
+ `查看所有可用的 QQBot 内置命令及其简要说明。`,
15
+ `在命令后追加 ? 可查看详细用法。`,
16
+ ].join("\n"),
17
+ handler: (ctx) => {
18
+ const isGroup = ctx.type === "group";
19
+ const lines = [`### QQBot 内置命令`, ``];
20
+ for (const [name, cmd] of registry.getAllCommands()) {
21
+ if (isGroup && cmd.c2cOnly) {
22
+ continue;
23
+ }
24
+ lines.push(`<qqbot-cmd-input text="/${name}" show="/${name}"/> ${cmd.description}`);
25
+ }
26
+ lines.push(``, `> 插件版本 v${getPluginVersionString()}`);
27
+ return lines.join("\n");
28
+ },
29
+ });
30
+
31
+ registry.register({
32
+ name: "bot-me",
33
+ description: "查看当前发送者的账号ID",
34
+ c2cOnly: true,
35
+ usage: [`/bot-me`, ``, `显示当前发送者的账号ID`].join("\n"),
36
+ handler: (ctx) => {
37
+ return `你的账号ID:\`${ctx.senderId}\``;
38
+ },
39
+ });
40
+
41
+ registry.register({
42
+ name: "bot-ping",
43
+ description: "测试 AutoBot 与 QQ 之间的网络延迟",
44
+ usage: [
45
+ `/bot-ping`,
46
+ ``,
47
+ `测试当前 AutoBot 宿主机与 QQ 服务器之间的网络延迟。`,
48
+ `返回网络传输耗时和插件处理耗时。`,
49
+ ].join("\n"),
50
+ handler: (ctx) => {
51
+ const now = Date.now();
52
+ const eventTime = new Date(ctx.eventTimestamp).getTime();
53
+ if (Number.isNaN(eventTime)) {
54
+ return `✅ pong!`;
55
+ }
56
+ const totalMs = now - eventTime;
57
+ const qqToPlugin = ctx.receivedAt - eventTime;
58
+ const pluginProcess = now - ctx.receivedAt;
59
+ const lines = [
60
+ `✅ pong!`,
61
+ ``,
62
+ `⏱ 延迟:${totalMs}ms`,
63
+ ` ├ 网络传输:${qqToPlugin}ms`,
64
+ ` └ 插件处理:${pluginProcess}ms`,
65
+ ];
66
+ return lines.join("\n");
67
+ },
68
+ });
69
+
70
+ registry.register({
71
+ name: "bot-version",
72
+ description: "查看 QQBot 插件版本和 AutoBot 框架版本",
73
+ c2cOnly: true,
74
+ usage: [`/bot-version`, ``, `查看当前 QQBot 插件版本和 AutoBot 框架版本。`].join("\n"),
75
+ handler: async () => {
76
+ const frameworkVersion = resolveRuntimeServiceVersion();
77
+ const ver = getPluginVersionString();
78
+ const lines = [
79
+ `🦞 AutoBot 框架版本:${frameworkVersion}`,
80
+ `🤖 QQBot 插件版本:v${ver}`,
81
+ `🌟 官方 GitHub 仓库:[点击前往](${QQBOT_PLUGIN_GITHUB_URL})`,
82
+ ];
83
+ return lines.join("\n");
84
+ },
85
+ });
86
+
87
+ registry.register({
88
+ name: "bot-upgrade",
89
+ description: "查看 QQBot 升级指引",
90
+ c2cOnly: true,
91
+ usage: [`/bot-upgrade`, ``, `查看 QQBot 升级说明。`].join("\n"),
92
+ handler: () =>
93
+ [`📘 QQBot 升级指引:`, `[点击查看升级说明](${QQBOT_UPGRADE_GUIDE_URL})`].join("\n"),
94
+ });
95
+ }
@@ -0,0 +1,187 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getQQBotMediaPath } from "../../utils/platform.js";
4
+ import type { SlashCommandRegistry } from "../slash-commands.js";
5
+
6
+ function scanDirectoryFiles(dirPath: string): { filePath: string; size: number }[] {
7
+ const files: { filePath: string; size: number }[] = [];
8
+ if (!fs.existsSync(dirPath)) {
9
+ return files;
10
+ }
11
+ const walk = (dir: string) => {
12
+ let entries: fs.Dirent[];
13
+ try {
14
+ entries = fs.readdirSync(dir, { withFileTypes: true });
15
+ } catch {
16
+ return;
17
+ }
18
+ for (const entry of entries) {
19
+ const fullPath = path.join(dir, entry.name);
20
+ if (entry.isDirectory()) {
21
+ walk(fullPath);
22
+ } else if (entry.isFile()) {
23
+ try {
24
+ const stat = fs.statSync(fullPath);
25
+ files.push({ filePath: fullPath, size: stat.size });
26
+ } catch {
27
+ // Skip inaccessible files.
28
+ }
29
+ }
30
+ }
31
+ };
32
+ walk(dirPath);
33
+ files.sort((a, b) => b.size - a.size);
34
+ return files;
35
+ }
36
+
37
+ function formatBytes(bytes: number): string {
38
+ if (bytes < 1024) {
39
+ return `${bytes} B`;
40
+ }
41
+ if (bytes < 1024 * 1024) {
42
+ return `${(bytes / 1024).toFixed(1)} KB`;
43
+ }
44
+ if (bytes < 1024 * 1024 * 1024) {
45
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
46
+ }
47
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
48
+ }
49
+
50
+ function removeEmptyDirs(dirPath: string): void {
51
+ if (!fs.existsSync(dirPath)) {
52
+ return;
53
+ }
54
+ let entries: fs.Dirent[];
55
+ try {
56
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
57
+ } catch {
58
+ return;
59
+ }
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ removeEmptyDirs(path.join(dirPath, entry.name));
63
+ }
64
+ }
65
+ try {
66
+ const remaining = fs.readdirSync(dirPath);
67
+ if (remaining.length === 0) {
68
+ fs.rmdirSync(dirPath);
69
+ }
70
+ } catch {
71
+ // Directory may be in use, skip.
72
+ }
73
+ }
74
+
75
+ const CLEAR_STORAGE_MAX_DISPLAY = 10;
76
+
77
+ /**
78
+ * Resolve the canonical QQBot downloads directory.
79
+ *
80
+ * All inbound attachments and outbound fallback downloads are stored directly
81
+ * under `~/.autobot/media/qqbot/downloads/` without appId subdivision.
82
+ * The clear-storage command therefore cleans the entire downloads root.
83
+ */
84
+ function resolveQqbotDownloadsDir(): string {
85
+ return getQQBotMediaPath("downloads");
86
+ }
87
+
88
+ export function registerClearStorageCommands(registry: SlashCommandRegistry): void {
89
+ registry.register({
90
+ name: "bot-clear-storage",
91
+ description: "清理通过 QQBot 对话产生的下载文件,释放主机磁盘空间",
92
+ requireAuth: true,
93
+ c2cOnly: true,
94
+ usage: [
95
+ `/bot-clear-storage`,
96
+ ``,
97
+ `扫描 QQBot 下载目录下的所有文件并列出明细。`,
98
+ `确认后执行删除,释放主机磁盘空间。`,
99
+ ``,
100
+ `/bot-clear-storage --force 确认执行清理`,
101
+ ``,
102
+ `⚠️ 仅在私聊中可用。`,
103
+ ].join("\n"),
104
+ handler: (ctx) => {
105
+ const isForce = ctx.args.trim() === "--force";
106
+ const targetDir = resolveQqbotDownloadsDir();
107
+ const displayDir = `~/.autobot/media/qqbot/downloads`;
108
+
109
+ if (!isForce) {
110
+ const files = scanDirectoryFiles(targetDir);
111
+
112
+ if (files.length === 0) {
113
+ return [`✅ 当前没有需要清理的文件`, ``, `目录 \`${displayDir}\` 为空或不存在。`].join(
114
+ "\n",
115
+ );
116
+ }
117
+
118
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
119
+ const lines: string[] = [
120
+ `即将清理 \`${displayDir}\` 目录下所有文件,总共 ${files.length} 个文件,占用磁盘存储空间 ${formatBytes(totalSize)}。`,
121
+ ``,
122
+ `目录文件概况:`,
123
+ ];
124
+
125
+ const displayFiles = files.slice(0, CLEAR_STORAGE_MAX_DISPLAY);
126
+ for (const f of displayFiles) {
127
+ const relativePath = path.relative(targetDir, f.filePath).replace(/\\/g, "/");
128
+ lines.push(`${relativePath} (${formatBytes(f.size)})`, ``, ``);
129
+ }
130
+ if (files.length > CLEAR_STORAGE_MAX_DISPLAY) {
131
+ lines.push(`...[合计:${files.length} 个文件(${formatBytes(totalSize)})]`, ``);
132
+ }
133
+
134
+ lines.push(
135
+ ``,
136
+ `---`,
137
+ ``,
138
+ `确认清理后,上述保存在 AutoBot 运行主机磁盘上的文件将永久删除,后续对话过程中 AI 无法再找回相关文件。`,
139
+ `‼️ 点击指令确认删除`,
140
+ `<qqbot-cmd-enter text="/bot-clear-storage --force" />`,
141
+ );
142
+
143
+ return lines.join("\n");
144
+ }
145
+
146
+ const files = scanDirectoryFiles(targetDir);
147
+
148
+ if (files.length === 0) {
149
+ return `✅ 目录已为空,无需清理`;
150
+ }
151
+
152
+ let deletedCount = 0;
153
+ let deletedSize = 0;
154
+ let failedCount = 0;
155
+
156
+ for (const f of files) {
157
+ try {
158
+ fs.unlinkSync(f.filePath);
159
+ deletedCount++;
160
+ deletedSize += f.size;
161
+ } catch {
162
+ failedCount++;
163
+ }
164
+ }
165
+
166
+ try {
167
+ removeEmptyDirs(targetDir);
168
+ } catch {
169
+ // Non-critical, silently ignore.
170
+ }
171
+
172
+ if (failedCount === 0) {
173
+ return [
174
+ `✅ 清理成功`,
175
+ ``,
176
+ `已删除 ${deletedCount} 个文件,释放 ${formatBytes(deletedSize)} 磁盘空间。`,
177
+ ].join("\n");
178
+ }
179
+
180
+ return [
181
+ `⚠️ 部分清理完成`,
182
+ ``,
183
+ `已删除 ${deletedCount} 个文件(${formatBytes(deletedSize)}),${failedCount} 个文件删除失败。`,
184
+ ].join("\n");
185
+ },
186
+ });
187
+ }
@@ -0,0 +1,20 @@
1
+ import type { SlashCommandRegistry } from "../slash-commands.js";
2
+ import { buildBotLogsResult } from "./log-helpers.js";
3
+
4
+ export function registerLogCommands(registry: SlashCommandRegistry): void {
5
+ registry.register({
6
+ name: "bot-logs",
7
+ description: "导出本地日志文件",
8
+ requireAuth: true,
9
+ c2cOnly: true,
10
+ usage: [
11
+ `/bot-logs`,
12
+ ``,
13
+ `导出最近的 AutoBot 日志文件(最多 4 个文件)。`,
14
+ `每个文件只保留最后 1000 行,并作为附件返回。`,
15
+ ].join("\n"),
16
+ handler: () => {
17
+ return buildBotLogsResult();
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,138 @@
1
+ import type { ApproveRuntimeGetter } from "../../adapter/commands.port.js";
2
+ import type { SlashCommandRegistry } from "../slash-commands.js";
3
+ import {
4
+ getApproveRuntimeGetter,
5
+ getPluginVersionString,
6
+ resolveRuntimeServiceVersion,
7
+ } from "./state.js";
8
+
9
+ function isStreamingConfigEnabled(streaming: unknown): boolean {
10
+ if (streaming === true) {
11
+ return true;
12
+ }
13
+ if (streaming === false || streaming === undefined || streaming === null) {
14
+ return false;
15
+ }
16
+ if (typeof streaming === "object") {
17
+ const o = streaming as Record<string, unknown>;
18
+ if (o.c2cStreamApi === true) {
19
+ return true;
20
+ }
21
+ if (o.mode === "off") {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ export function registerStreamingCommands(registry: SlashCommandRegistry): void {
30
+ registry.register({
31
+ name: "bot-streaming",
32
+ description: "一键开关流式消息",
33
+ requireAuth: true,
34
+ c2cOnly: true,
35
+ usage: [
36
+ `/bot-streaming on 开启流式消息`,
37
+ `/bot-streaming off 关闭流式消息`,
38
+ `/bot-streaming 查看当前流式消息状态`,
39
+ ``,
40
+ `开启后,AI 的回复会以流式形式逐步显示(打字机效果)。`,
41
+ `注意:仅 C2C(私聊)支持流式消息。`,
42
+ ].join("\n"),
43
+ handler: async (ctx) => {
44
+ const arg = ctx.args.trim().toLowerCase();
45
+ const currentOn = isStreamingConfigEnabled(ctx.accountConfig?.streaming);
46
+
47
+ if (!arg) {
48
+ return [
49
+ `📡 流式消息状态:${currentOn ? "✅ 已开启" : "❌ 已关闭"}`,
50
+ ``,
51
+ `使用 <qqbot-cmd-input text="/bot-streaming on" show="/bot-streaming on"/> 开启`,
52
+ `使用 <qqbot-cmd-input text="/bot-streaming off" show="/bot-streaming off"/> 关闭`,
53
+ ].join("\n");
54
+ }
55
+
56
+ if (arg !== "on" && arg !== "off") {
57
+ return `❌ 参数错误,请使用 on 或 off\n\n示例:/bot-streaming on`;
58
+ }
59
+
60
+ const wantOn = arg === "on";
61
+ if (wantOn === currentOn) {
62
+ return `📡 流式消息已经是${wantOn ? "开启" : "关闭"}状态,无需操作`;
63
+ }
64
+
65
+ let runtime: ReturnType<NonNullable<ApproveRuntimeGetter>>;
66
+ try {
67
+ const getter = getApproveRuntimeGetter();
68
+ if (!getter) {
69
+ throw new Error("runtime not available");
70
+ }
71
+ runtime = getter();
72
+ } catch {
73
+ const fwVer = resolveRuntimeServiceVersion();
74
+ const ver = getPluginVersionString();
75
+ return [
76
+ `❌ 当前版本不支持该指令`,
77
+ ``,
78
+ `🦞框架版本:${fwVer}`,
79
+ `🤖QQBot 插件版本:v${ver}`,
80
+ ``,
81
+ `可通过以下命令手动开启流式消息:`,
82
+ ``,
83
+ `\`\`\`shell`,
84
+ `# 1. 开启流式消息`,
85
+ `autobot config set channels.qqbot.streaming true`,
86
+ ``,
87
+ `# 2. 重启网关使配置生效`,
88
+ `autobot gateway restart`,
89
+ `\`\`\``,
90
+ ].join("\n");
91
+ }
92
+
93
+ try {
94
+ const configApi = runtime.config;
95
+ const currentCfg = structuredClone(configApi.current() as Record<string, unknown>);
96
+ const qqbot = ((currentCfg.channels ?? {}) as Record<string, unknown>).qqbot as
97
+ | Record<string, unknown>
98
+ | undefined;
99
+
100
+ if (!qqbot) {
101
+ return `❌ 配置文件中未找到 qqbot 通道配置`;
102
+ }
103
+
104
+ const accountId = ctx.accountId;
105
+ const newVal: unknown = wantOn;
106
+
107
+ if (accountId !== "default") {
108
+ const prevAccounts =
109
+ (qqbot.accounts as Record<string, Record<string, unknown>> | undefined) ?? {};
110
+ const nextAccounts = { ...prevAccounts };
111
+ const acct = { ...nextAccounts[accountId] };
112
+ acct.streaming = newVal;
113
+ nextAccounts[accountId] = acct;
114
+ qqbot.accounts = nextAccounts;
115
+ } else {
116
+ qqbot.streaming = newVal;
117
+ const accs = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
118
+ if (accs?.default && typeof accs.default === "object") {
119
+ const nextAccs = { ...accs };
120
+ const def = { ...accs.default, streaming: newVal };
121
+ nextAccs.default = def;
122
+ qqbot.accounts = nextAccs;
123
+ }
124
+ }
125
+
126
+ await configApi.replaceConfigFile({ nextConfig: currentCfg, afterWrite: { mode: "auto" } });
127
+
128
+ return [
129
+ `✅ 流式消息已${wantOn ? "开启" : "关闭"}`,
130
+ ``,
131
+ wantOn ? `AI 的回复将以流式形式逐步显示(仅私聊生效)。` : `AI 的回复将恢复为完整发送。`,
132
+ ].join("\n");
133
+ } catch (err: unknown) {
134
+ return `❌ 配置写入失败: ${err instanceof Error ? err.message : String(err)}`;
135
+ }
136
+ },
137
+ });
138
+ }
@@ -0,0 +1,31 @@
1
+ import type { ApproveRuntimeGetter, CommandsPort } from "../../adapter/commands.port.js";
2
+
3
+ let resolveVersionGetter: () => string = () => "unknown";
4
+ let approveRuntimeGetter: ApproveRuntimeGetter | null = null;
5
+ let PLUGIN_VERSION = "unknown";
6
+
7
+ /**
8
+ * Initialize command dependencies from the EngineAdapters.commands port.
9
+ * Called once by the bridge layer during startup.
10
+ */
11
+ export function initSlashCommandDeps(port: CommandsPort): void {
12
+ resolveVersionGetter = port.resolveVersion;
13
+ PLUGIN_VERSION = port.pluginVersion;
14
+ approveRuntimeGetter = port.approveRuntimeGetter ?? null;
15
+ }
16
+
17
+ export function resolveRuntimeServiceVersion(): string {
18
+ return resolveVersionGetter();
19
+ }
20
+
21
+ export function getPluginVersionString(): string {
22
+ return PLUGIN_VERSION;
23
+ }
24
+
25
+ export function getFrameworkVersionString(): string {
26
+ return resolveVersionGetter();
27
+ }
28
+
29
+ export function getApproveRuntimeGetter(): ApproveRuntimeGetter | null {
30
+ return approveRuntimeGetter;
31
+ }