evolclaw 3.0.0 → 3.1.1
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 +1 -1
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +47 -12
- package/dist/agents/codex-runner.js +2 -0
- package/dist/agents/gemini-runner.js +9 -9
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/aun/aid/identity.js +28 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/msg/group.js +3 -1
- package/dist/aun/msg/p2p.js +42 -1
- package/dist/channels/aun.js +427 -146
- package/dist/channels/dingtalk.js +3 -1
- package/dist/channels/feishu.js +128 -7
- package/dist/channels/qqbot.js +3 -1
- package/dist/channels/wechat.js +4 -1
- package/dist/channels/wecom.js +3 -1
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +418 -40
- package/dist/cli/init.js +3 -4
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +666 -0
- package/dist/config-store.js +82 -5
- package/dist/core/channel-loader.js +23 -10
- package/dist/core/command-handler.js +127 -99
- package/dist/core/evolagent.js +5 -10
- package/dist/core/message/im-renderer.js +93 -48
- package/dist/core/message/items-formatter.js +11 -4
- package/dist/core/message/message-bridge.js +11 -2
- package/dist/core/message/message-log.js +8 -1
- package/dist/core/message/message-processor.js +194 -127
- package/dist/core/message/message-queue.js +10 -3
- package/dist/core/permission.js +95 -3
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-manager.js +103 -65
- package/dist/core/trigger/manager.js +16 -0
- package/dist/core/trigger/parser.js +110 -0
- package/dist/core/trigger/scheduler.js +7 -1
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +186 -19
- package/dist/net-check.js +640 -0
- package/dist/paths.js +31 -40
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +10 -0
- package/dist/utils/cross-platform.js +17 -8
- package/dist/utils/error-utils.js +27 -15
- package/dist/utils/instance-registry.js +6 -5
- package/dist/utils/log-writer.js +2 -1
- package/dist/utils/logger.js +10 -0
- package/dist/utils/npm-ops.js +35 -3
- package/dist/utils/process-introspect.js +16 -38
- package/dist/utils/stats.js +216 -2
- package/dist/watch-msg.js +26 -11
- package/evolclaw-install-aun.md +14 -2
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +72 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +73 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +7 -5
- package/dist/agents/templates.js +0 -122
- package/dist/data/prompts.md +0 -137
- package/kits/aun/meta.md +0 -25
- package/kits/aun/role.md +0 -25
- package/kits/templates/group.md +0 -20
- package/kits/templates/private.md +0 -9
- package/kits/templates/system-fragments/personal-context.md +0 -3
- package/kits/templates/system-fragments/self-intro.md +0 -5
- package/kits/templates/system-fragments/speaker-intro.md +0 -5
- package/kits/templates/system-fragments/venue-intro.md +0 -5
- /package/kits/{channels → docs/channels}/aun.md +0 -0
- /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
- /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolvePaths } from '../paths.js';
|
|
4
|
+
import { decodeDirSegment, readAllJsonlLines } from '../core/session/session-fs-store.js';
|
|
5
|
+
// ==================== ANSI ====================
|
|
6
|
+
const isTTY = !!process.stdout.isTTY;
|
|
7
|
+
const RST = isTTY ? '\x1b[0m' : '';
|
|
8
|
+
const DIM = isTTY ? '\x1b[2m' : '';
|
|
9
|
+
const BOLD = isTTY ? '\x1b[1m' : '';
|
|
10
|
+
const CYAN = isTTY ? '\x1b[36m' : '';
|
|
11
|
+
const GREEN = isTTY ? '\x1b[32m' : '';
|
|
12
|
+
const BLUE = isTTY ? '\x1b[34m' : '';
|
|
13
|
+
const ORANGE = isTTY ? '\x1b[38;5;208m' : '';
|
|
14
|
+
const MAGENTA = isTTY ? '\x1b[35m' : '';
|
|
15
|
+
const BG_SEL = isTTY ? '\x1b[48;5;236m' : ''; // dark gray background for selected row
|
|
16
|
+
// ==================== Helpers ====================
|
|
17
|
+
function visualWidth(s) {
|
|
18
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
19
|
+
let w = 0;
|
|
20
|
+
for (const ch of stripped) {
|
|
21
|
+
const code = ch.charCodeAt(0);
|
|
22
|
+
w += (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x30ff) ||
|
|
23
|
+
(code >= 0xff00 && code <= 0xffef) ? 2 : 1;
|
|
24
|
+
}
|
|
25
|
+
return w;
|
|
26
|
+
}
|
|
27
|
+
function padRight(s, width) {
|
|
28
|
+
const pad = Math.max(0, width - visualWidth(s));
|
|
29
|
+
return s + ' '.repeat(pad);
|
|
30
|
+
}
|
|
31
|
+
function truncate(s, maxWidth) {
|
|
32
|
+
let w = 0;
|
|
33
|
+
let i = 0;
|
|
34
|
+
for (const ch of s) {
|
|
35
|
+
const code = ch.charCodeAt(0);
|
|
36
|
+
const cw = (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x30ff) ||
|
|
37
|
+
(code >= 0xff00 && code <= 0xffef) ? 2 : 1;
|
|
38
|
+
if (w + cw > maxWidth - 1)
|
|
39
|
+
return s.slice(0, i) + '…';
|
|
40
|
+
w += cw;
|
|
41
|
+
i += ch.length;
|
|
42
|
+
}
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
function wrapText(s, lineWidth, maxLines) {
|
|
46
|
+
const result = [];
|
|
47
|
+
let remaining = s;
|
|
48
|
+
while (remaining.length > 0 && result.length < maxLines) {
|
|
49
|
+
let w = 0;
|
|
50
|
+
let cutIdx = 0;
|
|
51
|
+
for (const ch of remaining) {
|
|
52
|
+
const code = ch.charCodeAt(0);
|
|
53
|
+
const cw = (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x30ff) ||
|
|
54
|
+
(code >= 0xff00 && code <= 0xffef) ? 2 : 1;
|
|
55
|
+
if (w + cw > lineWidth)
|
|
56
|
+
break;
|
|
57
|
+
w += cw;
|
|
58
|
+
cutIdx += ch.length;
|
|
59
|
+
}
|
|
60
|
+
if (cutIdx === 0)
|
|
61
|
+
break;
|
|
62
|
+
const isLast = result.length === maxLines - 1;
|
|
63
|
+
if (isLast && cutIdx < remaining.length) {
|
|
64
|
+
result.push(truncate(remaining, lineWidth));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
result.push(remaining.slice(0, cutIdx));
|
|
68
|
+
}
|
|
69
|
+
remaining = remaining.slice(cutIdx);
|
|
70
|
+
}
|
|
71
|
+
if (result.length === 0)
|
|
72
|
+
result.push('');
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function formatTimeAgo(ms) {
|
|
76
|
+
const sec = Math.floor(ms / 1000);
|
|
77
|
+
if (sec < 60)
|
|
78
|
+
return `${sec}s`;
|
|
79
|
+
const min = Math.floor(sec / 60);
|
|
80
|
+
if (min < 60)
|
|
81
|
+
return `${min}m`;
|
|
82
|
+
const hour = Math.floor(min / 60);
|
|
83
|
+
if (hour < 24)
|
|
84
|
+
return `${hour}h`;
|
|
85
|
+
return `${Math.floor(hour / 24)}d`;
|
|
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
|
+
}
|
|
115
|
+
function formatNumber(n) {
|
|
116
|
+
return n.toLocaleString('en-US');
|
|
117
|
+
}
|
|
118
|
+
function formatTime(ts) {
|
|
119
|
+
const d = new Date(ts);
|
|
120
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
121
|
+
}
|
|
122
|
+
function formatDateTime(ts) {
|
|
123
|
+
const d = new Date(ts);
|
|
124
|
+
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
125
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
126
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
127
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
128
|
+
return `${mo}-${dd} ${hh}:${mm}`;
|
|
129
|
+
}
|
|
130
|
+
function shortAid(aid) {
|
|
131
|
+
return aid.split('.')[0];
|
|
132
|
+
}
|
|
133
|
+
// ==================== Data Layer ====================
|
|
134
|
+
function getSessionsAunDir() {
|
|
135
|
+
const p = resolvePaths();
|
|
136
|
+
return path.join(p.sessionsDir, 'aun');
|
|
137
|
+
}
|
|
138
|
+
function listLocalAids(aunDir) {
|
|
139
|
+
try {
|
|
140
|
+
return fs.readdirSync(aunDir, { withFileTypes: true })
|
|
141
|
+
.filter(e => e.isDirectory())
|
|
142
|
+
.map(e => decodeDirSegment(e.name));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function listPeers(aunDir, localAid) {
|
|
149
|
+
const aidDir = path.join(aunDir, encodeSegment(localAid));
|
|
150
|
+
try {
|
|
151
|
+
return fs.readdirSync(aidDir, { withFileTypes: true })
|
|
152
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('_'))
|
|
153
|
+
.map(e => decodeDirSegment(e.name));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function readMessages(aunDir, localAid, peerId) {
|
|
160
|
+
const msgPath = path.join(aunDir, encodeSegment(localAid), encodeSegment(peerId), 'messages.jsonl');
|
|
161
|
+
return readAllJsonlLines(msgPath);
|
|
162
|
+
}
|
|
163
|
+
function readPeerName(aunDir, localAid, peerId) {
|
|
164
|
+
const activePath = path.join(aunDir, encodeSegment(localAid), encodeSegment(peerId), 'active.json');
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(fs.readFileSync(activePath, 'utf-8'));
|
|
167
|
+
return data?.metadata?.peerName || null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function encodeSegment(s) {
|
|
174
|
+
return s.replace(/[/%\\:*?"<>|]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
175
|
+
}
|
|
176
|
+
function loadAidInfo(aunDir, aid) {
|
|
177
|
+
const peers = listPeers(aunDir, aid);
|
|
178
|
+
let totalIn = 0, totalOut = 0;
|
|
179
|
+
for (const peer of peers) {
|
|
180
|
+
const msgs = readMessages(aunDir, aid, peer);
|
|
181
|
+
for (const m of msgs) {
|
|
182
|
+
if (m.dir === 'in')
|
|
183
|
+
totalIn++;
|
|
184
|
+
else
|
|
185
|
+
totalOut++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { aid, totalIn, totalOut, peerCount: peers.length };
|
|
189
|
+
}
|
|
190
|
+
function loadPeerInfos(aunDir, localAid) {
|
|
191
|
+
const peers = listPeers(aunDir, localAid);
|
|
192
|
+
const infos = [];
|
|
193
|
+
for (const peerId of peers) {
|
|
194
|
+
const msgs = readMessages(aunDir, localAid, peerId);
|
|
195
|
+
let inbound = 0, outbound = 0, lastAt = 0;
|
|
196
|
+
for (const m of msgs) {
|
|
197
|
+
if (m.dir === 'in')
|
|
198
|
+
inbound++;
|
|
199
|
+
else
|
|
200
|
+
outbound++;
|
|
201
|
+
if (m.ts > lastAt)
|
|
202
|
+
lastAt = m.ts;
|
|
203
|
+
}
|
|
204
|
+
const peerName = readPeerName(aunDir, localAid, peerId);
|
|
205
|
+
infos.push({ peerId, peerName, inbound, outbound, lastAt });
|
|
206
|
+
}
|
|
207
|
+
infos.sort((a, b) => b.lastAt - a.lastAt);
|
|
208
|
+
return infos;
|
|
209
|
+
}
|
|
210
|
+
function loadAllMessages(aunDir, localAid) {
|
|
211
|
+
const peers = listPeers(aunDir, localAid);
|
|
212
|
+
const all = [];
|
|
213
|
+
for (const peer of peers) {
|
|
214
|
+
all.push(...readMessages(aunDir, localAid, peer));
|
|
215
|
+
}
|
|
216
|
+
all.sort((a, b) => a.ts - b.ts);
|
|
217
|
+
if (all.length > 1000)
|
|
218
|
+
return all.slice(-1000);
|
|
219
|
+
return all;
|
|
220
|
+
}
|
|
221
|
+
// ==================== Rendering ====================
|
|
222
|
+
function renderScrollbar(totalLines, visibleLines, offset, height) {
|
|
223
|
+
if (totalLines <= visibleLines)
|
|
224
|
+
return Array(height).fill(' ');
|
|
225
|
+
const thumbSize = Math.max(1, Math.floor(height * visibleLines / totalLines));
|
|
226
|
+
const maxOffset = totalLines - visibleLines;
|
|
227
|
+
const thumbPos = Math.floor((maxOffset - offset) / maxOffset * (height - thumbSize));
|
|
228
|
+
const bar = [];
|
|
229
|
+
for (let i = 0; i < height; i++) {
|
|
230
|
+
bar.push(i >= thumbPos && i < thumbPos + thumbSize ? `${DIM}█${RST}` : `${DIM}░${RST}`);
|
|
231
|
+
}
|
|
232
|
+
return bar;
|
|
233
|
+
}
|
|
234
|
+
function renderScopePanel(state, width, height) {
|
|
235
|
+
const lines = [];
|
|
236
|
+
const title = `${DIM}─ Scope ─${RST}`;
|
|
237
|
+
lines.push(padRight(title, width));
|
|
238
|
+
const isActive = state.activePanel === 'scope';
|
|
239
|
+
for (let i = 0; i < state.localAids.length && lines.length < height; i++) {
|
|
240
|
+
const a = state.localAids[i];
|
|
241
|
+
const sel = isActive && i === state.scopeIndex;
|
|
242
|
+
const chosen = state.selectedLocalAid === a.aid;
|
|
243
|
+
const bg = sel ? BG_SEL : '';
|
|
244
|
+
const marker = sel ? `${bg}${CYAN}${BOLD}▸ ` : (chosen ? `${CYAN} ` : ' ');
|
|
245
|
+
const name = truncate(shortAid(a.aid), width - 4);
|
|
246
|
+
lines.push(padRight(`${marker}${name}${RST}`, width));
|
|
247
|
+
const statsBg = sel ? BG_SEL : '';
|
|
248
|
+
const stats = `${statsBg} ${DIM}↓${a.totalIn} ↑${a.totalOut} peers:${a.peerCount}${RST}`;
|
|
249
|
+
lines.push(padRight(stats, width));
|
|
250
|
+
if (lines.length < height)
|
|
251
|
+
lines.push(padRight('', width));
|
|
252
|
+
}
|
|
253
|
+
while (lines.length < height)
|
|
254
|
+
lines.push(padRight('', width));
|
|
255
|
+
return lines.slice(0, height);
|
|
256
|
+
}
|
|
257
|
+
function renderStatsPanel(state, width, height) {
|
|
258
|
+
const lines = [];
|
|
259
|
+
const title = `${DIM}─ Stats ─${RST}`;
|
|
260
|
+
lines.push(padRight(title, width));
|
|
261
|
+
if (!state.selectedLocalAid) {
|
|
262
|
+
lines.push(padRight(`${DIM} select an AID${RST}`, width));
|
|
263
|
+
while (lines.length < height)
|
|
264
|
+
lines.push(padRight('', width));
|
|
265
|
+
return lines.slice(0, height);
|
|
266
|
+
}
|
|
267
|
+
const isActive = state.activePanel === 'stats';
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
// "All" item at index 0
|
|
270
|
+
const allSel = isActive && state.statsIndex === 0;
|
|
271
|
+
const allBg = allSel ? BG_SEL : '';
|
|
272
|
+
const allMarker = allSel ? `${allBg}${CYAN}${BOLD}▸ ` : ' ';
|
|
273
|
+
lines.push(padRight(`${allMarker}All (${state.peers.length} peers)${RST}`, width));
|
|
274
|
+
if (lines.length < height)
|
|
275
|
+
lines.push(padRight('', width));
|
|
276
|
+
for (let i = 0; i < state.peers.length && lines.length < height; i++) {
|
|
277
|
+
const p = state.peers[i];
|
|
278
|
+
const sel = isActive && state.statsIndex === i + 1;
|
|
279
|
+
const bg = sel ? BG_SEL : '';
|
|
280
|
+
const marker = sel ? `${bg}${CYAN}${BOLD}▸ ` : ' ';
|
|
281
|
+
const displayName = p.peerName || shortAid(p.peerId);
|
|
282
|
+
const name = truncate(displayName, width - 4);
|
|
283
|
+
lines.push(padRight(`${marker}${name}${RST}`, width));
|
|
284
|
+
const detailBg = sel ? BG_SEL : '';
|
|
285
|
+
const ago = p.lastAt ? formatTimeAgo(now - p.lastAt) : '-';
|
|
286
|
+
const detail = `${detailBg} ${DIM}↓${p.inbound} ↑${p.outbound} ${ago}${RST}`;
|
|
287
|
+
lines.push(padRight(detail, width));
|
|
288
|
+
if (lines.length < height)
|
|
289
|
+
lines.push(padRight('', width));
|
|
290
|
+
}
|
|
291
|
+
while (lines.length < height)
|
|
292
|
+
lines.push(padRight('', width));
|
|
293
|
+
return lines.slice(0, height);
|
|
294
|
+
}
|
|
295
|
+
// ==================== Messages Panel ====================
|
|
296
|
+
function renderMessagesPanel(state, width, height) {
|
|
297
|
+
const lines = [];
|
|
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}`;
|
|
301
|
+
lines.push(padRight(title, width));
|
|
302
|
+
const contentHeight = height - 1;
|
|
303
|
+
const msgs = state.messages;
|
|
304
|
+
const totalMsgs = msgs.length;
|
|
305
|
+
const msgWidth = width - 3;
|
|
306
|
+
const contentLineWidth = msgWidth - 2;
|
|
307
|
+
const maxContentLines = 3;
|
|
308
|
+
// 先构造每条消息的渲染行,从最末尾的可见消息往回收集,直到填满 contentHeight
|
|
309
|
+
function renderOneMsg(m) {
|
|
310
|
+
const time = formatDateTime(m.ts);
|
|
311
|
+
const dir = m.dir === 'in' ? `${GREEN}↓${RST}` : `${BLUE}↑${RST}`;
|
|
312
|
+
const isGroup = m.chatType === 'group';
|
|
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 source = m.source === 'cli' ? 'cli' : 'daemon';
|
|
320
|
+
const method = m.msgType === 'thought' ? 'thought' : 'send';
|
|
321
|
+
typeTag = `${DIM}[${source}|${method}]${RST}`;
|
|
322
|
+
}
|
|
323
|
+
const byteLen = Buffer.byteLength(m.content, 'utf-8');
|
|
324
|
+
const lenTag = `${DIM}${formatNumber(byteLen)}B${RST}`;
|
|
325
|
+
const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from.split('.')[0];
|
|
326
|
+
const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to.split('.')[0];
|
|
327
|
+
const header = `${DIM}${time}${RST} ${dir}${chatTag}${metaTags}${typeTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
|
|
328
|
+
const out = [padRight(header, msgWidth)];
|
|
329
|
+
const rawContent = m.content.replace(/\n/g, ' ');
|
|
330
|
+
const wrappedLines = wrapText(rawContent, contentLineWidth, maxContentLines);
|
|
331
|
+
for (const wl of wrappedLines) {
|
|
332
|
+
out.push(padRight(` ${wl}`, msgWidth));
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
// 从 endIdx-1 开始倒序,往回累积,直到行数填满 contentHeight
|
|
337
|
+
const endIdx = Math.max(0, totalMsgs - state.messageScrollOffset);
|
|
338
|
+
const collected = []; // 每条消息的行数组
|
|
339
|
+
let totalLines = 0;
|
|
340
|
+
let firstShownIdx = endIdx; // 首条可见消息的下标
|
|
341
|
+
for (let i = endIdx - 1; i >= 0; i--) {
|
|
342
|
+
const rendered = renderOneMsg(msgs[i]);
|
|
343
|
+
if (totalLines + rendered.length > contentHeight && collected.length > 0)
|
|
344
|
+
break;
|
|
345
|
+
collected.unshift(rendered);
|
|
346
|
+
totalLines += rendered.length;
|
|
347
|
+
firstShownIdx = i;
|
|
348
|
+
if (totalLines >= contentHeight)
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const visibleMsgCount = endIdx - firstShownIdx;
|
|
352
|
+
const scrollbar = renderScrollbar(totalMsgs, visibleMsgCount, state.messageScrollOffset, contentHeight);
|
|
353
|
+
// 正序输出(旧→新)
|
|
354
|
+
for (const rendered of collected) {
|
|
355
|
+
for (const line of rendered) {
|
|
356
|
+
if (lines.length - 1 >= contentHeight)
|
|
357
|
+
break;
|
|
358
|
+
const sbIdx = lines.length - 1;
|
|
359
|
+
lines.push(`${line} ${scrollbar[sbIdx] || ' '}`);
|
|
360
|
+
}
|
|
361
|
+
if (lines.length - 1 >= contentHeight)
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
while (lines.length < height) {
|
|
365
|
+
const sbIdx = lines.length - 1;
|
|
366
|
+
lines.push(padRight('', msgWidth) + ` ${scrollbar[sbIdx] || ' '}`);
|
|
367
|
+
}
|
|
368
|
+
return lines.slice(0, height);
|
|
369
|
+
}
|
|
370
|
+
// ==================== Main Render ====================
|
|
371
|
+
function renderFrame(state) {
|
|
372
|
+
const cols = process.stdout.columns || 120;
|
|
373
|
+
const rows = (process.stdout.rows || 40);
|
|
374
|
+
const bodyHeight = rows - 4;
|
|
375
|
+
const leftW = Math.max(20, Math.floor(cols * 0.20));
|
|
376
|
+
const midW = Math.max(24, Math.floor(cols * 0.22));
|
|
377
|
+
const rightW = Math.max(40, cols - leftW - midW - 4);
|
|
378
|
+
const leftLines = renderScopePanel(state, leftW, bodyHeight);
|
|
379
|
+
const midLines = renderStatsPanel(state, midW, bodyHeight);
|
|
380
|
+
const msgLines = renderMessagesPanel(state, rightW, bodyHeight);
|
|
381
|
+
const sep = `${DIM}│${RST}`;
|
|
382
|
+
let buf = '\x1b[H';
|
|
383
|
+
const topBorder = `${DIM}┌${'─'.repeat(leftW)}┬${'─'.repeat(midW)}┬${'─'.repeat(rightW + 1)}┐${RST}`;
|
|
384
|
+
buf += `\x1b[2K${topBorder}\n`;
|
|
385
|
+
for (let i = 0; i < bodyHeight; i++) {
|
|
386
|
+
const l = leftLines[i] || padRight('', leftW);
|
|
387
|
+
const m = midLines[i] || padRight('', midW);
|
|
388
|
+
const r = msgLines[i] || padRight('', rightW);
|
|
389
|
+
buf += `\x1b[2K${sep}${l}${sep}${m}${sep}${r}${sep}\n`;
|
|
390
|
+
}
|
|
391
|
+
const bottomBorder = `${DIM}├${'─'.repeat(leftW)}┴${'─'.repeat(midW)}┴${'─'.repeat(rightW + 1)}┤${RST}`;
|
|
392
|
+
buf += `\x1b[2K${bottomBorder}\n`;
|
|
393
|
+
const helpText = `Tab: panel ↑↓: nav Enter: select Backspace: back ESC: exit`;
|
|
394
|
+
const helpLine = `${DIM}│ ${helpText.slice(0, cols - 4)} ${RST}`;
|
|
395
|
+
buf += `\x1b[2K${helpLine}\n`;
|
|
396
|
+
const closeBorder = `${DIM}└${'─'.repeat(cols - 2)}┘${RST}`;
|
|
397
|
+
buf += `\x1b[2K${closeBorder}`;
|
|
398
|
+
return buf;
|
|
399
|
+
}
|
|
400
|
+
// ==================== Main ====================
|
|
401
|
+
export async function cmdWatchMsg() {
|
|
402
|
+
const aunDir = getSessionsAunDir();
|
|
403
|
+
if (!fs.existsSync(aunDir)) {
|
|
404
|
+
console.log('No session data found.');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let watcher = null;
|
|
408
|
+
const state = {
|
|
409
|
+
activePanel: 'scope',
|
|
410
|
+
localAids: [],
|
|
411
|
+
scopeIndex: 0,
|
|
412
|
+
selectedLocalAid: null,
|
|
413
|
+
peers: [],
|
|
414
|
+
statsIndex: 0,
|
|
415
|
+
selectedPeer: null,
|
|
416
|
+
messages: [],
|
|
417
|
+
messageScrollOffset: 0,
|
|
418
|
+
dirty: true,
|
|
419
|
+
};
|
|
420
|
+
function loadScope() {
|
|
421
|
+
const aids = listLocalAids(aunDir);
|
|
422
|
+
state.localAids = aids.map(aid => loadAidInfo(aunDir, aid));
|
|
423
|
+
state.localAids.sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut));
|
|
424
|
+
}
|
|
425
|
+
function selectAid(aid) {
|
|
426
|
+
state.selectedLocalAid = aid;
|
|
427
|
+
state.peers = loadPeerInfos(aunDir, aid);
|
|
428
|
+
state.statsIndex = 0;
|
|
429
|
+
state.selectedPeer = null;
|
|
430
|
+
state.messages = loadAllMessages(aunDir, aid);
|
|
431
|
+
state.messageScrollOffset = 0;
|
|
432
|
+
startWatching(aid);
|
|
433
|
+
}
|
|
434
|
+
function selectPeer(peerId) {
|
|
435
|
+
state.selectedPeer = peerId;
|
|
436
|
+
if (!state.selectedLocalAid)
|
|
437
|
+
return;
|
|
438
|
+
if (peerId) {
|
|
439
|
+
state.messages = readMessages(aunDir, state.selectedLocalAid, peerId);
|
|
440
|
+
if (state.messages.length > 1000)
|
|
441
|
+
state.messages = state.messages.slice(-1000);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
|
|
445
|
+
}
|
|
446
|
+
state.messageScrollOffset = 0;
|
|
447
|
+
}
|
|
448
|
+
function startWatching(aid) {
|
|
449
|
+
if (watcher) {
|
|
450
|
+
watcher.close();
|
|
451
|
+
watcher = null;
|
|
452
|
+
}
|
|
453
|
+
const aidDir = path.join(aunDir, encodeSegment(aid));
|
|
454
|
+
try {
|
|
455
|
+
watcher = fs.watch(aidDir, { recursive: true }, (_, filename) => {
|
|
456
|
+
if (filename && filename.endsWith('messages.jsonl')) {
|
|
457
|
+
refreshData();
|
|
458
|
+
render();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch { /* directory may not exist */ }
|
|
463
|
+
}
|
|
464
|
+
function refreshData() {
|
|
465
|
+
if (!state.selectedLocalAid)
|
|
466
|
+
return;
|
|
467
|
+
state.peers = loadPeerInfos(aunDir, state.selectedLocalAid);
|
|
468
|
+
const prevCount = state.messages.length;
|
|
469
|
+
if (state.selectedPeer) {
|
|
470
|
+
state.messages = readMessages(aunDir, state.selectedLocalAid, state.selectedPeer);
|
|
471
|
+
if (state.messages.length > 1000)
|
|
472
|
+
state.messages = state.messages.slice(-1000);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
|
|
476
|
+
}
|
|
477
|
+
// 有新消息时自动滚到底部
|
|
478
|
+
if (state.messages.length > prevCount) {
|
|
479
|
+
state.messageScrollOffset = 0;
|
|
480
|
+
}
|
|
481
|
+
// Also refresh scope stats for the selected AID
|
|
482
|
+
const idx = state.localAids.findIndex(a => a.aid === state.selectedLocalAid);
|
|
483
|
+
if (idx >= 0) {
|
|
484
|
+
state.localAids[idx] = loadAidInfo(aunDir, state.selectedLocalAid);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function render() {
|
|
488
|
+
process.stdout.write(renderFrame(state));
|
|
489
|
+
}
|
|
490
|
+
function cleanup() {
|
|
491
|
+
if (watcher) {
|
|
492
|
+
watcher.close();
|
|
493
|
+
watcher = null;
|
|
494
|
+
}
|
|
495
|
+
clearInterval(pollTimer);
|
|
496
|
+
if (process.stdin.isTTY)
|
|
497
|
+
try {
|
|
498
|
+
process.stdin.setRawMode(false);
|
|
499
|
+
}
|
|
500
|
+
catch { }
|
|
501
|
+
process.stdin.pause();
|
|
502
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
503
|
+
}
|
|
504
|
+
function handleKey(data) {
|
|
505
|
+
// ESC
|
|
506
|
+
if (data[0] === 0x1b && data.length === 1) {
|
|
507
|
+
cleanup();
|
|
508
|
+
process.exit(0);
|
|
509
|
+
}
|
|
510
|
+
// Ctrl+C
|
|
511
|
+
if (data[0] === 0x03) {
|
|
512
|
+
cleanup();
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|
|
515
|
+
// Arrow keys
|
|
516
|
+
if (data[0] === 0x1b && data[1] === 0x5b) {
|
|
517
|
+
const code = data[2];
|
|
518
|
+
if (code === 0x41)
|
|
519
|
+
handleUp();
|
|
520
|
+
else if (code === 0x42)
|
|
521
|
+
handleDown();
|
|
522
|
+
else if (code === 0x43)
|
|
523
|
+
handleRight();
|
|
524
|
+
else if (code === 0x44)
|
|
525
|
+
handleLeft();
|
|
526
|
+
else if (code === 0x35)
|
|
527
|
+
handlePageUp(); // Page Up: \x1b[5~
|
|
528
|
+
else if (code === 0x36)
|
|
529
|
+
handlePageDown(); // Page Down: \x1b[6~
|
|
530
|
+
render();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Tab
|
|
534
|
+
if (data[0] === 0x09) {
|
|
535
|
+
handleRight();
|
|
536
|
+
render();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Shift+Tab (some terminals: \x1b[Z)
|
|
540
|
+
if (data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x5a) {
|
|
541
|
+
handleLeft();
|
|
542
|
+
render();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Enter
|
|
546
|
+
if (data[0] === 0x0d) {
|
|
547
|
+
handleEnter();
|
|
548
|
+
render();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// Backspace
|
|
552
|
+
if (data[0] === 0x7f || data[0] === 0x08) {
|
|
553
|
+
handleBackspace();
|
|
554
|
+
render();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function handleUp() {
|
|
559
|
+
if (state.activePanel === 'scope') {
|
|
560
|
+
state.scopeIndex = Math.max(0, state.scopeIndex - 1);
|
|
561
|
+
}
|
|
562
|
+
else if (state.activePanel === 'stats') {
|
|
563
|
+
state.statsIndex = Math.max(0, state.statsIndex - 1);
|
|
564
|
+
}
|
|
565
|
+
else if (state.activePanel === 'messages') {
|
|
566
|
+
state.messageScrollOffset = Math.min(Math.max(0, state.messages.length - 5), state.messageScrollOffset + 3);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function handleDown() {
|
|
570
|
+
if (state.activePanel === 'scope') {
|
|
571
|
+
state.scopeIndex = Math.min(state.localAids.length - 1, state.scopeIndex + 1);
|
|
572
|
+
}
|
|
573
|
+
else if (state.activePanel === 'stats') {
|
|
574
|
+
state.statsIndex = Math.min(state.peers.length, state.statsIndex + 1);
|
|
575
|
+
}
|
|
576
|
+
else if (state.activePanel === 'messages') {
|
|
577
|
+
state.messageScrollOffset = Math.max(0, state.messageScrollOffset - 3);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function handleLeft() {
|
|
581
|
+
if (state.activePanel === 'messages')
|
|
582
|
+
state.activePanel = 'stats';
|
|
583
|
+
else if (state.activePanel === 'stats')
|
|
584
|
+
state.activePanel = 'scope';
|
|
585
|
+
}
|
|
586
|
+
function handleRight() {
|
|
587
|
+
if (state.activePanel === 'scope')
|
|
588
|
+
state.activePanel = 'stats';
|
|
589
|
+
else if (state.activePanel === 'stats')
|
|
590
|
+
state.activePanel = 'messages';
|
|
591
|
+
}
|
|
592
|
+
function handlePageUp() {
|
|
593
|
+
if (state.activePanel === 'messages') {
|
|
594
|
+
const pageSize = (process.stdout.rows || 40) - 6;
|
|
595
|
+
state.messageScrollOffset = Math.min(Math.max(0, state.messages.length - 5), state.messageScrollOffset + pageSize);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function handlePageDown() {
|
|
599
|
+
if (state.activePanel === 'messages') {
|
|
600
|
+
const pageSize = (process.stdout.rows || 40) - 6;
|
|
601
|
+
state.messageScrollOffset = Math.max(0, state.messageScrollOffset - pageSize);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function handleEnter() {
|
|
605
|
+
if (state.activePanel === 'scope' && state.localAids.length > 0) {
|
|
606
|
+
const aid = state.localAids[state.scopeIndex];
|
|
607
|
+
selectAid(aid.aid);
|
|
608
|
+
state.activePanel = 'stats';
|
|
609
|
+
}
|
|
610
|
+
else if (state.activePanel === 'stats') {
|
|
611
|
+
if (state.statsIndex === 0) {
|
|
612
|
+
selectPeer(null);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const peer = state.peers[state.statsIndex - 1];
|
|
616
|
+
if (peer)
|
|
617
|
+
selectPeer(peer.peerId);
|
|
618
|
+
}
|
|
619
|
+
state.activePanel = 'messages';
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function handleBackspace() {
|
|
623
|
+
if (state.activePanel === 'messages') {
|
|
624
|
+
state.activePanel = 'stats';
|
|
625
|
+
state.messageScrollOffset = 0;
|
|
626
|
+
}
|
|
627
|
+
else if (state.activePanel === 'stats') {
|
|
628
|
+
state.activePanel = 'scope';
|
|
629
|
+
state.selectedLocalAid = null;
|
|
630
|
+
state.peers = [];
|
|
631
|
+
state.messages = [];
|
|
632
|
+
if (watcher) {
|
|
633
|
+
watcher.close();
|
|
634
|
+
watcher = null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// ── Init ──
|
|
639
|
+
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
640
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
641
|
+
loadScope();
|
|
642
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
643
|
+
render();
|
|
644
|
+
// 定时轮询:5 秒检查一次,有变化才刷新
|
|
645
|
+
let lastMsgCount = state.messages.length;
|
|
646
|
+
let lastMsgTs = state.messages.length > 0 ? state.messages[state.messages.length - 1].ts : 0;
|
|
647
|
+
const pollTimer = setInterval(() => {
|
|
648
|
+
if (!state.selectedLocalAid)
|
|
649
|
+
return;
|
|
650
|
+
refreshData();
|
|
651
|
+
const newCount = state.messages.length;
|
|
652
|
+
const newTs = newCount > 0 ? state.messages[newCount - 1].ts : 0;
|
|
653
|
+
if (newCount !== lastMsgCount || newTs !== lastMsgTs) {
|
|
654
|
+
lastMsgCount = newCount;
|
|
655
|
+
lastMsgTs = newTs;
|
|
656
|
+
render();
|
|
657
|
+
}
|
|
658
|
+
}, 5000);
|
|
659
|
+
if (process.stdin.isTTY) {
|
|
660
|
+
process.stdin.setRawMode(true);
|
|
661
|
+
process.stdin.resume();
|
|
662
|
+
process.stdin.on('data', handleKey);
|
|
663
|
+
}
|
|
664
|
+
// Keep process alive
|
|
665
|
+
await new Promise(() => { });
|
|
666
|
+
}
|