@yoooclaw/phone-notifications 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +234 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2233,7 +2233,7 @@ var require_websocket = __commonJS({
|
|
|
2233
2233
|
var http = __require("http");
|
|
2234
2234
|
var net = __require("net");
|
|
2235
2235
|
var tls = __require("tls");
|
|
2236
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
2236
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
2237
2237
|
var { Duplex, Readable } = __require("stream");
|
|
2238
2238
|
var { URL: URL2 } = __require("url");
|
|
2239
2239
|
var PerMessageDeflate = require_permessage_deflate();
|
|
@@ -2893,7 +2893,7 @@ var require_websocket = __commonJS({
|
|
|
2893
2893
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2894
2894
|
return;
|
|
2895
2895
|
}
|
|
2896
|
-
const digest =
|
|
2896
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
2897
2897
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2898
2898
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2899
2899
|
return;
|
|
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
|
|
|
3260
3260
|
var EventEmitter = __require("events");
|
|
3261
3261
|
var http = __require("http");
|
|
3262
3262
|
var { Duplex } = __require("stream");
|
|
3263
|
-
var { createHash } = __require("crypto");
|
|
3263
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
3264
3264
|
var extension = require_extension();
|
|
3265
3265
|
var PerMessageDeflate = require_permessage_deflate();
|
|
3266
3266
|
var subprotocol = require_subprotocol();
|
|
@@ -3561,7 +3561,7 @@ var require_websocket_server = __commonJS({
|
|
|
3561
3561
|
);
|
|
3562
3562
|
}
|
|
3563
3563
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3564
|
-
const digest =
|
|
3564
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
3565
3565
|
const headers = [
|
|
3566
3566
|
"HTTP/1.1 101 Switching Protocols",
|
|
3567
3567
|
"Upgrade: websocket",
|
|
@@ -3798,11 +3798,13 @@ import { join, dirname } from "path";
|
|
|
3798
3798
|
var ENV_CONFIG = {
|
|
3799
3799
|
development: {
|
|
3800
3800
|
lightApiUrl: "https://openclaw-service-dev.yoootek.com/api/message/tob/sendMessage",
|
|
3801
|
-
relayTunnelUrl: "wss://openclaw-service-dev.yoootek.com/message/messages/ws/plugin"
|
|
3801
|
+
relayTunnelUrl: "wss://openclaw-service-dev.yoootek.com/message/messages/ws/plugin",
|
|
3802
|
+
appNameMapUrl: "https://openclaw-service-dev.yoootek.com/api/application-config/app-package/config-all"
|
|
3802
3803
|
},
|
|
3803
3804
|
production: {
|
|
3804
3805
|
lightApiUrl: "https://openclaw-service.yoootek.com/api/message/tob/sendMessage",
|
|
3805
|
-
relayTunnelUrl: "wss://openclaw-service.yoootek.com/message/messages/ws/plugin"
|
|
3806
|
+
relayTunnelUrl: "wss://openclaw-service.yoootek.com/message/messages/ws/plugin",
|
|
3807
|
+
appNameMapUrl: "https://openclaw-service.yoootek.com/api/application-config/app-package/config-all"
|
|
3806
3808
|
}
|
|
3807
3809
|
};
|
|
3808
3810
|
var VALID_ENVS = new Set(Object.keys(ENV_CONFIG));
|
|
@@ -3812,13 +3814,13 @@ function envFilePath() {
|
|
|
3812
3814
|
}
|
|
3813
3815
|
function loadEnvName() {
|
|
3814
3816
|
const filePath = envFilePath();
|
|
3815
|
-
if (!existsSync(filePath)) return "
|
|
3817
|
+
if (!existsSync(filePath)) return "production";
|
|
3816
3818
|
try {
|
|
3817
3819
|
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
3818
3820
|
if (data.env && VALID_ENVS.has(data.env)) return data.env;
|
|
3819
3821
|
} catch {
|
|
3820
3822
|
}
|
|
3821
|
-
return "
|
|
3823
|
+
return "production";
|
|
3822
3824
|
}
|
|
3823
3825
|
function saveEnvName(env) {
|
|
3824
3826
|
if (!VALID_ENVS.has(env)) {
|
|
@@ -3839,12 +3841,12 @@ function getAvailableEnvs() {
|
|
|
3839
3841
|
}
|
|
3840
3842
|
|
|
3841
3843
|
// src/light/sender.ts
|
|
3842
|
-
async function sendLightEffect(
|
|
3844
|
+
async function sendLightEffect(apiKey, segments, logger, repeat) {
|
|
3843
3845
|
const apiUrl = getEnvUrls().lightApiUrl;
|
|
3844
3846
|
const appKey = "7Q617S1G5WD274JI";
|
|
3845
3847
|
const templateId = "1990771146010017788";
|
|
3846
3848
|
logger?.info(
|
|
3847
|
-
`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "\u2026" : "UNSET"}, templateId=${templateId ?? "UNSET"},
|
|
3849
|
+
`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "\u2026" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "\u2026" : "EMPTY"}, segments=${JSON.stringify(segments)}`
|
|
3848
3850
|
);
|
|
3849
3851
|
if (!apiUrl || !appKey || !templateId) {
|
|
3850
3852
|
return {
|
|
@@ -3869,7 +3871,7 @@ async function sendLightEffect(token, segments, logger, repeat) {
|
|
|
3869
3871
|
method: "POST",
|
|
3870
3872
|
headers: {
|
|
3871
3873
|
"Content-Type": "application/json",
|
|
3872
|
-
|
|
3874
|
+
"X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
|
|
3873
3875
|
},
|
|
3874
3876
|
body: JSON.stringify(requestBody)
|
|
3875
3877
|
});
|
|
@@ -3918,17 +3920,18 @@ function writeCredentials(creds) {
|
|
|
3918
3920
|
mode: 384
|
|
3919
3921
|
});
|
|
3920
3922
|
}
|
|
3921
|
-
function
|
|
3922
|
-
|
|
3923
|
+
function loadApiKey() {
|
|
3924
|
+
const creds = readCredentials();
|
|
3925
|
+
return creds.apiKey ?? creds.token;
|
|
3923
3926
|
}
|
|
3924
|
-
function
|
|
3925
|
-
const
|
|
3926
|
-
if (!
|
|
3927
|
+
function requireApiKey() {
|
|
3928
|
+
const apiKey = loadApiKey();
|
|
3929
|
+
if (!apiKey) {
|
|
3927
3930
|
throw new Error(
|
|
3928
|
-
"
|
|
3931
|
+
"API Key \u672A\u8BBE\u7F6E\uFF0C\u8BF7\u5148\u6267\u884C openclaw ntf auth set-api-key <apiKey>\uFF08\u82E5 ntf \u547D\u4EE4\u51B2\u7A81\uFF0C\u53EF\u4F7F\u7528 openclaw phone-notifications auth set-api-key <apiKey>\uFF09"
|
|
3929
3932
|
);
|
|
3930
3933
|
}
|
|
3931
|
-
return
|
|
3934
|
+
return apiKey;
|
|
3932
3935
|
}
|
|
3933
3936
|
function watchCredentials(onChange) {
|
|
3934
3937
|
const path2 = credentialsPath();
|
|
@@ -3958,8 +3961,8 @@ function watchCredentials(onChange) {
|
|
|
3958
3961
|
// src/notification/app-name-map.ts
|
|
3959
3962
|
var PLUGIN_STATE_DIR = "phone-notifications";
|
|
3960
3963
|
var CACHE_FILE = "app-name-map.json";
|
|
3961
|
-
var BUILTIN_APP_NAME_MAP_URL =
|
|
3962
|
-
var APP_NAME_MAP_URL = BUILTIN_APP_NAME_MAP_URL;
|
|
3964
|
+
var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
|
|
3965
|
+
var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
|
|
3963
3966
|
var APP_NAME_MAP_REFRESH_HOURS = 12;
|
|
3964
3967
|
function isRecordOfStrings(v) {
|
|
3965
3968
|
if (v === null || typeof v !== "object") return false;
|
|
@@ -3992,36 +3995,54 @@ function createAppNameMapProvider(opts) {
|
|
|
3992
3995
|
if (!isRecordOfStrings(raw)) return;
|
|
3993
3996
|
map.clear();
|
|
3994
3997
|
for (const [k, v] of Object.entries(raw)) map.set(k, v);
|
|
3998
|
+
logger.info(`[app-name-map] loaded ${map.size} entries from cache: ${path2}`);
|
|
3995
3999
|
} catch {
|
|
3996
4000
|
}
|
|
3997
4001
|
}
|
|
3998
4002
|
async function fetchFromServer() {
|
|
3999
|
-
const
|
|
4000
|
-
if (!
|
|
4001
|
-
|
|
4003
|
+
const apiKey = loadApiKey();
|
|
4004
|
+
if (!url) {
|
|
4005
|
+
logger.warn("[app-name-map] APP_NAME_MAP_URL is empty, skip refresh");
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
if (!apiKey) {
|
|
4009
|
+
logger.info("[app-name-map] api key missing, skip refresh");
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
|
|
4002
4013
|
try {
|
|
4003
4014
|
const res = await fetch(url, {
|
|
4004
4015
|
method: "POST",
|
|
4005
|
-
headers: { "Content-Type": "application/json",
|
|
4016
|
+
headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
|
|
4006
4017
|
body: JSON.stringify({
|
|
4007
4018
|
platform: ""
|
|
4008
4019
|
})
|
|
4009
4020
|
});
|
|
4010
4021
|
if (!res.ok) {
|
|
4022
|
+
logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
|
|
4011
4023
|
return;
|
|
4012
4024
|
}
|
|
4013
4025
|
const body = await res.json();
|
|
4014
4026
|
if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
|
|
4027
|
+
logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
|
|
4015
4028
|
return;
|
|
4016
4029
|
}
|
|
4017
4030
|
map.clear();
|
|
4018
4031
|
for (const item of body.data) {
|
|
4019
4032
|
if (item.packageName && item.appName) map.set(item.packageName, item.appName);
|
|
4020
4033
|
}
|
|
4034
|
+
if (map.size === 0) {
|
|
4035
|
+
logger.warn("[app-name-map] refresh succeeded but got 0 entries");
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4021
4038
|
const dir = join3(stateDir, "plugins", PLUGIN_STATE_DIR);
|
|
4022
4039
|
mkdirSync3(dir, { recursive: true });
|
|
4023
|
-
|
|
4040
|
+
const cachePath = getCachePath(stateDir);
|
|
4041
|
+
writeFileSync3(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
|
|
4042
|
+
logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
|
|
4024
4043
|
} catch (e) {
|
|
4044
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
4045
|
+
logger.warn(`[app-name-map] refresh error: ${message}`);
|
|
4025
4046
|
}
|
|
4026
4047
|
}
|
|
4027
4048
|
async function ensureOneFetch() {
|
|
@@ -4072,9 +4093,11 @@ import {
|
|
|
4072
4093
|
rmSync,
|
|
4073
4094
|
constants
|
|
4074
4095
|
} from "fs";
|
|
4096
|
+
import { createHash } from "crypto";
|
|
4075
4097
|
import { join as join4 } from "path";
|
|
4076
4098
|
var NOTIFICATION_DIR_NAME = "notifications";
|
|
4077
4099
|
var ID_INDEX_DIR_NAME = ".ids";
|
|
4100
|
+
var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
|
|
4078
4101
|
function getStateFallbackNotificationDir(stateDir) {
|
|
4079
4102
|
return join4(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
|
|
4080
4103
|
}
|
|
@@ -4110,56 +4133,75 @@ var NotificationStorage = class {
|
|
|
4110
4133
|
this.logger = logger;
|
|
4111
4134
|
this.dir = dir;
|
|
4112
4135
|
this.idIndexDir = join4(dir, ID_INDEX_DIR_NAME);
|
|
4136
|
+
this.contentKeyIndexDir = join4(dir, CONTENT_KEY_INDEX_DIR_NAME);
|
|
4113
4137
|
this.resolveDisplayName = resolveDisplayName;
|
|
4114
4138
|
}
|
|
4115
4139
|
dir;
|
|
4116
4140
|
idIndexDir;
|
|
4141
|
+
contentKeyIndexDir;
|
|
4117
4142
|
idCache = /* @__PURE__ */ new Map();
|
|
4143
|
+
contentKeyCache = /* @__PURE__ */ new Map();
|
|
4144
|
+
dateWriteChains = /* @__PURE__ */ new Map();
|
|
4118
4145
|
resolveDisplayName;
|
|
4119
4146
|
async init() {
|
|
4120
4147
|
mkdirSync4(this.dir, { recursive: true });
|
|
4121
4148
|
mkdirSync4(this.idIndexDir, { recursive: true });
|
|
4149
|
+
mkdirSync4(this.contentKeyIndexDir, { recursive: true });
|
|
4122
4150
|
}
|
|
4123
4151
|
async ingest(items) {
|
|
4152
|
+
const result = {
|
|
4153
|
+
received: items.length,
|
|
4154
|
+
ingested: 0,
|
|
4155
|
+
dedupedById: 0,
|
|
4156
|
+
dedupedByContent: 0,
|
|
4157
|
+
invalid: 0
|
|
4158
|
+
};
|
|
4124
4159
|
for (const n of items) {
|
|
4125
|
-
await this.writeNotification(n);
|
|
4160
|
+
const outcome = await this.writeNotification(n);
|
|
4161
|
+
result[outcome] += 1;
|
|
4126
4162
|
}
|
|
4127
4163
|
this.prune();
|
|
4164
|
+
return result;
|
|
4128
4165
|
}
|
|
4129
4166
|
async writeNotification(n) {
|
|
4130
4167
|
const ts = new Date(n.timestamp);
|
|
4131
4168
|
if (Number.isNaN(ts.getTime())) {
|
|
4132
4169
|
this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
|
|
4133
|
-
return;
|
|
4170
|
+
return "invalid";
|
|
4134
4171
|
}
|
|
4135
4172
|
const dateKey = this.formatDate(ts);
|
|
4136
4173
|
const filePath = join4(this.dir, `${dateKey}.json`);
|
|
4137
4174
|
const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4175
|
+
const entry = this.buildStoredNotification(n);
|
|
4176
|
+
return this.withDateWriteLock(dateKey, async () => {
|
|
4177
|
+
if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
|
|
4178
|
+
return "dedupedById";
|
|
4179
|
+
}
|
|
4180
|
+
if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
|
|
4181
|
+
return "dedupedByContent";
|
|
4182
|
+
}
|
|
4183
|
+
const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
|
|
4184
|
+
const storedEntry = {
|
|
4185
|
+
...entry,
|
|
4186
|
+
appDisplayName
|
|
4187
|
+
};
|
|
4188
|
+
const arr = this.readStoredNotifications(filePath);
|
|
4189
|
+
arr.push(storedEntry);
|
|
4190
|
+
writeFileSync4(filePath, JSON.stringify(arr, null, 2), "utf-8");
|
|
4191
|
+
if (normalizedId) {
|
|
4192
|
+
this.recordNotificationId(dateKey, normalizedId);
|
|
4193
|
+
}
|
|
4194
|
+
this.recordNotificationContentKey(dateKey, filePath, storedEntry);
|
|
4195
|
+
return "ingested";
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
buildStoredNotification(n) {
|
|
4199
|
+
return {
|
|
4200
|
+
appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
|
|
4146
4201
|
title: typeof n.title === "string" ? n.title : "",
|
|
4147
4202
|
content: this.buildContent(n),
|
|
4148
4203
|
timestamp: n.timestamp
|
|
4149
4204
|
};
|
|
4150
|
-
let arr = [];
|
|
4151
|
-
if (existsSync4(filePath)) {
|
|
4152
|
-
try {
|
|
4153
|
-
arr = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
4154
|
-
} catch {
|
|
4155
|
-
arr = [];
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
|
-
arr.push(entry);
|
|
4159
|
-
writeFileSync4(filePath, JSON.stringify(arr, null, 2), "utf-8");
|
|
4160
|
-
if (normalizedId) {
|
|
4161
|
-
this.recordNotificationId(dateKey, normalizedId);
|
|
4162
|
-
}
|
|
4163
4205
|
}
|
|
4164
4206
|
buildContent(n) {
|
|
4165
4207
|
const body = n.body?.trim();
|
|
@@ -4203,9 +4245,44 @@ var NotificationStorage = class {
|
|
|
4203
4245
|
this.idCache.set(dateKey, ids);
|
|
4204
4246
|
return ids;
|
|
4205
4247
|
}
|
|
4248
|
+
getContentKeyIndexPath(dateKey) {
|
|
4249
|
+
return join4(this.contentKeyIndexDir, `${dateKey}.keys`);
|
|
4250
|
+
}
|
|
4251
|
+
getContentKeySet(dateKey, filePath) {
|
|
4252
|
+
const cached = this.contentKeyCache.get(dateKey);
|
|
4253
|
+
if (cached) {
|
|
4254
|
+
return cached;
|
|
4255
|
+
}
|
|
4256
|
+
const keyPath = this.getContentKeyIndexPath(dateKey);
|
|
4257
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4258
|
+
if (existsSync4(keyPath)) {
|
|
4259
|
+
const lines = readFileSync4(keyPath, "utf-8").split(/\r?\n/);
|
|
4260
|
+
for (const line of lines) {
|
|
4261
|
+
const key = line.trim();
|
|
4262
|
+
if (key) {
|
|
4263
|
+
keys.add(key);
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
} else if (existsSync4(filePath)) {
|
|
4267
|
+
for (const item of this.readStoredNotifications(filePath)) {
|
|
4268
|
+
keys.add(this.buildNotificationContentKey(item));
|
|
4269
|
+
}
|
|
4270
|
+
if (keys.size > 0) {
|
|
4271
|
+
writeFileSync4(keyPath, `${Array.from(keys).join("\n")}
|
|
4272
|
+
`, "utf-8");
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
this.contentKeyCache.set(dateKey, keys);
|
|
4276
|
+
return keys;
|
|
4277
|
+
}
|
|
4206
4278
|
hasNotificationId(dateKey, id) {
|
|
4207
4279
|
return this.getIdSet(dateKey).has(id);
|
|
4208
4280
|
}
|
|
4281
|
+
hasNotificationContentKey(dateKey, filePath, entry) {
|
|
4282
|
+
return this.getContentKeySet(dateKey, filePath).has(
|
|
4283
|
+
this.buildNotificationContentKey(entry)
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4209
4286
|
recordNotificationId(dateKey, id) {
|
|
4210
4287
|
const ids = this.getIdSet(dateKey);
|
|
4211
4288
|
if (ids.has(id)) {
|
|
@@ -4215,12 +4292,55 @@ var NotificationStorage = class {
|
|
|
4215
4292
|
`, "utf-8");
|
|
4216
4293
|
ids.add(id);
|
|
4217
4294
|
}
|
|
4295
|
+
recordNotificationContentKey(dateKey, filePath, entry) {
|
|
4296
|
+
const keys = this.getContentKeySet(dateKey, filePath);
|
|
4297
|
+
const key = this.buildNotificationContentKey(entry);
|
|
4298
|
+
if (keys.has(key)) {
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
appendFileSync(this.getContentKeyIndexPath(dateKey), `${key}
|
|
4302
|
+
`, "utf-8");
|
|
4303
|
+
keys.add(key);
|
|
4304
|
+
}
|
|
4305
|
+
buildNotificationContentKey(entry) {
|
|
4306
|
+
return createHash("sha256").update(entry.appName).update("").update(entry.title).update("").update(entry.content).digest("hex");
|
|
4307
|
+
}
|
|
4308
|
+
readStoredNotifications(filePath) {
|
|
4309
|
+
if (!existsSync4(filePath)) {
|
|
4310
|
+
return [];
|
|
4311
|
+
}
|
|
4312
|
+
try {
|
|
4313
|
+
const parsed = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
4314
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
4315
|
+
} catch {
|
|
4316
|
+
return [];
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
async withDateWriteLock(dateKey, task) {
|
|
4320
|
+
const previous = this.dateWriteChains.get(dateKey) ?? Promise.resolve();
|
|
4321
|
+
let release;
|
|
4322
|
+
const current = new Promise((resolve) => {
|
|
4323
|
+
release = resolve;
|
|
4324
|
+
});
|
|
4325
|
+
const chain = previous.then(() => current);
|
|
4326
|
+
this.dateWriteChains.set(dateKey, chain);
|
|
4327
|
+
await previous;
|
|
4328
|
+
try {
|
|
4329
|
+
return await task();
|
|
4330
|
+
} finally {
|
|
4331
|
+
release();
|
|
4332
|
+
if (this.dateWriteChains.get(dateKey) === chain) {
|
|
4333
|
+
this.dateWriteChains.delete(dateKey);
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4218
4337
|
prune() {
|
|
4219
4338
|
const retentionDays = this.config.retentionDays ?? 30;
|
|
4220
4339
|
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
4221
4340
|
const cutoffDate = this.formatDate(new Date(cutoffMs));
|
|
4222
4341
|
this.pruneDataFiles(cutoffDate);
|
|
4223
4342
|
this.pruneIdIndex(cutoffDate);
|
|
4343
|
+
this.pruneContentKeyIndex(cutoffDate);
|
|
4224
4344
|
}
|
|
4225
4345
|
/** Remove expired .json, legacy .md files, and legacy date directories */
|
|
4226
4346
|
pruneDataFiles(cutoffDate) {
|
|
@@ -4254,8 +4374,24 @@ var NotificationStorage = class {
|
|
|
4254
4374
|
} catch {
|
|
4255
4375
|
}
|
|
4256
4376
|
}
|
|
4377
|
+
/** Remove expired .keys index files */
|
|
4378
|
+
pruneContentKeyIndex(cutoffDate) {
|
|
4379
|
+
try {
|
|
4380
|
+
for (const entry of readdirSync(this.contentKeyIndexDir, { withFileTypes: true })) {
|
|
4381
|
+
if (!entry.isFile()) continue;
|
|
4382
|
+
const match = /^(\d{4}-\d{2}-\d{2})\.keys$/.exec(entry.name);
|
|
4383
|
+
if (match && match[1] < cutoffDate) {
|
|
4384
|
+
rmSync(join4(this.contentKeyIndexDir, entry.name), { force: true });
|
|
4385
|
+
this.contentKeyCache.delete(match[1]);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
} catch {
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4257
4391
|
async close() {
|
|
4258
4392
|
this.idCache.clear();
|
|
4393
|
+
this.contentKeyCache.clear();
|
|
4394
|
+
this.dateWriteChains.clear();
|
|
4259
4395
|
}
|
|
4260
4396
|
};
|
|
4261
4397
|
|
|
@@ -4328,31 +4464,41 @@ function exitError(code, message) {
|
|
|
4328
4464
|
// src/cli/auth.ts
|
|
4329
4465
|
function registerAuthCli(program) {
|
|
4330
4466
|
const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
|
|
4331
|
-
auth.command("set-
|
|
4332
|
-
writeCredentials({ ...readCredentials(), token });
|
|
4467
|
+
auth.command("set-api-key <apiKey>").description("\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u6301\u4E45\u5316\u5230\u672C\u5730\u914D\u7F6E\uFF09").action((apiKey) => {
|
|
4468
|
+
writeCredentials({ ...readCredentials(), apiKey, token: void 0 });
|
|
4469
|
+
output({
|
|
4470
|
+
ok: true,
|
|
4471
|
+
apiKey: apiKey.slice(0, 8) + "\u2026",
|
|
4472
|
+
storedAt: credentialsPath()
|
|
4473
|
+
});
|
|
4474
|
+
});
|
|
4475
|
+
auth.command("set-token <token>").description("\uFF08\u517C\u5BB9\uFF09\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u65E7\u547D\u4EE4\u540D\uFF09").action((token) => {
|
|
4476
|
+
writeCredentials({ ...readCredentials(), apiKey: token, token: void 0 });
|
|
4333
4477
|
output({
|
|
4334
4478
|
ok: true,
|
|
4335
|
-
|
|
4479
|
+
apiKey: token.slice(0, 8) + "\u2026",
|
|
4336
4480
|
storedAt: credentialsPath()
|
|
4337
4481
|
});
|
|
4338
4482
|
});
|
|
4339
4483
|
auth.command("show").description("\u67E5\u770B\u5F53\u524D\u8BA4\u8BC1\u72B6\u6001").action(() => {
|
|
4340
4484
|
const creds = readCredentials();
|
|
4341
|
-
|
|
4485
|
+
const apiKey = creds.apiKey ?? creds.token;
|
|
4486
|
+
if (apiKey) {
|
|
4342
4487
|
output({
|
|
4343
4488
|
ok: true,
|
|
4344
|
-
|
|
4345
|
-
|
|
4489
|
+
hasApiKey: true,
|
|
4490
|
+
apiKey: apiKey.slice(0, 8) + "\u2026",
|
|
4346
4491
|
storedAt: credentialsPath()
|
|
4347
4492
|
});
|
|
4348
4493
|
} else {
|
|
4349
|
-
output({ ok: true,
|
|
4494
|
+
output({ ok: true, hasApiKey: false });
|
|
4350
4495
|
}
|
|
4351
4496
|
});
|
|
4352
4497
|
auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
|
|
4353
4498
|
const path2 = credentialsPath();
|
|
4354
4499
|
if (existsSync6(path2)) {
|
|
4355
4500
|
const creds = readCredentials();
|
|
4501
|
+
delete creds.apiKey;
|
|
4356
4502
|
delete creds.token;
|
|
4357
4503
|
if (Object.keys(creds).length === 0) {
|
|
4358
4504
|
rmSync2(path2, { force: true });
|
|
@@ -5012,14 +5158,14 @@ function registerLightRules(program, ctx) {
|
|
|
5012
5158
|
// src/cli/light-send.ts
|
|
5013
5159
|
function registerLightSend(light) {
|
|
5014
5160
|
light.command("send").description("\u53D1\u9001\u706F\u6548\u6307\u4EE4\u5230\u786C\u4EF6\u8BBE\u5907").requiredOption("--segments <json>", "\u706F\u6548\u53C2\u6570 JSON").option("--repeat", "\u65E0\u9650\u5FAA\u73AF\u64AD\u653E\uFF08\u9ED8\u8BA4\u4EC5\u64AD\u653E\u4E00\u8F6E\uFF09").action(async (opts) => {
|
|
5015
|
-
let
|
|
5161
|
+
let apiKey;
|
|
5016
5162
|
try {
|
|
5017
|
-
|
|
5163
|
+
apiKey = requireApiKey();
|
|
5018
5164
|
} catch (e) {
|
|
5019
5165
|
exitError("AUTH_REQUIRED", e.message);
|
|
5020
5166
|
}
|
|
5021
5167
|
const segments = parseAndValidateSegments(opts.segments);
|
|
5022
|
-
const result = await sendLightEffect(
|
|
5168
|
+
const result = await sendLightEffect(apiKey, segments, void 0, opts.repeat);
|
|
5023
5169
|
if (!result.ok) {
|
|
5024
5170
|
exitError("HTTP_ERROR", `\u8BF7\u6C42\u5931\u8D25: ${result.status} ${result.error}`);
|
|
5025
5171
|
}
|
|
@@ -5058,17 +5204,17 @@ function formatMessage(status) {
|
|
|
5058
5204
|
function registerTunnelStatus(ntf, ctx) {
|
|
5059
5205
|
ntf.command("tunnel-status").description("\u68C0\u67E5 Relay Tunnel \u96A7\u9053\u8FDE\u63A5\u72B6\u6001\uFF08\u8BFB\u53D6\u8FD0\u884C\u4E2D\u670D\u52A1\u7684\u72B6\u6001\u6587\u4EF6\uFF0C\u4E0D\u5F71\u54CD\u5DF2\u6709\u8FDE\u63A5\uFF09").action(async () => {
|
|
5060
5206
|
const tunnelUrl = getEnvUrls().relayTunnelUrl;
|
|
5061
|
-
const
|
|
5207
|
+
const apiKey = loadApiKey();
|
|
5062
5208
|
if (!tunnelUrl) {
|
|
5063
5209
|
exitError(
|
|
5064
5210
|
"TUNNEL_NOT_CONFIGURED",
|
|
5065
5211
|
"RELAY_TUNNEL_URL \u672A\u914D\u7F6E\uFF0C\u96A7\u9053\u529F\u80FD\u672A\u542F\u7528\u3002\u8BF7\u5728\u6784\u5EFA\u65F6\u8BBE\u7F6E RELAY_TUNNEL_URL \u73AF\u5883\u53D8\u91CF\u3002"
|
|
5066
5212
|
);
|
|
5067
5213
|
}
|
|
5068
|
-
if (!
|
|
5214
|
+
if (!apiKey) {
|
|
5069
5215
|
exitError(
|
|
5070
5216
|
"TOKEN_MISSING",
|
|
5071
|
-
"
|
|
5217
|
+
"API Key \u672A\u8BBE\u7F6E\uFF0C\u96A7\u9053\u65E0\u6CD5\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-api-key <apiKey>"
|
|
5072
5218
|
);
|
|
5073
5219
|
}
|
|
5074
5220
|
const status = readTunnelStatus(ctx);
|
|
@@ -5343,9 +5489,14 @@ var RelayClient = class {
|
|
|
5343
5489
|
resolve();
|
|
5344
5490
|
}
|
|
5345
5491
|
};
|
|
5346
|
-
const
|
|
5492
|
+
const rawApiKey = this.opts.apiKey.startsWith("Bearer ") ? this.opts.apiKey.slice("Bearer ".length) : this.opts.apiKey;
|
|
5493
|
+
const wsUrl = new URL(this.opts.tunnelUrl);
|
|
5494
|
+
if (!wsUrl.searchParams.get("apiKey")) {
|
|
5495
|
+
wsUrl.searchParams.set("apiKey", rawApiKey);
|
|
5496
|
+
}
|
|
5497
|
+
const ws = new wrapper_default(wsUrl.toString(), {
|
|
5347
5498
|
headers: {
|
|
5348
|
-
|
|
5499
|
+
"X-Api-Key-Id": rawApiKey
|
|
5349
5500
|
}
|
|
5350
5501
|
});
|
|
5351
5502
|
this.ws = ws;
|
|
@@ -6197,10 +6348,10 @@ function createTunnelService(opts) {
|
|
|
6197
6348
|
proxy?.cleanup();
|
|
6198
6349
|
proxy = null;
|
|
6199
6350
|
client = null;
|
|
6200
|
-
const
|
|
6201
|
-
if (!
|
|
6351
|
+
const apiKey = loadApiKey();
|
|
6352
|
+
if (!apiKey) {
|
|
6202
6353
|
opts.logger.warn(
|
|
6203
|
-
"Relay tunnel:
|
|
6354
|
+
"Relay tunnel: apiKey \u672A\u8BBE\u7F6E\uFF0C\u8DF3\u8FC7\u96A7\u9053\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-api-key <apiKey>"
|
|
6204
6355
|
);
|
|
6205
6356
|
return;
|
|
6206
6357
|
}
|
|
@@ -6217,7 +6368,7 @@ function createTunnelService(opts) {
|
|
|
6217
6368
|
try {
|
|
6218
6369
|
client = new RelayClient({
|
|
6219
6370
|
tunnelUrl: opts.tunnelUrl,
|
|
6220
|
-
|
|
6371
|
+
apiKey,
|
|
6221
6372
|
heartbeatSec: opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC,
|
|
6222
6373
|
reconnectBackoffMs: opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS,
|
|
6223
6374
|
statusFilePath,
|
|
@@ -6314,6 +6465,15 @@ function trimToUndefined(value) {
|
|
|
6314
6465
|
const trimmed = value.trim();
|
|
6315
6466
|
return trimmed || void 0;
|
|
6316
6467
|
}
|
|
6468
|
+
function createEmptyIngestResult() {
|
|
6469
|
+
return {
|
|
6470
|
+
received: 0,
|
|
6471
|
+
ingested: 0,
|
|
6472
|
+
dedupedById: 0,
|
|
6473
|
+
dedupedByContent: 0,
|
|
6474
|
+
invalid: 0
|
|
6475
|
+
};
|
|
6476
|
+
}
|
|
6317
6477
|
function resolveLocalGatewayAuth(params) {
|
|
6318
6478
|
const envGatewayToken = trimToUndefined(process.env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined(process.env.CLAWDBOT_GATEWAY_TOKEN);
|
|
6319
6479
|
const envGatewayPassword = trimToUndefined(process.env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined(process.env.CLAWDBOT_GATEWAY_PASSWORD);
|
|
@@ -6412,10 +6572,8 @@ var index_default = {
|
|
|
6412
6572
|
const { items } = params;
|
|
6413
6573
|
if (Array.isArray(items)) {
|
|
6414
6574
|
const filtered = filterNotifications(items);
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
}
|
|
6418
|
-
respond(true, { ingested: filtered.length });
|
|
6575
|
+
const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
|
|
6576
|
+
respond(true, result);
|
|
6419
6577
|
} else {
|
|
6420
6578
|
respond(false, null, {
|
|
6421
6579
|
code: "INVALID_PARAMS",
|
|
@@ -6538,9 +6696,9 @@ var index_default = {
|
|
|
6538
6696
|
}
|
|
6539
6697
|
},
|
|
6540
6698
|
async execute(_toolCallId, params) {
|
|
6541
|
-
let
|
|
6699
|
+
let apiKey;
|
|
6542
6700
|
try {
|
|
6543
|
-
|
|
6701
|
+
apiKey = requireApiKey();
|
|
6544
6702
|
} catch (e) {
|
|
6545
6703
|
return {
|
|
6546
6704
|
ok: false,
|
|
@@ -6548,7 +6706,7 @@ var index_default = {
|
|
|
6548
6706
|
};
|
|
6549
6707
|
}
|
|
6550
6708
|
const { segments, repeat } = params;
|
|
6551
|
-
const result = await sendLightEffect(
|
|
6709
|
+
const result = await sendLightEffect(apiKey, segments, logger, repeat);
|
|
6552
6710
|
if (!result.ok) {
|
|
6553
6711
|
logger.warn(
|
|
6554
6712
|
`Light control HTTP request failed: ${result.status} ${result.error}`
|
|
@@ -6593,11 +6751,9 @@ var index_default = {
|
|
|
6593
6751
|
return;
|
|
6594
6752
|
}
|
|
6595
6753
|
const filtered = filterNotifications(body.notifications);
|
|
6596
|
-
|
|
6597
|
-
await storage.ingest(filtered);
|
|
6598
|
-
}
|
|
6754
|
+
const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
|
|
6599
6755
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6600
|
-
res.end(JSON.stringify({ ok: true,
|
|
6756
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
6601
6757
|
}
|
|
6602
6758
|
});
|
|
6603
6759
|
logger.info("HTTP \u901A\u77E5\u7AEF\u70B9\u5DF2\u6CE8\u518C: POST /notifications");
|