evolclaw 3.1.4 → 3.1.5

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/claude-runner.js +348 -156
  3. package/dist/agents/kit-renderer.js +176 -21
  4. package/dist/aun/aid/agentmd.js +68 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/p2p.js +26 -2
  10. package/dist/aun/rpc/connection.js +23 -30
  11. package/dist/channels/aun.js +77 -88
  12. package/dist/channels/dingtalk.js +1 -0
  13. package/dist/channels/feishu.js +270 -190
  14. package/dist/channels/qqbot.js +1 -0
  15. package/dist/channels/wechat.js +1 -0
  16. package/dist/channels/wecom.js +1 -0
  17. package/dist/cli/agent.js +11 -5
  18. package/dist/cli/bench.js +40 -23
  19. package/dist/cli/index.js +170 -44
  20. package/dist/cli/init-channel.js +5 -1
  21. package/dist/cli/model.js +324 -0
  22. package/dist/cli/net-check.js +133 -50
  23. package/dist/cli/watch-msg.js +7 -7
  24. package/dist/cli/watch-web/debug-log.js +18 -0
  25. package/dist/cli/watch-web/server.js +306 -0
  26. package/dist/cli/watch-web/sources/aid.js +63 -0
  27. package/dist/cli/watch-web/sources/msg.js +70 -0
  28. package/dist/cli/watch-web/sources/session.js +638 -0
  29. package/dist/cli/watch-web/sources/types.js +10 -0
  30. package/dist/cli/watch-web/static/app.js +546 -0
  31. package/dist/cli/watch-web/static/index.html +54 -0
  32. package/dist/cli/watch-web/static/style.css +247 -0
  33. package/dist/core/channel-loader.js +7 -4
  34. package/dist/core/command-handler.js +81 -86
  35. package/dist/core/evolagent-registry.js +1 -1
  36. package/dist/core/evolagent.js +4 -4
  37. package/dist/core/interaction-router.js +59 -0
  38. package/dist/core/message/message-bridge.js +6 -6
  39. package/dist/core/message/message-log.js +2 -2
  40. package/dist/core/message/message-processor.js +86 -101
  41. package/dist/core/message/stream-idle-monitor.js +21 -0
  42. package/dist/core/model/model-catalog.js +215 -0
  43. package/dist/core/model/model-scope.js +250 -0
  44. package/dist/core/relation/peer-identity.js +40 -49
  45. package/dist/core/relation/peer-key.js +16 -0
  46. package/dist/core/session/session-fs-store.js +34 -55
  47. package/dist/core/session/session-key.js +24 -0
  48. package/dist/core/session/session-manager.js +308 -251
  49. package/dist/core/session/session-mapper.js +9 -4
  50. package/dist/core/trigger/manager.js +3 -3
  51. package/dist/core/trigger/scheduler.js +2 -1
  52. package/dist/index.js +6 -2
  53. package/dist/ipc.js +22 -0
  54. package/kits/docs/GUIDE.md +2 -2
  55. package/kits/docs/INDEX.md +11 -7
  56. package/kits/docs/channels/aun.md +56 -17
  57. package/kits/docs/channels/feishu.md +41 -12
  58. package/kits/docs/context-assembly.md +181 -0
  59. package/kits/docs/evolclaw/agent.md +49 -0
  60. package/kits/docs/evolclaw/aid.md +49 -0
  61. package/kits/docs/evolclaw/ctl.md +46 -0
  62. package/kits/docs/evolclaw/group.md +82 -0
  63. package/kits/docs/evolclaw/msg.md +86 -0
  64. package/kits/docs/evolclaw/rpc.md +35 -0
  65. package/kits/docs/evolclaw/storage.md +49 -0
  66. package/kits/docs/venues/aun-group.md +10 -0
  67. package/kits/docs/venues/aun-private.md +10 -0
  68. package/kits/docs/venues/client-desktop.md +10 -0
  69. package/kits/docs/venues/client-mobile.md +10 -0
  70. package/kits/docs/venues/feishu-group.md +13 -0
  71. package/kits/docs/venues/feishu-private.md +9 -0
  72. package/kits/docs/venues/group.md +11 -0
  73. package/kits/docs/venues/private.md +10 -0
  74. package/kits/eck_manifest.json +72 -36
  75. package/kits/rules/01-overview.md +20 -10
  76. package/kits/rules/06-channel.md +30 -27
  77. package/kits/templates/system-fragments/session.md +10 -3
  78. package/kits/templates/system-fragments/venue.md +9 -0
  79. package/package.json +11 -6
  80. package/dist/aun/aid/lifecycle-log.js +0 -33
  81. package/dist/utils/aid-lifecycle-log.js +0 -33
  82. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  83. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  84. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  85. package/kits/docs/evolclaw/tools.md +0 -25
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Watch Web 服务 — 本地浏览器监控面板的后端。
3
+ *
4
+ * - HTTP: 静态资源 + 配对 API
5
+ * - WebSocket: 订阅式实时推送(aid / msg / session)
6
+ * - 鉴权: 6 位配对码(5 分钟有效)→ token(24h,有访问自动续期),持久化到磁盘
7
+ * - 安全: 绑定 0.0.0.0(支持远程访问),token 校验,只读
8
+ *
9
+ * 借鉴 Kite 控制台:配对码换 token、首消息鉴权、订阅式推送、访问日志。
10
+ */
11
+ import http from 'http';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import crypto from 'crypto';
15
+ import { fileURLToPath } from 'url';
16
+ import { WebSocketServer } from 'ws';
17
+ import { resolvePaths } from '../../paths.js';
18
+ import { setDebugLog } from './debug-log.js';
19
+ import { aidSource } from './sources/aid.js';
20
+ import { msgSource } from './sources/msg.js';
21
+ import { sessionSource } from './sources/session.js';
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const STATIC_DIR = path.join(__dirname, 'static');
24
+ const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 小时
25
+ const PAIRING_TTL_MS = 5 * 60 * 1000; // 配对码 5 分钟
26
+ const DEFAULT_PORT = 20030;
27
+ const SOURCES = {
28
+ aid: aidSource,
29
+ msg: msgSource,
30
+ session: sessionSource,
31
+ };
32
+ // ── Token 持久化 ──
33
+ function tokenStorePath() {
34
+ return path.join(resolvePaths().instanceDir, 'watch-web-tokens.json');
35
+ }
36
+ function loadTokens() {
37
+ try {
38
+ const raw = fs.readFileSync(tokenStorePath(), 'utf-8');
39
+ const store = JSON.parse(raw);
40
+ if (Array.isArray(store.tokens))
41
+ return store;
42
+ }
43
+ catch { /* missing or corrupt */ }
44
+ return { tokens: [] };
45
+ }
46
+ function saveTokens(store) {
47
+ try {
48
+ fs.mkdirSync(resolvePaths().instanceDir, { recursive: true });
49
+ const tmp = tokenStorePath() + '.tmp';
50
+ fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
51
+ fs.renameSync(tmp, tokenStorePath());
52
+ }
53
+ catch { /* best effort */ }
54
+ }
55
+ function pruneExpired(store, now) {
56
+ const before = store.tokens.length;
57
+ store.tokens = store.tokens.filter(t => now - t.lastActive < TOKEN_TTL_MS);
58
+ return store.tokens.length !== before;
59
+ }
60
+ /** 校验 token,命中则续期(更新 lastActive)并持久化 */
61
+ function validateAndRenew(token, now) {
62
+ if (!token)
63
+ return false;
64
+ const store = loadTokens();
65
+ const changed = pruneExpired(store, now);
66
+ const rec = store.tokens.find(t => t.token === token);
67
+ if (rec) {
68
+ rec.lastActive = now;
69
+ saveTokens(store);
70
+ return true;
71
+ }
72
+ if (changed)
73
+ saveTokens(store);
74
+ return false;
75
+ }
76
+ // ── 静态资源 ──
77
+ const MIME = {
78
+ '.html': 'text/html; charset=utf-8',
79
+ '.js': 'text/javascript; charset=utf-8',
80
+ '.css': 'text/css; charset=utf-8',
81
+ '.json': 'application/json; charset=utf-8',
82
+ '.svg': 'image/svg+xml',
83
+ };
84
+ function serveStatic(req, res) {
85
+ let urlPath = (req.url || '/').split('?')[0];
86
+ if (urlPath === '/')
87
+ urlPath = '/index.html';
88
+ // 防目录穿越
89
+ const safe = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
90
+ const file = path.join(STATIC_DIR, safe);
91
+ if (!file.startsWith(STATIC_DIR)) {
92
+ res.writeHead(403).end('Forbidden');
93
+ return;
94
+ }
95
+ fs.readFile(file, (err, data) => {
96
+ if (err) {
97
+ res.writeHead(404).end('Not Found');
98
+ return;
99
+ }
100
+ res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
101
+ res.end(data);
102
+ });
103
+ }
104
+ function genPairingCode() {
105
+ return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
106
+ }
107
+ function clientIp(req) {
108
+ const fwd = req.headers['x-forwarded-for'];
109
+ if (typeof fwd === 'string' && fwd)
110
+ return fwd.split(',')[0].trim();
111
+ return req.socket.remoteAddress || '?';
112
+ }
113
+ export async function startWatchWebServer(opts = {}) {
114
+ const log = opts.log || (() => { });
115
+ setDebugLog(log); // 把日志 writer 注入各 source,建立调试闭环
116
+ const pairingCode = genPairingCode();
117
+ const pairingExpiry = Date.now() + PAIRING_TTL_MS;
118
+ const server = http.createServer((req, res) => {
119
+ const url = req.url || '/';
120
+ if (req.method === 'POST' && url === '/api/pair') {
121
+ handlePair(req, res, pairingCode, pairingExpiry, log);
122
+ return;
123
+ }
124
+ serveStatic(req, res);
125
+ });
126
+ const wss = new WebSocketServer({ noServer: true });
127
+ server.on('upgrade', (req, socket, head) => {
128
+ const { query } = parseUrl(req.url || '');
129
+ const token = query.token || '';
130
+ if (!validateAndRenew(token, Date.now())) {
131
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
132
+ socket.destroy();
133
+ log(`✗ WS 拒绝(无效 token) from ${req.socket.remoteAddress}`);
134
+ return;
135
+ }
136
+ wss.handleUpgrade(req, socket, head, (ws) => {
137
+ handleConnection(ws, req, log);
138
+ });
139
+ });
140
+ const port = await bindPort(server, opts.port ?? DEFAULT_PORT);
141
+ const url = `http://0.0.0.0:${port}`;
142
+ return {
143
+ url,
144
+ port,
145
+ pairingCode,
146
+ close() {
147
+ return new Promise((resolve) => {
148
+ for (const client of wss.clients) {
149
+ try {
150
+ client.close();
151
+ }
152
+ catch { /* ignore */ }
153
+ }
154
+ wss.close();
155
+ server.close(() => resolve());
156
+ });
157
+ },
158
+ };
159
+ }
160
+ // ── 配对 ──
161
+ function handlePair(req, res, pairingCode, pairingExpiry, log) {
162
+ let body = '';
163
+ req.on('data', (chunk) => {
164
+ body += chunk;
165
+ if (body.length > 4096)
166
+ req.destroy();
167
+ });
168
+ req.on('end', () => {
169
+ const ip = clientIp(req);
170
+ let code = '';
171
+ try {
172
+ code = String(JSON.parse(body).code || '');
173
+ }
174
+ catch { /* bad json */ }
175
+ if (Date.now() > pairingExpiry) {
176
+ res.writeHead(403, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ ok: false, reason: '配对码已过期,请重启 watch web' }));
178
+ log(`✗ 配对失败(码已过期) from ${ip}`);
179
+ return;
180
+ }
181
+ if (code !== pairingCode) {
182
+ res.writeHead(403, { 'Content-Type': 'application/json' });
183
+ res.end(JSON.stringify({ ok: false, reason: '配对码错误' }));
184
+ log(`✗ 配对失败(码错误: ${code}) from ${ip}`);
185
+ return;
186
+ }
187
+ // 配对成功,发放持久 token
188
+ const now = Date.now();
189
+ const token = crypto.randomBytes(32).toString('hex');
190
+ const store = loadTokens();
191
+ pruneExpired(store, now);
192
+ store.tokens.push({ token, createdAt: now, lastActive: now, label: ip });
193
+ saveTokens(store);
194
+ res.writeHead(200, { 'Content-Type': 'application/json' });
195
+ res.end(JSON.stringify({ ok: true, token }));
196
+ log(`✓ 配对成功 from ${ip}(token 已缓存,24h 有效)`);
197
+ });
198
+ }
199
+ // ── URL 解析 ──
200
+ function parseUrl(rawUrl) {
201
+ const qIdx = rawUrl.indexOf('?');
202
+ if (qIdx === -1)
203
+ return { path: rawUrl, query: {} };
204
+ const query = {};
205
+ for (const pair of rawUrl.slice(qIdx + 1).split('&')) {
206
+ const [k, v] = pair.split('=');
207
+ if (k)
208
+ query[decodeURIComponent(k)] = decodeURIComponent(v || '');
209
+ }
210
+ return { path: rawUrl.slice(0, qIdx), query };
211
+ }
212
+ // ── WebSocket 连接 ──
213
+ function handleConnection(ws, req, log) {
214
+ const ip = clientIp(req);
215
+ let unsubscribe = null;
216
+ let currentView = null;
217
+ log(`◆ WS 连接 from ${ip}`);
218
+ const send = (obj) => {
219
+ if (ws.readyState === ws.OPEN) {
220
+ try {
221
+ ws.send(JSON.stringify(obj));
222
+ }
223
+ catch { /* ignore */ }
224
+ }
225
+ };
226
+ const switchSubscription = async (view, params) => {
227
+ if (unsubscribe) {
228
+ unsubscribe();
229
+ unsubscribe = null;
230
+ }
231
+ currentView = view;
232
+ const source = SOURCES[view];
233
+ if (!source) {
234
+ send({ type: 'error', message: `unknown view: ${view}` });
235
+ return;
236
+ }
237
+ try {
238
+ const snap = await source.snapshot(params);
239
+ send({ type: 'snapshot', view, data: snap });
240
+ }
241
+ catch (e) {
242
+ send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
243
+ }
244
+ unsubscribe = source.subscribe(params, (data) => {
245
+ if (currentView === view)
246
+ send({ type: 'delta', view, data });
247
+ });
248
+ };
249
+ ws.on('message', async (raw) => {
250
+ let msg;
251
+ try {
252
+ msg = JSON.parse(raw.toString());
253
+ }
254
+ catch {
255
+ return;
256
+ }
257
+ if (msg.type === 'ping') {
258
+ send({ type: 'pong' });
259
+ return;
260
+ }
261
+ if (msg.type === 'subscribe' && msg.view) {
262
+ const params = {};
263
+ if (msg.aid)
264
+ params.aid = msg.aid;
265
+ if (msg.peer)
266
+ params.peer = msg.peer;
267
+ if (msg.sessionId)
268
+ params.sessionId = msg.sessionId;
269
+ if (msg.project)
270
+ params.project = msg.project;
271
+ log(`▸ 订阅 ${msg.view}` +
272
+ `${msg.project ? ` project=${String(msg.project).slice(-24)}` : ''}` +
273
+ `${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}` +
274
+ `${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}` +
275
+ `${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''} from ${ip}`);
276
+ await switchSubscription(msg.view, params);
277
+ }
278
+ });
279
+ ws.on('close', () => {
280
+ if (unsubscribe) {
281
+ unsubscribe();
282
+ unsubscribe = null;
283
+ }
284
+ log(`◇ WS 断开 from ${ip}`);
285
+ });
286
+ ws.on('error', () => { });
287
+ }
288
+ // ── 端口绑定(首选端口被占则 +1,最多尝试 10 次)──
289
+ function bindPort(server, preferred) {
290
+ return new Promise((resolve, reject) => {
291
+ let attempt = 0;
292
+ const tryBind = (port) => {
293
+ server.once('error', (err) => {
294
+ if (err.code === 'EADDRINUSE' && attempt < 10) {
295
+ attempt++;
296
+ tryBind(port + 1);
297
+ }
298
+ else {
299
+ reject(err);
300
+ }
301
+ });
302
+ server.listen(port, '0.0.0.0', () => resolve(port));
303
+ };
304
+ tryBind(preferred);
305
+ });
306
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * AID 数据源 — 复用 daemon 的 IPC socket(与 `watch aid` 同源)。
3
+ *
4
+ * daemon 运行时:拉 aun-aids / aun-aid-stats / status。
5
+ * daemon 未运行时:降级到 instance-registry 的 scanInstances()。
6
+ * IPC 无推送能力,故用 1s 轮询 + JSON diff,仅在变化时 push。
7
+ */
8
+ import { resolvePaths } from '../../../paths.js';
9
+ import { ipcQuery } from '../../../ipc.js';
10
+ import { scanInstances } from '../../../utils/instance-registry.js';
11
+ async function buildSnapshot() {
12
+ const p = resolvePaths();
13
+ const [aidsResp, statsResp, statusResp, agentsResp] = await Promise.all([
14
+ ipcQuery(p.socket, { type: 'aun-aids' }),
15
+ ipcQuery(p.socket, { type: 'aun-aid-stats' }),
16
+ ipcQuery(p.socket, { type: 'status' }),
17
+ ipcQuery(p.socket, { type: 'evolagent.list' }),
18
+ ]);
19
+ const daemonRunning = aidsResp !== null || statusResp !== null;
20
+ if (!daemonRunning) {
21
+ // 降级:读 instance-registry,标注 daemon 未运行
22
+ const inst = scanInstances();
23
+ const aids = Array.from(inst.aidLastActivity.entries()).map(([aid, info]) => ({
24
+ aid,
25
+ status: info.event === 'disconnected' ? 'disconnected' : 'offline',
26
+ lastActivity: info.ts,
27
+ lastEvent: info.event,
28
+ }));
29
+ return { daemonRunning: false, aids, stats: [], agents: [] };
30
+ }
31
+ return {
32
+ daemonRunning: true,
33
+ aids: aidsResp?.aids ?? [],
34
+ stats: statsResp?.stats ?? [],
35
+ status: statusResp ?? null,
36
+ agents: agentsResp?.agents ?? [],
37
+ };
38
+ }
39
+ export const aidSource = {
40
+ kind: 'aid',
41
+ async snapshot() {
42
+ return buildSnapshot();
43
+ },
44
+ subscribe(_params, push) {
45
+ let lastJson = '';
46
+ let stopped = false;
47
+ const tick = async () => {
48
+ if (stopped)
49
+ return;
50
+ try {
51
+ const snap = await buildSnapshot();
52
+ const json = JSON.stringify(snap);
53
+ if (json !== lastJson) {
54
+ lastJson = json;
55
+ push(snap);
56
+ }
57
+ }
58
+ catch { /* ignore transient IPC errors */ }
59
+ };
60
+ const timer = setInterval(tick, 1000);
61
+ return () => { stopped = true; clearInterval(timer); };
62
+ },
63
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 消息数据源 — 复用 watch-msg.ts 的数据层函数 + fs.watch 文件监听。
3
+ *
4
+ * snapshot:
5
+ * - 无 aid: 返回本地 AID 列表(含收发统计)
6
+ * - 有 aid 无 peer: 返回该 AID 的对端列表 + 全部消息
7
+ * - 有 aid 有 peer: 返回该对端的消息
8
+ * subscribe: fs.watch(sessions/aun, recursive),messages.jsonl 变化时防抖 150ms 后重推。
9
+ */
10
+ import fs from 'fs';
11
+ import { getSessionsAunDir, listLocalAids, loadAidInfo, loadPeerInfos, loadAllMessages, readMessages, } from '../../watch-msg.js';
12
+ function buildSnapshot(params) {
13
+ const aunDir = getSessionsAunDir();
14
+ if (!fs.existsSync(aunDir)) {
15
+ return { aids: [], peers: [], messages: [], aid: null, peer: null };
16
+ }
17
+ const aid = params.aid || null;
18
+ const peer = params.peer || null;
19
+ const aids = listLocalAids(aunDir)
20
+ .map(a => loadAidInfo(aunDir, a))
21
+ .sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut));
22
+ if (!aid) {
23
+ return { aids, peers: [], messages: [], aid: null, peer: null };
24
+ }
25
+ const peers = loadPeerInfos(aunDir, aid);
26
+ let messages;
27
+ if (peer) {
28
+ messages = readMessages(aunDir, aid, peer);
29
+ if (messages.length > 1000)
30
+ messages = messages.slice(-1000);
31
+ }
32
+ else {
33
+ messages = loadAllMessages(aunDir, aid);
34
+ }
35
+ return { aids, peers, messages, aid, peer };
36
+ }
37
+ export const msgSource = {
38
+ kind: 'msg',
39
+ async snapshot(params = {}) {
40
+ return buildSnapshot(params);
41
+ },
42
+ subscribe(params, push) {
43
+ const aunDir = getSessionsAunDir();
44
+ let watcher = null;
45
+ let debounce = null;
46
+ const fire = () => {
47
+ if (debounce)
48
+ clearTimeout(debounce);
49
+ debounce = setTimeout(() => {
50
+ try {
51
+ push(buildSnapshot(params));
52
+ }
53
+ catch { /* ignore */ }
54
+ }, 150);
55
+ };
56
+ try {
57
+ watcher = fs.watch(aunDir, { recursive: true }, (_evt, filename) => {
58
+ if (filename && String(filename).endsWith('messages.jsonl'))
59
+ fire();
60
+ });
61
+ }
62
+ catch { /* aunDir may not exist yet */ }
63
+ return () => {
64
+ if (watcher)
65
+ watcher.close();
66
+ if (debounce)
67
+ clearTimeout(debounce);
68
+ };
69
+ },
70
+ };