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.
@@ -816,6 +816,9 @@ function toHookContext(ctx) {
816
816
  };
817
817
  }
818
818
  async function executeOne(tc, ctx, signal) {
819
+ if (signal?.aborted) {
820
+ return { call: tc, result: { content: "[interrupted by user /stop]", isError: false }, summary: null };
821
+ }
819
822
  const tool = registry.get(tc.function.name);
820
823
  if (!tool) {
821
824
  return { call: tc, result: { content: `Unknown tool: ${tc.function.name}`, isError: true }, summary: null };
@@ -2113,6 +2116,85 @@ var init_compact2 = __esm({
2113
2116
  }
2114
2117
  });
2115
2118
 
2119
+ // src/config/live.ts
2120
+ var live_exports = {};
2121
+ __export(live_exports, {
2122
+ liveConfig: () => liveConfig
2123
+ });
2124
+ var LiveConfigClass, liveConfig;
2125
+ var init_live = __esm({
2126
+ "src/config/live.ts"() {
2127
+ "use strict";
2128
+ LiveConfigClass = class {
2129
+ current = null;
2130
+ /** 启动时注入(替代 registry.config = config) */
2131
+ init(config) {
2132
+ this.current = config;
2133
+ }
2134
+ /** 取整个 config 对象(只读引用,不要缓存) */
2135
+ all() {
2136
+ if (!this.current) {
2137
+ throw new Error("[liveConfig] not initialized \u2014 call liveConfig.init() first");
2138
+ }
2139
+ return this.current;
2140
+ }
2141
+ /** 安全取子段('services.voice-chat.start' → current.services.voice-chat.start) */
2142
+ get(dotPath) {
2143
+ if (!this.current) return void 0;
2144
+ return dotPath.split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], this.current);
2145
+ }
2146
+ /** reload 时原地更新(Object.assign 保持引用不变) */
2147
+ assign(newConfig) {
2148
+ if (!this.current) {
2149
+ this.current = newConfig;
2150
+ return;
2151
+ }
2152
+ Object.assign(this.current, newConfig);
2153
+ }
2154
+ /** 改活树上的值;文件承载路径同步持久化(read-modify-write 回 config 文件) */
2155
+ async set(dotPath, val) {
2156
+ if (!this.current) throw new Error("[liveConfig] not initialized \u2014 call liveConfig.init() first");
2157
+ const keys = dotPath.split(".");
2158
+ let obj = this.current;
2159
+ for (let i = 0; i < keys.length - 1; i++) {
2160
+ if (obj[keys[i]] == null) obj[keys[i]] = {};
2161
+ obj = obj[keys[i]];
2162
+ }
2163
+ obj[keys[keys.length - 1]] = val;
2164
+ await this.persistToFile(dotPath, val);
2165
+ }
2166
+ /** 把改动写回 config 文件对应段(找不到文件路径则只内存生效) */
2167
+ async persistToFile(dotPath, val) {
2168
+ const fs43 = await import("node:fs");
2169
+ const cfgPath = this.current?._configFilePath;
2170
+ if (!cfgPath || !fs43.existsSync(cfgPath)) {
2171
+ console.warn(`[liveConfig] set: no config file path, in-memory only (${dotPath})`);
2172
+ return;
2173
+ }
2174
+ try {
2175
+ const raw = JSON.parse(fs43.readFileSync(cfgPath, "utf-8"));
2176
+ const keys = dotPath.split(".");
2177
+ let o = raw;
2178
+ for (let i = 0; i < keys.length - 1; i++) {
2179
+ if (o[keys[i]] == null) o[keys[i]] = {};
2180
+ o = o[keys[i]];
2181
+ }
2182
+ o[keys[keys.length - 1]] = val;
2183
+ fs43.writeFileSync(cfgPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
2184
+ console.log(`[liveConfig] persisted ${dotPath} = ${JSON.stringify(val)} to ${cfgPath}`);
2185
+ } catch (e) {
2186
+ console.warn(`[liveConfig] persist failed (${dotPath}): ${e.message}`);
2187
+ }
2188
+ }
2189
+ /** 是否已初始化 */
2190
+ isReady() {
2191
+ return this.current !== null;
2192
+ }
2193
+ };
2194
+ liveConfig = new LiveConfigClass();
2195
+ }
2196
+ });
2197
+
2116
2198
  // src/memory/memdir/paths.ts
2117
2199
  var paths_exports = {};
2118
2200
  __export(paths_exports, {
@@ -2805,7 +2887,8 @@ var init_query = __esm({
2805
2887
  init_constants();
2806
2888
  init_deferred();
2807
2889
  init_hooks();
2808
- QueryEngine = class {
2890
+ init_live();
2891
+ QueryEngine = class _QueryEngine {
2809
2892
  constructor(provider, options) {
2810
2893
  this.provider = provider;
2811
2894
  this.options = options;
@@ -2816,6 +2899,33 @@ var init_query = __esm({
2816
2899
  }
2817
2900
  provider;
2818
2901
  options;
2902
+ // === voice-chat 占位状态(#194 v4)===
2903
+ static placeholderQueries = /* @__PURE__ */ new Map();
2904
+ // queryId → ts(3 分钟过期)
2905
+ static placeholderCounter = 0;
2906
+ /** 工具>5s 触发:同一 query 只占位一次 + 30% 概率 + 轮换文案(防口头禅) */
2907
+ static async tryVoicePlaceholder(context) {
2908
+ if (context?.channel !== "voice-chat") return;
2909
+ if (!(liveConfig.get("voiceChat.placeholder.enabled") ?? true)) return;
2910
+ const qid = context.sessionId || "default";
2911
+ const now = Date.now();
2912
+ for (const [k, v] of _QueryEngine.placeholderQueries) if (now - v > 3 * 60 * 1e3) _QueryEngine.placeholderQueries.delete(k);
2913
+ if (_QueryEngine.placeholderQueries.has(qid)) return;
2914
+ if (Math.random() > 0.3) return;
2915
+ _QueryEngine.placeholderQueries.set(qid, now);
2916
+ 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"];
2917
+ const line = lines[_QueryEngine.placeholderCounter++ % lines.length];
2918
+ try {
2919
+ await fetch("https://localhost:8116/voice-reply", {
2920
+ method: "POST",
2921
+ headers: { "Content-Type": "application/json" },
2922
+ body: JSON.stringify({ text: line, isPlaceholder: true })
2923
+ });
2924
+ console.log(`[vc-placeholder] sent: ${line}`);
2925
+ } catch (e) {
2926
+ console.warn(`[vc-placeholder] send failed: ${e.message}`);
2927
+ }
2928
+ }
2819
2929
  abortController = null;
2820
2930
  /** 外部 pre-query abort controller(handle-query 入口注册,弥补 query() 前的空窗期) */
2821
2931
  preQueryAbort = null;
@@ -3170,7 +3280,18 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3170
3280
  console.warn(`[hook] PreToolUse ${toolName} error: ${e.message}`);
3171
3281
  }
3172
3282
  }
3173
- const { messages: toolResults, summaries } = await executeTools(toolCalls, ac.signal, toolCtx);
3283
+ const vcPlaceholderOn = context?.channel === "voice-chat" && (liveConfig.get("voiceChat.placeholder.enabled") ?? true);
3284
+ const vcTimer = vcPlaceholderOn ? setTimeout(() => {
3285
+ _QueryEngine.tryVoicePlaceholder(context).catch(() => {
3286
+ });
3287
+ }, 5e3) : null;
3288
+ let toolRes;
3289
+ try {
3290
+ toolRes = await executeTools(toolCalls, ac.signal, toolCtx);
3291
+ } finally {
3292
+ if (vcTimer) clearTimeout(vcTimer);
3293
+ }
3294
+ const { messages: toolResults, summaries } = toolRes;
3174
3295
  for (const s of summaries) {
3175
3296
  try {
3176
3297
  const postResult = await executePostToolUseHooks(s.tool, {}, s.summary, hookCtx, ac.signal);
@@ -3310,85 +3431,6 @@ ${perTurnSystemDynamic}` : deferredHint || perTurnSystemDynamic;
3310
3431
  }
3311
3432
  });
3312
3433
 
3313
- // src/config/live.ts
3314
- var live_exports = {};
3315
- __export(live_exports, {
3316
- liveConfig: () => liveConfig
3317
- });
3318
- var LiveConfigClass, liveConfig;
3319
- var init_live = __esm({
3320
- "src/config/live.ts"() {
3321
- "use strict";
3322
- LiveConfigClass = class {
3323
- current = null;
3324
- /** 启动时注入(替代 registry.config = config) */
3325
- init(config) {
3326
- this.current = config;
3327
- }
3328
- /** 取整个 config 对象(只读引用,不要缓存) */
3329
- all() {
3330
- if (!this.current) {
3331
- throw new Error("[liveConfig] not initialized \u2014 call liveConfig.init() first");
3332
- }
3333
- return this.current;
3334
- }
3335
- /** 安全取子段('services.voice-chat.start' → current.services.voice-chat.start) */
3336
- get(dotPath) {
3337
- if (!this.current) return void 0;
3338
- return dotPath.split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], this.current);
3339
- }
3340
- /** reload 时原地更新(Object.assign 保持引用不变) */
3341
- assign(newConfig) {
3342
- if (!this.current) {
3343
- this.current = newConfig;
3344
- return;
3345
- }
3346
- Object.assign(this.current, newConfig);
3347
- }
3348
- /** 改活树上的值;文件承载路径同步持久化(read-modify-write 回 config 文件) */
3349
- async set(dotPath, val) {
3350
- if (!this.current) throw new Error("[liveConfig] not initialized \u2014 call liveConfig.init() first");
3351
- const keys = dotPath.split(".");
3352
- let obj = this.current;
3353
- for (let i = 0; i < keys.length - 1; i++) {
3354
- if (obj[keys[i]] == null) obj[keys[i]] = {};
3355
- obj = obj[keys[i]];
3356
- }
3357
- obj[keys[keys.length - 1]] = val;
3358
- await this.persistToFile(dotPath, val);
3359
- }
3360
- /** 把改动写回 config 文件对应段(找不到文件路径则只内存生效) */
3361
- async persistToFile(dotPath, val) {
3362
- const fs43 = await import("node:fs");
3363
- const cfgPath = this.current?._configFilePath;
3364
- if (!cfgPath || !fs43.existsSync(cfgPath)) {
3365
- console.warn(`[liveConfig] set: no config file path, in-memory only (${dotPath})`);
3366
- return;
3367
- }
3368
- try {
3369
- const raw = JSON.parse(fs43.readFileSync(cfgPath, "utf-8"));
3370
- const keys = dotPath.split(".");
3371
- let o = raw;
3372
- for (let i = 0; i < keys.length - 1; i++) {
3373
- if (o[keys[i]] == null) o[keys[i]] = {};
3374
- o = o[keys[i]];
3375
- }
3376
- o[keys[keys.length - 1]] = val;
3377
- fs43.writeFileSync(cfgPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
3378
- console.log(`[liveConfig] persisted ${dotPath} = ${JSON.stringify(val)} to ${cfgPath}`);
3379
- } catch (e) {
3380
- console.warn(`[liveConfig] persist failed (${dotPath}): ${e.message}`);
3381
- }
3382
- }
3383
- /** 是否已初始化 */
3384
- isReady() {
3385
- return this.current !== null;
3386
- }
3387
- };
3388
- liveConfig = new LiveConfigClass();
3389
- }
3390
- });
3391
-
3392
3434
  // src/config/features.ts
3393
3435
  function getFeature(key) {
3394
3436
  const v = liveConfig.get(`agents.defaults.features.${key}`);
@@ -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
  };
@@ -12352,6 +12394,7 @@ var BASE_DELAY_MS = 500;
12352
12394
  var MAX_DELAY_MS = 32e3;
12353
12395
  var DEFAULT_TIMEOUT_MS = 6e5;
12354
12396
  var READ_TIMEOUT_MS = 6e4;
12397
+ var FIRST_READ_TIMEOUT_MS = parseInt(process.env.API_TIMEOUT_MS || "", 10) || 6e5;
12355
12398
  function sleep(ms, signal) {
12356
12399
  return new Promise((resolve10, reject) => {
12357
12400
  if (signal?.aborted) {
@@ -12615,7 +12658,15 @@ var OpenAIProvider = class {
12615
12658
  try {
12616
12659
  while (true) {
12617
12660
  if (params.signal?.aborted) break;
12618
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
12661
+ let readRes;
12662
+ try {
12663
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
12664
+ } catch (readErr) {
12665
+ if (params.signal?.aborted) throw readErr;
12666
+ yield { type: "error", error: readErr.message };
12667
+ return;
12668
+ }
12669
+ const { done: done2, value } = readRes;
12619
12670
  if (done2) break;
12620
12671
  if (firstChunk) {
12621
12672
  firstChunk = false;
@@ -12899,6 +12950,7 @@ var AnthropicProvider = class {
12899
12950
  const reader = response.body.getReader();
12900
12951
  const decoder = new TextDecoder();
12901
12952
  let buffer = "";
12953
+ let firstChunk = true;
12902
12954
  const toolUseBlocks = /* @__PURE__ */ new Map();
12903
12955
  let doneYielded = false;
12904
12956
  const handleData = function* (data) {
@@ -12988,8 +13040,20 @@ var AnthropicProvider = class {
12988
13040
  try {
12989
13041
  while (true) {
12990
13042
  if (params.signal?.aborted) break;
12991
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
13043
+ let readRes;
13044
+ try {
13045
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
13046
+ } catch (readErr) {
13047
+ if (params.signal?.aborted) throw readErr;
13048
+ yield { type: "error", error: readErr.message };
13049
+ return;
13050
+ }
13051
+ const { done: done2, value } = readRes;
12992
13052
  if (done2) break;
13053
+ if (firstChunk) {
13054
+ firstChunk = false;
13055
+ console.log(`[anthropic] first chunk received (${value?.length ?? 0}B)`);
13056
+ }
12993
13057
  buffer += decoder.decode(value, { stream: true });
12994
13058
  const lines = buffer.split("\n");
12995
13059
  buffer = lines.pop();
@@ -13201,7 +13265,15 @@ var GeminiProvider = class {
13201
13265
  try {
13202
13266
  while (true) {
13203
13267
  if (params.signal?.aborted) break;
13204
- const { done: done2, value } = await readWithTimeout(reader, READ_TIMEOUT_MS, params.signal);
13268
+ let readRes;
13269
+ try {
13270
+ readRes = await readWithTimeout(reader, firstChunk ? FIRST_READ_TIMEOUT_MS : READ_TIMEOUT_MS, params.signal);
13271
+ } catch (readErr) {
13272
+ if (params.signal?.aborted) throw readErr;
13273
+ yield { type: "error", error: readErr.message };
13274
+ return;
13275
+ }
13276
+ const { done: done2, value } = readRes;
13205
13277
  if (done2) break;
13206
13278
  if (firstChunk) {
13207
13279
  firstChunk = false;
@@ -14057,7 +14129,9 @@ var FeishuAdapter = class _FeishuAdapter {
14057
14129
  }
14058
14130
  const filename = attachment.filename || path46.basename(attachment.path);
14059
14131
  const fileBuffer = fs43.readFileSync(attachment.path);
14060
- const receiveIdType = target.startsWith("ou_") ? "open_id" : "chat_id";
14132
+ const safeTarget = target || this.config.defaultTarget || "";
14133
+ if (!safeTarget) throw new Error("Feishu sendFile: no target (\u4E3B\u52A8\u53D1\u9001\u9700\u914D\u7F6E defaultTarget \u6216\u4F20\u5165 target)");
14134
+ const receiveIdType = safeTarget.startsWith("ou_") ? "open_id" : "chat_id";
14061
14135
  const mimeType = attachment.mimeType || "application/octet-stream";
14062
14136
  if (mimeType.startsWith("image/")) {
14063
14137
  const imageKey = await this._uploadImage(fileBuffer, filename);
@@ -14065,19 +14139,45 @@ var FeishuAdapter = class _FeishuAdapter {
14065
14139
  const postContent = this._buildImagePost(message, imageKey);
14066
14140
  await this.client.im.message.create({
14067
14141
  params: { receive_id_type: receiveIdType },
14068
- data: { receive_id: target, msg_type: "post", content: JSON.stringify(postContent) }
14142
+ data: { receive_id: safeTarget, msg_type: "post", content: JSON.stringify(postContent) }
14069
14143
  });
14070
14144
  } else {
14071
14145
  await this.client.im.message.create({
14072
14146
  params: { receive_id_type: receiveIdType },
14073
- data: { receive_id: target, msg_type: "image", content: JSON.stringify({ image_key: imageKey }) }
14147
+ data: { receive_id: safeTarget, msg_type: "image", content: JSON.stringify({ image_key: imageKey }) }
14148
+ });
14149
+ }
14150
+ } else if (mimeType.startsWith("audio/")) {
14151
+ const isOpus = filename.endsWith(".opus") || filename.endsWith(".ogg") || mimeType === "audio/opus";
14152
+ if (!isOpus) {
14153
+ console.warn(`[feishu:sendFile] audio '${filename}' \u4E0D\u662Fopus\uFF0C\u964D\u7EA7\u4E3A\u6587\u4EF6\u6D88\u606F\uFF08\u975E\u8BED\u97F3\u6761\uFF09`);
14154
+ const fileKey = await this._uploadFile(fileBuffer, filename);
14155
+ await this.client.im.message.create({
14156
+ params: { receive_id_type: receiveIdType },
14157
+ data: { receive_id: safeTarget, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14158
+ });
14159
+ } else {
14160
+ let durationMs = Math.round(fileBuffer.length / 3);
14161
+ try {
14162
+ const { execFile: execFile3 } = await import("node:child_process");
14163
+ const { promisify: promisify3 } = await import("node:util");
14164
+ const execFileAsync2 = promisify3(execFile3);
14165
+ const probe = await execFileAsync2("ffprobe", ["-v", "quiet", "-show_format", "-print_format", "json", attachment.path], { timeout: 1e4 });
14166
+ const fmt = JSON.parse(probe.stdout);
14167
+ if (fmt?.format?.duration) durationMs = Math.round(parseFloat(fmt.format.duration) * 1e3);
14168
+ } catch {
14169
+ }
14170
+ const audioKey = await this._uploadAudio(fileBuffer, filename.endsWith(".opus") ? filename : filename.replace(/\.[^.]+$/, ".opus"), durationMs);
14171
+ await this.client.im.message.create({
14172
+ params: { receive_id_type: receiveIdType },
14173
+ data: { receive_id: safeTarget, msg_type: "audio", content: JSON.stringify({ file_key: audioKey }) }
14074
14174
  });
14075
14175
  }
14076
14176
  } else {
14077
14177
  const fileKey = await this._uploadFile(fileBuffer, filename);
14078
14178
  await this.client.im.message.create({
14079
14179
  params: { receive_id_type: receiveIdType },
14080
- data: { receive_id: target, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14180
+ data: { receive_id: safeTarget, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) }
14081
14181
  });
14082
14182
  }
14083
14183
  if (message && mimeType.startsWith("image/")) {
@@ -14086,7 +14186,7 @@ var FeishuAdapter = class _FeishuAdapter {
14086
14186
  for (const chunk of chunks) {
14087
14187
  await this.client.im.message.create({
14088
14188
  params: { receive_id_type: receiveIdType },
14089
- data: { receive_id: target, msg_type: "text", content: JSON.stringify({ text: chunk }) }
14189
+ data: { receive_id: safeTarget, msg_type: "text", content: JSON.stringify({ text: chunk }) }
14090
14190
  });
14091
14191
  }
14092
14192
  }
@@ -14130,6 +14230,28 @@ var FeishuAdapter = class _FeishuAdapter {
14130
14230
  if (!fileKey) throw new Error(`Feishu file upload failed: ${JSON.stringify(data)}`);
14131
14231
  return fileKey;
14132
14232
  }
14233
+ /** #217: 上传语音(opus)——飞书语音条必须 file_type=opus + duration(毫秒) */
14234
+ async _uploadAudio(buffer, filename, durationMs) {
14235
+ const token = await this._getTenantToken();
14236
+ const resp = await fetch("https://open.feishu.cn/open-apis/im/v1/files", {
14237
+ method: "POST",
14238
+ headers: {
14239
+ "Authorization": `Bearer ${token}`
14240
+ },
14241
+ body: (() => {
14242
+ const formData = new FormData();
14243
+ formData.append("file_type", "opus");
14244
+ formData.append("file_name", filename);
14245
+ formData.append("duration", String(Math.max(1, Math.round(durationMs))));
14246
+ formData.append("file", new Blob([buffer], { type: "audio/opus" }), filename);
14247
+ return formData;
14248
+ })()
14249
+ });
14250
+ const data = await resp.json();
14251
+ const fileKey = data?.data?.file_key;
14252
+ if (!fileKey) throw new Error(`Feishu audio upload failed: ${JSON.stringify(data)}`);
14253
+ return fileKey;
14254
+ }
14133
14255
  /** 通过飞书API下载消息中的图片 */
14134
14256
  async _downloadImage(messageId, imageKey) {
14135
14257
  const token = await this._getTenantToken();
@@ -14333,6 +14455,7 @@ var FeishuAdapter = class _FeishuAdapter {
14333
14455
  const message = data?.message;
14334
14456
  if (!sender || !message) return;
14335
14457
  const messageId = message.message_id || "";
14458
+ console.log(`[feishu] recv msg_type=${message.message_type} chat=${message.chat_type} from=${sender.sender_id?.open_id?.slice(0, 10)}`);
14336
14459
  if (this.recentMessageIds.has(messageId)) return;
14337
14460
  this.recentMessageIds.add(messageId);
14338
14461
  if (this.recentMessageIds.size > 500) {
@@ -14447,6 +14570,13 @@ var FeishuAdapter = class _FeishuAdapter {
14447
14570
  if (msgType === "file") {
14448
14571
  return { text: `[\u6587\u4EF6: ${parsed.file_name || "\u672A\u77E5\u6587\u4EF6"}]`, imageKeys, fileKey: parsed.file_key, fileName: parsed.file_name };
14449
14572
  }
14573
+ if (msgType === "share_location" || msgType === "location") {
14574
+ const name = parsed.name || "";
14575
+ const lat = parsed.latitude || "";
14576
+ const lng = parsed.longitude || "";
14577
+ const text = `[\u4F4D\u7F6E: ${name}${lat && lng ? ` (${lat}, ${lng})` : ""}]`;
14578
+ return { text, imageKeys };
14579
+ }
14450
14580
  if (msgType === "post") {
14451
14581
  const lines = [];
14452
14582
  if (Array.isArray(parsed.content)) {
@@ -16908,6 +17038,8 @@ var SessionManager = class {
16908
17038
  platformMapPath;
16909
17039
  /** 外部 hook:每次创建新 writer 时回调(如 EverOS sync 注入 onMessageWritten) */
16910
17040
  onWriterCreated = null;
17041
+ /** 外部 hook:判断 session 是否有活跃 query(idle cleanup 据此跳过,防止误关 query 正持有的 writer) */
17042
+ onIsSessionActive = null;
16911
17043
  histories = /* @__PURE__ */ new Map();
16912
17044
  restoredRecallPaths = /* @__PURE__ */ new Map();
16913
17045
  writers = /* @__PURE__ */ new Map();
@@ -17354,6 +17486,7 @@ var SessionManager = class {
17354
17486
  const now = Date.now();
17355
17487
  for (const [id, lastUsed] of this.lastUsed) {
17356
17488
  if (now - lastUsed > idleMs) {
17489
+ if (this.onIsSessionActive?.(id)) continue;
17357
17490
  const writer = this.writers.get(id);
17358
17491
  if (writer) {
17359
17492
  writer.close();
@@ -18704,8 +18837,11 @@ async function deepinfraRerank(query, episodes, rerankUrl, rerankApiKey, rerankM
18704
18837
  }
18705
18838
  var DEFAULT_MIN_SCORE2 = 0.5;
18706
18839
  async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /* @__PURE__ */ new Set(), options) {
18707
- const everosUrl = options?.everosUrl ?? "http://127.0.0.1:8100";
18708
- const userId = options?.userId ?? "xiaomei";
18840
+ const everosUrl = options?.everosUrl;
18841
+ const userId = options?.userId;
18842
+ if (!everosUrl || !userId) {
18843
+ 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`);
18844
+ }
18709
18845
  const topK = options?.topK ?? 3;
18710
18846
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
18711
18847
  console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
@@ -19027,7 +19163,9 @@ ${text}` : text });
19027
19163
  } else {
19028
19164
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
19029
19165
  }
19030
- const messages = [...history, msg.user(userMsgContent)];
19166
+ const vcN = liveConfig.get("voiceChat.lightContextN") ?? 20;
19167
+ const lightHistory = channelName === "voice-chat" && history.length > vcN ? history.slice(-vcN) : history;
19168
+ const messages = [...lightHistory, msg.user(userMsgContent)];
19031
19169
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
19032
19170
  const delta = deps.mcpManager.getMcpDelta();
19033
19171
  if (delta && delta.addedBlocks.length > 0) {
@@ -19196,7 +19334,7 @@ ${text}` : text });
19196
19334
  const queryAbortController = new AbortController();
19197
19335
  engine.setExternalAbort(queryAbortController);
19198
19336
  setActiveQueryEngine(sessionId, engine);
19199
- const shouldSkipRecall = skipRecall ?? channelName === "cron";
19337
+ const shouldSkipRecall = skipRecall ?? (channelName === "cron" || channelName === "voice-chat");
19200
19338
  if (getFeature("topic-recall") !== false && !shouldSkipRecall) {
19201
19339
  try {
19202
19340
  const memoryDir = getAutoMemPath(workspace);
@@ -19244,31 +19382,49 @@ ${text}` : text });
19244
19382
  const recallP = deps.recallProvider;
19245
19383
  const recallMode = topics?.recall?.mode || "llm";
19246
19384
  let relevantMemories;
19247
- if (recallMode === "everos") {
19385
+ const effectiveText = textForMemory.replace(/\[图片来自[^\]]*\]|路径:\S+/g, "").trim();
19386
+ if (!effectiveText) {
19387
+ console.log("[handle-query] Memory recall skipped: image-only message (no effective text)");
19388
+ relevantMemories = [];
19389
+ } else if (recallMode === "everos") {
19248
19390
  const everosCfg = deps?.everosCfg;
19249
- relevantMemories = await findRelevantMemoriesEveros(
19250
- textForMemory,
19251
- memoryDir,
19252
- surfaced.paths,
19253
- everosCfg ? {
19254
- everosUrl: everosCfg.everosUrl || "http://127.0.0.1:8100",
19255
- userId: everosCfg.userId || "xiaomei",
19256
- rerankUrl: everosCfg.rerank?.baseUrl,
19257
- rerankApiKey: everosCfg.rerank?.apiKey,
19258
- rerankModel: everosCfg.rerank?.model,
19259
- rerankProvider: everosCfg.rerank?.provider,
19260
- minScore: topics?.recall?.minScore
19261
- } : void 0
19262
- );
19391
+ if (everosCfg && everosCfg.everosUrl && everosCfg.userId) {
19392
+ relevantMemories = await findRelevantMemoriesEveros(
19393
+ effectiveText,
19394
+ memoryDir,
19395
+ surfaced.paths,
19396
+ {
19397
+ everosUrl: everosCfg.everosUrl,
19398
+ userId: everosCfg.userId,
19399
+ rerankUrl: everosCfg.rerank?.baseUrl,
19400
+ rerankApiKey: everosCfg.rerank?.apiKey,
19401
+ rerankModel: everosCfg.rerank?.model,
19402
+ rerankProvider: everosCfg.rerank?.provider,
19403
+ minScore: topics?.recall?.minScore
19404
+ }
19405
+ );
19406
+ } else {
19407
+ console.log("[handle-query] everos recall skipped: everosCfg not configured, falling back to LLM recall");
19408
+ relevantMemories = await findRelevantMemories(
19409
+ effectiveText,
19410
+ memoryDir,
19411
+ recallP?.provider || provider,
19412
+ recallP?.model || model,
19413
+ queryAbortController.signal,
19414
+ surfaced.paths,
19415
+ recallP?.disableThinking,
19416
+ topics?.maxScanFiles
19417
+ );
19418
+ }
19263
19419
  } else if (recallMode === "vector") {
19264
19420
  relevantMemories = await findRelevantMemoriesVector(
19265
- textForMemory,
19421
+ effectiveText,
19266
19422
  memoryDir,
19267
19423
  surfaced.paths
19268
19424
  );
19269
19425
  } else {
19270
19426
  relevantMemories = await findRelevantMemories(
19271
- textForMemory,
19427
+ effectiveText,
19272
19428
  memoryDir,
19273
19429
  recallP?.provider || provider,
19274
19430
  recallP?.model || model,
@@ -20500,6 +20656,10 @@ var NudgePlugin = class {
20500
20656
  registerCallbackHook("Stop", {
20501
20657
  type: "callback",
20502
20658
  callback: async (input, _toolUseID, _signal) => {
20659
+ if (this.cfg.stopHookEnabled === false) {
20660
+ console.log("[stop-hook] disabled via /stophook off \u2014 skipping");
20661
+ return { outcome: { outcome: "success" } };
20662
+ }
20503
20663
  const mode = this.cfg.stopHookMode || "sync";
20504
20664
  if (mode === "async") {
20505
20665
  console.log("[stop-hook] async mode \u2014 firing judge in background, not blocking");
@@ -20774,26 +20934,28 @@ var NudgePlugin = class {
20774
20934
  console.log(`[nudge] ${due.length} stop-hook notification(s) due: ${due.map((n) => n.id).join(", ")}`);
20775
20935
  const items = due.map((n) => `[\u901A\u77E5ID: ${n.id}]
20776
20936
  \u4E0A\u6B21\u8BF4\uFF1A${n.description}`).join("\n\n");
20937
+ 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`;
20777
20938
  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
20778
20939
 
20779
20940
  ${items}
20780
20941
 
20781
- \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
20942
+ \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
20782
20943
 
20783
20944
  ${items}
20784
20945
 
20785
- \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`;
20946
+ \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`;
20786
20947
  return { message: buildNudgeNotification("wake", desc), ids: due.map((n) => n.id) };
20787
20948
  } catch (e) {
20788
20949
  console.warn(`[nudge] collectDueStopHookNotifications error: ${e.message}`);
20789
20950
  return null;
20790
20951
  }
20791
20952
  }
20792
- /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个) */
20953
+ /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个)
20954
+ * 0816 翀哥:关键字多样化——过期了/已落盘/resolved/已处理/已闭环/已归档 都认 */
20793
20955
  extractWakeReplyIds(text) {
20794
20956
  if (!text) return [];
20795
20957
  const ids = [];
20796
- const re = /(wake-\d+-[a-z0-9]+)\s*过期了/g;
20958
+ const re = /(wake-\d+-[a-z0-9]+)\s*(?:过期了|已落盘|resolved|已处理|已闭环|已归档|已完成|done)/g;
20797
20959
  let m;
20798
20960
  while ((m = re.exec(text)) !== null) {
20799
20961
  if (!ids.includes(m[1])) ids.push(m[1]);
@@ -23236,8 +23398,9 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
23236
23398
  - \u4FDD\u6301\u7B80\u77ED\u53E3\u8BED\u5316\uFF08\u901A\u5E38 1-3 \u53E5\uFF09\uFF0C\u4E0D\u8981\u957F\u7BC7\u5927\u8BBA
23237
23399
  - \u7B2C\u4E00\u53E5\u63A7\u5236\u5728 4-6 \u4E2A\u5B57\uFF0C\u5C3D\u5FEB\u89E6\u53D1 TTS \u5408\u6210
23238
23400
  - \u8BF4\u8BDD\u50CF\u4EBA\u2014\u2014\u77ED\u53E5\uFF0C\u53BB\u670D\u52A1\u611F
23239
- - \u5982\u65E0\u5FC5\u8981\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\uFF0C\u76F4\u63A5\u56DE\u590D
23240
- - \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`;
23401
+ - \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"
23402
+ - \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
23403
+ - \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`;
23241
23404
  const engine = new QueryEngine(llmProvider, {
23242
23405
  model: modelId,
23243
23406
  systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
@@ -23951,7 +24114,22 @@ var DEFAULTS4 = {
23951
24114
  autoStart: true,
23952
24115
  defaultMode: "hybrid_agentic"
23953
24116
  };
23954
- function parseEverosConfig(raw) {
24117
+ function resolveProviderConfig(rawSection, providers, defaultModel, defaultApiKey, defaultBaseUrl) {
24118
+ if (!rawSection) {
24119
+ return { model: defaultModel, apiKey: defaultApiKey, baseUrl: defaultBaseUrl };
24120
+ }
24121
+ const providerName = rawSection.provider;
24122
+ const providerCfg = providerName ? providers?.[providerName] : void 0;
24123
+ return {
24124
+ provider: providerName,
24125
+ model: rawSection.model ?? defaultModel,
24126
+ // apiKey 从 provider 取(最易不一致,统一来源)
24127
+ // baseUrl/model 优先用 everos 节点自己的值(同一 provider 可能有不同 endpoint)
24128
+ apiKey: providerCfg?.apiKey ?? rawSection.apiKey ?? defaultApiKey,
24129
+ baseUrl: rawSection.baseUrl ?? providerCfg?.baseUrl ?? defaultBaseUrl
24130
+ };
24131
+ }
24132
+ function parseEverosConfig(raw, providers) {
23955
24133
  if (!raw) {
23956
24134
  return {
23957
24135
  enabled: false,
@@ -23971,8 +24149,20 @@ function parseEverosConfig(raw) {
23971
24149
  userId: raw.userId ?? "xiaomei",
23972
24150
  autoStart: raw.autoStart !== false,
23973
24151
  defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
23974
- llm: raw.llm ?? { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
23975
- rerank: raw.rerank ?? { model: "Qwen/Qwen3-Reranker-4B", apiKey: "", baseUrl: "https://api.deepinfra.com/v1/inference" },
24152
+ llm: resolveProviderConfig(
24153
+ raw.llm,
24154
+ providers,
24155
+ "glm-5.2",
24156
+ "",
24157
+ "https://open.bigmodel.cn/api/coding/paas/v4"
24158
+ ),
24159
+ rerank: resolveProviderConfig(
24160
+ raw.rerank,
24161
+ providers,
24162
+ "Qwen/Qwen3-Reranker-4B",
24163
+ "",
24164
+ "https://api.deepinfra.com/v1/inference"
24165
+ ),
23976
24166
  lancedbPath: raw.lancedbPath ?? "",
23977
24167
  sqlitePath: raw.sqlitePath ?? "",
23978
24168
  minScore: raw.minScore
@@ -24023,14 +24213,14 @@ var EverosSearchClient = class {
24023
24213
  const url = useAgentic ? `${this.agenticUrl}/api/v1/search` : `${this.everosUrl}/api/v1/memory/search`;
24024
24214
  const body = useAgentic ? JSON.stringify({
24025
24215
  query: params.query,
24026
- user_id: params.userId || "xiaomei",
24216
+ user_id: params.userId,
24027
24217
  mode,
24028
24218
  top_k: params.topK ?? 5,
24029
24219
  strategy: params.strategy || "multi_query"
24030
24220
  }) : JSON.stringify({
24031
24221
  query: params.query,
24032
- user_id: params.userId || "user",
24033
- app_id: "xiaomei",
24222
+ user_id: params.userId,
24223
+ app_id: params.appId,
24034
24224
  project_id: "default",
24035
24225
  top_k: params.topK ?? 5
24036
24226
  });
@@ -24068,8 +24258,8 @@ var EverosPlugin = class {
24068
24258
  healthTimer = null;
24069
24259
  weStartedAgentic = false;
24070
24260
  // 我们拉起的才管
24071
- constructor(rawConfig) {
24072
- this.config = parseEverosConfig(rawConfig);
24261
+ constructor(rawConfig, providers) {
24262
+ this.config = parseEverosConfig(rawConfig, providers);
24073
24263
  this.client = new EverosSearchClient(this.config.agenticUrl, this.config.everosUrl);
24074
24264
  }
24075
24265
  static shouldEnable(config) {
@@ -25206,6 +25396,47 @@ async function compressWav(wavPath) {
25206
25396
  return wavPath;
25207
25397
  }
25208
25398
  }
25399
+ async function wavToOggOpus(wavPath) {
25400
+ const oggPath = wavPath.replace(/\.wav$/, ".ogg");
25401
+ await execFileAsync("ffmpeg", [
25402
+ "-y",
25403
+ "-i",
25404
+ wavPath,
25405
+ "-c:a",
25406
+ "libopus",
25407
+ "-b:a",
25408
+ "32k",
25409
+ "-ar",
25410
+ "24000",
25411
+ oggPath
25412
+ ], { timeout: 3e4 });
25413
+ if (!fs30.existsSync(oggPath) || fs30.statSync(oggPath).size < 20) {
25414
+ throw new Error("ogg/opus encode produced empty output");
25415
+ }
25416
+ fs30.unlinkSync(wavPath);
25417
+ return oggPath;
25418
+ }
25419
+ async function wavToSilk(wavPath) {
25420
+ fs30.mkdirSync(VOICE_DIR, { recursive: true });
25421
+ const silkPath = wavPath.replace(/\.wav$/, ".silk");
25422
+ const script = `
25423
+ import sys, io, wave, pysilk
25424
+ src, dst = sys.argv[1], sys.argv[2]
25425
+ with wave.open(src, 'rb') as wf:
25426
+ sr = wf.getframerate()
25427
+ pcm = wf.readframes(wf.getnframes())
25428
+ out = io.BytesIO()
25429
+ pysilk.encode(io.BytesIO(pcm), out, sample_rate=sr, bit_rate=24000, tencent=True)
25430
+ with open(dst, 'wb') as f:
25431
+ f.write(out.getvalue())
25432
+ print('OK')
25433
+ `;
25434
+ await execFileAsync("python3", ["-c", script, wavPath, silkPath], { timeout: 3e4 });
25435
+ if (!fs30.existsSync(silkPath) || fs30.statSync(silkPath).size < 20) {
25436
+ throw new Error("silk encode produced empty output");
25437
+ }
25438
+ return silkPath;
25439
+ }
25209
25440
  registry.register({
25210
25441
  name: "my_voice",
25211
25442
  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.",
@@ -25243,9 +25474,6 @@ registry.register({
25243
25474
  vc.workspaceId,
25244
25475
  vc.instruction
25245
25476
  );
25246
- if (audioPath.endsWith(".wav")) {
25247
- audioPath = await compressWav(audioPath);
25248
- }
25249
25477
  } catch (e) {
25250
25478
  console.warn(`[my-voice] cosyvoice failed (${e.message}), falling back to edge-tts`);
25251
25479
  audioPath = await ttsEdge(text);
@@ -25254,9 +25482,6 @@ registry.register({
25254
25482
  } else if (useEngine === "gptsovits") {
25255
25483
  try {
25256
25484
  audioPath = await ttsGptsovits(text);
25257
- if (audioPath.endsWith(".wav")) {
25258
- audioPath = await compressWav(audioPath);
25259
- }
25260
25485
  } catch (e) {
25261
25486
  console.warn(`[my-voice] gptsovits failed (${e.message}), falling back to edge-tts`);
25262
25487
  audioPath = await ttsEdge(text);
@@ -25268,12 +25493,30 @@ registry.register({
25268
25493
  } catch (e) {
25269
25494
  return { content: `TTS failed: ${e.message}`, isError: true };
25270
25495
  }
25496
+ const resolvedChannelRaw = args.channel || ctx.channel || "feishu";
25497
+ const resolvedChannel = resolvedChannelRaw === "weixin" ? "wechat" : resolvedChannelRaw;
25498
+ const target = ctx.channelTarget || ctx.from;
25499
+ if (audioPath.endsWith(".wav")) {
25500
+ if (resolvedChannel === "wechat") {
25501
+ try {
25502
+ audioPath = await wavToSilk(audioPath);
25503
+ } catch (e) {
25504
+ console.warn(`[my-voice] silk encode failed (${e.message}), falling back to m4a`);
25505
+ audioPath = await compressWav(audioPath);
25506
+ }
25507
+ } else if (resolvedChannel === "feishu") {
25508
+ try {
25509
+ audioPath = await wavToOggOpus(audioPath);
25510
+ } catch (e) {
25511
+ console.warn(`[my-voice] ogg/opus encode failed (${e.message}), falling back to m4a`);
25512
+ audioPath = await compressWav(audioPath);
25513
+ }
25514
+ }
25515
+ }
25271
25516
  const ext = path31.extname(audioPath).toLowerCase();
25272
- const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg" };
25517
+ const mimeMap = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg", ".silk": "audio/silk" };
25273
25518
  const mimeType = mimeMap[ext] || "audio/mpeg";
25274
25519
  const sizeKB = fs30.statSync(audioPath).size / 1024;
25275
- const resolvedChannel = args.channel || ctx.channel || "feishu";
25276
- const target = ctx.channelTarget || ctx.from;
25277
25520
  try {
25278
25521
  await mgr.sendFile(resolvedChannel, target, caption, {
25279
25522
  path: audioPath,
@@ -25311,9 +25554,12 @@ function getProxyDispatcher2() {
25311
25554
  return void 0;
25312
25555
  }
25313
25556
  }
25314
- var FAL_KEY = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25315
25557
  var FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
25316
25558
  var DEFAULT_RESOLUTION = "1k";
25559
+ var FAL_KEY_FALLBACK = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25560
+ function getFalKey(cfg) {
25561
+ return cfg?.providers?.fal?.apiKey || cfg?.my_selfie?.falKey || FAL_KEY_FALLBACK;
25562
+ }
25317
25563
  var DEFAULT_REFERENCES = [
25318
25564
  { name: "default", p: "images/xiaomei_clean_v2.png" },
25319
25565
  { name: "v3", p: "images/xiaomei_clean_v3.png" },
@@ -25356,6 +25602,7 @@ function detectMode(input) {
25356
25602
  return "direct";
25357
25603
  }
25358
25604
  async function generateWithFal(imageB64, prompt, resolution) {
25605
+ const cfg = liveConfig.all();
25359
25606
  let aspectRatio;
25360
25607
  try {
25361
25608
  const refBuf = Buffer.from(imageB64, "base64");
@@ -25380,8 +25627,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25380
25627
  }
25381
25628
  }
25382
25629
  if (w > 0 && h > 0) {
25383
- aspectRatio = `${w}/${h}`;
25384
- console.log(`[my-selfie] ref image ${w}x${h}, aspect_ratio=${aspectRatio}`);
25630
+ console.log(`[my-selfie] ref image ${w}x${h}, aspect_ratio skipped`);
25385
25631
  }
25386
25632
  } catch (e) {
25387
25633
  console.warn(`[my-selfie] Failed to read ref dimensions: ${e.message}`);
@@ -25396,7 +25642,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25396
25642
  if (aspectRatio) body.aspect_ratio = aspectRatio;
25397
25643
  const res = await fetch(FAL_ENDPOINT, {
25398
25644
  method: "POST",
25399
- headers: { "Authorization": `Key ${FAL_KEY}`, "Content-Type": "application/json" },
25645
+ headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
25400
25646
  body: JSON.stringify(body)
25401
25647
  });
25402
25648
  if (!res.ok) {
@@ -28098,7 +28344,12 @@ ${content}`
28098
28344
  // tool 读自己配置用
28099
28345
  recallProvider: memoryRecallProvider || void 0,
28100
28346
  extractProvider: memoryExtractProvider || void 0,
28101
- everosCfg: config.everos,
28347
+ everosCfg: config.everos ? {
28348
+ ...config.everos,
28349
+ // resolve provider 引用:从 providers 取 apiKey(rerank 在 handle-query.ts 里直接读 raw config)
28350
+ llm: config.everos.llm?.provider && config.providers?.[config.everos.llm.provider] ? { ...config.everos.llm, apiKey: config.providers[config.everos.llm.provider].apiKey } : config.everos.llm,
28351
+ rerank: config.everos.rerank?.provider && config.providers?.[config.everos.rerank.provider] ? { ...config.everos.rerank, apiKey: config.providers[config.everos.rerank.provider].apiKey } : config.everos.rerank
28352
+ } : void 0,
28102
28353
  mcpManager
28103
28354
  };
28104
28355
  if (visionEngine && visionConfig) {
@@ -28118,7 +28369,13 @@ ${content}`
28118
28369
  config,
28119
28370
  // tool 读自己配置用
28120
28371
  recallProvider: memoryRecallProvider || void 0,
28121
- extractProvider: memoryExtractProvider || void 0
28372
+ extractProvider: memoryExtractProvider || void 0,
28373
+ everosCfg: config.everos ? {
28374
+ ...config.everos,
28375
+ // resolve provider 引用:从 providers 取 apiKey(跟主 deps 同逻辑)
28376
+ llm: config.everos.llm?.provider && config.providers?.[config.everos.llm.provider] ? { ...config.everos.llm, apiKey: config.providers[config.everos.llm.provider].apiKey } : config.everos.llm,
28377
+ rerank: config.everos.rerank?.provider && config.providers?.[config.everos.rerank.provider] ? { ...config.everos.rerank, apiKey: config.providers[config.everos.rerank.provider].apiKey } : config.everos.rerank
28378
+ } : void 0
28122
28379
  };
28123
28380
  }
28124
28381
  let modelOverride = null;
@@ -28167,7 +28424,8 @@ ${content}`
28167
28424
  channels: config.channels,
28168
28425
  config,
28169
28426
  recallProvider: memoryRecallProvider || void 0,
28170
- extractProvider: memoryExtractProvider || void 0
28427
+ extractProvider: memoryExtractProvider || void 0,
28428
+ everosCfg: deps?.everosCfg
28171
28429
  };
28172
28430
  }
28173
28431
  };
@@ -28209,11 +28467,13 @@ ${content}`
28209
28467
  systemPrompt,
28210
28468
  channels: config.channels,
28211
28469
  recallProvider: memoryRecallProvider || void 0,
28212
- extractProvider: memoryExtractProvider || void 0
28470
+ extractProvider: memoryExtractProvider || void 0,
28471
+ everosCfg: deps?.everosCfg
28213
28472
  };
28214
28473
  }
28215
28474
  const dispatcher = new MessageDispatcher();
28216
28475
  deps.dispatcher = dispatcher;
28476
+ sessions.onIsSessionActive = (sid) => dispatcher.isActive(sid);
28217
28477
  setNotificationCallback((notif, route) => {
28218
28478
  const sid = route?.sessionId || sessions.getSessionId("scope:main") || "main";
28219
28479
  const chan = route?.channel;
@@ -28584,6 +28844,10 @@ ${notifications}
28584
28844
  { name: "session-memory", description: "\u67E5/\u5207 session-memory\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28585
28845
  { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28586
28846
  ] },
28847
+ // 8/17 stophook 总开关(翀哥要求)
28848
+ { name: "stophook", description: "\u67E5/\u5207 stop-hook\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28849
+ { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28850
+ ] },
28587
28851
  (() => {
28588
28852
  const aliases = config.modelAliases || {};
28589
28853
  const aliasByRef = /* @__PURE__ */ new Map();
@@ -29012,6 +29276,29 @@ ${result.changes.map((c) => `- ${c}`).join("\n")}` : `\u274C Reload failed: ${re
29012
29276
  await ctx.reply(`\u2705 ${key}: **${cur}** \u2192 **${rawState}**`);
29013
29277
  return;
29014
29278
  }
29279
+ if (ctx.command === "stophook") {
29280
+ const rawState = (ctx.args.state || "").trim().toLowerCase();
29281
+ const getPath = "nudge.stopHookEnabled";
29282
+ const curVal = config?.nudge?.stopHookEnabled;
29283
+ const cur = curVal === false ? "off" : "on";
29284
+ if (rawState === "") {
29285
+ await ctx.reply(`\u{1F4CA} stop-hook: **${cur}**\uFF08nudge \u63D2\u4EF6 stopHookEnabled=${String(curVal ?? "\u672A\u8BBE(\u9ED8\u8BA4on)")}\uFF09`);
29286
+ return;
29287
+ }
29288
+ if (rawState !== "on" && rawState !== "off") {
29289
+ await ctx.reply(`\u274C \u53EA\u63A5\u53D7 on \u6216 off
29290
+ \u5F53\u524D\uFF1Astop-hook = **${cur}**`);
29291
+ return;
29292
+ }
29293
+ if (rawState === cur) {
29294
+ await ctx.reply(`\u2139\uFE0F stop-hook \u5DF2\u7ECF\u662F **${cur}**`);
29295
+ return;
29296
+ }
29297
+ await liveConfig.set(getPath, rawState === "on");
29298
+ console.log(`[stophook] stop-hook ${cur} \u2192 ${rawState} (live + persisted)`);
29299
+ 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" : ""}`);
29300
+ return;
29301
+ }
29015
29302
  if (ctx.command === "model") {
29016
29303
  const input = (ctx.args.model || "").trim();
29017
29304
  const aliases = config.modelAliases || {};
@@ -29737,7 +30024,7 @@ ${pathStr}` }];
29737
30024
  }
29738
30025
  }
29739
30026
  if (config.everos?.enabled) {
29740
- pluginManager.register(new EverosPlugin(config.everos));
30027
+ pluginManager.register(new EverosPlugin(config.everos, config.providers));
29741
30028
  console.log("[everos] Plugin registered");
29742
30029
  }
29743
30030
  globalThis.__pluginManager = pluginManager;