@cxyhhhhh/openclaw-qqbot 2.0.0-dev.202607082305 → 2.0.0-dev.202607091957
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/dist/index.cjs +602 -228
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -7
- package/package.json +1 -1
- package/scripts/upgrade-via-npm.sh +41 -10
- package/src/adapter/contract.ts +1 -1
- package/src/adapter/resolve.ts +4 -1
- package/src/channel.ts +44 -2
- package/src/commands/bot-logs.ts +4 -2
- package/src/config.ts +1 -1
- package/src/dispatch/body-assembler.ts +58 -21
- package/src/dispatch/ctx-builder.ts +110 -0
- package/src/dispatch/dispatch.ts +3 -42
- package/src/features/history-store.ts +7 -2
- package/src/gateway/event-handlers.ts +33 -1
- package/src/gateway/middleware-setup.ts +20 -12
- package/src/gateway/qqbot-gateway.ts +56 -22
- package/src/outbound/deliver-pipeline.ts +37 -0
- package/src/outbound/image-size.ts +7 -1
- package/src/outbound/local-file-router.ts +25 -0
- package/src/outbound/media-send.ts +49 -15
- package/src/outbound/outbound-service.ts +82 -116
- package/src/outbound/sanitize.ts +32 -0
package/dist/index.cjs
CHANGED
|
@@ -2237,7 +2237,7 @@ var require_websocket = __commonJS({
|
|
|
2237
2237
|
var EventEmitter = require("events");
|
|
2238
2238
|
var https3 = require("https");
|
|
2239
2239
|
var http2 = require("http");
|
|
2240
|
-
var
|
|
2240
|
+
var net2 = require("net");
|
|
2241
2241
|
var tls = require("tls");
|
|
2242
2242
|
var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
|
|
2243
2243
|
var { Duplex, Readable } = require("stream");
|
|
@@ -2971,12 +2971,12 @@ var require_websocket = __commonJS({
|
|
|
2971
2971
|
}
|
|
2972
2972
|
function netConnect(options) {
|
|
2973
2973
|
options.path = options.socketPath;
|
|
2974
|
-
return
|
|
2974
|
+
return net2.connect(options);
|
|
2975
2975
|
}
|
|
2976
2976
|
function tlsConnect(options) {
|
|
2977
2977
|
options.path = void 0;
|
|
2978
2978
|
if (!options.servername && options.servername !== "") {
|
|
2979
|
-
options.servername =
|
|
2979
|
+
options.servername = net2.isIP(options.host) ? "" : options.host;
|
|
2980
2980
|
}
|
|
2981
2981
|
return tls.connect(options);
|
|
2982
2982
|
}
|
|
@@ -4819,7 +4819,7 @@ function evaluateMatchedGroupAccessForPolicy(params) {
|
|
|
4819
4819
|
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
|
|
4820
4820
|
}
|
|
4821
4821
|
var DEFAULT_GROUP_POLICY = "open";
|
|
4822
|
-
var DEFAULT_GROUP_HISTORY_LIMIT =
|
|
4822
|
+
var DEFAULT_GROUP_HISTORY_LIMIT = 20;
|
|
4823
4823
|
var DEFAULT_GROUP_CONFIG = {
|
|
4824
4824
|
requireMention: true,
|
|
4825
4825
|
ignoreOtherMentions: false,
|
|
@@ -5020,11 +5020,11 @@ var fs3 = __toESM(require("fs"), 1);
|
|
|
5020
5020
|
var path = __toESM(require("path"), 1);
|
|
5021
5021
|
|
|
5022
5022
|
// node_modules/.pnpm/@cxyhhhhh+qqbot-nodejs@1.0.2-dev.202607081646/node_modules/@cxyhhhhh/qqbot-nodejs/dist/middleware/types.js
|
|
5023
|
-
function resolvePolicy(ctx,
|
|
5023
|
+
function resolvePolicy(ctx, path22, explicit, defaultValue) {
|
|
5024
5024
|
if (explicit !== void 0 && explicit !== null) {
|
|
5025
5025
|
return explicit;
|
|
5026
5026
|
}
|
|
5027
|
-
const keys =
|
|
5027
|
+
const keys = path22.split(".");
|
|
5028
5028
|
let value = ctx.state.policy;
|
|
5029
5029
|
for (const key of keys) {
|
|
5030
5030
|
if (value === null || value === void 0)
|
|
@@ -5102,10 +5102,10 @@ var ApiError = class extends Error {
|
|
|
5102
5102
|
bizCode;
|
|
5103
5103
|
bizMessage;
|
|
5104
5104
|
name = "ApiError";
|
|
5105
|
-
constructor(message, httpStatus,
|
|
5105
|
+
constructor(message, httpStatus, path22, bizCode, bizMessage) {
|
|
5106
5106
|
super(message);
|
|
5107
5107
|
this.httpStatus = httpStatus;
|
|
5108
|
-
this.path =
|
|
5108
|
+
this.path = path22;
|
|
5109
5109
|
this.bizCode = bizCode;
|
|
5110
5110
|
this.bizMessage = bizMessage;
|
|
5111
5111
|
}
|
|
@@ -5193,14 +5193,14 @@ var ApiClient = class {
|
|
|
5193
5193
|
const ua = config.userAgent ?? "qqbot-nodejs/unknown";
|
|
5194
5194
|
this.resolveUserAgent = typeof ua === "function" ? ua : () => ua;
|
|
5195
5195
|
}
|
|
5196
|
-
async request(accessToken, method,
|
|
5197
|
-
const url = `${this.baseUrl}${
|
|
5196
|
+
async request(accessToken, method, path22, body, options) {
|
|
5197
|
+
const url = `${this.baseUrl}${path22}`;
|
|
5198
5198
|
const headers = {
|
|
5199
5199
|
Authorization: `QQBot ${accessToken}`,
|
|
5200
5200
|
"Content-Type": "application/json",
|
|
5201
5201
|
"User-Agent": this.resolveUserAgent()
|
|
5202
5202
|
};
|
|
5203
|
-
const isFileUpload = options?.uploadRequest === true ||
|
|
5203
|
+
const isFileUpload = options?.uploadRequest === true || path22.includes("/files") || path22.includes("/upload_prepare") || path22.includes("/upload_part_finish");
|
|
5204
5204
|
const timeout = options?.timeoutMs ?? (isFileUpload ? this.fileUploadTimeoutMs : this.defaultTimeoutMs);
|
|
5205
5205
|
const controller = new AbortController();
|
|
5206
5206
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -5225,10 +5225,10 @@ var ApiClient = class {
|
|
|
5225
5225
|
clearTimeout(timeoutId);
|
|
5226
5226
|
if (err instanceof Error && err.name === "AbortError") {
|
|
5227
5227
|
this.logger?.error?.(`[qqbot:api] <<< Timeout after ${timeout}ms`);
|
|
5228
|
-
throw new ApiError(`Request timeout [${
|
|
5228
|
+
throw new ApiError(`Request timeout [${path22}]: exceeded ${timeout}ms`, 0, path22);
|
|
5229
5229
|
}
|
|
5230
5230
|
this.logger?.error?.(`[qqbot:api] <<< Network error: ${formatErrorMessage(err)}`);
|
|
5231
|
-
throw new ApiError(`Network error [${
|
|
5231
|
+
throw new ApiError(`Network error [${path22}]: ${formatErrorMessage(err)}`, 0, path22);
|
|
5232
5232
|
} finally {
|
|
5233
5233
|
clearTimeout(timeoutId);
|
|
5234
5234
|
}
|
|
@@ -5238,7 +5238,7 @@ var ApiClient = class {
|
|
|
5238
5238
|
try {
|
|
5239
5239
|
rawBody = await res.text();
|
|
5240
5240
|
} catch (err) {
|
|
5241
|
-
throw new ApiError(`Failed to read response [${
|
|
5241
|
+
throw new ApiError(`Failed to read response [${path22}]: ${formatErrorMessage(err)}`, res.status, path22);
|
|
5242
5242
|
}
|
|
5243
5243
|
this.logger?.debug?.(`[qqbot:api] <<< Body: ${rawBody}`);
|
|
5244
5244
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -5246,26 +5246,26 @@ var ApiClient = class {
|
|
|
5246
5246
|
if (!res.ok) {
|
|
5247
5247
|
if (isHtmlResponse) {
|
|
5248
5248
|
const statusHint = res.status === 502 || res.status === 503 || res.status === 504 ? "\u8C03\u7528\u53D1\u751F\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u5019\u91CD\u8BD5" : res.status === 429 ? "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u5DF2\u88AB\u9650\u6D41" : `\u5F00\u653E\u5E73\u53F0\u8FD4\u56DE HTTP ${res.status}`;
|
|
5249
|
-
throw new ApiError(`${statusHint}\uFF08${
|
|
5249
|
+
throw new ApiError(`${statusHint}\uFF08${path22}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path22);
|
|
5250
5250
|
}
|
|
5251
5251
|
try {
|
|
5252
5252
|
const error = JSON.parse(rawBody);
|
|
5253
5253
|
const bizCode = error.code ?? error.err_code;
|
|
5254
|
-
throw new ApiError(`API Error [${
|
|
5254
|
+
throw new ApiError(`API Error [${path22}]: ${error.message ?? rawBody}`, res.status, path22, bizCode, error.message);
|
|
5255
5255
|
} catch (parseErr) {
|
|
5256
5256
|
if (parseErr instanceof ApiError) {
|
|
5257
5257
|
throw parseErr;
|
|
5258
5258
|
}
|
|
5259
|
-
throw new ApiError(`API Error [${
|
|
5259
|
+
throw new ApiError(`API Error [${path22}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`, res.status, path22);
|
|
5260
5260
|
}
|
|
5261
5261
|
}
|
|
5262
5262
|
if (isHtmlResponse) {
|
|
5263
|
-
throw new ApiError(`QQ \u670D\u52A1\u7AEF\u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\uFF08${
|
|
5263
|
+
throw new ApiError(`QQ \u670D\u52A1\u7AEF\u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\uFF08${path22}\uFF09\uFF0C\u53EF\u80FD\u662F\u4E34\u65F6\u6545\u969C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path22);
|
|
5264
5264
|
}
|
|
5265
5265
|
try {
|
|
5266
5266
|
return JSON.parse(rawBody);
|
|
5267
5267
|
} catch {
|
|
5268
|
-
throw new ApiError(`\u5F00\u653E\u5E73\u53F0\u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF08${
|
|
5268
|
+
throw new ApiError(`\u5F00\u653E\u5E73\u53F0\u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF08${path22}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path22);
|
|
5269
5269
|
}
|
|
5270
5270
|
}
|
|
5271
5271
|
};
|
|
@@ -5488,9 +5488,9 @@ var ChunkedMediaApi = class {
|
|
|
5488
5488
|
}
|
|
5489
5489
|
async callUploadPrepare(opts, fileName, fileSize, hashes, pathLabel) {
|
|
5490
5490
|
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
|
|
5491
|
-
const
|
|
5491
|
+
const path22 = uploadPreparePath(opts.scope, opts.targetId);
|
|
5492
5492
|
try {
|
|
5493
|
-
return await this.client.request(token, "POST",
|
|
5493
|
+
return await this.client.request(token, "POST", path22, {
|
|
5494
5494
|
file_type: opts.fileType,
|
|
5495
5495
|
file_name: fileName,
|
|
5496
5496
|
file_size: fileSize,
|
|
@@ -5507,10 +5507,10 @@ var ChunkedMediaApi = class {
|
|
|
5507
5507
|
}
|
|
5508
5508
|
async callUploadPartFinish(opts, uploadId, partIndex, blockSize, md5, retryTimeoutMs) {
|
|
5509
5509
|
const persistentPolicy = buildPartFinishPersistentPolicy(retryTimeoutMs);
|
|
5510
|
-
const
|
|
5510
|
+
const path22 = uploadPartFinishPath(opts.scope, opts.targetId);
|
|
5511
5511
|
await withRetry(async () => {
|
|
5512
5512
|
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
|
|
5513
|
-
return this.client.request(token, "POST",
|
|
5513
|
+
return this.client.request(token, "POST", path22, {
|
|
5514
5514
|
upload_id: uploadId,
|
|
5515
5515
|
part_index: partIndex,
|
|
5516
5516
|
block_size: blockSize,
|
|
@@ -5519,10 +5519,10 @@ var ChunkedMediaApi = class {
|
|
|
5519
5519
|
}, PART_FINISH_RETRY_POLICY, persistentPolicy, this.logger);
|
|
5520
5520
|
}
|
|
5521
5521
|
async callCompleteUpload(opts, uploadId) {
|
|
5522
|
-
const
|
|
5522
|
+
const path22 = uploadCompletePath(opts.scope, opts.targetId);
|
|
5523
5523
|
return withRetry(async () => {
|
|
5524
5524
|
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
|
|
5525
|
-
return this.client.request(token, "POST",
|
|
5525
|
+
return this.client.request(token, "POST", path22, { upload_id: uploadId }, { uploadRequest: true });
|
|
5526
5526
|
}, COMPLETE_UPLOAD_RETRY_POLICY, void 0, this.logger);
|
|
5527
5527
|
}
|
|
5528
5528
|
};
|
|
@@ -5716,8 +5716,8 @@ var MediaApi = class {
|
|
|
5716
5716
|
body.file_name = this.sanitize(opts.fileName);
|
|
5717
5717
|
}
|
|
5718
5718
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5719
|
-
const
|
|
5720
|
-
const result = await withRetry(() => this.client.request(token, "POST",
|
|
5719
|
+
const path22 = mediaUploadPath(scope, targetId);
|
|
5720
|
+
const result = await withRetry(() => this.client.request(token, "POST", path22, body, {
|
|
5721
5721
|
redactBodyKeys: ["file_data"],
|
|
5722
5722
|
uploadRequest: true
|
|
5723
5723
|
}), UPLOAD_RETRY_POLICY, void 0, this.logger);
|
|
@@ -5733,8 +5733,8 @@ var MediaApi = class {
|
|
|
5733
5733
|
async sendMediaMessage(scope, targetId, fileInfo, creds, opts) {
|
|
5734
5734
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5735
5735
|
const msgSeq = opts?.msgId ? getNextMsgSeq(opts.msgId) : 1;
|
|
5736
|
-
const
|
|
5737
|
-
return this.client.request(token, "POST",
|
|
5736
|
+
const path22 = messagePath(scope, targetId);
|
|
5737
|
+
return this.client.request(token, "POST", path22, {
|
|
5738
5738
|
msg_type: 7,
|
|
5739
5739
|
media: { file_info: fileInfo },
|
|
5740
5740
|
msg_seq: msgSeq,
|
|
@@ -5773,8 +5773,8 @@ var MessageApi = class {
|
|
|
5773
5773
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5774
5774
|
const msgSeq = opts?.msgId ? getNextMsgSeq(opts.msgId) : 1;
|
|
5775
5775
|
const body = this.buildMessageBody(content, opts?.msgId, msgSeq, opts?.messageReference, opts?.inlineKeyboard);
|
|
5776
|
-
const
|
|
5777
|
-
return this.sendAndNotify(creds.appId, token, "POST",
|
|
5776
|
+
const path22 = messagePath(scope, targetId);
|
|
5777
|
+
return this.sendAndNotify(creds.appId, token, "POST", path22, body, { text: content });
|
|
5778
5778
|
}
|
|
5779
5779
|
async sendProactiveMessage(scope, targetId, content, creds) {
|
|
5780
5780
|
if (!content?.trim()) {
|
|
@@ -5782,8 +5782,8 @@ var MessageApi = class {
|
|
|
5782
5782
|
}
|
|
5783
5783
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5784
5784
|
const body = this.buildProactiveBody(content);
|
|
5785
|
-
const
|
|
5786
|
-
return this.sendAndNotify(creds.appId, token, "POST",
|
|
5785
|
+
const path22 = messagePath(scope, targetId);
|
|
5786
|
+
return this.sendAndNotify(creds.appId, token, "POST", path22, body, { text: content });
|
|
5787
5787
|
}
|
|
5788
5788
|
async sendChannelMessage(opts) {
|
|
5789
5789
|
const token = await this.tokenManager.getAccessToken(opts.creds.appId, opts.creds.clientSecret);
|
|
@@ -5831,7 +5831,7 @@ var MessageApi = class {
|
|
|
5831
5831
|
*/
|
|
5832
5832
|
async sendC2CStreamMessage(creds, openid, req4) {
|
|
5833
5833
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5834
|
-
const
|
|
5834
|
+
const path22 = streamMessagePath(openid);
|
|
5835
5835
|
const body = {
|
|
5836
5836
|
input_mode: req4.input_mode,
|
|
5837
5837
|
input_state: req4.input_state,
|
|
@@ -5845,7 +5845,7 @@ var MessageApi = class {
|
|
|
5845
5845
|
if (req4.stream_msg_id) {
|
|
5846
5846
|
body.stream_msg_id = req4.stream_msg_id;
|
|
5847
5847
|
}
|
|
5848
|
-
return this.client.request(token, "POST",
|
|
5848
|
+
return this.client.request(token, "POST", path22, body);
|
|
5849
5849
|
}
|
|
5850
5850
|
/**
|
|
5851
5851
|
* Raw message send — transparently forwards all fields to the QQ Open Platform API.
|
|
@@ -5859,7 +5859,7 @@ var MessageApi = class {
|
|
|
5859
5859
|
*/
|
|
5860
5860
|
async sendRaw(scope, targetId, creds, body) {
|
|
5861
5861
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5862
|
-
const
|
|
5862
|
+
const path22 = messagePath(scope, targetId);
|
|
5863
5863
|
if (body.msg_seq === void 0) {
|
|
5864
5864
|
body.msg_seq = body.msg_id ? getNextMsgSeq(body.msg_id) : 1;
|
|
5865
5865
|
}
|
|
@@ -5876,7 +5876,7 @@ var MessageApi = class {
|
|
|
5876
5876
|
body.msg_type = 0;
|
|
5877
5877
|
}
|
|
5878
5878
|
const cleaned = Object.fromEntries(Object.entries(body).filter(([, v]) => v !== void 0));
|
|
5879
|
-
return this.sendAndNotify(creds.appId, token, "POST",
|
|
5879
|
+
return this.sendAndNotify(creds.appId, token, "POST", path22, cleaned, { text: cleaned.content ?? cleaned.markdown?.content });
|
|
5880
5880
|
}
|
|
5881
5881
|
/**
|
|
5882
5882
|
* Send a message to a guild text channel.
|
|
@@ -5900,11 +5900,11 @@ var MessageApi = class {
|
|
|
5900
5900
|
*/
|
|
5901
5901
|
async recallMessage(scope, targetId, messageId, creds) {
|
|
5902
5902
|
const token = await this.tokenManager.getAccessToken(creds.appId, creds.clientSecret);
|
|
5903
|
-
const
|
|
5904
|
-
await this.client.request(token, "DELETE",
|
|
5903
|
+
const path22 = `${messagePath(scope, targetId)}/${messageId}`;
|
|
5904
|
+
await this.client.request(token, "DELETE", path22);
|
|
5905
5905
|
}
|
|
5906
|
-
async sendAndNotify(_appId, accessToken, method,
|
|
5907
|
-
const result = await this.client.request(accessToken, method,
|
|
5906
|
+
async sendAndNotify(_appId, accessToken, method, path22, body, meta) {
|
|
5907
|
+
const result = await this.client.request(accessToken, method, path22, body);
|
|
5908
5908
|
if (result.ext_info?.ref_idx && this.messageSentHook) {
|
|
5909
5909
|
try {
|
|
5910
5910
|
this.messageSentHook(result.ext_info.ref_idx, meta);
|
|
@@ -6785,10 +6785,10 @@ function signValidationResponse(params) {
|
|
|
6785
6785
|
var http = __toESM(require("http"), 1);
|
|
6786
6786
|
var NodeHttpWebhookServer = class {
|
|
6787
6787
|
server = null;
|
|
6788
|
-
async listen(port,
|
|
6788
|
+
async listen(port, path22, handler) {
|
|
6789
6789
|
return new Promise((resolve2, reject) => {
|
|
6790
6790
|
const server = http.createServer(async (req4, res) => {
|
|
6791
|
-
if (req4.method !== "POST" || req4.url !==
|
|
6791
|
+
if (req4.method !== "POST" || req4.url !== path22) {
|
|
6792
6792
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
6793
6793
|
res.end("Not Found");
|
|
6794
6794
|
return;
|
|
@@ -6850,11 +6850,11 @@ var WebhookTransport = class {
|
|
|
6850
6850
|
}
|
|
6851
6851
|
async start() {
|
|
6852
6852
|
const port = this.opts.port ?? 8080;
|
|
6853
|
-
const
|
|
6854
|
-
this.log?.info?.(`[webhook] starting on port ${port}, path ${
|
|
6855
|
-
await this.server.listen(port,
|
|
6856
|
-
this.log?.info?.(`[webhook] listening on :${port}${
|
|
6857
|
-
this.callbacks.onReady?.({ transport: "webhook", port, path:
|
|
6853
|
+
const path22 = this.opts.path ?? "/";
|
|
6854
|
+
this.log?.info?.(`[webhook] starting on port ${port}, path ${path22}`);
|
|
6855
|
+
await this.server.listen(port, path22, (req4) => this.handleRequest(req4));
|
|
6856
|
+
this.log?.info?.(`[webhook] listening on :${port}${path22}`);
|
|
6857
|
+
this.callbacks.onReady?.({ transport: "webhook", port, path: path22 });
|
|
6858
6858
|
if (this.opts.abortSignal) {
|
|
6859
6859
|
await new Promise((resolve2) => {
|
|
6860
6860
|
if (this.opts.abortSignal.aborted) {
|
|
@@ -7744,25 +7744,25 @@ var QQBot = class {
|
|
|
7744
7744
|
if (this._apiGateway)
|
|
7745
7745
|
return this._apiGateway;
|
|
7746
7746
|
this._apiGateway = {
|
|
7747
|
-
get: (
|
|
7748
|
-
post: (
|
|
7749
|
-
put: (
|
|
7750
|
-
patch: (
|
|
7751
|
-
delete: (
|
|
7747
|
+
get: (path22, query) => this.apiRequest("GET", path22, void 0, query),
|
|
7748
|
+
post: (path22, body) => this.apiRequest("POST", path22, body),
|
|
7749
|
+
put: (path22, body) => this.apiRequest("PUT", path22, body),
|
|
7750
|
+
patch: (path22, body) => this.apiRequest("PATCH", path22, body),
|
|
7751
|
+
delete: (path22) => this.apiRequest("DELETE", path22),
|
|
7752
7752
|
getToken: () => this.tokenManager.getAccessToken(this.creds.appId, this.creds.clientSecret)
|
|
7753
7753
|
};
|
|
7754
7754
|
return this._apiGateway;
|
|
7755
7755
|
}
|
|
7756
|
-
async apiRequest(method,
|
|
7756
|
+
async apiRequest(method, path22, body, query) {
|
|
7757
7757
|
const token = await this.tokenManager.getAccessToken(this.creds.appId, this.creds.clientSecret);
|
|
7758
|
-
let fullPath =
|
|
7758
|
+
let fullPath = path22;
|
|
7759
7759
|
if (query && Object.keys(query).length > 0) {
|
|
7760
7760
|
const params = new URLSearchParams();
|
|
7761
7761
|
for (const [k, v] of Object.entries(query)) {
|
|
7762
7762
|
if (v !== void 0 && v !== null)
|
|
7763
7763
|
params.set(k, String(v));
|
|
7764
7764
|
}
|
|
7765
|
-
fullPath = `${
|
|
7765
|
+
fullPath = `${path22}?${params.toString()}`;
|
|
7766
7766
|
}
|
|
7767
7767
|
return this.apiClient.request(token, method, fullPath, body ?? void 0);
|
|
7768
7768
|
}
|
|
@@ -9004,13 +9004,105 @@ function parseTarget(to) {
|
|
|
9004
9004
|
return { scope: "c2c", targetId: id };
|
|
9005
9005
|
}
|
|
9006
9006
|
|
|
9007
|
+
// src/outbound/reply-limiter.ts
|
|
9008
|
+
var ReplyLimiter = class {
|
|
9009
|
+
limit;
|
|
9010
|
+
ttlMs;
|
|
9011
|
+
maxTracked;
|
|
9012
|
+
messages = /* @__PURE__ */ new Map();
|
|
9013
|
+
constructor(config) {
|
|
9014
|
+
this.limit = config?.limit ?? 4;
|
|
9015
|
+
this.ttlMs = config?.ttlMs ?? 36e5;
|
|
9016
|
+
this.maxTracked = config?.maxTrackedMessages ?? 1e4;
|
|
9017
|
+
}
|
|
9018
|
+
/**
|
|
9019
|
+
* 检查是否允许对指定消息继续被动回复
|
|
9020
|
+
*/
|
|
9021
|
+
checkLimit(messageId) {
|
|
9022
|
+
const now = Date.now();
|
|
9023
|
+
const tracked = this.messages.get(messageId);
|
|
9024
|
+
if (!tracked) {
|
|
9025
|
+
return { allowed: true, remaining: this.limit, shouldFallbackToProactive: false };
|
|
9026
|
+
}
|
|
9027
|
+
if (now - tracked.firstSeenAt > this.ttlMs) {
|
|
9028
|
+
this.messages.delete(messageId);
|
|
9029
|
+
return {
|
|
9030
|
+
allowed: false,
|
|
9031
|
+
remaining: 0,
|
|
9032
|
+
shouldFallbackToProactive: true,
|
|
9033
|
+
fallbackReason: "expired"
|
|
9034
|
+
};
|
|
9035
|
+
}
|
|
9036
|
+
const remaining = Math.max(0, this.limit - tracked.count);
|
|
9037
|
+
if (remaining <= 0) {
|
|
9038
|
+
return {
|
|
9039
|
+
allowed: false,
|
|
9040
|
+
remaining: 0,
|
|
9041
|
+
shouldFallbackToProactive: true,
|
|
9042
|
+
fallbackReason: "limit_exceeded"
|
|
9043
|
+
};
|
|
9044
|
+
}
|
|
9045
|
+
return { allowed: true, remaining, shouldFallbackToProactive: false };
|
|
9046
|
+
}
|
|
9047
|
+
/**
|
|
9048
|
+
* 记录一次被动回复
|
|
9049
|
+
*/
|
|
9050
|
+
record(messageId) {
|
|
9051
|
+
const now = Date.now();
|
|
9052
|
+
const tracked = this.messages.get(messageId);
|
|
9053
|
+
if (tracked) {
|
|
9054
|
+
tracked.count++;
|
|
9055
|
+
} else {
|
|
9056
|
+
if (this.messages.size >= this.maxTracked) {
|
|
9057
|
+
const firstKey = this.messages.keys().next().value;
|
|
9058
|
+
if (firstKey) this.messages.delete(firstKey);
|
|
9059
|
+
}
|
|
9060
|
+
this.messages.set(messageId, { count: 1, firstSeenAt: now });
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
/**
|
|
9064
|
+
* 获取统计信息
|
|
9065
|
+
*/
|
|
9066
|
+
getStats() {
|
|
9067
|
+
let totalReplies = 0;
|
|
9068
|
+
for (const [, v] of this.messages) {
|
|
9069
|
+
totalReplies += v.count;
|
|
9070
|
+
}
|
|
9071
|
+
return { trackedMessages: this.messages.size, totalReplies };
|
|
9072
|
+
}
|
|
9073
|
+
/**
|
|
9074
|
+
* 清除所有跟踪数据
|
|
9075
|
+
*/
|
|
9076
|
+
clear() {
|
|
9077
|
+
this.messages.clear();
|
|
9078
|
+
}
|
|
9079
|
+
};
|
|
9080
|
+
|
|
9007
9081
|
// src/outbound/outbound-service.ts
|
|
9008
9082
|
var gateways = /* @__PURE__ */ new Map();
|
|
9083
|
+
var limiters = /* @__PURE__ */ new Map();
|
|
9084
|
+
function getLimiter(accountId) {
|
|
9085
|
+
let l3 = limiters.get(accountId);
|
|
9086
|
+
if (!l3) {
|
|
9087
|
+
l3 = new ReplyLimiter();
|
|
9088
|
+
limiters.set(accountId, l3);
|
|
9089
|
+
}
|
|
9090
|
+
return l3;
|
|
9091
|
+
}
|
|
9092
|
+
function resolveMsgId(replyToId, accountId) {
|
|
9093
|
+
if (!replyToId) return void 0;
|
|
9094
|
+
const limiter = getLimiter(accountId);
|
|
9095
|
+
const result = limiter.checkLimit(replyToId);
|
|
9096
|
+
if (!result.allowed) return void 0;
|
|
9097
|
+
limiter.record(replyToId);
|
|
9098
|
+
return replyToId;
|
|
9099
|
+
}
|
|
9009
9100
|
function registerGateway(accountId, gw) {
|
|
9010
9101
|
gateways.set(accountId, gw);
|
|
9011
9102
|
}
|
|
9012
9103
|
function unregisterGateway(accountId) {
|
|
9013
9104
|
gateways.delete(accountId);
|
|
9105
|
+
limiters.delete(accountId);
|
|
9014
9106
|
}
|
|
9015
9107
|
function getGateway(accountId) {
|
|
9016
9108
|
return gateways.get(accountId);
|
|
@@ -9022,62 +9114,55 @@ var MEDIA_KIND_TO_FILE_TYPE = {
|
|
|
9022
9114
|
file: MediaFileType.FILE
|
|
9023
9115
|
};
|
|
9024
9116
|
async function sendText(params) {
|
|
9025
|
-
const
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
}
|
|
9117
|
+
const accountId = params.account.accountId;
|
|
9118
|
+
const gw = gateways.get(accountId);
|
|
9119
|
+
if (!gw) return { error: `Bot "${accountId}" not running` };
|
|
9029
9120
|
try {
|
|
9030
9121
|
const target = parseTarget(params.to);
|
|
9031
|
-
const
|
|
9122
|
+
const msgId = resolveMsgId(params.replyToId, accountId);
|
|
9123
|
+
const result = await gw.sendText(target, params.text, { msgId });
|
|
9032
9124
|
return { messageId: result.id };
|
|
9033
9125
|
} catch (err) {
|
|
9034
9126
|
return formatError(err);
|
|
9035
9127
|
}
|
|
9036
9128
|
}
|
|
9037
9129
|
async function sendMedia(params) {
|
|
9038
|
-
const
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
}
|
|
9130
|
+
const accountId = params.account.accountId;
|
|
9131
|
+
const gw = gateways.get(accountId);
|
|
9132
|
+
if (!gw) return { error: `Bot "${accountId}" not running` };
|
|
9042
9133
|
try {
|
|
9043
9134
|
const target = parseTarget(params.to);
|
|
9044
9135
|
const kind = params.mediaKind ?? "image";
|
|
9136
|
+
const msgId = resolveMsgId(params.replyToId, accountId);
|
|
9045
9137
|
if (kind === "voice") {
|
|
9046
|
-
const
|
|
9047
|
-
const
|
|
9048
|
-
const source = isLocalPath ? { localPath: params.mediaUrl } : isUrl ? { url: params.mediaUrl } : { base64: params.mediaUrl };
|
|
9049
|
-
const result2 = await gw.sendVoice(
|
|
9050
|
-
target,
|
|
9051
|
-
source,
|
|
9052
|
-
{ text: params.text, msgId: params.replyToId }
|
|
9053
|
-
);
|
|
9138
|
+
const source = resolveVoiceSource(params.mediaUrl);
|
|
9139
|
+
const result2 = await gw.sendVoice(target, source, { text: params.text, msgId });
|
|
9054
9140
|
return { messageId: result2.id };
|
|
9055
9141
|
}
|
|
9056
9142
|
if (kind === "video") {
|
|
9057
|
-
const result2 = await gw.sendVideo(target, params.mediaUrl, {
|
|
9058
|
-
text: params.text,
|
|
9059
|
-
msgId: params.replyToId
|
|
9060
|
-
});
|
|
9143
|
+
const result2 = await gw.sendVideo(target, params.mediaUrl, { text: params.text, msgId });
|
|
9061
9144
|
return { messageId: result2.id };
|
|
9062
9145
|
}
|
|
9063
9146
|
if (kind === "file") {
|
|
9064
|
-
const result2 = await gw.sendFile(target, params.mediaUrl, {
|
|
9065
|
-
text: params.text,
|
|
9066
|
-
msgId: params.replyToId
|
|
9067
|
-
});
|
|
9147
|
+
const result2 = await gw.sendFile(target, params.mediaUrl, { text: params.text, msgId });
|
|
9068
9148
|
return { messageId: result2.id };
|
|
9069
9149
|
}
|
|
9070
9150
|
const fileType = MEDIA_KIND_TO_FILE_TYPE[kind];
|
|
9071
|
-
const result = await gw.sendMedia(target, params.mediaUrl, {
|
|
9072
|
-
text: params.text,
|
|
9073
|
-
msgId: params.replyToId,
|
|
9074
|
-
fileType
|
|
9075
|
-
});
|
|
9151
|
+
const result = await gw.sendMedia(target, params.mediaUrl, { text: params.text, msgId, fileType });
|
|
9076
9152
|
return { messageId: result.id };
|
|
9077
9153
|
} catch (err) {
|
|
9078
9154
|
return formatError(err);
|
|
9079
9155
|
}
|
|
9080
9156
|
}
|
|
9157
|
+
function resolveVoiceSource(source) {
|
|
9158
|
+
if (source.startsWith("http://") || source.startsWith("https://")) return { url: source };
|
|
9159
|
+
if (source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) return { localPath: source };
|
|
9160
|
+
if (source.startsWith("data:")) {
|
|
9161
|
+
const i = source.indexOf(",");
|
|
9162
|
+
return { base64: i > 0 ? source.slice(i + 1) : source };
|
|
9163
|
+
}
|
|
9164
|
+
return { base64: source };
|
|
9165
|
+
}
|
|
9081
9166
|
function formatError(err) {
|
|
9082
9167
|
if (err instanceof Error) {
|
|
9083
9168
|
const result = { error: err.message };
|
|
@@ -9093,7 +9178,7 @@ var import_node_path2 = __toESM(require("path"), 1);
|
|
|
9093
9178
|
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
9094
9179
|
var _cachedOpenclawVersion;
|
|
9095
9180
|
function getPackageVersion() {
|
|
9096
|
-
return true ? "2.0.0-dev.
|
|
9181
|
+
return true ? "2.0.0-dev.202607091957" : "unknown";
|
|
9097
9182
|
}
|
|
9098
9183
|
function getOpenclawVersion(runtimeVersion) {
|
|
9099
9184
|
if (_cachedOpenclawVersion) return _cachedOpenclawVersion;
|
|
@@ -9508,12 +9593,12 @@ function tryGetQQBotRuntime() {
|
|
|
9508
9593
|
|
|
9509
9594
|
// src/adapter/resolve.ts
|
|
9510
9595
|
function probeFunction(rt, paths) {
|
|
9511
|
-
for (const
|
|
9596
|
+
for (const path22 of paths) {
|
|
9512
9597
|
let target = rt;
|
|
9513
9598
|
let parent = rt;
|
|
9514
|
-
for (let i = 0; i <
|
|
9599
|
+
for (let i = 0; i < path22.length; i++) {
|
|
9515
9600
|
parent = target;
|
|
9516
|
-
target = target?.[
|
|
9601
|
+
target = target?.[path22[i]];
|
|
9517
9602
|
if (target === void 0 || target === null) break;
|
|
9518
9603
|
}
|
|
9519
9604
|
if (typeof target === "function") {
|
|
@@ -9541,11 +9626,15 @@ function resolveRuntimeAdapters(rt, log4) {
|
|
|
9541
9626
|
]);
|
|
9542
9627
|
const rawFinalizeContext = !rawBuildContext ? probeFunction(rt, [["channel", "reply", "finalizeInboundContext"]]) : null;
|
|
9543
9628
|
const buildInboundContext = rawBuildContext ? (params) => rawBuildContext(params) : rawFinalizeContext ? (params) => {
|
|
9629
|
+
const isCommand = params.access?.commands?.authorized ?? false;
|
|
9544
9630
|
const rawCtx = {
|
|
9545
9631
|
Body: params.message.body,
|
|
9546
9632
|
BodyForAgent: params.message.bodyForAgent,
|
|
9547
9633
|
RawBody: params.message.rawBody,
|
|
9548
9634
|
CommandBody: params.message.commandBody ?? params.message.rawBody,
|
|
9635
|
+
CommandSource: isCommand ? "text" : void 0,
|
|
9636
|
+
CommandTurn: params.command ?? void 0,
|
|
9637
|
+
CommandAuthorized: isCommand,
|
|
9549
9638
|
From: params.from,
|
|
9550
9639
|
To: params.reply.to,
|
|
9551
9640
|
SessionKey: params.route.routeSessionKey,
|
|
@@ -9560,7 +9649,6 @@ function resolveRuntimeAdapters(rt, log4) {
|
|
|
9560
9649
|
Timestamp: params.timestamp ?? Date.now(),
|
|
9561
9650
|
OriginatingChannel: params.channel,
|
|
9562
9651
|
OriginatingTo: params.reply.originatingTo ?? params.reply.to,
|
|
9563
|
-
CommandAuthorized: false,
|
|
9564
9652
|
...params.extra
|
|
9565
9653
|
};
|
|
9566
9654
|
return rawFinalizeContext(rawCtx);
|
|
@@ -9672,6 +9760,63 @@ function resolveAgentWorkspace(cfg, agentId) {
|
|
|
9672
9760
|
return getQQBotMediaDir();
|
|
9673
9761
|
}
|
|
9674
9762
|
|
|
9763
|
+
// src/utils/ssrf-guard.ts
|
|
9764
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
9765
|
+
var import_promises = __toESM(require("dns/promises"), 1);
|
|
9766
|
+
var RESERVED_V4_PREFIXES = [
|
|
9767
|
+
"127.",
|
|
9768
|
+
// loopback
|
|
9769
|
+
"10.",
|
|
9770
|
+
// class-A private
|
|
9771
|
+
"192.168.",
|
|
9772
|
+
// class-C private
|
|
9773
|
+
"169.254."
|
|
9774
|
+
// link-local / cloud metadata
|
|
9775
|
+
];
|
|
9776
|
+
var PRIVATE_172_RE = /^172\.(1[6-9]|2\d|3[01])\./;
|
|
9777
|
+
function isReservedAddr(ip) {
|
|
9778
|
+
if (ip === "0.0.0.0") return true;
|
|
9779
|
+
for (const pfx of RESERVED_V4_PREFIXES) {
|
|
9780
|
+
if (ip.startsWith(pfx)) return true;
|
|
9781
|
+
}
|
|
9782
|
+
if (PRIVATE_172_RE.test(ip)) return true;
|
|
9783
|
+
const lower = ip.toLowerCase();
|
|
9784
|
+
if (lower === "::1" || lower === "::") return true;
|
|
9785
|
+
if (lower.startsWith("fe80:")) return true;
|
|
9786
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
9787
|
+
return false;
|
|
9788
|
+
}
|
|
9789
|
+
var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
9790
|
+
async function validateRemoteUrl(raw) {
|
|
9791
|
+
const url = new URL(raw);
|
|
9792
|
+
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
|
9793
|
+
throw new Error(
|
|
9794
|
+
`\u4E0D\u652F\u6301\u7684\u534F\u8BAE "${url.protocol}"\uFF0C\u4EC5\u5141\u8BB8 http/https\uFF08URL: ${raw}\uFF09`
|
|
9795
|
+
);
|
|
9796
|
+
}
|
|
9797
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
9798
|
+
if (import_node_net.default.isIP(host)) {
|
|
9799
|
+
assertPublicAddr(host, raw);
|
|
9800
|
+
return;
|
|
9801
|
+
}
|
|
9802
|
+
try {
|
|
9803
|
+
const ips = await import_promises.default.resolve(host);
|
|
9804
|
+
for (const ip of ips) {
|
|
9805
|
+
assertPublicAddr(ip, raw, host);
|
|
9806
|
+
}
|
|
9807
|
+
} catch (err) {
|
|
9808
|
+
if (err instanceof Error && err.message.includes("\u5185\u7F51")) throw err;
|
|
9809
|
+
console.warn(`[url-check] DNS \u89E3\u6790 "${host}" \u5931\u8D25: ${err}`);
|
|
9810
|
+
}
|
|
9811
|
+
}
|
|
9812
|
+
function assertPublicAddr(ip, originalUrl, domain) {
|
|
9813
|
+
if (!isReservedAddr(ip)) return;
|
|
9814
|
+
const target = domain ? `\u57DF\u540D "${domain}" \u89E3\u6790\u5230\u5185\u7F51\u5730\u5740 "${ip}"` : `\u5185\u7F51\u5730\u5740 "${ip}"`;
|
|
9815
|
+
throw new Error(
|
|
9816
|
+
`\u7981\u6B62\u8BBF\u95EE${target}\uFF0C\u5DF2\u62E6\u622A\u6F5C\u5728\u7684 SSRF \u8BF7\u6C42\uFF08URL: ${originalUrl}\uFF09`
|
|
9817
|
+
);
|
|
9818
|
+
}
|
|
9819
|
+
|
|
9675
9820
|
// src/outbound/local-file-router.ts
|
|
9676
9821
|
var path7 = __toESM(require("path"), 1);
|
|
9677
9822
|
var fs8 = __toESM(require("fs"), 1);
|
|
@@ -9706,6 +9851,9 @@ function isLocalFilePath(source) {
|
|
|
9706
9851
|
if (source.startsWith(".\\") || source.startsWith("..\\")) return true;
|
|
9707
9852
|
return false;
|
|
9708
9853
|
}
|
|
9854
|
+
function isDataUrl(source) {
|
|
9855
|
+
return source.startsWith("data:");
|
|
9856
|
+
}
|
|
9709
9857
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".ico"]);
|
|
9710
9858
|
var VOICE_EXTS = /* @__PURE__ */ new Set([".wav", ".mp3", ".silk", ".amr", ".ogg", ".flac", ".aac", ".m4a"]);
|
|
9711
9859
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv"]);
|
|
@@ -9723,14 +9871,48 @@ function inferMediaKindFromMime(mime) {
|
|
|
9723
9871
|
if (lower.startsWith("video/")) return "video";
|
|
9724
9872
|
return "file";
|
|
9725
9873
|
}
|
|
9874
|
+
function isPathInAllowedRoots(absPath, allowedRoots) {
|
|
9875
|
+
if (!allowedRoots.length) return false;
|
|
9876
|
+
try {
|
|
9877
|
+
const real = fs8.realpathSync(absPath);
|
|
9878
|
+
return allowedRoots.some((root) => {
|
|
9879
|
+
try {
|
|
9880
|
+
const rootReal = fs8.existsSync(root) ? fs8.realpathSync(root) : root;
|
|
9881
|
+
return real.startsWith(rootReal + path7.sep) || real === rootReal;
|
|
9882
|
+
} catch {
|
|
9883
|
+
return false;
|
|
9884
|
+
}
|
|
9885
|
+
});
|
|
9886
|
+
} catch {
|
|
9887
|
+
return false;
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9726
9890
|
|
|
9727
9891
|
// src/outbound/media-send.ts
|
|
9892
|
+
function resolveTempRoots() {
|
|
9893
|
+
const roots = /* @__PURE__ */ new Set();
|
|
9894
|
+
try {
|
|
9895
|
+
const tmp = os4.tmpdir();
|
|
9896
|
+
roots.add(fs9.existsSync(tmp) ? fs9.realpathSync(tmp) : tmp);
|
|
9897
|
+
} catch {
|
|
9898
|
+
}
|
|
9899
|
+
if (process.platform !== "win32") {
|
|
9900
|
+
try {
|
|
9901
|
+
roots.add(fs9.realpathSync("/tmp"));
|
|
9902
|
+
} catch {
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
return [...roots];
|
|
9906
|
+
}
|
|
9728
9907
|
var ALLOWED_MEDIA_ROOTS = [
|
|
9729
9908
|
path8.join(os4.homedir(), ".openclaw", "media"),
|
|
9730
9909
|
path8.join(os4.homedir(), ".openclaw", "workspace"),
|
|
9731
9910
|
// 框架 outbound 目录(saveMediaBuffer 写入)
|
|
9732
|
-
path8.join(os4.homedir(), ".openclaw", "outbound")
|
|
9911
|
+
path8.join(os4.homedir(), ".openclaw", "outbound"),
|
|
9912
|
+
// TTS 语音临时目录(deliver-pipeline 已通过 isTtsPathSafe 预检)
|
|
9913
|
+
...resolveTempRoots()
|
|
9733
9914
|
];
|
|
9915
|
+
var MAX_DATA_URL_BYTES = 10 * 1024 * 1024;
|
|
9734
9916
|
async function sendMedia2(params) {
|
|
9735
9917
|
const { source, accountId, log: log4 } = params;
|
|
9736
9918
|
const mlog = log4?.child("media");
|
|
@@ -9740,7 +9922,7 @@ async function sendMedia2(params) {
|
|
|
9740
9922
|
}
|
|
9741
9923
|
const wsDir = resolveWorkspaceFromAgent(params.agentId);
|
|
9742
9924
|
mlog?.debug(`resolveMediaPath source=${source} agentId=${params.agentId ?? "none"} workspaceDir=${wsDir ?? "none"}`);
|
|
9743
|
-
const resolved = resolveMediaPath(source, mlog, wsDir);
|
|
9925
|
+
const resolved = await resolveMediaPath(source, mlog, wsDir);
|
|
9744
9926
|
if (!resolved.ok) {
|
|
9745
9927
|
mlog?.error(`resolveMediaPath failed: ${resolved.error}`);
|
|
9746
9928
|
return { error: resolved.error };
|
|
@@ -9763,8 +9945,24 @@ async function sendMedia2(params) {
|
|
|
9763
9945
|
return sendImageMedia(gw, target, resolved.path, params);
|
|
9764
9946
|
}
|
|
9765
9947
|
}
|
|
9766
|
-
function resolveMediaPath(source, log4, workspaceDir) {
|
|
9948
|
+
async function resolveMediaPath(source, log4, workspaceDir) {
|
|
9767
9949
|
const normalized = normalizePath(source);
|
|
9950
|
+
if (isDataUrl(normalized)) {
|
|
9951
|
+
if (normalized.length > MAX_DATA_URL_BYTES) {
|
|
9952
|
+
const sizeMB = (normalized.length / (1024 * 1024)).toFixed(1);
|
|
9953
|
+
return { ok: false, error: `Data URL \u8FC7\u5927\uFF08${sizeMB}MB\uFF0C\u6700\u5927 10MB\uFF09` };
|
|
9954
|
+
}
|
|
9955
|
+
return { ok: true, path: normalized, isLocal: false };
|
|
9956
|
+
}
|
|
9957
|
+
if (normalized.startsWith("http://") || normalized.startsWith("https://")) {
|
|
9958
|
+
try {
|
|
9959
|
+
await validateRemoteUrl(normalized);
|
|
9960
|
+
} catch (err) {
|
|
9961
|
+
log4?.warn(`SSRF blocked for media URL: ${normalized}`);
|
|
9962
|
+
return { ok: false, error: `\u5A92\u4F53 URL \u88AB SSRF \u9632\u62A4\u62E6\u622A: ${err instanceof Error ? err.message : String(err)}` };
|
|
9963
|
+
}
|
|
9964
|
+
return { ok: true, path: normalized, isLocal: false };
|
|
9965
|
+
}
|
|
9768
9966
|
if (!isLocalFilePath(normalized)) {
|
|
9769
9967
|
const resolved2 = resolveWorkingFile(normalized, workspaceDir);
|
|
9770
9968
|
if (resolved2) {
|
|
@@ -9782,16 +9980,10 @@ function resolveMediaPath(source, log4, workspaceDir) {
|
|
|
9782
9980
|
} catch {
|
|
9783
9981
|
return { ok: false, error: `Cannot resolve path: ${resolved}` };
|
|
9784
9982
|
}
|
|
9785
|
-
const allowed = ALLOWED_MEDIA_ROOTS
|
|
9786
|
-
try {
|
|
9787
|
-
const resolvedRoot = fs9.existsSync(root) ? fs9.realpathSync(root) : root;
|
|
9788
|
-
return real.startsWith(resolvedRoot + path8.sep) || real === resolvedRoot;
|
|
9789
|
-
} catch {
|
|
9790
|
-
return false;
|
|
9791
|
-
}
|
|
9792
|
-
});
|
|
9983
|
+
const allowed = isPathInAllowedRoots(real, ALLOWED_MEDIA_ROOTS);
|
|
9793
9984
|
if (!allowed) {
|
|
9794
|
-
log4?.warn(`path not in allowed directory: ${real}`);
|
|
9985
|
+
log4?.warn(`path blocked \u2014 not in allowed directory: ${real}`);
|
|
9986
|
+
return { ok: false, error: `\u6587\u4EF6\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u7684\u76EE\u5F55\u4E2D` };
|
|
9795
9987
|
}
|
|
9796
9988
|
return { ok: true, path: real, isLocal: true };
|
|
9797
9989
|
}
|
|
@@ -9827,7 +10019,7 @@ async function sendImageMedia(gw, target, source, params) {
|
|
|
9827
10019
|
}
|
|
9828
10020
|
}
|
|
9829
10021
|
async function sendVoiceMedia(gw, target, source, params) {
|
|
9830
|
-
const voiceSource =
|
|
10022
|
+
const voiceSource = resolveVoiceSource2(source);
|
|
9831
10023
|
try {
|
|
9832
10024
|
const result = await gw.sendVoice(target, voiceSource, {
|
|
9833
10025
|
msgId: params.replyToId
|
|
@@ -9872,7 +10064,7 @@ async function sendFileMedia(gw, target, source, params) {
|
|
|
9872
10064
|
return { error: formatErr(err) };
|
|
9873
10065
|
}
|
|
9874
10066
|
}
|
|
9875
|
-
function
|
|
10067
|
+
function resolveVoiceSource2(source) {
|
|
9876
10068
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
9877
10069
|
return { url: source };
|
|
9878
10070
|
}
|
|
@@ -10170,6 +10362,31 @@ var qqbotSetupWizard = {
|
|
|
10170
10362
|
}
|
|
10171
10363
|
};
|
|
10172
10364
|
|
|
10365
|
+
// src/outbound/sanitize.ts
|
|
10366
|
+
var INTERNAL_TAGS = [
|
|
10367
|
+
// 框架脚手架标签
|
|
10368
|
+
/<system-reminder\b[^>]*>[\s\S]*?<\/system-reminder>/gi,
|
|
10369
|
+
/<previous_response\b[^>]*>[\s\S]*?<\/previous_response>/gi,
|
|
10370
|
+
/<\s*\/?\s*(?:system-reminder|previous_response)\b[^>]*\/?\s*>/gi,
|
|
10371
|
+
// 模型推理/思考内容
|
|
10372
|
+
// deepseek: `think`...`/think` — 匹配完整标签块,标签名 think 必须完整
|
|
10373
|
+
// 格式说明:deepseek 用 `` ` `` (反引号) 替代 XML 的 `<` `>` 作为标签定界符
|
|
10374
|
+
// `think` ≡ <think> 开标签
|
|
10375
|
+
// `/think` ≡ </think> 闭标签
|
|
10376
|
+
/`think`[\s\S]*?`\/think`/gi,
|
|
10377
|
+
/<\s*\/?\s*think\b[^>]*\/?\s*>/gi,
|
|
10378
|
+
// claude: <thinking>...</thinking>
|
|
10379
|
+
/<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi,
|
|
10380
|
+
/<\s*\/?\s*thinking\b[^>]*\/?\s*>/gi
|
|
10381
|
+
];
|
|
10382
|
+
function sanitizeQQBotText(text) {
|
|
10383
|
+
let result = text;
|
|
10384
|
+
for (const re of INTERNAL_TAGS) {
|
|
10385
|
+
result = result.replace(re, "");
|
|
10386
|
+
}
|
|
10387
|
+
return result.trim();
|
|
10388
|
+
}
|
|
10389
|
+
|
|
10173
10390
|
// src/gateway/qqbot-gateway.ts
|
|
10174
10391
|
var import_node_os4 = __toESM(require("os"), 1);
|
|
10175
10392
|
|
|
@@ -10652,6 +10869,7 @@ function botClearStorage(_account) {
|
|
|
10652
10869
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
10653
10870
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
10654
10871
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
10872
|
+
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
10655
10873
|
var MAX_LINES_PER_FILE = 1e3;
|
|
10656
10874
|
var MAX_FILES = 4;
|
|
10657
10875
|
var LOG_KEYWORDS = ["gateway", "openclaw", "clawdbot", "moltbot"];
|
|
@@ -10831,7 +11049,8 @@ function botLogs(runtime2) {
|
|
|
10831
11049
|
const tmpDir = getQQBotMediaDir("exports");
|
|
10832
11050
|
if (!import_node_fs5.default.existsSync(tmpDir)) import_node_fs5.default.mkdirSync(tmpDir, { recursive: true });
|
|
10833
11051
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
10834
|
-
const
|
|
11052
|
+
const suffix = import_node_crypto2.default.randomBytes(4).toString("hex");
|
|
11053
|
+
const tmpFile = import_node_path5.default.join(tmpDir, `bot-logs-${timestamp}-${suffix}.txt`);
|
|
10835
11054
|
import_node_fs5.default.writeFileSync(tmpFile, lines.join("\n"), "utf8");
|
|
10836
11055
|
let summary = `${recentFiles.length} \u4E2A\u65E5\u5FD7\u6587\u4EF6\uFF0C\u5171 ${totalIncluded} \u884C`;
|
|
10837
11056
|
if (truncatedCount > 0) summary += `\uFF08${truncatedCount} \u4E2A\u622A\u65AD\uFF0C\u539F\u59CB ${totalOriginal} \u884C\uFF09`;
|
|
@@ -10841,7 +11060,7 @@ function botLogs(runtime2) {
|
|
|
10841
11060
|
await ctx.bot.sendFile(
|
|
10842
11061
|
{ scope: "c2c", targetId: senderId, msgId: ctx.message.messageId },
|
|
10843
11062
|
{ localPath: tmpFile },
|
|
10844
|
-
{ fileName: `bot-logs-${timestamp}.txt` }
|
|
11063
|
+
{ fileName: `bot-logs-${timestamp}-${suffix}.txt` }
|
|
10845
11064
|
);
|
|
10846
11065
|
}
|
|
10847
11066
|
} catch (err) {
|
|
@@ -11436,10 +11655,10 @@ function formatDuration2(seconds) {
|
|
|
11436
11655
|
|
|
11437
11656
|
// src/adapter/media.ts
|
|
11438
11657
|
var import_node_module4 = require("module");
|
|
11439
|
-
var
|
|
11658
|
+
var dns2 = __toESM(require("dns"), 1);
|
|
11440
11659
|
var path16 = __toESM(require("path"), 1);
|
|
11441
11660
|
var fs18 = __toESM(require("fs"), 1);
|
|
11442
|
-
var
|
|
11661
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
11443
11662
|
var req3 = (0, import_node_module4.createRequire)(__filename);
|
|
11444
11663
|
var _save;
|
|
11445
11664
|
function downloadRemoteMedia(opts) {
|
|
@@ -11475,7 +11694,7 @@ function isPrivateIP(ip) {
|
|
|
11475
11694
|
return PRIVATE_RANGES.some(([mask, prefix]) => val >> 32n - BigInt(prefix) === mask >> 32n - BigInt(prefix));
|
|
11476
11695
|
}
|
|
11477
11696
|
async function assertSafeHostname(hostname) {
|
|
11478
|
-
const addresses = await
|
|
11697
|
+
const addresses = await dns2.promises.resolve4(hostname).catch(() => []);
|
|
11479
11698
|
if (addresses.length === 0) throw new Error(`DNS resolution failed: ${hostname}`);
|
|
11480
11699
|
for (const addr of addresses) {
|
|
11481
11700
|
if (isPrivateIP(addr)) {
|
|
@@ -11500,7 +11719,7 @@ async function downloadViaFetch(opts) {
|
|
|
11500
11719
|
if (buf.length > maxBytes) throw new Error(`Download exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB`);
|
|
11501
11720
|
const ext = opts.originalFilename ? path16.extname(opts.originalFilename) || ".bin" : ".bin";
|
|
11502
11721
|
const name = opts.originalFilename ? path16.basename(opts.originalFilename, path16.extname(opts.originalFilename)) : "download";
|
|
11503
|
-
const rand =
|
|
11722
|
+
const rand = crypto5.randomBytes(4).toString("hex");
|
|
11504
11723
|
const filePath = path16.join(dir, `${name}_${Date.now()}_${rand}${ext}`);
|
|
11505
11724
|
fs18.writeFileSync(filePath, buf);
|
|
11506
11725
|
return { path: filePath };
|
|
@@ -11712,10 +11931,13 @@ async function downloadMediaFile(url, filename, log4) {
|
|
|
11712
11931
|
// src/dispatch/body-assembler.ts
|
|
11713
11932
|
var QUOTE_BEGIN = "[Quoted message begins]";
|
|
11714
11933
|
var QUOTE_END = "[Quoted message ends]";
|
|
11715
|
-
var
|
|
11716
|
-
var
|
|
11717
|
-
var
|
|
11718
|
-
var
|
|
11934
|
+
var REF_BEGIN = "[Reference message begins]";
|
|
11935
|
+
var REF_END = "[Reference message ends]";
|
|
11936
|
+
var HISTORY_BEGIN = "[Chat history begins]";
|
|
11937
|
+
var HISTORY_END = "[Chat history ends]";
|
|
11938
|
+
var MERGE_CTX_BEGIN = "[Merged messages begins]";
|
|
11939
|
+
var MERGE_CTX_END = "[Merged messages ends]";
|
|
11940
|
+
var CURRENT_MSG = "[Current message]";
|
|
11719
11941
|
function assembleBody(ctx, msg, account, getRuntime) {
|
|
11720
11942
|
const rawBody = msg.content ?? "";
|
|
11721
11943
|
const isGroup = msg.kind === "group";
|
|
@@ -11728,7 +11950,7 @@ function assembleBody(ctx, msg, account, getRuntime) {
|
|
|
11728
11950
|
const userContent = buildUserContent(ctx.message.content ?? "", processed);
|
|
11729
11951
|
const quotePart = buildQuotePart(quote);
|
|
11730
11952
|
const userMessage = mergedMessages && mergedMessages.length > 0 ? buildMergedUserMessage({ messages: mergedMessages, quotePart, isGroup, wasMentioned, getRuntime }) : buildUserMessage({ msg, userContent, quotePart, isGroup, wasMentioned });
|
|
11731
|
-
const dynamicCtx = buildDynamicCtx(processed);
|
|
11953
|
+
const dynamicCtx = buildDynamicCtx(processed, msg, quote);
|
|
11732
11954
|
const agentBody = buildAgentBody({
|
|
11733
11955
|
userContent,
|
|
11734
11956
|
base: dynamicCtx + userMessage,
|
|
@@ -11754,12 +11976,11 @@ ${voiceText}` : voiceText) + attachmentInfo;
|
|
|
11754
11976
|
}
|
|
11755
11977
|
function buildQuotePart(quote) {
|
|
11756
11978
|
if (!quote) return "";
|
|
11757
|
-
const sender = quote.entry?.senderName ? `${quote.entry.senderName} (${quote.entry.senderId})` : "";
|
|
11758
|
-
const senderLine = sender ? `${sender}: ` : "";
|
|
11759
11979
|
const text = quote.text || "Original content unavailable";
|
|
11760
11980
|
return `${QUOTE_BEGIN}
|
|
11761
|
-
${
|
|
11981
|
+
${text}
|
|
11762
11982
|
${QUOTE_END}
|
|
11983
|
+
${CURRENT_MSG}
|
|
11763
11984
|
`;
|
|
11764
11985
|
}
|
|
11765
11986
|
function buildUserMessage(input) {
|
|
@@ -11767,7 +11988,7 @@ function buildUserMessage(input) {
|
|
|
11767
11988
|
const atYouTag = isGroup && wasMentioned ? " (@you)" : "";
|
|
11768
11989
|
if (isGroup) {
|
|
11769
11990
|
const senderLabel = formatSenderLabel(msg.senderName, msg.senderId);
|
|
11770
|
-
return
|
|
11991
|
+
return `${quotePart}[${senderLabel}] ${userContent}${atYouTag}`;
|
|
11771
11992
|
}
|
|
11772
11993
|
return `${quotePart}${userContent}`;
|
|
11773
11994
|
}
|
|
@@ -11793,7 +12014,7 @@ function buildMergedUserMessage(input) {
|
|
|
11793
12014
|
return lines.join("\n");
|
|
11794
12015
|
}
|
|
11795
12016
|
const last = lines.pop();
|
|
11796
|
-
return [
|
|
12017
|
+
return [MERGE_CTX_BEGIN, ...lines, MERGE_CTX_END, CURRENT_MSG, last].join("\n");
|
|
11797
12018
|
}
|
|
11798
12019
|
function formatMergedLine(ctx, opts) {
|
|
11799
12020
|
const m3 = ctx.message;
|
|
@@ -11814,13 +12035,12 @@ function allSameSender(messages) {
|
|
|
11814
12035
|
const first = messages[0]?.message.senderId;
|
|
11815
12036
|
return messages.every((c) => c.message.senderId === first);
|
|
11816
12037
|
}
|
|
11817
|
-
function buildDynamicCtx(processed) {
|
|
11818
|
-
if (!processed) return "";
|
|
12038
|
+
function buildDynamicCtx(processed, msg, quote) {
|
|
11819
12039
|
const lines = [];
|
|
11820
|
-
if (processed
|
|
12040
|
+
if (processed?.imageUrls.length) {
|
|
11821
12041
|
lines.push(`- Images: ${processed.imageUrls.join(", ")}`);
|
|
11822
12042
|
}
|
|
11823
|
-
const transcripts = processed
|
|
12043
|
+
const transcripts = processed?.transcripts ?? [];
|
|
11824
12044
|
const voiceRefs = unique([
|
|
11825
12045
|
...transcripts.map((t) => t.localPath).filter(isNonEmpty),
|
|
11826
12046
|
...transcripts.map((t) => t.remoteUrl).filter(isNonEmpty)
|
|
@@ -11834,10 +12054,35 @@ function buildDynamicCtx(processed) {
|
|
|
11834
12054
|
if (asrTexts.length > 0) {
|
|
11835
12055
|
lines.push(`- ASR: ${asrTexts.join(" | ")}`);
|
|
11836
12056
|
}
|
|
12057
|
+
if (!quote) {
|
|
12058
|
+
const elementsCtx = buildMsgElementsContext(msg);
|
|
12059
|
+
if (elementsCtx.length > 0) {
|
|
12060
|
+
lines.push(REF_BEGIN, ...elementsCtx, REF_END);
|
|
12061
|
+
}
|
|
12062
|
+
}
|
|
11837
12063
|
return lines.length > 0 ? `${lines.join("\n")}
|
|
11838
12064
|
|
|
11839
12065
|
` : "";
|
|
11840
12066
|
}
|
|
12067
|
+
function buildMsgElementsContext(msg) {
|
|
12068
|
+
const elements = msg.msgElements;
|
|
12069
|
+
if (!elements || elements.length === 0) return [];
|
|
12070
|
+
const lines = [];
|
|
12071
|
+
let index = 0;
|
|
12072
|
+
for (const el of elements) {
|
|
12073
|
+
const content = el.content?.trim();
|
|
12074
|
+
if (!content) continue;
|
|
12075
|
+
index += 1;
|
|
12076
|
+
const author = el.author;
|
|
12077
|
+
const sender = author?.username ?? "\u672A\u77E5";
|
|
12078
|
+
lines.push(
|
|
12079
|
+
`=== \u6D88\u606F ${index} ===`,
|
|
12080
|
+
`[\u6D88\u606F\u5185\u5BB9] ${content}`,
|
|
12081
|
+
`[\u53D1\u9001\u8005] ${sender}`
|
|
12082
|
+
);
|
|
12083
|
+
}
|
|
12084
|
+
return lines;
|
|
12085
|
+
}
|
|
11841
12086
|
function unique(arr) {
|
|
11842
12087
|
return [...new Set(arr)];
|
|
11843
12088
|
}
|
|
@@ -11854,7 +12099,7 @@ function buildAgentBody(input) {
|
|
|
11854
12099
|
const label = formatSenderLabel(h2.senderName, h2.senderId);
|
|
11855
12100
|
return `[${label}] ${h2.content}`;
|
|
11856
12101
|
}).join("\n");
|
|
11857
|
-
return [
|
|
12102
|
+
return [HISTORY_BEGIN, historyText, "", HISTORY_END, CURRENT_MSG, base].join("\n");
|
|
11858
12103
|
}
|
|
11859
12104
|
return base;
|
|
11860
12105
|
}
|
|
@@ -11921,8 +12166,11 @@ function getHistoryStore() {
|
|
|
11921
12166
|
if (!_store) _store = new MemoryHistoryStore();
|
|
11922
12167
|
return _store;
|
|
11923
12168
|
}
|
|
11924
|
-
function
|
|
11925
|
-
|
|
12169
|
+
function historyGroupKey(accountId, groupId) {
|
|
12170
|
+
return `${accountId}:${groupId}`;
|
|
12171
|
+
}
|
|
12172
|
+
function clearGroupHistory(accountId, groupId) {
|
|
12173
|
+
_store?.clear?.(historyGroupKey(accountId, groupId));
|
|
11926
12174
|
}
|
|
11927
12175
|
|
|
11928
12176
|
// src/middleware/access-control.ts
|
|
@@ -12015,6 +12263,14 @@ function setupMiddlewares(bot, account, opts) {
|
|
|
12015
12263
|
bot.use(errorHandler());
|
|
12016
12264
|
bot.use(messageFilter({ skipSelfEcho: false }));
|
|
12017
12265
|
bot.use(createPolicyInjector(account));
|
|
12266
|
+
bot.use(historyBuffer({
|
|
12267
|
+
store: getHistoryStore(),
|
|
12268
|
+
groupKey: (ctx) => {
|
|
12269
|
+
const gid = ctx.message.groupOpenid;
|
|
12270
|
+
if (ctx.message.kind !== "group" || !gid) return void 0;
|
|
12271
|
+
return historyGroupKey(account.accountId, gid);
|
|
12272
|
+
}
|
|
12273
|
+
}));
|
|
12018
12274
|
bot.use(dynamicAccessControl({
|
|
12019
12275
|
accountId: account.accountId,
|
|
12020
12276
|
getRuntime: opts.getRuntime
|
|
@@ -12041,7 +12297,6 @@ function setupMiddlewares(bot, account, opts) {
|
|
|
12041
12297
|
bot.use(quoteRef({
|
|
12042
12298
|
store: getPersistedRefIndexStore(account.accountId)
|
|
12043
12299
|
}));
|
|
12044
|
-
bot.use(historyBuffer({ store: getHistoryStore() }));
|
|
12045
12300
|
bot.use(attachmentProcessor({ getRuntime: opts.getRuntime }));
|
|
12046
12301
|
bot.use(envelopeFormatter({
|
|
12047
12302
|
format: (ctx) => {
|
|
@@ -12110,6 +12365,8 @@ function buildEnvelope2(ctx, msg, account) {
|
|
|
12110
12365
|
}
|
|
12111
12366
|
|
|
12112
12367
|
// src/outbound/deliver-pipeline.ts
|
|
12368
|
+
var path18 = __toESM(require("path"), 1);
|
|
12369
|
+
var os7 = __toESM(require("os"), 1);
|
|
12113
12370
|
function resolveMediaUrls(payload) {
|
|
12114
12371
|
if (payload.mediaUrls?.length) {
|
|
12115
12372
|
return payload.mediaUrls.filter(Boolean);
|
|
@@ -12158,6 +12415,7 @@ async function deliverReply(payload, _info, ctx) {
|
|
|
12158
12415
|
}
|
|
12159
12416
|
}
|
|
12160
12417
|
async function sendMediaUrls(ctx, urls) {
|
|
12418
|
+
const failedUrls = [];
|
|
12161
12419
|
for (const url of urls) {
|
|
12162
12420
|
const result = await sendMedia2({
|
|
12163
12421
|
to: ctx.qualifiedTarget,
|
|
@@ -12169,6 +12427,16 @@ async function sendMediaUrls(ctx, urls) {
|
|
|
12169
12427
|
});
|
|
12170
12428
|
if (result.error) {
|
|
12171
12429
|
ctx.log?.error(`[media] ${result.error}`);
|
|
12430
|
+
failedUrls.push(url);
|
|
12431
|
+
}
|
|
12432
|
+
}
|
|
12433
|
+
if (failedUrls.length > 0) {
|
|
12434
|
+
const count = failedUrls.length;
|
|
12435
|
+
const total = urls.length;
|
|
12436
|
+
const failMsg = count === total ? `\u26A0\uFE0F \u5A92\u4F53\u53D1\u9001\u5931\u8D25\uFF08${count} \u4E2A\uFF09\uFF0C\u8BF7\u91CD\u8BD5` : `\u26A0\uFE0F ${count}/${total} \u4E2A\u5A92\u4F53\u53D1\u9001\u5931\u8D25`;
|
|
12437
|
+
const textResult = await ctx.sendText(ctx.qualifiedTarget, failMsg);
|
|
12438
|
+
if (textResult.error) {
|
|
12439
|
+
ctx.log?.error(`[media] failed to send failure notification: ${textResult.error}`);
|
|
12172
12440
|
}
|
|
12173
12441
|
}
|
|
12174
12442
|
}
|
|
@@ -12188,6 +12456,10 @@ async function handleVoiceIntent(text, ctx) {
|
|
|
12188
12456
|
ctx.log?.error(`[tts] TTS failed: ${ttsResult.error ?? "no audio path returned"}`);
|
|
12189
12457
|
return false;
|
|
12190
12458
|
}
|
|
12459
|
+
if (!isTtsPathSafe(ttsResult.audioPath)) {
|
|
12460
|
+
ctx.log?.error(`[tts] TTS audio path blocked: ${ttsResult.audioPath}`);
|
|
12461
|
+
return false;
|
|
12462
|
+
}
|
|
12191
12463
|
if (ctx.audioFileToSilkBase64) {
|
|
12192
12464
|
const silkBase64 = await ctx.audioFileToSilkBase64(ttsResult.audioPath);
|
|
12193
12465
|
if (silkBase64) {
|
|
@@ -12208,6 +12480,86 @@ async function handleVoiceIntent(text, ctx) {
|
|
|
12208
12480
|
return false;
|
|
12209
12481
|
}
|
|
12210
12482
|
}
|
|
12483
|
+
var TTS_ALLOWED_ROOTS = [
|
|
12484
|
+
path18.join(os7.homedir(), ".openclaw"),
|
|
12485
|
+
os7.tmpdir(),
|
|
12486
|
+
"/tmp"
|
|
12487
|
+
];
|
|
12488
|
+
function isTtsPathSafe(audioPath) {
|
|
12489
|
+
if (!path18.isAbsolute(audioPath)) return false;
|
|
12490
|
+
return isPathInAllowedRoots(audioPath, TTS_ALLOWED_ROOTS);
|
|
12491
|
+
}
|
|
12492
|
+
|
|
12493
|
+
// src/dispatch/ctx-builder.ts
|
|
12494
|
+
function buildCtxPayload(params) {
|
|
12495
|
+
const { assembled, envelope, route, msg, ctx, adapters } = params;
|
|
12496
|
+
const isSlashCommand = /^\//.test(assembled.rawBody ?? "");
|
|
12497
|
+
const convKind = envelope.chatScope === "group" ? "group" : "direct";
|
|
12498
|
+
const peerId = convKind === "group" ? envelope.groupId ?? envelope.senderId : envelope.senderId;
|
|
12499
|
+
const groupId = convKind === "group" ? envelope.groupId : void 0;
|
|
12500
|
+
const processed = ctx.state.processedAttachments;
|
|
12501
|
+
const voicePaths = processed?.localMediaPaths?.filter((_2, i) => processed.localMediaTypes?.[i]?.startsWith("audio/")) ?? [];
|
|
12502
|
+
const voiceUrls = processed?.remoteMediaUrls?.filter((_2, i) => processed.remoteMediaUrls?.[i]?.startsWith?.("audio/")) ?? [];
|
|
12503
|
+
const msgTimestamp = msg.timestamp ?? msg.Timestamp;
|
|
12504
|
+
return adapters.buildInboundContext?.({
|
|
12505
|
+
channel: "qqbot",
|
|
12506
|
+
accountId: route.accountId,
|
|
12507
|
+
provider: "qqbot",
|
|
12508
|
+
surface: "qqbot",
|
|
12509
|
+
messageId: envelope.messageId,
|
|
12510
|
+
timestamp: msgTimestamp ? new Date(msgTimestamp).getTime() : Date.now(),
|
|
12511
|
+
from: envelope.targetId,
|
|
12512
|
+
sender: { id: envelope.senderId, name: envelope.senderName },
|
|
12513
|
+
conversation: {
|
|
12514
|
+
kind: convKind,
|
|
12515
|
+
id: peerId,
|
|
12516
|
+
label: assembled.systemPrompt
|
|
12517
|
+
},
|
|
12518
|
+
message: {
|
|
12519
|
+
body: assembled.webBody,
|
|
12520
|
+
bodyForAgent: assembled.agentBody,
|
|
12521
|
+
rawBody: assembled.rawBody,
|
|
12522
|
+
commandBody: assembled.rawBody
|
|
12523
|
+
},
|
|
12524
|
+
route: {
|
|
12525
|
+
agentId: route.agentId ?? "default",
|
|
12526
|
+
routeSessionKey: route.sessionKey,
|
|
12527
|
+
accountId: route.accountId
|
|
12528
|
+
},
|
|
12529
|
+
reply: {
|
|
12530
|
+
to: envelope.targetId,
|
|
12531
|
+
replyToId: envelope.messageId,
|
|
12532
|
+
originatingTo: envelope.targetId
|
|
12533
|
+
},
|
|
12534
|
+
access: {
|
|
12535
|
+
commands: { authorized: isSlashCommand }
|
|
12536
|
+
},
|
|
12537
|
+
command: isSlashCommand ? { kind: "text-slash", body: assembled.rawBody, authorized: true } : void 0,
|
|
12538
|
+
media: voicePaths.length > 0 ? voicePaths.map((p2, i) => ({
|
|
12539
|
+
contentType: processed?.localMediaTypes?.[i] ?? "audio/silk",
|
|
12540
|
+
localPath: p2,
|
|
12541
|
+
url: voiceUrls[i]
|
|
12542
|
+
})) : voiceUrls.length > 0 ? voiceUrls.map((u2) => ({ contentType: "audio/wav", url: u2 })) : void 0,
|
|
12543
|
+
supplemental: {
|
|
12544
|
+
quote: envelope.quote ? { id: envelope.messageId, body: envelope.quote.content, sender: envelope.quote.senderId } : void 0,
|
|
12545
|
+
groupSystemPrompt: envelope.systemPrompt
|
|
12546
|
+
},
|
|
12547
|
+
extra: {
|
|
12548
|
+
...isSlashCommand ? { CommandSource: "text" } : {},
|
|
12549
|
+
...groupId ? { QQGroupOpenid: groupId } : {},
|
|
12550
|
+
...processed?.localMediaPaths?.length ? {
|
|
12551
|
+
MediaPaths: processed.localMediaPaths,
|
|
12552
|
+
MediaPath: processed.localMediaPaths[0],
|
|
12553
|
+
MediaTypes: processed.localMediaTypes,
|
|
12554
|
+
MediaType: processed.localMediaTypes?.[0]
|
|
12555
|
+
} : {},
|
|
12556
|
+
...processed?.remoteMediaUrls?.length ? {
|
|
12557
|
+
MediaUrls: processed.remoteMediaUrls,
|
|
12558
|
+
MediaUrl: processed.remoteMediaUrls[0]
|
|
12559
|
+
} : {}
|
|
12560
|
+
}
|
|
12561
|
+
});
|
|
12562
|
+
}
|
|
12211
12563
|
|
|
12212
12564
|
// src/outbound/debounce.ts
|
|
12213
12565
|
var DEFAULT_WINDOW_MS = 1500;
|
|
@@ -12446,46 +12798,7 @@ async function dispatchToOpenClaw(ctx, msg, account, runtime2, log4) {
|
|
|
12446
12798
|
const qualifiedTarget = envelope.targetId;
|
|
12447
12799
|
const agentId = route.agentId ?? "default";
|
|
12448
12800
|
const storePath = adapters.resolveStorePath?.(cfg?.session?.store, { agentId }) ?? "";
|
|
12449
|
-
const ctxPayload =
|
|
12450
|
-
channel: "qqbot",
|
|
12451
|
-
accountId: route.accountId,
|
|
12452
|
-
provider: "qqbot",
|
|
12453
|
-
surface: "qqbot",
|
|
12454
|
-
messageId: envelope.messageId,
|
|
12455
|
-
timestamp: Date.now(),
|
|
12456
|
-
from: envelope.targetId,
|
|
12457
|
-
sender: { id: envelope.senderId, name: envelope.senderName },
|
|
12458
|
-
conversation: {
|
|
12459
|
-
kind: envelope.chatScope === "group" ? "group" : "direct",
|
|
12460
|
-
label: assembled.systemPrompt
|
|
12461
|
-
},
|
|
12462
|
-
message: {
|
|
12463
|
-
body: assembled.webBody,
|
|
12464
|
-
bodyForAgent: assembled.agentBody,
|
|
12465
|
-
rawBody: assembled.rawBody,
|
|
12466
|
-
commandBody: assembled.rawBody
|
|
12467
|
-
},
|
|
12468
|
-
route: {
|
|
12469
|
-
routeSessionKey: route.sessionKey,
|
|
12470
|
-
accountId: route.accountId
|
|
12471
|
-
},
|
|
12472
|
-
reply: {
|
|
12473
|
-
to: envelope.targetId,
|
|
12474
|
-
replyToId: envelope.messageId,
|
|
12475
|
-
originatingTo: envelope.targetId
|
|
12476
|
-
}
|
|
12477
|
-
});
|
|
12478
|
-
const processed = ctx.state.processedAttachments;
|
|
12479
|
-
if (processed?.localMediaPaths?.length) {
|
|
12480
|
-
ctxPayload.MediaPaths = processed.localMediaPaths;
|
|
12481
|
-
ctxPayload.MediaPath = processed.localMediaPaths[0];
|
|
12482
|
-
ctxPayload.MediaTypes = processed.localMediaTypes;
|
|
12483
|
-
ctxPayload.MediaType = processed.localMediaTypes?.[0];
|
|
12484
|
-
}
|
|
12485
|
-
if (processed?.remoteMediaUrls?.length) {
|
|
12486
|
-
ctxPayload.MediaUrls = processed.remoteMediaUrls;
|
|
12487
|
-
ctxPayload.MediaUrl = processed.remoteMediaUrls[0];
|
|
12488
|
-
}
|
|
12801
|
+
const ctxPayload = buildCtxPayload({ assembled, envelope, route, msg, ctx, adapters });
|
|
12489
12802
|
const ttsRuntime = runtime2?.tts ?? runtime2?.channel?.runtimeContexts?.get?.("tts");
|
|
12490
12803
|
const debounceConfig = account.config?.deliverDebounce;
|
|
12491
12804
|
const debouncer = debounceConfig?.enabled !== false ? new DeliverDebouncer(
|
|
@@ -12679,7 +12992,7 @@ async function dispatchToOpenClaw(ctx, msg, account, runtime2, log4) {
|
|
|
12679
12992
|
}
|
|
12680
12993
|
dlog?.debug(`inboundRun completed sessionKey=${route.sessionKey}`);
|
|
12681
12994
|
if (envelope.chatScope === "group") {
|
|
12682
|
-
clearGroupHistory(envelope.groupId ?? envelope.senderId);
|
|
12995
|
+
clearGroupHistory(account.accountId, envelope.groupId ?? envelope.senderId);
|
|
12683
12996
|
}
|
|
12684
12997
|
if (streamingController && !streamingController.isTerminal) {
|
|
12685
12998
|
await streamingController.finalize();
|
|
@@ -13020,10 +13333,10 @@ function getApprovalHandler(accountId) {
|
|
|
13020
13333
|
|
|
13021
13334
|
// src/features/proactive.ts
|
|
13022
13335
|
var fs19 = __toESM(require("fs"), 1);
|
|
13023
|
-
var
|
|
13336
|
+
var path20 = __toESM(require("path"), 1);
|
|
13024
13337
|
var log2 = createPluginLogger({ prefix: "[proactive]" });
|
|
13025
13338
|
var STORAGE_DIR = getQQBotDataDir("data");
|
|
13026
|
-
var KNOWN_USERS_FILE =
|
|
13339
|
+
var KNOWN_USERS_FILE = path20.join(STORAGE_DIR, "known-users.json");
|
|
13027
13340
|
var knownUsersCache = null;
|
|
13028
13341
|
var cacheLastModified = 0;
|
|
13029
13342
|
function ensureStorageDir() {
|
|
@@ -13227,6 +13540,11 @@ async function handleApproval(event, account, log4, ack) {
|
|
|
13227
13540
|
}
|
|
13228
13541
|
const buttonData = event.data?.resolved?.button_data;
|
|
13229
13542
|
if (!buttonData?.startsWith("approve:")) return;
|
|
13543
|
+
const operatorId = resolveOperatorId(event);
|
|
13544
|
+
if (!isApprovalAuthorized(account, operatorId)) {
|
|
13545
|
+
log4.warn(`[approval] unauthorized operator=${operatorId ?? "unknown"} account=${account.accountId}`);
|
|
13546
|
+
return;
|
|
13547
|
+
}
|
|
13230
13548
|
const parts = buttonData.split(":");
|
|
13231
13549
|
if (parts.length < 3) return;
|
|
13232
13550
|
const handler = getApprovalHandler(account.accountId);
|
|
@@ -13236,10 +13554,20 @@ async function handleApproval(event, account, log4, ack) {
|
|
|
13236
13554
|
try {
|
|
13237
13555
|
await handler.resolveApproval(approvalId, decision);
|
|
13238
13556
|
} catch (err) {
|
|
13239
|
-
log4.error(`interaction approve error: ${err}`);
|
|
13557
|
+
log4.error(`interaction approve error: ${err instanceof Error ? err.message : String(err)}`);
|
|
13240
13558
|
}
|
|
13241
13559
|
}
|
|
13242
13560
|
var CHANNEL_VER = getPackageVersion();
|
|
13561
|
+
function resolveOperatorId(event) {
|
|
13562
|
+
const evt = event;
|
|
13563
|
+
return evt.user_openid ?? evt.data?.resolved?.user_id ?? evt.data?.resolved?.user_openid ?? evt.openid;
|
|
13564
|
+
}
|
|
13565
|
+
function isApprovalAuthorized(account, operatorId) {
|
|
13566
|
+
if (!operatorId) return false;
|
|
13567
|
+
const allowFrom = account.config?.allowFrom ?? [];
|
|
13568
|
+
if (!allowFrom.length || allowFrom.includes("*")) return true;
|
|
13569
|
+
return allowFrom.includes(operatorId);
|
|
13570
|
+
}
|
|
13243
13571
|
function buildClawCfg(requireMention, mentionPatterns, groupPolicy) {
|
|
13244
13572
|
return {
|
|
13245
13573
|
channel_type: "qqbot",
|
|
@@ -13269,13 +13597,13 @@ function createPluginWebhookAdapter(params) {
|
|
|
13269
13597
|
let storedPath = "";
|
|
13270
13598
|
const accountId = params.account.accountId;
|
|
13271
13599
|
return {
|
|
13272
|
-
async listen(_port,
|
|
13600
|
+
async listen(_port, path22, handler) {
|
|
13273
13601
|
const ingress = await import("openclaw/plugin-sdk/webhook-ingress").catch(() => null);
|
|
13274
13602
|
if (!ingress?.registerWebhookTargetWithPluginRoute) {
|
|
13275
13603
|
params.log.error("Webhook ingress not available");
|
|
13276
13604
|
return;
|
|
13277
13605
|
}
|
|
13278
|
-
const webhookPath = storedPath =
|
|
13606
|
+
const webhookPath = storedPath = path22 && path22 !== "/" ? path22 : params.account.config.webhook?.path ?? "/qqbot/webhook";
|
|
13279
13607
|
const existing = sharedTargets.get(webhookPath);
|
|
13280
13608
|
if (existing) {
|
|
13281
13609
|
const dupIdx = existing.findIndex((t) => t.accountId === accountId);
|
|
@@ -13322,12 +13650,12 @@ function createPluginWebhookAdapter(params) {
|
|
|
13322
13650
|
}
|
|
13323
13651
|
};
|
|
13324
13652
|
}
|
|
13325
|
-
function createSharedHandler(
|
|
13653
|
+
function createSharedHandler(path22, log4) {
|
|
13326
13654
|
return async (req4, res) => {
|
|
13327
13655
|
try {
|
|
13328
13656
|
const ingress = await import("openclaw/plugin-sdk/webhook-ingress").catch(() => null);
|
|
13329
13657
|
if (!ingress?.withResolvedWebhookRequestPipeline) {
|
|
13330
|
-
await handleSimple(req4, res,
|
|
13658
|
+
await handleSimple(req4, res, path22, log4);
|
|
13331
13659
|
return;
|
|
13332
13660
|
}
|
|
13333
13661
|
await ingress.withResolvedWebhookRequestPipeline({
|
|
@@ -13368,12 +13696,12 @@ function createSharedHandler(path21, log4) {
|
|
|
13368
13696
|
if (payload.op === 13) {
|
|
13369
13697
|
const t = targets[0];
|
|
13370
13698
|
if (!t) {
|
|
13371
|
-
log4.warn?.(`[webhook] op:13 no target on ${
|
|
13699
|
+
log4.warn?.(`[webhook] op:13 no target on ${path22}`);
|
|
13372
13700
|
res.statusCode = 500;
|
|
13373
13701
|
res.end(JSON.stringify({ error: "no target" }));
|
|
13374
13702
|
return;
|
|
13375
13703
|
}
|
|
13376
|
-
const h3 = resolveTargetHandler(
|
|
13704
|
+
const h3 = resolveTargetHandler(path22, t.accountId);
|
|
13377
13705
|
if (!h3) {
|
|
13378
13706
|
log4.warn?.(`[webhook] op:13 no handler for ${t.accountId}`);
|
|
13379
13707
|
res.statusCode = 500;
|
|
@@ -13404,10 +13732,10 @@ function createSharedHandler(path21, log4) {
|
|
|
13404
13732
|
unauthorizedMessage: JSON.stringify({ error: "invalid signature" })
|
|
13405
13733
|
});
|
|
13406
13734
|
if (!matched) {
|
|
13407
|
-
log4.warn?.(`[webhook] signature mismatch on ${
|
|
13735
|
+
log4.warn?.(`[webhook] signature mismatch on ${path22} (${targets.length} target(s))`);
|
|
13408
13736
|
return;
|
|
13409
13737
|
}
|
|
13410
|
-
const h2 = resolveTargetHandler(
|
|
13738
|
+
const h2 = resolveTargetHandler(path22, matched.accountId);
|
|
13411
13739
|
if (!h2) {
|
|
13412
13740
|
log4.error?.(`[webhook] no handler for matched target ${matched.accountId}`);
|
|
13413
13741
|
res.statusCode = 500;
|
|
@@ -13427,8 +13755,8 @@ function createSharedHandler(path21, log4) {
|
|
|
13427
13755
|
}
|
|
13428
13756
|
};
|
|
13429
13757
|
}
|
|
13430
|
-
function resolveTargetHandler(
|
|
13431
|
-
return sharedTargets.get(
|
|
13758
|
+
function resolveTargetHandler(path22, accountId) {
|
|
13759
|
+
return sharedTargets.get(path22)?.find((t) => t.accountId === accountId)?.handler;
|
|
13432
13760
|
}
|
|
13433
13761
|
async function delegateToHandler(handler, req4, res, rawBody) {
|
|
13434
13762
|
const headers = {};
|
|
@@ -13442,7 +13770,7 @@ async function delegateToHandler(handler, req4, res, rawBody) {
|
|
|
13442
13770
|
}
|
|
13443
13771
|
res.end(resp.body);
|
|
13444
13772
|
}
|
|
13445
|
-
async function handleSimple(req4, res,
|
|
13773
|
+
async function handleSimple(req4, res, path22, log4) {
|
|
13446
13774
|
try {
|
|
13447
13775
|
const ct = String(req4.headers["content-type"] ?? "");
|
|
13448
13776
|
if (!ct.includes("application/json")) {
|
|
@@ -13463,7 +13791,7 @@ async function handleSimple(req4, res, path21, log4) {
|
|
|
13463
13791
|
chunks.push(buf);
|
|
13464
13792
|
}
|
|
13465
13793
|
const rawBody = Buffer.concat(chunks);
|
|
13466
|
-
const entries = sharedTargets.get(
|
|
13794
|
+
const entries = sharedTargets.get(path22) ?? [];
|
|
13467
13795
|
for (const entry of entries) {
|
|
13468
13796
|
const resp = await entry.handler({ body: rawBody, headers: mapHeaders(req4) });
|
|
13469
13797
|
if (resp.status < 400) {
|
|
@@ -13494,12 +13822,35 @@ function getHeader2(req4, key) {
|
|
|
13494
13822
|
}
|
|
13495
13823
|
|
|
13496
13824
|
// src/gateway/qqbot-gateway.ts
|
|
13825
|
+
var TEXT_TIMEOUT_MS = 3e4;
|
|
13826
|
+
var MEDIA_TIMEOUT_MS = 3e5;
|
|
13827
|
+
function resolveMs(envKey, defaultMs) {
|
|
13828
|
+
const env = process.env[envKey];
|
|
13829
|
+
if (env) {
|
|
13830
|
+
const v = Number(env);
|
|
13831
|
+
if (!Number.isNaN(v) && v > 0) return v;
|
|
13832
|
+
}
|
|
13833
|
+
return defaultMs;
|
|
13834
|
+
}
|
|
13835
|
+
function withTimeout(promise, ms, label) {
|
|
13836
|
+
if (ms <= 0) return promise;
|
|
13837
|
+
return Promise.race([
|
|
13838
|
+
promise,
|
|
13839
|
+
new Promise(
|
|
13840
|
+
(_2, reject) => setTimeout(() => reject(new Error(`\u51FA\u7AD9\u8D85\u65F6: ${label} (${ms}ms)`)), ms)
|
|
13841
|
+
)
|
|
13842
|
+
]);
|
|
13843
|
+
}
|
|
13497
13844
|
var QQBotGateway = class {
|
|
13498
13845
|
bot;
|
|
13499
13846
|
account;
|
|
13500
13847
|
runtime;
|
|
13501
13848
|
log;
|
|
13849
|
+
textTimeout;
|
|
13850
|
+
mediaTimeout;
|
|
13502
13851
|
constructor(account, runtime2, log4) {
|
|
13852
|
+
this.textTimeout = resolveMs("OPENCLAW_OUTBOUND_TIMEOUT_MS", TEXT_TIMEOUT_MS);
|
|
13853
|
+
this.mediaTimeout = resolveMs("OPENCLAW_OUTBOUND_MEDIA_TIMEOUT_MS", MEDIA_TIMEOUT_MS);
|
|
13503
13854
|
this.account = account;
|
|
13504
13855
|
this.runtime = runtime2;
|
|
13505
13856
|
this.log = log4 ?? createPluginLogger({ prefix: `[qqbot:${account.accountId}]` });
|
|
@@ -13564,69 +13915,66 @@ var QQBotGateway = class {
|
|
|
13564
13915
|
await this.bot.stop();
|
|
13565
13916
|
}
|
|
13566
13917
|
async sendText(target, text, opts) {
|
|
13567
|
-
return
|
|
13918
|
+
return withTimeout(
|
|
13919
|
+
this.bot.sendText(attachMsgId(target, opts), text),
|
|
13920
|
+
this.textTimeout,
|
|
13921
|
+
"sendText"
|
|
13922
|
+
);
|
|
13568
13923
|
}
|
|
13569
13924
|
async sendMedia(target, source, opts) {
|
|
13570
13925
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13571
13926
|
const fileType = opts?.fileType ?? MediaFileType.IMAGE;
|
|
13572
13927
|
const sourceOpts = resolveMediaSource(source);
|
|
13573
|
-
const result = await
|
|
13574
|
-
target: resolvedTarget,
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
});
|
|
13928
|
+
const result = await withTimeout(
|
|
13929
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType, ...sourceOpts, content: opts?.text }),
|
|
13930
|
+
this.mediaTimeout,
|
|
13931
|
+
"sendMedia"
|
|
13932
|
+
);
|
|
13579
13933
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13580
13934
|
}
|
|
13581
13935
|
async sendVoice(target, source, opts) {
|
|
13582
13936
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13583
13937
|
if (source.base64) {
|
|
13584
|
-
const result2 = await
|
|
13585
|
-
target: resolvedTarget,
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
});
|
|
13938
|
+
const result2 = await withTimeout(
|
|
13939
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.VOICE, fileData: source.base64, content: opts?.text }),
|
|
13940
|
+
this.mediaTimeout,
|
|
13941
|
+
"sendVoice(base64)"
|
|
13942
|
+
);
|
|
13590
13943
|
return result2.message ?? { id: "", timestamp: Date.now() };
|
|
13591
13944
|
}
|
|
13592
13945
|
if (source.localPath) {
|
|
13593
|
-
const result2 = await
|
|
13594
|
-
target: resolvedTarget,
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
});
|
|
13946
|
+
const result2 = await withTimeout(
|
|
13947
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.VOICE, localPath: source.localPath, content: opts?.text }),
|
|
13948
|
+
this.mediaTimeout,
|
|
13949
|
+
"sendVoice(path)"
|
|
13950
|
+
);
|
|
13599
13951
|
return result2.message ?? { id: "", timestamp: Date.now() };
|
|
13600
13952
|
}
|
|
13601
|
-
const result = await
|
|
13602
|
-
target: resolvedTarget,
|
|
13603
|
-
|
|
13604
|
-
url
|
|
13605
|
-
|
|
13606
|
-
});
|
|
13953
|
+
const result = await withTimeout(
|
|
13954
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.VOICE, url: source.url, content: opts?.text }),
|
|
13955
|
+
this.mediaTimeout,
|
|
13956
|
+
"sendVoice(url)"
|
|
13957
|
+
);
|
|
13607
13958
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13608
13959
|
}
|
|
13609
13960
|
async sendVideo(target, source, opts) {
|
|
13610
13961
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13611
13962
|
const sourceOpts = resolveMediaSource(source);
|
|
13612
|
-
const result = await
|
|
13613
|
-
target: resolvedTarget,
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
});
|
|
13963
|
+
const result = await withTimeout(
|
|
13964
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.VIDEO, ...sourceOpts, content: opts?.text }),
|
|
13965
|
+
this.mediaTimeout,
|
|
13966
|
+
"sendVideo"
|
|
13967
|
+
);
|
|
13618
13968
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13619
13969
|
}
|
|
13620
13970
|
async sendFile(target, source, opts) {
|
|
13621
13971
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13622
13972
|
const sourceOpts = resolveMediaSource(source);
|
|
13623
|
-
const result = await
|
|
13624
|
-
target: resolvedTarget,
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
content: opts?.text
|
|
13629
|
-
});
|
|
13973
|
+
const result = await withTimeout(
|
|
13974
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.FILE, ...sourceOpts, fileName: opts?.fileName, content: opts?.text }),
|
|
13975
|
+
this.mediaTimeout,
|
|
13976
|
+
"sendFile"
|
|
13977
|
+
);
|
|
13630
13978
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13631
13979
|
}
|
|
13632
13980
|
openStream(target, msgId) {
|
|
@@ -13993,6 +14341,11 @@ function stripMentionText(text, mentions) {
|
|
|
13993
14341
|
|
|
13994
14342
|
// src/channel.ts
|
|
13995
14343
|
var TEXT_CHUNK_LIMIT2 = 5e3;
|
|
14344
|
+
var GFM_TABLE_DATA_RE = /^\|.+\|.*\|/;
|
|
14345
|
+
var GFM_TABLE_SEP_RE = /^\|[\s:-]+\|/;
|
|
14346
|
+
function isGfmTableLine(line) {
|
|
14347
|
+
return GFM_TABLE_DATA_RE.test(line) || GFM_TABLE_SEP_RE.test(line);
|
|
14348
|
+
}
|
|
13996
14349
|
var qqbotPlugin = {
|
|
13997
14350
|
id: "qqbot",
|
|
13998
14351
|
meta: {
|
|
@@ -14108,13 +14461,33 @@ var qqbotPlugin = {
|
|
|
14108
14461
|
// ── 出站 ──
|
|
14109
14462
|
outbound: {
|
|
14110
14463
|
deliveryMode: "direct",
|
|
14464
|
+
sanitizeText: ({ text }) => sanitizeQQBotText(text),
|
|
14111
14465
|
chunker: (text, limit) => {
|
|
14112
14466
|
const adapters = getAdapters(getQQBotRuntime());
|
|
14113
14467
|
if (adapters.chunkMarkdownText) return adapters.chunkMarkdownText(text, limit);
|
|
14114
14468
|
const lines = text.split("\n");
|
|
14115
14469
|
const chunks = [];
|
|
14116
14470
|
let current = "";
|
|
14471
|
+
let tableBuffer = [];
|
|
14472
|
+
const flushTable = () => {
|
|
14473
|
+
if (tableBuffer.length === 0) return;
|
|
14474
|
+
const tableBlock = tableBuffer.join("\n");
|
|
14475
|
+
const candidate = current ? `${current}
|
|
14476
|
+
${tableBlock}` : tableBlock;
|
|
14477
|
+
if (candidate.length > limit && current) {
|
|
14478
|
+
chunks.push(current);
|
|
14479
|
+
current = tableBlock;
|
|
14480
|
+
} else {
|
|
14481
|
+
current = candidate;
|
|
14482
|
+
}
|
|
14483
|
+
tableBuffer = [];
|
|
14484
|
+
};
|
|
14117
14485
|
for (const line of lines) {
|
|
14486
|
+
if (isGfmTableLine(line)) {
|
|
14487
|
+
tableBuffer.push(line);
|
|
14488
|
+
continue;
|
|
14489
|
+
}
|
|
14490
|
+
flushTable();
|
|
14118
14491
|
const candidate = current ? `${current}
|
|
14119
14492
|
${line}` : line;
|
|
14120
14493
|
if (candidate.length > limit && current) {
|
|
@@ -14124,6 +14497,7 @@ ${line}` : line;
|
|
|
14124
14497
|
current = candidate;
|
|
14125
14498
|
}
|
|
14126
14499
|
}
|
|
14500
|
+
flushTable();
|
|
14127
14501
|
if (current) chunks.push(current);
|
|
14128
14502
|
return chunks.length > 0 ? chunks : [text];
|
|
14129
14503
|
},
|
|
@@ -14261,10 +14635,10 @@ function json(data) {
|
|
|
14261
14635
|
details: data
|
|
14262
14636
|
};
|
|
14263
14637
|
}
|
|
14264
|
-
function validatePath(
|
|
14265
|
-
if (!
|
|
14266
|
-
if (
|
|
14267
|
-
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(
|
|
14638
|
+
function validatePath(path22) {
|
|
14639
|
+
if (!path22.startsWith("/")) return "path \u5FC5\u987B\u4EE5 / \u5F00\u5934";
|
|
14640
|
+
if (path22.includes("..") || path22.includes("//")) return "path \u4E0D\u5141\u8BB8\u5305\u542B .. \u6216 //";
|
|
14641
|
+
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path22) && path22 !== "/") {
|
|
14268
14642
|
return "path \u5305\u542B\u975E\u6CD5\u5B57\u7B26";
|
|
14269
14643
|
}
|
|
14270
14644
|
return null;
|
|
@@ -14572,7 +14946,7 @@ function verifyRuntimeContract(rt) {
|
|
|
14572
14946
|
for (const r of OPTIONAL) {
|
|
14573
14947
|
if (!r.probe(rt)) degraded.push(r.name);
|
|
14574
14948
|
}
|
|
14575
|
-
log3.
|
|
14949
|
+
log3.debug(`openclaw=${version} required=${REQUIRED.length - missing.length}/${REQUIRED.length} degraded=${degraded.length}/${OPTIONAL.length}`);
|
|
14576
14950
|
if (missing.length) {
|
|
14577
14951
|
log3.error(`BROKEN \u2014 missing: ${missing.join(", ")}`);
|
|
14578
14952
|
}
|