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.
@@ -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();
@@ -16916,6 +17038,8 @@ var SessionManager = class {
16916
17038
  platformMapPath;
16917
17039
  /** 外部 hook:每次创建新 writer 时回调(如 EverOS sync 注入 onMessageWritten) */
16918
17040
  onWriterCreated = null;
17041
+ /** 外部 hook:判断 session 是否有活跃 query(idle cleanup 据此跳过,防止误关 query 正持有的 writer) */
17042
+ onIsSessionActive = null;
16919
17043
  histories = /* @__PURE__ */ new Map();
16920
17044
  restoredRecallPaths = /* @__PURE__ */ new Map();
16921
17045
  writers = /* @__PURE__ */ new Map();
@@ -17362,6 +17486,7 @@ var SessionManager = class {
17362
17486
  const now = Date.now();
17363
17487
  for (const [id, lastUsed] of this.lastUsed) {
17364
17488
  if (now - lastUsed > idleMs) {
17489
+ if (this.onIsSessionActive?.(id)) continue;
17365
17490
  const writer = this.writers.get(id);
17366
17491
  if (writer) {
17367
17492
  writer.close();
@@ -18712,8 +18837,11 @@ async function deepinfraRerank(query, episodes, rerankUrl, rerankApiKey, rerankM
18712
18837
  }
18713
18838
  var DEFAULT_MIN_SCORE2 = 0.5;
18714
18839
  async function findRelevantMemoriesEveros(query, _memoryDir, alreadySurfaced = /* @__PURE__ */ new Set(), options) {
18715
- const everosUrl = options?.everosUrl ?? "http://127.0.0.1:8100";
18716
- 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
+ }
18717
18845
  const topK = options?.topK ?? 3;
18718
18846
  const minScore = options?.minScore ?? DEFAULT_MIN_SCORE2;
18719
18847
  console.log(`[memdir] everos recall: query="${query.slice(0, 50)}..." url=${everosUrl} userId=${userId} topK=${topK}`);
@@ -19035,7 +19163,9 @@ ${text}` : text });
19035
19163
  } else {
19036
19164
  console.log(`[pre-llm-debug] userMsgContent is string, len=${String(userMsgContent).length}`);
19037
19165
  }
19038
- 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)];
19039
19169
  if (deps.mcpManager && !deps.mcpManager.isMcpDeltaSent(sessionId)) {
19040
19170
  const delta = deps.mcpManager.getMcpDelta();
19041
19171
  if (delta && delta.addedBlocks.length > 0) {
@@ -19204,7 +19334,7 @@ ${text}` : text });
19204
19334
  const queryAbortController = new AbortController();
19205
19335
  engine.setExternalAbort(queryAbortController);
19206
19336
  setActiveQueryEngine(sessionId, engine);
19207
- const shouldSkipRecall = skipRecall ?? channelName === "cron";
19337
+ const shouldSkipRecall = skipRecall ?? (channelName === "cron" || channelName === "voice-chat");
19208
19338
  if (getFeature("topic-recall") !== false && !shouldSkipRecall) {
19209
19339
  try {
19210
19340
  const memoryDir = getAutoMemPath(workspace);
@@ -19252,31 +19382,49 @@ ${text}` : text });
19252
19382
  const recallP = deps.recallProvider;
19253
19383
  const recallMode = topics?.recall?.mode || "llm";
19254
19384
  let relevantMemories;
19255
- 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") {
19256
19390
  const everosCfg = deps?.everosCfg;
19257
- relevantMemories = await findRelevantMemoriesEveros(
19258
- textForMemory,
19259
- memoryDir,
19260
- surfaced.paths,
19261
- everosCfg ? {
19262
- everosUrl: everosCfg.everosUrl || "http://127.0.0.1:8100",
19263
- userId: everosCfg.userId || "xiaomei",
19264
- rerankUrl: everosCfg.rerank?.baseUrl,
19265
- rerankApiKey: everosCfg.rerank?.apiKey,
19266
- rerankModel: everosCfg.rerank?.model,
19267
- rerankProvider: everosCfg.rerank?.provider,
19268
- minScore: topics?.recall?.minScore
19269
- } : void 0
19270
- );
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
+ }
19271
19419
  } else if (recallMode === "vector") {
19272
19420
  relevantMemories = await findRelevantMemoriesVector(
19273
- textForMemory,
19421
+ effectiveText,
19274
19422
  memoryDir,
19275
19423
  surfaced.paths
19276
19424
  );
19277
19425
  } else {
19278
19426
  relevantMemories = await findRelevantMemories(
19279
- textForMemory,
19427
+ effectiveText,
19280
19428
  memoryDir,
19281
19429
  recallP?.provider || provider,
19282
19430
  recallP?.model || model,
@@ -20508,6 +20656,10 @@ var NudgePlugin = class {
20508
20656
  registerCallbackHook("Stop", {
20509
20657
  type: "callback",
20510
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
+ }
20511
20663
  const mode = this.cfg.stopHookMode || "sync";
20512
20664
  if (mode === "async") {
20513
20665
  console.log("[stop-hook] async mode \u2014 firing judge in background, not blocking");
@@ -20782,26 +20934,28 @@ var NudgePlugin = class {
20782
20934
  console.log(`[nudge] ${due.length} stop-hook notification(s) due: ${due.map((n) => n.id).join(", ")}`);
20783
20935
  const items = due.map((n) => `[\u901A\u77E5ID: ${n.id}]
20784
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`;
20785
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
20786
20939
 
20787
20940
  ${items}
20788
20941
 
20789
- \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
20790
20943
 
20791
20944
  ${items}
20792
20945
 
20793
- \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`;
20794
20947
  return { message: buildNudgeNotification("wake", desc), ids: due.map((n) => n.id) };
20795
20948
  } catch (e) {
20796
20949
  console.warn(`[nudge] collectDueStopHookNotifications error: ${e.message}`);
20797
20950
  return null;
20798
20951
  }
20799
20952
  }
20800
- /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个) */
20953
+ /** 从回复文本里提取 "<id> 过期了" 的 wake id(一条回复可能处置多个)
20954
+ * 0816 翀哥:关键字多样化——过期了/已落盘/resolved/已处理/已闭环/已归档 都认 */
20801
20955
  extractWakeReplyIds(text) {
20802
20956
  if (!text) return [];
20803
20957
  const ids = [];
20804
- const re = /(wake-\d+-[a-z0-9]+)\s*过期了/g;
20958
+ const re = /(wake-\d+-[a-z0-9]+)\s*(?:过期了|已落盘|resolved|已处理|已闭环|已归档|已完成|done)/g;
20805
20959
  let m;
20806
20960
  while ((m = re.exec(text)) !== null) {
20807
20961
  if (!ids.includes(m[1])) ids.push(m[1]);
@@ -23244,8 +23398,9 @@ var VoiceChatPlugin = class _VoiceChatPlugin {
23244
23398
  - \u4FDD\u6301\u7B80\u77ED\u53E3\u8BED\u5316\uFF08\u901A\u5E38 1-3 \u53E5\uFF09\uFF0C\u4E0D\u8981\u957F\u7BC7\u5927\u8BBA
23245
23399
  - \u7B2C\u4E00\u53E5\u63A7\u5236\u5728 4-6 \u4E2A\u5B57\uFF0C\u5C3D\u5FEB\u89E6\u53D1 TTS \u5408\u6210
23246
23400
  - \u8BF4\u8BDD\u50CF\u4EBA\u2014\u2014\u77ED\u53E5\uFF0C\u53BB\u670D\u52A1\u611F
23247
- - \u5982\u65E0\u5FC5\u8981\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\uFF0C\u76F4\u63A5\u56DE\u590D
23248
- - \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`;
23249
23404
  const engine = new QueryEngine(llmProvider, {
23250
23405
  model: modelId,
23251
23406
  systemPrompt: (ctx.deps.systemPrompt || "") + voiceChatRules,
@@ -23959,7 +24114,22 @@ var DEFAULTS4 = {
23959
24114
  autoStart: true,
23960
24115
  defaultMode: "hybrid_agentic"
23961
24116
  };
23962
- 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) {
23963
24133
  if (!raw) {
23964
24134
  return {
23965
24135
  enabled: false,
@@ -23979,8 +24149,20 @@ function parseEverosConfig(raw) {
23979
24149
  userId: raw.userId ?? "xiaomei",
23980
24150
  autoStart: raw.autoStart !== false,
23981
24151
  defaultMode: raw.defaultMode ?? DEFAULTS4.defaultMode,
23982
- llm: raw.llm ?? { model: "glm-5.2", apiKey: "", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" },
23983
- 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
+ ),
23984
24166
  lancedbPath: raw.lancedbPath ?? "",
23985
24167
  sqlitePath: raw.sqlitePath ?? "",
23986
24168
  minScore: raw.minScore
@@ -24031,14 +24213,14 @@ var EverosSearchClient = class {
24031
24213
  const url = useAgentic ? `${this.agenticUrl}/api/v1/search` : `${this.everosUrl}/api/v1/memory/search`;
24032
24214
  const body = useAgentic ? JSON.stringify({
24033
24215
  query: params.query,
24034
- user_id: params.userId || "xiaomei",
24216
+ user_id: params.userId,
24035
24217
  mode,
24036
24218
  top_k: params.topK ?? 5,
24037
24219
  strategy: params.strategy || "multi_query"
24038
24220
  }) : JSON.stringify({
24039
24221
  query: params.query,
24040
- user_id: params.userId || "user",
24041
- app_id: "xiaomei",
24222
+ user_id: params.userId,
24223
+ app_id: params.appId,
24042
24224
  project_id: "default",
24043
24225
  top_k: params.topK ?? 5
24044
24226
  });
@@ -24076,8 +24258,8 @@ var EverosPlugin = class {
24076
24258
  healthTimer = null;
24077
24259
  weStartedAgentic = false;
24078
24260
  // 我们拉起的才管
24079
- constructor(rawConfig) {
24080
- this.config = parseEverosConfig(rawConfig);
24261
+ constructor(rawConfig, providers) {
24262
+ this.config = parseEverosConfig(rawConfig, providers);
24081
24263
  this.client = new EverosSearchClient(this.config.agenticUrl, this.config.everosUrl);
24082
24264
  }
24083
24265
  static shouldEnable(config) {
@@ -25214,6 +25396,47 @@ async function compressWav(wavPath) {
25214
25396
  return wavPath;
25215
25397
  }
25216
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
+ }
25217
25440
  registry.register({
25218
25441
  name: "my_voice",
25219
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.",
@@ -25251,9 +25474,6 @@ registry.register({
25251
25474
  vc.workspaceId,
25252
25475
  vc.instruction
25253
25476
  );
25254
- if (audioPath.endsWith(".wav")) {
25255
- audioPath = await compressWav(audioPath);
25256
- }
25257
25477
  } catch (e) {
25258
25478
  console.warn(`[my-voice] cosyvoice failed (${e.message}), falling back to edge-tts`);
25259
25479
  audioPath = await ttsEdge(text);
@@ -25262,9 +25482,6 @@ registry.register({
25262
25482
  } else if (useEngine === "gptsovits") {
25263
25483
  try {
25264
25484
  audioPath = await ttsGptsovits(text);
25265
- if (audioPath.endsWith(".wav")) {
25266
- audioPath = await compressWav(audioPath);
25267
- }
25268
25485
  } catch (e) {
25269
25486
  console.warn(`[my-voice] gptsovits failed (${e.message}), falling back to edge-tts`);
25270
25487
  audioPath = await ttsEdge(text);
@@ -25276,12 +25493,30 @@ registry.register({
25276
25493
  } catch (e) {
25277
25494
  return { content: `TTS failed: ${e.message}`, isError: true };
25278
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
+ }
25279
25516
  const ext = path31.extname(audioPath).toLowerCase();
25280
- 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" };
25281
25518
  const mimeType = mimeMap[ext] || "audio/mpeg";
25282
25519
  const sizeKB = fs30.statSync(audioPath).size / 1024;
25283
- const resolvedChannel = args.channel || ctx.channel || "feishu";
25284
- const target = ctx.channelTarget || ctx.from;
25285
25520
  try {
25286
25521
  await mgr.sendFile(resolvedChannel, target, caption, {
25287
25522
  path: audioPath,
@@ -25319,9 +25554,12 @@ function getProxyDispatcher2() {
25319
25554
  return void 0;
25320
25555
  }
25321
25556
  }
25322
- var FAL_KEY = "3b848fc6-bee5-46e5-8db7-ae81ac16dc28:2f1f15cf394db2d32c9bb9a4f23f3bee";
25323
25557
  var FAL_ENDPOINT = "https://fal.run/xai/grok-imagine-image/edit";
25324
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
+ }
25325
25563
  var DEFAULT_REFERENCES = [
25326
25564
  { name: "default", p: "images/xiaomei_clean_v2.png" },
25327
25565
  { name: "v3", p: "images/xiaomei_clean_v3.png" },
@@ -25364,6 +25602,7 @@ function detectMode(input) {
25364
25602
  return "direct";
25365
25603
  }
25366
25604
  async function generateWithFal(imageB64, prompt, resolution) {
25605
+ const cfg = liveConfig.all();
25367
25606
  let aspectRatio;
25368
25607
  try {
25369
25608
  const refBuf = Buffer.from(imageB64, "base64");
@@ -25388,8 +25627,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25388
25627
  }
25389
25628
  }
25390
25629
  if (w > 0 && h > 0) {
25391
- aspectRatio = `${w}/${h}`;
25392
- 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`);
25393
25631
  }
25394
25632
  } catch (e) {
25395
25633
  console.warn(`[my-selfie] Failed to read ref dimensions: ${e.message}`);
@@ -25404,7 +25642,7 @@ async function generateWithFal(imageB64, prompt, resolution) {
25404
25642
  if (aspectRatio) body.aspect_ratio = aspectRatio;
25405
25643
  const res = await fetch(FAL_ENDPOINT, {
25406
25644
  method: "POST",
25407
- headers: { "Authorization": `Key ${FAL_KEY}`, "Content-Type": "application/json" },
25645
+ headers: { "Authorization": `Key ${getFalKey(cfg)}`, "Content-Type": "application/json" },
25408
25646
  body: JSON.stringify(body)
25409
25647
  });
25410
25648
  if (!res.ok) {
@@ -28106,7 +28344,12 @@ ${content}`
28106
28344
  // tool 读自己配置用
28107
28345
  recallProvider: memoryRecallProvider || void 0,
28108
28346
  extractProvider: memoryExtractProvider || void 0,
28109
- 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,
28110
28353
  mcpManager
28111
28354
  };
28112
28355
  if (visionEngine && visionConfig) {
@@ -28126,7 +28369,13 @@ ${content}`
28126
28369
  config,
28127
28370
  // tool 读自己配置用
28128
28371
  recallProvider: memoryRecallProvider || void 0,
28129
- 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
28130
28379
  };
28131
28380
  }
28132
28381
  let modelOverride = null;
@@ -28175,7 +28424,8 @@ ${content}`
28175
28424
  channels: config.channels,
28176
28425
  config,
28177
28426
  recallProvider: memoryRecallProvider || void 0,
28178
- extractProvider: memoryExtractProvider || void 0
28427
+ extractProvider: memoryExtractProvider || void 0,
28428
+ everosCfg: deps?.everosCfg
28179
28429
  };
28180
28430
  }
28181
28431
  };
@@ -28217,11 +28467,13 @@ ${content}`
28217
28467
  systemPrompt,
28218
28468
  channels: config.channels,
28219
28469
  recallProvider: memoryRecallProvider || void 0,
28220
- extractProvider: memoryExtractProvider || void 0
28470
+ extractProvider: memoryExtractProvider || void 0,
28471
+ everosCfg: deps?.everosCfg
28221
28472
  };
28222
28473
  }
28223
28474
  const dispatcher = new MessageDispatcher();
28224
28475
  deps.dispatcher = dispatcher;
28476
+ sessions.onIsSessionActive = (sid) => dispatcher.isActive(sid);
28225
28477
  setNotificationCallback((notif, route) => {
28226
28478
  const sid = route?.sessionId || sessions.getSessionId("scope:main") || "main";
28227
28479
  const chan = route?.channel;
@@ -28592,6 +28844,10 @@ ${notifications}
28592
28844
  { name: "session-memory", description: "\u67E5/\u5207 session-memory\uFF08on/off/\u7559\u7A7A=\u67E5\uFF09", options: [
28593
28845
  { name: "state", description: "on \u6216 off\uFF0C\u7559\u7A7A=\u53EA\u67E5\u72B6\u6001", type: "string", required: false }
28594
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
+ ] },
28595
28851
  (() => {
28596
28852
  const aliases = config.modelAliases || {};
28597
28853
  const aliasByRef = /* @__PURE__ */ new Map();
@@ -29020,6 +29276,29 @@ ${result.changes.map((c) => `- ${c}`).join("\n")}` : `\u274C Reload failed: ${re
29020
29276
  await ctx.reply(`\u2705 ${key}: **${cur}** \u2192 **${rawState}**`);
29021
29277
  return;
29022
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
+ }
29023
29302
  if (ctx.command === "model") {
29024
29303
  const input = (ctx.args.model || "").trim();
29025
29304
  const aliases = config.modelAliases || {};
@@ -29745,7 +30024,7 @@ ${pathStr}` }];
29745
30024
  }
29746
30025
  }
29747
30026
  if (config.everos?.enabled) {
29748
- pluginManager.register(new EverosPlugin(config.everos));
30027
+ pluginManager.register(new EverosPlugin(config.everos, config.providers));
29749
30028
  console.log("[everos] Plugin registered");
29750
30029
  }
29751
30030
  globalThis.__pluginManager = pluginManager;