@sliverp/qqbot 1.3.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.
- package/README.md +231 -0
- package/clawdbot.plugin.json +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/src/api.d.ts +194 -0
- package/dist/src/api.js +555 -0
- package/dist/src/channel.d.ts +3 -0
- package/dist/src/channel.js +146 -0
- package/dist/src/config.d.ts +25 -0
- package/dist/src/config.js +148 -0
- package/dist/src/gateway.d.ts +17 -0
- package/dist/src/gateway.js +722 -0
- package/dist/src/image-server.d.ts +62 -0
- package/dist/src/image-server.js +401 -0
- package/dist/src/known-users.d.ts +100 -0
- package/dist/src/known-users.js +264 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +190 -0
- package/dist/src/outbound.d.ts +149 -0
- package/dist/src/outbound.js +476 -0
- package/dist/src/proactive.d.ts +170 -0
- package/dist/src/proactive.js +398 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/session-store.d.ts +49 -0
- package/dist/src/session-store.js +242 -0
- package/dist/src/types.d.ts +116 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils/image-size.d.ts +51 -0
- package/dist/src/utils/image-size.js +234 -0
- package/dist/src/utils/payload.d.ts +112 -0
- package/dist/src/utils/payload.js +186 -0
- package/index.ts +27 -0
- package/moltbot.plugin.json +16 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +13 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/node_modules/ws/lib/receiver.js +706 -0
- package/node_modules/ws/lib/sender.js +602 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +554 -0
- package/node_modules/ws/lib/websocket.js +1393 -0
- package/node_modules/ws/package.json +69 -0
- package/node_modules/ws/wrapper.mjs +8 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +38 -0
- package/qqbot-1.3.0.tgz +0 -0
- package/scripts/proactive-api-server.ts +346 -0
- package/scripts/send-proactive.ts +273 -0
- package/scripts/upgrade.sh +106 -0
- package/skills/qqbot-cron/SKILL.md +490 -0
- package/skills/qqbot-media/SKILL.md +138 -0
- package/src/api.ts +752 -0
- package/src/channel.ts +303 -0
- package/src/config.ts +172 -0
- package/src/gateway.ts +1588 -0
- package/src/image-server.ts +474 -0
- package/src/known-users.ts +358 -0
- package/src/onboarding.ts +254 -0
- package/src/openclaw-plugin-sdk.d.ts +483 -0
- package/src/outbound.ts +571 -0
- package/src/proactive.ts +528 -0
- package/src/runtime.ts +14 -0
- package/src/session-store.ts +292 -0
- package/src/types.ts +123 -0
- package/src/utils/image-size.ts +266 -0
- package/src/utils/payload.ts +265 -0
- package/tsconfig.json +16 -0
- package/upgrade-and-run.sh +89 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地图床服务器
|
|
3
|
+
* 提供安全的图片存储和访问服务
|
|
4
|
+
*/
|
|
5
|
+
export interface ImageServerConfig {
|
|
6
|
+
/** 监听端口 */
|
|
7
|
+
port: number;
|
|
8
|
+
/** 图片存储目录 */
|
|
9
|
+
storageDir: string;
|
|
10
|
+
/** 外部访问的基础 URL(如 http://your-server:port),留空则自动生成 */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** 图片过期时间(秒),0 表示不过期 */
|
|
13
|
+
ttlSeconds?: number;
|
|
14
|
+
/** 允许的图片格式 */
|
|
15
|
+
allowedFormats?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 启动图床服务器
|
|
19
|
+
*/
|
|
20
|
+
export declare function startImageServer(config?: Partial<ImageServerConfig>): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* 停止图床服务器
|
|
23
|
+
*/
|
|
24
|
+
export declare function stopImageServer(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* 保存图片并返回访问 URL
|
|
27
|
+
* @param imageData 图片数据(Buffer 或 base64 字符串)
|
|
28
|
+
* @param mimeType 图片 MIME 类型
|
|
29
|
+
* @param ttlSeconds 过期时间(秒),默认使用配置值
|
|
30
|
+
* @returns 图片访问 URL
|
|
31
|
+
*/
|
|
32
|
+
export declare function saveImage(imageData: Buffer | string, mimeType?: string, ttlSeconds?: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* 从本地文件路径保存图片到图床
|
|
35
|
+
* @param filePath 本地文件路径
|
|
36
|
+
* @param ttlSeconds 过期时间(秒),默认使用配置值
|
|
37
|
+
* @returns 图片访问 URL,如果文件不存在或不是图片则返回 null
|
|
38
|
+
*/
|
|
39
|
+
export declare function saveImageFromPath(filePath: string, ttlSeconds?: number): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* 检查图床服务器是否运行中
|
|
42
|
+
*/
|
|
43
|
+
export declare function isImageServerRunning(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 确保图床服务器正在运行
|
|
46
|
+
* 如果未运行,则自动启动
|
|
47
|
+
* @param publicBaseUrl 公网访问的基础 URL(如 http://your-server:18765)
|
|
48
|
+
* @returns 基础 URL,启动失败返回 null
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureImageServer(publicBaseUrl?: string): Promise<string | null>;
|
|
51
|
+
/**
|
|
52
|
+
* 下载远程文件并保存到本地
|
|
53
|
+
* @param url 远程文件 URL
|
|
54
|
+
* @param destDir 目标目录
|
|
55
|
+
* @param originalFilename 原始文件名(可选,完整文件名包含扩展名)
|
|
56
|
+
* @returns 本地文件路径,失败返回 null
|
|
57
|
+
*/
|
|
58
|
+
export declare function downloadFile(url: string, destDir: string, originalFilename?: string): Promise<string | null>;
|
|
59
|
+
/**
|
|
60
|
+
* 获取图床服务器配置
|
|
61
|
+
*/
|
|
62
|
+
export declare function getImageServerConfig(): Required<ImageServerConfig>;
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地图床服务器
|
|
3
|
+
* 提供安全的图片存储和访问服务
|
|
4
|
+
*/
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
port: 18765,
|
|
11
|
+
storageDir: "./qqbot-images",
|
|
12
|
+
baseUrl: "",
|
|
13
|
+
ttlSeconds: 3600, // 默认 1 小时过期
|
|
14
|
+
allowedFormats: ["png", "jpg", "jpeg", "gif", "webp"],
|
|
15
|
+
};
|
|
16
|
+
let serverInstance = null;
|
|
17
|
+
let currentConfig = { ...DEFAULT_CONFIG };
|
|
18
|
+
let imageIndex = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* 生成安全的随机 ID
|
|
21
|
+
*/
|
|
22
|
+
function generateImageId() {
|
|
23
|
+
return crypto.randomBytes(16).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 验证请求路径是否安全(防止目录遍历攻击)
|
|
27
|
+
*/
|
|
28
|
+
function isPathSafe(requestPath, baseDir) {
|
|
29
|
+
const normalizedBase = path.resolve(baseDir);
|
|
30
|
+
const normalizedPath = path.resolve(baseDir, requestPath);
|
|
31
|
+
return normalizedPath.startsWith(normalizedBase + path.sep) || normalizedPath === normalizedBase;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 获取 MIME 类型
|
|
35
|
+
*/
|
|
36
|
+
function getMimeType(ext) {
|
|
37
|
+
const mimeTypes = {
|
|
38
|
+
png: "image/png",
|
|
39
|
+
jpg: "image/jpeg",
|
|
40
|
+
jpeg: "image/jpeg",
|
|
41
|
+
gif: "image/gif",
|
|
42
|
+
webp: "image/webp",
|
|
43
|
+
};
|
|
44
|
+
return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 从 MIME 类型获取扩展名
|
|
48
|
+
*/
|
|
49
|
+
function getExtFromMime(mimeType) {
|
|
50
|
+
const extMap = {
|
|
51
|
+
"image/png": "png",
|
|
52
|
+
"image/jpeg": "jpg",
|
|
53
|
+
"image/gif": "gif",
|
|
54
|
+
"image/webp": "webp",
|
|
55
|
+
"application/pdf": "pdf",
|
|
56
|
+
"application/json": "json",
|
|
57
|
+
"text/plain": "txt",
|
|
58
|
+
"text/csv": "csv",
|
|
59
|
+
"application/msword": "doc",
|
|
60
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
61
|
+
"application/vnd.ms-excel": "xls",
|
|
62
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
63
|
+
};
|
|
64
|
+
return extMap[mimeType] || null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 清理过期图片
|
|
68
|
+
*/
|
|
69
|
+
function cleanupExpiredImages() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const expiredIds = [];
|
|
72
|
+
for (const [id, image] of imageIndex) {
|
|
73
|
+
if (image.ttl > 0 && now - image.createdAt > image.ttl * 1000) {
|
|
74
|
+
expiredIds.push(id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const id of expiredIds) {
|
|
78
|
+
const image = imageIndex.get(id);
|
|
79
|
+
if (image) {
|
|
80
|
+
const filePath = path.join(currentConfig.storageDir, image.filename);
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(filePath)) {
|
|
83
|
+
fs.unlinkSync(filePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 忽略删除错误
|
|
88
|
+
}
|
|
89
|
+
imageIndex.delete(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 加载已有的图片索引
|
|
95
|
+
*/
|
|
96
|
+
function loadImageIndex() {
|
|
97
|
+
const indexPath = path.join(currentConfig.storageDir, ".index.json");
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(indexPath)) {
|
|
100
|
+
const data = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
|
|
101
|
+
imageIndex = new Map(Object.entries(data));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
imageIndex = new Map();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 保存图片索引
|
|
110
|
+
*/
|
|
111
|
+
function saveImageIndex() {
|
|
112
|
+
const indexPath = path.join(currentConfig.storageDir, ".index.json");
|
|
113
|
+
try {
|
|
114
|
+
const data = Object.fromEntries(imageIndex);
|
|
115
|
+
fs.writeFileSync(indexPath, JSON.stringify(data, null, 2));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// 忽略保存错误
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 处理 HTTP 请求
|
|
123
|
+
*/
|
|
124
|
+
function handleRequest(req, res) {
|
|
125
|
+
const url = new URL(req.url || "/", `http://localhost:${currentConfig.port}`);
|
|
126
|
+
const pathname = url.pathname;
|
|
127
|
+
// 设置 CORS 头(允许 QQ 服务器访问)
|
|
128
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
129
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
130
|
+
if (req.method === "OPTIONS") {
|
|
131
|
+
res.writeHead(204);
|
|
132
|
+
res.end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// 只允许 GET 请求访问图片
|
|
136
|
+
if (req.method !== "GET") {
|
|
137
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
138
|
+
res.end("Method Not Allowed");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// 解析图片 ID(路径格式: /images/{id}.{ext})
|
|
142
|
+
const match = pathname.match(/^\/images\/([a-f0-9]{32})\.(\w+)$/);
|
|
143
|
+
if (!match) {
|
|
144
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
145
|
+
res.end("Not Found");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const [, imageId, requestedExt] = match;
|
|
149
|
+
const image = imageIndex.get(imageId);
|
|
150
|
+
if (!image) {
|
|
151
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
152
|
+
res.end("Image Not Found");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// 检查是否过期
|
|
156
|
+
if (image.ttl > 0 && Date.now() - image.createdAt > image.ttl * 1000) {
|
|
157
|
+
res.writeHead(410, { "Content-Type": "text/plain" });
|
|
158
|
+
res.end("Image Expired");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// 安全检查:确保文件路径在存储目录内
|
|
162
|
+
const filePath = path.join(currentConfig.storageDir, image.filename);
|
|
163
|
+
if (!isPathSafe(image.filename, currentConfig.storageDir)) {
|
|
164
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
165
|
+
res.end("Forbidden");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// 读取并返回图片
|
|
169
|
+
try {
|
|
170
|
+
if (!fs.existsSync(filePath)) {
|
|
171
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
172
|
+
res.end("File Not Found");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const imageData = fs.readFileSync(filePath);
|
|
176
|
+
res.writeHead(200, {
|
|
177
|
+
"Content-Type": image.mimeType,
|
|
178
|
+
"Content-Length": imageData.length,
|
|
179
|
+
"Cache-Control": image.ttl > 0 ? `max-age=${image.ttl}` : "max-age=31536000",
|
|
180
|
+
});
|
|
181
|
+
res.end(imageData);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
185
|
+
res.end("Internal Server Error");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 启动图床服务器
|
|
190
|
+
*/
|
|
191
|
+
export function startImageServer(config) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
if (serverInstance) {
|
|
194
|
+
const baseUrl = currentConfig.baseUrl || `http://localhost:${currentConfig.port}`;
|
|
195
|
+
resolve(baseUrl);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
currentConfig = { ...DEFAULT_CONFIG, ...config };
|
|
199
|
+
// 确保存储目录存在
|
|
200
|
+
if (!fs.existsSync(currentConfig.storageDir)) {
|
|
201
|
+
fs.mkdirSync(currentConfig.storageDir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
// 加载图片索引
|
|
204
|
+
loadImageIndex();
|
|
205
|
+
// 启动定期清理
|
|
206
|
+
const cleanupInterval = setInterval(cleanupExpiredImages, 60000); // 每分钟清理一次
|
|
207
|
+
serverInstance = http.createServer(handleRequest);
|
|
208
|
+
serverInstance.on("error", (err) => {
|
|
209
|
+
clearInterval(cleanupInterval);
|
|
210
|
+
reject(err);
|
|
211
|
+
});
|
|
212
|
+
serverInstance.listen(currentConfig.port, () => {
|
|
213
|
+
const baseUrl = currentConfig.baseUrl || `http://localhost:${currentConfig.port}`;
|
|
214
|
+
resolve(baseUrl);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 停止图床服务器
|
|
220
|
+
*/
|
|
221
|
+
export function stopImageServer() {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
if (serverInstance) {
|
|
224
|
+
serverInstance.close(() => {
|
|
225
|
+
serverInstance = null;
|
|
226
|
+
saveImageIndex();
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 保存图片并返回访问 URL
|
|
237
|
+
* @param imageData 图片数据(Buffer 或 base64 字符串)
|
|
238
|
+
* @param mimeType 图片 MIME 类型
|
|
239
|
+
* @param ttlSeconds 过期时间(秒),默认使用配置值
|
|
240
|
+
* @returns 图片访问 URL
|
|
241
|
+
*/
|
|
242
|
+
export function saveImage(imageData, mimeType = "image/png", ttlSeconds) {
|
|
243
|
+
// 转换 base64 为 Buffer
|
|
244
|
+
let buffer;
|
|
245
|
+
if (typeof imageData === "string") {
|
|
246
|
+
// 处理 data URL 格式
|
|
247
|
+
const base64Match = imageData.match(/^data:([^;]+);base64,(.+)$/);
|
|
248
|
+
if (base64Match) {
|
|
249
|
+
mimeType = base64Match[1];
|
|
250
|
+
buffer = Buffer.from(base64Match[2], "base64");
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
buffer = Buffer.from(imageData, "base64");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
buffer = imageData;
|
|
258
|
+
}
|
|
259
|
+
// 生成唯一 ID 和文件名
|
|
260
|
+
const imageId = generateImageId();
|
|
261
|
+
const ext = getExtFromMime(mimeType) || "png";
|
|
262
|
+
const filename = `${imageId}.${ext}`;
|
|
263
|
+
// 确保存储目录存在
|
|
264
|
+
if (!fs.existsSync(currentConfig.storageDir)) {
|
|
265
|
+
fs.mkdirSync(currentConfig.storageDir, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
// 保存文件
|
|
268
|
+
const filePath = path.join(currentConfig.storageDir, filename);
|
|
269
|
+
fs.writeFileSync(filePath, buffer);
|
|
270
|
+
// 记录到索引
|
|
271
|
+
const image = {
|
|
272
|
+
id: imageId,
|
|
273
|
+
filename,
|
|
274
|
+
mimeType,
|
|
275
|
+
createdAt: Date.now(),
|
|
276
|
+
ttl: ttlSeconds ?? currentConfig.ttlSeconds,
|
|
277
|
+
};
|
|
278
|
+
imageIndex.set(imageId, image);
|
|
279
|
+
saveImageIndex();
|
|
280
|
+
// 返回访问 URL
|
|
281
|
+
const baseUrl = currentConfig.baseUrl || `http://localhost:${currentConfig.port}`;
|
|
282
|
+
return `${baseUrl}/images/${imageId}.${ext}`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 从本地文件路径保存图片到图床
|
|
286
|
+
* @param filePath 本地文件路径
|
|
287
|
+
* @param ttlSeconds 过期时间(秒),默认使用配置值
|
|
288
|
+
* @returns 图片访问 URL,如果文件不存在或不是图片则返回 null
|
|
289
|
+
*/
|
|
290
|
+
export function saveImageFromPath(filePath, ttlSeconds) {
|
|
291
|
+
try {
|
|
292
|
+
console.log(`[image-server] saveImageFromPath: ${filePath}`);
|
|
293
|
+
// 检查文件是否存在
|
|
294
|
+
if (!fs.existsSync(filePath)) {
|
|
295
|
+
console.log(`[image-server] File not found: ${filePath}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
// 读取文件
|
|
299
|
+
const buffer = fs.readFileSync(filePath);
|
|
300
|
+
console.log(`[image-server] File size: ${buffer.length}`);
|
|
301
|
+
// 根据扩展名获取 MIME 类型
|
|
302
|
+
const ext = path.extname(filePath).toLowerCase().replace(".", "");
|
|
303
|
+
console.log(`[image-server] Extension: "${ext}"`);
|
|
304
|
+
const mimeType = getMimeType(ext);
|
|
305
|
+
console.log(`[image-server] MIME type: ${mimeType}`);
|
|
306
|
+
// 只处理图片文件
|
|
307
|
+
if (!mimeType.startsWith("image/")) {
|
|
308
|
+
console.log(`[image-server] Not an image file`);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
// 使用 saveImage 保存
|
|
312
|
+
return saveImage(buffer, mimeType, ttlSeconds);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
console.error(`[image-server] saveImageFromPath error:`, err);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 检查图床服务器是否运行中
|
|
321
|
+
*/
|
|
322
|
+
export function isImageServerRunning() {
|
|
323
|
+
return serverInstance !== null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 确保图床服务器正在运行
|
|
327
|
+
* 如果未运行,则自动启动
|
|
328
|
+
* @param publicBaseUrl 公网访问的基础 URL(如 http://your-server:18765)
|
|
329
|
+
* @returns 基础 URL,启动失败返回 null
|
|
330
|
+
*/
|
|
331
|
+
export async function ensureImageServer(publicBaseUrl) {
|
|
332
|
+
if (isImageServerRunning()) {
|
|
333
|
+
return publicBaseUrl || currentConfig.baseUrl || `http://0.0.0.0:${currentConfig.port}`;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const config = {
|
|
337
|
+
port: DEFAULT_CONFIG.port,
|
|
338
|
+
storageDir: DEFAULT_CONFIG.storageDir,
|
|
339
|
+
// 使用用户配置的公网地址
|
|
340
|
+
baseUrl: publicBaseUrl || `http://0.0.0.0:${DEFAULT_CONFIG.port}`,
|
|
341
|
+
ttlSeconds: 3600, // 1 小时过期
|
|
342
|
+
};
|
|
343
|
+
await startImageServer(config);
|
|
344
|
+
console.log(`[image-server] Auto-started on port ${config.port}, baseUrl: ${config.baseUrl}`);
|
|
345
|
+
return config.baseUrl;
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
console.error(`[image-server] Failed to auto-start: ${err}`);
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 下载远程文件并保存到本地
|
|
354
|
+
* @param url 远程文件 URL
|
|
355
|
+
* @param destDir 目标目录
|
|
356
|
+
* @param originalFilename 原始文件名(可选,完整文件名包含扩展名)
|
|
357
|
+
* @returns 本地文件路径,失败返回 null
|
|
358
|
+
*/
|
|
359
|
+
export async function downloadFile(url, destDir, originalFilename) {
|
|
360
|
+
try {
|
|
361
|
+
// 确保目录存在
|
|
362
|
+
if (!fs.existsSync(destDir)) {
|
|
363
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
// 下载文件
|
|
366
|
+
const response = await fetch(url);
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
console.error(`[image-server] Download failed: ${response.status} ${response.statusText}`);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
372
|
+
// 确定文件名
|
|
373
|
+
let finalFilename;
|
|
374
|
+
if (originalFilename) {
|
|
375
|
+
// 使用原始文件名,但添加时间戳避免冲突
|
|
376
|
+
const ext = path.extname(originalFilename);
|
|
377
|
+
const baseName = path.basename(originalFilename, ext);
|
|
378
|
+
const timestamp = Date.now();
|
|
379
|
+
finalFilename = `${baseName}_${timestamp}${ext}`;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// 没有原始文件名,生成随机名
|
|
383
|
+
finalFilename = `${generateImageId()}.bin`;
|
|
384
|
+
}
|
|
385
|
+
const filePath = path.join(destDir, finalFilename);
|
|
386
|
+
// 保存文件
|
|
387
|
+
fs.writeFileSync(filePath, buffer);
|
|
388
|
+
console.log(`[image-server] Downloaded file: ${filePath}`);
|
|
389
|
+
return filePath;
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
console.error(`[image-server] Download error:`, err);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* 获取图床服务器配置
|
|
398
|
+
*/
|
|
399
|
+
export function getImageServerConfig() {
|
|
400
|
+
return { ...currentConfig };
|
|
401
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 已知用户存储
|
|
3
|
+
* 记录与机器人交互过的所有用户
|
|
4
|
+
* 支持主动消息和批量通知功能
|
|
5
|
+
*/
|
|
6
|
+
export interface KnownUser {
|
|
7
|
+
/** 用户 openid(唯一标识) */
|
|
8
|
+
openid: string;
|
|
9
|
+
/** 消息类型:私聊用户 / 群组 */
|
|
10
|
+
type: "c2c" | "group";
|
|
11
|
+
/** 用户昵称(如有) */
|
|
12
|
+
nickname?: string;
|
|
13
|
+
/** 群组 openid(如果是群消息) */
|
|
14
|
+
groupOpenid?: string;
|
|
15
|
+
/** 关联的机器人账户 ID */
|
|
16
|
+
accountId: string;
|
|
17
|
+
/** 首次交互时间戳 */
|
|
18
|
+
firstSeenAt: number;
|
|
19
|
+
/** 最后交互时间戳 */
|
|
20
|
+
lastSeenAt: number;
|
|
21
|
+
/** 交互次数 */
|
|
22
|
+
interactionCount: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 强制立即保存(用于进程退出前)
|
|
26
|
+
*/
|
|
27
|
+
export declare function flushKnownUsers(): void;
|
|
28
|
+
/**
|
|
29
|
+
* 记录已知用户(收到消息时调用)
|
|
30
|
+
* @param user 用户信息(部分字段)
|
|
31
|
+
*/
|
|
32
|
+
export declare function recordKnownUser(user: {
|
|
33
|
+
openid: string;
|
|
34
|
+
type: "c2c" | "group";
|
|
35
|
+
nickname?: string;
|
|
36
|
+
groupOpenid?: string;
|
|
37
|
+
accountId: string;
|
|
38
|
+
}): void;
|
|
39
|
+
/**
|
|
40
|
+
* 获取单个用户信息
|
|
41
|
+
* @param accountId 机器人账户 ID
|
|
42
|
+
* @param openid 用户 openid
|
|
43
|
+
* @param type 消息类型
|
|
44
|
+
* @param groupOpenid 群组 openid(可选)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getKnownUser(accountId: string, openid: string, type?: "c2c" | "group", groupOpenid?: string): KnownUser | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* 列出所有已知用户
|
|
49
|
+
* @param options 筛选选项
|
|
50
|
+
*/
|
|
51
|
+
export declare function listKnownUsers(options?: {
|
|
52
|
+
/** 筛选特定机器人账户的用户 */
|
|
53
|
+
accountId?: string;
|
|
54
|
+
/** 筛选消息类型 */
|
|
55
|
+
type?: "c2c" | "group";
|
|
56
|
+
/** 最近活跃时间(毫秒,如 86400000 表示最近 24 小时) */
|
|
57
|
+
activeWithin?: number;
|
|
58
|
+
/** 返回数量限制 */
|
|
59
|
+
limit?: number;
|
|
60
|
+
/** 排序方式 */
|
|
61
|
+
sortBy?: "lastSeenAt" | "firstSeenAt" | "interactionCount";
|
|
62
|
+
/** 排序方向 */
|
|
63
|
+
sortOrder?: "asc" | "desc";
|
|
64
|
+
}): KnownUser[];
|
|
65
|
+
/**
|
|
66
|
+
* 获取用户统计信息
|
|
67
|
+
* @param accountId 机器人账户 ID(可选,不传则返回所有账户的统计)
|
|
68
|
+
*/
|
|
69
|
+
export declare function getKnownUsersStats(accountId?: string): {
|
|
70
|
+
totalUsers: number;
|
|
71
|
+
c2cUsers: number;
|
|
72
|
+
groupUsers: number;
|
|
73
|
+
activeIn24h: number;
|
|
74
|
+
activeIn7d: number;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* 删除用户记录
|
|
78
|
+
* @param accountId 机器人账户 ID
|
|
79
|
+
* @param openid 用户 openid
|
|
80
|
+
* @param type 消息类型
|
|
81
|
+
* @param groupOpenid 群组 openid(可选)
|
|
82
|
+
*/
|
|
83
|
+
export declare function removeKnownUser(accountId: string, openid: string, type?: "c2c" | "group", groupOpenid?: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* 清除所有用户记录
|
|
86
|
+
* @param accountId 机器人账户 ID(可选,不传则清除所有)
|
|
87
|
+
*/
|
|
88
|
+
export declare function clearKnownUsers(accountId?: string): number;
|
|
89
|
+
/**
|
|
90
|
+
* 获取用户的所有群组(某用户在哪些群里交互过)
|
|
91
|
+
* @param accountId 机器人账户 ID
|
|
92
|
+
* @param openid 用户 openid
|
|
93
|
+
*/
|
|
94
|
+
export declare function getUserGroups(accountId: string, openid: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* 获取群组的所有成员
|
|
97
|
+
* @param accountId 机器人账户 ID
|
|
98
|
+
* @param groupOpenid 群组 openid
|
|
99
|
+
*/
|
|
100
|
+
export declare function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[];
|