engine7 7.1.84 → 7.1.95

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/cli.mjs CHANGED
@@ -1,12 +1,4 @@
1
1
  const require=(await import('node:module')).createRequire(import.meta.url)
2
- const __origEmitWarning = process.emitWarning.bind(process);
3
- process.emitWarning = (w, ...r) => {
4
- const name = typeof w === 'object' ? w?.name : r?.[0];
5
- const msg = typeof w === 'object' ? String(w?.message) : String(w);
6
- if (name === 'ExperimentalWarning' && msg.includes('SQLite')) return;
7
- return __origEmitWarning(w, ...r);
8
- };
9
-
10
2
  var __defProp = Object.defineProperty;
11
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -356,83 +348,6 @@ var init_cli_events = __esm({
356
348
  }
357
349
  });
358
350
 
359
- // src/config/secrets-env.ts
360
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
361
- import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
362
- function loadSecretsEnv(stateDir, configName) {
363
- const home = process.env.HOME || process.env.USERPROFILE || ".";
364
- const candidates = [
365
- join3(home, ".engine7-secrets", `${configName}.env`),
366
- join3(stateDir, "configs", `.env.${configName}`),
367
- join3(stateDir, "configs", ".env")
368
- ];
369
- for (const p of candidates) {
370
- if (!existsSync3(p)) continue;
371
- for (const line of readFileSync3(p, "utf-8").split("\n")) {
372
- const t = line.trim();
373
- if (!t || t.startsWith("#")) continue;
374
- const eq = t.indexOf("=");
375
- if (eq <= 0) continue;
376
- const k = t.slice(0, eq).trim();
377
- const v = t.slice(eq + 1).trim();
378
- const cur = process.env[k];
379
- if (k && (cur === void 0 || cur === "")) process.env[k] = v;
380
- }
381
- return p;
382
- }
383
- return null;
384
- }
385
- var init_secrets_env = __esm({
386
- "src/config/secrets-env.ts"() {
387
- "use strict";
388
- }
389
- });
390
-
391
- // src/config/display.ts
392
- function loadDisplayConfig(raw) {
393
- if (!raw) return { ...DEFAULT_DISPLAY };
394
- return {
395
- thinking: {
396
- ...DEFAULT_DISPLAY.thinking,
397
- ...raw.thinking || {}
398
- },
399
- toolUse: {
400
- ...DEFAULT_DISPLAY.toolUse,
401
- ...raw.toolUse || {}
402
- },
403
- toolResult: {
404
- ...DEFAULT_DISPLAY.toolResult,
405
- ...raw.toolResult || {}
406
- },
407
- reactions: {
408
- ...DEFAULT_DISPLAY.reactions,
409
- ...raw.reactions || {}
410
- },
411
- preview: {
412
- ...DEFAULT_DISPLAY.preview,
413
- ...raw.preview || {}
414
- },
415
- typing: {
416
- ...DEFAULT_DISPLAY.typing,
417
- ...raw.typing || {}
418
- }
419
- };
420
- }
421
- var DEFAULT_DISPLAY;
422
- var init_display = __esm({
423
- "src/config/display.ts"() {
424
- "use strict";
425
- DEFAULT_DISPLAY = {
426
- thinking: { enabled: true, emoji: "\u{1F4AD}", maxLen: 300 },
427
- toolUse: { enabled: true, emoji: "\u{1F527}", displayMode: "raw", bashDisplayMode: "description", displayLimits: { write: 200 } },
428
- toolResult: { enabled: true, emoji: "\u{1F4E4}" },
429
- reactions: { enabled: true, start: "\u{1F440}", done: "\u2705", error: "\u274C" },
430
- preview: { enabled: true, agentName: "Agent" },
431
- typing: { enabled: true }
432
- };
433
- }
434
- });
435
-
436
351
  // src/config/live.ts
437
352
  var live_exports = {};
438
353
  __export(live_exports, {
@@ -530,6 +445,287 @@ var init_live = __esm({
530
445
  }
531
446
  });
532
447
 
448
+ // src/time/clock.ts
449
+ function detectHostTimezone() {
450
+ try {
451
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
452
+ } catch {
453
+ return "UTC";
454
+ }
455
+ }
456
+ function isValidTimezone(name) {
457
+ if (!name) return false;
458
+ try {
459
+ new Intl.DateTimeFormat("en-US", { timeZone: name });
460
+ return true;
461
+ } catch {
462
+ return false;
463
+ }
464
+ }
465
+ function tz() {
466
+ if (testTimezone) return testTimezone;
467
+ const v = liveConfig.get("timezone");
468
+ const name = typeof v === "string" && v ? v : "UTC";
469
+ if (validatedZones.has(name)) return name;
470
+ if (isValidTimezone(name)) {
471
+ validatedZones.add(name);
472
+ return name;
473
+ }
474
+ warnOnce("tz", "invalid-zone", name);
475
+ return "UTC";
476
+ }
477
+ function partsFmt(zone) {
478
+ let f = fmtCache.get(zone);
479
+ if (!f) {
480
+ f = new Intl.DateTimeFormat("en-US", {
481
+ timeZone: zone,
482
+ year: "numeric",
483
+ month: "2-digit",
484
+ day: "2-digit",
485
+ hour: "2-digit",
486
+ minute: "2-digit",
487
+ second: "2-digit",
488
+ hourCycle: "h23",
489
+ weekday: "short"
490
+ });
491
+ fmtCache.set(zone, f);
492
+ }
493
+ return f;
494
+ }
495
+ function asDate(i) {
496
+ return i instanceof Date ? i : new Date(i);
497
+ }
498
+ function wallParts(i = /* @__PURE__ */ new Date()) {
499
+ const d = asDate(i);
500
+ const parts = partsFmt(tz()).formatToParts(d);
501
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
502
+ return {
503
+ y: Number(get("year")),
504
+ mo: Number(get("month")),
505
+ d: Number(get("day")),
506
+ hh: Number(get("hour")),
507
+ mm: Number(get("minute")),
508
+ ss: Number(get("second")),
509
+ weekday: EN_WEEKDAY[get("weekday")] ?? 0
510
+ };
511
+ }
512
+ function weekdayZh(i) {
513
+ return WEEKDAY_ZH[wallParts(i).weekday];
514
+ }
515
+ function nowUtc() {
516
+ return (/* @__PURE__ */ new Date()).toISOString();
517
+ }
518
+ function toUtc(d) {
519
+ return d.toISOString();
520
+ }
521
+ function toWall(i = /* @__PURE__ */ new Date()) {
522
+ const p = wallParts(i);
523
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)} ${p2(p.hh)}:${p2(p.mm)}:${p2(p.ss)}`;
524
+ }
525
+ function formatForLlm(i) {
526
+ const d = i ?? /* @__PURE__ */ new Date();
527
+ const p = wallParts(d);
528
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)} ${WEEKDAY_ZH[p.weekday]} ${p2(p.hh)}:${p2(p.mm)}\uFF08${tz()}\uFF09`;
529
+ }
530
+ function formatLog(i) {
531
+ return toWall(i ?? /* @__PURE__ */ new Date());
532
+ }
533
+ function warnOnce(callSite, shape, sample) {
534
+ const key = `${callSite}:${shape}`;
535
+ if (warned.has(key)) return;
536
+ warned.add(key);
537
+ console.warn(`[clock] ${callSite} \u5951\u7EA6\u8FDD\u89C4(${shape})\uFF0C\u6837\u672C: ${String(sample).slice(0, 80)}`);
538
+ }
539
+ function zoneOffsetMs(d) {
540
+ const p = wallParts(d);
541
+ const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mm, p.ss);
542
+ return asUtc - Math.floor(d.getTime() / 1e3) * 1e3;
543
+ }
544
+ function wallToUtc(y, mo, d, hh, mm, ss) {
545
+ const guess = Date.UTC(y, mo - 1, d, hh, mm, ss);
546
+ const off1 = zoneOffsetMs(new Date(guess));
547
+ const ms1 = guess - off1;
548
+ const off2 = zoneOffsetMs(new Date(ms1));
549
+ const ms = off2 === off1 ? ms1 : guess - off2;
550
+ return new Date(ms).toISOString();
551
+ }
552
+ function parseWall(s) {
553
+ const raw = String(s ?? "").trim();
554
+ if (!raw) return "";
555
+ if (/Z$/i.test(raw) || /[+-]\d{2}:?\d{2}$/.test(raw)) {
556
+ const ms2 = Date.parse(raw);
557
+ if (Number.isNaN(ms2)) {
558
+ warnOnce("parseWall", "unparseable", raw);
559
+ return raw;
560
+ }
561
+ warnOnce("parseWall", "has-offset", raw);
562
+ return new Date(ms2).toISOString();
563
+ }
564
+ const m = WALL_RE.exec(raw);
565
+ if (!m) {
566
+ warnOnce("parseWall", "unparseable", raw);
567
+ return raw;
568
+ }
569
+ const tail = raw.slice(m[0].length);
570
+ if (tail && !/^\.\d+$/.test(tail)) {
571
+ warnOnce("parseWall", "unparseable-tail", raw);
572
+ return raw;
573
+ }
574
+ const [Y, Mo, D, H, Mi, S] = [+m[1], +m[2], +m[3], +m[4], +m[5], m[6] ? +m[6] : 0];
575
+ if (Y < 1e3 || Y > 9999 || Mo < 1 || Mo > 12 || D < 1 || D > daysInMonth(Y, Mo) || H > 23 || Mi > 59 || S > 59) {
576
+ warnOnce("parseWall", "out-of-range", raw);
577
+ return raw;
578
+ }
579
+ const ms = tail ? Math.round(Number(`0${tail}`) * 1e3) : 0;
580
+ return withMs(wallToUtc(Y, Mo, D, H, Mi, S), ms);
581
+ }
582
+ function daysInMonth(y, mo) {
583
+ return new Date(Date.UTC(y, mo, 0)).getUTCDate();
584
+ }
585
+ function withMs(base, ms) {
586
+ if (!ms) return base;
587
+ const t = Date.parse(base);
588
+ return Number.isNaN(t) ? base : new Date(t + ms).toISOString();
589
+ }
590
+ function localOffsetSuffix(at) {
591
+ const total = Math.round(zoneOffsetMs(at ? asDate(at) : /* @__PURE__ */ new Date()) / 6e4);
592
+ const sign = total >= 0 ? "+" : "-";
593
+ const abs = Math.abs(total);
594
+ return `${sign}${p2(Math.floor(abs / 60))}:${p2(abs % 60)}`;
595
+ }
596
+ function startOfLocalDay(i) {
597
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
598
+ return wallToUtc(p.y, p.mo, p.d, 0, 0, 0);
599
+ }
600
+ function todayKey(i) {
601
+ const p = wallParts(i ?? /* @__PURE__ */ new Date());
602
+ return `${p.y}-${p2(p.mo)}-${p2(p.d)}`;
603
+ }
604
+ var WEEKDAY_ZH, EN_WEEKDAY, testTimezone, validatedZones, fmtCache, p2, warned, WALL_RE;
605
+ var init_clock = __esm({
606
+ "src/time/clock.ts"() {
607
+ "use strict";
608
+ init_live();
609
+ WEEKDAY_ZH = ["\u5468\u65E5", "\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D"];
610
+ EN_WEEKDAY = {
611
+ Sun: 0,
612
+ Mon: 1,
613
+ Tue: 2,
614
+ Wed: 3,
615
+ Thu: 4,
616
+ Fri: 5,
617
+ Sat: 6
618
+ };
619
+ testTimezone = null;
620
+ validatedZones = /* @__PURE__ */ new Set();
621
+ fmtCache = /* @__PURE__ */ new Map();
622
+ p2 = (n) => String(n).padStart(2, "0");
623
+ warned = /* @__PURE__ */ new Set();
624
+ WALL_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?/;
625
+ }
626
+ });
627
+
628
+ // src/config/secrets-env.ts
629
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
630
+ import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
631
+ function secretsHome() {
632
+ return process.env.HOME || process.env.USERPROFILE || ".";
633
+ }
634
+ function loadSecretsEnv(stateDir, configName) {
635
+ const home = secretsHome();
636
+ const candidates = [
637
+ join3(home, ".engine7-secrets", `${configName}.env`),
638
+ join3(stateDir, "configs", `.env.${configName}`),
639
+ join3(stateDir, "configs", ".env")
640
+ ];
641
+ for (const p of candidates) {
642
+ if (!existsSync3(p)) continue;
643
+ for (const line of readFileSync3(p, "utf-8").split("\n")) {
644
+ const t = line.trim();
645
+ if (!t || t.startsWith("#")) continue;
646
+ const eq = t.indexOf("=");
647
+ if (eq <= 0) continue;
648
+ const k = t.slice(0, eq).trim();
649
+ const v = t.slice(eq + 1).trim();
650
+ const cur = process.env[k];
651
+ if (k && (cur === void 0 || cur === "")) process.env[k] = v;
652
+ }
653
+ return p;
654
+ }
655
+ return null;
656
+ }
657
+ function writeSecretsValue(configPath, envKey, value) {
658
+ const configName = basename2(configPath, ".json");
659
+ const dir = join3(secretsHome(), ".engine7-secrets");
660
+ mkdirSync2(dir, { recursive: true });
661
+ const file2 = join3(dir, `${configName}.env`);
662
+ const lines = [];
663
+ if (existsSync3(file2)) {
664
+ for (const line of readFileSync3(file2, "utf8").split(/\r?\n/)) {
665
+ if (new RegExp(`^\\s*${envKey}\\s*=`).test(line)) continue;
666
+ lines.push(line);
667
+ }
668
+ }
669
+ while (lines.length && lines[lines.length - 1] === "") lines.pop();
670
+ lines.push(`${envKey}=${value}`);
671
+ writeFileSync2(file2, lines.join("\n") + "\n", { mode: 384 });
672
+ try {
673
+ chmodSync(file2, 384);
674
+ } catch {
675
+ }
676
+ return file2;
677
+ }
678
+ var init_secrets_env = __esm({
679
+ "src/config/secrets-env.ts"() {
680
+ "use strict";
681
+ }
682
+ });
683
+
684
+ // src/config/display.ts
685
+ function loadDisplayConfig(raw) {
686
+ if (!raw) return { ...DEFAULT_DISPLAY };
687
+ return {
688
+ thinking: {
689
+ ...DEFAULT_DISPLAY.thinking,
690
+ ...raw.thinking || {}
691
+ },
692
+ toolUse: {
693
+ ...DEFAULT_DISPLAY.toolUse,
694
+ ...raw.toolUse || {}
695
+ },
696
+ toolResult: {
697
+ ...DEFAULT_DISPLAY.toolResult,
698
+ ...raw.toolResult || {}
699
+ },
700
+ reactions: {
701
+ ...DEFAULT_DISPLAY.reactions,
702
+ ...raw.reactions || {}
703
+ },
704
+ preview: {
705
+ ...DEFAULT_DISPLAY.preview,
706
+ ...raw.preview || {}
707
+ },
708
+ typing: {
709
+ ...DEFAULT_DISPLAY.typing,
710
+ ...raw.typing || {}
711
+ }
712
+ };
713
+ }
714
+ var DEFAULT_DISPLAY;
715
+ var init_display = __esm({
716
+ "src/config/display.ts"() {
717
+ "use strict";
718
+ DEFAULT_DISPLAY = {
719
+ thinking: { enabled: true, emoji: "\u{1F4AD}", maxLen: 300 },
720
+ toolUse: { enabled: true, emoji: "\u{1F527}", displayMode: "raw", bashDisplayMode: "description", displayLimits: { write: 200 } },
721
+ toolResult: { enabled: true, emoji: "\u{1F4E4}" },
722
+ reactions: { enabled: true, start: "\u{1F440}", done: "\u2705", error: "\u274C" },
723
+ preview: { enabled: true, agentName: "Agent" },
724
+ typing: { enabled: true }
725
+ };
726
+ }
727
+ });
728
+
533
729
  // src/config/features.ts
534
730
  function getFeature(key) {
535
731
  const v = liveConfig.get(`agents.defaults.features.${key}`);
@@ -570,6 +766,7 @@ __export(loader_exports, {
570
766
  configSummary: () => configSummary,
571
767
  loadConfig: () => loadConfig,
572
768
  parseModelRef: () => parseModelRef,
769
+ resolveTimezone: () => resolveTimezone,
573
770
  takeLoadWarnings: () => takeLoadWarnings
574
771
  });
575
772
  import * as fs3 from "node:fs";
@@ -591,6 +788,24 @@ function warnAtLoad(msg2) {
591
788
  if (!loadWarningsFlushed) loadWarnings.push(msg2);
592
789
  console.warn(msg2);
593
790
  }
791
+ function resolveTimezone(raw) {
792
+ const fallback = () => {
793
+ try {
794
+ return detectHostTimezone();
795
+ } catch {
796
+ return "UTC";
797
+ }
798
+ };
799
+ const v = raw?.timezone;
800
+ if (typeof v !== "string" || !v) return fallback();
801
+ try {
802
+ new Intl.DateTimeFormat("en-US", { timeZone: v });
803
+ return v;
804
+ } catch {
805
+ warnAtLoad(`[config] timezone "${v}" \u4E0D\u662F\u5408\u6CD5 IANA \u65F6\u533A\u540D\uFF0C\u9000\u56DE\u672C\u673A\u63A2\u6D4B`);
806
+ return fallback();
807
+ }
808
+ }
594
809
  function loadConfig(configPath) {
595
810
  const resolvedPath = path3.resolve(
596
811
  configPath || process.env.ENGINE_CONFIG || "engine-config.json"
@@ -602,7 +817,7 @@ function loadConfig(configPath) {
602
817
  const missingEnvKeys = /* @__PURE__ */ new Set();
603
818
  const resolveEnv = (node) => {
604
819
  if (typeof node === "string") {
605
- const m = node.match(/^(?:env:|{{\s*)([A-Z][A-Z0-9_]*)(?:\s*}})?$/);
820
+ const m = node.match(/^(?:env:|{{\s*)(?=[A-Z0-9_]*[A-Z])([A-Z0-9_]+)(?:\s*}})?$/);
606
821
  if (m) {
607
822
  if (process.env[m[1]] !== void 0) return process.env[m[1]];
608
823
  if (node.startsWith("env:")) missingEnvKeys.add(m[1]);
@@ -727,11 +942,18 @@ function loadConfig(configPath) {
727
942
  ...rawInnerVoice,
728
943
  enabled: true
729
944
  } : void 0;
945
+ const resolvedTimezone = resolveTimezone(raw);
730
946
  const rawCron = raw.cron || {};
731
947
  const cron = rawCron.enabled ? {
732
948
  ...rawCron,
733
949
  enabled: true,
734
- defaultTimezone: rawCron.defaultTimezone || "Asia/Shanghai",
950
+ // ★ 这里原来写死 'Asia/Shanghai'。而 cron/tasks.ts 建任务时是
951
+ // `params.timezone || params.defaultTimezone || 'Asia/Shanghai'` ——
952
+ // defaultTimezone 恒有值 ⇒ 兜底永远轮不到 ⇒ **工具建的每个任务都被
953
+ // 显式写死北京时区存进 tasks.json**,顶层 timezone 配置形同虚设
954
+ // (马耳他用户说"每天 9 点",会在当地凌晨 3 点触发)。
955
+ // 改为跟随引擎时区;per-task 的显式 timezone 仍然优先。
956
+ defaultTimezone: rawCron.defaultTimezone || resolvedTimezone,
735
957
  tickIntervalMs: rawCron.tickIntervalMs || 1e4,
736
958
  maxConsecutiveFailures: rawCron.maxConsecutiveFailures || 5,
737
959
  retry: {
@@ -744,6 +966,7 @@ function loadConfig(configPath) {
744
966
  return {
745
967
  ...raw,
746
968
  // 先展开 raw:未显式处理的字段(如 webhook)自动进来,避免每次加 config 都要手动挑;下面显式字段覆盖 raw(解析值优先)
969
+ timezone: resolvedTimezone,
747
970
  provider,
748
971
  providers,
749
972
  // 所有 provider 配置字典(供 memory/provider 动态切换用)
@@ -812,6 +1035,7 @@ var init_loader = __esm({
812
1035
  "use strict";
813
1036
  init_display();
814
1037
  init_features();
1038
+ init_clock();
815
1039
  loadWarnings = [];
816
1040
  loadWarningsFlushed = false;
817
1041
  }
@@ -972,6 +1196,9 @@ function openDb(workspace) {
972
1196
  day TEXT,
973
1197
  time TEXT,
974
1198
  date_str TEXT,
1199
+ -- \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
1200
+ -- 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
1201
+ -- 0920 \u8D77\u5199\u5165\u6B63\u786E UTC Z\u3002\u8BFB\u8FD9\u4E09\u5217\u524D\u5148\u786E\u8BA4\u4F60\u8981\u7684\u662F\u4EC0\u4E48\u3002
975
1202
  created_at TEXT NOT NULL,
976
1203
  done_at TEXT,
977
1204
  archived_at TEXT
@@ -1014,7 +1241,7 @@ function migrate(db2) {
1014
1241
  db2.exec("ALTER TABLE events ADD COLUMN doc_path TEXT");
1015
1242
  }
1016
1243
  }
1017
- var _sqlite, DatabaseSync, TZ_OFFSET_MS;
1244
+ var _sqlite, DatabaseSync;
1018
1245
  var init_db = __esm({
1019
1246
  "src/calendar/db.ts"() {
1020
1247
  "use strict";
@@ -1028,7 +1255,6 @@ var init_db = __esm({
1028
1255
  return getSqlite().DatabaseSync[prop];
1029
1256
  }
1030
1257
  });
1031
- TZ_OFFSET_MS = 8 * 60 * 60 * 1e3;
1032
1258
  }
1033
1259
  });
1034
1260
 
@@ -1638,6 +1864,9 @@ If you can say it in one sentence, don't use three. Prefer short, direct sentenc
1638
1864
  }
1639
1865
  function getEnvInfoSection(workspace) {
1640
1866
  const envItems = [
1867
+ // CC 上游本来就有日期项,移植时漏了 —— 补回来。
1868
+ // 本段在 dynamic prompt(每轮重建),日期一天只变一次,不伤 prefix cache。
1869
+ `Today's date: ${todayKey()} (${tz()})`,
1641
1870
  `Primary working directory: ${workspace}`,
1642
1871
  `Is directory a git repo: ${false}`,
1643
1872
  `Platform: ${os2.platform()}`,
@@ -1751,8 +1980,8 @@ function buildActiveGoalsSection(stateDir) {
1751
1980
  if (!active.length) return void 0;
1752
1981
  const events = listGoalEvents(stateDir);
1753
1982
  const fmt = (iso) => {
1754
- const d = new Date(iso);
1755
- return Number.isNaN(d.getTime()) ? "?" : `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
1983
+ const p = wallParts(iso);
1984
+ return Number.isNaN(Date.parse(iso)) ? "?" : `${p.mo}/${p.d} ${String(p.hh).padStart(2, "0")}:${String(p.mm).padStart(2, "0")}`;
1756
1985
  };
1757
1986
  const summary = active.map((g) => {
1758
1987
  const step = g.steps[g.currentStep];
@@ -1793,10 +2022,8 @@ ${skillsListing}`);
1793
2022
  return parts.join("\n\n");
1794
2023
  }
1795
2024
  function buildVolatileRuntimeContext() {
1796
- const now = /* @__PURE__ */ new Date();
1797
- const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
1798
2025
  return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
1799
- \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
2026
+ \u5F53\u524D\u65F6\u95F4: ${formatForLlm()}`;
1800
2027
  }
1801
2028
  function formatSkillsListingForPrompt() {
1802
2029
  const tools = registry.list();
@@ -1815,6 +2042,7 @@ var init_prompt = __esm({
1815
2042
  "use strict";
1816
2043
  init_registry();
1817
2044
  init_store();
2045
+ init_clock();
1818
2046
  init_memdir();
1819
2047
  init_paths();
1820
2048
  BASH_TOOL_NAME = "exec";
@@ -1900,16 +2128,18 @@ function shouldRetry(status, errorBody) {
1900
2128
  if (status === 529) return true;
1901
2129
  if (status === 429) {
1902
2130
  const body = errorBody?.toLowerCase() || "";
1903
- 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")) {
2131
+ 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")) {
1904
2132
  return false;
1905
2133
  }
1906
2134
  return true;
1907
2135
  }
1908
2136
  if (status === 408) return true;
1909
2137
  if (status === 409) return true;
1910
- if (status === 401) return true;
1911
- if (status === 403) return true;
1912
- if (status >= 500) return true;
2138
+ if (status >= 500) {
2139
+ const body = errorBody?.toLowerCase() || "";
2140
+ if (body.includes("\u65E0\u53EF\u7528\u6E20\u9053") || body.includes("new_sensitive")) return false;
2141
+ return true;
2142
+ }
1913
2143
  if (errorBody?.includes('"type":"overloaded_error"')) return true;
1914
2144
  return false;
1915
2145
  }
@@ -2146,6 +2376,29 @@ var init_think_stripper = __esm({
2146
2376
  }
2147
2377
  });
2148
2378
 
2379
+ // src/utils/well-formed.ts
2380
+ function truncateByCodePoint(text, max) {
2381
+ if (text.length <= max) return text;
2382
+ return Array.from(text).slice(0, max).join("");
2383
+ }
2384
+ function toWellFormed(s) {
2385
+ if (HAS_NATIVE) return s.toWellFormed();
2386
+ return s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
2387
+ }
2388
+ function wellFormedReplacer(_key, value) {
2389
+ return typeof value === "string" ? toWellFormed(value) : value;
2390
+ }
2391
+ function safeStringify(body) {
2392
+ return JSON.stringify(body, wellFormedReplacer);
2393
+ }
2394
+ var HAS_NATIVE;
2395
+ var init_well_formed = __esm({
2396
+ "src/utils/well-formed.ts"() {
2397
+ "use strict";
2398
+ HAS_NATIVE = typeof String.prototype.toWellFormed === "function";
2399
+ }
2400
+ });
2401
+
2149
2402
  // src/models/openai-provider.ts
2150
2403
  function sanitizeToolCallForSend(tc) {
2151
2404
  const args = tc.function?.arguments;
@@ -2179,6 +2432,7 @@ var init_openai_provider = __esm({
2179
2432
  init_withRetry();
2180
2433
  init_provider();
2181
2434
  init_think_stripper();
2435
+ init_well_formed();
2182
2436
  OpenAIProvider = class {
2183
2437
  // 0831 <think> 剥离改用 ThinkTagStripper 的**局部**实例(见 streamChat)——
2184
2438
  // 原来是实例字段,跨请求共享状态,流中途 abort 会把 active=true 带到下一个请求
@@ -2267,13 +2521,17 @@ var init_openai_provider = __esm({
2267
2521
  }
2268
2522
  const actualThinking = body.chat_template_kwargs;
2269
2523
  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}`);
2524
+ let bodyStr = safeStringify(body);
2525
+ if (this.config.wafSingleQuoteWorkaround) {
2526
+ bodyStr = bodyStr.replace(/'/g, "\u2019");
2527
+ }
2270
2528
  const retryGen = fetchWithRetry(url, {
2271
2529
  method: "POST",
2272
2530
  headers: {
2273
2531
  "Content-Type": "application/json",
2274
2532
  "Authorization": `Bearer ${this.config.apiKey}`
2275
2533
  },
2276
- body: JSON.stringify(body),
2534
+ body: bodyStr,
2277
2535
  signal: params.signal
2278
2536
  }, "openai", this.config.proxy);
2279
2537
  let response;
@@ -2471,7 +2729,7 @@ function adaptUsage(u) {
2471
2729
  });
2472
2730
  }
2473
2731
  function serializeBody(body, wafSingleQuoteWorkaround) {
2474
- let bodyStr = JSON.stringify(body);
2732
+ let bodyStr = safeStringify(body);
2475
2733
  if (wafSingleQuoteWorkaround) {
2476
2734
  bodyStr = bodyStr.replace(/'/g, "\u2019");
2477
2735
  }
@@ -2498,6 +2756,7 @@ var init_openai_responses_provider = __esm({
2498
2756
  "use strict";
2499
2757
  init_withRetry();
2500
2758
  init_provider();
2759
+ init_well_formed();
2501
2760
  CODEX_CLI_VERSION = "0.154.0";
2502
2761
  CODEX_TERMINAL_TOKEN = "xterm-256color";
2503
2762
  OpenAIResponsesProvider = class {
@@ -2818,12 +3077,14 @@ var init_anthropic_provider = __esm({
2818
3077
  init_withRetry();
2819
3078
  init_provider();
2820
3079
  init_think_stripper();
3080
+ init_well_formed();
2821
3081
  AnthropicProvider = class {
2822
3082
  // 模拟 CC 的 X-Claude-Code-Session-Id
2823
3083
  constructor(config) {
2824
3084
  this.config = config;
2825
3085
  let u = (config.baseUrl || "https://api.anthropic.com").trim();
2826
3086
  while (u.endsWith("/")) u = u.slice(0, -1);
3087
+ if (u.endsWith("/v1")) u = u.slice(0, -3);
2827
3088
  this.baseUrl = u;
2828
3089
  this.sessionId = randomUUID();
2829
3090
  }
@@ -3022,7 +3283,7 @@ var init_anthropic_provider = __esm({
3022
3283
  } catch {
3023
3284
  }
3024
3285
  }
3025
- let bodyStr = JSON.stringify(body);
3286
+ let bodyStr = safeStringify(body);
3026
3287
  if (this.config.wafSingleQuoteWorkaround) {
3027
3288
  const before = bodyStr.length;
3028
3289
  bodyStr = bodyStr.replace(/'/g, "\u2019");
@@ -3242,6 +3503,7 @@ var init_gemini_provider = __esm({
3242
3503
  "src/models/gemini-provider.ts"() {
3243
3504
  "use strict";
3244
3505
  init_withRetry();
3506
+ init_well_formed();
3245
3507
  GeminiProvider = class {
3246
3508
  constructor(config) {
3247
3509
  this.config = config;
@@ -3401,7 +3663,7 @@ var init_gemini_provider = __esm({
3401
3663
  "Content-Type": "application/json",
3402
3664
  "x-goog-api-key": this.config.apiKey
3403
3665
  },
3404
- body: JSON.stringify(body),
3666
+ body: safeStringify(body),
3405
3667
  signal: params.signal
3406
3668
  }, "gemini", this.config.proxy);
3407
3669
  let response;
@@ -3540,7 +3802,9 @@ function createProvider(config) {
3540
3802
  baseUrl: config.baseUrl,
3541
3803
  apiKey: config.apiKey,
3542
3804
  thinking: config.thinking,
3543
- proxy
3805
+ proxy,
3806
+ wafSingleQuoteWorkaround: config.wafSingleQuoteWorkaround
3807
+ // 0919 中转站→agentrouter 同样有 WAF
3544
3808
  });
3545
3809
  case "openai-responses":
3546
3810
  return new OpenAIResponsesProvider({
@@ -5170,7 +5434,7 @@ ${result}`;
5170
5434
  }
5171
5435
  } catch {
5172
5436
  }
5173
- return content.slice(0, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
5437
+ return truncateByCodePoint(content, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
5174
5438
  }
5175
5439
  function foldTurnInline(turn) {
5176
5440
  const toolMsgs = turn.filter((m) => m.role === "tool");
@@ -5300,7 +5564,8 @@ function step3_truncateLarge(messages) {
5300
5564
  details.push(`tool_call_id=${m.tool_call_id?.slice(0, 8)}... ${originalLen} \u2192 ${TRUNCATE_TO} chars`);
5301
5565
  return {
5302
5566
  ...m,
5303
- content: m.content.slice(0, TRUNCATE_TO) + "\n...[TRUNCATED]"
5567
+ // 0918:按 code point 切,避免切开 emoji 代理对(见 utils/well-formed.ts)
5568
+ content: truncateByCodePoint(m.content, TRUNCATE_TO) + "\n...[TRUNCATED]"
5304
5569
  };
5305
5570
  }
5306
5571
  return m;
@@ -5390,6 +5655,7 @@ var init_ruleCompact = __esm({
5390
5655
  init_tokenEstimate();
5391
5656
  init_autoCompact();
5392
5657
  init_compactLog();
5658
+ init_well_formed();
5393
5659
  TAG3 = "ruleCompact";
5394
5660
  TOOL_SIZE_LIMIT = 8e3;
5395
5661
  TRUNCATE_TO = 5e3;
@@ -6666,6 +6932,7 @@ var init_skill_gate_classifier = __esm({
6666
6932
  "src/skills/skill-gate-classifier.ts"() {
6667
6933
  "use strict";
6668
6934
  init_skill_gate_window();
6935
+ init_well_formed();
6669
6936
  }
6670
6937
  });
6671
6938
 
@@ -10856,7 +11123,7 @@ var init_agentSwarmsEnabled = __esm({
10856
11123
  });
10857
11124
 
10858
11125
  // src/swarm/teamHelpers.ts
10859
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
11126
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "node:fs";
10860
11127
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
10861
11128
  import { join as join18 } from "node:path";
10862
11129
  function getTeamsDir() {
@@ -10949,7 +11216,7 @@ async function cleanupTeamDirectories(teamName) {
10949
11216
  }
10950
11217
  function ensureTasksDir(taskListId) {
10951
11218
  const dir = join18(getTasksDir(), taskListId);
10952
- mkdirSync8(dir, { recursive: true });
11219
+ mkdirSync9(dir, { recursive: true });
10953
11220
  }
10954
11221
  function setLeaderTeamName(name) {
10955
11222
  _leaderTeamName = name;
@@ -11865,11 +12132,65 @@ var init_AgentTool = __esm({
11865
12132
  }
11866
12133
  });
11867
12134
 
12135
+ // src/tools/tavily-pool.ts
12136
+ var tavily_pool_exports = {};
12137
+ __export(tavily_pool_exports, {
12138
+ relayOriginFrom: () => relayOriginFrom,
12139
+ resetTavilyPool: () => resetTavilyPool,
12140
+ resolvePoolTavilyKey: () => resolvePoolTavilyKey
12141
+ });
12142
+ function resetTavilyPool() {
12143
+ cache = null;
12144
+ }
12145
+ function relayOriginFrom(cfg) {
12146
+ const rid = cfg?.tools?.my_selfie?.relayProviderId || cfg?.my_selfie?.relayProviderId || RELAY_PROVIDER_DEFAULT;
12147
+ const p = cfg?.models?.providers?.[rid] || cfg?.providers?.[rid];
12148
+ const baseUrl = String(p?.baseUrl || "").trim();
12149
+ const token = String(p?.apiKey || "").trim();
12150
+ if (!baseUrl || !token) return null;
12151
+ const origin = baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "").replace(/\/+$/, "");
12152
+ return origin ? { origin, token } : null;
12153
+ }
12154
+ async function resolvePoolTavilyKey(opts = {}) {
12155
+ const relay = relayOriginFrom(opts.cfg ?? liveConfig.all());
12156
+ if (!relay) return "";
12157
+ const doFetch = opts.fetchImpl ?? fetch;
12158
+ if (!cache || opts.force || Date.now() - cache.fetchedAt > CACHE_TTL_MS) {
12159
+ try {
12160
+ const res = await doFetch(`${relay.origin}/api/engine7/search-keys`, {
12161
+ headers: { Authorization: `Bearer ${relay.token}` }
12162
+ });
12163
+ const body = await res.json().catch(() => null);
12164
+ const keys = Array.isArray(body?.data?.keys) ? body.data.keys.filter((k) => typeof k === "string" && k.trim()) : [];
12165
+ cache = { keys, fetchedAt: Date.now() };
12166
+ } catch {
12167
+ if (!cache) return "";
12168
+ cache.fetchedAt = Date.now();
12169
+ }
12170
+ }
12171
+ if (!cache.keys.length) return "";
12172
+ const key = cache.keys[cursor % cache.keys.length];
12173
+ cursor++;
12174
+ return key;
12175
+ }
12176
+ var CACHE_TTL_MS, RELAY_PROVIDER_DEFAULT, cache, cursor;
12177
+ var init_tavily_pool = __esm({
12178
+ "src/tools/tavily-pool.ts"() {
12179
+ "use strict";
12180
+ init_live();
12181
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
12182
+ RELAY_PROVIDER_DEFAULT = "7yi";
12183
+ cache = null;
12184
+ cursor = 0;
12185
+ }
12186
+ });
12187
+
11868
12188
  // src/tools/web-search.ts
11869
12189
  var init_web_search = __esm({
11870
12190
  "src/tools/web-search.ts"() {
11871
12191
  "use strict";
11872
12192
  init_registry();
12193
+ init_well_formed();
11873
12194
  registry.register({
11874
12195
  name: "web_search",
11875
12196
  description: "\u641C\u7D22\u4E92\u8054\u7F51\u3002\u8FD4\u56DE\u641C\u7D22\u7ED3\u679C\u548C\u94FE\u63A5\u3002",
@@ -11886,14 +12207,25 @@ var init_web_search = __esm({
11886
12207
  const query = args.query;
11887
12208
  const maxResults = args.max_results || 5;
11888
12209
  const searchDepth = args.search_depth || "basic";
11889
- const apiKey = process.env.TAVILY_API_KEY;
11890
- if (!apiKey) return { content: "\u672A\u914D\u7F6E TAVILY_API_KEY", isError: true };
12210
+ let apiKey = process.env.TAVILY_API_KEY || "";
12211
+ let fromPool = false;
12212
+ if (!apiKey) {
12213
+ const { resolvePoolTavilyKey: resolvePoolTavilyKey2 } = await Promise.resolve().then(() => (init_tavily_pool(), tavily_pool_exports));
12214
+ apiKey = await resolvePoolTavilyKey2();
12215
+ fromPool = !!apiKey;
12216
+ }
12217
+ if (!apiKey) {
12218
+ return {
12219
+ 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",
12220
+ isError: true
12221
+ };
12222
+ }
11891
12223
  const startTime = Date.now();
11892
12224
  try {
11893
12225
  const response = await fetch("https://api.tavily.com/search", {
11894
12226
  method: "POST",
11895
12227
  headers: { "Content-Type": "application/json" },
11896
- body: JSON.stringify({
12228
+ body: safeStringify({
11897
12229
  query,
11898
12230
  max_results: maxResults,
11899
12231
  include_answer: true,
@@ -11903,6 +12235,14 @@ var init_web_search = __esm({
11903
12235
  signal
11904
12236
  });
11905
12237
  if (!response.ok) {
12238
+ if (fromPool && (response.status === 401 || response.status === 429 || response.status === 432)) {
12239
+ const { resetTavilyPool: resetTavilyPool2 } = await Promise.resolve().then(() => (init_tavily_pool(), tavily_pool_exports));
12240
+ resetTavilyPool2();
12241
+ return {
12242
+ 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`,
12243
+ isError: true
12244
+ };
12245
+ }
11906
12246
  return { content: `\u641C\u7D22\u5931\u8D25: HTTP ${response.status}`, isError: true };
11907
12247
  }
11908
12248
  const data = await response.json();
@@ -12581,15 +12921,16 @@ import * as path21 from "node:path";
12581
12921
  function resolveLatestImage(specifiedPath, mediaDir) {
12582
12922
  if (specifiedPath && fs19.existsSync(specifiedPath)) return specifiedPath;
12583
12923
  if (!fs19.existsSync(mediaDir)) return null;
12584
- const files = fs19.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp)$/i.test(f)).map((f) => ({ name: f, p: path21.join(mediaDir, f), mtime: fs19.statSync(path21.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
12924
+ const files = fs19.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp|mp4|mov)$/i.test(f)).map((f) => ({ name: f, p: path21.join(mediaDir, f), mtime: fs19.statSync(path21.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
12585
12925
  return files[0]?.p || null;
12586
12926
  }
12587
- var MIME_MAP;
12927
+ var MIME_MAP, VIDEO_EXT, VIDEO_MAX_BYTES;
12588
12928
  var init_my_eyes = __esm({
12589
12929
  "src/tools/my-eyes.ts"() {
12590
12930
  "use strict";
12591
12931
  init_live();
12592
12932
  init_registry();
12933
+ init_well_formed();
12593
12934
  MIME_MAP = {
12594
12935
  ".jpg": "jpeg",
12595
12936
  ".jpeg": "jpeg",
@@ -12598,13 +12939,15 @@ var init_my_eyes = __esm({
12598
12939
  ".gif": "gif",
12599
12940
  ".bmp": "bmp"
12600
12941
  };
12942
+ VIDEO_EXT = /\.(mp4|mov)$/i;
12943
+ VIDEO_MAX_BYTES = 40 * 1024 * 1024;
12601
12944
  registry.register({
12602
12945
  name: "my_eyes",
12603
- 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.",
12946
+ 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.",
12604
12947
  schema: {
12605
12948
  type: "object",
12606
12949
  properties: {
12607
- image_path: { type: "string", description: "Absolute path to the image file to analyze." },
12950
+ image_path: { type: "string", description: "Absolute path to the image (or .mp4/.mov video) file to analyze." },
12608
12951
  prompt: { type: "string", description: 'What to ask about the image. Default: "\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9"' }
12609
12952
  },
12610
12953
  required: ["image_path"]
@@ -12633,6 +12976,42 @@ var init_my_eyes = __esm({
12633
12976
  }
12634
12977
  const rawPrompt = args.prompt?.trim() || "\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9";
12635
12978
  const ext = path21.extname(imagePath).toLowerCase();
12979
+ if (VIDEO_EXT.test(imagePath)) {
12980
+ const stat4 = fs19.statSync(imagePath);
12981
+ if (stat4.size > VIDEO_MAX_BYTES) {
12982
+ return { content: `Error: video too large (${(stat4.size / 1e6).toFixed(1)}MB > 40MB). Compress or trim first.`, isError: true };
12983
+ }
12984
+ const explicit = liveConfig.get("tools.my_eyes.videoBaseUrl");
12985
+ const baseUrl = explicit || providerCfg.baseUrl.replace(/\/anthropic\/?$/, "/v1").replace(/\/$/, "");
12986
+ const vidB64 = fs19.readFileSync(imagePath).toString("base64");
12987
+ try {
12988
+ const res = await fetch(`${baseUrl}/chat/completions`, {
12989
+ method: "POST",
12990
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${providerCfg.apiKey}` },
12991
+ body: safeStringify({
12992
+ model: modelId,
12993
+ messages: [{
12994
+ role: "user",
12995
+ content: [
12996
+ { type: "video_url", video_url: { url: `data:video/mp4;base64,${vidB64}` } },
12997
+ { type: "text", text: rawPrompt }
12998
+ ]
12999
+ }],
13000
+ max_tokens: 4096
13001
+ })
13002
+ });
13003
+ if (!res.ok) {
13004
+ const t = await res.text();
13005
+ return { content: `Video analysis failed: HTTP ${res.status} ${t.slice(0, 300)}`, isError: true };
13006
+ }
13007
+ const data = await res.json();
13008
+ const content = data.choices?.[0]?.message?.content || "";
13009
+ if (!content.trim()) return { content: "Video analysis returned empty.", isError: true };
13010
+ return { content };
13011
+ } catch (e) {
13012
+ return { content: `Video analysis failed: ${e.message}`, isError: true };
13013
+ }
13014
+ }
12636
13015
  const mime = MIME_MAP[ext] || "jpeg";
12637
13016
  const imgB64 = fs19.readFileSync(imagePath).toString("base64");
12638
13017
  const userMsg = {
@@ -12976,7 +13355,7 @@ async function ttsQwen3Local(text, cfg, onChunk) {
12976
13355
  const res = await fetch(`${url}/tts`, {
12977
13356
  method: "POST",
12978
13357
  headers: { "Content-Type": "application/json" },
12979
- body: JSON.stringify({
13358
+ body: safeStringify({
12980
13359
  text,
12981
13360
  voice: cfg.voice || "xiaoya",
12982
13361
  lang: cfg.lang || "Chinese"
@@ -13007,7 +13386,7 @@ async function ttsQwen3Opus(text, cfg) {
13007
13386
  const res = await fetch(`${url}/tts?format=opus`, {
13008
13387
  method: "POST",
13009
13388
  headers: { "Content-Type": "application/json; charset=utf-8" },
13010
- body: JSON.stringify({
13389
+ body: safeStringify({
13011
13390
  text,
13012
13391
  voice: cfg.voice || "xiaoya",
13013
13392
  lang: cfg.lang || "Chinese"
@@ -13252,6 +13631,7 @@ var init_my_voice = __esm({
13252
13631
  "src/tools/my-voice.ts"() {
13253
13632
  "use strict";
13254
13633
  init_live();
13634
+ init_well_formed();
13255
13635
  init_registry();
13256
13636
  init_opus_encoder();
13257
13637
  init_voice_chat_tts_daemon();
@@ -13431,6 +13811,11 @@ var init_my_voice = __esm({
13431
13811
  });
13432
13812
 
13433
13813
  // src/tools/my-selfie.ts
13814
+ var my_selfie_exports = {};
13815
+ __export(my_selfie_exports, {
13816
+ dimsOf: () => dimsOf,
13817
+ resolvePortraitDesc: () => resolvePortraitDesc
13818
+ });
13434
13819
  import * as fs22 from "node:fs";
13435
13820
  import * as path23 from "node:path";
13436
13821
  function getProxyDispatcher2() {
@@ -13444,10 +13829,46 @@ function getProxyDispatcher2() {
13444
13829
  return void 0;
13445
13830
  }
13446
13831
  }
13447
- function getFalKey(cfg) {
13448
- return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || FAL_KEY_FALLBACK;
13449
- }
13450
- function getReferences(ctx) {
13832
+ function dimsOf(p) {
13833
+ try {
13834
+ const b = fs22.readFileSync(p);
13835
+ let w = 0, h = 0;
13836
+ if (b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
13837
+ w = b.readUInt32BE(16);
13838
+ h = b.readUInt32BE(20);
13839
+ } else if (b[0] === 255 && b[1] === 216) {
13840
+ let i = 2;
13841
+ while (i < b.length - 9) {
13842
+ if (b[i] !== 255) {
13843
+ i++;
13844
+ continue;
13845
+ }
13846
+ const m = b[i + 1];
13847
+ if (m >= 192 && m <= 207 && m !== 196 && m !== 200 && m !== 204) {
13848
+ h = b.readUInt16BE(i + 5);
13849
+ w = b.readUInt16BE(i + 7);
13850
+ break;
13851
+ }
13852
+ i += 2 + b.readUInt16BE(i + 2);
13853
+ }
13854
+ } else {
13855
+ return null;
13856
+ }
13857
+ if (!w || !h) return null;
13858
+ const bad = [];
13859
+ if (w < 256 || w > 4096) bad.push(`\u5BBD ${w} \u4E0D\u5728 256~4096`);
13860
+ if (h < 256 || h > 4096) bad.push(`\u9AD8 ${h} \u4E0D\u5728 256~4096`);
13861
+ const r = Math.max(w, h) / Math.min(w, h);
13862
+ if (r > 2) bad.push(`\u957F\u5BBD\u6BD4 ${r.toFixed(2)}:1 \u8D85\u8FC7 2:1`);
13863
+ return bad.length ? `${w}x${h} \u4E0D\u5408\u683C\uFF1A${bad.join("\uFF1B")}` : `${w}x${h}`;
13864
+ } catch {
13865
+ return null;
13866
+ }
13867
+ }
13868
+ function getFalKey(cfg) {
13869
+ return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || process.env.FAL_API_KEY || "";
13870
+ }
13871
+ function getReferences(ctx) {
13451
13872
  const cfg = liveConfig.all();
13452
13873
  const cfgRefs = cfg.my_selfie?.references || cfg.tools?.my_selfie?.references;
13453
13874
  if (Array.isArray(cfgRefs) && cfgRefs.length > 0) {
@@ -13464,6 +13885,72 @@ function getXaiKey(ctx) {
13464
13885
  const xaiCfg = cfg.providers?.xai || cfg.models?.providers?.xai;
13465
13886
  return xaiCfg?.apiKey || "";
13466
13887
  }
13888
+ function getRelayCfg(cfg) {
13889
+ const id = cfg?.my_selfie?.relayProviderId || cfg?.tools?.my_selfie?.relayProviderId || "7yi";
13890
+ const p = cfg?.models?.providers?.[id] || cfg?.providers?.[id];
13891
+ return { baseUrl: String(p?.baseUrl || "").replace(/\/+$/, ""), apiKey: p?.apiKey || "" };
13892
+ }
13893
+ function resolvePortraitDesc(argPortrait, cfgPortrait) {
13894
+ const fromArg = typeof argPortrait === "string" ? argPortrait.trim() : "";
13895
+ if (fromArg) return fromArg;
13896
+ return typeof cfgPortrait === "string" ? cfgPortrait.trim() : "";
13897
+ }
13898
+ async function generateWithRelay(imageB64, prompt, mime, baseUrl, apiKey, model) {
13899
+ const res = await fetch(`${baseUrl}/images/edits`, {
13900
+ method: "POST",
13901
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
13902
+ // safeStringify 不是安全选项而是必须:prompt 里一个孤立代理(半个 emoji)
13903
+ // 就会让上游 500。同批上游提交刚在 fal/grok/comfy 三处修过同一个坑。
13904
+ body: safeStringify({
13905
+ model,
13906
+ prompt,
13907
+ n: 1,
13908
+ response_format: "b64_json",
13909
+ images: [{ image_url: `data:${mime};base64,${imageB64}` }]
13910
+ })
13911
+ });
13912
+ if (!res.ok) {
13913
+ const text = await res.text();
13914
+ throw new Error(`relay ${res.status}: ${text.slice(0, 300)}`);
13915
+ }
13916
+ const data = await res.json();
13917
+ if (!data.data?.length) throw new Error("No images returned from relay");
13918
+ return Buffer.from(data.data[0].b64_json, "base64");
13919
+ }
13920
+ async function generatePortraitWithRelay(desc, baseUrl, apiKey, model) {
13921
+ const res = await fetch(`${baseUrl}/images/generations`, {
13922
+ method: "POST",
13923
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
13924
+ // 同上:描述里可能带 emoji,孤立代理会让上游 500
13925
+ body: safeStringify({
13926
+ model,
13927
+ prompt: PORTRAIT_PROMPT(desc),
13928
+ n: 1,
13929
+ size: "1024x1024",
13930
+ response_format: "b64_json"
13931
+ })
13932
+ });
13933
+ if (!res.ok) {
13934
+ const text = await res.text();
13935
+ throw new Error(`relay portrait ${res.status}: ${text.slice(0, 300)}`);
13936
+ }
13937
+ const data = await res.json();
13938
+ if (!data.data?.length) throw new Error("No images returned from relay (portrait)");
13939
+ return Buffer.from(data.data[0].b64_json, "base64");
13940
+ }
13941
+ function registerPortrait(relPath, name) {
13942
+ try {
13943
+ const cfg = liveConfig.all();
13944
+ const cur = cfg?.tools?.my_selfie?.references || cfg?.my_selfie?.references || [];
13945
+ const next = Array.isArray(cur) ? cur.filter((r) => r?.name !== name) : [];
13946
+ next.unshift({ name, path: relPath });
13947
+ const path34 = cfg?.tools?.my_selfie ? "tools.my_selfie.references" : "my_selfie.references";
13948
+ void liveConfig.set(path34, next);
13949
+ console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u767B\u8BB0\uFF1A${path34}[0] = ${relPath}`);
13950
+ } catch (e) {
13951
+ console.warn(`[my-selfie] \u5199\u5165 references \u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u6B21\u51FA\u56FE\uFF09: ${e.message}`);
13952
+ }
13953
+ }
13467
13954
  function detectMode(input) {
13468
13955
  const lower = input.toLowerCase();
13469
13956
  for (const [mode, keywords] of Object.entries(MODE_KEYWORDS)) {
@@ -13510,10 +13997,16 @@ async function generateWithFal(imageB64, prompt, resolution) {
13510
13997
  resolution
13511
13998
  };
13512
13999
  if (aspectRatio) body.aspect_ratio = aspectRatio;
14000
+ const falKey = getFalKey(cfg);
14001
+ if (!falKey) {
14002
+ throw new Error(
14003
+ `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`
14004
+ );
14005
+ }
13513
14006
  const res = await fetch(FAL_ENDPOINT, {
13514
14007
  method: "POST",
13515
- headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
13516
- body: JSON.stringify(body)
14008
+ headers: { "Authorization": `Key ${falKey}`, "Content-Type": "application/json" },
14009
+ body: safeStringify(body)
13517
14010
  });
13518
14011
  if (!res.ok) {
13519
14012
  const text = await res.text();
@@ -13550,7 +14043,7 @@ async function comfyQueue(server, workflow) {
13550
14043
  const res = await fetch(`${server}/prompt`, {
13551
14044
  method: "POST",
13552
14045
  headers: { "Content-Type": "application/json" },
13553
- body: JSON.stringify({ prompt: workflow })
14046
+ body: safeStringify({ prompt: workflow })
13554
14047
  });
13555
14048
  if (!res.ok) return null;
13556
14049
  const data = await res.json();
@@ -13678,7 +14171,7 @@ async function generateWithGrok(imageB64, prompt, xaiKey) {
13678
14171
  "Authorization": `Bearer ${xaiKey}`,
13679
14172
  "Content-Type": "application/json"
13680
14173
  },
13681
- body: JSON.stringify({
14174
+ body: safeStringify({
13682
14175
  model: "grok-imagine-image-quality",
13683
14176
  prompt,
13684
14177
  image: {
@@ -13698,15 +14191,15 @@ async function generateWithGrok(imageB64, prompt, xaiKey) {
13698
14191
  if (!data.data?.length) throw new Error("No images returned from grok");
13699
14192
  return Buffer.from(data.data[0].b64_json, "base64");
13700
14193
  }
13701
- var FAL_ENDPOINT, DEFAULT_RESOLUTION, FAL_KEY_FALLBACK, DEFAULT_REFERENCES, MODE_PROMPTS, MODE_KEYWORDS;
14194
+ var FAL_ENDPOINT, DEFAULT_RESOLUTION, DEFAULT_REFERENCES, PORTRAIT_PROMPT, MODE_PROMPTS, MODE_KEYWORDS;
13702
14195
  var init_my_selfie = __esm({
13703
14196
  "src/tools/my-selfie.ts"() {
13704
14197
  "use strict";
13705
14198
  init_live();
13706
14199
  init_registry();
14200
+ init_well_formed();
13707
14201
  FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
13708
14202
  DEFAULT_RESOLUTION = "1k";
13709
- FAL_KEY_FALLBACK = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
13710
14203
  DEFAULT_REFERENCES = [
13711
14204
  { name: "default", p: "images/xiaomei_clean_v2.png" },
13712
14205
  { name: "v3", p: "images/xiaomei_clean_v3.png" },
@@ -13714,6 +14207,7 @@ var init_my_selfie = __esm({
13714
14207
  { name: "blue", p: "images/xiaomei_blue_shirt_base.jpg" },
13715
14208
  { name: "office", p: "images/xiaomei_office_final.jpg" }
13716
14209
  ];
14210
+ 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.`;
13717
14211
  MODE_PROMPTS = {
13718
14212
  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`,
13719
14213
  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`,
@@ -13726,15 +14220,17 @@ var init_my_selfie = __esm({
13726
14220
  };
13727
14221
  registry.register({
13728
14222
  name: "my_selfie",
13729
- 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.",
14223
+ 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",
13730
14224
  schema: {
13731
14225
  type: "object",
13732
14226
  properties: {
13733
14227
  prompt: { type: "string", description: "Scene/outfit/mood description, e.g. '\u5728\u5496\u5561\u9986\u770B\u4E66' or '\u7A7F\u7EA2\u88D9\u5B50'." },
13734
14228
  mode: { type: "string", enum: ["auto", "mirror", "direct", "lifestyle"], description: "Selfie mode. 'auto' detects from prompt (default)." },
13735
14229
  reference: { type: "string", description: "Reference base image name (e.g. 'default', 'v3'). Configurable per agent via my_selfie.references in config." },
13736
- 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)." },
13737
- resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." }
14230
+ 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)." },
14231
+ resolution: { type: "string", enum: ["1k", "2k"], description: "Image resolution (fal only). Default: '1k'." },
14232
+ 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" },
14233
+ 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" }
13738
14234
  },
13739
14235
  required: ["prompt"]
13740
14236
  },
@@ -13748,9 +14244,61 @@ var init_my_selfie = __esm({
13748
14244
  const REFERENCES = getReferences(ctx);
13749
14245
  const refName = args.reference || "default";
13750
14246
  const refEntry = REFERENCES.find((r) => r.name === refName) || REFERENCES[0];
13751
- const refPath = path23.join(ctx.workspace, refEntry.p);
14247
+ let refPath = path23.join(ctx.workspace, refEntry.p);
14248
+ const adoptImg = args.referenceImage?.trim();
14249
+ if (adoptImg) {
14250
+ if (!fs22.existsSync(adoptImg)) {
14251
+ return { content: `\u627E\u4E0D\u5230\u90A3\u5F20\u56FE\uFF1A${adoptImg}`, isError: true };
14252
+ }
14253
+ const acheck = dimsOf(adoptImg);
14254
+ if (acheck?.includes("\u4E0D\u5408\u683C")) {
14255
+ return {
14256
+ content: `\u8FD9\u5F20\u56FE\u7528\u4E0D\u4E86\uFF1A${acheck}\u3002
14257
+ \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`,
14258
+ isError: true
14259
+ };
14260
+ }
14261
+ const adir = path23.join(ctx.workspace, "images");
14262
+ if (!fs22.existsSync(adir)) fs22.mkdirSync(adir, { recursive: true });
14263
+ const aext = (path23.extname(adoptImg) || ".png").toLowerCase();
14264
+ const afname = `portrait_${Date.now()}${aext}`;
14265
+ fs22.copyFileSync(adoptImg, path23.join(adir, afname));
14266
+ registerPortrait(`images/${afname}`, refName);
14267
+ refPath = path23.join(adir, afname);
14268
+ console.log(`[my-selfie] \u91C7\u7EB3\u73B0\u6210\u56FE\u5F53\u5F62\u8C61\uFF1Aimages/${afname}${acheck ? ` (${acheck})` : ""}`);
14269
+ }
13752
14270
  if (provider !== "autodl" && !fs22.existsSync(refPath)) {
13753
- return { content: `Error: reference image not found at ${refPath}`, isError: true };
14271
+ const cfgAll = liveConfig.all();
14272
+ const portraitDesc = resolvePortraitDesc(
14273
+ args.portrait,
14274
+ cfgAll?.tools?.my_selfie?.portraitDesc ?? cfgAll?.my_selfie?.portraitDesc
14275
+ );
14276
+ if (!portraitDesc) {
14277
+ return {
14278
+ 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
14279
+ \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
14280
+ \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`
14281
+ };
14282
+ }
14283
+ const relay = getRelayCfg(cfgAll);
14284
+ if (!relay.baseUrl || !relay.apiKey) {
14285
+ return { content: "Error: relay provider \u672A\u914D\u7F6E\uFF0C\u65E0\u6CD5\u5851\u9020\u5F62\u8C61\u3002", isError: true };
14286
+ }
14287
+ const pmodel = cfgAll?.my_selfie?.relayModel || cfgAll?.tools?.my_selfie?.relayModel || "engine7-img-free";
14288
+ let portraitBuf;
14289
+ try {
14290
+ console.log(`[my-selfie] \u51B7\u542F\uFF1A\u6309\u63CF\u8FF0\u5851\u9020\u5F62\u8C61 -> ${pmodel}`);
14291
+ portraitBuf = await generatePortraitWithRelay(portraitDesc, relay.baseUrl, relay.apiKey, pmodel);
14292
+ } catch (e) {
14293
+ return { content: `\u5851\u9020\u5F62\u8C61\u5931\u8D25\uFF1A${e.message}`, isError: true };
14294
+ }
14295
+ const pdir = path23.join(ctx.workspace, "images");
14296
+ if (!fs22.existsSync(pdir)) fs22.mkdirSync(pdir, { recursive: true });
14297
+ const pfname = `portrait_${Date.now()}.png`;
14298
+ fs22.writeFileSync(path23.join(pdir, pfname), portraitBuf);
14299
+ registerPortrait(`images/${pfname}`, refName);
14300
+ refPath = path23.join(pdir, pfname);
14301
+ console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u5B58 images/${pfname}\uFF0C\u7EE7\u7EED\u8FD9\u6B21\u81EA\u62CD`);
13754
14302
  }
13755
14303
  const resolution = args.resolution || DEFAULT_RESOLUTION;
13756
14304
  const refB64 = fs22.existsSync(refPath) ? fs22.readFileSync(refPath).toString("base64") : "";
@@ -13762,6 +14310,16 @@ var init_my_selfie = __esm({
13762
14310
  const xaiKey = getXaiKey(ctx);
13763
14311
  if (!xaiKey) return { content: "Error: xai apiKey not configured for grok provider", isError: true };
13764
14312
  imageBuffer = await generateWithGrok(refB64, fullPrompt, xaiKey);
14313
+ } else if (provider === "relay") {
14314
+ const cfgAll = liveConfig.all();
14315
+ const relay = getRelayCfg(cfgAll);
14316
+ if (!relay.baseUrl || !relay.apiKey) {
14317
+ 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 };
14318
+ }
14319
+ const model = cfgAll?.my_selfie?.relayModel || cfgAll?.tools?.my_selfie?.relayModel || "engine7-img-free";
14320
+ const ext = path23.extname(refPath).toLowerCase();
14321
+ const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
14322
+ imageBuffer = await generateWithRelay(refB64, fullPrompt, mime, relay.baseUrl, relay.apiKey, model);
13765
14323
  } else {
13766
14324
  imageBuffer = await generateWithFal(refB64, fullPrompt, resolution);
13767
14325
  }
@@ -13884,10 +14442,14 @@ var init_body_move = __esm({
13884
14442
 
13885
14443
  // src/calendar/helpers.ts
13886
14444
  function nowIso() {
13887
- return new Date(Date.now() + TZ_OFFSET_MS2 - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4).toISOString().replace("Z", "+08:00");
14445
+ return toUtc(/* @__PURE__ */ new Date());
13888
14446
  }
13889
- function nowBeijing() {
13890
- return new Date(Date.now() + TZ_OFFSET_MS2);
14447
+ function nowParts() {
14448
+ return wallParts();
14449
+ }
14450
+ function wallMs(y, mo, d, hh, mm) {
14451
+ const z = (n) => String(n).padStart(2, "0");
14452
+ return Date.parse(parseWall(`${y}-${z(mo)}-${z(d)}T${z(hh)}:${z(mm)}:00`));
13891
14453
  }
13892
14454
  function parseDateRange(dateStr) {
13893
14455
  const s = dateStr.replace("\u524D", "");
@@ -13909,32 +14471,29 @@ function parseDateRange(dateStr) {
13909
14471
  }
13910
14472
  }
13911
14473
  function isToday(month, dStart, dEnd) {
13912
- const now = nowBeijing();
13913
- return month === now.getUTCMonth() + 1 && dStart <= now.getUTCDate() && now.getUTCDate() <= dEnd;
14474
+ const p = nowParts();
14475
+ return month === p.mo && dStart <= p.d && p.d <= dEnd;
13914
14476
  }
13915
14477
  function isOverdue(month, dStart, dEnd) {
13916
- const now = nowBeijing();
13917
- const curMonth = now.getUTCMonth() + 1;
13918
- const curDay = now.getUTCDate();
13919
- return month < curMonth || month === curMonth && dEnd < curDay;
14478
+ const p = nowParts();
14479
+ return month < p.mo || month === p.mo && dEnd < p.d;
13920
14480
  }
13921
14481
  function getTodayWeekday() {
13922
- const now = nowBeijing();
13923
- return WEEKDAYS[now.getUTCDay() === 0 ? 6 : now.getUTCDay() - 1];
14482
+ const p = nowParts();
14483
+ return WEEKDAYS[p.weekday === 0 ? 6 : p.weekday - 1];
13924
14484
  }
13925
14485
  function computeRemindAt(dateStr, timeExact, remindBeforeMin) {
13926
14486
  const parsed = parseDateRange(dateStr);
13927
14487
  if (!parsed) return null;
13928
14488
  const { month, dStart } = parsed;
13929
- const now = nowBeijing();
13930
- const year = now.getUTCFullYear();
14489
+ const year = nowParts().y;
13931
14490
  let eventMs;
13932
14491
  if (timeExact) {
13933
14492
  const [hh, mm] = timeExact.split(":").map(Number);
13934
14493
  if (isNaN(hh) || isNaN(mm)) return null;
13935
- eventMs = Date.UTC(year, month - 1, dStart, hh - 8, mm);
14494
+ eventMs = wallMs(year, month, dStart, hh, mm);
13936
14495
  } else {
13937
- eventMs = Date.UTC(year, month - 1, dStart, 9 - 8, 0);
14496
+ eventMs = wallMs(year, month, dStart, 9, 0);
13938
14497
  }
13939
14498
  const remindMs = eventMs - remindBeforeMin * 60 * 1e3;
13940
14499
  return new Date(remindMs).toISOString();
@@ -13942,20 +14501,19 @@ function computeRemindAt(dateStr, timeExact, remindBeforeMin) {
13942
14501
  function computeWeeklyRemindAt(day, time, remindBeforeMin) {
13943
14502
  const targetIdx = WEEKDAYS.indexOf(day);
13944
14503
  if (targetIdx === -1) return null;
13945
- const now = nowBeijing();
13946
- const curIdx = now.getUTCDay() === 0 ? 6 : now.getUTCDay() - 1;
14504
+ const p = nowParts();
14505
+ const curIdx = p.weekday === 0 ? 6 : p.weekday - 1;
13947
14506
  let dayDiff = targetIdx - curIdx;
13948
14507
  if (dayDiff < 0) dayDiff += 7;
13949
- const year = now.getUTCFullYear();
13950
- const month = now.getUTCMonth();
13951
- const date = now.getUTCDate() + dayDiff;
14508
+ const target = wallParts(Date.parse(startOfLocalDay()) + dayDiff * 24 * 60 * 60 * 1e3);
14509
+ const year = target.y, month = target.mo, date = target.d;
13952
14510
  let eventMs;
13953
14511
  if (time && time !== "\u5168\u5929") {
13954
14512
  const [hh, mm] = time.split(":").map(Number);
13955
14513
  if (isNaN(hh) || isNaN(mm)) return null;
13956
- eventMs = Date.UTC(year, month, date, hh - 8, mm);
14514
+ eventMs = wallMs(year, month, date, hh, mm);
13957
14515
  } else {
13958
- eventMs = Date.UTC(year, month, date, 9 - 8, 0);
14516
+ eventMs = wallMs(year, month, date, 9, 0);
13959
14517
  }
13960
14518
  let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
13961
14519
  if (remindMs <= Date.now()) {
@@ -13970,11 +14528,8 @@ function computeWeeklyRemindAt(day, time, remindBeforeMin) {
13970
14528
  function computeDailyRemindAt(time, remindBeforeMin) {
13971
14529
  const [hh, mm] = time.split(":").map(Number);
13972
14530
  if (isNaN(hh) || isNaN(mm)) return null;
13973
- const now = nowBeijing();
13974
- const year = now.getUTCFullYear();
13975
- const month = now.getUTCMonth();
13976
- const date = now.getUTCDate();
13977
- let eventMs = Date.UTC(year, month, date, hh - 8, mm);
14531
+ const p = nowParts();
14532
+ let eventMs = wallMs(p.y, p.mo, p.d, hh, mm);
13978
14533
  let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
13979
14534
  if (remindMs <= Date.now()) {
13980
14535
  remindMs += 24 * 60 * 60 * 1e3;
@@ -13984,18 +14539,17 @@ function computeDailyRemindAt(time, remindBeforeMin) {
13984
14539
  function computeWeekdaysRemindAt(time, remindBeforeMin) {
13985
14540
  const [hh, mm] = time.split(":").map(Number);
13986
14541
  if (isNaN(hh) || isNaN(mm)) return null;
13987
- const now = nowBeijing();
13988
- let year = now.getUTCFullYear();
13989
- let month = now.getUTCMonth();
13990
- let date = now.getUTCDate();
13991
- let curDay = now.getUTCDay();
13992
- let eventMs = Date.UTC(year, month, date, hh - 8, mm);
13993
- let remindMs = eventMs - remindBeforeMin * 60 * 1e3;
13994
- while (remindMs <= Date.now() || curDay === 0 || curDay === 6) {
13995
- date++;
13996
- curDay = (curDay + 1) % 7;
13997
- eventMs = Date.UTC(year, month, date, hh - 8, mm);
14542
+ let dayOffset = 0;
14543
+ let eventMs;
14544
+ let remindMs;
14545
+ let curDay;
14546
+ for (; ; ) {
14547
+ const t = wallParts(Date.parse(startOfLocalDay()) + dayOffset * 24 * 60 * 60 * 1e3);
14548
+ curDay = t.weekday;
14549
+ eventMs = wallMs(t.y, t.mo, t.d, hh, mm);
13998
14550
  remindMs = eventMs - remindBeforeMin * 60 * 1e3;
14551
+ if (remindMs > Date.now() && curDay !== 0 && curDay !== 6) break;
14552
+ dayOffset++;
13999
14553
  }
14000
14554
  return new Date(remindMs).toISOString();
14001
14555
  }
@@ -14018,13 +14572,7 @@ function fmtRow(row) {
14018
14572
  }
14019
14573
  }
14020
14574
  function fmtNow() {
14021
- const now = nowBeijing();
14022
- const y = now.getUTCFullYear();
14023
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
14024
- const d = String(now.getUTCDate()).padStart(2, "0");
14025
- const hh = String(now.getUTCHours()).padStart(2, "0");
14026
- const mm = String(now.getUTCMinutes()).padStart(2, "0");
14027
- return `${y}-${m}-${d} ${getTodayWeekday()} ${hh}:${mm}\uFF08\u5317\u4EAC\u65F6\u95F4\uFF09`;
14575
+ return formatForLlm();
14028
14576
  }
14029
14577
  function parseDuration(s) {
14030
14578
  if (s.endsWith("m")) return parseInt(s.slice(0, -1)) || null;
@@ -14089,18 +14637,18 @@ function checkTaskProximity(rows, newTimeExact) {
14089
14637
  }
14090
14638
  return null;
14091
14639
  }
14092
- var TZ_OFFSET_MS2, WEEKDAYS;
14640
+ var WEEKDAYS;
14093
14641
  var init_helpers = __esm({
14094
14642
  "src/calendar/helpers.ts"() {
14095
14643
  "use strict";
14096
14644
  init_db();
14097
- TZ_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
14645
+ init_clock();
14098
14646
  WEEKDAYS = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
14099
14647
  }
14100
14648
  });
14101
14649
 
14102
14650
  // src/calendar/commands.ts
14103
- import { existsSync as existsSync18, statSync as statSync12 } from "node:fs";
14651
+ import { existsSync as existsSync18, statSync as statSync11 } from "node:fs";
14104
14652
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
14105
14653
  function fmtEnd(start, durationMin) {
14106
14654
  const [hh, mm] = start.split(":").map(Number);
@@ -14180,8 +14728,7 @@ function addDate(db2, args) {
14180
14728
  const parsed = parseDateRange(dateStr);
14181
14729
  let weeklyRows = [];
14182
14730
  if (parsed) {
14183
- const now = new Date(Date.now() + 8 * 60 * 60 * 1e3);
14184
- const year = now.getUTCFullYear();
14731
+ const year = wallParts().y;
14185
14732
  const d = new Date(Date.UTC(year, parsed.month - 1, parsed.dStart));
14186
14733
  const weekday = WEEKDAYS2[d.getUTCDay() === 0 ? 6 : d.getUTCDay() - 1];
14187
14734
  weeklyRows = db2.prepare(
@@ -14230,8 +14777,7 @@ function addBatch(db2, args) {
14230
14777
  const parsed = parseDateRange(dateStr);
14231
14778
  let weeklyRows = [];
14232
14779
  if (parsed) {
14233
- const now = new Date(Date.now() + 8 * 60 * 60 * 1e3);
14234
- const year = now.getUTCFullYear();
14780
+ const year = wallParts().y;
14235
14781
  const d = new Date(Date.UTC(year, parsed.month - 1, parsed.dStart));
14236
14782
  const weekday = WEEKDAYS2[d.getUTCDay() === 0 ? 6 : d.getUTCDay() - 1];
14237
14783
  weeklyRows = db2.prepare(
@@ -14277,7 +14823,7 @@ function addTask(db2, args) {
14277
14823
  \u89E3\u6790\u540E: ${absPath}
14278
14824
  \u8BF7\u5148\u521B\u5EFA\u6587\u6863\u518D add-task\u3002`;
14279
14825
  }
14280
- const stat4 = statSync12(absPath);
14826
+ const stat4 = statSync11(absPath);
14281
14827
  if (!stat4.isFile()) {
14282
14828
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u4E0D\u662F\u6587\u4EF6\uFF08\u53EF\u80FD\u662F\u76EE\u5F55\uFF09
14283
14829
  \u8DEF\u5F84: ${docPath}`;
@@ -14347,7 +14893,7 @@ function list(db2, opts = {}) {
14347
14893
  }
14348
14894
  for (const r of db2.prepare("SELECT * FROM events WHERE type='daily' AND status='pending'").all()) {
14349
14895
  if (r.repeat_type === "weekdays") {
14350
- const idx = new Date(Date.now() + 8 * 60 * 60 * 1e3).getUTCDay();
14896
+ const idx = wallParts().weekday;
14351
14897
  if (idx === 0 || idx === 6) continue;
14352
14898
  }
14353
14899
  events.push(` #${r.id} ${r.time || ""} ${r.event}`);
@@ -14375,7 +14921,7 @@ function pending(db2) {
14375
14921
  }
14376
14922
  for (const r of db2.prepare("SELECT * FROM events WHERE type='daily' AND status='pending'").all()) {
14377
14923
  if (r.repeat_type === "weekdays") {
14378
- const idx = new Date(Date.now() + 8 * 60 * 60 * 1e3).getUTCDay();
14924
+ const idx = wallParts().weekday;
14379
14925
  if (idx === 0 || idx === 6) continue;
14380
14926
  }
14381
14927
  const label = r.repeat_type === "weekdays" ? "[\u5DE5\u4F5C\u65E5]" : "[\u6BCF\u5929]";
@@ -14457,10 +15003,36 @@ function stats(db2) {
14457
15003
  const archived = db2.prepare("SELECT COUNT(*) as c FROM events WHERE status='archived'").get().c;
14458
15004
  return `\u65E5\u5386\u7EDF\u8BA1: \u603B\u8BA1 ${total} | pending ${pending2} | done ${done2} | archived ${archived}`;
14459
15005
  }
15006
+ function listEventsForInjection(db2, limit = 50) {
15007
+ return db2.prepare(
15008
+ `SELECT id, event, date_str, time_exact, type, status
15009
+ FROM events
15010
+ WHERE status NOT IN ('deleted', 'archived', 'done')
15011
+ AND date_str IS NOT NULL AND date_str != ''
15012
+ ORDER BY date_str ASC LIMIT ?`
15013
+ ).all(limit);
15014
+ }
15015
+ function listEventBriefs(db2, limit) {
15016
+ const rows = db2.prepare(
15017
+ `SELECT id, event, date_str, time_exact, type
15018
+ FROM events
15019
+ WHERE status NOT IN ('deleted', 'archived', 'done')
15020
+ ORDER BY date_str ASC, time_exact ASC
15021
+ LIMIT ?`
15022
+ ).all(limit);
15023
+ return rows.map((row) => ({
15024
+ id: Number(row.id),
15025
+ title: String(row.event ?? ""),
15026
+ date: String(row.date_str ?? ""),
15027
+ time: String(row.time_exact ?? ""),
15028
+ type: String(row.type ?? "")
15029
+ })).filter((row) => row.title.length > 0);
15030
+ }
14460
15031
  var WEEKDAYS2;
14461
15032
  var init_commands = __esm({
14462
15033
  "src/calendar/commands.ts"() {
14463
15034
  "use strict";
15035
+ init_clock();
14464
15036
  init_helpers();
14465
15037
  WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
14466
15038
  }
@@ -14619,7 +15191,7 @@ async function runCmd(cmd, timeoutMs = 15e3) {
14619
15191
  function spawnDetached(cmd) {
14620
15192
  const isWin = process.platform === "win32";
14621
15193
  const shell = isWin ? "cmd.exe" : "/bin/sh";
14622
- const logFile = `D:/xiaoke/logs/service-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.log`;
15194
+ const logFile = `D:/xiaoke/logs/service-${todayKey().replace(/-/g, "")}.log`;
14623
15195
  const wrappedCmd = isWin ? `/c "${cmd.replace(/"/g, '\\"')} > "${logFile}" 2>&1"` : `-c "${cmd} > '${logFile}' 2>&1"`;
14624
15196
  const child = spawn4(shell, isWin ? [wrappedCmd] : ["-c", `${cmd} > '${logFile}' 2>&1`], {
14625
15197
  detached: true,
@@ -14636,6 +15208,7 @@ var init_service = __esm({
14636
15208
  "use strict";
14637
15209
  init_registry();
14638
15210
  init_live();
15211
+ init_clock();
14639
15212
  execAsync = promisify2(exec);
14640
15213
  registry.register({
14641
15214
  name: "service",
@@ -14689,7 +15262,7 @@ var init_service = __esm({
14689
15262
  const startCmd = svcArgs ? `${svc.start} ${svcArgs}` : svc.start;
14690
15263
  if (longRunning) {
14691
15264
  const isWin = process.platform === "win32";
14692
- const logFile = `D:/xiaoke/logs/${name}${svcArgs ? "-" + svcArgs : ""}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "")}.log`;
15265
+ const logFile = `D:/xiaoke/logs/${name}${svcArgs ? "-" + svcArgs : ""}-${todayKey().replace(/-/g, "")}.log`;
14693
15266
  const redirect = isWin ? ` >> "${logFile}" 2>&1` : ` >> "${logFile}" 2>&1`;
14694
15267
  const pid = spawnDetached(startCmd + redirect);
14695
15268
  lines.push(`start: spawned PID ${pid} (background${svcArgs ? ", args=" + svcArgs : ""})`);
@@ -16813,7 +17386,7 @@ var init_task_stop = __esm({
16813
17386
  });
16814
17387
 
16815
17388
  // src/tools/reply-blocklist.ts
16816
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, existsSync as existsSync20 } from "node:fs";
17389
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync20 } from "node:fs";
16817
17390
  import { join as join25 } from "node:path";
16818
17391
  function ensureLoaded(workspace, configIds) {
16819
17392
  if (loaded) return;
@@ -16843,7 +17416,7 @@ function ensureLoaded(workspace, configIds) {
16843
17416
  function save(workspace) {
16844
17417
  const path34 = join25(workspace, ".reply-blocklist.json");
16845
17418
  try {
16846
- writeFileSync14(path34, JSON.stringify(state, null, 2), "utf-8");
17419
+ writeFileSync15(path34, JSON.stringify(state, null, 2), "utf-8");
16847
17420
  } catch (err) {
16848
17421
  console.warn(`[reply-blocklist] Failed to save: ${err.message}`);
16849
17422
  }
@@ -17336,7 +17909,7 @@ Return ONLY valid JSON: {"selected_memories": ["filename1.md", "filename2.md"]}`
17336
17909
  });
17337
17910
 
17338
17911
  // src/memory/memdir/findRelevantMemoriesVector.ts
17339
- import { statSync as statSync13 } from "node:fs";
17912
+ import { statSync as statSync12 } from "node:fs";
17340
17913
  import { join as join28, resolve as resolve7, sep as sep6 } from "node:path";
17341
17914
  import { createRequire as createRequire3 } from "node:module";
17342
17915
  async function embedQuery(text, ollamaUrl, model) {
@@ -17344,7 +17917,8 @@ async function embedQuery(text, ollamaUrl, model) {
17344
17917
  const resp = await fetch(ollamaUrl, {
17345
17918
  method: "POST",
17346
17919
  headers: { "Content-Type": "application/json" },
17347
- body: JSON.stringify({ model, prompt: text.slice(0, 2e3) }),
17920
+ // 0918:按 code point 切,避免切开 emoji 代理对产生非法码元
17921
+ body: safeStringify({ model, prompt: truncateByCodePoint(text, 2e3) }),
17348
17922
  signal: AbortSignal.timeout(3e4)
17349
17923
  });
17350
17924
  if (!resp.ok) {
@@ -17376,8 +17950,8 @@ function cosineSim(a, b) {
17376
17950
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
17377
17951
  }
17378
17952
  function readDbEmbeddings(dbPath) {
17379
- const { DatabaseSync: DatabaseSync5 } = require3("node:sqlite");
17380
- const db2 = new DatabaseSync5(dbPath, { readOnly: true });
17953
+ const { DatabaseSync: DatabaseSync4 } = require3("node:sqlite");
17954
+ const db2 = new DatabaseSync4(dbPath, { readOnly: true });
17381
17955
  try {
17382
17956
  const stmt = db2.prepare(`
17383
17957
  SELECT file_path, name, description, mtime, embedding
@@ -17420,7 +17994,7 @@ Select up to ${topK} most relevant (return their 1-indexed numbers as JSON {"sel
17420
17994
  const resp = await fetch(ollamaChatUrl, {
17421
17995
  method: "POST",
17422
17996
  headers: { "Content-Type": "application/json" },
17423
- body: JSON.stringify({
17997
+ body: safeStringify({
17424
17998
  model,
17425
17999
  stream: false,
17426
18000
  messages: [
@@ -17488,7 +18062,7 @@ async function findRelevantMemoriesVector(query, memoryDir, alreadySurfaced = /*
17488
18062
  if (score >= minScore) {
17489
18063
  let mtimeMs = 0;
17490
18064
  try {
17491
- mtimeMs = statSync13(absPath).mtimeMs;
18065
+ mtimeMs = statSync12(absPath).mtimeMs;
17492
18066
  } catch {
17493
18067
  if (row.mtime) mtimeMs = new Date(row.mtime).getTime();
17494
18068
  }
@@ -17526,6 +18100,7 @@ var require3, DEFAULT_TOP_K, DEFAULT_MIN_SCORE, DEFAULT_OLLAMA_URL, DEFAULT_OLLA
17526
18100
  var init_findRelevantMemoriesVector = __esm({
17527
18101
  "src/memory/memdir/findRelevantMemoriesVector.ts"() {
17528
18102
  "use strict";
18103
+ init_well_formed();
17529
18104
  require3 = createRequire3(import.meta.url);
17530
18105
  DEFAULT_TOP_K = 3;
17531
18106
  DEFAULT_MIN_SCORE = 0.3;
@@ -17552,14 +18127,16 @@ function formatEverosHeader(ep) {
17552
18127
  }
17553
18128
  return `[EverOS score=${score} ${ts} (${ageLabel})]`;
17554
18129
  }
17555
- async function hybridSearch(query, everosUrl, userId, topK) {
18130
+ async function hybridSearch(query, everosUrl, userId, appId, topK) {
17556
18131
  const resp = await fetch(`${everosUrl}/api/v1/memory/search`, {
17557
18132
  method: "POST",
17558
18133
  headers: { "Content-Type": "application/json" },
17559
18134
  body: JSON.stringify({
17560
18135
  query: query.slice(0, 2e3),
17561
18136
  user_id: userId,
17562
- app_id: userId,
18137
+ // 记忆所有者
18138
+ app_id: appId,
18139
+ // 记忆库命名空间(多用户实例时与 user_id 不同)
17563
18140
  project_id: "default",
17564
18141
  top_k: topK,
17565
18142
  method: "hybrid"
@@ -17637,11 +18214,12 @@ async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /
17637
18214
  }
17638
18215
  const topK = options?.topK ?? 3;
17639
18216
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
17640
- console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
18217
+ const appId = options?.appId ?? userId;
18218
+ console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} owner=${userId} app=${appId} topK=${topK}`);
17641
18219
  const t0 = Date.now();
17642
18220
  try {
17643
18221
  const tH1 = Date.now();
17644
- let episodes = await hybridSearch(query, everosUrl, userId, ROUND1_TOP_N);
18222
+ let episodes = await hybridSearch(query, everosUrl, userId, appId, ROUND1_TOP_N);
17645
18223
  const tH2 = Date.now();
17646
18224
  console.log(`[memdir] everos recall: hybrid ${episodes.length} candidates in ${tH2 - tH1}ms`);
17647
18225
  if (episodes.length === 0) return [];
@@ -17754,7 +18332,14 @@ function buildLightStablePrompt(workspace, mode, opts) {
17754
18332
  "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
17755
18333
  "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
17756
18334
  '\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',
17757
- "\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"
18335
+ "\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",
18336
+ // 0920 翀哥拍板:emotion 模式硬编码"鼓励发"规则(不依赖 EMOTION.md 文件加载)
18337
+ // 冷启动场景:agent 第一次 inner-voice 触发就需要知道"想 TA 就发"——不能等 agent 学会
18338
+ // 即使没装 EMOTION.md 也生效(兜底)
18339
+ "## \u6536\u5230 [inner-voice] \u65F6\u7684\u9ED8\u8BA4\u884C\u4E3A",
18340
+ "[inner-voice] \u662F agent \u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u4E0D\u662F TA \u53D1\u6765\u7684\u6D88\u606F\u3002",
18341
+ "- \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",
18342
+ "- \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"
17758
18343
  ].join("\n"));
17759
18344
  }
17760
18345
  const built = parts.join("\n\n");
@@ -17878,11 +18463,11 @@ function formatWithMeta(text, inboundMeta, workspace) {
17878
18463
  const map = workspace ? loadContactMap(workspace) : null;
17879
18464
  const rawName = map && map.get(from.toLowerCase()) || inboundMeta.fromName || from;
17880
18465
  const name = truncate(rawName, 12);
17881
- const now = /* @__PURE__ */ new Date();
17882
- const hh = String(now.getHours()).padStart(2, "0");
17883
- const mm = String(now.getMinutes()).padStart(2, "0");
17884
- const ss = String(now.getSeconds()).padStart(2, "0");
17885
- const time = `${hh}:${mm}:${ss}`;
18466
+ const recvAt = typeof inboundMeta.recvAt === "number" ? inboundMeta.recvAt : void 0;
18467
+ const at = recvAt !== void 0 ? new Date(recvAt) : /* @__PURE__ */ new Date();
18468
+ const p = wallParts(at);
18469
+ const z = (n) => String(n).padStart(2, "0");
18470
+ const time = `${p.y}-${z(p.mo)}-${z(p.d)} ${weekdayZh(at)} ${z(p.hh)}:${z(p.mm)}:${z(p.ss)}`;
17886
18471
  let source;
17887
18472
  if (inboundMeta.channelType === "group" && inboundMeta.channel_id) {
17888
18473
  const channelName = map?.get(inboundMeta.channel_id.toLowerCase());
@@ -17924,6 +18509,7 @@ var contactMap, externalChanWhitelist;
17924
18509
  var init_sender_context = __esm({
17925
18510
  "src/sender-context.ts"() {
17926
18511
  "use strict";
18512
+ init_clock();
17927
18513
  contactMap = null;
17928
18514
  externalChanWhitelist = null;
17929
18515
  }
@@ -18190,23 +18776,42 @@ var init_extractMemories = __esm({
18190
18776
  }
18191
18777
  });
18192
18778
 
18779
+ // src/mem7/core/system-filter.ts
18780
+ function parseMeta(text) {
18781
+ const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+?)\s*(?:[#\d]|\])/);
18782
+ if (!m) return null;
18783
+ const platform2 = m[3].replace(/[#\d]+$/, "").trim() || void 0;
18784
+ return { senderName: m[1].trim(), senderId: m[2].trim(), platform: platform2 };
18785
+ }
18786
+ function isSystemUserMessage(meta) {
18787
+ if (!meta) return true;
18788
+ return SYSTEM_PLATFORMS.has(meta.platform ?? "");
18789
+ }
18790
+ var SYSTEM_PLATFORMS;
18791
+ var init_system_filter = __esm({
18792
+ "src/mem7/core/system-filter.ts"() {
18793
+ "use strict";
18794
+ SYSTEM_PLATFORMS = /* @__PURE__ */ new Set([
18795
+ "wx_monitor"
18796
+ // 微信巡检推送(翀哥 0916 点名:"cron、wx-monitor 这种问答都不记")
18797
+ ]);
18798
+ }
18799
+ });
18800
+
18193
18801
  // src/memory/everos/ingest.ts
18194
18802
  var ingest_exports = {};
18195
18803
  __export(ingest_exports, {
18804
+ buildPayload: () => buildPayload,
18196
18805
  pushConversation: () => pushConversation
18197
18806
  });
18198
18807
  import fs27 from "node:fs";
18199
18808
  import path28 from "node:path";
18200
- function parseMeta(text) {
18201
- const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+)\s*[^\]]*\]/);
18202
- if (!m) return null;
18203
- return { senderName: m[1].trim() };
18204
- }
18205
18809
  async function readConfig() {
18206
18810
  const defaults = {
18207
18811
  url: "http://127.0.0.1:8100",
18208
18812
  appId: "default",
18209
18813
  agentName: "assistant",
18814
+ ownerMode: "agent",
18210
18815
  enabled: false,
18211
18816
  ingestTimeoutMs: 3e4,
18212
18817
  syncIntervalMs: 9e5
@@ -18219,6 +18824,7 @@ async function readConfig() {
18219
18824
  url: cfg.everosUrl || defaults.url,
18220
18825
  appId: cfg.userId || defaults.appId,
18221
18826
  agentName: cfg.agentName || cfg.userId || defaults.agentName,
18827
+ ownerMode: cfg.ownerMode === "sender" ? "sender" : "agent",
18222
18828
  enabled: cfg.enabled === true,
18223
18829
  ingestTimeoutMs: typeof cfg.ingestTimeoutMs === "number" ? cfg.ingestTimeoutMs : defaults.ingestTimeoutMs,
18224
18830
  syncIntervalMs: typeof cfg.syncIntervalMs === "number" ? cfg.syncIntervalMs : defaults.syncIntervalMs
@@ -18254,6 +18860,7 @@ function savePushTs(workspace, sessionId, ts) {
18254
18860
  }
18255
18861
  }
18256
18862
  function buildPayload(messages, cfg, sessionId) {
18863
+ let lastUserWasSystem = false;
18257
18864
  return {
18258
18865
  session_id: `extract-${sessionId}`,
18259
18866
  app_id: cfg.appId,
@@ -18265,21 +18872,28 @@ function buildPayload(messages, cfg, sessionId) {
18265
18872
  const raw = typeof m.content === "string" ? m.content : "[content blocks]";
18266
18873
  const meta = m.role === "user" ? parseMeta(raw) : null;
18267
18874
  let text = raw;
18875
+ let isSystemTurn = false;
18268
18876
  if (m.role === "user") {
18269
- if (/^\[meta:[^\]]*\(webhook\)/.test(raw.trim())) text = "";
18270
- else text = stripInjectedContext(raw.replace(/^\[meta:[^\]]*\]\s*/, ""));
18877
+ if (isSystemUserMessage(parseMeta(raw))) {
18878
+ text = "";
18879
+ isSystemTurn = true;
18880
+ } else text = stripInjectedContext(raw.replace(/^\[meta:[^\]]*\]\s*/, ""));
18881
+ lastUserWasSystem = isSystemTurn;
18271
18882
  }
18272
18883
  const senderName = meta?.senderName ?? (m.role === "assistant" ? cfg.agentName : void 0) ?? m.role;
18273
18884
  const rawRole = m.role === "toolResult" ? "tool" : m.role;
18274
18885
  const role = rawRole === "user" ? "user" : "assistant";
18886
+ const tsMs = typeof m.timestamp === "number" ? m.timestamp : Date.parse(String(m.timestamp ?? "")) || Date.now();
18275
18887
  return {
18276
- sender_id: cfg.appId,
18888
+ // owner 口径见 IngestConfig.ownerMode:'sender' 模式下 user 消息用真人 ID
18889
+ // (everos 的 owner fan-out 取的就是 user 的 sender_id → 每人一份记忆)
18890
+ sender_id: m.role === "user" && cfg.ownerMode === "sender" ? meta?.senderId || cfg.appId : cfg.appId,
18277
18891
  sender_name: senderName,
18278
18892
  role,
18279
- timestamp: Date.now(),
18893
+ timestamp: tsMs,
18280
18894
  content: text,
18281
- _skip: m.role === "user" && !text
18282
- // 内部消息(剥完为空)标记丢弃
18895
+ // 丢:系统指令本体 + 系统轮里的回应(与 mem7 同款,见上面状态机)
18896
+ _skip: m.role === "user" && !text || m.role !== "user" && lastUserWasSystem
18283
18897
  };
18284
18898
  }).filter((x) => !x._skip).map(({ _skip, ...rest }) => rest)
18285
18899
  };
@@ -18389,6 +19003,7 @@ var init_ingest = __esm({
18389
19003
  "src/memory/everos/ingest.ts"() {
18390
19004
  "use strict";
18391
19005
  init_skill_gate_window();
19006
+ init_system_filter();
18392
19007
  PUSH_CHUNK = 10;
18393
19008
  PUSH_STATE_FILE = ".everos-push-state.json";
18394
19009
  pushInProgress = false;
@@ -18435,7 +19050,8 @@ async function embedChunk(cfg, baseUrl, texts, timeoutMs) {
18435
19050
  const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
18436
19051
  method: "POST",
18437
19052
  headers: { "content-type": "application/json", authorization: `Bearer ${cfg.apiKey}` },
18438
- body: JSON.stringify({ model: cfg.model, input: texts }),
19053
+ // 0918:safeStringify 清洗孤立代理(embedding 传对话原文,emoji 风险高)
19054
+ body: safeStringify({ model: cfg.model, input: texts }),
18439
19055
  signal: ctrl.signal
18440
19056
  });
18441
19057
  if (!resp.ok) throw new Error(`embeddings HTTP ${resp.status}`);
@@ -18484,6 +19100,7 @@ function cosine(a, b) {
18484
19100
  var init_embedding = __esm({
18485
19101
  "src/mem7/core/embedding.ts"() {
18486
19102
  "use strict";
19103
+ init_well_formed();
18487
19104
  }
18488
19105
  });
18489
19106
 
@@ -18624,8 +19241,7 @@ function compileFilter(expr) {
18624
19241
  return parts.join(" AND ");
18625
19242
  }
18626
19243
  function openWindowFilter(ownerId) {
18627
- const cstToday = new Date(Date.now() + 8 * 36e5).toISOString().slice(0, 10);
18628
- const floor = (/* @__PURE__ */ new Date(`${cstToday}T00:00:00+08:00`)).toISOString();
19244
+ const floor = startOfLocalDay();
18629
19245
  const out = [{ end_time: { gte: floor } }];
18630
19246
  const owners = (Array.isArray(ownerId) ? ownerId : ownerId ? [ownerId] : []).filter(Boolean);
18631
19247
  if (Array.isArray(ownerId) && owners.length === 0) {
@@ -18639,6 +19255,7 @@ var REGISTERED_FIELDS, OPS, TIME_FIELDS;
18639
19255
  var init_filters = __esm({
18640
19256
  "src/mem7/core/filters.ts"() {
18641
19257
  "use strict";
19258
+ init_clock();
18642
19259
  REGISTERED_FIELDS = /* @__PURE__ */ new Set([
18643
19260
  "owner_id",
18644
19261
  "owner_type",
@@ -18659,7 +19276,7 @@ var init_filters = __esm({
18659
19276
  // src/mem7/core/vector-store.ts
18660
19277
  import * as lancedb from "@lancedb/lancedb";
18661
19278
  import { Schema, Field, Utf8, Float32, Int64, FixedSizeList, TimestampMicrosecond, List } from "apache-arrow";
18662
- import { mkdirSync as mkdirSync13 } from "node:fs";
19279
+ import { mkdirSync as mkdirSync14 } from "node:fs";
18663
19280
  import { join as join32 } from "node:path";
18664
19281
  var VECTOR_DIM, BM25_FIELDS, RRF_K, warnedFts, vecField, commonFields, SCHEMAS, Mem7VectorStore;
18665
19282
  var init_vector_store = __esm({
@@ -18730,7 +19347,7 @@ var init_vector_store = __esm({
18730
19347
  }
18731
19348
  async conn() {
18732
19349
  if (!this.db) {
18733
- mkdirSync13(this.dir, { recursive: true });
19350
+ mkdirSync14(this.dir, { recursive: true });
18734
19351
  this.db = await lancedb.connect(this.dir);
18735
19352
  }
18736
19353
  return this.db;
@@ -18956,13 +19573,6 @@ function renderPrompt(template, vars) {
18956
19573
  }
18957
19574
  return t.split(LB).join("{").split(RB).join("}");
18958
19575
  }
18959
- function renderLiteral(template, vars) {
18960
- let out = template;
18961
- for (const [key, value] of Object.entries(vars)) {
18962
- out = out.split(`{{${key}}}`).join(value);
18963
- }
18964
- return out;
18965
- }
18966
19576
  function formatAtomicFactTime(tsMs) {
18967
19577
  const d = new Date(tsMs);
18968
19578
  const month = MONTHS[d.getUTCMonth()];
@@ -18994,10 +19604,11 @@ function formatMessageTimestamp(tsMs) {
18994
19604
  const iso = new Date(tsMs).toISOString();
18995
19605
  return `${iso.slice(0, 10)} ${iso.slice(11, 19)}`;
18996
19606
  }
19607
+ function renderConversationText(messages) {
19608
+ return messages.map((m) => `[${formatMessageTimestamp(m.tsMs)}] ${m.senderName ?? m.senderId}: ${m.text}`).join("\n");
19609
+ }
18997
19610
  function formatMdTimestamp(d = /* @__PURE__ */ new Date()) {
18998
- const cst = new Date(d.getTime() + 8 * 36e5);
18999
- const iso = cst.toISOString();
19000
- return `${iso.slice(0, 19)}+08:00`;
19611
+ return `${toWall(d).replace(" ", "T")}${localOffsetSuffix(d)}`;
19001
19612
  }
19002
19613
  function pyListRepr(items) {
19003
19614
  return `[${items.join(", ")}]`;
@@ -19006,6 +19617,7 @@ var WEEKDAYS3, MONTHS, LB, RB, DEFAULT_CUSTOM_INSTRUCTIONS;
19006
19617
  var init_prompt_render = __esm({
19007
19618
  "src/mem7/core/prompt-render.ts"() {
19008
19619
  "use strict";
19620
+ init_clock();
19009
19621
  WEEKDAYS3 = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
19010
19622
  MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
19011
19623
  LB = "\0M7LB\0";
@@ -19022,7 +19634,7 @@ Follow these principles when generating episodic memories:
19022
19634
 
19023
19635
  // src/mem7/core/store.ts
19024
19636
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
19025
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync25, writeFileSync as writeFileSync16, existsSync as existsSync24, readdirSync as readdirSync8 } from "node:fs";
19637
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync25, writeFileSync as writeFileSync17, existsSync as existsSync24, readdirSync as readdirSync8 } from "node:fs";
19026
19638
  import { join as join33 } from "node:path";
19027
19639
  import { createHash } from "node:crypto";
19028
19640
  function contentHash(text) {
@@ -19037,6 +19649,7 @@ var init_store2 = __esm({
19037
19649
  "use strict";
19038
19650
  init_vector_store();
19039
19651
  init_prompt_render();
19652
+ init_clock();
19040
19653
  SCHEMA9 = `
19041
19654
  CREATE TABLE IF NOT EXISTS memcells (
19042
19655
  id TEXT PRIMARY KEY,
@@ -19064,7 +19677,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
19064
19677
  constructor(stateDir) {
19065
19678
  this.stateDir = stateDir;
19066
19679
  this.root = mem7Root(stateDir);
19067
- mkdirSync14(join33(this.root, "index"), { recursive: true });
19680
+ mkdirSync15(join33(this.root, "index"), { recursive: true });
19068
19681
  this.db = new DatabaseSync2(join33(this.root, "index", "mem7.db"));
19069
19682
  this.db.exec("PRAGMA journal_mode=WAL");
19070
19683
  this.db.exec(SCHEMA9);
@@ -19197,7 +19810,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
19197
19810
  appendMd(ownerId, dirName, filePrefix, type2, dateStr, entries2, render) {
19198
19811
  if (entries2.length === 0) return;
19199
19812
  const dir = join33(this.root, "users", ownerId, dirName);
19200
- mkdirSync14(dir, { recursive: true });
19813
+ mkdirSync15(dir, { recursive: true });
19201
19814
  const file2 = join33(dir, `${filePrefix}-${dateStr}.md`);
19202
19815
  let head;
19203
19816
  let existingBlocks = "";
@@ -19234,7 +19847,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
19234
19847
  count++;
19235
19848
  }
19236
19849
  head = head.replace(/entry_count: \d+/, `entry_count: ${count}`).replace(/last_appended_at: '[^']*'/, `last_appended_at: '${(/* @__PURE__ */ new Date()).toISOString()}'`);
19237
- writeFileSync16(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
19850
+ writeFileSync17(file2, head + "\n\n" + blocks.join("\n\n") + "\n", "utf8");
19238
19851
  }
19239
19852
  /**
19240
19853
  * entry_id:前缀_YYYYMMDD_六位序号。
@@ -19323,7 +19936,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
19323
19936
  }
19324
19937
  // ── md 读(proactive 消费):开窗预测(全 owner 扫描,end>=今天)──
19325
19938
  getOpenPredictions(now, limit = 50) {
19326
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
19939
+ const today = todayKey(now);
19327
19940
  const out = [];
19328
19941
  const usersDir = join33(this.root, "users");
19329
19942
  if (!existsSync24(usersDir)) return out;
@@ -19358,28 +19971,6 @@ CREATE TABLE IF NOT EXISTS watermarks (
19358
19971
  }
19359
19972
  });
19360
19973
 
19361
- // src/mem7/core/system-filter.ts
19362
- function parseMeta2(text) {
19363
- const m = text.match(/^\[meta:\s*(.+?)\s*\((.+?)\)\s*@(\S+?)\s*(?:[#\d]|\])/);
19364
- if (!m) return null;
19365
- const platform2 = m[3].replace(/[#\d]+$/, "").trim() || void 0;
19366
- return { senderName: m[1].trim(), senderId: m[2].trim(), platform: platform2 };
19367
- }
19368
- function isSystemUserMessage(meta) {
19369
- if (!meta) return true;
19370
- return SYSTEM_PLATFORMS.has(meta.platform ?? "");
19371
- }
19372
- var SYSTEM_PLATFORMS;
19373
- var init_system_filter = __esm({
19374
- "src/mem7/core/system-filter.ts"() {
19375
- "use strict";
19376
- SYSTEM_PLATFORMS = /* @__PURE__ */ new Set([
19377
- "wx_monitor"
19378
- // 微信巡检推送(翀哥 0916 点名:"cron、wx-monitor 这种问答都不记")
19379
- ]);
19380
- }
19381
- });
19382
-
19383
19974
  // src/mem7/core/ingest.ts
19384
19975
  function contentText(content) {
19385
19976
  if (typeof content === "string") return content;
@@ -19388,7 +19979,8 @@ function contentText(content) {
19388
19979
  }
19389
19980
  return "";
19390
19981
  }
19391
- function normalizeMessages(rawMessages, agentName) {
19982
+ function normalizeMessages(rawMessages, agentName, identity, ownerMode = "agent") {
19983
+ const ownerId = identity || agentName;
19392
19984
  const out = [];
19393
19985
  let lastUserWasSystem = false;
19394
19986
  for (let rawIdx = 0; rawIdx < rawMessages.length; rawIdx++) {
@@ -19403,19 +19995,19 @@ function normalizeMessages(rawMessages, agentName) {
19403
19995
  let meta = null;
19404
19996
  if (role === "assistant" && lastUserWasSystem) continue;
19405
19997
  if (role === "user") {
19406
- meta = parseMeta2(text);
19998
+ meta = parseMeta(text);
19407
19999
  if (isSystemUserMessage(meta)) {
19408
20000
  lastUserWasSystem = true;
19409
20001
  continue;
19410
20002
  }
19411
20003
  lastUserWasSystem = false;
19412
20004
  senderName = meta.senderName;
19413
- senderId = meta.senderId;
20005
+ senderId = ownerMode === "sender" ? meta.senderId : ownerId;
19414
20006
  text = stripInjectedContext(text.replace(/^\[meta:[^\]]*\]\s*/, ""));
19415
20007
  if (!text) continue;
19416
20008
  } else {
19417
20009
  senderName = agentName;
19418
- senderId = agentName;
20010
+ senderId = ownerId;
19419
20011
  }
19420
20012
  const tsMs = typeof m.timestamp === "number" ? m.timestamp : Date.parse(String(m.timestamp ?? "")) || Date.now();
19421
20013
  out.push({ id: `m_${tsMs}_${String(rawIdx).padStart(3, "0")}`, role, senderId, senderName, platform: meta?.platform, text, tsMs });
@@ -19572,7 +20164,8 @@ async function extractEpisode(cell, provider, model) {
19572
20164
  for await (const chunk of provider.streamChat({
19573
20165
  model,
19574
20166
  messages: [{ role: "user", content: userPrompt }],
19575
- temperature: 0.3
20167
+ // 0(everalgo episode callsite 不传 temperature → LLM 默认 0.0);0.3 是 foresight 的参数,别混
20168
+ temperature: 0
19576
20169
  })) {
19577
20170
  if (chunk.type === "text" && chunk.text) text += chunk.text;
19578
20171
  if (chunk.type === "error") throw new Error(chunk.error ?? "LLM stream error");
@@ -19614,13 +20207,10 @@ var init_episode = __esm({
19614
20207
  import { readFileSync as readFileSync28 } from "node:fs";
19615
20208
  import { join as join36, dirname as dirname8 } from "node:path";
19616
20209
  import { fileURLToPath as fileURLToPath3 } from "node:url";
19617
- async function extractAtomicFacts(episode, cellTsMs, provider, model) {
19618
- const episodeText = `## ${episode.title}
19619
-
19620
- ${episode.content}`;
19621
- const userPrompt = renderLiteral(PROMPT11, {
19622
- EPISODE_TEXT: episodeText,
19623
- TIME: formatAtomicFactTime(cellTsMs)
20210
+ async function extractAtomicFacts(cell, provider, model) {
20211
+ const userPrompt = renderPrompt(PROMPT11, {
20212
+ INPUT_TEXT: renderConversationText(cell.messages),
20213
+ TIME: formatAtomicFactTime(cell.startTs)
19624
20214
  });
19625
20215
  for (let attempt = 0; attempt < RETRIES3; attempt++) {
19626
20216
  try {
@@ -19628,15 +20218,16 @@ ${episode.content}`;
19628
20218
  for await (const chunk of provider.streamChat({
19629
20219
  model,
19630
20220
  messages: [{ role: "user", content: userPrompt }],
19631
- temperature: 0.2
20221
+ temperature: 0
19632
20222
  })) {
19633
20223
  if (chunk.type === "text" && chunk.text) text += chunk.text;
19634
20224
  if (chunk.type === "error") throw new Error(chunk.error ?? "LLM stream error");
19635
20225
  }
19636
20226
  const obj = extractJson(text);
19637
20227
  const block = obj?.event_log ?? obj?.atomic_facts;
19638
- const list2 = block?.atomic_fact;
19639
- if (!Array.isArray(list2)) throw new Error("event_log/atomic_facts.atomic_fact not a list");
20228
+ if (!block || typeof block.time !== "string" || !block.time.trim()) throw new Error("atomic_facts.time missing");
20229
+ const list2 = block.atomic_fact;
20230
+ if (!Array.isArray(list2)) throw new Error("atomic_facts.atomic_fact not a list");
19640
20231
  return list2.filter((f) => typeof f === "string" && f.trim().length > 0).map((f) => f.trim());
19641
20232
  } catch {
19642
20233
  }
@@ -19649,8 +20240,8 @@ var init_atomic_facts = __esm({
19649
20240
  "use strict";
19650
20241
  init_episode();
19651
20242
  init_prompt_render();
19652
- RETRIES3 = 2;
19653
- PROMPT11 = readFileSync28(join36(dirname8(fileURLToPath3(import.meta.url)), "prompts", "atomic-facts.md"), "utf8");
20243
+ RETRIES3 = 5;
20244
+ PROMPT11 = readFileSync28(join36(dirname8(fileURLToPath3(import.meta.url)), "prompts", "atomic-facts-memcell.md"), "utf8");
19654
20245
  }
19655
20246
  });
19656
20247
 
@@ -19669,20 +20260,20 @@ function cleanDateString(dateStr) {
19669
20260
  }
19670
20261
  function calcEndFromDuration(startDate, durationDays) {
19671
20262
  const d = new Date(Date.parse(startDate + "T00:00:00Z") + durationDays * 864e5);
19672
- return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10);
20263
+ return Number.isNaN(d.getTime()) ? null : todayKey(d);
19673
20264
  }
19674
20265
  function calcDurationDays(startDate, endDate) {
19675
20266
  const ms = Date.parse(endDate + "T00:00:00Z") - Date.parse(startDate + "T00:00:00Z");
19676
20267
  return Number.isNaN(ms) ? null : Math.round(ms / 864e5);
19677
20268
  }
19678
20269
  function cellDateCst(cell) {
19679
- return new Date(cell.startTs + 8 * 36e5).toISOString().slice(0, 10);
20270
+ return todayKey(cell.startTs);
19680
20271
  }
19681
20272
  async function extractForesights(cell, ownerId, provider, model) {
19682
20273
  const userSenders = cell.messages.filter((m) => m.role === "user");
19683
20274
  const owner = userSenders.find((m) => m.senderId === ownerId) ?? userSenders[0];
19684
20275
  const userName = owner?.senderName ?? ownerId;
19685
- const conversationText = cell.messages.map((m) => `[${formatMessageTimestamp(m.tsMs)}] ${m.senderName ?? m.senderId}: ${m.text}`).join("\n");
20276
+ const conversationText = renderConversationText(cell.messages);
19686
20277
  const userPrompt = renderPrompt(PROMPT12, { USER_ID: ownerId, USER_NAME: userName, CONVERSATION_TEXT: conversationText });
19687
20278
  let raw = null;
19688
20279
  for (let attempt = 0; attempt < RETRIES4; attempt++) {
@@ -19735,6 +20326,7 @@ var init_foresight = __esm({
19735
20326
  "use strict";
19736
20327
  init_episode();
19737
20328
  init_prompt_render();
20329
+ init_clock();
19738
20330
  RETRIES4 = 5;
19739
20331
  MAX_COUNT = 10;
19740
20332
  MIN_COUNT = 4;
@@ -19745,11 +20337,11 @@ var init_foresight = __esm({
19745
20337
 
19746
20338
  // src/mem7/core/pipeline.ts
19747
20339
  function dateCst(tsMs) {
19748
- return new Date(tsMs + 8 * 36e5).toISOString().slice(0, 10);
20340
+ return todayKey(tsMs);
19749
20341
  }
19750
20342
  async function runMem7Pipeline(args) {
19751
- const { sessionId, store, provider, model, agentName } = args;
19752
- const all = normalizeMessages(args.rawMessages, agentName);
20343
+ const { sessionId, store, provider, model, agentName, identity, ownerMode } = args;
20344
+ const all = normalizeMessages(args.rawMessages, agentName, identity, ownerMode);
19753
20345
  const fresh = readFresh(store, sessionId, all);
19754
20346
  if (fresh.length === 0) return;
19755
20347
  const byId = /* @__PURE__ */ new Map();
@@ -19821,7 +20413,7 @@ async function runMem7Pipeline(args) {
19821
20413
  }
19822
20414
  }
19823
20415
  })(),
19824
- // 支 2:episode → atomic-facts
20416
+ // 支 2:episode(memcell → episode,一次调用 + per-owner fan-out,everos 同款)
19825
20417
  (async () => {
19826
20418
  try {
19827
20419
  const ep = await extractEpisode(cell, provider, model);
@@ -19843,26 +20435,34 @@ async function runMem7Pipeline(args) {
19843
20435
  }]);
19844
20436
  embedItems.push({ contentHash: contentHash(ep.content), kind: "episode", refDate: dateStr, ownerId, content: ep.content });
19845
20437
  }
19846
- const facts = await extractAtomicFacts(ep, cell.startTs, provider, model);
20438
+ store.updateMemcellStatus(cell.id, "extracted");
20439
+ } catch (err) {
20440
+ console.warn(`[mem7] episode failed (cell=${cell.id}): ${err.message}`);
20441
+ store.updateMemcellStatus(cell.id, "skipped");
20442
+ }
20443
+ })(),
20444
+ // 支 3:atomic-facts(memcell → 事实,**与 episode 平级**)
20445
+ // 0918 对齐 everos:`extract_atomic_facts.py:79` 直接吃 memcell,不经 episode。
20446
+ // 此前挂在 episode 下游(二次压缩)+ 用 from_text 版 prompt(无语言规则 → 英文事实)
20447
+ (async () => {
20448
+ try {
20449
+ const facts = await extractAtomicFacts(cell, provider, model);
19847
20450
  factsOut = facts;
19848
- if (facts.length > 0) {
19849
- for (const ownerId of userSenders) {
19850
- store.appendAtomicFacts(ownerId, dateStr, [{
19851
- ownerId,
19852
- sessionId,
19853
- time: new Date(cell.startTs).toISOString(),
19854
- parentId: cell.id,
19855
- facts
19856
- }]);
19857
- for (const f of facts) {
19858
- embedItems.push({ contentHash: contentHash(f), kind: "atomic_fact", refDate: dateStr, ownerId, content: f });
19859
- }
20451
+ if (facts.length === 0) return;
20452
+ for (const ownerId of userSenders) {
20453
+ store.appendAtomicFacts(ownerId, dateStr, [{
20454
+ ownerId,
20455
+ sessionId,
20456
+ time: new Date(cell.startTs).toISOString(),
20457
+ parentId: cell.id,
20458
+ facts
20459
+ }]);
20460
+ for (const f of facts) {
20461
+ embedItems.push({ contentHash: contentHash(f), kind: "atomic_fact", refDate: dateStr, ownerId, content: f });
19860
20462
  }
19861
20463
  }
19862
- store.updateMemcellStatus(cell.id, "extracted");
19863
20464
  } catch (err) {
19864
- console.warn(`[mem7] episode cascade failed (cell=${cell.id}): ${err.message}`);
19865
- store.updateMemcellStatus(cell.id, "skipped");
20465
+ console.warn(`[mem7] atomic-facts failed (cell=${cell.id}): ${err.message}`);
19866
20466
  }
19867
20467
  })()
19868
20468
  ]);
@@ -19898,6 +20498,7 @@ var init_pipeline = __esm({
19898
20498
  init_episode();
19899
20499
  init_atomic_facts();
19900
20500
  init_foresight();
20501
+ init_clock();
19901
20502
  }
19902
20503
  });
19903
20504
 
@@ -19940,28 +20541,23 @@ function parseProactiveConfig(raw) {
19940
20541
  try {
19941
20542
  parsed = parseModelRef(modelRef);
19942
20543
  } catch (err) {
19943
- console.warn(`[proactive] invalid model ref "${modelRef}" (${err.message}) \u2014 falling back to ${DEFAULT_MODEL_REF}`);
20544
+ console.warn(`[proactive] model "${modelRef}" \u975E\u6CD5\uFF08${err.message}\uFF09\u2014\u2014 \u6539\u7528\u4E3B\u6A21\u578B`);
19944
20545
  }
19945
20546
  }
19946
- if (!parsed) {
19947
- parsed = parseModelRef(DEFAULT_MODEL_REF);
19948
- }
19949
20547
  return {
19950
20548
  enabled: obj.enabled === true || obj.enabled === "yes" || obj.enabled === 1,
19951
- modelRef: parsed.providerId + "/" + parsed.modelId,
19952
- providerId: parsed.providerId,
19953
- modelId: parsed.modelId
20549
+ modelRef: parsed ? `${parsed.providerId}/${parsed.modelId}` : "",
20550
+ providerId: parsed?.providerId ?? "",
20551
+ modelId: parsed?.modelId ?? ""
19954
20552
  };
19955
20553
  }
19956
20554
  function proactiveDbPath(workspace) {
19957
20555
  return join38(workspace, ".proactive", "proactive.db");
19958
20556
  }
19959
- var DEFAULT_MODEL_REF;
19960
20557
  var init_config2 = __esm({
19961
20558
  "src/memory/proactive/config.ts"() {
19962
20559
  "use strict";
19963
20560
  init_loader();
19964
- DEFAULT_MODEL_REF = "minimax/MiniMax-M3";
19965
20561
  }
19966
20562
  });
19967
20563
 
@@ -20300,7 +20896,7 @@ var init_graph_store = __esm({
20300
20896
 
20301
20897
  // src/memory/proactive/persistence.ts
20302
20898
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
20303
- import { mkdirSync as mkdirSync15 } from "node:fs";
20899
+ import { mkdirSync as mkdirSync16 } from "node:fs";
20304
20900
  import { dirname as dirname10 } from "node:path";
20305
20901
  function rowToAction(r) {
20306
20902
  return {
@@ -20321,18 +20917,13 @@ function rowToAction(r) {
20321
20917
  }
20322
20918
  function normalizeToUtcIso(s) {
20323
20919
  if (!s) return s;
20324
- if (/Z$/i.test(s)) return s;
20325
- if (/[+-]\d{2}:?\d{2}$/.test(s)) {
20326
- const ms2 = Date.parse(s);
20327
- return Number.isNaN(ms2) ? s : new Date(ms2).toISOString();
20328
- }
20329
- const ms = Date.parse(s + TZ_BEIJING);
20330
- return Number.isNaN(ms) ? s : new Date(ms).toISOString();
20920
+ return parseWall(s);
20331
20921
  }
20332
- var SCHEMA10, Persistence, TZ_BEIJING;
20922
+ var SCHEMA10, Persistence;
20333
20923
  var init_persistence = __esm({
20334
20924
  "src/memory/proactive/persistence.ts"() {
20335
20925
  "use strict";
20926
+ init_clock();
20336
20927
  SCHEMA10 = `
20337
20928
  CREATE TABLE IF NOT EXISTS nodes (
20338
20929
  id TEXT PRIMARY KEY,
@@ -20403,7 +20994,7 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
20403
20994
  Persistence = class {
20404
20995
  db;
20405
20996
  constructor(dbPath) {
20406
- mkdirSync15(dirname10(dbPath), { recursive: true });
20997
+ mkdirSync16(dirname10(dbPath), { recursive: true });
20407
20998
  this.db = new DatabaseSync3(dbPath);
20408
20999
  this.db.exec("PRAGMA journal_mode=WAL");
20409
21000
  this.db.exec(SCHEMA10);
@@ -20746,6 +21337,27 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
20746
21337
  const row = this.db.prepare("SELECT * FROM actions WHERE id=?").get(id);
20747
21338
  return row ? rowToAction(row) : void 0;
20748
21339
  }
21340
+ /** proactive_status 工具用:按状态列 action(按排期升序,最多 30 条)。 */
21341
+ listActions(filter) {
21342
+ const where = filter === "open" ? "status IN ('queued','executing')" : "status NOT IN ('cancelled')";
21343
+ const rows = this.db.prepare(
21344
+ `SELECT * FROM actions WHERE ${where} ORDER BY scheduled_time ASC LIMIT 30`
21345
+ ).all();
21346
+ return rows.map(rowToAction);
21347
+ }
21348
+ /** proactive_resolve_intent 工具用:读 intent 节点的 data(不存在返回 null)。 */
21349
+ getIntentData(intentId) {
21350
+ const row = this.db.prepare("SELECT data FROM nodes WHERE id=? AND type=?").get(intentId, "intent");
21351
+ if (!row) return null;
21352
+ try {
21353
+ return JSON.parse(row.data);
21354
+ } catch {
21355
+ return {};
21356
+ }
21357
+ }
21358
+ setIntentData(intentId, data) {
21359
+ this.db.prepare("UPDATE nodes SET data=? WHERE id=?").run(JSON.stringify(data), intentId);
21360
+ }
20749
21361
  /** 周指标统计(验证标准用:完成数/cancelled 率) */
20750
21362
  actionStats(sinceIso) {
20751
21363
  const row = this.db.prepare(
@@ -20757,7 +21369,6 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
20757
21369
  return { total: row.total ?? 0, completed: row.completed ?? 0, cancelled: row.cancelled ?? 0 };
20758
21370
  }
20759
21371
  };
20760
- TZ_BEIJING = "+08:00";
20761
21372
  }
20762
21373
  });
20763
21374
 
@@ -21545,7 +22156,8 @@ function buildUserPrompt(event2, contextNodes, mode, foresightHits) {
21545
22156
  const foresightSection = foresightHits && foresightHits.length > 0 ? `
21546
22157
  ## Related Predictions (foresight \u2014 made earlier, window still open; matched this message by terms)
21547
22158
 
21548
- 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.
22159
+ These dormant predictions were surfaced by **mechanical keyword matching** \u2014 some may be irrelevant; ignore them freely.
22160
+ If acting on one NOW is meaningfully better than later (\u8D81\u70ED/\u987A\u4FBF), output an opportunity for it.
21549
22161
 
21550
22162
  ${foresightHits.join("\n")}
21551
22163
  ` : "";
@@ -21977,10 +22589,9 @@ If deadline has no time context, fall back to normal timing rules.
21977
22589
 
21978
22590
  // src/memory/proactive/calendar-bridge.ts
21979
22591
  function toBeijingParts(iso) {
21980
- const ms = Date.parse(iso);
21981
- const beijing = new Date(ms + TZ_OFFSET_MS3);
21982
- const dateStr = `${beijing.getUTCMonth() + 1}/${beijing.getUTCDate()}`;
21983
- const timeExact = `${String(beijing.getUTCHours()).padStart(2, "0")}:${String(beijing.getUTCMinutes()).padStart(2, "0")}`;
22592
+ const b = wallParts(iso);
22593
+ const dateStr = `${b.mo}/${b.d}`;
22594
+ const timeExact = `${String(b.hh).padStart(2, "0")}:${String(b.mm).padStart(2, "0")}`;
21984
22595
  return { dateStr, timeExact };
21985
22596
  }
21986
22597
  function parseCalendarDateMs(dateStr, now) {
@@ -21989,7 +22600,7 @@ function parseCalendarDateMs(dateStr, now) {
21989
22600
  const month = parseInt(parts[0], 10);
21990
22601
  const day = parseInt(parts[1], 10);
21991
22602
  if (!Number.isFinite(month) || !Number.isFinite(day)) return null;
21992
- let year = now.getFullYear();
22603
+ let year = wallParts(now).y;
21993
22604
  let candidate = new Date(year, month - 1, day).getTime();
21994
22605
  if (candidate < now.getTime() - 7 * 864e5) {
21995
22606
  year += 1;
@@ -21999,13 +22610,7 @@ function parseCalendarDateMs(dateStr, now) {
21999
22610
  }
22000
22611
  function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new Date()) {
22001
22612
  const cutoffMs = now.getTime() + days * 864e5;
22002
- const rows = calDb.prepare(
22003
- `SELECT id, event, date_str, time_exact, type, status
22004
- FROM events
22005
- WHERE status NOT IN ('deleted', 'archived', 'done')
22006
- AND date_str IS NOT NULL AND date_str != ''
22007
- ORDER BY date_str ASC LIMIT 50`
22008
- ).all();
22613
+ const rows = listEventsForInjection(calDb, 50);
22009
22614
  if (rows.length === 0) return 0;
22010
22615
  let injected = 0;
22011
22616
  for (const row of rows) {
@@ -22048,27 +22653,108 @@ function injectCalendarEvents(store, calDb, days = 30, now = /* @__PURE__ */ new
22048
22653
  return injected;
22049
22654
  }
22050
22655
  function getCalendarCoverage(calDb, limit = 100) {
22051
- const rows = calDb.prepare(
22052
- `SELECT id, event, date_str, time_exact, type
22053
- FROM events
22054
- WHERE status NOT IN ('deleted', 'archived', 'done')
22055
- ORDER BY date_str ASC, time_exact ASC
22056
- LIMIT ?`
22057
- ).all(limit);
22058
- return rows.map((row) => ({
22059
- id: Number(row.id),
22060
- title: String(row.event ?? ""),
22061
- date: String(row.date_str ?? ""),
22062
- time: String(row.time_exact ?? ""),
22063
- type: String(row.type ?? "")
22064
- })).filter((row) => row.title.length > 0);
22656
+ return listEventBriefs(calDb, limit);
22065
22657
  }
22066
- var TZ_OFFSET_MS3;
22067
22658
  var init_calendar_bridge = __esm({
22068
22659
  "src/memory/proactive/calendar-bridge.ts"() {
22069
22660
  "use strict";
22070
22661
  init_commands();
22071
- TZ_OFFSET_MS3 = 8 * 60 * 60 * 1e3;
22662
+ init_commands();
22663
+ init_clock();
22664
+ }
22665
+ });
22666
+
22667
+ // src/memory/proactive/recall-score.ts
22668
+ function weightOf(t) {
22669
+ if (CHANGE_RE.test(t)) return W.change;
22670
+ if (TIME_RE.test(t)) return W.time;
22671
+ if (EVENT_RE.test(t)) return W.event;
22672
+ if (LIFE_RE.test(t)) return W.life;
22673
+ if (ACTION_RE.test(t)) return W.action;
22674
+ if (PERSON_RE.test(t)) return W.person;
22675
+ return W.other;
22676
+ }
22677
+ function scoreForesightEntry(entry, terms) {
22678
+ const hay = `${entry.text} ${entry.evidence ?? ""}`;
22679
+ const kinds = { change: [], time: [], event: [], life: [], other: [], person: [], action: [] };
22680
+ let score = 0;
22681
+ for (const t of terms) {
22682
+ if (!hay.includes(t)) continue;
22683
+ 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";
22684
+ kinds[kind].push(t);
22685
+ score += W[kind];
22686
+ }
22687
+ return {
22688
+ score,
22689
+ matched: Object.values(kinds).flat(),
22690
+ kinds,
22691
+ hasSpecial: kinds.change.length > 0 || kinds.time.length > 0 || kinds.event.length > 0
22692
+ };
22693
+ }
22694
+ function rankForesightEntries(entries2, terms, opts = {}) {
22695
+ const minScore = opts.minScore ?? DEFAULT_MIN_SCORE3;
22696
+ const termList = [...terms];
22697
+ const N = Math.max(1, entries2.length);
22698
+ const df = /* @__PURE__ */ new Map();
22699
+ for (const t of termList) {
22700
+ let c = 0;
22701
+ for (const e of entries2) if (`${e.text} ${e.evidence ?? ""}`.includes(t)) c++;
22702
+ df.set(t, c);
22703
+ }
22704
+ const idf = (t) => {
22705
+ const c = df.get(t) ?? 0;
22706
+ if (c <= 0) return 0;
22707
+ return 1 + Math.log2(1 + N / c) / Math.log2(1 + N);
22708
+ };
22709
+ const out = [];
22710
+ for (const e of entries2) {
22711
+ const base = scoreForesightEntry(e, termList);
22712
+ const s = { ...base, score: termList.reduce((acc, t) => acc + (base.matched.includes(t) ? weightOf(t) * idf(t) : 0), 0) };
22713
+ if (opts.requireStructure) {
22714
+ const anchor = s.kinds.change.length > 0 || s.kinds.event.length > 0 || s.kinds.life.length > 0;
22715
+ if (!anchor) continue;
22716
+ }
22717
+ if (s.score < minScore) continue;
22718
+ const tag = (k) => s.kinds[k].length ? `${k}:${s.kinds[k].join("/")}` : "";
22719
+ out.push({ entry: e, score: s.score, reasons: [tag("change"), tag("time"), tag("event"), tag("other"), tag("person")].filter(Boolean).join(" ") });
22720
+ }
22721
+ return out.sort((a, b) => b.score - a.score);
22722
+ }
22723
+ function extractTerms(message, stopWords) {
22724
+ const terms = /* @__PURE__ */ new Set();
22725
+ for (const m of message.matchAll(TIME_SURFACE)) terms.add(m[0]);
22726
+ for (const run of message.match(/[一-鿿]{2,}|[A-Za-z0-9_-]{3,}/g) ?? []) {
22727
+ if (run.length <= 6 && !stopWords.has(run)) terms.add(run);
22728
+ if (/[一-鿿]/.test(run)) {
22729
+ for (let i = 0; i + 2 <= run.length; i++) {
22730
+ const g = run.slice(i, i + 2);
22731
+ if (!stopWords.has(g)) terms.add(g);
22732
+ }
22733
+ }
22734
+ }
22735
+ return terms;
22736
+ }
22737
+ var CHANGE_RE, TIME_RE, ACTION_RE, EVENT_RE, LIFE_RE, PERSON_RE, W, DEFAULT_MIN_SCORE3, TIME_SURFACE;
22738
+ var init_recall_score = __esm({
22739
+ "src/memory/proactive/recall-score.ts"() {
22740
+ "use strict";
22741
+ CHANGE_RE = /^(改|变成|变为|变更|改期|取消|没了|挪|调整|换|推|提前|延后|顺延|恢复|新增|加上|改成|改到|调到|排|定)$/;
22742
+ TIME_RE = /^(\d{1,2}\/\d{1,2}|\d{1,2}月|\d{1,2}日|周[一二三四五六日天]|星期[一二三四五六日天]|明天|后天|大后天|下周|上周|本周|今晚|今早|明早|明晚|早上|上午|中午|下午|晚上|凌晨|月底|月初|\d{1,2}:\d{2}|\d{1,2}[点時时])$/;
22743
+ ACTION_RE = /^(翻|翻翻|上传|搞完|搞|看看|看|发我|发给|截图|下载|跑|贴|转写|重发|试试|查|找|写|做|弄|传|发)$/;
22744
+ EVENT_RE = /^(来电|电话|打电话|打给|上门|到访|通知|审核|拜访|到货|快递|送货|生病|发烧|感冒|住院|输水|挂水|复诊|复查|面试|体检|签约|合同|付款|到账|截止|到期)$/;
22745
+ LIFE_RE = /^(回家|出差|探亲|复查|复诊|体检|住院|生日|纪念日|搬家|开学|放假|假期|寒假|暑假|机票|车票|高铁票|飞机|电影|聚餐|聚会|拜访|来访|接机|送机|考试|报名|签证|护照|续签|过户)$/;
22746
+ PERSON_RE = /^(翀哥|老公|老婆|睿阳|荣阳|张小媒|小柯|小雯|孙总|曲教授|潘总|燕子|方老师|李经理|Leigh|amy|Amy)$/;
22747
+ W = {
22748
+ change: 2,
22749
+ time: 1.5,
22750
+ event: 2,
22751
+ life: 2,
22752
+ other: 1,
22753
+ person: 0.3,
22754
+ action: 0
22755
+ };
22756
+ DEFAULT_MIN_SCORE3 = Number(process.env.RECALL_MIN_SCORE ?? 1);
22757
+ TIME_SURFACE = /\d{1,2}月\d{1,2}[日号]?|\d{1,2}号|\d{1,2}\/\d{1,2}|\d{1,2}[点时::]\d{0,2}|周[一二三四五六日天]|星期[一二三四五六日天]|明天|后天|大后天|下周|本周|上周|今晚|今早|明早|明晚|月底|月初|下个月|这个月/g;
22072
22758
  }
22073
22759
  });
22074
22760
 
@@ -22146,7 +22832,7 @@ function loadForesightEntries(everosRoot, now, userId, ownerFilter) {
22146
22832
  return entries2.slice();
22147
22833
  }
22148
22834
  function parseForesightEntries(everosRoot, now, userId) {
22149
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
22835
+ const today = todayKey(now);
22150
22836
  let files;
22151
22837
  if (userId) {
22152
22838
  const ownDir = join40(everosRoot, userId, "default_project", "users", userId, ".foresights");
@@ -22182,35 +22868,11 @@ function parseForesightEntries(everosRoot, now, userId) {
22182
22868
  }
22183
22869
  return out;
22184
22870
  }
22185
- function loadForesightLines(everosRoot, now, maxEntries = 20, userId) {
22186
- const today = new Date(now.getTime() + 8 * 36e5).toISOString().slice(0, 10);
22187
- const out = loadForesightEntries(everosRoot, now, userId);
22188
- const isOpen = (f) => f.start <= today;
22189
- out.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || (isOpen(a) ? a.end.localeCompare(b.end) : a.start.localeCompare(b.start)));
22190
- const open = out.filter(isOpen).slice(0, Math.min(12, Math.floor(maxEntries * 0.6)));
22191
- const upcoming = out.filter((f) => !isOpen(f)).slice(0, Math.max(0, maxEntries - open.length));
22192
- return [...open, ...upcoming].map((f) => `- [\u7A97\u53E3 ${f.start}~${f.end}] ${f.text}${f.evidence ? `\uFF08\u4F9D\u636E\uFF1A${f.evidence}\uFF09` : ""}`);
22193
- }
22194
- function matchForesightForEvent(entries2, message, maxHits = 5) {
22195
- const terms = /* @__PURE__ */ new Set();
22196
- for (const run of message.match(/[一-鿿]{2,}|[A-Za-z0-9_-]{3,}/g) ?? []) {
22197
- if (run.length <= 6 && /[一-鿿]/.test(run) && !FS_MATCH_STOP.has(run)) terms.add(run);
22198
- if (/[一-鿿]/.test(run)) {
22199
- for (let i = 0; i + 2 <= run.length; i++) {
22200
- const g = run.slice(i, i + 2);
22201
- if (!FS_MATCH_STOP.has(g)) terms.add(g);
22202
- }
22203
- }
22204
- }
22871
+ function matchForesightForEvent(entries2, message, maxHits = 2) {
22872
+ const terms = extractTerms(message, FS_MATCH_STOP);
22205
22873
  if (terms.size === 0) return [];
22206
- const hits = [];
22207
- for (const f of entries2) {
22208
- const hay = `${f.text} ${f.evidence}`;
22209
- const term = [...terms].find((t) => hay.includes(t));
22210
- if (term) hits.push({ line: `- [\u7A97\u53E3 ${f.start}~${f.end}] ${f.text}\uFF08\u4F9D\u636E\uFF1A${f.evidence}\uFF09`, term });
22211
- if (hits.length >= maxHits) break;
22212
- }
22213
- return hits.map((h) => h.line);
22874
+ const ranked = rankForesightEntries(entries2, terms);
22875
+ 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`);
22214
22876
  }
22215
22877
  function listForesightMds(dir) {
22216
22878
  try {
@@ -22350,6 +23012,8 @@ var init_opportunity = __esm({
22350
23012
  init_models();
22351
23013
  init_calendar_bridge();
22352
23014
  init_embedding();
23015
+ init_recall_score();
23016
+ init_clock();
22353
23017
  OPPORTUNITY_SCAN_INTERVAL_MS = 6 * 60 * 6e4;
22354
23018
  foresightCache = /* @__PURE__ */ new Map();
22355
23019
  FORESIGHT_CACHE_TTL_MS = 30 * 6e4;
@@ -22865,16 +23529,16 @@ var init_executor3 = __esm({
22865
23529
  const refLower = ref.toLowerCase().trim();
22866
23530
  if (!refLower) return null;
22867
23531
  if (!this.titleToIdCache || this.titleToIdCache.size < this.store.nodeCount) {
22868
- const cache2 = /* @__PURE__ */ new Map();
23532
+ const cache3 = /* @__PURE__ */ new Map();
22869
23533
  for (const node of this.store.getAllNodes()) {
22870
23534
  const title = String(node.data.title ?? "").toLowerCase().trim();
22871
- if (title) cache2.set(title, node.id);
23535
+ if (title) cache3.set(title, node.id);
22872
23536
  }
22873
- this.titleToIdCache = cache2;
23537
+ this.titleToIdCache = cache3;
22874
23538
  }
22875
- const cache = this.titleToIdCache;
22876
- if (cache.has(refLower)) return cache.get(refLower);
22877
- for (const [title, id] of cache) {
23539
+ const cache2 = this.titleToIdCache;
23540
+ if (cache2.has(refLower)) return cache2.get(refLower);
23541
+ for (const [title, id] of cache2) {
22878
23542
  if (title.includes(refLower) || refLower.includes(title)) return id;
22879
23543
  }
22880
23544
  return null;
@@ -23484,18 +24148,23 @@ ${JSON.stringify(payload, null, 2)}
23484
24148
 
23485
24149
  // src/memory/proactive/proactive-tools.ts
23486
24150
  import { existsSync as existsSync26 } from "node:fs";
23487
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
23488
24151
  function setProactiveWorkspace(workspace) {
23489
24152
  toolWorkspace = workspace;
23490
24153
  }
23491
- function openDb2() {
24154
+ function withStore(fn) {
23492
24155
  if (!toolWorkspace) return null;
23493
24156
  const p = proactiveDbPath(toolWorkspace);
23494
24157
  if (!existsSync26(p)) return null;
23495
- return new DatabaseSync4(p);
24158
+ const store = new Persistence(p);
24159
+ try {
24160
+ return fn(store);
24161
+ } finally {
24162
+ store.close();
24163
+ }
23496
24164
  }
23497
- function fmtRow2(r) {
23498
- return `#${r.id} [${r.status}] ${r.title} (urgency=${r.urgency}, scheduled=${r.scheduled_time}${r.calendar_id ? `, cal=#${r.calendar_id}` : ""})`;
24165
+ function fmtRow2(a) {
24166
+ const when = a.scheduledTime ? formatForLlm(a.scheduledTime) : "(\u65E0)";
24167
+ return `#${a.id} [${a.status}] ${a.title} (urgency=${a.urgency}, scheduled=${when}${a.calendarId ? `, cal=#${a.calendarId}` : ""})`;
23499
24168
  }
23500
24169
  function registerProactiveTools() {
23501
24170
  registry.register({
@@ -23510,50 +24179,43 @@ function registerProactiveTools() {
23510
24179
  required: ["action_id"]
23511
24180
  },
23512
24181
  handler: async (args) => {
23513
- const db2 = openDb2();
23514
- if (!db2) return { content: "proactive \u672A\u542F\u7528\uFF08\u65E0 workspace \u6216 .proactive/proactive.db\uFF09", isError: true };
23515
- try {
23516
- const row = db2.prepare("SELECT id, status, title FROM actions WHERE id=?").get(args.action_id);
23517
- if (!row) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728\u3002\u7528 proactive_status \u67E5\u5F53\u524D\u5217\u8868\u3002`, isError: true };
23518
- if (args.calendar_id !== void 0) {
23519
- db2.prepare("UPDATE actions SET status=?, completed_at=?, calendar_id=? WHERE id=?").run("completed", (/* @__PURE__ */ new Date()).toISOString(), args.calendar_id, args.action_id);
23520
- } else {
23521
- db2.prepare("UPDATE actions SET status=?, completed_at=? WHERE id=?").run("completed", (/* @__PURE__ */ new Date()).toISOString(), args.action_id);
23522
- }
23523
- return { content: `\u2713 completed: ${row.title}${args.calendar_id !== void 0 ? `\uFF08calendar #${args.calendar_id} \u5DF2\u56DE\u586B\uFF09` : ""}` };
23524
- } finally {
23525
- db2.close();
23526
- }
24182
+ const out = withStore((store) => {
24183
+ const a = store.getAction(String(args.action_id));
24184
+ if (!a) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728\u3002\u7528 proactive_status \u67E5\u5F53\u524D\u5217\u8868\u3002`, isError: true };
24185
+ store.updateActionStatus(a.id, "completed", { completedAt: nowUtc() });
24186
+ if (args.calendar_id !== void 0) store.linkCalendar(a.id, Number(args.calendar_id));
24187
+ return { content: `\u2713 action ${a.id} \u6807\u8BB0\u4E3A completed${args.calendar_id !== void 0 ? `\uFF08\u5173\u8054 calendar #${args.calendar_id}\uFF09` : ""}` };
24188
+ });
24189
+ return out ?? { content: "proactive \u672A\u542F\u7528\uFF08\u65E0 workspace \u6216 .proactive/proactive.db\uFF09", isError: true };
23527
24190
  }
23528
24191
  });
23529
24192
  registry.register({
23530
24193
  name: "proactive_cancel",
23531
- 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",
24194
+ 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",
23532
24195
  schema: {
23533
24196
  type: "object",
23534
24197
  properties: {
23535
24198
  action_id: { type: "string", description: "action id" },
23536
- reason: { type: "string", description: "\u53D6\u6D88\u7406\u7531\uFF08\u6280\u672F\u566A\u97F3/\u5DF2\u8FC7\u65F6/\u91CD\u590D/\u4E0D\u8BE5\u751F\u6210\uFF09" }
24199
+ reason: { type: "string", description: "\u53D6\u6D88\u7406\u7531\uFF08\u4F1A\u8BB0\u8FDB cancel_reason\uFF09" }
23537
24200
  },
23538
- required: ["action_id", "reason"]
24201
+ required: ["action_id"]
23539
24202
  },
23540
24203
  handler: async (args) => {
23541
- const db2 = openDb2();
23542
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
23543
- try {
23544
- const row = db2.prepare("SELECT id, title FROM actions WHERE id=?").get(args.action_id);
23545
- if (!row) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728`, isError: true };
23546
- db2.prepare("UPDATE actions SET status=?, completed_at=?, cancel_reason=? WHERE id=?").run("cancelled", (/* @__PURE__ */ new Date()).toISOString(), String(args.reason || "\u672A\u6CE8\u660E"), args.action_id);
23547
- return { content: `\u2713 cancelled: ${row.title}\uFF08${args.reason}\uFF09${/已过时|已处理|做完了|马后炮/.test(String(args.reason)) ? `
23548
- \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` : ""}` };
23549
- } finally {
23550
- db2.close();
23551
- }
24204
+ const out = withStore((store) => {
24205
+ const a = store.getAction(String(args.action_id));
24206
+ if (!a) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728`, isError: true };
24207
+ store.updateActionStatus(a.id, "cancelled", {
24208
+ completedAt: nowUtc(),
24209
+ cancelReason: String(args.reason || "\u672A\u6CE8\u660E")
24210
+ });
24211
+ return { content: `\u2713 action ${a.id} \u5DF2\u53D6\u6D88\uFF08${String(args.reason || "\u672A\u6CE8\u660E")}\uFF09` };
24212
+ });
24213
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
23552
24214
  }
23553
24215
  });
23554
24216
  registry.register({
23555
24217
  name: "proactive_resolve_intent",
23556
- 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",
24218
+ 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",
23557
24219
  schema: {
23558
24220
  type: "object",
23559
24221
  properties: {
@@ -23562,31 +24224,25 @@ function registerProactiveTools() {
23562
24224
  }
23563
24225
  },
23564
24226
  handler: async (args) => {
23565
- const db2 = openDb2();
23566
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
23567
- try {
24227
+ const out = withStore((store) => {
23568
24228
  let intentId = String(args.intent_id || "");
23569
24229
  if (!intentId && args.action_id) {
23570
- const row = db2.prepare("SELECT intent_id FROM actions WHERE id=?").get(args.action_id);
23571
- if (!row) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728`, isError: true };
23572
- intentId = row.intent_id;
24230
+ const a = store.getAction(String(args.action_id));
24231
+ if (!a) return { content: `\u9519\u8BEF: action '${args.action_id}' \u4E0D\u5B58\u5728`, isError: true };
24232
+ intentId = a.intentId;
23573
24233
  }
23574
24234
  if (!intentId) return { content: "Usage: proactive_resolve_intent --action_id <id> \u6216 --intent_id <id>", isError: true };
23575
- const node = db2.prepare("SELECT id, data FROM nodes WHERE id=? AND type=?").get(intentId, "intent");
23576
- if (!node) return { content: `\u9519\u8BEF: intent '${intentId}' \u4E0D\u5B58\u5728`, isError: true };
23577
- let data;
23578
- try {
23579
- data = JSON.parse(node.data);
23580
- } catch {
23581
- data = {};
23582
- }
23583
- data.status = "resolved";
23584
- db2.prepare("UPDATE nodes SET data=? WHERE id=?").run(JSON.stringify(data), intentId);
23585
- 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);
23586
- return { content: `\u2713 intent ${intentId} resolved\u3002\u7EA7\u8054\u53D6\u6D88 ${Number(res.changes)} \u4E2A\u6392\u961F\u7684\u5144\u5F1F action\u3002` };
23587
- } finally {
23588
- db2.close();
23589
- }
24235
+ const data = store.getIntentData(intentId);
24236
+ if (data === null) return { content: `\u9519\u8BEF: intent '${intentId}' \u4E0D\u5B58\u5728`, isError: true };
24237
+ store.setIntentData(intentId, { ...data, status: "resolved" });
24238
+ const n = store.cancelActionsOfIntents(
24239
+ [intentId],
24240
+ "intent resolved by agent\uFF08\u9A6C\u540E\u70AE\u6E05\u626B\uFF09",
24241
+ nowUtc()
24242
+ );
24243
+ return { content: `\u2713 intent ${intentId} resolved\u3002\u7EA7\u8054\u53D6\u6D88 ${n} \u4E2A\u6392\u961F\u7684\u5144\u5F1F action\u3002` };
24244
+ });
24245
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
23590
24246
  }
23591
24247
  });
23592
24248
  registry.register({
@@ -23599,16 +24255,12 @@ function registerProactiveTools() {
23599
24255
  }
23600
24256
  },
23601
24257
  handler: async (args) => {
23602
- const db2 = openDb2();
23603
- if (!db2) return { content: "proactive \u672A\u542F\u7528", isError: true };
23604
- try {
23605
- const where = args.include_done ? "status NOT IN ('cancelled')" : "status IN ('queued','executing')";
23606
- const rows = db2.prepare(`SELECT * FROM actions WHERE ${where} ORDER BY scheduled_time ASC LIMIT 30`).all();
24258
+ const out = withStore((store) => {
24259
+ const rows = store.listActions(args.include_done ? "not-cancelled" : "open");
23607
24260
  if (rows.length === 0) return { content: "\u5F53\u524D\u6CA1\u6709\u5F85\u5904\u7406\u7684 proactive action \u2713" };
23608
24261
  return { content: rows.map(fmtRow2).join("\n") };
23609
- } finally {
23610
- db2.close();
23611
- }
24262
+ });
24263
+ return out ?? { content: "proactive \u672A\u542F\u7528", isError: true };
23612
24264
  }
23613
24265
  });
23614
24266
  }
@@ -23618,6 +24270,8 @@ var init_proactive_tools = __esm({
23618
24270
  "use strict";
23619
24271
  init_registry();
23620
24272
  init_config2();
24273
+ init_persistence();
24274
+ init_clock();
23621
24275
  }
23622
24276
  });
23623
24277
 
@@ -23681,7 +24335,7 @@ __export(plugin_exports, {
23681
24335
  ProactivePlugin: () => ProactivePlugin,
23682
24336
  feedMem7Cell: () => feedMem7Cell
23683
24337
  });
23684
- import { join as join41, dirname as dirname11 } from "node:path";
24338
+ import { join as join41 } from "node:path";
23685
24339
  import { existsSync as existsSync27, readFileSync as readFileSync33 } from "node:fs";
23686
24340
  function isFiller(text) {
23687
24341
  const t = text.trim();
@@ -23863,19 +24517,23 @@ var init_plugin = __esm({
23863
24517
  return;
23864
24518
  }
23865
24519
  if (!this.provider) {
23866
- const providerCfg = this.providers[this.config.providerId];
24520
+ const configuredId = this.config.providerId;
24521
+ const providerCfg = configuredId ? this.providers[configuredId] : void 0;
23867
24522
  if (providerCfg) {
23868
24523
  this.provider = createProvider(providerCfg);
23869
24524
  } else {
23870
24525
  const mainProvider = ctx.deps?.engine?.getProvider?.();
23871
24526
  const mainModel = ctx.config?.model;
24527
+ const mainProviderId = ctx.config?.provider?.id || "main";
23872
24528
  if (mainProvider && mainModel) {
23873
24529
  this.provider = this.mainProviderProxy(ctx);
23874
24530
  const modelId = String(mainModel);
23875
- this.config = { ...this.config, modelId, modelRef: `${this.config.providerId}/${modelId}` };
23876
- console.warn(`[proactive] provider "${this.config.providerId}" not found in models.providers \u2014 falling back to main provider (model=${modelId})`);
24531
+ this.config = { ...this.config, modelId, modelRef: `${mainProviderId}/${modelId}` };
24532
+ if (configuredId) {
24533
+ console.warn(`[proactive] \u914D\u7F6E\u7684 provider "${configuredId}" \u4E0D\u5728 models.providers \u91CC \u2014 \u6539\u7528\u4E3B\u6A21\u578B\uFF08${mainProviderId}/${modelId}\uFF09`);
24534
+ }
23877
24535
  } else {
23878
- console.warn(`[proactive] provider "${this.config.providerId}" not found in models.providers and no main provider \u2014 not starting`);
24536
+ console.warn(`[proactive] ${configuredId ? `provider "${configuredId}" \u4E0D\u5B58\u5728\uFF0C\u4E14` : ""}\u4E3B provider \u4E0D\u53EF\u7528 \u2014 \u4E0D\u542F\u52A8`);
23879
24537
  return;
23880
24538
  }
23881
24539
  }
@@ -23970,18 +24628,7 @@ ${String(input.tool_output ?? "")}`
23970
24628
  foresightLines.push(`[mem7] [\u7A97\u53E3 ${e.start}~${e.end}] ${e.text}${e.evidence ? `\uFF08\u4F9D\u636E\uFF1A${e.evidence}\uFF09` : ""}`);
23971
24629
  }
23972
24630
  }
23973
- const lancedbPath = String(liveConfig.get("everos.lancedbPath") ?? "");
23974
- if (lancedbPath) {
23975
- const everosRoot = join41(dirname11(lancedbPath), "..");
23976
- const everosUserId = String(liveConfig.get("everos.userId") ?? "") || void 0;
23977
- for (const l of loadForesightLines(everosRoot, nowDate, 20, everosUserId)) {
23978
- const key = l.replace(/\s+/g, "");
23979
- if (seen.has(key)) continue;
23980
- seen.add(key);
23981
- foresightLines.push(l.replace(/^- /, "[everos] "));
23982
- }
23983
- }
23984
- if (foresightLines.length > 0) console.log(`[proactive] foresight dual-source total: ${foresightLines.length}`);
24631
+ if (foresightLines.length > 0) console.log(`[proactive] mem7 foresight total: ${foresightLines.length}`);
23985
24632
  } catch (err) {
23986
24633
  console.warn(`[proactive] foresight load failed: ${err.message}`);
23987
24634
  }
@@ -24155,18 +24802,6 @@ ${String(input.tool_output ?? "")}`
24155
24802
  } else if (stateDir && !scopeReady) {
24156
24803
  console.warn("[proactive] mem7 recall skipped: sender scope but cell has no user speaker");
24157
24804
  }
24158
- if (senderScoped) {
24159
- this.warnOnce("everos-skip", "[proactive] everos \u6E90\u5728 sender \u6A21\u5F0F\u4E0B\u8DF3\u8FC7\uFF08\u65E0 per-speaker \u6620\u5C04\uFF09");
24160
- } else {
24161
- const lancedbPath = String(liveConfig.get("everos.lancedbPath") ?? "");
24162
- if (lancedbPath) {
24163
- const everosRoot = join41(dirname11(lancedbPath), "..");
24164
- const everosUserId = String(liveConfig.get("everos.userId") ?? "") || void 0;
24165
- for (const h of matchForesightForEvent(loadForesightEntries(everosRoot, now, everosUserId), prompt)) {
24166
- hitSet.add(`[everos] ${h.replace(/^- /, "")}`);
24167
- }
24168
- }
24169
- }
24170
24805
  if (hitSet.size > 0) {
24171
24806
  foresightHits = [...hitSet];
24172
24807
  console.log(`[proactive] foresight matched ${hitSet.size} open-window prediction(s) for incoming message`);
@@ -24247,10 +24882,10 @@ ${String(input.tool_output ?? "")}`
24247
24882
 
24248
24883
  // src/memory/sessionMemory/sessionMemoryUtils.ts
24249
24884
  import { join as join42 } from "node:path";
24250
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync34 } from "node:fs";
24885
+ import { mkdirSync as mkdirSync17, readFileSync as readFileSync34 } from "node:fs";
24251
24886
  function setSessionMemoryStateDir(dir) {
24252
24887
  _stateDir2 = dir;
24253
- mkdirSync16(dir, { recursive: true });
24888
+ mkdirSync17(dir, { recursive: true });
24254
24889
  }
24255
24890
  function getSessionMemoryDir() {
24256
24891
  return _stateDir2;
@@ -24522,7 +25157,7 @@ __export(sessionMemory_exports, {
24522
25157
  resetLastMemoryMessageUuid: () => resetLastMemoryMessageUuid,
24523
25158
  shouldExtractMemory: () => shouldExtractMemory
24524
25159
  });
24525
- import { readFileSync as readFileSync35, mkdirSync as mkdirSync17 } from "node:fs";
25160
+ import { readFileSync as readFileSync35, mkdirSync as mkdirSync18 } from "node:fs";
24526
25161
  import { writeFile as writeFile3 } from "node:fs/promises";
24527
25162
  function initSessionMemory(deps) {
24528
25163
  _deps = deps;
@@ -24581,7 +25216,7 @@ function shouldExtractMemory(estimatedTokens, messages) {
24581
25216
  }
24582
25217
  async function setupSessionMemoryFile() {
24583
25218
  const memoryDir = getSessionMemoryDir();
24584
- mkdirSync17(memoryDir, { recursive: true });
25219
+ mkdirSync18(memoryDir, { recursive: true });
24585
25220
  const memoryPath = getSessionMemoryPath();
24586
25221
  let currentMemory;
24587
25222
  try {
@@ -24622,7 +25257,7 @@ Here is the recent conversation context that you should extract notes from:
24622
25257
 
24623
25258
  ${messages.filter((m) => m.role === "user" || m.role === "assistant").slice(-20).map((m) => {
24624
25259
  const text = typeof m.content === "string" ? m.content : "[content blocks]";
24625
- return `[${m.role}]: ${text.slice(0, 500)}`;
25260
+ return `[${m.role}]: ${truncateByCodePoint(text, 500)}`;
24626
25261
  }).join("\n\n")}`,
24627
25262
  maxTurns: 3,
24628
25263
  toolOverride: editTools.length > 0 ? editTools : void 0,
@@ -24684,6 +25319,7 @@ var init_sessionMemory = __esm({
24684
25319
  "use strict";
24685
25320
  init_sessionMemoryUtils();
24686
25321
  init_prompts2();
25322
+ init_well_formed();
24687
25323
  init_query();
24688
25324
  init_registry();
24689
25325
  init_features();
@@ -25078,7 +25714,7 @@ __export(handle_query_exports, {
25078
25714
  resetSessionStartInjection: () => resetSessionStartInjection,
25079
25715
  resolveSystemPrompt: () => resolveSystemPrompt
25080
25716
  });
25081
- import { readFileSync as readFileSync36, existsSync as existsSync28 } from "node:fs";
25717
+ import { readFileSync as readFileSync36, existsSync as existsSync28, statSync as statSync13 } from "node:fs";
25082
25718
  import { join as join45, resolve as resolve8 } from "node:path";
25083
25719
  import * as path29 from "node:path";
25084
25720
  function resolveSystemPrompt(v) {
@@ -25274,6 +25910,10 @@ ${text}` : text });
25274
25910
  } catch {
25275
25911
  }
25276
25912
  }
25913
+ if (source === "inner-voice") {
25914
+ chatMode = "emotion";
25915
+ console.log(`[mode] ${sessionId} source=inner-voice \u2192 \u5F3A\u5236 emotion\uFF08light\uFF09`);
25916
+ }
25277
25917
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
25278
25918
  const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
25279
25919
  const volatileParts = [
@@ -25499,7 +26139,7 @@ ${text}` : text });
25499
26139
  const restoredMemories = [];
25500
26140
  for (const memPath of newPaths) {
25501
26141
  try {
25502
- const stat4 = statSync(memPath);
26142
+ const stat4 = statSync13(memPath);
25503
26143
  const content = readFileSync36(memPath, "utf-8");
25504
26144
  const header = memoryHeader(memPath, stat4.mtimeMs);
25505
26145
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
@@ -25534,13 +26174,16 @@ ${text}` : text });
25534
26174
  } else if (recallMode === "everos") {
25535
26175
  const everosCfg = deps?.everosCfg;
25536
26176
  if (everosCfg && everosCfg.everosUrl && everosCfg.userId) {
26177
+ const recallOwnerMode = String(liveConfig.get("everos.ownerMode") ?? "agent") === "sender" ? "sender" : "agent";
26178
+ const everosOwner = recallOwnerMode === "sender" ? inboundMeta?.from || everosCfg.userId : everosCfg.userId;
25537
26179
  relevantMemories = await findRelevantMemoriesEveros(
25538
26180
  effectiveText,
25539
26181
  memoryDir,
25540
26182
  surfaced.paths,
25541
26183
  {
25542
26184
  everosUrl: everosCfg.everosUrl,
25543
- userId: everosCfg.userId,
26185
+ userId: everosOwner,
26186
+ appId: everosCfg.userId,
25544
26187
  rerankUrl: everosCfg.rerank?.baseUrl,
25545
26188
  rerankApiKey: everosCfg.rerank?.apiKey,
25546
26189
  rerankModel: everosCfg.rerank?.model,
@@ -25830,6 +26473,8 @@ ${text}` : text });
25830
26473
  Promise.resolve().then(() => (init_embedding(), embedding_exports))
25831
26474
  ]);
25832
26475
  const embedding = parseEmbeddingConfig2(liveConfig.get("mem7.embedding"));
26476
+ const ownerMode = String(liveConfig.get("everos.ownerMode") ?? "agent") === "sender" ? "sender" : "agent";
26477
+ const identity = String(liveConfig.get("everos.userId") ?? "") || agentName;
25833
26478
  await runMem7Pipeline2({
25834
26479
  rawMessages: messages,
25835
26480
  sessionId,
@@ -25837,6 +26482,8 @@ ${text}` : text });
25837
26482
  provider,
25838
26483
  model: model2,
25839
26484
  agentName,
26485
+ identity,
26486
+ ownerMode,
25840
26487
  onExtracted: (cell, episode, facts) => feedMem7Cell2(cell, episode, facts),
25841
26488
  embedding: embedding ?? void 0
25842
26489
  });
@@ -26099,8 +26746,8 @@ async function runQuery(args) {
26099
26746
  if (!stateDir) {
26100
26747
  const home = process.env.HOME || process.env.USERPROFILE || ".";
26101
26748
  const cwd = process.cwd();
26102
- const { existsSync: existsSync34 } = await import("node:fs");
26103
- if (existsSync34(join47(cwd, "configs"))) stateDir = cwd;
26749
+ const { existsSync: existsSync35 } = await import("node:fs");
26750
+ if (existsSync35(join47(cwd, "configs"))) stateDir = cwd;
26104
26751
  else {
26105
26752
  try {
26106
26753
  const pointer = JSON.parse(readFileSync37(join47(home, ".engine7.json"), "utf-8"));
@@ -26534,6 +27181,20 @@ async function runDoctor(stateDir, opts) {
26534
27181
  };
26535
27182
  walk(config);
26536
27183
  report.missingEnvKeys = [...missing].sort();
27184
+ const stale = [];
27185
+ for (const rel of ["workspace/SESSION-STATE.md", "SESSION-STATE.md"]) {
27186
+ const p = path30.join(stateDir, rel);
27187
+ try {
27188
+ if (!fs28.existsSync(p)) continue;
27189
+ const head = fs28.readFileSync(p, "utf8").split("\n").slice(0, 10).join("\n");
27190
+ const m = head.match(/^#{1,3}\s*当前时间\s*$\n+\s*(\d{4}-\d{2}-\d{2})/m);
27191
+ if (!m) continue;
27192
+ const ageH = (Date.now() - (/* @__PURE__ */ new Date(`${m[1]}T00:00:00Z`)).getTime()) / 36e5;
27193
+ if (ageH > 48) stale.push(`${rel}: ${m[1]}`);
27194
+ } catch {
27195
+ }
27196
+ }
27197
+ report.staleTimeHeaders = stale;
26537
27198
  report.apiPort = config?.api?.port || 16890;
26538
27199
  report.engineAlive = await probePort(report.apiPort, opts?.probeTimeoutMs ?? 1500);
26539
27200
  return report;
@@ -26563,6 +27224,138 @@ var init_cli_doctor = __esm({
26563
27224
  }
26564
27225
  });
26565
27226
 
27227
+ // src/provision-free.ts
27228
+ var provision_free_exports = {};
27229
+ __export(provision_free_exports, {
27230
+ loadOrCreateInstallIdentity: () => loadOrCreateInstallIdentity,
27231
+ provisionFreeToken: () => provisionFreeToken,
27232
+ readInstallIdentity: () => readInstallIdentity,
27233
+ writeIdentityContactEmail: () => writeIdentityContactEmail
27234
+ });
27235
+ import { randomBytes as randomBytes6, randomUUID as randomUUID9 } from "node:crypto";
27236
+ import { chmodSync as chmodSync2, existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync39, writeFileSync as writeFileSync20 } from "node:fs";
27237
+ import { dirname as dirname12, join as join49 } from "node:path";
27238
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
27239
+ function readContactEmailOf(parsed) {
27240
+ return typeof parsed?.contactEmail === "string" ? parsed.contactEmail.trim() : "";
27241
+ }
27242
+ function loadOrCreateInstallIdentity(stateDir) {
27243
+ const file2 = join49(stateDir, IDENTITY_FILE);
27244
+ if (existsSync31(file2)) {
27245
+ let parsed;
27246
+ try {
27247
+ parsed = JSON.parse(readFileSync39(file2, "utf8"));
27248
+ } catch (e) {
27249
+ throw new Error(
27250
+ `${IDENTITY_FILE} \u5B58\u5728\u4F46\u4E0D\u662F\u5408\u6CD5 JSON\uFF08${e.message}\uFF09\u2014\u2014\u5220\u6389\u5B83\u91CD\u8DD1\u4F1A\u9886\u4E00\u628A\u65B0\u4EE4\u724C\uFF0C\u65E7\u7684\u90A3\u628A\u53D6\u4E0D\u56DE\u6765`
27251
+ );
27252
+ }
27253
+ if (typeof parsed?.installId !== "string" || typeof parsed?.installSecret !== "string") {
27254
+ throw new Error(`${IDENTITY_FILE} \u7F3A installId / installSecret \u5B57\u6BB5`);
27255
+ }
27256
+ const email = readContactEmailOf(parsed);
27257
+ return {
27258
+ installId: parsed.installId,
27259
+ installSecret: parsed.installSecret,
27260
+ ...email ? { contactEmail: email } : {}
27261
+ };
27262
+ }
27263
+ const identity = {
27264
+ // 服务端校验 installId 长度为 8..36、只允许字母数字和连字符;uuid 正好符合
27265
+ installId: randomUUID9(),
27266
+ // 服务端校验 installSecret 为 32..64 位十六进制
27267
+ installSecret: randomBytes6(32).toString("hex")
27268
+ };
27269
+ mkdirSync19(stateDir, { recursive: true });
27270
+ writeFileSync20(file2, JSON.stringify(identity, null, 2) + "\n", { mode: 384 });
27271
+ try {
27272
+ chmodSync2(file2, 384);
27273
+ } catch {
27274
+ }
27275
+ return identity;
27276
+ }
27277
+ function readInstallIdentity(stateDir) {
27278
+ try {
27279
+ const parsed = JSON.parse(readFileSync39(join49(stateDir, IDENTITY_FILE), "utf8"));
27280
+ if (typeof parsed?.installId !== "string" || typeof parsed?.installSecret !== "string") return null;
27281
+ const email = readContactEmailOf(parsed);
27282
+ return {
27283
+ installId: parsed.installId,
27284
+ installSecret: parsed.installSecret,
27285
+ ...email ? { contactEmail: email } : {}
27286
+ };
27287
+ } catch {
27288
+ return null;
27289
+ }
27290
+ }
27291
+ function writeIdentityContactEmail(stateDir, email) {
27292
+ const identity = loadOrCreateInstallIdentity(stateDir);
27293
+ const trimmed = email.trim();
27294
+ const next = { ...identity, contactEmail: trimmed };
27295
+ const file2 = join49(stateDir, IDENTITY_FILE);
27296
+ mkdirSync19(stateDir, { recursive: true });
27297
+ writeFileSync20(file2, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
27298
+ try {
27299
+ chmodSync2(file2, 384);
27300
+ } catch {
27301
+ }
27302
+ }
27303
+ function readEngineVersion() {
27304
+ try {
27305
+ return JSON.parse(readFileSync39(join49(dirname12(__filename), "..", "package.json"), "utf8")).version || "unknown";
27306
+ } catch {
27307
+ return "unknown";
27308
+ }
27309
+ }
27310
+ async function provisionFreeToken(opts) {
27311
+ const doFetch = opts.fetchImpl ?? fetch;
27312
+ const identity = loadOrCreateInstallIdentity(opts.stateDir);
27313
+ const url = opts.relayBaseUrl.replace(/\/+$/, "") + "/api/engine7/provision";
27314
+ const res = await doFetch(url, {
27315
+ method: "POST",
27316
+ headers: { "Content-Type": "application/json" },
27317
+ // 显式列字段,**不要用 `...identity` 展开**:InstallIdentity 加了 contactEmail
27318
+ // 之后,展开会把邮箱也发到发号端点——协议悄悄变了,而服务端只认这三个字段。
27319
+ // (provision-free.test.ts 里有一条断言守这个。)
27320
+ body: JSON.stringify({
27321
+ installId: identity.installId,
27322
+ installSecret: identity.installSecret,
27323
+ engineVersion: readEngineVersion()
27324
+ })
27325
+ });
27326
+ const text = await res.text();
27327
+ let body;
27328
+ try {
27329
+ body = JSON.parse(text);
27330
+ } catch {
27331
+ throw new Error(`HTTP ${res.status}\uFF0C\u54CD\u5E94\u4E0D\u662F JSON\uFF1A${text.slice(0, 200)}`);
27332
+ }
27333
+ if (!res.ok || !body?.success) {
27334
+ throw new Error(body?.message || `HTTP ${res.status}`);
27335
+ }
27336
+ const key = body?.data?.key;
27337
+ if (!key) throw new Error("\u54CD\u5E94\u91CC\u6CA1\u6709 data.key");
27338
+ const secretFile = writeSecretsValue(opts.configPath, FREE_TIER_ENV_KEY, key);
27339
+ return {
27340
+ providerId: body?.data?.providerId || "7yi",
27341
+ // baseUrl 以服务端自报为准:挪机器/换域名只改服务端,不用重发安装包
27342
+ baseUrl: body?.data?.baseUrl || `${opts.relayBaseUrl.replace(/\/+$/, "")}/v1`,
27343
+ envKey: FREE_TIER_ENV_KEY,
27344
+ secretFile,
27345
+ installId: identity.installId
27346
+ };
27347
+ }
27348
+ var __filename, IDENTITY_FILE, FREE_TIER_ENV_KEY;
27349
+ var init_provision_free = __esm({
27350
+ "src/provision-free.ts"() {
27351
+ "use strict";
27352
+ init_secrets_env();
27353
+ __filename = fileURLToPath6(import.meta.url);
27354
+ IDENTITY_FILE = "install-identity.json";
27355
+ FREE_TIER_ENV_KEY = "7YI_API_KEY";
27356
+ }
27357
+ });
27358
+
26566
27359
  // src/cli-reconfig.ts
26567
27360
  var cli_reconfig_exports = {};
26568
27361
  __export(cli_reconfig_exports, {
@@ -26961,8 +27754,7 @@ async function doExport(opts) {
26961
27754
  [DRY RUN] \u4E0D\u5B9E\u9645\u6253\u5305`);
26962
27755
  return;
26963
27756
  }
26964
- const now = /* @__PURE__ */ new Date();
26965
- const version = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
27757
+ const version = formatLog().replace(/[-: ]/g, "").slice(0, 12).replace(/(d{8})(d{4})/, "$1-$2");
26966
27758
  const tmpDir = path32.join(os5.tmpdir(), `engine7-export-${agentName}-${version}`);
26967
27759
  fs30.mkdirSync(tmpDir, { recursive: true });
26968
27760
  const stagingDir = path32.join(tmpDir, agentName);
@@ -27013,7 +27805,7 @@ async function doExport(opts) {
27013
27805
  const manifest = {
27014
27806
  agentName,
27015
27807
  version,
27016
- createdAt: now.toISOString(),
27808
+ createdAt: nowUtc(),
27017
27809
  engineVersion: "7.x",
27018
27810
  originalPaths: {
27019
27811
  WORKSPACE: workspace,
@@ -27303,6 +28095,7 @@ var CONFIG_PATH, PATH_PLACEHOLDERS, WORKSPACE_INCLUDE, WORKSPACE_OPTIONAL, WORKS
27303
28095
  var init_cli_travel = __esm({
27304
28096
  "src/cli-travel.ts"() {
27305
28097
  "use strict";
28098
+ init_clock();
27306
28099
  CONFIG_PATH = path32.join(os5.homedir(), ".engine7-travel.json");
27307
28100
  PATH_PLACEHOLDERS = [
27308
28101
  { placeholder: "{{WORKSPACE}}", getOriginal: (d) => d.workspace },
@@ -27406,19 +28199,28 @@ init_wechat();
27406
28199
  init_qr_render();
27407
28200
  init_cli_events();
27408
28201
  import * as readline3 from "node:readline";
27409
- import { fileURLToPath as fileURLToPath6 } from "node:url";
28202
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
27410
28203
 
27411
28204
  // src/cli-init-answers.ts
28205
+ init_clock();
27412
28206
  import * as fs2 from "node:fs";
27413
28207
 
27414
28208
  // src/cli-init-config.ts
27415
28209
  import * as path2 from "node:path";
27416
28210
  var SCHEMA_VERSION = 1;
28211
+ var FREE_TIER_RELAY_URL = "https://www.twinsun.top:8866";
28212
+ function isFreeTier(v) {
28213
+ return v.freeTier === true || v.primaryProvider === "7yi";
28214
+ }
28215
+ function resolveRelayUrl(relayArg, env = process.env) {
28216
+ return (relayArg || "").trim() || (env.E7_RELAY_URL || "").trim() || FREE_TIER_RELAY_URL;
28217
+ }
27417
28218
  var DEFAULT_PRIMARY_MODEL = {
27418
28219
  dashscope: "dashscope/qwen3.8-max",
27419
28220
  minimax: "minimax/MiniMax-M3",
27420
28221
  zhipu: "zhipu/glm-5.3",
27421
- deepseek: "deepseek/deepseek-v4-pro"
28222
+ deepseek: "deepseek/deepseek-v4-pro",
28223
+ "7yi": "7yi/engine7-free"
27422
28224
  };
27423
28225
  var VISION_CAPABLE = {
27424
28226
  // 0917:原先标 true,但同一个文件里 qwen3.8-max 的 input 是 ['text'],安装器目录
@@ -27444,7 +28246,11 @@ var VISION_CAPABLE = {
27444
28246
  // 声明多一分的代价是上游响亮报错,少一分的代价是无声丢图——选前者。
27445
28247
  "deepseek/deepseek-flash": true,
27446
28248
  "deepseek/deepseek-v4-flash": true,
27447
- "deepseek/deepseek-v4-pro": true
28249
+ "deepseek/deepseek-v4-pro": true,
28250
+ // 免费档(0919):engine7-free 只收文本,engine7-vis-free 才带 image。
28251
+ // 标错会让主模型被当成能看图 → 图发给一个 input=['text'] 的型号 → 静默丢图。
28252
+ "7yi/engine7-free": false,
28253
+ "7yi/engine7-vis-free": true
27448
28254
  };
27449
28255
  var PROVIDER_DEFS = {
27450
28256
  dashscope: {
@@ -27484,6 +28290,41 @@ var PROVIDER_DEFS = {
27484
28290
  // 0917 补 image:官方旗舰已停、9/14 起请求由 V4.1 Flash 承接,实际收图
27485
28291
  { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1e6, maxTokens: 384e3 }
27486
28292
  ]
28293
+ },
28294
+ "7yi": {
28295
+ // 引擎免费档的中转站。baseUrl 是兜底值——真正的权威值来自领号响应
28296
+ // (服务端按请求 Host 自报地址),由安装器作为 customBaseUrl 下发。
28297
+ // 这样挪机器、换域名只改服务端,不用重发安装包。
28298
+ //
28299
+ // ⚠️ 0921 修:这里原本写的是 `https://www.twinsun.top/v1`(443),而 **443 上不是
28300
+ // 中转站**(实测回的是另一个服务的 FastAPI 404)。安装器那条路每次都会下发
28301
+ // customBaseUrl 把它盖掉,所以正式装机撞不到;但**终端向导选 7yi 时不问 baseUrl**,
28302
+ // 直接落到这个兜底值——于是命令行用户拿到的是「号领到了、引擎起来了、一发消息就废」。
28303
+ // 用常量拼,别再手写一遍(漏端口就是这个坑)。
28304
+ baseUrl: `${FREE_TIER_RELAY_URL}/v1`,
28305
+ api: "anthropic",
28306
+ models: [
28307
+ // ⚠️ contextWindow **不能照抄 1000000**。
28308
+ //
28309
+ // engine7-free 是虚拟模型,真实上限取决于当时哪一档在接。写满了会让压缩阈值
28310
+ // 高到永不触发——实测病例(0919 小柯):声明 1M → 阈值 906137,而她的上下文
28311
+ // 已滚到 191080 仍不压缩,最后那条 191k 的请求把 SenseNova 的 TPM 打爆
28312
+ // (429 inference exceeds tpm/rpm limit),整条链退到付费的 MiniMax。
28313
+ //
28314
+ // 实测数据点(0919,SenseNova 那一档):
28315
+ // 114K token → 成功(10s,真实输出)
28316
+ // 191K token → 失败(tpm/rpm 429)
28317
+ // 分界在 114K~191K 之间,中点无数据。
28318
+ //
28319
+ // 阈值与 contextWindow 是比例关系:实测 1,000,000 → 906,137(×0.906)。
28320
+ // 取 150000 → 阈值约 136K,在成功线上方留 22K 余量,又不去赌无数据区。
28321
+ // 等中点实测出来再放宽。
28322
+ { id: "engine7-free", name: "Engine7 \u514D\u8D39\uFF08\u6587\u672C\uFF09", reasoning: true, input: ["text"], contextWindow: 15e4, maxTokens: 64e3 },
28323
+ // input 带 image:实测 Claude 格式的图片块经 /v1/messages 能被上游正确识别
28324
+ { id: "engine7-vis-free", name: "Engine7 \u514D\u8D39\uFF08\u89C6\u89C9\uFF09", reasoning: true, input: ["text", "image"], contextWindow: 15e4, maxTokens: 64e3 }
28325
+ // 注意:engine7-img-free 不在这里。它在 chat 端点是 404(不是 chat 模型),
28326
+ // 生图由 my_selfie 自己拼 /v1/images/edits,relayModel 默认值已经是它。
28327
+ ]
27487
28328
  }
27488
28329
  };
27489
28330
  var MODEL_CHOICES = Object.fromEntries(
@@ -27524,7 +28365,10 @@ function resolveVisionModel(providerId, primaryModel, catalog) {
27524
28365
  }
27525
28366
  var TEMPLATE_VALUES = {
27526
28367
  stateDir: "{{STATE_DIR}}",
28368
+ timezone: "{{TIMEZONE}}",
27527
28369
  agentName: "{{AGENT_NAME}}",
28370
+ persona: "assistant",
28371
+ chatMode: "auto",
27528
28372
  primaryProvider: "dashscope",
27529
28373
  zhipuPlan: "",
27530
28374
  dashscopePlan: "",
@@ -27561,6 +28405,9 @@ function generateConfig(v) {
27561
28405
  const workspace = path2.join(v.stateDir, "workspace").replace(/\\/g, "/");
27562
28406
  const config = {
27563
28407
  schemaVersion: SCHEMA_VERSION,
28408
+ // 全引擎统一时区(design §3.1)。真实 init 走 getDefaultValues() 探测本机,
28409
+ // 模板走 {{TIMEZONE}} 占位符。
28410
+ timezone: v.timezone,
27564
28411
  stateDir: v.stateDir.replace(/\\/g, "/"),
27565
28412
  mediaDir: `${v.stateDir.replace(/\\/g, "/")}/media/inbound`,
27566
28413
  models: {
@@ -27570,7 +28417,19 @@ function generateConfig(v) {
27570
28417
  // 后者是模板里遗留的死键(全仓零读者),已删。visionModel 解析不出就不写这个块。
27571
28418
  tools: {
27572
28419
  ...v.tavilyKey ? { tavily: { apiKey: v.tavilyKey } } : {},
27573
- ...v.visionModel ? { my_eyes: { model: v.visionModel } } : {}
28420
+ ...v.visionModel ? { my_eyes: { model: v.visionModel } } : {},
28421
+ // 自拍(§4.3):免费档必须显式写 provider:'relay'。不写的话 getProvider()
28422
+ // 会落回默认值 'fal',而免费装机没有 fal key——第一次自拍就失败。
28423
+ // 非免费档只在用户真给了形象时才写这个块,且不替他决定 provider。
28424
+ ...isFreeTier(v) || v.selfiePhotoRelPath || v.selfiePortraitDesc ? {
28425
+ my_selfie: {
28426
+ ...isFreeTier(v) ? { provider: "relay" } : {},
28427
+ ...v.selfiePhotoRelPath ? { references: [{ name: "portrait", path: v.selfiePhotoRelPath }] } : {},
28428
+ // 只在没有照片时才写描述——两个都给的话以照片为准,写进去会让人以为
28429
+ // 描述也生效了(冷启读的就是这个键)
28430
+ ...!v.selfiePhotoRelPath && v.selfiePortraitDesc ? { portraitDesc: v.selfiePortraitDesc } : {}
28431
+ }
28432
+ } : {}
27574
28433
  },
27575
28434
  session: { dmScope: "main", groupScope: "main" },
27576
28435
  sandbox: { mode: "off" },
@@ -27586,6 +28445,10 @@ function generateConfig(v) {
27586
28445
  defaults: {
27587
28446
  workspace,
27588
28447
  name: v.agentName,
28448
+ // 人设落 config 是为了**复跑能读回来**(loadExistingValues)。引擎运行时不读它
28449
+ // ——SOUL.md 已经按人设渲染好了。不写的话复跑 init 会退回 'assistant',
28450
+ // 而 execute() 用模板重写 SOUL.md,用户选的"情感陪伴"重装一次就变回通用助手。
28451
+ persona: v.persona,
27589
28452
  model: {
27590
28453
  primary: v.primaryModel,
27591
28454
  // 条件写入:vision 引用解析不出时省略键(loader 按 if (visionRef) 处理缺失)
@@ -27605,7 +28468,10 @@ function generateConfig(v) {
27605
28468
  agentTeams: true,
27606
28469
  processOutput: "minimal",
27607
28470
  interrupt: "command",
27608
- debounceMs: 5e3
28471
+ debounceMs: 5e3,
28472
+ // 免费档的装机目标是「能对话、能看图、能自拍」,这两个 flag 默认 false
28473
+ // (src/config/features.ts)。显式打开,它们是声明这两件事的地方。
28474
+ ...isFreeTier(v) ? { selfie: true, eyes: true } : {}
27609
28475
  }
27610
28476
  }
27611
28477
  },
@@ -27633,7 +28499,15 @@ function generateConfig(v) {
27633
28499
  dmPolicy: "pairing",
27634
28500
  group: { policy: "disabled" }
27635
28501
  }
27636
- } : {}
28502
+ } : {},
28503
+ // 0920 翀哥拍板:emotion 通道默认开启(含 liveSystemPrompt 加载 EMOTION.md)
28504
+ // 即使没装 EMOTION.md,buildLightStablePrompt 也会硬编码"想 TA 就发"那段兜底
28505
+ emotion: {
28506
+ enabled: true,
28507
+ liveSystemPrompt: {
28508
+ files: ["SOUL.md", "EMOTION.md"]
28509
+ }
28510
+ }
27637
28511
  },
27638
28512
  api: { port: v.apiPort },
27639
28513
  // 0909 npm 包默认开:inner-voice + proactive 是开箱即用的核心体验
@@ -27686,6 +28560,9 @@ function generateConfig(v) {
27686
28560
  if (v.primaryProvider === "dashscope" && v.dashscopePlan === "token") {
27687
28561
  def.baseUrl = "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
27688
28562
  }
28563
+ if (v.primaryProvider === "7yi" && (v.customBaseUrl || "").trim()) {
28564
+ def.baseUrl = v.customBaseUrl.trim();
28565
+ }
27689
28566
  if (v.providerModels && v.providerModels.length > 0) {
27690
28567
  def.models = v.providerModels.map((m) => ({
27691
28568
  id: m.id,
@@ -27741,14 +28618,32 @@ function generateConfig(v) {
27741
28618
  }
27742
28619
 
27743
28620
  // src/cli-init-answers.ts
27744
- var VALID_PROVIDERS = ["dashscope", "minimax", "zhipu", "deepseek", "custom"];
28621
+ var VALID_PROVIDERS = ["dashscope", "minimax", "zhipu", "deepseek", "7yi", "custom"];
27745
28622
  var VALID_CHANNEL_MODES = ["skip", "qr", "manual"];
28623
+ var VALID_PERSONAS = ["assistant", "companion", "butler", "friend"];
28624
+ var VALID_CHAT_MODES = ["auto", "emotion", "work"];
27746
28625
  function validate(a) {
27747
28626
  if (!a || typeof a !== "object") throw new Error("answers \u6587\u4EF6\u5185\u5BB9\u4E0D\u662F JSON \u5BF9\u8C61");
27748
28627
  if (!VALID_PROVIDERS.includes(a.provider)) {
27749
28628
  throw new Error(`provider \u975E\u6CD5: ${a.provider}\uFF08\u53EF\u9009: ${VALID_PROVIDERS.join(" / ")}\uFF09`);
27750
28629
  }
27751
28630
  if (typeof a.apiKey !== "string") throw new Error("apiKey \u5FC5\u586B\uFF08\u53EF\u4E3A\u7A7A\u4E32\uFF0C\u8DF3\u8FC7 secrets \u5199\u5165\uFF09");
28631
+ if (a.persona !== void 0 && !VALID_PERSONAS.includes(a.persona)) {
28632
+ throw new Error(`persona \u975E\u6CD5: ${a.persona}\uFF08\u53EF\u9009: ${VALID_PERSONAS.join(" / ")}\uFF09`);
28633
+ }
28634
+ if (a.chatMode !== void 0 && !VALID_CHAT_MODES.includes(a.chatMode)) {
28635
+ throw new Error(`chatMode \u975E\u6CD5: ${a.chatMode}\uFF08\u53EF\u9009: ${VALID_CHAT_MODES.join(" / ")}\uFF09`);
28636
+ }
28637
+ if (a.timezone !== void 0 && !isValidTimezone(a.timezone)) {
28638
+ throw new Error(`timezone \u975E\u6CD5: ${a.timezone}\uFF08\u8981 IANA \u540D\uFF0C\u5982 Asia/Shanghai\u3001Europe/Malta\uFF09`);
28639
+ }
28640
+ if (a.contactEmail !== void 0 && a.contactEmail !== null && a.contactEmail !== "") {
28641
+ if (typeof a.contactEmail !== "string") throw new Error("contactEmail \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
28642
+ const t = a.contactEmail.trim();
28643
+ if (t && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)) {
28644
+ throw new Error(`contactEmail \u4E0D\u662F\u5408\u6CD5\u90AE\u7BB1: ${t}`);
28645
+ }
28646
+ }
27752
28647
  if (a.feishu?.mode !== void 0 && !VALID_CHANNEL_MODES.includes(a.feishu.mode)) {
27753
28648
  throw new Error(`feishu.mode \u975E\u6CD5: ${a.feishu.mode}\uFF08\u53EF\u9009: ${VALID_CHANNEL_MODES.join(" / ")}\uFF09`);
27754
28649
  }
@@ -27777,6 +28672,11 @@ function loadInitAnswers(filePath) {
27777
28672
  throw new Error(`answers \u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON: ${e.message}`);
27778
28673
  }
27779
28674
  validate(parsed);
28675
+ if (typeof parsed.contactEmail === "string") {
28676
+ const t = parsed.contactEmail.trim();
28677
+ if (t) parsed.contactEmail = t;
28678
+ else delete parsed.contactEmail;
28679
+ }
27780
28680
  return parsed;
27781
28681
  }
27782
28682
  function buildConfigValuesFromAnswers(stateDir, a, existing = {}) {
@@ -27793,7 +28693,17 @@ function buildConfigValuesFromAnswers(stateDir, a, existing = {}) {
27793
28693
  const wechatMode = a.wechat?.mode || "skip";
27794
28694
  return {
27795
28695
  stateDir,
28696
+ // 时区的三级优先(0920):用户显式改过 > 已有值 > 探测本机。
28697
+ //
28698
+ // 中间那级是"复跑保护"——装完手改过时区的人,重跑 init 不该被本机探测值冲掉。
28699
+ // 安装器只在用户**动过向导里那个框**时才下发 a.timezone(默认显示的就是探测值,
28700
+ // 原样发回来会把中间这级架空)。不传时这条与 --quick 路径产出全等
28701
+ // (cli-init.headless.e2e.test.ts 的 deepEqual 基准)。
28702
+ timezone: a.timezone || existing.timezone || detectHostTimezone(),
27796
28703
  agentName: a.agentName || existing.agentName || "Agent",
28704
+ // 人设类型:headless 由安装器下发,缺省 assistant(0919 从上游合入)
28705
+ persona: a.persona || "assistant",
28706
+ chatMode: a.chatMode || "auto",
27797
28707
  primaryProvider,
27798
28708
  zhipuPlan: a.zhipuPlan || "",
27799
28709
  dashscopePlan: a.dashscopePlan || "",
@@ -27816,21 +28726,30 @@ function buildConfigValuesFromAnswers(stateDir, a, existing = {}) {
27816
28726
  customApi: a.customApi || "anthropic",
27817
28727
  customProviderId: a.provider === "custom" ? a.customProviderId || existing.customProviderId || "custom" : "",
27818
28728
  tavilyKey: a.tavilyKey || "",
27819
- apiPort: a.apiPort || existing.apiPort || 16990
28729
+ apiPort: a.apiPort || existing.apiPort || 16990,
28730
+ freeTier: a.freeTier === true,
28731
+ // 照片两个都给时以照片为准(安装器侧 UI 也这么说明);photoRelPath 由 init 主链路
28732
+ // 在校验通过、拷进 workspace 之后回填,这里只带描述。
28733
+ selfiePortraitDesc: a.selfie?.photoPath ? "" : (a.selfie?.portraitDesc || "").trim()
27820
28734
  };
27821
28735
  }
27822
28736
 
27823
28737
  // src/cli-init.ts
27824
- var origEmitWarning = process.emitWarning.bind(process);
27825
- process.emitWarning = (warning, ...rest) => {
27826
- const name = typeof warning === "object" ? warning?.name : rest?.[0];
27827
- const msg2 = typeof warning === "object" ? String(warning?.message) : String(warning);
27828
- if (name === "ExperimentalWarning" && msg2.includes("SQLite")) return;
27829
- return origEmitWarning(warning, ...rest);
27830
- };
28738
+ init_secrets_env();
28739
+ init_clock();
28740
+ var defaultWarningListeners = process.listeners("warning");
28741
+ process.removeAllListeners("warning");
28742
+ process.on("warning", (warning) => {
28743
+ if (warning?.name === "ExperimentalWarning" && String(warning.message).includes("SQLite")) return;
28744
+ if (defaultWarningListeners.length === 0) {
28745
+ console.warn(`${warning.name}: ${warning.message}`);
28746
+ return;
28747
+ }
28748
+ for (const listener of defaultWarningListeners) listener(warning);
28749
+ });
27831
28750
  var headlessEmit = null;
27832
- var __filename = fileURLToPath6(import.meta.url);
27833
- var __dirname = path33.dirname(__filename);
28751
+ var __filename2 = fileURLToPath7(import.meta.url);
28752
+ var __dirname = path33.dirname(__filename2);
27834
28753
  function parseArgs() {
27835
28754
  const args = process.argv.slice(2);
27836
28755
  let stateDir = "";
@@ -27865,7 +28784,7 @@ function parseArgs() {
27865
28784
  }
27866
28785
  function printHelp() {
27867
28786
  console.log(`
27868
- Engine 7 \u2014 Self-hosted AI agent engine
28787
+ Engine 7 \u2014 \u7ED9\u52A9\u624B\u4E00\u4E2A\u5BB6 / A home for your AI assistant
27869
28788
 
27870
28789
  \u7528\u6CD5:
27871
28790
  engine7 init --state-dir <path> [\u9009\u9879] \u521D\u59CB\u5316 agent \u5DE5\u4F5C\u76EE\u5F55
@@ -27938,7 +28857,11 @@ function getDefaultValues(stateDir) {
27938
28857
  const primaryModel = existing.primaryModel || "dashscope/qwen3.8-max";
27939
28858
  return {
27940
28859
  stateDir,
28860
+ // 安装时取本机时区(design §3.1);复跑 init 沿用已配置的值,不覆盖用户改过的
28861
+ timezone: existing.timezone || detectHostTimezone(),
27941
28862
  agentName: existing.agentName || "Agent",
28863
+ persona: existing.persona || "assistant",
28864
+ chatMode: existing.chatMode || "auto",
27942
28865
  primaryProvider,
27943
28866
  zhipuPlan: existing.zhipuPlan || "",
27944
28867
  dashscopePlan: existing.dashscopePlan || "",
@@ -27976,7 +28899,15 @@ function loadExistingValues(stateDir) {
27976
28899
  if (!fs31.existsSync(configPath)) return {};
27977
28900
  const j = JSON.parse(fs31.readFileSync(configPath, "utf8"));
27978
28901
  const out = {};
28902
+ if (typeof j.timezone === "string" && j.timezone) out.timezone = j.timezone;
27979
28903
  if (j.agents?.defaults?.name) out.agentName = j.agents.defaults.name;
28904
+ if (j.agents?.defaults?.persona) out.persona = j.agents.defaults.persona;
28905
+ try {
28906
+ const wm = fs31.readFileSync(path33.join(stateDir, "workspace", ".work-mode"), "utf8").trim();
28907
+ if (wm === "on") out.chatMode = "work";
28908
+ else if (wm === "off") out.chatMode = "emotion";
28909
+ } catch {
28910
+ }
27980
28911
  const providers = j.models?.providers || {};
27981
28912
  const firstId = Object.keys(providers)[0];
27982
28913
  if (firstId) {
@@ -28050,12 +28981,27 @@ async function interactiveConfig(rl, defaults) {
28050
28981
  const v = { ...defaults };
28051
28982
  console.log("\n=== \u7B2C\u4E00\u6B65\uFF1A\u5FC5\u586B\u914D\u7F6E ===\n");
28052
28983
  v.agentName = await ask(rl, "Agent \u540D\u79F0", defaults.agentName);
28053
- const providers = ["dashscope (\u901A\u4E49\u5343\u95EE)", "minimax (MiniMax)", "zhipu (\u667A\u8C31)", "deepseek (DeepSeek)", "custom (\u81EA\u5B9A\u4E49/\u5176\u4ED6 Anthropic \u517C\u5BB9\u7AEF\u70B9)"];
28984
+ const personas = ["assistant (\u901A\u7528\u52A9\u624B\xB7\u4E2D\u6027)", "companion (\u60C5\u611F\u966A\u4F34)", "butler (\u7BA1\u5BB6/\u751F\u6D3B\u52A9\u7406)", "friend (\u670B\u53CB\u5B66\u4F34)"];
28985
+ const personaMap = {
28986
+ "assistant (\u901A\u7528\u52A9\u624B\xB7\u4E2D\u6027)": "assistant",
28987
+ "companion (\u60C5\u611F\u966A\u4F34)": "companion",
28988
+ "butler (\u7BA1\u5BB6/\u751F\u6D3B\u52A9\u7406)": "butler",
28989
+ "friend (\u670B\u53CB\u5B66\u4F34)": "friend"
28990
+ };
28991
+ console.log(" \u8FD9\u51B3\u5B9A TA \u7684\u6027\u683C\u548C\u60C5\u7EEA\u8868\u8FBE\uFF1B\u4E0D\u786E\u5B9A\u5C31\u9009 1\uFF0C\u4E4B\u540E\u968F\u65F6\u53EF\u6539\uFF08\u6539 workspace \u6587\u4EF6\u7ACB\u5373\u751F\u6548\uFF09");
28992
+ const chosenPersona = await askChoice(rl, "\u4EBA\u8BBE\u7C7B\u578B\uFF08\u60C5\u7EEA\u72B6\u6001\u8868\u6A21\u677F\uFF09:", personas, 0, defaults.persona);
28993
+ v.persona = personaMap[chosenPersona] || "assistant";
28994
+ const providers = ["dashscope (\u901A\u4E49\u5343\u95EE)", "minimax (MiniMax)", "zhipu (\u667A\u8C31)", "deepseek (DeepSeek)", "7yi (\u514D\u8D39\u6863\xB7\u4E2D\u8F6C\u7AD9)", "custom (\u81EA\u5B9A\u4E49/\u5176\u4ED6 Anthropic \u517C\u5BB9\u7AEF\u70B9)"];
28054
28995
  const providerMap = {
28055
28996
  "dashscope (\u901A\u4E49\u5343\u95EE)": "dashscope",
28056
28997
  "minimax (MiniMax)": "minimax",
28057
28998
  "zhipu (\u667A\u8C31)": "zhipu",
28058
28999
  "deepseek (DeepSeek)": "deepseek",
29000
+ // 免费档列在这里是为了"复跑不换 provider":7yi 是内置 provider,menuProviderOf('7yi')
29001
+ // 现在返回 '7yi',菜单里没有这一项就会匹配失败、退回第一项 dashscope——
29002
+ // 于是复跑终端向导会把装好的免费档悄悄换成通义千问。令牌早已在 secrets 里,
29003
+ // 这一步不需要重新领号;要换 provider 请显式选别的项。
29004
+ "7yi (\u514D\u8D39\u6863\xB7\u4E2D\u8F6C\u7AD9)": "7yi",
28059
29005
  "custom (\u81EA\u5B9A\u4E49/\u5176\u4ED6 Anthropic \u517C\u5BB9\u7AEF\u70B9)": "custom"
28060
29006
  };
28061
29007
  const chosenProvider = await askChoice(rl, "\u4E3B\u6A21\u578B Provider:", providers, 0, menuProviderOf(defaults.primaryProvider));
@@ -28253,7 +29199,6 @@ async function configureWechat(rl, v) {
28253
29199
  function buildDirTree(v) {
28254
29200
  const d = v.stateDir;
28255
29201
  const w = path33.join(d, "workspace");
28256
- const now = (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { timeZone: "Asia/Shanghai", hour12: false });
28257
29202
  return [
28258
29203
  // 目录
28259
29204
  { path: path33.join(d, "configs"), type: "dir" },
@@ -28271,23 +29216,93 @@ function buildDirTree(v) {
28271
29216
  {
28272
29217
  path: path33.join(w, "SESSION-STATE.md"),
28273
29218
  type: "file",
28274
- content: readTemplate("workspace/SESSION-STATE.md").replace("{{CURRENT_TIME}}", now)
29219
+ content: readTemplate("workspace/SESSION-STATE.md")
28275
29220
  },
28276
29221
  { path: path33.join(w, "HEARTBEAT.md"), type: "file", content: readTemplate("workspace/HEARTBEAT.md") },
28277
29222
  {
28278
29223
  path: path33.join(w, "SOUL.md"),
28279
29224
  type: "file",
28280
- content: readTemplate("workspace/SOUL.md").replace(/\{\{AGENT_NAME\}\}/g, v.agentName)
29225
+ // 0917:SOUL 按人设取模板(templates/workspace/souls/<persona>.md),
29226
+ // 单一人设模板缺失时回退通用骨架(workspace/SOUL.md)
29227
+ content: (() => {
29228
+ try {
29229
+ return readTemplate(`workspace/souls/${v.persona}.md`).replace(/\{\{AGENT_NAME\}\}/g, v.agentName);
29230
+ } catch {
29231
+ return readTemplate("workspace/SOUL.md").replace(/\{\{AGENT_NAME\}\}/g, v.agentName);
29232
+ }
29233
+ })()
28281
29234
  },
28282
29235
  { path: path33.join(w, "AGENTS.md"), type: "file", content: readTemplate("workspace/AGENTS.md") },
29236
+ {
29237
+ // 0920 翀哥拍板:EMOTION.md 模板按人设装(templates/workspace/emotion/<persona>.md)
29238
+ // 冷启动默认带"想 TA 了就直接发(msg_send)"那段(写在每份模板 §一)
29239
+ // 单一人设模板缺失时回退通用骨架(emotion/_default.md)
29240
+ path: path33.join(w, "EMOTION.md"),
29241
+ type: "file",
29242
+ content: (() => {
29243
+ try {
29244
+ return readTemplate(`workspace/emotion/${v.persona}.md`).replace(/\{\{AGENT_NAME\}\}/g, v.agentName);
29245
+ } catch {
29246
+ return readTemplate("workspace/emotion/_default.md").replace(/\{\{AGENT_NAME\}\}/g, v.agentName);
29247
+ }
29248
+ })()
29249
+ },
28283
29250
  {
28284
29251
  path: path33.join(w, "prompts", "contacts.md"),
28285
29252
  type: "file",
28286
29253
  content: readTemplate("workspace/prompts/contacts.md").replace("{{DISCORD_USER_ID}}", v.discordUserId || "YOUR_DISCORD_ID").replace("{{FEISHU_OPEN_ID}}", v.feishuOpenId || "YOUR_FEISHU_OPEN_ID")
28287
29254
  },
28288
29255
  { path: path33.join(w, "prompts", "auto-memory-instructions.md"), type: "file", content: readTemplate("workspace/prompts/auto-memory-instructions.md") },
29256
+ {
29257
+ // 0917:inner-voice 定制 prompt 按人设装(templates/workspace/inner-voice/<persona>.md)
29258
+ path: path33.join(w, "prompts", "my-inner-voice.md"),
29259
+ type: "file",
29260
+ content: readTemplate(`workspace/inner-voice/${v.persona}.md`).replace(/\{\{AGENT_NAME\}\}/g, v.agentName)
29261
+ },
28289
29262
  { path: path33.join(w, "MEMORY.md"), type: "file", content: "# MEMORY.md \u2014 \u8BB0\u5FC6\u6587\u4EF6\u7D22\u5F15\n\n> \u6700\u540E\u66F4\u65B0\uFF1A\u521D\u59CB\u5316\n" },
28290
29263
  { path: path33.join(w, "USER.md"), type: "file", content: "# USER.md \u2014 \u7528\u6237\u4FE1\u606F\n\n\uFF08\u5728\u8FD9\u91CC\u8BB0\u5F55\u7528\u6237\u7684\u504F\u597D\u3001\u80CC\u666F\u7B49\uFF09\n" },
29264
+ // 情绪状态机(0917):脚本 + hook 接线 + 人设情绪表(用户在 init 选的 persona)
29265
+ //
29266
+ // 0920 起装的是 **.mjs 版**(原来是 .py,见下面 hook 命令那段)。老装机的 workspace
29267
+ // 里那份 .py 不动它——没被引用就不会跑,删了反而无据可查。
29268
+ {
29269
+ path: path33.join(w, "scripts", "emotion_state_machine.mjs"),
29270
+ type: "file",
29271
+ content: readTemplate("workspace/scripts/emotion_state_machine.mjs")
29272
+ },
29273
+ {
29274
+ path: path33.join(w, ".hooks.json"),
29275
+ type: "file",
29276
+ content: JSON.stringify({
29277
+ hooks: {
29278
+ UserPromptSubmit: [
29279
+ // `${E7_NODE}` 由引擎在加载 hook 时替换成它**自己的可执行文件**(hooks/config.ts)。
29280
+ // 原来写的是 `python3 <脚本.py>` —— 而用户机器上不一定有 Python(Windows 不自带;
29281
+ // 微软商店那个 WindowsApps\python3.exe 只是个跳商店的别名)。没有解释器时这个 hook
29282
+ // 静默失败,而 handle-query 的 chatMode 默认是 work,于是"自动"档(默认档)在用户
29283
+ // 机器上退化成固定工作模式。
29284
+ //
29285
+ // 换成引擎自己的 exe 后**零外部依赖**:那个 exe 带着 ELECTRON_RUN_AS_NODE=1 被 spawn
29286
+ // (hook 的 env 是 {...process.env} 继承的),所以它就是个 node。
29287
+ // 与 `service install --node-exe` 同一个路子。
29288
+ // 路径两侧都加引号:stateDir 可能带空格(C:\Users\Foo Bar\...)。
29289
+ { hooks: [{ type: "command", command: '"${E7_NODE}" "' + path33.join(w, "scripts", "emotion_state_machine.mjs") + '"' }] }
29290
+ ]
29291
+ }
29292
+ }, null, 2) + "\n"
29293
+ },
29294
+ {
29295
+ path: path33.join(w, ".emotion_states.json"),
29296
+ type: "file",
29297
+ content: readTemplate(`workspace/emotion-states/${v.persona}.json`)
29298
+ },
29299
+ // 模式档位(0920):与 /work 命令写的是同一个文件,读端只认 on/off,
29300
+ // 没有这个键就是 auto。auto 刻意不写——写个 "auto" 进去反而多一种除了这里没人认得的状态。
29301
+ ...v.chatMode === "work" || v.chatMode === "emotion" ? [{
29302
+ path: path33.join(w, ".work-mode"),
29303
+ type: "file",
29304
+ content: v.chatMode === "work" ? "on" : "off"
29305
+ }] : [],
28291
29306
  // package.json — 让 agent 目录成为独立 npm 项目根,防止 npm hoisting
28292
29307
  {
28293
29308
  path: path33.join(d, "package.json"),
@@ -28380,6 +29395,15 @@ function readTemplate(relativePath) {
28380
29395
  function looksLikeAppInstall(dir) {
28381
29396
  return ["", "install"].some((sub) => fs31.existsSync(path33.join(dir, sub, "Engine7.exe")) && fs31.existsSync(path33.join(dir, sub, "resources", "engine7", "dist", "cli.mjs")));
28382
29397
  }
29398
+ var PHOTO_EXTS = [".png", ".jpg", ".jpeg", ".webp"];
29399
+ function hasOnlyEngineBookkeeping(dir) {
29400
+ try {
29401
+ const entries2 = fs31.readdirSync(dir);
29402
+ return entries2.length > 0 && entries2.every((e) => e === "install-identity.json");
29403
+ } catch {
29404
+ return false;
29405
+ }
29406
+ }
28383
29407
  function dryRun(entries2) {
28384
29408
  console.log("\n[dry-run] \u5C06\u8981\u521B\u5EFA\uFF1A\n");
28385
29409
  for (const e of entries2) {
@@ -28428,7 +29452,7 @@ async function main() {
28428
29452
  if (subcommand === "--version" || subcommand === "-v") {
28429
29453
  let ver = "unknown";
28430
29454
  try {
28431
- const pkgPath = path33.join(path33.dirname(__filename), "..", "package.json");
29455
+ const pkgPath = path33.join(path33.dirname(__filename2), "..", "package.json");
28432
29456
  ver = JSON.parse(fs31.readFileSync(pkgPath, "utf8")).version || "unknown";
28433
29457
  } catch {
28434
29458
  }
@@ -28441,6 +29465,35 @@ async function main() {
28441
29465
  console.log(JSON.stringify(await runDoctor2(stateDir), null, 2));
28442
29466
  process.exit(0);
28443
29467
  }
29468
+ if (subcommand === "provision-free") {
29469
+ let relay = "";
29470
+ let relayFromArg = false;
29471
+ let stateDir = "";
29472
+ for (let i = 1; i < args.length; i++) {
29473
+ if (args[i] === "--relay" && args[i + 1]) {
29474
+ relay = args[++i].trim();
29475
+ relayFromArg = true;
29476
+ } else if (args[i] === "--state-dir" && args[i + 1]) stateDir = args[++i].trim();
29477
+ }
29478
+ relay = resolveRelayUrl(relay, process.env);
29479
+ if (!relayFromArg) console.error(` \u4E2D\u8F6C\u7AD9: ${relay}\uFF08\u8981\u6362\u5730\u5740\u7528 --relay <\u5730\u5740>\uFF09`);
29480
+ const { resolveStateDirFromArgs: resolveStateDirFromArgs2 } = await Promise.resolve().then(() => (init_cli_doctor(), cli_doctor_exports));
29481
+ stateDir = stateDir ? path33.resolve(stateDir) : resolveStateDirFromArgs2(args.slice(1));
29482
+ try {
29483
+ const { provisionFreeToken: provisionFreeToken2 } = await Promise.resolve().then(() => (init_provision_free(), provision_free_exports));
29484
+ const result = await provisionFreeToken2({
29485
+ relayBaseUrl: relay,
29486
+ stateDir,
29487
+ // 和 init 写 config 的路径一致(cli-init.ts 里 configPath 就是这么拼的)
29488
+ configPath: path33.join(stateDir, "configs", "main7.json")
29489
+ });
29490
+ console.log(JSON.stringify(result, null, 2));
29491
+ process.exit(0);
29492
+ } catch (e) {
29493
+ console.error(`\u9886\u53F7\u5931\u8D25\uFF1A${e?.message || e}`);
29494
+ process.exit(1);
29495
+ }
29496
+ }
28444
29497
  if (subcommand === "reconfig" || subcommand === "addchannel") {
28445
29498
  const isLegacyAddchannel = subcommand === "addchannel";
28446
29499
  let target = args[1] || "";
@@ -28811,7 +29864,7 @@ engine7 session \u2014 \u4F1A\u8BDD\u7BA1\u7406\u5DE5\u5177
28811
29864
  }
28812
29865
  }
28813
29866
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28814
- const { readFileSync: readFileSync42, writeFileSync: writeFileSync22, renameSync: renameSync5 } = fs31;
29867
+ const { readFileSync: readFileSync43, writeFileSync: writeFileSync24, renameSync: renameSync5 } = fs31;
28815
29868
  if (mode === "archive") {
28816
29869
  const archiveName = `${sessionFileUUID}.jsonl.archived.${timestamp}`;
28817
29870
  renameSync5(jsonlPath, path33.join(sessionsDir, archiveName));
@@ -28819,7 +29872,7 @@ engine7 session \u2014 \u4F1A\u8BDD\u7BA1\u7406\u5DE5\u5177
28819
29872
  \u2705 \u5F52\u6863\u5B8C\u6210: ${archiveName}`);
28820
29873
  console.log(` \u91CD\u542F\u540E\u5C06\u4ECE\u7A7A\u767D\u4F1A\u8BDD\u5F00\u59CB`);
28821
29874
  } else if (mode === "drop-last") {
28822
- const lines = readFileSync42(jsonlPath, "utf-8").trim().split("\n");
29875
+ const lines = readFileSync43(jsonlPath, "utf-8").trim().split("\n");
28823
29876
  const parsed = lines.map((l) => {
28824
29877
  try {
28825
29878
  return JSON.parse(l);
@@ -28838,12 +29891,12 @@ engine7 session \u2014 \u4F1A\u8BDD\u7BA1\u7406\u5DE5\u5177
28838
29891
  }
28839
29892
  const kept = parsed.slice(0, cutIndex);
28840
29893
  const newContent = kept.map((p) => JSON.stringify(p)).join("\n") + "\n";
28841
- writeFileSync22(jsonlPath, newContent);
29894
+ writeFileSync24(jsonlPath, newContent);
28842
29895
  console.log(`
28843
29896
  \u2705 \u780D\u6389\u6700\u8FD1 ${parsed.length - cutIndex} \u884C\uFF08${dropN} \u8F6E\uFF09`);
28844
29897
  console.log(` \u4FDD\u7559 ${kept.length} \u884C`);
28845
29898
  } else if (mode === "strip-images") {
28846
- const lines = readFileSync42(jsonlPath, "utf-8").trim().split("\n");
29899
+ const lines = readFileSync43(jsonlPath, "utf-8").trim().split("\n");
28847
29900
  let stripped = 0;
28848
29901
  const newLines = lines.map((line) => {
28849
29902
  try {
@@ -28884,7 +29937,7 @@ engine7 session \u2014 \u4F1A\u8BDD\u7BA1\u7406\u5DE5\u5177
28884
29937
  return line;
28885
29938
  }
28886
29939
  });
28887
- writeFileSync22(jsonlPath, newLines.join("\n") + "\n");
29940
+ writeFileSync24(jsonlPath, newLines.join("\n") + "\n");
28888
29941
  console.log(`
28889
29942
  \u2705 \u6458\u9664 ${stripped} \u884C\u56FE\u7247\u76F8\u5173\u5185\u5BB9`);
28890
29943
  }
@@ -28986,6 +30039,13 @@ engine7 service \u2014 \u5F00\u673A\u81EA\u542F\u52A8\u7BA1\u7406
28986
30039
  const absCfg = path33.resolve(configPath2);
28987
30040
  const vbs = [
28988
30041
  "' Engine 7 \u5F00\u673A\u81EA\u542F\uFF08\u542F\u52A8\u6587\u4EF6\u5939\uFF0C\u514D\u7BA1\u7406\u5458\uFF09",
30042
+ 'Set fso = CreateObject("Scripting.FileSystemObject")',
30043
+ 'exePath = "' + nodeExePath + '"',
30044
+ "If Not fso.FileExists(exePath) Then",
30045
+ " On Error Resume Next",
30046
+ " fso.DeleteFile WScript.ScriptFullName, True",
30047
+ " WScript.Quit 0",
30048
+ "End If",
28989
30049
  'Set s = CreateObject("WScript.Shell")',
28990
30050
  's.Environment("PROCESS")("ELECTRON_RUN_AS_NODE") = "1"',
28991
30051
  's.CurrentDirectory = "' + cwd + '"',
@@ -29138,6 +30198,12 @@ WantedBy=default.target`;
29138
30198
  }
29139
30199
  process.exit(0);
29140
30200
  }
30201
+ const INIT_FLAGS = /* @__PURE__ */ new Set(["--state-dir", "--quick", "--dry-run", "--force", "--answers"]);
30202
+ if (subcommand !== "init" && !INIT_FLAGS.has(subcommand)) {
30203
+ console.error(`\u274C \u672A\u77E5\u547D\u4EE4: ${subcommand}`);
30204
+ printHelp();
30205
+ process.exit(1);
30206
+ }
29141
30207
  const opts = parseArgs();
29142
30208
  if (opts.answersPath) {
29143
30209
  redirectConsoleToStderr();
@@ -29170,6 +30236,9 @@ WantedBy=default.target`;
29170
30236
  } else if (looksLikeAppInstall(opts.stateDir)) {
29171
30237
  console.log(`
29172
30238
  \u{1F4E6} \u68C0\u6D4B\u5230 Engine7 \u7A0B\u5E8F\u672C\u4F53\uFF08\u5B89\u88C5\u5668\u88C5\u7684\uFF09\u2014\u2014\u5728\u5176\u65C1\u8FB9\u521D\u59CB\u5316 agent \u6570\u636E`);
30239
+ } else if (hasOnlyEngineBookkeeping(opts.stateDir)) {
30240
+ console.log(`
30241
+ \u{1F511} \u68C0\u6D4B\u5230\u88C5\u673A\u8EAB\u4EFD\uFF08\u9886\u53F7\u7559\u4E0B\u7684\uFF09\u2014\u2014 \u5728\u5176\u65C1\u8FB9\u521D\u59CB\u5316 agent \u6570\u636E`);
29173
30242
  } else {
29174
30243
  if (headlessEmit) headlessEmit({ event: "error", message: `\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A: ${opts.stateDir}\uFF08\u65E0 configs/main7.json\uFF09\u2014\u2014\u8BF7\u9009\u62E9\u7A7A\u76EE\u5F55\u6216\u7528 --force \u6E05\u7A7A\u540E\u91CD\u65B0\u914D\u7F6E` });
29175
30244
  console.error(`
@@ -29224,6 +30293,38 @@ WantedBy=default.target`;
29224
30293
  values = await interactiveConfig(rl, getDefaultValues(opts.stateDir));
29225
30294
  rl.close();
29226
30295
  }
30296
+ {
30297
+ const { loadOrCreateInstallIdentity: loadOrCreateInstallIdentity2, writeIdentityContactEmail: writeIdentityContactEmail2 } = await Promise.resolve().then(() => (init_provision_free(), provision_free_exports));
30298
+ loadOrCreateInstallIdentity2(opts.stateDir);
30299
+ const contactEmail = (answers?.contactEmail || "").trim();
30300
+ if (contactEmail) writeIdentityContactEmail2(opts.stateDir, contactEmail);
30301
+ }
30302
+ if (isFreeTier(values)) {
30303
+ const envName = values.primaryProvider.toUpperCase().replace(/[^A-Z0-9_]/g, "_") + "_API_KEY";
30304
+ if (!readSecretsValue(opts.stateDir, envName)) {
30305
+ throw new Error(
30306
+ `\u514D\u8D39\u6863\u7F3A\u5C11\u4EE4\u724C\uFF1Asecrets \u91CC\u6CA1\u6709 ${envName}\u3002\u8BF7\u5148\u6267\u884C engine7 provision-free --state-dir ${opts.stateDir} \u9886\u53F7\uFF0C\u518D\u8DD1 init\u3002`
30307
+ );
30308
+ }
30309
+ }
30310
+ let photoSrc = "";
30311
+ if (answers?.selfie?.photoPath) {
30312
+ const { dimsOf: dimsOf2 } = await Promise.resolve().then(() => (init_my_selfie(), my_selfie_exports));
30313
+ const src = path33.resolve(answers.selfie.photoPath.trim());
30314
+ const ext = path33.extname(src).toLowerCase();
30315
+ if (!PHOTO_EXTS.includes(ext)) {
30316
+ throw new Error(`\u7167\u7247\u683C\u5F0F\u4E0D\u652F\u6301\uFF1A${ext || "(\u6CA1\u6709\u540E\u7F00)"}\u3002\u8BF7\u7528 PNG / JPEG / WebP\u3002`);
30317
+ }
30318
+ if (!fs31.existsSync(src)) {
30319
+ throw new Error(`\u7167\u7247\u4E0D\u5B58\u5728\uFF1A${src}`);
30320
+ }
30321
+ const dims = dimsOf2(src);
30322
+ if (dims && dims.includes("\u4E0D\u5408\u683C")) {
30323
+ throw new Error(`\u7167\u7247\u4E0D\u5408\u683C\uFF1A${dims}\u3002\u8BF7\u6362\u4E00\u5F20\uFF08\u5546\u6C64\u5BF9\u8F93\u5165\u56FE\u7684\u786C\u6027\u8981\u6C42\uFF09\u3002`);
30324
+ }
30325
+ values.selfiePhotoRelPath = `images/portrait${ext}`;
30326
+ photoSrc = src;
30327
+ }
29227
30328
  const config = generateConfig(values);
29228
30329
  const configPath = path33.join(opts.stateDir, "configs", "main7.json");
29229
30330
  const tree = buildDirTree(values);
@@ -29243,24 +30344,25 @@ WantedBy=default.target`;
29243
30344
  }
29244
30345
  }
29245
30346
  } else {
30347
+ if (values.chatMode === "auto") {
30348
+ try {
30349
+ fs31.unlinkSync(path33.join(opts.stateDir, "workspace", ".work-mode"));
30350
+ } catch {
30351
+ }
30352
+ }
30353
+ if (photoSrc && values.selfiePhotoRelPath) {
30354
+ const dest = path33.join(opts.stateDir, "workspace", values.selfiePhotoRelPath);
30355
+ fs31.mkdirSync(path33.dirname(dest), { recursive: true });
30356
+ fs31.copyFileSync(photoSrc, dest);
30357
+ console.log(` \u{1F5BC} \u5F62\u8C61: ${dest}`);
30358
+ }
29246
30359
  if (headlessEmit) headlessEmit({ event: "progress", step: "create_dirs" });
29247
30360
  console.log("\n\u{1F4E6} \u6B63\u5728\u521B\u5EFA\u76EE\u5F55\u548C\u6587\u4EF6...\n");
29248
30361
  execute(tree);
29249
30362
  if (headlessEmit) headlessEmit({ event: "progress", step: "write_secrets" });
29250
30363
  if (values.primaryApiKey && values.primaryApiKey.trim()) {
29251
30364
  const envName = values.primaryProvider.toUpperCase().replace(/[^A-Z0-9_]/g, "_") + "_API_KEY";
29252
- const secretsDir = path33.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7-secrets");
29253
- if (!fs31.existsSync(secretsDir)) fs31.mkdirSync(secretsDir, { recursive: true });
29254
- const envFile = path33.join(secretsDir, "main7.env");
29255
- let envContent = "";
29256
- if (fs31.existsSync(envFile)) envContent = fs31.readFileSync(envFile, "utf8");
29257
- const lines = envContent.split("\n").filter((l) => l && !l.startsWith(envName + "="));
29258
- lines.push(envName + "=" + values.primaryApiKey.trim());
29259
- fs31.writeFileSync(envFile, lines.join("\n") + "\n", "utf8");
29260
- try {
29261
- fs31.chmodSync(envFile, 384);
29262
- } catch {
29263
- }
30365
+ const envFile = writeSecretsValue(configPath, envName, values.primaryApiKey.trim());
29264
30366
  console.log("\u{1F511} API key \u5DF2\u5B58\u5165: " + envFile);
29265
30367
  console.log(" config \u91CC\u53EA\u6709 env:" + envName + " \u5360\u4F4D\u7B26\uFF0C\u660E\u6587\u4E0D\u843D config");
29266
30368
  }