@projectservan8n/cnapse 0.6.0 → 0.6.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/dist/index.js +710 -16
- package/package.json +1 -1
- package/src/components/App.tsx +20 -1
- package/src/lib/tasks.ts +243 -1
- package/src/services/telegram.ts +218 -3
package/package.json
CHANGED
package/src/components/App.tsx
CHANGED
|
@@ -202,6 +202,21 @@ export function App() {
|
|
|
202
202
|
}
|
|
203
203
|
}, [chat, tasks]);
|
|
204
204
|
|
|
205
|
+
// Check if message looks like a computer control request
|
|
206
|
+
const isComputerControlRequest = useCallback((text: string): boolean => {
|
|
207
|
+
const lower = text.toLowerCase();
|
|
208
|
+
const patterns = [
|
|
209
|
+
/^(can you |please |)?(open|close|minimize|maximize|restore|focus|click|type|press|scroll|move|drag)/i,
|
|
210
|
+
/^(can you |please |)?move (the |my |)mouse/i,
|
|
211
|
+
/^(can you |please |)?(start|launch|run) [a-z]/i,
|
|
212
|
+
/(open|close|minimize|maximize) (the |my |)?[a-z]/i,
|
|
213
|
+
/click (on |the |)/i,
|
|
214
|
+
/type ["'].+["']/i,
|
|
215
|
+
/press (enter|escape|tab|ctrl|alt|shift|space|backspace|delete|f\d+)/i,
|
|
216
|
+
];
|
|
217
|
+
return patterns.some(p => p.test(lower));
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
205
220
|
// Submit handler
|
|
206
221
|
const handleSubmit = useCallback(async (value: string) => {
|
|
207
222
|
if (!value.trim()) return;
|
|
@@ -209,12 +224,16 @@ export function App() {
|
|
|
209
224
|
|
|
210
225
|
if (value.startsWith('/')) {
|
|
211
226
|
await handleCommand(value);
|
|
227
|
+
} else if (isComputerControlRequest(value)) {
|
|
228
|
+
// Auto-route to task system for computer control
|
|
229
|
+
chat.addSystemMessage(`🤖 Executing: ${value}`);
|
|
230
|
+
await handleTaskCommand(value);
|
|
212
231
|
} else {
|
|
213
232
|
setStatus('Thinking...');
|
|
214
233
|
await chat.sendMessage(value);
|
|
215
234
|
setStatus('Ready');
|
|
216
235
|
}
|
|
217
|
-
}, [chat, handleCommand]);
|
|
236
|
+
}, [chat, handleCommand, handleTaskCommand, isComputerControlRequest]);
|
|
218
237
|
|
|
219
238
|
// Provider selection callback
|
|
220
239
|
const handleProviderSelect = useCallback((provider: string, model: string) => {
|
package/src/lib/tasks.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { chat, Message } from './api.js';
|
|
8
8
|
import * as computer from '../tools/computer.js';
|
|
9
9
|
import { describeScreen } from './vision.js';
|
|
10
|
+
import * as filesystem from '../tools/filesystem.js';
|
|
11
|
+
import { runCommand } from '../tools/shell.js';
|
|
10
12
|
import * as fs from 'fs';
|
|
11
13
|
import * as path from 'path';
|
|
12
14
|
import * as os from 'os';
|
|
@@ -185,14 +187,36 @@ Before outputting steps, THINK through these questions:
|
|
|
185
187
|
- Typing too fast -> add small waits
|
|
186
188
|
|
|
187
189
|
## AVAILABLE ACTIONS
|
|
190
|
+
|
|
191
|
+
### App Control
|
|
188
192
|
- open_app: Open app via Run dialog (e.g., "open_app:notepad", "open_app:code", "open_app:chrome")
|
|
193
|
+
- open_folder: Open VS Code with folder (e.g., "open_folder:E:/MyProject")
|
|
194
|
+
- focus_window: Focus by title (e.g., "focus_window:Notepad")
|
|
195
|
+
|
|
196
|
+
### Input
|
|
189
197
|
- type_text: Type text string (e.g., "type_text:Hello World")
|
|
190
198
|
- press_key: Single key (e.g., "press_key:enter", "press_key:escape", "press_key:tab")
|
|
191
199
|
- key_combo: Key combination (e.g., "key_combo:control+s", "key_combo:alt+f4", "key_combo:meta+r")
|
|
192
200
|
- click: Mouse click (e.g., "click:left", "click:right")
|
|
201
|
+
|
|
202
|
+
### File Operations
|
|
203
|
+
- read_file: Read file contents (e.g., "read_file:E:/test/index.html")
|
|
204
|
+
- write_file: Write content to file (e.g., "write_file:E:/test/output.txt|Hello World")
|
|
205
|
+
- list_files: List files in directory (e.g., "list_files:E:/test")
|
|
206
|
+
|
|
207
|
+
### AI Coding
|
|
208
|
+
- generate_code: AI generates code based on description (e.g., "generate_code:E:/test/index.html|create an HTML page with input on left, output on right")
|
|
209
|
+
- edit_code: AI modifies existing code (e.g., "edit_code:E:/test/app.js|add error handling to the fetch calls")
|
|
210
|
+
|
|
211
|
+
### Web Browsing
|
|
212
|
+
- open_url: Open URL in default browser (e.g., "open_url:https://perplexity.ai")
|
|
213
|
+
- browse_and_ask: Open AI website, type question, wait for response (e.g., "browse_and_ask:perplexity|What is the capital of France?")
|
|
214
|
+
- browse_and_ask: Supports: perplexity, chatgpt, claude, google
|
|
215
|
+
|
|
216
|
+
### Utility
|
|
193
217
|
- wait: Wait N seconds (e.g., "wait:2" - use 1-3s for app loads)
|
|
194
|
-
- focus_window: Focus by title (e.g., "focus_window:Notepad")
|
|
195
218
|
- screenshot: Capture and describe screen
|
|
219
|
+
- shell: Run shell command (e.g., "shell:npm install")
|
|
196
220
|
${learnedExamples}
|
|
197
221
|
## EXAMPLES WITH REASONING
|
|
198
222
|
|
|
@@ -235,6 +259,59 @@ Output:
|
|
|
235
259
|
{ "description": "Close active window with Alt+F4", "action": "key_combo:alt+f4" }
|
|
236
260
|
]
|
|
237
261
|
|
|
262
|
+
### Example 4: "open folder E:/Test in vscode and create an HTML editor"
|
|
263
|
+
Thinking:
|
|
264
|
+
- Goal: Open VS Code with folder, then create/edit HTML file to be an editor
|
|
265
|
+
- How: Use open_folder to launch VS Code with the folder, then use AI to generate code
|
|
266
|
+
- Sequence: Open folder -> List files to see what exists -> Generate/edit the HTML
|
|
267
|
+
- Edge case: File might not exist yet
|
|
268
|
+
|
|
269
|
+
Output:
|
|
270
|
+
[
|
|
271
|
+
{ "description": "Open VS Code with the Test folder", "action": "open_folder:E:/Test" },
|
|
272
|
+
{ "description": "Wait for VS Code to load", "action": "wait:3" },
|
|
273
|
+
{ "description": "List files in the folder", "action": "list_files:E:/Test" },
|
|
274
|
+
{ "description": "Generate HTML editor code", "action": "generate_code:E:/Test/editor.html|Create an HTML page with a code editor layout: textarea input on the left side, live preview output on the right side. Include basic CSS for split layout and JavaScript to update preview on input." }
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
### Example 5: "read the config.json and add a new setting"
|
|
278
|
+
Thinking:
|
|
279
|
+
- Goal: Read existing file, understand it, modify it
|
|
280
|
+
- How: read_file to get contents, then edit_code to modify
|
|
281
|
+
- Sequence: Read first, then edit
|
|
282
|
+
|
|
283
|
+
Output:
|
|
284
|
+
[
|
|
285
|
+
{ "description": "Read the config file", "action": "read_file:config.json" },
|
|
286
|
+
{ "description": "Add new setting to config", "action": "edit_code:config.json|add a new setting called 'darkMode' with value true" }
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
### Example 6: "ask perplexity what is the best programming language"
|
|
290
|
+
Thinking:
|
|
291
|
+
- Goal: Open Perplexity AI in browser and ask a question
|
|
292
|
+
- How: Use browse_and_ask with perplexity target
|
|
293
|
+
- Sequence: Open site -> type question -> wait for response -> screenshot result
|
|
294
|
+
|
|
295
|
+
Output:
|
|
296
|
+
[
|
|
297
|
+
{ "description": "Ask Perplexity the question", "action": "browse_and_ask:perplexity|what is the best programming language" },
|
|
298
|
+
{ "description": "Wait for response to generate", "action": "wait:5" },
|
|
299
|
+
{ "description": "Capture the response", "action": "screenshot" }
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
### Example 7: "search google for weather today"
|
|
303
|
+
Thinking:
|
|
304
|
+
- Goal: Open Google and search for something
|
|
305
|
+
- How: Use browse_and_ask with google target
|
|
306
|
+
- Sequence: Open Google, search, capture results
|
|
307
|
+
|
|
308
|
+
Output:
|
|
309
|
+
[
|
|
310
|
+
{ "description": "Search Google", "action": "browse_and_ask:google|weather today" },
|
|
311
|
+
{ "description": "Wait for results", "action": "wait:2" },
|
|
312
|
+
{ "description": "Capture search results", "action": "screenshot" }
|
|
313
|
+
]
|
|
314
|
+
|
|
238
315
|
## YOUR TASK
|
|
239
316
|
Now parse this request: "${input}"
|
|
240
317
|
|
|
@@ -349,6 +426,171 @@ async function executeStep(step: TaskStep): Promise<void> {
|
|
|
349
426
|
step.result = `Focused window: ${params}`;
|
|
350
427
|
break;
|
|
351
428
|
|
|
429
|
+
case 'open_folder':
|
|
430
|
+
// Open VS Code with a specific folder
|
|
431
|
+
await runCommand(`code "${params}"`, 10000);
|
|
432
|
+
step.result = `Opened VS Code with folder: ${params}`;
|
|
433
|
+
break;
|
|
434
|
+
|
|
435
|
+
case 'read_file': {
|
|
436
|
+
const readResult = await filesystem.readFile(params);
|
|
437
|
+
if (readResult.success) {
|
|
438
|
+
step.result = readResult.output;
|
|
439
|
+
} else {
|
|
440
|
+
throw new Error(readResult.error || 'Failed to read file');
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
case 'write_file': {
|
|
446
|
+
// Format: write_file:path|content
|
|
447
|
+
const [filePath, ...contentParts] = params.split('|');
|
|
448
|
+
const content = contentParts.join('|');
|
|
449
|
+
const writeResult = await filesystem.writeFile(filePath, content);
|
|
450
|
+
if (writeResult.success) {
|
|
451
|
+
step.result = `Written to ${filePath}`;
|
|
452
|
+
} else {
|
|
453
|
+
throw new Error(writeResult.error || 'Failed to write file');
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
case 'list_files': {
|
|
459
|
+
const listResult = await filesystem.listDir(params, false);
|
|
460
|
+
if (listResult.success) {
|
|
461
|
+
step.result = listResult.output;
|
|
462
|
+
} else {
|
|
463
|
+
throw new Error(listResult.error || 'Failed to list files');
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case 'generate_code': {
|
|
469
|
+
// Format: generate_code:path|description
|
|
470
|
+
const [codePath, ...descParts] = params.split('|');
|
|
471
|
+
const codeDescription = descParts.join('|');
|
|
472
|
+
|
|
473
|
+
// Ask AI to generate the code
|
|
474
|
+
const codePrompt = `Generate complete, working code for this request. Output ONLY the code, no explanations or markdown:
|
|
475
|
+
|
|
476
|
+
Request: ${codeDescription}
|
|
477
|
+
|
|
478
|
+
File: ${codePath}`;
|
|
479
|
+
|
|
480
|
+
const codeResponse = await chat([{ role: 'user', content: codePrompt }]);
|
|
481
|
+
let generatedCode = codeResponse.content;
|
|
482
|
+
|
|
483
|
+
// Strip markdown code blocks if present
|
|
484
|
+
generatedCode = generatedCode.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
485
|
+
|
|
486
|
+
// Write the generated code to file
|
|
487
|
+
const genResult = await filesystem.writeFile(codePath, generatedCode);
|
|
488
|
+
if (genResult.success) {
|
|
489
|
+
step.result = `Generated and saved code to ${codePath}`;
|
|
490
|
+
} else {
|
|
491
|
+
throw new Error(genResult.error || 'Failed to write generated code');
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
case 'edit_code': {
|
|
497
|
+
// Format: edit_code:path|instructions
|
|
498
|
+
const [editPath, ...instrParts] = params.split('|');
|
|
499
|
+
const instructions = instrParts.join('|');
|
|
500
|
+
|
|
501
|
+
// Read existing file
|
|
502
|
+
const existingResult = await filesystem.readFile(editPath);
|
|
503
|
+
if (!existingResult.success) {
|
|
504
|
+
throw new Error(`Cannot read file: ${existingResult.error}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Ask AI to edit the code
|
|
508
|
+
const editPrompt = `Edit this code according to the instructions. Output ONLY the complete modified code, no explanations or markdown:
|
|
509
|
+
|
|
510
|
+
Instructions: ${instructions}
|
|
511
|
+
|
|
512
|
+
Current code:
|
|
513
|
+
${existingResult.output}`;
|
|
514
|
+
|
|
515
|
+
const editResponse = await chat([{ role: 'user', content: editPrompt }]);
|
|
516
|
+
let editedCode = editResponse.content;
|
|
517
|
+
|
|
518
|
+
// Strip markdown code blocks if present
|
|
519
|
+
editedCode = editedCode.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
520
|
+
|
|
521
|
+
// Write the edited code back
|
|
522
|
+
const editWriteResult = await filesystem.writeFile(editPath, editedCode);
|
|
523
|
+
if (editWriteResult.success) {
|
|
524
|
+
step.result = `Edited and saved ${editPath}`;
|
|
525
|
+
} else {
|
|
526
|
+
throw new Error(editWriteResult.error || 'Failed to write edited code');
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case 'shell': {
|
|
532
|
+
const shellResult = await runCommand(params, 30000);
|
|
533
|
+
if (shellResult.success) {
|
|
534
|
+
step.result = shellResult.output || 'Command completed';
|
|
535
|
+
} else {
|
|
536
|
+
throw new Error(shellResult.error || 'Command failed');
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case 'open_url': {
|
|
542
|
+
// Open URL in default browser
|
|
543
|
+
const url = params.startsWith('http') ? params : `https://${params}`;
|
|
544
|
+
if (process.platform === 'win32') {
|
|
545
|
+
await runCommand(`start "" "${url}"`, 5000);
|
|
546
|
+
} else if (process.platform === 'darwin') {
|
|
547
|
+
await runCommand(`open "${url}"`, 5000);
|
|
548
|
+
} else {
|
|
549
|
+
await runCommand(`xdg-open "${url}"`, 5000);
|
|
550
|
+
}
|
|
551
|
+
step.result = `Opened ${url} in browser`;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case 'browse_and_ask': {
|
|
556
|
+
// Format: browse_and_ask:site|question
|
|
557
|
+
const [site, ...questionParts] = params.split('|');
|
|
558
|
+
const question = questionParts.join('|');
|
|
559
|
+
|
|
560
|
+
// Site-specific URLs and input selectors
|
|
561
|
+
const sites: Record<string, { url: string; waitTime: number; searchSelector?: string }> = {
|
|
562
|
+
perplexity: { url: 'https://www.perplexity.ai', waitTime: 3 },
|
|
563
|
+
chatgpt: { url: 'https://chat.openai.com', waitTime: 4 },
|
|
564
|
+
claude: { url: 'https://claude.ai', waitTime: 4 },
|
|
565
|
+
google: { url: 'https://www.google.com', waitTime: 2 },
|
|
566
|
+
bing: { url: 'https://www.bing.com', waitTime: 2 },
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const siteConfig = sites[site.toLowerCase()] || { url: `https://${site}`, waitTime: 3 };
|
|
570
|
+
|
|
571
|
+
// Open the site
|
|
572
|
+
if (process.platform === 'win32') {
|
|
573
|
+
await runCommand(`start "" "${siteConfig.url}"`, 5000);
|
|
574
|
+
} else if (process.platform === 'darwin') {
|
|
575
|
+
await runCommand(`open "${siteConfig.url}"`, 5000);
|
|
576
|
+
} else {
|
|
577
|
+
await runCommand(`xdg-open "${siteConfig.url}"`, 5000);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Wait for page to load
|
|
581
|
+
await sleep(siteConfig.waitTime * 1000);
|
|
582
|
+
|
|
583
|
+
// Type the question (most sites have autofocus on search/input)
|
|
584
|
+
await computer.typeText(question);
|
|
585
|
+
await sleep(300);
|
|
586
|
+
|
|
587
|
+
// Press Enter to submit
|
|
588
|
+
await computer.pressKey('Return');
|
|
589
|
+
|
|
590
|
+
step.result = `Asked ${site}: "${question}"`;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
352
594
|
case 'screenshot':
|
|
353
595
|
const vision = await describeScreen();
|
|
354
596
|
step.result = vision.description;
|
package/src/services/telegram.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { getConfig, getApiKey } from '../lib/config.js';
|
|
|
7
7
|
import { describeScreen, captureScreenshot } from '../lib/vision.js';
|
|
8
8
|
import { runCommand } from '../tools/shell.js';
|
|
9
9
|
import { chat as chatWithAI, chatWithVision, Message } from '../lib/api.js';
|
|
10
|
+
import * as computer from '../tools/computer.js';
|
|
10
11
|
|
|
11
12
|
export interface TelegramMessage {
|
|
12
13
|
chatId: number;
|
|
@@ -22,6 +23,101 @@ export interface TelegramBotEvents {
|
|
|
22
23
|
stopped: () => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Convert markdown to Telegram-safe format (MarkdownV2)
|
|
28
|
+
* Escapes special characters and converts some markdown syntax
|
|
29
|
+
*/
|
|
30
|
+
function formatForTelegram(text: string): { text: string; parseMode: 'MarkdownV2' | undefined } {
|
|
31
|
+
// Check if text has markdown that could be rendered
|
|
32
|
+
const hasMarkdown = /[*_`\[\]()]/.test(text);
|
|
33
|
+
|
|
34
|
+
if (!hasMarkdown) {
|
|
35
|
+
return { text, parseMode: undefined };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Convert to Telegram MarkdownV2 format
|
|
40
|
+
let formatted = text;
|
|
41
|
+
|
|
42
|
+
// First, escape special characters that aren't part of markdown
|
|
43
|
+
// MarkdownV2 requires escaping: _ * [ ] ( ) ~ ` > # + - = | { } . !
|
|
44
|
+
const escapeChars = ['\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
|
|
45
|
+
|
|
46
|
+
// Temporarily replace valid markdown with placeholders
|
|
47
|
+
const placeholders: { placeholder: string; original: string }[] = [];
|
|
48
|
+
let placeholderIndex = 0;
|
|
49
|
+
|
|
50
|
+
// Protect code blocks (```code```)
|
|
51
|
+
formatted = formatted.replace(/```([\s\S]*?)```/g, (match, code) => {
|
|
52
|
+
const placeholder = `__CODEBLOCK_${placeholderIndex++}__`;
|
|
53
|
+
placeholders.push({ placeholder, original: '```' + code.replace(/\\/g, '\\\\') + '```' });
|
|
54
|
+
return placeholder;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Protect inline code (`code`)
|
|
58
|
+
formatted = formatted.replace(/`([^`]+)`/g, (match, code) => {
|
|
59
|
+
const placeholder = `__INLINECODE_${placeholderIndex++}__`;
|
|
60
|
+
placeholders.push({ placeholder, original: '`' + code.replace(/\\/g, '\\\\') + '`' });
|
|
61
|
+
return placeholder;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Protect bold (**text** or __text__)
|
|
65
|
+
formatted = formatted.replace(/\*\*(.+?)\*\*/g, (match, text) => {
|
|
66
|
+
const placeholder = `__BOLD_${placeholderIndex++}__`;
|
|
67
|
+
placeholders.push({ placeholder, original: '*' + text + '*' });
|
|
68
|
+
return placeholder;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Protect italic (*text* or _text_) - but only single asterisks
|
|
72
|
+
formatted = formatted.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (match, text) => {
|
|
73
|
+
const placeholder = `__ITALIC_${placeholderIndex++}__`;
|
|
74
|
+
placeholders.push({ placeholder, original: '_' + text + '_' });
|
|
75
|
+
return placeholder;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Protect links [text](url)
|
|
79
|
+
formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
|
80
|
+
const placeholder = `__LINK_${placeholderIndex++}__`;
|
|
81
|
+
placeholders.push({ placeholder, original: '[' + text + '](' + url + ')' });
|
|
82
|
+
return placeholder;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Now escape remaining special characters
|
|
86
|
+
for (const char of escapeChars) {
|
|
87
|
+
if (char === '\\') continue; // Skip backslash for now
|
|
88
|
+
formatted = formatted.split(char).join('\\' + char);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Restore placeholders
|
|
92
|
+
for (const { placeholder, original } of placeholders) {
|
|
93
|
+
formatted = formatted.replace(placeholder, original);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { text: formatted, parseMode: 'MarkdownV2' };
|
|
97
|
+
} catch {
|
|
98
|
+
// If formatting fails, return plain text
|
|
99
|
+
return { text, parseMode: undefined };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Send a message with proper formatting, falling back to plain text if markdown fails
|
|
105
|
+
*/
|
|
106
|
+
async function sendFormattedMessage(ctx: any, text: string): Promise<void> {
|
|
107
|
+
const { text: formatted, parseMode } = formatForTelegram(text);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (parseMode) {
|
|
111
|
+
await ctx.reply(formatted, { parse_mode: parseMode });
|
|
112
|
+
} else {
|
|
113
|
+
await ctx.reply(text);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// If markdown parsing fails, send as plain text
|
|
117
|
+
await ctx.reply(text);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
25
121
|
export class TelegramBotService extends EventEmitter {
|
|
26
122
|
private bot: any = null;
|
|
27
123
|
private isRunning = false;
|
|
@@ -248,6 +344,14 @@ export class TelegramBotService extends EventEmitter {
|
|
|
248
344
|
// Send typing indicator
|
|
249
345
|
await ctx.sendChatAction('typing');
|
|
250
346
|
|
|
347
|
+
// Check if this looks like a computer control request
|
|
348
|
+
const computerControlResult = await this.tryComputerControl(userText);
|
|
349
|
+
if (computerControlResult) {
|
|
350
|
+
await sendFormattedMessage(ctx, computerControlResult);
|
|
351
|
+
history.push({ role: 'assistant', content: computerControlResult });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
251
355
|
// Check if this looks like a screen/vision request
|
|
252
356
|
const isVisionRequest = /screen|see|look|what('?s| is) (on|visible)|show me|screenshot/i.test(userText);
|
|
253
357
|
|
|
@@ -267,16 +371,16 @@ export class TelegramBotService extends EventEmitter {
|
|
|
267
371
|
// Add assistant response to history
|
|
268
372
|
history.push({ role: 'assistant', content: response.content });
|
|
269
373
|
|
|
270
|
-
// Send response (split if too long for Telegram)
|
|
374
|
+
// Send response with proper formatting (split if too long for Telegram)
|
|
271
375
|
const responseText = response.content || '(no response)';
|
|
272
376
|
if (responseText.length > 4000) {
|
|
273
377
|
// Split into chunks
|
|
274
378
|
const chunks = responseText.match(/.{1,4000}/gs) || [responseText];
|
|
275
379
|
for (const chunk of chunks) {
|
|
276
|
-
await ctx
|
|
380
|
+
await sendFormattedMessage(ctx, chunk);
|
|
277
381
|
}
|
|
278
382
|
} else {
|
|
279
|
-
await ctx
|
|
383
|
+
await sendFormattedMessage(ctx, responseText);
|
|
280
384
|
}
|
|
281
385
|
} catch (error) {
|
|
282
386
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -302,6 +406,117 @@ export class TelegramBotService extends EventEmitter {
|
|
|
302
406
|
return this.allowedChatIds.has(chatId);
|
|
303
407
|
}
|
|
304
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Try to execute computer control commands directly
|
|
411
|
+
* Returns response string if handled, null if not a computer command
|
|
412
|
+
*/
|
|
413
|
+
private async tryComputerControl(text: string): Promise<string | null> {
|
|
414
|
+
const lower = text.toLowerCase();
|
|
415
|
+
|
|
416
|
+
// Minimize window
|
|
417
|
+
let match = lower.match(/minimize\s+(?:the\s+)?(.+)/i);
|
|
418
|
+
if (match) {
|
|
419
|
+
const result = await computer.minimizeWindow(match[1].trim());
|
|
420
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Maximize window
|
|
424
|
+
match = lower.match(/maximize\s+(?:the\s+)?(.+)/i);
|
|
425
|
+
if (match) {
|
|
426
|
+
const result = await computer.maximizeWindow(match[1].trim());
|
|
427
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Close window
|
|
431
|
+
match = lower.match(/close\s+(?:the\s+)?(.+)/i);
|
|
432
|
+
if (match) {
|
|
433
|
+
const result = await computer.closeWindow(match[1].trim());
|
|
434
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Restore window
|
|
438
|
+
match = lower.match(/restore\s+(?:the\s+)?(.+)/i);
|
|
439
|
+
if (match) {
|
|
440
|
+
const result = await computer.restoreWindow(match[1].trim());
|
|
441
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Focus/open window
|
|
445
|
+
match = lower.match(/(?:focus|open|switch to)\s+(?:the\s+)?(.+)/i);
|
|
446
|
+
if (match) {
|
|
447
|
+
const result = await computer.focusWindow(match[1].trim());
|
|
448
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Type text
|
|
452
|
+
match = text.match(/type\s+["'](.+)["']/i);
|
|
453
|
+
if (match) {
|
|
454
|
+
const result = await computer.typeText(match[1]);
|
|
455
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Press key
|
|
459
|
+
match = lower.match(/press\s+(?:the\s+)?(\w+)/i);
|
|
460
|
+
if (match) {
|
|
461
|
+
const result = await computer.pressKey(match[1]);
|
|
462
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Click
|
|
466
|
+
if (/^click$/i.test(lower) || /click\s+(?:the\s+)?mouse/i.test(lower)) {
|
|
467
|
+
const result = await computer.clickMouse('left');
|
|
468
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Right click
|
|
472
|
+
if (/right\s*click/i.test(lower)) {
|
|
473
|
+
const result = await computer.clickMouse('right');
|
|
474
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Double click
|
|
478
|
+
if (/double\s*click/i.test(lower)) {
|
|
479
|
+
const result = await computer.doubleClick();
|
|
480
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Move mouse to coordinates
|
|
484
|
+
match = lower.match(/move\s+(?:the\s+)?mouse\s+(?:to\s+)?(\d+)[,\s]+(\d+)/i);
|
|
485
|
+
if (match) {
|
|
486
|
+
const result = await computer.moveMouse(parseInt(match[1]), parseInt(match[2]));
|
|
487
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Scroll
|
|
491
|
+
match = lower.match(/scroll\s+(up|down)(?:\s+(\d+))?/i);
|
|
492
|
+
if (match) {
|
|
493
|
+
const amount = match[1] === 'up' ? (parseInt(match[2]) || 3) : -(parseInt(match[2]) || 3);
|
|
494
|
+
const result = await computer.scrollMouse(amount);
|
|
495
|
+
return result.success ? `✅ ${result.output}` : `❌ ${result.error}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// List windows
|
|
499
|
+
if (/list\s+(?:all\s+)?windows/i.test(lower) || /what\s+windows/i.test(lower)) {
|
|
500
|
+
const result = await computer.listWindows();
|
|
501
|
+
return result.success ? `📋 Open Windows:\n${result.output}` : `❌ ${result.error}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Get active window
|
|
505
|
+
if (/(?:active|current|focused)\s+window/i.test(lower) || /what\s+(?:window|app)/i.test(lower)) {
|
|
506
|
+
const result = await computer.getActiveWindow();
|
|
507
|
+
return result.success ? `🪟 Active: ${result.output}` : `❌ ${result.error}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Mouse position
|
|
511
|
+
if (/mouse\s+position/i.test(lower) || /where.*mouse/i.test(lower)) {
|
|
512
|
+
const result = await computer.getMousePosition();
|
|
513
|
+
return result.success ? `🖱️ ${result.output}` : `❌ ${result.error}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Not a computer control command
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
305
520
|
/**
|
|
306
521
|
* Send a message to a specific chat
|
|
307
522
|
*/
|