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.
- package/CHANGELOG.md +36 -0
- package/README.md +7 -3
- package/dist/agents/claude-runner.js +23 -27
- package/dist/agents/codex-runner.js +90 -6
- package/dist/agents/runner-types.js +30 -0
- package/dist/aun/outbox.js +14 -2
- package/dist/channels/aun.js +506 -108
- package/dist/channels/feishu.js +29 -5
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +15 -3
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +12 -5027
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/core/channel-loader.js +4 -1
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +81 -0
- package/dist/core/evolagent.js +16 -0
- package/dist/core/message/im-renderer.js +67 -49
- package/dist/core/message/message-bridge.js +30 -9
- package/dist/core/message/message-processor.js +200 -122
- package/dist/core/message/message-queue.js +68 -0
- package/dist/core/permission.js +16 -0
- package/dist/core/session/session-manager.js +59 -13
- package/dist/core/stats/db.js +20 -0
- package/dist/core/stats/writer.js +3 -3
- package/dist/data/error-dict.json +7 -0
- package/dist/index.js +49 -6
- package/dist/ipc.js +99 -0
- package/dist/utils/cross-platform.js +35 -0
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +63 -6
- package/kits/eck_manifest.json +0 -12
- package/package.json +2 -3
- package/dist/core/command-handler.js +0 -4235
- package/dist/core/message/response-depth.js +0 -56
- 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(),
|
package/dist/core/evolagent.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
211
|
-
kind: 'progress',
|
|
201
|
+
this.emitProgress('progress', undefined, undefined, {
|
|
212
202
|
text,
|
|
213
203
|
state: opts.state,
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
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 (
|
|
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
|
-
||
|
|
507
|
-
||
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
}
|