@yoooclaw/phone-notifications 1.11.4-beta.1 → 1.11.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5480,7 +5480,7 @@ function readBuildInjectedVersion() {
5480
5480
  if (false) {
5481
5481
  return void 0;
5482
5482
  }
5483
- const version = "1.11.4-beta.1".trim();
5483
+ const version = "1.11.4-beta.2".trim();
5484
5484
  return version || void 0;
5485
5485
  }
5486
5486
  function readPluginVersionFromPackageJson() {
@@ -7097,6 +7097,109 @@ function resolveLightTitle(title, reason, segments) {
7097
7097
  return `Effect: ${modeDesc || "custom"}`;
7098
7098
  }
7099
7099
 
7100
+ // src/light-rules/local-matcher.ts
7101
+ var CONTACT_PREFIXES = ["\u91CD\u8981\u8054\u7CFB\u4EBA", "\u8054\u7CFB\u4EBA"];
7102
+ var CONTACT_ACTION_RE = /(给我)?发(?:来)?消息|(给我)?发信息|来消息|回复(?:我)?|回应(?:我)?|私聊(?:我)?/u;
7103
+ var LEADING_SENDER_RE = /^(.{1,40}?)(?:\s+回应)?\s*[::]/u;
7104
+ function matchNotificationsLocally(notifications, rules) {
7105
+ const parsedRules = rules.map((rule) => parseContactRule(rule)).filter((item) => item !== null);
7106
+ if (parsedRules.length === 0) {
7107
+ return [];
7108
+ }
7109
+ const results = [];
7110
+ notifications.forEach((notification, notificationIndex) => {
7111
+ const senders = collectSenderCandidates(notification);
7112
+ if (senders.length === 0) {
7113
+ return;
7114
+ }
7115
+ parsedRules.forEach((rule) => {
7116
+ if (senders.some((sender) => senderMatchesContact(sender, rule.contactName))) {
7117
+ results.push({ notificationIndex, ruleName: rule.ruleName });
7118
+ }
7119
+ });
7120
+ });
7121
+ return results;
7122
+ }
7123
+ function parseContactRule(rule) {
7124
+ const contactName = extractImportantContact(rule.description);
7125
+ if (!contactName) {
7126
+ return null;
7127
+ }
7128
+ return {
7129
+ ruleName: rule.name,
7130
+ contactName
7131
+ };
7132
+ }
7133
+ function extractImportantContact(description) {
7134
+ const text = description.trim();
7135
+ if (!text) {
7136
+ return null;
7137
+ }
7138
+ for (const prefix of CONTACT_PREFIXES) {
7139
+ const start = text.indexOf(prefix);
7140
+ if (start < 0) {
7141
+ continue;
7142
+ }
7143
+ const tail = text.slice(start + prefix.length);
7144
+ const actionMatch = CONTACT_ACTION_RE.exec(tail);
7145
+ if (!actionMatch || actionMatch.index === 0) {
7146
+ continue;
7147
+ }
7148
+ const rawName = tail.slice(0, actionMatch.index).trim();
7149
+ const cleaned = trimContactName(rawName);
7150
+ if (cleaned) {
7151
+ return cleaned;
7152
+ }
7153
+ }
7154
+ return null;
7155
+ }
7156
+ function trimContactName(value) {
7157
+ const cleaned = value.replace(/^[是为叫做名为\s]+/u, "").replace(/[,。,.;;].*$/u, "").trim();
7158
+ return cleaned || null;
7159
+ }
7160
+ function collectSenderCandidates(notification) {
7161
+ const candidates = [
7162
+ notification.senderName,
7163
+ notification.title,
7164
+ notification.conversationName,
7165
+ extractLeadingSender(notification.content)
7166
+ ];
7167
+ const deduped = /* @__PURE__ */ new Map();
7168
+ for (const candidate of candidates) {
7169
+ if (!candidate) {
7170
+ continue;
7171
+ }
7172
+ const normalized = normalizeComparableName(candidate);
7173
+ if (normalized.length < 2) {
7174
+ continue;
7175
+ }
7176
+ deduped.set(normalized, candidate);
7177
+ }
7178
+ return Array.from(deduped.values());
7179
+ }
7180
+ function extractLeadingSender(content) {
7181
+ const text = content.trim();
7182
+ if (!text) {
7183
+ return null;
7184
+ }
7185
+ const match = LEADING_SENDER_RE.exec(text);
7186
+ if (!match) {
7187
+ return null;
7188
+ }
7189
+ return match[1]?.trim() || null;
7190
+ }
7191
+ function senderMatchesContact(sender, contactName) {
7192
+ const normalizedSender = normalizeComparableName(sender);
7193
+ const normalizedContact = normalizeComparableName(contactName);
7194
+ if (!normalizedSender || !normalizedContact) {
7195
+ return false;
7196
+ }
7197
+ return normalizedSender.includes(normalizedContact) || normalizedContact.includes(normalizedSender);
7198
+ }
7199
+ function normalizeComparableName(value) {
7200
+ return value.normalize("NFKC").replace(new RegExp("\\p{Extended_Pictographic}", "gu"), "").replace(/\s+/gu, "").replace(/[^\p{L}\p{N}]/gu, "");
7201
+ }
7202
+
7100
7203
  // src/light-rules/inline-evaluator.ts
7101
7204
  var InlineLightRuleEvaluator = class {
7102
7205
  logger;
@@ -7116,8 +7219,40 @@ var InlineLightRuleEvaluator = class {
7116
7219
  if (notifications.length === 0) return true;
7117
7220
  const rules = this.registry.getEnabled();
7118
7221
  if (rules.length === 0) return true;
7119
- const matches = await this.invoker.matchNotifications(notifications, rules);
7120
- if (matches === null) return false;
7222
+ const localMatches = matchNotificationsLocally(notifications, rules);
7223
+ const localMatchedRuleNames = new Set(localMatches.map((match) => match.ruleName));
7224
+ const llmCandidateRules = rules.filter((rule) => !localMatchedRuleNames.has(rule.name));
7225
+ const combinedMatches = /* @__PURE__ */ new Map();
7226
+ for (const match of localMatches) {
7227
+ combinedMatches.set(
7228
+ `${match.notificationIndex}:${match.ruleName}`,
7229
+ match
7230
+ );
7231
+ }
7232
+ if (localMatches.length > 0) {
7233
+ this.logger.info(
7234
+ `lightrules: local matched ${localMatchedRuleNames.size} rule(s) (notifications=${notifications.length}, remainingRules=${llmCandidateRules.length})`
7235
+ );
7236
+ }
7237
+ if (llmCandidateRules.length > 0) {
7238
+ const llmMatches = await this.invoker.matchNotifications(notifications, llmCandidateRules);
7239
+ if (llmMatches === null) {
7240
+ if (combinedMatches.size === 0) {
7241
+ return false;
7242
+ }
7243
+ this.logger.warn(
7244
+ `lightrules: invoker failed; using ${combinedMatches.size} local fallback match(es)`
7245
+ );
7246
+ } else {
7247
+ for (const match of llmMatches) {
7248
+ combinedMatches.set(
7249
+ `${match.notificationIndex}:${match.ruleName}`,
7250
+ match
7251
+ );
7252
+ }
7253
+ }
7254
+ }
7255
+ const matches = Array.from(combinedMatches.values());
7121
7256
  if (matches.length === 0) {
7122
7257
  this.logger.info(
7123
7258
  `lightrules: 0 matches (notifications=${notifications.length}, rules=${rules.length})`
@@ -7135,14 +7270,15 @@ var InlineLightRuleEvaluator = class {
7135
7270
  continue;
7136
7271
  }
7137
7272
  firedRules.add(match.ruleName);
7138
- await this.triggerLight(rule);
7273
+ const notification = notifications[match.notificationIndex];
7274
+ await this.triggerLight(rule, notification);
7139
7275
  }
7140
7276
  this.logger.info(
7141
7277
  `lightrules: fired ${firedRules.size} rule(s): ${[...firedRules].join(", ")} (from ${matches.length} match(es) across ${notifications.length} notification(s))`
7142
7278
  );
7143
7279
  return true;
7144
7280
  }
7145
- async triggerLight(rule) {
7281
+ async triggerLight(rule, notification) {
7146
7282
  let apiKey;
7147
7283
  try {
7148
7284
  apiKey = requireApiKey();
@@ -7152,13 +7288,14 @@ var InlineLightRuleEvaluator = class {
7152
7288
  );
7153
7289
  return;
7154
7290
  }
7291
+ const reason = buildTriggerReason(rule, notification);
7155
7292
  try {
7156
7293
  const result = await sendLightEffect(
7157
7294
  apiKey,
7158
7295
  rule.segments,
7159
7296
  this.logger,
7160
7297
  { repeat_times: rule.repeat_times },
7161
- rule.description,
7298
+ reason,
7162
7299
  rule.title
7163
7300
  );
7164
7301
  if (!result.ok) {
@@ -7173,6 +7310,31 @@ var InlineLightRuleEvaluator = class {
7173
7310
  }
7174
7311
  }
7175
7312
  };
7313
+ var REASON_CONTENT_MAX = 40;
7314
+ function buildTriggerReason(rule, notification) {
7315
+ const ruleLabel = rule.title?.trim() || rule.name;
7316
+ if (!notification) {
7317
+ return `\u89E6\u53D1"${ruleLabel}"\u706F\u6548`;
7318
+ }
7319
+ const app = notification.appDisplayName?.trim() || notification.appName.trim();
7320
+ const sender = notification.senderName?.trim() || notification.title?.trim();
7321
+ const conversation = notification.conversationName?.trim();
7322
+ const isGroup = notification.conversationType === "group";
7323
+ let where = app;
7324
+ if (isGroup) {
7325
+ where = conversation ? `${app}\u7FA4\u804A"${conversation}"` : `${app}\u7FA4\u804A`;
7326
+ }
7327
+ const action = sender ? `${sender}\u53D1\u6765\u6D88\u606F` : "\u6536\u5230\u65B0\u6D88\u606F";
7328
+ const snippet = summarizeContent(notification.content);
7329
+ const detail = snippet ? `\uFF1A${snippet}` : "";
7330
+ return `\u68C0\u6D4B\u5230${where}\u4E2D${action}${detail}\uFF0C\u89E6\u53D1"${ruleLabel}"\u706F\u6548`;
7331
+ }
7332
+ function summarizeContent(content) {
7333
+ const trimmed = content?.trim();
7334
+ if (!trimmed) return "";
7335
+ if (trimmed.length <= REASON_CONTENT_MAX) return trimmed;
7336
+ return `${trimmed.slice(0, REASON_CONTENT_MAX)}\u2026`;
7337
+ }
7176
7338
 
7177
7339
  // src/light-rules/pi-invoker.ts
7178
7340
  var import_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
@@ -7180,6 +7342,7 @@ var DEFAULT_PROVIDER = "anthropic";
7180
7342
  var DEFAULT_MODEL_ID = "claude-haiku-4-5-20251001";
7181
7343
  var DEFAULT_AGENT_ID = "main";
7182
7344
  var DEFAULT_TIMEOUT_MS = 1e4;
7345
+ var DEFAULT_MAX_ATTEMPTS = 2;
7183
7346
  var MAX_OUTPUT_TOKENS = 512;
7184
7347
  var PiAiInvoker = class {
7185
7348
  constructor(api, logger, options = {}) {
@@ -7205,35 +7368,52 @@ var PiAiInvoker = class {
7205
7368
  }
7206
7369
  const systemPrompt = buildSystemPrompt(rules);
7207
7370
  const userMessage = buildUserMessage(notifications);
7208
- try {
7209
- const resp = await (0, import_agent_runtime.completeWithPreparedSimpleCompletionModel)({
7210
- model: prepared.model,
7211
- auth: prepared.auth,
7212
- context: {
7213
- systemPrompt,
7214
- messages: [
7215
- { role: "user", content: userMessage, timestamp: Date.now() }
7216
- ]
7217
- },
7218
- options: {
7219
- temperature: 0,
7220
- maxTokens: MAX_OUTPUT_TOKENS,
7221
- signal: AbortSignal.timeout(this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
7222
- cacheRetention: "short"
7371
+ const baseTimeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7372
+ const maxAttempts = Math.max(1, this.options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
7373
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7374
+ const timeoutMs = baseTimeoutMs * attempt;
7375
+ try {
7376
+ const resp = await (0, import_agent_runtime.completeWithPreparedSimpleCompletionModel)({
7377
+ model: prepared.model,
7378
+ auth: prepared.auth,
7379
+ context: {
7380
+ systemPrompt,
7381
+ messages: [
7382
+ { role: "user", content: userMessage, timestamp: Date.now() }
7383
+ ]
7384
+ },
7385
+ options: {
7386
+ temperature: 0,
7387
+ maxTokens: MAX_OUTPUT_TOKENS,
7388
+ signal: AbortSignal.timeout(timeoutMs),
7389
+ cacheRetention: "short"
7390
+ }
7391
+ });
7392
+ if (resp.stopReason === "error" || resp.stopReason === "aborted") {
7393
+ const retryable = resp.stopReason === "aborted" && attempt < maxAttempts;
7394
+ this.logger.warn(
7395
+ `PiAiInvoker: complete stopped with ${resp.stopReason}: ${resp.errorMessage ?? "n/a"} (attempt ${attempt}/${maxAttempts}, timeoutMs=${timeoutMs}${retryable ? ", retrying" : ""})`
7396
+ );
7397
+ if (retryable) {
7398
+ continue;
7399
+ }
7400
+ return null;
7223
7401
  }
7224
- });
7225
- if (resp.stopReason === "error" || resp.stopReason === "aborted") {
7402
+ const text = extractText(resp.content);
7403
+ return parseMatchResult(text, notifications.length, rules);
7404
+ } catch (err2) {
7405
+ const message = err2?.message ?? String(err2);
7406
+ const retryable = attempt < maxAttempts && (err2?.name === "AbortError" || /\babort(?:ed)?\b/i.test(message));
7226
7407
  this.logger.warn(
7227
- `PiAiInvoker: complete stopped with ${resp.stopReason}: ${resp.errorMessage ?? "n/a"}`
7408
+ `PiAiInvoker: complete failed: ${message} (attempt ${attempt}/${maxAttempts}${retryable ? ", retrying" : ""})`
7228
7409
  );
7410
+ if (retryable) {
7411
+ continue;
7412
+ }
7229
7413
  return null;
7230
7414
  }
7231
- const text = extractText(resp.content);
7232
- return parseMatchResult(text, notifications.length, rules);
7233
- } catch (err2) {
7234
- this.logger.warn(`PiAiInvoker: complete failed: ${err2?.message ?? err2}`);
7235
- return null;
7236
7415
  }
7416
+ return null;
7237
7417
  }
7238
7418
  async prepareCompletionModel() {
7239
7419
  if (this.options.provider || this.options.modelId) {