@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,69 @@
1
+ {
2
+ "name": "ws",
3
+ "version": "8.19.0",
4
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
+ "keywords": [
6
+ "HyBi",
7
+ "Push",
8
+ "RFC-6455",
9
+ "WebSocket",
10
+ "WebSockets",
11
+ "real-time"
12
+ ],
13
+ "homepage": "https://github.com/websockets/ws",
14
+ "bugs": "https://github.com/websockets/ws/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/websockets/ws.git"
18
+ },
19
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
20
+ "license": "MIT",
21
+ "main": "index.js",
22
+ "exports": {
23
+ ".": {
24
+ "browser": "./browser.js",
25
+ "import": "./wrapper.mjs",
26
+ "require": "./index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "browser": "browser.js",
31
+ "engines": {
32
+ "node": ">=10.0.0"
33
+ },
34
+ "files": [
35
+ "browser.js",
36
+ "index.js",
37
+ "lib/*.js",
38
+ "wrapper.mjs"
39
+ ],
40
+ "scripts": {
41
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
42
+ "integration": "mocha --throw-deprecation test/*.integration.js",
43
+ "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
44
+ },
45
+ "peerDependencies": {
46
+ "bufferutil": "^4.0.1",
47
+ "utf-8-validate": ">=5.0.2"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "bufferutil": {
51
+ "optional": true
52
+ },
53
+ "utf-8-validate": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "benchmark": "^2.1.4",
59
+ "bufferutil": "^4.0.1",
60
+ "eslint": "^9.0.0",
61
+ "eslint-config-prettier": "^10.0.1",
62
+ "eslint-plugin-prettier": "^5.0.0",
63
+ "globals": "^16.0.0",
64
+ "mocha": "^8.4.0",
65
+ "nyc": "^15.0.0",
66
+ "prettier": "^3.0.0",
67
+ "utf-8-validate": "^6.0.0"
68
+ }
69
+ }
@@ -0,0 +1,8 @@
1
+ import createWebSocketStream from './lib/stream.js';
2
+ import Receiver from './lib/receiver.js';
3
+ import Sender from './lib/sender.js';
4
+ import WebSocket from './lib/websocket.js';
5
+ import WebSocketServer from './lib/websocket-server.js';
6
+
7
+ export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
8
+ export default WebSocket;
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "openclaw-qqbot",
3
+ "name": "OpenClaw QQ Bot",
4
+ "description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
5
+ "channels": ["qqbot"],
6
+ "skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
7
+ "capabilities": {
8
+ "proactiveMessaging": true,
9
+ "cronJobs": true
10
+ },
11
+ "configSchema": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "properties": {}
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@ryantest/openclaw-qqbot",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "openclaw-qqbot": "./bin/qqbot-cli.js",
9
+ "qqbot": "./bin/qqbot-cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin",
14
+ "src",
15
+ "skills",
16
+ "scripts",
17
+ "index.ts",
18
+ "tsconfig.json",
19
+ "openclaw.plugin.json",
20
+ "clawdbot.plugin.json",
21
+ "moltbot.plugin.json"
22
+ ],
23
+ "clawdbot": {
24
+ "id": "openclaw-qqbot",
25
+ "extensions": [
26
+ "./index.ts"
27
+ ]
28
+ },
29
+ "moltbot": {
30
+ "id": "openclaw-qqbot",
31
+ "extensions": [
32
+ "./index.ts"
33
+ ]
34
+ },
35
+ "openclaw": {
36
+ "id": "openclaw-qqbot",
37
+ "extensions": [
38
+ "./index.ts"
39
+ ]
40
+ },
41
+ "scripts": {
42
+ "build": "tsc || true",
43
+ "dev": "tsc --watch",
44
+ "prepack": "npm install --omit=dev"
45
+ },
46
+ "dependencies": {
47
+ "mpg123-decoder": "^1.0.3",
48
+ "silk-wasm": "^3.7.1",
49
+ "ws": "^8.18.0"
50
+ },
51
+ "bundledDependencies": [
52
+ "mpg123-decoder",
53
+ "silk-wasm",
54
+ "ws"
55
+ ],
56
+ "devDependencies": {
57
+ "@types/node": "^20.0.0",
58
+ "@types/ws": "^8.5.0",
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "peerDependencies": {
62
+ "clawdbot": "*",
63
+ "moltbot": "*",
64
+ "openclaw": "*"
65
+ },
66
+ "homepage": "https://github.com/tencent-connect/openclaw-qqbot",
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/tencent-connect/openclaw-qqbot.git"
70
+ },
71
+ "bundleDependencies": [
72
+ "mpg123-decoder",
73
+ "silk-wasm",
74
+ "ws"
75
+ ]
76
+ }
@@ -0,0 +1,124 @@
1
+ #!/bin/bash
2
+ # qqbot 插件升级脚本
3
+ # 用于清理旧版本插件并重新安装
4
+ # 兼容 clawdbot 和 openclaw 两种安装
5
+
6
+ set -e
7
+
8
+ echo "=== qqbot 插件升级脚本 ==="
9
+
10
+ # 检测使用的是 clawdbot 还是 openclaw
11
+ detect_installation() {
12
+ if [ -d "$HOME/.clawdbot" ]; then
13
+ echo "clawdbot"
14
+ elif [ -d "$HOME/.openclaw" ]; then
15
+ echo "openclaw"
16
+ else
17
+ echo ""
18
+ fi
19
+ }
20
+
21
+ # 可能的扩展目录名(原仓库 qqbot + 本仓库框架推断名 openclaw-qq)
22
+ EXTENSION_DIRS=("qqbot" "openclaw-qq" "openclaw-qqbot")
23
+
24
+ # 清理指定目录的函数
25
+ cleanup_installation() {
26
+ local APP_NAME="$1"
27
+ local APP_DIR="$HOME/.$APP_NAME"
28
+ local CONFIG_FILE="$APP_DIR/$APP_NAME.json"
29
+
30
+ echo ""
31
+ echo ">>> 处理 $APP_NAME 安装..."
32
+
33
+ # 1. 删除所有可能的旧扩展目录
34
+ for dir_name in "${EXTENSION_DIRS[@]}"; do
35
+ local ext_dir="$APP_DIR/extensions/$dir_name"
36
+ if [ -d "$ext_dir" ]; then
37
+ echo "删除旧版本插件: $ext_dir"
38
+ rm -rf "$ext_dir"
39
+ fi
40
+ done
41
+
42
+ # 2. 清理配置文件中所有可能的插件 ID 相关字段
43
+ if [ -f "$CONFIG_FILE" ]; then
44
+ echo "清理配置文件中的插件字段..."
45
+
46
+ # 使用 node 处理 JSON(比 jq 更可靠处理复杂结构)
47
+ node -e "
48
+ const fs = require('fs');
49
+ const config = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
50
+ const ids = ['qqbot', 'openclaw-qq', '@sliverp/qqbot', '@tencent-connect/qqbot', '@tencent-connect/openclaw-qq', '@tencent-connect/openclaw-qqbot', 'openclaw-qqbot'];
51
+
52
+ for (const id of ids) {
53
+ // 注意: 不删除 channels.<id>,因为里面保存了用户的 appid/secret 凭证
54
+ // 凭证与插件版本无关,清理插件时不应清除凭证
55
+
56
+ // 删除 plugins.entries.<id>
57
+ if (config.plugins && config.plugins.entries && config.plugins.entries[id]) {
58
+ delete config.plugins.entries[id];
59
+ console.log(' - 已删除 plugins.entries.' + id);
60
+ }
61
+
62
+ // 删除 plugins.installs.<id>
63
+ if (config.plugins && config.plugins.installs && config.plugins.installs[id]) {
64
+ delete config.plugins.installs[id];
65
+ console.log(' - 已删除 plugins.installs.' + id);
66
+ }
67
+
68
+ // 删除 plugins.allow 中的 <id>
69
+ if (config.plugins && Array.isArray(config.plugins.allow)) {
70
+ const before = config.plugins.allow.length;
71
+ config.plugins.allow = config.plugins.allow.filter((x) => x !== id);
72
+ if (config.plugins.allow.length !== before) {
73
+ console.log(' - 已删除 plugins.allow 项: ' + id);
74
+ }
75
+ }
76
+ }
77
+
78
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(config, null, 2));
79
+ console.log('配置文件已更新');
80
+ "
81
+ else
82
+ echo "未找到配置文件: $CONFIG_FILE"
83
+ fi
84
+ }
85
+
86
+ # 检测并处理所有可能的安装
87
+ FOUND_INSTALLATION=""
88
+
89
+ # 检查 clawdbot
90
+ if [ -d "$HOME/.clawdbot" ]; then
91
+ cleanup_installation "clawdbot"
92
+ FOUND_INSTALLATION="clawdbot"
93
+ fi
94
+
95
+ # 检查 openclaw
96
+ if [ -d "$HOME/.openclaw" ]; then
97
+ cleanup_installation "openclaw"
98
+ FOUND_INSTALLATION="openclaw"
99
+ fi
100
+
101
+ # 检查 moltbot
102
+ if [ -d "$HOME/.moltbot" ]; then
103
+ cleanup_installation "moltbot"
104
+ FOUND_INSTALLATION="moltbot"
105
+ fi
106
+
107
+ # 如果都没找到
108
+ if [ -z "$FOUND_INSTALLATION" ]; then
109
+ echo "未找到 clawdbot / openclaw / moltbot 安装目录"
110
+ echo "请确认已安装其中之一"
111
+ exit 1
112
+ fi
113
+
114
+ # 使用检测到的安装类型作为命令
115
+ CMD="$FOUND_INSTALLATION"
116
+
117
+ echo ""
118
+ echo "=== 清理完成 ==="
119
+ echo ""
120
+ echo "接下来将执行以下命令重新安装插件:"
121
+ echo " cd /path/to/openclaw-qqbot"
122
+ echo " $CMD plugins install ."
123
+ echo " $CMD channels add --channel qqbot --token \"appid:appsecret\""
124
+ echo " $CMD gateway restart"
@@ -0,0 +1,369 @@
1
+ /**
2
+ * QQBot 主动消息 HTTP API 服务
3
+ *
4
+ * 提供 RESTful API 用于:
5
+ * 1. 发送主动消息
6
+ * 2. 查询已知用户
7
+ * 3. 广播消息
8
+ *
9
+ * 启动方式:
10
+ * npx ts-node scripts/proactive-api-server.ts --port 3721
11
+ *
12
+ * API 端点:
13
+ * POST /send - 发送主动消息
14
+ * GET /users - 列出已知用户
15
+ * GET /users/stats - 获取用户统计
16
+ * POST /broadcast - 广播消息
17
+ */
18
+
19
+ import * as http from "node:http";
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+ import * as url from "node:url";
23
+ import {
24
+ sendProactiveMessageDirect,
25
+ listKnownUsers,
26
+ getKnownUsersStats,
27
+ getKnownUser,
28
+ broadcastMessage,
29
+ } from "../src/proactive.js";
30
+ import type { ResolvedQQBotAccount } from "../src/types.js";
31
+
32
+ // 默认端口
33
+ const DEFAULT_PORT = 3721;
34
+
35
+ // 自动检测配置文件路径(兼容 openclaw / clawdbot / moltbot)
36
+ function detectConfigPath(): string | null {
37
+ const home = process.env.HOME || "/home/ubuntu";
38
+ for (const app of ["openclaw", "clawdbot", "moltbot"]) {
39
+ const p = path.join(home, `.${app}`, `${app}.json`);
40
+ if (fs.existsSync(p)) return p;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function normalizeAppId(raw: unknown): string {
46
+ if (raw === null || raw === undefined) return "";
47
+ return String(raw).trim();
48
+ }
49
+
50
+ // 从配置文件加载账户信息
51
+ function loadAccount(accountId = "default"): ResolvedQQBotAccount | null {
52
+ const configPath = detectConfigPath();
53
+
54
+ try {
55
+ // 优先从环境变量获取
56
+ const envAppId = process.env.QQBOT_APP_ID;
57
+ const envClientSecret = process.env.QQBOT_CLIENT_SECRET;
58
+
59
+ if (!configPath || !fs.existsSync(configPath)) {
60
+ if (envAppId && envClientSecret) {
61
+ return {
62
+ accountId,
63
+ appId: normalizeAppId(envAppId),
64
+ clientSecret: envClientSecret,
65
+ enabled: true,
66
+ secretSource: "env",
67
+ markdownSupport: true,
68
+ config: {},
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
75
+ const qqbot = config.channels?.qqbot;
76
+
77
+ if (!qqbot) {
78
+ if (envAppId && envClientSecret) {
79
+ return {
80
+ accountId,
81
+ appId: normalizeAppId(envAppId),
82
+ clientSecret: envClientSecret,
83
+ enabled: true,
84
+ secretSource: "env",
85
+ markdownSupport: true,
86
+ config: {},
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // 解析账户配置
93
+ if (accountId === "default") {
94
+ return {
95
+ accountId: "default",
96
+ appId: normalizeAppId(qqbot.appId ?? envAppId),
97
+ clientSecret: qqbot.clientSecret || envClientSecret,
98
+ enabled: qqbot.enabled ?? true,
99
+ secretSource: qqbot.clientSecret ? "config" : "env",
100
+ markdownSupport: qqbot.markdownSupport ?? true,
101
+ config: accountId === "default" ? (qqbot as Record<string, unknown>) : {},
102
+ };
103
+ }
104
+
105
+ const accountConfig = qqbot.accounts?.[accountId];
106
+ if (accountConfig) {
107
+ return {
108
+ accountId,
109
+ appId: normalizeAppId(accountConfig.appId ?? qqbot.appId ?? envAppId),
110
+ clientSecret: accountConfig.clientSecret || qqbot.clientSecret || envClientSecret,
111
+ enabled: accountConfig.enabled ?? true,
112
+ secretSource: accountConfig.clientSecret ? "config" : "env",
113
+ markdownSupport: accountConfig.markdownSupport ?? qqbot.markdownSupport ?? true,
114
+ config: accountConfig,
115
+ };
116
+ }
117
+
118
+ return null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ // 加载配置(用于 broadcastMessage)
125
+ function loadConfig(): Record<string, unknown> {
126
+ const configPath = detectConfigPath();
127
+ try {
128
+ if (configPath && fs.existsSync(configPath)) {
129
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
130
+ }
131
+ } catch {}
132
+ return {};
133
+ }
134
+
135
+ // 解析请求体
136
+ async function parseBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
137
+ return new Promise((resolve) => {
138
+ let body = "";
139
+ req.on("data", (chunk) => {
140
+ body += chunk;
141
+ });
142
+ req.on("end", () => {
143
+ try {
144
+ resolve(body ? JSON.parse(body) : {});
145
+ } catch {
146
+ resolve({});
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ // 发送 JSON 响应
153
+ function sendJson(res: http.ServerResponse, statusCode: number, data: unknown) {
154
+ res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
155
+ res.end(JSON.stringify(data, null, 2));
156
+ }
157
+
158
+ // 处理请求
159
+ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
160
+ const parsedUrl = url.parse(req.url || "", true);
161
+ const pathname = parsedUrl.pathname || "/";
162
+ const method = req.method || "GET";
163
+ const query = parsedUrl.query;
164
+
165
+ // CORS 支持
166
+ res.setHeader("Access-Control-Allow-Origin", "*");
167
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
168
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
169
+
170
+ if (method === "OPTIONS") {
171
+ res.writeHead(204);
172
+ res.end();
173
+ return;
174
+ }
175
+
176
+ console.log(`[${new Date().toISOString()}] ${method} ${pathname}`);
177
+
178
+ try {
179
+ // POST /send - 发送主动消息
180
+ if (pathname === "/send" && method === "POST") {
181
+ const body = await parseBody(req);
182
+ const { to, text, type = "c2c", accountId = "default" } = body as {
183
+ to?: string;
184
+ text?: string;
185
+ type?: "c2c" | "group";
186
+ accountId?: string;
187
+ };
188
+
189
+ if (!to || !text) {
190
+ sendJson(res, 400, { error: "Missing required fields: to, text" });
191
+ return;
192
+ }
193
+
194
+ const account = loadAccount(accountId);
195
+ if (!account) {
196
+ sendJson(res, 500, { error: "Failed to load account configuration" });
197
+ return;
198
+ }
199
+
200
+ const result = await sendProactiveMessageDirect(account, to, text, type);
201
+ sendJson(res, result.success ? 200 : 500, result);
202
+ return;
203
+ }
204
+
205
+ // GET /users - 列出已知用户
206
+ if (pathname === "/users" && method === "GET") {
207
+ const type = query.type as "c2c" | "group" | "channel" | undefined;
208
+ const accountId = query.accountId as string | undefined;
209
+ const limit = query.limit ? parseInt(query.limit as string, 10) : undefined;
210
+
211
+ const users = listKnownUsers({ type, accountId, limit });
212
+ sendJson(res, 200, { total: users.length, users });
213
+ return;
214
+ }
215
+
216
+ // GET /users/stats - 获取用户统计
217
+ if (pathname === "/users/stats" && method === "GET") {
218
+ const accountId = query.accountId as string | undefined;
219
+ const stats = getKnownUsersStats(accountId);
220
+ sendJson(res, 200, stats);
221
+ return;
222
+ }
223
+
224
+ // GET /users/:openid - 获取单个用户
225
+ if (pathname.startsWith("/users/") && method === "GET" && pathname !== "/users/stats") {
226
+ const openid = pathname.slice("/users/".length);
227
+ const type = (query.type as string) || "c2c";
228
+ const accountId = (query.accountId as string) || "default";
229
+
230
+ const user = getKnownUser(type, openid, accountId);
231
+ if (user) {
232
+ sendJson(res, 200, user);
233
+ } else {
234
+ sendJson(res, 404, { error: "User not found" });
235
+ }
236
+ return;
237
+ }
238
+
239
+ // POST /broadcast - 广播消息
240
+ if (pathname === "/broadcast" && method === "POST") {
241
+ const body = await parseBody(req);
242
+ const { text, type = "c2c", accountId, limit } = body as {
243
+ text?: string;
244
+ type?: "c2c" | "group";
245
+ accountId?: string;
246
+ limit?: number;
247
+ };
248
+
249
+ if (!text) {
250
+ sendJson(res, 400, { error: "Missing required field: text" });
251
+ return;
252
+ }
253
+
254
+ const cfg = loadConfig();
255
+ const result = await broadcastMessage(text, cfg as any, { type, accountId, limit });
256
+ sendJson(res, 200, result);
257
+ return;
258
+ }
259
+
260
+ // GET / - API 文档
261
+ if (pathname === "/" && method === "GET") {
262
+ sendJson(res, 200, {
263
+ name: "QQBot Proactive Message API",
264
+ version: "1.0.0",
265
+ endpoints: {
266
+ "POST /send": {
267
+ description: "发送主动消息",
268
+ body: {
269
+ to: "目标 openid (必需)",
270
+ text: "消息内容 (必需)",
271
+ type: "消息类型: c2c | group (默认 c2c)",
272
+ accountId: "账户 ID (默认 default)",
273
+ },
274
+ },
275
+ "GET /users": {
276
+ description: "列出已知用户",
277
+ query: {
278
+ type: "过滤类型: c2c | group | channel",
279
+ accountId: "过滤账户 ID",
280
+ limit: "限制返回数量",
281
+ },
282
+ },
283
+ "GET /users/stats": {
284
+ description: "获取用户统计",
285
+ query: {
286
+ accountId: "过滤账户 ID",
287
+ },
288
+ },
289
+ "GET /users/:openid": {
290
+ description: "获取单个用户信息",
291
+ query: {
292
+ type: "用户类型 (默认 c2c)",
293
+ accountId: "账户 ID (默认 default)",
294
+ },
295
+ },
296
+ "POST /broadcast": {
297
+ description: "广播消息给所有已知用户",
298
+ body: {
299
+ text: "消息内容 (必需)",
300
+ type: "消息类型: c2c | group (默认 c2c)",
301
+ accountId: "账户 ID",
302
+ limit: "限制发送数量",
303
+ },
304
+ },
305
+ },
306
+ notes: [
307
+ "只有曾经与机器人交互过的用户才能收到主动消息",
308
+ ],
309
+ });
310
+ return;
311
+ }
312
+
313
+ // 404
314
+ sendJson(res, 404, { error: "Not found" });
315
+ } catch (err) {
316
+ console.error(`Error handling request: ${err}`);
317
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
318
+ }
319
+ }
320
+
321
+ // 解析命令行参数获取端口
322
+ function getPort(): number {
323
+ const args = process.argv.slice(2);
324
+ for (let i = 0; i < args.length; i++) {
325
+ if (args[i] === "--port" && args[i + 1]) {
326
+ return parseInt(args[i + 1], 10) || DEFAULT_PORT;
327
+ }
328
+ }
329
+ return parseInt(process.env.PROACTIVE_API_PORT || "", 10) || DEFAULT_PORT;
330
+ }
331
+
332
+ // 启动服务器
333
+ function main() {
334
+ const port = getPort();
335
+
336
+ const server = http.createServer(handleRequest);
337
+
338
+ server.listen(port, () => {
339
+ console.log(`
340
+ ╔═══════════════════════════════════════════════════════════════╗
341
+ ║ QQBot Proactive Message API Server ║
342
+ ╠═══════════════════════════════════════════════════════════════╣
343
+ ║ Server running at: http://localhost:${port.toString().padEnd(25)}║
344
+ ║ ║
345
+ ║ Endpoints: ║
346
+ ║ GET / - API documentation ║
347
+ ║ POST /send - Send proactive message ║
348
+ ║ GET /users - List known users ║
349
+ ║ GET /users/stats - Get user statistics ║
350
+ ║ POST /broadcast - Broadcast message ║
351
+ ║ ║
352
+ ║ Example: ║
353
+ ║ curl -X POST http://localhost:${port}/send \\ ║
354
+ ║ -H "Content-Type: application/json" \\ ║
355
+ ║ -d '{"to":"openid","text":"Hello!"}' ║
356
+ ╚═══════════════════════════════════════════════════════════════╝
357
+ `);
358
+ });
359
+
360
+ // 优雅关闭
361
+ process.on("SIGINT", () => {
362
+ console.log("\nShutting down...");
363
+ server.close(() => {
364
+ process.exit(0);
365
+ });
366
+ });
367
+ }
368
+
369
+ main();