@yoooclaw/phone-notifications 1.10.3 → 1.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -206,9 +206,38 @@ var init_credentials = __esm({
206
206
  });
207
207
 
208
208
  // src/env.ts
209
+ function writeDotEnv(key, value) {
210
+ const path2 = resolveStateFile(".env");
211
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
212
+ const existing = (0, import_node_fs9.existsSync)(path2) ? (0, import_node_fs9.readFileSync)(path2, "utf-8") : "";
213
+ const lines = existing.split("\n");
214
+ const prefix = `${key}=`;
215
+ const idx = lines.findIndex((l) => l.startsWith(prefix));
216
+ const newLine = `${prefix}${value}`;
217
+ if (idx >= 0) {
218
+ lines[idx] = newLine;
219
+ } else {
220
+ if (existing && !existing.endsWith("\n")) lines.push("");
221
+ lines.push(newLine);
222
+ }
223
+ (0, import_node_fs9.writeFileSync)(path2, lines.join("\n"), "utf-8");
224
+ }
225
+ function readDotEnv() {
226
+ const path2 = resolveStateFile(".env");
227
+ if (!(0, import_node_fs9.existsSync)(path2)) return {};
228
+ return Object.fromEntries(
229
+ (0, import_node_fs9.readFileSync)(path2, "utf-8").split("\n").flatMap((line) => {
230
+ const eq = line.indexOf("=");
231
+ if (eq < 1) return [];
232
+ return [[line.slice(0, eq).trim(), line.slice(eq + 1).trim()]];
233
+ })
234
+ );
235
+ }
209
236
  function loadEnvName() {
210
- const fromEnvVar = process.env.OPENCLAW_ENV?.trim();
237
+ const fromEnvVar = process.env.PHONE_NOTIFICATIONS_ENV?.trim();
211
238
  if (fromEnvVar && VALID_ENVS.has(fromEnvVar)) return fromEnvVar;
239
+ const fromDotEnv = readDotEnv()["PHONE_NOTIFICATIONS_ENV"]?.trim();
240
+ if (fromDotEnv && VALID_ENVS.has(fromDotEnv)) return fromDotEnv;
212
241
  const { env } = readCredentials();
213
242
  if (env && VALID_ENVS.has(env)) return env;
214
243
  return "production";
@@ -219,7 +248,7 @@ function saveEnvName(env) {
219
248
  `\u65E0\u6548\u7684\u73AF\u5883\u540D\u79F0: ${env}\uFF0C\u53EF\u9009\u503C: ${[...VALID_ENVS].join(", ")}`
220
249
  );
221
250
  }
222
- writeCredentials({ ...readCredentials(), env });
251
+ writeDotEnv("PHONE_NOTIFICATIONS_ENV", env);
223
252
  }
224
253
  function getEnvUrls(env) {
225
254
  return ENV_CONFIG[env ?? loadEnvName()];
@@ -227,11 +256,14 @@ function getEnvUrls(env) {
227
256
  function getAvailableEnvs() {
228
257
  return Object.keys(ENV_CONFIG);
229
258
  }
230
- var ENV_CONFIG, VALID_ENVS;
259
+ var import_node_fs9, import_node_path8, ENV_CONFIG, VALID_ENVS;
231
260
  var init_env = __esm({
232
261
  "src/env.ts"() {
233
262
  "use strict";
263
+ import_node_fs9 = require("fs");
264
+ import_node_path8 = require("path");
234
265
  init_credentials();
266
+ init_host();
235
267
  ENV_CONFIG = {
236
268
  development: {
237
269
  lightApiUrl: "https://openclaw-service-dev.yoooclaw.com/api/message/tob/sendMessage",
@@ -500,20 +532,20 @@ function selectModelByMemory(availableGB, isAppleSilicon = false) {
500
532
  return "tiny";
501
533
  }
502
534
  function resolveModelsDir(dataDir) {
503
- const dir = (0, import_node_path21.join)(dataDir, WHISPER_MODELS_DIR);
504
- (0, import_node_fs25.mkdirSync)(dir, { recursive: true });
535
+ const dir = (0, import_node_path22.join)(dataDir, WHISPER_MODELS_DIR);
536
+ (0, import_node_fs26.mkdirSync)(dir, { recursive: true });
505
537
  return dir;
506
538
  }
507
539
  function resolveBinDir(dataDir) {
508
- const dir = (0, import_node_path21.join)(dataDir, WHISPER_BIN_DIR);
509
- (0, import_node_fs25.mkdirSync)(dir, { recursive: true });
540
+ const dir = (0, import_node_path22.join)(dataDir, WHISPER_BIN_DIR);
541
+ (0, import_node_fs26.mkdirSync)(dir, { recursive: true });
510
542
  return dir;
511
543
  }
512
544
  function isModelDownloaded(modelsDir, modelSize) {
513
- const modelPath = (0, import_node_path21.join)(modelsDir, MODEL_FILENAMES[modelSize]);
514
- if (!(0, import_node_fs25.existsSync)(modelPath)) return false;
545
+ const modelPath = (0, import_node_path22.join)(modelsDir, MODEL_FILENAMES[modelSize]);
546
+ if (!(0, import_node_fs26.existsSync)(modelPath)) return false;
515
547
  try {
516
- const stat = (0, import_node_fs25.statSync)(modelPath);
548
+ const stat = (0, import_node_fs26.statSync)(modelPath);
517
549
  const expectedSize = MODEL_DISK_SIZES[modelSize];
518
550
  return stat.size >= expectedSize * 0.8;
519
551
  } catch {
@@ -522,7 +554,7 @@ function isModelDownloaded(modelsDir, modelSize) {
522
554
  }
523
555
  async function downloadModel(modelsDir, modelSize, logger, modelSource, mirrorUrl) {
524
556
  const filename = MODEL_FILENAMES[modelSize];
525
- const modelPath = (0, import_node_path21.join)(modelsDir, filename);
557
+ const modelPath = (0, import_node_path22.join)(modelsDir, filename);
526
558
  if (isModelDownloaded(modelsDir, modelSize)) {
527
559
  logger.info(`[whisper-local] \u6A21\u578B\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u4E0B\u8F7D: ${filename}`);
528
560
  return { ok: true, modelPath };
@@ -578,9 +610,9 @@ async function probeUrl(url, logger) {
578
610
  } finally {
579
611
  clearTimeout(timer);
580
612
  }
581
- } catch (err) {
613
+ } catch (err2) {
582
614
  logger.info(
583
- `[whisper-local] \u8FDE\u901A\u6027\u63A2\u6D4B\u5931\u8D25: ${url} (${err?.name ?? err?.message ?? "unknown"})`
615
+ `[whisper-local] \u8FDE\u901A\u6027\u63A2\u6D4B\u5931\u8D25: ${url} (${err2?.name ?? err2?.message ?? "unknown"})`
584
616
  );
585
617
  return false;
586
618
  }
@@ -588,7 +620,7 @@ async function probeUrl(url, logger) {
588
620
  async function downloadFromUrl(url, modelPath, logger) {
589
621
  const tmpPath = `${modelPath}.downloading`;
590
622
  try {
591
- (0, import_node_fs25.mkdirSync)((0, import_node_path21.dirname)(modelPath), { recursive: true });
623
+ (0, import_node_fs26.mkdirSync)((0, import_node_path22.dirname)(modelPath), { recursive: true });
592
624
  const controller = new AbortController();
593
625
  const timer = setTimeout(() => controller.abort(), 30 * 60 * 1e3);
594
626
  try {
@@ -604,7 +636,7 @@ async function downloadFromUrl(url, modelPath, logger) {
604
636
  return { ok: false, modelPath, error: "\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25: \u54CD\u5E94\u4F53\u4E3A\u7A7A" };
605
637
  }
606
638
  const contentLength = Number(res.headers.get("content-length") ?? 0);
607
- const writeStream = (0, import_node_fs25.createWriteStream)(tmpPath);
639
+ const writeStream = (0, import_node_fs26.createWriteStream)(tmpPath);
608
640
  const readable = import_node_stream2.Readable.fromWeb(res.body);
609
641
  let downloaded = 0;
610
642
  let lastLogPercent = 0;
@@ -626,17 +658,17 @@ async function downloadFromUrl(url, modelPath, logger) {
626
658
  }
627
659
  const { renameSync: renameSync2 } = await import("fs");
628
660
  renameSync2(tmpPath, modelPath);
629
- const fileSize = (0, import_node_fs25.statSync)(modelPath).size;
661
+ const fileSize = (0, import_node_fs26.statSync)(modelPath).size;
630
662
  logger.info(
631
- `[whisper-local] \u6A21\u578B\u4E0B\u8F7D\u5B8C\u6210: ${(0, import_node_path21.basename)(modelPath)} (${formatBytes2(fileSize)})`
663
+ `[whisper-local] \u6A21\u578B\u4E0B\u8F7D\u5B8C\u6210: ${(0, import_node_path22.basename)(modelPath)} (${formatBytes2(fileSize)})`
632
664
  );
633
665
  return { ok: true, modelPath };
634
- } catch (err) {
666
+ } catch (err2) {
635
667
  try {
636
- if ((0, import_node_fs25.existsSync)(tmpPath)) (0, import_node_fs25.unlinkSync)(tmpPath);
668
+ if ((0, import_node_fs26.existsSync)(tmpPath)) (0, import_node_fs26.unlinkSync)(tmpPath);
637
669
  } catch {
638
670
  }
639
- const msg = err?.name === "AbortError" ? "\u6A21\u578B\u4E0B\u8F7D\u8D85\u65F6\uFF0830 \u5206\u949F\uFF09" : err?.message ?? String(err);
671
+ const msg = err2?.name === "AbortError" ? "\u6A21\u578B\u4E0B\u8F7D\u8D85\u65F6\uFF0830 \u5206\u949F\uFF09" : err2?.message ?? String(err2);
640
672
  logger.error(`[whisper-local] \u6A21\u578B\u4E0B\u8F7D\u5931\u8D25: ${msg}`);
641
673
  return { ok: false, modelPath, error: msg };
642
674
  }
@@ -645,8 +677,8 @@ function findWhisperBinary(dataDir, logger) {
645
677
  const binDir = resolveBinDir(dataDir);
646
678
  const binNames = (0, import_node_os4.platform)() === "win32" ? ["whisper-cli.exe", "whisper.exe", "main.exe"] : ["whisper-cli", "whisper", "main"];
647
679
  for (const name of binNames) {
648
- const binPath = (0, import_node_path21.join)(binDir, name);
649
- if ((0, import_node_fs25.existsSync)(binPath)) {
680
+ const binPath = (0, import_node_path22.join)(binDir, name);
681
+ if ((0, import_node_fs26.existsSync)(binPath)) {
650
682
  logger.info(`[whisper-local] \u627E\u5230\u672C\u5730\u4E8C\u8FDB\u5236: ${binPath}`);
651
683
  return binPath;
652
684
  }
@@ -697,7 +729,7 @@ async function transcribeWithWhisperLocal(audioFilePath, localConfig, dataDir, l
697
729
  return { ok: false, error: `\u6A21\u578B\u4E0B\u8F7D\u5931\u8D25: ${downloadResult.error}` };
698
730
  }
699
731
  }
700
- const modelPath = (0, import_node_path21.join)(modelsDir, MODEL_FILENAMES[modelSize]);
732
+ const modelPath = (0, import_node_path22.join)(modelsDir, MODEL_FILENAMES[modelSize]);
701
733
  let inputPath = audioFilePath;
702
734
  let tmpWavPath = null;
703
735
  const actualFmt = detectAudioFormat(audioFilePath);
@@ -733,9 +765,9 @@ async function transcribeWithWhisperLocal(audioFilePath, localConfig, dataDir, l
733
765
  // 100MB stdout buffer
734
766
  stdio: ["pipe", "pipe", "pipe"]
735
767
  });
736
- } catch (err) {
768
+ } catch (err2) {
737
769
  cleanupTmpWav(tmpWavPath);
738
- return { ok: false, error: `whisper.cpp \u6267\u884C\u5931\u8D25: ${err?.message ?? err}` };
770
+ return { ok: false, error: `whisper.cpp \u6267\u884C\u5931\u8D25: ${err2?.message ?? err2}` };
739
771
  }
740
772
  if (result.status !== 0) {
741
773
  cleanupTmpWav(tmpWavPath);
@@ -749,10 +781,10 @@ async function transcribeWithWhisperLocal(audioFilePath, localConfig, dataDir, l
749
781
  logger.info(`[whisper-local] \u8F6C\u5199\u8017\u65F6: ${Math.round(elapsed / 1e3)}s`);
750
782
  const jsonPath = inputPath + ".json";
751
783
  let jsonContent;
752
- if ((0, import_node_fs25.existsSync)(jsonPath)) {
753
- jsonContent = (0, import_node_fs25.readFileSync)(jsonPath, "utf-8");
784
+ if ((0, import_node_fs26.existsSync)(jsonPath)) {
785
+ jsonContent = (0, import_node_fs26.readFileSync)(jsonPath, "utf-8");
754
786
  try {
755
- (0, import_node_fs25.unlinkSync)(jsonPath);
787
+ (0, import_node_fs26.unlinkSync)(jsonPath);
756
788
  } catch {
757
789
  }
758
790
  } else {
@@ -760,9 +792,9 @@ async function transcribeWithWhisperLocal(audioFilePath, localConfig, dataDir, l
760
792
  }
761
793
  cleanupTmpWav(tmpWavPath);
762
794
  return parseWhisperOutput(jsonContent, logger);
763
- } catch (err) {
795
+ } catch (err2) {
764
796
  cleanupTmpWav(tmpWavPath);
765
- return { ok: false, error: `whisper.cpp \u8F6C\u5199\u5F02\u5E38: ${err?.message ?? err}` };
797
+ return { ok: false, error: `whisper.cpp \u8F6C\u5199\u5F02\u5E38: ${err2?.message ?? err2}` };
766
798
  }
767
799
  }
768
800
  function getWhisperLocalStatus(dataDir, logger) {
@@ -830,10 +862,10 @@ function getPhysicalCoreCount() {
830
862
  }
831
863
  function detectAudioFormat(filePath) {
832
864
  try {
833
- const fd = (0, import_node_fs25.openSync)(filePath, "r");
865
+ const fd = (0, import_node_fs26.openSync)(filePath, "r");
834
866
  const buf = Buffer.alloc(12);
835
- (0, import_node_fs25.readSync)(fd, buf, 0, 12, 0);
836
- (0, import_node_fs25.closeSync)(fd);
867
+ (0, import_node_fs26.readSync)(fd, buf, 0, 12, 0);
868
+ (0, import_node_fs26.closeSync)(fd);
837
869
  const header = buf.toString("ascii", 0, 4);
838
870
  const header8 = buf.toString("ascii", 0, 8);
839
871
  if (header === "RIFF" && buf.toString("ascii", 8, 12) === "WAVE") return ".wav";
@@ -868,7 +900,7 @@ function convertToWav(inputPath, outputPath, actualFmt, logger) {
868
900
  timeout: 12e4,
869
901
  stdio: ["pipe", "pipe", "pipe"]
870
902
  });
871
- if (ffmpegResult.status === 0 && (0, import_node_fs25.existsSync)(outputPath)) {
903
+ if (ffmpegResult.status === 0 && (0, import_node_fs26.existsSync)(outputPath)) {
872
904
  logger.info(`[whisper-local] ffmpeg \u8F6C\u6362\u5B8C\u6210: ${outputPath}`);
873
905
  return { ok: true };
874
906
  }
@@ -881,7 +913,7 @@ function convertToWav(inputPath, outputPath, actualFmt, logger) {
881
913
  ["--rate", "16000", "--mono", inputPath, outputPath],
882
914
  { encoding: "utf-8", timeout: 12e4, stdio: ["pipe", "pipe", "pipe"] }
883
915
  );
884
- if (opusResult.status === 0 && (0, import_node_fs25.existsSync)(outputPath)) {
916
+ if (opusResult.status === 0 && (0, import_node_fs26.existsSync)(outputPath)) {
885
917
  logger.info(`[whisper-local] opusdec \u8F6C\u6362\u5B8C\u6210: ${outputPath}`);
886
918
  return { ok: true };
887
919
  }
@@ -895,7 +927,7 @@ function convertToWav(inputPath, outputPath, actualFmt, logger) {
895
927
  if (detectedExt && !inputPath.endsWith(detectedExt)) {
896
928
  tmpCopy = inputPath + ".detected" + detectedExt;
897
929
  try {
898
- (0, import_node_fs25.copyFileSync)(inputPath, tmpCopy);
930
+ (0, import_node_fs26.copyFileSync)(inputPath, tmpCopy);
899
931
  actualInputPath = tmpCopy;
900
932
  logger.info(
901
933
  `[whisper-local] \u68C0\u6D4B\u5230\u5B9E\u9645\u683C\u5F0F ${detectedExt}\uFF0C\u4E34\u65F6\u91CD\u547D\u540D`
@@ -919,35 +951,35 @@ function convertToWav(inputPath, outputPath, actualFmt, logger) {
919
951
  timeout: 12e4,
920
952
  stdio: ["pipe", "pipe", "pipe"]
921
953
  });
922
- if (tmpCopy && (0, import_node_fs25.existsSync)(tmpCopy)) {
954
+ if (tmpCopy && (0, import_node_fs26.existsSync)(tmpCopy)) {
923
955
  try {
924
- (0, import_node_fs25.unlinkSync)(tmpCopy);
956
+ (0, import_node_fs26.unlinkSync)(tmpCopy);
925
957
  } catch {
926
958
  }
927
959
  }
928
- if (afResult.status === 0 && (0, import_node_fs25.existsSync)(outputPath)) {
960
+ if (afResult.status === 0 && (0, import_node_fs26.existsSync)(outputPath)) {
929
961
  logger.info(`[whisper-local] afconvert \u8F6C\u6362\u5B8C\u6210: ${outputPath}`);
930
962
  return { ok: true };
931
963
  }
932
964
  const stderr = afResult.stderr?.slice(0, 200) ?? "";
933
965
  return { ok: false, error: `afconvert \u8F6C\u6362\u5931\u8D25 (exit ${afResult.status}): ${stderr}` };
934
- } catch (err) {
935
- if (tmpCopy && (0, import_node_fs25.existsSync)(tmpCopy)) {
966
+ } catch (err2) {
967
+ if (tmpCopy && (0, import_node_fs26.existsSync)(tmpCopy)) {
936
968
  try {
937
- (0, import_node_fs25.unlinkSync)(tmpCopy);
969
+ (0, import_node_fs26.unlinkSync)(tmpCopy);
938
970
  } catch {
939
971
  }
940
972
  }
941
- return { ok: false, error: `afconvert \u4E0D\u53EF\u7528: ${err?.message}` };
973
+ return { ok: false, error: `afconvert \u4E0D\u53EF\u7528: ${err2?.message}` };
942
974
  }
943
975
  }
944
976
  const fmtHint = actualFmt === ".ogg" ? "OGG/Opus \u683C\u5F0F\u9700\u8981 ffmpeg \u6216 opus-tools\uFF08brew install opus-tools\uFF09" : "\u8BF7\u5B89\u88C5 ffmpeg\uFF08brew install ffmpeg\uFF09\u6216\u786E\u4FDD\u97F3\u9891\u6587\u4EF6\u4E3A WAV \u683C\u5F0F";
945
977
  return { ok: false, error: `\u65E0\u6CD5\u5C06\u97F3\u9891\u8F6C\u6362\u4E3A WAV \u683C\u5F0F\u3002${fmtHint}` };
946
978
  }
947
979
  function cleanupTmpWav(path2) {
948
- if (path2 && (0, import_node_fs25.existsSync)(path2)) {
980
+ if (path2 && (0, import_node_fs26.existsSync)(path2)) {
949
981
  try {
950
- (0, import_node_fs25.unlinkSync)(path2);
982
+ (0, import_node_fs26.unlinkSync)(path2);
951
983
  } catch {
952
984
  }
953
985
  }
@@ -1011,8 +1043,8 @@ function parseWhisperOutput(stdout, logger) {
1011
1043
  text: fullText,
1012
1044
  segments
1013
1045
  };
1014
- } catch (err) {
1015
- logger.warn(`[whisper-local] JSON \u89E3\u6790\u5931\u8D25\uFF0C\u5C1D\u8BD5\u7EAF\u6587\u672C: ${err?.message}`);
1046
+ } catch (err2) {
1047
+ logger.warn(`[whisper-local] JSON \u89E3\u6790\u5931\u8D25\uFF0C\u5C1D\u8BD5\u7EAF\u6587\u672C: ${err2?.message}`);
1016
1048
  return {
1017
1049
  ok: true,
1018
1050
  text: stdout.trim(),
@@ -1033,13 +1065,13 @@ function formatBytes2(bytes) {
1033
1065
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1034
1066
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
1035
1067
  }
1036
- var import_node_child_process2, import_node_fs25, import_node_path21, import_promises3, import_node_stream2, import_node_os4, WHISPER_MODELS_DIR, WHISPER_BIN_DIR, HF_MODEL_URL_TEMPLATE, MODELSCOPE_MODEL_URL_TEMPLATE, PROBE_TIMEOUT_MS, MODEL_FILENAMES, MODEL_DISK_SIZES;
1068
+ var import_node_child_process2, import_node_fs26, import_node_path22, import_promises3, import_node_stream2, import_node_os4, WHISPER_MODELS_DIR, WHISPER_BIN_DIR, HF_MODEL_URL_TEMPLATE, MODELSCOPE_MODEL_URL_TEMPLATE, PROBE_TIMEOUT_MS, MODEL_FILENAMES, MODEL_DISK_SIZES;
1037
1069
  var init_whisper_local = __esm({
1038
1070
  "src/recording/whisper-local.ts"() {
1039
1071
  "use strict";
1040
1072
  import_node_child_process2 = require("child_process");
1041
- import_node_fs25 = require("fs");
1042
- import_node_path21 = require("path");
1073
+ import_node_fs26 = require("fs");
1074
+ import_node_path22 = require("path");
1043
1075
  import_promises3 = require("stream/promises");
1044
1076
  import_node_stream2 = require("stream");
1045
1077
  import_node_os4 = require("os");
@@ -1194,7 +1226,7 @@ async function initializeAsr(config, dataDir, logger) {
1194
1226
  }
1195
1227
  }
1196
1228
  async function transcribeAudio(audioFilePath, config, logger, options = {}) {
1197
- if (!(0, import_node_fs26.existsSync)(audioFilePath)) {
1229
+ if (!(0, import_node_fs27.existsSync)(audioFilePath)) {
1198
1230
  return { ok: false, error: `\u97F3\u9891\u6587\u4EF6\u4E0D\u5B58\u5728: ${audioFilePath}` };
1199
1231
  }
1200
1232
  logger.info(
@@ -1218,8 +1250,8 @@ async function transcribeAudio(audioFilePath, config, logger, options = {}) {
1218
1250
  error: `\u672A\u77E5\u7684 ASR mode: ${config.mode}`
1219
1251
  };
1220
1252
  }
1221
- } catch (err) {
1222
- const msg = err?.message ?? String(err);
1253
+ } catch (err2) {
1254
+ const msg = err2?.message ?? String(err2);
1223
1255
  logger.error(`[asr] \u8F6C\u5199\u5F02\u5E38: ${msg}`);
1224
1256
  return { ok: false, error: msg };
1225
1257
  }
@@ -1338,8 +1370,8 @@ async function runTranscriptionWorkflow(params) {
1338
1370
  createdAt
1339
1371
  );
1340
1372
  const transcriptDataFilename = buildTranscriptDataFilename(recordingId);
1341
- const transcriptDataPath = (0, import_node_path22.join)(transcriptDataDir, transcriptDataFilename);
1342
- (0, import_node_fs26.writeFileSync)(
1373
+ const transcriptDataPath = (0, import_node_path23.join)(transcriptDataDir, transcriptDataFilename);
1374
+ (0, import_node_fs27.writeFileSync)(
1343
1375
  transcriptDataPath,
1344
1376
  JSON.stringify(transcriptData, null, 2),
1345
1377
  "utf-8"
@@ -1347,14 +1379,14 @@ async function runTranscriptionWorkflow(params) {
1347
1379
  logger.info(`[asr] \u8F6C\u5199 JSON \u5DF2\u5199\u5165: ${transcriptDataPath}`);
1348
1380
  const safeSummary = title.replace(/[/\\:*?"<>|]/g, "").trim().slice(0, 20);
1349
1381
  const filename = safeSummary ? `${recordingId}_${safeSummary}.md` : `${recordingId}.md`;
1350
- const filePath = (0, import_node_path22.join)(transcriptsDir, filename);
1351
- (0, import_node_fs26.writeFileSync)(filePath, markdown, "utf-8");
1382
+ const filePath = (0, import_node_path23.join)(transcriptsDir, filename);
1383
+ (0, import_node_fs27.writeFileSync)(filePath, markdown, "utf-8");
1352
1384
  logger.info(`[asr] \u8F6C\u5199\u6587\u672C\u5DF2\u5199\u5165: ${filePath}`);
1353
1385
  let summaryFilename;
1354
1386
  if (summary) {
1355
1387
  summaryFilename = `${recordingId}.md`;
1356
- const summaryFilePath = (0, import_node_path22.join)(summariesDir, summaryFilename);
1357
- (0, import_node_fs26.writeFileSync)(summaryFilePath, summary, "utf-8");
1388
+ const summaryFilePath = (0, import_node_path23.join)(summariesDir, summaryFilename);
1389
+ (0, import_node_fs27.writeFileSync)(summaryFilePath, summary, "utf-8");
1358
1390
  logger.info(`[asr] \u6458\u8981\u6587\u672C\u5DF2\u5199\u5165: ${summaryFilePath}`);
1359
1391
  }
1360
1392
  return {
@@ -1439,7 +1471,7 @@ async function transcribeWithModelProxy(audioOssUrl, apiConfig, logger) {
1439
1471
  }
1440
1472
  async function transcribeWithWhisperLocal2(audioFilePath, config, logger) {
1441
1473
  const { transcribeWithWhisperLocal: runLocal } = await Promise.resolve().then(() => (init_whisper_local(), whisper_local_exports));
1442
- const dataDir = process.env.OPENCLAW_STATE_DIR ?? process.env.QCLAW_STATE_DIR ?? (0, import_node_path22.join)(audioFilePath, "..", "..", "..");
1474
+ const dataDir = process.env.OPENCLAW_STATE_DIR ?? process.env.QCLAW_STATE_DIR ?? (0, import_node_path23.join)(audioFilePath, "..", "..", "..");
1443
1475
  const localConfig = config.local ?? {};
1444
1476
  const result = await runLocal(
1445
1477
  audioFilePath,
@@ -1742,12 +1774,12 @@ function formatTranscriptSegmentText(segment) {
1742
1774
  }
1743
1775
  return text;
1744
1776
  }
1745
- var import_node_fs26, import_node_path22, DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS, DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS, LONG_RECORDING_RUNNING_STATUSES, LONG_RECORDING_TERMINAL_FAILURE_STATUSES;
1777
+ var import_node_fs27, import_node_path23, DEFAULT_LONG_RECORDING_POLL_INTERVAL_MS, DEFAULT_LONG_RECORDING_MAX_POLL_ATTEMPTS, LONG_RECORDING_RUNNING_STATUSES, LONG_RECORDING_TERMINAL_FAILURE_STATUSES;
1746
1778
  var init_asr = __esm({
1747
1779
  "src/recording/asr.ts"() {
1748
1780
  "use strict";
1749
- import_node_fs26 = require("fs");
1750
- import_node_path22 = require("path");
1781
+ import_node_fs27 = require("fs");
1782
+ import_node_path23 = require("path");
1751
1783
  init_credentials();
1752
1784
  init_env();
1753
1785
  init_transcript_document();
@@ -2143,9 +2175,9 @@ var require_permessage_deflate = __commonJS({
2143
2175
  */
2144
2176
  decompress(data, fin, callback) {
2145
2177
  zlibLimiter.add((done) => {
2146
- this._decompress(data, fin, (err, result) => {
2178
+ this._decompress(data, fin, (err2, result) => {
2147
2179
  done();
2148
- callback(err, result);
2180
+ callback(err2, result);
2149
2181
  });
2150
2182
  });
2151
2183
  }
@@ -2159,9 +2191,9 @@ var require_permessage_deflate = __commonJS({
2159
2191
  */
2160
2192
  compress(data, fin, callback) {
2161
2193
  zlibLimiter.add((done) => {
2162
- this._compress(data, fin, (err, result) => {
2194
+ this._compress(data, fin, (err2, result) => {
2163
2195
  done();
2164
- callback(err, result);
2196
+ callback(err2, result);
2165
2197
  });
2166
2198
  });
2167
2199
  }
@@ -2192,11 +2224,11 @@ var require_permessage_deflate = __commonJS({
2192
2224
  this._inflate.write(data);
2193
2225
  if (fin) this._inflate.write(TRAILER);
2194
2226
  this._inflate.flush(() => {
2195
- const err = this._inflate[kError];
2196
- if (err) {
2227
+ const err2 = this._inflate[kError];
2228
+ if (err2) {
2197
2229
  this._inflate.close();
2198
2230
  this._inflate = null;
2199
- callback(err);
2231
+ callback(err2);
2200
2232
  return;
2201
2233
  }
2202
2234
  const data2 = bufferUtil.concat(
@@ -2277,14 +2309,14 @@ var require_permessage_deflate = __commonJS({
2277
2309
  this.removeListener("data", inflateOnData);
2278
2310
  this.reset();
2279
2311
  }
2280
- function inflateOnError(err) {
2312
+ function inflateOnError(err2) {
2281
2313
  this[kPerMessageDeflate]._inflate = null;
2282
2314
  if (this[kError]) {
2283
2315
  this[kCallback](this[kError]);
2284
2316
  return;
2285
2317
  }
2286
- err[kStatusCode] = 1007;
2287
- this[kCallback](err);
2318
+ err2[kStatusCode] = 1007;
2319
+ this[kCallback](err2);
2288
2320
  }
2289
2321
  }
2290
2322
  });
@@ -2907,8 +2939,8 @@ var require_receiver = __commonJS({
2907
2939
  */
2908
2940
  decompress(data, cb) {
2909
2941
  const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
2910
- perMessageDeflate.decompress(data, this._fin, (err, buf) => {
2911
- if (err) return cb(err);
2942
+ perMessageDeflate.decompress(data, this._fin, (err2, buf) => {
2943
+ if (err2) return cb(err2);
2912
2944
  if (buf.length) {
2913
2945
  this._messageLength += buf.length;
2914
2946
  if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
@@ -3069,13 +3101,13 @@ var require_receiver = __commonJS({
3069
3101
  createError(ErrorCtor, message, prefix, statusCode, errorCode) {
3070
3102
  this._loop = false;
3071
3103
  this._errored = true;
3072
- const err = new ErrorCtor(
3104
+ const err2 = new ErrorCtor(
3073
3105
  prefix ? `Invalid WebSocket frame: ${message}` : message
3074
3106
  );
3075
- Error.captureStackTrace(err, this.createError);
3076
- err.code = errorCode;
3077
- err[kStatusCode] = statusCode;
3078
- return err;
3107
+ Error.captureStackTrace(err2, this.createError);
3108
+ err2.code = errorCode;
3109
+ err2[kStatusCode] = statusCode;
3110
+ return err2;
3079
3111
  }
3080
3112
  };
3081
3113
  module2.exports = Receiver2;
@@ -3449,10 +3481,10 @@ var require_sender = __commonJS({
3449
3481
  this._state = GET_BLOB_DATA;
3450
3482
  blob.arrayBuffer().then((arrayBuffer) => {
3451
3483
  if (this._socket.destroyed) {
3452
- const err = new Error(
3484
+ const err2 = new Error(
3453
3485
  "The socket was closed while the blob was being read"
3454
3486
  );
3455
- process.nextTick(callCallbacks, this, err, cb);
3487
+ process.nextTick(callCallbacks, this, err2, cb);
3456
3488
  return;
3457
3489
  }
3458
3490
  this._bufferedBytes -= options[kByteLength];
@@ -3464,8 +3496,8 @@ var require_sender = __commonJS({
3464
3496
  } else {
3465
3497
  this.dispatch(data, compress, options, cb);
3466
3498
  }
3467
- }).catch((err) => {
3468
- process.nextTick(onError, this, err, cb);
3499
+ }).catch((err2) => {
3500
+ process.nextTick(onError, this, err2, cb);
3469
3501
  });
3470
3502
  }
3471
3503
  /**
@@ -3501,10 +3533,10 @@ var require_sender = __commonJS({
3501
3533
  this._state = DEFLATING;
3502
3534
  perMessageDeflate.compress(data, options.fin, (_, buf) => {
3503
3535
  if (this._socket.destroyed) {
3504
- const err = new Error(
3536
+ const err2 = new Error(
3505
3537
  "The socket was closed while data was being compressed"
3506
3538
  );
3507
- callCallbacks(this, err, cb);
3539
+ callCallbacks(this, err2, cb);
3508
3540
  return;
3509
3541
  }
3510
3542
  this._bufferedBytes -= options[kByteLength];
@@ -3555,17 +3587,17 @@ var require_sender = __commonJS({
3555
3587
  }
3556
3588
  };
3557
3589
  module2.exports = Sender2;
3558
- function callCallbacks(sender, err, cb) {
3559
- if (typeof cb === "function") cb(err);
3590
+ function callCallbacks(sender, err2, cb) {
3591
+ if (typeof cb === "function") cb(err2);
3560
3592
  for (let i = 0; i < sender._queue.length; i++) {
3561
3593
  const params = sender._queue[i];
3562
3594
  const callback = params[params.length - 1];
3563
- if (typeof callback === "function") callback(err);
3595
+ if (typeof callback === "function") callback(err2);
3564
3596
  }
3565
3597
  }
3566
- function onError(sender, err, cb) {
3567
- callCallbacks(sender, err, cb);
3568
- sender.onerror(err);
3598
+ function onError(sender, err2, cb) {
3599
+ callCallbacks(sender, err2, cb);
3600
+ sender.onerror(err2);
3569
3601
  }
3570
3602
  }
3571
3603
  });
@@ -4213,8 +4245,8 @@ var require_websocket = __commonJS({
4213
4245
  return;
4214
4246
  }
4215
4247
  this._readyState = _WebSocket.CLOSING;
4216
- this._sender.close(code, data, !this._isServer, (err) => {
4217
- if (err) return;
4248
+ this._sender.close(code, data, !this._isServer, (err2) => {
4249
+ if (err2) return;
4218
4250
  this._closeFrameSent = true;
4219
4251
  if (this._closeFrameReceived || this._receiver._writableState.errorEmitted) {
4220
4252
  this._socket.end();
@@ -4482,11 +4514,11 @@ var require_websocket = __commonJS({
4482
4514
  invalidUrlMessage = "The URL contains a fragment identifier";
4483
4515
  }
4484
4516
  if (invalidUrlMessage) {
4485
- const err = new SyntaxError(invalidUrlMessage);
4517
+ const err2 = new SyntaxError(invalidUrlMessage);
4486
4518
  if (websocket._redirects === 0) {
4487
- throw err;
4519
+ throw err2;
4488
4520
  } else {
4489
- emitErrorAndClose(websocket, err);
4521
+ emitErrorAndClose(websocket, err2);
4490
4522
  return;
4491
4523
  }
4492
4524
  }
@@ -4581,10 +4613,10 @@ var require_websocket = __commonJS({
4581
4613
  abortHandshake(websocket, req, "Opening handshake has timed out");
4582
4614
  });
4583
4615
  }
4584
- req.on("error", (err) => {
4616
+ req.on("error", (err2) => {
4585
4617
  if (req === null || req[kAborted]) return;
4586
4618
  req = websocket._req = null;
4587
- emitErrorAndClose(websocket, err);
4619
+ emitErrorAndClose(websocket, err2);
4588
4620
  });
4589
4621
  req.on("response", (res) => {
4590
4622
  const location = res.headers.location;
@@ -4599,8 +4631,8 @@ var require_websocket = __commonJS({
4599
4631
  try {
4600
4632
  addr = new URL2(location, address);
4601
4633
  } catch (e) {
4602
- const err = new SyntaxError(`Invalid URL: ${location}`);
4603
- emitErrorAndClose(websocket, err);
4634
+ const err2 = new SyntaxError(`Invalid URL: ${location}`);
4635
+ emitErrorAndClose(websocket, err2);
4604
4636
  return;
4605
4637
  }
4606
4638
  initAsClient(websocket, addr, protocols, options);
@@ -4652,7 +4684,7 @@ var require_websocket = __commonJS({
4652
4684
  let extensions;
4653
4685
  try {
4654
4686
  extensions = parse(secWebSocketExtensions);
4655
- } catch (err) {
4687
+ } catch (err2) {
4656
4688
  const message = "Invalid Sec-WebSocket-Extensions header";
4657
4689
  abortHandshake(websocket, socket, message);
4658
4690
  return;
@@ -4665,7 +4697,7 @@ var require_websocket = __commonJS({
4665
4697
  }
4666
4698
  try {
4667
4699
  perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
4668
- } catch (err) {
4700
+ } catch (err2) {
4669
4701
  const message = "Invalid Sec-WebSocket-Extensions header";
4670
4702
  abortHandshake(websocket, socket, message);
4671
4703
  return;
@@ -4685,10 +4717,10 @@ var require_websocket = __commonJS({
4685
4717
  req.end();
4686
4718
  }
4687
4719
  }
4688
- function emitErrorAndClose(websocket, err) {
4720
+ function emitErrorAndClose(websocket, err2) {
4689
4721
  websocket._readyState = WebSocket2.CLOSING;
4690
4722
  websocket._errorEmitted = true;
4691
- websocket.emit("error", err);
4723
+ websocket.emit("error", err2);
4692
4724
  websocket.emitClose();
4693
4725
  }
4694
4726
  function netConnect(options) {
@@ -4704,17 +4736,17 @@ var require_websocket = __commonJS({
4704
4736
  }
4705
4737
  function abortHandshake(websocket, stream, message) {
4706
4738
  websocket._readyState = WebSocket2.CLOSING;
4707
- const err = new Error(message);
4708
- Error.captureStackTrace(err, abortHandshake);
4739
+ const err2 = new Error(message);
4740
+ Error.captureStackTrace(err2, abortHandshake);
4709
4741
  if (stream.setHeader) {
4710
4742
  stream[kAborted] = true;
4711
4743
  stream.abort();
4712
4744
  if (stream.socket && !stream.socket.destroyed) {
4713
4745
  stream.socket.destroy();
4714
4746
  }
4715
- process.nextTick(emitErrorAndClose, websocket, err);
4747
+ process.nextTick(emitErrorAndClose, websocket, err2);
4716
4748
  } else {
4717
- stream.destroy(err);
4749
+ stream.destroy(err2);
4718
4750
  stream.once("error", websocket.emit.bind(websocket, "error"));
4719
4751
  stream.once("close", websocket.emitClose.bind(websocket));
4720
4752
  }
@@ -4726,10 +4758,10 @@ var require_websocket = __commonJS({
4726
4758
  else websocket._bufferedAmount += length;
4727
4759
  }
4728
4760
  if (cb) {
4729
- const err = new Error(
4761
+ const err2 = new Error(
4730
4762
  `WebSocket is not open: readyState ${websocket.readyState} (${readyStates[websocket.readyState]})`
4731
4763
  );
4732
- process.nextTick(cb, err);
4764
+ process.nextTick(cb, err2);
4733
4765
  }
4734
4766
  }
4735
4767
  function receiverOnConclude(code, reason) {
@@ -4747,16 +4779,16 @@ var require_websocket = __commonJS({
4747
4779
  const websocket = this[kWebSocket];
4748
4780
  if (!websocket.isPaused) websocket._socket.resume();
4749
4781
  }
4750
- function receiverOnError(err) {
4782
+ function receiverOnError(err2) {
4751
4783
  const websocket = this[kWebSocket];
4752
4784
  if (websocket._socket[kWebSocket] !== void 0) {
4753
4785
  websocket._socket.removeListener("data", socketOnData);
4754
4786
  process.nextTick(resume, websocket._socket);
4755
- websocket.close(err[kStatusCode]);
4787
+ websocket.close(err2[kStatusCode]);
4756
4788
  }
4757
4789
  if (!websocket._errorEmitted) {
4758
4790
  websocket._errorEmitted = true;
4759
- websocket.emit("error", err);
4791
+ websocket.emit("error", err2);
4760
4792
  }
4761
4793
  }
4762
4794
  function receiverOnFinish() {
@@ -4776,7 +4808,7 @@ var require_websocket = __commonJS({
4776
4808
  function resume(stream) {
4777
4809
  stream.resume();
4778
4810
  }
4779
- function senderOnError(err) {
4811
+ function senderOnError(err2) {
4780
4812
  const websocket = this[kWebSocket];
4781
4813
  if (websocket.readyState === WebSocket2.CLOSED) return;
4782
4814
  if (websocket.readyState === WebSocket2.OPEN) {
@@ -4786,7 +4818,7 @@ var require_websocket = __commonJS({
4786
4818
  this._socket.end();
4787
4819
  if (!websocket._errorEmitted) {
4788
4820
  websocket._errorEmitted = true;
4789
- websocket.emit("error", err);
4821
+ websocket.emit("error", err2);
4790
4822
  }
4791
4823
  }
4792
4824
  function setCloseTimer(websocket) {
@@ -4852,11 +4884,11 @@ var require_stream = __commonJS({
4852
4884
  this.destroy();
4853
4885
  }
4854
4886
  }
4855
- function duplexOnError(err) {
4887
+ function duplexOnError(err2) {
4856
4888
  this.removeListener("error", duplexOnError);
4857
4889
  this.destroy();
4858
4890
  if (this.listenerCount("error") === 0) {
4859
- this.emit("error", err);
4891
+ this.emit("error", err2);
4860
4892
  }
4861
4893
  }
4862
4894
  function createWebSocketStream2(ws, options) {
@@ -4872,28 +4904,28 @@ var require_stream = __commonJS({
4872
4904
  const data = !isBinary && duplex._readableState.objectMode ? msg.toString() : msg;
4873
4905
  if (!duplex.push(data)) ws.pause();
4874
4906
  });
4875
- ws.once("error", function error(err) {
4907
+ ws.once("error", function error(err2) {
4876
4908
  if (duplex.destroyed) return;
4877
4909
  terminateOnDestroy = false;
4878
- duplex.destroy(err);
4910
+ duplex.destroy(err2);
4879
4911
  });
4880
4912
  ws.once("close", function close() {
4881
4913
  if (duplex.destroyed) return;
4882
4914
  duplex.push(null);
4883
4915
  });
4884
- duplex._destroy = function(err, callback) {
4916
+ duplex._destroy = function(err2, callback) {
4885
4917
  if (ws.readyState === ws.CLOSED) {
4886
- callback(err);
4918
+ callback(err2);
4887
4919
  process.nextTick(emitClose, duplex);
4888
4920
  return;
4889
4921
  }
4890
4922
  let called = false;
4891
- ws.once("error", function error(err2) {
4923
+ ws.once("error", function error(err3) {
4892
4924
  called = true;
4893
- callback(err2);
4925
+ callback(err3);
4894
4926
  });
4895
4927
  ws.once("close", function close() {
4896
- if (!called) callback(err);
4928
+ if (!called) callback(err2);
4897
4929
  process.nextTick(emitClose, duplex);
4898
4930
  });
4899
4931
  if (terminateOnDestroy) ws.terminate();
@@ -5215,7 +5247,7 @@ var require_websocket_server = __commonJS({
5215
5247
  if (secWebSocketProtocol !== void 0) {
5216
5248
  try {
5217
5249
  protocols = subprotocol.parse(secWebSocketProtocol);
5218
- } catch (err) {
5250
+ } catch (err2) {
5219
5251
  const message = "Invalid Sec-WebSocket-Protocol header";
5220
5252
  abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
5221
5253
  return;
@@ -5235,7 +5267,7 @@ var require_websocket_server = __commonJS({
5235
5267
  perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
5236
5268
  extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
5237
5269
  }
5238
- } catch (err) {
5270
+ } catch (err2) {
5239
5271
  const message = "Invalid or unacceptable Sec-WebSocket-Extensions header";
5240
5272
  abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
5241
5273
  return;
@@ -5364,9 +5396,9 @@ var require_websocket_server = __commonJS({
5364
5396
  }
5365
5397
  function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
5366
5398
  if (server.listenerCount("wsClientError")) {
5367
- const err = new Error(message);
5368
- Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
5369
- server.emit("wsClientError", err, socket, req);
5399
+ const err2 = new Error(message);
5400
+ Error.captureStackTrace(err2, abortHandshakeOrEmitwsClientError);
5401
+ server.emit("wsClientError", err2, socket, req);
5370
5402
  } else {
5371
5403
  abortHandshake(socket, code, message, headers);
5372
5404
  }
@@ -5392,7 +5424,7 @@ function readBuildInjectedVersion() {
5392
5424
  if (false) {
5393
5425
  return void 0;
5394
5426
  }
5395
- const version = "1.10.3".trim();
5427
+ const version = "1.10.5".trim();
5396
5428
  return version || void 0;
5397
5429
  }
5398
5430
  function readPluginVersionFromPackageJson() {
@@ -6649,11 +6681,11 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6649
6681
  id: rule.name
6650
6682
  }));
6651
6683
  respond(true, { ok: true, rules });
6652
- } catch (err) {
6653
- logger.warn(`lightrules.list failed: ${err?.message}`);
6684
+ } catch (err2) {
6685
+ logger.warn(`lightrules.list failed: ${err2?.message}`);
6654
6686
  respond(false, null, {
6655
6687
  code: "INTERNAL_ERROR",
6656
- message: err?.message ?? "Unknown error"
6688
+ message: err2?.message ?? "Unknown error"
6657
6689
  });
6658
6690
  }
6659
6691
  });
@@ -6683,8 +6715,8 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6683
6715
  try {
6684
6716
  repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
6685
6717
  assertAncsRepeatTimes(repeatTimes);
6686
- } catch (err) {
6687
- respond(false, null, { code: "VALIDATION_FAILED", message: err?.message ?? "Unknown error" });
6718
+ } catch (err2) {
6719
+ respond(false, null, { code: "VALIDATION_FAILED", message: err2?.message ?? "Unknown error" });
6688
6720
  return;
6689
6721
  }
6690
6722
  try {
@@ -6698,12 +6730,12 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6698
6730
  });
6699
6731
  logger.info(`Light rule created: ${name}`);
6700
6732
  respond(true, { ok: true, name, cronHint: result.cronHint });
6701
- } catch (err) {
6702
- if (err instanceof LightRuleError) {
6703
- respond(false, null, { code: err.code, message: err.message });
6733
+ } catch (err2) {
6734
+ if (err2 instanceof LightRuleError) {
6735
+ respond(false, null, { code: err2.code, message: err2.message });
6704
6736
  } else {
6705
- logger.warn(`lightrules.create failed: ${err?.message}`);
6706
- respond(false, null, { code: "INTERNAL_ERROR", message: err?.message ?? "Unknown error" });
6737
+ logger.warn(`lightrules.create failed: ${err2?.message}`);
6738
+ respond(false, null, { code: "INTERNAL_ERROR", message: err2?.message ?? "Unknown error" });
6707
6739
  }
6708
6740
  }
6709
6741
  });
@@ -6742,8 +6774,8 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6742
6774
  try {
6743
6775
  repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
6744
6776
  assertAncsRepeatTimes(repeatTimes);
6745
- } catch (err) {
6746
- respond(false, null, { code: "VALIDATION_FAILED", message: err?.message ?? "Unknown error" });
6777
+ } catch (err2) {
6778
+ respond(false, null, { code: "VALIDATION_FAILED", message: err2?.message ?? "Unknown error" });
6747
6779
  return;
6748
6780
  }
6749
6781
  }
@@ -6766,12 +6798,12 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6766
6798
  rule: result.meta,
6767
6799
  cronHint: result.cronHint
6768
6800
  });
6769
- } catch (err) {
6770
- if (err instanceof LightRuleError) {
6771
- respond(false, null, { code: err.code, message: err.message });
6801
+ } catch (err2) {
6802
+ if (err2 instanceof LightRuleError) {
6803
+ respond(false, null, { code: err2.code, message: err2.message });
6772
6804
  } else {
6773
- logger.warn(`lightrules.update failed: ${err?.message}`);
6774
- respond(false, null, { code: "INTERNAL_ERROR", message: err?.message ?? "Unknown error" });
6805
+ logger.warn(`lightrules.update failed: ${err2?.message}`);
6806
+ respond(false, null, { code: "INTERNAL_ERROR", message: err2?.message ?? "Unknown error" });
6775
6807
  }
6776
6808
  }
6777
6809
  });
@@ -6794,273 +6826,17 @@ function registerLightRulesGateway(api, registry, logger, rememberBroadcast) {
6794
6826
  deleted: true,
6795
6827
  cronHint: result.cronHint
6796
6828
  });
6797
- } catch (err) {
6798
- if (err instanceof LightRuleError) {
6799
- respond(false, null, { code: err.code, message: err.message });
6829
+ } catch (err2) {
6830
+ if (err2 instanceof LightRuleError) {
6831
+ respond(false, null, { code: err2.code, message: err2.message });
6800
6832
  } else {
6801
- logger.warn(`lightrules.delete failed: ${err?.message}`);
6802
- respond(false, null, { code: "INTERNAL_ERROR", message: err?.message ?? "Unknown error" });
6833
+ logger.warn(`lightrules.delete failed: ${err2?.message}`);
6834
+ respond(false, null, { code: "INTERNAL_ERROR", message: err2?.message ?? "Unknown error" });
6803
6835
  }
6804
6836
  }
6805
6837
  });
6806
6838
  }
6807
6839
 
6808
- // src/light-rules/evaluator-job.ts
6809
- var EVALUATOR_JOB_ID = "light-rules-evaluator";
6810
- var EVALUATOR_SUBAGENT_SESSION_KEY = EVALUATOR_JOB_ID;
6811
- var FALLBACK_CRON_EXPR = "0 0 1 1 *";
6812
- function buildEvaluatorJobMessage(notificationsDir) {
6813
- return `\u706F\u6548\u89C4\u5219\u8BC4\u4F30\u4EFB\u52A1\u3002
6814
-
6815
- \u6267\u884C\u6B65\u9AA4\uFF1A
6816
- 1. \u8BFB\u53D6 tasks/light-rules-evaluator/checkpoint.json\uFF08\u8BB0\u5F55\u4E0A\u6B21\u5904\u7406\u8FDB\u5EA6\uFF09
6817
- 2. \u626B\u63CF ${notificationsDir} \u76EE\u5F55\uFF0C\u83B7\u53D6 checkpoint \u4E4B\u540E\u7684\u65B0\u901A\u77E5
6818
- 3. \u626B\u63CF tasks/ \u76EE\u5F55\uFF0C\u8BFB\u53D6\u6240\u6709 type=light-rule \u4E14 enabled=true \u7684 meta.json
6819
- 4. \u5BF9\u6BCF\u6761\u65B0\u901A\u77E5\uFF0C\u9010\u4E00\u5224\u65AD\u662F\u5426\u547D\u4E2D\u6BCF\u6761\u89C4\u5219\u7684 description\uFF08\u8BED\u4E49\u5339\u914D\uFF09
6820
- 5. \u547D\u4E2D\u65F6\uFF1A\u4EE5\u8BE5\u89C4\u5219\u7684 segments \u548C repeat_times \u8C03\u7528 light_control \u5DE5\u5177
6821
- 6. \u66F4\u65B0 checkpoint.json\uFF0C\u8BB0\u5F55\u5DF2\u5904\u7406\u5230\u7684\u6700\u65B0\u901A\u77E5\u4F4D\u7F6E
6822
- 7. \u82E5\u65E0\u65B0\u901A\u77E5\u6216\u65E0 enabled \u89C4\u5219\uFF1A\u8F93\u51FA NO_CHANGE\uFF0C\u76F4\u63A5\u7ED3\u675F`;
6823
- }
6824
- var LightRulesEvaluatorJob = class {
6825
- logger;
6826
- registry;
6827
- subagentRunner;
6828
- getNotificationsDir;
6829
- /**
6830
- * 记录本进程生命周期内 job 是否已确认存在。
6831
- * 仅在 `ensureJobExists` 成功后置 true,避免每次 push 都做检查。
6832
- */
6833
- jobEnsured = false;
6834
- /**
6835
- * 首次创建 job 时的并发保护。
6836
- * 避免冷启动瞬间多条通知并发到达时重复调用 `cron.add`。
6837
- */
6838
- ensureJobPromise = null;
6839
- /**
6840
- * subagent fallback 路径的并发保护。
6841
- * 若评估 session 已在运行中,跳过本次触发(checkpoint 保证下次补处理)。
6842
- */
6843
- subagentInFlight = false;
6844
- constructor(deps) {
6845
- this.logger = deps.logger;
6846
- this.registry = deps.registry;
6847
- this.subagentRunner = deps.subagentRunner;
6848
- this.getNotificationsDir = deps.getNotificationsDir ?? (() => void 0);
6849
- }
6850
- /**
6851
- * 通知落盘后调用。若有新增通知且存在 enabled 规则,则触发评估。
6852
- *
6853
- * 两条路径:
6854
- * - cron 不为 null:enqueueRun("force") 入队(gateway context 路径,正常路径)
6855
- * - cron 为 null:通过 subagentRunner 直接运行(HTTP Relay 路径,fallback)
6856
- *
6857
- * @param cron 来自 gateway context 的 CronService;HTTP 路径下为 null
6858
- * @param insertedCount 本次 ingest 新落盘的通知条数(StoredNotification 去重后)
6859
- */
6860
- async triggerIfNeeded(cron, insertedCount) {
6861
- if (insertedCount === 0) return;
6862
- if (this.registry.getEnabled().length === 0) return;
6863
- if (!cron) {
6864
- await this.triggerViaSubagent();
6865
- return;
6866
- }
6867
- try {
6868
- await this.ensureJobExists(cron);
6869
- } catch (err) {
6870
- this.logger.warn(`light-rules-evaluator: job ensure failed: ${err?.message ?? err}`);
6871
- return;
6872
- }
6873
- try {
6874
- const result = await cron.enqueueRun(EVALUATOR_JOB_ID, "force");
6875
- if (!result.ok) {
6876
- this.logger.warn("light-rules-evaluator: enqueueRun returned ok=false");
6877
- return;
6878
- }
6879
- if ("enqueued" in result && result.enqueued) {
6880
- this.logger.info(`light-rules-evaluator: enqueued runId=${result.runId}`);
6881
- } else if ("reason" in result) {
6882
- this.logger.info(`light-rules-evaluator: enqueueRun skipped (${result.reason})`);
6883
- }
6884
- } catch (err) {
6885
- this.logger.warn(`light-rules-evaluator: enqueueRun failed: ${err?.message ?? err}`);
6886
- }
6887
- }
6888
- /**
6889
- * cron service 不可用时的 fallback:直接通过 subagentRunner 运行评估 session。
6890
- *
6891
- * 并发保护:若上一次 subagent 运行尚未完成,本次跳过。
6892
- * checkpoint 保证即使本次跳过,下次触发时会处理所有积压通知。
6893
- */
6894
- async triggerViaSubagent() {
6895
- if (!this.subagentRunner) {
6896
- this.logger.warn(
6897
- "light-rules-evaluator: cron service unavailable and no subagent fallback configured; notifications ingested via HTTP will not trigger light rules until an agent session is active"
6898
- );
6899
- return;
6900
- }
6901
- if (this.subagentInFlight) {
6902
- this.logger.info("light-rules-evaluator: subagent run in-flight, skipping this trigger");
6903
- return;
6904
- }
6905
- const notificationsDir = this.getNotificationsDir();
6906
- if (!notificationsDir) {
6907
- this.logger.warn("light-rules-evaluator: notifications dir not ready, skipping subagent trigger");
6908
- return;
6909
- }
6910
- this.subagentInFlight = true;
6911
- try {
6912
- const result = await this.subagentRunner.run({
6913
- sessionKey: EVALUATOR_SUBAGENT_SESSION_KEY,
6914
- message: buildEvaluatorJobMessage(notificationsDir),
6915
- deliver: false,
6916
- idempotencyKey: `${EVALUATOR_SUBAGENT_SESSION_KEY}-${Date.now()}`
6917
- });
6918
- this.logger.info(`light-rules-evaluator: subagent triggered runId=${result.runId}`);
6919
- } catch (err) {
6920
- this.logger.warn(`light-rules-evaluator: subagent trigger failed: ${err?.message ?? err}`);
6921
- } finally {
6922
- this.subagentInFlight = false;
6923
- }
6924
- }
6925
- /**
6926
- * 按需创建 `light-rules-evaluator` job。
6927
- * 若 job 已存在(内存缓存或 cron store),直接返回;否则调用 `cron.add`。
6928
- */
6929
- async ensureJobExists(cron) {
6930
- if (this.jobEnsured) return;
6931
- if (!this.ensureJobPromise) {
6932
- this.ensureJobPromise = this.createJobIfNeeded(cron).finally(() => {
6933
- this.ensureJobPromise = null;
6934
- });
6935
- }
6936
- await this.ensureJobPromise;
6937
- }
6938
- async createJobIfNeeded(cron) {
6939
- if (cron.getJob(EVALUATOR_JOB_ID)) {
6940
- this.jobEnsured = true;
6941
- return;
6942
- }
6943
- try {
6944
- await cron.add({
6945
- id: EVALUATOR_JOB_ID,
6946
- name: "\u706F\u6548\u89C4\u5219\u8BC4\u4F30",
6947
- description: "\u4E8B\u4EF6\u9A71\u52A8\uFF1A\u901A\u77E5\u5230\u8FBE\u65F6\u8BC4\u4F30\u6240\u6709 enabled \u706F\u6548\u89C4\u5219\uFF0C\u547D\u4E2D\u5219\u8C03\u7528 light_control \u89E6\u53D1\u706F\u6548",
6948
- enabled: true,
6949
- schedule: { kind: "cron", expr: FALLBACK_CRON_EXPR },
6950
- sessionTarget: "isolated",
6951
- wakeMode: "now",
6952
- payload: {
6953
- kind: "agentTurn",
6954
- message: buildEvaluatorJobMessage(this.getNotificationsDir() ?? "notifications")
6955
- }
6956
- });
6957
- this.logger.info("light-rules-evaluator: job created");
6958
- } catch (err) {
6959
- if (!cron.getJob(EVALUATOR_JOB_ID)) {
6960
- throw err;
6961
- }
6962
- }
6963
- this.jobEnsured = true;
6964
- }
6965
- };
6966
-
6967
- // src/light-rules/migration.ts
6968
- var import_node_fs6 = require("fs");
6969
- var import_node_path5 = require("path");
6970
- var NO_MATCH_FETCH_PY = `#!/usr/bin/env python3
6971
- # \u6B64\u6587\u4EF6\u7531\u8FC1\u79FB\u5DE5\u5177\u751F\u6210\u3002
6972
- # \u706F\u6548\u89C4\u5219\u5DF2\u8FC1\u79FB\u81F3\u4E8B\u4EF6\u9A71\u52A8\u67B6\u6784\uFF0C\u6B64 cron job \u4E0D\u518D\u6267\u884C\u5B9E\u9645\u5DE5\u4F5C\u3002
6973
- print("NO_MATCH")
6974
- `;
6975
- function normalizeScriptText(text) {
6976
- return text.replace(/\r\n/g, "\n").trim();
6977
- }
6978
- function resolveTasksDir(ctx) {
6979
- if (ctx.workspaceDir) return (0, import_node_path5.join)(ctx.workspaceDir, "tasks");
6980
- if (ctx.stateDir) {
6981
- const inferredWorkspaceDir = (0, import_node_path5.join)(ctx.stateDir, "workspace");
6982
- if ((0, import_node_fs6.existsSync)(inferredWorkspaceDir)) return (0, import_node_path5.join)(inferredWorkspaceDir, "tasks");
6983
- return (0, import_node_path5.join)(ctx.stateDir, "tasks");
6984
- }
6985
- return null;
6986
- }
6987
- function migrateLegacyLightRuleTasks(ctx, logger) {
6988
- const tasksDir3 = resolveTasksDir(ctx);
6989
- if (!tasksDir3 || !(0, import_node_fs6.existsSync)(tasksDir3)) return;
6990
- try {
6991
- for (const entry of (0, import_node_fs6.readdirSync)(tasksDir3, { withFileTypes: true })) {
6992
- if (!entry.isDirectory()) continue;
6993
- migrateTaskDir((0, import_node_path5.join)(tasksDir3, String(entry.name)), logger);
6994
- }
6995
- } catch (err) {
6996
- logger.warn(`migration: failed to read tasks dir: ${err?.message}`);
6997
- }
6998
- }
6999
- function migrateTaskDir(taskDir, logger) {
7000
- const metaPath = (0, import_node_path5.join)(taskDir, "meta.json");
7001
- if (!(0, import_node_fs6.existsSync)(metaPath)) return;
7002
- let meta;
7003
- try {
7004
- meta = JSON.parse((0, import_node_fs6.readFileSync)(metaPath, "utf-8"));
7005
- } catch {
7006
- return;
7007
- }
7008
- if (meta.type !== "light-rule") return;
7009
- const name = typeof meta.name === "string" ? meta.name : taskDir;
7010
- mergeMatchRulesIntoDescription(meta, name, metaPath, logger);
7011
- replaceFetchPy(taskDir, name, logger);
7012
- for (const filename of ["README.md", "checkpoint.json"]) {
7013
- removeFile((0, import_node_path5.join)(taskDir, filename), name, filename, logger);
7014
- }
7015
- }
7016
- function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
7017
- const matchRules = meta.matchRules;
7018
- if (!matchRules || typeof matchRules !== "object") return;
7019
- const rules = matchRules;
7020
- const parts = [];
7021
- if (rules.appName) parts.push(`app=${rules.appName}`);
7022
- if (rules.senderKeywords?.length) {
7023
- parts.push(`\u53D1\u4EF6\u4EBA\u5173\u952E\u8BCD=${rules.senderKeywords.join("\u3001")}`);
7024
- }
7025
- if (rules.contentKeywords?.length) {
7026
- parts.push(`\u5185\u5BB9\u5173\u952E\u8BCD=${rules.contentKeywords.join("\u3001")}`);
7027
- }
7028
- if (parts.length > 0) {
7029
- const existing = typeof meta.description === "string" ? meta.description.trim() : "";
7030
- meta.description = existing ? `${existing}\u3002\u5339\u914D\u89C4\u5219\uFF1A${parts.join("\uFF0C")}` : `\u5339\u914D\u89C4\u5219\uFF1A${parts.join("\uFF0C")}`;
7031
- }
7032
- delete meta.matchRules;
7033
- try {
7034
- (0, import_node_fs6.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7035
- logger.info(`migration: merged matchRules into description for light rule: ${name}`);
7036
- } catch (err) {
7037
- logger.warn(`migration: failed to update meta.json for ${name}: ${err?.message}`);
7038
- }
7039
- }
7040
- function replaceFetchPy(taskDir, name, logger) {
7041
- const fetchPyPath = (0, import_node_path5.join)(taskDir, "fetch.py");
7042
- if (!(0, import_node_fs6.existsSync)(fetchPyPath)) return;
7043
- try {
7044
- const existing = (0, import_node_fs6.readFileSync)(fetchPyPath, "utf-8");
7045
- if (normalizeScriptText(existing) === normalizeScriptText(NO_MATCH_FETCH_PY)) {
7046
- return;
7047
- }
7048
- (0, import_node_fs6.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7049
- logger.info(`migration: replaced fetch.py with NO_MATCH placeholder for ${name}`);
7050
- } catch (err) {
7051
- logger.warn(`migration: failed to replace fetch.py for ${name}: ${err?.message}`);
7052
- }
7053
- }
7054
- function removeFile(filePath, ruleName, filename, logger) {
7055
- if (!(0, import_node_fs6.existsSync)(filePath)) return;
7056
- try {
7057
- (0, import_node_fs6.rmSync)(filePath);
7058
- logger.info(`migration: removed ${filename} for light rule: ${ruleName}`);
7059
- } catch (err) {
7060
- logger.warn(`migration: failed to remove ${filename} for ${ruleName}: ${err?.message}`);
7061
- }
7062
- }
7063
-
7064
6840
  // src/light-rules/registry.ts
7065
6841
  var LightRuleRegistry = class {
7066
6842
  ctx;
@@ -7181,1067 +6957,1542 @@ var LightRuleRegistry = class {
7181
6957
  }
7182
6958
  };
7183
6959
 
7184
- // src/plugin/auto-update.ts
7185
- init_env();
7186
-
7187
- // src/update/channel.ts
7188
- function resolveUpdateChannel(params) {
7189
- if (params.configuredChannel) {
7190
- return params.configuredChannel;
7191
- }
7192
- if (params.currentVersion.includes("-")) {
7193
- return "beta";
6960
+ // src/light/protocol.ts
6961
+ var MAX_LIGHT_SEGMENTS = 12;
6962
+ var PROTOCOL_DIGITS = [
6963
+ "\x80",
6964
+ "\x81",
6965
+ "\x82",
6966
+ "\x83",
6967
+ "\x84",
6968
+ "\x91",
6969
+ "\x92",
6970
+ "\x93",
6971
+ "\x94",
6972
+ "\x95",
6973
+ "\x96",
6974
+ "\x97"
6975
+ ];
6976
+ var LED_SEPARATOR_ONCE = "\x9A";
6977
+ var LED_SEPARATOR_LOOP = "\x9B";
6978
+ var DURATION_STEPS_S = [0.5, 1, 2, 3, 5, 6, 8, 16, 24, 32, 48];
6979
+ var INTERVAL_STEPS_MS = [50, 100, 200, 300, 500, 600, 800, 1600, 2400, 3200, 4800];
6980
+ var BREATH_STEPS_MS = [1040, 1560, 2080, 2600, 3100, 4160];
6981
+ var BRIGHTNESS_STEPS = [32, 64, 96, 128, 192, 255];
6982
+ var COLOR_STEPS = [0, 32, 64, 128, 192, 255];
6983
+ var BACKGROUND_BRIGHTNESS_STEPS = [0, 32, 64, 96, 128, 192, 255];
6984
+ var MULTI_CHANNEL_COLOR_COEFFICIENTS = { r: 1, g: 0.25, b: 0.25 };
6985
+ var PURE_WHITE_COLOR_COEFFICIENTS = { r: 1, g: 0.35, b: 0.35 };
6986
+ var MODE_TO_INDEX = {
6987
+ wave: 0,
6988
+ breath: 1,
6989
+ strobe: 2,
6990
+ steady: 3,
6991
+ wave_rainbow: 4,
6992
+ pixel_frame: 5
6993
+ };
6994
+ function buildLightEffectApnsBody(segments, repeatInput) {
6995
+ assertSegmentCount(segments);
6996
+ assertSegmentsValid(segments);
6997
+ const repeatTimes = normalizeRepeatTimes(repeatInput);
6998
+ assertAncsRepeatTimes(repeatTimes);
6999
+ const visibleText = summarizeSegments(segments);
7000
+ const separator = repeatTimes === 0 ? LED_SEPARATOR_LOOP : LED_SEPARATOR_ONCE;
7001
+ const payload = segments.map((segment) => encodeSegment(segment)).join("");
7002
+ return `${visibleText}${separator}${payload}`;
7003
+ }
7004
+ function assertSegmentCount(segments) {
7005
+ if (segments.length < 1 || segments.length > MAX_LIGHT_SEGMENTS) {
7006
+ throw new Error(`light_control supports 1-${MAX_LIGHT_SEGMENTS} segments`);
7194
7007
  }
7195
- return params.envName === "development" ? "beta" : "latest";
7196
7008
  }
7197
-
7198
- // src/update/index.ts
7199
- var import_node_path9 = require("path");
7200
-
7201
- // src/update/checker.ts
7202
- function parseSemver(v) {
7203
- const [main, pre = null] = v.split("-", 2);
7204
- const [major = 0, minor = 0, patch = 0] = main.split(".").map(Number);
7205
- return { major, minor, patch, pre: pre ?? null };
7009
+ function summarizeSegments(segments) {
7010
+ const modeDesc = segments.map((segment) => segment.mode).join("+");
7011
+ return `Effect: ${modeDesc} (${segments.length} segment${segments.length > 1 ? "s" : ""})`;
7206
7012
  }
7207
- function comparePreRelease(a, b) {
7208
- const aParts = a.split(".");
7209
- const bParts = b.split(".");
7210
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
7211
- const ap = aParts[i] ?? "";
7212
- const bp = bParts[i] ?? "";
7213
- const an = Number(ap);
7214
- const bn = Number(bp);
7215
- let diff;
7216
- if (!Number.isNaN(an) && !Number.isNaN(bn)) {
7217
- diff = an - bn;
7218
- } else if (ap < bp) {
7219
- diff = -1;
7220
- } else if (ap > bp) {
7221
- diff = 1;
7222
- } else {
7223
- diff = 0;
7224
- }
7225
- if (diff !== 0) return diff;
7013
+ function assertSegmentsValid(segments) {
7014
+ const validation = validateSegments(segments);
7015
+ if (!validation.valid) {
7016
+ throw new Error(
7017
+ validation.errors.map((error) => `${error.field}: ${error.message}`).join("; ")
7018
+ );
7226
7019
  }
7227
- return 0;
7228
7020
  }
7229
- function isNewerVersion(candidate, current) {
7230
- const a = parseSemver(candidate);
7231
- const b = parseSemver(current);
7232
- if (a.major !== b.major) return a.major > b.major;
7233
- if (a.minor !== b.minor) return a.minor > b.minor;
7234
- if (a.patch !== b.patch) return a.patch > b.patch;
7235
- if (a.pre === null) return b.pre !== null;
7236
- if (b.pre === null) return false;
7237
- return comparePreRelease(a.pre, b.pre) > 0;
7021
+ function encodeSegment(segment) {
7022
+ const common = [
7023
+ MODE_TO_INDEX[segment.mode],
7024
+ quantizeDuration(segment.duration_s)
7025
+ ];
7026
+ let values;
7027
+ switch (segment.mode) {
7028
+ case "wave":
7029
+ case "wave_rainbow":
7030
+ const color = normalizeProtocolColor(segment.color);
7031
+ const background = normalizeProtocolColor(segment.background);
7032
+ values = [
7033
+ ...common,
7034
+ quantize(segment.interval_ms ?? 200, INTERVAL_STEPS_MS),
7035
+ quantizeBrightnessValue(segment.brightness ?? 0),
7036
+ quantize(color.r, COLOR_STEPS),
7037
+ quantize(color.g, COLOR_STEPS),
7038
+ quantize(color.b, COLOR_STEPS),
7039
+ segment.direction === "rtl" ? 1 : 0,
7040
+ quantizeWindow(segment.window ?? 2),
7041
+ quantize(background.r, COLOR_STEPS),
7042
+ quantize(background.g, COLOR_STEPS),
7043
+ quantize(background.b, COLOR_STEPS),
7044
+ quantize(segment.background?.brightness ?? 0, BACKGROUND_BRIGHTNESS_STEPS)
7045
+ ];
7046
+ break;
7047
+ case "breath":
7048
+ const breathColor = normalizeProtocolColor(segment.color);
7049
+ values = [
7050
+ ...common,
7051
+ quantizeBreathRiseFall(segment.breath_timing?.rise_ms),
7052
+ quantizeBreathHoldOff(segment.breath_timing?.hold_ms),
7053
+ quantizeBreathRiseFall(segment.breath_timing?.fall_ms),
7054
+ quantizeBreathHoldOff(segment.breath_timing?.off_ms),
7055
+ quantizeBrightnessValue(segment.brightness ?? 0),
7056
+ quantize(breathColor.r, COLOR_STEPS),
7057
+ quantize(breathColor.g, COLOR_STEPS),
7058
+ quantize(breathColor.b, COLOR_STEPS)
7059
+ ];
7060
+ break;
7061
+ case "strobe":
7062
+ const strobeColor = normalizeProtocolColor(segment.color);
7063
+ values = [
7064
+ ...common,
7065
+ quantize(segment.interval_ms ?? 200, INTERVAL_STEPS_MS),
7066
+ quantizeBrightnessValue(segment.brightness ?? 0),
7067
+ quantize(strobeColor.r, COLOR_STEPS),
7068
+ quantize(strobeColor.g, COLOR_STEPS),
7069
+ quantize(strobeColor.b, COLOR_STEPS)
7070
+ ];
7071
+ break;
7072
+ case "steady":
7073
+ const steadyColor = normalizeProtocolColor(segment.color);
7074
+ values = [
7075
+ ...common,
7076
+ quantizeBrightnessValue(segment.brightness ?? 0),
7077
+ quantize(steadyColor.r, COLOR_STEPS),
7078
+ quantize(steadyColor.g, COLOR_STEPS),
7079
+ quantize(steadyColor.b, COLOR_STEPS)
7080
+ ];
7081
+ break;
7082
+ case "pixel_frame":
7083
+ values = encodePixelFrameValues(common, segment);
7084
+ break;
7085
+ }
7086
+ return values.map((value) => PROTOCOL_DIGITS[value]).join("");
7238
7087
  }
7239
- var CDN_BASE_URL = "https://artifact.yoooclaw.com/plugin";
7240
- var NPM_BASE_URL = "https://registry.npmjs.org/@yoooclaw/phone-notifications";
7241
- var FETCH_TIMEOUT_MS = 5e3;
7242
- var UpdateChecker = class {
7243
- constructor(logger, onUpdateFound, intervalMs, channel) {
7244
- this.logger = logger;
7245
- this.onUpdateFound = onUpdateFound;
7246
- this.intervalMs = intervalMs;
7247
- this.channel = channel;
7088
+ function encodePixelFrameValues(common, segment) {
7089
+ const pixels = segment.pixels ?? [];
7090
+ return [
7091
+ ...common,
7092
+ pixels.length - 1,
7093
+ ...pixels.flatMap((pixel) => {
7094
+ const color = normalizeProtocolColor(pixel.color);
7095
+ return [
7096
+ pixel.index,
7097
+ quantize(color.r, COLOR_STEPS),
7098
+ quantize(color.g, COLOR_STEPS),
7099
+ quantize(color.b, COLOR_STEPS),
7100
+ quantizeBrightnessValue(pixel.brightness)
7101
+ ];
7102
+ })
7103
+ ];
7104
+ }
7105
+ function normalizeProtocolColor(color) {
7106
+ const normalized = {
7107
+ r: color?.r ?? 0,
7108
+ g: color?.g ?? 0,
7109
+ b: color?.b ?? 0
7110
+ };
7111
+ if (isPureWhiteProtocolColor(normalized)) {
7112
+ return applyProtocolColorCoefficients(normalized, PURE_WHITE_COLOR_COEFFICIENTS);
7248
7113
  }
7249
- timer = null;
7250
- notifiedVersion = null;
7251
- start() {
7252
- this.timer = setTimeout(() => {
7253
- void this.check();
7254
- this.timer = setInterval(() => void this.check(), this.intervalMs);
7255
- }, 6e4);
7114
+ if (countActiveProtocolColorChannels(normalized) <= 1) {
7115
+ return normalized;
7256
7116
  }
7257
- stop() {
7258
- if (this.timer) {
7259
- clearInterval(this.timer);
7260
- this.timer = null;
7117
+ return applyProtocolColorCoefficients(normalized, MULTI_CHANNEL_COLOR_COEFFICIENTS);
7118
+ }
7119
+ function countActiveProtocolColorChannels(color) {
7120
+ return Number(color.r > 0) + Number(color.g > 0) + Number(color.b > 0);
7121
+ }
7122
+ function isPureWhiteProtocolColor(color) {
7123
+ return color.r === 255 && color.g === 255 && color.b === 255;
7124
+ }
7125
+ function applyProtocolColorCoefficients(color, coefficients) {
7126
+ return {
7127
+ r: scaleProtocolColorChannel(color.r, coefficients.r),
7128
+ g: scaleProtocolColorChannel(color.g, coefficients.g),
7129
+ b: scaleProtocolColorChannel(color.b, coefficients.b)
7130
+ };
7131
+ }
7132
+ function scaleProtocolColorChannel(value, coefficient) {
7133
+ return Math.max(0, Math.min(255, Math.round(value * coefficient)));
7134
+ }
7135
+ function quantize(value, steps) {
7136
+ let bestIndex = 0;
7137
+ let bestDistance = Number.POSITIVE_INFINITY;
7138
+ for (const [index, step] of steps.entries()) {
7139
+ const distance = Math.abs(value - step);
7140
+ if (distance < bestDistance) {
7141
+ bestIndex = index;
7142
+ bestDistance = distance;
7261
7143
  }
7262
7144
  }
7263
- async check() {
7264
- const latest = await this.fetchLatestVersion();
7265
- if (!latest || latest === PLUGIN_VERSION) return;
7266
- if (latest === this.notifiedVersion) return;
7267
- if (PLUGIN_VERSION.includes("-") && this.channel !== "beta") return;
7268
- if (!isNewerVersion(latest, PLUGIN_VERSION)) return;
7269
- this.notifiedVersion = latest;
7270
- this.logger.info(`\u53D1\u73B0\u65B0\u7248\u672C: ${PLUGIN_VERSION} \u2192 ${latest}`);
7271
- this.onUpdateFound({ current: PLUGIN_VERSION, latest });
7272
- }
7273
- async fetchLatestVersion() {
7274
- const tag = this.channel === "beta" ? "beta" : "latest";
7275
- try {
7276
- const res = await fetch(`${CDN_BASE_URL}/${tag}`, {
7277
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
7278
- });
7279
- if (res.ok) {
7280
- const text = (await res.text()).trim();
7281
- if (text) return text;
7282
- }
7283
- } catch {
7284
- }
7285
- try {
7286
- const res = await fetch(`${NPM_BASE_URL}/${tag}`, {
7287
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
7288
- });
7289
- if (res.ok) {
7290
- const data = await res.json();
7291
- return data.version ?? null;
7292
- }
7293
- } catch {
7294
- this.logger.info("\u7248\u672C\u68C0\u67E5\u5931\u8D25\uFF08CDN + npm \u5747\u4E0D\u53EF\u8FBE\uFF09\uFF0C\u8DF3\u8FC7");
7295
- }
7296
- return null;
7145
+ return bestIndex;
7146
+ }
7147
+ function quantizeDuration(duration_s) {
7148
+ if (duration_s === 0) return 11;
7149
+ return quantize(duration_s, DURATION_STEPS_S);
7150
+ }
7151
+ function quantizeBrightnessValue(brightness) {
7152
+ if (brightness === 0) return 11;
7153
+ return quantize(brightness, BRIGHTNESS_STEPS);
7154
+ }
7155
+ function quantizeBreathRiseFall(value) {
7156
+ return quantize(value ?? 1040, BREATH_STEPS_MS);
7157
+ }
7158
+ function quantizeBreathHoldOff(value) {
7159
+ if (value === 0) {
7160
+ return 5;
7297
7161
  }
7298
- };
7162
+ return quantize(value ?? 1040, BREATH_STEPS_MS.slice(0, 5));
7163
+ }
7164
+ function quantizeWindow(value) {
7165
+ return value - 1;
7166
+ }
7299
7167
 
7300
- // src/update/executor.ts
7301
- var import_node_fs9 = require("fs");
7302
- var import_node_path8 = require("path");
7303
- var import_node_os = require("os");
7304
- var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
7305
- var BASE_URL = "https://artifact.yoooclaw.com/plugin";
7306
- async function executeUpdate(version, runCommand, logger, targetDir, updateConfigRecord2) {
7307
- if (!VERSION_PATTERN.test(version)) {
7308
- return { success: false, message: `\u975E\u6CD5\u7248\u672C\u53F7: ${version}` };
7309
- }
7310
- const tgzUrl = `${BASE_URL}/v${version}/yoooclaw-phone-notifications-${version}.tgz`;
7311
- logger.info(`\u6267\u884C\u66F4\u65B0: ${tgzUrl} \u2192 ${targetDir}`);
7312
- const workDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path8.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
7313
- const tgzPath = (0, import_node_path8.join)(workDir, "plugin.tgz");
7314
- const stagingDir = (0, import_node_path8.join)(workDir, "staged");
7315
- let backupDir = null;
7316
- try {
7317
- logger.info("\u4E0B\u8F7D\u63D2\u4EF6\u5305...");
7318
- const response = await fetch(tgzUrl, { signal: AbortSignal.timeout(6e4) });
7319
- if (!response.ok) {
7320
- return { success: false, message: `\u4E0B\u8F7D\u5931\u8D25 (HTTP ${response.status}): ${tgzUrl}` };
7321
- }
7322
- const buffer = Buffer.from(await response.arrayBuffer());
7323
- (0, import_node_fs9.writeFileSync)(tgzPath, buffer);
7324
- logger.info(`\u4E0B\u8F7D\u5B8C\u6210 (${buffer.length} bytes)`);
7325
- (0, import_node_fs9.mkdirSync)(stagingDir, { recursive: true });
7326
- const tarResult = await runCommand(
7327
- ["tar", "-xzf", tgzPath, "-C", stagingDir, "--strip-components=1"],
7328
- { timeoutMs: 3e4 }
7329
- );
7330
- if (tarResult.code !== 0) {
7331
- const err = tarResult.stderr || tarResult.stdout || "unknown error";
7332
- return { success: false, message: `\u89E3\u538B\u5931\u8D25: ${err}` };
7333
- }
7334
- (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(targetDir), { recursive: true });
7335
- try {
7336
- backupDir = `${targetDir}.bak.${Date.now()}`;
7337
- (0, import_node_fs9.renameSync)(targetDir, backupDir);
7338
- } catch {
7339
- backupDir = null;
7340
- }
7341
- (0, import_node_fs9.renameSync)(stagingDir, targetDir);
7342
- try {
7343
- await updateConfigRecord2(version, tgzUrl);
7344
- } catch (err) {
7345
- logger.warn(`\u914D\u7F6E\u8BB0\u5F55\u66F4\u65B0\u5931\u8D25\uFF08\u63D2\u4EF6\u6587\u4EF6\u5DF2\u5C31\u4F4D\uFF09: ${String(err)}`);
7346
- }
7347
- if (backupDir) {
7348
- try {
7349
- (0, import_node_fs9.rmSync)(backupDir, { force: true, recursive: true });
7350
- } catch {
7168
+ // src/plugin/light-rules-tools.ts
7169
+ var segmentItemSchema = {
7170
+ type: "object",
7171
+ required: ["mode", "duration_s"],
7172
+ additionalProperties: false,
7173
+ properties: {
7174
+ mode: {
7175
+ type: "string",
7176
+ enum: ["wave", "breath", "strobe", "steady", "wave_rainbow", "pixel_frame"],
7177
+ description: "\u706F\u6548\u6A21\u5F0F\uFF1Awave \u6CE2\u6D6A / breath \u547C\u5438 / strobe \u9891\u95EA / steady \u5E38\u4EAE / wave_rainbow \u6D41\u5149 / pixel_frame \u9010\u7EC4\u50CF\u7D20\u5E27"
7178
+ },
7179
+ duration_s: { type: "number", minimum: 0, description: "\u6301\u7EED\u65F6\u957F\uFF08\u79D2\uFF09\uFF0C0 \u8868\u793A\u65E0\u9650" },
7180
+ brightness: { type: "number", minimum: 0, maximum: 255 },
7181
+ color: {
7182
+ type: "object",
7183
+ required: ["r", "g", "b"],
7184
+ additionalProperties: false,
7185
+ properties: {
7186
+ r: { type: "number", minimum: 0, maximum: 255 },
7187
+ g: { type: "number", minimum: 0, maximum: 255 },
7188
+ b: { type: "number", minimum: 0, maximum: 255 }
7351
7189
  }
7352
- }
7353
- const msg = `\u5DF2\u66F4\u65B0\u5230 ${version}\uFF0C\u8BF7\u91CD\u542F gateway \u751F\u6548`;
7354
- logger.info(msg);
7355
- return { success: true, message: msg };
7356
- } catch (err) {
7357
- if (backupDir) {
7358
- try {
7359
- (0, import_node_fs9.rmSync)(targetDir, { force: true, recursive: true });
7360
- (0, import_node_fs9.renameSync)(backupDir, targetDir);
7361
- logger.info("\u5DF2\u56DE\u6EDA\u5230\u4E4B\u524D\u7248\u672C");
7362
- } catch (rollbackErr) {
7363
- logger.error(`\u56DE\u6EDA\u5931\u8D25: ${String(rollbackErr)}`);
7190
+ },
7191
+ interval_ms: { type: "number", minimum: 0 },
7192
+ direction: { type: "string", enum: ["ltr", "rtl"] },
7193
+ window: { type: "number", enum: [1, 2, 3] },
7194
+ breath_timing: {
7195
+ type: "object",
7196
+ additionalProperties: false,
7197
+ properties: {
7198
+ rise_ms: { type: "number", minimum: 0 },
7199
+ hold_ms: { type: "number", minimum: 0 },
7200
+ fall_ms: { type: "number", minimum: 0 },
7201
+ off_ms: { type: "number", minimum: 0 }
7202
+ }
7203
+ },
7204
+ frames: { type: "array", items: { type: "array", items: { type: "number" } } },
7205
+ frame_duration_ms: { type: "number", minimum: 0 },
7206
+ background: {
7207
+ type: "object",
7208
+ required: ["r", "g", "b"],
7209
+ additionalProperties: false,
7210
+ properties: {
7211
+ r: { type: "number", minimum: 0, maximum: 255 },
7212
+ g: { type: "number", minimum: 0, maximum: 255 },
7213
+ b: { type: "number", minimum: 0, maximum: 255 }
7364
7214
  }
7365
7215
  }
7366
- const errMsg = `\u66F4\u65B0\u6267\u884C\u5F02\u5E38: ${String(err)}`;
7367
- logger.error(errMsg);
7368
- return { success: false, message: errMsg };
7369
- } finally {
7370
- try {
7371
- (0, import_node_fs9.rmSync)(workDir, { force: true, recursive: true });
7372
- } catch {
7373
- }
7374
- }
7375
- }
7376
-
7377
- // src/update/index.ts
7378
- var PLUGIN_ID = "phone-notifications";
7379
- function resolveTargetDir(api) {
7380
- try {
7381
- const cfg = api.runtime.config?.loadConfig?.();
7382
- const installPath = cfg?.plugins?.installs?.[PLUGIN_ID]?.installPath;
7383
- if (installPath) return installPath;
7384
- } catch {
7385
7216
  }
7386
- return (0, import_node_path9.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
7217
+ };
7218
+ var segmentsSchema = {
7219
+ type: "array",
7220
+ description: "\u706F\u6548\u6BB5\u5E8F\u5217\uFF0C1\u201312 \u6BB5\uFF0C\u6309\u987A\u5E8F\u64AD\u653E",
7221
+ minItems: 1,
7222
+ maxItems: MAX_LIGHT_SEGMENTS,
7223
+ items: segmentItemSchema
7224
+ };
7225
+ function ok(data) {
7226
+ return { content: [{ type: "text", text: JSON.stringify(data) }], details: data };
7387
7227
  }
7388
- async function updateConfigRecord(api, version, targetDir, tgzUrl) {
7389
- const configApi = api.runtime.config;
7390
- if (!configApi) return;
7391
- const cfg = configApi.loadConfig();
7392
- if (!cfg.plugins) cfg.plugins = {};
7393
- if (!cfg.plugins.installs) cfg.plugins.installs = {};
7394
- cfg.plugins.installs[PLUGIN_ID] = {
7395
- ...cfg.plugins.installs[PLUGIN_ID],
7396
- source: "archive",
7397
- sourcePath: tgzUrl,
7398
- installPath: targetDir,
7399
- version,
7400
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
7401
- };
7402
- await configApi.writeConfigFile(cfg);
7228
+ function err(code, message) {
7229
+ const data = { ok: false, error: { code, message } };
7230
+ return { content: [{ type: "text", text: JSON.stringify(data) }], details: data };
7403
7231
  }
7404
- function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast, externalUpdateNotifier) {
7405
- if (config.enabled === false) {
7406
- logger.info("\u81EA\u52A8\u66F4\u65B0\u5DF2\u7981\u7528 (autoUpdate.enabled = false)");
7407
- return { start() {
7408
- }, stop() {
7409
- }, notifyBroadcastReady() {
7410
- } };
7411
- }
7412
- let pendingUpdate = null;
7413
- let pendingGatewayUpdate = null;
7414
- function tryBroadcastUpdate(update) {
7415
- const broadcast = getBroadcast();
7416
- if (!broadcast) return false;
7417
- broadcast("plugin.updateAvailable", {
7418
- pluginId: "phone-notifications",
7419
- current: update.current,
7420
- latest: update.latest
7421
- });
7422
- pendingGatewayUpdate = null;
7423
- return true;
7424
- }
7425
- api.on("before_prompt_build", () => {
7426
- if (!pendingUpdate) return;
7427
- return {
7428
- appendSystemContext: `[\u7CFB\u7EDF\u901A\u77E5] phone-notifications \u63D2\u4EF6\u6709\u65B0\u7248\u672C ${pendingUpdate.latest} \u53EF\u7528\uFF08\u5F53\u524D\u7248\u672C ${pendingUpdate.current}\uFF09\u3002
7429
- \u8BF7\u7528\u81EA\u7136\u8BED\u8A00\u5728\u5408\u9002\u65F6\u673A\u544A\u77E5\u7528\u6237\uFF0C\u82E5\u7528\u6237\u540C\u610F\u66F4\u65B0\uFF0C\u8C03\u7528 plugin-update tool \u5E76\u4F20\u5165 version \u53C2\u6570\u3002\u82E5\u7528\u6237\u62D2\u7EDD\uFF0C\u4E0D\u518D\u91CD\u590D\u63D0\u9192\u3002`
7430
- };
7232
+ function registerLightRulesTools(api, registry, logger) {
7233
+ api.registerTool({
7234
+ name: "lightrules.list",
7235
+ label: "List Light Rules",
7236
+ description: '\u5217\u51FA\u6240\u6709\u706F\u6548\u89C4\u5219\uFF08\u5305\u542B enabled/disabled \u72B6\u6001\uFF09\u3002\u5F53\u7528\u6237\u8BF4"\u5217\u51FA\u706F\u6548\u89C4\u5219"\u3001"\u6709\u54EA\u4E9B\u706F\u6548\u89C4\u5219"\u3001"\u67E5\u770B\u89C4\u5219"\u7B49\u65F6\u8C03\u7528\u3002\u6CE8\u610F\uFF1A\u706F\u6548\u89C4\u5219\u7684\u6240\u6709 CRUD \u64CD\u4F5C\u5FC5\u987B\u901A\u8FC7 lightrules.* \u5DE5\u5177\u5B8C\u6210\uFF0C\u7981\u6B62\u76F4\u63A5\u7528 write/edit \u4FEE\u6539 tasks/*/meta.json\u3002',
7237
+ parameters: { type: "object", properties: {}, additionalProperties: false },
7238
+ async execute(_toolCallId, _params) {
7239
+ try {
7240
+ const rules = registry.list().map((rule) => ({ ...rule, id: rule.name }));
7241
+ return ok({ ok: true, rules });
7242
+ } catch (e) {
7243
+ logger.warn(`lightrules.list tool failed: ${e?.message}`);
7244
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7245
+ }
7246
+ }
7431
7247
  });
7432
7248
  api.registerTool({
7433
- name: "plugin-update",
7434
- label: "Plugin Update",
7435
- description: "\u5C06 phone-notifications \u63D2\u4EF6\u66F4\u65B0\u5230\u6307\u5B9A\u7248\u672C\u3002\u4EC5\u5728\u7528\u6237\u660E\u786E\u540C\u610F\u66F4\u65B0\u540E\u8C03\u7528\u3002\u66F4\u65B0\u5B8C\u6210\u540E\u9700\u91CD\u542F gateway \u751F\u6548\u3002",
7249
+ name: "lightrules.create",
7250
+ label: "Create Light Rule",
7251
+ description: '\u521B\u5EFA\u4E00\u6761\u706F\u6548\u89C4\u5219\uFF0C\u6307\u5B9A\u540D\u79F0\u3001\u81EA\u7136\u8BED\u8A00\u89E6\u53D1\u63CF\u8FF0\u548C\u706F\u6548\u53C2\u6570\u3002\u5F53\u7528\u6237\u8BF4"\u521B\u5EFA\u706F\u6548\u89C4\u5219"\u3001"\u65B0\u589E\u89C4\u5219"\u7B49\u65F6\u8C03\u7528\u3002',
7436
7252
  parameters: {
7437
7253
  type: "object",
7438
- required: ["version"],
7254
+ required: ["name", "description", "segments"],
7439
7255
  additionalProperties: false,
7440
7256
  properties: {
7441
- version: {
7257
+ name: { type: "string", description: "\u89C4\u5219\u7684\u552F\u4E00\u6807\u8BC6\u7B26\uFF08\u82F1\u6587 slug\uFF0C\u5982 red_light_on_wechat\uFF09" },
7258
+ description: {
7442
7259
  type: "string",
7443
- description: "\u76EE\u6807\u7248\u672C\u53F7\uFF0C\u5982 1.11.0"
7260
+ description: "\u81EA\u7136\u8BED\u8A00\u89E6\u53D1\u6761\u4EF6\uFF0C\u540C\u65F6\u4F5C\u4E3A\u89C4\u5219\u7528\u9014\u8BF4\u660E\u3002Agent \u6309\u6B64\u5B57\u6BB5\u5224\u65AD\u662F\u5426\u547D\u4E2D\uFF0C\u5FC5\u987B\u6E05\u6670\u63CF\u8FF0\u300C\u4F55\u65F6\u89E6\u53D1\u300D"
7261
+ },
7262
+ segments: segmentsSchema,
7263
+ repeat_times: {
7264
+ type: "number",
7265
+ description: "\u6574\u6761\u706F\u6548\u5E8F\u5217\u91CD\u590D\u6B21\u6570\uFF0C0=\u65E0\u9650\u5FAA\u73AF\uFF0C1=\u64AD\u653E\u4E00\u6B21\uFF08\u9ED8\u8BA4\uFF09"
7444
7266
  }
7445
7267
  }
7446
7268
  },
7447
7269
  async execute(_toolCallId, params) {
7448
- const { version } = params;
7449
- const targetDir = resolveTargetDir(api);
7450
- const result = await executeUpdate(
7451
- version,
7452
- api.runtime.system.runCommandWithTimeout,
7453
- logger,
7454
- targetDir,
7455
- (v, url) => updateConfigRecord(api, v, targetDir, url)
7456
- );
7457
- if (result.success) {
7458
- pendingUpdate = null;
7459
- externalUpdateNotifier?.clearPendingUpdate();
7270
+ const { name, description, segments, repeat_times } = params;
7271
+ if (!name || typeof name !== "string")
7272
+ return err("INVALID_PARAMS", "name is required");
7273
+ if (!description || typeof description !== "string")
7274
+ return err("INVALID_PARAMS", "description is required");
7275
+ const validation = validateSegments(segments);
7276
+ if (!validation.valid) return err("VALIDATION_FAILED", JSON.stringify(validation.errors));
7277
+ let repeatTimes;
7278
+ try {
7279
+ repeatTimes = normalizeRepeatTimes({ repeat_times });
7280
+ assertAncsRepeatTimes(repeatTimes);
7281
+ } catch (e) {
7282
+ return err("VALIDATION_FAILED", e?.message ?? "Unknown error");
7283
+ }
7284
+ try {
7285
+ const result = await registry.create({
7286
+ name,
7287
+ description,
7288
+ matchRules: {},
7289
+ segments: validation.segments,
7290
+ repeat_times: repeatTimes,
7291
+ cronSchedule: "*/5 * * * *"
7292
+ });
7293
+ logger.info(`lightrules.create tool: created ${name}`);
7294
+ return ok({ ok: true, name, cronHint: result.cronHint });
7295
+ } catch (e) {
7296
+ if (e instanceof LightRuleError) return err(e.code, e.message);
7297
+ logger.warn(`lightrules.create tool failed: ${e?.message}`);
7298
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7460
7299
  }
7461
- return {
7462
- content: [{ type: "text", text: result.message }],
7463
- details: result
7464
- };
7465
- }
7466
- });
7467
- api.registerGatewayMethod("plugin.update", async ({ params, respond, context }) => {
7468
- rememberBroadcast?.(context?.broadcast);
7469
- const version = params.version;
7470
- if (!version) {
7471
- respond(false, void 0, {
7472
- code: "MISSING_VERSION",
7473
- message: "version is required"
7474
- });
7475
- return;
7476
- }
7477
- const targetDir = resolveTargetDir(api);
7478
- const result = await executeUpdate(
7479
- version,
7480
- api.runtime.system.runCommandWithTimeout,
7481
- logger,
7482
- targetDir,
7483
- (v, url) => updateConfigRecord(api, v, targetDir, url)
7484
- );
7485
- if (result.success) {
7486
- pendingUpdate = null;
7487
- externalUpdateNotifier?.clearPendingUpdate();
7488
- }
7489
- if (result.success) {
7490
- respond(true, { message: result.message });
7491
- } else {
7492
- respond(false, void 0, {
7493
- code: "UPDATE_FAILED",
7494
- message: result.message
7495
- });
7496
7300
  }
7497
7301
  });
7498
- const intervalMs = (config.checkIntervalHours ?? 4) * 36e5;
7499
- const channel = config.channel ?? "latest";
7500
- logger.info(
7501
- `\u81EA\u52A8\u66F4\u65B0\u5DF2\u542F\u7528: channel=${channel}, checkIntervalHours=${config.checkIntervalHours ?? 4}`
7502
- );
7503
- const checker = new UpdateChecker(
7504
- logger,
7505
- (update) => {
7506
- pendingUpdate = update;
7507
- const broadcasted = tryBroadcastUpdate(update);
7508
- if (!broadcasted) pendingGatewayUpdate = update;
7509
- if (!broadcasted) {
7510
- externalUpdateNotifier?.notifyUpdateAvailable(update);
7302
+ api.registerTool({
7303
+ name: "lightrules.update",
7304
+ label: "Update Light Rule",
7305
+ description: '\u4FEE\u6539\u706F\u6548\u89C4\u5219\uFF08\u542F\u7528/\u7981\u7528\u3001\u6539\u63CF\u8FF0\u3001\u6539\u706F\u6548\u53C2\u6570\uFF09\u3002\u5F53\u7528\u6237\u8BF4"\u7981\u7528\u67D0\u6761\u89C4\u5219"\u3001"\u542F\u7528\u89C4\u5219"\u3001"\u4FEE\u6539\u706F\u6548\u89C4\u5219"\u7B49\u65F6\u8C03\u7528\u3002',
7306
+ parameters: {
7307
+ type: "object",
7308
+ required: ["name"],
7309
+ additionalProperties: false,
7310
+ properties: {
7311
+ name: { type: "string", description: "\u8981\u4FEE\u6539\u7684\u89C4\u5219\u540D\u79F0\uFF08\u552F\u4E00\u6807\u8BC6\u7B26\uFF09" },
7312
+ description: { type: "string", description: "\u65B0\u7684\u89E6\u53D1\u6761\u4EF6\u63CF\u8FF0\uFF08\u53EF\u9009\uFF09" },
7313
+ enabled: { type: "boolean", description: "true=\u542F\u7528\uFF0Cfalse=\u7981\u7528" },
7314
+ segments: { ...segmentsSchema, description: "\u65B0\u7684\u706F\u6548\u6BB5\u5E8F\u5217\uFF08\u53EF\u9009\uFF0C\u4E0D\u586B\u5219\u4FDD\u6301\u4E0D\u53D8\uFF09" },
7315
+ repeat_times: { type: "number", description: "\u65B0\u7684\u91CD\u590D\u6B21\u6570\uFF08\u53EF\u9009\uFF09" }
7511
7316
  }
7512
- logger.info(
7513
- `\u5DF2\u901A\u77E5\u66F4\u65B0 ${update.current} \u2192 ${update.latest}` + (broadcasted ? "\uFF08\u5BF9\u8BDD + \u7F51\u5173\uFF09" : "\uFF08\u5BF9\u8BDD\u901A\u9053\u5DF2\u751F\u6548\uFF0C\u7B49\u5F85\u4E0B\u6B21 gateway \u8BF7\u6C42\u65F6\u8865\u53D1\u5BA2\u6237\u7AEF\u4E8B\u4EF6\uFF09")
7514
- );
7515
7317
  },
7516
- intervalMs,
7517
- channel
7518
- );
7519
- return {
7520
- start: () => checker.start(),
7521
- stop: () => checker.stop(),
7522
- notifyBroadcastReady() {
7523
- const update = pendingGatewayUpdate;
7524
- if (!update) return;
7525
- if (!tryBroadcastUpdate(update)) return;
7526
- logger.info(`\u5DF2\u8865\u53D1\u63D2\u4EF6\u66F4\u65B0\u4E8B\u4EF6 ${update.current} \u2192 ${update.latest}`);
7318
+ async execute(_toolCallId, params) {
7319
+ const { name, description, enabled, segments, repeat_times } = params;
7320
+ if (!name || typeof name !== "string")
7321
+ return err("INVALID_PARAMS", "name is required");
7322
+ let validatedSegments;
7323
+ if (segments !== void 0) {
7324
+ const validation = validateSegments(segments);
7325
+ if (!validation.valid) return err("VALIDATION_FAILED", JSON.stringify(validation.errors));
7326
+ validatedSegments = validation.segments;
7327
+ }
7328
+ let repeatTimes;
7329
+ if (repeat_times !== void 0) {
7330
+ try {
7331
+ repeatTimes = normalizeRepeatTimes({ repeat_times });
7332
+ assertAncsRepeatTimes(repeatTimes);
7333
+ } catch (e) {
7334
+ return err("VALIDATION_FAILED", e?.message ?? "Unknown error");
7335
+ }
7336
+ }
7337
+ try {
7338
+ const result = await registry.update({
7339
+ name,
7340
+ description,
7341
+ segments: validatedSegments,
7342
+ repeat_times: repeatTimes,
7343
+ enabled
7344
+ });
7345
+ logger.info(`lightrules.update tool: updated ${name}`);
7346
+ return ok({
7347
+ ok: true,
7348
+ name: result.meta.name,
7349
+ updated: true,
7350
+ rule: result.meta,
7351
+ cronHint: result.cronHint
7352
+ });
7353
+ } catch (e) {
7354
+ if (e instanceof LightRuleError) return err(e.code, e.message);
7355
+ logger.warn(`lightrules.update tool failed: ${e?.message}`);
7356
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7357
+ }
7527
7358
  }
7528
- };
7529
- }
7530
-
7531
- // src/plugin/auto-update.ts
7532
- function registerAutoUpdateLifecycle(deps) {
7533
- const {
7534
- api,
7535
- config,
7536
- logger,
7537
- getBroadcastFn,
7538
- cacheBroadcast,
7539
- tunnelService
7540
- } = deps;
7541
- const autoUpdateConfig = {
7542
- ...config.autoUpdate,
7543
- channel: resolveUpdateChannel({
7544
- configuredChannel: config.autoUpdate?.channel,
7545
- currentVersion: PLUGIN_VERSION,
7546
- envName: loadEnvName()
7547
- })
7548
- };
7549
- const autoUpdateLifecycle = registerAutoUpdate(
7550
- api,
7551
- logger,
7552
- autoUpdateConfig,
7553
- getBroadcastFn,
7554
- cacheBroadcast,
7555
- tunnelService ? {
7556
- notifyUpdateAvailable: (update) => tunnelService.notifyUpdateAvailable(update),
7557
- clearPendingUpdate: () => tunnelService.clearPendingUpdate()
7558
- } : void 0
7559
- );
7560
- api.registerService({
7561
- id: "update-checker",
7562
- start() {
7563
- autoUpdateLifecycle.start();
7359
+ });
7360
+ api.registerTool({
7361
+ name: "lightrules.delete",
7362
+ label: "Delete Light Rule",
7363
+ description: '\u5220\u9664\u4E00\u6761\u706F\u6548\u89C4\u5219\uFF08\u4E0D\u53EF\u6062\u590D\uFF09\u3002\u5F53\u7528\u6237\u8BF4"\u5220\u9664\u706F\u6548\u89C4\u5219"\u3001"\u79FB\u9664\u89C4\u5219"\u7B49\u65F6\u8C03\u7528\u3002',
7364
+ parameters: {
7365
+ type: "object",
7366
+ required: ["name"],
7367
+ additionalProperties: false,
7368
+ properties: {
7369
+ name: { type: "string", description: "\u8981\u5220\u9664\u7684\u89C4\u5219\u540D\u79F0" }
7370
+ }
7564
7371
  },
7565
- stop() {
7566
- autoUpdateLifecycle.stop();
7372
+ async execute(_toolCallId, params) {
7373
+ const { name } = params;
7374
+ if (!name || typeof name !== "string")
7375
+ return err("INVALID_PARAMS", "name is required");
7376
+ try {
7377
+ const result = await registry.delete(name);
7378
+ logger.info(`lightrules.delete tool: deleted ${name}`);
7379
+ return ok({ ok: true, name: result.name, deleted: true, cronHint: result.cronHint });
7380
+ } catch (e) {
7381
+ if (e instanceof LightRuleError) return err(e.code, e.message);
7382
+ logger.warn(`lightrules.delete tool failed: ${e?.message}`);
7383
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7384
+ }
7567
7385
  }
7568
7386
  });
7569
- logger.info("\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\u670D\u52A1\u5DF2\u6CE8\u518C");
7570
- return autoUpdateLifecycle;
7571
7387
  }
7572
7388
 
7573
- // src/plugin/cli.ts
7574
- var import_node_path17 = require("path");
7389
+ // src/light-rules/evaluator-job.ts
7390
+ var EVALUATOR_JOB_ID = "light-rules-evaluator";
7391
+ var EVALUATOR_SUBAGENT_SESSION_KEY = EVALUATOR_JOB_ID;
7392
+ var FALLBACK_CRON_EXPR = "0 0 1 1 *";
7393
+ function buildEvaluatorJobMessage(notificationsDir) {
7394
+ return `\u706F\u6548\u89C4\u5219\u8BC4\u4F30\u4EFB\u52A1\u3002
7575
7395
 
7576
- // src/cli/auth.ts
7577
- var import_node_fs10 = require("fs");
7578
- init_credentials();
7579
- function registerAuthCli(program) {
7580
- const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
7581
- auth.command("set-api-key <apiKey>").description("\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u6301\u4E45\u5316\u5230\u672C\u5730\u914D\u7F6E\uFF09").action((apiKey) => {
7582
- writeCredentials({ ...readCredentials(), apiKey, token: void 0 });
7583
- output({
7584
- ok: true,
7585
- apiKey: apiKey.slice(0, 8) + "\u2026",
7586
- storedAt: credentialsPath()
7587
- });
7588
- });
7589
- auth.command("set-token <token>").description("\uFF08\u517C\u5BB9\uFF09\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u65E7\u547D\u4EE4\u540D\uFF09").action((token) => {
7590
- writeCredentials({ ...readCredentials(), apiKey: token, token: void 0 });
7591
- output({
7592
- ok: true,
7593
- apiKey: token.slice(0, 8) + "\u2026",
7594
- storedAt: credentialsPath()
7595
- });
7596
- });
7597
- auth.command("show").description("\u67E5\u770B\u5F53\u524D\u8BA4\u8BC1\u72B6\u6001").action(() => {
7598
- const creds = readCredentials();
7599
- const apiKey = creds.apiKey ?? creds.token;
7600
- if (apiKey) {
7601
- output({
7602
- ok: true,
7603
- hasApiKey: true,
7604
- apiKey: apiKey.slice(0, 8) + "\u2026",
7605
- storedAt: credentialsPath()
7606
- });
7607
- } else {
7608
- output({ ok: true, hasApiKey: false });
7609
- }
7610
- });
7611
- auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
7612
- const path2 = credentialsPath();
7613
- if ((0, import_node_fs10.existsSync)(path2)) {
7614
- const creds = readCredentials();
7615
- delete creds.apiKey;
7616
- delete creds.token;
7617
- if (Object.keys(creds).length === 0) {
7618
- (0, import_node_fs10.rmSync)(path2, { force: true });
7619
- } else {
7620
- writeCredentials(creds);
7621
- }
7622
- }
7623
- output({ ok: true, cleared: true });
7624
- });
7396
+ \u6267\u884C\u6B65\u9AA4\uFF1A
7397
+ 1. \u8BFB\u53D6 tasks/light-rules-evaluator/checkpoint.json\uFF08\u8BB0\u5F55\u4E0A\u6B21\u5904\u7406\u8FDB\u5EA6\uFF09
7398
+ 2. \u626B\u63CF ${notificationsDir} \u76EE\u5F55\uFF0C\u83B7\u53D6 checkpoint \u4E4B\u540E\u7684\u65B0\u901A\u77E5
7399
+ 3. \u626B\u63CF tasks/ \u76EE\u5F55\uFF0C\u8BFB\u53D6\u6240\u6709 type=light-rule \u4E14 enabled=true \u7684 meta.json
7400
+ 4. \u5BF9\u6BCF\u6761\u65B0\u901A\u77E5\uFF0C\u9010\u4E00\u5224\u65AD\u662F\u5426\u547D\u4E2D\u6BCF\u6761\u89C4\u5219\u7684 description\uFF08\u8BED\u4E49\u5339\u914D\uFF09
7401
+ 5. \u547D\u4E2D\u65F6\uFF1A\u4EE5\u8BE5\u89C4\u5219\u7684 segments \u548C repeat_times \u8C03\u7528 light_control \u5DE5\u5177
7402
+ 6. \u66F4\u65B0 checkpoint.json\uFF0C\u8BB0\u5F55\u5DF2\u5904\u7406\u5230\u7684\u6700\u65B0\u901A\u77E5\u4F4D\u7F6E
7403
+ 7. \u82E5\u65E0\u65B0\u901A\u77E5\u6216\u65E0 enabled \u89C4\u5219\uFF1A\u8F93\u51FA NO_CHANGE\uFF0C\u76F4\u63A5\u7ED3\u675F`;
7625
7404
  }
7626
-
7627
- // src/cli/ntf-search.ts
7628
- function filterItem(item, opts) {
7629
- if (opts.app && item.appName !== opts.app) return false;
7630
- if (opts.sender && !item.title.includes(opts.sender)) return false;
7631
- if (opts.keyword && !item.title.includes(opts.keyword) && !item.content.includes(opts.keyword)) {
7632
- return false;
7405
+ var LightRulesEvaluatorJob = class {
7406
+ logger;
7407
+ registry;
7408
+ subagentRunner;
7409
+ getNotificationsDir;
7410
+ /**
7411
+ * 记录本进程生命周期内 job 是否已确认存在。
7412
+ * 仅在 `ensureJobExists` 成功后置 true,避免每次 push 都做检查。
7413
+ */
7414
+ jobEnsured = false;
7415
+ /**
7416
+ * 首次创建 job 时的并发保护。
7417
+ * 避免冷启动瞬间多条通知并发到达时重复调用 `cron.add`。
7418
+ */
7419
+ ensureJobPromise = null;
7420
+ /**
7421
+ * subagent fallback 路径的并发保护。
7422
+ * 若评估 session 已在运行中,跳过本次触发(checkpoint 保证下次补处理)。
7423
+ */
7424
+ subagentInFlight = false;
7425
+ constructor(deps) {
7426
+ this.logger = deps.logger;
7427
+ this.registry = deps.registry;
7428
+ this.subagentRunner = deps.subagentRunner;
7429
+ this.getNotificationsDir = deps.getNotificationsDir ?? (() => void 0);
7633
7430
  }
7634
- return true;
7635
- }
7636
- function registerNtfSearch(ntf, ctx) {
7637
- ntf.command("search").description("\u67E5\u8BE2\u901A\u77E5\uFF08\u6309\u65F6\u95F4/\u5E94\u7528/\u53D1\u9001\u4EBA/\u5173\u952E\u8BCD\u7B5B\u9009\uFF09").option("--from <time>", "\u5F00\u59CB\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00").option("--to <time>", "\u7ED3\u675F\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T18:00:00+08:00").option("--app <name>", "\u6309\u5E94\u7528\u540D\u8FC7\u6EE4").option("--sender <name>", "\u6309\u53D1\u9001\u4EBA\u8FC7\u6EE4").option("--keyword <text>", "\u5728\u6807\u9898\u548C\u5185\u5BB9\u4E2D\u641C\u7D22\u5173\u952E\u8BCD").option("--limit <n>", "\u6700\u5927\u8FD4\u56DE\u6761\u6570", "100").action(
7638
- async (opts) => {
7639
- const dir = resolveNotificationsDir(ctx);
7640
- if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
7641
- progress("\u6B63\u5728\u641C\u7D22\u901A\u77E5...");
7642
- const limit = parseInt(opts.limit, 10) || 100;
7643
- if (limit <= 0) {
7644
- exitError("INVALID_LIMIT", "--limit \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570");
7645
- }
7646
- const hasFrom = typeof opts.from === "string" && opts.from.length > 0;
7647
- const hasTo = typeof opts.to === "string" && opts.to.length > 0;
7648
- const fromTs = hasFrom ? parseIsoTime(opts.from, "--from") : null;
7649
- const toTs = hasTo ? parseIsoTime(opts.to, "--to") : null;
7650
- if (fromTs !== null && toTs !== null && fromTs > toTs) {
7651
- exitError("INVALID_TIME_RANGE", "--from \u4E0D\u80FD\u665A\u4E8E --to");
7431
+ /**
7432
+ * 通知落盘后调用。若有新增通知且存在 enabled 规则,则触发评估。
7433
+ *
7434
+ * 两条路径:
7435
+ * - cron 不为 null:enqueueRun("force") 入队(gateway context 路径,正常路径)
7436
+ * - cron null:通过 subagentRunner 直接运行(HTTP Relay 路径,fallback)
7437
+ *
7438
+ * @param cron 来自 gateway context 的 CronService;HTTP 路径下为 null
7439
+ * @param insertedCount 本次 ingest 新落盘的通知条数(StoredNotification 去重后)
7440
+ */
7441
+ async triggerIfNeeded(cron, insertedCount) {
7442
+ if (insertedCount === 0) return;
7443
+ if (this.registry.getEnabled().length === 0) return;
7444
+ if (!cron) {
7445
+ await this.triggerViaSubagent();
7446
+ return;
7447
+ }
7448
+ try {
7449
+ await this.ensureJobExists(cron);
7450
+ } catch (err2) {
7451
+ this.logger.warn(`light-rules-evaluator: job ensure failed: ${err2?.message ?? err2}`);
7452
+ return;
7453
+ }
7454
+ try {
7455
+ const result = await cron.enqueueRun(EVALUATOR_JOB_ID, "force");
7456
+ if (!result.ok) {
7457
+ this.logger.warn("light-rules-evaluator: enqueueRun returned ok=false");
7458
+ return;
7652
7459
  }
7653
- const keys = await listDateKeysAsync(dir);
7654
- const results = [];
7655
- if (fromTs !== null || toTs !== null) {
7656
- const fromDateKey = hasFrom ? opts.from.slice(0, 10) : null;
7657
- const toDateKey = hasTo ? opts.to.slice(0, 10) : null;
7658
- for (const dateKey of keys) {
7659
- if (fromDateKey && dateKey < fromDateKey) continue;
7660
- if (toDateKey && dateKey > toDateKey) continue;
7661
- const items = await readDateFileAsync(dir, dateKey);
7662
- for (const item of items) {
7663
- if (!filterItem(item, opts)) continue;
7664
- const itemTs = Date.parse(item.timestamp);
7665
- if (Number.isNaN(itemTs)) continue;
7666
- if (fromTs !== null && itemTs < fromTs) continue;
7667
- if (toTs !== null && itemTs > toTs) continue;
7668
- results.push(item);
7669
- }
7670
- }
7671
- } else {
7672
- for (const dateKey of keys) {
7673
- const items = await readDateFileAsync(dir, dateKey);
7674
- for (const item of items) {
7675
- if (!filterItem(item, opts)) continue;
7676
- if (Number.isNaN(Date.parse(item.timestamp))) continue;
7677
- results.push(item);
7678
- }
7679
- }
7460
+ if ("enqueued" in result && result.enqueued) {
7461
+ this.logger.info(`light-rules-evaluator: enqueued runId=${result.runId}`);
7462
+ } else if ("reason" in result) {
7463
+ this.logger.info(`light-rules-evaluator: enqueueRun skipped (${result.reason})`);
7680
7464
  }
7681
- const notifications = sortNotificationsByTimestampDesc(results).slice(
7682
- 0,
7683
- limit
7684
- );
7685
- output({
7686
- ok: true,
7687
- total: notifications.length,
7688
- notifications
7689
- });
7465
+ } catch (err2) {
7466
+ this.logger.warn(`light-rules-evaluator: enqueueRun failed: ${err2?.message ?? err2}`);
7690
7467
  }
7691
- );
7692
- }
7693
-
7694
- // src/cli/ntf-stats.ts
7695
- function registerNtfStats(ntf, ctx) {
7696
- ntf.command("stats").description("\u901A\u77E5\u7EDF\u8BA1\u5206\u6790\uFF08\u6309\u65E5\u671F/\u5E94\u7528/\u53D1\u9001\u4EBA/\u65F6\u6BB5\u805A\u5408\uFF09").option("--from <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD", daysAgo(7)).option("--to <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD", today()).option("--app <name>", "\u53EA\u7EDF\u8BA1\u6307\u5B9A\u5E94\u7528").option("--dim <dimension>", "\u7EDF\u8BA1\u7EF4\u5EA6\uFF1Adate/app/sender/hour/all", "all").action(
7697
- (opts) => {
7698
- const dir = resolveNotificationsDir(ctx);
7699
- if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
7700
- const dim = opts.dim;
7701
- const keys = filterDateRange(listDateKeys(dir), opts.from, opts.to);
7702
- const byDate = {};
7703
- const byApp = {};
7704
- const bySender = {};
7705
- const byHour = {};
7706
- let total = 0;
7707
- for (const dateKey of keys) {
7708
- const items = readDateFile(dir, dateKey);
7709
- let dateCount = 0;
7710
- for (const item of items) {
7711
- if (opts.app && item.appName !== opts.app) continue;
7712
- total++;
7713
- dateCount++;
7714
- byApp[item.appName] = (byApp[item.appName] || 0) + 1;
7715
- if (item.title) {
7716
- bySender[item.title] = (bySender[item.title] || 0) + 1;
7717
- }
7718
- const hourMatch = /T(\d{2}):/.exec(item.timestamp);
7719
- if (hourMatch) {
7720
- const h = hourMatch[1];
7721
- byHour[h] = (byHour[h] || 0) + 1;
7722
- }
7723
- }
7724
- byDate[dateKey] = dateCount;
7725
- }
7726
- const result = {
7727
- ok: true,
7728
- range: { from: opts.from, to: opts.to },
7729
- total
7730
- };
7731
- if (dim === "date" || dim === "all") {
7732
- result.byDate = byDate;
7733
- }
7734
- if (dim === "app" || dim === "all") {
7735
- result.byApp = byApp;
7736
- }
7737
- if (dim === "sender" || dim === "all") {
7738
- const sorted = Object.entries(bySender).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([sender, count]) => ({ sender, count }));
7739
- result.bySender = sorted;
7740
- }
7741
- if (dim === "hour" || dim === "all") {
7742
- result.byHour = byHour;
7743
- }
7744
- output(result);
7745
- }
7746
- );
7747
- }
7748
-
7749
- // src/cli/ntf-sync.ts
7750
- var import_node_fs11 = require("fs");
7751
- var import_node_path10 = require("path");
7752
- var SYNC_FETCH_LIMIT = 300;
7753
- function checkpointPath(dir) {
7754
- return (0, import_node_path10.join)(dir, ".checkpoint.json");
7755
- }
7756
- function readCheckpoint(dir) {
7757
- const p = checkpointPath(dir);
7758
- if (!(0, import_node_fs11.existsSync)(p)) return {};
7759
- try {
7760
- return JSON.parse((0, import_node_fs11.readFileSync)(p, "utf-8"));
7761
- } catch {
7762
- return {};
7763
7468
  }
7764
- }
7765
- function writeCheckpoint(dir, data) {
7766
- (0, import_node_fs11.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
7767
- }
7768
- function registerNtfSync(ntf, ctx) {
7769
- const sync = ntf.command("sync").description("\u540C\u6B65\u901A\u77E5\u5230\u8BB0\u5FC6\u7CFB\u7EDF");
7770
- sync.command("scan").description("\u626B\u63CF\u672A\u5904\u7406\u7684\u901A\u77E5\uFF0C\u8FD4\u56DE\u5F85\u540C\u6B65\u6458\u8981").action(() => {
7771
- const dir = resolveNotificationsDir(ctx);
7772
- if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
7773
- const checkpoint = readCheckpoint(dir);
7774
- const keys = listDateKeys(dir);
7775
- const pending = [];
7776
- let totalPending = 0;
7777
- for (const dateKey of keys) {
7778
- const items = readDateFile(dir, dateKey);
7779
- const lastIndex = checkpoint[dateKey]?.lastIndex ?? -1;
7780
- const unprocessed = items.length - (lastIndex + 1);
7781
- if (unprocessed > 0) {
7782
- pending.push({
7783
- date: dateKey,
7784
- count: unprocessed,
7785
- startIndex: lastIndex + 1
7786
- });
7787
- totalPending += unprocessed;
7788
- }
7469
+ /**
7470
+ * cron service 不可用时的 fallback:直接通过 subagentRunner 运行评估 session。
7471
+ *
7472
+ * 并发保护:若上一次 subagent 运行尚未完成,本次跳过。
7473
+ * checkpoint 保证即使本次跳过,下次触发时会处理所有积压通知。
7474
+ */
7475
+ async triggerViaSubagent() {
7476
+ if (!this.subagentRunner) {
7477
+ this.logger.warn(
7478
+ "light-rules-evaluator: cron service unavailable and no subagent fallback configured; notifications ingested via HTTP will not trigger light rules until an agent session is active"
7479
+ );
7480
+ return;
7789
7481
  }
7790
- output({ ok: true, pending, totalPending });
7791
- });
7792
- sync.command("fetch").description("\u83B7\u53D6\u6307\u5B9A\u65E5\u671F\u7684\u672A\u5904\u7406\u901A\u77E5\u8BE6\u60C5").requiredOption("--date <date>", "\u76EE\u6807\u65E5\u671F YYYY-MM-DD").action((opts) => {
7793
- const dir = resolveNotificationsDir(ctx);
7794
- if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
7795
- const items = readDateFile(dir, opts.date);
7796
- if (items.length === 0) {
7797
- exitError("NO_DATA", `\u65E5\u671F ${opts.date} \u65E0\u901A\u77E5\u6570\u636E`);
7482
+ if (this.subagentInFlight) {
7483
+ this.logger.info("light-rules-evaluator: subagent run in-flight, skipping this trigger");
7484
+ return;
7798
7485
  }
7799
- const checkpoint = readCheckpoint(dir);
7800
- const lastIndex = checkpoint[opts.date]?.lastIndex ?? -1;
7801
- const startIndex = lastIndex + 1;
7802
- const unprocessed = items.slice(startIndex);
7803
- const notifications = unprocessed.slice(0, SYNC_FETCH_LIMIT);
7804
- const endIndex = notifications.length > 0 ? startIndex + notifications.length - 1 : lastIndex;
7805
- const hasMore = unprocessed.length > notifications.length;
7806
- if (unprocessed.length === 0) {
7807
- output({
7808
- ok: true,
7809
- date: opts.date,
7810
- startIndex,
7811
- endIndex,
7812
- nextStartIndex: null,
7813
- limit: SYNC_FETCH_LIMIT,
7814
- returned: 0,
7815
- totalUnprocessed: 0,
7816
- hasMore: false,
7817
- notifications: []
7486
+ const notificationsDir = this.getNotificationsDir();
7487
+ if (!notificationsDir) {
7488
+ this.logger.warn("light-rules-evaluator: notifications dir not ready, skipping subagent trigger");
7489
+ return;
7490
+ }
7491
+ this.subagentInFlight = true;
7492
+ try {
7493
+ const result = await this.subagentRunner.run({
7494
+ sessionKey: EVALUATOR_SUBAGENT_SESSION_KEY,
7495
+ message: buildEvaluatorJobMessage(notificationsDir),
7496
+ deliver: false,
7497
+ idempotencyKey: `${EVALUATOR_SUBAGENT_SESSION_KEY}-${Date.now()}`
7498
+ });
7499
+ this.logger.info(`light-rules-evaluator: subagent triggered runId=${result.runId}`);
7500
+ } catch (err2) {
7501
+ this.logger.warn(`light-rules-evaluator: subagent trigger failed: ${err2?.message ?? err2}`);
7502
+ } finally {
7503
+ this.subagentInFlight = false;
7504
+ }
7505
+ }
7506
+ /**
7507
+ * 按需创建 `light-rules-evaluator` job。
7508
+ * 若 job 已存在(内存缓存或 cron store),直接返回;否则调用 `cron.add`。
7509
+ */
7510
+ async ensureJobExists(cron) {
7511
+ if (this.jobEnsured) return;
7512
+ if (!this.ensureJobPromise) {
7513
+ this.ensureJobPromise = this.createJobIfNeeded(cron).finally(() => {
7514
+ this.ensureJobPromise = null;
7818
7515
  });
7516
+ }
7517
+ await this.ensureJobPromise;
7518
+ }
7519
+ async createJobIfNeeded(cron) {
7520
+ if (cron.getJob(EVALUATOR_JOB_ID)) {
7521
+ this.jobEnsured = true;
7819
7522
  return;
7820
7523
  }
7821
- output({
7822
- ok: true,
7823
- date: opts.date,
7824
- startIndex,
7825
- endIndex,
7826
- nextStartIndex: hasMore ? endIndex + 1 : null,
7827
- limit: SYNC_FETCH_LIMIT,
7828
- returned: notifications.length,
7829
- totalUnprocessed: unprocessed.length,
7830
- hasMore,
7831
- notifications
7832
- });
7833
- });
7834
- sync.command("commit").description("\u6807\u8BB0\u6307\u5B9A\u65E5\u671F\u5F53\u524D\u6279\u6B21\u5904\u7406\u5B8C\u6210\uFF0C\u66F4\u65B0 checkpoint").requiredOption("--date <date>", "\u76EE\u6807\u65E5\u671F YYYY-MM-DD").action((opts) => {
7835
- const dir = resolveNotificationsDir(ctx);
7836
- if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
7837
- const items = readDateFile(dir, opts.date);
7838
- if (items.length === 0) {
7839
- exitError("NO_DATA", `\u65E5\u671F ${opts.date} \u65E0\u901A\u77E5\u6570\u636E`);
7524
+ try {
7525
+ await cron.add({
7526
+ id: EVALUATOR_JOB_ID,
7527
+ name: "\u706F\u6548\u89C4\u5219\u8BC4\u4F30",
7528
+ description: "\u4E8B\u4EF6\u9A71\u52A8\uFF1A\u901A\u77E5\u5230\u8FBE\u65F6\u8BC4\u4F30\u6240\u6709 enabled \u706F\u6548\u89C4\u5219\uFF0C\u547D\u4E2D\u5219\u8C03\u7528 light_control \u89E6\u53D1\u706F\u6548",
7529
+ enabled: true,
7530
+ schedule: { kind: "cron", expr: FALLBACK_CRON_EXPR },
7531
+ sessionTarget: "isolated",
7532
+ wakeMode: "now",
7533
+ payload: {
7534
+ kind: "agentTurn",
7535
+ message: buildEvaluatorJobMessage(this.getNotificationsDir() ?? "notifications")
7536
+ }
7537
+ });
7538
+ this.logger.info("light-rules-evaluator: job created");
7539
+ } catch (err2) {
7540
+ if (!cron.getJob(EVALUATOR_JOB_ID)) {
7541
+ throw err2;
7542
+ }
7840
7543
  }
7841
- const checkpoint = readCheckpoint(dir);
7842
- const lastIndex = checkpoint[opts.date]?.lastIndex ?? -1;
7843
- const committedIndex = Math.min(items.length - 1, lastIndex + SYNC_FETCH_LIMIT);
7844
- const hasMore = committedIndex < items.length - 1;
7845
- checkpoint[opts.date] = { lastIndex: committedIndex };
7846
- writeCheckpoint(dir, checkpoint);
7847
- output({
7848
- ok: true,
7849
- date: opts.date,
7850
- committedIndex,
7851
- limit: SYNC_FETCH_LIMIT,
7852
- hasMore,
7853
- nextStartIndex: hasMore ? committedIndex + 1 : null
7854
- });
7855
- });
7856
- }
7544
+ this.jobEnsured = true;
7545
+ }
7546
+ };
7857
7547
 
7858
- // src/cli/ntf-monitor.ts
7859
- var import_node_fs12 = require("fs");
7860
- var import_node_path11 = require("path");
7861
- function tasksDir2(ctx) {
7862
- const base = ctx.workspaceDir || ctx.stateDir;
7863
- if (!base) throw new Error("workspaceDir and stateDir both unavailable");
7864
- return (0, import_node_path11.join)(base, "tasks");
7548
+ // src/light-rules/migration.ts
7549
+ var import_node_fs6 = require("fs");
7550
+ var import_node_path5 = require("path");
7551
+ var NO_MATCH_FETCH_PY = `#!/usr/bin/env python3
7552
+ # \u6B64\u6587\u4EF6\u7531\u8FC1\u79FB\u5DE5\u5177\u751F\u6210\u3002
7553
+ # \u706F\u6548\u89C4\u5219\u5DF2\u8FC1\u79FB\u81F3\u4E8B\u4EF6\u9A71\u52A8\u67B6\u6784\uFF0C\u6B64 cron job \u4E0D\u518D\u6267\u884C\u5B9E\u9645\u5DE5\u4F5C\u3002
7554
+ print("NO_MATCH")
7555
+ `;
7556
+ function normalizeScriptText(text) {
7557
+ return text.replace(/\r\n/g, "\n").trim();
7865
7558
  }
7866
- function readMeta2(taskDir) {
7867
- const metaPath = (0, import_node_path11.join)(taskDir, "meta.json");
7868
- if (!(0, import_node_fs12.existsSync)(metaPath)) return null;
7869
- try {
7870
- return JSON.parse((0, import_node_fs12.readFileSync)(metaPath, "utf-8"));
7871
- } catch {
7872
- return null;
7559
+ function resolveTasksDir(ctx) {
7560
+ if (ctx.workspaceDir) return (0, import_node_path5.join)(ctx.workspaceDir, "tasks");
7561
+ if (ctx.stateDir) {
7562
+ const inferredWorkspaceDir = (0, import_node_path5.join)(ctx.stateDir, "workspace");
7563
+ if ((0, import_node_fs6.existsSync)(inferredWorkspaceDir)) return (0, import_node_path5.join)(inferredWorkspaceDir, "tasks");
7564
+ return (0, import_node_path5.join)(ctx.stateDir, "tasks");
7873
7565
  }
7566
+ return null;
7874
7567
  }
7875
- function writeMeta2(taskDir, meta) {
7876
- (0, import_node_fs12.writeFileSync)((0, import_node_path11.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
7568
+ function migrateLegacyLightRuleTasks(ctx, logger) {
7569
+ const tasksDir3 = resolveTasksDir(ctx);
7570
+ if (!tasksDir3 || !(0, import_node_fs6.existsSync)(tasksDir3)) return;
7571
+ try {
7572
+ for (const entry of (0, import_node_fs6.readdirSync)(tasksDir3, { withFileTypes: true })) {
7573
+ if (!entry.isDirectory()) continue;
7574
+ migrateTaskDir((0, import_node_path5.join)(tasksDir3, String(entry.name)), logger);
7575
+ }
7576
+ } catch (err2) {
7577
+ logger.warn(`migration: failed to read tasks dir: ${err2?.message}`);
7578
+ }
7877
7579
  }
7878
- function generateReadme(name, description) {
7879
- return `# Monitor Task: ${name}
7880
-
7881
- ## \u63CF\u8FF0
7882
- ${description}
7883
-
7884
- ## \u5904\u7406\u6307\u5357
7885
- \u5F53 fetch.py \u8F93\u51FA\u5339\u914D\u7684\u901A\u77E5\u65F6\uFF0C\u8BF7\uFF1A
7886
- 1. \u9605\u8BFB\u5339\u914D\u5230\u7684\u901A\u77E5\u5185\u5BB9
7887
- 2. \u6839\u636E\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\u662F\u5426\u9700\u8981\u901A\u77E5\u7528\u6237
7888
- 3. \u5982\u9700\u901A\u77E5\u7528\u6237\uFF0C\u4F7F\u7528 message \u5DE5\u5177\u53D1\u9001\u6458\u8981
7889
- `;
7580
+ function migrateTaskDir(taskDir, logger) {
7581
+ const metaPath = (0, import_node_path5.join)(taskDir, "meta.json");
7582
+ if (!(0, import_node_fs6.existsSync)(metaPath)) return;
7583
+ let meta;
7584
+ try {
7585
+ meta = JSON.parse((0, import_node_fs6.readFileSync)(metaPath, "utf-8"));
7586
+ } catch {
7587
+ return;
7588
+ }
7589
+ if (meta.type !== "light-rule") return;
7590
+ const name = typeof meta.name === "string" ? meta.name : taskDir;
7591
+ mergeMatchRulesIntoDescription(meta, name, metaPath, logger);
7592
+ replaceFetchPy(taskDir, name, logger);
7593
+ for (const filename of ["README.md", "checkpoint.json"]) {
7594
+ removeFile((0, import_node_path5.join)(taskDir, filename), name, filename, logger);
7595
+ }
7890
7596
  }
7891
- function registerNtfMonitor(ntf, ctx) {
7892
- const monitor = ntf.command("monitor").description("\u901A\u77E5\u76D1\u63A7\u4EFB\u52A1\u7BA1\u7406");
7893
- monitor.command("list").description("\u5217\u51FA\u6240\u6709\u76D1\u63A7\u4EFB\u52A1").action(() => {
7894
- const dir = tasksDir2(ctx);
7895
- if (!(0, import_node_fs12.existsSync)(dir)) {
7896
- output({ ok: true, tasks: [] });
7597
+ function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
7598
+ const matchRules = meta.matchRules;
7599
+ if (!matchRules || typeof matchRules !== "object") return;
7600
+ const rules = matchRules;
7601
+ const parts = [];
7602
+ if (rules.appName) parts.push(`app=${rules.appName}`);
7603
+ if (rules.senderKeywords?.length) {
7604
+ parts.push(`\u53D1\u4EF6\u4EBA\u5173\u952E\u8BCD=${rules.senderKeywords.join("\u3001")}`);
7605
+ }
7606
+ if (rules.contentKeywords?.length) {
7607
+ parts.push(`\u5185\u5BB9\u5173\u952E\u8BCD=${rules.contentKeywords.join("\u3001")}`);
7608
+ }
7609
+ if (parts.length > 0) {
7610
+ const existing = typeof meta.description === "string" ? meta.description.trim() : "";
7611
+ meta.description = existing ? `${existing}\u3002\u5339\u914D\u89C4\u5219\uFF1A${parts.join("\uFF0C")}` : `\u5339\u914D\u89C4\u5219\uFF1A${parts.join("\uFF0C")}`;
7612
+ }
7613
+ delete meta.matchRules;
7614
+ try {
7615
+ (0, import_node_fs6.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7616
+ logger.info(`migration: merged matchRules into description for light rule: ${name}`);
7617
+ } catch (err2) {
7618
+ logger.warn(`migration: failed to update meta.json for ${name}: ${err2?.message}`);
7619
+ }
7620
+ }
7621
+ function replaceFetchPy(taskDir, name, logger) {
7622
+ const fetchPyPath = (0, import_node_path5.join)(taskDir, "fetch.py");
7623
+ if (!(0, import_node_fs6.existsSync)(fetchPyPath)) return;
7624
+ try {
7625
+ const existing = (0, import_node_fs6.readFileSync)(fetchPyPath, "utf-8");
7626
+ if (normalizeScriptText(existing) === normalizeScriptText(NO_MATCH_FETCH_PY)) {
7897
7627
  return;
7898
7628
  }
7899
- const tasks = [];
7900
- for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
7901
- if (!entry.isDirectory()) continue;
7902
- const meta = readMeta2((0, import_node_path11.join)(dir, entry.name));
7903
- if (meta) tasks.push(meta);
7629
+ (0, import_node_fs6.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7630
+ logger.info(`migration: replaced fetch.py with NO_MATCH placeholder for ${name}`);
7631
+ } catch (err2) {
7632
+ logger.warn(`migration: failed to replace fetch.py for ${name}: ${err2?.message}`);
7633
+ }
7634
+ }
7635
+ function removeFile(filePath, ruleName, filename, logger) {
7636
+ if (!(0, import_node_fs6.existsSync)(filePath)) return;
7637
+ try {
7638
+ (0, import_node_fs6.rmSync)(filePath);
7639
+ logger.info(`migration: removed ${filename} for light rule: ${ruleName}`);
7640
+ } catch (err2) {
7641
+ logger.warn(`migration: failed to remove ${filename} for ${ruleName}: ${err2?.message}`);
7642
+ }
7643
+ }
7644
+
7645
+ // src/plugin/auto-update.ts
7646
+ init_env();
7647
+
7648
+ // src/update/channel.ts
7649
+ function resolveUpdateChannel(params) {
7650
+ if (params.configuredChannel) {
7651
+ return params.configuredChannel;
7652
+ }
7653
+ if (params.currentVersion.includes("-")) {
7654
+ return "beta";
7655
+ }
7656
+ return params.envName === "development" ? "beta" : "latest";
7657
+ }
7658
+
7659
+ // src/update/index.ts
7660
+ var import_node_path10 = require("path");
7661
+
7662
+ // src/update/checker.ts
7663
+ function parseSemver(v) {
7664
+ const [main, pre = null] = v.split("-", 2);
7665
+ const [major = 0, minor = 0, patch = 0] = main.split(".").map(Number);
7666
+ return { major, minor, patch, pre: pre ?? null };
7667
+ }
7668
+ function comparePreRelease(a, b) {
7669
+ const aParts = a.split(".");
7670
+ const bParts = b.split(".");
7671
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
7672
+ const ap = aParts[i] ?? "";
7673
+ const bp = bParts[i] ?? "";
7674
+ const an = Number(ap);
7675
+ const bn = Number(bp);
7676
+ let diff;
7677
+ if (!Number.isNaN(an) && !Number.isNaN(bn)) {
7678
+ diff = an - bn;
7679
+ } else if (ap < bp) {
7680
+ diff = -1;
7681
+ } else if (ap > bp) {
7682
+ diff = 1;
7683
+ } else {
7684
+ diff = 0;
7904
7685
  }
7905
- output({ ok: true, tasks });
7906
- });
7907
- monitor.command("show <name>").description("\u67E5\u770B\u76D1\u63A7\u4EFB\u52A1\u8BE6\u60C5").action((name) => {
7908
- const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
7909
- const meta = readMeta2(taskDir);
7910
- if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
7911
- const checkpointPath2 = (0, import_node_path11.join)(taskDir, "checkpoint.json");
7912
- let checkpoint = {};
7913
- if ((0, import_node_fs12.existsSync)(checkpointPath2)) {
7914
- try {
7915
- checkpoint = JSON.parse((0, import_node_fs12.readFileSync)(checkpointPath2, "utf-8"));
7916
- } catch {
7686
+ if (diff !== 0) return diff;
7687
+ }
7688
+ return 0;
7689
+ }
7690
+ function isNewerVersion(candidate, current) {
7691
+ const a = parseSemver(candidate);
7692
+ const b = parseSemver(current);
7693
+ if (a.major !== b.major) return a.major > b.major;
7694
+ if (a.minor !== b.minor) return a.minor > b.minor;
7695
+ if (a.patch !== b.patch) return a.patch > b.patch;
7696
+ if (a.pre === null) return b.pre !== null;
7697
+ if (b.pre === null) return false;
7698
+ return comparePreRelease(a.pre, b.pre) > 0;
7699
+ }
7700
+ var CDN_BASE_URL = "https://artifact.yoooclaw.com/plugin";
7701
+ var NPM_BASE_URL = "https://registry.npmjs.org/@yoooclaw/phone-notifications";
7702
+ var FETCH_TIMEOUT_MS = 5e3;
7703
+ var UpdateChecker = class {
7704
+ constructor(logger, onUpdateFound, intervalMs, channel) {
7705
+ this.logger = logger;
7706
+ this.onUpdateFound = onUpdateFound;
7707
+ this.intervalMs = intervalMs;
7708
+ this.channel = channel;
7709
+ }
7710
+ timer = null;
7711
+ notifiedVersion = null;
7712
+ start() {
7713
+ this.timer = setTimeout(() => {
7714
+ void this.check();
7715
+ this.timer = setInterval(() => void this.check(), this.intervalMs);
7716
+ }, 6e4);
7717
+ }
7718
+ stop() {
7719
+ if (this.timer) {
7720
+ clearInterval(this.timer);
7721
+ this.timer = null;
7722
+ }
7723
+ }
7724
+ async check() {
7725
+ const latest = await this.fetchLatestVersion();
7726
+ if (!latest || latest === PLUGIN_VERSION) return;
7727
+ if (latest === this.notifiedVersion) return;
7728
+ if (PLUGIN_VERSION.includes("-") && this.channel !== "beta") return;
7729
+ if (!isNewerVersion(latest, PLUGIN_VERSION)) return;
7730
+ this.notifiedVersion = latest;
7731
+ this.logger.info(`\u53D1\u73B0\u65B0\u7248\u672C: ${PLUGIN_VERSION} \u2192 ${latest}`);
7732
+ this.onUpdateFound({ current: PLUGIN_VERSION, latest });
7733
+ }
7734
+ async fetchLatestVersion() {
7735
+ const tag = this.channel === "beta" ? "beta" : "latest";
7736
+ try {
7737
+ const res = await fetch(`${CDN_BASE_URL}/${tag}`, {
7738
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
7739
+ });
7740
+ if (res.ok) {
7741
+ const text = (await res.text()).trim();
7742
+ if (text) return text;
7917
7743
  }
7744
+ } catch {
7918
7745
  }
7919
- output({
7920
- ok: true,
7921
- ...meta,
7922
- matchScript: `tasks/${name}/fetch.py`,
7923
- readme: `tasks/${name}/README.md`,
7924
- checkpoint
7925
- });
7926
- });
7927
- monitor.command("create <name>").description("\u521B\u5EFA\u76D1\u63A7\u4EFB\u52A1").requiredOption("--description <text>", "\u4EFB\u52A1\u63CF\u8FF0").requiredOption("--match-rules <json>", "\u5339\u914D\u89C4\u5219 JSON").requiredOption("--schedule <cron>", "cron \u8868\u8FBE\u5F0F").action(
7928
- (name, opts) => {
7929
- const dir = tasksDir2(ctx);
7930
- const taskDir = (0, import_node_path11.join)(dir, name);
7931
- if ((0, import_node_fs12.existsSync)(taskDir)) {
7932
- exitError("ALREADY_EXISTS", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u5DF2\u5B58\u5728`);
7746
+ try {
7747
+ const res = await fetch(`${NPM_BASE_URL}/${tag}`, {
7748
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
7749
+ });
7750
+ if (res.ok) {
7751
+ const data = await res.json();
7752
+ return data.version ?? null;
7933
7753
  }
7934
- let matchRules;
7754
+ } catch {
7755
+ this.logger.info("\u7248\u672C\u68C0\u67E5\u5931\u8D25\uFF08CDN + npm \u5747\u4E0D\u53EF\u8FBE\uFF09\uFF0C\u8DF3\u8FC7");
7756
+ }
7757
+ return null;
7758
+ }
7759
+ };
7760
+
7761
+ // src/update/executor.ts
7762
+ var import_node_fs10 = require("fs");
7763
+ var import_node_path9 = require("path");
7764
+ var import_node_os = require("os");
7765
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
7766
+ var BASE_URL = "https://artifact.yoooclaw.com/plugin";
7767
+ async function executeUpdate(version, runCommand, logger, targetDir, updateConfigRecord2) {
7768
+ if (!VERSION_PATTERN.test(version)) {
7769
+ return { success: false, message: `\u975E\u6CD5\u7248\u672C\u53F7: ${version}` };
7770
+ }
7771
+ const tgzUrl = `${BASE_URL}/v${version}/yoooclaw-phone-notifications-${version}.tgz`;
7772
+ logger.info(`\u6267\u884C\u66F4\u65B0: ${tgzUrl} \u2192 ${targetDir}`);
7773
+ const workDir = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
7774
+ const tgzPath = (0, import_node_path9.join)(workDir, "plugin.tgz");
7775
+ const stagingDir = (0, import_node_path9.join)(workDir, "staged");
7776
+ let backupDir = null;
7777
+ try {
7778
+ logger.info("\u4E0B\u8F7D\u63D2\u4EF6\u5305...");
7779
+ const response = await fetch(tgzUrl, { signal: AbortSignal.timeout(6e4) });
7780
+ if (!response.ok) {
7781
+ return { success: false, message: `\u4E0B\u8F7D\u5931\u8D25 (HTTP ${response.status}): ${tgzUrl}` };
7782
+ }
7783
+ const buffer = Buffer.from(await response.arrayBuffer());
7784
+ (0, import_node_fs10.writeFileSync)(tgzPath, buffer);
7785
+ logger.info(`\u4E0B\u8F7D\u5B8C\u6210 (${buffer.length} bytes)`);
7786
+ (0, import_node_fs10.mkdirSync)(stagingDir, { recursive: true });
7787
+ const tarResult = await runCommand(
7788
+ ["tar", "-xzf", tgzPath, "-C", stagingDir, "--strip-components=1"],
7789
+ { timeoutMs: 3e4 }
7790
+ );
7791
+ if (tarResult.code !== 0) {
7792
+ const err2 = tarResult.stderr || tarResult.stdout || "unknown error";
7793
+ return { success: false, message: `\u89E3\u538B\u5931\u8D25: ${err2}` };
7794
+ }
7795
+ (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(targetDir), { recursive: true });
7796
+ try {
7797
+ backupDir = `${targetDir}.bak.${Date.now()}`;
7798
+ (0, import_node_fs10.renameSync)(targetDir, backupDir);
7799
+ } catch {
7800
+ backupDir = null;
7801
+ }
7802
+ (0, import_node_fs10.renameSync)(stagingDir, targetDir);
7803
+ try {
7804
+ await updateConfigRecord2(version, tgzUrl);
7805
+ } catch (err2) {
7806
+ logger.warn(`\u914D\u7F6E\u8BB0\u5F55\u66F4\u65B0\u5931\u8D25\uFF08\u63D2\u4EF6\u6587\u4EF6\u5DF2\u5C31\u4F4D\uFF09: ${String(err2)}`);
7807
+ }
7808
+ if (backupDir) {
7935
7809
  try {
7936
- matchRules = JSON.parse(opts.matchRules);
7810
+ (0, import_node_fs10.rmSync)(backupDir, { force: true, recursive: true });
7937
7811
  } catch {
7938
- exitError(
7939
- "VALIDATION_FAILED",
7940
- "match-rules \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON"
7941
- );
7942
7812
  }
7943
- (0, import_node_fs12.mkdirSync)(taskDir, { recursive: true });
7944
- const meta = {
7945
- name,
7946
- description: opts.description,
7947
- matchRules,
7948
- cronSchedule: opts.schedule,
7949
- enabled: true,
7950
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
7951
- };
7952
- writeMeta2(taskDir, meta);
7953
- (0, import_node_fs12.writeFileSync)(
7954
- (0, import_node_path11.join)(taskDir, "fetch.py"),
7955
- generateFetchPy(name, matchRules),
7956
- "utf-8"
7957
- );
7958
- (0, import_node_fs12.writeFileSync)(
7959
- (0, import_node_path11.join)(taskDir, "README.md"),
7960
- generateReadme(name, opts.description),
7961
- "utf-8"
7962
- );
7963
- output({
7964
- ok: true,
7965
- name,
7966
- created: {
7967
- script: `tasks/${name}/fetch.py`,
7968
- readme: `tasks/${name}/README.md`,
7969
- cronJob: `notif-${name}`
7970
- },
7971
- cronHint: {
7972
- action: "add",
7973
- job: {
7974
- name: `notif-${name}`,
7975
- schedule: opts.schedule,
7976
- sessionTarget: "isolated",
7977
- message: `\u624B\u673A\u901A\u77E5\u5DF2\u7531\u72EC\u7ACB\u670D\u52A1\u5B9E\u65F6\u6355\u83B7\u5230 notifications/ \u76EE\u5F55\u7684 JSON \u6587\u4EF6\u4E2D\u3002
7978
- \u6267\u884C\uFF1Apython3 tasks/${name}/fetch.py --notifications-dir notifications
7979
- - NO_CHANGE \u6216 NO_MATCH \u2192 \u4E0D\u56DE\u590D\uFF0C\u76F4\u63A5\u7ED3\u675F\u3002
7980
- - \u6709\u8F93\u51FA \u2192 \u8BFB tasks/${name}/README.md \u4E86\u89E3\u5982\u4F55\u5904\u7406\u6570\u636E\u5E76\u901A\u77E5\u7528\u6237\u3002`
7981
- }
7982
- }
7983
- });
7984
7813
  }
7985
- );
7986
- monitor.command("delete <name>").description("\u5220\u9664\u76D1\u63A7\u4EFB\u52A1").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").action((name, opts) => {
7987
- const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
7988
- if (!(0, import_node_fs12.existsSync)(taskDir)) {
7989
- exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
7814
+ const msg = `\u5DF2\u66F4\u65B0\u5230 ${version}\uFF0C\u8BF7\u91CD\u542F gateway \u751F\u6548`;
7815
+ logger.info(msg);
7816
+ return { success: true, message: msg };
7817
+ } catch (err2) {
7818
+ if (backupDir) {
7819
+ try {
7820
+ (0, import_node_fs10.rmSync)(targetDir, { force: true, recursive: true });
7821
+ (0, import_node_fs10.renameSync)(backupDir, targetDir);
7822
+ logger.info("\u5DF2\u56DE\u6EDA\u5230\u4E4B\u524D\u7248\u672C");
7823
+ } catch (rollbackErr) {
7824
+ logger.error(`\u56DE\u6EDA\u5931\u8D25: ${String(rollbackErr)}`);
7825
+ }
7990
7826
  }
7991
- if (!opts.yes) {
7992
- output({
7993
- ok: false,
7994
- error: {
7995
- code: "CONFIRMATION_REQUIRED",
7996
- message: `\u786E\u8BA4\u5220\u9664\u76D1\u63A7\u4EFB\u52A1 '${name}'\uFF1F\u6DFB\u52A0 --yes \u8DF3\u8FC7\u786E\u8BA4`
7997
- }
7998
- });
7999
- process.exit(1);
7827
+ const errMsg = `\u66F4\u65B0\u6267\u884C\u5F02\u5E38: ${String(err2)}`;
7828
+ logger.error(errMsg);
7829
+ return { success: false, message: errMsg };
7830
+ } finally {
7831
+ try {
7832
+ (0, import_node_fs10.rmSync)(workDir, { force: true, recursive: true });
7833
+ } catch {
8000
7834
  }
8001
- (0, import_node_fs12.rmSync)(taskDir, { recursive: true, force: true });
8002
- output({
8003
- ok: true,
8004
- name,
8005
- deleted: true,
8006
- cronHint: {
8007
- action: "remove",
8008
- name: `notif-${name}`
8009
- }
8010
- });
8011
- });
8012
- monitor.command("enable <name>").description("\u542F\u7528\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8013
- const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8014
- const meta = readMeta2(taskDir);
8015
- if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8016
- meta.enabled = true;
8017
- writeMeta2(taskDir, meta);
8018
- output({ ok: true, name, enabled: true });
8019
- });
8020
- monitor.command("disable <name>").description("\u6682\u505C\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8021
- const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8022
- const meta = readMeta2(taskDir);
8023
- if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8024
- meta.enabled = false;
8025
- writeMeta2(taskDir, meta);
8026
- output({ ok: true, name, enabled: false });
8027
- });
7835
+ }
8028
7836
  }
8029
7837
 
8030
- // src/cli/light-send.ts
8031
- init_credentials();
8032
-
8033
- // src/light/sender.ts
8034
- var import_node_crypto2 = require("crypto");
8035
-
8036
- // src/light/protocol.ts
8037
- var MAX_LIGHT_SEGMENTS = 12;
8038
- var PROTOCOL_DIGITS = [
8039
- "\x80",
8040
- "\x81",
8041
- "\x82",
8042
- "\x83",
8043
- "\x84",
8044
- "\x91",
8045
- "\x92",
8046
- "\x93",
8047
- "\x94",
8048
- "\x95",
8049
- "\x96",
8050
- "\x97"
8051
- ];
8052
- var LED_SEPARATOR_ONCE = "\x9A";
8053
- var LED_SEPARATOR_LOOP = "\x9B";
8054
- var DURATION_STEPS_S = [0.5, 1, 2, 3, 5, 6, 8, 16, 24, 32, 48];
8055
- var INTERVAL_STEPS_MS = [50, 100, 200, 300, 500, 600, 800, 1600, 2400, 3200, 4800];
8056
- var BREATH_STEPS_MS = [1040, 1560, 2080, 2600, 3100, 4160];
8057
- var BRIGHTNESS_STEPS = [32, 64, 96, 128, 192, 255];
8058
- var COLOR_STEPS = [0, 32, 64, 128, 192, 255];
8059
- var BACKGROUND_BRIGHTNESS_STEPS = [0, 32, 64, 96, 128, 192, 255];
8060
- var MULTI_CHANNEL_COLOR_COEFFICIENTS = { r: 1, g: 0.25, b: 0.25 };
8061
- var PURE_WHITE_COLOR_COEFFICIENTS = { r: 1, g: 0.35, b: 0.35 };
8062
- var MODE_TO_INDEX = {
8063
- wave: 0,
8064
- breath: 1,
8065
- strobe: 2,
8066
- steady: 3,
8067
- wave_rainbow: 4,
8068
- pixel_frame: 5
8069
- };
8070
- function buildLightEffectApnsBody(segments, repeatInput) {
8071
- assertSegmentCount(segments);
8072
- assertSegmentsValid(segments);
8073
- const repeatTimes = normalizeRepeatTimes(repeatInput);
8074
- assertAncsRepeatTimes(repeatTimes);
8075
- const visibleText = summarizeSegments(segments);
8076
- const separator = repeatTimes === 0 ? LED_SEPARATOR_LOOP : LED_SEPARATOR_ONCE;
8077
- const payload = segments.map((segment) => encodeSegment(segment)).join("");
8078
- return `${visibleText}${separator}${payload}`;
8079
- }
8080
- function assertSegmentCount(segments) {
8081
- if (segments.length < 1 || segments.length > MAX_LIGHT_SEGMENTS) {
8082
- throw new Error(`light_control supports 1-${MAX_LIGHT_SEGMENTS} segments`);
7838
+ // src/update/index.ts
7839
+ var PLUGIN_ID = "phone-notifications";
7840
+ function resolveTargetDir(api) {
7841
+ try {
7842
+ const cfg = api.runtime.config?.loadConfig?.();
7843
+ const installPath = cfg?.plugins?.installs?.[PLUGIN_ID]?.installPath;
7844
+ if (installPath) return installPath;
7845
+ } catch {
8083
7846
  }
7847
+ return (0, import_node_path10.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
8084
7848
  }
8085
- function summarizeSegments(segments) {
8086
- const modeDesc = segments.map((segment) => segment.mode).join("+");
8087
- return `Effect: ${modeDesc} (${segments.length} segment${segments.length > 1 ? "s" : ""})`;
7849
+ async function updateConfigRecord(api, version, targetDir, tgzUrl) {
7850
+ const configApi = api.runtime.config;
7851
+ if (!configApi) return;
7852
+ const cfg = configApi.loadConfig();
7853
+ if (!cfg.plugins) cfg.plugins = {};
7854
+ if (!cfg.plugins.installs) cfg.plugins.installs = {};
7855
+ cfg.plugins.installs[PLUGIN_ID] = {
7856
+ ...cfg.plugins.installs[PLUGIN_ID],
7857
+ source: "archive",
7858
+ sourcePath: tgzUrl,
7859
+ installPath: targetDir,
7860
+ version,
7861
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
7862
+ };
7863
+ await configApi.writeConfigFile(cfg);
8088
7864
  }
8089
- function assertSegmentsValid(segments) {
8090
- const validation = validateSegments(segments);
8091
- if (!validation.valid) {
8092
- throw new Error(
8093
- validation.errors.map((error) => `${error.field}: ${error.message}`).join("; ")
8094
- );
7865
+ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast, externalUpdateNotifier) {
7866
+ if (config.enabled === false) {
7867
+ logger.info("\u81EA\u52A8\u66F4\u65B0\u5DF2\u7981\u7528 (autoUpdate.enabled = false)");
7868
+ return { start() {
7869
+ }, stop() {
7870
+ }, notifyBroadcastReady() {
7871
+ } };
8095
7872
  }
8096
- }
8097
- function encodeSegment(segment) {
8098
- const common = [
8099
- MODE_TO_INDEX[segment.mode],
8100
- quantizeDuration(segment.duration_s)
8101
- ];
8102
- let values;
8103
- switch (segment.mode) {
8104
- case "wave":
8105
- case "wave_rainbow":
8106
- const color = normalizeProtocolColor(segment.color);
8107
- const background = normalizeProtocolColor(segment.background);
8108
- values = [
8109
- ...common,
8110
- quantize(segment.interval_ms ?? 200, INTERVAL_STEPS_MS),
8111
- quantizeBrightnessValue(segment.brightness ?? 0),
8112
- quantize(color.r, COLOR_STEPS),
8113
- quantize(color.g, COLOR_STEPS),
8114
- quantize(color.b, COLOR_STEPS),
8115
- segment.direction === "rtl" ? 1 : 0,
8116
- quantizeWindow(segment.window ?? 2),
8117
- quantize(background.r, COLOR_STEPS),
8118
- quantize(background.g, COLOR_STEPS),
8119
- quantize(background.b, COLOR_STEPS),
8120
- quantize(segment.background?.brightness ?? 0, BACKGROUND_BRIGHTNESS_STEPS)
8121
- ];
8122
- break;
8123
- case "breath":
8124
- const breathColor = normalizeProtocolColor(segment.color);
8125
- values = [
8126
- ...common,
8127
- quantizeBreathRiseFall(segment.breath_timing?.rise_ms),
8128
- quantizeBreathHoldOff(segment.breath_timing?.hold_ms),
8129
- quantizeBreathRiseFall(segment.breath_timing?.fall_ms),
8130
- quantizeBreathHoldOff(segment.breath_timing?.off_ms),
8131
- quantizeBrightnessValue(segment.brightness ?? 0),
8132
- quantize(breathColor.r, COLOR_STEPS),
8133
- quantize(breathColor.g, COLOR_STEPS),
8134
- quantize(breathColor.b, COLOR_STEPS)
8135
- ];
8136
- break;
8137
- case "strobe":
8138
- const strobeColor = normalizeProtocolColor(segment.color);
8139
- values = [
8140
- ...common,
8141
- quantize(segment.interval_ms ?? 200, INTERVAL_STEPS_MS),
8142
- quantizeBrightnessValue(segment.brightness ?? 0),
8143
- quantize(strobeColor.r, COLOR_STEPS),
8144
- quantize(strobeColor.g, COLOR_STEPS),
8145
- quantize(strobeColor.b, COLOR_STEPS)
8146
- ];
8147
- break;
8148
- case "steady":
8149
- const steadyColor = normalizeProtocolColor(segment.color);
8150
- values = [
8151
- ...common,
8152
- quantizeBrightnessValue(segment.brightness ?? 0),
8153
- quantize(steadyColor.r, COLOR_STEPS),
8154
- quantize(steadyColor.g, COLOR_STEPS),
8155
- quantize(steadyColor.b, COLOR_STEPS)
8156
- ];
8157
- break;
8158
- case "pixel_frame":
8159
- values = encodePixelFrameValues(common, segment);
8160
- break;
7873
+ let pendingUpdate = null;
7874
+ let pendingGatewayUpdate = null;
7875
+ function tryBroadcastUpdate(update) {
7876
+ const broadcast = getBroadcast();
7877
+ if (!broadcast) return false;
7878
+ broadcast("plugin.updateAvailable", {
7879
+ pluginId: "phone-notifications",
7880
+ current: update.current,
7881
+ latest: update.latest
7882
+ });
7883
+ pendingGatewayUpdate = null;
7884
+ return true;
8161
7885
  }
8162
- return values.map((value) => PROTOCOL_DIGITS[value]).join("");
8163
- }
8164
- function encodePixelFrameValues(common, segment) {
8165
- const pixels = segment.pixels ?? [];
8166
- return [
8167
- ...common,
8168
- pixels.length - 1,
8169
- ...pixels.flatMap((pixel) => {
8170
- const color = normalizeProtocolColor(pixel.color);
8171
- return [
8172
- pixel.index,
8173
- quantize(color.r, COLOR_STEPS),
8174
- quantize(color.g, COLOR_STEPS),
8175
- quantize(color.b, COLOR_STEPS),
8176
- quantizeBrightnessValue(pixel.brightness)
8177
- ];
8178
- })
8179
- ];
7886
+ api.on("before_prompt_build", () => {
7887
+ if (!pendingUpdate) return;
7888
+ return {
7889
+ appendSystemContext: `[\u7CFB\u7EDF\u901A\u77E5] phone-notifications \u63D2\u4EF6\u6709\u65B0\u7248\u672C ${pendingUpdate.latest} \u53EF\u7528\uFF08\u5F53\u524D\u7248\u672C ${pendingUpdate.current}\uFF09\u3002
7890
+ \u8BF7\u7528\u81EA\u7136\u8BED\u8A00\u5728\u5408\u9002\u65F6\u673A\u544A\u77E5\u7528\u6237\uFF0C\u82E5\u7528\u6237\u540C\u610F\u66F4\u65B0\uFF0C\u8C03\u7528 plugin-update tool \u5E76\u4F20\u5165 version \u53C2\u6570\u3002\u82E5\u7528\u6237\u62D2\u7EDD\uFF0C\u4E0D\u518D\u91CD\u590D\u63D0\u9192\u3002`
7891
+ };
7892
+ });
7893
+ api.registerTool({
7894
+ name: "plugin-update",
7895
+ label: "Plugin Update",
7896
+ description: "\u5C06 phone-notifications \u63D2\u4EF6\u66F4\u65B0\u5230\u6307\u5B9A\u7248\u672C\u3002\u4EC5\u5728\u7528\u6237\u660E\u786E\u540C\u610F\u66F4\u65B0\u540E\u8C03\u7528\u3002\u66F4\u65B0\u5B8C\u6210\u540E\u9700\u91CD\u542F gateway \u751F\u6548\u3002",
7897
+ parameters: {
7898
+ type: "object",
7899
+ required: ["version"],
7900
+ additionalProperties: false,
7901
+ properties: {
7902
+ version: {
7903
+ type: "string",
7904
+ description: "\u76EE\u6807\u7248\u672C\u53F7\uFF0C\u5982 1.11.0"
7905
+ }
7906
+ }
7907
+ },
7908
+ async execute(_toolCallId, params) {
7909
+ const { version } = params;
7910
+ const targetDir = resolveTargetDir(api);
7911
+ const result = await executeUpdate(
7912
+ version,
7913
+ api.runtime.system.runCommandWithTimeout,
7914
+ logger,
7915
+ targetDir,
7916
+ (v, url) => updateConfigRecord(api, v, targetDir, url)
7917
+ );
7918
+ if (result.success) {
7919
+ pendingUpdate = null;
7920
+ externalUpdateNotifier?.clearPendingUpdate();
7921
+ }
7922
+ return {
7923
+ content: [{ type: "text", text: result.message }],
7924
+ details: result
7925
+ };
7926
+ }
7927
+ });
7928
+ api.registerGatewayMethod("plugin.update", async ({ params, respond, context }) => {
7929
+ rememberBroadcast?.(context?.broadcast);
7930
+ const version = params.version;
7931
+ if (!version) {
7932
+ respond(false, void 0, {
7933
+ code: "MISSING_VERSION",
7934
+ message: "version is required"
7935
+ });
7936
+ return;
7937
+ }
7938
+ const targetDir = resolveTargetDir(api);
7939
+ const result = await executeUpdate(
7940
+ version,
7941
+ api.runtime.system.runCommandWithTimeout,
7942
+ logger,
7943
+ targetDir,
7944
+ (v, url) => updateConfigRecord(api, v, targetDir, url)
7945
+ );
7946
+ if (result.success) {
7947
+ pendingUpdate = null;
7948
+ externalUpdateNotifier?.clearPendingUpdate();
7949
+ }
7950
+ if (result.success) {
7951
+ respond(true, { message: result.message });
7952
+ } else {
7953
+ respond(false, void 0, {
7954
+ code: "UPDATE_FAILED",
7955
+ message: result.message
7956
+ });
7957
+ }
7958
+ });
7959
+ const intervalMs = (config.checkIntervalHours ?? 4) * 36e5;
7960
+ const channel = config.channel ?? "latest";
7961
+ logger.info(
7962
+ `\u81EA\u52A8\u66F4\u65B0\u5DF2\u542F\u7528: channel=${channel}, checkIntervalHours=${config.checkIntervalHours ?? 4}`
7963
+ );
7964
+ const checker = new UpdateChecker(
7965
+ logger,
7966
+ (update) => {
7967
+ pendingUpdate = update;
7968
+ const broadcasted = tryBroadcastUpdate(update);
7969
+ if (!broadcasted) pendingGatewayUpdate = update;
7970
+ if (!broadcasted) {
7971
+ externalUpdateNotifier?.notifyUpdateAvailable(update);
7972
+ }
7973
+ logger.info(
7974
+ `\u5DF2\u901A\u77E5\u66F4\u65B0 ${update.current} \u2192 ${update.latest}` + (broadcasted ? "\uFF08\u5BF9\u8BDD + \u7F51\u5173\uFF09" : "\uFF08\u5BF9\u8BDD\u901A\u9053\u5DF2\u751F\u6548\uFF0C\u7B49\u5F85\u4E0B\u6B21 gateway \u8BF7\u6C42\u65F6\u8865\u53D1\u5BA2\u6237\u7AEF\u4E8B\u4EF6\uFF09")
7975
+ );
7976
+ },
7977
+ intervalMs,
7978
+ channel
7979
+ );
7980
+ return {
7981
+ start: () => checker.start(),
7982
+ stop: () => checker.stop(),
7983
+ notifyBroadcastReady() {
7984
+ const update = pendingGatewayUpdate;
7985
+ if (!update) return;
7986
+ if (!tryBroadcastUpdate(update)) return;
7987
+ logger.info(`\u5DF2\u8865\u53D1\u63D2\u4EF6\u66F4\u65B0\u4E8B\u4EF6 ${update.current} \u2192 ${update.latest}`);
7988
+ }
7989
+ };
8180
7990
  }
8181
- function normalizeProtocolColor(color) {
8182
- const normalized = {
8183
- r: color?.r ?? 0,
8184
- g: color?.g ?? 0,
8185
- b: color?.b ?? 0
7991
+
7992
+ // src/plugin/auto-update.ts
7993
+ function registerAutoUpdateLifecycle(deps) {
7994
+ const {
7995
+ api,
7996
+ config,
7997
+ logger,
7998
+ getBroadcastFn,
7999
+ cacheBroadcast,
8000
+ tunnelService
8001
+ } = deps;
8002
+ const autoUpdateConfig = {
8003
+ ...config.autoUpdate,
8004
+ channel: resolveUpdateChannel({
8005
+ configuredChannel: config.autoUpdate?.channel,
8006
+ currentVersion: PLUGIN_VERSION,
8007
+ envName: loadEnvName()
8008
+ })
8186
8009
  };
8187
- if (isPureWhiteProtocolColor(normalized)) {
8188
- return applyProtocolColorCoefficients(normalized, PURE_WHITE_COLOR_COEFFICIENTS);
8189
- }
8190
- if (countActiveProtocolColorChannels(normalized) <= 1) {
8191
- return normalized;
8010
+ const autoUpdateLifecycle = registerAutoUpdate(
8011
+ api,
8012
+ logger,
8013
+ autoUpdateConfig,
8014
+ getBroadcastFn,
8015
+ cacheBroadcast,
8016
+ tunnelService ? {
8017
+ notifyUpdateAvailable: (update) => tunnelService.notifyUpdateAvailable(update),
8018
+ clearPendingUpdate: () => tunnelService.clearPendingUpdate()
8019
+ } : void 0
8020
+ );
8021
+ api.registerService({
8022
+ id: "update-checker",
8023
+ start() {
8024
+ autoUpdateLifecycle.start();
8025
+ },
8026
+ stop() {
8027
+ autoUpdateLifecycle.stop();
8028
+ }
8029
+ });
8030
+ logger.info("\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\u670D\u52A1\u5DF2\u6CE8\u518C");
8031
+ return autoUpdateLifecycle;
8032
+ }
8033
+
8034
+ // src/plugin/cli.ts
8035
+ var import_node_path18 = require("path");
8036
+
8037
+ // src/cli/auth.ts
8038
+ var import_node_fs11 = require("fs");
8039
+ init_credentials();
8040
+ function registerAuthCli(program) {
8041
+ const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
8042
+ auth.command("set-api-key <apiKey>").description("\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u6301\u4E45\u5316\u5230\u672C\u5730\u914D\u7F6E\uFF09").action((apiKey) => {
8043
+ writeCredentials({ ...readCredentials(), apiKey, token: void 0 });
8044
+ output({
8045
+ ok: true,
8046
+ apiKey: apiKey.slice(0, 8) + "\u2026",
8047
+ storedAt: credentialsPath()
8048
+ });
8049
+ });
8050
+ auth.command("set-token <token>").description("\uFF08\u517C\u5BB9\uFF09\u8BBE\u7F6E\u7528\u6237 API Key\uFF08\u65E7\u547D\u4EE4\u540D\uFF09").action((token) => {
8051
+ writeCredentials({ ...readCredentials(), apiKey: token, token: void 0 });
8052
+ output({
8053
+ ok: true,
8054
+ apiKey: token.slice(0, 8) + "\u2026",
8055
+ storedAt: credentialsPath()
8056
+ });
8057
+ });
8058
+ auth.command("show").description("\u67E5\u770B\u5F53\u524D\u8BA4\u8BC1\u72B6\u6001").action(() => {
8059
+ const creds = readCredentials();
8060
+ const apiKey = creds.apiKey ?? creds.token;
8061
+ if (apiKey) {
8062
+ output({
8063
+ ok: true,
8064
+ hasApiKey: true,
8065
+ apiKey: apiKey.slice(0, 8) + "\u2026",
8066
+ storedAt: credentialsPath()
8067
+ });
8068
+ } else {
8069
+ output({ ok: true, hasApiKey: false });
8070
+ }
8071
+ });
8072
+ auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
8073
+ const path2 = credentialsPath();
8074
+ if ((0, import_node_fs11.existsSync)(path2)) {
8075
+ const creds = readCredentials();
8076
+ delete creds.apiKey;
8077
+ delete creds.token;
8078
+ if (Object.keys(creds).length === 0) {
8079
+ (0, import_node_fs11.rmSync)(path2, { force: true });
8080
+ } else {
8081
+ writeCredentials(creds);
8082
+ }
8083
+ }
8084
+ output({ ok: true, cleared: true });
8085
+ });
8086
+ }
8087
+
8088
+ // src/cli/ntf-search.ts
8089
+ function filterItem(item, opts) {
8090
+ if (opts.app && item.appName !== opts.app) return false;
8091
+ if (opts.sender && !item.title.includes(opts.sender)) return false;
8092
+ if (opts.keyword && !item.title.includes(opts.keyword) && !item.content.includes(opts.keyword)) {
8093
+ return false;
8192
8094
  }
8193
- return applyProtocolColorCoefficients(normalized, MULTI_CHANNEL_COLOR_COEFFICIENTS);
8095
+ return true;
8194
8096
  }
8195
- function countActiveProtocolColorChannels(color) {
8196
- return Number(color.r > 0) + Number(color.g > 0) + Number(color.b > 0);
8097
+ function registerNtfSearch(ntf, ctx) {
8098
+ ntf.command("search").description("\u67E5\u8BE2\u901A\u77E5\uFF08\u6309\u65F6\u95F4/\u5E94\u7528/\u53D1\u9001\u4EBA/\u5173\u952E\u8BCD\u7B5B\u9009\uFF09").option("--from <time>", "\u5F00\u59CB\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00").option("--to <time>", "\u7ED3\u675F\u65F6\u95F4 ISO 8601\uFF0C\u4F8B\u5982 2026-03-01T18:00:00+08:00").option("--app <name>", "\u6309\u5E94\u7528\u540D\u8FC7\u6EE4").option("--sender <name>", "\u6309\u53D1\u9001\u4EBA\u8FC7\u6EE4").option("--keyword <text>", "\u5728\u6807\u9898\u548C\u5185\u5BB9\u4E2D\u641C\u7D22\u5173\u952E\u8BCD").option("--limit <n>", "\u6700\u5927\u8FD4\u56DE\u6761\u6570", "100").action(
8099
+ async (opts) => {
8100
+ const dir = resolveNotificationsDir(ctx);
8101
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8102
+ progress("\u6B63\u5728\u641C\u7D22\u901A\u77E5...");
8103
+ const limit = parseInt(opts.limit, 10) || 100;
8104
+ if (limit <= 0) {
8105
+ exitError("INVALID_LIMIT", "--limit \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570");
8106
+ }
8107
+ const hasFrom = typeof opts.from === "string" && opts.from.length > 0;
8108
+ const hasTo = typeof opts.to === "string" && opts.to.length > 0;
8109
+ const fromTs = hasFrom ? parseIsoTime(opts.from, "--from") : null;
8110
+ const toTs = hasTo ? parseIsoTime(opts.to, "--to") : null;
8111
+ if (fromTs !== null && toTs !== null && fromTs > toTs) {
8112
+ exitError("INVALID_TIME_RANGE", "--from \u4E0D\u80FD\u665A\u4E8E --to");
8113
+ }
8114
+ const keys = await listDateKeysAsync(dir);
8115
+ const results = [];
8116
+ if (fromTs !== null || toTs !== null) {
8117
+ const fromDateKey = hasFrom ? opts.from.slice(0, 10) : null;
8118
+ const toDateKey = hasTo ? opts.to.slice(0, 10) : null;
8119
+ for (const dateKey of keys) {
8120
+ if (fromDateKey && dateKey < fromDateKey) continue;
8121
+ if (toDateKey && dateKey > toDateKey) continue;
8122
+ const items = await readDateFileAsync(dir, dateKey);
8123
+ for (const item of items) {
8124
+ if (!filterItem(item, opts)) continue;
8125
+ const itemTs = Date.parse(item.timestamp);
8126
+ if (Number.isNaN(itemTs)) continue;
8127
+ if (fromTs !== null && itemTs < fromTs) continue;
8128
+ if (toTs !== null && itemTs > toTs) continue;
8129
+ results.push(item);
8130
+ }
8131
+ }
8132
+ } else {
8133
+ for (const dateKey of keys) {
8134
+ const items = await readDateFileAsync(dir, dateKey);
8135
+ for (const item of items) {
8136
+ if (!filterItem(item, opts)) continue;
8137
+ if (Number.isNaN(Date.parse(item.timestamp))) continue;
8138
+ results.push(item);
8139
+ }
8140
+ }
8141
+ }
8142
+ const notifications = sortNotificationsByTimestampDesc(results).slice(
8143
+ 0,
8144
+ limit
8145
+ );
8146
+ output({
8147
+ ok: true,
8148
+ total: notifications.length,
8149
+ notifications
8150
+ });
8151
+ }
8152
+ );
8197
8153
  }
8198
- function isPureWhiteProtocolColor(color) {
8199
- return color.r === 255 && color.g === 255 && color.b === 255;
8154
+
8155
+ // src/cli/ntf-stats.ts
8156
+ function registerNtfStats(ntf, ctx) {
8157
+ ntf.command("stats").description("\u901A\u77E5\u7EDF\u8BA1\u5206\u6790\uFF08\u6309\u65E5\u671F/\u5E94\u7528/\u53D1\u9001\u4EBA/\u65F6\u6BB5\u805A\u5408\uFF09").option("--from <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD", daysAgo(7)).option("--to <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD", today()).option("--app <name>", "\u53EA\u7EDF\u8BA1\u6307\u5B9A\u5E94\u7528").option("--dim <dimension>", "\u7EDF\u8BA1\u7EF4\u5EA6\uFF1Adate/app/sender/hour/all", "all").action(
8158
+ (opts) => {
8159
+ const dir = resolveNotificationsDir(ctx);
8160
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8161
+ const dim = opts.dim;
8162
+ const keys = filterDateRange(listDateKeys(dir), opts.from, opts.to);
8163
+ const byDate = {};
8164
+ const byApp = {};
8165
+ const bySender = {};
8166
+ const byHour = {};
8167
+ let total = 0;
8168
+ for (const dateKey of keys) {
8169
+ const items = readDateFile(dir, dateKey);
8170
+ let dateCount = 0;
8171
+ for (const item of items) {
8172
+ if (opts.app && item.appName !== opts.app) continue;
8173
+ total++;
8174
+ dateCount++;
8175
+ byApp[item.appName] = (byApp[item.appName] || 0) + 1;
8176
+ if (item.title) {
8177
+ bySender[item.title] = (bySender[item.title] || 0) + 1;
8178
+ }
8179
+ const hourMatch = /T(\d{2}):/.exec(item.timestamp);
8180
+ if (hourMatch) {
8181
+ const h = hourMatch[1];
8182
+ byHour[h] = (byHour[h] || 0) + 1;
8183
+ }
8184
+ }
8185
+ byDate[dateKey] = dateCount;
8186
+ }
8187
+ const result = {
8188
+ ok: true,
8189
+ range: { from: opts.from, to: opts.to },
8190
+ total
8191
+ };
8192
+ if (dim === "date" || dim === "all") {
8193
+ result.byDate = byDate;
8194
+ }
8195
+ if (dim === "app" || dim === "all") {
8196
+ result.byApp = byApp;
8197
+ }
8198
+ if (dim === "sender" || dim === "all") {
8199
+ const sorted = Object.entries(bySender).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([sender, count]) => ({ sender, count }));
8200
+ result.bySender = sorted;
8201
+ }
8202
+ if (dim === "hour" || dim === "all") {
8203
+ result.byHour = byHour;
8204
+ }
8205
+ output(result);
8206
+ }
8207
+ );
8200
8208
  }
8201
- function applyProtocolColorCoefficients(color, coefficients) {
8202
- return {
8203
- r: scaleProtocolColorChannel(color.r, coefficients.r),
8204
- g: scaleProtocolColorChannel(color.g, coefficients.g),
8205
- b: scaleProtocolColorChannel(color.b, coefficients.b)
8206
- };
8209
+
8210
+ // src/cli/ntf-sync.ts
8211
+ var import_node_fs12 = require("fs");
8212
+ var import_node_path11 = require("path");
8213
+ var SYNC_FETCH_LIMIT = 300;
8214
+ function checkpointPath(dir) {
8215
+ return (0, import_node_path11.join)(dir, ".checkpoint.json");
8207
8216
  }
8208
- function scaleProtocolColorChannel(value, coefficient) {
8209
- return Math.max(0, Math.min(255, Math.round(value * coefficient)));
8217
+ function readCheckpoint(dir) {
8218
+ const p = checkpointPath(dir);
8219
+ if (!(0, import_node_fs12.existsSync)(p)) return {};
8220
+ try {
8221
+ return JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
8222
+ } catch {
8223
+ return {};
8224
+ }
8210
8225
  }
8211
- function quantize(value, steps) {
8212
- let bestIndex = 0;
8213
- let bestDistance = Number.POSITIVE_INFINITY;
8214
- for (const [index, step] of steps.entries()) {
8215
- const distance = Math.abs(value - step);
8216
- if (distance < bestDistance) {
8217
- bestIndex = index;
8218
- bestDistance = distance;
8226
+ function writeCheckpoint(dir, data) {
8227
+ (0, import_node_fs12.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8228
+ }
8229
+ function registerNtfSync(ntf, ctx) {
8230
+ const sync = ntf.command("sync").description("\u540C\u6B65\u901A\u77E5\u5230\u8BB0\u5FC6\u7CFB\u7EDF");
8231
+ sync.command("scan").description("\u626B\u63CF\u672A\u5904\u7406\u7684\u901A\u77E5\uFF0C\u8FD4\u56DE\u5F85\u540C\u6B65\u6458\u8981").action(() => {
8232
+ const dir = resolveNotificationsDir(ctx);
8233
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8234
+ const checkpoint = readCheckpoint(dir);
8235
+ const keys = listDateKeys(dir);
8236
+ const pending = [];
8237
+ let totalPending = 0;
8238
+ for (const dateKey of keys) {
8239
+ const items = readDateFile(dir, dateKey);
8240
+ const lastIndex = checkpoint[dateKey]?.lastIndex ?? -1;
8241
+ const unprocessed = items.length - (lastIndex + 1);
8242
+ if (unprocessed > 0) {
8243
+ pending.push({
8244
+ date: dateKey,
8245
+ count: unprocessed,
8246
+ startIndex: lastIndex + 1
8247
+ });
8248
+ totalPending += unprocessed;
8249
+ }
8250
+ }
8251
+ output({ ok: true, pending, totalPending });
8252
+ });
8253
+ sync.command("fetch").description("\u83B7\u53D6\u6307\u5B9A\u65E5\u671F\u7684\u672A\u5904\u7406\u901A\u77E5\u8BE6\u60C5").requiredOption("--date <date>", "\u76EE\u6807\u65E5\u671F YYYY-MM-DD").action((opts) => {
8254
+ const dir = resolveNotificationsDir(ctx);
8255
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8256
+ const items = readDateFile(dir, opts.date);
8257
+ if (items.length === 0) {
8258
+ exitError("NO_DATA", `\u65E5\u671F ${opts.date} \u65E0\u901A\u77E5\u6570\u636E`);
8259
+ }
8260
+ const checkpoint = readCheckpoint(dir);
8261
+ const lastIndex = checkpoint[opts.date]?.lastIndex ?? -1;
8262
+ const startIndex = lastIndex + 1;
8263
+ const unprocessed = items.slice(startIndex);
8264
+ const notifications = unprocessed.slice(0, SYNC_FETCH_LIMIT);
8265
+ const endIndex = notifications.length > 0 ? startIndex + notifications.length - 1 : lastIndex;
8266
+ const hasMore = unprocessed.length > notifications.length;
8267
+ if (unprocessed.length === 0) {
8268
+ output({
8269
+ ok: true,
8270
+ date: opts.date,
8271
+ startIndex,
8272
+ endIndex,
8273
+ nextStartIndex: null,
8274
+ limit: SYNC_FETCH_LIMIT,
8275
+ returned: 0,
8276
+ totalUnprocessed: 0,
8277
+ hasMore: false,
8278
+ notifications: []
8279
+ });
8280
+ return;
8219
8281
  }
8220
- }
8221
- return bestIndex;
8282
+ output({
8283
+ ok: true,
8284
+ date: opts.date,
8285
+ startIndex,
8286
+ endIndex,
8287
+ nextStartIndex: hasMore ? endIndex + 1 : null,
8288
+ limit: SYNC_FETCH_LIMIT,
8289
+ returned: notifications.length,
8290
+ totalUnprocessed: unprocessed.length,
8291
+ hasMore,
8292
+ notifications
8293
+ });
8294
+ });
8295
+ sync.command("commit").description("\u6807\u8BB0\u6307\u5B9A\u65E5\u671F\u5F53\u524D\u6279\u6B21\u5904\u7406\u5B8C\u6210\uFF0C\u66F4\u65B0 checkpoint").requiredOption("--date <date>", "\u76EE\u6807\u65E5\u671F YYYY-MM-DD").action((opts) => {
8296
+ const dir = resolveNotificationsDir(ctx);
8297
+ if (!dir) exitError("STORAGE_UNAVAILABLE", "\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528");
8298
+ const items = readDateFile(dir, opts.date);
8299
+ if (items.length === 0) {
8300
+ exitError("NO_DATA", `\u65E5\u671F ${opts.date} \u65E0\u901A\u77E5\u6570\u636E`);
8301
+ }
8302
+ const checkpoint = readCheckpoint(dir);
8303
+ const lastIndex = checkpoint[opts.date]?.lastIndex ?? -1;
8304
+ const committedIndex = Math.min(items.length - 1, lastIndex + SYNC_FETCH_LIMIT);
8305
+ const hasMore = committedIndex < items.length - 1;
8306
+ checkpoint[opts.date] = { lastIndex: committedIndex };
8307
+ writeCheckpoint(dir, checkpoint);
8308
+ output({
8309
+ ok: true,
8310
+ date: opts.date,
8311
+ committedIndex,
8312
+ limit: SYNC_FETCH_LIMIT,
8313
+ hasMore,
8314
+ nextStartIndex: hasMore ? committedIndex + 1 : null
8315
+ });
8316
+ });
8222
8317
  }
8223
- function quantizeDuration(duration_s) {
8224
- if (duration_s === 0) return 11;
8225
- return quantize(duration_s, DURATION_STEPS_S);
8318
+
8319
+ // src/cli/ntf-monitor.ts
8320
+ var import_node_fs13 = require("fs");
8321
+ var import_node_path12 = require("path");
8322
+ function tasksDir2(ctx) {
8323
+ const base = ctx.workspaceDir || ctx.stateDir;
8324
+ if (!base) throw new Error("workspaceDir and stateDir both unavailable");
8325
+ return (0, import_node_path12.join)(base, "tasks");
8226
8326
  }
8227
- function quantizeBrightnessValue(brightness) {
8228
- if (brightness === 0) return 11;
8229
- return quantize(brightness, BRIGHTNESS_STEPS);
8327
+ function readMeta2(taskDir) {
8328
+ const metaPath = (0, import_node_path12.join)(taskDir, "meta.json");
8329
+ if (!(0, import_node_fs13.existsSync)(metaPath)) return null;
8330
+ try {
8331
+ return JSON.parse((0, import_node_fs13.readFileSync)(metaPath, "utf-8"));
8332
+ } catch {
8333
+ return null;
8334
+ }
8230
8335
  }
8231
- function quantizeBreathRiseFall(value) {
8232
- return quantize(value ?? 1040, BREATH_STEPS_MS);
8336
+ function writeMeta2(taskDir, meta) {
8337
+ (0, import_node_fs13.writeFileSync)((0, import_node_path12.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
8233
8338
  }
8234
- function quantizeBreathHoldOff(value) {
8235
- if (value === 0) {
8236
- return 5;
8237
- }
8238
- return quantize(value ?? 1040, BREATH_STEPS_MS.slice(0, 5));
8339
+ function generateReadme(name, description) {
8340
+ return `# Monitor Task: ${name}
8341
+
8342
+ ## \u63CF\u8FF0
8343
+ ${description}
8344
+
8345
+ ## \u5904\u7406\u6307\u5357
8346
+ \u5F53 fetch.py \u8F93\u51FA\u5339\u914D\u7684\u901A\u77E5\u65F6\uFF0C\u8BF7\uFF1A
8347
+ 1. \u9605\u8BFB\u5339\u914D\u5230\u7684\u901A\u77E5\u5185\u5BB9
8348
+ 2. \u6839\u636E\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\u662F\u5426\u9700\u8981\u901A\u77E5\u7528\u6237
8349
+ 3. \u5982\u9700\u901A\u77E5\u7528\u6237\uFF0C\u4F7F\u7528 message \u5DE5\u5177\u53D1\u9001\u6458\u8981
8350
+ `;
8239
8351
  }
8240
- function quantizeWindow(value) {
8241
- return value - 1;
8352
+ function registerNtfMonitor(ntf, ctx) {
8353
+ const monitor = ntf.command("monitor").description("\u901A\u77E5\u76D1\u63A7\u4EFB\u52A1\u7BA1\u7406");
8354
+ monitor.command("list").description("\u5217\u51FA\u6240\u6709\u76D1\u63A7\u4EFB\u52A1").action(() => {
8355
+ const dir = tasksDir2(ctx);
8356
+ if (!(0, import_node_fs13.existsSync)(dir)) {
8357
+ output({ ok: true, tasks: [] });
8358
+ return;
8359
+ }
8360
+ const tasks = [];
8361
+ for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
8362
+ if (!entry.isDirectory()) continue;
8363
+ const meta = readMeta2((0, import_node_path12.join)(dir, entry.name));
8364
+ if (meta) tasks.push(meta);
8365
+ }
8366
+ output({ ok: true, tasks });
8367
+ });
8368
+ monitor.command("show <name>").description("\u67E5\u770B\u76D1\u63A7\u4EFB\u52A1\u8BE6\u60C5").action((name) => {
8369
+ const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8370
+ const meta = readMeta2(taskDir);
8371
+ if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8372
+ const checkpointPath2 = (0, import_node_path12.join)(taskDir, "checkpoint.json");
8373
+ let checkpoint = {};
8374
+ if ((0, import_node_fs13.existsSync)(checkpointPath2)) {
8375
+ try {
8376
+ checkpoint = JSON.parse((0, import_node_fs13.readFileSync)(checkpointPath2, "utf-8"));
8377
+ } catch {
8378
+ }
8379
+ }
8380
+ output({
8381
+ ok: true,
8382
+ ...meta,
8383
+ matchScript: `tasks/${name}/fetch.py`,
8384
+ readme: `tasks/${name}/README.md`,
8385
+ checkpoint
8386
+ });
8387
+ });
8388
+ monitor.command("create <name>").description("\u521B\u5EFA\u76D1\u63A7\u4EFB\u52A1").requiredOption("--description <text>", "\u4EFB\u52A1\u63CF\u8FF0").requiredOption("--match-rules <json>", "\u5339\u914D\u89C4\u5219 JSON").requiredOption("--schedule <cron>", "cron \u8868\u8FBE\u5F0F").action(
8389
+ (name, opts) => {
8390
+ const dir = tasksDir2(ctx);
8391
+ const taskDir = (0, import_node_path12.join)(dir, name);
8392
+ if ((0, import_node_fs13.existsSync)(taskDir)) {
8393
+ exitError("ALREADY_EXISTS", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u5DF2\u5B58\u5728`);
8394
+ }
8395
+ let matchRules;
8396
+ try {
8397
+ matchRules = JSON.parse(opts.matchRules);
8398
+ } catch {
8399
+ exitError(
8400
+ "VALIDATION_FAILED",
8401
+ "match-rules \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON"
8402
+ );
8403
+ }
8404
+ (0, import_node_fs13.mkdirSync)(taskDir, { recursive: true });
8405
+ const meta = {
8406
+ name,
8407
+ description: opts.description,
8408
+ matchRules,
8409
+ cronSchedule: opts.schedule,
8410
+ enabled: true,
8411
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
8412
+ };
8413
+ writeMeta2(taskDir, meta);
8414
+ (0, import_node_fs13.writeFileSync)(
8415
+ (0, import_node_path12.join)(taskDir, "fetch.py"),
8416
+ generateFetchPy(name, matchRules),
8417
+ "utf-8"
8418
+ );
8419
+ (0, import_node_fs13.writeFileSync)(
8420
+ (0, import_node_path12.join)(taskDir, "README.md"),
8421
+ generateReadme(name, opts.description),
8422
+ "utf-8"
8423
+ );
8424
+ output({
8425
+ ok: true,
8426
+ name,
8427
+ created: {
8428
+ script: `tasks/${name}/fetch.py`,
8429
+ readme: `tasks/${name}/README.md`,
8430
+ cronJob: `notif-${name}`
8431
+ },
8432
+ cronHint: {
8433
+ action: "add",
8434
+ job: {
8435
+ name: `notif-${name}`,
8436
+ schedule: opts.schedule,
8437
+ sessionTarget: "isolated",
8438
+ message: `\u624B\u673A\u901A\u77E5\u5DF2\u7531\u72EC\u7ACB\u670D\u52A1\u5B9E\u65F6\u6355\u83B7\u5230 notifications/ \u76EE\u5F55\u7684 JSON \u6587\u4EF6\u4E2D\u3002
8439
+ \u6267\u884C\uFF1Apython3 tasks/${name}/fetch.py --notifications-dir notifications
8440
+ - NO_CHANGE \u6216 NO_MATCH \u2192 \u4E0D\u56DE\u590D\uFF0C\u76F4\u63A5\u7ED3\u675F\u3002
8441
+ - \u6709\u8F93\u51FA \u2192 \u8BFB tasks/${name}/README.md \u4E86\u89E3\u5982\u4F55\u5904\u7406\u6570\u636E\u5E76\u901A\u77E5\u7528\u6237\u3002`
8442
+ }
8443
+ }
8444
+ });
8445
+ }
8446
+ );
8447
+ monitor.command("delete <name>").description("\u5220\u9664\u76D1\u63A7\u4EFB\u52A1").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").action((name, opts) => {
8448
+ const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8449
+ if (!(0, import_node_fs13.existsSync)(taskDir)) {
8450
+ exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8451
+ }
8452
+ if (!opts.yes) {
8453
+ output({
8454
+ ok: false,
8455
+ error: {
8456
+ code: "CONFIRMATION_REQUIRED",
8457
+ message: `\u786E\u8BA4\u5220\u9664\u76D1\u63A7\u4EFB\u52A1 '${name}'\uFF1F\u6DFB\u52A0 --yes \u8DF3\u8FC7\u786E\u8BA4`
8458
+ }
8459
+ });
8460
+ process.exit(1);
8461
+ }
8462
+ (0, import_node_fs13.rmSync)(taskDir, { recursive: true, force: true });
8463
+ output({
8464
+ ok: true,
8465
+ name,
8466
+ deleted: true,
8467
+ cronHint: {
8468
+ action: "remove",
8469
+ name: `notif-${name}`
8470
+ }
8471
+ });
8472
+ });
8473
+ monitor.command("enable <name>").description("\u542F\u7528\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8474
+ const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8475
+ const meta = readMeta2(taskDir);
8476
+ if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8477
+ meta.enabled = true;
8478
+ writeMeta2(taskDir, meta);
8479
+ output({ ok: true, name, enabled: true });
8480
+ });
8481
+ monitor.command("disable <name>").description("\u6682\u505C\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8482
+ const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8483
+ const meta = readMeta2(taskDir);
8484
+ if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8485
+ meta.enabled = false;
8486
+ writeMeta2(taskDir, meta);
8487
+ output({ ok: true, name, enabled: false });
8488
+ });
8242
8489
  }
8243
8490
 
8491
+ // src/cli/light-send.ts
8492
+ init_credentials();
8493
+
8244
8494
  // src/light/sender.ts
8495
+ var import_node_crypto2 = require("crypto");
8245
8496
  init_env();
8246
8497
  async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
8247
8498
  const apiUrl = getEnvUrls().lightApiUrl;
@@ -8327,9 +8578,9 @@ function registerLightSend(light) {
8327
8578
  }
8328
8579
 
8329
8580
  // src/cli/light-setup-tools.ts
8330
- var import_node_fs13 = require("fs");
8581
+ var import_node_fs14 = require("fs");
8331
8582
  var import_node_os2 = require("os");
8332
- var import_node_path12 = require("path");
8583
+ var import_node_path13 = require("path");
8333
8584
  function isObject(value) {
8334
8585
  return !!value && typeof value === "object" && !Array.isArray(value);
8335
8586
  }
@@ -8343,13 +8594,25 @@ function ensureArray(obj, key) {
8343
8594
  function resolveConfigPath2() {
8344
8595
  const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
8345
8596
  if (fromEnv) return fromEnv;
8346
- return (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8347
- }
8597
+ return (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8598
+ }
8599
+ var LIGHT_TOOLS = [
8600
+ "light_control",
8601
+ "lightrules.list",
8602
+ "lightrules.create",
8603
+ "lightrules.update",
8604
+ "lightrules.delete"
8605
+ ];
8348
8606
  function upsertLightControlAlsoAllow(cfg) {
8349
8607
  if (!isObject(cfg.tools)) cfg.tools = {};
8350
8608
  const toolsAlsoAllow = ensureArray(cfg.tools, "alsoAllow");
8351
- const hasGlobal = toolsAlsoAllow.includes("light_control");
8352
- if (!hasGlobal) toolsAlsoAllow.push("light_control");
8609
+ let globalChanged = false;
8610
+ for (const tool of LIGHT_TOOLS) {
8611
+ if (!toolsAlsoAllow.includes(tool)) {
8612
+ toolsAlsoAllow.push(tool);
8613
+ globalChanged = true;
8614
+ }
8615
+ }
8353
8616
  if (!isObject(cfg.agents)) cfg.agents = {};
8354
8617
  const agents = cfg.agents;
8355
8618
  const list = ensureArray(agents, "list");
@@ -8362,33 +8625,35 @@ function upsertLightControlAlsoAllow(cfg) {
8362
8625
  }
8363
8626
  if (!isObject(mainAgent.tools)) mainAgent.tools = {};
8364
8627
  const mainAlsoAllow = ensureArray(mainAgent.tools, "alsoAllow");
8365
- const hasMain = mainAlsoAllow.includes("light_control");
8366
- if (!hasMain) mainAlsoAllow.push("light_control");
8367
- return {
8368
- globalChanged: !hasGlobal,
8369
- mainAgentChanged: !hasMain
8370
- };
8628
+ let mainAgentChanged = false;
8629
+ for (const tool of LIGHT_TOOLS) {
8630
+ if (!mainAlsoAllow.includes(tool)) {
8631
+ mainAlsoAllow.push(tool);
8632
+ mainAgentChanged = true;
8633
+ }
8634
+ }
8635
+ return { globalChanged, mainAgentChanged };
8371
8636
  }
8372
8637
  function registerLightSetupTools(light) {
8373
8638
  light.command("setup").description("\u81EA\u52A8\u653E\u884C light_control\uFF08\u5199\u5165 tools.alsoAllow \u4E0E agents.main.tools.alsoAllow\uFF09").action(() => {
8374
8639
  const configPath = resolveConfigPath2();
8375
- if (!(0, import_node_fs13.existsSync)(configPath)) {
8640
+ if (!(0, import_node_fs14.existsSync)(configPath)) {
8376
8641
  exitError("CONFIG_NOT_FOUND", `\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6: ${configPath}`);
8377
8642
  }
8378
8643
  let cfg = {};
8379
8644
  try {
8380
- const raw = (0, import_node_fs13.readFileSync)(configPath, "utf-8");
8645
+ const raw = (0, import_node_fs14.readFileSync)(configPath, "utf-8");
8381
8646
  const parsed = JSON.parse(raw);
8382
8647
  if (isObject(parsed)) cfg = parsed;
8383
- } catch (err) {
8384
- exitError("CONFIG_INVALID", `\u8BFB\u53D6/\u89E3\u6790\u914D\u7F6E\u5931\u8D25: ${err?.message ?? String(err)}`);
8648
+ } catch (err2) {
8649
+ exitError("CONFIG_INVALID", `\u8BFB\u53D6/\u89E3\u6790\u914D\u7F6E\u5931\u8D25: ${err2?.message ?? String(err2)}`);
8385
8650
  }
8386
8651
  const result = upsertLightControlAlsoAllow(cfg);
8387
8652
  try {
8388
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
8389
- (0, import_node_fs13.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8390
- } catch (err) {
8391
- exitError("WRITE_FAILED", `\u5199\u5165\u914D\u7F6E\u5931\u8D25: ${err?.message ?? String(err)}`);
8653
+ (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
8654
+ (0, import_node_fs14.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8655
+ } catch (err2) {
8656
+ exitError("WRITE_FAILED", `\u5199\u5165\u914D\u7F6E\u5931\u8D25: ${err2?.message ?? String(err2)}`);
8392
8657
  }
8393
8658
  output({
8394
8659
  ok: true,
@@ -8404,17 +8669,17 @@ function registerLightSetupTools(light) {
8404
8669
  }
8405
8670
 
8406
8671
  // src/cli/tunnel-status.ts
8407
- var import_node_fs14 = require("fs");
8408
- var import_node_path13 = require("path");
8672
+ var import_node_fs15 = require("fs");
8673
+ var import_node_path14 = require("path");
8409
8674
  init_credentials();
8410
8675
  init_env();
8411
- var STATUS_REL_PATH = (0, import_node_path13.join)("plugins", "phone-notifications", "tunnel-status.json");
8676
+ var STATUS_REL_PATH = (0, import_node_path14.join)("plugins", "phone-notifications", "tunnel-status.json");
8412
8677
  function readTunnelStatus(ctx) {
8413
8678
  if (!ctx.stateDir) return null;
8414
- const filePath = (0, import_node_path13.join)(ctx.stateDir, STATUS_REL_PATH);
8415
- if (!(0, import_node_fs14.existsSync)(filePath)) return null;
8679
+ const filePath = (0, import_node_path14.join)(ctx.stateDir, STATUS_REL_PATH);
8680
+ if (!(0, import_node_fs15.existsSync)(filePath)) return null;
8416
8681
  try {
8417
- return JSON.parse((0, import_node_fs14.readFileSync)(filePath, "utf-8"));
8682
+ return JSON.parse((0, import_node_fs15.readFileSync)(filePath, "utf-8"));
8418
8683
  } catch {
8419
8684
  return null;
8420
8685
  }
@@ -8456,9 +8721,9 @@ function registerTunnelStatus(ntf, ctx) {
8456
8721
  "\u672A\u627E\u5230\u96A7\u9053\u72B6\u6001\u6587\u4EF6\uFF0C\u96A7\u9053\u670D\u52A1\u53EF\u80FD\u5C1A\u672A\u542F\u52A8\u8FC7\u3002\u8BF7\u786E\u8BA4 openclaw \u4E3B\u8FDB\u7A0B\u6B63\u5728\u8FD0\u884C\u3002"
8457
8722
  );
8458
8723
  }
8459
- const ok = status.state === "connected";
8724
+ const ok2 = status.state === "connected";
8460
8725
  output({
8461
- ok,
8726
+ ok: ok2,
8462
8727
  tunnelUrl,
8463
8728
  connection: {
8464
8729
  state: status.state,
@@ -8468,7 +8733,7 @@ function registerTunnelStatus(ntf, ctx) {
8468
8733
  },
8469
8734
  message: formatMessage(status)
8470
8735
  });
8471
- if (!ok) process.exit(1);
8736
+ if (!ok2) process.exit(1);
8472
8737
  });
8473
8738
  }
8474
8739
 
@@ -8482,24 +8747,24 @@ function registerNtfStoragePath(ntf, ctx) {
8482
8747
  }
8483
8748
 
8484
8749
  // src/cli/log-search.ts
8485
- var import_node_fs15 = require("fs");
8486
- var import_node_path14 = require("path");
8750
+ var import_node_fs16 = require("fs");
8751
+ var import_node_path15 = require("path");
8487
8752
  function resolveLogsDir(ctx) {
8488
8753
  if (ctx.stateDir) {
8489
- const dir = (0, import_node_path14.join)(
8754
+ const dir = (0, import_node_path15.join)(
8490
8755
  ctx.stateDir,
8491
8756
  "plugins",
8492
8757
  "phone-notifications",
8493
8758
  "logs"
8494
8759
  );
8495
- if ((0, import_node_fs15.existsSync)(dir)) return dir;
8760
+ if ((0, import_node_fs16.existsSync)(dir)) return dir;
8496
8761
  }
8497
8762
  return null;
8498
8763
  }
8499
8764
  function listLogDateKeys(dir) {
8500
8765
  const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
8501
8766
  const keys = [];
8502
- for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
8767
+ for (const entry of (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true })) {
8503
8768
  if (!entry.isFile()) continue;
8504
8769
  const m = pattern.exec(entry.name);
8505
8770
  if (m) keys.push(m[1]);
@@ -8507,9 +8772,9 @@ function listLogDateKeys(dir) {
8507
8772
  return keys.sort().reverse();
8508
8773
  }
8509
8774
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
8510
- const filePath = (0, import_node_path14.join)(dir, `${dateKey}.log`);
8511
- if (!(0, import_node_fs15.existsSync)(filePath)) return;
8512
- const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
8775
+ const filePath = (0, import_node_path15.join)(dir, `${dateKey}.log`);
8776
+ if (!(0, import_node_fs16.existsSync)(filePath)) return;
8777
+ const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
8513
8778
  const lowerKeyword = keyword?.toLowerCase();
8514
8779
  for (const line of content.split("\n")) {
8515
8780
  if (collected.length >= limit) return;
@@ -8576,12 +8841,12 @@ function registerEnvCli(ntf) {
8576
8841
  }
8577
8842
 
8578
8843
  // src/cli/doctor.ts
8579
- var import_node_fs19 = require("fs");
8844
+ var import_node_fs20 = require("fs");
8580
8845
  var import_node_readline = require("readline");
8581
8846
  init_host();
8582
8847
 
8583
8848
  // src/cli/doctor/check-dangerous-flags.ts
8584
- var import_node_fs16 = require("fs");
8849
+ var import_node_fs17 = require("fs");
8585
8850
  function isObject2(v) {
8586
8851
  return !!v && typeof v === "object" && !Array.isArray(v);
8587
8852
  }
@@ -8598,13 +8863,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
8598
8863
  detail: "\u8FD9\u4F1A\u5173\u95ED Control UI \u7684\u8BBE\u5907\u8EAB\u4EFD\u9A8C\u8BC1\uFF0C\u4EFB\u4F55\u4EBA\u90FD\u53EF\u4EE5\u8BBF\u95EE\u63A7\u5236\u9762\u677F\u3002",
8599
8864
  fixDescription: "\u8BBE\u4E3A false",
8600
8865
  fix: () => {
8601
- const raw = (0, import_node_fs16.readFileSync)(configPath, "utf-8");
8866
+ const raw = (0, import_node_fs17.readFileSync)(configPath, "utf-8");
8602
8867
  const config = JSON.parse(raw);
8603
8868
  const gw = config.gateway;
8604
8869
  const cui = gw.controlUi;
8605
8870
  cui.dangerouslyDisableDeviceAuth = false;
8606
- (0, import_node_fs16.copyFileSync)(configPath, configPath + ".bak");
8607
- (0, import_node_fs16.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
8871
+ (0, import_node_fs17.copyFileSync)(configPath, configPath + ".bak");
8872
+ (0, import_node_fs17.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
8608
8873
  }
8609
8874
  };
8610
8875
  };
@@ -8652,11 +8917,11 @@ function warnEmpty() {
8652
8917
  }
8653
8918
 
8654
8919
  // src/cli/doctor/check-state-dir-perms.ts
8655
- var import_node_fs17 = require("fs");
8920
+ var import_node_fs18 = require("fs");
8656
8921
  var checkStateDirPerms = ({ stateDir }) => {
8657
8922
  let mode;
8658
8923
  try {
8659
- mode = (0, import_node_fs17.statSync)(stateDir).mode;
8924
+ mode = (0, import_node_fs18.statSync)(stateDir).mode;
8660
8925
  } catch {
8661
8926
  return null;
8662
8927
  }
@@ -8670,7 +8935,7 @@ var checkStateDirPerms = ({ stateDir }) => {
8670
8935
  detail: "\u5176\u4ED6\u7528\u6237\u53EF\u4EE5\u8BFB\u53D6\u8BE5\u76EE\u5F55\u4E0B\u7684\u51ED\u8BC1\u548C\u914D\u7F6E\u6587\u4EF6\u3002",
8671
8936
  fixDescription: "chmod 700 " + stateDir,
8672
8937
  fix: () => {
8673
- (0, import_node_fs17.chmodSync)(stateDir, 448);
8938
+ (0, import_node_fs18.chmodSync)(stateDir, 448);
8674
8939
  }
8675
8940
  };
8676
8941
  };
@@ -8725,16 +8990,16 @@ var checkCredentials = () => {
8725
8990
  };
8726
8991
 
8727
8992
  // src/cli/doctor/check-tunnel.ts
8728
- var import_node_fs18 = require("fs");
8729
- var import_node_path15 = require("path");
8730
- var STATUS_REL_PATH2 = (0, import_node_path15.join)(
8993
+ var import_node_fs19 = require("fs");
8994
+ var import_node_path16 = require("path");
8995
+ var STATUS_REL_PATH2 = (0, import_node_path16.join)(
8731
8996
  "plugins",
8732
8997
  "phone-notifications",
8733
8998
  "tunnel-status.json"
8734
8999
  );
8735
9000
  var checkTunnel = ({ stateDir }) => {
8736
- const filePath = (0, import_node_path15.join)(stateDir, STATUS_REL_PATH2);
8737
- if (!(0, import_node_fs18.existsSync)(filePath)) {
9001
+ const filePath = (0, import_node_path16.join)(stateDir, STATUS_REL_PATH2);
9002
+ if (!(0, import_node_fs19.existsSync)(filePath)) {
8738
9003
  return {
8739
9004
  id: "tunnel",
8740
9005
  severity: "warn",
@@ -8746,7 +9011,7 @@ var checkTunnel = ({ stateDir }) => {
8746
9011
  }
8747
9012
  let status;
8748
9013
  try {
8749
- status = JSON.parse((0, import_node_fs18.readFileSync)(filePath, "utf-8"));
9014
+ status = JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
8750
9015
  } catch {
8751
9016
  return {
8752
9017
  id: "tunnel",
@@ -8839,9 +9104,9 @@ function isObject5(v) {
8839
9104
  return !!v && typeof v === "object" && !Array.isArray(v);
8840
9105
  }
8841
9106
  function readConfig(configPath) {
8842
- if (!(0, import_node_fs19.existsSync)(configPath)) return {};
9107
+ if (!(0, import_node_fs20.existsSync)(configPath)) return {};
8843
9108
  try {
8844
- const parsed = JSON.parse((0, import_node_fs19.readFileSync)(configPath, "utf-8"));
9109
+ const parsed = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
8845
9110
  return isObject5(parsed) ? parsed : {};
8846
9111
  } catch {
8847
9112
  return {};
@@ -8950,9 +9215,9 @@ async function runDoctor(ctx, json, fix) {
8950
9215
  await issue.fix();
8951
9216
  log(`\x1B[32m\u2705\x1B[0m ${issue.title} \u2192 ${issue.fixDescription}`);
8952
9217
  fixed++;
8953
- } catch (err) {
9218
+ } catch (err2) {
8954
9219
  log(
8955
- `\x1B[31m\u274C\x1B[0m ${issue.title} \u4FEE\u590D\u5931\u8D25: ${err?.message ?? String(err)}`
9220
+ `\x1B[31m\u274C\x1B[0m ${issue.title} \u4FEE\u590D\u5931\u8D25: ${err2?.message ?? String(err2)}`
8956
9221
  );
8957
9222
  }
8958
9223
  }
@@ -9042,7 +9307,7 @@ function registerRecStoragePath(rec, ctx) {
9042
9307
 
9043
9308
  // src/cli/rec-setup.ts
9044
9309
  var import_node_readline2 = require("readline");
9045
- var import_node_fs20 = require("fs");
9310
+ var import_node_fs21 = require("fs");
9046
9311
  function ask(rl, question) {
9047
9312
  return new Promise((resolve) => rl.question(question, resolve));
9048
9313
  }
@@ -9128,9 +9393,9 @@ async function setupLocal(rl) {
9128
9393
  function registerRecSetup(rec, ctx) {
9129
9394
  rec.command("setup").description("\u4EA4\u4E92\u5F0F\u914D\u7F6E ASR \u8F6C\u5199\u53C2\u6570\uFF0C\u4FDD\u5B58\u5230\u672C\u5730\u914D\u7F6E\u6587\u4EF6").action(async () => {
9130
9395
  const configPath = resolveAsrConfigPath(ctx);
9131
- if ((0, import_node_fs20.existsSync)(configPath)) {
9396
+ if ((0, import_node_fs21.existsSync)(configPath)) {
9132
9397
  try {
9133
- const existing = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
9398
+ const existing = JSON.parse((0, import_node_fs21.readFileSync)(configPath, "utf-8"));
9134
9399
  process.stderr.write(`\u5F53\u524D\u5DF2\u6709\u914D\u7F6E\uFF1Amode = ${existing.mode}`);
9135
9400
  if (existing.updatedAt) process.stderr.write(`\uFF0C\u66F4\u65B0\u4E8E ${existing.updatedAt}`);
9136
9401
  process.stderr.write("\n");
@@ -9143,7 +9408,7 @@ function registerRecSetup(rec, ctx) {
9143
9408
  const modeIdx = await askChoice(rl, "\u9009\u62E9\u6A21\u5F0F", ["api\uFF08\u4E91\u7AEF model-proxy \u957F\u5F55\u97F3\uFF09", "local\uFF08\u672C\u5730 Whisper\uFF09"]);
9144
9409
  const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
9145
9410
  const stored = { ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
9146
- (0, import_node_fs20.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9411
+ (0, import_node_fs21.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9147
9412
  process.stderr.write(`
9148
9413
  \u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 ${configPath}
9149
9414
 
@@ -9157,8 +9422,8 @@ function registerRecSetup(rec, ctx) {
9157
9422
 
9158
9423
  // src/cli/update.ts
9159
9424
  var import_node_child_process = require("child_process");
9160
- var import_node_fs21 = require("fs");
9161
- var import_node_path16 = require("path");
9425
+ var import_node_fs22 = require("fs");
9426
+ var import_node_path17 = require("path");
9162
9427
  var import_node_os3 = __toESM(require("os"), 1);
9163
9428
  init_host();
9164
9429
  var BASE_URL2 = "https://artifact.yoooclaw.com/plugin";
@@ -9182,8 +9447,8 @@ async function runUpdate(ctx, opts) {
9182
9447
  let latest;
9183
9448
  try {
9184
9449
  latest = (await fetchText(`${BASE_URL2}/${channel}`)).trim();
9185
- } catch (err) {
9186
- const msg = `\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C: ${err?.message ?? String(err)}`;
9450
+ } catch (err2) {
9451
+ const msg = `\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C: ${err2?.message ?? String(err2)}`;
9187
9452
  if (json) {
9188
9453
  output({ ok: false, error: { code: "FETCH_FAILED", message: msg } });
9189
9454
  process.exit(1);
@@ -9207,8 +9472,8 @@ async function runUpdate(ctx, opts) {
9207
9472
  let installScript;
9208
9473
  try {
9209
9474
  installScript = await fetchText(`${BASE_URL2}/install-core.mjs`);
9210
- } catch (err) {
9211
- const msg = `\u4E0B\u8F7D\u5B89\u88C5\u811A\u672C\u5931\u8D25: ${err?.message ?? String(err)}`;
9475
+ } catch (err2) {
9476
+ const msg = `\u4E0B\u8F7D\u5B89\u88C5\u811A\u672C\u5931\u8D25: ${err2?.message ?? String(err2)}`;
9212
9477
  if (json) {
9213
9478
  output({ ok: false, error: { code: "DOWNLOAD_FAILED", message: msg } });
9214
9479
  process.exit(1);
@@ -9217,11 +9482,11 @@ async function runUpdate(ctx, opts) {
9217
9482
  `);
9218
9483
  process.exit(1);
9219
9484
  }
9220
- const tmpScript = (0, import_node_path16.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9485
+ const tmpScript = (0, import_node_path17.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9221
9486
  try {
9222
- (0, import_node_fs21.writeFileSync)(tmpScript, installScript, "utf-8");
9223
- } catch (err) {
9224
- const msg = `\u5199\u5165\u4E34\u65F6\u6587\u4EF6\u5931\u8D25: ${err?.message ?? String(err)}`;
9487
+ (0, import_node_fs22.writeFileSync)(tmpScript, installScript, "utf-8");
9488
+ } catch (err2) {
9489
+ const msg = `\u5199\u5165\u4E34\u65F6\u6587\u4EF6\u5931\u8D25: ${err2?.message ?? String(err2)}`;
9225
9490
  if (json) {
9226
9491
  output({ ok: false, error: { code: "WRITE_FAILED", message: msg } });
9227
9492
  process.exit(1);
@@ -9237,7 +9502,7 @@ async function runUpdate(ctx, opts) {
9237
9502
  { stdio: "inherit" }
9238
9503
  );
9239
9504
  try {
9240
- (0, import_node_fs21.unlinkSync)(tmpScript);
9505
+ (0, import_node_fs22.unlinkSync)(tmpScript);
9241
9506
  } catch {
9242
9507
  }
9243
9508
  if (result.error) {
@@ -9301,10 +9566,10 @@ function inferOpenClawRootDir(workspaceDir) {
9301
9566
  if (!workspaceDir) {
9302
9567
  return void 0;
9303
9568
  }
9304
- if ((0, import_node_path17.basename)(workspaceDir) !== "workspace") {
9569
+ if ((0, import_node_path18.basename)(workspaceDir) !== "workspace") {
9305
9570
  return void 0;
9306
9571
  }
9307
- return (0, import_node_path17.dirname)(workspaceDir);
9572
+ return (0, import_node_path18.dirname)(workspaceDir);
9308
9573
  }
9309
9574
  function registerPluginCli(api, params) {
9310
9575
  const { logger, openclawDir } = params;
@@ -9550,12 +9815,12 @@ function registerLightControlTool(api, logger) {
9550
9815
  }
9551
9816
 
9552
9817
  // src/plugin/lifecycle.ts
9553
- var import_node_fs30 = require("fs");
9818
+ var import_node_fs31 = require("fs");
9554
9819
  init_host();
9555
9820
 
9556
9821
  // src/notification/app-name-map.ts
9557
- var import_node_fs22 = require("fs");
9558
- var import_node_path18 = require("path");
9822
+ var import_node_fs23 = require("fs");
9823
+ var import_node_path19 = require("path");
9559
9824
  init_credentials();
9560
9825
  init_env();
9561
9826
  var PLUGIN_STATE_DIR = "phone-notifications";
@@ -9576,7 +9841,7 @@ function isAppNameMapApiResponse(v) {
9576
9841
  );
9577
9842
  }
9578
9843
  function getCachePath(stateDir) {
9579
- return (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
9844
+ return (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
9580
9845
  }
9581
9846
  function createAppNameMapProvider(opts) {
9582
9847
  const { stateDir, logger } = opts;
@@ -9588,9 +9853,9 @@ function createAppNameMapProvider(opts) {
9588
9853
  let inFlightFetch = null;
9589
9854
  function loadFromDisk() {
9590
9855
  const path2 = getCachePath(stateDir);
9591
- if (!(0, import_node_fs22.existsSync)(path2)) return;
9856
+ if (!(0, import_node_fs23.existsSync)(path2)) return;
9592
9857
  try {
9593
- const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf-8"));
9858
+ const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf-8"));
9594
9859
  if (!isRecordOfStrings(raw)) return;
9595
9860
  map.clear();
9596
9861
  for (const [k, v] of Object.entries(raw)) map.set(k, v);
@@ -9634,10 +9899,10 @@ function createAppNameMapProvider(opts) {
9634
9899
  logger.warn("[app-name-map] refresh succeeded but got 0 entries");
9635
9900
  return;
9636
9901
  }
9637
- const dir = (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
9638
- (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
9902
+ const dir = (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
9903
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9639
9904
  const cachePath = getCachePath(stateDir);
9640
- (0, import_node_fs22.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
9905
+ (0, import_node_fs23.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
9641
9906
  logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
9642
9907
  } catch (e) {
9643
9908
  const message = e instanceof Error ? e.message : String(e);
@@ -9681,8 +9946,8 @@ function createAppNameMapProvider(opts) {
9681
9946
  }
9682
9947
 
9683
9948
  // src/recording/storage.ts
9684
- var import_node_fs23 = require("fs");
9685
- var import_node_path19 = require("path");
9949
+ var import_node_fs24 = require("fs");
9950
+ var import_node_path20 = require("path");
9686
9951
 
9687
9952
  // src/recording/state-machine.ts
9688
9953
  var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
@@ -9743,7 +10008,7 @@ function stripMarkdownFence(markdown) {
9743
10008
  }
9744
10009
  function deriveTitleFromTranscriptPath(transcriptFile, recordingId) {
9745
10010
  if (!transcriptFile) return void 0;
9746
- const name = (0, import_node_path19.basename)(transcriptFile, ".md");
10011
+ const name = (0, import_node_path20.basename)(transcriptFile, ".md");
9747
10012
  const prefix = `${recordingId}_`;
9748
10013
  if (name.startsWith(prefix)) {
9749
10014
  const derived = name.slice(prefix.length).trim();
@@ -9771,22 +10036,22 @@ function extractTranscriptContent(markdown) {
9771
10036
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
9772
10037
  }
9773
10038
  function resolveRecordingStorageDir(ctx, logger) {
9774
- const stateRecDir = (0, import_node_path19.join)(
10039
+ const stateRecDir = (0, import_node_path20.join)(
9775
10040
  ctx.stateDir,
9776
10041
  "plugins",
9777
10042
  "phone-notifications",
9778
10043
  RECORDINGS_DIR
9779
10044
  );
9780
10045
  try {
9781
- (0, import_node_fs23.mkdirSync)(stateRecDir, { recursive: true });
10046
+ (0, import_node_fs24.mkdirSync)(stateRecDir, { recursive: true });
9782
10047
  logger.info(`\u5F55\u97F3\u5C06\u5199\u5165 stateDir \u8DEF\u5F84: ${stateRecDir}`);
9783
10048
  return stateRecDir;
9784
10049
  } catch {
9785
10050
  }
9786
10051
  if (ctx.workspaceDir) {
9787
- const wsRecDir = (0, import_node_path19.join)(ctx.workspaceDir, RECORDINGS_DIR);
10052
+ const wsRecDir = (0, import_node_path20.join)(ctx.workspaceDir, RECORDINGS_DIR);
9788
10053
  try {
9789
- (0, import_node_fs23.mkdirSync)(wsRecDir, { recursive: true });
10054
+ (0, import_node_fs24.mkdirSync)(wsRecDir, { recursive: true });
9790
10055
  logger.warn(`stateDir \u4E0D\u53EF\u7528\uFF0C\u5F55\u97F3\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${wsRecDir}`);
9791
10056
  return wsRecDir;
9792
10057
  } catch {
@@ -9798,11 +10063,11 @@ var RecordingStorage = class {
9798
10063
  constructor(dir, logger) {
9799
10064
  this.logger = logger;
9800
10065
  this.dir = dir;
9801
- this.audioDir = (0, import_node_path19.join)(dir, AUDIO_DIR);
9802
- this.transcriptDataDir = (0, import_node_path19.join)(dir, TRANSCRIPT_DATA_DIR);
9803
- this.transcriptsDir = (0, import_node_path19.join)(dir, TRANSCRIPTS_DIR);
9804
- this.summariesDir = (0, import_node_path19.join)(dir, SUMMARIES_DIR);
9805
- this.indexPath = (0, import_node_path19.join)(dir, INDEX_FILE);
10066
+ this.audioDir = (0, import_node_path20.join)(dir, AUDIO_DIR);
10067
+ this.transcriptDataDir = (0, import_node_path20.join)(dir, TRANSCRIPT_DATA_DIR);
10068
+ this.transcriptsDir = (0, import_node_path20.join)(dir, TRANSCRIPTS_DIR);
10069
+ this.summariesDir = (0, import_node_path20.join)(dir, SUMMARIES_DIR);
10070
+ this.indexPath = (0, import_node_path20.join)(dir, INDEX_FILE);
9806
10071
  }
9807
10072
  dir;
9808
10073
  audioDir;
@@ -9813,10 +10078,10 @@ var RecordingStorage = class {
9813
10078
  index = { recordings: [] };
9814
10079
  /** 初始化目录结构并加载索引 */
9815
10080
  async init() {
9816
- (0, import_node_fs23.mkdirSync)(this.audioDir, { recursive: true });
9817
- (0, import_node_fs23.mkdirSync)(this.transcriptDataDir, { recursive: true });
9818
- (0, import_node_fs23.mkdirSync)(this.transcriptsDir, { recursive: true });
9819
- (0, import_node_fs23.mkdirSync)(this.summariesDir, { recursive: true });
10081
+ (0, import_node_fs24.mkdirSync)(this.audioDir, { recursive: true });
10082
+ (0, import_node_fs24.mkdirSync)(this.transcriptDataDir, { recursive: true });
10083
+ (0, import_node_fs24.mkdirSync)(this.transcriptsDir, { recursive: true });
10084
+ (0, import_node_fs24.mkdirSync)(this.summariesDir, { recursive: true });
9820
10085
  this.loadIndex();
9821
10086
  this.logger.info(
9822
10087
  `\u5F55\u97F3\u5B58\u50A8\u5DF2\u521D\u59CB\u5316: ${this.dir}\uFF08\u5171 ${this.index.recordings.length} \u6761\u8BB0\u5F55\uFF09`
@@ -9860,13 +10125,13 @@ var RecordingStorage = class {
9860
10125
  return id;
9861
10126
  }
9862
10127
  if (existing.transcriptDataFile) {
9863
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, existing.transcriptDataFile), { force: true });
10128
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, existing.transcriptDataFile), { force: true });
9864
10129
  }
9865
10130
  if (existing.transcriptFile) {
9866
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, existing.transcriptFile), { force: true });
10131
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, existing.transcriptFile), { force: true });
9867
10132
  }
9868
10133
  if (existing.summaryFile) {
9869
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, existing.summaryFile), { force: true });
10134
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, existing.summaryFile), { force: true });
9870
10135
  }
9871
10136
  existing.metadata = metadata;
9872
10137
  existing.status = "syncing_openclaw";
@@ -9934,7 +10199,7 @@ var RecordingStorage = class {
9934
10199
  if (!entry) return;
9935
10200
  const nextTranscriptDataFile = `${TRANSCRIPT_DATA_DIR}/${filename}`;
9936
10201
  if (entry.transcriptDataFile && entry.transcriptDataFile !== nextTranscriptDataFile) {
9937
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.transcriptDataFile), { force: true });
10202
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, entry.transcriptDataFile), { force: true });
9938
10203
  }
9939
10204
  entry.transcriptDataFile = nextTranscriptDataFile;
9940
10205
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9948,7 +10213,7 @@ var RecordingStorage = class {
9948
10213
  if (!entry) return;
9949
10214
  const nextTranscriptFile = `${TRANSCRIPTS_DIR}/${filename}`;
9950
10215
  if (entry.transcriptFile && entry.transcriptFile !== nextTranscriptFile) {
9951
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.transcriptFile), { force: true });
10216
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, entry.transcriptFile), { force: true });
9952
10217
  }
9953
10218
  entry.transcriptFile = nextTranscriptFile;
9954
10219
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9962,7 +10227,7 @@ var RecordingStorage = class {
9962
10227
  if (!entry) return;
9963
10228
  const nextSummaryFile = `${SUMMARIES_DIR}/${filename}`;
9964
10229
  if (entry.summaryFile && entry.summaryFile !== nextSummaryFile) {
9965
- (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.summaryFile), { force: true });
10230
+ (0, import_node_fs24.rmSync)((0, import_node_path20.join)(this.dir, entry.summaryFile), { force: true });
9966
10231
  }
9967
10232
  entry.summaryFile = nextSummaryFile;
9968
10233
  entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -10060,24 +10325,24 @@ var RecordingStorage = class {
10060
10325
  const entry = this.findById(recordingId);
10061
10326
  if (!entry) return false;
10062
10327
  if (entry.audioFile) {
10063
- const audioPath = (0, import_node_path19.join)(this.dir, entry.audioFile);
10064
- (0, import_node_fs23.rmSync)(audioPath, { force: true });
10328
+ const audioPath = (0, import_node_path20.join)(this.dir, entry.audioFile);
10329
+ (0, import_node_fs24.rmSync)(audioPath, { force: true });
10065
10330
  }
10066
10331
  if (entry.srtFile) {
10067
- const srtPath = (0, import_node_path19.join)(this.dir, entry.srtFile);
10068
- (0, import_node_fs23.rmSync)(srtPath, { force: true });
10332
+ const srtPath = (0, import_node_path20.join)(this.dir, entry.srtFile);
10333
+ (0, import_node_fs24.rmSync)(srtPath, { force: true });
10069
10334
  }
10070
10335
  if (entry.transcriptDataFile) {
10071
- const transcriptDataPath = (0, import_node_path19.join)(this.dir, entry.transcriptDataFile);
10072
- (0, import_node_fs23.rmSync)(transcriptDataPath, { force: true });
10336
+ const transcriptDataPath = (0, import_node_path20.join)(this.dir, entry.transcriptDataFile);
10337
+ (0, import_node_fs24.rmSync)(transcriptDataPath, { force: true });
10073
10338
  }
10074
10339
  if (entry.transcriptFile) {
10075
- const transcriptPath = (0, import_node_path19.join)(this.dir, entry.transcriptFile);
10076
- (0, import_node_fs23.rmSync)(transcriptPath, { force: true });
10340
+ const transcriptPath = (0, import_node_path20.join)(this.dir, entry.transcriptFile);
10341
+ (0, import_node_fs24.rmSync)(transcriptPath, { force: true });
10077
10342
  }
10078
10343
  if (entry.summaryFile) {
10079
- const summaryPath = (0, import_node_path19.join)(this.dir, entry.summaryFile);
10080
- (0, import_node_fs23.rmSync)(summaryPath, { force: true });
10344
+ const summaryPath = (0, import_node_path20.join)(this.dir, entry.summaryFile);
10345
+ (0, import_node_fs24.rmSync)(summaryPath, { force: true });
10081
10346
  }
10082
10347
  if (opts?.localOnly) {
10083
10348
  entry.audioFile = void 0;
@@ -10135,34 +10400,34 @@ var RecordingStorage = class {
10135
10400
  * 获取音频文件的绝对路径。ossUrl 用于推断文件扩展名
10136
10401
  */
10137
10402
  getAudioFilePath(recordingId, ossUrl) {
10138
- return (0, import_node_path19.join)(this.audioDir, this.buildAudioFilename(recordingId, ossUrl));
10403
+ return (0, import_node_path20.join)(this.audioDir, this.buildAudioFilename(recordingId, ossUrl));
10139
10404
  }
10140
10405
  /**
10141
10406
  * 获取打点文件的绝对路径
10142
10407
  */
10143
10408
  getSrtFilePath(recordingId) {
10144
- return (0, import_node_path19.join)(this.audioDir, this.buildSrtFilename(recordingId));
10409
+ return (0, import_node_path20.join)(this.audioDir, this.buildSrtFilename(recordingId));
10145
10410
  }
10146
10411
  /**
10147
10412
  * 获取转写 JSON 文件的绝对路径
10148
10413
  */
10149
10414
  getTranscriptDataFilePath(recordingId) {
10150
- return (0, import_node_path19.join)(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
10415
+ return (0, import_node_path20.join)(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
10151
10416
  }
10152
10417
  /**
10153
10418
  * 获取摘要文件的绝对路径
10154
10419
  */
10155
10420
  getSummaryFilePath(recordingId) {
10156
- return (0, import_node_path19.join)(this.summariesDir, this.buildSummaryFilename(recordingId));
10421
+ return (0, import_node_path20.join)(this.summariesDir, this.buildSummaryFilename(recordingId));
10157
10422
  }
10158
10423
  // ─── Persistence ───
10159
10424
  loadIndex() {
10160
- if (!(0, import_node_fs23.existsSync)(this.indexPath)) {
10425
+ if (!(0, import_node_fs24.existsSync)(this.indexPath)) {
10161
10426
  this.index = { recordings: [] };
10162
10427
  return;
10163
10428
  }
10164
10429
  try {
10165
- const raw = JSON.parse((0, import_node_fs23.readFileSync)(this.indexPath, "utf-8"));
10430
+ const raw = JSON.parse((0, import_node_fs24.readFileSync)(this.indexPath, "utf-8"));
10166
10431
  if (raw && Array.isArray(raw.recordings)) {
10167
10432
  let needsRewrite = false;
10168
10433
  const normalized = raw.recordings.filter((entry) => entry && typeof entry === "object").map((entry) => {
@@ -10200,8 +10465,8 @@ var RecordingStorage = class {
10200
10465
  segments: []
10201
10466
  });
10202
10467
  const transcriptDataFilename = this.buildTranscriptDataFilename(compacted.id);
10203
- (0, import_node_fs23.writeFileSync)(
10204
- (0, import_node_path19.join)(this.transcriptDataDir, transcriptDataFilename),
10468
+ (0, import_node_fs24.writeFileSync)(
10469
+ (0, import_node_path20.join)(this.transcriptDataDir, transcriptDataFilename),
10205
10470
  JSON.stringify(transcriptDoc, null, 2),
10206
10471
  "utf-8"
10207
10472
  );
@@ -10213,8 +10478,8 @@ var RecordingStorage = class {
10213
10478
  compacted.summaryFile = entry.summaryFile;
10214
10479
  } else if (typeof entry.summary === "string" && entry.summary.trim()) {
10215
10480
  const summaryFilename = this.buildSummaryFilename(entry.id);
10216
- (0, import_node_fs23.writeFileSync)(
10217
- (0, import_node_path19.join)(this.summariesDir, summaryFilename),
10481
+ (0, import_node_fs24.writeFileSync)(
10482
+ (0, import_node_path20.join)(this.summariesDir, summaryFilename),
10218
10483
  entry.summary.trim(),
10219
10484
  "utf-8"
10220
10485
  );
@@ -10224,8 +10489,8 @@ var RecordingStorage = class {
10224
10489
  const summaryFromDocument = extractTranscriptSummaryFromDocument(transcriptDoc);
10225
10490
  if (summaryFromDocument) {
10226
10491
  const summaryFilename = this.buildSummaryFilename(entry.id);
10227
- (0, import_node_fs23.writeFileSync)(
10228
- (0, import_node_path19.join)(this.summariesDir, summaryFilename),
10492
+ (0, import_node_fs24.writeFileSync)(
10493
+ (0, import_node_path20.join)(this.summariesDir, summaryFilename),
10229
10494
  summaryFromDocument,
10230
10495
  "utf-8"
10231
10496
  );
@@ -10265,7 +10530,7 @@ var RecordingStorage = class {
10265
10530
  }
10266
10531
  readRelativeTextFile(relativePath) {
10267
10532
  try {
10268
- return (0, import_node_fs23.readFileSync)((0, import_node_path19.join)(this.dir, relativePath), "utf-8");
10533
+ return (0, import_node_fs24.readFileSync)((0, import_node_path20.join)(this.dir, relativePath), "utf-8");
10269
10534
  } catch {
10270
10535
  return void 0;
10271
10536
  }
@@ -10278,7 +10543,7 @@ var RecordingStorage = class {
10278
10543
  return parseTranscriptDocument(raw);
10279
10544
  }
10280
10545
  saveIndex() {
10281
- (0, import_node_fs23.writeFileSync)(
10546
+ (0, import_node_fs24.writeFileSync)(
10282
10547
  this.indexPath,
10283
10548
  JSON.stringify(this.index, null, 2),
10284
10549
  "utf-8"
@@ -10292,8 +10557,8 @@ var RecordingStorage = class {
10292
10557
  init_transcript_document();
10293
10558
 
10294
10559
  // src/recording/downloader.ts
10295
- var import_node_fs24 = require("fs");
10296
- var import_node_path20 = require("path");
10560
+ var import_node_fs25 = require("fs");
10561
+ var import_node_path21 = require("path");
10297
10562
  var import_promises2 = require("stream/promises");
10298
10563
  var import_node_stream = require("stream");
10299
10564
  var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -10303,7 +10568,7 @@ async function downloadFile(url, destPath, logger, options) {
10303
10568
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10304
10569
  const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
10305
10570
  const retryBackoffMs = options?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
10306
- (0, import_node_fs24.mkdirSync)((0, import_node_path20.dirname)(destPath), { recursive: true });
10571
+ (0, import_node_fs25.mkdirSync)((0, import_node_path21.dirname)(destPath), { recursive: true });
10307
10572
  let lastError;
10308
10573
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
10309
10574
  const startMs = Date.now();
@@ -10321,11 +10586,11 @@ async function downloadFile(url, destPath, logger, options) {
10321
10586
  if (!res.body) {
10322
10587
  throw new Error("\u54CD\u5E94\u4F53\u4E3A\u7A7A");
10323
10588
  }
10324
- const writeStream = (0, import_node_fs24.createWriteStream)(destPath);
10589
+ const writeStream = (0, import_node_fs25.createWriteStream)(destPath);
10325
10590
  const readable = import_node_stream.Readable.fromWeb(res.body);
10326
10591
  await (0, import_promises2.pipeline)(readable, writeStream);
10327
10592
  const elapsed = Date.now() - startMs;
10328
- const fileSize = (0, import_node_fs24.existsSync)(destPath) ? (0, import_node_fs24.statSync)(destPath).size : 0;
10593
+ const fileSize = (0, import_node_fs25.existsSync)(destPath) ? (0, import_node_fs25.statSync)(destPath).size : 0;
10329
10594
  logger.info(
10330
10595
  `[downloader] \u4E0B\u8F7D\u5B8C\u6210: ${destPath} (${formatBytes(fileSize)}, ${elapsed}ms)`
10331
10596
  );
@@ -10333,13 +10598,13 @@ async function downloadFile(url, destPath, logger, options) {
10333
10598
  } finally {
10334
10599
  clearTimeout(timer);
10335
10600
  }
10336
- } catch (err) {
10337
- lastError = err?.message ?? String(err);
10601
+ } catch (err2) {
10602
+ lastError = err2?.message ?? String(err2);
10338
10603
  try {
10339
- if ((0, import_node_fs24.existsSync)(destPath)) (0, import_node_fs24.unlinkSync)(destPath);
10604
+ if ((0, import_node_fs25.existsSync)(destPath)) (0, import_node_fs25.unlinkSync)(destPath);
10340
10605
  } catch {
10341
10606
  }
10342
- const isAbort = err?.name === "AbortError";
10607
+ const isAbort = err2?.name === "AbortError";
10343
10608
  logger.warn(
10344
10609
  `[downloader] \u4E0B\u8F7D\u5931\u8D25 (attempt ${attempt}/${maxRetries}): ${isAbort ? "\u8D85\u65F6" : lastError}`
10345
10610
  );
@@ -10401,9 +10666,9 @@ function emitRecordingStatus(recordingId, storage, logger, notifyStatus, error,
10401
10666
  updatedAt: entry.updatedAt,
10402
10667
  error
10403
10668
  });
10404
- } catch (err) {
10669
+ } catch (err2) {
10405
10670
  logger.error(
10406
- `[recording-status] \u72B6\u6001\u4E8B\u4EF6\u53D1\u9001\u5931\u8D25: ${recordingId}, ${err?.message ?? err}`
10671
+ `[recording-status] \u72B6\u6001\u4E8B\u4EF6\u53D1\u9001\u5931\u8D25: ${recordingId}, ${err2?.message ?? err2}`
10407
10672
  );
10408
10673
  }
10409
10674
  }
@@ -10467,8 +10732,8 @@ async function handleRecordingSync(recordingId, metadata, storage, asrConfig, lo
10467
10732
  asrConfig,
10468
10733
  logger,
10469
10734
  options
10470
- ).catch((err) => {
10471
- const error = `\u5F55\u97F3\u540C\u6B65\u5931\u8D25: ${err?.message ?? err}`;
10735
+ ).catch((err2) => {
10736
+ const error = `\u5F55\u97F3\u540C\u6B65\u5931\u8D25: ${err2?.message ?? err2}`;
10472
10737
  logger.error(`[recording-sync] ${error}: ${recordingId}`);
10473
10738
  emitRecordingStatus(
10474
10739
  recordingId,
@@ -10491,9 +10756,9 @@ async function handleRecordingSync(recordingId, metadata, storage, asrConfig, lo
10491
10756
  asrConfig,
10492
10757
  logger,
10493
10758
  options
10494
- ).catch((err) => {
10759
+ ).catch((err2) => {
10495
10760
  logger.error(
10496
- `[asr-trigger] \u8F6C\u5199\u89E6\u53D1\u5931\u8D25: ${recordingId}, ${err?.message ?? err}`
10761
+ `[asr-trigger] \u8F6C\u5199\u89E6\u53D1\u5931\u8D25: ${recordingId}, ${err2?.message ?? err2}`
10497
10762
  );
10498
10763
  });
10499
10764
  }
@@ -10574,13 +10839,13 @@ async function triggerTranscription(recordingId, storage, asrConfig, logger, opt
10574
10839
  }
10575
10840
 
10576
10841
  // src/tunnel/service.ts
10577
- var import_node_fs29 = require("fs");
10578
- var import_node_path25 = require("path");
10842
+ var import_node_fs30 = require("fs");
10843
+ var import_node_path26 = require("path");
10579
10844
  init_credentials();
10580
10845
 
10581
10846
  // src/tunnel/relay-client.ts
10582
- var import_node_fs27 = require("fs");
10583
- var import_node_path23 = require("path");
10847
+ var import_node_fs28 = require("fs");
10848
+ var import_node_path24 = require("path");
10584
10849
 
10585
10850
  // node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
10586
10851
  var import_stream = __toESM(require_stream(), 1);
@@ -10624,8 +10889,8 @@ var RelayClient = class {
10624
10889
  lastDisconnectReason
10625
10890
  };
10626
10891
  try {
10627
- (0, import_node_fs27.mkdirSync)((0, import_node_path23.dirname)(this.opts.statusFilePath), { recursive: true });
10628
- (0, import_node_fs27.writeFileSync)(this.opts.statusFilePath, JSON.stringify(info, null, 2));
10892
+ (0, import_node_fs28.mkdirSync)((0, import_node_path24.dirname)(this.opts.statusFilePath), { recursive: true });
10893
+ (0, import_node_fs28.writeFileSync)(this.opts.statusFilePath, JSON.stringify(info, null, 2));
10629
10894
  } catch {
10630
10895
  }
10631
10896
  }
@@ -10829,9 +11094,9 @@ var RelayClient = class {
10829
11094
  }
10830
11095
  settle();
10831
11096
  });
10832
- ws.on("error", (err) => {
11097
+ ws.on("error", (err2) => {
10833
11098
  this.opts.logger.error(
10834
- `Relay tunnel: WebSocket error: ${err.message} (readyState=${ws.readyState}, reconnectAttempt=${this.reconnectAttempt}, url=${wsUrl.toString()})`
11099
+ `Relay tunnel: WebSocket error: ${err2.message} (readyState=${ws.readyState}, reconnectAttempt=${this.reconnectAttempt}, url=${wsUrl.toString()})`
10835
11100
  );
10836
11101
  settle();
10837
11102
  });
@@ -10839,9 +11104,9 @@ var RelayClient = class {
10839
11104
  }
10840
11105
  emitConnected() {
10841
11106
  for (const handler of this.connectedHandlers) {
10842
- Promise.resolve(handler()).catch((err) => {
11107
+ Promise.resolve(handler()).catch((err2) => {
10843
11108
  this.opts.logger.warn(
10844
- `Relay tunnel: onConnected handler failed: ${err instanceof Error ? err.message : String(err)}`
11109
+ `Relay tunnel: onConnected handler failed: ${err2 instanceof Error ? err2.message : String(err2)}`
10845
11110
  );
10846
11111
  });
10847
11112
  }
@@ -10869,12 +11134,12 @@ var RelayClient = class {
10869
11134
  try {
10870
11135
  const result = handler(frame);
10871
11136
  if (result instanceof Promise) {
10872
- result.catch((err) => {
10873
- this.opts.logger.error(`Relay tunnel: handler error: ${err}`);
11137
+ result.catch((err2) => {
11138
+ this.opts.logger.error(`Relay tunnel: handler error: ${err2}`);
10874
11139
  });
10875
11140
  }
10876
- } catch (err) {
10877
- this.opts.logger.error(`Relay tunnel: handler error: ${err}`);
11141
+ } catch (err2) {
11142
+ this.opts.logger.error(`Relay tunnel: handler error: ${err2}`);
10878
11143
  }
10879
11144
  }
10880
11145
  }
@@ -10963,8 +11228,8 @@ var RelayClient = class {
10963
11228
  this.reconnectTimer = setTimeout(() => {
10964
11229
  this.reconnectTimer = null;
10965
11230
  if (!this.aborted) {
10966
- this.connect().catch((err) => {
10967
- this.opts.logger.error(`Relay tunnel: reconnect failed: ${err}`);
11231
+ this.connect().catch((err2) => {
11232
+ this.opts.logger.error(`Relay tunnel: reconnect failed: ${err2}`);
10968
11233
  });
10969
11234
  }
10970
11235
  }, delayMs);
@@ -11007,8 +11272,8 @@ init_host();
11007
11272
 
11008
11273
  // src/tunnel/device-identity.ts
11009
11274
  var import_node_crypto3 = __toESM(require("crypto"), 1);
11010
- var import_node_fs28 = __toESM(require("fs"), 1);
11011
- var import_node_path24 = __toESM(require("path"), 1);
11275
+ var import_node_fs29 = __toESM(require("fs"), 1);
11276
+ var import_node_path25 = __toESM(require("path"), 1);
11012
11277
  init_host();
11013
11278
  var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
11014
11279
  function base64UrlEncode(buf) {
@@ -11053,10 +11318,10 @@ function resolveClientStateDir(stateDir) {
11053
11318
  return stateDir ?? resolveStateDir();
11054
11319
  }
11055
11320
  function ensureDir(filePath) {
11056
- import_node_fs28.default.mkdirSync(import_node_path24.default.dirname(filePath), { recursive: true });
11321
+ import_node_fs29.default.mkdirSync(import_node_path25.default.dirname(filePath), { recursive: true });
11057
11322
  }
11058
11323
  function resolveIdentityPath(stateDir) {
11059
- return import_node_path24.default.join(stateDir, "identity", "device.json");
11324
+ return import_node_path25.default.join(stateDir, "identity", "device.json");
11060
11325
  }
11061
11326
  function normalizeDeviceAuthRole(role) {
11062
11327
  return role.trim();
@@ -11072,12 +11337,12 @@ function normalizeDeviceAuthScopes(scopes) {
11072
11337
  return [...out].sort();
11073
11338
  }
11074
11339
  function resolveDeviceAuthPath(stateDir) {
11075
- return import_node_path24.default.join(stateDir, "identity", "device-auth.json");
11340
+ return import_node_path25.default.join(stateDir, "identity", "device-auth.json");
11076
11341
  }
11077
11342
  function readDeviceAuthStore(filePath) {
11078
11343
  try {
11079
- if (!import_node_fs28.default.existsSync(filePath)) return null;
11080
- const raw = import_node_fs28.default.readFileSync(filePath, "utf8");
11344
+ if (!import_node_fs29.default.existsSync(filePath)) return null;
11345
+ const raw = import_node_fs29.default.readFileSync(filePath, "utf8");
11081
11346
  const parsed = JSON.parse(raw);
11082
11347
  if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") return null;
11083
11348
  if (!parsed.tokens || typeof parsed.tokens !== "object") return null;
@@ -11088,12 +11353,12 @@ function readDeviceAuthStore(filePath) {
11088
11353
  }
11089
11354
  function writeDeviceAuthStore(filePath, store) {
11090
11355
  ensureDir(filePath);
11091
- import_node_fs28.default.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}
11356
+ import_node_fs29.default.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}
11092
11357
  `, {
11093
11358
  mode: 384
11094
11359
  });
11095
11360
  try {
11096
- import_node_fs28.default.chmodSync(filePath, 384);
11361
+ import_node_fs29.default.chmodSync(filePath, 384);
11097
11362
  } catch {
11098
11363
  }
11099
11364
  }
@@ -11140,8 +11405,8 @@ function clearDeviceAuthToken(params) {
11140
11405
  function loadOrCreateDeviceIdentity(stateDir) {
11141
11406
  const filePath = resolveIdentityPath(stateDir);
11142
11407
  try {
11143
- if (import_node_fs28.default.existsSync(filePath)) {
11144
- const raw = import_node_fs28.default.readFileSync(filePath, "utf8");
11408
+ if (import_node_fs29.default.existsSync(filePath)) {
11409
+ const raw = import_node_fs29.default.readFileSync(filePath, "utf8");
11145
11410
  const parsed = JSON.parse(raw);
11146
11411
  if (parsed?.version === 1 && typeof parsed.deviceId === "string" && typeof parsed.publicKeyPem === "string" && typeof parsed.privateKeyPem === "string") {
11147
11412
  const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
@@ -11162,14 +11427,14 @@ function loadOrCreateDeviceIdentity(stateDir) {
11162
11427
  publicKeyPem,
11163
11428
  privateKeyPem
11164
11429
  };
11165
- import_node_fs28.default.mkdirSync(import_node_path24.default.dirname(filePath), { recursive: true });
11430
+ import_node_fs29.default.mkdirSync(import_node_path25.default.dirname(filePath), { recursive: true });
11166
11431
  const stored = {
11167
11432
  version: 1,
11168
11433
  ...identity,
11169
11434
  createdAtMs: Date.now()
11170
11435
  };
11171
11436
  ensureDir(filePath);
11172
- import_node_fs28.default.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
11437
+ import_node_fs29.default.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
11173
11438
  `, {
11174
11439
  mode: 384
11175
11440
  });
@@ -11272,8 +11537,8 @@ async function handleHttpRequest(opts, frame) {
11272
11537
  });
11273
11538
  return;
11274
11539
  }
11275
- } catch (err) {
11276
- const message = err instanceof Error ? err.message : String(err);
11540
+ } catch (err2) {
11541
+ const message = err2 instanceof Error ? err2.message : String(err2);
11277
11542
  opts.logger.error(
11278
11543
  `TunnelProxy: HTTP id=${frame.id} ${frame.method} ${mappedPath} failed after ${Date.now() - startedAtMs}ms: ${message}`
11279
11544
  );
@@ -11343,15 +11608,15 @@ async function streamResponse(opts, requestId, res, startedAtMs) {
11343
11608
  state: "end",
11344
11609
  data: ""
11345
11610
  });
11346
- } catch (err) {
11611
+ } catch (err2) {
11347
11612
  opts.logger.error(
11348
- `TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks and ${Date.now() - startedAtMs}ms: ${err instanceof Error ? err.message : String(err)}`
11613
+ `TunnelProxy: stream error id=${requestId} after ${chunkCount} chunks and ${Date.now() - startedAtMs}ms: ${err2 instanceof Error ? err2.message : String(err2)}`
11349
11614
  );
11350
11615
  opts.client.send({
11351
11616
  type: "proxy_error",
11352
11617
  id: requestId,
11353
11618
  status: 502,
11354
- message: `stream error: ${err instanceof Error ? err.message : String(err)}`
11619
+ message: `stream error: ${err2 instanceof Error ? err2.message : String(err2)}`
11355
11620
  });
11356
11621
  }
11357
11622
  }
@@ -11418,27 +11683,27 @@ var WsProxy = class {
11418
11683
  reason: reason.toString()
11419
11684
  });
11420
11685
  });
11421
- ws.on("error", (err) => {
11686
+ ws.on("error", (err2) => {
11422
11687
  this.opts.logger.warn(
11423
- `TunnelProxy: WS id=${frame.id} error: ${err.message}, active=${this.connections.size}`
11688
+ `TunnelProxy: WS id=${frame.id} error: ${err2.message}, active=${this.connections.size}`
11424
11689
  );
11425
11690
  this.connections.delete(frame.id);
11426
11691
  this.opts.client.send({
11427
11692
  type: "ws_close",
11428
11693
  id: frame.id,
11429
11694
  code: 1011,
11430
- reason: err.message
11695
+ reason: err2.message
11431
11696
  });
11432
11697
  });
11433
- } catch (err) {
11698
+ } catch (err2) {
11434
11699
  this.opts.logger.error(
11435
- `TunnelProxy: WS id=${frame.id} failed to connect: ${err instanceof Error ? err.message : String(err)}`
11700
+ `TunnelProxy: WS id=${frame.id} failed to connect: ${err2 instanceof Error ? err2.message : String(err2)}`
11436
11701
  );
11437
11702
  this.opts.client.send({
11438
11703
  type: "ws_close",
11439
11704
  id: frame.id,
11440
11705
  code: 1011,
11441
- reason: `failed to connect: ${err instanceof Error ? err.message : String(err)}`
11706
+ reason: `failed to connect: ${err2 instanceof Error ? err2.message : String(err2)}`
11442
11707
  });
11443
11708
  }
11444
11709
  }
@@ -11478,19 +11743,19 @@ var WsProxy = class {
11478
11743
  var MAX_AUTO_PAIRING_APPROVALS = 3;
11479
11744
  var approveDevicePairingPromise = null;
11480
11745
  var approveDevicePairingWarned = false;
11481
- function formatErrorMessage(err) {
11482
- if (err instanceof Error && err.message) return err.message;
11483
- return String(err);
11746
+ function formatErrorMessage(err2) {
11747
+ if (err2 instanceof Error && err2.message) return err2.message;
11748
+ return String(err2);
11484
11749
  }
11485
11750
  async function loadApproveDevicePairing(logger) {
11486
11751
  if (!approveDevicePairingPromise) {
11487
11752
  approveDevicePairingPromise = import("openclaw/plugin-sdk/device-bootstrap").then(
11488
11753
  (mod) => typeof mod.approveDevicePairing === "function" ? mod.approveDevicePairing : null
11489
- ).catch((err) => {
11754
+ ).catch((err2) => {
11490
11755
  if (!approveDevicePairingWarned) {
11491
11756
  approveDevicePairingWarned = true;
11492
11757
  logger.warn(
11493
- `TunnelProxy: local gateway auto-pairing disabled because current OpenClaw runtime does not expose device bootstrap SDK (${formatErrorMessage(err)})`
11758
+ `TunnelProxy: local gateway auto-pairing disabled because current OpenClaw runtime does not expose device bootstrap SDK (${formatErrorMessage(err2)})`
11494
11759
  );
11495
11760
  }
11496
11761
  return null;
@@ -11715,9 +11980,9 @@ var TunnelProxy = class {
11715
11980
  `TunnelProxy: auto-approved local gateway pairing request ${requestId} (reason=${reason || "not-paired"}, hostStateDir=${this.hostStateDir}, approval=${this.gatewayWsAutoPairingApprovals}/${MAX_AUTO_PAIRING_APPROVALS})`
11716
11981
  );
11717
11982
  return true;
11718
- } catch (err) {
11983
+ } catch (err2) {
11719
11984
  this.opts.logger.warn(
11720
- `TunnelProxy: failed to auto-approve gateway pairing request ${requestId}: ${err?.message ?? String(err)}`
11985
+ `TunnelProxy: failed to auto-approve gateway pairing request ${requestId}: ${err2?.message ?? String(err2)}`
11721
11986
  );
11722
11987
  return false;
11723
11988
  }
@@ -11831,9 +12096,9 @@ var TunnelProxy = class {
11831
12096
  queueMicrotask(() => this.ensureGatewayWs());
11832
12097
  }
11833
12098
  });
11834
- ws.on("error", (err) => {
12099
+ ws.on("error", (err2) => {
11835
12100
  this.opts.logger.warn(
11836
- `TunnelProxy: RPC WS error: ${err.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`
12101
+ `TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`
11837
12102
  );
11838
12103
  this.gatewayWsConnecting = false;
11839
12104
  if (this.gatewayWs === ws) {
@@ -11933,13 +12198,13 @@ function createTunnelService(opts) {
11933
12198
  try {
11934
12199
  process.kill(pid, 0);
11935
12200
  return true;
11936
- } catch (err) {
11937
- return err?.code === "EPERM";
12201
+ } catch (err2) {
12202
+ return err2?.code === "EPERM";
11938
12203
  }
11939
12204
  }
11940
12205
  function readLockOwner(filePath) {
11941
12206
  try {
11942
- const parsed = JSON.parse((0, import_node_fs29.readFileSync)(filePath, "utf-8"));
12207
+ const parsed = JSON.parse((0, import_node_fs30.readFileSync)(filePath, "utf-8"));
11943
12208
  return typeof parsed.pid === "number" ? parsed.pid : null;
11944
12209
  } catch {
11945
12210
  return null;
@@ -11952,23 +12217,23 @@ function createTunnelService(opts) {
11952
12217
  lockFd = null;
11953
12218
  if (fd !== null) {
11954
12219
  try {
11955
- (0, import_node_fs29.closeSync)(fd);
12220
+ (0, import_node_fs30.closeSync)(fd);
11956
12221
  } catch {
11957
12222
  }
11958
12223
  }
11959
12224
  if (filePath) {
11960
12225
  try {
11961
- (0, import_node_fs29.unlinkSync)(filePath);
12226
+ (0, import_node_fs30.unlinkSync)(filePath);
11962
12227
  } catch {
11963
12228
  }
11964
12229
  }
11965
12230
  }
11966
12231
  function acquireLock(filePath) {
11967
- (0, import_node_fs29.mkdirSync)((0, import_node_path25.dirname)(filePath), { recursive: true });
12232
+ (0, import_node_fs30.mkdirSync)((0, import_node_path26.dirname)(filePath), { recursive: true });
11968
12233
  for (let attempt = 0; attempt < 2; attempt++) {
11969
12234
  try {
11970
- const fd = (0, import_node_fs29.openSync)(filePath, "wx", 384);
11971
- (0, import_node_fs29.writeFileSync)(
12235
+ const fd = (0, import_node_fs30.openSync)(filePath, "wx", 384);
12236
+ (0, import_node_fs30.writeFileSync)(
11972
12237
  fd,
11973
12238
  JSON.stringify({
11974
12239
  pid: process.pid,
@@ -11979,10 +12244,10 @@ function createTunnelService(opts) {
11979
12244
  lockFilePath = filePath;
11980
12245
  lockFd = fd;
11981
12246
  return true;
11982
- } catch (err) {
11983
- if (err?.code !== "EEXIST") {
12247
+ } catch (err2) {
12248
+ if (err2?.code !== "EEXIST") {
11984
12249
  opts.logger.error(
11985
- `Relay tunnel: failed to acquire local lock ${filePath}: ${String(err)}`
12250
+ `Relay tunnel: failed to acquire local lock ${filePath}: ${String(err2)}`
11986
12251
  );
11987
12252
  return false;
11988
12253
  }
@@ -11992,7 +12257,7 @@ function createTunnelService(opts) {
11992
12257
  `Relay tunnel: removing stale local lock owned by dead pid=${ownerPid}`
11993
12258
  );
11994
12259
  try {
11995
- (0, import_node_fs29.unlinkSync)(filePath);
12260
+ (0, import_node_fs30.unlinkSync)(filePath);
11996
12261
  } catch {
11997
12262
  }
11998
12263
  continue;
@@ -12037,12 +12302,12 @@ function createTunnelService(opts) {
12037
12302
  return;
12038
12303
  }
12039
12304
  const { logger } = opts;
12040
- const baseStateDir = (0, import_node_path25.join)(ctx.stateDir, "plugins", "phone-notifications");
12305
+ const baseStateDir = (0, import_node_path26.join)(ctx.stateDir, "plugins", "phone-notifications");
12041
12306
  logger.info(
12042
12307
  `Relay tunnel: starting (pid=${process.pid}, url=${opts.tunnelUrl}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`
12043
12308
  );
12044
- const statusFilePath = (0, import_node_path25.join)(baseStateDir, "tunnel-status.json");
12045
- const lockPath = (0, import_node_path25.join)(baseStateDir, "relay-tunnel.lock");
12309
+ const statusFilePath = (0, import_node_path26.join)(baseStateDir, "tunnel-status.json");
12310
+ const lockPath = (0, import_node_path26.join)(baseStateDir, "relay-tunnel.lock");
12046
12311
  if (!acquireLock(lockPath)) {
12047
12312
  return;
12048
12313
  }
@@ -12070,14 +12335,14 @@ function createTunnelService(opts) {
12070
12335
  emitPendingPluginUpdate("relay connected");
12071
12336
  });
12072
12337
  abortController = new AbortController();
12073
- client.connectWithAutoReconnect(abortController.signal).catch((err) => {
12338
+ client.connectWithAutoReconnect(abortController.signal).catch((err2) => {
12074
12339
  releaseLock();
12075
- logger.error(`Relay tunnel: unexpected error: ${err}`);
12340
+ logger.error(`Relay tunnel: unexpected error: ${err2}`);
12076
12341
  });
12077
12342
  logger.info("Relay tunnel \u670D\u52A1\u5DF2\u542F\u52A8");
12078
- } catch (err) {
12343
+ } catch (err2) {
12079
12344
  releaseLock();
12080
- throw err;
12345
+ throw err2;
12081
12346
  }
12082
12347
  },
12083
12348
  async stop() {
@@ -12182,9 +12447,9 @@ function readHostGatewayConfig(params) {
12182
12447
  let configData;
12183
12448
  if (configPath) {
12184
12449
  try {
12185
- configData = JSON.parse((0, import_node_fs30.readFileSync)(configPath, "utf-8"));
12186
- } catch (err) {
12187
- if (err?.code !== "ENOENT") {
12450
+ configData = JSON.parse((0, import_node_fs31.readFileSync)(configPath, "utf-8"));
12451
+ } catch (err2) {
12452
+ if (err2?.code !== "ENOENT") {
12188
12453
  params.logger.warn(
12189
12454
  `Relay tunnel: \u65E0\u6CD5\u8BFB\u53D6 gateway \u9274\u6743\u914D\u7F6E (${configPath})`
12190
12455
  );
@@ -12294,7 +12559,7 @@ function registerNotificationInterfaces(deps) {
12294
12559
  } = deps;
12295
12560
  function triggerAfterIngest(insertedCount, cron) {
12296
12561
  if (insertedCount <= 0 || !onAfterIngest) return;
12297
- void Promise.resolve().then(() => onAfterIngest(insertedCount, cron)).catch((err) => logger.warn(`onAfterIngest failed: ${err?.message ?? err}`));
12562
+ void Promise.resolve().then(() => onAfterIngest(insertedCount, cron)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12298
12563
  }
12299
12564
  registerGatewayMethod(
12300
12565
  "notifications.push",
@@ -12687,9 +12952,9 @@ function registerRecordingInterfaces(deps) {
12687
12952
  }
12688
12953
  triggerTranscription(recordingId, recordingStorage, asr, logger, {
12689
12954
  notifyStatus: notifyRecordingStatus
12690
- }).catch((err) => {
12955
+ }).catch((err2) => {
12691
12956
  logger.error(
12692
- `[recordings.retranscribe] \u91CD\u8BD5\u8F6C\u5199\u5931\u8D25: ${recordingId}, ${err?.message ?? err}`
12957
+ `[recordings.retranscribe] \u91CD\u8BD5\u8F6C\u5199\u5931\u8D25: ${recordingId}, ${err2?.message ?? err2}`
12693
12958
  );
12694
12959
  });
12695
12960
  respond(true, { ok: true, recordingId, message: "\u8F6C\u5199\u5DF2\u91CD\u65B0\u89E6\u53D1" });
@@ -12935,8 +13200,9 @@ var index_default = {
12935
13200
  tunnelService
12936
13201
  });
12937
13202
  registerLightRulesGateway(api, lightRuleRegistry, logger, cacheBroadcast);
13203
+ registerLightRulesTools(api, lightRuleRegistry, logger);
12938
13204
  logger.info(
12939
- "Gateway \u706F\u6548\u89C4\u5219\u65B9\u6CD5\u5DF2\u6CE8\u518C: lightrules.list / lightrules.create / lightrules.update / lightrules.delete"
13205
+ "\u706F\u6548\u89C4\u5219\u65B9\u6CD5\u5DF2\u6CE8\u518C: lightrules.list / lightrules.create / lightrules.update / lightrules.delete"
12940
13206
  );
12941
13207
  autoUpdateLifecycle = registerAutoUpdateLifecycle({
12942
13208
  api,