@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 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 net = require("net");
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 net.connect(options);
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 = net.isIP(options.host) ? "" : options.host;
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, path21, explicit, defaultValue) {
5023
+ function resolvePolicy(ctx, path22, explicit, defaultValue) {
5024
5024
  if (explicit !== void 0 && explicit !== null) {
5025
5025
  return explicit;
5026
5026
  }
5027
- const keys = path21.split(".");
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, path21, bizCode, bizMessage) {
5105
+ constructor(message, httpStatus, path22, bizCode, bizMessage) {
5106
5106
  super(message);
5107
5107
  this.httpStatus = httpStatus;
5108
- this.path = path21;
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, path21, body, options) {
5197
- const url = `${this.baseUrl}${path21}`;
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 || path21.includes("/files") || path21.includes("/upload_prepare") || path21.includes("/upload_part_finish");
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 [${path21}]: exceeded ${timeout}ms`, 0, path21);
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 [${path21}]: ${formatErrorMessage(err)}`, 0, path21);
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 [${path21}]: ${formatErrorMessage(err)}`, res.status, path21);
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${path21}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path21);
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 [${path21}]: ${error.message ?? rawBody}`, res.status, path21, bizCode, error.message);
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 [${path21}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`, res.status, path21);
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${path21}\uFF09\uFF0C\u53EF\u80FD\u662F\u4E34\u65F6\u6545\u969C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path21);
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${path21}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, res.status, path21);
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 path21 = uploadPreparePath(opts.scope, opts.targetId);
5491
+ const path22 = uploadPreparePath(opts.scope, opts.targetId);
5492
5492
  try {
5493
- return await this.client.request(token, "POST", path21, {
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 path21 = uploadPartFinishPath(opts.scope, opts.targetId);
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", path21, {
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 path21 = uploadCompletePath(opts.scope, opts.targetId);
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", path21, { upload_id: uploadId }, { uploadRequest: true });
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 path21 = mediaUploadPath(scope, targetId);
5720
- const result = await withRetry(() => this.client.request(token, "POST", path21, body, {
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 path21 = messagePath(scope, targetId);
5737
- return this.client.request(token, "POST", path21, {
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 path21 = messagePath(scope, targetId);
5777
- return this.sendAndNotify(creds.appId, token, "POST", path21, body, { text: content });
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 path21 = messagePath(scope, targetId);
5786
- return this.sendAndNotify(creds.appId, token, "POST", path21, body, { text: content });
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 path21 = streamMessagePath(openid);
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", path21, body);
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 path21 = messagePath(scope, targetId);
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", path21, cleaned, { text: cleaned.content ?? cleaned.markdown?.content });
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 path21 = `${messagePath(scope, targetId)}/${messageId}`;
5904
- await this.client.request(token, "DELETE", path21);
5903
+ const path22 = `${messagePath(scope, targetId)}/${messageId}`;
5904
+ await this.client.request(token, "DELETE", path22);
5905
5905
  }
5906
- async sendAndNotify(_appId, accessToken, method, path21, body, meta) {
5907
- const result = await this.client.request(accessToken, method, path21, body);
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, path21, handler) {
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 !== path21) {
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 path21 = this.opts.path ?? "/";
6854
- this.log?.info?.(`[webhook] starting on port ${port}, path ${path21}`);
6855
- await this.server.listen(port, path21, (req4) => this.handleRequest(req4));
6856
- this.log?.info?.(`[webhook] listening on :${port}${path21}`);
6857
- this.callbacks.onReady?.({ transport: "webhook", port, path: path21 });
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: (path21, query) => this.apiRequest("GET", path21, void 0, query),
7748
- post: (path21, body) => this.apiRequest("POST", path21, body),
7749
- put: (path21, body) => this.apiRequest("PUT", path21, body),
7750
- patch: (path21, body) => this.apiRequest("PATCH", path21, body),
7751
- delete: (path21) => this.apiRequest("DELETE", path21),
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, path21, body, query) {
7756
+ async apiRequest(method, path22, body, query) {
7757
7757
  const token = await this.tokenManager.getAccessToken(this.creds.appId, this.creds.clientSecret);
7758
- let fullPath = path21;
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 = `${path21}?${params.toString()}`;
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 gw = gateways.get(params.account.accountId);
9026
- if (!gw) {
9027
- return { error: `Bot "${params.account.accountId}" not running` };
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 result = await gw.sendText(target, params.text, { msgId: params.replyToId });
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 gw = gateways.get(params.account.accountId);
9039
- if (!gw) {
9040
- return { error: `Bot "${params.account.accountId}" not running` };
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 isLocalPath = params.mediaUrl.startsWith("/") || params.mediaUrl.startsWith("./") || params.mediaUrl.startsWith("../");
9047
- const isUrl = params.mediaUrl.startsWith("http://") || params.mediaUrl.startsWith("https://");
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.202607091556" : "unknown";
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 path21 of paths) {
9596
+ for (const path22 of paths) {
9512
9597
  let target = rt;
9513
9598
  let parent = rt;
9514
- for (let i = 0; i < path21.length; i++) {
9599
+ for (let i = 0; i < path22.length; i++) {
9515
9600
  parent = target;
9516
- target = target?.[path21[i]];
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.some((root) => {
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 = resolveVoiceSource(source);
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 resolveVoiceSource(source) {
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 tmpFile = import_node_path5.default.join(tmpDir, `bot-logs-${timestamp}.txt`);
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 dns = __toESM(require("dns"), 1);
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 crypto4 = __toESM(require("crypto"), 1);
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 dns.promises.resolve4(hostname).catch(() => []);
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 = crypto4.randomBytes(4).toString("hex");
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 path19 = __toESM(require("path"), 1);
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 = path19.join(STORAGE_DIR, "known-users.json");
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, path21, handler) {
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 = path21 && path21 !== "/" ? path21 : params.account.config.webhook?.path ?? "/qqbot/webhook";
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(path21, log4) {
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, path21, log4);
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 ${path21}`);
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(path21, t.accountId);
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 ${path21} (${targets.length} target(s))`);
13735
+ log4.warn?.(`[webhook] signature mismatch on ${path22} (${targets.length} target(s))`);
13479
13736
  return;
13480
13737
  }
13481
- const h2 = resolveTargetHandler(path21, matched.accountId);
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(path21, accountId) {
13502
- return sharedTargets.get(path21)?.find((t) => t.accountId === accountId)?.handler;
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, path21, log4) {
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(path21) ?? [];
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 this.bot.sendText(attachMsgId(target, opts), text);
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 this.bot.sendMedia({
13645
- target: resolvedTarget,
13646
- fileType,
13647
- ...sourceOpts,
13648
- content: opts?.text
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 this.bot.sendMedia({
13656
- target: resolvedTarget,
13657
- fileType: MediaFileType.VOICE,
13658
- fileData: source.base64,
13659
- content: opts?.text
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 this.bot.sendMedia({
13665
- target: resolvedTarget,
13666
- fileType: MediaFileType.VOICE,
13667
- localPath: source.localPath,
13668
- content: opts?.text
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 this.bot.sendMedia({
13673
- target: resolvedTarget,
13674
- fileType: MediaFileType.VOICE,
13675
- url: source.url,
13676
- content: opts?.text
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 this.bot.sendMedia({
13684
- target: resolvedTarget,
13685
- fileType: MediaFileType.VIDEO,
13686
- ...sourceOpts,
13687
- content: opts?.text
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 this.bot.sendMedia({
13695
- target: resolvedTarget,
13696
- fileType: MediaFileType.FILE,
13697
- ...sourceOpts,
13698
- fileName: opts?.fileName,
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(path21) {
14336
- if (!path21.startsWith("/")) return "path \u5FC5\u987B\u4EE5 / \u5F00\u5934";
14337
- if (path21.includes("..") || path21.includes("//")) return "path \u4E0D\u5141\u8BB8\u5305\u542B .. \u6216 //";
14338
- if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path21) && path21 !== "/") {
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.info(`openclaw=${version} required=${REQUIRED.length - missing.length}/${REQUIRED.length} degraded=${degraded.length}/${OPTIONAL.length}`);
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
  }