@yoooclaw/phone-notifications 1.11.8 → 1.11.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/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.10".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,12 +12985,13 @@ 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 trustedProxyHeaderLower = opts.gatewayTrustedProxyHeader?.trim().toLowerCase();
12854
12989
  const localHeaders = {};
12855
12990
  for (const [k, v] of Object.entries(frame.headers ?? {})) {
12856
12991
  const lower = k.toLowerCase();
12857
- if (lower !== "authorization" && lower !== "x-openclaw-password") {
12858
- localHeaders[k] = v;
12859
- }
12992
+ if (lower === "authorization" || lower === "x-openclaw-password") continue;
12993
+ if (trustedProxyHeaderLower && lower === trustedProxyHeaderLower) continue;
12994
+ localHeaders[k] = v;
12860
12995
  }
12861
12996
  localHeaders[RELAY_INTERNAL_HTTP_HEADER] = "1";
12862
12997
  const authAttempts = buildLocalGatewayAuthAttempts(opts, localHeaders);
@@ -13303,6 +13438,16 @@ var TunnelProxy = class {
13303
13438
  `TunnelProxy: cleared stale stored device token after gateway mismatch (deviceId=${this.deviceIdentity.deviceId})`
13304
13439
  );
13305
13440
  }
13441
+ /**
13442
+ * 当 Gateway 处于 trusted-proxy 模式时,构造注入到本地连接的 user header。
13443
+ * 其他模式或参数未配齐时返回 undefined,本地用户保持原握手路径不受影响。
13444
+ */
13445
+ buildTrustedProxyHeaders() {
13446
+ const headerName = this.opts.gatewayTrustedProxyHeader?.trim();
13447
+ const headerValue = this.opts.gatewayTrustedProxyUser?.trim();
13448
+ if (!headerName || !headerValue) return void 0;
13449
+ return { [headerName]: headerValue };
13450
+ }
13306
13451
  async maybeAutoApproveGatewayPairing(frame) {
13307
13452
  const errorCode = typeof frame?.error?.code === "string" ? frame.error.code : void 0;
13308
13453
  const detailsCode = typeof frame?.error?.details?.code === "string" ? frame.error.details.code : void 0;
@@ -13473,10 +13618,12 @@ var TunnelProxy = class {
13473
13618
  this.gatewayWsConnecting = true;
13474
13619
  this.gatewayWsReady = false;
13475
13620
  const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
13621
+ const trustedProxyHeaders = this.buildTrustedProxyHeaders();
13622
+ const trustedProxyHint = trustedProxyHeaders ? `, trustedProxyHeader=${Object.keys(trustedProxyHeaders)[0]}` : "";
13476
13623
  this.opts.logger.info(
13477
- `TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
13624
+ `TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length}${trustedProxyHint})`
13478
13625
  );
13479
- const ws = new wrapper_default(wsUrl);
13626
+ const ws = trustedProxyHeaders ? new wrapper_default(wsUrl, { headers: trustedProxyHeaders }) : new wrapper_default(wsUrl);
13480
13627
  ws.on("open", () => {
13481
13628
  this.gatewayWs = ws;
13482
13629
  this.opts.logger.info(
@@ -13809,6 +13956,8 @@ function createTunnelService(opts) {
13809
13956
  gatewayAuthMode: opts.gatewayAuthMode,
13810
13957
  gatewayToken: opts.gatewayToken,
13811
13958
  gatewayPassword: opts.gatewayPassword,
13959
+ gatewayTrustedProxyHeader: opts.gatewayTrustedProxyHeader,
13960
+ gatewayTrustedProxyUser: opts.gatewayTrustedProxyUser,
13812
13961
  client,
13813
13962
  logger
13814
13963
  });
@@ -13921,9 +14070,14 @@ function readHostGatewayConfig(params) {
13921
14070
  }
13922
14071
  return configData;
13923
14072
  }
14073
+ var DEFAULT_TRUSTED_PROXY_HEADER = "x-forwarded-user";
14074
+ var DEFAULT_TRUSTED_PROXY_USER = "openclaw";
13924
14075
  function resolveLocalGatewayAuth(params) {
13925
14076
  const envGatewayToken = trimToUndefined2(process.env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined2(process.env.QCLAW_GATEWAY_TOKEN);
13926
14077
  const envGatewayPassword = trimToUndefined2(process.env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined2(process.env.QCLAW_GATEWAY_PASSWORD);
14078
+ const envTrustedProxyUser = trimToUndefined2(
14079
+ process.env.OPENCLAW_GATEWAY_TRUSTED_PROXY_USER
14080
+ );
13927
14081
  const configData = readHostGatewayConfig(params);
13928
14082
  let configGatewayAuthMode;
13929
14083
  const rawGatewayAuthMode = trimToUndefined2(configData?.gateway?.auth?.mode);
@@ -13932,10 +14086,18 @@ function resolveLocalGatewayAuth(params) {
13932
14086
  }
13933
14087
  const configGatewayToken = trimToUndefined2(configData?.gateway?.auth?.token);
13934
14088
  const configGatewayPassword = trimToUndefined2(configData?.gateway?.auth?.password);
14089
+ let gatewayTrustedProxyHeader;
14090
+ let gatewayTrustedProxyUser;
14091
+ if (rawGatewayAuthMode === "trusted-proxy") {
14092
+ gatewayTrustedProxyHeader = trimToUndefined2(configData?.gateway?.auth?.trustedProxy?.userHeader) ?? DEFAULT_TRUSTED_PROXY_HEADER;
14093
+ gatewayTrustedProxyUser = envTrustedProxyUser ?? DEFAULT_TRUSTED_PROXY_USER;
14094
+ }
13935
14095
  return {
13936
14096
  gatewayAuthMode: configGatewayAuthMode,
13937
14097
  gatewayToken: envGatewayToken ?? configGatewayToken,
13938
- gatewayPassword: envGatewayPassword ?? configGatewayPassword
14098
+ gatewayPassword: envGatewayPassword ?? configGatewayPassword,
14099
+ gatewayTrustedProxyHeader,
14100
+ gatewayTrustedProxyUser
13939
14101
  };
13940
14102
  }
13941
14103
  function resolveExclusiveTunnelHint(params) {
@@ -13984,10 +14146,21 @@ function registerRelayTunnelLifecycle(deps) {
13984
14146
  return null;
13985
14147
  }
13986
14148
  const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? process.env.QCLAW_GATEWAY_PORT ?? "18789";
13987
- const { gatewayAuthMode, gatewayToken, gatewayPassword } = resolveLocalGatewayAuth({
14149
+ const {
14150
+ gatewayAuthMode,
14151
+ gatewayToken,
14152
+ gatewayPassword,
14153
+ gatewayTrustedProxyHeader,
14154
+ gatewayTrustedProxyUser
14155
+ } = resolveLocalGatewayAuth({
13988
14156
  stateDir: openclawDir,
13989
14157
  logger
13990
14158
  });
14159
+ if (gatewayTrustedProxyHeader && gatewayTrustedProxyUser) {
14160
+ logger.info(
14161
+ `Relay tunnel: gateway in trusted-proxy mode, will inject ${gatewayTrustedProxyHeader} header on local connections`
14162
+ );
14163
+ }
13991
14164
  const tunnelService = createTunnelService({
13992
14165
  tunnelUrl,
13993
14166
  heartbeatSec: config.relay?.heartbeatSec,
@@ -13996,6 +14169,8 @@ function registerRelayTunnelLifecycle(deps) {
13996
14169
  gatewayAuthMode,
13997
14170
  gatewayToken,
13998
14171
  gatewayPassword,
14172
+ gatewayTrustedProxyHeader,
14173
+ gatewayTrustedProxyUser,
13999
14174
  logger
14000
14175
  });
14001
14176
  api.registerService(tunnelService);