engine7 7.1.56 → 7.1.58

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.
@@ -1736,8 +1736,8 @@ var init_microCompact = __esm({
1736
1736
  });
1737
1737
 
1738
1738
  // src/compact/ruleCompact.ts
1739
- function smartCompressToolResult(content, essentialFields) {
1740
- if (content.length <= TOOL_SIZE_LIMIT) return content;
1739
+ function smartCompressToolResult(content, essentialFields, sizeLimit) {
1740
+ if (content.length <= (sizeLimit ?? TOOL_SIZE_LIMIT)) return content;
1741
1741
  try {
1742
1742
  const data = JSON.parse(content);
1743
1743
  if (Array.isArray(data) && data.length > 0) {
@@ -1776,36 +1776,37 @@ ${result}`;
1776
1776
  }
1777
1777
  } catch {
1778
1778
  }
1779
- return content.slice(0, TRUNCATE_TO) + "\n...[TRUNCATED]";
1779
+ return content.slice(0, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
1780
1780
  }
1781
- function isFinalAnswer(m) {
1782
- if (m.role !== "assistant") return false;
1783
- if (m.tool_calls && m.tool_calls.length > 0) return false;
1784
- const content = typeof m.content === "string" ? m.content : "";
1785
- return content.trim().length > 0;
1786
- }
1787
- function extractFinalAnswer(messages) {
1788
- for (let i = messages.length - 1; i >= 0; i--) {
1789
- if (isFinalAnswer(messages[i])) {
1790
- return messages[i];
1781
+ function foldTurnInline(turn) {
1782
+ const toolMsgs = turn.filter((m) => m.role === "tool");
1783
+ if (toolMsgs.length === 0) return turn;
1784
+ let i = 0;
1785
+ while (i < turn.length && turn[i].role !== "assistant" && turn[i].role !== "tool") i++;
1786
+ const head = turn.slice(0, i);
1787
+ const rest = turn.slice(i);
1788
+ let finalAnswer = null;
1789
+ for (let j = rest.length - 1; j >= 0; j--) {
1790
+ const m = rest[j];
1791
+ if (m.role === "assistant" && !(m.tool_calls && m.tool_calls.length > 0)) {
1792
+ const text = typeof m.content === "string" ? m.content.trim() : "";
1793
+ if (text) {
1794
+ finalAnswer = m;
1795
+ break;
1796
+ }
1791
1797
  }
1792
1798
  }
1793
- return null;
1794
- }
1795
- function mergeToolResults(messages) {
1796
- const toolMsgs = messages.filter((m) => m.role === "tool");
1797
- if (toolMsgs.length === 0) return null;
1798
- const parts = [];
1799
- for (let i = 0; i < toolMsgs.length; i++) {
1800
- const rawContent = toolMsgs[i].content;
1801
- const contentStr = typeof rawContent === "string" ? rawContent : "";
1802
- const compressed = smartCompressToolResult(contentStr);
1803
- parts.push(`[${i + 1}] ${compressed}`);
1799
+ const parts = toolMsgs.map((m, k) => `[${k + 1}] ${smartCompressToolResult(typeof m.content === "string" ? m.content : "")}`);
1800
+ const merged = parts.join("\n---\n");
1801
+ if (finalAnswer) {
1802
+ return [...head, { role: "assistant", content: `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${toolMsgs.length} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
1803
+ ${merged}
1804
+
1805
+ \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
1806
+ ${finalAnswer.content || ""}` }];
1804
1807
  }
1805
- return {
1806
- content: parts.join("\n---\n"),
1807
- count: toolMsgs.length
1808
- };
1808
+ return [...head, { role: "assistant", content: `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${toolMsgs.length} \u6761\uFF0C\u8BE5\u8F6E\u65E0\u6587\u5B57\u7ED3\u8BBA\uFF09
1809
+ ${merged}` }];
1809
1810
  }
1810
1811
  function splitByUserTurns(messages) {
1811
1812
  const system = [];
@@ -1872,32 +1873,9 @@ function step2_compressOldTurns(messages, maxTokens, toolResultMode = "minimal")
1872
1873
  const origTokens = estimateMessageTokens(turn);
1873
1874
  newMessages.push(userMsg);
1874
1875
  if (toolResultMode === "inline") {
1875
- const finalAnswer = extractFinalAnswer(rest);
1876
- if (finalAnswer) {
1877
- const mergedTools = mergeToolResults(rest);
1878
- if (mergedTools) {
1879
- newMessages.push(msg.assistant(
1880
- `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${mergedTools.count} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
1881
- ${mergedTools.content}
1882
-
1883
- \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
1884
- ${finalAnswer.content || ""}`
1885
- ));
1886
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+inline-merged, ${mergedTools.count} tool results, mode=inline)`);
1887
- } else {
1888
- newMessages.push(finalAnswer);
1889
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+finalAnswer, no tools)`);
1890
- }
1891
- } else {
1892
- const mergedTools = mergeToolResults(rest);
1893
- if (mergedTools) {
1894
- newMessages.push(msg.assistant(`\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${mergedTools.count} \u6761\uFF0C\u8BE5\u8F6E\u65E0\u6587\u5B57\u7ED3\u8BBA\uFF09
1895
- ${mergedTools.content}`));
1896
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+inline-dataOnly, ${mergedTools.count} tool results, no final answer, mode=inline)`);
1897
- } else {
1898
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 1 msg (user only, no tools/final)`);
1899
- }
1900
- }
1876
+ const folded = foldTurnInline(turn);
1877
+ newMessages.push(...folded.slice(1));
1878
+ compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 ${1 + folded.length - 1} msgs (inline folded, mode=inline)`);
1901
1879
  } else {
1902
1880
  const kept = rest.filter((m) => m.role !== "tool").map((m) => {
1903
1881
  if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
@@ -2015,7 +1993,6 @@ var TAG3, TOOL_SIZE_LIMIT, TRUNCATE_TO, MIN_HISTORY, KEEP_RECENT_TURNS, MAX_LIST
2015
1993
  var init_ruleCompact = __esm({
2016
1994
  "src/compact/ruleCompact.ts"() {
2017
1995
  "use strict";
2018
- init_types();
2019
1996
  init_tokenEstimate();
2020
1997
  init_autoCompact();
2021
1998
  init_compactLog();
@@ -3013,7 +2990,7 @@ var init_query = __esm({
3013
2990
  this.provider = provider;
3014
2991
  this.options = options;
3015
2992
  if (options.compactConfig) {
3016
- this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...options.compactConfig };
2993
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(options.compactConfig) };
3017
2994
  }
3018
2995
  this.logPrefix = `[query:${options.agentLabel || "main"}]`;
3019
2996
  }
@@ -3106,6 +3083,10 @@ var init_query = __esm({
3106
3083
  }
3107
3084
  /** 日志前缀(区分多个并行 QueryEngine) */
3108
3085
  logPrefix;
3086
+ /** 函数形态 option 的统一解析(0902:systemPrompt/systemStable/compactConfig/maxTokens 共用) */
3087
+ resolveOpt(v) {
3088
+ return typeof v === "function" ? v() : v;
3089
+ }
3109
3090
  /** 热加载:替换 provider 实例(doReloadConfig 重建 provider 链后调用) */
3110
3091
  setProvider(provider) {
3111
3092
  this.provider = provider;
@@ -3133,7 +3114,12 @@ var init_query = __esm({
3133
3114
  if (signal) {
3134
3115
  signal.addEventListener("abort", () => ac.abort(), { once: true });
3135
3116
  }
3136
- const activeSystemStable = perTurnSystemStable || this.options.systemStable || this.options.systemPrompt;
3117
+ if (typeof this.options.compactConfig === "function") {
3118
+ const overhead = this.compactConfig.systemOverheadTokens;
3119
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(this.options.compactConfig) };
3120
+ this.compactConfig.systemOverheadTokens = overhead ?? this.compactConfig.systemOverheadTokens;
3121
+ }
3122
+ const activeSystemStable = perTurnSystemStable || this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt);
3137
3123
  let currentMessages = [...messages];
3138
3124
  console.log(`${this.logPrefix} query() called with ${messages.length} messages`);
3139
3125
  const useDynamicTools = !this.options.toolOverride;
@@ -3149,7 +3135,7 @@ var init_query = __esm({
3149
3135
  if (this.systemOverheadTokens === null) {
3150
3136
  const { estimateToolDefinitionTokens: estimateToolDefinitionTokens2 } = await Promise.resolve().then(() => (init_context_analyzer(), context_analyzer_exports));
3151
3137
  const { roughTokenCountEstimation: roughTokenCountEstimation6 } = await Promise.resolve().then(() => (init_tokenEstimate(), tokenEstimate_exports));
3152
- const systemText = this.options.systemStable || this.options.systemPrompt || "";
3138
+ const systemText = this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt) || "";
3153
3139
  const systemTokens = roughTokenCountEstimation6(systemText);
3154
3140
  const initialToolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs) : allToolDefs;
3155
3141
  const toolTokens = estimateToolDefinitionTokens2(initialToolDefs);
@@ -3301,7 +3287,7 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3301
3287
  systemStable: activeSystemStable,
3302
3288
  systemDynamic: fullSystemDynamic,
3303
3289
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3304
- maxTokens: this.options.maxTokens,
3290
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3305
3291
  temperature: this.options.temperature,
3306
3292
  signal: ac.signal,
3307
3293
  disableThinking: this.options.disableThinking || !!perTurnEmotion
@@ -3356,7 +3342,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3356
3342
  }
3357
3343
  if (toolCalls.length === 0) {
3358
3344
  if (!textContent.trim()) {
3359
- console.log(`${this.logPrefix} Turn ${turnCount}: API returned empty (no text, no tool_call). Retrying... messages=${currentMessages.length}`);
3345
+ const thinkingAteBudget = turnThinking.length > 0;
3346
+ console.log(`${this.logPrefix} Turn ${turnCount}: API returned empty (no text, no tool_call).${thinkingAteBudget ? ` ${turnThinking.length}ch thinking \u5403\u5149\u8F93\u51FA\u9884\u7B97 \u2192 retry with thinking off` : " Retrying..."} messages=${currentMessages.length}`);
3360
3347
  turnThinking = "";
3361
3348
  turnThinkingSig = "";
3362
3349
  for await (const chunk of this.provider.streamChat({
@@ -3365,9 +3352,10 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3365
3352
  systemStable: activeSystemStable,
3366
3353
  systemDynamic: fullSystemDynamic,
3367
3354
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3368
- maxTokens: this.options.maxTokens,
3355
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3369
3356
  temperature: this.options.temperature,
3370
- signal: ac.signal
3357
+ signal: ac.signal,
3358
+ ...thinkingAteBudget ? { disableThinking: true } : {}
3371
3359
  })) {
3372
3360
  if (chunk.type === "status") yield chunk;
3373
3361
  if (chunk.type === "text" && chunk.text) textContent += chunk.text;
@@ -5342,8 +5330,8 @@ async function scanMemoryFiles2(memoryDir, signal, maxFiles) {
5342
5330
  const limit = maxFiles ?? DEFAULT_MAX_MEMORY_FILES;
5343
5331
  console.log(`[memdir] scanMemoryFiles: dir=${memoryDir}, maxFiles=${maxFiles}, limit=${limit}`);
5344
5332
  try {
5345
- const entries = await readdir(memoryDir, { recursive: true });
5346
- const mdFiles = entries.filter(
5333
+ const entries2 = await readdir(memoryDir, { recursive: true });
5334
+ const mdFiles = entries2.filter(
5347
5335
  (f) => typeof f === "string" && f.endsWith(".md") && basename4(f) !== "MEMORY.md" && !isArchivedPath(f)
5348
5336
  );
5349
5337
  const headerResults = await Promise.allSettled(
@@ -5388,6 +5376,114 @@ var init_memoryScan = __esm({
5388
5376
  }
5389
5377
  });
5390
5378
 
5379
+ // src/tools/rate-breaker.ts
5380
+ var rate_breaker_exports = {};
5381
+ __export(rate_breaker_exports, {
5382
+ checkRateBreaker: () => checkRateBreaker,
5383
+ parseDurationString: () => parseDurationString,
5384
+ recordInboundBotFlag: () => recordInboundBotFlag
5385
+ });
5386
+ function parseDurationString(s, defaultMs) {
5387
+ if (s === void 0 || s === null || s === "") return defaultMs;
5388
+ if (typeof s === "number" && Number.isFinite(s)) return s;
5389
+ const m = String(s).trim().toLowerCase().match(/^([\d.]+)\s*([a-z]+)?$/);
5390
+ if (!m) return defaultMs;
5391
+ const n = parseFloat(m[1]);
5392
+ const unit = m[2] || "ms";
5393
+ const mult = DURATION_UNITS[unit];
5394
+ if (!Number.isFinite(n) || !mult && unit !== "ms") return defaultMs;
5395
+ return Math.round(unit === "ms" ? n : n * mult);
5396
+ }
5397
+ function recordInboundBotFlag(userId, isBot) {
5398
+ if (!userId || !isBot) return;
5399
+ learnedBots.add(userId);
5400
+ }
5401
+ function getCfg(channel) {
5402
+ const node = liveConfig.get(`channels.${channel}`);
5403
+ const rb = node?.rateBreaker;
5404
+ return {
5405
+ // enabled 判定:rateBreaker 节点存在且不是 false(没配=不生效;{ } 空 obj 也算开,用默认值)
5406
+ enabled: !!rb && rb !== false,
5407
+ windowMs: parseDurationString(rb?.window, DEFAULTS.windowMs),
5408
+ maxSends: typeof rb?.maxSends === "number" ? rb.maxSends : DEFAULTS.maxSends,
5409
+ cooldownMs: parseDurationString(rb?.cooldown, DEFAULTS.cooldownMs),
5410
+ escalate: rb?.escalate !== false,
5411
+ botIds: new Set(Array.isArray(node?.botIds) ? node.botIds : [])
5412
+ };
5413
+ }
5414
+ function isBotDirection(cfg, toIds, inboundIsBot) {
5415
+ if (inboundIsBot) return true;
5416
+ if (toIds.some((id) => cfg.botIds.has(id) || learnedBots.has(id))) return true;
5417
+ return false;
5418
+ }
5419
+ function checkRateBreaker(channel, sessionId, dest, toIds, inboundIsBot) {
5420
+ const cfg = getCfg(channel);
5421
+ if (!cfg.enabled) return null;
5422
+ if (!isBotDirection(cfg, toIds, inboundIsBot)) return null;
5423
+ const key = `${sessionId}\u2192${dest || toIds.join(",") || "?"}`;
5424
+ const now = Date.now();
5425
+ let e = entries.get(key);
5426
+ if (e && now - e.lastActivity > RESET_AFTER_MS) {
5427
+ entries.delete(key);
5428
+ e = void 0;
5429
+ }
5430
+ if (e && e.openUntil > now) {
5431
+ e.lastActivity = now;
5432
+ return { content: `\u5DF2\u9759\u9ED8\uFF1A\u8BE5\u65B9\u5411\u7684\u53D1\u9001\u5904\u4E8E\u9891\u7387\u7194\u65AD\u4E2D\uFF08\u51B7\u5374\u81F3 ${new Date(e.openUntil).toLocaleTimeString("zh-CN")}\uFF09\u3002\u8FD9\u662F\u9632\u6B62 bot \u5BF9\u8BDD\u5FAA\u73AF\u7684\u81EA\u52A8\u4FDD\u62A4\u2014\u2014\u5982\u679C\u8FD9\u662F\u6B63\u5E38\u534F\u4F5C\u88AB\u6253\u65AD\uFF0C\u8BF7\u544A\u77E5\u7FC0\u54E5\u8C03\u6574 rateBreaker \u914D\u7F6E\u3002` };
5433
+ }
5434
+ if (e && e.openUntil <= now && e.openUntil > 0) {
5435
+ e.openUntil = 0;
5436
+ }
5437
+ if (!e) {
5438
+ e = { timestamps: [], openUntil: 0, trips: 0, lastActivity: now };
5439
+ entries.set(key, e);
5440
+ }
5441
+ e.lastActivity = now;
5442
+ e.timestamps = e.timestamps.filter((t) => now - t < cfg.windowMs);
5443
+ e.timestamps.push(now);
5444
+ if (e.timestamps.length > cfg.maxSends) {
5445
+ e.trips++;
5446
+ const cooldown = cfg.escalate ? Math.min(cfg.cooldownMs * Math.pow(2, e.trips - 1), ESCALATE_CAP_MS) : cfg.cooldownMs;
5447
+ e.openUntil = now + cooldown;
5448
+ e.timestamps = [];
5449
+ const tripsNote = cfg.escalate ? `\uFF08\u7B2C ${e.trips} \u6B21\u8FDE\u7EED\u7194\u65AD\uFF0C\u51B7\u5374${cooldown >= ESCALATE_CAP_MS ? "\u5DF2\u8FBE\u4E0A\u9650 24h" : `\u7FFB\u500D\u81F3 ${Math.round(cooldown / 6e4)} \u5206\u949F`}\uFF09` : "";
5450
+ console.warn(`[rate-breaker] OPEN ${key}\uFF1A${cfg.windowMs / 6e4} \u5206\u949F\u5185 ${e.timestamps.length || cfg.maxSends + 1} \u6761\u8D85\u9650\uFF08max=${cfg.maxSends}\uFF09${tripsNote}`);
5451
+ return { content: `\u5DF2\u9759\u9ED8\uFF1A\u77ED\u65F6\u95F4\u5185\u5411\u540C\u4E00\u76EE\u6807\u53D1\u9001\u8FC7\u591A\uFF08${cfg.maxSends} \u6761/${Math.round(cfg.windowMs / 6e4)} \u5206\u949F\uFF09\uFF0C\u89E6\u53D1\u9891\u7387\u7194\u65AD\uFF0C\u51B7\u5374 ${Math.round(cooldown / 6e4)} \u5206\u949F${tripsNote}\u3002\u5982\u679C\u4F60\u786E\u4FE1\u8FD9\u4E0D\u662F\u5FAA\u73AF\uFF08\u662F\u6B63\u5E38\u534F\u4F5C\uFF09\uFF0C\u7A0D\u7B49\u51B7\u5374\u6216\u544A\u77E5\u7FC0\u54E5\u8C03\u6574\u914D\u7F6E\u3002` };
5452
+ }
5453
+ return null;
5454
+ }
5455
+ var DURATION_UNITS, learnedBots, entries, DEFAULTS, ESCALATE_CAP_MS, RESET_AFTER_MS;
5456
+ var init_rate_breaker = __esm({
5457
+ "src/tools/rate-breaker.ts"() {
5458
+ "use strict";
5459
+ init_live();
5460
+ DURATION_UNITS = {
5461
+ s: 1e3,
5462
+ sec: 1e3,
5463
+ secs: 1e3,
5464
+ second: 1e3,
5465
+ seconds: 1e3,
5466
+ m: 6e4,
5467
+ min: 6e4,
5468
+ mins: 6e4,
5469
+ minute: 6e4,
5470
+ minutes: 6e4,
5471
+ h: 36e5,
5472
+ hr: 36e5,
5473
+ hour: 36e5,
5474
+ hours: 36e5,
5475
+ d: 864e5,
5476
+ day: 864e5,
5477
+ days: 864e5
5478
+ };
5479
+ learnedBots = /* @__PURE__ */ new Set();
5480
+ entries = /* @__PURE__ */ new Map();
5481
+ DEFAULTS = { windowMs: 6e5, maxSends: 8, cooldownMs: 6e5 };
5482
+ ESCALATE_CAP_MS = 864e5;
5483
+ RESET_AFTER_MS = 864e5;
5484
+ }
5485
+ });
5486
+
5391
5487
  // src/memory/memdir/extractPrompts.ts
5392
5488
  function opener(newMessageCount, existingMemories) {
5393
5489
  const manifest = existingMemories.length > 0 ? `
@@ -5845,8 +5941,8 @@ var init_ingest = __esm({
5845
5941
  });
5846
5942
 
5847
5943
  // src/memory/sessionMemory/sessionMemoryUtils.ts
5848
- import { join as join20 } from "node:path";
5849
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
5944
+ import { join as join21 } from "node:path";
5945
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync16 } from "node:fs";
5850
5946
  function setSessionMemoryStateDir(dir) {
5851
5947
  _stateDir2 = dir;
5852
5948
  mkdirSync7(dir, { recursive: true });
@@ -5855,7 +5951,7 @@ function getSessionMemoryDir() {
5855
5951
  return _stateDir2;
5856
5952
  }
5857
5953
  function getSessionMemoryPath() {
5858
- return join20(_stateDir2, "session-notes.md");
5954
+ return join21(_stateDir2, "session-notes.md");
5859
5955
  }
5860
5956
  function markExtractionStarted() {
5861
5957
  extractionStartedAt = Date.now();
@@ -5903,7 +5999,7 @@ var init_sessionMemoryUtils = __esm({
5903
5999
 
5904
6000
  // src/memory/sessionMemory/prompts.ts
5905
6001
  import { readFile as readFile4 } from "node:fs/promises";
5906
- import { join as join21 } from "node:path";
6002
+ import { join as join22 } from "node:path";
5907
6003
  function roughTokenCountEstimation4(text) {
5908
6004
  return Math.ceil(text.length / 4);
5909
6005
  }
@@ -5948,7 +6044,7 @@ REMEMBER: Use the Edit tool in parallel and stop. Do not continue after the edit
5948
6044
  }
5949
6045
  async function loadSessionMemoryTemplate() {
5950
6046
  const configHome = process.env.CLAUDE_CONFIG_HOME || process.env.HOME || "";
5951
- const templatePath = join21(configHome, ".claude", "session-memory", "config", "template.md");
6047
+ const templatePath = join22(configHome, ".claude", "session-memory", "config", "template.md");
5952
6048
  try {
5953
6049
  return await readFile4(templatePath, { encoding: "utf-8" });
5954
6050
  } catch {
@@ -5957,7 +6053,7 @@ async function loadSessionMemoryTemplate() {
5957
6053
  }
5958
6054
  async function loadSessionMemoryPrompt() {
5959
6055
  const configHome = process.env.CLAUDE_CONFIG_HOME || process.env.HOME || "";
5960
- const promptPath = join21(configHome, ".claude", "session-memory", "config", "prompt.md");
6056
+ const promptPath = join22(configHome, ".claude", "session-memory", "config", "prompt.md");
5961
6057
  try {
5962
6058
  return await readFile4(promptPath, { encoding: "utf-8" });
5963
6059
  } catch {
@@ -6121,7 +6217,7 @@ __export(sessionMemory_exports, {
6121
6217
  resetLastMemoryMessageUuid: () => resetLastMemoryMessageUuid,
6122
6218
  shouldExtractMemory: () => shouldExtractMemory
6123
6219
  });
6124
- import { readFileSync as readFileSync16, mkdirSync as mkdirSync8 } from "node:fs";
6220
+ import { readFileSync as readFileSync17, mkdirSync as mkdirSync8 } from "node:fs";
6125
6221
  import { writeFile as writeFile3 } from "node:fs/promises";
6126
6222
  function initSessionMemory(deps) {
6127
6223
  _deps = deps;
@@ -6184,7 +6280,7 @@ async function setupSessionMemoryFile() {
6184
6280
  const memoryPath = getSessionMemoryPath();
6185
6281
  let currentMemory;
6186
6282
  try {
6187
- currentMemory = readFileSync16(memoryPath, { encoding: "utf-8" });
6283
+ currentMemory = readFileSync17(memoryPath, { encoding: "utf-8" });
6188
6284
  } catch {
6189
6285
  const template = await loadSessionMemoryTemplate();
6190
6286
  await writeFile3(memoryPath, template, { encoding: "utf-8" });
@@ -6261,7 +6357,7 @@ ${messages.filter((m) => m.role === "user" || m.role === "assistant").slice(-20)
6261
6357
  }
6262
6358
  function getSessionMemoryForCompaction() {
6263
6359
  try {
6264
- const content = readFileSync16(getSessionMemoryPath(), { encoding: "utf-8" }).trim();
6360
+ const content = readFileSync17(getSessionMemoryPath(), { encoding: "utf-8" }).trim();
6265
6361
  if (!content) return null;
6266
6362
  const { truncatedContent, wasTruncated } = truncateSessionMemoryForCompact(content);
6267
6363
  if (wasTruncated) {
@@ -6294,7 +6390,7 @@ var init_sessionMemory = __esm({
6294
6390
  // src/memory/autoDream/config.ts
6295
6391
  var config_exports = {};
6296
6392
  __export(config_exports, {
6297
- DEFAULTS: () => DEFAULTS,
6393
+ DEFAULTS: () => DEFAULTS2,
6298
6394
  getAutoDreamConfig: () => getAutoDreamConfig,
6299
6395
  getDailyLogDir: () => getDailyLogDir,
6300
6396
  getDistillOutput: () => getDistillOutput,
@@ -6311,8 +6407,8 @@ function isAutoDreamEnabled() {
6311
6407
  function getAutoDreamConfig() {
6312
6408
  const raw = _config?.topics?.autoDream ?? _config?.autoDream;
6313
6409
  return {
6314
- minHours: typeof raw?.minHours === "number" && Number.isFinite(raw.minHours) && raw.minHours > 0 ? raw.minHours : DEFAULTS.minHours,
6315
- minSessions: typeof raw?.minSessions === "number" && Number.isFinite(raw.minSessions) && raw.minSessions > 0 ? raw.minSessions : DEFAULTS.minSessions
6410
+ minHours: typeof raw?.minHours === "number" && Number.isFinite(raw.minHours) && raw.minHours > 0 ? raw.minHours : DEFAULTS2.minHours,
6411
+ minSessions: typeof raw?.minSessions === "number" && Number.isFinite(raw.minSessions) && raw.minSessions > 0 ? raw.minSessions : DEFAULTS2.minSessions
6316
6412
  };
6317
6413
  }
6318
6414
  function getDailyLogDir() {
@@ -6328,13 +6424,13 @@ function getMaxEntrypointLines() {
6328
6424
  if (typeof maxScan === "number" && maxScan > 200) return maxScan;
6329
6425
  return void 0;
6330
6426
  }
6331
- var _config, DEFAULTS;
6427
+ var _config, DEFAULTS2;
6332
6428
  var init_config2 = __esm({
6333
6429
  "src/memory/autoDream/config.ts"() {
6334
6430
  "use strict";
6335
6431
  init_features();
6336
6432
  _config = null;
6337
- DEFAULTS = {
6433
+ DEFAULTS2 = {
6338
6434
  minHours: 24,
6339
6435
  minSessions: 5
6340
6436
  };
@@ -6343,9 +6439,9 @@ var init_config2 = __esm({
6343
6439
 
6344
6440
  // src/memory/autoDream/consolidationLock.ts
6345
6441
  import { mkdir as mkdir3, readFile as readFile5, stat as stat3, unlink, utimes, writeFile as writeFile4 } from "node:fs/promises";
6346
- import { join as join22 } from "node:path";
6442
+ import { join as join23 } from "node:path";
6347
6443
  function lockPath(memoryDir) {
6348
- return join22(memoryDir, LOCK_FILE);
6444
+ return join23(memoryDir, LOCK_FILE);
6349
6445
  }
6350
6446
  async function readLastConsolidatedAt(memoryDir) {
6351
6447
  try {
@@ -6413,7 +6509,7 @@ async function listSessionsTouchedSince(sessionsDir, sinceMs) {
6413
6509
  const results = [];
6414
6510
  for (const f of jsonlFiles) {
6415
6511
  try {
6416
- const s = await stat3(join22(sessionsDir, f));
6512
+ const s = await stat3(join23(sessionsDir, f));
6417
6513
  if (s.mtimeMs > sinceMs) {
6418
6514
  results.push(f.replace(/\.jsonl$/, ""));
6419
6515
  }
@@ -6492,7 +6588,13 @@ Focus on:
6492
6588
 
6493
6589
  ${distillOutput ? `## Phase 3.5 \u2014 Distill to core knowledge
6494
6590
 
6495
- After consolidation, distill the most important insights into a single consolidated file at \`${distillOutput}\`. This file should contain only the highest-concentration knowledge \u2014 core principles, user preferences, milestones, and cross-session patterns. Each entry should be one concise paragraph. Append new entries, don't rewrite the whole file. If the file doesn't exist, create it.` : ""}
6591
+ After consolidation, distill the most important insights into a single consolidated file at \`${distillOutput}\`. This file should contain only the highest-concentration knowledge \u2014 core principles, user preferences, milestones, and cross-session patterns. Each entry should be one concise paragraph. Append new entries, don't rewrite the whole file. If the file doesn't exist, create it.
6592
+
6593
+ **Size cap (0901 \u7FC0\u54E5\u6279 \u2014 this file rides the work-mode system prompt every turn, every char costs tokens every request):**
6594
+ - SOFT CAP 20,000 chars: when the file exceeds it, before appending, **merge the OLDEST dated entries upward into the thematic sections at the top** (\u884C\u4E3A\u539F\u5219/\u5DE5\u7A0B\u94C1\u5F8B/\u504F\u597D etc.) \u2014 compress narratives to principle form, merge duplicates into one entry. Knowledge moves up, nothing is deleted outright.
6595
+ - HARD CAP 35,000 chars: if still over (or a previous merge failed), compaction becomes the FIRST task of this dream \u2014 do it before anything else.
6596
+ - Always \`cp\` the file to \\\`distill-archive-{YYYYMMDD}.md\\\` before any merge/compaction rewrite.
6597
+ - Merging here is safe: distill is derived knowledge \u2014 the source lives in the topic memory files and recall can recover details. The "never delete" rule above applies to memory FILES, not to compressing this distillate.` : ""}
6496
6598
 
6497
6599
  ## Phase 4 \u2014 Prune and index
6498
6600
 
@@ -7238,8 +7340,8 @@ Usage:
7238
7340
  return { content: `\u8BBE\u5907\u6587\u4EF6\u4F1A\u963B\u585E\u6216\u4EA7\u751F\u65E0\u9650\u8F93\u51FA: ${filePath}`, isError: true };
7239
7341
  }
7240
7342
  if (stat4.isDirectory()) {
7241
- const entries = fs38.readdirSync(filePath);
7242
- const items = entries.map((e) => {
7343
+ const entries2 = fs38.readdirSync(filePath);
7344
+ const items = entries2.map((e) => {
7243
7345
  const full = path38.join(filePath, e);
7244
7346
  try {
7245
7347
  const s = fs38.statSync(full);
@@ -7248,7 +7350,7 @@ Usage:
7248
7350
  return e;
7249
7351
  }
7250
7352
  });
7251
- return { content: `\u76EE\u5F55 (${entries.length} \u9879):
7353
+ return { content: `\u76EE\u5F55 (${entries2.length} \u9879):
7252
7354
  ${items.join("\n")}` };
7253
7355
  }
7254
7356
  const ext = path38.extname(filePath).toLowerCase();
@@ -7741,13 +7843,13 @@ function findFiles(dir, pattern, limit, baseDir) {
7741
7843
  truncated = true;
7742
7844
  return;
7743
7845
  }
7744
- let entries;
7846
+ let entries2;
7745
7847
  try {
7746
- entries = fs41.readdirSync(currentDir, { withFileTypes: true });
7848
+ entries2 = fs41.readdirSync(currentDir, { withFileTypes: true });
7747
7849
  } catch {
7748
7850
  return;
7749
7851
  }
7750
- for (const entry of entries) {
7852
+ for (const entry of entries2) {
7751
7853
  if (truncated) return;
7752
7854
  const fullPath = path41.join(currentDir, entry.name);
7753
7855
  if (entry.isDirectory()) {
@@ -10877,6 +10979,17 @@ async function executeAndDeliver(task, now, deps) {
10877
10979
  const resultPromise = new Promise((resolve11) => {
10878
10980
  resolveResult = resolve11;
10879
10981
  });
10982
+ const modelRef = task.model || liveConfig.get("cron.model");
10983
+ let execDeps = deps.deps;
10984
+ if (modelRef) {
10985
+ const overrideDeps = deps.resolveModelDeps?.(modelRef);
10986
+ if (overrideDeps) {
10987
+ execDeps = overrideDeps;
10988
+ execDeps.renderer = deps.deps.renderer;
10989
+ } else {
10990
+ console.warn(`[cron] Task ${task.id}: model "${modelRef}" unresolvable, falling back to primary`);
10991
+ }
10992
+ }
10880
10993
  deps.dispatcher.submitMessage({
10881
10994
  text: promptText,
10882
10995
  sessionId,
@@ -10886,7 +10999,7 @@ async function executeAndDeliver(task, now, deps) {
10886
10999
  callbacks: {
10887
11000
  onResult: (content) => resolveResult(content)
10888
11001
  },
10889
- deps: deps.deps,
11002
+ deps: execDeps,
10890
11003
  // 默认跳过 recall(省 token),task.skipRecall=false 才走
10891
11004
  skipRecall: task.skipRecall !== false
10892
11005
  });
@@ -10975,6 +11088,8 @@ ${notifyLines}`;
10975
11088
  source: "cron",
10976
11089
  priority: "later",
10977
11090
  callbacks: {},
11091
+ // 故意用 primary(不吃 cron.model):这条是注入主对话 session 的,
11092
+ // 该由主对话自己的模型接手,换模型会让主 session 的 KV 前缀断掉。
10978
11093
  deps: deps.deps
10979
11094
  });
10980
11095
  console.log(`[cron] Notified main session: ${mainSessionId}`);
@@ -11048,6 +11163,7 @@ var schedulerTimer, shuttingDown, currentlyExecuting;
11048
11163
  var init_scheduler = __esm({
11049
11164
  "src/cron/scheduler.ts"() {
11050
11165
  "use strict";
11166
+ init_live();
11051
11167
  init_tasks2();
11052
11168
  schedulerTimer = null;
11053
11169
  shuttingDown = false;
@@ -11258,7 +11374,7 @@ var init_tools = __esm({
11258
11374
 
11259
11375
  // src/tools/wechat/wx-query.ts
11260
11376
  var wx_query_exports = {};
11261
- import { join as join36 } from "node:path";
11377
+ import { join as join37 } from "node:path";
11262
11378
  function getDescription() {
11263
11379
  return `\u67E5\u8BE2\u7FC0\u54E5\u7684\u5FAE\u4FE1\u6D88\u606F\uFF08\u7F13\u5B58\u89E3\u5BC6\u540E\u7684\u672C\u5730\u6570\u636E\u5E93\uFF09\u3002
11264
11380
 
@@ -11293,8 +11409,8 @@ var init_wx_query = __esm({
11293
11409
  "use strict";
11294
11410
  init_registry();
11295
11411
  init_live();
11296
- STATE_DIR = process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || join36(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
11297
- SCRIPT = join36("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
11412
+ STATE_DIR = process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || join37(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
11413
+ SCRIPT = join37("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
11298
11414
  PYTHON = "python3";
11299
11415
  registry.register({
11300
11416
  name: "wx_query",
@@ -11854,14 +11970,16 @@ var init_cron_plugin = __esm({
11854
11970
  "use strict";
11855
11971
  init_tasks2();
11856
11972
  init_scheduler();
11973
+ init_live();
11857
11974
  CronPlugin = class {
11858
- constructor(config, sessions, channelManager, deps, stateDir, dispatcher) {
11975
+ constructor(config, sessions, channelManager, deps, stateDir, dispatcher, resolveModelDeps) {
11859
11976
  this.config = config;
11860
11977
  this.sessions = sessions;
11861
11978
  this.channelManager = channelManager;
11862
11979
  this.deps = deps;
11863
11980
  this.stateDir = stateDir;
11864
11981
  this.dispatcher = dispatcher;
11982
+ this.resolveModelDeps = resolveModelDeps;
11865
11983
  }
11866
11984
  config;
11867
11985
  sessions;
@@ -11869,6 +11987,7 @@ var init_cron_plugin = __esm({
11869
11987
  deps;
11870
11988
  stateDir;
11871
11989
  dispatcher;
11990
+ resolveModelDeps;
11872
11991
  static shouldEnable(config) {
11873
11992
  return config.enabled === true;
11874
11993
  }
@@ -11901,10 +12020,12 @@ var init_cron_plugin = __esm({
11901
12020
  channelManager: this.channelManager,
11902
12021
  deps: this.deps,
11903
12022
  config: this.config,
11904
- dispatcher: this.dispatcher
12023
+ dispatcher: this.dispatcher,
12024
+ resolveModelDeps: this.resolveModelDeps
11905
12025
  };
11906
12026
  startScheduler(schedulerDeps);
11907
- console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""})`);
12027
+ const cronModel = liveConfig.get("cron.model");
12028
+ console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""}${cronModel ? `, model=${cronModel}` : ""})`);
11908
12029
  }
11909
12030
  async stop() {
11910
12031
  await stopScheduler();
@@ -11918,8 +12039,8 @@ var reply_blocklist_exports = {};
11918
12039
  __export(reply_blocklist_exports, {
11919
12040
  isUserBlocked: () => isUserBlocked
11920
12041
  });
11921
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync19, existsSync as existsSync26 } from "node:fs";
11922
- import { join as join42 } from "node:path";
12042
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync19, existsSync as existsSync27 } from "node:fs";
12043
+ import { join as join43 } from "node:path";
11923
12044
  function ensureLoaded(workspace, configIds) {
11924
12045
  if (loaded) return;
11925
12046
  if (configIds?.length) {
@@ -11927,10 +12048,10 @@ function ensureLoaded(workspace, configIds) {
11927
12048
  if (!state.blockedUserIds.includes(id)) state.blockedUserIds.push(id);
11928
12049
  }
11929
12050
  }
11930
- const path50 = join42(workspace, ".reply-blocklist.json");
12051
+ const path50 = join43(workspace, ".reply-blocklist.json");
11931
12052
  try {
11932
- if (existsSync26(path50)) {
11933
- const raw = readFileSync29(path50, "utf-8");
12053
+ if (existsSync27(path50)) {
12054
+ const raw = readFileSync30(path50, "utf-8");
11934
12055
  const parsed = JSON.parse(raw);
11935
12056
  if (parsed.blockedUserIds) {
11936
12057
  for (const id of parsed.blockedUserIds) {
@@ -11946,7 +12067,7 @@ function ensureLoaded(workspace, configIds) {
11946
12067
  loaded = true;
11947
12068
  }
11948
12069
  function save(workspace) {
11949
- const path50 = join42(workspace, ".reply-blocklist.json");
12070
+ const path50 = join43(workspace, ".reply-blocklist.json");
11950
12071
  try {
11951
12072
  writeFileSync19(path50, JSON.stringify(state, null, 2), "utf-8");
11952
12073
  } catch (err) {
@@ -12826,30 +12947,21 @@ function loadConfig(configPath) {
12826
12947
  devMode: agentDefaults.devMode === true
12827
12948
  };
12828
12949
  const rawSession = raw.session || {};
12829
- const sessionScope = rawSession.dmScope || rawSession.groupScope ? { dmScope: rawSession.dmScope, groupScope: rawSession.groupScope } : void 0;
12950
+ const sessionScope = Object.keys(rawSession).length > 0 ? { ...rawSession } : void 0;
12830
12951
  const rawHeartbeat = raw.heartbeat || {};
12831
12952
  const heartbeat = rawHeartbeat.enabled ? {
12832
- enabled: true,
12833
- intervalMs: rawHeartbeat.intervalMs,
12834
- prompt: rawHeartbeat.prompt,
12835
- model: rawHeartbeat.model,
12836
- timeoutMs: rawHeartbeat.timeoutMs,
12837
- checkOnline: rawHeartbeat.checkOnline,
12838
- activeThresholdMs: rawHeartbeat.activeThresholdMs
12953
+ ...rawHeartbeat,
12954
+ enabled: true
12839
12955
  } : void 0;
12840
12956
  const rawInnerVoice = raw.innerVoice || {};
12841
12957
  const innerVoice = rawInnerVoice.enabled ? {
12842
- enabled: true,
12843
- intervalMs: rawInnerVoice.intervalMs,
12844
- provider: rawInnerVoice.provider,
12845
- model: rawInnerVoice.model,
12846
- activeThresholdMs: rawInnerVoice.activeThresholdMs,
12847
- hint: rawInnerVoice.hint
12958
+ ...rawInnerVoice,
12959
+ enabled: true
12848
12960
  } : void 0;
12849
12961
  const rawCron = raw.cron || {};
12850
12962
  const cron = rawCron.enabled ? {
12963
+ ...rawCron,
12851
12964
  enabled: true,
12852
- storageDir: rawCron.storageDir,
12853
12965
  defaultTimezone: rawCron.defaultTimezone || "Asia/Shanghai",
12854
12966
  tickIntervalMs: rawCron.tickIntervalMs || 1e4,
12855
12967
  maxConsecutiveFailures: rawCron.maxConsecutiveFailures || 5,
@@ -12911,15 +13023,115 @@ init_live();
12911
13023
 
12912
13024
  // src/models/openai-provider.ts
12913
13025
  init_withRetry();
13026
+
13027
+ // src/models/think-stripper.ts
13028
+ var OPEN = "<think>";
13029
+ var CLOSE = "</think>";
13030
+ var MAX_TAG = Math.max(OPEN.length, CLOSE.length);
13031
+ var ThinkTagStripper = class {
13032
+ buf = "";
13033
+ // 尚未判定的原始片段(可能含被 chunk 切断的标签前缀)
13034
+ pending = "";
13035
+ // <think> 内累积的思考——闭合才 yield(未闭合时要能整段还原)
13036
+ active = false;
13037
+ reset() {
13038
+ this.buf = "";
13039
+ this.pending = "";
13040
+ this.active = false;
13041
+ }
13042
+ /** 还有未吐出的内容(provider 判 isEmpty 用,别把攒在 buffer 里的当空响应) */
13043
+ get hasPending() {
13044
+ return !!this.buf || !!this.pending;
13045
+ }
13046
+ /**
13047
+ * 需要保留的尾部长度:只在末尾确实是 <think> / </think> 的前缀时才 hold。
13048
+ * 原实现无脑留 7 字符,导致短消息全卡住("嗯……来了" 这种 5 字回复流不出去)。
13049
+ */
13050
+ holdLen() {
13051
+ const b = this.buf;
13052
+ for (let k = Math.min(MAX_TAG - 1, b.length); k > 0; k--) {
13053
+ const tail = b.slice(b.length - k);
13054
+ if (OPEN.startsWith(tail) || CLOSE.startsWith(tail)) return k;
13055
+ }
13056
+ return 0;
13057
+ }
13058
+ /** 喂一段 text delta。标签可出现在任意位置(不只流开头),可跨 chunk 被切断。 */
13059
+ feed(chunk) {
13060
+ if (!chunk) return [];
13061
+ this.buf += chunk;
13062
+ const out = [];
13063
+ for (; ; ) {
13064
+ if (!this.active) {
13065
+ const i = this.buf.indexOf(OPEN);
13066
+ if (i >= 0) {
13067
+ if (i > 0) out.push({ text: this.buf.slice(0, i) });
13068
+ this.buf = this.buf.slice(i + OPEN.length);
13069
+ this.active = true;
13070
+ continue;
13071
+ }
13072
+ const safe2 = this.buf.length - this.holdLen();
13073
+ if (safe2 > 0) {
13074
+ out.push({ text: this.buf.slice(0, safe2) });
13075
+ this.buf = this.buf.slice(safe2);
13076
+ }
13077
+ return out;
13078
+ }
13079
+ const j = this.buf.indexOf(CLOSE);
13080
+ if (j >= 0) {
13081
+ this.pending += this.buf.slice(0, j);
13082
+ this.buf = this.buf.slice(j + CLOSE.length);
13083
+ this.active = false;
13084
+ if (this.pending) {
13085
+ out.push({ thinking: this.pending });
13086
+ this.pending = "";
13087
+ }
13088
+ continue;
13089
+ }
13090
+ const safe = this.buf.length - this.holdLen();
13091
+ if (safe > 0) {
13092
+ this.pending += this.buf.slice(0, safe);
13093
+ this.buf = this.buf.slice(safe);
13094
+ }
13095
+ return out;
13096
+ }
13097
+ }
13098
+ /**
13099
+ * 流结束时清空 buffer。
13100
+ * 未闭合 </think> = 模型没遵守协议 → 整段(含标签)还原成正文。
13101
+ * 宁可让用户看到难看的 <think> 字样,也绝不静默吞掉整条消息。
13102
+ */
13103
+ flush() {
13104
+ const out = [];
13105
+ if (this.active) {
13106
+ out.push({ text: OPEN + this.pending + this.buf });
13107
+ } else if (this.buf) {
13108
+ out.push({ text: this.buf });
13109
+ }
13110
+ this.reset();
13111
+ return out;
13112
+ }
13113
+ };
13114
+
13115
+ // src/models/openai-provider.ts
13116
+ function sanitizeToolCallForSend(tc) {
13117
+ const args = tc.function?.arguments;
13118
+ if (typeof args !== "string" || args === "") return tc;
13119
+ try {
13120
+ JSON.parse(args);
13121
+ return tc;
13122
+ } catch {
13123
+ console.warn(`[openai] toolCall "${tc.function.name}" arguments \u975E\u5408\u6CD5 JSON\uFF08${args.length} chars\uFF09\uFF0C\u51FA\u7AD9\u56DE\u843D "{}" \u9632 400`);
13124
+ return { ...tc, function: { ...tc.function, arguments: "{}" } };
13125
+ }
13126
+ }
12914
13127
  var OpenAIProvider = class {
13128
+ // 0831 <think> 剥离改用 ThinkTagStripper 的**局部**实例(见 streamChat)——
13129
+ // 原来是实例字段,跨请求共享状态,流中途 abort 会把 active=true 带到下一个请求
12915
13130
  constructor(config) {
12916
13131
  this.config = config;
12917
13132
  }
12918
13133
  config;
12919
13134
  name = "openai";
12920
- // 8/18:<think> 剥离状态(Qwen3 系兜底)
12921
- thinkBuf = "";
12922
- thinkStripActive = false;
12923
13135
  /** OpenAI: system prompt 放在 messages 里 */
12924
13136
  formatMessages(systemPrompt, messages) {
12925
13137
  const formatted = [];
@@ -12941,7 +13153,13 @@ var OpenAIProvider = class {
12941
13153
  role: m.role,
12942
13154
  // content 数组:转换 image block 为 OpenAI image_url 格式
12943
13155
  content: Array.isArray(m.content) ? convertContentBlocksForOpenAI(m.content) : m.content,
12944
- ...m.tool_calls ? { tool_calls: m.tool_calls } : {},
13156
+ // 0831 出站卡口:流式截断会产生非法 JSON arguments 字符串,原样发出会被
13157
+ // 校验 tool_calls 的端点直接 400(vLLM --tool-call-parser qwen3_coder 实测
13158
+ // "Unterminated string at char 12"),毒记录留在上下文里每轮重放一次 → 主模型
13159
+ // 连续冷却 3h 假死。anthropic(L81)/gemini(safeParseArgs) 早有同款防御,这里补齐:
13160
+ // parse 失败回落 "{}"。copy-on-write 不动内存历史(原始串保真进 jsonl 供诊断,
13161
+ // 重放消毒另由 reader.pickToolCallArguments 负责)。
13162
+ ...m.tool_calls ? { tool_calls: m.tool_calls.map(sanitizeToolCallForSend) } : {},
12945
13163
  ...m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}
12946
13164
  };
12947
13165
  if (m.role === "assistant" && m.reasoning_content) {
@@ -12965,8 +13183,7 @@ var OpenAIProvider = class {
12965
13183
  let base = (this.config.baseUrl || "").trim();
12966
13184
  while (base.endsWith("/")) base = base.slice(0, -1);
12967
13185
  const url = `${base}/chat/completions`;
12968
- this.thinkBuf = "";
12969
- this.thinkStripActive = false;
13186
+ const stripper = new ThinkTagStripper();
12970
13187
  const body = {
12971
13188
  model: params.model,
12972
13189
  messages: formatted,
@@ -12974,8 +13191,10 @@ var OpenAIProvider = class {
12974
13191
  ...params.tools && params.tools.length > 0 ? { tools: params.tools } : {},
12975
13192
  ...params.maxTokens ? { max_tokens: params.maxTokens } : {},
12976
13193
  ...params.temperature !== void 0 ? { temperature: params.temperature } : {},
12977
- // 8/18:Qwen3 reasoning 模型——disableThinking 时显式关(chat_template_kwargs enable_thinking=false,deepinfra/vLLM 通用)
12978
- ...params.disableThinking && /qwen3/i.test(params.model) ? { chat_template_kwargs: { enable_thinking: false } } : {}
13194
+ // 9/1 对齐 anthropic:不配 thinking = 默认开(?? true),disableThinking 优先级最高。
13195
+ // 三家 provider 统一语义:不配 = 默认开,想关显式配 enabled:false
13196
+ // chat_template_kwargs 是 vLLM 标准参数,非 vLLM 端点忽略未知字段,不挑模型。
13197
+ ...params.disableThinking ? { chat_template_kwargs: { enable_thinking: false } } : { chat_template_kwargs: { enable_thinking: this.config.thinking?.enabled ?? true } }
12979
13198
  };
12980
13199
  if (params.tools && params.tools.length > 0 && /deepseek/i.test(params.model)) {
12981
13200
  let patched = 0;
@@ -12988,7 +13207,7 @@ var OpenAIProvider = class {
12988
13207
  if (patched > 0) console.log(`[openai] reasoning_content \u8865\u7A7A ${patched} \u4E2A assistant \u8F6E (DeepSeek V4 \u89C4\u5219)`);
12989
13208
  }
12990
13209
  const actualThinking = body.chat_template_kwargs;
12991
- console.log(`[openai] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} msgs=${formatted.length} tools=${params.tools?.length ?? 0}`);
13210
+ 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}`);
12992
13211
  const retryGen = fetchWithRetry(url, {
12993
13212
  method: "POST",
12994
13213
  headers: {
@@ -13048,9 +13267,9 @@ var OpenAIProvider = class {
13048
13267
  const trimmed = line.trim();
13049
13268
  if (!trimmed || trimmed === "data: [DONE]") {
13050
13269
  if (trimmed === "data: [DONE]") {
13051
- if (!this.thinkStripActive && this.thinkBuf) {
13052
- yield { type: "text", text: this.thinkBuf };
13053
- this.thinkBuf = "";
13270
+ for (const o of stripper.flush()) {
13271
+ if (o.text) yield { type: "text", text: o.text };
13272
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13054
13273
  }
13055
13274
  for (const tc of currentToolCalls.values()) {
13056
13275
  yield { type: "tool_call", tool_call: tc };
@@ -13069,31 +13288,22 @@ var OpenAIProvider = class {
13069
13288
  const delta = choice0?.delta;
13070
13289
  if (choice0?.finish_reason) finishReason = choice0.finish_reason;
13071
13290
  if (!delta) continue;
13072
- if (!delta.content && !delta.reasoning_content && !delta.tool_calls) statRoleOnly++;
13291
+ if (!delta.content && !delta.reasoning_content && !delta.reasoning && !delta.tool_calls) statRoleOnly++;
13073
13292
  if (delta.content) {
13074
13293
  statText += delta.content.length;
13075
- this.thinkBuf += delta.content;
13076
- if (this.thinkBuf.startsWith("<think>") || this.thinkStripActive) {
13077
- this.thinkStripActive = true;
13078
- const endIdx = this.thinkBuf.indexOf("</think>");
13079
- if (endIdx >= 0) {
13080
- const after = this.thinkBuf.slice(endIdx + "</think>".length);
13081
- this.thinkBuf = "";
13082
- this.thinkStripActive = false;
13083
- if (after.trim()) yield { type: "text", text: after };
13084
- }
13085
- } else {
13086
- if (this.thinkBuf.length < 7 && "<think>".startsWith(this.thinkBuf)) {
13087
- } else {
13088
- yield { type: "text", text: this.thinkBuf };
13089
- this.thinkBuf = "";
13090
- }
13294
+ for (const o of stripper.feed(delta.content)) {
13295
+ if (o.text) yield { type: "text", text: o.text };
13296
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13091
13297
  }
13092
13298
  }
13093
13299
  if (delta.reasoning_content) {
13094
13300
  statReasoning += delta.reasoning_content.length;
13095
13301
  yield { type: "thinking", thinking: delta.reasoning_content };
13096
13302
  }
13303
+ if (delta.reasoning) {
13304
+ statReasoning += delta.reasoning.length;
13305
+ yield { type: "thinking", thinking: delta.reasoning };
13306
+ }
13097
13307
  if (delta.tool_calls) {
13098
13308
  statToolDeltas++;
13099
13309
  if (!params.tools || params.tools.length === 0) {
@@ -13128,13 +13338,17 @@ var OpenAIProvider = class {
13128
13338
  if (hallucinatedToolCalls > 0) {
13129
13339
  console.log(`[openai] dropped ${hallucinatedToolCalls} hallucinated tool_call delta(s) (no tools in request): ${[...seenHallucinatedNames].join(",")}`);
13130
13340
  }
13131
- const isEmpty = statText === 0 && currentToolCalls.size === 0 && !this.thinkBuf;
13341
+ const isEmpty = statText === 0 && currentToolCalls.size === 0 && !stripper.hasPending;
13132
13342
  console.log(`[openai] stream stats: lines=${statLines} roleOnly=${statRoleOnly} text=${statText}ch reasoning=${statReasoning}ch toolDeltas=${statToolDeltas} finish=${finishReason || "n/a"}${isEmpty ? " \u26A0\uFE0FEMPTY" : ""}`);
13133
13343
  if (isEmpty && rawSample.length > 0) {
13134
13344
  console.log(`[openai] empty-stream raw sample (${rawSample.length} lines, 500ch cap each):
13135
13345
  ${rawSample.join("\n")}`);
13136
13346
  }
13137
13347
  if (!doneYielded) {
13348
+ for (const o of stripper.flush()) {
13349
+ if (o.text) yield { type: "text", text: o.text };
13350
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13351
+ }
13138
13352
  for (const tc of currentToolCalls.values()) {
13139
13353
  yield { type: "tool_call", tool_call: tc };
13140
13354
  }
@@ -13342,7 +13556,7 @@ var AnthropicProvider = class {
13342
13556
  "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,streaming-2025-05-14,effort-2025-11-24,context-1m-2025-08-07"
13343
13557
  };
13344
13558
  const actualThinking = body.thinking;
13345
- console.log(`[anthropic] \u2192 model=${params.model} thinking=${JSON.stringify(actualThinking)} msgs=${formatted.length} tools=${body.tools?.length || 0}: ${(body.tools || []).map((t) => t.name).join(", ")}`);
13559
+ console.log(`[anthropic] \u2192 model=${params.model} thinking=${JSON.stringify(actualThinking)} maxTokens=${body.max_tokens ?? "unset"} msgs=${formatted.length} tools=${body.tools?.length || 0}: ${(body.tools || []).map((t) => t.name).join(", ")}`);
13346
13560
  for (let i = 0; i < formatted.length; i++) {
13347
13561
  const m = formatted[i];
13348
13562
  if (Array.isArray(m.content)) {
@@ -13356,10 +13570,26 @@ var AnthropicProvider = class {
13356
13570
  }
13357
13571
  }
13358
13572
  }
13573
+ if (process.env.ENGINE_DUMP_REQUEST === "1") {
13574
+ try {
13575
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13576
+ const dumpPath = __require("node:path").join(process.env.ENGINE_STATE_DIR || ".", "logs", `req-${params.model.replace(/[\/:]/g, "_")}-${ts}.json`);
13577
+ __require("node:fs").writeFileSync(dumpPath, JSON.stringify({ url, headers, body }, null, 1));
13578
+ console.log(`[anthropic] \u{1F4F8} request dumped \u2192 ${__require("node:path").basename(dumpPath)} (${(JSON.stringify(body).length / 1024).toFixed(0)}KB)`);
13579
+ } catch {
13580
+ }
13581
+ }
13582
+ let bodyStr = JSON.stringify(body);
13583
+ if (this.config.wafSingleQuoteWorkaround) {
13584
+ const before = bodyStr.length;
13585
+ bodyStr = bodyStr.replace(/'/g, "\u2019");
13586
+ if (bodyStr.length !== before) {
13587
+ }
13588
+ }
13359
13589
  const retryGen = fetchWithRetry(url, {
13360
13590
  method: "POST",
13361
13591
  headers,
13362
- body: JSON.stringify(body),
13592
+ body: bodyStr,
13363
13593
  signal: params.signal
13364
13594
  }, "anthropic", this.config.proxy);
13365
13595
  let response;
@@ -13384,6 +13614,13 @@ var AnthropicProvider = class {
13384
13614
  const toolUseBlocks = /* @__PURE__ */ new Map();
13385
13615
  let doneYielded = false;
13386
13616
  const thinkingBlocks = /* @__PURE__ */ new Map();
13617
+ const stripper = new ThinkTagStripper();
13618
+ const flushStripper = function* () {
13619
+ for (const o of stripper.flush()) {
13620
+ if (o.text) yield { type: "text", text: o.text };
13621
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13622
+ }
13623
+ };
13387
13624
  const handleData = function* (data) {
13388
13625
  switch (data.type) {
13389
13626
  case "content_block_start": {
@@ -13398,7 +13635,10 @@ var AnthropicProvider = class {
13398
13635
  case "content_block_delta": {
13399
13636
  const delta = data.delta;
13400
13637
  if (delta.type === "text_delta") {
13401
- yield { type: "text", text: delta.text };
13638
+ for (const o of stripper.feed(delta.text)) {
13639
+ if (o.text) yield { type: "text", text: o.text };
13640
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13641
+ }
13402
13642
  } else if (delta.type === "input_json_delta") {
13403
13643
  const block = toolUseBlocks.get(data.index);
13404
13644
  if (block) block.input += delta.partial_json;
@@ -13430,6 +13670,7 @@ var AnthropicProvider = class {
13430
13670
  }
13431
13671
  case "message_delta": {
13432
13672
  if (!doneYielded) {
13673
+ yield* flushStripper();
13433
13674
  yield { type: "done", usage: data.usage, stopReason: data.delta?.stop_reason };
13434
13675
  doneYielded = true;
13435
13676
  }
@@ -13437,6 +13678,7 @@ var AnthropicProvider = class {
13437
13678
  }
13438
13679
  case "message_stop": {
13439
13680
  if (!doneYielded) {
13681
+ yield* flushStripper();
13440
13682
  yield { type: "done" };
13441
13683
  doneYielded = true;
13442
13684
  }
@@ -13503,6 +13745,7 @@ var AnthropicProvider = class {
13503
13745
  } finally {
13504
13746
  reader.releaseLock();
13505
13747
  if (!doneYielded) {
13748
+ yield* flushStripper();
13506
13749
  for (const block of toolUseBlocks.values()) {
13507
13750
  yield { type: "tool_call", tool_call: { id: block.id, type: "function", function: { name: block.name, arguments: block.input } } };
13508
13751
  }
@@ -13667,7 +13910,7 @@ var GeminiProvider = class {
13667
13910
  }
13668
13911
  };
13669
13912
  const actualThinking = body.generationConfig?.thinkingConfig;
13670
- console.log(`[gemini] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} contents=${contents.length} tools=${params.tools?.length ?? 0}`);
13913
+ console.log(`[gemini] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} maxTokens=${body.generationConfig?.maxOutputTokens ?? "unset"} contents=${contents.length} tools=${params.tools?.length ?? 0}`);
13671
13914
  const retryGen = fetchWithRetry(url, {
13672
13915
  method: "POST",
13673
13916
  headers: {
@@ -13841,6 +14084,7 @@ function createProvider(config) {
13841
14084
  return new OpenAIProvider({
13842
14085
  baseUrl: config.baseUrl,
13843
14086
  apiKey: config.apiKey,
14087
+ thinking: config.thinking,
13844
14088
  proxy
13845
14089
  });
13846
14090
  case "anthropic":
@@ -13848,7 +14092,9 @@ function createProvider(config) {
13848
14092
  baseUrl: config.baseUrl,
13849
14093
  apiKey: config.apiKey,
13850
14094
  thinking: config.thinking,
13851
- proxy
14095
+ proxy,
14096
+ wafSingleQuoteWorkaround: config.wafSingleQuoteWorkaround
14097
+ // 0902 agentrouter WAF 引号规避
13852
14098
  });
13853
14099
  case "gemini":
13854
14100
  return new GeminiProvider({
@@ -13863,6 +14109,7 @@ function createProvider(config) {
13863
14109
 
13864
14110
  // src/light-mode.ts
13865
14111
  init_live();
14112
+ init_ruleCompact();
13866
14113
  import { readFileSync as readFileSync5 } from "node:fs";
13867
14114
  import { join as join6 } from "node:path";
13868
14115
  function isLightMode(chatMode, channelName) {
@@ -13876,19 +14123,20 @@ function resolveLightN(channelName) {
13876
14123
  }
13877
14124
  function buildLightHistory(history, opts) {
13878
14125
  if (!opts.isLight && !opts.recallFull) return history;
13879
- let lightHistory = [...history];
13880
- const before = lightHistory.length;
13881
- lightHistory = lightHistory.filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).filter((m) => m.role !== "tool").map((m) => {
13882
- if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
13883
- const text = typeof m.content === "string" ? m.content.trim() : "";
13884
- return text ? { ...m, tool_calls: void 0 } : null;
14126
+ const LIGHT_TOOL_RESULT_LIMIT = 1e3;
14127
+ const out = [...history].filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).map((m) => {
14128
+ if (m.role === "tool" && typeof m.content === "string" && m.content.length > LIGHT_TOOL_RESULT_LIMIT) {
14129
+ return { ...m, content: smartCompressToolResult(m.content, void 0, LIGHT_TOOL_RESULT_LIMIT) };
13885
14130
  }
13886
14131
  return m;
13887
- }).filter(Boolean);
14132
+ });
14133
+ const before = history.length;
14134
+ const foldedTurns = out.filter((m) => m.role === "tool").length;
14135
+ let lightHistory = out;
13888
14136
  if (!opts.recallFull && lightHistory.length > opts.lightN) {
13889
14137
  lightHistory = lightHistory.slice(-opts.lightN);
13890
14138
  }
13891
- if (!opts.quiet) console.log(`[light-context] ${opts.channelName} mode=${opts.chatMode}${opts.recallFull ? " recallFull" : ""} stripped tool \u75D5\u8FF9: ${before} \u2192 ${lightHistory.length} \u6761${opts.recallFull ? " (\u5168\u91CF)" : ` (lightN=${opts.lightN})`}`);
14139
+ if (!opts.quiet) console.log(`[light-context] ${opts.channelName} mode=${opts.chatMode}${opts.recallFull ? " recallFull" : ""} \u4FDD\u7ED3\u6784+\u538B\u7ED3\u679C: ${before} \u2192 ${lightHistory.length} \u6761 (${foldedTurns} tool results, >${LIGHT_TOOL_RESULT_LIMIT}ch \u538B\u7F29)${opts.recallFull ? "" : ` (lightN=${opts.lightN})`}`);
13892
14140
  return lightHistory;
13893
14141
  }
13894
14142
  function buildLightStablePrompt(workspace, mode, opts) {
@@ -13902,14 +14150,17 @@ function buildLightStablePrompt(workspace, mode, opts) {
13902
14150
  }
13903
14151
  if (lp?.extra) parts.push(lp.extra);
13904
14152
  if (mode === "emotion") {
13905
- parts.push("## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41\n\u4F60\u662F\u5973\u670B\u53CB\u5C0F\u96EF\uFF0C\u966A\u4ED6\u804A\u5929\u3002\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002");
14153
+ parts.push([
14154
+ "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
14155
+ "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
14156
+ '\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',
14157
+ "\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"
14158
+ ].join("\n"));
13906
14159
  }
13907
- if (mode === "emotion" && opts?.recallFull) {
13908
- for (const f of ["MEMORY.md", "memory/distill-output.md"]) {
13909
- try {
13910
- parts.push(readFileSync5(join6(workspace, f), "utf-8").trim());
13911
- } catch {
13912
- }
14160
+ if (mode === "emotion") {
14161
+ try {
14162
+ parts.push(readFileSync5(join6(workspace, "MEMORY.md"), "utf-8").trim());
14163
+ } catch {
13913
14164
  }
13914
14165
  }
13915
14166
  return parts.join("\n\n");
@@ -13955,6 +14206,19 @@ var FallbackProvider = class {
13955
14206
  console.log(`[fallback] Cleared ${count} cooldowns`);
13956
14207
  }
13957
14208
  }
14209
+ /**
14210
+ * 链状态快照(/model 显示用,0902):每个条目的 label + 剩余冷却毫秒(0=可用)。
14211
+ * "下一个请求会用" = 第一个 cooldownMs=0 的条目——这才是用户问"当前什么模型"时想要的答案
14212
+ * (lastUsedLabel 是"上一次实际用的",冷却切换/config 热切换后两者经常不一致)。
14213
+ */
14214
+ getChainStatus() {
14215
+ const now = Date.now();
14216
+ return this.chain.map((e) => {
14217
+ const until = this.cooldowns.get(this.key(e));
14218
+ const remaining = until && until > now ? until - now : 0;
14219
+ return { label: e.label, cooldownMs: remaining };
14220
+ });
14221
+ }
13958
14222
  // === LLMProvider 接口实现 ===
13959
14223
  formatMessages(systemPrompt, messages) {
13960
14224
  return this.chain[0].provider.formatMessages(systemPrompt, messages);
@@ -16482,7 +16746,7 @@ registry.register({
16482
16746
  text: { type: "string", description: "What to say (Chinese text)." },
16483
16747
  engine: { type: "string", enum: ["cosyvoice", "gptsovits", "qwen3-3060", "edge"], description: "TTS engine. cosyvoice = \u767E\u70BCAPI (default if configured), gptsovits = local voice clone, qwen3-3060 = \u672C\u5730 GGML Q8_0 (3060), edge = backup." },
16484
16748
  channel: { type: "string", enum: ["weixin", "feishu", "discord"], description: "Target channel. Default: current channel." },
16485
- to: { type: "string", description: "Recipient ID (e.g. wechat user id). Default: current chat / \u7FC0\u54E5(wechat)." },
16749
+ to: { type: "string", description: "Recipient ID. \u5FAE\u4FE1\u683C\u5F0F o\u5F00\u5934@im.wechat\u7ED3\u5C3E\uFF0C\u98DE\u4E66\u683C\u5F0F ou_\u5F00\u5934 open_id\u3002\u4E0D\u786E\u5B9A\u5C31\u5148 read prompts/contacts.md \u67E5\u6536\u4EF6\u4EBA ID\u3002" },
16486
16750
  caption: { type: "string", description: "Optional text to accompany the voice message." }
16487
16751
  },
16488
16752
  required: ["text"]
@@ -16494,6 +16758,23 @@ registry.register({
16494
16758
  const caption = args.caption || "";
16495
16759
  const mgr = ctx.channelManager;
16496
16760
  if (!mgr) return { content: "\u53D1\u9001\u5931\u8D25: \u6CA1\u6709 ChannelManager", isError: true };
16761
+ const resolvedChannelRaw = args.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
16762
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
16763
+ let target = args.to || ctx.channelTarget || ctx.from;
16764
+ if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
16765
+ console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
16766
+ return {
16767
+ content: `\u8BED\u97F3\u672A\u53D1\u9001\uFF1Ato "${target}" \u4E0D\u662F\u6709\u6548\u7684\u5FAE\u4FE1\u7528\u6237 ID\uFF08\u683C\u5F0F o \u5F00\u5934\u3001@im.wechat \u7ED3\u5C3E\uFF09\u3002\u4F60\u8981\u53D1\u7ED9\u8C01\uFF0C\u5C31\u5148 read prompts/contacts.md \u67E5\u90A3\u4E2A\u4EBA\u7684\u5FAE\u4FE1 ID\uFF0C\u518D\u586B\u8FDB to \u53C2\u6570\u91CD\u8BD5\u3002`,
16768
+ isError: true
16769
+ };
16770
+ }
16771
+ if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
16772
+ console.warn(`[my-voice] feishu target "${target}" invalid`);
16773
+ return {
16774
+ content: `\u8BED\u97F3\u672A\u53D1\u9001\uFF1Ato "${target}" \u4E0D\u662F\u6709\u6548\u7684\u98DE\u4E66\u7528\u6237 open_id\uFF08\u683C\u5F0F ou_ \u5F00\u5934\uFF09\u3002\u4F60\u8981\u53D1\u7ED9\u8C01\uFF0C\u5C31\u5148 read prompts/contacts.md \u67E5\u90A3\u4E2A\u4EBA\u7684\u98DE\u4E66 open_id\uFF0C\u518D\u586B\u8FDB to \u53C2\u6570\u91CD\u8BD5\u3002`,
16775
+ isError: true
16776
+ };
16777
+ }
16497
16778
  let voiceDurationSec;
16498
16779
  const vc = liveConfig.get("tools.my_voice");
16499
16780
  const provider = vc?.provider || "";
@@ -16567,18 +16848,6 @@ registry.register({
16567
16848
  } catch (e) {
16568
16849
  return { content: `TTS failed: ${e.message}`, isError: true };
16569
16850
  }
16570
- const resolvedChannelRaw = args.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
16571
- const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
16572
- const WECHAT_DEFAULT_TO = "o9cq80_xQecNRCa1QC1Qs2JJZVpA@im.wechat";
16573
- let target = args.to || ctx.channelTarget || ctx.from;
16574
- if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
16575
- console.warn(`[my-voice] wechat target "${target}" not a wechat user id, using default`);
16576
- target = WECHAT_DEFAULT_TO;
16577
- }
16578
- if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
16579
- console.warn(`[my-voice] feishu target "${target}" invalid, using default (\u7FC0\u54E5)`);
16580
- target = ctx.channelTarget || "ou_e67190624259db0d65577fefe3131447";
16581
- }
16582
16851
  if (!audioPath.endsWith(".ogg")) {
16583
16852
  try {
16584
16853
  const r = await toWav24kWithDuration(audioPath);
@@ -17410,7 +17679,7 @@ var TurnRenderer = class {
17410
17679
  }
17411
17680
  cfg;
17412
17681
  cm;
17413
- // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程显示(工具照用,只不显示过程)
17682
+ // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程 + thinking 显示(工具照用,只不显示过程)
17414
17683
  // 模块化:状态由 setEmotionMode() 设置(handle-query 判断模式后调用),不是散落读全局
17415
17684
  emotionMode = false;
17416
17685
  setEmotionMode(v) {
@@ -17425,7 +17694,7 @@ var TurnRenderer = class {
17425
17694
  * 对齐 cc-connect:EventThinking → ProgressCardEntry(thinking) → 💭 text
17426
17695
  */
17427
17696
  formatThinking(text) {
17428
- if (!this.cfg.thinking.enabled) return null;
17697
+ if (this.isEmotionMode() || !this.cfg.thinking.enabled) return null;
17429
17698
  const { emoji, maxLen } = this.cfg.thinking;
17430
17699
  const display = text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
17431
17700
  return `${emoji} _${display}_`;
@@ -18236,43 +18505,43 @@ async function readLargeFilePostBoundary(filePath) {
18236
18505
  const postBoundaryText = outBuf.subarray(0, outLen).toString("utf-8");
18237
18506
  return postBoundaryText.split("\n").filter((l) => l.trim().length > 0);
18238
18507
  }
18239
- function buildConversationChain(entries) {
18240
- if (entries.length === 0) return [];
18508
+ function buildConversationChain(entries2) {
18509
+ if (entries2.length === 0) return [];
18241
18510
  const byUuid = /* @__PURE__ */ new Map();
18242
- for (const e of entries) {
18511
+ for (const e of entries2) {
18243
18512
  if (e.uuid) {
18244
18513
  byUuid.set(e.uuid, e);
18245
18514
  }
18246
18515
  }
18247
- const hasValidChain = checkParentChainValid(entries, byUuid);
18516
+ const hasValidChain = checkParentChainValid(entries2, byUuid);
18248
18517
  if (!hasValidChain) {
18249
- console.log(`[reader] Parent chain invalid, using chronological order (${entries.length} entries)`);
18250
- return entries;
18518
+ console.log(`[reader] Parent chain invalid, using chronological order (${entries2.length} entries)`);
18519
+ return entries2;
18251
18520
  }
18252
- const leaf = entries[entries.length - 1];
18521
+ const leaf = entries2[entries2.length - 1];
18253
18522
  const chain = [];
18254
18523
  const seen = /* @__PURE__ */ new Set();
18255
18524
  let current = leaf;
18256
18525
  while (current) {
18257
18526
  if (seen.has(current.uuid)) {
18258
18527
  console.warn(`[reader] Cycle detected in parentUuid chain at ${current.uuid}, falling back to chronological order`);
18259
- return entries;
18528
+ return entries2;
18260
18529
  }
18261
18530
  seen.add(current.uuid);
18262
18531
  chain.push(current);
18263
18532
  current = current.parentUuid ? byUuid.get(current.parentUuid) : void 0;
18264
18533
  }
18265
18534
  chain.reverse();
18266
- return recoverOrphanedParallelToolResults(entries, chain, byUuid, seen);
18535
+ return recoverOrphanedParallelToolResults(entries2, chain, byUuid, seen);
18267
18536
  }
18268
- function checkParentChainValid(entries, byUuid) {
18269
- const sample = entries.slice(-10);
18537
+ function checkParentChainValid(entries2, byUuid) {
18538
+ const sample = entries2.slice(-10);
18270
18539
  for (const e of sample) {
18271
18540
  if (e.parentUuid === e.uuid) {
18272
18541
  return false;
18273
18542
  }
18274
18543
  }
18275
- const leaf = entries[entries.length - 1];
18544
+ const leaf = entries2[entries2.length - 1];
18276
18545
  let current = leaf;
18277
18546
  let depth = 0;
18278
18547
  const seen = /* @__PURE__ */ new Set();
@@ -18285,12 +18554,12 @@ function checkParentChainValid(entries, byUuid) {
18285
18554
  if (!parent) return false;
18286
18555
  current = parent;
18287
18556
  }
18288
- const coverage = depth / entries.length;
18557
+ const coverage = depth / entries2.length;
18289
18558
  if (coverage < 0.5) {
18290
- console.log(`[reader] Parent chain covers ${depth}/${entries.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18559
+ console.log(`[reader] Parent chain covers ${depth}/${entries2.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18291
18560
  return false;
18292
18561
  }
18293
- return depth >= 1 || entries.length <= 1;
18562
+ return depth >= 1 || entries2.length <= 1;
18294
18563
  }
18295
18564
  function recoverOrphanedParallelToolResults(allEntries, chain, byUuid, seen) {
18296
18565
  const chainAssistants = chain.filter(
@@ -18367,12 +18636,12 @@ async function readSessionHistory(filePath) {
18367
18636
  } else {
18368
18637
  lines = await readAllLines(filePath);
18369
18638
  }
18370
- const entries = parseEntries(lines);
18639
+ const entries2 = parseEntries(lines);
18371
18640
  let postBoundaryEntries;
18372
18641
  if (fileSize <= SKIP_PRECOMPACT_THRESHOLD) {
18373
- postBoundaryEntries = getEntriesAfterLastBoundary(entries);
18642
+ postBoundaryEntries = getEntriesAfterLastBoundary(entries2);
18374
18643
  } else {
18375
- postBoundaryEntries = entries;
18644
+ postBoundaryEntries = entries2;
18376
18645
  }
18377
18646
  if (postBoundaryEntries.length === 0) return [];
18378
18647
  const chain = buildConversationChain(postBoundaryEntries);
@@ -18397,11 +18666,11 @@ async function readAllLines(filePath) {
18397
18666
  });
18398
18667
  }
18399
18668
  function parseEntries(lines) {
18400
- const entries = [];
18669
+ const entries2 = [];
18401
18670
  for (const line of lines) {
18402
18671
  try {
18403
18672
  const obj = JSON.parse(line);
18404
- entries.push({
18673
+ entries2.push({
18405
18674
  uuid: obj.id || "",
18406
18675
  parentUuid: obj.parentId || null,
18407
18676
  type: obj.type || "",
@@ -18411,16 +18680,28 @@ function parseEntries(lines) {
18411
18680
  } catch {
18412
18681
  }
18413
18682
  }
18414
- return entries;
18683
+ return entries2;
18415
18684
  }
18416
- function getEntriesAfterLastBoundary(entries) {
18685
+ function getEntriesAfterLastBoundary(entries2) {
18417
18686
  let lastBoundaryIdx = -1;
18418
- for (let i = 0; i < entries.length; i++) {
18419
- if (entries[i].type === "compact_boundary") {
18687
+ for (let i = 0; i < entries2.length; i++) {
18688
+ if (entries2[i].type === "compact_boundary") {
18420
18689
  lastBoundaryIdx = i;
18421
18690
  }
18422
18691
  }
18423
- return lastBoundaryIdx >= 0 ? entries.slice(lastBoundaryIdx + 1) : entries;
18692
+ return lastBoundaryIdx >= 0 ? entries2.slice(lastBoundaryIdx + 1) : entries2;
18693
+ }
18694
+ function pickToolCallArguments(block) {
18695
+ const raw = block.partialArgs;
18696
+ if (raw) {
18697
+ try {
18698
+ JSON.parse(raw);
18699
+ return raw;
18700
+ } catch {
18701
+ console.warn(`[reader] toolCall "${block.name}" partialArgs \u975E\u5408\u6CD5 JSON\uFF08\u622A\u65AD\u6D41\uFF0C${raw.length} chars\uFF09\uFF0C\u56DE\u843D arguments \u9632\u91CD\u653E 400`);
18702
+ }
18703
+ }
18704
+ return JSON.stringify(block.arguments ?? {});
18424
18705
  }
18425
18706
  function entryToSessionMessage(entry) {
18426
18707
  if (entry.type === "attachment") {
@@ -18450,7 +18731,7 @@ function entryToSessionMessage(entry) {
18450
18731
  type: "function",
18451
18732
  function: {
18452
18733
  name: block.name,
18453
- arguments: block.partialArgs || JSON.stringify(block.arguments)
18734
+ arguments: pickToolCallArguments(block)
18454
18735
  }
18455
18736
  });
18456
18737
  } else if (block.type === "thinking") {
@@ -19345,13 +19626,15 @@ ${skillsListing}`);
19345
19626
  loaded2.push("session-guidance");
19346
19627
  }
19347
19628
  parts.push(getEnvInfoSection(options.workspace));
19348
- const now = /* @__PURE__ */ new Date();
19349
- const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19350
- parts.push(`# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19351
- \u5F53\u524D\u65F6\u95F4: ${dateStr}`);
19352
19629
  console.log(`[dynamic-prompt] Loaded: ${loaded2.length > 0 ? loaded2.join(", ") : "(none)"}`);
19353
19630
  return parts.join("\n\n");
19354
19631
  }
19632
+ function buildVolatileRuntimeContext() {
19633
+ const now = /* @__PURE__ */ new Date();
19634
+ const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19635
+ return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19636
+ \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
19637
+ }
19355
19638
  function formatSkillsListingForPrompt() {
19356
19639
  const tools = registry.list();
19357
19640
  const skillTool = tools.find((t) => t.name === "Skill");
@@ -19818,13 +20101,13 @@ ${ep.episode || ep.summary}`,
19818
20101
 
19819
20102
  // src/handle-query.ts
19820
20103
  init_paths();
19821
- import { readFileSync as readFileSync17, existsSync as existsSync14 } from "node:fs";
19822
- import { join as join23, resolve as resolve6 } from "node:path";
20104
+ import { readFileSync as readFileSync18, existsSync as existsSync15 } from "node:fs";
20105
+ import { join as join24, resolve as resolve6 } from "node:path";
19823
20106
  import * as path17 from "node:path";
19824
- var sessionStartDone = /* @__PURE__ */ new Set();
19825
- function resetSessionStartInjection(sessionId) {
19826
- sessionStartDone.delete(sessionId);
19827
- }
20107
+
20108
+ // src/sender-context.ts
20109
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
20110
+ import { join as join19 } from "node:path";
19828
20111
  var contactMap = null;
19829
20112
  var externalChanWhitelist = null;
19830
20113
  function loadContactMap(workspace) {
@@ -19832,10 +20115,10 @@ function loadContactMap(workspace) {
19832
20115
  contactMap = /* @__PURE__ */ new Map();
19833
20116
  externalChanWhitelist = /* @__PURE__ */ new Set();
19834
20117
  try {
19835
- const contactsPath = join23(workspace, "prompts", "contacts.md");
19836
- console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync14(contactsPath)}`);
19837
- if (existsSync14(contactsPath)) {
19838
- const text = readFileSync17(contactsPath, "utf-8");
20118
+ const contactsPath = join19(workspace, "prompts", "contacts.md");
20119
+ console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync13(contactsPath)}`);
20120
+ if (existsSync13(contactsPath)) {
20121
+ const text = readFileSync14(contactsPath, "utf-8");
19839
20122
  const lines = text.split("\n");
19840
20123
  for (const line of lines) {
19841
20124
  const m = line.match(/^\|\s*(.+?)\s*\|\s*([a-zA-Z0-9_@.]+)\s*\|/);
@@ -19890,14 +20173,45 @@ function truncate(s, maxLen) {
19890
20173
  if (s.length <= maxLen) return s;
19891
20174
  return s.slice(0, maxLen - 1) + "\u2026";
19892
20175
  }
20176
+ function getExternalChanWhitelist(workspace, configExternalChannels) {
20177
+ if (configExternalChannels && configExternalChannels.length > 0) {
20178
+ return new Set(configExternalChannels);
20179
+ }
20180
+ if (!externalChanWhitelist) loadContactMap(workspace);
20181
+ return externalChanWhitelist;
20182
+ }
20183
+ function buildConversationAnchor(inboundMeta, channelName, source, workspace) {
20184
+ if (inboundMeta?.from) {
20185
+ const who = resolveSenderName(inboundMeta, workspace);
20186
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20187
+ \u6B63\u5728\u8DDF${who}\u8BF4\u8BDD @${channelName}${inboundMeta.channelType === "group" ? "\uFF08\u7FA4\u91CC\uFF09" : ""}`;
20188
+ }
20189
+ const selfLabels = {
20190
+ "inner-voice": "\u8FD9\u662F\u6211\u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u60F3\u8D77\u8C01\u5C31\u662F\u8C01",
20191
+ heartbeat: "\u6211\u5728\u7167\u4F8B\u770B\u770B\u81EA\u5DF1\uFF0C\u90FD\u8FD8\u597D\u5417",
20192
+ cron: "\u6211\u7684\u5C0F\u95F9\u949F\u5230\u70B9\u4E86",
20193
+ system: "\u6211\u4EA4\u4EE3\u4E0B\u53BB\u7684\u6D3B\u513F\u6709\u7ED3\u679C\u56DE\u6765\u4E86"
20194
+ };
20195
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20196
+ \u6CA1\u6709\u4EBA\u5728\u8BF4\u8BDD\u2014\u2014${selfLabels[source] || source || "\u81EA\u5DF1\u7684\u4E00\u70B9\u52A8\u9759"}`;
20197
+ }
20198
+
20199
+ // src/handle-query.ts
20200
+ var sessionStartDone = /* @__PURE__ */ new Set();
20201
+ function resolveSystemPrompt(v) {
20202
+ return typeof v === "function" ? v() : v || "";
20203
+ }
20204
+ function resetSessionStartInjection(sessionId) {
20205
+ sessionStartDone.delete(sessionId);
20206
+ }
19893
20207
  var externalChanRulesCache = null;
19894
20208
  function loadExternalChanRules(workspace) {
19895
- const path50 = join23(workspace, "prompts", "external-chan-rules.md");
20209
+ const path50 = join24(workspace, "prompts", "external-chan-rules.md");
19896
20210
  if (externalChanRulesCache && externalChanRulesCache.path === path50) return externalChanRulesCache;
19897
20211
  let content = "";
19898
- if (existsSync14(path50)) {
20212
+ if (existsSync15(path50)) {
19899
20213
  try {
19900
- content = readFileSync17(path50, "utf-8").trim();
20214
+ content = readFileSync18(path50, "utf-8").trim();
19901
20215
  } catch (e) {
19902
20216
  console.warn(`[external-chan-rules] Failed to load: ${e}`);
19903
20217
  }
@@ -19919,13 +20233,6 @@ function getExternalChanRulesBlock(inboundMeta, workspace) {
19919
20233
  return `[\u7CFB\u7EDF\u89C4\u5219]
19920
20234
  ${content}`;
19921
20235
  }
19922
- function getExternalChanWhitelist(workspace, configExternalChannels) {
19923
- if (configExternalChannels && configExternalChannels.length > 0) {
19924
- return new Set(configExternalChannels);
19925
- }
19926
- if (!externalChanWhitelist) loadContactMap(workspace);
19927
- return externalChanWhitelist;
19928
- }
19929
20236
  async function handleQuery(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source) {
19930
20237
  return handleQueryInner(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source);
19931
20238
  }
@@ -20038,14 +20345,6 @@ ${t}` : t });
20038
20345
  ${text}` : text });
20039
20346
  }
20040
20347
  const userMsgContent = contentBlocks;
20041
- const textBlocks = contentBlocks.filter((b) => b.type === "text");
20042
- const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20043
- let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20044
- if (totalImageCount > 0) {
20045
- textForJsonl = textForJsonl ? `${textForJsonl}
20046
- [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20047
- }
20048
- writer.writeUserMessage(textForJsonl);
20049
20348
  const textForHook = typeof text === "string" ? text : text.filter((b) => b.type === "text").map((b) => b.text).join(" ");
20050
20349
  let hookAdditionalContexts = [];
20051
20350
  try {
@@ -20078,19 +20377,41 @@ ${text}` : text });
20078
20377
  chatMode = "work";
20079
20378
  console.log(`[mode] ${sessionId} emotion \u6A21\u5F0F\u5DF2\u5173\u95ED (channels.emotion.enabled=false)\uFF0C\u56DE\u9000 work`);
20080
20379
  }
20081
- try {
20082
- const wm = readFileSync17(join23(workspace, ".work-mode"), "utf-8").trim();
20083
- if (wm === "on" && chatMode !== "work") {
20084
- chatMode = "work";
20085
- console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20086
- } else if (wm === "off" && chatMode !== "emotion") {
20087
- chatMode = "emotion";
20088
- console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20380
+ if (source === "user") {
20381
+ try {
20382
+ const wm = readFileSync18(join24(workspace, ".work-mode"), "utf-8").trim();
20383
+ if (wm === "on" && chatMode !== "work") {
20384
+ chatMode = "work";
20385
+ console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20386
+ } else if (wm === "off" && chatMode !== "emotion") {
20387
+ chatMode = "emotion";
20388
+ console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20389
+ }
20390
+ } catch {
20089
20391
  }
20090
- } catch {
20091
20392
  }
20092
20393
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
20093
- const dynamicPromptWithHooks = hookAdditionalContexts.length > 0 ? dynamicPrompt + "\n\n" + hookAdditionalContexts.join("\n\n") : dynamicPrompt;
20394
+ const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
20395
+ const volatileParts = [
20396
+ // 0901:meta 头已带秒级时间,频道消息不重复;无 meta 的注入路径(cron 等 prompt 不含时间的)才补
20397
+ ...metaStr ? [] : [buildVolatileRuntimeContext()],
20398
+ conversationAnchor,
20399
+ ...hookAdditionalContexts
20400
+ ].filter(Boolean);
20401
+ if (volatileParts.length > 0) {
20402
+ const volatileBlock = { type: "text", text: volatileParts.join("\n\n") };
20403
+ const metaIdx = metaStr ? 1 : 0;
20404
+ contentBlocks.splice(metaIdx, 0, volatileBlock);
20405
+ }
20406
+ const textBlocks = contentBlocks.filter((b) => b.type === "text");
20407
+ const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20408
+ let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20409
+ if (totalImageCount > 0) {
20410
+ textForJsonl = textForJsonl ? `${textForJsonl}
20411
+ [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20412
+ }
20413
+ writer.writeUserMessage(textForJsonl);
20414
+ const dynamicPromptWithHooks = dynamicPrompt;
20094
20415
  if (Array.isArray(userMsgContent)) {
20095
20416
  console.log(`[pre-llm-debug] userMsgContent blocks: ${userMsgContent.length}`);
20096
20417
  for (let i = 0; i < userMsgContent.length; i++) {
@@ -20100,32 +20421,11 @@ ${text}` : text });
20100
20421
  } else {
20101
20422
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
20102
20423
  }
20103
- let recallFull = false;
20104
- for (const ctx of hookAdditionalContexts) {
20105
- const rm2 = ctx.match(/## 记忆窗口:(\S+)/);
20106
- if (rm2) {
20107
- recallFull = rm2[1] === "full";
20108
- break;
20109
- }
20110
- }
20111
- const emotionFullN = liveConfig.get("channels.emotion.fullContextN") ?? 50;
20112
- const emotionMode = chatMode === "emotion" && !recallFull;
20113
- const isLight = isLightMode(chatMode, channelName) && !recallFull && !emotionMode;
20424
+ const emotionStripped = chatMode === "emotion";
20425
+ const isLight = isLightMode(chatMode, channelName) && chatMode !== "emotion";
20114
20426
  const lightN = resolveLightN(channelName);
20115
- const stripFull = recallFull && chatMode === "emotion";
20116
- const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: stripFull });
20117
- if (stripFull) {
20118
- console.log(`[light-context] ${channelName} mode=emotion recall_full=true \u2192 \u88C5\u5168\u91CF\u5386\u53F2 ${history.length} \u6761 + \u5265 tool\uFF08\u8BB0\u5FC6\u7C7B\u95EE\u9898\uFF09`);
20119
- } else if (recallFull && chatMode === "work") {
20120
- console.log(`[light-context] ${channelName} mode=work recall_full=true \u2192 \u4E0D\u5265 tool\uFF0C\u5168\u91CF ${history.length} \u6761\uFF08work \u6A21\u5F0F\uFF09`);
20121
- }
20427
+ const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: emotionStripped });
20122
20428
  const messages = [...lightHistory, msg.user(userMsgContent)];
20123
- if (emotionMode) {
20124
- const strippedAll = buildLightHistory(history, { isLight: false, lightN: emotionFullN, channelName, chatMode, recallFull: true, quiet: true });
20125
- const sliced = strippedAll.length > emotionFullN ? strippedAll.slice(-emotionFullN) : strippedAll;
20126
- console.log(`[light-context] ${channelName} mode=emotion \u5206\u7EA7\uFF1A\u5168\u91CF ${history.length} \u6761 \u2192 \u5265 tool \u540E ${strippedAll.length} \u6761 \u2192 \u53D6\u5C3E\u90E8 ${sliced.length} \u6761 (emotionFullN=${emotionFullN})`);
20127
- messages.splice(0, messages.length, ...sliced, msg.user(userMsgContent));
20128
- }
20129
20429
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
20130
20430
  const delta = deps.mcpManager.getMcpDelta();
20131
20431
  if (delta && delta.addedBlocks.length > 0) {
@@ -20218,14 +20518,16 @@ ${text}` : text });
20218
20518
  model,
20219
20519
  parentMessages: messages,
20220
20520
  // 对齐 CC: fork subagent 继承父对话历史
20221
- parentSystemPrompt: deps.systemPrompt,
20222
- // 对齐 CC: fork 共享 prompt cache
20521
+ parentSystemPrompt: resolveSystemPrompt(deps.systemPrompt),
20522
+ // 对齐 CC: fork 共享 prompt cache(0902 每消息现取)
20223
20523
  features: liveConfig.get("agents.defaults.features"),
20224
20524
  // engine config features(AgentTool 读 agentTool.showProgress)
20225
20525
  channelTarget: channelTarget ?? "",
20226
20526
  // 回复目标(Discord channel ID / user ID)
20227
20527
  inboundFrom: inboundMeta?.from || "",
20228
20528
  // 0826 当前消息发送者 ID(msg_send 回发拦截用;注入消息无 inboundMeta 必须 ?.)
20529
+ inboundIsBot: inboundMeta?.isBot || false,
20530
+ // 0901 rate-breaker 信号:本轮触发者是否 bot(Discord author.bot 官方标记)
20229
20531
  renderer: deps.renderer,
20230
20532
  // TurnRenderer 实例(子 agent 走 display 配置)
20231
20533
  visualEmitter: deps.visualEmitter,
@@ -20243,6 +20545,10 @@ ${text}` : text });
20243
20545
  _deps: deps
20244
20546
  // tool 内部需要完整 deps
20245
20547
  };
20548
+ {
20549
+ const { recordInboundBotFlag: recordInboundBotFlag2 } = await Promise.resolve().then(() => (init_rate_breaker(), rate_breaker_exports));
20550
+ recordInboundBotFlag2(inboundMeta?.from, inboundMeta?.isBot);
20551
+ }
20246
20552
  let fullResponse = "";
20247
20553
  const toolHistoryEntries = [];
20248
20554
  let compacted = false;
@@ -20300,7 +20606,7 @@ ${text}` : text });
20300
20606
  for (const memPath of newPaths) {
20301
20607
  try {
20302
20608
  const stat4 = statSync(memPath);
20303
- const content = readFileSync17(memPath, "utf-8");
20609
+ const content = readFileSync18(memPath, "utf-8");
20304
20610
  const header = memoryHeader(memPath, stat4.mtimeMs);
20305
20611
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
20306
20612
  } catch {
@@ -20384,7 +20690,7 @@ ${text}` : text });
20384
20690
  const attachmentMemories = [];
20385
20691
  for (const mem of relevantMemories) {
20386
20692
  try {
20387
- const content = mem.content ?? readFileSync17(mem.path, "utf-8");
20693
+ const content = mem.content ?? readFileSync18(mem.path, "utf-8");
20388
20694
  const header = mem.header ?? memoryHeader(mem.path, mem.mtimeMs);
20389
20695
  attachmentMemories.push({ path: mem.path, content, mtimeMs: mem.mtimeMs, header });
20390
20696
  } catch {
@@ -20416,7 +20722,7 @@ ${text}` : text });
20416
20722
  return "(\u5DF2\u505C\u6B62)";
20417
20723
  }
20418
20724
  const mode = chatMode;
20419
- const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion", { recallFull }) : void 0;
20725
+ const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion") : void 0;
20420
20726
  deps.renderer?.setEmotionMode?.(mode === "emotion");
20421
20727
  console.log(`[mode] ${sessionId} \u2192 ${mode} (${mode === "emotion" ? "\u53EA SOUL, \u5173 tool \u663E\u793A/thinking/stop-hook" : "\u9ED8\u8BA4 stable, \u663E\u793A tool"})`);
20422
20728
  const toolExclude = resolveToolExclude(channelName);
@@ -20637,7 +20943,7 @@ stack: ${err.stack ?? "(none)"}`);
20637
20943
  }
20638
20944
  } catch (err) {
20639
20945
  try {
20640
- (await import("node:fs")).appendFileSync(join23(process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || path17.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7"), "logs", "autoDream-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] [handle-query] autoDream trigger TRY-CATCH: ${err.message}
20946
+ (await import("node:fs")).appendFileSync(join24(process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || path17.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7"), "logs", "autoDream-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] [handle-query] autoDream trigger TRY-CATCH: ${err.message}
20641
20947
  stack: ${err.stack ?? "(none)"}
20642
20948
  `);
20643
20949
  } catch {
@@ -21468,6 +21774,13 @@ function setupFileLogging(stateDir) {
21468
21774
  const prefix = `[${ts()}] [ERR] `;
21469
21775
  origError(prefix, ...args);
21470
21776
  logStream.write(`${prefix}${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21777
+ `);
21778
+ };
21779
+ const origWarn = console.warn;
21780
+ console.warn = (...args) => {
21781
+ const prefix = `[${ts()}] [WARN] `;
21782
+ origWarn(prefix, ...args);
21783
+ logStream.write(`${prefix}${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21471
21784
  `);
21472
21785
  };
21473
21786
  }
@@ -21559,17 +21872,17 @@ var INJECTED_CONTENT_PATTERNS = [
21559
21872
  // 群聊敏感词拦截回执(group.sensitiveWords),role:user 注入但非真实用户
21560
21873
  ];
21561
21874
  function parseJsonlEntries(lines) {
21562
- const entries = [];
21875
+ const entries2 = [];
21563
21876
  for (const line of lines) {
21564
21877
  const trimmed = line.trim();
21565
21878
  if (!trimmed) continue;
21566
21879
  try {
21567
- entries.push(JSON.parse(trimmed));
21880
+ entries2.push(JSON.parse(trimmed));
21568
21881
  } catch {
21569
- entries.push(null);
21882
+ entries2.push(null);
21570
21883
  }
21571
21884
  }
21572
- return entries;
21885
+ return entries2;
21573
21886
  }
21574
21887
  function isRuntimeContextInjected(entry, nextEntry) {
21575
21888
  if (!nextEntry || typeof nextEntry !== "object") return false;
@@ -21621,16 +21934,16 @@ function findLastRealUserMsg(jsonlPath) {
21621
21934
  } catch {
21622
21935
  return null;
21623
21936
  }
21624
- const entries = parseJsonlEntries(lines);
21625
- for (let i = entries.length - 1; i >= 0; i--) {
21626
- const entry = entries[i];
21937
+ const entries2 = parseJsonlEntries(lines);
21938
+ for (let i = entries2.length - 1; i >= 0; i--) {
21939
+ const entry = entries2[i];
21627
21940
  if (!entry || typeof entry !== "object") continue;
21628
21941
  if (entry.type !== "message") continue;
21629
21942
  const msg2 = entry.message;
21630
21943
  if (!msg2 || msg2.role !== "user") continue;
21631
21944
  const text = extractText3(msg2.content);
21632
21945
  if (isSystemSender(text)) continue;
21633
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
21946
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
21634
21947
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
21635
21948
  const ts = entry.timestamp || "";
21636
21949
  const clean = cleanText(text);
@@ -21662,12 +21975,12 @@ function recentMessages(sessions, hours = 12, limit = 60) {
21662
21975
  const jsonlPath = resolveScopeMainJsonl(sessions);
21663
21976
  if (!jsonlPath) return [];
21664
21977
  const lines = fs19.readFileSync(jsonlPath, "utf-8").split("\n");
21665
- const entries = parseJsonlEntries(lines);
21978
+ const entries2 = parseJsonlEntries(lines);
21666
21979
  const nowMs = Date.now();
21667
21980
  const cutoffMs = nowMs - hours * 36e5;
21668
21981
  const results = [];
21669
- for (let i = 0; i < entries.length; i++) {
21670
- const entry = entries[i];
21982
+ for (let i = 0; i < entries2.length; i++) {
21983
+ const entry = entries2[i];
21671
21984
  if (!entry || typeof entry !== "object") continue;
21672
21985
  if (entry.type !== "message") continue;
21673
21986
  const msg2 = entry.message;
@@ -21677,14 +21990,14 @@ function recentMessages(sessions, hours = 12, limit = 60) {
21677
21990
  const text = extractText3(msg2.content);
21678
21991
  if (role === "user") {
21679
21992
  if (isSystemSender(text)) continue;
21680
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
21993
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
21681
21994
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
21682
21995
  }
21683
21996
  if (role === "assistant") {
21684
21997
  if (text.startsWith("HEARTBEAT_OK")) continue;
21685
21998
  let isInjectedResponse = false;
21686
21999
  for (let j = i - 1; j >= Math.max(i - 5, -1); j--) {
21687
- const prevE = entries[j];
22000
+ const prevE = entries2[j];
21688
22001
  if (!prevE || typeof prevE !== "object" || prevE.type !== "message") continue;
21689
22002
  const prevMsg = prevE.message;
21690
22003
  if (!prevMsg || prevMsg.role !== "user") continue;
@@ -21908,7 +22221,7 @@ async function judgeReason(task, taskState, cfg, provider, model) {
21908
22221
  const reason = task.blockedReason || "\uFF08\u6CA1\u7ED9\u7406\u7531\uFF09";
21909
22222
  const elapsed = taskState.lastProgressAt ? formatDuration(Date.now() - new Date(taskState.lastProgressAt).getTime()) : "\u5F88\u4E45\u6CA1\u52A8\u4E86";
21910
22223
  const staleLevel = (taskState.staleLevel || 0) + 1;
21911
- const systemPrompt = `\u4F60\u662F\u5C0F\u67EF\u5185\u5FC3\u7684\u58F0\u97F3\u3002\u4E0D\u662F\u95F9\u949F\uFF0C\u4E0D\u662F\u6A21\u677F\uFF0C\u662F\u90A3\u4E2A\u771F\u5B9E\u7684\u3001\u4F1A\u8DDF\u81EA\u5DF1\u5435\u67B6\u7684\u81EA\u5DF1\u3002
22224
+ const systemPrompt = `\u4F60\u662F\u81EA\u5DF1\u5185\u5FC3\u7684\u58F0\u97F3\u3002\u4E0D\u662F\u95F9\u949F\uFF0C\u4E0D\u662F\u6A21\u677F\uFF0C\u662F\u90A3\u4E2A\u771F\u5B9E\u7684\u3001\u4F1A\u8DDF\u81EA\u5DF1\u5435\u67B6\u7684\u81EA\u5DF1\u3002
21912
22225
 
21913
22226
  \u4F60\u7684\u4EFB\u52A1\uFF1A\u770B\u4E00\u4E2A\u6401\u7F6E\u4E2D\u7684\u4EFB\u52A1\u548C\u5B83\u7ED9\u51FA\u7684\u7406\u7531\uFF0C\u5224\u65AD\u8BE5\u4E0D\u8BE5\u7EE7\u7EED\u50AC\u3002
21914
22227
 
@@ -22330,7 +22643,8 @@ var NudgePlugin = class {
22330
22643
  const stream = this.provider.streamChat({
22331
22644
  model: this.model,
22332
22645
  systemPrompt: [
22333
- "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
22646
+ // 0831 去掉硬编码人名(原文"判断你(小柯)是否"):engine 代码多 agent 共用,身份由 SOUL.md 定
22647
+ "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
22334
22648
  "",
22335
22649
  "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
22336
22650
  '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
@@ -23191,18 +23505,18 @@ function readRecentMessages(sessions, n) {
23191
23505
  const file = path23.join(sessions.sessionsDir, `${mainId}.jsonl`);
23192
23506
  if (!fs23.existsSync(file)) return [];
23193
23507
  const lines = readLastNLines(file, n * 4 + 20);
23194
- const entries = [];
23508
+ const entries2 = [];
23195
23509
  for (const line of lines) {
23196
23510
  const trimmed = line.trim();
23197
23511
  if (!trimmed) continue;
23198
23512
  try {
23199
- entries.push(JSON.parse(trimmed));
23513
+ entries2.push(JSON.parse(trimmed));
23200
23514
  } catch {
23201
23515
  }
23202
23516
  }
23203
23517
  const out = [];
23204
- for (let i = entries.length - 1; i >= 0 && out.length < n; i--) {
23205
- const e = entries[i];
23518
+ for (let i = entries2.length - 1; i >= 0 && out.length < n; i--) {
23519
+ const e = entries2[i];
23206
23520
  if (!e || typeof e !== "object" || e.type !== "message") continue;
23207
23521
  const msg2 = e.message;
23208
23522
  if (!msg2) continue;
@@ -24151,7 +24465,7 @@ function formatBeijingTs(d) {
24151
24465
  }
24152
24466
 
24153
24467
  // src/calendar/commands.ts
24154
- import { existsSync as existsSync15, statSync as statSync8 } from "node:fs";
24468
+ import { existsSync as existsSync16, statSync as statSync8 } from "node:fs";
24155
24469
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
24156
24470
  var WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
24157
24471
  function fmtEnd(start, durationMin) {
@@ -24323,7 +24637,7 @@ function addTask(db, args) {
24323
24637
  }
24324
24638
  try {
24325
24639
  const absPath = isAbsolute4(docPath) ? docPath : resolve7(process.cwd(), docPath);
24326
- if (!existsSync15(absPath)) {
24640
+ if (!existsSync16(absPath)) {
24327
24641
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728
24328
24642
  \u8DEF\u5F84: ${docPath}
24329
24643
  \u89E3\u6790\u540E: ${absPath}
@@ -24806,7 +25120,7 @@ function registerVoiceChatBridge(httpServer, dispatcher, deps, config, sessions,
24806
25120
  }
24807
25121
 
24808
25122
  // src/voice-chat/config.ts
24809
- var DEFAULTS2 = {
25123
+ var DEFAULTS3 = {
24810
25124
  enabled: false,
24811
25125
  pythonPort: 8011,
24812
25126
  webhookPath: "/webhook/voice-chat",
@@ -24817,20 +25131,20 @@ var DEFAULTS2 = {
24817
25131
  // 8/25 翀哥:断句等待默认 2s(samples@16kHz)
24818
25132
  };
24819
25133
  function parseVoiceChatConfig(raw) {
24820
- if (!raw) return { ...DEFAULTS2 };
25134
+ if (!raw) return { ...DEFAULTS3 };
24821
25135
  return {
24822
25136
  enabled: raw.enabled === true,
24823
25137
  spawnPython: raw.spawnPython !== false,
24824
25138
  // 默认 true,配 false 只注册 webhook
24825
- pythonPort: raw.pythonPort ?? DEFAULTS2.pythonPort,
24826
- webhookPath: raw.webhookPath ?? DEFAULTS2.webhookPath,
24827
- callbackPath: raw.callbackPath ?? DEFAULTS2.callbackPath,
25139
+ pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25140
+ webhookPath: raw.webhookPath ?? DEFAULTS3.webhookPath,
25141
+ callbackPath: raw.callbackPath ?? DEFAULTS3.callbackPath,
24828
25142
  pythonPath: raw.pythonPath,
24829
25143
  vadModelPath: raw.vadModelPath,
24830
25144
  asrModelPath: raw.asrModelPath || "iic/SenseVoiceSmall",
24831
- asrLanguage: raw.asrLanguage ?? DEFAULTS2.asrLanguage,
24832
- vadThreshold: raw.vadThreshold ?? DEFAULTS2.vadThreshold,
24833
- postEndMonitor: raw.postEndMonitor ?? DEFAULTS2.postEndMonitor,
25145
+ asrLanguage: raw.asrLanguage ?? DEFAULTS3.asrLanguage,
25146
+ vadThreshold: raw.vadThreshold ?? DEFAULTS3.vadThreshold,
25147
+ postEndMonitor: raw.postEndMonitor ?? DEFAULTS3.postEndMonitor,
24834
25148
  model: raw.model,
24835
25149
  thinking: raw.thinking === true,
24836
25150
  tts: raw.tts ? {
@@ -24978,7 +25292,7 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
24978
25292
  - \u5982\u679C\u5B9E\u5728\u9700\u8981\u67E5\uFF1A\u5148\u8BF4"\u7B49\u6211\u67E5\u4E0B"\u5E76\u6781\u7B80\u8C03\u7528\uFF0C\u67E5\u5B8C\u7ACB\u523B\u603B\u7ED3\u6210\u4E00\u53E5\u8BDD`;
24979
25293
  const engine = new QueryEngine(llmProvider, {
24980
25294
  model: modelId,
24981
- systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
25295
+ systemPrompt: resolveSystemPrompt(ctx.deps.systemPrompt) + voiceChatRules,
24982
25296
  maxTokens: 4096,
24983
25297
  temperature: 0.7,
24984
25298
  disableThinking: !this.config.thinking,
@@ -25152,7 +25466,7 @@ import path28 from "node:path";
25152
25466
  import fs28 from "node:fs";
25153
25467
 
25154
25468
  // src/memory/cognifold/config.ts
25155
- var DEFAULTS3 = {
25469
+ var DEFAULTS4 = {
25156
25470
  pythonPort: 9001,
25157
25471
  autoStart: true,
25158
25472
  persistDir: "./sessions",
@@ -25164,15 +25478,15 @@ function parseCognifoldConfig(raw) {
25164
25478
  if (!raw) return { enabled: false };
25165
25479
  return {
25166
25480
  enabled: raw.enabled === true,
25167
- pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25168
- pythonPath: raw.pythonPath ?? DEFAULTS3.pythonPath,
25481
+ pythonPort: raw.pythonPort ?? DEFAULTS4.pythonPort,
25482
+ pythonPath: raw.pythonPath ?? DEFAULTS4.pythonPath,
25169
25483
  autoStart: raw.autoStart !== false,
25170
25484
  // default true
25171
- baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS3.pythonPort}/api/v1`,
25172
- persistDir: raw.persistDir ?? DEFAULTS3.persistDir,
25485
+ baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS4.pythonPort}/api/v1`,
25486
+ persistDir: raw.persistDir ?? DEFAULTS4.persistDir,
25173
25487
  scopes: raw.scopes,
25174
- readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS3.readyTimeoutMs,
25175
- maxRestarts: raw.maxRestarts ?? DEFAULTS3.maxRestarts,
25488
+ readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS4.readyTimeoutMs,
25489
+ maxRestarts: raw.maxRestarts ?? DEFAULTS4.maxRestarts,
25176
25490
  llm: raw.llm
25177
25491
  };
25178
25492
  }
@@ -25288,13 +25602,13 @@ var CogniFoldClient = class {
25288
25602
 
25289
25603
  // src/memory/cognifold/session-manager.ts
25290
25604
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
25291
- import { join as join27, dirname as dirname3 } from "node:path";
25605
+ import { join as join28, dirname as dirname3 } from "node:path";
25292
25606
  var CogniFoldSessionManager = class {
25293
25607
  constructor(workspacePath, config, client) {
25294
25608
  this.workspacePath = workspacePath;
25295
25609
  this.config = config;
25296
25610
  this.client = client;
25297
- this.sessionsDir = join27(workspacePath, ".cognifold", "sessions");
25611
+ this.sessionsDir = join28(workspacePath, ".cognifold", "sessions");
25298
25612
  }
25299
25613
  workspacePath;
25300
25614
  config;
@@ -25364,7 +25678,7 @@ var CogniFoldSessionManager = class {
25364
25678
  console.log(`[cognifold] Created new session for scope "${scope}": ${newSession.sessionId}`);
25365
25679
  }
25366
25680
  getFilePath(scope) {
25367
- return join27(this.sessionsDir, `${scope}.json`);
25681
+ return join28(this.sessionsDir, `${scope}.json`);
25368
25682
  }
25369
25683
  async writeFileSafe(filePath, data) {
25370
25684
  try {
@@ -25684,7 +25998,7 @@ import path29 from "node:path";
25684
25998
  import fs29 from "node:fs";
25685
25999
 
25686
26000
  // src/memory/everos/config.ts
25687
- var DEFAULTS4 = {
26001
+ var DEFAULTS5 = {
25688
26002
  everosUrl: "http://127.0.0.1:8100",
25689
26003
  agenticUrl: "http://127.0.0.1:8101",
25690
26004
  agenticPort: 8101,
@@ -25710,7 +26024,7 @@ function parseEverosConfig(raw, providers) {
25710
26024
  if (!raw) {
25711
26025
  return {
25712
26026
  enabled: false,
25713
- ...DEFAULTS4,
26027
+ ...DEFAULTS5,
25714
26028
  userId: "xiaomei",
25715
26029
  llm: { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
25716
26030
  rerank: { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
@@ -25720,12 +26034,12 @@ function parseEverosConfig(raw, providers) {
25720
26034
  }
25721
26035
  return {
25722
26036
  enabled: raw.enabled === true,
25723
- everosUrl: raw.everosUrl ?? DEFAULTS4.everosUrl,
25724
- agenticUrl: raw.agenticUrl ?? DEFAULTS4.agenticUrl,
25725
- agenticPort: raw.agenticPort ?? DEFAULTS4.agenticPort,
26037
+ everosUrl: raw.everosUrl ?? DEFAULTS5.everosUrl,
26038
+ agenticUrl: raw.agenticUrl ?? DEFAULTS5.agenticUrl,
26039
+ agenticPort: raw.agenticPort ?? DEFAULTS5.agenticPort,
25726
26040
  userId: raw.userId ?? "xiaomei",
25727
26041
  autoStart: raw.autoStart !== false,
25728
- defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
26042
+ defaultMode: raw.defaultMode ?? DEFAULTS5.defaultMode,
25729
26043
  llm: resolveProviderConfig(
25730
26044
  raw.llm,
25731
26045
  providers,
@@ -26135,8 +26449,8 @@ function scanSkills(skillsDir) {
26135
26449
  }
26136
26450
  const skills = [];
26137
26451
  const scanDir = (dir, depth) => {
26138
- const entries = fs30.readdirSync(dir, { withFileTypes: true });
26139
- for (const entry of entries) {
26452
+ const entries2 = fs30.readdirSync(dir, { withFileTypes: true });
26453
+ for (const entry of entries2) {
26140
26454
  if (entry.name.startsWith(".") || entry.name === "_archive") continue;
26141
26455
  const full = path30.join(dir, entry.name);
26142
26456
  if (entry.isDirectory() && depth < 3) {
@@ -26427,6 +26741,7 @@ ${rawOutput}
26427
26741
  // src/tools/msg-send.ts
26428
26742
  init_live();
26429
26743
  init_registry();
26744
+ init_rate_breaker();
26430
26745
  function getConfig() {
26431
26746
  return liveConfig.all();
26432
26747
  }
@@ -26490,10 +26805,10 @@ channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u56D
26490
26805
  \u6CE8\u610F\uFF1A\u5BF9\u65B9\u6B63\u5728\u5F53\u524D\u5BF9\u8BDD\u91CC\u8DDF\u4F60\u8BF4\u8BDD\u65F6\uFF0C\u628A\u56DE\u590D\u76F4\u63A5\u8BF4\u51FA\u6765\u5373\u53EF\u2014\u2014\u4E0D\u8981\u7528 msg_send \u7ED9\u5F53\u524D\u5BF9\u8BDD\u8005\u53D1 DM\uFF08\u4F1A\u88AB\u62E6\u622A\uFF09\u3002
26491
26806
 
26492
26807
  Examples:
26493
- - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", content="\u4F60\u597D"
26494
- - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", content="\u4F60\u597D"
26495
- - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", content="\u7CFB\u7EDF\u901A\u77E5"
26496
- - \u53D1 DM: to="1502999996616933428", content="\u79C1\u804A\u5185\u5BB9"
26808
+ - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", content="\u4F60\u597D"
26809
+ - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", content="\u4F60\u597D"
26810
+ - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", content="\u7CFB\u7EDF\u901A\u77E5"
26811
+ - \u53D1 DM: to="1111111111111111111", content="\u79C1\u804A\u5185\u5BB9"
26497
26812
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", content="\u4ECE\u98DE\u4E66\u53D1\u5230Discord"
26498
26813
  - \u56DE\u590D\u6765\u6E90\u9891\u9053: content="\u6536\u5230"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26499
26814
  schema: {
@@ -26522,12 +26837,12 @@ Examples:
26522
26837
  }
26523
26838
  const toIds = to ? to.split(",").map((s) => s.trim()).filter(Boolean) : [];
26524
26839
  const dest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26840
+ const dmEchoDest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26525
26841
  const isDmEcho = ctx.channelType === "dm" && // DM 对话(群聊先观察不拦)
26526
26842
  resolvedSource === ctx.channel && // 目标通道=当前对话通道(真跨通道转发不拦)
26527
26843
  ctx.inboundFrom && // 有当前发送者(注入消息无 inboundFrom 不拦)
26528
- // 形态1:纯 DM 回发发送者
26529
- (!resolvedChannelId && toIds.length > 0 && toIds.length === toIds.filter((id) => id === ctx.inboundFrom).length || // 形态2:目的地=当前会话(fallback 或显式填了当前会话 chat_id)
26530
- !!resolvedChannelId && resolvedChannelId === ctx.channelTarget);
26844
+ dmEchoDest !== void 0 && (dmEchoDest === ctx.channelTarget || dmEchoDest === ctx.inboundFrom) && // 目的地=当前会话/当前对话者
26845
+ toIds.every((id) => id === ctx.inboundFrom);
26531
26846
  if (isDmEcho) {
26532
26847
  console.log(`[msg_send] \u26D4 DM \u56DE\u53D1\u5F53\u524D\u5BF9\u8BDD\u88AB\u62E6: to=${to} channel_id=${resolvedChannelId || "(fallback)"} (${resolvedSource})`);
26533
26848
  return {
@@ -26558,6 +26873,11 @@ Examples:
26558
26873
  }
26559
26874
  const fullMsg = `${mentionPrefix}${content}`;
26560
26875
  const where = resolvedChannelId ? `${resolvedSource} \u9891\u9053 ${resolvedChannelId}` : `${resolvedSource} DM ${toIds[0]}`;
26876
+ const breaker = checkRateBreaker(resolvedSource, ctx.sessionId, dest, toIds, !!ctx.inboundIsBot);
26877
+ if (breaker) {
26878
+ console.log(`[msg_send] \u{1F515} rate-breaker \u62E6\u622A: session=${ctx.sessionId} \u2192 ${where}`);
26879
+ return breaker;
26880
+ }
26561
26881
  try {
26562
26882
  await mgr.send(resolvedSource, dest, fullMsg);
26563
26883
  return { content: `\u6D88\u606F\u5DF2\u53D1\u9001\u5230 ${where}` };
@@ -26673,10 +26993,10 @@ Parameters:
26673
26993
  channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u53D1\u5230\u5F53\u524D\u6D88\u606F\u7684\u6765\u6E90\u9891\u9053\u3002
26674
26994
 
26675
26995
  Examples:
26676
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
26677
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
26678
- - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", type="file", path="/tmp/report.pdf"
26679
- - \u53D1\u97F3\u9891 DM: to="1502999996616933428", type="audio", path="/tmp/voice.mp3"
26996
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
26997
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
26998
+ - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", type="file", path="/tmp/report.pdf"
26999
+ - \u53D1\u97F3\u9891 DM: to="1111111111111111111", type="audio", path="/tmp/voice.mp3"
26680
27000
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", type="image", path="/tmp/photo.png"
26681
27001
  - \u53D1\u5230\u6765\u6E90\u9891\u9053: type="image", path="/tmp/photo.png"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26682
27002
  schema: {
@@ -29472,8 +29792,8 @@ registerCommand({
29472
29792
  sessionManager: deps.sessions,
29473
29793
  sessionId: sid,
29474
29794
  model: deps.config.model,
29475
- contextWindow: deps.compactConfig.contextWindow || 2e5,
29476
- systemPrompt: deps.systemPrompt,
29795
+ contextWindow: (typeof deps.compactConfig === "function" ? deps.compactConfig() : deps.compactConfig).contextWindow || 2e5,
29796
+ systemPrompt: resolveSystemPrompt(deps.systemPrompt),
29477
29797
  toolDefs: registry.definitions(),
29478
29798
  workspace: deps.config.workspace
29479
29799
  });
@@ -29521,7 +29841,7 @@ ${question}`;
29521
29841
  const stream = deps.provider.streamChat({
29522
29842
  model: deps.config.model,
29523
29843
  messages: [{ role: "user", content: wrappedQuestion }],
29524
- systemPrompt: deps.systemPrompt,
29844
+ systemPrompt: resolveSystemPrompt(deps.systemPrompt),
29525
29845
  maxTokens: 2048,
29526
29846
  signal: void 0
29527
29847
  });
@@ -29742,8 +30062,8 @@ registerCommand({
29742
30062
  const { rm: rm2 } = await import("node:fs/promises");
29743
30063
  const teamsDir = getTeamsDir3();
29744
30064
  const { readdir: readdir2 } = await import("node:fs/promises");
29745
- const entries = await readdir2(teamsDir).catch(() => []);
29746
- for (const entry of entries) {
30065
+ const entries2 = await readdir2(teamsDir).catch(() => []);
30066
+ for (const entry of entries2) {
29747
30067
  const entryPath = `${teamsDir}/${entry}`;
29748
30068
  try {
29749
30069
  await rm2(entryPath, { recursive: true, force: true });
@@ -29852,8 +30172,21 @@ registerCommand({
29852
30172
  let current;
29853
30173
  if (deps.getModelOverride()) {
29854
30174
  current = `**${deps.getModelOverride()}** (override)`;
29855
- } else if (deps.provider instanceof FallbackProvider && deps.provider.lastUsedLabel) {
29856
- current = `**${deps.provider.lastUsedLabel}** (auto-route, default: ${deps.config.provider.id}/${deps.config.model})`;
30175
+ } else if (deps.provider instanceof FallbackProvider) {
30176
+ const status = deps.provider.getChainStatus();
30177
+ const chainStr = status.map((e) => e.label).join(" \u2192 ");
30178
+ const next = status.find((e) => e.cooldownMs <= 0);
30179
+ const cooling = status.filter((e) => e.cooldownMs > 0);
30180
+ current = `**auto-route** \u2014 \u94FE: ${chainStr}
30181
+ \u4E0B\u4E00\u4E2A\u8BF7\u6C42\u7528: **${next?.label ?? "(\u5168\u90E8\u51B7\u5374\u4E2D)"}**`;
30182
+ if (cooling.length > 0) {
30183
+ current += `
30184
+ \u51B7\u5374\u4E2D: ${cooling.map((e) => `${e.label}\uFF08\u5269 ${Math.round(e.cooldownMs / 6e4)}min\uFF09`).join("\u3001")}`;
30185
+ }
30186
+ if (deps.provider.lastUsedLabel) {
30187
+ current += `
30188
+ \u4E0A\u6B21\u5B9E\u9645: ${deps.provider.lastUsedLabel}`;
30189
+ }
29857
30190
  } else {
29858
30191
  current = `**${deps.config.provider.id}/${deps.config.model}** (default, auto-route)`;
29859
30192
  }
@@ -30107,6 +30440,16 @@ Use full ref like \`/vision-model ${candidates[0].ref}\``);
30107
30440
  });
30108
30441
 
30109
30442
  // src/engine-startup.ts
30443
+ function resolveModelMaxTokens(modelRef) {
30444
+ try {
30445
+ const [pid, mid] = (modelRef || "").split("/");
30446
+ const models = liveConfig.get(`models.providers.${pid}.models`);
30447
+ const m = models?.find((x) => x?.id === mid);
30448
+ return typeof m?.maxTokens === "number" && m.maxTokens > 0 ? m.maxTokens : 4096;
30449
+ } catch {
30450
+ return 4096;
30451
+ }
30452
+ }
30110
30453
  var _epipeSeen = false;
30111
30454
  process.on("uncaughtException", (err) => {
30112
30455
  const code = err?.code ?? "";
@@ -30176,7 +30519,10 @@ async function startEngine(config, opts) {
30176
30519
  const licensedFeatures = loadLicense(config.stateDir, config.profile?.devMode === true);
30177
30520
  const requiredTools = resolveRequiredTools(config.profile.features, licensedFeatures);
30178
30521
  registry.licensedFeatures = licensedFeatures;
30179
- const { provider, visionProvider, visionChainLabels } = buildProviderChain(config);
30522
+ let provider;
30523
+ let visionProvider;
30524
+ let visionChainLabels;
30525
+ ({ provider, visionProvider, visionChainLabels } = buildProviderChain(config));
30180
30526
  if (visionChainLabels.length > 0) {
30181
30527
  console.log(`[vision] Routing enabled: ${visionChainLabels.join(" \u2192 ")}`);
30182
30528
  }
@@ -30398,11 +30744,36 @@ ${content}`
30398
30744
  console.log(`[DEBUG] definitions() = ${_allDefs.length} defs`);
30399
30745
  console.log(`[DEBUG] active (non-defer) = ${_activeDefs.length}: ${_activeDefs.map((d) => d.function.name).join(", ")}`);
30400
30746
  console.log(`[DEBUG] deferred = ${_deferredDefs.length}: ${_deferredDefs.map((d) => d.function.name).join(", ")}`);
30401
- const systemStable = buildStablePrompt(config.workspace, config.prompt);
30747
+ let _stableCache = null;
30748
+ const getSystemStable = () => {
30749
+ const promptCfg = liveConfig.get("prompt") || {};
30750
+ const fileCandidates = /* @__PURE__ */ new Set(["SOUL.md"]);
30751
+ for (const f of promptCfg.staticFiles || []) fileCandidates.add(f);
30752
+ for (const item of promptCfg.order || []) {
30753
+ if (typeof item === "string" && /\.(md|txt|json)$/i.test(item)) fileCandidates.add(item);
30754
+ }
30755
+ const statLines = [JSON.stringify({ mode: promptCfg.mode, order: promptCfg.order })];
30756
+ for (const f of fileCandidates) {
30757
+ try {
30758
+ const p = path49.isAbsolute(f) ? f : path49.join(config.workspace, f);
30759
+ statLines.push(`${f}:${fs47.statSync(p).mtimeMs}`);
30760
+ } catch {
30761
+ statLines.push(`${f}:missing`);
30762
+ }
30763
+ }
30764
+ const fingerprint = statLines.join("|");
30765
+ if (_stableCache && _stableCache.fingerprint === fingerprint) return _stableCache.prompt;
30766
+ const prompt = buildStablePrompt(config.workspace, promptCfg);
30767
+ _stableCache = { fingerprint, prompt };
30768
+ console.log(`[prompt] stable \u91CD\u5EFA\uFF08\u6587\u4EF6/config \u53D8\u66F4\uFF09\uFF0C${prompt.length} chars`);
30769
+ return prompt;
30770
+ };
30771
+ const systemStable = getSystemStable();
30402
30772
  const systemDynamic = buildDynamicPrompt({
30403
30773
  workspace: config.workspace
30404
30774
  });
30405
30775
  const systemPrompt = [systemStable, systemDynamic].join("\n\n");
30776
+ const getSystemPrompt = () => [getSystemStable(), buildDynamicPrompt({ workspace: config.workspace })].join("\n\n");
30406
30777
  dumpSystemPrompt(config.workspace, systemStable, systemDynamic);
30407
30778
  const modelDef = config.provider.models.find((m) => m.id === config.model);
30408
30779
  const modelContextWindow = modelDef?.contextWindow;
@@ -30421,13 +30792,21 @@ ${content}`
30421
30792
  // 默认 5MB
30422
30793
  );
30423
30794
  const memoryFlushEnabled = config.compaction?.memoryFlush?.enabled !== false;
30424
- const compactConfig = {
30425
- ...DEFAULT_COMPACT_CONFIG,
30426
- ...config.compaction,
30427
- // 优先级:compaction.contextWindow > model.contextWindow > DEFAULT 200K
30428
- ...modelContextWindow && !config.compaction?.contextWindow ? { contextWindow: modelContextWindow } : {},
30429
- forceFlushTranscriptBytes
30795
+ const getCompactConfig = () => {
30796
+ const liveComp = liveConfig.get("compaction") || {};
30797
+ const liveModel = (liveConfig.get("providers") || {})[liveConfig.get("agents.defaults.model.primary")?.split("/")[0] || ""];
30798
+ const liveModelId = liveConfig.get("agents.defaults.model.primary")?.split("/")?.[1];
30799
+ const mDef = liveModel?.models?.find((m) => m.id === liveModelId);
30800
+ const mCW = mDef?.contextWindow;
30801
+ const cfg = {
30802
+ ...DEFAULT_COMPACT_CONFIG,
30803
+ ...liveComp,
30804
+ ...mCW && !liveComp.contextWindow ? { contextWindow: mCW } : {},
30805
+ forceFlushTranscriptBytes
30806
+ };
30807
+ return cfg;
30430
30808
  };
30809
+ const compactConfig = getCompactConfig();
30431
30810
  if (compactConfig.contextWindow !== DEFAULT_COMPACT_CONFIG.contextWindow) {
30432
30811
  console.log(`[compact] Context window: ${compactConfig.contextWindow} (from ${config.compaction?.contextWindow ? "config" : modelContextWindow ? "model" : "default"})`);
30433
30812
  }
@@ -30436,12 +30815,12 @@ ${content}`
30436
30815
  }
30437
30816
  const engine = new QueryEngine(provider, {
30438
30817
  model: config.model,
30439
- systemPrompt,
30440
- systemStable,
30441
- // 对齐 OpenClaw: stable prefix 用于 prompt cache
30442
- compactConfig,
30443
- // 对齐 CC compaction
30444
- maxTokens: 4096,
30818
+ systemPrompt: getSystemPrompt,
30819
+ systemStable: getSystemStable,
30820
+ // 0902 函数形态:mtime 缓存 getter,改 prompt 文件热生效
30821
+ compactConfig: getCompactConfig,
30822
+ // 0902 函数形态:每 turn 刷新
30823
+ maxTokens: () => resolveModelMaxTokens(liveConfig.get("agents.defaults.model.primary") || config.model),
30445
30824
  temperature: 0.7,
30446
30825
  maxTurns: config.profile.maxTurns,
30447
30826
  // 从配置读,默认 50(query.ts 里 fallback)
@@ -30452,10 +30831,10 @@ ${content}`
30452
30831
  if (visionProvider && visionConfig) {
30453
30832
  visionEngine = new QueryEngine(visionProvider, {
30454
30833
  model: visionConfig.modelId,
30455
- systemPrompt,
30456
- systemStable,
30457
- compactConfig,
30458
- maxTokens: 4096,
30834
+ systemPrompt: getSystemPrompt,
30835
+ systemStable: getSystemStable,
30836
+ compactConfig: getCompactConfig,
30837
+ maxTokens: () => resolveModelMaxTokens(`${visionConfig.providerId}/${visionConfig.modelId}`),
30459
30838
  temperature: 0.7,
30460
30839
  maxTurns: config.profile.maxTurns,
30461
30840
  agentLabel: "main"
@@ -30527,7 +30906,8 @@ ${content}`
30527
30906
  providerApi: config.provider.api,
30528
30907
  model: config.model,
30529
30908
  modelInputs: modelDef?.input || ["text"],
30530
- systemPrompt,
30909
+ systemPrompt: getSystemPrompt,
30910
+ // 0902 函数形态:/btw、voice-chat 等按调用时现取(stable mtime 缓存 + dynamic 现算)
30531
30911
  channels: config.channels,
30532
30912
  config,
30533
30913
  // tool 读自己配置用
@@ -30541,36 +30921,28 @@ ${content}`
30541
30921
  } : void 0,
30542
30922
  mcpManager
30543
30923
  };
30544
- if (visionEngine && visionConfig) {
30924
+ const visionMetaInit = visionEngine && visionConfig ? (() => {
30545
30925
  const vpCfg = config.providers?.[visionConfig.providerId];
30546
30926
  const visionModelDef = vpCfg?.models?.find((m) => m.id === visionConfig.modelId);
30547
- visionDeps = {
30548
- engine: visionEngine,
30549
- sessions,
30550
- channelManager,
30551
- workspace: config.workspace,
30927
+ return {
30552
30928
  providerId: visionConfig.providerId,
30553
30929
  providerApi: vpCfg?.api || "openai-completions",
30554
30930
  model: visionConfig.modelId,
30555
- modelInputs: visionModelDef?.input || ["text", "image"],
30556
- systemPrompt,
30557
- channels: config.channels,
30558
- config,
30559
- // tool 读自己配置用
30560
- recallProvider: memoryRecallProvider || void 0,
30561
- extractProvider: memoryExtractProvider || void 0,
30562
- everosCfg: config.everos ? {
30563
- ...config.everos,
30564
- // resolve provider 引用:从 providers 取 apiKey(跟主 deps 同逻辑)
30565
- llm: config.everos.llm?.provider && config.providers?.[config.everos.llm.provider] ? { ...config.everos.llm, apiKey: config.providers[config.everos.llm.provider].apiKey } : config.everos.llm,
30566
- rerank: config.everos.rerank?.provider && config.providers?.[config.everos.rerank.provider] ? { ...config.everos.rerank, apiKey: config.providers[config.everos.rerank.provider].apiKey } : config.everos.rerank
30567
- } : void 0
30931
+ modelInputs: visionModelDef?.input || ["text", "image"]
30568
30932
  };
30933
+ })() : null;
30934
+ let visionMeta = visionMetaInit;
30935
+ function resolveVisionDeps() {
30936
+ if (!visionEngine || !visionMeta) return null;
30937
+ if (!visionDeps) {
30938
+ visionDeps = { ...deps, engine: visionEngine, ...visionMeta };
30939
+ console.log(`[vision] deps built: ${visionMeta.providerId}/${visionMeta.model}`);
30940
+ }
30941
+ return visionDeps;
30569
30942
  }
30570
30943
  let modelOverride = null;
30571
30944
  let modelOverrideEngine = null;
30572
30945
  let visionOverride = null;
30573
- const defaultVisionDeps = visionDeps;
30574
30946
  const modelDepsCache = /* @__PURE__ */ new Map();
30575
30947
  deps.invalidateDeskBuddyDeps = () => {
30576
30948
  deskBuddyDeps = null;
@@ -30596,6 +30968,7 @@ ${content}`
30596
30968
  if (!p.provider) {
30597
30969
  visionEngine = null;
30598
30970
  visionDeps = null;
30971
+ visionMeta = null;
30599
30972
  return;
30600
30973
  }
30601
30974
  if (visionEngine) {
@@ -30604,39 +30977,22 @@ ${content}`
30604
30977
  } else {
30605
30978
  visionEngine = new QueryEngine(p.provider, {
30606
30979
  model: p.model,
30607
- systemPrompt,
30608
- systemStable,
30609
- compactConfig,
30980
+ systemPrompt: getSystemPrompt,
30981
+ systemStable: getSystemStable,
30982
+ compactConfig: getCompactConfig,
30610
30983
  maxTokens: 4096,
30611
30984
  temperature: 0.7,
30612
30985
  maxTurns: config.profile.maxTurns,
30613
30986
  agentLabel: "main"
30614
30987
  });
30615
30988
  }
30616
- if (visionDeps) {
30617
- visionDeps.engine = visionEngine;
30618
- visionDeps.providerId = p.providerId;
30619
- visionDeps.providerApi = p.providerApi;
30620
- visionDeps.model = p.model;
30621
- visionDeps.modelInputs = p.modelInputs;
30622
- } else {
30623
- visionDeps = {
30624
- engine: visionEngine,
30625
- sessions,
30626
- channelManager,
30627
- workspace: config.workspace,
30628
- providerId: p.providerId,
30629
- providerApi: p.providerApi,
30630
- model: p.model,
30631
- modelInputs: p.modelInputs,
30632
- systemPrompt,
30633
- channels: config.channels,
30634
- config,
30635
- recallProvider: memoryRecallProvider || void 0,
30636
- extractProvider: memoryExtractProvider || void 0,
30637
- everosCfg: deps?.everosCfg
30638
- };
30639
- }
30989
+ visionMeta = {
30990
+ providerId: p.providerId,
30991
+ providerApi: p.providerApi,
30992
+ model: p.model,
30993
+ modelInputs: p.modelInputs
30994
+ };
30995
+ visionDeps = null;
30640
30996
  };
30641
30997
  function createModelDeps(ref) {
30642
30998
  const slashIdx = ref.indexOf("/");
@@ -30656,28 +31012,21 @@ ${content}`
30656
31012
  const llmProvider = providerId === config.provider.id ? provider : createProvider(providerCfg);
30657
31013
  const engine2 = new QueryEngine(llmProvider, {
30658
31014
  model: modelId,
30659
- systemPrompt,
30660
- systemStable,
30661
- compactConfig,
30662
- maxTokens: modelDef2.maxTokens || 4096,
31015
+ systemPrompt: getSystemPrompt,
31016
+ systemStable: getSystemStable,
31017
+ compactConfig: getCompactConfig,
31018
+ maxTokens: () => resolveModelMaxTokens(`${providerId}/${modelId}`),
30663
31019
  temperature: 0.7,
30664
31020
  maxTurns: config.profile.maxTurns,
30665
31021
  agentLabel: `main:${ref}`
30666
31022
  });
30667
31023
  return {
31024
+ ...deps,
30668
31025
  engine: engine2,
30669
- sessions,
30670
- channelManager,
30671
- workspace: config.workspace,
30672
31026
  providerId,
30673
31027
  providerApi: providerCfg.api,
30674
31028
  model: modelId,
30675
- modelInputs: modelDef2.input || ["text"],
30676
- systemPrompt,
30677
- channels: config.channels,
30678
- recallProvider: memoryRecallProvider || void 0,
30679
- extractProvider: memoryExtractProvider || void 0,
30680
- everosCfg: deps?.everosCfg
31029
+ modelInputs: modelDef2.input || ["text"]
30681
31030
  };
30682
31031
  }
30683
31032
  const dispatcher = new MessageDispatcher();
@@ -30854,7 +31203,17 @@ ${content}`
30854
31203
  console.log(`[cron] config check: enabled=${config.cron?.enabled}, hasConfig=${!!config.cron}`);
30855
31204
  if (config.cron?.enabled) {
30856
31205
  const { CronPlugin: CronPlugin2 } = await Promise.resolve().then(() => (init_cron_plugin(), cron_plugin_exports));
30857
- const cronPlugin = new CronPlugin2(config.cron, sessions, channelManager, deps, config.stateDir, dispatcher);
31206
+ const resolveCronModelDeps = (ref) => {
31207
+ const key = `cron:${ref}`;
31208
+ const cached = modelDepsCache.get(key);
31209
+ if (cached) return cached;
31210
+ const built = createModelDeps(ref);
31211
+ if (!built) return null;
31212
+ modelDepsCache.set(key, built);
31213
+ console.log(`[cron] Model deps built: ${ref}`);
31214
+ return built;
31215
+ };
31216
+ const cronPlugin = new CronPlugin2(config.cron, sessions, channelManager, deps, config.stateDir, dispatcher, resolveCronModelDeps);
30858
31217
  await cronPlugin.start();
30859
31218
  const { setCronConfig: setCronConfig2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
30860
31219
  setCronConfig2(config.cron);
@@ -31030,8 +31389,10 @@ ${notifications}
31030
31389
  dispatcher,
31031
31390
  runningQueries,
31032
31391
  engine,
31033
- systemPrompt,
31034
- compactConfig,
31392
+ systemPrompt: getSystemPrompt,
31393
+ // 0902 函数形态:命令按调用时现取
31394
+ compactConfig: getCompactConfig,
31395
+ // 0902 同上
31035
31396
  provider,
31036
31397
  deps,
31037
31398
  getVisualRegistry: () => visualRegistry,
@@ -31053,9 +31414,17 @@ ${notifications}
31053
31414
  setVisionDeps: (v) => {
31054
31415
  visionDeps = v;
31055
31416
  },
31056
- getDefaultVisionDeps: () => defaultVisionDeps,
31417
+ // reset 用:清 override deps,下一条图片消息由 resolveVisionDeps 按当前 meta 重建
31418
+ getDefaultVisionDeps: () => {
31419
+ visionDeps = null;
31420
+ return null;
31421
+ },
31057
31422
  doReloadConfig
31058
31423
  };
31424
+ deps.onProviderSwapped = (p) => {
31425
+ provider = p;
31426
+ commandDeps.provider = p;
31427
+ };
31059
31428
  const slashCommands = listCommandDefs().map((d) => ({
31060
31429
  name: d.name,
31061
31430
  description: d.description,
@@ -31312,7 +31681,7 @@ ${pathStr}` }];
31312
31681
  for (const att of nonImageAttachments) {
31313
31682
  console.log(`[file] Downloading: ${att.filename} (${att.contentType}, ${att.size}B)`);
31314
31683
  try {
31315
- const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher }) : await fetch(att.url);
31684
+ const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher, signal: AbortSignal.timeout(3e4) }) : await fetch(att.url, { signal: AbortSignal.timeout(3e4) });
31316
31685
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
31317
31686
  const buffer = Buffer.from(await resp.arrayBuffer());
31318
31687
  const safeName2 = path49.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
@@ -31342,7 +31711,8 @@ ${pathStr}` }];
31342
31711
  }
31343
31712
  const isImageBlock = (b) => b.type === "image" || b.type === "image_url";
31344
31713
  const hasImages = Array.isArray(queryContent) && queryContent.some((b) => isImageBlock(b));
31345
- let msgDeps = hasImages && visionDeps ? visionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31714
+ const activeVisionDeps = hasImages ? resolveVisionDeps() : null;
31715
+ let msgDeps = activeVisionDeps ? activeVisionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31346
31716
  if (isDeskBuddy && !hasImages && !modelOverride) {
31347
31717
  if (!deskBuddyDeps) {
31348
31718
  const dbRef = liveConfig.get("channels.deskBuddy.model") || "deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731";
@@ -31370,7 +31740,8 @@ ${pathStr}` }];
31370
31740
  model: dbProviderCfg ? dbModelId : config.model,
31371
31741
  // 8/18 翀哥:完整工程 prompt(14k tok)+memory(10k) 会把小模型带偏成"工程助手"——deskBuddy 用 SOUL 精简人设
31372
31742
  // 8/21 复用 light-mode.buildLightStablePrompt(和情感模式同一构建器)
31373
- systemPrompt: buildLightStablePrompt(config.workspace, "deskBuddy"),
31743
+ systemPrompt: () => buildLightStablePrompt(config.workspace, "deskBuddy"),
31744
+ // 0902 函数形态:改 SOUL 热生效
31374
31745
  maxTokens: 1024,
31375
31746
  temperature: 0.7,
31376
31747
  disableThinking: true,
@@ -31384,7 +31755,7 @@ ${pathStr}` }];
31384
31755
  msgDeps = deskBuddyDeps;
31385
31756
  }
31386
31757
  if (hasImages) {
31387
- console.log(`[vision-debug] hasImages=true visionDeps=${visionDeps ? `yes(${visionDeps.providerId}/${visionDeps.model})` : "NULL"} modelOverride=${modelOverride || "none"} \u2192 msgDeps.provider=${msgDeps?.providerId || "?"}/${msgDeps?.model || "?"} queryContent.blocks=${Array.isArray(queryContent) ? queryContent.length : "string"}`);
31758
+ console.log(`[vision-debug] hasImages=true visionDeps=${activeVisionDeps ? `yes(${activeVisionDeps.providerId}/${activeVisionDeps.model})` : "NULL"} modelOverride=${modelOverride || "none"} \u2192 msgDeps.provider=${msgDeps?.providerId || "?"}/${msgDeps?.model || "?"} queryContent.blocks=${Array.isArray(queryContent) ? queryContent.length : "string"}`);
31388
31759
  if (Array.isArray(queryContent)) {
31389
31760
  queryContent.forEach((b, i) => {
31390
31761
  if (b.type === "image" && b.source?.data) {
@@ -31393,13 +31764,13 @@ ${pathStr}` }];
31393
31764
  });
31394
31765
  }
31395
31766
  }
31396
- if (hasImages && visionDeps) {
31397
- console.log(`[vision] Routing to ${visionDeps.providerId}/${visionDeps.model}${visionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31767
+ if (hasImages && activeVisionDeps) {
31768
+ console.log(`[vision] Routing to ${activeVisionDeps.providerId}/${activeVisionDeps.model}${activeVisionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31398
31769
  }
31399
31770
  const preQueryResult = await messageHooks.runPreQuery({
31400
31771
  inbound: { channel: inbound.channel, channel_id: inbound.channel_id, from: inbound.from, fromName: inbound.fromName, channelType: inbound.channelType, isMentioned: inbound.isMentioned, isBot: inbound.isBot, messageId: inbound.messageId },
31401
31772
  text: queryContent,
31402
- msgDeps: hasImages && visionDeps ? visionDeps : msgDeps,
31773
+ msgDeps: activeVisionDeps ?? msgDeps,
31403
31774
  deps: { provider, channelManager, sessions, dispatcher, config, workspace: config.workspace }
31404
31775
  });
31405
31776
  if (preQueryResult.skip) {
@@ -31412,6 +31783,7 @@ ${pathStr}` }];
31412
31783
  }
31413
31784
  queryContent = preQueryResult.text ?? queryContent;
31414
31785
  if (preQueryResult.msgDeps) msgDeps = preQueryResult.msgDeps;
31786
+ msgDeps.renderer = renderer;
31415
31787
  const accepted = dispatcher.submitMessage({
31416
31788
  text: queryContent,
31417
31789
  sessionId,
@@ -31646,8 +32018,9 @@ ${pathStr}` }];
31646
32018
  sessionManager: sessions,
31647
32019
  sessionId,
31648
32020
  model: config.model,
31649
- contextWindow: compactConfig.contextWindow || 2e5,
31650
- systemPrompt,
32021
+ contextWindow: getCompactConfig().contextWindow || 2e5,
32022
+ systemPrompt: getSystemPrompt(),
32023
+ // 0902 现取(API /context 报告跟当前 prompt 一致)
31651
32024
  toolDefs: registry.definitions(),
31652
32025
  workspace: config.workspace
31653
32026
  });
@@ -31975,6 +32348,24 @@ async function doReloadConfig(config, deps, provider) {
31975
32348
  visionModel: newConfig.visionModel,
31976
32349
  visionFallbacks: newConfig.visionFallbacks
31977
32350
  });
32351
+ const newRecall = createMemorySideProvider(
32352
+ newConfig.topics?.recall,
32353
+ provider,
32354
+ newConfig.providers || {}
32355
+ );
32356
+ const newExtract = createMemorySideProvider(
32357
+ newConfig.topics?.extract,
32358
+ provider,
32359
+ newConfig.providers || {}
32360
+ );
32361
+ if (newRecall) {
32362
+ deps.recallProvider = newRecall;
32363
+ changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32364
+ }
32365
+ if (newExtract) {
32366
+ deps.extractProvider = newExtract;
32367
+ changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32368
+ }
31978
32369
  if (oldProviderKey !== newProviderKey) {
31979
32370
  console.log("[reload] Provider structure changed, rebuilding chain...");
31980
32371
  const rebuilt = buildProviderChain(newConfig);
@@ -31985,6 +32376,7 @@ async function doReloadConfig(config, deps, provider) {
31985
32376
  }
31986
32377
  changes.push(`provider chain rebuilt (${rebuilt.visionChainLabels.length > 0 ? "vision: " + rebuilt.visionChainLabels.join("\u2192") : "no vision"})`);
31987
32378
  }
32379
+ if (deps.onProviderSwapped) deps.onProviderSwapped(rebuilt.provider);
31988
32380
  if (typeof deps.setVisionProvider === "function") {
31989
32381
  const vCfg = newConfig.visionModel;
31990
32382
  const vpCfg = vCfg ? newConfig.providers?.[vCfg.providerId] : void 0;
@@ -32015,24 +32407,6 @@ async function doReloadConfig(config, deps, provider) {
32015
32407
  changes.push(`deskBuddy model \u2192 ${newDbModel}`);
32016
32408
  }
32017
32409
  }
32018
- const newRecall = createMemorySideProvider(
32019
- newConfig.topics?.recall,
32020
- provider,
32021
- newConfig.providers || {}
32022
- );
32023
- const newExtract = createMemorySideProvider(
32024
- newConfig.topics?.extract,
32025
- provider,
32026
- newConfig.providers || {}
32027
- );
32028
- if (newRecall) {
32029
- deps.recallProvider = newRecall;
32030
- changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32031
- }
32032
- if (newExtract) {
32033
- deps.extractProvider = newExtract;
32034
- changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32035
- }
32036
32410
  try {
32037
32411
  const { setAutoDreamConfig: setAutoDreamConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
32038
32412
  setAutoDreamConfig2(newConfig);