@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 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",
@@ -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 "development";
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 "development";
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(token, segments, logger, repeat) {
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"}, token=${token ? token.substring(0, 20) + "\u2026" : "EMPTY"}, segments=${JSON.stringify(segments)}`
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
- Authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`
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 loadToken() {
3922
- return readCredentials().token;
3923
+ function loadApiKey() {
3924
+ const creds = readCredentials();
3925
+ return creds.apiKey ?? creds.token;
3923
3926
  }
3924
- function requireToken() {
3925
- const token = loadToken();
3926
- if (!token) {
3927
+ function requireApiKey() {
3928
+ const apiKey = loadApiKey();
3929
+ if (!apiKey) {
3927
3930
  throw new Error(
3928
- "Token \u672A\u8BBE\u7F6E\uFF0C\u8BF7\u5148\u6267\u884C openclaw ntf auth set-token <token>\uFF08\u82E5 ntf \u547D\u4EE4\u51B2\u7A81\uFF0C\u53EF\u4F7F\u7528 openclaw phone-notifications auth set-token <token>\uFF09"
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 token;
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 = "https://openclaw-service-dev.yoootek.com/api/application-config/app-package/config-all";
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 token = loadToken();
4000
- if (!token || !url) return;
4001
- const auth = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
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", Authorization: auth },
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
- writeFileSync3(getCachePath(stateDir), JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
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
- if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
4139
- return;
4140
- }
4141
- const appName = typeof n.app === "string" && n.app ? n.app : "Unknown";
4142
- const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(appName) : appName;
4143
- const entry = {
4144
- appName,
4145
- 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",
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-token <token>").description("\u8BBE\u7F6E\u7528\u6237 Token\uFF08\u6301\u4E45\u5316\u5230\u672C\u5730\u914D\u7F6E\uFF09").action((token) => {
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
- token: token.slice(0, 8) + "\u2026",
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
- if (creds.token) {
4485
+ const apiKey = creds.apiKey ?? creds.token;
4486
+ if (apiKey) {
4342
4487
  output({
4343
4488
  ok: true,
4344
- hasToken: true,
4345
- token: creds.token.slice(0, 8) + "\u2026",
4489
+ hasApiKey: true,
4490
+ apiKey: apiKey.slice(0, 8) + "\u2026",
4346
4491
  storedAt: credentialsPath()
4347
4492
  });
4348
4493
  } else {
4349
- output({ ok: true, hasToken: false });
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 token;
5161
+ let apiKey;
5016
5162
  try {
5017
- token = requireToken();
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(token, segments, void 0, opts.repeat);
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 token = loadToken();
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 (!token) {
5214
+ if (!apiKey) {
5069
5215
  exitError(
5070
5216
  "TOKEN_MISSING",
5071
- "Token \u672A\u8BBE\u7F6E\uFF0C\u96A7\u9053\u65E0\u6CD5\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-token <token>"
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 ws = new wrapper_default(this.opts.tunnelUrl, {
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
- Authorization: this.opts.token.startsWith("Bearer ") ? this.opts.token : `Bearer ${this.opts.token}`
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 token = loadToken();
6201
- if (!token) {
6351
+ const apiKey = loadApiKey();
6352
+ if (!apiKey) {
6202
6353
  opts.logger.warn(
6203
- "Relay tunnel: token \u672A\u8BBE\u7F6E\uFF0C\u8DF3\u8FC7\u96A7\u9053\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-token <token>"
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
- token,
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
- if (filtered.length) {
6416
- await storage.ingest(filtered);
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 token;
6699
+ let apiKey;
6542
6700
  try {
6543
- token = requireToken();
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(token, segments, logger, repeat);
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
- if (filtered.length) {
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, ingested: filtered.length }));
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");