@jackle.dev/zalox-plugin 1.0.19 → 1.0.21

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/listener.ts +26 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackle.dev/zalox-plugin",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "OpenClaw channel plugin for Zalo via zca-js (in-process, single login)",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/listener.ts CHANGED
@@ -62,7 +62,6 @@ async function downloadZaloMedia(url: string, profile: string, filename?: string
62
62
  const credentialsPath = resolveCredentialsPath(profile);
63
63
  const creds = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
64
64
 
65
- // Zalo media URLs usually require cookies
66
65
  const headers: Record<string, string> = {
67
66
  'User-Agent': creds.userAgent || 'Mozilla/5.0',
68
67
  'Cookie': typeof creds.cookie === 'object' ? JSON.stringify(creds.cookie) : String(creds.cookie),
@@ -77,7 +76,6 @@ async function downloadZaloMedia(url: string, profile: string, filename?: string
77
76
  const contentType = res.headers.get('content-type');
78
77
  const buffer = await res.arrayBuffer();
79
78
 
80
- // Determine extension
81
79
  let ext = extname(url).split('?')[0];
82
80
  if (!ext || ext.length > 5) {
83
81
  if (contentType?.includes('jpeg')) ext = '.jpg';
@@ -119,13 +117,11 @@ async function processMessage(
119
117
  const core = getRuntime();
120
118
  const { threadId, content, msgType, timestamp, isGroup, senderId, senderName, groupName, mediaUrl } = message;
121
119
 
122
- // Allow empty content if it's an image
123
120
  if (!content?.trim() && !mediaUrl) return;
124
121
 
125
122
  const chatId = threadId;
126
123
  const rawBody = content.trim();
127
124
 
128
- // Handle media download
129
125
  let mediaPath: string | undefined;
130
126
  if (mediaUrl) {
131
127
  const downloaded = await downloadZaloMedia(mediaUrl, profile);
@@ -137,7 +133,6 @@ async function processMessage(
137
133
  }
138
134
  }
139
135
 
140
- // DM policy check
141
136
  const configAllowFrom = (account.config.allowFrom ?? []).map(String);
142
137
  const dmPolicy = account.config.dmPolicy ?? 'pairing';
143
138
 
@@ -194,7 +189,6 @@ async function processMessage(
194
189
  }
195
190
  }
196
191
 
197
- // Group command authorization
198
192
  if (
199
193
  isGroup &&
200
194
  core.channel.commands.isControlCommandMessage(rawBody, config) &&
@@ -203,7 +197,6 @@ async function processMessage(
203
197
  return;
204
198
  }
205
199
 
206
- // Route to agent
207
200
  const peer = isGroup
208
201
  ? { kind: 'group' as const, id: chatId }
209
202
  : { kind: 'dm' as const, id: senderId };
@@ -251,7 +244,7 @@ async function processMessage(
251
244
  MessageSid: message.msgId ?? `${timestamp}`,
252
245
  OriginatingChannel: 'zalox',
253
246
  OriginatingTo: `zalox:${chatId}`,
254
- MediaPath: mediaPath, // Pass media path here
247
+ MediaPath: mediaPath,
255
248
  });
256
249
 
257
250
  await core.channel.session.recordInboundSession({
@@ -304,7 +297,6 @@ export async function startInProcessListener(
304
297
 
305
298
  const listener = api.listener;
306
299
 
307
- // Handle incoming messages
308
300
  listener.on('message', (msg: any) => {
309
301
  if (stopped || abortSignal.aborted) return;
310
302
 
@@ -312,7 +304,6 @@ export async function startInProcessListener(
312
304
  const isGroup = msg.type === 1;
313
305
  const senderId = data.uidFrom ? String(data.uidFrom) : '';
314
306
 
315
- // Skip own messages
316
307
  if (senderId === ownId) return;
317
308
 
318
309
  const threadId = isGroup
@@ -321,17 +312,11 @@ export async function startInProcessListener(
321
312
 
322
313
  const msgType = data.msgType || 0;
323
314
 
324
- // DEBUG: Force reply details for every DM to user (DISABLED)
325
- // if (!isGroup) { ... }
326
-
327
315
  let mediaUrl = undefined;
328
- // Prioritize high-res URL if available
329
316
  mediaUrl = data.url || data.href;
330
317
  if (!mediaUrl && (msgType === 2 || String(msgType) === 'chat.photo')) {
331
- // Try to find URL in params or content
332
318
  if (data.params?.url) mediaUrl = data.params.url;
333
319
  else if (typeof data.content === 'object' && data.content !== null) {
334
- // Already parsed object
335
320
  mediaUrl = (data.content as any).href || (data.content as any).url || (data.content as any).thumb || (data.content as any).normalUrl;
336
321
  }
337
322
  else if (typeof data.content === 'string' && (data.content.includes('http') || data.content.startsWith('{'))) {
@@ -339,17 +324,14 @@ export async function startInProcessListener(
339
324
  const parsed = JSON.parse(data.content);
340
325
  mediaUrl = parsed.href || parsed.url || parsed.thumb || parsed.normalUrl;
341
326
  } catch {
342
- // Maybe content IS the url?
343
327
  if (data.content.startsWith('http')) mediaUrl = data.content;
344
328
  }
345
329
  }
346
330
  }
347
- // Also check for image extension if URL exists but type != 2
348
331
  if (!mediaUrl && data.url && (data.url.includes('.jpg') || data.url.includes('.png'))) {
349
332
  mediaUrl = data.url;
350
333
  }
351
334
 
352
- // Check quote/reply for media
353
335
  if (!mediaUrl && data.quote) {
354
336
  try {
355
337
  const q = data.quote;
@@ -360,11 +342,9 @@ export async function startInProcessListener(
360
342
  if (!mediaUrl && (q.href || q.url)) {
361
343
  mediaUrl = q.href || q.url;
362
344
  }
363
- } catch {}
345
+ } catch {}
364
346
  }
365
347
 
366
- // Group messages: only respond when bot is mentioned (@tagged)
367
- // UNLESS it's a media message — we might want to analyze all images?
368
348
  if (isGroup) {
369
349
  const mentions = data.mentions || data.mentionIds || [];
370
350
  const content = typeof data.content === 'string' ? data.content : (data.msg || '');
@@ -373,12 +353,17 @@ export async function startInProcessListener(
373
353
  ? mentions.some((m: any) => String(m.uid || m.id || m) === ownId)
374
354
  : false;
375
355
  const name = account.name || 'Bot';
376
- const textMention = content.includes(`@${name}`) || content.includes('@Tiệp Lê');
377
356
 
378
- // DEBUG Group logic
379
- console.log(`[ZaloX] Group Check: ownId=${ownId} mentioned=${isMentioned} textMention=${textMention} content="${content}"`);
357
+ const textMention =
358
+ content.includes(`@${name}`) ||
359
+ content.includes('@Bot') ||
360
+ content.includes('@Javis');
361
+
362
+ console.log(`[ZaloX] Group Check: ownId=${ownId} mentions=${JSON.stringify(mentions)} textMention=${textMention} content="${content.slice(0, 50)}..."`);
380
363
 
381
- if (!isMentioned && !textMention && !content.startsWith('/')) {
364
+ if (content.trim() === '/whoami') {
365
+ // Pass
366
+ } else if (!isMentioned && !textMention && !content.startsWith('/')) {
382
367
  return;
383
368
  }
384
369
  }
@@ -393,10 +378,10 @@ export async function startInProcessListener(
393
378
  if (typeof c !== 'string') return '';
394
379
  if (isGroup) {
395
380
  const name = account.name || 'Bot';
396
- // Simple cleanup without regex if possible, or use safe regex
397
- // But escaping special chars is safer
398
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
399
- c = c.replace(new RegExp(`@${escaped}\\s*`, 'gi'), '').trim();
381
+ // SAFE CLEANUP: No Regex
382
+ if (c.includes(`@${name}`)) c = c.split(`@${name}`).join('').trim();
383
+ if (c.includes('@Javis')) c = c.split('@Javis').join('').trim();
384
+ if (c.includes('@Bot')) c = c.split('@Bot').join('').trim();
400
385
  }
401
386
  return c;
402
387
  })(),
@@ -411,20 +396,27 @@ export async function startInProcessListener(
411
396
 
412
397
  if (!normalized.content?.trim() && !normalized.mediaUrl) return;
413
398
 
414
- // React logic removed.
415
- // Agent should decide when to react.
399
+ try {
400
+ const reactThreadType = isGroup ? ThreadType.Group : ThreadType.User;
401
+ api.addReaction(Reactions.HEART, {
402
+ threadId: normalized.threadId,
403
+ type: reactThreadType,
404
+ data: {
405
+ msgId: String(data.msgId || ''),
406
+ cliMsgId: String(data.cliMsgId || ''),
407
+ },
408
+ }).catch(() => {});
409
+ } catch {}
416
410
 
417
411
  processMessage(normalized, account, config, runtime, profile, statusSink).catch((err) => {
418
412
  runtime.error?.(`[${account.accountId}] ZaloX process error: ${String(err)}`);
419
413
  });
420
414
  });
421
415
 
422
- // Handle errors
423
416
  listener.on('error', (err: any) => {
424
417
  runtime.error?.(`[${account.accountId}] ZaloX listener error: ${err.message || err}`);
425
418
  });
426
419
 
427
- // Start WebSocket listener with retry
428
420
  listener.start({ retryOnClose: true });
429
421
  runtime.log?.(`[${account.accountId}] ZaloX: WebSocket listener started`);
430
422