evolclaw 2.1.2 → 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.
- package/README.md +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +567 -205
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /package/dist/{core → utils}/message-cache.js +0 -0
|
@@ -3,30 +3,56 @@ import { logger } from '../utils/logger.js';
|
|
|
3
3
|
export class MessageQueue {
|
|
4
4
|
queues = new Map();
|
|
5
5
|
processing = new Set();
|
|
6
|
+
externalLocks = new Map();
|
|
6
7
|
handler;
|
|
7
8
|
currentSessionKey;
|
|
8
9
|
currentProjectPath;
|
|
10
|
+
currentAgentId;
|
|
9
11
|
interruptCallback;
|
|
12
|
+
eventBus;
|
|
13
|
+
recentMessageIds = new Set();
|
|
14
|
+
DEDUP_WINDOW = 60_000; // 1 分钟窗口
|
|
10
15
|
constructor(handler) {
|
|
11
16
|
this.handler = handler;
|
|
12
17
|
}
|
|
13
18
|
setInterruptCallback(callback) {
|
|
14
19
|
this.interruptCallback = callback;
|
|
15
20
|
}
|
|
21
|
+
setEventBus(eventBus) {
|
|
22
|
+
this.eventBus = eventBus;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查消息是否应该处理(去重)
|
|
26
|
+
*/
|
|
27
|
+
shouldProcess(message) {
|
|
28
|
+
if (!message.messageId)
|
|
29
|
+
return true; // 无 ID 的消息不去重
|
|
30
|
+
if (this.recentMessageIds.has(message.messageId)) {
|
|
31
|
+
logger.debug(`[Queue] Duplicate message ${message.messageId}, skipping`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
this.recentMessageIds.add(message.messageId);
|
|
35
|
+
setTimeout(() => this.recentMessageIds.delete(message.messageId), this.DEDUP_WINDOW);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
16
38
|
/**
|
|
17
39
|
* 检查队列 key 是否属于指定 sessionKey
|
|
18
40
|
*/
|
|
19
41
|
matchesSession(key, sessionKey) {
|
|
20
|
-
return key.startsWith(sessionKey + '
|
|
42
|
+
return key.startsWith(sessionKey + '::');
|
|
21
43
|
}
|
|
22
44
|
/**
|
|
23
45
|
* 生成项目级别的队列 key
|
|
24
46
|
*/
|
|
25
47
|
getQueueKey(sessionKey, projectPath) {
|
|
26
|
-
const
|
|
27
|
-
return `${sessionKey}
|
|
48
|
+
const normalized = projectPath ? path.resolve(projectPath) : '';
|
|
49
|
+
return `${sessionKey}::${normalized}`;
|
|
28
50
|
}
|
|
29
|
-
async enqueue(sessionKey, message, projectPath) {
|
|
51
|
+
async enqueue(sessionKey, message, projectPath, options) {
|
|
52
|
+
// 消息去重检查
|
|
53
|
+
if (!this.shouldProcess(message)) {
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
}
|
|
30
56
|
const queueKey = this.getQueueKey(sessionKey, projectPath);
|
|
31
57
|
logger.debug(`[Queue] Enqueuing message for ${queueKey}`);
|
|
32
58
|
return new Promise((resolve, reject) => {
|
|
@@ -34,11 +60,19 @@ export class MessageQueue {
|
|
|
34
60
|
this.queues.set(queueKey, []);
|
|
35
61
|
}
|
|
36
62
|
this.queues.get(queueKey).push({ message, projectPath, resolve, reject });
|
|
37
|
-
//
|
|
63
|
+
// 根据 interruptible 选项决定是否触发中断
|
|
38
64
|
if (this.processing.has(queueKey)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
if (options?.interruptible !== false) {
|
|
66
|
+
// 单聊:保留中断行为
|
|
67
|
+
logger.debug(`[Queue] ${queueKey} is processing, triggering interrupt`);
|
|
68
|
+
this.eventBus?.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'new_message' });
|
|
69
|
+
if (this.interruptCallback) {
|
|
70
|
+
this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// 群聊:FIFO,不打断
|
|
75
|
+
logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
|
|
42
76
|
}
|
|
43
77
|
}
|
|
44
78
|
else {
|
|
@@ -51,6 +85,12 @@ export class MessageQueue {
|
|
|
51
85
|
this.processing.add(queueKey);
|
|
52
86
|
logger.debug(`[Queue] Processing queue ${queueKey}`);
|
|
53
87
|
while (true) {
|
|
88
|
+
// 等待外部锁释放(/compact, /clear 等快速命令)
|
|
89
|
+
const lock = this.getExternalLock(queueKey);
|
|
90
|
+
if (lock) {
|
|
91
|
+
logger.debug(`[Queue] Waiting for external lock on ${queueKey}`);
|
|
92
|
+
await lock;
|
|
93
|
+
}
|
|
54
94
|
const queue = this.queues.get(queueKey);
|
|
55
95
|
if (!queue || queue.length === 0) {
|
|
56
96
|
logger.debug(`[Queue] Queue ${queueKey} is empty, stopping`);
|
|
@@ -59,21 +99,76 @@ export class MessageQueue {
|
|
|
59
99
|
this.currentProjectPath = undefined;
|
|
60
100
|
return;
|
|
61
101
|
}
|
|
62
|
-
|
|
102
|
+
// FIFO 贪心合并:弹出队首连续同 peerId 的消息
|
|
103
|
+
const items = this.dequeueGreedy(queue);
|
|
104
|
+
const merged = items.length === 1 ? items[0] : this.mergeItems(items);
|
|
63
105
|
this.currentSessionKey = queueKey;
|
|
64
|
-
this.currentProjectPath = projectPath;
|
|
65
|
-
|
|
106
|
+
this.currentProjectPath = merged.projectPath;
|
|
107
|
+
this.currentAgentId = merged.message.agentId;
|
|
108
|
+
const resolves = items.map(i => i.resolve);
|
|
109
|
+
const rejects = items.map(i => i.reject);
|
|
110
|
+
logger.debug(`[Queue] Processing ${items.length} message(s) from ${merged.message.channel}:${merged.message.channelId}`);
|
|
66
111
|
try {
|
|
67
|
-
await this.handler(message);
|
|
112
|
+
await this.handler(merged.message);
|
|
68
113
|
logger.debug(`[Queue] Message processed successfully`);
|
|
69
|
-
|
|
114
|
+
resolves.forEach(r => r());
|
|
70
115
|
}
|
|
71
116
|
catch (error) {
|
|
72
117
|
logger.error(`[Queue] Message processing failed:`, error);
|
|
73
|
-
|
|
118
|
+
rejects.forEach(r => r(error));
|
|
74
119
|
}
|
|
75
120
|
}
|
|
76
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* 贪心弹出队首连续同 peerId 的消息。
|
|
124
|
+
* 遇到不同 peerId 或队列为空时停止。
|
|
125
|
+
*/
|
|
126
|
+
dequeueGreedy(queue) {
|
|
127
|
+
const first = queue.shift();
|
|
128
|
+
const result = [first];
|
|
129
|
+
const peerId = first.message.peerId;
|
|
130
|
+
while (queue.length > 0 && queue[0].message.peerId === peerId) {
|
|
131
|
+
result.push(queue.shift());
|
|
132
|
+
}
|
|
133
|
+
if (result.length > 1) {
|
|
134
|
+
logger.debug(`[Queue] Greedy dequeue: merged ${result.length} messages from peerId=${peerId}`);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 合并多条同 peerId 消息:
|
|
140
|
+
* - content: \n 连接
|
|
141
|
+
* - images / mentions: 扁平合并
|
|
142
|
+
* - messageId: 置空(合并后不代表某一条具体消息)
|
|
143
|
+
* - replyContext / peerName / 其余字段: 取最后一条
|
|
144
|
+
*/
|
|
145
|
+
mergeItems(items) {
|
|
146
|
+
const contents = [];
|
|
147
|
+
const allImages = [];
|
|
148
|
+
const allMentions = [];
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
const m = item.message;
|
|
151
|
+
contents.push(m.content);
|
|
152
|
+
if (m.images)
|
|
153
|
+
allImages.push(...m.images);
|
|
154
|
+
if (m.mentions)
|
|
155
|
+
allMentions.push(...m.mentions);
|
|
156
|
+
}
|
|
157
|
+
const last = items[items.length - 1];
|
|
158
|
+
const merged = {
|
|
159
|
+
...last.message,
|
|
160
|
+
content: contents.join('\n'),
|
|
161
|
+
images: allImages.length > 0 ? allImages : undefined,
|
|
162
|
+
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
163
|
+
messageId: undefined,
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
message: merged,
|
|
167
|
+
projectPath: last.projectPath,
|
|
168
|
+
resolve: () => { }, // 由调用方管理
|
|
169
|
+
reject: () => { },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
77
172
|
getQueueLength(sessionKey) {
|
|
78
173
|
// 计算该 sessionKey 下所有项目队列的总长度
|
|
79
174
|
let total = 0;
|
|
@@ -93,24 +188,53 @@ export class MessageQueue {
|
|
|
93
188
|
}
|
|
94
189
|
return false;
|
|
95
190
|
}
|
|
191
|
+
cancel(messageId) {
|
|
192
|
+
for (const queue of this.queues.values()) {
|
|
193
|
+
const idx = queue.findIndex(q => q.message.messageId === messageId);
|
|
194
|
+
if (idx !== -1) {
|
|
195
|
+
const [removed] = queue.splice(idx, 1);
|
|
196
|
+
removed.resolve();
|
|
197
|
+
logger.info(`[Queue] Cancelled queued message ${messageId}`);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
96
203
|
/**
|
|
97
|
-
*
|
|
204
|
+
* 外部锁:快速命令(/compact, /clear)执行期间阻塞队列处理
|
|
205
|
+
* 返回 release 函数
|
|
98
206
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
207
|
+
acquireLock(sessionKey) {
|
|
208
|
+
let releaseFn;
|
|
209
|
+
const promise = new Promise(resolve => { releaseFn = resolve; });
|
|
210
|
+
this.externalLocks.set(sessionKey, promise);
|
|
211
|
+
return () => {
|
|
212
|
+
this.externalLocks.delete(sessionKey);
|
|
213
|
+
releaseFn();
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** 检查是否有外部锁 */
|
|
217
|
+
getExternalLock(queueKey) {
|
|
218
|
+
for (const [key, promise] of this.externalLocks) {
|
|
219
|
+
if (this.matchesSession(queueKey, key))
|
|
220
|
+
return promise;
|
|
113
221
|
}
|
|
114
222
|
return undefined;
|
|
115
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* 获取全局队列长度(所有会话的待处理消息总数)
|
|
226
|
+
*/
|
|
227
|
+
getGlobalQueueLength() {
|
|
228
|
+
let total = 0;
|
|
229
|
+
for (const queue of this.queues.values()) {
|
|
230
|
+
total += queue.length;
|
|
231
|
+
}
|
|
232
|
+
return total;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 获取全局处理中队列数量
|
|
236
|
+
*/
|
|
237
|
+
getGlobalProcessingCount() {
|
|
238
|
+
return this.processing.size;
|
|
239
|
+
}
|
|
116
240
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { summarizeToolInput } from '../utils/permission-utils.js';
|
|
2
|
+
export class PermissionGateway {
|
|
3
|
+
pending = new Map();
|
|
4
|
+
timeout = 5 * 60 * 1000;
|
|
5
|
+
eventBus;
|
|
6
|
+
setEventBus(eventBus) {
|
|
7
|
+
this.eventBus = eventBus;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 请求人工审批。调用方负责模式判断(仅 approve 模式调用此方法)。
|
|
11
|
+
* 黑名单检查由调用方(preToolUseHook)在调用此方法前完成。
|
|
12
|
+
*/
|
|
13
|
+
async requestPermission(sessionId, toolName, toolInput, sendPrompt, summary, reason) {
|
|
14
|
+
const requestId = `perm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
15
|
+
const displaySummary = summary || summarizeToolInput(toolName, toolInput);
|
|
16
|
+
const reasonLine = reason ? `\n原因:${reason}` : '';
|
|
17
|
+
this.eventBus?.publish({ type: 'permission:requested', sessionId, requestId, toolName, input: displaySummary });
|
|
18
|
+
await sendPrompt(`🔐 权限请求\n工具:${toolName}\n操作:${displaySummary}${reasonLine}\n\n回复 /perm allow 批准 或 /perm deny 拒绝`);
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
this.pending.delete(requestId);
|
|
22
|
+
this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId });
|
|
23
|
+
resolve(false);
|
|
24
|
+
}, this.timeout);
|
|
25
|
+
this.pending.set(requestId, { sessionId, resolve, timer });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
resolvePermission(sessionId, requestId, approved) {
|
|
29
|
+
const pending = this.pending.get(requestId);
|
|
30
|
+
if (!pending || pending.sessionId !== sessionId)
|
|
31
|
+
return false;
|
|
32
|
+
clearTimeout(pending.timer);
|
|
33
|
+
pending.resolve(approved);
|
|
34
|
+
this.pending.delete(requestId);
|
|
35
|
+
this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, approved });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** 中断时取消指定会话的所有 pending 权限请求 */
|
|
39
|
+
cancelAll(sessionId) {
|
|
40
|
+
for (const [requestId, pending] of this.pending.entries()) {
|
|
41
|
+
if (pending.sessionId === sessionId) {
|
|
42
|
+
clearTimeout(pending.timer);
|
|
43
|
+
pending.resolve(false);
|
|
44
|
+
this.pending.delete(requestId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** 获取指定会话的所有 pending requestId */
|
|
49
|
+
getPendingRequests(sessionId) {
|
|
50
|
+
const ids = [];
|
|
51
|
+
for (const [requestId, pending] of this.pending.entries()) {
|
|
52
|
+
if (pending.sessionId === sessionId) {
|
|
53
|
+
ids.push(requestId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return ids;
|
|
57
|
+
}
|
|
58
|
+
}
|