evolclaw 2.8.2 → 3.0.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/README.md +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
|
|
2
2
|
import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
|
|
3
3
|
import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
|
|
4
|
-
import {
|
|
4
|
+
import { ensureDataDirs, resolvePaths, syncKitsFromPackage } from './paths.js';
|
|
5
|
+
import { resolveAnthropicConfig } from './agents/resolve.js';
|
|
6
|
+
import { loadDefaults, autoMigrateIfNeeded } from './config-store.js';
|
|
7
|
+
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
5
8
|
import { SessionManager } from './core/session/session-manager.js';
|
|
6
9
|
import { ClaudeAgentPlugin } from './agents/claude-runner.js';
|
|
7
10
|
import { CodexAgentPlugin } from './agents/codex-runner.js';
|
|
@@ -12,25 +15,45 @@ import { AUNChannelPlugin } from './channels/aun.js';
|
|
|
12
15
|
import { DingtalkChannelPlugin } from './channels/dingtalk.js';
|
|
13
16
|
import { QQBotChannelPlugin } from './channels/qqbot.js';
|
|
14
17
|
import { WecomChannelPlugin } from './channels/wecom.js';
|
|
15
|
-
import { MessageProcessor } from './core/message/message-processor.js';
|
|
18
|
+
import { MessageProcessor, buildEnvelope } from './core/message/message-processor.js';
|
|
16
19
|
import { MessageQueue } from './core/message/message-queue.js';
|
|
17
20
|
import { MessageBridge } from './core/message/message-bridge.js';
|
|
18
21
|
import { MessageCache } from './core/message/message-cache.js';
|
|
19
22
|
import { CommandHandler } from './core/command-handler.js';
|
|
20
23
|
import { EventBus } from './core/event-bus.js';
|
|
21
|
-
import { StatsCollector } from './utils/stats
|
|
24
|
+
import { StatsCollector } from './utils/stats.js';
|
|
25
|
+
import { AidStatsCollector } from './utils/stats.js';
|
|
22
26
|
import { PermissionGateway } from './core/permission.js';
|
|
23
27
|
import { InteractionRouter } from './core/interaction-router.js';
|
|
24
28
|
import { ChannelLoader } from './core/channel-loader.js';
|
|
25
|
-
import { AgentLoader } from './core/
|
|
29
|
+
import { AgentLoader } from './core/baseagent-loader.js';
|
|
26
30
|
import { EvolAgentRegistry } from './core/evolagent-registry.js';
|
|
27
|
-
import { buildReloadHooks } from './
|
|
31
|
+
import { buildReloadHooks } from './core/channel-loader.js';
|
|
28
32
|
import { IpcServer } from './ipc.js';
|
|
29
33
|
import { logger, setLogLevel } from './utils/logger.js';
|
|
34
|
+
import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
|
|
30
35
|
import { detectDuplicates } from './core/evolagent-registry.js';
|
|
31
36
|
import { loadPromptTemplates } from './agents/templates.js';
|
|
37
|
+
import { TriggerManager } from './core/trigger/manager.js';
|
|
38
|
+
import { TriggerScheduler } from './core/trigger/scheduler.js';
|
|
39
|
+
import { agentTriggersDir } from './paths.js';
|
|
32
40
|
import path from 'path';
|
|
33
41
|
import fs from 'fs';
|
|
42
|
+
import os from 'os';
|
|
43
|
+
import crypto from 'crypto';
|
|
44
|
+
/**
|
|
45
|
+
* 通过 adapter.send 发送系统类 payload(system.notice / system.error / 等)。
|
|
46
|
+
*
|
|
47
|
+
* 网关层(本文件)的所有出站系统通知(上线 / 重启完成 / 渠道告警 / agent 启动失败等)
|
|
48
|
+
* 走这里集中调度,让渠道按 capabilities 决定呈现方式。
|
|
49
|
+
*
|
|
50
|
+
* 当 adapter 还没实现 send(旧 adapter)时,按 payload.kind 降级到 sendText。
|
|
51
|
+
*
|
|
52
|
+
* Exported for unit test coverage; runtime callers are inside main() closure.
|
|
53
|
+
*/
|
|
54
|
+
export async function sendSystemPayload(adapter, envelope, payload) {
|
|
55
|
+
await adapter.send(envelope, payload);
|
|
56
|
+
}
|
|
34
57
|
async function main() {
|
|
35
58
|
// 过滤飞书 SDK 的 info 日志
|
|
36
59
|
const originalLog = console.log;
|
|
@@ -52,107 +75,159 @@ async function main() {
|
|
|
52
75
|
logger.info('EvolClaw starting...');
|
|
53
76
|
// 确保数据目录存在
|
|
54
77
|
ensureDataDirs();
|
|
78
|
+
// 同步包内 kits/ 到 EVOLCLAW_HOME/kits/(首次启动或升级时)
|
|
79
|
+
syncKitsFromPackage();
|
|
80
|
+
// ── 单实例保护(pre-check + post-write self-check)──
|
|
81
|
+
// pre-check:发现已有活 main 直接退出,避免起任何副作用
|
|
82
|
+
{
|
|
83
|
+
const pre = scanInstances();
|
|
84
|
+
const aliveOthers = pre.mains.filter(m => m.alive && m.record.pid !== process.pid);
|
|
85
|
+
if (aliveOthers.length > 0) {
|
|
86
|
+
const pids = aliveOthers.map(m => m.record.pid).join(', ');
|
|
87
|
+
const msg = `❌ Another EvolClaw instance is already running (PID: ${pids}). Use 'evolclaw restart' to replace it.`;
|
|
88
|
+
logger.error(msg);
|
|
89
|
+
console.error(msg);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 立即登记自己(让其他并发启动者能看见我)
|
|
94
|
+
const launchedBy = process.env.EVOLCLAW_LAUNCHED_BY || 'start';
|
|
95
|
+
writeMain(launchedBy);
|
|
96
|
+
logger.info(`✓ Instance record written: main-${process.pid}.json`);
|
|
97
|
+
// post-write 自检:写完 record 后再扫一次,发现并发对手时按 (startedAt, pid) 选赢家
|
|
98
|
+
{
|
|
99
|
+
const verdict = isMainWinner();
|
|
100
|
+
if (!verdict.winner) {
|
|
101
|
+
logger.warn(`Lost main election to PID ${verdict.conflictingPid}, yielding`);
|
|
102
|
+
console.error(`⚠ Another instance (PID ${verdict.conflictingPid}) started concurrently and won the election. Yielding.`);
|
|
103
|
+
removeAll();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
55
107
|
// 加载提示词模板
|
|
56
108
|
loadPromptTemplates();
|
|
57
|
-
//
|
|
58
|
-
const
|
|
109
|
+
// 加载配置(新结构:defaults.json + per-agent config.json)
|
|
110
|
+
const defaults = loadDefaults() ?? { $schema_version: CONFIG_SCHEMA_VERSION };
|
|
59
111
|
// 应用配置中的日志级别(优先于环境变量)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
112
|
+
// logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
|
|
113
|
+
// 阶段 2c 暂跳过
|
|
63
114
|
const paths = resolvePaths();
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
115
|
+
// ── 自动迁移:旧 data/evolclaw.json → 新结构 ──
|
|
116
|
+
autoMigrateIfNeeded();
|
|
117
|
+
// ── EvolAgent Registry:加载 agents/<aid>/config.json ──
|
|
118
|
+
const agentRegistry = new EvolAgentRegistry(paths.agentsDir);
|
|
119
|
+
agentRegistry.loadAll();
|
|
120
|
+
const agentInfos = agentRegistry.list();
|
|
121
|
+
// 启动期硬约束:必须至少有一个 self-agent
|
|
122
|
+
if (agentInfos.length === 0) {
|
|
123
|
+
const skipped = agentRegistry.getSkipped();
|
|
124
|
+
const lines = [
|
|
125
|
+
'❌ No self-agent configured.',
|
|
126
|
+
` Run \`evolclaw aid new <name>\` to create one.`,
|
|
127
|
+
];
|
|
128
|
+
if (skipped.length > 0) {
|
|
129
|
+
lines.push(` Skipped ${skipped.length} dir(s):`);
|
|
130
|
+
for (const s of skipped)
|
|
131
|
+
lines.push(` - ${s.dirName}: ${s.reason}`);
|
|
132
|
+
}
|
|
133
|
+
const msg = lines.join('\n');
|
|
68
134
|
logger.error(msg);
|
|
69
|
-
console.error(msg);
|
|
135
|
+
console.error(msg);
|
|
70
136
|
process.exit(1);
|
|
71
137
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
138
|
+
logger.info(`✓ Loaded ${agentInfos.length} self-agent(s)`);
|
|
139
|
+
for (const info of agentInfos) {
|
|
140
|
+
if (info.status === 'error') {
|
|
141
|
+
logger.error(` ✗ ${info.name}: ${info.error}`);
|
|
142
|
+
}
|
|
143
|
+
else if (info.status === 'disabled') {
|
|
144
|
+
logger.info(` ○ ${info.name} (disabled)`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
logger.info(` ● ${info.name} ${info.baseagent} @ ${path.basename(info.projectPath)}`);
|
|
82
148
|
}
|
|
83
149
|
}
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
// 跨 agent 凭证冲突
|
|
151
|
+
{
|
|
152
|
+
const dups = detectDuplicates(agentRegistry.runnableAgents());
|
|
153
|
+
for (const d of dups) {
|
|
154
|
+
const owners = d.agents.map(o => `${o.aid}(${o.channelName})`).join(', ');
|
|
155
|
+
logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} claimed by ${owners}.`);
|
|
156
|
+
}
|
|
86
157
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
158
|
+
// 选定主 agent(启动期 anthropic resolve 用,配合 IPC `evolagent.list` 显示)
|
|
159
|
+
// 主 agent 取第一个非 error 非 disabled 的 self-agent。
|
|
160
|
+
const primaryAgent = agentRegistry.runnableAgents()[0];
|
|
161
|
+
if (!primaryAgent) {
|
|
162
|
+
const msg = '❌ No runnable self-agent (all are error/disabled). Aborting.';
|
|
163
|
+
logger.error(msg);
|
|
164
|
+
console.error(msg);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
// 进程级设置(从 defaults 取,不属于任何 agent)
|
|
168
|
+
const globalSettings = {
|
|
169
|
+
idleMonitor: defaults.idleMonitor,
|
|
170
|
+
debug: defaults.debug,
|
|
171
|
+
};
|
|
172
|
+
if (globalSettings.debug?.logLevel) {
|
|
173
|
+
setLogLevel(globalSettings.debug.logLevel);
|
|
174
|
+
}
|
|
175
|
+
// 启动期 anthropic 凭证校验(用 primaryAgent 的 baseagents.claude)
|
|
176
|
+
const anthropic = resolveAnthropicConfig({
|
|
177
|
+
agents: { claude: primaryAgent.config.baseagents?.claude },
|
|
95
178
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (evolagentCount > 0) {
|
|
100
|
-
logger.info(`✓ Loaded ${evolagentCount} evolagent(s)`);
|
|
101
|
-
for (const info of agentInfos) {
|
|
102
|
-
if (info.isDefault)
|
|
103
|
-
continue;
|
|
104
|
-
if (info.status === 'error') {
|
|
105
|
-
logger.error(` ✗ [${info.name}] ${info.error}`);
|
|
106
|
-
}
|
|
107
|
-
else if (info.status === 'disabled') {
|
|
108
|
-
logger.info(` ○ [${info.name}] disabled`);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
logger.info(` ● [${info.name}] ${info.baseagent} @ ${path.basename(info.projectPath)}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
179
|
+
logger.info('✓ Config loaded (API keys hidden)');
|
|
180
|
+
if (anthropic.baseUrl) {
|
|
181
|
+
logger.info(`✓ Using custom API base URL: ${anthropic.baseUrl}`);
|
|
114
182
|
}
|
|
115
183
|
// Store for IPC access (T10 will wire this)
|
|
116
184
|
// M4: removed dead globalThis.__evolclaw_agentRegistry assignment
|
|
117
185
|
// 创建事件总线
|
|
118
186
|
const eventBus = new EventBus();
|
|
119
187
|
logger.info('✓ Event bus initialized');
|
|
188
|
+
// 把所有事件录到 events.log(受 EVENT_LOG 环境变量控制)
|
|
189
|
+
eventBus.subscribeAll((event) => logger.event(event));
|
|
120
190
|
// 统计收集器(近 1 小时滚动统计)
|
|
121
191
|
const statsCollector = new StatsCollector(eventBus);
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
const sessionManager = new SessionManager(
|
|
126
|
-
// sessionMode
|
|
127
|
-
sessionManager.setSessionModeResolver((
|
|
128
|
-
|
|
192
|
+
// Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
|
|
193
|
+
const aidStatsCollector = new AidStatsCollector();
|
|
194
|
+
// 初始化 SessionManager(文件系统后端)
|
|
195
|
+
const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
|
|
196
|
+
// sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
|
|
197
|
+
sessionManager.setSessionModeResolver((channelKey, chatType) => {
|
|
198
|
+
const agent = agentRegistry.resolveByChannel(channelKey);
|
|
199
|
+
const cm = agent?.config.chatmode;
|
|
200
|
+
if (!cm)
|
|
201
|
+
return undefined;
|
|
202
|
+
return chatType === 'group' ? cm.group : cm.private;
|
|
129
203
|
});
|
|
130
204
|
logger.info('✓ Database initialized');
|
|
131
205
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
132
206
|
sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
|
|
133
207
|
sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
|
|
134
208
|
sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
|
|
135
|
-
// Agent
|
|
209
|
+
// Agent 插件系统:每个 EvolAgent × 每个 baseagent 一个独立 runner(H1/H2 修复)
|
|
136
210
|
const agentLoader = new AgentLoader();
|
|
137
211
|
agentLoader.register(new ClaudeAgentPlugin());
|
|
138
212
|
agentLoader.register(new CodexAgentPlugin());
|
|
139
213
|
agentLoader.register(new GeminiAgentPlugin());
|
|
140
|
-
const agentInstances = agentLoader.createAll(
|
|
214
|
+
const agentInstances = agentLoader.createAll(agentRegistry, {
|
|
141
215
|
onSessionIdUpdate: async (sessionId, agentSessionId) => {
|
|
142
216
|
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
143
217
|
},
|
|
144
218
|
});
|
|
145
|
-
//
|
|
219
|
+
// agentMap 复合键:${aid}::${baseagent}
|
|
146
220
|
const agentMap = new Map();
|
|
147
221
|
for (const inst of agentInstances) {
|
|
148
|
-
agentMap.set(inst.
|
|
222
|
+
agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
|
|
149
223
|
}
|
|
150
|
-
const
|
|
151
|
-
const
|
|
224
|
+
const primaryBaseagent = primaryAgent.baseagent;
|
|
225
|
+
const primaryRunnerKey = `${primaryAgent.aid}::${primaryBaseagent}`;
|
|
226
|
+
const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
|
|
152
227
|
if (!agentRunner) {
|
|
153
|
-
throw new Error('No agent backend available. Check
|
|
228
|
+
throw new Error('No agent backend available. Check baseagents config (no runners created).');
|
|
154
229
|
}
|
|
155
|
-
logger.info(`✓
|
|
230
|
+
logger.info(`✓ Runners ready (primary key: ${primaryRunnerKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
|
|
156
231
|
// 权限审批网关
|
|
157
232
|
const permissionGateway = new PermissionGateway();
|
|
158
233
|
permissionGateway.setEventBus(eventBus);
|
|
@@ -177,67 +252,46 @@ async function main() {
|
|
|
177
252
|
channelLoader.register(new DingtalkChannelPlugin());
|
|
178
253
|
channelLoader.register(new QQBotChannelPlugin());
|
|
179
254
|
channelLoader.register(new WecomChannelPlugin());
|
|
180
|
-
// Create channel instances:
|
|
181
|
-
const defaultInstances = await channelLoader.createAll(config);
|
|
255
|
+
// Create channel instances: 每个 self-agent 各自的 channels
|
|
182
256
|
const evolagentInstances = [];
|
|
183
257
|
for (const agent of agentRegistry.runnableAgents()) {
|
|
184
|
-
// Rewrite channel instance names with agent prefix to avoid collisions
|
|
185
|
-
// with DefaultAgent and other EvolAgents.
|
|
186
|
-
// Rule (EvolAgent only):
|
|
187
|
-
// - explicit name → `${agent.name}-${type}-${name}`
|
|
188
|
-
// - omitted name → `${agent.name}-${type}`
|
|
189
|
-
const rewrittenChannels = {};
|
|
190
|
-
for (const [type, raw] of Object.entries(agent.config.channels || {})) {
|
|
191
|
-
if (type === 'defaultChannel') {
|
|
192
|
-
rewrittenChannels[type] = raw;
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const instances = Array.isArray(raw) ? raw : [raw];
|
|
196
|
-
const rewritten = instances.map((inst) => {
|
|
197
|
-
if (!inst || typeof inst !== 'object')
|
|
198
|
-
return inst;
|
|
199
|
-
const effName = agent.effectiveChannelName(type, inst.name);
|
|
200
|
-
return { ...inst, name: effName };
|
|
201
|
-
});
|
|
202
|
-
// Preserve original shape (array vs single object)
|
|
203
|
-
rewrittenChannels[type] = Array.isArray(raw) ? rewritten : rewritten[0];
|
|
204
|
-
}
|
|
205
|
-
const agentConfig = {
|
|
206
|
-
agents: agent.config.agents,
|
|
207
|
-
channels: rewrittenChannels,
|
|
208
|
-
projects: agent.config.projects,
|
|
209
|
-
};
|
|
210
258
|
try {
|
|
211
|
-
const instances = await channelLoader.
|
|
259
|
+
const instances = await channelLoader.createForAgent(agent);
|
|
212
260
|
evolagentInstances.push(...instances);
|
|
213
261
|
}
|
|
214
262
|
catch (e) {
|
|
215
|
-
logger.error(`[
|
|
263
|
+
logger.error(`[Agent ${agent.aid}] Failed to create channels: ${e}`);
|
|
216
264
|
agent.status = 'error';
|
|
217
265
|
agent.error = `Channel creation failed: ${e}`;
|
|
218
266
|
}
|
|
219
267
|
}
|
|
220
|
-
const channelInstances =
|
|
221
|
-
logger.info(`✓ Created ${channelInstances.length} channel instance(s)
|
|
222
|
-
//
|
|
223
|
-
|
|
268
|
+
const channelInstances = evolagentInstances;
|
|
269
|
+
logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
|
|
270
|
+
// 初始化触发器调度器(每个 EvolAgent 独立)
|
|
271
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
272
|
+
const triggersDir = agentTriggersDir(agent.aid);
|
|
273
|
+
const triggerManager = new TriggerManager(agent.aid, triggersDir);
|
|
274
|
+
const triggerScheduler = new TriggerScheduler(agent.aid, triggerManager, eventBus);
|
|
275
|
+
agent.triggerManager = triggerManager;
|
|
276
|
+
agent.triggerScheduler = triggerScheduler;
|
|
277
|
+
}
|
|
224
278
|
// 创建命令处理器
|
|
225
|
-
const cmdHandler = new CommandHandler(sessionManager, agentMap,
|
|
279
|
+
const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
|
|
226
280
|
cmdHandler.setPermissionGateway(permissionGateway);
|
|
227
281
|
cmdHandler.setInteractionRouter(interactionRouter);
|
|
228
282
|
cmdHandler.setStatsCollector(statsCollector);
|
|
229
283
|
// 创建消息处理器
|
|
230
|
-
const processor = new MessageProcessor(agentMap, sessionManager,
|
|
284
|
+
const processor = new MessageProcessor(agentMap, sessionManager, globalSettings, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
|
|
231
285
|
const sendFn = async (id, text, opts) => {
|
|
232
286
|
const adapter = cmdHandler.getAdapter(channel);
|
|
233
287
|
if (!adapter)
|
|
234
288
|
return;
|
|
235
289
|
if (text) {
|
|
236
|
-
await adapter.
|
|
290
|
+
await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
|
|
237
291
|
}
|
|
238
292
|
};
|
|
239
293
|
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
240
|
-
},
|
|
294
|
+
}, primaryRunnerKey);
|
|
241
295
|
// 回填 processor 和 messageQueue 的引用
|
|
242
296
|
cmdHandler.setProcessor(processor);
|
|
243
297
|
// Inject EvolAgentRegistry (methods added by T6/T7)
|
|
@@ -260,8 +314,11 @@ async function main() {
|
|
|
260
314
|
await processor.processMessage(message);
|
|
261
315
|
});
|
|
262
316
|
// 设置中断回调(精确中断正在处理的 agent)
|
|
263
|
-
messageQueue.setInterruptCallback(async (sessionKey, agentId) => {
|
|
264
|
-
const
|
|
317
|
+
messageQueue.setInterruptCallback(async (sessionKey, agentId, evolagentName) => {
|
|
318
|
+
const baseagent = agentId || primaryBaseagent;
|
|
319
|
+
const evol = evolagentName || primaryAgent.aid;
|
|
320
|
+
const agent = agentMap.get(`${evol}::${baseagent}`)
|
|
321
|
+
|| agentMap.get(primaryRunnerKey);
|
|
265
322
|
if (agent?.hasActiveStream(sessionKey)) {
|
|
266
323
|
await agent.interrupt(sessionKey);
|
|
267
324
|
}
|
|
@@ -270,6 +327,40 @@ async function main() {
|
|
|
270
327
|
// 回填 messageQueue 引用
|
|
271
328
|
cmdHandler.setMessageQueue(messageQueue);
|
|
272
329
|
processor.setMessageQueue(messageQueue);
|
|
330
|
+
// 启动触发器调度器,设置 fireCallback 投递合成消息
|
|
331
|
+
for (const agent of agentRegistry.runnableAgents()) {
|
|
332
|
+
if (!agent.triggerScheduler || !agent.triggerManager)
|
|
333
|
+
continue;
|
|
334
|
+
const scheduler = agent.triggerScheduler;
|
|
335
|
+
const primaryProjectPath = agent.config.projects?.defaultPath ?? primaryAgent.projectPath;
|
|
336
|
+
scheduler.setFireCallback((msg, trigger) => {
|
|
337
|
+
const sessionKey = `${msg.channel}:${msg.channelId}`;
|
|
338
|
+
messageQueue.enqueue(sessionKey, msg, primaryProjectPath, { interruptible: false }).catch(err => {
|
|
339
|
+
logger.error(`[Trigger] Failed to enqueue trigger ${trigger.id}: ${err}`);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
// Subscribe to trigger:completed/failed/skipped to update cron inflight state
|
|
343
|
+
eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
|
|
344
|
+
eventBus.subscribe('trigger:failed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'failed'));
|
|
345
|
+
eventBus.subscribe('trigger:skipped', (ev) => {
|
|
346
|
+
if (ev.reason === 'interrupted')
|
|
347
|
+
scheduler.onTriggerComplete(ev.triggerId, 'interrupted');
|
|
348
|
+
});
|
|
349
|
+
// Note: only the primary agent's scheduler is wired to cmdHandler.
|
|
350
|
+
// Non-primary agent channels will receive "⚠️ 触发器功能未启用" when using /trigger.
|
|
351
|
+
// Full per-channel scheduler routing is a future improvement.
|
|
352
|
+
try {
|
|
353
|
+
await scheduler.init();
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Inject primary agent's trigger scheduler into cmdHandler
|
|
360
|
+
const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
|
|
361
|
+
if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
|
|
362
|
+
cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
|
|
363
|
+
}
|
|
273
364
|
// 默认策略
|
|
274
365
|
const defaultPolicy = {
|
|
275
366
|
canSwitchProject: (chatType, role) => chatType === 'private' ? (role === 'owner' || role === 'admin') : role === 'owner',
|
|
@@ -283,18 +374,17 @@ async function main() {
|
|
|
283
374
|
accumulateErrors: () => true,
|
|
284
375
|
};
|
|
285
376
|
// ── MessageBridge:Channel ↔ Core 消息桥梁 ──
|
|
286
|
-
const msgBridge = new MessageBridge(
|
|
377
|
+
const msgBridge = new MessageBridge(primaryAgent.projectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, primaryAgent.config.debounce);
|
|
287
378
|
msgBridge.setAgentRegistry(agentRegistry);
|
|
288
379
|
// ── Channel instance registration (shared by startup and hot-load) ──
|
|
289
380
|
function registerChannelInstance(inst) {
|
|
290
381
|
// 1. 项目路径提供器
|
|
291
382
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
292
383
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
293
|
-
// Effective default path: agent
|
|
384
|
+
// Effective default path: use the agent that owns this channel.
|
|
294
385
|
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
295
|
-
const effectiveDefault =
|
|
296
|
-
|
|
297
|
-
: (config.projects?.defaultPath || process.cwd());
|
|
386
|
+
const effectiveDefault = owningAgent?.projectPath
|
|
387
|
+
?? primaryAgent.projectPath;
|
|
298
388
|
const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
|
|
299
389
|
return path.isAbsolute(session.projectPath)
|
|
300
390
|
? session.projectPath
|
|
@@ -317,114 +407,14 @@ async function main() {
|
|
|
317
407
|
interactionRouter.handle(response);
|
|
318
408
|
});
|
|
319
409
|
}
|
|
320
|
-
// 4. MessageBridge
|
|
410
|
+
// 4. MessageBridge 注册
|
|
321
411
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
await handler({
|
|
325
|
-
channel: channelType, channelId: chatId, content, images, chatType,
|
|
326
|
-
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
327
|
-
// 只在话题场景(threadId 有值)才设置 replyContext;
|
|
328
|
-
// 纯引用回复(rootId 有值但无 threadId)不设置,避免所有回复都带引用头
|
|
329
|
-
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
|
330
|
-
});
|
|
331
|
-
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
332
|
-
replyToMessageId: replyContext?.replyToMessageId,
|
|
333
|
-
replyInThread: replyContext?.replyInThread,
|
|
334
|
-
}), inst.adapter, channelType);
|
|
335
|
-
}
|
|
336
|
-
if (channelType === 'wechat') {
|
|
337
|
-
if (inst.channel.setEventBus) {
|
|
338
|
-
inst.channel.setEventBus(eventBus);
|
|
339
|
-
}
|
|
340
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (channelId, content, peerId, images, chatType) => {
|
|
341
|
-
handler({
|
|
342
|
-
channel: channelType,
|
|
343
|
-
channelId,
|
|
344
|
-
content,
|
|
345
|
-
images,
|
|
346
|
-
chatType: chatType || 'private',
|
|
347
|
-
peerId: peerId || '',
|
|
348
|
-
});
|
|
349
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
350
|
-
}
|
|
351
|
-
if (channelType === 'aun') {
|
|
352
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (opts) => {
|
|
353
|
-
handler({
|
|
354
|
-
channel: channelType,
|
|
355
|
-
channelId: opts.channelId,
|
|
356
|
-
content: opts.content,
|
|
357
|
-
chatType: opts.chatType || 'private',
|
|
358
|
-
peerId: opts.peerId || '',
|
|
359
|
-
peerName: opts.peerName,
|
|
360
|
-
messageId: opts.messageId,
|
|
361
|
-
mentions: opts.mentions,
|
|
362
|
-
threadId: opts.threadId,
|
|
363
|
-
replyContext: opts.replyContext,
|
|
364
|
-
});
|
|
365
|
-
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
|
|
366
|
-
// AUN 重连失败通知
|
|
367
|
-
if (inst.channel.setOnChannelDown) {
|
|
368
|
-
inst.channel.setOnChannelDown(() => {
|
|
369
|
-
eventBus.publish({
|
|
370
|
-
type: 'channel:health',
|
|
371
|
-
channel: channelType,
|
|
372
|
-
channelName: inst.adapter.channelName,
|
|
373
|
-
status: 'auth_error',
|
|
374
|
-
message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
375
|
-
timestamp: Date.now(),
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
// proactive 模式入站白名单:注入 sessionMode 查询器
|
|
380
|
-
if (typeof inst.channel.setSessionModeResolver === 'function') {
|
|
381
|
-
const chName = inst.adapter.channelName;
|
|
382
|
-
inst.channel.setSessionModeResolver(async (channelId) => {
|
|
383
|
-
const session = await sessionManager.getActiveSession(chName, channelId);
|
|
384
|
-
return session?.sessionMode;
|
|
385
|
-
});
|
|
386
|
-
}
|
|
412
|
+
if (inst.registerBridge) {
|
|
413
|
+
inst.registerBridge(msgBridge, channelType);
|
|
387
414
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
channel: channelType,
|
|
392
|
-
channelId: event.channelId,
|
|
393
|
-
content: event.content,
|
|
394
|
-
images: event.images,
|
|
395
|
-
chatType: event.chatType || 'private',
|
|
396
|
-
peerId: event.peerId || '',
|
|
397
|
-
peerName: event.peerName,
|
|
398
|
-
messageId: event.messageId,
|
|
399
|
-
});
|
|
400
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
401
|
-
}
|
|
402
|
-
if (channelType === 'qqbot') {
|
|
403
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
404
|
-
handler({
|
|
405
|
-
channel: channelType,
|
|
406
|
-
channelId: event.channelId,
|
|
407
|
-
content: event.content,
|
|
408
|
-
images: event.images,
|
|
409
|
-
chatType: event.chatType || 'private',
|
|
410
|
-
peerId: event.peerId || '',
|
|
411
|
-
peerName: event.peerName,
|
|
412
|
-
messageId: event.messageId,
|
|
413
|
-
});
|
|
414
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
415
|
-
}
|
|
416
|
-
if (channelType === 'wecom') {
|
|
417
|
-
msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
|
|
418
|
-
handler({
|
|
419
|
-
channel: channelType,
|
|
420
|
-
channelId: event.channelId,
|
|
421
|
-
content: event.content,
|
|
422
|
-
chatType: event.chatType || 'private',
|
|
423
|
-
peerId: event.peerId || '',
|
|
424
|
-
peerName: event.peerName,
|
|
425
|
-
messageId: event.messageId,
|
|
426
|
-
});
|
|
427
|
-
}), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
|
|
415
|
+
// 4b. 生命周期钩子
|
|
416
|
+
if (inst.registerHooks) {
|
|
417
|
+
inst.registerHooks({ eventBus, sessionManager });
|
|
428
418
|
}
|
|
429
419
|
// 5. 撤回消息 → 中断执行中任务
|
|
430
420
|
inst.channel.onRecall?.((messageId) => {
|
|
@@ -435,20 +425,18 @@ async function main() {
|
|
|
435
425
|
for (const inst of channelInstances) {
|
|
436
426
|
registerChannelInstance(inst);
|
|
437
427
|
}
|
|
438
|
-
//
|
|
439
|
-
const connected = await channelLoader.connectAll(channelInstances);
|
|
440
|
-
// Bind connected adapters to their owning agents
|
|
441
|
-
// I1: only mark 'running' if a channel actually connected for that agent
|
|
442
|
-
const connectedSet = new Set(connected);
|
|
428
|
+
// Bind adapters to their owning agents and mark running
|
|
443
429
|
for (const inst of channelInstances) {
|
|
444
430
|
const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
|
|
445
431
|
if (!agent || agent.status === 'error')
|
|
446
432
|
continue;
|
|
447
433
|
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
448
|
-
if (agent.status === 'stopped'
|
|
434
|
+
if (agent.status === 'stopped') {
|
|
449
435
|
agent.status = 'running';
|
|
450
436
|
}
|
|
451
437
|
}
|
|
438
|
+
// ── 连接所有渠道(异步,AUN 等 WebSocket 渠道在后台重连)──
|
|
439
|
+
const connected = await channelLoader.connectAll(channelInstances);
|
|
452
440
|
// 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
|
|
453
441
|
for (const inst of channelInstances) {
|
|
454
442
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
@@ -467,10 +455,51 @@ async function main() {
|
|
|
467
455
|
timestamp: Date.now()
|
|
468
456
|
});
|
|
469
457
|
}
|
|
458
|
+
// 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
|
|
459
|
+
// 需在配置中 debug.upmsg: true 手动开启
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
for (const name of connected) {
|
|
462
|
+
const agent = agentRegistry.resolveByChannel(name);
|
|
463
|
+
if (!agent)
|
|
464
|
+
continue;
|
|
465
|
+
if (!agent.config.debug?.upmsg)
|
|
466
|
+
continue;
|
|
467
|
+
const ownerAid = agent.config.owners?.[0];
|
|
468
|
+
if (!ownerAid)
|
|
469
|
+
continue;
|
|
470
|
+
const adapter = agent.channels.get(name);
|
|
471
|
+
if (!adapter)
|
|
472
|
+
continue;
|
|
473
|
+
// 尝试从 agent.md 读取 name
|
|
474
|
+
let agentName = agent.aid;
|
|
475
|
+
try {
|
|
476
|
+
const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
|
|
477
|
+
const agentMdPath = path.join(aunPath, 'AIDs', agent.aid, 'agent.md');
|
|
478
|
+
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
479
|
+
const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
|
|
480
|
+
if (nameMatch)
|
|
481
|
+
agentName = nameMatch[1].trim().replace(/"$/, '');
|
|
482
|
+
}
|
|
483
|
+
catch { }
|
|
484
|
+
const projectDir = path.basename(agent.projectPath);
|
|
485
|
+
const text = `✓ ${agentName} 已上线 | 工作目录: ${projectDir}`;
|
|
486
|
+
const envelope = buildEnvelope({
|
|
487
|
+
taskId: `system-online-${crypto.randomBytes(5).toString('hex')}`,
|
|
488
|
+
channel: adapter.channelName,
|
|
489
|
+
channelId: ownerAid,
|
|
490
|
+
agentName,
|
|
491
|
+
});
|
|
492
|
+
sendSystemPayload(adapter, envelope, {
|
|
493
|
+
kind: 'system.notice',
|
|
494
|
+
text,
|
|
495
|
+
subtype: 'restarted',
|
|
496
|
+
}).catch(() => { });
|
|
497
|
+
}
|
|
498
|
+
}, 1000 + Math.random() * 2000);
|
|
470
499
|
// 统一 channel:health 跨通道通知(仅 auth_error)
|
|
471
500
|
// 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
|
|
472
|
-
eventBus.subscribe('channel:
|
|
473
|
-
if (event.type !== 'channel:
|
|
501
|
+
eventBus.subscribe('channel:error', (event) => {
|
|
502
|
+
if (event.type !== 'channel:error' || event.status !== 'auth_error')
|
|
474
503
|
return;
|
|
475
504
|
const sourceChannelType = event.channel;
|
|
476
505
|
const sourceChannelName = event.channelName || sourceChannelType;
|
|
@@ -483,28 +512,46 @@ async function main() {
|
|
|
483
512
|
continue; // 跳过同类型通道
|
|
484
513
|
if (notified.has(otherType))
|
|
485
514
|
continue; // 同类型已通知过
|
|
486
|
-
const ownerId = agentRegistry.getOwner(other.adapter.channelName)
|
|
515
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelName);
|
|
487
516
|
if (!ownerId)
|
|
488
517
|
continue;
|
|
489
518
|
notified.add(otherType);
|
|
490
|
-
other.adapter.
|
|
519
|
+
const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelName);
|
|
520
|
+
const envelope = buildEnvelope({
|
|
521
|
+
taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
|
|
522
|
+
channel: other.adapter.channelName,
|
|
523
|
+
channelId: ownerId,
|
|
524
|
+
agentName: owningAgent?.aid || 'evolclaw',
|
|
525
|
+
});
|
|
526
|
+
sendSystemPayload(other.adapter, envelope, {
|
|
527
|
+
kind: 'system.error',
|
|
528
|
+
text: msg,
|
|
529
|
+
subtype: 'channel_down',
|
|
530
|
+
recoverable: false,
|
|
531
|
+
}).catch(err => {
|
|
491
532
|
logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
|
|
492
533
|
});
|
|
493
534
|
}
|
|
494
535
|
});
|
|
495
|
-
// 按 channelType
|
|
496
|
-
const
|
|
536
|
+
// 按 channelType 归组显示连接摘要(启动 banner 只显示类型+计数,详情看 `evolclaw status`)
|
|
537
|
+
const connectedTypeCount = new Map();
|
|
538
|
+
const typeOrder = [];
|
|
497
539
|
for (const inst of channelInstances) {
|
|
498
540
|
const name = inst.adapter.channelName;
|
|
499
541
|
if (!connected.includes(name))
|
|
500
542
|
continue;
|
|
501
543
|
const type = inst.channelType || name;
|
|
502
|
-
if (!
|
|
503
|
-
|
|
504
|
-
|
|
544
|
+
if (!connectedTypeCount.has(type)) {
|
|
545
|
+
connectedTypeCount.set(type, 0);
|
|
546
|
+
typeOrder.push(type);
|
|
547
|
+
}
|
|
548
|
+
connectedTypeCount.set(type, connectedTypeCount.get(type) + 1);
|
|
505
549
|
}
|
|
506
|
-
const channelSummary =
|
|
507
|
-
.map(
|
|
550
|
+
const channelSummary = typeOrder
|
|
551
|
+
.map(type => {
|
|
552
|
+
const n = connectedTypeCount.get(type);
|
|
553
|
+
return n === 1 ? type : `${type}×${n}`;
|
|
554
|
+
})
|
|
508
555
|
.join(', ');
|
|
509
556
|
const totalCount = connected.length;
|
|
510
557
|
logger.info(`🚀 EvolClaw is running with ${totalCount} channel(s): ${channelSummary}`);
|
|
@@ -522,12 +569,21 @@ async function main() {
|
|
|
522
569
|
sessionManager.clearProcessing(session.id);
|
|
523
570
|
continue;
|
|
524
571
|
}
|
|
525
|
-
|
|
572
|
+
// 复合键:${aid}::${baseagent},从 channel 反查 self-agent
|
|
573
|
+
const owningAgent = agentRegistry.resolveByChannel(session.channel);
|
|
574
|
+
if (!owningAgent) {
|
|
575
|
+
logger.warn(`[Resume] session ${session.id}: channel "${session.channel}" not routable, skipping`);
|
|
576
|
+
sessionManager.clearProcessing(session.id);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const evolName = owningAgent.aid;
|
|
580
|
+
const baseagentName = session.agentId || primaryBaseagent;
|
|
581
|
+
const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
|
|
526
582
|
if (!agent) {
|
|
527
583
|
sessionManager.clearProcessing(session.id);
|
|
528
584
|
continue;
|
|
529
585
|
}
|
|
530
|
-
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${
|
|
586
|
+
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
|
|
531
587
|
const resumeMessage = {
|
|
532
588
|
channel: session.channel,
|
|
533
589
|
channelId: session.channelId,
|
|
@@ -554,7 +610,19 @@ async function main() {
|
|
|
554
610
|
const replyContext = pending.rootId
|
|
555
611
|
? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
|
|
556
612
|
: undefined;
|
|
557
|
-
|
|
613
|
+
const owningAgent = agentRegistry.resolveByChannel(adapter.channelName);
|
|
614
|
+
const envelope = buildEnvelope({
|
|
615
|
+
taskId: `system-restart-${process.pid}`,
|
|
616
|
+
channel: adapter.channelName,
|
|
617
|
+
channelId: pending.channelId,
|
|
618
|
+
agentName: owningAgent?.aid || 'evolclaw',
|
|
619
|
+
replyContext,
|
|
620
|
+
});
|
|
621
|
+
await sendSystemPayload(adapter, envelope, {
|
|
622
|
+
kind: 'system.notice',
|
|
623
|
+
text: '✅ 服务重启成功!',
|
|
624
|
+
subtype: 'restarted',
|
|
625
|
+
});
|
|
558
626
|
logger.info(`[Restart] Notification sent via ${pending.channel}`);
|
|
559
627
|
}
|
|
560
628
|
fs.unlinkSync(pendingFile);
|
|
@@ -600,6 +668,37 @@ async function main() {
|
|
|
600
668
|
}, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
|
|
601
669
|
// M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
|
|
602
670
|
ipcServer.setAgentRegistry(agentRegistry);
|
|
671
|
+
// 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
|
|
672
|
+
ipcServer.setAunAidProvider(() => {
|
|
673
|
+
const out = [];
|
|
674
|
+
for (const inst of channelInstances) {
|
|
675
|
+
if (inst.channelType !== 'aun')
|
|
676
|
+
continue;
|
|
677
|
+
const ch = inst.channel;
|
|
678
|
+
if (typeof ch?.getAidState === 'function') {
|
|
679
|
+
try {
|
|
680
|
+
out.push(ch.getAidState());
|
|
681
|
+
}
|
|
682
|
+
catch { /* ignore */ }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
});
|
|
687
|
+
// 注入 Per-AID 统计收集器到所有 AUN channel 实例
|
|
688
|
+
for (const inst of channelInstances) {
|
|
689
|
+
if (inst.channelType !== 'aun')
|
|
690
|
+
continue;
|
|
691
|
+
const ch = inst.channel;
|
|
692
|
+
if (typeof ch?.setAidStatsCollector === 'function') {
|
|
693
|
+
ch.setAidStatsCollector(aidStatsCollector);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// 注入 Per-AID 统计 IPC provider
|
|
697
|
+
aidStatsCollector.setQueueStatsProvider((agentName) => ({
|
|
698
|
+
processing: messageQueue.getProcessingCountByAgent(agentName),
|
|
699
|
+
queued: messageQueue.getQueueLengthByAgent(agentName),
|
|
700
|
+
}));
|
|
701
|
+
ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
|
|
603
702
|
// ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
|
|
604
703
|
const reloadHooks = buildReloadHooks({
|
|
605
704
|
channelLoader,
|
|
@@ -609,36 +708,93 @@ async function main() {
|
|
|
609
708
|
});
|
|
610
709
|
// Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
|
|
611
710
|
globalThis.__evolclaw_reloadHooks = reloadHooks;
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
624
|
-
const backupPath = path.join(resolvePaths().dataDir, `evolclaw-${ts}.json`);
|
|
625
|
-
fs.writeFileSync(backupPath, JSON.stringify(config, null, 2));
|
|
626
|
-
logger.warn(`[Config Watch] Config file is not valid JSON. In-memory snapshot saved to ${backupPath}`);
|
|
627
|
-
eventBus.publish({ type: 'config:corrupted', backupPath, reasons: ['Invalid JSON'] });
|
|
628
|
-
return;
|
|
711
|
+
// Hot-load handler: dynamically add a new agent at runtime
|
|
712
|
+
globalThis.__evolclaw_hotLoadAgent = async (aid) => {
|
|
713
|
+
const agent = agentRegistry.loadNewAgent(aid);
|
|
714
|
+
if (!agent)
|
|
715
|
+
throw new Error(`Failed to load agent ${aid}`);
|
|
716
|
+
// 创建 channels
|
|
717
|
+
const instances = await channelLoader.createForAgent(agent);
|
|
718
|
+
for (const inst of instances) {
|
|
719
|
+
registerChannelInstance(inst);
|
|
720
|
+
agent.channels.set(inst.adapter.channelName, inst.adapter);
|
|
721
|
+
channelInstances.push(inst);
|
|
629
722
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
723
|
+
agent.status = 'running';
|
|
724
|
+
// 连接
|
|
725
|
+
await channelLoader.connectAll(instances);
|
|
726
|
+
logger.info(`[HotLoad] ✓ Agent ${aid} online with ${instances.length} channel(s)`);
|
|
727
|
+
};
|
|
728
|
+
// Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
|
|
729
|
+
globalThis.__evolclaw_resyncAgents = async () => {
|
|
730
|
+
const { loadAllAgents: scanAgents, loadDefaults: readDefaults, mergeForAgent: merge } = await import('./config-store.js');
|
|
731
|
+
const freshDefaults = readDefaults();
|
|
732
|
+
const { agents: diskAgents } = scanAgents();
|
|
733
|
+
const diskAidSet = new Set(diskAgents.map(a => a.aid));
|
|
734
|
+
const results = [];
|
|
735
|
+
// 1. 下线:运行时有但磁盘上没有 / disabled 的
|
|
736
|
+
for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
|
|
737
|
+
const diskCfg = diskAgents.find(a => a.aid === aid);
|
|
738
|
+
if (!diskCfg || diskCfg.enabled === false) {
|
|
739
|
+
// 断开所有 channels
|
|
740
|
+
for (const chName of agent.channelInstanceNames()) {
|
|
741
|
+
const inst = channelInstances.find(i => i.adapter.channelName === chName);
|
|
742
|
+
if (inst) {
|
|
743
|
+
try {
|
|
744
|
+
await inst.disconnect();
|
|
745
|
+
}
|
|
746
|
+
catch { }
|
|
747
|
+
const idx = channelInstances.indexOf(inst);
|
|
748
|
+
if (idx >= 0)
|
|
749
|
+
channelInstances.splice(idx, 1);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
agentRegistry.agents.delete(aid);
|
|
753
|
+
results.push(`- ${aid} (offline)`);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
637
756
|
}
|
|
638
|
-
|
|
639
|
-
|
|
757
|
+
// 2. 新增:磁盘上有但运行时没有的
|
|
758
|
+
for (const cfg of diskAgents) {
|
|
759
|
+
if (cfg.enabled === false)
|
|
760
|
+
continue;
|
|
761
|
+
if (agentRegistry.agents.has(cfg.aid))
|
|
762
|
+
continue;
|
|
763
|
+
try {
|
|
764
|
+
await globalThis.__evolclaw_hotLoadAgent(cfg.aid);
|
|
765
|
+
results.push(`+ ${cfg.aid} (online)`);
|
|
766
|
+
}
|
|
767
|
+
catch (e) {
|
|
768
|
+
results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
|
|
769
|
+
}
|
|
640
770
|
}
|
|
641
|
-
|
|
771
|
+
// 3. 已有的:重新 reload(config 可能改了)
|
|
772
|
+
const hooks = globalThis.__evolclaw_reloadHooks;
|
|
773
|
+
for (const cfg of diskAgents) {
|
|
774
|
+
if (cfg.enabled === false)
|
|
775
|
+
continue;
|
|
776
|
+
if (!agentRegistry.agents.has(cfg.aid))
|
|
777
|
+
continue;
|
|
778
|
+
// 只有磁盘上存在且运行时也存在的才 reload
|
|
779
|
+
try {
|
|
780
|
+
await agentRegistry.reload(cfg.aid, hooks);
|
|
781
|
+
results.push(`↻ ${cfg.aid} (reloaded)`);
|
|
782
|
+
}
|
|
783
|
+
catch (e) {
|
|
784
|
+
results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// 重建 channel index
|
|
788
|
+
agentRegistry.channelIndex.clear();
|
|
789
|
+
agentRegistry.buildChannelIndex();
|
|
790
|
+
logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
|
|
791
|
+
return results;
|
|
792
|
+
};
|
|
793
|
+
// I3: start IPC server LAST, after all hook setup, to eliminate race window
|
|
794
|
+
ipcServer.start();
|
|
795
|
+
// 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
|
|
796
|
+
// 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
|
|
797
|
+
// 显式触发更可控。
|
|
642
798
|
// 优雅关闭
|
|
643
799
|
let shutdownSignal = 'unknown';
|
|
644
800
|
const shutdown = async (signal) => {
|
|
@@ -647,7 +803,6 @@ async function main() {
|
|
|
647
803
|
const pid = process.pid;
|
|
648
804
|
const ppid = process.ppid;
|
|
649
805
|
logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
|
|
650
|
-
fs.unwatchFile(configPath);
|
|
651
806
|
ipcServer.stop();
|
|
652
807
|
eventBus.publish({
|
|
653
808
|
type: 'system:shutdown',
|
|
@@ -660,15 +815,24 @@ async function main() {
|
|
|
660
815
|
eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
|
|
661
816
|
}
|
|
662
817
|
sessionManager.close();
|
|
818
|
+
removeAll();
|
|
663
819
|
logger.info('✓ Shutdown complete');
|
|
664
820
|
process.exit(0);
|
|
665
821
|
};
|
|
666
822
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
667
823
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
824
|
+
// 兜底:进程退出前同步删除 instance 文件(防 async shutdown 未完成就被杀)
|
|
825
|
+
process.on('exit', () => {
|
|
826
|
+
removeAll();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
// 仅在直接执行时启动;导入此模块(如单元测试)时不触发 main()。
|
|
830
|
+
import { isMainScript } from './utils/cross-platform.js';
|
|
831
|
+
if (isMainScript(import.meta.url)) {
|
|
832
|
+
main().catch((error) => {
|
|
833
|
+
const msg = `Fatal error: ${error?.stack || error}`;
|
|
834
|
+
logger.error('Fatal error:', error);
|
|
835
|
+
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
836
|
+
process.exit(1);
|
|
837
|
+
});
|
|
668
838
|
}
|
|
669
|
-
main().catch((error) => {
|
|
670
|
-
const msg = `Fatal error: ${error?.stack || error}`;
|
|
671
|
-
logger.error('Fatal error:', error);
|
|
672
|
-
console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
|
|
673
|
-
process.exit(1);
|
|
674
|
-
});
|