@yoooclaw/phone-notifications 1.11.0-beta.0 → 1.11.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -52,24 +52,24 @@ function expandUserPath(value) {
52
52
  return homeDir();
53
53
  }
54
54
  if (value.startsWith("~/")) {
55
- return (0, import_node_path5.join)(homeDir(), value.slice(2));
55
+ return (0, import_node_path4.join)(homeDir(), value.slice(2));
56
56
  }
57
57
  return value;
58
58
  }
59
59
  function candidateMetaPaths() {
60
60
  const home = homeDir();
61
61
  return [
62
- (0, import_node_path5.join)(home, ".qclaw", "qclaw.json"),
63
- (0, import_node_path5.join)(home, ".qclow", "qclaw.json")
62
+ (0, import_node_path4.join)(home, ".qclaw", "qclaw.json"),
63
+ (0, import_node_path4.join)(home, ".qclow", "qclaw.json")
64
64
  ];
65
65
  }
66
66
  function loadQClawMeta() {
67
67
  for (const metaPath of candidateMetaPaths()) {
68
- if (!(0, import_node_fs6.existsSync)(metaPath)) {
68
+ if (!(0, import_node_fs5.existsSync)(metaPath)) {
69
69
  continue;
70
70
  }
71
71
  try {
72
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(metaPath, "utf-8"));
72
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(metaPath, "utf-8"));
73
73
  return {
74
74
  stateDir: expandUserPath(trimToUndefined(parsed?.stateDir)),
75
75
  configPath: expandUserPath(trimToUndefined(parsed?.configPath))
@@ -90,12 +90,12 @@ function resolveConfigPathFromEnv() {
90
90
  );
91
91
  }
92
92
  function hasOpenClawMarkers() {
93
- const baseDir = (0, import_node_path5.join)(homeDir(), ".openclaw");
93
+ const baseDir = (0, import_node_path4.join)(homeDir(), ".openclaw");
94
94
  return [
95
- (0, import_node_path5.join)(baseDir, "openclaw.json"),
96
- (0, import_node_path5.join)(baseDir, "credentials.json"),
97
- (0, import_node_path5.join)(baseDir, "extensions")
98
- ].some((candidate) => (0, import_node_fs6.existsSync)(candidate));
95
+ (0, import_node_path4.join)(baseDir, "openclaw.json"),
96
+ (0, import_node_path4.join)(baseDir, "credentials.json"),
97
+ (0, import_node_path4.join)(baseDir, "extensions")
98
+ ].some((candidate) => (0, import_node_fs5.existsSync)(candidate));
99
99
  }
100
100
  function resolveStateDir() {
101
101
  const envDir = resolveStateDirFromEnv();
@@ -103,16 +103,16 @@ function resolveStateDir() {
103
103
  return envDir;
104
104
  }
105
105
  if (hasOpenClawMarkers()) {
106
- return (0, import_node_path5.join)(homeDir(), ".openclaw");
106
+ return (0, import_node_path4.join)(homeDir(), ".openclaw");
107
107
  }
108
108
  const meta = loadQClawMeta();
109
109
  if (meta?.stateDir) {
110
110
  return meta.stateDir;
111
111
  }
112
112
  if (meta?.configPath) {
113
- return (0, import_node_path5.dirname)(meta.configPath);
113
+ return (0, import_node_path4.dirname)(meta.configPath);
114
114
  }
115
- return (0, import_node_path5.join)(homeDir(), ".openclaw");
115
+ return (0, import_node_path4.join)(homeDir(), ".openclaw");
116
116
  }
117
117
  function resolveConfigPath(stateDir = resolveStateDir()) {
118
118
  const envConfigPath = resolveConfigPathFromEnv();
@@ -123,17 +123,17 @@ function resolveConfigPath(stateDir = resolveStateDir()) {
123
123
  if (meta?.configPath && (!meta.stateDir || !stateDir || meta.stateDir === stateDir)) {
124
124
  return meta.configPath;
125
125
  }
126
- return (0, import_node_path5.join)(stateDir, "openclaw.json");
126
+ return (0, import_node_path4.join)(stateDir, "openclaw.json");
127
127
  }
128
128
  function resolveStateFile(filename) {
129
- return (0, import_node_path5.join)(resolveStateDir(), filename);
129
+ return (0, import_node_path4.join)(resolveStateDir(), filename);
130
130
  }
131
- var import_node_fs6, import_node_path5;
131
+ var import_node_fs5, import_node_path4;
132
132
  var init_host = __esm({
133
133
  "src/host.ts"() {
134
134
  "use strict";
135
- import_node_fs6 = require("fs");
136
- import_node_path5 = require("path");
135
+ import_node_fs5 = require("fs");
136
+ import_node_path4 = require("path");
137
137
  }
138
138
  });
139
139
 
@@ -143,17 +143,17 @@ function credentialsPath() {
143
143
  }
144
144
  function readCredentials() {
145
145
  const path2 = credentialsPath();
146
- if (!(0, import_node_fs7.existsSync)(path2)) return {};
146
+ if (!(0, import_node_fs6.existsSync)(path2)) return {};
147
147
  try {
148
- return JSON.parse((0, import_node_fs7.readFileSync)(path2, "utf-8"));
148
+ return JSON.parse((0, import_node_fs6.readFileSync)(path2, "utf-8"));
149
149
  } catch {
150
150
  return {};
151
151
  }
152
152
  }
153
153
  function writeCredentials(creds) {
154
154
  const path2 = credentialsPath();
155
- (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true, mode: 448 });
156
- (0, import_node_fs7.writeFileSync)(path2, JSON.stringify(creds, null, 2), {
155
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(path2), { recursive: true, mode: 448 });
156
+ (0, import_node_fs6.writeFileSync)(path2, JSON.stringify(creds, null, 2), {
157
157
  encoding: "utf-8",
158
158
  mode: 384
159
159
  });
@@ -173,8 +173,8 @@ function requireApiKey() {
173
173
  }
174
174
  function watchCredentials(onChange) {
175
175
  const path2 = credentialsPath();
176
- const dir = (0, import_node_path6.dirname)(path2);
177
- const filename = (0, import_node_path6.basename)(path2);
176
+ const dir = (0, import_node_path5.dirname)(path2);
177
+ const filename = (0, import_node_path5.basename)(path2);
178
178
  let debounceTimer = null;
179
179
  const delayMs = 200;
180
180
  const listener = (_event, changedName) => {
@@ -187,7 +187,7 @@ function watchCredentials(onChange) {
187
187
  };
188
188
  let watcher = null;
189
189
  try {
190
- watcher = (0, import_node_fs7.watch)(dir, { persistent: false }, listener);
190
+ watcher = (0, import_node_fs6.watch)(dir, { persistent: false }, listener);
191
191
  } catch {
192
192
  }
193
193
  return () => {
@@ -195,12 +195,12 @@ function watchCredentials(onChange) {
195
195
  watcher?.close();
196
196
  };
197
197
  }
198
- var import_node_fs7, import_node_path6;
198
+ var import_node_fs6, import_node_path5;
199
199
  var init_credentials = __esm({
200
200
  "src/auth/credentials.ts"() {
201
201
  "use strict";
202
- import_node_fs7 = require("fs");
203
- import_node_path6 = require("path");
202
+ import_node_fs6 = require("fs");
203
+ import_node_path5 = require("path");
204
204
  init_host();
205
205
  }
206
206
  });
@@ -208,8 +208,8 @@ var init_credentials = __esm({
208
208
  // src/env.ts
209
209
  function writeDotEnv(key, value) {
210
210
  const path2 = resolveStateFile(".env");
211
- (0, import_node_fs8.mkdirSync)((0, import_node_path7.dirname)(path2), { recursive: true });
212
- const existing = (0, import_node_fs8.existsSync)(path2) ? (0, import_node_fs8.readFileSync)(path2, "utf-8") : "";
211
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true });
212
+ const existing = (0, import_node_fs7.existsSync)(path2) ? (0, import_node_fs7.readFileSync)(path2, "utf-8") : "";
213
213
  const lines = existing.split("\n");
214
214
  const prefix = `${key}=`;
215
215
  const idx = lines.findIndex((l) => l.startsWith(prefix));
@@ -220,13 +220,13 @@ function writeDotEnv(key, value) {
220
220
  if (existing && !existing.endsWith("\n")) lines.push("");
221
221
  lines.push(newLine);
222
222
  }
223
- (0, import_node_fs8.writeFileSync)(path2, lines.join("\n"), "utf-8");
223
+ (0, import_node_fs7.writeFileSync)(path2, lines.join("\n"), "utf-8");
224
224
  }
225
225
  function readDotEnv() {
226
226
  const path2 = resolveStateFile(".env");
227
- if (!(0, import_node_fs8.existsSync)(path2)) return {};
227
+ if (!(0, import_node_fs7.existsSync)(path2)) return {};
228
228
  return Object.fromEntries(
229
- (0, import_node_fs8.readFileSync)(path2, "utf-8").split("\n").flatMap((line) => {
229
+ (0, import_node_fs7.readFileSync)(path2, "utf-8").split("\n").flatMap((line) => {
230
230
  const eq = line.indexOf("=");
231
231
  if (eq < 1) return [];
232
232
  return [[line.slice(0, eq).trim(), line.slice(eq + 1).trim()]];
@@ -256,12 +256,12 @@ function getEnvUrls(env) {
256
256
  function getAvailableEnvs() {
257
257
  return Object.keys(ENV_CONFIG);
258
258
  }
259
- var import_node_fs8, import_node_path7, ENV_CONFIG, VALID_ENVS;
259
+ var import_node_fs7, import_node_path6, ENV_CONFIG, VALID_ENVS;
260
260
  var init_env = __esm({
261
261
  "src/env.ts"() {
262
262
  "use strict";
263
- import_node_fs8 = require("fs");
264
- import_node_path7 = require("path");
263
+ import_node_fs7 = require("fs");
264
+ import_node_path6 = require("path");
265
265
  init_credentials();
266
266
  init_host();
267
267
  ENV_CONFIG = {
@@ -290,7 +290,7 @@ function buildTranscriptDataFilename(recordingId) {
290
290
  }
291
291
  function buildTranscriptDocument(params) {
292
292
  const segments = normalizeSegments(params.segments);
293
- const text = normalizeOptionalText(params.text) ?? joinSegmentsText(segments) ?? "";
293
+ const text = normalizePossiblyEmptyText(params.text) ?? joinSegmentsText(segments) ?? "";
294
294
  return {
295
295
  schemaVersion: TRANSCRIPT_DOCUMENT_SCHEMA_VERSION,
296
296
  recordingId: params.recordingId,
@@ -304,7 +304,7 @@ function buildTranscriptDocument(params) {
304
304
  normalized: {
305
305
  title: normalizeOptionalText(params.title),
306
306
  category: normalizeOptionalText(params.category),
307
- summary: normalizeOptionalText(params.summary),
307
+ summary: normalizePossiblyEmptyText(params.summary),
308
308
  text,
309
309
  segments
310
310
  },
@@ -335,11 +335,11 @@ function parseTranscriptDocument(value) {
335
335
  }
336
336
  function extractTranscriptTextFromDocument(doc) {
337
337
  if (!doc) return void 0;
338
- return normalizeOptionalText(doc.normalized.text) ?? joinSegmentsText(doc.normalized.segments);
338
+ return normalizePossiblyEmptyText(doc.normalized.text) ?? joinSegmentsText(doc.normalized.segments);
339
339
  }
340
340
  function extractTranscriptSummaryFromDocument(doc) {
341
341
  if (!doc) return void 0;
342
- return normalizeOptionalText(doc.normalized.summary);
342
+ return normalizePossiblyEmptyText(doc.normalized.summary);
343
343
  }
344
344
  function extractTranscriptTitleFromDocument(doc) {
345
345
  if (!doc) return void 0;
@@ -392,14 +392,14 @@ function normalizeDocumentBody(value) {
392
392
  }
393
393
  const normalized = value;
394
394
  const segments = normalizeSegments(normalized.segments);
395
- const text = normalizeOptionalText(normalized.text) ?? joinSegmentsText(segments);
396
- if (!text) {
395
+ const text = normalizePossiblyEmptyText(normalized.text) ?? joinSegmentsText(segments);
396
+ if (text === void 0) {
397
397
  return void 0;
398
398
  }
399
399
  return {
400
400
  title: normalizeOptionalText(normalized.title),
401
401
  category: normalizeOptionalText(normalized.category),
402
- summary: normalizeOptionalText(normalized.summary),
402
+ summary: normalizePossiblyEmptyText(normalized.summary),
403
403
  text,
404
404
  segments
405
405
  };
@@ -465,6 +465,9 @@ function extractRawSourceTextList(raw) {
465
465
  function normalizeOptionalText(value) {
466
466
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
467
467
  }
468
+ function normalizePossiblyEmptyText(value) {
469
+ return typeof value === "string" ? value.trim() : void 0;
470
+ }
468
471
  function normalizeOptionalNumber(value) {
469
472
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
470
473
  }
@@ -1342,7 +1345,7 @@ async function runTranscriptionWorkflow(params) {
1342
1345
  return { ok: false, error: result.error };
1343
1346
  }
1344
1347
  const title = normalizeOptionalText2(result.summary) ? normalizeOptionalText2(result.summary) : extractSummary(result.text ?? "");
1345
- const summary = normalizeOptionalText2(result.summaryText);
1348
+ const summary = result.summaryText ?? "";
1346
1349
  result.summary = title;
1347
1350
  const transcriptData = buildTranscriptDocument({
1348
1351
  recordingId,
@@ -1603,16 +1606,11 @@ function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1603
1606
  );
1604
1607
  const listResult = extractLongRecordingTextFromList(sourceTextList);
1605
1608
  const sourceText = normalizeOptionalText2(data?.recordResult?.sourceText);
1606
- const summaryText = normalizeOptionalText2(data?.recordResult?.summaryResult);
1609
+ const summaryText = normalizeOptionalText2(data?.recordResult?.summaryResult) ?? "";
1607
1610
  const title = normalizeOptionalText2(data?.recordResult?.title);
1608
1611
  const category = normalizeOptionalText2(data?.recordResult?.category);
1609
1612
  const text = listResult.text ?? sourceText ?? summaryText;
1610
- if (!text) {
1611
- return {
1612
- ok: false,
1613
- error: "Model Proxy ASR \u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u8FD4\u56DE sourceTextList\u3001sourceText \u6216 summaryResult"
1614
- };
1615
- }
1613
+ const status = normalizeLongRecordingStatus(data?.status) ?? "SUCCEEDED";
1616
1614
  if (!listResult.text && !sourceText && summaryText) {
1617
1615
  logger.warn(
1618
1616
  `[asr] Model Proxy \u957F\u5F55\u97F3\u7ED3\u679C\u7F3A\u5C11 sourceTextList/sourceText\uFF0C\u5DF2\u56DE\u9000\u4F7F\u7528 summaryResult \u4F5C\u4E3A\u8F6C\u5199\u6587\u672C: taskId=${taskId}`
@@ -1632,7 +1630,7 @@ function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1632
1630
  provider: "model-proxy",
1633
1631
  taskId,
1634
1632
  requestId,
1635
- status: "SUCCEEDED"
1633
+ status
1636
1634
  },
1637
1635
  rawResponse: data
1638
1636
  };
@@ -5424,7 +5422,7 @@ function readBuildInjectedVersion() {
5424
5422
  if (false) {
5425
5423
  return void 0;
5426
5424
  }
5427
- const version = "1.11.0-beta.0".trim();
5425
+ const version = "1.11.0-beta.2".trim();
5428
5426
  return version || void 0;
5429
5427
  }
5430
5428
  function readPluginVersionFromPackageJson() {
@@ -5549,1103 +5547,787 @@ function formatLocalTimestamp(d) {
5549
5547
  return `${y}-${m}-${day}T${hh}:${mm}:${ss}.${ms}${sign}${offsetHours}:${offsetMins}`;
5550
5548
  }
5551
5549
 
5552
- // src/notification/storage.ts
5550
+ // src/light/repeat.ts
5551
+ function normalizeRepeatTimes(input) {
5552
+ if (typeof input === "boolean") {
5553
+ return input ? 0 : 1;
5554
+ }
5555
+ if (typeof input === "number") {
5556
+ return validateRepeatTimes(input);
5557
+ }
5558
+ if (!input) {
5559
+ return 1;
5560
+ }
5561
+ if (input.repeat_times !== void 0) {
5562
+ return validateRepeatTimes(input.repeat_times);
5563
+ }
5564
+ if (input.repeat !== void 0) {
5565
+ return input.repeat ? 0 : 1;
5566
+ }
5567
+ return 1;
5568
+ }
5569
+ function assertAncsRepeatTimes(repeatTimes) {
5570
+ if (repeatTimes !== 0 && repeatTimes !== 1) {
5571
+ throw new Error(
5572
+ "\u5F53\u524D ANCS \u8DEF\u5F84\u4EC5\u652F\u6301 repeat_times=0\uFF08\u65E0\u9650\u5FAA\u73AF\uFF09\u6216 1\uFF08\u64AD\u653E\u4E00\u8F6E\uFF09\uFF1BN>=2 \u9700\u975E ANCS \u8DEF\u5F84"
5573
+ );
5574
+ }
5575
+ }
5576
+ function validateRepeatTimes(value) {
5577
+ if (!Number.isInteger(value) || value < 0) {
5578
+ throw new Error("repeat_times \u5FC5\u987B\u662F >=0 \u7684\u6574\u6570");
5579
+ }
5580
+ return value;
5581
+ }
5582
+
5583
+ // src/cli/helpers.ts
5553
5584
  var import_node_fs3 = require("fs");
5554
- var import_node_crypto = require("crypto");
5585
+ var import_promises = require("fs/promises");
5555
5586
  var import_node_path2 = require("path");
5556
- var NOTIFICATION_DIR_NAME = "notifications";
5557
- var ID_INDEX_DIR_NAME = ".ids";
5558
- var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
5559
- function getStateFallbackNotificationDir(stateDir) {
5560
- return (0, import_node_path2.join)(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
5587
+ function resolveNotificationsDir(ctx) {
5588
+ if (ctx.stateDir) {
5589
+ const dir = (0, import_node_path2.join)(
5590
+ ctx.stateDir,
5591
+ "plugins",
5592
+ "phone-notifications",
5593
+ "notifications"
5594
+ );
5595
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5596
+ }
5597
+ if (ctx.workspaceDir) {
5598
+ const dir = (0, import_node_path2.join)(ctx.workspaceDir, "notifications");
5599
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5600
+ }
5601
+ return null;
5561
5602
  }
5562
- function ensureWritableDirectory(dir) {
5603
+ function listDateKeys(dir) {
5604
+ const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5605
+ const keys = [];
5606
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
5607
+ if (!entry.isFile()) continue;
5608
+ const m = pattern.exec(entry.name);
5609
+ if (m) keys.push(m[1]);
5610
+ }
5611
+ return keys.sort().reverse();
5612
+ }
5613
+ function readDateFile(dir, dateKey) {
5614
+ const filePath = (0, import_node_path2.join)(dir, `${dateKey}.json`);
5615
+ if (!(0, import_node_fs3.existsSync)(filePath)) return [];
5563
5616
  try {
5564
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
5565
- (0, import_node_fs3.accessSync)(dir, import_node_fs3.constants.R_OK | import_node_fs3.constants.W_OK);
5566
- return true;
5617
+ return JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf-8"));
5567
5618
  } catch {
5568
- return false;
5619
+ return [];
5569
5620
  }
5570
5621
  }
5571
- function resolveNotificationStorageDir(ctx, logger) {
5572
- const stateNotifDir = getStateFallbackNotificationDir(ctx.stateDir);
5573
- if (ensureWritableDirectory(stateNotifDir)) {
5574
- logger.info(`\u901A\u77E5\u5C06\u5199\u5165 stateDir \u8DEF\u5F84: ${stateNotifDir}`);
5575
- return stateNotifDir;
5622
+ function today() {
5623
+ return formatDate2(/* @__PURE__ */ new Date());
5624
+ }
5625
+ function formatDate2(d) {
5626
+ const y = d.getFullYear();
5627
+ const m = String(d.getMonth() + 1).padStart(2, "0");
5628
+ const day = String(d.getDate()).padStart(2, "0");
5629
+ return `${y}-${m}-${day}`;
5630
+ }
5631
+ function daysAgo(n) {
5632
+ const d = /* @__PURE__ */ new Date();
5633
+ d.setDate(d.getDate() - n);
5634
+ return formatDate2(d);
5635
+ }
5636
+ function filterDateRange(keys, from, to) {
5637
+ return keys.filter((k) => k >= from && k <= to);
5638
+ }
5639
+ function parseIsoTime(value, optionName) {
5640
+ const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
5641
+ if (!isoPattern.test(value)) {
5642
+ exitError(
5643
+ "INVALID_TIME",
5644
+ `${optionName} \u5FC5\u987B\u662F ISO 8601 \u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5645
+ );
5646
+ }
5647
+ const ts = Date.parse(value);
5648
+ if (Number.isNaN(ts)) {
5649
+ exitError(
5650
+ "INVALID_TIME",
5651
+ `${optionName} \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5652
+ );
5653
+ }
5654
+ return ts;
5655
+ }
5656
+ function sortNotificationsByTimestampDesc(items) {
5657
+ return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
5658
+ }
5659
+ async function listDateKeysAsync(dir) {
5660
+ const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5661
+ const keys = [];
5662
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
5663
+ for (const entry of entries) {
5664
+ if (!entry.isFile()) continue;
5665
+ const m = pattern.exec(entry.name);
5666
+ if (m) keys.push(m[1]);
5667
+ }
5668
+ return keys.sort().reverse();
5669
+ }
5670
+ async function readDateFileAsync(dir, dateKey) {
5671
+ const filePath = (0, import_node_path2.join)(dir, `${dateKey}.json`);
5672
+ if (!(0, import_node_fs3.existsSync)(filePath)) return [];
5673
+ try {
5674
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
5675
+ return JSON.parse(content);
5676
+ } catch {
5677
+ return [];
5678
+ }
5679
+ }
5680
+ function progress(message) {
5681
+ process.stderr.write(message + "\n");
5682
+ }
5683
+ function output(data) {
5684
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
5685
+ }
5686
+ function exitError(code, message) {
5687
+ output({ ok: false, error: { code, message } });
5688
+ process.exit(1);
5689
+ }
5690
+ function resolveRecordingsDir(ctx) {
5691
+ if (ctx.stateDir) {
5692
+ const dir = (0, import_node_path2.join)(
5693
+ ctx.stateDir,
5694
+ "plugins",
5695
+ "phone-notifications",
5696
+ "recordings"
5697
+ );
5698
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5576
5699
  }
5577
5700
  if (ctx.workspaceDir) {
5578
- const workspaceDir = (0, import_node_path2.join)(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
5579
- if (ensureWritableDirectory(workspaceDir)) {
5580
- logger.warn(
5581
- `stateDir \u4E0D\u53EF\u7528\uFF0C\u901A\u77E5\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${workspaceDir}`
5582
- );
5583
- return workspaceDir;
5584
- }
5701
+ const dir = (0, import_node_path2.join)(ctx.workspaceDir, "recordings");
5702
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5585
5703
  }
5586
- throw new Error(`\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528: ${stateNotifDir}`);
5704
+ return null;
5587
5705
  }
5588
- var NotificationStorage = class {
5589
- constructor(dir, config, logger, resolveDisplayName) {
5590
- this.config = config;
5591
- this.logger = logger;
5592
- this.dir = dir;
5593
- this.idIndexDir = (0, import_node_path2.join)(dir, ID_INDEX_DIR_NAME);
5594
- this.contentKeyIndexDir = (0, import_node_path2.join)(dir, CONTENT_KEY_INDEX_DIR_NAME);
5595
- this.resolveDisplayName = resolveDisplayName;
5706
+ function resolveAsrConfigPath(ctx) {
5707
+ let base;
5708
+ if (ctx.stateDir) {
5709
+ base = (0, import_node_path2.join)(ctx.stateDir, "plugins", "phone-notifications", "recordings");
5710
+ } else if (ctx.workspaceDir) {
5711
+ base = (0, import_node_path2.join)(ctx.workspaceDir, "recordings");
5712
+ } else {
5713
+ exitError(
5714
+ "STORAGE_UNAVAILABLE",
5715
+ "\u65E0\u6CD5\u786E\u5B9A\u5F55\u97F3\u5B58\u50A8\u76EE\u5F55\uFF1AstateDir \u548C workspaceDir \u5747\u672A\u8BBE\u7F6E"
5716
+ );
5596
5717
  }
5597
- dir;
5598
- idIndexDir;
5599
- contentKeyIndexDir;
5600
- idCache = /* @__PURE__ */ new Map();
5601
- contentKeyCache = /* @__PURE__ */ new Map();
5602
- dateWriteChains = /* @__PURE__ */ new Map();
5603
- resolveDisplayName;
5604
- async init() {
5605
- (0, import_node_fs3.mkdirSync)(this.dir, { recursive: true });
5606
- (0, import_node_fs3.mkdirSync)(this.idIndexDir, { recursive: true });
5607
- (0, import_node_fs3.rmSync)(this.contentKeyIndexDir, { recursive: true, force: true });
5608
- (0, import_node_fs3.mkdirSync)(this.contentKeyIndexDir, { recursive: true });
5718
+ (0, import_node_fs3.mkdirSync)(base, { recursive: true });
5719
+ return (0, import_node_path2.join)(base, "asr-config.json");
5720
+ }
5721
+ function readRecordingIndex(dir) {
5722
+ const indexPath = (0, import_node_path2.join)(dir, "index.json");
5723
+ if (!(0, import_node_fs3.existsSync)(indexPath)) return [];
5724
+ try {
5725
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(indexPath, "utf-8"));
5726
+ return Array.isArray(raw?.recordings) ? raw.recordings : [];
5727
+ } catch {
5728
+ return [];
5609
5729
  }
5610
- async ingest(items) {
5611
- const result = {
5612
- received: items.length,
5613
- ingested: 0,
5614
- dedupedById: 0,
5615
- dedupedByContent: 0,
5616
- invalid: 0,
5617
- inserted: []
5730
+ }
5731
+
5732
+ // src/light/validators.ts
5733
+ var VALID_MODES = [
5734
+ "wave",
5735
+ "breath",
5736
+ "strobe",
5737
+ "steady",
5738
+ "wave_rainbow",
5739
+ "pixel_frame"
5740
+ ];
5741
+ var MAX_SEGMENTS = 12;
5742
+ function validateSegments(segments) {
5743
+ if (!Array.isArray(segments)) {
5744
+ return { valid: false, errors: [{ field: "segments", message: "\u5FC5\u987B\u662F\u6570\u7EC4" }] };
5745
+ }
5746
+ if (segments.length === 0) {
5747
+ return { valid: false, errors: [{ field: "segments", message: "\u4E0D\u80FD\u4E3A\u7A7A" }] };
5748
+ }
5749
+ if (segments.length > MAX_SEGMENTS) {
5750
+ return {
5751
+ valid: false,
5752
+ errors: [{ field: "segments", message: `\u6700\u591A ${MAX_SEGMENTS} \u6BB5` }]
5618
5753
  };
5619
- for (const n of items) {
5620
- const outcome = await this.writeNotification(n);
5621
- switch (outcome.kind) {
5622
- case "ingested":
5623
- result.ingested += 1;
5624
- result.inserted.push(outcome.entry);
5625
- break;
5626
- case "dedupedById":
5627
- result.dedupedById += 1;
5628
- break;
5629
- case "dedupedByContent":
5630
- result.dedupedByContent += 1;
5631
- break;
5632
- case "invalid":
5633
- result.invalid += 1;
5634
- break;
5635
- }
5636
- }
5637
- this.prune();
5638
- return result;
5639
5754
  }
5640
- async writeNotification(n) {
5641
- const ts = new Date(n.timestamp);
5642
- if (Number.isNaN(ts.getTime())) {
5643
- this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
5644
- return { kind: "invalid" };
5645
- }
5646
- const dateKey = this.formatDate(ts);
5647
- const filePath = (0, import_node_path2.join)(this.dir, `${dateKey}.json`);
5648
- const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
5649
- const entry = this.buildStoredNotification(n);
5650
- return this.withDateWriteLock(dateKey, async () => {
5651
- if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
5652
- return { kind: "dedupedById" };
5653
- }
5654
- if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
5655
- return { kind: "dedupedByContent" };
5656
- }
5657
- const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
5658
- const storedEntry = {
5659
- ...entry,
5660
- appDisplayName
5661
- };
5662
- const arr = this.readStoredNotifications(filePath);
5663
- arr.push(storedEntry);
5664
- (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(arr, null, 2), "utf-8");
5665
- if (normalizedId) {
5666
- this.recordNotificationId(dateKey, normalizedId);
5667
- }
5668
- this.recordNotificationContentKey(dateKey, filePath, storedEntry);
5669
- return { kind: "ingested", entry: storedEntry };
5670
- });
5755
+ const errors = [];
5756
+ for (let i = 0; i < segments.length; i++) {
5757
+ validateSegment(segments[i], `segments[${i}]`, errors);
5671
5758
  }
5672
- buildStoredNotification(n) {
5673
- return {
5674
- appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
5675
- title: typeof n.title === "string" ? n.title : "",
5676
- content: this.buildContent(n),
5677
- timestamp: n.timestamp
5678
- };
5759
+ if (errors.length > 0) return { valid: false, errors };
5760
+ return { valid: true, segments };
5761
+ }
5762
+ function parseAndValidateSegments(json) {
5763
+ let parsed;
5764
+ try {
5765
+ parsed = JSON.parse(json);
5766
+ } catch {
5767
+ exitError("VALIDATION_FAILED", "segments \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON");
5679
5768
  }
5680
- buildContent(n) {
5681
- const body = n.body?.trim();
5682
- if (body) {
5683
- return body;
5684
- }
5685
- const fallback = [];
5686
- if (n.category) {
5687
- fallback.push(`category:${n.category}`);
5688
- }
5689
- if (n.metadata && Object.keys(n.metadata).length > 0) {
5690
- fallback.push(`metadata:${JSON.stringify(n.metadata)}`);
5691
- }
5692
- return fallback.join(" ; ") || "-";
5769
+ const result = validateSegments(parsed);
5770
+ if (!result.valid) {
5771
+ output({ ok: false, error: { code: "VALIDATION_FAILED", details: result.errors } });
5772
+ process.exit(1);
5693
5773
  }
5694
- formatDate(d) {
5695
- const year = d.getFullYear();
5696
- const month = String(d.getMonth() + 1).padStart(2, "0");
5697
- const day = String(d.getDate()).padStart(2, "0");
5698
- return `${year}-${month}-${day}`;
5774
+ return result.segments;
5775
+ }
5776
+ function validateSegment(seg, prefix, errors) {
5777
+ if (!isRecord(seg)) {
5778
+ errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5779
+ return;
5699
5780
  }
5700
- getIdIndexPath(dateKey) {
5701
- return (0, import_node_path2.join)(this.idIndexDir, `${dateKey}.ids`);
5781
+ const mode = seg.mode;
5782
+ if (!VALID_MODES.includes(mode)) {
5783
+ errors.push({
5784
+ field: `${prefix}.mode`,
5785
+ message: `\u4E0D\u652F\u6301\u7684\u6A21\u5F0F '${String(mode)}'\uFF0C\u53EF\u9009\uFF1A${VALID_MODES.join("/")}`
5786
+ });
5702
5787
  }
5703
- getIdSet(dateKey) {
5704
- const cached = this.idCache.get(dateKey);
5705
- if (cached) {
5706
- return cached;
5707
- }
5708
- const idPath = this.getIdIndexPath(dateKey);
5709
- const ids = /* @__PURE__ */ new Set();
5710
- if ((0, import_node_fs3.existsSync)(idPath)) {
5711
- const lines = (0, import_node_fs3.readFileSync)(idPath, "utf-8").split(/\r?\n/);
5712
- for (const line of lines) {
5713
- const id = line.trim();
5714
- if (id) {
5715
- ids.add(id);
5716
- }
5788
+ validateNonNegativeNumber(seg.duration_s, `${prefix}.duration_s`, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57\uFF080 \u8868\u793A\u65E0\u9650\u65F6\u957F\uFF09");
5789
+ switch (mode) {
5790
+ case "wave":
5791
+ validateForegroundSegment(seg, prefix, errors);
5792
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5793
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5794
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5795
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5796
+ break;
5797
+ case "wave_rainbow":
5798
+ validateForegroundSegment(seg, prefix, errors);
5799
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5800
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5801
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5802
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5803
+ if (!hasNonZeroRgb(seg.color) && !hasNonZeroRgb(seg.background)) {
5804
+ errors.push({
5805
+ field: prefix,
5806
+ message: "wave_rainbow \u81F3\u5C11\u9700\u8981\u4E00\u7EC4\u975E\u96F6\u989C\u8272\u951A\u70B9\uFF08color \u6216 background\uFF09"
5807
+ });
5717
5808
  }
5718
- }
5719
- this.idCache.set(dateKey, ids);
5720
- return ids;
5809
+ break;
5810
+ case "breath":
5811
+ validateForegroundSegment(seg, prefix, errors);
5812
+ validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
5813
+ break;
5814
+ case "strobe":
5815
+ validateForegroundSegment(seg, prefix, errors);
5816
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5817
+ break;
5818
+ case "steady":
5819
+ validateForegroundSegment(seg, prefix, errors);
5820
+ break;
5821
+ case "pixel_frame":
5822
+ validatePixelFrame(seg.pixels, `${prefix}.pixels`, errors);
5823
+ break;
5824
+ default:
5825
+ validateOptionalNonNegativeNumber(seg.brightness, `${prefix}.brightness`, errors);
5826
+ validateOptionalColor(seg.color, `${prefix}.color`, errors);
5827
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5828
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5829
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5830
+ validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
5831
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5721
5832
  }
5722
- hasNotificationId(dateKey, id) {
5723
- return this.getIdSet(dateKey).has(id);
5833
+ }
5834
+ function validateForegroundSegment(seg, prefix, errors) {
5835
+ validateNumberInRange(
5836
+ seg.brightness,
5837
+ `${prefix}.brightness`,
5838
+ errors,
5839
+ 0,
5840
+ 255,
5841
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5842
+ );
5843
+ validateColor(seg.color, `${prefix}.color`, errors);
5844
+ if (seg.mode !== "steady" && seg.brightness === 0) {
5845
+ errors.push({
5846
+ field: `${prefix}.brightness`,
5847
+ message: "brightness=0 \u4EC5 steady \u6A21\u5F0F\u5141\u8BB8\uFF1B\u5176\u5B83\u6A21\u5F0F\u4F1A\u5728\u56FA\u4EF6\u4FA7\u88AB\u8FC7\u6EE4"
5848
+ });
5724
5849
  }
5725
- getContentKeyIndexPath(dateKey) {
5726
- return (0, import_node_path2.join)(this.contentKeyIndexDir, `${dateKey}.keys`);
5850
+ }
5851
+ function validatePixelFrame(value, field, errors) {
5852
+ if (!Array.isArray(value)) {
5853
+ errors.push({ field, message: "pixel_frame \u5FC5\u987B\u63D0\u4F9B pixels \u6570\u7EC4\uFF081\u20137 \u9879\uFF09" });
5854
+ return;
5727
5855
  }
5728
- getContentKeySet(dateKey, filePath) {
5729
- const cached = this.contentKeyCache.get(dateKey);
5730
- if (cached) {
5731
- return cached;
5732
- }
5733
- const keyPath = this.getContentKeyIndexPath(dateKey);
5734
- const keys = /* @__PURE__ */ new Set();
5735
- if ((0, import_node_fs3.existsSync)(filePath)) {
5736
- for (const item of this.readStoredNotifications(filePath)) {
5737
- keys.add(this.buildNotificationContentKey(item));
5738
- }
5856
+ if (value.length < 1 || value.length > 7) {
5857
+ errors.push({ field, message: "pixels \u5FC5\u987B\u4E3A 1\u20137 \u9879" });
5858
+ }
5859
+ const seen = /* @__PURE__ */ new Set();
5860
+ for (let i = 0; i < value.length; i++) {
5861
+ const pixel = value[i];
5862
+ const prefix = `${field}[${i}]`;
5863
+ if (!isRecord(pixel)) {
5864
+ errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5865
+ continue;
5739
5866
  }
5740
- if (keys.size > 0) {
5741
- (0, import_node_fs3.writeFileSync)(keyPath, `${Array.from(keys).join("\n")}
5742
- `, "utf-8");
5743
- } else if ((0, import_node_fs3.existsSync)(keyPath)) {
5744
- (0, import_node_fs3.rmSync)(keyPath, { force: true });
5867
+ const idx = pixel.index;
5868
+ if (!Number.isInteger(idx) || idx < 0 || idx > 6) {
5869
+ errors.push({ field: `${prefix}.index`, message: "index \u5FC5\u987B\u662F 0\u20136 \u7684\u6574\u6570" });
5870
+ } else if (seen.has(idx)) {
5871
+ errors.push({ field: `${prefix}.index`, message: `index=${idx} \u91CD\u590D` });
5872
+ } else {
5873
+ seen.add(idx);
5745
5874
  }
5746
- this.contentKeyCache.set(dateKey, keys);
5747
- return keys;
5748
- }
5749
- hasNotificationContentKey(dateKey, filePath, entry) {
5750
- return this.getContentKeySet(dateKey, filePath).has(
5751
- this.buildNotificationContentKey(entry)
5875
+ validateNumberInRange(
5876
+ pixel.brightness,
5877
+ `${prefix}.brightness`,
5878
+ errors,
5879
+ 0,
5880
+ 255,
5881
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5752
5882
  );
5883
+ validateColor(pixel.color, `${prefix}.color`, errors);
5753
5884
  }
5754
- recordNotificationId(dateKey, id) {
5755
- const ids = this.getIdSet(dateKey);
5756
- if (ids.has(id)) {
5757
- return;
5758
- }
5759
- (0, import_node_fs3.appendFileSync)(this.getIdIndexPath(dateKey), `${id}
5760
- `, "utf-8");
5761
- ids.add(id);
5885
+ }
5886
+ function validateOptionalBreathTiming(value, field, errors) {
5887
+ if (value === void 0) return;
5888
+ if (!isRecord(value)) {
5889
+ errors.push({ field, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5890
+ return;
5762
5891
  }
5763
- recordNotificationContentKey(dateKey, filePath, entry) {
5764
- const keys = this.getContentKeySet(dateKey, filePath);
5765
- const key = this.buildNotificationContentKey(entry);
5766
- if (keys.has(key)) {
5767
- return;
5768
- }
5769
- (0, import_node_fs3.appendFileSync)(this.getContentKeyIndexPath(dateKey), `${key}
5770
- `, "utf-8");
5771
- keys.add(key);
5772
- }
5773
- buildNotificationContentKey(entry) {
5774
- return (0, import_node_crypto.createHash)("sha256").update(entry.appName).update("").update(entry.title).update("").update(entry.content).update("").update(entry.timestamp).digest("hex");
5775
- }
5776
- readStoredNotifications(filePath) {
5777
- if (!(0, import_node_fs3.existsSync)(filePath)) {
5778
- return [];
5779
- }
5780
- try {
5781
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf-8"));
5782
- return Array.isArray(parsed) ? parsed : [];
5783
- } catch {
5784
- return [];
5785
- }
5786
- }
5787
- async withDateWriteLock(dateKey, task) {
5788
- const previous = this.dateWriteChains.get(dateKey) ?? Promise.resolve();
5789
- let release;
5790
- const current = new Promise((resolve) => {
5791
- release = resolve;
5792
- });
5793
- const chain = previous.then(() => current);
5794
- this.dateWriteChains.set(dateKey, chain);
5795
- await previous;
5796
- try {
5797
- return await task();
5798
- } finally {
5799
- release();
5800
- if (this.dateWriteChains.get(dateKey) === chain) {
5801
- this.dateWriteChains.delete(dateKey);
5802
- }
5803
- }
5804
- }
5805
- prune() {
5806
- const retentionDays = this.config.retentionDays;
5807
- if (retentionDays === void 0) {
5808
- return;
5809
- }
5810
- const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
5811
- const cutoffDate = this.formatDate(new Date(cutoffMs));
5812
- this.pruneDataFiles(cutoffDate);
5813
- this.pruneIdIndex(cutoffDate);
5814
- this.pruneContentKeyIndex(cutoffDate);
5815
- }
5816
- /** Remove expired .json, legacy .md files, and legacy date directories */
5817
- pruneDataFiles(cutoffDate) {
5818
- const dateFilePattern = /^(\d{4}-\d{2}-\d{2})\.(json|md)$/;
5819
- const dateDirPattern = /^\d{4}-\d{2}-\d{2}$/;
5820
- try {
5821
- for (const entry of (0, import_node_fs3.readdirSync)(this.dir, { withFileTypes: true })) {
5822
- if (entry.isFile()) {
5823
- const match = dateFilePattern.exec(entry.name);
5824
- if (match && match[1] < cutoffDate) {
5825
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.dir, entry.name), { force: true });
5826
- }
5827
- } else if (entry.isDirectory() && dateDirPattern.test(entry.name) && entry.name < cutoffDate) {
5828
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.dir, entry.name), { recursive: true, force: true });
5829
- }
5830
- }
5831
- } catch {
5832
- }
5833
- }
5834
- /** Remove expired .ids index files */
5835
- pruneIdIndex(cutoffDate) {
5836
- try {
5837
- for (const entry of (0, import_node_fs3.readdirSync)(this.idIndexDir, { withFileTypes: true })) {
5838
- if (!entry.isFile()) continue;
5839
- const match = /^(\d{4}-\d{2}-\d{2})\.ids$/.exec(entry.name);
5840
- if (match && match[1] < cutoffDate) {
5841
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.idIndexDir, entry.name), { force: true });
5842
- this.idCache.delete(match[1]);
5843
- }
5844
- }
5845
- } catch {
5846
- }
5847
- }
5848
- pruneContentKeyIndex(cutoffDate) {
5849
- try {
5850
- for (const entry of (0, import_node_fs3.readdirSync)(this.contentKeyIndexDir, { withFileTypes: true })) {
5851
- if (!entry.isFile()) continue;
5852
- const match = /^(\d{4}-\d{2}-\d{2})\.keys$/.exec(entry.name);
5853
- if (match && match[1] < cutoffDate) {
5854
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.contentKeyIndexDir, entry.name), { force: true });
5855
- this.contentKeyCache.delete(match[1]);
5856
- }
5857
- }
5858
- } catch {
5859
- }
5860
- }
5861
- async close() {
5862
- this.idCache.clear();
5863
- this.contentKeyCache.clear();
5864
- this.dateWriteChains.clear();
5865
- }
5866
- };
5867
-
5868
- // src/light/repeat.ts
5869
- function normalizeRepeatTimes(input) {
5870
- if (typeof input === "boolean") {
5871
- return input ? 0 : 1;
5872
- }
5873
- if (typeof input === "number") {
5874
- return validateRepeatTimes(input);
5875
- }
5876
- if (!input) {
5877
- return 1;
5878
- }
5879
- if (input.repeat_times !== void 0) {
5880
- return validateRepeatTimes(input.repeat_times);
5881
- }
5882
- if (input.repeat !== void 0) {
5883
- return input.repeat ? 0 : 1;
5884
- }
5885
- return 1;
5886
- }
5887
- function assertAncsRepeatTimes(repeatTimes) {
5888
- if (repeatTimes !== 0 && repeatTimes !== 1) {
5889
- throw new Error(
5890
- "\u5F53\u524D ANCS \u8DEF\u5F84\u4EC5\u652F\u6301 repeat_times=0\uFF08\u65E0\u9650\u5FAA\u73AF\uFF09\u6216 1\uFF08\u64AD\u653E\u4E00\u8F6E\uFF09\uFF1BN>=2 \u9700\u975E ANCS \u8DEF\u5F84"
5891
- );
5892
+ validatePositiveNumber(
5893
+ value.rise_ms,
5894
+ `${field}.rise_ms`,
5895
+ errors,
5896
+ "rise_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
5897
+ );
5898
+ validateNonNegativeNumber(
5899
+ value.hold_ms,
5900
+ `${field}.hold_ms`,
5901
+ errors,
5902
+ "hold_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
5903
+ );
5904
+ validatePositiveNumber(
5905
+ value.fall_ms,
5906
+ `${field}.fall_ms`,
5907
+ errors,
5908
+ "fall_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
5909
+ );
5910
+ validateNonNegativeNumber(
5911
+ value.off_ms,
5912
+ `${field}.off_ms`,
5913
+ errors,
5914
+ "off_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
5915
+ );
5916
+ }
5917
+ function validateOptionalBackground(value, field, errors) {
5918
+ if (value === void 0) return;
5919
+ if (!isRecord(value)) {
5920
+ errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b/brightness \u6570\u503C" });
5921
+ return;
5892
5922
  }
5923
+ validateColor(value, field, errors);
5924
+ validateNumberInRange(
5925
+ value.brightness,
5926
+ `${field}.brightness`,
5927
+ errors,
5928
+ 0,
5929
+ 255,
5930
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5931
+ );
5893
5932
  }
5894
- function validateRepeatTimes(value) {
5895
- if (!Number.isInteger(value) || value < 0) {
5896
- throw new Error("repeat_times \u5FC5\u987B\u662F >=0 \u7684\u6574\u6570");
5897
- }
5898
- return value;
5933
+ function validateOptionalColor(value, field, errors) {
5934
+ if (value === void 0) return;
5935
+ validateColor(value, field, errors);
5899
5936
  }
5900
-
5901
- // src/cli/helpers.ts
5902
- var import_node_fs4 = require("fs");
5903
- var import_promises = require("fs/promises");
5904
- var import_node_path3 = require("path");
5905
- function resolveNotificationsDir(ctx) {
5906
- if (ctx.stateDir) {
5907
- const dir = (0, import_node_path3.join)(
5908
- ctx.stateDir,
5909
- "plugins",
5910
- "phone-notifications",
5911
- "notifications"
5912
- );
5913
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
5914
- }
5915
- if (ctx.workspaceDir) {
5916
- const dir = (0, import_node_path3.join)(ctx.workspaceDir, "notifications");
5917
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
5937
+ function validateColor(value, field, errors) {
5938
+ if (!isRecord(value)) {
5939
+ errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b \u6570\u503C" });
5940
+ return;
5918
5941
  }
5919
- return null;
5942
+ validateNumberInRange(value.r, `${field}.r`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5943
+ validateNumberInRange(value.g, `${field}.g`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5944
+ validateNumberInRange(value.b, `${field}.b`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5920
5945
  }
5921
- function listDateKeys(dir) {
5922
- const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5923
- const keys = [];
5924
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
5925
- if (!entry.isFile()) continue;
5926
- const m = pattern.exec(entry.name);
5927
- if (m) keys.push(m[1]);
5946
+ function validateOptionalDirection(value, field, errors) {
5947
+ if (value === void 0) return;
5948
+ if (value !== "ltr" && value !== "rtl") {
5949
+ errors.push({ field, message: "direction \u5FC5\u987B\u662F ltr \u6216 rtl" });
5928
5950
  }
5929
- return keys.sort().reverse();
5930
5951
  }
5931
- function readDateFile(dir, dateKey) {
5932
- const filePath = (0, import_node_path3.join)(dir, `${dateKey}.json`);
5933
- if (!(0, import_node_fs4.existsSync)(filePath)) return [];
5934
- try {
5935
- return JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf-8"));
5936
- } catch {
5937
- return [];
5952
+ function validateOptionalWindow(value, field, errors) {
5953
+ if (value === void 0) return;
5954
+ if (value !== 1 && value !== 2 && value !== 3) {
5955
+ errors.push({ field, message: "window \u4EC5\u652F\u6301 1/2/3" });
5938
5956
  }
5939
5957
  }
5940
- function today() {
5941
- return formatDate2(/* @__PURE__ */ new Date());
5942
- }
5943
- function formatDate2(d) {
5944
- const y = d.getFullYear();
5945
- const m = String(d.getMonth() + 1).padStart(2, "0");
5946
- const day = String(d.getDate()).padStart(2, "0");
5947
- return `${y}-${m}-${day}`;
5948
- }
5949
- function daysAgo(n) {
5950
- const d = /* @__PURE__ */ new Date();
5951
- d.setDate(d.getDate() - n);
5952
- return formatDate2(d);
5953
- }
5954
- function filterDateRange(keys, from, to) {
5955
- return keys.filter((k) => k >= from && k <= to);
5958
+ function validateOptionalNonNegativeNumber(value, field, errors) {
5959
+ if (value === void 0) return;
5960
+ validateNonNegativeNumber(value, field, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57");
5956
5961
  }
5957
- function parseIsoTime(value, optionName) {
5958
- const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
5959
- if (!isoPattern.test(value)) {
5960
- exitError(
5961
- "INVALID_TIME",
5962
- `${optionName} \u5FC5\u987B\u662F ISO 8601 \u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5963
- );
5964
- }
5965
- const ts = Date.parse(value);
5966
- if (Number.isNaN(ts)) {
5967
- exitError(
5968
- "INVALID_TIME",
5969
- `${optionName} \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5970
- );
5962
+ function validatePositiveNumber(value, field, errors, message) {
5963
+ if (value === void 0) return;
5964
+ if (!isFiniteNumber(value) || value <= 0) {
5965
+ errors.push({ field, message });
5971
5966
  }
5972
- return ts;
5973
- }
5974
- function sortNotificationsByTimestampDesc(items) {
5975
- return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
5976
5967
  }
5977
- async function listDateKeysAsync(dir) {
5978
- const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5979
- const keys = [];
5980
- const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
5981
- for (const entry of entries) {
5982
- if (!entry.isFile()) continue;
5983
- const m = pattern.exec(entry.name);
5984
- if (m) keys.push(m[1]);
5968
+ function validateNonNegativeNumber(value, field, errors, message) {
5969
+ if (!isFiniteNumber(value) || value < 0) {
5970
+ errors.push({ field, message });
5985
5971
  }
5986
- return keys.sort().reverse();
5987
5972
  }
5988
- async function readDateFileAsync(dir, dateKey) {
5989
- const filePath = (0, import_node_path3.join)(dir, `${dateKey}.json`);
5990
- if (!(0, import_node_fs4.existsSync)(filePath)) return [];
5991
- try {
5992
- const content = await (0, import_promises.readFile)(filePath, "utf-8");
5993
- return JSON.parse(content);
5994
- } catch {
5995
- return [];
5973
+ function validateNumberInRange(value, field, errors, min, max, message) {
5974
+ if (!isFiniteNumber(value) || value < min || value > max) {
5975
+ errors.push({ field, message });
5996
5976
  }
5997
5977
  }
5998
- function progress(message) {
5999
- process.stderr.write(message + "\n");
6000
- }
6001
- function output(data) {
6002
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
5978
+ function hasNonZeroRgb(value) {
5979
+ if (!value) return false;
5980
+ return [value.r, value.g, value.b].some((channel) => isFiniteNumber(channel) && channel > 0);
6003
5981
  }
6004
- function exitError(code, message) {
6005
- output({ ok: false, error: { code, message } });
6006
- process.exit(1);
5982
+ function isFiniteNumber(value) {
5983
+ return typeof value === "number" && Number.isFinite(value);
6007
5984
  }
6008
- function resolveRecordingsDir(ctx) {
6009
- if (ctx.stateDir) {
6010
- const dir = (0, import_node_path3.join)(
6011
- ctx.stateDir,
6012
- "plugins",
6013
- "phone-notifications",
6014
- "recordings"
6015
- );
6016
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
6017
- }
6018
- if (ctx.workspaceDir) {
6019
- const dir = (0, import_node_path3.join)(ctx.workspaceDir, "recordings");
6020
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
6021
- }
6022
- return null;
5985
+ function isRecord(value) {
5986
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6023
5987
  }
6024
- function resolveAsrConfigPath(ctx) {
6025
- let base;
6026
- if (ctx.stateDir) {
6027
- base = (0, import_node_path3.join)(ctx.stateDir, "plugins", "phone-notifications", "recordings");
6028
- } else if (ctx.workspaceDir) {
6029
- base = (0, import_node_path3.join)(ctx.workspaceDir, "recordings");
6030
- } else {
6031
- exitError(
6032
- "STORAGE_UNAVAILABLE",
6033
- "\u65E0\u6CD5\u786E\u5B9A\u5F55\u97F3\u5B58\u50A8\u76EE\u5F55\uFF1AstateDir \u548C workspaceDir \u5747\u672A\u8BBE\u7F6E"
6034
- );
6035
- }
6036
- (0, import_node_fs4.mkdirSync)(base, { recursive: true });
6037
- return (0, import_node_path3.join)(base, "asr-config.json");
5988
+
5989
+ // src/light-rules/storage.ts
5990
+ var import_node_fs4 = require("fs");
5991
+ var import_node_path3 = require("path");
5992
+
5993
+ // src/monitor/fetch-gen.ts
5994
+ function generateFetchPy(name, matchRules) {
5995
+ const appName = typeof matchRules.appName === "string" ? matchRules.appName : "";
5996
+ const senderKeywords = stringArray(matchRules.senderKeywords);
5997
+ const contentKeywords = stringArray(matchRules.contentKeywords);
5998
+ return `#!/usr/bin/env python3
5999
+ """Auto-generated fetch script for monitor task: ${name}"""
6000
+ import json, sys, os
6001
+ from pathlib import Path
6002
+
6003
+ def matches(notification: dict) -> bool:
6004
+ app = str(notification.get("appName", "") or "")
6005
+ title = str(notification.get("title", "") or "")
6006
+ content = str(notification.get("content", "") or "")
6007
+ body = str(notification.get("body", "") or "")
6008
+
6009
+ app_name = ${pyLiteral(appName)}
6010
+ if app_name and app != app_name:
6011
+ return False
6012
+
6013
+ sender_keywords = ${pyLiteral(senderKeywords)}
6014
+ sender_haystack = f"{title}\\n{content}\\n{body}"
6015
+ if sender_keywords and not any(keyword in sender_haystack for keyword in sender_keywords):
6016
+ return False
6017
+
6018
+ content_keywords = ${pyLiteral(contentKeywords)}
6019
+ content_haystack = f"{content}\\n{body}"
6020
+ if content_keywords and not any(keyword in content_haystack for keyword in content_keywords):
6021
+ return False
6022
+
6023
+ return True
6024
+
6025
+ def main():
6026
+ import argparse
6027
+ parser = argparse.ArgumentParser()
6028
+ parser.add_argument("--notifications-dir", required=True)
6029
+ args = parser.parse_args()
6030
+
6031
+ checkpoint_path = Path(__file__).parent / "checkpoint.json"
6032
+ checkpoint = {}
6033
+ if checkpoint_path.exists():
6034
+ checkpoint = json.loads(checkpoint_path.read_text())
6035
+
6036
+ ntf_dir = Path(args.notifications_dir)
6037
+ matched = []
6038
+ new_checkpoint = dict(checkpoint)
6039
+
6040
+ for f in sorted(ntf_dir.glob("*.json")):
6041
+ date_key = f.stem
6042
+ items = json.loads(f.read_text())
6043
+ last_idx = checkpoint.get(date_key, {}).get("lastIndex", -1)
6044
+ for i, item in enumerate(items):
6045
+ if i <= last_idx:
6046
+ continue
6047
+ if matches(item):
6048
+ matched.append(item)
6049
+ new_checkpoint[date_key] = {"lastIndex": len(items) - 1}
6050
+
6051
+ checkpoint_path.write_text(json.dumps(new_checkpoint, indent=2))
6052
+
6053
+ if not matched:
6054
+ print("NO_MATCH")
6055
+ return
6056
+
6057
+ for m in matched:
6058
+ print(json.dumps(m, ensure_ascii=False))
6059
+
6060
+ if __name__ == "__main__":
6061
+ main()
6062
+ `;
6038
6063
  }
6039
- function readRecordingIndex(dir) {
6040
- const indexPath = (0, import_node_path3.join)(dir, "index.json");
6041
- if (!(0, import_node_fs4.existsSync)(indexPath)) return [];
6042
- try {
6043
- const raw = JSON.parse((0, import_node_fs4.readFileSync)(indexPath, "utf-8"));
6044
- return Array.isArray(raw?.recordings) ? raw.recordings : [];
6045
- } catch {
6046
- return [];
6047
- }
6064
+ function stringArray(value) {
6065
+ if (!Array.isArray(value)) return [];
6066
+ return value.filter((item) => typeof item === "string" && item.length > 0);
6067
+ }
6068
+ function pyLiteral(value) {
6069
+ return JSON.stringify(value);
6048
6070
  }
6049
6071
 
6050
- // src/light/validators.ts
6051
- var VALID_MODES = [
6052
- "wave",
6053
- "breath",
6054
- "strobe",
6055
- "steady",
6056
- "wave_rainbow",
6057
- "pixel_frame"
6058
- ];
6059
- var MAX_SEGMENTS = 12;
6060
- function validateSegments(segments) {
6061
- if (!Array.isArray(segments)) {
6062
- return { valid: false, errors: [{ field: "segments", message: "\u5FC5\u987B\u662F\u6570\u7EC4" }] };
6063
- }
6064
- if (segments.length === 0) {
6065
- return { valid: false, errors: [{ field: "segments", message: "\u4E0D\u80FD\u4E3A\u7A7A" }] };
6072
+ // src/light-rules/storage.ts
6073
+ var LEGACY_DEFAULT_CRON_SCHEDULE = "*/5 * * * *";
6074
+ function legacyReadMatchRules(input) {
6075
+ return input.matchRules ?? {};
6076
+ }
6077
+ function legacyReadCronSchedule(input) {
6078
+ return input.cronSchedule ?? LEGACY_DEFAULT_CRON_SCHEDULE;
6079
+ }
6080
+ function legacyHasMatchRules(input) {
6081
+ return input.matchRules !== void 0;
6082
+ }
6083
+ function legacyHasCronSchedule(input) {
6084
+ return input.cronSchedule !== void 0;
6085
+ }
6086
+ function legacyAssignMatchRules(meta, matchRules) {
6087
+ meta.matchRules = matchRules;
6088
+ }
6089
+ function legacyAssignCronSchedule(meta, cronSchedule) {
6090
+ meta.cronSchedule = cronSchedule;
6091
+ }
6092
+ function legacyReadMetaMatchRules(meta) {
6093
+ return meta.matchRules ?? {};
6094
+ }
6095
+ function legacyReadMetaCronSchedule(meta) {
6096
+ return meta.cronSchedule;
6097
+ }
6098
+ function resolveBaseDir(ctx) {
6099
+ if (ctx.workspaceDir) return ctx.workspaceDir;
6100
+ if (ctx.stateDir) {
6101
+ const inferredWorkspaceDir = (0, import_node_path3.join)(ctx.stateDir, "workspace");
6102
+ if ((0, import_node_fs4.existsSync)(inferredWorkspaceDir)) return inferredWorkspaceDir;
6103
+ return ctx.stateDir;
6066
6104
  }
6067
- if (segments.length > MAX_SEGMENTS) {
6105
+ throw new Error("workspaceDir and stateDir both unavailable");
6106
+ }
6107
+ function tasksDir(ctx) {
6108
+ return (0, import_node_path3.join)(resolveBaseDir(ctx), "tasks");
6109
+ }
6110
+ function normalizeLightRuleLookupName(name) {
6111
+ return name.trim().replace(/\.json$/i, "");
6112
+ }
6113
+ function resolveLightRuleTask(ctx, name) {
6114
+ const dir = tasksDir(ctx);
6115
+ const normalizedName = normalizeLightRuleLookupName(name);
6116
+ if (!normalizedName) return null;
6117
+ const directTaskDir = (0, import_node_path3.join)(dir, normalizedName);
6118
+ const directMeta = readMeta(directTaskDir);
6119
+ if (directMeta) {
6068
6120
  return {
6069
- valid: false,
6070
- errors: [{ field: "segments", message: `\u6700\u591A ${MAX_SEGMENTS} \u6BB5` }]
6121
+ taskDir: directTaskDir,
6122
+ meta: directMeta
6071
6123
  };
6072
6124
  }
6073
- const errors = [];
6074
- for (let i = 0; i < segments.length; i++) {
6075
- validateSegment(segments[i], `segments[${i}]`, errors);
6125
+ if (!(0, import_node_fs4.existsSync)(dir)) return null;
6126
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
6127
+ if (!entry.isDirectory()) continue;
6128
+ const taskDir = (0, import_node_path3.join)(dir, entry.name);
6129
+ const meta = readMeta(taskDir);
6130
+ if (meta?.name === normalizedName) {
6131
+ return {
6132
+ taskDir,
6133
+ meta
6134
+ };
6135
+ }
6076
6136
  }
6077
- if (errors.length > 0) return { valid: false, errors };
6078
- return { valid: true, segments };
6137
+ return null;
6079
6138
  }
6080
- function parseAndValidateSegments(json) {
6081
- let parsed;
6139
+ function readOptionalString(value) {
6140
+ if (typeof value !== "string") return void 0;
6141
+ const trimmed = value.trim();
6142
+ return trimmed || void 0;
6143
+ }
6144
+ function isLegacyLightRuleWithoutType(raw) {
6145
+ return raw.type === void 0 && readOptionalString(raw.name) !== void 0 && readOptionalString(raw.description) !== void 0 && Array.isArray(raw.segments);
6146
+ }
6147
+ function readMeta(taskDir) {
6148
+ const metaPath = (0, import_node_path3.join)(taskDir, "meta.json");
6149
+ if (!(0, import_node_fs4.existsSync)(metaPath)) return null;
6082
6150
  try {
6083
- parsed = JSON.parse(json);
6151
+ const raw = JSON.parse((0, import_node_fs4.readFileSync)(metaPath, "utf-8"));
6152
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
6153
+ if (raw.type !== "light-rule" && !isLegacyLightRuleWithoutType(raw)) return null;
6154
+ if (!Array.isArray(raw.segments)) return null;
6155
+ const name = readOptionalString(raw.name) ?? (0, import_node_path3.basename)(taskDir);
6156
+ const description = readOptionalString(raw.description) ?? readOptionalString(raw.reason) ?? name;
6157
+ const createdAt = readOptionalString(raw.createdAt) ?? (0, import_node_fs4.statSync)(metaPath).birthtime.toISOString();
6158
+ const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
6159
+ const repeatTimes = normalizeRepeatTimes({
6160
+ repeat: raw.repeat,
6161
+ repeat_times: raw.repeat_times
6162
+ });
6163
+ assertAncsRepeatTimes(repeatTimes);
6164
+ return {
6165
+ ...raw,
6166
+ name,
6167
+ type: "light-rule",
6168
+ description,
6169
+ segments: raw.segments,
6170
+ repeat_times: repeatTimes,
6171
+ enabled,
6172
+ createdAt
6173
+ };
6084
6174
  } catch {
6085
- exitError("VALIDATION_FAILED", "segments \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON");
6086
- }
6087
- const result = validateSegments(parsed);
6088
- if (!result.valid) {
6089
- output({ ok: false, error: { code: "VALIDATION_FAILED", details: result.errors } });
6090
- process.exit(1);
6175
+ return null;
6091
6176
  }
6092
- return result.segments;
6093
6177
  }
6094
- function validateSegment(seg, prefix, errors) {
6095
- if (!isRecord(seg)) {
6096
- errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6097
- return;
6178
+ function writeMeta(taskDir, meta) {
6179
+ (0, import_node_fs4.writeFileSync)((0, import_node_path3.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
6180
+ }
6181
+ function generateLightRuleReadme(name, description, segments, repeatTimes) {
6182
+ const segmentsJson = JSON.stringify(segments, null, 2);
6183
+ return `# ${name}
6184
+
6185
+ ## \u76D1\u63A7\u76EE\u6807
6186
+
6187
+ ${description}
6188
+
6189
+ ## \u89E6\u53D1\u540E\u5982\u4F55\u5904\u7406
6190
+
6191
+ \u5F53\u4EFB\u52A1\u811A\u672C\u8F93\u51FA\u5339\u914D\u7ED3\u679C\u65F6\uFF1A
6192
+
6193
+ 1. \u5C06\u8F93\u51FA\u6309"\u6BCF\u884C\u4E00\u4E2A JSON \u5BF9\u8C61"\u89E3\u6790
6194
+ 2. \u8FD9\u4E9B\u662F\u5019\u9009\u901A\u77E5\uFF0C\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u547D\u4E2D
6195
+ 3. \u6839\u636E\u76D1\u63A7\u76EE\u6807\u5224\u65AD\u662F\u5426\u771F\u6B63\u547D\u4E2D
6196
+
6197
+ **\u547D\u4E2D\u540E\u6267\u884C\u706F\u6548**\uFF1A\u8C03\u7528 light_control \u5DE5\u5177\uFF0C\u4F20\u5165\u4EE5\u4E0B\u706F\u6548\u53C2\u6570\uFF1A
6198
+
6199
+ \`\`\`json
6200
+ ${segmentsJson}
6201
+ \`\`\`
6202
+
6203
+ repeat_times: ${repeatTimes}
6204
+ reason: "${description}"
6205
+
6206
+ \u53EA\u6267\u884C\u706F\u6548\uFF0C\u4E0D\u4E3B\u52A8\u53D1\u9001\u901A\u77E5\u6458\u8981\u6216\u56DE\u590D\u7528\u6237\u3002
6207
+ `;
6208
+ }
6209
+ function listLightRules(ctx) {
6210
+ const dir = tasksDir(ctx);
6211
+ if (!(0, import_node_fs4.existsSync)(dir)) return [];
6212
+ const rules = [];
6213
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
6214
+ if (!entry.isDirectory()) continue;
6215
+ const meta = readMeta((0, import_node_path3.join)(dir, entry.name));
6216
+ if (meta) rules.push(meta);
6098
6217
  }
6099
- const mode = seg.mode;
6100
- if (!VALID_MODES.includes(mode)) {
6101
- errors.push({
6102
- field: `${prefix}.mode`,
6103
- message: `\u4E0D\u652F\u6301\u7684\u6A21\u5F0F '${String(mode)}'\uFF0C\u53EF\u9009\uFF1A${VALID_MODES.join("/")}`
6104
- });
6218
+ return rules;
6219
+ }
6220
+ function createLightRule(ctx, params) {
6221
+ const dir = tasksDir(ctx);
6222
+ const taskDir = (0, import_node_path3.join)(dir, params.name);
6223
+ if ((0, import_node_fs4.existsSync)(taskDir)) {
6224
+ throw new LightRuleError("ALREADY_EXISTS", `\u706F\u6548\u89C4\u5219 '${params.name}' \u5DF2\u5B58\u5728`);
6105
6225
  }
6106
- validateNonNegativeNumber(seg.duration_s, `${prefix}.duration_s`, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57\uFF080 \u8868\u793A\u65E0\u9650\u65F6\u957F\uFF09");
6107
- switch (mode) {
6108
- case "wave":
6109
- validateForegroundSegment(seg, prefix, errors);
6110
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6111
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6112
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6113
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6114
- break;
6115
- case "wave_rainbow":
6116
- validateForegroundSegment(seg, prefix, errors);
6117
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6118
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6119
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6120
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6121
- if (!hasNonZeroRgb(seg.color) && !hasNonZeroRgb(seg.background)) {
6122
- errors.push({
6123
- field: prefix,
6124
- message: "wave_rainbow \u81F3\u5C11\u9700\u8981\u4E00\u7EC4\u975E\u96F6\u989C\u8272\u951A\u70B9\uFF08color \u6216 background\uFF09"
6125
- });
6226
+ (0, import_node_fs4.mkdirSync)(taskDir, { recursive: true });
6227
+ const repeatTimes = normalizeRepeatTimes({
6228
+ repeat: params.repeat,
6229
+ repeat_times: params.repeat_times
6230
+ });
6231
+ assertAncsRepeatTimes(repeatTimes);
6232
+ const effectiveMatchRules = legacyReadMatchRules(params);
6233
+ const effectiveCronSchedule = legacyReadCronSchedule(params);
6234
+ const meta = {
6235
+ name: params.name,
6236
+ type: "light-rule",
6237
+ description: params.description,
6238
+ segments: params.segments,
6239
+ repeat_times: repeatTimes,
6240
+ enabled: true,
6241
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6242
+ };
6243
+ legacyAssignMatchRules(meta, effectiveMatchRules);
6244
+ legacyAssignCronSchedule(meta, effectiveCronSchedule);
6245
+ writeMeta(taskDir, meta);
6246
+ (0, import_node_fs4.writeFileSync)(
6247
+ (0, import_node_path3.join)(taskDir, "fetch.py"),
6248
+ generateFetchPy(params.name, effectiveMatchRules),
6249
+ "utf-8"
6250
+ );
6251
+ (0, import_node_fs4.writeFileSync)(
6252
+ (0, import_node_path3.join)(taskDir, "README.md"),
6253
+ generateLightRuleReadme(params.name, params.description, params.segments, repeatTimes),
6254
+ "utf-8"
6255
+ );
6256
+ return {
6257
+ meta,
6258
+ cronHint: {
6259
+ action: "add",
6260
+ job: {
6261
+ name: `notif-${params.name}`,
6262
+ schedule: effectiveCronSchedule,
6263
+ sessionTarget: "isolated",
6264
+ 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
6265
+ \u6267\u884C\uFF1Apython3 tasks/${params.name}/fetch.py --notifications-dir notifications
6266
+ - NO_CHANGE \u6216 NO_MATCH \u2192 \u4E0D\u56DE\u590D\uFF0C\u76F4\u63A5\u7ED3\u675F\u3002
6267
+ - \u6709\u8F93\u51FA \u2192 \u8BFB tasks/${params.name}/README.md \u4E86\u89E3\u5982\u4F55\u5904\u7406\u6570\u636E\u5E76\u901A\u77E5\u7528\u6237\u3002`
6126
6268
  }
6127
- break;
6128
- case "breath":
6129
- validateForegroundSegment(seg, prefix, errors);
6130
- validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
6131
- break;
6132
- case "strobe":
6133
- validateForegroundSegment(seg, prefix, errors);
6134
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6135
- break;
6136
- case "steady":
6137
- validateForegroundSegment(seg, prefix, errors);
6138
- break;
6139
- case "pixel_frame":
6140
- validatePixelFrame(seg.pixels, `${prefix}.pixels`, errors);
6141
- break;
6142
- default:
6143
- validateOptionalNonNegativeNumber(seg.brightness, `${prefix}.brightness`, errors);
6144
- validateOptionalColor(seg.color, `${prefix}.color`, errors);
6145
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6146
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6147
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6148
- validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
6149
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6150
- }
6269
+ }
6270
+ };
6151
6271
  }
6152
- function validateForegroundSegment(seg, prefix, errors) {
6153
- validateNumberInRange(
6154
- seg.brightness,
6155
- `${prefix}.brightness`,
6156
- errors,
6157
- 0,
6158
- 255,
6159
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6160
- );
6161
- validateColor(seg.color, `${prefix}.color`, errors);
6162
- if (seg.mode !== "steady" && seg.brightness === 0) {
6163
- errors.push({
6164
- field: `${prefix}.brightness`,
6165
- message: "brightness=0 \u4EC5 steady \u6A21\u5F0F\u5141\u8BB8\uFF1B\u5176\u5B83\u6A21\u5F0F\u4F1A\u5728\u56FA\u4EF6\u4FA7\u88AB\u8FC7\u6EE4"
6166
- });
6272
+ function updateLightRule(ctx, params) {
6273
+ const resolved = resolveLightRuleTask(ctx, params.name);
6274
+ const taskDir = resolved?.taskDir;
6275
+ const meta = resolved?.meta;
6276
+ if (!taskDir || !meta) {
6277
+ throw new LightRuleError("NOT_FOUND", `\u706F\u6548\u89C4\u5219 '${params.name}' \u4E0D\u5B58\u5728`);
6167
6278
  }
6168
- }
6169
- function validatePixelFrame(value, field, errors) {
6170
- if (!Array.isArray(value)) {
6171
- errors.push({ field, message: "pixel_frame \u5FC5\u987B\u63D0\u4F9B pixels \u6570\u7EC4\uFF081\u20137 \u9879\uFF09" });
6172
- return;
6279
+ let regenerateFetch = false;
6280
+ let regenerateReadme = false;
6281
+ if (params.description !== void 0) {
6282
+ meta.description = params.description;
6283
+ regenerateReadme = true;
6173
6284
  }
6174
- if (value.length < 1 || value.length > 7) {
6175
- errors.push({ field, message: "pixels \u5FC5\u987B\u4E3A 1\u20137 \u9879" });
6285
+ if (legacyHasMatchRules(params)) {
6286
+ legacyAssignMatchRules(meta, legacyReadMatchRules(params));
6287
+ regenerateFetch = true;
6176
6288
  }
6177
- const seen = /* @__PURE__ */ new Set();
6178
- for (let i = 0; i < value.length; i++) {
6179
- const pixel = value[i];
6180
- const prefix = `${field}[${i}]`;
6181
- if (!isRecord(pixel)) {
6182
- errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6183
- continue;
6184
- }
6185
- const idx = pixel.index;
6186
- if (!Number.isInteger(idx) || idx < 0 || idx > 6) {
6187
- errors.push({ field: `${prefix}.index`, message: "index \u5FC5\u987B\u662F 0\u20136 \u7684\u6574\u6570" });
6188
- } else if (seen.has(idx)) {
6189
- errors.push({ field: `${prefix}.index`, message: `index=${idx} \u91CD\u590D` });
6190
- } else {
6191
- seen.add(idx);
6192
- }
6193
- validateNumberInRange(
6194
- pixel.brightness,
6195
- `${prefix}.brightness`,
6196
- errors,
6197
- 0,
6198
- 255,
6199
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6200
- );
6201
- validateColor(pixel.color, `${prefix}.color`, errors);
6289
+ if (params.segments !== void 0) {
6290
+ meta.segments = params.segments;
6291
+ regenerateReadme = true;
6202
6292
  }
6203
- }
6204
- function validateOptionalBreathTiming(value, field, errors) {
6205
- if (value === void 0) return;
6206
- if (!isRecord(value)) {
6207
- errors.push({ field, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6208
- return;
6293
+ if (params.repeat !== void 0 || params.repeat_times !== void 0) {
6294
+ meta.repeat_times = normalizeRepeatTimes({
6295
+ repeat: params.repeat,
6296
+ repeat_times: params.repeat_times
6297
+ });
6298
+ assertAncsRepeatTimes(meta.repeat_times);
6299
+ regenerateReadme = true;
6209
6300
  }
6210
- validatePositiveNumber(
6211
- value.rise_ms,
6212
- `${field}.rise_ms`,
6213
- errors,
6214
- "rise_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
6215
- );
6216
- validateNonNegativeNumber(
6217
- value.hold_ms,
6218
- `${field}.hold_ms`,
6219
- errors,
6220
- "hold_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
6221
- );
6222
- validatePositiveNumber(
6223
- value.fall_ms,
6224
- `${field}.fall_ms`,
6225
- errors,
6226
- "fall_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
6227
- );
6228
- validateNonNegativeNumber(
6229
- value.off_ms,
6230
- `${field}.off_ms`,
6231
- errors,
6232
- "off_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
6233
- );
6234
- }
6235
- function validateOptionalBackground(value, field, errors) {
6236
- if (value === void 0) return;
6237
- if (!isRecord(value)) {
6238
- errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b/brightness \u6570\u503C" });
6239
- return;
6301
+ if (legacyHasCronSchedule(params)) {
6302
+ legacyAssignCronSchedule(meta, legacyReadCronSchedule(params));
6240
6303
  }
6241
- validateColor(value, field, errors);
6242
- validateNumberInRange(
6243
- value.brightness,
6244
- `${field}.brightness`,
6245
- errors,
6246
- 0,
6247
- 255,
6248
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6249
- );
6250
- }
6251
- function validateOptionalColor(value, field, errors) {
6252
- if (value === void 0) return;
6253
- validateColor(value, field, errors);
6254
- }
6255
- function validateColor(value, field, errors) {
6256
- if (!isRecord(value)) {
6257
- errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b \u6570\u503C" });
6258
- return;
6304
+ if (params.enabled !== void 0) {
6305
+ meta.enabled = params.enabled;
6259
6306
  }
6260
- validateNumberInRange(value.r, `${field}.r`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6261
- validateNumberInRange(value.g, `${field}.g`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6262
- validateNumberInRange(value.b, `${field}.b`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6263
- }
6264
- function validateOptionalDirection(value, field, errors) {
6265
- if (value === void 0) return;
6266
- if (value !== "ltr" && value !== "rtl") {
6267
- errors.push({ field, message: "direction \u5FC5\u987B\u662F ltr \u6216 rtl" });
6307
+ meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6308
+ writeMeta(taskDir, meta);
6309
+ if (regenerateFetch) {
6310
+ (0, import_node_fs4.writeFileSync)(
6311
+ (0, import_node_path3.join)(taskDir, "fetch.py"),
6312
+ generateFetchPy(meta.name, legacyReadMetaMatchRules(meta)),
6313
+ "utf-8"
6314
+ );
6268
6315
  }
6269
- }
6270
- function validateOptionalWindow(value, field, errors) {
6271
- if (value === void 0) return;
6272
- if (value !== 1 && value !== 2 && value !== 3) {
6273
- errors.push({ field, message: "window \u4EC5\u652F\u6301 1/2/3" });
6316
+ if (regenerateReadme) {
6317
+ (0, import_node_fs4.writeFileSync)(
6318
+ (0, import_node_path3.join)(taskDir, "README.md"),
6319
+ generateLightRuleReadme(meta.name, meta.description, meta.segments, meta.repeat_times),
6320
+ "utf-8"
6321
+ );
6274
6322
  }
6275
- }
6276
- function validateOptionalNonNegativeNumber(value, field, errors) {
6277
- if (value === void 0) return;
6278
- validateNonNegativeNumber(value, field, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57");
6279
- }
6280
- function validatePositiveNumber(value, field, errors, message) {
6281
- if (value === void 0) return;
6282
- if (!isFiniteNumber(value) || value <= 0) {
6283
- errors.push({ field, message });
6284
- }
6285
- }
6286
- function validateNonNegativeNumber(value, field, errors, message) {
6287
- if (!isFiniteNumber(value) || value < 0) {
6288
- errors.push({ field, message });
6289
- }
6290
- }
6291
- function validateNumberInRange(value, field, errors, min, max, message) {
6292
- if (!isFiniteNumber(value) || value < min || value > max) {
6293
- errors.push({ field, message });
6294
- }
6295
- }
6296
- function hasNonZeroRgb(value) {
6297
- if (!value) return false;
6298
- return [value.r, value.g, value.b].some((channel) => isFiniteNumber(channel) && channel > 0);
6299
- }
6300
- function isFiniteNumber(value) {
6301
- return typeof value === "number" && Number.isFinite(value);
6302
- }
6303
- function isRecord(value) {
6304
- return value !== null && typeof value === "object" && !Array.isArray(value);
6305
- }
6306
-
6307
- // src/light-rules/storage.ts
6308
- var import_node_fs5 = require("fs");
6309
- var import_node_path4 = require("path");
6310
-
6311
- // src/monitor/fetch-gen.ts
6312
- function generateFetchPy(name, matchRules) {
6313
- const appName = typeof matchRules.appName === "string" ? matchRules.appName : "";
6314
- const senderKeywords = stringArray(matchRules.senderKeywords);
6315
- const contentKeywords = stringArray(matchRules.contentKeywords);
6316
- return `#!/usr/bin/env python3
6317
- """Auto-generated fetch script for monitor task: ${name}"""
6318
- import json, sys, os
6319
- from pathlib import Path
6320
-
6321
- def matches(notification: dict) -> bool:
6322
- app = str(notification.get("appName", "") or "")
6323
- title = str(notification.get("title", "") or "")
6324
- content = str(notification.get("content", "") or "")
6325
- body = str(notification.get("body", "") or "")
6326
-
6327
- app_name = ${pyLiteral(appName)}
6328
- if app_name and app != app_name:
6329
- return False
6330
-
6331
- sender_keywords = ${pyLiteral(senderKeywords)}
6332
- sender_haystack = f"{title}\\n{content}\\n{body}"
6333
- if sender_keywords and not any(keyword in sender_haystack for keyword in sender_keywords):
6334
- return False
6335
-
6336
- content_keywords = ${pyLiteral(contentKeywords)}
6337
- content_haystack = f"{content}\\n{body}"
6338
- if content_keywords and not any(keyword in content_haystack for keyword in content_keywords):
6339
- return False
6340
-
6341
- return True
6342
-
6343
- def main():
6344
- import argparse
6345
- parser = argparse.ArgumentParser()
6346
- parser.add_argument("--notifications-dir", required=True)
6347
- args = parser.parse_args()
6348
-
6349
- checkpoint_path = Path(__file__).parent / "checkpoint.json"
6350
- checkpoint = {}
6351
- if checkpoint_path.exists():
6352
- checkpoint = json.loads(checkpoint_path.read_text())
6353
-
6354
- ntf_dir = Path(args.notifications_dir)
6355
- matched = []
6356
- new_checkpoint = dict(checkpoint)
6357
-
6358
- for f in sorted(ntf_dir.glob("*.json")):
6359
- date_key = f.stem
6360
- items = json.loads(f.read_text())
6361
- last_idx = checkpoint.get(date_key, {}).get("lastIndex", -1)
6362
- for i, item in enumerate(items):
6363
- if i <= last_idx:
6364
- continue
6365
- if matches(item):
6366
- matched.append(item)
6367
- new_checkpoint[date_key] = {"lastIndex": len(items) - 1}
6368
-
6369
- checkpoint_path.write_text(json.dumps(new_checkpoint, indent=2))
6370
-
6371
- if not matched:
6372
- print("NO_MATCH")
6373
- return
6374
-
6375
- for m in matched:
6376
- print(json.dumps(m, ensure_ascii=False))
6377
-
6378
- if __name__ == "__main__":
6379
- main()
6380
- `;
6381
- }
6382
- function stringArray(value) {
6383
- if (!Array.isArray(value)) return [];
6384
- return value.filter((item) => typeof item === "string" && item.length > 0);
6385
- }
6386
- function pyLiteral(value) {
6387
- return JSON.stringify(value);
6388
- }
6389
-
6390
- // src/light-rules/storage.ts
6391
- var LEGACY_DEFAULT_CRON_SCHEDULE = "*/5 * * * *";
6392
- function legacyReadMatchRules(input) {
6393
- return input.matchRules ?? {};
6394
- }
6395
- function legacyReadCronSchedule(input) {
6396
- return input.cronSchedule ?? LEGACY_DEFAULT_CRON_SCHEDULE;
6397
- }
6398
- function legacyHasMatchRules(input) {
6399
- return input.matchRules !== void 0;
6400
- }
6401
- function legacyHasCronSchedule(input) {
6402
- return input.cronSchedule !== void 0;
6403
- }
6404
- function legacyAssignMatchRules(meta, matchRules) {
6405
- meta.matchRules = matchRules;
6406
- }
6407
- function legacyAssignCronSchedule(meta, cronSchedule) {
6408
- meta.cronSchedule = cronSchedule;
6409
- }
6410
- function legacyReadMetaMatchRules(meta) {
6411
- return meta.matchRules ?? {};
6412
- }
6413
- function legacyReadMetaCronSchedule(meta) {
6414
- return meta.cronSchedule;
6415
- }
6416
- function resolveBaseDir(ctx) {
6417
- if (ctx.workspaceDir) return ctx.workspaceDir;
6418
- if (ctx.stateDir) {
6419
- const inferredWorkspaceDir = (0, import_node_path4.join)(ctx.stateDir, "workspace");
6420
- if ((0, import_node_fs5.existsSync)(inferredWorkspaceDir)) return inferredWorkspaceDir;
6421
- return ctx.stateDir;
6422
- }
6423
- throw new Error("workspaceDir and stateDir both unavailable");
6424
- }
6425
- function tasksDir(ctx) {
6426
- return (0, import_node_path4.join)(resolveBaseDir(ctx), "tasks");
6427
- }
6428
- function normalizeLightRuleLookupName(name) {
6429
- return name.trim().replace(/\.json$/i, "");
6430
- }
6431
- function resolveLightRuleTask(ctx, name) {
6432
- const dir = tasksDir(ctx);
6433
- const normalizedName = normalizeLightRuleLookupName(name);
6434
- if (!normalizedName) return null;
6435
- const directTaskDir = (0, import_node_path4.join)(dir, normalizedName);
6436
- const directMeta = readMeta(directTaskDir);
6437
- if (directMeta) {
6438
- return {
6439
- taskDir: directTaskDir,
6440
- meta: directMeta
6441
- };
6442
- }
6443
- if (!(0, import_node_fs5.existsSync)(dir)) return null;
6444
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
6445
- if (!entry.isDirectory()) continue;
6446
- const taskDir = (0, import_node_path4.join)(dir, entry.name);
6447
- const meta = readMeta(taskDir);
6448
- if (meta?.name === normalizedName) {
6449
- return {
6450
- taskDir,
6451
- meta
6452
- };
6453
- }
6454
- }
6455
- return null;
6456
- }
6457
- function readOptionalString(value) {
6458
- if (typeof value !== "string") return void 0;
6459
- const trimmed = value.trim();
6460
- return trimmed || void 0;
6461
- }
6462
- function isLegacyLightRuleWithoutType(raw) {
6463
- return raw.type === void 0 && readOptionalString(raw.name) !== void 0 && readOptionalString(raw.description) !== void 0 && Array.isArray(raw.segments);
6464
- }
6465
- function readMeta(taskDir) {
6466
- const metaPath = (0, import_node_path4.join)(taskDir, "meta.json");
6467
- if (!(0, import_node_fs5.existsSync)(metaPath)) return null;
6468
- try {
6469
- const raw = JSON.parse((0, import_node_fs5.readFileSync)(metaPath, "utf-8"));
6470
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
6471
- if (raw.type !== "light-rule" && !isLegacyLightRuleWithoutType(raw)) return null;
6472
- if (!Array.isArray(raw.segments)) return null;
6473
- const name = readOptionalString(raw.name) ?? (0, import_node_path4.basename)(taskDir);
6474
- const description = readOptionalString(raw.description) ?? readOptionalString(raw.reason) ?? name;
6475
- const createdAt = readOptionalString(raw.createdAt) ?? (0, import_node_fs5.statSync)(metaPath).birthtime.toISOString();
6476
- const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
6477
- const repeatTimes = normalizeRepeatTimes({
6478
- repeat: raw.repeat,
6479
- repeat_times: raw.repeat_times
6480
- });
6481
- assertAncsRepeatTimes(repeatTimes);
6482
- return {
6483
- ...raw,
6484
- name,
6485
- type: "light-rule",
6486
- description,
6487
- segments: raw.segments,
6488
- repeat_times: repeatTimes,
6489
- enabled,
6490
- createdAt
6491
- };
6492
- } catch {
6493
- return null;
6494
- }
6495
- }
6496
- function writeMeta(taskDir, meta) {
6497
- (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
6498
- }
6499
- function generateLightRuleReadme(name, description, segments, repeatTimes) {
6500
- const segmentsJson = JSON.stringify(segments, null, 2);
6501
- return `# ${name}
6502
-
6503
- ## \u76D1\u63A7\u76EE\u6807
6504
-
6505
- ${description}
6506
-
6507
- ## \u89E6\u53D1\u540E\u5982\u4F55\u5904\u7406
6508
-
6509
- \u5F53\u4EFB\u52A1\u811A\u672C\u8F93\u51FA\u5339\u914D\u7ED3\u679C\u65F6\uFF1A
6510
-
6511
- 1. \u5C06\u8F93\u51FA\u6309"\u6BCF\u884C\u4E00\u4E2A JSON \u5BF9\u8C61"\u89E3\u6790
6512
- 2. \u8FD9\u4E9B\u662F\u5019\u9009\u901A\u77E5\uFF0C\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u547D\u4E2D
6513
- 3. \u6839\u636E\u76D1\u63A7\u76EE\u6807\u5224\u65AD\u662F\u5426\u771F\u6B63\u547D\u4E2D
6514
-
6515
- **\u547D\u4E2D\u540E\u6267\u884C\u706F\u6548**\uFF1A\u8C03\u7528 light_control \u5DE5\u5177\uFF0C\u4F20\u5165\u4EE5\u4E0B\u706F\u6548\u53C2\u6570\uFF1A
6516
-
6517
- \`\`\`json
6518
- ${segmentsJson}
6519
- \`\`\`
6520
-
6521
- repeat_times: ${repeatTimes}
6522
- reason: "${description}"
6523
-
6524
- \u53EA\u6267\u884C\u706F\u6548\uFF0C\u4E0D\u4E3B\u52A8\u53D1\u9001\u901A\u77E5\u6458\u8981\u6216\u56DE\u590D\u7528\u6237\u3002
6525
- `;
6526
- }
6527
- function listLightRules(ctx) {
6528
- const dir = tasksDir(ctx);
6529
- if (!(0, import_node_fs5.existsSync)(dir)) return [];
6530
- const rules = [];
6531
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
6532
- if (!entry.isDirectory()) continue;
6533
- const meta = readMeta((0, import_node_path4.join)(dir, entry.name));
6534
- if (meta) rules.push(meta);
6535
- }
6536
- return rules;
6537
- }
6538
- function createLightRule(ctx, params) {
6539
- const dir = tasksDir(ctx);
6540
- const taskDir = (0, import_node_path4.join)(dir, params.name);
6541
- if ((0, import_node_fs5.existsSync)(taskDir)) {
6542
- throw new LightRuleError("ALREADY_EXISTS", `\u706F\u6548\u89C4\u5219 '${params.name}' \u5DF2\u5B58\u5728`);
6543
- }
6544
- (0, import_node_fs5.mkdirSync)(taskDir, { recursive: true });
6545
- const repeatTimes = normalizeRepeatTimes({
6546
- repeat: params.repeat,
6547
- repeat_times: params.repeat_times
6548
- });
6549
- assertAncsRepeatTimes(repeatTimes);
6550
- const effectiveMatchRules = legacyReadMatchRules(params);
6551
- const effectiveCronSchedule = legacyReadCronSchedule(params);
6552
- const meta = {
6553
- name: params.name,
6554
- type: "light-rule",
6555
- description: params.description,
6556
- segments: params.segments,
6557
- repeat_times: repeatTimes,
6558
- enabled: true,
6559
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
6560
- };
6561
- legacyAssignMatchRules(meta, effectiveMatchRules);
6562
- legacyAssignCronSchedule(meta, effectiveCronSchedule);
6563
- writeMeta(taskDir, meta);
6564
- (0, import_node_fs5.writeFileSync)(
6565
- (0, import_node_path4.join)(taskDir, "fetch.py"),
6566
- generateFetchPy(params.name, effectiveMatchRules),
6567
- "utf-8"
6568
- );
6569
- (0, import_node_fs5.writeFileSync)(
6570
- (0, import_node_path4.join)(taskDir, "README.md"),
6571
- generateLightRuleReadme(params.name, params.description, params.segments, repeatTimes),
6572
- "utf-8"
6573
- );
6574
- return {
6575
- meta,
6576
- cronHint: {
6577
- action: "add",
6578
- job: {
6579
- name: `notif-${params.name}`,
6580
- schedule: effectiveCronSchedule,
6581
- sessionTarget: "isolated",
6582
- 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
6583
- \u6267\u884C\uFF1Apython3 tasks/${params.name}/fetch.py --notifications-dir notifications
6584
- - NO_CHANGE \u6216 NO_MATCH \u2192 \u4E0D\u56DE\u590D\uFF0C\u76F4\u63A5\u7ED3\u675F\u3002
6585
- - \u6709\u8F93\u51FA \u2192 \u8BFB tasks/${params.name}/README.md \u4E86\u89E3\u5982\u4F55\u5904\u7406\u6570\u636E\u5E76\u901A\u77E5\u7528\u6237\u3002`
6586
- }
6587
- }
6588
- };
6589
- }
6590
- function updateLightRule(ctx, params) {
6591
- const resolved = resolveLightRuleTask(ctx, params.name);
6592
- const taskDir = resolved?.taskDir;
6593
- const meta = resolved?.meta;
6594
- if (!taskDir || !meta) {
6595
- throw new LightRuleError("NOT_FOUND", `\u706F\u6548\u89C4\u5219 '${params.name}' \u4E0D\u5B58\u5728`);
6596
- }
6597
- let regenerateFetch = false;
6598
- let regenerateReadme = false;
6599
- if (params.description !== void 0) {
6600
- meta.description = params.description;
6601
- regenerateReadme = true;
6602
- }
6603
- if (legacyHasMatchRules(params)) {
6604
- legacyAssignMatchRules(meta, legacyReadMatchRules(params));
6605
- regenerateFetch = true;
6606
- }
6607
- if (params.segments !== void 0) {
6608
- meta.segments = params.segments;
6609
- regenerateReadme = true;
6610
- }
6611
- if (params.repeat !== void 0 || params.repeat_times !== void 0) {
6612
- meta.repeat_times = normalizeRepeatTimes({
6613
- repeat: params.repeat,
6614
- repeat_times: params.repeat_times
6615
- });
6616
- assertAncsRepeatTimes(meta.repeat_times);
6617
- regenerateReadme = true;
6618
- }
6619
- if (legacyHasCronSchedule(params)) {
6620
- legacyAssignCronSchedule(meta, legacyReadCronSchedule(params));
6621
- }
6622
- if (params.enabled !== void 0) {
6623
- meta.enabled = params.enabled;
6624
- }
6625
- meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6626
- writeMeta(taskDir, meta);
6627
- if (regenerateFetch) {
6628
- (0, import_node_fs5.writeFileSync)(
6629
- (0, import_node_path4.join)(taskDir, "fetch.py"),
6630
- generateFetchPy(meta.name, legacyReadMetaMatchRules(meta)),
6631
- "utf-8"
6632
- );
6633
- }
6634
- if (regenerateReadme) {
6635
- (0, import_node_fs5.writeFileSync)(
6636
- (0, import_node_path4.join)(taskDir, "README.md"),
6637
- generateLightRuleReadme(meta.name, meta.description, meta.segments, meta.repeat_times),
6638
- "utf-8"
6639
- );
6640
- }
6641
- const cronHint = legacyHasCronSchedule(params) ? {
6642
- action: "update",
6643
- job: {
6644
- name: `notif-${meta.name}`,
6645
- schedule: legacyReadMetaCronSchedule(meta)
6646
- }
6647
- } : void 0;
6648
- return { meta, cronHint };
6323
+ const cronHint = legacyHasCronSchedule(params) ? {
6324
+ action: "update",
6325
+ job: {
6326
+ name: `notif-${meta.name}`,
6327
+ schedule: legacyReadMetaCronSchedule(meta)
6328
+ }
6329
+ } : void 0;
6330
+ return { meta, cronHint };
6649
6331
  }
6650
6332
  function deleteLightRule(ctx, name) {
6651
6333
  const resolved = resolveLightRuleTask(ctx, name);
@@ -6654,7 +6336,7 @@ function deleteLightRule(ctx, name) {
6654
6336
  if (!taskDir || !meta) {
6655
6337
  throw new LightRuleError("NOT_FOUND", `\u706F\u6548\u89C4\u5219 '${name}' \u4E0D\u5B58\u5728`);
6656
6338
  }
6657
- (0, import_node_fs5.rmSync)(taskDir, { recursive: true, force: true });
6339
+ (0, import_node_fs4.rmSync)(taskDir, { recursive: true, force: true });
6658
6340
  return {
6659
6341
  name: meta.name,
6660
6342
  cronHint: {
@@ -7009,16 +6691,21 @@ var MODE_TO_INDEX = {
7009
6691
  wave_rainbow: 4,
7010
6692
  pixel_frame: 5
7011
6693
  };
7012
- function buildLightEffectApnsBody(segments, repeatInput) {
6694
+ function buildLightEffectApnsBody(segments, repeatInput, visibleTextOverride) {
7013
6695
  assertSegmentCount(segments);
7014
6696
  assertSegmentsValid(segments);
7015
6697
  const repeatTimes = normalizeRepeatTimes(repeatInput);
7016
6698
  assertAncsRepeatTimes(repeatTimes);
7017
- const visibleText = summarizeSegments(segments);
6699
+ const visibleText = resolveVisibleText(visibleTextOverride, segments);
7018
6700
  const separator = repeatTimes === 0 ? LED_SEPARATOR_LOOP : LED_SEPARATOR_ONCE;
7019
6701
  const payload = segments.map((segment) => encodeSegment(segment)).join("");
7020
6702
  return `${visibleText}${separator}${payload}`;
7021
6703
  }
6704
+ function resolveVisibleText(override, segments) {
6705
+ const trimmed = override?.trim();
6706
+ if (trimmed) return trimmed;
6707
+ return summarizeSegments(segments);
6708
+ }
7022
6709
  function assertSegmentCount(segments) {
7023
6710
  if (segments.length < 1 || segments.length > MAX_LIGHT_SEGMENTS) {
7024
6711
  throw new Error(`light_control supports 1-${MAX_LIGHT_SEGMENTS} segments`);
@@ -7374,231 +7061,48 @@ function registerLightRulesTools(api, registry, logger) {
7374
7061
  updated: true,
7375
7062
  rule: result.meta,
7376
7063
  cronHint: result.cronHint
7377
- });
7378
- } catch (e) {
7379
- if (e instanceof LightRuleError) return err(e.code, e.message);
7380
- logger.warn(`lightrules.update tool failed: ${e?.message}`);
7381
- return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7382
- }
7383
- }
7384
- }, ["lightrules_update"]);
7385
- registerToolWithAliases(api, {
7386
- name: "lightrules.delete",
7387
- label: "Delete Light Rule",
7388
- 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',
7389
- parameters: {
7390
- type: "object",
7391
- required: ["name"],
7392
- additionalProperties: false,
7393
- properties: {
7394
- name: { type: "string", description: "\u8981\u5220\u9664\u7684\u89C4\u5219\u540D\u79F0" }
7395
- }
7396
- },
7397
- async execute(_toolCallId, params) {
7398
- const { name } = params;
7399
- if (!name || typeof name !== "string")
7400
- return err("INVALID_PARAMS", "name is required");
7401
- try {
7402
- const result = await registry.delete(name);
7403
- logger.info(`lightrules.delete tool: deleted ${name}`);
7404
- return ok({ ok: true, name: result.name, deleted: true, cronHint: result.cronHint });
7405
- } catch (e) {
7406
- if (e instanceof LightRuleError) return err(e.code, e.message);
7407
- logger.warn(`lightrules.delete tool failed: ${e?.message}`);
7408
- return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7409
- }
7410
- }
7411
- }, ["lightrules_delete"]);
7412
- }
7413
-
7414
- // src/light-rules/evaluator-job.ts
7415
- var EVALUATOR_JOB_ID = "light-rules-evaluator";
7416
- var EVALUATOR_SUBAGENT_SESSION_KEY = EVALUATOR_JOB_ID;
7417
- var FALLBACK_CRON_EXPR = "0 0 1 1 *";
7418
- function buildEvaluatorJobMessage(notificationsDir) {
7419
- return `\u706F\u6548\u89C4\u5219\u8BC4\u4F30\u4EFB\u52A1\u3002
7420
-
7421
- \u6267\u884C\u6B65\u9AA4\uFF1A
7422
- 1. \u8BFB\u53D6 tasks/light-rules-evaluator/checkpoint.json\uFF08\u8BB0\u5F55\u4E0A\u6B21\u5904\u7406\u8FDB\u5EA6\uFF09
7423
- 2. \u626B\u63CF ${notificationsDir} \u76EE\u5F55\uFF0C\u83B7\u53D6 checkpoint \u4E4B\u540E\u7684\u65B0\u901A\u77E5
7424
- 3. \u626B\u63CF tasks/ \u76EE\u5F55\uFF0C\u8BFB\u53D6\u6240\u6709 type=light-rule \u4E14 enabled=true \u7684 meta.json
7425
- 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
7426
- 5. \u547D\u4E2D\u65F6\uFF1A\u4EE5\u8BE5\u89C4\u5219\u7684 segments \u548C repeat_times \u8C03\u7528 light_control \u5DE5\u5177
7427
- 6. \u66F4\u65B0 checkpoint.json\uFF0C\u8BB0\u5F55\u5DF2\u5904\u7406\u5230\u7684\u6700\u65B0\u901A\u77E5\u4F4D\u7F6E
7428
- 7. \u82E5\u65E0\u65B0\u901A\u77E5\u6216\u65E0 enabled \u89C4\u5219\uFF1A\u8F93\u51FA NO_CHANGE\uFF0C\u76F4\u63A5\u7ED3\u675F`;
7429
- }
7430
- var LightRulesEvaluatorJob = class {
7431
- logger;
7432
- registry;
7433
- subagentRunner;
7434
- getNotificationsDir;
7435
- /**
7436
- * 轻量进程内评估器(事件驱动方案的正式路径)。
7437
- * 配置后:每批通知走一次 pi-ai `complete()`,不再启动 agent session。
7438
- * 未配置(或调用失败)时:回退 cron / subagent 老路径。
7439
- */
7440
- inlineEvaluator;
7441
- /**
7442
- * 记录本进程生命周期内 job 是否已确认存在。
7443
- * 仅在 `ensureJobExists` 成功后置 true,避免每次 push 都做检查。
7444
- */
7445
- jobEnsured = false;
7446
- /**
7447
- * 首次创建 job 时的并发保护。
7448
- * 避免冷启动瞬间多条通知并发到达时重复调用 `cron.add`。
7449
- */
7450
- ensureJobPromise = null;
7451
- /**
7452
- * subagent fallback 路径的并发保护。
7453
- * 若评估 session 已在运行中,跳过本次触发(checkpoint 保证下次补处理)。
7454
- */
7455
- subagentInFlight = false;
7456
- constructor(deps) {
7457
- this.logger = deps.logger;
7458
- this.registry = deps.registry;
7459
- this.inlineEvaluator = deps.inlineEvaluator;
7460
- this.subagentRunner = deps.subagentRunner;
7461
- this.getNotificationsDir = deps.getNotificationsDir ?? (() => void 0);
7462
- }
7463
- /**
7464
- * 通知落盘后调用。若有新增通知且存在 enabled 规则,则触发评估。
7465
- *
7466
- * 路径优先级:
7467
- * 1. inlineEvaluator(配置后的主路径):进程内一次 LLM 调用直接匹配 + 触发灯效
7468
- * 2. cron.enqueueRun("force"):legacy,gateway context 路径
7469
- * 3. subagentRunner.run:legacy,HTTP Relay 路径(cron 不可用时)
7470
- *
7471
- * inline 成功(包括 0 命中)立刻返回,不会再走 legacy 路径。
7472
- * inline 失败(LLM 错误 / 未配置 provider)才回退 legacy。
7473
- *
7474
- * @param cron 来自 gateway context 的 CronService;HTTP 路径下为 null
7475
- * @param inserted 本次 ingest 实际新落盘的通知(StoredNotification 去重后)
7476
- */
7477
- async triggerIfNeeded(cron, inserted) {
7478
- if (inserted.length === 0) return;
7479
- if (this.registry.getEnabled().length === 0) return;
7480
- if (this.inlineEvaluator) {
7481
- try {
7482
- const ok2 = await this.inlineEvaluator.evaluate(inserted);
7483
- if (ok2) return;
7484
- this.logger.warn(
7485
- "light-rules-evaluator: inline evaluator failed; falling back to legacy agent session path"
7486
- );
7487
- } catch (err2) {
7488
- this.logger.warn(
7489
- `light-rules-evaluator: inline evaluator threw: ${err2?.message ?? err2}; falling back`
7490
- );
7491
- }
7492
- }
7493
- if (!cron) {
7494
- await this.triggerViaSubagent();
7495
- return;
7496
- }
7497
- try {
7498
- await this.ensureJobExists(cron);
7499
- } catch (err2) {
7500
- this.logger.warn(`light-rules-evaluator: job ensure failed: ${err2?.message ?? err2}`);
7501
- return;
7502
- }
7503
- try {
7504
- const result = await cron.enqueueRun(EVALUATOR_JOB_ID, "force");
7505
- if (!result.ok) {
7506
- this.logger.warn("light-rules-evaluator: enqueueRun returned ok=false");
7507
- return;
7508
- }
7509
- if ("enqueued" in result && result.enqueued) {
7510
- this.logger.info(`light-rules-evaluator: enqueued runId=${result.runId}`);
7511
- } else if ("reason" in result) {
7512
- this.logger.info(`light-rules-evaluator: enqueueRun skipped (${result.reason})`);
7064
+ });
7065
+ } catch (e) {
7066
+ if (e instanceof LightRuleError) return err(e.code, e.message);
7067
+ logger.warn(`lightrules.update tool failed: ${e?.message}`);
7068
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7513
7069
  }
7514
- } catch (err2) {
7515
- this.logger.warn(`light-rules-evaluator: enqueueRun failed: ${err2?.message ?? err2}`);
7516
- }
7517
- }
7518
- /**
7519
- * cron service 不可用时的 fallback:直接通过 subagentRunner 运行评估 session。
7520
- *
7521
- * 并发保护:若上一次 subagent 运行尚未完成,本次跳过。
7522
- * checkpoint 保证即使本次跳过,下次触发时会处理所有积压通知。
7523
- */
7524
- async triggerViaSubagent() {
7525
- if (!this.subagentRunner) {
7526
- this.logger.warn(
7527
- "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"
7528
- );
7529
- return;
7530
- }
7531
- if (this.subagentInFlight) {
7532
- this.logger.info("light-rules-evaluator: subagent run in-flight, skipping this trigger");
7533
- return;
7534
7070
  }
7535
- const notificationsDir = this.getNotificationsDir();
7536
- if (!notificationsDir) {
7537
- this.logger.warn("light-rules-evaluator: notifications dir not ready, skipping subagent trigger");
7538
- return;
7539
- }
7540
- this.subagentInFlight = true;
7541
- try {
7542
- const result = await this.subagentRunner.run({
7543
- sessionKey: EVALUATOR_SUBAGENT_SESSION_KEY,
7544
- message: buildEvaluatorJobMessage(notificationsDir),
7545
- deliver: false,
7546
- idempotencyKey: `${EVALUATOR_SUBAGENT_SESSION_KEY}-${Date.now()}`
7547
- });
7548
- this.logger.info(`light-rules-evaluator: subagent triggered runId=${result.runId}`);
7549
- } catch (err2) {
7550
- this.logger.warn(`light-rules-evaluator: subagent trigger failed: ${err2?.message ?? err2}`);
7551
- } finally {
7552
- this.subagentInFlight = false;
7553
- }
7554
- }
7555
- /**
7556
- * 按需创建 `light-rules-evaluator` job。
7557
- * job 已存在(内存缓存或 cron store),直接返回;否则调用 `cron.add`。
7558
- */
7559
- async ensureJobExists(cron) {
7560
- if (this.jobEnsured) return;
7561
- if (!this.ensureJobPromise) {
7562
- this.ensureJobPromise = this.createJobIfNeeded(cron).finally(() => {
7563
- this.ensureJobPromise = null;
7564
- });
7565
- }
7566
- await this.ensureJobPromise;
7567
- }
7568
- async createJobIfNeeded(cron) {
7569
- if (cron.getJob(EVALUATOR_JOB_ID)) {
7570
- this.jobEnsured = true;
7571
- return;
7572
- }
7573
- try {
7574
- await cron.add({
7575
- id: EVALUATOR_JOB_ID,
7576
- name: "\u706F\u6548\u89C4\u5219\u8BC4\u4F30",
7577
- 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",
7578
- enabled: true,
7579
- schedule: { kind: "cron", expr: FALLBACK_CRON_EXPR },
7580
- sessionTarget: "isolated",
7581
- wakeMode: "now",
7582
- payload: {
7583
- kind: "agentTurn",
7584
- message: buildEvaluatorJobMessage(this.getNotificationsDir() ?? "notifications")
7585
- }
7586
- });
7587
- this.logger.info("light-rules-evaluator: job created");
7588
- } catch (err2) {
7589
- if (!cron.getJob(EVALUATOR_JOB_ID)) {
7590
- throw err2;
7071
+ }, ["lightrules_update"]);
7072
+ registerToolWithAliases(api, {
7073
+ name: "lightrules.delete",
7074
+ label: "Delete Light Rule",
7075
+ 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',
7076
+ parameters: {
7077
+ type: "object",
7078
+ required: ["name"],
7079
+ additionalProperties: false,
7080
+ properties: {
7081
+ name: { type: "string", description: "\u8981\u5220\u9664\u7684\u89C4\u5219\u540D\u79F0" }
7082
+ }
7083
+ },
7084
+ async execute(_toolCallId, params) {
7085
+ const { name } = params;
7086
+ if (!name || typeof name !== "string")
7087
+ return err("INVALID_PARAMS", "name is required");
7088
+ try {
7089
+ const result = await registry.delete(name);
7090
+ logger.info(`lightrules.delete tool: deleted ${name}`);
7091
+ return ok({ ok: true, name: result.name, deleted: true, cronHint: result.cronHint });
7092
+ } catch (e) {
7093
+ if (e instanceof LightRuleError) return err(e.code, e.message);
7094
+ logger.warn(`lightrules.delete tool failed: ${e?.message}`);
7095
+ return err("INTERNAL_ERROR", e?.message ?? "Unknown error");
7591
7096
  }
7592
7097
  }
7593
- this.jobEnsured = true;
7594
- }
7595
- };
7098
+ }, ["lightrules_delete"]);
7099
+ }
7596
7100
 
7597
7101
  // src/light-rules/inline-evaluator.ts
7598
7102
  init_credentials();
7599
7103
 
7600
7104
  // src/light/sender.ts
7601
- var import_node_crypto2 = require("crypto");
7105
+ var import_node_crypto = require("crypto");
7602
7106
  init_env();
7603
7107
  async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
7604
7108
  const apiUrl = getEnvUrls().lightApiUrl;
@@ -7615,11 +7119,11 @@ async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
7615
7119
  }
7616
7120
  let bizContent;
7617
7121
  try {
7618
- bizContent = buildLightEffectApnsBody(segments, repeatInput);
7122
+ bizContent = buildLightEffectApnsBody(segments, repeatInput, reason);
7619
7123
  } catch (error) {
7620
7124
  return { ok: false, error: error?.message ?? String(error) };
7621
7125
  }
7622
- const bizUniqueId = (0, import_node_crypto2.randomUUID)();
7126
+ const bizUniqueId = (0, import_node_crypto.randomUUID)();
7623
7127
  const requestBody = {
7624
7128
  appKey,
7625
7129
  bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", reason },
@@ -7727,8 +7231,8 @@ var InlineLightRuleEvaluator = class {
7727
7231
  };
7728
7232
 
7729
7233
  // src/light-rules/migration.ts
7730
- var import_node_fs9 = require("fs");
7731
- var import_node_path8 = require("path");
7234
+ var import_node_fs8 = require("fs");
7235
+ var import_node_path7 = require("path");
7732
7236
  var NO_MATCH_FETCH_PY = `#!/usr/bin/env python3
7733
7237
  # \u6B64\u6587\u4EF6\u7531\u8FC1\u79FB\u5DE5\u5177\u751F\u6210\u3002
7734
7238
  # \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
@@ -7738,32 +7242,32 @@ function normalizeScriptText(text) {
7738
7242
  return text.replace(/\r\n/g, "\n").trim();
7739
7243
  }
7740
7244
  function resolveTasksDir(ctx) {
7741
- if (ctx.workspaceDir) return (0, import_node_path8.join)(ctx.workspaceDir, "tasks");
7245
+ if (ctx.workspaceDir) return (0, import_node_path7.join)(ctx.workspaceDir, "tasks");
7742
7246
  if (ctx.stateDir) {
7743
- const inferredWorkspaceDir = (0, import_node_path8.join)(ctx.stateDir, "workspace");
7744
- if ((0, import_node_fs9.existsSync)(inferredWorkspaceDir)) return (0, import_node_path8.join)(inferredWorkspaceDir, "tasks");
7745
- return (0, import_node_path8.join)(ctx.stateDir, "tasks");
7247
+ const inferredWorkspaceDir = (0, import_node_path7.join)(ctx.stateDir, "workspace");
7248
+ if ((0, import_node_fs8.existsSync)(inferredWorkspaceDir)) return (0, import_node_path7.join)(inferredWorkspaceDir, "tasks");
7249
+ return (0, import_node_path7.join)(ctx.stateDir, "tasks");
7746
7250
  }
7747
7251
  return null;
7748
7252
  }
7749
7253
  function migrateLegacyLightRuleTasks(ctx, logger) {
7750
7254
  const tasksDir3 = resolveTasksDir(ctx);
7751
- if (!tasksDir3 || !(0, import_node_fs9.existsSync)(tasksDir3)) return;
7255
+ if (!tasksDir3 || !(0, import_node_fs8.existsSync)(tasksDir3)) return;
7752
7256
  try {
7753
- for (const entry of (0, import_node_fs9.readdirSync)(tasksDir3, { withFileTypes: true })) {
7257
+ for (const entry of (0, import_node_fs8.readdirSync)(tasksDir3, { withFileTypes: true })) {
7754
7258
  if (!entry.isDirectory()) continue;
7755
- migrateTaskDir((0, import_node_path8.join)(tasksDir3, String(entry.name)), logger);
7259
+ migrateTaskDir((0, import_node_path7.join)(tasksDir3, String(entry.name)), logger);
7756
7260
  }
7757
7261
  } catch (err2) {
7758
7262
  logger.warn(`migration: failed to read tasks dir: ${err2?.message}`);
7759
7263
  }
7760
7264
  }
7761
7265
  function migrateTaskDir(taskDir, logger) {
7762
- const metaPath = (0, import_node_path8.join)(taskDir, "meta.json");
7763
- if (!(0, import_node_fs9.existsSync)(metaPath)) return;
7266
+ const metaPath = (0, import_node_path7.join)(taskDir, "meta.json");
7267
+ if (!(0, import_node_fs8.existsSync)(metaPath)) return;
7764
7268
  let meta;
7765
7269
  try {
7766
- meta = JSON.parse((0, import_node_fs9.readFileSync)(metaPath, "utf-8"));
7270
+ meta = JSON.parse((0, import_node_fs8.readFileSync)(metaPath, "utf-8"));
7767
7271
  } catch {
7768
7272
  return;
7769
7273
  }
@@ -7772,7 +7276,7 @@ function migrateTaskDir(taskDir, logger) {
7772
7276
  mergeMatchRulesIntoDescription(meta, name, metaPath, logger);
7773
7277
  replaceFetchPy(taskDir, name, logger);
7774
7278
  for (const filename of ["README.md", "checkpoint.json"]) {
7775
- removeFile((0, import_node_path8.join)(taskDir, filename), name, filename, logger);
7279
+ removeFile((0, import_node_path7.join)(taskDir, filename), name, filename, logger);
7776
7280
  }
7777
7281
  }
7778
7282
  function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
@@ -7793,30 +7297,30 @@ function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
7793
7297
  }
7794
7298
  delete meta.matchRules;
7795
7299
  try {
7796
- (0, import_node_fs9.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7300
+ (0, import_node_fs8.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7797
7301
  logger.info(`migration: merged matchRules into description for light rule: ${name}`);
7798
7302
  } catch (err2) {
7799
7303
  logger.warn(`migration: failed to update meta.json for ${name}: ${err2?.message}`);
7800
7304
  }
7801
7305
  }
7802
7306
  function replaceFetchPy(taskDir, name, logger) {
7803
- const fetchPyPath = (0, import_node_path8.join)(taskDir, "fetch.py");
7804
- if (!(0, import_node_fs9.existsSync)(fetchPyPath)) return;
7307
+ const fetchPyPath = (0, import_node_path7.join)(taskDir, "fetch.py");
7308
+ if (!(0, import_node_fs8.existsSync)(fetchPyPath)) return;
7805
7309
  try {
7806
- const existing = (0, import_node_fs9.readFileSync)(fetchPyPath, "utf-8");
7310
+ const existing = (0, import_node_fs8.readFileSync)(fetchPyPath, "utf-8");
7807
7311
  if (normalizeScriptText(existing) === normalizeScriptText(NO_MATCH_FETCH_PY)) {
7808
7312
  return;
7809
7313
  }
7810
- (0, import_node_fs9.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7314
+ (0, import_node_fs8.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7811
7315
  logger.info(`migration: replaced fetch.py with NO_MATCH placeholder for ${name}`);
7812
7316
  } catch (err2) {
7813
7317
  logger.warn(`migration: failed to replace fetch.py for ${name}: ${err2?.message}`);
7814
7318
  }
7815
7319
  }
7816
7320
  function removeFile(filePath, ruleName, filename, logger) {
7817
- if (!(0, import_node_fs9.existsSync)(filePath)) return;
7321
+ if (!(0, import_node_fs8.existsSync)(filePath)) return;
7818
7322
  try {
7819
- (0, import_node_fs9.rmSync)(filePath);
7323
+ (0, import_node_fs8.rmSync)(filePath);
7820
7324
  logger.info(`migration: removed ${filename} for light rule: ${ruleName}`);
7821
7325
  } catch (err2) {
7822
7326
  logger.warn(`migration: failed to remove ${filename} for ${ruleName}: ${err2?.message}`);
@@ -7987,7 +7491,7 @@ function resolveUpdateChannel(params) {
7987
7491
  }
7988
7492
 
7989
7493
  // src/update/index.ts
7990
- var import_node_path10 = require("path");
7494
+ var import_node_path9 = require("path");
7991
7495
 
7992
7496
  // src/update/checker.ts
7993
7497
  function parseSemver(v) {
@@ -8089,8 +7593,8 @@ var UpdateChecker = class {
8089
7593
  };
8090
7594
 
8091
7595
  // src/update/executor.ts
8092
- var import_node_fs10 = require("fs");
8093
- var import_node_path9 = require("path");
7596
+ var import_node_fs9 = require("fs");
7597
+ var import_node_path8 = require("path");
8094
7598
  var import_node_os = require("os");
8095
7599
  var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
8096
7600
  var BASE_URL = "https://artifact.yoooclaw.com/plugin";
@@ -8100,9 +7604,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8100
7604
  }
8101
7605
  const tgzUrl = `${BASE_URL}/v${version}/yoooclaw-phone-notifications-${version}.tgz`;
8102
7606
  logger.info(`\u6267\u884C\u66F4\u65B0: ${tgzUrl} \u2192 ${targetDir}`);
8103
- const workDir = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
8104
- const tgzPath = (0, import_node_path9.join)(workDir, "plugin.tgz");
8105
- const stagingDir = (0, import_node_path9.join)(workDir, "staged");
7607
+ const workDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path8.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
7608
+ const tgzPath = (0, import_node_path8.join)(workDir, "plugin.tgz");
7609
+ const stagingDir = (0, import_node_path8.join)(workDir, "staged");
8106
7610
  let backupDir = null;
8107
7611
  try {
8108
7612
  logger.info("\u4E0B\u8F7D\u63D2\u4EF6\u5305...");
@@ -8111,9 +7615,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8111
7615
  return { success: false, message: `\u4E0B\u8F7D\u5931\u8D25 (HTTP ${response.status}): ${tgzUrl}` };
8112
7616
  }
8113
7617
  const buffer = Buffer.from(await response.arrayBuffer());
8114
- (0, import_node_fs10.writeFileSync)(tgzPath, buffer);
7618
+ (0, import_node_fs9.writeFileSync)(tgzPath, buffer);
8115
7619
  logger.info(`\u4E0B\u8F7D\u5B8C\u6210 (${buffer.length} bytes)`);
8116
- (0, import_node_fs10.mkdirSync)(stagingDir, { recursive: true });
7620
+ (0, import_node_fs9.mkdirSync)(stagingDir, { recursive: true });
8117
7621
  const tarResult = await runCommand(
8118
7622
  ["tar", "-xzf", tgzPath, "-C", stagingDir, "--strip-components=1"],
8119
7623
  { timeoutMs: 3e4 }
@@ -8122,14 +7626,14 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8122
7626
  const err2 = tarResult.stderr || tarResult.stdout || "unknown error";
8123
7627
  return { success: false, message: `\u89E3\u538B\u5931\u8D25: ${err2}` };
8124
7628
  }
8125
- (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(targetDir), { recursive: true });
7629
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(targetDir), { recursive: true });
8126
7630
  try {
8127
7631
  backupDir = `${targetDir}.bak.${Date.now()}`;
8128
- (0, import_node_fs10.renameSync)(targetDir, backupDir);
7632
+ (0, import_node_fs9.renameSync)(targetDir, backupDir);
8129
7633
  } catch {
8130
7634
  backupDir = null;
8131
7635
  }
8132
- (0, import_node_fs10.renameSync)(stagingDir, targetDir);
7636
+ (0, import_node_fs9.renameSync)(stagingDir, targetDir);
8133
7637
  try {
8134
7638
  await updateConfigRecord2(version, tgzUrl);
8135
7639
  } catch (err2) {
@@ -8137,7 +7641,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8137
7641
  }
8138
7642
  if (backupDir) {
8139
7643
  try {
8140
- (0, import_node_fs10.rmSync)(backupDir, { force: true, recursive: true });
7644
+ (0, import_node_fs9.rmSync)(backupDir, { force: true, recursive: true });
8141
7645
  } catch {
8142
7646
  }
8143
7647
  }
@@ -8147,8 +7651,8 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8147
7651
  } catch (err2) {
8148
7652
  if (backupDir) {
8149
7653
  try {
8150
- (0, import_node_fs10.rmSync)(targetDir, { force: true, recursive: true });
8151
- (0, import_node_fs10.renameSync)(backupDir, targetDir);
7654
+ (0, import_node_fs9.rmSync)(targetDir, { force: true, recursive: true });
7655
+ (0, import_node_fs9.renameSync)(backupDir, targetDir);
8152
7656
  logger.info("\u5DF2\u56DE\u6EDA\u5230\u4E4B\u524D\u7248\u672C");
8153
7657
  } catch (rollbackErr) {
8154
7658
  logger.error(`\u56DE\u6EDA\u5931\u8D25: ${String(rollbackErr)}`);
@@ -8159,7 +7663,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
8159
7663
  return { success: false, message: errMsg };
8160
7664
  } finally {
8161
7665
  try {
8162
- (0, import_node_fs10.rmSync)(workDir, { force: true, recursive: true });
7666
+ (0, import_node_fs9.rmSync)(workDir, { force: true, recursive: true });
8163
7667
  } catch {
8164
7668
  }
8165
7669
  }
@@ -8174,7 +7678,7 @@ function resolveTargetDir(api) {
8174
7678
  if (installPath) return installPath;
8175
7679
  } catch {
8176
7680
  }
8177
- return (0, import_node_path10.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
7681
+ return (0, import_node_path9.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
8178
7682
  }
8179
7683
  async function updateConfigRecord(api, version, targetDir, tgzUrl) {
8180
7684
  const configApi = api.runtime.config;
@@ -8362,10 +7866,10 @@ function registerAutoUpdateLifecycle(deps) {
8362
7866
  }
8363
7867
 
8364
7868
  // src/plugin/cli.ts
8365
- var import_node_path18 = require("path");
7869
+ var import_node_path17 = require("path");
8366
7870
 
8367
7871
  // src/cli/auth.ts
8368
- var import_node_fs11 = require("fs");
7872
+ var import_node_fs10 = require("fs");
8369
7873
  init_credentials();
8370
7874
  function registerAuthCli(program) {
8371
7875
  const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
@@ -8401,12 +7905,12 @@ function registerAuthCli(program) {
8401
7905
  });
8402
7906
  auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
8403
7907
  const path2 = credentialsPath();
8404
- if ((0, import_node_fs11.existsSync)(path2)) {
7908
+ if ((0, import_node_fs10.existsSync)(path2)) {
8405
7909
  const creds = readCredentials();
8406
7910
  delete creds.apiKey;
8407
7911
  delete creds.token;
8408
7912
  if (Object.keys(creds).length === 0) {
8409
- (0, import_node_fs11.rmSync)(path2, { force: true });
7913
+ (0, import_node_fs10.rmSync)(path2, { force: true });
8410
7914
  } else {
8411
7915
  writeCredentials(creds);
8412
7916
  }
@@ -8538,23 +8042,23 @@ function registerNtfStats(ntf, ctx) {
8538
8042
  }
8539
8043
 
8540
8044
  // src/cli/ntf-sync.ts
8541
- var import_node_fs12 = require("fs");
8542
- var import_node_path11 = require("path");
8045
+ var import_node_fs11 = require("fs");
8046
+ var import_node_path10 = require("path");
8543
8047
  var SYNC_FETCH_LIMIT = 300;
8544
8048
  function checkpointPath(dir) {
8545
- return (0, import_node_path11.join)(dir, ".checkpoint.json");
8049
+ return (0, import_node_path10.join)(dir, ".checkpoint.json");
8546
8050
  }
8547
8051
  function readCheckpoint(dir) {
8548
8052
  const p = checkpointPath(dir);
8549
- if (!(0, import_node_fs12.existsSync)(p)) return {};
8053
+ if (!(0, import_node_fs11.existsSync)(p)) return {};
8550
8054
  try {
8551
- return JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
8055
+ return JSON.parse((0, import_node_fs11.readFileSync)(p, "utf-8"));
8552
8056
  } catch {
8553
8057
  return {};
8554
8058
  }
8555
8059
  }
8556
8060
  function writeCheckpoint(dir, data) {
8557
- (0, import_node_fs12.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8061
+ (0, import_node_fs11.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8558
8062
  }
8559
8063
  function registerNtfSync(ntf, ctx) {
8560
8064
  const sync = ntf.command("sync").description("\u540C\u6B65\u901A\u77E5\u5230\u8BB0\u5FC6\u7CFB\u7EDF");
@@ -8647,24 +8151,24 @@ function registerNtfSync(ntf, ctx) {
8647
8151
  }
8648
8152
 
8649
8153
  // src/cli/ntf-monitor.ts
8650
- var import_node_fs13 = require("fs");
8651
- var import_node_path12 = require("path");
8154
+ var import_node_fs12 = require("fs");
8155
+ var import_node_path11 = require("path");
8652
8156
  function tasksDir2(ctx) {
8653
8157
  const base = ctx.workspaceDir || ctx.stateDir;
8654
8158
  if (!base) throw new Error("workspaceDir and stateDir both unavailable");
8655
- return (0, import_node_path12.join)(base, "tasks");
8159
+ return (0, import_node_path11.join)(base, "tasks");
8656
8160
  }
8657
8161
  function readMeta2(taskDir) {
8658
- const metaPath = (0, import_node_path12.join)(taskDir, "meta.json");
8659
- if (!(0, import_node_fs13.existsSync)(metaPath)) return null;
8162
+ const metaPath = (0, import_node_path11.join)(taskDir, "meta.json");
8163
+ if (!(0, import_node_fs12.existsSync)(metaPath)) return null;
8660
8164
  try {
8661
- return JSON.parse((0, import_node_fs13.readFileSync)(metaPath, "utf-8"));
8165
+ return JSON.parse((0, import_node_fs12.readFileSync)(metaPath, "utf-8"));
8662
8166
  } catch {
8663
8167
  return null;
8664
8168
  }
8665
8169
  }
8666
8170
  function writeMeta2(taskDir, meta) {
8667
- (0, import_node_fs13.writeFileSync)((0, import_node_path12.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
8171
+ (0, import_node_fs12.writeFileSync)((0, import_node_path11.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
8668
8172
  }
8669
8173
  function generateReadme(name, description) {
8670
8174
  return `# Monitor Task: ${name}
@@ -8683,27 +8187,27 @@ function registerNtfMonitor(ntf, ctx) {
8683
8187
  const monitor = ntf.command("monitor").description("\u901A\u77E5\u76D1\u63A7\u4EFB\u52A1\u7BA1\u7406");
8684
8188
  monitor.command("list").description("\u5217\u51FA\u6240\u6709\u76D1\u63A7\u4EFB\u52A1").action(() => {
8685
8189
  const dir = tasksDir2(ctx);
8686
- if (!(0, import_node_fs13.existsSync)(dir)) {
8190
+ if (!(0, import_node_fs12.existsSync)(dir)) {
8687
8191
  output({ ok: true, tasks: [] });
8688
8192
  return;
8689
8193
  }
8690
8194
  const tasks = [];
8691
- for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
8195
+ for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
8692
8196
  if (!entry.isDirectory()) continue;
8693
- const meta = readMeta2((0, import_node_path12.join)(dir, entry.name));
8197
+ const meta = readMeta2((0, import_node_path11.join)(dir, entry.name));
8694
8198
  if (meta) tasks.push(meta);
8695
8199
  }
8696
8200
  output({ ok: true, tasks });
8697
8201
  });
8698
8202
  monitor.command("show <name>").description("\u67E5\u770B\u76D1\u63A7\u4EFB\u52A1\u8BE6\u60C5").action((name) => {
8699
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8203
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8700
8204
  const meta = readMeta2(taskDir);
8701
8205
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8702
- const checkpointPath2 = (0, import_node_path12.join)(taskDir, "checkpoint.json");
8206
+ const checkpointPath2 = (0, import_node_path11.join)(taskDir, "checkpoint.json");
8703
8207
  let checkpoint = {};
8704
- if ((0, import_node_fs13.existsSync)(checkpointPath2)) {
8208
+ if ((0, import_node_fs12.existsSync)(checkpointPath2)) {
8705
8209
  try {
8706
- checkpoint = JSON.parse((0, import_node_fs13.readFileSync)(checkpointPath2, "utf-8"));
8210
+ checkpoint = JSON.parse((0, import_node_fs12.readFileSync)(checkpointPath2, "utf-8"));
8707
8211
  } catch {
8708
8212
  }
8709
8213
  }
@@ -8718,8 +8222,8 @@ function registerNtfMonitor(ntf, ctx) {
8718
8222
  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(
8719
8223
  (name, opts) => {
8720
8224
  const dir = tasksDir2(ctx);
8721
- const taskDir = (0, import_node_path12.join)(dir, name);
8722
- if ((0, import_node_fs13.existsSync)(taskDir)) {
8225
+ const taskDir = (0, import_node_path11.join)(dir, name);
8226
+ if ((0, import_node_fs12.existsSync)(taskDir)) {
8723
8227
  exitError("ALREADY_EXISTS", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u5DF2\u5B58\u5728`);
8724
8228
  }
8725
8229
  let matchRules;
@@ -8731,7 +8235,7 @@ function registerNtfMonitor(ntf, ctx) {
8731
8235
  "match-rules \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON"
8732
8236
  );
8733
8237
  }
8734
- (0, import_node_fs13.mkdirSync)(taskDir, { recursive: true });
8238
+ (0, import_node_fs12.mkdirSync)(taskDir, { recursive: true });
8735
8239
  const meta = {
8736
8240
  name,
8737
8241
  description: opts.description,
@@ -8741,13 +8245,13 @@ function registerNtfMonitor(ntf, ctx) {
8741
8245
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8742
8246
  };
8743
8247
  writeMeta2(taskDir, meta);
8744
- (0, import_node_fs13.writeFileSync)(
8745
- (0, import_node_path12.join)(taskDir, "fetch.py"),
8248
+ (0, import_node_fs12.writeFileSync)(
8249
+ (0, import_node_path11.join)(taskDir, "fetch.py"),
8746
8250
  generateFetchPy(name, matchRules),
8747
8251
  "utf-8"
8748
8252
  );
8749
- (0, import_node_fs13.writeFileSync)(
8750
- (0, import_node_path12.join)(taskDir, "README.md"),
8253
+ (0, import_node_fs12.writeFileSync)(
8254
+ (0, import_node_path11.join)(taskDir, "README.md"),
8751
8255
  generateReadme(name, opts.description),
8752
8256
  "utf-8"
8753
8257
  );
@@ -8775,8 +8279,8 @@ function registerNtfMonitor(ntf, ctx) {
8775
8279
  }
8776
8280
  );
8777
8281
  monitor.command("delete <name>").description("\u5220\u9664\u76D1\u63A7\u4EFB\u52A1").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").action((name, opts) => {
8778
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8779
- if (!(0, import_node_fs13.existsSync)(taskDir)) {
8282
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8283
+ if (!(0, import_node_fs12.existsSync)(taskDir)) {
8780
8284
  exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8781
8285
  }
8782
8286
  if (!opts.yes) {
@@ -8789,7 +8293,7 @@ function registerNtfMonitor(ntf, ctx) {
8789
8293
  });
8790
8294
  process.exit(1);
8791
8295
  }
8792
- (0, import_node_fs13.rmSync)(taskDir, { recursive: true, force: true });
8296
+ (0, import_node_fs12.rmSync)(taskDir, { recursive: true, force: true });
8793
8297
  output({
8794
8298
  ok: true,
8795
8299
  name,
@@ -8801,7 +8305,7 @@ function registerNtfMonitor(ntf, ctx) {
8801
8305
  });
8802
8306
  });
8803
8307
  monitor.command("enable <name>").description("\u542F\u7528\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8804
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8308
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8805
8309
  const meta = readMeta2(taskDir);
8806
8310
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8807
8311
  meta.enabled = true;
@@ -8809,7 +8313,7 @@ function registerNtfMonitor(ntf, ctx) {
8809
8313
  output({ ok: true, name, enabled: true });
8810
8314
  });
8811
8315
  monitor.command("disable <name>").description("\u6682\u505C\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8812
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8316
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8813
8317
  const meta = readMeta2(taskDir);
8814
8318
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8815
8319
  meta.enabled = false;
@@ -8853,9 +8357,9 @@ function registerLightSend(light) {
8853
8357
  }
8854
8358
 
8855
8359
  // src/cli/light-setup-tools.ts
8856
- var import_node_fs14 = require("fs");
8360
+ var import_node_fs13 = require("fs");
8857
8361
  var import_node_os2 = require("os");
8858
- var import_node_path13 = require("path");
8362
+ var import_node_path12 = require("path");
8859
8363
  function isObject(value) {
8860
8364
  return !!value && typeof value === "object" && !Array.isArray(value);
8861
8365
  }
@@ -8869,7 +8373,7 @@ function ensureArray(obj, key) {
8869
8373
  function resolveConfigPath2() {
8870
8374
  const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
8871
8375
  if (fromEnv) return fromEnv;
8872
- return (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8376
+ return (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8873
8377
  }
8874
8378
  var LIGHT_TOOLS = [
8875
8379
  "light_control",
@@ -8916,12 +8420,12 @@ function upsertLightControlAlsoAllow(cfg) {
8916
8420
  function registerLightSetupTools(light) {
8917
8421
  light.command("setup").description("\u81EA\u52A8\u653E\u884C light_control\uFF08\u5199\u5165 tools.alsoAllow \u4E0E agents.main.tools.alsoAllow\uFF09").action(() => {
8918
8422
  const configPath = resolveConfigPath2();
8919
- if (!(0, import_node_fs14.existsSync)(configPath)) {
8423
+ if (!(0, import_node_fs13.existsSync)(configPath)) {
8920
8424
  exitError("CONFIG_NOT_FOUND", `\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6: ${configPath}`);
8921
8425
  }
8922
8426
  let cfg = {};
8923
8427
  try {
8924
- const raw = (0, import_node_fs14.readFileSync)(configPath, "utf-8");
8428
+ const raw = (0, import_node_fs13.readFileSync)(configPath, "utf-8");
8925
8429
  const parsed = JSON.parse(raw);
8926
8430
  if (isObject(parsed)) cfg = parsed;
8927
8431
  } catch (err2) {
@@ -8929,8 +8433,8 @@ function registerLightSetupTools(light) {
8929
8433
  }
8930
8434
  const result = upsertLightControlAlsoAllow(cfg);
8931
8435
  try {
8932
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
8933
- (0, import_node_fs14.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8436
+ (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
8437
+ (0, import_node_fs13.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8934
8438
  } catch (err2) {
8935
8439
  exitError("WRITE_FAILED", `\u5199\u5165\u914D\u7F6E\u5931\u8D25: ${err2?.message ?? String(err2)}`);
8936
8440
  }
@@ -8948,17 +8452,17 @@ function registerLightSetupTools(light) {
8948
8452
  }
8949
8453
 
8950
8454
  // src/cli/tunnel-status.ts
8951
- var import_node_fs15 = require("fs");
8952
- var import_node_path14 = require("path");
8455
+ var import_node_fs14 = require("fs");
8456
+ var import_node_path13 = require("path");
8953
8457
  init_credentials();
8954
8458
  init_env();
8955
- var STATUS_REL_PATH = (0, import_node_path14.join)("plugins", "phone-notifications", "tunnel-status.json");
8459
+ var STATUS_REL_PATH = (0, import_node_path13.join)("plugins", "phone-notifications", "tunnel-status.json");
8956
8460
  function readTunnelStatus(ctx) {
8957
8461
  if (!ctx.stateDir) return null;
8958
- const filePath = (0, import_node_path14.join)(ctx.stateDir, STATUS_REL_PATH);
8959
- if (!(0, import_node_fs15.existsSync)(filePath)) return null;
8462
+ const filePath = (0, import_node_path13.join)(ctx.stateDir, STATUS_REL_PATH);
8463
+ if (!(0, import_node_fs14.existsSync)(filePath)) return null;
8960
8464
  try {
8961
- return JSON.parse((0, import_node_fs15.readFileSync)(filePath, "utf-8"));
8465
+ return JSON.parse((0, import_node_fs14.readFileSync)(filePath, "utf-8"));
8962
8466
  } catch {
8963
8467
  return null;
8964
8468
  }
@@ -9026,24 +8530,24 @@ function registerNtfStoragePath(ntf, ctx) {
9026
8530
  }
9027
8531
 
9028
8532
  // src/cli/log-search.ts
9029
- var import_node_fs16 = require("fs");
9030
- var import_node_path15 = require("path");
8533
+ var import_node_fs15 = require("fs");
8534
+ var import_node_path14 = require("path");
9031
8535
  function resolveLogsDir(ctx) {
9032
8536
  if (ctx.stateDir) {
9033
- const dir = (0, import_node_path15.join)(
8537
+ const dir = (0, import_node_path14.join)(
9034
8538
  ctx.stateDir,
9035
8539
  "plugins",
9036
8540
  "phone-notifications",
9037
8541
  "logs"
9038
8542
  );
9039
- if ((0, import_node_fs16.existsSync)(dir)) return dir;
8543
+ if ((0, import_node_fs15.existsSync)(dir)) return dir;
9040
8544
  }
9041
8545
  return null;
9042
8546
  }
9043
8547
  function listLogDateKeys(dir) {
9044
8548
  const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
9045
8549
  const keys = [];
9046
- for (const entry of (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true })) {
8550
+ for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
9047
8551
  if (!entry.isFile()) continue;
9048
8552
  const m = pattern.exec(entry.name);
9049
8553
  if (m) keys.push(m[1]);
@@ -9051,9 +8555,9 @@ function listLogDateKeys(dir) {
9051
8555
  return keys.sort().reverse();
9052
8556
  }
9053
8557
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
9054
- const filePath = (0, import_node_path15.join)(dir, `${dateKey}.log`);
9055
- if (!(0, import_node_fs16.existsSync)(filePath)) return;
9056
- const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
8558
+ const filePath = (0, import_node_path14.join)(dir, `${dateKey}.log`);
8559
+ if (!(0, import_node_fs15.existsSync)(filePath)) return;
8560
+ const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
9057
8561
  const lowerKeyword = keyword?.toLowerCase();
9058
8562
  for (const line of content.split("\n")) {
9059
8563
  if (collected.length >= limit) return;
@@ -9120,12 +8624,12 @@ function registerEnvCli(ntf) {
9120
8624
  }
9121
8625
 
9122
8626
  // src/cli/doctor.ts
9123
- var import_node_fs20 = require("fs");
8627
+ var import_node_fs19 = require("fs");
9124
8628
  var import_node_readline = require("readline");
9125
8629
  init_host();
9126
8630
 
9127
8631
  // src/cli/doctor/check-dangerous-flags.ts
9128
- var import_node_fs17 = require("fs");
8632
+ var import_node_fs16 = require("fs");
9129
8633
  function isObject2(v) {
9130
8634
  return !!v && typeof v === "object" && !Array.isArray(v);
9131
8635
  }
@@ -9142,13 +8646,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
9142
8646
  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",
9143
8647
  fixDescription: "\u8BBE\u4E3A false",
9144
8648
  fix: () => {
9145
- const raw = (0, import_node_fs17.readFileSync)(configPath, "utf-8");
8649
+ const raw = (0, import_node_fs16.readFileSync)(configPath, "utf-8");
9146
8650
  const config = JSON.parse(raw);
9147
8651
  const gw = config.gateway;
9148
8652
  const cui = gw.controlUi;
9149
8653
  cui.dangerouslyDisableDeviceAuth = false;
9150
- (0, import_node_fs17.copyFileSync)(configPath, configPath + ".bak");
9151
- (0, import_node_fs17.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
8654
+ (0, import_node_fs16.copyFileSync)(configPath, configPath + ".bak");
8655
+ (0, import_node_fs16.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
9152
8656
  }
9153
8657
  };
9154
8658
  };
@@ -9196,11 +8700,11 @@ function warnEmpty() {
9196
8700
  }
9197
8701
 
9198
8702
  // src/cli/doctor/check-state-dir-perms.ts
9199
- var import_node_fs18 = require("fs");
8703
+ var import_node_fs17 = require("fs");
9200
8704
  var checkStateDirPerms = ({ stateDir }) => {
9201
8705
  let mode;
9202
8706
  try {
9203
- mode = (0, import_node_fs18.statSync)(stateDir).mode;
8707
+ mode = (0, import_node_fs17.statSync)(stateDir).mode;
9204
8708
  } catch {
9205
8709
  return null;
9206
8710
  }
@@ -9214,7 +8718,7 @@ var checkStateDirPerms = ({ stateDir }) => {
9214
8718
  detail: "\u5176\u4ED6\u7528\u6237\u53EF\u4EE5\u8BFB\u53D6\u8BE5\u76EE\u5F55\u4E0B\u7684\u51ED\u8BC1\u548C\u914D\u7F6E\u6587\u4EF6\u3002",
9215
8719
  fixDescription: "chmod 700 " + stateDir,
9216
8720
  fix: () => {
9217
- (0, import_node_fs18.chmodSync)(stateDir, 448);
8721
+ (0, import_node_fs17.chmodSync)(stateDir, 448);
9218
8722
  }
9219
8723
  };
9220
8724
  };
@@ -9269,16 +8773,16 @@ var checkCredentials = () => {
9269
8773
  };
9270
8774
 
9271
8775
  // src/cli/doctor/check-tunnel.ts
9272
- var import_node_fs19 = require("fs");
9273
- var import_node_path16 = require("path");
9274
- var STATUS_REL_PATH2 = (0, import_node_path16.join)(
8776
+ var import_node_fs18 = require("fs");
8777
+ var import_node_path15 = require("path");
8778
+ var STATUS_REL_PATH2 = (0, import_node_path15.join)(
9275
8779
  "plugins",
9276
8780
  "phone-notifications",
9277
8781
  "tunnel-status.json"
9278
8782
  );
9279
8783
  var checkTunnel = ({ stateDir }) => {
9280
- const filePath = (0, import_node_path16.join)(stateDir, STATUS_REL_PATH2);
9281
- if (!(0, import_node_fs19.existsSync)(filePath)) {
8784
+ const filePath = (0, import_node_path15.join)(stateDir, STATUS_REL_PATH2);
8785
+ if (!(0, import_node_fs18.existsSync)(filePath)) {
9282
8786
  return {
9283
8787
  id: "tunnel",
9284
8788
  severity: "warn",
@@ -9290,7 +8794,7 @@ var checkTunnel = ({ stateDir }) => {
9290
8794
  }
9291
8795
  let status;
9292
8796
  try {
9293
- status = JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
8797
+ status = JSON.parse((0, import_node_fs18.readFileSync)(filePath, "utf-8"));
9294
8798
  } catch {
9295
8799
  return {
9296
8800
  id: "tunnel",
@@ -9383,9 +8887,9 @@ function isObject5(v) {
9383
8887
  return !!v && typeof v === "object" && !Array.isArray(v);
9384
8888
  }
9385
8889
  function readConfig(configPath) {
9386
- if (!(0, import_node_fs20.existsSync)(configPath)) return {};
8890
+ if (!(0, import_node_fs19.existsSync)(configPath)) return {};
9387
8891
  try {
9388
- const parsed = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
8892
+ const parsed = JSON.parse((0, import_node_fs19.readFileSync)(configPath, "utf-8"));
9389
8893
  return isObject5(parsed) ? parsed : {};
9390
8894
  } catch {
9391
8895
  return {};
@@ -9586,7 +9090,7 @@ function registerRecStoragePath(rec, ctx) {
9586
9090
 
9587
9091
  // src/cli/rec-setup.ts
9588
9092
  var import_node_readline2 = require("readline");
9589
- var import_node_fs21 = require("fs");
9093
+ var import_node_fs20 = require("fs");
9590
9094
  function ask(rl, question) {
9591
9095
  return new Promise((resolve) => rl.question(question, resolve));
9592
9096
  }
@@ -9672,9 +9176,9 @@ async function setupLocal(rl) {
9672
9176
  function registerRecSetup(rec, ctx) {
9673
9177
  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 () => {
9674
9178
  const configPath = resolveAsrConfigPath(ctx);
9675
- if ((0, import_node_fs21.existsSync)(configPath)) {
9179
+ if ((0, import_node_fs20.existsSync)(configPath)) {
9676
9180
  try {
9677
- const existing = JSON.parse((0, import_node_fs21.readFileSync)(configPath, "utf-8"));
9181
+ const existing = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
9678
9182
  process.stderr.write(`\u5F53\u524D\u5DF2\u6709\u914D\u7F6E\uFF1Amode = ${existing.mode}`);
9679
9183
  if (existing.updatedAt) process.stderr.write(`\uFF0C\u66F4\u65B0\u4E8E ${existing.updatedAt}`);
9680
9184
  process.stderr.write("\n");
@@ -9687,7 +9191,7 @@ function registerRecSetup(rec, ctx) {
9687
9191
  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"]);
9688
9192
  const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
9689
9193
  const stored = { ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
9690
- (0, import_node_fs21.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9194
+ (0, import_node_fs20.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9691
9195
  process.stderr.write(`
9692
9196
  \u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 ${configPath}
9693
9197
 
@@ -9701,8 +9205,8 @@ function registerRecSetup(rec, ctx) {
9701
9205
 
9702
9206
  // src/cli/update.ts
9703
9207
  var import_node_child_process = require("child_process");
9704
- var import_node_fs22 = require("fs");
9705
- var import_node_path17 = require("path");
9208
+ var import_node_fs21 = require("fs");
9209
+ var import_node_path16 = require("path");
9706
9210
  var import_node_os3 = __toESM(require("os"), 1);
9707
9211
  init_host();
9708
9212
  var BASE_URL2 = "https://artifact.yoooclaw.com/plugin";
@@ -9761,9 +9265,9 @@ async function runUpdate(ctx, opts) {
9761
9265
  `);
9762
9266
  process.exit(1);
9763
9267
  }
9764
- const tmpScript = (0, import_node_path17.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9268
+ const tmpScript = (0, import_node_path16.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9765
9269
  try {
9766
- (0, import_node_fs22.writeFileSync)(tmpScript, installScript, "utf-8");
9270
+ (0, import_node_fs21.writeFileSync)(tmpScript, installScript, "utf-8");
9767
9271
  } catch (err2) {
9768
9272
  const msg = `\u5199\u5165\u4E34\u65F6\u6587\u4EF6\u5931\u8D25: ${err2?.message ?? String(err2)}`;
9769
9273
  if (json) {
@@ -9781,7 +9285,7 @@ async function runUpdate(ctx, opts) {
9781
9285
  { stdio: "inherit" }
9782
9286
  );
9783
9287
  try {
9784
- (0, import_node_fs22.unlinkSync)(tmpScript);
9288
+ (0, import_node_fs21.unlinkSync)(tmpScript);
9785
9289
  } catch {
9786
9290
  }
9787
9291
  if (result.error) {
@@ -9845,10 +9349,10 @@ function inferOpenClawRootDir(workspaceDir) {
9845
9349
  if (!workspaceDir) {
9846
9350
  return void 0;
9847
9351
  }
9848
- if ((0, import_node_path18.basename)(workspaceDir) !== "workspace") {
9352
+ if ((0, import_node_path17.basename)(workspaceDir) !== "workspace") {
9849
9353
  return void 0;
9850
9354
  }
9851
- return (0, import_node_path18.dirname)(workspaceDir);
9355
+ return (0, import_node_path17.dirname)(workspaceDir);
9852
9356
  }
9853
9357
  function registerPluginCli(api, params) {
9854
9358
  const { logger, openclawDir } = params;
@@ -10015,214 +9519,530 @@ var lightControlParameters = {
10015
9519
  }
10016
9520
  }
10017
9521
  },
10018
- reason: {
10019
- type: "string",
10020
- description: "\u7ED3\u5408\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u7B80\u8981\u8BF4\u660E\u4E3A\u4EC0\u4E48\u8981\u4EAE\u706F\uFF08\u4F8B\u5982\uFF1A\u7528\u6237\u5FC3\u60C5\u4F4E\u843D\u60F3\u8981\u6E29\u6696\u6C1B\u56F4\u3001\u5E86\u795D\u751F\u65E5\u3001\u63D0\u9192\u559D\u6C34\u7B49\uFF09\u3002\u8FD9\u4E2A\u5B57\u6BB5\u5E2E\u52A9\u8BB0\u5F55\u6BCF\u6B21\u4EAE\u706F\u7684\u610F\u56FE\u548C\u52A8\u673A\u3002"
10021
- },
10022
- repeat: {
10023
- type: "boolean",
10024
- description: "\u517C\u5BB9\u65E7\u53C2\u6570\u3002true = \u65E0\u9650\u5FAA\u73AF\uFF0Cfalse/\u4E0D\u586B = \u4E00\u8F6E\uFF1B\u82E5\u540C\u65F6\u63D0\u4F9B repeat_times\uFF0C\u5219\u4EE5\u540E\u8005\u4E3A\u51C6"
9522
+ reason: {
9523
+ type: "string",
9524
+ description: "\u7ED3\u5408\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u7B80\u8981\u8BF4\u660E\u4E3A\u4EC0\u4E48\u8981\u4EAE\u706F\uFF08\u4F8B\u5982\uFF1A\u7528\u6237\u5FC3\u60C5\u4F4E\u843D\u60F3\u8981\u6E29\u6696\u6C1B\u56F4\u3001\u5E86\u795D\u751F\u65E5\u3001\u63D0\u9192\u559D\u6C34\u7B49\uFF09\u3002\u8FD9\u4E2A\u5B57\u6BB5\u5E2E\u52A9\u8BB0\u5F55\u6BCF\u6B21\u4EAE\u706F\u7684\u610F\u56FE\u548C\u52A8\u673A\u3002"
9525
+ },
9526
+ repeat: {
9527
+ type: "boolean",
9528
+ description: "\u517C\u5BB9\u65E7\u53C2\u6570\u3002true = \u65E0\u9650\u5FAA\u73AF\uFF0Cfalse/\u4E0D\u586B = \u4E00\u8F6E\uFF1B\u82E5\u540C\u65F6\u63D0\u4F9B repeat_times\uFF0C\u5219\u4EE5\u540E\u8005\u4E3A\u51C6"
9529
+ },
9530
+ repeat_times: {
9531
+ type: "number",
9532
+ minimum: 0,
9533
+ description: "\u6574\u6761\u7EC4\u5408\u91CD\u590D\u6B21\u6570\uFF1A0 = \u65E0\u9650\u5FAA\u73AF\uFF0C1 = \u64AD\u653E\u4E00\u8F6E\u3002\u5F53\u524D ANCS \u8DEF\u5F84\u4E0D\u652F\u6301 N>=2"
9534
+ }
9535
+ }
9536
+ };
9537
+ function registerLightControlTool(api, logger) {
9538
+ api.registerTool({
9539
+ name: "light_control",
9540
+ label: "Light Control",
9541
+ description: '\u63A7\u5236\u786C\u4EF6\u706F\u6548\uFF0C\u652F\u6301 1\u201312 \u6BB5\u987A\u5E8F\u706F\u6548\uFF0C\u6BCF\u6BB5\u72EC\u7ACB\u53C2\u6570\uFF086 \u79CD\u6A21\u5F0F\uFF1A\u6CE2\u6D6A/\u547C\u5438/\u9891\u95EA/\u5E38\u4EAE/\u6D41\u5149/\u9010\u7EC4\u50CF\u7D20\u5E27\uFF09\u3002Tool \u5185\u90E8\u81EA\u52A8\u91CF\u5316\u4E3A\u5D4C\u5165\u5F0F\u534F\u8BAE\u5B9A\u4E49\u7684\u79BB\u6563\u6863\u4F4D\uFF0C\u5E76\u6309\u6A21\u5F0F\u7CBE\u7B80\u4E3A\u53D8\u957F ANCS \u63A7\u5236\u5E8F\u5217\uFF0C\u901A\u8FC7 HTTP \u63A5\u53E3\u4E0B\u53D1\u706F\u6548\u6307\u4EE4\u3002\u5F53\u7528\u6237\u8BF4"\u5F00\u706F"\u3001"\u6362\u4E2A\u706F\u6548"\u3001"\u628A\u706F\u8C03\u6210\u7EA2\u8272"\u3001"\u8425\u9020\u6C1B\u56F4"\u7B49\u4E0E\u706F\u5149\u63A7\u5236\u76F8\u5173\u7684\u8868\u8FBE\u65F6\u8C03\u7528\u3002',
9542
+ parameters: lightControlParameters,
9543
+ async execute(_toolCallId, params) {
9544
+ let apiKey;
9545
+ try {
9546
+ apiKey = requireApiKey();
9547
+ } catch (e) {
9548
+ return {
9549
+ content: [{ type: "text", text: e.message }],
9550
+ details: { ok: false, error: { code: "AUTH_REQUIRED", message: e.message } }
9551
+ };
9552
+ }
9553
+ const { segments, reason, repeat, repeat_times } = params;
9554
+ const validation = validateSegments(segments);
9555
+ if (!validation.valid) {
9556
+ return {
9557
+ content: [{ type: "text", text: JSON.stringify(validation.errors) }],
9558
+ details: {
9559
+ ok: false,
9560
+ error: { code: "VALIDATION_FAILED", details: validation.errors }
9561
+ }
9562
+ };
9563
+ }
9564
+ let repeatTimes;
9565
+ try {
9566
+ repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
9567
+ assertAncsRepeatTimes(repeatTimes);
9568
+ } catch (error) {
9569
+ return {
9570
+ content: [{ type: "text", text: error?.message ?? String(error) }],
9571
+ details: {
9572
+ ok: false,
9573
+ error: { code: "VALIDATION_FAILED", message: error?.message ?? String(error) }
9574
+ }
9575
+ };
9576
+ }
9577
+ logger.info(`Light control reason: ${reason}`);
9578
+ const result = await sendLightEffect(
9579
+ apiKey,
9580
+ validation.segments,
9581
+ logger,
9582
+ { repeat_times: repeatTimes },
9583
+ reason
9584
+ );
9585
+ if (!result.ok) {
9586
+ logger.warn(
9587
+ `Light control HTTP request failed: ${result.status} ${result.error}`
9588
+ );
9589
+ } else {
9590
+ logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
9591
+ }
9592
+ return {
9593
+ content: [{ type: "text", text: JSON.stringify(result) }],
9594
+ details: result
9595
+ };
9596
+ }
9597
+ });
9598
+ }
9599
+
9600
+ // src/plugin/lifecycle.ts
9601
+ var import_node_fs31 = require("fs");
9602
+ init_host();
9603
+
9604
+ // src/notification/app-name-map.ts
9605
+ var import_node_fs22 = require("fs");
9606
+ var import_node_path18 = require("path");
9607
+ init_credentials();
9608
+ init_env();
9609
+ var PLUGIN_STATE_DIR = "phone-notifications";
9610
+ var CACHE_FILE = "app-name-map.json";
9611
+ var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
9612
+ var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
9613
+ var APP_NAME_MAP_REFRESH_HOURS = 12;
9614
+ function isRecordOfStrings(v) {
9615
+ if (v === null || typeof v !== "object") return false;
9616
+ for (const val of Object.values(v)) if (typeof val !== "string") return false;
9617
+ return true;
9618
+ }
9619
+ function isAppNameMapApiResponse(v) {
9620
+ if (v === null || typeof v !== "object") return false;
9621
+ const o = v;
9622
+ return Array.isArray(o.data) && o.data.every(
9623
+ (item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string"
9624
+ );
9625
+ }
9626
+ function getCachePath(stateDir) {
9627
+ return (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
9628
+ }
9629
+ function createAppNameMapProvider(opts) {
9630
+ const { stateDir, logger } = opts;
9631
+ const url = APP_NAME_MAP_URL;
9632
+ const refreshHours = APP_NAME_MAP_REFRESH_HOURS;
9633
+ const map = /* @__PURE__ */ new Map();
9634
+ let refreshTimer = null;
9635
+ let stopWatching = null;
9636
+ let inFlightFetch = null;
9637
+ function loadFromDisk() {
9638
+ const path2 = getCachePath(stateDir);
9639
+ if (!(0, import_node_fs22.existsSync)(path2)) return;
9640
+ try {
9641
+ const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf-8"));
9642
+ if (!isRecordOfStrings(raw)) return;
9643
+ map.clear();
9644
+ for (const [k, v] of Object.entries(raw)) map.set(k, v);
9645
+ logger.info(`[app-name-map] loaded ${map.size} entries from cache: ${path2}`);
9646
+ } catch {
9647
+ }
9648
+ }
9649
+ async function fetchFromServer() {
9650
+ const apiKey = loadApiKey();
9651
+ if (!url) {
9652
+ logger.warn("[app-name-map] APP_NAME_MAP_URL is empty, skip refresh");
9653
+ return;
9654
+ }
9655
+ if (!apiKey) {
9656
+ logger.info("[app-name-map] api key missing, skip refresh");
9657
+ return;
9658
+ }
9659
+ const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
9660
+ try {
9661
+ const res = await fetch(url, {
9662
+ method: "POST",
9663
+ headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
9664
+ body: JSON.stringify({
9665
+ platform: ""
9666
+ })
9667
+ });
9668
+ if (!res.ok) {
9669
+ logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
9670
+ return;
9671
+ }
9672
+ const body = await res.json();
9673
+ if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
9674
+ logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
9675
+ return;
9676
+ }
9677
+ map.clear();
9678
+ for (const item of body.data) {
9679
+ if (item.packageName && item.appName) map.set(item.packageName, item.appName);
9680
+ }
9681
+ if (map.size === 0) {
9682
+ logger.warn("[app-name-map] refresh succeeded but got 0 entries");
9683
+ return;
9684
+ }
9685
+ const dir = (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
9686
+ (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
9687
+ const cachePath = getCachePath(stateDir);
9688
+ (0, import_node_fs22.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
9689
+ logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
9690
+ } catch (e) {
9691
+ const message = e instanceof Error ? e.message : String(e);
9692
+ logger.warn(`[app-name-map] refresh error: ${message}`);
9693
+ }
9694
+ }
9695
+ async function ensureOneFetch() {
9696
+ if (inFlightFetch) return inFlightFetch;
9697
+ inFlightFetch = fetchFromServer().finally(() => {
9698
+ inFlightFetch = null;
9699
+ });
9700
+ return inFlightFetch;
9701
+ }
9702
+ return {
9703
+ async resolveDisplayName(packageName) {
9704
+ if (map.has(packageName)) return map.get(packageName);
9705
+ return packageName;
9706
+ },
9707
+ async start() {
9708
+ loadFromDisk();
9709
+ await fetchFromServer();
9710
+ if (!url) return;
9711
+ if (refreshHours > 0) {
9712
+ const ms = refreshHours * 60 * 60 * 1e3;
9713
+ refreshTimer = setInterval(() => fetchFromServer().catch(() => {
9714
+ }), ms);
9715
+ }
9716
+ stopWatching = watchCredentials(() => fetchFromServer().catch(() => {
9717
+ }));
10025
9718
  },
10026
- repeat_times: {
10027
- type: "number",
10028
- minimum: 0,
10029
- description: "\u6574\u6761\u7EC4\u5408\u91CD\u590D\u6B21\u6570\uFF1A0 = \u65E0\u9650\u5FAA\u73AF\uFF0C1 = \u64AD\u653E\u4E00\u8F6E\u3002\u5F53\u524D ANCS \u8DEF\u5F84\u4E0D\u652F\u6301 N>=2"
9719
+ stop() {
9720
+ stopWatching?.();
9721
+ stopWatching = null;
9722
+ if (refreshTimer) {
9723
+ clearInterval(refreshTimer);
9724
+ refreshTimer = null;
9725
+ }
9726
+ map.clear();
10030
9727
  }
9728
+ };
9729
+ }
9730
+
9731
+ // src/notification/storage.ts
9732
+ var import_node_fs23 = require("fs");
9733
+ var import_node_crypto2 = require("crypto");
9734
+ var import_node_path19 = require("path");
9735
+ var NOTIFICATION_DIR_NAME = "notifications";
9736
+ var ID_INDEX_DIR_NAME = ".ids";
9737
+ var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
9738
+ function getStateFallbackNotificationDir(stateDir) {
9739
+ return (0, import_node_path19.join)(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
9740
+ }
9741
+ function ensureWritableDirectory(dir) {
9742
+ try {
9743
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9744
+ (0, import_node_fs23.accessSync)(dir, import_node_fs23.constants.R_OK | import_node_fs23.constants.W_OK);
9745
+ return true;
9746
+ } catch {
9747
+ return false;
10031
9748
  }
10032
- };
10033
- function registerLightControlTool(api, logger) {
10034
- api.registerTool({
10035
- name: "light_control",
10036
- label: "Light Control",
10037
- description: '\u63A7\u5236\u786C\u4EF6\u706F\u6548\uFF0C\u652F\u6301 1\u201312 \u6BB5\u987A\u5E8F\u706F\u6548\uFF0C\u6BCF\u6BB5\u72EC\u7ACB\u53C2\u6570\uFF086 \u79CD\u6A21\u5F0F\uFF1A\u6CE2\u6D6A/\u547C\u5438/\u9891\u95EA/\u5E38\u4EAE/\u6D41\u5149/\u9010\u7EC4\u50CF\u7D20\u5E27\uFF09\u3002Tool \u5185\u90E8\u81EA\u52A8\u91CF\u5316\u4E3A\u5D4C\u5165\u5F0F\u534F\u8BAE\u5B9A\u4E49\u7684\u79BB\u6563\u6863\u4F4D\uFF0C\u5E76\u6309\u6A21\u5F0F\u7CBE\u7B80\u4E3A\u53D8\u957F ANCS \u63A7\u5236\u5E8F\u5217\uFF0C\u901A\u8FC7 HTTP \u63A5\u53E3\u4E0B\u53D1\u706F\u6548\u6307\u4EE4\u3002\u5F53\u7528\u6237\u8BF4"\u5F00\u706F"\u3001"\u6362\u4E2A\u706F\u6548"\u3001"\u628A\u706F\u8C03\u6210\u7EA2\u8272"\u3001"\u8425\u9020\u6C1B\u56F4"\u7B49\u4E0E\u706F\u5149\u63A7\u5236\u76F8\u5173\u7684\u8868\u8FBE\u65F6\u8C03\u7528\u3002',
10038
- parameters: lightControlParameters,
10039
- async execute(_toolCallId, params) {
10040
- let apiKey;
10041
- try {
10042
- apiKey = requireApiKey();
10043
- } catch (e) {
10044
- return {
10045
- content: [{ type: "text", text: e.message }],
10046
- details: { ok: false, error: { code: "AUTH_REQUIRED", message: e.message } }
10047
- };
9749
+ }
9750
+ function resolveNotificationStorageDir(ctx, logger) {
9751
+ const stateNotifDir = getStateFallbackNotificationDir(ctx.stateDir);
9752
+ if (ensureWritableDirectory(stateNotifDir)) {
9753
+ logger.info(`\u901A\u77E5\u5C06\u5199\u5165 stateDir \u8DEF\u5F84: ${stateNotifDir}`);
9754
+ return stateNotifDir;
9755
+ }
9756
+ if (ctx.workspaceDir) {
9757
+ const workspaceDir = (0, import_node_path19.join)(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
9758
+ if (ensureWritableDirectory(workspaceDir)) {
9759
+ logger.warn(
9760
+ `stateDir \u4E0D\u53EF\u7528\uFF0C\u901A\u77E5\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${workspaceDir}`
9761
+ );
9762
+ return workspaceDir;
9763
+ }
9764
+ }
9765
+ throw new Error(`\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528: ${stateNotifDir}`);
9766
+ }
9767
+ var NotificationStorage = class {
9768
+ constructor(dir, config, logger, resolveDisplayName) {
9769
+ this.config = config;
9770
+ this.logger = logger;
9771
+ this.dir = dir;
9772
+ this.idIndexDir = (0, import_node_path19.join)(dir, ID_INDEX_DIR_NAME);
9773
+ this.contentKeyIndexDir = (0, import_node_path19.join)(dir, CONTENT_KEY_INDEX_DIR_NAME);
9774
+ this.resolveDisplayName = resolveDisplayName;
9775
+ }
9776
+ dir;
9777
+ idIndexDir;
9778
+ contentKeyIndexDir;
9779
+ idCache = /* @__PURE__ */ new Map();
9780
+ contentKeyCache = /* @__PURE__ */ new Map();
9781
+ dateWriteChains = /* @__PURE__ */ new Map();
9782
+ resolveDisplayName;
9783
+ async init() {
9784
+ (0, import_node_fs23.mkdirSync)(this.dir, { recursive: true });
9785
+ (0, import_node_fs23.mkdirSync)(this.idIndexDir, { recursive: true });
9786
+ (0, import_node_fs23.rmSync)(this.contentKeyIndexDir, { recursive: true, force: true });
9787
+ (0, import_node_fs23.mkdirSync)(this.contentKeyIndexDir, { recursive: true });
9788
+ }
9789
+ async ingest(items) {
9790
+ const result = {
9791
+ received: items.length,
9792
+ ingested: 0,
9793
+ dedupedById: 0,
9794
+ dedupedByContent: 0,
9795
+ invalid: 0,
9796
+ inserted: []
9797
+ };
9798
+ for (const n of items) {
9799
+ const outcome = await this.writeNotification(n);
9800
+ switch (outcome.kind) {
9801
+ case "ingested":
9802
+ result.ingested += 1;
9803
+ result.inserted.push(outcome.entry);
9804
+ break;
9805
+ case "dedupedById":
9806
+ result.dedupedById += 1;
9807
+ break;
9808
+ case "dedupedByContent":
9809
+ result.dedupedByContent += 1;
9810
+ break;
9811
+ case "invalid":
9812
+ result.invalid += 1;
9813
+ break;
10048
9814
  }
10049
- const { segments, reason, repeat, repeat_times } = params;
10050
- const validation = validateSegments(segments);
10051
- if (!validation.valid) {
10052
- return {
10053
- content: [{ type: "text", text: JSON.stringify(validation.errors) }],
10054
- details: {
10055
- ok: false,
10056
- error: { code: "VALIDATION_FAILED", details: validation.errors }
10057
- }
10058
- };
9815
+ }
9816
+ this.prune();
9817
+ return result;
9818
+ }
9819
+ async writeNotification(n) {
9820
+ const ts = new Date(n.timestamp);
9821
+ if (Number.isNaN(ts.getTime())) {
9822
+ this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
9823
+ return { kind: "invalid" };
9824
+ }
9825
+ const dateKey = this.formatDate(ts);
9826
+ const filePath = (0, import_node_path19.join)(this.dir, `${dateKey}.json`);
9827
+ const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
9828
+ const entry = this.buildStoredNotification(n);
9829
+ return this.withDateWriteLock(dateKey, async () => {
9830
+ if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
9831
+ return { kind: "dedupedById" };
10059
9832
  }
10060
- let repeatTimes;
10061
- try {
10062
- repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
10063
- assertAncsRepeatTimes(repeatTimes);
10064
- } catch (error) {
10065
- return {
10066
- content: [{ type: "text", text: error?.message ?? String(error) }],
10067
- details: {
10068
- ok: false,
10069
- error: { code: "VALIDATION_FAILED", message: error?.message ?? String(error) }
10070
- }
10071
- };
9833
+ if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
9834
+ return { kind: "dedupedByContent" };
9835
+ }
9836
+ const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
9837
+ const storedEntry = {
9838
+ ...entry,
9839
+ appDisplayName
9840
+ };
9841
+ const arr = this.readStoredNotifications(filePath);
9842
+ arr.push(storedEntry);
9843
+ (0, import_node_fs23.writeFileSync)(filePath, JSON.stringify(arr, null, 2), "utf-8");
9844
+ if (normalizedId) {
9845
+ this.recordNotificationId(dateKey, normalizedId);
9846
+ }
9847
+ this.recordNotificationContentKey(dateKey, filePath, storedEntry);
9848
+ return { kind: "ingested", entry: storedEntry };
9849
+ });
9850
+ }
9851
+ buildStoredNotification(n) {
9852
+ return {
9853
+ appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
9854
+ title: typeof n.title === "string" ? n.title : "",
9855
+ content: this.buildContent(n),
9856
+ timestamp: n.timestamp
9857
+ };
9858
+ }
9859
+ buildContent(n) {
9860
+ const body = n.body?.trim();
9861
+ if (body) {
9862
+ return body;
9863
+ }
9864
+ const fallback = [];
9865
+ if (n.category) {
9866
+ fallback.push(`category:${n.category}`);
9867
+ }
9868
+ if (n.metadata && Object.keys(n.metadata).length > 0) {
9869
+ fallback.push(`metadata:${JSON.stringify(n.metadata)}`);
9870
+ }
9871
+ return fallback.join(" ; ") || "-";
9872
+ }
9873
+ formatDate(d) {
9874
+ const year = d.getFullYear();
9875
+ const month = String(d.getMonth() + 1).padStart(2, "0");
9876
+ const day = String(d.getDate()).padStart(2, "0");
9877
+ return `${year}-${month}-${day}`;
9878
+ }
9879
+ getIdIndexPath(dateKey) {
9880
+ return (0, import_node_path19.join)(this.idIndexDir, `${dateKey}.ids`);
9881
+ }
9882
+ getIdSet(dateKey) {
9883
+ const cached = this.idCache.get(dateKey);
9884
+ if (cached) {
9885
+ return cached;
9886
+ }
9887
+ const idPath = this.getIdIndexPath(dateKey);
9888
+ const ids = /* @__PURE__ */ new Set();
9889
+ if ((0, import_node_fs23.existsSync)(idPath)) {
9890
+ const lines = (0, import_node_fs23.readFileSync)(idPath, "utf-8").split(/\r?\n/);
9891
+ for (const line of lines) {
9892
+ const id = line.trim();
9893
+ if (id) {
9894
+ ids.add(id);
9895
+ }
10072
9896
  }
10073
- logger.info(`Light control reason: ${reason}`);
10074
- const result = await sendLightEffect(
10075
- apiKey,
10076
- validation.segments,
10077
- logger,
10078
- { repeat_times: repeatTimes },
10079
- reason
10080
- );
10081
- if (!result.ok) {
10082
- logger.warn(
10083
- `Light control HTTP request failed: ${result.status} ${result.error}`
10084
- );
10085
- } else {
10086
- logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
9897
+ }
9898
+ this.idCache.set(dateKey, ids);
9899
+ return ids;
9900
+ }
9901
+ hasNotificationId(dateKey, id) {
9902
+ return this.getIdSet(dateKey).has(id);
9903
+ }
9904
+ getContentKeyIndexPath(dateKey) {
9905
+ return (0, import_node_path19.join)(this.contentKeyIndexDir, `${dateKey}.keys`);
9906
+ }
9907
+ getContentKeySet(dateKey, filePath) {
9908
+ const cached = this.contentKeyCache.get(dateKey);
9909
+ if (cached) {
9910
+ return cached;
9911
+ }
9912
+ const keyPath = this.getContentKeyIndexPath(dateKey);
9913
+ const keys = /* @__PURE__ */ new Set();
9914
+ if ((0, import_node_fs23.existsSync)(filePath)) {
9915
+ for (const item of this.readStoredNotifications(filePath)) {
9916
+ keys.add(this.buildNotificationContentKey(item));
10087
9917
  }
10088
- return {
10089
- content: [{ type: "text", text: JSON.stringify(result) }],
10090
- details: result
10091
- };
10092
9918
  }
10093
- });
10094
- }
10095
-
10096
- // src/plugin/lifecycle.ts
10097
- var import_node_fs31 = require("fs");
10098
- init_host();
10099
-
10100
- // src/notification/app-name-map.ts
10101
- var import_node_fs23 = require("fs");
10102
- var import_node_path19 = require("path");
10103
- init_credentials();
10104
- init_env();
10105
- var PLUGIN_STATE_DIR = "phone-notifications";
10106
- var CACHE_FILE = "app-name-map.json";
10107
- var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
10108
- var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
10109
- var APP_NAME_MAP_REFRESH_HOURS = 12;
10110
- function isRecordOfStrings(v) {
10111
- if (v === null || typeof v !== "object") return false;
10112
- for (const val of Object.values(v)) if (typeof val !== "string") return false;
10113
- return true;
10114
- }
10115
- function isAppNameMapApiResponse(v) {
10116
- if (v === null || typeof v !== "object") return false;
10117
- const o = v;
10118
- return Array.isArray(o.data) && o.data.every(
10119
- (item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string"
10120
- );
10121
- }
10122
- function getCachePath(stateDir) {
10123
- return (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
10124
- }
10125
- function createAppNameMapProvider(opts) {
10126
- const { stateDir, logger } = opts;
10127
- const url = APP_NAME_MAP_URL;
10128
- const refreshHours = APP_NAME_MAP_REFRESH_HOURS;
10129
- const map = /* @__PURE__ */ new Map();
10130
- let refreshTimer = null;
10131
- let stopWatching = null;
10132
- let inFlightFetch = null;
10133
- function loadFromDisk() {
10134
- const path2 = getCachePath(stateDir);
10135
- if (!(0, import_node_fs23.existsSync)(path2)) return;
10136
- try {
10137
- const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf-8"));
10138
- if (!isRecordOfStrings(raw)) return;
10139
- map.clear();
10140
- for (const [k, v] of Object.entries(raw)) map.set(k, v);
10141
- logger.info(`[app-name-map] loaded ${map.size} entries from cache: ${path2}`);
10142
- } catch {
9919
+ if (keys.size > 0) {
9920
+ (0, import_node_fs23.writeFileSync)(keyPath, `${Array.from(keys).join("\n")}
9921
+ `, "utf-8");
9922
+ } else if ((0, import_node_fs23.existsSync)(keyPath)) {
9923
+ (0, import_node_fs23.rmSync)(keyPath, { force: true });
10143
9924
  }
9925
+ this.contentKeyCache.set(dateKey, keys);
9926
+ return keys;
10144
9927
  }
10145
- async function fetchFromServer() {
10146
- const apiKey = loadApiKey();
10147
- if (!url) {
10148
- logger.warn("[app-name-map] APP_NAME_MAP_URL is empty, skip refresh");
9928
+ hasNotificationContentKey(dateKey, filePath, entry) {
9929
+ return this.getContentKeySet(dateKey, filePath).has(
9930
+ this.buildNotificationContentKey(entry)
9931
+ );
9932
+ }
9933
+ recordNotificationId(dateKey, id) {
9934
+ const ids = this.getIdSet(dateKey);
9935
+ if (ids.has(id)) {
10149
9936
  return;
10150
9937
  }
10151
- if (!apiKey) {
10152
- logger.info("[app-name-map] api key missing, skip refresh");
9938
+ (0, import_node_fs23.appendFileSync)(this.getIdIndexPath(dateKey), `${id}
9939
+ `, "utf-8");
9940
+ ids.add(id);
9941
+ }
9942
+ recordNotificationContentKey(dateKey, filePath, entry) {
9943
+ const keys = this.getContentKeySet(dateKey, filePath);
9944
+ const key = this.buildNotificationContentKey(entry);
9945
+ if (keys.has(key)) {
10153
9946
  return;
10154
9947
  }
10155
- const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
9948
+ (0, import_node_fs23.appendFileSync)(this.getContentKeyIndexPath(dateKey), `${key}
9949
+ `, "utf-8");
9950
+ keys.add(key);
9951
+ }
9952
+ buildNotificationContentKey(entry) {
9953
+ return (0, import_node_crypto2.createHash)("sha256").update(entry.appName).update("").update(entry.title).update("").update(entry.content).update("").update(entry.timestamp).digest("hex");
9954
+ }
9955
+ readStoredNotifications(filePath) {
9956
+ if (!(0, import_node_fs23.existsSync)(filePath)) {
9957
+ return [];
9958
+ }
10156
9959
  try {
10157
- const res = await fetch(url, {
10158
- method: "POST",
10159
- headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
10160
- body: JSON.stringify({
10161
- platform: ""
10162
- })
10163
- });
10164
- if (!res.ok) {
10165
- logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
10166
- return;
10167
- }
10168
- const body = await res.json();
10169
- if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
10170
- logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
10171
- return;
10172
- }
10173
- map.clear();
10174
- for (const item of body.data) {
10175
- if (item.packageName && item.appName) map.set(item.packageName, item.appName);
10176
- }
10177
- if (map.size === 0) {
10178
- logger.warn("[app-name-map] refresh succeeded but got 0 entries");
10179
- return;
10180
- }
10181
- const dir = (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
10182
- (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
10183
- const cachePath = getCachePath(stateDir);
10184
- (0, import_node_fs23.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
10185
- logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
10186
- } catch (e) {
10187
- const message = e instanceof Error ? e.message : String(e);
10188
- logger.warn(`[app-name-map] refresh error: ${message}`);
9960
+ const parsed = JSON.parse((0, import_node_fs23.readFileSync)(filePath, "utf-8"));
9961
+ return Array.isArray(parsed) ? parsed : [];
9962
+ } catch {
9963
+ return [];
10189
9964
  }
10190
9965
  }
10191
- async function ensureOneFetch() {
10192
- if (inFlightFetch) return inFlightFetch;
10193
- inFlightFetch = fetchFromServer().finally(() => {
10194
- inFlightFetch = null;
9966
+ async withDateWriteLock(dateKey, task) {
9967
+ const previous = this.dateWriteChains.get(dateKey) ?? Promise.resolve();
9968
+ let release;
9969
+ const current = new Promise((resolve) => {
9970
+ release = resolve;
10195
9971
  });
10196
- return inFlightFetch;
9972
+ const chain = previous.then(() => current);
9973
+ this.dateWriteChains.set(dateKey, chain);
9974
+ await previous;
9975
+ try {
9976
+ return await task();
9977
+ } finally {
9978
+ release();
9979
+ if (this.dateWriteChains.get(dateKey) === chain) {
9980
+ this.dateWriteChains.delete(dateKey);
9981
+ }
9982
+ }
10197
9983
  }
10198
- return {
10199
- async resolveDisplayName(packageName) {
10200
- if (map.has(packageName)) return map.get(packageName);
10201
- return packageName;
10202
- },
10203
- async start() {
10204
- loadFromDisk();
10205
- await fetchFromServer();
10206
- if (!url) return;
10207
- if (refreshHours > 0) {
10208
- const ms = refreshHours * 60 * 60 * 1e3;
10209
- refreshTimer = setInterval(() => fetchFromServer().catch(() => {
10210
- }), ms);
9984
+ prune() {
9985
+ const retentionDays = this.config.retentionDays;
9986
+ if (retentionDays === void 0) {
9987
+ return;
9988
+ }
9989
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
9990
+ const cutoffDate = this.formatDate(new Date(cutoffMs));
9991
+ this.pruneDataFiles(cutoffDate);
9992
+ this.pruneIdIndex(cutoffDate);
9993
+ this.pruneContentKeyIndex(cutoffDate);
9994
+ }
9995
+ /** Remove expired .json, legacy .md files, and legacy date directories */
9996
+ pruneDataFiles(cutoffDate) {
9997
+ const dateFilePattern = /^(\d{4}-\d{2}-\d{2})\.(json|md)$/;
9998
+ const dateDirPattern = /^\d{4}-\d{2}-\d{2}$/;
9999
+ try {
10000
+ for (const entry of (0, import_node_fs23.readdirSync)(this.dir, { withFileTypes: true })) {
10001
+ if (entry.isFile()) {
10002
+ const match = dateFilePattern.exec(entry.name);
10003
+ if (match && match[1] < cutoffDate) {
10004
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.name), { force: true });
10005
+ }
10006
+ } else if (entry.isDirectory() && dateDirPattern.test(entry.name) && entry.name < cutoffDate) {
10007
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.name), { recursive: true, force: true });
10008
+ }
10211
10009
  }
10212
- stopWatching = watchCredentials(() => fetchFromServer().catch(() => {
10213
- }));
10214
- },
10215
- stop() {
10216
- stopWatching?.();
10217
- stopWatching = null;
10218
- if (refreshTimer) {
10219
- clearInterval(refreshTimer);
10220
- refreshTimer = null;
10010
+ } catch {
10011
+ }
10012
+ }
10013
+ /** Remove expired .ids index files */
10014
+ pruneIdIndex(cutoffDate) {
10015
+ try {
10016
+ for (const entry of (0, import_node_fs23.readdirSync)(this.idIndexDir, { withFileTypes: true })) {
10017
+ if (!entry.isFile()) continue;
10018
+ const match = /^(\d{4}-\d{2}-\d{2})\.ids$/.exec(entry.name);
10019
+ if (match && match[1] < cutoffDate) {
10020
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.idIndexDir, entry.name), { force: true });
10021
+ this.idCache.delete(match[1]);
10022
+ }
10221
10023
  }
10222
- map.clear();
10024
+ } catch {
10223
10025
  }
10224
- };
10225
- }
10026
+ }
10027
+ pruneContentKeyIndex(cutoffDate) {
10028
+ try {
10029
+ for (const entry of (0, import_node_fs23.readdirSync)(this.contentKeyIndexDir, { withFileTypes: true })) {
10030
+ if (!entry.isFile()) continue;
10031
+ const match = /^(\d{4}-\d{2}-\d{2})\.keys$/.exec(entry.name);
10032
+ if (match && match[1] < cutoffDate) {
10033
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.contentKeyIndexDir, entry.name), { force: true });
10034
+ this.contentKeyCache.delete(match[1]);
10035
+ }
10036
+ }
10037
+ } catch {
10038
+ }
10039
+ }
10040
+ async close() {
10041
+ this.idCache.clear();
10042
+ this.contentKeyCache.clear();
10043
+ this.dateWriteChains.clear();
10044
+ }
10045
+ };
10226
10046
 
10227
10047
  // src/recording/storage.ts
10228
10048
  var import_node_fs24 = require("fs");
@@ -10546,7 +10366,7 @@ var RecordingStorage = class {
10546
10366
  if (entry.transcriptDataFile) {
10547
10367
  const transcriptDoc = this.readRelativeTranscriptDocument(entry.transcriptDataFile);
10548
10368
  const transcriptFromJson = extractTranscriptTextFromDocument(transcriptDoc);
10549
- if (transcriptFromJson) {
10369
+ if (transcriptFromJson !== void 0) {
10550
10370
  return transcriptFromJson;
10551
10371
  }
10552
10372
  }
@@ -12851,16 +12671,15 @@ function registerNotificationInterfaces(deps) {
12851
12671
  filterNotifications,
12852
12672
  registerGatewayMethod,
12853
12673
  tunnelService,
12854
- onAfterIngest,
12855
- getCronForHttpIngest
12674
+ onAfterIngest
12856
12675
  } = deps;
12857
- function triggerAfterIngest(inserted, cron) {
12676
+ function triggerAfterIngest(inserted) {
12858
12677
  if (inserted.length === 0 || !onAfterIngest) return;
12859
- void Promise.resolve().then(() => onAfterIngest(inserted, cron)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12678
+ void Promise.resolve().then(() => onAfterIngest(inserted)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12860
12679
  }
12861
12680
  registerGatewayMethod(
12862
12681
  "notifications.push",
12863
- async ({ params, respond, context }) => {
12682
+ async ({ params, respond }) => {
12864
12683
  const storage = getStorage();
12865
12684
  if (!storage) {
12866
12685
  respond(false, null, {
@@ -12880,7 +12699,7 @@ function registerNotificationInterfaces(deps) {
12880
12699
  const filtered = filterNotifications(items);
12881
12700
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12882
12701
  respond(true, toIngestResponse(result));
12883
- triggerAfterIngest(result.inserted, context?.cron);
12702
+ triggerAfterIngest(result.inserted);
12884
12703
  }
12885
12704
  );
12886
12705
  api.registerHttpRoute({
@@ -12924,7 +12743,7 @@ function registerNotificationInterfaces(deps) {
12924
12743
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12925
12744
  res.writeHead(200, { "Content-Type": "application/json" });
12926
12745
  res.end(JSON.stringify({ ok: true, ...toIngestResponse(result) }));
12927
- triggerAfterIngest(result.inserted, getCronForHttpIngest?.());
12746
+ triggerAfterIngest(result.inserted);
12928
12747
  }
12929
12748
  });
12930
12749
  logger.info("Gateway \u901A\u77E5\u65B9\u6CD5\u5DF2\u6CE8\u518C: notifications.push");
@@ -13395,7 +13214,6 @@ var index_default = {
13395
13214
  let storage = null;
13396
13215
  let recordingStorage = null;
13397
13216
  let broadcastFn = null;
13398
- let cronService = null;
13399
13217
  let autoUpdateLifecycle = null;
13400
13218
  let tunnelService = null;
13401
13219
  const openclawDir = api.runtime.state.resolveStateDir();
@@ -13448,9 +13266,6 @@ var index_default = {
13448
13266
  function registerGatewayMethodWithBroadcastCapture(method, handler) {
13449
13267
  api.registerGatewayMethod(method, async (opts) => {
13450
13268
  cacheBroadcast(opts.context?.broadcast);
13451
- if (opts.context?.cron) {
13452
- cronService = opts.context.cron;
13453
- }
13454
13269
  await deactivateRelayForDirectGatewayRequest(method, opts);
13455
13270
  return handler(opts);
13456
13271
  });
@@ -13474,13 +13289,6 @@ var index_default = {
13474
13289
  registry: lightRuleRegistry,
13475
13290
  invoker: lightRuleInvoker
13476
13291
  });
13477
- const lightRulesEvaluatorJob = new LightRulesEvaluatorJob({
13478
- logger,
13479
- registry: lightRuleRegistry,
13480
- inlineEvaluator: inlineLightRuleEvaluator,
13481
- subagentRunner: api.runtime.subagent,
13482
- getNotificationsDir: () => openclawDir ? getStateFallbackNotificationDir(openclawDir) : void 0
13483
- });
13484
13292
  registerStorageLifecycle({
13485
13293
  api,
13486
13294
  config,
@@ -13510,10 +13318,9 @@ var index_default = {
13510
13318
  filterNotifications,
13511
13319
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
13512
13320
  tunnelService,
13513
- onAfterIngest: (inserted, cron) => {
13514
- void lightRulesEvaluatorJob.triggerIfNeeded(cron, inserted);
13515
- },
13516
- getCronForHttpIngest: () => cronService
13321
+ onAfterIngest: (inserted) => {
13322
+ void inlineLightRuleEvaluator.evaluate(inserted);
13323
+ }
13517
13324
  });
13518
13325
  registerLightControlTool(api, logger);
13519
13326
  registerRecordingInterfaces({