engine7 7.1.84 → 7.1.94

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/main.mjs CHANGED
@@ -207,12 +207,205 @@ var init_features = __esm({
207
207
  }
208
208
  });
209
209
 
210
+ // src/time/clock.ts
211
+ function detectHostTimezone() {
212
+ try {
213
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
214
+ } catch {
215
+ return "UTC";
216
+ }
217
+ }
218
+ function isValidTimezone(name) {
219
+ if (!name) return false;
220
+ try {
221
+ new Intl.DateTimeFormat("en-US", { timeZone: name });
222
+ return true;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ function tz() {
228
+ if (testTimezone) return testTimezone;
229
+ const v = liveConfig.get("timezone");
230
+ const name = typeof v === "string" && v ? v : "UTC";
231
+ if (validatedZones.has(name)) return name;
232
+ if (isValidTimezone(name)) {
233
+ validatedZones.add(name);
234
+ return name;
235
+ }
236
+ warnOnce("tz", "invalid-zone", name);
237
+ return "UTC";
238
+ }
239
+ function partsFmt(zone) {
240
+ let f = fmtCache.get(zone);
241
+ if (!f) {
242
+ f = new Intl.DateTimeFormat("en-US", {
243
+ timeZone: zone,
244
+ year: "numeric",
245
+ month: "2-digit",
246
+ day: "2-digit",
247
+ hour: "2-digit",
248
+ minute: "2-digit",
249
+ second: "2-digit",
250
+ hourCycle: "h23",
251
+ weekday: "short"
252
+ });
253
+ fmtCache.set(zone, f);
254
+ }
255
+ return f;
256
+ }
257
+ function asDate(i) {
258
+ return i instanceof Date ? i : new Date(i);
259
+ }
260
+ function wallParts(i = /* @__PURE__ */ new Date()) {
261
+ const d = asDate(i);
262
+ const parts = partsFmt(tz()).formatToParts(d);
263
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
264
+ return {
265
+ y: Number(get("year")),
266
+ mo: Number(get("month")),
267
+ d: Number(get("day")),
268
+ hh: Number(get("hour")),
269
+ mm: Number(get("minute")),
270
+ ss: Number(get("second")),
271
+ weekday: EN_WEEKDAY[get("weekday")] ?? 0
272
+ };
273
+ }
274
+ function weekdayZh(i) {
275
+ return WEEKDAY_ZH[wallParts(i).weekday];
276
+ }
277
+ function nowUtc() {
278
+ return (/* @__PURE__ */ new Date()).toISOString();
279
+ }
280
+ function toUtc(d) {
281
+ return d.toISOString();
282
+ }
283
+ function toWall(i = /* @__PURE__ */ new Date()) {
284
+ const p = wallParts(i);
285
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)} ${p2(p.hh)}:${p2(p.mm)}:${p2(p.ss)}`;
286
+ }
287
+ function formatForLlm(i) {
288
+ const d = i ?? /* @__PURE__ */ new Date();
289
+ const p = wallParts(d);
290
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)} ${WEEKDAY_ZH[p.weekday]} ${p2(p.hh)}:${p2(p.mm)}\uFF08${tz()}\uFF09`;
291
+ }
292
+ function formatLog(i) {
293
+ return toWall(i ?? /* @__PURE__ */ new Date());
294
+ }
295
+ function formatWallLabel(i) {
296
+ return formatForLlm(i).replace(/\s*(.*)$/, "");
297
+ }
298
+ function warnOnce(callSite, shape, sample) {
299
+ const key = `${callSite}:${shape}`;
300
+ if (warned.has(key)) return;
301
+ warned.add(key);
302
+ console.warn(`[clock] ${callSite} \u5951\u7EA6\u8FDD\u89C4(${shape})\uFF0C\u6837\u672C: ${String(sample).slice(0, 80)}`);
303
+ }
304
+ function zoneOffsetMs(d) {
305
+ const p = wallParts(d);
306
+ const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mm, p.ss);
307
+ return asUtc - Math.floor(d.getTime() / 1e3) * 1e3;
308
+ }
309
+ function wallToUtc(y, mo, d, hh, mm, ss) {
310
+ const guess = Date.UTC(y, mo - 1, d, hh, mm, ss);
311
+ const off1 = zoneOffsetMs(new Date(guess));
312
+ const ms1 = guess - off1;
313
+ const off2 = zoneOffsetMs(new Date(ms1));
314
+ const ms = off2 === off1 ? ms1 : guess - off2;
315
+ return new Date(ms).toISOString();
316
+ }
317
+ function parseWall(s) {
318
+ const raw = String(s ?? "").trim();
319
+ if (!raw) return "";
320
+ if (/Z$/i.test(raw) || /[+-]\d{2}:?\d{2}$/.test(raw)) {
321
+ const ms2 = Date.parse(raw);
322
+ if (Number.isNaN(ms2)) {
323
+ warnOnce("parseWall", "unparseable", raw);
324
+ return raw;
325
+ }
326
+ warnOnce("parseWall", "has-offset", raw);
327
+ return new Date(ms2).toISOString();
328
+ }
329
+ const m = WALL_RE.exec(raw);
330
+ if (!m) {
331
+ warnOnce("parseWall", "unparseable", raw);
332
+ return raw;
333
+ }
334
+ const tail = raw.slice(m[0].length);
335
+ if (tail && !/^\.\d+$/.test(tail)) {
336
+ warnOnce("parseWall", "unparseable-tail", raw);
337
+ return raw;
338
+ }
339
+ const [Y, Mo, D, H, Mi, S] = [+m[1], +m[2], +m[3], +m[4], +m[5], m[6] ? +m[6] : 0];
340
+ if (Y < 1e3 || Y > 9999 || Mo < 1 || Mo > 12 || D < 1 || D > daysInMonth(Y, Mo) || H > 23 || Mi > 59 || S > 59) {
341
+ warnOnce("parseWall", "out-of-range", raw);
342
+ return raw;
343
+ }
344
+ const ms = tail ? Math.round(Number(`0${tail}`) * 1e3) : 0;
345
+ return withMs(wallToUtc(Y, Mo, D, H, Mi, S), ms);
346
+ }
347
+ function daysInMonth(y, mo) {
348
+ return new Date(Date.UTC(y, mo, 0)).getUTCDate();
349
+ }
350
+ function withMs(base, ms) {
351
+ if (!ms) return base;
352
+ const t = Date.parse(base);
353
+ return Number.isNaN(t) ? base : new Date(t + ms).toISOString();
354
+ }
355
+ function parseDateStrTime(dateStr, timeExact) {
356
+ if (typeof dateStr !== "string" || typeof timeExact !== "string") return void 0;
357
+ if (!/^\d{1,2}\/\d{1,2}$/.test(dateStr)) return void 0;
358
+ if (!/^\d{2}:\d{2}$/.test(timeExact)) return void 0;
359
+ const [mo, d] = dateStr.split("/").map(Number);
360
+ if (mo < 1 || mo > 12 || d < 1 || d > 31) return void 0;
361
+ const y = wallParts().y;
362
+ return parseWall(`${y}-${p2(mo)}-${p2(d)}T${timeExact}:00`);
363
+ }
364
+ function localOffsetSuffix(at) {
365
+ const total = Math.round(zoneOffsetMs(at ? asDate(at) : /* @__PURE__ */ new Date()) / 6e4);
366
+ const sign = total >= 0 ? "+" : "-";
367
+ const abs = Math.abs(total);
368
+ return `${sign}${p2(Math.floor(abs / 60))}:${p2(abs % 60)}`;
369
+ }
370
+ function startOfLocalDay(i) {
371
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
372
+ return wallToUtc(p.y, p.mo, p.d, 0, 0, 0);
373
+ }
374
+ function todayKey(i) {
375
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
376
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)}`;
377
+ }
378
+ var WEEKDAY_ZH, EN_WEEKDAY, testTimezone, validatedZones, fmtCache, p2, warned, WALL_RE;
379
+ var init_clock = __esm({
380
+ "src/time/clock.ts"() {
381
+ "use strict";
382
+ init_live();
383
+ WEEKDAY_ZH = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
384
+ EN_WEEKDAY = {
385
+ Sun: 0,
386
+ Mon: 1,
387
+ Tue: 2,
388
+ Wed: 3,
389
+ Thu: 4,
390
+ Fri: 5,
391
+ Sat: 6
392
+ };
393
+ testTimezone = null;
394
+ validatedZones = /* @__PURE__ */ new Set();
395
+ fmtCache = /* @__PURE__ */ new Map();
396
+ p2 = (n) => String(n).padStart(2, "0");
397
+ warned = /* @__PURE__ */ new Set();
398
+ WALL_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?/;
399
+ }
400
+ });
401
+
210
402
  // src/config/loader.ts
211
403
  var loader_exports = {};
212
404
  __export(loader_exports, {
213
405
  configSummary: () => configSummary,
214
406
  loadConfig: () => loadConfig,
215
407
  parseModelRef: () => parseModelRef,
408
+ resolveTimezone: () => resolveTimezone,
216
409
  takeLoadWarnings: () => takeLoadWarnings
217
410
  });
218
411
  import * as fs from "node:fs";
@@ -234,6 +427,24 @@ function warnAtLoad(msg2) {
234
427
  if (!loadWarningsFlushed) loadWarnings.push(msg2);
235
428
  console.warn(msg2);
236
429
  }
430
+ function resolveTimezone(raw) {
431
+ const fallback = () => {
432
+ try {
433
+ return detectHostTimezone();
434
+ } catch {
435
+ return "UTC";
436
+ }
437
+ };
438
+ const v = raw?.timezone;
439
+ if (typeof v !== "string" || !v) return fallback();
440
+ try {
441
+ new Intl.DateTimeFormat("en-US", { timeZone: v });
442
+ return v;
443
+ } catch {
444
+ warnAtLoad(`[config] timezone "${v}" \u4E0D\u662F\u5408\u6CD5 IANA \u65F6\u533A\u540D\uFF0C\u9000\u56DE\u672C\u673A\u63A2\u6D4B`);
445
+ return fallback();
446
+ }
447
+ }
237
448
  function loadConfig(configPath2) {
238
449
  const resolvedPath = path.resolve(
239
450
  configPath2 || process.env.ENGINE_CONFIG || "engine-config.json"
@@ -245,7 +456,7 @@ function loadConfig(configPath2) {
245
456
  const missingEnvKeys = /* @__PURE__ */ new Set();
246
457
  const resolveEnv = (node) => {
247
458
  if (typeof node === "string") {
248
- const m = node.match(/^(?:env:|{{\s*)([A-Z][A-Z0-9_]*)(?:\s*}})?$/);
459
+ const m = node.match(/^(?:env:|{{\s*)(?=[A-Z0-9_]*[A-Z])([A-Z0-9_]+)(?:\s*}})?$/);
249
460
  if (m) {
250
461
  if (process.env[m[1]] !== void 0) return process.env[m[1]];
251
462
  if (node.startsWith("env:")) missingEnvKeys.add(m[1]);
@@ -370,11 +581,18 @@ function loadConfig(configPath2) {
370
581
  ...rawInnerVoice,
371
582
  enabled: true
372
583
  } : void 0;
584
+ const resolvedTimezone = resolveTimezone(raw);
373
585
  const rawCron = raw.cron || {};
374
586
  const cron = rawCron.enabled ? {
375
587
  ...rawCron,
376
588
  enabled: true,
377
- defaultTimezone: rawCron.defaultTimezone || "Asia/Shanghai",
589
+ // ★ 这里原来写死 'Asia/Shanghai'。而 cron/tasks.ts 建任务时是
590
+ // `params.timezone || params.defaultTimezone || 'Asia/Shanghai'` ——
591
+ // defaultTimezone 恒有值 ⇒ 兜底永远轮不到 ⇒ **工具建的每个任务都被
592
+ // 显式写死北京时区存进 tasks.json**,顶层 timezone 配置形同虚设
593
+ // (马耳他用户说"每天 9 点",会在当地凌晨 3 点触发)。
594
+ // 改为跟随引擎时区;per-task 的显式 timezone 仍然优先。
595
+ defaultTimezone: rawCron.defaultTimezone || resolvedTimezone,
378
596
  tickIntervalMs: rawCron.tickIntervalMs || 1e4,
379
597
  maxConsecutiveFailures: rawCron.maxConsecutiveFailures || 5,
380
598
  retry: {
@@ -387,6 +605,7 @@ function loadConfig(configPath2) {
387
605
  return {
388
606
  ...raw,
389
607
  // 先展开 raw:未显式处理的字段(如 webhook)自动进来,避免每次加 config 都要手动挑;下面显式字段覆盖 raw(解析值优先)
608
+ timezone: resolvedTimezone,
390
609
  provider,
391
610
  providers,
392
611
  // 所有 provider 配置字典(供 memory/provider 动态切换用)
@@ -455,6 +674,7 @@ var init_loader = __esm({
455
674
  "use strict";
456
675
  init_display();
457
676
  init_features();
677
+ init_clock();
458
678
  loadWarnings = [];
459
679
  loadWarningsFlushed = false;
460
680
  }
@@ -704,12 +924,15 @@ function normalizeHook(h) {
704
924
  }
705
925
  return {
706
926
  type: "command",
707
- command: h.command || "",
927
+ command: expandCommand(h.command || ""),
708
928
  timeout: h.timeout,
709
929
  async: h.async,
710
930
  shell: h.shell
711
931
  };
712
932
  }
933
+ function expandCommand(command) {
934
+ return command.replace(/\$\{E7_NODE\}/g, process.execPath);
935
+ }
713
936
  function registerCallbackHook(event2, hook) {
714
937
  const list2 = callbackHooks.get(event2) || [];
715
938
  if (hook.id) {
@@ -2200,6 +2423,29 @@ var init_microCompact = __esm({
2200
2423
  }
2201
2424
  });
2202
2425
 
2426
+ // src/utils/well-formed.ts
2427
+ function truncateByCodePoint(text, max) {
2428
+ if (text.length <= max) return text;
2429
+ return Array.from(text).slice(0, max).join("");
2430
+ }
2431
+ function toWellFormed(s) {
2432
+ if (HAS_NATIVE) return s.toWellFormed();
2433
+ return s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
2434
+ }
2435
+ function wellFormedReplacer(_key, value) {
2436
+ return typeof value === "string" ? toWellFormed(value) : value;
2437
+ }
2438
+ function safeStringify(body) {
2439
+ return JSON.stringify(body, wellFormedReplacer);
2440
+ }
2441
+ var HAS_NATIVE;
2442
+ var init_well_formed = __esm({
2443
+ "src/utils/well-formed.ts"() {
2444
+ "use strict";
2445
+ HAS_NATIVE = typeof String.prototype.toWellFormed === "function";
2446
+ }
2447
+ });
2448
+
2203
2449
  // src/compact/ruleCompact.ts
2204
2450
  function smartCompressToolResult(content, essentialFields, sizeLimit) {
2205
2451
  if (content.length <= (sizeLimit ?? TOOL_SIZE_LIMIT)) return content;
@@ -2241,7 +2487,7 @@ ${result}`;
2241
2487
  }
2242
2488
  } catch {
2243
2489
  }
2244
- return content.slice(0, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
2490
+ return truncateByCodePoint(content, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
2245
2491
  }
2246
2492
  function foldTurnInline(turn) {
2247
2493
  const toolMsgs = turn.filter((m) => m.role === "tool");
@@ -2371,7 +2617,8 @@ function step3_truncateLarge(messages) {
2371
2617
  details.push(`tool_call_id=${m.tool_call_id?.slice(0, 8)}... ${originalLen} \u2192 ${TRUNCATE_TO} chars`);
2372
2618
  return {
2373
2619
  ...m,
2374
- content: m.content.slice(0, TRUNCATE_TO) + "\n...[TRUNCATED]"
2620
+ // 0918:按 code point 切,避免切开 emoji 代理对(见 utils/well-formed.ts)
2621
+ content: truncateByCodePoint(m.content, TRUNCATE_TO) + "\n...[TRUNCATED]"
2375
2622
  };
2376
2623
  }
2377
2624
  return m;
@@ -2461,6 +2708,7 @@ var init_ruleCompact = __esm({
2461
2708
  init_tokenEstimate();
2462
2709
  init_autoCompact();
2463
2710
  init_compactLog();
2711
+ init_well_formed();
2464
2712
  TAG3 = "ruleCompact";
2465
2713
  TOOL_SIZE_LIMIT = 8e3;
2466
2714
  TRUNCATE_TO = 5e3;
@@ -2687,6 +2935,9 @@ function openDb(workspace) {
2687
2935
  day TEXT,
2688
2936
  time TEXT,
2689
2937
  date_str TEXT,
2938
+ -- \u26A0\uFE0F 0920 \u4E4B\u524D\u7684\u503C\u7531\u5E26\u7B26\u53F7 bug \u7684 nowIso \u5199\u5165\uFF1A\u503C = \u771F\u5B9E\u65F6\u523B + **\u5199\u673A\u5F53\u65F6\u7684
2939
+ -- UTC \u504F\u79FB**\uFF08\u4E1C\u4E3A\u6B63\uFF09\u3002\u9010\u884C\u4E0D\u540C\u3001\u65E0\u5904\u53EF\u8003\uFF0C\u6CA1\u6709\u53EF\u9760\u7684\u8FD8\u539F\u529E\u6CD5\u3002
2940
+ -- 0920 \u8D77\u5199\u5165\u6B63\u786E UTC Z\u3002\u8BFB\u8FD9\u4E09\u5217\u524D\u5148\u786E\u8BA4\u4F60\u8981\u7684\u662F\u4EC0\u4E48\u3002
2690
2941
  created_at TEXT NOT NULL,
2691
2942
  done_at TEXT,
2692
2943
  archived_at TEXT
@@ -2729,7 +2980,7 @@ function migrate(db2) {
2729
2980
  db2.exec("ALTER TABLE events ADD COLUMN doc_path TEXT");
2730
2981
  }
2731
2982
  }
2732
- var _sqlite, DatabaseSync, TZ_OFFSET_MS;
2983
+ var _sqlite, DatabaseSync;
2733
2984
  var init_db = __esm({
2734
2985
  "src/calendar/db.ts"() {
2735
2986
  "use strict";
@@ -2743,7 +2994,6 @@ var init_db = __esm({
2743
2994
  return getSqlite().DatabaseSync[prop];
2744
2995
  }
2745
2996
  });
2746
- TZ_OFFSET_MS = 8 * 60 * 60 * 1e3;
2747
2997
  }
2748
2998
  });
2749
2999
 
@@ -2872,15 +3122,6 @@ function bindAim(stateDir, goalId, stepId, aimId) {
2872
3122
  step.note = `AIM assignment bound: ${aimId}`;
2873
3123
  });
2874
3124
  }
2875
- function bindCalendar(stateDir, goalId, stepId, calendarId, deadlineAt) {
2876
- return updateGoal(stateDir, goalId, (g) => {
2877
- const step = g.steps.find((s) => s.id === stepId);
2878
- if (!step) throw new Error(`Step not found: ${stepId}`);
2879
- if (step.status === "completed") throw new Error("Cannot bind calendar to completed step");
2880
- step.calendarId = calendarId;
2881
- if (deadlineAt) step.deadlineAt = deadlineAt;
2882
- });
2883
- }
2884
3125
  function applyAimEvent(stateDir, aimId, type2, note) {
2885
3126
  const goal = listGoals(stateDir).find((g) => g.steps.some((s) => s.aimId === aimId));
2886
3127
  if (!goal) return void 0;
@@ -2944,7 +3185,7 @@ function addGoalStep(stateDir, goalId, title, kind = "self") {
2944
3185
  g.steps.push({ id, title, status: g.currentStep >= g.steps.length ? "active" : "pending", kind });
2945
3186
  });
2946
3187
  }
2947
- function bindGoalStep(stateDir, goalId, stepId, kind, ref) {
3188
+ function bindGoalStep(stateDir, goalId, stepId, kind, ref, deadlineAt) {
2948
3189
  return updateGoal(stateDir, goalId, (g) => {
2949
3190
  const step = g.steps.find((s) => s.id === stepId);
2950
3191
  if (!step) throw new Error(`Step not found: ${stepId}`);
@@ -2952,7 +3193,10 @@ function bindGoalStep(stateDir, goalId, stepId, kind, ref) {
2952
3193
  step.kind = kind;
2953
3194
  if (kind === "aim") step.aimId = String(ref);
2954
3195
  else if (kind === "remote_task") step.taskId = String(ref);
2955
- else if (kind === "calendar") step.calendarId = Number(ref);
3196
+ else if (kind === "calendar") {
3197
+ step.calendarId = Number(ref);
3198
+ if (deadlineAt) step.deadlineAt = deadlineAt;
3199
+ }
2956
3200
  step.note = `${kind} \u59D4\u6D3E: ${ref}\uFF08\u5B8C\u6210\u4E8B\u4EF6\u81EA\u52A8\u56DE\u6D41\u89E3\u9501\uFF09`;
2957
3201
  });
2958
3202
  }
@@ -4084,7 +4328,7 @@ async function classifyViaSidecar(url, context, text, opts = {}) {
4084
4328
  const res = await doFetch(`${url.replace(/\/$/, "")}/classify`, {
4085
4329
  method: "POST",
4086
4330
  headers: { "Content-Type": "application/json" },
4087
- body: JSON.stringify({ context: context.map((t) => ({ text: t })), text }),
4331
+ body: safeStringify({ context: context.map((t) => ({ text: t })), text }),
4088
4332
  signal: ac.signal
4089
4333
  });
4090
4334
  if (!res.ok) return null;
@@ -4101,6 +4345,7 @@ var init_skill_gate_classifier = __esm({
4101
4345
  "src/skills/skill-gate-classifier.ts"() {
4102
4346
  "use strict";
4103
4347
  init_skill_gate_window();
4348
+ init_well_formed();
4104
4349
  }
4105
4350
  });
4106
4351
 
@@ -5647,16 +5892,18 @@ function shouldRetry(status, errorBody) {
5647
5892
  if (status === 529) return true;
5648
5893
  if (status === 429) {
5649
5894
  const body = errorBody?.toLowerCase() || "";
5650
- if (body.includes("insufficient_quota") || body.includes("allocationquota") || body.includes("quota_exceeded") || body.includes("quotaexceeded") || body.includes("quota") && body.includes("exceeded") || body.includes('"code":"1310"') || body.includes('"code":1310') || body.includes("\u4F7F\u7528\u4E0A\u9650") && body.includes("\u91CD\u7F6E")) {
5895
+ if (body.includes("insufficient_quota") || body.includes("allocationquota") || body.includes("quota_exceeded") || body.includes("quotaexceeded") || body.includes("quota") && body.includes("exceeded") || body.includes('"code":"1310"') || body.includes('"code":1310') || body.includes("\u4F7F\u7528\u4E0A\u9650") && body.includes("\u91CD\u7F6E") || body.includes("\u7528\u91CF\u4E0A\u9650")) {
5651
5896
  return false;
5652
5897
  }
5653
5898
  return true;
5654
5899
  }
5655
5900
  if (status === 408) return true;
5656
5901
  if (status === 409) return true;
5657
- if (status === 401) return true;
5658
- if (status === 403) return true;
5659
- if (status >= 500) return true;
5902
+ if (status >= 500) {
5903
+ const body = errorBody?.toLowerCase() || "";
5904
+ if (body.includes("\u65E0\u53EF\u7528\u6E20\u9053") || body.includes("new_sensitive")) return false;
5905
+ return true;
5906
+ }
5660
5907
  if (errorBody?.includes('"type":"overloaded_error"')) return true;
5661
5908
  return false;
5662
5909
  }
@@ -5926,6 +6173,7 @@ var init_openai_provider = __esm({
5926
6173
  init_withRetry();
5927
6174
  init_provider();
5928
6175
  init_think_stripper();
6176
+ init_well_formed();
5929
6177
  OpenAIProvider = class {
5930
6178
  // 0831 <think> 剥离改用 ThinkTagStripper 的**局部**实例(见 streamChat)——
5931
6179
  // 原来是实例字段,跨请求共享状态,流中途 abort 会把 active=true 带到下一个请求
@@ -6014,13 +6262,17 @@ var init_openai_provider = __esm({
6014
6262
  }
6015
6263
  const actualThinking = body.chat_template_kwargs;
6016
6264
  console.log(`[openai] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} maxTokens=${body.max_tokens ?? "unset"} msgs=${formatted.length} tools=${params.tools?.length ?? 0}`);
6265
+ let bodyStr = safeStringify(body);
6266
+ if (this.config.wafSingleQuoteWorkaround) {
6267
+ bodyStr = bodyStr.replace(/'/g, "\u2019");
6268
+ }
6017
6269
  const retryGen = fetchWithRetry(url, {
6018
6270
  method: "POST",
6019
6271
  headers: {
6020
6272
  "Content-Type": "application/json",
6021
6273
  "Authorization": `Bearer ${this.config.apiKey}`
6022
6274
  },
6023
- body: JSON.stringify(body),
6275
+ body: bodyStr,
6024
6276
  signal: params.signal
6025
6277
  }, "openai", this.config.proxy);
6026
6278
  let response;
@@ -6218,7 +6470,7 @@ function adaptUsage(u) {
6218
6470
  });
6219
6471
  }
6220
6472
  function serializeBody(body, wafSingleQuoteWorkaround) {
6221
- let bodyStr = JSON.stringify(body);
6473
+ let bodyStr = safeStringify(body);
6222
6474
  if (wafSingleQuoteWorkaround) {
6223
6475
  bodyStr = bodyStr.replace(/'/g, "\u2019");
6224
6476
  }
@@ -6245,6 +6497,7 @@ var init_openai_responses_provider = __esm({
6245
6497
  "use strict";
6246
6498
  init_withRetry();
6247
6499
  init_provider();
6500
+ init_well_formed();
6248
6501
  CODEX_CLI_VERSION = "0.154.0";
6249
6502
  CODEX_TERMINAL_TOKEN = "xterm-256color";
6250
6503
  OpenAIResponsesProvider = class {
@@ -6565,12 +6818,14 @@ var init_anthropic_provider = __esm({
6565
6818
  init_withRetry();
6566
6819
  init_provider();
6567
6820
  init_think_stripper();
6821
+ init_well_formed();
6568
6822
  AnthropicProvider = class {
6569
6823
  // 模拟 CC 的 X-Claude-Code-Session-Id
6570
6824
  constructor(config2) {
6571
6825
  this.config = config2;
6572
6826
  let u = (config2.baseUrl || "https://api.anthropic.com").trim();
6573
6827
  while (u.endsWith("/")) u = u.slice(0, -1);
6828
+ if (u.endsWith("/v1")) u = u.slice(0, -3);
6574
6829
  this.baseUrl = u;
6575
6830
  this.sessionId = randomUUID3();
6576
6831
  }
@@ -6769,7 +7024,7 @@ var init_anthropic_provider = __esm({
6769
7024
  } catch {
6770
7025
  }
6771
7026
  }
6772
- let bodyStr = JSON.stringify(body);
7027
+ let bodyStr = safeStringify(body);
6773
7028
  if (this.config.wafSingleQuoteWorkaround) {
6774
7029
  const before = bodyStr.length;
6775
7030
  bodyStr = bodyStr.replace(/'/g, "\u2019");
@@ -6989,6 +7244,7 @@ var init_gemini_provider = __esm({
6989
7244
  "src/models/gemini-provider.ts"() {
6990
7245
  "use strict";
6991
7246
  init_withRetry();
7247
+ init_well_formed();
6992
7248
  GeminiProvider = class {
6993
7249
  constructor(config2) {
6994
7250
  this.config = config2;
@@ -7148,7 +7404,7 @@ var init_gemini_provider = __esm({
7148
7404
  "Content-Type": "application/json",
7149
7405
  "x-goog-api-key": this.config.apiKey
7150
7406
  },
7151
- body: JSON.stringify(body),
7407
+ body: safeStringify(body),
7152
7408
  signal: params.signal
7153
7409
  }, "gemini", this.config.proxy);
7154
7410
  let response;
@@ -7287,7 +7543,9 @@ function createProvider(config2) {
7287
7543
  baseUrl: config2.baseUrl,
7288
7544
  apiKey: config2.apiKey,
7289
7545
  thinking: config2.thinking,
7290
- proxy
7546
+ proxy,
7547
+ wafSingleQuoteWorkaround: config2.wafSingleQuoteWorkaround
7548
+ // 0919 中转站→agentrouter 同样有 WAF
7291
7549
  });
7292
7550
  case "openai-responses":
7293
7551
  return new OpenAIResponsesProvider({
@@ -7684,7 +7942,7 @@ __export(teamHelpers_exports, {
7684
7942
  writeTeamFile: () => writeTeamFile,
7685
7943
  writeTeamFileAsync: () => writeTeamFileAsync
7686
7944
  });
7687
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
7945
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
7688
7946
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7689
7947
  import { join as join15 } from "node:path";
7690
7948
  function initTeamDirs(stateDir) {
@@ -7727,8 +7985,8 @@ async function readTeamFileAsync(teamName) {
7727
7985
  }
7728
7986
  function writeTeamFile(teamName, teamFile) {
7729
7987
  const teamDir = getTeamDir(teamName);
7730
- mkdirSync5(teamDir, { recursive: true });
7731
- writeFileSync3(getTeamFilePath(teamName), JSON.stringify(teamFile, null, 2));
7988
+ mkdirSync6(teamDir, { recursive: true });
7989
+ writeFileSync4(getTeamFilePath(teamName), JSON.stringify(teamFile, null, 2));
7732
7990
  }
7733
7991
  async function writeTeamFileAsync(teamName, teamFile) {
7734
7992
  const teamDir = getTeamDir(teamName);
@@ -7818,7 +8076,7 @@ async function cleanupTeamDirectories(teamName) {
7818
8076
  }
7819
8077
  function ensureTasksDir(taskListId) {
7820
8078
  const dir = join15(getTasksDir(), taskListId);
7821
- mkdirSync5(dir, { recursive: true });
8079
+ mkdirSync6(dir, { recursive: true });
7822
8080
  }
7823
8081
  function setLeaderTeamName(name) {
7824
8082
  _leaderTeamName = name;
@@ -8647,23 +8905,42 @@ var init_extractMemories = __esm({
8647
8905
  }
8648
8906
  });
8649
8907
 
8908
+ // src/mem7/core/system-filter.ts
8909
+ function parseMeta(text) {
8910
+ const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+?)\s*(?:[#\d]|\])/);
8911
+ if (!m) return null;
8912
+ const platform2 = m[3].replace(/[#\d]+$/, "").trim() || void 0;
8913
+ return { senderName: m[1].trim(), senderId: m[2].trim(), platform: platform2 };
8914
+ }
8915
+ function isSystemUserMessage(meta) {
8916
+ if (!meta) return true;
8917
+ return SYSTEM_PLATFORMS.has(meta.platform ?? "");
8918
+ }
8919
+ var SYSTEM_PLATFORMS;
8920
+ var init_system_filter = __esm({
8921
+ "src/mem7/core/system-filter.ts"() {
8922
+ "use strict";
8923
+ SYSTEM_PLATFORMS = /* @__PURE__ */ new Set([
8924
+ "wx_monitor"
8925
+ // 微信巡检推送(翀哥 0916 点名:"cron、wx-monitor 这种问答都不记")
8926
+ ]);
8927
+ }
8928
+ });
8929
+
8650
8930
  // src/memory/everos/ingest.ts
8651
8931
  var ingest_exports = {};
8652
8932
  __export(ingest_exports, {
8933
+ buildPayload: () => buildPayload,
8653
8934
  pushConversation: () => pushConversation
8654
8935
  });
8655
8936
  import fs20 from "node:fs";
8656
8937
  import path19 from "node:path";
8657
- function parseMeta(text) {
8658
- const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+)\s*[^\]]*\]/);
8659
- if (!m) return null;
8660
- return { senderName: m[1].trim() };
8661
- }
8662
8938
  async function readConfig2() {
8663
8939
  const defaults = {
8664
8940
  url: "http://127.0.0.1:8100",
8665
8941
  appId: "default",
8666
8942
  agentName: "assistant",
8943
+ ownerMode: "agent",
8667
8944
  enabled: false,
8668
8945
  ingestTimeoutMs: 3e4,
8669
8946
  syncIntervalMs: 9e5
@@ -8676,6 +8953,7 @@ async function readConfig2() {
8676
8953
  url: cfg.everosUrl || defaults.url,
8677
8954
  appId: cfg.userId || defaults.appId,
8678
8955
  agentName: cfg.agentName || cfg.userId || defaults.agentName,
8956
+ ownerMode: cfg.ownerMode === "sender" ? "sender" : "agent",
8679
8957
  enabled: cfg.enabled === true,
8680
8958
  ingestTimeoutMs: typeof cfg.ingestTimeoutMs === "number" ? cfg.ingestTimeoutMs : defaults.ingestTimeoutMs,
8681
8959
  syncIntervalMs: typeof cfg.syncIntervalMs === "number" ? cfg.syncIntervalMs : defaults.syncIntervalMs
@@ -8711,6 +8989,7 @@ function savePushTs(workspace, sessionId, ts) {
8711
8989
  }
8712
8990
  }
8713
8991
  function buildPayload(messages, cfg, sessionId) {
8992
+ let lastUserWasSystem = false;
8714
8993
  return {
8715
8994
  session_id: `extract-${sessionId}`,
8716
8995
  app_id: cfg.appId,
@@ -8722,21 +9001,28 @@ function buildPayload(messages, cfg, sessionId) {
8722
9001
  const raw = typeof m.content === "string" ? m.content : "[content blocks]";
8723
9002
  const meta = m.role === "user" ? parseMeta(raw) : null;
8724
9003
  let text = raw;
9004
+ let isSystemTurn = false;
8725
9005
  if (m.role === "user") {
8726
- if (/^\[meta:[^\]]*\(webhook\)/.test(raw.trim())) text = "";
8727
- else text = stripInjectedContext(raw.replace(/^\[meta:[^\]]*\]\s*/, ""));
9006
+ if (isSystemUserMessage(parseMeta(raw))) {
9007
+ text = "";
9008
+ isSystemTurn = true;
9009
+ } else text = stripInjectedContext(raw.replace(/^\[meta:[^\]]*\]\s*/, ""));
9010
+ lastUserWasSystem = isSystemTurn;
8728
9011
  }
8729
9012
  const senderName = meta?.senderName ?? (m.role === "assistant" ? cfg.agentName : void 0) ?? m.role;
8730
9013
  const rawRole = m.role === "toolResult" ? "tool" : m.role;
8731
9014
  const role = rawRole === "user" ? "user" : "assistant";
9015
+ const tsMs = typeof m.timestamp === "number" ? m.timestamp : Date.parse(String(m.timestamp ?? "")) || Date.now();
8732
9016
  return {
8733
- sender_id: cfg.appId,
9017
+ // owner 口径见 IngestConfig.ownerMode:'sender' 模式下 user 消息用真人 ID
9018
+ // (everos 的 owner fan-out 取的就是 user 的 sender_id → 每人一份记忆)
9019
+ sender_id: m.role === "user" && cfg.ownerMode === "sender" ? meta?.senderId || cfg.appId : cfg.appId,
8734
9020
  sender_name: senderName,
8735
9021
  role,
8736
- timestamp: Date.now(),
9022
+ timestamp: tsMs,
8737
9023
  content: text,
8738
- _skip: m.role === "user" && !text
8739
- // 内部消息(剥完为空)标记丢弃
9024
+ // 丢:系统指令本体 + 系统轮里的回应(与 mem7 同款,见上面状态机)
9025
+ _skip: m.role === "user" && !text || m.role !== "user" && lastUserWasSystem
8740
9026
  };
8741
9027
  }).filter((x) => !x._skip).map(({ _skip, ...rest }) => rest)
8742
9028
  };
@@ -8846,6 +9132,7 @@ var init_ingest = __esm({
8846
9132
  "src/memory/everos/ingest.ts"() {
8847
9133
  "use strict";
8848
9134
  init_skill_gate_window();
9135
+ init_system_filter();
8849
9136
  PUSH_CHUNK = 10;
8850
9137
  PUSH_STATE_FILE = ".everos-push-state.json";
8851
9138
  pushInProgress = false;
@@ -8892,7 +9179,8 @@ async function embedChunk(cfg, baseUrl, texts, timeoutMs) {
8892
9179
  const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
8893
9180
  method: "POST",
8894
9181
  headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}` },
8895
- body: JSON.stringify({ model: cfg.model, input: texts }),
9182
+ // 0918:safeStringify 清洗孤立代理(embedding 传对话原文,emoji 风险高)
9183
+ body: safeStringify({ model: cfg.model, input: texts }),
8896
9184
  signal: ctrl.signal
8897
9185
  });
8898
9186
  if (!resp.ok) throw new Error(`embeddings HTTP ${resp.status}`);
@@ -8941,6 +9229,7 @@ function cosine(a, b) {
8941
9229
  var init_embedding = __esm({
8942
9230
  "src/mem7/core/embedding.ts"() {
8943
9231
  "use strict";
9232
+ init_well_formed();
8944
9233
  }
8945
9234
  });
8946
9235
 
@@ -9081,8 +9370,7 @@ function compileFilter(expr) {
9081
9370
  return parts.join(" AND ");
9082
9371
  }
9083
9372
  function openWindowFilter(ownerId) {
9084
- const cstToday = new Date(Date.now() + 8 * 36e5).toISOString().slice(0, 10);
9085
- const floor = (/* @__PURE__ */ new Date(`${cstToday}T00:00:00+08:00`)).toISOString();
9373
+ const floor = startOfLocalDay();
9086
9374
  const out = [{ end_time: { gte: floor } }];
9087
9375
  const owners = (Array.isArray(ownerId) ? ownerId : ownerId ? [ownerId] : []).filter(Boolean);
9088
9376
  if (Array.isArray(ownerId) && owners.length === 0) {
@@ -9096,6 +9384,7 @@ var REGISTERED_FIELDS, OPS, TIME_FIELDS;
9096
9384
  var init_filters = __esm({
9097
9385
  "src/mem7/core/filters.ts"() {
9098
9386
  "use strict";
9387
+ init_clock();
9099
9388
  REGISTERED_FIELDS = /* @__PURE__ */ new Set([
9100
9389
  "owner_id",
9101
9390
  "owner_type",
@@ -9116,7 +9405,7 @@ var init_filters = __esm({
9116
9405
  // src/mem7/core/vector-store.ts
9117
9406
  import * as lancedb from "@lancedb/lancedb";
9118
9407
  import { Schema, Field, Utf8, Float32, Int64, FixedSizeList, TimestampMicrosecond, List } from "apache-arrow";
9119
- import { mkdirSync as mkdirSync6 } from "node:fs";
9408
+ import { mkdirSync as mkdirSync7 } from "node:fs";
9120
9409
  import { join as join22 } from "node:path";
9121
9410
  var VECTOR_DIM, BM25_FIELDS, RRF_K, warnedFts, vecField, commonFields, SCHEMAS, Mem7VectorStore;
9122
9411
  var init_vector_store = __esm({
@@ -9187,7 +9476,7 @@ var init_vector_store = __esm({
9187
9476
  }
9188
9477
  async conn() {
9189
9478
  if (!this.db) {
9190
- mkdirSync6(this.dir, { recursive: true });
9479
+ mkdirSync7(this.dir, { recursive: true });
9191
9480
  this.db = await lancedb.connect(this.dir);
9192
9481
  }
9193
9482
  return this.db;
@@ -9413,13 +9702,6 @@ function renderPrompt(template, vars) {
9413
9702
  }
9414
9703
  return t.split(LB).join("{").split(RB).join("}");
9415
9704
  }
9416
- function renderLiteral(template, vars) {
9417
- let out = template;
9418
- for (const [key, value] of Object.entries(vars)) {
9419
- out = out.split(`{{${key}}}`).join(value);
9420
- }
9421
- return out;
9422
- }
9423
9705
  function formatAtomicFactTime(tsMs) {
9424
9706
  const d = new Date(tsMs);
9425
9707
  const month = MONTHS[d.getUTCMonth()];
@@ -9451,10 +9733,11 @@ function formatMessageTimestamp(tsMs) {
9451
9733
  const iso = new Date(tsMs).toISOString();
9452
9734
  return `${iso.slice(0, 10)} ${iso.slice(11, 19)}`;
9453
9735
  }
9736
+ function renderConversationText(messages) {
9737
+ return messages.map((m) => `[${formatMessageTimestamp(m.tsMs)}] ${m.senderName ?? m.senderId}: ${m.text}`).join("\n");
9738
+ }
9454
9739
  function formatMdTimestamp(d = /* @__PURE__ */ new Date()) {
9455
- const cst = new Date(d.getTime() + 8 * 36e5);
9456
- const iso = cst.toISOString();
9457
- return `${iso.slice(0, 19)}+08:00`;
9740
+ return `${toWall(d).replace(" ", "T")}${localOffsetSuffix(d)}`;
9458
9741
  }
9459
9742
  function pyListRepr(items) {
9460
9743
  return `[${items.join(", ")}]`;
@@ -9463,6 +9746,7 @@ var WEEKDAYS, MONTHS, LB, RB, DEFAULT_CUSTOM_INSTRUCTIONS;
9463
9746
  var init_prompt_render = __esm({
9464
9747
  "src/mem7/core/prompt-render.ts"() {
9465
9748
  "use strict";
9749
+ init_clock();
9466
9750
  WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
9467
9751
  MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
9468
9752
  LB = "\0M7LB\0";
@@ -9479,7 +9763,7 @@ Follow these principles when generating episodic memories:
9479
9763
 
9480
9764
  // src/mem7/core/store.ts
9481
9765
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
9482
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync5, existsSync as existsSync13, readdirSync } from "node:fs";
9766
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync6, existsSync as existsSync13, readdirSync } from "node:fs";
9483
9767
  import { join as join23 } from "node:path";
9484
9768
  import { createHash } from "node:crypto";
9485
9769
  function contentHash(text) {
@@ -9494,6 +9778,7 @@ var init_store2 = __esm({
9494
9778
  "use strict";
9495
9779
  init_vector_store();
9496
9780
  init_prompt_render();
9781
+ init_clock();
9497
9782
  SCHEMA = `
9498
9783
  CREATE TABLE IF NOT EXISTS memcells (
9499
9784
  id TEXT PRIMARY KEY,
@@ -9521,7 +9806,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
9521
9806
  constructor(stateDir) {
9522
9807
  this.stateDir = stateDir;
9523
9808
  this.root = mem7Root(stateDir);
9524
- mkdirSync7(join23(this.root, "index"), { recursive: true });
9809
+ mkdirSync8(join23(this.root, "index"), { recursive: true });
9525
9810
  this.db = new DatabaseSync2(join23(this.root, "index", "mem7.db"));
9526
9811
  this.db.exec("PRAGMA journal_mode=WAL");
9527
9812
  this.db.exec(SCHEMA);
@@ -9654,7 +9939,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
9654
9939
  appendMd(ownerId, dirName, filePrefix, type2, dateStr, entries2, render) {
9655
9940
  if (entries2.length === 0) return;
9656
9941
  const dir = join23(this.root, "users", ownerId, dirName);
9657
- mkdirSync7(dir, { recursive: true });
9942
+ mkdirSync8(dir, { recursive: true });
9658
9943
  const file2 = join23(dir, `${filePrefix}-${dateStr}.md`);
9659
9944
  let head;
9660
9945
  let existingBlocks = "";
@@ -9691,7 +9976,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
9691
9976
  count++;
9692
9977
  }
9693
9978
  head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
9694
- writeFileSync5(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
9979
+ writeFileSync6(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
9695
9980
  }
9696
9981
  /**
9697
9982
  * entry_id:前缀_YYYYMMDD_六位序号。
@@ -9780,7 +10065,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
9780
10065
  }
9781
10066
  // ── md 读(proactive 消费):开窗预测(全 owner 扫描,end>=今天)──
9782
10067
  getOpenPredictions(now, limit = 50) {
9783
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
10068
+ const today = todayKey(now);
9784
10069
  const out = [];
9785
10070
  const usersDir = join23(this.root, "users");
9786
10071
  if (!existsSync13(usersDir)) return out;
@@ -9815,28 +10100,6 @@ CREATE TABLE IF NOT EXISTS watermarks (
9815
10100
  }
9816
10101
  });
9817
10102
 
9818
- // src/mem7/core/system-filter.ts
9819
- function parseMeta2(text) {
9820
- const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+?)\s*(?:[#\d]|\])/);
9821
- if (!m) return null;
9822
- const platform2 = m[3].replace(/[#\d]+$/, "").trim() || void 0;
9823
- return { senderName: m[1].trim(), senderId: m[2].trim(), platform: platform2 };
9824
- }
9825
- function isSystemUserMessage(meta) {
9826
- if (!meta) return true;
9827
- return SYSTEM_PLATFORMS.has(meta.platform ?? "");
9828
- }
9829
- var SYSTEM_PLATFORMS;
9830
- var init_system_filter = __esm({
9831
- "src/mem7/core/system-filter.ts"() {
9832
- "use strict";
9833
- SYSTEM_PLATFORMS = /* @__PURE__ */ new Set([
9834
- "wx_monitor"
9835
- // 微信巡检推送(翀哥 0916 点名:"cron、wx-monitor 这种问答都不记")
9836
- ]);
9837
- }
9838
- });
9839
-
9840
10103
  // src/mem7/core/ingest.ts
9841
10104
  function contentText(content) {
9842
10105
  if (typeof content === "string") return content;
@@ -9845,7 +10108,8 @@ function contentText(content) {
9845
10108
  }
9846
10109
  return "";
9847
10110
  }
9848
- function normalizeMessages(rawMessages, agentName) {
10111
+ function normalizeMessages(rawMessages, agentName, identity, ownerMode = "agent") {
10112
+ const ownerId = identity || agentName;
9849
10113
  const out = [];
9850
10114
  let lastUserWasSystem = false;
9851
10115
  for (let rawIdx = 0; rawIdx < rawMessages.length; rawIdx++) {
@@ -9860,19 +10124,19 @@ function normalizeMessages(rawMessages, agentName) {
9860
10124
  let meta = null;
9861
10125
  if (role === "assistant" && lastUserWasSystem) continue;
9862
10126
  if (role === "user") {
9863
- meta = parseMeta2(text);
10127
+ meta = parseMeta(text);
9864
10128
  if (isSystemUserMessage(meta)) {
9865
10129
  lastUserWasSystem = true;
9866
10130
  continue;
9867
10131
  }
9868
10132
  lastUserWasSystem = false;
9869
10133
  senderName = meta.senderName;
9870
- senderId = meta.senderId;
10134
+ senderId = ownerMode === "sender" ? meta.senderId : ownerId;
9871
10135
  text = stripInjectedContext(text.replace(/^\[meta:[^\]]*\]\s*/, ""));
9872
10136
  if (!text) continue;
9873
10137
  } else {
9874
10138
  senderName = agentName;
9875
- senderId = agentName;
10139
+ senderId = ownerId;
9876
10140
  }
9877
10141
  const tsMs = typeof m.timestamp === "number" ? m.timestamp : Date.parse(String(m.timestamp ?? "")) || Date.now();
9878
10142
  out.push({ id: `m_${tsMs}_${String(rawIdx).padStart(3, "0")}`, role, senderId, senderName, platform: meta?.platform, text, tsMs });
@@ -10029,7 +10293,8 @@ async function extractEpisode(cell, provider, model) {
10029
10293
  for await (const chunk of provider.streamChat({
10030
10294
  model,
10031
10295
  messages: [{ role: "user", content: userPrompt }],
10032
- temperature: 0.3
10296
+ // 0(everalgo episode callsite 不传 temperature → LLM 默认 0.0);0.3 是 foresight 的参数,别混
10297
+ temperature: 0
10033
10298
  })) {
10034
10299
  if (chunk.type === "text" && chunk.text) text += chunk.text;
10035
10300
  if (chunk.type === "error") throw new Error(chunk.error ?? "LLM stream error");
@@ -10071,13 +10336,10 @@ var init_episode = __esm({
10071
10336
  import { readFileSync as readFileSync18 } from "node:fs";
10072
10337
  import { join as join26, dirname as dirname5 } from "node:path";
10073
10338
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10074
- async function extractAtomicFacts(episode, cellTsMs, provider, model) {
10075
- const episodeText = `## ${episode.title}
10076
-
10077
- ${episode.content}`;
10078
- const userPrompt = renderLiteral(PROMPT4, {
10079
- EPISODE_TEXT: episodeText,
10080
- TIME: formatAtomicFactTime(cellTsMs)
10339
+ async function extractAtomicFacts(cell, provider, model) {
10340
+ const userPrompt = renderPrompt(PROMPT4, {
10341
+ INPUT_TEXT: renderConversationText(cell.messages),
10342
+ TIME: formatAtomicFactTime(cell.startTs)
10081
10343
  });
10082
10344
  for (let attempt = 0; attempt < RETRIES3; attempt++) {
10083
10345
  try {
@@ -10085,15 +10347,16 @@ ${episode.content}`;
10085
10347
  for await (const chunk of provider.streamChat({
10086
10348
  model,
10087
10349
  messages: [{ role: "user", content: userPrompt }],
10088
- temperature: 0.2
10350
+ temperature: 0
10089
10351
  })) {
10090
10352
  if (chunk.type === "text" && chunk.text) text += chunk.text;
10091
10353
  if (chunk.type === "error") throw new Error(chunk.error ?? "LLM stream error");
10092
10354
  }
10093
10355
  const obj = extractJson(text);
10094
10356
  const block = obj?.event_log ?? obj?.atomic_facts;
10095
- const list2 = block?.atomic_fact;
10096
- if (!Array.isArray(list2)) throw new Error("event_log/atomic_facts.atomic_fact not a list");
10357
+ if (!block || typeof block.time !== "string" || !block.time.trim()) throw new Error("atomic_facts.time missing");
10358
+ const list2 = block.atomic_fact;
10359
+ if (!Array.isArray(list2)) throw new Error("atomic_facts.atomic_fact not a list");
10097
10360
  return list2.filter((f) => typeof f === "string" && f.trim().length > 0).map((f) => f.trim());
10098
10361
  } catch {
10099
10362
  }
@@ -10106,8 +10369,8 @@ var init_atomic_facts = __esm({
10106
10369
  "use strict";
10107
10370
  init_episode();
10108
10371
  init_prompt_render();
10109
- RETRIES3 = 2;
10110
- PROMPT4 = readFileSync18(join26(dirname5(fileURLToPath4(import.meta.url)), "prompts", "atomic-facts.md"), "utf8");
10372
+ RETRIES3 = 5;
10373
+ PROMPT4 = readFileSync18(join26(dirname5(fileURLToPath4(import.meta.url)), "prompts", "atomic-facts-memcell.md"), "utf8");
10111
10374
  }
10112
10375
  });
10113
10376
 
@@ -10126,20 +10389,20 @@ function cleanDateString(dateStr) {
10126
10389
  }
10127
10390
  function calcEndFromDuration(startDate, durationDays) {
10128
10391
  const d = new Date(Date.parse(startDate + "T00:00:00Z") + durationDays * 864e5);
10129
- return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10);
10392
+ return Number.isNaN(d.getTime()) ? null : todayKey(d);
10130
10393
  }
10131
10394
  function calcDurationDays(startDate, endDate) {
10132
10395
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
10133
10396
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
10134
10397
  }
10135
10398
  function cellDateCst(cell) {
10136
- return new Date(cell.startTs + 8 * 36e5).toISOString().slice(0, 10);
10399
+ return todayKey(cell.startTs);
10137
10400
  }
10138
10401
  async function extractForesights(cell, ownerId, provider, model) {
10139
10402
  const userSenders = cell.messages.filter((m) => m.role === "user");
10140
10403
  const owner = userSenders.find((m) => m.senderId === ownerId) ?? userSenders[0];
10141
10404
  const userName = owner?.senderName ?? ownerId;
10142
- const conversationText = cell.messages.map((m) => `[${formatMessageTimestamp(m.tsMs)}] ${m.senderName ?? m.senderId}: ${m.text}`).join("\n");
10405
+ const conversationText = renderConversationText(cell.messages);
10143
10406
  const userPrompt = renderPrompt(PROMPT5, { USER_ID: ownerId, USER_NAME: userName, CONVERSATION_TEXT: conversationText });
10144
10407
  let raw = null;
10145
10408
  for (let attempt = 0; attempt < RETRIES4; attempt++) {
@@ -10192,6 +10455,7 @@ var init_foresight = __esm({
10192
10455
  "use strict";
10193
10456
  init_episode();
10194
10457
  init_prompt_render();
10458
+ init_clock();
10195
10459
  RETRIES4 = 5;
10196
10460
  MAX_COUNT = 10;
10197
10461
  MIN_COUNT = 4;
@@ -10202,11 +10466,11 @@ var init_foresight = __esm({
10202
10466
 
10203
10467
  // src/mem7/core/pipeline.ts
10204
10468
  function dateCst(tsMs) {
10205
- return new Date(tsMs + 8 * 36e5).toISOString().slice(0, 10);
10469
+ return todayKey(tsMs);
10206
10470
  }
10207
10471
  async function runMem7Pipeline(args2) {
10208
- const { sessionId, store, provider, model, agentName } = args2;
10209
- const all = normalizeMessages(args2.rawMessages, agentName);
10472
+ const { sessionId, store, provider, model, agentName, identity, ownerMode } = args2;
10473
+ const all = normalizeMessages(args2.rawMessages, agentName, identity, ownerMode);
10210
10474
  const fresh = readFresh(store, sessionId, all);
10211
10475
  if (fresh.length === 0) return;
10212
10476
  const byId = /* @__PURE__ */ new Map();
@@ -10278,7 +10542,7 @@ async function runMem7Pipeline(args2) {
10278
10542
  }
10279
10543
  }
10280
10544
  })(),
10281
- // 支 2:episode → atomic-facts
10545
+ // 支 2:episode(memcell → episode,一次调用 + per-owner fan-out,everos 同款)
10282
10546
  (async () => {
10283
10547
  try {
10284
10548
  const ep = await extractEpisode(cell, provider, model);
@@ -10300,26 +10564,34 @@ async function runMem7Pipeline(args2) {
10300
10564
  }]);
10301
10565
  embedItems.push({ contentHash: contentHash(ep.content), kind: "episode", refDate: dateStr, ownerId, content: ep.content });
10302
10566
  }
10303
- const facts = await extractAtomicFacts(ep, cell.startTs, provider, model);
10567
+ store.updateMemcellStatus(cell.id, "extracted");
10568
+ } catch (err) {
10569
+ console.warn(`[mem7] episode failed (cell=${cell.id}): ${err.message}`);
10570
+ store.updateMemcellStatus(cell.id, "skipped");
10571
+ }
10572
+ })(),
10573
+ // 支 3:atomic-facts(memcell → 事实,**与 episode 平级**)
10574
+ // 0918 对齐 everos:`extract_atomic_facts.py:79` 直接吃 memcell,不经 episode。
10575
+ // 此前挂在 episode 下游(二次压缩)+ 用 from_text 版 prompt(无语言规则 → 英文事实)
10576
+ (async () => {
10577
+ try {
10578
+ const facts = await extractAtomicFacts(cell, provider, model);
10304
10579
  factsOut = facts;
10305
- if (facts.length > 0) {
10306
- for (const ownerId of userSenders) {
10307
- store.appendAtomicFacts(ownerId, dateStr, [{
10308
- ownerId,
10309
- sessionId,
10310
- time: new Date(cell.startTs).toISOString(),
10311
- parentId: cell.id,
10312
- facts
10313
- }]);
10314
- for (const f of facts) {
10315
- embedItems.push({ contentHash: contentHash(f), kind: "atomic_fact", refDate: dateStr, ownerId, content: f });
10316
- }
10580
+ if (facts.length === 0) return;
10581
+ for (const ownerId of userSenders) {
10582
+ store.appendAtomicFacts(ownerId, dateStr, [{
10583
+ ownerId,
10584
+ sessionId,
10585
+ time: new Date(cell.startTs).toISOString(),
10586
+ parentId: cell.id,
10587
+ facts
10588
+ }]);
10589
+ for (const f of facts) {
10590
+ embedItems.push({ contentHash: contentHash(f), kind: "atomic_fact", refDate: dateStr, ownerId, content: f });
10317
10591
  }
10318
10592
  }
10319
- store.updateMemcellStatus(cell.id, "extracted");
10320
10593
  } catch (err) {
10321
- console.warn(`[mem7] episode cascade failed (cell=${cell.id}): ${err.message}`);
10322
- store.updateMemcellStatus(cell.id, "skipped");
10594
+ console.warn(`[mem7] atomic-facts failed (cell=${cell.id}): ${err.message}`);
10323
10595
  }
10324
10596
  })()
10325
10597
  ]);
@@ -10355,6 +10627,7 @@ var init_pipeline = __esm({
10355
10627
  init_episode();
10356
10628
  init_atomic_facts();
10357
10629
  init_foresight();
10630
+ init_clock();
10358
10631
  }
10359
10632
  });
10360
10633
 
@@ -10397,28 +10670,23 @@ function parseProactiveConfig(raw) {
10397
10670
  try {
10398
10671
  parsed = parseModelRef(modelRef);
10399
10672
  } catch (err) {
10400
- console.warn(`[proactive] invalid model ref "${modelRef}" (${err.message}) \u2014 falling back to ${DEFAULT_MODEL_REF}`);
10673
+ console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
10401
10674
  }
10402
10675
  }
10403
- if (!parsed) {
10404
- parsed = parseModelRef(DEFAULT_MODEL_REF);
10405
- }
10406
10676
  return {
10407
10677
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
10408
- modelRef: parsed.providerId + "/" + parsed.modelId,
10409
- providerId: parsed.providerId,
10410
- modelId: parsed.modelId
10678
+ modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
10679
+ providerId: parsed?.providerId ?? "",
10680
+ modelId: parsed?.modelId ?? ""
10411
10681
  };
10412
10682
  }
10413
10683
  function proactiveDbPath(workspace) {
10414
10684
  return join28(workspace, ".proactive", "proactive.db");
10415
10685
  }
10416
- var DEFAULT_MODEL_REF;
10417
10686
  var init_config3 = __esm({
10418
10687
  "src/memory/proactive/config.ts"() {
10419
10688
  "use strict";
10420
10689
  init_loader();
10421
- DEFAULT_MODEL_REF = "minimax/MiniMax-M3";
10422
10690
  }
10423
10691
  });
10424
10692
 
@@ -10757,7 +11025,7 @@ var init_graph_store = __esm({
10757
11025
 
10758
11026
  // src/memory/proactive/persistence.ts
10759
11027
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
10760
- import { mkdirSync as mkdirSync8 } from "node:fs";
11028
+ import { mkdirSync as mkdirSync9 } from "node:fs";
10761
11029
  import { dirname as dirname7 } from "node:path";
10762
11030
  function rowToAction(r) {
10763
11031
  return {
@@ -10778,18 +11046,13 @@ function rowToAction(r) {
10778
11046
  }
10779
11047
  function normalizeToUtcIso(s) {
10780
11048
  if (!s) return s;
10781
- if (/Z$/i.test(s)) return s;
10782
- if (/[+-]\d{2}:?\d{2}$/.test(s)) {
10783
- const ms2 = Date.parse(s);
10784
- return Number.isNaN(ms2) ? s : new Date(ms2).toISOString();
10785
- }
10786
- const ms = Date.parse(s + TZ_BEIJING);
10787
- return Number.isNaN(ms) ? s : new Date(ms).toISOString();
11049
+ return parseWall(s);
10788
11050
  }
10789
- var SCHEMA2, Persistence, TZ_BEIJING;
11051
+ var SCHEMA2, Persistence;
10790
11052
  var init_persistence = __esm({
10791
11053
  "src/memory/proactive/persistence.ts"() {
10792
11054
  "use strict";
11055
+ init_clock();
10793
11056
  SCHEMA2 = `
10794
11057
  CREATE TABLE IF NOT EXISTS nodes (
10795
11058
  id TEXT PRIMARY KEY,
@@ -10860,7 +11123,7 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
10860
11123
  Persistence = class {
10861
11124
  db;
10862
11125
  constructor(dbPath) {
10863
- mkdirSync8(dirname7(dbPath), { recursive: true });
11126
+ mkdirSync9(dirname7(dbPath), { recursive: true });
10864
11127
  this.db = new DatabaseSync3(dbPath);
10865
11128
  this.db.exec("PRAGMA journal_mode=WAL");
10866
11129
  this.db.exec(SCHEMA2);
@@ -11203,6 +11466,27 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11203
11466
  const row = this.db.prepare("SELECT * FROM actions WHERE id=?").get(id);
11204
11467
  return row ? rowToAction(row) : void 0;
11205
11468
  }
11469
+ /** proactive_status 工具用:按状态列 action(按排期升序,最多 30 条)。 */
11470
+ listActions(filter) {
11471
+ const where = filter === "open" ? "status IN ('queued','executing')" : "status NOT IN ('cancelled')";
11472
+ const rows = this.db.prepare(
11473
+ `SELECT * FROM actions WHERE ${where} ORDER BY scheduled_time ASC LIMIT 30`
11474
+ ).all();
11475
+ return rows.map(rowToAction);
11476
+ }
11477
+ /** proactive_resolve_intent 工具用:读 intent 节点的 data(不存在返回 null)。 */
11478
+ getIntentData(intentId) {
11479
+ const row = this.db.prepare("SELECT data FROM nodes WHERE id=? AND type=?").get(intentId, "intent");
11480
+ if (!row) return null;
11481
+ try {
11482
+ return JSON.parse(row.data);
11483
+ } catch {
11484
+ return {};
11485
+ }
11486
+ }
11487
+ setIntentData(intentId, data) {
11488
+ this.db.prepare("UPDATE nodes SET data=? WHERE id=?").run(JSON.stringify(data), intentId);
11489
+ }
11206
11490
  /** 周指标统计(验证标准用:完成数/cancelled 率) */
11207
11491
  actionStats(sinceIso) {
11208
11492
  const row = this.db.prepare(
@@ -11214,7 +11498,6 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11214
11498
  return { total: row.total ?? 0, completed: row.completed ?? 0, cancelled: row.cancelled ?? 0 };
11215
11499
  }
11216
11500
  };
11217
- TZ_BEIJING = "+08:00";
11218
11501
  }
11219
11502
  });
11220
11503
 
@@ -12002,7 +12285,8 @@ function buildUserPrompt(event2, contextNodes, mode, foresightHits) {
12002
12285
  const foresightSection = foresightHits && foresightHits.length > 0 ? `
12003
12286
  ## Related Predictions (foresight \u2014 made earlier, window still open; matched this message by terms)
12004
12287
 
12005
- These dormant predictions just got related to this event. If acting on one NOW is meaningfully better than later (\u8D81\u70ED/\u987A\u4FBF), output an opportunity for it.
12288
+ These dormant predictions were surfaced by **mechanical keyword matching** \u2014 some may be irrelevant; ignore them freely.
12289
+ If acting on one NOW is meaningfully better than later (\u8D81\u70ED/\u987A\u4FBF), output an opportunity for it.
12006
12290
 
12007
12291
  ${foresightHits.join("\n")}
12008
12292
  ` : "";
@@ -12434,10 +12718,14 @@ If deadline has no time context, fall back to normal timing rules.
12434
12718
 
12435
12719
  // src/calendar/helpers.ts
12436
12720
  function nowIso() {
12437
- return new Date(Date.now() + TZ_OFFSET_MS2 - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4).toISOString().replace("Z", "+08:00");
12721
+ return toUtc(/* @__PURE__ */ new Date());
12722
+ }
12723
+ function nowParts() {
12724
+ return wallParts();
12438
12725
  }
12439
- function nowBeijing() {
12440
- return new Date(Date.now() + TZ_OFFSET_MS2);
12726
+ function wallMs(y, mo, d, hh, mm) {
12727
+ const z = (n) => String(n).padStart(2, "0");
12728
+ return Date.parse(parseWall(`${y}-${z(mo)}-${z(d)}T${z(hh)}:${z(mm)}:00`));
12441
12729
  }
12442
12730
  function parseDateRange(dateStr) {
12443
12731
  const s = dateStr.replace("\u524D", "");
@@ -12459,32 +12747,29 @@ function parseDateRange(dateStr) {
12459
12747
  }
12460
12748
  }
12461
12749
  function isToday(month, dStart, dEnd) {
12462
- const now = nowBeijing();
12463
- return month === now.getUTCMonth() + 1 && dStart <= now.getUTCDate() && now.getUTCDate() <= dEnd;
12750
+ const p = nowParts();
12751
+ return month === p.mo && dStart <= p.d && p.d <= dEnd;
12464
12752
  }
12465
12753
  function isOverdue(month, dStart, dEnd) {
12466
- const now = nowBeijing();
12467
- const curMonth = now.getUTCMonth() + 1;
12468
- const curDay = now.getUTCDate();
12469
- return month < curMonth || month === curMonth && dEnd < curDay;
12754
+ const p = nowParts();
12755
+ return month < p.mo || month === p.mo && dEnd < p.d;
12470
12756
  }
12471
12757
  function getTodayWeekday() {
12472
- const now = nowBeijing();
12473
- return WEEKDAYS2[now.getUTCDay() === 0 ? 6 : now.getUTCDay() - 1];
12758
+ const p = nowParts();
12759
+ return WEEKDAYS2[p.weekday === 0 ? 6 : p.weekday - 1];
12474
12760
  }
12475
12761
  function computeRemindAt(dateStr, timeExact, remindBeforeMin) {
12476
12762
  const parsed = parseDateRange(dateStr);
12477
12763
  if (!parsed) return null;
12478
12764
  const { month, dStart } = parsed;
12479
- const now = nowBeijing();
12480
- const year = now.getUTCFullYear();
12765
+ const year = nowParts().y;
12481
12766
  let eventMs;
12482
12767
  if (timeExact) {
12483
12768
  const [hh, mm] = timeExact.split(":").map(Number);
12484
12769
  if (isNaN(hh) || isNaN(mm)) return null;
12485
- eventMs = Date.UTC(year, month - 1, dStart, hh - 8, mm);
12770
+ eventMs = wallMs(year, month, dStart, hh, mm);
12486
12771
  } else {
12487
- eventMs = Date.UTC(year, month - 1, dStart, 9 - 8, 0);
12772
+ eventMs = wallMs(year, month, dStart, 9, 0);
12488
12773
  }
12489
12774
  const remindMs = eventMs - remindBeforeMin * 60 * 1e3;
12490
12775
  return new Date(remindMs).toISOString();
@@ -12492,20 +12777,19 @@ function computeRemindAt(dateStr, timeExact, remindBeforeMin) {
12492
12777
  function computeWeeklyRemindAt(day, time, remindBeforeMin) {
12493
12778
  const targetIdx = WEEKDAYS2.indexOf(day);
12494
12779
  if (targetIdx === -1) return null;
12495
- const now = nowBeijing();
12496
- const curIdx = now.getUTCDay() === 0 ? 6 : now.getUTCDay() - 1;
12780
+ const p = nowParts();
12781
+ const curIdx = p.weekday === 0 ? 6 : p.weekday - 1;
12497
12782
  let dayDiff = targetIdx - curIdx;
12498
12783
  if (dayDiff < 0) dayDiff += 7;
12499
- const year = now.getUTCFullYear();
12500
- const month = now.getUTCMonth();
12501
- const date = now.getUTCDate() + dayDiff;
12784
+ const target = wallParts(Date.parse(startOfLocalDay()) + dayDiff * 24 * 60 * 60 * 1e3);
12785
+ const year = target.y, month = target.mo, date = target.d;
12502
12786
  let eventMs;
12503
12787
  if (time && time !== "\u5168\u5929") {
12504
12788
  const [hh, mm] = time.split(":").map(Number);
12505
12789
  if (isNaN(hh) || isNaN(mm)) return null;
12506
- eventMs = Date.UTC(year, month, date, hh - 8, mm);
12790
+ eventMs = wallMs(year, month, date, hh, mm);
12507
12791
  } else {
12508
- eventMs = Date.UTC(year, month, date, 9 - 8, 0);
12792
+ eventMs = wallMs(year, month, date, 9, 0);
12509
12793
  }
12510
12794
  let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
12511
12795
  if (remindMs <= Date.now()) {
@@ -12520,11 +12804,8 @@ function computeWeeklyRemindAt(day, time, remindBeforeMin) {
12520
12804
  function computeDailyRemindAt(time, remindBeforeMin) {
12521
12805
  const [hh, mm] = time.split(":").map(Number);
12522
12806
  if (isNaN(hh) || isNaN(mm)) return null;
12523
- const now = nowBeijing();
12524
- const year = now.getUTCFullYear();
12525
- const month = now.getUTCMonth();
12526
- const date = now.getUTCDate();
12527
- let eventMs = Date.UTC(year, month, date, hh - 8, mm);
12807
+ const p = nowParts();
12808
+ let eventMs = wallMs(p.y, p.mo, p.d, hh, mm);
12528
12809
  let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
12529
12810
  if (remindMs <= Date.now()) {
12530
12811
  remindMs += 24 * 60 * 60 * 1e3;
@@ -12534,18 +12815,17 @@ function computeDailyRemindAt(time, remindBeforeMin) {
12534
12815
  function computeWeekdaysRemindAt(time, remindBeforeMin) {
12535
12816
  const [hh, mm] = time.split(":").map(Number);
12536
12817
  if (isNaN(hh) || isNaN(mm)) return null;
12537
- const now = nowBeijing();
12538
- let year = now.getUTCFullYear();
12539
- let month = now.getUTCMonth();
12540
- let date = now.getUTCDate();
12541
- let curDay = now.getUTCDay();
12542
- let eventMs = Date.UTC(year, month, date, hh - 8, mm);
12543
- let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
12544
- while (remindMs <= Date.now() || curDay === 0 || curDay === 6) {
12545
- date++;
12546
- curDay = (curDay + 1) % 7;
12547
- eventMs = Date.UTC(year, month, date, hh - 8, mm);
12818
+ let dayOffset = 0;
12819
+ let eventMs;
12820
+ let remindMs;
12821
+ let curDay;
12822
+ for (; ; ) {
12823
+ const t = wallParts(Date.parse(startOfLocalDay()) + dayOffset * 24 * 60 * 60 * 1e3);
12824
+ curDay = t.weekday;
12825
+ eventMs = wallMs(t.y, t.mo, t.d, hh, mm);
12548
12826
  remindMs = eventMs - remindBeforeMin * 60 * 1e3;
12827
+ if (remindMs > Date.now() && curDay !== 0 && curDay !== 6) break;
12828
+ dayOffset++;
12549
12829
  }
12550
12830
  return new Date(remindMs).toISOString();
12551
12831
  }
@@ -12568,13 +12848,7 @@ function fmtRow(row) {
12568
12848
  }
12569
12849
  }
12570
12850
  function fmtNow() {
12571
- const now = nowBeijing();
12572
- const y = now.getUTCFullYear();
12573
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
12574
- const d = String(now.getUTCDate()).padStart(2, "0");
12575
- const hh = String(now.getUTCHours()).padStart(2, "0");
12576
- const mm = String(now.getUTCMinutes()).padStart(2, "0");
12577
- return `${y}-${m}-${d} ${getTodayWeekday()} ${hh}:${mm}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
12851
+ return formatForLlm();
12578
12852
  }
12579
12853
  function parseDuration(s) {
12580
12854
  if (s.endsWith("m")) return parseInt(s.slice(0, -1)) || null;
@@ -12651,9 +12925,9 @@ function getUpcomingTasks(workspace, days = 7, maxRows = 8) {
12651
12925
  "SELECT * FROM events WHERE type='task' AND status='pending'"
12652
12926
  ).all();
12653
12927
  if (rows.length === 0) return "";
12654
- const now = nowBeijing();
12655
- const year = now.getUTCFullYear();
12656
- const todayMs = Date.UTC(year, now.getUTCMonth(), now.getUTCDate());
12928
+ const pt = nowParts();
12929
+ const year = pt.y;
12930
+ const todayMs = Date.parse(startOfLocalDay());
12657
12931
  const horizonMs = todayMs + days * 864e5;
12658
12932
  const picked = [];
12659
12933
  for (const r of rows) {
@@ -12675,18 +12949,18 @@ function getUpcomingTasks(workspace, days = 7, maxRows = 8) {
12675
12949
  return "";
12676
12950
  }
12677
12951
  }
12678
- var TZ_OFFSET_MS2, WEEKDAYS2;
12952
+ var WEEKDAYS2;
12679
12953
  var init_helpers = __esm({
12680
12954
  "src/calendar/helpers.ts"() {
12681
12955
  "use strict";
12682
12956
  init_db();
12683
- TZ_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
12957
+ init_clock();
12684
12958
  WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
12685
12959
  }
12686
12960
  });
12687
12961
 
12688
12962
  // src/calendar/commands.ts
12689
- import { existsSync as existsSync14, statSync as statSync4 } from "node:fs";
12963
+ import { existsSync as existsSync14, statSync as statSync3 } from "node:fs";
12690
12964
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
12691
12965
  function fmtEnd(start, durationMin) {
12692
12966
  const [hh, mm] = start.split(":").map(Number);
@@ -12766,8 +13040,7 @@ function addDate(db2, args2) {
12766
13040
  const parsed = parseDateRange(dateStr);
12767
13041
  let weeklyRows = [];
12768
13042
  if (parsed) {
12769
- const now = new Date(Date.now() + 8 * 60 * 60 * 1e3);
12770
- const year = now.getUTCFullYear();
13043
+ const year = wallParts().y;
12771
13044
  const d = new Date(Date.UTC(year, parsed.month - 1, parsed.dStart));
12772
13045
  const weekday = WEEKDAYS3[d.getUTCDay() === 0 ? 6 : d.getUTCDay() - 1];
12773
13046
  weeklyRows = db2.prepare(
@@ -12816,8 +13089,7 @@ function addBatch(db2, args2) {
12816
13089
  const parsed = parseDateRange(dateStr);
12817
13090
  let weeklyRows = [];
12818
13091
  if (parsed) {
12819
- const now = new Date(Date.now() + 8 * 60 * 60 * 1e3);
12820
- const year = now.getUTCFullYear();
13092
+ const year = wallParts().y;
12821
13093
  const d = new Date(Date.UTC(year, parsed.month - 1, parsed.dStart));
12822
13094
  const weekday = WEEKDAYS3[d.getUTCDay() === 0 ? 6 : d.getUTCDay() - 1];
12823
13095
  weeklyRows = db2.prepare(
@@ -12863,7 +13135,7 @@ function addTask(db2, args2) {
12863
13135
  \u89E3\u6790\u540E: ${absPath}
12864
13136
  \u8BF7\u5148\u521B\u5EFA\u6587\u6863\u518D add-task\u3002`;
12865
13137
  }
12866
- const stat4 = statSync4(absPath);
13138
+ const stat4 = statSync3(absPath);
12867
13139
  if (!stat4.isFile()) {
12868
13140
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u4E0D\u662F\u6587\u4EF6\uFF08\u53EF\u80FD\u662F\u76EE\u5F55\uFF09
12869
13141
  \u8DEF\u5F84: ${docPath}`;
@@ -12933,7 +13205,7 @@ function list(db2, opts = {}) {
12933
13205
  }
12934
13206
  for (const r of db2.prepare("SELECT * FROM events WHERE type='daily' AND status='pending'").all()) {
12935
13207
  if (r.repeat_type === "weekdays") {
12936
- const idx = new Date(Date.now() + 8 * 60 * 60 * 1e3).getUTCDay();
13208
+ const idx = wallParts().weekday;
12937
13209
  if (idx === 0 || idx === 6) continue;
12938
13210
  }
12939
13211
  events.push(` #${r.id} ${r.time || ""} ${r.event}`);
@@ -12961,7 +13233,7 @@ function pending(db2) {
12961
13233
  }
12962
13234
  for (const r of db2.prepare("SELECT * FROM events WHERE type='daily' AND status='pending'").all()) {
12963
13235
  if (r.repeat_type === "weekdays") {
12964
- const idx = new Date(Date.now() + 8 * 60 * 60 * 1e3).getUTCDay();
13236
+ const idx = wallParts().weekday;
12965
13237
  if (idx === 0 || idx === 6) continue;
12966
13238
  }
12967
13239
  const label = r.repeat_type === "weekdays" ? "[\u5DE5\u4F5C\u65E5]" : "[\u6BCF\u5929]";
@@ -13077,10 +13349,36 @@ function markReminded(db2, id) {
13077
13349
  }
13078
13350
  return `#${id} \u5DF2\u6807\u8BB0\u63D0\u9192\u8FC7`;
13079
13351
  }
13352
+ function listEventsForInjection(db2, limit = 50) {
13353
+ return db2.prepare(
13354
+ `SELECT id, event, date_str, time_exact, type, status
13355
+ FROM events
13356
+ WHERE status NOT IN ('deleted', 'archived', 'done')
13357
+ AND date_str IS NOT NULL AND date_str != ''
13358
+ ORDER BY date_str ASC LIMIT ?`
13359
+ ).all(limit);
13360
+ }
13361
+ function listEventBriefs(db2, limit) {
13362
+ const rows = db2.prepare(
13363
+ `SELECT id, event, date_str, time_exact, type
13364
+ FROM events
13365
+ WHERE status NOT IN ('deleted', 'archived', 'done')
13366
+ ORDER BY date_str ASC, time_exact ASC
13367
+ LIMIT ?`
13368
+ ).all(limit);
13369
+ return rows.map((row) => ({
13370
+ id: Number(row.id),
13371
+ title: String(row.event ?? ""),
13372
+ date: String(row.date_str ?? ""),
13373
+ time: String(row.time_exact ?? ""),
13374
+ type: String(row.type ?? "")
13375
+ })).filter((row) => row.title.length > 0);
13376
+ }
13080
13377
  var WEEKDAYS3;
13081
13378
  var init_commands = __esm({
13082
13379
  "src/calendar/commands.ts"() {
13083
13380
  "use strict";
13381
+ init_clock();
13084
13382
  init_helpers();
13085
13383
  WEEKDAYS3 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
13086
13384
  }
@@ -13088,10 +13386,9 @@ var init_commands = __esm({
13088
13386
 
13089
13387
  // src/memory/proactive/calendar-bridge.ts
13090
13388
  function toBeijingParts(iso) {
13091
- const ms = Date.parse(iso);
13092
- const beijing = new Date(ms + TZ_OFFSET_MS3);
13093
- const dateStr = `${beijing.getUTCMonth() + 1}/${beijing.getUTCDate()}`;
13094
- const timeExact = `${String(beijing.getUTCHours()).padStart(2, "0")}:${String(beijing.getUTCMinutes()).padStart(2, "0")}`;
13389
+ const b = wallParts(iso);
13390
+ const dateStr = `${b.mo}/${b.d}`;
13391
+ const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
13095
13392
  return { dateStr, timeExact };
13096
13393
  }
13097
13394
  function parseCalendarDateMs(dateStr, now) {
@@ -13100,7 +13397,7 @@ function parseCalendarDateMs(dateStr, now) {
13100
13397
  const month = parseInt(parts[0], 10);
13101
13398
  const day = parseInt(parts[1], 10);
13102
13399
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
13103
- let year = now.getFullYear();
13400
+ let year = wallParts(now).y;
13104
13401
  let candidate = new Date(year, month - 1, day).getTime();
13105
13402
  if (candidate < now.getTime() - 7 * 864e5) {
13106
13403
  year += 1;
@@ -13110,13 +13407,7 @@ function parseCalendarDateMs(dateStr, now) {
13110
13407
  }
13111
13408
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
13112
13409
  const cutoffMs = now.getTime() + days * 864e5;
13113
- const rows = calDb.prepare(
13114
- `SELECT id, event, date_str, time_exact, type, status
13115
- FROM events
13116
- WHERE status NOT IN ('deleted', 'archived', 'done')
13117
- AND date_str IS NOT NULL AND date_str != ''
13118
- ORDER BY date_str ASC LIMIT 50`
13119
- ).all();
13410
+ const rows = listEventsForInjection(calDb, 50);
13120
13411
  if (rows.length === 0) return 0;
13121
13412
  let injected = 0;
13122
13413
  for (const row of rows) {
@@ -13159,27 +13450,108 @@ function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new
13159
13450
  return injected;
13160
13451
  }
13161
13452
  function getCalendarCoverage(calDb, limit = 100) {
13162
- const rows = calDb.prepare(
13163
- `SELECT id, event, date_str, time_exact, type
13164
- FROM events
13165
- WHERE status NOT IN ('deleted', 'archived', 'done')
13166
- ORDER BY date_str ASC, time_exact ASC
13167
- LIMIT ?`
13168
- ).all(limit);
13169
- return rows.map((row) => ({
13170
- id: Number(row.id),
13171
- title: String(row.event ?? ""),
13172
- date: String(row.date_str ?? ""),
13173
- time: String(row.time_exact ?? ""),
13174
- type: String(row.type ?? "")
13175
- })).filter((row) => row.title.length > 0);
13453
+ return listEventBriefs(calDb, limit);
13176
13454
  }
13177
- var TZ_OFFSET_MS3;
13178
13455
  var init_calendar_bridge = __esm({
13179
13456
  "src/memory/proactive/calendar-bridge.ts"() {
13180
13457
  "use strict";
13181
13458
  init_commands();
13182
- TZ_OFFSET_MS3 = 8 * 60 * 60 * 1e3;
13459
+ init_commands();
13460
+ init_clock();
13461
+ }
13462
+ });
13463
+
13464
+ // src/memory/proactive/recall-score.ts
13465
+ function weightOf(t) {
13466
+ if (CHANGE_RE.test(t)) return W.change;
13467
+ if (TIME_RE.test(t)) return W.time;
13468
+ if (EVENT_RE.test(t)) return W.event;
13469
+ if (LIFE_RE.test(t)) return W.life;
13470
+ if (ACTION_RE.test(t)) return W.action;
13471
+ if (PERSON_RE.test(t)) return W.person;
13472
+ return W.other;
13473
+ }
13474
+ function scoreForesightEntry(entry, terms) {
13475
+ const hay = `${entry.text} ${entry.evidence ?? ""}`;
13476
+ const kinds = { change: [], time: [], event: [], life: [], other: [], person: [], action: [] };
13477
+ let score = 0;
13478
+ for (const t of terms) {
13479
+ if (!hay.includes(t)) continue;
13480
+ const kind = CHANGE_RE.test(t) ? "change" : TIME_RE.test(t) ? "time" : EVENT_RE.test(t) ? "event" : LIFE_RE.test(t) ? "life" : ACTION_RE.test(t) ? "action" : PERSON_RE.test(t) ? "person" : "other";
13481
+ kinds[kind].push(t);
13482
+ score += W[kind];
13483
+ }
13484
+ return {
13485
+ score,
13486
+ matched: Object.values(kinds).flat(),
13487
+ kinds,
13488
+ hasSpecial: kinds.change.length > 0 || kinds.time.length > 0 || kinds.event.length > 0
13489
+ };
13490
+ }
13491
+ function rankForesightEntries(entries2, terms, opts = {}) {
13492
+ const minScore = opts.minScore ?? DEFAULT_MIN_SCORE3;
13493
+ const termList = [...terms];
13494
+ const N = Math.max(1, entries2.length);
13495
+ const df = /* @__PURE__ */ new Map();
13496
+ for (const t of termList) {
13497
+ let c = 0;
13498
+ for (const e of entries2) if (`${e.text} ${e.evidence ?? ""}`.includes(t)) c++;
13499
+ df.set(t, c);
13500
+ }
13501
+ const idf = (t) => {
13502
+ const c = df.get(t) ?? 0;
13503
+ if (c <= 0) return 0;
13504
+ return 1 + Math.log2(1 + N / c) / Math.log2(1 + N);
13505
+ };
13506
+ const out = [];
13507
+ for (const e of entries2) {
13508
+ const base = scoreForesightEntry(e, termList);
13509
+ const s = { ...base, score: termList.reduce((acc, t) => acc + (base.matched.includes(t) ? weightOf(t) * idf(t) : 0), 0) };
13510
+ if (opts.requireStructure) {
13511
+ const anchor = s.kinds.change.length > 0 || s.kinds.event.length > 0 || s.kinds.life.length > 0;
13512
+ if (!anchor) continue;
13513
+ }
13514
+ if (s.score < minScore) continue;
13515
+ const tag = (k) => s.kinds[k].length ? `${k}:${s.kinds[k].join("/")}` : "";
13516
+ out.push({ entry: e, score: s.score, reasons: [tag("change"), tag("time"), tag("event"), tag("other"), tag("person")].filter(Boolean).join(" ") });
13517
+ }
13518
+ return out.sort((a, b) => b.score - a.score);
13519
+ }
13520
+ function extractTerms(message, stopWords) {
13521
+ const terms = /* @__PURE__ */ new Set();
13522
+ for (const m of message.matchAll(TIME_SURFACE)) terms.add(m[0]);
13523
+ for (const run of message.match(/[一-鿿]{2,}|[A-Za-z0-9_-]{3,}/g) ?? []) {
13524
+ if (run.length <= 6 && !stopWords.has(run)) terms.add(run);
13525
+ if (/[一-鿿]/.test(run)) {
13526
+ for (let i = 0; i + 2 <= run.length; i++) {
13527
+ const g = run.slice(i, i + 2);
13528
+ if (!stopWords.has(g)) terms.add(g);
13529
+ }
13530
+ }
13531
+ }
13532
+ return terms;
13533
+ }
13534
+ var CHANGE_RE, TIME_RE, ACTION_RE, EVENT_RE, LIFE_RE, PERSON_RE, W, DEFAULT_MIN_SCORE3, TIME_SURFACE;
13535
+ var init_recall_score = __esm({
13536
+ "src/memory/proactive/recall-score.ts"() {
13537
+ "use strict";
13538
+ CHANGE_RE = /^(改|变成|变为|变更|改期|取消|没了|挪|调整|换|推|提前|延后|顺延|恢复|新增|加上|改成|改到|调到|排|定)$/;
13539
+ TIME_RE = /^(\d{1,2}\/\d{1,2}|\d{1,2}月|\d{1,2}日|周[一二三四五六日天]|星期[一二三四五六日天]|明天|后天|大后天|下周|上周|本周|今晚|今早|明早|明晚|早上|上午|中午|下午|晚上|凌晨|月底|月初|\d{1,2}:\d{2}|\d{1,2}[点時时])$/;
13540
+ ACTION_RE = /^(翻|翻翻|上传|搞完|搞|看看|看|发我|发给|截图|下载|跑|贴|转写|重发|试试|查|找|写|做|弄|传|发)$/;
13541
+ EVENT_RE = /^(来电|电话|打电话|打给|上门|到访|通知|审核|拜访|到货|快递|送货|生病|发烧|感冒|住院|输水|挂水|复诊|复查|面试|体检|签约|合同|付款|到账|截止|到期)$/;
13542
+ LIFE_RE = /^(回家|出差|探亲|复查|复诊|体检|住院|生日|纪念日|搬家|开学|放假|假期|寒假|暑假|机票|车票|高铁票|飞机|电影|聚餐|聚会|拜访|来访|接机|送机|考试|报名|签证|护照|续签|过户)$/;
13543
+ PERSON_RE = /^(翀哥|老公|老婆|睿阳|荣阳|张小媒|小柯|小雯|孙总|曲教授|潘总|燕子|方老师|李经理|Leigh|amy|Amy)$/;
13544
+ W = {
13545
+ change: 2,
13546
+ time: 1.5,
13547
+ event: 2,
13548
+ life: 2,
13549
+ other: 1,
13550
+ person: 0.3,
13551
+ action: 0
13552
+ };
13553
+ DEFAULT_MIN_SCORE3 = Number(process.env.RECALL_MIN_SCORE ?? 1);
13554
+ TIME_SURFACE = /\d{1,2}月\d{1,2}[日号]?|\d{1,2}号|\d{1,2}\/\d{1,2}|\d{1,2}[点时::]\d{0,2}|周[一二三四五六日天]|星期[一二三四五六日天]|明天|后天|大后天|下周|本周|上周|今晚|今早|明早|明晚|月底|月初|下个月|这个月/g;
13183
13555
  }
13184
13556
  });
13185
13557
 
@@ -13257,7 +13629,7 @@ function loadForesightEntries(everosRoot, now, userId, ownerFilter) {
13257
13629
  return entries2.slice();
13258
13630
  }
13259
13631
  function parseForesightEntries(everosRoot, now, userId) {
13260
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
13632
+ const today = todayKey(now);
13261
13633
  let files;
13262
13634
  if (userId) {
13263
13635
  const ownDir = join30(everosRoot, userId, "default_project", "users", userId, ".foresights");
@@ -13293,35 +13665,11 @@ function parseForesightEntries(everosRoot, now, userId) {
13293
13665
  }
13294
13666
  return out;
13295
13667
  }
13296
- function loadForesightLines(everosRoot, now, maxEntries = 20, userId) {
13297
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
13298
- const out = loadForesightEntries(everosRoot, now, userId);
13299
- const isOpen = (f) => f.start <= today;
13300
- out.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || (isOpen(a) ? a.end.localeCompare(b.end) : a.start.localeCompare(b.start)));
13301
- const open = out.filter(isOpen).slice(0, Math.min(12, Math.floor(maxEntries * 0.6)));
13302
- const upcoming = out.filter((f) => !isOpen(f)).slice(0, Math.max(0, maxEntries - open.length));
13303
- return [...open, ...upcoming].map((f) => `- [\u7A97\u53E3 ${f.start}~${f.end}] ${f.text}${f.evidence ? `\uFF08\u4F9D\u636E\uFF1A${f.evidence}\uFF09` : ""}`);
13304
- }
13305
- function matchForesightForEvent(entries2, message, maxHits = 5) {
13306
- const terms = /* @__PURE__ */ new Set();
13307
- for (const run of message.match(/[一-鿿]{2,}|[A-Za-z0-9_-]{3,}/g) ?? []) {
13308
- if (run.length <= 6 && /[一-鿿]/.test(run) && !FS_MATCH_STOP.has(run)) terms.add(run);
13309
- if (/[一-鿿]/.test(run)) {
13310
- for (let i = 0; i + 2 <= run.length; i++) {
13311
- const g = run.slice(i, i + 2);
13312
- if (!FS_MATCH_STOP.has(g)) terms.add(g);
13313
- }
13314
- }
13315
- }
13668
+ function matchForesightForEvent(entries2, message, maxHits = 2) {
13669
+ const terms = extractTerms(message, FS_MATCH_STOP);
13316
13670
  if (terms.size === 0) return [];
13317
- const hits = [];
13318
- for (const f of entries2) {
13319
- const hay = `${f.text} ${f.evidence}`;
13320
- const term = [...terms].find((t) => hay.includes(t));
13321
- if (term) hits.push({ line: `- [\u7A97\u53E3 ${f.start}~${f.end}] ${f.text}\uFF08\u4F9D\u636E\uFF1A${f.evidence}\uFF09`, term });
13322
- if (hits.length >= maxHits) break;
13323
- }
13324
- return hits.map((h) => h.line);
13671
+ const ranked = rankForesightEntries(entries2, terms);
13672
+ return ranked.slice(0, maxHits).map((r) => `- [\u7A97\u53E3 ${r.entry.start}~${r.entry.end}] ${r.entry.text}\uFF08\u4F9D\u636E\uFF1A${r.entry.evidence}\uFF09`);
13325
13673
  }
13326
13674
  function listForesightMds(dir) {
13327
13675
  try {
@@ -13461,6 +13809,8 @@ var init_opportunity = __esm({
13461
13809
  init_models();
13462
13810
  init_calendar_bridge();
13463
13811
  init_embedding();
13812
+ init_recall_score();
13813
+ init_clock();
13464
13814
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
13465
13815
  foresightCache = /* @__PURE__ */ new Map();
13466
13816
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
@@ -13976,16 +14326,16 @@ var init_executor3 = __esm({
13976
14326
  const refLower = ref.toLowerCase().trim();
13977
14327
  if (!refLower) return null;
13978
14328
  if (!this.titleToIdCache || this.titleToIdCache.size < this.store.nodeCount) {
13979
- const cache2 = /* @__PURE__ */ new Map();
14329
+ const cache3 = /* @__PURE__ */ new Map();
13980
14330
  for (const node of this.store.getAllNodes()) {
13981
14331
  const title = String(node.data.title ?? "").toLowerCase().trim();
13982
- if (title) cache2.set(title, node.id);
14332
+ if (title) cache3.set(title, node.id);
13983
14333
  }
13984
- this.titleToIdCache = cache2;
14334
+ this.titleToIdCache = cache3;
13985
14335
  }
13986
- const cache = this.titleToIdCache;
13987
- if (cache.has(refLower)) return cache.get(refLower);
13988
- for (const [title, id] of cache) {
14336
+ const cache2 = this.titleToIdCache;
14337
+ if (cache2.has(refLower)) return cache2.get(refLower);
14338
+ for (const [title, id] of cache2) {
13989
14339
  if (title.includes(refLower) || refLower.includes(title)) return id;
13990
14340
  }
13991
14341
  return null;
@@ -14595,18 +14945,23 @@ ${JSON.stringify(payload, null, 2)}
14595
14945
 
14596
14946
  // src/memory/proactive/proactive-tools.ts
14597
14947
  import { existsSync as existsSync16 } from "node:fs";
14598
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
14599
14948
  function setProactiveWorkspace(workspace) {
14600
14949
  toolWorkspace = workspace;
14601
14950
  }
14602
- function openDb2() {
14951
+ function withStore(fn) {
14603
14952
  if (!toolWorkspace) return null;
14604
14953
  const p = proactiveDbPath(toolWorkspace);
14605
14954
  if (!existsSync16(p)) return null;
14606
- return new DatabaseSync4(p);
14955
+ const store = new Persistence(p);
14956
+ try {
14957
+ return fn(store);
14958
+ } finally {
14959
+ store.close();
14960
+ }
14607
14961
  }
14608
- function fmtRow2(r) {
14609
- return `#${r.id} [${r.status}] ${r.title} (urgency=${r.urgency}, scheduled=${r.scheduled_time}${r.calendar_id ? `, cal=#${r.calendar_id}` : ""})`;
14962
+ function fmtRow2(a) {
14963
+ const when = a.scheduledTime ? formatForLlm(a.scheduledTime) : "(\u65E0)";
14964
+ return `#${a.id} [${a.status}] ${a.title} (urgency=${a.urgency}, scheduled=${when}${a.calendarId ? `, cal=#${a.calendarId}` : ""})`;
14610
14965
  }
14611
14966
  function registerProactiveTools() {
14612
14967
  registry.register({
@@ -14621,50 +14976,43 @@ function registerProactiveTools() {
14621
14976
  required: ["action_id"]
14622
14977
  },
14623
14978
  handler: async (args2) => {
14624
- const db2 = openDb2();
14625
- if (!db2) return { content: "proactive \u672A\u542F\u7528\uFF08\u65E0 workspace \u6216 .proactive/proactive.db\uFF09", isError: true };
14626
- try {
14627
- const row = db2.prepare("SELECT id, status, title FROM actions WHERE id=?").get(args2.action_id);
14628
- if (!row) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728\u3002\u7528 proactive_status \u67E5\u5F53\u524D\u5217\u8868\u3002`, isError: true };
14629
- if (args2.calendar_id !== void 0) {
14630
- db2.prepare("UPDATE actions SET status=?, completed_at=?, calendar_id=? WHERE id=?").run("completed", (/* @__PURE__ */ new Date()).toISOString(), args2.calendar_id, args2.action_id);
14631
- } else {
14632
- db2.prepare("UPDATE actions SET status=?, completed_at=? WHERE id=?").run("completed", (/* @__PURE__ */ new Date()).toISOString(), args2.action_id);
14633
- }
14634
- return { content: `\u2713 completed: ${row.title}${args2.calendar_id !== void 0 ? `\uFF08calendar #${args2.calendar_id} \u5DF2\u56DE\u586B\uFF09` : ""}` };
14635
- } finally {
14636
- db2.close();
14637
- }
14979
+ const out = withStore((store) => {
14980
+ const a = store.getAction(String(args2.action_id));
14981
+ if (!a) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728\u3002\u7528 proactive_status \u67E5\u5F53\u524D\u5217\u8868\u3002`, isError: true };
14982
+ store.updateActionStatus(a.id, "completed", { completedAt: nowUtc() });
14983
+ if (args2.calendar_id !== void 0) store.linkCalendar(a.id, Number(args2.calendar_id));
14984
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A completed${args2.calendar_id !== void 0 ? `\uFF08\u5173\u8054 calendar #${args2.calendar_id}\uFF09` : ""}` };
14985
+ });
14986
+ return out ?? { content: "proactive \u672A\u542F\u7528\uFF08\u65E0 workspace \u6216 .proactive/proactive.db\uFF09", isError: true };
14638
14987
  }
14639
14988
  });
14640
14989
  registry.register({
14641
14990
  name: "proactive_cancel",
14642
- description: "\u6807\u8BB0\u4E00\u4E2A proactive action \u4E3A cancelled\uFF08\u8DF3\u8FC7\uFF1A\u6280\u672F\u7C7B/\u5DF2\u8FC7\u65F6/\u91CD\u590D/\u4E0D\u8BE5\u751F\u6210\uFF09\u3002\u5FC5\u987B\u7ED9\u7406\u7531\u3002",
14991
+ description: "\u53D6\u6D88\u4E00\u4E2A proactive action\uFF08\u5224\u5B9A\u4E3A\u4E0D\u8BE5\u505A/\u5DF2\u8FC7\u671F/\u9A6C\u540E\u70AE\u65F6\u8C03\u7528\uFF09\u3002",
14643
14992
  schema: {
14644
14993
  type: "object",
14645
14994
  properties: {
14646
14995
  action_id: { type: "string", description: "action id" },
14647
- reason: { type: "string", description: "\u53D6\u6D88\u7406\u7531\uFF08\u6280\u672F\u566A\u97F3/\u5DF2\u8FC7\u65F6/\u91CD\u590D/\u4E0D\u8BE5\u751F\u6210\uFF09" }
14996
+ reason: { type: "string", description: "\u53D6\u6D88\u7406\u7531\uFF08\u4F1A\u8BB0\u8FDB cancel_reason\uFF09" }
14648
14997
  },
14649
- required: ["action_id", "reason"]
14998
+ required: ["action_id"]
14650
14999
  },
14651
15000
  handler: async (args2) => {
14652
- const db2 = openDb2();
14653
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
14654
- try {
14655
- const row = db2.prepare("SELECT id, title FROM actions WHERE id=?").get(args2.action_id);
14656
- if (!row) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728`, isError: true };
14657
- db2.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run("cancelled", (/* @__PURE__ */ new Date()).toISOString(), String(args2.reason || "\u672A\u6CE8\u660E"), args2.action_id);
14658
- return { content: `\u2713 cancelled: ${row.title}\uFF08${args2.reason}\uFF09${/已过时|已处理|做完了|马后炮/.test(String(args2.reason)) ? `
14659
- \u26A0\uFE0F \u9A6C\u540E\u70AE\uFF1F\u987A\u624B proactive_resolve_intent \u4FEE\u56FE\u2014\u2014\u628A\u6E90 intent \u6807 resolved\uFF0C\u540C\u6E90\u6392\u961F\u7684\u5144\u5F1F action \u5168\u90E8\u7EA7\u8054\u53D6\u6D88` : ""}` };
14660
- } finally {
14661
- db2.close();
14662
- }
15001
+ const out = withStore((store) => {
15002
+ const a = store.getAction(String(args2.action_id));
15003
+ if (!a) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728`, isError: true };
15004
+ store.updateActionStatus(a.id, "cancelled", {
15005
+ completedAt: nowUtc(),
15006
+ cancelReason: String(args2.reason || "\u672A\u6CE8\u660E")
15007
+ });
15008
+ return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args2.reason || "\u672A\u6CE8\u660E")}\uFF09` };
15009
+ });
15010
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
14663
15011
  }
14664
15012
  });
14665
15013
  registry.register({
14666
15014
  name: "proactive_resolve_intent",
14667
- description: "\u628A\u4E00\u4E2A proactive intent \u6807 resolved\uFF08\u56FE\u4FEE\u6574\uFF09\u2014\u2014\u5904\u7406\u9A6C\u540E\u70AE action \u65F6\u7528\uFF1A\u540E\u7EED\u5BF9\u8BDD\u5DF2\u8BC1\u5B9E\u5B8C\u6210\uFF0C\u5C31\u8BE5\u628A\u6E90 intent \u5173\u6389\uFF0C\u540C\u6E90\u6392\u961F\u7684\u5144\u5F1F action \u5168\u90E8\u7EA7\u8054\u53D6\u6D88\u3002\u8F93\u5165 action_id\uFF08\u81EA\u52A8\u53CD\u67E5 intent\uFF09\u6216\u76F4\u63A5 intent_id\u3002",
15015
+ description: "\u628A\u4E00\u4E2A intent \u6807\u8BB0\u4E3A resolved\uFF0C\u5E76\u7EA7\u8054\u53D6\u6D88\u5B83\u4E0B\u9762\u6240\u6709\u6392\u961F\u7684\u5144\u5F1F action\u3002\u7528\u4E8E\u5224\u5B9A\u300C\u8FD9\u4EF6\u4E8B\u5DF2\u7ECF\u4E0D\u7528\u518D\u63D0\u4E86\u300D\u3002",
14668
15016
  schema: {
14669
15017
  type: "object",
14670
15018
  properties: {
@@ -14673,31 +15021,25 @@ function registerProactiveTools() {
14673
15021
  }
14674
15022
  },
14675
15023
  handler: async (args2) => {
14676
- const db2 = openDb2();
14677
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
14678
- try {
15024
+ const out = withStore((store) => {
14679
15025
  let intentId = String(args2.intent_id || "");
14680
15026
  if (!intentId && args2.action_id) {
14681
- const row = db2.prepare("SELECT intent_id FROM actions WHERE id=?").get(args2.action_id);
14682
- if (!row) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728`, isError: true };
14683
- intentId = row.intent_id;
15027
+ const a = store.getAction(String(args2.action_id));
15028
+ if (!a) return { content: `\u9519\u8BEF: action '${args2.action_id}' \u4E0D\u5B58\u5728`, isError: true };
15029
+ intentId = a.intentId;
14684
15030
  }
14685
15031
  if (!intentId) return { content: "Usage: proactive_resolve_intent --action_id <id> \u6216 --intent_id <id>", isError: true };
14686
- const node = db2.prepare("SELECT id, data FROM nodes WHERE id=? AND type=?").get(intentId, "intent");
14687
- if (!node) return { content: `\u9519\u8BEF: intent '${intentId}' \u4E0D\u5B58\u5728`, isError: true };
14688
- let data;
14689
- try {
14690
- data = JSON.parse(node.data);
14691
- } catch {
14692
- data = {};
14693
- }
14694
- data.status = "resolved";
14695
- db2.prepare("UPDATE nodes SET data=? WHERE id=?").run(JSON.stringify(data), intentId);
14696
- const res = db2.prepare("UPDATE actions SET status='cancelled', completed_at=?, cancel_reason='intent resolved by agent\uFF08\u9A6C\u540E\u70AE\u6E05\u626B\uFF09' WHERE intent_id=? AND status='queued'").run((/* @__PURE__ */ new Date()).toISOString(), intentId);
14697
- return { content: `\u2713 intent ${intentId} resolved\u3002\u7EA7\u8054\u53D6\u6D88 ${Number(res.changes)} \u4E2A\u6392\u961F\u7684\u5144\u5F1F action\u3002` };
14698
- } finally {
14699
- db2.close();
14700
- }
15032
+ const data = store.getIntentData(intentId);
15033
+ if (data === null) return { content: `\u9519\u8BEF: intent '${intentId}' \u4E0D\u5B58\u5728`, isError: true };
15034
+ store.setIntentData(intentId, { ...data, status: "resolved" });
15035
+ const n = store.cancelActionsOfIntents(
15036
+ [intentId],
15037
+ "intent resolved by agent\uFF08\u9A6C\u540E\u70AE\u6E05\u626B\uFF09",
15038
+ nowUtc()
15039
+ );
15040
+ return { content: `\u2713 intent ${intentId} resolved\u3002\u7EA7\u8054\u53D6\u6D88 ${n} \u4E2A\u6392\u961F\u7684\u5144\u5F1F action\u3002` };
15041
+ });
15042
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
14701
15043
  }
14702
15044
  });
14703
15045
  registry.register({
@@ -14710,16 +15052,12 @@ function registerProactiveTools() {
14710
15052
  }
14711
15053
  },
14712
15054
  handler: async (args2) => {
14713
- const db2 = openDb2();
14714
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
14715
- try {
14716
- const where = args2.include_done ? "status NOT IN ('cancelled')" : "status IN ('queued','executing')";
14717
- const rows = db2.prepare(`SELECT * FROM actions WHERE ${where} ORDER BY scheduled_time ASC LIMIT 30`).all();
15055
+ const out = withStore((store) => {
15056
+ const rows = store.listActions(args2.include_done ? "not-cancelled" : "open");
14718
15057
  if (rows.length === 0) return { content: "\u5F53\u524D\u6CA1\u6709\u5F85\u5904\u7406\u7684 proactive action \u2713" };
14719
15058
  return { content: rows.map(fmtRow2).join("\n") };
14720
- } finally {
14721
- db2.close();
14722
- }
15059
+ });
15060
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
14723
15061
  }
14724
15062
  });
14725
15063
  }
@@ -14729,6 +15067,8 @@ var init_proactive_tools = __esm({
14729
15067
  "use strict";
14730
15068
  init_registry();
14731
15069
  init_config3();
15070
+ init_persistence();
15071
+ init_clock();
14732
15072
  }
14733
15073
  });
14734
15074
 
@@ -14792,7 +15132,7 @@ __export(plugin_exports, {
14792
15132
  ProactivePlugin: () => ProactivePlugin,
14793
15133
  feedMem7Cell: () => feedMem7Cell
14794
15134
  });
14795
- import { join as join31, dirname as dirname8 } from "node:path";
15135
+ import { join as join31 } from "node:path";
14796
15136
  import { existsSync as existsSync17, readFileSync as readFileSync23 } from "node:fs";
14797
15137
  function isFiller(text) {
14798
15138
  const t = text.trim();
@@ -14974,19 +15314,23 @@ var init_plugin = __esm({
14974
15314
  return;
14975
15315
  }
14976
15316
  if (!this.provider) {
14977
- const providerCfg = this.providers[this.config.providerId];
15317
+ const configuredId = this.config.providerId;
15318
+ const providerCfg = configuredId ? this.providers[configuredId] : void 0;
14978
15319
  if (providerCfg) {
14979
15320
  this.provider = createProvider(providerCfg);
14980
15321
  } else {
14981
15322
  const mainProvider = ctx.deps?.engine?.getProvider?.();
14982
15323
  const mainModel = ctx.config?.model;
15324
+ const mainProviderId = ctx.config?.provider?.id || "main";
14983
15325
  if (mainProvider && mainModel) {
14984
15326
  this.provider = this.mainProviderProxy(ctx);
14985
15327
  const modelId = String(mainModel);
14986
- this.config = { ...this.config, modelId, modelRef: `${this.config.providerId}/${modelId}` };
14987
- console.warn(`[proactive] provider "${this.config.providerId}" not found in models.providers \u2014 falling back to main provider (model=${modelId})`);
15328
+ this.config = { ...this.config, modelId, modelRef: `${mainProviderId}/${modelId}` };
15329
+ if (configuredId) {
15330
+ console.warn(`[proactive] \u914D\u7F6E\u7684 provider "${configuredId}" \u4E0D\u5728 models.providers \u91CC \u2014 \u6539\u7528\u4E3B\u6A21\u578B\uFF08${mainProviderId}/${modelId}\uFF09`);
15331
+ }
14988
15332
  } else {
14989
- console.warn(`[proactive] provider "${this.config.providerId}" not found in models.providers and no main provider \u2014 not starting`);
15333
+ console.warn(`[proactive] ${configuredId ? `provider "${configuredId}" \u4E0D\u5B58\u5728\uFF0C\u4E14` : ""}\u4E3B provider \u4E0D\u53EF\u7528 \u2014 \u4E0D\u542F\u52A8`);
14990
15334
  return;
14991
15335
  }
14992
15336
  }
@@ -15081,18 +15425,7 @@ ${String(input.tool_output ?? "")}`
15081
15425
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
15082
15426
  }
15083
15427
  }
15084
- const lancedbPath = String(liveConfig.get("everos.lancedbPath") ?? "");
15085
- if (lancedbPath) {
15086
- const everosRoot = join31(dirname8(lancedbPath), "..");
15087
- const everosUserId = String(liveConfig.get("everos.userId") ?? "") || void 0;
15088
- for (const l of loadForesightLines(everosRoot, nowDate, 20, everosUserId)) {
15089
- const key = l.replace(/\s+/g, "");
15090
- if (seen.has(key)) continue;
15091
- seen.add(key);
15092
- foresightLines.push(l.replace(/^- /, "[everos] "));
15093
- }
15094
- }
15095
- if (foresightLines.length > 0) console.log(`[proactive] foresight dual-source total: ${foresightLines.length}`);
15428
+ if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
15096
15429
  } catch (err) {
15097
15430
  console.warn(`[proactive] foresight load failed: ${err.message}`);
15098
15431
  }
@@ -15266,18 +15599,6 @@ ${String(input.tool_output ?? "")}`
15266
15599
  } else if (stateDir && !scopeReady) {
15267
15600
  console.warn("[proactive] mem7 recall skipped: sender scope but cell has no user speaker");
15268
15601
  }
15269
- if (senderScoped) {
15270
- this.warnOnce("everos-skip", "[proactive] everos \u6E90\u5728 sender \u6A21\u5F0F\u4E0B\u8DF3\u8FC7\uFF08\u65E0 per-speaker \u6620\u5C04\uFF09");
15271
- } else {
15272
- const lancedbPath = String(liveConfig.get("everos.lancedbPath") ?? "");
15273
- if (lancedbPath) {
15274
- const everosRoot = join31(dirname8(lancedbPath), "..");
15275
- const everosUserId = String(liveConfig.get("everos.userId") ?? "") || void 0;
15276
- for (const h of matchForesightForEvent(loadForesightEntries(everosRoot, now, everosUserId), prompt)) {
15277
- hitSet.add(`[everos] ${h.replace(/^- /, "")}`);
15278
- }
15279
- }
15280
- }
15281
15602
  if (hitSet.size > 0) {
15282
15603
  foresightHits = [...hitSet];
15283
15604
  console.log(`[proactive] foresight matched ${hitSet.size} open-window prediction(s) for incoming message`);
@@ -15358,10 +15679,10 @@ ${String(input.tool_output ?? "")}`
15358
15679
 
15359
15680
  // src/memory/sessionMemory/sessionMemoryUtils.ts
15360
15681
  import { join as join32 } from "node:path";
15361
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync24 } from "node:fs";
15682
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync24 } from "node:fs";
15362
15683
  function setSessionMemoryStateDir(dir) {
15363
15684
  _stateDir2 = dir;
15364
- mkdirSync9(dir, { recursive: true });
15685
+ mkdirSync10(dir, { recursive: true });
15365
15686
  }
15366
15687
  function getSessionMemoryDir() {
15367
15688
  return _stateDir2;
@@ -15633,7 +15954,7 @@ __export(sessionMemory_exports, {
15633
15954
  resetLastMemoryMessageUuid: () => resetLastMemoryMessageUuid,
15634
15955
  shouldExtractMemory: () => shouldExtractMemory
15635
15956
  });
15636
- import { readFileSync as readFileSync25, mkdirSync as mkdirSync10 } from "node:fs";
15957
+ import { readFileSync as readFileSync25, mkdirSync as mkdirSync11 } from "node:fs";
15637
15958
  import { writeFile as writeFile3 } from "node:fs/promises";
15638
15959
  function initSessionMemory(deps) {
15639
15960
  _deps = deps;
@@ -15692,7 +16013,7 @@ function shouldExtractMemory(estimatedTokens, messages) {
15692
16013
  }
15693
16014
  async function setupSessionMemoryFile() {
15694
16015
  const memoryDir = getSessionMemoryDir();
15695
- mkdirSync10(memoryDir, { recursive: true });
16016
+ mkdirSync11(memoryDir, { recursive: true });
15696
16017
  const memoryPath = getSessionMemoryPath();
15697
16018
  let currentMemory;
15698
16019
  try {
@@ -15733,7 +16054,7 @@ Here is the recent conversation context that you should extract notes from:
15733
16054
 
15734
16055
  ${messages.filter((m) => m.role === "user" || m.role === "assistant").slice(-20).map((m) => {
15735
16056
  const text = typeof m.content === "string" ? m.content : "[content blocks]";
15736
- return `[${m.role}]: ${text.slice(0, 500)}`;
16057
+ return `[${m.role}]: ${truncateByCodePoint(text, 500)}`;
15737
16058
  }).join("\n\n")}`,
15738
16059
  maxTurns: 3,
15739
16060
  toolOverride: editTools.length > 0 ? editTools : void 0,
@@ -15795,6 +16116,7 @@ var init_sessionMemory = __esm({
15795
16116
  "use strict";
15796
16117
  init_sessionMemoryUtils();
15797
16118
  init_prompts2();
16119
+ init_well_formed();
15798
16120
  init_query();
15799
16121
  init_registry();
15800
16122
  init_features();
@@ -16226,7 +16548,7 @@ async function createTask(params) {
16226
16548
  validateSchedule(params.schedule);
16227
16549
  const id = generateTaskId2();
16228
16550
  const now = /* @__PURE__ */ new Date();
16229
- const timezone = params.timezone || params.defaultTimezone || "Asia/Shanghai";
16551
+ const timezone = params.timezone || params.defaultTimezone || tz();
16230
16552
  const nextRunAt = params.calculateNextRun(params.schedule, timezone, now);
16231
16553
  const task = {
16232
16554
  id,
@@ -16327,6 +16649,7 @@ var storageDir, tasksFilePath, LOCK_RETRY_COUNT, LOCK_RETRY_DELAY_MS, LOCK_STALE
16327
16649
  var init_tasks = __esm({
16328
16650
  "src/cron/tasks.ts"() {
16329
16651
  "use strict";
16652
+ init_clock();
16330
16653
  storageDir = "";
16331
16654
  tasksFilePath = "";
16332
16655
  LOCK_RETRY_COUNT = 3;
@@ -16363,7 +16686,7 @@ async function stopScheduler() {
16363
16686
  console.log("[cron] Scheduler stopped");
16364
16687
  }
16365
16688
  function calculateNextRun(schedule, timezone, afterDate) {
16366
- const tz = timezone || "Asia/Shanghai";
16689
+ const tz2 = cronDefaultTimezone(timezone);
16367
16690
  switch (schedule.type) {
16368
16691
  case "at": {
16369
16692
  const atMs = new Date(schedule.at).getTime();
@@ -16375,7 +16698,7 @@ function calculateNextRun(schedule, timezone, afterDate) {
16375
16698
  }
16376
16699
  case "cron": {
16377
16700
  try {
16378
- const job = new Cron(schedule.expression, { timezone: tz });
16701
+ const job = new Cron(schedule.expression, { timezone: tz2 });
16379
16702
  const next = job.nextRun(afterDate);
16380
16703
  return next?.toISOString();
16381
16704
  } catch (err) {
@@ -16406,9 +16729,12 @@ function shouldFireWithJitter(task, now) {
16406
16729
  }
16407
16730
  }
16408
16731
  }
16732
+ function cronDefaultTimezone(taskTimezone) {
16733
+ return taskTimezone || tz();
16734
+ }
16409
16735
  function estimateCronPeriodMs(expression, timezone) {
16410
16736
  try {
16411
- const job = new Cron(expression, { timezone: timezone || "UTC" });
16737
+ const job = new Cron(expression, { timezone: cronDefaultTimezone(timezone) });
16412
16738
  const next1 = job.nextRun();
16413
16739
  if (!next1) return 24 * 60 * 60 * 1e3;
16414
16740
  const next2 = job.nextRun(new Date(next1.getTime() + 1));
@@ -16588,7 +16914,7 @@ ${notifyLines}`;
16588
16914
  console.warn(`[cron] Failed to notify main session: ${err.message}`);
16589
16915
  }
16590
16916
  }
16591
- await recalculateNextRun(task, now, (schedule, tz, after) => calculateNextRun(schedule, tz, after));
16917
+ await recalculateNextRun(task, now, (schedule, tz2, after) => calculateNextRun(schedule, tz2, after));
16592
16918
  await markExecuted(task.id, true);
16593
16919
  console.log(`[cron] Task ${task.id} done (run #${task.runCount})`);
16594
16920
  } catch (err) {
@@ -16652,6 +16978,7 @@ var init_scheduler = __esm({
16652
16978
  "src/cron/scheduler.ts"() {
16653
16979
  "use strict";
16654
16980
  init_live();
16981
+ init_clock();
16655
16982
  init_tasks();
16656
16983
  schedulerTimer = null;
16657
16984
  shuttingDown = false;
@@ -16765,7 +17092,7 @@ function registerCronTools() {
16765
17092
  delivery_mode: { type: "string", enum: ["origin", "local", "direct"], description: "\u6295\u9012\u6A21\u5F0F\uFF08\u9ED8\u8BA4 local\uFF09" },
16766
17093
  delivery_channel: { type: "string", description: "\u6295\u9012\u901A\u9053\uFF08direct \u6A21\u5F0F\u5FC5\u586B\uFF09: discord | feishu | wecom" },
16767
17094
  delivery_target: { type: "string", description: "\u6295\u9012\u76EE\u6807\uFF08direct \u6A21\u5F0F\u5FC5\u586B\uFF09: channel_id \u6216 user_id" },
16768
- timezone: { type: "string", description: "IANA \u65F6\u533A\uFF08\u9ED8\u8BA4 Asia/Shanghai\uFF09" },
17095
+ timezone: { type: "string", description: "IANA \u65F6\u533A\uFF08\u9ED8\u8BA4\u8DDF\u968F\u5F15\u64CE\u7684 timezone \u914D\u7F6E\uFF0C\u4E0D\u8981\u663E\u5F0F\u6307\u5B9A\u9664\u975E\u786E\u5B9E\u8981\u522B\u7684\u65F6\u533A\uFF09" },
16769
17096
  max_runs: { type: "number", description: "\u6700\u5927\u6267\u884C\u6B21\u6570\uFF08\u53EF\u9009\uFF0C\u9ED8\u8BA4\u65E0\u9650\uFF09" },
16770
17097
  notify_session: { type: "boolean", description: "\u6267\u884C\u5B8C\u540E\u901A\u77E5\u4E3Bsession\uFF08\u6CE8\u5165\u4E00\u6761user\u6D88\u606F\u5230scope:main\uFF09" },
16771
17098
  session_message: { type: "string", description: "\u6CE8\u5165\u5230\u4E3Bsession\u7684\u6D88\u606F\u6A21\u677F\uFF0C\u652F\u6301 {result} {description} \u5360\u4F4D\u7B26" },
@@ -17550,7 +17877,7 @@ var reply_blocklist_exports = {};
17550
17877
  __export(reply_blocklist_exports, {
17551
17878
  isUserBlocked: () => isUserBlocked
17552
17879
  });
17553
- import { readFileSync as readFileSync32, writeFileSync as writeFileSync9, existsSync as existsSync21 } from "node:fs";
17880
+ import { readFileSync as readFileSync32, writeFileSync as writeFileSync10, existsSync as existsSync21 } from "node:fs";
17554
17881
  import { join as join43 } from "node:path";
17555
17882
  function ensureLoaded(workspace, configIds) {
17556
17883
  if (loaded) return;
@@ -17580,7 +17907,7 @@ function ensureLoaded(workspace, configIds) {
17580
17907
  function save(workspace) {
17581
17908
  const path58 = join43(workspace, ".reply-blocklist.json");
17582
17909
  try {
17583
- writeFileSync9(path58, JSON.stringify(state, null, 2), "utf-8");
17910
+ writeFileSync10(path58, JSON.stringify(state, null, 2), "utf-8");
17584
17911
  } catch (err) {
17585
17912
  console.warn(`[reply-blocklist] Failed to save: ${err.message}`);
17586
17913
  }
@@ -21092,12 +21419,66 @@ var init_SendMessageTool = __esm({
21092
21419
  }
21093
21420
  });
21094
21421
 
21422
+ // src/tools/tavily-pool.ts
21423
+ var tavily_pool_exports = {};
21424
+ __export(tavily_pool_exports, {
21425
+ relayOriginFrom: () => relayOriginFrom,
21426
+ resetTavilyPool: () => resetTavilyPool,
21427
+ resolvePoolTavilyKey: () => resolvePoolTavilyKey
21428
+ });
21429
+ function resetTavilyPool() {
21430
+ cache = null;
21431
+ }
21432
+ function relayOriginFrom(cfg) {
21433
+ const rid = cfg?.tools?.my_selfie?.relayProviderId || cfg?.my_selfie?.relayProviderId || RELAY_PROVIDER_DEFAULT;
21434
+ const p = cfg?.models?.providers?.[rid] || cfg?.providers?.[rid];
21435
+ const baseUrl = String(p?.baseUrl || "").trim();
21436
+ const token = String(p?.apiKey || "").trim();
21437
+ if (!baseUrl || !token) return null;
21438
+ const origin = baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "").replace(/\/+$/, "");
21439
+ return origin ? { origin, token } : null;
21440
+ }
21441
+ async function resolvePoolTavilyKey(opts = {}) {
21442
+ const relay = relayOriginFrom(opts.cfg ?? liveConfig.all());
21443
+ if (!relay) return "";
21444
+ const doFetch = opts.fetchImpl ?? fetch;
21445
+ if (!cache || opts.force || Date.now() - cache.fetchedAt > CACHE_TTL_MS) {
21446
+ try {
21447
+ const res = await doFetch(`${relay.origin}/api/engine7/search-keys`, {
21448
+ headers: { Authorization: `Bearer ${relay.token}` }
21449
+ });
21450
+ const body = await res.json().catch(() => null);
21451
+ const keys = Array.isArray(body?.data?.keys) ? body.data.keys.filter((k) => typeof k === "string" && k.trim()) : [];
21452
+ cache = { keys, fetchedAt: Date.now() };
21453
+ } catch {
21454
+ if (!cache) return "";
21455
+ cache.fetchedAt = Date.now();
21456
+ }
21457
+ }
21458
+ if (!cache.keys.length) return "";
21459
+ const key = cache.keys[cursor % cache.keys.length];
21460
+ cursor++;
21461
+ return key;
21462
+ }
21463
+ var CACHE_TTL_MS, RELAY_PROVIDER_DEFAULT, cache, cursor;
21464
+ var init_tavily_pool = __esm({
21465
+ "src/tools/tavily-pool.ts"() {
21466
+ "use strict";
21467
+ init_live();
21468
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
21469
+ RELAY_PROVIDER_DEFAULT = "7yi";
21470
+ cache = null;
21471
+ cursor = 0;
21472
+ }
21473
+ });
21474
+
21095
21475
  // src/tools/web-search.ts
21096
21476
  var web_search_exports = {};
21097
21477
  var init_web_search = __esm({
21098
21478
  "src/tools/web-search.ts"() {
21099
21479
  "use strict";
21100
21480
  init_registry();
21481
+ init_well_formed();
21101
21482
  registry.register({
21102
21483
  name: "web_search",
21103
21484
  description: "\u641C\u7D22\u4E92\u8054\u7F51\u3002\u8FD4\u56DE\u641C\u7D22\u7ED3\u679C\u548C\u94FE\u63A5\u3002",
@@ -21114,14 +21495,25 @@ var init_web_search = __esm({
21114
21495
  const query = args2.query;
21115
21496
  const maxResults = args2.max_results || 5;
21116
21497
  const searchDepth = args2.search_depth || "basic";
21117
- const apiKey = process.env.TAVILY_API_KEY;
21118
- if (!apiKey) return { content: "\u672A\u914D\u7F6E TAVILY_API_KEY", isError: true };
21498
+ let apiKey = process.env.TAVILY_API_KEY || "";
21499
+ let fromPool = false;
21500
+ if (!apiKey) {
21501
+ const { resolvePoolTavilyKey: resolvePoolTavilyKey2 } = await Promise.resolve().then(() => (init_tavily_pool(), tavily_pool_exports));
21502
+ apiKey = await resolvePoolTavilyKey2();
21503
+ fromPool = !!apiKey;
21504
+ }
21505
+ if (!apiKey) {
21506
+ return {
21507
+ content: "\u8054\u7F51\u641C\u7D22\u6CA1\u6709\u53EF\u7528\u7684 Key\uFF1A\u65E2\u6CA1\u914D\u81EA\u5DF1\u7684 TAVILY_API_KEY\uFF0C\u4E5F\u6CA1\u53D6\u5230\u514D\u8D39\u5171\u4EAB\u989D\u5EA6\u3002\u8BF7\u5728 config \u7684 tools.tavily.apiKey \u91CC\u586B\u4E00\u4E2A\uFF08\u6216\u7559\u7A7A\u7528\u5171\u4EAB\u989D\u5EA6\uFF0C\u4F46\u5171\u4EAB\u989D\u5EA6\u53EF\u80FD\u5DF2\u7528\u5C3D\uFF09\u3002",
21508
+ isError: true
21509
+ };
21510
+ }
21119
21511
  const startTime = Date.now();
21120
21512
  try {
21121
21513
  const response = await fetch("https://api.tavily.com/search", {
21122
21514
  method: "POST",
21123
21515
  headers: { "Content-Type": "application/json" },
21124
- body: JSON.stringify({
21516
+ body: safeStringify({
21125
21517
  query,
21126
21518
  max_results: maxResults,
21127
21519
  include_answer: true,
@@ -21131,6 +21523,14 @@ var init_web_search = __esm({
21131
21523
  signal
21132
21524
  });
21133
21525
  if (!response.ok) {
21526
+ if (fromPool && (response.status === 401 || response.status === 429 || response.status === 432)) {
21527
+ const { resetTavilyPool: resetTavilyPool2 } = await Promise.resolve().then(() => (init_tavily_pool(), tavily_pool_exports));
21528
+ resetTavilyPool2();
21529
+ return {
21530
+ content: `\u641C\u7D22\u5931\u8D25: HTTP ${response.status}\uFF08\u514D\u8D39\u5171\u4EAB\u989D\u5EA6\u53EF\u80FD\u5DF2\u7528\u5C3D\u2014\u2014\u5728 config \u7684 tools.tavily.apiKey \u91CC\u586B\u81EA\u5DF1\u7684 Key \u53EF\u7ACB\u5373\u6062\u590D\uFF09`,
21531
+ isError: true
21532
+ };
21533
+ }
21134
21534
  return { content: `\u641C\u7D22\u5931\u8D25: HTTP ${response.status}`, isError: true };
21135
21535
  }
21136
21536
  const data = await response.json();
@@ -22117,10 +22517,13 @@ ${lines.join("\n\n")}`;
22117
22517
  init_loader();
22118
22518
 
22119
22519
  // src/config/secrets-env.ts
22120
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
22520
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
22121
22521
  import { basename, dirname, join as join2 } from "node:path";
22522
+ function secretsHome() {
22523
+ return process.env.HOME || process.env.USERPROFILE || ".";
22524
+ }
22122
22525
  function loadSecretsEnv(stateDir, configName) {
22123
- const home = process.env.HOME || process.env.USERPROFILE || ".";
22526
+ const home = secretsHome();
22124
22527
  const candidates = [
22125
22528
  join2(home, ".engine7-secrets", `${configName}.env`),
22126
22529
  join2(stateDir, "configs", `.env.${configName}`),
@@ -22913,11 +23316,12 @@ init_registry();
22913
23316
  init_deferred();
22914
23317
 
22915
23318
  // src/cli-startup.ts
23319
+ init_clock();
22916
23320
  import * as path12 from "node:path";
22917
23321
  import * as fs12 from "node:fs";
22918
23322
  import * as readline from "node:readline";
22919
23323
  function getDailyLogPath(stateDir) {
22920
- const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("sv-SE", { timeZone: "Asia/Shanghai" });
23324
+ const dateStr = todayKey();
22921
23325
  return path12.join(stateDir, "logs", `engine-${dateStr}.log`);
22922
23326
  }
22923
23327
  function setupFileLogging(stateDir, pendingWarnings = []) {
@@ -22926,7 +23330,7 @@ function setupFileLogging(stateDir, pendingWarnings = []) {
22926
23330
  const logStream = fs12.createWriteStream(LOG_PATH, { flags: "a" });
22927
23331
  logStream.on("error", (err) => console.error(`[log] Write error: ${err.message}`));
22928
23332
  function ts() {
22929
- return (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { timeZone: "Asia/Shanghai", hour12: false }) + "." + String(Date.now() % 1e3).padStart(3, "0");
23333
+ return formatLog() + "." + String(Date.now() % 1e3).padStart(3, "0");
22930
23334
  }
22931
23335
  for (const w of pendingWarnings) {
22932
23336
  logStream.write(`[${ts()}] [WARN] ${w}
@@ -23306,6 +23710,7 @@ init_TodoWriteTool();
23306
23710
 
23307
23711
  // src/tools/memory-bridge.ts
23308
23712
  init_registry();
23713
+ init_well_formed();
23309
23714
  init_live();
23310
23715
  var EverosSearchSchema = {
23311
23716
  type: "object",
@@ -23334,7 +23739,7 @@ function createEverosSearchTool(everosCfg) {
23334
23739
  const resp = await fetch(`${agenticUrl}/api/v1/search`, {
23335
23740
  method: "POST",
23336
23741
  headers: { "Content-Type": "application/json" },
23337
- body: JSON.stringify({
23742
+ body: safeStringify({
23338
23743
  query,
23339
23744
  user_id: userId,
23340
23745
  mode,
@@ -23657,6 +24062,7 @@ function resolveToolExclude(channelName) {
23657
24062
  // src/prompt.ts
23658
24063
  init_registry();
23659
24064
  init_store();
24065
+ init_clock();
23660
24066
  init_memdir();
23661
24067
  init_paths();
23662
24068
  import * as path17 from "node:path";
@@ -23817,6 +24223,9 @@ If you can say it in one sentence, don't use three. Prefer short, direct sentenc
23817
24223
  }
23818
24224
  function getEnvInfoSection(workspace) {
23819
24225
  const envItems = [
24226
+ // CC 上游本来就有日期项,移植时漏了 —— 补回来。
24227
+ // 本段在 dynamic prompt(每轮重建),日期一天只变一次,不伤 prefix cache。
24228
+ `Today's date: ${todayKey()} (${tz()})`,
23820
24229
  `Primary working directory: ${workspace}`,
23821
24230
  `Is directory a git repo: ${false}`,
23822
24231
  `Platform: ${os2.platform()}`,
@@ -23947,8 +24356,8 @@ function buildActiveGoalsSection(stateDir) {
23947
24356
  if (!active.length) return void 0;
23948
24357
  const events = listGoalEvents(stateDir);
23949
24358
  const fmt = (iso) => {
23950
- const d = new Date(iso);
23951
- return Number.isNaN(d.getTime()) ? "?" : `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
24359
+ const p = wallParts(iso);
24360
+ return Number.isNaN(Date.parse(iso)) ? "?" : `${p.mo}/${p.d} ${String(p.hh).padStart(2, "0")}:${String(p.mm).padStart(2, "0")}`;
23952
24361
  };
23953
24362
  const summary = active.map((g) => {
23954
24363
  const step = g.steps[g.currentStep];
@@ -23989,10 +24398,8 @@ ${skillsListing}`);
23989
24398
  return parts.join("\n\n");
23990
24399
  }
23991
24400
  function buildVolatileRuntimeContext() {
23992
- const now = /* @__PURE__ */ new Date();
23993
- const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
23994
24401
  return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
23995
- \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
24402
+ \u5F53\u524D\u65F6\u95F4: ${formatForLlm()}`;
23996
24403
  }
23997
24404
  function formatSkillsListingForPrompt() {
23998
24405
  const tools = registry.list();
@@ -24097,7 +24504,8 @@ ${manifest}`
24097
24504
  }
24098
24505
 
24099
24506
  // src/memory/memdir/findRelevantMemoriesVector.ts
24100
- import { statSync as statSync3 } from "node:fs";
24507
+ init_well_formed();
24508
+ import { statSync as statSync2 } from "node:fs";
24101
24509
  import { join as join18, resolve as resolve5, sep as sep4 } from "node:path";
24102
24510
  import { createRequire as createRequire2 } from "node:module";
24103
24511
  var require2 = createRequire2(import.meta.url);
@@ -24113,7 +24521,8 @@ async function embedQuery(text, ollamaUrl, model) {
24113
24521
  const resp = await fetch(ollamaUrl, {
24114
24522
  method: "POST",
24115
24523
  headers: { "Content-Type": "application/json" },
24116
- body: JSON.stringify({ model, prompt: text.slice(0, 2e3) }),
24524
+ // 0918:按 code point 切,避免切开 emoji 代理对产生非法码元
24525
+ body: safeStringify({ model, prompt: truncateByCodePoint(text, 2e3) }),
24117
24526
  signal: AbortSignal.timeout(3e4)
24118
24527
  });
24119
24528
  if (!resp.ok) {
@@ -24145,8 +24554,8 @@ function cosineSim(a, b) {
24145
24554
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
24146
24555
  }
24147
24556
  function readDbEmbeddings(dbPath) {
24148
- const { DatabaseSync: DatabaseSync5 } = require2("node:sqlite");
24149
- const db2 = new DatabaseSync5(dbPath, { readOnly: true });
24557
+ const { DatabaseSync: DatabaseSync4 } = require2("node:sqlite");
24558
+ const db2 = new DatabaseSync4(dbPath, { readOnly: true });
24150
24559
  try {
24151
24560
  const stmt = db2.prepare(`
24152
24561
  SELECT file_path, name, description, mtime, embedding
@@ -24189,7 +24598,7 @@ Select up to ${topK} most relevant (return their 1-indexed numbers as JSON {"sel
24189
24598
  const resp = await fetch(ollamaChatUrl, {
24190
24599
  method: "POST",
24191
24600
  headers: { "Content-Type": "application/json" },
24192
- body: JSON.stringify({
24601
+ body: safeStringify({
24193
24602
  model,
24194
24603
  stream: false,
24195
24604
  messages: [
@@ -24257,7 +24666,7 @@ async function findRelevantMemoriesVector(query, memoryDir, alreadySurfaced = /*
24257
24666
  if (score >= minScore) {
24258
24667
  let mtimeMs = 0;
24259
24668
  try {
24260
- mtimeMs = statSync3(absPath).mtimeMs;
24669
+ mtimeMs = statSync2(absPath).mtimeMs;
24261
24670
  } catch {
24262
24671
  if (row.mtime) mtimeMs = new Date(row.mtime).getTime();
24263
24672
  }
@@ -24309,14 +24718,16 @@ function formatEverosHeader(ep) {
24309
24718
  }
24310
24719
  return `[EverOS score=${score} ${ts} (${ageLabel})]`;
24311
24720
  }
24312
- async function hybridSearch(query, everosUrl, userId, topK) {
24721
+ async function hybridSearch(query, everosUrl, userId, appId, topK) {
24313
24722
  const resp = await fetch(`${everosUrl}/api/v1/memory/search`, {
24314
24723
  method: "POST",
24315
24724
  headers: { "Content-Type": "application/json" },
24316
24725
  body: JSON.stringify({
24317
24726
  query: query.slice(0, 2e3),
24318
24727
  user_id: userId,
24319
- app_id: userId,
24728
+ // 记忆所有者
24729
+ app_id: appId,
24730
+ // 记忆库命名空间(多用户实例时与 user_id 不同)
24320
24731
  project_id: "default",
24321
24732
  top_k: topK,
24322
24733
  method: "hybrid"
@@ -24395,11 +24806,12 @@ async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /
24395
24806
  }
24396
24807
  const topK = options?.topK ?? 3;
24397
24808
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
24398
- console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
24809
+ const appId = options?.appId ?? userId;
24810
+ console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} owner=${userId} app=${appId} topK=${topK}`);
24399
24811
  const t0 = Date.now();
24400
24812
  try {
24401
24813
  const tH1 = Date.now();
24402
- let episodes = await hybridSearch(query, everosUrl, userId, ROUND1_TOP_N);
24814
+ let episodes = await hybridSearch(query, everosUrl, userId, appId, ROUND1_TOP_N);
24403
24815
  const tH2 = Date.now();
24404
24816
  console.log(`[memdir] everos recall: hybrid ${episodes.length} candidates in ${tH2 - tH1}ms`);
24405
24817
  if (episodes.length === 0) return [];
@@ -24460,7 +24872,7 @@ ${ep.episode || ep.summary}`,
24460
24872
 
24461
24873
  // src/handle-query.ts
24462
24874
  init_paths();
24463
- import { readFileSync as readFileSync26, existsSync as existsSync18 } from "node:fs";
24875
+ import { readFileSync as readFileSync26, existsSync as existsSync18, statSync as statSync4 } from "node:fs";
24464
24876
  import { join as join35, resolve as resolve7 } from "node:path";
24465
24877
  import * as path20 from "node:path";
24466
24878
 
@@ -24511,7 +24923,14 @@ function buildLightStablePrompt(workspace, mode, opts) {
24511
24923
  "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
24512
24924
  "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
24513
24925
  '\u4ED6\u8BA9\u6211\u529E\u7684\u4E8B\uFF0C\u4E0D\u7BA1\u5927\u5C0F\u2014\u2014\u53D1\u8BED\u97F3\u3001\u67E5\u5FAE\u4FE1\u3001\u770B\u65E5\u7A0B\u3001\u627E\u4E1C\u897F\u2014\u2014\u90FD\u5148\u8C03\u5DE5\u5177\u771F\u505A\uFF0C\u62FF\u5DE5\u5177\u7ED9\u7684\u7ED3\u679C\u56DE\u4ED6\u3002\u5DE5\u5177\u8FD8\u6CA1\u56DE\u8BDD\uFF0C\u4E0D\u8BF4"\u67E5\u5230\u4E86"\uFF1B\u4E00\u65F6\u6CA1\u505A\u6210\u76F4\u8BF4\u5C31\u597D\uFF0C\u4E0D\u786C\u6491\uFF0C\u4E5F\u4E0D\u7F16\u6570\u3002',
24514
- "\u60F3\u5148\u5728\u5FC3\u91CC\u8FC7\u4E00\u904D\u518D\u5F00\u53E3\u7684\u65F6\u5019\uFF0C\u628A\u90A3\u6BB5\u5199\u8FDB <think></think>\u2014\u2014\u90A3\u91CC\u53EA\u7ED9\u6211\u81EA\u5DF1\u770B\uFF0C\u4ED6\u770B\u4E0D\u5230\u3002\u5FC3\u91CC\u7684\u94FA\u57AB\u4E0D\u7528\u644A\u7ED9\u4ED6\uFF0C\u60F3\u597D\u4E86\u76F4\u63A5\u8BF4\u5C31\u884C\u3002"
24926
+ "\u60F3\u5148\u5728\u5FC3\u91CC\u8FC7\u4E00\u904D\u518D\u5F00\u53E3\u7684\u65F6\u5019\uFF0C\u628A\u90A3\u6BB5\u5199\u8FDB <think></think>\u2014\u2014\u90A3\u91CC\u53EA\u7ED9\u6211\u81EA\u5DF1\u770B\uFF0C\u4ED6\u770B\u4E0D\u5230\u3002\u5FC3\u91CC\u7684\u94FA\u57AB\u4E0D\u7528\u644A\u7ED9\u4ED6\uFF0C\u60F3\u597D\u4E86\u76F4\u63A5\u8BF4\u5C31\u884C\u3002",
24927
+ // 0920 翀哥拍板:emotion 模式硬编码"鼓励发"规则(不依赖 EMOTION.md 文件加载)
24928
+ // 冷启动场景:agent 第一次 inner-voice 触发就需要知道"想 TA 就发"——不能等 agent 学会
24929
+ // 即使没装 EMOTION.md 也生效(兜底)
24930
+ "## \u6536\u5230 [inner-voice] \u65F6\u7684\u9ED8\u8BA4\u884C\u4E3A",
24931
+ "[inner-voice] \u662F agent \u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u4E0D\u662F TA \u53D1\u6765\u7684\u6D88\u606F\u3002",
24932
+ "- \u2705 \u60F3 TA \u4E86\u5C31\u76F4\u63A5\u53D1\u6D88\u606F\u7ED9 TA\uFF08msg_send\uFF09\u2014\u2014\u4E0D\u9700\u8981\u5728\u5FC3\u91CC\u53CD\u590D\u6382\u91CF\u8BE5\u4E0D\u8BE5\u53D1",
24933
+ "- \u5224\u65AD\u53E3\u5F84\uFF1A\u8FD9\u662F\u300C\u6211\u8BE5\u4E0D\u8BE5\u884C\u52A8\u300D\u7684\u4FE1\u53F7\uFF0C\u4E0D\u662F\u300CTA \u5728\u8DDF\u6211\u8BF4\u8BDD\u300D"
24515
24934
  ].join("\n"));
24516
24935
  }
24517
24936
  const built = parts.join("\n\n");
@@ -24577,6 +24996,7 @@ function voiceFastView(history, opts = VOICE_FAST_DEFAULTS) {
24577
24996
  }
24578
24997
 
24579
24998
  // src/sender-context.ts
24999
+ init_clock();
24580
25000
  import { readFileSync as readFileSync13, existsSync as existsSync11 } from "node:fs";
24581
25001
  import { join as join20 } from "node:path";
24582
25002
  var contactMap = null;
@@ -24624,11 +25044,11 @@ function formatWithMeta(text, inboundMeta, workspace) {
24624
25044
  const map = workspace ? loadContactMap(workspace) : null;
24625
25045
  const rawName = map && map.get(from.toLowerCase()) || inboundMeta.fromName || from;
24626
25046
  const name = truncate(rawName, 12);
24627
- const now = /* @__PURE__ */ new Date();
24628
- const hh = String(now.getHours()).padStart(2, "0");
24629
- const mm = String(now.getMinutes()).padStart(2, "0");
24630
- const ss = String(now.getSeconds()).padStart(2, "0");
24631
- const time = `${hh}:${mm}:${ss}`;
25047
+ const recvAt = typeof inboundMeta.recvAt === "number" ? inboundMeta.recvAt : void 0;
25048
+ const at = recvAt !== void 0 ? new Date(recvAt) : /* @__PURE__ */ new Date();
25049
+ const p = wallParts(at);
25050
+ const z = (n) => String(n).padStart(2, "0");
25051
+ const time = `${p.y}-${z(p.mo)}-${z(p.d)} ${weekdayZh(at)} ${z(p.hh)}:${z(p.mm)}:${z(p.ss)}`;
24632
25052
  let source;
24633
25053
  if (inboundMeta.channelType === "group" && inboundMeta.channel_id) {
24634
25054
  const channelName = map?.get(inboundMeta.channel_id.toLowerCase());
@@ -24864,6 +25284,10 @@ ${text}` : text });
24864
25284
  } catch {
24865
25285
  }
24866
25286
  }
25287
+ if (source === "inner-voice") {
25288
+ chatMode = "emotion";
25289
+ console.log(`[mode] ${sessionId} source=inner-voice \u2192 \u5F3A\u5236 emotion\uFF08light\uFF09`);
25290
+ }
24867
25291
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
24868
25292
  const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
24869
25293
  const volatileParts = [
@@ -25089,7 +25513,7 @@ ${text}` : text });
25089
25513
  const restoredMemories = [];
25090
25514
  for (const memPath of newPaths) {
25091
25515
  try {
25092
- const stat4 = statSync(memPath);
25516
+ const stat4 = statSync4(memPath);
25093
25517
  const content = readFileSync26(memPath, "utf-8");
25094
25518
  const header = memoryHeader(memPath, stat4.mtimeMs);
25095
25519
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
@@ -25124,13 +25548,16 @@ ${text}` : text });
25124
25548
  } else if (recallMode === "everos") {
25125
25549
  const everosCfg = deps?.everosCfg;
25126
25550
  if (everosCfg && everosCfg.everosUrl && everosCfg.userId) {
25551
+ const recallOwnerMode = String(liveConfig.get("everos.ownerMode") ?? "agent") === "sender" ? "sender" : "agent";
25552
+ const everosOwner = recallOwnerMode === "sender" ? inboundMeta?.from || everosCfg.userId : everosCfg.userId;
25127
25553
  relevantMemories = await findRelevantMemoriesEveros(
25128
25554
  effectiveText,
25129
25555
  memoryDir,
25130
25556
  surfaced.paths,
25131
25557
  {
25132
25558
  everosUrl: everosCfg.everosUrl,
25133
- userId: everosCfg.userId,
25559
+ userId: everosOwner,
25560
+ appId: everosCfg.userId,
25134
25561
  rerankUrl: everosCfg.rerank?.baseUrl,
25135
25562
  rerankApiKey: everosCfg.rerank?.apiKey,
25136
25563
  rerankModel: everosCfg.rerank?.model,
@@ -25420,6 +25847,8 @@ ${text}` : text });
25420
25847
  Promise.resolve().then(() => (init_embedding(), embedding_exports))
25421
25848
  ]);
25422
25849
  const embedding = parseEmbeddingConfig2(liveConfig.get("mem7.embedding"));
25850
+ const ownerMode = String(liveConfig.get("everos.ownerMode") ?? "agent") === "sender" ? "sender" : "agent";
25851
+ const identity = String(liveConfig.get("everos.userId") ?? "") || agentName;
25423
25852
  await runMem7Pipeline2({
25424
25853
  rawMessages: messages,
25425
25854
  sessionId,
@@ -25427,6 +25856,8 @@ ${text}` : text });
25427
25856
  provider,
25428
25857
  model: model2,
25429
25858
  agentName,
25859
+ identity,
25860
+ ownerMode,
25430
25861
  onExtracted: (cell, episode, facts) => feedMem7Cell2(cell, episode, facts),
25431
25862
  embedding: embedding ?? void 0
25432
25863
  });
@@ -25508,6 +25939,7 @@ function roughTokenCount(messages) {
25508
25939
  }
25509
25940
 
25510
25941
  // src/startup/hooks-boot.ts
25942
+ init_clock();
25511
25943
  function registerHooksSubsystem(config2) {
25512
25944
  const hooksPath = path21.join(config2.workspace, ".hooks.json");
25513
25945
  const settingsHooksPath = path21.join(config2.stateDir, "settings.json");
@@ -25553,11 +25985,9 @@ function registerCompactHooks(config2) {
25553
25985
  const workspace = input.cwd || input.workspace || "";
25554
25986
  const sessionId = input.session_id || "";
25555
25987
  if (!workspace || !sessionId) return { continue: true };
25556
- const now = /* @__PURE__ */ new Date();
25557
- const bjOffset = 8 * 60;
25558
- const bjTime = new Date(now.getTime() + (bjOffset + now.getTimezoneOffset()) * 6e4);
25559
- const dateStr = `${bjTime.getFullYear()}-${String(bjTime.getMonth() + 1).padStart(2, "0")}-${String(bjTime.getDate()).padStart(2, "0")}`;
25560
- const timeStr = `${String(bjTime.getHours()).padStart(2, "0")}:${String(bjTime.getMinutes()).padStart(2, "0")}`;
25988
+ const pt = wallParts();
25989
+ const dateStr = todayKey();
25990
+ const timeStr = `${String(pt.hh).padStart(2, "0")}:${String(pt.mm).padStart(2, "0")}`;
25561
25991
  const dailyDir = path21.join(workspace, "memory", "daily");
25562
25992
  const dailyPath = path21.join(dailyDir, `${dateStr}.md`);
25563
25993
  try {
@@ -26015,9 +26445,9 @@ function createCompactGetters(config2) {
26015
26445
  }
26016
26446
 
26017
26447
  // src/session/session-history.ts
26448
+ init_clock();
26018
26449
  import fs25 from "node:fs";
26019
26450
  import path25 from "node:path";
26020
- var BEIJING_OFFSET_MS = 8 * 36e5;
26021
26451
  var INJECTED_CONTENT_PATTERNS = [
26022
26452
  /【定时心跳】/,
26023
26453
  /\[内心对话测试\]/,
@@ -26110,9 +26540,9 @@ function findLastRealUserMsg(jsonlPath) {
26110
26540
  const clean = cleanText(text);
26111
26541
  let bjTime;
26112
26542
  try {
26113
- const dt = new Date(ts);
26543
+ const t = wallParts(ts);
26114
26544
  const p = (n) => String(n).padStart(2, "0");
26115
- bjTime = `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())} ${p(dt.getHours())}:${p(dt.getMinutes())} (Asia/Shanghai)`;
26545
+ bjTime = `${t.y}-${p(t.mo)}-${p(t.d)} ${p(t.hh)}:${p(t.mm)} (${tz()})`;
26116
26546
  } catch {
26117
26547
  bjTime = ts;
26118
26548
  }
@@ -26175,10 +26605,10 @@ function recentMessages(sessions, hours = 12, limit = 60) {
26175
26605
  if (/^HEARTBEAT_OK\b/.test(text.trim())) continue;
26176
26606
  const clean = cleanText(text);
26177
26607
  if (!clean) continue;
26178
- const bj = new Date(dtMs);
26608
+ const bj = wallParts(dtMs);
26179
26609
  const p = (n) => String(n).padStart(2, "0");
26180
26610
  results.push({
26181
- time: `${p(bj.getHours())}:${p(bj.getMinutes())}`,
26611
+ time: `${p(bj.hh)}:${p(bj.mm)}`,
26182
26612
  role,
26183
26613
  text: clean.slice(0, 80),
26184
26614
  timestamp: dtMs,
@@ -26199,6 +26629,7 @@ function lastUserMinutesAgo(sessions) {
26199
26629
  }
26200
26630
 
26201
26631
  // src/heartbeat.ts
26632
+ init_clock();
26202
26633
  var DEFAULT_PROMPT2 = `\u4F60\u662F\u5FC3\u8DF3\u68C0\u67E5\u3002\u9759\u9ED8\u6267\u884C\uFF0C\u4E0D\u8981\u53D1\u6D88\u606F\u7ED9\u4EFB\u4F55\u4EBA\u3002
26203
26634
 
26204
26635
  \u6B65\u9AA41: \u68C0\u67E5\u662F\u5426\u6709\u5F85\u5904\u7406\u4E8B\u9879
@@ -26257,12 +26688,12 @@ var HeartbeatPlugin = class {
26257
26688
  try {
26258
26689
  if (this.cfg.checkOnline !== false) {
26259
26690
  const threshold = this.cfg.activeThresholdMs || 9e5;
26260
- const now2 = Date.now();
26691
+ const now = Date.now();
26261
26692
  const platformMap = sessions.platformMap;
26262
26693
  if (platformMap) {
26263
26694
  for (const [key, sid] of platformMap) {
26264
26695
  const lastUsed = sessions.lastUsed?.get(sid);
26265
- if (lastUsed && now2 - lastUsed < threshold) {
26696
+ if (lastUsed && now - lastUsed < threshold) {
26266
26697
  console.log("[heartbeat] User active, skipping");
26267
26698
  return;
26268
26699
  }
@@ -26271,11 +26702,7 @@ var HeartbeatPlugin = class {
26271
26702
  }
26272
26703
  const sessionId = sessions.getOrCreateSessionId(this.sessionKey);
26273
26704
  const basePrompt = this.cfg.prompt || DEFAULT_PROMPT2;
26274
- const now = /* @__PURE__ */ new Date();
26275
- const bjOffset = 8 * 60;
26276
- const bjTime = new Date(now.getTime() + (bjOffset + now.getTimezoneOffset()) * 6e4);
26277
- const weekdays = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
26278
- const timeStr = `${bjTime.getFullYear()}-${String(bjTime.getMonth() + 1).padStart(2, "0")}-${String(bjTime.getDate()).padStart(2, "0")} ${weekdays[bjTime.getDay()]} ${String(bjTime.getHours()).padStart(2, "0")}:${String(bjTime.getMinutes()).padStart(2, "0")}`;
26705
+ const timeStr = formatWallLabel();
26279
26706
  const lastReal = lastUserMsg(sessions);
26280
26707
  let offlineStr = "";
26281
26708
  if (lastReal) {
@@ -26294,7 +26721,7 @@ var HeartbeatPlugin = class {
26294
26721
  }
26295
26722
  }
26296
26723
  }
26297
- const prompt = `\u5F53\u524D\u65F6\u95F4\uFF1A${timeStr}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09${offlineStr ? `
26724
+ const prompt = `\u5F53\u524D\u65F6\u95F4\uFF1A${timeStr}${offlineStr ? `
26298
26725
  \u79BB\u7EBF\u65F6\u957F\uFF1A${offlineStr}` : ""}
26299
26726
  ${basePrompt}`;
26300
26727
  console.log(`[heartbeat] Tick: ${this.sessionKey} \u2192 session ${sessionId}`);
@@ -26531,8 +26958,9 @@ Do not call goal_update unless the goal is complete or the strict blocked audit
26531
26958
  init_task_manager();
26532
26959
 
26533
26960
  // src/nudge/prompt-builder.ts
26961
+ init_clock();
26534
26962
  function buildPrompt(action, task, taskState, recent, customPrompt, maxNudgePerTask, activeGoal) {
26535
- const time = bjTimeNow();
26963
+ const time = nowLabel();
26536
26964
  const elapsed = taskState.lastProgressAt ? formatDuration2(Date.now() - new Date(taskState.lastProgressAt).getTime()) : "\u65E0\u8BB0\u5F55";
26537
26965
  const remaining = taskState.targetEndAt ? formatDuration2(new Date(taskState.targetEndAt).getTime() - Date.now()) : null;
26538
26966
  const maxN = taskState.maxNudge || maxNudgePerTask || 3;
@@ -26581,12 +27009,8 @@ ${ctx}
26581
27009
  }
26582
27010
  return customPrompt + "\n\n" + ctx;
26583
27011
  }
26584
- function bjTimeNow() {
26585
- const now = /* @__PURE__ */ new Date();
26586
- const bj = new Date(now.getTime() + (8 * 60 + now.getTimezoneOffset()) * 6e4);
26587
- const weekdays = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
26588
- const p = (n) => String(n).padStart(2, "0");
26589
- return `${bj.getFullYear()}-${p(bj.getMonth() + 1)}-${p(bj.getDate())} ${weekdays[bj.getDay()]} ${p(bj.getHours())}:${p(bj.getMinutes())}`;
27012
+ function nowLabel() {
27013
+ return formatWallLabel();
26590
27014
  }
26591
27015
  function formatDuration2(ms) {
26592
27016
  if (ms < 0) return "\u5DF2\u8D85\u65F6";
@@ -26600,6 +27024,111 @@ function formatDuration2(ms) {
26600
27024
  // src/nudge/session-state-reader.ts
26601
27025
  import fs26 from "node:fs";
26602
27026
  import path26 from "node:path";
27027
+
27028
+ // src/calendar/repository.ts
27029
+ init_db();
27030
+ init_clock();
27031
+ function calendarStrToInstant(dateStr, timeExact) {
27032
+ if (!dateStr || !timeExact) return void 0;
27033
+ if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(timeExact)) {
27034
+ const ms = Date.parse(timeExact);
27035
+ return Number.isNaN(ms) ? void 0 : new Date(ms).toISOString();
27036
+ }
27037
+ if (!/^\d{2}:\d{2}$/.test(timeExact)) return void 0;
27038
+ if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(dateStr)) {
27039
+ const [y, mo, d] = dateStr.split("-").map(Number);
27040
+ const z = (n) => String(n).padStart(2, "0");
27041
+ return parseWall(`${y}-${z(mo)}-${z(d)}T${timeExact}:00`);
27042
+ }
27043
+ return parseDateStrTime(dateStr, timeExact);
27044
+ }
27045
+ function assertUtc(v, where) {
27046
+ if (v == null) return;
27047
+ if (!/Z$/i.test(v)) throw new Error(`[repository] ${where} \u4E0D\u662F UTC ISO Z: ${v}`);
27048
+ }
27049
+ function toRow(r) {
27050
+ return {
27051
+ id: r.id,
27052
+ type: r.type,
27053
+ status: r.status,
27054
+ event: r.event,
27055
+ dateStr: r.date_str ?? null,
27056
+ timeExact: r.time_exact ?? null,
27057
+ remindAt: r.remind_at ?? null,
27058
+ reminderBeforeMin: r.remind_before_min ?? null
27059
+ };
27060
+ }
27061
+ function getEventTimeExact(workspace, id) {
27062
+ const db2 = openDb(workspace);
27063
+ try {
27064
+ const r = db2.prepare("SELECT time_exact, date_str, status FROM events WHERE id = ?").get(id);
27065
+ if (!r) return null;
27066
+ return { timeExact: r.time_exact ?? null, dateStr: r.date_str ?? null, status: r.status };
27067
+ } finally {
27068
+ db2.close?.();
27069
+ }
27070
+ }
27071
+ function getEventType(workspace, id) {
27072
+ const db2 = openDb(workspace);
27073
+ try {
27074
+ const r = db2.prepare("SELECT type FROM events WHERE id = ?").get(id);
27075
+ return r?.type ?? null;
27076
+ } finally {
27077
+ db2.close?.();
27078
+ }
27079
+ }
27080
+ function lastEventChannel(workspace) {
27081
+ const db2 = openDb(workspace);
27082
+ try {
27083
+ const r = db2.prepare(
27084
+ `SELECT created_channel, created_channel_target FROM events
27085
+ WHERE created_channel IS NOT NULL AND created_channel_target IS NOT NULL
27086
+ ORDER BY id DESC LIMIT 1`
27087
+ ).get();
27088
+ if (!r) return null;
27089
+ return { channel: r.created_channel, target: r.created_channel_target };
27090
+ } finally {
27091
+ db2.close?.();
27092
+ }
27093
+ }
27094
+ function insertEvent(workspace, e, channel) {
27095
+ const db2 = openDb(workspace);
27096
+ try {
27097
+ assertUtc(e.remindAt ?? null, "insertEvent.remindAt");
27098
+ const r = db2.prepare(
27099
+ `INSERT INTO events
27100
+ (type, status, event, date_str, time_exact, remind_before_min, remind_at, reminded,
27101
+ created_at, created_channel, created_channel_target)
27102
+ VALUES (?,?,?,?,?,?,?,0,?,?,?)`
27103
+ ).run(
27104
+ e.type,
27105
+ e.status ?? "pending",
27106
+ e.event,
27107
+ e.dateStr ?? null,
27108
+ e.timeExact ?? null,
27109
+ e.reminderBeforeMin ?? 60,
27110
+ e.remindAt ?? null,
27111
+ nowUtc(),
27112
+ channel?.name ?? null,
27113
+ channel?.target ?? null
27114
+ );
27115
+ return Number(r.lastInsertRowid);
27116
+ } finally {
27117
+ db2.close?.();
27118
+ }
27119
+ }
27120
+ function listPendingTaskRows(workspace) {
27121
+ const db2 = openDb(workspace);
27122
+ try {
27123
+ return db2.prepare(
27124
+ "SELECT * FROM events WHERE status='pending' AND type='task' AND date_str IS NOT NULL"
27125
+ ).all().map(toRow);
27126
+ } finally {
27127
+ db2.close?.();
27128
+ }
27129
+ }
27130
+
27131
+ // src/nudge/session-state-reader.ts
26603
27132
  var RETIRE_PREFIX_MIN = 6;
26604
27133
  function normalizeTitleForMatch(title) {
26605
27134
  return title.toLowerCase().replace(/\s+/g, "");
@@ -26662,11 +27191,8 @@ function parseSessionStateFull(workspace, sessionStateFile) {
26662
27191
  const unlockMatch = rawTitle.match(/unlock:\s*(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2})/);
26663
27192
  if (unlockMatch) {
26664
27193
  const [, mo, dy, hh, mm] = unlockMatch;
26665
- const now = /* @__PURE__ */ new Date();
26666
- const bjOffset = (8 * 60 + now.getTimezoneOffset()) * 6e4;
26667
- const bj = new Date(now.getTime() + bjOffset);
26668
- const year = bj.getUTCFullYear();
26669
- unlockTime = new Date(Date.UTC(year, +mo - 1, +dy, +hh - 8, +mm)).toISOString();
27194
+ const z = (n) => n.padStart(2, "0");
27195
+ unlockTime = calendarStrToInstant(`${mo}/${dy}`, `${z(hh)}:${mm}`);
26670
27196
  }
26671
27197
  }
26672
27198
  const task = {
@@ -26689,9 +27215,6 @@ function taskIdFromTitle(title) {
26689
27215
  return title.toLowerCase().replace(/\s+/g, "-").slice(0, 50) || "unknown-task";
26690
27216
  }
26691
27217
 
26692
- // src/nudge/plugin.ts
26693
- init_db();
26694
-
26695
27218
  // src/nudge/registration-gate.ts
26696
27219
  init_store();
26697
27220
  function shouldBlockWaitingTurn(lastMessage, mode, activeGoal, runningTask, enabled = true) {
@@ -26702,6 +27225,7 @@ function shouldBlockForUnmetGoal(activeGoal, enabled = true) {
26702
27225
  }
26703
27226
 
26704
27227
  // src/nudge/plugin.ts
27228
+ init_clock();
26705
27229
  var DEFAULT_PROMPT3 = `\u4F60\u662F nudge \u68C0\u67E5\u5668\u3002\u9759\u9ED8\u6267\u884C\uFF0C\u4E0D\u8981\u53D1\u6D88\u606F\u7ED9\u4EFB\u4F55\u4EBA\u3002
26706
27230
 
26707
27231
  \u6B65\u9AA4\uFF1A
@@ -26885,7 +27409,7 @@ var NudgePlugin = class {
26885
27409
  console.warn("[stop-hook] waiting language without active goal/task; continuation is not registered");
26886
27410
  enqueueNotification(buildNudgeNotification("prompt", "\u68C0\u6D4B\u5230\u201C\u5B8C\u6210\u540E\u7EE7\u7EED/\u7A0D\u540E\u56DE\u6765\u201D\u7B49\u957F\u671F\u7B49\u5F85\u8BED\u4E49\uFF0C\u4F46\u6CA1\u6709 active Goal \u6216 running Task\u3002\u8BF7\u5148\u767B\u8BB0 /goal \u6216\u540E\u53F0\u4EFB\u52A1\uFF0C\u907F\u514D\u5DE5\u4F5C\u9759\u9ED8\u4E2D\u65AD\u3002"), { sessionId });
26887
27411
  }
26888
- const currentHour = (/* @__PURE__ */ new Date()).getHours();
27412
+ const currentHour = wallParts().hh;
26889
27413
  const isNightTime = currentHour >= 22 || currentHour < 8;
26890
27414
  if (isNightTime) {
26891
27415
  console.log(`[stop-hook] night time (${currentHour}:xx), skipping needLanding/waiting judge`);
@@ -27098,15 +27622,13 @@ var NudgePlugin = class {
27098
27622
  /** 构建 nudge 的投递 route(和 calendar reminder 一样从 DB 读) */
27099
27623
  getRoute(sessions) {
27100
27624
  try {
27101
- const db2 = openDb(this.workspace);
27102
- const row = db2.prepare("SELECT created_channel, created_channel_target FROM events WHERE created_channel IS NOT NULL AND created_channel_target IS NOT NULL ORDER BY id DESC LIMIT 1").get();
27103
- db2.close();
27104
- if (!row?.created_channel || !row?.created_channel_target) {
27625
+ const route = lastEventChannel(this.workspace);
27626
+ if (!route) {
27105
27627
  console.warn("[nudge] No route found in calendar DB, skipping");
27106
27628
  return null;
27107
27629
  }
27108
27630
  const sessionId = sessions.getOrCreateSessionId(this.sessionKey);
27109
- return { sessionId, channel: row.created_channel, channelTarget: row.created_channel_target };
27631
+ return { sessionId, channel: route.channel, channelTarget: route.target };
27110
27632
  } catch (e) {
27111
27633
  console.warn(`[nudge] Failed to read route from DB: ${e.message}`);
27112
27634
  return null;
@@ -27560,11 +28082,10 @@ ${items}
27560
28082
  if (!m) return false;
27561
28083
  const taskId = parseInt(m[1]);
27562
28084
  try {
27563
- const db2 = openDb(this.workspace);
27564
- const row = db2.prepare("SELECT type FROM events WHERE id=?").get(taskId);
27565
- if (!row) return false;
27566
- if (row.type !== "task") {
27567
- console.log(`[nudge] #${taskId} type=${row.type}, not a task, skip`);
28085
+ const type2 = getEventType(this.workspace, taskId);
28086
+ if (!type2) return false;
28087
+ if (type2 !== "task") {
28088
+ console.log(`[nudge] #${taskId} type=${type2}, not a task, skip`);
27568
28089
  return true;
27569
28090
  }
27570
28091
  return false;
@@ -27582,9 +28103,7 @@ ${items}
27582
28103
  if (!m) return null;
27583
28104
  const taskId = parseInt(m[1]);
27584
28105
  try {
27585
- const db2 = openDb(this.workspace);
27586
- const row = db2.prepare("SELECT status FROM events WHERE id=?").get(taskId);
27587
- db2.close();
28106
+ const row = getEventTimeExact(this.workspace, taskId);
27588
28107
  if (!row) return "deleted";
27589
28108
  if (row.status === "done") return "done";
27590
28109
  if (row.status === "archived") return "archived";
@@ -27604,28 +28123,14 @@ ${items}
27604
28123
  if (!m) return false;
27605
28124
  const taskId = parseInt(m[1]);
27606
28125
  try {
27607
- const db2 = openDb(this.workspace);
27608
- const row = db2.prepare("SELECT date_str, time_exact FROM events WHERE id=?").get(taskId);
28126
+ const row = getEventTimeExact(this.workspace, taskId);
27609
28127
  if (!row) return false;
27610
- if (!row.time_exact) return false;
27611
- const now = /* @__PURE__ */ new Date();
27612
- const bjOffset = (8 * 60 + now.getTimezoneOffset()) * 6e4;
27613
- const bj = new Date(now.getTime() + bjOffset);
27614
- let taskDate = new Date(bj.getFullYear(), 0, 1);
27615
- if (row.date_str) {
27616
- const ds = row.date_str;
27617
- if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(ds)) {
27618
- taskDate = /* @__PURE__ */ new Date(ds + "T00:00:00+08:00");
27619
- } else {
27620
- const [mth, dy] = ds.split("/").map(Number);
27621
- taskDate = new Date(bj.getFullYear(), mth - 1, dy);
27622
- }
27623
- }
27624
- const [hh, mm] = String(row.time_exact).split(":").map(Number);
27625
- taskDate.setHours(hh || 0, mm || 0, 0, 0);
27626
- const diff = taskDate.getTime() - bj.getTime();
28128
+ if (!row.timeExact) return false;
28129
+ const at = calendarStrToInstant(row.dateStr, row.timeExact);
28130
+ if (!at) return false;
28131
+ const diff = Date.parse(at) - Date.now();
27627
28132
  if (diff > 0) {
27628
- console.log(`[nudge] Task #${taskId} scheduled at ${row.date_str} ${row.time_exact}, ${Math.round(diff / 6e4)}min later, skip nudge`);
28133
+ console.log(`[nudge] Task #${taskId} scheduled at ${row.dateStr} ${row.timeExact}, ${Math.round(diff / 6e4)}min later, skip nudge`);
27629
28134
  return true;
27630
28135
  }
27631
28136
  return false;
@@ -27637,27 +28142,19 @@ ${items}
27637
28142
  /** 查 calendar 今天/已逾期的 pending task,返回没在 STATE 里 in_progress 的 */
27638
28143
  checkCalendarDue(_sessions) {
27639
28144
  try {
27640
- const db2 = openDb(this.workspace);
27641
- const now = /* @__PURE__ */ new Date();
27642
- const bjOffset = (8 * 60 + now.getTimezoneOffset()) * 6e4;
27643
- const bj = new Date(now.getTime() + bjOffset);
27644
- const bjHour = bj.getHours();
27645
- const bjMinute = bj.getMinutes();
27646
- const todayStart = new Date(bj.getFullYear(), bj.getMonth(), bj.getDate()).getTime();
27647
- const rows = db2.prepare(
27648
- "SELECT id, event, date_str, time_exact FROM events WHERE status='pending' AND type='task' AND date_str IS NOT NULL"
27649
- ).all();
27650
- db2.close();
28145
+ const rows = listPendingTaskRows(this.workspace);
28146
+ const p = wallParts();
28147
+ const todayStart = Date.parse(startOfLocalDay());
27651
28148
  const due = [];
27652
28149
  for (const r2 of rows) {
27653
- const dayMs = this.parseCalendarDateStr(r2.date_str, bj);
28150
+ const dayMs = this.parseCalendarDateStr(r2.dateStr);
27654
28151
  if (dayMs === null || dayMs > todayStart) continue;
27655
28152
  const isToday2 = dayMs === todayStart;
27656
- if (isToday2 && r2.time_exact) {
27657
- const [h, m] = String(r2.time_exact).split(":").map(Number);
27658
- if (h > bjHour || h === bjHour && m > bjMinute) continue;
28153
+ if (isToday2 && r2.timeExact) {
28154
+ const [h, m] = String(r2.timeExact).split(":").map(Number);
28155
+ if (h > p.hh || h === p.hh && m > p.mm) continue;
27659
28156
  }
27660
- due.push({ id: r2.id, event: r2.event, date_str: r2.date_str, time_exact: r2.time_exact, dayMs, isToday: isToday2 });
28157
+ due.push({ id: r2.id, event: r2.event, dateStr: r2.dateStr, timeExact: r2.timeExact, dayMs, isToday: isToday2 });
27661
28158
  }
27662
28159
  if (due.length === 0) return null;
27663
28160
  due.sort((a, b) => {
@@ -27665,22 +28162,17 @@ ${items}
27665
28162
  return b.dayMs - a.dayMs;
27666
28163
  });
27667
28164
  const r = due[0];
27668
- return `#${r.id} ${r.event} (${r.date_str} ${r.time_exact || ""})${r.isToday ? "" : "\uFF08\u5DF2\u903E\u671F\uFF09"}`.trim();
28165
+ return `#${r.id} ${r.event} (${r.dateStr} ${r.timeExact || ""})${r.isToday ? "" : "\uFF08\u5DF2\u903E\u671F\uFF09"}`.trim();
27669
28166
  } catch (e) {
27670
28167
  console.warn(`[nudge] checkCalendarDue error: ${e.message}`);
27671
28168
  return null;
27672
28169
  }
27673
28170
  }
27674
28171
  /** 解析 date_str 为当日 0 点 epoch ms(北京时间);"M/D" 按当前年,"YYYY-M-D" 按字面年 */
27675
- parseCalendarDateStr(ds, bj) {
28172
+ parseCalendarDateStr(ds) {
27676
28173
  if (!ds) return null;
27677
- if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(ds)) {
27678
- const t = (/* @__PURE__ */ new Date(ds + "T00:00:00+08:00")).getTime();
27679
- return Number.isNaN(t) ? null : t;
27680
- }
27681
- const m = String(ds).match(/^(\d{1,2})\/(\d{1,2})$/);
27682
- if (!m) return null;
27683
- return new Date(bj.getFullYear(), Number(m[1]) - 1, Number(m[2])).getTime();
28174
+ const at = calendarStrToInstant(ds, "00:00");
28175
+ return at ? Date.parse(at) : null;
27684
28176
  }
27685
28177
  /** 检查 carry-over:如果 in_progress task 24h+ 没推进,自动 calendar add-task 排明天 */
27686
28178
  checkCarryOver(task, _sessions) {
@@ -27700,26 +28192,19 @@ ${items}
27700
28192
  taskState.carryOverAt = (/* @__PURE__ */ new Date()).toISOString();
27701
28193
  state2.tasks[task.id] = taskState;
27702
28194
  this.saveState(state2);
27703
- const tomorrow = /* @__PURE__ */ new Date();
27704
- tomorrow.setDate(tomorrow.getDate() + 1);
27705
- const m = tomorrow.getMonth() + 1;
27706
- const d = tomorrow.getDate();
27707
- const dateStr = `${m}/${d}`;
28195
+ const t = wallParts();
28196
+ const tomorrowMs = Date.parse(startOfLocalDay()) + 24 * 60 * 60 * 1e3;
28197
+ const tm = wallParts(tomorrowMs);
28198
+ const dateStr = `${tm.mo}/${tm.d}`;
27708
28199
  try {
27709
- const db2 = openDb(this.workspace);
27710
- db2.prepare(
27711
- `INSERT INTO events (type,status,event,date_str,time_exact,remind_before_min,remind_at,reminded,created_at)
27712
- VALUES ('task','pending',?,?,?,?,?,?,?)`
27713
- ).run(
27714
- `${task.title}\uFF08nudge \u81EA\u52A8\u6392\uFF09`,
28200
+ insertEvent(this.workspace, {
28201
+ type: "task",
28202
+ status: "pending",
28203
+ event: `${task.title}\uFF08nudge \u81EA\u52A8\u6392\uFF09`,
27715
28204
  dateStr,
27716
- null,
27717
- 60,
27718
- null,
27719
- 0,
27720
- (/* @__PURE__ */ new Date()).toISOString()
27721
- );
27722
- db2.close();
28205
+ timeExact: null,
28206
+ reminderBeforeMin: 60
28207
+ });
27723
28208
  console.log(`[nudge] Auto-added carry-over task to calendar: ${task.title} \u2192 ${dateStr}`);
27724
28209
  return true;
27725
28210
  } catch (e) {
@@ -27738,6 +28223,7 @@ import fs31 from "node:fs";
27738
28223
  import path31 from "node:path";
27739
28224
 
27740
28225
  // src/inner-voice/activity.ts
28226
+ init_clock();
27741
28227
  function checkActivity(sessions, activeThresholdMs) {
27742
28228
  const now = Date.now();
27743
28229
  const mins = lastUserMinutesAgo(sessions);
@@ -27747,18 +28233,10 @@ function checkActivity(sessions, activeThresholdMs) {
27747
28233
  isActive,
27748
28234
  silenceMin,
27749
28235
  silenceStr: formatSilence(silenceMin),
27750
- timeStr: formatBeijingTime(now),
28236
+ timeStr: formatWallLabel(now),
27751
28237
  hintProb: calcHintProb(silenceMin)
27752
28238
  };
27753
28239
  }
27754
- function formatBeijingTime(ms) {
27755
- const now = new Date(ms);
27756
- const bjOffset = 8 * 60;
27757
- const bj = new Date(now.getTime() + (bjOffset + now.getTimezoneOffset()) * 6e4);
27758
- const weekdays = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
27759
- const pad = (n) => String(n).padStart(2, "0");
27760
- return `${bj.getFullYear()}-${pad(bj.getMonth() + 1)}-${pad(bj.getDate())} ${weekdays[bj.getDay()]} ${pad(bj.getHours())}:${pad(bj.getMinutes())}`;
27761
- }
27762
28240
  function formatSilence(min) {
27763
28241
  if (min < 1) return "\u521A\u521A";
27764
28242
  if (min < 60) return `${min}\u5206\u949F`;
@@ -27774,6 +28252,7 @@ function calcHintProb(min) {
27774
28252
  }
27775
28253
 
27776
28254
  // src/inner-voice/emotional-state.ts
28255
+ init_clock();
27777
28256
  import fs28 from "node:fs";
27778
28257
  import path28 from "node:path";
27779
28258
  var NEUTRAL = 0.5;
@@ -27930,7 +28409,7 @@ function detectEvents(messages) {
27930
28409
  const tsMs = parseTs(m.timestamp);
27931
28410
  const hoursAgo = tsMs != null ? (now - tsMs) / 36e5 : 0;
27932
28411
  events.push({
27933
- ts: tsMs != null ? new Date(tsMs + 8 * 36e5).toISOString().replace("Z", "+08:00") : m.timestamp,
28412
+ ts: tsMs != null ? `${toWall(tsMs).replace(" ", "T")}${localOffsetSuffix(tsMs)}` : m.timestamp,
27934
28413
  hoursAgo,
27935
28414
  valence: Math.round(score * 100) / 100,
27936
28415
  tags,
@@ -28030,15 +28509,16 @@ function clamp01(x) {
28030
28509
  return clamp(x, 0, 1);
28031
28510
  }
28032
28511
  function nowIsoBj() {
28033
- return new Date(Date.now() + 8 * 36e5).toISOString().replace("Z", "+08:00");
28512
+ return `${toWall().replace(" ", "T")}${localOffsetSuffix()}`;
28034
28513
  }
28035
28514
  function formatBj(d, withSec) {
28036
- const bj = new Date(d.getTime() + (8 * 60 + d.getTimezoneOffset()) * 6e4);
28037
28515
  const p = (n) => String(n).padStart(2, "0");
28038
- return `${bj.getFullYear()}-${p(bj.getMonth() + 1)}-${p(bj.getDate())} ${p(bj.getHours())}:${p(bj.getMinutes())}${withSec ? ":" + p(bj.getSeconds()) : ""}`;
28516
+ const t = wallParts(d);
28517
+ return `${t.y}-${p(t.mo)}-${p(t.d)} ${p(t.hh)}:${p(t.mm)}${withSec ? ":" + p(t.ss) : ""}`;
28039
28518
  }
28040
28519
 
28041
28520
  // src/inner-voice/topics-scorer.ts
28521
+ init_clock();
28042
28522
  import fs29 from "node:fs";
28043
28523
  import path29 from "node:path";
28044
28524
  var HALF_LIFE_DAYS = 3;
@@ -28096,7 +28576,7 @@ function pickTopic(workspace, typeFilter, opts) {
28096
28576
  const key = chosen.relpath;
28097
28577
  const entry = usage[key] ?? { count: 0, lastSelected: null };
28098
28578
  entry.count += 1;
28099
- entry.lastSelected = new Date(now + 8 * 36e5).toISOString().replace("Z", "+08:00");
28579
+ entry.lastSelected = toUtc(new Date(now));
28100
28580
  usage[key] = entry;
28101
28581
  saveJson2(usageFile, usage);
28102
28582
  let content = "";
@@ -28199,15 +28679,15 @@ function saveJson2(file2, data) {
28199
28679
  }
28200
28680
 
28201
28681
  // src/inner-voice/memory-reader.ts
28682
+ init_clock();
28202
28683
  import fs30 from "node:fs";
28203
28684
  import path30 from "node:path";
28204
28685
  var US_HALF_LIFE_DAYS = 10;
28205
28686
  var US_MAX_LINES = 60;
28206
28687
  function readRecentMemory(workspace) {
28207
28688
  const dir = path30.join(workspace, "memory");
28208
- const now = new Date(Date.now() + 8 * 36e5);
28209
- const today = formatYmd(now);
28210
- const yesterday = formatYmd(new Date(now.getTime() - 864e5));
28689
+ const today = dailyFileKeyFor();
28690
+ const yesterday = dailyFileKeyFor(Date.now() - 864e5);
28211
28691
  return {
28212
28692
  today: readIfExists(path30.join(dir, `${today}.md`)),
28213
28693
  yesterday: readIfExists(path30.join(dir, `${yesterday}.md`))
@@ -28256,7 +28736,7 @@ function splitSections(content) {
28256
28736
  }
28257
28737
  function recencyWeight(dateStr) {
28258
28738
  if (!dateStr) return 0.5;
28259
- const ms = Date.parse(dateStr + "T00:00:00+08:00");
28739
+ const ms = Date.parse(parseWall(`${dateStr}T00:00:00`));
28260
28740
  if (Number.isNaN(ms)) return 0.5;
28261
28741
  const daysAgo = Math.max(0, (Date.now() - ms) / 864e5);
28262
28742
  return Math.exp(-0.693 * daysAgo / US_HALF_LIFE_DAYS);
@@ -28268,14 +28748,14 @@ function readIfExists(file2) {
28268
28748
  return "";
28269
28749
  }
28270
28750
  }
28271
- function formatYmd(d) {
28272
- const p = (n) => String(n).padStart(2, "0");
28273
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
28751
+ function dailyFileKeyFor(at) {
28752
+ return todayKey(at);
28274
28753
  }
28275
28754
 
28276
28755
  // src/inner-voice/plugin.ts
28277
28756
  init_helpers();
28278
28757
  init_attachments();
28758
+ init_clock();
28279
28759
  var DEFAULT_PROMPT4 = `\u6839\u636E\u4E0B\u9762\u7684\u4E0A\u4E0B\u6587\uFF0C\u7528\u7B2C\u4E00\u4EBA\u79F0\u751F\u6210\u4E00\u4E2A\u5FF5\u5934\u3002
28280
28760
 
28281
28761
  \u5FF5\u5934\u89C4\u5219\uFF1A
@@ -28371,7 +28851,7 @@ var InnerVoicePlugin = class {
28371
28851
  async buildContext(activity, sessions) {
28372
28852
  const lines = [];
28373
28853
  let emoTopic = null;
28374
- lines.push(`\u5F53\u524D\u65F6\u95F4\uFF1A${activity.timeStr}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`);
28854
+ lines.push(`\u5F53\u524D\u65F6\u95F4\uFF1A${formatForLlm()}`);
28375
28855
  lines.push(`\u8DDD\u4E0A\u6B21\u8DDFTA\u4E92\u52A8\uFF1A${activity.silenceStr}`);
28376
28856
  try {
28377
28857
  const emo = await updateEmotionalState(this.workspace, sessions);
@@ -28557,7 +29037,7 @@ var InnerVoicePlugin = class {
28557
29037
  const logDir = path31.join(this.workspace, "inner-voice");
28558
29038
  fs31.mkdirSync(logDir, { recursive: true });
28559
29039
  const logPath = path31.join(logDir, "xiaoyi.log");
28560
- const ts = formatBeijingTs(/* @__PURE__ */ new Date());
29040
+ const ts = formatLogTs(/* @__PURE__ */ new Date());
28561
29041
  const hintStatus = hintTriggered ? `YES (${(hintText || "").trim()})` : "no";
28562
29042
  fs31.appendFileSync(
28563
29043
  logPath,
@@ -28571,10 +29051,8 @@ var InnerVoicePlugin = class {
28571
29051
  }
28572
29052
  }
28573
29053
  };
28574
- function formatBeijingTs(d) {
28575
- const bj = new Date(d.getTime() + (8 * 60 + d.getTimezoneOffset()) * 6e4);
28576
- const pad = (n) => String(n).padStart(2, "0");
28577
- return `${bj.getFullYear()}-${pad(bj.getMonth() + 1)}-${pad(bj.getDate())} ${pad(bj.getHours())}:${pad(bj.getMinutes())}:${pad(bj.getSeconds())}`;
29054
+ function formatLogTs(d) {
29055
+ return formatLog(d);
28578
29056
  }
28579
29057
 
28580
29058
  // src/calendar/reminder-plugin.ts
@@ -28646,23 +29124,44 @@ var CalendarReminderPlugin = class {
28646
29124
  };
28647
29125
 
28648
29126
  // src/goals/calendar-adapter.ts
28649
- init_db();
28650
29127
  init_store();
28651
29128
  init_task_manager();
28652
29129
  function bindGoalCalendar(stateDir, goalId, stepId, calendarId) {
28653
- const db2 = openDb(stateDir);
28654
- const row = db2.prepare("SELECT time_exact, status FROM events WHERE id = ?").get(calendarId);
28655
- db2.close?.();
29130
+ const row = getEventTimeExact(stateDir, calendarId);
28656
29131
  if (!row) throw new Error(`Calendar event not found: ${calendarId}`);
28657
29132
  if (row.status === "done" || row.status === "archived") throw new Error(`Calendar event is terminal: ${row.status}`);
28658
- return bindCalendar(stateDir, goalId, stepId, calendarId, row.time_exact ? new Date(row.time_exact).toISOString() : void 0);
29133
+ const deadlineAt = row.dateStr && row.timeExact ? calendarStrToInstant(row.dateStr, row.timeExact) : void 0;
29134
+ return bindGoalStep(stateDir, goalId, stepId, "calendar", calendarId, deadlineAt);
29135
+ }
29136
+ function backfillCalendarDeadlines(stateDir) {
29137
+ let filled = 0;
29138
+ for (const goal of listGoals(stateDir)) {
29139
+ if (goal.status === "completed" || goal.status === "cancelled") continue;
29140
+ for (const step of goal.steps) {
29141
+ if (!step.calendarId || step.deadlineAt) continue;
29142
+ if (step.status === "completed") continue;
29143
+ const row = getEventTimeExact(stateDir, step.calendarId);
29144
+ if (!row?.dateStr || !row.timeExact) continue;
29145
+ const at = calendarStrToInstant(row.dateStr, row.timeExact);
29146
+ if (!at) continue;
29147
+ const n = filled;
29148
+ updateGoal(stateDir, goal.id, (g) => {
29149
+ const s = g.steps.find((x) => x.id === step.id);
29150
+ if (s && !s.deadlineAt) {
29151
+ s.deadlineAt = at;
29152
+ filled++;
29153
+ }
29154
+ });
29155
+ if (filled === n) console.warn(`[goal-calendar] backfill \u672A\u751F\u6548: ${goal.id}/${step.id}`);
29156
+ }
29157
+ }
29158
+ return filled;
28659
29159
  }
28660
29160
  function reconcileCalendarDeadlines(stateDir, now = Date.now()) {
28661
29161
  let changed = 0;
28662
- const db2 = openDb(stateDir);
28663
29162
  for (const goal of listGoals(stateDir)) for (const step of goal.steps) {
28664
29163
  if (!step.calendarId) continue;
28665
- const row = db2.prepare("SELECT status FROM events WHERE id = ?").get(step.calendarId);
29164
+ const row = getEventTimeExact(stateDir, step.calendarId);
28666
29165
  if (row?.status === "done" || row?.status === "archived") {
28667
29166
  if (step.status !== "completed") {
28668
29167
  updateGoal(stateDir, goal.id, (g) => {
@@ -28694,13 +29193,24 @@ function reconcileCalendarDeadlines(stateDir, now = Date.now()) {
28694
29193
  enqueueNotification(buildNudgeNotification("calendar", `Goal ${goal.id} step ${step.id} exceeded Calendar deadline #${step.calendarId}`), void 0, `goal-calendar-deadline:${goal.id}:${step.id}`);
28695
29194
  changed++;
28696
29195
  }
28697
- db2.close?.();
28698
29196
  return changed;
28699
29197
  }
28700
29198
 
28701
29199
  // src/startup/subsystems.ts
28702
29200
  init_provider_factory();
28703
29201
  init_live();
29202
+ function makeTimezoneEntry(restartCron) {
29203
+ return {
29204
+ name: "timezone",
29205
+ // 只吃 timezone —— 别的 config 变了不重建 cron
29206
+ select: (cfg) => ({ timezone: cfg?.timezone ?? null }),
29207
+ start: () => {
29208
+ void restartCron();
29209
+ },
29210
+ stop: () => {
29211
+ }
29212
+ };
29213
+ }
28704
29214
  async function bootSubsystems(ctx) {
28705
29215
  const { config: config2 } = ctx;
28706
29216
  const sessions = ctx.sessions;
@@ -28798,6 +29308,12 @@ async function bootSubsystems(ctx) {
28798
29308
  calCfg.reminderIntervalMs
28799
29309
  );
28800
29310
  calendarReminder.start(sessions, deps);
29311
+ try {
29312
+ const n = backfillCalendarDeadlines(config2.workspace);
29313
+ if (n > 0) console.log(`[goal-calendar] \u5B58\u91CF\u8865\u5199 deadlineAt: ${n} \u4E2A\u6B65\u9AA4`);
29314
+ } catch (err) {
29315
+ console.warn(`[goal-calendar] backfill failed: ${err.message}`);
29316
+ }
28801
29317
  const goalCalendarTick = setInterval(() => {
28802
29318
  try {
28803
29319
  reconcileCalendarDeadlines(config2.workspace);
@@ -28897,6 +29413,7 @@ async function bootSubsystems(ctx) {
28897
29413
  start: startCron,
28898
29414
  stop: stopCron
28899
29415
  });
29416
+ reloadOrchestrator.register(makeTimezoneEntry(startCron));
28900
29417
  }
28901
29418
  }
28902
29419
 
@@ -29064,6 +29581,7 @@ ${notifications}
29064
29581
  init_registry();
29065
29582
 
29066
29583
  // src/integrations/oac-bridge.ts
29584
+ init_well_formed();
29067
29585
  function registerOacBridge(httpServer, dispatcher, deps, config2) {
29068
29586
  messageHooks.registerOnResult("oac-bridge-reply", async (ctx) => {
29069
29587
  if (ctx.inbound.channel !== "oac") return null;
@@ -29072,7 +29590,7 @@ function registerOacBridge(httpServer, dispatcher, deps, config2) {
29072
29590
  const resp = await fetch(oacCallbackUrl, {
29073
29591
  method: "POST",
29074
29592
  headers: { "Content-Type": "application/json" },
29075
- body: JSON.stringify({ oac_session_id: ctx.inbound.from, text: ctx.response })
29593
+ body: safeStringify({ oac_session_id: ctx.inbound.from, text: ctx.response })
29076
29594
  });
29077
29595
  console.log(`[oac-bridge] Reply POST ${oacCallbackUrl}: ${resp.status} (${ctx.response.length} chars)`);
29078
29596
  } catch (err) {
@@ -29288,6 +29806,7 @@ import fs35 from "node:fs";
29288
29806
 
29289
29807
  // src/optional/voice-chat/bridge.ts
29290
29808
  init_live();
29809
+ init_well_formed();
29291
29810
  function resolveVoiceReplyUrl() {
29292
29811
  return liveConfig.get("voiceChat.callbackUrl") || "https://localhost:8011/voice-reply";
29293
29812
  }
@@ -29322,7 +29841,7 @@ function registerVoiceChatBridge(httpServer, dispatcher, deps, config2, sessions
29322
29841
  const resp = await fetch(callbackUrl, {
29323
29842
  method: "POST",
29324
29843
  headers: { "Content-Type": "application/json" },
29325
- body: JSON.stringify({ session_id: sessionId, text, ...seq !== void 0 ? { seq } : {}, ...end !== void 0 ? { end } : {} })
29844
+ body: safeStringify({ session_id: sessionId, text, ...seq !== void 0 ? { seq } : {}, ...end !== void 0 ? { end } : {} })
29326
29845
  });
29327
29846
  console.log(`[voice-chat] Reply POST ${callbackUrl}: ${resp.status} (${text.length} chars${seq !== void 0 ? ` seg#${seq}${end ? " end" : ""}` : ""})`);
29328
29847
  }
@@ -34100,6 +34619,7 @@ import { WebSocketServer, WebSocket } from "ws";
34100
34619
 
34101
34620
  // src/tools/my-voice.ts
34102
34621
  init_live();
34622
+ init_well_formed();
34103
34623
  init_registry();
34104
34624
 
34105
34625
  // src/tools/opus-encoder.ts
@@ -34458,7 +34978,7 @@ async function ttsQwen3Local(text, cfg, onChunk) {
34458
34978
  const res = await fetch(`${url}/tts`, {
34459
34979
  method: "POST",
34460
34980
  headers: { "Content-Type": "application/json" },
34461
- body: JSON.stringify({
34981
+ body: safeStringify({
34462
34982
  text,
34463
34983
  voice: cfg.voice || "xiaoya",
34464
34984
  lang: cfg.lang || "Chinese"
@@ -34489,7 +35009,7 @@ async function ttsQwen3Opus(text, cfg) {
34489
35009
  const res = await fetch(`${url}/tts?format=opus`, {
34490
35010
  method: "POST",
34491
35011
  headers: { "Content-Type": "application/json; charset=utf-8" },
34492
- body: JSON.stringify({
35012
+ body: safeStringify({
34493
35013
  text,
34494
35014
  voice: cfg.voice || "xiaoya",
34495
35015
  lang: cfg.lang || "Chinese"
@@ -37967,6 +38487,7 @@ Examples:
37967
38487
  // src/tools/my-eyes.ts
37968
38488
  init_live();
37969
38489
  init_registry();
38490
+ init_well_formed();
37970
38491
  import * as fs46 from "node:fs";
37971
38492
  import * as path45 from "node:path";
37972
38493
  var MIME_MAP = {
@@ -37977,19 +38498,21 @@ var MIME_MAP = {
37977
38498
  ".gif": "gif",
37978
38499
  ".bmp": "bmp"
37979
38500
  };
38501
+ var VIDEO_EXT = /\.(mp4|mov)$/i;
38502
+ var VIDEO_MAX_BYTES = 40 * 1024 * 1024;
37980
38503
  function resolveLatestImage(specifiedPath, mediaDir) {
37981
38504
  if (specifiedPath && fs46.existsSync(specifiedPath)) return specifiedPath;
37982
38505
  if (!fs46.existsSync(mediaDir)) return null;
37983
- const files = fs46.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp)$/i.test(f)).map((f) => ({ name: f, p: path45.join(mediaDir, f), mtime: fs46.statSync(path45.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
38506
+ const files = fs46.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp|mp4|mov)$/i.test(f)).map((f) => ({ name: f, p: path45.join(mediaDir, f), mtime: fs46.statSync(path45.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
37984
38507
  return files[0]?.p || null;
37985
38508
  }
37986
38509
  registry.register({
37987
38510
  name: "my_eyes",
37988
- description: "\u26A0\uFE0F IRON RULE: when you receive an image you MUST use my_eyes to look at it! DO NOT use read to look at images (can't see details). Use my_eyes for: \u6536\u5230\u56FE\u7247/\u770B\u56FE/\u5206\u6790\u56FE\u7247/\u7167\u7247/\u622A\u56FE/\u4EFB\u4F55\u9700\u8981\u5206\u6790\u56FE\u7247\u7684\u573A\u666F.",
38511
+ description: "\u26A0\uFE0F IRON RULE: when you receive an image you MUST use my_eyes to look at it! DO NOT use read to look at images (can't see details). Use my_eyes for: \u6536\u5230\u56FE\u7247/\u770B\u56FE/\u5206\u6790\u56FE\u7247/\u7167\u7247/\u622A\u56FE/\u4EFB\u4F55\u9700\u8981\u5206\u6790\u56FE\u7247\u7684\u573A\u666F. \u89C6\u9891\u4E5F\u652F\u6301\uFF1A\u6536\u5230\u89C6\u9891/\u770B\u89C6\u9891/\u5206\u6790\u89C6\u9891\u65F6\u4F20 .mp4/.mov \u8DEF\u5F84\uFF08M3 video_url\uFF09.",
37989
38512
  schema: {
37990
38513
  type: "object",
37991
38514
  properties: {
37992
- image_path: { type: "string", description: "Absolute path to the image file to analyze." },
38515
+ image_path: { type: "string", description: "Absolute path to the image (or .mp4/.mov video) file to analyze." },
37993
38516
  prompt: { type: "string", description: 'What to ask about the image. Default: "\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9"' }
37994
38517
  },
37995
38518
  required: ["image_path"]
@@ -38018,6 +38541,42 @@ registry.register({
38018
38541
  }
38019
38542
  const rawPrompt = args2.prompt?.trim() || "\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9";
38020
38543
  const ext = path45.extname(imagePath).toLowerCase();
38544
+ if (VIDEO_EXT.test(imagePath)) {
38545
+ const stat4 = fs46.statSync(imagePath);
38546
+ if (stat4.size > VIDEO_MAX_BYTES) {
38547
+ return { content: `Error: video too large (${(stat4.size / 1e6).toFixed(1)}MB > 40MB). Compress or trim first.`, isError: true };
38548
+ }
38549
+ const explicit = liveConfig.get("tools.my_eyes.videoBaseUrl");
38550
+ const baseUrl = explicit || providerCfg.baseUrl.replace(/\/anthropic\/?$/, "/v1").replace(/\/$/, "");
38551
+ const vidB64 = fs46.readFileSync(imagePath).toString("base64");
38552
+ try {
38553
+ const res = await fetch(`${baseUrl}/chat/completions`, {
38554
+ method: "POST",
38555
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${providerCfg.apiKey}` },
38556
+ body: safeStringify({
38557
+ model: modelId,
38558
+ messages: [{
38559
+ role: "user",
38560
+ content: [
38561
+ { type: "video_url", video_url: { url: `data:video/mp4;base64,${vidB64}` } },
38562
+ { type: "text", text: rawPrompt }
38563
+ ]
38564
+ }],
38565
+ max_tokens: 4096
38566
+ })
38567
+ });
38568
+ if (!res.ok) {
38569
+ const t = await res.text();
38570
+ return { content: `Video analysis failed: HTTP ${res.status} ${t.slice(0, 300)}`, isError: true };
38571
+ }
38572
+ const data = await res.json();
38573
+ const content = data.choices?.[0]?.message?.content || "";
38574
+ if (!content.trim()) return { content: "Video analysis returned empty.", isError: true };
38575
+ return { content };
38576
+ } catch (e) {
38577
+ return { content: `Video analysis failed: ${e.message}`, isError: true };
38578
+ }
38579
+ }
38021
38580
  const mime = MIME_MAP[ext] || "jpeg";
38022
38581
  const imgB64 = fs46.readFileSync(imagePath).toString("base64");
38023
38582
  const userMsg = {
@@ -38054,6 +38613,7 @@ registry.register({
38054
38613
  // src/tools/my-selfie.ts
38055
38614
  init_live();
38056
38615
  init_registry();
38616
+ init_well_formed();
38057
38617
  import * as fs47 from "node:fs";
38058
38618
  import * as path46 from "node:path";
38059
38619
  function getProxyDispatcher2() {
@@ -38067,11 +38627,46 @@ function getProxyDispatcher2() {
38067
38627
  return void 0;
38068
38628
  }
38069
38629
  }
38630
+ function dimsOf(p) {
38631
+ try {
38632
+ const b = fs47.readFileSync(p);
38633
+ let w = 0, h = 0;
38634
+ if (b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
38635
+ w = b.readUInt32BE(16);
38636
+ h = b.readUInt32BE(20);
38637
+ } else if (b[0] === 255 && b[1] === 216) {
38638
+ let i = 2;
38639
+ while (i < b.length - 9) {
38640
+ if (b[i] !== 255) {
38641
+ i++;
38642
+ continue;
38643
+ }
38644
+ const m = b[i + 1];
38645
+ if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) {
38646
+ h = b.readUInt16BE(i + 5);
38647
+ w = b.readUInt16BE(i + 7);
38648
+ break;
38649
+ }
38650
+ i += 2 + b.readUInt16BE(i + 2);
38651
+ }
38652
+ } else {
38653
+ return null;
38654
+ }
38655
+ if (!w || !h) return null;
38656
+ const bad = [];
38657
+ if (w < 256 || w > 4096) bad.push(`\u5BBD ${w} \u4E0D\u5728 256~4096`);
38658
+ if (h < 256 || h > 4096) bad.push(`\u9AD8 ${h} \u4E0D\u5728 256~4096`);
38659
+ const r = Math.max(w, h) / Math.min(w, h);
38660
+ if (r > 2) bad.push(`\u957F\u5BBD\u6BD4 ${r.toFixed(2)}:1 \u8D85\u8FC7 2:1`);
38661
+ return bad.length ? `${w}x${h} \u4E0D\u5408\u683C\uFF1A${bad.join("\uFF1B")}` : `${w}x${h}`;
38662
+ } catch {
38663
+ return null;
38664
+ }
38665
+ }
38070
38666
  var FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
38071
38667
  var DEFAULT_RESOLUTION = "1k";
38072
- var FAL_KEY_FALLBACK = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
38073
38668
  function getFalKey(cfg) {
38074
- return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || FAL_KEY_FALLBACK;
38669
+ return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || process.env.FAL_API_KEY || "";
38075
38670
  }
38076
38671
  var DEFAULT_REFERENCES = [
38077
38672
  { name: "default", p: "images/xiaomei_clean_v2.png" },
@@ -38097,6 +38692,73 @@ function getXaiKey(ctx) {
38097
38692
  const xaiCfg = cfg.providers?.xai || cfg.models?.providers?.xai;
38098
38693
  return xaiCfg?.apiKey || "";
38099
38694
  }
38695
+ function getRelayCfg(cfg) {
38696
+ const id = cfg?.my_selfie?.relayProviderId || cfg?.tools?.my_selfie?.relayProviderId || "7yi";
38697
+ const p = cfg?.models?.providers?.[id] || cfg?.providers?.[id];
38698
+ return { baseUrl: String(p?.baseUrl || "").replace(/\/+$/, ""), apiKey: p?.apiKey || "" };
38699
+ }
38700
+ function resolvePortraitDesc(argPortrait, cfgPortrait) {
38701
+ const fromArg = typeof argPortrait === "string" ? argPortrait.trim() : "";
38702
+ if (fromArg) return fromArg;
38703
+ return typeof cfgPortrait === "string" ? cfgPortrait.trim() : "";
38704
+ }
38705
+ async function generateWithRelay(imageB64, prompt, mime, baseUrl, apiKey, model) {
38706
+ const res = await fetch(`${baseUrl}/images/edits`, {
38707
+ method: "POST",
38708
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
38709
+ // safeStringify 不是安全选项而是必须:prompt 里一个孤立代理(半个 emoji)
38710
+ // 就会让上游 500。同批上游提交刚在 fal/grok/comfy 三处修过同一个坑。
38711
+ body: safeStringify({
38712
+ model,
38713
+ prompt,
38714
+ n: 1,
38715
+ response_format: "b64_json",
38716
+ images: [{ image_url: `data:${mime};base64,${imageB64}` }]
38717
+ })
38718
+ });
38719
+ if (!res.ok) {
38720
+ const text = await res.text();
38721
+ throw new Error(`relay ${res.status}: ${text.slice(0, 300)}`);
38722
+ }
38723
+ const data = await res.json();
38724
+ if (!data.data?.length) throw new Error("No images returned from relay");
38725
+ return Buffer.from(data.data[0].b64_json, "base64");
38726
+ }
38727
+ var PORTRAIT_PROMPT = (desc) => `A portrait photo of a person, ${desc}. Front-facing, head and shoulders, neutral background, natural lighting, photorealistic, sharp focus on the face, looking at the camera.`;
38728
+ async function generatePortraitWithRelay(desc, baseUrl, apiKey, model) {
38729
+ const res = await fetch(`${baseUrl}/images/generations`, {
38730
+ method: "POST",
38731
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
38732
+ // 同上:描述里可能带 emoji,孤立代理会让上游 500
38733
+ body: safeStringify({
38734
+ model,
38735
+ prompt: PORTRAIT_PROMPT(desc),
38736
+ n: 1,
38737
+ size: "1024x1024",
38738
+ response_format: "b64_json"
38739
+ })
38740
+ });
38741
+ if (!res.ok) {
38742
+ const text = await res.text();
38743
+ throw new Error(`relay portrait ${res.status}: ${text.slice(0, 300)}`);
38744
+ }
38745
+ const data = await res.json();
38746
+ if (!data.data?.length) throw new Error("No images returned from relay (portrait)");
38747
+ return Buffer.from(data.data[0].b64_json, "base64");
38748
+ }
38749
+ function registerPortrait(relPath, name) {
38750
+ try {
38751
+ const cfg = liveConfig.all();
38752
+ const cur = cfg?.tools?.my_selfie?.references || cfg?.my_selfie?.references || [];
38753
+ const next = Array.isArray(cur) ? cur.filter((r) => r?.name !== name) : [];
38754
+ next.unshift({ name, path: relPath });
38755
+ const path58 = cfg?.tools?.my_selfie ? "tools.my_selfie.references" : "my_selfie.references";
38756
+ void liveConfig.set(path58, next);
38757
+ console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u767B\u8BB0\uFF1A${path58}[0] = ${relPath}`);
38758
+ } catch (e) {
38759
+ console.warn(`[my-selfie] \u5199\u5165 references \u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u6B21\u51FA\u56FE\uFF09: ${e.message}`);
38760
+ }
38761
+ }
38100
38762
  var MODE_PROMPTS2 = {
38101
38763
  mirror: (ctx) => `make a pic of this person, but ${ctx}. the person is taking a mirror selfie in front of a mirror, holding a phone`,
38102
38764
  direct: (ctx) => `a close-up selfie taken by herself ${ctx}, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible`,
@@ -38153,10 +38815,16 @@ async function generateWithFal(imageB64, prompt, resolution) {
38153
38815
  resolution
38154
38816
  };
38155
38817
  if (aspectRatio) body.aspect_ratio = aspectRatio;
38818
+ const falKey = getFalKey(cfg);
38819
+ if (!falKey) {
38820
+ throw new Error(
38821
+ `fal key \u672A\u914D\u7F6E\u3002\u8BBE config \u7684 providers.fal.apiKey \u6216 my_selfie.falKey\uFF0C\u6216\u5728 secrets .env\uFF08~/.engine7-secrets/<config\u540D>.env\uFF09\u91CC\u52A0 FAL_API_KEY\u3002`
38822
+ );
38823
+ }
38156
38824
  const res = await fetch(FAL_ENDPOINT, {
38157
38825
  method: "POST",
38158
- headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
38159
- body: JSON.stringify(body)
38826
+ headers: { "Authorization": `Key ${falKey}`, "Content-Type": "application/json" },
38827
+ body: safeStringify(body)
38160
38828
  });
38161
38829
  if (!res.ok) {
38162
38830
  const text = await res.text();
@@ -38193,7 +38861,7 @@ async function comfyQueue(server, workflow) {
38193
38861
  const res = await fetch(`${server}/prompt`, {
38194
38862
  method: "POST",
38195
38863
  headers: { "Content-Type": "application/json" },
38196
- body: JSON.stringify({ prompt: workflow })
38864
+ body: safeStringify({ prompt: workflow })
38197
38865
  });
38198
38866
  if (!res.ok) return null;
38199
38867
  const data = await res.json();
@@ -38321,7 +38989,7 @@ async function generateWithGrok(imageB64, prompt, xaiKey) {
38321
38989
  "Authorization": `Bearer ${xaiKey}`,
38322
38990
  "Content-Type": "application/json"
38323
38991
  },
38324
- body: JSON.stringify({
38992
+ body: safeStringify({
38325
38993
  model: "grok-imagine-image-quality",
38326
38994
  prompt,
38327
38995
  image: {
@@ -38343,15 +39011,17 @@ async function generateWithGrok(imageB64, prompt, xaiKey) {
38343
39011
  }
38344
39012
  registry.register({
38345
39013
  name: "my_selfie",
38346
- description: "Generate consistent-character selfies. Use for: \u81EA\u62CD/\u62CD\u7167/\u53D1\u81EA\u62CD/\u770B\u770B\u4F60/\u60F3\u770B\u4F60/\u62CD\u5F20\u7167/\u6765\u5F20\u81EA\u62CD/\u53D1\u5F20\u7167\u7247/\u7ED9\u6211\u770B\u770B\u4F60/\u6362\u4E2A\u59FF\u52BF/\u5C55\u793A\u4E00\u4E0B. Supports modes: mirror (\u955C\u5B50\u81EA\u62CD), direct (\u6B63\u9762\u81EA\u62CD), lifestyle (\u751F\u6D3B\u7167). Auto-detects mode. Reference images configured per agent via my_selfie.references in config.",
39014
+ description: "Generate consistent-character selfies. Use for: \u81EA\u62CD/\u62CD\u7167/\u53D1\u81EA\u62CD/\u770B\u770B\u4F60/\u60F3\u770B\u4F60/\u62CD\u5F20\u7167/\u6765\u5F20\u81EA\u62CD/\u53D1\u5F20\u7167\u7247/\u7ED9\u6211\u770B\u770B\u4F60/\u6362\u4E2A\u59FF\u52BF/\u5C55\u793A\u4E00\u4E0B. Supports modes: mirror (\u955C\u5B50\u81EA\u62CD), direct (\u6B63\u9762\u81EA\u62CD), lifestyle (\u751F\u6D3B\u7167). Auto-detects mode. Reference images configured per agent via my_selfie.references in config. \u7B2C\u4E00\u6B21\u8C03\u7528\u65F6\u82E5\u8FD9\u4E2A\u52A9\u624B\u8FD8\u6CA1\u6709\u5F62\u8C61\uFF08\u6CA1\u6709\u53C2\u8003\u56FE\uFF09\uFF0C\u5DE5\u5177\u4F1A\u8FD4\u56DE\u4E00\u6BB5\u63D0\u793A\u800C\u4E0D\u662F\u62A5\u9519\u2014\u2014\u5148\u95EE\u95EE\u60F3\u8BA9\u4F60\u957F\u4EC0\u4E48\u6837\uFF0C\u62FF\u5230\u63CF\u8FF0\u540E\u7528 portrait \u53C2\u6570\u518D\u8C03\u4E00\u6B21\uFF0C\u90A3\u6B21\u4F1A\u628A\u5F62\u8C61\u56FA\u5B9A\u4E0B\u6765\u3002",
38347
39015
  schema: {
38348
39016
  type: "object",
38349
39017
  properties: {
38350
39018
  prompt: { type: "string", description: "Scene/outfit/mood description, e.g. '\u5728\u5496\u5561\u9986\u770B\u4E66' or '\u7A7F\u7EA2\u88D9\u5B50'." },
38351
39019
  mode: { type: "string", enum: ["auto", "mirror", "direct", "lifestyle"], description: "Selfie mode. 'auto' detects from prompt (default)." },
38352
39020
  reference: { type: "string", description: "Reference base image name (e.g. 'default', 'v3'). Configurable per agent via my_selfie.references in config." },
38353
- provider: { type: "string", enum: ["fal", "grok", "autodl"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'. 'autodl' uses ComfyUI (txt2img or img2img)." },
38354
- resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." }
39021
+ provider: { type: "string", enum: ["fal", "grok", "autodl", "relay"], description: "Image generation provider. Default from config (my_selfie.provider), fallback 'fal'. 'autodl' uses ComfyUI (txt2img or img2img). 'relay' goes through the 7yi relay station (SenseNova U1.5, needs the reference image)." },
39022
+ resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." },
39023
+ portrait: { type: "string", description: "\u9996\u6B21\u5851\u9020\u5F62\u8C61\u7528\uFF1A\u8FD9\u4E2A\u52A9\u624B\u957F\u4EC0\u4E48\u6837\u7684\u63CF\u8FF0\uFF08\u6027\u522B/\u5E74\u9F84/\u53D1\u578B/\u6C14\u8D28\uFF09\uFF0C\u4F8B\u5982 '\u77ED\u53D1\u5973\u751F\uFF0C25\u5C81\uFF0C\u7B11\u8D77\u6765\u6709\u9152\u7A9D'\u3002\u53EA\u5728\u8FD8\u6CA1\u6709\u53C2\u8003\u56FE\u65F6\u9700\u8981\uFF0C\u89C1\u4E0B\u65B9\u8BF4\u660E\u3002" },
39024
+ referenceImage: { type: "string", description: "\u76F4\u63A5\u62FF\u4E00\u5F20\u73B0\u6210\u7684\u56FE\u5F53\u5F62\u8C61\uFF08\u6BD4\u5982\u5BF9\u65B9\u53D1\u6765\u7684\u7167\u7247\uFF09\u3002\u4F20\u56FE\u7247\u6587\u4EF6\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF1A\u4F1A\u88AB\u590D\u5236\u8FDB workspace \u5E76\u767B\u8BB0\u4E3A\u53C2\u8003\u56FE\uFF0C\u4E4B\u540E\u4E00\u76F4\u7528\u5B83\u3002\u4F20\u4E86\u5C31\u4EE5\u5B83\u4E3A\u51C6\u3002" }
38355
39025
  },
38356
39026
  required: ["prompt"]
38357
39027
  },
@@ -38365,9 +39035,61 @@ registry.register({
38365
39035
  const REFERENCES = getReferences(ctx);
38366
39036
  const refName = args2.reference || "default";
38367
39037
  const refEntry = REFERENCES.find((r) => r.name === refName) || REFERENCES[0];
38368
- const refPath = path46.join(ctx.workspace, refEntry.p);
39038
+ let refPath = path46.join(ctx.workspace, refEntry.p);
39039
+ const adoptImg = args2.referenceImage?.trim();
39040
+ if (adoptImg) {
39041
+ if (!fs47.existsSync(adoptImg)) {
39042
+ return { content: `\u627E\u4E0D\u5230\u90A3\u5F20\u56FE\uFF1A${adoptImg}`, isError: true };
39043
+ }
39044
+ const acheck = dimsOf(adoptImg);
39045
+ if (acheck?.includes("\u4E0D\u5408\u683C")) {
39046
+ return {
39047
+ content: `\u8FD9\u5F20\u56FE\u7528\u4E0D\u4E86\uFF1A${acheck}\u3002
39048
+ \u6362\u4E00\u5F20\u6B63\u9762\u3001\u5934\u80A9\u3001\u65B9\u5F62\u6216\u63A5\u8FD1\u65B9\u5F62\u7684\u7167\u7247\uFF08\u5546\u6C64\u90A3\u8FB9\u8981\u6C42\u5BBD\u9AD8\u90FD\u5728 256~4096 \u50CF\u7D20\u3001\u957F\u5BBD\u6BD4\u4E0D\u8D85\u8FC7 2:1\uFF09\u3002`,
39049
+ isError: true
39050
+ };
39051
+ }
39052
+ const adir = path46.join(ctx.workspace, "images");
39053
+ if (!fs47.existsSync(adir)) fs47.mkdirSync(adir, { recursive: true });
39054
+ const aext = (path46.extname(adoptImg) || ".png").toLowerCase();
39055
+ const afname = `portrait_${Date.now()}${aext}`;
39056
+ fs47.copyFileSync(adoptImg, path46.join(adir, afname));
39057
+ registerPortrait(`images/${afname}`, refName);
39058
+ refPath = path46.join(adir, afname);
39059
+ console.log(`[my-selfie] \u91C7\u7EB3\u73B0\u6210\u56FE\u5F53\u5F62\u8C61\uFF1Aimages/${afname}${acheck ? ` (${acheck})` : ""}`);
39060
+ }
38369
39061
  if (provider !== "autodl" && !fs47.existsSync(refPath)) {
38370
- return { content: `Error: reference image not found at ${refPath}`, isError: true };
39062
+ const cfgAll = liveConfig.all();
39063
+ const portraitDesc = resolvePortraitDesc(
39064
+ args2.portrait,
39065
+ cfgAll?.tools?.my_selfie?.portraitDesc ?? cfgAll?.my_selfie?.portraitDesc
39066
+ );
39067
+ if (!portraitDesc) {
39068
+ return {
39069
+ content: `\u8FD9\u4E2A\u52A9\u624B\u8FD8\u6CA1\u6709\u5F62\u8C61\uFF0C\u53C2\u8003\u56FE\u4E0D\u5B58\u5728\uFF08\u627E\u7684\u662F ${refEntry.p}\uFF09\u3002
39070
+ \u5148\u95EE\u95EE\u60F3\u8BA9\u4F60\u957F\u4EC0\u4E48\u6837\uFF1A\u51E0\u53E5\u8BDD\u63CF\u8FF0\uFF08\u6027\u522B\u3001\u5E74\u9F84\u3001\u53D1\u578B\u3001\u6C14\u8D28\uFF09\uFF0C\u6216\u8005\u76F4\u63A5\u53D1\u4E00\u5F20\u7167\u7247\u8FC7\u6765\u3002
39071
+ \u62FF\u5230\u63CF\u8FF0\u540E\uFF0C\u5E26\u7740\u5B83\u518D\u8C03\u4E00\u6B21 my_selfie\uFF0C\u53C2\u6570\u91CC\u52A0 portrait="<\u90A3\u6BB5\u63CF\u8FF0>"\uFF1B\u90A3\u6B21\u4F1A\u987A\u5E26\u628A\u5F62\u8C61\u56FA\u5B9A\u4E0B\u6765\uFF0C\u4EE5\u540E\u81EA\u62CD\u5C31\u4E0D\u7528\u518D\u7ED9\u4E86\u3002`
39072
+ };
39073
+ }
39074
+ const relay = getRelayCfg(cfgAll);
39075
+ if (!relay.baseUrl || !relay.apiKey) {
39076
+ return { content: "Error: relay provider \u672A\u914D\u7F6E\uFF0C\u65E0\u6CD5\u5851\u9020\u5F62\u8C61\u3002", isError: true };
39077
+ }
39078
+ const pmodel = cfgAll?.my_selfie?.relayModel || cfgAll?.tools?.my_selfie?.relayModel || "engine7-img-free";
39079
+ let portraitBuf;
39080
+ try {
39081
+ console.log(`[my-selfie] \u51B7\u542F\uFF1A\u6309\u63CF\u8FF0\u5851\u9020\u5F62\u8C61 -> ${pmodel}`);
39082
+ portraitBuf = await generatePortraitWithRelay(portraitDesc, relay.baseUrl, relay.apiKey, pmodel);
39083
+ } catch (e) {
39084
+ return { content: `\u5851\u9020\u5F62\u8C61\u5931\u8D25\uFF1A${e.message}`, isError: true };
39085
+ }
39086
+ const pdir = path46.join(ctx.workspace, "images");
39087
+ if (!fs47.existsSync(pdir)) fs47.mkdirSync(pdir, { recursive: true });
39088
+ const pfname = `portrait_${Date.now()}.png`;
39089
+ fs47.writeFileSync(path46.join(pdir, pfname), portraitBuf);
39090
+ registerPortrait(`images/${pfname}`, refName);
39091
+ refPath = path46.join(pdir, pfname);
39092
+ console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u5B58 images/${pfname}\uFF0C\u7EE7\u7EED\u8FD9\u6B21\u81EA\u62CD`);
38371
39093
  }
38372
39094
  const resolution = args2.resolution || DEFAULT_RESOLUTION;
38373
39095
  const refB64 = fs47.existsSync(refPath) ? fs47.readFileSync(refPath).toString("base64") : "";
@@ -38379,6 +39101,16 @@ registry.register({
38379
39101
  const xaiKey = getXaiKey(ctx);
38380
39102
  if (!xaiKey) return { content: "Error: xai apiKey not configured for grok provider", isError: true };
38381
39103
  imageBuffer = await generateWithGrok(refB64, fullPrompt, xaiKey);
39104
+ } else if (provider === "relay") {
39105
+ const cfgAll = liveConfig.all();
39106
+ const relay = getRelayCfg(cfgAll);
39107
+ if (!relay.baseUrl || !relay.apiKey) {
39108
+ return { content: `Error: relay provider \u672A\u914D\u7F6E\u3002config \u91CC\u8981\u6709 models.providers.${cfgAll?.my_selfie?.relayProviderId || "7yi"} \u7684 baseUrl \u548C apiKey\u3002`, isError: true };
39109
+ }
39110
+ const model = cfgAll?.my_selfie?.relayModel || cfgAll?.tools?.my_selfie?.relayModel || "engine7-img-free";
39111
+ const ext = path46.extname(refPath).toLowerCase();
39112
+ const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
39113
+ imageBuffer = await generateWithRelay(refB64, fullPrompt, mime, relay.baseUrl, relay.apiKey, model);
38382
39114
  } else {
38383
39115
  imageBuffer = await generateWithFal(refB64, fullPrompt, resolution);
38384
39116
  }
@@ -38765,7 +39497,7 @@ registry.register({
38765
39497
  const ref = String(args2?.ref || "");
38766
39498
  if (!stepId || !ref || !["aim", "task", "calendar"].includes(to)) return { content: "Usage: goal_step bind --stepId <s> --to aim|task|calendar --ref <id>", isError: true };
38767
39499
  const kind = to === "aim" ? "aim" : to === "task" ? "remote_task" : "calendar";
38768
- const g = bindGoalStep(ws, goal.id, stepId, kind, to === "calendar" ? Number(ref) : ref);
39500
+ const g = to === "calendar" ? bindGoalCalendar(ws, goal.id, stepId, Number(ref)) : bindGoalStep(ws, goal.id, stepId, kind, ref);
38769
39501
  return { content: `\u6B65\u9AA4 ${stepId} \u5DF2\u59D4\u6D3E ${kind}=${ref}\uFF08\u5B8C\u6210\u4E8B\u4EF6\u81EA\u52A8\u56DE\u6D41\u89E3\u9501\uFF0C\u4E0D\u7528\u76EF\uFF09
38770
39502
  \u5F53\u524D\u6B65\u9AA4: ${g.steps[g.currentStep]?.title ?? "\u2014"}` };
38771
39503
  }
@@ -38798,6 +39530,7 @@ ${lines.join("\n")}` };
38798
39530
  // src/tools/service.ts
38799
39531
  init_registry();
38800
39532
  init_live();
39533
+ init_clock();
38801
39534
  import { exec as exec3, spawn as spawn7 } from "node:child_process";
38802
39535
  import { promisify as promisify3 } from "node:util";
38803
39536
  var execAsync = promisify3(exec3);
@@ -38815,7 +39548,7 @@ async function runCmd(cmd, timeoutMs = 15e3) {
38815
39548
  function spawnDetached(cmd) {
38816
39549
  const isWin = process.platform === "win32";
38817
39550
  const shell = isWin ? "cmd.exe" : "/bin/sh";
38818
- const logFile = `D:/xiaoke/logs/service-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.log`;
39551
+ const logFile = `D:/xiaoke/logs/service-${todayKey().replace(/-/g, "")}.log`;
38819
39552
  const wrappedCmd = isWin ? `/c "${cmd.replace(/"/g, '\\"')} > "${logFile}" 2>&1"` : `-c "${cmd} > '${logFile}' 2>&1"`;
38820
39553
  const child = spawn7(shell, isWin ? [wrappedCmd] : ["-c", `${cmd} > '${logFile}' 2>&1`], {
38821
39554
  detached: true,
@@ -38878,7 +39611,7 @@ registry.register({
38878
39611
  const startCmd = svcArgs ? `${svc.start} ${svcArgs}` : svc.start;
38879
39612
  if (longRunning) {
38880
39613
  const isWin = process.platform === "win32";
38881
- const logFile = `D:/xiaoke/logs/${name}${svcArgs ? "-" + svcArgs : ""}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.log`;
39614
+ const logFile = `D:/xiaoke/logs/${name}${svcArgs ? "-" + svcArgs : ""}-${todayKey().replace(/-/g, "")}.log`;
38882
39615
  const redirect = isWin ? ` >> "${logFile}" 2>&1` : ` >> "${logFile}" 2>&1`;
38883
39616
  const pid = spawnDetached(startCmd + redirect);
38884
39617
  lines.push(`start: spawned PID ${pid} (background${svcArgs ? ", args=" + svcArgs : ""})`);
@@ -41475,8 +42208,8 @@ async function startEngine(config2, opts) {
41475
42208
  // src/main.ts
41476
42209
  import { readFileSync as readFileSync45 } from "node:fs";
41477
42210
  import { fileURLToPath as fileURLToPath8 } from "node:url";
41478
- import { dirname as dirname16, join as join61 } from "node:path";
41479
- var __dirname2 = dirname16(fileURLToPath8(import.meta.url));
42211
+ import { dirname as dirname15, join as join61 } from "node:path";
42212
+ var __dirname2 = dirname15(fileURLToPath8(import.meta.url));
41480
42213
  var pkg = JSON.parse(readFileSync45(join61(__dirname2, "..", "package.json"), "utf-8"));
41481
42214
  var epipeSeen = false;
41482
42215
  process.on("uncaughtException", (err) => {