@yoooclaw/phone-notifications 1.5.2 → 1.6.1

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
@@ -3647,8 +3647,8 @@ var require_websocket_server = __commonJS({
3647
3647
  });
3648
3648
 
3649
3649
  // src/index.ts
3650
- import { readFileSync as readFileSync11 } from "fs";
3651
- import { basename, dirname as dirname4 } from "path";
3650
+ import { readFileSync as readFileSync13 } from "fs";
3651
+ import { basename as basename2, dirname as dirname5 } from "path";
3652
3652
 
3653
3653
  // src/light/protocol.ts
3654
3654
  var MAX_LIGHT_SEGMENTS = 12;
@@ -3791,12 +3791,62 @@ function quantizeWindow(value) {
3791
3791
 
3792
3792
  // src/light/sender.ts
3793
3793
  import { randomUUID } from "crypto";
3794
- async function sendLightEffect(token, segments, logger, repeat) {
3795
- const apiUrl = "https://openclaw-service-dev.yoootek.com/api/message/tob/sendMessage";
3794
+
3795
+ // src/env.ts
3796
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3797
+ import { join, dirname } from "path";
3798
+ var ENV_CONFIG = {
3799
+ development: {
3800
+ lightApiUrl: "https://openclaw-service-dev.yoootek.com/api/message/tob/sendMessage",
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"
3803
+ },
3804
+ production: {
3805
+ lightApiUrl: "https://openclaw-service.yoootek.com/api/message/tob/sendMessage",
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"
3808
+ }
3809
+ };
3810
+ var VALID_ENVS = new Set(Object.keys(ENV_CONFIG));
3811
+ function envFilePath() {
3812
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
3813
+ return join(home, ".openclaw", "env.json");
3814
+ }
3815
+ function loadEnvName() {
3816
+ const filePath = envFilePath();
3817
+ if (!existsSync(filePath)) return "production";
3818
+ try {
3819
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
3820
+ if (data.env && VALID_ENVS.has(data.env)) return data.env;
3821
+ } catch {
3822
+ }
3823
+ return "production";
3824
+ }
3825
+ function saveEnvName(env) {
3826
+ if (!VALID_ENVS.has(env)) {
3827
+ throw new Error(`\u65E0\u6548\u7684\u73AF\u5883\u540D\u79F0: ${env}\uFF0C\u53EF\u9009\u503C: ${[...VALID_ENVS].join(", ")}`);
3828
+ }
3829
+ const filePath = envFilePath();
3830
+ mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
3831
+ writeFileSync(filePath, JSON.stringify({ env }, null, 2), {
3832
+ encoding: "utf-8",
3833
+ mode: 384
3834
+ });
3835
+ }
3836
+ function getEnvUrls(env) {
3837
+ return ENV_CONFIG[env ?? loadEnvName()];
3838
+ }
3839
+ function getAvailableEnvs() {
3840
+ return Object.keys(ENV_CONFIG);
3841
+ }
3842
+
3843
+ // src/light/sender.ts
3844
+ async function sendLightEffect(apiKey, segments, logger, repeat) {
3845
+ const apiUrl = getEnvUrls().lightApiUrl;
3796
3846
  const appKey = "7Q617S1G5WD274JI";
3797
3847
  const templateId = "1990771146010017788";
3798
3848
  logger?.info(
3799
- `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)}`
3800
3850
  );
3801
3851
  if (!apiUrl || !appKey || !templateId) {
3802
3852
  return {
@@ -3821,7 +3871,7 @@ async function sendLightEffect(token, segments, logger, repeat) {
3821
3871
  method: "POST",
3822
3872
  headers: {
3823
3873
  "Content-Type": "application/json",
3824
- Authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`
3874
+ "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
3825
3875
  },
3826
3876
  body: JSON.stringify(requestBody)
3827
3877
  });
@@ -3836,27 +3886,222 @@ async function sendLightEffect(token, segments, logger, repeat) {
3836
3886
  return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
3837
3887
  }
3838
3888
 
3889
+ // src/notification/app-name-map.ts
3890
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3891
+ import { join as join3 } from "path";
3892
+
3893
+ // src/auth/credentials.ts
3894
+ import {
3895
+ existsSync as existsSync2,
3896
+ mkdirSync as mkdirSync2,
3897
+ readFileSync as readFileSync2,
3898
+ writeFileSync as writeFileSync2,
3899
+ watch
3900
+ } from "fs";
3901
+ import { join as join2, dirname as dirname2, basename } from "path";
3902
+ function credentialsPath() {
3903
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
3904
+ return join2(home, ".openclaw", "credentials.json");
3905
+ }
3906
+ function readCredentials() {
3907
+ const path2 = credentialsPath();
3908
+ if (!existsSync2(path2)) return {};
3909
+ try {
3910
+ return JSON.parse(readFileSync2(path2, "utf-8"));
3911
+ } catch {
3912
+ return {};
3913
+ }
3914
+ }
3915
+ function writeCredentials(creds) {
3916
+ const path2 = credentialsPath();
3917
+ mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
3918
+ writeFileSync2(path2, JSON.stringify(creds, null, 2), {
3919
+ encoding: "utf-8",
3920
+ mode: 384
3921
+ });
3922
+ }
3923
+ function loadApiKey() {
3924
+ const creds = readCredentials();
3925
+ return creds.apiKey ?? creds.token;
3926
+ }
3927
+ function requireApiKey() {
3928
+ const apiKey = loadApiKey();
3929
+ if (!apiKey) {
3930
+ throw new Error(
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"
3932
+ );
3933
+ }
3934
+ return apiKey;
3935
+ }
3936
+ function watchCredentials(onChange) {
3937
+ const path2 = credentialsPath();
3938
+ const dir = dirname2(path2);
3939
+ const filename = basename(path2);
3940
+ let debounceTimer = null;
3941
+ const delayMs = 200;
3942
+ const listener = (_event, changedName) => {
3943
+ if (!changedName || changedName !== filename && !changedName.endsWith(filename)) return;
3944
+ if (debounceTimer) clearTimeout(debounceTimer);
3945
+ debounceTimer = setTimeout(() => {
3946
+ debounceTimer = null;
3947
+ onChange();
3948
+ }, delayMs);
3949
+ };
3950
+ let watcher = null;
3951
+ try {
3952
+ watcher = watch(dir, { persistent: false }, listener);
3953
+ } catch {
3954
+ }
3955
+ return () => {
3956
+ if (debounceTimer) clearTimeout(debounceTimer);
3957
+ watcher?.close();
3958
+ };
3959
+ }
3960
+
3961
+ // src/notification/app-name-map.ts
3962
+ var PLUGIN_STATE_DIR = "phone-notifications";
3963
+ var CACHE_FILE = "app-name-map.json";
3964
+ var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
3965
+ var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
3966
+ var APP_NAME_MAP_REFRESH_HOURS = 12;
3967
+ function isRecordOfStrings(v) {
3968
+ if (v === null || typeof v !== "object") return false;
3969
+ for (const val of Object.values(v)) if (typeof val !== "string") return false;
3970
+ return true;
3971
+ }
3972
+ function isAppNameMapApiResponse(v) {
3973
+ if (v === null || typeof v !== "object") return false;
3974
+ const o = v;
3975
+ return Array.isArray(o.data) && o.data.every(
3976
+ (item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string"
3977
+ );
3978
+ }
3979
+ function getCachePath(stateDir) {
3980
+ return join3(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
3981
+ }
3982
+ function createAppNameMapProvider(opts) {
3983
+ const { stateDir, logger } = opts;
3984
+ const url = APP_NAME_MAP_URL;
3985
+ const refreshHours = APP_NAME_MAP_REFRESH_HOURS;
3986
+ const map = /* @__PURE__ */ new Map();
3987
+ let refreshTimer = null;
3988
+ let stopWatching = null;
3989
+ let inFlightFetch = null;
3990
+ function loadFromDisk() {
3991
+ const path2 = getCachePath(stateDir);
3992
+ if (!existsSync3(path2)) return;
3993
+ try {
3994
+ const raw = JSON.parse(readFileSync3(path2, "utf-8"));
3995
+ if (!isRecordOfStrings(raw)) return;
3996
+ map.clear();
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}`);
3999
+ } catch {
4000
+ }
4001
+ }
4002
+ async function fetchFromServer() {
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;
4013
+ try {
4014
+ const res = await fetch(url, {
4015
+ method: "POST",
4016
+ headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
4017
+ body: JSON.stringify({
4018
+ platform: ""
4019
+ })
4020
+ });
4021
+ if (!res.ok) {
4022
+ logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
4023
+ return;
4024
+ }
4025
+ const body = await res.json();
4026
+ if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
4027
+ logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
4028
+ return;
4029
+ }
4030
+ map.clear();
4031
+ for (const item of body.data) {
4032
+ if (item.packageName && item.appName) map.set(item.packageName, item.appName);
4033
+ }
4034
+ if (map.size === 0) {
4035
+ logger.warn("[app-name-map] refresh succeeded but got 0 entries");
4036
+ return;
4037
+ }
4038
+ const dir = join3(stateDir, "plugins", PLUGIN_STATE_DIR);
4039
+ mkdirSync3(dir, { recursive: true });
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}`);
4043
+ } catch (e) {
4044
+ const message = e instanceof Error ? e.message : String(e);
4045
+ logger.warn(`[app-name-map] refresh error: ${message}`);
4046
+ }
4047
+ }
4048
+ async function ensureOneFetch() {
4049
+ if (inFlightFetch) return inFlightFetch;
4050
+ inFlightFetch = fetchFromServer().finally(() => {
4051
+ inFlightFetch = null;
4052
+ });
4053
+ return inFlightFetch;
4054
+ }
4055
+ return {
4056
+ async resolveDisplayName(packageName) {
4057
+ if (map.has(packageName)) return map.get(packageName);
4058
+ return packageName;
4059
+ },
4060
+ async start() {
4061
+ loadFromDisk();
4062
+ await fetchFromServer();
4063
+ if (!url) return;
4064
+ if (refreshHours > 0) {
4065
+ const ms = refreshHours * 60 * 60 * 1e3;
4066
+ refreshTimer = setInterval(() => fetchFromServer().catch(() => {
4067
+ }), ms);
4068
+ }
4069
+ stopWatching = watchCredentials(() => fetchFromServer().catch(() => {
4070
+ }));
4071
+ },
4072
+ stop() {
4073
+ stopWatching?.();
4074
+ stopWatching = null;
4075
+ if (refreshTimer) {
4076
+ clearInterval(refreshTimer);
4077
+ refreshTimer = null;
4078
+ }
4079
+ map.clear();
4080
+ }
4081
+ };
4082
+ }
4083
+
3839
4084
  // src/notification/storage.ts
3840
4085
  import {
3841
4086
  accessSync,
3842
- mkdirSync,
4087
+ mkdirSync as mkdirSync4,
3843
4088
  appendFileSync,
3844
4089
  readdirSync,
3845
- readFileSync,
3846
- writeFileSync,
3847
- existsSync,
4090
+ readFileSync as readFileSync4,
4091
+ writeFileSync as writeFileSync4,
4092
+ existsSync as existsSync4,
3848
4093
  rmSync,
3849
4094
  constants
3850
4095
  } from "fs";
3851
- import { join } from "path";
4096
+ import { join as join4 } from "path";
3852
4097
  var NOTIFICATION_DIR_NAME = "notifications";
3853
4098
  var ID_INDEX_DIR_NAME = ".ids";
3854
4099
  function getStateFallbackNotificationDir(stateDir) {
3855
- return join(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
4100
+ return join4(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
3856
4101
  }
3857
4102
  function ensureWritableDirectory(dir) {
3858
4103
  try {
3859
- mkdirSync(dir, { recursive: true });
4104
+ mkdirSync4(dir, { recursive: true });
3860
4105
  accessSync(dir, constants.R_OK | constants.W_OK);
3861
4106
  return true;
3862
4107
  } catch {
@@ -3870,7 +4115,7 @@ function resolveNotificationStorageDir(ctx, logger) {
3870
4115
  return stateNotifDir;
3871
4116
  }
3872
4117
  if (ctx.workspaceDir) {
3873
- const workspaceDir = join(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
4118
+ const workspaceDir = join4(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
3874
4119
  if (ensureWritableDirectory(workspaceDir)) {
3875
4120
  logger.warn(
3876
4121
  `stateDir \u4E0D\u53EF\u7528\uFF0C\u901A\u77E5\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${workspaceDir}`
@@ -3881,53 +4126,58 @@ function resolveNotificationStorageDir(ctx, logger) {
3881
4126
  throw new Error(`\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528: ${stateNotifDir}`);
3882
4127
  }
3883
4128
  var NotificationStorage = class {
3884
- constructor(dir, config, logger) {
4129
+ constructor(dir, config, logger, resolveDisplayName) {
3885
4130
  this.config = config;
3886
4131
  this.logger = logger;
3887
4132
  this.dir = dir;
3888
- this.idIndexDir = join(dir, ID_INDEX_DIR_NAME);
4133
+ this.idIndexDir = join4(dir, ID_INDEX_DIR_NAME);
4134
+ this.resolveDisplayName = resolveDisplayName;
3889
4135
  }
3890
4136
  dir;
3891
4137
  idIndexDir;
3892
4138
  idCache = /* @__PURE__ */ new Map();
4139
+ resolveDisplayName;
3893
4140
  async init() {
3894
- mkdirSync(this.dir, { recursive: true });
3895
- mkdirSync(this.idIndexDir, { recursive: true });
4141
+ mkdirSync4(this.dir, { recursive: true });
4142
+ mkdirSync4(this.idIndexDir, { recursive: true });
3896
4143
  }
3897
4144
  async ingest(items) {
3898
4145
  for (const n of items) {
3899
- this.writeNotification(n);
4146
+ await this.writeNotification(n);
3900
4147
  }
3901
4148
  this.prune();
3902
4149
  }
3903
- writeNotification(n) {
4150
+ async writeNotification(n) {
3904
4151
  const ts = new Date(n.timestamp);
3905
4152
  if (Number.isNaN(ts.getTime())) {
3906
4153
  this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
3907
4154
  return;
3908
4155
  }
3909
4156
  const dateKey = this.formatDate(ts);
3910
- const filePath = join(this.dir, `${dateKey}.json`);
4157
+ const filePath = join4(this.dir, `${dateKey}.json`);
3911
4158
  const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
3912
4159
  if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
3913
4160
  return;
3914
4161
  }
4162
+ const appName = typeof n.app === "string" && n.app ? n.app : "Unknown";
4163
+ const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(appName) : appName;
3915
4164
  const entry = {
3916
- appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
4165
+ appName,
4166
+ appDisplayName,
3917
4167
  title: typeof n.title === "string" ? n.title : "",
3918
4168
  content: this.buildContent(n),
3919
4169
  timestamp: n.timestamp
3920
4170
  };
3921
4171
  let arr = [];
3922
- if (existsSync(filePath)) {
4172
+ if (existsSync4(filePath)) {
3923
4173
  try {
3924
- arr = JSON.parse(readFileSync(filePath, "utf-8"));
4174
+ arr = JSON.parse(readFileSync4(filePath, "utf-8"));
3925
4175
  } catch {
3926
4176
  arr = [];
3927
4177
  }
3928
4178
  }
3929
4179
  arr.push(entry);
3930
- writeFileSync(filePath, JSON.stringify(arr, null, 2), "utf-8");
4180
+ writeFileSync4(filePath, JSON.stringify(arr, null, 2), "utf-8");
3931
4181
  if (normalizedId) {
3932
4182
  this.recordNotificationId(dateKey, normalizedId);
3933
4183
  }
@@ -3953,7 +4203,7 @@ var NotificationStorage = class {
3953
4203
  return `${year}-${month}-${day}`;
3954
4204
  }
3955
4205
  getIdIndexPath(dateKey) {
3956
- return join(this.idIndexDir, `${dateKey}.ids`);
4206
+ return join4(this.idIndexDir, `${dateKey}.ids`);
3957
4207
  }
3958
4208
  getIdSet(dateKey) {
3959
4209
  const cached = this.idCache.get(dateKey);
@@ -3962,8 +4212,8 @@ var NotificationStorage = class {
3962
4212
  }
3963
4213
  const idPath = this.getIdIndexPath(dateKey);
3964
4214
  const ids = /* @__PURE__ */ new Set();
3965
- if (existsSync(idPath)) {
3966
- const lines = readFileSync(idPath, "utf-8").split(/\r?\n/);
4215
+ if (existsSync4(idPath)) {
4216
+ const lines = readFileSync4(idPath, "utf-8").split(/\r?\n/);
3967
4217
  for (const line of lines) {
3968
4218
  const id = line.trim();
3969
4219
  if (id) {
@@ -4002,10 +4252,10 @@ var NotificationStorage = class {
4002
4252
  if (entry.isFile()) {
4003
4253
  const match = dateFilePattern.exec(entry.name);
4004
4254
  if (match && match[1] < cutoffDate) {
4005
- rmSync(join(this.dir, entry.name), { force: true });
4255
+ rmSync(join4(this.dir, entry.name), { force: true });
4006
4256
  }
4007
4257
  } else if (entry.isDirectory() && dateDirPattern.test(entry.name) && entry.name < cutoffDate) {
4008
- rmSync(join(this.dir, entry.name), { recursive: true, force: true });
4258
+ rmSync(join4(this.dir, entry.name), { recursive: true, force: true });
4009
4259
  }
4010
4260
  }
4011
4261
  } catch {
@@ -4018,7 +4268,7 @@ var NotificationStorage = class {
4018
4268
  if (!entry.isFile()) continue;
4019
4269
  const match = /^(\d{4}-\d{2}-\d{2})\.ids$/.exec(entry.name);
4020
4270
  if (match && match[1] < cutoffDate) {
4021
- rmSync(join(this.idIndexDir, entry.name), { force: true });
4271
+ rmSync(join4(this.idIndexDir, entry.name), { force: true });
4022
4272
  this.idCache.delete(match[1]);
4023
4273
  }
4024
4274
  }
@@ -4031,24 +4281,24 @@ var NotificationStorage = class {
4031
4281
  };
4032
4282
 
4033
4283
  // src/cli/auth.ts
4034
- import { existsSync as existsSync4, rmSync as rmSync2 } from "fs";
4284
+ import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
4035
4285
 
4036
4286
  // src/cli/helpers.ts
4037
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
4038
- import { join as join2 } from "path";
4287
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
4288
+ import { join as join5 } from "path";
4039
4289
  function resolveNotificationsDir(ctx) {
4040
4290
  if (ctx.stateDir) {
4041
- const dir = join2(
4291
+ const dir = join5(
4042
4292
  ctx.stateDir,
4043
4293
  "plugins",
4044
4294
  "phone-notifications",
4045
4295
  "notifications"
4046
4296
  );
4047
- if (existsSync2(dir)) return dir;
4297
+ if (existsSync5(dir)) return dir;
4048
4298
  }
4049
4299
  if (ctx.workspaceDir) {
4050
- const dir = join2(ctx.workspaceDir, "notifications");
4051
- if (existsSync2(dir)) return dir;
4300
+ const dir = join5(ctx.workspaceDir, "notifications");
4301
+ if (existsSync5(dir)) return dir;
4052
4302
  }
4053
4303
  return null;
4054
4304
  }
@@ -4063,10 +4313,10 @@ function listDateKeys(dir) {
4063
4313
  return keys.sort().reverse();
4064
4314
  }
4065
4315
  function readDateFile(dir, dateKey) {
4066
- const filePath = join2(dir, `${dateKey}.json`);
4067
- if (!existsSync2(filePath)) return [];
4316
+ const filePath = join5(dir, `${dateKey}.json`);
4317
+ if (!existsSync5(filePath)) return [];
4068
4318
  try {
4069
- return JSON.parse(readFileSync2(filePath, "utf-8"));
4319
+ return JSON.parse(readFileSync5(filePath, "utf-8"));
4070
4320
  } catch {
4071
4321
  return [];
4072
4322
  }
@@ -4096,76 +4346,44 @@ function exitError(code, message) {
4096
4346
  process.exit(1);
4097
4347
  }
4098
4348
 
4099
- // src/auth/credentials.ts
4100
- import {
4101
- existsSync as existsSync3,
4102
- mkdirSync as mkdirSync2,
4103
- readFileSync as readFileSync3,
4104
- writeFileSync as writeFileSync2
4105
- } from "fs";
4106
- import { join as join3, dirname } from "path";
4107
- function credentialsPath() {
4108
- const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
4109
- return join3(home, ".openclaw", "credentials.json");
4110
- }
4111
- function readCredentials() {
4112
- const path2 = credentialsPath();
4113
- if (!existsSync3(path2)) return {};
4114
- try {
4115
- return JSON.parse(readFileSync3(path2, "utf-8"));
4116
- } catch {
4117
- return {};
4118
- }
4119
- }
4120
- function writeCredentials(creds) {
4121
- const path2 = credentialsPath();
4122
- mkdirSync2(dirname(path2), { recursive: true, mode: 448 });
4123
- writeFileSync2(path2, JSON.stringify(creds, null, 2), {
4124
- encoding: "utf-8",
4125
- mode: 384
4126
- });
4127
- }
4128
- function loadToken() {
4129
- return readCredentials().token;
4130
- }
4131
- function requireToken() {
4132
- const token = loadToken();
4133
- if (!token) {
4134
- throw new Error(
4135
- "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"
4136
- );
4137
- }
4138
- return token;
4139
- }
4140
-
4141
4349
  // src/cli/auth.ts
4142
4350
  function registerAuthCli(program) {
4143
4351
  const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
4144
- auth.command("set-token <token>").description("\u8BBE\u7F6E\u7528\u6237 Token\uFF08\u6301\u4E45\u5316\u5230\u672C\u5730\u914D\u7F6E\uFF09").action((token) => {
4145
- writeCredentials({ ...readCredentials(), token });
4352
+ 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) => {
4353
+ writeCredentials({ ...readCredentials(), apiKey, token: void 0 });
4354
+ output({
4355
+ ok: true,
4356
+ apiKey: apiKey.slice(0, 8) + "\u2026",
4357
+ storedAt: credentialsPath()
4358
+ });
4359
+ });
4360
+ auth.command("set-token <token>").description("\uFF08\u517C\u5BB9\uFF09\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u65E7\u547D\u4EE4\u540D\uFF09").action((token) => {
4361
+ writeCredentials({ ...readCredentials(), apiKey: token, token: void 0 });
4146
4362
  output({
4147
4363
  ok: true,
4148
- token: token.slice(0, 8) + "\u2026",
4364
+ apiKey: token.slice(0, 8) + "\u2026",
4149
4365
  storedAt: credentialsPath()
4150
4366
  });
4151
4367
  });
4152
4368
  auth.command("show").description("\u67E5\u770B\u5F53\u524D\u8BA4\u8BC1\u72B6\u6001").action(() => {
4153
4369
  const creds = readCredentials();
4154
- if (creds.token) {
4370
+ const apiKey = creds.apiKey ?? creds.token;
4371
+ if (apiKey) {
4155
4372
  output({
4156
4373
  ok: true,
4157
- hasToken: true,
4158
- token: creds.token.slice(0, 8) + "\u2026",
4374
+ hasApiKey: true,
4375
+ apiKey: apiKey.slice(0, 8) + "\u2026",
4159
4376
  storedAt: credentialsPath()
4160
4377
  });
4161
4378
  } else {
4162
- output({ ok: true, hasToken: false });
4379
+ output({ ok: true, hasApiKey: false });
4163
4380
  }
4164
4381
  });
4165
4382
  auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
4166
4383
  const path2 = credentialsPath();
4167
- if (existsSync4(path2)) {
4384
+ if (existsSync6(path2)) {
4168
4385
  const creds = readCredentials();
4386
+ delete creds.apiKey;
4169
4387
  delete creds.token;
4170
4388
  if (Object.keys(creds).length === 0) {
4171
4389
  rmSync2(path2, { force: true });
@@ -4263,22 +4481,22 @@ function registerNtfStats(ntf, ctx) {
4263
4481
  }
4264
4482
 
4265
4483
  // src/cli/ntf-sync.ts
4266
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4267
- import { join as join4 } from "path";
4484
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4485
+ import { join as join6 } from "path";
4268
4486
  function checkpointPath(dir) {
4269
- return join4(dir, ".checkpoint.json");
4487
+ return join6(dir, ".checkpoint.json");
4270
4488
  }
4271
4489
  function readCheckpoint(dir) {
4272
4490
  const p = checkpointPath(dir);
4273
- if (!existsSync5(p)) return {};
4491
+ if (!existsSync7(p)) return {};
4274
4492
  try {
4275
- return JSON.parse(readFileSync4(p, "utf-8"));
4493
+ return JSON.parse(readFileSync6(p, "utf-8"));
4276
4494
  } catch {
4277
4495
  return {};
4278
4496
  }
4279
4497
  }
4280
4498
  function writeCheckpoint(dir, data) {
4281
- writeFileSync3(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
4499
+ writeFileSync5(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
4282
4500
  }
4283
4501
  function registerNtfSync(ntf, ctx) {
4284
4502
  const sync = ntf.command("sync").description("\u540C\u6B65\u901A\u77E5\u5230\u8BB0\u5FC6\u7CFB\u7EDF");
@@ -4352,30 +4570,30 @@ function registerNtfSync(ntf, ctx) {
4352
4570
 
4353
4571
  // src/cli/ntf-monitor.ts
4354
4572
  import {
4355
- existsSync as existsSync6,
4356
- mkdirSync as mkdirSync3,
4357
- readFileSync as readFileSync5,
4358
- writeFileSync as writeFileSync4,
4573
+ existsSync as existsSync8,
4574
+ mkdirSync as mkdirSync5,
4575
+ readFileSync as readFileSync7,
4576
+ writeFileSync as writeFileSync6,
4359
4577
  rmSync as rmSync3,
4360
4578
  readdirSync as readdirSync3
4361
4579
  } from "fs";
4362
- import { join as join5 } from "path";
4580
+ import { join as join7 } from "path";
4363
4581
  function tasksDir(ctx) {
4364
4582
  const base = ctx.workspaceDir || ctx.stateDir;
4365
4583
  if (!base) throw new Error("workspaceDir and stateDir both unavailable");
4366
- return join5(base, "tasks");
4584
+ return join7(base, "tasks");
4367
4585
  }
4368
4586
  function readMeta(taskDir) {
4369
- const metaPath = join5(taskDir, "meta.json");
4370
- if (!existsSync6(metaPath)) return null;
4587
+ const metaPath = join7(taskDir, "meta.json");
4588
+ if (!existsSync8(metaPath)) return null;
4371
4589
  try {
4372
- return JSON.parse(readFileSync5(metaPath, "utf-8"));
4590
+ return JSON.parse(readFileSync7(metaPath, "utf-8"));
4373
4591
  } catch {
4374
4592
  return null;
4375
4593
  }
4376
4594
  }
4377
4595
  function writeMeta(taskDir, meta) {
4378
- writeFileSync4(join5(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
4596
+ writeFileSync6(join7(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
4379
4597
  }
4380
4598
  function generateFetchPy(name, matchRules) {
4381
4599
  const appName = matchRules.appName || "";
@@ -4452,27 +4670,27 @@ function registerNtfMonitor(ntf, ctx) {
4452
4670
  const monitor = ntf.command("monitor").description("\u901A\u77E5\u76D1\u63A7\u4EFB\u52A1\u7BA1\u7406");
4453
4671
  monitor.command("list").description("\u5217\u51FA\u6240\u6709\u76D1\u63A7\u4EFB\u52A1").action(() => {
4454
4672
  const dir = tasksDir(ctx);
4455
- if (!existsSync6(dir)) {
4673
+ if (!existsSync8(dir)) {
4456
4674
  output({ ok: true, tasks: [] });
4457
4675
  return;
4458
4676
  }
4459
4677
  const tasks = [];
4460
4678
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
4461
4679
  if (!entry.isDirectory()) continue;
4462
- const meta = readMeta(join5(dir, entry.name));
4680
+ const meta = readMeta(join7(dir, entry.name));
4463
4681
  if (meta) tasks.push(meta);
4464
4682
  }
4465
4683
  output({ ok: true, tasks });
4466
4684
  });
4467
4685
  monitor.command("show <name>").description("\u67E5\u770B\u76D1\u63A7\u4EFB\u52A1\u8BE6\u60C5").action((name) => {
4468
- const taskDir = join5(tasksDir(ctx), name);
4686
+ const taskDir = join7(tasksDir(ctx), name);
4469
4687
  const meta = readMeta(taskDir);
4470
4688
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
4471
- const checkpointPath2 = join5(taskDir, "checkpoint.json");
4689
+ const checkpointPath2 = join7(taskDir, "checkpoint.json");
4472
4690
  let checkpoint = {};
4473
- if (existsSync6(checkpointPath2)) {
4691
+ if (existsSync8(checkpointPath2)) {
4474
4692
  try {
4475
- checkpoint = JSON.parse(readFileSync5(checkpointPath2, "utf-8"));
4693
+ checkpoint = JSON.parse(readFileSync7(checkpointPath2, "utf-8"));
4476
4694
  } catch {
4477
4695
  }
4478
4696
  }
@@ -4487,8 +4705,8 @@ function registerNtfMonitor(ntf, ctx) {
4487
4705
  monitor.command("create <name>").description("\u521B\u5EFA\u76D1\u63A7\u4EFB\u52A1").requiredOption("--description <text>", "\u4EFB\u52A1\u63CF\u8FF0").requiredOption("--match-rules <json>", "\u5339\u914D\u89C4\u5219 JSON").requiredOption("--schedule <cron>", "cron \u8868\u8FBE\u5F0F").action(
4488
4706
  (name, opts) => {
4489
4707
  const dir = tasksDir(ctx);
4490
- const taskDir = join5(dir, name);
4491
- if (existsSync6(taskDir)) {
4708
+ const taskDir = join7(dir, name);
4709
+ if (existsSync8(taskDir)) {
4492
4710
  exitError("ALREADY_EXISTS", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u5DF2\u5B58\u5728`);
4493
4711
  }
4494
4712
  let matchRules;
@@ -4500,7 +4718,7 @@ function registerNtfMonitor(ntf, ctx) {
4500
4718
  "match-rules \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON"
4501
4719
  );
4502
4720
  }
4503
- mkdirSync3(taskDir, { recursive: true });
4721
+ mkdirSync5(taskDir, { recursive: true });
4504
4722
  const meta = {
4505
4723
  name,
4506
4724
  description: opts.description,
@@ -4510,13 +4728,13 @@ function registerNtfMonitor(ntf, ctx) {
4510
4728
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4511
4729
  };
4512
4730
  writeMeta(taskDir, meta);
4513
- writeFileSync4(
4514
- join5(taskDir, "fetch.py"),
4731
+ writeFileSync6(
4732
+ join7(taskDir, "fetch.py"),
4515
4733
  generateFetchPy(name, matchRules),
4516
4734
  "utf-8"
4517
4735
  );
4518
- writeFileSync4(
4519
- join5(taskDir, "README.md"),
4736
+ writeFileSync6(
4737
+ join7(taskDir, "README.md"),
4520
4738
  generateReadme(name, opts.description),
4521
4739
  "utf-8"
4522
4740
  );
@@ -4544,8 +4762,8 @@ function registerNtfMonitor(ntf, ctx) {
4544
4762
  }
4545
4763
  );
4546
4764
  monitor.command("delete <name>").description("\u5220\u9664\u76D1\u63A7\u4EFB\u52A1").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").action((name, opts) => {
4547
- const taskDir = join5(tasksDir(ctx), name);
4548
- if (!existsSync6(taskDir)) {
4765
+ const taskDir = join7(tasksDir(ctx), name);
4766
+ if (!existsSync8(taskDir)) {
4549
4767
  exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
4550
4768
  }
4551
4769
  if (!opts.yes) {
@@ -4570,7 +4788,7 @@ function registerNtfMonitor(ntf, ctx) {
4570
4788
  });
4571
4789
  });
4572
4790
  monitor.command("enable <name>").description("\u542F\u7528\u76D1\u63A7\u4EFB\u52A1").action((name) => {
4573
- const taskDir = join5(tasksDir(ctx), name);
4791
+ const taskDir = join7(tasksDir(ctx), name);
4574
4792
  const meta = readMeta(taskDir);
4575
4793
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
4576
4794
  meta.enabled = true;
@@ -4578,7 +4796,7 @@ function registerNtfMonitor(ntf, ctx) {
4578
4796
  output({ ok: true, name, enabled: true });
4579
4797
  });
4580
4798
  monitor.command("disable <name>").description("\u6682\u505C\u76D1\u63A7\u4EFB\u52A1").action((name) => {
4581
- const taskDir = join5(tasksDir(ctx), name);
4799
+ const taskDir = join7(tasksDir(ctx), name);
4582
4800
  const meta = readMeta(taskDir);
4583
4801
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
4584
4802
  meta.enabled = false;
@@ -4588,8 +4806,8 @@ function registerNtfMonitor(ntf, ctx) {
4588
4806
  }
4589
4807
 
4590
4808
  // src/cli/light-rules.ts
4591
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
4592
- import { join as join6 } from "path";
4809
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
4810
+ import { join as join8 } from "path";
4593
4811
 
4594
4812
  // src/light/validators.ts
4595
4813
  var VALID_MODES = ["wave", "breath", "strobe", "steady", "wave_rainbow"];
@@ -4654,15 +4872,15 @@ var LIGHT_RULES_SECTION = "## \u706F\u6548\u89C4\u5219";
4654
4872
  function memoryPath(ctx) {
4655
4873
  const base = ctx.workspaceDir || ctx.stateDir;
4656
4874
  if (!base) throw new Error("workspaceDir and stateDir both unavailable");
4657
- return join6(base, "MEMORY.md");
4875
+ return join8(base, "MEMORY.md");
4658
4876
  }
4659
4877
  function readMemory(ctx) {
4660
4878
  const p = memoryPath(ctx);
4661
- if (!existsSync7(p)) return "";
4662
- return readFileSync6(p, "utf-8");
4879
+ if (!existsSync9(p)) return "";
4880
+ return readFileSync8(p, "utf-8");
4663
4881
  }
4664
4882
  function writeMemory(ctx, content) {
4665
- writeFileSync5(memoryPath(ctx), content, "utf-8");
4883
+ writeFileSync7(memoryPath(ctx), content, "utf-8");
4666
4884
  }
4667
4885
  function parseLightRules(content) {
4668
4886
  const rules = [];
@@ -4825,14 +5043,14 @@ function registerLightRules(program, ctx) {
4825
5043
  // src/cli/light-send.ts
4826
5044
  function registerLightSend(light) {
4827
5045
  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) => {
4828
- let token;
5046
+ let apiKey;
4829
5047
  try {
4830
- token = requireToken();
5048
+ apiKey = requireApiKey();
4831
5049
  } catch (e) {
4832
5050
  exitError("AUTH_REQUIRED", e.message);
4833
5051
  }
4834
5052
  const segments = parseAndValidateSegments(opts.segments);
4835
- const result = await sendLightEffect(token, segments, void 0, opts.repeat);
5053
+ const result = await sendLightEffect(apiKey, segments, void 0, opts.repeat);
4836
5054
  if (!result.ok) {
4837
5055
  exitError("HTTP_ERROR", `\u8BF7\u6C42\u5931\u8D25: ${result.status} ${result.error}`);
4838
5056
  }
@@ -4841,15 +5059,15 @@ function registerLightSend(light) {
4841
5059
  }
4842
5060
 
4843
5061
  // src/cli/tunnel-status.ts
4844
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
4845
- import { join as join7 } from "path";
4846
- var STATUS_REL_PATH = join7("plugins", "phone-notifications", "tunnel-status.json");
5062
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
5063
+ import { join as join9 } from "path";
5064
+ var STATUS_REL_PATH = join9("plugins", "phone-notifications", "tunnel-status.json");
4847
5065
  function readTunnelStatus(ctx) {
4848
5066
  if (!ctx.stateDir) return null;
4849
- const filePath = join7(ctx.stateDir, STATUS_REL_PATH);
4850
- if (!existsSync8(filePath)) return null;
5067
+ const filePath = join9(ctx.stateDir, STATUS_REL_PATH);
5068
+ if (!existsSync10(filePath)) return null;
4851
5069
  try {
4852
- return JSON.parse(readFileSync7(filePath, "utf-8"));
5070
+ return JSON.parse(readFileSync9(filePath, "utf-8"));
4853
5071
  } catch {
4854
5072
  return null;
4855
5073
  }
@@ -4870,18 +5088,18 @@ function formatMessage(status) {
4870
5088
  }
4871
5089
  function registerTunnelStatus(ntf, ctx) {
4872
5090
  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 () => {
4873
- const tunnelUrl = "wss://openclaw-service-dev.yoootek.com/message/messages/ws/plugin";
4874
- const token = loadToken();
5091
+ const tunnelUrl = getEnvUrls().relayTunnelUrl;
5092
+ const apiKey = loadApiKey();
4875
5093
  if (!tunnelUrl) {
4876
5094
  exitError(
4877
5095
  "TUNNEL_NOT_CONFIGURED",
4878
5096
  "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"
4879
5097
  );
4880
5098
  }
4881
- if (!token) {
5099
+ if (!apiKey) {
4882
5100
  exitError(
4883
5101
  "TOKEN_MISSING",
4884
- "Token \u672A\u8BBE\u7F6E\uFF0C\u96A7\u9053\u65E0\u6CD5\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-token <token>"
5102
+ "API Key \u672A\u8BBE\u7F6E\uFF0C\u96A7\u9053\u65E0\u6CD5\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-api-key <apiKey>"
4885
5103
  );
4886
5104
  }
4887
5105
  const status = readTunnelStatus(ctx);
@@ -4917,17 +5135,17 @@ function registerNtfStoragePath(ntf, ctx) {
4917
5135
  }
4918
5136
 
4919
5137
  // src/cli/log-search.ts
4920
- import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
4921
- import { join as join8 } from "path";
5138
+ import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
5139
+ import { join as join10 } from "path";
4922
5140
  function resolveLogsDir(ctx) {
4923
5141
  if (ctx.stateDir) {
4924
- const dir = join8(
5142
+ const dir = join10(
4925
5143
  ctx.stateDir,
4926
5144
  "plugins",
4927
5145
  "phone-notifications",
4928
5146
  "logs"
4929
5147
  );
4930
- if (existsSync9(dir)) return dir;
5148
+ if (existsSync11(dir)) return dir;
4931
5149
  }
4932
5150
  return null;
4933
5151
  }
@@ -4942,9 +5160,9 @@ function listLogDateKeys(dir) {
4942
5160
  return keys.sort().reverse();
4943
5161
  }
4944
5162
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
4945
- const filePath = join8(dir, `${dateKey}.log`);
4946
- if (!existsSync9(filePath)) return;
4947
- const content = readFileSync8(filePath, "utf-8");
5163
+ const filePath = join10(dir, `${dateKey}.log`);
5164
+ if (!existsSync11(filePath)) return;
5165
+ const content = readFileSync10(filePath, "utf-8");
4948
5166
  const lowerKeyword = keyword?.toLowerCase();
4949
5167
  for (const line of content.split("\n")) {
4950
5168
  if (collected.length >= limit) return;
@@ -4975,13 +5193,47 @@ function registerLogSearch(ntf, ctx) {
4975
5193
  );
4976
5194
  }
4977
5195
 
5196
+ // src/cli/env.ts
5197
+ function registerEnvCli(ntf) {
5198
+ const env = ntf.command("env").description("\u73AF\u5883\u7BA1\u7406\uFF08\u5207\u6362 development / production\uFF09");
5199
+ env.command("show").description("\u663E\u793A\u5F53\u524D\u73AF\u5883\u53CA\u5BF9\u5E94\u7684\u63A5\u53E3\u5730\u5740").action(() => {
5200
+ const current = loadEnvName();
5201
+ const urls = getEnvUrls(current);
5202
+ output({
5203
+ ok: true,
5204
+ env: current,
5205
+ lightApiUrl: urls.lightApiUrl,
5206
+ relayTunnelUrl: urls.relayTunnelUrl,
5207
+ availableEnvs: getAvailableEnvs()
5208
+ });
5209
+ });
5210
+ env.command("switch <env>").description("\u5207\u6362\u73AF\u5883\uFF08development / production\uFF09").action((envName) => {
5211
+ const available = getAvailableEnvs();
5212
+ if (!available.includes(envName)) {
5213
+ exitError(
5214
+ "INVALID_ENV",
5215
+ `\u65E0\u6548\u7684\u73AF\u5883\u540D\u79F0: ${envName}\uFF0C\u53EF\u9009\u503C: ${available.join(", ")}`
5216
+ );
5217
+ }
5218
+ saveEnvName(envName);
5219
+ const urls = getEnvUrls(envName);
5220
+ output({
5221
+ ok: true,
5222
+ message: `\u5DF2\u5207\u6362\u5230 ${envName} \u73AF\u5883`,
5223
+ env: envName,
5224
+ lightApiUrl: urls.lightApiUrl,
5225
+ relayTunnelUrl: urls.relayTunnelUrl
5226
+ });
5227
+ });
5228
+ }
5229
+
4978
5230
  // src/version.ts
4979
- import { readFileSync as readFileSync9 } from "fs";
5231
+ import { readFileSync as readFileSync11 } from "fs";
4980
5232
  function readPluginVersion() {
4981
5233
  try {
4982
5234
  const packageJsonUrl = new URL("../package.json", import.meta.url);
4983
5235
  const packageJson = JSON.parse(
4984
- readFileSync9(packageJsonUrl, "utf-8")
5236
+ readFileSync11(packageJsonUrl, "utf-8")
4985
5237
  );
4986
5238
  return packageJson.version ?? "unknown";
4987
5239
  } catch {
@@ -5003,22 +5255,23 @@ function registerAllCli(program, ctx, rootCommandName = "ntf") {
5003
5255
  registerNtfStoragePath(ntf, ctx);
5004
5256
  registerLogSearch(ntf, ctx);
5005
5257
  registerTunnelStatus(ntf, ctx);
5258
+ registerEnvCli(ntf);
5006
5259
  }
5007
5260
 
5008
5261
  // src/tunnel/service.ts
5009
5262
  import {
5010
5263
  closeSync,
5011
- mkdirSync as mkdirSync5,
5264
+ mkdirSync as mkdirSync7,
5012
5265
  openSync,
5013
- readFileSync as readFileSync10,
5266
+ readFileSync as readFileSync12,
5014
5267
  unlinkSync,
5015
- writeFileSync as writeFileSync7
5268
+ writeFileSync as writeFileSync9
5016
5269
  } from "fs";
5017
- import { dirname as dirname3, join as join9 } from "path";
5270
+ import { dirname as dirname4, join as join11 } from "path";
5018
5271
 
5019
5272
  // src/tunnel/relay-client.ts
5020
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4 } from "fs";
5021
- import { dirname as dirname2 } from "path";
5273
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
5274
+ import { dirname as dirname3 } from "path";
5022
5275
 
5023
5276
  // node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
5024
5277
  var import_stream = __toESM(require_stream(), 1);
@@ -5050,8 +5303,8 @@ var RelayClient = class {
5050
5303
  lastDisconnectReason
5051
5304
  };
5052
5305
  try {
5053
- mkdirSync4(dirname2(this.opts.statusFilePath), { recursive: true });
5054
- writeFileSync6(this.opts.statusFilePath, JSON.stringify(info, null, 2));
5306
+ mkdirSync6(dirname3(this.opts.statusFilePath), { recursive: true });
5307
+ writeFileSync8(this.opts.statusFilePath, JSON.stringify(info, null, 2));
5055
5308
  } catch {
5056
5309
  }
5057
5310
  }
@@ -5121,9 +5374,14 @@ var RelayClient = class {
5121
5374
  resolve();
5122
5375
  }
5123
5376
  };
5124
- const ws = new wrapper_default(this.opts.tunnelUrl, {
5377
+ const rawApiKey = this.opts.apiKey.startsWith("Bearer ") ? this.opts.apiKey.slice("Bearer ".length) : this.opts.apiKey;
5378
+ const wsUrl = new URL(this.opts.tunnelUrl);
5379
+ if (!wsUrl.searchParams.get("apiKey")) {
5380
+ wsUrl.searchParams.set("apiKey", rawApiKey);
5381
+ }
5382
+ const ws = new wrapper_default(wsUrl.toString(), {
5125
5383
  headers: {
5126
- Authorization: this.opts.token.startsWith("Bearer ") ? this.opts.token : `Bearer ${this.opts.token}`
5384
+ "X-Api-Key-Id": rawApiKey
5127
5385
  }
5128
5386
  });
5129
5387
  this.ws = ws;
@@ -5897,7 +6155,7 @@ function createTunnelService(opts) {
5897
6155
  }
5898
6156
  function readLockOwner(filePath) {
5899
6157
  try {
5900
- const parsed = JSON.parse(readFileSync10(filePath, "utf-8"));
6158
+ const parsed = JSON.parse(readFileSync12(filePath, "utf-8"));
5901
6159
  return typeof parsed.pid === "number" ? parsed.pid : null;
5902
6160
  } catch {
5903
6161
  return null;
@@ -5922,11 +6180,11 @@ function createTunnelService(opts) {
5922
6180
  }
5923
6181
  }
5924
6182
  function acquireLock(filePath) {
5925
- mkdirSync5(dirname3(filePath), { recursive: true });
6183
+ mkdirSync7(dirname4(filePath), { recursive: true });
5926
6184
  for (let attempt = 0; attempt < 2; attempt++) {
5927
6185
  try {
5928
6186
  const fd = openSync(filePath, "wx", 384);
5929
- writeFileSync7(
6187
+ writeFileSync9(
5930
6188
  fd,
5931
6189
  JSON.stringify({
5932
6190
  pid: process.pid,
@@ -5975,27 +6233,27 @@ function createTunnelService(opts) {
5975
6233
  proxy?.cleanup();
5976
6234
  proxy = null;
5977
6235
  client = null;
5978
- const token = loadToken();
5979
- if (!token) {
6236
+ const apiKey = loadApiKey();
6237
+ if (!apiKey) {
5980
6238
  opts.logger.warn(
5981
- "Relay tunnel: token \u672A\u8BBE\u7F6E\uFF0C\u8DF3\u8FC7\u96A7\u9053\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-token <token>"
6239
+ "Relay tunnel: apiKey \u672A\u8BBE\u7F6E\uFF0C\u8DF3\u8FC7\u96A7\u9053\u8FDE\u63A5\u3002\u8BF7\u6267\u884C openclaw ntf auth set-api-key <apiKey>"
5982
6240
  );
5983
6241
  return;
5984
6242
  }
5985
6243
  const { logger } = opts;
5986
- const baseStateDir = join9(ctx.stateDir, "plugins", "phone-notifications");
6244
+ const baseStateDir = join11(ctx.stateDir, "plugins", "phone-notifications");
5987
6245
  logger.info(
5988
6246
  `Relay tunnel: starting (pid=${process.pid}, url=${opts.tunnelUrl}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`
5989
6247
  );
5990
- const statusFilePath = join9(baseStateDir, "tunnel-status.json");
5991
- const lockPath = join9(baseStateDir, "relay-tunnel.lock");
6248
+ const statusFilePath = join11(baseStateDir, "tunnel-status.json");
6249
+ const lockPath = join11(baseStateDir, "relay-tunnel.lock");
5992
6250
  if (!acquireLock(lockPath)) {
5993
6251
  return;
5994
6252
  }
5995
6253
  try {
5996
6254
  client = new RelayClient({
5997
6255
  tunnelUrl: opts.tunnelUrl,
5998
- token,
6256
+ apiKey,
5999
6257
  heartbeatSec: opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC,
6000
6258
  reconnectBackoffMs: opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS,
6001
6259
  statusFilePath,
@@ -6036,13 +6294,13 @@ function createTunnelService(opts) {
6036
6294
  }
6037
6295
 
6038
6296
  // src/logger.ts
6039
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "fs";
6040
- import { join as join10 } from "path";
6297
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8 } from "fs";
6298
+ import { join as join12 } from "path";
6041
6299
  var PluginFileLogger = class {
6042
6300
  constructor(upstream, stateDir) {
6043
6301
  this.upstream = upstream;
6044
- this.logsDir = join10(stateDir, "plugins", "phone-notifications", "logs");
6045
- mkdirSync6(this.logsDir, { recursive: true });
6302
+ this.logsDir = join12(stateDir, "plugins", "phone-notifications", "logs");
6303
+ mkdirSync8(this.logsDir, { recursive: true });
6046
6304
  }
6047
6305
  logsDir;
6048
6306
  info(msg) {
@@ -6064,7 +6322,7 @@ var PluginFileLogger = class {
6064
6322
  const line = `${time} [${level}] ${msg}
6065
6323
  `;
6066
6324
  try {
6067
- appendFileSync2(join10(this.logsDir, `${dateKey}.log`), line);
6325
+ appendFileSync2(join12(this.logsDir, `${dateKey}.log`), line);
6068
6326
  } catch {
6069
6327
  }
6070
6328
  }
@@ -6101,7 +6359,7 @@ function resolveLocalGatewayAuth(params) {
6101
6359
  const configPath = params.stateDir ? `${params.stateDir}/openclaw.json` : void 0;
6102
6360
  if (configPath) {
6103
6361
  try {
6104
- const configData = JSON.parse(readFileSync11(configPath, "utf-8"));
6362
+ const configData = JSON.parse(readFileSync13(configPath, "utf-8"));
6105
6363
  const rawGatewayAuthMode = trimToUndefined(configData?.gateway?.auth?.mode);
6106
6364
  if (rawGatewayAuthMode === "token" || rawGatewayAuthMode === "password") {
6107
6365
  configGatewayAuthMode = rawGatewayAuthMode;
@@ -6128,6 +6386,7 @@ var index_default = {
6128
6386
  register(api) {
6129
6387
  const config = api.pluginConfig ?? {};
6130
6388
  let storage = null;
6389
+ let appNameMapProvider = null;
6131
6390
  const ignoredApps = new Set(config.ignoredApps ?? []);
6132
6391
  const openclawDir = api.runtime.state.resolveStateDir();
6133
6392
  const logger = openclawDir ? new PluginFileLogger(api.logger, openclawDir) : api.logger;
@@ -6138,17 +6397,25 @@ var index_default = {
6138
6397
  id: "notification-storage",
6139
6398
  async start(ctx) {
6140
6399
  const storageDir = resolveNotificationStorageDir(ctx, logger);
6141
- storage = new NotificationStorage(storageDir, config, logger);
6400
+ appNameMapProvider = createAppNameMapProvider({
6401
+ stateDir: ctx.stateDir,
6402
+ logger
6403
+ });
6404
+ await appNameMapProvider.start();
6405
+ const resolveDisplayName = appNameMapProvider.resolveDisplayName.bind(appNameMapProvider);
6406
+ storage = new NotificationStorage(storageDir, config, logger, resolveDisplayName);
6142
6407
  await storage.init();
6143
6408
  logger.info(`\u901A\u77E5\u5B58\u50A8\u670D\u52A1\u5DF2\u542F\u52A8: ${storageDir}`);
6144
6409
  },
6145
6410
  async stop() {
6146
6411
  await storage?.close();
6147
6412
  storage = null;
6413
+ appNameMapProvider?.stop();
6414
+ appNameMapProvider = null;
6148
6415
  logger.info("\u901A\u77E5\u5B58\u50A8\u670D\u52A1\u5DF2\u505C\u6B62");
6149
6416
  }
6150
6417
  });
6151
- const tunnelUrl = "wss://openclaw-service-dev.yoootek.com/message/messages/ws/plugin";
6418
+ const tunnelUrl = getEnvUrls().relayTunnelUrl;
6152
6419
  if (tunnelUrl) {
6153
6420
  const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT || "18789";
6154
6421
  const { gatewayAuthMode, gatewayToken, gatewayPassword } = resolveLocalGatewayAuth({
@@ -6307,9 +6574,9 @@ var index_default = {
6307
6574
  }
6308
6575
  },
6309
6576
  async execute(_toolCallId, params) {
6310
- let token;
6577
+ let apiKey;
6311
6578
  try {
6312
- token = requireToken();
6579
+ apiKey = requireApiKey();
6313
6580
  } catch (e) {
6314
6581
  return {
6315
6582
  ok: false,
@@ -6317,7 +6584,7 @@ var index_default = {
6317
6584
  };
6318
6585
  }
6319
6586
  const { segments, repeat } = params;
6320
- const result = await sendLightEffect(token, segments, logger, repeat);
6587
+ const result = await sendLightEffect(apiKey, segments, logger, repeat);
6321
6588
  if (!result.ok) {
6322
6589
  logger.warn(
6323
6590
  `Light control HTTP request failed: ${result.status} ${result.error}`
@@ -6373,8 +6640,8 @@ var index_default = {
6373
6640
  const inferOpenClawRootDir = (workspaceDir) => {
6374
6641
  if (openclawDir) return openclawDir;
6375
6642
  if (!workspaceDir) return void 0;
6376
- if (basename(workspaceDir) !== "workspace") return void 0;
6377
- return dirname4(workspaceDir);
6643
+ if (basename2(workspaceDir) !== "workspace") return void 0;
6644
+ return dirname5(workspaceDir);
6378
6645
  };
6379
6646
  api.registerCli(
6380
6647
  (ctx) => {