@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 +3 -0
- package/dist/index.cjs +241 -66
- package/dist/index.cjs.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/notification-query/SKILL.md +27 -1
- package/skills/{notification-statistics/SKILL.md → notification-query/references/statistics.md} +10 -65
package/README.md
CHANGED
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.
|
|
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
|
-
|
|
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 (
|
|
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-
|
|
8522
|
-
function
|
|
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
|
|
8548
|
-
|
|
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
|
|
12858
|
-
|
|
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 {
|
|
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);
|