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/cli/watch-msg.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolvePaths
|
|
3
|
+
import { resolvePaths } from '../paths.js';
|
|
4
4
|
import { decodeDirSegment, readAllJsonlLines } from '../core/session/session-fs-store.js';
|
|
5
5
|
// ==================== ANSI ====================
|
|
6
6
|
const isTTY = !!process.stdout.isTTY;
|
|
@@ -84,6 +84,34 @@ function formatTimeAgo(ms) {
|
|
|
84
84
|
return `${hour}h`;
|
|
85
85
|
return `${Math.floor(hour / 24)}d`;
|
|
86
86
|
}
|
|
87
|
+
function getCodeTime(pkgRoot) {
|
|
88
|
+
let latestMtime = 0;
|
|
89
|
+
const scanDir = fs.existsSync(path.join(pkgRoot, 'dist')) ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
|
|
90
|
+
const scanRecursive = (dir) => {
|
|
91
|
+
try {
|
|
92
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
if (entry.name === 'node_modules')
|
|
94
|
+
continue;
|
|
95
|
+
const full = path.join(dir, entry.name);
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
scanRecursive(full);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
101
|
+
const mt = fs.statSync(full).mtimeMs;
|
|
102
|
+
if (mt > latestMtime)
|
|
103
|
+
latestMtime = mt;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
};
|
|
109
|
+
scanRecursive(scanDir);
|
|
110
|
+
if (!latestMtime)
|
|
111
|
+
return '?';
|
|
112
|
+
const d = new Date(latestMtime);
|
|
113
|
+
return `${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')}`;
|
|
114
|
+
}
|
|
87
115
|
function formatNumber(n) {
|
|
88
116
|
return n.toLocaleString('en-US');
|
|
89
117
|
}
|
|
@@ -267,43 +295,73 @@ function renderStatsPanel(state, width, height) {
|
|
|
267
295
|
// ==================== Messages Panel ====================
|
|
268
296
|
function renderMessagesPanel(state, width, height) {
|
|
269
297
|
const lines = [];
|
|
270
|
-
const
|
|
298
|
+
const lastTs = state.messages.length > 0 ? state.messages[state.messages.length - 1].ts : 0;
|
|
299
|
+
const lastTimeStr = lastTs ? formatDateTime(lastTs) : '--';
|
|
300
|
+
const title = `${DIM}─ Messages (${state.messages.length}, last: ${lastTimeStr}) ─${RST}`;
|
|
271
301
|
lines.push(padRight(title, width));
|
|
272
302
|
const contentHeight = height - 1;
|
|
273
303
|
const msgs = state.messages;
|
|
274
304
|
const totalMsgs = msgs.length;
|
|
275
|
-
const visibleCount = contentHeight;
|
|
276
|
-
const startIdx = Math.max(0, totalMsgs - visibleCount - state.messageScrollOffset);
|
|
277
|
-
const endIdx = Math.min(totalMsgs, startIdx + visibleCount);
|
|
278
|
-
const scrollbar = renderScrollbar(totalMsgs, visibleCount, state.messageScrollOffset, contentHeight);
|
|
279
305
|
const msgWidth = width - 3;
|
|
280
306
|
const contentLineWidth = msgWidth - 2;
|
|
281
307
|
const maxContentLines = 3;
|
|
282
|
-
|
|
283
|
-
|
|
308
|
+
// 先构造每条消息的渲染行,从最末尾的可见消息往回收集,直到填满 contentHeight
|
|
309
|
+
function renderOneMsg(m) {
|
|
284
310
|
const time = formatDateTime(m.ts);
|
|
285
311
|
const dir = m.dir === 'in' ? `${GREEN}↓${RST}` : `${BLUE}↑${RST}`;
|
|
286
312
|
const isGroup = m.chatType === 'group';
|
|
287
|
-
const chatTag = isGroup ? `${MAGENTA}[群聊]${RST}
|
|
313
|
+
const chatTag = isGroup ? `${MAGENTA}[群聊]${RST}` : '';
|
|
314
|
+
const encLabel = m.encrypt ? '密文' : '明文';
|
|
315
|
+
const modeLabel = m.chatmode === 'proactive' ? '自主' : '响应';
|
|
316
|
+
const metaTags = (m.encrypt != null || m.chatmode) ? `${MAGENTA}[${encLabel}|${modeLabel}]${RST}` : '';
|
|
317
|
+
let typeTag = '';
|
|
318
|
+
if (m.dir === 'out') {
|
|
319
|
+
const rawSource = m.source;
|
|
320
|
+
// 4 种来源: daemon | ctl | msg | cli
|
|
321
|
+
const source = (rawSource === 'ctl' || rawSource === 'msg' || rawSource === 'cli') ? rawSource : 'daemon';
|
|
322
|
+
const method = m.msgType === 'thought' ? 'thought' : 'send';
|
|
323
|
+
typeTag = `${DIM}[${source}|${method}]${RST}`;
|
|
324
|
+
}
|
|
288
325
|
const byteLen = Buffer.byteLength(m.content, 'utf-8');
|
|
289
326
|
const lenTag = `${DIM}${formatNumber(byteLen)}B${RST}`;
|
|
290
|
-
const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from;
|
|
291
|
-
const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to;
|
|
292
|
-
const header = `${DIM}${time}${RST} ${dir}${chatTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from.split('.')[0];
|
|
328
|
+
const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to.split('.')[0];
|
|
329
|
+
const header = `${DIM}${time}${RST} ${dir}${chatTag}${metaTags}${typeTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
|
|
330
|
+
const out = [padRight(header, msgWidth)];
|
|
331
|
+
const rawContent = m.content.replace(/\n/g, ' ');
|
|
332
|
+
const wrappedLines = wrapText(rawContent, contentLineWidth, maxContentLines);
|
|
333
|
+
for (const wl of wrappedLines) {
|
|
334
|
+
out.push(padRight(` ${wl}`, msgWidth));
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
// 从 endIdx-1 开始倒序,往回累积,直到行数填满 contentHeight
|
|
339
|
+
const endIdx = Math.max(0, totalMsgs - state.messageScrollOffset);
|
|
340
|
+
const collected = []; // 每条消息的行数组
|
|
341
|
+
let totalLines = 0;
|
|
342
|
+
let firstShownIdx = endIdx; // 首条可见消息的下标
|
|
343
|
+
for (let i = endIdx - 1; i >= 0; i--) {
|
|
344
|
+
const rendered = renderOneMsg(msgs[i]);
|
|
345
|
+
if (totalLines + rendered.length > contentHeight && collected.length > 0)
|
|
346
|
+
break;
|
|
347
|
+
collected.unshift(rendered);
|
|
348
|
+
totalLines += rendered.length;
|
|
349
|
+
firstShownIdx = i;
|
|
350
|
+
if (totalLines >= contentHeight)
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
const visibleMsgCount = endIdx - firstShownIdx;
|
|
354
|
+
const scrollbar = renderScrollbar(totalMsgs, visibleMsgCount, state.messageScrollOffset, contentHeight);
|
|
355
|
+
// 正序输出(旧→新)
|
|
356
|
+
for (const rendered of collected) {
|
|
357
|
+
for (const line of rendered) {
|
|
358
|
+
if (lines.length - 1 >= contentHeight)
|
|
359
|
+
break;
|
|
360
|
+
const sbIdx = lines.length - 1;
|
|
361
|
+
lines.push(`${line} ${scrollbar[sbIdx] || ' '}`);
|
|
306
362
|
}
|
|
363
|
+
if (lines.length - 1 >= contentHeight)
|
|
364
|
+
break;
|
|
307
365
|
}
|
|
308
366
|
while (lines.length < height) {
|
|
309
367
|
const sbIdx = lines.length - 1;
|
|
@@ -314,11 +372,11 @@ function renderMessagesPanel(state, width, height) {
|
|
|
314
372
|
// ==================== Main Render ====================
|
|
315
373
|
function renderFrame(state) {
|
|
316
374
|
const cols = process.stdout.columns || 120;
|
|
317
|
-
const rows = (process.stdout.rows || 40)
|
|
375
|
+
const rows = (process.stdout.rows || 40);
|
|
376
|
+
const bodyHeight = rows - 4;
|
|
318
377
|
const leftW = Math.max(20, Math.floor(cols * 0.20));
|
|
319
378
|
const midW = Math.max(24, Math.floor(cols * 0.22));
|
|
320
379
|
const rightW = Math.max(40, cols - leftW - midW - 4);
|
|
321
|
-
const bodyHeight = rows - 2;
|
|
322
380
|
const leftLines = renderScopePanel(state, leftW, bodyHeight);
|
|
323
381
|
const midLines = renderStatsPanel(state, midW, bodyHeight);
|
|
324
382
|
const msgLines = renderMessagesPanel(state, rightW, bodyHeight);
|
|
@@ -334,11 +392,11 @@ function renderFrame(state) {
|
|
|
334
392
|
}
|
|
335
393
|
const bottomBorder = `${DIM}├${'─'.repeat(leftW)}┴${'─'.repeat(midW)}┴${'─'.repeat(rightW + 1)}┤${RST}`;
|
|
336
394
|
buf += `\x1b[2K${bottomBorder}\n`;
|
|
337
|
-
const
|
|
338
|
-
const helpLine = `${DIM}│
|
|
395
|
+
const helpText = `Tab: panel ↑↓: nav Enter: select Backspace: back ESC: exit`;
|
|
396
|
+
const helpLine = `${DIM}│ ${helpText.slice(0, cols - 4)} ${RST}`;
|
|
339
397
|
buf += `\x1b[2K${helpLine}\n`;
|
|
340
398
|
const closeBorder = `${DIM}└${'─'.repeat(cols - 2)}┘${RST}`;
|
|
341
|
-
buf += `\x1b[2K${closeBorder}
|
|
399
|
+
buf += `\x1b[2K${closeBorder}`;
|
|
342
400
|
return buf;
|
|
343
401
|
}
|
|
344
402
|
// ==================== Main ====================
|
|
@@ -409,6 +467,7 @@ export async function cmdWatchMsg() {
|
|
|
409
467
|
if (!state.selectedLocalAid)
|
|
410
468
|
return;
|
|
411
469
|
state.peers = loadPeerInfos(aunDir, state.selectedLocalAid);
|
|
470
|
+
const prevCount = state.messages.length;
|
|
412
471
|
if (state.selectedPeer) {
|
|
413
472
|
state.messages = readMessages(aunDir, state.selectedLocalAid, state.selectedPeer);
|
|
414
473
|
if (state.messages.length > 1000)
|
|
@@ -417,6 +476,10 @@ export async function cmdWatchMsg() {
|
|
|
417
476
|
else {
|
|
418
477
|
state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
|
|
419
478
|
}
|
|
479
|
+
// 有新消息时自动滚到底部
|
|
480
|
+
if (state.messages.length > prevCount) {
|
|
481
|
+
state.messageScrollOffset = 0;
|
|
482
|
+
}
|
|
420
483
|
// Also refresh scope stats for the selected AID
|
|
421
484
|
const idx = state.localAids.findIndex(a => a.aid === state.selectedLocalAid);
|
|
422
485
|
if (idx >= 0) {
|
|
@@ -431,6 +494,7 @@ export async function cmdWatchMsg() {
|
|
|
431
494
|
watcher.close();
|
|
432
495
|
watcher = null;
|
|
433
496
|
}
|
|
497
|
+
clearInterval(pollTimer);
|
|
434
498
|
if (process.stdin.isTTY)
|
|
435
499
|
try {
|
|
436
500
|
process.stdin.setRawMode(false);
|
|
@@ -579,6 +643,21 @@ export async function cmdWatchMsg() {
|
|
|
579
643
|
loadScope();
|
|
580
644
|
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
581
645
|
render();
|
|
646
|
+
// 定时轮询:5 秒检查一次,有变化才刷新
|
|
647
|
+
let lastMsgCount = state.messages.length;
|
|
648
|
+
let lastMsgTs = state.messages.length > 0 ? state.messages[state.messages.length - 1].ts : 0;
|
|
649
|
+
const pollTimer = setInterval(() => {
|
|
650
|
+
if (!state.selectedLocalAid)
|
|
651
|
+
return;
|
|
652
|
+
refreshData();
|
|
653
|
+
const newCount = state.messages.length;
|
|
654
|
+
const newTs = newCount > 0 ? state.messages[newCount - 1].ts : 0;
|
|
655
|
+
if (newCount !== lastMsgCount || newTs !== lastMsgTs) {
|
|
656
|
+
lastMsgCount = newCount;
|
|
657
|
+
lastMsgTs = newTs;
|
|
658
|
+
render();
|
|
659
|
+
}
|
|
660
|
+
}, 5000);
|
|
582
661
|
if (process.stdin.isTTY) {
|
|
583
662
|
process.stdin.setRawMode(true);
|
|
584
663
|
process.stdin.resume();
|
package/dist/config-store.js
CHANGED
|
@@ -75,8 +75,53 @@ export function loadDefaults() {
|
|
|
75
75
|
return expandEnvRefs(raw);
|
|
76
76
|
}
|
|
77
77
|
export function saveDefaults(value) {
|
|
78
|
+
backupDefaults(resolvePaths().defaultsConfig);
|
|
78
79
|
atomicWriteJson(resolvePaths().defaultsConfig, value);
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* 备份 defaults.json 为 defaults_YYYYMMDDhhmmss.json。文件不存在时为 no-op。
|
|
83
|
+
* 同秒重复调用会被覆盖(同一秒内的内容相同,可接受)。
|
|
84
|
+
*/
|
|
85
|
+
function backupDefaults(filePath) {
|
|
86
|
+
if (!fs.existsSync(filePath))
|
|
87
|
+
return;
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const ts = now.getFullYear().toString()
|
|
90
|
+
+ String(now.getMonth() + 1).padStart(2, '0')
|
|
91
|
+
+ String(now.getDate()).padStart(2, '0')
|
|
92
|
+
+ String(now.getHours()).padStart(2, '0')
|
|
93
|
+
+ String(now.getMinutes()).padStart(2, '0')
|
|
94
|
+
+ String(now.getSeconds()).padStart(2, '0');
|
|
95
|
+
const backupPath = path.join(path.dirname(filePath), `defaults_${ts}.json`);
|
|
96
|
+
try {
|
|
97
|
+
fs.copyFileSync(filePath, backupPath);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
logger.warn(`[config] backup failed: ${backupPath}: ${e}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 安全写入 defaults.json:备份现有文件 → 深合并 patch → 原子写入。
|
|
105
|
+
*
|
|
106
|
+
* 与 saveDefaults() 不同,本函数保留现有字段,仅覆盖 patch 中显式指定的字段。
|
|
107
|
+
* 适用场景:evolclaw init 仅修改 active_baseagent/baseagents 时,不应丢失 chatmode/projects 等其它字段。
|
|
108
|
+
*/
|
|
109
|
+
export function saveDefaultsSafe(patch) {
|
|
110
|
+
const p = resolvePaths().defaultsConfig;
|
|
111
|
+
let existing = null;
|
|
112
|
+
try {
|
|
113
|
+
existing = atomicReadJson(p);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
logger.warn(`[config] existing defaults.json unparsable, will be backed up and replaced: ${e}`);
|
|
117
|
+
}
|
|
118
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
119
|
+
backupDefaults(p);
|
|
120
|
+
const merged = existing
|
|
121
|
+
? deepMergeObject(existing, patch)
|
|
122
|
+
: { $schema_version: CONFIG_SCHEMA_VERSION, ...patch };
|
|
123
|
+
atomicWriteJson(p, merged);
|
|
124
|
+
}
|
|
80
125
|
export function loadProcessConfig() {
|
|
81
126
|
const raw = atomicReadJson(resolvePaths().processConfig);
|
|
82
127
|
if (raw === null)
|
|
@@ -303,9 +348,30 @@ export function loadAgent(aid) {
|
|
|
303
348
|
if (raw.aid !== aid) {
|
|
304
349
|
throw new Error(`[config] ${p}: aid field "${raw.aid}" != directory name "${aid}"`);
|
|
305
350
|
}
|
|
306
|
-
|
|
351
|
+
const cfg = expandEnvRefs(raw);
|
|
352
|
+
if (cfg.projects?.defaultPath) {
|
|
353
|
+
cfg.projects.defaultPath = cfg.projects.defaultPath.replace(/[/\\]+$/, '');
|
|
354
|
+
}
|
|
355
|
+
return cfg;
|
|
307
356
|
}
|
|
308
357
|
export function saveAgent(value) {
|
|
358
|
+
if (!isValidAid(value.aid)) {
|
|
359
|
+
throw new Error(`[config] saveAgent: invalid aid "${value.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
|
|
360
|
+
}
|
|
361
|
+
if (value.owners) {
|
|
362
|
+
for (const o of value.owners) {
|
|
363
|
+
if (!isValidAid(o)) {
|
|
364
|
+
throw new Error(`[config] saveAgent: invalid owner AID "${o}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (value.admins) {
|
|
369
|
+
for (const a of value.admins) {
|
|
370
|
+
if (!isValidAid(a)) {
|
|
371
|
+
throw new Error(`[config] saveAgent: invalid admin AID "${a}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
309
375
|
atomicWriteJson(agentConfigPath(value.aid), value);
|
|
310
376
|
}
|
|
311
377
|
/**
|
|
@@ -128,18 +128,18 @@ export class ChannelLoader {
|
|
|
128
128
|
}
|
|
129
129
|
const SEP = '#';
|
|
130
130
|
export function formatChannelKey(k) {
|
|
131
|
-
return `${k.
|
|
131
|
+
return `${k.type}${SEP}${encodeURIComponent(k.selfPeerId)}${SEP}${k.name}`;
|
|
132
132
|
}
|
|
133
133
|
export function parseChannelKey(key) {
|
|
134
134
|
const parts = key.split(SEP);
|
|
135
135
|
if (parts.length !== 3) {
|
|
136
136
|
throw new Error(`Invalid channel key (expected 3 segments separated by '#'): ${key}`);
|
|
137
137
|
}
|
|
138
|
-
const [
|
|
139
|
-
if (!
|
|
138
|
+
const [type, encodedSelfPeerId, name] = parts;
|
|
139
|
+
if (!type || !encodedSelfPeerId || !name) {
|
|
140
140
|
throw new Error(`Invalid channel key (empty segment): ${key}`);
|
|
141
141
|
}
|
|
142
|
-
return {
|
|
142
|
+
return { type, selfPeerId: decodeURIComponent(encodedSelfPeerId), name };
|
|
143
143
|
}
|
|
144
144
|
export function tryParseChannelKey(key) {
|
|
145
145
|
try {
|