engine7 7.1.37 → 7.1.38

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
@@ -929,6 +929,9 @@ function toHookContext(ctx) {
929
929
  };
930
930
  }
931
931
  async function executeOne(tc, ctx, signal) {
932
+ if (signal?.aborted) {
933
+ return { call: tc, result: { content: "[interrupted by user /stop]", isError: false }, summary: null };
934
+ }
932
935
  const tool = registry.get(tc.function.name);
933
936
  if (!tool) {
934
937
  return { call: tc, result: { content: `Unknown tool: ${tc.function.name}`, isError: true }, summary: null };
@@ -2918,7 +2921,8 @@ var init_query = __esm({
2918
2921
  init_constants();
2919
2922
  init_deferred();
2920
2923
  init_hooks();
2921
- QueryEngine = class {
2924
+ init_live();
2925
+ QueryEngine = class _QueryEngine {
2922
2926
  constructor(provider, options) {
2923
2927
  this.provider = provider;
2924
2928
  this.options = options;
@@ -2929,6 +2933,33 @@ var init_query = __esm({
2929
2933
  }
2930
2934
  provider;
2931
2935
  options;
2936
+ // === voice-chat 占位状态(#194 v4)===
2937
+ static placeholderQueries = /* @__PURE__ */ new Map();
2938
+ // queryId → ts(3 分钟过期)
2939
+ static placeholderCounter = 0;
2940
+ /** 工具>5s 触发:同一 query 只占位一次 + 30% 概率 + 轮换文案(防口头禅) */
2941
+ static async tryVoicePlaceholder(context) {
2942
+ if (context?.channel !== "voice-chat") return;
2943
+ if (!(liveConfig.get("voiceChat.placeholder.enabled") ?? true)) return;
2944
+ const qid = context.sessionId || "default";
2945
+ const now = Date.now();
2946
+ for (const [k, v] of _QueryEngine.placeholderQueries) if (now - v > 3 * 60 * 1e3) _QueryEngine.placeholderQueries.delete(k);
2947
+ if (_QueryEngine.placeholderQueries.has(qid)) return;
2948
+ if (Math.random() > 0.3) return;
2949
+ _QueryEngine.placeholderQueries.set(qid, now);
2950
+ const lines = ["\u7B49\u6211\u67E5\u4E0B\u54C8", "\u7A0D\u7B49\uFF0C\u6211\u770B\u4E00\u773C", "\u6211\u53BB\u7FFB\u7FFB", "\u7B49\u4E0B\u54E6", "\u55EF\u2026\u6211\u53BB\u770B\u770B"];
2951
+ const line = lines[_QueryEngine.placeholderCounter++ % lines.length];
2952
+ try {
2953
+ await fetch("https://localhost:8116/voice-reply", {
2954
+ method: "POST",
2955
+ headers: { "Content-Type": "application/json" },
2956
+ body: JSON.stringify({ text: line, isPlaceholder: true })
2957
+ });
2958
+ console.log(`[vc-placeholder] sent: ${line}`);
2959
+ } catch (e) {
2960
+ console.warn(`[vc-placeholder] send failed: ${e.message}`);
2961
+ }
2962
+ }
2932
2963
  abortController = null;
2933
2964
  /** 外部 pre-query abort controller(handle-query 入口注册,弥补 query() 前的空窗期) */
2934
2965
  preQueryAbort = null;
@@ -3283,7 +3314,18 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3283
3314
  console.warn(`[hook] PreToolUse ${toolName} error: ${e.message}`);
3284
3315
  }
3285
3316
  }
3286
- const { messages: toolResults, summaries } = await executeTools(toolCalls, ac.signal, toolCtx);
3317
+ const vcPlaceholderOn = context?.channel === "voice-chat" && (liveConfig.get("voiceChat.placeholder.enabled") ?? true);
3318
+ const vcTimer = vcPlaceholderOn ? setTimeout(() => {
3319
+ _QueryEngine.tryVoicePlaceholder(context).catch(() => {
3320
+ });
3321
+ }, 5e3) : null;
3322
+ let toolRes;
3323
+ try {
3324
+ toolRes = await executeTools(toolCalls, ac.signal, toolCtx);
3325
+ } finally {
3326
+ if (vcTimer) clearTimeout(vcTimer);
3327
+ }
3328
+ const { messages: toolResults, summaries } = toolRes;
3287
3329
  for (const s of summaries) {
3288
3330
  try {
3289
3331
  const postResult = await executePostToolUseHooks(s.tool, {}, s.summary, hookCtx, ac.signal);
@@ -5165,11 +5207,11 @@ function buildPayload(messages, cfg, sessionId) {
5165
5207
  const meta = m.role === "user" ? parseMeta(text) : null;
5166
5208
  const senderName = meta?.senderName ?? (m.role === "assistant" ? cfg.agentName : void 0) ?? m.role;
5167
5209
  const rawRole = m.role === "toolResult" ? "tool" : m.role;
5168
- const validRoles = ["user", "assistant", "tool"];
5210
+ const role = rawRole === "user" ? "user" : "assistant";
5169
5211
  return {
5170
5212
  sender_id: cfg.appId,
5171
5213
  sender_name: senderName,
5172
- role: validRoles.includes(rawRole) ? rawRole : "user",
5214
+ role,
5173
5215
  timestamp: Date.now(),
5174
5216
  content: text
5175
5217
  };
@@ -12726,6 +12768,7 @@ var BASE_DELAY_MS = 500;
12726
12768
  var MAX_DELAY_MS = 32e3;
12727
12769
  var DEFAULT_TIMEOUT_MS = 6e5;
12728
12770
  var READ_TIMEOUT_MS = 6e4;
12771
+ var FIRST_READ_TIMEOUT_MS = parseInt(process.env.API_TIMEOUT_MS || "", 10) || 6e5;
12729
12772
  function sleep(ms, signal) {
12730
12773
  return new Promise((resolve10, reject) => {
12731
12774
  if (signal?.aborted) {
@@ -12989,7 +13032,15 @@ var OpenAIProvider = class {
12989
13032
  try {
12990
13033
  while (true) {
12991
13034
  if (params.signal?.aborted) break;
12992
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
13035
+ let readRes;
13036
+ try {
13037
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
13038
+ } catch (readErr) {
13039
+ if (params.signal?.aborted) throw readErr;
13040
+ yield { type: "error", error: readErr.message };
13041
+ return;
13042
+ }
13043
+ const { done: done2, value } = readRes;
12993
13044
  if (done2) break;
12994
13045
  if (firstChunk) {
12995
13046
  firstChunk = false;
@@ -13273,6 +13324,7 @@ var AnthropicProvider = class {
13273
13324
  const reader = response.body.getReader();
13274
13325
  const decoder = new TextDecoder();
13275
13326
  let buffer = "";
13327
+ let firstChunk = true;
13276
13328
  const toolUseBlocks = /* @__PURE__ */ new Map();
13277
13329
  let doneYielded = false;
13278
13330
  const handleData = function* (data) {
@@ -13362,8 +13414,20 @@ var AnthropicProvider = class {
13362
13414
  try {
13363
13415
  while (true) {
13364
13416
  if (params.signal?.aborted) break;
13365
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
13417
+ let readRes;
13418
+ try {
13419
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
13420
+ } catch (readErr) {
13421
+ if (params.signal?.aborted) throw readErr;
13422
+ yield { type: "error", error: readErr.message };
13423
+ return;
13424
+ }
13425
+ const { done: done2, value } = readRes;
13366
13426
  if (done2) break;
13427
+ if (firstChunk) {
13428
+ firstChunk = false;
13429
+ console.log(`[anthropic] first chunk received (${value?.length ?? 0}B)`);
13430
+ }
13367
13431
  buffer += decoder.decode(value, { stream: true });
13368
13432
  const lines = buffer.split("\n");
13369
13433
  buffer = lines.pop();
@@ -13575,7 +13639,15 @@ var GeminiProvider = class {
13575
13639
  try {
13576
13640
  while (true) {
13577
13641
  if (params.signal?.aborted) break;
13578
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
13642
+ let readRes;
13643
+ try {
13644
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
13645
+ } catch (readErr) {
13646
+ if (params.signal?.aborted) throw readErr;
13647
+ yield { type: "error", error: readErr.message };
13648
+ return;
13649
+ }
13650
+ const { done: done2, value } = readRes;
13579
13651
  if (done2) break;
13580
13652
  if (firstChunk) {
13581
13653
  firstChunk = false;
@@ -14431,7 +14503,9 @@ var FeishuAdapter = class _FeishuAdapter {
14431
14503
  }
14432
14504
  const filename = attachment.filename || path46.basename(attachment.path);
14433
14505
  const fileBuffer = fs43.readFileSync(attachment.path);
14434
- const receiveIdType = target.startsWith("ou_") ? "open_id" : "chat_id";
14506
+ const safeTarget = target || this.config.defaultTarget || "";
14507
+ if (!safeTarget) throw new Error("Feishu sendFile: no target (\u4E3B\u52A8\u53D1\u9001\u9700\u914D\u7F6E defaultTarget \u6216\u4F20\u5165 target)");
14508
+ const receiveIdType = safeTarget.startsWith("ou_") ? "open_id" : "chat_id";
14435
14509
  const mimeType = attachment.mimeType || "application/octet-stream";
14436
14510
  if (mimeType.startsWith("image/")) {
14437
14511
  const imageKey = await this._uploadImage(fileBuffer, filename);
@@ -14439,19 +14513,45 @@ var FeishuAdapter = class _FeishuAdapter {
14439
14513
  const postContent = this._buildImagePost(message, imageKey);
14440
14514
  await this.client.im.message.create({
14441
14515
  params: { receive_id_type: receiveIdType },
14442
- data: { receive_id: target, msg_type: "post", content: JSON.stringify(postContent) }
14516
+ data: { receive_id: safeTarget, msg_type: "post", content: JSON.stringify(postContent) }
14517
+ });
14518
+ } else {
14519
+ await this.client.im.message.create({
14520
+ params: { receive_id_type: receiveIdType },
14521
+ data: { receive_id: safeTarget, msg_type: "image", content: JSON.stringify({ image_key: imageKey }) }
14522
+ });
14523
+ }
14524
+ } else if (mimeType.startsWith("audio/")) {
14525
+ const isOpus = filename.endsWith(".opus") || filename.endsWith(".ogg") || mimeType === "audio/opus";
14526
+ if (!isOpus) {
14527
+ console.warn(`[feishu:sendFile] audio '${filename}' \u4E0D\u662Fopus\uFF0C\u964D\u7EA7\u4E3A\u6587\u4EF6\u6D88\u606F\uFF08\u975E\u8BED\u97F3\u6761\uFF09`);
14528
+ const fileKey = await this._uploadFile(fileBuffer, filename);
14529
+ await this.client.im.message.create({
14530
+ params: { receive_id_type: receiveIdType },
14531
+ data: { receive_id: safeTarget, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14443
14532
  });
14444
14533
  } else {
14534
+ let durationMs = Math.round(fileBuffer.length / 3);
14535
+ try {
14536
+ const { execFile: execFile3 } = await import("node:child_process");
14537
+ const { promisify: promisify3 } = await import("node:util");
14538
+ const execFileAsync2 = promisify3(execFile3);
14539
+ const probe = await execFileAsync2("ffprobe", ["-v", "quiet", "-show_format", "-print_format", "json", attachment.path], { timeout: 1e4 });
14540
+ const fmt = JSON.parse(probe.stdout);
14541
+ if (fmt?.format?.duration) durationMs = Math.round(parseFloat(fmt.format.duration) * 1e3);
14542
+ } catch {
14543
+ }
14544
+ const audioKey = await this._uploadAudio(fileBuffer, filename.endsWith(".opus") ? filename : filename.replace(/\.[^.]+$/, ".opus"), durationMs);
14445
14545
  await this.client.im.message.create({
14446
14546
  params: { receive_id_type: receiveIdType },
14447
- data: { receive_id: target, msg_type: "image", content: JSON.stringify({ image_key: imageKey }) }
14547
+ data: { receive_id: safeTarget, msg_type: "audio", content: JSON.stringify({ file_key: audioKey }) }
14448
14548
  });
14449
14549
  }
14450
14550
  } else {
14451
14551
  const fileKey = await this._uploadFile(fileBuffer, filename);
14452
14552
  await this.client.im.message.create({
14453
14553
  params: { receive_id_type: receiveIdType },
14454
- data: { receive_id: target, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14554
+ data: { receive_id: safeTarget, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14455
14555
  });
14456
14556
  }
14457
14557
  if (message && mimeType.startsWith("image/")) {
@@ -14460,7 +14560,7 @@ var FeishuAdapter = class _FeishuAdapter {
14460
14560
  for (const chunk of chunks) {
14461
14561
  await this.client.im.message.create({
14462
14562
  params: { receive_id_type: receiveIdType },
14463
- data: { receive_id: target, msg_type: "text", content: JSON.stringify({ text: chunk }) }
14563
+ data: { receive_id: safeTarget, msg_type: "text", content: JSON.stringify({ text: chunk }) }
14464
14564
  });
14465
14565
  }
14466
14566
  }
@@ -14504,6 +14604,28 @@ var FeishuAdapter = class _FeishuAdapter {
14504
14604
  if (!fileKey) throw new Error(`Feishu file upload failed: ${JSON.stringify(data)}`);
14505
14605
  return fileKey;
14506
14606
  }
14607
+ /** #217: 上传语音(opus)——飞书语音条必须 file_type=opus + duration(毫秒) */
14608
+ async _uploadAudio(buffer, filename, durationMs) {
14609
+ const token = await this._getTenantToken();
14610
+ const resp = await fetch("https://open.feishu.cn/open-apis/im/v1/files", {
14611
+ method: "POST",
14612
+ headers: {
14613
+ "Authorization": `Bearer ${token}`
14614
+ },
14615
+ body: (() => {
14616
+ const formData = new FormData();
14617
+ formData.append("file_type", "opus");
14618
+ formData.append("file_name", filename);
14619
+ formData.append("duration", String(Math.max(1, Math.round(durationMs))));
14620
+ formData.append("file", new Blob([buffer], { type: "audio/opus" }), filename);
14621
+ return formData;
14622
+ })()
14623
+ });
14624
+ const data = await resp.json();
14625
+ const fileKey = data?.data?.file_key;
14626
+ if (!fileKey) throw new Error(`Feishu audio upload failed: ${JSON.stringify(data)}`);
14627
+ return fileKey;
14628
+ }
14507
14629
  /** 通过飞书API下载消息中的图片 */
14508
14630
  async _downloadImage(messageId, imageKey) {
14509
14631
  const token = await this._getTenantToken();
@@ -17290,6 +17412,8 @@ var SessionManager = class {
17290
17412
  platformMapPath;
17291
17413
  /** 外部 hook:每次创建新 writer 时回调(如 EverOS sync 注入 onMessageWritten) */
17292
17414
  onWriterCreated = null;
17415
+ /** 外部 hook:判断 session 是否有活跃 query(idle cleanup 据此跳过,防止误关 query 正持有的 writer) */
17416
+ onIsSessionActive = null;
17293
17417
  histories = /* @__PURE__ */ new Map();
17294
17418
  restoredRecallPaths = /* @__PURE__ */ new Map();
17295
17419
  writers = /* @__PURE__ */ new Map();
@@ -17736,6 +17860,7 @@ var SessionManager = class {
17736
17860
  const now = Date.now();
17737
17861
  for (const [id, lastUsed] of this.lastUsed) {
17738
17862
  if (now - lastUsed > idleMs) {
17863
+ if (this.onIsSessionActive?.(id)) continue;
17739
17864
  const writer = this.writers.get(id);
17740
17865
  if (writer) {
17741
17866
  writer.close();
@@ -19086,8 +19211,11 @@ async function deepinfraRerank(query, episodes, rerankUrl, rerankApiKey, rerankM
19086
19211
  }
19087
19212
  var DEFAULT_MIN_SCORE2 = 0.5;
19088
19213
  async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /* @__PURE__ */ new Set(), options) {
19089
- const everosUrl = options?.everosUrl ?? "http://127.0.0.1:8100";
19090
- const userId = options?.userId ?? "xiaomei";
19214
+ const everosUrl = options?.everosUrl;
19215
+ const userId = options?.userId;
19216
+ if (!everosUrl || !userId) {
19217
+ throw new Error(`[memdir] everos recall: everosUrl/userId \u672A\u914D\u7F6E (everosUrl=${everosUrl}, userId=${userId})\uFF0C\u4E0D\u6267\u884C everos recall\uFF0C\u907F\u514D\u4E32\u5230\u522B\u4EBA\u7684\u5E93`);
19218
+ }
19091
19219
  const topK = options?.topK ?? 3;
19092
19220
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
19093
19221
  console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
@@ -19409,7 +19537,9 @@ ${text}` : text });
19409
19537
  } else {
19410
19538
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
19411
19539
  }
19412
- const messages = [...history, msg.user(userMsgContent)];
19540
+ const vcN = liveConfig.get("voiceChat.lightContextN") ?? 20;
19541
+ const lightHistory = channelName === "voice-chat" && history.length > vcN ? history.slice(-vcN) : history;
19542
+ const messages = [...lightHistory, msg.user(userMsgContent)];
19413
19543
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
19414
19544
  const delta = deps.mcpManager.getMcpDelta();
19415
19545
  if (delta && delta.addedBlocks.length > 0) {
@@ -19578,7 +19708,7 @@ ${text}` : text });
19578
19708
  const queryAbortController = new AbortController();
19579
19709
  engine.setExternalAbort(queryAbortController);
19580
19710
  setActiveQueryEngine(sessionId, engine);
19581
- const shouldSkipRecall = skipRecall ?? channelName === "cron";
19711
+ const shouldSkipRecall = skipRecall ?? (channelName === "cron" || channelName === "voice-chat");
19582
19712
  if (getFeature("topic-recall") !== false && !shouldSkipRecall) {
19583
19713
  try {
19584
19714
  const memoryDir = getAutoMemPath(workspace);
@@ -19626,31 +19756,49 @@ ${text}` : text });
19626
19756
  const recallP = deps.recallProvider;
19627
19757
  const recallMode = topics?.recall?.mode || "llm";
19628
19758
  let relevantMemories;
19629
- if (recallMode === "everos") {
19759
+ const effectiveText = textForMemory.replace(/\[图片来自[^\]]*\]|路径:\S+/g, "").trim();
19760
+ if (!effectiveText) {
19761
+ console.log("[handle-query] Memory recall skipped: image-only message (no effective text)");
19762
+ relevantMemories = [];
19763
+ } else if (recallMode === "everos") {
19630
19764
  const everosCfg = deps?.everosCfg;
19631
- relevantMemories = await findRelevantMemoriesEveros(
19632
- textForMemory,
19633
- memoryDir,
19634
- surfaced.paths,
19635
- everosCfg ? {
19636
- everosUrl: everosCfg.everosUrl || "http://127.0.0.1:8100",
19637
- userId: everosCfg.userId || "xiaomei",
19638
- rerankUrl: everosCfg.rerank?.baseUrl,
19639
- rerankApiKey: everosCfg.rerank?.apiKey,
19640
- rerankModel: everosCfg.rerank?.model,
19641
- rerankProvider: everosCfg.rerank?.provider,
19642
- minScore: topics?.recall?.minScore
19643
- } : void 0
19644
- );
19765
+ if (everosCfg && everosCfg.everosUrl && everosCfg.userId) {
19766
+ relevantMemories = await findRelevantMemoriesEveros(
19767
+ effectiveText,
19768
+ memoryDir,
19769
+ surfaced.paths,
19770
+ {
19771
+ everosUrl: everosCfg.everosUrl,
19772
+ userId: everosCfg.userId,
19773
+ rerankUrl: everosCfg.rerank?.baseUrl,
19774
+ rerankApiKey: everosCfg.rerank?.apiKey,
19775
+ rerankModel: everosCfg.rerank?.model,
19776
+ rerankProvider: everosCfg.rerank?.provider,
19777
+ minScore: topics?.recall?.minScore
19778
+ }
19779
+ );
19780
+ } else {
19781
+ console.log("[handle-query] everos recall skipped: everosCfg not configured, falling back to LLM recall");
19782
+ relevantMemories = await findRelevantMemories(
19783
+ effectiveText,
19784
+ memoryDir,
19785
+ recallP?.provider || provider,
19786
+ recallP?.model || model,
19787
+ queryAbortController.signal,
19788
+ surfaced.paths,
19789
+ recallP?.disableThinking,
19790
+ topics?.maxScanFiles
19791
+ );
19792
+ }
19645
19793
  } else if (recallMode === "vector") {
19646
19794
  relevantMemories = await findRelevantMemoriesVector(
19647
- textForMemory,
19795
+ effectiveText,
19648
19796
  memoryDir,
19649
19797
  surfaced.paths
19650
19798
  );
19651
19799
  } else {
19652
19800
  relevantMemories = await findRelevantMemories(
19653
- textForMemory,
19801
+ effectiveText,
19654
19802
  memoryDir,
19655
19803
  recallP?.provider || provider,
19656
19804
  recallP?.model || model,
@@ -20882,6 +21030,10 @@ var NudgePlugin = class {
20882
21030
  registerCallbackHook("Stop", {
20883
21031
  type: "callback",
20884
21032
  callback: async (input, _toolUseID, _signal) => {
21033
+ if (this.cfg.stopHookEnabled === false) {
21034
+ console.log("[stop-hook] disabled via /stophook off \u2014 skipping");
21035
+ return { outcome: { outcome: "success" } };
21036
+ }
20885
21037
  const mode = this.cfg.stopHookMode || "sync";
20886
21038
  if (mode === "async") {
20887
21039
  console.log("[stop-hook] async mode \u2014 firing judge in background, not blocking");
@@ -21156,26 +21308,28 @@ var NudgePlugin = class {
21156
21308
  console.log(`[nudge] ${due.length} stop-hook notification(s) due: ${due.map((n) => n.id).join(", ")}`);
21157
21309
  const items = due.map((n) => `[\u901A\u77E5ID: ${n.id}]
21158
21310
  \u4E0A\u6B21\u8BF4\uFF1A${n.description}`).join("\n\n");
21311
+ const hint = `\uFF08\u6E05\u7406\u5173\u952E\u5B57\u53EF\u7528\uFF1A\u8FC7\u671F\u4E86 / \u5DF2\u843D\u76D8 / resolved / \u5DF2\u5904\u7406 / \u5DF2\u95ED\u73AF\uFF0C\u5982 "${due[0].id} \u5DF2\u5904\u7406"\uFF09`;
21159
21312
  const desc = due.length === 1 ? `\u4F60\u4E4B\u524D\u5728\u7B49\u5F85\u67D0\u4E2A\u5916\u90E8\u6761\u4EF6\uFF0C\u65F6\u95F4\u5230\u4E86\uFF0C\u56DE\u53BB\u68C0\u67E5\uFF01
21160
21313
 
21161
21314
  ${items}
21162
21315
 
21163
- \u68C0\u67E5\u6761\u4EF6\u662F\u5426\u6EE1\u8DB3\uFF0C\u6EE1\u8DB3\u5C31\u7EE7\u7EED\u5E72\u6D3B\uFF0C\u4E0D\u6EE1\u8DB3\u5C31\u56DE\u590D"${due[0].id} \u8FC7\u671F\u4E86"\u544A\u8BC9 nudge \u7CBE\u786E\u6E05\u7406\u8FD9\u6761\u3002` : `\u4F60\u4E4B\u524D\u6709 ${due.length} \u4E2A\u7B49\u5F85\u4E2D\u7684\u5916\u90E8\u6761\u4EF6\u90FD\u5230\u671F\u4E86\uFF0C\u56DE\u53BB\u9010\u4E2A\u68C0\u67E5\uFF01
21316
+ \u68C0\u67E5\u6761\u4EF6\u662F\u5426\u6EE1\u8DB3\uFF0C\u6EE1\u8DB3\u5C31\u7EE7\u7EED\u5E72\u6D3B\uFF0C\u4E0D\u6EE1\u8DB3\u5C31\u56DE\u590D"${due[0].id} \u8FC7\u671F\u4E86"\u6216"${due[0].id} \u5DF2\u5904\u7406"\u544A\u8BC9 nudge \u7CBE\u786E\u6E05\u7406\u8FD9\u6761\u3002${hint}` : `\u4F60\u4E4B\u524D\u6709 ${due.length} \u4E2A\u7B49\u5F85\u4E2D\u7684\u5916\u90E8\u6761\u4EF6\u90FD\u5230\u671F\u4E86\uFF0C\u56DE\u53BB\u9010\u4E2A\u68C0\u67E5\uFF01
21164
21317
 
21165
21318
  ${items}
21166
21319
 
21167
- \u5BF9\u6BCF\u4E00\u6761\uFF1A\u6761\u4EF6\u6EE1\u8DB3\u5C31\u7EE7\u7EED\u5E72\u6D3B\uFF0C\u4E0D\u6EE1\u8DB3\u5C31\u56DE\u590D\u5BF9\u5E94\u7684"<\u901A\u77E5ID> \u8FC7\u671F\u4E86"\u544A\u8BC9 nudge \u7CBE\u786E\u6E05\u7406\u3002`;
21320
+ \u5BF9\u6BCF\u4E00\u6761\uFF1A\u6761\u4EF6\u6EE1\u8DB3\u5C31\u7EE7\u7EED\u5E72\u6D3B\uFF0C\u4E0D\u6EE1\u8DB3\u5C31\u56DE\u590D\u5BF9\u5E94\u7684"<\u901A\u77E5ID> \u8FC7\u671F\u4E86"\uFF08\u6216 \u5DF2\u5904\u7406/\u5DF2\u843D\u76D8/resolved/\u5DF2\u95ED\u73AF\uFF09\u544A\u8BC9 nudge \u7CBE\u786E\u6E05\u7406\u3002`;
21168
21321
  return { message: buildNudgeNotification("wake", desc), ids: due.map((n) => n.id) };
21169
21322
  } catch (e) {
21170
21323
  console.warn(`[nudge] collectDueStopHookNotifications error: ${e.message}`);
21171
21324
  return null;
21172
21325
  }
21173
21326
  }
21174
- /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个) */
21327
+ /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个)
21328
+ * 0816 翀哥:关键字多样化——过期了/已落盘/resolved/已处理/已闭环/已归档 都认 */
21175
21329
  extractWakeReplyIds(text) {
21176
21330
  if (!text) return [];
21177
21331
  const ids = [];
21178
- const re = /(wake-\d+-[a-z0-9]+)\s*过期了/g;
21332
+ const re = /(wake-\d+-[a-z0-9]+)\s*(?:过期了|已落盘|resolved|已处理|已闭环|已归档|已完成|done)/g;
21179
21333
  let m;
21180
21334
  while ((m = re.exec(text)) !== null) {
21181
21335
  if (!ids.includes(m[1])) ids.push(m[1]);
@@ -23618,8 +23772,9 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
23618
23772
  - \u4FDD\u6301\u7B80\u77ED\u53E3\u8BED\u5316\uFF08\u901A\u5E38 1-3 \u53E5\uFF09\uFF0C\u4E0D\u8981\u957F\u7BC7\u5927\u8BBA
23619
23773
  - \u7B2C\u4E00\u53E5\u63A7\u5236\u5728 4-6 \u4E2A\u5B57\uFF0C\u5C3D\u5FEB\u89E6\u53D1 TTS \u5408\u6210
23620
23774
  - \u8BF4\u8BDD\u50CF\u4EBA\u2014\u2014\u77ED\u53E5\uFF0C\u53BB\u670D\u52A1\u611F
23621
- - \u5982\u65E0\u5FC5\u8981\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\uFF0C\u76F4\u63A5\u56DE\u590D
23622
- - \u5982\u679C\u786E\u5B9E\u9700\u8981\u8C03\u7528\u5DE5\u5177\uFF08\u6BD4\u5982\u76F4\u64AD\u63A8\u6D41\u3001\u67E5\u4FE1\u606F\uFF09\uFF0C\u5148\u8BF4\u4E00\u53E5\u7B80\u77ED\u7684\u8BDD\u8BA9\u6211\u77E5\u9053\uFF0C\u6BD4\u5982"\u7A0D\u7B49\u6211\u770B\u770B"\u3001"\u7B49\u6211\u4E00\u4E0B"\uFF0C\u518D\u8C03\u7528\u5DE5\u5177`;
23775
+ - \u3010\u786C\u89C4\u5219\u3011\u8BED\u97F3\u5BF9\u8BDD\u4E2D\u7981\u6B62\u8C03\u7528 exec / SSH / \u8BFB\u5927\u6587\u4EF6\u7B49\u6162\u5DE5\u5177\u2014\u2014\u67E5\u6570\u636E\u7C7B\u95EE\u9898\u76F4\u63A5\u7528\u5634\u7B54"\u8FD9\u4E2A\u6211\u98DE\u4E66\u91CC\u67E5\u4E86\u544A\u8BC9\u4F60"
23776
+ - \u8BED\u97F3\u91CC\u5141\u8BB8\u7684\u5DE5\u5177\uFF1Acalendar\u3001msg_send\u3001read\uFF08\u8BFB\u76F4\u64AD\u8349\u7A3F/\u811A\u672C\u7528\uFF0C\u672C\u5730\u6587\u4EF6\u5FEB\uFF09\u2014\u2014\u4E0D\u8981\u7528 my_voice\uFF08\u8BED\u97F3\u5BF9\u8BDD\u672C\u8EAB\u5C31\u5728\u51FA\u58F0\uFF0C\u518D\u751F\u6210\u662F\u5957\u5A03\uFF09
23777
+ - \u5982\u679C\u5B9E\u5728\u9700\u8981\u67E5\uFF1A\u5148\u8BF4"\u7B49\u6211\u67E5\u4E0B"\u5E76\u6781\u7B80\u8C03\u7528\uFF0C\u67E5\u5B8C\u7ACB\u523B\u603B\u7ED3\u6210\u4E00\u53E5\u8BDD`;
23623
23778
  const engine = new QueryEngine(llmProvider, {
23624
23779
  model: modelId,
23625
23780
  systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
@@ -24333,7 +24488,22 @@ var DEFAULTS4 = {
24333
24488
  autoStart: true,
24334
24489
  defaultMode: "hybrid_agentic"
24335
24490
  };
24336
- function parseEverosConfig(raw) {
24491
+ function resolveProviderConfig(rawSection, providers, defaultModel, defaultApiKey, defaultBaseUrl) {
24492
+ if (!rawSection) {
24493
+ return { model: defaultModel, apiKey: defaultApiKey, baseUrl: defaultBaseUrl };
24494
+ }
24495
+ const providerName = rawSection.provider;
24496
+ const providerCfg = providerName ? providers?.[providerName] : void 0;
24497
+ return {
24498
+ provider: providerName,
24499
+ model: rawSection.model ?? defaultModel,
24500
+ // apiKey 从 provider 取(最易不一致,统一来源)
24501
+ // baseUrl/model 优先用 everos 节点自己的值(同一 provider 可能有不同 endpoint)
24502
+ apiKey: providerCfg?.apiKey ?? rawSection.apiKey ?? defaultApiKey,
24503
+ baseUrl: rawSection.baseUrl ?? providerCfg?.baseUrl ?? defaultBaseUrl
24504
+ };
24505
+ }
24506
+ function parseEverosConfig(raw, providers) {
24337
24507
  if (!raw) {
24338
24508
  return {
24339
24509
  enabled: false,
@@ -24353,8 +24523,20 @@ function parseEverosConfig(raw) {
24353
24523
  userId: raw.userId ?? "xiaomei",
24354
24524
  autoStart: raw.autoStart !== false,
24355
24525
  defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
24356
- llm: raw.llm ?? { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
24357
- rerank: raw.rerank ?? { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
24526
+ llm: resolveProviderConfig(
24527
+ raw.llm,
24528
+ providers,
24529
+ "glm-5.2",
24530
+ "",
24531
+ "https://open.bigmodel.cn/api/coding/paas/v4"
24532
+ ),
24533
+ rerank: resolveProviderConfig(
24534
+ raw.rerank,
24535
+ providers,
24536
+ "Qwen/Qwen3-Reranker-4B",
24537
+ "",
24538
+ "https://api.deepinfra.com/v1/inference"
24539
+ ),
24358
24540
  lancedbPath: raw.lancedbPath ?? "",
24359
24541
  sqlitePath: raw.sqlitePath ?? "",
24360
24542
  minScore: raw.minScore
@@ -24405,14 +24587,14 @@ var EverosSearchClient = class {
24405
24587
  const url = useAgentic ? `${this.agenticUrl}/api/v1/search` : `${this.everosUrl}/api/v1/memory/search`;
24406
24588
  const body = useAgentic ? JSON.stringify({
24407
24589
  query: params.query,
24408
- user_id: params.userId || "xiaomei",
24590
+ user_id: params.userId,
24409
24591
  mode,
24410
24592
  top_k: params.topK ?? 5,
24411
24593
  strategy: params.strategy || "multi_query"
24412
24594
  }) : JSON.stringify({
24413
24595
  query: params.query,
24414
- user_id: params.userId || "user",
24415
- app_id: "xiaomei",
24596
+ user_id: params.userId,
24597
+ app_id: params.appId,
24416
24598
  project_id: "default",
24417
24599
  top_k: params.topK ?? 5
24418
24600
  });
@@ -24450,8 +24632,8 @@ var EverosPlugin = class {
24450
24632
  healthTimer = null;
24451
24633
  weStartedAgentic = false;
24452
24634
  // 我们拉起的才管
24453
- constructor(rawConfig) {
24454
- this.config = parseEverosConfig(rawConfig);
24635
+ constructor(rawConfig, providers) {
24636
+ this.config = parseEverosConfig(rawConfig, providers);
24455
24637
  this.client = new EverosSearchClient(this.config.agenticUrl, this.config.everosUrl);
24456
24638
  }
24457
24639
  static shouldEnable(config2) {
@@ -25588,6 +25770,47 @@ async function compressWav(wavPath) {
25588
25770
  return wavPath;
25589
25771
  }
25590
25772
  }
25773
+ async function wavToOggOpus(wavPath) {
25774
+ const oggPath = wavPath.replace(/\.wav$/, ".ogg");
25775
+ await execFileAsync("ffmpeg", [
25776
+ "-y",
25777
+ "-i",
25778
+ wavPath,
25779
+ "-c:a",
25780
+ "libopus",
25781
+ "-b:a",
25782
+ "32k",
25783
+ "-ar",
25784
+ "24000",
25785
+ oggPath
25786
+ ], { timeout: 3e4 });
25787
+ if (!fs30.existsSync(oggPath) || fs30.statSync(oggPath).size < 20) {
25788
+ throw new Error("ogg/opus encode produced empty output");
25789
+ }
25790
+ fs30.unlinkSync(wavPath);
25791
+ return oggPath;
25792
+ }
25793
+ async function wavToSilk(wavPath) {
25794
+ fs30.mkdirSync(VOICE_DIR, { recursive: true });
25795
+ const silkPath = wavPath.replace(/\.wav$/, ".silk");
25796
+ const script = `
25797
+ import sys, io, wave, pysilk
25798
+ src, dst = sys.argv[1], sys.argv[2]
25799
+ with wave.open(src, 'rb') as wf:
25800
+ sr = wf.getframerate()
25801
+ pcm = wf.readframes(wf.getnframes())
25802
+ out = io.BytesIO()
25803
+ pysilk.encode(io.BytesIO(pcm), out, sample_rate=sr, bit_rate=24000, tencent=True)
25804
+ with open(dst, 'wb') as f:
25805
+ f.write(out.getvalue())
25806
+ print('OK')
25807
+ `;
25808
+ await execFileAsync("python3", ["-c", script, wavPath, silkPath], { timeout: 3e4 });
25809
+ if (!fs30.existsSync(silkPath) || fs30.statSync(silkPath).size < 20) {
25810
+ throw new Error("silk encode produced empty output");
25811
+ }
25812
+ return silkPath;
25813
+ }
25591
25814
  registry.register({
25592
25815
  name: "my_voice",
25593
25816
  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.",
@@ -25625,9 +25848,6 @@ registry.register({
25625
25848
  vc.workspaceId,
25626
25849
  vc.instruction
25627
25850
  );
25628
- if (audioPath.endsWith(".wav")) {
25629
- audioPath = await compressWav(audioPath);
25630
- }
25631
25851
  } catch (e) {
25632
25852
  console.warn(`[my-voice] cosyvoice failed (${e.message}), falling back to edge-tts`);
25633
25853
  audioPath = await ttsEdge(text);
@@ -25636,9 +25856,6 @@ registry.register({
25636
25856
  } else if (useEngine === "gptsovits") {
25637
25857
  try {
25638
25858
  audioPath = await ttsGptsovits(text);
25639
- if (audioPath.endsWith(".wav")) {
25640
- audioPath = await compressWav(audioPath);
25641
- }
25642
25859
  } catch (e) {
25643
25860
  console.warn(`[my-voice] gptsovits failed (${e.message}), falling back to edge-tts`);
25644
25861
  audioPath = await ttsEdge(text);
@@ -25650,12 +25867,30 @@ registry.register({
25650
25867
  } catch (e) {
25651
25868
  return { content: `TTS failed: ${e.message}`, isError: true };
25652
25869
  }
25870
+ const resolvedChannelRaw = args2.channel || ctx.channel || "feishu";
25871
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
25872
+ const target = ctx.channelTarget || ctx.from;
25873
+ if (audioPath.endsWith(".wav")) {
25874
+ if (resolvedChannel === "wechat") {
25875
+ try {
25876
+ audioPath = await wavToSilk(audioPath);
25877
+ } catch (e) {
25878
+ console.warn(`[my-voice] silk encode failed (${e.message}), falling back to m4a`);
25879
+ audioPath = await compressWav(audioPath);
25880
+ }
25881
+ } else if (resolvedChannel === "feishu") {
25882
+ try {
25883
+ audioPath = await wavToOggOpus(audioPath);
25884
+ } catch (e) {
25885
+ console.warn(`[my-voice] ogg/opus encode failed (${e.message}), falling back to m4a`);
25886
+ audioPath = await compressWav(audioPath);
25887
+ }
25888
+ }
25889
+ }
25653
25890
  const ext = path31.extname(audioPath).toLowerCase();
25654
- const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg" };
25891
+ const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg", ".silk": "audio/silk" };
25655
25892
  const mimeType = mimeMap[ext] || "audio/mpeg";
25656
25893
  const sizeKB = fs30.statSync(audioPath).size / 1024;
25657
- const resolvedChannel = args2.channel || ctx.channel || "feishu";
25658
- const target = ctx.channelTarget || ctx.from;
25659
25894
  try {
25660
25895
  await mgr.sendFile(resolvedChannel, target, caption, {
25661
25896
  path: audioPath,
@@ -25693,9 +25928,12 @@ function getProxyDispatcher2() {
25693
25928
  return void 0;
25694
25929
  }
25695
25930
  }
25696
- var FAL_KEY = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25697
25931
  var FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
25698
25932
  var DEFAULT_RESOLUTION = "1k";
25933
+ var FAL_KEY_FALLBACK = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25934
+ function getFalKey(cfg) {
25935
+ return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || FAL_KEY_FALLBACK;
25936
+ }
25699
25937
  var DEFAULT_REFERENCES = [
25700
25938
  { name: "default", p: "images/xiaomei_clean_v2.png" },
25701
25939
  { name: "v3", p: "images/xiaomei_clean_v3.png" },
@@ -25738,6 +25976,7 @@ function detectMode(input) {
25738
25976
  return "direct";
25739
25977
  }
25740
25978
  async function generateWithFal(imageB64, prompt, resolution) {
25979
+ const cfg = liveConfig.all();
25741
25980
  let aspectRatio;
25742
25981
  try {
25743
25982
  const refBuf = Buffer.from(imageB64, "base64");
@@ -25762,8 +26001,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25762
26001
  }
25763
26002
  }
25764
26003
  if (w > 0 && h > 0) {
25765
- aspectRatio = `${w}/${h}`;
25766
- console.log(`[my-selfie] ref image ${w}x${h}, aspect_ratio=${aspectRatio}`);
26004
+ console.log(`[my-selfie] ref image ${w}x${h}, aspect_ratio skipped`);
25767
26005
  }
25768
26006
  } catch (e) {
25769
26007
  console.warn(`[my-selfie] Failed to read ref dimensions: ${e.message}`);
@@ -25778,7 +26016,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25778
26016
  if (aspectRatio) body.aspect_ratio = aspectRatio;
25779
26017
  const res = await fetch(FAL_ENDPOINT, {
25780
26018
  method: "POST",
25781
- headers: { "Authorization": `Key ${FAL_KEY}`, "Content-Type": "application/json" },
26019
+ headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
25782
26020
  body: JSON.stringify(body)
25783
26021
  });
25784
26022
  if (!res.ok) {
@@ -28190,7 +28428,12 @@ ${content}`
28190
28428
  // tool 读自己配置用
28191
28429
  recallProvider: memoryRecallProvider || void 0,
28192
28430
  extractProvider: memoryExtractProvider || void 0,
28193
- everosCfg: config2.everos,
28431
+ everosCfg: config2.everos ? {
28432
+ ...config2.everos,
28433
+ // resolve provider 引用:从 providers 取 apiKey(rerank 在 handle-query.ts 里直接读 raw config)
28434
+ llm: config2.everos.llm?.provider && config2.providers?.[config2.everos.llm.provider] ? { ...config2.everos.llm, apiKey: config2.providers[config2.everos.llm.provider].apiKey } : config2.everos.llm,
28435
+ rerank: config2.everos.rerank?.provider && config2.providers?.[config2.everos.rerank.provider] ? { ...config2.everos.rerank, apiKey: config2.providers[config2.everos.rerank.provider].apiKey } : config2.everos.rerank
28436
+ } : void 0,
28194
28437
  mcpManager
28195
28438
  };
28196
28439
  if (visionEngine && visionConfig) {
@@ -28210,7 +28453,13 @@ ${content}`
28210
28453
  config: config2,
28211
28454
  // tool 读自己配置用
28212
28455
  recallProvider: memoryRecallProvider || void 0,
28213
- extractProvider: memoryExtractProvider || void 0
28456
+ extractProvider: memoryExtractProvider || void 0,
28457
+ everosCfg: config2.everos ? {
28458
+ ...config2.everos,
28459
+ // resolve provider 引用:从 providers 取 apiKey(跟主 deps 同逻辑)
28460
+ llm: config2.everos.llm?.provider && config2.providers?.[config2.everos.llm.provider] ? { ...config2.everos.llm, apiKey: config2.providers[config2.everos.llm.provider].apiKey } : config2.everos.llm,
28461
+ rerank: config2.everos.rerank?.provider && config2.providers?.[config2.everos.rerank.provider] ? { ...config2.everos.rerank, apiKey: config2.providers[config2.everos.rerank.provider].apiKey } : config2.everos.rerank
28462
+ } : void 0
28214
28463
  };
28215
28464
  }
28216
28465
  let modelOverride = null;
@@ -28259,7 +28508,8 @@ ${content}`
28259
28508
  channels: config2.channels,
28260
28509
  config: config2,
28261
28510
  recallProvider: memoryRecallProvider || void 0,
28262
- extractProvider: memoryExtractProvider || void 0
28511
+ extractProvider: memoryExtractProvider || void 0,
28512
+ everosCfg: deps?.everosCfg
28263
28513
  };
28264
28514
  }
28265
28515
  };
@@ -28301,11 +28551,13 @@ ${content}`
28301
28551
  systemPrompt,
28302
28552
  channels: config2.channels,
28303
28553
  recallProvider: memoryRecallProvider || void 0,
28304
- extractProvider: memoryExtractProvider || void 0
28554
+ extractProvider: memoryExtractProvider || void 0,
28555
+ everosCfg: deps?.everosCfg
28305
28556
  };
28306
28557
  }
28307
28558
  const dispatcher = new MessageDispatcher();
28308
28559
  deps.dispatcher = dispatcher;
28560
+ sessions.onIsSessionActive = (sid) => dispatcher.isActive(sid);
28309
28561
  setNotificationCallback((notif, route) => {
28310
28562
  const sid = route?.sessionId || sessions.getSessionId("scope:main") || "main";
28311
28563
  const chan = route?.channel;
@@ -28676,6 +28928,10 @@ ${notifications}
28676
28928
  { name: "session-memory", description: "\u67E5/\u5207 session-memory\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28677
28929
  { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28678
28930
  ] },
28931
+ // 8/17 stophook 总开关(翀哥要求)
28932
+ { name: "stophook", description: "\u67E5/\u5207 stop-hook\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28933
+ { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28934
+ ] },
28679
28935
  (() => {
28680
28936
  const aliases = config2.modelAliases || {};
28681
28937
  const aliasByRef = /* @__PURE__ */ new Map();
@@ -29104,6 +29360,29 @@ ${result.changes.map((c) => `- ${c}`).join("\n")}` : `\u274C Reload failed: ${re
29104
29360
  await ctx.reply(`\u2705 ${key}: **${cur}** \u2192 **${rawState}**`);
29105
29361
  return;
29106
29362
  }
29363
+ if (ctx.command === "stophook") {
29364
+ const rawState = (ctx.args.state || "").trim().toLowerCase();
29365
+ const getPath = "nudge.stopHookEnabled";
29366
+ const curVal = config2?.nudge?.stopHookEnabled;
29367
+ const cur = curVal === false ? "off" : "on";
29368
+ if (rawState === "") {
29369
+ await ctx.reply(`\u{1F4CA} stop-hook: **${cur}**\uFF08nudge \u63D2\u4EF6 stopHookEnabled=${String(curVal ?? "\u672A\u8BBE(\u9ED8\u8BA4on)")}\uFF09`);
29370
+ return;
29371
+ }
29372
+ if (rawState !== "on" && rawState !== "off") {
29373
+ await ctx.reply(`\u274C \u53EA\u63A5\u53D7 on \u6216 off
29374
+ \u5F53\u524D\uFF1Astop-hook = **${cur}**`);
29375
+ return;
29376
+ }
29377
+ if (rawState === cur) {
29378
+ await ctx.reply(`\u2139\uFE0F stop-hook \u5DF2\u7ECF\u662F **${cur}**`);
29379
+ return;
29380
+ }
29381
+ await liveConfig.set(getPath, rawState === "on");
29382
+ console.log(`[stophook] stop-hook ${cur} \u2192 ${rawState} (live + persisted)`);
29383
+ await ctx.reply(`\u2705 stop-hook: **${cur}** \u2192 **${rawState}**${rawState === "off" ? "\\n\uFF08\u8FDE\u73AF nudge \u9759\u9ED8\u4E2D\u2014\u2014wake-up/corrective \u5168\u505C\uFF0C\u8981\u7528\u518D /stophook on\uFF09" : ""}`);
29384
+ return;
29385
+ }
29107
29386
  if (ctx.command === "model") {
29108
29387
  const input = (ctx.args.model || "").trim();
29109
29388
  const aliases = config2.modelAliases || {};
@@ -29829,7 +30108,7 @@ ${pathStr}` }];
29829
30108
  }
29830
30109
  }
29831
30110
  if (config2.everos?.enabled) {
29832
- pluginManager.register(new EverosPlugin(config2.everos));
30111
+ pluginManager.register(new EverosPlugin(config2.everos, config2.providers));
29833
30112
  console.log("[everos] Plugin registered");
29834
30113
  }
29835
30114
  globalThis.__pluginManager = pluginManager;