@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 +3 -0
- package/dist/index.cjs +246 -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.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
|
-
|
|
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,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
|
|
12858
|
-
|
|
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 {
|
|
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);
|