evolclaw 3.3.0 → 3.4.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +7 -3
  3. package/dist/agents/claude-runner.js +23 -27
  4. package/dist/agents/codex-runner.js +90 -6
  5. package/dist/agents/runner-types.js +30 -0
  6. package/dist/aun/outbox.js +14 -2
  7. package/dist/channels/aun.js +506 -108
  8. package/dist/channels/feishu.js +29 -5
  9. package/dist/cli/agent-command.js +591 -0
  10. package/dist/cli/agent.js +15 -3
  11. package/dist/cli/aun-commands.js +1444 -0
  12. package/dist/cli/ctl-command.js +78 -0
  13. package/dist/cli/daemon-commands.js +2707 -0
  14. package/dist/cli/index.js +12 -5027
  15. package/dist/cli/restart-monitor.js +539 -0
  16. package/dist/cli/watch-logs.js +33 -0
  17. package/dist/core/channel-loader.js +4 -1
  18. package/dist/core/command/command-handler.js +1189 -0
  19. package/dist/core/command/menu-handler.js +1478 -0
  20. package/dist/core/command/slash-gate.js +142 -0
  21. package/dist/core/command/slash-handler.js +2090 -0
  22. package/dist/core/evolagent-registry.js +81 -0
  23. package/dist/core/evolagent.js +16 -0
  24. package/dist/core/message/im-renderer.js +67 -49
  25. package/dist/core/message/message-bridge.js +30 -9
  26. package/dist/core/message/message-processor.js +200 -122
  27. package/dist/core/message/message-queue.js +68 -0
  28. package/dist/core/permission.js +16 -0
  29. package/dist/core/session/session-manager.js +59 -13
  30. package/dist/core/stats/db.js +20 -0
  31. package/dist/core/stats/writer.js +3 -3
  32. package/dist/data/error-dict.json +7 -0
  33. package/dist/index.js +49 -6
  34. package/dist/ipc.js +99 -0
  35. package/dist/utils/cross-platform.js +35 -0
  36. package/dist/utils/ecweb-launch.js +49 -0
  37. package/dist/utils/error-utils.js +18 -5
  38. package/dist/utils/npm-ops.js +38 -8
  39. package/dist/utils/stats.js +63 -6
  40. package/kits/eck_manifest.json +0 -12
  41. package/package.json +2 -3
  42. package/dist/core/command-handler.js +0 -4235
  43. package/dist/core/message/response-depth.js +0 -56
  44. package/kits/templates/system-fragments/response-depth.md +0 -16
@@ -1,5 +1,7 @@
1
+ import fs from 'fs';
1
2
  import { EvolAgent } from './evolagent.js';
2
3
  import { logger } from '../utils/logger.js';
4
+ import { agentMdPath } from '../paths.js';
3
5
  import { loadDefaults, loadAllAgents, mergeForAgent, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
4
6
  // ── Channel Fingerprint ────────────────────────────────────────────────────
5
7
  // 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
@@ -333,6 +335,44 @@ export class EvolAgentRegistry {
333
335
  throw err;
334
336
  }
335
337
  }
338
+ // ── Stop / Start(运行时断连/重连,不改 config.enabled)──────────────────
339
+ async stopAgent(aidOrName, hooks) {
340
+ const agent = this.agents.get(aidOrName);
341
+ if (!agent)
342
+ throw new Error(`Agent "${aidOrName}" not found`);
343
+ if (agent.status === 'disabled')
344
+ throw new Error(`Agent is disabled; use enable/disable instead`);
345
+ if (agent.status === 'stopped')
346
+ return;
347
+ // 先断开 AID 连接(下线),让未送达的消息保留在云端;
348
+ // 然后中断正在执行的大模型调用(不等它跑完)。
349
+ for (const ch of agent.channelInstanceNames()) {
350
+ try {
351
+ await hooks.disconnectChannel(ch);
352
+ }
353
+ catch { }
354
+ }
355
+ agent.status = 'stopped';
356
+ this.channelIndex.clear();
357
+ this.buildChannelIndex();
358
+ logger.info(`[Registry] Stopped agent ${aidOrName}`);
359
+ }
360
+ async startAgent(aidOrName, hooks) {
361
+ const agent = this.agents.get(aidOrName);
362
+ if (!agent)
363
+ throw new Error(`Agent "${aidOrName}" not found`);
364
+ if (agent.status === 'disabled')
365
+ throw new Error(`Agent is disabled; use enable instead`);
366
+ if (agent.status === 'running')
367
+ return;
368
+ for (const ch of agent.channelInstanceNames()) {
369
+ await hooks.startChannel(agent, ch);
370
+ }
371
+ agent.status = 'running';
372
+ this.channelIndex.clear();
373
+ this.buildChannelIndex();
374
+ logger.info(`[Registry] Started agent ${aidOrName}`);
375
+ }
336
376
  checkConflictForReload(newRaw, excludeAid) {
337
377
  const newFps = new Set();
338
378
  for (const inst of newRaw.channels) {
@@ -353,9 +393,50 @@ export class EvolAgentRegistry {
353
393
  }
354
394
  return null;
355
395
  }
396
+ // ── 友好名缓存(从本地 agent.md 解析,缺失时异步从网络拉取)──
397
+ displayNameCache = new Map();
398
+ displayNamePending = new Set();
399
+ resolveDisplayName(aid) {
400
+ const cached = this.displayNameCache.get(aid);
401
+ if (cached)
402
+ return cached;
403
+ try {
404
+ const mdPath = agentMdPath(aid);
405
+ if (fs.existsSync(mdPath)) {
406
+ const content = fs.readFileSync(mdPath, 'utf-8');
407
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
408
+ if (fm) {
409
+ const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
410
+ if (nm?.[1]) {
411
+ this.displayNameCache.set(aid, nm[1]);
412
+ return nm[1];
413
+ }
414
+ }
415
+ }
416
+ }
417
+ catch { /* ignore */ }
418
+ // 异步从网络拉取(仅一次,不阻塞)
419
+ if (!this.displayNamePending.has(aid)) {
420
+ this.displayNamePending.add(aid);
421
+ import('../aun/aid/index.js').then(({ agentmdGet }) => {
422
+ agentmdGet(aid).then(content => {
423
+ if (typeof content === 'string') {
424
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
425
+ if (fm) {
426
+ const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
427
+ if (nm?.[1])
428
+ this.displayNameCache.set(aid, nm[1]);
429
+ }
430
+ }
431
+ }).catch(() => { }).finally(() => this.displayNamePending.delete(aid));
432
+ }).catch(() => { this.displayNamePending.delete(aid); });
433
+ }
434
+ return undefined;
435
+ }
356
436
  toInfo(agent) {
357
437
  return {
358
438
  name: agent.name,
439
+ displayName: this.resolveDisplayName(agent.aid),
359
440
  aid: agent.aid,
360
441
  status: agent.status,
361
442
  channels: agent.channelInstanceNames(),
@@ -177,6 +177,14 @@ export class EvolAgent {
177
177
  delete block.model;
178
178
  else
179
179
  block.model = value;
180
+ // sync merged so getter reflects the change immediately
181
+ if (!this.merged.baseagents)
182
+ this.merged.baseagents = {};
183
+ const mBlock = ((this.merged.baseagents)[ba] ??= {});
184
+ if (value === undefined)
185
+ delete mBlock.model;
186
+ else
187
+ mBlock.model = value;
180
188
  this.persist();
181
189
  }
182
190
  setBaseagentEffort(value) {
@@ -189,6 +197,14 @@ export class EvolAgent {
189
197
  delete block[fieldName];
190
198
  else
191
199
  block[fieldName] = value;
200
+ // sync merged so getter reflects the change immediately
201
+ if (!this.merged.baseagents)
202
+ this.merged.baseagents = {};
203
+ const mBlock = ((this.merged.baseagents)[ba] ??= {});
204
+ if (value === undefined)
205
+ delete mBlock[fieldName];
206
+ else
207
+ mBlock[fieldName] = value;
192
208
  this.persist();
193
209
  }
194
210
  /** 设置私聊 chatmode(群聊/非 human 强制 proactive,无可写入项)。 */
@@ -1,23 +1,9 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { summarizeToolInput } from '../permission.js';
3
+ import { CONTEXT_TOO_LONG_PATTERN, isContextTooLongText } from '../../utils/error-utils.js';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
6
  import { resolvePaths } from '../../paths.js';
6
- /**
7
- * 检测是否为上下文过长错误
8
- * 统一的检测逻辑,覆盖所有已知的错误文本模式
9
- */
10
- function isContextTooLongError(text) {
11
- if (!text)
12
- return false;
13
- const lower = text.toLowerCase();
14
- return (lower.includes('prompt is too long') ||
15
- lower.includes('input is too long') ||
16
- lower.includes('context too long') ||
17
- lower.includes('context limit') ||
18
- lower.includes('context_length_exceeded') ||
19
- text.includes('上下文过长'));
20
- }
21
7
  let diagStream = null;
22
8
  function getDiagStream() {
23
9
  if (!diagStream) {
@@ -107,11 +93,16 @@ export class IMRenderer {
107
93
  clearTimeout(this.timer);
108
94
  this.timer = undefined;
109
95
  }
110
- const text = this.textBuffer;
96
+ // 文件标记过滤:tool_use 前的文本块也不能把 marker 暴露给用户
97
+ const rawText = this.opts.fileMarkerPattern
98
+ ? this.textBuffer.replace(this.opts.fileMarkerPattern, '')
99
+ : this.textBuffer;
111
100
  this.textBuffer = '';
101
+ if (!rawText)
102
+ return;
112
103
  // 清掉 itemsQueue 中的 text items(已发出)
113
104
  this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
114
- const payload = { kind: 'result.text', text, isFinal: false };
105
+ const payload = { kind: 'result.text', text: rawText, isFinal: false };
115
106
  this.sentContent = true;
116
107
  this.sendChain = this.sendChain
117
108
  .then(() => this.opts.send(payload))
@@ -135,9 +126,9 @@ export class IMRenderer {
135
126
  // ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
136
127
  /** 添加文本片段(流式 text) */
137
128
  addText(text, outputTokens, turn) {
138
- this.emitProgress('text', outputTokens, turn);
139
129
  if (this.opts.envelope.chatmode === 'proactive')
140
130
  return;
131
+ this.emitProgress('text', outputTokens, turn);
141
132
  if (!text)
142
133
  return;
143
134
  // 同一窗口内连续 text delta 合并到最后一个 text item
@@ -162,9 +153,9 @@ export class IMRenderer {
162
153
  }
163
154
  /** 添加工具调用 */
164
155
  addToolCall(name, input, callId, descText, turn, outputTokens) {
165
- this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
166
156
  if (this.opts.envelope.chatmode === 'proactive')
167
157
  return;
158
+ this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
168
159
  if (this.opts.suppressActivities)
169
160
  return;
170
161
  this.itemsQueue.push({
@@ -181,9 +172,9 @@ export class IMRenderer {
181
172
  }
182
173
  /** 添加工具结果 */
183
174
  addToolResult(name, ok, result, error, callId, durationMs, descText) {
184
- this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
185
175
  if (this.opts.envelope.chatmode === 'proactive')
186
176
  return;
177
+ this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
187
178
  if (this.opts.suppressActivities)
188
179
  return;
189
180
  this.itemsQueue.push({
@@ -207,20 +198,35 @@ export class IMRenderer {
207
198
  return;
208
199
  if (this.opts.suppressActivities)
209
200
  return;
210
- this.itemsQueue.push({
211
- kind: 'progress',
201
+ this.emitProgress('progress', undefined, undefined, {
212
202
  text,
213
203
  state: opts.state,
214
- tool_uses: opts.toolUses,
215
- duration_ms: opts.durationMs,
204
+ toolUses: opts.toolUses,
205
+ durationMs: opts.durationMs,
216
206
  });
217
- this.messageTimestamps.push(Date.now());
218
- this.scheduleFlush();
219
207
  }
208
+ /**
209
+ * proactive 下放行为 thought 的 notice subtype 白名单——仅"真·终态错误":
210
+ * - context-too-long:上下文超限且无法 auto-compact,任务到此终止
211
+ * - process-exit:Agent 子进程异常崩溃(无 complete 事件,emit 完全覆盖不到)
212
+ * 二者都是用户必须知道、否则会困惑"任务为什么停了"的终态信号。
213
+ *
214
+ * 其余 subtype 一律不发:
215
+ * - compact / runtime-error / task-error:emit() 路径(mapEventToItem)已投影,重复
216
+ * - compact-start / compact-trigger / compact-retry / retry:内部机务噪音(压缩中/
217
+ * 重试中/压缩完成),proactive 下用户要的是工作产出而非流水账,压缩后 thought 会
218
+ * 继续输出,过程本身无需播报。
219
+ */
220
+ static PROACTIVE_NOTICE_ALLOW = new Set(['context-too-long', 'process-exit']);
220
221
  /** 添加系统提示 / 通知。force=true 时绕过 suppressActivities(用于 compact/retry/error 等操作反馈) */
221
222
  addNotice(text, severity, subtype, force = false) {
222
- if (this.opts.envelope.chatmode === 'proactive')
223
+ // proactive 模式:只放行真·终态错误,机务噪音(压缩/重试)和 emit 已覆盖的 subtype 均不发。
224
+ if (this.opts.envelope.chatmode === 'proactive') {
225
+ if (subtype == null || !IMRenderer.PROACTIVE_NOTICE_ALLOW.has(subtype))
226
+ return;
227
+ this.emitProactiveItem({ kind: 'notice', text, severity, subtype });
223
228
  return;
229
+ }
224
230
  if (this.opts.suppressActivities && !force)
225
231
  return;
226
232
  this.itemsQueue.push({ kind: 'notice', text, severity, subtype });
@@ -308,10 +314,25 @@ export class IMRenderer {
308
314
  clearTimeout(this.timer);
309
315
  this.timer = undefined;
310
316
  }
317
+ // 文件标记过滤:marker 任何时候都不该出现在用户文本里,必须在 *每次* flush
318
+ // (含非最终的定时 flush)执行。否则文本块若滞留 buffer 后被定时器触发,会走
319
+ // 下方非-final 的 result.text 路径,把 [SEND_FILE:...] 原样发给用户。
320
+ // 非最终 flush 不做 trim,避免 trim 掉 Markdown 块级换行。
321
+ if (this.opts.fileMarkerPattern) {
322
+ const strip = (s) => {
323
+ const replaced = s.replace(this.opts.fileMarkerPattern, '');
324
+ return isFinal ? replaced.trim() : replaced;
325
+ };
326
+ this.textBuffer = strip(this.textBuffer);
327
+ for (const item of this.itemsQueue) {
328
+ if (item.kind === 'text')
329
+ item.text = item.text.replace(this.opts.fileMarkerPattern, '');
330
+ }
331
+ }
311
332
  if (isFinal) {
312
333
  // 上下文错误短语过滤:剔除错误关键词本身,保留前后内容。
313
334
  // 只在最终 flush 清理,避免中间定时 flush trim 掉 Markdown 块级换行。
314
- const ctxErrPattern = /prompt is too long|input is too long|context too long|context limit|context_length_exceeded|上下文过长/gi;
335
+ const ctxErrPattern = new RegExp(CONTEXT_TOO_LONG_PATTERN.source, 'gi');
315
336
  const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
316
337
  this.textBuffer = stripCtxErr(this.textBuffer);
317
338
  this.allText = stripCtxErr(this.allText);
@@ -319,14 +340,6 @@ export class IMRenderer {
319
340
  if (item.kind === 'text')
320
341
  item.text = stripCtxErr(item.text);
321
342
  }
322
- // 文件标记过滤
323
- if (this.opts.fileMarkerPattern) {
324
- this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
325
- for (const item of this.itemsQueue) {
326
- if (item.kind === 'text')
327
- item.text = item.text.replace(this.opts.fileMarkerPattern, '');
328
- }
329
- }
330
343
  }
331
344
  // 清掉空 text items
332
345
  const items = this.itemsQueue.filter(it => {
@@ -402,6 +415,9 @@ export class IMRenderer {
402
415
  ...(extra?.callId != null && { callId: extra.callId }),
403
416
  ...(extra?.ok != null && { ok: extra.ok }),
404
417
  ...(extra?.durationMs != null && { durationMs: extra.durationMs }),
418
+ ...(extra?.text != null && { text: extra.text }),
419
+ ...(extra?.state != null && { state: extra.state }),
420
+ ...(extra?.toolUses != null && { toolUses: extra.toolUses }),
405
421
  },
406
422
  };
407
423
  this.opts.send(payload).catch(() => { });
@@ -422,17 +438,19 @@ export class IMRenderer {
422
438
  this.hasEmittedText = true;
423
439
  this.allText += item.text;
424
440
  }
425
- const outputTokens = event.outputTokens;
426
- const turn = event.turn;
427
- const activityType = item.kind === 'text' ? 'text' : item.kind === 'tool_call' ? 'tool_call' : 'tool_result';
428
- const extra = item.kind === 'tool_call'
429
- ? { toolName: item.name, callId: item.call_id }
430
- : item.kind === 'tool_result'
431
- ? { toolName: item.name, callId: item.call_id, ok: item.ok, durationMs: item.duration_ms }
432
- : undefined;
433
- this.emitProgress(activityType, outputTokens, turn, extra);
441
+ // proactive 模式:status.progress 是 interactive 的处理状态指示器,proactive 下
442
+ // 过程由 thought(activity.batch)表达,不再发 status.progress(与 addProgress 在
443
+ // proactive 下的拦截保持一致)。progress-kind item 若进 activity.batch 会被 aun
444
+ // 回转成 status.progress(见 aun.ts 'activity.batch' 分支),故直接丢弃。
445
+ // 终态 status(started/completed/interrupted/error)由 message-processor 发送,不受影响。
446
+ if (item.kind === 'progress') {
447
+ return;
448
+ }
449
+ this.emitProactiveItem(item);
450
+ }
451
+ /** proactive 模式逐条投影:单个 ThoughtItem 包成 activity.batch[1] 发出(fire-and-forget)。 */
452
+ emitProactiveItem(item) {
434
453
  const payload = { kind: 'activity.batch', items: [item] };
435
- // fire-and-forget
436
454
  this.opts.send(payload).catch(err => {
437
455
  logger.debug(`[IMRenderer] proactive send failed: ${err.message}`);
438
456
  });
@@ -496,15 +514,15 @@ export class IMRenderer {
496
514
  }
497
515
  case 'error': {
498
516
  // 上下文过长错误不输出(留给外层 auto-compact 处理)
499
- if (isContextTooLongError(event.error || ''))
517
+ if (isContextTooLongText(event.error || ''))
500
518
  return null;
501
519
  return { kind: 'notice', text: event.error, severity: 'warn' };
502
520
  }
503
521
  case 'complete': {
504
522
  // 上下文过长错误不输出(留给外层 auto-compact 处理)
505
523
  const hasContextError = event.terminalReason === 'prompt_too_long'
506
- || isContextTooLongError(event.errors?.join(' ') || '')
507
- || isContextTooLongError(event.result || '');
524
+ || isContextTooLongText(event.errors?.join(' ') || '')
525
+ || isContextTooLongText(event.result || '');
508
526
  if (event.isError && hasContextError) {
509
527
  return null;
510
528
  }
@@ -110,7 +110,11 @@ export class MessageBridge {
110
110
  // 3. session 解析(使用 Channel 层填充的 chatType)
111
111
  const chatType = msg.chatType || 'private';
112
112
  if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
113
- await sendReply(msg.channelId, '群聊中无权限创建话题', msg.replyContext);
113
+ // 静默丢弃:绝不向群里注入回复。
114
+ // 拒绝消息本身会带原 thread_id(AUN replyContext 透传),变成一条新群消息;
115
+ // 若发送者也是 agent,该拒绝消息又会 @ 回对方 → A 拒绝→B 收到→B 拒绝→A 收到 的无限循环。
116
+ // AUN 自主模式下「不响应」是合法的,因此无权限创建话题时只记日志、直接 return。
117
+ logger.info(`[MessageBridge] Thread creation denied (silent drop): channel=${channelName} channelId=${msg.channelId} thread=${msg.threadId} sender=${msg.peerId}`);
114
118
  return;
115
119
  }
116
120
  const metadata = {};
@@ -231,6 +235,7 @@ export class MessageBridge {
231
235
  cli: '/cli',
232
236
  agent: '/agent',
233
237
  trigger: '/trigger',
238
+ file: '/file',
234
239
  };
235
240
  extractTopicName(msg) {
236
241
  const raw = msg.topicName
@@ -246,8 +251,16 @@ export class MessageBridge {
246
251
  const existing = await this.sessionManager.getThreadSession(channel, msg.channelId, msg.threadId);
247
252
  if (existing)
248
253
  return true;
249
- const role = this.sessionManager.resolveIdentity(channel, msg.peerId).role;
250
- return role === 'owner' || role === 'admin';
254
+ // 群话题创建权限只看「发送者在该群里的角色」(AUN group.get_admins 实时查询,权威源)。
255
+ // 仅群 owner/admin 可建话题;member / 非成员 / 查询失败(undefined)一律 fail-closed 拒绝。
256
+ // 这与 bot 的 owner/admin 无关——不引入 resolveIdentity 兜底。
257
+ // 不暴露群角色的渠道(adapter 无 getGroupMemberRole)不受此守卫约束,放行。
258
+ const adapter = this.processor?.getChannelInfo?.(channel)?.adapter;
259
+ if (adapter?.getGroupMemberRole) {
260
+ const groupRole = await adapter.getGroupMemberRole(msg.channelId, msg.peerId);
261
+ return groupRole === 'owner' || groupRole === 'admin';
262
+ }
263
+ return true;
251
264
  }
252
265
  resolveCmd(name, cmd) {
253
266
  if (cmd)
@@ -292,7 +305,7 @@ export class MessageBridge {
292
305
  const { id } = req;
293
306
  try {
294
307
  const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
295
- const data = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
308
+ const data = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent');
296
309
  await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, data }, sendReply);
297
310
  }
298
311
  catch (err) {
@@ -306,7 +319,7 @@ export class MessageBridge {
306
319
  const { id, name, cmd } = req;
307
320
  try {
308
321
  const resolvedCmd = this.resolveCmd(name, cmd);
309
- const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId, req.args, msg.chatType);
322
+ const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId, req.args, msg.chatType, msg.isControlChannel ?? false);
310
323
  if ('error' in result)
311
324
  throw { code: result.code || 'EXEC_FAILED', message: result.error };
312
325
  await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
@@ -322,7 +335,7 @@ export class MessageBridge {
322
335
  const { id, name, cmd } = req;
323
336
  try {
324
337
  const resolvedCmd = this.resolveCmd(name, cmd);
325
- const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId, req.args, undefined, msg.chatType) ?? [];
338
+ const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId, req.args, undefined, msg.chatType, msg.isControlChannel ?? false) ?? [];
326
339
  await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data }, sendReply);
327
340
  }
328
341
  catch (err) {
@@ -333,12 +346,12 @@ export class MessageBridge {
333
346
  }
334
347
  }
335
348
  async handleMenuUpdate(req, channel, msg, adapter, sendReply) {
336
- const { id, name, cmd, value } = req;
349
+ const { id, name, cmd, value, args } = req;
337
350
  try {
338
351
  if (!value)
339
352
  throw { code: 'MISSING_VALUE', message: '缺少 value 参数' };
340
353
  const resolvedCmd = this.resolveCmd(name, cmd);
341
- const result = await this.cmdHandler.execMenuUpdate(resolvedCmd, value, channel, msg.channelId, msg.peerId);
354
+ const result = await this.cmdHandler.execMenuUpdate(resolvedCmd, value, channel, msg.channelId, msg.peerId, undefined, msg.isControlChannel ?? false, args);
342
355
  if ('error' in result)
343
356
  throw { code: result.code || 'EXEC_FAILED', message: result.error };
344
357
  await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
@@ -356,7 +369,7 @@ export class MessageBridge {
356
369
  if (!action)
357
370
  throw { code: 'MISSING_VALUE', message: '缺少 action 参数' };
358
371
  const resolvedCmd = this.resolveCmd(name, cmd);
359
- const result = await this.cmdHandler.execMenuAction(resolvedCmd, action, args, channel, msg.channelId, msg.peerId, undefined, msg.chatType);
372
+ const result = await this.cmdHandler.execMenuAction(resolvedCmd, action, args, channel, msg.channelId, msg.peerId, undefined, msg.chatType, id, msg.isControlChannel ?? false);
360
373
  if ('error' in result)
361
374
  throw { code: result.code || 'EXEC_FAILED', message: result.error };
362
375
  await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
@@ -473,4 +486,12 @@ export class MessageBridge {
473
486
  d.dispose();
474
487
  this.debouncers.clear();
475
488
  }
489
+ /** 注销单个渠道的 debouncer(热重载断开渠道时调用) */
490
+ removeChannel(channelName) {
491
+ const d = this.debouncers.get(channelName);
492
+ if (d) {
493
+ d.dispose();
494
+ this.debouncers.delete(channelName);
495
+ }
496
+ }
476
497
  }