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

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_path5.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_path5.join)(home, ".qclaw", "qclaw.json"),
63
+ (0, import_node_path5.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_fs6.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_fs6.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_path5.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_path5.join)(baseDir, "openclaw.json"),
96
+ (0, import_node_path5.join)(baseDir, "credentials.json"),
97
+ (0, import_node_path5.join)(baseDir, "extensions")
98
+ ].some((candidate) => (0, import_node_fs6.existsSync)(candidate));
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_path5.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_path5.dirname)(meta.configPath);
114
114
  }
115
- return (0, import_node_path6.join)(homeDir(), ".openclaw");
115
+ return (0, import_node_path5.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_path5.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_path5.join)(resolveStateDir(), filename);
130
130
  }
131
- var import_node_fs7, import_node_path6;
131
+ var import_node_fs6, import_node_path5;
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_fs6 = require("fs");
136
+ import_node_path5 = 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_fs7.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_fs7.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_fs7.mkdirSync)((0, import_node_path6.dirname)(path2), { recursive: true, mode: 448 });
156
+ (0, import_node_fs7.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_path6.dirname)(path2);
177
+ const filename = (0, import_node_path6.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_fs7.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_fs7, import_node_path6;
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_fs7 = require("fs");
203
+ import_node_path6 = 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_fs8.mkdirSync)((0, import_node_path7.dirname)(path2), { recursive: true });
212
+ const existing = (0, import_node_fs8.existsSync)(path2) ? (0, import_node_fs8.readFileSync)(path2, "utf-8") : "";
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_fs8.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_fs8.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_fs8.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_fs8, import_node_path7, 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_fs8 = require("fs");
264
+ import_node_path7 = require("path");
265
265
  init_credentials();
266
266
  init_host();
267
267
  ENV_CONFIG = {
@@ -5424,7 +5424,7 @@ function readBuildInjectedVersion() {
5424
5424
  if (false) {
5425
5425
  return void 0;
5426
5426
  }
5427
- const version = "1.10.6".trim();
5427
+ const version = "1.11.0-beta.0".trim();
5428
5428
  return version || void 0;
5429
5429
  }
5430
5430
  function readPluginVersionFromPackageJson() {
@@ -7432,6 +7432,12 @@ var LightRulesEvaluatorJob = class {
7432
7432
  registry;
7433
7433
  subagentRunner;
7434
7434
  getNotificationsDir;
7435
+ /**
7436
+ * 轻量进程内评估器(事件驱动方案的正式路径)。
7437
+ * 配置后:每批通知走一次 pi-ai `complete()`,不再启动 agent session。
7438
+ * 未配置(或调用失败)时:回退 cron / subagent 老路径。
7439
+ */
7440
+ inlineEvaluator;
7435
7441
  /**
7436
7442
  * 记录本进程生命周期内 job 是否已确认存在。
7437
7443
  * 仅在 `ensureJobExists` 成功后置 true,避免每次 push 都做检查。
@@ -7450,22 +7456,40 @@ var LightRulesEvaluatorJob = class {
7450
7456
  constructor(deps) {
7451
7457
  this.logger = deps.logger;
7452
7458
  this.registry = deps.registry;
7459
+ this.inlineEvaluator = deps.inlineEvaluator;
7453
7460
  this.subagentRunner = deps.subagentRunner;
7454
7461
  this.getNotificationsDir = deps.getNotificationsDir ?? (() => void 0);
7455
7462
  }
7456
7463
  /**
7457
7464
  * 通知落盘后调用。若有新增通知且存在 enabled 规则,则触发评估。
7458
7465
  *
7459
- * 两条路径:
7460
- * - cron 不为 null:enqueueRun("force") 入队(gateway context 路径,正常路径)
7461
- * - cron null:通过 subagentRunner 直接运行(HTTP Relay 路径,fallback)
7466
+ * 路径优先级:
7467
+ * 1. inlineEvaluator(配置后的主路径):进程内一次 LLM 调用直接匹配 + 触发灯效
7468
+ * 2. cron.enqueueRun("force"):legacy,gateway context 路径
7469
+ * 3. subagentRunner.run:legacy,HTTP Relay 路径(cron 不可用时)
7462
7470
  *
7463
- * @param cron 来自 gateway context CronService;HTTP 路径下为 null
7464
- * @param insertedCount 本次 ingest 新落盘的通知条数(StoredNotification 去重后)
7471
+ * inline 成功(包括 0 命中)立刻返回,不会再走 legacy 路径。
7472
+ * inline 失败(LLM 错误 / 未配置 provider)才回退 legacy。
7473
+ *
7474
+ * @param cron 来自 gateway context 的 CronService;HTTP 路径下为 null
7475
+ * @param inserted 本次 ingest 实际新落盘的通知(StoredNotification 去重后)
7465
7476
  */
7466
- async triggerIfNeeded(cron, insertedCount) {
7467
- if (insertedCount === 0) return;
7477
+ async triggerIfNeeded(cron, inserted) {
7478
+ if (inserted.length === 0) return;
7468
7479
  if (this.registry.getEnabled().length === 0) return;
7480
+ if (this.inlineEvaluator) {
7481
+ try {
7482
+ const ok2 = await this.inlineEvaluator.evaluate(inserted);
7483
+ if (ok2) return;
7484
+ this.logger.warn(
7485
+ "light-rules-evaluator: inline evaluator failed; falling back to legacy agent session path"
7486
+ );
7487
+ } catch (err2) {
7488
+ this.logger.warn(
7489
+ `light-rules-evaluator: inline evaluator threw: ${err2?.message ?? err2}; falling back`
7490
+ );
7491
+ }
7492
+ }
7469
7493
  if (!cron) {
7470
7494
  await this.triggerViaSubagent();
7471
7495
  return;
@@ -7570,9 +7594,141 @@ var LightRulesEvaluatorJob = class {
7570
7594
  }
7571
7595
  };
7572
7596
 
7597
+ // src/light-rules/inline-evaluator.ts
7598
+ init_credentials();
7599
+
7600
+ // src/light/sender.ts
7601
+ var import_node_crypto2 = require("crypto");
7602
+ init_env();
7603
+ async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
7604
+ const apiUrl = getEnvUrls().lightApiUrl;
7605
+ const appKey = "7Q617S1G5WD274JI";
7606
+ const templateId = "1990771146010017788";
7607
+ logger?.info(
7608
+ `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)}`
7609
+ );
7610
+ if (!apiUrl || !appKey || !templateId) {
7611
+ return {
7612
+ ok: false,
7613
+ error: "\u706F\u6548 API \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF LIGHT_API_URL / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
7614
+ };
7615
+ }
7616
+ let bizContent;
7617
+ try {
7618
+ bizContent = buildLightEffectApnsBody(segments, repeatInput);
7619
+ } catch (error) {
7620
+ return { ok: false, error: error?.message ?? String(error) };
7621
+ }
7622
+ const bizUniqueId = (0, import_node_crypto2.randomUUID)();
7623
+ const requestBody = {
7624
+ appKey,
7625
+ bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", reason },
7626
+ bizUniqueId,
7627
+ paramsMap: { bizContent },
7628
+ pushType: "SPECIFY_PUSH",
7629
+ templateId
7630
+ };
7631
+ logger?.info(
7632
+ `Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`
7633
+ );
7634
+ const res = await fetch(apiUrl, {
7635
+ method: "POST",
7636
+ headers: {
7637
+ "Content-Type": "application/json",
7638
+ "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
7639
+ },
7640
+ body: JSON.stringify(requestBody)
7641
+ });
7642
+ const resBody = await res.text();
7643
+ if (!res.ok) {
7644
+ logger?.warn(
7645
+ `Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`
7646
+ );
7647
+ return { ok: false, status: res.status, error: resBody };
7648
+ }
7649
+ logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
7650
+ return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
7651
+ }
7652
+
7653
+ // src/light-rules/inline-evaluator.ts
7654
+ var InlineLightRuleEvaluator = class {
7655
+ logger;
7656
+ registry;
7657
+ invoker;
7658
+ constructor(deps) {
7659
+ this.logger = deps.logger;
7660
+ this.registry = deps.registry;
7661
+ this.invoker = deps.invoker;
7662
+ }
7663
+ /**
7664
+ * 评估一批新通知。
7665
+ *
7666
+ * @returns true 表示评估流程正常结束;false 表示 invoker 失败,调用方可选择回退。
7667
+ */
7668
+ async evaluate(notifications) {
7669
+ if (notifications.length === 0) return true;
7670
+ const rules = this.registry.getEnabled();
7671
+ if (rules.length === 0) return true;
7672
+ const matches = await this.invoker.matchNotifications(notifications, rules);
7673
+ if (matches === null) return false;
7674
+ if (matches.length === 0) {
7675
+ this.logger.info(
7676
+ `lightrules: 0 matches (notifications=${notifications.length}, rules=${rules.length})`
7677
+ );
7678
+ return true;
7679
+ }
7680
+ const firedRules = /* @__PURE__ */ new Set();
7681
+ for (const match of matches) {
7682
+ if (firedRules.has(match.ruleName)) continue;
7683
+ const rule = this.registry.get(match.ruleName);
7684
+ if (!rule) {
7685
+ this.logger.warn(
7686
+ `lightrules: matched rule '${match.ruleName}' not found in registry`
7687
+ );
7688
+ continue;
7689
+ }
7690
+ firedRules.add(match.ruleName);
7691
+ await this.triggerLight(rule);
7692
+ }
7693
+ this.logger.info(
7694
+ `lightrules: fired ${firedRules.size} rule(s): ${[...firedRules].join(", ")} (from ${matches.length} match(es) across ${notifications.length} notification(s))`
7695
+ );
7696
+ return true;
7697
+ }
7698
+ async triggerLight(rule) {
7699
+ let apiKey;
7700
+ try {
7701
+ apiKey = requireApiKey();
7702
+ } catch (err2) {
7703
+ this.logger.warn(
7704
+ `lightrules: trigger '${rule.name}' skipped - no API key: ${err2?.message ?? err2}`
7705
+ );
7706
+ return;
7707
+ }
7708
+ try {
7709
+ const result = await sendLightEffect(
7710
+ apiKey,
7711
+ rule.segments,
7712
+ this.logger,
7713
+ { repeat_times: rule.repeat_times },
7714
+ rule.description
7715
+ );
7716
+ if (!result.ok) {
7717
+ this.logger.warn(
7718
+ `lightrules: trigger '${rule.name}' http-failed (${result.status ?? "?"}): ${result.error}`
7719
+ );
7720
+ }
7721
+ } catch (err2) {
7722
+ this.logger.warn(
7723
+ `lightrules: trigger '${rule.name}' exception: ${err2?.message ?? err2}`
7724
+ );
7725
+ }
7726
+ }
7727
+ };
7728
+
7573
7729
  // src/light-rules/migration.ts
7574
- var import_node_fs6 = require("fs");
7575
- var import_node_path5 = require("path");
7730
+ var import_node_fs9 = require("fs");
7731
+ var import_node_path8 = require("path");
7576
7732
  var NO_MATCH_FETCH_PY = `#!/usr/bin/env python3
7577
7733
  # \u6B64\u6587\u4EF6\u7531\u8FC1\u79FB\u5DE5\u5177\u751F\u6210\u3002
7578
7734
  # \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
@@ -7582,32 +7738,32 @@ function normalizeScriptText(text) {
7582
7738
  return text.replace(/\r\n/g, "\n").trim();
7583
7739
  }
7584
7740
  function resolveTasksDir(ctx) {
7585
- if (ctx.workspaceDir) return (0, import_node_path5.join)(ctx.workspaceDir, "tasks");
7741
+ if (ctx.workspaceDir) return (0, import_node_path8.join)(ctx.workspaceDir, "tasks");
7586
7742
  if (ctx.stateDir) {
7587
- const inferredWorkspaceDir = (0, import_node_path5.join)(ctx.stateDir, "workspace");
7588
- if ((0, import_node_fs6.existsSync)(inferredWorkspaceDir)) return (0, import_node_path5.join)(inferredWorkspaceDir, "tasks");
7589
- return (0, import_node_path5.join)(ctx.stateDir, "tasks");
7743
+ const inferredWorkspaceDir = (0, import_node_path8.join)(ctx.stateDir, "workspace");
7744
+ if ((0, import_node_fs9.existsSync)(inferredWorkspaceDir)) return (0, import_node_path8.join)(inferredWorkspaceDir, "tasks");
7745
+ return (0, import_node_path8.join)(ctx.stateDir, "tasks");
7590
7746
  }
7591
7747
  return null;
7592
7748
  }
7593
7749
  function migrateLegacyLightRuleTasks(ctx, logger) {
7594
7750
  const tasksDir3 = resolveTasksDir(ctx);
7595
- if (!tasksDir3 || !(0, import_node_fs6.existsSync)(tasksDir3)) return;
7751
+ if (!tasksDir3 || !(0, import_node_fs9.existsSync)(tasksDir3)) return;
7596
7752
  try {
7597
- for (const entry of (0, import_node_fs6.readdirSync)(tasksDir3, { withFileTypes: true })) {
7753
+ for (const entry of (0, import_node_fs9.readdirSync)(tasksDir3, { withFileTypes: true })) {
7598
7754
  if (!entry.isDirectory()) continue;
7599
- migrateTaskDir((0, import_node_path5.join)(tasksDir3, String(entry.name)), logger);
7755
+ migrateTaskDir((0, import_node_path8.join)(tasksDir3, String(entry.name)), logger);
7600
7756
  }
7601
7757
  } catch (err2) {
7602
7758
  logger.warn(`migration: failed to read tasks dir: ${err2?.message}`);
7603
7759
  }
7604
7760
  }
7605
7761
  function migrateTaskDir(taskDir, logger) {
7606
- const metaPath = (0, import_node_path5.join)(taskDir, "meta.json");
7607
- if (!(0, import_node_fs6.existsSync)(metaPath)) return;
7762
+ const metaPath = (0, import_node_path8.join)(taskDir, "meta.json");
7763
+ if (!(0, import_node_fs9.existsSync)(metaPath)) return;
7608
7764
  let meta;
7609
7765
  try {
7610
- meta = JSON.parse((0, import_node_fs6.readFileSync)(metaPath, "utf-8"));
7766
+ meta = JSON.parse((0, import_node_fs9.readFileSync)(metaPath, "utf-8"));
7611
7767
  } catch {
7612
7768
  return;
7613
7769
  }
@@ -7616,7 +7772,7 @@ function migrateTaskDir(taskDir, logger) {
7616
7772
  mergeMatchRulesIntoDescription(meta, name, metaPath, logger);
7617
7773
  replaceFetchPy(taskDir, name, logger);
7618
7774
  for (const filename of ["README.md", "checkpoint.json"]) {
7619
- removeFile((0, import_node_path5.join)(taskDir, filename), name, filename, logger);
7775
+ removeFile((0, import_node_path8.join)(taskDir, filename), name, filename, logger);
7620
7776
  }
7621
7777
  }
7622
7778
  function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
@@ -7637,36 +7793,185 @@ function mergeMatchRulesIntoDescription(meta, name, metaPath, logger) {
7637
7793
  }
7638
7794
  delete meta.matchRules;
7639
7795
  try {
7640
- (0, import_node_fs6.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7796
+ (0, import_node_fs9.writeFileSync)(metaPath, JSON.stringify(meta, null, 2), "utf-8");
7641
7797
  logger.info(`migration: merged matchRules into description for light rule: ${name}`);
7642
7798
  } catch (err2) {
7643
7799
  logger.warn(`migration: failed to update meta.json for ${name}: ${err2?.message}`);
7644
7800
  }
7645
7801
  }
7646
7802
  function replaceFetchPy(taskDir, name, logger) {
7647
- const fetchPyPath = (0, import_node_path5.join)(taskDir, "fetch.py");
7648
- if (!(0, import_node_fs6.existsSync)(fetchPyPath)) return;
7803
+ const fetchPyPath = (0, import_node_path8.join)(taskDir, "fetch.py");
7804
+ if (!(0, import_node_fs9.existsSync)(fetchPyPath)) return;
7649
7805
  try {
7650
- const existing = (0, import_node_fs6.readFileSync)(fetchPyPath, "utf-8");
7806
+ const existing = (0, import_node_fs9.readFileSync)(fetchPyPath, "utf-8");
7651
7807
  if (normalizeScriptText(existing) === normalizeScriptText(NO_MATCH_FETCH_PY)) {
7652
7808
  return;
7653
7809
  }
7654
- (0, import_node_fs6.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7810
+ (0, import_node_fs9.writeFileSync)(fetchPyPath, NO_MATCH_FETCH_PY, "utf-8");
7655
7811
  logger.info(`migration: replaced fetch.py with NO_MATCH placeholder for ${name}`);
7656
7812
  } catch (err2) {
7657
7813
  logger.warn(`migration: failed to replace fetch.py for ${name}: ${err2?.message}`);
7658
7814
  }
7659
7815
  }
7660
7816
  function removeFile(filePath, ruleName, filename, logger) {
7661
- if (!(0, import_node_fs6.existsSync)(filePath)) return;
7817
+ if (!(0, import_node_fs9.existsSync)(filePath)) return;
7662
7818
  try {
7663
- (0, import_node_fs6.rmSync)(filePath);
7819
+ (0, import_node_fs9.rmSync)(filePath);
7664
7820
  logger.info(`migration: removed ${filename} for light rule: ${ruleName}`);
7665
7821
  } catch (err2) {
7666
7822
  logger.warn(`migration: failed to remove ${filename} for ${ruleName}: ${err2?.message}`);
7667
7823
  }
7668
7824
  }
7669
7825
 
7826
+ // src/light-rules/pi-invoker.ts
7827
+ var import_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
7828
+ var DEFAULT_PROVIDER = "anthropic";
7829
+ var DEFAULT_MODEL_ID = "claude-haiku-4-5-20251001";
7830
+ var DEFAULT_TIMEOUT_MS = 1e4;
7831
+ var MAX_OUTPUT_TOKENS = 512;
7832
+ var PiAiInvoker = class {
7833
+ constructor(api, logger, options = {}) {
7834
+ this.api = api;
7835
+ this.logger = logger;
7836
+ this.options = options;
7837
+ }
7838
+ /**
7839
+ * 对一批通知和当前启用的规则做一次 LLM 匹配。
7840
+ *
7841
+ * 返回:
7842
+ * - `LlmMatchResult[]`:匹配成功(可能为空数组,即 0 命中)
7843
+ * - `null`:调用失败(模型准备失败 / 网络 / 解析)。调用方据此决定是否回退。
7844
+ */
7845
+ async matchNotifications(notifications, rules) {
7846
+ if (notifications.length === 0 || rules.length === 0) return [];
7847
+ const provider = this.options.provider ?? DEFAULT_PROVIDER;
7848
+ const modelId = this.options.modelId ?? DEFAULT_MODEL_ID;
7849
+ const prepared = await (0, import_agent_runtime.prepareSimpleCompletionModel)({
7850
+ cfg: this.api.config,
7851
+ provider,
7852
+ modelId
7853
+ });
7854
+ if ("error" in prepared) {
7855
+ this.logger.warn(
7856
+ `PiAiInvoker: prepare ${provider}/${modelId} failed: ${prepared.error}`
7857
+ );
7858
+ return null;
7859
+ }
7860
+ const systemPrompt = buildSystemPrompt(rules);
7861
+ const userMessage = buildUserMessage(notifications);
7862
+ try {
7863
+ const resp = await (0, import_agent_runtime.completeWithPreparedSimpleCompletionModel)({
7864
+ model: prepared.model,
7865
+ auth: prepared.auth,
7866
+ context: {
7867
+ systemPrompt,
7868
+ messages: [
7869
+ { role: "user", content: userMessage, timestamp: Date.now() }
7870
+ ]
7871
+ },
7872
+ options: {
7873
+ temperature: 0,
7874
+ maxTokens: MAX_OUTPUT_TOKENS,
7875
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
7876
+ cacheRetention: "short"
7877
+ }
7878
+ });
7879
+ if (resp.stopReason === "error" || resp.stopReason === "aborted") {
7880
+ this.logger.warn(
7881
+ `PiAiInvoker: complete stopped with ${resp.stopReason}: ${resp.errorMessage ?? "n/a"}`
7882
+ );
7883
+ return null;
7884
+ }
7885
+ const text = extractText(resp.content);
7886
+ return parseMatchResult(text, notifications.length, rules);
7887
+ } catch (err2) {
7888
+ this.logger.warn(`PiAiInvoker: complete failed: ${err2?.message ?? err2}`);
7889
+ return null;
7890
+ }
7891
+ }
7892
+ };
7893
+ function extractText(content) {
7894
+ if (!Array.isArray(content)) return "";
7895
+ const parts = [];
7896
+ for (const c of content) {
7897
+ if (c && typeof c === "object" && c.type === "text") {
7898
+ const t = c.text;
7899
+ if (typeof t === "string") parts.push(t);
7900
+ }
7901
+ }
7902
+ return parts.join("");
7903
+ }
7904
+ function buildSystemPrompt(rules) {
7905
+ const lines = [
7906
+ '\u4F60\u662F"\u624B\u673A\u901A\u77E5 \u2192 \u706F\u6548\u89C4\u5219"\u547D\u4E2D\u5339\u914D\u52A9\u624B\u3002',
7907
+ "",
7908
+ "\u5F53\u524D\u542F\u7528\u7684\u706F\u6548\u89C4\u5219\uFF08\u6309\u521B\u5EFA\u65F6\u95F4\u5012\u5E8F\uFF09\uFF1A",
7909
+ ""
7910
+ ];
7911
+ const sorted = [...rules].sort(
7912
+ (a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")
7913
+ );
7914
+ sorted.forEach((rule, idx) => {
7915
+ lines.push(`[${idx + 1}] name: ${rule.name}`);
7916
+ lines.push(` description: ${rule.description}`);
7917
+ lines.push("");
7918
+ });
7919
+ lines.push(
7920
+ "\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",
7921
+ "",
7922
+ "\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",
7923
+ '{"matches":[{"i":<\u901A\u77E5\u4E0B\u6807>,"rule":"<\u89C4\u5219name>"}]}',
7924
+ "",
7925
+ '- \u547D\u4E2D 0 \u6761 \u2192 \u8F93\u51FA {"matches":[]}',
7926
+ "- \u4E00\u6761\u901A\u77E5\u53EF\u80FD\u547D\u4E2D\u591A\u6761\u89C4\u5219 \u2192 \u6BCF\u4E2A\u7EC4\u5408\u4E00\u4E2A\u6761\u76EE",
7927
+ '- \u4E25\u683C\u6309\u89C4\u5219 description \u5224\u65AD\uFF0C\u62FF\u4E0D\u51C6\u65F6\u503E\u5411\u4E8E"\u4E0D\u89E6\u53D1"',
7928
+ "- rule \u5FC5\u987B\u7CBE\u786E\u7B49\u4E8E\u4E0A\u9762\u5217\u51FA\u7684 name \u5B57\u7B26\u4E32"
7929
+ );
7930
+ return lines.join("\n");
7931
+ }
7932
+ function buildUserMessage(notifications) {
7933
+ const lines = [
7934
+ `\u4EE5\u4E0B ${notifications.length} \u6761\u65B0\u624B\u673A\u901A\u77E5\uFF0C\u6309\u987A\u5E8F\u7F16\u53F7\uFF1A`,
7935
+ ""
7936
+ ];
7937
+ notifications.forEach((n, i) => {
7938
+ const app = n.appDisplayName ?? n.appName;
7939
+ lines.push(
7940
+ `[${i}] app=${app} time=${n.timestamp}`,
7941
+ ` title: ${n.title}`,
7942
+ ` content: ${n.content}`,
7943
+ ""
7944
+ );
7945
+ });
7946
+ 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");
7947
+ return lines.join("\n");
7948
+ }
7949
+ function parseMatchResult(rawText, notificationCount, rules) {
7950
+ if (!rawText) return [];
7951
+ const text = rawText.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
7952
+ let parsed;
7953
+ try {
7954
+ parsed = JSON.parse(text);
7955
+ } catch {
7956
+ return [];
7957
+ }
7958
+ if (!parsed || typeof parsed !== "object") return [];
7959
+ const raw = parsed.matches;
7960
+ if (!Array.isArray(raw)) return [];
7961
+ const validRuleNames = new Set(rules.map((r) => r.name));
7962
+ const results = [];
7963
+ for (const m of raw) {
7964
+ if (!m || typeof m !== "object") continue;
7965
+ const entry = m;
7966
+ const i = typeof entry.i === "number" ? entry.i : typeof entry.index === "number" ? entry.index : -1;
7967
+ const ruleName = typeof entry.rule === "string" ? entry.rule : null;
7968
+ if (!Number.isInteger(i) || i < 0 || i >= notificationCount) continue;
7969
+ if (!ruleName || !validRuleNames.has(ruleName)) continue;
7970
+ results.push({ notificationIndex: i, ruleName });
7971
+ }
7972
+ return results;
7973
+ }
7974
+
7670
7975
  // src/plugin/auto-update.ts
7671
7976
  init_env();
7672
7977
 
@@ -8515,61 +8820,6 @@ function registerNtfMonitor(ntf, ctx) {
8515
8820
 
8516
8821
  // src/cli/light-send.ts
8517
8822
  init_credentials();
8518
-
8519
- // src/light/sender.ts
8520
- var import_node_crypto2 = require("crypto");
8521
- init_env();
8522
- async function sendLightEffect(apiKey, segments, logger, repeatInput, reason) {
8523
- const apiUrl = getEnvUrls().lightApiUrl;
8524
- const appKey = "7Q617S1G5WD274JI";
8525
- const templateId = "1990771146010017788";
8526
- logger?.info(
8527
- `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)}`
8528
- );
8529
- if (!apiUrl || !appKey || !templateId) {
8530
- return {
8531
- ok: false,
8532
- error: "\u706F\u6548 API \u672A\u914D\u7F6E\uFF0C\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF LIGHT_API_URL / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
8533
- };
8534
- }
8535
- let bizContent;
8536
- try {
8537
- bizContent = buildLightEffectApnsBody(segments, repeatInput);
8538
- } catch (error) {
8539
- return { ok: false, error: error?.message ?? String(error) };
8540
- }
8541
- const bizUniqueId = (0, import_node_crypto2.randomUUID)();
8542
- const requestBody = {
8543
- appKey,
8544
- bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", reason },
8545
- bizUniqueId,
8546
- paramsMap: { bizContent },
8547
- pushType: "SPECIFY_PUSH",
8548
- templateId
8549
- };
8550
- logger?.info(
8551
- `Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`
8552
- );
8553
- const res = await fetch(apiUrl, {
8554
- method: "POST",
8555
- headers: {
8556
- "Content-Type": "application/json",
8557
- "X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
8558
- },
8559
- body: JSON.stringify(requestBody)
8560
- });
8561
- const resBody = await res.text();
8562
- if (!res.ok) {
8563
- logger?.warn(
8564
- `Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`
8565
- );
8566
- return { ok: false, status: res.status, error: resBody };
8567
- }
8568
- logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
8569
- return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
8570
- }
8571
-
8572
- // src/cli/light-send.ts
8573
8823
  function registerLightSend(light) {
8574
8824
  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) => {
8575
8825
  let apiKey;
@@ -10590,11 +10840,11 @@ var import_node_fs25 = require("fs");
10590
10840
  var import_node_path21 = require("path");
10591
10841
  var import_promises2 = require("stream/promises");
10592
10842
  var import_node_stream = require("stream");
10593
- var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
10843
+ var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
10594
10844
  var DEFAULT_MAX_RETRIES = 3;
10595
10845
  var DEFAULT_RETRY_BACKOFF_MS = 2e3;
10596
10846
  async function downloadFile(url, destPath, logger, options) {
10597
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10847
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
10598
10848
  const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
10599
10849
  const retryBackoffMs = options?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
10600
10850
  (0, import_node_fs25.mkdirSync)((0, import_node_path21.dirname)(destPath), { recursive: true });
@@ -10879,7 +11129,7 @@ var import_node_path24 = require("path");
10879
11129
  // node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
10880
11130
  var import_stream = __toESM(require_stream(), 1);
10881
11131
  var import_receiver = __toESM(require_receiver(), 1);
10882
- var import_sender3 = __toESM(require_sender(), 1);
11132
+ var import_sender4 = __toESM(require_sender(), 1);
10883
11133
  var import_websocket = __toESM(require_websocket(), 1);
10884
11134
  var import_websocket_server = __toESM(require_websocket_server(), 1);
10885
11135
  var wrapper_default = import_websocket.default;
@@ -12604,9 +12854,9 @@ function registerNotificationInterfaces(deps) {
12604
12854
  onAfterIngest,
12605
12855
  getCronForHttpIngest
12606
12856
  } = deps;
12607
- function triggerAfterIngest(insertedCount, cron) {
12608
- if (insertedCount <= 0 || !onAfterIngest) return;
12609
- void Promise.resolve().then(() => onAfterIngest(insertedCount, cron)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12857
+ function triggerAfterIngest(inserted, cron) {
12858
+ if (inserted.length === 0 || !onAfterIngest) return;
12859
+ void Promise.resolve().then(() => onAfterIngest(inserted, cron)).catch((err2) => logger.warn(`onAfterIngest failed: ${err2?.message ?? err2}`));
12610
12860
  }
12611
12861
  registerGatewayMethod(
12612
12862
  "notifications.push",
@@ -12630,7 +12880,7 @@ function registerNotificationInterfaces(deps) {
12630
12880
  const filtered = filterNotifications(items);
12631
12881
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12632
12882
  respond(true, toIngestResponse(result));
12633
- triggerAfterIngest(result.inserted.length, context?.cron);
12883
+ triggerAfterIngest(result.inserted, context?.cron);
12634
12884
  }
12635
12885
  );
12636
12886
  api.registerHttpRoute({
@@ -12674,7 +12924,7 @@ function registerNotificationInterfaces(deps) {
12674
12924
  const result = filtered.length ? await storage.ingest(filtered) : createEmptyIngestResult();
12675
12925
  res.writeHead(200, { "Content-Type": "application/json" });
12676
12926
  res.end(JSON.stringify({ ok: true, ...toIngestResponse(result) }));
12677
- triggerAfterIngest(result.inserted.length, getCronForHttpIngest?.());
12927
+ triggerAfterIngest(result.inserted, getCronForHttpIngest?.());
12678
12928
  }
12679
12929
  });
12680
12930
  logger.info("Gateway \u901A\u77E5\u65B9\u6CD5\u5DF2\u6CE8\u518C: notifications.push");
@@ -13218,9 +13468,16 @@ var index_default = {
13218
13468
  broadcastFn("recording.status", event);
13219
13469
  }
13220
13470
  const lightRuleRegistry = new LightRuleRegistry(lightRuleCtx);
13471
+ const lightRuleInvoker = new PiAiInvoker(api, logger);
13472
+ const inlineLightRuleEvaluator = new InlineLightRuleEvaluator({
13473
+ logger,
13474
+ registry: lightRuleRegistry,
13475
+ invoker: lightRuleInvoker
13476
+ });
13221
13477
  const lightRulesEvaluatorJob = new LightRulesEvaluatorJob({
13222
13478
  logger,
13223
13479
  registry: lightRuleRegistry,
13480
+ inlineEvaluator: inlineLightRuleEvaluator,
13224
13481
  subagentRunner: api.runtime.subagent,
13225
13482
  getNotificationsDir: () => openclawDir ? getStateFallbackNotificationDir(openclawDir) : void 0
13226
13483
  });
@@ -13253,8 +13510,8 @@ var index_default = {
13253
13510
  filterNotifications,
13254
13511
  registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
13255
13512
  tunnelService,
13256
- onAfterIngest: (insertedCount, cron) => {
13257
- void lightRulesEvaluatorJob.triggerIfNeeded(cron, insertedCount);
13513
+ onAfterIngest: (inserted, cron) => {
13514
+ void lightRulesEvaluatorJob.triggerIfNeeded(cron, inserted);
13258
13515
  },
13259
13516
  getCronForHttpIngest: () => cronService
13260
13517
  });