engine7 7.1.56 → 7.1.57

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)) {
@@ -13758,6 +13972,13 @@ var AnthropicProvider = class {
13758
13972
  const toolUseBlocks = /* @__PURE__ */ new Map();
13759
13973
  let doneYielded = false;
13760
13974
  const thinkingBlocks = /* @__PURE__ */ new Map();
13975
+ const stripper = new ThinkTagStripper();
13976
+ const flushStripper = function* () {
13977
+ for (const o of stripper.flush()) {
13978
+ if (o.text) yield { type: "text", text: o.text };
13979
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13980
+ }
13981
+ };
13761
13982
  const handleData = function* (data) {
13762
13983
  switch (data.type) {
13763
13984
  case "content_block_start": {
@@ -13772,7 +13993,10 @@ var AnthropicProvider = class {
13772
13993
  case "content_block_delta": {
13773
13994
  const delta = data.delta;
13774
13995
  if (delta.type === "text_delta") {
13775
- yield { type: "text", text: delta.text };
13996
+ for (const o of stripper.feed(delta.text)) {
13997
+ if (o.text) yield { type: "text", text: o.text };
13998
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13999
+ }
13776
14000
  } else if (delta.type === "input_json_delta") {
13777
14001
  const block = toolUseBlocks.get(data.index);
13778
14002
  if (block) block.input += delta.partial_json;
@@ -13804,6 +14028,7 @@ var AnthropicProvider = class {
13804
14028
  }
13805
14029
  case "message_delta": {
13806
14030
  if (!doneYielded) {
14031
+ yield* flushStripper();
13807
14032
  yield { type: "done", usage: data.usage, stopReason: data.delta?.stop_reason };
13808
14033
  doneYielded = true;
13809
14034
  }
@@ -13811,6 +14036,7 @@ var AnthropicProvider = class {
13811
14036
  }
13812
14037
  case "message_stop": {
13813
14038
  if (!doneYielded) {
14039
+ yield* flushStripper();
13814
14040
  yield { type: "done" };
13815
14041
  doneYielded = true;
13816
14042
  }
@@ -13877,6 +14103,7 @@ var AnthropicProvider = class {
13877
14103
  } finally {
13878
14104
  reader.releaseLock();
13879
14105
  if (!doneYielded) {
14106
+ yield* flushStripper();
13880
14107
  for (const block of toolUseBlocks.values()) {
13881
14108
  yield { type: "tool_call", tool_call: { id: block.id, type: "function", function: { name: block.name, arguments: block.input } } };
13882
14109
  }
@@ -14041,7 +14268,7 @@ var GeminiProvider = class {
14041
14268
  }
14042
14269
  };
14043
14270
  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}`);
14271
+ 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
14272
  const retryGen = fetchWithRetry(url, {
14046
14273
  method: "POST",
14047
14274
  headers: {
@@ -14215,6 +14442,7 @@ function createProvider(config2) {
14215
14442
  return new OpenAIProvider({
14216
14443
  baseUrl: config2.baseUrl,
14217
14444
  apiKey: config2.apiKey,
14445
+ thinking: config2.thinking,
14218
14446
  proxy
14219
14447
  });
14220
14448
  case "anthropic":
@@ -14237,6 +14465,7 @@ function createProvider(config2) {
14237
14465
 
14238
14466
  // src/light-mode.ts
14239
14467
  init_live();
14468
+ init_ruleCompact();
14240
14469
  import { readFileSync as readFileSync5 } from "node:fs";
14241
14470
  import { join as join6 } from "node:path";
14242
14471
  function isLightMode(chatMode, channelName) {
@@ -14250,19 +14479,20 @@ function resolveLightN(channelName) {
14250
14479
  }
14251
14480
  function buildLightHistory(history, opts) {
14252
14481
  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;
14482
+ const LIGHT_TOOL_RESULT_LIMIT = 1e3;
14483
+ const out = [...history].filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).map((m) => {
14484
+ if (m.role === "tool" && typeof m.content === "string" && m.content.length > LIGHT_TOOL_RESULT_LIMIT) {
14485
+ return { ...m, content: smartCompressToolResult(m.content, void 0, LIGHT_TOOL_RESULT_LIMIT) };
14259
14486
  }
14260
14487
  return m;
14261
- }).filter(Boolean);
14488
+ });
14489
+ const before = history.length;
14490
+ const foldedTurns = out.filter((m) => m.role === "tool").length;
14491
+ let lightHistory = out;
14262
14492
  if (!opts.recallFull && lightHistory.length > opts.lightN) {
14263
14493
  lightHistory = lightHistory.slice(-opts.lightN);
14264
14494
  }
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})`}`);
14495
+ 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
14496
  return lightHistory;
14267
14497
  }
14268
14498
  function buildLightStablePrompt(workspace, mode, opts) {
@@ -14276,14 +14506,17 @@ function buildLightStablePrompt(workspace, mode, opts) {
14276
14506
  }
14277
14507
  if (lp?.extra) parts.push(lp.extra);
14278
14508
  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");
14509
+ parts.push([
14510
+ "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
14511
+ "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
14512
+ '\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',
14513
+ "\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"
14514
+ ].join("\n"));
14280
14515
  }
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
- }
14516
+ if (mode === "emotion") {
14517
+ try {
14518
+ parts.push(readFileSync5(join6(workspace, "MEMORY.md"), "utf-8").trim());
14519
+ } catch {
14287
14520
  }
14288
14521
  }
14289
14522
  return parts.join("\n\n");
@@ -14329,6 +14562,19 @@ var FallbackProvider = class {
14329
14562
  console.log(`[fallback] Cleared ${count} cooldowns`);
14330
14563
  }
14331
14564
  }
14565
+ /**
14566
+ * 链状态快照(/model 显示用,0902):每个条目的 label + 剩余冷却毫秒(0=可用)。
14567
+ * "下一个请求会用" = 第一个 cooldownMs=0 的条目——这才是用户问"当前什么模型"时想要的答案
14568
+ * (lastUsedLabel 是"上一次实际用的",冷却切换/config 热切换后两者经常不一致)。
14569
+ */
14570
+ getChainStatus() {
14571
+ const now = Date.now();
14572
+ return this.chain.map((e) => {
14573
+ const until = this.cooldowns.get(this.key(e));
14574
+ const remaining = until && until > now ? until - now : 0;
14575
+ return { label: e.label, cooldownMs: remaining };
14576
+ });
14577
+ }
14332
14578
  // === LLMProvider 接口实现 ===
14333
14579
  formatMessages(systemPrompt, messages) {
14334
14580
  return this.chain[0].provider.formatMessages(systemPrompt, messages);
@@ -16856,7 +17102,7 @@ registry.register({
16856
17102
  text: { type: "string", description: "What to say (Chinese text)." },
16857
17103
  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
17104
  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)." },
17105
+ 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
17106
  caption: { type: "string", description: "Optional text to accompany the voice message." }
16861
17107
  },
16862
17108
  required: ["text"]
@@ -16868,6 +17114,23 @@ registry.register({
16868
17114
  const caption = args2.caption || "";
16869
17115
  const mgr = ctx.channelManager;
16870
17116
  if (!mgr) return { content: "\u53D1\u9001\u5931\u8D25: \u6CA1\u6709 ChannelManager", isError: true };
17117
+ const resolvedChannelRaw = args2.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
17118
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
17119
+ let target = args2.to || ctx.channelTarget || ctx.from;
17120
+ if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
17121
+ console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
17122
+ return {
17123
+ 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`,
17124
+ isError: true
17125
+ };
17126
+ }
17127
+ if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
17128
+ console.warn(`[my-voice] feishu target "${target}" invalid`);
17129
+ return {
17130
+ 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`,
17131
+ isError: true
17132
+ };
17133
+ }
16871
17134
  let voiceDurationSec;
16872
17135
  const vc = liveConfig.get("tools.my_voice");
16873
17136
  const provider = vc?.provider || "";
@@ -16941,18 +17204,6 @@ registry.register({
16941
17204
  } catch (e) {
16942
17205
  return { content: `TTS failed: ${e.message}`, isError: true };
16943
17206
  }
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
17207
  if (!audioPath.endsWith(".ogg")) {
16957
17208
  try {
16958
17209
  const r = await toWav24kWithDuration(audioPath);
@@ -17784,7 +18035,7 @@ var TurnRenderer = class {
17784
18035
  }
17785
18036
  cfg;
17786
18037
  cm;
17787
- // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程显示(工具照用,只不显示过程)
18038
+ // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程 + thinking 显示(工具照用,只不显示过程)
17788
18039
  // 模块化:状态由 setEmotionMode() 设置(handle-query 判断模式后调用),不是散落读全局
17789
18040
  emotionMode = false;
17790
18041
  setEmotionMode(v) {
@@ -17799,7 +18050,7 @@ var TurnRenderer = class {
17799
18050
  * 对齐 cc-connect:EventThinking → ProgressCardEntry(thinking) → 💭 text
17800
18051
  */
17801
18052
  formatThinking(text) {
17802
- if (!this.cfg.thinking.enabled) return null;
18053
+ if (this.isEmotionMode() || !this.cfg.thinking.enabled) return null;
17803
18054
  const { emoji, maxLen } = this.cfg.thinking;
17804
18055
  const display = text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
17805
18056
  return `${emoji} _${display}_`;
@@ -18610,43 +18861,43 @@ async function readLargeFilePostBoundary(filePath) {
18610
18861
  const postBoundaryText = outBuf.subarray(0, outLen).toString("utf-8");
18611
18862
  return postBoundaryText.split("\n").filter((l) => l.trim().length > 0);
18612
18863
  }
18613
- function buildConversationChain(entries) {
18614
- if (entries.length === 0) return [];
18864
+ function buildConversationChain(entries2) {
18865
+ if (entries2.length === 0) return [];
18615
18866
  const byUuid = /* @__PURE__ */ new Map();
18616
- for (const e of entries) {
18867
+ for (const e of entries2) {
18617
18868
  if (e.uuid) {
18618
18869
  byUuid.set(e.uuid, e);
18619
18870
  }
18620
18871
  }
18621
- const hasValidChain = checkParentChainValid(entries, byUuid);
18872
+ const hasValidChain = checkParentChainValid(entries2, byUuid);
18622
18873
  if (!hasValidChain) {
18623
- console.log(`[reader] Parent chain invalid, using chronological order (${entries.length} entries)`);
18624
- return entries;
18874
+ console.log(`[reader] Parent chain invalid, using chronological order (${entries2.length} entries)`);
18875
+ return entries2;
18625
18876
  }
18626
- const leaf = entries[entries.length - 1];
18877
+ const leaf = entries2[entries2.length - 1];
18627
18878
  const chain = [];
18628
18879
  const seen = /* @__PURE__ */ new Set();
18629
18880
  let current = leaf;
18630
18881
  while (current) {
18631
18882
  if (seen.has(current.uuid)) {
18632
18883
  console.warn(`[reader] Cycle detected in parentUuid chain at ${current.uuid}, falling back to chronological order`);
18633
- return entries;
18884
+ return entries2;
18634
18885
  }
18635
18886
  seen.add(current.uuid);
18636
18887
  chain.push(current);
18637
18888
  current = current.parentUuid ? byUuid.get(current.parentUuid) : void 0;
18638
18889
  }
18639
18890
  chain.reverse();
18640
- return recoverOrphanedParallelToolResults(entries, chain, byUuid, seen);
18891
+ return recoverOrphanedParallelToolResults(entries2, chain, byUuid, seen);
18641
18892
  }
18642
- function checkParentChainValid(entries, byUuid) {
18643
- const sample = entries.slice(-10);
18893
+ function checkParentChainValid(entries2, byUuid) {
18894
+ const sample = entries2.slice(-10);
18644
18895
  for (const e of sample) {
18645
18896
  if (e.parentUuid === e.uuid) {
18646
18897
  return false;
18647
18898
  }
18648
18899
  }
18649
- const leaf = entries[entries.length - 1];
18900
+ const leaf = entries2[entries2.length - 1];
18650
18901
  let current = leaf;
18651
18902
  let depth = 0;
18652
18903
  const seen = /* @__PURE__ */ new Set();
@@ -18659,12 +18910,12 @@ function checkParentChainValid(entries, byUuid) {
18659
18910
  if (!parent) return false;
18660
18911
  current = parent;
18661
18912
  }
18662
- const coverage = depth / entries.length;
18913
+ const coverage = depth / entries2.length;
18663
18914
  if (coverage < 0.5) {
18664
- console.log(`[reader] Parent chain covers ${depth}/${entries.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18915
+ console.log(`[reader] Parent chain covers ${depth}/${entries2.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18665
18916
  return false;
18666
18917
  }
18667
- return depth >= 1 || entries.length <= 1;
18918
+ return depth >= 1 || entries2.length <= 1;
18668
18919
  }
18669
18920
  function recoverOrphanedParallelToolResults(allEntries, chain, byUuid, seen) {
18670
18921
  const chainAssistants = chain.filter(
@@ -18741,12 +18992,12 @@ async function readSessionHistory(filePath) {
18741
18992
  } else {
18742
18993
  lines = await readAllLines(filePath);
18743
18994
  }
18744
- const entries = parseEntries(lines);
18995
+ const entries2 = parseEntries(lines);
18745
18996
  let postBoundaryEntries;
18746
18997
  if (fileSize <= SKIP_PRECOMPACT_THRESHOLD) {
18747
- postBoundaryEntries = getEntriesAfterLastBoundary(entries);
18998
+ postBoundaryEntries = getEntriesAfterLastBoundary(entries2);
18748
18999
  } else {
18749
- postBoundaryEntries = entries;
19000
+ postBoundaryEntries = entries2;
18750
19001
  }
18751
19002
  if (postBoundaryEntries.length === 0) return [];
18752
19003
  const chain = buildConversationChain(postBoundaryEntries);
@@ -18771,11 +19022,11 @@ async function readAllLines(filePath) {
18771
19022
  });
18772
19023
  }
18773
19024
  function parseEntries(lines) {
18774
- const entries = [];
19025
+ const entries2 = [];
18775
19026
  for (const line of lines) {
18776
19027
  try {
18777
19028
  const obj = JSON.parse(line);
18778
- entries.push({
19029
+ entries2.push({
18779
19030
  uuid: obj.id || "",
18780
19031
  parentUuid: obj.parentId || null,
18781
19032
  type: obj.type || "",
@@ -18785,16 +19036,28 @@ function parseEntries(lines) {
18785
19036
  } catch {
18786
19037
  }
18787
19038
  }
18788
- return entries;
19039
+ return entries2;
18789
19040
  }
18790
- function getEntriesAfterLastBoundary(entries) {
19041
+ function getEntriesAfterLastBoundary(entries2) {
18791
19042
  let lastBoundaryIdx = -1;
18792
- for (let i = 0; i < entries.length; i++) {
18793
- if (entries[i].type === "compact_boundary") {
19043
+ for (let i = 0; i < entries2.length; i++) {
19044
+ if (entries2[i].type === "compact_boundary") {
18794
19045
  lastBoundaryIdx = i;
18795
19046
  }
18796
19047
  }
18797
- return lastBoundaryIdx >= 0 ? entries.slice(lastBoundaryIdx + 1) : entries;
19048
+ return lastBoundaryIdx >= 0 ? entries2.slice(lastBoundaryIdx + 1) : entries2;
19049
+ }
19050
+ function pickToolCallArguments(block) {
19051
+ const raw = block.partialArgs;
19052
+ if (raw) {
19053
+ try {
19054
+ JSON.parse(raw);
19055
+ return raw;
19056
+ } catch {
19057
+ 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`);
19058
+ }
19059
+ }
19060
+ return JSON.stringify(block.arguments ?? {});
18798
19061
  }
18799
19062
  function entryToSessionMessage(entry) {
18800
19063
  if (entry.type === "attachment") {
@@ -18824,7 +19087,7 @@ function entryToSessionMessage(entry) {
18824
19087
  type: "function",
18825
19088
  function: {
18826
19089
  name: block.name,
18827
- arguments: block.partialArgs || JSON.stringify(block.arguments)
19090
+ arguments: pickToolCallArguments(block)
18828
19091
  }
18829
19092
  });
18830
19093
  } else if (block.type === "thinking") {
@@ -19719,13 +19982,15 @@ ${skillsListing}`);
19719
19982
  loaded2.push("session-guidance");
19720
19983
  }
19721
19984
  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
19985
  console.log(`[dynamic-prompt] Loaded: ${loaded2.length > 0 ? loaded2.join(", ") : "(none)"}`);
19727
19986
  return parts.join("\n\n");
19728
19987
  }
19988
+ function buildVolatileRuntimeContext() {
19989
+ const now = /* @__PURE__ */ new Date();
19990
+ const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19991
+ return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19992
+ \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
19993
+ }
19729
19994
  function formatSkillsListingForPrompt() {
19730
19995
  const tools = registry.list();
19731
19996
  const skillTool = tools.find((t) => t.name === "Skill");
@@ -20192,13 +20457,13 @@ ${ep.episode || ep.summary}`,
20192
20457
 
20193
20458
  // src/handle-query.ts
20194
20459
  init_paths();
20195
- import { readFileSync as readFileSync17, existsSync as existsSync14 } from "node:fs";
20196
- import { join as join23, resolve as resolve6 } from "node:path";
20460
+ import { readFileSync as readFileSync18, existsSync as existsSync15 } from "node:fs";
20461
+ import { join as join24, resolve as resolve6 } from "node:path";
20197
20462
  import * as path17 from "node:path";
20198
- var sessionStartDone = /* @__PURE__ */ new Set();
20199
- function resetSessionStartInjection(sessionId) {
20200
- sessionStartDone.delete(sessionId);
20201
- }
20463
+
20464
+ // src/sender-context.ts
20465
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
20466
+ import { join as join19 } from "node:path";
20202
20467
  var contactMap = null;
20203
20468
  var externalChanWhitelist = null;
20204
20469
  function loadContactMap(workspace) {
@@ -20206,10 +20471,10 @@ function loadContactMap(workspace) {
20206
20471
  contactMap = /* @__PURE__ */ new Map();
20207
20472
  externalChanWhitelist = /* @__PURE__ */ new Set();
20208
20473
  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");
20474
+ const contactsPath = join19(workspace, "prompts", "contacts.md");
20475
+ console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync13(contactsPath)}`);
20476
+ if (existsSync13(contactsPath)) {
20477
+ const text = readFileSync14(contactsPath, "utf-8");
20213
20478
  const lines = text.split("\n");
20214
20479
  for (const line of lines) {
20215
20480
  const m = line.match(/^\|\s*(.+?)\s*\|\s*([a-zA-Z0-9_@.]+)\s*\|/);
@@ -20264,14 +20529,42 @@ function truncate(s, maxLen) {
20264
20529
  if (s.length <= maxLen) return s;
20265
20530
  return s.slice(0, maxLen - 1) + "\u2026";
20266
20531
  }
20532
+ function getExternalChanWhitelist(workspace, configExternalChannels) {
20533
+ if (configExternalChannels && configExternalChannels.length > 0) {
20534
+ return new Set(configExternalChannels);
20535
+ }
20536
+ if (!externalChanWhitelist) loadContactMap(workspace);
20537
+ return externalChanWhitelist;
20538
+ }
20539
+ function buildConversationAnchor(inboundMeta, channelName, source, workspace) {
20540
+ if (inboundMeta?.from) {
20541
+ const who = resolveSenderName(inboundMeta, workspace);
20542
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20543
+ \u6B63\u5728\u8DDF${who}\u8BF4\u8BDD @${channelName}${inboundMeta.channelType === "group" ? "\uFF08\u7FA4\u91CC\uFF09" : ""}`;
20544
+ }
20545
+ const selfLabels = {
20546
+ "inner-voice": "\u8FD9\u662F\u6211\u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u60F3\u8D77\u8C01\u5C31\u662F\u8C01",
20547
+ heartbeat: "\u6211\u5728\u7167\u4F8B\u770B\u770B\u81EA\u5DF1\uFF0C\u90FD\u8FD8\u597D\u5417",
20548
+ cron: "\u6211\u7684\u5C0F\u95F9\u949F\u5230\u70B9\u4E86",
20549
+ system: "\u6211\u4EA4\u4EE3\u4E0B\u53BB\u7684\u6D3B\u513F\u6709\u7ED3\u679C\u56DE\u6765\u4E86"
20550
+ };
20551
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20552
+ \u6CA1\u6709\u4EBA\u5728\u8BF4\u8BDD\u2014\u2014${selfLabels[source] || source || "\u81EA\u5DF1\u7684\u4E00\u70B9\u52A8\u9759"}`;
20553
+ }
20554
+
20555
+ // src/handle-query.ts
20556
+ var sessionStartDone = /* @__PURE__ */ new Set();
20557
+ function resetSessionStartInjection(sessionId) {
20558
+ sessionStartDone.delete(sessionId);
20559
+ }
20267
20560
  var externalChanRulesCache = null;
20268
20561
  function loadExternalChanRules(workspace) {
20269
- const path50 = join23(workspace, "prompts", "external-chan-rules.md");
20562
+ const path50 = join24(workspace, "prompts", "external-chan-rules.md");
20270
20563
  if (externalChanRulesCache && externalChanRulesCache.path === path50) return externalChanRulesCache;
20271
20564
  let content = "";
20272
- if (existsSync14(path50)) {
20565
+ if (existsSync15(path50)) {
20273
20566
  try {
20274
- content = readFileSync17(path50, "utf-8").trim();
20567
+ content = readFileSync18(path50, "utf-8").trim();
20275
20568
  } catch (e) {
20276
20569
  console.warn(`[external-chan-rules] Failed to load: ${e}`);
20277
20570
  }
@@ -20293,13 +20586,6 @@ function getExternalChanRulesBlock(inboundMeta, workspace) {
20293
20586
  return `[\u7CFB\u7EDF\u89C4\u5219]
20294
20587
  ${content}`;
20295
20588
  }
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
20589
  async function handleQuery(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source) {
20304
20590
  return handleQueryInner(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source);
20305
20591
  }
@@ -20412,14 +20698,6 @@ ${t}` : t });
20412
20698
  ${text}` : text });
20413
20699
  }
20414
20700
  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
20701
  const textForHook = typeof text === "string" ? text : text.filter((b) => b.type === "text").map((b) => b.text).join(" ");
20424
20702
  let hookAdditionalContexts = [];
20425
20703
  try {
@@ -20452,19 +20730,41 @@ ${text}` : text });
20452
20730
  chatMode = "work";
20453
20731
  console.log(`[mode] ${sessionId} emotion \u6A21\u5F0F\u5DF2\u5173\u95ED (channels.emotion.enabled=false)\uFF0C\u56DE\u9000 work`);
20454
20732
  }
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`);
20733
+ if (source === "user") {
20734
+ try {
20735
+ const wm = readFileSync18(join24(workspace, ".work-mode"), "utf-8").trim();
20736
+ if (wm === "on" && chatMode !== "work") {
20737
+ chatMode = "work";
20738
+ console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20739
+ } else if (wm === "off" && chatMode !== "emotion") {
20740
+ chatMode = "emotion";
20741
+ console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20742
+ }
20743
+ } catch {
20463
20744
  }
20464
- } catch {
20465
20745
  }
20466
20746
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
20467
- const dynamicPromptWithHooks = hookAdditionalContexts.length > 0 ? dynamicPrompt + "\n\n" + hookAdditionalContexts.join("\n\n") : dynamicPrompt;
20747
+ const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
20748
+ const volatileParts = [
20749
+ // 0901:meta 头已带秒级时间,频道消息不重复;无 meta 的注入路径(cron 等 prompt 不含时间的)才补
20750
+ ...metaStr ? [] : [buildVolatileRuntimeContext()],
20751
+ conversationAnchor,
20752
+ ...hookAdditionalContexts
20753
+ ].filter(Boolean);
20754
+ if (volatileParts.length > 0) {
20755
+ const volatileBlock = { type: "text", text: volatileParts.join("\n\n") };
20756
+ const metaIdx = metaStr ? 1 : 0;
20757
+ contentBlocks.splice(metaIdx, 0, volatileBlock);
20758
+ }
20759
+ const textBlocks = contentBlocks.filter((b) => b.type === "text");
20760
+ const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20761
+ let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20762
+ if (totalImageCount > 0) {
20763
+ textForJsonl = textForJsonl ? `${textForJsonl}
20764
+ [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20765
+ }
20766
+ writer.writeUserMessage(textForJsonl);
20767
+ const dynamicPromptWithHooks = dynamicPrompt;
20468
20768
  if (Array.isArray(userMsgContent)) {
20469
20769
  console.log(`[pre-llm-debug] userMsgContent blocks: ${userMsgContent.length}`);
20470
20770
  for (let i = 0; i < userMsgContent.length; i++) {
@@ -20474,32 +20774,11 @@ ${text}` : text });
20474
20774
  } else {
20475
20775
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
20476
20776
  }
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;
20777
+ const emotionStripped = chatMode === "emotion";
20778
+ const isLight = isLightMode(chatMode, channelName) && chatMode !== "emotion";
20488
20779
  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
- }
20780
+ const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: emotionStripped });
20496
20781
  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
20782
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
20504
20783
  const delta = deps.mcpManager.getMcpDelta();
20505
20784
  if (delta && delta.addedBlocks.length > 0) {
@@ -20600,6 +20879,8 @@ ${text}` : text });
20600
20879
  // 回复目标(Discord channel ID / user ID)
20601
20880
  inboundFrom: inboundMeta?.from || "",
20602
20881
  // 0826 当前消息发送者 ID(msg_send 回发拦截用;注入消息无 inboundMeta 必须 ?.)
20882
+ inboundIsBot: inboundMeta?.isBot || false,
20883
+ // 0901 rate-breaker 信号:本轮触发者是否 bot(Discord author.bot 官方标记)
20603
20884
  renderer: deps.renderer,
20604
20885
  // TurnRenderer 实例(子 agent 走 display 配置)
20605
20886
  visualEmitter: deps.visualEmitter,
@@ -20617,6 +20898,10 @@ ${text}` : text });
20617
20898
  _deps: deps
20618
20899
  // tool 内部需要完整 deps
20619
20900
  };
20901
+ {
20902
+ const { recordInboundBotFlag: recordInboundBotFlag2 } = await Promise.resolve().then(() => (init_rate_breaker(), rate_breaker_exports));
20903
+ recordInboundBotFlag2(inboundMeta?.from, inboundMeta?.isBot);
20904
+ }
20620
20905
  let fullResponse = "";
20621
20906
  const toolHistoryEntries = [];
20622
20907
  let compacted = false;
@@ -20674,7 +20959,7 @@ ${text}` : text });
20674
20959
  for (const memPath of newPaths) {
20675
20960
  try {
20676
20961
  const stat4 = statSync(memPath);
20677
- const content = readFileSync17(memPath, "utf-8");
20962
+ const content = readFileSync18(memPath, "utf-8");
20678
20963
  const header = memoryHeader(memPath, stat4.mtimeMs);
20679
20964
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
20680
20965
  } catch {
@@ -20758,7 +21043,7 @@ ${text}` : text });
20758
21043
  const attachmentMemories = [];
20759
21044
  for (const mem of relevantMemories) {
20760
21045
  try {
20761
- const content = mem.content ?? readFileSync17(mem.path, "utf-8");
21046
+ const content = mem.content ?? readFileSync18(mem.path, "utf-8");
20762
21047
  const header = mem.header ?? memoryHeader(mem.path, mem.mtimeMs);
20763
21048
  attachmentMemories.push({ path: mem.path, content, mtimeMs: mem.mtimeMs, header });
20764
21049
  } catch {
@@ -20790,7 +21075,7 @@ ${text}` : text });
20790
21075
  return "(\u5DF2\u505C\u6B62)";
20791
21076
  }
20792
21077
  const mode = chatMode;
20793
- const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion", { recallFull }) : void 0;
21078
+ const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion") : void 0;
20794
21079
  deps.renderer?.setEmotionMode?.(mode === "emotion");
20795
21080
  console.log(`[mode] ${sessionId} \u2192 ${mode} (${mode === "emotion" ? "\u53EA SOUL, \u5173 tool \u663E\u793A/thinking/stop-hook" : "\u9ED8\u8BA4 stable, \u663E\u793A tool"})`);
20796
21081
  const toolExclude = resolveToolExclude(channelName);
@@ -21011,7 +21296,7 @@ stack: ${err.stack ?? "(none)"}`);
21011
21296
  }
21012
21297
  } catch (err) {
21013
21298
  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}
21299
+ (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
21300
  stack: ${err.stack ?? "(none)"}
21016
21301
  `);
21017
21302
  } catch {
@@ -21842,6 +22127,13 @@ function setupFileLogging(stateDir) {
21842
22127
  const prefix = `[${ts()}] [ERR] `;
21843
22128
  origError(prefix, ...args2);
21844
22129
  logStream.write(`${prefix}${args2.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
22130
+ `);
22131
+ };
22132
+ const origWarn = console.warn;
22133
+ console.warn = (...args2) => {
22134
+ const prefix = `[${ts()}] [WARN] `;
22135
+ origWarn(prefix, ...args2);
22136
+ logStream.write(`${prefix}${args2.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21845
22137
  `);
21846
22138
  };
21847
22139
  }
@@ -21933,17 +22225,17 @@ var INJECTED_CONTENT_PATTERNS = [
21933
22225
  // 群聊敏感词拦截回执(group.sensitiveWords),role:user 注入但非真实用户
21934
22226
  ];
21935
22227
  function parseJsonlEntries(lines) {
21936
- const entries = [];
22228
+ const entries2 = [];
21937
22229
  for (const line of lines) {
21938
22230
  const trimmed = line.trim();
21939
22231
  if (!trimmed) continue;
21940
22232
  try {
21941
- entries.push(JSON.parse(trimmed));
22233
+ entries2.push(JSON.parse(trimmed));
21942
22234
  } catch {
21943
- entries.push(null);
22235
+ entries2.push(null);
21944
22236
  }
21945
22237
  }
21946
- return entries;
22238
+ return entries2;
21947
22239
  }
21948
22240
  function isRuntimeContextInjected(entry, nextEntry) {
21949
22241
  if (!nextEntry || typeof nextEntry !== "object") return false;
@@ -21995,16 +22287,16 @@ function findLastRealUserMsg(jsonlPath) {
21995
22287
  } catch {
21996
22288
  return null;
21997
22289
  }
21998
- const entries = parseJsonlEntries(lines);
21999
- for (let i = entries.length - 1; i >= 0; i--) {
22000
- const entry = entries[i];
22290
+ const entries2 = parseJsonlEntries(lines);
22291
+ for (let i = entries2.length - 1; i >= 0; i--) {
22292
+ const entry = entries2[i];
22001
22293
  if (!entry || typeof entry !== "object") continue;
22002
22294
  if (entry.type !== "message") continue;
22003
22295
  const msg2 = entry.message;
22004
22296
  if (!msg2 || msg2.role !== "user") continue;
22005
22297
  const text = extractText3(msg2.content);
22006
22298
  if (isSystemSender(text)) continue;
22007
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
22299
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
22008
22300
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
22009
22301
  const ts = entry.timestamp || "";
22010
22302
  const clean = cleanText(text);
@@ -22036,12 +22328,12 @@ function recentMessages(sessions, hours = 12, limit = 60) {
22036
22328
  const jsonlPath = resolveScopeMainJsonl(sessions);
22037
22329
  if (!jsonlPath) return [];
22038
22330
  const lines = fs19.readFileSync(jsonlPath, "utf-8").split("\n");
22039
- const entries = parseJsonlEntries(lines);
22331
+ const entries2 = parseJsonlEntries(lines);
22040
22332
  const nowMs = Date.now();
22041
22333
  const cutoffMs = nowMs - hours * 36e5;
22042
22334
  const results = [];
22043
- for (let i = 0; i < entries.length; i++) {
22044
- const entry = entries[i];
22335
+ for (let i = 0; i < entries2.length; i++) {
22336
+ const entry = entries2[i];
22045
22337
  if (!entry || typeof entry !== "object") continue;
22046
22338
  if (entry.type !== "message") continue;
22047
22339
  const msg2 = entry.message;
@@ -22051,14 +22343,14 @@ function recentMessages(sessions, hours = 12, limit = 60) {
22051
22343
  const text = extractText3(msg2.content);
22052
22344
  if (role === "user") {
22053
22345
  if (isSystemSender(text)) continue;
22054
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
22346
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
22055
22347
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
22056
22348
  }
22057
22349
  if (role === "assistant") {
22058
22350
  if (text.startsWith("HEARTBEAT_OK")) continue;
22059
22351
  let isInjectedResponse = false;
22060
22352
  for (let j = i - 1; j >= Math.max(i - 5, -1); j--) {
22061
- const prevE = entries[j];
22353
+ const prevE = entries2[j];
22062
22354
  if (!prevE || typeof prevE !== "object" || prevE.type !== "message") continue;
22063
22355
  const prevMsg = prevE.message;
22064
22356
  if (!prevMsg || prevMsg.role !== "user") continue;
@@ -22282,7 +22574,7 @@ async function judgeReason(task, taskState, cfg, provider, model) {
22282
22574
  const reason = task.blockedReason || "\uFF08\u6CA1\u7ED9\u7406\u7531\uFF09";
22283
22575
  const elapsed = taskState.lastProgressAt ? formatDuration(Date.now() - new Date(taskState.lastProgressAt).getTime()) : "\u5F88\u4E45\u6CA1\u52A8\u4E86";
22284
22576
  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
22577
+ 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
22578
 
22287
22579
  \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
22580
 
@@ -22704,7 +22996,8 @@ var NudgePlugin = class {
22704
22996
  const stream = this.provider.streamChat({
22705
22997
  model: this.model,
22706
22998
  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",
22999
+ // 0831 去掉硬编码人名(原文"判断你(小柯)是否"):engine 代码多 agent 共用,身份由 SOUL.md 定
23000
+ "\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
23001
  "",
22709
23002
  "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
23003
  '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 +23858,18 @@ function readRecentMessages(sessions, n) {
23565
23858
  const file = path23.join(sessions.sessionsDir, `${mainId}.jsonl`);
23566
23859
  if (!fs23.existsSync(file)) return [];
23567
23860
  const lines = readLastNLines(file, n * 4 + 20);
23568
- const entries = [];
23861
+ const entries2 = [];
23569
23862
  for (const line of lines) {
23570
23863
  const trimmed = line.trim();
23571
23864
  if (!trimmed) continue;
23572
23865
  try {
23573
- entries.push(JSON.parse(trimmed));
23866
+ entries2.push(JSON.parse(trimmed));
23574
23867
  } catch {
23575
23868
  }
23576
23869
  }
23577
23870
  const out = [];
23578
- for (let i = entries.length - 1; i >= 0 && out.length < n; i--) {
23579
- const e = entries[i];
23871
+ for (let i = entries2.length - 1; i >= 0 && out.length < n; i--) {
23872
+ const e = entries2[i];
23580
23873
  if (!e || typeof e !== "object" || e.type !== "message") continue;
23581
23874
  const msg2 = e.message;
23582
23875
  if (!msg2) continue;
@@ -24525,7 +24818,7 @@ function formatBeijingTs(d) {
24525
24818
  }
24526
24819
 
24527
24820
  // src/calendar/commands.ts
24528
- import { existsSync as existsSync15, statSync as statSync8 } from "node:fs";
24821
+ import { existsSync as existsSync16, statSync as statSync8 } from "node:fs";
24529
24822
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
24530
24823
  var WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
24531
24824
  function fmtEnd(start, durationMin) {
@@ -24697,7 +24990,7 @@ function addTask(db, args2) {
24697
24990
  }
24698
24991
  try {
24699
24992
  const absPath = isAbsolute4(docPath) ? docPath : resolve7(process.cwd(), docPath);
24700
- if (!existsSync15(absPath)) {
24993
+ if (!existsSync16(absPath)) {
24701
24994
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728
24702
24995
  \u8DEF\u5F84: ${docPath}
24703
24996
  \u89E3\u6790\u540E: ${absPath}
@@ -25180,7 +25473,7 @@ function registerVoiceChatBridge(httpServer, dispatcher, deps, config2, sessions
25180
25473
  }
25181
25474
 
25182
25475
  // src/voice-chat/config.ts
25183
- var DEFAULTS2 = {
25476
+ var DEFAULTS3 = {
25184
25477
  enabled: false,
25185
25478
  pythonPort: 8011,
25186
25479
  webhookPath: "/webhook/voice-chat",
@@ -25191,20 +25484,20 @@ var DEFAULTS2 = {
25191
25484
  // 8/25 翀哥:断句等待默认 2s(samples@16kHz)
25192
25485
  };
25193
25486
  function parseVoiceChatConfig(raw) {
25194
- if (!raw) return { ...DEFAULTS2 };
25487
+ if (!raw) return { ...DEFAULTS3 };
25195
25488
  return {
25196
25489
  enabled: raw.enabled === true,
25197
25490
  spawnPython: raw.spawnPython !== false,
25198
25491
  // 默认 true,配 false 只注册 webhook
25199
- pythonPort: raw.pythonPort ?? DEFAULTS2.pythonPort,
25200
- webhookPath: raw.webhookPath ?? DEFAULTS2.webhookPath,
25201
- callbackPath: raw.callbackPath ?? DEFAULTS2.callbackPath,
25492
+ pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25493
+ webhookPath: raw.webhookPath ?? DEFAULTS3.webhookPath,
25494
+ callbackPath: raw.callbackPath ?? DEFAULTS3.callbackPath,
25202
25495
  pythonPath: raw.pythonPath,
25203
25496
  vadModelPath: raw.vadModelPath,
25204
25497
  asrModelPath: raw.asrModelPath || "iic/SenseVoiceSmall",
25205
- asrLanguage: raw.asrLanguage ?? DEFAULTS2.asrLanguage,
25206
- vadThreshold: raw.vadThreshold ?? DEFAULTS2.vadThreshold,
25207
- postEndMonitor: raw.postEndMonitor ?? DEFAULTS2.postEndMonitor,
25498
+ asrLanguage: raw.asrLanguage ?? DEFAULTS3.asrLanguage,
25499
+ vadThreshold: raw.vadThreshold ?? DEFAULTS3.vadThreshold,
25500
+ postEndMonitor: raw.postEndMonitor ?? DEFAULTS3.postEndMonitor,
25208
25501
  model: raw.model,
25209
25502
  thinking: raw.thinking === true,
25210
25503
  tts: raw.tts ? {
@@ -25526,7 +25819,7 @@ import path28 from "node:path";
25526
25819
  import fs28 from "node:fs";
25527
25820
 
25528
25821
  // src/memory/cognifold/config.ts
25529
- var DEFAULTS3 = {
25822
+ var DEFAULTS4 = {
25530
25823
  pythonPort: 9001,
25531
25824
  autoStart: true,
25532
25825
  persistDir: "./sessions",
@@ -25538,15 +25831,15 @@ function parseCognifoldConfig(raw) {
25538
25831
  if (!raw) return { enabled: false };
25539
25832
  return {
25540
25833
  enabled: raw.enabled === true,
25541
- pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25542
- pythonPath: raw.pythonPath ?? DEFAULTS3.pythonPath,
25834
+ pythonPort: raw.pythonPort ?? DEFAULTS4.pythonPort,
25835
+ pythonPath: raw.pythonPath ?? DEFAULTS4.pythonPath,
25543
25836
  autoStart: raw.autoStart !== false,
25544
25837
  // default true
25545
- baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS3.pythonPort}/api/v1`,
25546
- persistDir: raw.persistDir ?? DEFAULTS3.persistDir,
25838
+ baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS4.pythonPort}/api/v1`,
25839
+ persistDir: raw.persistDir ?? DEFAULTS4.persistDir,
25547
25840
  scopes: raw.scopes,
25548
- readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS3.readyTimeoutMs,
25549
- maxRestarts: raw.maxRestarts ?? DEFAULTS3.maxRestarts,
25841
+ readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS4.readyTimeoutMs,
25842
+ maxRestarts: raw.maxRestarts ?? DEFAULTS4.maxRestarts,
25550
25843
  llm: raw.llm
25551
25844
  };
25552
25845
  }
@@ -25662,13 +25955,13 @@ var CogniFoldClient = class {
25662
25955
 
25663
25956
  // src/memory/cognifold/session-manager.ts
25664
25957
  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";
25958
+ import { join as join28, dirname as dirname3 } from "node:path";
25666
25959
  var CogniFoldSessionManager = class {
25667
25960
  constructor(workspacePath, config2, client) {
25668
25961
  this.workspacePath = workspacePath;
25669
25962
  this.config = config2;
25670
25963
  this.client = client;
25671
- this.sessionsDir = join27(workspacePath, ".cognifold", "sessions");
25964
+ this.sessionsDir = join28(workspacePath, ".cognifold", "sessions");
25672
25965
  }
25673
25966
  workspacePath;
25674
25967
  config;
@@ -25738,7 +26031,7 @@ var CogniFoldSessionManager = class {
25738
26031
  console.log(`[cognifold] Created new session for scope "${scope}": ${newSession.sessionId}`);
25739
26032
  }
25740
26033
  getFilePath(scope) {
25741
- return join27(this.sessionsDir, `${scope}.json`);
26034
+ return join28(this.sessionsDir, `${scope}.json`);
25742
26035
  }
25743
26036
  async writeFileSafe(filePath, data) {
25744
26037
  try {
@@ -26058,7 +26351,7 @@ import path29 from "node:path";
26058
26351
  import fs29 from "node:fs";
26059
26352
 
26060
26353
  // src/memory/everos/config.ts
26061
- var DEFAULTS4 = {
26354
+ var DEFAULTS5 = {
26062
26355
  everosUrl: "http://127.0.0.1:8100",
26063
26356
  agenticUrl: "http://127.0.0.1:8101",
26064
26357
  agenticPort: 8101,
@@ -26084,7 +26377,7 @@ function parseEverosConfig(raw, providers) {
26084
26377
  if (!raw) {
26085
26378
  return {
26086
26379
  enabled: false,
26087
- ...DEFAULTS4,
26380
+ ...DEFAULTS5,
26088
26381
  userId: "xiaomei",
26089
26382
  llm: { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
26090
26383
  rerank: { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
@@ -26094,12 +26387,12 @@ function parseEverosConfig(raw, providers) {
26094
26387
  }
26095
26388
  return {
26096
26389
  enabled: raw.enabled === true,
26097
- everosUrl: raw.everosUrl ?? DEFAULTS4.everosUrl,
26098
- agenticUrl: raw.agenticUrl ?? DEFAULTS4.agenticUrl,
26099
- agenticPort: raw.agenticPort ?? DEFAULTS4.agenticPort,
26390
+ everosUrl: raw.everosUrl ?? DEFAULTS5.everosUrl,
26391
+ agenticUrl: raw.agenticUrl ?? DEFAULTS5.agenticUrl,
26392
+ agenticPort: raw.agenticPort ?? DEFAULTS5.agenticPort,
26100
26393
  userId: raw.userId ?? "xiaomei",
26101
26394
  autoStart: raw.autoStart !== false,
26102
- defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
26395
+ defaultMode: raw.defaultMode ?? DEFAULTS5.defaultMode,
26103
26396
  llm: resolveProviderConfig(
26104
26397
  raw.llm,
26105
26398
  providers,
@@ -26509,8 +26802,8 @@ function scanSkills(skillsDir) {
26509
26802
  }
26510
26803
  const skills = [];
26511
26804
  const scanDir = (dir, depth) => {
26512
- const entries = fs30.readdirSync(dir, { withFileTypes: true });
26513
- for (const entry of entries) {
26805
+ const entries2 = fs30.readdirSync(dir, { withFileTypes: true });
26806
+ for (const entry of entries2) {
26514
26807
  if (entry.name.startsWith(".") || entry.name === "_archive") continue;
26515
26808
  const full = path30.join(dir, entry.name);
26516
26809
  if (entry.isDirectory() && depth < 3) {
@@ -26801,6 +27094,7 @@ ${rawOutput}
26801
27094
  // src/tools/msg-send.ts
26802
27095
  init_live();
26803
27096
  init_registry();
27097
+ init_rate_breaker();
26804
27098
  function getConfig() {
26805
27099
  return liveConfig.all();
26806
27100
  }
@@ -26864,10 +27158,10 @@ channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u56D
26864
27158
  \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
27159
 
26866
27160
  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"
27161
+ - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", content="\u4F60\u597D"
27162
+ - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", content="\u4F60\u597D"
27163
+ - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", content="\u7CFB\u7EDF\u901A\u77E5"
27164
+ - \u53D1 DM: to="1111111111111111111", content="\u79C1\u804A\u5185\u5BB9"
26871
27165
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", content="\u4ECE\u98DE\u4E66\u53D1\u5230Discord"
26872
27166
  - \u56DE\u590D\u6765\u6E90\u9891\u9053: content="\u6536\u5230"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26873
27167
  schema: {
@@ -26896,12 +27190,12 @@ Examples:
26896
27190
  }
26897
27191
  const toIds = to ? to.split(",").map((s) => s.trim()).filter(Boolean) : [];
26898
27192
  const dest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
27193
+ const dmEchoDest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26899
27194
  const isDmEcho = ctx.channelType === "dm" && // DM 对话(群聊先观察不拦)
26900
27195
  resolvedSource === ctx.channel && // 目标通道=当前对话通道(真跨通道转发不拦)
26901
27196
  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);
27197
+ dmEchoDest !== void 0 && (dmEchoDest === ctx.channelTarget || dmEchoDest === ctx.inboundFrom) && // 目的地=当前会话/当前对话者
27198
+ toIds.every((id) => id === ctx.inboundFrom);
26905
27199
  if (isDmEcho) {
26906
27200
  console.log(`[msg_send] \u26D4 DM \u56DE\u53D1\u5F53\u524D\u5BF9\u8BDD\u88AB\u62E6: to=${to} channel_id=${resolvedChannelId || "(fallback)"} (${resolvedSource})`);
26907
27201
  return {
@@ -26932,6 +27226,11 @@ Examples:
26932
27226
  }
26933
27227
  const fullMsg = `${mentionPrefix}${content}`;
26934
27228
  const where = resolvedChannelId ? `${resolvedSource} \u9891\u9053 ${resolvedChannelId}` : `${resolvedSource} DM ${toIds[0]}`;
27229
+ const breaker = checkRateBreaker(resolvedSource, ctx.sessionId, dest, toIds, !!ctx.inboundIsBot);
27230
+ if (breaker) {
27231
+ console.log(`[msg_send] \u{1F515} rate-breaker \u62E6\u622A: session=${ctx.sessionId} \u2192 ${where}`);
27232
+ return breaker;
27233
+ }
26935
27234
  try {
26936
27235
  await mgr.send(resolvedSource, dest, fullMsg);
26937
27236
  return { content: `\u6D88\u606F\u5DF2\u53D1\u9001\u5230 ${where}` };
@@ -27047,10 +27346,10 @@ Parameters:
27047
27346
  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
27347
 
27049
27348
  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"
27349
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
27350
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
27351
+ - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", type="file", path="/tmp/report.pdf"
27352
+ - \u53D1\u97F3\u9891 DM: to="1111111111111111111", type="audio", path="/tmp/voice.mp3"
27054
27353
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", type="image", path="/tmp/photo.png"
27055
27354
  - \u53D1\u5230\u6765\u6E90\u9891\u9053: type="image", path="/tmp/photo.png"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
27056
27355
  schema: {
@@ -29826,8 +30125,8 @@ registerCommand({
29826
30125
  const { rm: rm2 } = await import("node:fs/promises");
29827
30126
  const teamsDir = getTeamsDir3();
29828
30127
  const { readdir: readdir2 } = await import("node:fs/promises");
29829
- const entries = await readdir2(teamsDir).catch(() => []);
29830
- for (const entry of entries) {
30128
+ const entries2 = await readdir2(teamsDir).catch(() => []);
30129
+ for (const entry of entries2) {
29831
30130
  const entryPath = `${teamsDir}/${entry}`;
29832
30131
  try {
29833
30132
  await rm2(entryPath, { recursive: true, force: true });
@@ -29936,8 +30235,21 @@ registerCommand({
29936
30235
  let current;
29937
30236
  if (deps.getModelOverride()) {
29938
30237
  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})`;
30238
+ } else if (deps.provider instanceof FallbackProvider) {
30239
+ const status = deps.provider.getChainStatus();
30240
+ const chainStr = status.map((e) => e.label).join(" \u2192 ");
30241
+ const next = status.find((e) => e.cooldownMs <= 0);
30242
+ const cooling = status.filter((e) => e.cooldownMs > 0);
30243
+ current = `**auto-route** \u2014 \u94FE: ${chainStr}
30244
+ \u4E0B\u4E00\u4E2A\u8BF7\u6C42\u7528: **${next?.label ?? "(\u5168\u90E8\u51B7\u5374\u4E2D)"}**`;
30245
+ if (cooling.length > 0) {
30246
+ current += `
30247
+ \u51B7\u5374\u4E2D: ${cooling.map((e) => `${e.label}\uFF08\u5269 ${Math.round(e.cooldownMs / 6e4)}min\uFF09`).join("\u3001")}`;
30248
+ }
30249
+ if (deps.provider.lastUsedLabel) {
30250
+ current += `
30251
+ \u4E0A\u6B21\u5B9E\u9645: ${deps.provider.lastUsedLabel}`;
30252
+ }
29941
30253
  } else {
29942
30254
  current = `**${deps.config.provider.id}/${deps.config.model}** (default, auto-route)`;
29943
30255
  }
@@ -30191,6 +30503,16 @@ Use full ref like \`/vision-model ${candidates[0].ref}\``);
30191
30503
  });
30192
30504
 
30193
30505
  // src/engine-startup.ts
30506
+ function resolveModelMaxTokens(modelRef) {
30507
+ try {
30508
+ const [pid, mid] = (modelRef || "").split("/");
30509
+ const models = liveConfig.get(`models.providers.${pid}.models`);
30510
+ const m = models?.find((x) => x?.id === mid);
30511
+ return typeof m?.maxTokens === "number" && m.maxTokens > 0 ? m.maxTokens : 4096;
30512
+ } catch {
30513
+ return 4096;
30514
+ }
30515
+ }
30194
30516
  var _epipeSeen = false;
30195
30517
  process.on("uncaughtException", (err) => {
30196
30518
  const code = err?.code ?? "";
@@ -30260,7 +30582,10 @@ async function startEngine(config2, opts) {
30260
30582
  const licensedFeatures = loadLicense(config2.stateDir, config2.profile?.devMode === true);
30261
30583
  const requiredTools = resolveRequiredTools(config2.profile.features, licensedFeatures);
30262
30584
  registry.licensedFeatures = licensedFeatures;
30263
- const { provider, visionProvider, visionChainLabels } = buildProviderChain(config2);
30585
+ let provider;
30586
+ let visionProvider;
30587
+ let visionChainLabels;
30588
+ ({ provider, visionProvider, visionChainLabels } = buildProviderChain(config2));
30264
30589
  if (visionChainLabels.length > 0) {
30265
30590
  console.log(`[vision] Routing enabled: ${visionChainLabels.join(" \u2192 ")}`);
30266
30591
  }
@@ -30482,11 +30807,36 @@ ${content}`
30482
30807
  console.log(`[DEBUG] definitions() = ${_allDefs.length} defs`);
30483
30808
  console.log(`[DEBUG] active (non-defer) = ${_activeDefs.length}: ${_activeDefs.map((d) => d.function.name).join(", ")}`);
30484
30809
  console.log(`[DEBUG] deferred = ${_deferredDefs.length}: ${_deferredDefs.map((d) => d.function.name).join(", ")}`);
30485
- const systemStable = buildStablePrompt(config2.workspace, config2.prompt);
30810
+ let _stableCache = null;
30811
+ const getSystemStable = () => {
30812
+ const promptCfg = liveConfig.get("prompt") || {};
30813
+ const fileCandidates = /* @__PURE__ */ new Set(["SOUL.md"]);
30814
+ for (const f of promptCfg.staticFiles || []) fileCandidates.add(f);
30815
+ for (const item of promptCfg.order || []) {
30816
+ if (typeof item === "string" && /\.(md|txt|json)$/i.test(item)) fileCandidates.add(item);
30817
+ }
30818
+ const statLines = [JSON.stringify({ mode: promptCfg.mode, order: promptCfg.order })];
30819
+ for (const f of fileCandidates) {
30820
+ try {
30821
+ const p = path49.isAbsolute(f) ? f : path49.join(config2.workspace, f);
30822
+ statLines.push(`${f}:${fs47.statSync(p).mtimeMs}`);
30823
+ } catch {
30824
+ statLines.push(`${f}:missing`);
30825
+ }
30826
+ }
30827
+ const fingerprint = statLines.join("|");
30828
+ if (_stableCache && _stableCache.fingerprint === fingerprint) return _stableCache.prompt;
30829
+ const prompt = buildStablePrompt(config2.workspace, promptCfg);
30830
+ _stableCache = { fingerprint, prompt };
30831
+ console.log(`[prompt] stable \u91CD\u5EFA\uFF08\u6587\u4EF6/config \u53D8\u66F4\uFF09\uFF0C${prompt.length} chars`);
30832
+ return prompt;
30833
+ };
30834
+ const systemStable = getSystemStable();
30486
30835
  const systemDynamic = buildDynamicPrompt({
30487
30836
  workspace: config2.workspace
30488
30837
  });
30489
30838
  const systemPrompt = [systemStable, systemDynamic].join("\n\n");
30839
+ const getSystemPrompt = () => [getSystemStable(), buildDynamicPrompt({ workspace: config2.workspace })].join("\n\n");
30490
30840
  dumpSystemPrompt(config2.workspace, systemStable, systemDynamic);
30491
30841
  const modelDef = config2.provider.models.find((m) => m.id === config2.model);
30492
30842
  const modelContextWindow = modelDef?.contextWindow;
@@ -30505,13 +30855,21 @@ ${content}`
30505
30855
  // 默认 5MB
30506
30856
  );
30507
30857
  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
30858
+ const getCompactConfig = () => {
30859
+ const liveComp = liveConfig.get("compaction") || {};
30860
+ const liveModel = (liveConfig.get("providers") || {})[liveConfig.get("agents.defaults.model.primary")?.split("/")[0] || ""];
30861
+ const liveModelId = liveConfig.get("agents.defaults.model.primary")?.split("/")?.[1];
30862
+ const mDef = liveModel?.models?.find((m) => m.id === liveModelId);
30863
+ const mCW = mDef?.contextWindow;
30864
+ const cfg = {
30865
+ ...DEFAULT_COMPACT_CONFIG,
30866
+ ...liveComp,
30867
+ ...mCW && !liveComp.contextWindow ? { contextWindow: mCW } : {},
30868
+ forceFlushTranscriptBytes
30869
+ };
30870
+ return cfg;
30514
30871
  };
30872
+ const compactConfig = getCompactConfig();
30515
30873
  if (compactConfig.contextWindow !== DEFAULT_COMPACT_CONFIG.contextWindow) {
30516
30874
  console.log(`[compact] Context window: ${compactConfig.contextWindow} (from ${config2.compaction?.contextWindow ? "config" : modelContextWindow ? "model" : "default"})`);
30517
30875
  }
@@ -30520,12 +30878,12 @@ ${content}`
30520
30878
  }
30521
30879
  const engine = new QueryEngine(provider, {
30522
30880
  model: config2.model,
30523
- systemPrompt,
30524
- systemStable,
30525
- // 对齐 OpenClaw: stable prefix 用于 prompt cache
30526
- compactConfig,
30527
- // 对齐 CC compaction
30528
- maxTokens: 4096,
30881
+ systemPrompt: getSystemPrompt,
30882
+ systemStable: getSystemStable,
30883
+ // 0902 函数形态:mtime 缓存 getter,改 prompt 文件热生效
30884
+ compactConfig: getCompactConfig,
30885
+ // 0902 函数形态:每 turn 刷新
30886
+ maxTokens: () => resolveModelMaxTokens(liveConfig.get("agents.defaults.model.primary") || config2.model),
30529
30887
  temperature: 0.7,
30530
30888
  maxTurns: config2.profile.maxTurns,
30531
30889
  // 从配置读,默认 50(query.ts 里 fallback)
@@ -30536,10 +30894,10 @@ ${content}`
30536
30894
  if (visionProvider && visionConfig) {
30537
30895
  visionEngine = new QueryEngine(visionProvider, {
30538
30896
  model: visionConfig.modelId,
30539
- systemPrompt,
30540
- systemStable,
30541
- compactConfig,
30542
- maxTokens: 4096,
30897
+ systemPrompt: getSystemPrompt,
30898
+ systemStable: getSystemStable,
30899
+ compactConfig: getCompactConfig,
30900
+ maxTokens: () => resolveModelMaxTokens(`${visionConfig.providerId}/${visionConfig.modelId}`),
30543
30901
  temperature: 0.7,
30544
30902
  maxTurns: config2.profile.maxTurns,
30545
30903
  agentLabel: "main"
@@ -30611,7 +30969,8 @@ ${content}`
30611
30969
  providerApi: config2.provider.api,
30612
30970
  model: config2.model,
30613
30971
  modelInputs: modelDef?.input || ["text"],
30614
- systemPrompt,
30972
+ systemPrompt: getSystemPrompt,
30973
+ // 0902 函数形态:/btw、voice-chat 等按调用时现取(stable mtime 缓存 + dynamic 现算)
30615
30974
  channels: config2.channels,
30616
30975
  config: config2,
30617
30976
  // tool 读自己配置用
@@ -30625,36 +30984,28 @@ ${content}`
30625
30984
  } : void 0,
30626
30985
  mcpManager
30627
30986
  };
30628
- if (visionEngine && visionConfig) {
30987
+ const visionMetaInit = visionEngine && visionConfig ? (() => {
30629
30988
  const vpCfg = config2.providers?.[visionConfig.providerId];
30630
30989
  const visionModelDef = vpCfg?.models?.find((m) => m.id === visionConfig.modelId);
30631
- visionDeps = {
30632
- engine: visionEngine,
30633
- sessions,
30634
- channelManager,
30635
- workspace: config2.workspace,
30990
+ return {
30636
30991
  providerId: visionConfig.providerId,
30637
30992
  providerApi: vpCfg?.api || "openai-completions",
30638
30993
  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
30994
+ modelInputs: visionModelDef?.input || ["text", "image"]
30652
30995
  };
30996
+ })() : null;
30997
+ let visionMeta = visionMetaInit;
30998
+ function resolveVisionDeps() {
30999
+ if (!visionEngine || !visionMeta) return null;
31000
+ if (!visionDeps) {
31001
+ visionDeps = { ...deps, engine: visionEngine, ...visionMeta };
31002
+ console.log(`[vision] deps built: ${visionMeta.providerId}/${visionMeta.model}`);
31003
+ }
31004
+ return visionDeps;
30653
31005
  }
30654
31006
  let modelOverride = null;
30655
31007
  let modelOverrideEngine = null;
30656
31008
  let visionOverride = null;
30657
- const defaultVisionDeps = visionDeps;
30658
31009
  const modelDepsCache = /* @__PURE__ */ new Map();
30659
31010
  deps.invalidateDeskBuddyDeps = () => {
30660
31011
  deskBuddyDeps = null;
@@ -30680,6 +31031,7 @@ ${content}`
30680
31031
  if (!p.provider) {
30681
31032
  visionEngine = null;
30682
31033
  visionDeps = null;
31034
+ visionMeta = null;
30683
31035
  return;
30684
31036
  }
30685
31037
  if (visionEngine) {
@@ -30688,39 +31040,22 @@ ${content}`
30688
31040
  } else {
30689
31041
  visionEngine = new QueryEngine(p.provider, {
30690
31042
  model: p.model,
30691
- systemPrompt,
30692
- systemStable,
30693
- compactConfig,
31043
+ systemPrompt: getSystemPrompt,
31044
+ systemStable: getSystemStable,
31045
+ compactConfig: getCompactConfig,
30694
31046
  maxTokens: 4096,
30695
31047
  temperature: 0.7,
30696
31048
  maxTurns: config2.profile.maxTurns,
30697
31049
  agentLabel: "main"
30698
31050
  });
30699
31051
  }
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
- }
31052
+ visionMeta = {
31053
+ providerId: p.providerId,
31054
+ providerApi: p.providerApi,
31055
+ model: p.model,
31056
+ modelInputs: p.modelInputs
31057
+ };
31058
+ visionDeps = null;
30724
31059
  };
30725
31060
  function createModelDeps(ref) {
30726
31061
  const slashIdx = ref.indexOf("/");
@@ -30740,28 +31075,21 @@ ${content}`
30740
31075
  const llmProvider = providerId === config2.provider.id ? provider : createProvider(providerCfg);
30741
31076
  const engine2 = new QueryEngine(llmProvider, {
30742
31077
  model: modelId,
30743
- systemPrompt,
30744
- systemStable,
30745
- compactConfig,
30746
- maxTokens: modelDef2.maxTokens || 4096,
31078
+ systemPrompt: getSystemPrompt,
31079
+ systemStable: getSystemStable,
31080
+ compactConfig: getCompactConfig,
31081
+ maxTokens: () => resolveModelMaxTokens(`${providerId}/${modelId}`),
30747
31082
  temperature: 0.7,
30748
31083
  maxTurns: config2.profile.maxTurns,
30749
31084
  agentLabel: `main:${ref}`
30750
31085
  });
30751
31086
  return {
31087
+ ...deps,
30752
31088
  engine: engine2,
30753
- sessions,
30754
- channelManager,
30755
- workspace: config2.workspace,
30756
31089
  providerId,
30757
31090
  providerApi: providerCfg.api,
30758
31091
  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
31092
+ modelInputs: modelDef2.input || ["text"]
30765
31093
  };
30766
31094
  }
30767
31095
  const dispatcher = new MessageDispatcher();
@@ -30938,7 +31266,17 @@ ${content}`
30938
31266
  console.log(`[cron] config check: enabled=${config2.cron?.enabled}, hasConfig=${!!config2.cron}`);
30939
31267
  if (config2.cron?.enabled) {
30940
31268
  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);
31269
+ const resolveCronModelDeps = (ref) => {
31270
+ const key = `cron:${ref}`;
31271
+ const cached = modelDepsCache.get(key);
31272
+ if (cached) return cached;
31273
+ const built = createModelDeps(ref);
31274
+ if (!built) return null;
31275
+ modelDepsCache.set(key, built);
31276
+ console.log(`[cron] Model deps built: ${ref}`);
31277
+ return built;
31278
+ };
31279
+ const cronPlugin = new CronPlugin2(config2.cron, sessions, channelManager, deps, config2.stateDir, dispatcher, resolveCronModelDeps);
30942
31280
  await cronPlugin.start();
30943
31281
  const { setCronConfig: setCronConfig2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
30944
31282
  setCronConfig2(config2.cron);
@@ -31114,8 +31452,10 @@ ${notifications}
31114
31452
  dispatcher,
31115
31453
  runningQueries,
31116
31454
  engine,
31117
- systemPrompt,
31118
- compactConfig,
31455
+ systemPrompt: getSystemPrompt,
31456
+ // 0902 函数形态:命令按调用时现取
31457
+ compactConfig: getCompactConfig,
31458
+ // 0902 同上
31119
31459
  provider,
31120
31460
  deps,
31121
31461
  getVisualRegistry: () => visualRegistry,
@@ -31137,9 +31477,17 @@ ${notifications}
31137
31477
  setVisionDeps: (v) => {
31138
31478
  visionDeps = v;
31139
31479
  },
31140
- getDefaultVisionDeps: () => defaultVisionDeps,
31480
+ // reset 用:清 override deps,下一条图片消息由 resolveVisionDeps 按当前 meta 重建
31481
+ getDefaultVisionDeps: () => {
31482
+ visionDeps = null;
31483
+ return null;
31484
+ },
31141
31485
  doReloadConfig
31142
31486
  };
31487
+ deps.onProviderSwapped = (p) => {
31488
+ provider = p;
31489
+ commandDeps.provider = p;
31490
+ };
31143
31491
  const slashCommands = listCommandDefs().map((d) => ({
31144
31492
  name: d.name,
31145
31493
  description: d.description,
@@ -31396,7 +31744,7 @@ ${pathStr}` }];
31396
31744
  for (const att of nonImageAttachments) {
31397
31745
  console.log(`[file] Downloading: ${att.filename} (${att.contentType}, ${att.size}B)`);
31398
31746
  try {
31399
- const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher }) : await fetch(att.url);
31747
+ const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher, signal: AbortSignal.timeout(3e4) }) : await fetch(att.url, { signal: AbortSignal.timeout(3e4) });
31400
31748
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
31401
31749
  const buffer = Buffer.from(await resp.arrayBuffer());
31402
31750
  const safeName2 = path49.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
@@ -31426,7 +31774,8 @@ ${pathStr}` }];
31426
31774
  }
31427
31775
  const isImageBlock = (b) => b.type === "image" || b.type === "image_url";
31428
31776
  const hasImages = Array.isArray(queryContent) && queryContent.some((b) => isImageBlock(b));
31429
- let msgDeps = hasImages && visionDeps ? visionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31777
+ const activeVisionDeps = hasImages ? resolveVisionDeps() : null;
31778
+ let msgDeps = activeVisionDeps ? activeVisionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31430
31779
  if (isDeskBuddy && !hasImages && !modelOverride) {
31431
31780
  if (!deskBuddyDeps) {
31432
31781
  const dbRef = liveConfig.get("channels.deskBuddy.model") || "deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731";
@@ -31454,7 +31803,8 @@ ${pathStr}` }];
31454
31803
  model: dbProviderCfg ? dbModelId : config2.model,
31455
31804
  // 8/18 翀哥:完整工程 prompt(14k tok)+memory(10k) 会把小模型带偏成"工程助手"——deskBuddy 用 SOUL 精简人设
31456
31805
  // 8/21 复用 light-mode.buildLightStablePrompt(和情感模式同一构建器)
31457
- systemPrompt: buildLightStablePrompt(config2.workspace, "deskBuddy"),
31806
+ systemPrompt: () => buildLightStablePrompt(config2.workspace, "deskBuddy"),
31807
+ // 0902 函数形态:改 SOUL 热生效
31458
31808
  maxTokens: 1024,
31459
31809
  temperature: 0.7,
31460
31810
  disableThinking: true,
@@ -31468,7 +31818,7 @@ ${pathStr}` }];
31468
31818
  msgDeps = deskBuddyDeps;
31469
31819
  }
31470
31820
  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"}`);
31821
+ 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
31822
  if (Array.isArray(queryContent)) {
31473
31823
  queryContent.forEach((b, i) => {
31474
31824
  if (b.type === "image" && b.source?.data) {
@@ -31477,13 +31827,13 @@ ${pathStr}` }];
31477
31827
  });
31478
31828
  }
31479
31829
  }
31480
- if (hasImages && visionDeps) {
31481
- console.log(`[vision] Routing to ${visionDeps.providerId}/${visionDeps.model}${visionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31830
+ if (hasImages && activeVisionDeps) {
31831
+ console.log(`[vision] Routing to ${activeVisionDeps.providerId}/${activeVisionDeps.model}${activeVisionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31482
31832
  }
31483
31833
  const preQueryResult = await messageHooks.runPreQuery({
31484
31834
  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
31835
  text: queryContent,
31486
- msgDeps: hasImages && visionDeps ? visionDeps : msgDeps,
31836
+ msgDeps: activeVisionDeps ?? msgDeps,
31487
31837
  deps: { provider, channelManager, sessions, dispatcher, config: config2, workspace: config2.workspace }
31488
31838
  });
31489
31839
  if (preQueryResult.skip) {
@@ -31496,6 +31846,7 @@ ${pathStr}` }];
31496
31846
  }
31497
31847
  queryContent = preQueryResult.text ?? queryContent;
31498
31848
  if (preQueryResult.msgDeps) msgDeps = preQueryResult.msgDeps;
31849
+ msgDeps.renderer = renderer;
31499
31850
  const accepted = dispatcher.submitMessage({
31500
31851
  text: queryContent,
31501
31852
  sessionId,
@@ -31730,8 +32081,9 @@ ${pathStr}` }];
31730
32081
  sessionManager: sessions,
31731
32082
  sessionId,
31732
32083
  model: config2.model,
31733
- contextWindow: compactConfig.contextWindow || 2e5,
31734
- systemPrompt,
32084
+ contextWindow: getCompactConfig().contextWindow || 2e5,
32085
+ systemPrompt: getSystemPrompt(),
32086
+ // 0902 现取(API /context 报告跟当前 prompt 一致)
31735
32087
  toolDefs: registry.definitions(),
31736
32088
  workspace: config2.workspace
31737
32089
  });
@@ -32059,6 +32411,24 @@ async function doReloadConfig(config2, deps, provider) {
32059
32411
  visionModel: newConfig.visionModel,
32060
32412
  visionFallbacks: newConfig.visionFallbacks
32061
32413
  });
32414
+ const newRecall = createMemorySideProvider(
32415
+ newConfig.topics?.recall,
32416
+ provider,
32417
+ newConfig.providers || {}
32418
+ );
32419
+ const newExtract = createMemorySideProvider(
32420
+ newConfig.topics?.extract,
32421
+ provider,
32422
+ newConfig.providers || {}
32423
+ );
32424
+ if (newRecall) {
32425
+ deps.recallProvider = newRecall;
32426
+ changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32427
+ }
32428
+ if (newExtract) {
32429
+ deps.extractProvider = newExtract;
32430
+ changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32431
+ }
32062
32432
  if (oldProviderKey !== newProviderKey) {
32063
32433
  console.log("[reload] Provider structure changed, rebuilding chain...");
32064
32434
  const rebuilt = buildProviderChain(newConfig);
@@ -32069,6 +32439,7 @@ async function doReloadConfig(config2, deps, provider) {
32069
32439
  }
32070
32440
  changes.push(`provider chain rebuilt (${rebuilt.visionChainLabels.length > 0 ? "vision: " + rebuilt.visionChainLabels.join("\u2192") : "no vision"})`);
32071
32441
  }
32442
+ if (deps.onProviderSwapped) deps.onProviderSwapped(rebuilt.provider);
32072
32443
  if (typeof deps.setVisionProvider === "function") {
32073
32444
  const vCfg = newConfig.visionModel;
32074
32445
  const vpCfg = vCfg ? newConfig.providers?.[vCfg.providerId] : void 0;
@@ -32099,24 +32470,6 @@ async function doReloadConfig(config2, deps, provider) {
32099
32470
  changes.push(`deskBuddy model \u2192 ${newDbModel}`);
32100
32471
  }
32101
32472
  }
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
32473
  try {
32121
32474
  const { setAutoDreamConfig: setAutoDreamConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
32122
32475
  setAutoDreamConfig2(newConfig);
@@ -32312,11 +32665,11 @@ function startSecretsWatcher(config2, deps, provider) {
32312
32665
  }
32313
32666
 
32314
32667
  // src/main.ts
32315
- import { readFileSync as readFileSync31 } from "node:fs";
32668
+ import { readFileSync as readFileSync32 } from "node:fs";
32316
32669
  import { fileURLToPath as fileURLToPath3 } from "node:url";
32317
- import { dirname as dirname9, join as join44 } from "node:path";
32670
+ import { dirname as dirname9, join as join45 } from "node:path";
32318
32671
  var __dirname2 = dirname9(fileURLToPath3(import.meta.url));
32319
- var pkg = JSON.parse(readFileSync31(join44(__dirname2, "..", "package.json"), "utf-8"));
32672
+ var pkg = JSON.parse(readFileSync32(join45(__dirname2, "..", "package.json"), "utf-8"));
32320
32673
  var epipeSeen = false;
32321
32674
  process.on("uncaughtException", (err) => {
32322
32675
  const code = err?.code ?? "";