@cxyhhhhh/openclaw-qqbot 2.0.0-dev.202607091556 → 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 +470 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/upgrade-via-npm.sh +41 -10
- package/skills/qqbot-group/SKILL.md +75 -0
- package/skills/qqbot-group/references/api_reference.md +94 -0
- package/src/adapter/contract.ts +1 -1
- package/src/channel.ts +44 -2
- package/src/commands/bot-logs.ts +4 -2
- package/src/gateway/event-handlers.ts +33 -1
- 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
|
}
|
|
@@ -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") {
|
|
@@ -9675,6 +9760,63 @@ function resolveAgentWorkspace(cfg, agentId) {
|
|
|
9675
9760
|
return getQQBotMediaDir();
|
|
9676
9761
|
}
|
|
9677
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
|
+
|
|
9678
9820
|
// src/outbound/local-file-router.ts
|
|
9679
9821
|
var path7 = __toESM(require("path"), 1);
|
|
9680
9822
|
var fs8 = __toESM(require("fs"), 1);
|
|
@@ -9709,6 +9851,9 @@ function isLocalFilePath(source) {
|
|
|
9709
9851
|
if (source.startsWith(".\\") || source.startsWith("..\\")) return true;
|
|
9710
9852
|
return false;
|
|
9711
9853
|
}
|
|
9854
|
+
function isDataUrl(source) {
|
|
9855
|
+
return source.startsWith("data:");
|
|
9856
|
+
}
|
|
9712
9857
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".ico"]);
|
|
9713
9858
|
var VOICE_EXTS = /* @__PURE__ */ new Set([".wav", ".mp3", ".silk", ".amr", ".ogg", ".flac", ".aac", ".m4a"]);
|
|
9714
9859
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv"]);
|
|
@@ -9726,14 +9871,48 @@ function inferMediaKindFromMime(mime) {
|
|
|
9726
9871
|
if (lower.startsWith("video/")) return "video";
|
|
9727
9872
|
return "file";
|
|
9728
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
|
+
}
|
|
9729
9890
|
|
|
9730
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
|
+
}
|
|
9731
9907
|
var ALLOWED_MEDIA_ROOTS = [
|
|
9732
9908
|
path8.join(os4.homedir(), ".openclaw", "media"),
|
|
9733
9909
|
path8.join(os4.homedir(), ".openclaw", "workspace"),
|
|
9734
9910
|
// 框架 outbound 目录(saveMediaBuffer 写入)
|
|
9735
|
-
path8.join(os4.homedir(), ".openclaw", "outbound")
|
|
9911
|
+
path8.join(os4.homedir(), ".openclaw", "outbound"),
|
|
9912
|
+
// TTS 语音临时目录(deliver-pipeline 已通过 isTtsPathSafe 预检)
|
|
9913
|
+
...resolveTempRoots()
|
|
9736
9914
|
];
|
|
9915
|
+
var MAX_DATA_URL_BYTES = 10 * 1024 * 1024;
|
|
9737
9916
|
async function sendMedia2(params) {
|
|
9738
9917
|
const { source, accountId, log: log4 } = params;
|
|
9739
9918
|
const mlog = log4?.child("media");
|
|
@@ -9743,7 +9922,7 @@ async function sendMedia2(params) {
|
|
|
9743
9922
|
}
|
|
9744
9923
|
const wsDir = resolveWorkspaceFromAgent(params.agentId);
|
|
9745
9924
|
mlog?.debug(`resolveMediaPath source=${source} agentId=${params.agentId ?? "none"} workspaceDir=${wsDir ?? "none"}`);
|
|
9746
|
-
const resolved = resolveMediaPath(source, mlog, wsDir);
|
|
9925
|
+
const resolved = await resolveMediaPath(source, mlog, wsDir);
|
|
9747
9926
|
if (!resolved.ok) {
|
|
9748
9927
|
mlog?.error(`resolveMediaPath failed: ${resolved.error}`);
|
|
9749
9928
|
return { error: resolved.error };
|
|
@@ -9766,8 +9945,24 @@ async function sendMedia2(params) {
|
|
|
9766
9945
|
return sendImageMedia(gw, target, resolved.path, params);
|
|
9767
9946
|
}
|
|
9768
9947
|
}
|
|
9769
|
-
function resolveMediaPath(source, log4, workspaceDir) {
|
|
9948
|
+
async function resolveMediaPath(source, log4, workspaceDir) {
|
|
9770
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
|
+
}
|
|
9771
9966
|
if (!isLocalFilePath(normalized)) {
|
|
9772
9967
|
const resolved2 = resolveWorkingFile(normalized, workspaceDir);
|
|
9773
9968
|
if (resolved2) {
|
|
@@ -9785,16 +9980,10 @@ function resolveMediaPath(source, log4, workspaceDir) {
|
|
|
9785
9980
|
} catch {
|
|
9786
9981
|
return { ok: false, error: `Cannot resolve path: ${resolved}` };
|
|
9787
9982
|
}
|
|
9788
|
-
const allowed = ALLOWED_MEDIA_ROOTS
|
|
9789
|
-
try {
|
|
9790
|
-
const resolvedRoot = fs9.existsSync(root) ? fs9.realpathSync(root) : root;
|
|
9791
|
-
return real.startsWith(resolvedRoot + path8.sep) || real === resolvedRoot;
|
|
9792
|
-
} catch {
|
|
9793
|
-
return false;
|
|
9794
|
-
}
|
|
9795
|
-
});
|
|
9983
|
+
const allowed = isPathInAllowedRoots(real, ALLOWED_MEDIA_ROOTS);
|
|
9796
9984
|
if (!allowed) {
|
|
9797
|
-
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` };
|
|
9798
9987
|
}
|
|
9799
9988
|
return { ok: true, path: real, isLocal: true };
|
|
9800
9989
|
}
|
|
@@ -9830,7 +10019,7 @@ async function sendImageMedia(gw, target, source, params) {
|
|
|
9830
10019
|
}
|
|
9831
10020
|
}
|
|
9832
10021
|
async function sendVoiceMedia(gw, target, source, params) {
|
|
9833
|
-
const voiceSource =
|
|
10022
|
+
const voiceSource = resolveVoiceSource2(source);
|
|
9834
10023
|
try {
|
|
9835
10024
|
const result = await gw.sendVoice(target, voiceSource, {
|
|
9836
10025
|
msgId: params.replyToId
|
|
@@ -9875,7 +10064,7 @@ async function sendFileMedia(gw, target, source, params) {
|
|
|
9875
10064
|
return { error: formatErr(err) };
|
|
9876
10065
|
}
|
|
9877
10066
|
}
|
|
9878
|
-
function
|
|
10067
|
+
function resolveVoiceSource2(source) {
|
|
9879
10068
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
9880
10069
|
return { url: source };
|
|
9881
10070
|
}
|
|
@@ -10173,6 +10362,31 @@ var qqbotSetupWizard = {
|
|
|
10173
10362
|
}
|
|
10174
10363
|
};
|
|
10175
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
|
+
|
|
10176
10390
|
// src/gateway/qqbot-gateway.ts
|
|
10177
10391
|
var import_node_os4 = __toESM(require("os"), 1);
|
|
10178
10392
|
|
|
@@ -10655,6 +10869,7 @@ function botClearStorage(_account) {
|
|
|
10655
10869
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
10656
10870
|
var import_node_path5 = __toESM(require("path"), 1);
|
|
10657
10871
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
10872
|
+
var import_node_crypto2 = __toESM(require("crypto"), 1);
|
|
10658
10873
|
var MAX_LINES_PER_FILE = 1e3;
|
|
10659
10874
|
var MAX_FILES = 4;
|
|
10660
10875
|
var LOG_KEYWORDS = ["gateway", "openclaw", "clawdbot", "moltbot"];
|
|
@@ -10834,7 +11049,8 @@ function botLogs(runtime2) {
|
|
|
10834
11049
|
const tmpDir = getQQBotMediaDir("exports");
|
|
10835
11050
|
if (!import_node_fs5.default.existsSync(tmpDir)) import_node_fs5.default.mkdirSync(tmpDir, { recursive: true });
|
|
10836
11051
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
10837
|
-
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`);
|
|
10838
11054
|
import_node_fs5.default.writeFileSync(tmpFile, lines.join("\n"), "utf8");
|
|
10839
11055
|
let summary = `${recentFiles.length} \u4E2A\u65E5\u5FD7\u6587\u4EF6\uFF0C\u5171 ${totalIncluded} \u884C`;
|
|
10840
11056
|
if (truncatedCount > 0) summary += `\uFF08${truncatedCount} \u4E2A\u622A\u65AD\uFF0C\u539F\u59CB ${totalOriginal} \u884C\uFF09`;
|
|
@@ -10844,7 +11060,7 @@ function botLogs(runtime2) {
|
|
|
10844
11060
|
await ctx.bot.sendFile(
|
|
10845
11061
|
{ scope: "c2c", targetId: senderId, msgId: ctx.message.messageId },
|
|
10846
11062
|
{ localPath: tmpFile },
|
|
10847
|
-
{ fileName: `bot-logs-${timestamp}.txt` }
|
|
11063
|
+
{ fileName: `bot-logs-${timestamp}-${suffix}.txt` }
|
|
10848
11064
|
);
|
|
10849
11065
|
}
|
|
10850
11066
|
} catch (err) {
|
|
@@ -11439,10 +11655,10 @@ function formatDuration2(seconds) {
|
|
|
11439
11655
|
|
|
11440
11656
|
// src/adapter/media.ts
|
|
11441
11657
|
var import_node_module4 = require("module");
|
|
11442
|
-
var
|
|
11658
|
+
var dns2 = __toESM(require("dns"), 1);
|
|
11443
11659
|
var path16 = __toESM(require("path"), 1);
|
|
11444
11660
|
var fs18 = __toESM(require("fs"), 1);
|
|
11445
|
-
var
|
|
11661
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
11446
11662
|
var req3 = (0, import_node_module4.createRequire)(__filename);
|
|
11447
11663
|
var _save;
|
|
11448
11664
|
function downloadRemoteMedia(opts) {
|
|
@@ -11478,7 +11694,7 @@ function isPrivateIP(ip) {
|
|
|
11478
11694
|
return PRIVATE_RANGES.some(([mask, prefix]) => val >> 32n - BigInt(prefix) === mask >> 32n - BigInt(prefix));
|
|
11479
11695
|
}
|
|
11480
11696
|
async function assertSafeHostname(hostname) {
|
|
11481
|
-
const addresses = await
|
|
11697
|
+
const addresses = await dns2.promises.resolve4(hostname).catch(() => []);
|
|
11482
11698
|
if (addresses.length === 0) throw new Error(`DNS resolution failed: ${hostname}`);
|
|
11483
11699
|
for (const addr of addresses) {
|
|
11484
11700
|
if (isPrivateIP(addr)) {
|
|
@@ -11503,7 +11719,7 @@ async function downloadViaFetch(opts) {
|
|
|
11503
11719
|
if (buf.length > maxBytes) throw new Error(`Download exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB`);
|
|
11504
11720
|
const ext = opts.originalFilename ? path16.extname(opts.originalFilename) || ".bin" : ".bin";
|
|
11505
11721
|
const name = opts.originalFilename ? path16.basename(opts.originalFilename, path16.extname(opts.originalFilename)) : "download";
|
|
11506
|
-
const rand =
|
|
11722
|
+
const rand = crypto5.randomBytes(4).toString("hex");
|
|
11507
11723
|
const filePath = path16.join(dir, `${name}_${Date.now()}_${rand}${ext}`);
|
|
11508
11724
|
fs18.writeFileSync(filePath, buf);
|
|
11509
11725
|
return { path: filePath };
|
|
@@ -12149,6 +12365,8 @@ function buildEnvelope2(ctx, msg, account) {
|
|
|
12149
12365
|
}
|
|
12150
12366
|
|
|
12151
12367
|
// src/outbound/deliver-pipeline.ts
|
|
12368
|
+
var path18 = __toESM(require("path"), 1);
|
|
12369
|
+
var os7 = __toESM(require("os"), 1);
|
|
12152
12370
|
function resolveMediaUrls(payload) {
|
|
12153
12371
|
if (payload.mediaUrls?.length) {
|
|
12154
12372
|
return payload.mediaUrls.filter(Boolean);
|
|
@@ -12197,6 +12415,7 @@ async function deliverReply(payload, _info, ctx) {
|
|
|
12197
12415
|
}
|
|
12198
12416
|
}
|
|
12199
12417
|
async function sendMediaUrls(ctx, urls) {
|
|
12418
|
+
const failedUrls = [];
|
|
12200
12419
|
for (const url of urls) {
|
|
12201
12420
|
const result = await sendMedia2({
|
|
12202
12421
|
to: ctx.qualifiedTarget,
|
|
@@ -12208,6 +12427,16 @@ async function sendMediaUrls(ctx, urls) {
|
|
|
12208
12427
|
});
|
|
12209
12428
|
if (result.error) {
|
|
12210
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}`);
|
|
12211
12440
|
}
|
|
12212
12441
|
}
|
|
12213
12442
|
}
|
|
@@ -12227,6 +12456,10 @@ async function handleVoiceIntent(text, ctx) {
|
|
|
12227
12456
|
ctx.log?.error(`[tts] TTS failed: ${ttsResult.error ?? "no audio path returned"}`);
|
|
12228
12457
|
return false;
|
|
12229
12458
|
}
|
|
12459
|
+
if (!isTtsPathSafe(ttsResult.audioPath)) {
|
|
12460
|
+
ctx.log?.error(`[tts] TTS audio path blocked: ${ttsResult.audioPath}`);
|
|
12461
|
+
return false;
|
|
12462
|
+
}
|
|
12230
12463
|
if (ctx.audioFileToSilkBase64) {
|
|
12231
12464
|
const silkBase64 = await ctx.audioFileToSilkBase64(ttsResult.audioPath);
|
|
12232
12465
|
if (silkBase64) {
|
|
@@ -12247,6 +12480,15 @@ async function handleVoiceIntent(text, ctx) {
|
|
|
12247
12480
|
return false;
|
|
12248
12481
|
}
|
|
12249
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
|
+
}
|
|
12250
12492
|
|
|
12251
12493
|
// src/dispatch/ctx-builder.ts
|
|
12252
12494
|
function buildCtxPayload(params) {
|
|
@@ -13091,10 +13333,10 @@ function getApprovalHandler(accountId) {
|
|
|
13091
13333
|
|
|
13092
13334
|
// src/features/proactive.ts
|
|
13093
13335
|
var fs19 = __toESM(require("fs"), 1);
|
|
13094
|
-
var
|
|
13336
|
+
var path20 = __toESM(require("path"), 1);
|
|
13095
13337
|
var log2 = createPluginLogger({ prefix: "[proactive]" });
|
|
13096
13338
|
var STORAGE_DIR = getQQBotDataDir("data");
|
|
13097
|
-
var KNOWN_USERS_FILE =
|
|
13339
|
+
var KNOWN_USERS_FILE = path20.join(STORAGE_DIR, "known-users.json");
|
|
13098
13340
|
var knownUsersCache = null;
|
|
13099
13341
|
var cacheLastModified = 0;
|
|
13100
13342
|
function ensureStorageDir() {
|
|
@@ -13298,6 +13540,11 @@ async function handleApproval(event, account, log4, ack) {
|
|
|
13298
13540
|
}
|
|
13299
13541
|
const buttonData = event.data?.resolved?.button_data;
|
|
13300
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
|
+
}
|
|
13301
13548
|
const parts = buttonData.split(":");
|
|
13302
13549
|
if (parts.length < 3) return;
|
|
13303
13550
|
const handler = getApprovalHandler(account.accountId);
|
|
@@ -13307,10 +13554,20 @@ async function handleApproval(event, account, log4, ack) {
|
|
|
13307
13554
|
try {
|
|
13308
13555
|
await handler.resolveApproval(approvalId, decision);
|
|
13309
13556
|
} catch (err) {
|
|
13310
|
-
log4.error(`interaction approve error: ${err}`);
|
|
13557
|
+
log4.error(`interaction approve error: ${err instanceof Error ? err.message : String(err)}`);
|
|
13311
13558
|
}
|
|
13312
13559
|
}
|
|
13313
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
|
+
}
|
|
13314
13571
|
function buildClawCfg(requireMention, mentionPatterns, groupPolicy) {
|
|
13315
13572
|
return {
|
|
13316
13573
|
channel_type: "qqbot",
|
|
@@ -13340,13 +13597,13 @@ function createPluginWebhookAdapter(params) {
|
|
|
13340
13597
|
let storedPath = "";
|
|
13341
13598
|
const accountId = params.account.accountId;
|
|
13342
13599
|
return {
|
|
13343
|
-
async listen(_port,
|
|
13600
|
+
async listen(_port, path22, handler) {
|
|
13344
13601
|
const ingress = await import("openclaw/plugin-sdk/webhook-ingress").catch(() => null);
|
|
13345
13602
|
if (!ingress?.registerWebhookTargetWithPluginRoute) {
|
|
13346
13603
|
params.log.error("Webhook ingress not available");
|
|
13347
13604
|
return;
|
|
13348
13605
|
}
|
|
13349
|
-
const webhookPath = storedPath =
|
|
13606
|
+
const webhookPath = storedPath = path22 && path22 !== "/" ? path22 : params.account.config.webhook?.path ?? "/qqbot/webhook";
|
|
13350
13607
|
const existing = sharedTargets.get(webhookPath);
|
|
13351
13608
|
if (existing) {
|
|
13352
13609
|
const dupIdx = existing.findIndex((t) => t.accountId === accountId);
|
|
@@ -13393,12 +13650,12 @@ function createPluginWebhookAdapter(params) {
|
|
|
13393
13650
|
}
|
|
13394
13651
|
};
|
|
13395
13652
|
}
|
|
13396
|
-
function createSharedHandler(
|
|
13653
|
+
function createSharedHandler(path22, log4) {
|
|
13397
13654
|
return async (req4, res) => {
|
|
13398
13655
|
try {
|
|
13399
13656
|
const ingress = await import("openclaw/plugin-sdk/webhook-ingress").catch(() => null);
|
|
13400
13657
|
if (!ingress?.withResolvedWebhookRequestPipeline) {
|
|
13401
|
-
await handleSimple(req4, res,
|
|
13658
|
+
await handleSimple(req4, res, path22, log4);
|
|
13402
13659
|
return;
|
|
13403
13660
|
}
|
|
13404
13661
|
await ingress.withResolvedWebhookRequestPipeline({
|
|
@@ -13439,12 +13696,12 @@ function createSharedHandler(path21, log4) {
|
|
|
13439
13696
|
if (payload.op === 13) {
|
|
13440
13697
|
const t = targets[0];
|
|
13441
13698
|
if (!t) {
|
|
13442
|
-
log4.warn?.(`[webhook] op:13 no target on ${
|
|
13699
|
+
log4.warn?.(`[webhook] op:13 no target on ${path22}`);
|
|
13443
13700
|
res.statusCode = 500;
|
|
13444
13701
|
res.end(JSON.stringify({ error: "no target" }));
|
|
13445
13702
|
return;
|
|
13446
13703
|
}
|
|
13447
|
-
const h3 = resolveTargetHandler(
|
|
13704
|
+
const h3 = resolveTargetHandler(path22, t.accountId);
|
|
13448
13705
|
if (!h3) {
|
|
13449
13706
|
log4.warn?.(`[webhook] op:13 no handler for ${t.accountId}`);
|
|
13450
13707
|
res.statusCode = 500;
|
|
@@ -13475,10 +13732,10 @@ function createSharedHandler(path21, log4) {
|
|
|
13475
13732
|
unauthorizedMessage: JSON.stringify({ error: "invalid signature" })
|
|
13476
13733
|
});
|
|
13477
13734
|
if (!matched) {
|
|
13478
|
-
log4.warn?.(`[webhook] signature mismatch on ${
|
|
13735
|
+
log4.warn?.(`[webhook] signature mismatch on ${path22} (${targets.length} target(s))`);
|
|
13479
13736
|
return;
|
|
13480
13737
|
}
|
|
13481
|
-
const h2 = resolveTargetHandler(
|
|
13738
|
+
const h2 = resolveTargetHandler(path22, matched.accountId);
|
|
13482
13739
|
if (!h2) {
|
|
13483
13740
|
log4.error?.(`[webhook] no handler for matched target ${matched.accountId}`);
|
|
13484
13741
|
res.statusCode = 500;
|
|
@@ -13498,8 +13755,8 @@ function createSharedHandler(path21, log4) {
|
|
|
13498
13755
|
}
|
|
13499
13756
|
};
|
|
13500
13757
|
}
|
|
13501
|
-
function resolveTargetHandler(
|
|
13502
|
-
return sharedTargets.get(
|
|
13758
|
+
function resolveTargetHandler(path22, accountId) {
|
|
13759
|
+
return sharedTargets.get(path22)?.find((t) => t.accountId === accountId)?.handler;
|
|
13503
13760
|
}
|
|
13504
13761
|
async function delegateToHandler(handler, req4, res, rawBody) {
|
|
13505
13762
|
const headers = {};
|
|
@@ -13513,7 +13770,7 @@ async function delegateToHandler(handler, req4, res, rawBody) {
|
|
|
13513
13770
|
}
|
|
13514
13771
|
res.end(resp.body);
|
|
13515
13772
|
}
|
|
13516
|
-
async function handleSimple(req4, res,
|
|
13773
|
+
async function handleSimple(req4, res, path22, log4) {
|
|
13517
13774
|
try {
|
|
13518
13775
|
const ct = String(req4.headers["content-type"] ?? "");
|
|
13519
13776
|
if (!ct.includes("application/json")) {
|
|
@@ -13534,7 +13791,7 @@ async function handleSimple(req4, res, path21, log4) {
|
|
|
13534
13791
|
chunks.push(buf);
|
|
13535
13792
|
}
|
|
13536
13793
|
const rawBody = Buffer.concat(chunks);
|
|
13537
|
-
const entries = sharedTargets.get(
|
|
13794
|
+
const entries = sharedTargets.get(path22) ?? [];
|
|
13538
13795
|
for (const entry of entries) {
|
|
13539
13796
|
const resp = await entry.handler({ body: rawBody, headers: mapHeaders(req4) });
|
|
13540
13797
|
if (resp.status < 400) {
|
|
@@ -13565,12 +13822,35 @@ function getHeader2(req4, key) {
|
|
|
13565
13822
|
}
|
|
13566
13823
|
|
|
13567
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
|
+
}
|
|
13568
13844
|
var QQBotGateway = class {
|
|
13569
13845
|
bot;
|
|
13570
13846
|
account;
|
|
13571
13847
|
runtime;
|
|
13572
13848
|
log;
|
|
13849
|
+
textTimeout;
|
|
13850
|
+
mediaTimeout;
|
|
13573
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);
|
|
13574
13854
|
this.account = account;
|
|
13575
13855
|
this.runtime = runtime2;
|
|
13576
13856
|
this.log = log4 ?? createPluginLogger({ prefix: `[qqbot:${account.accountId}]` });
|
|
@@ -13635,69 +13915,66 @@ var QQBotGateway = class {
|
|
|
13635
13915
|
await this.bot.stop();
|
|
13636
13916
|
}
|
|
13637
13917
|
async sendText(target, text, opts) {
|
|
13638
|
-
return
|
|
13918
|
+
return withTimeout(
|
|
13919
|
+
this.bot.sendText(attachMsgId(target, opts), text),
|
|
13920
|
+
this.textTimeout,
|
|
13921
|
+
"sendText"
|
|
13922
|
+
);
|
|
13639
13923
|
}
|
|
13640
13924
|
async sendMedia(target, source, opts) {
|
|
13641
13925
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13642
13926
|
const fileType = opts?.fileType ?? MediaFileType.IMAGE;
|
|
13643
13927
|
const sourceOpts = resolveMediaSource(source);
|
|
13644
|
-
const result = await
|
|
13645
|
-
target: resolvedTarget,
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
});
|
|
13928
|
+
const result = await withTimeout(
|
|
13929
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType, ...sourceOpts, content: opts?.text }),
|
|
13930
|
+
this.mediaTimeout,
|
|
13931
|
+
"sendMedia"
|
|
13932
|
+
);
|
|
13650
13933
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13651
13934
|
}
|
|
13652
13935
|
async sendVoice(target, source, opts) {
|
|
13653
13936
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13654
13937
|
if (source.base64) {
|
|
13655
|
-
const result2 = await
|
|
13656
|
-
target: resolvedTarget,
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
});
|
|
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
|
+
);
|
|
13661
13943
|
return result2.message ?? { id: "", timestamp: Date.now() };
|
|
13662
13944
|
}
|
|
13663
13945
|
if (source.localPath) {
|
|
13664
|
-
const result2 = await
|
|
13665
|
-
target: resolvedTarget,
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
});
|
|
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
|
+
);
|
|
13670
13951
|
return result2.message ?? { id: "", timestamp: Date.now() };
|
|
13671
13952
|
}
|
|
13672
|
-
const result = await
|
|
13673
|
-
target: resolvedTarget,
|
|
13674
|
-
|
|
13675
|
-
url
|
|
13676
|
-
|
|
13677
|
-
});
|
|
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
|
+
);
|
|
13678
13958
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13679
13959
|
}
|
|
13680
13960
|
async sendVideo(target, source, opts) {
|
|
13681
13961
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13682
13962
|
const sourceOpts = resolveMediaSource(source);
|
|
13683
|
-
const result = await
|
|
13684
|
-
target: resolvedTarget,
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
});
|
|
13963
|
+
const result = await withTimeout(
|
|
13964
|
+
this.bot.sendMedia({ target: resolvedTarget, fileType: MediaFileType.VIDEO, ...sourceOpts, content: opts?.text }),
|
|
13965
|
+
this.mediaTimeout,
|
|
13966
|
+
"sendVideo"
|
|
13967
|
+
);
|
|
13689
13968
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13690
13969
|
}
|
|
13691
13970
|
async sendFile(target, source, opts) {
|
|
13692
13971
|
const resolvedTarget = attachMsgId(target, opts);
|
|
13693
13972
|
const sourceOpts = resolveMediaSource(source);
|
|
13694
|
-
const result = await
|
|
13695
|
-
target: resolvedTarget,
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
content: opts?.text
|
|
13700
|
-
});
|
|
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
|
+
);
|
|
13701
13978
|
return result.message ?? { id: "", timestamp: Date.now() };
|
|
13702
13979
|
}
|
|
13703
13980
|
openStream(target, msgId) {
|
|
@@ -14064,6 +14341,11 @@ function stripMentionText(text, mentions) {
|
|
|
14064
14341
|
|
|
14065
14342
|
// src/channel.ts
|
|
14066
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
|
+
}
|
|
14067
14349
|
var qqbotPlugin = {
|
|
14068
14350
|
id: "qqbot",
|
|
14069
14351
|
meta: {
|
|
@@ -14179,13 +14461,33 @@ var qqbotPlugin = {
|
|
|
14179
14461
|
// ── 出站 ──
|
|
14180
14462
|
outbound: {
|
|
14181
14463
|
deliveryMode: "direct",
|
|
14464
|
+
sanitizeText: ({ text }) => sanitizeQQBotText(text),
|
|
14182
14465
|
chunker: (text, limit) => {
|
|
14183
14466
|
const adapters = getAdapters(getQQBotRuntime());
|
|
14184
14467
|
if (adapters.chunkMarkdownText) return adapters.chunkMarkdownText(text, limit);
|
|
14185
14468
|
const lines = text.split("\n");
|
|
14186
14469
|
const chunks = [];
|
|
14187
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
|
+
};
|
|
14188
14485
|
for (const line of lines) {
|
|
14486
|
+
if (isGfmTableLine(line)) {
|
|
14487
|
+
tableBuffer.push(line);
|
|
14488
|
+
continue;
|
|
14489
|
+
}
|
|
14490
|
+
flushTable();
|
|
14189
14491
|
const candidate = current ? `${current}
|
|
14190
14492
|
${line}` : line;
|
|
14191
14493
|
if (candidate.length > limit && current) {
|
|
@@ -14195,6 +14497,7 @@ ${line}` : line;
|
|
|
14195
14497
|
current = candidate;
|
|
14196
14498
|
}
|
|
14197
14499
|
}
|
|
14500
|
+
flushTable();
|
|
14198
14501
|
if (current) chunks.push(current);
|
|
14199
14502
|
return chunks.length > 0 ? chunks : [text];
|
|
14200
14503
|
},
|
|
@@ -14332,10 +14635,10 @@ function json(data) {
|
|
|
14332
14635
|
details: data
|
|
14333
14636
|
};
|
|
14334
14637
|
}
|
|
14335
|
-
function validatePath(
|
|
14336
|
-
if (!
|
|
14337
|
-
if (
|
|
14338
|
-
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 !== "/") {
|
|
14339
14642
|
return "path \u5305\u542B\u975E\u6CD5\u5B57\u7B26";
|
|
14340
14643
|
}
|
|
14341
14644
|
return null;
|
|
@@ -14643,7 +14946,7 @@ function verifyRuntimeContract(rt) {
|
|
|
14643
14946
|
for (const r of OPTIONAL) {
|
|
14644
14947
|
if (!r.probe(rt)) degraded.push(r.name);
|
|
14645
14948
|
}
|
|
14646
|
-
log3.
|
|
14949
|
+
log3.debug(`openclaw=${version} required=${REQUIRED.length - missing.length}/${REQUIRED.length} degraded=${degraded.length}/${OPTIONAL.length}`);
|
|
14647
14950
|
if (missing.length) {
|
|
14648
14951
|
log3.error(`BROKEN \u2014 missing: ${missing.join(", ")}`);
|
|
14649
14952
|
}
|