squidclaw 3.2.0 → 3.2.1
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/ai/gateway.js +4 -2
- package/lib/ai/prompt-builder.js +8 -4
- package/lib/channels/telegram/bot.js +7 -0
- package/lib/core/agent.js +7 -2
- package/lib/engine.js +1 -1
- package/lib/features/auto-memory.js +58 -41
- package/lib/features/config-manager.js +6 -3
- package/lib/middleware/response-sender.js +49 -7
- package/lib/tools/slides-engine.js +9 -9
- package/package.json +1 -1
package/lib/ai/gateway.js
CHANGED
|
@@ -31,8 +31,10 @@ export class AIGateway {
|
|
|
31
31
|
// Smart routing: use best model for the task
|
|
32
32
|
let routedModel = options.model;
|
|
33
33
|
if (!routedModel && options.taskHint) {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
try {
|
|
35
|
+
const route = getRouteForTask(options.taskHint, options.toolName, this.config);
|
|
36
|
+
if (route) routedModel = route.model;
|
|
37
|
+
} catch {}
|
|
36
38
|
}
|
|
37
39
|
const model = routedModel || this.config.ai?.defaultModel;
|
|
38
40
|
const fallbackChain = options.fallbackChain || this.config.ai?.fallbackChain || [];
|
package/lib/ai/prompt-builder.js
CHANGED
|
@@ -74,11 +74,15 @@ export class PromptBuilder {
|
|
|
74
74
|
|
|
75
75
|
// 4. Contact context
|
|
76
76
|
const contact = await this.storage.getContact(agent.id, contactId);
|
|
77
|
+
parts.push('\n## About This Person');
|
|
77
78
|
if (contact) {
|
|
78
|
-
parts.push(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
if (contact.name) parts.push('- Name: ' + contact.name);
|
|
80
|
+
parts.push('- Messages exchanged: ' + (contact.message_count || 0));
|
|
81
|
+
}
|
|
82
|
+
// Also check memories for name (more reliable)
|
|
83
|
+
const memName = memories.find(m => m.key === 'name');
|
|
84
|
+
if (memName && memName.value) {
|
|
85
|
+
parts.push('- Confirmed name: ' + memName.value + ' (USE THIS NAME)');
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
// 5. Knowledge base (RAG)
|
|
@@ -157,6 +157,13 @@ export class TelegramManager {
|
|
|
157
157
|
/**
|
|
158
158
|
* Send a reaction
|
|
159
159
|
*/
|
|
160
|
+
async sendChatAction(agentId, contactId, action, metadata = {}) {
|
|
161
|
+
const botInfo = this.bots.get(agentId);
|
|
162
|
+
if (!botInfo?.bot) return;
|
|
163
|
+
const chatId = metadata.chatId || contactId.replace('tg_', '');
|
|
164
|
+
try { await botInfo.bot.api.sendChatAction(chatId, action); } catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
160
167
|
async sendReaction(agentId, contactId, messageId, emoji, metadata = {}) {
|
|
161
168
|
const botInfo = this.bots.get(agentId);
|
|
162
169
|
if (!botInfo?.bot) return;
|
package/lib/core/agent.js
CHANGED
|
@@ -66,12 +66,17 @@ export class Agent {
|
|
|
66
66
|
const systemPrompt = await this.promptBuilder.build(this, contactId, message);
|
|
67
67
|
|
|
68
68
|
// Get conversation history
|
|
69
|
-
const history = await this.storage.getConversation(this.id, contactId,
|
|
69
|
+
const history = await this.storage.getConversation(this.id, contactId, 20);
|
|
70
70
|
|
|
71
71
|
// Build messages array for AI
|
|
72
72
|
const messages = [
|
|
73
73
|
{ role: 'system', content: systemPrompt },
|
|
74
|
-
...history.map(h => ({
|
|
74
|
+
...history.map(h => ({
|
|
75
|
+
role: h.role,
|
|
76
|
+
content: h.role === 'assistant'
|
|
77
|
+
? h.content.replace(/---TOOL:\w+:.{0,200}---/gs, '[tool used]').slice(0, 500)
|
|
78
|
+
: h.content.slice(0, 500),
|
|
79
|
+
})),
|
|
75
80
|
];
|
|
76
81
|
|
|
77
82
|
// If the last message in history is already the current message, don't add again
|
package/lib/engine.js
CHANGED
|
@@ -117,7 +117,7 @@ export class SquidclawEngine {
|
|
|
117
117
|
initLogger(this.config.engine?.logLevel || 'info');
|
|
118
118
|
|
|
119
119
|
console.log(`
|
|
120
|
-
🦑 Squidclaw Engine
|
|
120
|
+
🦑 Squidclaw Engine v${require('./package.json').version}
|
|
121
121
|
──────────────────────────`);
|
|
122
122
|
|
|
123
123
|
// 1. Storage
|
|
@@ -1,47 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🦑 Auto Memory —
|
|
3
|
-
*
|
|
2
|
+
* 🦑 Auto Memory v2 — Smarter fact extraction
|
|
3
|
+
* Fixes: no more saving "ur" as a name, better filtering
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { logger } from '../core/logger.js';
|
|
7
7
|
|
|
8
|
+
// Words that should NEVER be saved as names
|
|
9
|
+
const NAME_BLACKLIST = new Set([
|
|
10
|
+
'ur', 'u', 'im', 'me', 'my', 'i', 'the', 'a', 'an', 'is', 'am', 'are',
|
|
11
|
+
'here', 'there', 'this', 'that', 'it', 'he', 'she', 'they', 'we', 'you',
|
|
12
|
+
'not', 'no', 'yes', 'ok', 'okay', 'hi', 'hey', 'hello', 'bye',
|
|
13
|
+
'just', 'very', 'really', 'actually', 'basically', 'literally',
|
|
14
|
+
'new', 'old', 'good', 'bad', 'great', 'nice', 'cool', 'fine',
|
|
15
|
+
'going', 'doing', 'working', 'looking', 'trying', 'getting',
|
|
16
|
+
'available', 'interested', 'confused', 'tired', 'busy', 'free',
|
|
17
|
+
'happy', 'sad', 'angry', 'bored', 'excited', 'worried',
|
|
18
|
+
'still', 'also', 'too', 'now', 'then', 'so', 'but',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// Must look like a real name: 2+ chars, starts with letter, no pure numbers
|
|
22
|
+
function isValidName(name) {
|
|
23
|
+
if (!name || name.length < 2 || name.length > 30) return false;
|
|
24
|
+
if (NAME_BLACKLIST.has(name.toLowerCase())) return false;
|
|
25
|
+
if (/^\d+$/.test(name)) return false;
|
|
26
|
+
if (/^[a-z]/.test(name) && !/[\u0600-\u06FF]/.test(name)) return false; // English names must be capitalized
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Must look like a real location
|
|
31
|
+
function isValidLocation(loc) {
|
|
32
|
+
if (!loc || loc.length < 2 || loc.length > 40) return false;
|
|
33
|
+
if (/^(here|there|home|work|office|bed|room|outside)$/i.test(loc.trim())) return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
const FACT_PATTERNS = [
|
|
9
|
-
// Names
|
|
10
|
-
{ regex: /(?:my name is|call me
|
|
38
|
+
// Names — stricter matching
|
|
39
|
+
{ regex: /(?:my name is|call me|اسمي)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF]{1,25})/i, key: 'name', extract: 1, validate: isValidName },
|
|
40
|
+
|
|
41
|
+
// "I'm Tamer" — but ONLY if the word after is a capitalized proper noun (not "I'm fine")
|
|
42
|
+
{ regex: /^(?:i'?m|ana)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF]{1,25})$/im, key: 'name', extract: 1, validate: isValidName },
|
|
11
43
|
|
|
12
44
|
// Location
|
|
13
|
-
{ regex: /(?:i live in|i'?m from|
|
|
45
|
+
{ regex: /(?:i live in|i'?m from|based in|located in|ساكن في|من مدينة)\s+([A-Z\u0600-\u06FF][\w\u0600-\u06FF\s]{2,30})/i, key: 'location', extract: 1, validate: isValidLocation },
|
|
14
46
|
|
|
15
47
|
// Job
|
|
16
|
-
{ regex: /(?:i work (?:as|at|in|for)|my job is
|
|
48
|
+
{ regex: /(?:i work (?:as|at|in|for)|my job is|اشتغل|شغلي)\s+(.{3,40}?)(?:\.|,|!|\?|$)/i, key: 'job', extract: 1 },
|
|
17
49
|
|
|
18
|
-
// Age
|
|
19
|
-
{ regex: /(?:i'?m|i am|عمري)\s+(\d{1,3})\s*(?:years? old|سنة|سنه)
|
|
50
|
+
// Age — must be reasonable
|
|
51
|
+
{ regex: /(?:i'?m|i am|عمري)\s+(\d{1,3})\s*(?:years? old|سنة|سنه)/i, key: 'age', extract: 1, validate: v => { const n = parseInt(v); return n > 5 && n < 120; } },
|
|
20
52
|
|
|
21
53
|
// Family
|
|
22
|
-
{ regex: /(?:my (?:wife|husband|son|daughter|brother|sister|mom|dad|father|mother)(?:'s name)? is)\s+(\w+)/i, key: (m) => m[0].match(/wife|husband|son|daughter|brother|sister|mom|dad|father|mother/i)[0], extract: 1 },
|
|
54
|
+
{ regex: /(?:my (?:wife|husband|son|daughter|brother|sister|mom|dad|father|mother)(?:'s name)? is)\s+([A-Z\u0600-\u06FF]\w+)/i, key: (m) => m[0].match(/wife|husband|son|daughter|brother|sister|mom|dad|father|mother/i)[0], extract: 1, validate: isValidName },
|
|
23
55
|
|
|
24
56
|
// Favorites
|
|
25
|
-
{ regex: /(?:my fav(?:orite|ourite)?\s+(\w+)\s+is)\s+(
|
|
26
|
-
{ regex: /(?:i (?:love|like|prefer|enjoy))\s+(.{3,30}?)(?:\.|,|!|\?|$)/i, key: 'likes', extract: 1, append: true },
|
|
27
|
-
{ regex: /(?:i (?:hate|dislike|don'?t like))\s+(.{3,30}?)(?:\.|,|!|\?|$)/i, key: 'dislikes', extract: 1, append: true },
|
|
28
|
-
|
|
29
|
-
// Timezone
|
|
30
|
-
{ regex: /(?:my time(?:zone)? is|i'?m in)\s+(UTC[+-]\d+|GMT[+-]\d+|[A-Z]{2,4}\/[A-Za-z_]+)/i, key: 'timezone', extract: 1 },
|
|
57
|
+
{ regex: /(?:my fav(?:orite|ourite)?\s+(\w+)\s+is)\s+(.{2,30}?)(?:\.|,|!|$)/i, key: (m) => 'favorite_' + m[1], extract: 2 },
|
|
31
58
|
|
|
32
59
|
// Birthday
|
|
33
|
-
{ regex: /(?:my birthday is|born on|ميلادي)\s+(.{5,
|
|
60
|
+
{ regex: /(?:my birthday is|born on|ميلادي)\s+(.{5,25})/i, key: 'birthday', extract: 1 },
|
|
34
61
|
|
|
35
62
|
// Language
|
|
36
|
-
{ regex: /(?:i speak|my language is|لغتي)\s+(\w+)/i, key: 'language', extract: 1 },
|
|
63
|
+
{ regex: /(?:i speak|my language is|لغتي)\s+([A-Z\u0600-\u06FF]\w+)/i, key: 'language', extract: 1 },
|
|
37
64
|
|
|
38
65
|
// Pet
|
|
39
|
-
{ regex: /(?:my (?:cat|dog|pet)(?:'s name)? is)\s+(\w+)/i, key: (m) => m[0].match(/cat|dog|pet/i)[0] + '_name', extract: 1 },
|
|
66
|
+
{ regex: /(?:my (?:cat|dog|pet)(?:'s name)? is)\s+([A-Z\u0600-\u06FF]\w+)/i, key: (m) => m[0].match(/cat|dog|pet/i)[0] + '_name', extract: 1, validate: isValidName },
|
|
40
67
|
];
|
|
41
68
|
|
|
42
|
-
// Extract per-contact facts
|
|
43
69
|
const CONTACT_PATTERNS = [
|
|
44
|
-
{ regex: /(?:remember|don'?t forget|note|تذكر|لا تنسى)\s+(?:that\s+)?(.{5,100})/i, key: 'noted', extract: 1 },
|
|
70
|
+
{ regex: /(?:remember|don'?t forget|note that|تذكر|لا تنسى)\s+(?:that\s+)?(.{5,100})/i, key: 'noted', extract: 1 },
|
|
45
71
|
];
|
|
46
72
|
|
|
47
73
|
export class AutoMemory {
|
|
@@ -49,10 +75,10 @@ export class AutoMemory {
|
|
|
49
75
|
this.storage = storage;
|
|
50
76
|
}
|
|
51
77
|
|
|
52
|
-
/**
|
|
53
|
-
* Scan a user message for facts and auto-save them
|
|
54
|
-
*/
|
|
55
78
|
async extract(agentId, contactId, message) {
|
|
79
|
+
// Skip very short messages or commands
|
|
80
|
+
if (!message || message.length < 4 || message.startsWith('/')) return [];
|
|
81
|
+
|
|
56
82
|
const extracted = [];
|
|
57
83
|
|
|
58
84
|
for (const pattern of FACT_PATTERNS) {
|
|
@@ -62,23 +88,20 @@ export class AutoMemory {
|
|
|
62
88
|
const key = typeof pattern.key === 'function' ? pattern.key(match) : pattern.key;
|
|
63
89
|
const value = match[pattern.extract].trim();
|
|
64
90
|
|
|
91
|
+
// Validate
|
|
65
92
|
if (value.length < 2 || value.length > 100) continue;
|
|
93
|
+
if (pattern.validate && !pattern.validate(value)) continue;
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (existing && existing.includes(value)) continue;
|
|
71
|
-
const newValue = existing ? existing + ', ' + value : value;
|
|
72
|
-
await this.storage.saveMemory(agentId, key, newValue, 'auto');
|
|
73
|
-
} else {
|
|
74
|
-
await this.storage.saveMemory(agentId, key, value, 'auto');
|
|
75
|
-
}
|
|
95
|
+
// Don't overwrite with worse data
|
|
96
|
+
const existing = await this._getMemory(agentId, key);
|
|
97
|
+
if (existing && key === 'name' && isValidName(existing) && existing.length > value.length) continue;
|
|
76
98
|
|
|
99
|
+
await this.storage.saveMemory(agentId, key, value, 'auto');
|
|
77
100
|
extracted.push({ key, value });
|
|
78
101
|
logger.info('auto-memory', `Extracted: ${key} = ${value}`);
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
//
|
|
104
|
+
// "Remember this" patterns
|
|
82
105
|
for (const pattern of CONTACT_PATTERNS) {
|
|
83
106
|
const match = message.match(pattern.regex);
|
|
84
107
|
if (!match) continue;
|
|
@@ -88,7 +111,6 @@ export class AutoMemory {
|
|
|
88
111
|
const key = 'user_note_' + Date.now().toString(36);
|
|
89
112
|
await this.storage.saveMemory(agentId, key, value, 'noted');
|
|
90
113
|
extracted.push({ key, value });
|
|
91
|
-
logger.info('auto-memory', `User note: ${value}`);
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
return extracted;
|
|
@@ -96,12 +118,7 @@ export class AutoMemory {
|
|
|
96
118
|
|
|
97
119
|
async _getMemory(agentId, key) {
|
|
98
120
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
).get(agentId, key);
|
|
102
|
-
return row?.value;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
121
|
+
return this.storage.db.prepare('SELECT value FROM memories WHERE agent_id = ? AND key = ?').get(agentId, key)?.value;
|
|
122
|
+
} catch { return null; }
|
|
106
123
|
}
|
|
107
124
|
}
|
|
@@ -212,11 +212,12 @@ export class ConfigManager {
|
|
|
212
212
|
static parseConfigCommand(message) {
|
|
213
213
|
const lower = message.toLowerCase().trim();
|
|
214
214
|
|
|
215
|
-
// "switch to gpt-4o" / "use gemini"
|
|
216
|
-
const modelMatch = lower.match(
|
|
215
|
+
// "switch to gpt-4o" / "use gemini" — must be the MAIN intent (start of message)
|
|
216
|
+
const modelMatch = lower.match(/^(?:switch|change|use)\s+(?:to\s+)?(?:model\s+)?(\S+(?:\s+\S+){0,3})$/);
|
|
217
217
|
if (modelMatch) {
|
|
218
218
|
const model = modelMatch[1].trim();
|
|
219
|
-
//
|
|
219
|
+
// Only match if it looks like a model name (known alias or contains provider-like patterns)
|
|
220
|
+
const knownPatterns = /^(gpt|claude|sonnet|opus|haiku|gemini|flash|llama|deepseek|mistral|groq|ollama|o[1-4]|\S+-\d)/i;
|
|
220
221
|
const aliases = {
|
|
221
222
|
'gpt4': 'gpt-4o', 'gpt-4': 'gpt-4o', 'gpt4o': 'gpt-4o',
|
|
222
223
|
'claude': 'claude-sonnet-4-20250514', 'sonnet': 'claude-sonnet-4-20250514',
|
|
@@ -224,6 +225,8 @@ export class ConfigManager {
|
|
|
224
225
|
'gemini': 'gemini-2.5-flash', 'flash': 'gemini-2.5-flash',
|
|
225
226
|
'llama': 'llama-3.3-70b-versatile', 'deepseek': 'deepseek-chat',
|
|
226
227
|
};
|
|
228
|
+
if (!knownPatterns.test(model) && !aliases[model]) return null; // Not a model name
|
|
229
|
+
// Map common names to actual model IDs
|
|
227
230
|
return { action: 'set_model', model: aliases[model] || model };
|
|
228
231
|
}
|
|
229
232
|
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Send the response back to the user via the appropriate channel
|
|
3
|
+
* With human-like timing — small delays to feel natural
|
|
3
4
|
*/
|
|
4
5
|
import { logger } from '../core/logger.js';
|
|
5
6
|
|
|
7
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
8
|
+
|
|
9
|
+
// Calculate human-like "typing" delay based on message length
|
|
10
|
+
function typingDelay(text) {
|
|
11
|
+
const len = (text || '').length;
|
|
12
|
+
if (len < 20) return 400 + Math.random() * 300; // Short: 0.4-0.7s
|
|
13
|
+
if (len < 100) return 700 + Math.random() * 500; // Medium: 0.7-1.2s
|
|
14
|
+
if (len < 300) return 1000 + Math.random() * 700; // Long: 1.0-1.7s
|
|
15
|
+
return 1500 + Math.random() * 800; // Very long: 1.5-2.3s
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Delay between split messages
|
|
19
|
+
function splitDelay(text) {
|
|
20
|
+
const len = (text || '').length;
|
|
21
|
+
if (len < 50) return 300 + Math.random() * 400; // 0.3-0.7s
|
|
22
|
+
return 500 + Math.random() * 700; // 0.5-1.2s
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
export async function responseSenderMiddleware(ctx, next) {
|
|
7
26
|
if (!ctx.response) return;
|
|
8
27
|
|
|
@@ -15,9 +34,10 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
15
34
|
ctx.engine.reminders.add(agentId, contactId, response._reminder.message, response._reminder.time, ctx.platform, metadata);
|
|
16
35
|
}
|
|
17
36
|
|
|
18
|
-
// Send reaction
|
|
37
|
+
// Send reaction first (with tiny natural delay)
|
|
19
38
|
if (response.reaction && metadata.messageId) {
|
|
20
39
|
try {
|
|
40
|
+
await sleep(200 + Math.random() * 400); // 0.2-0.6s to "see" the message
|
|
21
41
|
if (ctx.platform === 'telegram' && tm) {
|
|
22
42
|
await tm.sendReaction(agentId, contactId, metadata.messageId, response.reaction, metadata);
|
|
23
43
|
}
|
|
@@ -27,9 +47,18 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
27
47
|
// No messages to send
|
|
28
48
|
if (!response.messages?.length && !response.image) return;
|
|
29
49
|
|
|
50
|
+
// Human-like delay before first reply (thinking time)
|
|
51
|
+
const firstMsg = response.messages?.[0] || '';
|
|
52
|
+
await sleep(typingDelay(firstMsg));
|
|
53
|
+
|
|
54
|
+
// Send typing indicator if available
|
|
55
|
+
if (ctx.platform === 'telegram' && tm) {
|
|
56
|
+
try { await tm.sendChatAction(agentId, contactId, 'typing', metadata); } catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
// Send via appropriate channel
|
|
31
60
|
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
-
// Send file attachment
|
|
61
|
+
// Send file attachment
|
|
33
62
|
if (response.filePath) {
|
|
34
63
|
logger.info('sender', 'SENDING FILE: ' + response.filePath);
|
|
35
64
|
try {
|
|
@@ -38,7 +67,6 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
38
67
|
return;
|
|
39
68
|
} catch (err) {
|
|
40
69
|
logger.error('sender', 'File send failed: ' + err.message);
|
|
41
|
-
// Fall through to text
|
|
42
70
|
}
|
|
43
71
|
}
|
|
44
72
|
|
|
@@ -53,14 +81,28 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
53
81
|
const audio = await vr.generate(response.messages[0], { language: lang });
|
|
54
82
|
await tm.sendVoice(agentId, contactId, audio, metadata);
|
|
55
83
|
} catch {
|
|
56
|
-
|
|
84
|
+
// Fallback: send one by one with timing
|
|
85
|
+
for (let i = 0; i < response.messages.length; i++) {
|
|
86
|
+
if (i > 0) { try { await tm.sendChatAction(agentId, contactId, "typing", metadata); } catch {} await sleep(splitDelay(response.messages[i])); }
|
|
87
|
+
await tm.sendMessage(agentId, contactId, response.messages[i], metadata);
|
|
88
|
+
}
|
|
57
89
|
}
|
|
58
90
|
} else {
|
|
59
|
-
|
|
91
|
+
// Send messages with human-like gaps between splits
|
|
92
|
+
for (let i = 0; i < response.messages.length; i++) {
|
|
93
|
+
if (i > 0) {
|
|
94
|
+
// Typing indicator + delay between messages
|
|
95
|
+
try { await tm.sendChatAction(agentId, contactId, 'typing', metadata); } catch {}
|
|
96
|
+
await sleep(splitDelay(response.messages[i]));
|
|
97
|
+
}
|
|
98
|
+
await tm.sendMessage(agentId, contactId, response.messages[i], metadata);
|
|
99
|
+
}
|
|
60
100
|
}
|
|
61
101
|
} else if (wm) {
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
// WhatsApp — same human timing
|
|
103
|
+
for (let i = 0; i < response.messages.length; i++) {
|
|
104
|
+
if (i > 0) await sleep(splitDelay(response.messages[i]));
|
|
105
|
+
await wm.sendMessage(agentId, contactId, response.messages[i]);
|
|
64
106
|
}
|
|
65
107
|
}
|
|
66
108
|
|
|
@@ -53,14 +53,14 @@ async function fetchImage(query, width = 1200, height = 800) {
|
|
|
53
53
|
// ── Slide Templates (HTML) ──
|
|
54
54
|
|
|
55
55
|
const CSS_BASE = `
|
|
56
|
-
|
|
56
|
+
/* Fonts: system stack (no external loads) */
|
|
57
57
|
|
|
58
58
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
59
59
|
|
|
60
60
|
.slide {
|
|
61
61
|
width: 1280px; height: 720px;
|
|
62
62
|
position: relative; overflow: hidden;
|
|
63
|
-
font-family:
|
|
63
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/* Dark gradient overlay for text readability on images */
|
|
@@ -88,8 +88,8 @@ const CSS_BASE = `
|
|
|
88
88
|
.content-right img { width: 100%; height: 100%; object-fit: cover; }
|
|
89
89
|
|
|
90
90
|
/* Typography */
|
|
91
|
-
.title-xl { font-family: '
|
|
92
|
-
.title-lg { font-family: '
|
|
91
|
+
.title-xl { font-family: Georgia, 'Times New Roman', serif; font-size: 56px; font-weight: 800; color: #fff; line-height: 1.1; letter-spacing: -1px; }
|
|
92
|
+
.title-lg { font-family: Georgia, 'Times New Roman', serif; font-size: 42px; font-weight: 700; color: #fff; line-height: 1.15; }
|
|
93
93
|
.title-md { font-size: 28px; font-weight: 700; color: #fff; }
|
|
94
94
|
.subtitle { font-size: 18px; color: rgba(255,255,255,0.7); margin-top: 16px; line-height: 1.6; font-weight: 300; }
|
|
95
95
|
.label { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 3px; margin-bottom: 20px; }
|
|
@@ -153,8 +153,8 @@ const CSS_BASE = `
|
|
|
153
153
|
.timeline-desc { font-size: 13px; color: rgba(255,255,255,0.6); margin-top: 4px; line-height: 1.4; }
|
|
154
154
|
|
|
155
155
|
/* Quote */
|
|
156
|
-
.quote-mark { font-family: '
|
|
157
|
-
.quote-text { font-family: '
|
|
156
|
+
.quote-mark { font-family: Georgia, 'Times New Roman', serif; font-size: 120px; color: rgba(88,166,255,0.2); line-height: 0.8; position: absolute; top: 40px; left: 50px; }
|
|
157
|
+
.quote-text { font-family: Georgia, 'Times New Roman', serif; font-size: 28px; color: #fff; line-height: 1.5; font-style: italic; max-width: 800px; }
|
|
158
158
|
.quote-author { font-size: 14px; color: rgba(255,255,255,0.5); margin-top: 24px; }
|
|
159
159
|
|
|
160
160
|
/* Progress */
|
|
@@ -612,10 +612,10 @@ export async function generateSlides(input) {
|
|
|
612
612
|
for (let i = 0; i < slideHtmls.length; i++) {
|
|
613
613
|
const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>${CSS_BASE}</style></head><body style="margin:0;padding:0;background:#000">${slideHtmls[i]}</body></html>`;
|
|
614
614
|
|
|
615
|
-
await page.setContent(html, { waitUntil: '
|
|
615
|
+
await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
616
616
|
// Wait for fonts to load
|
|
617
|
-
|
|
618
|
-
await new Promise(r => setTimeout(r,
|
|
617
|
+
// fonts are system, no wait needed
|
|
618
|
+
await new Promise(r => setTimeout(r, 100));
|
|
619
619
|
|
|
620
620
|
const screenshot = await page.screenshot({ type: 'jpeg', quality: 95 });
|
|
621
621
|
|