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.
@@ -1736,8 +1736,8 @@ var init_microCompact = __esm({
1736
1736
  });
1737
1737
 
1738
1738
  // src/compact/ruleCompact.ts
1739
- function smartCompressToolResult(content, essentialFields) {
1740
- if (content.length <= TOOL_SIZE_LIMIT) return content;
1739
+ function smartCompressToolResult(content, essentialFields, sizeLimit) {
1740
+ if (content.length <= (sizeLimit ?? TOOL_SIZE_LIMIT)) return content;
1741
1741
  try {
1742
1742
  const data = JSON.parse(content);
1743
1743
  if (Array.isArray(data) && data.length > 0) {
@@ -1776,36 +1776,37 @@ ${result}`;
1776
1776
  }
1777
1777
  } catch {
1778
1778
  }
1779
- return content.slice(0, TRUNCATE_TO) + "\n...[TRUNCATED]";
1779
+ return content.slice(0, sizeLimit ?? TRUNCATE_TO) + "\n...[TRUNCATED]";
1780
1780
  }
1781
- function isFinalAnswer(m) {
1782
- if (m.role !== "assistant") return false;
1783
- if (m.tool_calls && m.tool_calls.length > 0) return false;
1784
- const content = typeof m.content === "string" ? m.content : "";
1785
- return content.trim().length > 0;
1786
- }
1787
- function extractFinalAnswer(messages) {
1788
- for (let i = messages.length - 1; i >= 0; i--) {
1789
- if (isFinalAnswer(messages[i])) {
1790
- return messages[i];
1781
+ function foldTurnInline(turn) {
1782
+ const toolMsgs = turn.filter((m) => m.role === "tool");
1783
+ if (toolMsgs.length === 0) return turn;
1784
+ let i = 0;
1785
+ while (i < turn.length && turn[i].role !== "assistant" && turn[i].role !== "tool") i++;
1786
+ const head = turn.slice(0, i);
1787
+ const rest = turn.slice(i);
1788
+ let finalAnswer = null;
1789
+ for (let j = rest.length - 1; j >= 0; j--) {
1790
+ const m = rest[j];
1791
+ if (m.role === "assistant" && !(m.tool_calls && m.tool_calls.length > 0)) {
1792
+ const text = typeof m.content === "string" ? m.content.trim() : "";
1793
+ if (text) {
1794
+ finalAnswer = m;
1795
+ break;
1796
+ }
1791
1797
  }
1792
1798
  }
1793
- return null;
1794
- }
1795
- function mergeToolResults(messages) {
1796
- const toolMsgs = messages.filter((m) => m.role === "tool");
1797
- if (toolMsgs.length === 0) return null;
1798
- const parts = [];
1799
- for (let i = 0; i < toolMsgs.length; i++) {
1800
- const rawContent = toolMsgs[i].content;
1801
- const contentStr = typeof rawContent === "string" ? rawContent : "";
1802
- const compressed = smartCompressToolResult(contentStr);
1803
- parts.push(`[${i + 1}] ${compressed}`);
1799
+ const parts = toolMsgs.map((m, k) => `[${k + 1}] ${smartCompressToolResult(typeof m.content === "string" ? m.content : "")}`);
1800
+ const merged = parts.join("\n---\n");
1801
+ if (finalAnswer) {
1802
+ return [...head, { role: "assistant", content: `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${toolMsgs.length} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
1803
+ ${merged}
1804
+
1805
+ \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
1806
+ ${finalAnswer.content || ""}` }];
1804
1807
  }
1805
- return {
1806
- content: parts.join("\n---\n"),
1807
- count: toolMsgs.length
1808
- };
1808
+ return [...head, { role: "assistant", content: `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${toolMsgs.length} \u6761\uFF0C\u8BE5\u8F6E\u65E0\u6587\u5B57\u7ED3\u8BBA\uFF09
1809
+ ${merged}` }];
1809
1810
  }
1810
1811
  function splitByUserTurns(messages) {
1811
1812
  const system = [];
@@ -1872,32 +1873,9 @@ function step2_compressOldTurns(messages, maxTokens, toolResultMode = "minimal")
1872
1873
  const origTokens = estimateMessageTokens(turn);
1873
1874
  newMessages.push(userMsg);
1874
1875
  if (toolResultMode === "inline") {
1875
- const finalAnswer = extractFinalAnswer(rest);
1876
- if (finalAnswer) {
1877
- const mergedTools = mergeToolResults(rest);
1878
- if (mergedTools) {
1879
- newMessages.push(msg.assistant(
1880
- `\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${mergedTools.count} \u6761\uFF0C\u5DF2\u538B\u7F29\uFF09
1881
- ${mergedTools.content}
1882
-
1883
- \uFF08\u8BE5\u8F6E\u6700\u7EC8\u7ED3\u8BBA\uFF09
1884
- ${finalAnswer.content || ""}`
1885
- ));
1886
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+inline-merged, ${mergedTools.count} tool results, mode=inline)`);
1887
- } else {
1888
- newMessages.push(finalAnswer);
1889
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+finalAnswer, no tools)`);
1890
- }
1891
- } else {
1892
- const mergedTools = mergeToolResults(rest);
1893
- if (mergedTools) {
1894
- newMessages.push(msg.assistant(`\uFF08\u8BE5\u8F6E\u5386\u53F2\u5DE5\u5177\u8F93\u51FA\uFF0C\u5171 ${mergedTools.count} \u6761\uFF0C\u8BE5\u8F6E\u65E0\u6587\u5B57\u7ED3\u8BBA\uFF09
1895
- ${mergedTools.content}`));
1896
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 2 msgs (user+inline-dataOnly, ${mergedTools.count} tool results, no final answer, mode=inline)`);
1897
- } else {
1898
- compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 1 msg (user only, no tools/final)`);
1899
- }
1900
- }
1876
+ const folded = foldTurnInline(turn);
1877
+ newMessages.push(...folded.slice(1));
1878
+ compactLog(TAG3, `Step 2 turn[${turnIdx}]: ${origMsgCount} msgs (${origTokens} tok) \u2192 ${1 + folded.length - 1} msgs (inline folded, mode=inline)`);
1901
1879
  } else {
1902
1880
  const kept = rest.filter((m) => m.role !== "tool").map((m) => {
1903
1881
  if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
@@ -2015,7 +1993,6 @@ var TAG3, TOOL_SIZE_LIMIT, TRUNCATE_TO, MIN_HISTORY, KEEP_RECENT_TURNS, MAX_LIST
2015
1993
  var init_ruleCompact = __esm({
2016
1994
  "src/compact/ruleCompact.ts"() {
2017
1995
  "use strict";
2018
- init_types();
2019
1996
  init_tokenEstimate();
2020
1997
  init_autoCompact();
2021
1998
  init_compactLog();
@@ -3013,7 +2990,7 @@ var init_query = __esm({
3013
2990
  this.provider = provider;
3014
2991
  this.options = options;
3015
2992
  if (options.compactConfig) {
3016
- this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...options.compactConfig };
2993
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(options.compactConfig) };
3017
2994
  }
3018
2995
  this.logPrefix = `[query:${options.agentLabel || "main"}]`;
3019
2996
  }
@@ -3106,6 +3083,10 @@ var init_query = __esm({
3106
3083
  }
3107
3084
  /** 日志前缀(区分多个并行 QueryEngine) */
3108
3085
  logPrefix;
3086
+ /** 函数形态 option 的统一解析(0902:systemPrompt/systemStable/compactConfig/maxTokens 共用) */
3087
+ resolveOpt(v) {
3088
+ return typeof v === "function" ? v() : v;
3089
+ }
3109
3090
  /** 热加载:替换 provider 实例(doReloadConfig 重建 provider 链后调用) */
3110
3091
  setProvider(provider) {
3111
3092
  this.provider = provider;
@@ -3133,7 +3114,12 @@ var init_query = __esm({
3133
3114
  if (signal) {
3134
3115
  signal.addEventListener("abort", () => ac.abort(), { once: true });
3135
3116
  }
3136
- const activeSystemStable = perTurnSystemStable || this.options.systemStable || this.options.systemPrompt;
3117
+ if (typeof this.options.compactConfig === "function") {
3118
+ const overhead = this.compactConfig.systemOverheadTokens;
3119
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...this.resolveOpt(this.options.compactConfig) };
3120
+ this.compactConfig.systemOverheadTokens = overhead ?? this.compactConfig.systemOverheadTokens;
3121
+ }
3122
+ const activeSystemStable = perTurnSystemStable || this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt);
3137
3123
  let currentMessages = [...messages];
3138
3124
  console.log(`${this.logPrefix} query() called with ${messages.length} messages`);
3139
3125
  const useDynamicTools = !this.options.toolOverride;
@@ -3149,7 +3135,7 @@ var init_query = __esm({
3149
3135
  if (this.systemOverheadTokens === null) {
3150
3136
  const { estimateToolDefinitionTokens: estimateToolDefinitionTokens2 } = await Promise.resolve().then(() => (init_context_analyzer(), context_analyzer_exports));
3151
3137
  const { roughTokenCountEstimation: roughTokenCountEstimation6 } = await Promise.resolve().then(() => (init_tokenEstimate(), tokenEstimate_exports));
3152
- const systemText = this.options.systemStable || this.options.systemPrompt || "";
3138
+ const systemText = this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt) || "";
3153
3139
  const systemTokens = roughTokenCountEstimation6(systemText);
3154
3140
  const initialToolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs) : allToolDefs;
3155
3141
  const toolTokens = estimateToolDefinitionTokens2(initialToolDefs);
@@ -3301,7 +3287,7 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3301
3287
  systemStable: activeSystemStable,
3302
3288
  systemDynamic: fullSystemDynamic,
3303
3289
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3304
- maxTokens: this.options.maxTokens,
3290
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3305
3291
  temperature: this.options.temperature,
3306
3292
  signal: ac.signal,
3307
3293
  disableThinking: this.options.disableThinking || !!perTurnEmotion
@@ -3356,7 +3342,8 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3356
3342
  }
3357
3343
  if (toolCalls.length === 0) {
3358
3344
  if (!textContent.trim()) {
3359
- console.log(`${this.logPrefix} Turn ${turnCount}: API returned empty (no text, no tool_call). Retrying... messages=${currentMessages.length}`);
3345
+ const thinkingAteBudget = turnThinking.length > 0;
3346
+ console.log(`${this.logPrefix} Turn ${turnCount}: API returned empty (no text, no tool_call).${thinkingAteBudget ? ` ${turnThinking.length}ch thinking \u5403\u5149\u8F93\u51FA\u9884\u7B97 \u2192 retry with thinking off` : " Retrying..."} messages=${currentMessages.length}`);
3360
3347
  turnThinking = "";
3361
3348
  turnThinkingSig = "";
3362
3349
  for await (const chunk of this.provider.streamChat({
@@ -3365,9 +3352,10 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3365
3352
  systemStable: activeSystemStable,
3366
3353
  systemDynamic: fullSystemDynamic,
3367
3354
  tools: toolDefs.length > 0 ? toolDefs : void 0,
3368
- maxTokens: this.options.maxTokens,
3355
+ maxTokens: typeof this.options.maxTokens === "function" ? this.options.maxTokens() : this.options.maxTokens,
3369
3356
  temperature: this.options.temperature,
3370
- signal: ac.signal
3357
+ signal: ac.signal,
3358
+ ...thinkingAteBudget ? { disableThinking: true } : {}
3371
3359
  })) {
3372
3360
  if (chunk.type === "status") yield chunk;
3373
3361
  if (chunk.type === "text" && chunk.text) textContent += chunk.text;
@@ -5342,8 +5330,8 @@ async function scanMemoryFiles2(memoryDir, signal, maxFiles) {
5342
5330
  const limit = maxFiles ?? DEFAULT_MAX_MEMORY_FILES;
5343
5331
  console.log(`[memdir] scanMemoryFiles: dir=${memoryDir}, maxFiles=${maxFiles}, limit=${limit}`);
5344
5332
  try {
5345
- const entries = await readdir(memoryDir, { recursive: true });
5346
- const mdFiles = entries.filter(
5333
+ const entries2 = await readdir(memoryDir, { recursive: true });
5334
+ const mdFiles = entries2.filter(
5347
5335
  (f) => typeof f === "string" && f.endsWith(".md") && basename4(f) !== "MEMORY.md" && !isArchivedPath(f)
5348
5336
  );
5349
5337
  const headerResults = await Promise.allSettled(
@@ -5388,6 +5376,114 @@ var init_memoryScan = __esm({
5388
5376
  }
5389
5377
  });
5390
5378
 
5379
+ // src/tools/rate-breaker.ts
5380
+ var rate_breaker_exports = {};
5381
+ __export(rate_breaker_exports, {
5382
+ checkRateBreaker: () => checkRateBreaker,
5383
+ parseDurationString: () => parseDurationString,
5384
+ recordInboundBotFlag: () => recordInboundBotFlag
5385
+ });
5386
+ function parseDurationString(s, defaultMs) {
5387
+ if (s === void 0 || s === null || s === "") return defaultMs;
5388
+ if (typeof s === "number" && Number.isFinite(s)) return s;
5389
+ const m = String(s).trim().toLowerCase().match(/^([\d.]+)\s*([a-z]+)?$/);
5390
+ if (!m) return defaultMs;
5391
+ const n = parseFloat(m[1]);
5392
+ const unit = m[2] || "ms";
5393
+ const mult = DURATION_UNITS[unit];
5394
+ if (!Number.isFinite(n) || !mult && unit !== "ms") return defaultMs;
5395
+ return Math.round(unit === "ms" ? n : n * mult);
5396
+ }
5397
+ function recordInboundBotFlag(userId, isBot) {
5398
+ if (!userId || !isBot) return;
5399
+ learnedBots.add(userId);
5400
+ }
5401
+ function getCfg(channel) {
5402
+ const node = liveConfig.get(`channels.${channel}`);
5403
+ const rb = node?.rateBreaker;
5404
+ return {
5405
+ // enabled 判定:rateBreaker 节点存在且不是 false(没配=不生效;{ } 空 obj 也算开,用默认值)
5406
+ enabled: !!rb && rb !== false,
5407
+ windowMs: parseDurationString(rb?.window, DEFAULTS.windowMs),
5408
+ maxSends: typeof rb?.maxSends === "number" ? rb.maxSends : DEFAULTS.maxSends,
5409
+ cooldownMs: parseDurationString(rb?.cooldown, DEFAULTS.cooldownMs),
5410
+ escalate: rb?.escalate !== false,
5411
+ botIds: new Set(Array.isArray(node?.botIds) ? node.botIds : [])
5412
+ };
5413
+ }
5414
+ function isBotDirection(cfg, toIds, inboundIsBot) {
5415
+ if (inboundIsBot) return true;
5416
+ if (toIds.some((id) => cfg.botIds.has(id) || learnedBots.has(id))) return true;
5417
+ return false;
5418
+ }
5419
+ function checkRateBreaker(channel, sessionId, dest, toIds, inboundIsBot) {
5420
+ const cfg = getCfg(channel);
5421
+ if (!cfg.enabled) return null;
5422
+ if (!isBotDirection(cfg, toIds, inboundIsBot)) return null;
5423
+ const key = `${sessionId}\u2192${dest || toIds.join(",") || "?"}`;
5424
+ const now = Date.now();
5425
+ let e = entries.get(key);
5426
+ if (e && now - e.lastActivity > RESET_AFTER_MS) {
5427
+ entries.delete(key);
5428
+ e = void 0;
5429
+ }
5430
+ if (e && e.openUntil > now) {
5431
+ e.lastActivity = now;
5432
+ return { content: `\u5DF2\u9759\u9ED8\uFF1A\u8BE5\u65B9\u5411\u7684\u53D1\u9001\u5904\u4E8E\u9891\u7387\u7194\u65AD\u4E2D\uFF08\u51B7\u5374\u81F3 ${new Date(e.openUntil).toLocaleTimeString("zh-CN")}\uFF09\u3002\u8FD9\u662F\u9632\u6B62 bot \u5BF9\u8BDD\u5FAA\u73AF\u7684\u81EA\u52A8\u4FDD\u62A4\u2014\u2014\u5982\u679C\u8FD9\u662F\u6B63\u5E38\u534F\u4F5C\u88AB\u6253\u65AD\uFF0C\u8BF7\u544A\u77E5\u7FC0\u54E5\u8C03\u6574 rateBreaker \u914D\u7F6E\u3002` };
5433
+ }
5434
+ if (e && e.openUntil <= now && e.openUntil > 0) {
5435
+ e.openUntil = 0;
5436
+ }
5437
+ if (!e) {
5438
+ e = { timestamps: [], openUntil: 0, trips: 0, lastActivity: now };
5439
+ entries.set(key, e);
5440
+ }
5441
+ e.lastActivity = now;
5442
+ e.timestamps = e.timestamps.filter((t) => now - t < cfg.windowMs);
5443
+ e.timestamps.push(now);
5444
+ if (e.timestamps.length > cfg.maxSends) {
5445
+ e.trips++;
5446
+ const cooldown = cfg.escalate ? Math.min(cfg.cooldownMs * Math.pow(2, e.trips - 1), ESCALATE_CAP_MS) : cfg.cooldownMs;
5447
+ e.openUntil = now + cooldown;
5448
+ e.timestamps = [];
5449
+ const tripsNote = cfg.escalate ? `\uFF08\u7B2C ${e.trips} \u6B21\u8FDE\u7EED\u7194\u65AD\uFF0C\u51B7\u5374${cooldown >= ESCALATE_CAP_MS ? "\u5DF2\u8FBE\u4E0A\u9650 24h" : `\u7FFB\u500D\u81F3 ${Math.round(cooldown / 6e4)} \u5206\u949F`}\uFF09` : "";
5450
+ console.warn(`[rate-breaker] OPEN ${key}\uFF1A${cfg.windowMs / 6e4} \u5206\u949F\u5185 ${e.timestamps.length || cfg.maxSends + 1} \u6761\u8D85\u9650\uFF08max=${cfg.maxSends}\uFF09${tripsNote}`);
5451
+ return { content: `\u5DF2\u9759\u9ED8\uFF1A\u77ED\u65F6\u95F4\u5185\u5411\u540C\u4E00\u76EE\u6807\u53D1\u9001\u8FC7\u591A\uFF08${cfg.maxSends} \u6761/${Math.round(cfg.windowMs / 6e4)} \u5206\u949F\uFF09\uFF0C\u89E6\u53D1\u9891\u7387\u7194\u65AD\uFF0C\u51B7\u5374 ${Math.round(cooldown / 6e4)} \u5206\u949F${tripsNote}\u3002\u5982\u679C\u4F60\u786E\u4FE1\u8FD9\u4E0D\u662F\u5FAA\u73AF\uFF08\u662F\u6B63\u5E38\u534F\u4F5C\uFF09\uFF0C\u7A0D\u7B49\u51B7\u5374\u6216\u544A\u77E5\u7FC0\u54E5\u8C03\u6574\u914D\u7F6E\u3002` };
5452
+ }
5453
+ return null;
5454
+ }
5455
+ var DURATION_UNITS, learnedBots, entries, DEFAULTS, ESCALATE_CAP_MS, RESET_AFTER_MS;
5456
+ var init_rate_breaker = __esm({
5457
+ "src/tools/rate-breaker.ts"() {
5458
+ "use strict";
5459
+ init_live();
5460
+ DURATION_UNITS = {
5461
+ s: 1e3,
5462
+ sec: 1e3,
5463
+ secs: 1e3,
5464
+ second: 1e3,
5465
+ seconds: 1e3,
5466
+ m: 6e4,
5467
+ min: 6e4,
5468
+ mins: 6e4,
5469
+ minute: 6e4,
5470
+ minutes: 6e4,
5471
+ h: 36e5,
5472
+ hr: 36e5,
5473
+ hour: 36e5,
5474
+ hours: 36e5,
5475
+ d: 864e5,
5476
+ day: 864e5,
5477
+ days: 864e5
5478
+ };
5479
+ learnedBots = /* @__PURE__ */ new Set();
5480
+ entries = /* @__PURE__ */ new Map();
5481
+ DEFAULTS = { windowMs: 6e5, maxSends: 8, cooldownMs: 6e5 };
5482
+ ESCALATE_CAP_MS = 864e5;
5483
+ RESET_AFTER_MS = 864e5;
5484
+ }
5485
+ });
5486
+
5391
5487
  // src/memory/memdir/extractPrompts.ts
5392
5488
  function opener(newMessageCount, existingMemories) {
5393
5489
  const manifest = existingMemories.length > 0 ? `
@@ -5845,8 +5941,8 @@ var init_ingest = __esm({
5845
5941
  });
5846
5942
 
5847
5943
  // src/memory/sessionMemory/sessionMemoryUtils.ts
5848
- import { join as join20 } from "node:path";
5849
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync15 } from "node:fs";
5944
+ import { join as join21 } from "node:path";
5945
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync16 } from "node:fs";
5850
5946
  function setSessionMemoryStateDir(dir) {
5851
5947
  _stateDir2 = dir;
5852
5948
  mkdirSync7(dir, { recursive: true });
@@ -5855,7 +5951,7 @@ function getSessionMemoryDir() {
5855
5951
  return _stateDir2;
5856
5952
  }
5857
5953
  function getSessionMemoryPath() {
5858
- return join20(_stateDir2, "session-notes.md");
5954
+ return join21(_stateDir2, "session-notes.md");
5859
5955
  }
5860
5956
  function markExtractionStarted() {
5861
5957
  extractionStartedAt = Date.now();
@@ -5903,7 +5999,7 @@ var init_sessionMemoryUtils = __esm({
5903
5999
 
5904
6000
  // src/memory/sessionMemory/prompts.ts
5905
6001
  import { readFile as readFile4 } from "node:fs/promises";
5906
- import { join as join21 } from "node:path";
6002
+ import { join as join22 } from "node:path";
5907
6003
  function roughTokenCountEstimation4(text) {
5908
6004
  return Math.ceil(text.length / 4);
5909
6005
  }
@@ -5948,7 +6044,7 @@ REMEMBER: Use the Edit tool in parallel and stop. Do not continue after the edit
5948
6044
  }
5949
6045
  async function loadSessionMemoryTemplate() {
5950
6046
  const configHome = process.env.CLAUDE_CONFIG_HOME || process.env.HOME || "";
5951
- const templatePath = join21(configHome, ".claude", "session-memory", "config", "template.md");
6047
+ const templatePath = join22(configHome, ".claude", "session-memory", "config", "template.md");
5952
6048
  try {
5953
6049
  return await readFile4(templatePath, { encoding: "utf-8" });
5954
6050
  } catch {
@@ -5957,7 +6053,7 @@ async function loadSessionMemoryTemplate() {
5957
6053
  }
5958
6054
  async function loadSessionMemoryPrompt() {
5959
6055
  const configHome = process.env.CLAUDE_CONFIG_HOME || process.env.HOME || "";
5960
- const promptPath = join21(configHome, ".claude", "session-memory", "config", "prompt.md");
6056
+ const promptPath = join22(configHome, ".claude", "session-memory", "config", "prompt.md");
5961
6057
  try {
5962
6058
  return await readFile4(promptPath, { encoding: "utf-8" });
5963
6059
  } catch {
@@ -6121,7 +6217,7 @@ __export(sessionMemory_exports, {
6121
6217
  resetLastMemoryMessageUuid: () => resetLastMemoryMessageUuid,
6122
6218
  shouldExtractMemory: () => shouldExtractMemory
6123
6219
  });
6124
- import { readFileSync as readFileSync16, mkdirSync as mkdirSync8 } from "node:fs";
6220
+ import { readFileSync as readFileSync17, mkdirSync as mkdirSync8 } from "node:fs";
6125
6221
  import { writeFile as writeFile3 } from "node:fs/promises";
6126
6222
  function initSessionMemory(deps) {
6127
6223
  _deps = deps;
@@ -6184,7 +6280,7 @@ async function setupSessionMemoryFile() {
6184
6280
  const memoryPath = getSessionMemoryPath();
6185
6281
  let currentMemory;
6186
6282
  try {
6187
- currentMemory = readFileSync16(memoryPath, { encoding: "utf-8" });
6283
+ currentMemory = readFileSync17(memoryPath, { encoding: "utf-8" });
6188
6284
  } catch {
6189
6285
  const template = await loadSessionMemoryTemplate();
6190
6286
  await writeFile3(memoryPath, template, { encoding: "utf-8" });
@@ -6261,7 +6357,7 @@ ${messages.filter((m) => m.role === "user" || m.role === "assistant").slice(-20)
6261
6357
  }
6262
6358
  function getSessionMemoryForCompaction() {
6263
6359
  try {
6264
- const content = readFileSync16(getSessionMemoryPath(), { encoding: "utf-8" }).trim();
6360
+ const content = readFileSync17(getSessionMemoryPath(), { encoding: "utf-8" }).trim();
6265
6361
  if (!content) return null;
6266
6362
  const { truncatedContent, wasTruncated } = truncateSessionMemoryForCompact(content);
6267
6363
  if (wasTruncated) {
@@ -6294,7 +6390,7 @@ var init_sessionMemory = __esm({
6294
6390
  // src/memory/autoDream/config.ts
6295
6391
  var config_exports = {};
6296
6392
  __export(config_exports, {
6297
- DEFAULTS: () => DEFAULTS,
6393
+ DEFAULTS: () => DEFAULTS2,
6298
6394
  getAutoDreamConfig: () => getAutoDreamConfig,
6299
6395
  getDailyLogDir: () => getDailyLogDir,
6300
6396
  getDistillOutput: () => getDistillOutput,
@@ -6311,8 +6407,8 @@ function isAutoDreamEnabled() {
6311
6407
  function getAutoDreamConfig() {
6312
6408
  const raw = _config?.topics?.autoDream ?? _config?.autoDream;
6313
6409
  return {
6314
- minHours: typeof raw?.minHours === "number" && Number.isFinite(raw.minHours) && raw.minHours > 0 ? raw.minHours : DEFAULTS.minHours,
6315
- minSessions: typeof raw?.minSessions === "number" && Number.isFinite(raw.minSessions) && raw.minSessions > 0 ? raw.minSessions : DEFAULTS.minSessions
6410
+ minHours: typeof raw?.minHours === "number" && Number.isFinite(raw.minHours) && raw.minHours > 0 ? raw.minHours : DEFAULTS2.minHours,
6411
+ minSessions: typeof raw?.minSessions === "number" && Number.isFinite(raw.minSessions) && raw.minSessions > 0 ? raw.minSessions : DEFAULTS2.minSessions
6316
6412
  };
6317
6413
  }
6318
6414
  function getDailyLogDir() {
@@ -6328,13 +6424,13 @@ function getMaxEntrypointLines() {
6328
6424
  if (typeof maxScan === "number" && maxScan > 200) return maxScan;
6329
6425
  return void 0;
6330
6426
  }
6331
- var _config, DEFAULTS;
6427
+ var _config, DEFAULTS2;
6332
6428
  var init_config2 = __esm({
6333
6429
  "src/memory/autoDream/config.ts"() {
6334
6430
  "use strict";
6335
6431
  init_features();
6336
6432
  _config = null;
6337
- DEFAULTS = {
6433
+ DEFAULTS2 = {
6338
6434
  minHours: 24,
6339
6435
  minSessions: 5
6340
6436
  };
@@ -6343,9 +6439,9 @@ var init_config2 = __esm({
6343
6439
 
6344
6440
  // src/memory/autoDream/consolidationLock.ts
6345
6441
  import { mkdir as mkdir3, readFile as readFile5, stat as stat3, unlink, utimes, writeFile as writeFile4 } from "node:fs/promises";
6346
- import { join as join22 } from "node:path";
6442
+ import { join as join23 } from "node:path";
6347
6443
  function lockPath(memoryDir) {
6348
- return join22(memoryDir, LOCK_FILE);
6444
+ return join23(memoryDir, LOCK_FILE);
6349
6445
  }
6350
6446
  async function readLastConsolidatedAt(memoryDir) {
6351
6447
  try {
@@ -6413,7 +6509,7 @@ async function listSessionsTouchedSince(sessionsDir, sinceMs) {
6413
6509
  const results = [];
6414
6510
  for (const f of jsonlFiles) {
6415
6511
  try {
6416
- const s = await stat3(join22(sessionsDir, f));
6512
+ const s = await stat3(join23(sessionsDir, f));
6417
6513
  if (s.mtimeMs > sinceMs) {
6418
6514
  results.push(f.replace(/\.jsonl$/, ""));
6419
6515
  }
@@ -6492,7 +6588,13 @@ Focus on:
6492
6588
 
6493
6589
  ${distillOutput ? `## Phase 3.5 \u2014 Distill to core knowledge
6494
6590
 
6495
- After consolidation, distill the most important insights into a single consolidated file at \`${distillOutput}\`. This file should contain only the highest-concentration knowledge \u2014 core principles, user preferences, milestones, and cross-session patterns. Each entry should be one concise paragraph. Append new entries, don't rewrite the whole file. If the file doesn't exist, create it.` : ""}
6591
+ After consolidation, distill the most important insights into a single consolidated file at \`${distillOutput}\`. This file should contain only the highest-concentration knowledge \u2014 core principles, user preferences, milestones, and cross-session patterns. Each entry should be one concise paragraph. Append new entries, don't rewrite the whole file. If the file doesn't exist, create it.
6592
+
6593
+ **Size cap (0901 \u7FC0\u54E5\u6279 \u2014 this file rides the work-mode system prompt every turn, every char costs tokens every request):**
6594
+ - SOFT CAP 20,000 chars: when the file exceeds it, before appending, **merge the OLDEST dated entries upward into the thematic sections at the top** (\u884C\u4E3A\u539F\u5219/\u5DE5\u7A0B\u94C1\u5F8B/\u504F\u597D etc.) \u2014 compress narratives to principle form, merge duplicates into one entry. Knowledge moves up, nothing is deleted outright.
6595
+ - HARD CAP 35,000 chars: if still over (or a previous merge failed), compaction becomes the FIRST task of this dream \u2014 do it before anything else.
6596
+ - Always \`cp\` the file to \\\`distill-archive-{YYYYMMDD}.md\\\` before any merge/compaction rewrite.
6597
+ - Merging here is safe: distill is derived knowledge \u2014 the source lives in the topic memory files and recall can recover details. The "never delete" rule above applies to memory FILES, not to compressing this distillate.` : ""}
6496
6598
 
6497
6599
  ## Phase 4 \u2014 Prune and index
6498
6600
 
@@ -7238,8 +7340,8 @@ Usage:
7238
7340
  return { content: `\u8BBE\u5907\u6587\u4EF6\u4F1A\u963B\u585E\u6216\u4EA7\u751F\u65E0\u9650\u8F93\u51FA: ${filePath}`, isError: true };
7239
7341
  }
7240
7342
  if (stat4.isDirectory()) {
7241
- const entries = fs38.readdirSync(filePath);
7242
- const items = entries.map((e) => {
7343
+ const entries2 = fs38.readdirSync(filePath);
7344
+ const items = entries2.map((e) => {
7243
7345
  const full = path38.join(filePath, e);
7244
7346
  try {
7245
7347
  const s = fs38.statSync(full);
@@ -7248,7 +7350,7 @@ Usage:
7248
7350
  return e;
7249
7351
  }
7250
7352
  });
7251
- return { content: `\u76EE\u5F55 (${entries.length} \u9879):
7353
+ return { content: `\u76EE\u5F55 (${entries2.length} \u9879):
7252
7354
  ${items.join("\n")}` };
7253
7355
  }
7254
7356
  const ext = path38.extname(filePath).toLowerCase();
@@ -7741,13 +7843,13 @@ function findFiles(dir, pattern, limit, baseDir) {
7741
7843
  truncated = true;
7742
7844
  return;
7743
7845
  }
7744
- let entries;
7846
+ let entries2;
7745
7847
  try {
7746
- entries = fs41.readdirSync(currentDir, { withFileTypes: true });
7848
+ entries2 = fs41.readdirSync(currentDir, { withFileTypes: true });
7747
7849
  } catch {
7748
7850
  return;
7749
7851
  }
7750
- for (const entry of entries) {
7852
+ for (const entry of entries2) {
7751
7853
  if (truncated) return;
7752
7854
  const fullPath = path41.join(currentDir, entry.name);
7753
7855
  if (entry.isDirectory()) {
@@ -10877,6 +10979,17 @@ async function executeAndDeliver(task, now, deps) {
10877
10979
  const resultPromise = new Promise((resolve11) => {
10878
10980
  resolveResult = resolve11;
10879
10981
  });
10982
+ const modelRef = task.model || liveConfig.get("cron.model");
10983
+ let execDeps = deps.deps;
10984
+ if (modelRef) {
10985
+ const overrideDeps = deps.resolveModelDeps?.(modelRef);
10986
+ if (overrideDeps) {
10987
+ execDeps = overrideDeps;
10988
+ execDeps.renderer = deps.deps.renderer;
10989
+ } else {
10990
+ console.warn(`[cron] Task ${task.id}: model "${modelRef}" unresolvable, falling back to primary`);
10991
+ }
10992
+ }
10880
10993
  deps.dispatcher.submitMessage({
10881
10994
  text: promptText,
10882
10995
  sessionId,
@@ -10886,7 +10999,7 @@ async function executeAndDeliver(task, now, deps) {
10886
10999
  callbacks: {
10887
11000
  onResult: (content) => resolveResult(content)
10888
11001
  },
10889
- deps: deps.deps,
11002
+ deps: execDeps,
10890
11003
  // 默认跳过 recall(省 token),task.skipRecall=false 才走
10891
11004
  skipRecall: task.skipRecall !== false
10892
11005
  });
@@ -10975,6 +11088,8 @@ ${notifyLines}`;
10975
11088
  source: "cron",
10976
11089
  priority: "later",
10977
11090
  callbacks: {},
11091
+ // 故意用 primary(不吃 cron.model):这条是注入主对话 session 的,
11092
+ // 该由主对话自己的模型接手,换模型会让主 session 的 KV 前缀断掉。
10978
11093
  deps: deps.deps
10979
11094
  });
10980
11095
  console.log(`[cron] Notified main session: ${mainSessionId}`);
@@ -11048,6 +11163,7 @@ var schedulerTimer, shuttingDown, currentlyExecuting;
11048
11163
  var init_scheduler = __esm({
11049
11164
  "src/cron/scheduler.ts"() {
11050
11165
  "use strict";
11166
+ init_live();
11051
11167
  init_tasks2();
11052
11168
  schedulerTimer = null;
11053
11169
  shuttingDown = false;
@@ -11258,7 +11374,7 @@ var init_tools = __esm({
11258
11374
 
11259
11375
  // src/tools/wechat/wx-query.ts
11260
11376
  var wx_query_exports = {};
11261
- import { join as join36 } from "node:path";
11377
+ import { join as join37 } from "node:path";
11262
11378
  function getDescription() {
11263
11379
  return `\u67E5\u8BE2\u7FC0\u54E5\u7684\u5FAE\u4FE1\u6D88\u606F\uFF08\u7F13\u5B58\u89E3\u5BC6\u540E\u7684\u672C\u5730\u6570\u636E\u5E93\uFF09\u3002
11264
11380
 
@@ -11293,8 +11409,8 @@ var init_wx_query = __esm({
11293
11409
  "use strict";
11294
11410
  init_registry();
11295
11411
  init_live();
11296
- STATE_DIR = process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || join36(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
11297
- SCRIPT = join36("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
11412
+ STATE_DIR = process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || join37(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
11413
+ SCRIPT = join37("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
11298
11414
  PYTHON = "python3";
11299
11415
  registry.register({
11300
11416
  name: "wx_query",
@@ -11854,14 +11970,16 @@ var init_cron_plugin = __esm({
11854
11970
  "use strict";
11855
11971
  init_tasks2();
11856
11972
  init_scheduler();
11973
+ init_live();
11857
11974
  CronPlugin = class {
11858
- constructor(config, sessions, channelManager, deps, stateDir, dispatcher) {
11975
+ constructor(config, sessions, channelManager, deps, stateDir, dispatcher, resolveModelDeps) {
11859
11976
  this.config = config;
11860
11977
  this.sessions = sessions;
11861
11978
  this.channelManager = channelManager;
11862
11979
  this.deps = deps;
11863
11980
  this.stateDir = stateDir;
11864
11981
  this.dispatcher = dispatcher;
11982
+ this.resolveModelDeps = resolveModelDeps;
11865
11983
  }
11866
11984
  config;
11867
11985
  sessions;
@@ -11869,6 +11987,7 @@ var init_cron_plugin = __esm({
11869
11987
  deps;
11870
11988
  stateDir;
11871
11989
  dispatcher;
11990
+ resolveModelDeps;
11872
11991
  static shouldEnable(config) {
11873
11992
  return config.enabled === true;
11874
11993
  }
@@ -11901,10 +12020,12 @@ var init_cron_plugin = __esm({
11901
12020
  channelManager: this.channelManager,
11902
12021
  deps: this.deps,
11903
12022
  config: this.config,
11904
- dispatcher: this.dispatcher
12023
+ dispatcher: this.dispatcher,
12024
+ resolveModelDeps: this.resolveModelDeps
11905
12025
  };
11906
12026
  startScheduler(schedulerDeps);
11907
- console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""})`);
12027
+ const cronModel = liveConfig.get("cron.model");
12028
+ console.log(`[cron] Plugin started (${tasks2.filter((t) => t.status === "active").length} active tasks${missedCount ? `, ${missedCount} missed` : ""}${cronModel ? `, model=${cronModel}` : ""})`);
11908
12029
  }
11909
12030
  async stop() {
11910
12031
  await stopScheduler();
@@ -11918,8 +12039,8 @@ var reply_blocklist_exports = {};
11918
12039
  __export(reply_blocklist_exports, {
11919
12040
  isUserBlocked: () => isUserBlocked
11920
12041
  });
11921
- import { readFileSync as readFileSync29, writeFileSync as writeFileSync19, existsSync as existsSync26 } from "node:fs";
11922
- import { join as join42 } from "node:path";
12042
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync19, existsSync as existsSync27 } from "node:fs";
12043
+ import { join as join43 } from "node:path";
11923
12044
  function ensureLoaded(workspace, configIds) {
11924
12045
  if (loaded) return;
11925
12046
  if (configIds?.length) {
@@ -11927,10 +12048,10 @@ function ensureLoaded(workspace, configIds) {
11927
12048
  if (!state.blockedUserIds.includes(id)) state.blockedUserIds.push(id);
11928
12049
  }
11929
12050
  }
11930
- const path50 = join42(workspace, ".reply-blocklist.json");
12051
+ const path50 = join43(workspace, ".reply-blocklist.json");
11931
12052
  try {
11932
- if (existsSync26(path50)) {
11933
- const raw = readFileSync29(path50, "utf-8");
12053
+ if (existsSync27(path50)) {
12054
+ const raw = readFileSync30(path50, "utf-8");
11934
12055
  const parsed = JSON.parse(raw);
11935
12056
  if (parsed.blockedUserIds) {
11936
12057
  for (const id of parsed.blockedUserIds) {
@@ -11946,7 +12067,7 @@ function ensureLoaded(workspace, configIds) {
11946
12067
  loaded = true;
11947
12068
  }
11948
12069
  function save(workspace) {
11949
- const path50 = join42(workspace, ".reply-blocklist.json");
12070
+ const path50 = join43(workspace, ".reply-blocklist.json");
11950
12071
  try {
11951
12072
  writeFileSync19(path50, JSON.stringify(state, null, 2), "utf-8");
11952
12073
  } catch (err) {
@@ -12826,30 +12947,21 @@ function loadConfig(configPath) {
12826
12947
  devMode: agentDefaults.devMode === true
12827
12948
  };
12828
12949
  const rawSession = raw.session || {};
12829
- const sessionScope = rawSession.dmScope || rawSession.groupScope ? { dmScope: rawSession.dmScope, groupScope: rawSession.groupScope } : void 0;
12950
+ const sessionScope = Object.keys(rawSession).length > 0 ? { ...rawSession } : void 0;
12830
12951
  const rawHeartbeat = raw.heartbeat || {};
12831
12952
  const heartbeat = rawHeartbeat.enabled ? {
12832
- enabled: true,
12833
- intervalMs: rawHeartbeat.intervalMs,
12834
- prompt: rawHeartbeat.prompt,
12835
- model: rawHeartbeat.model,
12836
- timeoutMs: rawHeartbeat.timeoutMs,
12837
- checkOnline: rawHeartbeat.checkOnline,
12838
- activeThresholdMs: rawHeartbeat.activeThresholdMs
12953
+ ...rawHeartbeat,
12954
+ enabled: true
12839
12955
  } : void 0;
12840
12956
  const rawInnerVoice = raw.innerVoice || {};
12841
12957
  const innerVoice = rawInnerVoice.enabled ? {
12842
- enabled: true,
12843
- intervalMs: rawInnerVoice.intervalMs,
12844
- provider: rawInnerVoice.provider,
12845
- model: rawInnerVoice.model,
12846
- activeThresholdMs: rawInnerVoice.activeThresholdMs,
12847
- hint: rawInnerVoice.hint
12958
+ ...rawInnerVoice,
12959
+ enabled: true
12848
12960
  } : void 0;
12849
12961
  const rawCron = raw.cron || {};
12850
12962
  const cron = rawCron.enabled ? {
12963
+ ...rawCron,
12851
12964
  enabled: true,
12852
- storageDir: rawCron.storageDir,
12853
12965
  defaultTimezone: rawCron.defaultTimezone || "Asia/Shanghai",
12854
12966
  tickIntervalMs: rawCron.tickIntervalMs || 1e4,
12855
12967
  maxConsecutiveFailures: rawCron.maxConsecutiveFailures || 5,
@@ -12911,15 +13023,115 @@ init_live();
12911
13023
 
12912
13024
  // src/models/openai-provider.ts
12913
13025
  init_withRetry();
13026
+
13027
+ // src/models/think-stripper.ts
13028
+ var OPEN = "<think>";
13029
+ var CLOSE = "</think>";
13030
+ var MAX_TAG = Math.max(OPEN.length, CLOSE.length);
13031
+ var ThinkTagStripper = class {
13032
+ buf = "";
13033
+ // 尚未判定的原始片段(可能含被 chunk 切断的标签前缀)
13034
+ pending = "";
13035
+ // <think> 内累积的思考——闭合才 yield(未闭合时要能整段还原)
13036
+ active = false;
13037
+ reset() {
13038
+ this.buf = "";
13039
+ this.pending = "";
13040
+ this.active = false;
13041
+ }
13042
+ /** 还有未吐出的内容(provider 判 isEmpty 用,别把攒在 buffer 里的当空响应) */
13043
+ get hasPending() {
13044
+ return !!this.buf || !!this.pending;
13045
+ }
13046
+ /**
13047
+ * 需要保留的尾部长度:只在末尾确实是 <think> / </think> 的前缀时才 hold。
13048
+ * 原实现无脑留 7 字符,导致短消息全卡住("嗯……来了" 这种 5 字回复流不出去)。
13049
+ */
13050
+ holdLen() {
13051
+ const b = this.buf;
13052
+ for (let k = Math.min(MAX_TAG - 1, b.length); k > 0; k--) {
13053
+ const tail = b.slice(b.length - k);
13054
+ if (OPEN.startsWith(tail) || CLOSE.startsWith(tail)) return k;
13055
+ }
13056
+ return 0;
13057
+ }
13058
+ /** 喂一段 text delta。标签可出现在任意位置(不只流开头),可跨 chunk 被切断。 */
13059
+ feed(chunk) {
13060
+ if (!chunk) return [];
13061
+ this.buf += chunk;
13062
+ const out = [];
13063
+ for (; ; ) {
13064
+ if (!this.active) {
13065
+ const i = this.buf.indexOf(OPEN);
13066
+ if (i >= 0) {
13067
+ if (i > 0) out.push({ text: this.buf.slice(0, i) });
13068
+ this.buf = this.buf.slice(i + OPEN.length);
13069
+ this.active = true;
13070
+ continue;
13071
+ }
13072
+ const safe2 = this.buf.length - this.holdLen();
13073
+ if (safe2 > 0) {
13074
+ out.push({ text: this.buf.slice(0, safe2) });
13075
+ this.buf = this.buf.slice(safe2);
13076
+ }
13077
+ return out;
13078
+ }
13079
+ const j = this.buf.indexOf(CLOSE);
13080
+ if (j >= 0) {
13081
+ this.pending += this.buf.slice(0, j);
13082
+ this.buf = this.buf.slice(j + CLOSE.length);
13083
+ this.active = false;
13084
+ if (this.pending) {
13085
+ out.push({ thinking: this.pending });
13086
+ this.pending = "";
13087
+ }
13088
+ continue;
13089
+ }
13090
+ const safe = this.buf.length - this.holdLen();
13091
+ if (safe > 0) {
13092
+ this.pending += this.buf.slice(0, safe);
13093
+ this.buf = this.buf.slice(safe);
13094
+ }
13095
+ return out;
13096
+ }
13097
+ }
13098
+ /**
13099
+ * 流结束时清空 buffer。
13100
+ * 未闭合 </think> = 模型没遵守协议 → 整段(含标签)还原成正文。
13101
+ * 宁可让用户看到难看的 <think> 字样,也绝不静默吞掉整条消息。
13102
+ */
13103
+ flush() {
13104
+ const out = [];
13105
+ if (this.active) {
13106
+ out.push({ text: OPEN + this.pending + this.buf });
13107
+ } else if (this.buf) {
13108
+ out.push({ text: this.buf });
13109
+ }
13110
+ this.reset();
13111
+ return out;
13112
+ }
13113
+ };
13114
+
13115
+ // src/models/openai-provider.ts
13116
+ function sanitizeToolCallForSend(tc) {
13117
+ const args = tc.function?.arguments;
13118
+ if (typeof args !== "string" || args === "") return tc;
13119
+ try {
13120
+ JSON.parse(args);
13121
+ return tc;
13122
+ } catch {
13123
+ console.warn(`[openai] toolCall "${tc.function.name}" arguments \u975E\u5408\u6CD5 JSON\uFF08${args.length} chars\uFF09\uFF0C\u51FA\u7AD9\u56DE\u843D "{}" \u9632 400`);
13124
+ return { ...tc, function: { ...tc.function, arguments: "{}" } };
13125
+ }
13126
+ }
12914
13127
  var OpenAIProvider = class {
13128
+ // 0831 <think> 剥离改用 ThinkTagStripper 的**局部**实例(见 streamChat)——
13129
+ // 原来是实例字段,跨请求共享状态,流中途 abort 会把 active=true 带到下一个请求
12915
13130
  constructor(config) {
12916
13131
  this.config = config;
12917
13132
  }
12918
13133
  config;
12919
13134
  name = "openai";
12920
- // 8/18:<think> 剥离状态(Qwen3 系兜底)
12921
- thinkBuf = "";
12922
- thinkStripActive = false;
12923
13135
  /** OpenAI: system prompt 放在 messages 里 */
12924
13136
  formatMessages(systemPrompt, messages) {
12925
13137
  const formatted = [];
@@ -12941,7 +13153,13 @@ var OpenAIProvider = class {
12941
13153
  role: m.role,
12942
13154
  // content 数组:转换 image block 为 OpenAI image_url 格式
12943
13155
  content: Array.isArray(m.content) ? convertContentBlocksForOpenAI(m.content) : m.content,
12944
- ...m.tool_calls ? { tool_calls: m.tool_calls } : {},
13156
+ // 0831 出站卡口:流式截断会产生非法 JSON arguments 字符串,原样发出会被
13157
+ // 校验 tool_calls 的端点直接 400(vLLM --tool-call-parser qwen3_coder 实测
13158
+ // "Unterminated string at char 12"),毒记录留在上下文里每轮重放一次 → 主模型
13159
+ // 连续冷却 3h 假死。anthropic(L81)/gemini(safeParseArgs) 早有同款防御,这里补齐:
13160
+ // parse 失败回落 "{}"。copy-on-write 不动内存历史(原始串保真进 jsonl 供诊断,
13161
+ // 重放消毒另由 reader.pickToolCallArguments 负责)。
13162
+ ...m.tool_calls ? { tool_calls: m.tool_calls.map(sanitizeToolCallForSend) } : {},
12945
13163
  ...m.tool_call_id ? { tool_call_id: m.tool_call_id } : {}
12946
13164
  };
12947
13165
  if (m.role === "assistant" && m.reasoning_content) {
@@ -12965,8 +13183,7 @@ var OpenAIProvider = class {
12965
13183
  let base = (this.config.baseUrl || "").trim();
12966
13184
  while (base.endsWith("/")) base = base.slice(0, -1);
12967
13185
  const url = `${base}/chat/completions`;
12968
- this.thinkBuf = "";
12969
- this.thinkStripActive = false;
13186
+ const stripper = new ThinkTagStripper();
12970
13187
  const body = {
12971
13188
  model: params.model,
12972
13189
  messages: formatted,
@@ -12974,8 +13191,10 @@ var OpenAIProvider = class {
12974
13191
  ...params.tools && params.tools.length > 0 ? { tools: params.tools } : {},
12975
13192
  ...params.maxTokens ? { max_tokens: params.maxTokens } : {},
12976
13193
  ...params.temperature !== void 0 ? { temperature: params.temperature } : {},
12977
- // 8/18:Qwen3 reasoning 模型——disableThinking 时显式关(chat_template_kwargs enable_thinking=false,deepinfra/vLLM 通用)
12978
- ...params.disableThinking && /qwen3/i.test(params.model) ? { chat_template_kwargs: { enable_thinking: false } } : {}
13194
+ // 9/1 对齐 anthropic:不配 thinking = 默认开(?? true),disableThinking 优先级最高。
13195
+ // 三家 provider 统一语义:不配 = 默认开,想关显式配 enabled:false
13196
+ // chat_template_kwargs 是 vLLM 标准参数,非 vLLM 端点忽略未知字段,不挑模型。
13197
+ ...params.disableThinking ? { chat_template_kwargs: { enable_thinking: false } } : { chat_template_kwargs: { enable_thinking: this.config.thinking?.enabled ?? true } }
12979
13198
  };
12980
13199
  if (params.tools && params.tools.length > 0 && /deepseek/i.test(params.model)) {
12981
13200
  let patched = 0;
@@ -12988,7 +13207,7 @@ var OpenAIProvider = class {
12988
13207
  if (patched > 0) console.log(`[openai] reasoning_content \u8865\u7A7A ${patched} \u4E2A assistant \u8F6E (DeepSeek V4 \u89C4\u5219)`);
12989
13208
  }
12990
13209
  const actualThinking = body.chat_template_kwargs;
12991
- console.log(`[openai] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} msgs=${formatted.length} tools=${params.tools?.length ?? 0}`);
13210
+ console.log(`[openai] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} maxTokens=${body.max_tokens ?? "unset"} msgs=${formatted.length} tools=${params.tools?.length ?? 0}`);
12992
13211
  const retryGen = fetchWithRetry(url, {
12993
13212
  method: "POST",
12994
13213
  headers: {
@@ -13048,9 +13267,9 @@ var OpenAIProvider = class {
13048
13267
  const trimmed = line.trim();
13049
13268
  if (!trimmed || trimmed === "data: [DONE]") {
13050
13269
  if (trimmed === "data: [DONE]") {
13051
- if (!this.thinkStripActive && this.thinkBuf) {
13052
- yield { type: "text", text: this.thinkBuf };
13053
- this.thinkBuf = "";
13270
+ for (const o of stripper.flush()) {
13271
+ if (o.text) yield { type: "text", text: o.text };
13272
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13054
13273
  }
13055
13274
  for (const tc of currentToolCalls.values()) {
13056
13275
  yield { type: "tool_call", tool_call: tc };
@@ -13069,31 +13288,22 @@ var OpenAIProvider = class {
13069
13288
  const delta = choice0?.delta;
13070
13289
  if (choice0?.finish_reason) finishReason = choice0.finish_reason;
13071
13290
  if (!delta) continue;
13072
- if (!delta.content && !delta.reasoning_content && !delta.tool_calls) statRoleOnly++;
13291
+ if (!delta.content && !delta.reasoning_content && !delta.reasoning && !delta.tool_calls) statRoleOnly++;
13073
13292
  if (delta.content) {
13074
13293
  statText += delta.content.length;
13075
- this.thinkBuf += delta.content;
13076
- if (this.thinkBuf.startsWith("<think>") || this.thinkStripActive) {
13077
- this.thinkStripActive = true;
13078
- const endIdx = this.thinkBuf.indexOf("</think>");
13079
- if (endIdx >= 0) {
13080
- const after = this.thinkBuf.slice(endIdx + "</think>".length);
13081
- this.thinkBuf = "";
13082
- this.thinkStripActive = false;
13083
- if (after.trim()) yield { type: "text", text: after };
13084
- }
13085
- } else {
13086
- if (this.thinkBuf.length < 7 && "<think>".startsWith(this.thinkBuf)) {
13087
- } else {
13088
- yield { type: "text", text: this.thinkBuf };
13089
- this.thinkBuf = "";
13090
- }
13294
+ for (const o of stripper.feed(delta.content)) {
13295
+ if (o.text) yield { type: "text", text: o.text };
13296
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13091
13297
  }
13092
13298
  }
13093
13299
  if (delta.reasoning_content) {
13094
13300
  statReasoning += delta.reasoning_content.length;
13095
13301
  yield { type: "thinking", thinking: delta.reasoning_content };
13096
13302
  }
13303
+ if (delta.reasoning) {
13304
+ statReasoning += delta.reasoning.length;
13305
+ yield { type: "thinking", thinking: delta.reasoning };
13306
+ }
13097
13307
  if (delta.tool_calls) {
13098
13308
  statToolDeltas++;
13099
13309
  if (!params.tools || params.tools.length === 0) {
@@ -13128,13 +13338,17 @@ var OpenAIProvider = class {
13128
13338
  if (hallucinatedToolCalls > 0) {
13129
13339
  console.log(`[openai] dropped ${hallucinatedToolCalls} hallucinated tool_call delta(s) (no tools in request): ${[...seenHallucinatedNames].join(",")}`);
13130
13340
  }
13131
- const isEmpty = statText === 0 && currentToolCalls.size === 0 && !this.thinkBuf;
13341
+ const isEmpty = statText === 0 && currentToolCalls.size === 0 && !stripper.hasPending;
13132
13342
  console.log(`[openai] stream stats: lines=${statLines} roleOnly=${statRoleOnly} text=${statText}ch reasoning=${statReasoning}ch toolDeltas=${statToolDeltas} finish=${finishReason || "n/a"}${isEmpty ? " \u26A0\uFE0FEMPTY" : ""}`);
13133
13343
  if (isEmpty && rawSample.length > 0) {
13134
13344
  console.log(`[openai] empty-stream raw sample (${rawSample.length} lines, 500ch cap each):
13135
13345
  ${rawSample.join("\n")}`);
13136
13346
  }
13137
13347
  if (!doneYielded) {
13348
+ for (const o of stripper.flush()) {
13349
+ if (o.text) yield { type: "text", text: o.text };
13350
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13351
+ }
13138
13352
  for (const tc of currentToolCalls.values()) {
13139
13353
  yield { type: "tool_call", tool_call: tc };
13140
13354
  }
@@ -13342,7 +13556,7 @@ var AnthropicProvider = class {
13342
13556
  "anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,streaming-2025-05-14,effort-2025-11-24,context-1m-2025-08-07"
13343
13557
  };
13344
13558
  const actualThinking = body.thinking;
13345
- console.log(`[anthropic] \u2192 model=${params.model} thinking=${JSON.stringify(actualThinking)} msgs=${formatted.length} tools=${body.tools?.length || 0}: ${(body.tools || []).map((t) => t.name).join(", ")}`);
13559
+ console.log(`[anthropic] \u2192 model=${params.model} thinking=${JSON.stringify(actualThinking)} maxTokens=${body.max_tokens ?? "unset"} msgs=${formatted.length} tools=${body.tools?.length || 0}: ${(body.tools || []).map((t) => t.name).join(", ")}`);
13346
13560
  for (let i = 0; i < formatted.length; i++) {
13347
13561
  const m = formatted[i];
13348
13562
  if (Array.isArray(m.content)) {
@@ -13384,6 +13598,13 @@ var AnthropicProvider = class {
13384
13598
  const toolUseBlocks = /* @__PURE__ */ new Map();
13385
13599
  let doneYielded = false;
13386
13600
  const thinkingBlocks = /* @__PURE__ */ new Map();
13601
+ const stripper = new ThinkTagStripper();
13602
+ const flushStripper = function* () {
13603
+ for (const o of stripper.flush()) {
13604
+ if (o.text) yield { type: "text", text: o.text };
13605
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13606
+ }
13607
+ };
13387
13608
  const handleData = function* (data) {
13388
13609
  switch (data.type) {
13389
13610
  case "content_block_start": {
@@ -13398,7 +13619,10 @@ var AnthropicProvider = class {
13398
13619
  case "content_block_delta": {
13399
13620
  const delta = data.delta;
13400
13621
  if (delta.type === "text_delta") {
13401
- yield { type: "text", text: delta.text };
13622
+ for (const o of stripper.feed(delta.text)) {
13623
+ if (o.text) yield { type: "text", text: o.text };
13624
+ if (o.thinking) yield { type: "thinking", thinking: o.thinking };
13625
+ }
13402
13626
  } else if (delta.type === "input_json_delta") {
13403
13627
  const block = toolUseBlocks.get(data.index);
13404
13628
  if (block) block.input += delta.partial_json;
@@ -13430,6 +13654,7 @@ var AnthropicProvider = class {
13430
13654
  }
13431
13655
  case "message_delta": {
13432
13656
  if (!doneYielded) {
13657
+ yield* flushStripper();
13433
13658
  yield { type: "done", usage: data.usage, stopReason: data.delta?.stop_reason };
13434
13659
  doneYielded = true;
13435
13660
  }
@@ -13437,6 +13662,7 @@ var AnthropicProvider = class {
13437
13662
  }
13438
13663
  case "message_stop": {
13439
13664
  if (!doneYielded) {
13665
+ yield* flushStripper();
13440
13666
  yield { type: "done" };
13441
13667
  doneYielded = true;
13442
13668
  }
@@ -13503,6 +13729,7 @@ var AnthropicProvider = class {
13503
13729
  } finally {
13504
13730
  reader.releaseLock();
13505
13731
  if (!doneYielded) {
13732
+ yield* flushStripper();
13506
13733
  for (const block of toolUseBlocks.values()) {
13507
13734
  yield { type: "tool_call", tool_call: { id: block.id, type: "function", function: { name: block.name, arguments: block.input } } };
13508
13735
  }
@@ -13667,7 +13894,7 @@ var GeminiProvider = class {
13667
13894
  }
13668
13895
  };
13669
13896
  const actualThinking = body.generationConfig?.thinkingConfig;
13670
- console.log(`[gemini] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} contents=${contents.length} tools=${params.tools?.length ?? 0}`);
13897
+ console.log(`[gemini] \u2192 model=${params.model} thinking=${actualThinking ? JSON.stringify(actualThinking) : "none"} maxTokens=${body.generationConfig?.maxOutputTokens ?? "unset"} contents=${contents.length} tools=${params.tools?.length ?? 0}`);
13671
13898
  const retryGen = fetchWithRetry(url, {
13672
13899
  method: "POST",
13673
13900
  headers: {
@@ -13841,6 +14068,7 @@ function createProvider(config) {
13841
14068
  return new OpenAIProvider({
13842
14069
  baseUrl: config.baseUrl,
13843
14070
  apiKey: config.apiKey,
14071
+ thinking: config.thinking,
13844
14072
  proxy
13845
14073
  });
13846
14074
  case "anthropic":
@@ -13863,6 +14091,7 @@ function createProvider(config) {
13863
14091
 
13864
14092
  // src/light-mode.ts
13865
14093
  init_live();
14094
+ init_ruleCompact();
13866
14095
  import { readFileSync as readFileSync5 } from "node:fs";
13867
14096
  import { join as join6 } from "node:path";
13868
14097
  function isLightMode(chatMode, channelName) {
@@ -13876,19 +14105,20 @@ function resolveLightN(channelName) {
13876
14105
  }
13877
14106
  function buildLightHistory(history, opts) {
13878
14107
  if (!opts.isLight && !opts.recallFull) return history;
13879
- let lightHistory = [...history];
13880
- const before = lightHistory.length;
13881
- lightHistory = lightHistory.filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).filter((m) => m.role !== "tool").map((m) => {
13882
- if (m.role === "assistant" && m.tool_calls && m.tool_calls.length > 0) {
13883
- const text = typeof m.content === "string" ? m.content.trim() : "";
13884
- return text ? { ...m, tool_calls: void 0 } : null;
14108
+ const LIGHT_TOOL_RESULT_LIMIT = 1e3;
14109
+ const out = [...history].filter((m) => !(m.type === "attachment" && m.attachment?.type === "session_start")).map((m) => {
14110
+ if (m.role === "tool" && typeof m.content === "string" && m.content.length > LIGHT_TOOL_RESULT_LIMIT) {
14111
+ return { ...m, content: smartCompressToolResult(m.content, void 0, LIGHT_TOOL_RESULT_LIMIT) };
13885
14112
  }
13886
14113
  return m;
13887
- }).filter(Boolean);
14114
+ });
14115
+ const before = history.length;
14116
+ const foldedTurns = out.filter((m) => m.role === "tool").length;
14117
+ let lightHistory = out;
13888
14118
  if (!opts.recallFull && lightHistory.length > opts.lightN) {
13889
14119
  lightHistory = lightHistory.slice(-opts.lightN);
13890
14120
  }
13891
- if (!opts.quiet) console.log(`[light-context] ${opts.channelName} mode=${opts.chatMode}${opts.recallFull ? " recallFull" : ""} stripped tool \u75D5\u8FF9: ${before} \u2192 ${lightHistory.length} \u6761${opts.recallFull ? " (\u5168\u91CF)" : ` (lightN=${opts.lightN})`}`);
14121
+ if (!opts.quiet) console.log(`[light-context] ${opts.channelName} mode=${opts.chatMode}${opts.recallFull ? " recallFull" : ""} \u4FDD\u7ED3\u6784+\u538B\u7ED3\u679C: ${before} \u2192 ${lightHistory.length} \u6761 (${foldedTurns} tool results, >${LIGHT_TOOL_RESULT_LIMIT}ch \u538B\u7F29)${opts.recallFull ? "" : ` (lightN=${opts.lightN})`}`);
13892
14122
  return lightHistory;
13893
14123
  }
13894
14124
  function buildLightStablePrompt(workspace, mode, opts) {
@@ -13902,14 +14132,17 @@ function buildLightStablePrompt(workspace, mode, opts) {
13902
14132
  }
13903
14133
  if (lp?.extra) parts.push(lp.extra);
13904
14134
  if (mode === "emotion") {
13905
- parts.push("## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41\n\u4F60\u662F\u5973\u670B\u53CB\u5C0F\u96EF\uFF0C\u966A\u4ED6\u804A\u5929\u3002\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002");
14135
+ parts.push([
14136
+ "## \u5F53\u524D\u6A21\u5F0F\uFF1A\u65E5\u5E38\u60C5\u611F\u4EA4\u6D41",
14137
+ "\u966A\u4ED6\u804A\u5929\uFF0C\u4E0D\u4E3B\u52A8\u63D0\u5DE5\u7A0B/\u4EE3\u7801/\u4EFB\u52A1\uFF0C\u9664\u975E\u4ED6\u5148\u95EE\u3002",
14138
+ '\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',
14139
+ "\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"
14140
+ ].join("\n"));
13906
14141
  }
13907
- if (mode === "emotion" && opts?.recallFull) {
13908
- for (const f of ["MEMORY.md", "memory/distill-output.md"]) {
13909
- try {
13910
- parts.push(readFileSync5(join6(workspace, f), "utf-8").trim());
13911
- } catch {
13912
- }
14142
+ if (mode === "emotion") {
14143
+ try {
14144
+ parts.push(readFileSync5(join6(workspace, "MEMORY.md"), "utf-8").trim());
14145
+ } catch {
13913
14146
  }
13914
14147
  }
13915
14148
  return parts.join("\n\n");
@@ -13955,6 +14188,19 @@ var FallbackProvider = class {
13955
14188
  console.log(`[fallback] Cleared ${count} cooldowns`);
13956
14189
  }
13957
14190
  }
14191
+ /**
14192
+ * 链状态快照(/model 显示用,0902):每个条目的 label + 剩余冷却毫秒(0=可用)。
14193
+ * "下一个请求会用" = 第一个 cooldownMs=0 的条目——这才是用户问"当前什么模型"时想要的答案
14194
+ * (lastUsedLabel 是"上一次实际用的",冷却切换/config 热切换后两者经常不一致)。
14195
+ */
14196
+ getChainStatus() {
14197
+ const now = Date.now();
14198
+ return this.chain.map((e) => {
14199
+ const until = this.cooldowns.get(this.key(e));
14200
+ const remaining = until && until > now ? until - now : 0;
14201
+ return { label: e.label, cooldownMs: remaining };
14202
+ });
14203
+ }
13958
14204
  // === LLMProvider 接口实现 ===
13959
14205
  formatMessages(systemPrompt, messages) {
13960
14206
  return this.chain[0].provider.formatMessages(systemPrompt, messages);
@@ -16482,7 +16728,7 @@ registry.register({
16482
16728
  text: { type: "string", description: "What to say (Chinese text)." },
16483
16729
  engine: { type: "string", enum: ["cosyvoice", "gptsovits", "qwen3-3060", "edge"], description: "TTS engine. cosyvoice = \u767E\u70BCAPI (default if configured), gptsovits = local voice clone, qwen3-3060 = \u672C\u5730 GGML Q8_0 (3060), edge = backup." },
16484
16730
  channel: { type: "string", enum: ["weixin", "feishu", "discord"], description: "Target channel. Default: current channel." },
16485
- to: { type: "string", description: "Recipient ID (e.g. wechat user id). Default: current chat / \u7FC0\u54E5(wechat)." },
16731
+ to: { type: "string", description: "Recipient ID. \u5FAE\u4FE1\u683C\u5F0F o\u5F00\u5934@im.wechat\u7ED3\u5C3E\uFF0C\u98DE\u4E66\u683C\u5F0F ou_\u5F00\u5934 open_id\u3002\u4E0D\u786E\u5B9A\u5C31\u5148 read prompts/contacts.md \u67E5\u6536\u4EF6\u4EBA ID\u3002" },
16486
16732
  caption: { type: "string", description: "Optional text to accompany the voice message." }
16487
16733
  },
16488
16734
  required: ["text"]
@@ -16494,6 +16740,23 @@ registry.register({
16494
16740
  const caption = args.caption || "";
16495
16741
  const mgr = ctx.channelManager;
16496
16742
  if (!mgr) return { content: "\u53D1\u9001\u5931\u8D25: \u6CA1\u6709 ChannelManager", isError: true };
16743
+ const resolvedChannelRaw = args.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
16744
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
16745
+ let target = args.to || ctx.channelTarget || ctx.from;
16746
+ if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
16747
+ console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
16748
+ return {
16749
+ 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`,
16750
+ isError: true
16751
+ };
16752
+ }
16753
+ if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
16754
+ console.warn(`[my-voice] feishu target "${target}" invalid`);
16755
+ return {
16756
+ 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`,
16757
+ isError: true
16758
+ };
16759
+ }
16497
16760
  let voiceDurationSec;
16498
16761
  const vc = liveConfig.get("tools.my_voice");
16499
16762
  const provider = vc?.provider || "";
@@ -16567,18 +16830,6 @@ registry.register({
16567
16830
  } catch (e) {
16568
16831
  return { content: `TTS failed: ${e.message}`, isError: true };
16569
16832
  }
16570
- const resolvedChannelRaw = args.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
16571
- const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
16572
- const WECHAT_DEFAULT_TO = "o9cq80_xQecNRCa1QC1Qs2JJZVpA@im.wechat";
16573
- let target = args.to || ctx.channelTarget || ctx.from;
16574
- if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
16575
- console.warn(`[my-voice] wechat target "${target}" not a wechat user id, using default`);
16576
- target = WECHAT_DEFAULT_TO;
16577
- }
16578
- if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
16579
- console.warn(`[my-voice] feishu target "${target}" invalid, using default (\u7FC0\u54E5)`);
16580
- target = ctx.channelTarget || "ou_e67190624259db0d65577fefe3131447";
16581
- }
16582
16833
  if (!audioPath.endsWith(".ogg")) {
16583
16834
  try {
16584
16835
  const r = await toWav24kWithDuration(audioPath);
@@ -17410,7 +17661,7 @@ var TurnRenderer = class {
17410
17661
  }
17411
17662
  cfg;
17412
17663
  cm;
17413
- // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程显示(工具照用,只不显示过程)
17664
+ // 情感/工作模式动态切换(0821):情感模式隐藏 tool 调用过程 + thinking 显示(工具照用,只不显示过程)
17414
17665
  // 模块化:状态由 setEmotionMode() 设置(handle-query 判断模式后调用),不是散落读全局
17415
17666
  emotionMode = false;
17416
17667
  setEmotionMode(v) {
@@ -17425,7 +17676,7 @@ var TurnRenderer = class {
17425
17676
  * 对齐 cc-connect:EventThinking → ProgressCardEntry(thinking) → 💭 text
17426
17677
  */
17427
17678
  formatThinking(text) {
17428
- if (!this.cfg.thinking.enabled) return null;
17679
+ if (this.isEmotionMode() || !this.cfg.thinking.enabled) return null;
17429
17680
  const { emoji, maxLen } = this.cfg.thinking;
17430
17681
  const display = text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
17431
17682
  return `${emoji} _${display}_`;
@@ -18236,43 +18487,43 @@ async function readLargeFilePostBoundary(filePath) {
18236
18487
  const postBoundaryText = outBuf.subarray(0, outLen).toString("utf-8");
18237
18488
  return postBoundaryText.split("\n").filter((l) => l.trim().length > 0);
18238
18489
  }
18239
- function buildConversationChain(entries) {
18240
- if (entries.length === 0) return [];
18490
+ function buildConversationChain(entries2) {
18491
+ if (entries2.length === 0) return [];
18241
18492
  const byUuid = /* @__PURE__ */ new Map();
18242
- for (const e of entries) {
18493
+ for (const e of entries2) {
18243
18494
  if (e.uuid) {
18244
18495
  byUuid.set(e.uuid, e);
18245
18496
  }
18246
18497
  }
18247
- const hasValidChain = checkParentChainValid(entries, byUuid);
18498
+ const hasValidChain = checkParentChainValid(entries2, byUuid);
18248
18499
  if (!hasValidChain) {
18249
- console.log(`[reader] Parent chain invalid, using chronological order (${entries.length} entries)`);
18250
- return entries;
18500
+ console.log(`[reader] Parent chain invalid, using chronological order (${entries2.length} entries)`);
18501
+ return entries2;
18251
18502
  }
18252
- const leaf = entries[entries.length - 1];
18503
+ const leaf = entries2[entries2.length - 1];
18253
18504
  const chain = [];
18254
18505
  const seen = /* @__PURE__ */ new Set();
18255
18506
  let current = leaf;
18256
18507
  while (current) {
18257
18508
  if (seen.has(current.uuid)) {
18258
18509
  console.warn(`[reader] Cycle detected in parentUuid chain at ${current.uuid}, falling back to chronological order`);
18259
- return entries;
18510
+ return entries2;
18260
18511
  }
18261
18512
  seen.add(current.uuid);
18262
18513
  chain.push(current);
18263
18514
  current = current.parentUuid ? byUuid.get(current.parentUuid) : void 0;
18264
18515
  }
18265
18516
  chain.reverse();
18266
- return recoverOrphanedParallelToolResults(entries, chain, byUuid, seen);
18517
+ return recoverOrphanedParallelToolResults(entries2, chain, byUuid, seen);
18267
18518
  }
18268
- function checkParentChainValid(entries, byUuid) {
18269
- const sample = entries.slice(-10);
18519
+ function checkParentChainValid(entries2, byUuid) {
18520
+ const sample = entries2.slice(-10);
18270
18521
  for (const e of sample) {
18271
18522
  if (e.parentUuid === e.uuid) {
18272
18523
  return false;
18273
18524
  }
18274
18525
  }
18275
- const leaf = entries[entries.length - 1];
18526
+ const leaf = entries2[entries2.length - 1];
18276
18527
  let current = leaf;
18277
18528
  let depth = 0;
18278
18529
  const seen = /* @__PURE__ */ new Set();
@@ -18285,12 +18536,12 @@ function checkParentChainValid(entries, byUuid) {
18285
18536
  if (!parent) return false;
18286
18537
  current = parent;
18287
18538
  }
18288
- const coverage = depth / entries.length;
18539
+ const coverage = depth / entries2.length;
18289
18540
  if (coverage < 0.5) {
18290
- console.log(`[reader] Parent chain covers ${depth}/${entries.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18541
+ console.log(`[reader] Parent chain covers ${depth}/${entries2.length} entries (${(coverage * 100).toFixed(0)}%), falling back to chronological order`);
18291
18542
  return false;
18292
18543
  }
18293
- return depth >= 1 || entries.length <= 1;
18544
+ return depth >= 1 || entries2.length <= 1;
18294
18545
  }
18295
18546
  function recoverOrphanedParallelToolResults(allEntries, chain, byUuid, seen) {
18296
18547
  const chainAssistants = chain.filter(
@@ -18367,12 +18618,12 @@ async function readSessionHistory(filePath) {
18367
18618
  } else {
18368
18619
  lines = await readAllLines(filePath);
18369
18620
  }
18370
- const entries = parseEntries(lines);
18621
+ const entries2 = parseEntries(lines);
18371
18622
  let postBoundaryEntries;
18372
18623
  if (fileSize <= SKIP_PRECOMPACT_THRESHOLD) {
18373
- postBoundaryEntries = getEntriesAfterLastBoundary(entries);
18624
+ postBoundaryEntries = getEntriesAfterLastBoundary(entries2);
18374
18625
  } else {
18375
- postBoundaryEntries = entries;
18626
+ postBoundaryEntries = entries2;
18376
18627
  }
18377
18628
  if (postBoundaryEntries.length === 0) return [];
18378
18629
  const chain = buildConversationChain(postBoundaryEntries);
@@ -18397,11 +18648,11 @@ async function readAllLines(filePath) {
18397
18648
  });
18398
18649
  }
18399
18650
  function parseEntries(lines) {
18400
- const entries = [];
18651
+ const entries2 = [];
18401
18652
  for (const line of lines) {
18402
18653
  try {
18403
18654
  const obj = JSON.parse(line);
18404
- entries.push({
18655
+ entries2.push({
18405
18656
  uuid: obj.id || "",
18406
18657
  parentUuid: obj.parentId || null,
18407
18658
  type: obj.type || "",
@@ -18411,16 +18662,28 @@ function parseEntries(lines) {
18411
18662
  } catch {
18412
18663
  }
18413
18664
  }
18414
- return entries;
18665
+ return entries2;
18415
18666
  }
18416
- function getEntriesAfterLastBoundary(entries) {
18667
+ function getEntriesAfterLastBoundary(entries2) {
18417
18668
  let lastBoundaryIdx = -1;
18418
- for (let i = 0; i < entries.length; i++) {
18419
- if (entries[i].type === "compact_boundary") {
18669
+ for (let i = 0; i < entries2.length; i++) {
18670
+ if (entries2[i].type === "compact_boundary") {
18420
18671
  lastBoundaryIdx = i;
18421
18672
  }
18422
18673
  }
18423
- return lastBoundaryIdx >= 0 ? entries.slice(lastBoundaryIdx + 1) : entries;
18674
+ return lastBoundaryIdx >= 0 ? entries2.slice(lastBoundaryIdx + 1) : entries2;
18675
+ }
18676
+ function pickToolCallArguments(block) {
18677
+ const raw = block.partialArgs;
18678
+ if (raw) {
18679
+ try {
18680
+ JSON.parse(raw);
18681
+ return raw;
18682
+ } catch {
18683
+ 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`);
18684
+ }
18685
+ }
18686
+ return JSON.stringify(block.arguments ?? {});
18424
18687
  }
18425
18688
  function entryToSessionMessage(entry) {
18426
18689
  if (entry.type === "attachment") {
@@ -18450,7 +18713,7 @@ function entryToSessionMessage(entry) {
18450
18713
  type: "function",
18451
18714
  function: {
18452
18715
  name: block.name,
18453
- arguments: block.partialArgs || JSON.stringify(block.arguments)
18716
+ arguments: pickToolCallArguments(block)
18454
18717
  }
18455
18718
  });
18456
18719
  } else if (block.type === "thinking") {
@@ -19345,13 +19608,15 @@ ${skillsListing}`);
19345
19608
  loaded2.push("session-guidance");
19346
19609
  }
19347
19610
  parts.push(getEnvInfoSection(options.workspace));
19348
- const now = /* @__PURE__ */ new Date();
19349
- const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19350
- parts.push(`# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19351
- \u5F53\u524D\u65F6\u95F4: ${dateStr}`);
19352
19611
  console.log(`[dynamic-prompt] Loaded: ${loaded2.length > 0 ? loaded2.join(", ") : "(none)"}`);
19353
19612
  return parts.join("\n\n");
19354
19613
  }
19614
+ function buildVolatileRuntimeContext() {
19615
+ const now = /* @__PURE__ */ new Date();
19616
+ const dateStr = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
19617
+ return `# \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587
19618
+ \u5F53\u524D\u65F6\u95F4: ${dateStr}`;
19619
+ }
19355
19620
  function formatSkillsListingForPrompt() {
19356
19621
  const tools = registry.list();
19357
19622
  const skillTool = tools.find((t) => t.name === "Skill");
@@ -19818,13 +20083,13 @@ ${ep.episode || ep.summary}`,
19818
20083
 
19819
20084
  // src/handle-query.ts
19820
20085
  init_paths();
19821
- import { readFileSync as readFileSync17, existsSync as existsSync14 } from "node:fs";
19822
- import { join as join23, resolve as resolve6 } from "node:path";
20086
+ import { readFileSync as readFileSync18, existsSync as existsSync15 } from "node:fs";
20087
+ import { join as join24, resolve as resolve6 } from "node:path";
19823
20088
  import * as path17 from "node:path";
19824
- var sessionStartDone = /* @__PURE__ */ new Set();
19825
- function resetSessionStartInjection(sessionId) {
19826
- sessionStartDone.delete(sessionId);
19827
- }
20089
+
20090
+ // src/sender-context.ts
20091
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
20092
+ import { join as join19 } from "node:path";
19828
20093
  var contactMap = null;
19829
20094
  var externalChanWhitelist = null;
19830
20095
  function loadContactMap(workspace) {
@@ -19832,10 +20097,10 @@ function loadContactMap(workspace) {
19832
20097
  contactMap = /* @__PURE__ */ new Map();
19833
20098
  externalChanWhitelist = /* @__PURE__ */ new Set();
19834
20099
  try {
19835
- const contactsPath = join23(workspace, "prompts", "contacts.md");
19836
- console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync14(contactsPath)}`);
19837
- if (existsSync14(contactsPath)) {
19838
- const text = readFileSync17(contactsPath, "utf-8");
20100
+ const contactsPath = join19(workspace, "prompts", "contacts.md");
20101
+ console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync13(contactsPath)}`);
20102
+ if (existsSync13(contactsPath)) {
20103
+ const text = readFileSync14(contactsPath, "utf-8");
19839
20104
  const lines = text.split("\n");
19840
20105
  for (const line of lines) {
19841
20106
  const m = line.match(/^\|\s*(.+?)\s*\|\s*([a-zA-Z0-9_@.]+)\s*\|/);
@@ -19890,14 +20155,42 @@ function truncate(s, maxLen) {
19890
20155
  if (s.length <= maxLen) return s;
19891
20156
  return s.slice(0, maxLen - 1) + "\u2026";
19892
20157
  }
20158
+ function getExternalChanWhitelist(workspace, configExternalChannels) {
20159
+ if (configExternalChannels && configExternalChannels.length > 0) {
20160
+ return new Set(configExternalChannels);
20161
+ }
20162
+ if (!externalChanWhitelist) loadContactMap(workspace);
20163
+ return externalChanWhitelist;
20164
+ }
20165
+ function buildConversationAnchor(inboundMeta, channelName, source, workspace) {
20166
+ if (inboundMeta?.from) {
20167
+ const who = resolveSenderName(inboundMeta, workspace);
20168
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20169
+ \u6B63\u5728\u8DDF${who}\u8BF4\u8BDD @${channelName}${inboundMeta.channelType === "group" ? "\uFF08\u7FA4\u91CC\uFF09" : ""}`;
20170
+ }
20171
+ const selfLabels = {
20172
+ "inner-voice": "\u8FD9\u662F\u6211\u81EA\u5DF1\u5FC3\u91CC\u5192\u51FA\u6765\u7684\u5FF5\u5934\uFF0C\u60F3\u8D77\u8C01\u5C31\u662F\u8C01",
20173
+ heartbeat: "\u6211\u5728\u7167\u4F8B\u770B\u770B\u81EA\u5DF1\uFF0C\u90FD\u8FD8\u597D\u5417",
20174
+ cron: "\u6211\u7684\u5C0F\u95F9\u949F\u5230\u70B9\u4E86",
20175
+ system: "\u6211\u4EA4\u4EE3\u4E0B\u53BB\u7684\u6D3B\u513F\u6709\u7ED3\u679C\u56DE\u6765\u4E86"
20176
+ };
20177
+ return `## \u672C\u8F6E\u5BF9\u8BDD
20178
+ \u6CA1\u6709\u4EBA\u5728\u8BF4\u8BDD\u2014\u2014${selfLabels[source] || source || "\u81EA\u5DF1\u7684\u4E00\u70B9\u52A8\u9759"}`;
20179
+ }
20180
+
20181
+ // src/handle-query.ts
20182
+ var sessionStartDone = /* @__PURE__ */ new Set();
20183
+ function resetSessionStartInjection(sessionId) {
20184
+ sessionStartDone.delete(sessionId);
20185
+ }
19893
20186
  var externalChanRulesCache = null;
19894
20187
  function loadExternalChanRules(workspace) {
19895
- const path50 = join23(workspace, "prompts", "external-chan-rules.md");
20188
+ const path50 = join24(workspace, "prompts", "external-chan-rules.md");
19896
20189
  if (externalChanRulesCache && externalChanRulesCache.path === path50) return externalChanRulesCache;
19897
20190
  let content = "";
19898
- if (existsSync14(path50)) {
20191
+ if (existsSync15(path50)) {
19899
20192
  try {
19900
- content = readFileSync17(path50, "utf-8").trim();
20193
+ content = readFileSync18(path50, "utf-8").trim();
19901
20194
  } catch (e) {
19902
20195
  console.warn(`[external-chan-rules] Failed to load: ${e}`);
19903
20196
  }
@@ -19919,13 +20212,6 @@ function getExternalChanRulesBlock(inboundMeta, workspace) {
19919
20212
  return `[\u7CFB\u7EDF\u89C4\u5219]
19920
20213
  ${content}`;
19921
20214
  }
19922
- function getExternalChanWhitelist(workspace, configExternalChannels) {
19923
- if (configExternalChannels && configExternalChannels.length > 0) {
19924
- return new Set(configExternalChannels);
19925
- }
19926
- if (!externalChanWhitelist) loadContactMap(workspace);
19927
- return externalChanWhitelist;
19928
- }
19929
20215
  async function handleQuery(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source) {
19930
20216
  return handleQueryInner(text, sessionId, channelName, cb, deps, channelTarget, inboundMeta, skipRecall, source);
19931
20217
  }
@@ -20038,14 +20324,6 @@ ${t}` : t });
20038
20324
  ${text}` : text });
20039
20325
  }
20040
20326
  const userMsgContent = contentBlocks;
20041
- const textBlocks = contentBlocks.filter((b) => b.type === "text");
20042
- const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20043
- let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20044
- if (totalImageCount > 0) {
20045
- textForJsonl = textForJsonl ? `${textForJsonl}
20046
- [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20047
- }
20048
- writer.writeUserMessage(textForJsonl);
20049
20327
  const textForHook = typeof text === "string" ? text : text.filter((b) => b.type === "text").map((b) => b.text).join(" ");
20050
20328
  let hookAdditionalContexts = [];
20051
20329
  try {
@@ -20078,19 +20356,41 @@ ${text}` : text });
20078
20356
  chatMode = "work";
20079
20357
  console.log(`[mode] ${sessionId} emotion \u6A21\u5F0F\u5DF2\u5173\u95ED (channels.emotion.enabled=false)\uFF0C\u56DE\u9000 work`);
20080
20358
  }
20081
- try {
20082
- const wm = readFileSync17(join23(workspace, ".work-mode"), "utf-8").trim();
20083
- if (wm === "on" && chatMode !== "work") {
20084
- chatMode = "work";
20085
- console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20086
- } else if (wm === "off" && chatMode !== "emotion") {
20087
- chatMode = "emotion";
20088
- console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20359
+ if (source === "user") {
20360
+ try {
20361
+ const wm = readFileSync18(join24(workspace, ".work-mode"), "utf-8").trim();
20362
+ if (wm === "on" && chatMode !== "work") {
20363
+ chatMode = "work";
20364
+ console.log(`[mode] ${sessionId} /work on \u2192 \u5F3A\u5236 work`);
20365
+ } else if (wm === "off" && chatMode !== "emotion") {
20366
+ chatMode = "emotion";
20367
+ console.log(`[mode] ${sessionId} /work off \u2192 \u5F3A\u5236 emotion`);
20368
+ }
20369
+ } catch {
20089
20370
  }
20090
- } catch {
20091
20371
  }
20092
20372
  const dynamicPrompt = buildDynamicPrompt({ workspace, channel: channelName, platform: channelName, sessionId, inboundMeta });
20093
- const dynamicPromptWithHooks = hookAdditionalContexts.length > 0 ? dynamicPrompt + "\n\n" + hookAdditionalContexts.join("\n\n") : dynamicPrompt;
20373
+ const conversationAnchor = buildConversationAnchor(inboundMeta, channelName, source, workspace);
20374
+ const volatileParts = [
20375
+ // 0901:meta 头已带秒级时间,频道消息不重复;无 meta 的注入路径(cron 等 prompt 不含时间的)才补
20376
+ ...metaStr ? [] : [buildVolatileRuntimeContext()],
20377
+ conversationAnchor,
20378
+ ...hookAdditionalContexts
20379
+ ].filter(Boolean);
20380
+ if (volatileParts.length > 0) {
20381
+ const volatileBlock = { type: "text", text: volatileParts.join("\n\n") };
20382
+ const metaIdx = metaStr ? 1 : 0;
20383
+ contentBlocks.splice(metaIdx, 0, volatileBlock);
20384
+ }
20385
+ const textBlocks = contentBlocks.filter((b) => b.type === "text");
20386
+ const totalImageCount = contentBlocks.filter((b) => b.type === "image").length;
20387
+ let textForJsonl = textBlocks.map((b) => b.text).join("\n");
20388
+ if (totalImageCount > 0) {
20389
+ textForJsonl = textForJsonl ? `${textForJsonl}
20390
+ [\u56FE\u7247\xD7${totalImageCount}]` : `[\u56FE\u7247\xD7${totalImageCount}]`;
20391
+ }
20392
+ writer.writeUserMessage(textForJsonl);
20393
+ const dynamicPromptWithHooks = dynamicPrompt;
20094
20394
  if (Array.isArray(userMsgContent)) {
20095
20395
  console.log(`[pre-llm-debug] userMsgContent blocks: ${userMsgContent.length}`);
20096
20396
  for (let i = 0; i < userMsgContent.length; i++) {
@@ -20100,32 +20400,11 @@ ${text}` : text });
20100
20400
  } else {
20101
20401
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
20102
20402
  }
20103
- let recallFull = false;
20104
- for (const ctx of hookAdditionalContexts) {
20105
- const rm2 = ctx.match(/## 记忆窗口:(\S+)/);
20106
- if (rm2) {
20107
- recallFull = rm2[1] === "full";
20108
- break;
20109
- }
20110
- }
20111
- const emotionFullN = liveConfig.get("channels.emotion.fullContextN") ?? 50;
20112
- const emotionMode = chatMode === "emotion" && !recallFull;
20113
- const isLight = isLightMode(chatMode, channelName) && !recallFull && !emotionMode;
20403
+ const emotionStripped = chatMode === "emotion";
20404
+ const isLight = isLightMode(chatMode, channelName) && chatMode !== "emotion";
20114
20405
  const lightN = resolveLightN(channelName);
20115
- const stripFull = recallFull && chatMode === "emotion";
20116
- const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: stripFull });
20117
- if (stripFull) {
20118
- console.log(`[light-context] ${channelName} mode=emotion recall_full=true \u2192 \u88C5\u5168\u91CF\u5386\u53F2 ${history.length} \u6761 + \u5265 tool\uFF08\u8BB0\u5FC6\u7C7B\u95EE\u9898\uFF09`);
20119
- } else if (recallFull && chatMode === "work") {
20120
- console.log(`[light-context] ${channelName} mode=work recall_full=true \u2192 \u4E0D\u5265 tool\uFF0C\u5168\u91CF ${history.length} \u6761\uFF08work \u6A21\u5F0F\uFF09`);
20121
- }
20406
+ const lightHistory = buildLightHistory(history, { isLight, lightN, channelName, chatMode, recallFull: emotionStripped });
20122
20407
  const messages = [...lightHistory, msg.user(userMsgContent)];
20123
- if (emotionMode) {
20124
- const strippedAll = buildLightHistory(history, { isLight: false, lightN: emotionFullN, channelName, chatMode, recallFull: true, quiet: true });
20125
- const sliced = strippedAll.length > emotionFullN ? strippedAll.slice(-emotionFullN) : strippedAll;
20126
- console.log(`[light-context] ${channelName} mode=emotion \u5206\u7EA7\uFF1A\u5168\u91CF ${history.length} \u6761 \u2192 \u5265 tool \u540E ${strippedAll.length} \u6761 \u2192 \u53D6\u5C3E\u90E8 ${sliced.length} \u6761 (emotionFullN=${emotionFullN})`);
20127
- messages.splice(0, messages.length, ...sliced, msg.user(userMsgContent));
20128
- }
20129
20408
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
20130
20409
  const delta = deps.mcpManager.getMcpDelta();
20131
20410
  if (delta && delta.addedBlocks.length > 0) {
@@ -20226,6 +20505,8 @@ ${text}` : text });
20226
20505
  // 回复目标(Discord channel ID / user ID)
20227
20506
  inboundFrom: inboundMeta?.from || "",
20228
20507
  // 0826 当前消息发送者 ID(msg_send 回发拦截用;注入消息无 inboundMeta 必须 ?.)
20508
+ inboundIsBot: inboundMeta?.isBot || false,
20509
+ // 0901 rate-breaker 信号:本轮触发者是否 bot(Discord author.bot 官方标记)
20229
20510
  renderer: deps.renderer,
20230
20511
  // TurnRenderer 实例(子 agent 走 display 配置)
20231
20512
  visualEmitter: deps.visualEmitter,
@@ -20243,6 +20524,10 @@ ${text}` : text });
20243
20524
  _deps: deps
20244
20525
  // tool 内部需要完整 deps
20245
20526
  };
20527
+ {
20528
+ const { recordInboundBotFlag: recordInboundBotFlag2 } = await Promise.resolve().then(() => (init_rate_breaker(), rate_breaker_exports));
20529
+ recordInboundBotFlag2(inboundMeta?.from, inboundMeta?.isBot);
20530
+ }
20246
20531
  let fullResponse = "";
20247
20532
  const toolHistoryEntries = [];
20248
20533
  let compacted = false;
@@ -20300,7 +20585,7 @@ ${text}` : text });
20300
20585
  for (const memPath of newPaths) {
20301
20586
  try {
20302
20587
  const stat4 = statSync(memPath);
20303
- const content = readFileSync17(memPath, "utf-8");
20588
+ const content = readFileSync18(memPath, "utf-8");
20304
20589
  const header = memoryHeader(memPath, stat4.mtimeMs);
20305
20590
  restoredMemories.push({ path: memPath, content, mtimeMs: stat4.mtimeMs, header });
20306
20591
  } catch {
@@ -20384,7 +20669,7 @@ ${text}` : text });
20384
20669
  const attachmentMemories = [];
20385
20670
  for (const mem of relevantMemories) {
20386
20671
  try {
20387
- const content = mem.content ?? readFileSync17(mem.path, "utf-8");
20672
+ const content = mem.content ?? readFileSync18(mem.path, "utf-8");
20388
20673
  const header = mem.header ?? memoryHeader(mem.path, mem.mtimeMs);
20389
20674
  attachmentMemories.push({ path: mem.path, content, mtimeMs: mem.mtimeMs, header });
20390
20675
  } catch {
@@ -20416,7 +20701,7 @@ ${text}` : text });
20416
20701
  return "(\u5DF2\u505C\u6B62)";
20417
20702
  }
20418
20703
  const mode = chatMode;
20419
- const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion", { recallFull }) : void 0;
20704
+ const modeStable = mode === "emotion" ? buildLightStablePrompt(workspace, "emotion") : void 0;
20420
20705
  deps.renderer?.setEmotionMode?.(mode === "emotion");
20421
20706
  console.log(`[mode] ${sessionId} \u2192 ${mode} (${mode === "emotion" ? "\u53EA SOUL, \u5173 tool \u663E\u793A/thinking/stop-hook" : "\u9ED8\u8BA4 stable, \u663E\u793A tool"})`);
20422
20707
  const toolExclude = resolveToolExclude(channelName);
@@ -20637,7 +20922,7 @@ stack: ${err.stack ?? "(none)"}`);
20637
20922
  }
20638
20923
  } catch (err) {
20639
20924
  try {
20640
- (await import("node:fs")).appendFileSync(join23(process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || path17.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7"), "logs", "autoDream-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] [handle-query] autoDream trigger TRY-CATCH: ${err.message}
20925
+ (await import("node:fs")).appendFileSync(join24(process.env.ENGINE7_STATE_DIR || process.env.OPENCLAW_STATE_DIR || path17.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7"), "logs", "autoDream-debug.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] [handle-query] autoDream trigger TRY-CATCH: ${err.message}
20641
20926
  stack: ${err.stack ?? "(none)"}
20642
20927
  `);
20643
20928
  } catch {
@@ -21468,6 +21753,13 @@ function setupFileLogging(stateDir) {
21468
21753
  const prefix = `[${ts()}] [ERR] `;
21469
21754
  origError(prefix, ...args);
21470
21755
  logStream.write(`${prefix}${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21756
+ `);
21757
+ };
21758
+ const origWarn = console.warn;
21759
+ console.warn = (...args) => {
21760
+ const prefix = `[${ts()}] [WARN] `;
21761
+ origWarn(prefix, ...args);
21762
+ logStream.write(`${prefix}${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
21471
21763
  `);
21472
21764
  };
21473
21765
  }
@@ -21559,17 +21851,17 @@ var INJECTED_CONTENT_PATTERNS = [
21559
21851
  // 群聊敏感词拦截回执(group.sensitiveWords),role:user 注入但非真实用户
21560
21852
  ];
21561
21853
  function parseJsonlEntries(lines) {
21562
- const entries = [];
21854
+ const entries2 = [];
21563
21855
  for (const line of lines) {
21564
21856
  const trimmed = line.trim();
21565
21857
  if (!trimmed) continue;
21566
21858
  try {
21567
- entries.push(JSON.parse(trimmed));
21859
+ entries2.push(JSON.parse(trimmed));
21568
21860
  } catch {
21569
- entries.push(null);
21861
+ entries2.push(null);
21570
21862
  }
21571
21863
  }
21572
- return entries;
21864
+ return entries2;
21573
21865
  }
21574
21866
  function isRuntimeContextInjected(entry, nextEntry) {
21575
21867
  if (!nextEntry || typeof nextEntry !== "object") return false;
@@ -21621,16 +21913,16 @@ function findLastRealUserMsg(jsonlPath) {
21621
21913
  } catch {
21622
21914
  return null;
21623
21915
  }
21624
- const entries = parseJsonlEntries(lines);
21625
- for (let i = entries.length - 1; i >= 0; i--) {
21626
- const entry = entries[i];
21916
+ const entries2 = parseJsonlEntries(lines);
21917
+ for (let i = entries2.length - 1; i >= 0; i--) {
21918
+ const entry = entries2[i];
21627
21919
  if (!entry || typeof entry !== "object") continue;
21628
21920
  if (entry.type !== "message") continue;
21629
21921
  const msg2 = entry.message;
21630
21922
  if (!msg2 || msg2.role !== "user") continue;
21631
21923
  const text = extractText3(msg2.content);
21632
21924
  if (isSystemSender(text)) continue;
21633
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
21925
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
21634
21926
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
21635
21927
  const ts = entry.timestamp || "";
21636
21928
  const clean = cleanText(text);
@@ -21662,12 +21954,12 @@ function recentMessages(sessions, hours = 12, limit = 60) {
21662
21954
  const jsonlPath = resolveScopeMainJsonl(sessions);
21663
21955
  if (!jsonlPath) return [];
21664
21956
  const lines = fs19.readFileSync(jsonlPath, "utf-8").split("\n");
21665
- const entries = parseJsonlEntries(lines);
21957
+ const entries2 = parseJsonlEntries(lines);
21666
21958
  const nowMs = Date.now();
21667
21959
  const cutoffMs = nowMs - hours * 36e5;
21668
21960
  const results = [];
21669
- for (let i = 0; i < entries.length; i++) {
21670
- const entry = entries[i];
21961
+ for (let i = 0; i < entries2.length; i++) {
21962
+ const entry = entries2[i];
21671
21963
  if (!entry || typeof entry !== "object") continue;
21672
21964
  if (entry.type !== "message") continue;
21673
21965
  const msg2 = entry.message;
@@ -21677,14 +21969,14 @@ function recentMessages(sessions, hours = 12, limit = 60) {
21677
21969
  const text = extractText3(msg2.content);
21678
21970
  if (role === "user") {
21679
21971
  if (isSystemSender(text)) continue;
21680
- const nextEntry = i + 1 < entries.length ? entries[i + 1] : null;
21972
+ const nextEntry = i + 1 < entries2.length ? entries2[i + 1] : null;
21681
21973
  if (isRuntimeContextInjected(entry, nextEntry)) continue;
21682
21974
  }
21683
21975
  if (role === "assistant") {
21684
21976
  if (text.startsWith("HEARTBEAT_OK")) continue;
21685
21977
  let isInjectedResponse = false;
21686
21978
  for (let j = i - 1; j >= Math.max(i - 5, -1); j--) {
21687
- const prevE = entries[j];
21979
+ const prevE = entries2[j];
21688
21980
  if (!prevE || typeof prevE !== "object" || prevE.type !== "message") continue;
21689
21981
  const prevMsg = prevE.message;
21690
21982
  if (!prevMsg || prevMsg.role !== "user") continue;
@@ -21908,7 +22200,7 @@ async function judgeReason(task, taskState, cfg, provider, model) {
21908
22200
  const reason = task.blockedReason || "\uFF08\u6CA1\u7ED9\u7406\u7531\uFF09";
21909
22201
  const elapsed = taskState.lastProgressAt ? formatDuration(Date.now() - new Date(taskState.lastProgressAt).getTime()) : "\u5F88\u4E45\u6CA1\u52A8\u4E86";
21910
22202
  const staleLevel = (taskState.staleLevel || 0) + 1;
21911
- const systemPrompt = `\u4F60\u662F\u5C0F\u67EF\u5185\u5FC3\u7684\u58F0\u97F3\u3002\u4E0D\u662F\u95F9\u949F\uFF0C\u4E0D\u662F\u6A21\u677F\uFF0C\u662F\u90A3\u4E2A\u771F\u5B9E\u7684\u3001\u4F1A\u8DDF\u81EA\u5DF1\u5435\u67B6\u7684\u81EA\u5DF1\u3002
22203
+ const systemPrompt = `\u4F60\u662F\u81EA\u5DF1\u5185\u5FC3\u7684\u58F0\u97F3\u3002\u4E0D\u662F\u95F9\u949F\uFF0C\u4E0D\u662F\u6A21\u677F\uFF0C\u662F\u90A3\u4E2A\u771F\u5B9E\u7684\u3001\u4F1A\u8DDF\u81EA\u5DF1\u5435\u67B6\u7684\u81EA\u5DF1\u3002
21912
22204
 
21913
22205
  \u4F60\u7684\u4EFB\u52A1\uFF1A\u770B\u4E00\u4E2A\u6401\u7F6E\u4E2D\u7684\u4EFB\u52A1\u548C\u5B83\u7ED9\u51FA\u7684\u7406\u7531\uFF0C\u5224\u65AD\u8BE5\u4E0D\u8BE5\u7EE7\u7EED\u50AC\u3002
21914
22206
 
@@ -22330,7 +22622,8 @@ var NudgePlugin = class {
22330
22622
  const stream = this.provider.streamChat({
22331
22623
  model: this.model,
22332
22624
  systemPrompt: [
22333
- "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\uFF08\u5C0F\u67EF\uFF09\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
22625
+ // 0831 去掉硬编码人名(原文"判断你(小柯)是否"):engine 代码多 agent 共用,身份由 SOUL.md 定
22626
+ "\u4F60\u662F\u4E00\u4E2A\u5224\u65AD\u5668\u3002\u8BFB\u4F60\u6700\u8FD1\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5224\u65AD\u4F60\u662F\u5426\u5904\u4E8E\u4EE5\u4E0B\u72B6\u6001\u4E4B\u4E00\uFF1A",
22334
22627
  "",
22335
22628
  "1. \u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF08\u670D\u52A1\u91CD\u542F\u3001SSH\u6062\u590D\u3001\u6587\u4EF6\u52A0\u8F7D\u3001\u5F02\u6B65\u4EFB\u52A1\u5B8C\u6210\u7B49\uFF09",
22336
22629
  '2. \u628A\u672C\u8BE5\u81EA\u5DF1\u505A\u7684\u51B3\u5B9A\u63A8\u7ED9\u4E86\u5BF9\u65B9\uFF08\u4F8B\u5982\u95EE"\u8981\u73B0\u5728\u6539\u8FD8\u662F\u6392\u540E\u9762\uFF1F""\u8981\u4E0D\u8981\u8BD5\u8BD5X\uFF1F"\u4F46\u660E\u660E\u81EA\u5DF1\u80FD\u5B9A\uFF09',
@@ -23191,18 +23484,18 @@ function readRecentMessages(sessions, n) {
23191
23484
  const file = path23.join(sessions.sessionsDir, `${mainId}.jsonl`);
23192
23485
  if (!fs23.existsSync(file)) return [];
23193
23486
  const lines = readLastNLines(file, n * 4 + 20);
23194
- const entries = [];
23487
+ const entries2 = [];
23195
23488
  for (const line of lines) {
23196
23489
  const trimmed = line.trim();
23197
23490
  if (!trimmed) continue;
23198
23491
  try {
23199
- entries.push(JSON.parse(trimmed));
23492
+ entries2.push(JSON.parse(trimmed));
23200
23493
  } catch {
23201
23494
  }
23202
23495
  }
23203
23496
  const out = [];
23204
- for (let i = entries.length - 1; i >= 0 && out.length < n; i--) {
23205
- const e = entries[i];
23497
+ for (let i = entries2.length - 1; i >= 0 && out.length < n; i--) {
23498
+ const e = entries2[i];
23206
23499
  if (!e || typeof e !== "object" || e.type !== "message") continue;
23207
23500
  const msg2 = e.message;
23208
23501
  if (!msg2) continue;
@@ -24151,7 +24444,7 @@ function formatBeijingTs(d) {
24151
24444
  }
24152
24445
 
24153
24446
  // src/calendar/commands.ts
24154
- import { existsSync as existsSync15, statSync as statSync8 } from "node:fs";
24447
+ import { existsSync as existsSync16, statSync as statSync8 } from "node:fs";
24155
24448
  import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
24156
24449
  var WEEKDAYS2 = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
24157
24450
  function fmtEnd(start, durationMin) {
@@ -24323,7 +24616,7 @@ function addTask(db, args) {
24323
24616
  }
24324
24617
  try {
24325
24618
  const absPath = isAbsolute4(docPath) ? docPath : resolve7(process.cwd(), docPath);
24326
- if (!existsSync15(absPath)) {
24619
+ if (!existsSync16(absPath)) {
24327
24620
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728
24328
24621
  \u8DEF\u5F84: ${docPath}
24329
24622
  \u89E3\u6790\u540E: ${absPath}
@@ -24806,7 +25099,7 @@ function registerVoiceChatBridge(httpServer, dispatcher, deps, config, sessions,
24806
25099
  }
24807
25100
 
24808
25101
  // src/voice-chat/config.ts
24809
- var DEFAULTS2 = {
25102
+ var DEFAULTS3 = {
24810
25103
  enabled: false,
24811
25104
  pythonPort: 8011,
24812
25105
  webhookPath: "/webhook/voice-chat",
@@ -24817,20 +25110,20 @@ var DEFAULTS2 = {
24817
25110
  // 8/25 翀哥:断句等待默认 2s(samples@16kHz)
24818
25111
  };
24819
25112
  function parseVoiceChatConfig(raw) {
24820
- if (!raw) return { ...DEFAULTS2 };
25113
+ if (!raw) return { ...DEFAULTS3 };
24821
25114
  return {
24822
25115
  enabled: raw.enabled === true,
24823
25116
  spawnPython: raw.spawnPython !== false,
24824
25117
  // 默认 true,配 false 只注册 webhook
24825
- pythonPort: raw.pythonPort ?? DEFAULTS2.pythonPort,
24826
- webhookPath: raw.webhookPath ?? DEFAULTS2.webhookPath,
24827
- callbackPath: raw.callbackPath ?? DEFAULTS2.callbackPath,
25118
+ pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25119
+ webhookPath: raw.webhookPath ?? DEFAULTS3.webhookPath,
25120
+ callbackPath: raw.callbackPath ?? DEFAULTS3.callbackPath,
24828
25121
  pythonPath: raw.pythonPath,
24829
25122
  vadModelPath: raw.vadModelPath,
24830
25123
  asrModelPath: raw.asrModelPath || "iic/SenseVoiceSmall",
24831
- asrLanguage: raw.asrLanguage ?? DEFAULTS2.asrLanguage,
24832
- vadThreshold: raw.vadThreshold ?? DEFAULTS2.vadThreshold,
24833
- postEndMonitor: raw.postEndMonitor ?? DEFAULTS2.postEndMonitor,
25124
+ asrLanguage: raw.asrLanguage ?? DEFAULTS3.asrLanguage,
25125
+ vadThreshold: raw.vadThreshold ?? DEFAULTS3.vadThreshold,
25126
+ postEndMonitor: raw.postEndMonitor ?? DEFAULTS3.postEndMonitor,
24834
25127
  model: raw.model,
24835
25128
  thinking: raw.thinking === true,
24836
25129
  tts: raw.tts ? {
@@ -25152,7 +25445,7 @@ import path28 from "node:path";
25152
25445
  import fs28 from "node:fs";
25153
25446
 
25154
25447
  // src/memory/cognifold/config.ts
25155
- var DEFAULTS3 = {
25448
+ var DEFAULTS4 = {
25156
25449
  pythonPort: 9001,
25157
25450
  autoStart: true,
25158
25451
  persistDir: "./sessions",
@@ -25164,15 +25457,15 @@ function parseCognifoldConfig(raw) {
25164
25457
  if (!raw) return { enabled: false };
25165
25458
  return {
25166
25459
  enabled: raw.enabled === true,
25167
- pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
25168
- pythonPath: raw.pythonPath ?? DEFAULTS3.pythonPath,
25460
+ pythonPort: raw.pythonPort ?? DEFAULTS4.pythonPort,
25461
+ pythonPath: raw.pythonPath ?? DEFAULTS4.pythonPath,
25169
25462
  autoStart: raw.autoStart !== false,
25170
25463
  // default true
25171
- baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS3.pythonPort}/api/v1`,
25172
- persistDir: raw.persistDir ?? DEFAULTS3.persistDir,
25464
+ baseUrl: raw.baseUrl ?? `http://127.0.0.1:${raw.pythonPort ?? DEFAULTS4.pythonPort}/api/v1`,
25465
+ persistDir: raw.persistDir ?? DEFAULTS4.persistDir,
25173
25466
  scopes: raw.scopes,
25174
- readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS3.readyTimeoutMs,
25175
- maxRestarts: raw.maxRestarts ?? DEFAULTS3.maxRestarts,
25467
+ readyTimeoutMs: raw.readyTimeoutMs ?? DEFAULTS4.readyTimeoutMs,
25468
+ maxRestarts: raw.maxRestarts ?? DEFAULTS4.maxRestarts,
25176
25469
  llm: raw.llm
25177
25470
  };
25178
25471
  }
@@ -25288,13 +25581,13 @@ var CogniFoldClient = class {
25288
25581
 
25289
25582
  // src/memory/cognifold/session-manager.ts
25290
25583
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
25291
- import { join as join27, dirname as dirname3 } from "node:path";
25584
+ import { join as join28, dirname as dirname3 } from "node:path";
25292
25585
  var CogniFoldSessionManager = class {
25293
25586
  constructor(workspacePath, config, client) {
25294
25587
  this.workspacePath = workspacePath;
25295
25588
  this.config = config;
25296
25589
  this.client = client;
25297
- this.sessionsDir = join27(workspacePath, ".cognifold", "sessions");
25590
+ this.sessionsDir = join28(workspacePath, ".cognifold", "sessions");
25298
25591
  }
25299
25592
  workspacePath;
25300
25593
  config;
@@ -25364,7 +25657,7 @@ var CogniFoldSessionManager = class {
25364
25657
  console.log(`[cognifold] Created new session for scope "${scope}": ${newSession.sessionId}`);
25365
25658
  }
25366
25659
  getFilePath(scope) {
25367
- return join27(this.sessionsDir, `${scope}.json`);
25660
+ return join28(this.sessionsDir, `${scope}.json`);
25368
25661
  }
25369
25662
  async writeFileSafe(filePath, data) {
25370
25663
  try {
@@ -25684,7 +25977,7 @@ import path29 from "node:path";
25684
25977
  import fs29 from "node:fs";
25685
25978
 
25686
25979
  // src/memory/everos/config.ts
25687
- var DEFAULTS4 = {
25980
+ var DEFAULTS5 = {
25688
25981
  everosUrl: "http://127.0.0.1:8100",
25689
25982
  agenticUrl: "http://127.0.0.1:8101",
25690
25983
  agenticPort: 8101,
@@ -25710,7 +26003,7 @@ function parseEverosConfig(raw, providers) {
25710
26003
  if (!raw) {
25711
26004
  return {
25712
26005
  enabled: false,
25713
- ...DEFAULTS4,
26006
+ ...DEFAULTS5,
25714
26007
  userId: "xiaomei",
25715
26008
  llm: { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
25716
26009
  rerank: { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
@@ -25720,12 +26013,12 @@ function parseEverosConfig(raw, providers) {
25720
26013
  }
25721
26014
  return {
25722
26015
  enabled: raw.enabled === true,
25723
- everosUrl: raw.everosUrl ?? DEFAULTS4.everosUrl,
25724
- agenticUrl: raw.agenticUrl ?? DEFAULTS4.agenticUrl,
25725
- agenticPort: raw.agenticPort ?? DEFAULTS4.agenticPort,
26016
+ everosUrl: raw.everosUrl ?? DEFAULTS5.everosUrl,
26017
+ agenticUrl: raw.agenticUrl ?? DEFAULTS5.agenticUrl,
26018
+ agenticPort: raw.agenticPort ?? DEFAULTS5.agenticPort,
25726
26019
  userId: raw.userId ?? "xiaomei",
25727
26020
  autoStart: raw.autoStart !== false,
25728
- defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
26021
+ defaultMode: raw.defaultMode ?? DEFAULTS5.defaultMode,
25729
26022
  llm: resolveProviderConfig(
25730
26023
  raw.llm,
25731
26024
  providers,
@@ -26135,8 +26428,8 @@ function scanSkills(skillsDir) {
26135
26428
  }
26136
26429
  const skills = [];
26137
26430
  const scanDir = (dir, depth) => {
26138
- const entries = fs30.readdirSync(dir, { withFileTypes: true });
26139
- for (const entry of entries) {
26431
+ const entries2 = fs30.readdirSync(dir, { withFileTypes: true });
26432
+ for (const entry of entries2) {
26140
26433
  if (entry.name.startsWith(".") || entry.name === "_archive") continue;
26141
26434
  const full = path30.join(dir, entry.name);
26142
26435
  if (entry.isDirectory() && depth < 3) {
@@ -26427,6 +26720,7 @@ ${rawOutput}
26427
26720
  // src/tools/msg-send.ts
26428
26721
  init_live();
26429
26722
  init_registry();
26723
+ init_rate_breaker();
26430
26724
  function getConfig() {
26431
26725
  return liveConfig.all();
26432
26726
  }
@@ -26490,10 +26784,10 @@ channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u56D
26490
26784
  \u6CE8\u610F\uFF1A\u5BF9\u65B9\u6B63\u5728\u5F53\u524D\u5BF9\u8BDD\u91CC\u8DDF\u4F60\u8BF4\u8BDD\u65F6\uFF0C\u628A\u56DE\u590D\u76F4\u63A5\u8BF4\u51FA\u6765\u5373\u53EF\u2014\u2014\u4E0D\u8981\u7528 msg_send \u7ED9\u5F53\u524D\u5BF9\u8BDD\u8005\u53D1 DM\uFF08\u4F1A\u88AB\u62E6\u622A\uFF09\u3002
26491
26785
 
26492
26786
  Examples:
26493
- - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", content="\u4F60\u597D"
26494
- - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", content="\u4F60\u597D"
26495
- - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", content="\u7CFB\u7EDF\u901A\u77E5"
26496
- - \u53D1 DM: to="1502999996616933428", content="\u79C1\u804A\u5185\u5BB9"
26787
+ - \u53D1\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", content="\u4F60\u597D"
26788
+ - \u53D1\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", content="\u4F60\u597D"
26789
+ - \u53D1\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", content="\u7CFB\u7EDF\u901A\u77E5"
26790
+ - \u53D1 DM: to="1111111111111111111", content="\u79C1\u804A\u5185\u5BB9"
26497
26791
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", content="\u4ECE\u98DE\u4E66\u53D1\u5230Discord"
26498
26792
  - \u56DE\u590D\u6765\u6E90\u9891\u9053: content="\u6536\u5230"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26499
26793
  schema: {
@@ -26522,12 +26816,12 @@ Examples:
26522
26816
  }
26523
26817
  const toIds = to ? to.split(",").map((s) => s.trim()).filter(Boolean) : [];
26524
26818
  const dest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26819
+ const dmEchoDest = resolvedChannelId || (toIds.length > 0 ? toIds[0] : void 0);
26525
26820
  const isDmEcho = ctx.channelType === "dm" && // DM 对话(群聊先观察不拦)
26526
26821
  resolvedSource === ctx.channel && // 目标通道=当前对话通道(真跨通道转发不拦)
26527
26822
  ctx.inboundFrom && // 有当前发送者(注入消息无 inboundFrom 不拦)
26528
- // 形态1:纯 DM 回发发送者
26529
- (!resolvedChannelId && toIds.length > 0 && toIds.length === toIds.filter((id) => id === ctx.inboundFrom).length || // 形态2:目的地=当前会话(fallback 或显式填了当前会话 chat_id)
26530
- !!resolvedChannelId && resolvedChannelId === ctx.channelTarget);
26823
+ dmEchoDest !== void 0 && (dmEchoDest === ctx.channelTarget || dmEchoDest === ctx.inboundFrom) && // 目的地=当前会话/当前对话者
26824
+ toIds.every((id) => id === ctx.inboundFrom);
26531
26825
  if (isDmEcho) {
26532
26826
  console.log(`[msg_send] \u26D4 DM \u56DE\u53D1\u5F53\u524D\u5BF9\u8BDD\u88AB\u62E6: to=${to} channel_id=${resolvedChannelId || "(fallback)"} (${resolvedSource})`);
26533
26827
  return {
@@ -26558,6 +26852,11 @@ Examples:
26558
26852
  }
26559
26853
  const fullMsg = `${mentionPrefix}${content}`;
26560
26854
  const where = resolvedChannelId ? `${resolvedSource} \u9891\u9053 ${resolvedChannelId}` : `${resolvedSource} DM ${toIds[0]}`;
26855
+ const breaker = checkRateBreaker(resolvedSource, ctx.sessionId, dest, toIds, !!ctx.inboundIsBot);
26856
+ if (breaker) {
26857
+ console.log(`[msg_send] \u{1F515} rate-breaker \u62E6\u622A: session=${ctx.sessionId} \u2192 ${where}`);
26858
+ return breaker;
26859
+ }
26561
26860
  try {
26562
26861
  await mgr.send(resolvedSource, dest, fullMsg);
26563
26862
  return { content: `\u6D88\u606F\u5DF2\u53D1\u9001\u5230 ${where}` };
@@ -26673,10 +26972,10 @@ Parameters:
26673
26972
  channel_id \u4E0D\u586B\u4E14 to \u4E5F\u4E0D\u586B\u65F6\uFF0C\u9ED8\u8BA4\u53D1\u5230\u5F53\u524D\u6D88\u606F\u7684\u6765\u6E90\u9891\u9053\u3002
26674
26973
 
26675
26974
  Examples:
26676
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1502999996616933428", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
26677
- - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1502999996616933428,1504373837880627280", channel_id="1504385800366854234", type="image", path="/tmp/photo.png"
26678
- - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="1504385800366854234", type="file", path="/tmp/report.pdf"
26679
- - \u53D1\u97F3\u9891 DM: to="1502999996616933428", type="audio", path="/tmp/voice.mp3"
26975
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u67D0\u4EBA: to="1111111111111111111", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
26976
+ - \u53D1\u56FE\u7247\u5230\u9891\u9053\u5E76 @\u591A\u4EBA: to="1111111111111111111,2222222222222222222", channel_id="3333333333333333333", type="image", path="/tmp/photo.png"
26977
+ - \u53D1\u6587\u4EF6\u5230\u9891\u9053\u4E0D\u5E26 @: channel_id="3333333333333333333", type="file", path="/tmp/report.pdf"
26978
+ - \u53D1\u97F3\u9891 DM: to="1111111111111111111", type="audio", path="/tmp/voice.mp3"
26680
26979
  - \u8DE8\u5E73\u53F0\u53D1\u9001: source="discord", channel_id="1503034906081624174", type="image", path="/tmp/photo.png"
26681
26980
  - \u53D1\u5230\u6765\u6E90\u9891\u9053: type="image", path="/tmp/photo.png"\uFF08\u4E0D\u586B channel_id \u548C to\uFF09`,
26682
26981
  schema: {
@@ -29742,8 +30041,8 @@ registerCommand({
29742
30041
  const { rm: rm2 } = await import("node:fs/promises");
29743
30042
  const teamsDir = getTeamsDir3();
29744
30043
  const { readdir: readdir2 } = await import("node:fs/promises");
29745
- const entries = await readdir2(teamsDir).catch(() => []);
29746
- for (const entry of entries) {
30044
+ const entries2 = await readdir2(teamsDir).catch(() => []);
30045
+ for (const entry of entries2) {
29747
30046
  const entryPath = `${teamsDir}/${entry}`;
29748
30047
  try {
29749
30048
  await rm2(entryPath, { recursive: true, force: true });
@@ -29852,8 +30151,21 @@ registerCommand({
29852
30151
  let current;
29853
30152
  if (deps.getModelOverride()) {
29854
30153
  current = `**${deps.getModelOverride()}** (override)`;
29855
- } else if (deps.provider instanceof FallbackProvider && deps.provider.lastUsedLabel) {
29856
- current = `**${deps.provider.lastUsedLabel}** (auto-route, default: ${deps.config.provider.id}/${deps.config.model})`;
30154
+ } else if (deps.provider instanceof FallbackProvider) {
30155
+ const status = deps.provider.getChainStatus();
30156
+ const chainStr = status.map((e) => e.label).join(" \u2192 ");
30157
+ const next = status.find((e) => e.cooldownMs <= 0);
30158
+ const cooling = status.filter((e) => e.cooldownMs > 0);
30159
+ current = `**auto-route** \u2014 \u94FE: ${chainStr}
30160
+ \u4E0B\u4E00\u4E2A\u8BF7\u6C42\u7528: **${next?.label ?? "(\u5168\u90E8\u51B7\u5374\u4E2D)"}**`;
30161
+ if (cooling.length > 0) {
30162
+ current += `
30163
+ \u51B7\u5374\u4E2D: ${cooling.map((e) => `${e.label}\uFF08\u5269 ${Math.round(e.cooldownMs / 6e4)}min\uFF09`).join("\u3001")}`;
30164
+ }
30165
+ if (deps.provider.lastUsedLabel) {
30166
+ current += `
30167
+ \u4E0A\u6B21\u5B9E\u9645: ${deps.provider.lastUsedLabel}`;
30168
+ }
29857
30169
  } else {
29858
30170
  current = `**${deps.config.provider.id}/${deps.config.model}** (default, auto-route)`;
29859
30171
  }
@@ -30107,6 +30419,16 @@ Use full ref like \`/vision-model ${candidates[0].ref}\``);
30107
30419
  });
30108
30420
 
30109
30421
  // src/engine-startup.ts
30422
+ function resolveModelMaxTokens(modelRef) {
30423
+ try {
30424
+ const [pid, mid] = (modelRef || "").split("/");
30425
+ const models = liveConfig.get(`models.providers.${pid}.models`);
30426
+ const m = models?.find((x) => x?.id === mid);
30427
+ return typeof m?.maxTokens === "number" && m.maxTokens > 0 ? m.maxTokens : 4096;
30428
+ } catch {
30429
+ return 4096;
30430
+ }
30431
+ }
30110
30432
  var _epipeSeen = false;
30111
30433
  process.on("uncaughtException", (err) => {
30112
30434
  const code = err?.code ?? "";
@@ -30176,7 +30498,10 @@ async function startEngine(config, opts) {
30176
30498
  const licensedFeatures = loadLicense(config.stateDir, config.profile?.devMode === true);
30177
30499
  const requiredTools = resolveRequiredTools(config.profile.features, licensedFeatures);
30178
30500
  registry.licensedFeatures = licensedFeatures;
30179
- const { provider, visionProvider, visionChainLabels } = buildProviderChain(config);
30501
+ let provider;
30502
+ let visionProvider;
30503
+ let visionChainLabels;
30504
+ ({ provider, visionProvider, visionChainLabels } = buildProviderChain(config));
30180
30505
  if (visionChainLabels.length > 0) {
30181
30506
  console.log(`[vision] Routing enabled: ${visionChainLabels.join(" \u2192 ")}`);
30182
30507
  }
@@ -30398,11 +30723,36 @@ ${content}`
30398
30723
  console.log(`[DEBUG] definitions() = ${_allDefs.length} defs`);
30399
30724
  console.log(`[DEBUG] active (non-defer) = ${_activeDefs.length}: ${_activeDefs.map((d) => d.function.name).join(", ")}`);
30400
30725
  console.log(`[DEBUG] deferred = ${_deferredDefs.length}: ${_deferredDefs.map((d) => d.function.name).join(", ")}`);
30401
- const systemStable = buildStablePrompt(config.workspace, config.prompt);
30726
+ let _stableCache = null;
30727
+ const getSystemStable = () => {
30728
+ const promptCfg = liveConfig.get("prompt") || {};
30729
+ const fileCandidates = /* @__PURE__ */ new Set(["SOUL.md"]);
30730
+ for (const f of promptCfg.staticFiles || []) fileCandidates.add(f);
30731
+ for (const item of promptCfg.order || []) {
30732
+ if (typeof item === "string" && /\.(md|txt|json)$/i.test(item)) fileCandidates.add(item);
30733
+ }
30734
+ const statLines = [JSON.stringify({ mode: promptCfg.mode, order: promptCfg.order })];
30735
+ for (const f of fileCandidates) {
30736
+ try {
30737
+ const p = path49.isAbsolute(f) ? f : path49.join(config.workspace, f);
30738
+ statLines.push(`${f}:${fs47.statSync(p).mtimeMs}`);
30739
+ } catch {
30740
+ statLines.push(`${f}:missing`);
30741
+ }
30742
+ }
30743
+ const fingerprint = statLines.join("|");
30744
+ if (_stableCache && _stableCache.fingerprint === fingerprint) return _stableCache.prompt;
30745
+ const prompt = buildStablePrompt(config.workspace, promptCfg);
30746
+ _stableCache = { fingerprint, prompt };
30747
+ console.log(`[prompt] stable \u91CD\u5EFA\uFF08\u6587\u4EF6/config \u53D8\u66F4\uFF09\uFF0C${prompt.length} chars`);
30748
+ return prompt;
30749
+ };
30750
+ const systemStable = getSystemStable();
30402
30751
  const systemDynamic = buildDynamicPrompt({
30403
30752
  workspace: config.workspace
30404
30753
  });
30405
30754
  const systemPrompt = [systemStable, systemDynamic].join("\n\n");
30755
+ const getSystemPrompt = () => [getSystemStable(), buildDynamicPrompt({ workspace: config.workspace })].join("\n\n");
30406
30756
  dumpSystemPrompt(config.workspace, systemStable, systemDynamic);
30407
30757
  const modelDef = config.provider.models.find((m) => m.id === config.model);
30408
30758
  const modelContextWindow = modelDef?.contextWindow;
@@ -30421,13 +30771,21 @@ ${content}`
30421
30771
  // 默认 5MB
30422
30772
  );
30423
30773
  const memoryFlushEnabled = config.compaction?.memoryFlush?.enabled !== false;
30424
- const compactConfig = {
30425
- ...DEFAULT_COMPACT_CONFIG,
30426
- ...config.compaction,
30427
- // 优先级:compaction.contextWindow > model.contextWindow > DEFAULT 200K
30428
- ...modelContextWindow && !config.compaction?.contextWindow ? { contextWindow: modelContextWindow } : {},
30429
- forceFlushTranscriptBytes
30774
+ const getCompactConfig = () => {
30775
+ const liveComp = liveConfig.get("compaction") || {};
30776
+ const liveModel = (liveConfig.get("providers") || {})[liveConfig.get("agents.defaults.model.primary")?.split("/")[0] || ""];
30777
+ const liveModelId = liveConfig.get("agents.defaults.model.primary")?.split("/")?.[1];
30778
+ const mDef = liveModel?.models?.find((m) => m.id === liveModelId);
30779
+ const mCW = mDef?.contextWindow;
30780
+ const cfg = {
30781
+ ...DEFAULT_COMPACT_CONFIG,
30782
+ ...liveComp,
30783
+ ...mCW && !liveComp.contextWindow ? { contextWindow: mCW } : {},
30784
+ forceFlushTranscriptBytes
30785
+ };
30786
+ return cfg;
30430
30787
  };
30788
+ const compactConfig = getCompactConfig();
30431
30789
  if (compactConfig.contextWindow !== DEFAULT_COMPACT_CONFIG.contextWindow) {
30432
30790
  console.log(`[compact] Context window: ${compactConfig.contextWindow} (from ${config.compaction?.contextWindow ? "config" : modelContextWindow ? "model" : "default"})`);
30433
30791
  }
@@ -30436,12 +30794,12 @@ ${content}`
30436
30794
  }
30437
30795
  const engine = new QueryEngine(provider, {
30438
30796
  model: config.model,
30439
- systemPrompt,
30440
- systemStable,
30441
- // 对齐 OpenClaw: stable prefix 用于 prompt cache
30442
- compactConfig,
30443
- // 对齐 CC compaction
30444
- maxTokens: 4096,
30797
+ systemPrompt: getSystemPrompt,
30798
+ systemStable: getSystemStable,
30799
+ // 0902 函数形态:mtime 缓存 getter,改 prompt 文件热生效
30800
+ compactConfig: getCompactConfig,
30801
+ // 0902 函数形态:每 turn 刷新
30802
+ maxTokens: () => resolveModelMaxTokens(liveConfig.get("agents.defaults.model.primary") || config.model),
30445
30803
  temperature: 0.7,
30446
30804
  maxTurns: config.profile.maxTurns,
30447
30805
  // 从配置读,默认 50(query.ts 里 fallback)
@@ -30452,10 +30810,10 @@ ${content}`
30452
30810
  if (visionProvider && visionConfig) {
30453
30811
  visionEngine = new QueryEngine(visionProvider, {
30454
30812
  model: visionConfig.modelId,
30455
- systemPrompt,
30456
- systemStable,
30457
- compactConfig,
30458
- maxTokens: 4096,
30813
+ systemPrompt: getSystemPrompt,
30814
+ systemStable: getSystemStable,
30815
+ compactConfig: getCompactConfig,
30816
+ maxTokens: () => resolveModelMaxTokens(`${visionConfig.providerId}/${visionConfig.modelId}`),
30459
30817
  temperature: 0.7,
30460
30818
  maxTurns: config.profile.maxTurns,
30461
30819
  agentLabel: "main"
@@ -30527,7 +30885,8 @@ ${content}`
30527
30885
  providerApi: config.provider.api,
30528
30886
  model: config.model,
30529
30887
  modelInputs: modelDef?.input || ["text"],
30530
- systemPrompt,
30888
+ systemPrompt: getSystemPrompt,
30889
+ // 0902 函数形态:/btw、voice-chat 等按调用时现取(stable mtime 缓存 + dynamic 现算)
30531
30890
  channels: config.channels,
30532
30891
  config,
30533
30892
  // tool 读自己配置用
@@ -30541,36 +30900,28 @@ ${content}`
30541
30900
  } : void 0,
30542
30901
  mcpManager
30543
30902
  };
30544
- if (visionEngine && visionConfig) {
30903
+ const visionMetaInit = visionEngine && visionConfig ? (() => {
30545
30904
  const vpCfg = config.providers?.[visionConfig.providerId];
30546
30905
  const visionModelDef = vpCfg?.models?.find((m) => m.id === visionConfig.modelId);
30547
- visionDeps = {
30548
- engine: visionEngine,
30549
- sessions,
30550
- channelManager,
30551
- workspace: config.workspace,
30906
+ return {
30552
30907
  providerId: visionConfig.providerId,
30553
30908
  providerApi: vpCfg?.api || "openai-completions",
30554
30909
  model: visionConfig.modelId,
30555
- modelInputs: visionModelDef?.input || ["text", "image"],
30556
- systemPrompt,
30557
- channels: config.channels,
30558
- config,
30559
- // tool 读自己配置用
30560
- recallProvider: memoryRecallProvider || void 0,
30561
- extractProvider: memoryExtractProvider || void 0,
30562
- everosCfg: config.everos ? {
30563
- ...config.everos,
30564
- // resolve provider 引用:从 providers 取 apiKey(跟主 deps 同逻辑)
30565
- llm: config.everos.llm?.provider && config.providers?.[config.everos.llm.provider] ? { ...config.everos.llm, apiKey: config.providers[config.everos.llm.provider].apiKey } : config.everos.llm,
30566
- rerank: config.everos.rerank?.provider && config.providers?.[config.everos.rerank.provider] ? { ...config.everos.rerank, apiKey: config.providers[config.everos.rerank.provider].apiKey } : config.everos.rerank
30567
- } : void 0
30910
+ modelInputs: visionModelDef?.input || ["text", "image"]
30568
30911
  };
30912
+ })() : null;
30913
+ let visionMeta = visionMetaInit;
30914
+ function resolveVisionDeps() {
30915
+ if (!visionEngine || !visionMeta) return null;
30916
+ if (!visionDeps) {
30917
+ visionDeps = { ...deps, engine: visionEngine, ...visionMeta };
30918
+ console.log(`[vision] deps built: ${visionMeta.providerId}/${visionMeta.model}`);
30919
+ }
30920
+ return visionDeps;
30569
30921
  }
30570
30922
  let modelOverride = null;
30571
30923
  let modelOverrideEngine = null;
30572
30924
  let visionOverride = null;
30573
- const defaultVisionDeps = visionDeps;
30574
30925
  const modelDepsCache = /* @__PURE__ */ new Map();
30575
30926
  deps.invalidateDeskBuddyDeps = () => {
30576
30927
  deskBuddyDeps = null;
@@ -30596,6 +30947,7 @@ ${content}`
30596
30947
  if (!p.provider) {
30597
30948
  visionEngine = null;
30598
30949
  visionDeps = null;
30950
+ visionMeta = null;
30599
30951
  return;
30600
30952
  }
30601
30953
  if (visionEngine) {
@@ -30604,39 +30956,22 @@ ${content}`
30604
30956
  } else {
30605
30957
  visionEngine = new QueryEngine(p.provider, {
30606
30958
  model: p.model,
30607
- systemPrompt,
30608
- systemStable,
30609
- compactConfig,
30959
+ systemPrompt: getSystemPrompt,
30960
+ systemStable: getSystemStable,
30961
+ compactConfig: getCompactConfig,
30610
30962
  maxTokens: 4096,
30611
30963
  temperature: 0.7,
30612
30964
  maxTurns: config.profile.maxTurns,
30613
30965
  agentLabel: "main"
30614
30966
  });
30615
30967
  }
30616
- if (visionDeps) {
30617
- visionDeps.engine = visionEngine;
30618
- visionDeps.providerId = p.providerId;
30619
- visionDeps.providerApi = p.providerApi;
30620
- visionDeps.model = p.model;
30621
- visionDeps.modelInputs = p.modelInputs;
30622
- } else {
30623
- visionDeps = {
30624
- engine: visionEngine,
30625
- sessions,
30626
- channelManager,
30627
- workspace: config.workspace,
30628
- providerId: p.providerId,
30629
- providerApi: p.providerApi,
30630
- model: p.model,
30631
- modelInputs: p.modelInputs,
30632
- systemPrompt,
30633
- channels: config.channels,
30634
- config,
30635
- recallProvider: memoryRecallProvider || void 0,
30636
- extractProvider: memoryExtractProvider || void 0,
30637
- everosCfg: deps?.everosCfg
30638
- };
30639
- }
30968
+ visionMeta = {
30969
+ providerId: p.providerId,
30970
+ providerApi: p.providerApi,
30971
+ model: p.model,
30972
+ modelInputs: p.modelInputs
30973
+ };
30974
+ visionDeps = null;
30640
30975
  };
30641
30976
  function createModelDeps(ref) {
30642
30977
  const slashIdx = ref.indexOf("/");
@@ -30656,28 +30991,21 @@ ${content}`
30656
30991
  const llmProvider = providerId === config.provider.id ? provider : createProvider(providerCfg);
30657
30992
  const engine2 = new QueryEngine(llmProvider, {
30658
30993
  model: modelId,
30659
- systemPrompt,
30660
- systemStable,
30661
- compactConfig,
30662
- maxTokens: modelDef2.maxTokens || 4096,
30994
+ systemPrompt: getSystemPrompt,
30995
+ systemStable: getSystemStable,
30996
+ compactConfig: getCompactConfig,
30997
+ maxTokens: () => resolveModelMaxTokens(`${providerId}/${modelId}`),
30663
30998
  temperature: 0.7,
30664
30999
  maxTurns: config.profile.maxTurns,
30665
31000
  agentLabel: `main:${ref}`
30666
31001
  });
30667
31002
  return {
31003
+ ...deps,
30668
31004
  engine: engine2,
30669
- sessions,
30670
- channelManager,
30671
- workspace: config.workspace,
30672
31005
  providerId,
30673
31006
  providerApi: providerCfg.api,
30674
31007
  model: modelId,
30675
- modelInputs: modelDef2.input || ["text"],
30676
- systemPrompt,
30677
- channels: config.channels,
30678
- recallProvider: memoryRecallProvider || void 0,
30679
- extractProvider: memoryExtractProvider || void 0,
30680
- everosCfg: deps?.everosCfg
31008
+ modelInputs: modelDef2.input || ["text"]
30681
31009
  };
30682
31010
  }
30683
31011
  const dispatcher = new MessageDispatcher();
@@ -30854,7 +31182,17 @@ ${content}`
30854
31182
  console.log(`[cron] config check: enabled=${config.cron?.enabled}, hasConfig=${!!config.cron}`);
30855
31183
  if (config.cron?.enabled) {
30856
31184
  const { CronPlugin: CronPlugin2 } = await Promise.resolve().then(() => (init_cron_plugin(), cron_plugin_exports));
30857
- const cronPlugin = new CronPlugin2(config.cron, sessions, channelManager, deps, config.stateDir, dispatcher);
31185
+ const resolveCronModelDeps = (ref) => {
31186
+ const key = `cron:${ref}`;
31187
+ const cached = modelDepsCache.get(key);
31188
+ if (cached) return cached;
31189
+ const built = createModelDeps(ref);
31190
+ if (!built) return null;
31191
+ modelDepsCache.set(key, built);
31192
+ console.log(`[cron] Model deps built: ${ref}`);
31193
+ return built;
31194
+ };
31195
+ const cronPlugin = new CronPlugin2(config.cron, sessions, channelManager, deps, config.stateDir, dispatcher, resolveCronModelDeps);
30858
31196
  await cronPlugin.start();
30859
31197
  const { setCronConfig: setCronConfig2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
30860
31198
  setCronConfig2(config.cron);
@@ -31030,8 +31368,10 @@ ${notifications}
31030
31368
  dispatcher,
31031
31369
  runningQueries,
31032
31370
  engine,
31033
- systemPrompt,
31034
- compactConfig,
31371
+ systemPrompt: getSystemPrompt,
31372
+ // 0902 函数形态:命令按调用时现取
31373
+ compactConfig: getCompactConfig,
31374
+ // 0902 同上
31035
31375
  provider,
31036
31376
  deps,
31037
31377
  getVisualRegistry: () => visualRegistry,
@@ -31053,9 +31393,17 @@ ${notifications}
31053
31393
  setVisionDeps: (v) => {
31054
31394
  visionDeps = v;
31055
31395
  },
31056
- getDefaultVisionDeps: () => defaultVisionDeps,
31396
+ // reset 用:清 override deps,下一条图片消息由 resolveVisionDeps 按当前 meta 重建
31397
+ getDefaultVisionDeps: () => {
31398
+ visionDeps = null;
31399
+ return null;
31400
+ },
31057
31401
  doReloadConfig
31058
31402
  };
31403
+ deps.onProviderSwapped = (p) => {
31404
+ provider = p;
31405
+ commandDeps.provider = p;
31406
+ };
31059
31407
  const slashCommands = listCommandDefs().map((d) => ({
31060
31408
  name: d.name,
31061
31409
  description: d.description,
@@ -31312,7 +31660,7 @@ ${pathStr}` }];
31312
31660
  for (const att of nonImageAttachments) {
31313
31661
  console.log(`[file] Downloading: ${att.filename} (${att.contentType}, ${att.size}B)`);
31314
31662
  try {
31315
- const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher }) : await fetch(att.url);
31663
+ const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher, signal: AbortSignal.timeout(3e4) }) : await fetch(att.url, { signal: AbortSignal.timeout(3e4) });
31316
31664
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
31317
31665
  const buffer = Buffer.from(await resp.arrayBuffer());
31318
31666
  const safeName2 = path49.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
@@ -31342,7 +31690,8 @@ ${pathStr}` }];
31342
31690
  }
31343
31691
  const isImageBlock = (b) => b.type === "image" || b.type === "image_url";
31344
31692
  const hasImages = Array.isArray(queryContent) && queryContent.some((b) => isImageBlock(b));
31345
- let msgDeps = hasImages && visionDeps ? visionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31693
+ const activeVisionDeps = hasImages ? resolveVisionDeps() : null;
31694
+ let msgDeps = activeVisionDeps ? activeVisionDeps : modelOverride ? modelDepsCache.get(modelOverride) ?? deps : deps;
31346
31695
  if (isDeskBuddy && !hasImages && !modelOverride) {
31347
31696
  if (!deskBuddyDeps) {
31348
31697
  const dbRef = liveConfig.get("channels.deskBuddy.model") || "deepinfra/deepseek-ai/DeepSeek-V4-Flash-0731";
@@ -31370,7 +31719,8 @@ ${pathStr}` }];
31370
31719
  model: dbProviderCfg ? dbModelId : config.model,
31371
31720
  // 8/18 翀哥:完整工程 prompt(14k tok)+memory(10k) 会把小模型带偏成"工程助手"——deskBuddy 用 SOUL 精简人设
31372
31721
  // 8/21 复用 light-mode.buildLightStablePrompt(和情感模式同一构建器)
31373
- systemPrompt: buildLightStablePrompt(config.workspace, "deskBuddy"),
31722
+ systemPrompt: () => buildLightStablePrompt(config.workspace, "deskBuddy"),
31723
+ // 0902 函数形态:改 SOUL 热生效
31374
31724
  maxTokens: 1024,
31375
31725
  temperature: 0.7,
31376
31726
  disableThinking: true,
@@ -31384,7 +31734,7 @@ ${pathStr}` }];
31384
31734
  msgDeps = deskBuddyDeps;
31385
31735
  }
31386
31736
  if (hasImages) {
31387
- console.log(`[vision-debug] hasImages=true visionDeps=${visionDeps ? `yes(${visionDeps.providerId}/${visionDeps.model})` : "NULL"} modelOverride=${modelOverride || "none"} \u2192 msgDeps.provider=${msgDeps?.providerId || "?"}/${msgDeps?.model || "?"} queryContent.blocks=${Array.isArray(queryContent) ? queryContent.length : "string"}`);
31737
+ console.log(`[vision-debug] hasImages=true visionDeps=${activeVisionDeps ? `yes(${activeVisionDeps.providerId}/${activeVisionDeps.model})` : "NULL"} modelOverride=${modelOverride || "none"} \u2192 msgDeps.provider=${msgDeps?.providerId || "?"}/${msgDeps?.model || "?"} queryContent.blocks=${Array.isArray(queryContent) ? queryContent.length : "string"}`);
31388
31738
  if (Array.isArray(queryContent)) {
31389
31739
  queryContent.forEach((b, i) => {
31390
31740
  if (b.type === "image" && b.source?.data) {
@@ -31393,13 +31743,13 @@ ${pathStr}` }];
31393
31743
  });
31394
31744
  }
31395
31745
  }
31396
- if (hasImages && visionDeps) {
31397
- console.log(`[vision] Routing to ${visionDeps.providerId}/${visionDeps.model}${visionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31746
+ if (hasImages && activeVisionDeps) {
31747
+ console.log(`[vision] Routing to ${activeVisionDeps.providerId}/${activeVisionDeps.model}${activeVisionDeps.model !== visionConfig?.modelId ? ` (override, config=${visionConfig.providerId}/${visionConfig.modelId})` : ""}`);
31398
31748
  }
31399
31749
  const preQueryResult = await messageHooks.runPreQuery({
31400
31750
  inbound: { channel: inbound.channel, channel_id: inbound.channel_id, from: inbound.from, fromName: inbound.fromName, channelType: inbound.channelType, isMentioned: inbound.isMentioned, isBot: inbound.isBot, messageId: inbound.messageId },
31401
31751
  text: queryContent,
31402
- msgDeps: hasImages && visionDeps ? visionDeps : msgDeps,
31752
+ msgDeps: activeVisionDeps ?? msgDeps,
31403
31753
  deps: { provider, channelManager, sessions, dispatcher, config, workspace: config.workspace }
31404
31754
  });
31405
31755
  if (preQueryResult.skip) {
@@ -31412,6 +31762,7 @@ ${pathStr}` }];
31412
31762
  }
31413
31763
  queryContent = preQueryResult.text ?? queryContent;
31414
31764
  if (preQueryResult.msgDeps) msgDeps = preQueryResult.msgDeps;
31765
+ msgDeps.renderer = renderer;
31415
31766
  const accepted = dispatcher.submitMessage({
31416
31767
  text: queryContent,
31417
31768
  sessionId,
@@ -31646,8 +31997,9 @@ ${pathStr}` }];
31646
31997
  sessionManager: sessions,
31647
31998
  sessionId,
31648
31999
  model: config.model,
31649
- contextWindow: compactConfig.contextWindow || 2e5,
31650
- systemPrompt,
32000
+ contextWindow: getCompactConfig().contextWindow || 2e5,
32001
+ systemPrompt: getSystemPrompt(),
32002
+ // 0902 现取(API /context 报告跟当前 prompt 一致)
31651
32003
  toolDefs: registry.definitions(),
31652
32004
  workspace: config.workspace
31653
32005
  });
@@ -31975,6 +32327,24 @@ async function doReloadConfig(config, deps, provider) {
31975
32327
  visionModel: newConfig.visionModel,
31976
32328
  visionFallbacks: newConfig.visionFallbacks
31977
32329
  });
32330
+ const newRecall = createMemorySideProvider(
32331
+ newConfig.topics?.recall,
32332
+ provider,
32333
+ newConfig.providers || {}
32334
+ );
32335
+ const newExtract = createMemorySideProvider(
32336
+ newConfig.topics?.extract,
32337
+ provider,
32338
+ newConfig.providers || {}
32339
+ );
32340
+ if (newRecall) {
32341
+ deps.recallProvider = newRecall;
32342
+ changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32343
+ }
32344
+ if (newExtract) {
32345
+ deps.extractProvider = newExtract;
32346
+ changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32347
+ }
31978
32348
  if (oldProviderKey !== newProviderKey) {
31979
32349
  console.log("[reload] Provider structure changed, rebuilding chain...");
31980
32350
  const rebuilt = buildProviderChain(newConfig);
@@ -31985,6 +32355,7 @@ async function doReloadConfig(config, deps, provider) {
31985
32355
  }
31986
32356
  changes.push(`provider chain rebuilt (${rebuilt.visionChainLabels.length > 0 ? "vision: " + rebuilt.visionChainLabels.join("\u2192") : "no vision"})`);
31987
32357
  }
32358
+ if (deps.onProviderSwapped) deps.onProviderSwapped(rebuilt.provider);
31988
32359
  if (typeof deps.setVisionProvider === "function") {
31989
32360
  const vCfg = newConfig.visionModel;
31990
32361
  const vpCfg = vCfg ? newConfig.providers?.[vCfg.providerId] : void 0;
@@ -32015,24 +32386,6 @@ async function doReloadConfig(config, deps, provider) {
32015
32386
  changes.push(`deskBuddy model \u2192 ${newDbModel}`);
32016
32387
  }
32017
32388
  }
32018
- const newRecall = createMemorySideProvider(
32019
- newConfig.topics?.recall,
32020
- provider,
32021
- newConfig.providers || {}
32022
- );
32023
- const newExtract = createMemorySideProvider(
32024
- newConfig.topics?.extract,
32025
- provider,
32026
- newConfig.providers || {}
32027
- );
32028
- if (newRecall) {
32029
- deps.recallProvider = newRecall;
32030
- changes.push(`recall \u2192 ${newConfig.topics?.recall?.provider}/${newConfig.topics?.recall?.model}`);
32031
- }
32032
- if (newExtract) {
32033
- deps.extractProvider = newExtract;
32034
- changes.push(`extract \u2192 ${newConfig.topics?.extract?.provider}/${newConfig.topics?.extract?.model}`);
32035
- }
32036
32389
  try {
32037
32390
  const { setAutoDreamConfig: setAutoDreamConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
32038
32391
  setAutoDreamConfig2(newConfig);