test-mirror-qqbot 1.5.5
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/CHANGELOG.md +165 -0
- package/CHANGELOG.zh.md +165 -0
- package/LICENSE +22 -0
- package/README.md +470 -0
- package/README.zh.md +465 -0
- package/bin/qqbot-cli.js +243 -0
- package/clawdbot.plugin.json +16 -0
- package/docs/UPGRADE_GUIDE.md +93 -0
- package/docs/UPGRADE_GUIDE.zh.md +93 -0
- package/docs/commands.md +247 -0
- package/docs/images/07bff56ab68e03173d2af586eeb3bcee_720.jpg +0 -0
- package/docs/images/17cada70df90185d45a2d6dd36e92f2f_720.jpg +0 -0
- package/docs/images/21dce8bfc553ce23d1bd1b270e9c516c copy.jpg +0 -0
- package/docs/images/21dce8bfc553ce23d1bd1b270e9c516c.jpg +0 -0
- package/docs/images/4645f2b3a20822b7f8d6664a708529eb_720.jpg +0 -0
- package/docs/images/59d421891f813b0d3c0cbe12574b6a72_720.jpg +0 -0
- package/docs/images/85d03b8a216f267ab7b2aee248a18a41_720.jpg +0 -0
- package/docs/images/bot_say_hello.jpg +0 -0
- package/docs/images/create_robot.png +0 -0
- package/docs/images/developer_group.png +0 -0
- package/docs/images/fc7b2236896cfba3a37c94be5d59ce3e_720.jpg +0 -0
- package/docs/images/find_appid_secret.png +0 -0
- package/docs/images/hot-update.jpg +0 -0
- package/docs/images/lighthouse_head.png +0 -0
- package/docs/images/ref_msg.png +0 -0
- package/docs/images/reminder.jpg +0 -0
- package/docs/images/slash-help.jpg +0 -0
- package/docs/images/slash-logs.jpg +0 -0
- package/docs/images/slash-ping.jpg +0 -0
- package/docs/images/slash-upgrade.jpg +0 -0
- package/docs/images/slash-version.jpg +0 -0
- package/index.ts +31 -0
- package/moltbot.plugin.json +16 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +25 -0
- package/scripts/cleanup-legacy-plugins.sh +124 -0
- package/scripts/proactive-api-server.ts +369 -0
- package/scripts/send-proactive.ts +293 -0
- package/scripts/set-markdown.sh +156 -0
- package/scripts/test-sendmedia.ts +116 -0
- package/scripts/upgrade-via-alt-pkg.sh +307 -0
- package/scripts/upgrade-via-npm.ps1 +296 -0
- package/scripts/upgrade-via-npm.sh +301 -0
- package/scripts/upgrade-via-source.sh +774 -0
- package/skills/qqbot-channel/SKILL.md +263 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +56 -0
- package/skills/qqbot-remind/SKILL.md +149 -0
- package/src/admin-resolver.ts +140 -0
- package/src/api.ts +819 -0
- package/src/channel.ts +381 -0
- package/src/config.ts +187 -0
- package/src/credential-backup.ts +72 -0
- package/src/gateway.ts +1404 -0
- package/src/image-server.ts +539 -0
- package/src/inbound-attachments.ts +304 -0
- package/src/known-users.ts +353 -0
- package/src/message-queue.ts +169 -0
- package/src/onboarding.ts +274 -0
- package/src/openclaw-plugin-sdk.d.ts +522 -0
- package/src/outbound-deliver.ts +552 -0
- package/src/outbound.ts +1266 -0
- package/src/proactive.ts +530 -0
- package/src/ref-index-store.ts +357 -0
- package/src/reply-dispatcher.ts +334 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +303 -0
- package/src/slash-commands.ts +1305 -0
- package/src/startup-greeting.ts +98 -0
- package/src/stt.ts +86 -0
- package/src/tools/channel.ts +281 -0
- package/src/tools/remind.ts +296 -0
- package/src/types.ts +183 -0
- package/src/typing-keepalive.ts +59 -0
- package/src/update-checker.ts +179 -0
- package/src/user-messages.ts +7 -0
- package/src/utils/audio-convert.ts +803 -0
- package/src/utils/file-utils.ts +167 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/media-tags.ts +182 -0
- package/src/utils/payload.ts +265 -0
- package/src/utils/platform.ts +435 -0
- package/src/utils/text-parsing.ts +82 -0
- package/src/utils/upload-cache.ts +128 -0
- package/tsconfig.json +16 -0
|
@@ -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();
|