evolclaw 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels/aun.js +64 -8
- package/dist/channels/feishu.js +4 -3
- package/dist/cli.js +104 -7
- package/dist/config.js +1 -1
- package/dist/core/command-handler.js +99 -2
- package/dist/core/message/message-processor.js +49 -48
- package/dist/core/message/message-queue.js +10 -2
- package/dist/core/message/stream-debouncer.js +9 -1
- package/dist/core/message/thought-emitter.js +146 -0
- package/dist/index.js +51 -30
- package/dist/templates/skills.md +5 -3
- package/dist/utils/init-channel.js +78 -5
- package/dist/utils/init.js +3 -3
- package/dist/utils/upgrade.js +100 -0
- package/package.json +3 -3
package/dist/channels/aun.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUNClient, GatewayDiscovery } from '@agentunion/
|
|
1
|
+
import { AUNClient, GatewayDiscovery } from '@agentunion/fastaun';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
@@ -317,7 +317,8 @@ export class AUNChannel {
|
|
|
317
317
|
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
318
318
|
logger.warn(`[AUN] Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
|
|
319
319
|
}
|
|
320
|
-
// Name:
|
|
320
|
+
// Name: prefer existing agent.md name if user has customized it,
|
|
321
|
+
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
321
322
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
322
323
|
let ownerDisplayName;
|
|
323
324
|
if (ownerInfo.name) {
|
|
@@ -326,7 +327,18 @@ export class AUNChannel {
|
|
|
326
327
|
else {
|
|
327
328
|
ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
328
329
|
}
|
|
329
|
-
|
|
330
|
+
// Check if init wrote a meaningful name (vs just the aid first label default)
|
|
331
|
+
const currentNameMatch = frontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
332
|
+
const currentName = currentNameMatch?.[1]?.trim();
|
|
333
|
+
const aidLabel = aidName.split('.')[0];
|
|
334
|
+
let agentDisplayName;
|
|
335
|
+
if (currentName && currentName !== aidLabel) {
|
|
336
|
+
// User or previous init set a custom name — keep it
|
|
337
|
+
agentDisplayName = currentName;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
|
|
341
|
+
}
|
|
330
342
|
// Generate new agent.md with proper fields
|
|
331
343
|
const newAgentMd = `---
|
|
332
344
|
aid: "${aid}"
|
|
@@ -531,11 +543,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
531
543
|
this.acknowledgeImmediately(messageId, seq);
|
|
532
544
|
return;
|
|
533
545
|
}
|
|
546
|
+
// dispatch_mode from server tells agent how to work in this group
|
|
547
|
+
const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
534
548
|
const mentionedSelf = this._aid
|
|
535
549
|
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
536
550
|
: false;
|
|
537
551
|
const mentionedAll = this.hasExplicitMention(text, 'all') || payloadMentions.includes('all');
|
|
538
|
-
|
|
552
|
+
// In mention mode, only respond when explicitly mentioned; in broadcast mode, respond to all
|
|
553
|
+
if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
|
|
539
554
|
this.acknowledgeImmediately(messageId, seq);
|
|
540
555
|
return;
|
|
541
556
|
}
|
|
@@ -550,7 +565,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
550
565
|
this.acknowledgeImmediately(messageId, seq);
|
|
551
566
|
return;
|
|
552
567
|
}
|
|
553
|
-
const mentions = mentionedAll
|
|
568
|
+
const mentions = mentionedAll
|
|
569
|
+
? ['all']
|
|
570
|
+
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
554
571
|
// Process attachments
|
|
555
572
|
let finalText = strippedText;
|
|
556
573
|
if (hasAttachments && this.client) {
|
|
@@ -732,6 +749,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
732
749
|
logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
|
|
733
750
|
}
|
|
734
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* 发送 thought 内容(Proactive 模式可观测)
|
|
754
|
+
* 群聊:调用 group.thought.put
|
|
755
|
+
* 单聊:调用 message.thought.put
|
|
756
|
+
*
|
|
757
|
+
* selector 使用 context: { type: 'task', id: taskId }
|
|
758
|
+
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
759
|
+
*/
|
|
760
|
+
async sendThought(channelId, taskId, payload) {
|
|
761
|
+
if (!this.connected || !this.client)
|
|
762
|
+
return;
|
|
763
|
+
if (!taskId)
|
|
764
|
+
return;
|
|
765
|
+
// Multi-instance routing
|
|
766
|
+
const colonIdx = channelId.indexOf(':');
|
|
767
|
+
const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
768
|
+
const params = {
|
|
769
|
+
context: { type: 'task', id: taskId },
|
|
770
|
+
payload,
|
|
771
|
+
encrypt: true,
|
|
772
|
+
};
|
|
773
|
+
try {
|
|
774
|
+
if (this.isGroupId(channelId)) {
|
|
775
|
+
params.group_id = targetId;
|
|
776
|
+
this.trace('OUT', 'group.thought.put', params);
|
|
777
|
+
await this.client.call('group.thought.put', params);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
params.to = targetId;
|
|
781
|
+
this.trace('OUT', 'message.thought.put', params);
|
|
782
|
+
await this.client.call('message.thought.put', params);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
this.trace('OUT', 'thought.put.error', { channelId, error: String(e) });
|
|
787
|
+
logger.debug(`[AUN] thought.put failed to ${channelId}: ${e}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
735
790
|
async sendFile(channelId, filePath, context) {
|
|
736
791
|
if (!this.connected || !this.client) {
|
|
737
792
|
logger.warn('[AUN] Cannot sendFile: not connected');
|
|
@@ -834,7 +889,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
834
889
|
// to avoid duplicate "已送达" at the sender CLI
|
|
835
890
|
this.messageSeqMap.delete(messageId);
|
|
836
891
|
}
|
|
837
|
-
sendProcessingStatus(channelId, status, sessionId, context) {
|
|
892
|
+
sendProcessingStatus(channelId, status, sessionId, taskId, context) {
|
|
838
893
|
if (status === 'start')
|
|
839
894
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
840
895
|
if (!this.client || !this.connected)
|
|
@@ -849,7 +904,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
849
904
|
const payload = {
|
|
850
905
|
type: 'event',
|
|
851
906
|
event: eventMap[status] ?? `task.${status}`,
|
|
852
|
-
data: { session_id: sessionId },
|
|
907
|
+
data: { task_id: taskId, session_id: sessionId },
|
|
853
908
|
severity: status === 'error' || status === 'timeout' ? 'error' : 'info',
|
|
854
909
|
};
|
|
855
910
|
if (context?.threadId)
|
|
@@ -1048,10 +1103,11 @@ export class AUNChannelPlugin {
|
|
|
1048
1103
|
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
1049
1104
|
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
1050
1105
|
acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
|
|
1051
|
-
sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
|
|
1106
|
+
sendProcessingStatus: (id, status, sessionId, taskId, context) => channel.sendProcessingStatus(id, status, sessionId, taskId, context),
|
|
1052
1107
|
sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
|
|
1053
1108
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
1054
1109
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
1110
|
+
putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
|
|
1055
1111
|
_selfAid: () => channel.getStatus().aid,
|
|
1056
1112
|
};
|
|
1057
1113
|
const policy = {
|
package/dist/channels/feishu.js
CHANGED
|
@@ -499,11 +499,12 @@ export class FeishuChannel {
|
|
|
499
499
|
return;
|
|
500
500
|
try {
|
|
501
501
|
// 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
|
|
502
|
-
|
|
502
|
+
// 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
|
|
503
|
+
const header = Buffer.alloc(4100);
|
|
503
504
|
const fd = fs.openSync(filePath, 'r');
|
|
504
|
-
fs.readSync(fd, header, 0,
|
|
505
|
+
const bytesRead = fs.readSync(fd, header, 0, 4100, 0);
|
|
505
506
|
fs.closeSync(fd);
|
|
506
|
-
const imgType = await imageType(header);
|
|
507
|
+
const imgType = await imageType(header.subarray(0, bytesRead)).catch(() => undefined);
|
|
507
508
|
if (imgType) {
|
|
508
509
|
logger.info(`[Feishu] Detected image (${imgType.mime}), sending as inline image:`, filePath);
|
|
509
510
|
const buf = fs.readFileSync(filePath);
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ipcQuery } from './ipc.js';
|
|
|
12
12
|
import { cmdInitWechat, cmdInitFeishu, cmdInitAun, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './utils/init-channel.js';
|
|
13
13
|
import * as platform from './utils/cross-platform.js';
|
|
14
14
|
import { EventBus } from './core/event-bus.js';
|
|
15
|
+
import { tryUpgrade } from './utils/upgrade.js';
|
|
15
16
|
// Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
|
|
16
17
|
process.removeAllListeners('warning');
|
|
17
18
|
process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
|
|
@@ -372,6 +373,25 @@ async function cmdStop() {
|
|
|
372
373
|
async function cmdRestart() {
|
|
373
374
|
console.log('🔄 Restarting EvolClaw...');
|
|
374
375
|
const p = resolvePaths();
|
|
376
|
+
// 版本检查与自动升级
|
|
377
|
+
console.log('📦 Checking for updates...');
|
|
378
|
+
const upgrade = await tryUpgrade();
|
|
379
|
+
switch (upgrade.status) {
|
|
380
|
+
case 'upgraded':
|
|
381
|
+
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
382
|
+
break;
|
|
383
|
+
case 'no-update':
|
|
384
|
+
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
385
|
+
break;
|
|
386
|
+
case 'skipped':
|
|
387
|
+
console.log(upgrade.error
|
|
388
|
+
? '⏭ Skipped upgrade (network unavailable)'
|
|
389
|
+
: '⏭ Skipped upgrade check (dev mode)');
|
|
390
|
+
break;
|
|
391
|
+
case 'failed':
|
|
392
|
+
console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
375
395
|
await stopAndWait(p.pid);
|
|
376
396
|
setTimeout(() => cmdStart(), 1000);
|
|
377
397
|
}
|
|
@@ -737,6 +757,27 @@ async function cmdRestartMonitor() {
|
|
|
737
757
|
});
|
|
738
758
|
await sleep(3000);
|
|
739
759
|
}
|
|
760
|
+
// 版本检查与自动升级
|
|
761
|
+
log('Checking for updates...');
|
|
762
|
+
const upgrade = await tryUpgrade();
|
|
763
|
+
switch (upgrade.status) {
|
|
764
|
+
case 'upgraded':
|
|
765
|
+
log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
766
|
+
await notifyChannel(p, pendingInfo, `📦 已升级 ${upgrade.from} → ${upgrade.to}`, log);
|
|
767
|
+
break;
|
|
768
|
+
case 'no-update':
|
|
769
|
+
log(`Already up to date (${upgrade.from})`);
|
|
770
|
+
break;
|
|
771
|
+
case 'skipped':
|
|
772
|
+
log(upgrade.error
|
|
773
|
+
? 'Skipped upgrade (network unavailable)'
|
|
774
|
+
: 'Skipped upgrade check (dev mode)');
|
|
775
|
+
break;
|
|
776
|
+
case 'failed':
|
|
777
|
+
log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
|
|
778
|
+
await notifyChannel(p, pendingInfo, `⚠️ 升级失败,使用当前版本继续`, log);
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
740
781
|
// 启动并检测 ready signal
|
|
741
782
|
let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
742
783
|
if (started) {
|
|
@@ -1244,14 +1285,43 @@ async function cmdDiagnose() {
|
|
|
1244
1285
|
// ==================== Ctl ====================
|
|
1245
1286
|
async function cmdCtl(args) {
|
|
1246
1287
|
if (args.length === 0) {
|
|
1247
|
-
console.error(
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1288
|
+
console.error(`用法: evolclaw ctl <command> [args...]
|
|
1289
|
+
|
|
1290
|
+
查询:
|
|
1291
|
+
status 查看会话状态
|
|
1292
|
+
check 检查渠道健康状态
|
|
1293
|
+
help 显示帮助
|
|
1294
|
+
|
|
1295
|
+
配置:
|
|
1296
|
+
model [model-id] 查看/切换模型(如 opus, sonnet, haiku)
|
|
1297
|
+
effort [low|medium|high] 查看/切换推理强度
|
|
1298
|
+
compact 压缩当前会话上下文
|
|
1299
|
+
chatmode [mode] 查看/切换会话模式
|
|
1300
|
+
activity [all|dm|owner|none] 查看/控制中间输出显示模式
|
|
1301
|
+
perm [mode] 查看/切换权限模式
|
|
1302
|
+
|
|
1303
|
+
项目:
|
|
1304
|
+
bind <path> 注册项目目录(不切换当前会话)
|
|
1305
|
+
|
|
1306
|
+
消息:
|
|
1307
|
+
send <消息内容> 主动发送文本消息(proactive 模式)
|
|
1308
|
+
file [channel] <path> 发送项目内文件
|
|
1309
|
+
|
|
1310
|
+
运维:
|
|
1311
|
+
agentmd [put|set <内容>] 查看/管理 agent.md(仅 AUN 通道)
|
|
1312
|
+
restart [channel] 重启服务或重连指定渠道
|
|
1313
|
+
|
|
1314
|
+
示例:
|
|
1315
|
+
evolclaw ctl model sonnet
|
|
1316
|
+
evolclaw ctl effort high
|
|
1317
|
+
evolclaw ctl compact
|
|
1318
|
+
evolclaw ctl chatmode proactive`);
|
|
1253
1319
|
process.exit(1);
|
|
1254
1320
|
}
|
|
1321
|
+
// help 不需要连接服务,直接复用无参数时的帮助输出
|
|
1322
|
+
if (args[0] === 'help') {
|
|
1323
|
+
return cmdCtl([]);
|
|
1324
|
+
}
|
|
1255
1325
|
const sessionId = process.env.EVOLCLAW_SESSION_ID;
|
|
1256
1326
|
if (!sessionId) {
|
|
1257
1327
|
console.error('错误: EVOLCLAW_SESSION_ID 未设置(仅在 evolclaw 托管环境中可用)');
|
|
@@ -1294,7 +1364,32 @@ export async function main(args) {
|
|
|
1294
1364
|
}
|
|
1295
1365
|
switch (cmd) {
|
|
1296
1366
|
case 'init':
|
|
1297
|
-
if (args[1] === '
|
|
1367
|
+
if (args[1] === 'help') {
|
|
1368
|
+
console.log(`用法: evolclaw init [渠道] [选项]
|
|
1369
|
+
|
|
1370
|
+
交互式初始化:
|
|
1371
|
+
evolclaw init 创建基础配置文件(交互式)
|
|
1372
|
+
evolclaw init feishu 飞书扫码登录并写入配置
|
|
1373
|
+
evolclaw init wechat 微信扫码登录并写入配置
|
|
1374
|
+
evolclaw init dingtalk 钉钉扫码登录并写入配置
|
|
1375
|
+
evolclaw init qqbot QQ 机器人扫码绑定并写入配置
|
|
1376
|
+
evolclaw init wecom 企业微信 AI Bot 配置(手动输入)
|
|
1377
|
+
evolclaw init aun AUN 交互式配置(AID 创建 + Owner 绑定)
|
|
1378
|
+
|
|
1379
|
+
非交互式初始化:
|
|
1380
|
+
evolclaw init --non-interactive [选项]
|
|
1381
|
+
|
|
1382
|
+
选项:
|
|
1383
|
+
--default-path <path> 项目目录(默认: 当前目录)
|
|
1384
|
+
--channel <name> 渠道类型(默认: aun)
|
|
1385
|
+
--aun-aid <aid> AUN Agent ID(必填,如 mybot.agentid.pub)
|
|
1386
|
+
--aun-owner <aid> Owner AID(可选,如 alice.agentid.pub)
|
|
1387
|
+
|
|
1388
|
+
示例:
|
|
1389
|
+
evolclaw init --non-interactive --aun-aid mybot.agentid.pub --aun-owner alice.agentid.pub
|
|
1390
|
+
evolclaw init --non-interactive --default-path /home/user/project --aun-aid bot.agentid.pub`);
|
|
1391
|
+
}
|
|
1392
|
+
else if (args[1] === 'wechat') {
|
|
1298
1393
|
await cmdInitWechat();
|
|
1299
1394
|
}
|
|
1300
1395
|
else if (args[1] === 'feishu') {
|
|
@@ -1380,6 +1475,8 @@ Commands:
|
|
|
1380
1475
|
--level error|warn 只显示指定级别及以上
|
|
1381
1476
|
--module <name> 只显示指定模块(如 feishu、AgentRunner)
|
|
1382
1477
|
--raw 原始输出,不着色
|
|
1478
|
+
ctl 运行时自管理(模型切换、推理强度、压缩上下文等)
|
|
1479
|
+
evolclaw ctl help 查看完整命令列表
|
|
1383
1480
|
diagnose 诊断启动环境(配置、数据库、进程)
|
|
1384
1481
|
mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
|
|
1385
1482
|
|
package/dist/config.js
CHANGED
|
@@ -177,7 +177,7 @@ export function saveConfig(config, configPath = resolvePaths().config) {
|
|
|
177
177
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
178
178
|
}
|
|
179
179
|
// ── Channel instance normalization ──
|
|
180
|
-
export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot'];
|
|
180
|
+
export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
|
|
181
181
|
/**
|
|
182
182
|
* Normalize a channel config value (single object, array, or undefined) into an array
|
|
183
183
|
* where every element has a `name` field.
|
|
@@ -103,7 +103,7 @@ function formatIdleTime(ms) {
|
|
|
103
103
|
return '刚刚';
|
|
104
104
|
}
|
|
105
105
|
// 支持的命令列表
|
|
106
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/agentmd', '/chatmode'];
|
|
106
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode'];
|
|
107
107
|
// 命令别名映射
|
|
108
108
|
const aliases = {
|
|
109
109
|
'/p': '/project',
|
|
@@ -127,6 +127,7 @@ export class CommandHandler {
|
|
|
127
127
|
permissionGateway;
|
|
128
128
|
interactionRouter;
|
|
129
129
|
statsCollector;
|
|
130
|
+
hotLoadChannel;
|
|
130
131
|
agentMap;
|
|
131
132
|
defaultAgentId;
|
|
132
133
|
/** 按 agentId 获取 agent,回退到默认 */
|
|
@@ -266,6 +267,9 @@ export class CommandHandler {
|
|
|
266
267
|
setMessageQueue(messageQueue) {
|
|
267
268
|
this.messageQueue = messageQueue;
|
|
268
269
|
}
|
|
270
|
+
setHotLoadChannel(fn) {
|
|
271
|
+
this.hotLoadChannel = fn;
|
|
272
|
+
}
|
|
269
273
|
setPermissionGateway(gateway) {
|
|
270
274
|
this.permissionGateway = gateway;
|
|
271
275
|
}
|
|
@@ -407,6 +411,10 @@ export class CommandHandler {
|
|
|
407
411
|
] : []),
|
|
408
412
|
...(isOwner ? [
|
|
409
413
|
{ cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
414
|
+
{ cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
|
|
415
|
+
{ value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
|
|
416
|
+
{ value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
|
|
417
|
+
] } },
|
|
410
418
|
{ cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
|
|
411
419
|
{ value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
|
|
412
420
|
{ value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
|
|
@@ -634,6 +642,7 @@ export class CommandHandler {
|
|
|
634
642
|
...(isOwner ? [
|
|
635
643
|
' /restart - 重启服务',
|
|
636
644
|
' /file [channel] <path> - 发送项目内文件',
|
|
645
|
+
' /aid [list|new <aid>] - AID 管理',
|
|
637
646
|
' /agentmd [put|set <内容>] - 管理 agent.md',
|
|
638
647
|
] : []),
|
|
639
648
|
'',
|
|
@@ -1139,6 +1148,94 @@ export class CommandHandler {
|
|
|
1139
1148
|
}
|
|
1140
1149
|
return `✓ 推理强度: ${newEffort}`;
|
|
1141
1150
|
}
|
|
1151
|
+
// /aid 命令:AID 管理(list / new)
|
|
1152
|
+
if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
|
|
1153
|
+
if (!isOwner)
|
|
1154
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1155
|
+
const adapter = this.adapters.get(channel);
|
|
1156
|
+
const channelType = this.channelTypeMap.get(channel);
|
|
1157
|
+
if (channelType !== 'aun')
|
|
1158
|
+
return '❌ 此命令仅在 AUN 通道中可用';
|
|
1159
|
+
const arg = normalizedContent.slice(4).trim();
|
|
1160
|
+
// /aid 或 /aid list — 列出所有 AUN 实例
|
|
1161
|
+
if (!arg || arg === 'list') {
|
|
1162
|
+
const { normalizeChannelInstances } = await import('../config.js');
|
|
1163
|
+
const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
|
|
1164
|
+
if (instances.length === 0)
|
|
1165
|
+
return '暂无 AUN 实例';
|
|
1166
|
+
const lines = ['AUN 实例:'];
|
|
1167
|
+
for (const inst of instances) {
|
|
1168
|
+
if (inst.enabled === false || !inst.aid)
|
|
1169
|
+
continue;
|
|
1170
|
+
const channelObj = this.channelObjects.get(inst.name);
|
|
1171
|
+
const status = channelObj?.getStatus?.();
|
|
1172
|
+
const connected = status?.connected ?? false;
|
|
1173
|
+
const icon = connected ? '✓' : '✗';
|
|
1174
|
+
const state = connected ? '已连接' : '未连接';
|
|
1175
|
+
lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
|
|
1176
|
+
}
|
|
1177
|
+
return lines.join('\n');
|
|
1178
|
+
}
|
|
1179
|
+
// /aid new <aid> — 创建新 AID 并热加载
|
|
1180
|
+
if (arg.startsWith('new ')) {
|
|
1181
|
+
const rawName = arg.slice(4).trim();
|
|
1182
|
+
if (!rawName)
|
|
1183
|
+
return '用法: /aid new <aid>\n例: /aid new reviewer';
|
|
1184
|
+
if (!this.hotLoadChannel)
|
|
1185
|
+
return '❌ 热加载未就绪';
|
|
1186
|
+
// Derive full AID: if no dots, append domain from current AID
|
|
1187
|
+
const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
|
|
1188
|
+
let fullAid = rawName;
|
|
1189
|
+
if (!rawName.includes('.')) {
|
|
1190
|
+
const domain = selfAid.split('.').slice(1).join('.');
|
|
1191
|
+
if (!domain)
|
|
1192
|
+
return '❌ 无法推导 AID 域(当前实例未连接)';
|
|
1193
|
+
fullAid = `${rawName}.${domain}`;
|
|
1194
|
+
}
|
|
1195
|
+
// Validate AID format
|
|
1196
|
+
const { isValidAid } = await import('../utils/init-channel.js');
|
|
1197
|
+
if (!isValidAid(fullAid))
|
|
1198
|
+
return `❌ 无效 AID 格式: ${fullAid}`;
|
|
1199
|
+
// Check instance name conflict
|
|
1200
|
+
const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
|
|
1201
|
+
const { normalizeChannelInstances } = await import('../config.js');
|
|
1202
|
+
const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
|
|
1203
|
+
if (existing.some(e => e.name === instName)) {
|
|
1204
|
+
return `❌ 实例名 "${instName}" 已存在`;
|
|
1205
|
+
}
|
|
1206
|
+
if (existing.some(e => e.aid === fullAid)) {
|
|
1207
|
+
return `❌ AID ${fullAid} 已在配置中`;
|
|
1208
|
+
}
|
|
1209
|
+
// Create AID (reuse init-channel.ts silent logic)
|
|
1210
|
+
try {
|
|
1211
|
+
const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
|
|
1212
|
+
const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
|
|
1213
|
+
// Resolve owner from current AUN instance config
|
|
1214
|
+
const owner = this.config.channels?.aun
|
|
1215
|
+
? (Array.isArray(this.config.channels.aun)
|
|
1216
|
+
? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
|
|
1217
|
+
: this.config.channels.aun.owner)
|
|
1218
|
+
: undefined;
|
|
1219
|
+
// Hot-load: build and register new channel instance BEFORE writing config
|
|
1220
|
+
const { AUNChannelPlugin } = await import('../channels/aun.js');
|
|
1221
|
+
const plugin = new AUNChannelPlugin();
|
|
1222
|
+
const tempConfig = JSON.parse(JSON.stringify(this.config));
|
|
1223
|
+
tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
|
|
1224
|
+
const newInstances = await plugin.createChannels(tempConfig);
|
|
1225
|
+
if (newInstances.length === 0)
|
|
1226
|
+
return '❌ 通道实例创建失败';
|
|
1227
|
+
await this.hotLoadChannel(newInstances[0]);
|
|
1228
|
+
// Write config only after successful hot-load
|
|
1229
|
+
appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
|
|
1230
|
+
const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
|
|
1231
|
+
return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
|
|
1232
|
+
}
|
|
1233
|
+
catch (e) {
|
|
1234
|
+
return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return '用法: /aid [list|new <aid>]';
|
|
1238
|
+
}
|
|
1142
1239
|
// /activity 命令:控制中间输出显示模式
|
|
1143
1240
|
if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
|
|
1144
1241
|
if (!isOwner)
|
|
@@ -2601,7 +2698,7 @@ export class CommandHandler {
|
|
|
2601
2698
|
static CTL_COMMANDS = [
|
|
2602
2699
|
'/help', '/status', '/check',
|
|
2603
2700
|
'/model', '/effort', '/perm',
|
|
2604
|
-
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd',
|
|
2701
|
+
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
|
|
2605
2702
|
];
|
|
2606
2703
|
/**
|
|
2607
2704
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import crypto from 'crypto';
|
|
3
4
|
import { hasCompact } from '../../agents/claude-runner.js';
|
|
4
5
|
import { StreamFlusher } from './stream-flusher.js';
|
|
6
|
+
import { ThoughtEmitter } from './thought-emitter.js';
|
|
5
7
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
6
8
|
import { logger } from '../../utils/logger.js';
|
|
7
9
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
@@ -27,7 +29,6 @@ export class MessageProcessor {
|
|
|
27
29
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
28
30
|
interactionRouter;
|
|
29
31
|
messageQueue;
|
|
30
|
-
skillsHintDesc = undefined; // undefined=未加载, null=无模板, string=缓存描述
|
|
31
32
|
skillsEnsured = false; // 全局 SKILLS.md 是否已确保
|
|
32
33
|
/** 按 agentId 获取 agent,回退到默认 */
|
|
33
34
|
getAgent(agentId) {
|
|
@@ -279,6 +280,8 @@ export class MessageProcessor {
|
|
|
279
280
|
const { adapter, options } = channelInfo;
|
|
280
281
|
const agent = this.getAgent(session.agentId);
|
|
281
282
|
const streamKey = session.id;
|
|
283
|
+
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
284
|
+
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
282
285
|
try {
|
|
283
286
|
const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
284
287
|
// 记录收到消息
|
|
@@ -302,7 +305,7 @@ export class MessageProcessor {
|
|
|
302
305
|
logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
303
306
|
// 记录开始处理
|
|
304
307
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
305
|
-
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
|
|
308
|
+
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
|
|
306
309
|
logger.message({
|
|
307
310
|
msgId: messageId,
|
|
308
311
|
sessionId: session.id,
|
|
@@ -338,6 +341,12 @@ export class MessageProcessor {
|
|
|
338
341
|
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
|
|
339
342
|
// 保存当前 flusher,用于 compact 事件
|
|
340
343
|
this.currentFlusher = flusher;
|
|
344
|
+
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
345
|
+
// selector: context = { type: 'task', id: taskId }
|
|
346
|
+
let thoughtEmitter = null;
|
|
347
|
+
if (isProactive && adapter.putThought) {
|
|
348
|
+
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
|
|
349
|
+
}
|
|
341
350
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
342
351
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
343
352
|
const capturedChannelId = message.channelId;
|
|
@@ -439,14 +448,16 @@ export class MessageProcessor {
|
|
|
439
448
|
contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
|
|
440
449
|
}
|
|
441
450
|
// 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
451
|
+
// 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
|
|
452
|
+
// 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
|
|
453
|
+
// if (!this.skillsEnsured) {
|
|
454
|
+
// this.ensureSkillsFile();
|
|
455
|
+
// this.skillsEnsured = true;
|
|
456
|
+
// }
|
|
457
|
+
// const skillsHint = this.getSkillsHint();
|
|
458
|
+
// if (skillsHint) {
|
|
459
|
+
// contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
|
|
460
|
+
// }
|
|
450
461
|
// 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
|
|
451
462
|
if (isProactive) {
|
|
452
463
|
contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
|
|
@@ -463,7 +474,7 @@ export class MessageProcessor {
|
|
|
463
474
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
464
475
|
agent.registerStream(streamKey, stream);
|
|
465
476
|
streamRegistered = true;
|
|
466
|
-
streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress);
|
|
477
|
+
streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
|
|
467
478
|
break; // 成功,跳出重试循环
|
|
468
479
|
}
|
|
469
480
|
catch (retryError) {
|
|
@@ -493,7 +504,7 @@ export class MessageProcessor {
|
|
|
493
504
|
flusher.addActivity('\u2705 压缩完成,正在重试...');
|
|
494
505
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
|
|
495
506
|
agent.registerStream(streamKey, retryStream);
|
|
496
|
-
streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress);
|
|
507
|
+
streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
|
|
497
508
|
}
|
|
498
509
|
else {
|
|
499
510
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -607,7 +618,7 @@ export class MessageProcessor {
|
|
|
607
618
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
608
619
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
609
620
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
610
|
-
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(message));
|
|
621
|
+
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, this.getReplyContext(message));
|
|
611
622
|
this.eventBus.publish({
|
|
612
623
|
type: 'message:error',
|
|
613
624
|
sessionId: session.id,
|
|
@@ -635,7 +646,7 @@ export class MessageProcessor {
|
|
|
635
646
|
}
|
|
636
647
|
else {
|
|
637
648
|
// 真正的成功
|
|
638
|
-
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(message));
|
|
649
|
+
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, this.getReplyContext(message));
|
|
639
650
|
await this.sessionManager.recordSuccess(session.id);
|
|
640
651
|
this.eventBus.publish({
|
|
641
652
|
type: 'message:completed',
|
|
@@ -688,7 +699,7 @@ export class MessageProcessor {
|
|
|
688
699
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
689
700
|
if (!isUserInterrupt) {
|
|
690
701
|
try {
|
|
691
|
-
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(message));
|
|
702
|
+
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, this.getReplyContext(message));
|
|
692
703
|
}
|
|
693
704
|
catch { }
|
|
694
705
|
}
|
|
@@ -764,7 +775,7 @@ export class MessageProcessor {
|
|
|
764
775
|
* 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
|
|
765
776
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
766
777
|
*/
|
|
767
|
-
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
|
|
778
|
+
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
|
|
768
779
|
let hasReceivedText = false;
|
|
769
780
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
770
781
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -777,6 +788,10 @@ export class MessageProcessor {
|
|
|
777
788
|
resetTimer(event.type, toolName);
|
|
778
789
|
// 记录所有事件类型
|
|
779
790
|
logger.info(`[MessageProcessor] Event: type=${event.type}`);
|
|
791
|
+
// Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
|
|
792
|
+
if (thoughtEmitter) {
|
|
793
|
+
thoughtEmitter.emit(event).catch(() => { });
|
|
794
|
+
}
|
|
780
795
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
781
796
|
if (event.type === 'session_id') {
|
|
782
797
|
logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -1077,42 +1092,28 @@ export class MessageProcessor {
|
|
|
1077
1092
|
return 0;
|
|
1078
1093
|
}
|
|
1079
1094
|
/**
|
|
1080
|
-
*
|
|
1095
|
+
* 从 data/SKILLS.md 读取 frontmatter 并生成提示。
|
|
1096
|
+
* 不缓存:每次读取保证用户编辑立即生效。
|
|
1097
|
+
* 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
|
|
1081
1098
|
*/
|
|
1082
1099
|
getSkillsHint() {
|
|
1083
|
-
if (this.skillsHintDesc === undefined) {
|
|
1084
|
-
this.skillsHintDesc = this.loadSkillsHint();
|
|
1085
|
-
}
|
|
1086
|
-
return this.skillsHintDesc;
|
|
1087
|
-
}
|
|
1088
|
-
/**
|
|
1089
|
-
* 从包模板源读取 frontmatter 并生成提示(仅执行一次)
|
|
1090
|
-
*/
|
|
1091
|
-
loadSkillsHint() {
|
|
1092
1100
|
try {
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1101
|
+
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1102
|
+
if (!fs.existsSync(skillsPath))
|
|
1103
|
+
return null;
|
|
1104
|
+
const content = fs.readFileSync(skillsPath, 'utf-8');
|
|
1105
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1106
|
+
if (!frontmatterMatch)
|
|
1107
|
+
return null;
|
|
1108
|
+
const fm = frontmatterMatch[1];
|
|
1109
|
+
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1110
|
+
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1111
|
+
const parts = [
|
|
1112
|
+
`可通过 Bash 指令管理运行时,${desc}。`,
|
|
1113
|
+
trigger ? `触发时机:${trigger}。` : '',
|
|
1114
|
+
`完整文档见 ${skillsPath}`,
|
|
1096
1115
|
];
|
|
1097
|
-
|
|
1098
|
-
if (!fs.existsSync(templatePath))
|
|
1099
|
-
continue;
|
|
1100
|
-
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
1101
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1102
|
-
if (!frontmatterMatch)
|
|
1103
|
-
continue;
|
|
1104
|
-
const fm = frontmatterMatch[1];
|
|
1105
|
-
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1106
|
-
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1107
|
-
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1108
|
-
const parts = [
|
|
1109
|
-
`可通过 Bash 执行 \`evolclaw ctl <cmd>\` 管理运行时:${desc}`,
|
|
1110
|
-
trigger ? `触发时机:${trigger}` : '',
|
|
1111
|
-
`完整文档见 ${skillsPath}`,
|
|
1112
|
-
];
|
|
1113
|
-
return parts.filter(Boolean).join('\n');
|
|
1114
|
-
}
|
|
1115
|
-
return null;
|
|
1116
|
+
return parts.filter(Boolean).join('');
|
|
1116
1117
|
}
|
|
1117
1118
|
catch {
|
|
1118
1119
|
return null;
|
|
@@ -169,7 +169,7 @@ export class MessageQueue {
|
|
|
169
169
|
* 合并多条同 peerId 消息:
|
|
170
170
|
* - content: \n 连接
|
|
171
171
|
* - images / mentions: 扁平合并
|
|
172
|
-
* - messageId:
|
|
172
|
+
* - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
173
173
|
* - replyContext / peerName / 其余字段: 取最后一条
|
|
174
174
|
*/
|
|
175
175
|
mergeItems(items) {
|
|
@@ -185,12 +185,20 @@ export class MessageQueue {
|
|
|
185
185
|
allMentions.push(...m.mentions);
|
|
186
186
|
}
|
|
187
187
|
const last = items[items.length - 1];
|
|
188
|
+
// 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
|
|
189
|
+
let latestMessageId;
|
|
190
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
191
|
+
if (items[i].message.messageId) {
|
|
192
|
+
latestMessageId = items[i].message.messageId;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
188
196
|
const merged = {
|
|
189
197
|
...last.message,
|
|
190
198
|
content: contents.join('\n'),
|
|
191
199
|
images: allImages.length > 0 ? allImages : undefined,
|
|
192
200
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
193
|
-
messageId:
|
|
201
|
+
messageId: latestMessageId,
|
|
194
202
|
};
|
|
195
203
|
return {
|
|
196
204
|
message: merged,
|
|
@@ -95,13 +95,21 @@ export class StreamDebouncer {
|
|
|
95
95
|
allMentions.push(...e.mentions);
|
|
96
96
|
}
|
|
97
97
|
const last = entries[entries.length - 1];
|
|
98
|
+
// 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
99
|
+
let latestMessageId;
|
|
100
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
101
|
+
if (entries[i].messageId) {
|
|
102
|
+
latestMessageId = entries[i].messageId;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
98
106
|
const merged = {
|
|
99
107
|
...last.rest,
|
|
100
108
|
content: contents.join('\n'),
|
|
101
109
|
images: allImages.length > 0 ? allImages : undefined,
|
|
102
110
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
103
111
|
replyContext: last.replyContext,
|
|
104
|
-
messageId:
|
|
112
|
+
messageId: latestMessageId,
|
|
105
113
|
};
|
|
106
114
|
const resolves = entries.map(e => e.resolve);
|
|
107
115
|
const rejects = entries.map(e => e.reject);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { logger } from '../../utils/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* ThoughtEmitter — 将 Proactive 模式下的流式 AgentEvent 实时发送为 thought
|
|
4
|
+
*
|
|
5
|
+
* 设计特点:
|
|
6
|
+
* - 不做聚合/batching,逐事件调用 adapter.putThought()
|
|
7
|
+
* - 不感知 group vs P2P,通道差异由 adapter 内部处理
|
|
8
|
+
* - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
|
|
9
|
+
* - fire-and-forget:调用方不 await emit(),错误被内部捕获
|
|
10
|
+
*/
|
|
11
|
+
export class ThoughtEmitter {
|
|
12
|
+
adapter;
|
|
13
|
+
channelId;
|
|
14
|
+
taskId;
|
|
15
|
+
hasEmittedText = false;
|
|
16
|
+
constructor(adapter, channelId, taskId) {
|
|
17
|
+
if (!taskId) {
|
|
18
|
+
throw new Error('[ThoughtEmitter] taskId is required at construction');
|
|
19
|
+
}
|
|
20
|
+
this.adapter = adapter;
|
|
21
|
+
this.channelId = channelId;
|
|
22
|
+
this.taskId = taskId;
|
|
23
|
+
}
|
|
24
|
+
async emit(event) {
|
|
25
|
+
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
26
|
+
if (event.type === 'complete' &&
|
|
27
|
+
!event.isError &&
|
|
28
|
+
event.result &&
|
|
29
|
+
this.hasEmittedText) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const payload = this.mapEventToPayload(event);
|
|
33
|
+
if (!payload)
|
|
34
|
+
return;
|
|
35
|
+
if (!this.adapter.putThought)
|
|
36
|
+
return;
|
|
37
|
+
if (payload.stage === 'thinking') {
|
|
38
|
+
this.hasEmittedText = true;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await this.adapter.putThought(this.channelId, this.taskId, payload);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
mapEventToPayload(event) {
|
|
48
|
+
switch (event.type) {
|
|
49
|
+
case 'text':
|
|
50
|
+
if (!event.text)
|
|
51
|
+
return null;
|
|
52
|
+
return { type: 'thought', text: event.text, stage: 'thinking' };
|
|
53
|
+
case 'tool_use': {
|
|
54
|
+
const desc = this.summarizeInput(event.input);
|
|
55
|
+
return {
|
|
56
|
+
type: 'thought',
|
|
57
|
+
text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
|
|
58
|
+
stage: 'tool',
|
|
59
|
+
metadata: { tool: event.name, input: desc },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case 'tool_result':
|
|
63
|
+
if (event.isError) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'thought',
|
|
66
|
+
text: `⚠️ ${event.name}: ${event.error || '执行失败'}`,
|
|
67
|
+
stage: 'tool',
|
|
68
|
+
metadata: { tool: event.name, ok: false },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
const resultText = this.truncate(this.stringifyResult(event.result), 200);
|
|
73
|
+
return {
|
|
74
|
+
type: 'thought',
|
|
75
|
+
text: resultText ? `✅ ${event.name}: ${resultText}` : `✅ ${event.name}`,
|
|
76
|
+
stage: 'tool',
|
|
77
|
+
metadata: { tool: event.name, ok: true },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case 'compact':
|
|
81
|
+
return {
|
|
82
|
+
type: 'thought',
|
|
83
|
+
text: `💡 会话压缩完成 (压缩前 tokens: ${event.preTokens})`,
|
|
84
|
+
stage: 'system',
|
|
85
|
+
};
|
|
86
|
+
case 'task_progress': {
|
|
87
|
+
const stats = this.formatTaskStats(event);
|
|
88
|
+
const text = event.summary
|
|
89
|
+
? `⏳ 子任务: ${event.summary}${stats ? ` (${stats})` : ''}`
|
|
90
|
+
: `⏳ 子任务进行中${stats ? `: ${stats}` : ''}`;
|
|
91
|
+
return { type: 'thought', text, stage: 'planning' };
|
|
92
|
+
}
|
|
93
|
+
case 'error':
|
|
94
|
+
return { type: 'thought', text: `❌ ${event.error}`, stage: 'error' };
|
|
95
|
+
case 'complete':
|
|
96
|
+
if (event.isError) {
|
|
97
|
+
const errText = event.errors?.join('; ') || event.result || '任务失败';
|
|
98
|
+
return { type: 'thought', text: `❌ ${errText}`, stage: 'error' };
|
|
99
|
+
}
|
|
100
|
+
if (event.result) {
|
|
101
|
+
return { type: 'thought', text: event.result, stage: 'summary' };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
case 'session_id':
|
|
105
|
+
case 'state_changed':
|
|
106
|
+
case 'status':
|
|
107
|
+
return null;
|
|
108
|
+
default:
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
summarizeInput(input) {
|
|
113
|
+
if (!input || typeof input !== 'object')
|
|
114
|
+
return '';
|
|
115
|
+
return (input.description ||
|
|
116
|
+
input.file_path ||
|
|
117
|
+
input.pattern ||
|
|
118
|
+
(typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
|
|
119
|
+
(typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
|
|
120
|
+
(typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
|
|
121
|
+
'');
|
|
122
|
+
}
|
|
123
|
+
stringifyResult(result) {
|
|
124
|
+
if (result === null || result === undefined)
|
|
125
|
+
return '';
|
|
126
|
+
if (typeof result === 'string')
|
|
127
|
+
return result;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(result);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return String(result);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
truncate(text, maxLen) {
|
|
136
|
+
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
|
|
137
|
+
}
|
|
138
|
+
formatTaskStats(event) {
|
|
139
|
+
const parts = [];
|
|
140
|
+
if (event.toolUses)
|
|
141
|
+
parts.push(`${event.toolUses} tools`);
|
|
142
|
+
if (event.durationMs)
|
|
143
|
+
parts.push(`${Math.round(event.durationMs / 1000)}s`);
|
|
144
|
+
return parts.join(', ');
|
|
145
|
+
}
|
|
146
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -204,9 +204,11 @@ async function main() {
|
|
|
204
204
|
showIdleMonitor: () => true,
|
|
205
205
|
accumulateErrors: () => true,
|
|
206
206
|
};
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
208
|
+
const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
|
|
209
|
+
// ── Channel instance registration (shared by startup and hot-load) ──
|
|
210
|
+
function registerChannelInstance(inst) {
|
|
211
|
+
// 1. 项目路径提供器
|
|
210
212
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
211
213
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
212
214
|
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
|
|
@@ -215,7 +217,7 @@ async function main() {
|
|
|
215
217
|
: path.resolve(process.cwd(), session.projectPath);
|
|
216
218
|
});
|
|
217
219
|
}
|
|
218
|
-
// 注册 adapter、policy 和 options(注入 channelType)
|
|
220
|
+
// 2. 注册 adapter、policy 和 options(注入 channelType)
|
|
219
221
|
const opts = inst.channelType
|
|
220
222
|
? { ...inst.options, channelType: inst.channelType }
|
|
221
223
|
: inst.options;
|
|
@@ -225,18 +227,13 @@ async function main() {
|
|
|
225
227
|
if (inst.policy) {
|
|
226
228
|
cmdHandler.registerPolicy(inst.adapter.channelName, inst.policy);
|
|
227
229
|
}
|
|
228
|
-
//
|
|
230
|
+
// 3. 交互回调
|
|
229
231
|
if (inst.adapter.onInteraction) {
|
|
230
232
|
inst.adapter.onInteraction((response) => {
|
|
231
233
|
interactionRouter.handle(response);
|
|
232
234
|
});
|
|
233
235
|
}
|
|
234
|
-
|
|
235
|
-
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
236
|
-
const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
|
|
237
|
-
// ── 渠道消息注册 ──
|
|
238
|
-
// 连接插件系统的渠道
|
|
239
|
-
for (const inst of channelInstances) {
|
|
236
|
+
// 4. MessageBridge 注册(按 channelType 分发)
|
|
240
237
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
241
238
|
if (channelType === 'feishu') {
|
|
242
239
|
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType }) => {
|
|
@@ -251,7 +248,6 @@ async function main() {
|
|
|
251
248
|
}), inst.adapter, channelType);
|
|
252
249
|
}
|
|
253
250
|
if (channelType === 'wechat') {
|
|
254
|
-
// 注入 EventBus(用于 channel:health 事件)
|
|
255
251
|
if (inst.channel.setEventBus) {
|
|
256
252
|
inst.channel.setEventBus(eventBus);
|
|
257
253
|
}
|
|
@@ -281,6 +277,19 @@ async function main() {
|
|
|
281
277
|
replyContext: opts.replyContext,
|
|
282
278
|
});
|
|
283
279
|
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
|
|
280
|
+
// AUN 重连失败通知
|
|
281
|
+
if (inst.channel.setOnChannelDown) {
|
|
282
|
+
inst.channel.setOnChannelDown(() => {
|
|
283
|
+
eventBus.publish({
|
|
284
|
+
type: 'channel:health',
|
|
285
|
+
channel: channelType,
|
|
286
|
+
channelName: inst.adapter.channelName,
|
|
287
|
+
status: 'auth_error',
|
|
288
|
+
message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
284
293
|
}
|
|
285
294
|
if (channelType === 'dingtalk') {
|
|
286
295
|
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
@@ -310,11 +319,40 @@ async function main() {
|
|
|
310
319
|
});
|
|
311
320
|
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
312
321
|
}
|
|
313
|
-
|
|
322
|
+
if (channelType === 'wecom') {
|
|
323
|
+
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
324
|
+
handler({
|
|
325
|
+
channel: channelType,
|
|
326
|
+
channelId: event.channelId,
|
|
327
|
+
content: event.content,
|
|
328
|
+
chatType: event.chatType || 'private',
|
|
329
|
+
peerId: event.peerId || '',
|
|
330
|
+
peerName: event.peerName,
|
|
331
|
+
messageId: event.messageId,
|
|
332
|
+
});
|
|
333
|
+
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
334
|
+
}
|
|
335
|
+
// 5. 撤回消息 → 中断执行中任务
|
|
314
336
|
inst.channel.onRecall?.((messageId) => {
|
|
315
337
|
msgBridge.cancel(messageId);
|
|
316
338
|
});
|
|
317
339
|
}
|
|
340
|
+
// ── 注册所有渠道实例 ──
|
|
341
|
+
for (const inst of channelInstances) {
|
|
342
|
+
registerChannelInstance(inst);
|
|
343
|
+
}
|
|
344
|
+
// ── 设置热加载回调 ──
|
|
345
|
+
cmdHandler.setHotLoadChannel(async (inst) => {
|
|
346
|
+
registerChannelInstance(inst);
|
|
347
|
+
channelInstances.push(inst);
|
|
348
|
+
await inst.connect();
|
|
349
|
+
eventBus.publish({
|
|
350
|
+
type: 'channel:connected',
|
|
351
|
+
channel: (inst.channelType || inst.adapter.channelName).toLowerCase(),
|
|
352
|
+
channelName: inst.adapter.channelName,
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
});
|
|
355
|
+
});
|
|
318
356
|
// ── 连接所有渠道 ──
|
|
319
357
|
const connected = await channelLoader.connectAll(channelInstances);
|
|
320
358
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
@@ -326,7 +364,6 @@ async function main() {
|
|
|
326
364
|
}
|
|
327
365
|
}
|
|
328
366
|
for (const name of connected) {
|
|
329
|
-
// 查找对应实例以获取 channelType
|
|
330
367
|
const inst = channelInstances.find(i => i.adapter.channelName === name);
|
|
331
368
|
const type = inst?.channelType || name;
|
|
332
369
|
eventBus.publish({
|
|
@@ -336,22 +373,6 @@ async function main() {
|
|
|
336
373
|
timestamp: Date.now()
|
|
337
374
|
});
|
|
338
375
|
}
|
|
339
|
-
// AUN 重连失败通知:通过 channel:health 事件
|
|
340
|
-
for (const inst of channelInstances) {
|
|
341
|
-
const channelType = inst.channelType || inst.adapter.channelName;
|
|
342
|
-
if (channelType === 'aun' && inst.channel.setOnChannelDown) {
|
|
343
|
-
inst.channel.setOnChannelDown(() => {
|
|
344
|
-
eventBus.publish({
|
|
345
|
-
type: 'channel:health',
|
|
346
|
-
channel: channelType,
|
|
347
|
-
channelName: inst.adapter.channelName,
|
|
348
|
-
status: 'auth_error',
|
|
349
|
-
message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
350
|
-
timestamp: Date.now(),
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
376
|
// 统一 channel:health 跨通道通知(仅 auth_error)
|
|
356
377
|
// 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
|
|
357
378
|
eventBus.subscribe('channel:health', (event) => {
|
package/dist/templates/skills.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: evolclaw-ctl
|
|
3
|
-
version: 1.
|
|
4
|
-
description:
|
|
5
|
-
trigger:
|
|
3
|
+
version: 1.1.0
|
|
4
|
+
description: 仅在 evolclaw 运行时可用
|
|
5
|
+
trigger: 用户询问或需要切换模型、调整推理强度、查看运行状态、压缩上下文、检查通道健康、管理权限模式、重启服务、重连渠道等
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# EvolClaw Ctl
|
|
@@ -35,6 +35,8 @@ trigger: 用户询问或需要切换模型、调整推理强度、查看运行
|
|
|
35
35
|
- `evolclaw ctl agentmd` — 查看当前 agent.md
|
|
36
36
|
- `evolclaw ctl agentmd put` — 发布本地 agent.md
|
|
37
37
|
- `evolclaw ctl agentmd set <内容>` — 直接设置 agent.md 内容
|
|
38
|
+
- `evolclaw ctl aid` — 列出所有 AUN 实例及连接状态
|
|
39
|
+
- `evolclaw ctl aid new <aid>` — 创建新 AID 并热加载(仅 AUN 通道)
|
|
38
40
|
|
|
39
41
|
## 使用示例
|
|
40
42
|
|
|
@@ -580,9 +580,9 @@ export async function cmdInitWechat() {
|
|
|
580
580
|
process.exit(1);
|
|
581
581
|
}
|
|
582
582
|
// ==================== AUN ====================
|
|
583
|
-
// 最低 @agentunion/
|
|
584
|
-
const MIN_AUN_CORE_SDK = [0, 2,
|
|
585
|
-
const AUN_CORE_SDK_PKG = '@agentunion/
|
|
583
|
+
// 最低 @agentunion/fastaun 版本要求
|
|
584
|
+
const MIN_AUN_CORE_SDK = [0, 2, 14];
|
|
585
|
+
const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
|
|
586
586
|
function compareVersion(a, min) {
|
|
587
587
|
const parts = a.split('.').map(n => parseInt(n, 10));
|
|
588
588
|
if (parts.length < 3 || parts.some(isNaN))
|
|
@@ -711,10 +711,83 @@ export async function checkAunEnvironment(rl) {
|
|
|
711
711
|
console.log('');
|
|
712
712
|
return true;
|
|
713
713
|
}
|
|
714
|
-
function isValidAid(name) {
|
|
714
|
+
export function isValidAid(name) {
|
|
715
715
|
const labels = name.split('.');
|
|
716
716
|
return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
|
|
717
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* Non-interactive AID creation + agent.md publish.
|
|
720
|
+
* Reuses the same logic as `evolclaw init --non-interactive --channel aun`.
|
|
721
|
+
*
|
|
722
|
+
* Returns the created AID string, or throws on failure.
|
|
723
|
+
*/
|
|
724
|
+
export async function createAidSilent(opts) {
|
|
725
|
+
const aunPath = path.join(os.homedir(), '.aun');
|
|
726
|
+
const aidDir = path.join(aunPath, 'AIDs', opts.aid);
|
|
727
|
+
// Skip creation if AID already exists locally
|
|
728
|
+
if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
|
|
729
|
+
return { aid: opts.aid, alreadyExisted: true };
|
|
730
|
+
}
|
|
731
|
+
const { AUNClient } = await import('@agentunion/fastaun');
|
|
732
|
+
let client = new AUNClient({ aun_path: aunPath });
|
|
733
|
+
const result = await client.auth.createAid({ aid: opts.aid });
|
|
734
|
+
// Download CA root cert (if not already present)
|
|
735
|
+
const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '');
|
|
736
|
+
// Rebuild client with CA cert + AID identity for uploadAgentMd
|
|
737
|
+
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
738
|
+
if (caDownloaded && fs.existsSync(caCertPath)) {
|
|
739
|
+
try {
|
|
740
|
+
await client.close();
|
|
741
|
+
}
|
|
742
|
+
catch { /* ignore */ }
|
|
743
|
+
client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
|
|
744
|
+
}
|
|
745
|
+
// Write initial agent.md (initialized: false, name = aid first label)
|
|
746
|
+
const agentName = opts.aid.split('.')[0];
|
|
747
|
+
const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
|
|
748
|
+
const agentMdPath = path.join(aidDir, 'agent.md');
|
|
749
|
+
try {
|
|
750
|
+
await client.auth.uploadAgentMd(agentMdContent);
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
// Non-fatal: first connection will auto-retry
|
|
754
|
+
}
|
|
755
|
+
fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
|
|
756
|
+
try {
|
|
757
|
+
await client.close();
|
|
758
|
+
}
|
|
759
|
+
catch { /* ignore */ }
|
|
760
|
+
if (!fs.existsSync(agentMdPath)) {
|
|
761
|
+
throw new Error(`agent.md write verification failed: ${agentMdPath}`);
|
|
762
|
+
}
|
|
763
|
+
return { aid: opts.aid, alreadyExisted: false };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Append a new AUN instance to the config's channels.aun array and save.
|
|
767
|
+
* Handles upgrade from single-object to array format.
|
|
768
|
+
*/
|
|
769
|
+
export function appendAunInstance(config, inst) {
|
|
770
|
+
if (!config.channels)
|
|
771
|
+
config.channels = {};
|
|
772
|
+
const newInst = {
|
|
773
|
+
name: inst.name,
|
|
774
|
+
enabled: inst.enabled ?? true,
|
|
775
|
+
aid: inst.aid,
|
|
776
|
+
...(inst.owner && { owner: inst.owner }),
|
|
777
|
+
};
|
|
778
|
+
if (Array.isArray(config.channels.aun)) {
|
|
779
|
+
config.channels.aun.push(newInst);
|
|
780
|
+
}
|
|
781
|
+
else if (config.channels.aun) {
|
|
782
|
+
const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
|
|
783
|
+
config.channels.aun = [oldInst, newInst];
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
config.channels.aun = [newInst];
|
|
787
|
+
}
|
|
788
|
+
const p = resolvePaths();
|
|
789
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
790
|
+
}
|
|
718
791
|
export async function setupAunAid(rl, _config) {
|
|
719
792
|
let aid = '';
|
|
720
793
|
let gatewayPort; // only used locally for AID creation, not written to config
|
|
@@ -755,7 +828,7 @@ export async function setupAunAid(rl, _config) {
|
|
|
755
828
|
console.log(' 正在创建 AID...');
|
|
756
829
|
let failed = false;
|
|
757
830
|
try {
|
|
758
|
-
const { AUNClient } = await import('@agentunion/
|
|
831
|
+
const { AUNClient } = await import('@agentunion/fastaun');
|
|
759
832
|
let client = new AUNClient({ aun_path: aunPath });
|
|
760
833
|
// 如果用户指定了自定义端口,手动设置 gateway URL;否则让 SDK 自动发现
|
|
761
834
|
if (gatewayPort) {
|
package/dist/utils/init.js
CHANGED
|
@@ -408,14 +408,14 @@ export async function cmdInit(options) {
|
|
|
408
408
|
// 自动安装 AUN SDK
|
|
409
409
|
const { resolveAunCoreSdkPkg, npmInstallGlobal, downloadCaRoot } = await import('./init-channel.js');
|
|
410
410
|
if (!resolveAunCoreSdkPkg()) {
|
|
411
|
-
console.log('正在安装 @agentunion/
|
|
412
|
-
await npmInstallGlobal('@agentunion/
|
|
411
|
+
console.log('正在安装 @agentunion/fastaun...');
|
|
412
|
+
await npmInstallGlobal('@agentunion/fastaun@latest');
|
|
413
413
|
}
|
|
414
414
|
// 创建 AID(如果本地不存在)
|
|
415
415
|
const aunPath = path.join(os.homedir(), '.aun');
|
|
416
416
|
const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
|
|
417
417
|
if (!fs.existsSync(path.join(aidDir, 'private'))) {
|
|
418
|
-
const { AUNClient } = await import('@agentunion/
|
|
418
|
+
const { AUNClient } = await import('@agentunion/fastaun');
|
|
419
419
|
let client = new AUNClient({ aun_path: aunPath });
|
|
420
420
|
// 让 SDK 通过 well-known 自动发现网关
|
|
421
421
|
const result = await client.auth.createAid({ aid: options.aunAid });
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
|
+
import { getPackageRoot } from '../paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* 比较两个 semver 版本号 (a.b.c 格式)
|
|
7
|
+
* 返回 -1 (a < b), 0 (a == b), 1 (a > b)
|
|
8
|
+
* 自动剥离 pre-release 标签 (e.g. 2.6.0-beta.1 → 2.6.0)
|
|
9
|
+
*/
|
|
10
|
+
export function compareVersions(a, b) {
|
|
11
|
+
const pa = a.split('-')[0].split('.').map(Number);
|
|
12
|
+
const pb = b.split('-')[0].split('.').map(Number);
|
|
13
|
+
const len = Math.max(pa.length, pb.length);
|
|
14
|
+
for (let i = 0; i < len; i++) {
|
|
15
|
+
const na = pa[i] ?? 0;
|
|
16
|
+
const nb = pb[i] ?? 0;
|
|
17
|
+
if (na < nb)
|
|
18
|
+
return -1;
|
|
19
|
+
if (na > nb)
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查当前安装是否为 npm link 开发模式。
|
|
26
|
+
* 正式全局安装的路径结构为 .../node_modules/evolclaw,
|
|
27
|
+
* 而 npm link 指向项目源码目录,其父目录不是 node_modules。
|
|
28
|
+
*/
|
|
29
|
+
export function isLinkedInstall() {
|
|
30
|
+
const pkgRoot = getPackageRoot();
|
|
31
|
+
return path.basename(path.dirname(pkgRoot)) !== 'node_modules';
|
|
32
|
+
}
|
|
33
|
+
/** 获取本地 package.json 中的版本号 */
|
|
34
|
+
export function getLocalVersion() {
|
|
35
|
+
const pkgPath = path.join(getPackageRoot(), 'package.json');
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
37
|
+
return pkg.version;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 查询 npm registry 上 evolclaw 的最新版本。
|
|
41
|
+
* 超时 15 秒,失败返回 null。
|
|
42
|
+
*/
|
|
43
|
+
export function checkLatestVersion() {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
execFile('npm', ['view', 'evolclaw', 'version'], { timeout: 15000 }, (err, stdout) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
resolve(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const ver = stdout.trim();
|
|
51
|
+
resolve(ver || null);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 执行 npm install -g evolclaw@latest
|
|
57
|
+
*/
|
|
58
|
+
function runInstall() {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile('npm', ['install', '-g', 'evolclaw@latest'], { timeout: 120000 }, (err, _stdout, stderr) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
resolve({ ok: false, error: stderr || err.message });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
resolve({ ok: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 完整升级流程:检查 → 比较 → 安装(失败重试一次)
|
|
72
|
+
*/
|
|
73
|
+
export async function tryUpgrade() {
|
|
74
|
+
// 开发模式跳过
|
|
75
|
+
if (isLinkedInstall()) {
|
|
76
|
+
return { status: 'skipped' };
|
|
77
|
+
}
|
|
78
|
+
const localVer = getLocalVersion();
|
|
79
|
+
// 查询 registry
|
|
80
|
+
const remoteVer = await checkLatestVersion();
|
|
81
|
+
if (!remoteVer) {
|
|
82
|
+
return { status: 'skipped', error: 'Failed to check remote version' };
|
|
83
|
+
}
|
|
84
|
+
// 版本比较
|
|
85
|
+
if (compareVersions(localVer, remoteVer) >= 0) {
|
|
86
|
+
return { status: 'no-update', from: localVer };
|
|
87
|
+
}
|
|
88
|
+
// 有新版本,执行升级(失败重试一次)
|
|
89
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
90
|
+
const result = await runInstall();
|
|
91
|
+
if (result.ok) {
|
|
92
|
+
return { status: 'upgraded', from: localVer, to: remoteVer };
|
|
93
|
+
}
|
|
94
|
+
if (attempt === 1) {
|
|
95
|
+
return { status: 'failed', from: localVer, to: remoteVer, error: result.error };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// unreachable
|
|
99
|
+
return { status: 'failed', from: localVer, to: remoteVer };
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolclaw",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"prepublishOnly": "npm run build && npm test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@agentunion/fastaun": "^0.2.15",
|
|
26
27
|
"@anthropic-ai/claude-agent-sdk": "^0.2.100",
|
|
27
|
-
"@agentunion/aun-node": "^0.2.12",
|
|
28
28
|
"image-type": "^6.0.0",
|
|
29
29
|
"qrcode-terminal": "^0.12.0"
|
|
30
30
|
},
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"pure-qqbot": "^2.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^25.5.0",
|
|
41
40
|
"@types/form-data": "^2.2.1",
|
|
41
|
+
"@types/node": "^25.5.0",
|
|
42
42
|
"@types/qrcode-terminal": "^0.12.2",
|
|
43
43
|
"@vitest/coverage-v8": "^4.1.0",
|
|
44
44
|
"tsx": "^4.19.0",
|