squidclaw 2.8.0 → 3.0.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.
- package/lib/engine.js +6 -0
- package/lib/features/canvas.js +228 -0
- package/lib/middleware/commands.js +47 -0
- package/lib/tools/router.js +68 -0
- package/package.json +1 -1
package/lib/engine.js
CHANGED
|
@@ -228,6 +228,12 @@ export class SquidclawEngine {
|
|
|
228
228
|
if (pending.c > 0) console.log(` ⏰ Reminders: ${pending.c} pending`);
|
|
229
229
|
} catch {}
|
|
230
230
|
|
|
231
|
+
// Canvas (Visual Rendering)
|
|
232
|
+
try {
|
|
233
|
+
const { Canvas } = await import('./features/canvas.js');
|
|
234
|
+
this.canvas = new Canvas(this);
|
|
235
|
+
} catch (err) { logger.error('engine', 'Canvas init failed: ' + err.message); }
|
|
236
|
+
|
|
231
237
|
// Nodes (Paired Devices)
|
|
232
238
|
try {
|
|
233
239
|
const { NodeManager } = await import('./features/nodes.js');
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Canvas — Render HTML/charts as images in chat
|
|
3
|
+
* Uses Puppeteer to render HTML → screenshot → send as image
|
|
4
|
+
* Mini-apps, dashboards, visual reports
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../core/logger.js';
|
|
8
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
export class Canvas {
|
|
12
|
+
constructor(engine) {
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.outputDir = '/tmp/squidclaw-canvas';
|
|
15
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Render HTML to image ──
|
|
19
|
+
|
|
20
|
+
async renderHtml(html, options = {}) {
|
|
21
|
+
const puppeteer = await import('puppeteer-core');
|
|
22
|
+
const { existsSync } = await import('fs');
|
|
23
|
+
|
|
24
|
+
const paths = ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium'];
|
|
25
|
+
let execPath = null;
|
|
26
|
+
for (const p of paths) { if (existsSync(p)) { execPath = p; break; } }
|
|
27
|
+
if (!execPath) throw new Error('No Chrome/Chromium found');
|
|
28
|
+
|
|
29
|
+
const browser = await puppeteer.default.launch({
|
|
30
|
+
executablePath: execPath,
|
|
31
|
+
headless: 'new',
|
|
32
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const page = await browser.newPage();
|
|
37
|
+
const width = options.width || 800;
|
|
38
|
+
const height = options.height || 600;
|
|
39
|
+
await page.setViewport({ width, height });
|
|
40
|
+
|
|
41
|
+
// Full HTML page
|
|
42
|
+
const fullHtml = html.includes('<html') ? html : `
|
|
43
|
+
<!DOCTYPE html>
|
|
44
|
+
<html>
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8">
|
|
47
|
+
<style>
|
|
48
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
49
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; }
|
|
50
|
+
h1 { color: #58a6ff; font-size: 24px; margin-bottom: 16px; }
|
|
51
|
+
h2 { color: #58a6ff; font-size: 18px; margin: 20px 0 8px; }
|
|
52
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; margin: 12px 0; }
|
|
53
|
+
.stat { display: inline-block; text-align: center; padding: 16px 24px; }
|
|
54
|
+
.stat-value { font-size: 36px; font-weight: bold; color: #58a6ff; }
|
|
55
|
+
.stat-label { font-size: 12px; color: #8b949e; margin-top: 4px; }
|
|
56
|
+
.grid { display: grid; gap: 12px; }
|
|
57
|
+
.grid-2 { grid-template-columns: 1fr 1fr; }
|
|
58
|
+
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
|
59
|
+
.grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
|
60
|
+
.bar { height: 24px; border-radius: 4px; margin: 4px 0; }
|
|
61
|
+
.bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #58a6ff, #3fb950); }
|
|
62
|
+
.progress { background: #21262d; border-radius: 8px; overflow: hidden; height: 12px; margin: 6px 0; }
|
|
63
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, #58a6ff, #3fb950); border-radius: 8px; }
|
|
64
|
+
table { width: 100%; border-collapse: collapse; }
|
|
65
|
+
th { background: #161b22; color: #58a6ff; padding: 10px; text-align: left; border-bottom: 2px solid #30363d; }
|
|
66
|
+
td { padding: 10px; border-bottom: 1px solid #21262d; }
|
|
67
|
+
tr:nth-child(even) { background: #161b22; }
|
|
68
|
+
.tag { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 2px 10px; border-radius: 12px; font-size: 12px; margin: 2px; }
|
|
69
|
+
.green { color: #3fb950; } .red { color: #f85149; } .yellow { color: #d29922; }
|
|
70
|
+
.accent { color: #58a6ff; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>${html}</body>
|
|
74
|
+
</html>`;
|
|
75
|
+
|
|
76
|
+
await page.setContent(fullHtml, { waitUntil: 'networkidle0', timeout: 10000 });
|
|
77
|
+
|
|
78
|
+
// Auto-height if not specified
|
|
79
|
+
if (!options.height) {
|
|
80
|
+
const bodyHeight = await page.evaluate(() => document.body.scrollHeight);
|
|
81
|
+
await page.setViewport({ width, height: Math.min(bodyHeight + 48, 2000) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const screenshot = await page.screenshot({
|
|
85
|
+
type: 'png',
|
|
86
|
+
fullPage: !options.height,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const filename = (options.name || 'canvas_' + Date.now()) + '.png';
|
|
90
|
+
const filepath = join(this.outputDir, filename);
|
|
91
|
+
writeFileSync(filepath, screenshot);
|
|
92
|
+
|
|
93
|
+
logger.info('canvas', `Rendered: ${filepath} (${width}x${options.height || 'auto'})`);
|
|
94
|
+
return { buffer: screenshot, filepath, filename };
|
|
95
|
+
} finally {
|
|
96
|
+
await browser.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Pre-built templates ──
|
|
101
|
+
|
|
102
|
+
async renderDashboard(data) {
|
|
103
|
+
const { title, stats, chart, table } = data;
|
|
104
|
+
|
|
105
|
+
let html = `<h1>${title || 'Dashboard'}</h1>`;
|
|
106
|
+
|
|
107
|
+
// Stats cards
|
|
108
|
+
if (stats && stats.length > 0) {
|
|
109
|
+
html += `<div class="card"><div class="grid grid-${Math.min(stats.length, 4)}">`;
|
|
110
|
+
for (const s of stats) {
|
|
111
|
+
html += `<div class="stat">
|
|
112
|
+
${s.icon ? `<div style="font-size:32px;margin-bottom:4px">${s.icon}</div>` : ''}
|
|
113
|
+
<div class="stat-value" style="color:${s.color || '#58a6ff'}">${s.value}</div>
|
|
114
|
+
<div class="stat-label">${s.label}</div>
|
|
115
|
+
</div>`;
|
|
116
|
+
}
|
|
117
|
+
html += `</div></div>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Bar chart
|
|
121
|
+
if (chart && chart.length > 0) {
|
|
122
|
+
const max = Math.max(...chart.map(c => c.value));
|
|
123
|
+
html += `<div class="card"><h2>${data.chartTitle || 'Chart'}</h2>`;
|
|
124
|
+
for (const item of chart) {
|
|
125
|
+
const pct = (item.value / max * 100).toFixed(0);
|
|
126
|
+
html += `<div style="display:flex;align-items:center;margin:8px 0">
|
|
127
|
+
<div style="width:100px;font-size:13px">${item.label}</div>
|
|
128
|
+
<div style="flex:1;background:#21262d;border-radius:4px;height:24px;overflow:hidden">
|
|
129
|
+
<div style="width:${pct}%;height:100%;background:linear-gradient(90deg,#58a6ff,#3fb950);border-radius:4px;display:flex;align-items:center;padding-left:8px">
|
|
130
|
+
<span style="font-size:11px;color:#fff;font-weight:bold">${item.value}</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>`;
|
|
134
|
+
}
|
|
135
|
+
html += `</div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Table
|
|
139
|
+
if (table && table.length > 0) {
|
|
140
|
+
html += `<div class="card"><h2>${data.tableTitle || 'Data'}</h2><table>`;
|
|
141
|
+
html += `<tr>${table[0].map(h => `<th>${h}</th>`).join('')}</tr>`;
|
|
142
|
+
for (let i = 1; i < table.length; i++) {
|
|
143
|
+
html += `<tr>${table[i].map(c => `<td>${c}</td>`).join('')}</tr>`;
|
|
144
|
+
}
|
|
145
|
+
html += `</table></div>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
html += `<div style="text-align:center;margin-top:16px;font-size:11px;color:#484f58">Squidclaw AI 🦑 · ${new Date().toLocaleString()}</div>`;
|
|
149
|
+
|
|
150
|
+
return this.renderHtml(html, { name: 'dashboard' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async renderChart(data) {
|
|
154
|
+
const { title, type, items } = data;
|
|
155
|
+
const max = Math.max(...items.map(i => i.value));
|
|
156
|
+
const colors = ['#58a6ff', '#3fb950', '#d29922', '#f85149', '#bc8cff', '#f778ba', '#a5d6ff', '#7ee787'];
|
|
157
|
+
|
|
158
|
+
let html = `<h1>${title || 'Chart'}</h1><div class="card" style="padding:24px">`;
|
|
159
|
+
|
|
160
|
+
if (type === 'pie' || type === 'doughnut') {
|
|
161
|
+
const total = items.reduce((s, i) => s + i.value, 0);
|
|
162
|
+
let rotation = 0;
|
|
163
|
+
const gradientParts = [];
|
|
164
|
+
|
|
165
|
+
items.forEach((item, idx) => {
|
|
166
|
+
const pct = (item.value / total * 100);
|
|
167
|
+
gradientParts.push(`${colors[idx % colors.length]} ${rotation}deg ${rotation + pct * 3.6}deg`);
|
|
168
|
+
rotation += pct * 3.6;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const hole = type === 'doughnut' ? 'radial-gradient(circle, #0d1117 40%, transparent 40%),' : '';
|
|
172
|
+
html += `<div style="display:flex;align-items:center;gap:32px">
|
|
173
|
+
<div style="width:200px;height:200px;border-radius:50%;background:${hole}conic-gradient(${gradientParts.join(',')})"></div>
|
|
174
|
+
<div>`;
|
|
175
|
+
items.forEach((item, idx) => {
|
|
176
|
+
html += `<div style="display:flex;align-items:center;gap:8px;margin:6px 0">
|
|
177
|
+
<div style="width:12px;height:12px;border-radius:2px;background:${colors[idx % colors.length]}"></div>
|
|
178
|
+
<span style="font-size:13px">${item.label}: <strong>${item.value}</strong> (${(item.value / total * 100).toFixed(0)}%)</span>
|
|
179
|
+
</div>`;
|
|
180
|
+
});
|
|
181
|
+
html += `</div></div>`;
|
|
182
|
+
} else {
|
|
183
|
+
// Bar chart
|
|
184
|
+
for (const item of items) {
|
|
185
|
+
const pct = (item.value / max * 100).toFixed(0);
|
|
186
|
+
const color = item.color || colors[items.indexOf(item) % colors.length];
|
|
187
|
+
html += `<div style="display:flex;align-items:center;margin:10px 0">
|
|
188
|
+
<div style="width:120px;font-size:13px;color:#8b949e">${item.label}</div>
|
|
189
|
+
<div style="flex:1;background:#21262d;border-radius:6px;height:28px;overflow:hidden">
|
|
190
|
+
<div style="width:${pct}%;height:100%;background:${color};border-radius:6px;display:flex;align-items:center;padding-left:10px">
|
|
191
|
+
<span style="font-size:12px;color:#fff;font-weight:600">${item.value}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
html += `</div>`;
|
|
199
|
+
return this.renderHtml(html, { name: 'chart' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async renderReport(data) {
|
|
203
|
+
const { title, sections } = data;
|
|
204
|
+
let html = `<h1>${title || 'Report'}</h1>`;
|
|
205
|
+
|
|
206
|
+
for (const section of (sections || [])) {
|
|
207
|
+
html += `<div class="card"><h2>${section.title || ''}</h2>`;
|
|
208
|
+
if (section.text) html += `<p style="line-height:1.7;color:#8b949e">${section.text}</p>`;
|
|
209
|
+
if (section.bullets) {
|
|
210
|
+
html += `<ul style="list-style:none;padding:0">`;
|
|
211
|
+
section.bullets.forEach(b => {
|
|
212
|
+
html += `<li style="padding:4px 0;color:#c9d1d9">● ${b}</li>`;
|
|
213
|
+
});
|
|
214
|
+
html += `</ul>`;
|
|
215
|
+
}
|
|
216
|
+
if (section.stats) {
|
|
217
|
+
html += `<div class="grid grid-${Math.min(section.stats.length, 3)}" style="margin-top:12px">`;
|
|
218
|
+
section.stats.forEach(s => {
|
|
219
|
+
html += `<div class="stat"><div class="stat-value" style="font-size:28px">${s.value}</div><div class="stat-label">${s.label}</div></div>`;
|
|
220
|
+
});
|
|
221
|
+
html += `</div>`;
|
|
222
|
+
}
|
|
223
|
+
html += `</div>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.renderHtml(html, { name: 'report' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -145,6 +145,53 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/canvas') {
|
|
149
|
+
if (!ctx.engine.canvas) { await ctx.reply('❌ Canvas not available'); return; }
|
|
150
|
+
const args = msg.slice(8).trim();
|
|
151
|
+
|
|
152
|
+
if (!args || args === 'help') {
|
|
153
|
+
await ctx.reply('🎨 *Canvas*\n\nRender visual content as images.\n\n/canvas demo — see a sample dashboard\n\nOr ask me naturally: "Show me a dashboard of our stats" or "Make a pie chart of market share"');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (args === 'demo') {
|
|
158
|
+
try {
|
|
159
|
+
const result = await ctx.engine.canvas.renderDashboard({
|
|
160
|
+
title: 'Squidclaw Dashboard',
|
|
161
|
+
stats: [
|
|
162
|
+
{ icon: '🦑', value: 'v2.8', label: 'Version', color: '#58a6ff' },
|
|
163
|
+
{ icon: '⚡', value: '40+', label: 'Skills', color: '#3fb950' },
|
|
164
|
+
{ icon: '🔌', value: '3', label: 'Plugins', color: '#d29922' },
|
|
165
|
+
{ icon: '💬', value: '15', label: 'Middleware', color: '#bc8cff' },
|
|
166
|
+
],
|
|
167
|
+
chartTitle: 'Feature Growth by Version',
|
|
168
|
+
chart: [
|
|
169
|
+
{ label: 'v1.0', value: 20 },
|
|
170
|
+
{ label: 'v1.5', value: 28 },
|
|
171
|
+
{ label: 'v2.0', value: 40 },
|
|
172
|
+
{ label: 'v2.5', value: 48 },
|
|
173
|
+
{ label: 'v2.8', value: 55 },
|
|
174
|
+
],
|
|
175
|
+
tableTitle: 'System Status',
|
|
176
|
+
table: [
|
|
177
|
+
['Component', 'Status', 'Uptime'],
|
|
178
|
+
['Telegram', '🟢 Connected', '99.9%'],
|
|
179
|
+
['WhatsApp', '🔴 Off', '—'],
|
|
180
|
+
['Plugins', '🟢 3 Active', '100%'],
|
|
181
|
+
['Sandbox', '🟢 Ready', '100%'],
|
|
182
|
+
['Nodes', '🟢 Listening', '100%'],
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Send as image
|
|
187
|
+
if (ctx.platform === 'telegram' && ctx.engine.telegramManager) {
|
|
188
|
+
await ctx.engine.telegramManager.sendPhoto(ctx.agentId, ctx.contactId, { base64: result.buffer.toString('base64') }, '🎨 Squidclaw Dashboard Demo', ctx.metadata);
|
|
189
|
+
}
|
|
190
|
+
} catch (err) { await ctx.reply('❌ ' + err.message); }
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
148
195
|
if (cmd === '/nodes' || cmd === '/devices') {
|
|
149
196
|
if (!ctx.engine.nodeManager) { await ctx.reply('❌ Nodes not available'); return; }
|
|
150
197
|
const args = msg.split(/\s+/).slice(1);
|
package/lib/tools/router.js
CHANGED
|
@@ -169,6 +169,17 @@ export class ToolRouter {
|
|
|
169
169
|
'---TOOL:handoff:reason---',
|
|
170
170
|
'Transfer the conversation to a human agent. Use when you cannot help further.');
|
|
171
171
|
|
|
172
|
+
tools.push('', '### Render Dashboard (sends as image!)',
|
|
173
|
+
'---TOOL:canvas_dashboard:title|stat_json|chart_json---',
|
|
174
|
+
'Render a visual dashboard and send as image. Use JSON for data.',
|
|
175
|
+
'Simple format: ---TOOL:canvas_dashboard:Title|icon:value:label,icon:value:label|label:value,label:value---',
|
|
176
|
+
'', '### Render Chart (sends as image!)',
|
|
177
|
+
'---TOOL:canvas_chart:title|type|label:value,label:value---',
|
|
178
|
+
'Render a chart (bar, pie, doughnut) as image. Separate items with commas.',
|
|
179
|
+
'', '### Render Custom HTML (sends as image!)',
|
|
180
|
+
'---TOOL:canvas_html:html content---',
|
|
181
|
+
'Render any HTML as an image. Built-in dark theme CSS with cards, grids, stats, tables.');
|
|
182
|
+
|
|
172
183
|
tools.push('', '### Pair Device',
|
|
173
184
|
'---TOOL:node_pair:device name---',
|
|
174
185
|
'Generate a pairing code for a new device (phone, PC, server).',
|
|
@@ -697,6 +708,63 @@ export class ToolRouter {
|
|
|
697
708
|
}
|
|
698
709
|
break;
|
|
699
710
|
}
|
|
711
|
+
case 'canvas_dashboard': {
|
|
712
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
713
|
+
try {
|
|
714
|
+
const parts = toolArg.split('|');
|
|
715
|
+
const title = parts[0]?.trim() || 'Dashboard';
|
|
716
|
+
|
|
717
|
+
// Parse stats: icon:value:label,icon:value:label
|
|
718
|
+
const stats = [];
|
|
719
|
+
if (parts[1]) {
|
|
720
|
+
parts[1].split(',').forEach(s => {
|
|
721
|
+
const [icon, value, label] = s.trim().split(':');
|
|
722
|
+
if (value) stats.push({ icon: icon || '', value: value || '', label: label || '' });
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Parse chart: label:value,label:value
|
|
727
|
+
const chart = [];
|
|
728
|
+
if (parts[2]) {
|
|
729
|
+
parts[2].split(',').forEach(c => {
|
|
730
|
+
const [label, value] = c.trim().split(':');
|
|
731
|
+
if (value) chart.push({ label: label || '', value: parseInt(value) || 0 });
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const result = await this._engine.canvas.renderDashboard({ title, stats, chart });
|
|
736
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Dashboard rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
737
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case 'canvas_chart': {
|
|
741
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
742
|
+
try {
|
|
743
|
+
const parts = toolArg.split('|');
|
|
744
|
+
const title = parts[0]?.trim() || 'Chart';
|
|
745
|
+
const type = parts[1]?.trim() || 'bar';
|
|
746
|
+
const items = [];
|
|
747
|
+
if (parts[2]) {
|
|
748
|
+
parts[2].split(',').forEach(c => {
|
|
749
|
+
const [label, value] = c.trim().split(':');
|
|
750
|
+
if (value) items.push({ label: label || '', value: parseInt(value) || 0 });
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const result = await this._engine.canvas.renderChart({ title, type, items });
|
|
754
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Chart rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
755
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case 'canvas_html':
|
|
759
|
+
case 'canvas_render':
|
|
760
|
+
case 'canvas': {
|
|
761
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
762
|
+
try {
|
|
763
|
+
const result = await this._engine.canvas.renderHtml(toolArg);
|
|
764
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
765
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
700
768
|
case 'node_pair': {
|
|
701
769
|
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
702
770
|
const token = this._engine.nodeManager.generatePairToken(agentId, toolArg || 'Device');
|