@rune-kit/rune 2.1.1 → 2.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/README.md +40 -34
- package/compiler/__tests__/pack-split.test.js +145 -0
- package/compiler/adapters/antigravity.js +1 -1
- package/compiler/adapters/codex.js +77 -0
- package/compiler/adapters/cursor.js +1 -1
- package/compiler/adapters/generic.js +1 -1
- package/compiler/adapters/index.js +4 -0
- package/compiler/adapters/opencode.js +86 -0
- package/compiler/adapters/windsurf.js +1 -1
- package/compiler/bin/rune.js +10 -7
- package/compiler/doctor.js +42 -0
- package/compiler/emitter.js +64 -10
- package/compiler/parser.js +42 -3
- package/compiler/transformer.js +10 -6
- package/compiler/transforms/branding.js +1 -1
- package/compiler/transforms/compliance.js +40 -0
- package/extensions/ai-ml/PACK.md +38 -474
- package/extensions/ai-ml/skills/ai-agents.md +172 -0
- package/extensions/ai-ml/skills/code-sandbox.md +187 -0
- package/extensions/ai-ml/skills/deep-research.md +146 -0
- package/extensions/ai-ml/skills/embedding-search.md +66 -0
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -0
- package/extensions/ai-ml/skills/llm-architect.md +125 -0
- package/extensions/ai-ml/skills/llm-integration.md +64 -0
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -0
- package/extensions/ai-ml/skills/rag-patterns.md +66 -0
- package/extensions/ai-ml/skills/web-extraction.md +114 -0
- package/extensions/analytics/PACK.md +19 -484
- package/extensions/analytics/skills/ab-testing.md +72 -0
- package/extensions/analytics/skills/dashboard-patterns.md +83 -0
- package/extensions/analytics/skills/data-validation.md +68 -0
- package/extensions/analytics/skills/funnel-analysis.md +81 -0
- package/extensions/analytics/skills/sql-patterns.md +57 -0
- package/extensions/analytics/skills/statistical-analysis.md +79 -0
- package/extensions/analytics/skills/tracking-setup.md +71 -0
- package/extensions/backend/PACK.md +44 -618
- package/extensions/backend/skills/api-patterns.md +84 -0
- package/extensions/backend/skills/async-pipeline.md +193 -0
- package/extensions/backend/skills/auth-patterns.md +97 -0
- package/extensions/backend/skills/background-jobs.md +133 -0
- package/extensions/backend/skills/caching-patterns.md +108 -0
- package/extensions/backend/skills/cli-generation.md +133 -0
- package/extensions/backend/skills/database-patterns.md +87 -0
- package/extensions/backend/skills/middleware-patterns.md +104 -0
- package/extensions/chrome-ext/PACK.md +19 -921
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -0
- package/extensions/chrome-ext/skills/cws-publish.md +104 -0
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -0
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -0
- package/extensions/chrome-ext/skills/ext-storage.md +133 -0
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -0
- package/extensions/content/PACK.md +43 -335
- package/extensions/content/skills/blog-patterns.md +88 -0
- package/extensions/content/skills/cms-integration.md +131 -0
- package/extensions/content/skills/content-scoring.md +107 -0
- package/extensions/content/skills/i18n.md +83 -0
- package/extensions/content/skills/mdx-authoring.md +137 -0
- package/extensions/content/skills/reference.md +1014 -0
- package/extensions/content/skills/seo-patterns.md +67 -0
- package/extensions/content/skills/video-repurpose.md +153 -0
- package/extensions/devops/PACK.md +38 -457
- package/extensions/devops/skills/chaos-testing.md +67 -0
- package/extensions/devops/skills/ci-cd.md +75 -0
- package/extensions/devops/skills/docker.md +58 -0
- package/extensions/devops/skills/edge-serverless.md +163 -0
- package/extensions/devops/skills/infra-as-code.md +158 -0
- package/extensions/devops/skills/kubernetes.md +110 -0
- package/extensions/devops/skills/monitoring.md +57 -0
- package/extensions/devops/skills/server-setup.md +64 -0
- package/extensions/devops/skills/ssl-domain.md +42 -0
- package/extensions/ecommerce/PACK.md +62 -226
- package/extensions/ecommerce/skills/cart-system.md +79 -0
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -0
- package/extensions/ecommerce/skills/order-management.md +126 -0
- package/extensions/ecommerce/skills/payment-integration.md +472 -0
- package/extensions/ecommerce/skills/shopify-dev.md +69 -0
- package/extensions/ecommerce/skills/subscription-billing.md +93 -0
- package/extensions/ecommerce/skills/tax-compliance.md +117 -0
- package/extensions/gamedev/PACK.md +66 -317
- package/extensions/gamedev/skills/asset-pipeline.md +74 -0
- package/extensions/gamedev/skills/audio-system.md +129 -0
- package/extensions/gamedev/skills/camera-system.md +87 -0
- package/extensions/gamedev/skills/ecs.md +98 -0
- package/extensions/gamedev/skills/game-loops.md +72 -0
- package/extensions/gamedev/skills/input-system.md +199 -0
- package/extensions/gamedev/skills/multiplayer.md +180 -0
- package/extensions/gamedev/skills/particles.md +105 -0
- package/extensions/gamedev/skills/physics-engine.md +89 -0
- package/extensions/gamedev/skills/scene-management.md +146 -0
- package/extensions/gamedev/skills/threejs-patterns.md +90 -0
- package/extensions/gamedev/skills/webgl.md +71 -0
- package/extensions/mobile/PACK.md +56 -223
- package/extensions/mobile/skills/app-store-connect.md +152 -0
- package/extensions/mobile/skills/app-store-prep.md +66 -0
- package/extensions/mobile/skills/deep-linking.md +109 -0
- package/extensions/mobile/skills/flutter.md +60 -0
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -0
- package/extensions/mobile/skills/native-bridge.md +66 -0
- package/extensions/mobile/skills/ota-updates.md +97 -0
- package/extensions/mobile/skills/push-notifications.md +111 -0
- package/extensions/mobile/skills/react-native.md +82 -0
- package/extensions/saas/PACK.md +26 -720
- package/extensions/saas/skills/billing-integration.md +121 -0
- package/extensions/saas/skills/feature-flags.md +130 -0
- package/extensions/saas/skills/multi-tenant.md +103 -0
- package/extensions/saas/skills/onboarding-flow.md +139 -0
- package/extensions/saas/skills/subscription-flow.md +95 -0
- package/extensions/saas/skills/team-management.md +144 -0
- package/extensions/security/PACK.md +10 -448
- package/extensions/security/skills/api-security.md +140 -0
- package/extensions/security/skills/compliance.md +68 -0
- package/extensions/security/skills/owasp-audit.md +64 -0
- package/extensions/security/skills/pentest-patterns.md +77 -0
- package/extensions/security/skills/secret-mgmt.md +65 -0
- package/extensions/security/skills/supply-chain.md +65 -0
- package/extensions/trading/PACK.md +18 -535
- package/extensions/trading/skills/chart-components.md +55 -0
- package/extensions/trading/skills/experiment-loop.md +125 -0
- package/extensions/trading/skills/fintech-patterns.md +47 -0
- package/extensions/trading/skills/indicator-library.md +58 -0
- package/extensions/trading/skills/quant-analysis.md +111 -0
- package/extensions/trading/skills/realtime-data.md +58 -0
- package/extensions/trading/skills/trade-logic.md +104 -0
- package/extensions/ui/PACK.md +34 -853
- package/extensions/ui/skills/a11y-audit.md +91 -0
- package/extensions/ui/skills/animation-patterns.md +106 -0
- package/extensions/ui/skills/component-patterns.md +75 -0
- package/extensions/ui/skills/design-decision.md +98 -0
- package/extensions/ui/skills/design-system.md +68 -0
- package/extensions/ui/skills/landing-patterns.md +155 -0
- package/extensions/ui/skills/palette-picker.md +162 -0
- package/extensions/ui/skills/react-health.md +90 -0
- package/extensions/ui/skills/type-system.md +125 -0
- package/extensions/ui/skills/web-vitals.md +153 -0
- package/extensions/zalo/PACK.md +117 -0
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -0
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -0
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -0
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -0
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -0
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -0
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -0
- package/package.json +5 -2
- package/skills/brainstorm/SKILL.md +63 -1
- package/skills/cook/SKILL.md +89 -6
- package/skills/debug/SKILL.md +5 -0
- package/skills/fix/SKILL.md +5 -0
- package/skills/mcp-builder/SKILL.md +48 -1
- package/skills/neural-memory/SKILL.md +362 -0
- package/skills/plan/SKILL.md +3 -0
- package/skills/rescue/SKILL.md +5 -0
- package/skills/review/SKILL.md +44 -5
- package/skills/review-intake/SKILL.md +17 -1
- package/skills/skill-router/SKILL.md +106 -8
- package/skills/team/SKILL.md +24 -1
- package/skills/test/SKILL.md +18 -0
- package/skills/verification/SKILL.md +40 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zalo-oa-webhook
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: "Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-oa-webhook
|
|
10
|
+
|
|
11
|
+
Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Register webhook at Zalo Developer Portal**
|
|
16
|
+
Go to [developers.zalo.me](https://developers.zalo.me) → select App → **App Settings → Webhook**. Enter your HTTPS endpoint URL (e.g., `https://your-domain.com/webhook/zalo`). Zalo sends `POST` requests to this URL for every OA event. The URL must be HTTPS — no plain HTTP. For local dev, use ngrok: `ngrok http 3000` and paste the `https://` tunnel URL. Remember to update the URL when the tunnel restarts.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Verify signature on every request (CRITICAL)**
|
|
19
|
+
Every request from Zalo includes `X-ZEvent-Signature` header — HMAC-SHA256 of the raw request body, signed with your **OA Secret Key** (not the App Secret — different keys). Verify before processing. Use `crypto.timingSafeEqual` to prevent timing attacks. Reject with 403 if invalid.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import crypto from 'crypto'
|
|
23
|
+
|
|
24
|
+
function verifyWebhookSignature(
|
|
25
|
+
body: string,
|
|
26
|
+
signature: string,
|
|
27
|
+
oaSecretKey: string
|
|
28
|
+
): boolean {
|
|
29
|
+
const computed = crypto
|
|
30
|
+
.createHmac('sha256', oaSecretKey)
|
|
31
|
+
.update(body)
|
|
32
|
+
.digest('hex')
|
|
33
|
+
return crypto.timingSafeEqual(
|
|
34
|
+
Buffer.from(computed, 'hex'),
|
|
35
|
+
Buffer.from(signature, 'hex')
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
NEVER skip verification — even in development. NEVER use `===` string compare (timing leak).
|
|
41
|
+
|
|
42
|
+
**Step 3 — Respond within 5 seconds**
|
|
43
|
+
Zalo expects `200 OK` within 5 seconds or it marks the delivery failed and retries up to 3 times. Acknowledge immediately, then process asynchronously:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Return 200 first, then process
|
|
47
|
+
return c.json({ received: true }) // respond immediately
|
|
48
|
+
await queue.push(event) // async processing
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Step 4 — Implement idempotency**
|
|
52
|
+
Retries cause duplicate events. Use `msg_id` (present on message events) to deduplicate. Check before processing, mark as processed after:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const processedIds = new Set<string>() // or Redis for production
|
|
56
|
+
|
|
57
|
+
async function idempotentHandle(event: ZaloEvent): Promise<void> {
|
|
58
|
+
const id = event.message?.msg_id ?? `${event.event_name}:${event.timestamp}`
|
|
59
|
+
if (processedIds.has(id)) return
|
|
60
|
+
processedIds.add(id)
|
|
61
|
+
await routeEvent(event)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Step 5 — Route events by event_name**
|
|
66
|
+
|
|
67
|
+
| event_name | Trigger | Key payload fields |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `user_send_text` | User sends text | `sender.id`, `message.text`, `message.msg_id` |
|
|
70
|
+
| `user_send_image` | User sends image | `sender.id`, `message.attachments[].payload.url` |
|
|
71
|
+
| `user_send_file` | User sends file | `sender.id`, `message.attachments[]` |
|
|
72
|
+
| `user_send_sticker` | User sends sticker | `sender.id`, `message.attachments[]` |
|
|
73
|
+
| `user_send_location` | User sends location | `sender.id`, `message.attachments[].payload.coordinates` |
|
|
74
|
+
| `follow` | User follows OA | `follower.id` |
|
|
75
|
+
| `unfollow` | User unfollows OA | `follower.id` |
|
|
76
|
+
| `user_click_button` | User clicks button | `sender.id`, `message.text` (button payload) |
|
|
77
|
+
| `oa_send_text` | OA message delivered | — |
|
|
78
|
+
|
|
79
|
+
Note: naming is inconsistent — messages use `user_send_*` prefix, follow/unfollow do not.
|
|
80
|
+
|
|
81
|
+
#### Server Implementations
|
|
82
|
+
|
|
83
|
+
**Hono (recommended — edge-ready)**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Hono } from 'hono'
|
|
87
|
+
import { serve } from '@hono/node-server'
|
|
88
|
+
import crypto from 'crypto'
|
|
89
|
+
|
|
90
|
+
const OA_SECRET_KEY = process.env.ZALO_OA_SECRET_KEY!
|
|
91
|
+
const app = new Hono()
|
|
92
|
+
|
|
93
|
+
app.post('/webhook/zalo', async (c) => {
|
|
94
|
+
const signature = c.req.header('X-ZEvent-Signature') ?? ''
|
|
95
|
+
const body = await c.req.text() // raw body — MUST use text(), not json()
|
|
96
|
+
|
|
97
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
98
|
+
return c.json({ error: 'Invalid signature' }, 403)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
102
|
+
c.executionCtx?.waitUntil(idempotentHandle(event)) // non-blocking
|
|
103
|
+
return c.json({ received: true })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
async function routeEvent(event: ZaloEvent): Promise<void> {
|
|
107
|
+
switch (event.event_name) {
|
|
108
|
+
case 'user_send_text': return handleTextMessage(event)
|
|
109
|
+
case 'user_send_image': return handleImageMessage(event)
|
|
110
|
+
case 'user_send_file': return handleFileMessage(event)
|
|
111
|
+
case 'user_send_location': return handleLocation(event)
|
|
112
|
+
case 'follow': return handleFollow(event)
|
|
113
|
+
case 'unfollow': return handleUnfollow(event)
|
|
114
|
+
case 'user_click_button': return handleButtonClick(event)
|
|
115
|
+
default: console.warn('Unhandled Zalo event:', event.event_name)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Express**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import express from 'express'
|
|
126
|
+
|
|
127
|
+
const app = express()
|
|
128
|
+
|
|
129
|
+
// MUST use raw body parser — not express.json() — to preserve signature input
|
|
130
|
+
app.post('/webhook/zalo', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
131
|
+
const signature = req.headers['x-zevent-signature'] as string ?? ''
|
|
132
|
+
const body = req.body.toString()
|
|
133
|
+
|
|
134
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
135
|
+
return res.status(403).json({ error: 'Invalid signature' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
139
|
+
res.json({ received: true }) // respond first
|
|
140
|
+
idempotentHandle(event).catch(console.error) // then process
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Fastify**
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import Fastify from 'fastify'
|
|
148
|
+
|
|
149
|
+
const fastify = Fastify()
|
|
150
|
+
|
|
151
|
+
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
|
|
152
|
+
done(null, body) // keep raw string for signature verification
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
fastify.post('/webhook/zalo', async (request, reply) => {
|
|
156
|
+
const signature = request.headers['x-zevent-signature'] as string ?? ''
|
|
157
|
+
const body = request.body as string
|
|
158
|
+
|
|
159
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
160
|
+
return reply.status(403).send({ error: 'Invalid signature' })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
164
|
+
reply.send({ received: true })
|
|
165
|
+
idempotentHandle(event).catch(console.error)
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Local Development Tunnel
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# ngrok (most common)
|
|
173
|
+
ngrok http 3000
|
|
174
|
+
# → copy https://xxxx.ngrok.io → paste to Zalo Developer Portal
|
|
175
|
+
|
|
176
|
+
# cloudflared (free, no account needed for temp tunnels)
|
|
177
|
+
cloudflare tunnel --url http://localhost:3000
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Update webhook URL in Zalo portal every time the tunnel restarts. Use a stable subdomain (`ngrok http --subdomain=myapp 3000`) with a paid ngrok account to avoid this.
|
|
181
|
+
|
|
182
|
+
#### Sharp Edges
|
|
183
|
+
|
|
184
|
+
- **5-second timeout**: If your handler takes longer, Zalo marks it failed and retries. Always return 200 immediately, process async.
|
|
185
|
+
- **Wrong secret key**: Signature uses **OA Secret Key** from OA Management → Settings, NOT the App Secret Key from Developer Portal. Different keys, same name confusion.
|
|
186
|
+
- **Raw body required**: Parse body as raw string before verification. Using `express.json()` or Hono's `.json()` before verification will break the HMAC because the body gets re-serialized.
|
|
187
|
+
- **Inconsistent event naming**: `user_send_text` but just `follow` — not `user_follow`. Handle both patterns in your router.
|
|
188
|
+
- **HTTPS required**: Zalo rejects plain HTTP webhook URLs. ngrok/cloudflared tunnels provide HTTPS automatically.
|
|
189
|
+
- **msg_id deduplication is mandatory in production**: Zalo retries on non-200 (up to 3x), and network issues can cause duplicate deliveries. A Redis-backed `SETNX msg_id EX 86400` is the production-safe pattern.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zalo-personal-messaging
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: Personal and group messaging via zca-js — text, media, reactions, group management, mention gating, message buffer for context. UNOFFICIAL — risk-gated.
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-personal-messaging
|
|
10
|
+
|
|
11
|
+
> ⚠️ Track B (unofficial). See zalo-personal-setup for full risk disclaimer.
|
|
12
|
+
> This skill assumes you have completed zalo-personal-setup and have an active API instance.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Send messages, media, and reactions to Zalo personal accounts and groups via zca-js. Covers 1:1 DMs, group messaging, mention-gated bot patterns, and context buffering.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Direct Messages (1:1)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Send text
|
|
24
|
+
await api.sendMessage('Hello!', threadId, 'User')
|
|
25
|
+
|
|
26
|
+
// Send image (local file path — download first if URL)
|
|
27
|
+
await api.sendMessage({
|
|
28
|
+
body: 'Check this image',
|
|
29
|
+
attachments: [imagePath]
|
|
30
|
+
}, threadId, 'User')
|
|
31
|
+
|
|
32
|
+
// Chunk long messages (2000-char limit applies to DMs too)
|
|
33
|
+
async function sendLong(text: string, threadId: string, type: 'User' | 'Group') {
|
|
34
|
+
const chunks = text.match(/.{1,1900}/gs) ?? [text]
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
await api.sendMessage(chunk, threadId, type)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Group Messaging
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Send to group
|
|
47
|
+
await api.sendMessage('Hello group!', groupId, 'Group')
|
|
48
|
+
|
|
49
|
+
// Send with mention
|
|
50
|
+
await api.sendMessage({
|
|
51
|
+
body: '@John check this',
|
|
52
|
+
mentions: [{ pos: 0, len: 5, uid: johnUserId }]
|
|
53
|
+
}, groupId, 'Group')
|
|
54
|
+
|
|
55
|
+
// Group management
|
|
56
|
+
await api.createGroup('Bot Test Group', [userId1, userId2]) // min 3 members incl. self
|
|
57
|
+
await api.addGroupMembers(groupId, [newMemberId])
|
|
58
|
+
await api.removeGroupMembers(groupId, [memberId])
|
|
59
|
+
await api.changeGroupName(groupId, 'New Name')
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Media Types
|
|
65
|
+
|
|
66
|
+
| Type | Notes |
|
|
67
|
+
|------|-------|
|
|
68
|
+
| Text | 2000-char limit — chunk if needed |
|
|
69
|
+
| Image | Local file path only — download URL first |
|
|
70
|
+
| Video | Local file path |
|
|
71
|
+
| Voice | Local file path |
|
|
72
|
+
| Sticker | By sticker ID — IDs undocumented, capture from received msgs |
|
|
73
|
+
| File | Local file path |
|
|
74
|
+
| Contact card | User ID reference |
|
|
75
|
+
| Link | Auto-generates preview |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Reactions
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// React to a message (11 types)
|
|
83
|
+
await api.sendReaction(messageId, threadId, '❤️', 'User')
|
|
84
|
+
|
|
85
|
+
// Available: ❤️ 😆 😮 😢 😠 👍 👎 ✊ 🎉 😏 🥰
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Mention Gating Pattern
|
|
91
|
+
|
|
92
|
+
For group bots — only process when @mentioned, buffer other messages for context:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
function isMentioned(msg: GroupMessage, botId: string): boolean {
|
|
96
|
+
return msg.data.mentions?.some(m => m.uid === botId) ?? false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
listener.on('group_message', async (msg) => {
|
|
100
|
+
if (!isMentioned(msg, BOT_USER_ID)) {
|
|
101
|
+
messageBuffer.push(msg) // buffer for context
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
// Bot was mentioned — process with buffered context
|
|
105
|
+
const context = messageBuffer.getRecent(msg.threadId, 20)
|
|
106
|
+
const response = await processWithContext(msg, context)
|
|
107
|
+
await api.sendMessage(response, msg.threadId, 'Group')
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> Use `msg.data.mentions` array — never parse `@` from message text (unreliable).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Message Buffer
|
|
116
|
+
|
|
117
|
+
Buffer recent group messages per thread for context injection:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
class MessageBuffer {
|
|
121
|
+
private buffer: Map<string, Message[]> = new Map()
|
|
122
|
+
private maxPerThread = 50
|
|
123
|
+
|
|
124
|
+
push(msg: Message) {
|
|
125
|
+
const threadId = msg.threadId
|
|
126
|
+
const msgs = this.buffer.get(threadId) ?? []
|
|
127
|
+
msgs.push(msg)
|
|
128
|
+
if (msgs.length > this.maxPerThread) msgs.shift() // cap to avoid unbounded growth
|
|
129
|
+
this.buffer.set(threadId, msgs)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getRecent(threadId: string, count: number): Message[] {
|
|
133
|
+
return (this.buffer.get(threadId) ?? []).slice(-count)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Name Cache
|
|
141
|
+
|
|
142
|
+
Resolve user IDs to display names with TTL to avoid API hammering:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
class NameCache {
|
|
146
|
+
private cache = new Map<string, { name: string; expiresAt: number }>()
|
|
147
|
+
private ttl = 60 * 60 * 1000 // 1 hour
|
|
148
|
+
|
|
149
|
+
async resolve(userId: string, api: ZaloApi): Promise<string> {
|
|
150
|
+
const cached = this.cache.get(userId)
|
|
151
|
+
if (cached && cached.expiresAt > Date.now()) return cached.name
|
|
152
|
+
try {
|
|
153
|
+
const profile = await api.getUserInfo(userId)
|
|
154
|
+
const name = profile.displayName || 'Unknown'
|
|
155
|
+
this.cache.set(userId, { name, expiresAt: Date.now() + this.ttl })
|
|
156
|
+
return name
|
|
157
|
+
} catch {
|
|
158
|
+
return 'Unknown'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Event Listeners
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// DM and group events are SEPARATE — wire both
|
|
170
|
+
listener.on('message', async (msg) => {
|
|
171
|
+
// 1:1 personal messages
|
|
172
|
+
const senderId = msg.data.uidFrom
|
|
173
|
+
await handleDM(msg, senderId)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
listener.on('group_message', async (msg) => {
|
|
177
|
+
// Group messages — includes mention data
|
|
178
|
+
const senderId = msg.data.uidFrom
|
|
179
|
+
await handleGroup(msg, senderId)
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Sharp Edges
|
|
186
|
+
|
|
187
|
+
- **Separate events**: `message` (DM) vs `group_message` (group) — missing one = silent drop
|
|
188
|
+
- **Mention detection**: check `msg.data.mentions` array, not text `@` parsing
|
|
189
|
+
- **2000-char limit**: applies to both DM and group — always chunk
|
|
190
|
+
- **Image upload**: local file path only — download remote URLs before sending
|
|
191
|
+
- **Sticker IDs**: undocumented — sniff from received sticker messages to build your own map
|
|
192
|
+
- **Group create**: minimum 3 members including self — 2-member call throws
|
|
193
|
+
- **Buffer cap**: always set `maxPerThread` — unbounded growth crashes long-running bots
|
|
194
|
+
- **Name cache TTL**: don't skip — `getUserInfo` rate-limited aggressively on personal accounts
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zalo-personal-setup
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: Personal Zalo account automation setup via zca-js — QR login, credential persistence, WebSocket listener, session management. UNOFFICIAL — risk-gated.
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-personal-setup
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
Bootstrap a personal Zalo account automation using zca-js — the community-maintained reverse-engineered client. Handles first-time QR login, credential persistence, WebSocket listener setup, and session restore on subsequent runs.
|
|
14
|
+
|
|
15
|
+
<HARD-GATE>
|
|
16
|
+
This skill uses UNOFFICIAL reverse-engineered APIs via zca-js.
|
|
17
|
+
BEFORE proceeding, acknowledge ALL risks:
|
|
18
|
+
1. ToS VIOLATION — Zalo can ban your account without warning
|
|
19
|
+
2. SINGLE SESSION — cannot use Zalo mobile/web simultaneously
|
|
20
|
+
3. API INSTABILITY — Zalo can break internal APIs anytime
|
|
21
|
+
4. NO SUPPORT — Zalo will not help with issues from unofficial usage
|
|
22
|
+
5. NOT FOR PRODUCTION — personal projects and prototypes ONLY
|
|
23
|
+
|
|
24
|
+
If building for business/production → use Track A (zalo-oa-setup) instead.
|
|
25
|
+
</HARD-GATE>
|
|
26
|
+
|
|
27
|
+
## Step 1 — Install Dependency
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install zca-js
|
|
31
|
+
# zca-js: https://github.com/RFS-ADRENO/zca-js (359★, 202 forks)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Minimum Node.js: 18+. TypeScript users add `@types/node` if not already present.
|
|
35
|
+
|
|
36
|
+
## Step 2 — QR Login (First Run)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Zalo } from 'zca-js'
|
|
40
|
+
|
|
41
|
+
const zalo = new Zalo()
|
|
42
|
+
|
|
43
|
+
// First-time login: QR code
|
|
44
|
+
const api = await zalo.loginQR()
|
|
45
|
+
// Terminal displays QR → scan with Zalo mobile app
|
|
46
|
+
// Returns API instance with full access
|
|
47
|
+
|
|
48
|
+
// Save credentials for next time
|
|
49
|
+
const credentials = {
|
|
50
|
+
imei: api.getImei(), // generated device ID
|
|
51
|
+
cookie: api.getCookie(), // session cookies
|
|
52
|
+
userAgent: api.getUserAgent() // browser fingerprint
|
|
53
|
+
}
|
|
54
|
+
await saveCredentials(credentials)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
QR code expires in ~60 seconds — scan quickly. After scan, zca-js completes handshake and returns a live API instance.
|
|
58
|
+
|
|
59
|
+
## Step 3 — Credential Persistence
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { readFile, writeFile, chmod } from 'fs/promises'
|
|
63
|
+
import { join } from 'path'
|
|
64
|
+
import { homedir } from 'os'
|
|
65
|
+
|
|
66
|
+
const CRED_PATH = join(homedir(), '.zalo-personal', 'credentials.json')
|
|
67
|
+
|
|
68
|
+
async function saveCredentials(creds: ZaloCredentials): Promise<void> {
|
|
69
|
+
await writeFile(CRED_PATH, JSON.stringify(creds, null, 2))
|
|
70
|
+
await chmod(CRED_PATH, 0o600) // owner-only read/write
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadCredentials(): Promise<ZaloCredentials | null> {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(await readFile(CRED_PATH, 'utf-8'))
|
|
76
|
+
} catch { return null }
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Store at `~/.zalo-personal/credentials.json` — outside the project repo. Never commit credentials to git. Add `.zalo-personal/` to `.gitignore`.
|
|
81
|
+
|
|
82
|
+
## Step 4 — Session Restore (Subsequent Runs)
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const creds = await loadCredentials()
|
|
86
|
+
|
|
87
|
+
const api = creds
|
|
88
|
+
? await zalo.login({
|
|
89
|
+
imei: creds.imei,
|
|
90
|
+
cookie: creds.cookie,
|
|
91
|
+
userAgent: creds.userAgent
|
|
92
|
+
})
|
|
93
|
+
: await zalo.loginQR() // fall back to QR if no saved creds
|
|
94
|
+
|
|
95
|
+
// Always re-persist after login — cookies may have refreshed
|
|
96
|
+
await saveCredentials({
|
|
97
|
+
imei: api.getImei(),
|
|
98
|
+
cookie: api.getCookie(),
|
|
99
|
+
userAgent: api.getUserAgent()
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Step 5 — WebSocket Listener
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const listener = api.listener
|
|
107
|
+
await listener.start({ retryOnClose: true })
|
|
108
|
+
|
|
109
|
+
listener.on('message', (msg) => {
|
|
110
|
+
// Handle incoming DMs
|
|
111
|
+
console.log(`[DM] ${msg.data.content}`)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
listener.on('group_message', (msg) => {
|
|
115
|
+
// Group messages arrive on separate event
|
|
116
|
+
console.log(`[Group] ${msg.data.content}`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// keepAlive is automatic via zca-js — no manual ping needed
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`retryOnClose: true` enables automatic reconnect using the retry schedule provided by Zalo's server.
|
|
123
|
+
|
|
124
|
+
## Session Management Notes
|
|
125
|
+
|
|
126
|
+
| Concept | Detail |
|
|
127
|
+
|---------|--------|
|
|
128
|
+
| IMEI | Deterministic UUID from userAgent — acts as device fingerprint. Must stay consistent across restarts. |
|
|
129
|
+
| Cookies | Auto-refreshed on keepAlive. Always re-persist after each session start. |
|
|
130
|
+
| DuplicateConnection (3000) | Another session opened — this one closes. Cannot run bot + Zalo mobile simultaneously. |
|
|
131
|
+
| Reconnect | Handled by zca-js via server retry schedule. No manual logic needed. |
|
|
132
|
+
|
|
133
|
+
## Anti-Detection Baseline
|
|
134
|
+
|
|
135
|
+
- Use consistent `userAgent` across sessions — don't randomize on each run
|
|
136
|
+
- Don't send messages too fast (see `zalo-rate-guard` for throttle patterns)
|
|
137
|
+
- Avoid running during unusual hours (3–6 AM local time)
|
|
138
|
+
- Keep sessions long-lived — frequent login/logout is suspicious
|
|
139
|
+
- Never change profile info programmatically
|
|
140
|
+
|
|
141
|
+
## Sharp Edges
|
|
142
|
+
|
|
143
|
+
- Cookie refresh happens on keepAlive — **MUST** persist updated cookies after every session start, not just first login
|
|
144
|
+
- IMEI must stay consistent — changing it looks like a new device to Zalo's backend
|
|
145
|
+
- If Zalo mobile is active on same account, bot receives `DuplicateConnection` kick immediately
|
|
146
|
+
- zca-js depends on Zalo's internal undocumented API — breaks without warning on Zalo updates
|
|
147
|
+
- No official rate limits documented — err heavily on the side of caution
|
|
148
|
+
|
|
149
|
+
## Mesh Links
|
|
150
|
+
|
|
151
|
+
- `zalo-oa-setup` — Track A (official OA API) if this use case grows to production
|
|
152
|
+
- `zalo-rate-guard` — rate limiting and message throttle for personal bots
|
|
153
|
+
- `zalo-personal-messaging` — send/reply DMs and group messages once session is live
|