@yoooclaw/phone-notifications 1.12.0 → 1.12.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.
Files changed (76) hide show
  1. package/README.md +212 -29
  2. package/dist/bin/ntf.cjs +918 -113
  3. package/dist/bin/ntf.cjs.map +15 -10
  4. package/dist/cli/helpers.d.ts +27 -0
  5. package/dist/cli/helpers.d.ts.map +1 -1
  6. package/dist/cli/image-list.d.ts +3 -0
  7. package/dist/cli/image-list.d.ts.map +1 -0
  8. package/dist/cli/image-path.d.ts +3 -0
  9. package/dist/cli/image-path.d.ts.map +1 -0
  10. package/dist/cli/image-status.d.ts +3 -0
  11. package/dist/cli/image-status.d.ts.map +1 -0
  12. package/dist/cli/image-storage-path.d.ts +3 -0
  13. package/dist/cli/image-storage-path.d.ts.map +1 -0
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/ntf-query.d.ts +11 -0
  16. package/dist/cli/ntf-query.d.ts.map +1 -1
  17. package/dist/cli/ntf-summary-job.d.ts +3 -0
  18. package/dist/cli/ntf-summary-job.d.ts.map +1 -0
  19. package/dist/cli/ntf-summary.d.ts.map +1 -1
  20. package/dist/cli/ntf-sync.d.ts.map +1 -1
  21. package/dist/env.d.ts +3 -0
  22. package/dist/env.d.ts.map +1 -1
  23. package/dist/image/handler.d.ts +19 -0
  24. package/dist/image/handler.d.ts.map +1 -0
  25. package/dist/image/index.d.ts +3 -0
  26. package/dist/image/index.d.ts.map +1 -0
  27. package/dist/image/store.d.ts +76 -0
  28. package/dist/image/store.d.ts.map +1 -0
  29. package/dist/index.cjs +9036 -7110
  30. package/dist/index.cjs.map +39 -34
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/light-rules/client.d.ts +58 -0
  33. package/dist/light-rules/client.d.ts.map +1 -0
  34. package/dist/light-rules/gateway.d.ts.map +1 -1
  35. package/dist/light-rules/types.d.ts +13 -8
  36. package/dist/light-rules/types.d.ts.map +1 -1
  37. package/dist/notification/storage.d.ts +1 -0
  38. package/dist/notification/storage.d.ts.map +1 -1
  39. package/dist/notification/summary.d.ts +144 -0
  40. package/dist/notification/summary.d.ts.map +1 -0
  41. package/dist/plugin/images.d.ts +16 -0
  42. package/dist/plugin/images.d.ts.map +1 -0
  43. package/dist/plugin/lifecycle.d.ts +2 -0
  44. package/dist/plugin/lifecycle.d.ts.map +1 -1
  45. package/dist/plugin/light-rules-tools.d.ts +2 -2
  46. package/dist/plugin/light-rules-tools.d.ts.map +1 -1
  47. package/dist/plugin/notifications.d.ts +5 -3
  48. package/dist/plugin/notifications.d.ts.map +1 -1
  49. package/dist/plugin/recordings.d.ts.map +1 -1
  50. package/dist/recording/handler.d.ts +1 -0
  51. package/dist/recording/handler.d.ts.map +1 -1
  52. package/dist/recording/index.d.ts +1 -0
  53. package/dist/recording/index.d.ts.map +1 -1
  54. package/dist/recording/result-writer.d.ts +9 -0
  55. package/dist/recording/result-writer.d.ts.map +1 -0
  56. package/dist/recording/storage.d.ts +2 -0
  57. package/dist/recording/storage.d.ts.map +1 -1
  58. package/dist/recording/transcript-document.d.ts +1 -0
  59. package/dist/recording/transcript-document.d.ts.map +1 -1
  60. package/dist/tunnel/frame-slimmer.d.ts +37 -0
  61. package/dist/tunnel/frame-slimmer.d.ts.map +1 -0
  62. package/dist/tunnel/proxy.d.ts.map +1 -1
  63. package/dist/types.d.ts +120 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/update/index.d.ts.map +1 -1
  66. package/openclaw.plugin.json +13 -1
  67. package/package.json +3 -2
  68. package/skills/notification-monitor/SKILL.md +14 -13
  69. package/skills/notification-query/SKILL.md +101 -7
  70. package/skills/notification-to-memory/SKILL.md +57 -22
  71. package/skills/notification-to-memory/references/step-0-preflight.md +0 -40
  72. package/skills/notification-to-memory/references/step-1-scan-pending.md +0 -137
  73. package/skills/notification-to-memory/references/step-2-process-dates.md +0 -98
  74. package/skills/notification-to-memory/references/step-3-check-cron-status.md +0 -38
  75. package/skills/notification-to-memory/references/step-4-final-reporting.md +0 -57
  76. package/skills/notification-to-memory/scripts/select-memory-prompt.sh +0 -18
package/dist/bin/ntf.cjs CHANGED
@@ -2592,6 +2592,34 @@ function readRecordingIndex(dir) {
2592
2592
  return [];
2593
2593
  }
2594
2594
  }
2595
+ function resolveImagesDir(ctx) {
2596
+ if (ctx.stateDir) {
2597
+ const dir = import_node_path4.join(ctx.stateDir, "plugins", "phone-notifications", "images");
2598
+ if (import_node_fs3.existsSync(dir))
2599
+ return dir;
2600
+ }
2601
+ if (ctx.workspaceDir) {
2602
+ const dir = import_node_path4.join(ctx.workspaceDir, "images");
2603
+ if (import_node_fs3.existsSync(dir))
2604
+ return dir;
2605
+ }
2606
+ return null;
2607
+ }
2608
+ function readImageIndex(dir) {
2609
+ const indexPath = import_node_path4.join(dir, "index.json");
2610
+ if (!import_node_fs3.existsSync(indexPath))
2611
+ return [];
2612
+ try {
2613
+ const raw = JSON.parse(import_node_fs3.readFileSync(indexPath, "utf-8"));
2614
+ const images = Array.isArray(raw?.images) ? raw.images : [];
2615
+ return images.sort((a, b) => b.metadata.created_at.localeCompare(a.metadata.created_at));
2616
+ } catch {
2617
+ return [];
2618
+ }
2619
+ }
2620
+ function resolveImageFile(dir, relative) {
2621
+ return import_node_path4.isAbsolute(relative) ? relative : import_node_path4.join(dir, relative);
2622
+ }
2595
2623
 
2596
2624
  // src/auth/credentials.ts
2597
2625
  var import_node_fs4 = require("node:fs");
@@ -2808,27 +2836,43 @@ function matchesNotificationQuery(item, opts) {
2808
2836
  return true;
2809
2837
  }
2810
2838
  async function collectMatchingNotifications(dir, opts) {
2839
+ const result = await collectMatchingNotificationsWithStats(dir, opts);
2840
+ return result.notifications;
2841
+ }
2842
+ async function collectMatchingNotificationsWithStats(dir, opts) {
2811
2843
  const keys = await listDateKeysAsync(dir);
2812
2844
  const results = [];
2813
- const canStopAfterLimit = opts.fromTs === null && opts.toTs === null;
2845
+ const stats = {
2846
+ daysScanned: 0,
2847
+ itemsScanned: 0,
2848
+ matched: 0,
2849
+ stoppedAfterLimit: false
2850
+ };
2814
2851
  for (const dateKey of keys) {
2815
2852
  if (opts.fromDateKey && dateKey < opts.fromDateKey)
2816
2853
  continue;
2817
2854
  if (opts.toDateKey && dateKey > opts.toDateKey)
2818
2855
  continue;
2856
+ stats.daysScanned += 1;
2819
2857
  const items = sortNotificationsByTimestampDesc([
2820
2858
  ...await readDateFileAsync(dir, dateKey)
2821
2859
  ]);
2822
2860
  for (const item of items) {
2861
+ stats.itemsScanned += 1;
2823
2862
  if (!matchesNotificationQuery(item, opts))
2824
2863
  continue;
2864
+ stats.matched += 1;
2825
2865
  results.push(item);
2826
- if (canStopAfterLimit && results.length >= opts.limit) {
2827
- return results;
2866
+ if (results.length >= opts.limit) {
2867
+ stats.stoppedAfterLimit = true;
2868
+ return { notifications: results, stats };
2828
2869
  }
2829
2870
  }
2830
2871
  }
2831
- return sortNotificationsByTimestampDesc(results).slice(0, opts.limit);
2872
+ return {
2873
+ notifications: sortNotificationsByTimestampDesc(results).slice(0, opts.limit),
2874
+ stats
2875
+ };
2832
2876
  }
2833
2877
 
2834
2878
  // src/cli/ntf-search.ts
@@ -2914,7 +2958,10 @@ function registerNtfSummary(ntf, ctx) {
2914
2958
  const sampleLimit = parsePositiveIntegerOption(opts.sample, 30, "--sample", "INVALID_SAMPLE");
2915
2959
  const topLimit = parsePositiveIntegerOption(opts.top, 10, "--top", "INVALID_TOP");
2916
2960
  const maxContent = parsePositiveIntegerOption(opts.maxContent, 80, "--max-content", "INVALID_MAX_CONTENT");
2917
- const notifications = await collectMatchingNotifications(dir, query);
2961
+ const startedAtMs = Date.now();
2962
+ const queryResult = await collectMatchingNotificationsWithStats(dir, query);
2963
+ const elapsedMs = Date.now() - startedAtMs;
2964
+ const { notifications, stats } = queryResult;
2918
2965
  const byApp = new Map;
2919
2966
  const bySender = new Map;
2920
2967
  const byConversation = new Map;
@@ -2946,6 +2993,13 @@ function registerNtfSummary(ntf, ctx) {
2946
2993
  newest: notifications[0]?.timestamp ?? null,
2947
2994
  oldest: notifications[notifications.length - 1]?.timestamp ?? null
2948
2995
  },
2996
+ scan: {
2997
+ daysScanned: stats.daysScanned,
2998
+ itemsScanned: stats.itemsScanned,
2999
+ matched: stats.matched,
3000
+ stoppedAfterLimit: stats.stoppedAfterLimit,
3001
+ elapsedMs
3002
+ },
2949
3003
  byApp: topRows(byApp, topLimit),
2950
3004
  bySender: senderRows,
2951
3005
  byConversation: topRows(byConversation, topLimit),
@@ -2956,6 +3010,555 @@ function registerNtfSummary(ntf, ctx) {
2956
3010
  });
2957
3011
  }
2958
3012
 
3013
+ // src/cli/ntf-summary-job.ts
3014
+ var import_node_crypto = require("node:crypto");
3015
+ var import_node_fs6 = require("node:fs");
3016
+ var import_node_path6 = require("node:path");
3017
+ var SUMMARY_ROOT = ".summaries";
3018
+ var JOBS_DIR = "jobs";
3019
+ var CHUNKS_DIR = "chunks";
3020
+ var SUMMARIES_DIR = "summaries";
3021
+ var RESULT_FILE = "result.md";
3022
+ var DEFAULT_JOB_LIMIT = 1000;
3023
+ var DEFAULT_CHUNK_SIZE = 150;
3024
+ var DEFAULT_MAX_CONTENT = 120;
3025
+ var DEFAULT_RUN_MAX_CHUNKS = 20;
3026
+ var ATTENTION_KEYWORDS = [
3027
+ "待办",
3028
+ "需要",
3029
+ "麻烦",
3030
+ "确认",
3031
+ "回复",
3032
+ "审批",
3033
+ "开会",
3034
+ "会议",
3035
+ "安排",
3036
+ "截止",
3037
+ "紧急",
3038
+ "异常",
3039
+ "失败",
3040
+ "风险",
3041
+ "todo",
3042
+ "deadline",
3043
+ "urgent",
3044
+ "error",
3045
+ "failed"
3046
+ ];
3047
+ function truncate2(value, maxLength = 120) {
3048
+ if (value.length <= maxLength)
3049
+ return value;
3050
+ return `${value.slice(0, maxLength - 1)}…`;
3051
+ }
3052
+ function compactNotification2(item, maxContent) {
3053
+ const result = {
3054
+ appName: item.appName,
3055
+ title: truncate2(item.title, maxContent),
3056
+ content: truncate2(item.content, maxContent),
3057
+ timestamp: item.timestamp
3058
+ };
3059
+ if (item.appDisplayName)
3060
+ result.appDisplayName = item.appDisplayName;
3061
+ if (item.senderName)
3062
+ result.senderName = item.senderName;
3063
+ if (item.conversationType)
3064
+ result.conversationType = item.conversationType;
3065
+ if (item.conversationName)
3066
+ result.conversationName = item.conversationName;
3067
+ return result;
3068
+ }
3069
+ function notificationSender(item) {
3070
+ return item.senderName?.trim() || item.conversationName?.trim() || item.title.trim() || "(unknown)";
3071
+ }
3072
+ function notificationApp(item) {
3073
+ return item.appDisplayName?.trim() || item.appName.trim() || "(unknown)";
3074
+ }
3075
+ function increment2(map, key, seed) {
3076
+ const existing = map.get(key);
3077
+ if (existing) {
3078
+ existing.count += 1;
3079
+ return;
3080
+ }
3081
+ map.set(key, { ...seed, count: 1 });
3082
+ }
3083
+ function topRows2(map, limit) {
3084
+ return [...map.values()].sort((a, b) => b.count - a.count).slice(0, limit);
3085
+ }
3086
+ function formatTopRows(rows, labelKey) {
3087
+ if (rows.length === 0)
3088
+ return "无";
3089
+ return rows.map((row) => `${String(row[labelKey])} ${row.count}`).join(";");
3090
+ }
3091
+ function compactLine(item) {
3092
+ const app = notificationApp(item);
3093
+ const sender = notificationSender(item);
3094
+ return `[${item.timestamp}] ${app} · ${sender}: ${item.content}`;
3095
+ }
3096
+ function isAttentionCandidate(item) {
3097
+ const text = `${item.title}
3098
+ ${item.content}`.toLowerCase();
3099
+ return ATTENTION_KEYWORDS.some((keyword) => text.includes(keyword.toLowerCase()));
3100
+ }
3101
+ function buildExtractiveChunkSummary(chunk, notifications) {
3102
+ const byApp = new Map;
3103
+ const bySender = new Map;
3104
+ for (const item of notifications) {
3105
+ const app = notificationApp(item);
3106
+ const sender = notificationSender(item);
3107
+ increment2(byApp, app, { app });
3108
+ increment2(bySender, sender, { sender });
3109
+ }
3110
+ const latest = notifications.slice(0, 5).map(compactLine);
3111
+ const attention = notifications.filter(isAttentionCandidate).slice(0, 5).map(compactLine);
3112
+ const lines = [
3113
+ `## 分片 ${chunk.id}`,
3114
+ "",
3115
+ `- 数量: ${chunk.count}`,
3116
+ `- 时间范围: ${chunk.range.oldest ?? "-"} 至 ${chunk.range.newest ?? "-"}`,
3117
+ `- 主要 App: ${formatTopRows(topRows2(byApp, 5), "app")}`,
3118
+ `- 主要发送人/会话: ${formatTopRows(topRows2(bySender, 8), "sender")}`,
3119
+ "",
3120
+ "### 可能需要关注",
3121
+ ...attention.length > 0 ? attention.map((line) => `- ${line}`) : ["- 无明显待办、风险或异常样例"],
3122
+ "",
3123
+ "### 最近样例",
3124
+ ...latest.length > 0 ? latest.map((line) => `- ${line}`) : ["- 无"],
3125
+ ""
3126
+ ];
3127
+ return lines.join(`
3128
+ `).trimEnd() + `
3129
+ `;
3130
+ }
3131
+ function nowIso() {
3132
+ return new Date().toISOString();
3133
+ }
3134
+ function createJobId() {
3135
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
3136
+ return `${stamp}-${import_node_crypto.randomUUID().slice(0, 8)}`;
3137
+ }
3138
+ function assertJobId(id) {
3139
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(id)) {
3140
+ exitError("INVALID_JOB_ID", "summary job id 不合法");
3141
+ }
3142
+ }
3143
+ function summaryJobsDir(notificationsDir) {
3144
+ return import_node_path6.join(notificationsDir, SUMMARY_ROOT, JOBS_DIR);
3145
+ }
3146
+ function summaryJobDir(notificationsDir, id) {
3147
+ assertJobId(id);
3148
+ return import_node_path6.join(summaryJobsDir(notificationsDir), id);
3149
+ }
3150
+ function metaPath(jobDir) {
3151
+ return import_node_path6.join(jobDir, "meta.json");
3152
+ }
3153
+ function chunkPath(jobDir, chunkId) {
3154
+ return import_node_path6.join(jobDir, CHUNKS_DIR, `${chunkId}.json`);
3155
+ }
3156
+ function writeJson(path, value) {
3157
+ import_node_fs6.writeFileSync(path, JSON.stringify(value, null, 2), "utf-8");
3158
+ }
3159
+ function readJson(path) {
3160
+ try {
3161
+ return JSON.parse(import_node_fs6.readFileSync(path, "utf-8"));
3162
+ } catch {
3163
+ exitError("READ_FAILED", `无法读取 JSON: ${path}`);
3164
+ }
3165
+ }
3166
+ function readMeta(notificationsDir, id) {
3167
+ const dir = summaryJobDir(notificationsDir, id);
3168
+ const path = metaPath(dir);
3169
+ if (!import_node_fs6.existsSync(path)) {
3170
+ exitError("JOB_NOT_FOUND", `summary job 不存在: ${id}`);
3171
+ }
3172
+ const meta = readJson(path);
3173
+ if (!isSummaryJobMeta(meta)) {
3174
+ exitError("INVALID_JOB", `summary job 元数据损坏: ${id}`);
3175
+ }
3176
+ return meta;
3177
+ }
3178
+ function saveMeta(notificationsDir, meta) {
3179
+ meta.updatedAt = nowIso();
3180
+ writeJson(metaPath(summaryJobDir(notificationsDir, meta.id)), meta);
3181
+ }
3182
+ function isSummaryJobMeta(value) {
3183
+ if (!value || typeof value !== "object")
3184
+ return false;
3185
+ const obj = value;
3186
+ return obj.schemaVersion === 1 && typeof obj.id === "string" && Array.isArray(obj.chunks) && typeof obj.total === "number";
3187
+ }
3188
+ function refreshStatus(meta) {
3189
+ if (meta.status === "cancelled" || meta.status === "complete") {
3190
+ return meta.status;
3191
+ }
3192
+ if (meta.chunks.every((chunk) => chunk.status === "done")) {
3193
+ return "ready";
3194
+ }
3195
+ if (meta.chunks.some((chunk) => chunk.status === "in_progress")) {
3196
+ return "in_progress";
3197
+ }
3198
+ return "pending";
3199
+ }
3200
+ function progressFor(meta) {
3201
+ const doneChunks = meta.chunks.filter((chunk) => chunk.status === "done");
3202
+ const doneNotifications = doneChunks.reduce((sum, chunk) => sum + chunk.count, 0);
3203
+ return {
3204
+ totalChunks: meta.chunks.length,
3205
+ doneChunks: doneChunks.length,
3206
+ totalNotifications: meta.total,
3207
+ doneNotifications,
3208
+ remainingChunks: meta.chunks.length - doneChunks.length
3209
+ };
3210
+ }
3211
+ function toPublicStatus(meta) {
3212
+ return {
3213
+ ok: true,
3214
+ id: meta.id,
3215
+ status: meta.status,
3216
+ createdAt: meta.createdAt,
3217
+ updatedAt: meta.updatedAt,
3218
+ query: meta.query,
3219
+ total: meta.total,
3220
+ chunkSize: meta.chunkSize,
3221
+ range: meta.range,
3222
+ scan: meta.scan,
3223
+ progress: progressFor(meta),
3224
+ resultFile: meta.resultFile ?? null,
3225
+ chunks: meta.chunks.map((chunk) => ({
3226
+ id: chunk.id,
3227
+ index: chunk.index,
3228
+ count: chunk.count,
3229
+ status: chunk.status,
3230
+ range: chunk.range,
3231
+ summaryFile: chunk.summaryFile ?? null
3232
+ }))
3233
+ };
3234
+ }
3235
+ function readChunkNotifications(jobDir, chunkId) {
3236
+ const chunk = readJson(chunkPath(jobDir, chunkId));
3237
+ if (!chunk || typeof chunk !== "object" || !Array.isArray(chunk.notifications)) {
3238
+ exitError("INVALID_CHUNK", `summary job 分片损坏: ${chunkId}`);
3239
+ }
3240
+ return chunk.notifications;
3241
+ }
3242
+ function buildCommitCommand(jobId, chunkId) {
3243
+ return `ntf summary-job commit ${jobId} --chunk-id ${chunkId} --summary-file <path>`;
3244
+ }
3245
+ function buildResultCommand(jobId) {
3246
+ return `ntf summary-job result ${jobId}`;
3247
+ }
3248
+ function resolveSummaryText(opts) {
3249
+ const hasSummary = typeof opts.summary === "string" && opts.summary.length > 0;
3250
+ const hasSummaryFile = typeof opts.summaryFile === "string" && opts.summaryFile.length > 0;
3251
+ if (hasSummary && hasSummaryFile) {
3252
+ exitError("INVALID_COMMIT", "--summary 和 --summary-file 只能二选一");
3253
+ }
3254
+ if (!hasSummary && !hasSummaryFile) {
3255
+ exitError("INVALID_COMMIT", "必须提供 --summary 或 --summary-file");
3256
+ }
3257
+ const text = hasSummary ? opts.summary : import_node_fs6.readFileSync(import_node_path6.resolve(opts.summaryFile), "utf-8");
3258
+ if (!text.trim()) {
3259
+ exitError("INVALID_COMMIT", "分片摘要不能为空");
3260
+ }
3261
+ return text.trimEnd() + `
3262
+ `;
3263
+ }
3264
+ function buildResultMarkdown(meta, jobDir) {
3265
+ const lines = [
3266
+ `# 通知总结任务 ${meta.id}`,
3267
+ "",
3268
+ `- 通知数: ${meta.total}`,
3269
+ `- 时间范围: ${meta.range.oldest ?? "-"} 至 ${meta.range.newest ?? "-"}`,
3270
+ `- 分片数: ${meta.chunks.length}`,
3271
+ "",
3272
+ "## 分片摘要",
3273
+ ""
3274
+ ];
3275
+ for (const chunk of meta.chunks) {
3276
+ lines.push(`### ${chunk.id} · ${chunk.count} 条 · ${chunk.range.oldest ?? "-"} 至 ${chunk.range.newest ?? "-"}`, "");
3277
+ if (!chunk.summaryFile) {
3278
+ lines.push("(未提交摘要)", "");
3279
+ continue;
3280
+ }
3281
+ lines.push(import_node_fs6.readFileSync(import_node_path6.join(jobDir, chunk.summaryFile), "utf-8").trim(), "");
3282
+ }
3283
+ return lines.join(`
3284
+ `).trimEnd() + `
3285
+ `;
3286
+ }
3287
+ function writeChunkSummary(notificationsDir, meta, chunk, text) {
3288
+ const jobDir = summaryJobDir(notificationsDir, meta.id);
3289
+ const relativeSummaryFile = import_node_path6.join(SUMMARIES_DIR, `${chunk.id}.md`);
3290
+ import_node_fs6.writeFileSync(import_node_path6.join(jobDir, relativeSummaryFile), text, "utf-8");
3291
+ chunk.status = "done";
3292
+ chunk.completedAt = nowIso();
3293
+ chunk.summaryFile = relativeSummaryFile;
3294
+ }
3295
+ function finalizeResultIfReady(notificationsDir, meta) {
3296
+ meta.status = refreshStatus(meta);
3297
+ if (meta.status !== "ready" && meta.status !== "complete") {
3298
+ return null;
3299
+ }
3300
+ const jobDir = summaryJobDir(notificationsDir, meta.id);
3301
+ const markdown = buildResultMarkdown(meta, jobDir);
3302
+ import_node_fs6.writeFileSync(import_node_path6.join(jobDir, RESULT_FILE), markdown, "utf-8");
3303
+ meta.status = "complete";
3304
+ meta.resultFile = RESULT_FILE;
3305
+ return markdown;
3306
+ }
3307
+ function registerNtfSummaryJob(ntf, ctx) {
3308
+ const summaryJob = ntf.command("summary-job").description("分片通知总结任务管理");
3309
+ summaryJob.command("create").description("创建分片通知总结任务").option("--from <time>", "开始时间 ISO 8601").option("--to <time>", "结束时间 ISO 8601").option("--app <name>", "按应用名过滤").option("--sender <name>", "按发送人过滤").option("--conversation-type <type>", "按会话类型过滤(group/private)").option("--keyword <text>", "在标题和内容中搜索关键词").option("--limit <n>", "纳入任务的最大通知条数", String(DEFAULT_JOB_LIMIT)).option("--chunk-size <n>", "每个分片的通知条数", String(DEFAULT_CHUNK_SIZE)).option("--max-content <n>", "分片中单条通知标题/正文最大字数", String(DEFAULT_MAX_CONTENT)).action(async (opts) => {
3310
+ const dir = resolveNotificationsDir(ctx);
3311
+ if (!dir)
3312
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3313
+ progress("正在创建分片通知总结任务...");
3314
+ const query = parseNotificationQueryOptions(opts, DEFAULT_JOB_LIMIT);
3315
+ const chunkSize = parsePositiveIntegerOption(opts.chunkSize, DEFAULT_CHUNK_SIZE, "--chunk-size", "INVALID_CHUNK_SIZE");
3316
+ const maxContent = parsePositiveIntegerOption(opts.maxContent, DEFAULT_MAX_CONTENT, "--max-content", "INVALID_MAX_CONTENT");
3317
+ const startedAtMs = Date.now();
3318
+ const queryResult = await collectMatchingNotificationsWithStats(dir, query);
3319
+ const elapsedMs = Date.now() - startedAtMs;
3320
+ const notifications = queryResult.notifications;
3321
+ const id = createJobId();
3322
+ const createdAt = nowIso();
3323
+ const jobDir = summaryJobDir(dir, id);
3324
+ import_node_fs6.mkdirSync(import_node_path6.join(jobDir, CHUNKS_DIR), { recursive: true });
3325
+ import_node_fs6.mkdirSync(import_node_path6.join(jobDir, SUMMARIES_DIR), { recursive: true });
3326
+ const chunks = [];
3327
+ for (let start = 0;start < notifications.length; start += chunkSize) {
3328
+ const rawChunk = notifications.slice(start, start + chunkSize);
3329
+ const chunkNotifications = rawChunk.map((item) => compactNotification2(item, maxContent));
3330
+ const index = chunks.length;
3331
+ const chunkId = String(index + 1).padStart(4, "0");
3332
+ const end = start + chunkNotifications.length - 1;
3333
+ writeJson(chunkPath(jobDir, chunkId), {
3334
+ id: chunkId,
3335
+ index,
3336
+ start,
3337
+ end,
3338
+ notifications: chunkNotifications
3339
+ });
3340
+ chunks.push({
3341
+ id: chunkId,
3342
+ index,
3343
+ start,
3344
+ end,
3345
+ count: chunkNotifications.length,
3346
+ status: "pending",
3347
+ range: {
3348
+ newest: chunkNotifications[0]?.timestamp ?? null,
3349
+ oldest: chunkNotifications[chunkNotifications.length - 1]?.timestamp ?? null
3350
+ }
3351
+ });
3352
+ }
3353
+ const meta = {
3354
+ schemaVersion: 1,
3355
+ id,
3356
+ status: chunks.length === 0 ? "ready" : "pending",
3357
+ createdAt,
3358
+ updatedAt: createdAt,
3359
+ query: {
3360
+ ...query.from ? { from: query.from } : {},
3361
+ ...query.to ? { to: query.to } : {},
3362
+ ...query.app ? { app: query.app } : {},
3363
+ ...query.sender ? { sender: query.sender } : {},
3364
+ ...query.conversationType ? { conversationType: query.conversationType } : {},
3365
+ ...query.keyword ? { keyword: query.keyword } : {},
3366
+ limit: query.limit
3367
+ },
3368
+ chunkSize,
3369
+ maxContent,
3370
+ total: notifications.length,
3371
+ range: {
3372
+ newest: notifications[0]?.timestamp ?? null,
3373
+ oldest: notifications[notifications.length - 1]?.timestamp ?? null
3374
+ },
3375
+ scan: {
3376
+ daysScanned: queryResult.stats.daysScanned,
3377
+ itemsScanned: queryResult.stats.itemsScanned,
3378
+ matched: queryResult.stats.matched,
3379
+ stoppedAfterLimit: queryResult.stats.stoppedAfterLimit,
3380
+ elapsedMs
3381
+ },
3382
+ chunks
3383
+ };
3384
+ writeJson(metaPath(jobDir), meta);
3385
+ output({
3386
+ ...toPublicStatus(meta),
3387
+ nextCommand: `ntf summary-job next ${id}`,
3388
+ resultCommand: buildResultCommand(id)
3389
+ });
3390
+ });
3391
+ summaryJob.command("status <id>").description("查看分片通知总结任务状态").action((id) => {
3392
+ const dir = resolveNotificationsDir(ctx);
3393
+ if (!dir)
3394
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3395
+ const meta = readMeta(dir, id);
3396
+ meta.status = refreshStatus(meta);
3397
+ saveMeta(dir, meta);
3398
+ output(toPublicStatus(meta));
3399
+ });
3400
+ summaryJob.command("next <id>").description("领取或重试下一个待总结分片").action((id) => {
3401
+ const dir = resolveNotificationsDir(ctx);
3402
+ if (!dir)
3403
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3404
+ const meta = readMeta(dir, id);
3405
+ if (meta.status === "cancelled") {
3406
+ exitError("JOB_CANCELLED", `summary job 已取消: ${id}`);
3407
+ }
3408
+ const jobDir = summaryJobDir(dir, id);
3409
+ let chunk = meta.chunks.find((item) => item.status === "in_progress");
3410
+ if (!chunk) {
3411
+ chunk = meta.chunks.find((item) => item.status === "pending");
3412
+ if (chunk) {
3413
+ chunk.status = "in_progress";
3414
+ chunk.claimedAt = nowIso();
3415
+ }
3416
+ }
3417
+ if (!chunk) {
3418
+ meta.status = refreshStatus(meta);
3419
+ saveMeta(dir, meta);
3420
+ output({
3421
+ ok: true,
3422
+ id,
3423
+ done: true,
3424
+ status: meta.status,
3425
+ progress: progressFor(meta),
3426
+ resultCommand: buildResultCommand(id)
3427
+ });
3428
+ return;
3429
+ }
3430
+ meta.status = refreshStatus(meta);
3431
+ saveMeta(dir, meta);
3432
+ output({
3433
+ ok: true,
3434
+ id,
3435
+ done: false,
3436
+ status: meta.status,
3437
+ chunk: {
3438
+ id: chunk.id,
3439
+ index: chunk.index,
3440
+ start: chunk.start,
3441
+ end: chunk.end,
3442
+ count: chunk.count,
3443
+ range: chunk.range
3444
+ },
3445
+ progress: progressFor(meta),
3446
+ commitCommand: buildCommitCommand(id, chunk.id),
3447
+ notifications: readChunkNotifications(jobDir, chunk.id)
3448
+ });
3449
+ });
3450
+ summaryJob.command("commit <id>").description("提交分片摘要并标记分片完成").requiredOption("--chunk-id <id>", "next 返回的分片 id").option("--summary <text>", "直接传入分片摘要文本").option("--summary-file <path>", "读取文件内容作为分片摘要").action((id, opts) => {
3451
+ const dir = resolveNotificationsDir(ctx);
3452
+ if (!dir)
3453
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3454
+ const meta = readMeta(dir, id);
3455
+ if (meta.status === "cancelled") {
3456
+ exitError("JOB_CANCELLED", `summary job 已取消: ${id}`);
3457
+ }
3458
+ const chunk = meta.chunks.find((item) => item.id === opts.chunkId);
3459
+ if (!chunk) {
3460
+ exitError("CHUNK_NOT_FOUND", `summary job 分片不存在: ${opts.chunkId}`);
3461
+ }
3462
+ const text = resolveSummaryText(opts);
3463
+ writeChunkSummary(dir, meta, chunk, text);
3464
+ meta.status = refreshStatus(meta);
3465
+ saveMeta(dir, meta);
3466
+ output({
3467
+ ok: true,
3468
+ id,
3469
+ chunkId: chunk.id,
3470
+ status: meta.status,
3471
+ progress: progressFor(meta),
3472
+ nextCommand: meta.status === "ready" ? null : `ntf summary-job next ${id}`,
3473
+ resultCommand: meta.status === "ready" ? buildResultCommand(id) : null
3474
+ });
3475
+ });
3476
+ summaryJob.command("run <id>").description("自动处理待总结分片并在完成后生成结果").option("--max-chunks <n>", "本次最多自动处理的分片数", String(DEFAULT_RUN_MAX_CHUNKS)).option("--include-result", "完成时在输出中包含 markdown 结果").action((id, opts) => {
3477
+ const dir = resolveNotificationsDir(ctx);
3478
+ if (!dir)
3479
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3480
+ const meta = readMeta(dir, id);
3481
+ if (meta.status === "cancelled") {
3482
+ exitError("JOB_CANCELLED", `summary job 已取消: ${id}`);
3483
+ }
3484
+ const maxChunks = parsePositiveIntegerOption(opts.maxChunks, DEFAULT_RUN_MAX_CHUNKS, "--max-chunks", "INVALID_MAX_CHUNKS");
3485
+ const jobDir = summaryJobDir(dir, id);
3486
+ const processed = [];
3487
+ for (const chunk of meta.chunks) {
3488
+ if (processed.length >= maxChunks)
3489
+ break;
3490
+ if (chunk.status === "done")
3491
+ continue;
3492
+ chunk.status = "in_progress";
3493
+ chunk.claimedAt = nowIso();
3494
+ const notifications = readChunkNotifications(jobDir, chunk.id);
3495
+ const summary = buildExtractiveChunkSummary(chunk, notifications);
3496
+ writeChunkSummary(dir, meta, chunk, summary);
3497
+ processed.push({ chunkId: chunk.id, count: chunk.count });
3498
+ }
3499
+ const markdown = finalizeResultIfReady(dir, meta);
3500
+ if (!markdown) {
3501
+ meta.status = refreshStatus(meta);
3502
+ }
3503
+ saveMeta(dir, meta);
3504
+ output({
3505
+ ok: true,
3506
+ id,
3507
+ status: meta.status,
3508
+ processed,
3509
+ progress: progressFor(meta),
3510
+ nextCommand: meta.status === "complete" ? null : `ntf summary-job run ${id}`,
3511
+ resultCommand: meta.status === "complete" ? buildResultCommand(id) : null,
3512
+ resultFile: meta.resultFile ?? null,
3513
+ ...opts.includeResult && markdown ? { markdown } : {}
3514
+ });
3515
+ });
3516
+ summaryJob.command("result <id>").description("合并已提交的分片摘要并返回结果").action((id) => {
3517
+ const dir = resolveNotificationsDir(ctx);
3518
+ if (!dir)
3519
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3520
+ const meta = readMeta(dir, id);
3521
+ meta.status = refreshStatus(meta);
3522
+ if (meta.status !== "ready" && meta.status !== "complete") {
3523
+ saveMeta(dir, meta);
3524
+ output({
3525
+ ok: true,
3526
+ id,
3527
+ done: false,
3528
+ status: meta.status,
3529
+ progress: progressFor(meta),
3530
+ nextCommand: `ntf summary-job next ${id}`
3531
+ });
3532
+ return;
3533
+ }
3534
+ const jobDir = summaryJobDir(dir, id);
3535
+ const markdown = finalizeResultIfReady(dir, meta) ?? buildResultMarkdown(meta, jobDir);
3536
+ saveMeta(dir, meta);
3537
+ output({
3538
+ ok: true,
3539
+ id,
3540
+ done: true,
3541
+ status: meta.status,
3542
+ resultFile: RESULT_FILE,
3543
+ markdown
3544
+ });
3545
+ });
3546
+ summaryJob.command("cancel <id>").description("取消分片通知总结任务").action((id) => {
3547
+ const dir = resolveNotificationsDir(ctx);
3548
+ if (!dir)
3549
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3550
+ const meta = readMeta(dir, id);
3551
+ meta.status = "cancelled";
3552
+ saveMeta(dir, meta);
3553
+ output({
3554
+ ok: true,
3555
+ id,
3556
+ status: meta.status,
3557
+ progress: progressFor(meta)
3558
+ });
3559
+ });
3560
+ }
3561
+
2959
3562
  // src/cli/ntf-stats.ts
2960
3563
  function registerNtfStats(ntf, ctx) {
2961
3564
  ntf.command("stats").description("通知统计分析(按日期/应用/发送人/时段聚合)").option("--from <date>", "开始日期 YYYY-MM-DD", daysAgo(7)).option("--to <date>", "结束日期 YYYY-MM-DD", today()).option("--app <name>", "只统计指定应用").option("--dim <dimension>", "统计维度:date/app/sender/hour/all", "all").action((opts) => {
@@ -3013,24 +3616,25 @@ function registerNtfStats(ntf, ctx) {
3013
3616
  }
3014
3617
 
3015
3618
  // src/cli/ntf-sync.ts
3016
- var import_node_fs6 = require("node:fs");
3017
- var import_node_path6 = require("node:path");
3619
+ var import_node_child_process = require("node:child_process");
3620
+ var import_node_fs7 = require("node:fs");
3621
+ var import_node_path7 = require("node:path");
3018
3622
  var SYNC_FETCH_LIMIT = 100;
3019
3623
  function checkpointPath(dir) {
3020
- return import_node_path6.join(dir, ".checkpoint.json");
3624
+ return import_node_path7.join(dir, ".checkpoint.json");
3021
3625
  }
3022
3626
  function readCheckpoint(dir) {
3023
3627
  const p = checkpointPath(dir);
3024
- if (!import_node_fs6.existsSync(p))
3628
+ if (!import_node_fs7.existsSync(p))
3025
3629
  return {};
3026
3630
  try {
3027
- return JSON.parse(import_node_fs6.readFileSync(p, "utf-8"));
3631
+ return JSON.parse(import_node_fs7.readFileSync(p, "utf-8"));
3028
3632
  } catch {
3029
3633
  return {};
3030
3634
  }
3031
3635
  }
3032
3636
  function writeCheckpoint(dir, data) {
3033
- import_node_fs6.writeFileSync(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
3637
+ import_node_fs7.writeFileSync(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
3034
3638
  }
3035
3639
  function validateDateKey(value, optionName) {
3036
3640
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
@@ -3081,6 +3685,28 @@ function isDateInScope(dateKey, scope) {
3081
3685
  return false;
3082
3686
  return true;
3083
3687
  }
3688
+ var MEMORY_PLUGIN_KEYWORD = "memory-lancedb-ultra";
3689
+ function detectMemoryBackend() {
3690
+ try {
3691
+ const out = import_node_child_process.execFileSync("openclaw", ["plugins", "list"], {
3692
+ encoding: "utf-8",
3693
+ timeout: 15000,
3694
+ stdio: ["ignore", "pipe", "ignore"]
3695
+ });
3696
+ const matched = out.split(`
3697
+ `).filter((line) => line.toLowerCase().includes(MEMORY_PLUGIN_KEYWORD));
3698
+ if (matched.length > 0 && !matched.some((line) => /disabled/i.test(line))) {
3699
+ return {
3700
+ backend: "lancedb-plugin",
3701
+ promptFile: "skills/notification-to-memory/references/write-memory-lancedb-plugin.md"
3702
+ };
3703
+ }
3704
+ } catch {}
3705
+ return {
3706
+ backend: "openclaw-native",
3707
+ promptFile: "skills/notification-to-memory/references/write-memory-openclaw-native.md"
3708
+ };
3709
+ }
3084
3710
  function registerNtfSync(ntf, ctx) {
3085
3711
  const sync = ntf.command("sync").description("同步通知到记忆系统");
3086
3712
  sync.command("scan").description("扫描未处理的通知,默认只返回本地当天的待同步摘要").option("--all", "扫描 checkpoint 之后所有日期的未处理通知").option("--date <date>", "只扫描指定日期 YYYY-MM-DD").option("--from-date <date>", "只扫描该日期及之后的数据 YYYY-MM-DD").option("--to-date <date>", "只扫描该日期及之前的数据 YYYY-MM-DD").action((opts) => {
@@ -3117,6 +3743,67 @@ function registerNtfSync(ntf, ctx) {
3117
3743
  allDatesTotalPending: totalPending + outsideScopePending
3118
3744
  });
3119
3745
  });
3746
+ sync.command("next").description("返回范围内下一批待同步通知(≤100 条)及记忆写入提示词路径;全部处理完时返回 done=true").option("--all", "处理 checkpoint 之后所有日期的未处理通知").option("--date <date>", "只处理指定日期 YYYY-MM-DD").option("--from-date <date>", "只处理该日期及之后的数据 YYYY-MM-DD").option("--to-date <date>", "只处理该日期及之前的数据 YYYY-MM-DD").action((opts) => {
3747
+ const dir = resolveNotificationsDir(ctx);
3748
+ if (!dir)
3749
+ exitError("STORAGE_UNAVAILABLE", "通知存储目录不可用");
3750
+ const scope = resolveScanScope(opts);
3751
+ const checkpoint = readCheckpoint(dir);
3752
+ const keys = listDateKeys(dir);
3753
+ let totalPending = 0;
3754
+ let outsideScopePending = 0;
3755
+ let nextDate = null;
3756
+ let nextItems = [];
3757
+ let nextStartIndex = 0;
3758
+ for (const dateKey of keys) {
3759
+ const items = readDateFile(dir, dateKey);
3760
+ const lastIndex = checkpoint[dateKey]?.lastIndex ?? -1;
3761
+ const unprocessed = items.length - (lastIndex + 1);
3762
+ if (unprocessed <= 0)
3763
+ continue;
3764
+ if (!isDateInScope(dateKey, scope)) {
3765
+ outsideScopePending += unprocessed;
3766
+ continue;
3767
+ }
3768
+ totalPending += unprocessed;
3769
+ if (!nextDate) {
3770
+ nextDate = dateKey;
3771
+ nextItems = items;
3772
+ nextStartIndex = lastIndex + 1;
3773
+ }
3774
+ }
3775
+ if (!nextDate) {
3776
+ output({
3777
+ ok: true,
3778
+ done: true,
3779
+ scope,
3780
+ totalPending: 0,
3781
+ outsideScopePending,
3782
+ allDatesTotalPending: outsideScopePending
3783
+ });
3784
+ return;
3785
+ }
3786
+ const notifications = nextItems.slice(nextStartIndex, nextStartIndex + SYNC_FETCH_LIMIT);
3787
+ const endIndex = nextStartIndex + notifications.length - 1;
3788
+ const unprocessedInDate = nextItems.length - nextStartIndex;
3789
+ const memory = detectMemoryBackend();
3790
+ output({
3791
+ ok: true,
3792
+ done: false,
3793
+ scope,
3794
+ date: nextDate,
3795
+ startIndex: nextStartIndex,
3796
+ endIndex,
3797
+ returned: notifications.length,
3798
+ hasMoreInDate: unprocessedInDate > notifications.length,
3799
+ remainingInScope: totalPending - notifications.length,
3800
+ outsideScopePending,
3801
+ memoryBackend: memory.backend,
3802
+ memoryPromptFile: memory.promptFile,
3803
+ commitCommand: `ntf sync commit --date ${nextDate} --end-index ${endIndex}`,
3804
+ notifications
3805
+ });
3806
+ });
3120
3807
  sync.command("fetch").description("获取指定日期的未处理通知详情").requiredOption("--date <date>", "目标日期 YYYY-MM-DD").option("--max-end-index <index>", "本次同步快照允许读取的最大 endIndex").action((opts) => {
3121
3808
  const dir = resolveNotificationsDir(ctx);
3122
3809
  if (!dir)
@@ -3218,8 +3905,8 @@ function registerNtfSync(ntf, ctx) {
3218
3905
  }
3219
3906
 
3220
3907
  // src/cli/ntf-monitor.ts
3221
- var import_node_fs7 = require("node:fs");
3222
- var import_node_path7 = require("node:path");
3908
+ var import_node_fs8 = require("node:fs");
3909
+ var import_node_path8 = require("node:path");
3223
3910
 
3224
3911
  // src/monitor/fetch-gen.ts
3225
3912
  function generateFetchPy(name, matchRules) {
@@ -3306,20 +3993,20 @@ function tasksDir(ctx) {
3306
3993
  const base = ctx.workspaceDir || ctx.stateDir;
3307
3994
  if (!base)
3308
3995
  throw new Error("workspaceDir and stateDir both unavailable");
3309
- return import_node_path7.join(base, "tasks");
3996
+ return import_node_path8.join(base, "tasks");
3310
3997
  }
3311
- function readMeta(taskDir) {
3312
- const metaPath = import_node_path7.join(taskDir, "meta.json");
3313
- if (!import_node_fs7.existsSync(metaPath))
3998
+ function readMeta2(taskDir) {
3999
+ const metaPath2 = import_node_path8.join(taskDir, "meta.json");
4000
+ if (!import_node_fs8.existsSync(metaPath2))
3314
4001
  return null;
3315
4002
  try {
3316
- return JSON.parse(import_node_fs7.readFileSync(metaPath, "utf-8"));
4003
+ return JSON.parse(import_node_fs8.readFileSync(metaPath2, "utf-8"));
3317
4004
  } catch {
3318
4005
  return null;
3319
4006
  }
3320
4007
  }
3321
4008
  function writeMeta(taskDir, meta) {
3322
- import_node_fs7.writeFileSync(import_node_path7.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
4009
+ import_node_fs8.writeFileSync(import_node_path8.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
3323
4010
  }
3324
4011
  function generateReadme(name, description) {
3325
4012
  return `# Monitor Task: ${name}
@@ -3338,30 +4025,30 @@ function registerNtfMonitor(ntf, ctx) {
3338
4025
  const monitor = ntf.command("monitor").description("通知监控任务管理");
3339
4026
  monitor.command("list").description("列出所有监控任务").action(() => {
3340
4027
  const dir = tasksDir(ctx);
3341
- if (!import_node_fs7.existsSync(dir)) {
4028
+ if (!import_node_fs8.existsSync(dir)) {
3342
4029
  output({ ok: true, tasks: [] });
3343
4030
  return;
3344
4031
  }
3345
4032
  const tasks = [];
3346
- for (const entry of import_node_fs7.readdirSync(dir, { withFileTypes: true })) {
4033
+ for (const entry of import_node_fs8.readdirSync(dir, { withFileTypes: true })) {
3347
4034
  if (!entry.isDirectory())
3348
4035
  continue;
3349
- const meta = readMeta(import_node_path7.join(dir, entry.name));
4036
+ const meta = readMeta2(import_node_path8.join(dir, entry.name));
3350
4037
  if (meta)
3351
4038
  tasks.push(meta);
3352
4039
  }
3353
4040
  output({ ok: true, tasks });
3354
4041
  });
3355
4042
  monitor.command("show <name>").description("查看监控任务详情").action((name) => {
3356
- const taskDir = import_node_path7.join(tasksDir(ctx), name);
3357
- const meta = readMeta(taskDir);
4043
+ const taskDir = import_node_path8.join(tasksDir(ctx), name);
4044
+ const meta = readMeta2(taskDir);
3358
4045
  if (!meta)
3359
4046
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
3360
- const checkpointPath2 = import_node_path7.join(taskDir, "checkpoint.json");
4047
+ const checkpointPath2 = import_node_path8.join(taskDir, "checkpoint.json");
3361
4048
  let checkpoint = {};
3362
- if (import_node_fs7.existsSync(checkpointPath2)) {
4049
+ if (import_node_fs8.existsSync(checkpointPath2)) {
3363
4050
  try {
3364
- checkpoint = JSON.parse(import_node_fs7.readFileSync(checkpointPath2, "utf-8"));
4051
+ checkpoint = JSON.parse(import_node_fs8.readFileSync(checkpointPath2, "utf-8"));
3365
4052
  } catch {}
3366
4053
  }
3367
4054
  output({
@@ -3374,8 +4061,8 @@ function registerNtfMonitor(ntf, ctx) {
3374
4061
  });
3375
4062
  monitor.command("create <name>").description("创建监控任务").requiredOption("--description <text>", "任务描述").requiredOption("--match-rules <json>", "匹配规则 JSON").requiredOption("--schedule <cron>", "cron 表达式").action((name, opts) => {
3376
4063
  const dir = tasksDir(ctx);
3377
- const taskDir = import_node_path7.join(dir, name);
3378
- if (import_node_fs7.existsSync(taskDir)) {
4064
+ const taskDir = import_node_path8.join(dir, name);
4065
+ if (import_node_fs8.existsSync(taskDir)) {
3379
4066
  exitError("ALREADY_EXISTS", `监控任务 '${name}' 已存在`);
3380
4067
  }
3381
4068
  let matchRules;
@@ -3384,7 +4071,7 @@ function registerNtfMonitor(ntf, ctx) {
3384
4071
  } catch {
3385
4072
  exitError("VALIDATION_FAILED", "match-rules 必须是合法的 JSON");
3386
4073
  }
3387
- import_node_fs7.mkdirSync(taskDir, { recursive: true });
4074
+ import_node_fs8.mkdirSync(taskDir, { recursive: true });
3388
4075
  const meta = {
3389
4076
  name,
3390
4077
  description: opts.description,
@@ -3394,8 +4081,8 @@ function registerNtfMonitor(ntf, ctx) {
3394
4081
  createdAt: new Date().toISOString()
3395
4082
  };
3396
4083
  writeMeta(taskDir, meta);
3397
- import_node_fs7.writeFileSync(import_node_path7.join(taskDir, "fetch.py"), generateFetchPy(name, matchRules), "utf-8");
3398
- import_node_fs7.writeFileSync(import_node_path7.join(taskDir, "README.md"), generateReadme(name, opts.description), "utf-8");
4084
+ import_node_fs8.writeFileSync(import_node_path8.join(taskDir, "fetch.py"), generateFetchPy(name, matchRules), "utf-8");
4085
+ import_node_fs8.writeFileSync(import_node_path8.join(taskDir, "README.md"), generateReadme(name, opts.description), "utf-8");
3399
4086
  output({
3400
4087
  ok: true,
3401
4088
  name,
@@ -3419,8 +4106,8 @@ function registerNtfMonitor(ntf, ctx) {
3419
4106
  });
3420
4107
  });
3421
4108
  monitor.command("delete <name>").description("删除监控任务").option("--yes", "跳过确认").action((name, opts) => {
3422
- const taskDir = import_node_path7.join(tasksDir(ctx), name);
3423
- if (!import_node_fs7.existsSync(taskDir)) {
4109
+ const taskDir = import_node_path8.join(tasksDir(ctx), name);
4110
+ if (!import_node_fs8.existsSync(taskDir)) {
3424
4111
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
3425
4112
  }
3426
4113
  if (!opts.yes) {
@@ -3433,7 +4120,7 @@ function registerNtfMonitor(ntf, ctx) {
3433
4120
  });
3434
4121
  process.exit(1);
3435
4122
  }
3436
- import_node_fs7.rmSync(taskDir, { recursive: true, force: true });
4123
+ import_node_fs8.rmSync(taskDir, { recursive: true, force: true });
3437
4124
  output({
3438
4125
  ok: true,
3439
4126
  name,
@@ -3445,8 +4132,8 @@ function registerNtfMonitor(ntf, ctx) {
3445
4132
  });
3446
4133
  });
3447
4134
  monitor.command("enable <name>").description("启用监控任务").action((name) => {
3448
- const taskDir = import_node_path7.join(tasksDir(ctx), name);
3449
- const meta = readMeta(taskDir);
4135
+ const taskDir = import_node_path8.join(tasksDir(ctx), name);
4136
+ const meta = readMeta2(taskDir);
3450
4137
  if (!meta)
3451
4138
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
3452
4139
  meta.enabled = true;
@@ -3454,8 +4141,8 @@ function registerNtfMonitor(ntf, ctx) {
3454
4141
  output({ ok: true, name, enabled: true });
3455
4142
  });
3456
4143
  monitor.command("disable <name>").description("暂停监控任务").action((name) => {
3457
- const taskDir = import_node_path7.join(tasksDir(ctx), name);
3458
- const meta = readMeta(taskDir);
4144
+ const taskDir = import_node_path8.join(tasksDir(ctx), name);
4145
+ const meta = readMeta2(taskDir);
3459
4146
  if (!meta)
3460
4147
  exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
3461
4148
  meta.enabled = false;
@@ -3465,7 +4152,7 @@ function registerNtfMonitor(ntf, ctx) {
3465
4152
  }
3466
4153
 
3467
4154
  // src/light/sender.ts
3468
- var import_node_crypto2 = require("node:crypto");
4155
+ var import_node_crypto3 = require("node:crypto");
3469
4156
 
3470
4157
  // src/light/repeat.ts
3471
4158
  function normalizeRepeatTimes(input) {
@@ -3971,9 +4658,9 @@ function quantizeWindow(value) {
3971
4658
  }
3972
4659
 
3973
4660
  // src/env.ts
3974
- var import_node_crypto = require("node:crypto");
3975
- var import_node_fs8 = require("node:fs");
3976
- var import_node_path8 = require("node:path");
4661
+ var import_node_crypto2 = require("node:crypto");
4662
+ var import_node_fs9 = require("node:fs");
4663
+ var import_node_path9 = require("node:path");
3977
4664
  function parseEnvContent(content) {
3978
4665
  const entries = new Map;
3979
4666
  const tokens = content.split(/\r?\n/).flatMap((line) => line.split(/(?<![A-Z0-9_])(?=[A-Z_][A-Z0-9_]*=)/));
@@ -3993,20 +4680,20 @@ function parseEnvContent(content) {
3993
4680
  }
3994
4681
  function writeDotEnv(key, value) {
3995
4682
  const path = resolveStateFile(".env");
3996
- import_node_fs8.mkdirSync(import_node_path8.dirname(path), { recursive: true });
3997
- const existing = import_node_fs8.existsSync(path) ? import_node_fs8.readFileSync(path, "utf-8") : "";
4683
+ import_node_fs9.mkdirSync(import_node_path9.dirname(path), { recursive: true });
4684
+ const existing = import_node_fs9.existsSync(path) ? import_node_fs9.readFileSync(path, "utf-8") : "";
3998
4685
  const entries = parseEnvContent(existing);
3999
4686
  entries.set(key, value);
4000
4687
  const output2 = Array.from(entries, ([k, v]) => `${k}=${v}`).join(`
4001
4688
  `) + `
4002
4689
  `;
4003
- const tmpPath = `${path}.tmp.${process.pid}.${import_node_crypto.randomBytes(4).toString("hex")}`;
4690
+ const tmpPath = `${path}.tmp.${process.pid}.${import_node_crypto2.randomBytes(4).toString("hex")}`;
4004
4691
  try {
4005
- import_node_fs8.writeFileSync(tmpPath, output2, { encoding: "utf-8", mode: 384 });
4006
- import_node_fs8.renameSync(tmpPath, path);
4692
+ import_node_fs9.writeFileSync(tmpPath, output2, { encoding: "utf-8", mode: 384 });
4693
+ import_node_fs9.renameSync(tmpPath, path);
4007
4694
  } catch (error) {
4008
4695
  try {
4009
- import_node_fs8.rmSync(tmpPath, { force: true });
4696
+ import_node_fs9.rmSync(tmpPath, { force: true });
4010
4697
  } catch {}
4011
4698
  throw error;
4012
4699
  }
@@ -4016,6 +4703,10 @@ var DEFAULT_ENV_HOSTS = {
4016
4703
  test: "openclaw-service-test.yoooclaw.com",
4017
4704
  production: "openclaw-service.yoooclaw.com"
4018
4705
  };
4706
+ var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH = "/api/plugin/notification-intelligence/light-rules";
4707
+ var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_APP_PATH = "/api/notification-intelligence/light-rules";
4708
+ var NOTIFICATION_INTELLIGENCE_PLUGIN_BASE_PATH = "/api/plugin/notification-intelligence";
4709
+ var NOTIFICATION_INTELLIGENCE_APP_BASE_PATH = "/api/notification-intelligence";
4019
4710
  function normalizeHost(host) {
4020
4711
  const trimmed = host?.trim();
4021
4712
  if (!trimmed)
@@ -4037,11 +4728,36 @@ function getEnvHost(env) {
4037
4728
  }
4038
4729
  return normalizeHost(host) ?? DEFAULT_ENV_HOSTS[env];
4039
4730
  }
4731
+ function normalizeNotificationIntelligenceLightRulesPluginUrl(url) {
4732
+ const trimmed = url.trim().replace(/\/+$/, "");
4733
+ if (!trimmed)
4734
+ return trimmed;
4735
+ if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH)) {
4736
+ return trimmed;
4737
+ }
4738
+ if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_RULES_APP_PATH)) {
4739
+ const origin = trimmed.slice(0, -NOTIFICATION_INTELLIGENCE_LIGHT_RULES_APP_PATH.length);
4740
+ return `${origin}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`;
4741
+ }
4742
+ if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_PLUGIN_BASE_PATH)) {
4743
+ return `${trimmed}/light-rules`;
4744
+ }
4745
+ if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_APP_BASE_PATH)) {
4746
+ const origin = trimmed.slice(0, -NOTIFICATION_INTELLIGENCE_APP_BASE_PATH.length);
4747
+ return `${origin}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`;
4748
+ }
4749
+ if (/^https?:\/\/[^/]+$/i.test(trimmed)) {
4750
+ return `${trimmed}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`;
4751
+ }
4752
+ return trimmed;
4753
+ }
4040
4754
  function buildEnvUrls(host) {
4041
4755
  const https = `https://${host}`;
4042
4756
  const wss = `wss://${host}`;
4757
+ const lightRulesUrl = normalizeNotificationIntelligenceLightRulesPluginUrl(process.env.NOTIFICATION_INTELLIGENCE_LIGHT_RULES_URL || `${https}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`);
4043
4758
  return {
4044
4759
  lightApiUrl: `${https}/api/message/tob/sendMessage`,
4760
+ notificationIntelligenceLightRulesPluginUrl: lightRulesUrl,
4045
4761
  relayTunnelUrl: `${wss}/message/messages/ws/plugin`,
4046
4762
  appNameMapUrl: `${https}/api/application-config/app-package/config-all`,
4047
4763
  modelProxyLongRecordingSubmitTaskUrl: `${https}/api/model-proxy/long-recording/submit-task`,
@@ -4057,9 +4773,9 @@ var VALID_ENVS = new Set([
4057
4773
  ]);
4058
4774
  function readDotEnv() {
4059
4775
  const path = resolveStateFile(".env");
4060
- if (!import_node_fs8.existsSync(path))
4776
+ if (!import_node_fs9.existsSync(path))
4061
4777
  return {};
4062
- return Object.fromEntries(parseEnvContent(import_node_fs8.readFileSync(path, "utf-8")));
4778
+ return Object.fromEntries(parseEnvContent(import_node_fs9.readFileSync(path, "utf-8")));
4063
4779
  }
4064
4780
  function readPersistedEnvName() {
4065
4781
  const fromDotEnv = readDotEnv()["PHONE_NOTIFICATIONS_ENV"]?.trim();
@@ -4111,7 +4827,7 @@ async function sendLightEffect(apiKey, segments, logger, repeatInput, reason, ti
4111
4827
  } catch (error) {
4112
4828
  return { ok: false, error: error?.message ?? String(error) };
4113
4829
  }
4114
- const bizUniqueId = import_node_crypto2.randomUUID();
4830
+ const bizUniqueId = import_node_crypto3.randomUUID();
4115
4831
  const requestBody = {
4116
4832
  appKey,
4117
4833
  bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", title: resolvedTitle, reason },
@@ -4177,8 +4893,8 @@ function registerLightSend(light) {
4177
4893
  }
4178
4894
 
4179
4895
  // src/cli/light-setup-tools.ts
4180
- var import_node_fs9 = require("node:fs");
4181
- var import_node_path9 = require("node:path");
4896
+ var import_node_fs10 = require("node:fs");
4897
+ var import_node_path10 = require("node:path");
4182
4898
 
4183
4899
  // src/light-rules/names.ts
4184
4900
  var LIGHT_RULE_GATEWAY_METHODS = {
@@ -4252,8 +4968,8 @@ function upsertToolsForPolicy(policy, tools) {
4252
4968
  function resolveConfigPath2() {
4253
4969
  return resolveConfigPath();
4254
4970
  }
4255
- var LIGHT_TOOLS = ["light_control", ...LIGHT_RULE_TOOL_NAME_LIST];
4256
- function upsertLightControlAlsoAllow(cfg) {
4971
+ var LIGHT_TOOLS = [...LIGHT_RULE_TOOL_NAME_LIST];
4972
+ function upsertLightRuleToolsAlsoAllow(cfg) {
4257
4973
  if (!isObject(cfg.tools))
4258
4974
  cfg.tools = {};
4259
4975
  const globalChanged = upsertToolsForPolicy(cfg.tools, LIGHT_TOOLS);
@@ -4272,24 +4988,24 @@ function upsertLightControlAlsoAllow(cfg) {
4272
4988
  return { globalChanged, mainAgentChanged };
4273
4989
  }
4274
4990
  function registerLightSetupTools(light) {
4275
- light.command("setup").description("自动放行 light_control(兼容 tools.allow / tools.alsoAllow)").action(() => {
4991
+ light.command("setup").description("自动放行云端灯效规则工具(兼容 tools.allow / tools.alsoAllow)").action(() => {
4276
4992
  const configPath = resolveConfigPath2();
4277
- if (!import_node_fs9.existsSync(configPath)) {
4993
+ if (!import_node_fs10.existsSync(configPath)) {
4278
4994
  exitError("CONFIG_NOT_FOUND", `未找到配置文件: ${configPath}`);
4279
4995
  }
4280
4996
  let cfg = {};
4281
4997
  try {
4282
- const raw = import_node_fs9.readFileSync(configPath, "utf-8");
4998
+ const raw = import_node_fs10.readFileSync(configPath, "utf-8");
4283
4999
  const parsed = JSON.parse(raw);
4284
5000
  if (isObject(parsed))
4285
5001
  cfg = parsed;
4286
5002
  } catch (err) {
4287
5003
  exitError("CONFIG_INVALID", `读取/解析配置失败: ${err?.message ?? String(err)}`);
4288
5004
  }
4289
- const result = upsertLightControlAlsoAllow(cfg);
5005
+ const result = upsertLightRuleToolsAlsoAllow(cfg);
4290
5006
  try {
4291
- import_node_fs9.mkdirSync(import_node_path9.dirname(configPath), { recursive: true });
4292
- import_node_fs9.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + `
5007
+ import_node_fs10.mkdirSync(import_node_path10.dirname(configPath), { recursive: true });
5008
+ import_node_fs10.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + `
4293
5009
  `, "utf-8");
4294
5010
  } catch (err) {
4295
5011
  exitError("WRITE_FAILED", `写入配置失败: ${err?.message ?? String(err)}`);
@@ -4308,10 +5024,10 @@ function registerLightSetupTools(light) {
4308
5024
  }
4309
5025
 
4310
5026
  // src/tunnel/status.ts
4311
- var import_node_fs10 = require("node:fs");
4312
- var import_node_path10 = require("node:path");
4313
- var TUNNEL_STATUS_REL_PATH = import_node_path10.join("plugins", "phone-notifications", "tunnel-status.json");
4314
- var TUNNEL_LOCK_REL_PATH = import_node_path10.join("plugins", "phone-notifications", "relay-tunnel.lock");
5027
+ var import_node_fs11 = require("node:fs");
5028
+ var import_node_path11 = require("node:path");
5029
+ var TUNNEL_STATUS_REL_PATH = import_node_path11.join("plugins", "phone-notifications", "tunnel-status.json");
5030
+ var TUNNEL_LOCK_REL_PATH = import_node_path11.join("plugins", "phone-notifications", "relay-tunnel.lock");
4315
5031
  function isTunnelState(value) {
4316
5032
  return value === "connected" || value === "connecting" || value === "disconnected" || value === "stopped";
4317
5033
  }
@@ -4360,9 +5076,9 @@ function staleStatusMessage(status, lock) {
4360
5076
  return `${prefix},但当前运行锁启动于 ${lock.startedAt},晚于状态时间,说明状态未随新进程刷新。请重启 openclaw 主进程。`;
4361
5077
  }
4362
5078
  function assessTunnelStatus(stateDir) {
4363
- const statusFilePath = import_node_path10.join(stateDir, TUNNEL_STATUS_REL_PATH);
4364
- const lockFilePath = import_node_path10.join(stateDir, TUNNEL_LOCK_REL_PATH);
4365
- if (!import_node_fs10.existsSync(statusFilePath)) {
5079
+ const statusFilePath = import_node_path11.join(stateDir, TUNNEL_STATUS_REL_PATH);
5080
+ const lockFilePath = import_node_path11.join(stateDir, TUNNEL_LOCK_REL_PATH);
5081
+ if (!import_node_fs11.existsSync(statusFilePath)) {
4366
5082
  return {
4367
5083
  status: null,
4368
5084
  issueCode: "STATUS_NOT_FOUND",
@@ -4370,7 +5086,7 @@ function assessTunnelStatus(stateDir) {
4370
5086
  statusFilePath,
4371
5087
  lockFilePath,
4372
5088
  lock: {
4373
- exists: import_node_fs10.existsSync(lockFilePath),
5089
+ exists: import_node_fs11.existsSync(lockFilePath),
4374
5090
  pid: null,
4375
5091
  startedAt: null,
4376
5092
  active: null
@@ -4379,7 +5095,7 @@ function assessTunnelStatus(stateDir) {
4379
5095
  }
4380
5096
  let rawStatus;
4381
5097
  try {
4382
- rawStatus = JSON.parse(import_node_fs10.readFileSync(statusFilePath, "utf-8"));
5098
+ rawStatus = JSON.parse(import_node_fs11.readFileSync(statusFilePath, "utf-8"));
4383
5099
  } catch {
4384
5100
  return {
4385
5101
  status: null,
@@ -4388,7 +5104,7 @@ function assessTunnelStatus(stateDir) {
4388
5104
  statusFilePath,
4389
5105
  lockFilePath,
4390
5106
  lock: {
4391
- exists: import_node_fs10.existsSync(lockFilePath),
5107
+ exists: import_node_fs11.existsSync(lockFilePath),
4392
5108
  pid: null,
4393
5109
  startedAt: null,
4394
5110
  active: null
@@ -4403,7 +5119,7 @@ function assessTunnelStatus(stateDir) {
4403
5119
  statusFilePath,
4404
5120
  lockFilePath,
4405
5121
  lock: {
4406
- exists: import_node_fs10.existsSync(lockFilePath),
5122
+ exists: import_node_fs11.existsSync(lockFilePath),
4407
5123
  pid: null,
4408
5124
  startedAt: null,
4409
5125
  active: null
@@ -4411,11 +5127,11 @@ function assessTunnelStatus(stateDir) {
4411
5127
  };
4412
5128
  }
4413
5129
  const status = rawStatus;
4414
- const lockExists = import_node_fs10.existsSync(lockFilePath);
5130
+ const lockExists = import_node_fs11.existsSync(lockFilePath);
4415
5131
  let lockInfo = null;
4416
5132
  if (lockExists) {
4417
5133
  try {
4418
- lockInfo = parseTunnelLockInfo(JSON.parse(import_node_fs10.readFileSync(lockFilePath, "utf-8")));
5134
+ lockInfo = parseTunnelLockInfo(JSON.parse(import_node_fs11.readFileSync(lockFilePath, "utf-8")));
4419
5135
  } catch {
4420
5136
  lockInfo = null;
4421
5137
  }
@@ -4518,12 +5234,12 @@ function registerNtfStoragePath(ntf, ctx) {
4518
5234
  }
4519
5235
 
4520
5236
  // src/cli/log-search.ts
4521
- var import_node_fs11 = require("node:fs");
4522
- var import_node_path11 = require("node:path");
5237
+ var import_node_fs12 = require("node:fs");
5238
+ var import_node_path12 = require("node:path");
4523
5239
  function resolveLogsDir(ctx) {
4524
5240
  if (ctx.stateDir) {
4525
- const dir = import_node_path11.join(ctx.stateDir, "plugins", "phone-notifications", "logs");
4526
- if (import_node_fs11.existsSync(dir))
5241
+ const dir = import_node_path12.join(ctx.stateDir, "plugins", "phone-notifications", "logs");
5242
+ if (import_node_fs12.existsSync(dir))
4527
5243
  return dir;
4528
5244
  }
4529
5245
  return null;
@@ -4531,7 +5247,7 @@ function resolveLogsDir(ctx) {
4531
5247
  function listLogDateKeys(dir) {
4532
5248
  const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
4533
5249
  const keys = [];
4534
- for (const entry of import_node_fs11.readdirSync(dir, { withFileTypes: true })) {
5250
+ for (const entry of import_node_fs12.readdirSync(dir, { withFileTypes: true })) {
4535
5251
  if (!entry.isFile())
4536
5252
  continue;
4537
5253
  const m = pattern.exec(entry.name);
@@ -4541,10 +5257,10 @@ function listLogDateKeys(dir) {
4541
5257
  return keys.sort().reverse();
4542
5258
  }
4543
5259
  function collectLogLines(dir, dateKey, keyword, limit, collected) {
4544
- const filePath = import_node_path11.join(dir, `${dateKey}.log`);
4545
- if (!import_node_fs11.existsSync(filePath))
5260
+ const filePath = import_node_path12.join(dir, `${dateKey}.log`);
5261
+ if (!import_node_fs12.existsSync(filePath))
4546
5262
  return;
4547
- const content = import_node_fs11.readFileSync(filePath, "utf-8");
5263
+ const content = import_node_fs12.readFileSync(filePath, "utf-8");
4548
5264
  const lowerKeyword = keyword?.toLowerCase();
4549
5265
  for (const line of content.split(`
4550
5266
  `)) {
@@ -4580,7 +5296,7 @@ function registerLogSearch(ntf, ctx) {
4580
5296
  }
4581
5297
 
4582
5298
  // src/cli/env.ts
4583
- var import_node_child_process = require("node:child_process");
5299
+ var import_node_child_process2 = require("node:child_process");
4584
5300
 
4585
5301
  // src/update/restart.ts
4586
5302
  var DEFAULT_RESTART_GRACE_MS = 1500;
@@ -4704,7 +5420,7 @@ function triggerGatewayReloadAfterEnvSwitch(deps = {}) {
4704
5420
  }
4705
5421
  const hostCli = deps.hostCli?.trim() || process.env.HOST_CLI?.trim() || "openclaw";
4706
5422
  const command = `${hostCli} gateway restart`;
4707
- const run = deps.spawnSync ?? import_node_child_process.spawnSync;
5423
+ const run = deps.spawnSync ?? import_node_child_process2.spawnSync;
4708
5424
  const result = run(hostCli, ["gateway", "restart"], {
4709
5425
  encoding: "utf-8",
4710
5426
  stdio: "pipe",
@@ -4758,11 +5474,11 @@ function registerEnvCli(ntf, deps = {}) {
4758
5474
  }
4759
5475
 
4760
5476
  // src/cli/doctor.ts
4761
- var import_node_fs15 = require("node:fs");
5477
+ var import_node_fs16 = require("node:fs");
4762
5478
  var import_node_readline = require("node:readline");
4763
5479
 
4764
5480
  // src/cli/doctor/check-dangerous-flags.ts
4765
- var import_node_fs12 = require("node:fs");
5481
+ var import_node_fs13 = require("node:fs");
4766
5482
  function isObject2(v) {
4767
5483
  return !!v && typeof v === "object" && !Array.isArray(v);
4768
5484
  }
@@ -4782,13 +5498,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
4782
5498
  detail: "这会关闭 Control UI 的设备身份验证,任何人都可以访问控制面板。",
4783
5499
  fixDescription: "设为 false",
4784
5500
  fix: () => {
4785
- const raw = import_node_fs12.readFileSync(configPath, "utf-8");
5501
+ const raw = import_node_fs13.readFileSync(configPath, "utf-8");
4786
5502
  const config = JSON.parse(raw);
4787
5503
  const gw = config.gateway;
4788
5504
  const cui = gw.controlUi;
4789
5505
  cui.dangerouslyDisableDeviceAuth = false;
4790
- import_node_fs12.copyFileSync(configPath, configPath + ".bak");
4791
- import_node_fs12.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
5506
+ import_node_fs13.copyFileSync(configPath, configPath + ".bak");
5507
+ import_node_fs13.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
4792
5508
  `, "utf-8");
4793
5509
  }
4794
5510
  };
@@ -4845,11 +5561,11 @@ function warnEmpty() {
4845
5561
  }
4846
5562
 
4847
5563
  // src/cli/doctor/check-state-dir-perms.ts
4848
- var import_node_fs13 = require("node:fs");
5564
+ var import_node_fs14 = require("node:fs");
4849
5565
  var checkStateDirPerms = ({ stateDir }) => {
4850
5566
  let mode;
4851
5567
  try {
4852
- mode = import_node_fs13.statSync(stateDir).mode;
5568
+ mode = import_node_fs14.statSync(stateDir).mode;
4853
5569
  } catch {
4854
5570
  return null;
4855
5571
  }
@@ -4864,7 +5580,7 @@ var checkStateDirPerms = ({ stateDir }) => {
4864
5580
  detail: "其他用户可以读取该目录下的凭证和配置文件。",
4865
5581
  fixDescription: "chmod 700 " + stateDir,
4866
5582
  fix: () => {
4867
- import_node_fs13.chmodSync(stateDir, 448);
5583
+ import_node_fs14.chmodSync(stateDir, 448);
4868
5584
  }
4869
5585
  };
4870
5586
  };
@@ -5073,16 +5789,16 @@ function pickNewestVersion(versions) {
5073
5789
  }
5074
5790
 
5075
5791
  // src/version.ts
5076
- var import_node_fs14 = require("node:fs");
5792
+ var import_node_fs15 = require("node:fs");
5077
5793
  function readBuildInjectedVersion() {
5078
5794
  if (false) {}
5079
- const version = "1.12.0".trim();
5795
+ const version = "1.12.1".trim();
5080
5796
  return version || undefined;
5081
5797
  }
5082
5798
  function readPluginVersionFromPackageJson() {
5083
5799
  try {
5084
5800
  const packageJsonUrl = new URL("../package.json", "file:///home/runner/work/openclaw-plugin/openclaw-plugin/src/version.ts");
5085
- const packageJson = JSON.parse(import_node_fs14.readFileSync(packageJsonUrl, "utf-8"));
5801
+ const packageJson = JSON.parse(import_node_fs15.readFileSync(packageJsonUrl, "utf-8"));
5086
5802
  const version = packageJson.version?.trim();
5087
5803
  return version || undefined;
5088
5804
  } catch {
@@ -5140,10 +5856,10 @@ function isObject5(v) {
5140
5856
  return !!v && typeof v === "object" && !Array.isArray(v);
5141
5857
  }
5142
5858
  function readConfig(configPath) {
5143
- if (!import_node_fs15.existsSync(configPath))
5859
+ if (!import_node_fs16.existsSync(configPath))
5144
5860
  return {};
5145
5861
  try {
5146
- const parsed = JSON.parse(import_node_fs15.readFileSync(configPath, "utf-8"));
5862
+ const parsed = JSON.parse(import_node_fs16.readFileSync(configPath, "utf-8"));
5147
5863
  return isObject5(parsed) ? parsed : {};
5148
5864
  } catch {
5149
5865
  return {};
@@ -5171,10 +5887,10 @@ function severityOrder(s) {
5171
5887
  }
5172
5888
  function confirm(question) {
5173
5889
  const rl = import_node_readline.createInterface({ input: process.stdin, output: process.stderr });
5174
- return new Promise((resolve) => {
5890
+ return new Promise((resolve2) => {
5175
5891
  rl.question(question, (answer) => {
5176
5892
  rl.close();
5177
- resolve(answer.trim().toLowerCase() === "y");
5893
+ resolve2(answer.trim().toLowerCase() === "y");
5178
5894
  });
5179
5895
  });
5180
5896
  }
@@ -5354,9 +6070,9 @@ function registerRecStoragePath(rec, ctx) {
5354
6070
 
5355
6071
  // src/cli/rec-setup.ts
5356
6072
  var import_node_readline2 = require("node:readline");
5357
- var import_node_fs16 = require("node:fs");
6073
+ var import_node_fs17 = require("node:fs");
5358
6074
  function ask(rl, question) {
5359
- return new Promise((resolve) => rl.question(question, resolve));
6075
+ return new Promise((resolve2) => rl.question(question, resolve2));
5360
6076
  }
5361
6077
  async function askChoice(rl, prompt, choices, defaultIdx = 0) {
5362
6078
  choices.forEach((c, i) => {
@@ -5455,9 +6171,9 @@ async function setupLocal(rl) {
5455
6171
  function registerRecSetup(rec, ctx) {
5456
6172
  rec.command("setup").description("交互式配置 ASR 转写参数,保存到本地配置文件").action(async () => {
5457
6173
  const configPath = resolveAsrConfigPath(ctx);
5458
- if (import_node_fs16.existsSync(configPath)) {
6174
+ if (import_node_fs17.existsSync(configPath)) {
5459
6175
  try {
5460
- const existing = JSON.parse(import_node_fs16.readFileSync(configPath, "utf-8"));
6176
+ const existing = JSON.parse(import_node_fs17.readFileSync(configPath, "utf-8"));
5461
6177
  process.stderr.write(`当前已有配置:mode = ${existing.mode}`);
5462
6178
  if (existing.updatedAt)
5463
6179
  process.stderr.write(`,更新于 ${existing.updatedAt}`);
@@ -5473,7 +6189,7 @@ function registerRecSetup(rec, ctx) {
5473
6189
  const modeIdx = await askChoice(rl, "选择模式", ["api(云端 model-proxy 长录音)", "local(本地 Whisper)"]);
5474
6190
  const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
5475
6191
  const stored = { ...config, updatedAt: new Date().toISOString() };
5476
- import_node_fs16.writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
6192
+ import_node_fs17.writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
5477
6193
  process.stderr.write(`
5478
6194
  ✓ 配置已保存到 ${configPath}
5479
6195
 
@@ -5485,10 +6201,93 @@ function registerRecSetup(rec, ctx) {
5485
6201
  });
5486
6202
  }
5487
6203
 
6204
+ // src/cli/image-list.ts
6205
+ function registerImageList(image, ctx) {
6206
+ image.command("list").description("列出所有图片(可按状态/来源应用过滤)").option("--status <status>", "按状态过滤(syncing|synced|sync_failed)").option("--app <app>", "按来源应用过滤").action((opts) => {
6207
+ const dir = resolveImagesDir(ctx);
6208
+ if (!dir)
6209
+ exitError("STORAGE_UNAVAILABLE", "图片存储目录不可用");
6210
+ let images = readImageIndex(dir);
6211
+ if (opts.status) {
6212
+ images = images.filter((e) => e.status === opts.status);
6213
+ }
6214
+ if (opts.app) {
6215
+ images = images.filter((e) => e.metadata.source_app === opts.app);
6216
+ }
6217
+ const items = images.map((e) => ({
6218
+ imageId: e.imageId,
6219
+ status: e.status,
6220
+ source_app: e.metadata.source_app ?? null,
6221
+ caption: e.metadata.caption ?? null,
6222
+ mime_type: e.metadata.mime_type ?? null,
6223
+ size_bytes: e.metadata.size_bytes ?? null,
6224
+ created_at: e.metadata.created_at,
6225
+ has_file: !!e.localFile,
6226
+ localFile: e.localFile ?? null,
6227
+ syncedAt: e.syncedAt ?? null,
6228
+ error: e.lastError ?? null
6229
+ }));
6230
+ output({ ok: true, total: items.length, images: items });
6231
+ });
6232
+ }
6233
+
6234
+ // src/cli/image-status.ts
6235
+ function registerImageStatus(image, ctx) {
6236
+ image.command("status <id>").description("查看单张图片详情").action((id) => {
6237
+ const dir = resolveImagesDir(ctx);
6238
+ if (!dir)
6239
+ exitError("STORAGE_UNAVAILABLE", "图片存储目录不可用");
6240
+ const entry = readImageIndex(dir).find((e) => e.imageId === id);
6241
+ if (!entry) {
6242
+ exitError("NOT_FOUND", `图片不存在: ${id}`);
6243
+ }
6244
+ output({
6245
+ ok: true,
6246
+ image: {
6247
+ imageId: entry.imageId,
6248
+ clientLabel: entry.clientLabel ?? null,
6249
+ status: entry.status,
6250
+ metadata: entry.metadata,
6251
+ localFile: entry.localFile ?? null,
6252
+ thumbnail: entry.thumbnail ?? null,
6253
+ syncedAt: entry.syncedAt ?? null,
6254
+ error: entry.lastError ?? null
6255
+ }
6256
+ });
6257
+ });
6258
+ }
6259
+
6260
+ // src/cli/image-path.ts
6261
+ function registerImagePath(image, ctx) {
6262
+ image.command("path <id>").description("打印图片本地文件绝对路径").action((id) => {
6263
+ const dir = resolveImagesDir(ctx);
6264
+ if (!dir)
6265
+ exitError("STORAGE_UNAVAILABLE", "图片存储目录不可用");
6266
+ const entry = readImageIndex(dir).find((e) => e.imageId === id);
6267
+ if (!entry) {
6268
+ exitError("NOT_FOUND", `图片不存在: ${id}`);
6269
+ }
6270
+ if (entry.status !== "synced" || !entry.localFile) {
6271
+ exitError("IMAGE_NOT_READY", `图片 ${id} 尚未下载完成(status=${entry.status}${entry.lastError ? `, error=${entry.lastError}` : ""})`);
6272
+ }
6273
+ output({ ok: true, path: resolveImageFile(dir, entry.localFile) });
6274
+ });
6275
+ }
6276
+
6277
+ // src/cli/image-storage-path.ts
6278
+ function registerImageStoragePath(image, ctx) {
6279
+ image.command("storage-path").description("查询图片存储路径").action(() => {
6280
+ const dir = resolveImagesDir(ctx);
6281
+ if (!dir)
6282
+ exitError("STORAGE_UNAVAILABLE", "图片存储目录不可用");
6283
+ output({ ok: true, path: dir });
6284
+ });
6285
+ }
6286
+
5488
6287
  // src/cli/update.ts
5489
- var import_node_child_process2 = require("node:child_process");
5490
- var import_node_fs17 = require("node:fs");
5491
- var import_node_path12 = require("node:path");
6288
+ var import_node_child_process3 = require("node:child_process");
6289
+ var import_node_fs18 = require("node:fs");
6290
+ var import_node_path13 = require("node:path");
5492
6291
  var import_node_os = __toESM(require("node:os"));
5493
6292
  async function fetchText(url) {
5494
6293
  const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
@@ -5561,9 +6360,9 @@ async function runUpdate(ctx, opts) {
5561
6360
  `);
5562
6361
  process.exit(1);
5563
6362
  }
5564
- const tmpScript = import_node_path12.join(import_node_os.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
6363
+ const tmpScript = import_node_path13.join(import_node_os.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
5565
6364
  try {
5566
- import_node_fs17.writeFileSync(tmpScript, installScript, "utf-8");
6365
+ import_node_fs18.writeFileSync(tmpScript, installScript, "utf-8");
5567
6366
  } catch (err) {
5568
6367
  const msg = `写入临时文件失败: ${err?.message ?? String(err)}`;
5569
6368
  if (json) {
@@ -5575,9 +6374,9 @@ async function runUpdate(ctx, opts) {
5575
6374
  process.exit(1);
5576
6375
  }
5577
6376
  const stateDir = resolveStateDir(ctx.stateDir);
5578
- const result = import_node_child_process2.spawnSync(process.execPath, [tmpScript, "--version", latest, "--state-dir", stateDir], { stdio: "inherit" });
6377
+ const result = import_node_child_process3.spawnSync(process.execPath, [tmpScript, "--version", latest, "--state-dir", stateDir], { stdio: "inherit" });
5579
6378
  try {
5580
- import_node_fs17.unlinkSync(tmpScript);
6379
+ import_node_fs18.unlinkSync(tmpScript);
5581
6380
  } catch {}
5582
6381
  if (result.error) {
5583
6382
  const msg = `安装脚本执行失败: ${result.error.message}`;
@@ -5621,6 +6420,7 @@ function registerAllCli(program2, ctx, rootCommandName = "ntf") {
5621
6420
  registerAuthCli(ntf);
5622
6421
  registerNtfSearch(ntf, normalizedCtx);
5623
6422
  registerNtfSummary(ntf, normalizedCtx);
6423
+ registerNtfSummaryJob(ntf, normalizedCtx);
5624
6424
  registerNtfStats(ntf, normalizedCtx);
5625
6425
  registerNtfSync(ntf, normalizedCtx);
5626
6426
  registerNtfMonitor(ntf, normalizedCtx);
@@ -5637,6 +6437,11 @@ function registerAllCli(program2, ctx, rootCommandName = "ntf") {
5637
6437
  registerRecStatus(rec, normalizedCtx);
5638
6438
  registerRecStoragePath(rec, normalizedCtx);
5639
6439
  registerRecSetup(rec, normalizedCtx);
6440
+ const image = ntf.command("image").description("图片管理");
6441
+ registerImageList(image, normalizedCtx);
6442
+ registerImageStatus(image, normalizedCtx);
6443
+ registerImagePath(image, normalizedCtx);
6444
+ registerImageStoragePath(image, normalizedCtx);
5640
6445
  registerUpdate(ntf, normalizedCtx);
5641
6446
  }
5642
6447
 
@@ -5651,5 +6456,5 @@ program2.parseAsync(process.argv).catch((err) => {
5651
6456
  process.exit(1);
5652
6457
  });
5653
6458
 
5654
- //# debugId=4B1B72A253EC571164756E2164756E21
6459
+ //# debugId=37D91C049CB0123F64756E2164756E21
5655
6460
  //# sourceMappingURL=ntf.cjs.map