@yoooclaw/phone-notifications 1.12.3 → 1.12.4

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.
Files changed (58) hide show
  1. package/README.md +3 -4
  2. package/dist/bin/ntf.cjs +327 -9
  3. package/dist/bin/ntf.cjs.map +9 -7
  4. package/dist/cli/doctor/check-local-gateway.d.ts +3 -0
  5. package/dist/cli/doctor/check-local-gateway.d.ts.map +1 -0
  6. package/dist/cli/doctor/checkers.d.ts.map +1 -1
  7. package/dist/env.d.ts +8 -0
  8. package/dist/env.d.ts.map +1 -1
  9. package/dist/fs-atomic.d.ts +11 -0
  10. package/dist/fs-atomic.d.ts.map +1 -0
  11. package/dist/image/store.d.ts.map +1 -1
  12. package/dist/index.cjs +2084 -740
  13. package/dist/index.cjs.map +34 -24
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/light/sender.d.ts +14 -3
  16. package/dist/light/sender.d.ts.map +1 -1
  17. package/dist/logger.d.ts.map +1 -1
  18. package/dist/notification/storage.d.ts +4 -3
  19. package/dist/notification/storage.d.ts.map +1 -1
  20. package/dist/plugin/auto-update.d.ts +2 -0
  21. package/dist/plugin/auto-update.d.ts.map +1 -1
  22. package/dist/plugin/lifecycle.d.ts +7 -14
  23. package/dist/plugin/lifecycle.d.ts.map +1 -1
  24. package/dist/plugin/light-control.d.ts.map +1 -1
  25. package/dist/plugin/light-rules-tools.d.ts.map +1 -1
  26. package/dist/plugin/notifications.d.ts +1 -0
  27. package/dist/plugin/notifications.d.ts.map +1 -1
  28. package/dist/plugin/register-guard.d.ts +16 -0
  29. package/dist/plugin/register-guard.d.ts.map +1 -0
  30. package/dist/plugin/runtime.d.ts +64 -0
  31. package/dist/plugin/runtime.d.ts.map +1 -0
  32. package/dist/plugin/shared-storage.d.ts +31 -0
  33. package/dist/plugin/shared-storage.d.ts.map +1 -0
  34. package/dist/plugin/system.d.ts +17 -0
  35. package/dist/plugin/system.d.ts.map +1 -0
  36. package/dist/recording/storage.d.ts +2 -1
  37. package/dist/recording/storage.d.ts.map +1 -1
  38. package/dist/system/os-info.d.ts +34 -0
  39. package/dist/system/os-info.d.ts.map +1 -0
  40. package/dist/tunnel/connect-status-reporter.d.ts.map +1 -1
  41. package/dist/tunnel/frame-slimmer.d.ts +82 -1
  42. package/dist/tunnel/frame-slimmer.d.ts.map +1 -1
  43. package/dist/tunnel/gateway-target.d.ts +17 -0
  44. package/dist/tunnel/gateway-target.d.ts.map +1 -0
  45. package/dist/tunnel/http-proxy.d.ts.map +1 -1
  46. package/dist/tunnel/proxy.d.ts +20 -1
  47. package/dist/tunnel/proxy.d.ts.map +1 -1
  48. package/dist/tunnel/service.d.ts.map +1 -1
  49. package/dist/update/executor.d.ts +3 -3
  50. package/dist/update/executor.d.ts.map +1 -1
  51. package/dist/update/index.d.ts +3 -2
  52. package/dist/update/index.d.ts.map +1 -1
  53. package/dist/update/stale-runtime.d.ts +43 -0
  54. package/dist/update/stale-runtime.d.ts.map +1 -0
  55. package/openclaw.plugin.json +1 -0
  56. package/package.json +1 -1
  57. package/skills/notification-monitor/SKILL.md +2 -2
  58. package/skills/notification-to-memory/SKILL.md +2 -2
package/dist/index.cjs CHANGED
@@ -1719,7 +1719,7 @@ var require_websocket = __commonJS((exports2, module2) => {
1719
1719
  var EventEmitter = require("events");
1720
1720
  var https = require("https");
1721
1721
  var http = require("http");
1722
- var net = require("net");
1722
+ var net2 = require("net");
1723
1723
  var tls = require("tls");
1724
1724
  var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
1725
1725
  var { Duplex, Readable: Readable2 } = require("stream");
@@ -2339,12 +2339,12 @@ var require_websocket = __commonJS((exports2, module2) => {
2339
2339
  }
2340
2340
  function netConnect(options) {
2341
2341
  options.path = options.socketPath;
2342
- return net.connect(options);
2342
+ return net2.connect(options);
2343
2343
  }
2344
2344
  function tlsConnect(options) {
2345
2345
  options.path = undefined;
2346
2346
  if (!options.servername && options.servername !== "") {
2347
- options.servername = net.isIP(options.host) ? "" : options.host;
2347
+ options.servername = net2.isIP(options.host) ? "" : options.host;
2348
2348
  }
2349
2349
  return tls.connect(options);
2350
2350
  }
@@ -2959,7 +2959,7 @@ var import_node_path = require("node:path");
2959
2959
  var import_node_fs = require("node:fs");
2960
2960
  function readBuildInjectedVersion() {
2961
2961
  if (false) {}
2962
- const version = "1.12.3".trim();
2962
+ const version = "1.12.4".trim();
2963
2963
  return version || undefined;
2964
2964
  }
2965
2965
  function readPluginVersionFromPackageJson() {
@@ -3086,7 +3086,6 @@ class PluginFileLogger {
3086
3086
  this.lastPruneDateKey = formatDate(now);
3087
3087
  }
3088
3088
  info(msg) {
3089
- this.prepareLogDate(new Date);
3090
3089
  const safeMsg = this.formatLogMessage(msg);
3091
3090
  this.upstream.info(safeMsg);
3092
3091
  this.append("INFO", safeMsg);
@@ -3137,20 +3136,48 @@ class PluginFileLogger {
3137
3136
  } catch {}
3138
3137
  }
3139
3138
  }
3139
+ function quotedObjectFieldRegex(keys) {
3140
+ return new RegExp(`(["'])(${buildKeyPattern(keys)})\\1\\s*:\\s*(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,}\\]]+)`, "gi");
3141
+ }
3142
+ function keyValueFieldRegex(keys) {
3143
+ return new RegExp(`(?<![?&])\\b(${buildKeyPattern(keys)})\\s*=\\s*(?:Bearer\\s+[^,\\s)]+|"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,\\s)]+)`, "gi");
3144
+ }
3145
+ function structuredKeyValueFieldRegex(keys) {
3146
+ return new RegExp(`\\b(${buildKeyPattern(keys)})\\s*=\\s*([\\[{])`, "gi");
3147
+ }
3148
+ function colonTextFieldRegex(keys) {
3149
+ return new RegExp(`\\b(${buildKeyPattern(keys)})\\s*:\\s*([^,\\n]+)`, "gi");
3150
+ }
3151
+ function buildRules(build) {
3152
+ return [
3153
+ { regex: build(SECRET_KEYS), placeholder: REDACTED_SECRET },
3154
+ { regex: build(USER_TEXT_KEYS), placeholder: REDACTED },
3155
+ { regex: build(USER_URL_KEYS), placeholder: REDACTED_URL }
3156
+ ];
3157
+ }
3158
+ var QUOTED_OBJECT_FIELD_RULES = buildRules(quotedObjectFieldRegex);
3159
+ var KEY_VALUE_FIELD_RULES = buildRules(keyValueFieldRegex);
3160
+ var STRUCTURED_KEY_VALUE_FIELD_RULES = buildRules(structuredKeyValueFieldRegex);
3161
+ var COLON_TEXT_FIELD_RULES = [
3162
+ { regex: colonTextFieldRegex(SECRET_KEYS), placeholder: REDACTED_SECRET },
3163
+ { regex: colonTextFieldRegex(USER_TEXT_KEYS), placeholder: REDACTED }
3164
+ ];
3165
+ var SENSITIVE_QUERY_PARAM_REGEX = new RegExp(`([?&](${SENSITIVE_QUERY_KEYS.map(escapeRegExp).join("|")})=)([^&#\\s]+)`, "gi");
3140
3166
  function redactLogMessage(msg) {
3141
3167
  let redacted = String(msg);
3142
- redacted = redactQuotedObjectFields(redacted, SECRET_KEYS, REDACTED_SECRET);
3143
- redacted = redactQuotedObjectFields(redacted, USER_TEXT_KEYS, REDACTED);
3144
- redacted = redactQuotedObjectFields(redacted, USER_URL_KEYS, REDACTED_URL);
3145
- redacted = redactStructuredKeyValueFields(redacted, SECRET_KEYS, REDACTED_SECRET);
3146
- redacted = redactStructuredKeyValueFields(redacted, USER_TEXT_KEYS, REDACTED);
3147
- redacted = redactStructuredKeyValueFields(redacted, USER_URL_KEYS, REDACTED_URL);
3148
- redacted = redactKeyValueFields(redacted, SECRET_KEYS, REDACTED_SECRET);
3149
- redacted = redactKeyValueFields(redacted, USER_TEXT_KEYS, REDACTED);
3150
- redacted = redactKeyValueFields(redacted, USER_URL_KEYS, REDACTED_URL);
3151
- redacted = redactColonTextFields(redacted, SECRET_KEYS, REDACTED_SECRET);
3152
- redacted = redactColonTextFields(redacted, USER_TEXT_KEYS, REDACTED);
3153
- redacted = redactQueryParams(redacted, SENSITIVE_QUERY_KEYS);
3168
+ for (const rule of QUOTED_OBJECT_FIELD_RULES) {
3169
+ redacted = redactQuotedObjectFields(redacted, rule);
3170
+ }
3171
+ for (const rule of STRUCTURED_KEY_VALUE_FIELD_RULES) {
3172
+ redacted = redactStructuredKeyValueFields(redacted, rule);
3173
+ }
3174
+ for (const rule of KEY_VALUE_FIELD_RULES) {
3175
+ redacted = redactKeyValueFields(redacted, rule);
3176
+ }
3177
+ for (const rule of COLON_TEXT_FIELD_RULES) {
3178
+ redacted = redactColonTextFields(redacted, rule);
3179
+ }
3180
+ redacted = redactQueryParams(redacted);
3154
3181
  redacted = redactBearerTokens(redacted);
3155
3182
  redacted = redactLikelyUserUrls(redacted);
3156
3183
  redacted = redactEmails(redacted);
@@ -3159,17 +3186,14 @@ function redactLogMessage(msg) {
3159
3186
  redacted = redactLongHexTokens(redacted);
3160
3187
  return redacted;
3161
3188
  }
3162
- function redactQuotedObjectFields(input, keys, placeholder) {
3163
- const keyPattern = buildKeyPattern(keys);
3164
- return input.replace(new RegExp(`(["'])(${keyPattern})\\1\\s*:\\s*(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,}\\]]+)`, "gi"), (_match, quote, key) => `${quote}${key}${quote}: "${placeholder}"`);
3189
+ function redactQuotedObjectFields(input, { regex, placeholder }) {
3190
+ return input.replace(regex, (_match, quote, key) => `${quote}${key}${quote}: "${placeholder}"`);
3165
3191
  }
3166
- function redactKeyValueFields(input, keys, placeholder) {
3167
- const keyPattern = buildKeyPattern(keys);
3168
- return input.replace(new RegExp(`(?<![?&])\\b(${keyPattern})\\s*=\\s*(?:Bearer\\s+[^,\\s)]+|"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,\\s)]+)`, "gi"), (_match, key) => `${key}=${placeholder}`);
3192
+ function redactKeyValueFields(input, { regex, placeholder }) {
3193
+ return input.replace(regex, (_match, key) => `${key}=${placeholder}`);
3169
3194
  }
3170
- function redactStructuredKeyValueFields(input, keys, placeholder) {
3171
- const keyPattern = buildKeyPattern(keys);
3172
- const regex = new RegExp(`\\b(${keyPattern})\\s*=\\s*([\\[{])`, "gi");
3195
+ function redactStructuredKeyValueFields(input, { regex, placeholder }) {
3196
+ regex.lastIndex = 0;
3173
3197
  let result = "";
3174
3198
  let lastIndex = 0;
3175
3199
  let match;
@@ -3224,13 +3248,11 @@ function findStructuredValueEnd(input, start) {
3224
3248
  }
3225
3249
  return null;
3226
3250
  }
3227
- function redactColonTextFields(input, keys, placeholder) {
3228
- const keyPattern = buildKeyPattern(keys);
3229
- return input.replace(new RegExp(`\\b(${keyPattern})\\s*:\\s*([^,\\n]+)`, "gi"), (_match, key) => `${key}: ${placeholder}`);
3251
+ function redactColonTextFields(input, { regex, placeholder }) {
3252
+ return input.replace(regex, (_match, key) => `${key}: ${placeholder}`);
3230
3253
  }
3231
- function redactQueryParams(input, keys) {
3232
- const keyPattern = buildKeyPattern(keys);
3233
- return input.replace(new RegExp(`([?&](${keyPattern})=)([^&#\\s]+)`, "gi"), (_match, prefix) => `${prefix}${encodeURIComponent(REDACTED)}`);
3254
+ function redactQueryParams(input) {
3255
+ return input.replace(SENSITIVE_QUERY_PARAM_REGEX, (_match, prefix) => `${prefix}${encodeURIComponent(REDACTED)}`);
3234
3256
  }
3235
3257
  function redactBearerTokens(input) {
3236
3258
  return input.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, `$1${REDACTED_SECRET}`);
@@ -4036,9 +4058,28 @@ async function runNotificationSummary(params) {
4036
4058
  }
4037
4059
 
4038
4060
  // src/notification/storage.ts
4039
- var import_node_fs4 = require("node:fs");
4061
+ var import_node_fs5 = require("node:fs");
4040
4062
  var import_node_crypto = require("node:crypto");
4063
+ var import_node_path4 = require("node:path");
4064
+
4065
+ // src/fs-atomic.ts
4066
+ var import_node_fs4 = require("node:fs");
4041
4067
  var import_node_path3 = require("node:path");
4068
+ function writeFileAtomic(path, data) {
4069
+ import_node_fs4.mkdirSync(import_node_path3.dirname(path), { recursive: true });
4070
+ const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
4071
+ import_node_fs4.writeFileSync(tmpPath, data, "utf-8");
4072
+ import_node_fs4.renameSync(tmpPath, path);
4073
+ }
4074
+ function quarantineCorruptFile(path) {
4075
+ const quarantinePath = `${path}.corrupt.${Date.now()}`;
4076
+ try {
4077
+ import_node_fs4.renameSync(path, quarantinePath);
4078
+ return quarantinePath;
4079
+ } catch {
4080
+ return null;
4081
+ }
4082
+ }
4042
4083
 
4043
4084
  // src/notification/feishu-normalize.ts
4044
4085
  function normalizeOptionalText(value) {
@@ -4180,7 +4221,7 @@ var NOTIFICATION_DIR_NAME = "notifications";
4180
4221
  var ID_INDEX_DIR_NAME = ".ids";
4181
4222
  var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
4182
4223
  function getStateFallbackNotificationDir(stateDir) {
4183
- return import_node_path3.join(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
4224
+ return import_node_path4.join(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
4184
4225
  }
4185
4226
  function computeLagMs(timestamp) {
4186
4227
  if (!timestamp)
@@ -4206,8 +4247,8 @@ function buildFallbackContent(n) {
4206
4247
  }
4207
4248
  function ensureWritableDirectory(dir) {
4208
4249
  try {
4209
- import_node_fs4.mkdirSync(dir, { recursive: true });
4210
- import_node_fs4.accessSync(dir, import_node_fs4.constants.R_OK | import_node_fs4.constants.W_OK);
4250
+ import_node_fs5.mkdirSync(dir, { recursive: true });
4251
+ import_node_fs5.accessSync(dir, import_node_fs5.constants.R_OK | import_node_fs5.constants.W_OK);
4211
4252
  return true;
4212
4253
  } catch {
4213
4254
  return false;
@@ -4220,7 +4261,7 @@ function resolveNotificationStorageDir(ctx, logger) {
4220
4261
  return stateNotifDir;
4221
4262
  }
4222
4263
  if (ctx.workspaceDir) {
4223
- const workspaceDir = import_node_path3.join(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
4264
+ const workspaceDir = import_node_path4.join(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
4224
4265
  if (ensureWritableDirectory(workspaceDir)) {
4225
4266
  logger.warn(`stateDir 不可用,通知已回退到 workspace 路径: ${workspaceDir}`);
4226
4267
  return workspaceDir;
@@ -4228,6 +4269,7 @@ function resolveNotificationStorageDir(ctx, logger) {
4228
4269
  }
4229
4270
  throw new Error(`通知存储目录不可用: ${stateNotifDir}`);
4230
4271
  }
4272
+ var DAY_ENTRIES_CACHE_MAX = 4;
4231
4273
 
4232
4274
  class NotificationStorage {
4233
4275
  logger;
@@ -4236,23 +4278,24 @@ class NotificationStorage {
4236
4278
  contentKeyIndexDir;
4237
4279
  idCache = new Map;
4238
4280
  contentKeyCache = new Map;
4281
+ dayEntriesCache = new Map;
4239
4282
  dateWriteChains = new Map;
4240
4283
  resolveDisplayName;
4241
4284
  constructor(dir, logger, resolveDisplayName) {
4242
4285
  this.logger = logger;
4243
4286
  this.dir = dir;
4244
- this.idIndexDir = import_node_path3.join(dir, ID_INDEX_DIR_NAME);
4245
- this.contentKeyIndexDir = import_node_path3.join(dir, CONTENT_KEY_INDEX_DIR_NAME);
4287
+ this.idIndexDir = import_node_path4.join(dir, ID_INDEX_DIR_NAME);
4288
+ this.contentKeyIndexDir = import_node_path4.join(dir, CONTENT_KEY_INDEX_DIR_NAME);
4246
4289
  this.resolveDisplayName = resolveDisplayName;
4247
4290
  }
4248
4291
  getStorageDir() {
4249
4292
  return this.dir;
4250
4293
  }
4251
4294
  async init() {
4252
- import_node_fs4.mkdirSync(this.dir, { recursive: true });
4253
- import_node_fs4.mkdirSync(this.idIndexDir, { recursive: true });
4254
- import_node_fs4.rmSync(this.contentKeyIndexDir, { recursive: true, force: true });
4255
- import_node_fs4.mkdirSync(this.contentKeyIndexDir, { recursive: true });
4295
+ import_node_fs5.mkdirSync(this.dir, { recursive: true });
4296
+ import_node_fs5.mkdirSync(this.idIndexDir, { recursive: true });
4297
+ import_node_fs5.rmSync(this.contentKeyIndexDir, { recursive: true, force: true });
4298
+ import_node_fs5.mkdirSync(this.contentKeyIndexDir, { recursive: true });
4256
4299
  }
4257
4300
  async ingest(items, ingestId) {
4258
4301
  const result = {
@@ -4264,8 +4307,7 @@ class NotificationStorage {
4264
4307
  inserted: []
4265
4308
  };
4266
4309
  const traceTag = ingestId ? `[${ingestId}]` : "";
4267
- for (const n of items) {
4268
- const outcome = await this.writeNotification(n);
4310
+ const recordOutcome = (n, outcome) => {
4269
4311
  const lagMs = computeLagMs(n.timestamp);
4270
4312
  const lagPart = lagMs === null ? "lag=?" : `lag=${lagMs}ms`;
4271
4313
  this.logger.info(`ingest${traceTag}: app=${n.app ?? "Unknown"} id=${n.id ?? "-"} ts=${n.timestamp} ${lagPart} outcome=${outcome.kind}`);
@@ -4284,41 +4326,108 @@ class NotificationStorage {
4284
4326
  result.invalid += 1;
4285
4327
  break;
4286
4328
  }
4329
+ };
4330
+ const groups = new Map;
4331
+ for (const n of items) {
4332
+ const ts = new Date(n.timestamp);
4333
+ if (Number.isNaN(ts.getTime())) {
4334
+ this.logger.warn(`忽略非法 timestamp 的通知: ${n.id}`);
4335
+ recordOutcome(n, { kind: "invalid" });
4336
+ continue;
4337
+ }
4338
+ const dateKey = this.formatDate(ts);
4339
+ const group = groups.get(dateKey);
4340
+ if (group) {
4341
+ group.push(n);
4342
+ } else {
4343
+ groups.set(dateKey, [n]);
4344
+ }
4345
+ }
4346
+ for (const [dateKey, group] of groups) {
4347
+ await this.writeDayNotifications(dateKey, group, recordOutcome);
4287
4348
  }
4288
4349
  return result;
4289
4350
  }
4290
- async writeNotification(n) {
4291
- const ts = new Date(n.timestamp);
4292
- if (Number.isNaN(ts.getTime())) {
4293
- this.logger.warn(`忽略非法 timestamp 的通知: ${n.id}`);
4294
- return { kind: "invalid" };
4295
- }
4296
- const dateKey = this.formatDate(ts);
4297
- const filePath = import_node_path3.join(this.dir, `${dateKey}.json`);
4298
- const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
4299
- const entry = this.buildStoredNotification(n);
4300
- return this.withDateWriteLock(dateKey, async () => {
4301
- if (normalizedId && this.hasNotificationId(dateKey, entry.clientLabel ?? "legacy", normalizedId)) {
4302
- return { kind: "dedupedById" };
4303
- }
4304
- if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
4305
- return { kind: "dedupedByContent" };
4306
- }
4307
- const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
4308
- const storedEntry = {
4309
- ...entry,
4310
- appDisplayName
4311
- };
4312
- const arr = this.readStoredNotifications(filePath);
4313
- arr.push(storedEntry);
4314
- import_node_fs4.writeFileSync(filePath, JSON.stringify(arr, null, 2), "utf-8");
4315
- if (normalizedId) {
4316
- this.recordNotificationId(dateKey, storedEntry.clientLabel ?? "legacy", normalizedId);
4317
- }
4318
- this.recordNotificationContentKey(dateKey, filePath, storedEntry);
4319
- return { kind: "ingested", entry: storedEntry };
4351
+ async writeDayNotifications(dateKey, group, recordOutcome) {
4352
+ const filePath = import_node_path4.join(this.dir, `${dateKey}.json`);
4353
+ await this.withDateWriteLock(dateKey, async () => {
4354
+ const entries = this.getDayEntries(dateKey, filePath);
4355
+ const ids = this.getIdSet(dateKey);
4356
+ const contentKeys = this.getContentKeySet(dateKey, filePath);
4357
+ const pendingIdKeys = [];
4358
+ const pendingContentKeys = [];
4359
+ let insertedCount = 0;
4360
+ const outcomes = [];
4361
+ for (const n of group) {
4362
+ const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
4363
+ const entry = this.buildStoredNotification(n);
4364
+ const clientLabel = entry.clientLabel ?? "legacy";
4365
+ if (normalizedId && this.hasNotificationId(dateKey, clientLabel, normalizedId)) {
4366
+ outcomes.push({ n, outcome: { kind: "dedupedById" } });
4367
+ continue;
4368
+ }
4369
+ if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
4370
+ outcomes.push({ n, outcome: { kind: "dedupedByContent" } });
4371
+ continue;
4372
+ }
4373
+ let appDisplayName = entry.appName;
4374
+ if (this.resolveDisplayName) {
4375
+ try {
4376
+ appDisplayName = await this.resolveDisplayName(entry.appName);
4377
+ } catch {
4378
+ appDisplayName = entry.appName;
4379
+ }
4380
+ }
4381
+ const storedEntry = { ...entry, appDisplayName };
4382
+ entries.push(storedEntry);
4383
+ insertedCount += 1;
4384
+ if (normalizedId) {
4385
+ const idKey = this.buildNotificationIdKey(clientLabel, normalizedId);
4386
+ if (!ids.has(idKey)) {
4387
+ ids.add(idKey);
4388
+ pendingIdKeys.push(idKey);
4389
+ }
4390
+ }
4391
+ const contentKey = this.buildNotificationContentKey(storedEntry);
4392
+ if (!contentKeys.has(contentKey)) {
4393
+ contentKeys.add(contentKey);
4394
+ pendingContentKeys.push(contentKey);
4395
+ }
4396
+ outcomes.push({ n, outcome: { kind: "ingested", entry: storedEntry } });
4397
+ }
4398
+ if (insertedCount > 0) {
4399
+ writeFileAtomic(filePath, JSON.stringify(entries, null, 2));
4400
+ if (pendingIdKeys.length > 0) {
4401
+ import_node_fs5.appendFileSync(this.getIdIndexPath(dateKey), `${pendingIdKeys.join(`
4402
+ `)}
4403
+ `, "utf-8");
4404
+ }
4405
+ if (pendingContentKeys.length > 0) {
4406
+ import_node_fs5.appendFileSync(this.getContentKeyIndexPath(dateKey), `${pendingContentKeys.join(`
4407
+ `)}
4408
+ `, "utf-8");
4409
+ }
4410
+ }
4411
+ for (const { n, outcome } of outcomes) {
4412
+ recordOutcome(n, outcome);
4413
+ }
4320
4414
  });
4321
4415
  }
4416
+ getDayEntries(dateKey, filePath) {
4417
+ const cached = this.dayEntriesCache.get(dateKey);
4418
+ if (cached) {
4419
+ return cached;
4420
+ }
4421
+ const entries = this.readStoredNotifications(filePath);
4422
+ this.dayEntriesCache.set(dateKey, entries);
4423
+ while (this.dayEntriesCache.size > DAY_ENTRIES_CACHE_MAX) {
4424
+ const oldest = this.dayEntriesCache.keys().next().value;
4425
+ if (oldest === undefined)
4426
+ break;
4427
+ this.dayEntriesCache.delete(oldest);
4428
+ }
4429
+ return entries;
4430
+ }
4322
4431
  buildStoredNotification(n) {
4323
4432
  const appName = typeof n.app === "string" && n.app ? n.app : "Unknown";
4324
4433
  const clientLabel = currentClientLabel("default");
@@ -4348,7 +4457,7 @@ class NotificationStorage {
4348
4457
  return `${year}-${month}-${day}`;
4349
4458
  }
4350
4459
  getIdIndexPath(dateKey) {
4351
- return import_node_path3.join(this.idIndexDir, `${dateKey}.ids`);
4460
+ return import_node_path4.join(this.idIndexDir, `${dateKey}.ids`);
4352
4461
  }
4353
4462
  getIdSet(dateKey) {
4354
4463
  const cached = this.idCache.get(dateKey);
@@ -4357,8 +4466,8 @@ class NotificationStorage {
4357
4466
  }
4358
4467
  const idPath = this.getIdIndexPath(dateKey);
4359
4468
  const ids = new Set;
4360
- if (import_node_fs4.existsSync(idPath)) {
4361
- const lines = import_node_fs4.readFileSync(idPath, "utf-8").split(/\r?\n/);
4469
+ if (import_node_fs5.existsSync(idPath)) {
4470
+ const lines = import_node_fs5.readFileSync(idPath, "utf-8").split(/\r?\n/);
4362
4471
  for (const line of lines) {
4363
4472
  const id = line.trim();
4364
4473
  if (id) {
@@ -4383,7 +4492,7 @@ class NotificationStorage {
4383
4492
  return ids.has(this.buildNotificationIdKey(clientLabel, id)) || this.usesLegacyIdKey(clientLabel) && ids.has(id);
4384
4493
  }
4385
4494
  getContentKeyIndexPath(dateKey) {
4386
- return import_node_path3.join(this.contentKeyIndexDir, `${dateKey}.keys`);
4495
+ return import_node_path4.join(this.contentKeyIndexDir, `${dateKey}.keys`);
4387
4496
  }
4388
4497
  getContentKeySet(dateKey, filePath) {
4389
4498
  const cached = this.contentKeyCache.get(dateKey);
@@ -4392,17 +4501,15 @@ class NotificationStorage {
4392
4501
  }
4393
4502
  const keyPath = this.getContentKeyIndexPath(dateKey);
4394
4503
  const keys = new Set;
4395
- if (import_node_fs4.existsSync(filePath)) {
4396
- for (const item of this.readStoredNotifications(filePath)) {
4397
- keys.add(this.buildNotificationContentKey(item));
4398
- }
4504
+ for (const item of this.getDayEntries(dateKey, filePath)) {
4505
+ keys.add(this.buildNotificationContentKey(item));
4399
4506
  }
4400
4507
  if (keys.size > 0) {
4401
- import_node_fs4.writeFileSync(keyPath, `${Array.from(keys).join(`
4508
+ writeFileAtomic(keyPath, `${Array.from(keys).join(`
4402
4509
  `)}
4403
- `, "utf-8");
4404
- } else if (import_node_fs4.existsSync(keyPath)) {
4405
- import_node_fs4.rmSync(keyPath, { force: true });
4510
+ `);
4511
+ } else if (import_node_fs5.existsSync(keyPath)) {
4512
+ import_node_fs5.rmSync(keyPath, { force: true });
4406
4513
  }
4407
4514
  this.contentKeyCache.set(dateKey, keys);
4408
4515
  return keys;
@@ -4411,26 +4518,6 @@ class NotificationStorage {
4411
4518
  const keys = this.getContentKeySet(dateKey, filePath);
4412
4519
  return this.contentKeyLabels(entry.clientLabel).some((clientLabel) => keys.has(this.buildNotificationContentKey({ ...entry, clientLabel })));
4413
4520
  }
4414
- recordNotificationId(dateKey, clientLabel, id) {
4415
- const ids = this.getIdSet(dateKey);
4416
- const key = this.buildNotificationIdKey(clientLabel, id);
4417
- if (ids.has(key)) {
4418
- return;
4419
- }
4420
- import_node_fs4.appendFileSync(this.getIdIndexPath(dateKey), `${key}
4421
- `, "utf-8");
4422
- ids.add(key);
4423
- }
4424
- recordNotificationContentKey(dateKey, filePath, entry) {
4425
- const keys = this.getContentKeySet(dateKey, filePath);
4426
- const key = this.buildNotificationContentKey(entry);
4427
- if (keys.has(key)) {
4428
- return;
4429
- }
4430
- import_node_fs4.appendFileSync(this.getContentKeyIndexPath(dateKey), `${key}
4431
- `, "utf-8");
4432
- keys.add(key);
4433
- }
4434
4521
  buildNotificationContentKey(entry) {
4435
4522
  return import_node_crypto.createHash("sha256").update(entry.clientLabel ?? "legacy").update("\x1F").update(entry.appName).update("\x1F").update(entry.title).update("\x1F").update(entry.content).update("\x1F").update(entry.timestamp).digest("hex");
4436
4523
  }
@@ -4439,13 +4526,15 @@ class NotificationStorage {
4439
4526
  return label === "default" ? ["default", "legacy"] : [label];
4440
4527
  }
4441
4528
  readStoredNotifications(filePath) {
4442
- if (!import_node_fs4.existsSync(filePath)) {
4529
+ if (!import_node_fs5.existsSync(filePath)) {
4443
4530
  return [];
4444
4531
  }
4445
4532
  try {
4446
- const parsed = JSON.parse(import_node_fs4.readFileSync(filePath, "utf-8"));
4533
+ const parsed = JSON.parse(import_node_fs5.readFileSync(filePath, "utf-8"));
4447
4534
  return Array.isArray(parsed) ? parsed : [];
4448
4535
  } catch {
4536
+ const quarantinePath = quarantineCorruptFile(filePath);
4537
+ this.logger.warn(quarantinePath ? `通知文件损坏,已隔离到 ${quarantinePath},当日通知从空文件重新累积` : `通知文件损坏且隔离失败,后续写入可能覆盖原文件: ${filePath}`);
4449
4538
  return [];
4450
4539
  }
4451
4540
  }
@@ -4470,6 +4559,7 @@ class NotificationStorage {
4470
4559
  async close() {
4471
4560
  this.idCache.clear();
4472
4561
  this.contentKeyCache.clear();
4562
+ this.dayEntriesCache.clear();
4473
4563
  this.dateWriteChains.clear();
4474
4564
  }
4475
4565
  }
@@ -4790,31 +4880,31 @@ var LIGHT_RULE_TOOL_NAME_LIST = [
4790
4880
  ];
4791
4881
 
4792
4882
  // src/light-rules/storage.ts
4793
- var import_node_fs5 = require("node:fs");
4794
- var import_node_path4 = require("node:path");
4883
+ var import_node_fs6 = require("node:fs");
4884
+ var import_node_path5 = require("node:path");
4795
4885
  function addUniquePath(paths, path) {
4796
4886
  if (!path)
4797
4887
  return;
4798
- const normalized = import_node_path4.resolve(path);
4888
+ const normalized = import_node_path5.resolve(path);
4799
4889
  if (!paths.includes(normalized)) {
4800
4890
  paths.push(normalized);
4801
4891
  }
4802
4892
  }
4803
4893
  function inferSiblingWorkspaceDir(stateDir) {
4804
- const stateDirName = import_node_path4.basename(stateDir);
4894
+ const stateDirName = import_node_path5.basename(stateDir);
4805
4895
  if (!stateDirName.startsWith("."))
4806
4896
  return;
4807
- const siblingStateDir = import_node_path4.join(import_node_path4.dirname(stateDir), stateDirName.slice(1));
4808
- const siblingWorkspaceDir = import_node_path4.join(siblingStateDir, "workspace");
4809
- return import_node_fs5.existsSync(siblingWorkspaceDir) ? siblingWorkspaceDir : undefined;
4897
+ const siblingStateDir = import_node_path5.join(import_node_path5.dirname(stateDir), stateDirName.slice(1));
4898
+ const siblingWorkspaceDir = import_node_path5.join(siblingStateDir, "workspace");
4899
+ return import_node_fs6.existsSync(siblingWorkspaceDir) ? siblingWorkspaceDir : undefined;
4810
4900
  }
4811
4901
  function resolveBaseDirs(ctx) {
4812
4902
  const dirs = [];
4813
4903
  addUniquePath(dirs, ctx.workspaceDir);
4814
4904
  if (ctx.stateDir) {
4815
4905
  addUniquePath(dirs, inferSiblingWorkspaceDir(ctx.stateDir));
4816
- const inferredWorkspaceDir = import_node_path4.join(ctx.stateDir, "workspace");
4817
- if (import_node_fs5.existsSync(inferredWorkspaceDir)) {
4906
+ const inferredWorkspaceDir = import_node_path5.join(ctx.stateDir, "workspace");
4907
+ if (import_node_fs6.existsSync(inferredWorkspaceDir)) {
4818
4908
  addUniquePath(dirs, inferredWorkspaceDir);
4819
4909
  }
4820
4910
  addUniquePath(dirs, ctx.stateDir);
@@ -4828,10 +4918,10 @@ function resolveWriteBaseDir(ctx) {
4828
4918
  return resolveBaseDirs(ctx)[0];
4829
4919
  }
4830
4920
  function tasksDirs(ctx) {
4831
- return resolveBaseDirs(ctx).map((baseDir) => import_node_path4.join(baseDir, "tasks"));
4921
+ return resolveBaseDirs(ctx).map((baseDir) => import_node_path5.join(baseDir, "tasks"));
4832
4922
  }
4833
4923
  function writeTasksDir(ctx) {
4834
- return import_node_path4.join(resolveWriteBaseDir(ctx), "tasks");
4924
+ return import_node_path5.join(resolveWriteBaseDir(ctx), "tasks");
4835
4925
  }
4836
4926
  function normalizeLightRuleLookupName(name) {
4837
4927
  return name.trim().replace(/\.json$/i, "");
@@ -4842,7 +4932,7 @@ function resolveLightRuleTask(ctx, name) {
4842
4932
  return null;
4843
4933
  const dirs = tasksDirs(ctx);
4844
4934
  for (const dir of dirs) {
4845
- const directTaskDir = import_node_path4.join(dir, normalizedName);
4935
+ const directTaskDir = import_node_path5.join(dir, normalizedName);
4846
4936
  const directMeta = readMeta(directTaskDir);
4847
4937
  if (directMeta) {
4848
4938
  return {
@@ -4852,12 +4942,12 @@ function resolveLightRuleTask(ctx, name) {
4852
4942
  }
4853
4943
  }
4854
4944
  for (const dir of dirs) {
4855
- if (!import_node_fs5.existsSync(dir))
4945
+ if (!import_node_fs6.existsSync(dir))
4856
4946
  continue;
4857
- for (const entry of import_node_fs5.readdirSync(dir, { withFileTypes: true })) {
4947
+ for (const entry of import_node_fs6.readdirSync(dir, { withFileTypes: true })) {
4858
4948
  if (!entry.isDirectory())
4859
4949
  continue;
4860
- const taskDir = import_node_path4.join(dir, entry.name);
4950
+ const taskDir = import_node_path5.join(dir, entry.name);
4861
4951
  const meta = readMeta(taskDir);
4862
4952
  if (meta?.name === normalizedName) {
4863
4953
  return {
@@ -4876,21 +4966,21 @@ function readOptionalString(value) {
4876
4966
  return trimmed || undefined;
4877
4967
  }
4878
4968
  function readMeta(taskDir) {
4879
- const metaPath = import_node_path4.join(taskDir, "meta.json");
4880
- if (!import_node_fs5.existsSync(metaPath))
4969
+ const metaPath = import_node_path5.join(taskDir, "meta.json");
4970
+ if (!import_node_fs6.existsSync(metaPath))
4881
4971
  return null;
4882
4972
  try {
4883
- const raw = JSON.parse(import_node_fs5.readFileSync(metaPath, "utf-8"));
4973
+ const raw = JSON.parse(import_node_fs6.readFileSync(metaPath, "utf-8"));
4884
4974
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
4885
4975
  return null;
4886
4976
  if (raw.type !== "light-rule")
4887
4977
  return null;
4888
4978
  if (!Array.isArray(raw.segments))
4889
4979
  return null;
4890
- const name = readOptionalString(raw.name) ?? import_node_path4.basename(taskDir);
4980
+ const name = readOptionalString(raw.name) ?? import_node_path5.basename(taskDir);
4891
4981
  const title = readOptionalString(raw.title) ?? name;
4892
4982
  const description = readOptionalString(raw.description) ?? name;
4893
- const createdAt = readOptionalString(raw.createdAt) ?? import_node_fs5.statSync(metaPath).birthtime.toISOString();
4983
+ const createdAt = readOptionalString(raw.createdAt) ?? import_node_fs6.statSync(metaPath).birthtime.toISOString();
4894
4984
  const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
4895
4985
  const repeatTimes = normalizeRepeatTimes({
4896
4986
  repeat: raw.repeat,
@@ -4913,18 +5003,18 @@ function readMeta(taskDir) {
4913
5003
  }
4914
5004
  }
4915
5005
  function writeMeta(taskDir, meta) {
4916
- import_node_fs5.writeFileSync(import_node_path4.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
5006
+ import_node_fs6.writeFileSync(import_node_path5.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
4917
5007
  }
4918
5008
  function listLightRules(ctx) {
4919
5009
  const rules = [];
4920
5010
  const seenNames = new Set;
4921
5011
  for (const dir of tasksDirs(ctx)) {
4922
- if (!import_node_fs5.existsSync(dir))
5012
+ if (!import_node_fs6.existsSync(dir))
4923
5013
  continue;
4924
- for (const entry of import_node_fs5.readdirSync(dir, { withFileTypes: true })) {
5014
+ for (const entry of import_node_fs6.readdirSync(dir, { withFileTypes: true })) {
4925
5015
  if (!entry.isDirectory())
4926
5016
  continue;
4927
- const meta = readMeta(import_node_path4.join(dir, entry.name));
5017
+ const meta = readMeta(import_node_path5.join(dir, entry.name));
4928
5018
  if (!meta || seenNames.has(meta.name))
4929
5019
  continue;
4930
5020
  rules.push(meta);
@@ -4939,11 +5029,11 @@ function createLightRule(ctx, params) {
4939
5029
  throw new LightRuleError("ALREADY_EXISTS", `灯效规则 '${params.name}' 已存在`);
4940
5030
  }
4941
5031
  const dir = writeTasksDir(ctx);
4942
- const taskDir = import_node_path4.join(dir, params.name);
4943
- if (import_node_fs5.existsSync(taskDir)) {
5032
+ const taskDir = import_node_path5.join(dir, params.name);
5033
+ if (import_node_fs6.existsSync(taskDir)) {
4944
5034
  throw new LightRuleError("ALREADY_EXISTS", `灯效规则 '${params.name}' 已存在`);
4945
5035
  }
4946
- import_node_fs5.mkdirSync(taskDir, { recursive: true });
5036
+ import_node_fs6.mkdirSync(taskDir, { recursive: true });
4947
5037
  const repeatTimes = normalizeRepeatTimes({
4948
5038
  repeat: params.repeat,
4949
5039
  repeat_times: params.repeat_times
@@ -4999,7 +5089,7 @@ function deleteLightRule(ctx, name) {
4999
5089
  if (!taskDir || !meta) {
5000
5090
  throw new LightRuleError("NOT_FOUND", `灯效规则 '${name}' 不存在`);
5001
5091
  }
5002
- import_node_fs5.rmSync(taskDir, { recursive: true, force: true });
5092
+ import_node_fs6.rmSync(taskDir, { recursive: true, force: true });
5003
5093
  return { name: meta.name };
5004
5094
  }
5005
5095
 
@@ -5435,16 +5525,16 @@ function quantizeWindow(value) {
5435
5525
  }
5436
5526
 
5437
5527
  // src/auth/credentials.ts
5438
- var import_node_fs8 = require("node:fs");
5439
- var import_node_path8 = require("node:path");
5528
+ var import_node_fs9 = require("node:fs");
5529
+ var import_node_path9 = require("node:path");
5440
5530
 
5441
5531
  // src/profile/detect.ts
5442
- var import_node_fs7 = require("node:fs");
5443
- var import_node_path6 = require("node:path");
5532
+ var import_node_fs8 = require("node:fs");
5533
+ var import_node_path7 = require("node:path");
5444
5534
 
5445
5535
  // src/profile/paths.ts
5446
- var import_node_fs6 = require("node:fs");
5447
- var import_node_path5 = require("node:path");
5536
+ var import_node_fs7 = require("node:fs");
5537
+ var import_node_path6 = require("node:path");
5448
5538
  var DEFAULT_JVSCLAW_STATE_DIR = "/home/admin/.openclaw";
5449
5539
  var PHONE_NOTIFICATIONS_STATE_DIR_ENV = "PHONE_NOTIFICATIONS_STATE_DIR";
5450
5540
  var PHONE_NOTIFICATIONS_CONFIG_PATH_ENV = "PHONE_NOTIFICATIONS_CONFIG_PATH";
@@ -5466,24 +5556,24 @@ function expandUserPath(value) {
5466
5556
  return homeDir();
5467
5557
  }
5468
5558
  if (value.startsWith("~/")) {
5469
- return import_node_path5.join(homeDir(), value.slice(2));
5559
+ return import_node_path6.join(homeDir(), value.slice(2));
5470
5560
  }
5471
5561
  return value;
5472
5562
  }
5473
5563
  function candidateMetaPaths() {
5474
5564
  const home = homeDir();
5475
5565
  return [
5476
- import_node_path5.join(home, ".qclaw", "qclaw.json"),
5477
- import_node_path5.join(home, ".qclow", "qclaw.json")
5566
+ import_node_path6.join(home, ".qclaw", "qclaw.json"),
5567
+ import_node_path6.join(home, ".qclow", "qclaw.json")
5478
5568
  ];
5479
5569
  }
5480
5570
  function loadQClawMeta() {
5481
5571
  for (const metaPath of candidateMetaPaths()) {
5482
- if (!import_node_fs6.existsSync(metaPath)) {
5572
+ if (!import_node_fs7.existsSync(metaPath)) {
5483
5573
  continue;
5484
5574
  }
5485
5575
  try {
5486
- const parsed = JSON.parse(import_node_fs6.readFileSync(metaPath, "utf-8"));
5576
+ const parsed = JSON.parse(import_node_fs7.readFileSync(metaPath, "utf-8"));
5487
5577
  return {
5488
5578
  stateDir: expandUserPath(trimToUndefined(parsed?.stateDir)),
5489
5579
  configPath: expandUserPath(trimToUndefined(parsed?.configPath))
@@ -5505,12 +5595,12 @@ function resolveConfigPathFromEnv() {
5505
5595
  return expandUserPath(trimToUndefined(process.env.JVSCLAW_CONFIG_PATH) ?? trimToUndefined(process.env.OPENCLAW_CONFIG_PATH) ?? trimToUndefined(process.env.QCLAW_CONFIG_PATH));
5506
5596
  }
5507
5597
  function hasOpenClawMarkers() {
5508
- const baseDir = import_node_path5.join(homeDir(), ".openclaw");
5598
+ const baseDir = import_node_path6.join(homeDir(), ".openclaw");
5509
5599
  return [
5510
- import_node_path5.join(baseDir, "openclaw.json"),
5511
- import_node_path5.join(baseDir, "credentials.json"),
5512
- import_node_path5.join(baseDir, "extensions")
5513
- ].some((candidate) => import_node_fs6.existsSync(candidate));
5600
+ import_node_path6.join(baseDir, "openclaw.json"),
5601
+ import_node_path6.join(baseDir, "credentials.json"),
5602
+ import_node_path6.join(baseDir, "extensions")
5603
+ ].some((candidate) => import_node_fs7.existsSync(candidate));
5514
5604
  }
5515
5605
  function resolveHostStateDir(stateDir) {
5516
5606
  const explicitDir = expandUserPath(trimToUndefined(stateDir));
@@ -5522,16 +5612,16 @@ function resolveHostStateDir(stateDir) {
5522
5612
  return envDir;
5523
5613
  }
5524
5614
  if (hasOpenClawMarkers()) {
5525
- return import_node_path5.join(homeDir(), ".openclaw");
5615
+ return import_node_path6.join(homeDir(), ".openclaw");
5526
5616
  }
5527
5617
  const meta = loadQClawMeta();
5528
5618
  if (meta?.stateDir) {
5529
5619
  return meta.stateDir;
5530
5620
  }
5531
5621
  if (meta?.configPath) {
5532
- return import_node_path5.dirname(meta.configPath);
5622
+ return import_node_path6.dirname(meta.configPath);
5533
5623
  }
5534
- return import_node_path5.join(homeDir(), ".openclaw");
5624
+ return import_node_path6.join(homeDir(), ".openclaw");
5535
5625
  }
5536
5626
  function resolveHostConfigPath(stateDir = resolveHostStateDir()) {
5537
5627
  const envConfigPath = resolveConfigPathFromEnv();
@@ -5542,7 +5632,7 @@ function resolveHostConfigPath(stateDir = resolveHostStateDir()) {
5542
5632
  if (meta?.configPath && (!meta.stateDir || !stateDir || meta.stateDir === stateDir)) {
5543
5633
  return meta.configPath;
5544
5634
  }
5545
- return import_node_path5.join(stateDir, "openclaw.json");
5635
+ return import_node_path6.join(stateDir, "openclaw.json");
5546
5636
  }
5547
5637
  function resolvePersistedProfileStateDirs() {
5548
5638
  const dirs = [];
@@ -5597,7 +5687,7 @@ function readPersistedClawProfile() {
5597
5687
  for (const stateDir of resolvePersistedProfileStateDirs()) {
5598
5688
  let content;
5599
5689
  try {
5600
- content = import_node_fs7.readFileSync(import_node_path6.join(stateDir, ".env"), "utf-8");
5690
+ content = import_node_fs8.readFileSync(import_node_path7.join(stateDir, ".env"), "utf-8");
5601
5691
  } catch {
5602
5692
  continue;
5603
5693
  }
@@ -5621,7 +5711,7 @@ function readPluginLinkSecret(configPath) {
5621
5711
  return;
5622
5712
  let config;
5623
5713
  try {
5624
- config = JSON.parse(import_node_fs7.readFileSync(configPath, "utf-8"));
5714
+ config = JSON.parse(import_node_fs8.readFileSync(configPath, "utf-8"));
5625
5715
  } catch {
5626
5716
  return;
5627
5717
  }
@@ -5635,7 +5725,7 @@ function hasJvsclawLinkSecret() {
5635
5725
  const paths = [
5636
5726
  resolvePluginConfigPathOverrideFromEnv(),
5637
5727
  resolveConfigPathFromEnv(),
5638
- import_node_path6.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json")
5728
+ import_node_path7.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json")
5639
5729
  ].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
5640
5730
  return paths.some((configPath) => Boolean(readPluginLinkSecret(configPath)));
5641
5731
  }
@@ -5661,7 +5751,7 @@ function resolveClawKind() {
5661
5751
  return "openclaw";
5662
5752
  }
5663
5753
  // src/profile/runtime-paths.ts
5664
- var import_node_path7 = require("node:path");
5754
+ var import_node_path8 = require("node:path");
5665
5755
  function resolveStateDir(hostStateDir) {
5666
5756
  const override = resolvePluginStateDirOverrideFromEnv();
5667
5757
  if (override) {
@@ -5678,12 +5768,12 @@ function resolveConfigPath(hostStateDir) {
5678
5768
  return override;
5679
5769
  }
5680
5770
  if (resolveClawKind() === "jvsclaw") {
5681
- return import_node_path7.join(resolveStateDir(hostStateDir), "openclaw.json");
5771
+ return import_node_path8.join(resolveStateDir(hostStateDir), "openclaw.json");
5682
5772
  }
5683
5773
  return resolveHostConfigPath(resolveHostStateDir(hostStateDir));
5684
5774
  }
5685
5775
  function resolveStateFile(filename, hostStateDir) {
5686
- return import_node_path7.join(resolveStateDir(hostStateDir), filename);
5776
+ return import_node_path8.join(resolveStateDir(hostStateDir), filename);
5687
5777
  }
5688
5778
  // src/auth/credentials.ts
5689
5779
  var API_KEY_ENV = "YOOOCLAW_API_KEY";
@@ -5692,18 +5782,18 @@ function credentialsPath() {
5692
5782
  }
5693
5783
  function readCredentials() {
5694
5784
  const path = credentialsPath();
5695
- if (!import_node_fs8.existsSync(path))
5785
+ if (!import_node_fs9.existsSync(path))
5696
5786
  return {};
5697
5787
  try {
5698
- return JSON.parse(import_node_fs8.readFileSync(path, "utf-8"));
5788
+ return JSON.parse(import_node_fs9.readFileSync(path, "utf-8"));
5699
5789
  } catch {
5700
5790
  return {};
5701
5791
  }
5702
5792
  }
5703
5793
  function writeCredentials(creds) {
5704
5794
  const path = credentialsPath();
5705
- import_node_fs8.mkdirSync(import_node_path8.dirname(path), { recursive: true, mode: 448 });
5706
- import_node_fs8.writeFileSync(path, JSON.stringify(creds, null, 2), {
5795
+ import_node_fs9.mkdirSync(import_node_path9.dirname(path), { recursive: true, mode: 448 });
5796
+ import_node_fs9.writeFileSync(path, JSON.stringify(creds, null, 2), {
5707
5797
  encoding: "utf-8",
5708
5798
  mode: 384
5709
5799
  });
@@ -5751,8 +5841,8 @@ function requireApiKey() {
5751
5841
  }
5752
5842
  function watchCredentials(onChange) {
5753
5843
  const path = credentialsPath();
5754
- const dir = import_node_path8.dirname(path);
5755
- const filename = import_node_path8.basename(path);
5844
+ const dir = import_node_path9.dirname(path);
5845
+ const filename = import_node_path9.basename(path);
5756
5846
  let debounceTimer = null;
5757
5847
  const delayMs = 200;
5758
5848
  const listener = (_event, changedName) => {
@@ -5767,7 +5857,7 @@ function watchCredentials(onChange) {
5767
5857
  };
5768
5858
  let watcher = null;
5769
5859
  try {
5770
- watcher = import_node_fs8.watch(dir, { persistent: false }, listener);
5860
+ watcher = import_node_fs9.watch(dir, { persistent: false }, listener);
5771
5861
  } catch {}
5772
5862
  return () => {
5773
5863
  if (debounceTimer)
@@ -5778,8 +5868,8 @@ function watchCredentials(onChange) {
5778
5868
 
5779
5869
  // src/env.ts
5780
5870
  var import_node_crypto2 = require("node:crypto");
5781
- var import_node_fs9 = require("node:fs");
5782
- var import_node_path9 = require("node:path");
5871
+ var import_node_fs10 = require("node:fs");
5872
+ var import_node_path10 = require("node:path");
5783
5873
  function parseEnvContent(content) {
5784
5874
  const entries = new Map;
5785
5875
  const tokens = content.split(/\r?\n/).flatMap((line) => line.split(/(?<![A-Z0-9_])(?=[A-Z_][A-Z0-9_]*=)/));
@@ -5799,8 +5889,8 @@ function parseEnvContent(content) {
5799
5889
  }
5800
5890
  function writeDotEnv(key, value) {
5801
5891
  const path = resolveStateFile(".env");
5802
- import_node_fs9.mkdirSync(import_node_path9.dirname(path), { recursive: true });
5803
- const existing = import_node_fs9.existsSync(path) ? import_node_fs9.readFileSync(path, "utf-8") : "";
5892
+ import_node_fs10.mkdirSync(import_node_path10.dirname(path), { recursive: true });
5893
+ const existing = import_node_fs10.existsSync(path) ? import_node_fs10.readFileSync(path, "utf-8") : "";
5804
5894
  const entries = parseEnvContent(existing);
5805
5895
  entries.set(key, value);
5806
5896
  const output2 = Array.from(entries, ([k, v]) => `${k}=${v}`).join(`
@@ -5808,11 +5898,11 @@ function writeDotEnv(key, value) {
5808
5898
  `;
5809
5899
  const tmpPath = `${path}.tmp.${process.pid}.${import_node_crypto2.randomBytes(4).toString("hex")}`;
5810
5900
  try {
5811
- import_node_fs9.writeFileSync(tmpPath, output2, { encoding: "utf-8", mode: 384 });
5812
- import_node_fs9.renameSync(tmpPath, path);
5901
+ import_node_fs10.writeFileSync(tmpPath, output2, { encoding: "utf-8", mode: 384 });
5902
+ import_node_fs10.renameSync(tmpPath, path);
5813
5903
  } catch (error) {
5814
5904
  try {
5815
- import_node_fs9.rmSync(tmpPath, { force: true });
5905
+ import_node_fs10.rmSync(tmpPath, { force: true });
5816
5906
  } catch {}
5817
5907
  throw error;
5818
5908
  }
@@ -5823,6 +5913,7 @@ var DEFAULT_ENV_HOSTS = {
5823
5913
  production: "openclaw-service.yoooclaw.com"
5824
5914
  };
5825
5915
  var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH = "/api/plugin/notification-intelligence/light-rules";
5916
+ var NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH = "/api/plugin/notification-intelligence/light-effects/send";
5826
5917
  var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_APP_PATH = "/api/notification-intelligence/light-rules";
5827
5918
  var NOTIFICATION_INTELLIGENCE_PLUGIN_BASE_PATH = "/api/plugin/notification-intelligence";
5828
5919
  var NOTIFICATION_INTELLIGENCE_APP_BASE_PATH = "/api/notification-intelligence";
@@ -5870,13 +5961,29 @@ function normalizeNotificationIntelligenceLightRulesPluginUrl(url) {
5870
5961
  }
5871
5962
  return trimmed;
5872
5963
  }
5964
+ function normalizeNotificationIntelligenceLightEffectsSendUrl(url) {
5965
+ const trimmed = url.trim().replace(/\/+$/, "");
5966
+ if (!trimmed)
5967
+ return trimmed;
5968
+ if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH)) {
5969
+ return trimmed;
5970
+ }
5971
+ const normalizedRulesUrl = normalizeNotificationIntelligenceLightRulesPluginUrl(trimmed);
5972
+ if (normalizedRulesUrl.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH)) {
5973
+ const origin = normalizedRulesUrl.slice(0, -NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH.length);
5974
+ return `${origin}${NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH}`;
5975
+ }
5976
+ return `${trimmed}/light-effects/send`;
5977
+ }
5873
5978
  function buildEnvUrls(host) {
5874
5979
  const https = `https://${host}`;
5875
5980
  const wss = `wss://${host}`;
5876
5981
  const lightRulesUrl = normalizeNotificationIntelligenceLightRulesPluginUrl(process.env.NOTIFICATION_INTELLIGENCE_LIGHT_RULES_URL || `${https}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`);
5982
+ const lightEffectsSendUrl = normalizeNotificationIntelligenceLightEffectsSendUrl(process.env.NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_URL || lightRulesUrl);
5877
5983
  return {
5878
5984
  lightApiUrl: `${https}/api/message/tob/sendMessage`,
5879
5985
  notificationIntelligenceLightRulesPluginUrl: lightRulesUrl,
5986
+ notificationIntelligenceLightEffectsSendUrl: lightEffectsSendUrl,
5880
5987
  relayTunnelUrl: `${wss}/message/messages/ws/plugin`,
5881
5988
  appNameMapUrl: `${https}/api/application-config/app-package/config-all`,
5882
5989
  modelProxyLongRecordingSubmitTaskUrl: `${https}/api/model-proxy/long-recording/submit-task`,
@@ -5892,9 +5999,9 @@ var VALID_ENVS = new Set([
5892
5999
  ]);
5893
6000
  function readDotEnv() {
5894
6001
  const path = resolveStateFile(".env");
5895
- if (!import_node_fs9.existsSync(path))
6002
+ if (!import_node_fs10.existsSync(path))
5896
6003
  return {};
5897
- return Object.fromEntries(parseEnvContent(import_node_fs9.readFileSync(path, "utf-8")));
6004
+ return Object.fromEntries(parseEnvContent(import_node_fs10.readFileSync(path, "utf-8")));
5898
6005
  }
5899
6006
  function readPersistedEnvName() {
5900
6007
  const fromDotEnv = readDotEnv()["PHONE_NOTIFICATIONS_ENV"]?.trim();
@@ -6251,6 +6358,10 @@ function readTrimmedString(value) {
6251
6358
  const trimmed = value.trim();
6252
6359
  return trimmed || undefined;
6253
6360
  }
6361
+ var NOTIFICATION_RULE_SIGNAL = /(?:通知|消息|推送|来电|短信|邮件|微信|飞书|钉钉|notification|message|push|incoming\s+call|sms|e-?mail)/i;
6362
+ function isNotificationTriggeredRuleText(ruleText) {
6363
+ return NOTIFICATION_RULE_SIGNAL.test(ruleText);
6364
+ }
6254
6365
  function hasAnyPlainUpdateField(params) {
6255
6366
  return [
6256
6367
  "title",
@@ -6366,6 +6477,9 @@ function registerLightRulesTools(api, service, logger) {
6366
6477
  const ruleText = readTrimmedString(params.ruleText);
6367
6478
  if (!ruleText)
6368
6479
  return err("INVALID_PARAMS", "ruleText is required");
6480
+ if (!isNotificationTriggeredRuleText(ruleText)) {
6481
+ return err("INVALID_RULE_SCOPE", "持久灯效规则必须包含通知、消息、来电等触发条件;一次性亮灯请调用 light_control");
6482
+ }
6369
6483
  try {
6370
6484
  const result = await service.create({ ruleText });
6371
6485
  logger.info(`${LIGHT_RULE_TOOL_NAMES.create} tool: created ${result.id}/${result.name}`);
@@ -6483,6 +6597,409 @@ function registerLightRulesTools(api, service, logger) {
6483
6597
  });
6484
6598
  }
6485
6599
 
6600
+ // src/light/sender.ts
6601
+ var import_node_crypto3 = require("node:crypto");
6602
+ async function sendLightEffect(apiKey, segments, logger, repeatInput, reason, title, options) {
6603
+ const resolvedTitle = resolveLightTitle(title, reason, segments);
6604
+ let repeatTimes;
6605
+ try {
6606
+ repeatTimes = normalizeRepeatTimes(repeatInput);
6607
+ } catch (error) {
6608
+ return { ok: false, error: error?.message ?? String(error) };
6609
+ }
6610
+ const intelligence = await sendViaNotificationIntelligence(apiKey, segments, repeatTimes, resolvedTitle, reason, options?.bizUniqueId, logger);
6611
+ if (!intelligence.fallback) {
6612
+ return intelligence.result;
6613
+ }
6614
+ logger?.warn(`Light sender: notification-intelligence entry unavailable (${intelligence.fallbackReason}), falling back to legacy message-service`);
6615
+ return sendViaLegacyMessageService(apiKey, segments, logger, repeatInput, reason, resolvedTitle);
6616
+ }
6617
+ async function sendViaNotificationIntelligence(apiKey, segments, repeatTimes, title, reason, bizUniqueId, logger) {
6618
+ const url = getEnvUrls().notificationIntelligenceLightEffectsSendUrl;
6619
+ const requestBody = {
6620
+ title,
6621
+ repeat_times: repeatTimes,
6622
+ segments
6623
+ };
6624
+ if (reason?.trim())
6625
+ requestBody.reason = reason;
6626
+ const trimmedBizUniqueId = bizUniqueId?.trim();
6627
+ if (trimmedBizUniqueId)
6628
+ requestBody.bizUniqueId = trimmedBizUniqueId;
6629
+ logger?.info(`Light sender: POST ${url}, bizUniqueId=${trimmedBizUniqueId ?? "(server-generated)"}, ` + `title=${title}, repeat_times=${repeatTimes}, segments_count=${segments.length}`);
6630
+ let res;
6631
+ try {
6632
+ res = await fetch(url, {
6633
+ method: "POST",
6634
+ headers: {
6635
+ "Content-Type": "application/json",
6636
+ "X-Api-Key-Id": stripBearer(apiKey)
6637
+ },
6638
+ body: JSON.stringify(requestBody)
6639
+ });
6640
+ } catch (error) {
6641
+ return {
6642
+ fallback: true,
6643
+ fallbackReason: `network error: ${error?.message ?? String(error)}`,
6644
+ result: { ok: false, error: error?.message ?? String(error) }
6645
+ };
6646
+ }
6647
+ const resBody = await res.text();
6648
+ if (!res.ok) {
6649
+ if (res.status === 404 || res.status === 405 || res.status === 401 && /jwt is missing/i.test(resBody)) {
6650
+ return {
6651
+ fallback: true,
6652
+ fallbackReason: `HTTP ${res.status}`,
6653
+ result: { ok: false, status: res.status, error: resBody }
6654
+ };
6655
+ }
6656
+ logger?.warn(`Light sender: FAILED ${res.status}, url=${url}, resBody=${resBody.substring(0, 500)}`);
6657
+ return {
6658
+ fallback: false,
6659
+ result: {
6660
+ ok: false,
6661
+ status: res.status,
6662
+ error: resBody,
6663
+ via: "notification-intelligence"
6664
+ }
6665
+ };
6666
+ }
6667
+ let parsed = null;
6668
+ try {
6669
+ parsed = resBody.trim() ? JSON.parse(resBody) : null;
6670
+ } catch {}
6671
+ if (!parsed || typeof parsed !== "object") {
6672
+ logger?.warn(`Light sender: invalid JSON from ${url}, resBody=${resBody.substring(0, 200)}`);
6673
+ return {
6674
+ fallback: false,
6675
+ result: {
6676
+ ok: false,
6677
+ error: "invalid JSON response",
6678
+ via: "notification-intelligence"
6679
+ }
6680
+ };
6681
+ }
6682
+ const data = parsed.data ?? null;
6683
+ if (parsed.code && parsed.code !== "000000") {
6684
+ const message = parsed.msg ?? parsed.message ?? `remote code ${parsed.code}`;
6685
+ logger?.warn(`Light sender: FAILED code=${parsed.code}, msg=${message}`);
6686
+ return {
6687
+ fallback: false,
6688
+ result: {
6689
+ ok: false,
6690
+ error: String(message),
6691
+ response: parsed,
6692
+ via: "notification-intelligence"
6693
+ }
6694
+ };
6695
+ }
6696
+ if (data?.success === false) {
6697
+ const message = data.message ?? "remote business request failed";
6698
+ logger?.warn(`Light sender: FAILED business, message=${message}`);
6699
+ return {
6700
+ fallback: false,
6701
+ result: {
6702
+ ok: false,
6703
+ error: String(message),
6704
+ response: parsed,
6705
+ via: "notification-intelligence"
6706
+ }
6707
+ };
6708
+ }
6709
+ const effectiveBizUniqueId = typeof data?.bizUniqueId === "string" && data.bizUniqueId.trim() || trimmedBizUniqueId;
6710
+ logger?.info(`Light sender: OK bizUniqueId=${effectiveBizUniqueId ?? "-"}, resBody=${resBody.substring(0, 200)}`);
6711
+ return {
6712
+ fallback: false,
6713
+ result: {
6714
+ ok: true,
6715
+ bizUniqueId: effectiveBizUniqueId || undefined,
6716
+ response: parsed,
6717
+ via: "notification-intelligence"
6718
+ }
6719
+ };
6720
+ }
6721
+ async function sendViaLegacyMessageService(apiKey, segments, logger, repeatInput, reason, resolvedTitle) {
6722
+ const apiUrl = getEnvUrls().lightApiUrl;
6723
+ const appKey = "phone-notifications";
6724
+ const templateId = "1990771146010017800";
6725
+ logger?.info(`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "…" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "…" : "EMPTY"}, title=${resolvedTitle}, reason=${reason ?? ""}, segments=${JSON.stringify(segments)}`);
6726
+ if (!apiUrl || !appKey || !templateId) {
6727
+ return {
6728
+ ok: false,
6729
+ error: "灯效 API 未配置,请确认构建时已封装 OPENCLAW_HOST_* / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
6730
+ };
6731
+ }
6732
+ let bizContent;
6733
+ try {
6734
+ bizContent = buildLightEffectApnsBody(segments, repeatInput, reason);
6735
+ } catch (error) {
6736
+ return { ok: false, error: error?.message ?? String(error) };
6737
+ }
6738
+ const bizUniqueId = import_node_crypto3.randomUUID();
6739
+ const requestBody = {
6740
+ appKey,
6741
+ bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", title: resolvedTitle, reason },
6742
+ bizUniqueId,
6743
+ paramsMap: { bizContent },
6744
+ pushType: "SPECIFY_PUSH",
6745
+ templateId
6746
+ };
6747
+ logger?.info(`Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`);
6748
+ const res = await fetch(apiUrl, {
6749
+ method: "POST",
6750
+ headers: {
6751
+ "Content-Type": "application/json",
6752
+ "X-Api-Key-Id": stripBearer(apiKey)
6753
+ },
6754
+ body: JSON.stringify(requestBody)
6755
+ });
6756
+ const resBody = await res.text();
6757
+ if (!res.ok) {
6758
+ logger?.warn(`Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`);
6759
+ return { ok: false, status: res.status, error: resBody, via: "message-service" };
6760
+ }
6761
+ logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
6762
+ return {
6763
+ ok: true,
6764
+ bizUniqueId,
6765
+ response: JSON.parse(resBody),
6766
+ via: "message-service"
6767
+ };
6768
+ }
6769
+ function stripBearer(apiKey) {
6770
+ return apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
6771
+ }
6772
+ function resolveLightTitle(title, reason, segments) {
6773
+ const trimmedTitle = title?.trim();
6774
+ if (trimmedTitle)
6775
+ return trimmedTitle;
6776
+ const trimmedReason = reason?.trim();
6777
+ if (trimmedReason)
6778
+ return trimmedReason;
6779
+ const modeDesc = segments.map((segment) => segment.mode).join("+");
6780
+ return `Effect: ${modeDesc || "custom"}`;
6781
+ }
6782
+
6783
+ // src/plugin/light-control.ts
6784
+ var lightControlParameters = {
6785
+ type: "object",
6786
+ required: ["segments", "reason"],
6787
+ additionalProperties: false,
6788
+ properties: {
6789
+ segments: {
6790
+ type: "array",
6791
+ description: "灯效段序列,1–12 段,按顺序播放",
6792
+ minItems: 1,
6793
+ maxItems: MAX_LIGHT_SEGMENTS,
6794
+ items: {
6795
+ type: "object",
6796
+ required: ["mode", "duration_s"],
6797
+ additionalProperties: false,
6798
+ properties: {
6799
+ mode: {
6800
+ type: "string",
6801
+ enum: ["wave", "breath", "strobe", "steady", "color_flow", "pixel_frame"],
6802
+ description: "灯效模式:wave 波浪(单色跑马,前景色+可选底色)/ breath 呼吸 / strobe 频闪 / steady 常亮 / " + "color_flow 流光(调色板沿圆周流动,1~2 个颜色锚点插值;**不是彩虹**,无法生成 7 色彩虹波浪)/ " + "pixel_frame 逐组像素帧。" + '易错对照:用户说"红色/蓝色波浪"等单一颜色波浪 → 必须用 wave,不要用 color_flow;' + '用户说"彩虹/七彩/rainbow" → 固件不支持真正的 7 色彩虹,需告知限制并给出近似方案(双锚点 color_flow 或多段 wave 轮播),' + "禁止静默用 color_flow 单锚点冒充彩虹。"
6803
+ },
6804
+ duration_s: {
6805
+ type: "number",
6806
+ minimum: 0,
6807
+ description: "本段持续时长(秒),0 表示无限时长。推荐离散值:0.5/1/2/3/5/6/8/16/24/32/48,建议单段 ≤8s 省电"
6808
+ },
6809
+ brightness: {
6810
+ type: "number",
6811
+ minimum: 0,
6812
+ maximum: 255,
6813
+ description: "前景亮度 0–255。非 pixel_frame 模式必填;brightness=0 仅 steady 模式有效(显式灭灯)"
6814
+ },
6815
+ color: {
6816
+ type: "object",
6817
+ required: ["r", "g", "b"],
6818
+ additionalProperties: false,
6819
+ properties: {
6820
+ r: { type: "number", minimum: 0, maximum: 255 },
6821
+ g: { type: "number", minimum: 0, maximum: 255 },
6822
+ b: { type: "number", minimum: 0, maximum: 255 }
6823
+ },
6824
+ description: "前景色 RGB。非 pixel_frame 模式必填;color_flow 下作为第一颜色锚点,至少 color/background 其一非零"
6825
+ },
6826
+ interval_ms: {
6827
+ type: "number",
6828
+ minimum: 0,
6829
+ description: "步进间隔(ms),wave/strobe/color_flow 有效,默认 200。量化档位:50/100/200/300/500/600/800/1600/2400/3200/4800"
6830
+ },
6831
+ direction: {
6832
+ type: "string",
6833
+ enum: ["ltr", "rtl"],
6834
+ description: "波浪/流光方向,wave/color_flow 有效,默认 ltr"
6835
+ },
6836
+ window: {
6837
+ type: "number",
6838
+ enum: [1, 2, 3],
6839
+ description: "wave:窗口灯数,默认 2;color_flow:调色板平移步幅(速度)"
6840
+ },
6841
+ breath_timing: {
6842
+ type: "object",
6843
+ additionalProperties: false,
6844
+ properties: {
6845
+ rise_ms: {
6846
+ type: "number",
6847
+ minimum: 0,
6848
+ description: "上升时间(ms),量化到 1040/1560/2080/2600/3100/4160"
6849
+ },
6850
+ hold_ms: {
6851
+ type: "number",
6852
+ minimum: 0,
6853
+ description: "保持最高亮度时间(ms),支持 0ms"
6854
+ },
6855
+ fall_ms: {
6856
+ type: "number",
6857
+ minimum: 0,
6858
+ description: "下降时间(ms),量化到 1040/1560/2080/2600/3100/4160"
6859
+ },
6860
+ off_ms: {
6861
+ type: "number",
6862
+ minimum: 0,
6863
+ description: "熄灭时间(ms),支持 0ms"
6864
+ }
6865
+ },
6866
+ description: "呼吸四段时间,仅 breath 有效;rise/fall 量化档位:1040/1560/2080/2600/3100/4160,hold/off 额外支持 0ms"
6867
+ },
6868
+ background: {
6869
+ type: "object",
6870
+ required: ["r", "g", "b", "brightness"],
6871
+ additionalProperties: false,
6872
+ properties: {
6873
+ r: { type: "number", minimum: 0, maximum: 255 },
6874
+ g: { type: "number", minimum: 0, maximum: 255 },
6875
+ b: { type: "number", minimum: 0, maximum: 255 },
6876
+ brightness: {
6877
+ type: "number",
6878
+ minimum: 0,
6879
+ maximum: 255,
6880
+ description: "底色亮度,量化档位:0/32/64/96/128/192/255"
6881
+ }
6882
+ },
6883
+ description: "底色 RGB + 亮度,wave 有效为底色层;color_flow 有效为第二颜色锚点(与前景色插值生成多色调色板)"
6884
+ },
6885
+ pixels: {
6886
+ type: "array",
6887
+ minItems: 1,
6888
+ maxItems: 7,
6889
+ items: {
6890
+ type: "object",
6891
+ required: ["index", "color", "brightness"],
6892
+ additionalProperties: false,
6893
+ properties: {
6894
+ index: {
6895
+ type: "number",
6896
+ minimum: 0,
6897
+ maximum: 6,
6898
+ description: "LED 组索引,0–6"
6899
+ },
6900
+ color: {
6901
+ type: "object",
6902
+ required: ["r", "g", "b"],
6903
+ additionalProperties: false,
6904
+ properties: {
6905
+ r: { type: "number", minimum: 0, maximum: 255 },
6906
+ g: { type: "number", minimum: 0, maximum: 255 },
6907
+ b: { type: "number", minimum: 0, maximum: 255 }
6908
+ },
6909
+ description: "该像素的 RGB"
6910
+ },
6911
+ brightness: {
6912
+ type: "number",
6913
+ minimum: 0,
6914
+ maximum: 255,
6915
+ description: "该像素亮度 0–255;0 会编码为像素熄灭"
6916
+ }
6917
+ }
6918
+ },
6919
+ description: "仅 pixel_frame 有效:播放前先清 7 组,再按此列表写入 (Idx,R,G,B,Br)"
6920
+ }
6921
+ }
6922
+ }
6923
+ },
6924
+ reason: {
6925
+ type: "string",
6926
+ description: "结合对话上下文,简要说明为什么要亮灯(例如:用户心情低落想要温暖氛围、庆祝生日、提醒喝水等)。" + "这个字段帮助记录每次亮灯的意图和动机。"
6927
+ },
6928
+ title: {
6929
+ type: "string",
6930
+ description: "对本次亮灯的简短标题/摘要(例如:温暖氛围、生日庆祝、喝水提醒)。不填时默认回退到 reason。"
6931
+ },
6932
+ repeat: {
6933
+ type: "boolean",
6934
+ description: "兼容旧参数。true = 无限循环,false/不填 = 一轮;若同时提供 repeat_times,则以后者为准"
6935
+ },
6936
+ repeat_times: {
6937
+ type: "number",
6938
+ minimum: 0,
6939
+ description: "整条组合重复次数:0 = 无限循环,1 = 播放一轮。当前 ANCS 路径不支持 N>=2"
6940
+ }
6941
+ }
6942
+ };
6943
+ function registerLightControlTool(api, logger) {
6944
+ api.registerTool({
6945
+ name: "light_control",
6946
+ label: "Light Control",
6947
+ description: "立即控制硬件灯效(一次性执行,不会创建或保存通知触发规则),支持 1–12 段顺序灯效,每段独立参数(6 种模式:波浪/呼吸/频闪/常亮/流光/逐组像素帧)。" + "Tool 内部自动量化为嵌入式协议定义的离散档位,并按模式精简为变长 ANCS 控制序列,通过 HTTP 接口下发灯效指令。" + '当用户说"开灯"、"换个灯效"、"把灯调成红色"、"营造氛围"、"测试/预览灯效"等与当前即时灯光控制相关的表达时调用。' + `如果用户说"收到某类通知/消息时亮灯/闪灯/变灯效",那是持久灯效规则,改用 ${LIGHT_RULE_TOOL_NAMES.create} 或 ${LIGHT_RULE_TOOL_NAMES.update}。`,
6948
+ parameters: lightControlParameters,
6949
+ async execute(toolCallId, params) {
6950
+ let apiKey;
6951
+ try {
6952
+ apiKey = requireApiKey();
6953
+ } catch (e) {
6954
+ return {
6955
+ content: [{ type: "text", text: e.message }],
6956
+ details: { ok: false, error: { code: "AUTH_REQUIRED", message: e.message } }
6957
+ };
6958
+ }
6959
+ const { segments, title, reason, repeat, repeat_times } = params;
6960
+ const validation = validateSegments(segments);
6961
+ if (!validation.valid) {
6962
+ return {
6963
+ content: [{ type: "text", text: JSON.stringify(validation.errors) }],
6964
+ details: {
6965
+ ok: false,
6966
+ error: { code: "VALIDATION_FAILED", details: validation.errors }
6967
+ }
6968
+ };
6969
+ }
6970
+ let repeatTimes;
6971
+ try {
6972
+ repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
6973
+ assertAncsRepeatTimes(repeatTimes);
6974
+ } catch (error) {
6975
+ return {
6976
+ content: [{ type: "text", text: error?.message ?? String(error) }],
6977
+ details: {
6978
+ ok: false,
6979
+ error: { code: "VALIDATION_FAILED", message: error?.message ?? String(error) }
6980
+ }
6981
+ };
6982
+ }
6983
+ logger.info(`Light control title: ${title ?? ""}, reason: ${reason}`);
6984
+ for (const warning of validation.warnings) {
6985
+ logger.warn(`Light control validation warning [${warning.code}] ${warning.field}: ${warning.message}`);
6986
+ }
6987
+ const bizUniqueId = typeof toolCallId === "string" && toolCallId.trim() ? `plugin-tool-${toolCallId.trim()}` : undefined;
6988
+ const result = await sendLightEffect(apiKey, validation.segments, logger, { repeat_times: repeatTimes }, reason, title, { bizUniqueId });
6989
+ if (!result.ok) {
6990
+ logger.warn(`Light control HTTP request failed: ${result.status} ${result.error}`);
6991
+ } else {
6992
+ logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
6993
+ }
6994
+ const details = validation.warnings.length > 0 ? { ...result, warnings: validation.warnings } : result;
6995
+ return {
6996
+ content: [{ type: "text", text: JSON.stringify(details) }],
6997
+ details
6998
+ };
6999
+ }
7000
+ });
7001
+ }
7002
+
6486
7003
  // src/light-rules/registry.ts
6487
7004
  class LightRuleRegistry {
6488
7005
  ctx;
@@ -6557,7 +7074,7 @@ class LightRuleRegistry {
6557
7074
  }
6558
7075
 
6559
7076
  // src/index.ts
6560
- var import_node_path31 = require("node:path");
7077
+ var import_node_path33 = require("node:path");
6561
7078
 
6562
7079
  // src/update/channel.ts
6563
7080
  var PRODUCTION_UPDATE_BASE_URL = "https://artifact.yoooclaw.com/plugin";
@@ -6585,8 +7102,8 @@ function resolveUpdateBaseUrl(params) {
6585
7102
  }
6586
7103
 
6587
7104
  // src/update/index.ts
6588
- var import_node_fs11 = require("node:fs");
6589
- var import_node_path11 = require("node:path");
7105
+ var import_node_fs13 = require("node:fs");
7106
+ var import_node_path13 = require("node:path");
6590
7107
 
6591
7108
  // src/plugin/shared.ts
6592
7109
  function readBody(req) {
@@ -6782,9 +7299,9 @@ class UpdateChecker {
6782
7299
  }
6783
7300
 
6784
7301
  // src/update/executor.ts
6785
- var import_node_fs10 = require("node:fs");
7302
+ var import_node_fs11 = require("node:fs");
6786
7303
  var import_node_child_process = require("node:child_process");
6787
- var import_node_path10 = require("node:path");
7304
+ var import_node_path11 = require("node:path");
6788
7305
  var import_node_os = require("node:os");
6789
7306
  var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
6790
7307
  var UPDATE_DIR_NAME = ".phone-notifications-update";
@@ -6800,27 +7317,27 @@ function normalizeBaseUrlAndDeps(baseUrlOrDeps, deps) {
6800
7317
  };
6801
7318
  }
6802
7319
  function updateRootForTarget(targetDir) {
6803
- return import_node_path10.join(import_node_path10.dirname(targetDir), UPDATE_DIR_NAME);
7320
+ return import_node_path11.join(import_node_path11.dirname(targetDir), UPDATE_DIR_NAME);
6804
7321
  }
6805
7322
  function lastUpdateStatusPath(targetDir) {
6806
- return import_node_path10.join(updateRootForTarget(targetDir), LAST_UPDATE_FILE);
7323
+ return import_node_path11.join(updateRootForTarget(targetDir), LAST_UPDATE_FILE);
6807
7324
  }
6808
7325
  function writeJsonAtomic(path, value) {
6809
- import_node_fs10.mkdirSync(import_node_path10.dirname(path), { recursive: true });
7326
+ import_node_fs11.mkdirSync(import_node_path11.dirname(path), { recursive: true });
6810
7327
  const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
6811
- import_node_fs10.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}
7328
+ import_node_fs11.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}
6812
7329
  `);
6813
- import_node_fs10.renameSync(tmpPath, path);
7330
+ import_node_fs11.renameSync(tmpPath, path);
6814
7331
  }
6815
7332
  function writeLastUpdateStatus(status) {
6816
7333
  writeJsonAtomic(lastUpdateStatusPath(status.targetDir), status);
6817
7334
  }
6818
7335
  function readLastUpdateStatus(targetDir) {
6819
7336
  const path = lastUpdateStatusPath(targetDir);
6820
- if (!import_node_fs10.existsSync(path))
7337
+ if (!import_node_fs11.existsSync(path))
6821
7338
  return;
6822
7339
  try {
6823
- return JSON.parse(import_node_fs10.readFileSync(path, "utf8"));
7340
+ return JSON.parse(import_node_fs11.readFileSync(path, "utf8"));
6824
7341
  } catch {
6825
7342
  return;
6826
7343
  }
@@ -6878,17 +7395,17 @@ function isCrossDeviceError(err2) {
6878
7395
  }
6879
7396
  function movePathSync(source, target) {
6880
7397
  try {
6881
- import_node_fs10.renameSync(source, target);
7398
+ import_node_fs11.renameSync(source, target);
6882
7399
  } catch (err2) {
6883
7400
  if (!isCrossDeviceError(err2))
6884
7401
  throw err2;
6885
- import_node_fs10.cpSync(source, target, {
7402
+ import_node_fs11.cpSync(source, target, {
6886
7403
  errorOnExist: false,
6887
7404
  force: true,
6888
7405
  recursive: true,
6889
7406
  verbatimSymlinks: true
6890
7407
  });
6891
- import_node_fs10.rmSync(source, { recursive: true, force: true });
7408
+ import_node_fs11.rmSync(source, { recursive: true, force: true });
6892
7409
  }
6893
7410
  }
6894
7411
  function buildLastUpdateNotice(status) {
@@ -6951,23 +7468,11 @@ function markLastUpdateRestartStatus(targetDir, restartStatus, fields = {}) {
6951
7468
  updatedAt: new Date().toISOString()
6952
7469
  });
6953
7470
  }
6954
- function markLastUpdateReconciled(targetDir) {
6955
- const status = readLastUpdateStatus(targetDir);
6956
- if (!status || status.status !== "succeeded")
6957
- return;
6958
- if (status.reconciledAt)
6959
- return;
6960
- writeLastUpdateStatus({
6961
- ...status,
6962
- reconciledAt: new Date().toISOString(),
6963
- updatedAt: new Date().toISOString()
6964
- });
6965
- }
6966
7471
  function cleanupPendingDir(status) {
6967
7472
  if (!status?.pendingDir)
6968
7473
  return;
6969
7474
  try {
6970
- import_node_fs10.rmSync(status.pendingDir, { recursive: true, force: true });
7475
+ import_node_fs11.rmSync(status.pendingDir, { recursive: true, force: true });
6971
7476
  } catch {}
6972
7477
  }
6973
7478
  function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pending") {
@@ -7001,12 +7506,12 @@ function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pe
7001
7506
  };
7002
7507
  writeLastUpdateStatus(applyingStatus);
7003
7508
  try {
7004
- if (import_node_fs10.existsSync(status.targetDir)) {
7509
+ if (import_node_fs11.existsSync(status.targetDir)) {
7005
7510
  movePathSync(status.targetDir, backupDir);
7006
7511
  }
7007
7512
  movePathSync(status.packageDir, status.targetDir);
7008
- import_node_fs10.rmSync(backupDir, { recursive: true, force: true });
7009
- import_node_fs10.rmSync(status.pendingDir, { recursive: true, force: true });
7513
+ import_node_fs11.rmSync(backupDir, { recursive: true, force: true });
7514
+ import_node_fs11.rmSync(status.pendingDir, { recursive: true, force: true });
7010
7515
  writeLastUpdateStatus({
7011
7516
  ...applyingStatus,
7012
7517
  status: "succeeded",
@@ -7028,7 +7533,7 @@ function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pe
7028
7533
  } catch (err2) {
7029
7534
  const error = String(err2 instanceof Error ? err2.stack ?? err2.message : err2);
7030
7535
  try {
7031
- if (!import_node_fs10.existsSync(status.targetDir) && import_node_fs10.existsSync(backupDir)) {
7536
+ if (!import_node_fs11.existsSync(status.targetDir) && import_node_fs11.existsSync(backupDir)) {
7032
7537
  movePathSync(backupDir, status.targetDir);
7033
7538
  }
7034
7539
  } catch {}
@@ -7205,13 +7710,13 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
7205
7710
  const updateId = createUpdateId(now, deps.random?.() ?? Math.random());
7206
7711
  const updateRoot = updateRootForTarget(targetDir);
7207
7712
  const statusPath = lastUpdateStatusPath(targetDir);
7208
- const pendingDir = import_node_path10.join(updateRoot, updateId);
7209
- const packageDir = import_node_path10.join(pendingDir, "package");
7713
+ const pendingDir = import_node_path11.join(updateRoot, updateId);
7714
+ const packageDir = import_node_path11.join(pendingDir, "package");
7210
7715
  let workDir = "";
7211
7716
  try {
7212
- import_node_fs10.mkdirSync(updateRoot, { recursive: true });
7213
- workDir = import_node_fs10.mkdtempSync(import_node_path10.join(updateRoot, ".work-"));
7214
- const tgzPath = import_node_path10.join(workDir, "plugin.tgz");
7717
+ import_node_fs11.mkdirSync(updateRoot, { recursive: true });
7718
+ workDir = import_node_fs11.mkdtempSync(import_node_path11.join(updateRoot, ".work-"));
7719
+ const tgzPath = import_node_path11.join(workDir, "plugin.tgz");
7215
7720
  const existingStatus = readLastUpdateStatus(targetDir);
7216
7721
  if (existingStatus && isActiveUpdate(existingStatus, now)) {
7217
7722
  return {
@@ -7240,9 +7745,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
7240
7745
  };
7241
7746
  }
7242
7747
  const buffer = Buffer.from(await response.arrayBuffer());
7243
- import_node_fs10.writeFileSync(tgzPath, buffer);
7748
+ import_node_fs11.writeFileSync(tgzPath, buffer);
7244
7749
  logger.info(`下载完成 (${buffer.length} bytes)`);
7245
- import_node_fs10.mkdirSync(packageDir, { recursive: true });
7750
+ import_node_fs11.mkdirSync(packageDir, { recursive: true });
7246
7751
  const tarResult = await runCommand(["tar", "-xzf", tgzPath, "-C", packageDir, "--strip-components=1"], { timeoutMs: 30000 });
7247
7752
  if (tarResult.code !== 0) {
7248
7753
  const err2 = tarResult.stderr || tarResult.stdout || "unknown error";
@@ -7321,7 +7826,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
7321
7826
  } finally {
7322
7827
  try {
7323
7828
  if (workDir)
7324
- import_node_fs10.rmSync(workDir, { force: true, recursive: true });
7829
+ import_node_fs11.rmSync(workDir, { force: true, recursive: true });
7325
7830
  } catch {}
7326
7831
  }
7327
7832
  }
@@ -7397,6 +7902,123 @@ function scheduleGatewayRestart(logger, deps = {}) {
7397
7902
  };
7398
7903
  }
7399
7904
 
7905
+ // src/update/stale-runtime.ts
7906
+ var import_node_child_process2 = require("node:child_process");
7907
+ var import_node_fs12 = require("node:fs");
7908
+ var import_node_path12 = require("node:path");
7909
+ var ESCALATION_FILE = "restart-escalation.json";
7910
+ var ESCALATION_SPAWN_DELAY_MS = 5000;
7911
+ function readPackageVersion(packageJsonPath) {
7912
+ try {
7913
+ const parsed = JSON.parse(import_node_fs12.readFileSync(packageJsonPath, "utf8"));
7914
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
7915
+ } catch {
7916
+ return;
7917
+ }
7918
+ }
7919
+ function detectStalePluginRuntime(targetDir, runningVersion = PLUGIN_VERSION) {
7920
+ const diskVersion = readPackageVersion(import_node_path12.join(targetDir, "package.json"));
7921
+ if (!diskVersion || !isNewerVersion(diskVersion, runningVersion)) {
7922
+ return { stale: false, runningVersion, diskVersion };
7923
+ }
7924
+ return { stale: true, runningVersion, diskVersion };
7925
+ }
7926
+ function escalationRecordPath(targetDir) {
7927
+ return import_node_path12.join(updateRootForTarget(targetDir), ESCALATION_FILE);
7928
+ }
7929
+ function readEscalationRecord(targetDir) {
7930
+ try {
7931
+ const parsed = JSON.parse(import_node_fs12.readFileSync(escalationRecordPath(targetDir), "utf8"));
7932
+ return typeof parsed?.version === "string" ? parsed : undefined;
7933
+ } catch {
7934
+ return;
7935
+ }
7936
+ }
7937
+ function writeEscalationRecord(targetDir, record) {
7938
+ const path = escalationRecordPath(targetDir);
7939
+ import_node_fs12.mkdirSync(import_node_path12.dirname(path), { recursive: true });
7940
+ const tmpPath = `${path}.tmp.${process.pid}`;
7941
+ import_node_fs12.writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}
7942
+ `);
7943
+ import_node_fs12.renameSync(tmpPath, path);
7944
+ }
7945
+ function resolveRestartCommand(deps) {
7946
+ const argv1 = deps.argv1 ?? process.argv[1];
7947
+ const execPath = deps.execPath ?? process.execPath;
7948
+ if (argv1 && (/openclaw/i.test(import_node_path12.basename(argv1)) || /openclaw/i.test(argv1))) {
7949
+ return [execPath, argv1, "gateway", "restart"];
7950
+ }
7951
+ return ["openclaw", "gateway", "restart"];
7952
+ }
7953
+ function maybeEscalateStaleRuntimeRestart(logger, targetDir, deps = {}) {
7954
+ const info = detectStalePluginRuntime(targetDir, deps.runningVersion);
7955
+ if (!info.stale || !info.diskVersion)
7956
+ return { action: "none" };
7957
+ const diskVersion = info.diskVersion;
7958
+ logger.error(`插件更新未生效:磁盘已安装 ${diskVersion},进程仍在运行 ${info.runningVersion}。` + "宿主的进程内重启不会重新加载插件代码,需要完整重启 openclaw 进程。");
7959
+ try {
7960
+ const cfg = deps.loadConfig?.();
7961
+ if (cfg?.commands?.restart === false) {
7962
+ return {
7963
+ action: "manual-required",
7964
+ diskVersion,
7965
+ reason: "commands.restart=false,自动重启被配置禁用"
7966
+ };
7967
+ }
7968
+ } catch (err2) {
7969
+ logger.warn(`陈旧运行时检查读取配置失败: ${String(err2)}`);
7970
+ }
7971
+ const attempted = readEscalationRecord(targetDir);
7972
+ if (attempted?.version === diskVersion) {
7973
+ return {
7974
+ action: "manual-required",
7975
+ diskVersion,
7976
+ reason: `已为 ${diskVersion} 自动触发过一次完整重启但未生效(宿主可能未由 service manager 托管)`
7977
+ };
7978
+ }
7979
+ const command = resolveRestartCommand(deps);
7980
+ const now = deps.now?.() ?? new Date;
7981
+ try {
7982
+ writeEscalationRecord(targetDir, {
7983
+ version: diskVersion,
7984
+ attemptedAt: now.toISOString()
7985
+ });
7986
+ } catch (err2) {
7987
+ return {
7988
+ action: "manual-required",
7989
+ diskVersion,
7990
+ reason: `无法记录自动重启尝试(${String(err2)}),为避免重启循环不再自动触发`
7991
+ };
7992
+ }
7993
+ const spawnFn = deps.spawnFn ?? import_node_child_process2.spawn;
7994
+ const scheduleFn = deps.scheduleFn ?? ((task, delayMs) => setTimeout(task, delayMs));
7995
+ const timer = scheduleFn(() => {
7996
+ try {
7997
+ const child = spawnFn(command[0], command.slice(1), {
7998
+ detached: true,
7999
+ stdio: "ignore"
8000
+ });
8001
+ child.unref?.();
8002
+ logger.warn(`已触发完整 gateway 重启以加载插件 ${diskVersion}: ${command.join(" ")}`);
8003
+ } catch (err2) {
8004
+ logger.error(`自动完整重启失败(${String(err2)}),请手动完整重启 openclaw 进程以加载 ${diskVersion}`);
8005
+ }
8006
+ }, ESCALATION_SPAWN_DELAY_MS);
8007
+ timer.unref?.();
8008
+ logger.warn(`将在 ${ESCALATION_SPAWN_DELAY_MS / 1000}s 后尝试完整重启 gateway(openclaw gateway restart)以加载 ${diskVersion}`);
8009
+ return { action: "restart-scheduled", diskVersion, command };
8010
+ }
8011
+ function buildStaleRuntimePromptNotice(info, escalation) {
8012
+ if (!info.stale || !info.diskVersion)
8013
+ return;
8014
+ const base = `[系统通知] phone-notifications 插件已更新到 ${info.diskVersion} 并安装完成,` + `但当前进程仍在运行旧版本 ${info.runningVersion}:宿主的进程内重启(SIGUSR1 / ` + `gateway.restart)不会重新加载插件代码。`;
8015
+ if (escalation.action === "restart-scheduled") {
8016
+ return base + "系统已自动触发一次完整重启,若几分钟后此提示仍出现,说明自动重启未生效," + "请告知用户需要完整重启 openclaw 进程(例如 systemctl restart openclaw," + "或结束进程后由服务管理器拉起)。";
8017
+ }
8018
+ const reason = escalation.action === "manual-required" ? `(${escalation.reason})` : "";
8019
+ return base + `请告知用户需要完整重启 openclaw 进程才能生效${reason}:` + "例如 systemctl restart openclaw,或结束 openclaw 进程后由服务管理器拉起;" + "仅执行进程内的 gateway 重启无效。";
8020
+ }
8021
+
7400
8022
  // src/update/index.ts
7401
8023
 
7402
8024
  var PLUGIN_ID2 = "phone-notifications";
@@ -7408,32 +8030,7 @@ function shouldSuppressUpdateNotifications() {
7408
8030
  return process.platform === "win32";
7409
8031
  }
7410
8032
  function resolveTargetDir(api) {
7411
- try {
7412
- const cfg = api.runtime.config?.loadConfig?.();
7413
- const installPath = cfg?.plugins?.installs?.[PLUGIN_ID2]?.installPath;
7414
- if (installPath)
7415
- return installPath;
7416
- } catch {}
7417
- return import_node_path11.join(resolvePluginStateDir(api), "extensions", PLUGIN_ID2);
7418
- }
7419
- async function updateConfigRecord(api, version, targetDir, tgzUrl) {
7420
- const configApi = api.runtime.config;
7421
- if (!configApi)
7422
- return;
7423
- const cfg = configApi.loadConfig();
7424
- if (!cfg.plugins)
7425
- cfg.plugins = {};
7426
- if (!cfg.plugins.installs)
7427
- cfg.plugins.installs = {};
7428
- cfg.plugins.installs[PLUGIN_ID2] = {
7429
- ...cfg.plugins.installs[PLUGIN_ID2],
7430
- source: "archive",
7431
- sourcePath: tgzUrl,
7432
- installPath: targetDir,
7433
- version,
7434
- installedAt: new Date().toISOString()
7435
- };
7436
- await configApi.writeConfigFile(cfg);
8033
+ return import_node_path13.join(resolvePluginStateDir(api), "extensions", PLUGIN_ID2);
7437
8034
  }
7438
8035
  function applyManualRestart(logger, result, targetDir) {
7439
8036
  const version = result.version ?? "目标版本";
@@ -7528,25 +8125,12 @@ function finalizeUpdateResult(api, logger, result, targetDir) {
7528
8125
  message: `已应用更新到 ${version},正在重启 gateway`
7529
8126
  };
7530
8127
  }
7531
- async function reconcileSucceededUpdateRecord(api, logger, targetDir) {
7532
- const status = readLastUpdateStatus(targetDir);
7533
- if (status?.status !== "succeeded" || !status.tgzUrl)
7534
- return;
7535
- if (status.reconciledAt)
7536
- return;
7537
- try {
7538
- await updateConfigRecord(api, status.version, targetDir, status.tgzUrl);
7539
- markLastUpdateReconciled(targetDir);
7540
- } catch (err2) {
7541
- logger.warn(`更新已应用,但配置记录补写失败: ${String(err2)}`);
7542
- }
7543
- }
7544
8128
  function resolveModulePackageJsonPath() {
7545
- return import_node_path11.join(__dirname, "..", "package.json");
8129
+ return import_node_path13.join(__dirname, "..", "package.json");
7546
8130
  }
7547
8131
  function resolveEffectiveCurrentVersion(targetDir, modulePackageJsonPath = resolveModulePackageJsonPath()) {
7548
8132
  const installedVersion = pickNewestVersion([
7549
- readPackageVersionAt(import_node_path11.join(targetDir, "package.json")),
8133
+ readPackageVersionAt(import_node_path13.join(targetDir, "package.json")),
7550
8134
  readPackageVersionAt(modulePackageJsonPath)
7551
8135
  ]);
7552
8136
  if (installedVersion && isNewerVersion(installedVersion, PLUGIN_VERSION)) {
@@ -7558,9 +8142,39 @@ function resolveEffectiveCurrentVersion(targetDir, modulePackageJsonPath = resol
7558
8142
  }
7559
8143
  return PLUGIN_VERSION;
7560
8144
  }
8145
+ function registerPluginUpdateTool(api, logger, baseUrl, onSuccess) {
8146
+ api.registerTool({
8147
+ name: "plugin-update",
8148
+ label: "Plugin Update",
8149
+ description: "将 phone-notifications 插件更新到指定版本。" + "仅在用户明确同意更新后调用。更新完成后会自动尝试重启 gateway;如宿主不支持,则需手动重启。",
8150
+ parameters: {
8151
+ type: "object",
8152
+ required: ["version"],
8153
+ additionalProperties: false,
8154
+ properties: {
8155
+ version: {
8156
+ type: "string",
8157
+ description: "目标版本号,如 1.11.0"
8158
+ }
8159
+ }
8160
+ },
8161
+ async execute(_toolCallId, params) {
8162
+ const { version } = params;
8163
+ const targetDir = resolveTargetDir(api);
8164
+ const result = await executeUpdate(version, api.runtime.system.runCommandWithTimeout, logger, targetDir, baseUrl);
8165
+ const finalResult = finalizeUpdateResult(api, logger, result, targetDir);
8166
+ if (finalResult.success)
8167
+ onSuccess?.();
8168
+ return {
8169
+ content: [{ type: "text", text: finalResult.message }],
8170
+ details: finalResult
8171
+ };
8172
+ }
8173
+ });
8174
+ }
7561
8175
  function readPackageVersionAt(packageJsonPath) {
7562
8176
  try {
7563
- const parsed = JSON.parse(import_node_fs11.readFileSync(packageJsonPath, "utf-8"));
8177
+ const parsed = JSON.parse(import_node_fs13.readFileSync(packageJsonPath, "utf-8"));
7564
8178
  const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
7565
8179
  return VERSION_PATTERN2.test(version) ? version : undefined;
7566
8180
  } catch {
@@ -7574,11 +8188,15 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
7574
8188
  }
7575
8189
  let pendingUpdate = null;
7576
8190
  let pendingGatewayUpdate = null;
8191
+ let staleRuntimeInfo = {
8192
+ stale: false,
8193
+ runningVersion: PLUGIN_VERSION
8194
+ };
8195
+ let staleRuntimeEscalation = { action: "none" };
7577
8196
  const intervalMs = (config.checkIntervalHours ?? 4) * 3600000;
7578
8197
  const channel = config.channel ?? "latest";
7579
8198
  const baseUrl = config.baseUrl ?? PRODUCTION_UPDATE_BASE_URL;
7580
8199
  const suppressUpdateNotifications = shouldSuppressUpdateNotifications();
7581
- reconcileSucceededUpdateRecord(api, logger, resolveTargetDir(api));
7582
8200
  function clearPendingUpdateNotice() {
7583
8201
  pendingUpdate = null;
7584
8202
  pendingGatewayUpdate = null;
@@ -7610,42 +8228,21 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
7610
8228
  api.on("before_prompt_build", () => {
7611
8229
  if (suppressUpdateNotifications)
7612
8230
  return;
7613
- if (!pendingUpdate)
8231
+ const notices = [];
8232
+ const staleNotice = buildStaleRuntimePromptNotice(staleRuntimeInfo, staleRuntimeEscalation);
8233
+ if (staleNotice) {
8234
+ notices.push(staleNotice);
8235
+ } else if (pendingUpdate) {
8236
+ notices.push(`[系统通知] phone-notifications 插件有新版本 ${pendingUpdate.latest} 可用` + `(当前版本 ${pendingUpdate.current})。
8237
+ ` + `请用自然语言在合适时机告知用户,若用户同意更新,` + `调用 plugin-update tool 并传入 version 参数。` + `若用户拒绝,不再重复提醒。`);
8238
+ }
8239
+ if (notices.length === 0)
7614
8240
  return;
7615
- return {
7616
- appendSystemContext: `[系统通知] phone-notifications 插件有新版本 ${pendingUpdate.latest} 可用` + `(当前版本 ${pendingUpdate.current})。
7617
- ` + `请用自然语言在合适时机告知用户,若用户同意更新,` + `调用 plugin-update tool 并传入 version 参数。` + `若用户拒绝,不再重复提醒。`
7618
- };
7619
- });
7620
- api.registerTool({
7621
- name: "plugin-update",
7622
- label: "Plugin Update",
7623
- description: "将 phone-notifications 插件更新到指定版本。" + "仅在用户明确同意更新后调用。更新完成后会自动尝试重启 gateway;如宿主不支持,则需手动重启。",
7624
- parameters: {
7625
- type: "object",
7626
- required: ["version"],
7627
- additionalProperties: false,
7628
- properties: {
7629
- version: {
7630
- type: "string",
7631
- description: "目标版本号,如 1.11.0"
7632
- }
7633
- }
7634
- },
7635
- async execute(_toolCallId, params) {
7636
- const { version } = params;
7637
- const targetDir = resolveTargetDir(api);
7638
- const result = await executeUpdate(version, api.runtime.system.runCommandWithTimeout, logger, targetDir, baseUrl);
7639
- const finalResult = finalizeUpdateResult(api, logger, result, targetDir);
7640
- if (finalResult.success) {
7641
- clearPendingUpdateNotice();
7642
- }
7643
- return {
7644
- content: [{ type: "text", text: finalResult.message }],
7645
- details: finalResult
7646
- };
7647
- }
8241
+ return { appendSystemContext: notices.join(`
8242
+
8243
+ `) };
7648
8244
  });
8245
+ registerPluginUpdateTool(api, logger, baseUrl, clearPendingUpdateNotice);
7649
8246
  if (!suppressUpdateNotifications) {
7650
8247
  logger.info(`自动更新已启用: channel=${channel}, baseUrl=${baseUrl}, checkIntervalHours=${config.checkIntervalHours ?? 4}`);
7651
8248
  }
@@ -7807,6 +8404,14 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
7807
8404
  });
7808
8405
  return {
7809
8406
  start: () => {
8407
+ const targetDir = resolveTargetDir(api);
8408
+ staleRuntimeInfo = detectStalePluginRuntime(targetDir);
8409
+ staleRuntimeEscalation = staleRuntimeInfo.stale ? maybeEscalateStaleRuntimeRestart(logger, targetDir, {
8410
+ loadConfig: () => {
8411
+ const configApi = api.runtime.config;
8412
+ return configApi?.loadConfig?.();
8413
+ }
8414
+ }) : { action: "none" };
7810
8415
  if (!suppressUpdateNotifications)
7811
8416
  checker.start();
7812
8417
  },
@@ -7825,17 +8430,9 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
7825
8430
  }
7826
8431
 
7827
8432
  // src/plugin/auto-update.ts
7828
- function registerAutoUpdateLifecycle(deps) {
7829
- const {
7830
- api,
7831
- config,
7832
- logger,
7833
- getBroadcastFn,
7834
- cacheBroadcast,
7835
- tunnelService
7836
- } = deps;
8433
+ function resolveAutoUpdateConfig(config) {
7837
8434
  const envName = loadEnvName();
7838
- const autoUpdateConfig = {
8435
+ return {
7839
8436
  ...config.autoUpdate,
7840
8437
  channel: resolveUpdateChannel({
7841
8438
  configuredChannel: config.autoUpdate?.channel,
@@ -7847,6 +8444,27 @@ function registerAutoUpdateLifecycle(deps) {
7847
8444
  envName
7848
8445
  })
7849
8446
  };
8447
+ }
8448
+ function registerAutoUpdateAgentTool({
8449
+ api,
8450
+ config,
8451
+ logger
8452
+ }) {
8453
+ const autoUpdateConfig = resolveAutoUpdateConfig(config);
8454
+ if (autoUpdateConfig.enabled === false)
8455
+ return;
8456
+ registerPluginUpdateTool(api, logger, autoUpdateConfig.baseUrl);
8457
+ }
8458
+ function registerAutoUpdateLifecycle(deps) {
8459
+ const {
8460
+ api,
8461
+ config,
8462
+ logger,
8463
+ getBroadcastFn,
8464
+ cacheBroadcast,
8465
+ tunnelService
8466
+ } = deps;
8467
+ const autoUpdateConfig = resolveAutoUpdateConfig(config);
7850
8468
  const autoUpdateLifecycle = registerAutoUpdate(api, logger, autoUpdateConfig, getBroadcastFn, cacheBroadcast, tunnelService ? {
7851
8469
  notifyUpdateAvailable: (update) => tunnelService.notifyUpdateAvailable(update),
7852
8470
  clearPendingUpdate: () => tunnelService.clearPendingUpdate()
@@ -7865,10 +8483,10 @@ function registerAutoUpdateLifecycle(deps) {
7865
8483
  }
7866
8484
 
7867
8485
  // src/plugin/cli.ts
7868
- var import_node_path19 = require("node:path");
8486
+ var import_node_path21 = require("node:path");
7869
8487
 
7870
8488
  // src/cli/auth.ts
7871
- var import_node_fs12 = require("node:fs");
8489
+ var import_node_fs14 = require("node:fs");
7872
8490
  function registerAuthCli(program) {
7873
8491
  const auth = program.command("auth").description("用户认证管理");
7874
8492
  auth.command("set-api-key <apiKey>").description("设置用户 API Key(持久化到本地配置)").action((apiKey) => {
@@ -7902,13 +8520,13 @@ function registerAuthCli(program) {
7902
8520
  });
7903
8521
  auth.command("clear").description("清除已保存的认证信息").action(() => {
7904
8522
  const path = credentialsPath();
7905
- if (import_node_fs12.existsSync(path)) {
8523
+ if (import_node_fs14.existsSync(path)) {
7906
8524
  const creds = readCredentials();
7907
8525
  delete creds.apiKey;
7908
8526
  delete creds.apiKeys;
7909
8527
  delete creds.token;
7910
8528
  if (Object.keys(creds).length === 0) {
7911
- import_node_fs12.rmSync(path, { force: true });
8529
+ import_node_fs14.rmSync(path, { force: true });
7912
8530
  } else {
7913
8531
  writeCredentials(creds);
7914
8532
  }
@@ -8053,9 +8671,9 @@ function registerNtfSummary(ntf, ctx) {
8053
8671
  }
8054
8672
 
8055
8673
  // src/cli/ntf-summary-job.ts
8056
- var import_node_crypto3 = require("node:crypto");
8057
- var import_node_fs13 = require("node:fs");
8058
- var import_node_path12 = require("node:path");
8674
+ var import_node_crypto4 = require("node:crypto");
8675
+ var import_node_fs15 = require("node:fs");
8676
+ var import_node_path14 = require("node:path");
8059
8677
  var SUMMARY_ROOT = ".summaries";
8060
8678
  var JOBS_DIR = "jobs";
8061
8679
  var CHUNKS_DIR = "chunks";
@@ -8175,7 +8793,7 @@ function nowIso2() {
8175
8793
  }
8176
8794
  function createJobId() {
8177
8795
  const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
8178
- return `${stamp}-${import_node_crypto3.randomUUID().slice(0, 8)}`;
8796
+ return `${stamp}-${import_node_crypto4.randomUUID().slice(0, 8)}`;
8179
8797
  }
8180
8798
  function assertJobId(id) {
8181
8799
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(id)) {
@@ -8183,24 +8801,24 @@ function assertJobId(id) {
8183
8801
  }
8184
8802
  }
8185
8803
  function summaryJobsDir(notificationsDir) {
8186
- return import_node_path12.join(notificationsDir, SUMMARY_ROOT, JOBS_DIR);
8804
+ return import_node_path14.join(notificationsDir, SUMMARY_ROOT, JOBS_DIR);
8187
8805
  }
8188
8806
  function summaryJobDir(notificationsDir, id) {
8189
8807
  assertJobId(id);
8190
- return import_node_path12.join(summaryJobsDir(notificationsDir), id);
8808
+ return import_node_path14.join(summaryJobsDir(notificationsDir), id);
8191
8809
  }
8192
8810
  function metaPath(jobDir) {
8193
- return import_node_path12.join(jobDir, "meta.json");
8811
+ return import_node_path14.join(jobDir, "meta.json");
8194
8812
  }
8195
8813
  function chunkPath(jobDir, chunkId) {
8196
- return import_node_path12.join(jobDir, CHUNKS_DIR, `${chunkId}.json`);
8814
+ return import_node_path14.join(jobDir, CHUNKS_DIR, `${chunkId}.json`);
8197
8815
  }
8198
8816
  function writeJson(path, value) {
8199
- import_node_fs13.writeFileSync(path, JSON.stringify(value, null, 2), "utf-8");
8817
+ import_node_fs15.writeFileSync(path, JSON.stringify(value, null, 2), "utf-8");
8200
8818
  }
8201
8819
  function readJson(path) {
8202
8820
  try {
8203
- return JSON.parse(import_node_fs13.readFileSync(path, "utf-8"));
8821
+ return JSON.parse(import_node_fs15.readFileSync(path, "utf-8"));
8204
8822
  } catch {
8205
8823
  exitError("READ_FAILED", `无法读取 JSON: ${path}`);
8206
8824
  }
@@ -8208,7 +8826,7 @@ function readJson(path) {
8208
8826
  function readMeta2(notificationsDir, id) {
8209
8827
  const dir = summaryJobDir(notificationsDir, id);
8210
8828
  const path = metaPath(dir);
8211
- if (!import_node_fs13.existsSync(path)) {
8829
+ if (!import_node_fs15.existsSync(path)) {
8212
8830
  exitError("JOB_NOT_FOUND", `summary job 不存在: ${id}`);
8213
8831
  }
8214
8832
  const meta = readJson(path);
@@ -8296,7 +8914,7 @@ function resolveSummaryText(opts) {
8296
8914
  if (!hasSummary && !hasSummaryFile) {
8297
8915
  exitError("INVALID_COMMIT", "必须提供 --summary 或 --summary-file");
8298
8916
  }
8299
- const text = hasSummary ? opts.summary : import_node_fs13.readFileSync(import_node_path12.resolve(opts.summaryFile), "utf-8");
8917
+ const text = hasSummary ? opts.summary : import_node_fs15.readFileSync(import_node_path14.resolve(opts.summaryFile), "utf-8");
8300
8918
  if (!text.trim()) {
8301
8919
  exitError("INVALID_COMMIT", "分片摘要不能为空");
8302
8920
  }
@@ -8320,7 +8938,7 @@ function buildResultMarkdown(meta, jobDir) {
8320
8938
  lines.push("(未提交摘要)", "");
8321
8939
  continue;
8322
8940
  }
8323
- lines.push(import_node_fs13.readFileSync(import_node_path12.join(jobDir, chunk.summaryFile), "utf-8").trim(), "");
8941
+ lines.push(import_node_fs15.readFileSync(import_node_path14.join(jobDir, chunk.summaryFile), "utf-8").trim(), "");
8324
8942
  }
8325
8943
  return lines.join(`
8326
8944
  `).trimEnd() + `
@@ -8328,8 +8946,8 @@ function buildResultMarkdown(meta, jobDir) {
8328
8946
  }
8329
8947
  function writeChunkSummary(notificationsDir, meta, chunk, text) {
8330
8948
  const jobDir = summaryJobDir(notificationsDir, meta.id);
8331
- const relativeSummaryFile = import_node_path12.join(SUMMARIES_DIR, `${chunk.id}.md`);
8332
- import_node_fs13.writeFileSync(import_node_path12.join(jobDir, relativeSummaryFile), text, "utf-8");
8949
+ const relativeSummaryFile = import_node_path14.join(SUMMARIES_DIR, `${chunk.id}.md`);
8950
+ import_node_fs15.writeFileSync(import_node_path14.join(jobDir, relativeSummaryFile), text, "utf-8");
8333
8951
  chunk.status = "done";
8334
8952
  chunk.completedAt = nowIso2();
8335
8953
  chunk.summaryFile = relativeSummaryFile;
@@ -8341,7 +8959,7 @@ function finalizeResultIfReady(notificationsDir, meta) {
8341
8959
  }
8342
8960
  const jobDir = summaryJobDir(notificationsDir, meta.id);
8343
8961
  const markdown = buildResultMarkdown(meta, jobDir);
8344
- import_node_fs13.writeFileSync(import_node_path12.join(jobDir, RESULT_FILE), markdown, "utf-8");
8962
+ import_node_fs15.writeFileSync(import_node_path14.join(jobDir, RESULT_FILE), markdown, "utf-8");
8345
8963
  meta.status = "complete";
8346
8964
  meta.resultFile = RESULT_FILE;
8347
8965
  return markdown;
@@ -8363,8 +8981,8 @@ function registerNtfSummaryJob(ntf, ctx) {
8363
8981
  const id = createJobId();
8364
8982
  const createdAt = nowIso2();
8365
8983
  const jobDir = summaryJobDir(dir, id);
8366
- import_node_fs13.mkdirSync(import_node_path12.join(jobDir, CHUNKS_DIR), { recursive: true });
8367
- import_node_fs13.mkdirSync(import_node_path12.join(jobDir, SUMMARIES_DIR), { recursive: true });
8984
+ import_node_fs15.mkdirSync(import_node_path14.join(jobDir, CHUNKS_DIR), { recursive: true });
8985
+ import_node_fs15.mkdirSync(import_node_path14.join(jobDir, SUMMARIES_DIR), { recursive: true });
8368
8986
  const chunks = [];
8369
8987
  for (let start = 0;start < notifications.length; start += chunkSize) {
8370
8988
  const rawChunk = notifications.slice(start, start + chunkSize);
@@ -8658,25 +9276,25 @@ function registerNtfStats(ntf, ctx) {
8658
9276
  }
8659
9277
 
8660
9278
  // src/cli/ntf-sync.ts
8661
- var import_node_child_process2 = require("node:child_process");
8662
- var import_node_fs14 = require("node:fs");
8663
- var import_node_path13 = require("node:path");
9279
+ var import_node_child_process3 = require("node:child_process");
9280
+ var import_node_fs16 = require("node:fs");
9281
+ var import_node_path15 = require("node:path");
8664
9282
  var SYNC_FETCH_LIMIT = 100;
8665
9283
  function checkpointPath(dir) {
8666
- return import_node_path13.join(dir, ".checkpoint.json");
9284
+ return import_node_path15.join(dir, ".checkpoint.json");
8667
9285
  }
8668
9286
  function readCheckpoint(dir) {
8669
9287
  const p = checkpointPath(dir);
8670
- if (!import_node_fs14.existsSync(p))
9288
+ if (!import_node_fs16.existsSync(p))
8671
9289
  return {};
8672
9290
  try {
8673
- return JSON.parse(import_node_fs14.readFileSync(p, "utf-8"));
9291
+ return JSON.parse(import_node_fs16.readFileSync(p, "utf-8"));
8674
9292
  } catch {
8675
9293
  return {};
8676
9294
  }
8677
9295
  }
8678
9296
  function writeCheckpoint(dir, data) {
8679
- import_node_fs14.writeFileSync(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
9297
+ import_node_fs16.writeFileSync(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8680
9298
  }
8681
9299
  function validateDateKey(value, optionName) {
8682
9300
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
@@ -8730,7 +9348,7 @@ function isDateInScope(dateKey, scope) {
8730
9348
  var MEMORY_PLUGIN_KEYWORD = "memory-lancedb-ultra";
8731
9349
  function detectMemoryBackend() {
8732
9350
  try {
8733
- const out = import_node_child_process2.execFileSync("openclaw", ["plugins", "list"], {
9351
+ const out = import_node_child_process3.execFileSync("openclaw", ["plugins", "list"], {
8734
9352
  encoding: "utf-8",
8735
9353
  timeout: 15000,
8736
9354
  stdio: ["ignore", "pipe", "ignore"]
@@ -8947,8 +9565,8 @@ function registerNtfSync(ntf, ctx) {
8947
9565
  }
8948
9566
 
8949
9567
  // src/cli/ntf-monitor.ts
8950
- var import_node_fs15 = require("node:fs");
8951
- var import_node_path14 = require("node:path");
9568
+ var import_node_fs17 = require("node:fs");
9569
+ var import_node_path16 = require("node:path");
8952
9570
 
8953
9571
  // src/monitor/fetch-gen.ts
8954
9572
  function generateFetchPy(name, matchRules) {
@@ -9035,20 +9653,20 @@ function tasksDir(ctx) {
9035
9653
  const base = ctx.workspaceDir || ctx.stateDir;
9036
9654
  if (!base)
9037
9655
  throw new Error("workspaceDir and stateDir both unavailable");
9038
- return import_node_path14.join(base, "tasks");
9656
+ return import_node_path16.join(base, "tasks");
9039
9657
  }
9040
9658
  function readMeta3(taskDir) {
9041
- const metaPath2 = import_node_path14.join(taskDir, "meta.json");
9042
- if (!import_node_fs15.existsSync(metaPath2))
9659
+ const metaPath2 = import_node_path16.join(taskDir, "meta.json");
9660
+ if (!import_node_fs17.existsSync(metaPath2))
9043
9661
  return null;
9044
9662
  try {
9045
- return JSON.parse(import_node_fs15.readFileSync(metaPath2, "utf-8"));
9663
+ return JSON.parse(import_node_fs17.readFileSync(metaPath2, "utf-8"));
9046
9664
  } catch {
9047
9665
  return null;
9048
9666
  }
9049
9667
  }
9050
9668
  function writeMeta2(taskDir, meta) {
9051
- import_node_fs15.writeFileSync(import_node_path14.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
9669
+ import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
9052
9670
  }
9053
9671
  function generateReadme(name, description) {
9054
9672
  return `# Monitor Task: ${name}
@@ -9067,30 +9685,30 @@ function registerNtfMonitor(ntf, ctx) {
9067
9685
  const monitor = ntf.command("monitor").description("通知监控任务管理");
9068
9686
  monitor.command("list").description("列出所有监控任务").action(() => {
9069
9687
  const dir = tasksDir(ctx);
9070
- if (!import_node_fs15.existsSync(dir)) {
9688
+ if (!import_node_fs17.existsSync(dir)) {
9071
9689
  output({ ok: true, tasks: [] });
9072
9690
  return;
9073
9691
  }
9074
9692
  const tasks = [];
9075
- for (const entry of import_node_fs15.readdirSync(dir, { withFileTypes: true })) {
9693
+ for (const entry of import_node_fs17.readdirSync(dir, { withFileTypes: true })) {
9076
9694
  if (!entry.isDirectory())
9077
9695
  continue;
9078
- const meta = readMeta3(import_node_path14.join(dir, entry.name));
9696
+ const meta = readMeta3(import_node_path16.join(dir, entry.name));
9079
9697
  if (meta)
9080
9698
  tasks.push(meta);
9081
9699
  }
9082
9700
  output({ ok: true, tasks });
9083
9701
  });
9084
9702
  monitor.command("show <name>").description("查看监控任务详情").action((name) => {
9085
- const taskDir = import_node_path14.join(tasksDir(ctx), name);
9703
+ const taskDir = import_node_path16.join(tasksDir(ctx), name);
9086
9704
  const meta = readMeta3(taskDir);
9087
9705
  if (!meta)
9088
9706
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
9089
- const checkpointPath2 = import_node_path14.join(taskDir, "checkpoint.json");
9707
+ const checkpointPath2 = import_node_path16.join(taskDir, "checkpoint.json");
9090
9708
  let checkpoint = {};
9091
- if (import_node_fs15.existsSync(checkpointPath2)) {
9709
+ if (import_node_fs17.existsSync(checkpointPath2)) {
9092
9710
  try {
9093
- checkpoint = JSON.parse(import_node_fs15.readFileSync(checkpointPath2, "utf-8"));
9711
+ checkpoint = JSON.parse(import_node_fs17.readFileSync(checkpointPath2, "utf-8"));
9094
9712
  } catch {}
9095
9713
  }
9096
9714
  output({
@@ -9103,8 +9721,8 @@ function registerNtfMonitor(ntf, ctx) {
9103
9721
  });
9104
9722
  monitor.command("create <name>").description("创建监控任务").requiredOption("--description <text>", "任务描述").requiredOption("--match-rules <json>", "匹配规则 JSON").requiredOption("--schedule <cron>", "cron 表达式").action((name, opts) => {
9105
9723
  const dir = tasksDir(ctx);
9106
- const taskDir = import_node_path14.join(dir, name);
9107
- if (import_node_fs15.existsSync(taskDir)) {
9724
+ const taskDir = import_node_path16.join(dir, name);
9725
+ if (import_node_fs17.existsSync(taskDir)) {
9108
9726
  exitError("ALREADY_EXISTS", `监控任务 '${name}' 已存在`);
9109
9727
  }
9110
9728
  let matchRules;
@@ -9113,7 +9731,7 @@ function registerNtfMonitor(ntf, ctx) {
9113
9731
  } catch {
9114
9732
  exitError("VALIDATION_FAILED", "match-rules 必须是合法的 JSON");
9115
9733
  }
9116
- import_node_fs15.mkdirSync(taskDir, { recursive: true });
9734
+ import_node_fs17.mkdirSync(taskDir, { recursive: true });
9117
9735
  const meta = {
9118
9736
  name,
9119
9737
  description: opts.description,
@@ -9123,8 +9741,8 @@ function registerNtfMonitor(ntf, ctx) {
9123
9741
  createdAt: new Date().toISOString()
9124
9742
  };
9125
9743
  writeMeta2(taskDir, meta);
9126
- import_node_fs15.writeFileSync(import_node_path14.join(taskDir, "fetch.py"), generateFetchPy(name, matchRules), "utf-8");
9127
- import_node_fs15.writeFileSync(import_node_path14.join(taskDir, "README.md"), generateReadme(name, opts.description), "utf-8");
9744
+ import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "fetch.py"), generateFetchPy(name, matchRules), "utf-8");
9745
+ import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "README.md"), generateReadme(name, opts.description), "utf-8");
9128
9746
  output({
9129
9747
  ok: true,
9130
9748
  name,
@@ -9148,8 +9766,8 @@ function registerNtfMonitor(ntf, ctx) {
9148
9766
  });
9149
9767
  });
9150
9768
  monitor.command("delete <name>").description("删除监控任务").option("--yes", "跳过确认").action((name, opts) => {
9151
- const taskDir = import_node_path14.join(tasksDir(ctx), name);
9152
- if (!import_node_fs15.existsSync(taskDir)) {
9769
+ const taskDir = import_node_path16.join(tasksDir(ctx), name);
9770
+ if (!import_node_fs17.existsSync(taskDir)) {
9153
9771
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
9154
9772
  }
9155
9773
  if (!opts.yes) {
@@ -9162,7 +9780,7 @@ function registerNtfMonitor(ntf, ctx) {
9162
9780
  });
9163
9781
  process.exit(1);
9164
9782
  }
9165
- import_node_fs15.rmSync(taskDir, { recursive: true, force: true });
9783
+ import_node_fs17.rmSync(taskDir, { recursive: true, force: true });
9166
9784
  output({
9167
9785
  ok: true,
9168
9786
  name,
@@ -9174,7 +9792,7 @@ function registerNtfMonitor(ntf, ctx) {
9174
9792
  });
9175
9793
  });
9176
9794
  monitor.command("enable <name>").description("启用监控任务").action((name) => {
9177
- const taskDir = import_node_path14.join(tasksDir(ctx), name);
9795
+ const taskDir = import_node_path16.join(tasksDir(ctx), name);
9178
9796
  const meta = readMeta3(taskDir);
9179
9797
  if (!meta)
9180
9798
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
@@ -9183,7 +9801,7 @@ function registerNtfMonitor(ntf, ctx) {
9183
9801
  output({ ok: true, name, enabled: true });
9184
9802
  });
9185
9803
  monitor.command("disable <name>").description("暂停监控任务").action((name) => {
9186
- const taskDir = import_node_path14.join(tasksDir(ctx), name);
9804
+ const taskDir = import_node_path16.join(tasksDir(ctx), name);
9187
9805
  const meta = readMeta3(taskDir);
9188
9806
  if (!meta)
9189
9807
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
@@ -9193,63 +9811,6 @@ function registerNtfMonitor(ntf, ctx) {
9193
9811
  });
9194
9812
  }
9195
9813
 
9196
- // src/light/sender.ts
9197
- var import_node_crypto4 = require("node:crypto");
9198
- async function sendLightEffect(apiKey, segments, logger, repeatInput, reason, title) {
9199
- const apiUrl = getEnvUrls().lightApiUrl;
9200
- const appKey = "phone-notifications";
9201
- const templateId = "1990771146010017800";
9202
- const resolvedTitle = resolveLightTitle(title, reason, segments);
9203
- logger?.info(`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "…" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "…" : "EMPTY"}, title=${resolvedTitle}, reason=${reason ?? ""}, segments=${JSON.stringify(segments)}`);
9204
- if (!apiUrl || !appKey || !templateId) {
9205
- return {
9206
- ok: false,
9207
- error: "灯效 API 未配置,请确认构建时已封装 OPENCLAW_HOST_* / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
9208
- };
9209
- }
9210
- let bizContent;
9211
- try {
9212
- bizContent = buildLightEffectApnsBody(segments, repeatInput, reason);
9213
- } catch (error) {
9214
- return { ok: false, error: error?.message ?? String(error) };
9215
- }
9216
- const bizUniqueId = import_node_crypto4.randomUUID();
9217
- const requestBody = {
9218
- appKey,
9219
- bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", title: resolvedTitle, reason },
9220
- bizUniqueId,
9221
- paramsMap: { bizContent },
9222
- pushType: "SPECIFY_PUSH",
9223
- templateId
9224
- };
9225
- logger?.info(`Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`);
9226
- const res = await fetch(apiUrl, {
9227
- method: "POST",
9228
- headers: {
9229
- "Content-Type": "application/json",
9230
- "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
9231
- },
9232
- body: JSON.stringify(requestBody)
9233
- });
9234
- const resBody = await res.text();
9235
- if (!res.ok) {
9236
- logger?.warn(`Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`);
9237
- return { ok: false, status: res.status, error: resBody };
9238
- }
9239
- logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
9240
- return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
9241
- }
9242
- function resolveLightTitle(title, reason, segments) {
9243
- const trimmedTitle = title?.trim();
9244
- if (trimmedTitle)
9245
- return trimmedTitle;
9246
- const trimmedReason = reason?.trim();
9247
- if (trimmedReason)
9248
- return trimmedReason;
9249
- const modeDesc = segments.map((segment) => segment.mode).join("+");
9250
- return `Effect: ${modeDesc || "custom"}`;
9251
- }
9252
-
9253
9814
  // src/cli/light-send.ts
9254
9815
  function registerLightSend(light) {
9255
9816
  light.command("send").description("发送灯效指令到硬件设备").requiredOption("--segments <json>", "灯效参数 JSON").option("--repeat", "无限循环播放(默认仅播放一轮)").option("--repeat-times <n>", "整条组合重复次数:0=无限,1=一轮;当前 ANCS 路径不支持 >=2").action(async (opts) => {
@@ -9279,8 +9840,8 @@ function registerLightSend(light) {
9279
9840
  }
9280
9841
 
9281
9842
  // src/cli/light-setup-tools.ts
9282
- var import_node_fs16 = require("node:fs");
9283
- var import_node_path15 = require("node:path");
9843
+ var import_node_fs18 = require("node:fs");
9844
+ var import_node_path17 = require("node:path");
9284
9845
  function isObject(value) {
9285
9846
  return !!value && typeof value === "object" && !Array.isArray(value);
9286
9847
  }
@@ -9326,7 +9887,7 @@ function upsertToolsForPolicy(policy, tools) {
9326
9887
  function resolveConfigPath2() {
9327
9888
  return resolveConfigPath();
9328
9889
  }
9329
- var LIGHT_TOOLS = [...LIGHT_RULE_TOOL_NAME_LIST];
9890
+ var LIGHT_TOOLS = ["light_control", ...LIGHT_RULE_TOOL_NAME_LIST];
9330
9891
  function upsertLightRuleToolsAlsoAllow(cfg) {
9331
9892
  if (!isObject(cfg.tools))
9332
9893
  cfg.tools = {};
@@ -9346,14 +9907,14 @@ function upsertLightRuleToolsAlsoAllow(cfg) {
9346
9907
  return { globalChanged, mainAgentChanged };
9347
9908
  }
9348
9909
  function registerLightSetupTools(light) {
9349
- light.command("setup").description("自动放行云端灯效规则工具(兼容 tools.allow / tools.alsoAllow)").action(() => {
9910
+ light.command("setup").description("自动放行一次性灯效与云端灯效规则工具(兼容 tools.allow / tools.alsoAllow)").action(() => {
9350
9911
  const configPath = resolveConfigPath2();
9351
- if (!import_node_fs16.existsSync(configPath)) {
9912
+ if (!import_node_fs18.existsSync(configPath)) {
9352
9913
  exitError("CONFIG_NOT_FOUND", `未找到配置文件: ${configPath}`);
9353
9914
  }
9354
9915
  let cfg = {};
9355
9916
  try {
9356
- const raw = import_node_fs16.readFileSync(configPath, "utf-8");
9917
+ const raw = import_node_fs18.readFileSync(configPath, "utf-8");
9357
9918
  const parsed = JSON.parse(raw);
9358
9919
  if (isObject(parsed))
9359
9920
  cfg = parsed;
@@ -9362,8 +9923,8 @@ function registerLightSetupTools(light) {
9362
9923
  }
9363
9924
  const result = upsertLightRuleToolsAlsoAllow(cfg);
9364
9925
  try {
9365
- import_node_fs16.mkdirSync(import_node_path15.dirname(configPath), { recursive: true });
9366
- import_node_fs16.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + `
9926
+ import_node_fs18.mkdirSync(import_node_path17.dirname(configPath), { recursive: true });
9927
+ import_node_fs18.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + `
9367
9928
  `, "utf-8");
9368
9929
  } catch (err2) {
9369
9930
  exitError("WRITE_FAILED", `写入配置失败: ${err2?.message ?? String(err2)}`);
@@ -9382,10 +9943,10 @@ function registerLightSetupTools(light) {
9382
9943
  }
9383
9944
 
9384
9945
  // src/tunnel/status.ts
9385
- var import_node_fs17 = require("node:fs");
9386
- var import_node_path16 = require("node:path");
9387
- var TUNNEL_STATUS_REL_PATH = import_node_path16.join("plugins", "phone-notifications", "tunnel-status.json");
9388
- var TUNNEL_LOCK_REL_PATH = import_node_path16.join("plugins", "phone-notifications", "relay-tunnel.lock");
9946
+ var import_node_fs19 = require("node:fs");
9947
+ var import_node_path18 = require("node:path");
9948
+ var TUNNEL_STATUS_REL_PATH = import_node_path18.join("plugins", "phone-notifications", "tunnel-status.json");
9949
+ var TUNNEL_LOCK_REL_PATH = import_node_path18.join("plugins", "phone-notifications", "relay-tunnel.lock");
9389
9950
  function isTunnelState(value) {
9390
9951
  return value === "connected" || value === "connecting" || value === "disconnected" || value === "stopped";
9391
9952
  }
@@ -9434,9 +9995,9 @@ function staleStatusMessage(status, lock) {
9434
9995
  return `${prefix},但当前运行锁启动于 ${lock.startedAt},晚于状态时间,说明状态未随新进程刷新。请重启 openclaw 主进程。`;
9435
9996
  }
9436
9997
  function assessTunnelStatus(stateDir) {
9437
- const statusFilePath = import_node_path16.join(stateDir, TUNNEL_STATUS_REL_PATH);
9438
- const lockFilePath = import_node_path16.join(stateDir, TUNNEL_LOCK_REL_PATH);
9439
- if (!import_node_fs17.existsSync(statusFilePath)) {
9998
+ const statusFilePath = import_node_path18.join(stateDir, TUNNEL_STATUS_REL_PATH);
9999
+ const lockFilePath = import_node_path18.join(stateDir, TUNNEL_LOCK_REL_PATH);
10000
+ if (!import_node_fs19.existsSync(statusFilePath)) {
9440
10001
  return {
9441
10002
  status: null,
9442
10003
  issueCode: "STATUS_NOT_FOUND",
@@ -9444,7 +10005,7 @@ function assessTunnelStatus(stateDir) {
9444
10005
  statusFilePath,
9445
10006
  lockFilePath,
9446
10007
  lock: {
9447
- exists: import_node_fs17.existsSync(lockFilePath),
10008
+ exists: import_node_fs19.existsSync(lockFilePath),
9448
10009
  pid: null,
9449
10010
  startedAt: null,
9450
10011
  active: null
@@ -9453,7 +10014,7 @@ function assessTunnelStatus(stateDir) {
9453
10014
  }
9454
10015
  let rawStatus;
9455
10016
  try {
9456
- rawStatus = JSON.parse(import_node_fs17.readFileSync(statusFilePath, "utf-8"));
10017
+ rawStatus = JSON.parse(import_node_fs19.readFileSync(statusFilePath, "utf-8"));
9457
10018
  } catch {
9458
10019
  return {
9459
10020
  status: null,
@@ -9462,7 +10023,7 @@ function assessTunnelStatus(stateDir) {
9462
10023
  statusFilePath,
9463
10024
  lockFilePath,
9464
10025
  lock: {
9465
- exists: import_node_fs17.existsSync(lockFilePath),
10026
+ exists: import_node_fs19.existsSync(lockFilePath),
9466
10027
  pid: null,
9467
10028
  startedAt: null,
9468
10029
  active: null
@@ -9477,7 +10038,7 @@ function assessTunnelStatus(stateDir) {
9477
10038
  statusFilePath,
9478
10039
  lockFilePath,
9479
10040
  lock: {
9480
- exists: import_node_fs17.existsSync(lockFilePath),
10041
+ exists: import_node_fs19.existsSync(lockFilePath),
9481
10042
  pid: null,
9482
10043
  startedAt: null,
9483
10044
  active: null
@@ -9485,11 +10046,11 @@ function assessTunnelStatus(stateDir) {
9485
10046
  };
9486
10047
  }
9487
10048
  const status = rawStatus;
9488
- const lockExists = import_node_fs17.existsSync(lockFilePath);
10049
+ const lockExists = import_node_fs19.existsSync(lockFilePath);
9489
10050
  let lockInfo = null;
9490
10051
  if (lockExists) {
9491
10052
  try {
9492
- lockInfo = parseTunnelLockInfo(JSON.parse(import_node_fs17.readFileSync(lockFilePath, "utf-8")));
10053
+ lockInfo = parseTunnelLockInfo(JSON.parse(import_node_fs19.readFileSync(lockFilePath, "utf-8")));
9493
10054
  } catch {
9494
10055
  lockInfo = null;
9495
10056
  }
@@ -9592,12 +10153,12 @@ function registerNtfStoragePath(ntf, ctx) {
9592
10153
  }
9593
10154
 
9594
10155
  // src/cli/log-search.ts
9595
- var import_node_fs18 = require("node:fs");
9596
- var import_node_path17 = require("node:path");
10156
+ var import_node_fs20 = require("node:fs");
10157
+ var import_node_path19 = require("node:path");
9597
10158
  function resolveLogsDir(ctx) {
9598
10159
  if (ctx.stateDir) {
9599
- const dir = import_node_path17.join(ctx.stateDir, "plugins", "phone-notifications", "logs");
9600
- if (import_node_fs18.existsSync(dir))
10160
+ const dir = import_node_path19.join(ctx.stateDir, "plugins", "phone-notifications", "logs");
10161
+ if (import_node_fs20.existsSync(dir))
9601
10162
  return dir;
9602
10163
  }
9603
10164
  return null;
@@ -9605,7 +10166,7 @@ function resolveLogsDir(ctx) {
9605
10166
  function listLogDateKeys(dir) {
9606
10167
  const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
9607
10168
  const keys = [];
9608
- for (const entry of import_node_fs18.readdirSync(dir, { withFileTypes: true })) {
10169
+ for (const entry of import_node_fs20.readdirSync(dir, { withFileTypes: true })) {
9609
10170
  if (!entry.isFile())
9610
10171
  continue;
9611
10172
  const m = pattern.exec(entry.name);
@@ -9615,10 +10176,10 @@ function listLogDateKeys(dir) {
9615
10176
  return keys.sort().reverse();
9616
10177
  }
9617
10178
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
9618
- const filePath = import_node_path17.join(dir, `${dateKey}.log`);
9619
- if (!import_node_fs18.existsSync(filePath))
10179
+ const filePath = import_node_path19.join(dir, `${dateKey}.log`);
10180
+ if (!import_node_fs20.existsSync(filePath))
9620
10181
  return;
9621
- const content = import_node_fs18.readFileSync(filePath, "utf-8");
10182
+ const content = import_node_fs20.readFileSync(filePath, "utf-8");
9622
10183
  const lowerKeyword = keyword?.toLowerCase();
9623
10184
  for (const line of content.split(`
9624
10185
  `)) {
@@ -9654,7 +10215,7 @@ function registerLogSearch(ntf, ctx) {
9654
10215
  }
9655
10216
 
9656
10217
  // src/cli/env.ts
9657
- var import_node_child_process3 = require("node:child_process");
10218
+ var import_node_child_process4 = require("node:child_process");
9658
10219
  var GATEWAY_RESTART_TIMEOUT_MS = 30000;
9659
10220
  var NOOP_LOGGER = {
9660
10221
  info() {},
@@ -9705,7 +10266,7 @@ function triggerGatewayReloadAfterEnvSwitch(deps = {}) {
9705
10266
  }
9706
10267
  const hostCli = deps.hostCli?.trim() || process.env.HOST_CLI?.trim() || "openclaw";
9707
10268
  const command = `${hostCli} gateway restart`;
9708
- const run = deps.spawnSync ?? import_node_child_process3.spawnSync;
10269
+ const run = deps.spawnSync ?? import_node_child_process4.spawnSync;
9709
10270
  const result = run(hostCli, ["gateway", "restart"], {
9710
10271
  encoding: "utf-8",
9711
10272
  stdio: "pipe",
@@ -9759,11 +10320,11 @@ function registerEnvCli(ntf, deps = {}) {
9759
10320
  }
9760
10321
 
9761
10322
  // src/cli/doctor.ts
9762
- var import_node_fs21 = require("node:fs");
10323
+ var import_node_fs23 = require("node:fs");
9763
10324
  var import_node_readline = require("node:readline");
9764
10325
 
9765
10326
  // src/cli/doctor/check-dangerous-flags.ts
9766
- var import_node_fs19 = require("node:fs");
10327
+ var import_node_fs21 = require("node:fs");
9767
10328
  function isObject2(v) {
9768
10329
  return !!v && typeof v === "object" && !Array.isArray(v);
9769
10330
  }
@@ -9783,13 +10344,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
9783
10344
  detail: "这会关闭 Control UI 的设备身份验证,任何人都可以访问控制面板。",
9784
10345
  fixDescription: "设为 false",
9785
10346
  fix: () => {
9786
- const raw = import_node_fs19.readFileSync(configPath, "utf-8");
10347
+ const raw = import_node_fs21.readFileSync(configPath, "utf-8");
9787
10348
  const config = JSON.parse(raw);
9788
10349
  const gw = config.gateway;
9789
10350
  const cui = gw.controlUi;
9790
10351
  cui.dangerouslyDisableDeviceAuth = false;
9791
- import_node_fs19.copyFileSync(configPath, configPath + ".bak");
9792
- import_node_fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
10352
+ import_node_fs21.copyFileSync(configPath, configPath + ".bak");
10353
+ import_node_fs21.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
9793
10354
  `, "utf-8");
9794
10355
  }
9795
10356
  };
@@ -9846,11 +10407,11 @@ function warnEmpty() {
9846
10407
  }
9847
10408
 
9848
10409
  // src/cli/doctor/check-state-dir-perms.ts
9849
- var import_node_fs20 = require("node:fs");
10410
+ var import_node_fs22 = require("node:fs");
9850
10411
  var checkStateDirPerms = ({ stateDir }) => {
9851
10412
  let mode;
9852
10413
  try {
9853
- mode = import_node_fs20.statSync(stateDir).mode;
10414
+ mode = import_node_fs22.statSync(stateDir).mode;
9854
10415
  } catch {
9855
10416
  return null;
9856
10417
  }
@@ -9865,7 +10426,7 @@ var checkStateDirPerms = ({ stateDir }) => {
9865
10426
  detail: "其他用户可以读取该目录下的凭证和配置文件。",
9866
10427
  fixDescription: "chmod 700 " + stateDir,
9867
10428
  fix: () => {
9868
- import_node_fs20.chmodSync(stateDir, 448);
10429
+ import_node_fs22.chmodSync(stateDir, 448);
9869
10430
  }
9870
10431
  };
9871
10432
  };
@@ -9970,6 +10531,180 @@ var checkTunnel = ({ stateDir }) => {
9970
10531
  };
9971
10532
  };
9972
10533
 
10534
+ // src/cli/doctor/check-local-gateway.ts
10535
+ var import_node_net = __toESM(require("node:net"));
10536
+
10537
+ // src/tunnel/gateway-target.ts
10538
+ var DEFAULT_GATEWAY_PORT = "18789";
10539
+ var DEFAULT_GATEWAY_HOST = "127.0.0.1";
10540
+ var BASE_URL_ENV_KEYS = [
10541
+ "PHONE_NOTIFICATIONS_GATEWAY_BASE_URL",
10542
+ "OPENCLAW_GATEWAY_BASE_URL",
10543
+ "QCLAW_GATEWAY_BASE_URL"
10544
+ ];
10545
+ var PORT_ENV_KEYS = [
10546
+ "OPENCLAW_GATEWAY_PORT",
10547
+ "QCLAW_GATEWAY_PORT"
10548
+ ];
10549
+ function trimToUndefined3(value) {
10550
+ if (typeof value !== "string")
10551
+ return;
10552
+ const trimmed = value.trim();
10553
+ return trimmed || undefined;
10554
+ }
10555
+ function firstEnv(env, keys) {
10556
+ for (const key of keys) {
10557
+ const value = trimToUndefined3(env[key]);
10558
+ if (value)
10559
+ return { key, value };
10560
+ }
10561
+ return;
10562
+ }
10563
+ function normalizePort(value) {
10564
+ const raw = typeof value === "number" && Number.isInteger(value) ? String(value) : trimToUndefined3(value);
10565
+ if (!raw || !/^\d+$/.test(raw))
10566
+ return;
10567
+ const port = Number.parseInt(raw, 10);
10568
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
10569
+ return;
10570
+ }
10571
+ return String(port);
10572
+ }
10573
+ function normalizeBaseUrl(raw) {
10574
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
10575
+ let parsed;
10576
+ try {
10577
+ parsed = new URL(withScheme);
10578
+ } catch {
10579
+ return;
10580
+ }
10581
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
10582
+ return;
10583
+ }
10584
+ if (!parsed.hostname)
10585
+ return;
10586
+ return `${parsed.protocol}//${parsed.host}`;
10587
+ }
10588
+ function isGatewayTlsEnabled(config) {
10589
+ return config.gateway?.tls?.enabled === true;
10590
+ }
10591
+ function resolveGatewayPort(params) {
10592
+ const envPort = firstEnv(params.env, PORT_ENV_KEYS);
10593
+ const normalizedEnvPort = envPort ? normalizePort(envPort.value) : undefined;
10594
+ if (envPort && normalizedEnvPort) {
10595
+ return { port: normalizedEnvPort, source: envPort.key };
10596
+ }
10597
+ const configPort = normalizePort(params.config.gateway?.port);
10598
+ if (configPort) {
10599
+ return { port: configPort, source: "openclaw.json gateway.port" };
10600
+ }
10601
+ return { port: DEFAULT_GATEWAY_PORT, source: "default" };
10602
+ }
10603
+ function resolveLocalGatewayBaseUrl(params = {}) {
10604
+ const config = params.config ?? {};
10605
+ const env = params.env ?? process.env;
10606
+ const tlsEnabled = isGatewayTlsEnabled(config);
10607
+ const envBaseUrl = firstEnv(env, BASE_URL_ENV_KEYS);
10608
+ if (envBaseUrl) {
10609
+ const normalized = normalizeBaseUrl(envBaseUrl.value);
10610
+ if (normalized) {
10611
+ return {
10612
+ baseUrl: normalized,
10613
+ source: envBaseUrl.key,
10614
+ explicit: true,
10615
+ tlsEnabled
10616
+ };
10617
+ }
10618
+ }
10619
+ const { port, source } = resolveGatewayPort({ config, env });
10620
+ const baseUrl = `http://${DEFAULT_GATEWAY_HOST}:${port}`;
10621
+ const warning = envBaseUrl ? `忽略无效的 ${envBaseUrl.key}=${envBaseUrl.value}` : undefined;
10622
+ return {
10623
+ baseUrl,
10624
+ source,
10625
+ explicit: false,
10626
+ tlsEnabled,
10627
+ warning
10628
+ };
10629
+ }
10630
+ function resolveLocalGatewayTcpTarget(baseUrl) {
10631
+ let parsed;
10632
+ try {
10633
+ parsed = new URL(baseUrl);
10634
+ } catch {
10635
+ return null;
10636
+ }
10637
+ const port = parsed.port ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : parsed.protocol === "http:" ? 80 : Number.NaN;
10638
+ if (!parsed.hostname || !Number.isInteger(port))
10639
+ return null;
10640
+ return { host: parsed.hostname, port };
10641
+ }
10642
+
10643
+ // src/cli/doctor/check-local-gateway.ts
10644
+ var PROBE_TIMEOUT_MS = 1200;
10645
+ function probeTcp(host, port) {
10646
+ return new Promise((resolve3) => {
10647
+ const socket = import_node_net.default.connect({ host, port });
10648
+ let settled = false;
10649
+ const finish = (result) => {
10650
+ if (settled)
10651
+ return;
10652
+ settled = true;
10653
+ socket.destroy();
10654
+ resolve3(result);
10655
+ };
10656
+ socket.setTimeout(PROBE_TIMEOUT_MS);
10657
+ socket.once("connect", () => finish({ ok: true }));
10658
+ socket.once("timeout", () => finish({ ok: false, error: "timeout" }));
10659
+ socket.once("error", (err2) => finish({ ok: false, error: err2.message }));
10660
+ });
10661
+ }
10662
+ var checkLocalGateway = async ({ cfg }) => {
10663
+ const resolution = resolveLocalGatewayBaseUrl({ config: cfg });
10664
+ if (resolution.warning) {
10665
+ return {
10666
+ id: "local-gateway",
10667
+ severity: "warn",
10668
+ title: "本地 Gateway 地址配置无效",
10669
+ detail: `${resolution.warning},当前回退到 ${resolution.baseUrl}`,
10670
+ fixDescription: "请修正 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL,或清理该环境变量后重启 openclaw/gateway",
10671
+ fix: null
10672
+ };
10673
+ }
10674
+ if (resolution.tlsEnabled && !resolution.explicit) {
10675
+ return {
10676
+ id: "local-gateway",
10677
+ severity: "warn",
10678
+ title: "Gateway TLS 可能阻断 Relay 本地代理",
10679
+ detail: `检测到 gateway.tls.enabled=true,但插件本地代理默认连接 ${resolution.baseUrl}。` + "如果 Gateway 只接受 HTTPS/WSS,本地 RPC 握手会失败。",
10680
+ fixDescription: "设置 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=https://127.0.0.1:<port>,或关闭 gateway.tls.enabled 后重启 gateway",
10681
+ fix: null
10682
+ };
10683
+ }
10684
+ const target = resolveLocalGatewayTcpTarget(resolution.baseUrl);
10685
+ if (!target) {
10686
+ return {
10687
+ id: "local-gateway",
10688
+ severity: "warn",
10689
+ title: "本地 Gateway 地址无法解析",
10690
+ detail: `无法解析插件将使用的本地 Gateway 地址: ${resolution.baseUrl}`,
10691
+ fixDescription: "请设置 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=http://127.0.0.1:<port>",
10692
+ fix: null
10693
+ };
10694
+ }
10695
+ const probe = await probeTcp(target.host, target.port);
10696
+ if (probe.ok)
10697
+ return null;
10698
+ return {
10699
+ id: "local-gateway",
10700
+ severity: "warn",
10701
+ title: "本地 Gateway 不可达",
10702
+ detail: `插件将连接 ${resolution.baseUrl},但 TCP 探测失败` + (probe.error ? `: ${probe.error}` : ""),
10703
+ fixDescription: "请执行 openclaw gateway status,确认 gateway.port 与 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL 是否一致",
10704
+ fix: null
10705
+ };
10706
+ };
10707
+
9973
10708
  // src/cli/doctor/check-node-version.ts
9974
10709
  var MIN_MAJOR = 22;
9975
10710
  var MIN_MINOR = 12;
@@ -10029,6 +10764,7 @@ var allCheckers = [
10029
10764
  checkStateDirPerms,
10030
10765
  checkToolPolicy,
10031
10766
  checkTunnel,
10767
+ checkLocalGateway,
10032
10768
  checkNodeVersion,
10033
10769
  checkPluginVersion
10034
10770
  ];
@@ -10038,10 +10774,10 @@ function isObject5(v) {
10038
10774
  return !!v && typeof v === "object" && !Array.isArray(v);
10039
10775
  }
10040
10776
  function readConfig(configPath) {
10041
- if (!import_node_fs21.existsSync(configPath))
10777
+ if (!import_node_fs23.existsSync(configPath))
10042
10778
  return {};
10043
10779
  try {
10044
- const parsed = JSON.parse(import_node_fs21.readFileSync(configPath, "utf-8"));
10780
+ const parsed = JSON.parse(import_node_fs23.readFileSync(configPath, "utf-8"));
10045
10781
  return isObject5(parsed) ? parsed : {};
10046
10782
  } catch {
10047
10783
  return {};
@@ -10252,7 +10988,7 @@ function registerRecStoragePath(rec, ctx) {
10252
10988
 
10253
10989
  // src/cli/rec-setup.ts
10254
10990
  var import_node_readline2 = require("node:readline");
10255
- var import_node_fs22 = require("node:fs");
10991
+ var import_node_fs24 = require("node:fs");
10256
10992
  function ask(rl, question) {
10257
10993
  return new Promise((resolve3) => rl.question(question, resolve3));
10258
10994
  }
@@ -10353,9 +11089,9 @@ async function setupLocal(rl) {
10353
11089
  function registerRecSetup(rec, ctx) {
10354
11090
  rec.command("setup").description("交互式配置 ASR 转写参数,保存到本地配置文件").action(async () => {
10355
11091
  const configPath = resolveAsrConfigPath(ctx);
10356
- if (import_node_fs22.existsSync(configPath)) {
11092
+ if (import_node_fs24.existsSync(configPath)) {
10357
11093
  try {
10358
- const existing = JSON.parse(import_node_fs22.readFileSync(configPath, "utf-8"));
11094
+ const existing = JSON.parse(import_node_fs24.readFileSync(configPath, "utf-8"));
10359
11095
  process.stderr.write(`当前已有配置:mode = ${existing.mode}`);
10360
11096
  if (existing.updatedAt)
10361
11097
  process.stderr.write(`,更新于 ${existing.updatedAt}`);
@@ -10371,7 +11107,7 @@ function registerRecSetup(rec, ctx) {
10371
11107
  const modeIdx = await askChoice(rl, "选择模式", ["api(云端 model-proxy 长录音)", "local(本地 Whisper)"]);
10372
11108
  const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
10373
11109
  const stored = { ...config, updatedAt: new Date().toISOString() };
10374
- import_node_fs22.writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
11110
+ import_node_fs24.writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
10375
11111
  process.stderr.write(`
10376
11112
  ✓ 配置已保存到 ${configPath}
10377
11113
 
@@ -10467,9 +11203,9 @@ function registerImageStoragePath(image, ctx) {
10467
11203
  }
10468
11204
 
10469
11205
  // src/cli/update.ts
10470
- var import_node_child_process4 = require("node:child_process");
10471
- var import_node_fs23 = require("node:fs");
10472
- var import_node_path18 = require("node:path");
11206
+ var import_node_child_process5 = require("node:child_process");
11207
+ var import_node_fs25 = require("node:fs");
11208
+ var import_node_path20 = require("node:path");
10473
11209
  var import_node_os2 = __toESM(require("node:os"));
10474
11210
  async function fetchText(url) {
10475
11211
  const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
@@ -10542,9 +11278,9 @@ async function runUpdate(ctx, opts) {
10542
11278
  `);
10543
11279
  process.exit(1);
10544
11280
  }
10545
- const tmpScript = import_node_path18.join(import_node_os2.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
11281
+ const tmpScript = import_node_path20.join(import_node_os2.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
10546
11282
  try {
10547
- import_node_fs23.writeFileSync(tmpScript, installScript, "utf-8");
11283
+ import_node_fs25.writeFileSync(tmpScript, installScript, "utf-8");
10548
11284
  } catch (err2) {
10549
11285
  const msg = `写入临时文件失败: ${err2?.message ?? String(err2)}`;
10550
11286
  if (json) {
@@ -10556,9 +11292,9 @@ async function runUpdate(ctx, opts) {
10556
11292
  process.exit(1);
10557
11293
  }
10558
11294
  const stateDir = resolveStateDir(ctx.stateDir);
10559
- const result = import_node_child_process4.spawnSync(process.execPath, [tmpScript, "--version", latest, "--state-dir", stateDir], { stdio: "inherit" });
11295
+ const result = import_node_child_process5.spawnSync(process.execPath, [tmpScript, "--version", latest, "--state-dir", stateDir], { stdio: "inherit" });
10560
11296
  try {
10561
- import_node_fs23.unlinkSync(tmpScript);
11297
+ import_node_fs25.unlinkSync(tmpScript);
10562
11298
  } catch {}
10563
11299
  if (result.error) {
10564
11300
  const msg = `安装脚本执行失败: ${result.error.message}`;
@@ -10632,10 +11368,10 @@ function inferOpenClawRootDir(workspaceDir) {
10632
11368
  if (!workspaceDir) {
10633
11369
  return;
10634
11370
  }
10635
- if (import_node_path19.basename(workspaceDir) !== "workspace") {
11371
+ if (import_node_path21.basename(workspaceDir) !== "workspace") {
10636
11372
  return;
10637
11373
  }
10638
- return import_node_path19.dirname(workspaceDir);
11374
+ return import_node_path21.dirname(workspaceDir);
10639
11375
  }
10640
11376
  function registerPluginCli(api, params) {
10641
11377
  const { logger, openclawDir } = params;
@@ -10655,12 +11391,12 @@ function registerPluginCli(api, params) {
10655
11391
  }
10656
11392
 
10657
11393
  // src/plugin/ntf-bin.ts
10658
- var import_node_fs24 = require("node:fs");
10659
- var import_node_path20 = require("node:path");
11394
+ var import_node_fs26 = require("node:fs");
11395
+ var import_node_path22 = require("node:path");
10660
11396
  var import_node_process = __toESM(require("node:process"));
10661
11397
  function ensureNtfBinInstalled(opts) {
10662
11398
  const { ntfCjsPath, stateDir, logger } = opts;
10663
- if (!import_node_fs24.existsSync(ntfCjsPath)) {
11399
+ if (!import_node_fs26.existsSync(ntfCjsPath)) {
10664
11400
  logger.warn(`ntf wrapper: 目标不存在 ${ntfCjsPath},跳过`);
10665
11401
  return;
10666
11402
  }
@@ -10688,29 +11424,29 @@ function resolveNtfBinInstallDirs(opts) {
10688
11424
  const envPath = opts.envPath ?? import_node_process.default.env.PATH ?? "";
10689
11425
  const pathDirs = splitPathDirs(envPath);
10690
11426
  const candidates = [];
10691
- pushCandidate(candidates, import_node_path20.dirname(import_node_process.default.execPath), "node 同目录", true);
11427
+ pushCandidate(candidates, import_node_path22.dirname(import_node_process.default.execPath), "node 同目录", true);
10692
11428
  for (const cliName of inferHostCliNames(argv)) {
10693
11429
  const cliPath = findCommandOnPath(cliName, pathDirs);
10694
11430
  if (cliPath) {
10695
- pushCandidate(candidates, import_node_path20.dirname(cliPath), `宿主 CLI (${cliName}) 同目录`, true);
11431
+ pushCandidate(candidates, import_node_path22.dirname(cliPath), `宿主 CLI (${cliName}) 同目录`, true);
10696
11432
  }
10697
11433
  }
10698
11434
  for (const dir of pathDirs) {
10699
11435
  pushCandidate(candidates, dir, "PATH 目录", true);
10700
11436
  }
10701
11437
  if (opts.stateDir) {
10702
- pushCandidate(candidates, import_node_path20.join(opts.stateDir, "bin"), "stateDir/bin 兜底", false);
11438
+ pushCandidate(candidates, import_node_path22.join(opts.stateDir, "bin"), "stateDir/bin 兜底", false);
10703
11439
  }
10704
11440
  return candidates;
10705
11441
  }
10706
11442
  function tryInstallWrapper(dir, target) {
10707
11443
  try {
10708
- import_node_fs24.mkdirSync(dir, { recursive: true });
11444
+ import_node_fs26.mkdirSync(dir, { recursive: true });
10709
11445
  } catch (err2) {
10710
11446
  return { ok: false, reason: `mkdir ${dir} 失败: ${describeError(err2)}` };
10711
11447
  }
10712
11448
  const wrapperName = import_node_process.default.platform === "win32" ? "ntf.cmd" : "ntf";
10713
- const wrapperPath = import_node_path20.join(dir, wrapperName);
11449
+ const wrapperPath = import_node_path22.join(dir, wrapperName);
10714
11450
  if (pathExists(wrapperPath)) {
10715
11451
  if (isExistingWrapperForTarget(wrapperPath, target)) {
10716
11452
  return { ok: true, path: wrapperPath, created: false };
@@ -10719,13 +11455,13 @@ function tryInstallWrapper(dir, target) {
10719
11455
  }
10720
11456
  try {
10721
11457
  if (import_node_process.default.platform === "win32") {
10722
- import_node_fs24.writeFileSync(wrapperPath, `@echo off\r
11458
+ import_node_fs26.writeFileSync(wrapperPath, `@echo off\r
10723
11459
  node "${target}" %*\r
10724
11460
  `, "utf-8");
10725
11461
  } else {
10726
- import_node_fs24.symlinkSync(target, wrapperPath);
11462
+ import_node_fs26.symlinkSync(target, wrapperPath);
10727
11463
  try {
10728
- import_node_fs24.chmodSync(target, 493);
11464
+ import_node_fs26.chmodSync(target, 493);
10729
11465
  } catch {}
10730
11466
  }
10731
11467
  return { ok: true, path: wrapperPath, created: true };
@@ -10735,7 +11471,7 @@ node "${target}" %*\r
10735
11471
  }
10736
11472
  function pathExists(path) {
10737
11473
  try {
10738
- import_node_fs24.lstatSync(path);
11474
+ import_node_fs26.lstatSync(path);
10739
11475
  return true;
10740
11476
  } catch {
10741
11477
  return false;
@@ -10744,16 +11480,16 @@ function pathExists(path) {
10744
11480
  function isExistingWrapperForTarget(wrapperPath, target) {
10745
11481
  if (import_node_process.default.platform === "win32") {
10746
11482
  try {
10747
- return import_node_fs24.readFileSync(wrapperPath, "utf-8").includes(`"${target}"`);
11483
+ return import_node_fs26.readFileSync(wrapperPath, "utf-8").includes(`"${target}"`);
10748
11484
  } catch {
10749
11485
  return false;
10750
11486
  }
10751
11487
  }
10752
11488
  try {
10753
- if (!import_node_fs24.lstatSync(wrapperPath).isSymbolicLink()) {
11489
+ if (!import_node_fs26.lstatSync(wrapperPath).isSymbolicLink()) {
10754
11490
  return false;
10755
11491
  }
10756
- return import_node_fs24.realpathSync(wrapperPath) === import_node_fs24.realpathSync(target);
11492
+ return import_node_fs26.realpathSync(wrapperPath) === import_node_fs26.realpathSync(target);
10757
11493
  } catch {
10758
11494
  return false;
10759
11495
  }
@@ -10764,7 +11500,7 @@ function describeError(err2) {
10764
11500
  return String(err2);
10765
11501
  }
10766
11502
  function splitPathDirs(envPath) {
10767
- return envPath.split(import_node_path20.delimiter).map((entry) => entry.trim()).filter((entry) => entry && import_node_path20.isAbsolute(entry));
11503
+ return envPath.split(import_node_path22.delimiter).map((entry) => entry.trim()).filter((entry) => entry && import_node_path22.isAbsolute(entry));
10768
11504
  }
10769
11505
  function inferHostCliNames(argv) {
10770
11506
  const names = ["openclaw", "jvsclaw", "qclaw"];
@@ -10778,8 +11514,8 @@ function findCommandOnPath(command, pathDirs) {
10778
11514
  const names = import_node_process.default.platform === "win32" && !command.toLowerCase().endsWith(".cmd") ? [command, `${command}.cmd`, `${command}.exe`] : [command];
10779
11515
  for (const dir of pathDirs) {
10780
11516
  for (const name of names) {
10781
- const candidate = import_node_path20.join(dir, name);
10782
- if (import_node_fs24.existsSync(candidate)) {
11517
+ const candidate = import_node_path22.join(dir, name);
11518
+ if (import_node_fs26.existsSync(candidate)) {
10783
11519
  return candidate;
10784
11520
  }
10785
11521
  }
@@ -10794,11 +11530,11 @@ function pushCandidate(candidates, dir, label, onPath) {
10794
11530
  }
10795
11531
 
10796
11532
  // src/plugin/lifecycle.ts
10797
- var import_node_fs34 = require("node:fs");
11533
+ var import_node_fs36 = require("node:fs");
10798
11534
 
10799
11535
  // src/notification/app-name-map.ts
10800
- var import_node_fs25 = require("node:fs");
10801
- var import_node_path21 = require("node:path");
11536
+ var import_node_fs27 = require("node:fs");
11537
+ var import_node_path23 = require("node:path");
10802
11538
  var PLUGIN_STATE_DIR = "phone-notifications";
10803
11539
  var CACHE_FILE = "app-name-map.json";
10804
11540
  var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
@@ -10819,7 +11555,7 @@ function isAppNameMapApiResponse(v) {
10819
11555
  return Array.isArray(o.data) && o.data.every((item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string");
10820
11556
  }
10821
11557
  function getCachePath(stateDir) {
10822
- return import_node_path21.join(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
11558
+ return import_node_path23.join(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
10823
11559
  }
10824
11560
  function createAppNameMapProvider(opts) {
10825
11561
  const { stateDir, logger } = opts;
@@ -10831,10 +11567,10 @@ function createAppNameMapProvider(opts) {
10831
11567
  let inFlightFetch = null;
10832
11568
  function loadFromDisk() {
10833
11569
  const path = getCachePath(stateDir);
10834
- if (!import_node_fs25.existsSync(path))
11570
+ if (!import_node_fs27.existsSync(path))
10835
11571
  return;
10836
11572
  try {
10837
- const raw = JSON.parse(import_node_fs25.readFileSync(path, "utf-8"));
11573
+ const raw = JSON.parse(import_node_fs27.readFileSync(path, "utf-8"));
10838
11574
  if (!isRecordOfStrings(raw))
10839
11575
  return;
10840
11576
  map.clear();
@@ -10880,10 +11616,10 @@ function createAppNameMapProvider(opts) {
10880
11616
  logger.warn("[app-name-map] refresh succeeded but got 0 entries");
10881
11617
  return;
10882
11618
  }
10883
- const dir = import_node_path21.join(stateDir, "plugins", PLUGIN_STATE_DIR);
10884
- import_node_fs25.mkdirSync(dir, { recursive: true });
11619
+ const dir = import_node_path23.join(stateDir, "plugins", PLUGIN_STATE_DIR);
11620
+ import_node_fs27.mkdirSync(dir, { recursive: true });
10885
11621
  const cachePath = getCachePath(stateDir);
10886
- import_node_fs25.writeFileSync(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
11622
+ import_node_fs27.writeFileSync(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
10887
11623
  logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
10888
11624
  } catch (e) {
10889
11625
  const message = e instanceof Error ? e.message : String(e);
@@ -10934,8 +11670,8 @@ function createAppNameMapProvider(opts) {
10934
11670
  }
10935
11671
 
10936
11672
  // src/recording/storage.ts
10937
- var import_node_fs26 = require("node:fs");
10938
- var import_node_path22 = require("node:path");
11673
+ var import_node_fs28 = require("node:fs");
11674
+ var import_node_path24 = require("node:path");
10939
11675
 
10940
11676
  // src/recording/state-machine.ts
10941
11677
  var VALID_TRANSITIONS = new Map([
@@ -11199,7 +11935,7 @@ function stripMarkdownFence(markdown) {
11199
11935
  function deriveTitleFromTranscriptPath(transcriptFile, recordingId) {
11200
11936
  if (!transcriptFile)
11201
11937
  return;
11202
- const name = import_node_path22.basename(transcriptFile, ".md");
11938
+ const name = import_node_path24.basename(transcriptFile, ".md");
11203
11939
  const prefix = `${recordingId}_`;
11204
11940
  if (name.startsWith(prefix)) {
11205
11941
  const derived = name.slice(prefix.length).trim();
@@ -11241,16 +11977,16 @@ function extractTranscriptContent(markdown) {
11241
11977
  `).trim();
11242
11978
  }
11243
11979
  function resolveRecordingStorageDir(ctx, logger) {
11244
- const stateRecDir = import_node_path22.join(ctx.stateDir, "plugins", "phone-notifications", RECORDINGS_DIR);
11980
+ const stateRecDir = import_node_path24.join(ctx.stateDir, "plugins", "phone-notifications", RECORDINGS_DIR);
11245
11981
  try {
11246
- import_node_fs26.mkdirSync(stateRecDir, { recursive: true });
11982
+ import_node_fs28.mkdirSync(stateRecDir, { recursive: true });
11247
11983
  logger.info(`录音将写入 stateDir 路径: ${stateRecDir}`);
11248
11984
  return stateRecDir;
11249
11985
  } catch {}
11250
11986
  if (ctx.workspaceDir) {
11251
- const wsRecDir = import_node_path22.join(ctx.workspaceDir, RECORDINGS_DIR);
11987
+ const wsRecDir = import_node_path24.join(ctx.workspaceDir, RECORDINGS_DIR);
11252
11988
  try {
11253
- import_node_fs26.mkdirSync(wsRecDir, { recursive: true });
11989
+ import_node_fs28.mkdirSync(wsRecDir, { recursive: true });
11254
11990
  logger.warn(`stateDir 不可用,录音已回退到 workspace 路径: ${wsRecDir}`);
11255
11991
  return wsRecDir;
11256
11992
  } catch {}
@@ -11270,17 +12006,17 @@ class RecordingStorage {
11270
12006
  constructor(dir, logger) {
11271
12007
  this.logger = logger;
11272
12008
  this.dir = dir;
11273
- this.audioDir = import_node_path22.join(dir, AUDIO_DIR);
11274
- this.transcriptDataDir = import_node_path22.join(dir, TRANSCRIPT_DATA_DIR);
11275
- this.transcriptsDir = import_node_path22.join(dir, TRANSCRIPTS_DIR);
11276
- this.summariesDir = import_node_path22.join(dir, SUMMARIES_DIR2);
11277
- this.indexPath = import_node_path22.join(dir, INDEX_FILE);
12009
+ this.audioDir = import_node_path24.join(dir, AUDIO_DIR);
12010
+ this.transcriptDataDir = import_node_path24.join(dir, TRANSCRIPT_DATA_DIR);
12011
+ this.transcriptsDir = import_node_path24.join(dir, TRANSCRIPTS_DIR);
12012
+ this.summariesDir = import_node_path24.join(dir, SUMMARIES_DIR2);
12013
+ this.indexPath = import_node_path24.join(dir, INDEX_FILE);
11278
12014
  }
11279
12015
  async init() {
11280
- import_node_fs26.mkdirSync(this.audioDir, { recursive: true });
11281
- import_node_fs26.mkdirSync(this.transcriptDataDir, { recursive: true });
11282
- import_node_fs26.mkdirSync(this.transcriptsDir, { recursive: true });
11283
- import_node_fs26.mkdirSync(this.summariesDir, { recursive: true });
12016
+ import_node_fs28.mkdirSync(this.audioDir, { recursive: true });
12017
+ import_node_fs28.mkdirSync(this.transcriptDataDir, { recursive: true });
12018
+ import_node_fs28.mkdirSync(this.transcriptsDir, { recursive: true });
12019
+ import_node_fs28.mkdirSync(this.summariesDir, { recursive: true });
11284
12020
  this.loadIndex();
11285
12021
  this.logger.info(`录音存储已初始化: ${this.dir}(共 ${this.index.recordings.length} 条记录)`);
11286
12022
  }
@@ -11312,13 +12048,13 @@ class RecordingStorage {
11312
12048
  return id;
11313
12049
  }
11314
12050
  if (existing.transcriptDataFile) {
11315
- import_node_fs26.rmSync(import_node_path22.join(this.dir, existing.transcriptDataFile), { force: true });
12051
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.transcriptDataFile), { force: true });
11316
12052
  }
11317
12053
  if (existing.transcriptFile) {
11318
- import_node_fs26.rmSync(import_node_path22.join(this.dir, existing.transcriptFile), { force: true });
12054
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.transcriptFile), { force: true });
11319
12055
  }
11320
12056
  if (existing.summaryFile) {
11321
- import_node_fs26.rmSync(import_node_path22.join(this.dir, existing.summaryFile), { force: true });
12057
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.summaryFile), { force: true });
11322
12058
  }
11323
12059
  existing.metadata = metadata;
11324
12060
  existing.clientLabel = clientLabel;
@@ -11375,7 +12111,7 @@ class RecordingStorage {
11375
12111
  return;
11376
12112
  const nextAudioFile = `${AUDIO_DIR}/${filename}`;
11377
12113
  if (entry.audioFile && entry.audioFile !== nextAudioFile) {
11378
- import_node_fs26.rmSync(import_node_path22.join(this.dir, entry.audioFile), { force: true });
12114
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.audioFile), { force: true });
11379
12115
  }
11380
12116
  entry.audioFile = nextAudioFile;
11381
12117
  entry.updatedAt = new Date().toISOString();
@@ -11395,7 +12131,7 @@ class RecordingStorage {
11395
12131
  return;
11396
12132
  const nextTranscriptDataFile = `${TRANSCRIPT_DATA_DIR}/${filename}`;
11397
12133
  if (entry.transcriptDataFile && entry.transcriptDataFile !== nextTranscriptDataFile) {
11398
- import_node_fs26.rmSync(import_node_path22.join(this.dir, entry.transcriptDataFile), { force: true });
12134
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.transcriptDataFile), { force: true });
11399
12135
  }
11400
12136
  entry.transcriptDataFile = nextTranscriptDataFile;
11401
12137
  entry.updatedAt = new Date().toISOString();
@@ -11407,7 +12143,7 @@ class RecordingStorage {
11407
12143
  return;
11408
12144
  const nextTranscriptFile = `${TRANSCRIPTS_DIR}/${filename}`;
11409
12145
  if (entry.transcriptFile && entry.transcriptFile !== nextTranscriptFile) {
11410
- import_node_fs26.rmSync(import_node_path22.join(this.dir, entry.transcriptFile), { force: true });
12146
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.transcriptFile), { force: true });
11411
12147
  }
11412
12148
  entry.transcriptFile = nextTranscriptFile;
11413
12149
  entry.updatedAt = new Date().toISOString();
@@ -11419,7 +12155,7 @@ class RecordingStorage {
11419
12155
  return;
11420
12156
  const nextSummaryFile = `${SUMMARIES_DIR2}/${filename}`;
11421
12157
  if (entry.summaryFile && entry.summaryFile !== nextSummaryFile) {
11422
- import_node_fs26.rmSync(import_node_path22.join(this.dir, entry.summaryFile), { force: true });
12158
+ import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.summaryFile), { force: true });
11423
12159
  }
11424
12160
  entry.summaryFile = nextSummaryFile;
11425
12161
  entry.updatedAt = new Date().toISOString();
@@ -11508,24 +12244,24 @@ class RecordingStorage {
11508
12244
  if (!entry)
11509
12245
  return false;
11510
12246
  if (entry.audioFile) {
11511
- const audioPath = import_node_path22.join(this.dir, entry.audioFile);
11512
- import_node_fs26.rmSync(audioPath, { force: true });
12247
+ const audioPath = import_node_path24.join(this.dir, entry.audioFile);
12248
+ import_node_fs28.rmSync(audioPath, { force: true });
11513
12249
  }
11514
12250
  if (entry.srtFile) {
11515
- const srtPath = import_node_path22.join(this.dir, entry.srtFile);
11516
- import_node_fs26.rmSync(srtPath, { force: true });
12251
+ const srtPath = import_node_path24.join(this.dir, entry.srtFile);
12252
+ import_node_fs28.rmSync(srtPath, { force: true });
11517
12253
  }
11518
12254
  if (entry.transcriptDataFile) {
11519
- const transcriptDataPath = import_node_path22.join(this.dir, entry.transcriptDataFile);
11520
- import_node_fs26.rmSync(transcriptDataPath, { force: true });
12255
+ const transcriptDataPath = import_node_path24.join(this.dir, entry.transcriptDataFile);
12256
+ import_node_fs28.rmSync(transcriptDataPath, { force: true });
11521
12257
  }
11522
12258
  if (entry.transcriptFile) {
11523
- const transcriptPath = import_node_path22.join(this.dir, entry.transcriptFile);
11524
- import_node_fs26.rmSync(transcriptPath, { force: true });
12259
+ const transcriptPath = import_node_path24.join(this.dir, entry.transcriptFile);
12260
+ import_node_fs28.rmSync(transcriptPath, { force: true });
11525
12261
  }
11526
12262
  if (entry.summaryFile) {
11527
- const summaryPath = import_node_path22.join(this.dir, entry.summaryFile);
11528
- import_node_fs26.rmSync(summaryPath, { force: true });
12263
+ const summaryPath = import_node_path24.join(this.dir, entry.summaryFile);
12264
+ import_node_fs28.rmSync(summaryPath, { force: true });
11529
12265
  }
11530
12266
  if (opts?.localOnly) {
11531
12267
  entry.audioFile = undefined;
@@ -11561,24 +12297,24 @@ class RecordingStorage {
11561
12297
  return `${recordingId}.md`;
11562
12298
  }
11563
12299
  getAudioFilePath(recordingId, ossUrl) {
11564
- return import_node_path22.join(this.audioDir, this.buildAudioFilename(recordingId, ossUrl));
12300
+ return import_node_path24.join(this.audioDir, this.buildAudioFilename(recordingId, ossUrl));
11565
12301
  }
11566
12302
  getSrtFilePath(recordingId) {
11567
- return import_node_path22.join(this.audioDir, this.buildSrtFilename(recordingId));
12303
+ return import_node_path24.join(this.audioDir, this.buildSrtFilename(recordingId));
11568
12304
  }
11569
12305
  getTranscriptDataFilePath(recordingId) {
11570
- return import_node_path22.join(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
12306
+ return import_node_path24.join(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
11571
12307
  }
11572
12308
  getSummaryFilePath(recordingId) {
11573
- return import_node_path22.join(this.summariesDir, this.buildSummaryFilename(recordingId));
12309
+ return import_node_path24.join(this.summariesDir, this.buildSummaryFilename(recordingId));
11574
12310
  }
11575
12311
  loadIndex() {
11576
- if (!import_node_fs26.existsSync(this.indexPath)) {
12312
+ if (!import_node_fs28.existsSync(this.indexPath)) {
11577
12313
  this.index = { recordings: [] };
11578
12314
  return;
11579
12315
  }
11580
12316
  try {
11581
- const raw = JSON.parse(import_node_fs26.readFileSync(this.indexPath, "utf-8"));
12317
+ const raw = JSON.parse(import_node_fs28.readFileSync(this.indexPath, "utf-8"));
11582
12318
  if (raw && Array.isArray(raw.recordings)) {
11583
12319
  let needsRewrite = false;
11584
12320
  const normalized = raw.recordings.filter((entry) => entry && typeof entry === "object").map((entry) => {
@@ -11599,14 +12335,23 @@ class RecordingStorage {
11599
12335
  if (typeof entry.transcriptFile === "string") {
11600
12336
  compacted.transcriptFile = entry.transcriptFile;
11601
12337
  }
11602
- let transcriptDoc = typeof entry.transcriptDataFile === "string" ? this.readRelativeTranscriptDocument(entry.transcriptDataFile) : undefined;
11603
- if (typeof entry.transcriptDataFile === "string" && transcriptDoc) {
12338
+ let transcriptDocLoaded = false;
12339
+ let transcriptDoc;
12340
+ const loadTranscriptDoc = () => {
12341
+ if (!transcriptDocLoaded) {
12342
+ transcriptDocLoaded = true;
12343
+ transcriptDoc = typeof compacted.transcriptDataFile === "string" ? this.readRelativeTranscriptDocument(compacted.transcriptDataFile) : undefined;
12344
+ }
12345
+ return transcriptDoc;
12346
+ };
12347
+ if (typeof entry.transcriptDataFile === "string") {
11604
12348
  compacted.transcriptDataFile = entry.transcriptDataFile;
11605
12349
  } else {
11606
12350
  const legacyTranscriptText = typeof entry.transcript === "string" && entry.transcript.trim() ? entry.transcript.trim() : compacted.transcriptFile ? extractTranscriptContent(this.readRelativeTextFile(compacted.transcriptFile) ?? "") || undefined : undefined;
11607
12351
  const legacyTitle = typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : deriveTitleFromTranscriptPath(compacted.transcriptFile, compacted.id);
11608
12352
  const legacySummary = typeof entry.summary === "string" && entry.summary.trim() ? entry.summary.trim() : undefined;
11609
12353
  if (legacyTranscriptText) {
12354
+ transcriptDocLoaded = true;
11610
12355
  transcriptDoc = buildTranscriptDocument({
11611
12356
  recordingId: compacted.id,
11612
12357
  generatedAt: compacted.updatedAt,
@@ -11619,7 +12364,7 @@ class RecordingStorage {
11619
12364
  segments: []
11620
12365
  });
11621
12366
  const transcriptDataFilename = this.buildTranscriptDataFilename(compacted.id);
11622
- import_node_fs26.writeFileSync(import_node_path22.join(this.transcriptDataDir, transcriptDataFilename), JSON.stringify(transcriptDoc, null, 2), "utf-8");
12367
+ import_node_fs28.writeFileSync(import_node_path24.join(this.transcriptDataDir, transcriptDataFilename), JSON.stringify(transcriptDoc, null, 2), "utf-8");
11623
12368
  compacted.transcriptDataFile = `${TRANSCRIPT_DATA_DIR}/${transcriptDataFilename}`;
11624
12369
  needsRewrite = true;
11625
12370
  }
@@ -11628,14 +12373,14 @@ class RecordingStorage {
11628
12373
  compacted.summaryFile = entry.summaryFile;
11629
12374
  } else if (typeof entry.summary === "string" && entry.summary.trim()) {
11630
12375
  const summaryFilename = this.buildSummaryFilename(entry.id);
11631
- import_node_fs26.writeFileSync(import_node_path22.join(this.summariesDir, summaryFilename), entry.summary.trim(), "utf-8");
12376
+ import_node_fs28.writeFileSync(import_node_path24.join(this.summariesDir, summaryFilename), entry.summary.trim(), "utf-8");
11632
12377
  compacted.summaryFile = `${SUMMARIES_DIR2}/${summaryFilename}`;
11633
12378
  needsRewrite = true;
11634
12379
  } else {
11635
- const summaryFromDocument = extractTranscriptSummaryFromDocument(transcriptDoc);
12380
+ const summaryFromDocument = extractTranscriptSummaryFromDocument(loadTranscriptDoc());
11636
12381
  if (summaryFromDocument) {
11637
12382
  const summaryFilename = this.buildSummaryFilename(entry.id);
11638
- import_node_fs26.writeFileSync(import_node_path22.join(this.summariesDir, summaryFilename), summaryFromDocument, "utf-8");
12383
+ import_node_fs28.writeFileSync(import_node_path24.join(this.summariesDir, summaryFilename), summaryFromDocument, "utf-8");
11639
12384
  compacted.summaryFile = `${SUMMARIES_DIR2}/${summaryFilename}`;
11640
12385
  needsRewrite = true;
11641
12386
  }
@@ -11643,7 +12388,7 @@ class RecordingStorage {
11643
12388
  if (typeof entry.title === "string" && entry.title.trim()) {
11644
12389
  compacted.title = entry.title.trim();
11645
12390
  } else {
11646
- const titleFromDocument = extractTranscriptTitleFromDocument(transcriptDoc);
12391
+ const titleFromDocument = extractTranscriptTitleFromDocument(loadTranscriptDoc());
11647
12392
  const derivedTitle = titleFromDocument ?? deriveTitleFromTranscriptPath(compacted.transcriptFile, compacted.id);
11648
12393
  if (derivedTitle) {
11649
12394
  compacted.title = derivedTitle;
@@ -11670,13 +12415,14 @@ class RecordingStorage {
11670
12415
  this.index = { recordings: [] };
11671
12416
  }
11672
12417
  } catch {
11673
- this.logger.warn(`录音索引文件损坏,已重建: ${this.indexPath}`);
12418
+ const quarantinePath = quarantineCorruptFile(this.indexPath);
12419
+ this.logger.warn(quarantinePath ? `录音索引文件损坏,已隔离到 ${quarantinePath} 并重建空索引` : `录音索引文件损坏且隔离失败,已重建空索引: ${this.indexPath}`);
11674
12420
  this.index = { recordings: [] };
11675
12421
  }
11676
12422
  }
11677
12423
  readRelativeTextFile(relativePath) {
11678
12424
  try {
11679
- return import_node_fs26.readFileSync(import_node_path22.join(this.dir, relativePath), "utf-8");
12425
+ return import_node_fs28.readFileSync(import_node_path24.join(this.dir, relativePath), "utf-8");
11680
12426
  } catch {
11681
12427
  return;
11682
12428
  }
@@ -11689,13 +12435,13 @@ class RecordingStorage {
11689
12435
  return parseTranscriptDocument(raw);
11690
12436
  }
11691
12437
  saveIndex() {
11692
- import_node_fs26.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), "utf-8");
12438
+ writeFileAtomic(this.indexPath, JSON.stringify(this.index, null, 2));
11693
12439
  }
11694
12440
  async close() {}
11695
12441
  }
11696
12442
  // src/recording/downloader.ts
11697
- var import_node_fs27 = require("node:fs");
11698
- var import_node_path23 = require("node:path");
12443
+ var import_node_fs29 = require("node:fs");
12444
+ var import_node_path25 = require("node:path");
11699
12445
  var import_promises2 = require("node:stream/promises");
11700
12446
  var import_node_stream = require("node:stream");
11701
12447
  var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
@@ -11705,7 +12451,7 @@ async function downloadFile(url, destPath, logger, options) {
11705
12451
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11706
12452
  const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
11707
12453
  const retryBackoffMs = options?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
11708
- import_node_fs27.mkdirSync(import_node_path23.dirname(destPath), { recursive: true });
12454
+ import_node_fs29.mkdirSync(import_node_path25.dirname(destPath), { recursive: true });
11709
12455
  let lastError;
11710
12456
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
11711
12457
  const startMs = Date.now();
@@ -11721,11 +12467,11 @@ async function downloadFile(url, destPath, logger, options) {
11721
12467
  if (!res.body) {
11722
12468
  throw new Error("响应体为空");
11723
12469
  }
11724
- const writeStream = import_node_fs27.createWriteStream(destPath);
12470
+ const writeStream = import_node_fs29.createWriteStream(destPath);
11725
12471
  const readable = import_node_stream.Readable.fromWeb(res.body);
11726
12472
  await import_promises2.pipeline(readable, writeStream);
11727
12473
  const elapsed = Date.now() - startMs;
11728
- const fileSize = import_node_fs27.existsSync(destPath) ? import_node_fs27.statSync(destPath).size : 0;
12474
+ const fileSize = import_node_fs29.existsSync(destPath) ? import_node_fs29.statSync(destPath).size : 0;
11729
12475
  logger.info(`[downloader] 下载完成: ${destPath} (${formatBytes(fileSize)}, ${elapsed}ms)`);
11730
12476
  return { ok: true, sizeBytes: fileSize, elapsedMs: elapsed };
11731
12477
  } finally {
@@ -11734,8 +12480,8 @@ async function downloadFile(url, destPath, logger, options) {
11734
12480
  } catch (err2) {
11735
12481
  lastError = err2?.message ?? String(err2);
11736
12482
  try {
11737
- if (import_node_fs27.existsSync(destPath))
11738
- import_node_fs27.unlinkSync(destPath);
12483
+ if (import_node_fs29.existsSync(destPath))
12484
+ import_node_fs29.unlinkSync(destPath);
11739
12485
  } catch {}
11740
12486
  const isAbort = err2?.name === "AbortError";
11741
12487
  logger.warn(`[downloader] 下载失败 (attempt ${attempt}/${maxRetries}): ${isAbort ? "超时" : lastError}`);
@@ -11759,8 +12505,8 @@ function sleep(ms) {
11759
12505
  return new Promise((resolve3) => setTimeout(resolve3, ms));
11760
12506
  }
11761
12507
  // src/recording/result-writer.ts
11762
- var import_node_fs28 = require("node:fs");
11763
- var import_node_path24 = require("node:path");
12508
+ var import_node_fs30 = require("node:fs");
12509
+ var import_node_path26 = require("node:path");
11764
12510
  function handleRecordingResultWrite(params, storage2, logger, options = {}) {
11765
12511
  const recordingId = normalizeRequiredText(params.recordingId, "recordingId");
11766
12512
  if (!params.transcript && !params.summary) {
@@ -11840,11 +12586,11 @@ function writeTranscript(recordingId, recording, transcript, storage2, logger) {
11840
12586
  raw: transcript
11841
12587
  });
11842
12588
  const transcriptDataFilename = storage2.buildTranscriptDataFilename(recordingId);
11843
- import_node_fs28.writeFileSync(storage2.getTranscriptDataFilePath(recordingId), JSON.stringify(transcriptDocument, null, 2), "utf-8");
12589
+ import_node_fs30.writeFileSync(storage2.getTranscriptDataFilePath(recordingId), JSON.stringify(transcriptDocument, null, 2), "utf-8");
11844
12590
  storage2.setTranscriptDataFile(recordingId, transcriptDataFilename);
11845
12591
  const transcriptFilename = storage2.buildTranscriptFilename(recordingId, title);
11846
12592
  const markdown = normalizePossiblyEmptyText2(transcript.markdown) ?? buildTranscriptMarkdown(recording, title, text, segments);
11847
- import_node_fs28.writeFileSync(import_node_path24.join(storage2.getTranscriptsDir(), transcriptFilename), ensureTrailingNewline(markdown), "utf-8");
12593
+ import_node_fs30.writeFileSync(import_node_path26.join(storage2.getTranscriptsDir(), transcriptFilename), ensureTrailingNewline(markdown), "utf-8");
11848
12594
  storage2.setTranscriptFile(recordingId, transcriptFilename);
11849
12595
  storage2.setTitle(recordingId, title);
11850
12596
  logger.info(`[recording-result] 转录文本已写入: ${recordingId}`);
@@ -11859,7 +12605,7 @@ function writeSummary(recordingId, summary, storage2, logger) {
11859
12605
  return;
11860
12606
  }
11861
12607
  const summaryFilename = storage2.buildSummaryFilename(recordingId);
11862
- import_node_fs28.writeFileSync(storage2.getSummaryFilePath(recordingId), ensureTrailingNewline(markdown), "utf-8");
12608
+ import_node_fs30.writeFileSync(storage2.getSummaryFilePath(recordingId), ensureTrailingNewline(markdown), "utf-8");
11863
12609
  storage2.setSummaryFile(recordingId, summaryFilename);
11864
12610
  logger.info(`[recording-result] 总结已写入: ${recordingId}`);
11865
12611
  return markdown.trim();
@@ -12032,8 +12778,8 @@ function formatTimestamp(ms) {
12032
12778
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
12033
12779
  }
12034
12780
  // src/image/store.ts
12035
- var import_node_fs29 = require("node:fs");
12036
- var import_node_path25 = require("node:path");
12781
+ var import_node_fs31 = require("node:fs");
12782
+ var import_node_path27 = require("node:path");
12037
12783
  var IMAGES_DIR = "images";
12038
12784
  var FILES_DIR = "files";
12039
12785
  var INDEX_FILE2 = "index.json";
@@ -12048,16 +12794,16 @@ function extFromMime(mime) {
12048
12794
  return "jpg";
12049
12795
  }
12050
12796
  function resolveImageStorageDir(ctx, logger) {
12051
- const stateImagesDir = import_node_path25.join(ctx.stateDir, "plugins", "phone-notifications", IMAGES_DIR);
12797
+ const stateImagesDir = import_node_path27.join(ctx.stateDir, "plugins", "phone-notifications", IMAGES_DIR);
12052
12798
  try {
12053
- import_node_fs29.mkdirSync(stateImagesDir, { recursive: true });
12799
+ import_node_fs31.mkdirSync(stateImagesDir, { recursive: true });
12054
12800
  logger.info(`图片将写入 stateDir 路径: ${stateImagesDir}`);
12055
12801
  return stateImagesDir;
12056
12802
  } catch {}
12057
12803
  if (ctx.workspaceDir) {
12058
- const wsImagesDir = import_node_path25.join(ctx.workspaceDir, IMAGES_DIR);
12804
+ const wsImagesDir = import_node_path27.join(ctx.workspaceDir, IMAGES_DIR);
12059
12805
  try {
12060
- import_node_fs29.mkdirSync(wsImagesDir, { recursive: true });
12806
+ import_node_fs31.mkdirSync(wsImagesDir, { recursive: true });
12061
12807
  logger.warn(`stateDir 不可用,图片已回退到 workspace 路径: ${wsImagesDir}`);
12062
12808
  return wsImagesDir;
12063
12809
  } catch {}
@@ -12074,11 +12820,11 @@ class ImageStorage {
12074
12820
  constructor(dir, logger) {
12075
12821
  this.logger = logger;
12076
12822
  this.dir = dir;
12077
- this.filesDir = import_node_path25.join(dir, FILES_DIR);
12078
- this.indexPath = import_node_path25.join(dir, INDEX_FILE2);
12823
+ this.filesDir = import_node_path27.join(dir, FILES_DIR);
12824
+ this.indexPath = import_node_path27.join(dir, INDEX_FILE2);
12079
12825
  }
12080
12826
  async init() {
12081
- import_node_fs29.mkdirSync(this.filesDir, { recursive: true });
12827
+ import_node_fs31.mkdirSync(this.filesDir, { recursive: true });
12082
12828
  this.loadIndex();
12083
12829
  this.logger.info(`图片存储已初始化: ${this.dir}(共 ${this.index.images.length} 条记录)`);
12084
12830
  }
@@ -12096,7 +12842,7 @@ class ImageStorage {
12096
12842
  return existing;
12097
12843
  }
12098
12844
  if (existing.localFile) {
12099
- import_node_fs29.rmSync(import_node_path25.join(this.dir, existing.localFile), { force: true });
12845
+ import_node_fs31.rmSync(import_node_path27.join(this.dir, existing.localFile), { force: true });
12100
12846
  }
12101
12847
  existing.metadata = metadata;
12102
12848
  existing.clientLabel = clientLabel;
@@ -12145,10 +12891,10 @@ class ImageStorage {
12145
12891
  return `${FILES_DIR}/${this.buildFilename(imageId, mime)}`;
12146
12892
  }
12147
12893
  getFilePath(imageId, mime) {
12148
- return import_node_path25.join(this.filesDir, this.buildFilename(imageId, mime));
12894
+ return import_node_path27.join(this.filesDir, this.buildFilename(imageId, mime));
12149
12895
  }
12150
12896
  resolveFile(relative) {
12151
- return import_node_path25.isAbsolute(relative) ? relative : import_node_path25.join(this.dir, relative);
12897
+ return import_node_path27.isAbsolute(relative) ? relative : import_node_path27.join(this.dir, relative);
12152
12898
  }
12153
12899
  findById(id) {
12154
12900
  return this.index.images.find((e) => e.imageId === id);
@@ -12157,26 +12903,27 @@ class ImageStorage {
12157
12903
  return [...this.index.images].sort((a, b) => b.metadata.created_at.localeCompare(a.metadata.created_at));
12158
12904
  }
12159
12905
  loadIndex() {
12160
- if (!import_node_fs29.existsSync(this.indexPath)) {
12906
+ if (!import_node_fs31.existsSync(this.indexPath)) {
12161
12907
  this.index = { images: [] };
12162
12908
  return;
12163
12909
  }
12164
12910
  try {
12165
- const raw = JSON.parse(import_node_fs29.readFileSync(this.indexPath, "utf-8"));
12911
+ const raw = JSON.parse(import_node_fs31.readFileSync(this.indexPath, "utf-8"));
12166
12912
  this.index = raw && Array.isArray(raw.images) ? { images: raw.images } : { images: [] };
12167
12913
  } catch {
12168
- this.logger.warn(`图片索引文件损坏,已重建: ${this.indexPath}`);
12914
+ const quarantinePath = quarantineCorruptFile(this.indexPath);
12915
+ this.logger.warn(quarantinePath ? `图片索引文件损坏,已隔离到 ${quarantinePath} 并重建空索引` : `图片索引文件损坏且隔离失败,已重建空索引: ${this.indexPath}`);
12169
12916
  this.index = { images: [] };
12170
12917
  }
12171
12918
  }
12172
12919
  saveIndex() {
12173
- import_node_fs29.writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), "utf-8");
12920
+ writeFileAtomic(this.indexPath, JSON.stringify(this.index, null, 2));
12174
12921
  }
12175
12922
  async close() {}
12176
12923
  }
12177
12924
  // src/image/handler.ts
12178
- var import_node_fs30 = require("node:fs");
12179
- var import_node_path26 = require("node:path");
12925
+ var import_node_fs32 = require("node:fs");
12926
+ var import_node_path28 = require("node:path");
12180
12927
  var DEFAULT_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
12181
12928
  var DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
12182
12929
  function handleImageSync(imageId, metadata, storage2, maxBytes, logger) {
@@ -12209,13 +12956,13 @@ async function runImageDownloadInBackground(imageId, metadata, storage2, maxByte
12209
12956
  logger.info(`[image-sync] 图片已同步: ${imageId} (${result.sizeBytes} bytes)`);
12210
12957
  }
12211
12958
  async function downloadImage(url, destPath, maxBytes, logger) {
12212
- import_node_fs30.mkdirSync(import_node_path26.dirname(destPath), { recursive: true });
12959
+ import_node_fs32.mkdirSync(import_node_path28.dirname(destPath), { recursive: true });
12213
12960
  const controller = new AbortController;
12214
12961
  const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
12215
12962
  const cleanup = () => {
12216
12963
  try {
12217
- if (import_node_fs30.existsSync(destPath))
12218
- import_node_fs30.unlinkSync(destPath);
12964
+ if (import_node_fs32.existsSync(destPath))
12965
+ import_node_fs32.unlinkSync(destPath);
12219
12966
  } catch {}
12220
12967
  };
12221
12968
  try {
@@ -12230,7 +12977,7 @@ async function downloadImage(url, destPath, maxBytes, logger) {
12230
12977
  if (contentLength > 0 && contentLength > maxBytes) {
12231
12978
  return { ok: false, error: `图片超过上限 ${maxBytes} 字节` };
12232
12979
  }
12233
- const writeStream = import_node_fs30.createWriteStream(destPath);
12980
+ const writeStream = import_node_fs32.createWriteStream(destPath);
12234
12981
  let total = 0;
12235
12982
  try {
12236
12983
  for await (const chunk of res.body) {
@@ -12264,19 +13011,19 @@ async function downloadImage(url, destPath, maxBytes, logger) {
12264
13011
  }
12265
13012
  }
12266
13013
  // src/tunnel/service.ts
12267
- var import_node_fs32 = require("node:fs");
12268
- var import_node_path29 = require("node:path");
13014
+ var import_node_fs34 = require("node:fs");
13015
+ var import_node_path31 = require("node:path");
12269
13016
 
12270
13017
  // src/tunnel/relay-client.ts
12271
- var import_node_fs31 = require("node:fs");
12272
- var import_node_path27 = require("node:path");
13018
+ var import_node_fs33 = require("node:fs");
13019
+ var import_node_path29 = require("node:path");
12273
13020
 
12274
13021
  // node_modules/ws/wrapper.mjs
12275
13022
  var import_stream = __toESM(require_stream(), 1);
12276
13023
  var import_extension = __toESM(require_extension(), 1);
12277
13024
  var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
12278
13025
  var import_receiver = __toESM(require_receiver(), 1);
12279
- var import_sender2 = __toESM(require_sender(), 1);
13026
+ var import_sender3 = __toESM(require_sender(), 1);
12280
13027
  var import_subprotocol = __toESM(require_subprotocol(), 1);
12281
13028
  var import_websocket = __toESM(require_websocket(), 1);
12282
13029
  var import_websocket_server = __toESM(require_websocket_server(), 1);
@@ -12386,8 +13133,8 @@ class RelayClient {
12386
13133
  lastDisconnectReason
12387
13134
  };
12388
13135
  try {
12389
- import_node_fs31.mkdirSync(import_node_path27.dirname(this.opts.statusFilePath), { recursive: true });
12390
- import_node_fs31.writeFileSync(this.opts.statusFilePath, JSON.stringify(info, null, 2));
13136
+ import_node_fs33.mkdirSync(import_node_path29.dirname(this.opts.statusFilePath), { recursive: true });
13137
+ import_node_fs33.writeFileSync(this.opts.statusFilePath, JSON.stringify(info, null, 2));
12391
13138
  } catch {}
12392
13139
  }
12393
13140
  onInbound(handler) {
@@ -12789,13 +13536,13 @@ var import_node_crypto5 = require("node:crypto");
12789
13536
 
12790
13537
  // src/host-sdk.ts
12791
13538
  var import_node_module = require("node:module");
12792
- var import_node_path28 = require("node:path");
13539
+ var import_node_path30 = require("node:path");
12793
13540
  var import_node_url = require("node:url");
12794
13541
  function resolveHostSdkModuleFromHostEntry(specifier, hostEntry = process.argv[1]) {
12795
13542
  if (!hostEntry?.trim())
12796
13543
  return "";
12797
13544
  try {
12798
- return import_node_module.createRequire(import_node_path28.resolve(hostEntry)).resolve(specifier);
13545
+ return import_node_module.createRequire(import_node_path30.resolve(hostEntry)).resolve(specifier);
12799
13546
  } catch {
12800
13547
  return "";
12801
13548
  }
@@ -12988,6 +13735,33 @@ function loadOrCreateDeviceIdentity(stateDir) {
12988
13735
 
12989
13736
  // src/tunnel/http-proxy.ts
12990
13737
  var RELAY_INTERNAL_HTTP_HEADER = "x-openclaw-relay-internal";
13738
+ var GATEWAY_RESPONSE_HEADERS_TIMEOUT_MS = 30000;
13739
+ var LARGE_RESPONSE_BODY_WARN_CHARS = 512 * 1024;
13740
+ async function fetchWithHeadersTimeout(url, init, upstreamSignal, timeoutMs) {
13741
+ const controller = new AbortController;
13742
+ if (upstreamSignal) {
13743
+ if (upstreamSignal.aborted) {
13744
+ controller.abort(upstreamSignal.reason);
13745
+ } else {
13746
+ upstreamSignal.addEventListener("abort", () => controller.abort(upstreamSignal.reason), { once: true });
13747
+ }
13748
+ }
13749
+ let timedOut = false;
13750
+ const timer = setTimeout(() => {
13751
+ timedOut = true;
13752
+ controller.abort();
13753
+ }, timeoutMs);
13754
+ try {
13755
+ return await fetch(url, { ...init, signal: controller.signal });
13756
+ } catch (err2) {
13757
+ if (timedOut) {
13758
+ throw new Error(`local gateway did not respond within ${timeoutMs}ms`);
13759
+ }
13760
+ throw err2;
13761
+ } finally {
13762
+ clearTimeout(timer);
13763
+ }
13764
+ }
12991
13765
  var PATH_MAP = {
12992
13766
  "/api/message/messageBridge/send": "/notifications"
12993
13767
  };
@@ -13058,12 +13832,11 @@ async function handleHttpRequest(opts, frame) {
13058
13832
  for (let attemptIndex = 0;attemptIndex < authAttempts.length; attemptIndex++) {
13059
13833
  const attempt = authAttempts[attemptIndex];
13060
13834
  opts.logger.info(`TunnelProxy: HTTP id=${frame.id} attempt ${attemptIndex + 1}/${authAttempts.length} auth=${attempt.label}`);
13061
- const res = await fetch(url.toString(), {
13835
+ const res = await fetchWithHeadersTimeout(url.toString(), {
13062
13836
  method: frame.method,
13063
13837
  headers: attempt.headers,
13064
- signal: opts.abortSignal,
13065
13838
  body: frame.method !== "GET" && frame.method !== "HEAD" ? frame.body : undefined
13066
- });
13839
+ }, opts.abortSignal, GATEWAY_RESPONSE_HEADERS_TIMEOUT_MS);
13067
13840
  const hasFallback = attemptIndex < authAttempts.length - 1;
13068
13841
  if (res.status === 401 && hasFallback) {
13069
13842
  const body = await res.text();
@@ -13103,6 +13876,9 @@ async function sendHttpResponse(opts, params) {
13103
13876
  }
13104
13877
  const body = await res.text();
13105
13878
  opts.logger.info(`TunnelProxy: HTTP id=${frameId} response body=${previewText(body)}`);
13879
+ if (body.length > LARGE_RESPONSE_BODY_WARN_CHARS) {
13880
+ opts.logger.warn(`TunnelProxy: HTTP id=${frameId} ${method} ${path2} response body is ${body.length} chars; single proxy_response frame may exceed the app-side WebSocket message limit (~1MiB)`);
13881
+ }
13106
13882
  const headers = {};
13107
13883
  res.headers.forEach((value, key) => {
13108
13884
  headers[key] = value;
@@ -13520,6 +14296,158 @@ function slimMessagesPayload(payload) {
13520
14296
  }
13521
14297
  return changed;
13522
14298
  }
14299
+ var TOOL_MESSAGE_ROLES = new Set(["toolResult", "tool"]);
14300
+ var TOOL_CONTENT_TYPES = new Set([
14301
+ "toolResult",
14302
+ "tool_result",
14303
+ "toolUse",
14304
+ "tool_use",
14305
+ "toolCall",
14306
+ "tool_call",
14307
+ "thinking",
14308
+ "redacted_thinking",
14309
+ "reasoning"
14310
+ ]);
14311
+ var MESSAGE_REASONING_FIELDS = [
14312
+ "thinking",
14313
+ "redactedThinking",
14314
+ "redacted_thinking",
14315
+ "reasoning",
14316
+ "reasoningContent",
14317
+ "reasoning_content",
14318
+ "thinkingSignature"
14319
+ ];
14320
+ function stripReasoningFields(message) {
14321
+ if (!isRecord2(message))
14322
+ return { message, changed: false };
14323
+ if (!MESSAGE_REASONING_FIELDS.some((field) => (field in message))) {
14324
+ return { message, changed: false };
14325
+ }
14326
+ const next = { ...message };
14327
+ for (const field of MESSAGE_REASONING_FIELDS) {
14328
+ delete next[field];
14329
+ }
14330
+ return { message: next, changed: true };
14331
+ }
14332
+ function isToolMessage(message) {
14333
+ return isRecord2(message) && typeof message.role === "string" && TOOL_MESSAGE_ROLES.has(message.role);
14334
+ }
14335
+ function stripToolContentBlocks(content) {
14336
+ if (!Array.isArray(content))
14337
+ return { content, changed: false };
14338
+ const kept = content.filter((item) => !(isRecord2(item) && typeof item.type === "string" && TOOL_CONTENT_TYPES.has(item.type)));
14339
+ return { content: kept, changed: kept.length !== content.length };
14340
+ }
14341
+ function isEmptyMessageContent(content) {
14342
+ if (content == null)
14343
+ return true;
14344
+ if (typeof content === "string")
14345
+ return content.trim().length === 0;
14346
+ if (Array.isArray(content))
14347
+ return content.length === 0;
14348
+ return false;
14349
+ }
14350
+ function hasDisplayableMessageContent(message) {
14351
+ if (!isRecord2(message))
14352
+ return false;
14353
+ if (!isEmptyMessageContent(message.content))
14354
+ return true;
14355
+ return typeof message.text === "string" && message.text.trim().length > 0;
14356
+ }
14357
+ function messageText(message) {
14358
+ if (!isRecord2(message))
14359
+ return "";
14360
+ if (typeof message.content === "string")
14361
+ return message.content;
14362
+ if (!Array.isArray(message.content))
14363
+ return "";
14364
+ return message.content.filter((item) => isRecord2(item) && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("");
14365
+ }
14366
+ function isInternalSubagentAnnounce(message) {
14367
+ if (!isRecord2(message))
14368
+ return false;
14369
+ const provenance = isRecord2(message.provenance) ? message.provenance : undefined;
14370
+ if (provenance?.kind === "inter_session" && provenance?.sourceTool === "subagent_announce") {
14371
+ return true;
14372
+ }
14373
+ const text = messageText(message);
14374
+ return message.role === "user" && text.includes("[Inter-session message]") && text.includes("sourceTool=subagent_announce") && text.includes("isUser=false");
14375
+ }
14376
+ function filterInternalMessages(messages) {
14377
+ const filtered = messages.filter((message) => !isInternalSubagentAnnounce(message));
14378
+ return {
14379
+ messages: filtered,
14380
+ removed: messages.length - filtered.length,
14381
+ changed: filtered.length !== messages.length
14382
+ };
14383
+ }
14384
+ function filterToolMessages(messages) {
14385
+ const result = [];
14386
+ let changed = false;
14387
+ for (const message of messages) {
14388
+ if (isToolMessage(message)) {
14389
+ changed = true;
14390
+ continue;
14391
+ }
14392
+ const stripped = stripToolContentFromMessage(message);
14393
+ if (stripped.changed) {
14394
+ changed = true;
14395
+ if (!hasDisplayableMessageContent(stripped.message))
14396
+ continue;
14397
+ result.push(stripped.message);
14398
+ continue;
14399
+ }
14400
+ result.push(message);
14401
+ }
14402
+ return { messages: result, removed: messages.length - result.length, changed };
14403
+ }
14404
+ function stripToolContentFromMessage(message) {
14405
+ const reasoningStripped = stripReasoningFields(message);
14406
+ let next = reasoningStripped.message;
14407
+ let changed = reasoningStripped.changed;
14408
+ if (isRecord2(next) && Array.isArray(next.content)) {
14409
+ const stripped = stripToolContentBlocks(next.content);
14410
+ if (stripped.changed) {
14411
+ next = { ...next, content: stripped.content };
14412
+ changed = true;
14413
+ }
14414
+ }
14415
+ return changed ? { message: next, changed } : { message, changed: false };
14416
+ }
14417
+ function rewriteSessionEventFrame(frame) {
14418
+ if (frame?.type !== "event")
14419
+ return { action: "keep" };
14420
+ if (frame.event === "session.tool") {
14421
+ return { action: "drop", reason: "tool event mirror" };
14422
+ }
14423
+ if (frame.event !== "session.message")
14424
+ return { action: "keep" };
14425
+ const message = frame?.payload?.message;
14426
+ if (!isRecord2(message))
14427
+ return { action: "keep" };
14428
+ if (isToolMessage(message)) {
14429
+ return { action: "drop", reason: "tool message" };
14430
+ }
14431
+ if (isInternalSubagentAnnounce(message)) {
14432
+ return { action: "drop", reason: "internal subagent announce" };
14433
+ }
14434
+ const stripped = stripToolContentFromMessage(message);
14435
+ if (!stripped.changed)
14436
+ return { action: "keep" };
14437
+ if (!hasDisplayableMessageContent(stripped.message)) {
14438
+ return { action: "drop", reason: "no displayable content after strip" };
14439
+ }
14440
+ return {
14441
+ action: "rewrite",
14442
+ frame: {
14443
+ ...frame,
14444
+ payload: {
14445
+ ...frame.payload,
14446
+ message: stripped.message
14447
+ }
14448
+ }
14449
+ };
14450
+ }
13523
14451
  function slimSessionsListPayload(payload) {
13524
14452
  if (!Array.isArray(payload?.sessions))
13525
14453
  return false;
@@ -13611,6 +14539,27 @@ function slimConfigGetPayload(payload) {
13611
14539
  rawPlaceholder: CONFIG_RAW_PLACEHOLDER
13612
14540
  });
13613
14541
  }
14542
+ var SUPPRESSED_AGENT_STREAMS = new Set([
14543
+ "command_output",
14544
+ "tool",
14545
+ "thinking",
14546
+ "thought"
14547
+ ]);
14548
+ var TOOL_ITEM_KINDS = new Set(["command", "tool"]);
14549
+ function isSuppressedAgentEventFrame(frame) {
14550
+ if (frame?.type !== "event" || frame?.event !== "agent")
14551
+ return false;
14552
+ const payload = frame.payload;
14553
+ if (!isRecord2(payload) || typeof payload.stream !== "string")
14554
+ return false;
14555
+ if (SUPPRESSED_AGENT_STREAMS.has(payload.stream))
14556
+ return true;
14557
+ if (payload.stream === "item") {
14558
+ const kind = isRecord2(payload.data) ? payload.data.kind : undefined;
14559
+ return typeof kind === "string" && TOOL_ITEM_KINDS.has(kind);
14560
+ }
14561
+ return false;
14562
+ }
13614
14563
  function slimRelayFrame(frame) {
13615
14564
  if (frame?.type === "event" && frame.event === "agent") {
13616
14565
  return slimAgentEventPayload(frame.payload);
@@ -13624,6 +14573,15 @@ function slimRelayFrame(frame) {
13624
14573
  }
13625
14574
  return false;
13626
14575
  }
14576
+ function slimParsedRelayFrame(text, frame, logger) {
14577
+ if (text.length <= SLIM_TRIGGER_CHARS)
14578
+ return text;
14579
+ if (!slimRelayFrame(frame))
14580
+ return text;
14581
+ const slimmed = JSON.stringify(frame);
14582
+ logger?.info(`TunnelProxy: slimmed relay-bound frame ${text.length} → ${slimmed.length} chars`);
14583
+ return slimmed;
14584
+ }
13627
14585
  function slimRelayFrameText(text, logger) {
13628
14586
  if (text.length <= SLIM_TRIGGER_CHARS)
13629
14587
  return text;
@@ -13633,11 +14591,7 @@ function slimRelayFrameText(text, logger) {
13633
14591
  } catch {
13634
14592
  return text;
13635
14593
  }
13636
- if (!slimRelayFrame(frame))
13637
- return text;
13638
- const slimmed = JSON.stringify(frame);
13639
- logger?.info(`TunnelProxy: slimmed relay-bound frame ${text.length} → ${slimmed.length} chars`);
13640
- return slimmed;
14594
+ return slimParsedRelayFrame(text, frame, logger);
13641
14595
  }
13642
14596
 
13643
14597
  // src/tunnel/proxy.ts
@@ -13655,6 +14609,9 @@ var MAX_AUTO_PAIRING_APPROVALS = 3;
13655
14609
  var RECENT_ABORTED_CHAT_RUN_TTL_MS = 60000;
13656
14610
  var GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS = 1e4;
13657
14611
  var MAX_GATEWAY_WS_PENDING = 200;
14612
+ var GATEWAY_RPC_CONNECT_LOG_INTERVAL_MS = 1e4;
14613
+ var GATEWAY_RPC_RECONNECT_BASE_MS = 1000;
14614
+ var GATEWAY_RPC_RECONNECT_MAX_MS = 30000;
13658
14615
  var approveDevicePairingPromise = null;
13659
14616
  var approveDevicePairingWarned = false;
13660
14617
  function formatErrorMessage(err2) {
@@ -13692,6 +14649,12 @@ class TunnelProxy {
13692
14649
  gatewayReqMetaById = new Map;
13693
14650
  recentAbortedChatRuns = new Map;
13694
14651
  httpAbortControllers = new Map;
14652
+ gatewayWsConnectLogLastAt = 0;
14653
+ gatewayWsConnectLogSuppressed = 0;
14654
+ maintainGatewayWs = false;
14655
+ gatewayWsReconnectTimer = null;
14656
+ gatewayWsReconnectAttempt = 0;
14657
+ gatewayConnectionState = null;
13695
14658
  deviceIdentity;
13696
14659
  stateDir;
13697
14660
  hostStateDir;
@@ -13732,14 +14695,22 @@ class TunnelProxy {
13732
14695
  break;
13733
14696
  }
13734
14697
  }
13735
- cleanup() {
13736
- this.opts.logger.info(`TunnelProxy: cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`);
14698
+ cleanupRelayState() {
14699
+ this.opts.logger.info(`TunnelProxy: relay cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`);
13737
14700
  this.wsProxy.cleanup();
13738
14701
  for (const [id, controller] of this.httpAbortControllers) {
13739
14702
  this.opts.logger.info(`TunnelProxy: aborting HTTP proxy id=${id}`);
13740
14703
  controller.abort();
13741
14704
  }
13742
14705
  this.httpAbortControllers.clear();
14706
+ this.gatewayWsPending = [];
14707
+ this.gatewayReqMetaById.clear();
14708
+ this.recentAbortedChatRuns.clear();
14709
+ }
14710
+ cleanup() {
14711
+ this.cleanupRelayState();
14712
+ this.maintainGatewayWs = false;
14713
+ this.clearGatewayReconnectTimer();
13743
14714
  const gatewayWs = this.gatewayWs;
13744
14715
  this.gatewayWs = null;
13745
14716
  this.gatewayWsReady = false;
@@ -13747,14 +14718,19 @@ class TunnelProxy {
13747
14718
  this.gatewayWsReconnectRequested = false;
13748
14719
  this.gatewayWsPairingApprovalPending = false;
13749
14720
  this.gatewayWsAutoPairingApprovals = 0;
13750
- this.gatewayWsPending = [];
13751
- this.gatewayReqMetaById.clear();
13752
- this.recentAbortedChatRuns.clear();
14721
+ const wasConnected = this.gatewayConnectionState === "connected";
13753
14722
  if (gatewayWs) {
13754
14723
  try {
13755
14724
  gatewayWs.close();
13756
14725
  } catch {}
13757
14726
  }
14727
+ if (wasConnected) {
14728
+ this.setGatewayConnectionState("disconnected", "plugin stopping");
14729
+ }
14730
+ }
14731
+ startGatewayConnection() {
14732
+ this.maintainGatewayWs = true;
14733
+ this.ensureGatewayWs();
13758
14734
  }
13759
14735
  async handleRequestFrame(frame) {
13760
14736
  const controller = new AbortController;
@@ -14020,6 +14996,21 @@ class TunnelProxy {
14020
14996
  }
14021
14997
  maybeRewriteGatewayFrame(text, frame) {
14022
14998
  this.pruneRecentAbortedChatRuns();
14999
+ if (isSuppressedAgentEventFrame(frame)) {
15000
+ const stream = frame?.payload?.stream;
15001
+ const kind = frame?.payload?.data?.kind;
15002
+ this.opts.logger.info(`TunnelProxy: dropping agent event stream=${stream}${kind ? ` kind=${kind}` : ""} for session=${frame?.payload?.sessionKey ?? "unknown"}`);
15003
+ return null;
15004
+ }
15005
+ const sessionEvent = rewriteSessionEventFrame(frame);
15006
+ if (sessionEvent.action === "drop") {
15007
+ this.opts.logger.info(`TunnelProxy: dropping ${frame?.event} event (${sessionEvent.reason}) for session=${frame?.payload?.sessionKey ?? "unknown"}`);
15008
+ return null;
15009
+ }
15010
+ if (sessionEvent.action === "rewrite") {
15011
+ this.opts.logger.info(`TunnelProxy: stripped thinking/tool content blocks from session.message for session=${frame?.payload?.sessionKey ?? "unknown"}`);
15012
+ return JSON.stringify(sessionEvent.frame);
15013
+ }
14023
15014
  if (frame?.type === "event" && frame?.event === "chat" && frame?.payload?.state === "aborted") {
14024
15015
  this.rememberRecentAbortedChatRun(frame.payload?.runId, frame.payload?.sessionKey);
14025
15016
  return text;
@@ -14048,22 +15039,36 @@ class TunnelProxy {
14048
15039
  this.opts.logger.info(`TunnelProxy: suppressing synthetic abort failure final for runId=${runId}`);
14049
15040
  return null;
14050
15041
  }
15042
+ const stripped = stripToolContentFromMessage(frame?.payload?.message);
15043
+ if (stripped.changed) {
15044
+ this.opts.logger.info(`TunnelProxy: stripped thinking/tool content blocks from chat final for runId=${runId ?? "unknown"}`);
15045
+ return JSON.stringify({
15046
+ ...frame,
15047
+ payload: { ...frame.payload, message: stripped.message }
15048
+ });
15049
+ }
14051
15050
  return text;
14052
15051
  }
14053
15052
  if (frame?.type === "res" && frame?.ok === true && reqMeta?.method === "chat.history" && Array.isArray(frame?.payload?.messages)) {
14054
15053
  const sessionKey = this.normalizeSessionKey(frame?.payload?.sessionKey) ?? reqMeta.sessionKey;
14055
- if (!sessionKey)
14056
- return text;
14057
- const filteredMessages = frame.payload.messages.filter((message) => !this.isSyntheticAbortHistoryMessage(message, sessionKey));
14058
- if (filteredMessages.length === frame.payload.messages.length) {
15054
+ const originalCount = frame.payload.messages.length;
15055
+ let messages = frame.payload.messages;
15056
+ if (sessionKey) {
15057
+ messages = messages.filter((message) => !this.isSyntheticAbortHistoryMessage(message, sessionKey));
15058
+ }
15059
+ const internalFiltered = filterInternalMessages(messages);
15060
+ messages = internalFiltered.messages;
15061
+ const toolFiltered = filterToolMessages(messages);
15062
+ messages = toolFiltered.messages;
15063
+ if (messages.length === originalCount && !internalFiltered.changed && !toolFiltered.changed) {
14059
15064
  return text;
14060
15065
  }
14061
- this.opts.logger.info(`TunnelProxy: removed ${frame.payload.messages.length - filteredMessages.length} synthetic abort history message(s) for session=${sessionKey}`);
15066
+ this.opts.logger.info(`TunnelProxy: chat.history filtered ${originalCount - messages.length} message(s) (tool+internal+synthetic) for session=${sessionKey ?? "unknown"}`);
14062
15067
  return JSON.stringify({
14063
15068
  ...frame,
14064
15069
  payload: {
14065
15070
  ...frame.payload,
14066
- messages: filteredMessages
15071
+ messages
14067
15072
  }
14068
15073
  });
14069
15074
  }
@@ -14074,10 +15079,11 @@ class TunnelProxy {
14074
15079
  return;
14075
15080
  if (this.gatewayWsConnecting)
14076
15081
  return;
15082
+ this.clearGatewayReconnectTimer();
14077
15083
  this.gatewayWsConnecting = true;
14078
15084
  this.gatewayWsReady = false;
14079
15085
  const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
14080
- this.opts.logger.info(`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`);
15086
+ this.logGatewayWsConnecting(wsUrl);
14081
15087
  let ws;
14082
15088
  try {
14083
15089
  ws = new wrapper_default(wsUrl, {
@@ -14087,7 +15093,9 @@ class TunnelProxy {
14087
15093
  const message = formatErrorMessage(err2);
14088
15094
  this.gatewayWsConnecting = false;
14089
15095
  this.opts.logger.error(`TunnelProxy: RPC WS failed to create gateway connection: ${message}`);
15096
+ this.setGatewayConnectionState("disconnected", message);
14090
15097
  this.failAllGatewayRequests("GATEWAY_RPC_CONNECT_FAILED", `local gateway RPC connect failed: ${message}`);
15098
+ this.scheduleGatewayReconnect(message);
14091
15099
  return;
14092
15100
  }
14093
15101
  this.gatewayWs = ws;
@@ -14095,10 +15103,11 @@ class TunnelProxy {
14095
15103
  handshakeTimer = null;
14096
15104
  if (this.gatewayWs !== ws || this.gatewayWsReady)
14097
15105
  return;
14098
- this.opts.logger.warn(`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length})`);
15106
+ this.opts.logger.warn(`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length}); ${this.gatewayTroubleshootingHint()}`);
14099
15107
  this.gatewayWs = null;
14100
15108
  this.gatewayWsReady = false;
14101
15109
  this.gatewayWsConnecting = false;
15110
+ this.setGatewayConnectionState("disconnected", "gateway RPC handshake timed out");
14102
15111
  this.failAllGatewayRequests("GATEWAY_RPC_HANDSHAKE_TIMEOUT", "local gateway RPC handshake timed out");
14103
15112
  try {
14104
15113
  if (typeof ws.terminate === "function") {
@@ -14107,6 +15116,7 @@ class TunnelProxy {
14107
15116
  ws.close();
14108
15117
  }
14109
15118
  } catch {}
15119
+ this.scheduleGatewayReconnect("gateway RPC handshake timed out");
14110
15120
  }, GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS);
14111
15121
  const clearHandshakeTimer = () => {
14112
15122
  if (handshakeTimer) {
@@ -14150,6 +15160,9 @@ class TunnelProxy {
14150
15160
  this.gatewayWsReconnectRequested = false;
14151
15161
  this.gatewayWsReady = true;
14152
15162
  this.gatewayWsConnecting = false;
15163
+ this.gatewayWsReconnectAttempt = 0;
15164
+ this.clearGatewayReconnectTimer();
15165
+ this.setGatewayConnectionState("connected", "hello-ok");
14153
15166
  this.opts.logger.info(`TunnelProxy: RPC WS handshake done (hello-ok), flushing ${this.gatewayWsPending.length} pending frames`);
14154
15167
  const pending = this.gatewayWsPending;
14155
15168
  this.gatewayWsPending = [];
@@ -14175,13 +15188,19 @@ class TunnelProxy {
14175
15188
  return;
14176
15189
  }
14177
15190
  this.opts.logger.error(`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText(JSON.stringify(frame.error), 500)}`);
15191
+ this.setGatewayConnectionState("disconnected", "gateway RPC handshake failed");
14178
15192
  clearHandshakeTimer();
14179
15193
  this.failAllGatewayRequests("GATEWAY_RPC_HANDSHAKE_FAILED", `local gateway RPC handshake failed: ${previewText(JSON.stringify(frame.error), 500)}`);
14180
15194
  ws.close();
14181
15195
  return;
14182
15196
  }
14183
15197
  const rewritten = this.maybeRewriteGatewayFrame(text, frame);
14184
- if (rewritten !== null) {
15198
+ if (rewritten === null) {
15199
+ return;
15200
+ }
15201
+ if (rewritten === text) {
15202
+ this.opts.client.sendRaw(slimParsedRelayFrame(text, frame, this.opts.logger));
15203
+ } else {
14185
15204
  this.opts.client.sendRaw(slimRelayFrameText(rewritten, this.opts.logger));
14186
15205
  }
14187
15206
  });
@@ -14198,6 +15217,9 @@ class TunnelProxy {
14198
15217
  this.gatewayWsReady = false;
14199
15218
  }
14200
15219
  this.gatewayWsConnecting = false;
15220
+ if (wasReady || this.maintainGatewayWs && !shouldReconnect && !shouldHoldForPairing) {
15221
+ this.setGatewayConnectionState("disconnected", `gateway closed: code=${code}, reason=${reasonText}`);
15222
+ }
14201
15223
  this.maybeClearStoredDeviceTokenOnMismatch(code, reasonText);
14202
15224
  if (shouldReconnect) {
14203
15225
  this.gatewayWsReconnectRequested = false;
@@ -14206,16 +15228,20 @@ class TunnelProxy {
14206
15228
  } else if (!shouldHoldForPairing && (pendingCount > 0 || this.gatewayReqMetaById.size > 0)) {
14207
15229
  this.failAllGatewayRequests("GATEWAY_RPC_DISCONNECTED", `local gateway RPC disconnected: code=${code}, reason=${reasonText}`);
14208
15230
  }
15231
+ if (!shouldReconnect && !shouldHoldForPairing) {
15232
+ this.scheduleGatewayReconnect(`gateway closed: code=${code}, reason=${reasonText}`);
15233
+ }
14209
15234
  });
14210
15235
  ws.on("error", (err2) => {
14211
15236
  clearHandshakeTimer();
14212
- this.opts.logger.warn(`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`);
15237
+ this.opts.logger.warn(`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount}); ${this.gatewayTroubleshootingHint()}`);
14213
15238
  const shouldFailRequests = this.gatewayWs === ws && (this.gatewayWsPending.length > 0 || this.gatewayReqMetaById.size > 0);
14214
15239
  this.gatewayWsConnecting = false;
14215
15240
  if (this.gatewayWs === ws) {
14216
15241
  this.gatewayWs = null;
14217
15242
  this.gatewayWsReady = false;
14218
15243
  }
15244
+ this.setGatewayConnectionState("disconnected", `gateway RPC error: ${err2.message}`);
14219
15245
  if (shouldFailRequests) {
14220
15246
  this.failAllGatewayRequests("GATEWAY_RPC_ERROR", `local gateway RPC error: ${err2.message}`);
14221
15247
  }
@@ -14224,8 +15250,58 @@ class TunnelProxy {
14224
15250
  ws.terminate();
14225
15251
  }
14226
15252
  } catch {}
15253
+ this.scheduleGatewayReconnect(`gateway RPC error: ${err2.message}`);
14227
15254
  });
14228
15255
  }
15256
+ setGatewayConnectionState(state, reason) {
15257
+ if (this.gatewayConnectionState === state)
15258
+ return;
15259
+ this.gatewayConnectionState = state;
15260
+ if (state === "connected") {
15261
+ this.opts.logger.info(`TunnelProxy: local gateway connection state connected (${reason})`);
15262
+ this.opts.onGatewayConnected?.();
15263
+ return;
15264
+ }
15265
+ this.opts.logger.warn(`TunnelProxy: local gateway connection state disconnected (${reason})`);
15266
+ this.opts.onGatewayDisconnected?.(reason);
15267
+ }
15268
+ clearGatewayReconnectTimer() {
15269
+ if (!this.gatewayWsReconnectTimer)
15270
+ return;
15271
+ clearTimeout(this.gatewayWsReconnectTimer);
15272
+ this.gatewayWsReconnectTimer = null;
15273
+ }
15274
+ scheduleGatewayReconnect(reason) {
15275
+ if (!this.maintainGatewayWs || this.gatewayWsReconnectTimer)
15276
+ return;
15277
+ if (this.gatewayWsReady || this.gatewayWsConnecting)
15278
+ return;
15279
+ const baseDelayMs = Math.min(GATEWAY_RPC_RECONNECT_BASE_MS * Math.pow(2, this.gatewayWsReconnectAttempt), GATEWAY_RPC_RECONNECT_MAX_MS);
15280
+ const jitterMs = Math.floor(Math.random() * Math.max(1, baseDelayMs / 4));
15281
+ const delayMs = baseDelayMs + jitterMs;
15282
+ this.gatewayWsReconnectAttempt++;
15283
+ this.opts.logger.info(`TunnelProxy: reconnecting local gateway RPC in ${delayMs}ms (attempt ${this.gatewayWsReconnectAttempt}, reason=${reason})`);
15284
+ this.gatewayWsReconnectTimer = setTimeout(() => {
15285
+ this.gatewayWsReconnectTimer = null;
15286
+ this.ensureGatewayWs();
15287
+ }, delayMs);
15288
+ this.gatewayWsReconnectTimer.unref?.();
15289
+ }
15290
+ logGatewayWsConnecting(wsUrl) {
15291
+ const now = Date.now();
15292
+ const shouldLog = this.gatewayWsConnectLogLastAt === 0 || now - this.gatewayWsConnectLogLastAt >= GATEWAY_RPC_CONNECT_LOG_INTERVAL_MS;
15293
+ if (!shouldLog) {
15294
+ this.gatewayWsConnectLogSuppressed++;
15295
+ return;
15296
+ }
15297
+ const suppressed = this.gatewayWsConnectLogSuppressed > 0 ? `, suppressed=${this.gatewayWsConnectLogSuppressed}` : "";
15298
+ this.opts.logger.info(`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length}${suppressed})`);
15299
+ this.gatewayWsConnectLogLastAt = now;
15300
+ this.gatewayWsConnectLogSuppressed = 0;
15301
+ }
15302
+ gatewayTroubleshootingHint() {
15303
+ return `check local gateway (${this.opts.gatewayBaseUrl}), openclaw gateway status, gateway.port/gateway.tls.enabled, or PHONE_NOTIFICATIONS_GATEWAY_BASE_URL`;
15304
+ }
14229
15305
  handleConnectChallenge(ws, frame) {
14230
15306
  const challengeNonce = frame.payload?.nonce ?? "";
14231
15307
  const connectRequestId = `tunnel-connect-${import_node_crypto5.randomUUID()}`;
@@ -14278,57 +15354,105 @@ class TunnelProxy {
14278
15354
  }
14279
15355
 
14280
15356
  // src/tunnel/connect-status-reporter.ts
15357
+ var REPORT_TIMEOUT_MS = 1e4;
15358
+ var RETRY_BASE_MS = 1000;
15359
+ var RETRY_MAX_MS = 60000;
14281
15360
  function createConnectStatusReporter(opts) {
14282
15361
  const { logger } = opts;
14283
15362
  let lastReported = null;
15363
+ let desiredStatus = null;
14284
15364
  let inFlight = null;
15365
+ let retryTimer = null;
15366
+ let retryAttempt = 0;
14285
15367
  async function post(status) {
14286
15368
  const url = getEnvUrls().gatewayConnectStatusUrl;
14287
15369
  if (!url) {
14288
15370
  logger.warn("[gateway-connect-status] url is empty for current env, skip report");
14289
- return;
15371
+ return "skip";
14290
15372
  }
14291
15373
  const apiKey = loadApiKey();
14292
15374
  if (!apiKey) {
14293
15375
  logger.info("[gateway-connect-status] api key missing, skip report");
14294
- return;
15376
+ return "skip";
14295
15377
  }
14296
15378
  const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
14297
15379
  try {
14298
15380
  const res = await fetch(url, {
14299
15381
  method: "POST",
14300
15382
  headers: { "Content-Type": "application/json" },
14301
- body: JSON.stringify({ apiKey: rawApiKey, connectStatus: status })
15383
+ body: JSON.stringify({ apiKey: rawApiKey, connectStatus: status }),
15384
+ signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
14302
15385
  });
14303
15386
  if (!res.ok) {
14304
15387
  logger.warn(`[gateway-connect-status] report ${status} failed: HTTP ${res.status} ${res.statusText}`);
14305
- return;
15388
+ return res.status >= 500 || res.status === 429 ? "retry" : "skip";
14306
15389
  }
14307
15390
  logger.info(`[gateway-connect-status] reported ${status}`);
15391
+ return "success";
14308
15392
  } catch (err2) {
14309
15393
  const message = err2 instanceof Error ? err2.message : String(err2);
14310
15394
  logger.warn(`[gateway-connect-status] report ${status} error: ${message}`);
15395
+ return "retry";
15396
+ }
15397
+ }
15398
+ function clearRetryTimer() {
15399
+ if (!retryTimer)
15400
+ return;
15401
+ clearTimeout(retryTimer);
15402
+ retryTimer = null;
15403
+ }
15404
+ function scheduleRetry() {
15405
+ if (retryTimer || desiredStatus === null || desiredStatus === lastReported) {
15406
+ return;
14311
15407
  }
15408
+ const delayMs = Math.min(RETRY_BASE_MS * Math.pow(2, retryAttempt), RETRY_MAX_MS);
15409
+ retryAttempt++;
15410
+ logger.info(`[gateway-connect-status] retrying ${desiredStatus} in ${delayMs}ms (attempt ${retryAttempt})`);
15411
+ retryTimer = setTimeout(() => {
15412
+ retryTimer = null;
15413
+ drain();
15414
+ }, delayMs);
15415
+ retryTimer.unref?.();
15416
+ }
15417
+ function drain() {
15418
+ if (inFlight || desiredStatus === null || desiredStatus === lastReported) {
15419
+ return;
15420
+ }
15421
+ clearRetryTimer();
15422
+ const status = desiredStatus;
15423
+ inFlight = post(status).then((result) => {
15424
+ if (result === "success") {
15425
+ lastReported = status;
15426
+ retryAttempt = 0;
15427
+ } else if (result === "skip" && desiredStatus === status) {
15428
+ desiredStatus = null;
15429
+ retryAttempt = 0;
15430
+ }
15431
+ }).finally(() => {
15432
+ inFlight = null;
15433
+ if (desiredStatus === lastReported)
15434
+ return;
15435
+ if (desiredStatus !== status) {
15436
+ retryAttempt = 0;
15437
+ drain();
15438
+ return;
15439
+ }
15440
+ scheduleRetry();
15441
+ });
14312
15442
  }
14313
15443
  return {
14314
15444
  report(status) {
14315
- if (lastReported === status)
15445
+ desiredStatus = status;
15446
+ if (lastReported === status) {
15447
+ clearRetryTimer();
14316
15448
  return;
14317
- lastReported = status;
14318
- const run = async () => {
14319
- if (inFlight) {
14320
- try {
14321
- await inFlight;
14322
- } catch {}
14323
- }
14324
- await post(status);
14325
- };
14326
- inFlight = run().finally(() => {
14327
- inFlight = null;
14328
- });
15449
+ }
15450
+ drain();
14329
15451
  },
14330
15452
  reset() {
14331
15453
  lastReported = null;
15454
+ retryAttempt = 0;
15455
+ drain();
14332
15456
  }
14333
15457
  };
14334
15458
  }
@@ -14378,6 +15502,9 @@ function createTunnelService(opts) {
14378
15502
  let authRefreshTimer = null;
14379
15503
  let authRefreshPromise = null;
14380
15504
  let pendingAuthRefreshReason = null;
15505
+ const connectStatusReporter = createConnectStatusReporter({
15506
+ logger: opts.logger
15507
+ });
14381
15508
  function emitRelayEvent(event, payload) {
14382
15509
  if (!client?.isConnected())
14383
15510
  return false;
@@ -14405,7 +15532,7 @@ function createTunnelService(opts) {
14405
15532
  }
14406
15533
  function readLockInfo(filePath) {
14407
15534
  try {
14408
- const parsed = JSON.parse(import_node_fs32.readFileSync(filePath, "utf-8"));
15535
+ const parsed = JSON.parse(import_node_fs34.readFileSync(filePath, "utf-8"));
14409
15536
  const pid = typeof parsed.pid === "number" ? parsed.pid : null;
14410
15537
  const updatedAtMs = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
14411
15538
  return {
@@ -14438,7 +15565,7 @@ function createTunnelService(opts) {
14438
15565
  if (!filePath)
14439
15566
  return;
14440
15567
  try {
14441
- import_node_fs32.writeFileSync(filePath, buildLockPayload(), { mode: 384 });
15568
+ import_node_fs34.writeFileSync(filePath, buildLockPayload(), { mode: 384 });
14442
15569
  } catch {}
14443
15570
  }, intervalMs);
14444
15571
  lockRefreshTimer.unref?.();
@@ -14452,25 +15579,25 @@ function createTunnelService(opts) {
14452
15579
  lockStartedAt = null;
14453
15580
  if (fd !== null) {
14454
15581
  try {
14455
- import_node_fs32.closeSync(fd);
15582
+ import_node_fs34.closeSync(fd);
14456
15583
  } catch {}
14457
15584
  }
14458
15585
  if (filePath) {
14459
15586
  try {
14460
- import_node_fs32.unlinkSync(filePath);
15587
+ import_node_fs34.unlinkSync(filePath);
14461
15588
  } catch {}
14462
15589
  }
14463
15590
  }
14464
15591
  function acquireLock(filePath) {
14465
- import_node_fs32.mkdirSync(import_node_path29.dirname(filePath), { recursive: true });
15592
+ import_node_fs34.mkdirSync(import_node_path31.dirname(filePath), { recursive: true });
14466
15593
  const heartbeatSec = opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC;
14467
15594
  const refreshMs = heartbeatSec * 1000 * LOCK_REFRESH_HEARTBEAT_MULTIPLIER;
14468
15595
  const staleMs = heartbeatSec * 1000 * LOCK_STALE_HEARTBEAT_MULTIPLIER;
14469
15596
  for (let attempt = 0;attempt < 2; attempt++) {
14470
15597
  try {
14471
15598
  lockStartedAt = new Date().toISOString();
14472
- const fd = import_node_fs32.openSync(filePath, "wx", 384);
14473
- import_node_fs32.writeFileSync(fd, buildLockPayload());
15599
+ const fd = import_node_fs34.openSync(filePath, "wx", 384);
15600
+ import_node_fs34.writeFileSync(fd, buildLockPayload());
14474
15601
  lockFilePath = filePath;
14475
15602
  lockFd = fd;
14476
15603
  startLockRefresh(refreshMs);
@@ -14488,7 +15615,7 @@ function createTunnelService(opts) {
14488
15615
  const staleReason = describeStaleLock(ownerPid, updatedAt, staleMs);
14489
15616
  opts.logger.warn(`Relay tunnel: removing stale local lock (${staleReason})`);
14490
15617
  try {
14491
- import_node_fs32.unlinkSync(filePath);
15618
+ import_node_fs34.unlinkSync(filePath);
14492
15619
  } catch {}
14493
15620
  continue;
14494
15621
  }
@@ -14593,10 +15720,10 @@ function createTunnelService(opts) {
14593
15720
  const { logger } = opts;
14594
15721
  const hostStateDir = resolveHostStateDir(ctx.stateDir);
14595
15722
  const stateDir = resolveStateDir(hostStateDir);
14596
- const baseStateDir = import_node_path29.join(stateDir, "plugins", "phone-notifications");
15723
+ const baseStateDir = import_node_path31.join(stateDir, "plugins", "phone-notifications");
14597
15724
  logger.info(`Relay tunnel: starting (pid=${process.pid}, url=${redactUrlSecrets(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})`);
14598
- const statusFilePath = import_node_path29.join(baseStateDir, "tunnel-status.json");
14599
- const lockPath = import_node_path29.join(baseStateDir, "relay-tunnel.lock");
15725
+ const statusFilePath = import_node_path31.join(baseStateDir, "tunnel-status.json");
15726
+ const lockPath = import_node_path31.join(baseStateDir, "relay-tunnel.lock");
14600
15727
  if (!acquireLock(lockPath)) {
14601
15728
  return;
14602
15729
  }
@@ -14616,20 +15743,24 @@ function createTunnelService(opts) {
14616
15743
  gatewayAuthMode: opts.gatewayAuthMode,
14617
15744
  gatewayToken: opts.gatewayToken,
14618
15745
  gatewayPassword: opts.gatewayPassword,
15746
+ onGatewayConnected: () => {
15747
+ connectStatusReporter.report("connected");
15748
+ },
15749
+ onGatewayDisconnected: () => {
15750
+ connectStatusReporter.report("disconnected");
15751
+ },
14619
15752
  client,
14620
15753
  logger
14621
15754
  });
14622
- const connectStatusReporter = createConnectStatusReporter({ logger });
14623
15755
  client.onInbound((frame) => proxy.handleFrame(frame));
14624
15756
  client.onConnected(() => {
14625
- connectStatusReporter.report("connected");
14626
15757
  emitPendingPluginUpdate("relay connected");
14627
15758
  });
14628
15759
  client.onDisconnected((reason) => {
14629
- logger.warn(`Relay tunnel: relay disconnected, cleaning local proxy state (${reason})`);
14630
- connectStatusReporter.report("disconnected");
14631
- proxy?.cleanup();
15760
+ logger.warn(`Relay tunnel: relay disconnected, cleaning relay proxy state (${reason})`);
15761
+ proxy?.cleanupRelayState();
14632
15762
  });
15763
+ proxy.startGatewayConnection();
14633
15764
  abortController = new AbortController;
14634
15765
  client.connectWithAutoReconnect(abortController.signal).catch((err2) => {
14635
15766
  releaseLock();
@@ -14692,7 +15823,7 @@ function createTunnelService(opts) {
14692
15823
  }
14693
15824
 
14694
15825
  // src/profile/auth/base-apikey.ts
14695
- function stripBearer(apiKey) {
15826
+ function stripBearer2(apiKey) {
14696
15827
  return apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
14697
15828
  }
14698
15829
 
@@ -14710,7 +15841,7 @@ class BaseApiKeyAuthProvider {
14710
15841
  if (!apiKey) {
14711
15842
  throw new Error(`apiKey 未设置,请先执行 ntf auth set-api-key <apiKey>(或 ${credentialsPath()})`);
14712
15843
  }
14713
- const raw = stripBearer(apiKey);
15844
+ const raw = stripBearer2(apiKey);
14714
15845
  return {
14715
15846
  query: { apiKey: raw },
14716
15847
  headers: { "X-Api-Key-Id": raw }
@@ -14737,11 +15868,11 @@ class ArkclawAuthProvider extends BaseApiKeyAuthProvider {
14737
15868
  // src/profile/auth/jvsclaw.ts
14738
15869
  var import_node_crypto6 = require("node:crypto");
14739
15870
  var import_node_os3 = __toESM(require("node:os"));
14740
- var import_node_fs33 = require("node:fs");
14741
- var import_node_path30 = require("node:path");
15871
+ var import_node_fs35 = require("node:fs");
15872
+ var import_node_path32 = require("node:path");
14742
15873
  var CLAW_CODE = "JvsClaw";
14743
- var RETRY_BASE_MS = 2000;
14744
- var RETRY_MAX_MS = 60000;
15874
+ var RETRY_BASE_MS2 = 2000;
15875
+ var RETRY_MAX_MS2 = 60000;
14745
15876
  var FATAL_READY_CODES = new Set([
14746
15877
  "54016",
14747
15878
  "54017",
@@ -14749,7 +15880,7 @@ var FATAL_READY_CODES = new Set([
14749
15880
  "54020",
14750
15881
  "54024"
14751
15882
  ]);
14752
- var JVSCLAW_LINK_SECRET_CONFIG_PATH = import_node_path30.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json");
15883
+ var JVSCLAW_LINK_SECRET_CONFIG_PATH = import_node_path32.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json");
14753
15884
  var PLUGIN_ID3 = "phone-notifications";
14754
15885
  var LINK_SECRET_WATCH_DEBOUNCE_MS = 250;
14755
15886
 
@@ -14802,7 +15933,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
14802
15933
  logger.error(`jvsclaw auth: 置换失败且不可恢复(code=${err2.code}): ${err2.message},停止轮询`);
14803
15934
  return;
14804
15935
  }
14805
- const delayMs = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_MS);
15936
+ const delayMs = Math.min(RETRY_BASE_MS2 * 2 ** (attempt - 1), RETRY_MAX_MS2);
14806
15937
  logger.warn(`jvsclaw auth: 置换失败(attempt ${attempt}): ${err2?.message ?? String(err2)},${delayMs}ms 后重试`);
14807
15938
  const aborted = await sleep2(delayMs, abortSignal);
14808
15939
  if (aborted)
@@ -14861,8 +15992,8 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
14861
15992
  watch(onChange) {
14862
15993
  const unwatchCredentials = super.watch(onChange);
14863
15994
  const configPath = resolveConfigPath();
14864
- const configDir = import_node_path30.dirname(configPath);
14865
- const configFile = import_node_path30.basename(configPath);
15995
+ const configDir = import_node_path32.dirname(configPath);
15996
+ const configFile = import_node_path32.basename(configPath);
14866
15997
  let debounceTimer = null;
14867
15998
  let configWatcher = null;
14868
15999
  const emitChange = () => {
@@ -14875,7 +16006,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
14875
16006
  debounceTimer.unref?.();
14876
16007
  };
14877
16008
  try {
14878
- configWatcher = import_node_fs33.watch(configDir, { persistent: false }, (_event, changedName) => {
16009
+ configWatcher = import_node_fs35.watch(configDir, { persistent: false }, (_event, changedName) => {
14879
16010
  if (!changedName)
14880
16011
  return;
14881
16012
  const name = String(changedName);
@@ -14900,7 +16031,7 @@ function hashLinkSecret(value) {
14900
16031
  function readJvsclawLinkSecretFromConfigFile(configPath = resolveConfigPath()) {
14901
16032
  let config;
14902
16033
  try {
14903
- config = JSON.parse(import_node_fs33.readFileSync(configPath, "utf-8"));
16034
+ config = JSON.parse(import_node_fs35.readFileSync(configPath, "utf-8"));
14904
16035
  } catch {
14905
16036
  return;
14906
16037
  }
@@ -14948,17 +16079,51 @@ function resolveAuthProvider(deps) {
14948
16079
  }
14949
16080
  }
14950
16081
 
16082
+ // src/plugin/register-guard.ts
16083
+ var DEFAULT_KEY = "__default__";
16084
+ var wiredRuntimes = new Map;
16085
+ function normalizeKey(key) {
16086
+ return key && key.length > 0 ? key : DEFAULT_KEY;
16087
+ }
16088
+ function claimWiredRuntime(key, create) {
16089
+ const normalized = normalizeKey(key);
16090
+ const existing = wiredRuntimes.get(normalized);
16091
+ if (existing) {
16092
+ return { runtime: existing, firstAssembly: false };
16093
+ }
16094
+ const runtime = create();
16095
+ wiredRuntimes.set(normalized, runtime);
16096
+ return { runtime, firstAssembly: true };
16097
+ }
16098
+ function releaseWiredRuntime(key) {
16099
+ wiredRuntimes.delete(normalizeKey(key));
16100
+ }
16101
+
16102
+ // src/plugin/shared-storage.ts
16103
+ var DEFAULT_KEY2 = "__default__";
16104
+ var registry = new Map;
16105
+ function normalizeKey2(key) {
16106
+ return key && key.length > 0 ? key : DEFAULT_KEY2;
16107
+ }
16108
+ function publishSharedStorage(key, handles) {
16109
+ registry.set(normalizeKey2(key), handles);
16110
+ }
16111
+ function clearSharedStorage(key) {
16112
+ registry.delete(normalizeKey2(key));
16113
+ }
16114
+ function getSharedNotificationStorage(key) {
16115
+ return registry.get(normalizeKey2(key))?.storage ?? null;
16116
+ }
16117
+ function getSharedRecordingStorage(key) {
16118
+ return registry.get(normalizeKey2(key))?.recordingStorage ?? null;
16119
+ }
16120
+ function getSharedImageStorage(key) {
16121
+ return registry.get(normalizeKey2(key))?.imageStorage ?? null;
16122
+ }
16123
+
14951
16124
  // src/plugin/lifecycle.ts
14952
16125
  function registerStorageLifecycle(deps) {
14953
- const {
14954
- api,
14955
- logger,
14956
- lightRuleCtx,
14957
- setStorage,
14958
- setRecordingStorage,
14959
- setImageStorage,
14960
- onStorageReady
14961
- } = deps;
16126
+ const { api, logger, runtime, sharedStorageKey } = deps;
14962
16127
  let storage2 = null;
14963
16128
  let recordingStorage = null;
14964
16129
  let imageStorage = null;
@@ -14977,32 +16142,38 @@ function registerStorageLifecycle(deps) {
14977
16142
  const resolveDisplayName = appNameMapProvider.resolveDisplayName.bind(appNameMapProvider);
14978
16143
  storage2 = new NotificationStorage(storageDir, logger, resolveDisplayName);
14979
16144
  await storage2.init();
14980
- setStorage(storage2);
14981
16145
  logger.info(`通知存储服务已启动: ${storageDir}`);
14982
16146
  const recDir = resolveRecordingStorageDir(storageCtx, logger);
14983
16147
  recordingStorage = new RecordingStorage(recDir, logger);
14984
16148
  await recordingStorage.init();
14985
- setRecordingStorage(recordingStorage);
14986
16149
  logger.info(`录音存储服务已启动: ${recDir}`);
14987
16150
  const imageDir = resolveImageStorageDir(storageCtx, logger);
14988
16151
  imageStorage = new ImageStorage(imageDir, logger);
14989
16152
  await imageStorage.init();
14990
- setImageStorage(imageStorage);
14991
16153
  logger.info(`图片存储服务已启动: ${imageDir}`);
14992
- lightRuleCtx.workspaceDir = ctx.workspaceDir;
14993
- lightRuleCtx.stateDir = stateDir;
14994
- onStorageReady?.();
16154
+ runtime.markStorageReady({
16155
+ storage: storage2,
16156
+ recordingStorage,
16157
+ imageStorage,
16158
+ workspaceDir: ctx.workspaceDir,
16159
+ stateDir
16160
+ });
16161
+ publishSharedStorage(sharedStorageKey, {
16162
+ storage: storage2,
16163
+ recordingStorage,
16164
+ imageStorage
16165
+ });
14995
16166
  },
14996
16167
  async stop() {
16168
+ runtime.markStorageStopped();
16169
+ clearSharedStorage(sharedStorageKey);
16170
+ releaseWiredRuntime(sharedStorageKey);
14997
16171
  await storage2?.close();
14998
16172
  storage2 = null;
14999
- setStorage(null);
15000
16173
  await recordingStorage?.close();
15001
16174
  recordingStorage = null;
15002
- setRecordingStorage(null);
15003
16175
  await imageStorage?.close();
15004
16176
  imageStorage = null;
15005
- setImageStorage(null);
15006
16177
  appNameMapProvider?.stop();
15007
16178
  appNameMapProvider = null;
15008
16179
  logger.info("通知/录音/图片存储服务已停止");
@@ -15014,7 +16185,7 @@ function readHostGatewayConfig(params) {
15014
16185
  let configData;
15015
16186
  if (configPath) {
15016
16187
  try {
15017
- configData = JSON.parse(import_node_fs34.readFileSync(configPath, "utf-8"));
16188
+ configData = JSON.parse(import_node_fs36.readFileSync(configPath, "utf-8"));
15018
16189
  } catch (err2) {
15019
16190
  if (err2?.code !== "ENOENT") {
15020
16191
  params.logger.warn(`Relay tunnel: 无法读取 gateway 鉴权配置 (${configPath})`);
@@ -15094,7 +16265,20 @@ function registerRelayTunnelLifecycle(deps) {
15094
16265
  logger.info(`Relay tunnel: detected ${exclusiveTunnelHint} in host gateway config, skipping relay startup to keep ingress mutually exclusive`);
15095
16266
  return null;
15096
16267
  }
15097
- const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? process.env.QCLAW_GATEWAY_PORT ?? "18789";
16268
+ const hostGatewayConfig = readHostGatewayConfig({
16269
+ stateDir: openclawDir,
16270
+ logger
16271
+ });
16272
+ const gatewayBaseUrl = resolveLocalGatewayBaseUrl({
16273
+ config: hostGatewayConfig
16274
+ });
16275
+ if (gatewayBaseUrl.warning) {
16276
+ logger.warn(`Relay tunnel: ${gatewayBaseUrl.warning}`);
16277
+ }
16278
+ if (gatewayBaseUrl.tlsEnabled && !gatewayBaseUrl.explicit) {
16279
+ logger.warn("Relay tunnel: detected gateway.tls.enabled=true; local relay proxy still uses HTTP by default. If local RPC fails, set PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=https://127.0.0.1:<port> or disable gateway TLS.");
16280
+ }
16281
+ logger.info(`Relay tunnel: local gateway base URL ${gatewayBaseUrl.baseUrl} (source=${gatewayBaseUrl.source})`);
15098
16282
  const { gatewayAuthMode, gatewayToken, gatewayPassword } = resolveLocalGatewayAuth({
15099
16283
  stateDir: openclawDir,
15100
16284
  logger
@@ -15105,7 +16289,7 @@ function registerRelayTunnelLifecycle(deps) {
15105
16289
  authProvider,
15106
16290
  heartbeatSec: config.relay?.heartbeatSec,
15107
16291
  reconnectBackoffMs: config.relay?.reconnectBackoffMs,
15108
- gatewayBaseUrl: `http://localhost:${gatewayPort}`,
16292
+ gatewayBaseUrl: gatewayBaseUrl.baseUrl,
15109
16293
  gatewayAuthMode,
15110
16294
  gatewayToken,
15111
16295
  gatewayPassword,
@@ -15237,8 +16421,10 @@ function registerNotificationInterfaces(deps) {
15237
16421
  registerGatewayMethod,
15238
16422
  notifyNotificationSummaryStatus,
15239
16423
  notifyNotificationSummaryResult,
15240
- tunnelService
16424
+ tunnelService,
16425
+ agentToolsOnly = false
15241
16426
  } = deps;
16427
+ const registerGatewayMethodForMode = agentToolsOnly ? () => {} : registerGatewayMethod;
15242
16428
  const summaryJobs = new Map;
15243
16429
  function pruneSummaryJobs() {
15244
16430
  while (summaryJobs.size > SUMMARY_JOB_HISTORY_LIMIT) {
@@ -15330,7 +16516,7 @@ function registerNotificationInterfaces(deps) {
15330
16516
  function resolveSummaryNotificationsDir() {
15331
16517
  return getStorage()?.getStorageDir() ?? summaryNotificationsDir ?? null;
15332
16518
  }
15333
- registerGatewayMethod("notifications.push", async ({ params, respond }) => {
16519
+ registerGatewayMethodForMode("notifications.push", async ({ params, respond }) => {
15334
16520
  const storage2 = getStorage();
15335
16521
  if (!storage2) {
15336
16522
  respond(false, null, {
@@ -15358,7 +16544,7 @@ function registerNotificationInterfaces(deps) {
15358
16544
  logger.info(`notifications[${ingestId}]: ingest done in ${Date.now() - startMs}ms ` + `(received=${result.received} ingested=${result.ingested} ` + `dedupedById=${result.dedupedById} dedupedByContent=${result.dedupedByContent} invalid=${result.invalid})`);
15359
16545
  respond(true, toIngestResponse(result));
15360
16546
  });
15361
- registerGatewayMethod("notifications.summary.start", async ({ params, respond }) => {
16547
+ registerGatewayMethodForMode("notifications.summary.start", async ({ params, respond }) => {
15362
16548
  const notificationsDir = resolveSummaryNotificationsDir();
15363
16549
  if (!notificationsDir) {
15364
16550
  respond(false, null, {
@@ -15390,7 +16576,7 @@ function registerNotificationInterfaces(deps) {
15390
16576
  });
15391
16577
  });
15392
16578
  });
15393
- registerGatewayMethod("notifications.summary.status", async ({ params, respond }) => {
16579
+ registerGatewayMethodForMode("notifications.summary.status", async ({ params, respond }) => {
15394
16580
  const jobId = trimToUndefined2(params?.jobId);
15395
16581
  if (!jobId) {
15396
16582
  respond(false, null, {
@@ -15409,7 +16595,7 @@ function registerNotificationInterfaces(deps) {
15409
16595
  }
15410
16596
  respond(true, serializeSummaryJob(state));
15411
16597
  });
15412
- registerGatewayMethod("notifications.summary.result", async ({ params, respond }) => {
16598
+ registerGatewayMethodForMode("notifications.summary.result", async ({ params, respond }) => {
15413
16599
  const jobId = trimToUndefined2(params?.jobId);
15414
16600
  if (!jobId) {
15415
16601
  respond(false, null, {
@@ -15558,6 +16744,10 @@ function registerNotificationInterfaces(deps) {
15558
16744
  }
15559
16745
  }
15560
16746
  });
16747
+ if (agentToolsOnly) {
16748
+ logger.info("Agent 通知工具已注册: notifications_summary");
16749
+ return;
16750
+ }
15561
16751
  api.registerHttpRoute({
15562
16752
  path: "/notifications",
15563
16753
  auth: "gateway",
@@ -15991,6 +17181,76 @@ function registerImageInterfaces(deps) {
15991
17181
  logger.info("HTTP 图片端点已注册: POST /images");
15992
17182
  }
15993
17183
 
17184
+ // src/system/os-info.ts
17185
+ var import_node_fs37 = require("node:fs");
17186
+ var import_node_os4 = __toESM(require("node:os"));
17187
+ function defaultReadProcVersion() {
17188
+ try {
17189
+ return import_node_fs37.readFileSync("/proc/version", "utf-8");
17190
+ } catch {
17191
+ return null;
17192
+ }
17193
+ }
17194
+ function detectWsl(release, env, readProcVersion) {
17195
+ const distro = env.WSL_DISTRO_NAME?.trim() || null;
17196
+ const hasWslEnv = Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
17197
+ const haystacks = [release, readProcVersion() ?? ""].map((value) => value.toLowerCase());
17198
+ const kernelHit = haystacks.some((value) => value.includes("microsoft") || value.includes("wsl"));
17199
+ if (!hasWslEnv && !kernelHit) {
17200
+ return null;
17201
+ }
17202
+ const isWsl2 = Boolean(env.WSL_INTEROP) || haystacks.some((value) => value.includes("wsl2"));
17203
+ const version = isWsl2 ? 2 : kernelHit || hasWslEnv ? 1 : null;
17204
+ return { version, distro };
17205
+ }
17206
+ function detectOsInfo(probe = {}) {
17207
+ const platform = probe.platform ?? import_node_os4.default.platform();
17208
+ const arch = probe.arch ?? import_node_os4.default.arch();
17209
+ const release = probe.release ?? import_node_os4.default.release();
17210
+ const env = probe.env ?? process.env;
17211
+ const readProcVersion = probe.readProcVersion ?? defaultReadProcVersion;
17212
+ if (platform === "win32") {
17213
+ return { os: "windows", isWsl: false, platform, arch, release };
17214
+ }
17215
+ if (platform === "darwin") {
17216
+ return { os: "macos", isWsl: false, platform, arch, release };
17217
+ }
17218
+ if (platform === "linux") {
17219
+ const wsl = detectWsl(release, env, readProcVersion);
17220
+ if (wsl) {
17221
+ return { os: "wsl", isWsl: true, platform, arch, release, wsl };
17222
+ }
17223
+ return { os: "linux", isWsl: false, platform, arch, release };
17224
+ }
17225
+ return { os: "unknown", isWsl: false, platform, arch, release };
17226
+ }
17227
+
17228
+ // src/plugin/system.ts
17229
+ function buildOsResponse() {
17230
+ return detectOsInfo();
17231
+ }
17232
+ function registerSystemInterfaces(deps) {
17233
+ const { api, logger, registerGatewayMethod } = deps;
17234
+ registerGatewayMethod("system.os", async ({ respond }) => {
17235
+ respond(true, buildOsResponse());
17236
+ });
17237
+ api.registerHttpRoute({
17238
+ path: "/system/os",
17239
+ auth: "gateway",
17240
+ async handler(req, res) {
17241
+ if (req.method !== "GET") {
17242
+ res.writeHead(405, { "Content-Type": "application/json" });
17243
+ res.end(JSON.stringify({ ok: false, error: "Method Not Allowed" }));
17244
+ return;
17245
+ }
17246
+ res.writeHead(200, { "Content-Type": "application/json" });
17247
+ res.end(JSON.stringify({ ok: true, ...buildOsResponse() }));
17248
+ }
17249
+ });
17250
+ logger.info("Gateway 系统方法已注册: system.os");
17251
+ logger.info("HTTP 系统端点已注册: GET /system/os");
17252
+ }
17253
+
15994
17254
  // src/plugin/runtime-mode.ts
15995
17255
  var PLUGIN_CLI_COMMANDS = new Set(["ntf", "phone-notifications"]);
15996
17256
  var GLOBAL_OPTIONS_WITH_VALUE = [
@@ -16025,15 +17285,94 @@ function isPluginCliInvocation(argv = process.argv) {
16025
17285
  return primaryCommand ? PLUGIN_CLI_COMMANDS.has(primaryCommand) : false;
16026
17286
  }
16027
17287
 
17288
+ // src/plugin/runtime.ts
17289
+ class PluginRuntime {
17290
+ logger;
17291
+ lightRuleCtx;
17292
+ tunnelService = null;
17293
+ storage = null;
17294
+ recordingStorage = null;
17295
+ imageStorage = null;
17296
+ broadcastFn = null;
17297
+ storageReady = false;
17298
+ storageReadyListeners = [];
17299
+ broadcastChangedListeners = [];
17300
+ constructor(logger, stateDir) {
17301
+ this.logger = logger;
17302
+ this.lightRuleCtx = { stateDir };
17303
+ }
17304
+ getStorage() {
17305
+ return this.storage;
17306
+ }
17307
+ getRecordingStorage() {
17308
+ return this.recordingStorage;
17309
+ }
17310
+ getImageStorage() {
17311
+ return this.imageStorage;
17312
+ }
17313
+ getBroadcastFn() {
17314
+ return this.broadcastFn;
17315
+ }
17316
+ markStorageReady(params) {
17317
+ this.storage = params.storage;
17318
+ this.recordingStorage = params.recordingStorage;
17319
+ this.imageStorage = params.imageStorage;
17320
+ this.lightRuleCtx.workspaceDir = params.workspaceDir;
17321
+ this.lightRuleCtx.stateDir = params.stateDir;
17322
+ this.storageReady = true;
17323
+ this.emitAll(this.storageReadyListeners, "onStorageReady");
17324
+ }
17325
+ markStorageStopped() {
17326
+ this.storage = null;
17327
+ this.recordingStorage = null;
17328
+ this.imageStorage = null;
17329
+ this.storageReady = false;
17330
+ }
17331
+ onStorageReady(listener) {
17332
+ this.storageReadyListeners.push(listener);
17333
+ if (this.storageReady) {
17334
+ this.emitOne(listener, "onStorageReady");
17335
+ }
17336
+ }
17337
+ cacheBroadcast(broadcast) {
17338
+ if (!broadcast) {
17339
+ return;
17340
+ }
17341
+ const changed = this.broadcastFn !== broadcast;
17342
+ this.broadcastFn = broadcast;
17343
+ if (changed) {
17344
+ this.emitAll(this.broadcastChangedListeners, "onBroadcastChanged");
17345
+ }
17346
+ }
17347
+ onBroadcastChanged(listener) {
17348
+ this.broadcastChangedListeners.push(listener);
17349
+ if (this.broadcastFn) {
17350
+ this.emitOne(listener, "onBroadcastChanged");
17351
+ }
17352
+ }
17353
+ emitAll(listeners, name) {
17354
+ for (const listener of listeners) {
17355
+ this.emitOne(listener, name);
17356
+ }
17357
+ }
17358
+ emitOne(listener, name) {
17359
+ try {
17360
+ listener();
17361
+ } catch (err2) {
17362
+ this.logger.warn(`PluginRuntime: ${name} listener failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
17363
+ }
17364
+ }
17365
+ }
17366
+
16028
17367
  // src/index.ts
16029
17368
 
16030
17369
  var LIGHT_RULE_PROMPT_GUIDANCE = [
16031
- "灯效规则路由提示:",
17370
+ "灯效路由提示:",
16032
17371
  `- 当用户表达“收到/当/如果/有某类通知或消息时,亮灯/闪灯/变成某种灯效”时,这是云端持久灯效规则请求,必须调用 ${LIGHT_RULE_TOOL_NAMES.create} 或 ${LIGHT_RULE_TOOL_NAMES.update}。`,
17372
+ "- 当用户要求现在/立即亮灯,或在当前一次性任务完成后亮灯时,必须调用 light_control;不要创建灯效规则。",
16033
17373
  "- 创建规则时把用户的自然语言诉求整理成 ruleText;由 Notification Intelligence Service 的独立 Agent 编译和保存规则。",
16034
17374
  "- 灯效规则的创建、修改、删除和查询都走 lightrules_* 工具;不要手写或编辑 tasks/*/meta.json。",
16035
- "- 如果 lightrules_* 工具返回错误,必须如实说明本次查询/修改失败;不要使用聊天历史或旧 toolResult 里的规则列表冒充当前云端结果。",
16036
- "- 插件不再提供 OpenClaw 层的一次性 light_control,下发、评估和亮灯原因由云端服务处理。"
17375
+ "- 如果 lightrules_* 工具返回错误,必须如实说明本次查询/修改失败;不要使用聊天历史或旧 toolResult 里的规则列表冒充当前云端结果。"
16037
17376
  ].join(`
16038
17377
  `);
16039
17378
  var src_default = {
@@ -16042,16 +17381,12 @@ var src_default = {
16042
17381
  register(api) {
16043
17382
  const config = api.pluginConfig ?? {};
16044
17383
  const ignoredApps = new Set(config.ignoredApps ?? []);
16045
- let storage2 = null;
16046
- let recordingStorage = null;
16047
- let imageStorage = null;
16048
- let broadcastFn = null;
16049
- let autoUpdateLifecycle = null;
16050
- let tunnelService = null;
16051
17384
  const openclawDir = resolvePluginStateDir(api);
16052
17385
  const logger = openclawDir ? new PluginFileLogger(api.logger, openclawDir) : createVersionAwareLogger(api.logger);
17386
+ const registrationMode = api.registrationMode ?? "(unknown)";
17387
+ logger.info(`register() 调用: registrationMode=${registrationMode} cliInvocation=${isPluginCliInvocation()}`);
16053
17388
  ensureNtfBinInstalled({
16054
- ntfCjsPath: import_node_path31.join(__dirname, "bin", "ntf.cjs"),
17389
+ ntfCjsPath: import_node_path33.join(__dirname, "bin", "ntf.cjs"),
16055
17390
  stateDir: openclawDir,
16056
17391
  logger
16057
17392
  });
@@ -16063,19 +17398,10 @@ var src_default = {
16063
17398
  logger.info("检测到插件 CLI 命令执行上下文,仅注册 CLI,跳过后台服务以避免影响 Relay tunnel 常驻连接");
16064
17399
  return;
16065
17400
  }
17401
+ const { runtime, firstAssembly } = claimWiredRuntime(openclawDir, () => new PluginRuntime(logger, openclawDir));
16066
17402
  api.on("before_prompt_build", () => ({
16067
17403
  appendSystemContext: LIGHT_RULE_PROMPT_GUIDANCE
16068
17404
  }));
16069
- function cacheBroadcast(broadcast) {
16070
- if (!broadcast) {
16071
- return;
16072
- }
16073
- const changed = broadcastFn !== broadcast;
16074
- broadcastFn = broadcast;
16075
- if (changed) {
16076
- autoUpdateLifecycle?.notifyBroadcastReady();
16077
- }
16078
- }
16079
17405
  function isDirectMobileGatewayRequest(opts) {
16080
17406
  const client = opts.client?.connect?.client;
16081
17407
  if (!client)
@@ -16086,18 +17412,18 @@ var src_default = {
16086
17412
  return client.id === "openclaw-ios" || client.id === "openclaw-android";
16087
17413
  }
16088
17414
  async function deactivateRelayForDirectGatewayRequest(method, opts) {
16089
- if (!tunnelService || !isDirectMobileGatewayRequest(opts))
17415
+ if (!runtime.tunnelService || !isDirectMobileGatewayRequest(opts))
16090
17416
  return;
16091
17417
  const client = opts.client?.connect?.client;
16092
17418
  try {
16093
- await tunnelService.deactivateForExternalTunnel(`gateway method ${method} from ${client?.id ?? "unknown"}`);
17419
+ await runtime.tunnelService.deactivateForExternalTunnel(`gateway method ${method} from ${client?.id ?? "unknown"}`);
16094
17420
  } catch (err2) {
16095
17421
  logger.warn(`Relay tunnel: failed to deactivate after direct gateway method ${method}: ${err2?.message ?? String(err2)}`);
16096
17422
  }
16097
17423
  }
16098
17424
  function registerGatewayMethodWithBroadcastCapture(method, handler) {
16099
17425
  api.registerGatewayMethod(method, async (opts) => {
16100
- cacheBroadcast(opts.context?.broadcast);
17426
+ runtime.cacheBroadcast(opts.context?.broadcast);
16101
17427
  await deactivateRelayForDirectGatewayRequest(method, opts);
16102
17428
  return handler(opts);
16103
17429
  });
@@ -16106,7 +17432,8 @@ var src_default = {
16106
17432
  return items.filter((notification) => !ignoredApps.has(notification.app));
16107
17433
  }
16108
17434
  function notifyRecordingStatus(event) {
16109
- const relayEmitted = tunnelService?.emitEvent("recording.status", event) ?? false;
17435
+ const relayEmitted = runtime.tunnelService?.emitEvent("recording.status", event) ?? false;
17436
+ const broadcastFn = runtime.getBroadcastFn();
16110
17437
  if (!broadcastFn) {
16111
17438
  if (relayEmitted)
16112
17439
  return;
@@ -16116,7 +17443,8 @@ var src_default = {
16116
17443
  broadcastFn("recording.status", event);
16117
17444
  }
16118
17445
  function notifyNotificationSummaryStatus(event) {
16119
- const relayEmitted = tunnelService?.emitEvent(NOTIFICATION_SUMMARY_STATUS_EVENT, event) ?? false;
17446
+ const relayEmitted = runtime.tunnelService?.emitEvent(NOTIFICATION_SUMMARY_STATUS_EVENT, event) ?? false;
17447
+ const broadcastFn = runtime.getBroadcastFn();
16120
17448
  if (!broadcastFn) {
16121
17449
  if (relayEmitted)
16122
17450
  return;
@@ -16128,7 +17456,8 @@ var src_default = {
16128
17456
  broadcastFn(NOTIFICATION_SUMMARY_STATUS_EVENT, event);
16129
17457
  }
16130
17458
  function notifyNotificationSummaryResult(event) {
16131
- const relayEmitted = tunnelService?.emitEvent(NOTIFICATION_SUMMARY_RESULT_EVENT, event) ?? false;
17459
+ const relayEmitted = runtime.tunnelService?.emitEvent(NOTIFICATION_SUMMARY_RESULT_EVENT, event) ?? false;
17460
+ const broadcastFn = runtime.getBroadcastFn();
16132
17461
  if (!broadcastFn) {
16133
17462
  if (relayEmitted)
16134
17463
  return;
@@ -16140,28 +17469,36 @@ var src_default = {
16140
17469
  broadcastFn(NOTIFICATION_SUMMARY_RESULT_EVENT, event);
16141
17470
  }
16142
17471
  const lightRuleClient = new NotificationIntelligenceLightRuleClient({ logger });
16143
- const lightRuleCtx = {
16144
- stateDir: openclawDir
16145
- };
16146
- const lightRuleRegistry = new LightRuleRegistry(lightRuleCtx);
17472
+ if (!firstAssembly) {
17473
+ registerNotificationInterfaces({
17474
+ api,
17475
+ logger,
17476
+ getStorage: () => runtime.getStorage() ?? getSharedNotificationStorage(openclawDir),
17477
+ summaryNotificationsDir: getStateFallbackNotificationDir(openclawDir),
17478
+ filterNotifications,
17479
+ registerGatewayMethod: () => {},
17480
+ notifyNotificationSummaryStatus,
17481
+ notifyNotificationSummaryResult,
17482
+ tunnelService: runtime.tunnelService,
17483
+ agentToolsOnly: true
17484
+ });
17485
+ registerLightControlTool(api, logger);
17486
+ registerLightRulesTools(api, lightRuleClient, logger);
17487
+ registerAutoUpdateAgentTool({ api, config, logger });
17488
+ logger.info(`register(): 本进程已完成后台装配(openclawDir=${openclawDir ?? "(default)"}),` + "已为当前 Agent collector 补注册工具,跳过重复后台 wiring");
17489
+ return;
17490
+ }
17491
+ const lightRuleRegistry = new LightRuleRegistry(runtime.lightRuleCtx);
17492
+ runtime.onStorageReady(() => {
17493
+ lightRuleRegistry.reload();
17494
+ });
16147
17495
  registerStorageLifecycle({
16148
17496
  api,
16149
17497
  logger,
16150
- lightRuleCtx,
16151
- setStorage(nextStorage) {
16152
- storage2 = nextStorage;
16153
- },
16154
- setRecordingStorage(nextRecordingStorage) {
16155
- recordingStorage = nextRecordingStorage;
16156
- },
16157
- setImageStorage(nextImageStorage) {
16158
- imageStorage = nextImageStorage;
16159
- },
16160
- onStorageReady() {
16161
- lightRuleRegistry.reload();
16162
- }
17498
+ runtime,
17499
+ sharedStorageKey: openclawDir
16163
17500
  });
16164
- tunnelService = registerRelayTunnelLifecycle({
17501
+ runtime.tunnelService = registerRelayTunnelLifecycle({
16165
17502
  api,
16166
17503
  config,
16167
17504
  logger,
@@ -16170,41 +17507,48 @@ var src_default = {
16170
17507
  registerNotificationInterfaces({
16171
17508
  api,
16172
17509
  logger,
16173
- getStorage: () => storage2,
17510
+ getStorage: () => runtime.getStorage() ?? getSharedNotificationStorage(openclawDir),
16174
17511
  summaryNotificationsDir: getStateFallbackNotificationDir(openclawDir),
16175
17512
  filterNotifications,
16176
17513
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
16177
17514
  notifyNotificationSummaryStatus,
16178
17515
  notifyNotificationSummaryResult,
16179
- tunnelService
17516
+ tunnelService: runtime.tunnelService
16180
17517
  });
16181
17518
  registerRecordingInterfaces({
16182
17519
  api,
16183
17520
  logger,
16184
- getRecordingStorage: () => recordingStorage,
17521
+ getRecordingStorage: () => runtime.getRecordingStorage() ?? getSharedRecordingStorage(openclawDir),
16185
17522
  notifyRecordingStatus,
16186
17523
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
16187
- tunnelService
17524
+ tunnelService: runtime.tunnelService
16188
17525
  });
16189
17526
  registerImageInterfaces({
16190
17527
  api,
16191
17528
  logger,
16192
- getImageStorage: () => imageStorage,
17529
+ getImageStorage: () => runtime.getImageStorage() ?? getSharedImageStorage(openclawDir),
16193
17530
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
16194
17531
  maxBytes: config.image?.maxBytes ?? 0,
16195
- tunnelService
17532
+ tunnelService: runtime.tunnelService
17533
+ });
17534
+ registerSystemInterfaces({
17535
+ api,
17536
+ logger,
17537
+ registerGatewayMethod: registerGatewayMethodWithBroadcastCapture
16196
17538
  });
16197
- registerLightRulesGateway(api, lightRuleRegistry, logger, cacheBroadcast);
17539
+ registerLightControlTool(api, logger);
17540
+ registerLightRulesGateway(api, lightRuleRegistry, logger, (broadcast) => runtime.cacheBroadcast(broadcast));
16198
17541
  registerLightRulesTools(api, lightRuleClient, logger);
16199
17542
  logger.info(`灯效规则方法已注册: gateway=${LIGHT_RULE_GATEWAY_METHOD_LIST.join(", ")}; tools=${LIGHT_RULE_TOOL_NAME_LIST.join(", ")}`);
16200
- autoUpdateLifecycle = registerAutoUpdateLifecycle({
17543
+ const autoUpdateLifecycle = registerAutoUpdateLifecycle({
16201
17544
  api,
16202
17545
  config,
16203
17546
  logger,
16204
- getBroadcastFn: () => broadcastFn,
16205
- cacheBroadcast,
16206
- tunnelService
17547
+ getBroadcastFn: () => runtime.getBroadcastFn(),
17548
+ cacheBroadcast: (broadcast) => runtime.cacheBroadcast(broadcast),
17549
+ tunnelService: runtime.tunnelService
16207
17550
  });
17551
+ runtime.onBroadcastChanged(() => autoUpdateLifecycle.notifyBroadcastReady());
16208
17552
  registerPluginCli(api, {
16209
17553
  logger,
16210
17554
  openclawDir
@@ -16212,5 +17556,5 @@ var src_default = {
16212
17556
  }
16213
17557
  };
16214
17558
 
16215
- //# debugId=1C4AEF73472B01C364756E2164756E21
17559
+ //# debugId=3D7085ABD8318D3264756E2164756E21
16216
17560
  //# sourceMappingURL=index.cjs.map