evolclaw 3.1.0 → 3.1.2
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 +407 -0
- package/README.md +1 -1
- package/SKILLS.md +311 -0
- package/dist/agents/claude-runner.js +40 -3
- package/dist/aun/aid/agentmd.js +7 -6
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +51 -0
- package/dist/aun/msg/upload.js +57 -18
- package/dist/channels/aun.js +124 -50
- package/dist/channels/dingtalk.js +2 -0
- package/dist/channels/feishu.js +15 -6
- package/dist/channels/qqbot.js +2 -0
- package/dist/channels/wechat.js +2 -0
- package/dist/channels/wecom.js +2 -0
- package/dist/cli/agent.js +130 -35
- package/dist/cli/index.js +221 -48
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +44 -23
- package/dist/cli/watch-msg.js +109 -30
- package/dist/config-store.js +67 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +95 -84
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +4 -4
- package/dist/core/message/im-renderer.js +47 -8
- package/dist/core/message/message-bridge.js +30 -1
- package/dist/core/message/message-log.js +6 -1
- package/dist/core/message/message-processor.js +29 -35
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +11 -4
- package/dist/core/trigger/manager.js +16 -0
- package/dist/core/trigger/parser.js +110 -0
- package/dist/core/trigger/scheduler.js +6 -0
- package/dist/index.js +64 -20
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/dist/utils/error-utils.js +17 -13
- package/dist/utils/stats.js +216 -2
- package/kits/docs/INDEX.md +6 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
- package/kits/rules/06-channel.md +30 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { ensureDataDirs, resolvePaths, getPackageRoot } from './paths.js';
|
|
4
|
+
import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './paths.js';
|
|
5
5
|
import { resolveAnthropicConfig } from './agents/resolve.js';
|
|
6
6
|
import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded } from './config-store.js';
|
|
7
7
|
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
@@ -41,7 +41,6 @@ import { agentTriggersDir } from './paths.js';
|
|
|
41
41
|
import { isLinkedInstall } from './utils/npm-ops.js';
|
|
42
42
|
import path from 'path';
|
|
43
43
|
import fs from 'fs';
|
|
44
|
-
import os from 'os';
|
|
45
44
|
import crypto from 'crypto';
|
|
46
45
|
import { fileURLToPath } from 'url';
|
|
47
46
|
/** 出站 payload 摘要(用于 channel-out.log) */
|
|
@@ -124,6 +123,46 @@ function readFastaunVersion() {
|
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
125
|
async function main() {
|
|
126
|
+
// 启动信息:目录类型 + 版本号 + 代码最新时间戳
|
|
127
|
+
{
|
|
128
|
+
const pkgRoot = getPackageRoot();
|
|
129
|
+
const runDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'));
|
|
130
|
+
const isDist = runDir.includes(path.join(pkgRoot, 'dist'));
|
|
131
|
+
const isLinked = fs.existsSync(path.join(pkgRoot, '.git'));
|
|
132
|
+
const dirType = isDist ? (isLinked ? '开发仓/dist' : '安装路径/dist') : '源码(tsx)';
|
|
133
|
+
const scanDir = isDist ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
|
|
134
|
+
let latestMtime = 0;
|
|
135
|
+
const scanRecursive = (dir) => {
|
|
136
|
+
try {
|
|
137
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
138
|
+
if (entry.name === 'node_modules')
|
|
139
|
+
continue;
|
|
140
|
+
const full = path.join(dir, entry.name);
|
|
141
|
+
if (entry.isDirectory()) {
|
|
142
|
+
scanRecursive(full);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
146
|
+
const mt = fs.statSync(full).mtimeMs;
|
|
147
|
+
if (mt > latestMtime)
|
|
148
|
+
latestMtime = mt;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
};
|
|
154
|
+
scanRecursive(scanDir);
|
|
155
|
+
let version = '?';
|
|
156
|
+
try {
|
|
157
|
+
version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
const fmtTime = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; };
|
|
161
|
+
console.error(`[EvolClaw] EvolClaw v${version}`);
|
|
162
|
+
console.error(`[EvolClaw] 执行类型: ${dirType}`);
|
|
163
|
+
console.error(`[EvolClaw] 包路径: ${pkgRoot}`);
|
|
164
|
+
console.error(`[EvolClaw] 代码时间: ${latestMtime ? fmtTime(latestMtime) : '?'}`);
|
|
165
|
+
}
|
|
127
166
|
// 过滤飞书 SDK 的 info 日志
|
|
128
167
|
const originalLog = console.log;
|
|
129
168
|
const originalInfo = console.info;
|
|
@@ -261,16 +300,22 @@ async function main() {
|
|
|
261
300
|
// 统计收集器(近 1 小时滚动统计)
|
|
262
301
|
const statsCollector = new StatsCollector(eventBus);
|
|
263
302
|
// Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
|
|
264
|
-
const aidStatsCollector = new AidStatsCollector();
|
|
303
|
+
const aidStatsCollector = new AidStatsCollector(eventBus);
|
|
304
|
+
aidStatsCollector.setSessionsDir(paths.sessionsDir);
|
|
265
305
|
// 初始化 SessionManager(文件系统后端)
|
|
266
306
|
const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
|
|
267
307
|
// sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
|
|
268
|
-
sessionManager.setSessionModeResolver((channelKey, chatType) => {
|
|
308
|
+
sessionManager.setSessionModeResolver((channelKey, chatType, peerType) => {
|
|
269
309
|
const agent = agentRegistry.resolveByChannel(channelKey);
|
|
270
310
|
const cm = agent?.config.chatmode;
|
|
271
311
|
if (!cm)
|
|
272
312
|
return undefined;
|
|
273
|
-
|
|
313
|
+
// 优先级:群聊 > nothuman > private
|
|
314
|
+
if (chatType === 'group')
|
|
315
|
+
return cm.group;
|
|
316
|
+
if (peerType && peerType !== 'human' && peerType !== 'unknown')
|
|
317
|
+
return cm.nothuman;
|
|
318
|
+
return cm.private;
|
|
274
319
|
});
|
|
275
320
|
logger.info('✓ Database initialized');
|
|
276
321
|
// 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
|
|
@@ -427,7 +472,7 @@ async function main() {
|
|
|
427
472
|
logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
|
|
428
473
|
}
|
|
429
474
|
}
|
|
430
|
-
// Inject primary agent's trigger scheduler
|
|
475
|
+
// Inject primary agent's trigger scheduler as fallback (used when owning agent has no scheduler)
|
|
431
476
|
const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
|
|
432
477
|
if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
|
|
433
478
|
cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
|
|
@@ -502,10 +547,10 @@ async function main() {
|
|
|
502
547
|
if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
|
|
503
548
|
inst.channel.onProjectPathRequest(async (channelId) => {
|
|
504
549
|
// Effective default path: use the agent that owns this channel.
|
|
505
|
-
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.
|
|
550
|
+
const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
|
|
506
551
|
const effectiveDefault = owningAgent?.projectPath
|
|
507
552
|
?? primaryAgent.projectPath;
|
|
508
|
-
const session = await sessionManager.getOrCreateSession(inst.adapter.
|
|
553
|
+
const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
|
|
509
554
|
return path.isAbsolute(session.projectPath)
|
|
510
555
|
? session.projectPath
|
|
511
556
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -547,10 +592,10 @@ async function main() {
|
|
|
547
592
|
}
|
|
548
593
|
// Bind adapters to their owning agents and mark running
|
|
549
594
|
for (const inst of channelInstances) {
|
|
550
|
-
const agent = agentRegistry.resolveByChannel(inst.adapter.
|
|
595
|
+
const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
|
|
551
596
|
if (!agent || agent.status === 'error')
|
|
552
597
|
continue;
|
|
553
|
-
agent.channels.set(inst.adapter.
|
|
598
|
+
agent.channels.set(inst.adapter.channelKey, inst.adapter);
|
|
554
599
|
if (agent.status === 'stopped') {
|
|
555
600
|
agent.status = 'running';
|
|
556
601
|
}
|
|
@@ -565,7 +610,7 @@ async function main() {
|
|
|
565
610
|
for (const inst of channelInstances) {
|
|
566
611
|
const channelType = inst.channelType || inst.adapter.channelName;
|
|
567
612
|
if (channelType === 'feishu' && 'preloadThreads' in inst.channel) {
|
|
568
|
-
const threadIds = sessionManager.getKnownThreadIds(inst.adapter.
|
|
613
|
+
const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelKey);
|
|
569
614
|
inst.channel.preloadThreads(threadIds);
|
|
570
615
|
}
|
|
571
616
|
}
|
|
@@ -597,9 +642,8 @@ async function main() {
|
|
|
597
642
|
// 尝试从 agent.md 读取 name
|
|
598
643
|
let agentName = agent.aid;
|
|
599
644
|
try {
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
645
|
+
const mdPath = agentMdPath(agent.aid);
|
|
646
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
603
647
|
const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
|
|
604
648
|
if (nameMatch)
|
|
605
649
|
agentName = nameMatch[1].trim().replace(/"$/, '');
|
|
@@ -636,14 +680,14 @@ async function main() {
|
|
|
636
680
|
continue; // 跳过同类型通道
|
|
637
681
|
if (notified.has(otherType))
|
|
638
682
|
continue; // 同类型已通知过
|
|
639
|
-
const ownerId = agentRegistry.getOwner(other.adapter.
|
|
683
|
+
const ownerId = agentRegistry.getOwner(other.adapter.channelKey);
|
|
640
684
|
if (!ownerId)
|
|
641
685
|
continue;
|
|
642
686
|
notified.add(otherType);
|
|
643
|
-
const owningAgent = agentRegistry.resolveByChannel(other.adapter.
|
|
687
|
+
const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelKey);
|
|
644
688
|
const envelope = buildEnvelope({
|
|
645
689
|
taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
|
|
646
|
-
channel: other.adapter.
|
|
690
|
+
channel: other.adapter.channelKey,
|
|
647
691
|
channelId: ownerId,
|
|
648
692
|
agentName: owningAgent?.aid || 'evolclaw',
|
|
649
693
|
});
|
|
@@ -734,10 +778,10 @@ async function main() {
|
|
|
734
778
|
const replyContext = pending.rootId
|
|
735
779
|
? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
|
|
736
780
|
: undefined;
|
|
737
|
-
const owningAgent = agentRegistry.resolveByChannel(adapter.
|
|
781
|
+
const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
|
|
738
782
|
const envelope = buildEnvelope({
|
|
739
783
|
taskId: `system-restart-${process.pid}`,
|
|
740
|
-
channel: adapter.
|
|
784
|
+
channel: adapter.channelKey,
|
|
741
785
|
channelId: pending.channelId,
|
|
742
786
|
agentName: owningAgent?.aid || 'evolclaw',
|
|
743
787
|
replyContext,
|
|
@@ -837,7 +881,7 @@ async function main() {
|
|
|
837
881
|
const instances = await channelLoader.createForAgent(agent);
|
|
838
882
|
for (const inst of instances) {
|
|
839
883
|
registerChannelInstance(inst);
|
|
840
|
-
agent.channels.set(inst.adapter.
|
|
884
|
+
agent.channels.set(inst.adapter.channelKey, inst.adapter);
|
|
841
885
|
channelInstances.push(inst);
|
|
842
886
|
}
|
|
843
887
|
agent.status = 'running';
|
package/dist/paths.js
CHANGED
|
@@ -45,6 +45,16 @@ export function resolvePaths() {
|
|
|
45
45
|
aidLogsDir: path.join(root, 'logs', 'aids'),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
// ── AID 路径(agent.md 存放在 $EVOLCLAW_HOME/AIDs/<aid>/)──
|
|
49
|
+
export function aidsDir() {
|
|
50
|
+
return path.join(resolveRoot(), 'AIDs');
|
|
51
|
+
}
|
|
52
|
+
export function aidLocalDir(aid) {
|
|
53
|
+
return path.join(resolveRoot(), 'AIDs', aid);
|
|
54
|
+
}
|
|
55
|
+
export function agentMdPath(aid) {
|
|
56
|
+
return path.join(resolveRoot(), 'AIDs', aid, 'agent.md');
|
|
57
|
+
}
|
|
48
58
|
// ── per-agent 路径(参数化,不进 resolvePaths() 的固定 map)──
|
|
49
59
|
export function agentDir(aid) {
|
|
50
60
|
return path.join(resolveRoot(), 'agents', aid);
|
|
@@ -98,6 +108,31 @@ export function ensureDataDirs() {
|
|
|
98
108
|
fs.mkdirSync(p.outboxDir, { recursive: true });
|
|
99
109
|
fs.mkdirSync(p.eckDir, { recursive: true });
|
|
100
110
|
fs.mkdirSync(eckDebugDir(), { recursive: true });
|
|
111
|
+
fs.mkdirSync(aidsDir(), { recursive: true });
|
|
112
|
+
migrateAgentMdFromAun();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One-time migration: copy agent.md from ~/.aun/AIDs/<aid>/ to $EVOLCLAW_HOME/AIDs/<aid>/
|
|
116
|
+
* if the new location doesn't have it yet.
|
|
117
|
+
*/
|
|
118
|
+
function migrateAgentMdFromAun() {
|
|
119
|
+
const aunAidsDir = path.join(os.homedir(), '.aun', 'AIDs');
|
|
120
|
+
const ecAids = aidsDir();
|
|
121
|
+
if (!fs.existsSync(aunAidsDir) || aunAidsDir === ecAids)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
for (const entry of fs.readdirSync(aunAidsDir, { withFileTypes: true })) {
|
|
125
|
+
if (!entry.isDirectory())
|
|
126
|
+
continue;
|
|
127
|
+
const oldMd = path.join(aunAidsDir, entry.name, 'agent.md');
|
|
128
|
+
const newMd = path.join(ecAids, entry.name, 'agent.md');
|
|
129
|
+
if (fs.existsSync(oldMd) && !fs.existsSync(newMd)) {
|
|
130
|
+
fs.mkdirSync(path.join(ecAids, entry.name), { recursive: true });
|
|
131
|
+
fs.copyFileSync(oldMd, newMd);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { /* best-effort migration */ }
|
|
101
136
|
}
|
|
102
137
|
// ── kits 路径(始终从包内读取,不复制到 EVOLCLAW_HOME)──
|
|
103
138
|
export function kitsDir() {
|
|
@@ -12,7 +12,8 @@ export const isWindows = process.platform === 'win32';
|
|
|
12
12
|
* C:\Users\project -> C--Users-project
|
|
13
13
|
*/
|
|
14
14
|
export function encodePath(projectPath) {
|
|
15
|
-
|
|
15
|
+
const normalized = projectPath.replace(/[/\\]+$/, '');
|
|
16
|
+
return normalized.replace(/[/\\:]/g, '-');
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Cross-platform process liveness check.
|
|
@@ -245,27 +245,29 @@ export function isRetryableError(error) {
|
|
|
245
245
|
return true;
|
|
246
246
|
return false;
|
|
247
247
|
}
|
|
248
|
-
export function getErrorMessage(error, terminalReason) {
|
|
248
|
+
export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
249
249
|
// terminalReason 提供更精确的错误提示(SDK 0.2.100+)
|
|
250
250
|
if (terminalReason) {
|
|
251
|
+
const prefix = includeEmoji ? '❌ ' : '';
|
|
252
|
+
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
251
253
|
switch (terminalReason) {
|
|
252
254
|
case 'max_turns':
|
|
253
|
-
return
|
|
255
|
+
return `${prefix}任务达到最大轮次限制,请简化需求或分步执行`;
|
|
254
256
|
case 'prompt_too_long':
|
|
255
|
-
return
|
|
257
|
+
return `${warnPrefix}输入过长,请精简提问或使用 /compact 压缩上下文`;
|
|
256
258
|
case 'rapid_refill_breaker':
|
|
257
|
-
return
|
|
259
|
+
return `${warnPrefix}API 限流中,请稍后重试`;
|
|
258
260
|
case 'context_compact_failed':
|
|
259
|
-
return
|
|
261
|
+
return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
260
262
|
case 'model_error':
|
|
261
|
-
return
|
|
263
|
+
return `${prefix}模型服务异常,请稍后重试`;
|
|
262
264
|
case 'tool_error':
|
|
263
|
-
return
|
|
265
|
+
return `${prefix}工具执行失败,请检查操作或重试`;
|
|
264
266
|
case 'permission_denied':
|
|
265
|
-
return
|
|
267
|
+
return `${prefix}权限被拒绝,操作已取消`;
|
|
266
268
|
case 'aborted_streaming':
|
|
267
269
|
case 'aborted_tools':
|
|
268
|
-
return
|
|
270
|
+
return `${prefix}任务已中断`;
|
|
269
271
|
}
|
|
270
272
|
}
|
|
271
273
|
// 回退到原有的错误消息匹配逻辑
|
|
@@ -275,15 +277,17 @@ export function getErrorMessage(error, terminalReason) {
|
|
|
275
277
|
if (rule?.message)
|
|
276
278
|
return rule.message;
|
|
277
279
|
// 内置兜底规则(结构性错误)
|
|
280
|
+
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
281
|
+
const errPrefix = includeEmoji ? '❌ ' : '';
|
|
278
282
|
if (msg.includes('CONTEXT_COMPACT_FAILED') || msg.includes('context_length_exceeded')
|
|
279
283
|
|| msg.includes('Context limit')) {
|
|
280
|
-
return
|
|
284
|
+
return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
281
285
|
}
|
|
282
286
|
if (msg.includes('401') || msg.includes('authentication_error')) {
|
|
283
|
-
return
|
|
287
|
+
return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
|
|
284
288
|
}
|
|
285
289
|
if (msg.includes('timeout')) {
|
|
286
|
-
return
|
|
290
|
+
return `${warnPrefix}请求超时,请重试`;
|
|
287
291
|
}
|
|
288
|
-
return
|
|
292
|
+
return `${errPrefix}处理消息时出错,请稍后重试`;
|
|
289
293
|
}
|
package/dist/utils/stats.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
export class StatsCollector {
|
|
2
4
|
events = [];
|
|
3
5
|
startTime;
|
|
@@ -100,6 +102,71 @@ export class StatsCollector {
|
|
|
100
102
|
export class AidStatsCollector {
|
|
101
103
|
entries = new Map();
|
|
102
104
|
queueStatsProvider;
|
|
105
|
+
/** sessionId → 当前正在跑该 session 的 agent,task:started 写入,task:completed/error 清除 */
|
|
106
|
+
sessionToAgent = new Map();
|
|
107
|
+
constructor(eventBus) {
|
|
108
|
+
if (!eventBus)
|
|
109
|
+
return;
|
|
110
|
+
eventBus.subscribe('task:started', (event) => {
|
|
111
|
+
const e = event;
|
|
112
|
+
if (e.agentName)
|
|
113
|
+
this.onTaskStart(e.agentName, e.encrypt, e.chatmode);
|
|
114
|
+
if (e.agentName && e.sessionId)
|
|
115
|
+
this.sessionToAgent.set(e.sessionId, e.agentName);
|
|
116
|
+
});
|
|
117
|
+
eventBus.subscribe('task:completed', (event) => {
|
|
118
|
+
const e = event;
|
|
119
|
+
if (e.agentName)
|
|
120
|
+
this.onTaskEnd(e.agentName, 'completed', undefined, e.finalText, e.numTurns);
|
|
121
|
+
if (e.sessionId)
|
|
122
|
+
this.sessionToAgent.delete(e.sessionId);
|
|
123
|
+
});
|
|
124
|
+
eventBus.subscribe('task:error', (event) => {
|
|
125
|
+
const e = event;
|
|
126
|
+
if (e.agentName)
|
|
127
|
+
this.onTaskEnd(e.agentName, 'error', e.errorType);
|
|
128
|
+
if (e.sessionId)
|
|
129
|
+
this.sessionToAgent.delete(e.sessionId);
|
|
130
|
+
});
|
|
131
|
+
// thought.put 次数 + 最后一次 thought 文本
|
|
132
|
+
// 注意:thought.put 是 fire-and-forget async,可能在 task:completed 之后才到达,
|
|
133
|
+
// 所以同时累加到 currentTask(task 进行中)或 lastTaskEnd(task 已结束但 thought 属于它)
|
|
134
|
+
eventBus.subscribe('message:thought-put', (event) => {
|
|
135
|
+
const e = event;
|
|
136
|
+
if (!e.agentName)
|
|
137
|
+
return;
|
|
138
|
+
const entry = this.entries.get(e.agentName);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return;
|
|
141
|
+
if (entry.currentTaskStartAt != null) {
|
|
142
|
+
// task 进行中
|
|
143
|
+
entry.currentTaskThoughtPutCount++;
|
|
144
|
+
if (e.text)
|
|
145
|
+
entry.currentTaskLastThoughtText = e.text;
|
|
146
|
+
}
|
|
147
|
+
else if (entry.lastTaskEnd) {
|
|
148
|
+
// task 已结束,回填到最近一次 task
|
|
149
|
+
entry.lastTaskEnd.thoughtPutCount++;
|
|
150
|
+
entry.lastTaskEnd.thoughtDuringTask = true;
|
|
151
|
+
if (e.text) {
|
|
152
|
+
const t = e.text.length > 100 ? e.text.slice(0, 100) + '…' : e.text;
|
|
153
|
+
entry.lastTaskEnd.lastThoughtText = t;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// 工具调用次数(tool:use 事件)
|
|
158
|
+
eventBus.subscribe('tool:use', (event) => {
|
|
159
|
+
const e = event;
|
|
160
|
+
if (!e.sessionId)
|
|
161
|
+
return;
|
|
162
|
+
const agent = this.sessionToAgent.get(e.sessionId);
|
|
163
|
+
if (!agent)
|
|
164
|
+
return;
|
|
165
|
+
const entry = this.entries.get(agent);
|
|
166
|
+
if (entry && entry.currentTaskStartAt != null)
|
|
167
|
+
entry.currentTaskToolUseCount++;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
103
170
|
setQueueStatsProvider(provider) {
|
|
104
171
|
this.queueStatsProvider = provider;
|
|
105
172
|
}
|
|
@@ -119,19 +186,145 @@ export class AidStatsCollector {
|
|
|
119
186
|
lastSentAt: null,
|
|
120
187
|
lastReceivedText: null,
|
|
121
188
|
lastReceivedFrom: null,
|
|
189
|
+
lastReceivedEncrypt: null,
|
|
190
|
+
lastReceivedChatmode: null,
|
|
122
191
|
lastSentText: null,
|
|
123
192
|
lastSentTo: null,
|
|
193
|
+
lastSentEncrypt: null,
|
|
194
|
+
lastSentChatmode: null,
|
|
124
195
|
uniquePeers: new Set(),
|
|
196
|
+
currentTaskStartAt: null,
|
|
197
|
+
currentTaskReplyCount: 0,
|
|
198
|
+
currentTaskThoughtPutCount: 0,
|
|
199
|
+
currentTaskToolUseCount: 0,
|
|
200
|
+
currentTaskNumTurns: 0,
|
|
201
|
+
currentTaskLastThoughtText: null,
|
|
202
|
+
currentTaskSessionId: null,
|
|
203
|
+
currentTaskChatmode: null,
|
|
204
|
+
currentTaskEncrypt: null,
|
|
205
|
+
lastTaskEnd: undefined,
|
|
125
206
|
};
|
|
126
207
|
this.entries.set(aid, entry);
|
|
127
208
|
}
|
|
128
209
|
return entry;
|
|
129
210
|
}
|
|
211
|
+
sessionsDir;
|
|
212
|
+
setSessionsDir(dir) {
|
|
213
|
+
this.sessionsDir = dir;
|
|
214
|
+
}
|
|
215
|
+
onTaskStart(aid, encrypt, chatmode) {
|
|
216
|
+
const entry = this.getOrCreate(aid);
|
|
217
|
+
entry.currentTaskStartAt = Date.now();
|
|
218
|
+
entry.currentTaskReplyCount = 0;
|
|
219
|
+
entry.currentTaskThoughtPutCount = 0;
|
|
220
|
+
entry.currentTaskToolUseCount = 0;
|
|
221
|
+
entry.currentTaskNumTurns = 0;
|
|
222
|
+
entry.currentTaskLastThoughtText = null;
|
|
223
|
+
entry.currentTaskChatmode = chatmode ?? null;
|
|
224
|
+
entry.currentTaskEncrypt = encrypt ?? null;
|
|
225
|
+
}
|
|
226
|
+
onTaskEnd(aid, status, errorType, finalText, numTurns) {
|
|
227
|
+
const entry = this.getOrCreate(aid);
|
|
228
|
+
const startedAt = entry.currentTaskStartAt;
|
|
229
|
+
const taskEndTs = Date.now();
|
|
230
|
+
// 先用内存计数写入初始值(立即可用)
|
|
231
|
+
const buildTaskEnd = (msgCount, thoughtCount, lastThought) => ({
|
|
232
|
+
ts: taskEndTs,
|
|
233
|
+
status,
|
|
234
|
+
errorType,
|
|
235
|
+
sentDuringTask: msgCount > 0,
|
|
236
|
+
thoughtDuringTask: thoughtCount > 0,
|
|
237
|
+
lastThoughtText: lastThought,
|
|
238
|
+
replyCount: msgCount,
|
|
239
|
+
thoughtPutCount: thoughtCount,
|
|
240
|
+
toolUseCount: entry.currentTaskToolUseCount,
|
|
241
|
+
numTurns: numTurns ?? entry.currentTaskNumTurns,
|
|
242
|
+
finalText: finalText ? (finalText.length > 100 ? finalText.slice(0, 100) + '…' : finalText) : undefined,
|
|
243
|
+
chatmode: entry.currentTaskChatmode ?? undefined,
|
|
244
|
+
encrypt: entry.currentTaskEncrypt ?? undefined,
|
|
245
|
+
});
|
|
246
|
+
entry.lastTaskEnd = buildTaskEnd(entry.currentTaskReplyCount, entry.currentTaskThoughtPutCount, entry.currentTaskLastThoughtText ?? undefined);
|
|
247
|
+
// 500ms 后从 jsonl 重新统计(覆盖 thought.put 异步延迟问题)
|
|
248
|
+
if (this.sessionsDir && startedAt != null) {
|
|
249
|
+
const sessionsDir = this.sessionsDir;
|
|
250
|
+
const toolUseCount = entry.currentTaskToolUseCount;
|
|
251
|
+
const resolvedNumTurns = numTurns ?? entry.currentTaskNumTurns;
|
|
252
|
+
const chatmode = entry.currentTaskChatmode;
|
|
253
|
+
const encrypt = entry.currentTaskEncrypt;
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
try {
|
|
256
|
+
const { chatDirPath } = require('../core/session/session-fs-store.js');
|
|
257
|
+
// 找该 aid 下所有 peer 的 messages.jsonl,统计 ts >= startedAt 的出站条目
|
|
258
|
+
const aidDir = path.join(sessionsDir, 'aun', aid.replace(/[/%\\:*?"<>|]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')));
|
|
259
|
+
if (!fs.existsSync(aidDir))
|
|
260
|
+
return;
|
|
261
|
+
let msgCount = 0, thoughtCount = 0;
|
|
262
|
+
let lastThoughtText;
|
|
263
|
+
let lastMsgText;
|
|
264
|
+
for (const peerDir of fs.readdirSync(aidDir, { withFileTypes: true })) {
|
|
265
|
+
if (!peerDir.isDirectory() || peerDir.name.startsWith('_'))
|
|
266
|
+
continue;
|
|
267
|
+
const msgFile = path.join(aidDir, peerDir.name, 'messages.jsonl');
|
|
268
|
+
if (!fs.existsSync(msgFile))
|
|
269
|
+
continue;
|
|
270
|
+
const lines = fs.readFileSync(msgFile, 'utf-8').split('\n').filter(Boolean);
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
try {
|
|
273
|
+
const rec = JSON.parse(line);
|
|
274
|
+
if (rec.dir !== 'out' || rec.ts < startedAt || rec.ts > taskEndTs + 2000)
|
|
275
|
+
continue;
|
|
276
|
+
if (rec.msgType === 'thought') {
|
|
277
|
+
thoughtCount++;
|
|
278
|
+
if (rec.content)
|
|
279
|
+
lastThoughtText = rec.content.length > 100 ? rec.content.slice(0, 100) + '…' : rec.content;
|
|
280
|
+
}
|
|
281
|
+
else if (rec.msgType === 'text') {
|
|
282
|
+
msgCount++;
|
|
283
|
+
if (rec.content)
|
|
284
|
+
lastMsgText = rec.content.length > 100 ? rec.content.slice(0, 100) + '…' : rec.content;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const currentEntry = this.entries.get(aid);
|
|
291
|
+
if (currentEntry?.lastTaskEnd?.ts === taskEndTs) {
|
|
292
|
+
currentEntry.lastTaskEnd = {
|
|
293
|
+
ts: taskEndTs,
|
|
294
|
+
status,
|
|
295
|
+
errorType,
|
|
296
|
+
sentDuringTask: msgCount > 0,
|
|
297
|
+
thoughtDuringTask: thoughtCount > 0,
|
|
298
|
+
lastThoughtText: lastThoughtText,
|
|
299
|
+
replyCount: msgCount,
|
|
300
|
+
thoughtPutCount: thoughtCount,
|
|
301
|
+
toolUseCount,
|
|
302
|
+
numTurns: resolvedNumTurns,
|
|
303
|
+
finalText: finalText ? (finalText.length > 100 ? finalText.slice(0, 100) + '…' : finalText) : undefined,
|
|
304
|
+
chatmode: chatmode ?? undefined,
|
|
305
|
+
encrypt: encrypt ?? undefined,
|
|
306
|
+
};
|
|
307
|
+
// 更新 lastSentText 为最后一条 msg(如果有)
|
|
308
|
+
if (lastMsgText && msgCount > 0) {
|
|
309
|
+
currentEntry.lastSentText = lastMsgText;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}, 500);
|
|
315
|
+
}
|
|
316
|
+
entry.currentTaskStartAt = null;
|
|
317
|
+
entry.currentTaskReplyCount = 0;
|
|
318
|
+
entry.currentTaskThoughtPutCount = 0;
|
|
319
|
+
entry.currentTaskToolUseCount = 0;
|
|
320
|
+
entry.currentTaskNumTurns = 0;
|
|
321
|
+
entry.currentTaskLastThoughtText = null;
|
|
322
|
+
}
|
|
130
323
|
setSelfName(aid, name) {
|
|
131
324
|
const entry = this.getOrCreate(aid);
|
|
132
325
|
entry.selfName = name;
|
|
133
326
|
}
|
|
134
|
-
recordInbound(aid, fromPeer, byteLength, text, isSystem = false) {
|
|
327
|
+
recordInbound(aid, fromPeer, byteLength, text, isSystem = false, encrypt, chatmode) {
|
|
135
328
|
const entry = this.getOrCreate(aid);
|
|
136
329
|
if (isSystem) {
|
|
137
330
|
entry.systemReceived++;
|
|
@@ -142,11 +335,15 @@ export class AidStatsCollector {
|
|
|
142
335
|
entry.lastReceivedFrom = fromPeer;
|
|
143
336
|
if (text)
|
|
144
337
|
entry.lastReceivedText = text.length > 100 ? text.slice(0, 100) + '…' : text;
|
|
338
|
+
if (encrypt != null)
|
|
339
|
+
entry.lastReceivedEncrypt = encrypt;
|
|
340
|
+
if (chatmode)
|
|
341
|
+
entry.lastReceivedChatmode = chatmode;
|
|
145
342
|
}
|
|
146
343
|
entry.bytesReceived += byteLength;
|
|
147
344
|
entry.uniquePeers.add(fromPeer);
|
|
148
345
|
}
|
|
149
|
-
recordOutbound(aid, toPeer, byteLength, text, isSystem = false) {
|
|
346
|
+
recordOutbound(aid, toPeer, byteLength, text, isSystem = false, encrypt, chatmode) {
|
|
150
347
|
const entry = this.getOrCreate(aid);
|
|
151
348
|
if (isSystem) {
|
|
152
349
|
entry.systemSent++;
|
|
@@ -157,6 +354,18 @@ export class AidStatsCollector {
|
|
|
157
354
|
entry.lastSentTo = toPeer;
|
|
158
355
|
if (text)
|
|
159
356
|
entry.lastSentText = text.length > 100 ? text.slice(0, 100) + '…' : text;
|
|
357
|
+
if (encrypt != null)
|
|
358
|
+
entry.lastSentEncrypt = encrypt;
|
|
359
|
+
if (chatmode)
|
|
360
|
+
entry.lastSentChatmode = chatmode;
|
|
361
|
+
// 累计当前 task 的回复数
|
|
362
|
+
if (entry.currentTaskStartAt != null) {
|
|
363
|
+
entry.currentTaskReplyCount++;
|
|
364
|
+
if (chatmode)
|
|
365
|
+
entry.currentTaskChatmode = chatmode;
|
|
366
|
+
if (encrypt != null)
|
|
367
|
+
entry.currentTaskEncrypt = encrypt;
|
|
368
|
+
}
|
|
160
369
|
}
|
|
161
370
|
entry.bytesSent += byteLength;
|
|
162
371
|
entry.uniquePeers.add(toPeer);
|
|
@@ -180,11 +389,16 @@ export class AidStatsCollector {
|
|
|
180
389
|
lastSentAt: entry.lastSentAt,
|
|
181
390
|
lastReceivedText: entry.lastReceivedText,
|
|
182
391
|
lastReceivedFrom: entry.lastReceivedFrom,
|
|
392
|
+
lastReceivedEncrypt: entry.lastReceivedEncrypt,
|
|
393
|
+
lastReceivedChatmode: entry.lastReceivedChatmode,
|
|
183
394
|
lastSentText: entry.lastSentText,
|
|
184
395
|
lastSentTo: entry.lastSentTo,
|
|
396
|
+
lastSentEncrypt: entry.lastSentEncrypt,
|
|
397
|
+
lastSentChatmode: entry.lastSentChatmode,
|
|
185
398
|
uniquePeerCount: entry.uniquePeers.size,
|
|
186
399
|
processing: queueStats.processing,
|
|
187
400
|
queued: queueStats.queued,
|
|
401
|
+
lastTaskEnd: entry.lastTaskEnd,
|
|
188
402
|
});
|
|
189
403
|
}
|
|
190
404
|
return out;
|
package/kits/docs/INDEX.md
CHANGED
|
@@ -50,3 +50,9 @@
|
|
|
50
50
|
| 路径注册表模板 | `eck_templates/path-registry.template.md` | 路径实例模板 |
|
|
51
51
|
| 索引模板 | `eck_templates/INDEX.template.md` | agent 级索引模板 |
|
|
52
52
|
| 指南模板 | `eck_templates/GUIDE.template.md` | agent 级查阅指南模板 |
|
|
53
|
+
|
|
54
|
+
## Base Agent
|
|
55
|
+
|
|
56
|
+
| 文档 | 路径 | 说明 |
|
|
57
|
+
|------|------|------|
|
|
58
|
+
| Claude Code 日志 | `baseagent/cc-logs.md` | CC 会话日志查阅(找完整对话/工具调用/注入) |
|
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
# 私聊消息命令
|
|
2
2
|
|
|
3
|
-
<!-- TODO: 填充私聊消息命令详细参考 -->
|
|
4
|
-
|
|
5
3
|
## 发送消息
|
|
6
4
|
|
|
5
|
+
### 以指定 AID 发送(首选)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 明文
|
|
9
|
+
ec msg send <from-aid> <to-aid> "<message>"
|
|
10
|
+
|
|
11
|
+
# 密文(E2EE)
|
|
12
|
+
ec msg send <from-aid> <to-aid> "<message>" --encrypt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 发送文件
|
|
16
|
+
|
|
7
17
|
```bash
|
|
8
|
-
|
|
18
|
+
ec msg send <from-aid> <to-aid> --file <path>
|
|
19
|
+
ec msg send <from-aid> <to-aid> --file <path> --as image
|
|
20
|
+
ec msg send <from-aid> <to-aid> --file <path> --encrypt
|
|
9
21
|
```
|
|
10
22
|
|
|
23
|
+
`--as` 可选值:`image` | `video` | `voice` | `file`(默认按扩展名推断)
|
|
24
|
+
|
|
11
25
|
## 拉取消息
|
|
12
26
|
|
|
13
27
|
```bash
|
|
14
|
-
|
|
28
|
+
ec msg pull <self-aid> --app <app-name>
|
|
29
|
+
ec msg pull <self-aid> --app <app-name> --after-seq <N> --limit <N>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 确认消息已读
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
ec msg ack <self-aid> <seq> --app <app-name>
|
|
15
36
|
```
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
`--app` 必须传,否则会污染 daemon 游标。
|
|
39
|
+
|
|
40
|
+
## 撤回消息
|
|
18
41
|
|
|
19
42
|
```bash
|
|
20
|
-
|
|
43
|
+
ec msg recall <self-aid> <message-id>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 查询在线状态
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ec msg online <self-aid> <target-aid>
|
|
21
50
|
```
|
|
22
51
|
|
|
23
52
|
## 自主回复策略
|
|
24
53
|
|
|
25
54
|
收到消息 ≠ 必须回复。是否回复、怎么回复、何时回复由 agent 自主决定。
|
|
55
|
+
|
|
56
|
+
加密策略:
|
|
57
|
+
- 对端发来密文消息时,回复也应使用 `--encrypt`(保持对话加密一致性)
|
|
58
|
+
- 对端发来明文消息时,默认明文回复
|
|
59
|
+
|
|
60
|
+
## 在当前会话中快速回复(备选)
|
|
61
|
+
|
|
62
|
+
仅当无法使用 `ec msg send` 时(如不知道自己的 AID),可用 `ec ctl send`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 明文
|
|
66
|
+
ec ctl send "<text>"
|
|
67
|
+
|
|
68
|
+
# 密文
|
|
69
|
+
ec ctl send --encrypt "<text>"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`ec ctl send` 自动继承当前会话的 AID 和对端,无需指定。
|