@searchfe/openclaw-baiduapp 0.1.6 → 0.1.7-beta.10

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.js CHANGED
@@ -1,5 +1,12 @@
1
- import crypto from 'crypto';
1
+ import path2 from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import crypto3 from 'crypto';
2
4
  import { createRequire } from 'module';
5
+ import { lookup } from 'dns/promises';
6
+ import fs2 from 'fs/promises';
7
+ import net from 'net';
8
+ import fs from 'fs';
9
+ import { tmpdir } from 'os';
3
10
 
4
11
  var __defProp = Object.defineProperty;
5
12
  var __export = (target, all) => {
@@ -7,7 +14,7 @@ var __export = (target, all) => {
7
14
  __defProp(target, name, { get: all[name], enumerable: true });
8
15
  };
9
16
 
10
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
17
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
11
18
  var external_exports = {};
12
19
  __export(external_exports, {
13
20
  BRAND: () => BRAND,
@@ -119,7 +126,7 @@ __export(external_exports, {
119
126
  void: () => voidType
120
127
  });
121
128
 
122
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
129
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
123
130
  var util;
124
131
  (function(util2) {
125
132
  util2.assertEqual = (_) => {
@@ -253,7 +260,7 @@ var getParsedType = (data) => {
253
260
  }
254
261
  };
255
262
 
256
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js
263
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js
257
264
  var ZodIssueCode = util.arrayToEnum([
258
265
  "invalid_type",
259
266
  "invalid_literal",
@@ -371,7 +378,7 @@ ZodError.create = (issues) => {
371
378
  return error;
372
379
  };
373
380
 
374
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js
381
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js
375
382
  var errorMap = (issue, _ctx) => {
376
383
  let message;
377
384
  switch (issue.code) {
@@ -474,7 +481,7 @@ var errorMap = (issue, _ctx) => {
474
481
  };
475
482
  var en_default = errorMap;
476
483
 
477
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js
484
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js
478
485
  var overrideErrorMap = en_default;
479
486
  function setErrorMap(map) {
480
487
  overrideErrorMap = map;
@@ -483,10 +490,10 @@ function getErrorMap() {
483
490
  return overrideErrorMap;
484
491
  }
485
492
 
486
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
493
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
487
494
  var makeIssue = (params) => {
488
- const { data, path, errorMaps, issueData } = params;
489
- const fullPath = [...path, ...issueData.path || []];
495
+ const { data, path: path4, errorMaps, issueData } = params;
496
+ const fullPath = [...path4, ...issueData.path || []];
490
497
  const fullIssue = {
491
498
  ...issueData,
492
499
  path: fullPath
@@ -593,20 +600,20 @@ var isDirty = (x) => x.status === "dirty";
593
600
  var isValid = (x) => x.status === "valid";
594
601
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
595
602
 
596
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
603
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
597
604
  var errorUtil;
598
605
  (function(errorUtil2) {
599
606
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
600
607
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
601
608
  })(errorUtil || (errorUtil = {}));
602
609
 
603
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
610
+ // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
604
611
  var ParseInputLazyPath = class {
605
- constructor(parent, value, path, key) {
612
+ constructor(parent, value, path4, key) {
606
613
  this._cachedPath = [];
607
614
  this.parent = parent;
608
615
  this.data = value;
609
- this._path = path;
616
+ this._path = path4;
610
617
  this._key = key;
611
618
  }
612
619
  get path() {
@@ -4053,6 +4060,7 @@ var DEFAULT_API_BASE = "https://claw.baidu.com";
4053
4060
  var BaiduAppAccountSchema = external_exports.object({
4054
4061
  name: external_exports.string().optional(),
4055
4062
  enabled: external_exports.boolean().optional(),
4063
+ pollingEnabled: external_exports.boolean().optional(),
4056
4064
  webhookPath: external_exports.string().optional(),
4057
4065
  token: external_exports.string().optional(),
4058
4066
  encodingAESKey: external_exports.string().optional(),
@@ -4072,6 +4080,7 @@ var BaiduAppConfigJsonSchema = {
4072
4080
  properties: {
4073
4081
  name: { type: "string" },
4074
4082
  enabled: { type: "boolean" },
4083
+ pollingEnabled: { type: "boolean" },
4075
4084
  webhookPath: { type: "string" },
4076
4085
  token: { type: "string" },
4077
4086
  encodingAESKey: { type: "string" },
@@ -4088,6 +4097,7 @@ var BaiduAppConfigJsonSchema = {
4088
4097
  properties: {
4089
4098
  name: { type: "string" },
4090
4099
  enabled: { type: "boolean" },
4100
+ pollingEnabled: { type: "boolean" },
4091
4101
  webhookPath: { type: "string" },
4092
4102
  token: { type: "string" },
4093
4103
  encodingAESKey: { type: "string" },
@@ -4117,6 +4127,9 @@ function listBaiduAppAccountIds(cfg) {
4117
4127
  if (ids.length === 0) {
4118
4128
  return [DEFAULT_ACCOUNT_ID];
4119
4129
  }
4130
+ if (!ids.includes(DEFAULT_ACCOUNT_ID)) {
4131
+ ids.push(DEFAULT_ACCOUNT_ID);
4132
+ }
4120
4133
  return ids.sort((a, b) => a.localeCompare(b));
4121
4134
  }
4122
4135
  function resolveDefaultBaiduAppAccountId(cfg) {
@@ -4154,7 +4167,7 @@ function resolveBaiduAppAccount(params) {
4154
4167
  const appKey = merged.appKey?.trim() || (isDefaultAccount ? process.env.BAIDU_APP_KEY?.trim() : void 0) || void 0;
4155
4168
  const appSecret = merged.appSecret?.trim() || (isDefaultAccount ? process.env.BAIDU_APP_SECRET?.trim() : void 0) || void 0;
4156
4169
  const configured = Boolean(token && encodingAESKey);
4157
- const canSendActive = Boolean(appKey && token && encodingAESKey);
4170
+ const canSendActive = Boolean(appKey && appSecret && token && encodingAESKey);
4158
4171
  const rawApiBase = merged.apiBase?.trim() || (isDefaultAccount ? process.env.BAIDU_API_BASE?.trim() : void 0) || void 0;
4159
4172
  const apiBase = (rawApiBase || DEFAULT_API_BASE).replace(/\/+$/, "");
4160
4173
  return {
@@ -4217,7 +4230,7 @@ function pkcs7Unpad(buf, blockSize) {
4217
4230
  return buf.subarray(0, buf.length - pad);
4218
4231
  }
4219
4232
  function sha1Hex(input) {
4220
- return crypto.createHash("sha1").update(input).digest("hex");
4233
+ return crypto3.createHash("sha1").update(input).digest("hex");
4221
4234
  }
4222
4235
  function computeBaiduAppMsgSignature(params) {
4223
4236
  const parts = [params.token, params.timestamp, params.nonce, params.encrypt].map((value) => String(value ?? "")).sort();
@@ -4235,7 +4248,7 @@ function verifyBaiduAppSignature(params) {
4235
4248
  function decryptBaiduAppEncrypted(params) {
4236
4249
  const aesKey = decodeEncodingAESKey(params.encodingAESKey);
4237
4250
  const iv = aesKey.subarray(0, 16);
4238
- const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
4251
+ const decipher = crypto3.createDecipheriv("aes-256-cbc", aesKey, iv);
4239
4252
  decipher.setAutoPadding(false);
4240
4253
  const decryptedPadded = Buffer.concat([decipher.update(Buffer.from(params.encrypt, "base64")), decipher.final()]);
4241
4254
  const decrypted = pkcs7Unpad(decryptedPadded, PKCS7_BLOCK_SIZE);
@@ -4253,13 +4266,13 @@ function decryptBaiduAppEncrypted(params) {
4253
4266
  function encryptBaiduAppPlaintext(params) {
4254
4267
  const aesKey = decodeEncodingAESKey(params.encodingAESKey);
4255
4268
  const iv = aesKey.subarray(0, 16);
4256
- const random16 = crypto.randomBytes(16);
4269
+ const random16 = crypto3.randomBytes(16);
4257
4270
  const msg = Buffer.from(params.plaintext ?? "", "utf8");
4258
4271
  const msgLen = Buffer.alloc(4);
4259
4272
  msgLen.writeUInt32BE(msg.length, 0);
4260
4273
  const raw = Buffer.concat([random16, msgLen, msg]);
4261
4274
  const padded = pkcs7Pad(raw, PKCS7_BLOCK_SIZE);
4262
- const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
4275
+ const cipher = crypto3.createCipheriv("aes-256-cbc", aesKey, iv);
4263
4276
  cipher.setAutoPadding(false);
4264
4277
  const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
4265
4278
  return encrypted.toString("base64");
@@ -4279,9 +4292,12 @@ async function sendBaiduAppMessage(account, message, options) {
4279
4292
  errmsg: "Account not configured for active sending (missing appKey, token, or encodingAESKey)"
4280
4293
  };
4281
4294
  }
4282
- const payload = {
4295
+ const normalizedPayload = typeof message === "string" ? {
4283
4296
  msgtype: "text",
4284
- text: { content: message },
4297
+ text: { content: message }
4298
+ } : message;
4299
+ const payload = {
4300
+ ...normalizedPayload,
4285
4301
  version: PLUGIN_VERSION,
4286
4302
  ...options?.msgid != null ? { msgid: options.msgid } : {},
4287
4303
  ...options?.streamId != null ? { streamId: options.streamId } : {},
@@ -4293,7 +4309,7 @@ async function sendBaiduAppMessage(account, message, options) {
4293
4309
  plaintext
4294
4310
  });
4295
4311
  const timestamp = String(Math.floor(Date.now() / 1e3));
4296
- const nonce = crypto.randomBytes(8).toString("hex");
4312
+ const nonce = crypto3.randomBytes(8).toString("hex");
4297
4313
  const msgSignature = computeBaiduAppMsgSignature({
4298
4314
  token: account.token ?? "",
4299
4315
  timestamp,
@@ -4325,22 +4341,30 @@ async function sendBaiduAppMessage(account, message, options) {
4325
4341
  return { ok: false, errcode: resp.status, errmsg };
4326
4342
  }
4327
4343
  const result = {
4328
- ok: data.errcode === 0,
4329
- errcode: data.errcode,
4330
- errmsg: data.errmsg,
4331
- invaliduser: data.invaliduser,
4332
- msgid: data.msgid
4344
+ ok: true
4333
4345
  };
4334
- if (result.ok) {
4335
- logger.info(`request succeeded: msgid=${result.msgid ?? "unknown"}`);
4336
- } else {
4337
- logger.error(`request failed: errcode=${result.errcode} errmsg=${result.errmsg ?? "unknown"}`);
4338
- }
4346
+ logger.info(`request succeeded`);
4339
4347
  return result;
4340
4348
  }
4341
4349
 
4350
+ // src/media-payload.ts
4351
+ function buildMediaPayload(mediaList, opts) {
4352
+ const first = mediaList[0];
4353
+ const mediaPaths = mediaList.map((media) => media.path);
4354
+ const rawMediaTypes = mediaList.map((media) => media.contentType ?? "");
4355
+ const mediaTypes = opts?.preserveMediaTypeCardinality ? rawMediaTypes : rawMediaTypes.filter((value) => Boolean(value));
4356
+ return {
4357
+ MediaPath: first?.path,
4358
+ MediaType: first?.contentType,
4359
+ MediaUrl: first?.path,
4360
+ MediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
4361
+ MediaUrls: mediaPaths.length > 0 ? mediaPaths : void 0,
4362
+ MediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0
4363
+ };
4364
+ }
4365
+
4342
4366
  // src/bot.ts
4343
- function extractBaiduAppContent(msg) {
4367
+ function extractBaiduAppTextContent(msg) {
4344
4368
  const msgtype = String(msg.msgtype ?? msg.MsgType ?? "").toLowerCase();
4345
4369
  if (msgtype === "text") {
4346
4370
  const content = msg.text?.content ?? msg.Content;
@@ -4354,13 +4378,71 @@ function extractBaiduAppContent(msg) {
4354
4378
  }
4355
4379
  return msgtype ? `[${msgtype}]` : "";
4356
4380
  }
4381
+ function canDispatchBaiduAppInboundMessage(msg) {
4382
+ const msgtype = String(msg.msgtype ?? msg.MsgType ?? "").toLowerCase();
4383
+ if (msgtype === "text") {
4384
+ if (extractBaiduAppTextContent(msg)) {
4385
+ return true;
4386
+ }
4387
+ const files = msg.files;
4388
+ return Array.isArray(files) && files.length > 0;
4389
+ }
4390
+ return msgtype === "event" || Boolean(msgtype);
4391
+ }
4392
+ function buildFilesOnlySummary(fileCount) {
4393
+ return fileCount === 1 ? "[files] 1 attachment received" : `[files] ${fileCount} attachments received`;
4394
+ }
4395
+ function countInboundFiles(msg) {
4396
+ const files = msg.files;
4397
+ return Array.isArray(files) ? files.length : 0;
4398
+ }
4399
+ function appendLocalFilesBlock(base, localPaths) {
4400
+ if (localPaths.length === 0) {
4401
+ return base;
4402
+ }
4403
+ const fileList = localPaths.map((localPath) => `- ${localPath}`).join("\n");
4404
+ return `${base}
4405
+
4406
+ [local files]
4407
+ ${fileList}`;
4408
+ }
4409
+ function appendNonEmptySections(sections) {
4410
+ return sections.map((section) => section?.trim()).filter(Boolean).join("\n\n");
4411
+ }
4412
+ function buildBaiduAppInboundDispatchContent(msg, localFiles = [], diagnostics = {}) {
4413
+ const baseText = extractBaiduAppTextContent(msg);
4414
+ const inboundFileCount = countInboundFiles(msg);
4415
+ const summaryLine = diagnostics.summaryLine ?? (inboundFileCount > 0 ? buildFilesOnlySummary(inboundFileCount) : "");
4416
+ const failureBlock = diagnostics.failurePlaceholders?.filter(Boolean).join("\n");
4417
+ const baseBody = appendNonEmptySections([baseText, summaryLine, failureBlock]);
4418
+ const localPaths = localFiles.map((file) => file.path);
4419
+ const rawBody = appendLocalFilesBlock(baseBody, localPaths);
4420
+ const mediaPayload = buildMediaPayload(
4421
+ localFiles.map((file) => ({ path: file.path, contentType: file.contentType })),
4422
+ { preserveMediaTypeCardinality: true }
4423
+ );
4424
+ const contextPatch = {
4425
+ MediaPath: mediaPayload.MediaPath,
4426
+ MediaPaths: mediaPayload.MediaPaths,
4427
+ MediaType: mediaPayload.MediaType,
4428
+ MediaTypes: mediaPayload.MediaTypes
4429
+ };
4430
+ if (localFiles.length === 1) {
4431
+ contextPatch.FileName = localFiles[0]?.fileName;
4432
+ contextPatch.FileSize = localFiles[0]?.size;
4433
+ }
4434
+ return {
4435
+ rawBody,
4436
+ contextPatch
4437
+ };
4438
+ }
4357
4439
  async function dispatchBaiduAppMessage(params) {
4358
4440
  const { cfg, account, msg, core, hooks } = params;
4359
4441
  const safeCfg = cfg ?? {};
4360
- const logger2 = createLogger("openclaw-baiduapp", { log: params.log, error: params.error });
4442
+ const logger3 = createLogger("openclaw-baiduapp", { log: params.log, error: params.error });
4361
4443
  const channel = core.channel;
4362
4444
  if (!channel?.routing?.resolveAgentRoute || !channel.reply?.dispatchReplyWithBufferedBlockDispatcher) {
4363
- logger2.warn("core routing or buffered dispatcher missing, skipping dispatch");
4445
+ logger3.warn("core routing or buffered dispatcher missing, skipping dispatch");
4364
4446
  return;
4365
4447
  }
4366
4448
  const route = channel.routing.resolveAgentRoute({
@@ -4369,13 +4451,17 @@ async function dispatchBaiduAppMessage(params) {
4369
4451
  accountId: account.accountId,
4370
4452
  peer: { kind: "dm", id: "default" }
4371
4453
  });
4372
- logger2.info(`SessionKey: ${route.sessionKey}`);
4373
- route.sessionKey = "agent:main:main";
4374
- logger2.info(
4454
+ logger3.info(`SessionKey: ${route.sessionKey}`);
4455
+ logger3.info(
4375
4456
  `route resolved: sessionKey=${route.sessionKey} agentId=${route.agentId ?? "default"} accountId=${route.accountId}`
4376
4457
  );
4377
- const rawBody = extractBaiduAppContent(msg);
4378
- logger2.debug(
4458
+ const inboundContent = buildBaiduAppInboundDispatchContent(
4459
+ msg,
4460
+ params.inboundMediaFiles ?? [],
4461
+ params.inboundFileDiagnostic
4462
+ );
4463
+ const rawBody = inboundContent.rawBody;
4464
+ logger3.debug(
4379
4465
  `message content extracted: len=${rawBody.length} preview="${rawBody.slice(0, 80)}${rawBody.length > 80 ? "..." : ""}"`
4380
4466
  );
4381
4467
  const storePath = channel.session?.resolveStorePath?.(safeCfg.session?.store, {
@@ -4409,7 +4495,8 @@ async function dispatchBaiduAppMessage(params) {
4409
4495
  MessageSid: msgid,
4410
4496
  OriginatingChannel: "openclaw-baiduapp",
4411
4497
  OriginatingTo: "user",
4412
- CommandAuthorized: true
4498
+ CommandAuthorized: true,
4499
+ ...inboundContent.contextPatch
4413
4500
  }) : {
4414
4501
  Body: body,
4415
4502
  RawBody: rawBody,
@@ -4425,7 +4512,8 @@ async function dispatchBaiduAppMessage(params) {
4425
4512
  MessageSid: msgid,
4426
4513
  OriginatingChannel: "openclaw-baiduapp",
4427
4514
  OriginatingTo: "user",
4428
- CommandAuthorized: true
4515
+ CommandAuthorized: true,
4516
+ ...inboundContent.contextPatch
4429
4517
  };
4430
4518
  if (channel.session?.recordInboundSession && storePath) {
4431
4519
  await channel.session.recordInboundSession({
@@ -4433,7 +4521,7 @@ async function dispatchBaiduAppMessage(params) {
4433
4521
  sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
4434
4522
  ctx: ctxPayload,
4435
4523
  onRecordError: (err) => {
4436
- logger2.error(`openclaw-baiduapp: failed updating session meta: ${String(err)}`);
4524
+ logger3.error(`openclaw-baiduapp: failed updating session meta: ${String(err)}`);
4437
4525
  }
4438
4526
  });
4439
4527
  }
@@ -4442,7 +4530,7 @@ async function dispatchBaiduAppMessage(params) {
4442
4530
  channel: "openclaw-baiduapp",
4443
4531
  accountId: account.accountId
4444
4532
  }) : void 0;
4445
- logger2.info(`dispatching to agent: sessionKey=${route.sessionKey}`);
4533
+ logger3.info(`dispatching to agent: sessionKey=${route.sessionKey}`);
4446
4534
  await channel.reply.dispatchReplyWithBufferedBlockDispatcher({
4447
4535
  ctx: ctxPayload,
4448
4536
  cfg: safeCfg,
@@ -4450,20 +4538,575 @@ async function dispatchBaiduAppMessage(params) {
4450
4538
  deliver: async (payload) => {
4451
4539
  const rawText = payload.text ?? "";
4452
4540
  if (!rawText.trim()) {
4453
- logger2.debug("deliver callback: empty text, skipping");
4541
+ logger3.debug("deliver callback: empty text, skipping");
4454
4542
  return;
4455
4543
  }
4456
4544
  const converted = channel.text?.convertMarkdownTables && tableMode ? channel.text.convertMarkdownTables(rawText, tableMode) : rawText;
4457
- logger2.debug(`deliver callback: textLen=${converted.length}`);
4545
+ logger3.debug(`deliver callback: textLen=${converted.length}`);
4458
4546
  hooks.onChunk(converted);
4459
4547
  },
4460
4548
  onError: (err, info) => {
4461
4549
  hooks.onError?.(err);
4462
- logger2.error(`${info.kind} reply failed: ${String(err)}`);
4550
+ logger3.error(`${info.kind} reply failed: ${String(err)}`);
4551
+ }
4552
+ }
4553
+ });
4554
+ logger3.info(`agent reply dispatch complete: sessionKey=${route.sessionKey}`);
4555
+ }
4556
+ var POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
4557
+ var TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
4558
+ function isNodeErrorWithCode(err, code) {
4559
+ return typeof err === "object" && err !== null && "code" in err && err.code === code;
4560
+ }
4561
+ function resolvePreferredOpenClawTmpDir(options = {}) {
4562
+ const accessSync = options.accessSync ?? fs.accessSync;
4563
+ const chmodSync = options.chmodSync ?? fs.chmodSync;
4564
+ const lstatSync = options.lstatSync ?? fs.lstatSync;
4565
+ const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
4566
+ const warn = options.warn ?? ((message) => console.warn(message));
4567
+ const getuid = options.getuid ?? (() => {
4568
+ try {
4569
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
4570
+ } catch {
4571
+ return void 0;
4572
+ }
4573
+ });
4574
+ const tmpdir$1 = typeof options.tmpdir === "function" ? options.tmpdir : tmpdir;
4575
+ const uid = getuid();
4576
+ const isSecureDirForUser = (st) => {
4577
+ if (uid === void 0) {
4578
+ return true;
4579
+ }
4580
+ if (typeof st.uid === "number" && st.uid !== uid) {
4581
+ return false;
4582
+ }
4583
+ if (typeof st.mode === "number" && (st.mode & 18) !== 0) {
4584
+ return false;
4585
+ }
4586
+ return true;
4587
+ };
4588
+ const fallback = () => {
4589
+ const base = tmpdir$1();
4590
+ const suffix = uid === void 0 ? "openclaw" : `openclaw-${uid}`;
4591
+ return path2.join(base, suffix);
4592
+ };
4593
+ const isTrustedTmpDir = (st) => {
4594
+ return st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st);
4595
+ };
4596
+ const resolveDirState = (candidatePath) => {
4597
+ try {
4598
+ const candidate = lstatSync(candidatePath);
4599
+ if (!isTrustedTmpDir(candidate)) {
4600
+ return "invalid";
4601
+ }
4602
+ accessSync(candidatePath, TMP_DIR_ACCESS_MODE);
4603
+ return "available";
4604
+ } catch (err) {
4605
+ if (isNodeErrorWithCode(err, "ENOENT")) {
4606
+ return "missing";
4607
+ }
4608
+ return "invalid";
4609
+ }
4610
+ };
4611
+ const tryRepairWritableBits = (candidatePath) => {
4612
+ try {
4613
+ const st = lstatSync(candidatePath);
4614
+ if (!st.isDirectory() || st.isSymbolicLink()) {
4615
+ return false;
4616
+ }
4617
+ if (uid !== void 0 && typeof st.uid === "number" && st.uid !== uid) {
4618
+ return false;
4619
+ }
4620
+ if (typeof st.mode !== "number" || (st.mode & 18) === 0) {
4621
+ return false;
4622
+ }
4623
+ chmodSync(candidatePath, 448);
4624
+ warn(`[openclaw] tightened permissions on temp dir: ${candidatePath}`);
4625
+ return resolveDirState(candidatePath) === "available";
4626
+ } catch {
4627
+ return false;
4628
+ }
4629
+ };
4630
+ const ensureTrustedFallbackDir = () => {
4631
+ const fallbackPath = fallback();
4632
+ const state = resolveDirState(fallbackPath);
4633
+ if (state === "available") {
4634
+ return fallbackPath;
4635
+ }
4636
+ if (state === "invalid") {
4637
+ if (tryRepairWritableBits(fallbackPath)) {
4638
+ return fallbackPath;
4639
+ }
4640
+ throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
4641
+ }
4642
+ try {
4643
+ mkdirSync(fallbackPath, { recursive: true, mode: 448 });
4644
+ chmodSync(fallbackPath, 448);
4645
+ } catch {
4646
+ throw new Error(`Unable to create fallback OpenClaw temp dir: ${fallbackPath}`);
4647
+ }
4648
+ if (resolveDirState(fallbackPath) !== "available" && !tryRepairWritableBits(fallbackPath)) {
4649
+ throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
4650
+ }
4651
+ return fallbackPath;
4652
+ };
4653
+ const existingPreferredState = resolveDirState(POSIX_OPENCLAW_TMP_DIR);
4654
+ if (existingPreferredState === "available") {
4655
+ return POSIX_OPENCLAW_TMP_DIR;
4656
+ }
4657
+ if (existingPreferredState === "invalid") {
4658
+ if (tryRepairWritableBits(POSIX_OPENCLAW_TMP_DIR)) {
4659
+ return POSIX_OPENCLAW_TMP_DIR;
4660
+ }
4661
+ return ensureTrustedFallbackDir();
4662
+ }
4663
+ try {
4664
+ accessSync("/tmp", TMP_DIR_ACCESS_MODE);
4665
+ mkdirSync(POSIX_OPENCLAW_TMP_DIR, { recursive: true, mode: 448 });
4666
+ chmodSync(POSIX_OPENCLAW_TMP_DIR, 448);
4667
+ if (resolveDirState(POSIX_OPENCLAW_TMP_DIR) !== "available" && !tryRepairWritableBits(POSIX_OPENCLAW_TMP_DIR)) {
4668
+ return ensureTrustedFallbackDir();
4669
+ }
4670
+ return POSIX_OPENCLAW_TMP_DIR;
4671
+ } catch {
4672
+ return ensureTrustedFallbackDir();
4673
+ }
4674
+ }
4675
+
4676
+ // src/media.ts
4677
+ var MEDIA_TEMP_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
4678
+ var DEFAULT_DOWNLOAD_TIMEOUT_MS = 3e4;
4679
+ var DEFAULT_DOWNLOAD_MAX_BYTES = 20 * 1024 * 1024;
4680
+ path2.join("openclaw-baiduapp", "media");
4681
+ var require3 = createRequire(import.meta.url);
4682
+ var BaiduCloudSdk = require3("@baiducloud/sdk");
4683
+ var DefaultBosClient = BaiduCloudSdk.BosClient;
4684
+ var MAX_DOWNLOAD_REDIRECTS = 5;
4685
+ var EMPTY_QUERY_MD5 = crypto3.createHash("md5").update("").digest("hex");
4686
+ var SKS_BASE_TOKEN_POSITIONS = [12, 37, 5, 23, 48, 15, 62, 33];
4687
+ async function fetchSksCredentials(account, deps = {}) {
4688
+ if (!account.appKey?.trim()) {
4689
+ throw new Error("Cannot fetch SKS credentials without account.appKey");
4690
+ }
4691
+ if (!account.appSecret?.trim()) {
4692
+ throw new Error("Cannot fetch SKS credentials without account.appSecret");
4693
+ }
4694
+ const fetchImpl = deps.fetchImpl ?? fetch;
4695
+ const apiBase = account.apiBase.replace(/\/+$/, "");
4696
+ const pageLid = deps.createPageLid?.() ?? generateSksPageLid();
4697
+ const timestamp = String((deps.now?.() ?? /* @__PURE__ */ new Date()).getTime());
4698
+ const token = createSksRequestToken({ pageLid, timestamp });
4699
+ const url = `${apiBase}/file/sts?ak=${encodeURIComponent(account.appKey)}&sk=${encodeURIComponent(account.appSecret)}&tk=${encodeURIComponent(token)}`;
4700
+ const response = await fetchImpl(url, { method: "POST" });
4701
+ if (!response.ok) {
4702
+ throw new Error(`SKS request failed with HTTP ${response.status}`);
4703
+ }
4704
+ let payload;
4705
+ try {
4706
+ payload = await response.json();
4707
+ } catch (error) {
4708
+ throw new Error(`SKS response is not valid JSON: ${formatError(error)}`, { cause: error });
4709
+ }
4710
+ return parseSksCredentialsFromPayload(payload);
4711
+ }
4712
+ function parseSksCredentialsFromPayload(payload) {
4713
+ const parsed = parseSksResponse(payload);
4714
+ if (parsed.status !== 0) {
4715
+ throw new Error(`SKS request failed with status ${parsed.status}`);
4716
+ }
4717
+ if (!parsed.data) {
4718
+ throw new Error("SKS response missing data");
4719
+ }
4720
+ return {
4721
+ ak: requireNonEmptyString(parsed.data.ak, "SKS data.ak"),
4722
+ sk: requireNonEmptyString(parsed.data.sk, "SKS data.sk"),
4723
+ sessionToken: requireNonEmptyString(parsed.data.token, "SKS data.token"),
4724
+ bucketName: requireNonEmptyString(parsed.data.bucketName, "SKS data.bucketName"),
4725
+ prefixPath: sanitizeObjectKeyPrefix(requireNonEmptyString(parsed.data.preFixPath, "SKS data.preFixPath")),
4726
+ endpoint: requireNonEmptyString(parsed.data.bceUrl, "SKS data.bceUrl")
4727
+ };
4728
+ }
4729
+ function createSksRequestToken(params) {
4730
+ const pageLid = requireNonEmptyString(params.pageLid, "SKS pageLid");
4731
+ const timestamp = requireNonEmptyString(params.timestamp, "SKS timestamp");
4732
+ const baseToken = createSksBaseToken(pageLid);
4733
+ const payload = `${baseToken}|${EMPTY_QUERY_MD5}|${timestamp}|${pageLid}`;
4734
+ return `${Buffer.from(payload, "utf8").toString("base64")}-${pageLid}-3`;
4735
+ }
4736
+ function createBosClient(credentials, deps = {}) {
4737
+ const BosClientCtor = deps.bosClientCtor ?? DefaultBosClient;
4738
+ return new BosClientCtor({
4739
+ endpoint: credentials.endpoint,
4740
+ sessionToken: credentials.sessionToken,
4741
+ credentials: {
4742
+ ak: credentials.ak,
4743
+ sk: credentials.sk
4744
+ }
4745
+ });
4746
+ }
4747
+ async function uploadLocalFileToBos(options, deps = {}) {
4748
+ const localFile = await assertUploadableLocalFile(options.filePath);
4749
+ const credentials = await fetchSksCredentials(options.account, deps);
4750
+ const client = createBosClient(credentials, deps);
4751
+ const sourceName = options.fileName ?? path2.basename(localFile);
4752
+ const sanitizedFileName = sanitizeFileName(sourceName);
4753
+ const key = buildBosObjectKey({
4754
+ prefixPath: credentials.prefixPath,
4755
+ fileName: sanitizedFileName,
4756
+ now: deps.now
4757
+ });
4758
+ const uploadOptions = options.contentType?.trim() ? {
4759
+ "Content-Type": options.contentType.trim()
4760
+ } : void 0;
4761
+ const [, fileStat] = await Promise.all([
4762
+ client.putObjectFromFile(credentials.bucketName, key, localFile, uploadOptions),
4763
+ fs2.stat(localFile)
4764
+ ]);
4765
+ return {
4766
+ bucketName: credentials.bucketName,
4767
+ key,
4768
+ url: client.generateUrl(credentials.bucketName, key),
4769
+ fileName: sanitizedFileName,
4770
+ fileSize: fileStat.size
4771
+ };
4772
+ }
4773
+ async function downloadInboundFileToTemp(options, deps = {}) {
4774
+ const remoteUrl = parseRemoteHttpUrl(options.remoteUrl);
4775
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS;
4776
+ const maxBytes = options.maxBytes ?? DEFAULT_DOWNLOAD_MAX_BYTES;
4777
+ const fetchImpl = deps.fetchImpl ?? fetch;
4778
+ const tempDir = await ensurePluginTempDir(deps.resolveTmpDir);
4779
+ const lookupHost = deps.lookupHost ?? defaultLookupHost;
4780
+ const controller = new AbortController();
4781
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
4782
+ try {
4783
+ const response = await fetchInboundFileWithRedirectProtection({
4784
+ fetchImpl,
4785
+ lookupHost,
4786
+ remoteUrl,
4787
+ signal: controller.signal
4788
+ });
4789
+ if (!response.ok) {
4790
+ throw new Error(`Download failed with HTTP ${response.status}`);
4791
+ }
4792
+ const declaredLength = response.headers.get("content-length");
4793
+ if (declaredLength) {
4794
+ const parsedLength = Number.parseInt(declaredLength, 10);
4795
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) {
4796
+ throw new Error(`Download exceeds max size of ${maxBytes} bytes`);
4797
+ }
4798
+ }
4799
+ const resolvedFileName = resolveDownloadFileName({
4800
+ explicitFileName: options.fileName,
4801
+ headerFileName: parseContentDispositionFileName(response.headers.get("content-disposition")),
4802
+ urlFileName: path2.posix.basename(remoteUrl.pathname)
4803
+ });
4804
+ const targetPath = buildTempFilePath({
4805
+ tempDir,
4806
+ fileName: resolvedFileName,
4807
+ now: deps.now
4808
+ });
4809
+ const arrayBuffer = await response.arrayBuffer();
4810
+ const buffer = Buffer.from(arrayBuffer);
4811
+ if (buffer.byteLength > maxBytes) {
4812
+ throw new Error(`Download exceeds max size of ${maxBytes} bytes`);
4813
+ }
4814
+ await fs2.writeFile(targetPath, buffer, { mode: 384, flag: "wx" });
4815
+ return {
4816
+ path: targetPath,
4817
+ fileName: path2.basename(targetPath),
4818
+ size: buffer.byteLength,
4819
+ contentType: response.headers.get("content-type") ?? void 0
4820
+ };
4821
+ } catch (error) {
4822
+ if (isAbortError(error)) {
4823
+ throw new Error(`Download timed out after ${timeoutMs}ms`, { cause: error });
4824
+ }
4825
+ throw error;
4826
+ } finally {
4827
+ clearTimeout(timeout);
4828
+ }
4829
+ }
4830
+ async function pruneExpiredTempFiles(deps = {}) {
4831
+ const tempDir = await ensurePluginTempDir(deps.resolveTmpDir);
4832
+ const now = deps.now?.() ?? /* @__PURE__ */ new Date();
4833
+ let deletedFiles = 0;
4834
+ const walk = async (currentDir) => {
4835
+ const entries = await fs2.readdir(currentDir, { withFileTypes: true });
4836
+ let hasRemainingEntries = false;
4837
+ for (const entry of entries) {
4838
+ const entryPath = path2.join(currentDir, entry.name);
4839
+ const stat = await fs2.lstat(entryPath);
4840
+ if (stat.isSymbolicLink()) {
4841
+ throw new Error(`Refusing to traverse symlink in media temp dir: ${entryPath}`);
4842
+ }
4843
+ if (stat.isDirectory()) {
4844
+ const childEmpty = await walk(entryPath);
4845
+ if (childEmpty) {
4846
+ await fs2.rmdir(entryPath);
4847
+ } else {
4848
+ hasRemainingEntries = true;
4849
+ }
4850
+ continue;
4851
+ }
4852
+ if (!stat.isFile()) {
4853
+ hasRemainingEntries = true;
4854
+ continue;
4855
+ }
4856
+ if (now.getTime() - stat.mtimeMs > MEDIA_TEMP_RETENTION_MS) {
4857
+ await fs2.rm(entryPath, { force: true });
4858
+ deletedFiles += 1;
4859
+ continue;
4463
4860
  }
4861
+ hasRemainingEntries = true;
4464
4862
  }
4863
+ return !hasRemainingEntries;
4864
+ };
4865
+ await walk(tempDir);
4866
+ return { deletedFiles };
4867
+ }
4868
+ function parseSksResponse(payload) {
4869
+ if (!payload || typeof payload !== "object") {
4870
+ throw new Error("SKS response must be an object");
4871
+ }
4872
+ const record = payload;
4873
+ const status = typeof record.code === "number" ? record.code : typeof record.status === "number" ? record.status : void 0;
4874
+ const data = record.data;
4875
+ return {
4876
+ status,
4877
+ data: data && typeof data === "object" ? {
4878
+ ak: data.ak,
4879
+ sk: data.sk,
4880
+ token: data.token,
4881
+ bucketName: data.bucketName,
4882
+ preFixPath: data.preFixPath,
4883
+ bceUrl: data.bceUrl
4884
+ } : void 0
4885
+ };
4886
+ }
4887
+ function generateSksPageLid() {
4888
+ return crypto3.randomBytes(16).toString("hex");
4889
+ }
4890
+ function createSksBaseToken(pageLid) {
4891
+ const pageLidHash = crypto3.createHash("sha256").update(pageLid).digest("hex");
4892
+ const characters = SKS_BASE_TOKEN_POSITIONS.map((position) => {
4893
+ const character = pageLidHash[position];
4894
+ if (!character) {
4895
+ throw new Error(`SKS base token position is out of range: ${position}`);
4896
+ }
4897
+ return character;
4898
+ });
4899
+ return characters.join("");
4900
+ }
4901
+ function requireNonEmptyString(value, label) {
4902
+ if (typeof value !== "string" || !value.trim()) {
4903
+ throw new Error(`${label} is required`);
4904
+ }
4905
+ return value.trim();
4906
+ }
4907
+ function sanitizeObjectKeyPrefix(prefixPath) {
4908
+ const normalized = prefixPath.replace(/\\/g, "/").trim();
4909
+ const segments = normalized.split("/").map((segment) => segment.trim()).filter(Boolean);
4910
+ if (segments.length === 0) {
4911
+ throw new Error("SKS data.preFixPath must contain at least one safe path segment");
4912
+ }
4913
+ const safeSegments = segments.map((segment) => {
4914
+ if (segment === "." || segment === "..") {
4915
+ throw new Error("SKS data.preFixPath contains path traversal");
4916
+ }
4917
+ return sanitizeObjectKeySegment(segment);
4465
4918
  });
4466
- logger2.info(`agent reply dispatch complete: sessionKey=${route.sessionKey}`);
4919
+ return safeSegments.join("/");
4920
+ }
4921
+ function sanitizeObjectKeySegment(segment) {
4922
+ const sanitized = segment.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4923
+ if (!sanitized) {
4924
+ throw new Error(`Object key segment is not safe: ${segment}`);
4925
+ }
4926
+ return sanitized.slice(0, 80);
4927
+ }
4928
+ function sanitizeFileName(fileName) {
4929
+ assertSafeUntrustedFileName(fileName);
4930
+ const parsed = path2.parse(fileName.trim());
4931
+ const safeBase = parsed.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4932
+ const safeExt = parsed.ext.replace(/[^a-zA-Z0-9.]+/g, "").slice(0, 16);
4933
+ const baseName = safeBase || "file";
4934
+ return `${baseName.slice(0, 80)}${safeExt}`;
4935
+ }
4936
+ function assertSafeUntrustedFileName(fileName) {
4937
+ const trimmed = fileName.trim();
4938
+ if (!trimmed) {
4939
+ throw new Error("Filename cannot be empty");
4940
+ }
4941
+ if (trimmed.includes("/") || trimmed.includes("\\")) {
4942
+ throw new Error(`Filename contains path separators: ${fileName}`);
4943
+ }
4944
+ if (trimmed === "." || trimmed === "..") {
4945
+ throw new Error(`Filename is not safe: ${fileName}`);
4946
+ }
4947
+ }
4948
+ function buildBosObjectKey(params) {
4949
+ const timestamp = formatUtcDate(params.now?.() ?? /* @__PURE__ */ new Date());
4950
+ const uniqueId = crypto3.randomUUID();
4951
+ return `${params.prefixPath}/${timestamp}/${uniqueId}-${params.fileName}`;
4952
+ }
4953
+ function formatUtcDate(date) {
4954
+ return date.toISOString().slice(0, 10);
4955
+ }
4956
+ async function assertUploadableLocalFile(filePath) {
4957
+ const resolvedPath = path2.resolve(filePath);
4958
+ const stat = await fs2.lstat(resolvedPath);
4959
+ if (stat.isSymbolicLink()) {
4960
+ throw new Error(`Refusing to upload symlink: ${resolvedPath}`);
4961
+ }
4962
+ if (!stat.isFile()) {
4963
+ throw new Error(`Upload source must be a regular file: ${resolvedPath}`);
4964
+ }
4965
+ return resolvedPath;
4966
+ }
4967
+ function parseRemoteHttpUrl(remoteUrl) {
4968
+ let url;
4969
+ try {
4970
+ url = new URL(remoteUrl);
4971
+ } catch {
4972
+ throw new Error(`Invalid remote URL: ${remoteUrl}`);
4973
+ }
4974
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
4975
+ throw new Error(`Unsupported remote URL protocol: ${url.protocol}`);
4976
+ }
4977
+ if (url.username || url.password) {
4978
+ throw new Error("Remote URL must not contain credentials");
4979
+ }
4980
+ return url;
4981
+ }
4982
+ async function fetchInboundFileWithRedirectProtection(params) {
4983
+ let currentUrl = params.remoteUrl;
4984
+ for (let redirectCount = 0; redirectCount <= MAX_DOWNLOAD_REDIRECTS; redirectCount += 1) {
4985
+ await assertSafeRemoteHttpUrl(currentUrl, params.lookupHost);
4986
+ const response = await params.fetchImpl(currentUrl.toString(), {
4987
+ signal: params.signal,
4988
+ redirect: "manual"
4989
+ });
4990
+ if (!isRedirectResponse(response.status)) {
4991
+ return response;
4992
+ }
4993
+ const location = response.headers.get("location");
4994
+ if (!location) {
4995
+ throw new Error(`Redirect response missing Location header: HTTP ${response.status}`);
4996
+ }
4997
+ if (redirectCount === MAX_DOWNLOAD_REDIRECTS) {
4998
+ throw new Error(`Download exceeded max redirects of ${MAX_DOWNLOAD_REDIRECTS}`);
4999
+ }
5000
+ currentUrl = parseRemoteHttpUrl(new URL(location, currentUrl).toString());
5001
+ }
5002
+ throw new Error(`Download exceeded max redirects of ${MAX_DOWNLOAD_REDIRECTS}`);
5003
+ }
5004
+ async function assertSafeRemoteHttpUrl(remoteUrl, lookupHost) {
5005
+ const hostname = remoteUrl.hostname.trim().replace(/\.$/, "").toLowerCase();
5006
+ if (!hostname) {
5007
+ throw new Error("Remote URL hostname is required");
5008
+ }
5009
+ if (isBlockedHostname(hostname)) {
5010
+ throw new Error(`Remote URL host is not allowed: ${hostname}`);
5011
+ }
5012
+ if (net.isIP(hostname)) {
5013
+ if (isBlockedIpAddress(hostname)) {
5014
+ throw new Error(`Remote URL host is not allowed: ${hostname}`);
5015
+ }
5016
+ return;
5017
+ }
5018
+ let resolvedAddresses;
5019
+ try {
5020
+ resolvedAddresses = await lookupHost(hostname);
5021
+ } catch (error) {
5022
+ throw new Error(`Remote URL host lookup failed: ${hostname} (${formatError(error)})`, { cause: error });
5023
+ }
5024
+ if (!resolvedAddresses.length) {
5025
+ throw new Error(`Remote URL host lookup returned no addresses: ${hostname}`);
5026
+ }
5027
+ const blockedAddress = resolvedAddresses.find((result) => isBlockedIpAddress(result.address));
5028
+ if (blockedAddress) {
5029
+ throw new Error(`Remote URL host resolved to a blocked address: ${hostname} -> ${blockedAddress.address}`);
5030
+ }
5031
+ }
5032
+ async function defaultLookupHost(hostname) {
5033
+ return await lookup(hostname, { all: true, verbatim: true });
5034
+ }
5035
+ function isRedirectResponse(status) {
5036
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
5037
+ }
5038
+ function isBlockedHostname(hostname) {
5039
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "metadata" || hostname.endsWith(".metadata") || hostname === "metadata.google.internal" || hostname.endsWith(".metadata.google.internal");
5040
+ }
5041
+ function isBlockedIpAddress(address) {
5042
+ const family = net.isIP(address);
5043
+ if (family === 4) {
5044
+ return isBlockedIpv4Address(address);
5045
+ }
5046
+ if (family === 6) {
5047
+ return isBlockedIpv6Address(address);
5048
+ }
5049
+ return false;
5050
+ }
5051
+ function isBlockedIpv4Address(address) {
5052
+ const octets = address.split(".").map((part) => Number.parseInt(part, 10));
5053
+ if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
5054
+ return false;
5055
+ }
5056
+ const [a, b] = octets;
5057
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a === 224 && b >= 0 || a >= 240;
5058
+ }
5059
+ function isBlockedIpv6Address(address) {
5060
+ const normalized = address.toLowerCase();
5061
+ return normalized === "::1" || normalized === "::" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:") || normalized.startsWith("::ffff:127.") || normalized.startsWith("::ffff:10.") || normalized.startsWith("::ffff:169.254.") || normalized.startsWith("::ffff:172.16.") || normalized.startsWith("::ffff:172.17.") || normalized.startsWith("::ffff:172.18.") || normalized.startsWith("::ffff:172.19.") || normalized.startsWith("::ffff:172.2") || normalized.startsWith("::ffff:172.30.") || normalized.startsWith("::ffff:172.31.") || normalized.startsWith("::ffff:192.168.");
5062
+ }
5063
+ function parseContentDispositionFileName(contentDisposition) {
5064
+ if (!contentDisposition) {
5065
+ return void 0;
5066
+ }
5067
+ const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
5068
+ if (utf8Match?.[1]) {
5069
+ return decodeURIComponent(utf8Match[1].replace(/^"|"$/g, "").trim());
5070
+ }
5071
+ const plainMatch = contentDisposition.match(/filename=([^;]+)/i);
5072
+ if (plainMatch?.[1]) {
5073
+ return plainMatch[1].replace(/^"|"$/g, "").trim();
5074
+ }
5075
+ return void 0;
5076
+ }
5077
+ function resolveDownloadFileName(params) {
5078
+ const candidate = params.explicitFileName ?? params.headerFileName ?? params.urlFileName ?? "file.bin";
5079
+ return sanitizeFileName(candidate);
5080
+ }
5081
+ function buildTempFilePath(params) {
5082
+ const timestamp = (params.now?.() ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5083
+ return path2.join(params.tempDir, `${timestamp}-${crypto3.randomUUID()}-${params.fileName}`);
5084
+ }
5085
+ async function ensurePluginTempDir(resolveTmpDir) {
5086
+ const baseTmpDir = resolveTmpDir?.() ?? resolvePreferredOpenClawTmpDir();
5087
+ const pluginRoot = await ensureSecureChildDirectory(baseTmpDir, "openclaw-baiduapp");
5088
+ return ensureSecureChildDirectory(pluginRoot, "media");
5089
+ }
5090
+ async function ensureSecureChildDirectory(parentDir, childName) {
5091
+ const targetDir = path2.join(parentDir, childName);
5092
+ const stat = await fs2.lstat(targetDir).catch(() => null);
5093
+ if (stat) {
5094
+ if (stat.isSymbolicLink()) {
5095
+ throw new Error(`Refusing to use symlink as temp directory: ${targetDir}`);
5096
+ }
5097
+ if (!stat.isDirectory()) {
5098
+ throw new Error(`Expected directory but found file: ${targetDir}`);
5099
+ }
5100
+ return targetDir;
5101
+ }
5102
+ await fs2.mkdir(targetDir, { mode: 448 });
5103
+ return targetDir;
5104
+ }
5105
+ function formatError(error) {
5106
+ return error instanceof Error ? error.message : String(error);
5107
+ }
5108
+ function isAbortError(error) {
5109
+ return error instanceof Error && error.name === "AbortError";
4467
5110
  }
4468
5111
 
4469
5112
  // src/runtime.ts
@@ -4488,6 +5131,7 @@ var msgidToStreamId = /* @__PURE__ */ new Map();
4488
5131
  var STREAM_TTL_MS = 10 * 60 * 1e3;
4489
5132
  var STREAM_MAX_BYTES = 512e3;
4490
5133
  var MAX_MESSAGE_BYTES = 2048;
5134
+ var tempPruneInFlight = null;
4491
5135
  function normalizeWebhookPath(raw) {
4492
5136
  const trimmed = raw.trim();
4493
5137
  if (!trimmed) {
@@ -4654,7 +5298,7 @@ function buildStreamReplyFromState(state) {
4654
5298
  };
4655
5299
  }
4656
5300
  function createStreamId() {
4657
- return crypto.randomBytes(16).toString("hex");
5301
+ return crypto3.randomBytes(16).toString("hex");
4658
5302
  }
4659
5303
  function parseBaiduAppPlainMessage(raw) {
4660
5304
  const trimmed = raw.trim();
@@ -4725,230 +5369,96 @@ function buildLogger(target) {
4725
5369
  error: target.runtime.error
4726
5370
  });
4727
5371
  }
4728
- function registerBaiduAppWebhookTarget(target) {
4729
- const key = normalizeWebhookPath(target.path);
4730
- const normalizedTarget = { ...target, path: key };
4731
- const existing = webhookTargets.get(key) ?? [];
4732
- const next = [...existing, normalizedTarget];
4733
- webhookTargets.set(key, next);
4734
- return () => {
4735
- const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
4736
- if (updated.length > 0) {
4737
- webhookTargets.set(key, updated);
4738
- } else {
4739
- webhookTargets.delete(key);
4740
- }
4741
- };
5372
+ function buildRuntimeLogger(runtime2) {
5373
+ return createLogger("openclaw-baiduapp", {
5374
+ log: runtime2.log,
5375
+ error: runtime2.error
5376
+ });
4742
5377
  }
4743
- async function handleBaiduAppWebhookRequest(req, res) {
4744
- pruneStreams();
4745
- const path = resolvePath(req);
4746
- const targets = webhookTargets.get(path);
4747
- if (!targets || targets.length === 0) {
4748
- return false;
5378
+ function triggerBestEffortTempPrune(params) {
5379
+ if (tempPruneInFlight) {
5380
+ return;
4749
5381
  }
4750
- const query = resolveQueryParams(req);
4751
- const timestamp = query.get("timestamp") ?? "";
4752
- const nonce = query.get("nonce") ?? "";
4753
- const signature = resolveSignatureParam(query);
4754
- const primary = targets[0];
4755
- const logger2 = buildLogger(primary);
4756
- if (req.method === "GET") {
4757
- const echostr = query.get("echostr") ?? "";
4758
- if (!timestamp || !nonce || !signature || !echostr) {
4759
- jsonError(res, "missing query params", 400);
4760
- return true;
4761
- }
4762
- const signatureMatched2 = targets.filter((candidate) => {
4763
- if (!candidate.account.token) {
4764
- return false;
5382
+ const logger3 = buildRuntimeLogger(params.runtime);
5383
+ tempPruneInFlight = pruneExpiredTempFiles().then(() => void 0).catch((error) => {
5384
+ logger3.warn(`temp prune failed: reason=${params.reason} error=${String(error)}`);
5385
+ }).finally(() => {
5386
+ tempPruneInFlight = null;
5387
+ });
5388
+ }
5389
+ async function downloadInboundFiles(params) {
5390
+ const rawFiles = params.msg.files;
5391
+ if (!Array.isArray(rawFiles) || rawFiles.length === 0) {
5392
+ return { localFiles: [], diagnostic: {} };
5393
+ }
5394
+ const localFiles = [];
5395
+ const failurePlaceholders = [];
5396
+ try {
5397
+ for (const [index, rawFile] of rawFiles.entries()) {
5398
+ if (!rawFile || typeof rawFile !== "object") {
5399
+ params.logger.warn(`inbound file skipped: index=${index} reason=invalid-file-record`);
5400
+ continue;
5401
+ }
5402
+ const fileRecord = rawFile;
5403
+ const remoteUrl = typeof fileRecord.url === "string" ? fileRecord.url.trim() : "";
5404
+ if (!remoteUrl) {
5405
+ params.logger.warn(`inbound file skipped: index=${index} reason=missing-url`);
5406
+ continue;
5407
+ }
5408
+ try {
5409
+ const downloaded = await downloadInboundFileToTemp({ remoteUrl });
5410
+ localFiles.push({
5411
+ ...downloaded,
5412
+ contentType: downloaded.contentType ?? (typeof fileRecord.fileType === "string" && fileRecord.fileType.trim() ? fileRecord.fileType : void 0)
5413
+ });
5414
+ params.logger.info(
5415
+ `inbound file downloaded: index=${index} path=${downloaded.path} size=${downloaded.size}`
5416
+ );
5417
+ } catch (err) {
5418
+ failurePlaceholders.push(`[file download failed] ${remoteUrl} (${err instanceof Error ? err.message : String(err)})`);
5419
+ params.logger.warn(`inbound file download failed: index=${index} error=${String(err)}`);
4765
5420
  }
4766
- return verifyBaiduAppSignature({
4767
- token: candidate.account.token,
4768
- timestamp,
4769
- nonce,
4770
- encrypt: echostr,
4771
- signature
4772
- });
4773
- });
4774
- if (signatureMatched2.length === 0) {
4775
- jsonError(res, "unauthorized");
4776
- return true;
4777
- }
4778
- const decryptable2 = signatureMatched2.filter((candidate) => Boolean(candidate.account.encodingAESKey));
4779
- if (decryptable2.length === 0) {
4780
- jsonError(res, "unauthorized");
4781
- return true;
4782
- }
4783
- const decryptedCandidates2 = decryptBaiduAppCandidates({
4784
- candidates: decryptable2,
4785
- encrypt: echostr
4786
- });
4787
- if (decryptedCandidates2.length === 0) {
4788
- jsonError(res, "decrypt failed");
4789
- return true;
4790
5421
  }
4791
- const selected2 = selectDecryptedTarget({ candidates: decryptedCandidates2, logger: logger2 });
4792
- jsonOk(res, selected2.plaintext);
4793
- return true;
5422
+ } finally {
5423
+ triggerBestEffortTempPrune({ runtime: params.runtime, reason: "inbound-file" });
4794
5424
  }
4795
- if (req.method !== "POST") {
4796
- jsonError(res, "Method Not Allowed", 405);
4797
- return true;
4798
- }
4799
- if (!timestamp || !nonce || !signature) {
4800
- jsonError(res, "missing query params");
4801
- return true;
4802
- }
4803
- const body = await readRawBody(req, 1024 * 1024);
4804
- if (!body.ok || !body.raw) {
4805
- jsonError(res, body.error ?? "invalid payload");
4806
- return true;
4807
- }
4808
- const rawBody = body.raw;
4809
- let encrypt = "";
4810
- let msgSignature = signature;
4811
- let msgTimestamp = timestamp;
4812
- let msgNonce = nonce;
4813
- if (isXmlFormat(rawBody)) {
4814
- const xmlData = parseXmlBody(rawBody);
4815
- encrypt = xmlData.Encrypt ?? "";
4816
- msgSignature = xmlData.MsgSignature ?? signature;
4817
- msgTimestamp = xmlData.TimeStamp ?? timestamp;
4818
- msgNonce = xmlData.Nonce ?? nonce;
4819
- logger2.info(`inbound xml parsed: hasEncrypt=${Boolean(encrypt)}, msg_signature=${msgSignature ? "yes" : "no"}`);
4820
- } else {
4821
- try {
4822
- const record = JSON.parse(rawBody);
4823
- encrypt = String(record.encrypt ?? record.Encrypt ?? "");
4824
- logger2.info(`inbound json parsed: hasEncrypt=${Boolean(encrypt)}`);
4825
- } catch {
4826
- logger2.warn(`inbound payload parse failed: not valid xml or json`);
4827
- jsonError(res, "invalid payload format");
4828
- return true;
4829
- }
4830
- }
4831
- if (!encrypt) {
4832
- jsonError(res, "missing encrypt");
4833
- return true;
4834
- }
4835
- const signatureMatched = targets.filter((candidate) => {
4836
- if (!candidate.account.token) {
4837
- return false;
5425
+ return {
5426
+ localFiles,
5427
+ diagnostic: {
5428
+ failurePlaceholders
4838
5429
  }
4839
- return verifyBaiduAppSignature({
4840
- token: candidate.account.token,
4841
- timestamp: msgTimestamp,
4842
- nonce: msgNonce,
4843
- encrypt,
4844
- signature: msgSignature
4845
- });
4846
- });
4847
- if (signatureMatched.length === 0) {
4848
- logger2.warn(`signature verification failed: checked ${targets.length} account(s), none matched`);
4849
- jsonError(res, "unauthorized");
4850
- return true;
4851
- }
4852
- logger2.debug(`signature verified: ${signatureMatched.length} account(s) matched`);
4853
- const decryptable = signatureMatched.filter((candidate) => Boolean(candidate.account.encodingAESKey));
4854
- if (decryptable.length === 0) {
4855
- logger2.warn(`no account has encodingAESKey configured`);
4856
- jsonError(res, "openclaw-baiduapp not configured");
4857
- return true;
4858
- }
4859
- const decryptedCandidates = decryptBaiduAppCandidates({
4860
- candidates: decryptable,
4861
- encrypt
4862
- });
4863
- if (decryptedCandidates.length === 0) {
4864
- logger2.warn(`decrypt failed for all ${decryptable.length} candidate account(s)`);
4865
- jsonError(res, "decrypt failed");
4866
- return true;
4867
- }
4868
- const selected = selectDecryptedTarget({ candidates: decryptedCandidates, logger: logger2 });
4869
- const target = selected.target;
4870
- if (!target.account.configured || !target.account.token || !target.account.encodingAESKey) {
4871
- logger2.warn(`selected account ${target.account.accountId} not fully configured`);
4872
- jsonError(res, "openclaw-baiduapp not configured");
4873
- return true;
4874
- }
4875
- const msg = selected.msg;
5430
+ };
5431
+ }
5432
+ async function processBaiduAppInboundMessage(params) {
5433
+ const { target, msg } = params;
5434
+ const logger3 = buildLogger(target);
4876
5435
  target.statusSink?.({ lastInboundAt: Date.now() });
4877
5436
  const msgtype = String(msg.msgtype ?? msg.MsgType ?? "").toLowerCase();
4878
5437
  const msgid = msg.msgid ?? msg.MsgId ? String(msg.msgid ?? msg.MsgId) : void 0;
4879
- logger2.info(`inbound: type=${msgtype || "unknown"} msgid=${msgid ?? "none"} account=${target.account.accountId}`);
5438
+ logger3.info(`inbound: type=${msgtype || "unknown"} msgid=${msgid ?? "none"} account=${target.account.accountId}`);
4880
5439
  if (msgtype === "stream") {
4881
5440
  const streamId2 = String(msg.stream?.id ?? "").trim();
4882
5441
  const state = streamId2 ? streams.get(streamId2) : void 0;
4883
- logger2.info(
5442
+ logger3.info(
4884
5443
  `[REPLY-MODE:STREAM-POLL] stream poll request: streamId=${streamId2 || "none"} found=${Boolean(state)} finished=${state?.finished ?? "n/a"} contentLen=${state?.content.length ?? 0}`
4885
5444
  );
4886
- const reply = state ? buildStreamReplyFromState(state) : buildStreamReplyFromState({
5445
+ return state ? buildStreamReplyFromState(state) : buildStreamReplyFromState({
4887
5446
  streamId: streamId2 || "unknown",
4888
5447
  finished: true,
4889
5448
  content: ""
4890
5449
  });
4891
- jsonOk(
4892
- res,
4893
- buildEncryptedJsonReply({
4894
- account: target.account,
4895
- plaintextJson: reply,
4896
- nonce: msgNonce,
4897
- timestamp: msgTimestamp
4898
- })
4899
- );
4900
- return true;
4901
5450
  }
4902
5451
  if (msgid && msgidToStreamId.has(msgid)) {
4903
5452
  const streamId2 = msgidToStreamId.get(msgid) ?? "";
4904
- logger2.debug(`duplicate msgid detected: msgid=${msgid} streamId=${streamId2}, returning placeholder`);
4905
- const reply = buildStreamPlaceholderReply(streamId2);
4906
- jsonOk(
4907
- res,
4908
- buildEncryptedJsonReply({
4909
- account: target.account,
4910
- plaintextJson: reply,
4911
- nonce: msgNonce,
4912
- timestamp: msgTimestamp
4913
- })
4914
- );
4915
- return true;
5453
+ logger3.debug(`duplicate msgid detected: msgid=${msgid} streamId=${streamId2}, returning placeholder`);
5454
+ return buildStreamPlaceholderReply(streamId2);
4916
5455
  }
4917
- if (msgtype === "event") {
4918
- const eventtype = String(
4919
- msg.event?.eventtype ?? msg.Event ?? ""
4920
- ).toLowerCase();
4921
- logger2.info(`event received: type=${eventtype || "unknown"}`);
4922
- if (eventtype === "enter_chat" || eventtype === "subscribe") {
4923
- const welcome = target.account.config.welcomeText?.trim();
4924
- if (welcome && target.account.canSendActive) {
4925
- logger2.info("sending welcome message");
4926
- sendBaiduAppMessage(target.account, welcome).catch((err) => {
4927
- logger2.error(`failed to send welcome message: ${String(err)}`);
4928
- });
4929
- }
4930
- jsonOk(
4931
- res,
4932
- buildEncryptedJsonReply({
4933
- account: target.account,
4934
- plaintextJson: {},
4935
- nonce: msgNonce,
4936
- timestamp: msgTimestamp
4937
- })
4938
- );
4939
- return true;
4940
- }
4941
- jsonOk(
4942
- res,
4943
- buildEncryptedJsonReply({
4944
- account: target.account,
4945
- plaintextJson: {},
4946
- nonce: msgNonce,
4947
- timestamp: msgTimestamp
4948
- })
4949
- );
4950
- return true;
5456
+ if (!canDispatchBaiduAppInboundMessage(msg)) {
5457
+ logger3.warn(`inbound message skipped: type=${msgtype || "unknown"} reason=no-dispatchable-content`);
5458
+ return {};
4951
5459
  }
5460
+ const inboundFileResult = msgtype === "text" ? await downloadInboundFiles({ msg, logger: logger3, runtime: target.runtime }) : { localFiles: [], diagnostic: {} };
5461
+ const inboundMediaFiles = inboundFileResult.localFiles;
4952
5462
  const streamId = createStreamId();
4953
5463
  if (msgid) {
4954
5464
  msgidToStreamId.set(msgid, streamId);
@@ -4962,14 +5472,14 @@ async function handleBaiduAppWebhookRequest(req, res) {
4962
5472
  finished: false,
4963
5473
  content: ""
4964
5474
  });
4965
- logger2.info(`stream created: streamId=${streamId} msgid=${msgid ?? "none"}`);
5475
+ logger3.info(`stream created: streamId=${streamId} msgid=${msgid ?? "none"}`);
4966
5476
  const core = tryGetBaiduAppRuntime();
4967
5477
  if (core) {
4968
5478
  const state = streams.get(streamId);
4969
5479
  if (state) {
4970
5480
  state.started = true;
4971
5481
  }
4972
- logger2.info(`agent dispatch started: streamId=${streamId} canSendActive=${target.account.canSendActive}`);
5482
+ logger3.info(`agent dispatch started: streamId=${streamId} canSendActive=${target.account.canSendActive}`);
4973
5483
  const hooks = {
4974
5484
  onChunk: (text) => {
4975
5485
  const current = streams.get(streamId);
@@ -4977,7 +5487,7 @@ async function handleBaiduAppWebhookRequest(req, res) {
4977
5487
  return;
4978
5488
  }
4979
5489
  appendStreamContent(current, text);
4980
- logger2.debug(
5490
+ logger3.debug(
4981
5491
  `chunk received: streamId=${streamId} chunkLen=${text.length} totalLen=${current.content.length}`
4982
5492
  );
4983
5493
  target.statusSink?.({ lastOutboundAt: Date.now() });
@@ -4990,7 +5500,7 @@ async function handleBaiduAppWebhookRequest(req, res) {
4990
5500
  current.finished = true;
4991
5501
  current.updatedAt = Date.now();
4992
5502
  }
4993
- logger2.error(`openclaw-baiduapp agent failed: ${String(err)}`);
5503
+ logger3.error(`openclaw-baiduapp agent failed: ${String(err)}`);
4994
5504
  }
4995
5505
  };
4996
5506
  dispatchBaiduAppMessage({
@@ -4999,6 +5509,8 @@ async function handleBaiduAppWebhookRequest(req, res) {
4999
5509
  msg,
5000
5510
  core,
5001
5511
  hooks,
5512
+ inboundMediaFiles,
5513
+ inboundFileDiagnostic: inboundFileResult.diagnostic,
5002
5514
  log: target.runtime.log,
5003
5515
  error: target.runtime.error
5004
5516
  }).then(async () => {
@@ -5007,20 +5519,20 @@ async function handleBaiduAppWebhookRequest(req, res) {
5007
5519
  current.finished = true;
5008
5520
  current.updatedAt = Date.now();
5009
5521
  const contentLen = current.content.trim().length;
5010
- logger2.info(
5522
+ logger3.info(
5011
5523
  `agent dispatch done: streamId=${streamId} contentLen=${contentLen} canSendActive=${target.account.canSendActive}`
5012
5524
  );
5013
5525
  if (!target.account.canSendActive) {
5014
- logger2.warn(
5526
+ logger3.warn(
5015
5527
  `active send skipped: appKey/appSecret not configured for account ${target.account.accountId}`
5016
5528
  );
5017
5529
  } else if (!contentLen) {
5018
- logger2.warn(`active send skipped: agent produced no content`);
5530
+ logger3.warn(`active send skipped: agent produced no content`);
5019
5531
  }
5020
5532
  if (target.account.canSendActive && current.content.trim()) {
5021
5533
  try {
5022
5534
  const chunks = splitMessageByBytes(current.content, MAX_MESSAGE_BYTES);
5023
- logger2.info(
5535
+ logger3.info(
5024
5536
  `[REPLY-MODE:ACTIVE-SEND] active send starting: streamId=${streamId} chunks=${chunks.length} contentLen=${contentLen}`
5025
5537
  );
5026
5538
  for (let i = 0; i < chunks.length; i++) {
@@ -5029,13 +5541,13 @@ async function handleBaiduAppWebhookRequest(req, res) {
5029
5541
  streamId,
5030
5542
  chunkKey: i
5031
5543
  });
5032
- logger2.debug(`active send chunk ${i + 1}/${chunks.length} sent: streamId=${streamId}`);
5544
+ logger3.debug(`active send chunk ${i + 1}/${chunks.length} sent: streamId=${streamId}`);
5033
5545
  }
5034
- logger2.info(
5546
+ logger3.info(
5035
5547
  `[REPLY-MODE:ACTIVE-SEND] active send complete: streamId=${streamId} chunks=${chunks.length}`
5036
5548
  );
5037
5549
  } catch (err) {
5038
- logger2.error(`active send failed: streamId=${streamId} error=${String(err)}`);
5550
+ logger3.error(`active send failed: streamId=${streamId} error=${String(err)}`);
5039
5551
  }
5040
5552
  }
5041
5553
  }
@@ -5047,10 +5559,10 @@ async function handleBaiduAppWebhookRequest(req, res) {
5047
5559
  current.finished = true;
5048
5560
  current.updatedAt = Date.now();
5049
5561
  }
5050
- logger2.error(`agent dispatch failed: streamId=${streamId} error=${String(err)}`);
5562
+ logger3.error(`agent dispatch failed: streamId=${streamId} error=${String(err)}`);
5051
5563
  });
5052
5564
  } else {
5053
- logger2.warn(`runtime not available: streamId=${streamId} \u2014 agent dispatch skipped, no reply will be generated`);
5565
+ logger3.warn(`runtime not available: streamId=${streamId} \u2014 agent dispatch skipped, no reply will be generated`);
5054
5566
  const state = streams.get(streamId);
5055
5567
  if (state) {
5056
5568
  state.finished = true;
@@ -5058,12 +5570,165 @@ async function handleBaiduAppWebhookRequest(req, res) {
5058
5570
  }
5059
5571
  }
5060
5572
  const placeholderReply = buildStreamPlaceholderReply(streamId);
5061
- logger2.debug(`stream placeholder reply sent: streamId=${streamId}`);
5573
+ logger3.debug(`stream placeholder reply sent: streamId=${streamId}`);
5574
+ return placeholderReply;
5575
+ }
5576
+ function registerBaiduAppWebhookTarget(target) {
5577
+ const key = normalizeWebhookPath(target.path);
5578
+ const normalizedTarget = { ...target, path: key };
5579
+ const existing = webhookTargets.get(key) ?? [];
5580
+ const next = [...existing, normalizedTarget];
5581
+ webhookTargets.set(key, next);
5582
+ return () => {
5583
+ const updated = (webhookTargets.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
5584
+ if (updated.length > 0) {
5585
+ webhookTargets.set(key, updated);
5586
+ } else {
5587
+ webhookTargets.delete(key);
5588
+ }
5589
+ };
5590
+ }
5591
+ async function handleBaiduAppWebhookRequest(req, res) {
5592
+ pruneStreams();
5593
+ const path4 = resolvePath(req);
5594
+ const targets = webhookTargets.get(path4);
5595
+ if (!targets || targets.length === 0) {
5596
+ return false;
5597
+ }
5598
+ const query = resolveQueryParams(req);
5599
+ const timestamp = query.get("timestamp") ?? "";
5600
+ const nonce = query.get("nonce") ?? "";
5601
+ const signature = resolveSignatureParam(query);
5602
+ const primary = targets[0];
5603
+ const logger3 = buildLogger(primary);
5604
+ if (req.method === "GET") {
5605
+ const echostr = query.get("echostr") ?? "";
5606
+ if (!timestamp || !nonce || !signature || !echostr) {
5607
+ jsonError(res, "missing query params", 400);
5608
+ return true;
5609
+ }
5610
+ const signatureMatched2 = targets.filter((candidate) => {
5611
+ if (!candidate.account.token) {
5612
+ return false;
5613
+ }
5614
+ return verifyBaiduAppSignature({
5615
+ token: candidate.account.token,
5616
+ timestamp,
5617
+ nonce,
5618
+ encrypt: echostr,
5619
+ signature
5620
+ });
5621
+ });
5622
+ if (signatureMatched2.length === 0) {
5623
+ jsonError(res, "unauthorized");
5624
+ return true;
5625
+ }
5626
+ const decryptable2 = signatureMatched2.filter((candidate) => Boolean(candidate.account.encodingAESKey));
5627
+ if (decryptable2.length === 0) {
5628
+ jsonError(res, "unauthorized");
5629
+ return true;
5630
+ }
5631
+ const decryptedCandidates2 = decryptBaiduAppCandidates({
5632
+ candidates: decryptable2,
5633
+ encrypt: echostr
5634
+ });
5635
+ if (decryptedCandidates2.length === 0) {
5636
+ jsonError(res, "decrypt failed");
5637
+ return true;
5638
+ }
5639
+ const selected2 = selectDecryptedTarget({ candidates: decryptedCandidates2, logger: logger3 });
5640
+ jsonOk(res, selected2.plaintext);
5641
+ return true;
5642
+ }
5643
+ if (req.method !== "POST") {
5644
+ jsonError(res, "Method Not Allowed", 405);
5645
+ return true;
5646
+ }
5647
+ if (!timestamp || !nonce || !signature) {
5648
+ jsonError(res, "missing query params");
5649
+ return true;
5650
+ }
5651
+ const body = await readRawBody(req, 1024 * 1024);
5652
+ if (!body.ok || !body.raw) {
5653
+ jsonError(res, body.error ?? "invalid payload");
5654
+ return true;
5655
+ }
5656
+ const rawBody = body.raw;
5657
+ let encrypt = "";
5658
+ let msgSignature = signature;
5659
+ let msgTimestamp = timestamp;
5660
+ let msgNonce = nonce;
5661
+ if (isXmlFormat(rawBody)) {
5662
+ const xmlData = parseXmlBody(rawBody);
5663
+ encrypt = xmlData.Encrypt ?? "";
5664
+ msgSignature = xmlData.MsgSignature ?? signature;
5665
+ msgTimestamp = xmlData.TimeStamp ?? timestamp;
5666
+ msgNonce = xmlData.Nonce ?? nonce;
5667
+ logger3.info(`inbound xml parsed: hasEncrypt=${Boolean(encrypt)}, msg_signature=${msgSignature ? "yes" : "no"}`);
5668
+ } else {
5669
+ try {
5670
+ const record = JSON.parse(rawBody);
5671
+ encrypt = String(record.encrypt ?? record.Encrypt ?? "");
5672
+ logger3.info(`inbound json parsed: hasEncrypt=${Boolean(encrypt)}`);
5673
+ } catch {
5674
+ logger3.warn(`inbound payload parse failed: not valid xml or json`);
5675
+ jsonError(res, "invalid payload format");
5676
+ return true;
5677
+ }
5678
+ }
5679
+ if (!encrypt) {
5680
+ jsonError(res, "missing encrypt");
5681
+ return true;
5682
+ }
5683
+ const signatureMatched = targets.filter((candidate) => {
5684
+ if (!candidate.account.token) {
5685
+ return false;
5686
+ }
5687
+ return verifyBaiduAppSignature({
5688
+ token: candidate.account.token,
5689
+ timestamp: msgTimestamp,
5690
+ nonce: msgNonce,
5691
+ encrypt,
5692
+ signature: msgSignature
5693
+ });
5694
+ });
5695
+ if (signatureMatched.length === 0) {
5696
+ logger3.warn(`signature verification failed: checked ${targets.length} account(s), none matched`);
5697
+ jsonError(res, "unauthorized");
5698
+ return true;
5699
+ }
5700
+ logger3.debug(`signature verified: ${signatureMatched.length} account(s) matched`);
5701
+ const decryptable = signatureMatched.filter((candidate) => Boolean(candidate.account.encodingAESKey));
5702
+ if (decryptable.length === 0) {
5703
+ logger3.warn(`no account has encodingAESKey configured`);
5704
+ jsonError(res, "openclaw-baiduapp not configured");
5705
+ return true;
5706
+ }
5707
+ const decryptedCandidates = decryptBaiduAppCandidates({
5708
+ candidates: decryptable,
5709
+ encrypt
5710
+ });
5711
+ if (decryptedCandidates.length === 0) {
5712
+ logger3.warn(`decrypt failed for all ${decryptable.length} candidate account(s)`);
5713
+ jsonError(res, "decrypt failed");
5714
+ return true;
5715
+ }
5716
+ const selected = selectDecryptedTarget({ candidates: decryptedCandidates, logger: logger3 });
5717
+ const target = selected.target;
5718
+ if (!target.account.configured || !target.account.token || !target.account.encodingAESKey) {
5719
+ logger3.warn(`selected account ${target.account.accountId} not fully configured`);
5720
+ jsonError(res, "openclaw-baiduapp not configured");
5721
+ return true;
5722
+ }
5723
+ const reply = await processBaiduAppInboundMessage({
5724
+ target,
5725
+ msg: selected.msg
5726
+ });
5062
5727
  jsonOk(
5063
5728
  res,
5064
5729
  buildEncryptedJsonReply({
5065
5730
  account: target.account,
5066
- plaintextJson: placeholderReply,
5731
+ plaintextJson: reply,
5067
5732
  nonce: msgNonce,
5068
5733
  timestamp: msgTimestamp
5069
5734
  })
@@ -5071,7 +5736,243 @@ async function handleBaiduAppWebhookRequest(req, res) {
5071
5736
  return true;
5072
5737
  }
5073
5738
 
5739
+ // src/poller.ts
5740
+ var DEFAULT_POLL_INTERVAL_MS = 1e3;
5741
+ var DEFAULT_POLL_REQUEST_TIMEOUT_MS = 1e4;
5742
+ var accountPollers = /* @__PURE__ */ new Map();
5743
+ function buildPollingTextInboundMessage(content) {
5744
+ return {
5745
+ msgtype: "text",
5746
+ text: {
5747
+ content
5748
+ }
5749
+ };
5750
+ }
5751
+ async function dispatchPendingPollingMessages(data, target) {
5752
+ if (!target || data.length === 0) {
5753
+ return;
5754
+ }
5755
+ for (const item of data) {
5756
+ if (String(item.msgtype).toLowerCase() !== "text") {
5757
+ continue;
5758
+ }
5759
+ const content = item.data?.content;
5760
+ if (typeof content !== "string" || content.length === 0) {
5761
+ continue;
5762
+ }
5763
+ await processBaiduAppInboundMessage({
5764
+ target,
5765
+ msg: buildPollingTextInboundMessage(content)
5766
+ });
5767
+ }
5768
+ }
5769
+ function isAbortError2(error) {
5770
+ return error instanceof Error && error.name === "AbortError";
5771
+ }
5772
+ function buildPollResult(params) {
5773
+ return {
5774
+ ok: params.ok,
5775
+ data: params.data ?? [],
5776
+ error: params.error
5777
+ };
5778
+ }
5779
+ function createAbortSignalController(params) {
5780
+ if (params.signal == null && params.timeoutMs == null) {
5781
+ return {
5782
+ signal: void 0,
5783
+ cleanup: () => void 0,
5784
+ didTimeout: () => false
5785
+ };
5786
+ }
5787
+ const controller = new AbortController();
5788
+ let timeoutId;
5789
+ let timedOut = false;
5790
+ const abortFromParent = () => {
5791
+ controller.abort();
5792
+ };
5793
+ if (params.signal) {
5794
+ if (params.signal.aborted) {
5795
+ controller.abort();
5796
+ } else {
5797
+ params.signal.addEventListener("abort", abortFromParent, { once: true });
5798
+ }
5799
+ }
5800
+ if (params.timeoutMs != null) {
5801
+ timeoutId = setTimeout(() => {
5802
+ timedOut = true;
5803
+ controller.abort();
5804
+ }, params.timeoutMs);
5805
+ }
5806
+ return {
5807
+ signal: controller.signal,
5808
+ cleanup: () => {
5809
+ if (timeoutId) {
5810
+ clearTimeout(timeoutId);
5811
+ }
5812
+ if (params.signal) {
5813
+ params.signal.removeEventListener("abort", abortFromParent);
5814
+ }
5815
+ },
5816
+ didTimeout: () => timedOut
5817
+ };
5818
+ }
5819
+ async function pollBaiduAppChatlistOnce(account, loggerOptions, requestOptions) {
5820
+ const endpoint = `${account.apiBase}/chat/demo/chatlist`;
5821
+ const query = new URLSearchParams({
5822
+ ak: account.appKey ?? "",
5823
+ sk: account.appSecret ?? ""
5824
+ });
5825
+ const url = `${endpoint}?${query.toString()}`;
5826
+ const requestSignal = createAbortSignalController({
5827
+ signal: requestOptions?.signal,
5828
+ timeoutMs: requestOptions?.timeoutMs
5829
+ });
5830
+ let response;
5831
+ try {
5832
+ response = await (requestOptions?.fetchImpl ?? fetch)(url, {
5833
+ method: "GET",
5834
+ signal: requestSignal.signal
5835
+ });
5836
+ } catch (error) {
5837
+ requestSignal.cleanup();
5838
+ if (requestSignal.didTimeout()) {
5839
+ return buildPollResult({
5840
+ ok: false,
5841
+ error: {
5842
+ kind: "timeout",
5843
+ message: `chatlist request timed out after ${requestOptions?.timeoutMs}ms`
5844
+ }
5845
+ });
5846
+ }
5847
+ if (isAbortError2(error)) {
5848
+ return buildPollResult({
5849
+ ok: false,
5850
+ error: {
5851
+ kind: "aborted",
5852
+ message: "chatlist request aborted"
5853
+ }
5854
+ });
5855
+ }
5856
+ return buildPollResult({
5857
+ ok: false,
5858
+ error: {
5859
+ kind: "request-failed",
5860
+ message: error instanceof Error ? error.message : String(error)
5861
+ }
5862
+ });
5863
+ }
5864
+ const responseText = await response.text();
5865
+ requestSignal.cleanup();
5866
+ if (!responseText) {
5867
+ return buildPollResult({
5868
+ ok: false,
5869
+ error: {
5870
+ kind: "empty-response",
5871
+ message: `empty response from chatlist API (status=${response.status})`
5872
+ }
5873
+ });
5874
+ }
5875
+ let parsed;
5876
+ try {
5877
+ parsed = JSON.parse(responseText);
5878
+ } catch {
5879
+ return buildPollResult({
5880
+ ok: false,
5881
+ error: {
5882
+ kind: "invalid-json",
5883
+ message: `invalid JSON response from chatlist API (status=${response.status})`
5884
+ }
5885
+ });
5886
+ }
5887
+ const data = Array.isArray(parsed.data) ? parsed.data : [];
5888
+ return buildPollResult({
5889
+ ok: true,
5890
+ data
5891
+ });
5892
+ }
5893
+ function scheduleNextPoll(account, state) {
5894
+ if (state.stopped) {
5895
+ return;
5896
+ }
5897
+ state.timer = setTimeout(() => {
5898
+ void runPollCycle(account, state);
5899
+ }, state.intervalMs);
5900
+ }
5901
+ async function runPollCycle(account, state) {
5902
+ if (state.stopped || state.pending) {
5903
+ return;
5904
+ }
5905
+ const controller = new AbortController();
5906
+ state.activeController = controller;
5907
+ const pending = (async () => {
5908
+ try {
5909
+ const result = await pollBaiduAppChatlistOnce(account, void 0, {
5910
+ signal: controller.signal,
5911
+ fetchImpl: state.fetchImpl,
5912
+ timeoutMs: state.requestTimeoutMs
5913
+ });
5914
+ await dispatchPendingPollingMessages(result.data, state.dispatchTarget);
5915
+ } catch (error) {
5916
+ if (!(state.stopped && isAbortError2(error))) {
5917
+ state.onError?.(error);
5918
+ }
5919
+ } finally {
5920
+ if (state.activeController === controller) {
5921
+ state.activeController = null;
5922
+ }
5923
+ state.pending = null;
5924
+ if (!state.stopped) {
5925
+ scheduleNextPoll(account, state);
5926
+ }
5927
+ }
5928
+ })();
5929
+ state.pending = pending;
5930
+ await pending;
5931
+ }
5932
+ function startAccountPolling(params) {
5933
+ if (accountPollers.has(params.account.accountId)) {
5934
+ return;
5935
+ }
5936
+ const state = {
5937
+ stopped: false,
5938
+ timer: null,
5939
+ activeController: null,
5940
+ pending: null,
5941
+ intervalMs: params.intervalMs ?? DEFAULT_POLL_INTERVAL_MS,
5942
+ requestTimeoutMs: params.requestTimeoutMs ?? DEFAULT_POLL_REQUEST_TIMEOUT_MS,
5943
+ fetchImpl: params.fetchImpl,
5944
+ onError: params.onError,
5945
+ dispatchTarget: params.dispatchTarget
5946
+ };
5947
+ accountPollers.set(params.account.accountId, state);
5948
+ void runPollCycle(params.account, state);
5949
+ }
5950
+ function stopAccountPolling(accountId) {
5951
+ const state = accountPollers.get(accountId);
5952
+ if (!state) {
5953
+ return;
5954
+ }
5955
+ state.stopped = true;
5956
+ if (state.timer) {
5957
+ clearTimeout(state.timer);
5958
+ state.timer = null;
5959
+ }
5960
+ state.activeController?.abort();
5961
+ state.activeController = null;
5962
+ accountPollers.delete(accountId);
5963
+ }
5964
+
5074
5965
  // src/channel.ts
5966
+ function resolveOutboundMediaUrls(payload) {
5967
+ if (payload.mediaUrls?.length) {
5968
+ return payload.mediaUrls;
5969
+ }
5970
+ if (payload.mediaUrl) {
5971
+ return [payload.mediaUrl];
5972
+ }
5973
+ return [];
5974
+ }
5975
+ var logger2 = createLogger("openclaw-baiduapp");
5075
5976
  var meta = {
5076
5977
  id: "openclaw-baiduapp",
5077
5978
  label: "Baidu App",
@@ -5083,6 +5984,87 @@ var meta = {
5083
5984
  order: 85
5084
5985
  };
5085
5986
  var unregisterHooks = /* @__PURE__ */ new Map();
5987
+ function resolveOutboundLocalMediaPath(mediaUrl) {
5988
+ const trimmed = mediaUrl?.trim();
5989
+ if (!trimmed) {
5990
+ return void 0;
5991
+ }
5992
+ if (trimmed.startsWith("file://")) {
5993
+ return fileURLToPath(trimmed);
5994
+ }
5995
+ if (/^[a-zA-Z]:[\\/]/.test(trimmed)) {
5996
+ return trimmed;
5997
+ }
5998
+ if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) {
5999
+ return trimmed;
6000
+ }
6001
+ if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)) {
6002
+ return void 0;
6003
+ }
6004
+ return trimmed;
6005
+ }
6006
+ function inferBaiduOutboundFileType(params) {
6007
+ const ext = path2.extname(params.mediaPath).toLowerCase();
6008
+ return ext.startsWith(".") ? ext.slice(1) : ext || "bin";
6009
+ }
6010
+ function buildOutboundMediaPayload(params) {
6011
+ const caption = params.caption?.trim();
6012
+ return {
6013
+ msgtype: "text",
6014
+ ...caption ? { text: { content: caption } } : {},
6015
+ files: [{ url: params.uploadedUrl, fileType: params.fileType }]
6016
+ };
6017
+ }
6018
+ function resolveDirectOutboundFiles(files) {
6019
+ if (!Array.isArray(files) || files.length === 0) {
6020
+ return [];
6021
+ }
6022
+ return files.map((file) => ({
6023
+ mediaUrl: file.url?.trim() ?? "",
6024
+ mimeType: file.fileType?.trim() || void 0
6025
+ })).filter((file) => Boolean(file.mediaUrl));
6026
+ }
6027
+ async function sendBaiduAppPayload(params) {
6028
+ const text = params.payload.text ?? params.payload.content ?? "";
6029
+ const mediaInputs = [
6030
+ ...resolveOutboundMediaUrls(params.payload).map((mediaUrl) => ({ mediaUrl, mimeType: void 0 })),
6031
+ ...resolveDirectOutboundFiles(params.payload.files)
6032
+ ];
6033
+ if (!text && mediaInputs.length === 0) {
6034
+ return {
6035
+ channel: "openclaw-baiduapp",
6036
+ ok: false,
6037
+ messageId: ""
6038
+ };
6039
+ }
6040
+ if (mediaInputs.length === 0) {
6041
+ return baiduAppPlugin.outbound.sendText({
6042
+ cfg: params.cfg,
6043
+ accountId: params.accountId ?? void 0,
6044
+ to: params.to,
6045
+ text
6046
+ });
6047
+ }
6048
+ let lastResult = {
6049
+ channel: "openclaw-baiduapp",
6050
+ ok: false,
6051
+ messageId: ""
6052
+ };
6053
+ for (const [index, mediaInput] of mediaInputs.entries()) {
6054
+ lastResult = await baiduAppPlugin.outbound.sendMedia({
6055
+ cfg: params.cfg,
6056
+ accountId: params.accountId,
6057
+ to: params.to,
6058
+ text: index === 0 ? text : "",
6059
+ mediaUrl: mediaInput.mediaUrl,
6060
+ mimeType: mediaInput.mimeType
6061
+ });
6062
+ if (!lastResult.ok) {
6063
+ return lastResult;
6064
+ }
6065
+ }
6066
+ return lastResult;
6067
+ }
5086
6068
  var baiduAppPlugin = {
5087
6069
  id: "openclaw-baiduapp",
5088
6070
  meta: {
@@ -5090,7 +6072,7 @@ var baiduAppPlugin = {
5090
6072
  },
5091
6073
  capabilities: {
5092
6074
  chatTypes: ["direct"],
5093
- media: false,
6075
+ media: true,
5094
6076
  reactions: false,
5095
6077
  threads: false,
5096
6078
  edit: false,
@@ -5098,6 +6080,19 @@ var baiduAppPlugin = {
5098
6080
  polls: false,
5099
6081
  activeSend: true
5100
6082
  },
6083
+ messaging: {
6084
+ normalizeTarget: (raw) => {
6085
+ const trimmed = raw.trim();
6086
+ if (!trimmed) {
6087
+ return void 0;
6088
+ }
6089
+ return trimmed.replace(/^openclaw-baiduapp:/i, "").trim() || void 0;
6090
+ },
6091
+ targetResolver: {
6092
+ looksLikeId: () => true,
6093
+ hint: "<user|openclaw-baiduapp:userId>"
6094
+ }
6095
+ },
5101
6096
  configSchema: BaiduAppConfigJsonSchema,
5102
6097
  reload: { configPrefixes: ["channels.openclaw-baiduapp"] },
5103
6098
  config: {
@@ -5113,7 +6108,7 @@ var baiduAppPlugin = {
5113
6108
  channels: {
5114
6109
  ...params.cfg.channels,
5115
6110
  "openclaw-baiduapp": {
5116
- ...params.cfg.channels?.["openclaw-baiduapp"] ?? {},
6111
+ ...params.cfg.channels?.["openclaw-baiduapp"],
5117
6112
  enabled: params.enabled
5118
6113
  }
5119
6114
  }
@@ -5124,11 +6119,11 @@ var baiduAppPlugin = {
5124
6119
  channels: {
5125
6120
  ...params.cfg.channels,
5126
6121
  "openclaw-baiduapp": {
5127
- ...params.cfg.channels?.["openclaw-baiduapp"] ?? {},
6122
+ ...params.cfg.channels?.["openclaw-baiduapp"],
5128
6123
  accounts: {
5129
- ...params.cfg.channels?.["openclaw-baiduapp"]?.accounts ?? {},
6124
+ ...params.cfg.channels?.["openclaw-baiduapp"]?.accounts,
5130
6125
  [accountId]: {
5131
- ...params.cfg.channels?.["openclaw-baiduapp"]?.accounts?.[accountId] ?? {},
6126
+ ...params.cfg.channels?.["openclaw-baiduapp"]?.accounts?.[accountId],
5132
6127
  enabled: params.enabled
5133
6128
  }
5134
6129
  }
@@ -5151,7 +6146,7 @@ var baiduAppPlugin = {
5151
6146
  };
5152
6147
  return next;
5153
6148
  }
5154
- const accounts = { ...current.accounts ?? {} };
6149
+ const accounts = { ...current.accounts };
5155
6150
  delete accounts[accountId];
5156
6151
  next.channels = {
5157
6152
  ...next.channels,
@@ -5239,6 +6234,10 @@ var baiduAppPlugin = {
5239
6234
  },
5240
6235
  outbound: {
5241
6236
  deliveryMode: "direct",
6237
+ resolveTarget: (params) => {
6238
+ return { ok: true, to: params.to?.trim() || "default" };
6239
+ },
6240
+ sendPayload: async (params) => sendBaiduAppPayload(params),
5242
6241
  sendText: async (params) => {
5243
6242
  const account = resolveBaiduAppAccount({ cfg: params.cfg, accountId: params.accountId });
5244
6243
  if (!account.canSendActive) {
@@ -5267,6 +6266,7 @@ var baiduAppPlugin = {
5267
6266
  }
5268
6267
  },
5269
6268
  sendMedia: async (params) => {
6269
+ logger2.info(`sendMedia start ${params.mediaUrl}`);
5270
6270
  const account = resolveBaiduAppAccount({ cfg: params.cfg, accountId: params.accountId ?? void 0 });
5271
6271
  if (!account.canSendActive) {
5272
6272
  return {
@@ -5276,29 +6276,84 @@ var baiduAppPlugin = {
5276
6276
  error: new Error("Account not configured for active sending (missing appKey or appSecret)")
5277
6277
  };
5278
6278
  }
5279
- const content = params.text?.trim() || params.mediaUrl || "";
5280
- if (!content) {
6279
+ const localMediaPath = resolveOutboundLocalMediaPath(params.mediaUrl);
6280
+ if (!localMediaPath) {
6281
+ const remoteUrl = params.mediaUrl?.trim() ?? "";
6282
+ if (!remoteUrl) {
6283
+ return {
6284
+ channel: "openclaw-baiduapp",
6285
+ ok: false,
6286
+ messageId: "",
6287
+ error: new Error("Outbound media requires a URL or local file path")
6288
+ };
6289
+ }
6290
+ try {
6291
+ const result = await sendBaiduAppMessage(
6292
+ account,
6293
+ buildOutboundMediaPayload({
6294
+ caption: params.text,
6295
+ uploadedUrl: remoteUrl,
6296
+ fileType: inferBaiduOutboundFileType({ mediaPath: remoteUrl })
6297
+ })
6298
+ );
6299
+ return {
6300
+ channel: "openclaw-baiduapp",
6301
+ ok: true,
6302
+ messageId: result.msgid ?? ""
6303
+ };
6304
+ } catch (err) {
6305
+ return {
6306
+ channel: "openclaw-baiduapp",
6307
+ ok: false,
6308
+ messageId: "",
6309
+ error: err instanceof Error ? err : new Error(String(err))
6310
+ };
6311
+ }
6312
+ }
6313
+ let uploaded;
6314
+ try {
6315
+ uploaded = await uploadLocalFileToBos({
6316
+ account,
6317
+ filePath: localMediaPath,
6318
+ contentType: params.mimeType
6319
+ });
6320
+ } catch (err) {
5281
6321
  return {
5282
6322
  channel: "openclaw-baiduapp",
5283
6323
  ok: false,
5284
6324
  messageId: "",
5285
- error: new Error("No content to send (media not supported, text is empty)")
6325
+ error: new Error(
6326
+ `Failed to upload outbound media: ${err instanceof Error ? err.message : String(err)}`
6327
+ )
5286
6328
  };
5287
6329
  }
5288
6330
  try {
5289
- const result = await sendBaiduAppMessage(account, content);
6331
+ const result = await sendBaiduAppMessage(
6332
+ account,
6333
+ buildOutboundMediaPayload({
6334
+ caption: params.text,
6335
+ uploadedUrl: uploaded.url,
6336
+ fileType: inferBaiduOutboundFileType({
6337
+ mediaPath: uploaded.fileName || localMediaPath
6338
+ })
6339
+ })
6340
+ );
5290
6341
  return {
5291
6342
  channel: "openclaw-baiduapp",
5292
6343
  ok: result.ok,
5293
6344
  messageId: result.msgid ?? "",
5294
- error: result.ok ? void 0 : new Error(result.errmsg ?? "send failed")
6345
+ error: result.ok ? void 0 : new Error(result.errmsg ?? "send failed"),
6346
+ fileName: uploaded.fileName,
6347
+ fileSize: uploaded.fileSize
5295
6348
  };
5296
6349
  } catch (err) {
5297
6350
  return {
5298
6351
  channel: "openclaw-baiduapp",
5299
6352
  ok: false,
5300
6353
  messageId: "",
5301
- error: err instanceof Error ? err : new Error(String(err))
6354
+ error: new Error(
6355
+ `Failed to send media message: ${err instanceof Error ? err.message : String(err)}`
6356
+ )
5302
6357
  };
5303
6358
  }
5304
6359
  }
@@ -5318,35 +6373,66 @@ var baiduAppPlugin = {
5318
6373
  ctx.setStatus?.({ accountId: ctx.accountId, running: false, configured: false });
5319
6374
  return;
5320
6375
  }
5321
- const path = (account.config.webhookPath ?? "/openclaw-baiduapp").trim();
6376
+ const path4 = (account.config.webhookPath ?? "/openclaw-baiduapp").trim();
6377
+ const runtime2 = {
6378
+ log: ctx.log?.info ?? console.log,
6379
+ error: ctx.log?.error ?? console.error
6380
+ };
5322
6381
  const unregister = registerBaiduAppWebhookTarget({
5323
6382
  account,
5324
6383
  config: ctx.cfg ?? {},
5325
- runtime: {
5326
- log: ctx.log?.info ?? console.log,
5327
- error: ctx.log?.error ?? console.error
5328
- },
5329
- path,
6384
+ runtime: runtime2,
6385
+ path: path4,
5330
6386
  statusSink: (patch) => ctx.setStatus?.({ accountId: ctx.accountId, ...patch })
5331
6387
  });
6388
+ try {
6389
+ triggerBestEffortTempPrune({ runtime: runtime2, reason: "startup" });
6390
+ } catch (error) {
6391
+ runtime2.error(`[openclaw-baiduapp] temp prune failed during startup: ${String(error)}`);
6392
+ }
5332
6393
  const existing = unregisterHooks.get(ctx.accountId);
5333
6394
  if (existing) {
5334
6395
  existing();
5335
6396
  }
5336
6397
  unregisterHooks.set(ctx.accountId, unregister);
6398
+ if (account.config.pollingEnabled === true) {
6399
+ startAccountPolling({
6400
+ account,
6401
+ dispatchTarget: {
6402
+ account,
6403
+ config: ctx.cfg ?? {},
6404
+ runtime: runtime2,
6405
+ statusSink: (patch) => ctx.setStatus?.({ accountId: ctx.accountId, ...patch })
6406
+ },
6407
+ onError: (error) => {
6408
+ const message = error instanceof Error ? error.message : String(error);
6409
+ ctx.log?.error(`[openclaw-baiduapp] polling failed for account ${ctx.accountId}: ${message}`);
6410
+ }
6411
+ });
6412
+ }
5337
6413
  ctx.log?.info(
5338
- `[openclaw-baiduapp] webhook registered at ${path} for account ${ctx.accountId} (canSendActive=${account.canSendActive})`
6414
+ `[openclaw-baiduapp] webhook registered at ${path4} for account ${ctx.accountId} (canSendActive=${account.canSendActive})`
5339
6415
  );
5340
6416
  ctx.setStatus?.({
5341
6417
  accountId: ctx.accountId,
5342
6418
  running: true,
5343
6419
  configured: true,
5344
6420
  canSendActive: account.canSendActive,
5345
- webhookPath: path,
6421
+ webhookPath: path4,
5346
6422
  lastStartAt: Date.now()
5347
6423
  });
6424
+ if (ctx.abortSignal) {
6425
+ await new Promise((resolve) => {
6426
+ if (ctx.abortSignal?.aborted) {
6427
+ resolve();
6428
+ return;
6429
+ }
6430
+ ctx.abortSignal?.addEventListener("abort", () => resolve(), { once: true });
6431
+ });
6432
+ }
5348
6433
  },
5349
6434
  stopAccount: async (ctx) => {
6435
+ stopAccountPolling(ctx.accountId);
5350
6436
  const unregister = unregisterHooks.get(ctx.accountId);
5351
6437
  if (unregister) {
5352
6438
  unregister();
@@ -5413,7 +6499,14 @@ var plugin = {
5413
6499
  setBaiduAppRuntime(api.runtime);
5414
6500
  }
5415
6501
  api.registerChannel({ plugin: baiduAppPlugin });
5416
- if (api.registerHttpHandler) {
6502
+ if (api.registerHttpRoute) {
6503
+ api.registerHttpRoute({
6504
+ path: "/openclaw-baiduapp",
6505
+ auth: "plugin",
6506
+ match: "prefix",
6507
+ handler: handleBaiduAppWebhookRequest
6508
+ });
6509
+ } else if (api.registerHttpHandler) {
5417
6510
  api.registerHttpHandler(handleBaiduAppWebhookRequest);
5418
6511
  }
5419
6512
  }