@jackle.dev/zalox-plugin 1.0.11 → 1.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackle.dev/zalox-plugin",
3
- "version": "1.0.11",
3
+ "version": "1.0.14",
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
@@ -373,10 +373,10 @@ export async function startInProcessListener(
373
373
  ? mentions.some((m: any) => String(m.uid || m.id || m) === ownId)
374
374
  : false;
375
375
  const name = account.name || 'Bot';
376
- const textMention = content.includes(`@${name}`);
376
+ const textMention = content.includes(`@${name}`) || content.includes('@Tiệp Lê');
377
377
 
378
378
  // DEBUG Group logic
379
- // console.log(`[ZaloX] Group msg: mentioned=${isMentioned} textMention=${textMention} (name=${name})`);
379
+ console.log(`[ZaloX] Group msg: mentioned=${isMentioned} textMention=${textMention} name=${name} content="${content}"`);
380
380
 
381
381
  if (!isMentioned && !textMention && !content.startsWith('/')) {
382
382
  return;
@@ -411,20 +411,8 @@ export async function startInProcessListener(
411
411
 
412
412
  if (!normalized.content?.trim() && !normalized.mediaUrl) return;
413
413
 
414
- // React with ❤️
415
- try {
416
- const reactThreadType = isGroup ? ThreadType.Group : ThreadType.User;
417
- api.addReaction(Reactions.HEART, {
418
- threadId: normalized.threadId,
419
- type: reactThreadType,
420
- data: {
421
- msgId: String(data.msgId || ''),
422
- cliMsgId: String(data.cliMsgId || ''),
423
- },
424
- }).catch((err: any) => {
425
- runtime.error?.(`[${account.accountId}] ZaloX reaction failed: ${String(err)}`);
426
- });
427
- } catch {}
414
+ // React logic removed.
415
+ // Agent should decide when to react.
428
416
 
429
417
  processMessage(normalized, account, config, runtime, profile, statusSink).catch((err) => {
430
418
  runtime.error?.(`[${account.accountId}] ZaloX process error: ${String(err)}`);
package/src/send.ts CHANGED
@@ -16,6 +16,26 @@ export type SendResult = {
16
16
  error?: string;
17
17
  };
18
18
 
19
+ function cleanText(input: string): string {
20
+ if (!input) return '';
21
+ // Remove <ctrl...> artifacts
22
+ return input.replace(/<ctrl\d+>/gi, '').trim();
23
+ }
24
+
25
+ async function withRetry<T>(fn: () => Promise<T>, retries = 3, delay = 1000): Promise<T> {
26
+ let lastErr;
27
+ for (let i = 0; i < retries; i++) {
28
+ try {
29
+ return await fn();
30
+ } catch (err) {
31
+ lastErr = err;
32
+ console.warn(`[ZaloX] Send failed (attempt ${i+1}/${retries}): ${err}`);
33
+ await new Promise(r => setTimeout(r, delay * (i + 1)));
34
+ }
35
+ }
36
+ throw lastErr;
37
+ }
38
+
19
39
  export async function sendText(
20
40
  threadId: string,
21
41
  text: string,
@@ -24,8 +44,10 @@ export async function sendText(
24
44
  if (!threadId?.trim()) {
25
45
  return { ok: false, error: 'No threadId provided' };
26
46
  }
27
- if (!text?.trim()) {
28
- return { ok: false, error: 'No text provided' };
47
+
48
+ const cleaned = cleanText(text);
49
+ if (!cleaned) {
50
+ return { ok: false, error: 'No text provided (or empty after cleanup)' };
29
51
  }
30
52
 
31
53
  const cached = getCachedApi(options.profile);
@@ -35,10 +57,19 @@ export async function sendText(
35
57
 
36
58
  try {
37
59
  const type = options.isGroup ? ThreadType.Group : ThreadType.User;
38
- const result = await cached.api.sendMessage(text.slice(0, 2000), threadId.trim(), type);
60
+
61
+ console.log(`[ZaloX] Sending text to ${threadId} (type=${type}): "${cleaned.slice(0, 50)}..."`);
62
+
63
+ // Add retry logic
64
+ const result = await withRetry(() =>
65
+ cached.api.sendMessage(cleaned.slice(0, 2000), threadId.trim(), type)
66
+ );
67
+
68
+ console.log(`[ZaloX] Send result:`, JSON.stringify(result));
39
69
  const msgId = result?.message?.msgId ? String(result.message.msgId) : undefined;
40
70
  return { ok: true, messageId: msgId };
41
71
  } catch (err: any) {
72
+ console.error(`[ZaloX] Final send error: ${err}`);
42
73
  return { ok: false, error: err.message || String(err) };
43
74
  }
44
75
  }
@@ -57,6 +88,8 @@ export async function sendMedia(
57
88
  if (!cached) {
58
89
  return { ok: false, error: `No active API session for profile "${options.profile}". Listener may not be running.` };
59
90
  }
91
+
92
+ const cleaned = cleanText(text);
60
93
 
61
94
  try {
62
95
  const type = options.isGroup ? ThreadType.Group : ThreadType.User;
@@ -71,11 +104,13 @@ export async function sendMedia(
71
104
  }
72
105
 
73
106
  if (filePath) {
74
- // Send with local file attachment
75
- const result = await cached.api.sendMessage(
76
- { msg: text || '', attachments: [filePath] },
77
- threadId.trim(),
78
- type,
107
+ // Send with local file attachment (with retry)
108
+ const result = await withRetry(() =>
109
+ cached.api.sendMessage(
110
+ { msg: cleaned || '', attachments: [filePath!] },
111
+ threadId.trim(),
112
+ type,
113
+ )
79
114
  );
80
115
  const msgId = result?.message?.msgId ? String(result.message.msgId) : undefined;
81
116
  return { ok: true, messageId: msgId };
@@ -96,10 +131,13 @@ export async function sendMedia(
96
131
  writeFileSync(tmpPath, buffer);
97
132
 
98
133
  try {
99
- const result = await cached.api.sendMessage(
100
- { msg: text || '', attachments: [tmpPath] },
101
- threadId.trim(),
102
- type,
134
+ // Send attachment with retry
135
+ const result = await withRetry(() =>
136
+ cached.api.sendMessage(
137
+ { msg: cleaned || '', attachments: [tmpPath!] },
138
+ threadId.trim(),
139
+ type,
140
+ )
103
141
  );
104
142
 
105
143
  const msgId = result?.message?.msgId ? String(result.message.msgId) : undefined;
@@ -107,12 +145,12 @@ export async function sendMedia(
107
145
  } catch (sendErr) {
108
146
  console.error(`[ZaloX] Send attachment failed: ${sendErr}`);
109
147
  // Fallback: send text with URL as link
110
- const fullText = text ? `${text}\n${mediaUrl}` : mediaUrl;
148
+ const fullText = cleaned ? `${cleaned}\n${mediaUrl}` : mediaUrl;
111
149
  return sendText(threadId, fullText, options);
112
150
  }
113
151
  } catch (dlErr: any) {
114
152
  // Fallback: send text with URL as link
115
- const fullText = text ? `${text}\n${mediaUrl}` : mediaUrl;
153
+ const fullText = cleaned ? `${cleaned}\n${mediaUrl}` : mediaUrl;
116
154
  return sendText(threadId, fullText, options);
117
155
  } finally {
118
156
  if (tmpPath) try { unlinkSync(tmpPath); } catch {}
@@ -120,7 +158,7 @@ export async function sendMedia(
120
158
  }
121
159
 
122
160
  // Fallback: send text only
123
- const fullText = text ? `${text}\n${mediaUrl}` : mediaUrl;
161
+ const fullText = cleaned ? `${cleaned}\n${mediaUrl}` : mediaUrl;
124
162
  return sendText(threadId, fullText, options);
125
163
  } catch (err: any) {
126
164
  return { ok: false, error: err.message || String(err) };