engine7 7.1.107 → 7.1.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.mjs CHANGED
@@ -141,14 +141,14 @@ var init_live = __esm({
141
141
  }
142
142
  /** 把改动写回 config 文件对应段(找不到文件路径则只内存生效) */
143
143
  async persistToFile(dotPath, val) {
144
- const fs56 = await import("node:fs");
144
+ const fs58 = await import("node:fs");
145
145
  const cfgPath = this.current?._configFilePath;
146
- if (!cfgPath || !fs56.existsSync(cfgPath)) {
146
+ if (!cfgPath || !fs58.existsSync(cfgPath)) {
147
147
  console.warn(`[liveConfig] set: no config file path, in-memory only (${dotPath})`);
148
148
  return;
149
149
  }
150
150
  try {
151
- const raw = JSON.parse(fs56.readFileSync(cfgPath, "utf-8"));
151
+ const raw = JSON.parse(fs58.readFileSync(cfgPath, "utf-8"));
152
152
  const keys = dotPath.split(".");
153
153
  let o = raw;
154
154
  for (let i = 0; i < keys.length - 1; i++) {
@@ -156,7 +156,7 @@ var init_live = __esm({
156
156
  o = o[keys[i]];
157
157
  }
158
158
  o[keys[keys.length - 1]] = val;
159
- fs56.writeFileSync(cfgPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
159
+ fs58.writeFileSync(cfgPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
160
160
  console.log(`[liveConfig] persisted ${dotPath} = ${JSON.stringify(val)} to ${cfgPath}`);
161
161
  markSelfWrite();
162
162
  } catch (e) {
@@ -407,6 +407,7 @@ var init_clock = __esm({
407
407
  var loader_exports = {};
408
408
  __export(loader_exports, {
409
409
  configSummary: () => configSummary,
410
+ isUnresolvedEnvRef: () => isUnresolvedEnvRef,
410
411
  loadConfig: () => loadConfig,
411
412
  parseModelRef: () => parseModelRef,
412
413
  resolveTimezone: () => resolveTimezone,
@@ -449,6 +450,9 @@ function resolveTimezone(raw) {
449
450
  return fallback();
450
451
  }
451
452
  }
453
+ function isUnresolvedEnvRef(value) {
454
+ return typeof value === "string" && ENV_REF_RE.test(value);
455
+ }
452
456
  function loadConfig(configPath2) {
453
457
  const resolvedPath = path.resolve(
454
458
  configPath2 || process.env.ENGINE_CONFIG || "engine-config.json"
@@ -460,7 +464,7 @@ function loadConfig(configPath2) {
460
464
  const missingEnvKeys = /* @__PURE__ */ new Set();
461
465
  const resolveEnv = (node) => {
462
466
  if (typeof node === "string") {
463
- const m = node.match(/^(?:env:|{{\s*)(?=[A-Z0-9_]*[A-Z])([A-Z0-9_]+)(?:\s*}})?$/);
467
+ const m = ENV_REF_RE.exec(node);
464
468
  if (m) {
465
469
  if (process.env[m[1]] !== void 0) return process.env[m[1]];
466
470
  if (node.startsWith("env:")) missingEnvKeys.add(m[1]);
@@ -672,7 +676,7 @@ function configSummary(config2) {
672
676
  lines.push(`Interrupt: ${config2.profile.features.interrupt}`);
673
677
  return lines.join("\n");
674
678
  }
675
- var loadWarnings, loadWarningsFlushed;
679
+ var loadWarnings, loadWarningsFlushed, ENV_REF_RE;
676
680
  var init_loader = __esm({
677
681
  "src/config/loader.ts"() {
678
682
  "use strict";
@@ -681,6 +685,7 @@ var init_loader = __esm({
681
685
  init_clock();
682
686
  loadWarnings = [];
683
687
  loadWarningsFlushed = false;
688
+ ENV_REF_RE = /^(?:env:|{{\s*)(?=[A-Z0-9_]*[A-Z])([A-Z0-9_]+)(?:\s*}})?$/;
684
689
  }
685
690
  });
686
691
 
@@ -1522,6 +1527,18 @@ function toHookContext(ctx) {
1522
1527
  cwd: ctx.workspace
1523
1528
  };
1524
1529
  }
1530
+ function noteCallSignature(sig) {
1531
+ if (sig === _lastCallSig) {
1532
+ _lastCallCount++;
1533
+ return _lastCallCount;
1534
+ }
1535
+ _lastCallSig = sig;
1536
+ _lastCallCount = 1;
1537
+ return 1;
1538
+ }
1539
+ function loopSig(tc) {
1540
+ return `${tc.function.name}::${tc.function.arguments ?? ""}`;
1541
+ }
1525
1542
  async function executeOne(tc, ctx, signal) {
1526
1543
  if (signal?.aborted) {
1527
1544
  return { call: tc, result: { content: "[interrupted by user /stop]", isError: false }, summary: null };
@@ -1530,6 +1547,23 @@ async function executeOne(tc, ctx, signal) {
1530
1547
  if (!tool) {
1531
1548
  return { call: tc, result: { content: `Unknown tool: ${tc.function.name}`, isError: true }, summary: null };
1532
1549
  }
1550
+ const repeatN = noteCallSignature(loopSig(tc));
1551
+ if (repeatN >= LOOP_HARD_AT) {
1552
+ console.warn(`[executor] \u5FAA\u73AF\u5239\u8F66\u89E6\u53D1\uFF1A${tc.function.name} \u8FDE\u7EED\u7B2C ${repeatN} \u6B21\uFF08\u540C\u4E00\u53C2\u6570\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u6267\u884C`);
1553
+ return {
1554
+ call: tc,
1555
+ result: {
1556
+ content: `\u26D4 \u5FAA\u73AF\u5239\u8F66\uFF1A\u4F60\u5DF2\u8FDE\u7EED ${repeatN} \u6B21\u7528**\u5B8C\u5168\u76F8\u540C\u7684\u53C2\u6570**\u8C03\u7528 \`${tc.function.name}\`\uFF0C\u672C\u5DE5\u5177\u88AB\u62D2\u7EDD\u6267\u884C\u3002
1557
+
1558
+ \u540C\u4E00\u4E2A\u8C03\u7528\u91CD\u590D\u518D\u591A\u904D\uFF0C\u8FD4\u56DE\u4E5F\u53EA\u4F1A\u662F\u540C\u4E00\u4EFD\u7ED3\u679C\u3002**\u8BF7\u7ACB\u523B\u505C\u6B62\u8C03\u7528\u672C\u5DE5\u5177**\uFF0C\u6539\u7528\u5176\u4E2D\u4E00\u6761\uFF1A
1559
+ 1. \u7528\u73B0\u6709\u4FE1\u606F\u76F4\u63A5\u4F5C\u7B54\uFF08\u5E76\u8BF4\u660E\u54EA\u4E9B\u5730\u65B9\u56E0\u4FE1\u606F\u4E0D\u8DB3\u800C\u65E0\u6CD5\u786E\u5B9A\uFF09
1560
+ 2. \u6362\u4E00\u4E2A**\u4E0D\u540C\u53C2\u6570**\u6216**\u4E0D\u540C\u5DE5\u5177**\u518D\u8BD5
1561
+ 3. \u5982\u679C\u786E\u5B9E\u62FF\u4E0D\u5230\u6570\u636E\uFF0C\u76F4\u63A5\u544A\u8BC9\u7528\u6237"\u8BE5\u9879\u6570\u636E\u4E0D\u53EF\u7528"\uFF0C**\u4E0D\u8981\u7EE7\u7EED\u91CD\u8BD5**`,
1562
+ isError: true
1563
+ },
1564
+ summary: null
1565
+ };
1566
+ }
1533
1567
  if (isPlanMode() && PLAN_BLOCKED_TOOLS.has(tc.function.name)) {
1534
1568
  return {
1535
1569
  call: tc,
@@ -1574,6 +1608,13 @@ async function executeOne(tc, ctx, signal) {
1574
1608
  }
1575
1609
  const result = await tool.handler(args2, ctx, signal);
1576
1610
  await executePostToolUseHooks(tc.function.name, args2, result.content, hookCtx, signal);
1611
+ if (repeatN >= LOOP_SOFT_AT) {
1612
+ const prefix = `\u26A0\uFE0F \u4F60\u5DF2\u8FDE\u7EED ${repeatN} \u6B21\u7528**\u5B8C\u5168\u76F8\u540C\u7684\u53C2\u6570**\u8C03\u7528 \`${tc.function.name}\`\u3002\u91CD\u590D\u4E0D\u4F1A\u5F97\u5230\u65B0\u7ED3\u679C\u2014\u2014\u5982\u679C\u4E0B\u9762\u8FD9\u4EFD\u4FE1\u606F\u4E0D\u591F\u7528\uFF0C\u8BF7\u6362\u53C2\u6570/\u6362\u5DE5\u5177\uFF0C\u6216\u76F4\u63A5\u7528\u73B0\u6709\u4FE1\u606F\u4F5C\u7B54\u5E76\u8BF4\u660E\u54EA\u91CC\u4E0D\u786E\u5B9A\u3002
1613
+
1614
+ `;
1615
+ const body = typeof result.content === "string" ? result.content : JSON.stringify(result.content);
1616
+ return { call: tc, result: { ...result, content: prefix + body }, summary: null };
1617
+ }
1577
1618
  return { call: tc, result, summary: null };
1578
1619
  } catch (err) {
1579
1620
  await executePostToolUseFailureHooks(tc.function.name, args2, err.message, hookCtx, signal);
@@ -1626,7 +1667,7 @@ async function executeTools(toolCalls, signal, context) {
1626
1667
  }
1627
1668
  return { messages, summaries };
1628
1669
  }
1629
- var PLAN_BLOCKED_TOOLS;
1670
+ var PLAN_BLOCKED_TOOLS, LOOP_SOFT_AT, LOOP_HARD_AT, _lastCallSig, _lastCallCount;
1630
1671
  var init_executor2 = __esm({
1631
1672
  "src/tools/BashTool/executor.ts"() {
1632
1673
  "use strict";
@@ -1636,6 +1677,10 @@ var init_executor2 = __esm({
1636
1677
  init_constants2();
1637
1678
  init_hooks();
1638
1679
  PLAN_BLOCKED_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
1680
+ LOOP_SOFT_AT = 2;
1681
+ LOOP_HARD_AT = 5;
1682
+ _lastCallSig = "";
1683
+ _lastCallCount = 0;
1639
1684
  }
1640
1685
  });
1641
1686
 
@@ -4554,9 +4599,9 @@ function isAutoMemPath(absolutePath, workspace) {
4554
4599
  return normalizedPath.startsWith(getAutoMemPath(workspace));
4555
4600
  }
4556
4601
  async function ensureMemoryDirExists(memoryDir) {
4557
- const fs56 = await import("node:fs");
4602
+ const fs58 = await import("node:fs");
4558
4603
  try {
4559
- await fs56.promises.mkdir(memoryDir, { recursive: true });
4604
+ await fs58.promises.mkdir(memoryDir, { recursive: true });
4560
4605
  } catch (e) {
4561
4606
  const code = e?.code;
4562
4607
  if (code !== "EEXIST") {
@@ -5203,8 +5248,30 @@ var init_context_analyzer = __esm({
5203
5248
  // src/core/query.ts
5204
5249
  var query_exports = {};
5205
5250
  __export(query_exports, {
5206
- QueryEngine: () => QueryEngine
5251
+ QueryEngine: () => QueryEngine,
5252
+ namesFromHistory: () => namesFromHistory
5207
5253
  });
5254
+ function namesFromHistory(messages, deferredNames) {
5255
+ const out = /* @__PURE__ */ new Set();
5256
+ const searchCallIds = /* @__PURE__ */ new Set();
5257
+ for (const m of messages) {
5258
+ for (const tc of m?.tool_calls ?? []) {
5259
+ const name = tc?.function?.name;
5260
+ if (!name) continue;
5261
+ out.add(name);
5262
+ if (name === TOOL_SEARCH_TOOL_NAME && tc.id) searchCallIds.add(tc.id);
5263
+ }
5264
+ }
5265
+ if (searchCallIds.size > 0 && deferredNames.size > 0) {
5266
+ for (const m of messages) {
5267
+ if (m?.role !== "tool" || !m.tool_call_id || !searchCallIds.has(m.tool_call_id)) continue;
5268
+ const text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((b) => b && typeof b === "object" && "text" in b ? String(b.text ?? "") : "").join("") : "";
5269
+ if (!text) continue;
5270
+ for (const n of deferredNames) if (text.includes(n)) out.add(n);
5271
+ }
5272
+ }
5273
+ return out;
5274
+ }
5208
5275
  var QueryEngine;
5209
5276
  var init_query = __esm({
5210
5277
  "src/core/query.ts"() {
@@ -5398,7 +5465,7 @@ var init_query = __esm({
5398
5465
  const { roughTokenCountEstimation: roughTokenCountEstimation6 } = await Promise.resolve().then(() => (init_tokenEstimate(), tokenEstimate_exports));
5399
5466
  const systemText = this.resolveOpt(this.options.systemStable) || this.resolveOpt(this.options.systemPrompt) || "";
5400
5467
  const systemTokens = roughTokenCountEstimation6(systemText);
5401
- const initialToolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs) : allToolDefs;
5468
+ const initialToolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs, currentMessages) : allToolDefs;
5402
5469
  const toolTokens = estimateToolDefinitionTokens2(initialToolDefs);
5403
5470
  let memoryTokens = 0;
5404
5471
  try {
@@ -5522,7 +5589,7 @@ var init_query = __esm({
5522
5589
  let turnAborted = false;
5523
5590
  try {
5524
5591
  const apiMessages = normalizeMessagesForAPI(currentMessages);
5525
- const toolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs) : allToolDefs;
5592
+ const toolDefs = useDynamicTools ? this.buildActiveTools(allToolDefs, currentMessages) : allToolDefs;
5526
5593
  const deferredHint = deferredNamesForHint.length > 0 ? `<available-deferred-tools>
5527
5594
  ${deferredNamesForHint.join("\n")}
5528
5595
  </available-deferred-tools>
@@ -5939,11 +6006,20 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
5939
6006
  }
5940
6007
  /**
5941
6008
  * 组装本轮 active tools(对齐 CC toolSearch 动态加载):
5942
- * 非 defer 工具全 schema + 已加载的 deferred 工具 schema(ToolSearch 调用后累积)。
6009
+ * 非 defer 工具全 schema + 已加载的 deferred 工具 schema。
5943
6010
  * 去掉 shouldDefer 字段(发 provider 不能带,对齐 CC:智谱不认 defer_loading)。
6011
+ *
6012
+ * 「已加载」有**两个真源,取并集**(0927,见 namesFromHistory 的注释):
6013
+ * ① this.loadedDeferredTools —— 本进程内 ToolSearch 的累积(内存,重启即空)
6014
+ * ② namesFromHistory(messages) —— 这个会话历史上出现过的(从对话推导,重启后仍在)
6015
+ * 缺①则本进程刚加载的会丢;缺②则重启/换引擎后历史里用过的会丢。
5944
6016
  */
5945
- buildActiveTools(allDefs) {
5946
- const active = allDefs.filter((d) => !d.shouldDefer || this.loadedDeferredTools.has(d.function.name)).map((d) => {
6017
+ buildActiveTools(allDefs, messages) {
6018
+ const deferredNames = new Set(
6019
+ allDefs.filter((d) => d.shouldDefer).map((d) => d.function.name)
6020
+ );
6021
+ const fromHistory = namesFromHistory(messages, deferredNames);
6022
+ const active = allDefs.filter((d) => !d.shouldDefer || this.loadedDeferredTools.has(d.function.name) || fromHistory.has(d.function.name)).map((d) => {
5947
6023
  if (!d.shouldDefer) return d;
5948
6024
  const { shouldDefer: _drop, ...rest2 } = d;
5949
6025
  return rest2;
@@ -6717,16 +6793,23 @@ var init_openai_responses_provider = __esm({
6717
6793
  // 官方:store:false / ZDR 场景靠 encrypted_content 才能把推理跨轮带过去
6718
6794
  include: ["reasoning.encrypted_content"],
6719
6795
  ...system ? { instructions: system } : {},
6720
- ...params.tools && params.tools.length > 0 ? {
6721
- tools: params.tools.map((t) => ({
6722
- type: "function",
6723
- name: t.function.name,
6724
- description: t.function.description,
6725
- parameters: t.function.parameters,
6726
- // 官方 migrate §5:Responses 省略 strict 会**尝试** strict 模式(schema 不兼容时
6727
- // 回退 best-effort)。显式 false 保持引擎既有的非严格行为。
6728
- strict: false
6729
- }))
6796
+ // 工具 = 服务端内建工具(若有) + 引擎的 function 工具。
6797
+ // 顺序:内建在前——它们更"声明式",放前面读日志时一眼能看到。
6798
+ // 判据用两者任一非空:只配了 serverTools、一个 function 都没有时也要发 tools,
6799
+ // 否则内建搜索会被整段省略(这正是"为什么她不触发内建搜索"的症结)。
6800
+ ...params.tools && params.tools.length > 0 || (this.config.serverTools?.length ?? 0) > 0 ? {
6801
+ tools: [
6802
+ ...this.config.serverTools ?? [],
6803
+ ...(params.tools ?? []).map((t) => ({
6804
+ type: "function",
6805
+ name: t.function.name,
6806
+ description: t.function.description,
6807
+ parameters: t.function.parameters,
6808
+ // 官方 migrate §5:Responses 省略 strict 会**尝试** strict 模式(schema 不兼容时
6809
+ // 回退 best-effort)。显式 false 保持引擎既有的非严格行为。
6810
+ strict: false
6811
+ }))
6812
+ ]
6730
6813
  } : {},
6731
6814
  ...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
6732
6815
  // disableThinking → reasoning.effort='none'。**必须实现**:Responses 里 reasoning token
@@ -6737,7 +6820,8 @@ var init_openai_responses_provider = __esm({
6737
6820
  // 不发 temperature:推理模型不支持(gpt-6-astra 默认 medium effort)。
6738
6821
  // 官方 reasoning 指南:reasoning 模型忽略/拒绝 temperature、top_p、penalty。
6739
6822
  };
6740
- console.log(`[openai-responses] \u2192 model=${params.model} items=${inputItems.length} tools=${params.tools?.length ?? 0} maxTokens=${body.max_output_tokens ?? "unset"} reasoning=${body.reasoning?.effort ?? "default"} store=false`);
6823
+ const st = this.config.serverTools ?? [];
6824
+ console.log(`[openai-responses] \u2192 model=${params.model} items=${inputItems.length} tools=${params.tools?.length ?? 0}${st.length ? ` +serverTools=[${st.map((x) => x?.type).join(",")}]` : ""} maxTokens=${body.max_output_tokens ?? "unset"} reasoning=${body.reasoning?.effort ?? "default"} store=false`);
6741
6825
  const retryGen = fetchWithRetry(url, {
6742
6826
  method: "POST",
6743
6827
  headers: {
@@ -6868,6 +6952,9 @@ var init_openai_responses_provider = __esm({
6868
6952
  };
6869
6953
  if (item.name) tc.function.name = item.name;
6870
6954
  if (typeof item.arguments === "string") tc.function.arguments = item.arguments;
6955
+ if (item.call_id && tc.id && tc.id !== item.call_id) {
6956
+ console.warn(`[openai-responses] output_index=${idx} \u649E\u8F66: \u5DF2\u6709 call_id=${tc.id}\uFF0C\u65B0\u5230 call_id=${item.call_id} \u2014\u2014 \u53EF\u80FD\u662F\u7F51\u5173\u4E71\u5E8F/\u590D\u7528 index\uFF0C\u524D\u8005\u5DF2\u88AB\u8986\u76D6`);
6957
+ }
6871
6958
  if (item.call_id) tc.id = item.call_id;
6872
6959
  if (!tc.id) tc.id = item.id ?? `call_${idx}`;
6873
6960
  toolCallsByIndex.set(idx, tc);
@@ -6927,6 +7014,9 @@ ${rawSample.join("\n")}`);
6927
7014
  yield { type: "error", error: `OpenAI Responses: response incomplete (${incompleteReason})` };
6928
7015
  return;
6929
7016
  }
7017
+ if (incompleteReason && toolCallsByIndex.size > 0) {
7018
+ console.warn(`[openai-responses] response incomplete (${incompleteReason}) \u4E14\u5E26 ${toolCallsByIndex.size} \u4E2A tool_call \u2014\u2014 **arguments \u53EF\u80FD\u662F\u6B8B\u7F3A JSON**\uFF0C\u4E0B\u6E38 parse \u5931\u8D25\u5C5E\u9884\u671F\uFF0C\u91CD\u8BD5\u5373\u53EF\uFF1B\u82E5\u9891\u7E41\u51FA\u73B0\u5E94\u8C03\u5927 max_output_tokens`);
7019
+ }
6930
7020
  doneYielded = true;
6931
7021
  yield { type: "done", usage: pendingUsage };
6932
7022
  }
@@ -7775,7 +7865,9 @@ function createProvider(config2) {
7775
7865
  baseUrl: config2.baseUrl,
7776
7866
  apiKey: config2.apiKey,
7777
7867
  proxy,
7778
- wafSingleQuoteWorkaround: config2.wafSingleQuoteWorkaround
7868
+ wafSingleQuoteWorkaround: config2.wafSingleQuoteWorkaround,
7869
+ serverTools: config2.serverTools
7870
+ // 0927 服务端内建工具(如方舟联网搜索)
7779
7871
  });
7780
7872
  case "anthropic":
7781
7873
  return new AnthropicProvider({
@@ -8167,7 +8259,7 @@ __export(teamHelpers_exports, {
8167
8259
  writeTeamFile: () => writeTeamFile,
8168
8260
  writeTeamFileAsync: () => writeTeamFileAsync
8169
8261
  });
8170
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
8262
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
8171
8263
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
8172
8264
  import { join as join16 } from "node:path";
8173
8265
  function initTeamDirs(stateDir) {
@@ -8210,7 +8302,7 @@ async function readTeamFileAsync(teamName) {
8210
8302
  }
8211
8303
  function writeTeamFile(teamName, teamFile) {
8212
8304
  const teamDir = getTeamDir(teamName);
8213
- mkdirSync7(teamDir, { recursive: true });
8305
+ mkdirSync8(teamDir, { recursive: true });
8214
8306
  writeFileSync5(getTeamFilePath(teamName), JSON.stringify(teamFile, null, 2));
8215
8307
  }
8216
8308
  async function writeTeamFileAsync(teamName, teamFile) {
@@ -8301,7 +8393,7 @@ async function cleanupTeamDirectories(teamName) {
8301
8393
  }
8302
8394
  function ensureTasksDir(taskListId) {
8303
8395
  const dir = join16(getTasksDir(), taskListId);
8304
- mkdirSync7(dir, { recursive: true });
8396
+ mkdirSync8(dir, { recursive: true });
8305
8397
  }
8306
8398
  function setLeaderTeamName(name) {
8307
8399
  _leaderTeamName = name;
@@ -8399,7 +8491,7 @@ async function acquireLock(inboxPath) {
8399
8491
  for (let i = 0; i < LOCK_RETRIES; i++) {
8400
8492
  if (await isLockStale(lockPath2)) {
8401
8493
  try {
8402
- await import("node:fs/promises").then((fs56) => fs56.rm(lockPath2, { force: true }));
8494
+ await import("node:fs/promises").then((fs58) => fs58.rm(lockPath2, { force: true }));
8403
8495
  } catch {
8404
8496
  }
8405
8497
  }
@@ -8407,7 +8499,7 @@ async function acquireLock(inboxPath) {
8407
8499
  await writeFile2(lockPath2, `${process.pid}-${Date.now()}`, { encoding: "utf-8", flag: "wx" });
8408
8500
  return async () => {
8409
8501
  try {
8410
- await import("node:fs/promises").then((fs56) => fs56.rm(lockPath2, { force: true }));
8502
+ await import("node:fs/promises").then((fs58) => fs58.rm(lockPath2, { force: true }));
8411
8503
  } catch {
8412
8504
  }
8413
8505
  };
@@ -8774,10 +8866,10 @@ function parseDurationString(s, defaultMs) {
8774
8866
  const m = String(s).trim().toLowerCase().match(/^([\d.]+)\s*([a-z]+)?$/);
8775
8867
  if (!m) return defaultMs;
8776
8868
  const n = parseFloat(m[1]);
8777
- const unit = m[2] || "ms";
8778
- const mult = DURATION_UNITS[unit];
8779
- if (!Number.isFinite(n) || !mult && unit !== "ms") return defaultMs;
8780
- return Math.round(unit === "ms" ? n : n * mult);
8869
+ const unit2 = m[2] || "ms";
8870
+ const mult = DURATION_UNITS[unit2];
8871
+ if (!Number.isFinite(n) || !mult && unit2 !== "ms") return defaultMs;
8872
+ return Math.round(unit2 === "ms" ? n : n * mult);
8781
8873
  }
8782
8874
  function recordInboundBotFlag(userId, isBot) {
8783
8875
  if (!userId || !isBot) return;
@@ -9653,7 +9745,7 @@ var init_filters = __esm({
9653
9745
 
9654
9746
  // src/mem7/core/vector-store.ts
9655
9747
  import { Schema, Field, Utf8, Float32, Int64, FixedSizeList, TimestampMicrosecond, List } from "apache-arrow";
9656
- import { mkdirSync as mkdirSync8 } from "node:fs";
9748
+ import { mkdirSync as mkdirSync9 } from "node:fs";
9657
9749
  import { join as join23 } from "node:path";
9658
9750
  function lanceSpecifierFor(platform2, arch) {
9659
9751
  return platform2 === "darwin" && arch === "x64" ? "@engine7/lancedb-intel" : "@lancedb/lancedb";
@@ -9756,7 +9848,7 @@ var init_vector_store = __esm({
9756
9848
  if (!this.db) {
9757
9849
  const lance = await loadLance();
9758
9850
  if (!lance) throw new Mem7VectorUnavailableError();
9759
- mkdirSync8(this.dir, { recursive: true });
9851
+ mkdirSync9(this.dir, { recursive: true });
9760
9852
  this.db = await lance.connect(this.dir);
9761
9853
  }
9762
9854
  return this.db;
@@ -10045,7 +10137,7 @@ Follow these principles when generating episodic memories:
10045
10137
 
10046
10138
  // src/mem7/core/store.ts
10047
10139
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
10048
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync7, existsSync as existsSync14, readdirSync as readdirSync2 } from "node:fs";
10140
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync7, existsSync as existsSync15, readdirSync as readdirSync2 } from "node:fs";
10049
10141
  import { join as join24 } from "node:path";
10050
10142
  import { createHash } from "node:crypto";
10051
10143
  function contentHash(text) {
@@ -10088,7 +10180,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
10088
10180
  constructor(stateDir) {
10089
10181
  this.stateDir = stateDir;
10090
10182
  this.root = mem7Root(stateDir);
10091
- mkdirSync9(join24(this.root, "index"), { recursive: true });
10183
+ mkdirSync10(join24(this.root, "index"), { recursive: true });
10092
10184
  this.db = new DatabaseSync2(join24(this.root, "index", "mem7.db"));
10093
10185
  this.db.exec("PRAGMA journal_mode=WAL");
10094
10186
  this.db.exec(SCHEMA);
@@ -10221,12 +10313,12 @@ CREATE TABLE IF NOT EXISTS watermarks (
10221
10313
  appendMd(ownerId, dirName, filePrefix, type2, dateStr, entries2, render) {
10222
10314
  if (entries2.length === 0) return;
10223
10315
  const dir = join24(this.root, "users", ownerId, dirName);
10224
- mkdirSync9(dir, { recursive: true });
10316
+ mkdirSync10(dir, { recursive: true });
10225
10317
  const file2 = join24(dir, `${filePrefix}-${dateStr}.md`);
10226
10318
  let head;
10227
10319
  let existingBlocks = "";
10228
10320
  let count;
10229
- if (existsSync14(file2)) {
10321
+ if (existsSync15(file2)) {
10230
10322
  const text = readFileSync16(file2, "utf8");
10231
10323
  const fmEnd = text.indexOf("\n---", 3);
10232
10324
  const split = fmEnd >= 0 ? fmEnd + 4 : text.length;
@@ -10280,7 +10372,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
10280
10372
  const [dirName, filePrefix] = dirByPrefix[prefix];
10281
10373
  const file2 = join24(this.root, "users", ownerId, dirName, `${filePrefix}-${dateStr}.md`);
10282
10374
  let max = 0;
10283
- if (existsSync14(file2)) {
10375
+ if (existsSync15(file2)) {
10284
10376
  const re = new RegExp(`${prefix}_${dateStr.replace(/-/g, "")}_(\\d+)`, "g");
10285
10377
  for (const m of readFileSync16(file2, "utf8").matchAll(re)) max = Math.max(max, Number(m[1]));
10286
10378
  }
@@ -10347,14 +10439,14 @@ CREATE TABLE IF NOT EXISTS watermarks (
10347
10439
  }
10348
10440
  // ── md 读(proactive 消费):开窗预测(全 owner 扫描,end>=今天)──
10349
10441
  getOpenPredictions(now, limit = 50) {
10350
- const today2 = todayKey(now);
10442
+ const today3 = todayKey(now);
10351
10443
  const out = [];
10352
10444
  const usersDir = join24(this.root, "users");
10353
- if (!existsSync14(usersDir)) return out;
10445
+ if (!existsSync15(usersDir)) return out;
10354
10446
  for (const owner of readdirSync2(usersDir, { withFileTypes: true })) {
10355
10447
  if (!owner.isDirectory()) continue;
10356
10448
  const dir = join24(usersDir, owner.name, ".foresights");
10357
- if (!existsSync14(dir)) continue;
10449
+ if (!existsSync15(dir)) continue;
10358
10450
  for (const f of readdirSync2(dir)) {
10359
10451
  if (!/^foresight-\d{4}-\d{2}-\d{2}\.md$/.test(f)) continue;
10360
10452
  let content;
@@ -10370,7 +10462,7 @@ CREATE TABLE IF NOT EXISTS watermarks (
10370
10462
  const text = ((block.match(/### Foresight\s*\n+([\s\S]*?)(?:\n### Evidence|$)/) ?? [])[1] ?? "").trim();
10371
10463
  const evidence = ((block.match(/### Evidence\s*\n+([\s\S]*?)$/) ?? [])[1] ?? "").trim();
10372
10464
  if (!end || !start || !text) continue;
10373
- if (end < today2) continue;
10465
+ if (end < today3) continue;
10374
10466
  out.push({ ownerId: owner.name, startDate: start, endDate: end, content: text.replace(/\n+/g, " ").slice(0, 200), evidence: evidence.replace(/\n+/g, " ").slice(0, 120) });
10375
10467
  if (out.length >= limit) return out;
10376
10468
  }
@@ -11314,7 +11406,7 @@ var init_graph_store = __esm({
11314
11406
 
11315
11407
  // src/memory/proactive/persistence.ts
11316
11408
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
11317
- import { mkdirSync as mkdirSync10 } from "node:fs";
11409
+ import { mkdirSync as mkdirSync11 } from "node:fs";
11318
11410
  import { dirname as dirname8 } from "node:path";
11319
11411
  function rowToAction(r) {
11320
11412
  return {
@@ -11416,7 +11508,7 @@ CREATE INDEX IF NOT EXISTS idx_proactive_decisions_created ON proactive_decision
11416
11508
  Persistence = class {
11417
11509
  db;
11418
11510
  constructor(dbPath) {
11419
- mkdirSync10(dirname8(dbPath), { recursive: true });
11511
+ mkdirSync11(dirname8(dbPath), { recursive: true });
11420
11512
  this.db = new DatabaseSync3(dbPath);
11421
11513
  this.db.exec("PRAGMA journal_mode=WAL");
11422
11514
  this.db.exec(SCHEMA2);
@@ -13298,7 +13390,7 @@ var init_helpers = __esm({
13298
13390
  });
13299
13391
 
13300
13392
  // src/calendar/commands.ts
13301
- import { existsSync as existsSync15, statSync as statSync4 } from "node:fs";
13393
+ import { existsSync as existsSync16, statSync as statSync4 } from "node:fs";
13302
13394
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
13303
13395
  function fmtEnd(start, durationMin) {
13304
13396
  const [hh, mm] = start.split(":").map(Number);
@@ -13467,7 +13559,7 @@ function addTask(db2, args2) {
13467
13559
  }
13468
13560
  try {
13469
13561
  const absPath = isAbsolute4(docPath) ? docPath : resolve6(process.cwd(), docPath);
13470
- if (!existsSync15(absPath)) {
13562
+ if (!existsSync16(absPath)) {
13471
13563
  return `\u274C \u521B\u5EFA\u5931\u8D25\uFF1Adoc_path \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728
13472
13564
  \u8DEF\u5F84: ${docPath}
13473
13565
  \u89E3\u6790\u540E: ${absPath}
@@ -13900,7 +13992,7 @@ var init_recall_score = __esm({
13900
13992
  });
13901
13993
 
13902
13994
  // src/memory/proactive/opportunity.ts
13903
- import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as existsSync16 } from "node:fs";
13995
+ import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as existsSync17 } from "node:fs";
13904
13996
  import { join as join31 } from "node:path";
13905
13997
  function normalizeOpportunity(raw) {
13906
13998
  if (!raw || typeof raw !== "object") return null;
@@ -13958,8 +14050,8 @@ function enqueueOpportunity(persist, opp, anchor, planningContext, previousTitle
13958
14050
  function isProfilePrediction(text) {
13959
14051
  return PROFILE_RE.test(text);
13960
14052
  }
13961
- function capForesightEntries(entries2, today2, openCap = 12, upcomingCap = 8) {
13962
- const isOpen = (e) => e.start <= today2;
14053
+ function capForesightEntries(entries2, today3, openCap = 12, upcomingCap = 8) {
14054
+ const isOpen = (e) => e.start <= today3;
13963
14055
  const sorted = entries2.slice().sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || (isOpen(a) ? a.end.localeCompare(b.end) : a.start.localeCompare(b.start)));
13964
14056
  const open = sorted.filter(isOpen).slice(0, openCap);
13965
14057
  const upcoming = sorted.filter((e) => !isOpen(e)).slice(0, upcomingCap);
@@ -13983,11 +14075,11 @@ function loadForesightEntries(everosRoot, now, userId, ownerFilter) {
13983
14075
  return entries2.slice();
13984
14076
  }
13985
14077
  function parseForesightEntries(everosRoot, now, userId) {
13986
- const today2 = todayKey(now);
14078
+ const today3 = todayKey(now);
13987
14079
  let files;
13988
14080
  if (userId) {
13989
14081
  const ownDir = join31(everosRoot, userId, "default_project", "users", userId, ".foresights");
13990
- files = existsSync16(ownDir) ? listForesightMds(ownDir) : findForesightFiles(everosRoot);
14082
+ files = existsSync17(ownDir) ? listForesightMds(ownDir) : findForesightFiles(everosRoot);
13991
14083
  } else {
13992
14084
  files = findForesightFiles(everosRoot);
13993
14085
  }
@@ -14008,7 +14100,7 @@ function parseForesightEntries(everosRoot, now, userId) {
14008
14100
  const end = (block.match(/\*\*end_time\*\*:\s*(\d{4}-\d{2}-\d{2})/) ?? [])[1];
14009
14101
  const text = ((block.match(/### Foresight\s*\n+([\s\S]*?)(?:\n### Evidence|$)/) ?? [])[1] ?? "").trim();
14010
14102
  if (!start || !end || !text) continue;
14011
- if (end < today2) continue;
14103
+ if (end < today3) continue;
14012
14104
  const evidence = ((block.match(/### Evidence\s*\n+([\s\S]*?)$/) ?? [])[1] ?? "").trim();
14013
14105
  const blockOwner = (block.match(/\*\*owner_id\*\*:\s*(\S+)/) ?? [])[1] ?? fileOwner;
14014
14106
  const key = text.replace(/\s+/g, "");
@@ -15420,14 +15512,14 @@ ${JSON.stringify(payload, null, 2)}
15420
15512
  });
15421
15513
 
15422
15514
  // src/memory/proactive/proactive-tools.ts
15423
- import { existsSync as existsSync17 } from "node:fs";
15515
+ import { existsSync as existsSync18 } from "node:fs";
15424
15516
  function setProactiveWorkspace(workspace) {
15425
15517
  toolWorkspace = workspace;
15426
15518
  }
15427
15519
  function withStore(fn) {
15428
15520
  if (!toolWorkspace) return null;
15429
15521
  const p = proactiveDbPath(toolWorkspace);
15430
- if (!existsSync17(p)) return null;
15522
+ if (!existsSync18(p)) return null;
15431
15523
  const store = new Persistence(p);
15432
15524
  try {
15433
15525
  return fn(store);
@@ -15681,7 +15773,7 @@ __export(plugin_exports, {
15681
15773
  feedMem7Cell: () => feedMem7Cell
15682
15774
  });
15683
15775
  import { join as join32 } from "node:path";
15684
- import { existsSync as existsSync18, readFileSync as readFileSync24, appendFileSync as appendFileSync3 } from "node:fs";
15776
+ import { existsSync as existsSync19, readFileSync as readFileSync24, appendFileSync as appendFileSync3 } from "node:fs";
15685
15777
  function isFiller(text) {
15686
15778
  const t = text.trim();
15687
15779
  if (t.length >= 5) return false;
@@ -15904,7 +15996,7 @@ var init_plugin = __esm({
15904
15996
  this.actionAgent = new ActionAgent(this.provider, this.config.modelId, persona);
15905
15997
  this.ingestWorkspace = workspace;
15906
15998
  const calDbPath = join32(workspace, ".calendar", "calendar.db");
15907
- this.calDb = existsSync18(calDbPath) ? openDb(workspace) : null;
15999
+ this.calDb = existsSync19(calDbPath) ? openDb(workspace) : null;
15908
16000
  registerCallbackHook("PostToolUse", {
15909
16001
  type: "callback",
15910
16002
  id: "proactive-completion-evidence",
@@ -16304,10 +16396,10 @@ ${String(input.tool_output ?? "")}`
16304
16396
 
16305
16397
  // src/memory/sessionMemory/sessionMemoryUtils.ts
16306
16398
  import { join as join33 } from "node:path";
16307
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync25 } from "node:fs";
16399
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync25 } from "node:fs";
16308
16400
  function setSessionMemoryStateDir(dir) {
16309
16401
  _stateDir2 = dir;
16310
- mkdirSync11(dir, { recursive: true });
16402
+ mkdirSync12(dir, { recursive: true });
16311
16403
  }
16312
16404
  function getSessionMemoryDir() {
16313
16405
  return _stateDir2;
@@ -16579,7 +16671,7 @@ __export(sessionMemory_exports, {
16579
16671
  resetLastMemoryMessageUuid: () => resetLastMemoryMessageUuid,
16580
16672
  shouldExtractMemory: () => shouldExtractMemory
16581
16673
  });
16582
- import { readFileSync as readFileSync26, mkdirSync as mkdirSync12 } from "node:fs";
16674
+ import { readFileSync as readFileSync26, mkdirSync as mkdirSync13 } from "node:fs";
16583
16675
  import { writeFile as writeFile3 } from "node:fs/promises";
16584
16676
  function initSessionMemory(deps) {
16585
16677
  _deps = deps;
@@ -16638,7 +16730,7 @@ function shouldExtractMemory(estimatedTokens, messages) {
16638
16730
  }
16639
16731
  async function setupSessionMemoryFile() {
16640
16732
  const memoryDir = getSessionMemoryDir();
16641
- mkdirSync12(memoryDir, { recursive: true });
16733
+ mkdirSync13(memoryDir, { recursive: true });
16642
16734
  const memoryPath = getSessionMemoryPath();
16643
16735
  let currentMemory;
16644
16736
  try {
@@ -16765,11 +16857,11 @@ async function readLastConsolidatedAt(memoryDir) {
16765
16857
  }
16766
16858
  }
16767
16859
  async function tryAcquireConsolidationLock(memoryDir) {
16768
- const path58 = lockPath(memoryDir);
16860
+ const path60 = lockPath(memoryDir);
16769
16861
  let mtimeMs;
16770
16862
  let holderPid;
16771
16863
  try {
16772
- const [s, raw] = await Promise.all([stat3(path58), readFile5(path58, "utf8")]);
16864
+ const [s, raw] = await Promise.all([stat3(path60), readFile5(path60, "utf8")]);
16773
16865
  mtimeMs = s.mtimeMs;
16774
16866
  const parsed = parseInt(raw.trim(), 10);
16775
16867
  holderPid = Number.isFinite(parsed) ? parsed : void 0;
@@ -16782,10 +16874,10 @@ async function tryAcquireConsolidationLock(memoryDir) {
16782
16874
  }
16783
16875
  }
16784
16876
  await mkdir3(memoryDir, { recursive: true });
16785
- await writeFile4(path58, String(process.pid));
16877
+ await writeFile4(path60, String(process.pid));
16786
16878
  let verify2;
16787
16879
  try {
16788
- verify2 = await readFile5(path58, "utf8");
16880
+ verify2 = await readFile5(path60, "utf8");
16789
16881
  } catch {
16790
16882
  return null;
16791
16883
  }
@@ -16793,15 +16885,15 @@ async function tryAcquireConsolidationLock(memoryDir) {
16793
16885
  return mtimeMs ?? 0;
16794
16886
  }
16795
16887
  async function rollbackConsolidationLock(memoryDir, priorMtime) {
16796
- const path58 = lockPath(memoryDir);
16888
+ const path60 = lockPath(memoryDir);
16797
16889
  try {
16798
16890
  if (priorMtime === 0) {
16799
- await unlink(path58);
16891
+ await unlink(path60);
16800
16892
  return;
16801
16893
  }
16802
- await writeFile4(path58, "");
16894
+ await writeFile4(path60, "");
16803
16895
  const t = priorMtime / 1e3;
16804
- await utimes(path58, t, t);
16896
+ await utimes(path60, t, t);
16805
16897
  } catch (e) {
16806
16898
  console.log(`[autoDream] rollback failed: ${e.message} \u2014 next trigger delayed to minHours`);
16807
16899
  }
@@ -17403,12 +17495,12 @@ async function executeAndDeliver(task, now, deps) {
17403
17495
  if (promptText.startsWith("@")) {
17404
17496
  let filePath = promptText.slice(1).trim();
17405
17497
  try {
17406
- const fs56 = await import("fs");
17407
- const path58 = await import("path");
17408
- if (!path58.isAbsolute(filePath)) {
17409
- filePath = path58.join(deps.sessions["config"].stateDir, filePath);
17498
+ const fs58 = await import("fs");
17499
+ const path60 = await import("path");
17500
+ if (!path60.isAbsolute(filePath)) {
17501
+ filePath = path60.join(deps.sessions["config"].stateDir, filePath);
17410
17502
  }
17411
- promptText = fs56.readFileSync(filePath, "utf-8");
17503
+ promptText = fs58.readFileSync(filePath, "utf-8");
17412
17504
  console.log(`[cron] Loaded prompt from ${filePath} (${promptText.length} chars)`);
17413
17505
  } catch (err) {
17414
17506
  throw new Error(`Prompt file not found: ${filePath}: ${err.message}`);
@@ -17446,19 +17538,19 @@ async function executeAndDeliver(task, now, deps) {
17446
17538
  let finalResult = result;
17447
17539
  if (task.postProcess) {
17448
17540
  try {
17449
- const path58 = await import("path");
17450
- const fs56 = await import("fs");
17541
+ const path60 = await import("path");
17542
+ const fs58 = await import("fs");
17451
17543
  let scriptPath = task.postProcess;
17452
- if (!path58.isAbsolute(scriptPath)) {
17453
- scriptPath = path58.join(deps.sessions["config"].stateDir, scriptPath);
17454
- }
17455
- const resultsDirTmp = path58.join(getStorageDir(), "results");
17456
- fs56.mkdirSync(resultsDirTmp, { recursive: true });
17457
- const inputFile = path58.join(resultsDirTmp, `${task.id}.input.txt`);
17458
- fs56.writeFileSync(inputFile, result, "utf-8");
17459
- const { execFile: execFile4 } = await import("child_process");
17544
+ if (!path60.isAbsolute(scriptPath)) {
17545
+ scriptPath = path60.join(deps.sessions["config"].stateDir, scriptPath);
17546
+ }
17547
+ const resultsDirTmp = path60.join(getStorageDir(), "results");
17548
+ fs58.mkdirSync(resultsDirTmp, { recursive: true });
17549
+ const inputFile = path60.join(resultsDirTmp, `${task.id}.input.txt`);
17550
+ fs58.writeFileSync(inputFile, result, "utf-8");
17551
+ const { execFile: execFile5 } = await import("child_process");
17460
17552
  await new Promise((resolve12) => {
17461
- execFile4("python", [scriptPath, "main", "--file", inputFile], {
17553
+ execFile5("python", [scriptPath, "main", "--file", inputFile], {
17462
17554
  cwd: deps.sessions["config"].stateDir,
17463
17555
  timeout: 3e4,
17464
17556
  maxBuffer: 1024 * 1024,
@@ -17482,12 +17574,12 @@ async function executeAndDeliver(task, now, deps) {
17482
17574
  }
17483
17575
  }
17484
17576
  try {
17485
- const fs56 = await import("fs");
17486
- const path58 = await import("path");
17487
- const resultsDir = path58.join(getStorageDir(), "results");
17488
- fs56.mkdirSync(resultsDir, { recursive: true });
17489
- const resultFile = path58.join(resultsDir, `${task.id}.json`);
17490
- fs56.writeFileSync(resultFile, JSON.stringify({
17577
+ const fs58 = await import("fs");
17578
+ const path60 = await import("path");
17579
+ const resultsDir = path60.join(getStorageDir(), "results");
17580
+ fs58.mkdirSync(resultsDir, { recursive: true });
17581
+ const resultFile = path60.join(resultsDir, `${task.id}.json`);
17582
+ fs58.writeFileSync(resultFile, JSON.stringify({
17491
17583
  taskId: task.id,
17492
17584
  description: task.description,
17493
17585
  executedAt: now.toISOString(),
@@ -17817,30 +17909,30 @@ function registerCronTools() {
17817
17909
  }
17818
17910
  },
17819
17911
  handler: async (args2) => {
17820
- const fs56 = await import("fs");
17821
- const path58 = await import("path");
17822
- const resultsDir = path58.join(getStorageDir(), "results");
17823
- if (!fs56.existsSync(resultsDir)) {
17912
+ const fs58 = await import("fs");
17913
+ const path60 = await import("path");
17914
+ const resultsDir = path60.join(getStorageDir(), "results");
17915
+ if (!fs58.existsSync(resultsDir)) {
17824
17916
  return { content: "\u6682\u65E0cron\u6267\u884C\u7ED3\u679C" };
17825
17917
  }
17826
17918
  if (args2.task_id) {
17827
- const file2 = path58.join(resultsDir, `${args2.task_id}.json`);
17828
- if (!fs56.existsSync(file2)) {
17919
+ const file2 = path60.join(resultsDir, `${args2.task_id}.json`);
17920
+ if (!fs58.existsSync(file2)) {
17829
17921
  return { content: `\u4EFB\u52A1 ${args2.task_id} \u6682\u65E0\u6267\u884C\u7ED3\u679C`, isError: true };
17830
17922
  }
17831
- const data = JSON.parse(fs56.readFileSync(file2, "utf-8"));
17923
+ const data = JSON.parse(fs58.readFileSync(file2, "utf-8"));
17832
17924
  return { content: `## ${data.description}
17833
17925
  \u6267\u884C\u65F6\u95F4: ${data.executedAt}
17834
17926
  \u7B2C${data.runCount}\u6B21\u6267\u884C
17835
17927
 
17836
17928
  ${data.result}` };
17837
17929
  }
17838
- const files = fs56.readdirSync(resultsDir).filter((f) => f.endsWith(".json"));
17930
+ const files = fs58.readdirSync(resultsDir).filter((f) => f.endsWith(".json"));
17839
17931
  if (files.length === 0) {
17840
17932
  return { content: "\u6682\u65E0cron\u6267\u884C\u7ED3\u679C" };
17841
17933
  }
17842
17934
  const results = files.map((f) => {
17843
- const data = JSON.parse(fs56.readFileSync(path58.join(resultsDir, f), "utf-8"));
17935
+ const data = JSON.parse(fs58.readFileSync(path60.join(resultsDir, f), "utf-8"));
17844
17936
  return `### ${data.description} (${data.taskId.slice(0, 8)})
17845
17937
  \u6267\u884C: ${data.executedAt} | \u7B2C${data.runCount}\u6B21
17846
17938
  ${data.result.slice(0, 500)}${data.result.length > 500 ? "..." : ""}`;
@@ -18383,10 +18475,13 @@ function readEngineVersion() {
18383
18475
  }
18384
18476
  return "unknown";
18385
18477
  }
18478
+ function hasUsable(v) {
18479
+ return !!v && !isUnresolvedEnvRef(v);
18480
+ }
18386
18481
  function channelHasCredentials(name, conf) {
18387
- if (name === "feishu") return !!(conf?.appId && conf?.appSecret);
18388
- if (name === "wechat") return !!(conf?.token && conf?.accountId);
18389
- if (name === "discord") return !!conf?.accounts?.default?.token;
18482
+ if (name === "feishu") return hasUsable(conf?.appId) && hasUsable(conf?.appSecret);
18483
+ if (name === "wechat") return hasUsable(conf?.token) && hasUsable(conf?.accountId);
18484
+ if (name === "discord") return hasUsable(conf?.accounts?.default?.token);
18390
18485
  return true;
18391
18486
  }
18392
18487
  function buildEngineStatus(config2, uptimeSec, version) {
@@ -18401,6 +18496,7 @@ function buildEngineStatus(config2, uptimeSec, version) {
18401
18496
  var init_api_status = __esm({
18402
18497
  "src/startup/api-status.ts"() {
18403
18498
  "use strict";
18499
+ init_loader();
18404
18500
  }
18405
18501
  });
18406
18502
 
@@ -18502,7 +18598,7 @@ var reply_blocklist_exports = {};
18502
18598
  __export(reply_blocklist_exports, {
18503
18599
  isUserBlocked: () => isUserBlocked
18504
18600
  });
18505
- import { readFileSync as readFileSync33, writeFileSync as writeFileSync11, existsSync as existsSync22 } from "node:fs";
18601
+ import { readFileSync as readFileSync33, writeFileSync as writeFileSync11, existsSync as existsSync23 } from "node:fs";
18506
18602
  import { join as join44 } from "node:path";
18507
18603
  function ensureLoaded(workspace, configIds) {
18508
18604
  if (loaded) return;
@@ -18511,10 +18607,10 @@ function ensureLoaded(workspace, configIds) {
18511
18607
  if (!state.blockedUserIds.includes(id)) state.blockedUserIds.push(id);
18512
18608
  }
18513
18609
  }
18514
- const path58 = join44(workspace, ".reply-blocklist.json");
18610
+ const path60 = join44(workspace, ".reply-blocklist.json");
18515
18611
  try {
18516
- if (existsSync22(path58)) {
18517
- const raw = readFileSync33(path58, "utf-8");
18612
+ if (existsSync23(path60)) {
18613
+ const raw = readFileSync33(path60, "utf-8");
18518
18614
  const parsed = JSON.parse(raw);
18519
18615
  if (parsed.blockedUserIds) {
18520
18616
  for (const id of parsed.blockedUserIds) {
@@ -18530,9 +18626,9 @@ function ensureLoaded(workspace, configIds) {
18530
18626
  loaded = true;
18531
18627
  }
18532
18628
  function save(workspace) {
18533
- const path58 = join44(workspace, ".reply-blocklist.json");
18629
+ const path60 = join44(workspace, ".reply-blocklist.json");
18534
18630
  try {
18535
- writeFileSync11(path58, JSON.stringify(state, null, 2), "utf-8");
18631
+ writeFileSync11(path60, JSON.stringify(state, null, 2), "utf-8");
18536
18632
  } catch (err) {
18537
18633
  console.warn(`[reply-blocklist] Failed to save: ${err.message}`);
18538
18634
  }
@@ -18975,16 +19071,16 @@ __export(voice_chat_tts_daemon_exports, {
18975
19071
  startTtsDaemon: () => startTtsDaemon,
18976
19072
  stopTtsDaemon: () => stopTtsDaemon
18977
19073
  });
18978
- import { spawn as spawn6 } from "child_process";
18979
- import net3 from "net";
18980
- import path40 from "path";
18981
- import fs41 from "fs";
19074
+ import { spawn as spawn5 } from "child_process";
19075
+ import net2 from "net";
19076
+ import path39 from "path";
19077
+ import fs39 from "fs";
18982
19078
  function _log(msg2) {
18983
19079
  console.log(`[tts-daemon] ${msg2}`);
18984
19080
  }
18985
19081
  function _healthOnce() {
18986
19082
  return new Promise((resolve12) => {
18987
- const sock = net3.connect(SOCK_PATH);
19083
+ const sock = net2.connect(SOCK_PATH);
18988
19084
  sock.setTimeout(2e3);
18989
19085
  sock.on("connect", () => {
18990
19086
  sock.write(JSON.stringify({ action: "health" }) + "\n");
@@ -19023,14 +19119,14 @@ async function startTtsDaemon(warmupCfg) {
19023
19119
  return;
19024
19120
  }
19025
19121
  const script = (() => {
19026
- const distDir = path40.dirname(new URL(import.meta.url).pathname);
19122
+ const distDir = path39.dirname(new URL(import.meta.url).pathname);
19027
19123
  const candidates = [
19028
- path40.join(distDir, "..", "python", "tts_daemon.py"),
19029
- path40.join(process.cwd(), "engine/src/python/tts_daemon.py"),
19124
+ path39.join(distDir, "..", "python", "tts_daemon.py"),
19125
+ path39.join(process.cwd(), "engine/src/python/tts_daemon.py"),
19030
19126
  "/Users/chongzhang/work/twinsun-hearth/engine/src/python/tts_daemon.py"
19031
19127
  ];
19032
19128
  for (const c of candidates) {
19033
- if (fs41.existsSync(c)) return c;
19129
+ if (fs39.existsSync(c)) return c;
19034
19130
  }
19035
19131
  return candidates[0];
19036
19132
  })();
@@ -19045,7 +19141,7 @@ async function startTtsDaemon(warmupCfg) {
19045
19141
  ...warmupCfg?.model ? { TTS_MODEL: warmupCfg.model } : {},
19046
19142
  ...warmupCfg?.workspaceId ? { TTS_WORKSPACE_ID: warmupCfg.workspaceId } : {}
19047
19143
  };
19048
- daemonProcess = spawn6("python3", [script], {
19144
+ daemonProcess = spawn5("python3", [script], {
19049
19145
  stdio: ["pipe", "pipe", "pipe"],
19050
19146
  env
19051
19147
  });
@@ -19102,15 +19198,15 @@ var read_exports = {};
19102
19198
  __export(read_exports, {
19103
19199
  readFileState: () => readFileState
19104
19200
  });
19105
- import * as fs49 from "node:fs";
19106
- import * as path48 from "node:path";
19201
+ import * as fs51 from "node:fs";
19202
+ import * as path50 from "node:path";
19107
19203
  function isBlockedDevicePath(filePath) {
19108
19204
  if (BLOCKED_DEVICE_PATHS.has(filePath)) return true;
19109
19205
  if (filePath.startsWith("/proc/") && (filePath.endsWith("/fd/0") || filePath.endsWith("/fd/1") || filePath.endsWith("/fd/2"))) return true;
19110
19206
  return false;
19111
19207
  }
19112
19208
  function checkReadLoop(filePath, offset, limit) {
19113
- const stat4 = fs49.statSync(filePath);
19209
+ const stat4 = fs51.statSync(filePath);
19114
19210
  const mtimeMs = stat4.mtimeMs;
19115
19211
  const prev = readHistory.get(filePath);
19116
19212
  if (prev && prev.offset === offset && prev.limit === limit && prev.mtimeMs === mtimeMs) {
@@ -19124,19 +19220,19 @@ function checkReadLoop(filePath, offset, limit) {
19124
19220
  return null;
19125
19221
  }
19126
19222
  function readFileContent(filePath) {
19127
- const fd = fs49.openSync(filePath, "r");
19223
+ const fd = fs51.openSync(filePath, "r");
19128
19224
  const bom = Buffer.alloc(2);
19129
- fs49.readSync(fd, bom, 0, 2, 0);
19130
- fs49.closeSync(fd);
19225
+ fs51.readSync(fd, bom, 0, 2, 0);
19226
+ fs51.closeSync(fd);
19131
19227
  let encoding = "utf8";
19132
19228
  if (bom[0] === 255 && bom[1] === 254) {
19133
19229
  encoding = "utf16le";
19134
19230
  }
19135
- const stat4 = fs49.statSync(filePath);
19231
+ const stat4 = fs51.statSync(filePath);
19136
19232
  if (stat4.size > MAX_FILE_SIZE) {
19137
19233
  throw new Error(`\u6587\u4EF6\u592A\u5927 (${(stat4.size / 1024).toFixed(1)}KB)\uFF0C\u8D85\u8FC7 ${MAX_FILE_SIZE / 1024}KB \u9650\u5236\u3002\u8BF7\u4F7F\u7528 offset + limit \u5206\u6BB5\u8BFB\u53D6\u3002`);
19138
19234
  }
19139
- const raw = fs49.readFileSync(filePath, encoding);
19235
+ const raw = fs51.readFileSync(filePath, encoding);
19140
19236
  const content = raw.toString().replaceAll("\r\n", "\n");
19141
19237
  return { content, encoding };
19142
19238
  }
@@ -19278,11 +19374,11 @@ Usage:
19278
19374
  } catch (e) {
19279
19375
  return { content: e.message, isError: true };
19280
19376
  }
19281
- if (!fs49.existsSync(filePath)) {
19377
+ if (!fs51.existsSync(filePath)) {
19282
19378
  return { content: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`, isError: true };
19283
19379
  }
19284
- const stat4 = fs49.statSync(filePath);
19285
- const baseName = path48.basename(filePath).toUpperCase();
19380
+ const stat4 = fs51.statSync(filePath);
19381
+ const baseName = path50.basename(filePath).toUpperCase();
19286
19382
  if (BLOCKED_BASENAMES.has(baseName)) {
19287
19383
  return { content: `\u8BBE\u5907\u6587\u4EF6\u4E0D\u652F\u6301\u8BFB\u53D6: ${filePath}`, isError: true };
19288
19384
  }
@@ -19290,11 +19386,11 @@ Usage:
19290
19386
  return { content: `\u8BBE\u5907\u6587\u4EF6\u4F1A\u963B\u585E\u6216\u4EA7\u751F\u65E0\u9650\u8F93\u51FA: ${filePath}`, isError: true };
19291
19387
  }
19292
19388
  if (stat4.isDirectory()) {
19293
- const entries2 = fs49.readdirSync(filePath);
19389
+ const entries2 = fs51.readdirSync(filePath);
19294
19390
  const items = entries2.map((e) => {
19295
- const full = path48.join(filePath, e);
19391
+ const full = path50.join(filePath, e);
19296
19392
  try {
19297
- const s = fs49.statSync(full);
19393
+ const s = fs51.statSync(full);
19298
19394
  return s.isDirectory() ? `${e}/` : e;
19299
19395
  } catch {
19300
19396
  return e;
@@ -19303,7 +19399,7 @@ Usage:
19303
19399
  return { content: `\u76EE\u5F55 (${entries2.length} \u9879):
19304
19400
  ${items.join("\n")}` };
19305
19401
  }
19306
- const ext = path48.extname(filePath).toLowerCase();
19402
+ const ext = path50.extname(filePath).toLowerCase();
19307
19403
  if (BINARY_EXTENSIONS.has(ext)) {
19308
19404
  return { content: `\u4E8C\u8FDB\u5236\u6587\u4EF6\u4E0D\u652F\u6301\u8BFB\u53D6 (${ext}): ${filePath}`, isError: true };
19309
19405
  }
@@ -19351,22 +19447,22 @@ ${result}` : result };
19351
19447
 
19352
19448
  // src/tools/write.ts
19353
19449
  var write_exports = {};
19354
- import * as fs50 from "node:fs";
19355
- import * as path49 from "node:path";
19450
+ import * as fs52 from "node:fs";
19451
+ import * as path51 from "node:path";
19356
19452
  function isBlockedPath(filePath) {
19357
19453
  return BLOCKED_PATTERNS.some((p) => p.test(filePath));
19358
19454
  }
19359
19455
  function atomicWrite(filePath, content) {
19360
19456
  const tmpPath = filePath + ".tmp." + Date.now() + ".write";
19361
- fs50.writeFileSync(tmpPath, content, "utf-8");
19457
+ fs52.writeFileSync(tmpPath, content, "utf-8");
19362
19458
  try {
19363
- fs50.renameSync(tmpPath, filePath);
19459
+ fs52.renameSync(tmpPath, filePath);
19364
19460
  } catch (e) {
19365
19461
  try {
19366
- fs50.unlinkSync(tmpPath);
19462
+ fs52.unlinkSync(tmpPath);
19367
19463
  } catch {
19368
19464
  }
19369
- fs50.writeFileSync(filePath, content, "utf-8");
19465
+ fs52.writeFileSync(filePath, content, "utf-8");
19370
19466
  }
19371
19467
  }
19372
19468
  function simpleDiff(oldContent, newContent) {
@@ -19460,15 +19556,15 @@ Usage:
19460
19556
  }
19461
19557
  const rawContent = args2.content;
19462
19558
  const content = rawContent.replaceAll("\r\n", "\n");
19463
- if (fs50.existsSync(filePath) && fs50.statSync(filePath).isDirectory()) {
19559
+ if (fs52.existsSync(filePath) && fs52.statSync(filePath).isDirectory()) {
19464
19560
  return { content: `\u8DEF\u5F84\u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6: ${filePath}`, isError: true };
19465
19561
  }
19466
19562
  let oldContent = null;
19467
19563
  let isCreate = true;
19468
- if (fs50.existsSync(filePath)) {
19564
+ if (fs52.existsSync(filePath)) {
19469
19565
  isCreate = false;
19470
19566
  try {
19471
- oldContent = fs50.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n");
19567
+ oldContent = fs52.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n");
19472
19568
  } catch {
19473
19569
  isCreate = true;
19474
19570
  }
@@ -19484,10 +19580,10 @@ Usage:
19484
19580
  isError: true
19485
19581
  };
19486
19582
  }
19487
- const currentStat = fs50.statSync(filePath);
19583
+ const currentStat = fs52.statSync(filePath);
19488
19584
  const lastWriteTime = Math.floor(currentStat.mtimeMs);
19489
19585
  if (lastWriteTime > readState.timestamp) {
19490
- const currentContent = fs50.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n");
19586
+ const currentContent = fs52.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n");
19491
19587
  if (currentContent !== oldContent) {
19492
19588
  return {
19493
19589
  content: `\u6587\u4EF6\u5728\u8BFB\u53D6\u540E\u88AB\u4FEE\u6539\u3002\u8BF7\u5148\u91CD\u65B0\u8BFB\u53D6\u6587\u4EF6\u518D\u5199\u5165: ${filePath}`,
@@ -19496,9 +19592,9 @@ Usage:
19496
19592
  }
19497
19593
  }
19498
19594
  }
19499
- const dir = path49.dirname(filePath);
19595
+ const dir = path51.dirname(filePath);
19500
19596
  try {
19501
- fs50.mkdirSync(dir, { recursive: true });
19597
+ fs52.mkdirSync(dir, { recursive: true });
19502
19598
  } catch (e) {
19503
19599
  return { content: `\u65E0\u6CD5\u521B\u5EFA\u76EE\u5F55: ${dir} \u2014 ${e.message}`, isError: true };
19504
19600
  }
@@ -19507,11 +19603,11 @@ Usage:
19507
19603
  } catch (e) {
19508
19604
  return { content: `\u5199\u5165\u5931\u8D25: ${e.message}`, isError: true };
19509
19605
  }
19510
- readFileState.set(filePath, { timestamp: fs50.statSync(filePath).mtimeMs });
19606
+ readFileState.set(filePath, { timestamp: fs52.statSync(filePath).mtimeMs });
19511
19607
  const action = isCreate ? "\u521B\u5EFA" : "\u66F4\u65B0";
19512
19608
  const lines = content.split("\n").length;
19513
19609
  const chars = content.length;
19514
- const stat4 = fs50.statSync(filePath);
19610
+ const stat4 = fs52.statSync(filePath);
19515
19611
  let diff = "";
19516
19612
  if (!isCreate && oldContent !== null) {
19517
19613
  diff = `
@@ -19531,8 +19627,8 @@ ${simpleDiff(oldContent, content)}`;
19531
19627
 
19532
19628
  // src/tools/edit.ts
19533
19629
  var edit_exports = {};
19534
- import * as fs51 from "node:fs";
19535
- import * as path50 from "node:path";
19630
+ import * as fs53 from "node:fs";
19631
+ import * as path52 from "node:path";
19536
19632
  function normalizeQuotes(str) {
19537
19633
  return str.replaceAll(LEFT_SINGLE_CURLY, "'").replaceAll(RIGHT_SINGLE_CURLY, "'").replaceAll(LEFT_DOUBLE_CURLY, '"').replaceAll(RIGHT_DOUBLE_CURLY, '"');
19538
19634
  }
@@ -19685,7 +19781,7 @@ Usage:
19685
19781
  }
19686
19782
  let fileContent = null;
19687
19783
  try {
19688
- const stat4 = fs51.statSync(filePath);
19784
+ const stat4 = fs53.statSync(filePath);
19689
19785
  if (stat4.isDirectory()) {
19690
19786
  return { content: `\u8DEF\u5F84\u662F\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6: ${filePath}`, isError: true };
19691
19787
  }
@@ -19695,21 +19791,21 @@ Usage:
19695
19791
  } catch (e) {
19696
19792
  if (e.code === "ENOENT") {
19697
19793
  if (oldString === "") {
19698
- const dir = path50.dirname(filePath);
19699
- fs51.mkdirSync(dir, { recursive: true });
19700
- fs51.writeFileSync(filePath, newString, "utf-8");
19701
- readFileState.set(filePath, { timestamp: fs51.statSync(filePath).mtimeMs });
19794
+ const dir = path52.dirname(filePath);
19795
+ fs53.mkdirSync(dir, { recursive: true });
19796
+ fs53.writeFileSync(filePath, newString, "utf-8");
19797
+ readFileState.set(filePath, { timestamp: fs53.statSync(filePath).mtimeMs });
19702
19798
  return { content: `\u521B\u5EFA\u6587\u4EF6: ${filePath} (${newString.split("\n").length} \u884C)` };
19703
19799
  }
19704
19800
  return { content: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`, isError: true };
19705
19801
  }
19706
19802
  throw e;
19707
19803
  }
19708
- const rawContent = fs51.readFileSync(filePath, "utf-8");
19804
+ const rawContent = fs53.readFileSync(filePath, "utf-8");
19709
19805
  fileContent = rawContent.replaceAll("\r\n", "\n");
19710
19806
  if (oldString === "" && fileContent.trim() === "") {
19711
- fs51.writeFileSync(filePath, newString, "utf-8");
19712
- readFileState.set(filePath, { timestamp: fs51.statSync(filePath).mtimeMs });
19807
+ fs53.writeFileSync(filePath, newString, "utf-8");
19808
+ readFileState.set(filePath, { timestamp: fs53.statSync(filePath).mtimeMs });
19713
19809
  return { content: `\u5199\u5165\u7A7A\u6587\u4EF6: ${filePath} (${newString.split("\n").length} \u884C)` };
19714
19810
  }
19715
19811
  const readState = readFileState.get(filePath);
@@ -19719,11 +19815,11 @@ Usage:
19719
19815
  isError: true
19720
19816
  };
19721
19817
  }
19722
- const currentStat = fs51.statSync(filePath);
19818
+ const currentStat = fs53.statSync(filePath);
19723
19819
  const lastWriteTime = Math.floor(currentStat.mtimeMs);
19724
19820
  if (lastWriteTime > readState.timestamp) {
19725
19821
  if (fileContent !== rawContent.replaceAll("\r\n", "\n")) {
19726
- if (fileContent !== fs51.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n")) {
19822
+ if (fileContent !== fs53.readFileSync(filePath, "utf-8").replaceAll("\r\n", "\n")) {
19727
19823
  return {
19728
19824
  content: `\u6587\u4EF6\u5728\u8BFB\u53D6\u540E\u88AB\u4FEE\u6539\u3002\u8BF7\u5148\u91CD\u65B0\u8BFB\u53D6\u6587\u4EF6\u518D\u7F16\u8F91: ${filePath}`,
19729
19825
  isError: true
@@ -19755,8 +19851,8 @@ ${preview}
19755
19851
  const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
19756
19852
  const diffView = generateEditDiff(fileContent, actualOldString, actualNewString);
19757
19853
  const newContent = applyEditToFile(fileContent, actualOldString, actualNewString, replaceAll);
19758
- fs51.writeFileSync(filePath, newContent, "utf-8");
19759
- readFileState.set(filePath, { timestamp: fs51.statSync(filePath).mtimeMs });
19854
+ fs53.writeFileSync(filePath, newContent, "utf-8");
19855
+ readFileState.set(filePath, { timestamp: fs53.statSync(filePath).mtimeMs });
19760
19856
  const strategy = actualOldString === oldString ? "\u7CBE\u786E\u5339\u914D" : "\u5F15\u53F7\u89C4\u8303\u5316\u5339\u914D";
19761
19857
  const count = replaceAll ? matchCount : 1;
19762
19858
  const diff = `${oldString.length}\u2192${newString.length}\u5B57\u7B26`;
@@ -19774,8 +19870,8 @@ ${diffView}`
19774
19870
 
19775
19871
  // src/tools/glob.ts
19776
19872
  var glob_exports = {};
19777
- import * as fs52 from "node:fs";
19778
- import * as path51 from "node:path";
19873
+ import * as fs54 from "node:fs";
19874
+ import * as path53 from "node:path";
19779
19875
  function globMatch(pattern, filename) {
19780
19876
  const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
19781
19877
  try {
@@ -19795,18 +19891,18 @@ function findFiles(dir, pattern, limit, baseDir) {
19795
19891
  }
19796
19892
  let entries2;
19797
19893
  try {
19798
- entries2 = fs52.readdirSync(currentDir, { withFileTypes: true });
19894
+ entries2 = fs54.readdirSync(currentDir, { withFileTypes: true });
19799
19895
  } catch {
19800
19896
  return;
19801
19897
  }
19802
19898
  for (const entry of entries2) {
19803
19899
  if (truncated) return;
19804
- const fullPath = path51.join(currentDir, entry.name);
19900
+ const fullPath = path53.join(currentDir, entry.name);
19805
19901
  if (entry.isDirectory()) {
19806
19902
  if (VCS_DIRS.has(entry.name)) continue;
19807
19903
  walk(fullPath);
19808
19904
  } else if (entry.isFile()) {
19809
- const relativePath = path51.relative(baseDir, fullPath).replace(/\\/g, "/");
19905
+ const relativePath = path53.relative(baseDir, fullPath).replace(/\\/g, "/");
19810
19906
  const patternsToTry = [pattern];
19811
19907
  if (pattern.startsWith("**/")) {
19812
19908
  patternsToTry.push(pattern.slice(3));
@@ -19816,7 +19912,7 @@ function findFiles(dir, pattern, limit, baseDir) {
19816
19912
  );
19817
19913
  if (matched) {
19818
19914
  try {
19819
- const stat4 = fs52.statSync(fullPath);
19915
+ const stat4 = fs54.statSync(fullPath);
19820
19916
  results.push({ path: fullPath, mtimeMs: stat4.mtimeMs });
19821
19917
  } catch {
19822
19918
  }
@@ -19833,7 +19929,7 @@ function findFiles(dir, pattern, limit, baseDir) {
19833
19929
  };
19834
19930
  }
19835
19931
  function toRelativePath(absolutePath, cwd) {
19836
- if (absolutePath.startsWith(cwd + path51.sep)) {
19932
+ if (absolutePath.startsWith(cwd + path53.sep)) {
19837
19933
  return absolutePath.slice(cwd.length + 1);
19838
19934
  }
19839
19935
  return absolutePath;
@@ -19866,10 +19962,10 @@ var init_glob = __esm({
19866
19962
  const searchPath = args2.path ? resolvePath(args2.path, ctx.workspace) : ctx.workspace;
19867
19963
  const pattern = args2.pattern;
19868
19964
  const limit = args2.limit || DEFAULT_LIMIT;
19869
- if (!fs52.existsSync(searchPath)) {
19965
+ if (!fs54.existsSync(searchPath)) {
19870
19966
  return { content: `\u76EE\u5F55\u4E0D\u5B58\u5728: ${searchPath}`, isError: true };
19871
19967
  }
19872
- if (!fs52.statSync(searchPath).isDirectory()) {
19968
+ if (!fs54.statSync(searchPath).isDirectory()) {
19873
19969
  return { content: `\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55: ${searchPath}`, isError: true };
19874
19970
  }
19875
19971
  const start = Date.now();
@@ -19893,12 +19989,12 @@ ${filenames.join("\n")}${truncatedNote}`
19893
19989
 
19894
19990
  // src/tools/grep.ts
19895
19991
  var grep_exports = {};
19896
- import { execFile as execFile3 } from "node:child_process";
19897
- import * as path52 from "node:path";
19992
+ import { execFile as execFile4 } from "node:child_process";
19993
+ import * as path54 from "node:path";
19898
19994
  function ripGrep(args2, searchPath, signal) {
19899
19995
  return new Promise((resolve12) => {
19900
19996
  const fullArgs = [...args2, searchPath];
19901
- const child = execFile3("rg", fullArgs, {
19997
+ const child = execFile4("rg", fullArgs, {
19902
19998
  maxBuffer: 50 * 1024 * 1024,
19903
19999
  // 50MB
19904
20000
  timeout: RG_TIMEOUT_MS,
@@ -19926,7 +20022,7 @@ function applyHeadLimit(items, limit, offset = 0) {
19926
20022
  };
19927
20023
  }
19928
20024
  function toRelativePath2(absolutePath, cwd) {
19929
- if (absolutePath.startsWith(cwd + path52.sep)) {
20025
+ if (absolutePath.startsWith(cwd + path54.sep)) {
19930
20026
  return absolutePath.slice(cwd.length + 1);
19931
20027
  }
19932
20028
  if (absolutePath.startsWith(cwd)) {
@@ -22338,7 +22434,7 @@ var init_web_fetch = __esm({
22338
22434
 
22339
22435
  // src/tools/wechat/wx-query.ts
22340
22436
  var wx_query_exports = {};
22341
- import { join as join57 } from "node:path";
22437
+ import { join as join60 } from "node:path";
22342
22438
  function getDescription() {
22343
22439
  return `\u67E5\u8BE2\u7FC0\u54E5\u7684\u5FAE\u4FE1\u6D88\u606F\uFF08\u7F13\u5B58\u89E3\u5BC6\u540E\u7684\u672C\u5730\u6570\u636E\u5E93\uFF09\u3002
22344
22440
 
@@ -22373,8 +22469,8 @@ var init_wx_query = __esm({
22373
22469
  "use strict";
22374
22470
  init_registry();
22375
22471
  init_live();
22376
- STATE_DIR = process.env.ENGINE7_STATE_DIR || join57(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
22377
- SCRIPT = join57("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
22472
+ STATE_DIR = process.env.ENGINE7_STATE_DIR || join60(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
22473
+ SCRIPT = join60("/Users/chongzhang/work/twinsun-hearth/engine", "src", "tools", "wechat", "wx_query.py");
22378
22474
  PYTHON = "python3";
22379
22475
  registry.register({
22380
22476
  name: "wx_query",
@@ -22402,12 +22498,12 @@ var init_wx_query = __esm({
22402
22498
  handler: async (args2, _context) => {
22403
22499
  const { findShell: findShell2 } = await Promise.resolve().then(() => (init_BashTool(), BashTool_exports));
22404
22500
  const shell = findShell2();
22405
- const { spawn: spawn8 } = await import("node:child_process");
22501
+ const { spawn: spawn7 } = await import("node:child_process");
22406
22502
  const cmd = args2.command.trim();
22407
22503
  const timeout = args2.timeout || 12e4;
22408
22504
  const fullCmd = `${PYTHON} "${SCRIPT}" ${cmd.replace(/^python3\s+.*wx_query\.py\s*/, "")}`;
22409
22505
  return new Promise((resolve12) => {
22410
- const child = spawn8(shell.shell, [...shell.args, fullCmd], {
22506
+ const child = spawn7(shell.shell, [...shell.args, fullCmd], {
22411
22507
  timeout,
22412
22508
  env: { ...process.env }
22413
22509
  });
@@ -22660,10 +22756,11 @@ var init_features2 = __esm({
22660
22756
  // src/mcp/manager.ts
22661
22757
  var manager_exports = {};
22662
22758
  __export(manager_exports, {
22663
- McpManager: () => McpManager
22759
+ McpManager: () => McpManager,
22760
+ convertInputSchema: () => convertInputSchema
22664
22761
  });
22665
- import * as fs55 from "node:fs";
22666
- import * as path55 from "node:path";
22762
+ import * as fs57 from "node:fs";
22763
+ import * as path57 from "node:path";
22667
22764
  import { Client as Client3 } from "@modelcontextprotocol/sdk/client/index.js";
22668
22765
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
22669
22766
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -22681,6 +22778,14 @@ function convertInputSchema(inputSchema) {
22681
22778
  const properties = {};
22682
22779
  if (inputSchema.properties && typeof inputSchema.properties === "object") {
22683
22780
  for (const [key, prop] of Object.entries(inputSchema.properties)) {
22781
+ const union = prop.anyOf ?? prop.oneOf;
22782
+ if (union) {
22783
+ properties[key] = {
22784
+ anyOf: union,
22785
+ ...prop.description ? { description: prop.description } : {}
22786
+ };
22787
+ continue;
22788
+ }
22684
22789
  properties[key] = {
22685
22790
  type: prop.type ?? "string",
22686
22791
  ...prop.description ? { description: prop.description } : {},
@@ -22696,12 +22801,12 @@ function convertInputSchema(inputSchema) {
22696
22801
  }
22697
22802
  function persistBinary(base64Data, mimeType, persistId) {
22698
22803
  const ext = mimeType?.split("/")[1] || "bin";
22699
- const dir = path55.join(process.env.ENGINE_STATE_DIR || ".engine", "mcp-blobs");
22700
- fs55.mkdirSync(dir, { recursive: true });
22701
- const filepath = path55.join(dir, `${persistId}.${ext}`);
22804
+ const dir = path57.join(process.env.ENGINE_STATE_DIR || ".engine", "mcp-blobs");
22805
+ fs57.mkdirSync(dir, { recursive: true });
22806
+ const filepath = path57.join(dir, `${persistId}.${ext}`);
22702
22807
  try {
22703
22808
  const buf = Buffer.from(base64Data, "base64");
22704
- fs55.writeFileSync(filepath, buf);
22809
+ fs57.writeFileSync(filepath, buf);
22705
22810
  return { filepath, size: buf.length };
22706
22811
  } catch (err) {
22707
22812
  return { error: err.message };
@@ -23035,7 +23140,7 @@ __export(resources_exports, {
23035
23140
  registerMcpResourceTools: () => registerMcpResourceTools,
23036
23141
  unregisterMcpResourceTools: () => unregisterMcpResourceTools
23037
23142
  });
23038
- import * as path56 from "node:path";
23143
+ import * as path58 from "node:path";
23039
23144
  function registerMcpResourceTools(manager) {
23040
23145
  mcpManagerRef = manager;
23041
23146
  registry.register(listResourcesTool);
@@ -23053,7 +23158,7 @@ var init_resources = __esm({
23053
23158
  "use strict";
23054
23159
  init_registry();
23055
23160
  MAX_RESULT_CHARS2 = 1e5;
23056
- MEDIA_DIR = process.env.ENGINE_MEDIA_DIR || path56.join(process.env.ENGINE_STATE_DIR || ".engine", "media", "inbound");
23161
+ MEDIA_DIR = process.env.ENGINE_MEDIA_DIR || path58.join(process.env.ENGINE_STATE_DIR || ".engine", "media", "inbound");
23057
23162
  MCP_LIST_RESOURCES_TOOL = "mcp__list_resources";
23058
23163
  MCP_READ_RESOURCE_TOOL = "mcp__read_resource";
23059
23164
  mcpManagerRef = null;
@@ -23147,14 +23252,15 @@ import { basename, dirname, join as join2 } from "node:path";
23147
23252
  function secretsHome() {
23148
23253
  return process.env.HOME || process.env.USERPROFILE || ".";
23149
23254
  }
23150
- function loadSecretsEnv(stateDir, configName) {
23151
- const home = secretsHome();
23152
- const candidates = [
23153
- join2(home, ".engine7-secrets", `${configName}.env`),
23255
+ function secretsCandidates(stateDir, configName) {
23256
+ return [
23257
+ join2(secretsHome(), ".engine7-secrets", `${configName}.env`),
23154
23258
  join2(stateDir, "configs", `.env.${configName}`),
23155
23259
  join2(stateDir, "configs", ".env")
23156
23260
  ];
23157
- for (const p of candidates) {
23261
+ }
23262
+ function loadSecretsEnv(stateDir, configName) {
23263
+ for (const p of secretsCandidates(stateDir, configName)) {
23158
23264
  if (!existsSync2(p)) continue;
23159
23265
  for (const line of readFileSync2(p, "utf-8").split("\n")) {
23160
23266
  const t = line.trim();
@@ -23170,17 +23276,21 @@ function loadSecretsEnv(stateDir, configName) {
23170
23276
  }
23171
23277
  return null;
23172
23278
  }
23173
- function loadSecretsEnvForConfig(configPath2) {
23279
+ function resolveConfigIdentity(configPath2) {
23174
23280
  const cfgName = basename(configPath2, ".json");
23175
23281
  const cfgDir = dirname(configPath2);
23176
23282
  const stateDir = basename(cfgDir) === "configs" ? dirname(cfgDir) : cfgDir;
23177
- return loadSecretsEnv(stateDir, cfgName);
23283
+ return { stateDir, configName: cfgName };
23284
+ }
23285
+ function loadSecretsEnvForConfig(configPath2) {
23286
+ const { stateDir, configName } = resolveConfigIdentity(configPath2);
23287
+ return loadSecretsEnv(stateDir, configName);
23178
23288
  }
23179
23289
 
23180
23290
  // src/engine-startup.ts
23181
23291
  init_query();
23182
23292
  init_live();
23183
- import * as path57 from "node:path";
23293
+ import * as path59 from "node:path";
23184
23294
 
23185
23295
  // src/startup/providers.ts
23186
23296
  init_provider_factory();
@@ -23792,79 +23902,101 @@ function startConfigWatcher(config2, deps, provider) {
23792
23902
  }
23793
23903
  return activeWatcher;
23794
23904
  }
23795
- function startSecretsWatcher(config2, deps, provider) {
23796
- const secretsDir = path10.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7-secrets");
23905
+ function shouldHandleSecretsEvent(filename, configName, ownNames = [`${configName}.env`]) {
23906
+ if (!filename) return false;
23907
+ if (ownNames.includes(filename)) return true;
23908
+ if (filename.startsWith(".")) return false;
23909
+ if (filename.endsWith("~")) return false;
23910
+ if (/\.(swp|swx|swo|tmp)$/.test(filename)) return false;
23911
+ if (filename.includes(".tmp-")) return false;
23912
+ return filename === `${configName}.env`;
23913
+ }
23914
+ function startSecretsWatcher(config2, deps, provider, watchFn = fs10.watch) {
23797
23915
  const cfgBase = path10.basename(config2._configFilePath || "", ".json");
23798
- const secretCandidates = [
23799
- path10.join(secretsDir, `${cfgBase}.env`),
23800
- path10.join(path10.dirname(config2._configFilePath || ""), `.env.${cfgBase}`),
23801
- path10.join(path10.dirname(config2._configFilePath || ""), ".env")
23802
- ];
23803
- let secretsPath2 = null;
23804
- for (const p of secretCandidates) {
23805
- if (fs10.existsSync(p)) {
23806
- secretsPath2 = p;
23807
- break;
23808
- }
23809
- }
23810
- if (!secretsPath2) {
23811
- console.log("[secrets-watch] no .env file found, watcher disabled");
23812
- return null;
23916
+ const secretCandidates = secretsCandidates(config2.stateDir, cfgBase);
23917
+ const watchTargets = /* @__PURE__ */ new Map();
23918
+ for (const c of secretCandidates) {
23919
+ const dir = path10.dirname(c);
23920
+ const names = watchTargets.get(dir);
23921
+ if (names) names.push(path10.basename(c));
23922
+ else watchTargets.set(dir, [path10.basename(c)]);
23813
23923
  }
23814
23924
  let debounceTimer = null;
23815
- let activeWatcher = null;
23816
- const startWatch = () => {
23817
- if (activeWatcher) activeWatcher.close();
23818
- activeWatcher = fs10.watch(secretsPath2, { persistent: true }, (eventType) => {
23819
- if (eventType === "rename") {
23820
- if (fs10.existsSync(secretsPath2)) {
23821
- console.log("[secrets-watch] rename detected, re-watching file...");
23822
- startWatch();
23823
- } else {
23824
- console.log("[secrets-watch] file deleted, watcher paused");
23825
- return;
23826
- }
23925
+ const onSecretsEvent = (eventType, filename, ownNames) => {
23926
+ if (!shouldHandleSecretsEvent(String(filename || ""), cfgBase, ownNames)) return;
23927
+ if (debounceTimer) clearTimeout(debounceTimer);
23928
+ debounceTimer = setTimeout(async () => {
23929
+ const p = secretCandidates.find((c) => fs10.existsSync(c));
23930
+ if (!p) {
23931
+ console.log("[secrets-watch] \u4E8B\u4EF6\u8FC7\u540E\u6587\u4EF6\u4ECD\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7");
23932
+ return;
23827
23933
  }
23828
- if (debounceTimer) clearTimeout(debounceTimer);
23829
- debounceTimer = setTimeout(async () => {
23830
- console.log(`[secrets-watch] file changed (${eventType}), reloading secrets...`);
23831
- try {
23832
- const content = fs10.readFileSync(secretsPath2, "utf-8");
23833
- let updated = 0;
23834
- for (const line of content.split("\n")) {
23835
- const trimmed = line.trim();
23836
- if (!trimmed || trimmed.startsWith("#")) continue;
23837
- const eq = trimmed.indexOf("=");
23838
- if (eq > 0) {
23839
- const k = trimmed.slice(0, eq).trim();
23840
- const v = trimmed.slice(eq + 1).trim();
23841
- if (k) {
23842
- process.env[k] = v;
23843
- updated++;
23844
- }
23934
+ console.log(`[secrets-watch] file changed (${eventType}), reloading secrets...`);
23935
+ try {
23936
+ const content = fs10.readFileSync(p, "utf-8");
23937
+ let updated = 0;
23938
+ for (const line of content.split("\n")) {
23939
+ const trimmed = line.trim();
23940
+ if (!trimmed || trimmed.startsWith("#")) continue;
23941
+ const eq = trimmed.indexOf("=");
23942
+ if (eq > 0) {
23943
+ const k = trimmed.slice(0, eq).trim();
23944
+ const v = trimmed.slice(eq + 1).trim();
23945
+ if (k) {
23946
+ process.env[k] = v;
23947
+ updated++;
23845
23948
  }
23846
23949
  }
23847
- console.log(`[secrets-watch] ${updated} keys reloaded from ${secretsPath2}`);
23848
- console.log(`[secrets-watch] force reload\uFF08\u8DF3\u8FC7\u539F\u6587\u5BF9\u6BD4\uFF09`);
23849
- const result = await doReloadConfig(config2, deps, provider, true);
23850
- console.log(`[secrets-watch] config reloaded: ok=${result.ok} changes=${result.changes.join(",")}`);
23851
- try {
23852
- fs10.appendFileSync(path10.join(config2.stateDir, "logs", "engine-config-watch.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] SECRETS RELOAD: ok=${result.ok} keys=${updated}
23950
+ }
23951
+ console.log(`[secrets-watch] ${updated} keys reloaded from ${p}`);
23952
+ console.log(`[secrets-watch] force reload\uFF08\u8DF3\u8FC7\u539F\u6587\u5BF9\u6BD4\uFF09`);
23953
+ const result = await doReloadConfig(config2, deps, provider, true);
23954
+ console.log(`[secrets-watch] config reloaded: ok=${result.ok} changes=${result.changes.join(",")}`);
23955
+ try {
23956
+ fs10.appendFileSync(path10.join(config2.stateDir, "logs", "engine-config-watch.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] SECRETS RELOAD: ok=${result.ok} keys=${updated}
23853
23957
  `);
23854
- } catch {
23855
- }
23856
- } catch (err) {
23857
- console.error(`[secrets-watch] reload failed: ${err.message}`);
23958
+ } catch {
23858
23959
  }
23859
- }, 500);
23860
- });
23861
- activeWatcher.on("error", (err) => {
23960
+ } catch (err) {
23961
+ console.error(`[secrets-watch] reload failed: ${err.message}`);
23962
+ }
23963
+ }, 500);
23964
+ };
23965
+ const watchers = [];
23966
+ for (const [dir, ownNames] of watchTargets) {
23967
+ try {
23968
+ fs10.mkdirSync(dir, { recursive: true });
23969
+ } catch {
23970
+ }
23971
+ try {
23972
+ watchers.push(watchFn(dir, (eventType, filename) => onSecretsEvent(eventType, filename, ownNames)));
23973
+ } catch (err) {
23974
+ console.warn(`[secrets-watch] watch ${dir} failed: ${err.message}\uFF0C\u8BE5\u76EE\u5F55\u9000\u5316\u4E3A"\u6C38\u4E0D\u89E6\u53D1"`);
23975
+ }
23976
+ }
23977
+ if (watchers.length === 0) {
23978
+ console.warn("[secrets-watch] \u6CA1\u6709\u76D1\u542C\u4E0A\u7684 secrets \u76EE\u5F55\uFF0Cwatcher disabled");
23979
+ return null;
23980
+ }
23981
+ for (const w of watchers) {
23982
+ w.on("error", (err) => {
23862
23983
  console.error(`[secrets-watch] error: ${err.message}`);
23863
23984
  });
23864
- };
23865
- startWatch();
23866
- console.log(`[secrets-watch] watching ${secretsPath2}`);
23867
- return activeWatcher;
23985
+ }
23986
+ const primary = watchers[0];
23987
+ if (watchers.length > 1) {
23988
+ const closes = watchers.map((w) => w.close.bind(w));
23989
+ primary.close = () => {
23990
+ for (const c of closes) {
23991
+ try {
23992
+ c();
23993
+ } catch {
23994
+ }
23995
+ }
23996
+ };
23997
+ }
23998
+ console.log(`[secrets-watch] watching ${[...watchTargets.keys()].join(", ")} (per-event path resolution, profile=${cfgBase})`);
23999
+ return primary;
23868
24000
  }
23869
24001
 
23870
24002
  // src/startup/bootstrap.ts
@@ -24511,7 +24643,7 @@ ${formatted}` };
24511
24643
  };
24512
24644
  }
24513
24645
  function createEverosGetTool() {
24514
- const fs56 = __require("node:fs/promises");
24646
+ const fs58 = __require("node:fs/promises");
24515
24647
  return {
24516
24648
  name: "memory_get",
24517
24649
  description: "Read a memory file by path.",
@@ -24527,7 +24659,7 @@ function createEverosGetTool() {
24527
24659
  handler: async (args2) => {
24528
24660
  try {
24529
24661
  const filePath = args2.path;
24530
- const content = await fs56.readFile(filePath, "utf-8");
24662
+ const content = await fs58.readFile(filePath, "utf-8");
24531
24663
  const lines = content.split("\n");
24532
24664
  const fromLine = args2.from ?? 1;
24533
24665
  const numLines = args2.lines ?? lines.length;
@@ -24628,9 +24760,9 @@ async function checkMediaCapabilities() {
24628
24760
  console.warn(`[opus] @discordjs/opus \u4E0D\u53EF\u7528 (${e.message}) \u2014 \u8BED\u97F3\u6761 opus \u8F6C\u6362\u56DE\u9000\u7CFB\u7EDF ffmpeg`);
24629
24761
  }
24630
24762
  try {
24631
- const { execFile: execFile4 } = await import("node:child_process");
24632
- const { promisify: promisify4 } = await import("node:util");
24633
- await promisify4(execFile4)("ffmpeg", ["-version"], { timeout: 5e3 });
24763
+ const { execFile: execFile5 } = await import("node:child_process");
24764
+ const { promisify: promisify5 } = await import("node:util");
24765
+ await promisify5(execFile5)("ffmpeg", ["-version"], { timeout: 5e3 });
24634
24766
  console.log(`[ffmpeg] OK \u2014 mp3 \u5F52\u4E00\u5316 / opus&m4a \u56DE\u9000\u53EF\u7528`);
24635
24767
  } catch {
24636
24768
  ;
@@ -26074,15 +26206,31 @@ ${ep.episode || ep.summary}`,
26074
26206
 
26075
26207
  // src/handle-query.ts
26076
26208
  init_paths();
26077
- import { readFileSync as readFileSync27, existsSync as existsSync19, statSync as statSync5 } from "node:fs";
26209
+ import { readFileSync as readFileSync27, existsSync as existsSync20, statSync as statSync5 } from "node:fs";
26078
26210
  import { join as join36, resolve as resolve7 } from "node:path";
26079
26211
  import * as path21 from "node:path";
26080
26212
 
26081
26213
  // src/light-mode.ts
26082
26214
  init_live();
26083
- init_ruleCompact();
26084
26215
  import { readFileSync as readFileSync13 } from "node:fs";
26216
+
26217
+ // src/config/persona-files.ts
26218
+ import { existsSync as existsSync11 } from "node:fs";
26085
26219
  import { join as join20 } from "node:path";
26220
+ var LEGACY_RELATIVE = {
26221
+ "my-inner-voice.md": "prompts/my-inner-voice.md"
26222
+ };
26223
+ function resolvePersonaFile(workspace, persona, name) {
26224
+ if (persona) {
26225
+ const scoped = join20(workspace, "personas", persona, name);
26226
+ if (existsSync11(scoped)) return scoped;
26227
+ }
26228
+ const legacy = join20(workspace, LEGACY_RELATIVE[name] ?? name);
26229
+ return existsSync11(legacy) ? legacy : null;
26230
+ }
26231
+
26232
+ // src/light-mode.ts
26233
+ init_ruleCompact();
26086
26234
  function isLightMode(chatMode, channelName) {
26087
26235
  return channelName === "deskBuddy" || chatMode === "emotion";
26088
26236
  }
@@ -26114,9 +26262,12 @@ function buildLightStablePrompt(workspace, mode, opts) {
26114
26262
  const lp = liveConfig.get(`channels.${mode}.liveSystemPrompt`);
26115
26263
  const parts = [];
26116
26264
  const files = lp?.files ?? (mode === "emotion" ? ["SOUL.md", "EMOTION.md"] : ["SOUL.md"]);
26265
+ const persona = liveConfig.get("agents.defaults.persona");
26117
26266
  for (const f of files) {
26267
+ const filePath = resolvePersonaFile(workspace, persona, f);
26268
+ if (!filePath) continue;
26118
26269
  try {
26119
- parts.push(readFileSync13(join20(workspace, f), "utf-8").trim());
26270
+ parts.push(readFileSync13(filePath, "utf-8").trim());
26120
26271
  } catch {
26121
26272
  }
26122
26273
  }
@@ -26200,7 +26351,7 @@ function voiceFastView(history, opts = VOICE_FAST_DEFAULTS) {
26200
26351
 
26201
26352
  // src/sender-context.ts
26202
26353
  init_clock();
26203
- import { readFileSync as readFileSync14, existsSync as existsSync12 } from "node:fs";
26354
+ import { readFileSync as readFileSync14, existsSync as existsSync13 } from "node:fs";
26204
26355
  import { join as join21 } from "node:path";
26205
26356
  var contactMap = null;
26206
26357
  var externalChanWhitelist = null;
@@ -26210,8 +26361,8 @@ function loadContactMap(workspace) {
26210
26361
  externalChanWhitelist = /* @__PURE__ */ new Set();
26211
26362
  try {
26212
26363
  const contactsPath = join21(workspace, "prompts", "contacts.md");
26213
- console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync12(contactsPath)}`);
26214
- if (existsSync12(contactsPath)) {
26364
+ console.log(`[meta] Loading contacts from ${contactsPath}, exists=${existsSync13(contactsPath)}`);
26365
+ if (existsSync13(contactsPath)) {
26215
26366
  const text = readFileSync14(contactsPath, "utf-8");
26216
26367
  const lines = text.split("\n");
26217
26368
  for (const line of lines) {
@@ -26301,18 +26452,18 @@ function resetSessionStartInjection(sessionId) {
26301
26452
  }
26302
26453
  var externalChanRulesCache = null;
26303
26454
  function loadExternalChanRules(workspace) {
26304
- const path58 = join36(workspace, "prompts", "external-chan-rules.md");
26305
- if (externalChanRulesCache && externalChanRulesCache.path === path58) return externalChanRulesCache;
26455
+ const path60 = join36(workspace, "prompts", "external-chan-rules.md");
26456
+ if (externalChanRulesCache && externalChanRulesCache.path === path60) return externalChanRulesCache;
26306
26457
  let content = "";
26307
- if (existsSync19(path58)) {
26458
+ if (existsSync20(path60)) {
26308
26459
  try {
26309
- content = readFileSync27(path58, "utf-8").trim();
26460
+ content = readFileSync27(path60, "utf-8").trim();
26310
26461
  } catch (e) {
26311
26462
  console.warn(`[external-chan-rules] Failed to load: ${e}`);
26312
26463
  }
26313
26464
  }
26314
- externalChanRulesCache = { path: path58, content };
26315
- console.log(`[external-chan-rules] Loaded ${content.length} chars from ${path58}`);
26465
+ externalChanRulesCache = { path: path60, content };
26466
+ console.log(`[external-chan-rules] Loaded ${content.length} chars from ${path60}`);
26316
26467
  return externalChanRulesCache;
26317
26468
  }
26318
26469
  function getExternalChanRulesBlock(inboundMeta, workspace) {
@@ -27199,15 +27350,15 @@ function registerCompactHooks(config2) {
27199
27350
  const dailyDir = path22.join(workspace, "memory", "daily");
27200
27351
  const dailyPath = path22.join(dailyDir, `${dateStr}.md`);
27201
27352
  try {
27202
- const fs56 = await import("node:fs");
27203
- if (!fs56.existsSync(dailyDir)) {
27204
- fs56.mkdirSync(dailyDir, { recursive: true });
27353
+ const fs58 = await import("node:fs");
27354
+ if (!fs58.existsSync(dailyDir)) {
27355
+ fs58.mkdirSync(dailyDir, { recursive: true });
27205
27356
  }
27206
27357
  const sessionsDir = path22.join(config2.stateDir, "agents", "main", "sessions");
27207
27358
  const sessionFile = path22.join(sessionsDir, `${sessionId}.jsonl`);
27208
27359
  const recentLines = [];
27209
- if (fs56.existsSync(sessionFile)) {
27210
- const content = fs56.readFileSync(sessionFile, "utf-8");
27360
+ if (fs58.existsSync(sessionFile)) {
27361
+ const content = fs58.readFileSync(sessionFile, "utf-8");
27211
27362
  const lines = content.trim().split("\n").filter(Boolean);
27212
27363
  const userLines = lines.filter((l) => {
27213
27364
  try {
@@ -27237,10 +27388,10 @@ function registerCompactHooks(config2) {
27237
27388
  const entry = `${header}
27238
27389
  ${body}
27239
27390
  `;
27240
- if (fs56.existsSync(dailyPath)) {
27241
- fs56.appendFileSync(dailyPath, entry);
27391
+ if (fs58.existsSync(dailyPath)) {
27392
+ fs58.appendFileSync(dailyPath, entry);
27242
27393
  } else {
27243
- fs56.writeFileSync(dailyPath, `# ${dateStr} \u65E5\u5FD7
27394
+ fs58.writeFileSync(dailyPath, `# ${dateStr} \u65E5\u5FD7
27244
27395
  ${entry}`);
27245
27396
  }
27246
27397
  console.log(`[hooks] PreCompact: saved ${recentLines.length} lines to ${dailyPath}`);
@@ -27256,16 +27407,16 @@ ${entry}`);
27256
27407
  const workspace = input.cwd || input.workspace || "";
27257
27408
  if (!workspace) return { continue: true };
27258
27409
  try {
27259
- const fs56 = await import("node:fs");
27410
+ const fs58 = await import("node:fs");
27260
27411
  const bufferPath = path22.join(workspace, "memory", "working-buffer.md");
27261
- if (fs56.existsSync(bufferPath)) {
27262
- const stat4 = fs56.statSync(bufferPath);
27412
+ if (fs58.existsSync(bufferPath)) {
27413
+ const stat4 = fs58.statSync(bufferPath);
27263
27414
  const ageMs = Date.now() - stat4.mtimeMs;
27264
27415
  const ageMin = Math.round(ageMs / 6e4);
27265
27416
  if (ageMin > 10) {
27266
27417
  console.warn(`[hooks] PostCompact: \u26A0\uFE0F working-buffer.md is ${ageMin}min old (last modified ${stat4.mtime.toISOString()}) \u2014 content may be stale!`);
27267
27418
  }
27268
- const content = fs56.readFileSync(bufferPath, "utf-8");
27419
+ const content = fs58.readFileSync(bufferPath, "utf-8");
27269
27420
  if (content.trim()) {
27270
27421
  console.log(`[hooks] PostCompact: injecting working-buffer (${content.length} chars, ${ageMin}min old)`);
27271
27422
  return {
@@ -27616,9 +27767,9 @@ function createCompactGetters(config2) {
27616
27767
  const m = s.trim().toLowerCase().match(/^([\d.]+)\s*(mb|kb|gb|b)?$/);
27617
27768
  if (!m) return defaultBytes;
27618
27769
  const n = parseFloat(m[1]);
27619
- const unit = m[2] || "b";
27770
+ const unit2 = m[2] || "b";
27620
27771
  const multipliers = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
27621
- return Math.round(n * (multipliers[unit] || 1));
27772
+ return Math.round(n * (multipliers[unit2] || 1));
27622
27773
  }
27623
27774
  const forceFlushTranscriptBytes = parseSizeString(
27624
27775
  config2.compaction?.memoryFlush?.forceFlushTranscriptBytes,
@@ -29427,6 +29578,7 @@ ${items}
29427
29578
  };
29428
29579
 
29429
29580
  // src/inner-voice/plugin.ts
29581
+ init_live();
29430
29582
  import fs32 from "node:fs";
29431
29583
  import path32 from "node:path";
29432
29584
 
@@ -29894,10 +30046,10 @@ var US_HALF_LIFE_DAYS = 10;
29894
30046
  var US_MAX_LINES = 60;
29895
30047
  function readRecentMemory(workspace) {
29896
30048
  const dir = path31.join(workspace, "memory");
29897
- const today2 = dailyFileKeyFor();
30049
+ const today3 = dailyFileKeyFor();
29898
30050
  const yesterday = dailyFileKeyFor(Date.now() - 864e5);
29899
30051
  return {
29900
- today: readIfExists(path31.join(dir, `${today2}.md`)),
30052
+ today: readIfExists(path31.join(dir, `${today3}.md`)),
29901
30053
  yesterday: readIfExists(path31.join(dir, `${yesterday}.md`))
29902
30054
  };
29903
30055
  }
@@ -29982,7 +30134,6 @@ var InnerVoicePlugin = class {
29982
30134
  this.provider = provider;
29983
30135
  this.model = model;
29984
30136
  this.workspace = workspace;
29985
- this.customPrompt = this.loadPrompt(workspace);
29986
30137
  }
29987
30138
  cfg;
29988
30139
  provider;
@@ -29991,23 +30142,31 @@ var InnerVoicePlugin = class {
29991
30142
  running = false;
29992
30143
  workspace;
29993
30144
  dispatcher;
29994
- /** 从 my-inner-voice.md 加载的定制 prompt */
29995
- customPrompt;
29996
30145
  static shouldEnable(cfg) {
29997
30146
  return !!cfg.enabled;
29998
30147
  }
29999
- /** 读 workspace/prompts/my-inner-voice.md,不存在用 DEFAULT_PROMPT */
30000
- loadPrompt(workspace) {
30001
- const promptPath = path32.join(workspace, "prompts", "my-inner-voice.md");
30002
- try {
30003
- const content = fs32.readFileSync(promptPath, "utf-8").trim();
30004
- if (content) {
30005
- console.log(`[inner-voice] Loaded custom prompt from ${promptPath}`);
30006
- return content;
30148
+ /**
30149
+ * 读定制 prompt:workspace/personas/<persona>/my-inner-voice.md 优先,
30150
+ * 回退 workspace/prompts/my-inner-voice.md,两处都没有用 DEFAULT_PROMPT。
30151
+ *
30152
+ * ⚠️ 每次取用**现读**,不在构造期固化:agents.defaults.persona 是热加载字段
30153
+ * (registry effect=hot),固化意味着切换 persona 后内心戏一直念旧人设的 prompt,
30154
+ * 而旧文件那时已被迁移 rename 走。成本是每 tick 一次 readFileSync(默认 30min 一次)。
30155
+ */
30156
+ loadPrompt() {
30157
+ const persona = liveConfig.get("agents.defaults.persona");
30158
+ const promptPath = resolvePersonaFile(this.workspace, persona, "my-inner-voice.md");
30159
+ if (promptPath) {
30160
+ try {
30161
+ const content = fs32.readFileSync(promptPath, "utf-8").trim();
30162
+ if (content) {
30163
+ console.log(`[inner-voice] Loaded custom prompt from ${promptPath}`);
30164
+ return content;
30165
+ }
30166
+ } catch {
30007
30167
  }
30008
- } catch {
30009
- console.log(`[inner-voice] No custom prompt at ${promptPath}, using default`);
30010
30168
  }
30169
+ console.log(`[inner-voice] No custom prompt (persona=${persona || "(unset)"}), using default`);
30011
30170
  return DEFAULT_PROMPT4;
30012
30171
  }
30013
30172
  start(sessions, deps, dispatcher) {
@@ -30138,7 +30297,7 @@ var InnerVoicePlugin = class {
30138
30297
  async generateThought(context) {
30139
30298
  const stream = this.provider.streamChat({
30140
30299
  model: this.model,
30141
- systemPrompt: this.customPrompt,
30300
+ systemPrompt: this.loadPrompt(),
30142
30301
  messages: [{ role: "user", content: context }],
30143
30302
  maxTokens: 512,
30144
30303
  temperature: 0.8,
@@ -31009,12 +31168,6 @@ async function startHttpApi(ctx) {
31009
31168
  return httpServer;
31010
31169
  }
31011
31170
 
31012
- // src/optional/voice-chat/plugin.ts
31013
- import { spawn as spawn4, exec } from "node:child_process";
31014
- import net from "node:net";
31015
- import path36 from "node:path";
31016
- import fs36 from "node:fs";
31017
-
31018
31171
  // src/optional/voice-chat/bridge.ts
31019
31172
  init_live();
31020
31173
  init_well_formed();
@@ -31257,8 +31410,9 @@ function parseVoiceChatConfig(raw) {
31257
31410
  if (!raw) return { ...DEFAULTS3 };
31258
31411
  return {
31259
31412
  enabled: raw.enabled === true,
31413
+ // 0925 起惰性:引擎不启动 python(Python 是外部依赖,见 optional/voice-chat/plugin.ts 顶部说明)。
31414
+ // 仍然解析只为让老 config 里的 spawnPython 键原样通过、不报错——字段已 @deprecated,零读者。
31260
31415
  spawnPython: raw.spawnPython !== false,
31261
- // 默认 true,配 false 只注册 webhook
31262
31416
  pythonPort: raw.pythonPort ?? DEFAULTS3.pythonPort,
31263
31417
  webhookPath: raw.webhookPath ?? DEFAULTS3.webhookPath,
31264
31418
  callbackPath: raw.callbackPath ?? DEFAULTS3.callbackPath,
@@ -31284,7 +31438,9 @@ function parseVoiceChatConfig(raw) {
31284
31438
  provider: raw.avatar.provider || "none",
31285
31439
  tts: raw.avatar.tts,
31286
31440
  image: raw.avatar.image,
31287
- selfieDir: raw.avatar.selfieDir || "D:/xiaoke/workspace/selfie"
31441
+ // 0925:默认留空,由使用方按自己的 workspace 解析——引擎不该知道任何人的盘
31442
+ // (此前默认值是 'D:/xiaoke/workspace/selfie',把一台机器的路径写进了引擎源码)
31443
+ selfieDir: raw.avatar.selfieDir || ""
31288
31444
  } : void 0,
31289
31445
  // local 分组:本地 TTS/avatar(以后本地能跑 avatar 时往这加)
31290
31446
  local: raw.local ? {
@@ -31299,24 +31455,20 @@ function parseVoiceChatConfig(raw) {
31299
31455
  provider: raw.local.avatar.provider || "none",
31300
31456
  tts: raw.local.avatar.tts,
31301
31457
  image: raw.local.avatar.image,
31302
- selfieDir: raw.local.avatar.selfieDir || "D:/xiaoke/workspace/selfie"
31458
+ // 0925:同上——不留任何机器专属默认路径
31459
+ selfieDir: raw.local.avatar.selfieDir || ""
31303
31460
  } : void 0
31304
31461
  } : void 0
31305
31462
  };
31306
31463
  }
31307
31464
 
31308
31465
  // src/optional/voice-chat/plugin.ts
31309
- init_BashTool();
31310
31466
  init_provider_factory();
31311
31467
  init_query();
31312
- var VoiceChatPlugin = class _VoiceChatPlugin {
31468
+ var VoiceChatPlugin = class {
31313
31469
  name = "voice-chat";
31314
- pythonProcess = null;
31315
- restartTimer = null;
31316
31470
  config;
31317
31471
  workspace = "";
31318
- restartCount = 0;
31319
- static MAX_RESTARTS = 3;
31320
31472
  // 0908:deps 改懒建+失效(对齐 deskBuddy quick deps 模式)——start() 不再固化
31321
31473
  // provider/QueryEngine 实例。改 voiceChat.model 热加载后,invalidateVoiceDeps()
31322
31474
  // 清缓存,下一条语音消息用新配置重建。此前 start() 一次性建死进 bridge 闭包,
@@ -31345,13 +31497,6 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
31345
31497
  () => this.resolveVoiceDeps()
31346
31498
  );
31347
31499
  console.log(`[voice-chat] Bridge registered at ${this.config.webhookPath ?? "/webhook/voice-chat"}${this.config.model ? ` (model: ${this.config.model}, thinking: ${this.config.thinking ?? false})` : ""}`);
31348
- if (this.config.spawnPython === false) {
31349
- console.log("[voice-chat] spawnPython=false, skipping Python subprocess (webhook-only mode)");
31350
- return;
31351
- }
31352
- const port = this.config.pythonPort ?? 8011;
31353
- await this.killProcessOnPort(port);
31354
- this.pythonProcess = this.startPython();
31355
31500
  }
31356
31501
  async stop() {
31357
31502
  if (this.bridgeCleanup) {
@@ -31361,29 +31506,13 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
31361
31506
  }
31362
31507
  this.bridgeCleanup = void 0;
31363
31508
  }
31364
- if (this.restartTimer) {
31365
- clearTimeout(this.restartTimer);
31366
- this.restartTimer = null;
31367
- }
31368
- if (this.pythonProcess) {
31369
- this.pythonProcess.kill("SIGTERM");
31370
- const proc = this.pythonProcess;
31371
- const killTimer = setTimeout(() => {
31372
- if (!proc.killed) {
31373
- proc.kill("SIGKILL");
31374
- }
31375
- }, 5e3);
31376
- this.pythonProcess = null;
31377
- }
31378
31509
  }
31379
31510
  getStatus() {
31380
31511
  return {
31381
- pythonRunning: this.pythonProcess !== null,
31382
- pythonPid: this.pythonProcess?.pid,
31383
31512
  webrtcConnected: false
31384
31513
  };
31385
31514
  }
31386
- /** 热加载:刷新 config 中可热变的字段(model/thinking 等),不影响已运行的 Python 进程 */
31515
+ /** 热加载:刷新 config 中可热变的字段(model/thinking 等),不碰外部 Python 服务 */
31387
31516
  reloadConfig(newConfig) {
31388
31517
  const vcNew = newConfig?.voiceChat;
31389
31518
  if (!vcNew) return;
@@ -31491,159 +31620,13 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
31491
31620
  modelInputs: modelDef.input || ["text"]
31492
31621
  };
31493
31622
  }
31494
- async isPortAlive(port) {
31495
- return new Promise((resolve12) => {
31496
- const socket = new net.Socket();
31497
- socket.setTimeout(2e3);
31498
- socket.on("connect", () => {
31499
- socket.destroy();
31500
- resolve12(true);
31501
- });
31502
- socket.on("timeout", () => {
31503
- socket.destroy();
31504
- resolve12(false);
31505
- });
31506
- socket.on("error", () => {
31507
- socket.destroy();
31508
- resolve12(false);
31509
- });
31510
- socket.connect(port, "127.0.0.1");
31511
- });
31512
- }
31513
- async killProcessOnPort(port) {
31514
- const run = (cmd) => new Promise((resolve12) => {
31515
- exec(cmd, { timeout: 5e3 }, (err, stdout) => resolve12(err ? "" : stdout));
31516
- });
31517
- try {
31518
- if (process.platform === "win32") {
31519
- const out = await run(`netstat -ano | findstr ":${port}"`);
31520
- const lines = out.split("\n").filter((l) => l.includes("LISTENING"));
31521
- const pids = /* @__PURE__ */ new Set();
31522
- for (const line of lines) {
31523
- const pid = line.trim().split(/\s+/).pop();
31524
- if (pid) pids.add(pid);
31525
- }
31526
- for (const pid of pids) {
31527
- await run(`taskkill /PID ${pid} /F`);
31528
- console.log(`[voice-chat] Killed orphan PID ${pid} on port ${port}`);
31529
- }
31530
- } else {
31531
- const out = await run(`lsof -ti :${port}`);
31532
- if (out.trim()) {
31533
- await run(`kill -9 ${out.trim()}`);
31534
- console.log(`[voice-chat] Killed orphan PID ${out.trim()} on port ${port}`);
31535
- }
31536
- }
31537
- if (process.platform === "win32") {
31538
- const stillAlive = await run(`netstat -ano | findstr ":${port}"`);
31539
- if (stillAlive.includes("LISTENING")) {
31540
- await new Promise((r) => setTimeout(r, 500));
31541
- }
31542
- }
31543
- } catch {
31544
- }
31545
- }
31546
- findPython() {
31547
- if (this.config.pythonPath && fs36.existsSync(this.config.pythonPath)) {
31548
- return this.config.pythonPath;
31549
- }
31550
- return "python";
31551
- }
31552
- getPythonDir() {
31553
- const dir = import.meta.dirname;
31554
- const srcDir = path36.resolve(dir, "..", "src", "voice-chat", "python");
31555
- const localDir = path36.join(dir, "python");
31556
- return fs36.existsSync(srcDir) ? srcDir : localDir;
31557
- }
31558
- startPython() {
31559
- const pythonDir = this.getPythonDir();
31560
- const serverPy = path36.join(pythonDir, "server.py");
31561
- const pythonBin = this.findPython();
31562
- const args2 = [serverPy];
31563
- if (this.config.pythonPort) args2.push("--port", String(this.config.pythonPort));
31564
- if (this.config.vadModelPath) args2.push("--vad-model", this.config.vadModelPath);
31565
- if (this.config.asrModelPath) args2.push("--asr-model", this.config.asrModelPath);
31566
- if (this.config.asrLanguage) args2.push("--asr-lang", this.config.asrLanguage);
31567
- if (this.config.vadThreshold) args2.push("--vad-threshold", String(this.config.vadThreshold));
31568
- if (this.config.postEndMonitor) args2.push("--post-end-monitor", String(this.config.postEndMonitor));
31569
- if (this.workspace) args2.push("--workspace", this.workspace);
31570
- const ttsCfg = this.config.local?.tts || this.config.tts;
31571
- if (ttsCfg) {
31572
- args2.push("--tts-provider", ttsCfg.provider);
31573
- if (ttsCfg.apiKey) args2.push("--tts-api-key", ttsCfg.apiKey);
31574
- if (ttsCfg.model) args2.push("--tts-model", ttsCfg.model);
31575
- if (ttsCfg.voice !== void 0) args2.push("--tts-voice", ttsCfg.voice);
31576
- if (ttsCfg.workspaceId) args2.push("--tts-workspace-id", ttsCfg.workspaceId);
31577
- }
31578
- const avatarCfg = this.config.local?.avatar || this.config.avatar;
31579
- if (avatarCfg) {
31580
- args2.push("--avatar-provider", avatarCfg.provider);
31581
- if (avatarCfg.tts) args2.push("--avatar-tts", avatarCfg.tts);
31582
- if (avatarCfg.image) args2.push("--avatar-image", avatarCfg.image);
31583
- if (avatarCfg.selfieDir) args2.push("--selfie-dir", avatarCfg.selfieDir);
31584
- }
31585
- if (this.config.playbackSpeed) {
31586
- args2.push("--playback-speed", String(this.config.playbackSpeed));
31587
- }
31588
- console.log(`[voice-chat] Starting Python: ${pythonBin} ${args2.join(" ")}`);
31589
- console.log(`[voice-chat] Python dir: ${pythonDir}`);
31590
- if (!fs36.existsSync(pythonDir)) {
31591
- console.error(`[voice-chat] FATAL: Python directory does not exist: ${pythonDir}`);
31592
- throw new Error(`voice-chat: python dir not found: ${pythonDir}`);
31593
- }
31594
- let child;
31595
- if (process.platform === "win32") {
31596
- const { shell, args: shellArgs } = findShell();
31597
- const cmd = `${pythonBin} ${args2.map((a) => `"${a}"`).join(" ")}`;
31598
- child = spawn4(shell, [...shellArgs, cmd], {
31599
- cwd: pythonDir,
31600
- stdio: ["ignore", "pipe", "pipe"],
31601
- env: { ...process.env, PYTHONUNBUFFERED: "1" }
31602
- });
31603
- } else {
31604
- child = spawn4(pythonBin, args2, {
31605
- cwd: pythonDir,
31606
- stdio: ["ignore", "pipe", "pipe"],
31607
- env: { ...process.env, PYTHONUNBUFFERED: "1" }
31608
- });
31609
- }
31610
- child.on("error", (err) => {
31611
- console.error(`[voice-chat] spawn error: ${err.message}`);
31612
- console.error(`[voice-chat] shell=${pythonBin} cwd=${pythonDir}`);
31613
- console.error(`[voice-chat] cwd exists=${fs36.existsSync(pythonDir)}`);
31614
- });
31615
- child.stdout?.on("data", (data) => {
31616
- const lines = data.toString().trim().split("\n");
31617
- for (const line of lines) console.log(`[voice-chat:py] ${line}`);
31618
- });
31619
- child.stderr?.on("data", (data) => {
31620
- const lines = data.toString().trim().split("\n");
31621
- for (const line of lines) console.error(`[voice-chat:py] ${line}`);
31622
- });
31623
- child.on("exit", (code, signal) => {
31624
- console.log(`[voice-chat] Python exited (code=${code}, signal=${signal})`);
31625
- this.pythonProcess = null;
31626
- if (code !== 0 && code !== null) {
31627
- if (this.restartCount >= _VoiceChatPlugin.MAX_RESTARTS) {
31628
- console.error(`[voice-chat] Max restarts (${_VoiceChatPlugin.MAX_RESTARTS}) reached, giving up`);
31629
- return;
31630
- }
31631
- this.restartCount++;
31632
- console.log(`[voice-chat] Python crashed, restarting in 5s... (attempt ${this.restartCount}/${_VoiceChatPlugin.MAX_RESTARTS})`);
31633
- this.restartTimer = setTimeout(() => {
31634
- this.pythonProcess = this.startPython();
31635
- }, 5e3);
31636
- }
31637
- });
31638
- return child;
31639
- }
31640
31623
  };
31641
31624
 
31642
31625
  // src/memory/everos/plugin.ts
31643
- import { spawn as spawn5 } from "node:child_process";
31644
- import net2 from "node:net";
31645
- import path37 from "node:path";
31646
- import fs37 from "node:fs";
31626
+ import { spawn as spawn4 } from "node:child_process";
31627
+ import net from "node:net";
31628
+ import path36 from "node:path";
31629
+ import fs36 from "node:fs";
31647
31630
 
31648
31631
  // src/memory/everos/config.ts
31649
31632
  var DEFAULTS4 = {
@@ -31877,8 +31860,8 @@ var EverosPlugin = class {
31877
31860
  }, 3e5);
31878
31861
  }
31879
31862
  async startEveros() {
31880
- const pythonDir = path37.dirname(this.config.lancedbPath);
31881
- const configPath2 = path37.join(pythonDir, "config.toml");
31863
+ const pythonDir = path36.dirname(this.config.lancedbPath);
31864
+ const configPath2 = path36.join(pythonDir, "config.toml");
31882
31865
  await this.ensureFcntlCompat();
31883
31866
  const venvPython = this.findVenvPython();
31884
31867
  const everosBin = venvPython.replace(/python\.exe$/, "everos.exe");
@@ -31887,13 +31870,13 @@ var EverosPlugin = class {
31887
31870
  console.log(`[everos] Starting EverOS: ${cmd}`);
31888
31871
  console.log(`[everos] LLM config: ${this.config.llm.model} @ ${this.config.llm.baseUrl}`);
31889
31872
  if (process.platform === "win32") {
31890
- spawn5(everosBin, args2, {
31873
+ spawn4(everosBin, args2, {
31891
31874
  cwd: pythonDir,
31892
31875
  stdio: "ignore",
31893
31876
  env: { ...process.env, PYTHONUNBUFFERED: "1", NO_PROXY: "127.0.0.1,localhost", no_proxy: "127.0.0.1,localhost" }
31894
31877
  });
31895
31878
  } else {
31896
- spawn5(venvPython, args2, {
31879
+ spawn4(venvPython, args2, {
31897
31880
  cwd: pythonDir,
31898
31881
  stdio: "ignore",
31899
31882
  env: { ...process.env, PYTHONUNBUFFERED: "1", NO_PROXY: "127.0.0.1,localhost", no_proxy: "127.0.0.1,localhost" }
@@ -31938,13 +31921,13 @@ var EverosPlugin = class {
31938
31921
  console.log(`[everos] EVEROS_USER_ID=${childEnv.EVEROS_USER_ID}`);
31939
31922
  let child;
31940
31923
  if (process.platform === "win32") {
31941
- child = spawn5(venvPython, args2, {
31924
+ child = spawn4(venvPython, args2, {
31942
31925
  cwd: pythonDir,
31943
31926
  stdio: ["ignore", "pipe", "pipe"],
31944
31927
  env: childEnv
31945
31928
  });
31946
31929
  } else {
31947
- child = spawn5(venvPython, args2, {
31930
+ child = spawn4(venvPython, args2, {
31948
31931
  cwd: pythonDir,
31949
31932
  stdio: ["ignore", "pipe", "pipe"],
31950
31933
  env: childEnv
@@ -31968,19 +31951,19 @@ var EverosPlugin = class {
31968
31951
  return child;
31969
31952
  }
31970
31953
  findVenvPython() {
31971
- const stateDir = process.env.ENGINE7_STATE_DIR || path37.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
31954
+ const stateDir = process.env.ENGINE7_STATE_DIR || path36.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
31972
31955
  if (process.platform === "win32") {
31973
- return path37.join(stateDir, "everos-venv", "Scripts", "python.exe");
31956
+ return path36.join(stateDir, "everos-venv", "Scripts", "python.exe");
31974
31957
  }
31975
- return path37.join(stateDir, "everos-venv", "bin", "python");
31958
+ return path36.join(stateDir, "everos-venv", "bin", "python");
31976
31959
  }
31977
31960
  /** 检测 venv 是否存在,不存在就自动创建 + 装 EverOS */
31978
31961
  async ensureVenv() {
31979
31962
  const venvPython = this.findVenvPython();
31980
- if (fs37.existsSync(venvPython)) return;
31981
- const stateDir = process.env.ENGINE7_STATE_DIR || path37.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
31982
- const venvDir = path37.join(stateDir, "everos-venv");
31983
- const everosSrc = path37.join(stateDir, "workspace", "research", "EverOS");
31963
+ if (fs36.existsSync(venvPython)) return;
31964
+ const stateDir = process.env.ENGINE7_STATE_DIR || path36.join(process.env.HOME || process.env.USERPROFILE || ".", ".engine7");
31965
+ const venvDir = path36.join(stateDir, "everos-venv");
31966
+ const everosSrc = path36.join(stateDir, "workspace", "research", "EverOS");
31984
31967
  console.log(`[everos] venv not found at ${venvDir}, auto-creating...`);
31985
31968
  console.log(`[everos] \u23F3 This may take a few minutes on first run...`);
31986
31969
  const pyCandidates = process.platform === "win32" ? ["python", "python3", "C:\\Python314\\python.exe", "C:\\Python313\\python.exe", "C:\\Python312\\python.exe"] : ["python3", "python"];
@@ -32002,9 +31985,9 @@ var EverosPlugin = class {
32002
31985
  console.log(`[everos] Creating venv with ${sysPython}...`);
32003
31986
  const { execSync: execSync3 } = await import("node:child_process");
32004
31987
  execSync3(`"${sysPython}" -m venv "${venvDir}"`, { stdio: "pipe", shell: true });
32005
- const pip = process.platform === "win32" ? path37.join(venvDir, "Scripts", "pip.exe") : path37.join(venvDir, "bin", "pip");
32006
- const everosReq = path37.join(this.getPythonDir(), "requirements.txt");
32007
- if (fs37.existsSync(everosReq)) {
31988
+ const pip = process.platform === "win32" ? path36.join(venvDir, "Scripts", "pip.exe") : path36.join(venvDir, "bin", "pip");
31989
+ const everosReq = path36.join(this.getPythonDir(), "requirements.txt");
31990
+ if (fs36.existsSync(everosReq)) {
32008
31991
  console.log(`[everos] Installing from requirements.txt...`);
32009
31992
  execSync3(`"${pip}" install -r "${everosReq}" -q`, { stdio: "pipe", shell: true, timeout: 3e5 });
32010
31993
  } else {
@@ -32020,12 +32003,12 @@ var EverosPlugin = class {
32020
32003
  getPythonDir() {
32021
32004
  const dir = import.meta.dirname;
32022
32005
  const candidates = [
32023
- path37.join(dir, "python"),
32024
- path37.resolve(dir, "..", "src", "memory", "everos", "python"),
32025
- path37.resolve(dir, "..", "..", "..", "src", "memory", "everos", "python")
32006
+ path36.join(dir, "python"),
32007
+ path36.resolve(dir, "..", "src", "memory", "everos", "python"),
32008
+ path36.resolve(dir, "..", "..", "..", "src", "memory", "everos", "python")
32026
32009
  ];
32027
32010
  for (const candidate of candidates) {
32028
- if (fs37.existsSync(path37.join(candidate, "agentic_server.py"))) {
32011
+ if (fs36.existsSync(path36.join(candidate, "agentic_server.py"))) {
32029
32012
  return candidate;
32030
32013
  }
32031
32014
  }
@@ -32034,14 +32017,14 @@ var EverosPlugin = class {
32034
32017
  async ensureFcntlCompat() {
32035
32018
  if (process.platform !== "win32") return;
32036
32019
  const venvPython = this.findVenvPython();
32037
- const venvDir = path37.dirname(path37.dirname(venvPython));
32038
- const sitePackages = path37.join(venvDir, "Lib", "site-packages");
32039
- const target = path37.join(sitePackages, "fcntl.py");
32040
- if (fs37.existsSync(target)) return;
32041
- const source = path37.join(this.getPythonDir(), "fcntl_compat.py");
32042
- if (fs37.existsSync(source)) {
32020
+ const venvDir = path36.dirname(path36.dirname(venvPython));
32021
+ const sitePackages = path36.join(venvDir, "Lib", "site-packages");
32022
+ const target = path36.join(sitePackages, "fcntl.py");
32023
+ if (fs36.existsSync(target)) return;
32024
+ const source = path36.join(this.getPythonDir(), "fcntl_compat.py");
32025
+ if (fs36.existsSync(source)) {
32043
32026
  try {
32044
- fs37.copyFileSync(source, target);
32027
+ fs36.copyFileSync(source, target);
32045
32028
  console.log(`[everos] Installed fcntl compat shim to ${target}`);
32046
32029
  } catch (err) {
32047
32030
  console.warn(`[everos] Failed to install fcntl shim: ${err.message}`);
@@ -32065,7 +32048,7 @@ var EverosPlugin = class {
32065
32048
  }
32066
32049
  async isPortAlive(port) {
32067
32050
  return new Promise((resolve12) => {
32068
- const socket = new net2.Socket();
32051
+ const socket = new net.Socket();
32069
32052
  socket.setTimeout(2e3);
32070
32053
  socket.on("connect", () => {
32071
32054
  socket.destroy();
@@ -32650,8 +32633,8 @@ function createVisualBoot(ctx) {
32650
32633
  }
32651
32634
 
32652
32635
  // src/startup/message-pipeline.ts
32653
- import * as fs38 from "node:fs";
32654
- import * as path38 from "node:path";
32636
+ import * as fs37 from "node:fs";
32637
+ import * as path37 from "node:path";
32655
32638
 
32656
32639
  // src/commands/registry.ts
32657
32640
  var commands = /* @__PURE__ */ new Map();
@@ -33542,7 +33525,7 @@ function setupMessagePipeline(ctx) {
33542
33525
  console.log(`[vision] Downloading image: ${att.filename}`);
33543
33526
  let rawBuffer;
33544
33527
  if (att.url.startsWith("file://")) {
33545
- rawBuffer = fs38.readFileSync(decodeURIComponent(att.url.slice(7)));
33528
+ rawBuffer = fs37.readFileSync(decodeURIComponent(att.url.slice(7)));
33546
33529
  } else {
33547
33530
  rawBuffer = await downloadImage2(att.url, attachDispatcher);
33548
33531
  }
@@ -33550,8 +33533,8 @@ function setupMessagePipeline(ctx) {
33550
33533
  const ext = detected.split("/")[1] || "png";
33551
33534
  const resized = await maybeResizeAndDownsampleImageBuffer2(rawBuffer, rawBuffer.length, ext);
33552
33535
  const imageId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
33553
- const savedPath = path38.join(config2.mediaDir, `${imageId}.${ext}`);
33554
- fs38.writeFileSync(savedPath, resized.buffer);
33536
+ const savedPath = path37.join(config2.mediaDir, `${imageId}.${ext}`);
33537
+ fs37.writeFileSync(savedPath, resized.buffer);
33555
33538
  savedPaths.push(savedPath);
33556
33539
  console.log(`[vision] Saved: ${savedPath} (${resized.buffer.length}B)`);
33557
33540
  imageBlocks.push({
@@ -33581,8 +33564,8 @@ ${pathStr}` }];
33581
33564
  }
33582
33565
  const nonImageAttachments = inbound.attachments?.filter((a) => !a.contentType.startsWith("image/"));
33583
33566
  if (nonImageAttachments && nonImageAttachments.length > 0) {
33584
- const outDir = path38.join(config2.mediaDir, sessionId);
33585
- fs38.mkdirSync(outDir, { recursive: true });
33567
+ const outDir = path37.join(config2.mediaDir, sessionId);
33568
+ fs37.mkdirSync(outDir, { recursive: true });
33586
33569
  const resolved = [];
33587
33570
  for (const att of nonImageAttachments) {
33588
33571
  console.log(`[file] Downloading: ${att.filename} (${att.contentType}, ${att.size}B)`);
@@ -33590,9 +33573,9 @@ ${pathStr}` }];
33590
33573
  const resp = attachDispatcher ? await fetch(att.url, { dispatcher: attachDispatcher, signal: AbortSignal.timeout(3e4) }) : await fetch(att.url, { signal: AbortSignal.timeout(3e4) });
33591
33574
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
33592
33575
  const buffer = Buffer.from(await resp.arrayBuffer());
33593
- const safeName2 = path38.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
33594
- const savedPath = path38.join(outDir, safeName2);
33595
- fs38.writeFileSync(savedPath, buffer);
33576
+ const safeName2 = path37.basename(att.filename).replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") || "attachment";
33577
+ const savedPath = path37.join(outDir, safeName2);
33578
+ fs37.writeFileSync(savedPath, buffer);
33596
33579
  resolved.push(savedPath);
33597
33580
  console.log(`[file] Saved: ${savedPath} (${buffer.length}B)`);
33598
33581
  } catch (err) {
@@ -33841,7 +33824,7 @@ ${pathStr}` }];
33841
33824
  init_live();
33842
33825
 
33843
33826
  // src/provision-free.ts
33844
- import { chmodSync as chmodSync2, existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync35, writeFileSync as writeFileSync13 } from "node:fs";
33827
+ import { chmodSync as chmodSync2, existsSync as existsSync24, mkdirSync as mkdirSync16, readFileSync as readFileSync35, writeFileSync as writeFileSync13 } from "node:fs";
33845
33828
  import { dirname as dirname11, join as join46 } from "node:path";
33846
33829
  import { fileURLToPath as fileURLToPath7 } from "node:url";
33847
33830
  var __filename = fileURLToPath7(import.meta.url);
@@ -33865,13 +33848,13 @@ function readInstallIdentity(stateDir) {
33865
33848
  }
33866
33849
 
33867
33850
  // src/optional/reporting/state.ts
33868
- import { existsSync as existsSync24, mkdirSync as mkdirSync16, readFileSync as readFileSync36, writeFileSync as writeFileSync14 } from "node:fs";
33851
+ import { existsSync as existsSync25, mkdirSync as mkdirSync17, readFileSync as readFileSync36, writeFileSync as writeFileSync14 } from "node:fs";
33869
33852
  import { join as join47 } from "node:path";
33870
33853
  var STATE_FILE2 = "report-state.json";
33871
33854
  function readReportState(stateDir) {
33872
33855
  try {
33873
33856
  const p = join47(stateDir, STATE_FILE2);
33874
- if (!existsSync24(p)) return {};
33857
+ if (!existsSync25(p)) return {};
33875
33858
  const parsed = JSON.parse(readFileSync36(p, "utf8"));
33876
33859
  if (!parsed || typeof parsed !== "object") return {};
33877
33860
  return parsed;
@@ -33881,7 +33864,7 @@ function readReportState(stateDir) {
33881
33864
  }
33882
33865
  function writeReportState(stateDir, s) {
33883
33866
  try {
33884
- mkdirSync16(stateDir, { recursive: true });
33867
+ mkdirSync17(stateDir, { recursive: true });
33885
33868
  writeFileSync14(join47(stateDir, STATE_FILE2), JSON.stringify(s, null, 2) + "\n");
33886
33869
  } catch {
33887
33870
  }
@@ -33911,6 +33894,9 @@ function buildReportPayload(opts) {
33911
33894
  if (opts.counters.lastMsgAt > 0 || opts.counters.msgsTotal === 0) {
33912
33895
  p.lastMsgAt = opts.counters.lastMsgAt;
33913
33896
  }
33897
+ if (opts.tts) {
33898
+ p.tts = { free: opts.tts.free, billing: opts.tts.billing, failed: opts.tts.failed };
33899
+ }
33914
33900
  if (opts.identity.contactEmail) p.contactEmail = opts.identity.contactEmail;
33915
33901
  return p;
33916
33902
  }
@@ -33949,6 +33935,71 @@ function restoreCounters(s) {
33949
33935
  lastMsgAt = typeof s.lastMsgAt === "number" && Number.isFinite(s.lastMsgAt) && s.lastMsgAt > 0 ? s.lastMsgAt : 0;
33950
33936
  }
33951
33937
 
33938
+ // src/tools/tts/usage.ts
33939
+ init_clock();
33940
+ var nowFn2 = () => /* @__PURE__ */ new Date();
33941
+ var today2 = "";
33942
+ var buckets = {
33943
+ free: { chars: 0, seconds: 0, calls: 0 },
33944
+ billing: { chars: 0, seconds: 0, calls: 0 }
33945
+ };
33946
+ var failedCount = 0;
33947
+ function rolloverIfNeeded2(now) {
33948
+ const d = todayKey(now);
33949
+ if (d !== today2) {
33950
+ today2 = d;
33951
+ for (const k of ["free", "billing"]) {
33952
+ buckets[k] = { chars: 0, seconds: 0, calls: 0 };
33953
+ }
33954
+ failedCount = 0;
33955
+ }
33956
+ }
33957
+ function noteTtsUsage(u) {
33958
+ captureFn?.(u);
33959
+ const now = nowFn2();
33960
+ rolloverIfNeeded2(now);
33961
+ if (!u.ok) {
33962
+ failedCount++;
33963
+ return;
33964
+ }
33965
+ const b = buckets[u.pool];
33966
+ if (!b) return;
33967
+ const amount = Number.isFinite(u.unit?.amount) && u.unit.amount > 0 ? u.unit.amount : 0;
33968
+ if (u.unit?.kind === "seconds") b.seconds += amount;
33969
+ else b.chars += amount;
33970
+ b.calls++;
33971
+ }
33972
+ function safeCount(v) {
33973
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
33974
+ }
33975
+ function readBucket(v) {
33976
+ const o = v && typeof v === "object" ? v : {};
33977
+ return { chars: safeCount(o.chars), seconds: safeCount(o.seconds), calls: safeCount(o.calls) };
33978
+ }
33979
+ function restoreTtsCounters(s) {
33980
+ today2 = todayKey(nowFn2());
33981
+ const o = s && typeof s === "object" ? s : {};
33982
+ if (o.day === today2) {
33983
+ buckets.free = readBucket(o.free);
33984
+ buckets.billing = readBucket(o.billing);
33985
+ failedCount = safeCount(o.failed);
33986
+ } else {
33987
+ buckets.free = { chars: 0, seconds: 0, calls: 0 };
33988
+ buckets.billing = { chars: 0, seconds: 0, calls: 0 };
33989
+ failedCount = 0;
33990
+ }
33991
+ }
33992
+ function snapshotTtsCounters() {
33993
+ rolloverIfNeeded2(nowFn2());
33994
+ return {
33995
+ day: today2,
33996
+ free: { ...buckets.free },
33997
+ billing: { ...buckets.billing },
33998
+ failed: failedCount
33999
+ };
34000
+ }
34001
+ var captureFn = null;
34002
+
33952
34003
  // src/optional/reporting/reporter.ts
33953
34004
  init_api_status();
33954
34005
  var DEFAULT_REPORT_URL = "https://www.twinsun.top:8866/api/engine7/report";
@@ -33963,12 +34014,12 @@ function acceptReportUrl(v) {
33963
34014
  function startReporting(opts) {
33964
34015
  const { stateDir } = opts;
33965
34016
  const doFetch = opts.fetchImpl ?? fetch;
33966
- const nowFn2 = opts.now ?? (() => Date.now());
34017
+ const nowFn3 = opts.now ?? (() => Date.now());
33967
34018
  const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
33968
34019
  const initialDelayMs = opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
33969
34020
  const log = opts.log ?? (() => {
33970
34021
  });
33971
- const startedAt = nowFn2();
34022
+ const startedAt = nowFn3();
33972
34023
  const maybeIdentity = readInstallIdentity(stateDir);
33973
34024
  if (!maybeIdentity) {
33974
34025
  log("[reporting] \u6CA1\u6709\u53EF\u7528\u7684\u88C5\u673A\u8EAB\u4EFD\uFF0C\u4E0A\u62A5\u5173\u95ED");
@@ -33977,7 +34028,9 @@ function startReporting(opts) {
33977
34028
  } };
33978
34029
  }
33979
34030
  const identity = maybeIdentity;
33980
- restoreCounters(readReportState(stateDir));
34031
+ const bootState = readReportState(stateDir);
34032
+ restoreCounters(bootState);
34033
+ restoreTtsCounters(bootState.tts);
33981
34034
  const engineVersion = readEngineVersion();
33982
34035
  let timer = null;
33983
34036
  let stopped = false;
@@ -33986,7 +34039,7 @@ function startReporting(opts) {
33986
34039
  const enabled = liveConfig.get("reporting.enabled") !== false;
33987
34040
  const counters = snapshotCounters();
33988
34041
  if (!enabled) {
33989
- writeReportState(stateDir, { ...counters, reportUrl: state2.reportUrl });
34042
+ writeReportState(stateDir, { ...counters, tts: snapshotTtsCounters(), reportUrl: state2.reportUrl });
33990
34043
  return;
33991
34044
  }
33992
34045
  const fromServer = acceptReportUrl(state2.reportUrl);
@@ -33997,8 +34050,10 @@ function startReporting(opts) {
33997
34050
  stateDir,
33998
34051
  engineVersion,
33999
34052
  startedAt,
34000
- now: nowFn2(),
34001
- counters
34053
+ now: nowFn3(),
34054
+ counters,
34055
+ // 现取不缓存:snapshotTtsCounters 内部要做跨天检查(进程常驻,可能好几天没人发语音)
34056
+ tts: snapshotTtsCounters()
34002
34057
  });
34003
34058
  let nextUrl = state2.reportUrl;
34004
34059
  try {
@@ -34013,7 +34068,7 @@ function startReporting(opts) {
34013
34068
  if (adopted) nextUrl = adopted;
34014
34069
  } catch {
34015
34070
  }
34016
- writeReportState(stateDir, { ...counters, reportUrl: nextUrl });
34071
+ writeReportState(stateDir, { ...counters, tts: snapshotTtsCounters(), reportUrl: nextUrl });
34017
34072
  }
34018
34073
  function schedule(delay) {
34019
34074
  if (stopped) return;
@@ -34196,13 +34251,13 @@ var DiscordAdapter = class _DiscordAdapter {
34196
34251
  }
34197
34252
  /** 发送媒体附件(图片/文件/音频)— discord.js channel.send({ files }) */
34198
34253
  async sendFile(target, message, attachment) {
34199
- const fs56 = await import("node:fs");
34200
- const path58 = await import("node:path");
34201
- if (!fs56.existsSync(attachment.path)) {
34254
+ const fs58 = await import("node:fs");
34255
+ const path60 = await import("node:path");
34256
+ if (!fs58.existsSync(attachment.path)) {
34202
34257
  throw new Error(`File not found: ${attachment.path}`);
34203
34258
  }
34204
- const filename = attachment.filename || path58.basename(attachment.path);
34205
- const fileBuffer = fs56.readFileSync(attachment.path);
34259
+ const filename = attachment.filename || path60.basename(attachment.path);
34260
+ const fileBuffer = fs58.readFileSync(attachment.path);
34206
34261
  const filePayload = {
34207
34262
  attachment: fileBuffer,
34208
34263
  name: filename
@@ -34651,13 +34706,13 @@ var FeishuAdapter = class _FeishuAdapter {
34651
34706
  }
34652
34707
  /** 发送媒体附件(图片/文件) */
34653
34708
  async sendFile(target, message, attachment) {
34654
- const fs56 = await import("node:fs");
34655
- const path58 = await import("node:path");
34656
- if (!fs56.existsSync(attachment.path)) {
34709
+ const fs58 = await import("node:fs");
34710
+ const path60 = await import("node:path");
34711
+ if (!fs58.existsSync(attachment.path)) {
34657
34712
  throw new Error(`File not found: ${attachment.path}`);
34658
34713
  }
34659
- const filename = attachment.filename || path58.basename(attachment.path);
34660
- const fileBuffer = fs56.readFileSync(attachment.path);
34714
+ const filename = attachment.filename || path60.basename(attachment.path);
34715
+ const fileBuffer = fs58.readFileSync(attachment.path);
34661
34716
  const safeTarget = target || this.config.defaultTarget || "";
34662
34717
  if (!safeTarget) throw new Error("Feishu sendFile: no target (\u4E3B\u52A8\u53D1\u9001\u9700\u914D\u7F6E defaultTarget \u6216\u4F20\u5165 target)");
34663
34718
  const receiveIdType = safeTarget.startsWith("ou_") ? "open_id" : "chat_id";
@@ -34688,10 +34743,10 @@ var FeishuAdapter = class _FeishuAdapter {
34688
34743
  } else {
34689
34744
  let durationMs = Math.round(fileBuffer.length / 3);
34690
34745
  try {
34691
- const { execFile: execFile4 } = await import("node:child_process");
34692
- const { promisify: promisify4 } = await import("node:util");
34693
- const execFileAsync3 = promisify4(execFile4);
34694
- const probe = await execFileAsync3("ffprobe", ["-v", "quiet", "-show_format", "-print_format", "json", attachment.path], { timeout: 1e4 });
34746
+ const { execFile: execFile5 } = await import("node:child_process");
34747
+ const { promisify: promisify5 } = await import("node:util");
34748
+ const execFileAsync4 = promisify5(execFile5);
34749
+ const probe = await execFileAsync4("ffprobe", ["-v", "quiet", "-show_format", "-print_format", "json", attachment.path], { timeout: 1e4 });
34695
34750
  const fmt = JSON.parse(probe.stdout);
34696
34751
  if (fmt?.format?.duration) durationMs = Math.round(parseFloat(fmt.format.duration) * 1e3);
34697
34752
  } catch {
@@ -35254,8 +35309,8 @@ var FeishuAdapter = class _FeishuAdapter {
35254
35309
 
35255
35310
  // src/channels/wechat.ts
35256
35311
  import * as crypto5 from "crypto";
35257
- import * as fs39 from "fs";
35258
- import * as path39 from "path";
35312
+ import * as fs38 from "fs";
35313
+ import * as path38 from "path";
35259
35314
  import * as os4 from "os";
35260
35315
  import { promises as dns } from "dns";
35261
35316
  function sleep3(ms) {
@@ -35479,7 +35534,7 @@ function guessChatType(msg2, accountId) {
35479
35534
  return isGroup ? { type: "group", chatId: roomId || toId || String(msg2.from_user_id || "") } : { type: "dm", chatId: String(msg2.from_user_id || "") };
35480
35535
  }
35481
35536
  function mimeFromFilename(filename) {
35482
- const ext = path39.extname(filename).toLowerCase();
35537
+ const ext = path38.extname(filename).toLowerCase();
35483
35538
  const map = {
35484
35539
  ".jpg": "image/jpeg",
35485
35540
  ".jpeg": "image/jpeg",
@@ -35546,15 +35601,15 @@ var ContextTokenStore = class {
35546
35601
  cache = /* @__PURE__ */ new Map();
35547
35602
  filePath;
35548
35603
  constructor(stateDir, accountId) {
35549
- this.filePath = path39.join(stateDir, "weixin", `${accountId}.context-tokens.json`);
35604
+ this.filePath = path38.join(stateDir, "weixin", `${accountId}.context-tokens.json`);
35550
35605
  }
35551
35606
  k(a, p) {
35552
35607
  return `${a}:${p}`;
35553
35608
  }
35554
35609
  restore() {
35555
35610
  try {
35556
- if (fs39.existsSync(this.filePath)) {
35557
- for (const [k, v] of Object.entries(JSON.parse(fs39.readFileSync(this.filePath, "utf-8"))))
35611
+ if (fs38.existsSync(this.filePath)) {
35612
+ for (const [k, v] of Object.entries(JSON.parse(fs38.readFileSync(this.filePath, "utf-8"))))
35558
35613
  this.cache.set(k, String(v));
35559
35614
  }
35560
35615
  } catch {
@@ -35562,11 +35617,11 @@ var ContextTokenStore = class {
35562
35617
  }
35563
35618
  save() {
35564
35619
  try {
35565
- const dir = path39.dirname(this.filePath);
35566
- if (!fs39.existsSync(dir)) fs39.mkdirSync(dir, { recursive: true });
35620
+ const dir = path38.dirname(this.filePath);
35621
+ if (!fs38.existsSync(dir)) fs38.mkdirSync(dir, { recursive: true });
35567
35622
  const obj = {};
35568
35623
  this.cache.forEach((v, k) => obj[k] = v);
35569
- fs39.writeFileSync(this.filePath, JSON.stringify(obj));
35624
+ fs38.writeFileSync(this.filePath, JSON.stringify(obj));
35570
35625
  } catch {
35571
35626
  }
35572
35627
  }
@@ -35586,18 +35641,18 @@ var ContextTokenStore = class {
35586
35641
  };
35587
35642
  function loadSyncBuf(stateDir, accountId) {
35588
35643
  try {
35589
- const fp = path39.join(stateDir, "weixin", `${accountId}.sync.json`);
35590
- if (fs39.existsSync(fp)) return String(JSON.parse(fs39.readFileSync(fp, "utf-8")).sync_buf || "");
35644
+ const fp = path38.join(stateDir, "weixin", `${accountId}.sync.json`);
35645
+ if (fs38.existsSync(fp)) return String(JSON.parse(fs38.readFileSync(fp, "utf-8")).sync_buf || "");
35591
35646
  } catch {
35592
35647
  }
35593
35648
  return "";
35594
35649
  }
35595
35650
  function saveSyncBuf(stateDir, accountId, buf) {
35596
35651
  try {
35597
- const fp = path39.join(stateDir, "weixin", `${accountId}.sync.json`);
35598
- const dir = path39.dirname(fp);
35599
- if (!fs39.existsSync(dir)) fs39.mkdirSync(dir, { recursive: true });
35600
- fs39.writeFileSync(fp, JSON.stringify({ sync_buf: buf }));
35652
+ const fp = path38.join(stateDir, "weixin", `${accountId}.sync.json`);
35653
+ const dir = path38.dirname(fp);
35654
+ if (!fs38.existsSync(dir)) fs38.mkdirSync(dir, { recursive: true });
35655
+ fs38.writeFileSync(fp, JSON.stringify({ sync_buf: buf }));
35601
35656
  } catch {
35602
35657
  }
35603
35658
  }
@@ -35639,8 +35694,8 @@ var WechatAdapter = class {
35639
35694
  this.config = config2;
35640
35695
  this.baseUrl = config2.baseUrl?.replace(/\/$/, "") || ILINK_BASE_URL;
35641
35696
  this.cdnBaseUrl = config2.cdnBaseUrl?.replace(/\/$/, "") || WEIXIN_CDN_BASE_URL;
35642
- this.stateDir = config2.stateDir || path39.join(os4.homedir?.() || "/tmp", ".engine7");
35643
- if (!fs39.existsSync(this.stateDir)) fs39.mkdirSync(this.stateDir, { recursive: true });
35697
+ this.stateDir = config2.stateDir || path38.join(os4.homedir?.() || "/tmp", ".engine7");
35698
+ if (!fs38.existsSync(this.stateDir)) fs38.mkdirSync(this.stateDir, { recursive: true });
35644
35699
  }
35645
35700
  // --- ChannelAdapter interface ---
35646
35701
  onMessage(handler2) {
@@ -35693,8 +35748,8 @@ var WechatAdapter = class {
35693
35748
  }
35694
35749
  async sendFile(target, message, attachment) {
35695
35750
  const filePath = attachment.path;
35696
- if (!fs39.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
35697
- const plaintext = fs39.readFileSync(filePath);
35751
+ if (!fs38.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
35752
+ const plaintext = fs38.readFileSync(filePath);
35698
35753
  const mediaType = this.guessMediaType(filePath, attachment.mimeType);
35699
35754
  const filekey = crypto5.randomBytes(16).toString("hex");
35700
35755
  const aesKey = crypto5.randomBytes(16);
@@ -35726,7 +35781,7 @@ var WechatAdapter = class {
35726
35781
  aesKeyForApi,
35727
35782
  ciphertextSize: ciphertext.length,
35728
35783
  plaintextSize: rawsize,
35729
- filename: path39.basename(filePath),
35784
+ filename: path38.basename(filePath),
35730
35785
  rawfilemd5,
35731
35786
  voiceDurationSec: attachment.voiceDurationSec
35732
35787
  });
@@ -35985,10 +36040,10 @@ var WechatAdapter = class {
35985
36040
  mediaRef.full_url,
35986
36041
  timeoutSec
35987
36042
  );
35988
- const tmpDir = path39.join(os4.tmpdir(), "weixin-media");
35989
- if (!fs39.existsSync(tmpDir)) fs39.mkdirSync(tmpDir, { recursive: true });
35990
- const tmpPath = path39.join(tmpDir, `${crypto5.randomUUID().replace(/-/g, "")}-${filename}`);
35991
- fs39.writeFileSync(tmpPath, data);
36043
+ const tmpDir = path38.join(os4.tmpdir(), "weixin-media");
36044
+ if (!fs38.existsSync(tmpDir)) fs38.mkdirSync(tmpDir, { recursive: true });
36045
+ const tmpPath = path38.join(tmpDir, `${crypto5.randomUUID().replace(/-/g, "")}-${filename}`);
36046
+ fs38.writeFileSync(tmpPath, data);
35992
36047
  return { url: `file://${tmpPath}`, filename, contentType, size: data.length };
35993
36048
  }
35994
36049
  // --- Internal: send helpers ---
@@ -36038,7 +36093,7 @@ var WechatAdapter = class {
36038
36093
  throw lastError;
36039
36094
  }
36040
36095
  guessMediaType(filePath, mimeType) {
36041
- const ext = path39.extname(filePath).toLowerCase();
36096
+ const ext = path38.extname(filePath).toLowerCase();
36042
36097
  const mime = mimeType || mimeFromFilename(filePath);
36043
36098
  if (mime.startsWith("image/")) return MEDIA_IMAGE;
36044
36099
  if (mime.startsWith("video/")) return MEDIA_VIDEO;
@@ -36077,180 +36132,54 @@ var WechatAdapter = class {
36077
36132
  // src/channels/desk-buddy.ts
36078
36133
  import { WebSocketServer, WebSocket } from "ws";
36079
36134
 
36080
- // src/tools/my-voice.ts
36135
+ // src/tools/tts/impl.ts
36081
36136
  init_live();
36082
36137
  init_well_formed();
36083
- init_registry();
36084
-
36085
- // src/tools/opus-encoder.ts
36138
+ init_voice_chat_tts_daemon();
36139
+ import { execFile as execFile2 } from "node:child_process";
36140
+ import { createInterface as createInterface2 } from "node:readline";
36141
+ import { promisify as promisify2 } from "node:util";
36086
36142
  import * as fs40 from "node:fs";
36087
- var FRAME_MS = 20;
36088
- var PRESKIP_48K = 312;
36089
- var OPUS_RATE = 48e3;
36090
- var NATIVE_RATES = [8e3, 12e3, 16e3, 24e3, 48e3];
36091
- var BITRATE = 32e3;
36092
- var CRC_TABLE = (() => {
36093
- const t = new Uint32Array(256);
36094
- for (let i = 0; i < 256; i++) {
36095
- let r = i << 24;
36096
- for (let k = 0; k < 8; k++) r = r & 2147483648 ? r << 1 ^ 79764919 : r << 1;
36097
- t[i] = r >>> 0;
36098
- }
36099
- return t;
36100
- })();
36101
- function oggCrc32(buf) {
36102
- let crc = 0;
36103
- for (let i = 0; i < buf.length; i++) {
36104
- crc = (crc << 8 ^ CRC_TABLE[(crc >>> 24 ^ buf[i]) & 255]) >>> 0;
36105
- }
36106
- return crc >>> 0;
36107
- }
36108
- var OggWriter = class {
36109
- serial;
36110
- seq = 0;
36111
- pages = [];
36112
- constructor() {
36113
- this.serial = Math.random() * 4294967296 >>> 0;
36114
- }
36115
- /** 组一页。packets 为完整数据包;granule = 该页最后一包的 48k 域样本位置。 */
36116
- page(packets, granule, bos = false, eos = false) {
36117
- const lacing = [];
36118
- for (const p of packets) {
36119
- let rem = p.length;
36120
- while (rem >= 255) {
36121
- lacing.push(255);
36122
- rem -= 255;
36123
- }
36124
- lacing.push(rem);
36125
- }
36126
- const payloadLen = packets.reduce((n, p) => n + p.length, 0);
36127
- const page = Buffer.alloc(27 + lacing.length + payloadLen);
36128
- page.write("OggS", 0, "ascii");
36129
- page[4] = 0;
36130
- page[5] = (bos ? 2 : 0) | (eos ? 1 : 0);
36131
- page.writeBigUInt64LE(BigInt(granule), 6);
36132
- page.writeUInt32LE(this.serial, 14);
36133
- page.writeUInt32LE(this.seq++, 18);
36134
- page.writeUInt32LE(0, 22);
36135
- page[26] = lacing.length;
36136
- lacing.forEach((v, i) => {
36137
- page[27 + i] = v;
36138
- });
36139
- let off = 27 + lacing.length;
36140
- for (const p of packets) {
36141
- p.copy(page, off);
36142
- off += p.length;
36143
- }
36144
- page.writeUInt32LE(oggCrc32(page), 22);
36145
- this.pages.push(page);
36146
- }
36147
- finish() {
36148
- return Buffer.concat(this.pages);
36149
- }
36150
- };
36151
- function opusHeadPacket(channels, inputRate) {
36152
- const b = Buffer.alloc(19);
36153
- b.write("OpusHead", 0, "ascii");
36154
- b[8] = 1;
36155
- b[9] = channels;
36156
- b.writeUInt16LE(PRESKIP_48K, 10);
36157
- b.writeUInt32LE(inputRate, 12);
36158
- b.writeInt16LE(0, 16);
36159
- b[18] = 0;
36160
- return b;
36161
- }
36162
- function opusTagsPacket() {
36163
- const vendor = Buffer.from("engine7-native-opus", "utf8");
36164
- const b = Buffer.alloc(8 + 4 + vendor.length + 4);
36165
- b.write("OpusTags", 0, "ascii");
36166
- b.writeUInt32LE(vendor.length, 8);
36167
- vendor.copy(b, 12);
36168
- b.writeUInt32LE(0, 12 + vendor.length);
36169
- return b;
36170
- }
36171
- function parseWav(buf) {
36172
- if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
36173
- throw new Error("not a RIFF/WAVE file");
36143
+ import * as path40 from "node:path";
36144
+ import * as os5 from "node:os";
36145
+ import net3 from "net";
36146
+ var execFileAsync2 = promisify2(execFile2);
36147
+ var VOICE_DIR = path40.join(os5.tmpdir(), "engine-voice");
36148
+ function wavDurationSec(buf) {
36149
+ if (buf.length < 12 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
36150
+ throw new Error("wavDurationSec: not a RIFF/WAVE file");
36174
36151
  }
36175
36152
  let off = 12;
36176
- let fmt = null;
36177
- let pcm = null;
36178
- while (off + 8 <= buf.length) {
36153
+ let fmtFound = false;
36154
+ let dataFound = false;
36155
+ let byteRate = 0;
36156
+ let dataOff = 0;
36157
+ let dataSize = 0;
36158
+ while (off + 8 <= buf.length && !(fmtFound && dataFound)) {
36179
36159
  const id = buf.toString("ascii", off, off + 4);
36180
36160
  const size = buf.readUInt32LE(off + 4);
36181
36161
  const body = off + 8;
36182
- if (id === "fmt " && body + 16 <= buf.length) {
36183
- fmt = {
36184
- format: buf.readUInt16LE(body),
36185
- ch: buf.readUInt16LE(body + 2),
36186
- rate: buf.readUInt32LE(body + 4),
36187
- bits: buf.readUInt16LE(body + 14)
36188
- };
36189
- } else if (id === "data") {
36190
- pcm = buf.subarray(body, body + Math.min(size, buf.length - body));
36162
+ if (id === "fmt " && !fmtFound) {
36163
+ if (body + 12 > buf.length) throw new Error("wavDurationSec: truncated fmt chunk");
36164
+ byteRate = buf.readUInt32LE(body + 8);
36165
+ fmtFound = true;
36166
+ } else if (id === "data" && !dataFound) {
36167
+ dataOff = body;
36168
+ dataSize = size;
36169
+ dataFound = true;
36191
36170
  }
36192
36171
  off = body + size + (size & 1);
36193
36172
  }
36194
- if (!fmt || !pcm) throw new Error("missing fmt/data chunk");
36195
- if (fmt.format !== 1) throw new Error(`unsupported wav format ${fmt.format} (need PCM)`);
36196
- if (fmt.bits !== 16) throw new Error(`unsupported ${fmt.bits}-bit (need s16le)`);
36197
- return { sampleRate: fmt.rate, channels: fmt.ch, bits: fmt.bits, pcm };
36198
- }
36199
- async function wavToOggOpusNative(wavPath) {
36200
- const mod = await import("@discordjs/opus");
36201
- const OpusEncoder = mod.OpusEncoder ?? mod.default?.OpusEncoder;
36202
- if (!OpusEncoder) throw new Error("@discordjs/opus export shape unexpected");
36203
- const wav = parseWav(fs40.readFileSync(wavPath));
36204
- if (wav.channels !== 1) throw new Error(`unsupported ${wav.channels}ch (need mono)`);
36205
- if (!NATIVE_RATES.includes(wav.sampleRate)) throw new Error(`unsupported rate ${wav.sampleRate}`);
36206
- if (wav.pcm.length === 0) throw new Error("empty pcm data");
36207
- const encoder = new OpusEncoder(wav.sampleRate, wav.channels);
36208
- encoder.setBitrate(BITRATE);
36209
- const samplesPerFrame = Math.round(wav.sampleRate * FRAME_MS / 1e3);
36210
- const bytesPerFrame = samplesPerFrame * 2;
36211
- const frames = Math.ceil(wav.pcm.length / bytesPerFrame);
36212
- const padded = Buffer.alloc(frames * bytesPerFrame);
36213
- wav.pcm.copy(padded);
36214
- const ogg = new OggWriter();
36215
- ogg.page([opusHeadPacket(wav.channels, wav.sampleRate)], 0, true);
36216
- ogg.page([opusTagsPacket()], 0);
36217
- let cur = [];
36218
- let curSegs = 0;
36219
- let curBytes = 0;
36220
- let pageGranule = 0;
36221
- let pos48k = PRESKIP_48K;
36222
- for (let f = 0; f < frames; f++) {
36223
- const packet = encoder.encode(padded.subarray(f * bytesPerFrame, (f + 1) * bytesPerFrame));
36224
- const pSegs = Math.floor((packet.length - 1) / 255) + 1;
36225
- if (curSegs + pSegs > 255 || curBytes + packet.length > 6e4) {
36226
- ogg.page(cur, pageGranule);
36227
- cur = [];
36228
- curSegs = 0;
36229
- curBytes = 0;
36230
- }
36231
- cur.push(packet);
36232
- curSegs += pSegs;
36233
- curBytes += packet.length;
36234
- pos48k += samplesPerFrame * (OPUS_RATE / wav.sampleRate);
36235
- pageGranule = pos48k;
36173
+ if (!fmtFound) throw new Error("wavDurationSec: missing fmt chunk");
36174
+ if (!dataFound) throw new Error("wavDurationSec: missing data chunk");
36175
+ if (!(byteRate > 0)) throw new Error(`wavDurationSec: invalid byteRate ${byteRate}`);
36176
+ const bytes = dataSize === 4294967295 ? buf.length - dataOff : dataSize;
36177
+ const sec = Math.round(bytes / byteRate * 1e3) / 1e3;
36178
+ if (!Number.isFinite(sec) || sec <= 0) {
36179
+ throw new Error(`wavDurationSec: non-positive duration (${bytes} bytes / ${byteRate} Bps)`);
36236
36180
  }
36237
- ogg.page(cur, pageGranule, false, true);
36238
- const oggPath = wavPath.replace(/\.wav$/, ".ogg");
36239
- fs40.writeFileSync(oggPath, ogg.finish());
36240
- return oggPath;
36181
+ return sec;
36241
36182
  }
36242
-
36243
- // src/tools/my-voice.ts
36244
- init_voice_chat_tts_daemon();
36245
- import { execFile as execFile2 } from "node:child_process";
36246
- import { createInterface as createInterface2 } from "node:readline";
36247
- import { promisify as promisify2 } from "node:util";
36248
- import * as fs42 from "node:fs";
36249
- import * as path41 from "node:path";
36250
- import * as os5 from "node:os";
36251
- import net4 from "net";
36252
- var execFileAsync2 = promisify2(execFile2);
36253
- var VOICE_DIR = path41.join(os5.tmpdir(), "engine-voice");
36254
36183
  var _pyCache = null;
36255
36184
  function resolvePython(configuredPath) {
36256
36185
  if (configuredPath) {
@@ -36263,11 +36192,16 @@ function resolvePython(configuredPath) {
36263
36192
  }
36264
36193
  }
36265
36194
  if (_pyCache) return _pyCache;
36195
+ const localAppData = process.env.LOCALAPPDATA;
36266
36196
  const candidates = [
36267
36197
  "python3",
36268
36198
  "python",
36269
- process.env.LOCALAPPDATA + "MicrosoftWindowsAppspython3.exe",
36270
- process.env.LOCALAPPDATA + "MicrosoftWindowsAppspython.exe"
36199
+ // LOCALAPPDATA 缺席(非 Windows / 精简环境)时**不加这两个候选**:
36200
+ // 拼出来会是相对路径 'Microsoft/WindowsApps/python.exe',那可能撞上 cwd 里的东西。
36201
+ ...localAppData ? [
36202
+ path40.join(localAppData, "Microsoft", "WindowsApps", "python3.exe"),
36203
+ path40.join(localAppData, "Microsoft", "WindowsApps", "python.exe")
36204
+ ] : []
36271
36205
  ];
36272
36206
  for (const c of candidates) {
36273
36207
  try {
@@ -36286,8 +36220,8 @@ async function ttsCosyvoice(text, apiKey, model, voice, workspaceId, instruction
36286
36220
  }
36287
36221
  async function ttsCosyvoiceStream(text, cfg, onChunk) {
36288
36222
  const { apiKey, model, voice, workspaceId, instruction } = cfg;
36289
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36290
- const output = path41.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36223
+ fs40.mkdirSync(VOICE_DIR, { recursive: true });
36224
+ const output = path40.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36291
36225
  const script = `
36292
36226
  import sys, json, base64, threading
36293
36227
  import dashscope
@@ -36372,7 +36306,7 @@ print('EOS')
36372
36306
  header.writeUInt16LE(16, 34);
36373
36307
  header.write("data", 36);
36374
36308
  header.writeUInt32LE(pcm.length, 40);
36375
- fs42.writeFileSync(output, Buffer.concat([header, pcm]));
36309
+ fs40.writeFileSync(output, Buffer.concat([header, pcm]));
36376
36310
  return output;
36377
36311
  }
36378
36312
  async function ttsCosyvoiceWorker(text, cfg, onChunk) {
@@ -36384,7 +36318,7 @@ async function ttsCosyvoiceWorker(text, cfg, onChunk) {
36384
36318
  }
36385
36319
  }
36386
36320
  return new Promise((resolve12, reject) => {
36387
- const sock = net4.connect(getSockPath());
36321
+ const sock = net3.connect(getSockPath());
36388
36322
  let buf = "";
36389
36323
  const finish = (err) => {
36390
36324
  try {
@@ -36482,35 +36416,39 @@ async function ttsQwen3Opus(text, cfg) {
36482
36416
  const ogg = Buffer.from(await res.arrayBuffer());
36483
36417
  if (ogg.length < 20) throw new Error("qwen3-3060 opus: empty output");
36484
36418
  const durationMs = parseInt(res.headers.get("x-duration-ms") || "0", 10);
36485
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36486
- const oggPath = path41.join(VOICE_DIR, `tts_${Date.now()}.ogg`);
36487
- fs42.writeFileSync(oggPath, ogg);
36419
+ fs40.mkdirSync(VOICE_DIR, { recursive: true });
36420
+ const oggPath = path40.join(VOICE_DIR, `tts_${Date.now()}.ogg`);
36421
+ fs40.writeFileSync(oggPath, ogg);
36488
36422
  return { oggPath, durationMs };
36489
36423
  }
36490
- var GPTSOVITS_API = "http://127.0.0.1:9880";
36491
- var GPTSOVITS_REF_WAV = "/home/chong/voice/ref/shanshan_ref_v2.wav";
36492
- var GPTSOVITS_REF_TEXT = "\u6625\u7720\u4E0D\u89C9\u6653\uFF0C\u5904\u5904\u95FB\u557C\u9E1F\uFF0C\u591C\u6765\u98CE\u96E8\u58F0\uFF0C\u82B1\u843D\u77E5\u591A\u5C11";
36493
- var GPTSOVITS_REF_LANG = "zh";
36494
- async function ttsGptsovits(text) {
36495
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36496
- const output = path41.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36424
+ async function ttsGptsovits(text, c) {
36425
+ const url = String(c.url || "").replace(/\/+$/, "");
36426
+ const refWav = String(c.refWav || "");
36427
+ const refText = String(c.refText || "");
36428
+ if (!url || !refWav || !refText) {
36429
+ throw new TtsConfigError(
36430
+ "gptsovits \u672A\u914D\u7F6E\uFF1A\u9700\u8981 tools.my_voice \u91CC\u7684 gptsovitsUrl / gptsovitsRefWav / gptsovitsRefText\u3002\u53C2\u8003\u97F3\u9891\u4E0E\u53C2\u8003\u6587\u672C**\u5FC5\u987B\u9010\u5B57\u5BF9\u5E94**\uFF0C\u5426\u5219\u5408\u6210\u51FA\u6765\u7684\u58F0\u97F3\u4F1A\u5E26\u6742\u97F3\uFF080925 \u8E29\u8FC7\u88C1\u574F ref.wav \u7684\u5751\uFF09\u3002"
36431
+ );
36432
+ }
36433
+ fs40.mkdirSync(VOICE_DIR, { recursive: true });
36434
+ const output = path40.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36497
36435
  const params = new URLSearchParams({
36498
36436
  text,
36499
36437
  text_language: "zh",
36500
- refer_wav_path: GPTSOVITS_REF_WAV,
36501
- prompt_text: GPTSOVITS_REF_TEXT,
36502
- prompt_language: GPTSOVITS_REF_LANG
36438
+ refer_wav_path: refWav,
36439
+ prompt_text: refText,
36440
+ prompt_language: String(c.refLang || "zh")
36503
36441
  });
36504
- const res = await fetch(`${GPTSOVITS_API}/?${params}`);
36442
+ const res = await fetch(`${url}/?${params}`);
36505
36443
  if (!res.ok) throw new Error(`GPT-SoVITS API ${res.status}`);
36506
36444
  const buf = Buffer.from(await res.arrayBuffer());
36507
- fs42.writeFileSync(output, buf);
36445
+ fs40.writeFileSync(output, buf);
36508
36446
  return output;
36509
36447
  }
36510
36448
  var EDGE_VOICE = "zh-CN-XiaoxiaoNeural";
36511
36449
  async function ttsEdge(text) {
36512
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36513
- const output = path41.join(VOICE_DIR, `tts_${Date.now()}.mp3`);
36450
+ fs40.mkdirSync(VOICE_DIR, { recursive: true });
36451
+ const output = path40.join(VOICE_DIR, `tts_${Date.now()}.mp3`);
36514
36452
  const script = `
36515
36453
  import asyncio, edge_tts, sys
36516
36454
  async def main():
@@ -36521,363 +36459,64 @@ asyncio.run(main())
36521
36459
  await execFileAsync2(resolvePython((liveConfig.get("tools.my_voice") || {}).pythonPath), ["-c", script, text, EDGE_VOICE, output], { timeout: 3e4 });
36522
36460
  return output;
36523
36461
  }
36524
- async function toWav24kWithDuration(audioPath) {
36525
- let wavPath = audioPath;
36526
- if (!audioPath.endsWith(".wav")) {
36527
- wavPath = audioPath.replace(/\.[^.]+$/, ".wav");
36528
- await execFileAsync2("ffmpeg", [
36529
- "-y",
36530
- "-i",
36531
- audioPath,
36532
- "-ar",
36533
- "24000",
36534
- "-ac",
36535
- "1",
36536
- "-c:a",
36537
- "pcm_s16le",
36538
- wavPath
36539
- ], { timeout: 3e4 });
36540
- if (!fs42.existsSync(wavPath) || fs42.statSync(wavPath).size < 44) {
36541
- throw new Error("toWav24k produced empty output");
36542
- }
36543
- try {
36544
- fs42.unlinkSync(audioPath);
36545
- } catch {
36546
- }
36547
- }
36548
- const script = `
36549
- import sys, wave
36550
- with wave.open(sys.argv[1], 'rb') as wf:
36551
- print(round(wf.getnframes() / wf.getframerate(), 3))
36552
- `;
36553
- const { stdout } = await execFileAsync2(resolvePython((liveConfig.get("tools.my_voice") || {}).pythonPath), ["-c", script, wavPath], { timeout: 15e3 });
36554
- const durationSec = parseFloat(stdout.trim());
36555
- if (!Number.isFinite(durationSec) || durationSec <= 0) {
36556
- throw new Error(`invalid duration: ${stdout.trim()}`);
36557
- }
36558
- return { wavPath, durationSec };
36559
- }
36560
- async function compressWav(wavPath) {
36561
- const m4aPath = wavPath.replace(/\.wav$/, ".m4a");
36562
- try {
36563
- await execFileAsync2("ffmpeg", [
36564
- "-y",
36565
- "-i",
36566
- wavPath,
36567
- "-c:a",
36568
- "aac",
36569
- "-b:a",
36570
- "64k",
36571
- "-movflags",
36572
- "+faststart",
36573
- m4aPath
36574
- ], { timeout: 3e4 });
36575
- fs42.unlinkSync(wavPath);
36576
- return m4aPath;
36577
- } catch {
36578
- return wavPath;
36462
+ var unit = (text) => ({ kind: "chars", amount: [...text].length });
36463
+ var TtsConfigError = class extends Error {
36464
+ constructor(message) {
36465
+ super(message);
36466
+ this.name = "TtsConfigError";
36579
36467
  }
36468
+ };
36469
+ async function synthCosyvoice(text, _sink) {
36470
+ const c = liveConfig.get("tools.my_voice") || {};
36471
+ if (!c.apiKey || !c.voice) throw new TtsConfigError("cosyvoice provider requires apiKey and voice in config");
36472
+ const p = await ttsCosyvoice(text, c.apiKey, c.model || "cosyvoice-v1", c.voice, c.workspaceId, c.instruction);
36473
+ return { path: p, format: "wav", unit: unit(text), model: c.model, voice: c.voice };
36474
+ }
36475
+ async function synthGptsovits(text, _sink) {
36476
+ const c = liveConfig.get("tools.my_voice") || {};
36477
+ const p = await ttsGptsovits(text, {
36478
+ url: c.gptsovitsUrl,
36479
+ refWav: c.gptsovitsRefWav,
36480
+ refText: c.gptsovitsRefText,
36481
+ refLang: c.gptsovitsRefLang
36482
+ });
36483
+ return { path: p, format: "wav", unit: unit(text) };
36580
36484
  }
36581
- async function wavToOggOpus(wavPath) {
36582
- let oggPath;
36485
+ async function synthQwen3(text, _sink) {
36486
+ const c = liveConfig.get("tools.my_voice") || {};
36487
+ if (!c.url) throw new TtsConfigError("qwen3-3060 provider requires url in config");
36583
36488
  try {
36584
- oggPath = await wavToOggOpusNative(wavPath);
36489
+ const { oggPath, durationMs } = await ttsQwen3Opus(text, { url: c.url, voice: c.voice, lang: c.lang });
36490
+ return { path: oggPath, format: "ogg", durationMs, unit: unit(text), voice: c.voice };
36585
36491
  } catch (e) {
36586
- if (globalThis.__ffmpegMissing) throw e;
36587
- console.warn(`[my-voice] native opus encode failed (${e.message}), trying ffmpeg`);
36588
- oggPath = wavPath.replace(/\.wav$/, ".ogg");
36589
- await execFileAsync2("ffmpeg", [
36590
- "-y",
36591
- "-i",
36592
- wavPath,
36593
- "-c:a",
36594
- "libopus",
36595
- "-b:a",
36596
- "32k",
36597
- "-ar",
36598
- "24000",
36599
- oggPath
36600
- ], { timeout: 3e4 });
36601
- }
36602
- if (!fs42.existsSync(oggPath) || fs42.statSync(oggPath).size < 20) {
36603
- throw new Error("ogg/opus encode produced empty output");
36604
- }
36605
- fs42.unlinkSync(wavPath);
36606
- return oggPath;
36492
+ console.warn(`[my-voice] qwen3-3060 opus failed (${e.message}), trying PCM stream`);
36493
+ const pcmChunks = [];
36494
+ await ttsQwen3Local(text, { url: c.url, voice: c.voice, lang: c.lang }, (pcm2) => pcmChunks.push(pcm2));
36495
+ const pcm = Buffer.concat(pcmChunks);
36496
+ fs40.mkdirSync(VOICE_DIR, { recursive: true });
36497
+ const wavPath = path40.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36498
+ const header = Buffer.alloc(44);
36499
+ header.write("RIFF", 0);
36500
+ header.writeUInt32LE(36 + pcm.length, 4);
36501
+ header.write("WAVE", 8);
36502
+ header.write("fmt ", 12);
36503
+ header.writeUInt32LE(16, 16);
36504
+ header.writeUInt16LE(1, 20);
36505
+ header.writeUInt16LE(1, 22);
36506
+ header.writeUInt32LE(24e3, 24);
36507
+ header.writeUInt32LE(48e3, 28);
36508
+ header.writeUInt16LE(2, 32);
36509
+ header.writeUInt16LE(16, 34);
36510
+ header.write("data", 36);
36511
+ header.writeUInt32LE(pcm.length, 40);
36512
+ fs40.writeFileSync(wavPath, Buffer.concat([header, pcm]));
36513
+ return { path: wavPath, format: "wav", unit: unit(text), voice: c.voice };
36514
+ }
36515
+ }
36516
+ async function synthEdge(text, _sink) {
36517
+ const p = await ttsEdge(text);
36518
+ return { path: p, format: "mp3", unit: unit(text) };
36607
36519
  }
36608
- async function sendVoiceToMulticaIssue(text, issueId, opts) {
36609
- const vc = liveConfig.get("tools.my_voice");
36610
- let audioPath = "";
36611
- let actualEngine = opts.engine || vc?.provider || "edge";
36612
- try {
36613
- if (actualEngine === "qwen3-3060" && vc?.url) {
36614
- try {
36615
- const { oggPath } = await ttsQwen3Opus(text, { url: vc.url, voice: vc.voice, lang: vc.lang });
36616
- audioPath = oggPath;
36617
- } catch (e) {
36618
- console.warn(`[my-voice:multaca] qwen3 opus failed (${e.message}), trying PCM`);
36619
- const pcmChunks = [];
36620
- await ttsQwen3Local(text, { url: vc.url, voice: vc.voice, lang: vc.lang }, (pcm2) => pcmChunks.push(pcm2));
36621
- const pcm = Buffer.concat(pcmChunks);
36622
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36623
- const wavPath = path41.join(VOICE_DIR, `tts_mc_${Date.now()}.wav`);
36624
- const header = Buffer.alloc(44);
36625
- header.write("RIFF", 0);
36626
- header.writeUInt32LE(36 + pcm.length, 4);
36627
- header.write("WAVE", 8);
36628
- header.write("fmt ", 12);
36629
- header.writeUInt32LE(16, 16);
36630
- header.writeUInt16LE(1, 20);
36631
- header.writeUInt16LE(1, 22);
36632
- header.writeUInt32LE(24e3, 24);
36633
- header.writeUInt32LE(48e3, 28);
36634
- header.writeUInt16LE(2, 32);
36635
- header.writeUInt16LE(16, 34);
36636
- header.write("data", 36);
36637
- header.writeUInt32LE(pcm.length, 40);
36638
- fs42.writeFileSync(wavPath, Buffer.concat([header, pcm]));
36639
- audioPath = wavPath;
36640
- }
36641
- } else if (actualEngine === "cosyvoice" && vc?.apiKey) {
36642
- audioPath = await ttsCosyvoice(text, vc.apiKey, vc.model || "cosyvoice-v1", vc.voice, vc.workspaceId, vc.instruction);
36643
- } else if (actualEngine === "gptsovits") {
36644
- audioPath = await ttsGptsovits(text);
36645
- } else {
36646
- audioPath = await ttsEdge(text);
36647
- actualEngine = "edge";
36648
- }
36649
- } catch (e) {
36650
- return { content: `\u8BED\u97F3\u751F\u6210\u5931\u8D25\uFF08headless\u2192multaca\uFF09\uFF1A${e.message}\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002`, isError: true };
36651
- }
36652
- if (!audioPath || !fs42.existsSync(audioPath)) {
36653
- return { content: "\u8BED\u97F3\u751F\u6210\u5931\u8D25\uFF08headless\u2192multaca\uFF09\uFF1ATTS \u672A\u4EA7\u51FA\u6587\u4EF6\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002", isError: true };
36654
- }
36655
- if (audioPath.endsWith(".wav")) {
36656
- try {
36657
- audioPath = await wavToOggOpus(audioPath);
36658
- } catch (e) {
36659
- console.warn(`[my-voice:multaca] opus encode failed (${e.message}), attaching wav`);
36660
- }
36661
- }
36662
- const ext = path41.extname(audioPath).toLowerCase();
36663
- const sizeKB = fs42.statSync(audioPath).size / 1024;
36664
- const voiceFile = `voice_${Date.now()}${ext}`;
36665
- const cwdCopy = path41.join(process.cwd(), voiceFile);
36666
- try {
36667
- fs42.copyFileSync(audioPath, cwdCopy);
36668
- } catch (e) {
36669
- try {
36670
- fs42.unlinkSync(audioPath);
36671
- } catch {
36672
- }
36673
- return { content: `\u8BED\u97F3\u6302\u8F7D\u5931\u8D25\uFF08\u62F7\u8D1D\u5230\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1A${e.message}`, isError: true };
36674
- }
36675
- try {
36676
- fs42.unlinkSync(audioPath);
36677
- } catch {
36678
- }
36679
- const content = opts.caption || `\u{1F399}\uFE0F \u8BED\u97F3\u6D88\u606F\uFF08${text.length} \u5B57\uFF09`;
36680
- try {
36681
- const { execFile: execFile4 } = await import("node:child_process");
36682
- const { promisify: promisify4 } = await import("node:util");
36683
- const execFileA = promisify4(execFile4);
36684
- const { stdout } = await execFileA("multica", [
36685
- "issue",
36686
- "comment",
36687
- "add",
36688
- issueId,
36689
- "--content",
36690
- content,
36691
- "--attachment",
36692
- voiceFile,
36693
- "--output",
36694
- "json"
36695
- ], { timeout: 6e4 });
36696
- let ok = false;
36697
- try {
36698
- const parsed = JSON.parse(stdout);
36699
- ok = !!(parsed?.comment_id || parsed?.id || parsed?.attachments && parsed.attachments.length > 0);
36700
- } catch {
36701
- ok = stdout.includes("Comment added");
36702
- }
36703
- if (!ok) throw new Error(`unexpected CLI output: ${String(stdout).slice(0, 200)}`);
36704
- try {
36705
- fs42.unlinkSync(cwdCopy);
36706
- } catch {
36707
- }
36708
- return { content: `Voice sent to multaca issue ${issueId}! (${actualEngine}, ${sizeKB.toFixed(0)}KB, \u9644\u4EF6\u5DF2\u6302\u5230\u8BC4\u8BBA\u2014\u2014\u5728 multaca \u5BA2\u6237\u7AEF\u64AD\u653E)` };
36709
- } catch (e) {
36710
- try {
36711
- fs42.unlinkSync(cwdCopy);
36712
- } catch {
36713
- }
36714
- return { content: `\u8BED\u97F3\u6302\u5230 multaca issue \u5931\u8D25\uFF1A${e.message}\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002`, isError: true };
36715
- }
36716
- }
36717
- registry.register({
36718
- name: "my_voice",
36719
- description: "Generate and send a voice message. Use for: \u53D1\u8BED\u97F3/\u8BF4\u53E5\u8BDD/\u5F55\u4E00\u6BB5/\u53D1\u58F0\u97F3/\u60F3\u542C\u4F60\u7684\u58F0\u97F3/\u8BED\u97F3\u6D88\u606F/\u8BF4\u7ED9\u6211\u542C. Uses CosyVoice (\u914D\u7F6E voiceChat.local.tts) with edge-tts fallback. Sends to weixin or feishu.",
36720
- schema: {
36721
- type: "object",
36722
- properties: {
36723
- text: { type: "string", description: "What to say (Chinese text)." },
36724
- 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." },
36725
- channel: { type: "string", enum: ["weixin", "feishu", "discord"], description: "Target channel. Default: current channel." },
36726
- 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\u3002multaca \u4EFB\u52A1\u91CC\u4E0D\u9700\u8981\u586B to\u2014\u2014\u8BED\u97F3\u4F1A\u6302\u5230\u5F53\u524D issue \u8BC4\u8BBA\uFF0C\u586B issueId \u6307\u5B9A\u76EE\u6807 issue\u3002" },
36727
- issueId: { type: "string", description: 'multaca headless \u4E13\u7528\uFF1A\u76EE\u6807 issue ID\uFF08\u4EFB\u52A1 prompt \u91CC\u7684 "Your assigned issue ID"\uFF09\u3002\u666E\u901A\u98DE\u4E66/\u5FAE\u4FE1\u573A\u666F\u4E0D\u8981\u586B\u3002' },
36728
- caption: { type: "string", description: "Optional text to accompany the voice message." }
36729
- },
36730
- required: ["text"]
36731
- },
36732
- handler: async (args2, ctx) => {
36733
- const text = args2.text?.trim();
36734
- if (!text) return { content: "Error: text is required", isError: true };
36735
- const requestedEngine = args2.engine || "";
36736
- const caption = args2.caption || "";
36737
- const mgr = ctx.channelManager;
36738
- if (!mgr) {
36739
- const multicaTaskId = process.env.MULTICA_TASK_ID || "";
36740
- const issueId = args2.issueId || multicaTaskId;
36741
- if (multicaTaskId && issueId) {
36742
- return await sendVoiceToMulticaIssue(text, issueId, { engine: requestedEngine, caption });
36743
- }
36744
- return {
36745
- content: "\u8BED\u97F3\u672A\u53D1\u9001\uFF1A\u5F53\u524D\u662F\u65E0\u6E20\u9053\u73AF\u5883\uFF08headless/CLI\uFF09\uFF0C\u4E14\u6CA1\u6709 multaca \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08MULTICA_TASK_ID\uFF09\u6216 issueId\u3002\u6CA1\u6709\u53EF\u7528\u7684\u53D1\u9001\u51FA\u53E3\uFF0C\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002",
36746
- isError: true
36747
- };
36748
- }
36749
- const resolvedChannelRaw = args2.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
36750
- const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
36751
- let target = args2.to || ctx.channelTarget || ctx.from;
36752
- if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
36753
- console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
36754
- return {
36755
- 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`,
36756
- isError: true
36757
- };
36758
- }
36759
- if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
36760
- console.warn(`[my-voice] feishu target "${target}" invalid`);
36761
- return {
36762
- 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`,
36763
- isError: true
36764
- };
36765
- }
36766
- let voiceDurationSec;
36767
- const vc = liveConfig.get("tools.my_voice");
36768
- const provider = vc?.provider || "";
36769
- const useEngine = requestedEngine || provider || "edge";
36770
- let audioPath;
36771
- let actualEngine = useEngine;
36772
- try {
36773
- if (useEngine === "cosyvoice") {
36774
- if (!vc?.apiKey || !vc?.voice) throw new Error("cosyvoice provider requires apiKey and voice in config");
36775
- try {
36776
- audioPath = await ttsCosyvoice(
36777
- text,
36778
- vc.apiKey,
36779
- vc.model || "cosyvoice-v1",
36780
- vc.voice,
36781
- vc.workspaceId,
36782
- vc.instruction
36783
- );
36784
- } catch (e) {
36785
- console.log(`[my-voice] cosyvoice failed (${e.message}), falling back to edge-tts`);
36786
- audioPath = await ttsEdge(text);
36787
- actualEngine = "edge";
36788
- }
36789
- } else if (useEngine === "gptsovits") {
36790
- try {
36791
- audioPath = await ttsGptsovits(text);
36792
- } catch (e) {
36793
- console.log(`[my-voice] gptsovits failed (${e.message}), falling back to edge-tts`);
36794
- audioPath = await ttsEdge(text);
36795
- actualEngine = "edge";
36796
- }
36797
- } else if (useEngine === "qwen3-3060") {
36798
- if (!vc?.url) throw new Error("qwen3-3060 provider requires url in config");
36799
- try {
36800
- try {
36801
- const { oggPath, durationMs } = await ttsQwen3Opus(text, { url: vc.url, voice: vc.voice, lang: vc.lang });
36802
- audioPath = oggPath;
36803
- if (durationMs > 0) voiceDurationSec = durationMs / 1e3;
36804
- } catch (e) {
36805
- console.warn(`[my-voice] qwen3-3060 opus failed (${e.message}), trying PCM stream`);
36806
- const pcmChunks = [];
36807
- await ttsQwen3Local(text, { url: vc.url, voice: vc.voice, lang: vc.lang }, (pcm2) => pcmChunks.push(pcm2));
36808
- const pcm = Buffer.concat(pcmChunks);
36809
- fs42.mkdirSync(VOICE_DIR, { recursive: true });
36810
- const wavPath = path41.join(VOICE_DIR, `tts_${Date.now()}.wav`);
36811
- const header = Buffer.alloc(44);
36812
- header.write("RIFF", 0);
36813
- header.writeUInt32LE(36 + pcm.length, 4);
36814
- header.write("WAVE", 8);
36815
- header.write("fmt ", 12);
36816
- header.writeUInt32LE(16, 16);
36817
- header.writeUInt16LE(1, 20);
36818
- header.writeUInt16LE(1, 22);
36819
- header.writeUInt32LE(24e3, 24);
36820
- header.writeUInt32LE(48e3, 28);
36821
- header.writeUInt16LE(2, 32);
36822
- header.writeUInt16LE(16, 34);
36823
- header.write("data", 36);
36824
- header.writeUInt32LE(pcm.length, 40);
36825
- fs42.writeFileSync(wavPath, Buffer.concat([header, pcm]));
36826
- audioPath = wavPath;
36827
- }
36828
- } catch (e) {
36829
- console.warn(`[my-voice] qwen3-3060 failed (${e.message}), falling back to edge-tts`);
36830
- audioPath = await ttsEdge(text);
36831
- actualEngine = "edge";
36832
- }
36833
- } else {
36834
- audioPath = await ttsEdge(text);
36835
- }
36836
- } catch (e) {
36837
- return { content: `TTS failed: ${e.message}`, isError: true };
36838
- }
36839
- if (!audioPath.endsWith(".ogg")) {
36840
- try {
36841
- const r = await toWav24kWithDuration(audioPath);
36842
- audioPath = r.wavPath;
36843
- voiceDurationSec = voiceDurationSec ?? r.durationSec;
36844
- } catch (e) {
36845
- console.warn(`[my-voice] toWav24k failed (${e.message}), sending raw file`);
36846
- }
36847
- }
36848
- if (audioPath.endsWith(".wav")) {
36849
- try {
36850
- audioPath = await wavToOggOpus(audioPath);
36851
- } catch (e) {
36852
- console.warn(`[my-voice] ogg/opus encode failed (${e.message}), falling back to m4a`);
36853
- audioPath = await compressWav(audioPath);
36854
- }
36855
- }
36856
- const ext = path41.extname(audioPath).toLowerCase();
36857
- const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg", ".silk": "audio/silk" };
36858
- const mimeType = mimeMap[ext] || "audio/mpeg";
36859
- const sizeKB = fs42.statSync(audioPath).size / 1024;
36860
- try {
36861
- await mgr.sendFile(resolvedChannel, target, caption, {
36862
- path: audioPath,
36863
- mimeType,
36864
- filename: `voice_${Date.now()}${ext}`,
36865
- ...voiceDurationSec !== void 0 ? { voiceDurationSec } : {}
36866
- });
36867
- try {
36868
- fs42.unlinkSync(audioPath);
36869
- } catch {
36870
- }
36871
- return { content: `Voice sent! (${actualEngine}, ${sizeKB.toFixed(0)}KB, ${resolvedChannel})` };
36872
- } catch (err) {
36873
- return { content: `Voice send failed: ${err.message}`, isError: true };
36874
- }
36875
- },
36876
- isConcurrencySafe: () => false,
36877
- interruptBehavior: () => "block",
36878
- isReadOnly: () => false,
36879
- isDestructive: () => false
36880
- });
36881
36520
 
36882
36521
  // src/channels/desk-buddy.ts
36883
36522
  init_live();
@@ -36894,23 +36533,23 @@ var DeskBuddyAdapter = class {
36894
36533
  appendHistory(entry) {
36895
36534
  try {
36896
36535
  if (!this.historyFile) {
36897
- const os6 = __require("os");
36898
- const fs57 = __require("fs");
36899
- const path58 = __require("path");
36900
- const dir = path58.join(os6.homedir(), "xiaowen", "workspace", ".desktopBuddy");
36901
- fs57.mkdirSync(dir, { recursive: true });
36902
- this.historyFile = path58.join(dir, "history.json");
36903
- this._fs = fs57;
36904
- }
36905
- const fs56 = this._fs;
36536
+ const os7 = __require("os");
36537
+ const fs59 = __require("fs");
36538
+ const path60 = __require("path");
36539
+ const dir = path60.join(os7.homedir(), "xiaowen", "workspace", ".desktopBuddy");
36540
+ fs59.mkdirSync(dir, { recursive: true });
36541
+ this.historyFile = path60.join(dir, "history.json");
36542
+ this._fs = fs59;
36543
+ }
36544
+ const fs58 = this._fs;
36906
36545
  let arr = [];
36907
36546
  try {
36908
- arr = JSON.parse(fs56.readFileSync(this.historyFile, "utf-8")) || [];
36547
+ arr = JSON.parse(fs58.readFileSync(this.historyFile, "utf-8")) || [];
36909
36548
  } catch {
36910
36549
  }
36911
36550
  arr.push({ ...entry, ts: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN", { hour12: false }) });
36912
36551
  if (arr.length > 500) arr = arr.slice(-500);
36913
- fs56.writeFileSync(this.historyFile, JSON.stringify(arr, null, 1));
36552
+ fs58.writeFileSync(this.historyFile, JSON.stringify(arr, null, 1));
36914
36553
  } catch (e) {
36915
36554
  console.warn("[deskBuddy] history write fail", e);
36916
36555
  }
@@ -37006,8 +36645,8 @@ var DeskBuddyAdapter = class {
37006
36645
  startAsr() {
37007
36646
  if (this.asrSock) return;
37008
36647
  const port = this.asrBackend === "sensevoice" ? 9790 : 9791;
37009
- const net5 = __require("node:net");
37010
- const sock = net5.connect(port, "127.0.0.1");
36648
+ const net4 = __require("node:net");
36649
+ const sock = net4.connect(port, "127.0.0.1");
37011
36650
  sock.setNoDelay(true);
37012
36651
  this.asrSock = sock;
37013
36652
  this.asrBuf = "";
@@ -37068,9 +36707,9 @@ var DeskBuddyAdapter = class {
37068
36707
  sock.on("error", (e) => {
37069
36708
  console.warn(`[deskBuddy] ASR tcp error (${this.asrBackend}):`, e.message);
37070
36709
  this.sendToClient({ type: "asr-text", text: "(ASR \u670D\u52A1\u6CA1\u5728\u8DD1\uFF0C\u6B63\u5728\u62C9\u8D77\u2026\u518D\u6309\u4E00\u6B21\u8BF4\u8BDD)" });
37071
- const { exec: exec4 } = __require("node:child_process");
36710
+ const { exec: exec3 } = __require("node:child_process");
37072
36711
  const ctr = this.asrBackend === "sensevoice" ? "xiaowen-asr-svc" : "xw-asr-stream";
37073
- exec4(`docker start ${ctr} 2>/dev/null || true`);
36712
+ exec3(`docker start ${ctr} 2>/dev/null || true`);
37074
36713
  this.asrSock = null;
37075
36714
  });
37076
36715
  }
@@ -37523,20 +37162,20 @@ var ChannelManager = class {
37523
37162
  // src/session/session-manager.ts
37524
37163
  init_types();
37525
37164
  init_types();
37526
- import * as path43 from "node:path";
37527
- import * as fs44 from "node:fs";
37165
+ import * as path42 from "node:path";
37166
+ import * as fs42 from "node:fs";
37528
37167
  import * as crypto6 from "node:crypto";
37529
37168
 
37530
37169
  // src/session/reader.ts
37531
37170
  init_attachments();
37532
- import * as fs43 from "node:fs";
37533
- import * as path42 from "node:path";
37171
+ import * as fs41 from "node:fs";
37172
+ import * as path41 from "node:path";
37534
37173
  import * as readline2 from "node:readline";
37535
37174
  var SKIP_PRECOMPACT_THRESHOLD = 5 * 1024 * 1024;
37536
37175
  var TRANSCRIPT_READ_CHUNK_SIZE = 1024 * 1024;
37537
37176
  var MAX_TRANSCRIPT_READ_BYTES = 50 * 1024 * 1024;
37538
37177
  async function readSessionHeader(filePath) {
37539
- const stream = fs43.createReadStream(filePath, { encoding: "utf-8" });
37178
+ const stream = fs41.createReadStream(filePath, { encoding: "utf-8" });
37540
37179
  const rl = readline2.createInterface({ input: stream });
37541
37180
  return new Promise((resolve12) => {
37542
37181
  let resolved = false;
@@ -37567,7 +37206,7 @@ async function readSessionHeader(filePath) {
37567
37206
  });
37568
37207
  }
37569
37208
  async function readLargeFilePostBoundary(filePath) {
37570
- const stat4 = fs43.statSync(filePath);
37209
+ const stat4 = fs41.statSync(filePath);
37571
37210
  const fileSize = stat4.size;
37572
37211
  const boundaryMarker = '"compact_boundary"';
37573
37212
  const CHUNK_SIZE = TRANSCRIPT_READ_CHUNK_SIZE;
@@ -37575,13 +37214,13 @@ async function readLargeFilePostBoundary(filePath) {
37575
37214
  let outLen = 0;
37576
37215
  let boundaryStartOffset = 0;
37577
37216
  const chunk = Buffer.allocUnsafe(CHUNK_SIZE);
37578
- const fd = fs43.openSync(filePath, "r");
37217
+ const fd = fs41.openSync(filePath, "r");
37579
37218
  try {
37580
37219
  let filePos = 0;
37581
37220
  let carryBuf;
37582
37221
  let carryLen = 0;
37583
37222
  while (filePos < fileSize) {
37584
- const bytesRead = fs43.readSync(fd, chunk, 0, Math.min(CHUNK_SIZE, fileSize - filePos), filePos);
37223
+ const bytesRead = fs41.readSync(fd, chunk, 0, Math.min(CHUNK_SIZE, fileSize - filePos), filePos);
37585
37224
  if (bytesRead === 0) break;
37586
37225
  filePos += bytesRead;
37587
37226
  let scanBuf;
@@ -37633,7 +37272,7 @@ async function readLargeFilePostBoundary(filePath) {
37633
37272
  }
37634
37273
  }
37635
37274
  } finally {
37636
- fs43.closeSync(fd);
37275
+ fs41.closeSync(fd);
37637
37276
  }
37638
37277
  const postBoundaryText = outBuf.subarray(0, outLen).toString("utf-8");
37639
37278
  return postBoundaryText.split("\n").filter((l) => l.trim().length > 0);
@@ -37757,7 +37396,7 @@ function recoverOrphanedParallelToolResults(allEntries, chain, byUuid, seen) {
37757
37396
  return result;
37758
37397
  }
37759
37398
  async function readSessionHistory(filePath) {
37760
- const stat4 = fs43.statSync(filePath);
37399
+ const stat4 = fs41.statSync(filePath);
37761
37400
  const fileSize = stat4.size;
37762
37401
  if (fileSize > MAX_TRANSCRIPT_READ_BYTES) {
37763
37402
  console.warn(`[reader] Session file exceeds 50MB limit (${(fileSize / 1024 / 1024).toFixed(1)}MB), using chunked fd read with boundary skip`);
@@ -37796,7 +37435,7 @@ async function readSessionHistory(filePath) {
37796
37435
  return [...sessionStarts, ...filtered, ...restAttachments];
37797
37436
  }
37798
37437
  async function readAllLines(filePath) {
37799
- const stream = fs43.createReadStream(filePath, { encoding: "utf-8" });
37438
+ const stream = fs41.createReadStream(filePath, { encoding: "utf-8" });
37800
37439
  const rl = readline2.createInterface({ input: stream });
37801
37440
  const lines = [];
37802
37441
  return new Promise((resolve12, reject) => {
@@ -37950,12 +37589,12 @@ function extractText4(content) {
37950
37589
  return content.filter((b) => b.type === "text").map((b) => b.text || "").join("") || null;
37951
37590
  }
37952
37591
  function listSessions(sessionsDir) {
37953
- if (!fs43.existsSync(sessionsDir)) return [];
37954
- const files = fs43.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl") || f.includes(".jsonl.")).map((f) => ({
37955
- file: path42.join(sessionsDir, f),
37592
+ if (!fs41.existsSync(sessionsDir)) return [];
37593
+ const files = fs41.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl") || f.includes(".jsonl.")).map((f) => ({
37594
+ file: path41.join(sessionsDir, f),
37956
37595
  id: f.replace(/\.jsonl.*$/, ""),
37957
37596
  // strip .jsonl and everything after (archived suffix)
37958
- modified: new Date(fs43.statSync(path42.join(sessionsDir, f)).mtimeMs)
37597
+ modified: new Date(fs41.statSync(path41.join(sessionsDir, f)).mtimeMs)
37959
37598
  })).sort((a, b) => b.modified.getTime() - a.modified.getTime());
37960
37599
  return files;
37961
37600
  }
@@ -37990,24 +37629,24 @@ var SessionManager = class {
37990
37629
  constructor(config2) {
37991
37630
  this.config = config2;
37992
37631
  this.scopeConfig = config2.scopeConfig;
37993
- this.sessionsDir = path43.join(config2.stateDir, "agents", "main", "sessions");
37994
- this.indexPath = path43.join(this.sessionsDir, "session-index.json");
37995
- this.platformMapPath = path43.join(this.sessionsDir, "platform-map.json");
37996
- fs44.mkdirSync(this.sessionsDir, { recursive: true });
37632
+ this.sessionsDir = path42.join(config2.stateDir, "agents", "main", "sessions");
37633
+ this.indexPath = path42.join(this.sessionsDir, "session-index.json");
37634
+ this.platformMapPath = path42.join(this.sessionsDir, "platform-map.json");
37635
+ fs42.mkdirSync(this.sessionsDir, { recursive: true });
37997
37636
  }
37998
37637
  // === 索引操作 ===
37999
37638
  loadIndex() {
38000
37639
  try {
38001
- if (fs44.existsSync(this.indexPath)) {
38002
- const raw = fs44.readFileSync(this.indexPath, "utf-8");
37640
+ if (fs42.existsSync(this.indexPath)) {
37641
+ const raw = fs42.readFileSync(this.indexPath, "utf-8");
38003
37642
  const obj = JSON.parse(raw);
38004
37643
  this.index = new Map(Object.entries(obj));
38005
37644
  let fixed = 0;
38006
37645
  for (const [key, entry] of this.index) {
38007
- if (fs44.existsSync(entry.file)) continue;
38008
- const basename10 = path43.basename(entry.file);
38009
- const localPath = path43.join(this.sessionsDir, basename10);
38010
- if (fs44.existsSync(localPath)) {
37646
+ if (fs42.existsSync(entry.file)) continue;
37647
+ const basename10 = path42.basename(entry.file);
37648
+ const localPath = path42.join(this.sessionsDir, basename10);
37649
+ if (fs42.existsSync(localPath)) {
38011
37650
  entry.file = localPath;
38012
37651
  fixed++;
38013
37652
  }
@@ -38026,7 +37665,7 @@ var SessionManager = class {
38026
37665
  try {
38027
37666
  const obj = {};
38028
37667
  for (const [k, v] of this.index) obj[k] = v;
38029
- fs44.writeFileSync(this.indexPath, JSON.stringify(obj, null, 2));
37668
+ fs42.writeFileSync(this.indexPath, JSON.stringify(obj, null, 2));
38030
37669
  } catch (err) {
38031
37670
  console.error(`[session] Failed to save index: ${err.message}`);
38032
37671
  }
@@ -38034,8 +37673,8 @@ var SessionManager = class {
38034
37673
  // === 平台映射 ===
38035
37674
  loadPlatformMap() {
38036
37675
  try {
38037
- if (fs44.existsSync(this.platformMapPath)) {
38038
- const raw = fs44.readFileSync(this.platformMapPath, "utf-8");
37676
+ if (fs42.existsSync(this.platformMapPath)) {
37677
+ const raw = fs42.readFileSync(this.platformMapPath, "utf-8");
38039
37678
  const obj = JSON.parse(raw);
38040
37679
  this.platformMap = new Map(Object.entries(obj));
38041
37680
  console.log(`[session] Platform map loaded: ${this.platformMap.size} entries`);
@@ -38047,7 +37686,7 @@ var SessionManager = class {
38047
37686
  try {
38048
37687
  const obj = {};
38049
37688
  for (const [k, v] of this.platformMap) obj[k] = v;
38050
- fs44.writeFileSync(this.platformMapPath, JSON.stringify(obj, null, 2));
37689
+ fs42.writeFileSync(this.platformMapPath, JSON.stringify(obj, null, 2));
38051
37690
  } catch (err) {
38052
37691
  console.error(`[session] Failed to save platform map: ${err.message}`);
38053
37692
  }
@@ -38086,17 +37725,17 @@ var SessionManager = class {
38086
37725
  */
38087
37726
  getSessionFilePath(sessionId) {
38088
37727
  const entry = this.index.get(sessionId);
38089
- if (entry && fs44.existsSync(entry.file)) return entry.file;
38090
- const direct = path43.join(this.sessionsDir, `${sessionId}.jsonl`);
38091
- return fs44.existsSync(direct) ? direct : null;
37728
+ if (entry && fs42.existsSync(entry.file)) return entry.file;
37729
+ const direct = path42.join(this.sessionsDir, `${sessionId}.jsonl`);
37730
+ return fs42.existsSync(direct) ? direct : null;
38092
37731
  }
38093
37732
  // === Writer 管理 ===
38094
37733
  getWriter(sessionId) {
38095
37734
  let writer = this.writers.get(sessionId);
38096
37735
  if (!writer) {
38097
37736
  const existing = this.index.get(sessionId);
38098
- if (existing && fs44.existsSync(existing.file)) {
38099
- const existingFileId = path43.basename(existing.file, ".jsonl");
37737
+ if (existing && fs42.existsSync(existing.file)) {
37738
+ const existingFileId = path42.basename(existing.file, ".jsonl");
38100
37739
  writer = new SessionWriter(this.sessionsDir, existingFileId);
38101
37740
  writer.open(this.config.workspace, sessionId);
38102
37741
  } else {
@@ -38108,7 +37747,7 @@ var SessionManager = class {
38108
37747
  this.saveIndex();
38109
37748
  this.writers.set(sessionId, writer);
38110
37749
  this.onWriterCreated?.(writer, sessionId);
38111
- console.log(`[session] ${existing ? "Resumed" : "New"}: ${sessionId} \u2192 ${path43.basename(writer.path)}`);
37750
+ console.log(`[session] ${existing ? "Resumed" : "New"}: ${sessionId} \u2192 ${path42.basename(writer.path)}`);
38112
37751
  }
38113
37752
  return writer;
38114
37753
  }
@@ -38127,7 +37766,7 @@ var SessionManager = class {
38127
37766
  if (!writer) return null;
38128
37767
  try {
38129
37768
  this.lastArchiveCheck = Date.now();
38130
- const stat4 = fs44.statSync(writer.path);
37769
+ const stat4 = fs42.statSync(writer.path);
38131
37770
  if (stat4.size < threshold) return null;
38132
37771
  const sizeMB = (stat4.size / 1024 / 1024).toFixed(1);
38133
37772
  const thresholdMB = (threshold / 1024 / 1024).toFixed(1);
@@ -38136,10 +37775,10 @@ var SessionManager = class {
38136
37775
  if (archivedPath) {
38137
37776
  this.index.set(sessionId, { file: writer.path, updatedAt: Date.now() });
38138
37777
  this.saveIndex();
38139
- const newSize = fs44.statSync(writer.path).size;
37778
+ const newSize = fs42.statSync(writer.path).size;
38140
37779
  const oldSizeMB = (stat4.size / 1024 / 1024).toFixed(1);
38141
- console.log(`[session] Archive complete: ${path43.basename(archivedPath)} (${oldSizeMB}MB) \u2192 new JSONL created (${newSize} bytes)`);
38142
- const fileId = path43.basename(writer.path).replace(/\.jsonl$/, "");
37780
+ console.log(`[session] Archive complete: ${path42.basename(archivedPath)} (${oldSizeMB}MB) \u2192 new JSONL created (${newSize} bytes)`);
37781
+ const fileId = path42.basename(writer.path).replace(/\.jsonl$/, "");
38143
37782
  this.purgeOldArchives(fileId, 3);
38144
37783
  return archivedPath;
38145
37784
  }
@@ -38155,15 +37794,15 @@ var SessionManager = class {
38155
37794
  purgeOldArchives(sessionId, keepCount = 3) {
38156
37795
  try {
38157
37796
  const dir = this.sessionsDir;
38158
- if (!fs44.existsSync(dir)) return;
38159
- const allFiles = fs44.readdirSync(dir);
37797
+ if (!fs42.existsSync(dir)) return;
37798
+ const allFiles = fs42.readdirSync(dir);
38160
37799
  const archiveFiles = allFiles.filter((f) => {
38161
37800
  if (!f.startsWith(sessionId)) return false;
38162
37801
  return f.includes(".archived.") || f.includes(".compaction.");
38163
37802
  }).map((f) => ({
38164
37803
  name: f,
38165
- path: path43.join(dir, f),
38166
- mtime: fs44.statSync(path43.join(dir, f)).mtimeMs
37804
+ path: path42.join(dir, f),
37805
+ mtime: fs42.statSync(path42.join(dir, f)).mtimeMs
38167
37806
  })).sort((a, b) => b.mtime - a.mtime);
38168
37807
  console.log(`[session] purgeOldArchives: sessionId=${sessionId.substring(0, 8)}... found=${archiveFiles.length} keep=${keepCount}`);
38169
37808
  if (archiveFiles.length <= keepCount) return;
@@ -38173,7 +37812,7 @@ var SessionManager = class {
38173
37812
  console.log(`[session] Skipping archived (kept for history): ${f.name}`);
38174
37813
  continue;
38175
37814
  }
38176
- fs44.unlinkSync(f.path);
37815
+ fs42.unlinkSync(f.path);
38177
37816
  console.log(`[session] Purged old archive: ${f.name}`);
38178
37817
  }
38179
37818
  if (toDelete.length > 0) {
@@ -38222,29 +37861,29 @@ var SessionManager = class {
38222
37861
  /** 列出 session 的所有文件(当前 + 归档),按修改时间排序 */
38223
37862
  findAllSessionFiles(sessionId) {
38224
37863
  const results = [];
38225
- const mainFile = path43.join(this.sessionsDir, `${sessionId}.jsonl`);
38226
- if (fs44.existsSync(mainFile)) {
37864
+ const mainFile = path42.join(this.sessionsDir, `${sessionId}.jsonl`);
37865
+ if (fs42.existsSync(mainFile)) {
38227
37866
  results.push(mainFile);
38228
37867
  }
38229
- if (fs44.existsSync(this.sessionsDir)) {
38230
- const compactionFiles = fs44.readdirSync(this.sessionsDir).filter((f) => f.startsWith(`${sessionId}.jsonl.compaction.`)).map((f) => ({
37868
+ if (fs42.existsSync(this.sessionsDir)) {
37869
+ const compactionFiles = fs42.readdirSync(this.sessionsDir).filter((f) => f.startsWith(`${sessionId}.jsonl.compaction.`)).map((f) => ({
38231
37870
  name: f,
38232
- path: path43.join(this.sessionsDir, f),
38233
- mtime: fs44.statSync(path43.join(this.sessionsDir, f)).mtimeMs
37871
+ path: path42.join(this.sessionsDir, f),
37872
+ mtime: fs42.statSync(path42.join(this.sessionsDir, f)).mtimeMs
38234
37873
  })).sort((a, b) => b.mtime - a.mtime);
38235
37874
  if (compactionFiles.length > 0) {
38236
37875
  results.push(compactionFiles[0].path);
38237
37876
  }
38238
37877
  }
38239
37878
  results.sort((a, b) => {
38240
- const ta = fs44.statSync(a).mtimeMs;
38241
- const tb = fs44.statSync(b).mtimeMs;
37879
+ const ta = fs42.statSync(a).mtimeMs;
37880
+ const tb = fs42.statSync(b).mtimeMs;
38242
37881
  return ta - tb;
38243
37882
  });
38244
37883
  return results;
38245
37884
  }
38246
37885
  async findSessionFiles(sessionId) {
38247
- if (!fs44.existsSync(this.sessionsDir)) {
37886
+ if (!fs42.existsSync(this.sessionsDir)) {
38248
37887
  console.log(`[session:find] SESSIONS_DIR not found: ${this.sessionsDir}`);
38249
37888
  return [];
38250
37889
  }
@@ -38256,15 +37895,15 @@ var SessionManager = class {
38256
37895
  return mappedFiles;
38257
37896
  }
38258
37897
  const indexedEntry = this.index.get(mappedId);
38259
- if (indexedEntry && fs44.existsSync(indexedEntry.file)) {
38260
- const fileUuid = path43.basename(indexedEntry.file, ".jsonl");
37898
+ if (indexedEntry && fs42.existsSync(indexedEntry.file)) {
37899
+ const fileUuid = path42.basename(indexedEntry.file, ".jsonl");
38261
37900
  return this.findAllSessionFiles(fileUuid);
38262
37901
  }
38263
37902
  }
38264
37903
  const allFiles = this.findAllSessionFiles(sessionId);
38265
37904
  if (allFiles.length > 0) {
38266
37905
  const labels = allFiles.map((f) => {
38267
- const base = path43.basename(f);
37906
+ const base = path42.basename(f);
38268
37907
  return base.endsWith(".jsonl") ? base : `${base} (archived)`;
38269
37908
  });
38270
37909
  console.log(`[session:find] Found ${allFiles.length} file(s) for ${sessionId}: ${labels.join(", ")}`);
@@ -38272,9 +37911,9 @@ var SessionManager = class {
38272
37911
  }
38273
37912
  const indexed = this.index.get(sessionId);
38274
37913
  if (indexed) {
38275
- console.log(`[session:find] Index hit: ${sessionId} \u2192 ${indexed.file} (exists=${fs44.existsSync(indexed.file)})`);
38276
- if (fs44.existsSync(indexed.file)) {
38277
- return this.findAllSessionFiles(path43.basename(indexed.file, ".jsonl"));
37914
+ console.log(`[session:find] Index hit: ${sessionId} \u2192 ${indexed.file} (exists=${fs42.existsSync(indexed.file)})`);
37915
+ if (fs42.existsSync(indexed.file)) {
37916
+ return this.findAllSessionFiles(path42.basename(indexed.file, ".jsonl"));
38278
37917
  }
38279
37918
  } else {
38280
37919
  console.log(`[session:find] Index miss: ${sessionId} (index has ${this.index.size} entries)`);
@@ -38303,8 +37942,8 @@ var SessionManager = class {
38303
37942
  return [];
38304
37943
  }
38305
37944
  files.sort((a, b) => {
38306
- const ta = fs44.statSync(a).mtimeMs;
38307
- const tb = fs44.statSync(b).mtimeMs;
37945
+ const ta = fs42.statSync(a).mtimeMs;
37946
+ const tb = fs42.statSync(b).mtimeMs;
38308
37947
  return ta - tb;
38309
37948
  });
38310
37949
  const latestFile = files[files.length - 1];
@@ -38312,7 +37951,7 @@ var SessionManager = class {
38312
37951
  if (!currentEntry || currentEntry.file !== latestFile) {
38313
37952
  this.index.set(sessionId, { file: latestFile, updatedAt: Date.now() });
38314
37953
  this.saveIndex();
38315
- console.log(`[session:restore] Synced index: ${sessionId} \u2192 ${path43.basename(latestFile)}`);
37954
+ console.log(`[session:restore] Synced index: ${sessionId} \u2192 ${path42.basename(latestFile)}`);
38316
37955
  }
38317
37956
  const allMessages = [];
38318
37957
  for (const file2 of files) {
@@ -38351,7 +37990,7 @@ var SessionManager = class {
38351
37990
  if (isAttachmentMessage(m) && m.attachment.type === "relevant_memories") {
38352
37991
  for (const mem of m.attachment.memories) {
38353
37992
  rawCount++;
38354
- restoredPaths.add(path43.resolve(mem.path));
37993
+ restoredPaths.add(path42.resolve(mem.path));
38355
37994
  }
38356
37995
  }
38357
37996
  }
@@ -38383,15 +38022,15 @@ var SessionManager = class {
38383
38022
  */
38384
38023
  rotateMainSession() {
38385
38024
  const entry = this.index.get("main");
38386
- if (!entry || !fs44.existsSync(entry.file)) return;
38387
- const oldFileId = path43.basename(entry.file, ".jsonl");
38388
- const stat4 = fs44.statSync(entry.file);
38025
+ if (!entry || !fs42.existsSync(entry.file)) return;
38026
+ const oldFileId = path42.basename(entry.file, ".jsonl");
38027
+ const stat4 = fs42.statSync(entry.file);
38389
38028
  if (stat4.size < 1024) return;
38390
38029
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
38391
38030
  const archivedPath = `${entry.file}.archived.${ts}`;
38392
- fs44.renameSync(entry.file, archivedPath);
38031
+ fs42.renameSync(entry.file, archivedPath);
38393
38032
  const sizeMB = (stat4.size / 1024 / 1024).toFixed(1);
38394
- console.log(`[session] Archived: ${path43.basename(entry.file)} \u2192 ${path43.basename(archivedPath)} (${sizeMB}MB)`);
38033
+ console.log(`[session] Archived: ${path42.basename(entry.file)} \u2192 ${path42.basename(archivedPath)} (${sizeMB}MB)`);
38395
38034
  const newWriter = new SessionWriter(this.sessionsDir);
38396
38035
  newWriter.open(this.config.workspace, "main");
38397
38036
  newWriter.writeModelChange(this.config.providerId, this.config.model);
@@ -38400,7 +38039,7 @@ var SessionManager = class {
38400
38039
  this.index.set("main", { file: newPath, updatedAt: Date.now() });
38401
38040
  this.saveIndex();
38402
38041
  this.purgeOldArchives(oldFileId, 3);
38403
- console.log(`[session] Rotated main: ${oldFileId}.jsonl \u2192 ${path43.basename(newPath)}`);
38042
+ console.log(`[session] Rotated main: ${oldFileId}.jsonl \u2192 ${path42.basename(newPath)}`);
38404
38043
  }
38405
38044
  // === 迁移 ===
38406
38045
  /** 迁移旧索引:session-index.json 里有 "discord:xxx" 格式的 key 时,生成 UUID 替换 */
@@ -39292,12 +38931,12 @@ Examples:
39292
38931
  // src/tools/msg-husband.ts
39293
38932
  init_registry();
39294
38933
  init_live();
39295
- import fs45 from "node:fs";
39296
- import path44 from "node:path";
38934
+ import fs43 from "node:fs";
38935
+ import path43 from "node:path";
39297
38936
  function getHusbandFeishuId(workspace) {
39298
- const contactsPath = path44.join(workspace, "prompts", "contacts.md");
38937
+ const contactsPath = path43.join(workspace, "prompts", "contacts.md");
39299
38938
  try {
39300
- const text = fs45.readFileSync(contactsPath, "utf-8");
38939
+ const text = fs43.readFileSync(contactsPath, "utf-8");
39301
38940
  const m = text.match(/\|\s*翀哥\s*\|\s*(ou_[a-f0-9]+)\s*\|/);
39302
38941
  return m ? m[1] : null;
39303
38942
  } catch {
@@ -39439,8 +39078,8 @@ Examples:
39439
39078
  if (!to && !resolvedChannelId) {
39440
39079
  return { content: "to \u548C channel_id \u4E0D\u80FD\u540C\u65F6\u4E3A\u7A7A\uFF0C\u4E14\u6CA1\u6709\u53EF\u7528\u7684\u6765\u6E90\u9891\u9053", isError: true };
39441
39080
  }
39442
- const fs56 = await import("node:fs");
39443
- if (!fs56.existsSync(filePath)) {
39081
+ const fs58 = await import("node:fs");
39082
+ if (!fs58.existsSync(filePath)) {
39444
39083
  return { content: `\u53D1\u9001\u5931\u8D25: \u6587\u4EF6\u4E0D\u5B58\u5728 ${filePath}`, isError: true };
39445
39084
  }
39446
39085
  const toIds = to ? to.split(",").map((s) => s.trim()).filter(Boolean) : [];
@@ -39468,7 +39107,7 @@ Examples:
39468
39107
  md: "text/markdown"
39469
39108
  };
39470
39109
  const mimeType = mimeTypeMap[ext] || "application/octet-stream";
39471
- const stat4 = fs56.statSync(filePath);
39110
+ const stat4 = fs58.statSync(filePath);
39472
39111
  const sizeMB = stat4.size / 1024 / 1024;
39473
39112
  if (sizeMB > 25) {
39474
39113
  return { content: `\u53D1\u9001\u5931\u8D25: \u6587\u4EF6 ${sizeMB.toFixed(1)}MB \u8D85\u8FC7 Discord 25MB \u9650\u5236`, isError: true };
@@ -39498,8 +39137,8 @@ Examples:
39498
39137
  init_live();
39499
39138
  init_registry();
39500
39139
  init_well_formed();
39501
- import * as fs46 from "node:fs";
39502
- import * as path45 from "node:path";
39140
+ import * as fs44 from "node:fs";
39141
+ import * as path44 from "node:path";
39503
39142
  var MIME_MAP = {
39504
39143
  ".jpg": "jpeg",
39505
39144
  ".jpeg": "jpeg",
@@ -39511,9 +39150,9 @@ var MIME_MAP = {
39511
39150
  var VIDEO_EXT = /\.(mp4|mov)$/i;
39512
39151
  var VIDEO_MAX_BYTES = 40 * 1024 * 1024;
39513
39152
  function resolveLatestImage(specifiedPath, mediaDir) {
39514
- if (specifiedPath && fs46.existsSync(specifiedPath)) return specifiedPath;
39515
- if (!fs46.existsSync(mediaDir)) return null;
39516
- const files = fs46.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp|mp4|mov)$/i.test(f)).map((f) => ({ name: f, p: path45.join(mediaDir, f), mtime: fs46.statSync(path45.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
39153
+ if (specifiedPath && fs44.existsSync(specifiedPath)) return specifiedPath;
39154
+ if (!fs44.existsSync(mediaDir)) return null;
39155
+ const files = fs44.readdirSync(mediaDir).filter((f) => /\.(jpg|jpeg|png|webp|gif|bmp|mp4|mov)$/i.test(f)).map((f) => ({ name: f, p: path44.join(mediaDir, f), mtime: fs44.statSync(path44.join(mediaDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
39517
39156
  return files[0]?.p || null;
39518
39157
  }
39519
39158
  registry.register({
@@ -39544,21 +39183,21 @@ registry.register({
39544
39183
  if (!provider?.streamChat) {
39545
39184
  return { content: "Error: provider \u4E0D\u53EF\u7528\u3002", isError: true };
39546
39185
  }
39547
- const mediaDir = path45.join(ctx.stateDir, "media", "inbound");
39186
+ const mediaDir = path44.join(ctx.stateDir, "media", "inbound");
39548
39187
  const imagePath = resolveLatestImage(args2.image_path, mediaDir);
39549
39188
  if (!imagePath) {
39550
39189
  return { content: "Error: no image found. Provide image_path or ensure media/inbound has images.", isError: true };
39551
39190
  }
39552
39191
  const rawPrompt = args2.prompt?.trim() || "\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247\u7684\u5185\u5BB9";
39553
- const ext = path45.extname(imagePath).toLowerCase();
39192
+ const ext = path44.extname(imagePath).toLowerCase();
39554
39193
  if (VIDEO_EXT.test(imagePath)) {
39555
- const stat4 = fs46.statSync(imagePath);
39194
+ const stat4 = fs44.statSync(imagePath);
39556
39195
  if (stat4.size > VIDEO_MAX_BYTES) {
39557
39196
  return { content: `Error: video too large (${(stat4.size / 1e6).toFixed(1)}MB > 40MB). Compress or trim first.`, isError: true };
39558
39197
  }
39559
39198
  const explicit = liveConfig.get("tools.my_eyes.videoBaseUrl");
39560
39199
  const baseUrl = explicit || providerCfg.baseUrl.replace(/\/anthropic\/?$/, "/v1").replace(/\/$/, "");
39561
- const vidB64 = fs46.readFileSync(imagePath).toString("base64");
39200
+ const vidB64 = fs44.readFileSync(imagePath).toString("base64");
39562
39201
  try {
39563
39202
  const res = await fetch(`${baseUrl}/chat/completions`, {
39564
39203
  method: "POST",
@@ -39588,7 +39227,7 @@ registry.register({
39588
39227
  }
39589
39228
  }
39590
39229
  const mime = MIME_MAP[ext] || "jpeg";
39591
- const imgB64 = fs46.readFileSync(imagePath).toString("base64");
39230
+ const imgB64 = fs44.readFileSync(imagePath).toString("base64");
39592
39231
  const userMsg = {
39593
39232
  role: "user",
39594
39233
  content: [
@@ -39620,12 +39259,675 @@ registry.register({
39620
39259
  }
39621
39260
  });
39622
39261
 
39623
- // src/tools/my-selfie.ts
39262
+ // src/tools/my-voice.ts
39624
39263
  init_live();
39264
+
39265
+ // src/config/tts-providers.ts
39266
+ var TTS_PROVIDERS = [
39267
+ // relay:走 7yi 中转站的 /v1/audio/speech。
39268
+ // 后三个 billing* 键也在这支读——选池要用;漏了的话托盘切 provider 会把它们清掉。
39269
+ //
39270
+ // ⚠️ `voiceTier` 与 `fallback` 都在这一支读,且**都由引擎侧读、托盘没有输入框**
39271
+ // (见 config-editors.ts 的 VOICE_FORM_KEYS —— 托盘只建它有框的那几格)。
39272
+ // 它们出现在这里**不是为了给用户编辑**,而是为了让托盘的保存路径**别把它们删掉**:
39273
+ // mergeMyVoice 的 `keep` 来自这份清单(它决定"这一版 provider 会读哪些键"),
39274
+ // 而不在 keep 里的键会被整份替换下去 —— 漏了 `voiceTier` 就是"托盘一保存,
39275
+ // 计价档位静默消失"。这是本清单**唯一**的第二个用途,别把这两个键当成多余的。
39276
+ // · voiceTier → providers.ts:39 算 tier(route 用它选池、usage 用它记账;
39277
+ // spec §4.4 留的克隆音色计费钩子,今天没有生产者)
39278
+ // · fallback → my-voice.ts 主路径的回退链(spec §4.1,默认 ['edge'])
39279
+ { id: "relay", requiredKeys: [
39280
+ "pythonPath",
39281
+ "relayProviderId",
39282
+ "relayModel",
39283
+ "voice",
39284
+ "billingProviderId",
39285
+ "billingModel",
39286
+ "billingMode",
39287
+ "voiceTier",
39288
+ "fallback"
39289
+ ] },
39290
+ { id: "cosyvoice", requiredKeys: ["pythonPath", "apiKey", "model", "voice", "workspaceId", "instruction"] },
39291
+ // gptsovits:四个 gptsovits* 键是 Task 9 从硬编码改过来的(原来是 GPTSOVITS_API / REF_WAV /
39292
+ // REF_TEXT 写死在 my-voice.ts:303-306,那个 REF_WAV 是翀哥 Linux 机器上的路径,所有用户都会去找它)
39293
+ { id: "gptsovits", requiredKeys: [
39294
+ "pythonPath",
39295
+ "gptsovitsUrl",
39296
+ "gptsovitsRefWav",
39297
+ "gptsovitsRefText",
39298
+ "gptsovitsRefLang"
39299
+ ] },
39300
+ { id: "qwen3-3060", requiredKeys: ["pythonPath", "url", "voice", "lang"] },
39301
+ { id: "edge", requiredKeys: ["pythonPath"] }
39302
+ ];
39303
+
39304
+ // src/tools/my-voice.ts
39625
39305
  init_registry();
39626
- init_well_formed();
39306
+
39307
+ // src/tools/opus-encoder.ts
39308
+ import * as fs45 from "node:fs";
39309
+ var FRAME_MS = 20;
39310
+ var PRESKIP_48K = 312;
39311
+ var OPUS_RATE = 48e3;
39312
+ var NATIVE_RATES = [8e3, 12e3, 16e3, 24e3, 48e3];
39313
+ var BITRATE = 32e3;
39314
+ var CRC_TABLE = (() => {
39315
+ const t = new Uint32Array(256);
39316
+ for (let i = 0; i < 256; i++) {
39317
+ let r = i << 24;
39318
+ for (let k = 0; k < 8; k++) r = r & 2147483648 ? r << 1 ^ 79764919 : r << 1;
39319
+ t[i] = r >>> 0;
39320
+ }
39321
+ return t;
39322
+ })();
39323
+ function oggCrc32(buf) {
39324
+ let crc = 0;
39325
+ for (let i = 0; i < buf.length; i++) {
39326
+ crc = (crc << 8 ^ CRC_TABLE[(crc >>> 24 ^ buf[i]) & 255]) >>> 0;
39327
+ }
39328
+ return crc >>> 0;
39329
+ }
39330
+ var OggWriter = class {
39331
+ serial;
39332
+ seq = 0;
39333
+ pages = [];
39334
+ constructor() {
39335
+ this.serial = Math.random() * 4294967296 >>> 0;
39336
+ }
39337
+ /** 组一页。packets 为完整数据包;granule = 该页最后一包的 48k 域样本位置。 */
39338
+ page(packets, granule, bos = false, eos = false) {
39339
+ const lacing = [];
39340
+ for (const p of packets) {
39341
+ let rem = p.length;
39342
+ while (rem >= 255) {
39343
+ lacing.push(255);
39344
+ rem -= 255;
39345
+ }
39346
+ lacing.push(rem);
39347
+ }
39348
+ const payloadLen = packets.reduce((n, p) => n + p.length, 0);
39349
+ const page = Buffer.alloc(27 + lacing.length + payloadLen);
39350
+ page.write("OggS", 0, "ascii");
39351
+ page[4] = 0;
39352
+ page[5] = (bos ? 2 : 0) | (eos ? 1 : 0);
39353
+ page.writeBigUInt64LE(BigInt(granule), 6);
39354
+ page.writeUInt32LE(this.serial, 14);
39355
+ page.writeUInt32LE(this.seq++, 18);
39356
+ page.writeUInt32LE(0, 22);
39357
+ page[26] = lacing.length;
39358
+ lacing.forEach((v, i) => {
39359
+ page[27 + i] = v;
39360
+ });
39361
+ let off = 27 + lacing.length;
39362
+ for (const p of packets) {
39363
+ p.copy(page, off);
39364
+ off += p.length;
39365
+ }
39366
+ page.writeUInt32LE(oggCrc32(page), 22);
39367
+ this.pages.push(page);
39368
+ }
39369
+ finish() {
39370
+ return Buffer.concat(this.pages);
39371
+ }
39372
+ };
39373
+ function opusHeadPacket(channels, inputRate) {
39374
+ const b = Buffer.alloc(19);
39375
+ b.write("OpusHead", 0, "ascii");
39376
+ b[8] = 1;
39377
+ b[9] = channels;
39378
+ b.writeUInt16LE(PRESKIP_48K, 10);
39379
+ b.writeUInt32LE(inputRate, 12);
39380
+ b.writeInt16LE(0, 16);
39381
+ b[18] = 0;
39382
+ return b;
39383
+ }
39384
+ function opusTagsPacket() {
39385
+ const vendor = Buffer.from("engine7-native-opus", "utf8");
39386
+ const b = Buffer.alloc(8 + 4 + vendor.length + 4);
39387
+ b.write("OpusTags", 0, "ascii");
39388
+ b.writeUInt32LE(vendor.length, 8);
39389
+ vendor.copy(b, 12);
39390
+ b.writeUInt32LE(0, 12 + vendor.length);
39391
+ return b;
39392
+ }
39393
+ function parseWav(buf) {
39394
+ if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
39395
+ throw new Error("not a RIFF/WAVE file");
39396
+ }
39397
+ let off = 12;
39398
+ let fmt = null;
39399
+ let pcm = null;
39400
+ while (off + 8 <= buf.length) {
39401
+ const id = buf.toString("ascii", off, off + 4);
39402
+ const size = buf.readUInt32LE(off + 4);
39403
+ const body = off + 8;
39404
+ if (id === "fmt " && body + 16 <= buf.length) {
39405
+ fmt = {
39406
+ format: buf.readUInt16LE(body),
39407
+ ch: buf.readUInt16LE(body + 2),
39408
+ rate: buf.readUInt32LE(body + 4),
39409
+ bits: buf.readUInt16LE(body + 14)
39410
+ };
39411
+ } else if (id === "data") {
39412
+ pcm = buf.subarray(body, body + Math.min(size, buf.length - body));
39413
+ }
39414
+ off = body + size + (size & 1);
39415
+ }
39416
+ if (!fmt || !pcm) throw new Error("missing fmt/data chunk");
39417
+ if (fmt.format !== 1) throw new Error(`unsupported wav format ${fmt.format} (need PCM)`);
39418
+ if (fmt.bits !== 16) throw new Error(`unsupported ${fmt.bits}-bit (need s16le)`);
39419
+ return { sampleRate: fmt.rate, channels: fmt.ch, bits: fmt.bits, pcm };
39420
+ }
39421
+ async function wavToOggOpusNative(wavPath) {
39422
+ const mod = await import("@discordjs/opus");
39423
+ const OpusEncoder = mod.OpusEncoder ?? mod.default?.OpusEncoder;
39424
+ if (!OpusEncoder) throw new Error("@discordjs/opus export shape unexpected");
39425
+ const wav = parseWav(fs45.readFileSync(wavPath));
39426
+ if (wav.channels !== 1) throw new Error(`unsupported ${wav.channels}ch (need mono)`);
39427
+ if (!NATIVE_RATES.includes(wav.sampleRate)) throw new Error(`unsupported rate ${wav.sampleRate}`);
39428
+ if (wav.pcm.length === 0) throw new Error("empty pcm data");
39429
+ const encoder = new OpusEncoder(wav.sampleRate, wav.channels);
39430
+ encoder.setBitrate(BITRATE);
39431
+ const samplesPerFrame = Math.round(wav.sampleRate * FRAME_MS / 1e3);
39432
+ const bytesPerFrame = samplesPerFrame * 2;
39433
+ const frames = Math.ceil(wav.pcm.length / bytesPerFrame);
39434
+ const padded = Buffer.alloc(frames * bytesPerFrame);
39435
+ wav.pcm.copy(padded);
39436
+ const ogg = new OggWriter();
39437
+ ogg.page([opusHeadPacket(wav.channels, wav.sampleRate)], 0, true);
39438
+ ogg.page([opusTagsPacket()], 0);
39439
+ let cur = [];
39440
+ let curSegs = 0;
39441
+ let curBytes = 0;
39442
+ let pageGranule = 0;
39443
+ let pos48k = PRESKIP_48K;
39444
+ for (let f = 0; f < frames; f++) {
39445
+ const packet = encoder.encode(padded.subarray(f * bytesPerFrame, (f + 1) * bytesPerFrame));
39446
+ const pSegs = Math.floor((packet.length - 1) / 255) + 1;
39447
+ if (curSegs + pSegs > 255 || curBytes + packet.length > 6e4) {
39448
+ ogg.page(cur, pageGranule);
39449
+ cur = [];
39450
+ curSegs = 0;
39451
+ curBytes = 0;
39452
+ }
39453
+ cur.push(packet);
39454
+ curSegs += pSegs;
39455
+ curBytes += packet.length;
39456
+ pos48k += samplesPerFrame * (OPUS_RATE / wav.sampleRate);
39457
+ pageGranule = pos48k;
39458
+ }
39459
+ ogg.page(cur, pageGranule, false, true);
39460
+ const oggPath = wavPath.replace(/\.wav$/, ".ogg");
39461
+ fs45.writeFileSync(oggPath, ogg.finish());
39462
+ return oggPath;
39463
+ }
39464
+
39465
+ // src/tools/tts/pick.ts
39466
+ var KNOWN = new Set(TTS_PROVIDERS.map((p) => p.id));
39467
+ function pickProvider(requested, cfgProvider) {
39468
+ const req = typeof requested === "string" ? requested.trim() : "";
39469
+ const cfg = typeof cfgProvider === "string" ? cfgProvider.trim() : "";
39470
+ const name = cfg || req;
39471
+ if (KNOWN.has(name)) return { id: name };
39472
+ return { id: "edge" };
39473
+ }
39474
+ function fallbackChainOf(raw) {
39475
+ const list2 = Array.isArray(raw) ? raw : ["edge"];
39476
+ const out = [];
39477
+ for (const item of list2) {
39478
+ if (typeof item !== "string") continue;
39479
+ const id = item.trim();
39480
+ if (!KNOWN.has(id) || out.includes(id)) continue;
39481
+ out.push(id);
39482
+ }
39483
+ return out;
39484
+ }
39485
+
39486
+ // src/tools/tts/providers.ts
39487
+ init_live();
39488
+
39489
+ // src/tools/tts/relay.ts
39490
+ import * as fs46 from "node:fs";
39491
+ import * as os6 from "node:os";
39492
+ import * as path45 from "node:path";
39493
+ var VOICE_DIR2 = path45.join(os6.tmpdir(), "engine-voice");
39494
+ var fetchFn = fetch;
39495
+ async function synthRelay(text, cfg) {
39496
+ const res = await fetchFn(`${cfg.baseUrl}/audio/speech`, {
39497
+ method: "POST",
39498
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
39499
+ body: JSON.stringify({
39500
+ model: cfg.model,
39501
+ input: text,
39502
+ voice: cfg.voice,
39503
+ response_format: "mp3"
39504
+ })
39505
+ });
39506
+ if (!res.ok) {
39507
+ const body = await res.text().catch(() => "");
39508
+ throw new Error(`relay tts http ${res.status}: ${body.slice(0, 300)}`);
39509
+ }
39510
+ const buf = Buffer.from(await res.arrayBuffer());
39511
+ if (buf.length < 20) throw new Error(`relay tts: \u54CD\u5E94\u4F53\u53EA\u6709 ${buf.length} \u5B57\u8282\uFF0C\u4E0D\u662F\u6709\u6548\u97F3\u9891`);
39512
+ fs46.mkdirSync(VOICE_DIR2, { recursive: true });
39513
+ const out = path45.join(VOICE_DIR2, `tts_relay_${Date.now()}.mp3`);
39514
+ fs46.writeFileSync(out, buf);
39515
+ return {
39516
+ path: out,
39517
+ format: "mp3",
39518
+ unit: { kind: "chars", amount: [...text].length },
39519
+ model: cfg.model,
39520
+ voice: cfg.voice
39521
+ };
39522
+ }
39523
+
39524
+ // src/tools/tts/route.ts
39525
+ init_loader();
39526
+ function providerOf(cfg, id) {
39527
+ const p = cfg?.models?.providers?.[id];
39528
+ const baseUrl = String(p?.baseUrl || "").replace(/\/+$/, "");
39529
+ const apiKey = String(p?.apiKey || "");
39530
+ if (!baseUrl || !apiKey) return null;
39531
+ if (isUnresolvedEnvRef(apiKey)) return null;
39532
+ return { baseUrl, apiKey };
39533
+ }
39534
+ function resolveTtsRoute(cfg, tier) {
39535
+ const mv = cfg?.tools?.my_voice || {};
39536
+ const mode = String(mv.billingMode || "off");
39537
+ const billingId = String(mv.billingProviderId || "").trim();
39538
+ const voice = String(mv.voice || "");
39539
+ const freePool = (warning) => {
39540
+ const free = providerOf(cfg, String(mv.relayProviderId || "7yi"));
39541
+ if (!free) {
39542
+ throw new TtsConfigError(
39543
+ `\u514D\u8D39\u6C60\u672A\u914D\u7F6E\uFF1Amodels.providers.${mv.relayProviderId || "7yi"} \u7F3A baseUrl \u6216 apiKey\u3002\u514D\u8D39\u6863\u88C5\u673A\u5E94\u5DF2\u81EA\u52A8\u914D\u597D\uFF0C\u53EF\u8DD1 \`engine7 reconfig llm --free\` \u8865\u3002`
39544
+ );
39545
+ }
39546
+ return {
39547
+ pool: "free",
39548
+ ...free,
39549
+ model: String(mv.relayModel || "engine7-tts"),
39550
+ voice,
39551
+ ...warning ? { warning } : {}
39552
+ };
39553
+ };
39554
+ const knownMode = mode === "off" || mode === "custom-only" || mode === "all";
39555
+ const modeWarning = knownMode ? "" : `billingMode=${mode} \u4E0D\u5728 off / custom-only / all \u91CC\u2014\u2014\u8FD9\u4E2A\u540D\u5B57\u6CA1\u4EBA\u8BA4\u8BC6\uFF0C\u6309\u300C\u672A\u542F\u7528\u300D\u5904\u7406\uFF08\u8D70\u514D\u8D39\u6C60\uFF09\u3002\u591A\u534A\u662F\u624B\u6253\u9519\u4E86\uFF08\u6BD4\u5982 customOnly \u5C11\u4E86\u8FDE\u5B57\u7B26\uFF09\u3002`;
39556
+ const wantBilling = knownMode && (mode === "all" || mode === "custom-only" && tier === "custom");
39557
+ if (!billingId) {
39558
+ return freePool(modeWarning || (wantBilling ? `billingMode=${mode} \u58F0\u79F0\u8981\u8D70\u8BA1\u8D39\u6C60\uFF0C\u4F46 billingProviderId \u662F\u7A7A\u7684\u2014\u2014\u4ECD\u8D70\u514D\u8D39\u6C60\u3002` : ""));
39559
+ }
39560
+ if (!wantBilling) return freePool(modeWarning);
39561
+ const bill = providerOf(cfg, billingId);
39562
+ if (!bill) {
39563
+ throw new TtsConfigError(
39564
+ `\u8BA1\u8D39\u6C60\u5DF2\u542F\u7528\u4F46\u4E0D\u53EF\u7528\uFF1Amodels.providers.${billingId} \u7F3A baseUrl \u6216 apiKey\u3002\u8FD9\u6761**\u4E0D\u4F1A**\u81EA\u52A8\u56DE\u843D\u514D\u8D39\u6C60\u2014\u2014\u4ED8\u8D39\u529F\u80FD\u9759\u9ED8\u767D\u9001\u6BD4\u62A5\u9519\u5371\u9669\u5F97\u591A\u3002\u8BF7\u8865\u914D\u7F6E\u6216\u628A billingMode \u6539\u56DE off\u3002`
39565
+ );
39566
+ }
39567
+ return { pool: "billing", ...bill, model: String(mv.billingModel || mv.relayModel || "engine7-tts"), voice };
39568
+ }
39569
+
39570
+ // src/tools/tts/providers.ts
39571
+ var TTS_IMPL = {
39572
+ relay: {
39573
+ supportsStreaming: false,
39574
+ synthesize: async (text) => {
39575
+ const all = liveConfig.all();
39576
+ const mv = all?.tools?.my_voice || {};
39577
+ const tier = mv.voiceTier === "custom" ? "custom" : "official";
39578
+ const r = resolveTtsRoute(all, tier);
39579
+ if (r.warning) console.warn(`[my-voice] ${r.warning}`);
39580
+ if (!r.voice) {
39581
+ throw new TtsConfigError(
39582
+ "relay TTS \u7F3A\u97F3\u8272\uFF1Atools.my_voice.voice \u662F\u7A7A\u7684\uFF0C\u4E0A\u6E38\u4F1A\u62D2\u3002\u514D\u8D39\u6863\u88C5\u673A\u5E94\u5DF2\u7531 applyFreeTier \u586B\u597D\u9ED8\u8BA4\u97F3\u8272\u3002"
39583
+ );
39584
+ }
39585
+ try {
39586
+ const res = await synthRelay(text, {
39587
+ baseUrl: r.baseUrl,
39588
+ apiKey: r.apiKey,
39589
+ model: r.model,
39590
+ voice: r.voice
39591
+ });
39592
+ noteTtsUsage({
39593
+ provider: "relay",
39594
+ model: r.model,
39595
+ voice: r.voice,
39596
+ tier,
39597
+ pool: r.pool,
39598
+ unit: res.unit,
39599
+ ok: true,
39600
+ at: Date.now()
39601
+ });
39602
+ return res;
39603
+ } catch (e) {
39604
+ noteTtsUsage({
39605
+ provider: "relay",
39606
+ model: r.model,
39607
+ voice: r.voice,
39608
+ tier,
39609
+ pool: r.pool,
39610
+ unit: { kind: "chars", amount: 0 },
39611
+ ok: false,
39612
+ at: Date.now()
39613
+ });
39614
+ throw e;
39615
+ }
39616
+ }
39617
+ },
39618
+ cosyvoice: { supportsStreaming: true, synthesize: synthCosyvoice },
39619
+ gptsovits: { supportsStreaming: false, synthesize: synthGptsovits },
39620
+ "qwen3-3060": { supportsStreaming: true, synthesize: synthQwen3 },
39621
+ edge: { supportsStreaming: false, synthesize: synthEdge }
39622
+ };
39623
+ var overrides = {};
39624
+ function synthesize(id, text, sink) {
39625
+ const impl = overrides[id] ?? TTS_IMPL[id];
39626
+ if (!impl) throw new Error(`TTS provider \u6CA1\u6709\u5B9E\u73B0\uFF1A${id}`);
39627
+ return impl.synthesize(text, sink);
39628
+ }
39629
+
39630
+ // src/tools/my-voice.ts
39631
+ import { execFile as execFile3 } from "node:child_process";
39632
+ import { promisify as promisify3 } from "node:util";
39627
39633
  import * as fs47 from "node:fs";
39628
39634
  import * as path46 from "node:path";
39635
+ var execFileAsync3 = promisify3(execFile3);
39636
+ async function toWav24kWithDuration(audioPath) {
39637
+ let wavPath = audioPath;
39638
+ if (!audioPath.endsWith(".wav")) {
39639
+ wavPath = audioPath.replace(/\.[^.]+$/, ".wav");
39640
+ await execFileAsync3("ffmpeg", [
39641
+ "-y",
39642
+ "-i",
39643
+ audioPath,
39644
+ "-ar",
39645
+ "24000",
39646
+ "-ac",
39647
+ "1",
39648
+ "-c:a",
39649
+ "pcm_s16le",
39650
+ wavPath
39651
+ ], { timeout: 3e4 });
39652
+ if (!fs47.existsSync(wavPath) || fs47.statSync(wavPath).size < 44) {
39653
+ throw new Error("toWav24k produced empty output");
39654
+ }
39655
+ try {
39656
+ fs47.unlinkSync(audioPath);
39657
+ } catch {
39658
+ }
39659
+ }
39660
+ const durationSec = wavDurationSec(fs47.readFileSync(wavPath));
39661
+ return { wavPath, durationSec };
39662
+ }
39663
+ async function compressWav(wavPath) {
39664
+ const m4aPath = wavPath.replace(/\.wav$/, ".m4a");
39665
+ try {
39666
+ await execFileAsync3("ffmpeg", [
39667
+ "-y",
39668
+ "-i",
39669
+ wavPath,
39670
+ "-c:a",
39671
+ "aac",
39672
+ "-b:a",
39673
+ "64k",
39674
+ "-movflags",
39675
+ "+faststart",
39676
+ m4aPath
39677
+ ], { timeout: 3e4 });
39678
+ fs47.unlinkSync(wavPath);
39679
+ return m4aPath;
39680
+ } catch {
39681
+ return wavPath;
39682
+ }
39683
+ }
39684
+ async function wavToOggOpus(wavPath) {
39685
+ let oggPath;
39686
+ try {
39687
+ oggPath = await wavToOggOpusNative(wavPath);
39688
+ } catch (e) {
39689
+ if (globalThis.__ffmpegMissing) throw e;
39690
+ console.warn(`[my-voice] native opus encode failed (${e.message}), trying ffmpeg`);
39691
+ oggPath = wavPath.replace(/\.wav$/, ".ogg");
39692
+ await execFileAsync3("ffmpeg", [
39693
+ "-y",
39694
+ "-i",
39695
+ wavPath,
39696
+ "-c:a",
39697
+ "libopus",
39698
+ "-b:a",
39699
+ "32k",
39700
+ "-ar",
39701
+ "24000",
39702
+ oggPath
39703
+ ], { timeout: 3e4 });
39704
+ }
39705
+ if (!fs47.existsSync(oggPath) || fs47.statSync(oggPath).size < 20) {
39706
+ throw new Error("ogg/opus encode produced empty output");
39707
+ }
39708
+ fs47.unlinkSync(wavPath);
39709
+ return oggPath;
39710
+ }
39711
+ async function sendVoiceToMulticaIssue(text, issueId, opts) {
39712
+ const vc = liveConfig.get("tools.my_voice");
39713
+ let audioPath = "";
39714
+ let actualEngine;
39715
+ const picked = pickProvider(opts.engine, vc?.provider);
39716
+ actualEngine = picked.id;
39717
+ try {
39718
+ const r = await synthesize(picked.id, text, {});
39719
+ audioPath = r.path;
39720
+ } catch (e) {
39721
+ return { content: `\u8BED\u97F3\u751F\u6210\u5931\u8D25\uFF08headless\u2192multaca\uFF09\uFF1A${e.message}\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002`, isError: true };
39722
+ }
39723
+ if (!audioPath || !fs47.existsSync(audioPath)) {
39724
+ return { content: "\u8BED\u97F3\u751F\u6210\u5931\u8D25\uFF08headless\u2192multaca\uFF09\uFF1ATTS \u672A\u4EA7\u51FA\u6587\u4EF6\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002", isError: true };
39725
+ }
39726
+ if (audioPath.endsWith(".wav")) {
39727
+ try {
39728
+ audioPath = await wavToOggOpus(audioPath);
39729
+ } catch (e) {
39730
+ console.warn(`[my-voice:multaca] opus encode failed (${e.message}), attaching wav`);
39731
+ }
39732
+ }
39733
+ const ext = path46.extname(audioPath).toLowerCase();
39734
+ const sizeKB = fs47.statSync(audioPath).size / 1024;
39735
+ const voiceFile = `voice_${Date.now()}${ext}`;
39736
+ const cwdCopy = path46.join(process.cwd(), voiceFile);
39737
+ try {
39738
+ fs47.copyFileSync(audioPath, cwdCopy);
39739
+ } catch (e) {
39740
+ try {
39741
+ fs47.unlinkSync(audioPath);
39742
+ } catch {
39743
+ }
39744
+ return { content: `\u8BED\u97F3\u6302\u8F7D\u5931\u8D25\uFF08\u62F7\u8D1D\u5230\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1A${e.message}`, isError: true };
39745
+ }
39746
+ try {
39747
+ fs47.unlinkSync(audioPath);
39748
+ } catch {
39749
+ }
39750
+ const content = opts.caption || `\u{1F399}\uFE0F \u8BED\u97F3\u6D88\u606F\uFF08${text.length} \u5B57\uFF09`;
39751
+ try {
39752
+ const { execFile: execFile5 } = await import("node:child_process");
39753
+ const { promisify: promisify5 } = await import("node:util");
39754
+ const execFileA = promisify5(execFile5);
39755
+ const { stdout } = await execFileA("multica", [
39756
+ "issue",
39757
+ "comment",
39758
+ "add",
39759
+ issueId,
39760
+ "--content",
39761
+ content,
39762
+ "--attachment",
39763
+ voiceFile,
39764
+ "--output",
39765
+ "json"
39766
+ ], { timeout: 6e4 });
39767
+ let ok = false;
39768
+ try {
39769
+ const parsed = JSON.parse(stdout);
39770
+ ok = !!(parsed?.comment_id || parsed?.id || parsed?.attachments && parsed.attachments.length > 0);
39771
+ } catch {
39772
+ ok = stdout.includes("Comment added");
39773
+ }
39774
+ if (!ok) throw new Error(`unexpected CLI output: ${String(stdout).slice(0, 200)}`);
39775
+ try {
39776
+ fs47.unlinkSync(cwdCopy);
39777
+ } catch {
39778
+ }
39779
+ return { content: `Voice sent to multaca issue ${issueId}! (${actualEngine}, ${sizeKB.toFixed(0)}KB, \u9644\u4EF6\u5DF2\u6302\u5230\u8BC4\u8BBA\u2014\u2014\u5728 multaca \u5BA2\u6237\u7AEF\u64AD\u653E)` };
39780
+ } catch (e) {
39781
+ try {
39782
+ fs47.unlinkSync(cwdCopy);
39783
+ } catch {
39784
+ }
39785
+ return { content: `\u8BED\u97F3\u6302\u5230 multaca issue \u5931\u8D25\uFF1A${e.message}\u3002\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002`, isError: true };
39786
+ }
39787
+ }
39788
+ registry.register({
39789
+ name: "my_voice",
39790
+ description: "Generate and send a voice message. Use for: \u53D1\u8BED\u97F3/\u8BF4\u53E5\u8BDD/\u5F55\u4E00\u6BB5/\u53D1\u58F0\u97F3/\u60F3\u542C\u4F60\u7684\u58F0\u97F3/\u8BED\u97F3\u6D88\u606F/\u8BF4\u7ED9\u6211\u542C. TTS provider comes from config (tools.my_voice.provider); relay = 7yi relay station, the free-tier default. Do NOT pass `engine` unless the user explicitly names one \u2014 the configured provider is correct. Sends to weixin or feishu.",
39791
+ schema: {
39792
+ type: "object",
39793
+ properties: {
39794
+ text: { type: "string", description: "What to say (Chinese text)." },
39795
+ // ⚠️ enum **必须从 TTS_PROVIDERS 派生**,不许再手写一份四支的字面量:
39796
+ // 硬编的话模型传 engine:'relay' 会被工具 schema 拒掉,而托盘把它列成可选项 ——
39797
+ // 两边打架,「唯一源」就成了空话。providers.test.ts 有一条断言钉着它。
39798
+ engine: {
39799
+ type: "string",
39800
+ enum: TTS_PROVIDERS.map((p) => p.id),
39801
+ description: "TTS engine. relay = 7yi \u4E2D\u8F6C\u7AD9\uFF08\u514D\u8D39\u6863\u9ED8\u8BA4\uFF09\uFF0Ccosyvoice = \u767E\u70BC API\uFF0Cgptsovits = \u672C\u5730\u514B\u9686\uFF0Cqwen3-3060 = \u672C\u5730 GGML\uFF0Cedge = \u515C\u5E95\u3002"
39802
+ },
39803
+ channel: { type: "string", enum: ["weixin", "feishu", "discord"], description: "Target channel. Default: current channel." },
39804
+ 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\u3002multaca \u4EFB\u52A1\u91CC\u4E0D\u9700\u8981\u586B to\u2014\u2014\u8BED\u97F3\u4F1A\u6302\u5230\u5F53\u524D issue \u8BC4\u8BBA\uFF0C\u586B issueId \u6307\u5B9A\u76EE\u6807 issue\u3002" },
39805
+ issueId: { type: "string", description: 'multaca headless \u4E13\u7528\uFF1A\u76EE\u6807 issue ID\uFF08\u4EFB\u52A1 prompt \u91CC\u7684 "Your assigned issue ID"\uFF09\u3002\u666E\u901A\u98DE\u4E66/\u5FAE\u4FE1\u573A\u666F\u4E0D\u8981\u586B\u3002' },
39806
+ caption: { type: "string", description: "Optional text to accompany the voice message." }
39807
+ },
39808
+ required: ["text"]
39809
+ },
39810
+ handler: async (args2, ctx) => {
39811
+ const text = args2.text?.trim();
39812
+ if (!text) return { content: "Error: text is required", isError: true };
39813
+ const requestedEngine = args2.engine || "";
39814
+ const caption = args2.caption || "";
39815
+ const mgr = ctx.channelManager;
39816
+ if (!mgr) {
39817
+ const multicaTaskId = process.env.MULTICA_TASK_ID || "";
39818
+ const issueId = args2.issueId || multicaTaskId;
39819
+ if (multicaTaskId && issueId) {
39820
+ return await sendVoiceToMulticaIssue(text, issueId, { engine: requestedEngine, caption });
39821
+ }
39822
+ return {
39823
+ content: "\u8BED\u97F3\u672A\u53D1\u9001\uFF1A\u5F53\u524D\u662F\u65E0\u6E20\u9053\u73AF\u5883\uFF08headless/CLI\uFF09\uFF0C\u4E14\u6CA1\u6709 multaca \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08MULTICA_TASK_ID\uFF09\u6216 issueId\u3002\u6CA1\u6709\u53EF\u7528\u7684\u53D1\u9001\u51FA\u53E3\uFF0C\u8BF7\u7528\u6587\u5B57\u56DE\u590D\u4EE3\u66FF\u3002",
39824
+ isError: true
39825
+ };
39826
+ }
39827
+ const resolvedChannelRaw = args2.channel || (ctx.channel === "deskBuddy" ? "feishu" : ctx.channel) || "feishu";
39828
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
39829
+ let target = args2.to || ctx.channelTarget || ctx.from;
39830
+ if (resolvedChannel === "wechat" && !/^o[\w-]+@im\.wechat$/.test(target || "")) {
39831
+ console.warn(`[my-voice] wechat target "${target}" not a wechat user id`);
39832
+ return {
39833
+ 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`,
39834
+ isError: true
39835
+ };
39836
+ }
39837
+ if (resolvedChannel === "feishu" && !/^ou_[a-f0-9]+$/.test(target || "")) {
39838
+ console.warn(`[my-voice] feishu target "${target}" invalid`);
39839
+ return {
39840
+ 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`,
39841
+ isError: true
39842
+ };
39843
+ }
39844
+ let voiceDurationSec;
39845
+ const vc = liveConfig.get("tools.my_voice");
39846
+ const picked = pickProvider(requestedEngine, vc?.provider);
39847
+ const useEngine = picked.id;
39848
+ console.log(`[my-voice] engine=requested:"${requestedEngine}" config:"${vc?.provider || ""}" \u2192 ${useEngine}`);
39849
+ let audioPath;
39850
+ let actualEngine = useEngine;
39851
+ try {
39852
+ const r = await synthesize(picked.id, text, {});
39853
+ audioPath = r.path;
39854
+ if (r.durationMs && r.durationMs > 0) voiceDurationSec = r.durationMs / 1e3;
39855
+ } catch (e) {
39856
+ if (e?.name === "TtsConfigError") {
39857
+ return { content: `TTS failed: ${e.message}`, isError: true };
39858
+ }
39859
+ const chain = fallbackChainOf(liveConfig.get("tools.my_voice")?.fallback);
39860
+ const tried = /* @__PURE__ */ new Set([picked.id]);
39861
+ let lastErr = e;
39862
+ let recovered = false;
39863
+ for (const id of chain) {
39864
+ if (tried.has(id)) continue;
39865
+ tried.add(id);
39866
+ console.log(`[my-voice] ${picked.id} failed (${e.message}), falling back to ${id}`);
39867
+ try {
39868
+ const r = await synthesize(id, text, {});
39869
+ audioPath = r.path;
39870
+ actualEngine = id;
39871
+ if (r.durationMs && r.durationMs > 0) voiceDurationSec = r.durationMs / 1e3;
39872
+ recovered = true;
39873
+ break;
39874
+ } catch (e2) {
39875
+ lastErr = e2;
39876
+ }
39877
+ }
39878
+ if (!recovered) {
39879
+ return { content: `TTS failed: ${lastErr.message}`, isError: true };
39880
+ }
39881
+ }
39882
+ if (!audioPath.endsWith(".ogg")) {
39883
+ try {
39884
+ const r = await toWav24kWithDuration(audioPath);
39885
+ audioPath = r.wavPath;
39886
+ voiceDurationSec = voiceDurationSec ?? r.durationSec;
39887
+ } catch (e) {
39888
+ console.warn(`[my-voice] toWav24k failed (${e.message}), sending raw file`);
39889
+ }
39890
+ }
39891
+ if (audioPath.endsWith(".wav")) {
39892
+ try {
39893
+ audioPath = await wavToOggOpus(audioPath);
39894
+ } catch (e) {
39895
+ console.warn(`[my-voice] ogg/opus encode failed (${e.message}), falling back to m4a`);
39896
+ audioPath = await compressWav(audioPath);
39897
+ }
39898
+ }
39899
+ const ext = path46.extname(audioPath).toLowerCase();
39900
+ const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg", ".silk": "audio/silk" };
39901
+ const mimeType = mimeMap[ext] || "audio/mpeg";
39902
+ const sizeKB = fs47.statSync(audioPath).size / 1024;
39903
+ try {
39904
+ await mgr.sendFile(resolvedChannel, target, caption, {
39905
+ path: audioPath,
39906
+ mimeType,
39907
+ filename: `voice_${Date.now()}${ext}`,
39908
+ ...voiceDurationSec !== void 0 ? { voiceDurationSec } : {}
39909
+ });
39910
+ try {
39911
+ fs47.unlinkSync(audioPath);
39912
+ } catch {
39913
+ }
39914
+ return { content: `Voice sent! (${actualEngine}, ${sizeKB.toFixed(0)}KB, ${resolvedChannel})` };
39915
+ } catch (err) {
39916
+ return { content: `Voice send failed: ${err.message}`, isError: true };
39917
+ }
39918
+ },
39919
+ isConcurrencySafe: () => false,
39920
+ interruptBehavior: () => "block",
39921
+ isReadOnly: () => false,
39922
+ isDestructive: () => false
39923
+ });
39924
+
39925
+ // src/tools/my-selfie.ts
39926
+ init_live();
39927
+ init_registry();
39928
+ init_well_formed();
39929
+ import * as fs48 from "node:fs";
39930
+ import * as path47 from "node:path";
39629
39931
  function getProxyDispatcher2() {
39630
39932
  const cfg = liveConfig.all();
39631
39933
  const proxy = cfg.providers?.xai?.proxy;
@@ -39639,7 +39941,7 @@ function getProxyDispatcher2() {
39639
39941
  }
39640
39942
  function dimsOf(p) {
39641
39943
  try {
39642
- const b = fs47.readFileSync(p);
39944
+ const b = fs48.readFileSync(p);
39643
39945
  let w = 0, h = 0;
39644
39946
  if (b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
39645
39947
  w = b.readUInt32BE(16);
@@ -39762,9 +40064,9 @@ function registerPortrait(relPath, name) {
39762
40064
  const cur = cfg?.tools?.my_selfie?.references || cfg?.my_selfie?.references || [];
39763
40065
  const next = Array.isArray(cur) ? cur.filter((r) => r?.name !== name) : [];
39764
40066
  next.unshift({ name, path: relPath });
39765
- const path58 = cfg?.tools?.my_selfie ? "tools.my_selfie.references" : "my_selfie.references";
39766
- void liveConfig.set(path58, next);
39767
- console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u767B\u8BB0\uFF1A${path58}[0] = ${relPath}`);
40067
+ const path60 = cfg?.tools?.my_selfie ? "tools.my_selfie.references" : "my_selfie.references";
40068
+ void liveConfig.set(path60, next);
40069
+ console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u767B\u8BB0\uFF1A${path60}[0] = ${relPath}`);
39768
40070
  } catch (e) {
39769
40071
  console.warn(`[my-selfie] \u5199\u5165 references \u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u6B21\u51FA\u56FE\uFF09: ${e.message}`);
39770
40072
  }
@@ -40045,10 +40347,10 @@ registry.register({
40045
40347
  const REFERENCES = getReferences(ctx);
40046
40348
  const refName = args2.reference || "default";
40047
40349
  const refEntry = REFERENCES.find((r) => r.name === refName) || REFERENCES[0];
40048
- let refPath = path46.join(ctx.workspace, refEntry.p);
40350
+ let refPath = path47.join(ctx.workspace, refEntry.p);
40049
40351
  const adoptImg = args2.referenceImage?.trim();
40050
40352
  if (adoptImg) {
40051
- if (!fs47.existsSync(adoptImg)) {
40353
+ if (!fs48.existsSync(adoptImg)) {
40052
40354
  return { content: `\u627E\u4E0D\u5230\u90A3\u5F20\u56FE\uFF1A${adoptImg}`, isError: true };
40053
40355
  }
40054
40356
  const acheck = dimsOf(adoptImg);
@@ -40059,16 +40361,16 @@ registry.register({
40059
40361
  isError: true
40060
40362
  };
40061
40363
  }
40062
- const adir = path46.join(ctx.workspace, "images");
40063
- if (!fs47.existsSync(adir)) fs47.mkdirSync(adir, { recursive: true });
40064
- const aext = (path46.extname(adoptImg) || ".png").toLowerCase();
40364
+ const adir = path47.join(ctx.workspace, "images");
40365
+ if (!fs48.existsSync(adir)) fs48.mkdirSync(adir, { recursive: true });
40366
+ const aext = (path47.extname(adoptImg) || ".png").toLowerCase();
40065
40367
  const afname = `portrait_${Date.now()}${aext}`;
40066
- fs47.copyFileSync(adoptImg, path46.join(adir, afname));
40368
+ fs48.copyFileSync(adoptImg, path47.join(adir, afname));
40067
40369
  registerPortrait(`images/${afname}`, refName);
40068
- refPath = path46.join(adir, afname);
40370
+ refPath = path47.join(adir, afname);
40069
40371
  console.log(`[my-selfie] \u91C7\u7EB3\u73B0\u6210\u56FE\u5F53\u5F62\u8C61\uFF1Aimages/${afname}${acheck ? ` (${acheck})` : ""}`);
40070
40372
  }
40071
- if (provider !== "autodl" && !fs47.existsSync(refPath)) {
40373
+ if (provider !== "autodl" && !fs48.existsSync(refPath)) {
40072
40374
  const cfgAll = liveConfig.all();
40073
40375
  const portraitDesc = resolvePortraitDesc(
40074
40376
  args2.portrait,
@@ -40093,16 +40395,16 @@ registry.register({
40093
40395
  } catch (e) {
40094
40396
  return { content: `\u5851\u9020\u5F62\u8C61\u5931\u8D25\uFF1A${e.message}`, isError: true };
40095
40397
  }
40096
- const pdir = path46.join(ctx.workspace, "images");
40097
- if (!fs47.existsSync(pdir)) fs47.mkdirSync(pdir, { recursive: true });
40398
+ const pdir = path47.join(ctx.workspace, "images");
40399
+ if (!fs48.existsSync(pdir)) fs48.mkdirSync(pdir, { recursive: true });
40098
40400
  const pfname = `portrait_${Date.now()}.png`;
40099
- fs47.writeFileSync(path46.join(pdir, pfname), portraitBuf);
40401
+ fs48.writeFileSync(path47.join(pdir, pfname), portraitBuf);
40100
40402
  registerPortrait(`images/${pfname}`, refName);
40101
- refPath = path46.join(pdir, pfname);
40403
+ refPath = path47.join(pdir, pfname);
40102
40404
  console.log(`[my-selfie] \u5F62\u8C61\u5DF2\u5B58 images/${pfname}\uFF0C\u7EE7\u7EED\u8FD9\u6B21\u81EA\u62CD`);
40103
40405
  }
40104
40406
  const resolution = args2.resolution || DEFAULT_RESOLUTION;
40105
- const refB64 = fs47.existsSync(refPath) ? fs47.readFileSync(refPath).toString("base64") : "";
40407
+ const refB64 = fs48.existsSync(refPath) ? fs48.readFileSync(refPath).toString("base64") : "";
40106
40408
  let imageBuffer;
40107
40409
  try {
40108
40410
  if (provider === "autodl") {
@@ -40118,7 +40420,7 @@ registry.register({
40118
40420
  return { content: `Error: relay provider \u672A\u914D\u7F6E\u3002config \u91CC\u8981\u6709 models.providers.${cfgAll?.my_selfie?.relayProviderId || "7yi"} \u7684 baseUrl \u548C apiKey\u3002`, isError: true };
40119
40421
  }
40120
40422
  const model = cfgAll?.my_selfie?.relayModel || cfgAll?.tools?.my_selfie?.relayModel || "engine7-img-free";
40121
- const ext = path46.extname(refPath).toLowerCase();
40423
+ const ext = path47.extname(refPath).toLowerCase();
40122
40424
  const mime = ext === ".png" ? "image/png" : ext === ".webp" ? "image/webp" : "image/jpeg";
40123
40425
  imageBuffer = await generateWithRelay(refB64, fullPrompt, mime, relay.baseUrl, relay.apiKey, model);
40124
40426
  } else {
@@ -40127,11 +40429,11 @@ registry.register({
40127
40429
  } catch (err) {
40128
40430
  return { content: `Selfie generation failed: ${err.message}`, isError: true };
40129
40431
  }
40130
- const imagesDir = path46.join(ctx.workspace, "images");
40131
- if (!fs47.existsSync(imagesDir)) fs47.mkdirSync(imagesDir, { recursive: true });
40432
+ const imagesDir = path47.join(ctx.workspace, "images");
40433
+ if (!fs48.existsSync(imagesDir)) fs48.mkdirSync(imagesDir, { recursive: true });
40132
40434
  const filename = `selfie_${Date.now()}.jpg`;
40133
- const outputPath = path46.join(imagesDir, filename);
40134
- fs47.writeFileSync(outputPath, imageBuffer);
40435
+ const outputPath = path47.join(imagesDir, filename);
40436
+ fs48.writeFileSync(outputPath, imageBuffer);
40135
40437
  const mgr = ctx.channelManager;
40136
40438
  if (mgr) {
40137
40439
  const resolvedChannel = ctx.channel || "feishu";
@@ -40142,11 +40444,11 @@ registry.register({
40142
40444
  mimeType: "image/jpeg"
40143
40445
  });
40144
40446
  } catch (err) {
40145
- return { content: `Selfie generated but send failed: ${err.message}. Image: ${path46.resolve(outputPath)}`, isError: false };
40447
+ return { content: `Selfie generated but send failed: ${err.message}. Image: ${path47.resolve(outputPath)}`, isError: false };
40146
40448
  }
40147
40449
  return { content: `Selfie sent! Mode: ${mode}, Provider: ${provider}, Ref: ${refEntry.name}` };
40148
40450
  }
40149
- return { content: `Selfie generated! Mode: ${mode}, Ref: ${refEntry.name}. Image: ${path46.resolve(outputPath)}` };
40451
+ return { content: `Selfie generated! Mode: ${mode}, Ref: ${refEntry.name}. Image: ${path47.resolve(outputPath)}` };
40150
40452
  },
40151
40453
  isConcurrencySafe: () => false,
40152
40454
  interruptBehavior: () => "block",
@@ -40698,12 +41000,20 @@ ${lines.join("\n")}` };
40698
41000
  init_registry();
40699
41001
  init_live();
40700
41002
  init_clock();
40701
- import { exec as exec3, spawn as spawn7 } from "node:child_process";
40702
- import { promisify as promisify3 } from "node:util";
40703
- var execAsync = promisify3(exec3);
41003
+ import * as fs49 from "node:fs";
41004
+ import * as path48 from "node:path";
41005
+ import { exec as exec2, spawn as spawn6 } from "node:child_process";
41006
+ import { promisify as promisify4 } from "node:util";
41007
+ var execAsync = promisify4(exec2);
40704
41008
  function getServices() {
40705
41009
  return liveConfig.get("services") || {};
40706
41010
  }
41011
+ function serviceLogPath(fileName) {
41012
+ const stateDir = String(liveConfig.get("stateDir") ?? "");
41013
+ const logDir = path48.join(stateDir, "logs");
41014
+ if (!fs49.existsSync(logDir)) fs49.mkdirSync(logDir, { recursive: true });
41015
+ return path48.join(logDir, fileName);
41016
+ }
40707
41017
  async function runCmd(cmd, timeoutMs = 15e3) {
40708
41018
  try {
40709
41019
  const { stdout, stderr } = await execAsync(cmd, { timeout: timeoutMs, maxBuffer: 1024 * 1024 });
@@ -40715,9 +41025,9 @@ async function runCmd(cmd, timeoutMs = 15e3) {
40715
41025
  function spawnDetached(cmd) {
40716
41026
  const isWin = process.platform === "win32";
40717
41027
  const shell = isWin ? "cmd.exe" : "/bin/sh";
40718
- const logFile = `D:/xiaoke/logs/service-${todayKey().replace(/-/g, "")}.log`;
41028
+ const logFile = serviceLogPath(`service-${todayKey().replace(/-/g, "")}.log`);
40719
41029
  const wrappedCmd = isWin ? `/c "${cmd.replace(/"/g, '\\"')} > "${logFile}" 2>&1"` : `-c "${cmd} > '${logFile}' 2>&1"`;
40720
- const child = spawn7(shell, isWin ? [wrappedCmd] : ["-c", `${cmd} > '${logFile}' 2>&1`], {
41030
+ const child = spawn6(shell, isWin ? [wrappedCmd] : ["-c", `${cmd} > '${logFile}' 2>&1`], {
40721
41031
  detached: true,
40722
41032
  stdio: "ignore",
40723
41033
  windowsHide: true,
@@ -40778,7 +41088,7 @@ registry.register({
40778
41088
  const startCmd = svcArgs ? `${svc.start} ${svcArgs}` : svc.start;
40779
41089
  if (longRunning) {
40780
41090
  const isWin = process.platform === "win32";
40781
- const logFile = `D:/xiaoke/logs/${name}${svcArgs ? "-" + svcArgs : ""}-${todayKey().replace(/-/g, "")}.log`;
41091
+ const logFile = serviceLogPath(`${name}${svcArgs ? "-" + svcArgs : ""}-${todayKey().replace(/-/g, "")}.log`);
40782
41092
  const redirect = isWin ? ` >> "${logFile}" 2>&1` : ` >> "${logFile}" 2>&1`;
40783
41093
  const pid = spawnDetached(startCmd + redirect);
40784
41094
  lines.push(`start: spawned PID ${pid} (background${svcArgs ? ", args=" + svcArgs : ""})`);
@@ -41021,16 +41331,16 @@ var EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode";
41021
41331
  init_planModeState();
41022
41332
 
41023
41333
  // src/utils/plans.ts
41024
- import * as fs48 from "node:fs";
41025
- import * as path47 from "node:path";
41334
+ import * as fs50 from "node:fs";
41335
+ import * as path49 from "node:path";
41026
41336
  import * as crypto7 from "node:crypto";
41027
41337
  var MAX_SLUG_RETRIES = 10;
41028
41338
  function generateSlug() {
41029
41339
  return crypto7.randomBytes(4).toString("hex");
41030
41340
  }
41031
41341
  function getPlansDirectory(stateDir) {
41032
- const plansDir = path47.join(stateDir, "plans");
41033
- fs48.mkdirSync(plansDir, { recursive: true });
41342
+ const plansDir = path49.join(stateDir, "plans");
41343
+ fs50.mkdirSync(plansDir, { recursive: true });
41034
41344
  return plansDir;
41035
41345
  }
41036
41346
  var planSlugCache = /* @__PURE__ */ new Map();
@@ -41040,8 +41350,8 @@ function getPlanSlug(sessionId, stateDir) {
41040
41350
  const plansDir = getPlansDirectory(stateDir);
41041
41351
  for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
41042
41352
  slug = generateSlug();
41043
- const filePath = path47.join(plansDir, `${slug}.md`);
41044
- if (!fs48.existsSync(filePath)) {
41353
+ const filePath = path49.join(plansDir, `${slug}.md`);
41354
+ if (!fs50.existsSync(filePath)) {
41045
41355
  break;
41046
41356
  }
41047
41357
  }
@@ -41052,21 +41362,21 @@ function getPlanSlug(sessionId, stateDir) {
41052
41362
  function getPlanFilePath(sessionId, stateDir, agentId) {
41053
41363
  const slug = getPlanSlug(sessionId, stateDir);
41054
41364
  if (!agentId) {
41055
- return path47.join(getPlansDirectory(stateDir), `${slug}.md`);
41365
+ return path49.join(getPlansDirectory(stateDir), `${slug}.md`);
41056
41366
  }
41057
- return path47.join(getPlansDirectory(stateDir), `${slug}-agent-${agentId}.md`);
41367
+ return path49.join(getPlansDirectory(stateDir), `${slug}-agent-${agentId}.md`);
41058
41368
  }
41059
41369
  function getPlan(sessionId, stateDir, agentId) {
41060
41370
  const filePath = getPlanFilePath(sessionId, stateDir, agentId);
41061
41371
  try {
41062
- return fs48.readFileSync(filePath, "utf-8");
41372
+ return fs50.readFileSync(filePath, "utf-8");
41063
41373
  } catch {
41064
41374
  return null;
41065
41375
  }
41066
41376
  }
41067
41377
  function writePlan(sessionId, stateDir, content, agentId) {
41068
41378
  const filePath = getPlanFilePath(sessionId, stateDir, agentId);
41069
- fs48.writeFileSync(filePath, content, "utf-8");
41379
+ fs50.writeFileSync(filePath, content, "utf-8");
41070
41380
  return filePath;
41071
41381
  }
41072
41382
 
@@ -41707,8 +42017,8 @@ init_features2();
41707
42017
  init_agent_registry();
41708
42018
 
41709
42019
  // src/commands/work.ts
41710
- import * as fs53 from "node:fs";
41711
- import * as path53 from "node:path";
42020
+ import * as fs55 from "node:fs";
42021
+ import * as path55 from "node:path";
41712
42022
  registerCommand({
41713
42023
  name: "work",
41714
42024
  description: "\u5DE5\u4F5C\u6A21\u5F0F\uFF1A\u65E0\u53C2\u770B\u72B6\u6001\uFF0Con=\u5F3A\u5236work\uFF0Coff=\u5F3A\u5236emotion\uFF0Cauto=\u81EA\u52A8\u5224\u5B9A",
@@ -41721,20 +42031,20 @@ registerCommand({
41721
42031
  ],
41722
42032
  handler: async (ctx, deps) => {
41723
42033
  const mode = (ctx.args.mode || "").trim().toLowerCase();
41724
- const modeFile = path53.join(deps.config.workspace, ".work-mode");
42034
+ const modeFile = path55.join(deps.config.workspace, ".work-mode");
41725
42035
  if (mode === "on" || mode === "off") {
41726
- fs53.writeFileSync(modeFile, mode, "utf8");
42036
+ fs55.writeFileSync(modeFile, mode, "utf8");
41727
42037
  await ctx.reply(mode === "on" ? "\u5DE5\u4F5C\u6A21\u5F0F\u5DF2\u5F3A\u5236 **work**\uFF08\u6240\u6709\u6D88\u606F\u5168\u91CF\u4E0A\u4E0B\u6587\uFF0C\u4E0B\u4E00\u6761\u751F\u6548\uFF09" : "\u5DE5\u4F5C\u6A21\u5F0F\u5DF2\u5F3A\u5236 **emotion**\uFF08\u6240\u6709\u6D88\u606F\u8F7B\u91CF\u95F2\u804A\uFF0C\u4E0B\u4E00\u6761\u751F\u6548\uFF09");
41728
42038
  } else if (mode === "auto") {
41729
42039
  try {
41730
- fs53.unlinkSync(modeFile);
42040
+ fs55.unlinkSync(modeFile);
41731
42041
  } catch {
41732
42042
  }
41733
42043
  await ctx.reply("\u5DE5\u4F5C\u6A21\u5F0F\u5DF2\u6062\u590D **auto**\uFF08emotion_state_machine \u9010\u6761\u81EA\u52A8\u5224\u5B9A\uFF09");
41734
42044
  } else {
41735
42045
  let cur = "auto";
41736
42046
  try {
41737
- cur = fs53.readFileSync(modeFile, "utf8").trim() || "auto";
42047
+ cur = fs55.readFileSync(modeFile, "utf8").trim() || "auto";
41738
42048
  } catch {
41739
42049
  }
41740
42050
  const hint = cur === "on" ? "\uFF08\u5F3A\u5236 work\uFF1A\u5168\u91CF\u4E0A\u4E0B\u6587\uFF09" : cur === "off" ? "\uFF08\u5F3A\u5236 emotion\uFF1A\u8F7B\u91CF\u4E0A\u4E0B\u6587\uFF09" : "\uFF08\u6BCF\u6761\u6D88\u606F\u7531 emotion_state_machine \u81EA\u52A8\u5224\u5B9A\uFF09";
@@ -42204,8 +42514,8 @@ ${err.stack}`);
42204
42514
  });
42205
42515
 
42206
42516
  // src/commands/model.ts
42207
- import * as fs54 from "node:fs";
42208
- import * as path54 from "node:path";
42517
+ import * as fs56 from "node:fs";
42518
+ import * as path56 from "node:path";
42209
42519
  import { fileURLToPath as fileURLToPath8 } from "node:url";
42210
42520
  function collectModels(config2) {
42211
42521
  const aliases = config2.modelAliases || {};
@@ -42237,11 +42547,11 @@ function resolveConfigWritePath(config2, label) {
42237
42547
  const configPath2 = config2._configFilePath;
42238
42548
  if (!configPath2) return void 0;
42239
42549
  let writePath = configPath2;
42240
- if (configPath2 && !fs54.existsSync(configPath2)) {
42550
+ if (configPath2 && !fs56.existsSync(configPath2)) {
42241
42551
  const __pFile = fileURLToPath8(import.meta.url);
42242
- const __pDir = path54.dirname(__pFile);
42243
- const altPath = path54.join(path54.resolve(__pDir, "../configs"), path54.basename(configPath2));
42244
- if (fs54.existsSync(altPath)) {
42552
+ const __pDir = path56.dirname(__pFile);
42553
+ const altPath = path56.join(path56.resolve(__pDir, "../configs"), path56.basename(configPath2));
42554
+ if (fs56.existsSync(altPath)) {
42245
42555
  console.warn(`[${label}] Config not found at ${configPath2}, falling back to ${altPath}`);
42246
42556
  writePath = altPath;
42247
42557
  }
@@ -42396,14 +42706,14 @@ Use full ref like \`/primary ${candidates[0].ref}\``);
42396
42706
  return;
42397
42707
  }
42398
42708
  try {
42399
- const raw = await fs54.promises.readFile(writePath, "utf-8");
42709
+ const raw = await fs56.promises.readFile(writePath, "utf-8");
42400
42710
  const cfg = JSON.parse(raw);
42401
42711
  if (!cfg.agents?.defaults?.model) {
42402
42712
  await ctx.reply(`\u26A0\uFE0F Config structure mismatch: agents.defaults.model not found`);
42403
42713
  return;
42404
42714
  }
42405
42715
  cfg.agents.defaults.model.primary = target;
42406
- await fs54.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42716
+ await fs56.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42407
42717
  console.log(`[primary] Persisted primary=${target} to ${writePath}`);
42408
42718
  await ctx.reply(`\u2705 Primary model set to **${target}** (${candidates[0].name})
42409
42719
  Written to config, hot-reloading provider chain now.`);
@@ -42450,7 +42760,7 @@ Use full ref like \`/vision-primary ${candidates[0].ref}\``);
42450
42760
  return;
42451
42761
  }
42452
42762
  try {
42453
- const raw = await fs54.promises.readFile(writePath, "utf-8");
42763
+ const raw = await fs56.promises.readFile(writePath, "utf-8");
42454
42764
  const cfg = JSON.parse(raw);
42455
42765
  if (!cfg.agents?.defaults?.model) {
42456
42766
  await ctx.reply(`\u26A0\uFE0F Config structure mismatch: agents.defaults.model not found`);
@@ -42458,12 +42768,12 @@ Use full ref like \`/vision-primary ${candidates[0].ref}\``);
42458
42768
  }
42459
42769
  if (input === "auto" || input === "reset") {
42460
42770
  delete cfg.agents.defaults.model.vision;
42461
- await fs54.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42771
+ await fs56.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42462
42772
  console.log(`[vision-primary] Cleared vision primary in ${writePath}`);
42463
42773
  await ctx.reply(`\u2705 Vision primary cleared (auto). Written to config. Hot-reload will apply.`);
42464
42774
  } else {
42465
42775
  cfg.agents.defaults.model.vision = target;
42466
- await fs54.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42776
+ await fs56.promises.writeFile(writePath, JSON.stringify(cfg, null, 2), "utf-8");
42467
42777
  console.log(`[vision-primary] Persisted vision=${target} to ${writePath}`);
42468
42778
  await ctx.reply(`\u2705 Vision primary set to **${target}** (${candidates[0].name})
42469
42779
  Written to config. Hot-reload will apply.`);
@@ -42871,7 +43181,7 @@ async function startEngine(config2, opts) {
42871
43181
  const { initSessionMemory: initSessionMemory2 } = await Promise.resolve().then(() => (init_sessionMemory(), sessionMemory_exports));
42872
43182
  initSessionMemory2({
42873
43183
  workspace: config2.workspace,
42874
- stateDir: path57.join(config2.stateDir, "session-memory"),
43184
+ stateDir: path59.join(config2.stateDir, "session-memory"),
42875
43185
  provider,
42876
43186
  model: config2.provider.modelId || config2.model || "deepseek-v4-flash",
42877
43187
  features: config2.profile.features
@@ -43400,11 +43710,11 @@ async function startEngine(config2, opts) {
43400
43710
  }
43401
43711
 
43402
43712
  // src/main.ts
43403
- import { readFileSync as readFileSync47 } from "node:fs";
43713
+ import { readFileSync as readFileSync48 } from "node:fs";
43404
43714
  import { fileURLToPath as fileURLToPath9 } from "node:url";
43405
- import { dirname as dirname16, join as join63 } from "node:path";
43715
+ import { dirname as dirname16, join as join66 } from "node:path";
43406
43716
  var __dirname2 = dirname16(fileURLToPath9(import.meta.url));
43407
- var pkg = JSON.parse(readFileSync47(join63(__dirname2, "..", "package.json"), "utf-8"));
43717
+ var pkg = JSON.parse(readFileSync48(join66(__dirname2, "..", "package.json"), "utf-8"));
43408
43718
  var epipeSeen = false;
43409
43719
  process.on("uncaughtException", (err) => {
43410
43720
  const code = err?.code ?? "";