evolclaw 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +571 -223
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/error-utils.js +4 -2
  27. package/dist/utils/init-feishu.js +2 -0
  28. package/dist/utils/init-wechat.js +2 -0
  29. package/dist/utils/init.js +285 -53
  30. package/dist/utils/ipc-client.js +36 -0
  31. package/dist/utils/migrate-project.js +122 -0
  32. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  33. package/dist/utils/rich-content-renderer.js +228 -0
  34. package/dist/utils/session-file-health.js +11 -34
  35. package/dist/utils/stream-debouncer.js +122 -0
  36. package/dist/utils/stream-idle-monitor.js +1 -1
  37. package/package.json +3 -1
  38. package/dist/core/agent-runner.js +0 -348
  39. package/dist/core/message-stream.js +0 -59
  40. package/dist/index.js.bak +0 -340
  41. package/dist/utils/markdown-to-feishu.js +0 -94
  42. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  43. /package/dist/{core → utils}/message-cache.js +0 -0
@@ -0,0 +1,32 @@
1
+ import { EventEmitter } from 'events';
2
+ export class EventBus extends EventEmitter {
3
+ publish(event) {
4
+ const handlers = [
5
+ ...this.listeners(event.type),
6
+ ...this.listeners('*'),
7
+ ];
8
+ for (const handler of handlers) {
9
+ try {
10
+ handler(event);
11
+ }
12
+ catch (err) {
13
+ console.error(`[EventBus] Handler error for ${event.type}:`, err);
14
+ }
15
+ }
16
+ }
17
+ subscribe(eventType, handler) {
18
+ this.on(eventType, handler);
19
+ }
20
+ subscribeAll(handler) {
21
+ this.on('*', handler);
22
+ }
23
+ subscribePrefix(prefix, handler) {
24
+ this.on('*', (event) => {
25
+ if (event.type.startsWith(prefix))
26
+ handler(event);
27
+ });
28
+ }
29
+ unsubscribe(eventType, handler) {
30
+ this.off(eventType, handler);
31
+ }
32
+ }
@@ -0,0 +1,71 @@
1
+ import net from 'net';
2
+ import fs from 'fs';
3
+ import { logger } from '../utils/logger.js';
4
+ export class IpcServer {
5
+ socketPath;
6
+ getStatus;
7
+ server = null;
8
+ constructor(socketPath, getStatus) {
9
+ this.socketPath = socketPath;
10
+ this.getStatus = getStatus;
11
+ }
12
+ start() {
13
+ // Remove stale socket file
14
+ try {
15
+ fs.unlinkSync(this.socketPath);
16
+ }
17
+ catch { }
18
+ this.server = net.createServer((conn) => {
19
+ let buf = '';
20
+ conn.on('data', (data) => {
21
+ buf += data.toString();
22
+ // Simple newline-delimited JSON protocol
23
+ const idx = buf.indexOf('\n');
24
+ if (idx === -1)
25
+ return;
26
+ const line = buf.slice(0, idx);
27
+ buf = buf.slice(idx + 1);
28
+ try {
29
+ const cmd = JSON.parse(line);
30
+ const response = this.handleCommand(cmd);
31
+ conn.end(JSON.stringify(response) + '\n');
32
+ }
33
+ catch {
34
+ conn.end(JSON.stringify({ error: 'invalid request' }) + '\n');
35
+ }
36
+ });
37
+ conn.on('error', () => { }); // ignore client errors
38
+ });
39
+ this.server.on('error', (err) => {
40
+ logger.error('[IPC] Server error:', err);
41
+ });
42
+ this.server.listen(this.socketPath, () => {
43
+ // Ensure socket is readable by current user only
44
+ try {
45
+ fs.chmodSync(this.socketPath, 0o600);
46
+ }
47
+ catch { }
48
+ logger.info(`[IPC] Listening on ${this.socketPath}`);
49
+ });
50
+ }
51
+ stop() {
52
+ if (this.server) {
53
+ this.server.close();
54
+ this.server = null;
55
+ }
56
+ try {
57
+ fs.unlinkSync(this.socketPath);
58
+ }
59
+ catch { }
60
+ }
61
+ handleCommand(cmd) {
62
+ switch (cmd.type) {
63
+ case 'status':
64
+ return this.getStatus();
65
+ case 'ping':
66
+ return { pong: true, pid: process.pid };
67
+ default:
68
+ return { error: `unknown command: ${cmd.type}` };
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,187 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { StreamDebouncer } from '../utils/stream-debouncer.js';
3
+ /**
4
+ * MessageBridge — Channel 与 Core 之间的消息桥梁
5
+ *
6
+ * 入站管线:Channel.onMessage → owner 绑定 → 命令路由 → session 解析
7
+ * → 策略前缀 → 构造 Message → debounce → ACK → enqueue
8
+ * 出站:命令响应通过 sendReply 回调直接发送到渠道
9
+ */
10
+ export class MessageBridge {
11
+ config;
12
+ sessionManager;
13
+ processor;
14
+ messageQueue;
15
+ cmdHandler;
16
+ eventBus;
17
+ debouncers = new Map();
18
+ defaultDebounce;
19
+ constructor(config, sessionManager, processor, messageQueue, cmdHandler, eventBus) {
20
+ this.config = config;
21
+ this.sessionManager = sessionManager;
22
+ this.processor = processor;
23
+ this.messageQueue = messageQueue;
24
+ this.cmdHandler = cmdHandler;
25
+ this.eventBus = eventBus;
26
+ this.defaultDebounce = config.debounce ?? 2;
27
+ }
28
+ getDebouncer(channelName) {
29
+ let d = this.debouncers.get(channelName);
30
+ if (!d) {
31
+ const chConfig = this.config.channels?.[channelName];
32
+ const seconds = chConfig?.debounce ?? this.defaultDebounce;
33
+ d = new StreamDebouncer(seconds);
34
+ this.debouncers.set(channelName, d);
35
+ }
36
+ return d;
37
+ }
38
+ /**
39
+ * 为渠道注册消息桥梁:入站处理管线 + 出站命令响应
40
+ *
41
+ * @param channelName 渠道标识
42
+ * @param onMessage 注册入站消息监听
43
+ * @param sendReply 出站:命令响应发送回调
44
+ * @param adapter 渠道适配器(用于 ACK)
45
+ */
46
+ register(channelName, onMessage, sendReply, adapter) {
47
+ onMessage(async (msg) => {
48
+ let content = msg.content.trim();
49
+ // 0. 自定义消息快速路径(menu.query 等)
50
+ if (await this.handleCustomPayload(content, channelName, msg, sendReply, adapter))
51
+ return;
52
+ // 1. owner 绑定
53
+ if (msg.peerId)
54
+ await this.autoBindOwner(channelName, msg.peerId);
55
+ // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
56
+ const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
57
+ if (await this.handleCommand(contentForCmd || content, channelName, msg.channelId, (text) => sendReply(msg.channelId, text, msg.replyContext), msg.peerId, msg.threadId))
58
+ return;
59
+ // 3. session 解析(使用 Channel 层填充的 chatType)
60
+ const chatType = msg.chatType || 'private';
61
+ const metadata = {};
62
+ if (msg.replyContext)
63
+ metadata.replyContext = msg.replyContext;
64
+ if (chatType === 'private' && msg.peerId) {
65
+ metadata.peerId = msg.peerId;
66
+ if (msg.peerName)
67
+ metadata.peerName = msg.peerName;
68
+ }
69
+ const session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, this.config.projects?.defaultPath || process.cwd(), msg.threadId, Object.keys(metadata).length ? metadata : undefined, undefined, msg.peerId, chatType);
70
+ // 4. 消息前缀(由 policy 决定)
71
+ const channelInfo = this.processor.getChannelInfo?.(channelName);
72
+ if (channelInfo?.policy) {
73
+ const prefix = channelInfo.policy.messagePrefix(chatType, msg.peerName);
74
+ if (prefix)
75
+ content = prefix + content;
76
+ }
77
+ // 5. 构造完整消息
78
+ const fullMessage = {
79
+ channel: channelName, channelId: msg.channelId, content,
80
+ chatType,
81
+ images: msg.images, timestamp: Date.now(),
82
+ peerId: msg.peerId, peerName: msg.peerName,
83
+ messageId: msg.messageId,
84
+ mentions: msg.mentions, threadId: msg.threadId,
85
+ replyContext: msg.replyContext,
86
+ };
87
+ // 6. ACK + debounce/enqueue
88
+ // ACK 在到达时立即做(每条独立 ACK),不等合并
89
+ // Interrupt 模式(单聊)→ 入队前 debounce 合并
90
+ // FIFO 模式(群聊) → 跳过 debouncer,独立入队,出队时贪心合并
91
+ if (fullMessage.messageId)
92
+ adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
93
+ const isInterrupt = chatType !== 'group';
94
+ const doEnqueue = async (m) => {
95
+ return this.messageQueue.enqueue(session.id, m, session.projectPath, {
96
+ interruptible: isInterrupt,
97
+ });
98
+ };
99
+ if (isInterrupt) {
100
+ const debouncer = this.getDebouncer(channelName);
101
+ if (debouncer.enabled) {
102
+ const debounceKey = msg.peerId ? `${session.id}:${msg.peerId}` : session.id;
103
+ await debouncer.submit(debounceKey, fullMessage, doEnqueue);
104
+ }
105
+ else {
106
+ await doEnqueue(fullMessage);
107
+ }
108
+ }
109
+ else {
110
+ // 群聊 FIFO:直接入队,由 MessageQueue.processNext 出队时合并
111
+ await doEnqueue(fullMessage);
112
+ }
113
+ });
114
+ }
115
+ /** 自定义消息快速路径:拦截 menu.query 等自定义 payload,返回 true 表示已处理 */
116
+ async handleCustomPayload(content, channel, msg, sendReply, adapter) {
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(content);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ if (!parsed || typeof parsed !== 'object' || !parsed.type)
125
+ return false;
126
+ if (parsed.type === 'menu.query') {
127
+ const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
128
+ const isAdmin = identity.role === 'owner';
129
+ const items = this.cmdHandler.getMenuItems(isAdmin);
130
+ const response = JSON.stringify({ type: 'menu.response', items });
131
+ if (adapter?.sendCustomPayload) {
132
+ adapter.sendCustomPayload(msg.channelId, response);
133
+ }
134
+ else {
135
+ await sendReply(msg.channelId, response);
136
+ }
137
+ return true;
138
+ }
139
+ return false;
140
+ }
141
+ /** 首次交互自动绑定 owner */
142
+ async autoBindOwner(channel, userId) {
143
+ const channelConfig = this.config.channels?.[channel];
144
+ if (channelConfig && !channelConfig.owner) {
145
+ const { setOwner } = await import('../config.js');
146
+ setOwner(this.config, channel, userId);
147
+ logger.info(`[Owner] Auto-bound ${channel} owner: ${userId}`);
148
+ this.eventBus.publish({ type: 'channel:owner-bound', channel, userId });
149
+ }
150
+ }
151
+ /** 命令快速路径:返回 true 表示已处理 */
152
+ async handleCommand(content, channel, channelId, sendReply, userId, threadId) {
153
+ if (!this.cmdHandler.isCommand(content))
154
+ return false;
155
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId);
156
+ if (cmdResult === null)
157
+ return false;
158
+ if (cmdResult) {
159
+ try {
160
+ await sendReply(cmdResult);
161
+ }
162
+ catch (error) {
163
+ logger.error(`[${channel}] Failed to send command response:`, error);
164
+ }
165
+ }
166
+ return true;
167
+ }
168
+ /**
169
+ * 撤回消息:先查 debounce 窗口,再查 message queue。
170
+ * @returns true 如果找到并取消
171
+ */
172
+ cancel(messageId) {
173
+ // 阶段 1: debounce 窗口(尚未入队)
174
+ for (const d of this.debouncers.values()) {
175
+ if (d.cancel(messageId))
176
+ return true;
177
+ }
178
+ // 阶段 2: 已入队但未处理(合并后 messageId 可能是逗号分隔的多个 id)
179
+ return this.messageQueue.cancel(messageId);
180
+ }
181
+ /** 清理资源 */
182
+ dispose() {
183
+ for (const d of this.debouncers.values())
184
+ d.dispose();
185
+ this.debouncers.clear();
186
+ }
187
+ }