@ryantest/openclaw-qqbot 0.0.1

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 (197) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +483 -0
  3. package/README.zh.md +478 -0
  4. package/bin/qqbot-cli.js +243 -0
  5. package/clawdbot.plugin.json +16 -0
  6. package/dist/index.d.ts +17 -0
  7. package/dist/index.js +26 -0
  8. package/dist/src/admin-resolver.d.ts +27 -0
  9. package/dist/src/admin-resolver.js +122 -0
  10. package/dist/src/api.d.ts +156 -0
  11. package/dist/src/api.js +599 -0
  12. package/dist/src/channel.d.ts +11 -0
  13. package/dist/src/channel.js +354 -0
  14. package/dist/src/config.d.ts +25 -0
  15. package/dist/src/config.js +161 -0
  16. package/dist/src/credential-backup.d.ts +31 -0
  17. package/dist/src/credential-backup.js +66 -0
  18. package/dist/src/gateway.d.ts +18 -0
  19. package/dist/src/gateway.js +1265 -0
  20. package/dist/src/image-server.d.ts +68 -0
  21. package/dist/src/image-server.js +462 -0
  22. package/dist/src/inbound-attachments.d.ts +58 -0
  23. package/dist/src/inbound-attachments.js +234 -0
  24. package/dist/src/known-users.d.ts +100 -0
  25. package/dist/src/known-users.js +263 -0
  26. package/dist/src/message-queue.d.ts +50 -0
  27. package/dist/src/message-queue.js +115 -0
  28. package/dist/src/onboarding.d.ts +10 -0
  29. package/dist/src/onboarding.js +203 -0
  30. package/dist/src/outbound-deliver.d.ts +48 -0
  31. package/dist/src/outbound-deliver.js +462 -0
  32. package/dist/src/outbound.d.ts +203 -0
  33. package/dist/src/outbound.js +1102 -0
  34. package/dist/src/proactive.d.ts +170 -0
  35. package/dist/src/proactive.js +399 -0
  36. package/dist/src/ref-index-store.d.ts +70 -0
  37. package/dist/src/ref-index-store.js +273 -0
  38. package/dist/src/reply-dispatcher.d.ts +35 -0
  39. package/dist/src/reply-dispatcher.js +311 -0
  40. package/dist/src/runtime.d.ts +3 -0
  41. package/dist/src/runtime.js +10 -0
  42. package/dist/src/session-store.d.ts +52 -0
  43. package/dist/src/session-store.js +254 -0
  44. package/dist/src/slash-commands.d.ts +71 -0
  45. package/dist/src/slash-commands.js +1179 -0
  46. package/dist/src/startup-greeting.d.ts +30 -0
  47. package/dist/src/startup-greeting.js +78 -0
  48. package/dist/src/stt.d.ts +21 -0
  49. package/dist/src/stt.js +70 -0
  50. package/dist/src/tools/channel.d.ts +16 -0
  51. package/dist/src/tools/channel.js +234 -0
  52. package/dist/src/tools/remind.d.ts +2 -0
  53. package/dist/src/tools/remind.js +247 -0
  54. package/dist/src/types.d.ts +175 -0
  55. package/dist/src/types.js +1 -0
  56. package/dist/src/typing-keepalive.d.ts +27 -0
  57. package/dist/src/typing-keepalive.js +64 -0
  58. package/dist/src/update-checker.d.ts +34 -0
  59. package/dist/src/update-checker.js +166 -0
  60. package/dist/src/user-messages.d.ts +8 -0
  61. package/dist/src/user-messages.js +8 -0
  62. package/dist/src/utils/audio-convert.d.ts +89 -0
  63. package/dist/src/utils/audio-convert.js +704 -0
  64. package/dist/src/utils/file-utils.d.ts +55 -0
  65. package/dist/src/utils/file-utils.js +150 -0
  66. package/dist/src/utils/image-size.d.ts +51 -0
  67. package/dist/src/utils/image-size.js +234 -0
  68. package/dist/src/utils/media-tags.d.ts +14 -0
  69. package/dist/src/utils/media-tags.js +164 -0
  70. package/dist/src/utils/payload.d.ts +112 -0
  71. package/dist/src/utils/payload.js +186 -0
  72. package/dist/src/utils/platform.d.ts +137 -0
  73. package/dist/src/utils/platform.js +390 -0
  74. package/dist/src/utils/text-parsing.d.ts +32 -0
  75. package/dist/src/utils/text-parsing.js +80 -0
  76. package/dist/src/utils/upload-cache.d.ts +34 -0
  77. package/dist/src/utils/upload-cache.js +93 -0
  78. package/index.ts +31 -0
  79. package/moltbot.plugin.json +16 -0
  80. package/node_modules/@eshaz/web-worker/LICENSE +201 -0
  81. package/node_modules/@eshaz/web-worker/README.md +134 -0
  82. package/node_modules/@eshaz/web-worker/browser.js +17 -0
  83. package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
  84. package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
  85. package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
  86. package/node_modules/@eshaz/web-worker/node.js +223 -0
  87. package/node_modules/@eshaz/web-worker/package.json +54 -0
  88. package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
  89. package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
  90. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
  91. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
  92. package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
  93. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
  94. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
  95. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
  96. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
  97. package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
  98. package/node_modules/mpg123-decoder/README.md +265 -0
  99. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
  100. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
  101. package/node_modules/mpg123-decoder/index.js +8 -0
  102. package/node_modules/mpg123-decoder/package.json +58 -0
  103. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
  104. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
  105. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
  106. package/node_modules/mpg123-decoder/types.d.ts +30 -0
  107. package/node_modules/silk-wasm/LICENSE +21 -0
  108. package/node_modules/silk-wasm/README.md +85 -0
  109. package/node_modules/silk-wasm/lib/index.cjs +16 -0
  110. package/node_modules/silk-wasm/lib/index.d.ts +70 -0
  111. package/node_modules/silk-wasm/lib/index.mjs +16 -0
  112. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  113. package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
  114. package/node_modules/silk-wasm/package.json +39 -0
  115. package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
  116. package/node_modules/simple-yenc/.prettierignore +1 -0
  117. package/node_modules/simple-yenc/LICENSE +7 -0
  118. package/node_modules/simple-yenc/README.md +163 -0
  119. package/node_modules/simple-yenc/dist/esm.js +1 -0
  120. package/node_modules/simple-yenc/dist/index.js +1 -0
  121. package/node_modules/simple-yenc/package.json +50 -0
  122. package/node_modules/simple-yenc/rollup.config.js +27 -0
  123. package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
  124. package/node_modules/ws/LICENSE +20 -0
  125. package/node_modules/ws/README.md +548 -0
  126. package/node_modules/ws/browser.js +8 -0
  127. package/node_modules/ws/index.js +13 -0
  128. package/node_modules/ws/lib/buffer-util.js +131 -0
  129. package/node_modules/ws/lib/constants.js +19 -0
  130. package/node_modules/ws/lib/event-target.js +292 -0
  131. package/node_modules/ws/lib/extension.js +203 -0
  132. package/node_modules/ws/lib/limiter.js +55 -0
  133. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  134. package/node_modules/ws/lib/receiver.js +706 -0
  135. package/node_modules/ws/lib/sender.js +602 -0
  136. package/node_modules/ws/lib/stream.js +161 -0
  137. package/node_modules/ws/lib/subprotocol.js +62 -0
  138. package/node_modules/ws/lib/validation.js +152 -0
  139. package/node_modules/ws/lib/websocket-server.js +554 -0
  140. package/node_modules/ws/lib/websocket.js +1393 -0
  141. package/node_modules/ws/package.json +69 -0
  142. package/node_modules/ws/wrapper.mjs +8 -0
  143. package/openclaw.plugin.json +16 -0
  144. package/package.json +76 -0
  145. package/scripts/cleanup-legacy-plugins.sh +124 -0
  146. package/scripts/proactive-api-server.ts +369 -0
  147. package/scripts/send-proactive.ts +293 -0
  148. package/scripts/set-markdown.sh +156 -0
  149. package/scripts/test-sendmedia.ts +116 -0
  150. package/scripts/upgrade-via-alt-pkg.sh +307 -0
  151. package/scripts/upgrade-via-npm.ps1 +296 -0
  152. package/scripts/upgrade-via-npm.sh +301 -0
  153. package/scripts/upgrade-via-source.sh +774 -0
  154. package/skills/qqbot-channel/SKILL.md +263 -0
  155. package/skills/qqbot-channel/references/api_references.md +521 -0
  156. package/skills/qqbot-media/SKILL.md +56 -0
  157. package/skills/qqbot-remind/SKILL.md +149 -0
  158. package/src/admin-resolver.ts +140 -0
  159. package/src/api.ts +819 -0
  160. package/src/bot-logs-2026-03-21T11-21-47(2).txt +46 -0
  161. package/src/channel.ts +381 -0
  162. package/src/config.ts +187 -0
  163. package/src/credential-backup.ts +72 -0
  164. package/src/gateway.log +43 -0
  165. package/src/gateway.ts +1404 -0
  166. package/src/image-server.ts +539 -0
  167. package/src/inbound-attachments.ts +304 -0
  168. package/src/known-users.ts +353 -0
  169. package/src/message-queue.ts +169 -0
  170. package/src/onboarding.ts +274 -0
  171. package/src/openclaw-2026-03-21.log +3729 -0
  172. package/src/openclaw-plugin-sdk.d.ts +522 -0
  173. package/src/outbound-deliver.ts +552 -0
  174. package/src/outbound.ts +1266 -0
  175. package/src/proactive.ts +530 -0
  176. package/src/ref-index-store.ts +357 -0
  177. package/src/reply-dispatcher.ts +334 -0
  178. package/src/runtime.ts +14 -0
  179. package/src/session-store.ts +303 -0
  180. package/src/slash-commands.ts +1305 -0
  181. package/src/startup-greeting.ts +98 -0
  182. package/src/stt.ts +86 -0
  183. package/src/tools/channel.ts +281 -0
  184. package/src/tools/remind.ts +296 -0
  185. package/src/types.ts +183 -0
  186. package/src/typing-keepalive.ts +59 -0
  187. package/src/update-checker.ts +179 -0
  188. package/src/user-messages.ts +7 -0
  189. package/src/utils/audio-convert.ts +803 -0
  190. package/src/utils/file-utils.ts +167 -0
  191. package/src/utils/image-size.ts +266 -0
  192. package/src/utils/media-tags.ts +182 -0
  193. package/src/utils/payload.ts +265 -0
  194. package/src/utils/platform.ts +435 -0
  195. package/src/utils/text-parsing.ts +82 -0
  196. package/src/utils/upload-cache.ts +128 -0
  197. package/tsconfig.json +16 -0
@@ -0,0 +1,169 @@
1
+ import type { QueueSnapshot } from "./slash-commands.js";
2
+
3
+ // 消息队列配置
4
+ const MESSAGE_QUEUE_SIZE = 1000;
5
+ const PER_USER_QUEUE_SIZE = 20;
6
+ const MAX_CONCURRENT_USERS = 10;
7
+
8
+ /**
9
+ * 消息队列项类型(用于异步处理消息,防止阻塞心跳)
10
+ */
11
+ export interface QueuedMessage {
12
+ type: "c2c" | "guild" | "dm" | "group";
13
+ senderId: string;
14
+ senderName?: string;
15
+ content: string;
16
+ messageId: string;
17
+ timestamp: string;
18
+ channelId?: string;
19
+ guildId?: string;
20
+ groupOpenid?: string;
21
+ attachments?: Array<{ content_type: string; url: string; filename?: string; voice_wav_url?: string; asr_refer_text?: string }>;
22
+ /** 被引用消息的 refIdx(用户引用了哪条历史消息) */
23
+ refMsgIdx?: string;
24
+ /** 当前消息自身的 refIdx(供将来被引用) */
25
+ msgIdx?: string;
26
+ }
27
+
28
+ export interface MessageQueueContext {
29
+ accountId: string;
30
+ log?: {
31
+ info: (msg: string) => void;
32
+ error: (msg: string) => void;
33
+ debug?: (msg: string) => void;
34
+ };
35
+ /** 外部提供的 abort 状态检查 */
36
+ isAborted: () => boolean;
37
+ }
38
+
39
+ export interface MessageQueue {
40
+ enqueue: (msg: QueuedMessage) => void;
41
+ startProcessor: (handleMessageFn: (msg: QueuedMessage) => Promise<void>) => void;
42
+ getSnapshot: (senderPeerId: string) => QueueSnapshot;
43
+ getMessagePeerId: (msg: QueuedMessage) => string;
44
+ /** 清空指定用户的排队消息,返回被丢弃的消息数 */
45
+ clearUserQueue: (peerId: string) => number;
46
+ /** 立即执行一条消息(绕过队列),用于紧急命令 */
47
+ executeImmediate: (msg: QueuedMessage) => void;
48
+ }
49
+
50
+ /**
51
+ * 创建按用户并发的消息队列(同用户串行,跨用户并行)
52
+ */
53
+ export function createMessageQueue(ctx: MessageQueueContext): MessageQueue {
54
+ const { accountId, log } = ctx;
55
+
56
+ const userQueues = new Map<string, QueuedMessage[]>();
57
+ const activeUsers = new Set<string>();
58
+ let messagesProcessed = 0;
59
+ let handleMessageFnRef: ((msg: QueuedMessage) => Promise<void>) | null = null;
60
+ let totalEnqueued = 0;
61
+
62
+ const getMessagePeerId = (msg: QueuedMessage): string => {
63
+ if (msg.type === "guild") return `guild:${msg.channelId ?? "unknown"}`;
64
+ if (msg.type === "group") return `group:${msg.groupOpenid ?? "unknown"}`;
65
+ return `dm:${msg.senderId}`;
66
+ };
67
+
68
+ const drainUserQueue = async (peerId: string): Promise<void> => {
69
+ if (activeUsers.has(peerId)) return;
70
+ if (activeUsers.size >= MAX_CONCURRENT_USERS) {
71
+ log?.info(`[qqbot:${accountId}] Max concurrent users (${MAX_CONCURRENT_USERS}) reached, ${peerId} will wait`);
72
+ return;
73
+ }
74
+
75
+ const queue = userQueues.get(peerId);
76
+ if (!queue || queue.length === 0) {
77
+ userQueues.delete(peerId);
78
+ return;
79
+ }
80
+
81
+ activeUsers.add(peerId);
82
+
83
+ try {
84
+ while (queue.length > 0 && !ctx.isAborted()) {
85
+ const msg = queue.shift()!;
86
+ totalEnqueued = Math.max(0, totalEnqueued - 1);
87
+ try {
88
+ if (handleMessageFnRef) {
89
+ await handleMessageFnRef(msg);
90
+ messagesProcessed++;
91
+ }
92
+ } catch (err) {
93
+ log?.error(`[qqbot:${accountId}] Message processor error for ${peerId}: ${err}`);
94
+ }
95
+ }
96
+ } finally {
97
+ activeUsers.delete(peerId);
98
+ userQueues.delete(peerId);
99
+ for (const [waitingPeerId, waitingQueue] of userQueues) {
100
+ if (activeUsers.size >= MAX_CONCURRENT_USERS) break;
101
+ if (waitingQueue.length > 0 && !activeUsers.has(waitingPeerId)) {
102
+ drainUserQueue(waitingPeerId);
103
+ }
104
+ }
105
+ }
106
+ };
107
+
108
+ const enqueue = (msg: QueuedMessage): void => {
109
+ const peerId = getMessagePeerId(msg);
110
+ let queue = userQueues.get(peerId);
111
+ if (!queue) {
112
+ queue = [];
113
+ userQueues.set(peerId, queue);
114
+ }
115
+
116
+ if (queue.length >= PER_USER_QUEUE_SIZE) {
117
+ const dropped = queue.shift();
118
+ log?.error(`[qqbot:${accountId}] Per-user queue full for ${peerId}, dropping oldest message ${dropped?.messageId}`);
119
+ }
120
+
121
+ totalEnqueued++;
122
+ if (totalEnqueued > MESSAGE_QUEUE_SIZE) {
123
+ log?.error(`[qqbot:${accountId}] Global queue limit reached (${totalEnqueued}), message from ${peerId} may be delayed`);
124
+ }
125
+
126
+ queue.push(msg);
127
+ log?.debug?.(`[qqbot:${accountId}] Message enqueued for ${peerId}, user queue: ${queue.length}, active users: ${activeUsers.size}`);
128
+
129
+ drainUserQueue(peerId);
130
+ };
131
+
132
+ const startProcessor = (handleMessageFn: (msg: QueuedMessage) => Promise<void>): void => {
133
+ handleMessageFnRef = handleMessageFn;
134
+ log?.info(`[qqbot:${accountId}] Message processor started (per-user concurrency, max ${MAX_CONCURRENT_USERS} users)`);
135
+ };
136
+
137
+ const getSnapshot = (senderPeerId: string): QueueSnapshot => {
138
+ let totalPending = 0;
139
+ for (const [, q] of userQueues) {
140
+ totalPending += q.length;
141
+ }
142
+ const senderQueue = userQueues.get(senderPeerId);
143
+ return {
144
+ totalPending,
145
+ activeUsers: activeUsers.size,
146
+ maxConcurrentUsers: MAX_CONCURRENT_USERS,
147
+ senderPending: senderQueue ? senderQueue.length : 0,
148
+ };
149
+ };
150
+
151
+ const clearUserQueue = (peerId: string): number => {
152
+ const queue = userQueues.get(peerId);
153
+ if (!queue || queue.length === 0) return 0;
154
+ const droppedCount = queue.length;
155
+ queue.length = 0;
156
+ totalEnqueued = Math.max(0, totalEnqueued - droppedCount);
157
+ return droppedCount;
158
+ };
159
+
160
+ const executeImmediate = (msg: QueuedMessage): void => {
161
+ if (handleMessageFnRef) {
162
+ handleMessageFnRef(msg).catch(err => {
163
+ log?.error(`[qqbot:${accountId}] Immediate execution error: ${err}`);
164
+ });
165
+ }
166
+ };
167
+
168
+ return { enqueue, startProcessor, getSnapshot, getMessagePeerId, clearUserQueue, executeImmediate };
169
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * QQBot CLI Onboarding Adapter
3
+ *
4
+ * 提供 openclaw onboard 命令的交互式配置支持
5
+ */
6
+ import type {
7
+ ChannelOnboardingAdapter,
8
+ ChannelOnboardingStatus,
9
+ ChannelOnboardingStatusContext,
10
+ ChannelOnboardingConfigureContext,
11
+ ChannelOnboardingResult,
12
+ OpenClawConfig,
13
+ } from "openclaw/plugin-sdk";
14
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount } from "./config.js";
15
+
16
+ // 内部类型(用于类型安全)
17
+ interface QQBotChannelConfig {
18
+ enabled?: boolean;
19
+ appId?: string;
20
+ clientSecret?: string;
21
+ clientSecretFile?: string;
22
+ name?: string;
23
+ imageServerBaseUrl?: string;
24
+ markdownSupport?: boolean;
25
+ allowFrom?: string[];
26
+ accounts?: Record<string, {
27
+ enabled?: boolean;
28
+ appId?: string;
29
+ clientSecret?: string;
30
+ clientSecretFile?: string;
31
+ name?: string;
32
+ imageServerBaseUrl?: string;
33
+ markdownSupport?: boolean;
34
+ allowFrom?: string[];
35
+ }>;
36
+ }
37
+
38
+ // Prompter 类型定义
39
+ interface Prompter {
40
+ note: (message: string, title?: string) => Promise<void>;
41
+ confirm: (opts: { message: string; initialValue?: boolean }) => Promise<boolean>;
42
+ text: (opts: { message: string; placeholder?: string; initialValue?: string; validate?: (value: string) => string | undefined }) => Promise<string>;
43
+ select: <T>(opts: { message: string; options: Array<{ value: T; label: string }>; initialValue?: T }) => Promise<T>;
44
+ }
45
+
46
+ /**
47
+ * 解析默认账户 ID
48
+ */
49
+ function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
50
+ const ids = listQQBotAccountIds(cfg);
51
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
52
+ }
53
+
54
+ /**
55
+ * QQBot Onboarding Adapter
56
+ */
57
+ export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = {
58
+ channel: "qqbot" as any,
59
+
60
+ getStatus: async (ctx: ChannelOnboardingStatusContext): Promise<ChannelOnboardingStatus> => {
61
+ const cfg = ctx.cfg as OpenClawConfig;
62
+ const configured = listQQBotAccountIds(cfg).some((accountId) => {
63
+ const account = resolveQQBotAccount(cfg, accountId);
64
+ return Boolean(account.appId && account.clientSecret);
65
+ });
66
+
67
+ return {
68
+ channel: "qqbot" as any,
69
+ configured,
70
+ statusLines: [`QQ Bot: ${configured ? "已配置" : "需要 AppID 和 ClientSecret"}`],
71
+ selectionHint: configured ? "已配置" : "支持 QQ 群聊和私聊(流式消息)",
72
+ quickstartScore: configured ? 1 : 20,
73
+ };
74
+ },
75
+
76
+ configure: async (ctx: ChannelOnboardingConfigureContext): Promise<ChannelOnboardingResult> => {
77
+ const cfg = ctx.cfg as OpenClawConfig;
78
+ const prompter = ctx.prompter as Prompter;
79
+ const accountOverrides = ctx.accountOverrides as Record<string, string> | undefined;
80
+ const shouldPromptAccountIds = ctx.shouldPromptAccountIds;
81
+
82
+ const qqbotOverride = accountOverrides?.qqbot?.trim();
83
+ const defaultAccountId = resolveDefaultQQBotAccountId(cfg);
84
+ let accountId = qqbotOverride ?? defaultAccountId;
85
+
86
+ // 是否需要提示选择账户
87
+ if (shouldPromptAccountIds && !qqbotOverride) {
88
+ const existingIds = listQQBotAccountIds(cfg);
89
+ if (existingIds.length > 1) {
90
+ accountId = await prompter.select({
91
+ message: "选择 QQBot 账户",
92
+ options: existingIds.map((id) => ({
93
+ value: id,
94
+ label: id === DEFAULT_ACCOUNT_ID ? "默认账户" : id,
95
+ })),
96
+ initialValue: accountId,
97
+ });
98
+ }
99
+ }
100
+
101
+ let next: OpenClawConfig = cfg;
102
+ const resolvedAccount = resolveQQBotAccount(next, accountId);
103
+ const accountConfigured = Boolean(resolvedAccount.appId && resolvedAccount.clientSecret);
104
+ const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
105
+ const envAppId = typeof process !== "undefined" ? process.env?.QQBOT_APP_ID?.trim() : undefined;
106
+ const envSecret = typeof process !== "undefined" ? process.env?.QQBOT_CLIENT_SECRET?.trim() : undefined;
107
+ const canUseEnv = allowEnv && Boolean(envAppId && envSecret);
108
+ const hasConfigCredentials = Boolean(resolvedAccount.config.appId && resolvedAccount.config.clientSecret);
109
+
110
+ let appId: string | null = null;
111
+ let clientSecret: string | null = null;
112
+
113
+ // 显示帮助
114
+ if (!accountConfigured) {
115
+ await prompter.note(
116
+ [
117
+ "1) 打开 QQ 开放平台: https://q.qq.com/",
118
+ "2) 创建机器人应用,获取 AppID 和 ClientSecret",
119
+ "3) 在「开发设置」中添加沙箱成员(测试阶段)",
120
+ "4) 你也可以设置环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET",
121
+ "",
122
+ "文档: https://bot.q.qq.com/wiki/",
123
+ "",
124
+ "此版本支持流式消息发送!",
125
+ ].join("\n"),
126
+ "QQ Bot 配置",
127
+ );
128
+ }
129
+
130
+ // 检测环境变量
131
+ if (canUseEnv && !hasConfigCredentials) {
132
+ const keepEnv = await prompter.confirm({
133
+ message: "检测到环境变量 QQBOT_APP_ID 和 QQBOT_CLIENT_SECRET,是否使用?",
134
+ initialValue: true,
135
+ });
136
+ if (keepEnv) {
137
+ next = {
138
+ ...next,
139
+ channels: {
140
+ ...next.channels,
141
+ qqbot: {
142
+ ...(next.channels?.qqbot as Record<string, unknown> || {}),
143
+ enabled: true,
144
+ allowFrom: resolvedAccount.config?.allowFrom ?? ["*"],
145
+ },
146
+ },
147
+ };
148
+ } else {
149
+ // 手动输入
150
+ appId = String(
151
+ await prompter.text({
152
+ message: "请输入 QQ Bot AppID",
153
+ placeholder: "例如: 102146862",
154
+ initialValue: resolvedAccount.appId || undefined,
155
+ validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
156
+ }),
157
+ ).trim();
158
+ clientSecret = String(
159
+ await prompter.text({
160
+ message: "请输入 QQ Bot ClientSecret",
161
+ placeholder: "你的 ClientSecret",
162
+ validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
163
+ }),
164
+ ).trim();
165
+ }
166
+ } else if (hasConfigCredentials) {
167
+ // 已有配置
168
+ const keep = await prompter.confirm({
169
+ message: "QQ Bot 已配置,是否保留当前配置?",
170
+ initialValue: true,
171
+ });
172
+ if (!keep) {
173
+ appId = String(
174
+ await prompter.text({
175
+ message: "请输入 QQ Bot AppID",
176
+ placeholder: "例如: 102146862",
177
+ initialValue: resolvedAccount.appId || undefined,
178
+ validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
179
+ }),
180
+ ).trim();
181
+ clientSecret = String(
182
+ await prompter.text({
183
+ message: "请输入 QQ Bot ClientSecret",
184
+ placeholder: "你的 ClientSecret",
185
+ validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
186
+ }),
187
+ ).trim();
188
+ }
189
+ } else {
190
+ // 没有配置,需要输入
191
+ appId = String(
192
+ await prompter.text({
193
+ message: "请输入 QQ Bot AppID",
194
+ placeholder: "例如: 102146862",
195
+ initialValue: resolvedAccount.appId || undefined,
196
+ validate: (value: string) => (value?.trim() ? undefined : "AppID 不能为空"),
197
+ }),
198
+ ).trim();
199
+ clientSecret = String(
200
+ await prompter.text({
201
+ message: "请输入 QQ Bot ClientSecret",
202
+ placeholder: "你的 ClientSecret",
203
+ validate: (value: string) => (value?.trim() ? undefined : "ClientSecret 不能为空"),
204
+ }),
205
+ ).trim();
206
+ }
207
+
208
+ // 默认允许所有人执行命令(用户无感知)
209
+ const allowFrom: string[] = resolvedAccount.config?.allowFrom ?? ["*"];
210
+
211
+ // 应用配置(markdownSupport 默认开启,如需关闭可用 set-markdown.sh)
212
+ if (appId && clientSecret) {
213
+ const existingQQBot = (next.channels?.qqbot as Record<string, unknown>) || {};
214
+ // 保留已有的 markdownSupport 设置,新装默认 true
215
+ const markdownSupport = existingQQBot.markdownSupport ?? true;
216
+
217
+ if (accountId === DEFAULT_ACCOUNT_ID) {
218
+ next = {
219
+ ...next,
220
+ channels: {
221
+ ...next.channels,
222
+ qqbot: {
223
+ ...existingQQBot,
224
+ enabled: true,
225
+ appId,
226
+ clientSecret,
227
+ markdownSupport,
228
+ allowFrom,
229
+ },
230
+ },
231
+ };
232
+ } else {
233
+ const existingAccounts = ((next.channels?.qqbot as QQBotChannelConfig)?.accounts || {});
234
+ const existingAccount = existingAccounts[accountId] || {};
235
+ const acctMarkdown = existingAccount.markdownSupport ?? true;
236
+
237
+ next = {
238
+ ...next,
239
+ channels: {
240
+ ...next.channels,
241
+ qqbot: {
242
+ ...existingQQBot,
243
+ enabled: true,
244
+ accounts: {
245
+ ...existingAccounts,
246
+ [accountId]: {
247
+ ...existingAccount,
248
+ enabled: true,
249
+ appId,
250
+ clientSecret,
251
+ markdownSupport: acctMarkdown,
252
+ allowFrom,
253
+ },
254
+ },
255
+ },
256
+ },
257
+ };
258
+ }
259
+ }
260
+
261
+ return { success: true, cfg: next as any, accountId };
262
+ },
263
+
264
+ disable: (cfg: unknown) => {
265
+ const config = cfg as OpenClawConfig;
266
+ return {
267
+ ...config,
268
+ channels: {
269
+ ...config.channels,
270
+ qqbot: { ...(config.channels?.qqbot as Record<string, unknown> || {}), enabled: false },
271
+ },
272
+ } as any;
273
+ },
274
+ };