evolclaw 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Rich Content Renderer
3
+ *
4
+ * Renders LaTeX formulas and Mermaid diagrams to PNG images using Playwright.
5
+ * KaTeX CSS/JS/fonts and Mermaid JS are loaded from node_modules and injected
6
+ * inline via setContent — no network, CDN, or local server needed.
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { getPackageRoot } from '../paths.js';
11
+ import { logger } from './logger.js';
12
+ // Lazy-loaded Playwright
13
+ let chromium;
14
+ let browserInstance;
15
+ let browserContext;
16
+ // Cached inline resources (loaded once, reused)
17
+ let katexCSS = null;
18
+ let katexJS = null;
19
+ let mermaidJS = null;
20
+ // Dependency availability check (cached)
21
+ let dependenciesAvailable = null;
22
+ function getKatexCSS() {
23
+ if (katexCSS)
24
+ return katexCSS;
25
+ const katexDir = path.join(getPackageRoot(), 'node_modules', 'katex', 'dist');
26
+ let css = fs.readFileSync(path.join(katexDir, 'katex.min.css'), 'utf8');
27
+ // Embed woff2 fonts as data URIs so they work without file:// or http
28
+ css = css.replace(/url\(fonts\/([\w.-]+\.woff2)\)/g, (_m, fontFile) => {
29
+ const fontPath = path.join(katexDir, 'fonts', fontFile);
30
+ if (fs.existsSync(fontPath)) {
31
+ const b64 = fs.readFileSync(fontPath).toString('base64');
32
+ return `url(data:font/woff2;base64,${b64})`;
33
+ }
34
+ return _m;
35
+ });
36
+ katexCSS = css;
37
+ return css;
38
+ }
39
+ function getKatexJS() {
40
+ if (katexJS)
41
+ return katexJS;
42
+ const katexDir = path.join(getPackageRoot(), 'node_modules', 'katex', 'dist');
43
+ katexJS = fs.readFileSync(path.join(katexDir, 'katex.min.js'), 'utf8');
44
+ return katexJS;
45
+ }
46
+ function getMermaidJS() {
47
+ if (mermaidJS)
48
+ return mermaidJS;
49
+ const mermaidDir = path.join(getPackageRoot(), 'node_modules', 'mermaid', 'dist');
50
+ mermaidJS = fs.readFileSync(path.join(mermaidDir, 'mermaid.min.js'), 'utf8');
51
+ return mermaidJS;
52
+ }
53
+ async function getBrowser() {
54
+ if (browserInstance?.isConnected?.())
55
+ return browserInstance;
56
+ try {
57
+ // @ts-ignore - runtime path, not a declared module
58
+ const pw = await import('/root/.claude/node_modules/playwright/index.mjs');
59
+ chromium = pw.chromium;
60
+ }
61
+ catch {
62
+ // @ts-ignore - optional dependency
63
+ const pw = await import('playwright');
64
+ chromium = pw.chromium;
65
+ }
66
+ browserInstance = await chromium.launch({
67
+ headless: false,
68
+ args: ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage'],
69
+ });
70
+ browserContext = await browserInstance.newContext({
71
+ viewport: { width: 1280, height: 800 },
72
+ });
73
+ return browserInstance;
74
+ }
75
+ async function getPage() {
76
+ await getBrowser();
77
+ return browserContext.newPage();
78
+ }
79
+ export async function renderLatex(formula, displayMode = true) {
80
+ const page = await getPage();
81
+ try {
82
+ const css = getKatexCSS();
83
+ const js = getKatexJS();
84
+ const escaped = formula.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
85
+ const html = '<!DOCTYPE html><html><head><style>' + css + '</style>'
86
+ + '<style>body{background:white;margin:0;padding:16px;display:inline-block;}#f{font-size:1.2em;}</style>'
87
+ + '</head><body><div id="f"></div>'
88
+ + '<scr' + 'ipt>' + js + '</scr' + 'ipt>'
89
+ + '<scr' + 'ipt>'
90
+ + 'try{katex.render("' + escaped + '",document.getElementById("f"),'
91
+ + '{displayMode:' + displayMode + ',throwOnError:false});'
92
+ + '}catch(e){document.getElementById("f").textContent="Error: "+e.message;}'
93
+ + '</scr' + 'ipt></body></html>';
94
+ await page.setContent(html, { waitUntil: 'load' });
95
+ await page.waitForTimeout(500);
96
+ const el = page.locator('#f');
97
+ const box = await el.boundingBox();
98
+ if (!box || box.width === 0 || box.height === 0) {
99
+ throw new Error('LaTeX element has zero size');
100
+ }
101
+ return Buffer.from(await el.screenshot({ type: 'png' }));
102
+ }
103
+ finally {
104
+ await page.close();
105
+ }
106
+ }
107
+ export async function renderMermaid(code) {
108
+ const page = await getPage();
109
+ try {
110
+ const js = getMermaidJS();
111
+ const escaped = code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
112
+ const html = '<!DOCTYPE html><html><head>'
113
+ + '<style>body{background:white;margin:0;padding:16px;display:inline-block;}</style>'
114
+ + '</head><body><div id="m"></div>'
115
+ + '<scr' + 'ipt>' + js + '</scr' + 'ipt>'
116
+ + '<scr' + 'ipt>'
117
+ + '(async()=>{'
118
+ + 'mermaid.initialize({startOnLoad:false,theme:"default"});'
119
+ + 'try{const{svg}=await mermaid.render("md",`' + escaped + '`);'
120
+ + 'document.getElementById("m").innerHTML=svg;'
121
+ + '}catch(e){document.getElementById("m").textContent="Error: "+e.message;}'
122
+ + '})();'
123
+ + '</scr' + 'ipt></body></html>';
124
+ await page.setContent(html, { waitUntil: 'load' });
125
+ await page.waitForTimeout(1500);
126
+ const el = page.locator('#m');
127
+ const box = await el.boundingBox();
128
+ if (!box || box.width === 0 || box.height === 0) {
129
+ throw new Error('Mermaid element has zero size');
130
+ }
131
+ return Buffer.from(await el.screenshot({ type: 'png' }));
132
+ }
133
+ finally {
134
+ await page.close();
135
+ }
136
+ }
137
+ export function extractLatex(text) {
138
+ const results = [];
139
+ const blockRe = /\$\$([\s\S]+?)\$\$/g;
140
+ let m;
141
+ while ((m = blockRe.exec(text)) !== null) {
142
+ results.push({
143
+ match: m[0], formula: m[1].trim(), displayMode: true,
144
+ start: m.index, end: m.index + m[0].length,
145
+ });
146
+ }
147
+ const inlineRe = /(?<!\$)\$(?!\$)(.+?)(?<!\$)\$(?!\$)/g;
148
+ while ((m = inlineRe.exec(text)) !== null) {
149
+ const overlaps = results.some(r => m.index >= r.start && m.index < r.end);
150
+ if (!overlaps) {
151
+ results.push({
152
+ match: m[0], formula: m[1].trim(), displayMode: false,
153
+ start: m.index, end: m.index + m[0].length,
154
+ });
155
+ }
156
+ }
157
+ results.sort((a, b) => a.start - b.start);
158
+ return results;
159
+ }
160
+ export function extractMermaid(text) {
161
+ const results = [];
162
+ const re = /```mermaid\s*\n([\s\S]+?)```/g;
163
+ let m;
164
+ while ((m = re.exec(text)) !== null) {
165
+ results.push({ match: m[0], code: m[1].trim(), start: m.index, end: m.index + m[0].length });
166
+ }
167
+ return results;
168
+ }
169
+ export async function renderAllRichContent(text) {
170
+ const latexItems = extractLatex(text);
171
+ const mermaidItems = extractMermaid(text);
172
+ const results = [];
173
+ for (const item of latexItems) {
174
+ try {
175
+ const png = await renderLatex(item.formula, item.displayMode);
176
+ results.push({ match: item.match, start: item.start, end: item.end, type: 'latex', png });
177
+ }
178
+ catch (err) {
179
+ logger.warn('[RichContent] LaTeX render failed: ' + item.formula, err);
180
+ }
181
+ }
182
+ for (const item of mermaidItems) {
183
+ try {
184
+ const png = await renderMermaid(item.code);
185
+ results.push({ match: item.match, start: item.start, end: item.end, type: 'mermaid', png });
186
+ }
187
+ catch (err) {
188
+ logger.warn('[RichContent] Mermaid render failed', err);
189
+ }
190
+ }
191
+ return results;
192
+ }
193
+ /**
194
+ * 检查富内容渲染依赖是否可用(playwright、katex、mermaid)
195
+ */
196
+ export function checkDependencies() {
197
+ if (dependenciesAvailable !== null)
198
+ return dependenciesAvailable;
199
+ try {
200
+ const pkgRoot = getPackageRoot();
201
+ const playwrightPath = path.join(pkgRoot, 'node_modules', 'playwright');
202
+ const katexPath = path.join(pkgRoot, 'node_modules', 'katex', 'dist', 'katex.min.js');
203
+ const mermaidPath = path.join(pkgRoot, 'node_modules', 'mermaid', 'dist', 'mermaid.min.js');
204
+ dependenciesAvailable = fs.existsSync(playwrightPath) && fs.existsSync(katexPath) && fs.existsSync(mermaidPath);
205
+ if (!dependenciesAvailable) {
206
+ logger.warn('[RichContent] Dependencies not installed (playwright/katex/mermaid), rich content rendering disabled');
207
+ }
208
+ return dependenciesAvailable;
209
+ }
210
+ catch (err) {
211
+ logger.warn('[RichContent] Failed to check dependencies:', err);
212
+ dependenciesAvailable = false;
213
+ return false;
214
+ }
215
+ }
216
+ export function hasRichContent(text) {
217
+ return extractLatex(text).length > 0 || extractMermaid(text).length > 0;
218
+ }
219
+ export async function closeBrowser() {
220
+ if (browserContext) {
221
+ await browserContext.close().catch(() => { });
222
+ browserContext = null;
223
+ }
224
+ if (browserInstance) {
225
+ await browserInstance.close().catch(() => { });
226
+ browserInstance = null;
227
+ }
228
+ }
@@ -0,0 +1,102 @@
1
+ export class StatsCollector {
2
+ events = [];
3
+ startTime;
4
+ HOUR_MS = 3_600_000;
5
+ constructor(eventBus) {
6
+ this.startTime = Date.now();
7
+ // 订阅相关事件
8
+ eventBus.subscribe('message:received', (event) => {
9
+ const e = event;
10
+ this.recordEvent({ type: 'received', timestamp: e.timestamp || Date.now() });
11
+ });
12
+ eventBus.subscribe('message:completed', (event) => {
13
+ const e = event;
14
+ this.recordEvent({ type: 'completed', timestamp: e.timestamp || Date.now(), durationMs: e.durationMs });
15
+ });
16
+ eventBus.subscribe('message:error', (event) => {
17
+ const e = event;
18
+ this.recordEvent({ type: 'error', timestamp: Date.now(), errorType: e.errorType });
19
+ });
20
+ eventBus.subscribe('message:interrupted', (_event) => {
21
+ this.recordEvent({ type: 'interrupted', timestamp: Date.now() });
22
+ });
23
+ eventBus.subscribe('session:safe-mode-entered', (_event) => {
24
+ this.recordEvent({ type: 'safe-mode-entered', timestamp: Date.now() });
25
+ });
26
+ eventBus.subscribe('tool:result', (event) => {
27
+ const e = event;
28
+ if (e.isError) {
29
+ this.recordEvent({ type: 'tool-error', timestamp: Date.now(), toolName: e.toolName });
30
+ }
31
+ });
32
+ }
33
+ recordEvent(record) {
34
+ this.events.push(record);
35
+ }
36
+ /**
37
+ * 获取统计快照(自动裁剪 >1h 的事件)
38
+ */
39
+ getSnapshot() {
40
+ const now = Date.now();
41
+ const cutoff = now - this.HOUR_MS;
42
+ // 裁剪过期事件
43
+ this.events = this.events.filter(e => e.timestamp >= cutoff);
44
+ // 聚合统计
45
+ let received = 0;
46
+ let completed = 0;
47
+ let errors = 0;
48
+ const errorsByType = {};
49
+ let toolErrors = 0;
50
+ const toolErrorsByName = {};
51
+ let interrupts = 0;
52
+ let safeModeEntries = 0;
53
+ let totalDuration = 0;
54
+ let durationCount = 0;
55
+ for (const event of this.events) {
56
+ switch (event.type) {
57
+ case 'received':
58
+ received++;
59
+ break;
60
+ case 'completed':
61
+ completed++;
62
+ if (event.durationMs !== undefined) {
63
+ totalDuration += event.durationMs;
64
+ durationCount++;
65
+ }
66
+ break;
67
+ case 'error':
68
+ errors++;
69
+ if (event.errorType) {
70
+ errorsByType[event.errorType] = (errorsByType[event.errorType] || 0) + 1;
71
+ }
72
+ break;
73
+ case 'tool-error':
74
+ toolErrors++;
75
+ if (event.toolName) {
76
+ toolErrorsByName[event.toolName] = (toolErrorsByName[event.toolName] || 0) + 1;
77
+ }
78
+ break;
79
+ case 'interrupted':
80
+ interrupts++;
81
+ break;
82
+ case 'safe-mode-entered':
83
+ safeModeEntries++;
84
+ break;
85
+ }
86
+ }
87
+ return {
88
+ uptimeMs: now - this.startTime,
89
+ lastHour: {
90
+ received,
91
+ completed,
92
+ errors,
93
+ errorsByType,
94
+ toolErrors,
95
+ toolErrorsByName,
96
+ interrupts,
97
+ safeModeEntries,
98
+ avgResponseMs: durationCount > 0 ? totalDuration / durationCount : 0
99
+ }
100
+ };
101
+ }
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.1.2",
3
+ "version": "2.3.0",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,8 +22,10 @@
22
22
  "prepublishOnly": "npm run build && npm test"
23
23
  },
24
24
  "dependencies": {
25
- "@anthropic-ai/claude-agent-sdk": "^0.2.75",
25
+ "@anthropic-ai/claude-agent-sdk": "^0.2.100",
26
+ "@eleans/aun-core-node": "^0.3.0",
26
27
  "@larksuiteoapi/node-sdk": "^1.59.0",
28
+ "@openai/codex-sdk": "^0.118.0",
27
29
  "image-type": "^6.0.0",
28
30
  "qrcode-terminal": "^0.12.0"
29
31
  },