@yoooclaw/phone-notifications 1.11.8 → 1.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -441,6 +441,9 @@ openclaw ntf search --sender "张三" --from 2026-03-01T00:00:00+08:00
441
441
 
442
442
  # 仅看飞书群聊消息
443
443
  openclaw ntf search --app Feishu --conversation-type group
444
+
445
+ # 生成轻量通知摘要输入,适合 Agent 总结大量通知
446
+ openclaw ntf summary --limit 700
444
447
  ```
445
448
 
446
449
  ### 通知统计
package/dist/index.cjs CHANGED
@@ -5574,7 +5574,7 @@ function readBuildInjectedVersion() {
5574
5574
  if (false) {
5575
5575
  return void 0;
5576
5576
  }
5577
- const version = "1.11.8".trim();
5577
+ const version = "1.11.9".trim();
5578
5578
  return version || void 0;
5579
5579
  }
5580
5580
  function readPluginVersionFromPackageJson() {
@@ -7985,13 +7985,13 @@ init_env();
7985
7985
 
7986
7986
  // src/update/channel.ts
7987
7987
  function resolveUpdateChannel(params) {
7988
+ if (params.envName === "production") {
7989
+ return "latest";
7990
+ }
7988
7991
  if (params.configuredChannel) {
7989
7992
  return params.configuredChannel;
7990
7993
  }
7991
- if (params.currentVersion.includes("-")) {
7992
- return "beta";
7993
- }
7994
- return params.envName === "production" ? "latest" : "beta";
7994
+ return "beta";
7995
7995
  }
7996
7996
 
7997
7997
  // src/update/index.ts
@@ -8100,7 +8100,7 @@ var UpdateChecker = class {
8100
8100
  const latest = await this.fetchLatestVersion();
8101
8101
  if (!latest || latest === PLUGIN_VERSION) return;
8102
8102
  if (latest === this.notifiedVersion) return;
8103
- if (PLUGIN_VERSION.includes("-") && this.channel !== "beta") return;
8103
+ if (this.channel !== "beta" && latest.includes("-")) return;
8104
8104
  if (!isNewerVersion(latest, PLUGIN_VERSION)) return;
8105
8105
  this.notifiedVersion = latest;
8106
8106
  this.logger.info(`\u53D1\u73B0\u65B0\u7248\u672C: ${PLUGIN_VERSION} \u2192 ${latest}`);
@@ -8436,7 +8436,6 @@ function registerAutoUpdateLifecycle(deps) {
8436
8436
  ...config.autoUpdate,
8437
8437
  channel: resolveUpdateChannel({
8438
8438
  configuredChannel: config.autoUpdate?.channel,
8439
- currentVersion: PLUGIN_VERSION,
8440
8439
  envName: loadEnvName()
8441
8440
  })
8442
8441
  };
@@ -8518,8 +8517,53 @@ function registerAuthCli(program) {
8518
8517
  });
8519
8518
  }
8520
8519
 
8521
- // src/cli/ntf-search.ts
8522
- function filterItem(item, opts) {
8520
+ // src/cli/ntf-query.ts
8521
+ function parsePositiveIntegerOption(rawValue, defaultValue, optionName, errorCode) {
8522
+ const value = rawValue && rawValue.length > 0 ? rawValue : String(defaultValue);
8523
+ if (!/^\d+$/.test(value)) {
8524
+ exitError(errorCode, `${optionName} \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570`);
8525
+ }
8526
+ const parsed = Number.parseInt(value, 10);
8527
+ if (parsed <= 0) {
8528
+ exitError(errorCode, `${optionName} \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570`);
8529
+ }
8530
+ return parsed;
8531
+ }
8532
+ function parseNotificationQueryOptions(opts, defaultLimit = 100) {
8533
+ const limit = parsePositiveIntegerOption(
8534
+ opts.limit,
8535
+ defaultLimit,
8536
+ "--limit",
8537
+ "INVALID_LIMIT"
8538
+ );
8539
+ if (opts.conversationType && opts.conversationType !== "group" && opts.conversationType !== "private") {
8540
+ exitError(
8541
+ "INVALID_CONVERSATION_TYPE",
8542
+ "--conversation-type \u53EA\u80FD\u662F group \u6216 private"
8543
+ );
8544
+ }
8545
+ const hasFrom = typeof opts.from === "string" && opts.from.length > 0;
8546
+ const hasTo = typeof opts.to === "string" && opts.to.length > 0;
8547
+ const fromTs = hasFrom ? parseIsoTime(opts.from, "--from") : null;
8548
+ const toTs = hasTo ? parseIsoTime(opts.to, "--to") : null;
8549
+ if (fromTs !== null && toTs !== null && fromTs > toTs) {
8550
+ exitError("INVALID_TIME_RANGE", "--from \u4E0D\u80FD\u665A\u4E8E --to");
8551
+ }
8552
+ return {
8553
+ from: opts.from,
8554
+ to: opts.to,
8555
+ app: opts.app,
8556
+ sender: opts.sender,
8557
+ conversationType: opts.conversationType,
8558
+ keyword: opts.keyword,
8559
+ limit,
8560
+ fromTs,
8561
+ toTs,
8562
+ fromDateKey: hasFrom ? opts.from.slice(0, 10) : null,
8563
+ toDateKey: hasTo ? opts.to.slice(0, 10) : null
8564
+ };
8565
+ }
8566
+ function matchesNotificationQuery(item, opts) {
8523
8567
  if (opts.app && !matchesNotificationAppFilter(item, opts.app)) return false;
8524
8568
  if (opts.conversationType && item.conversationType !== opts.conversationType) {
8525
8569
  return false;
@@ -8533,8 +8577,34 @@ function filterItem(item, opts) {
8533
8577
  item.conversationName
8534
8578
  ].filter((value) => typeof value === "string" && value.length > 0).join("\n");
8535
8579
  if (opts.keyword && !keywordHaystack.includes(opts.keyword)) return false;
8580
+ const itemTs = Date.parse(item.timestamp);
8581
+ if (Number.isNaN(itemTs)) return false;
8582
+ if (opts.fromTs !== null && itemTs < opts.fromTs) return false;
8583
+ if (opts.toTs !== null && itemTs > opts.toTs) return false;
8536
8584
  return true;
8537
8585
  }
8586
+ async function collectMatchingNotifications(dir, opts) {
8587
+ const keys = await listDateKeysAsync(dir);
8588
+ const results = [];
8589
+ const canStopAfterLimit = opts.fromTs === null && opts.toTs === null;
8590
+ for (const dateKey of keys) {
8591
+ if (opts.fromDateKey && dateKey < opts.fromDateKey) continue;
8592
+ if (opts.toDateKey && dateKey > opts.toDateKey) continue;
8593
+ const items = sortNotificationsByTimestampDesc([
8594
+ ...await readDateFileAsync(dir, dateKey)
8595
+ ]);
8596
+ for (const item of items) {
8597
+ if (!matchesNotificationQuery(item, opts)) continue;
8598
+ results.push(item);
8599
+ if (canStopAfterLimit && results.length >= opts.limit) {
8600
+ return results;
8601
+ }
8602
+ }
8603
+ }
8604
+ return sortNotificationsByTimestampDesc(results).slice(0, opts.limit);
8605
+ }
8606
+
8607
+ // src/cli/ntf-search.ts
8538
8608
  function registerNtfSearch(ntf, ctx) {
8539
8609
  ntf.command("search").description("\u67E5\u8BE2\u901A\u77E5\uFF08\u6309\u65F6\u95F4/\u5E94\u7528/\u53D1\u9001\u4EBA/\u5173\u952E\u8BCD\u7B5B\u9009\uFF09").option("--from <time>", "\u5F00\u59CB\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00").option("--to <time>", "\u7ED3\u675F\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T18:00:00+08:00").option("--app <name>", "\u6309\u5E94\u7528\u540D\u8FC7\u6EE4").option("--sender <name>", "\u6309\u53D1\u9001\u4EBA\u8FC7\u6EE4").option(
8540
8610
  "--conversation-type <type>",
@@ -8544,55 +8614,8 @@ function registerNtfSearch(ntf, ctx) {
8544
8614
  const dir = resolveNotificationsDir(ctx);
8545
8615
  if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8546
8616
  progress("\u6B63\u5728\u641C\u7D22\u901A\u77E5...");
8547
- const limit = parseInt(opts.limit, 10) || 100;
8548
- if (limit <= 0) {
8549
- exitError("INVALID_LIMIT", "--limit \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570");
8550
- }
8551
- if (opts.conversationType && opts.conversationType !== "group" && opts.conversationType !== "private") {
8552
- exitError(
8553
- "INVALID_CONVERSATION_TYPE",
8554
- "--conversation-type \u53EA\u80FD\u662F group \u6216 private"
8555
- );
8556
- }
8557
- const hasFrom = typeof opts.from === "string" && opts.from.length > 0;
8558
- const hasTo = typeof opts.to === "string" && opts.to.length > 0;
8559
- const fromTs = hasFrom ? parseIsoTime(opts.from, "--from") : null;
8560
- const toTs = hasTo ? parseIsoTime(opts.to, "--to") : null;
8561
- if (fromTs !== null && toTs !== null && fromTs > toTs) {
8562
- exitError("INVALID_TIME_RANGE", "--from \u4E0D\u80FD\u665A\u4E8E --to");
8563
- }
8564
- const keys = await listDateKeysAsync(dir);
8565
- const results = [];
8566
- if (fromTs !== null || toTs !== null) {
8567
- const fromDateKey = hasFrom ? opts.from.slice(0, 10) : null;
8568
- const toDateKey = hasTo ? opts.to.slice(0, 10) : null;
8569
- for (const dateKey of keys) {
8570
- if (fromDateKey && dateKey < fromDateKey) continue;
8571
- if (toDateKey && dateKey > toDateKey) continue;
8572
- const items = await readDateFileAsync(dir, dateKey);
8573
- for (const item of items) {
8574
- if (!filterItem(item, opts)) continue;
8575
- const itemTs = Date.parse(item.timestamp);
8576
- if (Number.isNaN(itemTs)) continue;
8577
- if (fromTs !== null && itemTs < fromTs) continue;
8578
- if (toTs !== null && itemTs > toTs) continue;
8579
- results.push(item);
8580
- }
8581
- }
8582
- } else {
8583
- for (const dateKey of keys) {
8584
- const items = await readDateFileAsync(dir, dateKey);
8585
- for (const item of items) {
8586
- if (!filterItem(item, opts)) continue;
8587
- if (Number.isNaN(Date.parse(item.timestamp))) continue;
8588
- results.push(item);
8589
- }
8590
- }
8591
- }
8592
- const notifications = sortNotificationsByTimestampDesc(results).slice(
8593
- 0,
8594
- limit
8595
- );
8617
+ const query = parseNotificationQueryOptions(opts);
8618
+ const notifications = await collectMatchingNotifications(dir, query);
8596
8619
  output({
8597
8620
  ok: true,
8598
8621
  total: notifications.length,
@@ -8602,6 +8625,117 @@ function registerNtfSearch(ntf, ctx) {
8602
8625
  );
8603
8626
  }
8604
8627
 
8628
+ // src/cli/ntf-summary.ts
8629
+ function compactNotification(item) {
8630
+ const result = {
8631
+ appName: item.appName,
8632
+ title: item.title,
8633
+ content: item.content,
8634
+ timestamp: item.timestamp
8635
+ };
8636
+ if (item.appDisplayName) result.appDisplayName = item.appDisplayName;
8637
+ if (item.senderName) result.senderName = item.senderName;
8638
+ if (item.conversationType) result.conversationType = item.conversationType;
8639
+ if (item.conversationName) result.conversationName = item.conversationName;
8640
+ return result;
8641
+ }
8642
+ function truncate(value, maxLength = 120) {
8643
+ if (value.length <= maxLength) return value;
8644
+ return `${value.slice(0, maxLength - 1)}\u2026`;
8645
+ }
8646
+ function increment(map, key, seed) {
8647
+ const existing = map.get(key);
8648
+ if (existing) {
8649
+ existing.count += 1;
8650
+ return;
8651
+ }
8652
+ map.set(key, { ...seed, count: 1 });
8653
+ }
8654
+ function topRows(map, limit) {
8655
+ return [...map.values()].sort((a, b) => b.count - a.count).slice(0, limit);
8656
+ }
8657
+ function hourBucket(timestamp) {
8658
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):/.exec(timestamp);
8659
+ if (!match) return null;
8660
+ return `${match[1]} ${match[2]}:00`;
8661
+ }
8662
+ function buildSamplesBySender(notifications, senderRows) {
8663
+ return senderRows.map((row) => {
8664
+ const samples = [];
8665
+ for (const item of notifications) {
8666
+ const sender = item.senderName?.trim() || item.title?.trim() || "(unknown)";
8667
+ if (sender !== row.sender) continue;
8668
+ const content = truncate(item.content.trim());
8669
+ if (content && !samples.includes(content)) samples.push(content);
8670
+ if (samples.length >= 5) break;
8671
+ }
8672
+ return { sender: row.sender, count: row.count, samples };
8673
+ });
8674
+ }
8675
+ function registerNtfSummary(ntf, ctx) {
8676
+ ntf.command("summary").description("\u751F\u6210\u901A\u77E5\u6458\u8981\u8F93\u5165\uFF08\u805A\u5408\u7EDF\u8BA1 + \u6837\u4F8B\uFF0C\u4F9B Agent \u5FEB\u901F\u603B\u7ED3\uFF09").option("--from <time>", "\u5F00\u59CB\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00").option("--to <time>", "\u7ED3\u675F\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T18:00:00+08:00").option("--app <name>", "\u6309\u5E94\u7528\u540D\u8FC7\u6EE4").option("--sender <name>", "\u6309\u53D1\u9001\u4EBA\u8FC7\u6EE4").option(
8677
+ "--conversation-type <type>",
8678
+ "\u6309\u4F1A\u8BDD\u7C7B\u578B\u8FC7\u6EE4\uFF08group/private\uFF09"
8679
+ ).option("--keyword <text>", "\u5728\u6807\u9898\u548C\u5185\u5BB9\u4E2D\u641C\u7D22\u5173\u952E\u8BCD").option("--limit <n>", "\u7EB3\u5165\u6458\u8981\u7684\u6700\u5927\u901A\u77E5\u6761\u6570", "100").option("--sample <n>", "\u8FD4\u56DE\u6700\u8FD1\u6837\u4F8B\u6761\u6570", "30").option("--top <n>", "\u8FD4\u56DE\u805A\u5408\u699C\u5355\u6761\u6570", "10").action(
8680
+ async (opts) => {
8681
+ const dir = resolveNotificationsDir(ctx);
8682
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8683
+ progress("\u6B63\u5728\u751F\u6210\u901A\u77E5\u6458\u8981\u8F93\u5165...");
8684
+ const query = parseNotificationQueryOptions(opts);
8685
+ const sampleLimit = parsePositiveIntegerOption(
8686
+ opts.sample,
8687
+ 30,
8688
+ "--sample",
8689
+ "INVALID_SAMPLE"
8690
+ );
8691
+ const topLimit = parsePositiveIntegerOption(
8692
+ opts.top,
8693
+ 10,
8694
+ "--top",
8695
+ "INVALID_TOP"
8696
+ );
8697
+ const notifications = await collectMatchingNotifications(dir, query);
8698
+ const byApp = /* @__PURE__ */ new Map();
8699
+ const bySender = /* @__PURE__ */ new Map();
8700
+ const byConversation = /* @__PURE__ */ new Map();
8701
+ const byHour = /* @__PURE__ */ new Map();
8702
+ for (const item of notifications) {
8703
+ const appKey = `${item.appName}\0${item.appDisplayName ?? ""}`;
8704
+ increment(byApp, appKey, {
8705
+ appName: item.appName,
8706
+ ...item.appDisplayName ? { appDisplayName: item.appDisplayName } : {}
8707
+ });
8708
+ const sender = item.senderName?.trim() || item.title?.trim() || "(unknown)";
8709
+ increment(bySender, sender, { sender });
8710
+ const conversationName = item.conversationName?.trim() || item.title?.trim() || "(unknown)";
8711
+ const conversationKey = `${conversationName}\0${item.conversationType ?? ""}`;
8712
+ increment(byConversation, conversationKey, {
8713
+ conversationName,
8714
+ ...item.conversationType ? { conversationType: item.conversationType } : {}
8715
+ });
8716
+ const hour = hourBucket(item.timestamp);
8717
+ if (hour) increment(byHour, hour, { hour });
8718
+ }
8719
+ const senderRows = topRows(bySender, topLimit);
8720
+ output({
8721
+ ok: true,
8722
+ limit: query.limit,
8723
+ total: notifications.length,
8724
+ range: {
8725
+ newest: notifications[0]?.timestamp ?? null,
8726
+ oldest: notifications[notifications.length - 1]?.timestamp ?? null
8727
+ },
8728
+ byApp: topRows(byApp, topLimit),
8729
+ bySender: senderRows,
8730
+ byConversation: topRows(byConversation, topLimit),
8731
+ byHour: topRows(byHour, topLimit),
8732
+ latest: notifications.slice(0, sampleLimit).map(compactNotification),
8733
+ samplesBySender: buildSamplesBySender(notifications, senderRows)
8734
+ });
8735
+ }
8736
+ );
8737
+ }
8738
+
8605
8739
  // src/cli/ntf-stats.ts
8606
8740
  function registerNtfStats(ntf, ctx) {
8607
8741
  ntf.command("stats").description("\u901A\u77E5\u7EDF\u8BA1\u5206\u6790\uFF08\u6309\u65E5\u671F/\u5E94\u7528/\u53D1\u9001\u4EBA/\u65F6\u6BB5\u805A\u5408\uFF09").option("--from <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD", daysAgo(7)).option("--to <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD", today()).option("--app <name>", "\u53EA\u7EDF\u8BA1\u6307\u5B9A\u5E94\u7528").option("--dim <dimension>", "\u7EDF\u8BA1\u7EF4\u5EA6\uFF1Adate/app/sender/hour/all", "all").action(
@@ -9788,7 +9922,6 @@ var checkNodeVersion = () => {
9788
9922
  init_env();
9789
9923
  var checkPluginVersion = async () => {
9790
9924
  const channel = resolveUpdateChannel({
9791
- currentVersion: PLUGIN_VERSION,
9792
9925
  envName: loadEnvName()
9793
9926
  });
9794
9927
  let latest;
@@ -10275,6 +10408,7 @@ function registerAllCli(program, ctx, rootCommandName = "ntf") {
10275
10408
  const ntf = program.command(rootCommandName).description("\u624B\u673A\u901A\u77E5\u6570\u636E\u7BA1\u7406").version(PLUGIN_VERSION, "-v, --version", "\u663E\u793A\u63D2\u4EF6\u7248\u672C");
10276
10409
  registerAuthCli(ntf);
10277
10410
  registerNtfSearch(ntf, ctx);
10411
+ registerNtfSummary(ntf, ctx);
10278
10412
  registerNtfStats(ntf, ctx);
10279
10413
  registerNtfSync(ntf, ctx);
10280
10414
  registerNtfMonitor(ntf, ctx);
@@ -12851,14 +12985,20 @@ async function handleHttpRequest(opts, frame) {
12851
12985
  const mappedPath = mapPath(frame.path);
12852
12986
  const url = new URL(mappedPath, opts.gatewayBaseUrl);
12853
12987
  const startedAtMs = Date.now();
12988
+ const trustedProxyHeader = opts.gatewayTrustedProxyHeader?.trim();
12989
+ const trustedProxyUser = opts.gatewayTrustedProxyUser?.trim();
12990
+ const trustedProxyHeaderLower = trustedProxyHeader?.toLowerCase();
12854
12991
  const localHeaders = {};
12855
12992
  for (const [k, v] of Object.entries(frame.headers ?? {})) {
12856
12993
  const lower = k.toLowerCase();
12857
- if (lower !== "authorization" && lower !== "x-openclaw-password") {
12858
- localHeaders[k] = v;
12859
- }
12994
+ if (lower === "authorization" || lower === "x-openclaw-password") continue;
12995
+ if (trustedProxyHeaderLower && lower === trustedProxyHeaderLower) continue;
12996
+ localHeaders[k] = v;
12860
12997
  }
12861
12998
  localHeaders[RELAY_INTERNAL_HTTP_HEADER] = "1";
12999
+ if (trustedProxyHeader && trustedProxyUser) {
13000
+ localHeaders[trustedProxyHeader] = trustedProxyUser;
13001
+ }
12862
13002
  const authAttempts = buildLocalGatewayAuthAttempts(opts, localHeaders);
12863
13003
  opts.logger.info(
12864
13004
  `TunnelProxy: HTTP id=${frame.id} ${frame.method} ${frame.path} \u2192 ${url.toString()}${summarizeRequestHeaders(frame.headers)}, authAttempts=${authAttempts.map((a) => a.label).join(" -> ")}, body=${previewText2(frame.body)}`
@@ -13303,6 +13443,16 @@ var TunnelProxy = class {
13303
13443
  `TunnelProxy: cleared stale stored device token after gateway mismatch (deviceId=${this.deviceIdentity.deviceId})`
13304
13444
  );
13305
13445
  }
13446
+ /**
13447
+ * 当 Gateway 处于 trusted-proxy 模式时,构造注入到本地连接的 user header。
13448
+ * 其他模式或参数未配齐时返回 undefined,本地用户保持原握手路径不受影响。
13449
+ */
13450
+ buildTrustedProxyHeaders() {
13451
+ const headerName = this.opts.gatewayTrustedProxyHeader?.trim();
13452
+ const headerValue = this.opts.gatewayTrustedProxyUser?.trim();
13453
+ if (!headerName || !headerValue) return void 0;
13454
+ return { [headerName]: headerValue };
13455
+ }
13306
13456
  async maybeAutoApproveGatewayPairing(frame) {
13307
13457
  const errorCode = typeof frame?.error?.code === "string" ? frame.error.code : void 0;
13308
13458
  const detailsCode = typeof frame?.error?.details?.code === "string" ? frame.error.details.code : void 0;
@@ -13473,10 +13623,12 @@ var TunnelProxy = class {
13473
13623
  this.gatewayWsConnecting = true;
13474
13624
  this.gatewayWsReady = false;
13475
13625
  const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
13626
+ const trustedProxyHeaders = this.buildTrustedProxyHeaders();
13627
+ const trustedProxyHint = trustedProxyHeaders ? `, trustedProxyHeader=${Object.keys(trustedProxyHeaders)[0]}` : "";
13476
13628
  this.opts.logger.info(
13477
- `TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
13629
+ `TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length}${trustedProxyHint})`
13478
13630
  );
13479
- const ws = new wrapper_default(wsUrl);
13631
+ const ws = trustedProxyHeaders ? new wrapper_default(wsUrl, { headers: trustedProxyHeaders }) : new wrapper_default(wsUrl);
13480
13632
  ws.on("open", () => {
13481
13633
  this.gatewayWs = ws;
13482
13634
  this.opts.logger.info(
@@ -13809,6 +13961,8 @@ function createTunnelService(opts) {
13809
13961
  gatewayAuthMode: opts.gatewayAuthMode,
13810
13962
  gatewayToken: opts.gatewayToken,
13811
13963
  gatewayPassword: opts.gatewayPassword,
13964
+ gatewayTrustedProxyHeader: opts.gatewayTrustedProxyHeader,
13965
+ gatewayTrustedProxyUser: opts.gatewayTrustedProxyUser,
13812
13966
  client,
13813
13967
  logger
13814
13968
  });
@@ -13921,9 +14075,14 @@ function readHostGatewayConfig(params) {
13921
14075
  }
13922
14076
  return configData;
13923
14077
  }
14078
+ var DEFAULT_TRUSTED_PROXY_HEADER = "x-forwarded-user";
14079
+ var DEFAULT_TRUSTED_PROXY_USER = "openclaw";
13924
14080
  function resolveLocalGatewayAuth(params) {
13925
14081
  const envGatewayToken = trimToUndefined2(process.env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined2(process.env.QCLAW_GATEWAY_TOKEN);
13926
14082
  const envGatewayPassword = trimToUndefined2(process.env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined2(process.env.QCLAW_GATEWAY_PASSWORD);
14083
+ const envTrustedProxyUser = trimToUndefined2(
14084
+ process.env.OPENCLAW_GATEWAY_TRUSTED_PROXY_USER
14085
+ );
13927
14086
  const configData = readHostGatewayConfig(params);
13928
14087
  let configGatewayAuthMode;
13929
14088
  const rawGatewayAuthMode = trimToUndefined2(configData?.gateway?.auth?.mode);
@@ -13932,10 +14091,18 @@ function resolveLocalGatewayAuth(params) {
13932
14091
  }
13933
14092
  const configGatewayToken = trimToUndefined2(configData?.gateway?.auth?.token);
13934
14093
  const configGatewayPassword = trimToUndefined2(configData?.gateway?.auth?.password);
14094
+ let gatewayTrustedProxyHeader;
14095
+ let gatewayTrustedProxyUser;
14096
+ if (rawGatewayAuthMode === "trusted-proxy") {
14097
+ gatewayTrustedProxyHeader = trimToUndefined2(configData?.gateway?.auth?.trustedProxy?.userHeader) ?? DEFAULT_TRUSTED_PROXY_HEADER;
14098
+ gatewayTrustedProxyUser = envTrustedProxyUser ?? DEFAULT_TRUSTED_PROXY_USER;
14099
+ }
13935
14100
  return {
13936
14101
  gatewayAuthMode: configGatewayAuthMode,
13937
14102
  gatewayToken: envGatewayToken ?? configGatewayToken,
13938
- gatewayPassword: envGatewayPassword ?? configGatewayPassword
14103
+ gatewayPassword: envGatewayPassword ?? configGatewayPassword,
14104
+ gatewayTrustedProxyHeader,
14105
+ gatewayTrustedProxyUser
13939
14106
  };
13940
14107
  }
13941
14108
  function resolveExclusiveTunnelHint(params) {
@@ -13984,10 +14151,21 @@ function registerRelayTunnelLifecycle(deps) {
13984
14151
  return null;
13985
14152
  }
13986
14153
  const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? process.env.QCLAW_GATEWAY_PORT ?? "18789";
13987
- const { gatewayAuthMode, gatewayToken, gatewayPassword } = resolveLocalGatewayAuth({
14154
+ const {
14155
+ gatewayAuthMode,
14156
+ gatewayToken,
14157
+ gatewayPassword,
14158
+ gatewayTrustedProxyHeader,
14159
+ gatewayTrustedProxyUser
14160
+ } = resolveLocalGatewayAuth({
13988
14161
  stateDir: openclawDir,
13989
14162
  logger
13990
14163
  });
14164
+ if (gatewayTrustedProxyHeader && gatewayTrustedProxyUser) {
14165
+ logger.info(
14166
+ `Relay tunnel: gateway in trusted-proxy mode, will inject ${gatewayTrustedProxyHeader} header on local connections`
14167
+ );
14168
+ }
13991
14169
  const tunnelService = createTunnelService({
13992
14170
  tunnelUrl,
13993
14171
  heartbeatSec: config.relay?.heartbeatSec,
@@ -13996,6 +14174,8 @@ function registerRelayTunnelLifecycle(deps) {
13996
14174
  gatewayAuthMode,
13997
14175
  gatewayToken,
13998
14176
  gatewayPassword,
14177
+ gatewayTrustedProxyHeader,
14178
+ gatewayTrustedProxyUser,
13999
14179
  logger
14000
14180
  });
14001
14181
  api.registerService(tunnelService);