squidclaw 1.1.0 → 1.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.
- package/lib/engine.js +195 -427
- package/lib/middleware/ai-process.js +20 -0
- package/lib/middleware/allowlist.js +15 -0
- package/lib/middleware/api-key-detect.js +17 -0
- package/lib/middleware/auto-links.js +14 -0
- package/lib/middleware/auto-memory.js +9 -0
- package/lib/middleware/commands.js +121 -0
- package/lib/middleware/media.js +108 -0
- package/lib/middleware/pipeline.js +87 -0
- package/lib/middleware/response-sender.js +55 -0
- package/lib/middleware/skill-check.js +15 -0
- package/lib/middleware/typing.js +26 -0
- package/lib/middleware/usage-alerts.js +19 -0
- package/lib/tools/browser-control.js +218 -0
- package/lib/tools/router.js +43 -0
- package/package.json +2 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core AI processing — sends message to agent, gets response
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function aiProcessMiddleware(ctx, next) {
|
|
7
|
+
if (ctx.handled) return;
|
|
8
|
+
|
|
9
|
+
const result = await ctx.agent.processMessage(ctx.contactId, ctx.message, ctx.metadata);
|
|
10
|
+
|
|
11
|
+
ctx.response = {
|
|
12
|
+
messages: result.messages || [],
|
|
13
|
+
reaction: result.reaction || null,
|
|
14
|
+
image: result.image || null,
|
|
15
|
+
_reminder: result._reminder || null,
|
|
16
|
+
};
|
|
17
|
+
ctx.metadata.originalType = ctx.metadata.originalType || null;
|
|
18
|
+
|
|
19
|
+
await next();
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist middleware — blocks unauthorized senders
|
|
3
|
+
*/
|
|
4
|
+
export async function allowlistMiddleware(ctx, next) {
|
|
5
|
+
const allowFrom = ctx.config.channels?.[ctx.platform]?.allowFrom;
|
|
6
|
+
if (allowFrom && allowFrom !== '*') {
|
|
7
|
+
const senderId = ctx.contactId.replace('tg_', '');
|
|
8
|
+
const allowed = Array.isArray(allowFrom) ? allowFrom.includes(senderId) : String(allowFrom) === senderId;
|
|
9
|
+
if (!allowed) {
|
|
10
|
+
ctx.handled = true;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
await next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects and saves API keys pasted in chat
|
|
3
|
+
*/
|
|
4
|
+
export async function apiKeyDetectMiddleware(ctx, next) {
|
|
5
|
+
const { detectApiKey, saveApiKey, getKeyConfirmation } = await import('../features/self-config.js');
|
|
6
|
+
|
|
7
|
+
const keyDetected = detectApiKey(ctx.message);
|
|
8
|
+
if (keyDetected && keyDetected.provider !== 'unknown') {
|
|
9
|
+
saveApiKey(keyDetected.provider, keyDetected.key);
|
|
10
|
+
const { loadConfig } = await import('../core/config.js');
|
|
11
|
+
ctx.engine.config = loadConfig();
|
|
12
|
+
ctx.config = ctx.engine.config;
|
|
13
|
+
await ctx.reply(getKeyConfirmation(keyDetected.provider));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await next();
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detect and read URLs in messages
|
|
3
|
+
*/
|
|
4
|
+
export async function autoLinksMiddleware(ctx, next) {
|
|
5
|
+
const urlRegex = /https?:\/\/[^\s<>"')\]]+/gi;
|
|
6
|
+
if (urlRegex.test(ctx.message) && ctx.engine.toolRouter?.browser) {
|
|
7
|
+
try {
|
|
8
|
+
const { extractAndReadLinks } = await import('../features/auto-links.js');
|
|
9
|
+
const content = await extractAndReadLinks(ctx.message, ctx.engine.toolRouter.browser);
|
|
10
|
+
if (content) ctx.metadata._linkContext = content;
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
await next();
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract facts from messages automatically
|
|
3
|
+
*/
|
|
4
|
+
export async function autoMemoryMiddleware(ctx, next) {
|
|
5
|
+
if (ctx.engine.autoMemory) {
|
|
6
|
+
try { await ctx.engine.autoMemory.extract(ctx.agentId, ctx.contactId, ctx.message); } catch {}
|
|
7
|
+
}
|
|
8
|
+
await next();
|
|
9
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle slash commands: /help, /status, /backup, /memories, /tasks, /usage
|
|
3
|
+
*/
|
|
4
|
+
export async function commandsMiddleware(ctx, next) {
|
|
5
|
+
const msg = ctx.message.trim();
|
|
6
|
+
const cmd = msg.startsWith('/') ? msg.split(' ')[0].toLowerCase() : null;
|
|
7
|
+
|
|
8
|
+
// Also match plain text commands
|
|
9
|
+
const textCmd = msg.toLowerCase();
|
|
10
|
+
|
|
11
|
+
if (cmd === '/help') {
|
|
12
|
+
await ctx.reply([
|
|
13
|
+
'🦑 *Commands*', '',
|
|
14
|
+
'/status — model, uptime, usage stats',
|
|
15
|
+
'/backup — save me to a backup file',
|
|
16
|
+
'/memories — what I remember about you',
|
|
17
|
+
'/tasks — your todo list',
|
|
18
|
+
'/usage — spending report',
|
|
19
|
+
'/help — this message',
|
|
20
|
+
'', 'Just chat normally! 🦑',
|
|
21
|
+
].join('\n'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (cmd === '/status' || textCmd === 'status') {
|
|
26
|
+
const uptime = process.uptime();
|
|
27
|
+
const h = Math.floor(uptime / 3600);
|
|
28
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
29
|
+
const usage = await ctx.storage.getUsage(ctx.agentId) || {};
|
|
30
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
31
|
+
const fmtT = tokens >= 1e6 ? (tokens/1e6).toFixed(1)+'M' : tokens >= 1e3 ? (tokens/1e3).toFixed(1)+'K' : String(tokens);
|
|
32
|
+
const waOn = Object.values(ctx.engine.whatsappManager?.getStatuses() || {}).some(s => s.connected);
|
|
33
|
+
|
|
34
|
+
await ctx.reply([
|
|
35
|
+
`🦑 *${ctx.agent?.name || ctx.agentId} Status*`,
|
|
36
|
+
'────────────────────',
|
|
37
|
+
`🧠 Model: ${ctx.agent?.model || 'unknown'}`,
|
|
38
|
+
`⏱️ Uptime: ${h}h ${m}m`,
|
|
39
|
+
`💬 Messages: ${usage.messages || 0}`,
|
|
40
|
+
`🪙 Tokens: ${fmtT}`,
|
|
41
|
+
`💰 Cost: $${(usage.cost_usd || 0).toFixed(4)}`,
|
|
42
|
+
'────────────────────',
|
|
43
|
+
`📱 WhatsApp: ${waOn ? '✅' : '❌'}`,
|
|
44
|
+
`✈️ Telegram: ${ctx.engine.telegramManager ? '✅' : '❌'}`,
|
|
45
|
+
'────────────────────',
|
|
46
|
+
`⚡ Skills: 20+`,
|
|
47
|
+
`🗣️ Language: ${ctx.agent?.language || 'bilingual'}`,
|
|
48
|
+
].join('\n'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cmd === '/backup' || (textCmd.includes('backup') && textCmd.includes('yourself'))) {
|
|
53
|
+
try {
|
|
54
|
+
const { AgentBackup } = await import('../features/backup.js');
|
|
55
|
+
const { mkdirSync } = await import('fs');
|
|
56
|
+
const backup = await AgentBackup.create(ctx.agentId, ctx.engine.home, ctx.storage);
|
|
57
|
+
const filename = (ctx.agent?.name || ctx.agentId) + '_backup_' + new Date().toISOString().slice(0, 10) + '.json';
|
|
58
|
+
mkdirSync(ctx.engine.home + '/backups', { recursive: true });
|
|
59
|
+
await AgentBackup.saveToFile(backup, ctx.engine.home + '/backups/' + filename);
|
|
60
|
+
const size = (JSON.stringify(backup).length / 1024).toFixed(1);
|
|
61
|
+
await ctx.reply([
|
|
62
|
+
'💾 *Backup Complete!*', '',
|
|
63
|
+
'📦 ' + filename,
|
|
64
|
+
'📏 ' + size + ' KB',
|
|
65
|
+
'💬 ' + (backup.conversations?.length || 0) + ' messages',
|
|
66
|
+
'🧠 ' + (backup.memories?.length || 0) + ' memories',
|
|
67
|
+
'', 'I am safe! 🦑',
|
|
68
|
+
].join('\n'));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
await ctx.reply('❌ Backup failed: ' + err.message);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (cmd === '/memories' || cmd === '/remember') {
|
|
76
|
+
const memories = await ctx.storage.getMemories(ctx.agentId);
|
|
77
|
+
if (memories.length === 0) {
|
|
78
|
+
await ctx.reply('🧠 No memories yet. Tell me things!');
|
|
79
|
+
} else {
|
|
80
|
+
const list = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
|
|
81
|
+
await ctx.reply('🧠 *What I Remember*\n\n' + list);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cmd === '/tasks' || cmd === '/todo') {
|
|
87
|
+
try {
|
|
88
|
+
const { TaskManager } = await import('../tools/tasks.js');
|
|
89
|
+
const tm = new TaskManager(ctx.storage);
|
|
90
|
+
const tasks = tm.list(ctx.agentId, ctx.contactId);
|
|
91
|
+
if (tasks.length === 0) {
|
|
92
|
+
await ctx.reply('📋 No pending tasks! Tell me to add one.');
|
|
93
|
+
} else {
|
|
94
|
+
const list = tasks.map((t, i) => (i + 1) + '. ' + t.task).join('\n');
|
|
95
|
+
await ctx.reply('📋 *Your Tasks*\n\n' + list);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
await ctx.reply('❌ ' + err.message);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (cmd === '/usage') {
|
|
104
|
+
try {
|
|
105
|
+
const { UsageAlerts } = await import('../features/usage-alerts.js');
|
|
106
|
+
const ua = new UsageAlerts(ctx.storage);
|
|
107
|
+
const s = await ua.getSummary(ctx.agentId);
|
|
108
|
+
const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n);
|
|
109
|
+
await ctx.reply([
|
|
110
|
+
'📊 *Usage Report*', '',
|
|
111
|
+
'*Today:*', ' 💬 ' + s.today.calls + ' msgs · 🪙 ' + fmtT(s.today.tokens) + ' tokens · 💰 $' + s.today.cost, '',
|
|
112
|
+
'*30 days:*', ' 💬 ' + s.month.calls + ' msgs · 🪙 ' + fmtT(s.month.tokens) + ' tokens · 💰 $' + s.month.cost,
|
|
113
|
+
].join('\n'));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
await ctx.reply('❌ ' + err.message);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await next();
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process media: voice → transcribe, image → analyze, document → ingest
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function mediaMiddleware(ctx, next) {
|
|
7
|
+
if (!ctx.metadata._ctx || !ctx.metadata.mediaType) {
|
|
8
|
+
await next();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const token = ctx.config.channels?.telegram?.token;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (ctx.metadata.mediaType === 'audio') {
|
|
16
|
+
await _transcribeVoice(ctx, token);
|
|
17
|
+
} else if (ctx.metadata.mediaType === 'image') {
|
|
18
|
+
await _analyzeImage(ctx, token);
|
|
19
|
+
} else if (ctx.metadata.mediaType === 'document') {
|
|
20
|
+
await _ingestDocument(ctx, token);
|
|
21
|
+
if (ctx.handled) return;
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logger.error('media', 'Processing error: ' + err.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await next();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function _transcribeVoice(ctx, token) {
|
|
31
|
+
const file = await ctx.metadata._ctx.getFile();
|
|
32
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
33
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
34
|
+
|
|
35
|
+
const groqKey = ctx.config.ai?.providers?.groq?.key;
|
|
36
|
+
const openaiKey = ctx.config.ai?.providers?.openai?.key;
|
|
37
|
+
const apiKey = groqKey || openaiKey;
|
|
38
|
+
if (!apiKey) return;
|
|
39
|
+
|
|
40
|
+
const apiUrl = groqKey ? 'https://api.groq.com/openai/v1/audio/transcriptions' : 'https://api.openai.com/v1/audio/transcriptions';
|
|
41
|
+
const model = groqKey ? 'whisper-large-v3' : 'whisper-1';
|
|
42
|
+
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.append('file', new Blob([buffer], { type: 'audio/ogg' }), 'voice.ogg');
|
|
45
|
+
form.append('model', model);
|
|
46
|
+
|
|
47
|
+
const res = await fetch(apiUrl, { method: 'POST', headers: { 'Authorization': 'Bearer ' + apiKey }, body: form });
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (data.text) {
|
|
50
|
+
ctx.message = '[Voice note]: "' + data.text + '"';
|
|
51
|
+
ctx.metadata.originalType = 'voice';
|
|
52
|
+
logger.info('media', 'Transcribed: ' + data.text.slice(0, 50));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function _analyzeImage(ctx, token) {
|
|
57
|
+
const photos = ctx.metadata._ctx.message?.photo;
|
|
58
|
+
if (!photos?.length) return;
|
|
59
|
+
|
|
60
|
+
const photo = photos[photos.length - 1];
|
|
61
|
+
const file = await ctx.metadata._ctx.api.getFile(photo.file_id);
|
|
62
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
63
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
64
|
+
const base64 = buffer.toString('base64');
|
|
65
|
+
|
|
66
|
+
const anthropicKey = ctx.config.ai?.providers?.anthropic?.key;
|
|
67
|
+
if (!anthropicKey) return;
|
|
68
|
+
|
|
69
|
+
const caption = ctx.message.replace('[📸 Image]', '').trim();
|
|
70
|
+
const prompt = caption || 'What is in this image? Be concise.';
|
|
71
|
+
|
|
72
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'x-api-key': anthropicKey, 'content-type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
model: 'claude-sonnet-4-20250514',
|
|
77
|
+
max_tokens: 300,
|
|
78
|
+
messages: [{ role: 'user', content: [
|
|
79
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: base64 } },
|
|
80
|
+
{ type: 'text', text: prompt },
|
|
81
|
+
]}],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
const analysis = data.content?.[0]?.text;
|
|
86
|
+
if (analysis) {
|
|
87
|
+
ctx.message = caption
|
|
88
|
+
? `[Image: "${caption}"] ${analysis}`
|
|
89
|
+
: `[Image] ${analysis}`;
|
|
90
|
+
logger.info('media', 'Analyzed image: ' + analysis.slice(0, 50));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function _ingestDocument(ctx, token) {
|
|
95
|
+
const file = await ctx.metadata._ctx.getFile();
|
|
96
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
97
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
98
|
+
const filename = ctx.message.match(/Document: (.+?)\]/)?.[1] || 'file.txt';
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { DocIngester } = await import('../features/doc-ingest.js');
|
|
102
|
+
const ingester = new DocIngester(ctx.storage, ctx.engine.knowledgeBase, ctx.engine.home);
|
|
103
|
+
const result = await ingester.ingest(ctx.agentId, buffer, filename, ctx.metadata.mimeType);
|
|
104
|
+
await ctx.reply(`📄 *Document absorbed!*\n📁 ${filename}\n📊 ${result.chunks} chunks\n📝 ${result.chars} chars\n\nAsk me anything about it! 🦑`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
ctx.message = `[Document: ${filename}] (Error: ${err.message})`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Message Pipeline
|
|
3
|
+
* Clean middleware-based message processing
|
|
4
|
+
*
|
|
5
|
+
* Flow: message → [middleware1] → [middleware2] → ... → response
|
|
6
|
+
* Each middleware can: modify context, short-circuit (return response), or pass through
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../core/logger.js';
|
|
10
|
+
|
|
11
|
+
export class MessagePipeline {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.middleware = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add middleware to the pipeline
|
|
18
|
+
* @param {string} name - middleware name for logging
|
|
19
|
+
* @param {Function} fn - async (ctx, next) => void
|
|
20
|
+
*/
|
|
21
|
+
use(name, fn) {
|
|
22
|
+
this.middleware.push({ name, fn });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Process a message through the pipeline
|
|
27
|
+
* @param {object} ctx - message context
|
|
28
|
+
* @returns {object} ctx with response populated
|
|
29
|
+
*/
|
|
30
|
+
async process(ctx) {
|
|
31
|
+
let index = 0;
|
|
32
|
+
|
|
33
|
+
const next = async () => {
|
|
34
|
+
if (index >= this.middleware.length) return;
|
|
35
|
+
if (ctx.handled) return; // short-circuited
|
|
36
|
+
|
|
37
|
+
const mw = this.middleware[index++];
|
|
38
|
+
try {
|
|
39
|
+
await mw.fn(ctx, next);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
logger.error('pipeline', `Middleware "${mw.name}" error: ${err.message}`);
|
|
42
|
+
// Don't crash — continue to next middleware
|
|
43
|
+
await next();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await next();
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a message context object
|
|
54
|
+
*/
|
|
55
|
+
export function createContext(engine, agentId, contactId, message, metadata) {
|
|
56
|
+
return {
|
|
57
|
+
// Input
|
|
58
|
+
engine,
|
|
59
|
+
agentId,
|
|
60
|
+
contactId,
|
|
61
|
+
message, // mutable — middleware can modify
|
|
62
|
+
originalMessage: message,
|
|
63
|
+
metadata,
|
|
64
|
+
platform: metadata.platform || 'telegram',
|
|
65
|
+
|
|
66
|
+
// Agent
|
|
67
|
+
agent: engine.agentManager.get(agentId),
|
|
68
|
+
config: engine.config,
|
|
69
|
+
storage: engine.storage,
|
|
70
|
+
|
|
71
|
+
// State (middleware populates these)
|
|
72
|
+
handled: false, // true = stop pipeline, send response
|
|
73
|
+
response: null, // { messages: string[], reaction: string|null, image: object|null, voice: Buffer|null, _reminder: object|null }
|
|
74
|
+
linkContext: null, // fetched URL content
|
|
75
|
+
memoryExtracted: [], // auto-extracted facts
|
|
76
|
+
|
|
77
|
+
// Helpers
|
|
78
|
+
reply: async function(text) {
|
|
79
|
+
this.handled = true;
|
|
80
|
+
this.response = { messages: [text] };
|
|
81
|
+
},
|
|
82
|
+
replyMulti: async function(messages) {
|
|
83
|
+
this.handled = true;
|
|
84
|
+
this.response = { messages };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send the response back to the user via the appropriate channel
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function responseSenderMiddleware(ctx, next) {
|
|
7
|
+
if (!ctx.response) return;
|
|
8
|
+
|
|
9
|
+
const tm = ctx.engine.telegramManager;
|
|
10
|
+
const wm = ctx.engine.whatsappManager;
|
|
11
|
+
const { agentId, contactId, metadata, response } = ctx;
|
|
12
|
+
|
|
13
|
+
// Handle reminder
|
|
14
|
+
if (response._reminder && ctx.engine.reminders) {
|
|
15
|
+
ctx.engine.reminders.add(agentId, contactId, response._reminder.message, response._reminder.time, ctx.platform, metadata);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Send reaction
|
|
19
|
+
if (response.reaction && metadata.messageId) {
|
|
20
|
+
try {
|
|
21
|
+
if (ctx.platform === 'telegram' && tm) {
|
|
22
|
+
await tm.sendReaction(agentId, contactId, metadata.messageId, response.reaction, metadata);
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// No messages to send
|
|
28
|
+
if (!response.messages?.length && !response.image) return;
|
|
29
|
+
|
|
30
|
+
// Send via appropriate channel
|
|
31
|
+
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
+
if (response.image) {
|
|
33
|
+
const photoData = response.image.url ? { url: response.image.url } : { base64: response.image.base64 };
|
|
34
|
+
await tm.sendPhoto(agentId, contactId, photoData, response.messages?.[0] || '', metadata);
|
|
35
|
+
} else if (metadata.originalType === 'voice' && response.messages.length === 1 && response.messages[0].length < 500) {
|
|
36
|
+
try {
|
|
37
|
+
const { VoiceReply } = await import('../features/voice-reply.js');
|
|
38
|
+
const vr = new VoiceReply(ctx.config);
|
|
39
|
+
const lang = /[\u0600-\u06FF]/.test(response.messages[0]) ? 'ar' : 'en';
|
|
40
|
+
const audio = await vr.generate(response.messages[0], { language: lang });
|
|
41
|
+
await tm.sendVoice(agentId, contactId, audio, metadata);
|
|
42
|
+
} catch {
|
|
43
|
+
await tm.sendMessages(agentId, contactId, response.messages, metadata);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
await tm.sendMessages(agentId, contactId, response.messages, metadata);
|
|
47
|
+
}
|
|
48
|
+
} else if (wm) {
|
|
49
|
+
for (const msg of response.messages) {
|
|
50
|
+
await wm.sendMessage(agentId, contactId, msg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await next();
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if user is asking for a skill that needs an API key
|
|
3
|
+
*/
|
|
4
|
+
export async function skillCheckMiddleware(ctx, next) {
|
|
5
|
+
const { detectSkillRequest, checkSkillAvailable, getKeyRequestMessage } = await import('../features/self-config.js');
|
|
6
|
+
const skill = detectSkillRequest(ctx.message);
|
|
7
|
+
if (skill) {
|
|
8
|
+
const avail = checkSkillAvailable(skill, ctx.config);
|
|
9
|
+
if (!avail.available) {
|
|
10
|
+
const msg = getKeyRequestMessage(skill);
|
|
11
|
+
if (msg) { await ctx.reply(msg); return; }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
await next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show typing indicator while AI processes
|
|
3
|
+
* Wraps the next() call with typing on/off
|
|
4
|
+
*/
|
|
5
|
+
export async function typingMiddleware(ctx, next) {
|
|
6
|
+
if (ctx.handled || ctx.platform !== 'telegram') {
|
|
7
|
+
await next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const chatId = ctx.metadata.chatId || ctx.contactId.replace('tg_', '');
|
|
12
|
+
const botInfo = ctx.engine.telegramManager?.bots?.get(ctx.agentId);
|
|
13
|
+
|
|
14
|
+
let interval;
|
|
15
|
+
if (botInfo?.bot) {
|
|
16
|
+
const sendTyping = () => { try { botInfo.bot.api.sendChatAction(chatId, 'typing').catch(() => {}); } catch {} };
|
|
17
|
+
sendTyping();
|
|
18
|
+
interval = setInterval(sendTyping, 4000);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await next();
|
|
23
|
+
} finally {
|
|
24
|
+
if (interval) clearInterval(interval);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check spending thresholds and alert
|
|
3
|
+
*/
|
|
4
|
+
export async function usageAlertsMiddleware(ctx, next) {
|
|
5
|
+
await next(); // Run after AI responds
|
|
6
|
+
|
|
7
|
+
if (ctx.engine.usageAlerts) {
|
|
8
|
+
try {
|
|
9
|
+
const alert = await ctx.engine.usageAlerts.check(ctx.agentId);
|
|
10
|
+
if (alert.alert) {
|
|
11
|
+
// Append alert to response
|
|
12
|
+
const alertMsg = '⚠️ *Usage Alert* — $' + alert.total + ' spent in 24h';
|
|
13
|
+
if (ctx.response?.messages) {
|
|
14
|
+
ctx.response.messages.push(alertMsg);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|