engine7 7.1.36 → 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) }
14443
14517
  });
14444
14518
  } else {
14445
14519
  await this.client.im.message.create({
14446
14520
  params: { receive_id_type: receiveIdType },
14447
- data: { receive_id: target, msg_type: "image", content: JSON.stringify({ image_key: imageKey }) }
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 }) }
14532
+ });
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);
14545
+ await this.client.im.message.create({
14546
+ params: { receive_id_type: receiveIdType },
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();
@@ -14707,6 +14829,7 @@ var FeishuAdapter = class _FeishuAdapter {
14707
14829
  const message = data?.message;
14708
14830
  if (!sender || !message) return;
14709
14831
  const messageId = message.message_id || "";
14832
+ console.log(`[feishu] recv msg_type=${message.message_type} chat=${message.chat_type} from=${sender.sender_id?.open_id?.slice(0, 10)}`);
14710
14833
  if (this.recentMessageIds.has(messageId)) return;
14711
14834
  this.recentMessageIds.add(messageId);
14712
14835
  if (this.recentMessageIds.size > 500) {
@@ -14821,6 +14944,13 @@ var FeishuAdapter = class _FeishuAdapter {
14821
14944
  if (msgType === "file") {
14822
14945
  return { text: `[\u6587\u4EF6: ${parsed.file_name || "\u672A\u77E5\u6587\u4EF6"}]`, imageKeys, fileKey: parsed.file_key, fileName: parsed.file_name };
14823
14946
  }
14947
+ if (msgType === "share_location" || msgType === "location") {
14948
+ const name = parsed.name || "";
14949
+ const lat = parsed.latitude || "";
14950
+ const lng = parsed.longitude || "";
14951
+ const text = `[\u4F4D\u7F6E: ${name}${lat && lng ? ` (${lat}, ${lng})` : ""}]`;
14952
+ return { text, imageKeys };
14953
+ }
14824
14954
  if (msgType === "post") {
14825
14955
  const lines = [];
14826
14956
  if (Array.isArray(parsed.content)) {
@@ -17282,6 +17412,8 @@ var SessionManager = class {
17282
17412
  platformMapPath;
17283
17413
  /** 外部 hook:每次创建新 writer 时回调(如 EverOS sync 注入 onMessageWritten) */
17284
17414
  onWriterCreated = null;
17415
+ /** 外部 hook:判断 session 是否有活跃 query(idle cleanup 据此跳过,防止误关 query 正持有的 writer) */
17416
+ onIsSessionActive = null;
17285
17417
  histories = /* @__PURE__ */ new Map();
17286
17418
  restoredRecallPaths = /* @__PURE__ */ new Map();
17287
17419
  writers = /* @__PURE__ */ new Map();
@@ -17728,6 +17860,7 @@ var SessionManager = class {
17728
17860
  const now = Date.now();
17729
17861
  for (const [id, lastUsed] of this.lastUsed) {
17730
17862
  if (now - lastUsed > idleMs) {
17863
+ if (this.onIsSessionActive?.(id)) continue;
17731
17864
  const writer = this.writers.get(id);
17732
17865
  if (writer) {
17733
17866
  writer.close();
@@ -19078,8 +19211,11 @@ async function deepinfraRerank(query, episodes, rerankUrl, rerankApiKey, rerankM
19078
19211
  }
19079
19212
  var DEFAULT_MIN_SCORE2 = 0.5;
19080
19213
  async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /* @__PURE__ */ new Set(), options) {
19081
- const everosUrl = options?.everosUrl ?? "http://127.0.0.1:8100";
19082
- 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
+ }
19083
19219
  const topK = options?.topK ?? 3;
19084
19220
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
19085
19221
  console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
@@ -19401,7 +19537,9 @@ ${text}` : text });
19401
19537
  } else {
19402
19538
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
19403
19539
  }
19404
- 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)];
19405
19543
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
19406
19544
  const delta = deps.mcpManager.getMcpDelta();
19407
19545
  if (delta && delta.addedBlocks.length > 0) {
@@ -19570,7 +19708,7 @@ ${text}` : text });
19570
19708
  const queryAbortController = new AbortController();
19571
19709
  engine.setExternalAbort(queryAbortController);
19572
19710
  setActiveQueryEngine(sessionId, engine);
19573
- const shouldSkipRecall = skipRecall ?? channelName === "cron";
19711
+ const shouldSkipRecall = skipRecall ?? (channelName === "cron" || channelName === "voice-chat");
19574
19712
  if (getFeature("topic-recall") !== false && !shouldSkipRecall) {
19575
19713
  try {
19576
19714
  const memoryDir = getAutoMemPath(workspace);
@@ -19618,31 +19756,49 @@ ${text}` : text });
19618
19756
  const recallP = deps.recallProvider;
19619
19757
  const recallMode = topics?.recall?.mode || "llm";
19620
19758
  let relevantMemories;
19621
- 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") {
19622
19764
  const everosCfg = deps?.everosCfg;
19623
- relevantMemories = await findRelevantMemoriesEveros(
19624
- textForMemory,
19625
- memoryDir,
19626
- surfaced.paths,
19627
- everosCfg ? {
19628
- everosUrl: everosCfg.everosUrl || "http://127.0.0.1:8100",
19629
- userId: everosCfg.userId || "xiaomei",
19630
- rerankUrl: everosCfg.rerank?.baseUrl,
19631
- rerankApiKey: everosCfg.rerank?.apiKey,
19632
- rerankModel: everosCfg.rerank?.model,
19633
- rerankProvider: everosCfg.rerank?.provider,
19634
- minScore: topics?.recall?.minScore
19635
- } : void 0
19636
- );
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
+ }
19637
19793
  } else if (recallMode === "vector") {
19638
19794
  relevantMemories = await findRelevantMemoriesVector(
19639
- textForMemory,
19795
+ effectiveText,
19640
19796
  memoryDir,
19641
19797
  surfaced.paths
19642
19798
  );
19643
19799
  } else {
19644
19800
  relevantMemories = await findRelevantMemories(
19645
- textForMemory,
19801
+ effectiveText,
19646
19802
  memoryDir,
19647
19803
  recallP?.provider || provider,
19648
19804
  recallP?.model || model,
@@ -20874,6 +21030,10 @@ var NudgePlugin = class {
20874
21030
  registerCallbackHook("Stop", {
20875
21031
  type: "callback",
20876
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
+ }
20877
21037
  const mode = this.cfg.stopHookMode || "sync";
20878
21038
  if (mode === "async") {
20879
21039
  console.log("[stop-hook] async mode \u2014 firing judge in background, not blocking");
@@ -21148,26 +21308,28 @@ var NudgePlugin = class {
21148
21308
  console.log(`[nudge] ${due.length} stop-hook notification(s) due: ${due.map((n) => n.id).join(", ")}`);
21149
21309
  const items = due.map((n) => `[\u901A\u77E5ID: ${n.id}]
21150
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`;
21151
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
21152
21313
 
21153
21314
  ${items}
21154
21315
 
21155
- \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
21156
21317
 
21157
21318
  ${items}
21158
21319
 
21159
- \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`;
21160
21321
  return { message: buildNudgeNotification("wake", desc), ids: due.map((n) => n.id) };
21161
21322
  } catch (e) {
21162
21323
  console.warn(`[nudge] collectDueStopHookNotifications error: ${e.message}`);
21163
21324
  return null;
21164
21325
  }
21165
21326
  }
21166
- /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个) */
21327
+ /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个)
21328
+ * 0816 翀哥:关键字多样化——过期了/已落盘/resolved/已处理/已闭环/已归档 都认 */
21167
21329
  extractWakeReplyIds(text) {
21168
21330
  if (!text) return [];
21169
21331
  const ids = [];
21170
- const re = /(wake-\d+-[a-z0-9]+)\s*过期了/g;
21332
+ const re = /(wake-\d+-[a-z0-9]+)\s*(?:过期了|已落盘|resolved|已处理|已闭环|已归档|已完成|done)/g;
21171
21333
  let m;
21172
21334
  while ((m = re.exec(text)) !== null) {
21173
21335
  if (!ids.includes(m[1])) ids.push(m[1]);
@@ -23610,8 +23772,9 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
23610
23772
  - \u4FDD\u6301\u7B80\u77ED\u53E3\u8BED\u5316\uFF08\u901A\u5E38 1-3 \u53E5\uFF09\uFF0C\u4E0D\u8981\u957F\u7BC7\u5927\u8BBA
23611
23773
  - \u7B2C\u4E00\u53E5\u63A7\u5236\u5728 4-6 \u4E2A\u5B57\uFF0C\u5C3D\u5FEB\u89E6\u53D1 TTS \u5408\u6210
23612
23774
  - \u8BF4\u8BDD\u50CF\u4EBA\u2014\u2014\u77ED\u53E5\uFF0C\u53BB\u670D\u52A1\u611F
23613
- - \u5982\u65E0\u5FC5\u8981\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\uFF0C\u76F4\u63A5\u56DE\u590D
23614
- - \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`;
23615
23778
  const engine = new QueryEngine(llmProvider, {
23616
23779
  model: modelId,
23617
23780
  systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
@@ -24325,7 +24488,22 @@ var DEFAULTS4 = {
24325
24488
  autoStart: true,
24326
24489
  defaultMode: "hybrid_agentic"
24327
24490
  };
24328
- 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) {
24329
24507
  if (!raw) {
24330
24508
  return {
24331
24509
  enabled: false,
@@ -24345,8 +24523,20 @@ function parseEverosConfig(raw) {
24345
24523
  userId: raw.userId ?? "xiaomei",
24346
24524
  autoStart: raw.autoStart !== false,
24347
24525
  defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
24348
- llm: raw.llm ?? { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
24349
- 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
+ ),
24350
24540
  lancedbPath: raw.lancedbPath ?? "",
24351
24541
  sqlitePath: raw.sqlitePath ?? "",
24352
24542
  minScore: raw.minScore
@@ -24397,14 +24587,14 @@ var EverosSearchClient = class {
24397
24587
  const url = useAgentic ? `${this.agenticUrl}/api/v1/search` : `${this.everosUrl}/api/v1/memory/search`;
24398
24588
  const body = useAgentic ? JSON.stringify({
24399
24589
  query: params.query,
24400
- user_id: params.userId || "xiaomei",
24590
+ user_id: params.userId,
24401
24591
  mode,
24402
24592
  top_k: params.topK ?? 5,
24403
24593
  strategy: params.strategy || "multi_query"
24404
24594
  }) : JSON.stringify({
24405
24595
  query: params.query,
24406
- user_id: params.userId || "user",
24407
- app_id: "xiaomei",
24596
+ user_id: params.userId,
24597
+ app_id: params.appId,
24408
24598
  project_id: "default",
24409
24599
  top_k: params.topK ?? 5
24410
24600
  });
@@ -24442,8 +24632,8 @@ var EverosPlugin = class {
24442
24632
  healthTimer = null;
24443
24633
  weStartedAgentic = false;
24444
24634
  // 我们拉起的才管
24445
- constructor(rawConfig) {
24446
- this.config = parseEverosConfig(rawConfig);
24635
+ constructor(rawConfig, providers) {
24636
+ this.config = parseEverosConfig(rawConfig, providers);
24447
24637
  this.client = new EverosSearchClient(this.config.agenticUrl, this.config.everosUrl);
24448
24638
  }
24449
24639
  static shouldEnable(config2) {
@@ -25580,6 +25770,47 @@ async function compressWav(wavPath) {
25580
25770
  return wavPath;
25581
25771
  }
25582
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
+ }
25583
25814
  registry.register({
25584
25815
  name: "my_voice",
25585
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.",
@@ -25617,9 +25848,6 @@ registry.register({
25617
25848
  vc.workspaceId,
25618
25849
  vc.instruction
25619
25850
  );
25620
- if (audioPath.endsWith(".wav")) {
25621
- audioPath = await compressWav(audioPath);
25622
- }
25623
25851
  } catch (e) {
25624
25852
  console.warn(`[my-voice] cosyvoice failed (${e.message}), falling back to edge-tts`);
25625
25853
  audioPath = await ttsEdge(text);
@@ -25628,9 +25856,6 @@ registry.register({
25628
25856
  } else if (useEngine === "gptsovits") {
25629
25857
  try {
25630
25858
  audioPath = await ttsGptsovits(text);
25631
- if (audioPath.endsWith(".wav")) {
25632
- audioPath = await compressWav(audioPath);
25633
- }
25634
25859
  } catch (e) {
25635
25860
  console.warn(`[my-voice] gptsovits failed (${e.message}), falling back to edge-tts`);
25636
25861
  audioPath = await ttsEdge(text);
@@ -25642,12 +25867,30 @@ registry.register({
25642
25867
  } catch (e) {
25643
25868
  return { content: `TTS failed: ${e.message}`, isError: true };
25644
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
+ }
25645
25890
  const ext = path31.extname(audioPath).toLowerCase();
25646
- 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" };
25647
25892
  const mimeType = mimeMap[ext] || "audio/mpeg";
25648
25893
  const sizeKB = fs30.statSync(audioPath).size / 1024;
25649
- const resolvedChannel = args2.channel || ctx.channel || "feishu";
25650
- const target = ctx.channelTarget || ctx.from;
25651
25894
  try {
25652
25895
  await mgr.sendFile(resolvedChannel, target, caption, {
25653
25896
  path: audioPath,
@@ -25685,9 +25928,12 @@ function getProxyDispatcher2() {
25685
25928
  return void 0;
25686
25929
  }
25687
25930
  }
25688
- var FAL_KEY = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25689
25931
  var FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
25690
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
+ }
25691
25937
  var DEFAULT_REFERENCES = [
25692
25938
  { name: "default", p: "images/xiaomei_clean_v2.png" },
25693
25939
  { name: "v3", p: "images/xiaomei_clean_v3.png" },
@@ -25730,6 +25976,7 @@ function detectMode(input) {
25730
25976
  return "direct";
25731
25977
  }
25732
25978
  async function generateWithFal(imageB64, prompt, resolution) {
25979
+ const cfg = liveConfig.all();
25733
25980
  let aspectRatio;
25734
25981
  try {
25735
25982
  const refBuf = Buffer.from(imageB64, "base64");
@@ -25754,8 +26001,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25754
26001
  }
25755
26002
  }
25756
26003
  if (w > 0 && h > 0) {
25757
- aspectRatio = `${w}/${h}`;
25758
- 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`);
25759
26005
  }
25760
26006
  } catch (e) {
25761
26007
  console.warn(`[my-selfie] Failed to read ref dimensions: ${e.message}`);
@@ -25770,7 +26016,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25770
26016
  if (aspectRatio) body.aspect_ratio = aspectRatio;
25771
26017
  const res = await fetch(FAL_ENDPOINT, {
25772
26018
  method: "POST",
25773
- headers: { "Authorization": `Key ${FAL_KEY}`, "Content-Type": "application/json" },
26019
+ headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
25774
26020
  body: JSON.stringify(body)
25775
26021
  });
25776
26022
  if (!res.ok) {
@@ -28182,7 +28428,12 @@ ${content}`
28182
28428
  // tool 读自己配置用
28183
28429
  recallProvider: memoryRecallProvider || void 0,
28184
28430
  extractProvider: memoryExtractProvider || void 0,
28185
- 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,
28186
28437
  mcpManager
28187
28438
  };
28188
28439
  if (visionEngine && visionConfig) {
@@ -28202,7 +28453,13 @@ ${content}`
28202
28453
  config: config2,
28203
28454
  // tool 读自己配置用
28204
28455
  recallProvider: memoryRecallProvider || void 0,
28205
- 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
28206
28463
  };
28207
28464
  }
28208
28465
  let modelOverride = null;
@@ -28251,7 +28508,8 @@ ${content}`
28251
28508
  channels: config2.channels,
28252
28509
  config: config2,
28253
28510
  recallProvider: memoryRecallProvider || void 0,
28254
- extractProvider: memoryExtractProvider || void 0
28511
+ extractProvider: memoryExtractProvider || void 0,
28512
+ everosCfg: deps?.everosCfg
28255
28513
  };
28256
28514
  }
28257
28515
  };
@@ -28293,11 +28551,13 @@ ${content}`
28293
28551
  systemPrompt,
28294
28552
  channels: config2.channels,
28295
28553
  recallProvider: memoryRecallProvider || void 0,
28296
- extractProvider: memoryExtractProvider || void 0
28554
+ extractProvider: memoryExtractProvider || void 0,
28555
+ everosCfg: deps?.everosCfg
28297
28556
  };
28298
28557
  }
28299
28558
  const dispatcher = new MessageDispatcher();
28300
28559
  deps.dispatcher = dispatcher;
28560
+ sessions.onIsSessionActive = (sid) => dispatcher.isActive(sid);
28301
28561
  setNotificationCallback((notif, route) => {
28302
28562
  const sid = route?.sessionId || sessions.getSessionId("scope:main") || "main";
28303
28563
  const chan = route?.channel;
@@ -28668,6 +28928,10 @@ ${notifications}
28668
28928
  { name: "session-memory", description: "\u67E5/\u5207 session-memory\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28669
28929
  { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28670
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
+ ] },
28671
28935
  (() => {
28672
28936
  const aliases = config2.modelAliases || {};
28673
28937
  const aliasByRef = /* @__PURE__ */ new Map();
@@ -29096,6 +29360,29 @@ ${result.changes.map((c) => `- ${c}`).join("\n")}` : `\u274C Reload failed: ${re
29096
29360
  await ctx.reply(`\u2705 ${key}: **${cur}** \u2192 **${rawState}**`);
29097
29361
  return;
29098
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
+ }
29099
29386
  if (ctx.command === "model") {
29100
29387
  const input = (ctx.args.model || "").trim();
29101
29388
  const aliases = config2.modelAliases || {};
@@ -29821,7 +30108,7 @@ ${pathStr}` }];
29821
30108
  }
29822
30109
  }
29823
30110
  if (config2.everos?.enabled) {
29824
- pluginManager.register(new EverosPlugin(config2.everos));
30111
+ pluginManager.register(new EverosPlugin(config2.everos, config2.providers));
29825
30112
  console.log("[everos] Plugin registered");
29826
30113
  }
29827
30114
  globalThis.__pluginManager = pluginManager;