@yoooclaw/phone-notifications 1.10.7 → 1.11.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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_path6.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_path6.join)(home, ".qclaw", "qclaw.json"),
63
- (0, import_node_path6.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_fs7.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_fs7.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_path6.join)(homeDir(), ".openclaw");
93
+ const baseDir = (0, import_node_path4.join)(homeDir(), ".openclaw");
94
94
  return [
95
- (0, import_node_path6.join)(baseDir, "openclaw.json"),
96
- (0, import_node_path6.join)(baseDir, "credentials.json"),
97
- (0, import_node_path6.join)(baseDir, "extensions")
98
- ].some((candidate) => (0, import_node_fs7.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_path6.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_path6.dirname)(meta.configPath);
113
+ return (0, import_node_path4.dirname)(meta.configPath);
114
114
  }
115
- return (0, import_node_path6.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_path6.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_path6.join)(resolveStateDir(), filename);
129
+ return (0, import_node_path4.join)(resolveStateDir(), filename);
130
130
  }
131
- var import_node_fs7, import_node_path6;
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_fs7 = require("fs");
136
- import_node_path6 = 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_fs8.existsSync)(path2)) return {};
146
+ if (!(0, import_node_fs6.existsSync)(path2)) return {};
147
147
  try {
148
- return JSON.parse((0, import_node_fs8.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_fs8.mkdirSync)((0, import_node_path7.dirname)(path2), { recursive: true, mode: 448 });
156
- (0, import_node_fs8.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_path7.dirname)(path2);
177
- const filename = (0, import_node_path7.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_fs8.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_fs8, import_node_path7;
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_fs8 = require("fs");
203
- import_node_path7 = 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_fs9.mkdirSync)((0, import_node_path8.dirname)(path2), { recursive: true });
212
- const existing = (0, import_node_fs9.existsSync)(path2) ? (0, import_node_fs9.readFileSync)(path2, "utf-8") : "";
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_fs9.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_fs9.existsSync)(path2)) return {};
227
+ if (!(0, import_node_fs7.existsSync)(path2)) return {};
228
228
  return Object.fromEntries(
229
- (0, import_node_fs9.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_fs9, import_node_path8, 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_fs9 = require("fs");
264
- import_node_path8 = 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 = normalizePossiblyEmptyText(params.text) ?? joinSegmentsText(segments) ?? "";
293
+ const text = normalizeOptionalText(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: normalizePossiblyEmptyText(params.summary),
307
+ summary: normalizeOptionalText(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 normalizePossiblyEmptyText(doc.normalized.text) ?? joinSegmentsText(doc.normalized.segments);
338
+ return normalizeOptionalText(doc.normalized.text) ?? joinSegmentsText(doc.normalized.segments);
339
339
  }
340
340
  function extractTranscriptSummaryFromDocument(doc) {
341
341
  if (!doc) return void 0;
342
- return normalizePossiblyEmptyText(doc.normalized.summary);
342
+ return normalizeOptionalText(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 = normalizePossiblyEmptyText(normalized.text) ?? joinSegmentsText(segments);
396
- if (text === void 0) {
395
+ const text = normalizeOptionalText(normalized.text) ?? joinSegmentsText(segments);
396
+ if (!text) {
397
397
  return void 0;
398
398
  }
399
399
  return {
400
400
  title: normalizeOptionalText(normalized.title),
401
401
  category: normalizeOptionalText(normalized.category),
402
- summary: normalizePossiblyEmptyText(normalized.summary),
402
+ summary: normalizeOptionalText(normalized.summary),
403
403
  text,
404
404
  segments
405
405
  };
@@ -465,9 +465,6 @@ 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
- }
471
468
  function normalizeOptionalNumber(value) {
472
469
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
473
470
  }
@@ -1345,7 +1342,7 @@ async function runTranscriptionWorkflow(params) {
1345
1342
  return { ok: false, error: result.error };
1346
1343
  }
1347
1344
  const title = normalizeOptionalText2(result.summary) ? normalizeOptionalText2(result.summary) : extractSummary(result.text ?? "");
1348
- const summary = result.summaryText ?? "";
1345
+ const summary = normalizeOptionalText2(result.summaryText);
1349
1346
  result.summary = title;
1350
1347
  const transcriptData = buildTranscriptDocument({
1351
1348
  recordingId,
@@ -1606,11 +1603,16 @@ function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1606
1603
  );
1607
1604
  const listResult = extractLongRecordingTextFromList(sourceTextList);
1608
1605
  const sourceText = normalizeOptionalText2(data?.recordResult?.sourceText);
1609
- const summaryText = normalizeOptionalText2(data?.recordResult?.summaryResult) ?? "";
1606
+ const summaryText = normalizeOptionalText2(data?.recordResult?.summaryResult);
1610
1607
  const title = normalizeOptionalText2(data?.recordResult?.title);
1611
1608
  const category = normalizeOptionalText2(data?.recordResult?.category);
1612
1609
  const text = listResult.text ?? sourceText ?? summaryText;
1613
- const status = normalizeLongRecordingStatus(data?.status) ?? "SUCCEEDED";
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
+ }
1614
1616
  if (!listResult.text && !sourceText && summaryText) {
1615
1617
  logger.warn(
1616
1618
  `[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}`
@@ -1630,7 +1632,7 @@ function buildLongRecordingSuccessResult(taskId, requestId, data, logger) {
1630
1632
  provider: "model-proxy",
1631
1633
  taskId,
1632
1634
  requestId,
1633
- status
1635
+ status: "SUCCEEDED"
1634
1636
  },
1635
1637
  rawResponse: data
1636
1638
  };
@@ -5422,7 +5424,7 @@ function readBuildInjectedVersion() {
5422
5424
  if (false) {
5423
5425
  return void 0;
5424
5426
  }
5425
- const version = "1.10.7".trim();
5427
+ const version = "1.11.0-beta.1".trim();
5426
5428
  return version || void 0;
5427
5429
  }
5428
5430
  function readPluginVersionFromPackageJson() {
@@ -5547,764 +5549,448 @@ function formatLocalTimestamp(d) {
5547
5549
  return `${y}-${m}-${day}T${hh}:${mm}:${ss}.${ms}${sign}${offsetHours}:${offsetMins}`;
5548
5550
  }
5549
5551
 
5550
- // src/notification/storage.ts
5552
+ // src/light/repeat.ts
5553
+ function normalizeRepeatTimes(input) {
5554
+ if (typeof input === "boolean") {
5555
+ return input ? 0 : 1;
5556
+ }
5557
+ if (typeof input === "number") {
5558
+ return validateRepeatTimes(input);
5559
+ }
5560
+ if (!input) {
5561
+ return 1;
5562
+ }
5563
+ if (input.repeat_times !== void 0) {
5564
+ return validateRepeatTimes(input.repeat_times);
5565
+ }
5566
+ if (input.repeat !== void 0) {
5567
+ return input.repeat ? 0 : 1;
5568
+ }
5569
+ return 1;
5570
+ }
5571
+ function assertAncsRepeatTimes(repeatTimes) {
5572
+ if (repeatTimes !== 0 && repeatTimes !== 1) {
5573
+ throw new Error(
5574
+ "\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"
5575
+ );
5576
+ }
5577
+ }
5578
+ function validateRepeatTimes(value) {
5579
+ if (!Number.isInteger(value) || value < 0) {
5580
+ throw new Error("repeat_times \u5FC5\u987B\u662F >=0 \u7684\u6574\u6570");
5581
+ }
5582
+ return value;
5583
+ }
5584
+
5585
+ // src/cli/helpers.ts
5551
5586
  var import_node_fs3 = require("fs");
5552
- var import_node_crypto = require("crypto");
5587
+ var import_promises = require("fs/promises");
5553
5588
  var import_node_path2 = require("path");
5554
- var NOTIFICATION_DIR_NAME = "notifications";
5555
- var ID_INDEX_DIR_NAME = ".ids";
5556
- var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
5557
- function getStateFallbackNotificationDir(stateDir) {
5558
- return (0, import_node_path2.join)(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
5589
+ function resolveNotificationsDir(ctx) {
5590
+ if (ctx.stateDir) {
5591
+ const dir = (0, import_node_path2.join)(
5592
+ ctx.stateDir,
5593
+ "plugins",
5594
+ "phone-notifications",
5595
+ "notifications"
5596
+ );
5597
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5598
+ }
5599
+ if (ctx.workspaceDir) {
5600
+ const dir = (0, import_node_path2.join)(ctx.workspaceDir, "notifications");
5601
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5602
+ }
5603
+ return null;
5559
5604
  }
5560
- function ensureWritableDirectory(dir) {
5605
+ function listDateKeys(dir) {
5606
+ const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5607
+ const keys = [];
5608
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
5609
+ if (!entry.isFile()) continue;
5610
+ const m = pattern.exec(entry.name);
5611
+ if (m) keys.push(m[1]);
5612
+ }
5613
+ return keys.sort().reverse();
5614
+ }
5615
+ function readDateFile(dir, dateKey) {
5616
+ const filePath = (0, import_node_path2.join)(dir, `${dateKey}.json`);
5617
+ if (!(0, import_node_fs3.existsSync)(filePath)) return [];
5561
5618
  try {
5562
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
5563
- (0, import_node_fs3.accessSync)(dir, import_node_fs3.constants.R_OK | import_node_fs3.constants.W_OK);
5564
- return true;
5619
+ return JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf-8"));
5565
5620
  } catch {
5566
- return false;
5621
+ return [];
5567
5622
  }
5568
5623
  }
5569
- function resolveNotificationStorageDir(ctx, logger) {
5570
- const stateNotifDir = getStateFallbackNotificationDir(ctx.stateDir);
5571
- if (ensureWritableDirectory(stateNotifDir)) {
5572
- logger.info(`\u901A\u77E5\u5C06\u5199\u5165 stateDir \u8DEF\u5F84: ${stateNotifDir}`);
5573
- return stateNotifDir;
5624
+ function today() {
5625
+ return formatDate2(/* @__PURE__ */ new Date());
5626
+ }
5627
+ function formatDate2(d) {
5628
+ const y = d.getFullYear();
5629
+ const m = String(d.getMonth() + 1).padStart(2, "0");
5630
+ const day = String(d.getDate()).padStart(2, "0");
5631
+ return `${y}-${m}-${day}`;
5632
+ }
5633
+ function daysAgo(n) {
5634
+ const d = /* @__PURE__ */ new Date();
5635
+ d.setDate(d.getDate() - n);
5636
+ return formatDate2(d);
5637
+ }
5638
+ function filterDateRange(keys, from, to) {
5639
+ return keys.filter((k) => k >= from && k <= to);
5640
+ }
5641
+ function parseIsoTime(value, optionName) {
5642
+ const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
5643
+ if (!isoPattern.test(value)) {
5644
+ exitError(
5645
+ "INVALID_TIME",
5646
+ `${optionName} \u5FC5\u987B\u662F ISO 8601 \u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5647
+ );
5574
5648
  }
5575
- if (ctx.workspaceDir) {
5576
- const workspaceDir = (0, import_node_path2.join)(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
5577
- if (ensureWritableDirectory(workspaceDir)) {
5578
- logger.warn(
5579
- `stateDir \u4E0D\u53EF\u7528\uFF0C\u901A\u77E5\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${workspaceDir}`
5580
- );
5581
- return workspaceDir;
5582
- }
5649
+ const ts = Date.parse(value);
5650
+ if (Number.isNaN(ts)) {
5651
+ exitError(
5652
+ "INVALID_TIME",
5653
+ `${optionName} \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5654
+ );
5583
5655
  }
5584
- throw new Error(`\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528: ${stateNotifDir}`);
5656
+ return ts;
5585
5657
  }
5586
- var NotificationStorage = class {
5587
- constructor(dir, config, logger, resolveDisplayName) {
5588
- this.config = config;
5589
- this.logger = logger;
5590
- this.dir = dir;
5591
- this.idIndexDir = (0, import_node_path2.join)(dir, ID_INDEX_DIR_NAME);
5592
- this.contentKeyIndexDir = (0, import_node_path2.join)(dir, CONTENT_KEY_INDEX_DIR_NAME);
5593
- this.resolveDisplayName = resolveDisplayName;
5658
+ function sortNotificationsByTimestampDesc(items) {
5659
+ return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
5660
+ }
5661
+ async function listDateKeysAsync(dir) {
5662
+ const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5663
+ const keys = [];
5664
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
5665
+ for (const entry of entries) {
5666
+ if (!entry.isFile()) continue;
5667
+ const m = pattern.exec(entry.name);
5668
+ if (m) keys.push(m[1]);
5594
5669
  }
5595
- dir;
5596
- idIndexDir;
5597
- contentKeyIndexDir;
5598
- idCache = /* @__PURE__ */ new Map();
5599
- contentKeyCache = /* @__PURE__ */ new Map();
5600
- dateWriteChains = /* @__PURE__ */ new Map();
5601
- resolveDisplayName;
5602
- async init() {
5603
- (0, import_node_fs3.mkdirSync)(this.dir, { recursive: true });
5604
- (0, import_node_fs3.mkdirSync)(this.idIndexDir, { recursive: true });
5605
- (0, import_node_fs3.rmSync)(this.contentKeyIndexDir, { recursive: true, force: true });
5606
- (0, import_node_fs3.mkdirSync)(this.contentKeyIndexDir, { recursive: true });
5670
+ return keys.sort().reverse();
5671
+ }
5672
+ async function readDateFileAsync(dir, dateKey) {
5673
+ const filePath = (0, import_node_path2.join)(dir, `${dateKey}.json`);
5674
+ if (!(0, import_node_fs3.existsSync)(filePath)) return [];
5675
+ try {
5676
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
5677
+ return JSON.parse(content);
5678
+ } catch {
5679
+ return [];
5607
5680
  }
5608
- async ingest(items) {
5609
- const result = {
5610
- received: items.length,
5611
- ingested: 0,
5612
- dedupedById: 0,
5613
- dedupedByContent: 0,
5614
- invalid: 0,
5615
- inserted: []
5616
- };
5617
- for (const n of items) {
5618
- const outcome = await this.writeNotification(n);
5619
- switch (outcome.kind) {
5620
- case "ingested":
5621
- result.ingested += 1;
5622
- result.inserted.push(outcome.entry);
5623
- break;
5624
- case "dedupedById":
5625
- result.dedupedById += 1;
5626
- break;
5627
- case "dedupedByContent":
5628
- result.dedupedByContent += 1;
5629
- break;
5630
- case "invalid":
5631
- result.invalid += 1;
5632
- break;
5633
- }
5634
- }
5635
- this.prune();
5636
- return result;
5681
+ }
5682
+ function progress(message) {
5683
+ process.stderr.write(message + "\n");
5684
+ }
5685
+ function output(data) {
5686
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
5687
+ }
5688
+ function exitError(code, message) {
5689
+ output({ ok: false, error: { code, message } });
5690
+ process.exit(1);
5691
+ }
5692
+ function resolveRecordingsDir(ctx) {
5693
+ if (ctx.stateDir) {
5694
+ const dir = (0, import_node_path2.join)(
5695
+ ctx.stateDir,
5696
+ "plugins",
5697
+ "phone-notifications",
5698
+ "recordings"
5699
+ );
5700
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5637
5701
  }
5638
- async writeNotification(n) {
5639
- const ts = new Date(n.timestamp);
5640
- if (Number.isNaN(ts.getTime())) {
5641
- this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
5642
- return { kind: "invalid" };
5643
- }
5644
- const dateKey = this.formatDate(ts);
5645
- const filePath = (0, import_node_path2.join)(this.dir, `${dateKey}.json`);
5646
- const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
5647
- const entry = this.buildStoredNotification(n);
5648
- return this.withDateWriteLock(dateKey, async () => {
5649
- if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
5650
- return { kind: "dedupedById" };
5651
- }
5652
- if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
5653
- return { kind: "dedupedByContent" };
5654
- }
5655
- const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
5656
- const storedEntry = {
5657
- ...entry,
5658
- appDisplayName
5659
- };
5660
- const arr = this.readStoredNotifications(filePath);
5661
- arr.push(storedEntry);
5662
- (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(arr, null, 2), "utf-8");
5663
- if (normalizedId) {
5664
- this.recordNotificationId(dateKey, normalizedId);
5665
- }
5666
- this.recordNotificationContentKey(dateKey, filePath, storedEntry);
5667
- return { kind: "ingested", entry: storedEntry };
5668
- });
5702
+ if (ctx.workspaceDir) {
5703
+ const dir = (0, import_node_path2.join)(ctx.workspaceDir, "recordings");
5704
+ if ((0, import_node_fs3.existsSync)(dir)) return dir;
5669
5705
  }
5670
- buildStoredNotification(n) {
5671
- return {
5672
- appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
5673
- title: typeof n.title === "string" ? n.title : "",
5674
- content: this.buildContent(n),
5675
- timestamp: n.timestamp
5676
- };
5706
+ return null;
5707
+ }
5708
+ function resolveAsrConfigPath(ctx) {
5709
+ let base;
5710
+ if (ctx.stateDir) {
5711
+ base = (0, import_node_path2.join)(ctx.stateDir, "plugins", "phone-notifications", "recordings");
5712
+ } else if (ctx.workspaceDir) {
5713
+ base = (0, import_node_path2.join)(ctx.workspaceDir, "recordings");
5714
+ } else {
5715
+ exitError(
5716
+ "STORAGE_UNAVAILABLE",
5717
+ "\u65E0\u6CD5\u786E\u5B9A\u5F55\u97F3\u5B58\u50A8\u76EE\u5F55\uFF1AstateDir \u548C workspaceDir \u5747\u672A\u8BBE\u7F6E"
5718
+ );
5677
5719
  }
5678
- buildContent(n) {
5679
- const body = n.body?.trim();
5680
- if (body) {
5681
- return body;
5682
- }
5683
- const fallback = [];
5684
- if (n.category) {
5685
- fallback.push(`category:${n.category}`);
5686
- }
5687
- if (n.metadata && Object.keys(n.metadata).length > 0) {
5688
- fallback.push(`metadata:${JSON.stringify(n.metadata)}`);
5689
- }
5690
- return fallback.join(" ; ") || "-";
5720
+ (0, import_node_fs3.mkdirSync)(base, { recursive: true });
5721
+ return (0, import_node_path2.join)(base, "asr-config.json");
5722
+ }
5723
+ function readRecordingIndex(dir) {
5724
+ const indexPath = (0, import_node_path2.join)(dir, "index.json");
5725
+ if (!(0, import_node_fs3.existsSync)(indexPath)) return [];
5726
+ try {
5727
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(indexPath, "utf-8"));
5728
+ return Array.isArray(raw?.recordings) ? raw.recordings : [];
5729
+ } catch {
5730
+ return [];
5691
5731
  }
5692
- formatDate(d) {
5693
- const year = d.getFullYear();
5694
- const month = String(d.getMonth() + 1).padStart(2, "0");
5695
- const day = String(d.getDate()).padStart(2, "0");
5696
- return `${year}-${month}-${day}`;
5732
+ }
5733
+
5734
+ // src/light/validators.ts
5735
+ var VALID_MODES = [
5736
+ "wave",
5737
+ "breath",
5738
+ "strobe",
5739
+ "steady",
5740
+ "wave_rainbow",
5741
+ "pixel_frame"
5742
+ ];
5743
+ var MAX_SEGMENTS = 12;
5744
+ function validateSegments(segments) {
5745
+ if (!Array.isArray(segments)) {
5746
+ return { valid: false, errors: [{ field: "segments", message: "\u5FC5\u987B\u662F\u6570\u7EC4" }] };
5697
5747
  }
5698
- getIdIndexPath(dateKey) {
5699
- return (0, import_node_path2.join)(this.idIndexDir, `${dateKey}.ids`);
5748
+ if (segments.length === 0) {
5749
+ return { valid: false, errors: [{ field: "segments", message: "\u4E0D\u80FD\u4E3A\u7A7A" }] };
5700
5750
  }
5701
- getIdSet(dateKey) {
5702
- const cached = this.idCache.get(dateKey);
5703
- if (cached) {
5704
- return cached;
5705
- }
5706
- const idPath = this.getIdIndexPath(dateKey);
5707
- const ids = /* @__PURE__ */ new Set();
5708
- if ((0, import_node_fs3.existsSync)(idPath)) {
5709
- const lines = (0, import_node_fs3.readFileSync)(idPath, "utf-8").split(/\r?\n/);
5710
- for (const line of lines) {
5711
- const id = line.trim();
5712
- if (id) {
5713
- ids.add(id);
5714
- }
5715
- }
5716
- }
5717
- this.idCache.set(dateKey, ids);
5718
- return ids;
5751
+ if (segments.length > MAX_SEGMENTS) {
5752
+ return {
5753
+ valid: false,
5754
+ errors: [{ field: "segments", message: `\u6700\u591A ${MAX_SEGMENTS} \u6BB5` }]
5755
+ };
5719
5756
  }
5720
- hasNotificationId(dateKey, id) {
5721
- return this.getIdSet(dateKey).has(id);
5757
+ const errors = [];
5758
+ for (let i = 0; i < segments.length; i++) {
5759
+ validateSegment(segments[i], `segments[${i}]`, errors);
5722
5760
  }
5723
- getContentKeyIndexPath(dateKey) {
5724
- return (0, import_node_path2.join)(this.contentKeyIndexDir, `${dateKey}.keys`);
5761
+ if (errors.length > 0) return { valid: false, errors };
5762
+ return { valid: true, segments };
5763
+ }
5764
+ function parseAndValidateSegments(json) {
5765
+ let parsed;
5766
+ try {
5767
+ parsed = JSON.parse(json);
5768
+ } catch {
5769
+ exitError("VALIDATION_FAILED", "segments \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON");
5725
5770
  }
5726
- getContentKeySet(dateKey, filePath) {
5727
- const cached = this.contentKeyCache.get(dateKey);
5728
- if (cached) {
5729
- return cached;
5730
- }
5731
- const keyPath = this.getContentKeyIndexPath(dateKey);
5732
- const keys = /* @__PURE__ */ new Set();
5733
- if ((0, import_node_fs3.existsSync)(filePath)) {
5734
- for (const item of this.readStoredNotifications(filePath)) {
5735
- keys.add(this.buildNotificationContentKey(item));
5736
- }
5737
- }
5738
- if (keys.size > 0) {
5739
- (0, import_node_fs3.writeFileSync)(keyPath, `${Array.from(keys).join("\n")}
5740
- `, "utf-8");
5741
- } else if ((0, import_node_fs3.existsSync)(keyPath)) {
5742
- (0, import_node_fs3.rmSync)(keyPath, { force: true });
5743
- }
5744
- this.contentKeyCache.set(dateKey, keys);
5745
- return keys;
5771
+ const result = validateSegments(parsed);
5772
+ if (!result.valid) {
5773
+ output({ ok: false, error: { code: "VALIDATION_FAILED", details: result.errors } });
5774
+ process.exit(1);
5746
5775
  }
5747
- hasNotificationContentKey(dateKey, filePath, entry) {
5748
- return this.getContentKeySet(dateKey, filePath).has(
5749
- this.buildNotificationContentKey(entry)
5750
- );
5776
+ return result.segments;
5777
+ }
5778
+ function validateSegment(seg, prefix, errors) {
5779
+ if (!isRecord(seg)) {
5780
+ errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5781
+ return;
5751
5782
  }
5752
- recordNotificationId(dateKey, id) {
5753
- const ids = this.getIdSet(dateKey);
5754
- if (ids.has(id)) {
5755
- return;
5756
- }
5757
- (0, import_node_fs3.appendFileSync)(this.getIdIndexPath(dateKey), `${id}
5758
- `, "utf-8");
5759
- ids.add(id);
5783
+ const mode = seg.mode;
5784
+ if (!VALID_MODES.includes(mode)) {
5785
+ errors.push({
5786
+ field: `${prefix}.mode`,
5787
+ message: `\u4E0D\u652F\u6301\u7684\u6A21\u5F0F '${String(mode)}'\uFF0C\u53EF\u9009\uFF1A${VALID_MODES.join("/")}`
5788
+ });
5760
5789
  }
5761
- recordNotificationContentKey(dateKey, filePath, entry) {
5762
- const keys = this.getContentKeySet(dateKey, filePath);
5763
- const key = this.buildNotificationContentKey(entry);
5764
- if (keys.has(key)) {
5765
- return;
5766
- }
5767
- (0, import_node_fs3.appendFileSync)(this.getContentKeyIndexPath(dateKey), `${key}
5768
- `, "utf-8");
5769
- keys.add(key);
5790
+ validateNonNegativeNumber(seg.duration_s, `${prefix}.duration_s`, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57\uFF080 \u8868\u793A\u65E0\u9650\u65F6\u957F\uFF09");
5791
+ switch (mode) {
5792
+ case "wave":
5793
+ validateForegroundSegment(seg, prefix, errors);
5794
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5795
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5796
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5797
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5798
+ break;
5799
+ case "wave_rainbow":
5800
+ validateForegroundSegment(seg, prefix, errors);
5801
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5802
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5803
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5804
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5805
+ if (!hasNonZeroRgb(seg.color) && !hasNonZeroRgb(seg.background)) {
5806
+ errors.push({
5807
+ field: prefix,
5808
+ message: "wave_rainbow \u81F3\u5C11\u9700\u8981\u4E00\u7EC4\u975E\u96F6\u989C\u8272\u951A\u70B9\uFF08color \u6216 background\uFF09"
5809
+ });
5810
+ }
5811
+ break;
5812
+ case "breath":
5813
+ validateForegroundSegment(seg, prefix, errors);
5814
+ validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
5815
+ break;
5816
+ case "strobe":
5817
+ validateForegroundSegment(seg, prefix, errors);
5818
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5819
+ break;
5820
+ case "steady":
5821
+ validateForegroundSegment(seg, prefix, errors);
5822
+ break;
5823
+ case "pixel_frame":
5824
+ validatePixelFrame(seg.pixels, `${prefix}.pixels`, errors);
5825
+ break;
5826
+ default:
5827
+ validateOptionalNonNegativeNumber(seg.brightness, `${prefix}.brightness`, errors);
5828
+ validateOptionalColor(seg.color, `${prefix}.color`, errors);
5829
+ validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
5830
+ validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
5831
+ validateOptionalWindow(seg.window, `${prefix}.window`, errors);
5832
+ validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
5833
+ validateOptionalBackground(seg.background, `${prefix}.background`, errors);
5770
5834
  }
5771
- buildNotificationContentKey(entry) {
5772
- return (0, import_node_crypto.createHash)("sha256").update(entry.appName).update("").update(entry.title).update("").update(entry.content).update("").update(entry.timestamp).digest("hex");
5835
+ }
5836
+ function validateForegroundSegment(seg, prefix, errors) {
5837
+ validateNumberInRange(
5838
+ seg.brightness,
5839
+ `${prefix}.brightness`,
5840
+ errors,
5841
+ 0,
5842
+ 255,
5843
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5844
+ );
5845
+ validateColor(seg.color, `${prefix}.color`, errors);
5846
+ if (seg.mode !== "steady" && seg.brightness === 0) {
5847
+ errors.push({
5848
+ field: `${prefix}.brightness`,
5849
+ message: "brightness=0 \u4EC5 steady \u6A21\u5F0F\u5141\u8BB8\uFF1B\u5176\u5B83\u6A21\u5F0F\u4F1A\u5728\u56FA\u4EF6\u4FA7\u88AB\u8FC7\u6EE4"
5850
+ });
5773
5851
  }
5774
- readStoredNotifications(filePath) {
5775
- if (!(0, import_node_fs3.existsSync)(filePath)) {
5776
- return [];
5777
- }
5778
- try {
5779
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf-8"));
5780
- return Array.isArray(parsed) ? parsed : [];
5781
- } catch {
5782
- return [];
5783
- }
5852
+ }
5853
+ function validatePixelFrame(value, field, errors) {
5854
+ if (!Array.isArray(value)) {
5855
+ errors.push({ field, message: "pixel_frame \u5FC5\u987B\u63D0\u4F9B pixels \u6570\u7EC4\uFF081\u20137 \u9879\uFF09" });
5856
+ return;
5784
5857
  }
5785
- async withDateWriteLock(dateKey, task) {
5786
- const previous = this.dateWriteChains.get(dateKey) ?? Promise.resolve();
5787
- let release;
5788
- const current = new Promise((resolve) => {
5789
- release = resolve;
5790
- });
5791
- const chain = previous.then(() => current);
5792
- this.dateWriteChains.set(dateKey, chain);
5793
- await previous;
5794
- try {
5795
- return await task();
5796
- } finally {
5797
- release();
5798
- if (this.dateWriteChains.get(dateKey) === chain) {
5799
- this.dateWriteChains.delete(dateKey);
5800
- }
5801
- }
5858
+ if (value.length < 1 || value.length > 7) {
5859
+ errors.push({ field, message: "pixels \u5FC5\u987B\u4E3A 1\u20137 \u9879" });
5802
5860
  }
5803
- prune() {
5804
- const retentionDays = this.config.retentionDays;
5805
- if (retentionDays === void 0) {
5806
- return;
5861
+ const seen = /* @__PURE__ */ new Set();
5862
+ for (let i = 0; i < value.length; i++) {
5863
+ const pixel = value[i];
5864
+ const prefix = `${field}[${i}]`;
5865
+ if (!isRecord(pixel)) {
5866
+ errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5867
+ continue;
5807
5868
  }
5808
- const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
5809
- const cutoffDate = this.formatDate(new Date(cutoffMs));
5810
- this.pruneDataFiles(cutoffDate);
5811
- this.pruneIdIndex(cutoffDate);
5812
- this.pruneContentKeyIndex(cutoffDate);
5813
- }
5814
- /** Remove expired .json, legacy .md files, and legacy date directories */
5815
- pruneDataFiles(cutoffDate) {
5816
- const dateFilePattern = /^(\d{4}-\d{2}-\d{2})\.(json|md)$/;
5817
- const dateDirPattern = /^\d{4}-\d{2}-\d{2}$/;
5818
- try {
5819
- for (const entry of (0, import_node_fs3.readdirSync)(this.dir, { withFileTypes: true })) {
5820
- if (entry.isFile()) {
5821
- const match = dateFilePattern.exec(entry.name);
5822
- if (match && match[1] < cutoffDate) {
5823
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.dir, entry.name), { force: true });
5824
- }
5825
- } else if (entry.isDirectory() && dateDirPattern.test(entry.name) && entry.name < cutoffDate) {
5826
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.dir, entry.name), { recursive: true, force: true });
5827
- }
5828
- }
5829
- } catch {
5869
+ const idx = pixel.index;
5870
+ if (!Number.isInteger(idx) || idx < 0 || idx > 6) {
5871
+ errors.push({ field: `${prefix}.index`, message: "index \u5FC5\u987B\u662F 0\u20136 \u7684\u6574\u6570" });
5872
+ } else if (seen.has(idx)) {
5873
+ errors.push({ field: `${prefix}.index`, message: `index=${idx} \u91CD\u590D` });
5874
+ } else {
5875
+ seen.add(idx);
5830
5876
  }
5831
- }
5832
- /** Remove expired .ids index files */
5833
- pruneIdIndex(cutoffDate) {
5834
- try {
5835
- for (const entry of (0, import_node_fs3.readdirSync)(this.idIndexDir, { withFileTypes: true })) {
5836
- if (!entry.isFile()) continue;
5837
- const match = /^(\d{4}-\d{2}-\d{2})\.ids$/.exec(entry.name);
5838
- if (match && match[1] < cutoffDate) {
5839
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.idIndexDir, entry.name), { force: true });
5840
- this.idCache.delete(match[1]);
5841
- }
5842
- }
5843
- } catch {
5844
- }
5845
- }
5846
- pruneContentKeyIndex(cutoffDate) {
5847
- try {
5848
- for (const entry of (0, import_node_fs3.readdirSync)(this.contentKeyIndexDir, { withFileTypes: true })) {
5849
- if (!entry.isFile()) continue;
5850
- const match = /^(\d{4}-\d{2}-\d{2})\.keys$/.exec(entry.name);
5851
- if (match && match[1] < cutoffDate) {
5852
- (0, import_node_fs3.rmSync)((0, import_node_path2.join)(this.contentKeyIndexDir, entry.name), { force: true });
5853
- this.contentKeyCache.delete(match[1]);
5854
- }
5855
- }
5856
- } catch {
5857
- }
5858
- }
5859
- async close() {
5860
- this.idCache.clear();
5861
- this.contentKeyCache.clear();
5862
- this.dateWriteChains.clear();
5863
- }
5864
- };
5865
-
5866
- // src/light/repeat.ts
5867
- function normalizeRepeatTimes(input) {
5868
- if (typeof input === "boolean") {
5869
- return input ? 0 : 1;
5870
- }
5871
- if (typeof input === "number") {
5872
- return validateRepeatTimes(input);
5873
- }
5874
- if (!input) {
5875
- return 1;
5876
- }
5877
- if (input.repeat_times !== void 0) {
5878
- return validateRepeatTimes(input.repeat_times);
5879
- }
5880
- if (input.repeat !== void 0) {
5881
- return input.repeat ? 0 : 1;
5882
- }
5883
- return 1;
5884
- }
5885
- function assertAncsRepeatTimes(repeatTimes) {
5886
- if (repeatTimes !== 0 && repeatTimes !== 1) {
5887
- throw new Error(
5888
- "\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"
5877
+ validateNumberInRange(
5878
+ pixel.brightness,
5879
+ `${prefix}.brightness`,
5880
+ errors,
5881
+ 0,
5882
+ 255,
5883
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5889
5884
  );
5885
+ validateColor(pixel.color, `${prefix}.color`, errors);
5890
5886
  }
5891
5887
  }
5892
- function validateRepeatTimes(value) {
5893
- if (!Number.isInteger(value) || value < 0) {
5894
- throw new Error("repeat_times \u5FC5\u987B\u662F >=0 \u7684\u6574\u6570");
5888
+ function validateOptionalBreathTiming(value, field, errors) {
5889
+ if (value === void 0) return;
5890
+ if (!isRecord(value)) {
5891
+ errors.push({ field, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
5892
+ return;
5895
5893
  }
5896
- return value;
5894
+ validatePositiveNumber(
5895
+ value.rise_ms,
5896
+ `${field}.rise_ms`,
5897
+ errors,
5898
+ "rise_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
5899
+ );
5900
+ validateNonNegativeNumber(
5901
+ value.hold_ms,
5902
+ `${field}.hold_ms`,
5903
+ errors,
5904
+ "hold_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
5905
+ );
5906
+ validatePositiveNumber(
5907
+ value.fall_ms,
5908
+ `${field}.fall_ms`,
5909
+ errors,
5910
+ "fall_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
5911
+ );
5912
+ validateNonNegativeNumber(
5913
+ value.off_ms,
5914
+ `${field}.off_ms`,
5915
+ errors,
5916
+ "off_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
5917
+ );
5897
5918
  }
5898
-
5899
- // src/cli/helpers.ts
5900
- var import_node_fs4 = require("fs");
5901
- var import_promises = require("fs/promises");
5902
- var import_node_path3 = require("path");
5903
- function resolveNotificationsDir(ctx) {
5904
- if (ctx.stateDir) {
5905
- const dir = (0, import_node_path3.join)(
5906
- ctx.stateDir,
5907
- "plugins",
5908
- "phone-notifications",
5909
- "notifications"
5910
- );
5911
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
5912
- }
5913
- if (ctx.workspaceDir) {
5914
- const dir = (0, import_node_path3.join)(ctx.workspaceDir, "notifications");
5915
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
5919
+ function validateOptionalBackground(value, field, errors) {
5920
+ if (value === void 0) return;
5921
+ if (!isRecord(value)) {
5922
+ errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b/brightness \u6570\u503C" });
5923
+ return;
5916
5924
  }
5917
- return null;
5925
+ validateColor(value, field, errors);
5926
+ validateNumberInRange(
5927
+ value.brightness,
5928
+ `${field}.brightness`,
5929
+ errors,
5930
+ 0,
5931
+ 255,
5932
+ "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
5933
+ );
5918
5934
  }
5919
- function listDateKeys(dir) {
5920
- const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5921
- const keys = [];
5922
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
5923
- if (!entry.isFile()) continue;
5924
- const m = pattern.exec(entry.name);
5925
- if (m) keys.push(m[1]);
5926
- }
5927
- return keys.sort().reverse();
5935
+ function validateOptionalColor(value, field, errors) {
5936
+ if (value === void 0) return;
5937
+ validateColor(value, field, errors);
5928
5938
  }
5929
- function readDateFile(dir, dateKey) {
5930
- const filePath = (0, import_node_path3.join)(dir, `${dateKey}.json`);
5931
- if (!(0, import_node_fs4.existsSync)(filePath)) return [];
5932
- try {
5933
- return JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf-8"));
5934
- } catch {
5935
- return [];
5939
+ function validateColor(value, field, errors) {
5940
+ if (!isRecord(value)) {
5941
+ errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b \u6570\u503C" });
5942
+ return;
5936
5943
  }
5944
+ validateNumberInRange(value.r, `${field}.r`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5945
+ validateNumberInRange(value.g, `${field}.g`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5946
+ validateNumberInRange(value.b, `${field}.b`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
5937
5947
  }
5938
- function today() {
5939
- return formatDate2(/* @__PURE__ */ new Date());
5940
- }
5941
- function formatDate2(d) {
5942
- const y = d.getFullYear();
5943
- const m = String(d.getMonth() + 1).padStart(2, "0");
5944
- const day = String(d.getDate()).padStart(2, "0");
5945
- return `${y}-${m}-${day}`;
5946
- }
5947
- function daysAgo(n) {
5948
- const d = /* @__PURE__ */ new Date();
5949
- d.setDate(d.getDate() - n);
5950
- return formatDate2(d);
5951
- }
5952
- function filterDateRange(keys, from, to) {
5953
- return keys.filter((k) => k >= from && k <= to);
5954
- }
5955
- function parseIsoTime(value, optionName) {
5956
- const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
5957
- if (!isoPattern.test(value)) {
5958
- exitError(
5959
- "INVALID_TIME",
5960
- `${optionName} \u5FC5\u987B\u662F ISO 8601 \u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5961
- );
5948
+ function validateOptionalDirection(value, field, errors) {
5949
+ if (value === void 0) return;
5950
+ if (value !== "ltr" && value !== "rtl") {
5951
+ errors.push({ field, message: "direction \u5FC5\u987B\u662F ltr \u6216 rtl" });
5962
5952
  }
5963
- const ts = Date.parse(value);
5964
- if (Number.isNaN(ts)) {
5965
- exitError(
5966
- "INVALID_TIME",
5967
- `${optionName} \u4E0D\u662F\u5408\u6CD5\u65F6\u95F4\uFF0C\u4F8B\u5982 2026-03-01T09:00:00+08:00`
5968
- );
5953
+ }
5954
+ function validateOptionalWindow(value, field, errors) {
5955
+ if (value === void 0) return;
5956
+ if (value !== 1 && value !== 2 && value !== 3) {
5957
+ errors.push({ field, message: "window \u4EC5\u652F\u6301 1/2/3" });
5969
5958
  }
5970
- return ts;
5971
5959
  }
5972
- function sortNotificationsByTimestampDesc(items) {
5973
- return items.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
5960
+ function validateOptionalNonNegativeNumber(value, field, errors) {
5961
+ if (value === void 0) return;
5962
+ validateNonNegativeNumber(value, field, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57");
5974
5963
  }
5975
- async function listDateKeysAsync(dir) {
5976
- const pattern = /^(\d{4}-\d{2}-\d{2})\.json$/;
5977
- const keys = [];
5978
- const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
5979
- for (const entry of entries) {
5980
- if (!entry.isFile()) continue;
5981
- const m = pattern.exec(entry.name);
5982
- if (m) keys.push(m[1]);
5964
+ function validatePositiveNumber(value, field, errors, message) {
5965
+ if (value === void 0) return;
5966
+ if (!isFiniteNumber(value) || value <= 0) {
5967
+ errors.push({ field, message });
5983
5968
  }
5984
- return keys.sort().reverse();
5985
5969
  }
5986
- async function readDateFileAsync(dir, dateKey) {
5987
- const filePath = (0, import_node_path3.join)(dir, `${dateKey}.json`);
5988
- if (!(0, import_node_fs4.existsSync)(filePath)) return [];
5989
- try {
5990
- const content = await (0, import_promises.readFile)(filePath, "utf-8");
5991
- return JSON.parse(content);
5992
- } catch {
5993
- return [];
5970
+ function validateNonNegativeNumber(value, field, errors, message) {
5971
+ if (!isFiniteNumber(value) || value < 0) {
5972
+ errors.push({ field, message });
5994
5973
  }
5995
5974
  }
5996
- function progress(message) {
5997
- process.stderr.write(message + "\n");
5998
- }
5999
- function output(data) {
6000
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
6001
- }
6002
- function exitError(code, message) {
6003
- output({ ok: false, error: { code, message } });
6004
- process.exit(1);
6005
- }
6006
- function resolveRecordingsDir(ctx) {
6007
- if (ctx.stateDir) {
6008
- const dir = (0, import_node_path3.join)(
6009
- ctx.stateDir,
6010
- "plugins",
6011
- "phone-notifications",
6012
- "recordings"
6013
- );
6014
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
6015
- }
6016
- if (ctx.workspaceDir) {
6017
- const dir = (0, import_node_path3.join)(ctx.workspaceDir, "recordings");
6018
- if ((0, import_node_fs4.existsSync)(dir)) return dir;
5975
+ function validateNumberInRange(value, field, errors, min, max, message) {
5976
+ if (!isFiniteNumber(value) || value < min || value > max) {
5977
+ errors.push({ field, message });
6019
5978
  }
6020
- return null;
6021
5979
  }
6022
- function resolveAsrConfigPath(ctx) {
6023
- let base;
6024
- if (ctx.stateDir) {
6025
- base = (0, import_node_path3.join)(ctx.stateDir, "plugins", "phone-notifications", "recordings");
6026
- } else if (ctx.workspaceDir) {
6027
- base = (0, import_node_path3.join)(ctx.workspaceDir, "recordings");
6028
- } else {
6029
- exitError(
6030
- "STORAGE_UNAVAILABLE",
6031
- "\u65E0\u6CD5\u786E\u5B9A\u5F55\u97F3\u5B58\u50A8\u76EE\u5F55\uFF1AstateDir \u548C workspaceDir \u5747\u672A\u8BBE\u7F6E"
6032
- );
6033
- }
6034
- (0, import_node_fs4.mkdirSync)(base, { recursive: true });
6035
- return (0, import_node_path3.join)(base, "asr-config.json");
5980
+ function hasNonZeroRgb(value) {
5981
+ if (!value) return false;
5982
+ return [value.r, value.g, value.b].some((channel) => isFiniteNumber(channel) && channel > 0);
6036
5983
  }
6037
- function readRecordingIndex(dir) {
6038
- const indexPath = (0, import_node_path3.join)(dir, "index.json");
6039
- if (!(0, import_node_fs4.existsSync)(indexPath)) return [];
6040
- try {
6041
- const raw = JSON.parse((0, import_node_fs4.readFileSync)(indexPath, "utf-8"));
6042
- return Array.isArray(raw?.recordings) ? raw.recordings : [];
6043
- } catch {
6044
- return [];
6045
- }
6046
- }
6047
-
6048
- // src/light/validators.ts
6049
- var VALID_MODES = [
6050
- "wave",
6051
- "breath",
6052
- "strobe",
6053
- "steady",
6054
- "wave_rainbow",
6055
- "pixel_frame"
6056
- ];
6057
- var MAX_SEGMENTS = 12;
6058
- function validateSegments(segments) {
6059
- if (!Array.isArray(segments)) {
6060
- return { valid: false, errors: [{ field: "segments", message: "\u5FC5\u987B\u662F\u6570\u7EC4" }] };
6061
- }
6062
- if (segments.length === 0) {
6063
- return { valid: false, errors: [{ field: "segments", message: "\u4E0D\u80FD\u4E3A\u7A7A" }] };
6064
- }
6065
- if (segments.length > MAX_SEGMENTS) {
6066
- return {
6067
- valid: false,
6068
- errors: [{ field: "segments", message: `\u6700\u591A ${MAX_SEGMENTS} \u6BB5` }]
6069
- };
6070
- }
6071
- const errors = [];
6072
- for (let i = 0; i < segments.length; i++) {
6073
- validateSegment(segments[i], `segments[${i}]`, errors);
6074
- }
6075
- if (errors.length > 0) return { valid: false, errors };
6076
- return { valid: true, segments };
6077
- }
6078
- function parseAndValidateSegments(json) {
6079
- let parsed;
6080
- try {
6081
- parsed = JSON.parse(json);
6082
- } catch {
6083
- exitError("VALIDATION_FAILED", "segments \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON");
6084
- }
6085
- const result = validateSegments(parsed);
6086
- if (!result.valid) {
6087
- output({ ok: false, error: { code: "VALIDATION_FAILED", details: result.errors } });
6088
- process.exit(1);
6089
- }
6090
- return result.segments;
6091
- }
6092
- function validateSegment(seg, prefix, errors) {
6093
- if (!isRecord(seg)) {
6094
- errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6095
- return;
6096
- }
6097
- const mode = seg.mode;
6098
- if (!VALID_MODES.includes(mode)) {
6099
- errors.push({
6100
- field: `${prefix}.mode`,
6101
- message: `\u4E0D\u652F\u6301\u7684\u6A21\u5F0F '${String(mode)}'\uFF0C\u53EF\u9009\uFF1A${VALID_MODES.join("/")}`
6102
- });
6103
- }
6104
- validateNonNegativeNumber(seg.duration_s, `${prefix}.duration_s`, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57\uFF080 \u8868\u793A\u65E0\u9650\u65F6\u957F\uFF09");
6105
- switch (mode) {
6106
- case "wave":
6107
- validateForegroundSegment(seg, prefix, errors);
6108
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6109
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6110
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6111
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6112
- break;
6113
- case "wave_rainbow":
6114
- validateForegroundSegment(seg, prefix, errors);
6115
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6116
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6117
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6118
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6119
- if (!hasNonZeroRgb(seg.color) && !hasNonZeroRgb(seg.background)) {
6120
- errors.push({
6121
- field: prefix,
6122
- message: "wave_rainbow \u81F3\u5C11\u9700\u8981\u4E00\u7EC4\u975E\u96F6\u989C\u8272\u951A\u70B9\uFF08color \u6216 background\uFF09"
6123
- });
6124
- }
6125
- break;
6126
- case "breath":
6127
- validateForegroundSegment(seg, prefix, errors);
6128
- validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
6129
- break;
6130
- case "strobe":
6131
- validateForegroundSegment(seg, prefix, errors);
6132
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6133
- break;
6134
- case "steady":
6135
- validateForegroundSegment(seg, prefix, errors);
6136
- break;
6137
- case "pixel_frame":
6138
- validatePixelFrame(seg.pixels, `${prefix}.pixels`, errors);
6139
- break;
6140
- default:
6141
- validateOptionalNonNegativeNumber(seg.brightness, `${prefix}.brightness`, errors);
6142
- validateOptionalColor(seg.color, `${prefix}.color`, errors);
6143
- validateOptionalNonNegativeNumber(seg.interval_ms, `${prefix}.interval_ms`, errors);
6144
- validateOptionalDirection(seg.direction, `${prefix}.direction`, errors);
6145
- validateOptionalWindow(seg.window, `${prefix}.window`, errors);
6146
- validateOptionalBreathTiming(seg.breath_timing, `${prefix}.breath_timing`, errors);
6147
- validateOptionalBackground(seg.background, `${prefix}.background`, errors);
6148
- }
6149
- }
6150
- function validateForegroundSegment(seg, prefix, errors) {
6151
- validateNumberInRange(
6152
- seg.brightness,
6153
- `${prefix}.brightness`,
6154
- errors,
6155
- 0,
6156
- 255,
6157
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6158
- );
6159
- validateColor(seg.color, `${prefix}.color`, errors);
6160
- if (seg.mode !== "steady" && seg.brightness === 0) {
6161
- errors.push({
6162
- field: `${prefix}.brightness`,
6163
- message: "brightness=0 \u4EC5 steady \u6A21\u5F0F\u5141\u8BB8\uFF1B\u5176\u5B83\u6A21\u5F0F\u4F1A\u5728\u56FA\u4EF6\u4FA7\u88AB\u8FC7\u6EE4"
6164
- });
6165
- }
6166
- }
6167
- function validatePixelFrame(value, field, errors) {
6168
- if (!Array.isArray(value)) {
6169
- errors.push({ field, message: "pixel_frame \u5FC5\u987B\u63D0\u4F9B pixels \u6570\u7EC4\uFF081\u20137 \u9879\uFF09" });
6170
- return;
6171
- }
6172
- if (value.length < 1 || value.length > 7) {
6173
- errors.push({ field, message: "pixels \u5FC5\u987B\u4E3A 1\u20137 \u9879" });
6174
- }
6175
- const seen = /* @__PURE__ */ new Set();
6176
- for (let i = 0; i < value.length; i++) {
6177
- const pixel = value[i];
6178
- const prefix = `${field}[${i}]`;
6179
- if (!isRecord(pixel)) {
6180
- errors.push({ field: prefix, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6181
- continue;
6182
- }
6183
- const idx = pixel.index;
6184
- if (!Number.isInteger(idx) || idx < 0 || idx > 6) {
6185
- errors.push({ field: `${prefix}.index`, message: "index \u5FC5\u987B\u662F 0\u20136 \u7684\u6574\u6570" });
6186
- } else if (seen.has(idx)) {
6187
- errors.push({ field: `${prefix}.index`, message: `index=${idx} \u91CD\u590D` });
6188
- } else {
6189
- seen.add(idx);
6190
- }
6191
- validateNumberInRange(
6192
- pixel.brightness,
6193
- `${prefix}.brightness`,
6194
- errors,
6195
- 0,
6196
- 255,
6197
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6198
- );
6199
- validateColor(pixel.color, `${prefix}.color`, errors);
6200
- }
6201
- }
6202
- function validateOptionalBreathTiming(value, field, errors) {
6203
- if (value === void 0) return;
6204
- if (!isRecord(value)) {
6205
- errors.push({ field, message: "\u5FC5\u987B\u662F\u5BF9\u8C61" });
6206
- return;
6207
- }
6208
- validatePositiveNumber(
6209
- value.rise_ms,
6210
- `${field}.rise_ms`,
6211
- errors,
6212
- "rise_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
6213
- );
6214
- validateNonNegativeNumber(
6215
- value.hold_ms,
6216
- `${field}.hold_ms`,
6217
- errors,
6218
- "hold_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
6219
- );
6220
- validatePositiveNumber(
6221
- value.fall_ms,
6222
- `${field}.fall_ms`,
6223
- errors,
6224
- "fall_ms \u5FC5\u987B\u662F >0 \u7684\u6570\u5B57\uFF08\u4E0D\u652F\u6301 0ms\uFF09"
6225
- );
6226
- validateNonNegativeNumber(
6227
- value.off_ms,
6228
- `${field}.off_ms`,
6229
- errors,
6230
- "off_ms \u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57"
6231
- );
6232
- }
6233
- function validateOptionalBackground(value, field, errors) {
6234
- if (value === void 0) return;
6235
- if (!isRecord(value)) {
6236
- errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b/brightness \u6570\u503C" });
6237
- return;
6238
- }
6239
- validateColor(value, field, errors);
6240
- validateNumberInRange(
6241
- value.brightness,
6242
- `${field}.brightness`,
6243
- errors,
6244
- 0,
6245
- 255,
6246
- "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57"
6247
- );
6248
- }
6249
- function validateOptionalColor(value, field, errors) {
6250
- if (value === void 0) return;
6251
- validateColor(value, field, errors);
6252
- }
6253
- function validateColor(value, field, errors) {
6254
- if (!isRecord(value)) {
6255
- errors.push({ field, message: "\u5FC5\u987B\u5305\u542B r/g/b \u6570\u503C" });
6256
- return;
6257
- }
6258
- validateNumberInRange(value.r, `${field}.r`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6259
- validateNumberInRange(value.g, `${field}.g`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6260
- validateNumberInRange(value.b, `${field}.b`, errors, 0, 255, "\u5FC5\u987B\u662F 0\u2013255 \u7684\u6570\u5B57");
6261
- }
6262
- function validateOptionalDirection(value, field, errors) {
6263
- if (value === void 0) return;
6264
- if (value !== "ltr" && value !== "rtl") {
6265
- errors.push({ field, message: "direction \u5FC5\u987B\u662F ltr \u6216 rtl" });
6266
- }
6267
- }
6268
- function validateOptionalWindow(value, field, errors) {
6269
- if (value === void 0) return;
6270
- if (value !== 1 && value !== 2 && value !== 3) {
6271
- errors.push({ field, message: "window \u4EC5\u652F\u6301 1/2/3" });
6272
- }
6273
- }
6274
- function validateOptionalNonNegativeNumber(value, field, errors) {
6275
- if (value === void 0) return;
6276
- validateNonNegativeNumber(value, field, errors, "\u5FC5\u987B\u662F \u22650 \u7684\u6570\u5B57");
6277
- }
6278
- function validatePositiveNumber(value, field, errors, message) {
6279
- if (value === void 0) return;
6280
- if (!isFiniteNumber(value) || value <= 0) {
6281
- errors.push({ field, message });
6282
- }
6283
- }
6284
- function validateNonNegativeNumber(value, field, errors, message) {
6285
- if (!isFiniteNumber(value) || value < 0) {
6286
- errors.push({ field, message });
6287
- }
6288
- }
6289
- function validateNumberInRange(value, field, errors, min, max, message) {
6290
- if (!isFiniteNumber(value) || value < min || value > max) {
6291
- errors.push({ field, message });
6292
- }
6293
- }
6294
- function hasNonZeroRgb(value) {
6295
- if (!value) return false;
6296
- return [value.r, value.g, value.b].some((channel) => isFiniteNumber(channel) && channel > 0);
6297
- }
6298
- function isFiniteNumber(value) {
6299
- return typeof value === "number" && Number.isFinite(value);
5984
+ function isFiniteNumber(value) {
5985
+ return typeof value === "number" && Number.isFinite(value);
6300
5986
  }
6301
5987
  function isRecord(value) {
6302
5988
  return value !== null && typeof value === "object" && !Array.isArray(value);
6303
5989
  }
6304
5990
 
6305
5991
  // src/light-rules/storage.ts
6306
- var import_node_fs5 = require("fs");
6307
- var import_node_path4 = require("path");
5992
+ var import_node_fs4 = require("fs");
5993
+ var import_node_path3 = require("path");
6308
5994
 
6309
5995
  // src/monitor/fetch-gen.ts
6310
5996
  function generateFetchPy(name, matchRules) {
@@ -6414,14 +6100,14 @@ function legacyReadMetaCronSchedule(meta) {
6414
6100
  function resolveBaseDir(ctx) {
6415
6101
  if (ctx.workspaceDir) return ctx.workspaceDir;
6416
6102
  if (ctx.stateDir) {
6417
- const inferredWorkspaceDir = (0, import_node_path4.join)(ctx.stateDir, "workspace");
6418
- if ((0, import_node_fs5.existsSync)(inferredWorkspaceDir)) return inferredWorkspaceDir;
6103
+ const inferredWorkspaceDir = (0, import_node_path3.join)(ctx.stateDir, "workspace");
6104
+ if ((0, import_node_fs4.existsSync)(inferredWorkspaceDir)) return inferredWorkspaceDir;
6419
6105
  return ctx.stateDir;
6420
6106
  }
6421
6107
  throw new Error("workspaceDir and stateDir both unavailable");
6422
6108
  }
6423
6109
  function tasksDir(ctx) {
6424
- return (0, import_node_path4.join)(resolveBaseDir(ctx), "tasks");
6110
+ return (0, import_node_path3.join)(resolveBaseDir(ctx), "tasks");
6425
6111
  }
6426
6112
  function normalizeLightRuleLookupName(name) {
6427
6113
  return name.trim().replace(/\.json$/i, "");
@@ -6430,7 +6116,7 @@ function resolveLightRuleTask(ctx, name) {
6430
6116
  const dir = tasksDir(ctx);
6431
6117
  const normalizedName = normalizeLightRuleLookupName(name);
6432
6118
  if (!normalizedName) return null;
6433
- const directTaskDir = (0, import_node_path4.join)(dir, normalizedName);
6119
+ const directTaskDir = (0, import_node_path3.join)(dir, normalizedName);
6434
6120
  const directMeta = readMeta(directTaskDir);
6435
6121
  if (directMeta) {
6436
6122
  return {
@@ -6438,10 +6124,10 @@ function resolveLightRuleTask(ctx, name) {
6438
6124
  meta: directMeta
6439
6125
  };
6440
6126
  }
6441
- if (!(0, import_node_fs5.existsSync)(dir)) return null;
6442
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
6127
+ if (!(0, import_node_fs4.existsSync)(dir)) return null;
6128
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
6443
6129
  if (!entry.isDirectory()) continue;
6444
- const taskDir = (0, import_node_path4.join)(dir, entry.name);
6130
+ const taskDir = (0, import_node_path3.join)(dir, entry.name);
6445
6131
  const meta = readMeta(taskDir);
6446
6132
  if (meta?.name === normalizedName) {
6447
6133
  return {
@@ -6461,16 +6147,16 @@ function isLegacyLightRuleWithoutType(raw) {
6461
6147
  return raw.type === void 0 && readOptionalString(raw.name) !== void 0 && readOptionalString(raw.description) !== void 0 && Array.isArray(raw.segments);
6462
6148
  }
6463
6149
  function readMeta(taskDir) {
6464
- const metaPath = (0, import_node_path4.join)(taskDir, "meta.json");
6465
- if (!(0, import_node_fs5.existsSync)(metaPath)) return null;
6150
+ const metaPath = (0, import_node_path3.join)(taskDir, "meta.json");
6151
+ if (!(0, import_node_fs4.existsSync)(metaPath)) return null;
6466
6152
  try {
6467
- const raw = JSON.parse((0, import_node_fs5.readFileSync)(metaPath, "utf-8"));
6153
+ const raw = JSON.parse((0, import_node_fs4.readFileSync)(metaPath, "utf-8"));
6468
6154
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
6469
6155
  if (raw.type !== "light-rule" && !isLegacyLightRuleWithoutType(raw)) return null;
6470
6156
  if (!Array.isArray(raw.segments)) return null;
6471
- const name = readOptionalString(raw.name) ?? (0, import_node_path4.basename)(taskDir);
6157
+ const name = readOptionalString(raw.name) ?? (0, import_node_path3.basename)(taskDir);
6472
6158
  const description = readOptionalString(raw.description) ?? readOptionalString(raw.reason) ?? name;
6473
- const createdAt = readOptionalString(raw.createdAt) ?? (0, import_node_fs5.statSync)(metaPath).birthtime.toISOString();
6159
+ const createdAt = readOptionalString(raw.createdAt) ?? (0, import_node_fs4.statSync)(metaPath).birthtime.toISOString();
6474
6160
  const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
6475
6161
  const repeatTimes = normalizeRepeatTimes({
6476
6162
  repeat: raw.repeat,
@@ -6492,7 +6178,7 @@ function readMeta(taskDir) {
6492
6178
  }
6493
6179
  }
6494
6180
  function writeMeta(taskDir, meta) {
6495
- (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
6181
+ (0, import_node_fs4.writeFileSync)((0, import_node_path3.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
6496
6182
  }
6497
6183
  function generateLightRuleReadme(name, description, segments, repeatTimes) {
6498
6184
  const segmentsJson = JSON.stringify(segments, null, 2);
@@ -6524,22 +6210,22 @@ reason: "${description}"
6524
6210
  }
6525
6211
  function listLightRules(ctx) {
6526
6212
  const dir = tasksDir(ctx);
6527
- if (!(0, import_node_fs5.existsSync)(dir)) return [];
6213
+ if (!(0, import_node_fs4.existsSync)(dir)) return [];
6528
6214
  const rules = [];
6529
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
6215
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
6530
6216
  if (!entry.isDirectory()) continue;
6531
- const meta = readMeta((0, import_node_path4.join)(dir, entry.name));
6217
+ const meta = readMeta((0, import_node_path3.join)(dir, entry.name));
6532
6218
  if (meta) rules.push(meta);
6533
6219
  }
6534
6220
  return rules;
6535
6221
  }
6536
6222
  function createLightRule(ctx, params) {
6537
6223
  const dir = tasksDir(ctx);
6538
- const taskDir = (0, import_node_path4.join)(dir, params.name);
6539
- if ((0, import_node_fs5.existsSync)(taskDir)) {
6224
+ const taskDir = (0, import_node_path3.join)(dir, params.name);
6225
+ if ((0, import_node_fs4.existsSync)(taskDir)) {
6540
6226
  throw new LightRuleError("ALREADY_EXISTS", `\u706F\u6548\u89C4\u5219 '${params.name}' \u5DF2\u5B58\u5728`);
6541
6227
  }
6542
- (0, import_node_fs5.mkdirSync)(taskDir, { recursive: true });
6228
+ (0, import_node_fs4.mkdirSync)(taskDir, { recursive: true });
6543
6229
  const repeatTimes = normalizeRepeatTimes({
6544
6230
  repeat: params.repeat,
6545
6231
  repeat_times: params.repeat_times
@@ -6559,13 +6245,13 @@ function createLightRule(ctx, params) {
6559
6245
  legacyAssignMatchRules(meta, effectiveMatchRules);
6560
6246
  legacyAssignCronSchedule(meta, effectiveCronSchedule);
6561
6247
  writeMeta(taskDir, meta);
6562
- (0, import_node_fs5.writeFileSync)(
6563
- (0, import_node_path4.join)(taskDir, "fetch.py"),
6248
+ (0, import_node_fs4.writeFileSync)(
6249
+ (0, import_node_path3.join)(taskDir, "fetch.py"),
6564
6250
  generateFetchPy(params.name, effectiveMatchRules),
6565
6251
  "utf-8"
6566
6252
  );
6567
- (0, import_node_fs5.writeFileSync)(
6568
- (0, import_node_path4.join)(taskDir, "README.md"),
6253
+ (0, import_node_fs4.writeFileSync)(
6254
+ (0, import_node_path3.join)(taskDir, "README.md"),
6569
6255
  generateLightRuleReadme(params.name, params.description, params.segments, repeatTimes),
6570
6256
  "utf-8"
6571
6257
  );
@@ -6623,15 +6309,15 @@ function updateLightRule(ctx, params) {
6623
6309
  meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
6624
6310
  writeMeta(taskDir, meta);
6625
6311
  if (regenerateFetch) {
6626
- (0, import_node_fs5.writeFileSync)(
6627
- (0, import_node_path4.join)(taskDir, "fetch.py"),
6312
+ (0, import_node_fs4.writeFileSync)(
6313
+ (0, import_node_path3.join)(taskDir, "fetch.py"),
6628
6314
  generateFetchPy(meta.name, legacyReadMetaMatchRules(meta)),
6629
6315
  "utf-8"
6630
6316
  );
6631
6317
  }
6632
6318
  if (regenerateReadme) {
6633
- (0, import_node_fs5.writeFileSync)(
6634
- (0, import_node_path4.join)(taskDir, "README.md"),
6319
+ (0, import_node_fs4.writeFileSync)(
6320
+ (0, import_node_path3.join)(taskDir, "README.md"),
6635
6321
  generateLightRuleReadme(meta.name, meta.description, meta.segments, meta.repeat_times),
6636
6322
  "utf-8"
6637
6323
  );
@@ -6652,7 +6338,7 @@ function deleteLightRule(ctx, name) {
6652
6338
  if (!taskDir || !meta) {
6653
6339
  throw new LightRuleError("NOT_FOUND", `\u706F\u6548\u89C4\u5219 '${name}' \u4E0D\u5B58\u5728`);
6654
6340
  }
6655
- (0, import_node_fs5.rmSync)(taskDir, { recursive: true, force: true });
6341
+ (0, import_node_fs4.rmSync)(taskDir, { recursive: true, force: true });
6656
6342
  return {
6657
6343
  name: meta.name,
6658
6344
  cronHint: {
@@ -7409,168 +7095,141 @@ function registerLightRulesTools(api, registry, logger) {
7409
7095
  }, ["lightrules_delete"]);
7410
7096
  }
7411
7097
 
7412
- // src/light-rules/evaluator-job.ts
7413
- var EVALUATOR_JOB_ID = "light-rules-evaluator";
7414
- var EVALUATOR_SUBAGENT_SESSION_KEY = EVALUATOR_JOB_ID;
7415
- var FALLBACK_CRON_EXPR = "0 0 1 1 *";
7416
- function buildEvaluatorJobMessage(notificationsDir) {
7417
- return `\u706F\u6548\u89C4\u5219\u8BC4\u4F30\u4EFB\u52A1\u3002
7098
+ // src/light-rules/inline-evaluator.ts
7099
+ init_credentials();
7418
7100
 
7419
- \u6267\u884C\u6B65\u9AA4\uFF1A
7420
- 1. \u8BFB\u53D6 tasks/light-rules-evaluator/checkpoint.json\uFF08\u8BB0\u5F55\u4E0A\u6B21\u5904\u7406\u8FDB\u5EA6\uFF09
7421
- 2. \u626B\u63CF ${notificationsDir} \u76EE\u5F55\uFF0C\u83B7\u53D6 checkpoint \u4E4B\u540E\u7684\u65B0\u901A\u77E5
7422
- 3. \u626B\u63CF tasks/ \u76EE\u5F55\uFF0C\u8BFB\u53D6\u6240\u6709 type=light-rule \u4E14 enabled=true \u7684 meta.json
7423
- 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
7424
- 5. \u547D\u4E2D\u65F6\uFF1A\u4EE5\u8BE5\u89C4\u5219\u7684 segments \u548C repeat_times \u8C03\u7528 light_control \u5DE5\u5177
7425
- 6. \u66F4\u65B0 checkpoint.json\uFF0C\u8BB0\u5F55\u5DF2\u5904\u7406\u5230\u7684\u6700\u65B0\u901A\u77E5\u4F4D\u7F6E
7426
- 7. \u82E5\u65E0\u65B0\u901A\u77E5\u6216\u65E0 enabled \u89C4\u5219\uFF1A\u8F93\u51FA NO_CHANGE\uFF0C\u76F4\u63A5\u7ED3\u675F`;
7101
+ // src/light/sender.ts
7102
+ var import_node_crypto = require("crypto");
7103
+ init_env();
7104
+ async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
7105
+ const apiUrl = getEnvUrls().lightApiUrl;
7106
+ const appKey = "7Q617S1G5WD274JI";
7107
+ const templateId = "1990771146010017788";
7108
+ logger?.info(
7109
+ `Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "\u2026" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "\u2026" : "EMPTY"}, segments=${JSON.stringify(segments)}`
7110
+ );
7111
+ if (!apiUrl || !appKey || !templateId) {
7112
+ return {
7113
+ ok: false,
7114
+ error: "\u706F\u6548 API \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF LIGHT_API_URL / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
7115
+ };
7116
+ }
7117
+ let bizContent;
7118
+ try {
7119
+ bizContent = buildLightEffectApnsBody(segments, repeatInput);
7120
+ } catch (error) {
7121
+ return { ok: false, error: error?.message ?? String(error) };
7122
+ }
7123
+ const bizUniqueId = (0, import_node_crypto.randomUUID)();
7124
+ const requestBody = {
7125
+ appKey,
7126
+ bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", reason },
7127
+ bizUniqueId,
7128
+ paramsMap: { bizContent },
7129
+ pushType: "SPECIFY_PUSH",
7130
+ templateId
7131
+ };
7132
+ logger?.info(
7133
+ `Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`
7134
+ );
7135
+ const res = await fetch(apiUrl, {
7136
+ method: "POST",
7137
+ headers: {
7138
+ "Content-Type": "application/json",
7139
+ "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
7140
+ },
7141
+ body: JSON.stringify(requestBody)
7142
+ });
7143
+ const resBody = await res.text();
7144
+ if (!res.ok) {
7145
+ logger?.warn(
7146
+ `Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`
7147
+ );
7148
+ return { ok: false, status: res.status, error: resBody };
7149
+ }
7150
+ logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
7151
+ return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
7427
7152
  }
7428
- var LightRulesEvaluatorJob = class {
7153
+
7154
+ // src/light-rules/inline-evaluator.ts
7155
+ var InlineLightRuleEvaluator = class {
7429
7156
  logger;
7430
7157
  registry;
7431
- subagentRunner;
7432
- getNotificationsDir;
7433
- /**
7434
- * 记录本进程生命周期内 job 是否已确认存在。
7435
- * 仅在 `ensureJobExists` 成功后置 true,避免每次 push 都做检查。
7436
- */
7437
- jobEnsured = false;
7438
- /**
7439
- * 首次创建 job 时的并发保护。
7440
- * 避免冷启动瞬间多条通知并发到达时重复调用 `cron.add`。
7441
- */
7442
- ensureJobPromise = null;
7443
- /**
7444
- * subagent fallback 路径的并发保护。
7445
- * 若评估 session 已在运行中,跳过本次触发(checkpoint 保证下次补处理)。
7446
- */
7447
- subagentInFlight = false;
7158
+ invoker;
7448
7159
  constructor(deps) {
7449
7160
  this.logger = deps.logger;
7450
7161
  this.registry = deps.registry;
7451
- this.subagentRunner = deps.subagentRunner;
7452
- this.getNotificationsDir = deps.getNotificationsDir ?? (() => void 0);
7162
+ this.invoker = deps.invoker;
7453
7163
  }
7454
7164
  /**
7455
- * 通知落盘后调用。若有新增通知且存在 enabled 规则,则触发评估。
7456
- *
7457
- * 两条路径:
7458
- * - cron 不为 null:enqueueRun("force") 入队(gateway context 路径,正常路径)
7459
- * - cron 为 null:通过 subagentRunner 直接运行(HTTP Relay 路径,fallback)
7165
+ * 评估一批新通知。
7460
7166
  *
7461
- * @param cron 来自 gateway context CronService;HTTP 路径下为 null
7462
- * @param insertedCount 本次 ingest 新落盘的通知条数(StoredNotification 去重后)
7167
+ * @returns true 表示评估流程正常结束;false 表示 invoker 失败,调用方可选择回退。
7463
7168
  */
7464
- async triggerIfNeeded(cron, insertedCount) {
7465
- if (insertedCount === 0) return;
7466
- if (this.registry.getEnabled().length === 0) return;
7467
- if (!cron) {
7468
- await this.triggerViaSubagent();
7469
- return;
7169
+ async evaluate(notifications) {
7170
+ if (notifications.length === 0) return true;
7171
+ const rules = this.registry.getEnabled();
7172
+ if (rules.length === 0) return true;
7173
+ const matches = await this.invoker.matchNotifications(notifications, rules);
7174
+ if (matches === null) return false;
7175
+ if (matches.length === 0) {
7176
+ this.logger.info(
7177
+ `lightrules: 0 matches (notifications=${notifications.length}, rules=${rules.length})`
7178
+ );
7179
+ return true;
7180
+ }
7181
+ const firedRules = /* @__PURE__ */ new Set();
7182
+ for (const match of matches) {
7183
+ if (firedRules.has(match.ruleName)) continue;
7184
+ const rule = this.registry.get(match.ruleName);
7185
+ if (!rule) {
7186
+ this.logger.warn(
7187
+ `lightrules: matched rule '${match.ruleName}' not found in registry`
7188
+ );
7189
+ continue;
7190
+ }
7191
+ firedRules.add(match.ruleName);
7192
+ await this.triggerLight(rule);
7470
7193
  }
7194
+ this.logger.info(
7195
+ `lightrules: fired ${firedRules.size} rule(s): ${[...firedRules].join(", ")} (from ${matches.length} match(es) across ${notifications.length} notification(s))`
7196
+ );
7197
+ return true;
7198
+ }
7199
+ async triggerLight(rule) {
7200
+ let apiKey;
7471
7201
  try {
7472
- await this.ensureJobExists(cron);
7202
+ apiKey = requireApiKey();
7473
7203
  } catch (err2) {
7474
- this.logger.warn(`light-rules-evaluator: job ensure failed: ${err2?.message ?? err2}`);
7204
+ this.logger.warn(
7205
+ `lightrules: trigger '${rule.name}' skipped - no API key: ${err2?.message ?? err2}`
7206
+ );
7475
7207
  return;
7476
7208
  }
7477
7209
  try {
7478
- const result = await cron.enqueueRun(EVALUATOR_JOB_ID, "force");
7210
+ const result = await sendLightEffect(
7211
+ apiKey,
7212
+ rule.segments,
7213
+ this.logger,
7214
+ { repeat_times: rule.repeat_times },
7215
+ rule.description
7216
+ );
7479
7217
  if (!result.ok) {
7480
- this.logger.warn("light-rules-evaluator: enqueueRun returned ok=false");
7481
- return;
7482
- }
7483
- if ("enqueued" in result && result.enqueued) {
7484
- this.logger.info(`light-rules-evaluator: enqueued runId=${result.runId}`);
7485
- } else if ("reason" in result) {
7486
- this.logger.info(`light-rules-evaluator: enqueueRun skipped (${result.reason})`);
7218
+ this.logger.warn(
7219
+ `lightrules: trigger '${rule.name}' http-failed (${result.status ?? "?"}): ${result.error}`
7220
+ );
7487
7221
  }
7488
7222
  } catch (err2) {
7489
- this.logger.warn(`light-rules-evaluator: enqueueRun failed: ${err2?.message ?? err2}`);
7490
- }
7491
- }
7492
- /**
7493
- * cron service 不可用时的 fallback:直接通过 subagentRunner 运行评估 session。
7494
- *
7495
- * 并发保护:若上一次 subagent 运行尚未完成,本次跳过。
7496
- * checkpoint 保证即使本次跳过,下次触发时会处理所有积压通知。
7497
- */
7498
- async triggerViaSubagent() {
7499
- if (!this.subagentRunner) {
7500
7223
  this.logger.warn(
7501
- "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"
7224
+ `lightrules: trigger '${rule.name}' exception: ${err2?.message ?? err2}`
7502
7225
  );
7503
- return;
7504
- }
7505
- if (this.subagentInFlight) {
7506
- this.logger.info("light-rules-evaluator: subagent run in-flight, skipping this trigger");
7507
- return;
7508
- }
7509
- const notificationsDir = this.getNotificationsDir();
7510
- if (!notificationsDir) {
7511
- this.logger.warn("light-rules-evaluator: notifications dir not ready, skipping subagent trigger");
7512
- return;
7513
- }
7514
- this.subagentInFlight = true;
7515
- try {
7516
- const result = await this.subagentRunner.run({
7517
- sessionKey: EVALUATOR_SUBAGENT_SESSION_KEY,
7518
- message: buildEvaluatorJobMessage(notificationsDir),
7519
- deliver: false,
7520
- idempotencyKey: `${EVALUATOR_SUBAGENT_SESSION_KEY}-${Date.now()}`
7521
- });
7522
- this.logger.info(`light-rules-evaluator: subagent triggered runId=${result.runId}`);
7523
- } catch (err2) {
7524
- this.logger.warn(`light-rules-evaluator: subagent trigger failed: ${err2?.message ?? err2}`);
7525
- } finally {
7526
- this.subagentInFlight = false;
7527
7226
  }
7528
7227
  }
7529
- /**
7530
- * 按需创建 `light-rules-evaluator` job。
7531
- * 若 job 已存在(内存缓存或 cron store),直接返回;否则调用 `cron.add`。
7532
- */
7533
- async ensureJobExists(cron) {
7534
- if (this.jobEnsured) return;
7535
- if (!this.ensureJobPromise) {
7536
- this.ensureJobPromise = this.createJobIfNeeded(cron).finally(() => {
7537
- this.ensureJobPromise = null;
7538
- });
7539
- }
7540
- await this.ensureJobPromise;
7541
- }
7542
- async createJobIfNeeded(cron) {
7543
- if (cron.getJob(EVALUATOR_JOB_ID)) {
7544
- this.jobEnsured = true;
7545
- return;
7546
- }
7547
- try {
7548
- await cron.add({
7549
- id: EVALUATOR_JOB_ID,
7550
- name: "\u706F\u6548\u89C4\u5219\u8BC4\u4F30",
7551
- 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",
7552
- enabled: true,
7553
- schedule: { kind: "cron", expr: FALLBACK_CRON_EXPR },
7554
- sessionTarget: "isolated",
7555
- wakeMode: "now",
7556
- payload: {
7557
- kind: "agentTurn",
7558
- message: buildEvaluatorJobMessage(this.getNotificationsDir() ?? "notifications")
7559
- }
7560
- });
7561
- this.logger.info("light-rules-evaluator: job created");
7562
- } catch (err2) {
7563
- if (!cron.getJob(EVALUATOR_JOB_ID)) {
7564
- throw err2;
7565
- }
7566
- }
7567
- this.jobEnsured = true;
7568
- }
7569
7228
  };
7570
7229
 
7571
7230
  // src/light-rules/migration.ts
7572
- var import_node_fs6 = require("fs");
7573
- var import_node_path5 = require("path");
7231
+ var import_node_fs8 = require("fs");
7232
+ var import_node_path7 = require("path");
7574
7233
  var NO_MATCH_FETCH_PY = `#!/usr/bin/env python3
7575
7234
  # \u6B64\u6587\u4EF6\u7531\u8FC1\u79FB\u5DE5\u5177\u751F\u6210\u3002
7576
7235
  # \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
@@ -7580,32 +7239,32 @@ function normalizeScriptText(text) {
7580
7239
  return text.replace(/\r\n/g, "\n").trim();
7581
7240
  }
7582
7241
  function resolveTasksDir(ctx) {
7583
- if (ctx.workspaceDir) return (0, import_node_path5.join)(ctx.workspaceDir, "tasks");
7242
+ if (ctx.workspaceDir) return (0, import_node_path7.join)(ctx.workspaceDir, "tasks");
7584
7243
  if (ctx.stateDir) {
7585
- const inferredWorkspaceDir = (0, import_node_path5.join)(ctx.stateDir, "workspace");
7586
- if ((0, import_node_fs6.existsSync)(inferredWorkspaceDir)) return (0, import_node_path5.join)(inferredWorkspaceDir, "tasks");
7587
- return (0, import_node_path5.join)(ctx.stateDir, "tasks");
7244
+ const inferredWorkspaceDir = (0, import_node_path7.join)(ctx.stateDir, "workspace");
7245
+ if ((0, import_node_fs8.existsSync)(inferredWorkspaceDir)) return (0, import_node_path7.join)(inferredWorkspaceDir, "tasks");
7246
+ return (0, import_node_path7.join)(ctx.stateDir, "tasks");
7588
7247
  }
7589
7248
  return null;
7590
7249
  }
7591
7250
  function migrateLegacyLightRuleTasks(ctx, logger) {
7592
7251
  const tasksDir3 = resolveTasksDir(ctx);
7593
- if (!tasksDir3 || !(0, import_node_fs6.existsSync)(tasksDir3)) return;
7252
+ if (!tasksDir3 || !(0, import_node_fs8.existsSync)(tasksDir3)) return;
7594
7253
  try {
7595
- for (const entry of (0, import_node_fs6.readdirSync)(tasksDir3, { withFileTypes: true })) {
7254
+ for (const entry of (0, import_node_fs8.readdirSync)(tasksDir3, { withFileTypes: true })) {
7596
7255
  if (!entry.isDirectory()) continue;
7597
- migrateTaskDir((0, import_node_path5.join)(tasksDir3, String(entry.name)), logger);
7256
+ migrateTaskDir((0, import_node_path7.join)(tasksDir3, String(entry.name)), logger);
7598
7257
  }
7599
7258
  } catch (err2) {
7600
7259
  logger.warn(`migration: failed to read tasks dir: ${err2?.message}`);
7601
7260
  }
7602
7261
  }
7603
7262
  function migrateTaskDir(taskDir, logger) {
7604
- const metaPath = (0, import_node_path5.join)(taskDir, "meta.json");
7605
- if (!(0, import_node_fs6.existsSync)(metaPath)) return;
7263
+ const metaPath = (0, import_node_path7.join)(taskDir, "meta.json");
7264
+ if (!(0, import_node_fs8.existsSync)(metaPath)) return;
7606
7265
  let meta;
7607
7266
  try {
7608
- meta = JSON.parse((0, import_node_fs6.readFileSync)(metaPath, "utf-8"));
7267
+ meta = JSON.parse((0, import_node_fs8.readFileSync)(metaPath, "utf-8"));
7609
7268
  } catch {
7610
7269
  return;
7611
7270
  }
@@ -7614,7 +7273,7 @@ function migrateTaskDir(taskDir, logger) {
7614
7273
  mergeMatchRulesIntoDescription(meta, name, metaPath, logger);
7615
7274
  replaceFetchPy(taskDir, name, logger);
7616
7275
  for (const filename of ["README.md", "checkpoint.json"]) {
7617
- removeFile((0, import_node_path5.join)(taskDir, filename), name, filename, logger);
7276
+ removeFile((0, import_node_path7.join)(taskDir, filename), name, filename, logger);
7618
7277
  }
7619
7278
  }
7620
7279
  function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
@@ -7635,36 +7294,185 @@ function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
7635
7294
  }
7636
7295
  delete meta.matchRules;
7637
7296
  try {
7638
- (0, import_node_fs6.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7297
+ (0, import_node_fs8.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7639
7298
  logger.info(`migration: merged matchRules into description for light rule: ${name}`);
7640
7299
  } catch (err2) {
7641
7300
  logger.warn(`migration: failed to update meta.json for ${name}: ${err2?.message}`);
7642
7301
  }
7643
7302
  }
7644
7303
  function replaceFetchPy(taskDir, name, logger) {
7645
- const fetchPyPath = (0, import_node_path5.join)(taskDir, "fetch.py");
7646
- if (!(0, import_node_fs6.existsSync)(fetchPyPath)) return;
7304
+ const fetchPyPath = (0, import_node_path7.join)(taskDir, "fetch.py");
7305
+ if (!(0, import_node_fs8.existsSync)(fetchPyPath)) return;
7647
7306
  try {
7648
- const existing = (0, import_node_fs6.readFileSync)(fetchPyPath, "utf-8");
7307
+ const existing = (0, import_node_fs8.readFileSync)(fetchPyPath, "utf-8");
7649
7308
  if (normalizeScriptText(existing) === normalizeScriptText(NO_MATCH_FETCH_PY)) {
7650
7309
  return;
7651
7310
  }
7652
- (0, import_node_fs6.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7311
+ (0, import_node_fs8.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7653
7312
  logger.info(`migration: replaced fetch.py with NO_MATCH placeholder for ${name}`);
7654
7313
  } catch (err2) {
7655
7314
  logger.warn(`migration: failed to replace fetch.py for ${name}: ${err2?.message}`);
7656
7315
  }
7657
7316
  }
7658
7317
  function removeFile(filePath, ruleName, filename, logger) {
7659
- if (!(0, import_node_fs6.existsSync)(filePath)) return;
7318
+ if (!(0, import_node_fs8.existsSync)(filePath)) return;
7660
7319
  try {
7661
- (0, import_node_fs6.rmSync)(filePath);
7320
+ (0, import_node_fs8.rmSync)(filePath);
7662
7321
  logger.info(`migration: removed ${filename} for light rule: ${ruleName}`);
7663
7322
  } catch (err2) {
7664
7323
  logger.warn(`migration: failed to remove ${filename} for ${ruleName}: ${err2?.message}`);
7665
7324
  }
7666
7325
  }
7667
7326
 
7327
+ // src/light-rules/pi-invoker.ts
7328
+ var import_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
7329
+ var DEFAULT_PROVIDER = "anthropic";
7330
+ var DEFAULT_MODEL_ID = "claude-haiku-4-5-20251001";
7331
+ var DEFAULT_TIMEOUT_MS = 1e4;
7332
+ var MAX_OUTPUT_TOKENS = 512;
7333
+ var PiAiInvoker = class {
7334
+ constructor(api, logger, options = {}) {
7335
+ this.api = api;
7336
+ this.logger = logger;
7337
+ this.options = options;
7338
+ }
7339
+ /**
7340
+ * 对一批通知和当前启用的规则做一次 LLM 匹配。
7341
+ *
7342
+ * 返回:
7343
+ * - `LlmMatchResult[]`:匹配成功(可能为空数组,即 0 命中)
7344
+ * - `null`:调用失败(模型准备失败 / 网络 / 解析)。调用方据此决定是否回退。
7345
+ */
7346
+ async matchNotifications(notifications, rules) {
7347
+ if (notifications.length === 0 || rules.length === 0) return [];
7348
+ const provider = this.options.provider ?? DEFAULT_PROVIDER;
7349
+ const modelId = this.options.modelId ?? DEFAULT_MODEL_ID;
7350
+ const prepared = await (0, import_agent_runtime.prepareSimpleCompletionModel)({
7351
+ cfg: this.api.config,
7352
+ provider,
7353
+ modelId
7354
+ });
7355
+ if ("error" in prepared) {
7356
+ this.logger.warn(
7357
+ `PiAiInvoker: prepare ${provider}/${modelId} failed: ${prepared.error}`
7358
+ );
7359
+ return null;
7360
+ }
7361
+ const systemPrompt = buildSystemPrompt(rules);
7362
+ const userMessage = buildUserMessage(notifications);
7363
+ try {
7364
+ const resp = await (0, import_agent_runtime.completeWithPreparedSimpleCompletionModel)({
7365
+ model: prepared.model,
7366
+ auth: prepared.auth,
7367
+ context: {
7368
+ systemPrompt,
7369
+ messages: [
7370
+ { role: "user", content: userMessage, timestamp: Date.now() }
7371
+ ]
7372
+ },
7373
+ options: {
7374
+ temperature: 0,
7375
+ maxTokens: MAX_OUTPUT_TOKENS,
7376
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
7377
+ cacheRetention: "short"
7378
+ }
7379
+ });
7380
+ if (resp.stopReason === "error" || resp.stopReason === "aborted") {
7381
+ this.logger.warn(
7382
+ `PiAiInvoker: complete stopped with ${resp.stopReason}: ${resp.errorMessage ?? "n/a"}`
7383
+ );
7384
+ return null;
7385
+ }
7386
+ const text = extractText(resp.content);
7387
+ return parseMatchResult(text, notifications.length, rules);
7388
+ } catch (err2) {
7389
+ this.logger.warn(`PiAiInvoker: complete failed: ${err2?.message ?? err2}`);
7390
+ return null;
7391
+ }
7392
+ }
7393
+ };
7394
+ function extractText(content) {
7395
+ if (!Array.isArray(content)) return "";
7396
+ const parts = [];
7397
+ for (const c of content) {
7398
+ if (c && typeof c === "object" && c.type === "text") {
7399
+ const t = c.text;
7400
+ if (typeof t === "string") parts.push(t);
7401
+ }
7402
+ }
7403
+ return parts.join("");
7404
+ }
7405
+ function buildSystemPrompt(rules) {
7406
+ const lines = [
7407
+ '\u4F60\u662F"\u624B\u673A\u901A\u77E5 \u2192 \u706F\u6548\u89C4\u5219"\u547D\u4E2D\u5339\u914D\u52A9\u624B\u3002',
7408
+ "",
7409
+ "\u5F53\u524D\u542F\u7528\u7684\u706F\u6548\u89C4\u5219\uFF08\u6309\u521B\u5EFA\u65F6\u95F4\u5012\u5E8F\uFF09\uFF1A",
7410
+ ""
7411
+ ];
7412
+ const sorted = [...rules].sort(
7413
+ (a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")
7414
+ );
7415
+ sorted.forEach((rule, idx) => {
7416
+ lines.push(`[${idx + 1}] name: ${rule.name}`);
7417
+ lines.push(` description: ${rule.description}`);
7418
+ lines.push("");
7419
+ });
7420
+ lines.push(
7421
+ "\u4EFB\u52A1\uFF1A\u5BF9\u7528\u6237\u63A5\u4E0B\u6765\u53D1\u6765\u7684\u6BCF\u6761\u901A\u77E5\uFF0C\u5224\u65AD\u5B83\u547D\u4E2D\u4E86\u54EA\u4E9B\u89C4\u5219\u3002",
7422
+ "",
7423
+ "\u8F93\u51FA\u5FC5\u987B\u662F\u7EAF JSON\uFF08\u4E0D\u8981 Markdown \u4EE3\u7801\u5757\u3001\u4E0D\u8981\u4EFB\u4F55\u89E3\u91CA\u6587\u5B57\uFF09\uFF1A",
7424
+ '{"matches":[{"i":<\u901A\u77E5\u4E0B\u6807>,"rule":"<\u89C4\u5219name>"}]}',
7425
+ "",
7426
+ '- \u547D\u4E2D 0 \u6761 \u2192 \u8F93\u51FA {"matches":[]}',
7427
+ "- \u4E00\u6761\u901A\u77E5\u53EF\u80FD\u547D\u4E2D\u591A\u6761\u89C4\u5219 \u2192 \u6BCF\u4E2A\u7EC4\u5408\u4E00\u4E2A\u6761\u76EE",
7428
+ '- \u4E25\u683C\u6309\u89C4\u5219 description \u5224\u65AD\uFF0C\u62FF\u4E0D\u51C6\u65F6\u503E\u5411\u4E8E"\u4E0D\u89E6\u53D1"',
7429
+ "- rule \u5FC5\u987B\u7CBE\u786E\u7B49\u4E8E\u4E0A\u9762\u5217\u51FA\u7684 name \u5B57\u7B26\u4E32"
7430
+ );
7431
+ return lines.join("\n");
7432
+ }
7433
+ function buildUserMessage(notifications) {
7434
+ const lines = [
7435
+ `\u4EE5\u4E0B ${notifications.length} \u6761\u65B0\u624B\u673A\u901A\u77E5\uFF0C\u6309\u987A\u5E8F\u7F16\u53F7\uFF1A`,
7436
+ ""
7437
+ ];
7438
+ notifications.forEach((n, i) => {
7439
+ const app = n.appDisplayName ?? n.appName;
7440
+ lines.push(
7441
+ `[${i}] app=${app} time=${n.timestamp}`,
7442
+ ` title: ${n.title}`,
7443
+ ` content: ${n.content}`,
7444
+ ""
7445
+ );
7446
+ });
7447
+ lines.push("\u8BF7\u5224\u65AD\u6BCF\u6761\u901A\u77E5\u547D\u4E2D\u4E86\u54EA\u4E9B\u89C4\u5219\uFF0C\u6309\u7EA6\u5B9A\u683C\u5F0F\u8F93\u51FA JSON\u3002");
7448
+ return lines.join("\n");
7449
+ }
7450
+ function parseMatchResult(rawText, notificationCount, rules) {
7451
+ if (!rawText) return [];
7452
+ const text = rawText.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
7453
+ let parsed;
7454
+ try {
7455
+ parsed = JSON.parse(text);
7456
+ } catch {
7457
+ return [];
7458
+ }
7459
+ if (!parsed || typeof parsed !== "object") return [];
7460
+ const raw = parsed.matches;
7461
+ if (!Array.isArray(raw)) return [];
7462
+ const validRuleNames = new Set(rules.map((r) => r.name));
7463
+ const results = [];
7464
+ for (const m of raw) {
7465
+ if (!m || typeof m !== "object") continue;
7466
+ const entry = m;
7467
+ const i = typeof entry.i === "number" ? entry.i : typeof entry.index === "number" ? entry.index : -1;
7468
+ const ruleName = typeof entry.rule === "string" ? entry.rule : null;
7469
+ if (!Number.isInteger(i) || i < 0 || i >= notificationCount) continue;
7470
+ if (!ruleName || !validRuleNames.has(ruleName)) continue;
7471
+ results.push({ notificationIndex: i, ruleName });
7472
+ }
7473
+ return results;
7474
+ }
7475
+
7668
7476
  // src/plugin/auto-update.ts
7669
7477
  init_env();
7670
7478
 
@@ -7680,7 +7488,7 @@ function resolveUpdateChannel(params) {
7680
7488
  }
7681
7489
 
7682
7490
  // src/update/index.ts
7683
- var import_node_path10 = require("path");
7491
+ var import_node_path9 = require("path");
7684
7492
 
7685
7493
  // src/update/checker.ts
7686
7494
  function parseSemver(v) {
@@ -7782,8 +7590,8 @@ var UpdateChecker = class {
7782
7590
  };
7783
7591
 
7784
7592
  // src/update/executor.ts
7785
- var import_node_fs10 = require("fs");
7786
- var import_node_path9 = require("path");
7593
+ var import_node_fs9 = require("fs");
7594
+ var import_node_path8 = require("path");
7787
7595
  var import_node_os = require("os");
7788
7596
  var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
7789
7597
  var BASE_URL = "https://artifact.yoooclaw.com/plugin";
@@ -7793,9 +7601,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7793
7601
  }
7794
7602
  const tgzUrl = `${BASE_URL}/v${version}/yoooclaw-phone-notifications-${version}.tgz`;
7795
7603
  logger.info(`\u6267\u884C\u66F4\u65B0: ${tgzUrl} \u2192 ${targetDir}`);
7796
- const workDir = (0, import_node_fs10.mkdtempSync)((0, import_node_path9.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
7797
- const tgzPath = (0, import_node_path9.join)(workDir, "plugin.tgz");
7798
- const stagingDir = (0, import_node_path9.join)(workDir, "staged");
7604
+ const workDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path8.join)((0, import_node_os.tmpdir)(), ".openclaw-plugin-update-"));
7605
+ const tgzPath = (0, import_node_path8.join)(workDir, "plugin.tgz");
7606
+ const stagingDir = (0, import_node_path8.join)(workDir, "staged");
7799
7607
  let backupDir = null;
7800
7608
  try {
7801
7609
  logger.info("\u4E0B\u8F7D\u63D2\u4EF6\u5305...");
@@ -7804,9 +7612,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7804
7612
  return { success: false, message: `\u4E0B\u8F7D\u5931\u8D25 (HTTP ${response.status}): ${tgzUrl}` };
7805
7613
  }
7806
7614
  const buffer = Buffer.from(await response.arrayBuffer());
7807
- (0, import_node_fs10.writeFileSync)(tgzPath, buffer);
7615
+ (0, import_node_fs9.writeFileSync)(tgzPath, buffer);
7808
7616
  logger.info(`\u4E0B\u8F7D\u5B8C\u6210 (${buffer.length} bytes)`);
7809
- (0, import_node_fs10.mkdirSync)(stagingDir, { recursive: true });
7617
+ (0, import_node_fs9.mkdirSync)(stagingDir, { recursive: true });
7810
7618
  const tarResult = await runCommand(
7811
7619
  ["tar", "-xzf", tgzPath, "-C", stagingDir, "--strip-components=1"],
7812
7620
  { timeoutMs: 3e4 }
@@ -7815,14 +7623,14 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7815
7623
  const err2 = tarResult.stderr || tarResult.stdout || "unknown error";
7816
7624
  return { success: false, message: `\u89E3\u538B\u5931\u8D25: ${err2}` };
7817
7625
  }
7818
- (0, import_node_fs10.mkdirSync)((0, import_node_path9.dirname)(targetDir), { recursive: true });
7626
+ (0, import_node_fs9.mkdirSync)((0, import_node_path8.dirname)(targetDir), { recursive: true });
7819
7627
  try {
7820
7628
  backupDir = `${targetDir}.bak.${Date.now()}`;
7821
- (0, import_node_fs10.renameSync)(targetDir, backupDir);
7629
+ (0, import_node_fs9.renameSync)(targetDir, backupDir);
7822
7630
  } catch {
7823
7631
  backupDir = null;
7824
7632
  }
7825
- (0, import_node_fs10.renameSync)(stagingDir, targetDir);
7633
+ (0, import_node_fs9.renameSync)(stagingDir, targetDir);
7826
7634
  try {
7827
7635
  await updateConfigRecord2(version, tgzUrl);
7828
7636
  } catch (err2) {
@@ -7830,7 +7638,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7830
7638
  }
7831
7639
  if (backupDir) {
7832
7640
  try {
7833
- (0, import_node_fs10.rmSync)(backupDir, { force: true, recursive: true });
7641
+ (0, import_node_fs9.rmSync)(backupDir, { force: true, recursive: true });
7834
7642
  } catch {
7835
7643
  }
7836
7644
  }
@@ -7840,8 +7648,8 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7840
7648
  } catch (err2) {
7841
7649
  if (backupDir) {
7842
7650
  try {
7843
- (0, import_node_fs10.rmSync)(targetDir, { force: true, recursive: true });
7844
- (0, import_node_fs10.renameSync)(backupDir, targetDir);
7651
+ (0, import_node_fs9.rmSync)(targetDir, { force: true, recursive: true });
7652
+ (0, import_node_fs9.renameSync)(backupDir, targetDir);
7845
7653
  logger.info("\u5DF2\u56DE\u6EDA\u5230\u4E4B\u524D\u7248\u672C");
7846
7654
  } catch (rollbackErr) {
7847
7655
  logger.error(`\u56DE\u6EDA\u5931\u8D25: ${String(rollbackErr)}`);
@@ -7852,7 +7660,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, updateConfi
7852
7660
  return { success: false, message: errMsg };
7853
7661
  } finally {
7854
7662
  try {
7855
- (0, import_node_fs10.rmSync)(workDir, { force: true, recursive: true });
7663
+ (0, import_node_fs9.rmSync)(workDir, { force: true, recursive: true });
7856
7664
  } catch {
7857
7665
  }
7858
7666
  }
@@ -7867,7 +7675,7 @@ function resolveTargetDir(api) {
7867
7675
  if (installPath) return installPath;
7868
7676
  } catch {
7869
7677
  }
7870
- return (0, import_node_path10.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
7678
+ return (0, import_node_path9.join)(api.runtime.state.resolveStateDir(), "extensions", PLUGIN_ID);
7871
7679
  }
7872
7680
  async function updateConfigRecord(api, version, targetDir, tgzUrl) {
7873
7681
  const configApi = api.runtime.config;
@@ -8055,10 +7863,10 @@ function registerAutoUpdateLifecycle(deps) {
8055
7863
  }
8056
7864
 
8057
7865
  // src/plugin/cli.ts
8058
- var import_node_path18 = require("path");
7866
+ var import_node_path17 = require("path");
8059
7867
 
8060
7868
  // src/cli/auth.ts
8061
- var import_node_fs11 = require("fs");
7869
+ var import_node_fs10 = require("fs");
8062
7870
  init_credentials();
8063
7871
  function registerAuthCli(program) {
8064
7872
  const auth = program.command("auth").description("\u7528\u6237\u8BA4\u8BC1\u7BA1\u7406");
@@ -8094,12 +7902,12 @@ function registerAuthCli(program) {
8094
7902
  });
8095
7903
  auth.command("clear").description("\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u8BA4\u8BC1\u4FE1\u606F").action(() => {
8096
7904
  const path2 = credentialsPath();
8097
- if ((0, import_node_fs11.existsSync)(path2)) {
7905
+ if ((0, import_node_fs10.existsSync)(path2)) {
8098
7906
  const creds = readCredentials();
8099
7907
  delete creds.apiKey;
8100
7908
  delete creds.token;
8101
7909
  if (Object.keys(creds).length === 0) {
8102
- (0, import_node_fs11.rmSync)(path2, { force: true });
7910
+ (0, import_node_fs10.rmSync)(path2, { force: true });
8103
7911
  } else {
8104
7912
  writeCredentials(creds);
8105
7913
  }
@@ -8231,23 +8039,23 @@ function registerNtfStats(ntf, ctx) {
8231
8039
  }
8232
8040
 
8233
8041
  // src/cli/ntf-sync.ts
8234
- var import_node_fs12 = require("fs");
8235
- var import_node_path11 = require("path");
8042
+ var import_node_fs11 = require("fs");
8043
+ var import_node_path10 = require("path");
8236
8044
  var SYNC_FETCH_LIMIT = 300;
8237
8045
  function checkpointPath(dir) {
8238
- return (0, import_node_path11.join)(dir, ".checkpoint.json");
8046
+ return (0, import_node_path10.join)(dir, ".checkpoint.json");
8239
8047
  }
8240
8048
  function readCheckpoint(dir) {
8241
8049
  const p = checkpointPath(dir);
8242
- if (!(0, import_node_fs12.existsSync)(p)) return {};
8050
+ if (!(0, import_node_fs11.existsSync)(p)) return {};
8243
8051
  try {
8244
- return JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
8052
+ return JSON.parse((0, import_node_fs11.readFileSync)(p, "utf-8"));
8245
8053
  } catch {
8246
8054
  return {};
8247
8055
  }
8248
8056
  }
8249
8057
  function writeCheckpoint(dir, data) {
8250
- (0, import_node_fs12.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8058
+ (0, import_node_fs11.writeFileSync)(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
8251
8059
  }
8252
8060
  function registerNtfSync(ntf, ctx) {
8253
8061
  const sync = ntf.command("sync").description("\u540C\u6B65\u901A\u77E5\u5230\u8BB0\u5FC6\u7CFB\u7EDF");
@@ -8340,24 +8148,24 @@ function registerNtfSync(ntf, ctx) {
8340
8148
  }
8341
8149
 
8342
8150
  // src/cli/ntf-monitor.ts
8343
- var import_node_fs13 = require("fs");
8344
- var import_node_path12 = require("path");
8151
+ var import_node_fs12 = require("fs");
8152
+ var import_node_path11 = require("path");
8345
8153
  function tasksDir2(ctx) {
8346
8154
  const base = ctx.workspaceDir || ctx.stateDir;
8347
8155
  if (!base) throw new Error("workspaceDir and stateDir both unavailable");
8348
- return (0, import_node_path12.join)(base, "tasks");
8156
+ return (0, import_node_path11.join)(base, "tasks");
8349
8157
  }
8350
8158
  function readMeta2(taskDir) {
8351
- const metaPath = (0, import_node_path12.join)(taskDir, "meta.json");
8352
- if (!(0, import_node_fs13.existsSync)(metaPath)) return null;
8159
+ const metaPath = (0, import_node_path11.join)(taskDir, "meta.json");
8160
+ if (!(0, import_node_fs12.existsSync)(metaPath)) return null;
8353
8161
  try {
8354
- return JSON.parse((0, import_node_fs13.readFileSync)(metaPath, "utf-8"));
8162
+ return JSON.parse((0, import_node_fs12.readFileSync)(metaPath, "utf-8"));
8355
8163
  } catch {
8356
8164
  return null;
8357
8165
  }
8358
8166
  }
8359
8167
  function writeMeta2(taskDir, meta) {
8360
- (0, import_node_fs13.writeFileSync)((0, import_node_path12.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
8168
+ (0, import_node_fs12.writeFileSync)((0, import_node_path11.join)(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
8361
8169
  }
8362
8170
  function generateReadme(name, description) {
8363
8171
  return `# Monitor Task: ${name}
@@ -8376,27 +8184,27 @@ function registerNtfMonitor(ntf, ctx) {
8376
8184
  const monitor = ntf.command("monitor").description("\u901A\u77E5\u76D1\u63A7\u4EFB\u52A1\u7BA1\u7406");
8377
8185
  monitor.command("list").description("\u5217\u51FA\u6240\u6709\u76D1\u63A7\u4EFB\u52A1").action(() => {
8378
8186
  const dir = tasksDir2(ctx);
8379
- if (!(0, import_node_fs13.existsSync)(dir)) {
8187
+ if (!(0, import_node_fs12.existsSync)(dir)) {
8380
8188
  output({ ok: true, tasks: [] });
8381
8189
  return;
8382
8190
  }
8383
8191
  const tasks = [];
8384
- for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
8192
+ for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
8385
8193
  if (!entry.isDirectory()) continue;
8386
- const meta = readMeta2((0, import_node_path12.join)(dir, entry.name));
8194
+ const meta = readMeta2((0, import_node_path11.join)(dir, entry.name));
8387
8195
  if (meta) tasks.push(meta);
8388
8196
  }
8389
8197
  output({ ok: true, tasks });
8390
8198
  });
8391
8199
  monitor.command("show <name>").description("\u67E5\u770B\u76D1\u63A7\u4EFB\u52A1\u8BE6\u60C5").action((name) => {
8392
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8200
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8393
8201
  const meta = readMeta2(taskDir);
8394
8202
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8395
- const checkpointPath2 = (0, import_node_path12.join)(taskDir, "checkpoint.json");
8203
+ const checkpointPath2 = (0, import_node_path11.join)(taskDir, "checkpoint.json");
8396
8204
  let checkpoint = {};
8397
- if ((0, import_node_fs13.existsSync)(checkpointPath2)) {
8205
+ if ((0, import_node_fs12.existsSync)(checkpointPath2)) {
8398
8206
  try {
8399
- checkpoint = JSON.parse((0, import_node_fs13.readFileSync)(checkpointPath2, "utf-8"));
8207
+ checkpoint = JSON.parse((0, import_node_fs12.readFileSync)(checkpointPath2, "utf-8"));
8400
8208
  } catch {
8401
8209
  }
8402
8210
  }
@@ -8411,8 +8219,8 @@ function registerNtfMonitor(ntf, ctx) {
8411
8219
  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(
8412
8220
  (name, opts) => {
8413
8221
  const dir = tasksDir2(ctx);
8414
- const taskDir = (0, import_node_path12.join)(dir, name);
8415
- if ((0, import_node_fs13.existsSync)(taskDir)) {
8222
+ const taskDir = (0, import_node_path11.join)(dir, name);
8223
+ if ((0, import_node_fs12.existsSync)(taskDir)) {
8416
8224
  exitError("ALREADY_EXISTS", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u5DF2\u5B58\u5728`);
8417
8225
  }
8418
8226
  let matchRules;
@@ -8424,7 +8232,7 @@ function registerNtfMonitor(ntf, ctx) {
8424
8232
  "match-rules \u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON"
8425
8233
  );
8426
8234
  }
8427
- (0, import_node_fs13.mkdirSync)(taskDir, { recursive: true });
8235
+ (0, import_node_fs12.mkdirSync)(taskDir, { recursive: true });
8428
8236
  const meta = {
8429
8237
  name,
8430
8238
  description: opts.description,
@@ -8434,13 +8242,13 @@ function registerNtfMonitor(ntf, ctx) {
8434
8242
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8435
8243
  };
8436
8244
  writeMeta2(taskDir, meta);
8437
- (0, import_node_fs13.writeFileSync)(
8438
- (0, import_node_path12.join)(taskDir, "fetch.py"),
8245
+ (0, import_node_fs12.writeFileSync)(
8246
+ (0, import_node_path11.join)(taskDir, "fetch.py"),
8439
8247
  generateFetchPy(name, matchRules),
8440
8248
  "utf-8"
8441
8249
  );
8442
- (0, import_node_fs13.writeFileSync)(
8443
- (0, import_node_path12.join)(taskDir, "README.md"),
8250
+ (0, import_node_fs12.writeFileSync)(
8251
+ (0, import_node_path11.join)(taskDir, "README.md"),
8444
8252
  generateReadme(name, opts.description),
8445
8253
  "utf-8"
8446
8254
  );
@@ -8468,8 +8276,8 @@ function registerNtfMonitor(ntf, ctx) {
8468
8276
  }
8469
8277
  );
8470
8278
  monitor.command("delete <name>").description("\u5220\u9664\u76D1\u63A7\u4EFB\u52A1").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").action((name, opts) => {
8471
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8472
- if (!(0, import_node_fs13.existsSync)(taskDir)) {
8279
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8280
+ if (!(0, import_node_fs12.existsSync)(taskDir)) {
8473
8281
  exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8474
8282
  }
8475
8283
  if (!opts.yes) {
@@ -8482,7 +8290,7 @@ function registerNtfMonitor(ntf, ctx) {
8482
8290
  });
8483
8291
  process.exit(1);
8484
8292
  }
8485
- (0, import_node_fs13.rmSync)(taskDir, { recursive: true, force: true });
8293
+ (0, import_node_fs12.rmSync)(taskDir, { recursive: true, force: true });
8486
8294
  output({
8487
8295
  ok: true,
8488
8296
  name,
@@ -8494,7 +8302,7 @@ function registerNtfMonitor(ntf, ctx) {
8494
8302
  });
8495
8303
  });
8496
8304
  monitor.command("enable <name>").description("\u542F\u7528\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8497
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8305
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8498
8306
  const meta = readMeta2(taskDir);
8499
8307
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8500
8308
  meta.enabled = true;
@@ -8502,7 +8310,7 @@ function registerNtfMonitor(ntf, ctx) {
8502
8310
  output({ ok: true, name, enabled: true });
8503
8311
  });
8504
8312
  monitor.command("disable <name>").description("\u6682\u505C\u76D1\u63A7\u4EFB\u52A1").action((name) => {
8505
- const taskDir = (0, import_node_path12.join)(tasksDir2(ctx), name);
8313
+ const taskDir = (0, import_node_path11.join)(tasksDir2(ctx), name);
8506
8314
  const meta = readMeta2(taskDir);
8507
8315
  if (!meta) exitError("NOT_FOUND", `\u76D1\u63A7\u4EFB\u52A1 '${name}' \u4E0D\u5B58\u5728`);
8508
8316
  meta.enabled = false;
@@ -8513,61 +8321,6 @@ function registerNtfMonitor(ntf, ctx) {
8513
8321
 
8514
8322
  // src/cli/light-send.ts
8515
8323
  init_credentials();
8516
-
8517
- // src/light/sender.ts
8518
- var import_node_crypto2 = require("crypto");
8519
- init_env();
8520
- async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
8521
- const apiUrl = getEnvUrls().lightApiUrl;
8522
- const appKey = "7Q617S1G5WD274JI";
8523
- const templateId = "1990771146010017788";
8524
- logger?.info(
8525
- `Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "\u2026" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "\u2026" : "EMPTY"}, segments=${JSON.stringify(segments)}`
8526
- );
8527
- if (!apiUrl || !appKey || !templateId) {
8528
- return {
8529
- ok: false,
8530
- error: "\u706F\u6548 API \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF LIGHT_API_URL / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
8531
- };
8532
- }
8533
- let bizContent;
8534
- try {
8535
- bizContent = buildLightEffectApnsBody(segments, repeatInput);
8536
- } catch (error) {
8537
- return { ok: false, error: error?.message ?? String(error) };
8538
- }
8539
- const bizUniqueId = (0, import_node_crypto2.randomUUID)();
8540
- const requestBody = {
8541
- appKey,
8542
- bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", reason },
8543
- bizUniqueId,
8544
- paramsMap: { bizContent },
8545
- pushType: "SPECIFY_PUSH",
8546
- templateId
8547
- };
8548
- logger?.info(
8549
- `Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`
8550
- );
8551
- const res = await fetch(apiUrl, {
8552
- method: "POST",
8553
- headers: {
8554
- "Content-Type": "application/json",
8555
- "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
8556
- },
8557
- body: JSON.stringify(requestBody)
8558
- });
8559
- const resBody = await res.text();
8560
- if (!res.ok) {
8561
- logger?.warn(
8562
- `Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`
8563
- );
8564
- return { ok: false, status: res.status, error: resBody };
8565
- }
8566
- logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
8567
- return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
8568
- }
8569
-
8570
- // src/cli/light-send.ts
8571
8324
  function registerLightSend(light) {
8572
8325
  light.command("send").description("\u53D1\u9001\u706F\u6548\u6307\u4EE4\u5230\u786C\u4EF6\u8BBE\u5907").requiredOption("--segments <json>", "\u706F\u6548\u53C2\u6570 JSON").option("--repeat", "\u65E0\u9650\u5FAA\u73AF\u64AD\u653E\uFF08\u9ED8\u8BA4\u4EC5\u64AD\u653E\u4E00\u8F6E\uFF09").option("--repeat-times <n>", "\u6574\u6761\u7EC4\u5408\u91CD\u590D\u6B21\u6570\uFF1A0=\u65E0\u9650\uFF0C1=\u4E00\u8F6E\uFF1B\u5F53\u524D ANCS \u8DEF\u5F84\u4E0D\u652F\u6301 >=2").action(async (opts) => {
8573
8326
  let apiKey;
@@ -8601,9 +8354,9 @@ function registerLightSend(light) {
8601
8354
  }
8602
8355
 
8603
8356
  // src/cli/light-setup-tools.ts
8604
- var import_node_fs14 = require("fs");
8357
+ var import_node_fs13 = require("fs");
8605
8358
  var import_node_os2 = require("os");
8606
- var import_node_path13 = require("path");
8359
+ var import_node_path12 = require("path");
8607
8360
  function isObject(value) {
8608
8361
  return !!value && typeof value === "object" && !Array.isArray(value);
8609
8362
  }
@@ -8617,7 +8370,7 @@ function ensureArray(obj, key) {
8617
8370
  function resolveConfigPath2() {
8618
8371
  const fromEnv = process.env.OPENCLAW_CONFIG_PATH?.trim();
8619
8372
  if (fromEnv) return fromEnv;
8620
- return (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8373
+ return (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".openclaw", "openclaw.json");
8621
8374
  }
8622
8375
  var LIGHT_TOOLS = [
8623
8376
  "light_control",
@@ -8664,12 +8417,12 @@ function upsertLightControlAlsoAllow(cfg) {
8664
8417
  function registerLightSetupTools(light) {
8665
8418
  light.command("setup").description("\u81EA\u52A8\u653E\u884C light_control\uFF08\u5199\u5165 tools.alsoAllow \u4E0E agents.main.tools.alsoAllow\uFF09").action(() => {
8666
8419
  const configPath = resolveConfigPath2();
8667
- if (!(0, import_node_fs14.existsSync)(configPath)) {
8420
+ if (!(0, import_node_fs13.existsSync)(configPath)) {
8668
8421
  exitError("CONFIG_NOT_FOUND", `\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6: ${configPath}`);
8669
8422
  }
8670
8423
  let cfg = {};
8671
8424
  try {
8672
- const raw = (0, import_node_fs14.readFileSync)(configPath, "utf-8");
8425
+ const raw = (0, import_node_fs13.readFileSync)(configPath, "utf-8");
8673
8426
  const parsed = JSON.parse(raw);
8674
8427
  if (isObject(parsed)) cfg = parsed;
8675
8428
  } catch (err2) {
@@ -8677,8 +8430,8 @@ function registerLightSetupTools(light) {
8677
8430
  }
8678
8431
  const result = upsertLightControlAlsoAllow(cfg);
8679
8432
  try {
8680
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(configPath), { recursive: true });
8681
- (0, import_node_fs14.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8433
+ (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
8434
+ (0, import_node_fs13.writeFileSync)(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
8682
8435
  } catch (err2) {
8683
8436
  exitError("WRITE_FAILED", `\u5199\u5165\u914D\u7F6E\u5931\u8D25: ${err2?.message ?? String(err2)}`);
8684
8437
  }
@@ -8696,17 +8449,17 @@ function registerLightSetupTools(light) {
8696
8449
  }
8697
8450
 
8698
8451
  // src/cli/tunnel-status.ts
8699
- var import_node_fs15 = require("fs");
8700
- var import_node_path14 = require("path");
8452
+ var import_node_fs14 = require("fs");
8453
+ var import_node_path13 = require("path");
8701
8454
  init_credentials();
8702
8455
  init_env();
8703
- var STATUS_REL_PATH = (0, import_node_path14.join)("plugins", "phone-notifications", "tunnel-status.json");
8456
+ var STATUS_REL_PATH = (0, import_node_path13.join)("plugins", "phone-notifications", "tunnel-status.json");
8704
8457
  function readTunnelStatus(ctx) {
8705
8458
  if (!ctx.stateDir) return null;
8706
- const filePath = (0, import_node_path14.join)(ctx.stateDir, STATUS_REL_PATH);
8707
- if (!(0, import_node_fs15.existsSync)(filePath)) return null;
8459
+ const filePath = (0, import_node_path13.join)(ctx.stateDir, STATUS_REL_PATH);
8460
+ if (!(0, import_node_fs14.existsSync)(filePath)) return null;
8708
8461
  try {
8709
- return JSON.parse((0, import_node_fs15.readFileSync)(filePath, "utf-8"));
8462
+ return JSON.parse((0, import_node_fs14.readFileSync)(filePath, "utf-8"));
8710
8463
  } catch {
8711
8464
  return null;
8712
8465
  }
@@ -8774,24 +8527,24 @@ function registerNtfStoragePath(ntf, ctx) {
8774
8527
  }
8775
8528
 
8776
8529
  // src/cli/log-search.ts
8777
- var import_node_fs16 = require("fs");
8778
- var import_node_path15 = require("path");
8530
+ var import_node_fs15 = require("fs");
8531
+ var import_node_path14 = require("path");
8779
8532
  function resolveLogsDir(ctx) {
8780
8533
  if (ctx.stateDir) {
8781
- const dir = (0, import_node_path15.join)(
8534
+ const dir = (0, import_node_path14.join)(
8782
8535
  ctx.stateDir,
8783
8536
  "plugins",
8784
8537
  "phone-notifications",
8785
8538
  "logs"
8786
8539
  );
8787
- if ((0, import_node_fs16.existsSync)(dir)) return dir;
8540
+ if ((0, import_node_fs15.existsSync)(dir)) return dir;
8788
8541
  }
8789
8542
  return null;
8790
8543
  }
8791
8544
  function listLogDateKeys(dir) {
8792
8545
  const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
8793
8546
  const keys = [];
8794
- for (const entry of (0, import_node_fs16.readdirSync)(dir, { withFileTypes: true })) {
8547
+ for (const entry of (0, import_node_fs15.readdirSync)(dir, { withFileTypes: true })) {
8795
8548
  if (!entry.isFile()) continue;
8796
8549
  const m = pattern.exec(entry.name);
8797
8550
  if (m) keys.push(m[1]);
@@ -8799,9 +8552,9 @@ function listLogDateKeys(dir) {
8799
8552
  return keys.sort().reverse();
8800
8553
  }
8801
8554
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
8802
- const filePath = (0, import_node_path15.join)(dir, `${dateKey}.log`);
8803
- if (!(0, import_node_fs16.existsSync)(filePath)) return;
8804
- const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
8555
+ const filePath = (0, import_node_path14.join)(dir, `${dateKey}.log`);
8556
+ if (!(0, import_node_fs15.existsSync)(filePath)) return;
8557
+ const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
8805
8558
  const lowerKeyword = keyword?.toLowerCase();
8806
8559
  for (const line of content.split("\n")) {
8807
8560
  if (collected.length >= limit) return;
@@ -8868,12 +8621,12 @@ function registerEnvCli(ntf) {
8868
8621
  }
8869
8622
 
8870
8623
  // src/cli/doctor.ts
8871
- var import_node_fs20 = require("fs");
8624
+ var import_node_fs19 = require("fs");
8872
8625
  var import_node_readline = require("readline");
8873
8626
  init_host();
8874
8627
 
8875
8628
  // src/cli/doctor/check-dangerous-flags.ts
8876
- var import_node_fs17 = require("fs");
8629
+ var import_node_fs16 = require("fs");
8877
8630
  function isObject2(v) {
8878
8631
  return !!v && typeof v === "object" && !Array.isArray(v);
8879
8632
  }
@@ -8890,13 +8643,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
8890
8643
  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",
8891
8644
  fixDescription: "\u8BBE\u4E3A false",
8892
8645
  fix: () => {
8893
- const raw = (0, import_node_fs17.readFileSync)(configPath, "utf-8");
8646
+ const raw = (0, import_node_fs16.readFileSync)(configPath, "utf-8");
8894
8647
  const config = JSON.parse(raw);
8895
8648
  const gw = config.gateway;
8896
8649
  const cui = gw.controlUi;
8897
8650
  cui.dangerouslyDisableDeviceAuth = false;
8898
- (0, import_node_fs17.copyFileSync)(configPath, configPath + ".bak");
8899
- (0, import_node_fs17.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
8651
+ (0, import_node_fs16.copyFileSync)(configPath, configPath + ".bak");
8652
+ (0, import_node_fs16.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
8900
8653
  }
8901
8654
  };
8902
8655
  };
@@ -8944,11 +8697,11 @@ function warnEmpty() {
8944
8697
  }
8945
8698
 
8946
8699
  // src/cli/doctor/check-state-dir-perms.ts
8947
- var import_node_fs18 = require("fs");
8700
+ var import_node_fs17 = require("fs");
8948
8701
  var checkStateDirPerms = ({ stateDir }) => {
8949
8702
  let mode;
8950
8703
  try {
8951
- mode = (0, import_node_fs18.statSync)(stateDir).mode;
8704
+ mode = (0, import_node_fs17.statSync)(stateDir).mode;
8952
8705
  } catch {
8953
8706
  return null;
8954
8707
  }
@@ -8962,7 +8715,7 @@ var checkStateDirPerms = ({ stateDir }) => {
8962
8715
  detail: "\u5176\u4ED6\u7528\u6237\u53EF\u4EE5\u8BFB\u53D6\u8BE5\u76EE\u5F55\u4E0B\u7684\u51ED\u8BC1\u548C\u914D\u7F6E\u6587\u4EF6\u3002",
8963
8716
  fixDescription: "chmod 700 " + stateDir,
8964
8717
  fix: () => {
8965
- (0, import_node_fs18.chmodSync)(stateDir, 448);
8718
+ (0, import_node_fs17.chmodSync)(stateDir, 448);
8966
8719
  }
8967
8720
  };
8968
8721
  };
@@ -9017,16 +8770,16 @@ var checkCredentials = () => {
9017
8770
  };
9018
8771
 
9019
8772
  // src/cli/doctor/check-tunnel.ts
9020
- var import_node_fs19 = require("fs");
9021
- var import_node_path16 = require("path");
9022
- var STATUS_REL_PATH2 = (0, import_node_path16.join)(
8773
+ var import_node_fs18 = require("fs");
8774
+ var import_node_path15 = require("path");
8775
+ var STATUS_REL_PATH2 = (0, import_node_path15.join)(
9023
8776
  "plugins",
9024
8777
  "phone-notifications",
9025
8778
  "tunnel-status.json"
9026
8779
  );
9027
8780
  var checkTunnel = ({ stateDir }) => {
9028
- const filePath = (0, import_node_path16.join)(stateDir, STATUS_REL_PATH2);
9029
- if (!(0, import_node_fs19.existsSync)(filePath)) {
8781
+ const filePath = (0, import_node_path15.join)(stateDir, STATUS_REL_PATH2);
8782
+ if (!(0, import_node_fs18.existsSync)(filePath)) {
9030
8783
  return {
9031
8784
  id: "tunnel",
9032
8785
  severity: "warn",
@@ -9038,7 +8791,7 @@ var checkTunnel = ({ stateDir }) => {
9038
8791
  }
9039
8792
  let status;
9040
8793
  try {
9041
- status = JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
8794
+ status = JSON.parse((0, import_node_fs18.readFileSync)(filePath, "utf-8"));
9042
8795
  } catch {
9043
8796
  return {
9044
8797
  id: "tunnel",
@@ -9131,9 +8884,9 @@ function isObject5(v) {
9131
8884
  return !!v && typeof v === "object" && !Array.isArray(v);
9132
8885
  }
9133
8886
  function readConfig(configPath) {
9134
- if (!(0, import_node_fs20.existsSync)(configPath)) return {};
8887
+ if (!(0, import_node_fs19.existsSync)(configPath)) return {};
9135
8888
  try {
9136
- const parsed = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
8889
+ const parsed = JSON.parse((0, import_node_fs19.readFileSync)(configPath, "utf-8"));
9137
8890
  return isObject5(parsed) ? parsed : {};
9138
8891
  } catch {
9139
8892
  return {};
@@ -9334,7 +9087,7 @@ function registerRecStoragePath(rec, ctx) {
9334
9087
 
9335
9088
  // src/cli/rec-setup.ts
9336
9089
  var import_node_readline2 = require("readline");
9337
- var import_node_fs21 = require("fs");
9090
+ var import_node_fs20 = require("fs");
9338
9091
  function ask(rl, question) {
9339
9092
  return new Promise((resolve) => rl.question(question, resolve));
9340
9093
  }
@@ -9420,9 +9173,9 @@ async function setupLocal(rl) {
9420
9173
  function registerRecSetup(rec, ctx) {
9421
9174
  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 () => {
9422
9175
  const configPath = resolveAsrConfigPath(ctx);
9423
- if ((0, import_node_fs21.existsSync)(configPath)) {
9176
+ if ((0, import_node_fs20.existsSync)(configPath)) {
9424
9177
  try {
9425
- const existing = JSON.parse((0, import_node_fs21.readFileSync)(configPath, "utf-8"));
9178
+ const existing = JSON.parse((0, import_node_fs20.readFileSync)(configPath, "utf-8"));
9426
9179
  process.stderr.write(`\u5F53\u524D\u5DF2\u6709\u914D\u7F6E\uFF1Amode = ${existing.mode}`);
9427
9180
  if (existing.updatedAt) process.stderr.write(`\uFF0C\u66F4\u65B0\u4E8E ${existing.updatedAt}`);
9428
9181
  process.stderr.write("\n");
@@ -9435,7 +9188,7 @@ function registerRecSetup(rec, ctx) {
9435
9188
  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"]);
9436
9189
  const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
9437
9190
  const stored = { ...config, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
9438
- (0, import_node_fs21.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9191
+ (0, import_node_fs20.writeFileSync)(configPath, JSON.stringify(stored, null, 2), "utf-8");
9439
9192
  process.stderr.write(`
9440
9193
  \u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 ${configPath}
9441
9194
 
@@ -9449,8 +9202,8 @@ function registerRecSetup(rec, ctx) {
9449
9202
 
9450
9203
  // src/cli/update.ts
9451
9204
  var import_node_child_process = require("child_process");
9452
- var import_node_fs22 = require("fs");
9453
- var import_node_path17 = require("path");
9205
+ var import_node_fs21 = require("fs");
9206
+ var import_node_path16 = require("path");
9454
9207
  var import_node_os3 = __toESM(require("os"), 1);
9455
9208
  init_host();
9456
9209
  var BASE_URL2 = "https://artifact.yoooclaw.com/plugin";
@@ -9509,9 +9262,9 @@ async function runUpdate(ctx, opts) {
9509
9262
  `);
9510
9263
  process.exit(1);
9511
9264
  }
9512
- const tmpScript = (0, import_node_path17.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9265
+ const tmpScript = (0, import_node_path16.join)(import_node_os3.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
9513
9266
  try {
9514
- (0, import_node_fs22.writeFileSync)(tmpScript, installScript, "utf-8");
9267
+ (0, import_node_fs21.writeFileSync)(tmpScript, installScript, "utf-8");
9515
9268
  } catch (err2) {
9516
9269
  const msg = `\u5199\u5165\u4E34\u65F6\u6587\u4EF6\u5931\u8D25: ${err2?.message ?? String(err2)}`;
9517
9270
  if (json) {
@@ -9529,7 +9282,7 @@ async function runUpdate(ctx, opts) {
9529
9282
  { stdio: "inherit" }
9530
9283
  );
9531
9284
  try {
9532
- (0, import_node_fs22.unlinkSync)(tmpScript);
9285
+ (0, import_node_fs21.unlinkSync)(tmpScript);
9533
9286
  } catch {
9534
9287
  }
9535
9288
  if (result.error) {
@@ -9593,10 +9346,10 @@ function inferOpenClawRootDir(workspaceDir) {
9593
9346
  if (!workspaceDir) {
9594
9347
  return void 0;
9595
9348
  }
9596
- if ((0, import_node_path18.basename)(workspaceDir) !== "workspace") {
9349
+ if ((0, import_node_path17.basename)(workspaceDir) !== "workspace") {
9597
9350
  return void 0;
9598
9351
  }
9599
- return (0, import_node_path18.dirname)(workspaceDir);
9352
+ return (0, import_node_path17.dirname)(workspaceDir);
9600
9353
  }
9601
9354
  function registerPluginCli(api, params) {
9602
9355
  const { logger, openclawDir } = params;
@@ -9818,159 +9571,475 @@ function registerLightControlTool(api, logger) {
9818
9571
  }
9819
9572
  };
9820
9573
  }
9821
- logger.info(`Light control reason: ${reason}`);
9822
- const result = await sendLightEffect(
9823
- apiKey,
9824
- validation.segments,
9825
- logger,
9826
- { repeat_times: repeatTimes },
9827
- reason
9828
- );
9829
- if (!result.ok) {
9830
- logger.warn(
9831
- `Light control HTTP request failed: ${result.status} ${result.error}`
9832
- );
9833
- } else {
9834
- logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
9574
+ logger.info(`Light control reason: ${reason}`);
9575
+ const result = await sendLightEffect(
9576
+ apiKey,
9577
+ validation.segments,
9578
+ logger,
9579
+ { repeat_times: repeatTimes },
9580
+ reason
9581
+ );
9582
+ if (!result.ok) {
9583
+ logger.warn(
9584
+ `Light control HTTP request failed: ${result.status} ${result.error}`
9585
+ );
9586
+ } else {
9587
+ logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
9588
+ }
9589
+ return {
9590
+ content: [{ type: "text", text: JSON.stringify(result) }],
9591
+ details: result
9592
+ };
9593
+ }
9594
+ });
9595
+ }
9596
+
9597
+ // src/plugin/lifecycle.ts
9598
+ var import_node_fs31 = require("fs");
9599
+ init_host();
9600
+
9601
+ // src/notification/app-name-map.ts
9602
+ var import_node_fs22 = require("fs");
9603
+ var import_node_path18 = require("path");
9604
+ init_credentials();
9605
+ init_env();
9606
+ var PLUGIN_STATE_DIR = "phone-notifications";
9607
+ var CACHE_FILE = "app-name-map.json";
9608
+ var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
9609
+ var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
9610
+ var APP_NAME_MAP_REFRESH_HOURS = 12;
9611
+ function isRecordOfStrings(v) {
9612
+ if (v === null || typeof v !== "object") return false;
9613
+ for (const val of Object.values(v)) if (typeof val !== "string") return false;
9614
+ return true;
9615
+ }
9616
+ function isAppNameMapApiResponse(v) {
9617
+ if (v === null || typeof v !== "object") return false;
9618
+ const o = v;
9619
+ return Array.isArray(o.data) && o.data.every(
9620
+ (item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string"
9621
+ );
9622
+ }
9623
+ function getCachePath(stateDir) {
9624
+ return (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
9625
+ }
9626
+ function createAppNameMapProvider(opts) {
9627
+ const { stateDir, logger } = opts;
9628
+ const url = APP_NAME_MAP_URL;
9629
+ const refreshHours = APP_NAME_MAP_REFRESH_HOURS;
9630
+ const map = /* @__PURE__ */ new Map();
9631
+ let refreshTimer = null;
9632
+ let stopWatching = null;
9633
+ let inFlightFetch = null;
9634
+ function loadFromDisk() {
9635
+ const path2 = getCachePath(stateDir);
9636
+ if (!(0, import_node_fs22.existsSync)(path2)) return;
9637
+ try {
9638
+ const raw = JSON.parse((0, import_node_fs22.readFileSync)(path2, "utf-8"));
9639
+ if (!isRecordOfStrings(raw)) return;
9640
+ map.clear();
9641
+ for (const [k, v] of Object.entries(raw)) map.set(k, v);
9642
+ logger.info(`[app-name-map] loaded ${map.size} entries from cache: ${path2}`);
9643
+ } catch {
9644
+ }
9645
+ }
9646
+ async function fetchFromServer() {
9647
+ const apiKey = loadApiKey();
9648
+ if (!url) {
9649
+ logger.warn("[app-name-map] APP_NAME_MAP_URL is empty, skip refresh");
9650
+ return;
9651
+ }
9652
+ if (!apiKey) {
9653
+ logger.info("[app-name-map] api key missing, skip refresh");
9654
+ return;
9655
+ }
9656
+ const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
9657
+ try {
9658
+ const res = await fetch(url, {
9659
+ method: "POST",
9660
+ headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
9661
+ body: JSON.stringify({
9662
+ platform: ""
9663
+ })
9664
+ });
9665
+ if (!res.ok) {
9666
+ logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
9667
+ return;
9668
+ }
9669
+ const body = await res.json();
9670
+ if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
9671
+ logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
9672
+ return;
9673
+ }
9674
+ map.clear();
9675
+ for (const item of body.data) {
9676
+ if (item.packageName && item.appName) map.set(item.packageName, item.appName);
9677
+ }
9678
+ if (map.size === 0) {
9679
+ logger.warn("[app-name-map] refresh succeeded but got 0 entries");
9680
+ return;
9681
+ }
9682
+ const dir = (0, import_node_path18.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
9683
+ (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
9684
+ const cachePath = getCachePath(stateDir);
9685
+ (0, import_node_fs22.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
9686
+ logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
9687
+ } catch (e) {
9688
+ const message = e instanceof Error ? e.message : String(e);
9689
+ logger.warn(`[app-name-map] refresh error: ${message}`);
9690
+ }
9691
+ }
9692
+ async function ensureOneFetch() {
9693
+ if (inFlightFetch) return inFlightFetch;
9694
+ inFlightFetch = fetchFromServer().finally(() => {
9695
+ inFlightFetch = null;
9696
+ });
9697
+ return inFlightFetch;
9698
+ }
9699
+ return {
9700
+ async resolveDisplayName(packageName) {
9701
+ if (map.has(packageName)) return map.get(packageName);
9702
+ return packageName;
9703
+ },
9704
+ async start() {
9705
+ loadFromDisk();
9706
+ await fetchFromServer();
9707
+ if (!url) return;
9708
+ if (refreshHours > 0) {
9709
+ const ms = refreshHours * 60 * 60 * 1e3;
9710
+ refreshTimer = setInterval(() => fetchFromServer().catch(() => {
9711
+ }), ms);
9712
+ }
9713
+ stopWatching = watchCredentials(() => fetchFromServer().catch(() => {
9714
+ }));
9715
+ },
9716
+ stop() {
9717
+ stopWatching?.();
9718
+ stopWatching = null;
9719
+ if (refreshTimer) {
9720
+ clearInterval(refreshTimer);
9721
+ refreshTimer = null;
9722
+ }
9723
+ map.clear();
9724
+ }
9725
+ };
9726
+ }
9727
+
9728
+ // src/notification/storage.ts
9729
+ var import_node_fs23 = require("fs");
9730
+ var import_node_crypto2 = require("crypto");
9731
+ var import_node_path19 = require("path");
9732
+ var NOTIFICATION_DIR_NAME = "notifications";
9733
+ var ID_INDEX_DIR_NAME = ".ids";
9734
+ var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
9735
+ function getStateFallbackNotificationDir(stateDir) {
9736
+ return (0, import_node_path19.join)(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
9737
+ }
9738
+ function ensureWritableDirectory(dir) {
9739
+ try {
9740
+ (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9741
+ (0, import_node_fs23.accessSync)(dir, import_node_fs23.constants.R_OK | import_node_fs23.constants.W_OK);
9742
+ return true;
9743
+ } catch {
9744
+ return false;
9745
+ }
9746
+ }
9747
+ function resolveNotificationStorageDir(ctx, logger) {
9748
+ const stateNotifDir = getStateFallbackNotificationDir(ctx.stateDir);
9749
+ if (ensureWritableDirectory(stateNotifDir)) {
9750
+ logger.info(`\u901A\u77E5\u5C06\u5199\u5165 stateDir \u8DEF\u5F84: ${stateNotifDir}`);
9751
+ return stateNotifDir;
9752
+ }
9753
+ if (ctx.workspaceDir) {
9754
+ const workspaceDir = (0, import_node_path19.join)(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
9755
+ if (ensureWritableDirectory(workspaceDir)) {
9756
+ logger.warn(
9757
+ `stateDir \u4E0D\u53EF\u7528\uFF0C\u901A\u77E5\u5DF2\u56DE\u9000\u5230 workspace \u8DEF\u5F84: ${workspaceDir}`
9758
+ );
9759
+ return workspaceDir;
9760
+ }
9761
+ }
9762
+ throw new Error(`\u901A\u77E5\u5B58\u50A8\u76EE\u5F55\u4E0D\u53EF\u7528: ${stateNotifDir}`);
9763
+ }
9764
+ var NotificationStorage = class {
9765
+ constructor(dir, config, logger, resolveDisplayName) {
9766
+ this.config = config;
9767
+ this.logger = logger;
9768
+ this.dir = dir;
9769
+ this.idIndexDir = (0, import_node_path19.join)(dir, ID_INDEX_DIR_NAME);
9770
+ this.contentKeyIndexDir = (0, import_node_path19.join)(dir, CONTENT_KEY_INDEX_DIR_NAME);
9771
+ this.resolveDisplayName = resolveDisplayName;
9772
+ }
9773
+ dir;
9774
+ idIndexDir;
9775
+ contentKeyIndexDir;
9776
+ idCache = /* @__PURE__ */ new Map();
9777
+ contentKeyCache = /* @__PURE__ */ new Map();
9778
+ dateWriteChains = /* @__PURE__ */ new Map();
9779
+ resolveDisplayName;
9780
+ async init() {
9781
+ (0, import_node_fs23.mkdirSync)(this.dir, { recursive: true });
9782
+ (0, import_node_fs23.mkdirSync)(this.idIndexDir, { recursive: true });
9783
+ (0, import_node_fs23.rmSync)(this.contentKeyIndexDir, { recursive: true, force: true });
9784
+ (0, import_node_fs23.mkdirSync)(this.contentKeyIndexDir, { recursive: true });
9785
+ }
9786
+ async ingest(items) {
9787
+ const result = {
9788
+ received: items.length,
9789
+ ingested: 0,
9790
+ dedupedById: 0,
9791
+ dedupedByContent: 0,
9792
+ invalid: 0,
9793
+ inserted: []
9794
+ };
9795
+ for (const n of items) {
9796
+ const outcome = await this.writeNotification(n);
9797
+ switch (outcome.kind) {
9798
+ case "ingested":
9799
+ result.ingested += 1;
9800
+ result.inserted.push(outcome.entry);
9801
+ break;
9802
+ case "dedupedById":
9803
+ result.dedupedById += 1;
9804
+ break;
9805
+ case "dedupedByContent":
9806
+ result.dedupedByContent += 1;
9807
+ break;
9808
+ case "invalid":
9809
+ result.invalid += 1;
9810
+ break;
9811
+ }
9812
+ }
9813
+ this.prune();
9814
+ return result;
9815
+ }
9816
+ async writeNotification(n) {
9817
+ const ts = new Date(n.timestamp);
9818
+ if (Number.isNaN(ts.getTime())) {
9819
+ this.logger.warn(`\u5FFD\u7565\u975E\u6CD5 timestamp \u7684\u901A\u77E5: ${n.id}`);
9820
+ return { kind: "invalid" };
9821
+ }
9822
+ const dateKey = this.formatDate(ts);
9823
+ const filePath = (0, import_node_path19.join)(this.dir, `${dateKey}.json`);
9824
+ const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
9825
+ const entry = this.buildStoredNotification(n);
9826
+ return this.withDateWriteLock(dateKey, async () => {
9827
+ if (normalizedId && this.hasNotificationId(dateKey, normalizedId)) {
9828
+ return { kind: "dedupedById" };
9829
+ }
9830
+ if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
9831
+ return { kind: "dedupedByContent" };
9832
+ }
9833
+ const appDisplayName = this.resolveDisplayName ? await this.resolveDisplayName(entry.appName) : entry.appName;
9834
+ const storedEntry = {
9835
+ ...entry,
9836
+ appDisplayName
9837
+ };
9838
+ const arr = this.readStoredNotifications(filePath);
9839
+ arr.push(storedEntry);
9840
+ (0, import_node_fs23.writeFileSync)(filePath, JSON.stringify(arr, null, 2), "utf-8");
9841
+ if (normalizedId) {
9842
+ this.recordNotificationId(dateKey, normalizedId);
9843
+ }
9844
+ this.recordNotificationContentKey(dateKey, filePath, storedEntry);
9845
+ return { kind: "ingested", entry: storedEntry };
9846
+ });
9847
+ }
9848
+ buildStoredNotification(n) {
9849
+ return {
9850
+ appName: typeof n.app === "string" && n.app ? n.app : "Unknown",
9851
+ title: typeof n.title === "string" ? n.title : "",
9852
+ content: this.buildContent(n),
9853
+ timestamp: n.timestamp
9854
+ };
9855
+ }
9856
+ buildContent(n) {
9857
+ const body = n.body?.trim();
9858
+ if (body) {
9859
+ return body;
9860
+ }
9861
+ const fallback = [];
9862
+ if (n.category) {
9863
+ fallback.push(`category:${n.category}`);
9864
+ }
9865
+ if (n.metadata && Object.keys(n.metadata).length > 0) {
9866
+ fallback.push(`metadata:${JSON.stringify(n.metadata)}`);
9867
+ }
9868
+ return fallback.join(" ; ") || "-";
9869
+ }
9870
+ formatDate(d) {
9871
+ const year = d.getFullYear();
9872
+ const month = String(d.getMonth() + 1).padStart(2, "0");
9873
+ const day = String(d.getDate()).padStart(2, "0");
9874
+ return `${year}-${month}-${day}`;
9875
+ }
9876
+ getIdIndexPath(dateKey) {
9877
+ return (0, import_node_path19.join)(this.idIndexDir, `${dateKey}.ids`);
9878
+ }
9879
+ getIdSet(dateKey) {
9880
+ const cached = this.idCache.get(dateKey);
9881
+ if (cached) {
9882
+ return cached;
9883
+ }
9884
+ const idPath = this.getIdIndexPath(dateKey);
9885
+ const ids = /* @__PURE__ */ new Set();
9886
+ if ((0, import_node_fs23.existsSync)(idPath)) {
9887
+ const lines = (0, import_node_fs23.readFileSync)(idPath, "utf-8").split(/\r?\n/);
9888
+ for (const line of lines) {
9889
+ const id = line.trim();
9890
+ if (id) {
9891
+ ids.add(id);
9892
+ }
9893
+ }
9894
+ }
9895
+ this.idCache.set(dateKey, ids);
9896
+ return ids;
9897
+ }
9898
+ hasNotificationId(dateKey, id) {
9899
+ return this.getIdSet(dateKey).has(id);
9900
+ }
9901
+ getContentKeyIndexPath(dateKey) {
9902
+ return (0, import_node_path19.join)(this.contentKeyIndexDir, `${dateKey}.keys`);
9903
+ }
9904
+ getContentKeySet(dateKey, filePath) {
9905
+ const cached = this.contentKeyCache.get(dateKey);
9906
+ if (cached) {
9907
+ return cached;
9908
+ }
9909
+ const keyPath = this.getContentKeyIndexPath(dateKey);
9910
+ const keys = /* @__PURE__ */ new Set();
9911
+ if ((0, import_node_fs23.existsSync)(filePath)) {
9912
+ for (const item of this.readStoredNotifications(filePath)) {
9913
+ keys.add(this.buildNotificationContentKey(item));
9835
9914
  }
9836
- return {
9837
- content: [{ type: "text", text: JSON.stringify(result) }],
9838
- details: result
9839
- };
9840
9915
  }
9841
- });
9842
- }
9843
-
9844
- // src/plugin/lifecycle.ts
9845
- var import_node_fs31 = require("fs");
9846
- init_host();
9847
-
9848
- // src/notification/app-name-map.ts
9849
- var import_node_fs23 = require("fs");
9850
- var import_node_path19 = require("path");
9851
- init_credentials();
9852
- init_env();
9853
- var PLUGIN_STATE_DIR = "phone-notifications";
9854
- var CACHE_FILE = "app-name-map.json";
9855
- var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
9856
- var APP_NAME_MAP_URL = ("".trim() ? "".trim() : void 0) ?? BUILTIN_APP_NAME_MAP_URL;
9857
- var APP_NAME_MAP_REFRESH_HOURS = 12;
9858
- function isRecordOfStrings(v) {
9859
- if (v === null || typeof v !== "object") return false;
9860
- for (const val of Object.values(v)) if (typeof val !== "string") return false;
9861
- return true;
9862
- }
9863
- function isAppNameMapApiResponse(v) {
9864
- if (v === null || typeof v !== "object") return false;
9865
- const o = v;
9866
- return Array.isArray(o.data) && o.data.every(
9867
- (item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string"
9868
- );
9869
- }
9870
- function getCachePath(stateDir) {
9871
- return (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
9872
- }
9873
- function createAppNameMapProvider(opts) {
9874
- const { stateDir, logger } = opts;
9875
- const url = APP_NAME_MAP_URL;
9876
- const refreshHours = APP_NAME_MAP_REFRESH_HOURS;
9877
- const map = /* @__PURE__ */ new Map();
9878
- let refreshTimer = null;
9879
- let stopWatching = null;
9880
- let inFlightFetch = null;
9881
- function loadFromDisk() {
9882
- const path2 = getCachePath(stateDir);
9883
- if (!(0, import_node_fs23.existsSync)(path2)) return;
9884
- try {
9885
- const raw = JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf-8"));
9886
- if (!isRecordOfStrings(raw)) return;
9887
- map.clear();
9888
- for (const [k, v] of Object.entries(raw)) map.set(k, v);
9889
- logger.info(`[app-name-map] loaded ${map.size} entries from cache: ${path2}`);
9890
- } catch {
9916
+ if (keys.size > 0) {
9917
+ (0, import_node_fs23.writeFileSync)(keyPath, `${Array.from(keys).join("\n")}
9918
+ `, "utf-8");
9919
+ } else if ((0, import_node_fs23.existsSync)(keyPath)) {
9920
+ (0, import_node_fs23.rmSync)(keyPath, { force: true });
9891
9921
  }
9922
+ this.contentKeyCache.set(dateKey, keys);
9923
+ return keys;
9892
9924
  }
9893
- async function fetchFromServer() {
9894
- const apiKey = loadApiKey();
9895
- if (!url) {
9896
- logger.warn("[app-name-map] APP_NAME_MAP_URL is empty, skip refresh");
9925
+ hasNotificationContentKey(dateKey, filePath, entry) {
9926
+ return this.getContentKeySet(dateKey, filePath).has(
9927
+ this.buildNotificationContentKey(entry)
9928
+ );
9929
+ }
9930
+ recordNotificationId(dateKey, id) {
9931
+ const ids = this.getIdSet(dateKey);
9932
+ if (ids.has(id)) {
9897
9933
  return;
9898
9934
  }
9899
- if (!apiKey) {
9900
- logger.info("[app-name-map] api key missing, skip refresh");
9935
+ (0, import_node_fs23.appendFileSync)(this.getIdIndexPath(dateKey), `${id}
9936
+ `, "utf-8");
9937
+ ids.add(id);
9938
+ }
9939
+ recordNotificationContentKey(dateKey, filePath, entry) {
9940
+ const keys = this.getContentKeySet(dateKey, filePath);
9941
+ const key = this.buildNotificationContentKey(entry);
9942
+ if (keys.has(key)) {
9901
9943
  return;
9902
9944
  }
9903
- const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
9945
+ (0, import_node_fs23.appendFileSync)(this.getContentKeyIndexPath(dateKey), `${key}
9946
+ `, "utf-8");
9947
+ keys.add(key);
9948
+ }
9949
+ buildNotificationContentKey(entry) {
9950
+ return (0, import_node_crypto2.createHash)("sha256").update(entry.appName).update("").update(entry.title).update("").update(entry.content).update("").update(entry.timestamp).digest("hex");
9951
+ }
9952
+ readStoredNotifications(filePath) {
9953
+ if (!(0, import_node_fs23.existsSync)(filePath)) {
9954
+ return [];
9955
+ }
9904
9956
  try {
9905
- const res = await fetch(url, {
9906
- method: "POST",
9907
- headers: { "Content-Type": "application/json", "X-Api-Key-Id": rawApiKey },
9908
- body: JSON.stringify({
9909
- platform: ""
9910
- })
9911
- });
9912
- if (!res.ok) {
9913
- logger.warn(`[app-name-map] refresh failed: HTTP ${res.status} ${res.statusText}`);
9914
- return;
9915
- }
9916
- const body = await res.json();
9917
- if (!isAppNameMapApiResponse(body) || !body.success || !body.data?.length) {
9918
- logger.warn("[app-name-map] refresh failed: unexpected response shape or empty data");
9919
- return;
9920
- }
9921
- map.clear();
9922
- for (const item of body.data) {
9923
- if (item.packageName && item.appName) map.set(item.packageName, item.appName);
9924
- }
9925
- if (map.size === 0) {
9926
- logger.warn("[app-name-map] refresh succeeded but got 0 entries");
9927
- return;
9928
- }
9929
- const dir = (0, import_node_path19.join)(stateDir, "plugins", PLUGIN_STATE_DIR);
9930
- (0, import_node_fs23.mkdirSync)(dir, { recursive: true });
9931
- const cachePath = getCachePath(stateDir);
9932
- (0, import_node_fs23.writeFileSync)(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
9933
- logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
9934
- } catch (e) {
9935
- const message = e instanceof Error ? e.message : String(e);
9936
- logger.warn(`[app-name-map] refresh error: ${message}`);
9957
+ const parsed = JSON.parse((0, import_node_fs23.readFileSync)(filePath, "utf-8"));
9958
+ return Array.isArray(parsed) ? parsed : [];
9959
+ } catch {
9960
+ return [];
9937
9961
  }
9938
9962
  }
9939
- async function ensureOneFetch() {
9940
- if (inFlightFetch) return inFlightFetch;
9941
- inFlightFetch = fetchFromServer().finally(() => {
9942
- inFlightFetch = null;
9963
+ async withDateWriteLock(dateKey, task) {
9964
+ const previous = this.dateWriteChains.get(dateKey) ?? Promise.resolve();
9965
+ let release;
9966
+ const current = new Promise((resolve) => {
9967
+ release = resolve;
9943
9968
  });
9944
- return inFlightFetch;
9969
+ const chain = previous.then(() => current);
9970
+ this.dateWriteChains.set(dateKey, chain);
9971
+ await previous;
9972
+ try {
9973
+ return await task();
9974
+ } finally {
9975
+ release();
9976
+ if (this.dateWriteChains.get(dateKey) === chain) {
9977
+ this.dateWriteChains.delete(dateKey);
9978
+ }
9979
+ }
9945
9980
  }
9946
- return {
9947
- async resolveDisplayName(packageName) {
9948
- if (map.has(packageName)) return map.get(packageName);
9949
- return packageName;
9950
- },
9951
- async start() {
9952
- loadFromDisk();
9953
- await fetchFromServer();
9954
- if (!url) return;
9955
- if (refreshHours > 0) {
9956
- const ms = refreshHours * 60 * 60 * 1e3;
9957
- refreshTimer = setInterval(() => fetchFromServer().catch(() => {
9958
- }), ms);
9981
+ prune() {
9982
+ const retentionDays = this.config.retentionDays;
9983
+ if (retentionDays === void 0) {
9984
+ return;
9985
+ }
9986
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
9987
+ const cutoffDate = this.formatDate(new Date(cutoffMs));
9988
+ this.pruneDataFiles(cutoffDate);
9989
+ this.pruneIdIndex(cutoffDate);
9990
+ this.pruneContentKeyIndex(cutoffDate);
9991
+ }
9992
+ /** Remove expired .json, legacy .md files, and legacy date directories */
9993
+ pruneDataFiles(cutoffDate) {
9994
+ const dateFilePattern = /^(\d{4}-\d{2}-\d{2})\.(json|md)$/;
9995
+ const dateDirPattern = /^\d{4}-\d{2}-\d{2}$/;
9996
+ try {
9997
+ for (const entry of (0, import_node_fs23.readdirSync)(this.dir, { withFileTypes: true })) {
9998
+ if (entry.isFile()) {
9999
+ const match = dateFilePattern.exec(entry.name);
10000
+ if (match && match[1] < cutoffDate) {
10001
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.name), { force: true });
10002
+ }
10003
+ } else if (entry.isDirectory() && dateDirPattern.test(entry.name) && entry.name < cutoffDate) {
10004
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.dir, entry.name), { recursive: true, force: true });
10005
+ }
9959
10006
  }
9960
- stopWatching = watchCredentials(() => fetchFromServer().catch(() => {
9961
- }));
9962
- },
9963
- stop() {
9964
- stopWatching?.();
9965
- stopWatching = null;
9966
- if (refreshTimer) {
9967
- clearInterval(refreshTimer);
9968
- refreshTimer = null;
10007
+ } catch {
10008
+ }
10009
+ }
10010
+ /** Remove expired .ids index files */
10011
+ pruneIdIndex(cutoffDate) {
10012
+ try {
10013
+ for (const entry of (0, import_node_fs23.readdirSync)(this.idIndexDir, { withFileTypes: true })) {
10014
+ if (!entry.isFile()) continue;
10015
+ const match = /^(\d{4}-\d{2}-\d{2})\.ids$/.exec(entry.name);
10016
+ if (match && match[1] < cutoffDate) {
10017
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.idIndexDir, entry.name), { force: true });
10018
+ this.idCache.delete(match[1]);
10019
+ }
9969
10020
  }
9970
- map.clear();
10021
+ } catch {
9971
10022
  }
9972
- };
9973
- }
10023
+ }
10024
+ pruneContentKeyIndex(cutoffDate) {
10025
+ try {
10026
+ for (const entry of (0, import_node_fs23.readdirSync)(this.contentKeyIndexDir, { withFileTypes: true })) {
10027
+ if (!entry.isFile()) continue;
10028
+ const match = /^(\d{4}-\d{2}-\d{2})\.keys$/.exec(entry.name);
10029
+ if (match && match[1] < cutoffDate) {
10030
+ (0, import_node_fs23.rmSync)((0, import_node_path19.join)(this.contentKeyIndexDir, entry.name), { force: true });
10031
+ this.contentKeyCache.delete(match[1]);
10032
+ }
10033
+ }
10034
+ } catch {
10035
+ }
10036
+ }
10037
+ async close() {
10038
+ this.idCache.clear();
10039
+ this.contentKeyCache.clear();
10040
+ this.dateWriteChains.clear();
10041
+ }
10042
+ };
9974
10043
 
9975
10044
  // src/recording/storage.ts
9976
10045
  var import_node_fs24 = require("fs");
@@ -10294,7 +10363,7 @@ var RecordingStorage = class {
10294
10363
  if (entry.transcriptDataFile) {
10295
10364
  const transcriptDoc = this.readRelativeTranscriptDocument(entry.transcriptDataFile);
10296
10365
  const transcriptFromJson = extractTranscriptTextFromDocument(transcriptDoc);
10297
- if (transcriptFromJson !== void 0) {
10366
+ if (transcriptFromJson) {
10298
10367
  return transcriptFromJson;
10299
10368
  }
10300
10369
  }
@@ -10588,11 +10657,11 @@ var import_node_fs25 = require("fs");
10588
10657
  var import_node_path21 = require("path");
10589
10658
  var import_promises2 = require("stream/promises");
10590
10659
  var import_node_stream = require("stream");
10591
- var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
10660
+ var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
10592
10661
  var DEFAULT_MAX_RETRIES = 3;
10593
10662
  var DEFAULT_RETRY_BACKOFF_MS = 2e3;
10594
10663
  async function downloadFile(url, destPath, logger, options) {
10595
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10664
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
10596
10665
  const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
10597
10666
  const retryBackoffMs = options?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
10598
10667
  (0, import_node_fs25.mkdirSync)((0, import_node_path21.dirname)(destPath), { recursive: true });
@@ -10877,7 +10946,7 @@ var import_node_path24 = require("path");
10877
10946
  // node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
10878
10947
  var import_stream = __toESM(require_stream(), 1);
10879
10948
  var import_receiver = __toESM(require_receiver(), 1);
10880
- var import_sender3 = __toESM(require_sender(), 1);
10949
+ var import_sender4 = __toESM(require_sender(), 1);
10881
10950
  var import_websocket = __toESM(require_websocket(), 1);
10882
10951
  var import_websocket_server = __toESM(require_websocket_server(), 1);
10883
10952
  var wrapper_default = import_websocket.default;
@@ -12599,16 +12668,15 @@ function registerNotificationInterfaces(deps) {
12599
12668
  filterNotifications,
12600
12669
  registerGatewayMethod,
12601
12670
  tunnelService,
12602
- onAfterIngest,
12603
- getCronForHttpIngest
12671
+ onAfterIngest
12604
12672
  } = deps;
12605
- function triggerAfterIngest(insertedCount, cron) {
12606
- if (insertedCount <= 0 || !onAfterIngest) return;
12607
- void Promise.resolve().then(() => onAfterIngest(insertedCount, cron)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12673
+ function triggerAfterIngest(inserted) {
12674
+ if (inserted.length === 0 || !onAfterIngest) return;
12675
+ void Promise.resolve().then(() => onAfterIngest(inserted)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12608
12676
  }
12609
12677
  registerGatewayMethod(
12610
12678
  "notifications.push",
12611
- async ({ params, respond, context }) => {
12679
+ async ({ params, respond }) => {
12612
12680
  const storage = getStorage();
12613
12681
  if (!storage) {
12614
12682
  respond(false, null, {
@@ -12628,7 +12696,7 @@ function registerNotificationInterfaces(deps) {
12628
12696
  const filtered = filterNotifications(items);
12629
12697
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12630
12698
  respond(true, toIngestResponse(result));
12631
- triggerAfterIngest(result.inserted.length, context?.cron);
12699
+ triggerAfterIngest(result.inserted);
12632
12700
  }
12633
12701
  );
12634
12702
  api.registerHttpRoute({
@@ -12672,7 +12740,7 @@ function registerNotificationInterfaces(deps) {
12672
12740
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12673
12741
  res.writeHead(200, { "Content-Type": "application/json" });
12674
12742
  res.end(JSON.stringify({ ok: true, ...toIngestResponse(result) }));
12675
- triggerAfterIngest(result.inserted.length, getCronForHttpIngest?.());
12743
+ triggerAfterIngest(result.inserted);
12676
12744
  }
12677
12745
  });
12678
12746
  logger.info("Gateway \u901A\u77E5\u65B9\u6CD5\u5DF2\u6CE8\u518C: notifications.push");
@@ -13143,7 +13211,6 @@ var index_default = {
13143
13211
  let storage = null;
13144
13212
  let recordingStorage = null;
13145
13213
  let broadcastFn = null;
13146
- let cronService = null;
13147
13214
  let autoUpdateLifecycle = null;
13148
13215
  let tunnelService = null;
13149
13216
  const openclawDir = api.runtime.state.resolveStateDir();
@@ -13196,9 +13263,6 @@ var index_default = {
13196
13263
  function registerGatewayMethodWithBroadcastCapture(method, handler) {
13197
13264
  api.registerGatewayMethod(method, async (opts) => {
13198
13265
  cacheBroadcast(opts.context?.broadcast);
13199
- if (opts.context?.cron) {
13200
- cronService = opts.context.cron;
13201
- }
13202
13266
  await deactivateRelayForDirectGatewayRequest(method, opts);
13203
13267
  return handler(opts);
13204
13268
  });
@@ -13216,11 +13280,11 @@ var index_default = {
13216
13280
  broadcastFn("recording.status", event);
13217
13281
  }
13218
13282
  const lightRuleRegistry = new LightRuleRegistry(lightRuleCtx);
13219
- const lightRulesEvaluatorJob = new LightRulesEvaluatorJob({
13283
+ const lightRuleInvoker = new PiAiInvoker(api, logger);
13284
+ const inlineLightRuleEvaluator = new InlineLightRuleEvaluator({
13220
13285
  logger,
13221
13286
  registry: lightRuleRegistry,
13222
- subagentRunner: api.runtime.subagent,
13223
- getNotificationsDir: () => openclawDir ? getStateFallbackNotificationDir(openclawDir) : void 0
13287
+ invoker: lightRuleInvoker
13224
13288
  });
13225
13289
  registerStorageLifecycle({
13226
13290
  api,
@@ -13251,10 +13315,9 @@ var index_default = {
13251
13315
  filterNotifications,
13252
13316
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
13253
13317
  tunnelService,
13254
- onAfterIngest: (insertedCount, cron) => {
13255
- void lightRulesEvaluatorJob.triggerIfNeeded(cron, insertedCount);
13256
- },
13257
- getCronForHttpIngest: () => cronService
13318
+ onAfterIngest: (inserted) => {
13319
+ void inlineLightRuleEvaluator.evaluate(inserted);
13320
+ }
13258
13321
  });
13259
13322
  registerLightControlTool(api, logger);
13260
13323
  registerRecordingInterfaces({