@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 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 = createHash("sha1").update(key + GUID).digest("base64");
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 = createHash("sha1").update(key + GUID).digest("base64");
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
- if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
4160
- return;
4161
- }
4162
- const appName = typeof n.app === "string" && n.app ? n.app : "Unknown";
4163
- const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(appName) : appName;
4164
- const entry = {
4165
- appName,
4166
- appDisplayName,
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
- if (filtered.length) {
6452
- await storage.ingest(filtered);
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
- if (filtered.length) {
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, ingested: filtered.length }));
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");