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.
package/dist/main.mjs CHANGED
@@ -1861,8 +1861,8 @@ var init_microCompact = __esm({
1861
1861
  });
1862
1862
 
1863
1863
  // src/compact/ruleCompact.ts
1864
- function smartCompressToolResult(content, essentialFields) {
1865
- if (content.length <= TOOL_SIZE_LIMIT) return content;
1864
+ function smartCompressToolResult(content, essentialFields, sizeLimit) {
1865
+ if (content.length <= (sizeLimit ?? TOOL_SIZE_LIMIT)) return content;
1866
1866
  try {
1867
1867
  const data = JSON.parse(content);
1868
1868
  if (Array.isArray(data) && data.length > 0) {
@@ -1901,36 +1901,37 @@ ${result}`;
1901
1901
  }
1902
1902
  } catch {
1903
1903
  }
1904
- return content.slice(0, TRUNCATE_TO) + "\n...[TRUNCATED]";
1904
+ return content.slice(0, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
1905
1905
  }
1906
- function isFinalAnswer(m) {
1907
- if (m.role !== "assistant") return false;
1908
- if (m.tool_calls && m.tool_calls.length > 0) return false;
1909
- const content = typeof m.content === "string" ? m.content : "";
1910
- return content.trim().length > 0;
1911
- }
1912
- function extractFinalAnswer(messages) {
1913
- for (let i = messages.length - 1; i >= 0; i--) {
1914
- if (isFinalAnswer(messages[i])) {
1915
- return messages[i];
1906
+ function foldTurnInline(turn) {
1907
+ const toolMsgs = turn.filter((m) => m.role === "tool");
1908
+ if (toolMsgs.length === 0) return turn;
1909
+ let i = 0;
1910
+ while (i < turn.length && turn[i].role !== "assistant" && turn[i].role !== "tool") i++;
1911
+ const head = turn.slice(0, i);
1912
+ const rest = turn.slice(i);
1913
+ let finalAnswer = null;
1914
+ for (let j = rest.length - 1; j >= 0; j--) {
1915
+ const m = rest[j];
1916
+ if (m.role === "assistant" && !(m.tool_calls && m.tool_calls.length > 0)) {
1917
+ const text = typeof m.content === "string" ? m.content.trim() : "";
1918
+ if (text) {
1919
+ finalAnswer = m;
1920
+ break;
1921
+ }
1916
1922
  }
1917
1923
  }
1918
- return null;
1919
- }
1920
- function mergeToolResults(messages) {
1921
- const toolMsgs = messages.filter((m) => m.role === "tool");
1922
- if (toolMsgs.length === 0) return null;
1923
- const parts = [];
1924
- for (let i = 0; i < toolMsgs.length; i++) {
1925
- const rawContent = toolMsgs[i].content;
1926
- const contentStr = typeof rawContent === "string" ? rawContent : "";
1927
- const compressed = smartCompressToolResult(contentStr);
1928
- parts.push(`[${i + 1}] ${compressed}`);
1924
+ const parts = toolMsgs.map((m, k) => `[${k + 1}] ${smartCompressToolResult(typeof m.content === "string" ? m.content : "")}`);
1925
+ const merged = parts.join("\n---\n");
1926
+ if (finalAnswer) {
1927
+ return [...head, { role: "assistant", content: `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${toolMsgs.length} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
1928
+ ${merged}
1929
+
1930
+ \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
1931
+ ${finalAnswer.content || ""}` }];
1929
1932
  }
1930
- return {
1931
- content: parts.join("\n---\n"),
1932
- count: toolMsgs.length
1933
- };
1933
+ 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
1934
+ ${merged}` }];
1934
1935
  }
1935
1936
  function splitByUserTurns(messages) {
1936
1937
  const system = [];
@@ -1997,32 +1998,9 @@ function step2_compressOldTurns(messages, maxTokens, toolResultMode = "minimal")
1997
1998
  const origTokens = estimateMessageTokens(turn);
1998
1999
  newMessages.push(userMsg);
1999
2000
  if (toolResultMode === "inline") {
2000
- const finalAnswer = extractFinalAnswer(rest);
2001
- if (finalAnswer) {
2002
- const mergedTools = mergeToolResults(rest);
2003
- if (mergedTools) {
2004
- newMessages.push(msg.assistant(
2005
- `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${mergedTools.count} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
2006
- ${mergedTools.content}
2007
-
2008
- \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
2009
- ${finalAnswer.content || ""}`
2010
- ));
2011
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+inline-merged, ${mergedTools.count} tool results, mode=inline)`);
2012
- } else {
2013
- newMessages.push(finalAnswer);
2014
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+finalAnswer, no tools)`);
2015
- }
2016
- } else {
2017
- const mergedTools = mergeToolResults(rest);
2018
- if (mergedTools) {
2019
- 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
2020
- ${mergedTools.content}`));
2021
- 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)`);
2022
- } else {
2023
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 1 msg (user only, no tools/final)`);
2024
- }
2025
- }
2001
+ const folded = foldTurnInline(turn);
2002
+ newMessages.push(...folded.slice(1));
2003
+ compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 ${1 + folded.length - 1} msgs (inline folded, mode=inline)`);
2026
2004
  } else {
2027
2005
  const kept = rest.filter((m) => m.role !== "tool").map((m) => {
2028
2006
  if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
@@ -2140,7 +2118,6 @@ var TAG3, TOOL_SIZE_LIMIT, TRUNCATE_TO, MIN_HISTORY, KEEP_RECENT_TURNS, MAX_LIST
2140
2118
  var init_ruleCompact = __esm({
2141
2119
  "src/compact/ruleCompact.ts"() {
2142
2120
  "use strict";
2143
- init_types();
2144
2121
  init_tokenEstimate();
2145
2122
  init_autoCompact();
2146
2123
  init_compactLog();
@@ -3047,7 +3024,7 @@ var init_query = __esm({
3047
3024
  this.provider = provider;
3048
3025
  this.options = options;
3049
3026
  if (options.compactConfig) {
3050
- this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...options.compactConfig };
3027
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(options.compactConfig) };
3051
3028
  }
3052
3029
  this.logPrefix = `[query:${options.agentLabel || "main"}]`;
3053
3030
  }
@@ -3140,6 +3117,10 @@ var init_query = __esm({
3140
3117
  }
3141
3118
  /** 日志前缀(区分多个并行 QueryEngine) */
3142
3119
  logPrefix;
3120
+ /** 函数形态 option 的统一解析(0902:systemPrompt/systemStable/compactConfig/maxTokens 共用) */
3121
+ resolveOpt(v) {
3122
+ return typeof v === "function" ? v() : v;
3123
+ }
3143
3124
  /** 热加载:替换 provider 实例(doReloadConfig 重建 provider 链后调用) */
3144
3125
  setProvider(provider) {
3145
3126
  this.provider = provider;
@@ -3167,7 +3148,12 @@ var init_query = __esm({
3167
3148
  if (signal) {
3168
3149
  signal.addEventListener("abort", () => ac.abort(), { once: true });
3169
3150
  }
3170
- const activeSystemStable = perTurnSystemStable || this.options.systemStable || this.options.systemPrompt;
3151
+ if (typeof this.options.compactConfig === "function") {
3152
+ const overhead = this.compactConfig.systemOverheadTokens;
3153
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(this.options.compactConfig) };
3154
+ this.compactConfig.systemOverheadTokens = overhead ?? this.compactConfig.systemOverheadTokens;
3155
+ }
3156
+ const activeSystemStable = perTurnSystemStable || this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt);
3171
3157
  let currentMessages = [...messages];
3172
3158
  console.log(`${this.logPrefix} query() called with ${messages.length} messages`);
3173
3159
  const useDynamicTools = !this.options.toolOverride;
@@ -3183,7 +3169,7 @@ var init_query = __esm({
3183
3169
  if (this.systemOverheadTokens === null) {
3184
3170
  const { estimateToolDefinitionTokens: estimateToolDefinitionTokens2 } = await Promise.resolve().then(() => (init_context_analyzer(), context_analyzer_exports));
3185
3171
  const { roughTokenCountEstimation: roughTokenCountEstimation6 } = await Promise.resolve().then(() => (init_tokenEstimate(), tokenEstimate_exports));
3186
- const systemText = this.options.systemStable || this.options.systemPrompt || "";
3172
+ const systemText = this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt) || "";
3187
3173
  const systemTokens = roughTokenCountEstimation6(systemText);
3188
3174
  const initialToolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs) : allToolDefs;
3189
3175
  const toolTokens = estimateToolDefinitionTokens2(initialToolDefs);
@@ -3335,7 +3321,7 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3335
3321
  systemStable: activeSystemStable,
3336
3322
  systemDynamic: fullSystemDynamic,
3337
3323
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3338
- maxTokens: this.options.maxTokens,
3324
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3339
3325
  temperature: this.options.temperature,
3340
3326
  signal: ac.signal,
3341
3327
  disableThinking: this.options.disableThinking || !!perTurnEmotion
@@ -3390,7 +3376,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3390
3376
  }
3391
3377
  if (toolCalls.length === 0) {
3392
3378
  if (!textContent.trim()) {
3393
- console.log(`${this.logPrefix} Turn ${turnCount}: API returned empty (no text, no tool_call). Retrying... messages=${currentMessages.length}`);
3379
+ const thinkingAteBudget = turnThinking.length > 0;
3380
+ 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}`);
3394
3381
  turnThinking = "";
3395
3382
  turnThinkingSig = "";
3396
3383
  for await (const chunk of this.provider.streamChat({
@@ -3399,9 +3386,10 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3399
3386
  systemStable: activeSystemStable,
3400
3387
  systemDynamic: fullSystemDynamic,
3401
3388
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3402
- maxTokens: this.options.maxTokens,
3389
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3403
3390
  temperature: this.options.temperature,
3404
- signal: ac.signal
3391
+ signal: ac.signal,
3392
+ ...thinkingAteBudget ? { disableThinking: true } : {}
3405
3393
  })) {
3406
3394
  if (chunk.type === "status") yield chunk;
3407
3395
  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",
@@ -12207,14 +12323,16 @@ var init_cron_plugin = __esm({
12207
12323
  "use strict";
12208
12324
  init_tasks2();
12209
12325
  init_scheduler();
12326
+ init_live();
12210
12327
  CronPlugin = class {
12211
- constructor(config2, sessions, channelManager, deps, stateDir, dispatcher) {
12328
+ constructor(config2, sessions, channelManager, deps, stateDir, dispatcher, resolveModelDeps) {
12212
12329
  this.config = config2;
12213
12330
  this.sessions = sessions;
12214
12331
  this.channelManager = channelManager;
12215
12332
  this.deps = deps;
12216
12333
  this.stateDir = stateDir;
12217
12334
  this.dispatcher = dispatcher;
12335
+ this.resolveModelDeps = resolveModelDeps;
12218
12336
  }
12219
12337
  config;
12220
12338
  sessions;
@@ -12222,6 +12340,7 @@ var init_cron_plugin = __esm({
12222
12340
  deps;
12223
12341
  stateDir;
12224
12342
  dispatcher;
12343
+ resolveModelDeps;
12225
12344
  static shouldEnable(config2) {
12226
12345
  return config2.enabled === true;
12227
12346
  }
@@ -12254,10 +12373,12 @@ var init_cron_plugin = __esm({
12254
12373
  channelManager: this.channelManager,
12255
12374
  deps: this.deps,
12256
12375
  config: this.config,
12257
- dispatcher: this.dispatcher
12376
+ dispatcher: this.dispatcher,
12377
+ resolveModelDeps: this.resolveModelDeps
12258
12378
  };
12259
12379
  startScheduler(schedulerDeps);
12260
- console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""})`);
12380
+ const cronModel = liveConfig.get("cron.model");
12381
+ console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""}${cronModel ? `, model=${cronModel}` : ""})`);
12261
12382
  }
12262
12383
  async stop() {
12263
12384
  await stopScheduler();
@@ -12271,8 +12392,8 @@ var reply_blocklist_exports = {};
12271
12392
  __export(reply_blocklist_exports, {
12272
12393
  isUserBlocked: () => isUserBlocked
12273
12394
  });
12274
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync19, existsSync as existsSync26 } from "node:fs";
12275
- import { join as join42 } from "node:path";
12395
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync19, existsSync as existsSync27 } from "node:fs";
12396
+ import { join as join43 } from "node:path";
12276
12397
  function ensureLoaded(workspace, configIds) {
12277
12398
  if (loaded) return;
12278
12399
  if (configIds?.length) {
@@ -12280,10 +12401,10 @@ function ensureLoaded(workspace, configIds) {
12280
12401
  if (!state.blockedUserIds.includes(id)) state.blockedUserIds.push(id);
12281
12402
  }
12282
12403
  }
12283
- const path50 = join42(workspace, ".reply-blocklist.json");
12404
+ const path50 = join43(workspace, ".reply-blocklist.json");
12284
12405
  try {
12285
- if (existsSync26(path50)) {
12286
- const raw = readFileSync29(path50, "utf-8");
12406
+ if (existsSync27(path50)) {
12407
+ const raw = readFileSync30(path50, "utf-8");
12287
12408
  const parsed = JSON.parse(raw);
12288
12409
  if (parsed.blockedUserIds) {
12289
12410
  for (const id of parsed.blockedUserIds) {
@@ -12299,7 +12420,7 @@ function ensureLoaded(workspace, configIds) {
12299
12420
  loaded = true;
12300
12421
  }
12301
12422
  function save(workspace) {
12302
- const path50 = join42(workspace, ".reply-blocklist.json");
12423
+ const path50 = join43(workspace, ".reply-blocklist.json");
12303
12424
  try {
12304
12425
  writeFileSync19(path50, JSON.stringify(state, null, 2), "utf-8");
12305
12426
  } catch (err) {
@@ -13065,30 +13186,21 @@ function loadConfig(configPath2) {
13065
13186
  devMode: agentDefaults.devMode === true
13066
13187
  };
13067
13188
  const rawSession = raw.session || {};
13068
- const sessionScope = rawSession.dmScope || rawSession.groupScope ? { dmScope: rawSession.dmScope, groupScope: rawSession.groupScope } : void 0;
13189
+ const sessionScope = Object.keys(rawSession).length > 0 ? { ...rawSession } : void 0;
13069
13190
  const rawHeartbeat = raw.heartbeat || {};
13070
13191
  const heartbeat = rawHeartbeat.enabled ? {
13071
- enabled: true,
13072
- intervalMs: rawHeartbeat.intervalMs,
13073
- prompt: rawHeartbeat.prompt,
13074
- model: rawHeartbeat.model,
13075
- timeoutMs: rawHeartbeat.timeoutMs,
13076
- checkOnline: rawHeartbeat.checkOnline,
13077
- activeThresholdMs: rawHeartbeat.activeThresholdMs
13192
+ ...rawHeartbeat,
13193
+ enabled: true
13078
13194
  } : void 0;
13079
13195
  const rawInnerVoice = raw.innerVoice || {};
13080
13196
  const innerVoice = rawInnerVoice.enabled ? {
13081
- enabled: true,
13082
- intervalMs: rawInnerVoice.intervalMs,
13083
- provider: rawInnerVoice.provider,
13084
- model: rawInnerVoice.model,
13085
- activeThresholdMs: rawInnerVoice.activeThresholdMs,
13086
- hint: rawInnerVoice.hint
13197
+ ...rawInnerVoice,
13198
+ enabled: true
13087
13199
  } : void 0;
13088
13200
  const rawCron = raw.cron || {};
13089
13201
  const cron = rawCron.enabled ? {
13202
+ ...rawCron,
13090
13203
  enabled: true,
13091
- storageDir: rawCron.storageDir,
13092
13204
  defaultTimezone: rawCron.defaultTimezone || "Asia/Shanghai",
13093
13205
  tickIntervalMs: rawCron.tickIntervalMs || 1e4,
13094
13206
  maxConsecutiveFailures: rawCron.maxConsecutiveFailures || 5,
@@ -13285,15 +13397,115 @@ init_live();
13285
13397
 
13286
13398
  // src/models/openai-provider.ts
13287
13399
  init_withRetry();
13400
+
13401
+ // src/models/think-stripper.ts
13402
+ var OPEN = "<think>";
13403
+ var CLOSE = "</think>";
13404
+ var MAX_TAG = Math.max(OPEN.length, CLOSE.length);
13405
+ var ThinkTagStripper = class {
13406
+ buf = "";
13407
+ // 尚未判定的原始片段(可能含被 chunk 切断的标签前缀)
13408
+ pending = "";
13409
+ // <think> 内累积的思考——闭合才 yield(未闭合时要能整段还原)
13410
+ active = false;
13411
+ reset() {
13412
+ this.buf = "";
13413
+ this.pending = "";
13414
+ this.active = false;
13415
+ }
13416
+ /** 还有未吐出的内容(provider 判 isEmpty 用,别把攒在 buffer 里的当空响应) */
13417
+ get hasPending() {
13418
+ return !!this.buf || !!this.pending;
13419
+ }
13420
+ /**
13421
+ * 需要保留的尾部长度:只在末尾确实是 <think> / </think> 的前缀时才 hold。
13422
+ * 原实现无脑留 7 字符,导致短消息全卡住("嗯……来了" 这种 5 字回复流不出去)。
13423
+ */
13424
+ holdLen() {
13425
+ const b = this.buf;
13426
+ for (let k = Math.min(MAX_TAG - 1, b.length); k > 0; k--) {
13427
+ const tail = b.slice(b.length - k);
13428
+ if (OPEN.startsWith(tail) || CLOSE.startsWith(tail)) return k;
13429
+ }
13430
+ return 0;
13431
+ }
13432
+ /** 喂一段 text delta。标签可出现在任意位置(不只流开头),可跨 chunk 被切断。 */
13433
+ feed(chunk) {
13434
+ if (!chunk) return [];
13435
+ this.buf += chunk;
13436
+ const out = [];
13437
+ for (; ; ) {
13438
+ if (!this.active) {
13439
+ const i = this.buf.indexOf(OPEN);
13440
+ if (i >= 0) {
13441
+ if (i > 0) out.push({ text: this.buf.slice(0, i) });
13442
+ this.buf = this.buf.slice(i + OPEN.length);
13443
+ this.active = true;
13444
+ continue;
13445
+ }
13446
+ const safe2 = this.buf.length - this.holdLen();
13447
+ if (safe2 > 0) {
13448
+ out.push({ text: this.buf.slice(0, safe2) });
13449
+ this.buf = this.buf.slice(safe2);
13450
+ }
13451
+ return out;
13452
+ }
13453
+ const j = this.buf.indexOf(CLOSE);
13454
+ if (j >= 0) {
13455
+ this.pending += this.buf.slice(0, j);
13456
+ this.buf = this.buf.slice(j + CLOSE.length);
13457
+ this.active = false;
13458
+ if (this.pending) {
13459
+ out.push({ thinking: this.pending });
13460
+ this.pending = "";
13461
+ }
13462
+ continue;
13463
+ }
13464
+ const safe = this.buf.length - this.holdLen();
13465
+ if (safe > 0) {
13466
+ this.pending += this.buf.slice(0, safe);
13467
+ this.buf = this.buf.slice(safe);
13468
+ }
13469
+ return out;
13470
+ }
13471
+ }
13472
+ /**
13473
+ * 流结束时清空 buffer。
13474
+ * 未闭合 </think> = 模型没遵守协议 → 整段(含标签)还原成正文。
13475
+ * 宁可让用户看到难看的 <think> 字样,也绝不静默吞掉整条消息。
13476
+ */
13477
+ flush() {
13478
+ const out = [];
13479
+ if (this.active) {
13480
+ out.push({ text: OPEN + this.pending + this.buf });
13481
+ } else if (this.buf) {
13482
+ out.push({ text: this.buf });
13483
+ }
13484
+ this.reset();
13485
+ return out;
13486
+ }
13487
+ };
13488
+
13489
+ // src/models/openai-provider.ts
13490
+ function sanitizeToolCallForSend(tc) {
13491
+ const args2 = tc.function?.arguments;
13492
+ if (typeof args2 !== "string" || args2 === "") return tc;
13493
+ try {
13494
+ JSON.parse(args2);
13495
+ return tc;
13496
+ } catch {
13497
+ console.warn(`[openai] toolCall "${tc.function.name}" arguments \u975E\u5408\u6CD5 JSON\uFF08${args2.length} chars\uFF09\uFF0C\u51FA\u7AD9\u56DE\u843D "{}" \u9632 400`);
13498
+ return { ...tc, function: { ...tc.function, arguments: "{}" } };
13499
+ }
13500
+ }
13288
13501
  var OpenAIProvider = class {
13502
+ // 0831 <think> 剥离改用 ThinkTagStripper 的**局部**实例(见 streamChat)——
13503
+ // 原来是实例字段,跨请求共享状态,流中途 abort 会把 active=true 带到下一个请求
13289
13504
  constructor(config2) {
13290
13505
  this.config = config2;
13291
13506
  }
13292
13507
  config;
13293
13508
  name = "openai";
13294
- // 8/18:<think> 剥离状态(Qwen3 系兜底)
13295
- thinkBuf = "";
13296
- thinkStripActive = false;
13297
13509
  /** OpenAI: system prompt 放在 messages 里 */
13298
13510
  formatMessages(systemPrompt, messages) {
13299
13511
  const formatted = [];
@@ -13315,7 +13527,13 @@ var OpenAIProvider = class {
13315
13527
  role: m.role,
13316
13528
  // content 数组:转换 image block 为 OpenAI image_url 格式
13317
13529
  content: Array.isArray(m.content) ? convertContentBlocksForOpenAI(m.content) : m.content,
13318
- ...m.tool_calls ? { tool_calls: m.tool_calls } : {},
13530
+ // 0831 出站卡口:流式截断会产生非法 JSON arguments 字符串,原样发出会被
13531
+ // 校验 tool_calls 的端点直接 400(vLLM --tool-call-parser qwen3_coder 实测
13532
+ // "Unterminated string at char 12"),毒记录留在上下文里每轮重放一次 → 主模型
13533
+ // 连续冷却 3h 假死。anthropic(L81)/gemini(safeParseArgs) 早有同款防御,这里补齐:
13534
+ // parse 失败回落 "{}"。copy-on-write 不动内存历史(原始串保真进 jsonl 供诊断,
13535
+ // 重放消毒另由 reader.pickToolCallArguments 负责)。
13536
+ ...m.tool_calls ? { tool_calls: m.tool_calls.map(sanitizeToolCallForSend) } : {},
13319
13537
  ...m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}
13320
13538
  };
13321
13539
  if (m.role === "assistant" && m.reasoning_content) {
@@ -13339,8 +13557,7 @@ var OpenAIProvider = class {
13339
13557
  let base = (this.config.baseUrl || "").trim();
13340
13558
  while (base.endsWith("/")) base = base.slice(0, -1);
13341
13559
  const url = `${base}/chat/completions`;
13342
- this.thinkBuf = "";
13343
- this.thinkStripActive = false;
13560
+ const stripper = new ThinkTagStripper();
13344
13561
  const body = {
13345
13562
  model: params.model,
13346
13563
  messages: formatted,
@@ -13348,8 +13565,10 @@ var OpenAIProvider = class {
13348
13565
  ...params.tools && params.tools.length > 0 ? { tools: params.tools } : {},
13349
13566
  ...params.maxTokens ? { max_tokens: params.maxTokens } : {},
13350
13567
  ...params.temperature !== void 0 ? { temperature: params.temperature } : {},
13351
- // 8/18:Qwen3 reasoning 模型——disableThinking 时显式关(chat_template_kwargs enable_thinking=false,deepinfra/vLLM 通用)
13352
- ...params.disableThinking && /qwen3/i.test(params.model) ? { chat_template_kwargs: { enable_thinking: false } } : {}
13568
+ // 9/1 对齐 anthropic:不配 thinking = 默认开(?? true),disableThinking 优先级最高。
13569
+ // 三家 provider 统一语义:不配 = 默认开,想关显式配 enabled:false
13570
+ // chat_template_kwargs 是 vLLM 标准参数,非 vLLM 端点忽略未知字段,不挑模型。
13571
+ ...params.disableThinking ? { chat_template_kwargs: { enable_thinking: false } } : { chat_template_kwargs: { enable_thinking: this.config.thinking?.enabled ?? true } }
13353
13572
  };
13354
13573
  if (params.tools && params.tools.length > 0 && /deepseek/i.test(params.model)) {
13355
13574
  let patched = 0;
@@ -13362,7 +13581,7 @@ var OpenAIProvider = class {
13362
13581
  if (patched > 0) console.log(`[openai] reasoning_content \u8865\u7A7A ${patched} \u4E2A assistant \u8F6E (DeepSeek V4 \u89C4\u5219)`);
13363
13582
  }
13364
13583
  const actualThinking = body.chat_template_kwargs;
13365
- console.log(`[openai] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} msgs=${formatted.length} tools=${params.tools?.length ?? 0}`);
13584
+ 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}`);
13366
13585
  const retryGen = fetchWithRetry(url, {
13367
13586
  method: "POST",
13368
13587
  headers: {
@@ -13422,9 +13641,9 @@ var OpenAIProvider = class {
13422
13641
  const trimmed = line.trim();
13423
13642
  if (!trimmed || trimmed === "data: [DONE]") {
13424
13643
  if (trimmed === "data: [DONE]") {
13425
- if (!this.thinkStripActive && this.thinkBuf) {
13426
- yield { type: "text", text: this.thinkBuf };
13427
- this.thinkBuf = "";
13644
+ for (const o of stripper.flush()) {
13645
+ if (o.text) yield { type: "text", text: o.text };
13646
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13428
13647
  }
13429
13648
  for (const tc of currentToolCalls.values()) {
13430
13649
  yield { type: "tool_call", tool_call: tc };
@@ -13443,31 +13662,22 @@ var OpenAIProvider = class {
13443
13662
  const delta = choice0?.delta;
13444
13663
  if (choice0?.finish_reason) finishReason = choice0.finish_reason;
13445
13664
  if (!delta) continue;
13446
- if (!delta.content && !delta.reasoning_content && !delta.tool_calls) statRoleOnly++;
13665
+ if (!delta.content && !delta.reasoning_content && !delta.reasoning && !delta.tool_calls) statRoleOnly++;
13447
13666
  if (delta.content) {
13448
13667
  statText += delta.content.length;
13449
- this.thinkBuf += delta.content;
13450
- if (this.thinkBuf.startsWith("<think>") || this.thinkStripActive) {
13451
- this.thinkStripActive = true;
13452
- const endIdx = this.thinkBuf.indexOf("</think>");
13453
- if (endIdx >= 0) {
13454
- const after = this.thinkBuf.slice(endIdx + "</think>".length);
13455
- this.thinkBuf = "";
13456
- this.thinkStripActive = false;
13457
- if (after.trim()) yield { type: "text", text: after };
13458
- }
13459
- } else {
13460
- if (this.thinkBuf.length < 7 && "<think>".startsWith(this.thinkBuf)) {
13461
- } else {
13462
- yield { type: "text", text: this.thinkBuf };
13463
- this.thinkBuf = "";
13464
- }
13668
+ for (const o of stripper.feed(delta.content)) {
13669
+ if (o.text) yield { type: "text", text: o.text };
13670
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13465
13671
  }
13466
13672
  }
13467
13673
  if (delta.reasoning_content) {
13468
13674
  statReasoning += delta.reasoning_content.length;
13469
13675
  yield { type: "thinking", thinking: delta.reasoning_content };
13470
13676
  }
13677
+ if (delta.reasoning) {
13678
+ statReasoning += delta.reasoning.length;
13679
+ yield { type: "thinking", thinking: delta.reasoning };
13680
+ }
13471
13681
  if (delta.tool_calls) {
13472
13682
  statToolDeltas++;
13473
13683
  if (!params.tools || params.tools.length === 0) {
@@ -13502,13 +13712,17 @@ var OpenAIProvider = class {
13502
13712
  if (hallucinatedToolCalls > 0) {
13503
13713
  console.log(`[openai] dropped ${hallucinatedToolCalls} hallucinated tool_call delta(s) (no tools in request): ${[...seenHallucinatedNames].join(",")}`);
13504
13714
  }
13505
- const isEmpty = statText === 0 && currentToolCalls.size === 0 && !this.thinkBuf;
13715
+ const isEmpty = statText === 0 && currentToolCalls.size === 0 && !stripper.hasPending;
13506
13716
  console.log(`[openai] stream stats: lines=${statLines} roleOnly=${statRoleOnly} text=${statText}ch reasoning=${statReasoning}ch toolDeltas=${statToolDeltas} finish=${finishReason || "n/a"}${isEmpty ? " \u26A0\uFE0FEMPTY" : ""}`);
13507
13717
  if (isEmpty && rawSample.length > 0) {
13508
13718
  console.log(`[openai] empty-stream raw sample (${rawSample.length} lines, 500ch cap each):
13509
13719
  ${rawSample.join("\n")}`);
13510
13720
  }
13511
13721
  if (!doneYielded) {
13722
+ for (const o of stripper.flush()) {
13723
+ if (o.text) yield { type: "text", text: o.text };
13724
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13725
+ }
13512
13726
  for (const tc of currentToolCalls.values()) {
13513
13727
  yield { type: "tool_call", tool_call: tc };
13514
13728
  }
@@ -13716,7 +13930,7 @@ var AnthropicProvider = class {
13716
13930
  "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,streaming-2025-05-14,effort-2025-11-24,context-1m-2025-08-07"
13717
13931
  };
13718
13932
  const actualThinking = body.thinking;
13719
- 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(", ")}`);
13933
+ 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(", ")}`);
13720
13934
  for (let i = 0; i < formatted.length; i++) {
13721
13935
  const m = formatted[i];
13722
13936
  if (Array.isArray(m.content)) {
@@ -13730,10 +13944,26 @@ var AnthropicProvider = class {
13730
13944
  }
13731
13945
  }
13732
13946
  }
13947
+ if (process.env.ENGINE_DUMP_REQUEST === "1") {
13948
+ try {
13949
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13950
+ const dumpPath = __require("node:path").join(process.env.ENGINE_STATE_DIR || ".", "logs", `req-${params.model.replace(/[\/:]/g, "_")}-${ts}.json`);
13951
+ __require("node:fs").writeFileSync(dumpPath, JSON.stringify({ url, headers, body }, null, 1));
13952
+ console.log(`[anthropic] \u{1F4F8} request dumped \u2192 ${__require("node:path").basename(dumpPath)} (${(JSON.stringify(body).length / 1024).toFixed(0)}KB)`);
13953
+ } catch {
13954
+ }
13955
+ }
13956
+ let bodyStr = JSON.stringify(body);
13957
+ if (this.config.wafSingleQuoteWorkaround) {
13958
+ const before = bodyStr.length;
13959
+ bodyStr = bodyStr.replace(/'/g, "\u2019");
13960
+ if (bodyStr.length !== before) {
13961
+ }
13962
+ }
13733
13963
  const retryGen = fetchWithRetry(url, {
13734
13964
  method: "POST",
13735
13965
  headers,
13736
- body: JSON.stringify(body),
13966
+ body: bodyStr,
13737
13967
  signal: params.signal
13738
13968
  }, "anthropic", this.config.proxy);
13739
13969
  let response;
@@ -13758,6 +13988,13 @@ var AnthropicProvider = class {
13758
13988
  const toolUseBlocks = /* @__PURE__ */ new Map();
13759
13989
  let doneYielded = false;
13760
13990
  const thinkingBlocks = /* @__PURE__ */ new Map();
13991
+ const stripper = new ThinkTagStripper();
13992
+ const flushStripper = function* () {
13993
+ for (const o of stripper.flush()) {
13994
+ if (o.text) yield { type: "text", text: o.text };
13995
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13996
+ }
13997
+ };
13761
13998
  const handleData = function* (data) {
13762
13999
  switch (data.type) {
13763
14000
  case "content_block_start": {
@@ -13772,7 +14009,10 @@ var AnthropicProvider = class {
13772
14009
  case "content_block_delta": {
13773
14010
  const delta = data.delta;
13774
14011
  if (delta.type === "text_delta") {
13775
- yield { type: "text", text: delta.text };
14012
+ for (const o of stripper.feed(delta.text)) {
14013
+ if (o.text) yield { type: "text", text: o.text };
14014
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
14015
+ }
13776
14016
  } else if (delta.type === "input_json_delta") {
13777
14017
  const block = toolUseBlocks.get(data.index);
13778
14018
  if (block) block.input += delta.partial_json;
@@ -13804,6 +14044,7 @@ var AnthropicProvider = class {
13804
14044
  }
13805
14045
  case "message_delta": {
13806
14046
  if (!doneYielded) {
14047
+ yield* flushStripper();
13807
14048
  yield { type: "done", usage: data.usage, stopReason: data.delta?.stop_reason };
13808
14049
  doneYielded = true;
13809
14050
  }
@@ -13811,6 +14052,7 @@ var AnthropicProvider = class {
13811
14052
  }
13812
14053
  case "message_stop": {
13813
14054
  if (!doneYielded) {
14055
+ yield* flushStripper();
13814
14056
  yield { type: "done" };
13815
14057
  doneYielded = true;
13816
14058
  }
@@ -13877,6 +14119,7 @@ var AnthropicProvider = class {
13877
14119
  } finally {
13878
14120
  reader.releaseLock();
13879
14121
  if (!doneYielded) {
14122
+ yield* flushStripper();
13880
14123
  for (const block of toolUseBlocks.values()) {
13881
14124
  yield { type: "tool_call", tool_call: { id: block.id, type: "function", function: { name: block.name, arguments: block.input } } };
13882
14125
  }
@@ -14041,7 +14284,7 @@ var GeminiProvider = class {
14041
14284
  }
14042
14285
  };
14043
14286
  const actualThinking = body.generationConfig?.thinkingConfig;
14044
- console.log(`[gemini] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} contents=${contents.length} tools=${params.tools?.length ?? 0}`);
14287
+ 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}`);
14045
14288
  const retryGen = fetchWithRetry(url, {
14046
14289
  method: "POST",
14047
14290
  headers: {
@@ -14215,6 +14458,7 @@ function createProvider(config2) {
14215
14458
  return new OpenAIProvider({
14216
14459
  baseUrl: config2.baseUrl,
14217
14460
  apiKey: config2.apiKey,
14461
+ thinking: config2.thinking,
14218
14462
  proxy
14219
14463
  });
14220
14464
  case "anthropic":
@@ -14222,7 +14466,9 @@ function createProvider(config2) {
14222
14466
  baseUrl: config2.baseUrl,
14223
14467
  apiKey: config2.apiKey,
14224
14468
  thinking: config2.thinking,
14225
- proxy
14469
+ proxy,
14470
+ wafSingleQuoteWorkaround: config2.wafSingleQuoteWorkaround
14471
+ // 0902 agentrouter WAF 引号规避
14226
14472
  });
14227
14473
  case "gemini":
14228
14474
  return new GeminiProvider({
@@ -14237,6 +14483,7 @@ function createProvider(config2) {
14237
14483
 
14238
14484
  // src/light-mode.ts
14239
14485
  init_live();
14486
+ init_ruleCompact();
14240
14487
  import { readFileSync as readFileSync5 } from "node:fs";
14241
14488
  import { join as join6 } from "node:path";
14242
14489
  function isLightMode(chatMode, channelName) {
@@ -14250,19 +14497,20 @@ function resolveLightN(channelName) {
14250
14497
  }
14251
14498
  function buildLightHistory(history, opts) {
14252
14499
  if (!opts.isLight && !opts.recallFull) return history;
14253
- let lightHistory = [...history];
14254
- const before = lightHistory.length;
14255
- lightHistory = lightHistory.filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).filter((m) => m.role !== "tool").map((m) => {
14256
- if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
14257
- const text = typeof m.content === "string" ? m.content.trim() : "";
14258
- return text ? { ...m, tool_calls: void 0 } : null;
14500
+ const LIGHT_TOOL_RESULT_LIMIT = 1e3;
14501
+ const out = [...history].filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).map((m) => {
14502
+ if (m.role === "tool" && typeof m.content === "string" && m.content.length > LIGHT_TOOL_RESULT_LIMIT) {
14503
+ return { ...m, content: smartCompressToolResult(m.content, void 0, LIGHT_TOOL_RESULT_LIMIT) };
14259
14504
  }
14260
14505
  return m;
14261
- }).filter(Boolean);
14506
+ });
14507
+ const before = history.length;
14508
+ const foldedTurns = out.filter((m) => m.role === "tool").length;
14509
+ let lightHistory = out;
14262
14510
  if (!opts.recallFull && lightHistory.length > opts.lightN) {
14263
14511
  lightHistory = lightHistory.slice(-opts.lightN);
14264
14512
  }
14265
- 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})`}`);
14513
+ 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})`}`);
14266
14514
  return lightHistory;
14267
14515
  }
14268
14516
  function buildLightStablePrompt(workspace, mode, opts) {
@@ -14276,14 +14524,17 @@ function buildLightStablePrompt(workspace, mode, opts) {
14276
14524
  }
14277
14525
  if (lp?.extra) parts.push(lp.extra);
14278
14526
  if (mode === "emotion") {
14279
- 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");
14527
+ parts.push([
14528
+ "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
14529
+ "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
14530
+ '\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',
14531
+ "\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"
14532
+ ].join("\n"));
14280
14533
  }
14281
- if (mode === "emotion" && opts?.recallFull) {
14282
- for (const f of ["MEMORY.md", "memory/distill-output.md"]) {
14283
- try {
14284
- parts.push(readFileSync5(join6(workspace, f), "utf-8").trim());
14285
- } catch {
14286
- }
14534
+ if (mode === "emotion") {
14535
+ try {
14536
+ parts.push(readFileSync5(join6(workspace, "MEMORY.md"), "utf-8").trim());
14537
+ } catch {
14287
14538
  }
14288
14539
  }
14289
14540
  return parts.join("\n\n");
@@ -14329,6 +14580,19 @@ var FallbackProvider = class {
14329
14580
  console.log(`[fallback] Cleared ${count} cooldowns`);
14330
14581
  }
14331
14582
  }
14583
+ /**
14584
+ * 链状态快照(/model 显示用,0902):每个条目的 label + 剩余冷却毫秒(0=可用)。
14585
+ * "下一个请求会用" = 第一个 cooldownMs=0 的条目——这才是用户问"当前什么模型"时想要的答案
14586
+ * (lastUsedLabel 是"上一次实际用的",冷却切换/config 热切换后两者经常不一致)。
14587
+ */
14588
+ getChainStatus() {
14589
+ const now = Date.now();
14590
+ return this.chain.map((e) => {
14591
+ const until = this.cooldowns.get(this.key(e));
14592
+ const remaining = until && until > now ? until - now : 0;
14593
+ return { label: e.label, cooldownMs: remaining };
14594
+ });
14595
+ }
14332
14596
  // === LLMProvider 接口实现 ===
14333
14597
  formatMessages(systemPrompt, messages) {
14334
14598
  return this.chain[0].provider.formatMessages(systemPrompt, messages);
@@ -16856,7 +17120,7 @@ registry.register({
16856
17120
  text: { type: "string", description: "What to say (Chinese text)." },
16857
17121
  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." },
16858
17122
  channel: { type: "string", enum: ["weixin", "feishu", "discord"], description: "Target channel. Default: current channel." },
16859
- to: { type: "string", description: "Recipient ID (e.g. wechat user id). Default: current chat / \u7FC0\u54E5(wechat)." },
17123
+ 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" },
16860
17124
  caption: { type: "string", description: "Optional text to accompany the voice message." }
16861
17125
  },
16862
17126
  required: ["text"]
@@ -16868,6 +17132,23 @@ registry.register({
16868
17132
  const caption = args2.caption || "";
16869
17133
  const mgr = ctx.channelManager;
16870
17134
  if (!mgr) return { content: "\u53D1\u9001\u5931\u8D25: \u6CA1\u6709 ChannelManager", isError: true };
17135
+ const resolvedChannelRaw = args2.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
17136
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
17137
+ let target = args2.to || ctx.channelTarget || ctx.from;
17138
+ if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
17139
+ console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
17140
+ return {
17141
+ 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`,
17142
+ isError: true
17143
+ };
17144
+ }
17145
+ if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
17146
+ console.warn(`[my-voice] feishu target "${target}" invalid`);
17147
+ return {
17148
+ 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`,
17149
+ isError: true
17150
+ };
17151
+ }
16871
17152
  let voiceDurationSec;
16872
17153
  const vc = liveConfig.get("tools.my_voice");
16873
17154
  const provider = vc?.provider || "";
@@ -16941,18 +17222,6 @@ registry.register({
16941
17222
  } catch (e) {
16942
17223
  return { content: `TTS failed: ${e.message}`, isError: true };
16943
17224
  }
16944
- const resolvedChannelRaw = args2.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
16945
- const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
16946
- const WECHAT_DEFAULT_TO = "o9cq80_xQecNRCa1QC1Qs2JJZVpA@im.wechat";
16947
- let target = args2.to || ctx.channelTarget || ctx.from;
16948
- if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
16949
- console.warn(`[my-voice] wechat target "${target}" not a wechat user id, using default`);
16950
- target = WECHAT_DEFAULT_TO;
16951
- }
16952
- if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
16953
- console.warn(`[my-voice] feishu target "${target}" invalid, using default (\u7FC0\u54E5)`);
16954
- target = ctx.channelTarget || "ou_e67190624259db0d65577fefe3131447";
16955
- }
16956
17225
  if (!audioPath.endsWith(".ogg")) {
16957
17226
  try {
16958
17227
  const r = await toWav24kWithDuration(audioPath);
@@ -17784,7 +18053,7 @@ var TurnRenderer = class {
17784
18053
  }
17785
18054
  cfg;
17786
18055
  cm;
17787
- // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程显示(工具照用,只不显示过程)
18056
+ // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程 + thinking 显示(工具照用,只不显示过程)
17788
18057
  // 模块化:状态由 setEmotionMode() 设置(handle-query 判断模式后调用),不是散落读全局
17789
18058
  emotionMode = false;
17790
18059
  setEmotionMode(v) {
@@ -17799,7 +18068,7 @@ var TurnRenderer = class {
17799
18068
  * 对齐 cc-connect:EventThinking → ProgressCardEntry(thinking) → 💭 text
17800
18069
  */
17801
18070
  formatThinking(text) {
17802
- if (!this.cfg.thinking.enabled) return null;
18071
+ if (this.isEmotionMode() || !this.cfg.thinking.enabled) return null;
17803
18072
  const { emoji, maxLen } = this.cfg.thinking;
17804
18073
  const display = text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
17805
18074
  return `${emoji} _${display}_`;
@@ -18610,43 +18879,43 @@ async function readLargeFilePostBoundary(filePath) {
18610
18879
  const postBoundaryText = outBuf.subarray(0, outLen).toString("utf-8");
18611
18880
  return postBoundaryText.split("\n").filter((l) => l.trim().length > 0);
18612
18881
  }
18613
- function buildConversationChain(entries) {
18614
- if (entries.length === 0) return [];
18882
+ function buildConversationChain(entries2) {
18883
+ if (entries2.length === 0) return [];
18615
18884
  const byUuid = /* @__PURE__ */ new Map();
18616
- for (const e of entries) {
18885
+ for (const e of entries2) {
18617
18886
  if (e.uuid) {
18618
18887
  byUuid.set(e.uuid, e);
18619
18888
  }
18620
18889
  }
18621
- const hasValidChain = checkParentChainValid(entries, byUuid);
18890
+ const hasValidChain = checkParentChainValid(entries2, byUuid);
18622
18891
  if (!hasValidChain) {
18623
- console.log(`[reader] Parent chain invalid, using chronological order (${entries.length} entries)`);
18624
- return entries;
18892
+ console.log(`[reader] Parent chain invalid, using chronological order (${entries2.length} entries)`);
18893
+ return entries2;
18625
18894
  }
18626
- const leaf = entries[entries.length - 1];
18895
+ const leaf = entries2[entries2.length - 1];
18627
18896
  const chain = [];
18628
18897
  const seen = /* @__PURE__ */ new Set();
18629
18898
  let current = leaf;
18630
18899
  while (current) {
18631
18900
  if (seen.has(current.uuid)) {
18632
18901
  console.warn(`[reader] Cycle detected in parentUuid chain at ${current.uuid}, falling back to chronological order`);
18633
- return entries;
18902
+ return entries2;
18634
18903
  }
18635
18904
  seen.add(current.uuid);
18636
18905
  chain.push(current);
18637
18906
  current = current.parentUuid ? byUuid.get(current.parentUuid) : void 0;
18638
18907
  }
18639
18908
  chain.reverse();
18640
- return recoverOrphanedParallelToolResults(entries, chain, byUuid, seen);
18909
+ return recoverOrphanedParallelToolResults(entries2, chain, byUuid, seen);
18641
18910
  }
18642
- function checkParentChainValid(entries, byUuid) {
18643
- const sample = entries.slice(-10);
18911
+ function checkParentChainValid(entries2, byUuid) {
18912
+ const sample = entries2.slice(-10);
18644
18913
  for (const e of sample) {
18645
18914
  if (e.parentUuid === e.uuid) {
18646
18915
  return false;
18647
18916
  }
18648
18917
  }
18649
- const leaf = entries[entries.length - 1];
18918
+ const leaf = entries2[entries2.length - 1];
18650
18919
  let current = leaf;
18651
18920
  let depth = 0;
18652
18921
  const seen = /* @__PURE__ */ new Set();
@@ -18659,12 +18928,12 @@ function checkParentChainValid(entries, byUuid) {
18659
18928
  if (!parent) return false;
18660
18929
  current = parent;
18661
18930
  }
18662
- const coverage = depth / entries.length;
18931
+ const coverage = depth / entries2.length;
18663
18932
  if (coverage < 0.5) {
18664
- console.log(`[reader] Parent chain covers ${depth}/${entries.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18933
+ console.log(`[reader] Parent chain covers ${depth}/${entries2.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18665
18934
  return false;
18666
18935
  }
18667
- return depth >= 1 || entries.length <= 1;
18936
+ return depth >= 1 || entries2.length <= 1;
18668
18937
  }
18669
18938
  function recoverOrphanedParallelToolResults(allEntries, chain, byUuid, seen) {
18670
18939
  const chainAssistants = chain.filter(
@@ -18741,12 +19010,12 @@ async function readSessionHistory(filePath) {
18741
19010
  } else {
18742
19011
  lines = await readAllLines(filePath);
18743
19012
  }
18744
- const entries = parseEntries(lines);
19013
+ const entries2 = parseEntries(lines);
18745
19014
  let postBoundaryEntries;
18746
19015
  if (fileSize <= SKIP_PRECOMPACT_THRESHOLD) {
18747
- postBoundaryEntries = getEntriesAfterLastBoundary(entries);
19016
+ postBoundaryEntries = getEntriesAfterLastBoundary(entries2);
18748
19017
  } else {
18749
- postBoundaryEntries = entries;
19018
+ postBoundaryEntries = entries2;
18750
19019
  }
18751
19020
  if (postBoundaryEntries.length === 0) return [];
18752
19021
  const chain = buildConversationChain(postBoundaryEntries);
@@ -18771,11 +19040,11 @@ async function readAllLines(filePath) {
18771
19040
  });
18772
19041
  }
18773
19042
  function parseEntries(lines) {
18774
- const entries = [];
19043
+ const entries2 = [];
18775
19044
  for (const line of lines) {
18776
19045
  try {
18777
19046
  const obj = JSON.parse(line);
18778
- entries.push({
19047
+ entries2.push({
18779
19048
  uuid: obj.id || "",
18780
19049
  parentUuid: obj.parentId || null,
18781
19050
  type: obj.type || "",
@@ -18785,16 +19054,28 @@ function parseEntries(lines) {
18785
19054
  } catch {
18786
19055
  }
18787
19056
  }
18788
- return entries;
19057
+ return entries2;
18789
19058
  }
18790
- function getEntriesAfterLastBoundary(entries) {
19059
+ function getEntriesAfterLastBoundary(entries2) {
18791
19060
  let lastBoundaryIdx = -1;
18792
- for (let i = 0; i < entries.length; i++) {
18793
- if (entries[i].type === "compact_boundary") {
19061
+ for (let i = 0; i < entries2.length; i++) {
19062
+ if (entries2[i].type === "compact_boundary") {
18794
19063
  lastBoundaryIdx = i;
18795
19064
  }
18796
19065
  }
18797
- return lastBoundaryIdx >= 0 ? entries.slice(lastBoundaryIdx + 1) : entries;
19066
+ return lastBoundaryIdx >= 0 ? entries2.slice(lastBoundaryIdx + 1) : entries2;
19067
+ }
19068
+ function pickToolCallArguments(block) {
19069
+ const raw = block.partialArgs;
19070
+ if (raw) {
19071
+ try {
19072
+ JSON.parse(raw);
19073
+ return raw;
19074
+ } catch {
19075
+ 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`);
19076
+ }
19077
+ }
19078
+ return JSON.stringify(block.arguments ?? {});
18798
19079
  }
18799
19080
  function entryToSessionMessage(entry) {
18800
19081
  if (entry.type === "attachment") {
@@ -18824,7 +19105,7 @@ function entryToSessionMessage(entry) {
18824
19105
  type: "function",
18825
19106
  function: {
18826
19107
  name: block.name,
18827
- arguments: block.partialArgs || JSON.stringify(block.arguments)
19108
+ arguments: pickToolCallArguments(block)
18828
19109
  }
18829
19110
  });
18830
19111
  } else if (block.type === "thinking") {
@@ -19719,13 +20000,15 @@ ${skillsListing}`);
19719
20000
  loaded2.push("session-guidance");
19720
20001
  }
19721
20002
  parts.push(getEnvInfoSection(options.workspace));
19722
- const now = /* @__PURE__ */ new Date();
19723
- const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19724
- parts.push(`# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19725
- \u5F53\u524D\u65F6\u95F4: ${dateStr}`);
19726
20003
  console.log(`[dynamic-prompt] Loaded: ${loaded2.length > 0 ? loaded2.join(", ") : "(none)"}`);
19727
20004
  return parts.join("\n\n");
19728
20005
  }
20006
+ function buildVolatileRuntimeContext() {
20007
+ const now = /* @__PURE__ */ new Date();
20008
+ const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
20009
+ return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
20010
+ \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
20011
+ }
19729
20012
  function formatSkillsListingForPrompt() {
19730
20013
  const tools = registry.list();
19731
20014
  const skillTool = tools.find((t) => t.name === "Skill");
@@ -20192,13 +20475,13 @@ ${ep.episode || ep.summary}`,
20192
20475
 
20193
20476
  // src/handle-query.ts
20194
20477
  init_paths();
20195
- import { readFileSync as readFileSync17, existsSync as existsSync14 } from "node:fs";
20196
- import { join as join23, resolve as resolve6 } from "node:path";
20478
+ import { readFileSync as readFileSync18, existsSync as existsSync15 } from "node:fs";
20479
+ import { join as join24, resolve as resolve6 } from "node:path";
20197
20480
  import * as path17 from "node:path";
20198
- var sessionStartDone = /* @__PURE__ */ new Set();
20199
- function resetSessionStartInjection(sessionId) {
20200
- sessionStartDone.delete(sessionId);
20201
- }
20481
+
20482
+ // src/sender-context.ts
20483
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
20484
+ import { join as join19 } from "node:path";
20202
20485
  var contactMap = null;
20203
20486
  var externalChanWhitelist = null;
20204
20487
  function loadContactMap(workspace) {
@@ -20206,10 +20489,10 @@ function loadContactMap(workspace) {
20206
20489
  contactMap = /* @__PURE__ */ new Map();
20207
20490
  externalChanWhitelist = /* @__PURE__ */ new Set();
20208
20491
  try {
20209
- const contactsPath = join23(workspace, "prompts", "contacts.md");
20210
- console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync14(contactsPath)}`);
20211
- if (existsSync14(contactsPath)) {
20212
- const text = readFileSync17(contactsPath, "utf-8");
20492
+ const contactsPath = join19(workspace, "prompts", "contacts.md");
20493
+ console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync13(contactsPath)}`);
20494
+ if (existsSync13(contactsPath)) {
20495
+ const text = readFileSync14(contactsPath, "utf-8");
20213
20496
  const lines = text.split("\n");
20214
20497
  for (const line of lines) {
20215
20498
  const m = line.match(/^\|\s*(.+?)\s*\|\s*([a-zA-Z0-9_@.]+)\s*\|/);
@@ -20264,14 +20547,45 @@ function truncate(s, maxLen) {
20264
20547
  if (s.length <= maxLen) return s;
20265
20548
  return s.slice(0, maxLen - 1) + "\u2026";
20266
20549
  }
20550
+ function getExternalChanWhitelist(workspace, configExternalChannels) {
20551
+ if (configExternalChannels && configExternalChannels.length > 0) {
20552
+ return new Set(configExternalChannels);
20553
+ }
20554
+ if (!externalChanWhitelist) loadContactMap(workspace);
20555
+ return externalChanWhitelist;
20556
+ }
20557
+ function buildConversationAnchor(inboundMeta, channelName, source, workspace) {
20558
+ if (inboundMeta?.from) {
20559
+ const who = resolveSenderName(inboundMeta, workspace);
20560
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20561
+ \u6B63\u5728\u8DDF${who}\u8BF4\u8BDD @${channelName}${inboundMeta.channelType === "group" ? "\uFF08\u7FA4\u91CC\uFF09" : ""}`;
20562
+ }
20563
+ const selfLabels = {
20564
+ "inner-voice": "\u8FD9\u662F\u6211\u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u60F3\u8D77\u8C01\u5C31\u662F\u8C01",
20565
+ heartbeat: "\u6211\u5728\u7167\u4F8B\u770B\u770B\u81EA\u5DF1\uFF0C\u90FD\u8FD8\u597D\u5417",
20566
+ cron: "\u6211\u7684\u5C0F\u95F9\u949F\u5230\u70B9\u4E86",
20567
+ system: "\u6211\u4EA4\u4EE3\u4E0B\u53BB\u7684\u6D3B\u513F\u6709\u7ED3\u679C\u56DE\u6765\u4E86"
20568
+ };
20569
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20570
+ \u6CA1\u6709\u4EBA\u5728\u8BF4\u8BDD\u2014\u2014${selfLabels[source] || source || "\u81EA\u5DF1\u7684\u4E00\u70B9\u52A8\u9759"}`;
20571
+ }
20572
+
20573
+ // src/handle-query.ts
20574
+ var sessionStartDone = /* @__PURE__ */ new Set();
20575
+ function resolveSystemPrompt(v) {
20576
+ return typeof v === "function" ? v() : v || "";
20577
+ }
20578
+ function resetSessionStartInjection(sessionId) {
20579
+ sessionStartDone.delete(sessionId);
20580
+ }
20267
20581
  var externalChanRulesCache = null;
20268
20582
  function loadExternalChanRules(workspace) {
20269
- const path50 = join23(workspace, "prompts", "external-chan-rules.md");
20583
+ const path50 = join24(workspace, "prompts", "external-chan-rules.md");
20270
20584
  if (externalChanRulesCache && externalChanRulesCache.path === path50) return externalChanRulesCache;
20271
20585
  let content = "";
20272
- if (existsSync14(path50)) {
20586
+ if (existsSync15(path50)) {
20273
20587
  try {
20274
- content = readFileSync17(path50, "utf-8").trim();
20588
+ content = readFileSync18(path50, "utf-8").trim();
20275
20589
  } catch (e) {
20276
20590
  console.warn(`[external-chan-rules] Failed to load: ${e}`);
20277
20591
  }
@@ -20293,13 +20607,6 @@ function getExternalChanRulesBlock(inboundMeta, workspace) {
20293
20607
  return `[\u7CFB\u7EDF\u89C4\u5219]
20294
20608
  ${content}`;
20295
20609
  }
20296
- function getExternalChanWhitelist(workspace, configExternalChannels) {
20297
- if (configExternalChannels && configExternalChannels.length > 0) {
20298
- return new Set(configExternalChannels);
20299
- }
20300
- if (!externalChanWhitelist) loadContactMap(workspace);
20301
- return externalChanWhitelist;
20302
- }
20303
20610
  async function handleQuery(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source) {
20304
20611
  return handleQueryInner(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source);
20305
20612
  }
@@ -20412,14 +20719,6 @@ ${t}` : t });
20412
20719
  ${text}` : text });
20413
20720
  }
20414
20721
  const userMsgContent = contentBlocks;
20415
- const textBlocks = contentBlocks.filter((b) => b.type === "text");
20416
- const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20417
- let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20418
- if (totalImageCount > 0) {
20419
- textForJsonl = textForJsonl ? `${textForJsonl}
20420
- [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20421
- }
20422
- writer.writeUserMessage(textForJsonl);
20423
20722
  const textForHook = typeof text === "string" ? text : text.filter((b) => b.type === "text").map((b) => b.text).join(" ");
20424
20723
  let hookAdditionalContexts = [];
20425
20724
  try {
@@ -20452,19 +20751,41 @@ ${text}` : text });
20452
20751
  chatMode = "work";
20453
20752
  console.log(`[mode] ${sessionId} emotion \u6A21\u5F0F\u5DF2\u5173\u95ED (channels.emotion.enabled=false)\uFF0C\u56DE\u9000 work`);
20454
20753
  }
20455
- try {
20456
- const wm = readFileSync17(join23(workspace, ".work-mode"), "utf-8").trim();
20457
- if (wm === "on" && chatMode !== "work") {
20458
- chatMode = "work";
20459
- console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20460
- } else if (wm === "off" && chatMode !== "emotion") {
20461
- chatMode = "emotion";
20462
- console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20754
+ if (source === "user") {
20755
+ try {
20756
+ const wm = readFileSync18(join24(workspace, ".work-mode"), "utf-8").trim();
20757
+ if (wm === "on" && chatMode !== "work") {
20758
+ chatMode = "work";
20759
+ console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20760
+ } else if (wm === "off" && chatMode !== "emotion") {
20761
+ chatMode = "emotion";
20762
+ console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20763
+ }
20764
+ } catch {
20463
20765
  }
20464
- } catch {
20465
20766
  }
20466
20767
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
20467
- const dynamicPromptWithHooks = hookAdditionalContexts.length > 0 ? dynamicPrompt + "\n\n" + hookAdditionalContexts.join("\n\n") : dynamicPrompt;
20768
+ const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
20769
+ const volatileParts = [
20770
+ // 0901:meta 头已带秒级时间,频道消息不重复;无 meta 的注入路径(cron 等 prompt 不含时间的)才补
20771
+ ...metaStr ? [] : [buildVolatileRuntimeContext()],
20772
+ conversationAnchor,
20773
+ ...hookAdditionalContexts
20774
+ ].filter(Boolean);
20775
+ if (volatileParts.length > 0) {
20776
+ const volatileBlock = { type: "text", text: volatileParts.join("\n\n") };
20777
+ const metaIdx = metaStr ? 1 : 0;
20778
+ contentBlocks.splice(metaIdx, 0, volatileBlock);
20779
+ }
20780
+ const textBlocks = contentBlocks.filter((b) => b.type === "text");
20781
+ const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20782
+ let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20783
+ if (totalImageCount > 0) {
20784
+ textForJsonl = textForJsonl ? `${textForJsonl}
20785
+ [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20786
+ }
20787
+ writer.writeUserMessage(textForJsonl);
20788
+ const dynamicPromptWithHooks = dynamicPrompt;
20468
20789
  if (Array.isArray(userMsgContent)) {
20469
20790
  console.log(`[pre-llm-debug] userMsgContent blocks: ${userMsgContent.length}`);
20470
20791
  for (let i = 0; i < userMsgContent.length; i++) {
@@ -20474,32 +20795,11 @@ ${text}` : text });
20474
20795
  } else {
20475
20796
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
20476
20797
  }
20477
- let recallFull = false;
20478
- for (const ctx of hookAdditionalContexts) {
20479
- const rm2 = ctx.match(/## 记忆窗口:(\S+)/);
20480
- if (rm2) {
20481
- recallFull = rm2[1] === "full";
20482
- break;
20483
- }
20484
- }
20485
- const emotionFullN = liveConfig.get("channels.emotion.fullContextN") ?? 50;
20486
- const emotionMode = chatMode === "emotion" && !recallFull;
20487
- const isLight = isLightMode(chatMode, channelName) && !recallFull && !emotionMode;
20798
+ const emotionStripped = chatMode === "emotion";
20799
+ const isLight = isLightMode(chatMode, channelName) && chatMode !== "emotion";
20488
20800
  const lightN = resolveLightN(channelName);
20489
- const stripFull = recallFull && chatMode === "emotion";
20490
- const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: stripFull });
20491
- if (stripFull) {
20492
- 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`);
20493
- } else if (recallFull && chatMode === "work") {
20494
- console.log(`[light-context] ${channelName} mode=work recall_full=true \u2192 \u4E0D\u5265 tool\uFF0C\u5168\u91CF ${history.length} \u6761\uFF08work \u6A21\u5F0F\uFF09`);
20495
- }
20801
+ const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: emotionStripped });
20496
20802
  const messages = [...lightHistory, msg.user(userMsgContent)];
20497
- if (emotionMode) {
20498
- const strippedAll = buildLightHistory(history, { isLight: false, lightN: emotionFullN, channelName, chatMode, recallFull: true, quiet: true });
20499
- const sliced = strippedAll.length > emotionFullN ? strippedAll.slice(-emotionFullN) : strippedAll;
20500
- 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})`);
20501
- messages.splice(0, messages.length, ...sliced, msg.user(userMsgContent));
20502
- }
20503
20803
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
20504
20804
  const delta = deps.mcpManager.getMcpDelta();
20505
20805
  if (delta && delta.addedBlocks.length > 0) {
@@ -20592,14 +20892,16 @@ ${text}` : text });
20592
20892
  model,
20593
20893
  parentMessages: messages,
20594
20894
  // 对齐 CC: fork subagent 继承父对话历史
20595
- parentSystemPrompt: deps.systemPrompt,
20596
- // 对齐 CC: fork 共享 prompt cache
20895
+ parentSystemPrompt: resolveSystemPrompt(deps.systemPrompt),
20896
+ // 对齐 CC: fork 共享 prompt cache(0902 每消息现取)
20597
20897
  features: liveConfig.get("agents.defaults.features"),
20598
20898
  // engine config features(AgentTool 读 agentTool.showProgress)
20599
20899
  channelTarget: channelTarget ?? "",
20600
20900
  // 回复目标(Discord channel ID / user ID)
20601
20901
  inboundFrom: inboundMeta?.from || "",
20602
20902
  // 0826 当前消息发送者 ID(msg_send 回发拦截用;注入消息无 inboundMeta 必须 ?.)
20903
+ inboundIsBot: inboundMeta?.isBot || false,
20904
+ // 0901 rate-breaker 信号:本轮触发者是否 bot(Discord author.bot 官方标记)
20603
20905
  renderer: deps.renderer,
20604
20906
  // TurnRenderer 实例(子 agent 走 display 配置)
20605
20907
  visualEmitter: deps.visualEmitter,
@@ -20617,6 +20919,10 @@ ${text}` : text });
20617
20919
  _deps: deps
20618
20920
  // tool 内部需要完整 deps
20619
20921
  };
20922
+ {
20923
+ const { recordInboundBotFlag: recordInboundBotFlag2 } = await Promise.resolve().then(() => (init_rate_breaker(), rate_breaker_exports));
20924
+ recordInboundBotFlag2(inboundMeta?.from, inboundMeta?.isBot);
20925
+ }
20620
20926
  let fullResponse = "";
20621
20927
  const toolHistoryEntries = [];
20622
20928
  let compacted = false;
@@ -20674,7 +20980,7 @@ ${text}` : text });
20674
20980
  for (const memPath of newPaths) {
20675
20981
  try {
20676
20982
  const stat4 = statSync(memPath);
20677
- const content = readFileSync17(memPath, "utf-8");
20983
+ const content = readFileSync18(memPath, "utf-8");
20678
20984
  const header = memoryHeader(memPath, stat4.mtimeMs);
20679
20985
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
20680
20986
  } catch {
@@ -20758,7 +21064,7 @@ ${text}` : text });
20758
21064
  const attachmentMemories = [];
20759
21065
  for (const mem of relevantMemories) {
20760
21066
  try {
20761
- const content = mem.content ?? readFileSync17(mem.path, "utf-8");
21067
+ const content = mem.content ?? readFileSync18(mem.path, "utf-8");
20762
21068
  const header = mem.header ?? memoryHeader(mem.path, mem.mtimeMs);
20763
21069
  attachmentMemories.push({ path: mem.path, content, mtimeMs: mem.mtimeMs, header });
20764
21070
  } catch {
@@ -20790,7 +21096,7 @@ ${text}` : text });
20790
21096
  return "(\u5DF2\u505C\u6B62)";
20791
21097
  }
20792
21098
  const mode = chatMode;
20793
- const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion", { recallFull }) : void 0;
21099
+ const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion") : void 0;
20794
21100
  deps.renderer?.setEmotionMode?.(mode === "emotion");
20795
21101
  console.log(`[mode] ${sessionId} \u2192 ${mode} (${mode === "emotion" ? "\u53EA SOUL, \u5173 tool \u663E\u793A/thinking/stop-hook" : "\u9ED8\u8BA4 stable, \u663E\u793A tool"})`);
20796
21102
  const toolExclude = resolveToolExclude(channelName);
@@ -21011,7 +21317,7 @@ stack: ${err.stack ?? "(none)"}`);
21011
21317
  }
21012
21318
  } catch (err) {
21013
21319
  try {
21014
- (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}
21320
+ (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}
21015
21321
  stack: ${err.stack ?? "(none)"}
21016
21322
  `);
21017
21323
  } catch {
@@ -21842,6 +22148,13 @@ function setupFileLogging(stateDir) {
21842
22148
  const prefix = `[${ts()}] [ERR] `;
21843
22149
  origError(prefix, ...args2);
21844
22150
  logStream.write(`${prefix}${args2.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
22151
+ `);
22152
+ };
22153
+ const origWarn = console.warn;
22154
+ console.warn = (...args2) => {
22155
+ const prefix = `[${ts()}] [WARN] `;
22156
+ origWarn(prefix, ...args2);
22157
+ logStream.write(`${prefix}${args2.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21845
22158
  `);
21846
22159
  };
21847
22160
  }
@@ -21933,17 +22246,17 @@ var INJECTED_CONTENT_PATTERNS = [
21933
22246
  // 群聊敏感词拦截回执(group.sensitiveWords),role:user 注入但非真实用户
21934
22247
  ];
21935
22248
  function parseJsonlEntries(lines) {
21936
- const entries = [];
22249
+ const entries2 = [];
21937
22250
  for (const line of lines) {
21938
22251
  const trimmed = line.trim();
21939
22252
  if (!trimmed) continue;
21940
22253
  try {
21941
- entries.push(JSON.parse(trimmed));
22254
+ entries2.push(JSON.parse(trimmed));
21942
22255
  } catch {
21943
- entries.push(null);
22256
+ entries2.push(null);
21944
22257
  }
21945
22258
  }
21946
- return entries;
22259
+ return entries2;
21947
22260
  }
21948
22261
  function isRuntimeContextInjected(entry, nextEntry) {
21949
22262
  if (!nextEntry || typeof nextEntry !== "object") return false;
@@ -21995,16 +22308,16 @@ function findLastRealUserMsg(jsonlPath) {
21995
22308
  } catch {
21996
22309
  return null;
21997
22310
  }
21998
- const entries = parseJsonlEntries(lines);
21999
- for (let i = entries.length - 1; i >= 0; i--) {
22000
- const entry = entries[i];
22311
+ const entries2 = parseJsonlEntries(lines);
22312
+ for (let i = entries2.length - 1; i >= 0; i--) {
22313
+ const entry = entries2[i];
22001
22314
  if (!entry || typeof entry !== "object") continue;
22002
22315
  if (entry.type !== "message") continue;
22003
22316
  const msg2 = entry.message;
22004
22317
  if (!msg2 || msg2.role !== "user") continue;
22005
22318
  const text = extractText3(msg2.content);
22006
22319
  if (isSystemSender(text)) continue;
22007
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
22320
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
22008
22321
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
22009
22322
  const ts = entry.timestamp || "";
22010
22323
  const clean = cleanText(text);
@@ -22036,12 +22349,12 @@ function recentMessages(sessions, hours = 12, limit = 60) {
22036
22349
  const jsonlPath = resolveScopeMainJsonl(sessions);
22037
22350
  if (!jsonlPath) return [];
22038
22351
  const lines = fs19.readFileSync(jsonlPath, "utf-8").split("\n");
22039
- const entries = parseJsonlEntries(lines);
22352
+ const entries2 = parseJsonlEntries(lines);
22040
22353
  const nowMs = Date.now();
22041
22354
  const cutoffMs = nowMs - hours * 36e5;
22042
22355
  const results = [];
22043
- for (let i = 0; i < entries.length; i++) {
22044
- const entry = entries[i];
22356
+ for (let i = 0; i < entries2.length; i++) {
22357
+ const entry = entries2[i];
22045
22358
  if (!entry || typeof entry !== "object") continue;
22046
22359
  if (entry.type !== "message") continue;
22047
22360
  const msg2 = entry.message;
@@ -22051,14 +22364,14 @@ function recentMessages(sessions, hours = 12, limit = 60) {
22051
22364
  const text = extractText3(msg2.content);
22052
22365
  if (role === "user") {
22053
22366
  if (isSystemSender(text)) continue;
22054
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
22367
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
22055
22368
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
22056
22369
  }
22057
22370
  if (role === "assistant") {
22058
22371
  if (text.startsWith("HEARTBEAT_OK")) continue;
22059
22372
  let isInjectedResponse = false;
22060
22373
  for (let j = i - 1; j >= Math.max(i - 5, -1); j--) {
22061
- const prevE = entries[j];
22374
+ const prevE = entries2[j];
22062
22375
  if (!prevE || typeof prevE !== "object" || prevE.type !== "message") continue;
22063
22376
  const prevMsg = prevE.message;
22064
22377
  if (!prevMsg || prevMsg.role !== "user") continue;
@@ -22282,7 +22595,7 @@ async function judgeReason(task, taskState, cfg, provider, model) {
22282
22595
  const reason = task.blockedReason || "\uFF08\u6CA1\u7ED9\u7406\u7531\uFF09";
22283
22596
  const elapsed = taskState.lastProgressAt ? formatDuration(Date.now() - new Date(taskState.lastProgressAt).getTime()) : "\u5F88\u4E45\u6CA1\u52A8\u4E86";
22284
22597
  const staleLevel = (taskState.staleLevel || 0) + 1;
22285
- 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
22598
+ 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
22286
22599
 
22287
22600
  \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
22288
22601
 
@@ -22704,7 +23017,8 @@ var NudgePlugin = class {
22704
23017
  const stream = this.provider.streamChat({
22705
23018
  model: this.model,
22706
23019
  systemPrompt: [
22707
- "\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",
23020
+ // 0831 去掉硬编码人名(原文"判断你(小柯)是否"):engine 代码多 agent 共用,身份由 SOUL.md 定
23021
+ "\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",
22708
23022
  "",
22709
23023
  "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",
22710
23024
  '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',
@@ -23565,18 +23879,18 @@ function readRecentMessages(sessions, n) {
23565
23879
  const file = path23.join(sessions.sessionsDir, `${mainId}.jsonl`);
23566
23880
  if (!fs23.existsSync(file)) return [];
23567
23881
  const lines = readLastNLines(file, n * 4 + 20);
23568
- const entries = [];
23882
+ const entries2 = [];
23569
23883
  for (const line of lines) {
23570
23884
  const trimmed = line.trim();
23571
23885
  if (!trimmed) continue;
23572
23886
  try {
23573
- entries.push(JSON.parse(trimmed));
23887
+ entries2.push(JSON.parse(trimmed));
23574
23888
  } catch {
23575
23889
  }
23576
23890
  }
23577
23891
  const out = [];
23578
- for (let i = entries.length - 1; i >= 0 && out.length < n; i--) {
23579
- const e = entries[i];
23892
+ for (let i = entries2.length - 1; i >= 0 && out.length < n; i--) {
23893
+ const e = entries2[i];
23580
23894
  if (!e || typeof e !== "object" || e.type !== "message") continue;
23581
23895
  const msg2 = e.message;
23582
23896
  if (!msg2) continue;
@@ -24525,7 +24839,7 @@ function formatBeijingTs(d) {
24525
24839
  }
24526
24840
 
24527
24841
  // src/calendar/commands.ts
24528
- import { existsSync as existsSync15, statSync as statSync8 } from "node:fs";
24842
+ import { existsSync as existsSync16, statSync as statSync8 } from "node:fs";
24529
24843
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
24530
24844
  var WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
24531
24845
  function fmtEnd(start, durationMin) {
@@ -24697,7 +25011,7 @@ function addTask(db, args2) {
24697
25011
  }
24698
25012
  try {
24699
25013
  const absPath = isAbsolute4(docPath) ? docPath : resolve7(process.cwd(), docPath);
24700
- if (!existsSync15(absPath)) {
25014
+ if (!existsSync16(absPath)) {
24701
25015
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728
24702
25016
  \u8DEF\u5F84: ${docPath}
24703
25017
  \u89E3\u6790\u540E: ${absPath}
@@ -25180,7 +25494,7 @@ function registerVoiceChatBridge(httpServer, dispatcher, deps, config2, sessions
25180
25494
  }
25181
25495
 
25182
25496
  // src/voice-chat/config.ts
25183
- var DEFAULTS2 = {
25497
+ var DEFAULTS3 = {
25184
25498
  enabled: false,
25185
25499
  pythonPort: 8011,
25186
25500
  webhookPath: "/webhook/voice-chat",
@@ -25191,20 +25505,20 @@ var DEFAULTS2 = {
25191
25505
  // 8/25 翀哥:断句等待默认 2s(samples@16kHz)
25192
25506
  };
25193
25507
  function parseVoiceChatConfig(raw) {
25194
- if (!raw) return { ...DEFAULTS2 };
25508
+ if (!raw) return { ...DEFAULTS3 };
25195
25509
  return {
25196
25510
  enabled: raw.enabled === true,
25197
25511
  spawnPython: raw.spawnPython !== false,
25198
25512
  // 默认 true,配 false 只注册 webhook
25199
- pythonPort: raw.pythonPort ?? DEFAULTS2.pythonPort,
25200
- webhookPath: raw.webhookPath ?? DEFAULTS2.webhookPath,
25201
- callbackPath: raw.callbackPath ?? DEFAULTS2.callbackPath,
25513
+ pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25514
+ webhookPath: raw.webhookPath ?? DEFAULTS3.webhookPath,
25515
+ callbackPath: raw.callbackPath ?? DEFAULTS3.callbackPath,
25202
25516
  pythonPath: raw.pythonPath,
25203
25517
  vadModelPath: raw.vadModelPath,
25204
25518
  asrModelPath: raw.asrModelPath || "iic/SenseVoiceSmall",
25205
- asrLanguage: raw.asrLanguage ?? DEFAULTS2.asrLanguage,
25206
- vadThreshold: raw.vadThreshold ?? DEFAULTS2.vadThreshold,
25207
- postEndMonitor: raw.postEndMonitor ?? DEFAULTS2.postEndMonitor,
25519
+ asrLanguage: raw.asrLanguage ?? DEFAULTS3.asrLanguage,
25520
+ vadThreshold: raw.vadThreshold ?? DEFAULTS3.vadThreshold,
25521
+ postEndMonitor: raw.postEndMonitor ?? DEFAULTS3.postEndMonitor,
25208
25522
  model: raw.model,
25209
25523
  thinking: raw.thinking === true,
25210
25524
  tts: raw.tts ? {
@@ -25352,7 +25666,7 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
25352
25666
  - \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`;
25353
25667
  const engine = new QueryEngine(llmProvider, {
25354
25668
  model: modelId,
25355
- systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
25669
+ systemPrompt: resolveSystemPrompt(ctx.deps.systemPrompt) + voiceChatRules,
25356
25670
  maxTokens: 4096,
25357
25671
  temperature: 0.7,
25358
25672
  disableThinking: !this.config.thinking,
@@ -25526,7 +25840,7 @@ import path28 from "node:path";
25526
25840
  import fs28 from "node:fs";
25527
25841
 
25528
25842
  // src/memory/cognifold/config.ts
25529
- var DEFAULTS3 = {
25843
+ var DEFAULTS4 = {
25530
25844
  pythonPort: 9001,
25531
25845
  autoStart: true,
25532
25846
  persistDir: "./sessions",
@@ -25538,15 +25852,15 @@ function parseCognifoldConfig(raw) {
25538
25852
  if (!raw) return { enabled: false };
25539
25853
  return {
25540
25854
  enabled: raw.enabled === true,
25541
- pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25542
- pythonPath: raw.pythonPath ?? DEFAULTS3.pythonPath,
25855
+ pythonPort: raw.pythonPort ?? DEFAULTS4.pythonPort,
25856
+ pythonPath: raw.pythonPath ?? DEFAULTS4.pythonPath,
25543
25857
  autoStart: raw.autoStart !== false,
25544
25858
  // default true
25545
- baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS3.pythonPort}/api/v1`,
25546
- persistDir: raw.persistDir ?? DEFAULTS3.persistDir,
25859
+ baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS4.pythonPort}/api/v1`,
25860
+ persistDir: raw.persistDir ?? DEFAULTS4.persistDir,
25547
25861
  scopes: raw.scopes,
25548
- readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS3.readyTimeoutMs,
25549
- maxRestarts: raw.maxRestarts ?? DEFAULTS3.maxRestarts,
25862
+ readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS4.readyTimeoutMs,
25863
+ maxRestarts: raw.maxRestarts ?? DEFAULTS4.maxRestarts,
25550
25864
  llm: raw.llm
25551
25865
  };
25552
25866
  }
@@ -25662,13 +25976,13 @@ var CogniFoldClient = class {
25662
25976
 
25663
25977
  // src/memory/cognifold/session-manager.ts
25664
25978
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
25665
- import { join as join27, dirname as dirname3 } from "node:path";
25979
+ import { join as join28, dirname as dirname3 } from "node:path";
25666
25980
  var CogniFoldSessionManager = class {
25667
25981
  constructor(workspacePath, config2, client) {
25668
25982
  this.workspacePath = workspacePath;
25669
25983
  this.config = config2;
25670
25984
  this.client = client;
25671
- this.sessionsDir = join27(workspacePath, ".cognifold", "sessions");
25985
+ this.sessionsDir = join28(workspacePath, ".cognifold", "sessions");
25672
25986
  }
25673
25987
  workspacePath;
25674
25988
  config;
@@ -25738,7 +26052,7 @@ var CogniFoldSessionManager = class {
25738
26052
  console.log(`[cognifold] Created new session for scope "${scope}": ${newSession.sessionId}`);
25739
26053
  }
25740
26054
  getFilePath(scope) {
25741
- return join27(this.sessionsDir, `${scope}.json`);
26055
+ return join28(this.sessionsDir, `${scope}.json`);
25742
26056
  }
25743
26057
  async writeFileSafe(filePath, data) {
25744
26058
  try {
@@ -26058,7 +26372,7 @@ import path29 from "node:path";
26058
26372
  import fs29 from "node:fs";
26059
26373
 
26060
26374
  // src/memory/everos/config.ts
26061
- var DEFAULTS4 = {
26375
+ var DEFAULTS5 = {
26062
26376
  everosUrl: "http://127.0.0.1:8100",
26063
26377
  agenticUrl: "http://127.0.0.1:8101",
26064
26378
  agenticPort: 8101,
@@ -26084,7 +26398,7 @@ function parseEverosConfig(raw, providers) {
26084
26398
  if (!raw) {
26085
26399
  return {
26086
26400
  enabled: false,
26087
- ...DEFAULTS4,
26401
+ ...DEFAULTS5,
26088
26402
  userId: "xiaomei",
26089
26403
  llm: { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
26090
26404
  rerank: { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
@@ -26094,12 +26408,12 @@ function parseEverosConfig(raw, providers) {
26094
26408
  }
26095
26409
  return {
26096
26410
  enabled: raw.enabled === true,
26097
- everosUrl: raw.everosUrl ?? DEFAULTS4.everosUrl,
26098
- agenticUrl: raw.agenticUrl ?? DEFAULTS4.agenticUrl,
26099
- agenticPort: raw.agenticPort ?? DEFAULTS4.agenticPort,
26411
+ everosUrl: raw.everosUrl ?? DEFAULTS5.everosUrl,
26412
+ agenticUrl: raw.agenticUrl ?? DEFAULTS5.agenticUrl,
26413
+ agenticPort: raw.agenticPort ?? DEFAULTS5.agenticPort,
26100
26414
  userId: raw.userId ?? "xiaomei",
26101
26415
  autoStart: raw.autoStart !== false,
26102
- defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
26416
+ defaultMode: raw.defaultMode ?? DEFAULTS5.defaultMode,
26103
26417
  llm: resolveProviderConfig(
26104
26418
  raw.llm,
26105
26419
  providers,
@@ -26509,8 +26823,8 @@ function scanSkills(skillsDir) {
26509
26823
  }
26510
26824
  const skills = [];
26511
26825
  const scanDir = (dir, depth) => {
26512
- const entries = fs30.readdirSync(dir, { withFileTypes: true });
26513
- for (const entry of entries) {
26826
+ const entries2 = fs30.readdirSync(dir, { withFileTypes: true });
26827
+ for (const entry of entries2) {
26514
26828
  if (entry.name.startsWith(".") || entry.name === "_archive") continue;
26515
26829
  const full = path30.join(dir, entry.name);
26516
26830
  if (entry.isDirectory() && depth < 3) {
@@ -26801,6 +27115,7 @@ ${rawOutput}
26801
27115
  // src/tools/msg-send.ts
26802
27116
  init_live();
26803
27117
  init_registry();
27118
+ init_rate_breaker();
26804
27119
  function getConfig() {
26805
27120
  return liveConfig.all();
26806
27121
  }
@@ -26864,10 +27179,10 @@ channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u56D
26864
27179
  \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
26865
27180
 
26866
27181
  Examples:
26867
- - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", content="\u4F60\u597D"
26868
- - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", content="\u4F60\u597D"
26869
- - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", content="\u7CFB\u7EDF\u901A\u77E5"
26870
- - \u53D1 DM: to="1502999996616933428", content="\u79C1\u804A\u5185\u5BB9"
27182
+ - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", content="\u4F60\u597D"
27183
+ - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", content="\u4F60\u597D"
27184
+ - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", content="\u7CFB\u7EDF\u901A\u77E5"
27185
+ - \u53D1 DM: to="1111111111111111111", content="\u79C1\u804A\u5185\u5BB9"
26871
27186
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", content="\u4ECE\u98DE\u4E66\u53D1\u5230Discord"
26872
27187
  - \u56DE\u590D\u6765\u6E90\u9891\u9053: content="\u6536\u5230"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26873
27188
  schema: {
@@ -26896,12 +27211,12 @@ Examples:
26896
27211
  }
26897
27212
  const toIds = to ? to.split(",").map((s) => s.trim()).filter(Boolean) : [];
26898
27213
  const dest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
27214
+ const dmEchoDest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26899
27215
  const isDmEcho = ctx.channelType === "dm" && // DM 对话(群聊先观察不拦)
26900
27216
  resolvedSource === ctx.channel && // 目标通道=当前对话通道(真跨通道转发不拦)
26901
27217
  ctx.inboundFrom && // 有当前发送者(注入消息无 inboundFrom 不拦)
26902
- // 形态1:纯 DM 回发发送者
26903
- (!resolvedChannelId && toIds.length > 0 && toIds.length === toIds.filter((id) => id === ctx.inboundFrom).length || // 形态2:目的地=当前会话(fallback 或显式填了当前会话 chat_id)
26904
- !!resolvedChannelId && resolvedChannelId === ctx.channelTarget);
27218
+ dmEchoDest !== void 0 && (dmEchoDest === ctx.channelTarget || dmEchoDest === ctx.inboundFrom) && // 目的地=当前会话/当前对话者
27219
+ toIds.every((id) => id === ctx.inboundFrom);
26905
27220
  if (isDmEcho) {
26906
27221
  console.log(`[msg_send] \u26D4 DM \u56DE\u53D1\u5F53\u524D\u5BF9\u8BDD\u88AB\u62E6: to=${to} channel_id=${resolvedChannelId || "(fallback)"} (${resolvedSource})`);
26907
27222
  return {
@@ -26932,6 +27247,11 @@ Examples:
26932
27247
  }
26933
27248
  const fullMsg = `${mentionPrefix}${content}`;
26934
27249
  const where = resolvedChannelId ? `${resolvedSource} \u9891\u9053 ${resolvedChannelId}` : `${resolvedSource} DM ${toIds[0]}`;
27250
+ const breaker = checkRateBreaker(resolvedSource, ctx.sessionId, dest, toIds, !!ctx.inboundIsBot);
27251
+ if (breaker) {
27252
+ console.log(`[msg_send] \u{1F515} rate-breaker \u62E6\u622A: session=${ctx.sessionId} \u2192 ${where}`);
27253
+ return breaker;
27254
+ }
26935
27255
  try {
26936
27256
  await mgr.send(resolvedSource, dest, fullMsg);
26937
27257
  return { content: `\u6D88\u606F\u5DF2\u53D1\u9001\u5230 ${where}` };
@@ -27047,10 +27367,10 @@ Parameters:
27047
27367
  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
27048
27368
 
27049
27369
  Examples:
27050
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
27051
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
27052
- - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", type="file", path="/tmp/report.pdf"
27053
- - \u53D1\u97F3\u9891 DM: to="1502999996616933428", type="audio", path="/tmp/voice.mp3"
27370
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
27371
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
27372
+ - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", type="file", path="/tmp/report.pdf"
27373
+ - \u53D1\u97F3\u9891 DM: to="1111111111111111111", type="audio", path="/tmp/voice.mp3"
27054
27374
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", type="image", path="/tmp/photo.png"
27055
27375
  - \u53D1\u5230\u6765\u6E90\u9891\u9053: type="image", path="/tmp/photo.png"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
27056
27376
  schema: {
@@ -29556,8 +29876,8 @@ registerCommand({
29556
29876
  sessionManager: deps.sessions,
29557
29877
  sessionId: sid,
29558
29878
  model: deps.config.model,
29559
- contextWindow: deps.compactConfig.contextWindow || 2e5,
29560
- systemPrompt: deps.systemPrompt,
29879
+ contextWindow: (typeof deps.compactConfig === "function" ? deps.compactConfig() : deps.compactConfig).contextWindow || 2e5,
29880
+ systemPrompt: resolveSystemPrompt(deps.systemPrompt),
29561
29881
  toolDefs: registry.definitions(),
29562
29882
  workspace: deps.config.workspace
29563
29883
  });
@@ -29605,7 +29925,7 @@ ${question}`;
29605
29925
  const stream = deps.provider.streamChat({
29606
29926
  model: deps.config.model,
29607
29927
  messages: [{ role: "user", content: wrappedQuestion }],
29608
- systemPrompt: deps.systemPrompt,
29928
+ systemPrompt: resolveSystemPrompt(deps.systemPrompt),
29609
29929
  maxTokens: 2048,
29610
29930
  signal: void 0
29611
29931
  });
@@ -29826,8 +30146,8 @@ registerCommand({
29826
30146
  const { rm: rm2 } = await import("node:fs/promises");
29827
30147
  const teamsDir = getTeamsDir3();
29828
30148
  const { readdir: readdir2 } = await import("node:fs/promises");
29829
- const entries = await readdir2(teamsDir).catch(() => []);
29830
- for (const entry of entries) {
30149
+ const entries2 = await readdir2(teamsDir).catch(() => []);
30150
+ for (const entry of entries2) {
29831
30151
  const entryPath = `${teamsDir}/${entry}`;
29832
30152
  try {
29833
30153
  await rm2(entryPath, { recursive: true, force: true });
@@ -29936,8 +30256,21 @@ registerCommand({
29936
30256
  let current;
29937
30257
  if (deps.getModelOverride()) {
29938
30258
  current = `**${deps.getModelOverride()}** (override)`;
29939
- } else if (deps.provider instanceof FallbackProvider && deps.provider.lastUsedLabel) {
29940
- current = `**${deps.provider.lastUsedLabel}** (auto-route, default: ${deps.config.provider.id}/${deps.config.model})`;
30259
+ } else if (deps.provider instanceof FallbackProvider) {
30260
+ const status = deps.provider.getChainStatus();
30261
+ const chainStr = status.map((e) => e.label).join(" \u2192 ");
30262
+ const next = status.find((e) => e.cooldownMs <= 0);
30263
+ const cooling = status.filter((e) => e.cooldownMs > 0);
30264
+ current = `**auto-route** \u2014 \u94FE: ${chainStr}
30265
+ \u4E0B\u4E00\u4E2A\u8BF7\u6C42\u7528: **${next?.label ?? "(\u5168\u90E8\u51B7\u5374\u4E2D)"}**`;
30266
+ if (cooling.length > 0) {
30267
+ current += `
30268
+ \u51B7\u5374\u4E2D: ${cooling.map((e) => `${e.label}\uFF08\u5269 ${Math.round(e.cooldownMs / 6e4)}min\uFF09`).join("\u3001")}`;
30269
+ }
30270
+ if (deps.provider.lastUsedLabel) {
30271
+ current += `
30272
+ \u4E0A\u6B21\u5B9E\u9645: ${deps.provider.lastUsedLabel}`;
30273
+ }
29941
30274
  } else {
29942
30275
  current = `**${deps.config.provider.id}/${deps.config.model}** (default, auto-route)`;
29943
30276
  }
@@ -30191,6 +30524,16 @@ Use full ref like \`/vision-model ${candidates[0].ref}\``);
30191
30524
  });
30192
30525
 
30193
30526
  // src/engine-startup.ts
30527
+ function resolveModelMaxTokens(modelRef) {
30528
+ try {
30529
+ const [pid, mid] = (modelRef || "").split("/");
30530
+ const models = liveConfig.get(`models.providers.${pid}.models`);
30531
+ const m = models?.find((x) => x?.id === mid);
30532
+ return typeof m?.maxTokens === "number" && m.maxTokens > 0 ? m.maxTokens : 4096;
30533
+ } catch {
30534
+ return 4096;
30535
+ }
30536
+ }
30194
30537
  var _epipeSeen = false;
30195
30538
  process.on("uncaughtException", (err) => {
30196
30539
  const code = err?.code ?? "";
@@ -30260,7 +30603,10 @@ async function startEngine(config2, opts) {
30260
30603
  const licensedFeatures = loadLicense(config2.stateDir, config2.profile?.devMode === true);
30261
30604
  const requiredTools = resolveRequiredTools(config2.profile.features, licensedFeatures);
30262
30605
  registry.licensedFeatures = licensedFeatures;
30263
- const { provider, visionProvider, visionChainLabels } = buildProviderChain(config2);
30606
+ let provider;
30607
+ let visionProvider;
30608
+ let visionChainLabels;
30609
+ ({ provider, visionProvider, visionChainLabels } = buildProviderChain(config2));
30264
30610
  if (visionChainLabels.length > 0) {
30265
30611
  console.log(`[vision] Routing enabled: ${visionChainLabels.join(" \u2192 ")}`);
30266
30612
  }
@@ -30482,11 +30828,36 @@ ${content}`
30482
30828
  console.log(`[DEBUG] definitions() = ${_allDefs.length} defs`);
30483
30829
  console.log(`[DEBUG] active (non-defer) = ${_activeDefs.length}: ${_activeDefs.map((d) => d.function.name).join(", ")}`);
30484
30830
  console.log(`[DEBUG] deferred = ${_deferredDefs.length}: ${_deferredDefs.map((d) => d.function.name).join(", ")}`);
30485
- const systemStable = buildStablePrompt(config2.workspace, config2.prompt);
30831
+ let _stableCache = null;
30832
+ const getSystemStable = () => {
30833
+ const promptCfg = liveConfig.get("prompt") || {};
30834
+ const fileCandidates = /* @__PURE__ */ new Set(["SOUL.md"]);
30835
+ for (const f of promptCfg.staticFiles || []) fileCandidates.add(f);
30836
+ for (const item of promptCfg.order || []) {
30837
+ if (typeof item === "string" && /\.(md|txt|json)$/i.test(item)) fileCandidates.add(item);
30838
+ }
30839
+ const statLines = [JSON.stringify({ mode: promptCfg.mode, order: promptCfg.order })];
30840
+ for (const f of fileCandidates) {
30841
+ try {
30842
+ const p = path49.isAbsolute(f) ? f : path49.join(config2.workspace, f);
30843
+ statLines.push(`${f}:${fs47.statSync(p).mtimeMs}`);
30844
+ } catch {
30845
+ statLines.push(`${f}:missing`);
30846
+ }
30847
+ }
30848
+ const fingerprint = statLines.join("|");
30849
+ if (_stableCache && _stableCache.fingerprint === fingerprint) return _stableCache.prompt;
30850
+ const prompt = buildStablePrompt(config2.workspace, promptCfg);
30851
+ _stableCache = { fingerprint, prompt };
30852
+ console.log(`[prompt] stable \u91CD\u5EFA\uFF08\u6587\u4EF6/config \u53D8\u66F4\uFF09\uFF0C${prompt.length} chars`);
30853
+ return prompt;
30854
+ };
30855
+ const systemStable = getSystemStable();
30486
30856
  const systemDynamic = buildDynamicPrompt({
30487
30857
  workspace: config2.workspace
30488
30858
  });
30489
30859
  const systemPrompt = [systemStable, systemDynamic].join("\n\n");
30860
+ const getSystemPrompt = () => [getSystemStable(), buildDynamicPrompt({ workspace: config2.workspace })].join("\n\n");
30490
30861
  dumpSystemPrompt(config2.workspace, systemStable, systemDynamic);
30491
30862
  const modelDef = config2.provider.models.find((m) => m.id === config2.model);
30492
30863
  const modelContextWindow = modelDef?.contextWindow;
@@ -30505,13 +30876,21 @@ ${content}`
30505
30876
  // 默认 5MB
30506
30877
  );
30507
30878
  const memoryFlushEnabled = config2.compaction?.memoryFlush?.enabled !== false;
30508
- const compactConfig = {
30509
- ...DEFAULT_COMPACT_CONFIG,
30510
- ...config2.compaction,
30511
- // 优先级:compaction.contextWindow > model.contextWindow > DEFAULT 200K
30512
- ...modelContextWindow && !config2.compaction?.contextWindow ? { contextWindow: modelContextWindow } : {},
30513
- forceFlushTranscriptBytes
30879
+ const getCompactConfig = () => {
30880
+ const liveComp = liveConfig.get("compaction") || {};
30881
+ const liveModel = (liveConfig.get("providers") || {})[liveConfig.get("agents.defaults.model.primary")?.split("/")[0] || ""];
30882
+ const liveModelId = liveConfig.get("agents.defaults.model.primary")?.split("/")?.[1];
30883
+ const mDef = liveModel?.models?.find((m) => m.id === liveModelId);
30884
+ const mCW = mDef?.contextWindow;
30885
+ const cfg = {
30886
+ ...DEFAULT_COMPACT_CONFIG,
30887
+ ...liveComp,
30888
+ ...mCW && !liveComp.contextWindow ? { contextWindow: mCW } : {},
30889
+ forceFlushTranscriptBytes
30890
+ };
30891
+ return cfg;
30514
30892
  };
30893
+ const compactConfig = getCompactConfig();
30515
30894
  if (compactConfig.contextWindow !== DEFAULT_COMPACT_CONFIG.contextWindow) {
30516
30895
  console.log(`[compact] Context window: ${compactConfig.contextWindow} (from ${config2.compaction?.contextWindow ? "config" : modelContextWindow ? "model" : "default"})`);
30517
30896
  }
@@ -30520,12 +30899,12 @@ ${content}`
30520
30899
  }
30521
30900
  const engine = new QueryEngine(provider, {
30522
30901
  model: config2.model,
30523
- systemPrompt,
30524
- systemStable,
30525
- // 对齐 OpenClaw: stable prefix 用于 prompt cache
30526
- compactConfig,
30527
- // 对齐 CC compaction
30528
- maxTokens: 4096,
30902
+ systemPrompt: getSystemPrompt,
30903
+ systemStable: getSystemStable,
30904
+ // 0902 函数形态:mtime 缓存 getter,改 prompt 文件热生效
30905
+ compactConfig: getCompactConfig,
30906
+ // 0902 函数形态:每 turn 刷新
30907
+ maxTokens: () => resolveModelMaxTokens(liveConfig.get("agents.defaults.model.primary") || config2.model),
30529
30908
  temperature: 0.7,
30530
30909
  maxTurns: config2.profile.maxTurns,
30531
30910
  // 从配置读,默认 50(query.ts 里 fallback)
@@ -30536,10 +30915,10 @@ ${content}`
30536
30915
  if (visionProvider && visionConfig) {
30537
30916
  visionEngine = new QueryEngine(visionProvider, {
30538
30917
  model: visionConfig.modelId,
30539
- systemPrompt,
30540
- systemStable,
30541
- compactConfig,
30542
- maxTokens: 4096,
30918
+ systemPrompt: getSystemPrompt,
30919
+ systemStable: getSystemStable,
30920
+ compactConfig: getCompactConfig,
30921
+ maxTokens: () => resolveModelMaxTokens(`${visionConfig.providerId}/${visionConfig.modelId}`),
30543
30922
  temperature: 0.7,
30544
30923
  maxTurns: config2.profile.maxTurns,
30545
30924
  agentLabel: "main"
@@ -30611,7 +30990,8 @@ ${content}`
30611
30990
  providerApi: config2.provider.api,
30612
30991
  model: config2.model,
30613
30992
  modelInputs: modelDef?.input || ["text"],
30614
- systemPrompt,
30993
+ systemPrompt: getSystemPrompt,
30994
+ // 0902 函数形态:/btw、voice-chat 等按调用时现取(stable mtime 缓存 + dynamic 现算)
30615
30995
  channels: config2.channels,
30616
30996
  config: config2,
30617
30997
  // tool 读自己配置用
@@ -30625,36 +31005,28 @@ ${content}`
30625
31005
  } : void 0,
30626
31006
  mcpManager
30627
31007
  };
30628
- if (visionEngine && visionConfig) {
31008
+ const visionMetaInit = visionEngine && visionConfig ? (() => {
30629
31009
  const vpCfg = config2.providers?.[visionConfig.providerId];
30630
31010
  const visionModelDef = vpCfg?.models?.find((m) => m.id === visionConfig.modelId);
30631
- visionDeps = {
30632
- engine: visionEngine,
30633
- sessions,
30634
- channelManager,
30635
- workspace: config2.workspace,
31011
+ return {
30636
31012
  providerId: visionConfig.providerId,
30637
31013
  providerApi: vpCfg?.api || "openai-completions",
30638
31014
  model: visionConfig.modelId,
30639
- modelInputs: visionModelDef?.input || ["text", "image"],
30640
- systemPrompt,
30641
- channels: config2.channels,
30642
- config: config2,
30643
- // tool 读自己配置用
30644
- recallProvider: memoryRecallProvider || void 0,
30645
- extractProvider: memoryExtractProvider || void 0,
30646
- everosCfg: config2.everos ? {
30647
- ...config2.everos,
30648
- // resolve provider 引用:从 providers 取 apiKey(跟主 deps 同逻辑)
30649
- llm: config2.everos.llm?.provider && config2.providers?.[config2.everos.llm.provider] ? { ...config2.everos.llm, apiKey: config2.providers[config2.everos.llm.provider].apiKey } : config2.everos.llm,
30650
- rerank: config2.everos.rerank?.provider && config2.providers?.[config2.everos.rerank.provider] ? { ...config2.everos.rerank, apiKey: config2.providers[config2.everos.rerank.provider].apiKey } : config2.everos.rerank
30651
- } : void 0
31015
+ modelInputs: visionModelDef?.input || ["text", "image"]
30652
31016
  };
31017
+ })() : null;
31018
+ let visionMeta = visionMetaInit;
31019
+ function resolveVisionDeps() {
31020
+ if (!visionEngine || !visionMeta) return null;
31021
+ if (!visionDeps) {
31022
+ visionDeps = { ...deps, engine: visionEngine, ...visionMeta };
31023
+ console.log(`[vision] deps built: ${visionMeta.providerId}/${visionMeta.model}`);
31024
+ }
31025
+ return visionDeps;
30653
31026
  }
30654
31027
  let modelOverride = null;
30655
31028
  let modelOverrideEngine = null;
30656
31029
  let visionOverride = null;
30657
- const defaultVisionDeps = visionDeps;
30658
31030
  const modelDepsCache = /* @__PURE__ */ new Map();
30659
31031
  deps.invalidateDeskBuddyDeps = () => {
30660
31032
  deskBuddyDeps = null;
@@ -30680,6 +31052,7 @@ ${content}`
30680
31052
  if (!p.provider) {
30681
31053
  visionEngine = null;
30682
31054
  visionDeps = null;
31055
+ visionMeta = null;
30683
31056
  return;
30684
31057
  }
30685
31058
  if (visionEngine) {
@@ -30688,39 +31061,22 @@ ${content}`
30688
31061
  } else {
30689
31062
  visionEngine = new QueryEngine(p.provider, {
30690
31063
  model: p.model,
30691
- systemPrompt,
30692
- systemStable,
30693
- compactConfig,
31064
+ systemPrompt: getSystemPrompt,
31065
+ systemStable: getSystemStable,
31066
+ compactConfig: getCompactConfig,
30694
31067
  maxTokens: 4096,
30695
31068
  temperature: 0.7,
30696
31069
  maxTurns: config2.profile.maxTurns,
30697
31070
  agentLabel: "main"
30698
31071
  });
30699
31072
  }
30700
- if (visionDeps) {
30701
- visionDeps.engine = visionEngine;
30702
- visionDeps.providerId = p.providerId;
30703
- visionDeps.providerApi = p.providerApi;
30704
- visionDeps.model = p.model;
30705
- visionDeps.modelInputs = p.modelInputs;
30706
- } else {
30707
- visionDeps = {
30708
- engine: visionEngine,
30709
- sessions,
30710
- channelManager,
30711
- workspace: config2.workspace,
30712
- providerId: p.providerId,
30713
- providerApi: p.providerApi,
30714
- model: p.model,
30715
- modelInputs: p.modelInputs,
30716
- systemPrompt,
30717
- channels: config2.channels,
30718
- config: config2,
30719
- recallProvider: memoryRecallProvider || void 0,
30720
- extractProvider: memoryExtractProvider || void 0,
30721
- everosCfg: deps?.everosCfg
30722
- };
30723
- }
31073
+ visionMeta = {
31074
+ providerId: p.providerId,
31075
+ providerApi: p.providerApi,
31076
+ model: p.model,
31077
+ modelInputs: p.modelInputs
31078
+ };
31079
+ visionDeps = null;
30724
31080
  };
30725
31081
  function createModelDeps(ref) {
30726
31082
  const slashIdx = ref.indexOf("/");
@@ -30740,28 +31096,21 @@ ${content}`
30740
31096
  const llmProvider = providerId === config2.provider.id ? provider : createProvider(providerCfg);
30741
31097
  const engine2 = new QueryEngine(llmProvider, {
30742
31098
  model: modelId,
30743
- systemPrompt,
30744
- systemStable,
30745
- compactConfig,
30746
- maxTokens: modelDef2.maxTokens || 4096,
31099
+ systemPrompt: getSystemPrompt,
31100
+ systemStable: getSystemStable,
31101
+ compactConfig: getCompactConfig,
31102
+ maxTokens: () => resolveModelMaxTokens(`${providerId}/${modelId}`),
30747
31103
  temperature: 0.7,
30748
31104
  maxTurns: config2.profile.maxTurns,
30749
31105
  agentLabel: `main:${ref}`
30750
31106
  });
30751
31107
  return {
31108
+ ...deps,
30752
31109
  engine: engine2,
30753
- sessions,
30754
- channelManager,
30755
- workspace: config2.workspace,
30756
31110
  providerId,
30757
31111
  providerApi: providerCfg.api,
30758
31112
  model: modelId,
30759
- modelInputs: modelDef2.input || ["text"],
30760
- systemPrompt,
30761
- channels: config2.channels,
30762
- recallProvider: memoryRecallProvider || void 0,
30763
- extractProvider: memoryExtractProvider || void 0,
30764
- everosCfg: deps?.everosCfg
31113
+ modelInputs: modelDef2.input || ["text"]
30765
31114
  };
30766
31115
  }
30767
31116
  const dispatcher = new MessageDispatcher();
@@ -30938,7 +31287,17 @@ ${content}`
30938
31287
  console.log(`[cron] config check: enabled=${config2.cron?.enabled}, hasConfig=${!!config2.cron}`);
30939
31288
  if (config2.cron?.enabled) {
30940
31289
  const { CronPlugin: CronPlugin2 } = await Promise.resolve().then(() => (init_cron_plugin(), cron_plugin_exports));
30941
- const cronPlugin = new CronPlugin2(config2.cron, sessions, channelManager, deps, config2.stateDir, dispatcher);
31290
+ const resolveCronModelDeps = (ref) => {
31291
+ const key = `cron:${ref}`;
31292
+ const cached = modelDepsCache.get(key);
31293
+ if (cached) return cached;
31294
+ const built = createModelDeps(ref);
31295
+ if (!built) return null;
31296
+ modelDepsCache.set(key, built);
31297
+ console.log(`[cron] Model deps built: ${ref}`);
31298
+ return built;
31299
+ };
31300
+ const cronPlugin = new CronPlugin2(config2.cron, sessions, channelManager, deps, config2.stateDir, dispatcher, resolveCronModelDeps);
30942
31301
  await cronPlugin.start();
30943
31302
  const { setCronConfig: setCronConfig2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
30944
31303
  setCronConfig2(config2.cron);
@@ -31114,8 +31473,10 @@ ${notifications}
31114
31473
  dispatcher,
31115
31474
  runningQueries,
31116
31475
  engine,
31117
- systemPrompt,
31118
- compactConfig,
31476
+ systemPrompt: getSystemPrompt,
31477
+ // 0902 函数形态:命令按调用时现取
31478
+ compactConfig: getCompactConfig,
31479
+ // 0902 同上
31119
31480
  provider,
31120
31481
  deps,
31121
31482
  getVisualRegistry: () => visualRegistry,
@@ -31137,9 +31498,17 @@ ${notifications}
31137
31498
  setVisionDeps: (v) => {
31138
31499
  visionDeps = v;
31139
31500
  },
31140
- getDefaultVisionDeps: () => defaultVisionDeps,
31501
+ // reset 用:清 override deps,下一条图片消息由 resolveVisionDeps 按当前 meta 重建
31502
+ getDefaultVisionDeps: () => {
31503
+ visionDeps = null;
31504
+ return null;
31505
+ },
31141
31506
  doReloadConfig
31142
31507
  };
31508
+ deps.onProviderSwapped = (p) => {
31509
+ provider = p;
31510
+ commandDeps.provider = p;
31511
+ };
31143
31512
  const slashCommands = listCommandDefs().map((d) => ({
31144
31513
  name: d.name,
31145
31514
  description: d.description,
@@ -31396,7 +31765,7 @@ ${pathStr}` }];
31396
31765
  for (const att of nonImageAttachments) {
31397
31766
  console.log(`[file] Downloading: ${att.filename} (${att.contentType}, ${att.size}B)`);
31398
31767
  try {
31399
- const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher }) : await fetch(att.url);
31768
+ const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher, signal: AbortSignal.timeout(3e4) }) : await fetch(att.url, { signal: AbortSignal.timeout(3e4) });
31400
31769
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
31401
31770
  const buffer = Buffer.from(await resp.arrayBuffer());
31402
31771
  const safeName2 = path49.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
@@ -31426,7 +31795,8 @@ ${pathStr}` }];
31426
31795
  }
31427
31796
  const isImageBlock = (b) => b.type === "image" || b.type === "image_url";
31428
31797
  const hasImages = Array.isArray(queryContent) && queryContent.some((b) => isImageBlock(b));
31429
- let msgDeps = hasImages && visionDeps ? visionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31798
+ const activeVisionDeps = hasImages ? resolveVisionDeps() : null;
31799
+ let msgDeps = activeVisionDeps ? activeVisionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31430
31800
  if (isDeskBuddy && !hasImages && !modelOverride) {
31431
31801
  if (!deskBuddyDeps) {
31432
31802
  const dbRef = liveConfig.get("channels.deskBuddy.model") || "deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731";
@@ -31454,7 +31824,8 @@ ${pathStr}` }];
31454
31824
  model: dbProviderCfg ? dbModelId : config2.model,
31455
31825
  // 8/18 翀哥:完整工程 prompt(14k tok)+memory(10k) 会把小模型带偏成"工程助手"——deskBuddy 用 SOUL 精简人设
31456
31826
  // 8/21 复用 light-mode.buildLightStablePrompt(和情感模式同一构建器)
31457
- systemPrompt: buildLightStablePrompt(config2.workspace, "deskBuddy"),
31827
+ systemPrompt: () => buildLightStablePrompt(config2.workspace, "deskBuddy"),
31828
+ // 0902 函数形态:改 SOUL 热生效
31458
31829
  maxTokens: 1024,
31459
31830
  temperature: 0.7,
31460
31831
  disableThinking: true,
@@ -31468,7 +31839,7 @@ ${pathStr}` }];
31468
31839
  msgDeps = deskBuddyDeps;
31469
31840
  }
31470
31841
  if (hasImages) {
31471
- 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"}`);
31842
+ 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"}`);
31472
31843
  if (Array.isArray(queryContent)) {
31473
31844
  queryContent.forEach((b, i) => {
31474
31845
  if (b.type === "image" && b.source?.data) {
@@ -31477,13 +31848,13 @@ ${pathStr}` }];
31477
31848
  });
31478
31849
  }
31479
31850
  }
31480
- if (hasImages && visionDeps) {
31481
- console.log(`[vision] Routing to ${visionDeps.providerId}/${visionDeps.model}${visionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31851
+ if (hasImages && activeVisionDeps) {
31852
+ console.log(`[vision] Routing to ${activeVisionDeps.providerId}/${activeVisionDeps.model}${activeVisionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31482
31853
  }
31483
31854
  const preQueryResult = await messageHooks.runPreQuery({
31484
31855
  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 },
31485
31856
  text: queryContent,
31486
- msgDeps: hasImages && visionDeps ? visionDeps : msgDeps,
31857
+ msgDeps: activeVisionDeps ?? msgDeps,
31487
31858
  deps: { provider, channelManager, sessions, dispatcher, config: config2, workspace: config2.workspace }
31488
31859
  });
31489
31860
  if (preQueryResult.skip) {
@@ -31496,6 +31867,7 @@ ${pathStr}` }];
31496
31867
  }
31497
31868
  queryContent = preQueryResult.text ?? queryContent;
31498
31869
  if (preQueryResult.msgDeps) msgDeps = preQueryResult.msgDeps;
31870
+ msgDeps.renderer = renderer;
31499
31871
  const accepted = dispatcher.submitMessage({
31500
31872
  text: queryContent,
31501
31873
  sessionId,
@@ -31730,8 +32102,9 @@ ${pathStr}` }];
31730
32102
  sessionManager: sessions,
31731
32103
  sessionId,
31732
32104
  model: config2.model,
31733
- contextWindow: compactConfig.contextWindow || 2e5,
31734
- systemPrompt,
32105
+ contextWindow: getCompactConfig().contextWindow || 2e5,
32106
+ systemPrompt: getSystemPrompt(),
32107
+ // 0902 现取(API /context 报告跟当前 prompt 一致)
31735
32108
  toolDefs: registry.definitions(),
31736
32109
  workspace: config2.workspace
31737
32110
  });
@@ -32059,6 +32432,24 @@ async function doReloadConfig(config2, deps, provider) {
32059
32432
  visionModel: newConfig.visionModel,
32060
32433
  visionFallbacks: newConfig.visionFallbacks
32061
32434
  });
32435
+ const newRecall = createMemorySideProvider(
32436
+ newConfig.topics?.recall,
32437
+ provider,
32438
+ newConfig.providers || {}
32439
+ );
32440
+ const newExtract = createMemorySideProvider(
32441
+ newConfig.topics?.extract,
32442
+ provider,
32443
+ newConfig.providers || {}
32444
+ );
32445
+ if (newRecall) {
32446
+ deps.recallProvider = newRecall;
32447
+ changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32448
+ }
32449
+ if (newExtract) {
32450
+ deps.extractProvider = newExtract;
32451
+ changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32452
+ }
32062
32453
  if (oldProviderKey !== newProviderKey) {
32063
32454
  console.log("[reload] Provider structure changed, rebuilding chain...");
32064
32455
  const rebuilt = buildProviderChain(newConfig);
@@ -32069,6 +32460,7 @@ async function doReloadConfig(config2, deps, provider) {
32069
32460
  }
32070
32461
  changes.push(`provider chain rebuilt (${rebuilt.visionChainLabels.length > 0 ? "vision: " + rebuilt.visionChainLabels.join("\u2192") : "no vision"})`);
32071
32462
  }
32463
+ if (deps.onProviderSwapped) deps.onProviderSwapped(rebuilt.provider);
32072
32464
  if (typeof deps.setVisionProvider === "function") {
32073
32465
  const vCfg = newConfig.visionModel;
32074
32466
  const vpCfg = vCfg ? newConfig.providers?.[vCfg.providerId] : void 0;
@@ -32099,24 +32491,6 @@ async function doReloadConfig(config2, deps, provider) {
32099
32491
  changes.push(`deskBuddy model \u2192 ${newDbModel}`);
32100
32492
  }
32101
32493
  }
32102
- const newRecall = createMemorySideProvider(
32103
- newConfig.topics?.recall,
32104
- provider,
32105
- newConfig.providers || {}
32106
- );
32107
- const newExtract = createMemorySideProvider(
32108
- newConfig.topics?.extract,
32109
- provider,
32110
- newConfig.providers || {}
32111
- );
32112
- if (newRecall) {
32113
- deps.recallProvider = newRecall;
32114
- changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32115
- }
32116
- if (newExtract) {
32117
- deps.extractProvider = newExtract;
32118
- changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32119
- }
32120
32494
  try {
32121
32495
  const { setAutoDreamConfig: setAutoDreamConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
32122
32496
  setAutoDreamConfig2(newConfig);
@@ -32312,11 +32686,11 @@ function startSecretsWatcher(config2, deps, provider) {
32312
32686
  }
32313
32687
 
32314
32688
  // src/main.ts
32315
- import { readFileSync as readFileSync31 } from "node:fs";
32689
+ import { readFileSync as readFileSync32 } from "node:fs";
32316
32690
  import { fileURLToPath as fileURLToPath3 } from "node:url";
32317
- import { dirname as dirname9, join as join44 } from "node:path";
32691
+ import { dirname as dirname9, join as join45 } from "node:path";
32318
32692
  var __dirname2 = dirname9(fileURLToPath3(import.meta.url));
32319
- var pkg = JSON.parse(readFileSync31(join44(__dirname2, "..", "package.json"), "utf-8"));
32693
+ var pkg = JSON.parse(readFileSync32(join45(__dirname2, "..", "package.json"), "utf-8"));
32320
32694
  var epipeSeen = false;
32321
32695
  process.on("uncaughtException", (err) => {
32322
32696
  const code = err?.code ?? "";