@yoooclaw/phone-notifications 1.6.1 → 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 +155 -35
- 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",
|
|
@@ -4093,9 +4093,11 @@ import {
|
|
|
4093
4093
|
rmSync,
|
|
4094
4094
|
constants
|
|
4095
4095
|
} from "fs";
|
|
4096
|
+
import { createHash } from "crypto";
|
|
4096
4097
|
import { join as join4 } from "path";
|
|
4097
4098
|
var NOTIFICATION_DIR_NAME = "notifications";
|
|
4098
4099
|
var ID_INDEX_DIR_NAME = ".ids";
|
|
4100
|
+
var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
|
|
4099
4101
|
function getStateFallbackNotificationDir(stateDir) {
|
|
4100
4102
|
return join4(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
|
|
4101
4103
|
}
|
|
@@ -4131,56 +4133,75 @@ var NotificationStorage = class {
|
|
|
4131
4133
|
this.logger = logger;
|
|
4132
4134
|
this.dir = dir;
|
|
4133
4135
|
this.idIndexDir = join4(dir, ID_INDEX_DIR_NAME);
|
|
4136
|
+
this.contentKeyIndexDir = join4(dir, CONTENT_KEY_INDEX_DIR_NAME);
|
|
4134
4137
|
this.resolveDisplayName = resolveDisplayName;
|
|
4135
4138
|
}
|
|
4136
4139
|
dir;
|
|
4137
4140
|
idIndexDir;
|
|
4141
|
+
contentKeyIndexDir;
|
|
4138
4142
|
idCache = /* @__PURE__ */ new Map();
|
|
4143
|
+
contentKeyCache = /* @__PURE__ */ new Map();
|
|
4144
|
+
dateWriteChains = /* @__PURE__ */ new Map();
|
|
4139
4145
|
resolveDisplayName;
|
|
4140
4146
|
async init() {
|
|
4141
4147
|
mkdirSync4(this.dir, { recursive: true });
|
|
4142
4148
|
mkdirSync4(this.idIndexDir, { recursive: true });
|
|
4149
|
+
mkdirSync4(this.contentKeyIndexDir, { recursive: true });
|
|
4143
4150
|
}
|
|
4144
4151
|
async ingest(items) {
|
|
4152
|
+
const result = {
|
|
4153
|
+
received: items.length,
|
|
4154
|
+
ingested: 0,
|
|
4155
|
+
dedupedById: 0,
|
|
4156
|
+
dedupedByContent: 0,
|
|
4157
|
+
invalid: 0
|
|
4158
|
+
};
|
|
4145
4159
|
for (const n of items) {
|
|
4146
|
-
await this.writeNotification(n);
|
|
4160
|
+
const outcome = await this.writeNotification(n);
|
|
4161
|
+
result[outcome] += 1;
|
|
4147
4162
|
}
|
|
4148
4163
|
this.prune();
|
|
4164
|
+
return result;
|
|
4149
4165
|
}
|
|
4150
4166
|
async writeNotification(n) {
|
|
4151
4167
|
const ts = new Date(n.timestamp);
|
|
4152
4168
|
if (Number.isNaN(ts.getTime())) {
|
|
4153
4169
|
this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
|
|
4154
|
-
return;
|
|
4170
|
+
return "invalid";
|
|
4155
4171
|
}
|
|
4156
4172
|
const dateKey = this.formatDate(ts);
|
|
4157
4173
|
const filePath = join4(this.dir, `${dateKey}.json`);
|
|
4158
4174
|
const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
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",
|
|
4167
4201
|
title: typeof n.title === "string" ? n.title : "",
|
|
4168
4202
|
content: this.buildContent(n),
|
|
4169
4203
|
timestamp: n.timestamp
|
|
4170
4204
|
};
|
|
4171
|
-
let arr = [];
|
|
4172
|
-
if (existsSync4(filePath)) {
|
|
4173
|
-
try {
|
|
4174
|
-
arr = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
4175
|
-
} catch {
|
|
4176
|
-
arr = [];
|
|
4177
|
-
}
|
|
4178
|
-
}
|
|
4179
|
-
arr.push(entry);
|
|
4180
|
-
writeFileSync4(filePath, JSON.stringify(arr, null, 2), "utf-8");
|
|
4181
|
-
if (normalizedId) {
|
|
4182
|
-
this.recordNotificationId(dateKey, normalizedId);
|
|
4183
|
-
}
|
|
4184
4205
|
}
|
|
4185
4206
|
buildContent(n) {
|
|
4186
4207
|
const body = n.body?.trim();
|
|
@@ -4224,9 +4245,44 @@ var NotificationStorage = class {
|
|
|
4224
4245
|
this.idCache.set(dateKey, ids);
|
|
4225
4246
|
return ids;
|
|
4226
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
|
+
}
|
|
4227
4278
|
hasNotificationId(dateKey, id) {
|
|
4228
4279
|
return this.getIdSet(dateKey).has(id);
|
|
4229
4280
|
}
|
|
4281
|
+
hasNotificationContentKey(dateKey, filePath, entry) {
|
|
4282
|
+
return this.getContentKeySet(dateKey, filePath).has(
|
|
4283
|
+
this.buildNotificationContentKey(entry)
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4230
4286
|
recordNotificationId(dateKey, id) {
|
|
4231
4287
|
const ids = this.getIdSet(dateKey);
|
|
4232
4288
|
if (ids.has(id)) {
|
|
@@ -4236,12 +4292,55 @@ var NotificationStorage = class {
|
|
|
4236
4292
|
`, "utf-8");
|
|
4237
4293
|
ids.add(id);
|
|
4238
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
|
+
}
|
|
4239
4337
|
prune() {
|
|
4240
4338
|
const retentionDays = this.config.retentionDays ?? 30;
|
|
4241
4339
|
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
4242
4340
|
const cutoffDate = this.formatDate(new Date(cutoffMs));
|
|
4243
4341
|
this.pruneDataFiles(cutoffDate);
|
|
4244
4342
|
this.pruneIdIndex(cutoffDate);
|
|
4343
|
+
this.pruneContentKeyIndex(cutoffDate);
|
|
4245
4344
|
}
|
|
4246
4345
|
/** Remove expired .json, legacy .md files, and legacy date directories */
|
|
4247
4346
|
pruneDataFiles(cutoffDate) {
|
|
@@ -4275,8 +4374,24 @@ var NotificationStorage = class {
|
|
|
4275
4374
|
} catch {
|
|
4276
4375
|
}
|
|
4277
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
|
+
}
|
|
4278
4391
|
async close() {
|
|
4279
4392
|
this.idCache.clear();
|
|
4393
|
+
this.contentKeyCache.clear();
|
|
4394
|
+
this.dateWriteChains.clear();
|
|
4280
4395
|
}
|
|
4281
4396
|
};
|
|
4282
4397
|
|
|
@@ -6350,6 +6465,15 @@ function trimToUndefined(value) {
|
|
|
6350
6465
|
const trimmed = value.trim();
|
|
6351
6466
|
return trimmed || void 0;
|
|
6352
6467
|
}
|
|
6468
|
+
function createEmptyIngestResult() {
|
|
6469
|
+
return {
|
|
6470
|
+
received: 0,
|
|
6471
|
+
ingested: 0,
|
|
6472
|
+
dedupedById: 0,
|
|
6473
|
+
dedupedByContent: 0,
|
|
6474
|
+
invalid: 0
|
|
6475
|
+
};
|
|
6476
|
+
}
|
|
6353
6477
|
function resolveLocalGatewayAuth(params) {
|
|
6354
6478
|
const envGatewayToken = trimToUndefined(process.env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined(process.env.CLAWDBOT_GATEWAY_TOKEN);
|
|
6355
6479
|
const envGatewayPassword = trimToUndefined(process.env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined(process.env.CLAWDBOT_GATEWAY_PASSWORD);
|
|
@@ -6448,10 +6572,8 @@ var index_default = {
|
|
|
6448
6572
|
const { items } = params;
|
|
6449
6573
|
if (Array.isArray(items)) {
|
|
6450
6574
|
const filtered = filterNotifications(items);
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
}
|
|
6454
|
-
respond(true, { ingested: filtered.length });
|
|
6575
|
+
const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
|
|
6576
|
+
respond(true, result);
|
|
6455
6577
|
} else {
|
|
6456
6578
|
respond(false, null, {
|
|
6457
6579
|
code: "INVALID_PARAMS",
|
|
@@ -6629,11 +6751,9 @@ var index_default = {
|
|
|
6629
6751
|
return;
|
|
6630
6752
|
}
|
|
6631
6753
|
const filtered = filterNotifications(body.notifications);
|
|
6632
|
-
|
|
6633
|
-
await storage.ingest(filtered);
|
|
6634
|
-
}
|
|
6754
|
+
const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
|
|
6635
6755
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6636
|
-
res.end(JSON.stringify({ ok: true,
|
|
6756
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
6637
6757
|
}
|
|
6638
6758
|
});
|
|
6639
6759
|
logger.info("HTTP \u901A\u77E5\u7AEF\u70B9\u5DF2\u6CE8\u518C: POST /notifications");
|