evolclaw 3.1.10 → 3.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/CHANGELOG.md +38 -0
- package/README.md +26 -4
- package/dist/agents/kit-renderer.js +5 -1
- package/dist/agents/manifest-engine.js +108 -35
- package/dist/agents/message-renderer.js +2 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/channels/aun.js +212 -158
- package/dist/channels/feishu.js +10 -14
- package/dist/channels/wechat.js +8 -2
- package/dist/cli/agent.js +38 -10
- package/dist/cli/index.js +50 -8
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +162 -82
- package/dist/config-store.js +38 -7
- package/dist/core/cache/file-cache.js +216 -0
- package/dist/core/command-handler.js +291 -68
- package/dist/core/evolagent-registry.js +3 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/message/command-handler-agent-control.js +153 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/message-bridge.js +5 -3
- package/dist/core/message/message-processor.js +44 -36
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/model/model-scope.js +39 -6
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/evolclaw-config.js +11 -0
- package/dist/index.js +57 -2
- package/dist/ipc.js +6 -0
- package/dist/paths.js +7 -3
- package/dist/utils/media-cache.js +40 -1
- package/dist/utils/npm-ops.js +13 -3
- package/kits/templates/message-fragments/item.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { agentCreateNonInteractive, agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, } from '../../cli/agent.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { resolvePaths } from '../../paths.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { CreateStatusWriter, readCreateStatus } from './create-status.js';
|
|
6
|
+
/** 把 cli/agent.ts 的 error 字符串映射为结构化错误码 */
|
|
7
|
+
function classifyError(error) {
|
|
8
|
+
if (/already exists/i.test(error))
|
|
9
|
+
return 'CONFLICT';
|
|
10
|
+
if (/not found/i.test(error))
|
|
11
|
+
return 'NOT_FOUND';
|
|
12
|
+
if (/invalid|must be|required|缺少/i.test(error))
|
|
13
|
+
return 'INVALID_ARGS';
|
|
14
|
+
return 'INTERNAL';
|
|
15
|
+
}
|
|
16
|
+
/** 后台异步:实际创建 agent + 落 model/chatmode,全程写构建进度(D3)。
|
|
17
|
+
* 失败仅写日志 + create-status,不回传(受理即返回)。
|
|
18
|
+
* agentSet key 对照 cli/agent.ts 的 setNestedValue:
|
|
19
|
+
* model → 'models.default'(ModelsBlock.default);chatmode → 'chatmode'(ChatmodeBlock 对象)。 */
|
|
20
|
+
async function runCreateInBackground(opts) {
|
|
21
|
+
const agentDir = path.join(resolvePaths().agentsDir, opts.aid);
|
|
22
|
+
const w = new CreateStatusWriter(agentDir, opts.aid);
|
|
23
|
+
let curPhase = 'validating'; // 跟踪当前环节,供 catch 兜底时标注正确 phase
|
|
24
|
+
try {
|
|
25
|
+
// onPhase 把 agentCreateNonInteractive 内部环节(0-3、5)映射到进度文件
|
|
26
|
+
const res = await agentCreateNonInteractive({
|
|
27
|
+
aid: opts.aid, name: opts.name, baseagent: opts.baseagent,
|
|
28
|
+
project: opts.project, owner: opts.owner,
|
|
29
|
+
onPhase: (phase, state, detail) => {
|
|
30
|
+
if (state === 'begin') {
|
|
31
|
+
curPhase = phase;
|
|
32
|
+
w.begin(phase);
|
|
33
|
+
}
|
|
34
|
+
else if (state === 'done')
|
|
35
|
+
w.done(phase, detail);
|
|
36
|
+
else if (state === 'warn')
|
|
37
|
+
w.warn(phase, detail);
|
|
38
|
+
else if (state === 'failed')
|
|
39
|
+
w.finishFailed(phase, detail ?? 'failed');
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (!('ok' in res) || res.ok !== true) {
|
|
43
|
+
// 硬失败:onPhase('failed') 已写终态;这里仅兜底日志(防回调未覆盖的 return 路径)
|
|
44
|
+
const err = res.error;
|
|
45
|
+
logger.warn(`[agent-control] create ${opts.aid} failed: ${err}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// 环节 4:applying_config(model/chatmode,agentCreateNonInteractive 之外)
|
|
49
|
+
if (opts.model || opts.chatmode) {
|
|
50
|
+
curPhase = 'applying_config';
|
|
51
|
+
w.begin('applying_config');
|
|
52
|
+
let warned;
|
|
53
|
+
if (opts.model) {
|
|
54
|
+
const r = await agentSet(opts.aid, 'models.default', opts.model);
|
|
55
|
+
if (!('ok' in r) || !r.ok)
|
|
56
|
+
warned = `model: ${r.error}`;
|
|
57
|
+
}
|
|
58
|
+
if (opts.chatmode) {
|
|
59
|
+
const r = await agentSet(opts.aid, 'chatmode', JSON.stringify(opts.chatmode));
|
|
60
|
+
if (!('ok' in r) || !r.ok)
|
|
61
|
+
warned = `${warned ? warned + '; ' : ''}chatmode: ${r.error}`;
|
|
62
|
+
}
|
|
63
|
+
if (warned) {
|
|
64
|
+
logger.warn(`[agent-control] applying_config ${opts.aid}: ${warned}`);
|
|
65
|
+
w.warn('applying_config', warned);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
w.done('applying_config');
|
|
69
|
+
}
|
|
70
|
+
w.finishReady();
|
|
71
|
+
logger.info(`[agent-control] create ${opts.aid} ready`);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const msg = e?.message || String(e);
|
|
75
|
+
logger.warn(`[agent-control] create ${opts.aid} threw at ${curPhase}: ${msg}`);
|
|
76
|
+
w.finishFailed(curPhase, msg); // 兜底终态,标注真实失败环节
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** name=agent 的 menu.action 执行。peerId 自动填为新 agent 的 owner。
|
|
80
|
+
* create 受理即返回(D3);delete/enable/disable 同步等结果。
|
|
81
|
+
* 调用方负责传入已兜底的 args.project(见 command-handler 装配)。 */
|
|
82
|
+
export async function execAgentAction(action, args, peerId) {
|
|
83
|
+
const a = args ?? {};
|
|
84
|
+
if (action === 'create') {
|
|
85
|
+
if (!peerId)
|
|
86
|
+
return { error: '缺少发起者 AID(无法绑定 owner)', code: 'INVALID_ARGS' };
|
|
87
|
+
if (!a.aid || !a.name || !a.baseagent) {
|
|
88
|
+
return { error: '缺少必填参数:aid / name / baseagent', code: 'INVALID_ARGS' };
|
|
89
|
+
}
|
|
90
|
+
if (!a.project || typeof a.project !== 'string') {
|
|
91
|
+
return { error: 'project 缺失且无法兜底(需 defaults.projects.rootPath/defaultPath)', code: 'INVALID_ARGS' };
|
|
92
|
+
}
|
|
93
|
+
// D3: 受理即返回,重副作用转后台
|
|
94
|
+
// 受理即返回;后台 promise fire-and-forget。runCreateInBackground 内部有 try/catch,
|
|
95
|
+
// 但 CreateStatusWriter 构造(mkdir/写盘)在 try 之前,故再加一层兜底防未处理拒绝。
|
|
96
|
+
void runCreateInBackground({
|
|
97
|
+
aid: a.aid, name: a.name, baseagent: a.baseagent,
|
|
98
|
+
project: a.project, owner: peerId,
|
|
99
|
+
model: a.model, chatmode: a.chatmode,
|
|
100
|
+
}).catch(e => logger.error(`[agent-control] runCreateInBackground unhandled ${a.aid}: ${e?.message || e}`));
|
|
101
|
+
return { data: { accepted: true, aid: a.aid } };
|
|
102
|
+
}
|
|
103
|
+
if (action === 'delete') {
|
|
104
|
+
if (!a.aid)
|
|
105
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
106
|
+
const res = await agentDelete(a.aid, false);
|
|
107
|
+
if (!('ok' in res) || res.ok !== true)
|
|
108
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
109
|
+
return { data: { aid: res.aid, purged: res.purged } };
|
|
110
|
+
}
|
|
111
|
+
if (action === 'enable' || action === 'disable') {
|
|
112
|
+
if (!a.aid)
|
|
113
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
114
|
+
const res = action === 'enable' ? await agentEnable(a.aid) : await agentDisable(a.aid);
|
|
115
|
+
if (!('ok' in res) || res.ok !== true)
|
|
116
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
117
|
+
return { data: { aid: res.aid, enabled: res.enabled, reloaded: res.reloaded } };
|
|
118
|
+
}
|
|
119
|
+
return { error: `不支持的 action: ${action}`, code: 'INVALID_ARGS' };
|
|
120
|
+
}
|
|
121
|
+
/** project 兜底:显式值 > rootPath 合成 > defaultPath > undefined */
|
|
122
|
+
export function resolveProjectPath(explicit, aid, defaults) {
|
|
123
|
+
if (explicit && explicit.trim())
|
|
124
|
+
return explicit;
|
|
125
|
+
const root = defaults?.projects?.rootPath;
|
|
126
|
+
if (root)
|
|
127
|
+
return path.join(root, aid.split('.')[0]);
|
|
128
|
+
return defaults?.projects?.defaultPath;
|
|
129
|
+
}
|
|
130
|
+
/** name=agent 的 menu.query:查单个 agent 详情,附构建进度(D3)。 */
|
|
131
|
+
export async function execAgentQuery(args) {
|
|
132
|
+
const aid = args?.aid;
|
|
133
|
+
if (!aid)
|
|
134
|
+
return { error: '缺少 aid', code: 'INVALID_ARGS' };
|
|
135
|
+
const res = await agentShow(aid);
|
|
136
|
+
if (!('ok' in res) || res.ok !== true)
|
|
137
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
138
|
+
// 叠加构建进度(create 受理后、ready 前可见;ready 后文件仍在,可反映软失败 warn)
|
|
139
|
+
const agentDir = path.join(resolvePaths().agentsDir, aid);
|
|
140
|
+
const progress = readCreateStatus(agentDir);
|
|
141
|
+
return { data: progress ? { ...res, createProgress: progress } : res };
|
|
142
|
+
}
|
|
143
|
+
/** name=agent 的 menu.options:列出 agent(enabled 默认 / all) */
|
|
144
|
+
export async function execAgentOptions(args) {
|
|
145
|
+
const scope = args?.options === 'all' ? 'all' : 'enabled';
|
|
146
|
+
const res = await agentList();
|
|
147
|
+
if (!('ok' in res) || res.ok !== true)
|
|
148
|
+
return { error: res.error, code: classifyError(res.error) };
|
|
149
|
+
const agents = scope === 'all'
|
|
150
|
+
? res.agents
|
|
151
|
+
: res.agents.filter((x) => x.status !== 'disabled');
|
|
152
|
+
return { data: { agents, scope } };
|
|
153
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const FILE = 'create-status.json';
|
|
4
|
+
export function readCreateStatus(agentDir) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = fs.readFileSync(path.join(agentDir, FILE), 'utf-8');
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** 删除构建进度文件(agent 删除时清理)。非 purge 删除只移除 config.json,
|
|
14
|
+
* 故需显式清理本文件,避免目录残留陈旧进度。 */
|
|
15
|
+
export function removeCreateStatus(agentDir) {
|
|
16
|
+
try {
|
|
17
|
+
fs.rmSync(path.join(agentDir, FILE), { force: true });
|
|
18
|
+
}
|
|
19
|
+
catch { /* ignore */ }
|
|
20
|
+
}
|
|
21
|
+
/** 构建进度写入器。每次状态变更原子落盘(写临时文件 + rename)。 */
|
|
22
|
+
export class CreateStatusWriter {
|
|
23
|
+
agentDir;
|
|
24
|
+
status;
|
|
25
|
+
constructor(agentDir, aid) {
|
|
26
|
+
this.agentDir = agentDir;
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
this.status = { aid, status: 'in_progress', currentPhase: null, steps: [], error: null, startedAt: now, updatedAt: now };
|
|
29
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
30
|
+
this.flush();
|
|
31
|
+
}
|
|
32
|
+
begin(phase) {
|
|
33
|
+
this.status.currentPhase = phase;
|
|
34
|
+
this.status.steps.push({ phase, state: 'in_progress', ts: Date.now() });
|
|
35
|
+
this.flush();
|
|
36
|
+
}
|
|
37
|
+
done(phase, detail) { this.mark(phase, 'done', detail); }
|
|
38
|
+
warn(phase, detail) { this.mark(phase, 'warn', detail); }
|
|
39
|
+
finishReady() { this.status.status = 'ready'; this.status.currentPhase = null; this.flush(); }
|
|
40
|
+
finishFailed(phase, error) {
|
|
41
|
+
this.mark(phase, 'failed', error);
|
|
42
|
+
this.status.status = 'failed';
|
|
43
|
+
this.status.error = error;
|
|
44
|
+
this.status.currentPhase = null;
|
|
45
|
+
this.flush();
|
|
46
|
+
}
|
|
47
|
+
mark(phase, state, detail) {
|
|
48
|
+
const step = [...this.status.steps].reverse().find(s => s.phase === phase);
|
|
49
|
+
if (step) {
|
|
50
|
+
step.state = state;
|
|
51
|
+
if (detail)
|
|
52
|
+
step.detail = detail;
|
|
53
|
+
step.ts = Date.now();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.status.steps.push({ phase, state, detail, ts: Date.now() });
|
|
57
|
+
}
|
|
58
|
+
this.flush();
|
|
59
|
+
}
|
|
60
|
+
flush() {
|
|
61
|
+
this.status.updatedAt = Date.now();
|
|
62
|
+
const file = path.join(this.agentDir, FILE);
|
|
63
|
+
const tmp = `${file}.tmp`;
|
|
64
|
+
fs.writeFileSync(tmp, JSON.stringify(this.status, null, 2));
|
|
65
|
+
fs.renameSync(tmp, file);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -151,7 +151,7 @@ export class MessageBridge {
|
|
|
151
151
|
sameNetwork: msg.sameNetwork,
|
|
152
152
|
sameEgressIp: msg.sameEgressIp,
|
|
153
153
|
messageId: msg.messageId,
|
|
154
|
-
mentions: msg.mentions, threadId: msg.threadId,
|
|
154
|
+
mentions: msg.mentions, mentionAids: msg.mentionAids, threadId: msg.threadId,
|
|
155
155
|
replyContext: msg.replyContext,
|
|
156
156
|
};
|
|
157
157
|
// 5.5 写入消息记录(入方向)
|
|
@@ -218,6 +218,8 @@ export class MessageBridge {
|
|
|
218
218
|
activity: '/activity',
|
|
219
219
|
system: '/system',
|
|
220
220
|
cli: '/cli',
|
|
221
|
+
agent: '/agent',
|
|
222
|
+
trigger: '/trigger',
|
|
221
223
|
};
|
|
222
224
|
resolveCmd(name, cmd) {
|
|
223
225
|
if (cmd)
|
|
@@ -276,7 +278,7 @@ export class MessageBridge {
|
|
|
276
278
|
const { id, name, cmd } = req;
|
|
277
279
|
try {
|
|
278
280
|
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
279
|
-
const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId);
|
|
281
|
+
const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId, req.args);
|
|
280
282
|
if ('error' in result)
|
|
281
283
|
throw { code: result.code || 'EXEC_FAILED', message: result.error };
|
|
282
284
|
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
|
|
@@ -292,7 +294,7 @@ export class MessageBridge {
|
|
|
292
294
|
const { id, name, cmd } = req;
|
|
293
295
|
try {
|
|
294
296
|
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
295
|
-
const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId) ?? [];
|
|
297
|
+
const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId, req.args) ?? [];
|
|
296
298
|
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data }, sendReply);
|
|
297
299
|
}
|
|
298
300
|
catch (err) {
|
|
@@ -295,16 +295,9 @@ export class MessageProcessor {
|
|
|
295
295
|
const streamKey = session.id;
|
|
296
296
|
const chatType = message.chatType || 'private';
|
|
297
297
|
const identityRole = session.identity?.role || 'anonymous';
|
|
298
|
-
const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
299
|
-
// Resolve agent context from registry (Phase 2 foundation)
|
|
300
|
-
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
301
|
-
if (agentContext) {
|
|
302
|
-
logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
|
|
303
|
-
}
|
|
304
|
-
// 按 session.agentId 选择 agent 后端
|
|
305
|
-
const agent = this.getAgent(channelKey, session.agentId);
|
|
306
298
|
const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
|
|
307
|
-
|
|
299
|
+
// 按 session.agentId 选择 agent 后端(idle-kill 路径需要 interrupt)
|
|
300
|
+
const agent = this.getAgent(channelKey, session.agentId);
|
|
308
301
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
309
302
|
const shouldSuppress = () => {
|
|
310
303
|
return !policy.showMiddleResult(chatType, identityRole);
|
|
@@ -313,6 +306,7 @@ export class MessageProcessor {
|
|
|
313
306
|
let monitor;
|
|
314
307
|
let monitorInterval;
|
|
315
308
|
let rejectFn;
|
|
309
|
+
let lastIdleSec = 0;
|
|
316
310
|
const resetTimer = (eventType, toolName) => {
|
|
317
311
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
318
312
|
};
|
|
@@ -329,17 +323,9 @@ export class MessageProcessor {
|
|
|
329
323
|
let result = monitor.check();
|
|
330
324
|
while (result) {
|
|
331
325
|
if (result.action === 'kill') {
|
|
326
|
+
lastIdleSec = result.idleSec;
|
|
332
327
|
logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
333
328
|
this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
|
|
334
|
-
// 后台任务也需要中断(释放资源),但不发送通知
|
|
335
|
-
if (channelInfo && !isBackground) {
|
|
336
|
-
const msg = showIdleMonitor
|
|
337
|
-
? result.message
|
|
338
|
-
: `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
|
|
339
|
-
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: msg, subtype: 'health' }).catch(e => {
|
|
340
|
-
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
329
|
logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
|
|
344
330
|
agent.interrupt(streamKey).catch(e => {
|
|
345
331
|
logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
|
|
@@ -348,15 +334,16 @@ export class MessageProcessor {
|
|
|
348
334
|
return;
|
|
349
335
|
}
|
|
350
336
|
else {
|
|
351
|
-
// notify or warn:
|
|
337
|
+
// notify or warn: publish event, task continues
|
|
352
338
|
logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
339
|
+
this.eventBus.publish({
|
|
340
|
+
type: result.action === 'notify' ? 'runner:idle-notify' : 'runner:idle-warn',
|
|
341
|
+
sessionId: streamKey,
|
|
342
|
+
idleSec: result.idleSec,
|
|
343
|
+
totalEvents: result.state.totalEvents,
|
|
344
|
+
totalToolCalls: result.state.totalToolCalls,
|
|
345
|
+
lastToolName: result.state.lastToolName,
|
|
346
|
+
});
|
|
360
347
|
}
|
|
361
348
|
result = monitor.check();
|
|
362
349
|
}
|
|
@@ -364,7 +351,7 @@ export class MessageProcessor {
|
|
|
364
351
|
});
|
|
365
352
|
try {
|
|
366
353
|
await Promise.race([
|
|
367
|
-
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress),
|
|
354
|
+
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec),
|
|
368
355
|
timeoutPromise
|
|
369
356
|
]);
|
|
370
357
|
}
|
|
@@ -412,7 +399,7 @@ export class MessageProcessor {
|
|
|
412
399
|
return message.replyContext;
|
|
413
400
|
}
|
|
414
401
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
415
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
|
|
402
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec) {
|
|
416
403
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
417
404
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
418
405
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -684,6 +671,11 @@ export class MessageProcessor {
|
|
|
684
671
|
sameNetwork: message.sameNetwork ?? false,
|
|
685
672
|
sameEgressIp: message.sameEgressIp ?? false,
|
|
686
673
|
groupId: session.metadata?.groupId || undefined,
|
|
674
|
+
groupName: session.metadata?.groupName || undefined,
|
|
675
|
+
// 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
|
|
676
|
+
groupLabel: session.metadata?.groupId
|
|
677
|
+
? (session.metadata?.groupName ? `${session.metadata.groupName}<${session.metadata.groupId}>` : session.metadata.groupId)
|
|
678
|
+
: undefined,
|
|
687
679
|
chatType: session.chatType || null,
|
|
688
680
|
channel: currentChannelType || null,
|
|
689
681
|
venueUid: undefined,
|
|
@@ -735,6 +727,7 @@ export class MessageProcessor {
|
|
|
735
727
|
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
736
728
|
content: message.content, timestamp: message.timestamp,
|
|
737
729
|
images: message.images,
|
|
730
|
+
mentionAids: message.mentionAids,
|
|
738
731
|
}];
|
|
739
732
|
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
740
733
|
if (renderResult.body.trim())
|
|
@@ -1100,7 +1093,7 @@ export class MessageProcessor {
|
|
|
1100
1093
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
1101
1094
|
if (!isUserInterrupt) {
|
|
1102
1095
|
const statusPayload = procStatus === 'timeout'
|
|
1103
|
-
? { kind: 'status.timeout' }
|
|
1096
|
+
? { kind: 'status.timeout', metadata: { idleSec: getLastIdleSec?.() || undefined } }
|
|
1104
1097
|
: procStatus === 'interrupted'
|
|
1105
1098
|
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
1106
1099
|
: { kind: 'status.error' };
|
|
@@ -1133,20 +1126,22 @@ export class MessageProcessor {
|
|
|
1133
1126
|
if (error instanceof Error && !isUserInterrupt) {
|
|
1134
1127
|
logger.error(`[${message.channel}] Error stack:`, error.stack);
|
|
1135
1128
|
}
|
|
1136
|
-
//
|
|
1129
|
+
// 发送用户友好的错误消息
|
|
1137
1130
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
|
|
1138
1131
|
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
else if (isUserInterrupt) {
|
|
1132
|
+
const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
|
|
1133
|
+
if (isUserInterrupt) {
|
|
1143
1134
|
logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
|
|
1144
1135
|
}
|
|
1145
1136
|
else if (error?._errorAlreadySent) {
|
|
1146
1137
|
logger.info(`[MessageProcessor] Error already sent via renderer, skip sending duplicate message`);
|
|
1147
1138
|
}
|
|
1148
1139
|
else {
|
|
1149
|
-
|
|
1140
|
+
// SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
|
|
1141
|
+
const idleSec = getLastIdleSec?.() || 0;
|
|
1142
|
+
const userMessage = isTimeout
|
|
1143
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
1144
|
+
: getErrorMessage(error, undefined);
|
|
1150
1145
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
1151
1146
|
let sendOpts;
|
|
1152
1147
|
try {
|
|
@@ -1159,7 +1154,10 @@ export class MessageProcessor {
|
|
|
1159
1154
|
...(sendOpts ?? {}),
|
|
1160
1155
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
1161
1156
|
};
|
|
1162
|
-
|
|
1157
|
+
const errorPayload = isTimeout
|
|
1158
|
+
? { kind: 'result.error', text: userMessage, reason: 'timeout' }
|
|
1159
|
+
: { kind: 'result.text', text: userMessage, isFinal: true };
|
|
1160
|
+
await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
|
|
1163
1161
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
1164
1162
|
}
|
|
1165
1163
|
}
|
|
@@ -1196,6 +1194,16 @@ export class MessageProcessor {
|
|
|
1196
1194
|
session.sessionMode = 'proactive';
|
|
1197
1195
|
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
1198
1196
|
}
|
|
1197
|
+
// 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
|
|
1198
|
+
// 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
|
|
1199
|
+
if (message.chatType === 'group' && session.metadata?.groupId && !session.metadata.groupName) {
|
|
1200
|
+
const adapter = this.resolveChannelInfo(message.channel)?.adapter;
|
|
1201
|
+
const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
|
|
1202
|
+
if (groupName) {
|
|
1203
|
+
session.metadata.groupName = groupName;
|
|
1204
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1199
1207
|
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
1200
1208
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
1201
1209
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
@@ -40,16 +40,22 @@ export class MessageQueue {
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* 检查消息是否应该处理(去重)
|
|
43
|
+
*
|
|
44
|
+
* 去重 key = `${sessionKey}:${messageId}`,而非裸 messageId。
|
|
45
|
+
* MessageQueue 是进程级单例,被所有 evolagent 共享。AUN 群广播时同一条群消息
|
|
46
|
+
* 会投递给群里每个 evolagent,它们 messageId 相同但 session 不同,必须各处理一次。
|
|
47
|
+
* 裸 messageId 去重会让先入队的 agent 吞掉其他 agent 的消息。
|
|
43
48
|
*/
|
|
44
|
-
shouldProcess(message) {
|
|
49
|
+
shouldProcess(sessionKey, message) {
|
|
45
50
|
if (!message.messageId)
|
|
46
51
|
return true; // 无 ID 的消息不去重
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
const dedupKey = `${sessionKey}:${message.messageId}`;
|
|
53
|
+
if (this.recentMessageIds.has(dedupKey)) {
|
|
54
|
+
logger.debug(`[Queue] Duplicate message ${dedupKey}, skipping`);
|
|
49
55
|
return false;
|
|
50
56
|
}
|
|
51
|
-
this.recentMessageIds.add(
|
|
52
|
-
setTimeout(() => this.recentMessageIds.delete(
|
|
57
|
+
this.recentMessageIds.add(dedupKey);
|
|
58
|
+
setTimeout(() => this.recentMessageIds.delete(dedupKey), this.DEDUP_WINDOW);
|
|
53
59
|
return true;
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
@@ -67,7 +73,7 @@ export class MessageQueue {
|
|
|
67
73
|
}
|
|
68
74
|
async enqueue(sessionKey, message, projectPath, options) {
|
|
69
75
|
// 消息去重检查
|
|
70
|
-
if (!this.shouldProcess(message)) {
|
|
76
|
+
if (!this.shouldProcess(sessionKey, message)) {
|
|
71
77
|
return Promise.resolve();
|
|
72
78
|
}
|
|
73
79
|
// 拦截器检查:AskUserQuestion 等场景的一次性消息拦截
|
|
@@ -212,6 +218,7 @@ export class MessageQueue {
|
|
|
212
218
|
sameDevice: m.sameDevice, sameNetwork: m.sameNetwork, sameEgressIp: m.sameEgressIp,
|
|
213
219
|
content: m.content, timestamp: m.timestamp,
|
|
214
220
|
images: m.images && m.images.length > 0 ? m.images : undefined,
|
|
221
|
+
mentionAids: m.mentionAids && m.mentionAids.length > 0 ? m.mentionAids : undefined,
|
|
215
222
|
});
|
|
216
223
|
}
|
|
217
224
|
}
|
|
@@ -16,9 +16,42 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
|
-
import { agentRelationsDir } from '../../paths.js';
|
|
19
|
+
import { agentRelationsDir, agentConfig as agentConfigPath, resolvePaths } from '../../paths.js';
|
|
20
20
|
import { loadDefaults, saveDefaultsSafe, loadAgent, saveAgent } from '../../config-store.js';
|
|
21
21
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-key.js';
|
|
22
|
+
import { fileCache } from '../cache/file-cache.js';
|
|
23
|
+
// ── mtime 门控缓存(统一走 FileCache)─────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// resolveEffectiveModel 每条消息按 关系>agent>全局 解析,原本每次都
|
|
26
|
+
// loadAgent()/loadDefaults()/读 preferences.json —— 一条消息最多读盘 5 次。
|
|
27
|
+
//
|
|
28
|
+
// model-scope 被 CLI 子进程与 daemon 共用,CLI 改文件后 daemon 无失效通知,
|
|
29
|
+
// 故靠 mtime 门控:每次只 statSync 比对 mtime,未变用缓存,变了才真正重读 +
|
|
30
|
+
// 重解析。跨进程天然正确(文件 mtime 变即感知),改配置即时生效不变;
|
|
31
|
+
// statSync 远比 read+JSON.parse 便宜。CLI 进程的 fileCache 是独立空实例、随进程
|
|
32
|
+
// 退出,等同直读最新盘值,安全。
|
|
33
|
+
//
|
|
34
|
+
// config/defaults 与 relation-prefs 三者统一走 daemon 单例 FileCache(消除原先
|
|
35
|
+
// 第二套 makeMtimeCache),读取计数一并进监控。config/defaults 的实际读盘 + 解析
|
|
36
|
+
// 仍委托原 loadAgent/loadDefaults(保留 atomicRead 崩溃恢复 + expandEnvRefs/校验),
|
|
37
|
+
// 故传 noopRead 让 FileCache 只做 mtime 门控、不重复读盘(loader 忽略 raw)。
|
|
38
|
+
/** FileCache 的 read 钩子占位:config/defaults 的真实读盘在 loader 里(loadAgent/
|
|
39
|
+
* loadDefaults,含崩溃恢复),此处返回 null 避免 FileCache 再读一遍。 */
|
|
40
|
+
const noopRead = () => null;
|
|
41
|
+
// agent config.json:mtime 门控,per-agent 分组(config:<aid>)便于 per-agent 监控视图。
|
|
42
|
+
const loadAgentCached = (self) => fileCache.get(agentConfigPath(self), () => loadAgent(self), { policy: 'mtime', group: `config:${self}`, read: noopRead });
|
|
43
|
+
// defaults.json:单文件,mtime 门控。
|
|
44
|
+
const loadDefaultsCached = () => fileCache.get(resolvePaths().defaultsConfig, () => loadDefaults(), { policy: 'mtime', group: 'config', read: noopRead });
|
|
45
|
+
// 关系级 preferences.json —— 走统一 FileCache(mtime 门控,带外改 + 不 reload)。
|
|
46
|
+
const readPrefsCached = (file) => fileCache.get(file, (raw) => (raw === null ? null : safeParsePrefs(raw)), { policy: 'mtime', group: 'relation-prefs' });
|
|
47
|
+
function safeParsePrefs(raw) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
22
55
|
export class ModelScopeError extends Error {
|
|
23
56
|
code;
|
|
24
57
|
constructor(code, message) {
|
|
@@ -76,11 +109,11 @@ export function determineScope(sel) {
|
|
|
76
109
|
export function activeBaseagent(self) {
|
|
77
110
|
try {
|
|
78
111
|
if (self) {
|
|
79
|
-
const cfg =
|
|
112
|
+
const cfg = loadAgentCached(self);
|
|
80
113
|
if (cfg?.active_baseagent)
|
|
81
114
|
return cfg.active_baseagent;
|
|
82
115
|
}
|
|
83
|
-
const d =
|
|
116
|
+
const d = loadDefaultsCached();
|
|
84
117
|
if (d?.active_baseagent)
|
|
85
118
|
return d.active_baseagent;
|
|
86
119
|
}
|
|
@@ -114,17 +147,17 @@ function writeJsonAtomic(file, data) {
|
|
|
114
147
|
export function readScope(scope, sel, ba) {
|
|
115
148
|
switch (scope) {
|
|
116
149
|
case 'global': {
|
|
117
|
-
const block = (
|
|
150
|
+
const block = (loadDefaultsCached()?.baseagents || {});
|
|
118
151
|
const c = block[ba] || {};
|
|
119
152
|
return { model: c.model, effort: c[effortField(ba)] };
|
|
120
153
|
}
|
|
121
154
|
case 'agent': {
|
|
122
|
-
const cfg = sel.self ?
|
|
155
|
+
const cfg = sel.self ? loadAgentCached(sel.self) : null;
|
|
123
156
|
const c = (cfg?.baseagents || {})[ba] || {};
|
|
124
157
|
return { model: c.model, effort: c[effortField(ba)] };
|
|
125
158
|
}
|
|
126
159
|
case 'relation': {
|
|
127
|
-
const p =
|
|
160
|
+
const p = readPrefsCached(relationPrefsPath(sel.self, sel.peerKey));
|
|
128
161
|
return { model: p?.model, effort: p?.effort };
|
|
129
162
|
}
|
|
130
163
|
}
|
|
@@ -95,24 +95,67 @@ export class ClaudeSessionFileAdapter {
|
|
|
95
95
|
}
|
|
96
96
|
return null;
|
|
97
97
|
}
|
|
98
|
+
// CLI 会话白名单:只有真正由人手发起的终端会话才值得导入。
|
|
99
|
+
// 其它 entrypoint(sdk-ts=EvolClaw 自身、sdk-py=security-guidance 等插件的后台 SDK 会话)
|
|
100
|
+
// 同样把 JSONL 写进项目目录,但不是用户会话,导入它们没有意义。
|
|
101
|
+
static CLI_ENTRYPOINTS = new Set(['cli', 'sdk-cli']);
|
|
102
|
+
// entrypoint 不可变,进程内缓存 filePath→entrypoint,避免重复读文件头部
|
|
103
|
+
entrypointCache = new Map();
|
|
104
|
+
// entrypoint 字段位于首个 user 事件行(实测恒在前 4 行内,cli 会话首次出现 < 8KB)。
|
|
105
|
+
// 插件会话首行 queue-operation 可能携带巨大 diff(数百 KB),把 entrypoint 推到很后面,
|
|
106
|
+
// 因此读取上限设为 32KB:cli 会话必命中,插件会话要么命中 sdk-py、要么读不到 → 一律排除。
|
|
107
|
+
static ENTRYPOINT_SCAN_BYTES = 32 * 1024;
|
|
108
|
+
/** 读取会话文件的 entrypoint,结果缓存在实例内(entrypoint 不可变,无需失效)。 */
|
|
109
|
+
readEntrypoint(filePath) {
|
|
110
|
+
if (this.entrypointCache.has(filePath))
|
|
111
|
+
return this.entrypointCache.get(filePath);
|
|
112
|
+
let fd;
|
|
113
|
+
let result = null;
|
|
114
|
+
try {
|
|
115
|
+
fd = fs.openSync(filePath, 'r');
|
|
116
|
+
const buf = Buffer.allocUnsafe(ClaudeSessionFileAdapter.ENTRYPOINT_SCAN_BYTES);
|
|
117
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
118
|
+
const head = buf.toString('utf-8', 0, bytesRead);
|
|
119
|
+
const m = head.match(/"entrypoint":"([^"]*)"/);
|
|
120
|
+
result = m ? m[1] : null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// keep null
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (fd !== undefined)
|
|
127
|
+
fs.closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
this.entrypointCache.set(filePath, result);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
98
132
|
scanCliSessions(projectPath) {
|
|
99
133
|
const homeDir = os.homedir();
|
|
100
134
|
const encodedPath = encodePath(projectPath);
|
|
101
135
|
const sessionDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
102
136
|
if (!fs.existsSync(sessionDir))
|
|
103
137
|
return [];
|
|
104
|
-
const
|
|
138
|
+
const candidates = fs.readdirSync(sessionDir)
|
|
105
139
|
.filter(f => f.endsWith('.jsonl'))
|
|
106
140
|
.filter(f => !f.startsWith('agent-'))
|
|
107
141
|
.map(f => {
|
|
108
142
|
const filePath = path.join(sessionDir, f);
|
|
109
143
|
const stat = fs.statSync(filePath);
|
|
110
|
-
return { uuid: f.replace('.jsonl', ''), mtime: stat.mtimeMs, size: stat.size };
|
|
144
|
+
return { uuid: f.replace('.jsonl', ''), filePath, mtime: stat.mtimeMs, size: stat.size };
|
|
111
145
|
})
|
|
112
146
|
.filter(f => f.size > 0)
|
|
113
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
148
|
+
// 按 mtime 降序惰性判定 entrypoint,凑够 10 个白名单会话即停,避免读取全部文件。
|
|
149
|
+
const result = [];
|
|
150
|
+
for (const f of candidates) {
|
|
151
|
+
const entrypoint = this.readEntrypoint(f.filePath);
|
|
152
|
+
if (entrypoint && ClaudeSessionFileAdapter.CLI_ENTRYPOINTS.has(entrypoint)) {
|
|
153
|
+
result.push({ uuid: f.uuid, mtime: f.mtime });
|
|
154
|
+
if (result.length >= 10)
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
116
159
|
}
|
|
117
160
|
async listSdkSessions(projectPath) {
|
|
118
161
|
try {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { resolvePaths } from './paths.js';
|
|
2
|
+
import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
|
|
3
|
+
/** 读 {root}/evolclaw.json。文件不存在返回 {},不报错。 */
|
|
4
|
+
export function loadEvolclawConfig() {
|
|
5
|
+
const raw = atomicReadJson(resolvePaths().evolclawJson);
|
|
6
|
+
return raw ?? {};
|
|
7
|
+
}
|
|
8
|
+
/** 原子写入 {root}/evolclaw.json。调用方负责传完整对象(含要保留的字段)。 */
|
|
9
|
+
export function saveEvolclawConfig(value) {
|
|
10
|
+
atomicWriteJson(resolvePaths().evolclawJson, value);
|
|
11
|
+
}
|