@rune-kit/rune 2.3.3 → 2.4.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/README.md +51 -1
- package/compiler/__tests__/scripts-bundling.test.js +284 -0
- package/compiler/__tests__/tier-override.test.js +41 -0
- package/compiler/adapters/antigravity.js +4 -0
- package/compiler/adapters/codex.js +4 -0
- package/compiler/adapters/cursor.js +4 -0
- package/compiler/adapters/generic.js +4 -0
- package/compiler/adapters/openclaw.js +4 -0
- package/compiler/adapters/opencode.js +4 -0
- package/compiler/adapters/windsurf.js +4 -0
- package/compiler/emitter.js +85 -5
- package/compiler/transforms/scripts-path.js +18 -0
- package/extensions/zalo/PACK.md +20 -1
- package/extensions/zalo/references/conversation-management.md +214 -0
- package/extensions/zalo/references/eval-scenarios.md +157 -0
- package/extensions/zalo/references/listen-mode.md +237 -0
- package/extensions/zalo/references/mcp-production.md +274 -0
- package/extensions/zalo/references/multi-account-proxy.md +224 -0
- package/extensions/zalo/references/vietqr-banking.md +160 -0
- package/package.json +2 -3
- package/skills/marketing/SKILL.md +3 -0
- package/skills/sentinel/SKILL.md +4 -1
- package/skills/sentinel/references/auth-crypto-reference.md +192 -0
- package/skills/sentinel/references/desktop-security.md +201 -0
- package/skills/sentinel/references/supply-chain.md +160 -0
- package/skills/slides/SKILL.md +142 -0
- package/skills/slides/scripts/build-deck.js +158 -0
- package/docs/ANTIGRAVITY-GAP-ANALYSIS.md +0 -369
- package/docs/ARCHITECTURE.md +0 -332
- package/docs/COMMUNITY-PACKS.md +0 -109
- package/docs/CONTRIBUTING-L4.md +0 -215
- package/docs/CROSS-IDE-ANALYSIS.md +0 -164
- package/docs/EXTENSION-TEMPLATE.md +0 -126
- package/docs/MESH-RULES.md +0 -34
- package/docs/MULTI-PLATFORM.md +0 -804
- package/docs/SKILL-DEPTH-AUDIT.md +0 -191
- package/docs/SKILL-TEMPLATE.md +0 -118
- package/docs/TRADE-MATRIX.md +0 -327
- package/docs/VERSIONING.md +0 -91
- package/docs/VISION.md +0 -263
- package/docs/assets/demo-subtitles.srt +0 -215
- package/docs/assets/end-card.html +0 -276
- package/docs/assets/mesh-diagram.html +0 -654
- package/docs/assets/thumbnail.html +0 -295
- package/docs/guides/cli.md +0 -403
- package/docs/guides/index.html +0 -1450
- package/docs/index.html +0 -1005
- package/docs/references/claudekit-analysis.md +0 -414
- package/docs/references/voltagent-analysis.md +0 -189
- package/docs/script.js +0 -495
- package/docs/skills/index.html +0 -832
- package/docs/style.css +0 -958
- package/docs/video-demo-plan.md +0 -172
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# MCP Server Production Reference
|
|
2
|
+
|
|
3
|
+
> Loaded by `@rune/zalo` when MCP server setup, production deployment, or cursor-based pagination patterns detected.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## MCP Server Modes
|
|
8
|
+
|
|
9
|
+
### Local (stdio)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Start MCP server in stdio mode (for local Claude Code / AI IDE)
|
|
13
|
+
zalo-agent mcp start
|
|
14
|
+
|
|
15
|
+
# Connects via stdin/stdout — AI agent calls tools directly
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Remote (HTTP)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Start MCP server in HTTP mode (for remote agents, multi-client)
|
|
22
|
+
zalo-agent mcp start --http 3847 --auth your-secret-token
|
|
23
|
+
|
|
24
|
+
# Clients connect via HTTP transport with Bearer auth
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Enhanced Tool Definitions
|
|
30
|
+
|
|
31
|
+
### zalo_get_messages — Cursor-Based Pagination
|
|
32
|
+
|
|
33
|
+
Replace simple queue polling with cursor-based pagination from a ring buffer:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
{
|
|
37
|
+
name: 'zalo_get_messages',
|
|
38
|
+
description: 'Get messages from ring buffer with cursor-based pagination.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
cursor: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Opaque cursor from previous response. Omit for latest messages.'
|
|
45
|
+
},
|
|
46
|
+
limit: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
default: 20,
|
|
49
|
+
maximum: 100,
|
|
50
|
+
description: 'Max messages to return'
|
|
51
|
+
},
|
|
52
|
+
thread_id: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Filter by specific thread. Omit for all threads.'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"messages": [
|
|
66
|
+
{
|
|
67
|
+
"id": "msg_abc123",
|
|
68
|
+
"thread_id": "user_456",
|
|
69
|
+
"sender_name": "Nguyen Van A",
|
|
70
|
+
"text": "Hello",
|
|
71
|
+
"timestamp": 1711234567890,
|
|
72
|
+
"type": "text",
|
|
73
|
+
"attachments": []
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"next_cursor": "eyJ0IjoxNzExMjM0NTY3ODkwfQ==",
|
|
77
|
+
"has_more": true
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Implementation
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
class RingBuffer {
|
|
85
|
+
private buffer: Message[] = []
|
|
86
|
+
private maxSize: number
|
|
87
|
+
|
|
88
|
+
constructor(maxSize = 5000) {
|
|
89
|
+
this.maxSize = maxSize
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
push(msg: Message) {
|
|
93
|
+
this.buffer.push(msg)
|
|
94
|
+
if (this.buffer.length > this.maxSize) {
|
|
95
|
+
this.buffer = this.buffer.slice(-this.maxSize) // keep newest
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
query(cursor?: string, limit = 20, threadId?: string): QueryResult {
|
|
100
|
+
let startIdx = 0
|
|
101
|
+
|
|
102
|
+
if (cursor) {
|
|
103
|
+
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString())
|
|
104
|
+
startIdx = this.buffer.findIndex(m => m.timestamp > decoded.t)
|
|
105
|
+
if (startIdx === -1) startIdx = this.buffer.length
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let filtered = this.buffer.slice(startIdx)
|
|
109
|
+
if (threadId) {
|
|
110
|
+
filtered = filtered.filter(m => m.thread_id === threadId)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const page = filtered.slice(0, limit)
|
|
114
|
+
const hasMore = filtered.length > limit
|
|
115
|
+
|
|
116
|
+
const nextCursor = page.length > 0
|
|
117
|
+
? Buffer.from(JSON.stringify({ t: page[page.length - 1].timestamp })).toString('base64url')
|
|
118
|
+
: cursor
|
|
119
|
+
|
|
120
|
+
return { messages: page, next_cursor: nextCursor, has_more: hasMore }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### zalo_list_threads — Active Conversations
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
{
|
|
129
|
+
name: 'zalo_list_threads',
|
|
130
|
+
description: 'List active conversations with unread counts.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
limit: { type: 'number', default: 20 },
|
|
135
|
+
unread_only: { type: 'boolean', default: false }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Returns: `{ threads: [{ id, name, type, unread_count, last_message_at }] }`
|
|
142
|
+
|
|
143
|
+
### zalo_mark_read — Clear Buffer
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
{
|
|
147
|
+
name: 'zalo_mark_read',
|
|
148
|
+
description: 'Mark messages as processed. Clears from unread count.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
required: ['thread_id'],
|
|
152
|
+
properties: {
|
|
153
|
+
thread_id: { type: 'string' },
|
|
154
|
+
up_to_cursor: { type: 'string', description: 'Mark all messages up to this cursor as read' }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### zalo_view_image — Media Download
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
{
|
|
164
|
+
name: 'zalo_view_image',
|
|
165
|
+
description: 'Download and view an image from a Zalo message locally.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
required: ['message_id'],
|
|
169
|
+
properties: {
|
|
170
|
+
message_id: { type: 'string' },
|
|
171
|
+
save_to: { type: 'string', description: 'Local path to save. Default: temp directory.' }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## MCP Configuration
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
// mcp-config.json
|
|
183
|
+
{
|
|
184
|
+
"watchThreads": ["group_abc", "user_xyz"],
|
|
185
|
+
"keywords": ["help", "support", "bug"],
|
|
186
|
+
"buffer": {
|
|
187
|
+
"maxSize": 5000,
|
|
188
|
+
"persistPath": "./mcp-buffer.json"
|
|
189
|
+
},
|
|
190
|
+
"autoReconnect": true,
|
|
191
|
+
"reconnectDelayMs": 5000
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| Field | Description | Default |
|
|
196
|
+
|-------|-------------|---------|
|
|
197
|
+
| `watchThreads` | Only capture messages from these threads | All threads |
|
|
198
|
+
| `keywords` | Keyword triggers — only buffer matching messages | All messages |
|
|
199
|
+
| `buffer.maxSize` | Ring buffer capacity | 5000 |
|
|
200
|
+
| `buffer.persistPath` | Persist buffer to disk on shutdown | In-memory only |
|
|
201
|
+
| `autoReconnect` | Auto-reconnect WebSocket on disconnect | true |
|
|
202
|
+
| `reconnectDelayMs` | Delay before reconnect attempt | 5000 |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Production Deployment
|
|
207
|
+
|
|
208
|
+
### pm2 (Recommended)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# ecosystem.config.js
|
|
212
|
+
module.exports = {
|
|
213
|
+
apps: [{
|
|
214
|
+
name: 'zalo-mcp',
|
|
215
|
+
script: 'zalo-agent',
|
|
216
|
+
args: 'mcp start --http 3847 --auth $MCP_AUTH_SECRET',
|
|
217
|
+
autorestart: true,
|
|
218
|
+
max_restarts: 10,
|
|
219
|
+
restart_delay: 5000,
|
|
220
|
+
env: {
|
|
221
|
+
NODE_ENV: 'production',
|
|
222
|
+
MCP_AUTH_SECRET: 'your-secret'
|
|
223
|
+
}
|
|
224
|
+
}]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Start
|
|
228
|
+
pm2 start ecosystem.config.js
|
|
229
|
+
pm2 save
|
|
230
|
+
pm2 startup
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### WebSocket Session Management
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
Key constraints:
|
|
237
|
+
1. ONE WebSocket connection per Zalo account — no parallelism
|
|
238
|
+
2. Browser Zalo and bot CANNOT coexist on same account
|
|
239
|
+
3. WebSocket drops on network change → auto-reconnect required
|
|
240
|
+
4. Session cookie expires → re-login with stored credentials
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Health Check Pattern
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Periodic health check for MCP + WebSocket
|
|
247
|
+
setInterval(async () => {
|
|
248
|
+
const wsAlive = api.isConnected()
|
|
249
|
+
const lastMsg = ringBuffer.getLatest()?.timestamp ?? 0
|
|
250
|
+
const silentMinutes = (Date.now() - lastMsg) / 60_000
|
|
251
|
+
|
|
252
|
+
if (!wsAlive) {
|
|
253
|
+
logger.error('WebSocket disconnected — attempting reconnect')
|
|
254
|
+
await api.reconnect()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (silentMinutes > 30 && wsAlive) {
|
|
258
|
+
logger.warn(`No messages for ${silentMinutes.toFixed(0)}m — possible silent disconnect`)
|
|
259
|
+
// Force reconnect if no messages for 60+ minutes
|
|
260
|
+
if (silentMinutes > 60) await api.reconnect()
|
|
261
|
+
}
|
|
262
|
+
}, 60_000) // check every minute
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Sharp Edges
|
|
268
|
+
|
|
269
|
+
- **Ring buffer vs queue**: Ring buffer overwrites oldest when full (no data loss notification). Queue drops with warning. Choose based on whether losing old messages is acceptable.
|
|
270
|
+
- **Cursor encoding**: Use `base64url` not `base64` — cursors appear in URLs and JSON, `+` and `/` cause issues
|
|
271
|
+
- **HTTP MCP auth**: `--auth` token is sent as `Bearer` header — use HTTPS in production or attacker can sniff the token
|
|
272
|
+
- **pm2 + credentials**: Never pass credentials as CLI args (visible in `ps aux`) — use env vars or credential file
|
|
273
|
+
- **Buffer persistence**: If `persistPath` is set, buffer writes to disk on SIGTERM — unclean kill loses buffered messages
|
|
274
|
+
- **WebSocket single-session**: Starting MCP server kicks out browser Zalo. Warn user before starting.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Multi-Account & Proxy Management Reference
|
|
2
|
+
|
|
3
|
+
> Loaded by `@rune/zalo` when multi-account setup, proxy configuration, or production account management patterns detected.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Multi-Account Architecture
|
|
8
|
+
|
|
9
|
+
Run multiple Zalo accounts on a single server — each with its own session, proxy, and WebSocket connection.
|
|
10
|
+
|
|
11
|
+
### Account Operations
|
|
12
|
+
|
|
13
|
+
| Command | Description |
|
|
14
|
+
|---------|-------------|
|
|
15
|
+
| `account list` | Show all registered accounts (ID, name, status, proxy) |
|
|
16
|
+
| `account login -n <name> -p <proxy>` | Login new account with proxy |
|
|
17
|
+
| `account switch <ownerId>` | Switch active account |
|
|
18
|
+
| `account remove <ownerId>` | Remove account and credentials |
|
|
19
|
+
| `account info <ownerId>` | Show account details |
|
|
20
|
+
| `account export -o <path>` | Export credentials for headless re-login |
|
|
21
|
+
|
|
22
|
+
### Login Flow
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# First account — interactive QR login
|
|
26
|
+
zalo-agent login --qr-url
|
|
27
|
+
# Opens http://localhost:18927/qr — scan with Zalo app
|
|
28
|
+
|
|
29
|
+
# Additional accounts — with proxy
|
|
30
|
+
zalo-agent account login \
|
|
31
|
+
-n "support-bot" \
|
|
32
|
+
-p "socks5://user:pass@proxy1.example.com:1080"
|
|
33
|
+
|
|
34
|
+
# Headless re-login (VPS, CI/CD)
|
|
35
|
+
zalo-agent account export -o ./creds-support.json
|
|
36
|
+
zalo-agent login --credentials ./creds-support.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### QR Server Ports
|
|
40
|
+
|
|
41
|
+
Port auto-tries in order: `18927 → 8080 → 3000 → 9000`. QR code expires after **60 seconds**.
|
|
42
|
+
|
|
43
|
+
For VPS without browser access:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Start QR server accessible from outside
|
|
47
|
+
zalo-agent login --qr-url
|
|
48
|
+
# Access http://<VPS_IP>:18927/qr from your phone/laptop browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Proxy Configuration
|
|
54
|
+
|
|
55
|
+
### Why Proxies Are Required
|
|
56
|
+
|
|
57
|
+
Zalo detects multiple accounts from the same IP address. Without proxies:
|
|
58
|
+
- Accounts may be flagged for suspicious activity
|
|
59
|
+
- Rate limits are shared across all accounts on the same IP
|
|
60
|
+
- Ban on one account can cascade to others
|
|
61
|
+
|
|
62
|
+
### Rules
|
|
63
|
+
|
|
64
|
+
1. **1 unique proxy per account** — never share proxies between accounts
|
|
65
|
+
2. **Residential proxies preferred** — datacenter IPs are flagged more aggressively
|
|
66
|
+
3. **Vietnamese proxies recommended** — foreign IPs trigger additional verification
|
|
67
|
+
4. **Consistent proxy** — don't rotate proxies for the same account (triggers re-verification)
|
|
68
|
+
|
|
69
|
+
### Supported Formats
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
http://host:port
|
|
73
|
+
http://user:pass@host:port
|
|
74
|
+
https://user:pass@host:port
|
|
75
|
+
socks5://host:port
|
|
76
|
+
socks5://user:pass@host:port
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Implementation Pattern
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { HttpsProxyAgent } from 'https-proxy-agent'
|
|
83
|
+
import { SocksProxyAgent } from 'socks-proxy-agent'
|
|
84
|
+
|
|
85
|
+
function createProxyAgent(proxyUrl: string) {
|
|
86
|
+
const url = new URL(proxyUrl)
|
|
87
|
+
if (url.protocol.startsWith('socks')) {
|
|
88
|
+
return new SocksProxyAgent(proxyUrl)
|
|
89
|
+
}
|
|
90
|
+
return new HttpsProxyAgent(proxyUrl)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Per-account proxy assignment
|
|
94
|
+
const accounts: Map<string, { api: ZaloApi; proxy: string }> = new Map()
|
|
95
|
+
|
|
96
|
+
async function loginWithProxy(name: string, proxyUrl: string) {
|
|
97
|
+
const agent = createProxyAgent(proxyUrl)
|
|
98
|
+
const api = new ZaloApi({ proxy: agent, name })
|
|
99
|
+
await api.login()
|
|
100
|
+
accounts.set(api.ownerId, { api, proxy: proxyUrl })
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Credential Storage
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
~/.zalo-agent-cli/
|
|
110
|
+
├── credentials.json # Default account
|
|
111
|
+
├── accounts/
|
|
112
|
+
│ ├── <ownerId1>.json # Account 1 credentials
|
|
113
|
+
│ └── <ownerId2>.json # Account 2 credentials
|
|
114
|
+
└── config.json # Global config (proxies, defaults)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Security Rules
|
|
118
|
+
|
|
119
|
+
- File permissions: `0600` (owner read/write only)
|
|
120
|
+
- **Never** log credential file contents
|
|
121
|
+
- **Never** commit to git (add to `.gitignore`)
|
|
122
|
+
- **Never** pass credentials as CLI arguments (visible in `ps aux`)
|
|
123
|
+
- Use environment variables or credential files for headless login
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# .gitignore
|
|
127
|
+
.zalo-agent-cli/
|
|
128
|
+
*.zalo-creds.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Account Switching in Code
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
class AccountManager {
|
|
137
|
+
private accounts = new Map<string, ZaloApi>()
|
|
138
|
+
private active: string | null = null
|
|
139
|
+
|
|
140
|
+
async switch(ownerId: string): Promise<ZaloApi> {
|
|
141
|
+
const api = this.accounts.get(ownerId)
|
|
142
|
+
if (!api) throw new Error(`Account ${ownerId} not found`)
|
|
143
|
+
this.active = ownerId
|
|
144
|
+
return api
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getActive(): ZaloApi {
|
|
148
|
+
if (!this.active) throw new Error('No active account')
|
|
149
|
+
return this.accounts.get(this.active)!
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Route messages to correct account
|
|
153
|
+
async routeMessage(msg: InboundMessage) {
|
|
154
|
+
const targetAccount = this.resolveAccount(msg.recipientId)
|
|
155
|
+
const api = this.accounts.get(targetAccount)
|
|
156
|
+
if (!api) return // account not managed
|
|
157
|
+
|
|
158
|
+
await api.sendMessage(msg.response, msg.threadId, msg.type)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Production Multi-Account Setup
|
|
166
|
+
|
|
167
|
+
### pm2 Ecosystem
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
// ecosystem.config.js — one process per account
|
|
171
|
+
module.exports = {
|
|
172
|
+
apps: [
|
|
173
|
+
{
|
|
174
|
+
name: 'zalo-bot-main',
|
|
175
|
+
script: 'zalo-agent',
|
|
176
|
+
args: 'listen -w http://localhost:3001/webhook',
|
|
177
|
+
env: { ZALO_ACCOUNT: 'main', ZALO_PROXY: 'socks5://user:pass@proxy1:1080' }
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'zalo-bot-support',
|
|
181
|
+
script: 'zalo-agent',
|
|
182
|
+
args: 'listen -w http://localhost:3001/webhook',
|
|
183
|
+
env: { ZALO_ACCOUNT: 'support', ZALO_PROXY: 'socks5://user:pass@proxy2:1080' }
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Health Monitoring
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Check all accounts periodically
|
|
193
|
+
async function healthCheck() {
|
|
194
|
+
for (const [id, { api, proxy }] of accounts) {
|
|
195
|
+
const connected = api.isConnected()
|
|
196
|
+
const status = await api.getStatus().catch(() => 'error')
|
|
197
|
+
|
|
198
|
+
if (!connected) {
|
|
199
|
+
logger.error(`Account ${id} disconnected via ${proxy}`)
|
|
200
|
+
await api.reconnect()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (status === 'banned') {
|
|
204
|
+
logger.critical(`Account ${id} BANNED — removing from rotation`)
|
|
205
|
+
accounts.delete(id)
|
|
206
|
+
// Alert operator
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
setInterval(healthCheck, 5 * 60_000) // every 5 minutes
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Sharp Edges
|
|
217
|
+
|
|
218
|
+
- **Proxy per account**: Sharing proxies between accounts = ban risk for ALL accounts on that proxy
|
|
219
|
+
- **QR expiry**: 60 seconds — have Zalo app ready before starting login
|
|
220
|
+
- **Session conflicts**: Only ONE active session per account — starting a new login invalidates existing sessions
|
|
221
|
+
- **Credential export**: Exported JSON contains session cookies — treat as sensitive as passwords
|
|
222
|
+
- **VPS QR access**: Firewall must allow inbound on QR port (18927) — close after login
|
|
223
|
+
- **Proxy rotation**: Do NOT rotate proxies for established sessions — Zalo treats IP change as suspicious
|
|
224
|
+
- **Rate limits per account**: Each account has independent rate limits — but shared IP without proxy means shared rate limit bucket
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# VietQR & Banking Reference
|
|
2
|
+
|
|
3
|
+
> Loaded by `@rune/zalo` when payment, bank transfer, QR code, or VietQR patterns detected.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## VietQR Transfer Messages
|
|
8
|
+
|
|
9
|
+
Send bank transfer QR codes directly in Zalo chat — recipient scans to pay via their banking app.
|
|
10
|
+
|
|
11
|
+
### Send Bank Card
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# CLI: send bank card with account details
|
|
15
|
+
zalo-agent msg send-card -t <threadId> \
|
|
16
|
+
--bank vcb \
|
|
17
|
+
--account-number 1234567890 \
|
|
18
|
+
--account-name "NGUYEN VAN A"
|
|
19
|
+
|
|
20
|
+
# With custom amount and message
|
|
21
|
+
zalo-agent msg send-card -t <threadId> \
|
|
22
|
+
--bank mbbank \
|
|
23
|
+
--account-number 0987654321 \
|
|
24
|
+
--account-name "TRAN THI B" \
|
|
25
|
+
--amount 500000 \
|
|
26
|
+
--message "Thanh toan don hang #123"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Send QR Transfer
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Generate VietQR and send as image
|
|
33
|
+
zalo-agent msg send-qr-transfer -t <threadId> \
|
|
34
|
+
--bank techcombank \
|
|
35
|
+
--account-number 19033456789 \
|
|
36
|
+
--account-name "LE VAN C" \
|
|
37
|
+
--amount 1000000 \
|
|
38
|
+
--message "Tien thue thang 3"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Implementation Pattern
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { generateVietQR } from './vietqr-utils'
|
|
45
|
+
|
|
46
|
+
interface BankTransfer {
|
|
47
|
+
bankCode: string // BIN code or alias (e.g., 'vcb', 'mbbank')
|
|
48
|
+
accountNumber: string
|
|
49
|
+
accountName: string
|
|
50
|
+
amount?: number
|
|
51
|
+
message?: string // Noi dung chuyen khoan
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function sendBankCard(api: ZaloApi, threadId: string, transfer: BankTransfer) {
|
|
55
|
+
// 1. Resolve bank alias to BIN code
|
|
56
|
+
const bin = BANK_ALIASES[transfer.bankCode] ?? transfer.bankCode
|
|
57
|
+
|
|
58
|
+
// 2. Generate VietQR URL (NAPAS standard)
|
|
59
|
+
const qrUrl = `https://img.vietqr.io/image/${bin}-${transfer.accountNumber}-compact.png`
|
|
60
|
+
+ (transfer.amount ? `?amount=${transfer.amount}` : '')
|
|
61
|
+
+ (transfer.message ? `&addInfo=${encodeURIComponent(transfer.message)}` : '')
|
|
62
|
+
|
|
63
|
+
// 3. Download QR image to temp file
|
|
64
|
+
const tempPath = await downloadToTemp(qrUrl)
|
|
65
|
+
|
|
66
|
+
// 4. Send as image with caption
|
|
67
|
+
const caption = [
|
|
68
|
+
`🏦 ${BANK_NAMES[bin] ?? bin}`,
|
|
69
|
+
`📋 STK: ${transfer.accountNumber}`,
|
|
70
|
+
`👤 ${transfer.accountName}`,
|
|
71
|
+
transfer.amount ? `💰 ${new Intl.NumberFormat('vi-VN').format(transfer.amount)} VND` : null,
|
|
72
|
+
transfer.message ? `📝 ${transfer.message}` : null,
|
|
73
|
+
].filter(Boolean).join('\n')
|
|
74
|
+
|
|
75
|
+
await api.sendMessage({ body: caption, attachments: [tempPath] }, threadId, 'User')
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Vietnamese Bank Aliases
|
|
82
|
+
|
|
83
|
+
55+ banks supported via VietQR / NAPAS standard:
|
|
84
|
+
|
|
85
|
+
| Alias | Bank Name | BIN |
|
|
86
|
+
|-------|-----------|-----|
|
|
87
|
+
| `vcb`, `vietcombank` | Vietcombank | 970436 |
|
|
88
|
+
| `bidv` | BIDV | 970418 |
|
|
89
|
+
| `vietinbank`, `ctg` | VietinBank | 970415 |
|
|
90
|
+
| `agribank` | Agribank | 970405 |
|
|
91
|
+
| `mb`, `mbbank` | MB Bank | 970422 |
|
|
92
|
+
| `techcombank`, `tcb` | Techcombank | 970407 |
|
|
93
|
+
| `acb` | ACB | 970416 |
|
|
94
|
+
| `vpbank` | VPBank | 970432 |
|
|
95
|
+
| `tpbank` | TPBank | 970423 |
|
|
96
|
+
| `sacombank`, `stb` | Sacombank | 970403 |
|
|
97
|
+
| `hdbank` | HDBank | 970437 |
|
|
98
|
+
| `ocb` | OCB | 970448 |
|
|
99
|
+
| `msb` | MSB | 970426 |
|
|
100
|
+
| `seabank` | SeABank | 970440 |
|
|
101
|
+
| `shb` | SHB | 970443 |
|
|
102
|
+
| `eximbank` | Eximbank | 970431 |
|
|
103
|
+
| `vib` | VIB | 970441 |
|
|
104
|
+
| `scb` | SCB | 970429 |
|
|
105
|
+
| `ncb` | NCB | 970419 |
|
|
106
|
+
| `abbank` | ABBank | 970425 |
|
|
107
|
+
| `dongabank` | DongA Bank | 970406 |
|
|
108
|
+
| `kienlongbank` | Kien Long Bank | 970452 |
|
|
109
|
+
| `baovibank` | BaoViet Bank | 970438 |
|
|
110
|
+
| `pvcombank` | PVcomBank | 970412 |
|
|
111
|
+
| `lienvietpostbank`, `lpb` | LienVietPostBank | 970449 |
|
|
112
|
+
| `namabank` | Nam A Bank | 970428 |
|
|
113
|
+
| `pgbank` | PG Bank | 970430 |
|
|
114
|
+
| `vietabank` | VietA Bank | 970427 |
|
|
115
|
+
| `bacabank` | Bac A Bank | 970409 |
|
|
116
|
+
| `saigonbank` | Saigon Bank | 970400 |
|
|
117
|
+
| `gpbank` | GP Bank | 970408 |
|
|
118
|
+
| `cbbank` | CB Bank | 970444 |
|
|
119
|
+
| `oceanbank` | OceanBank | 970414 |
|
|
120
|
+
| `shinhan` | Shinhan Vietnam | 970424 |
|
|
121
|
+
| `hsbc` | HSBC Vietnam | 970443 |
|
|
122
|
+
| `cimb` | CIMB Vietnam | 422589 |
|
|
123
|
+
| `woori` | Woori Vietnam | 970457 |
|
|
124
|
+
| `uob` | UOB Vietnam | 970458 |
|
|
125
|
+
| `standardchartered`, `sc` | Standard Chartered VN | 970410 |
|
|
126
|
+
| `publicbank` | Public Bank VN | 970439 |
|
|
127
|
+
| `cake` | CAKE (VPBank Digital) | 546034 |
|
|
128
|
+
| `ubank` | Ubank (VPBank) | 546035 |
|
|
129
|
+
| `timo` | Timo (Ban Viet) | 963388 |
|
|
130
|
+
| `vnptmoney` | VNPT Money | 971011 |
|
|
131
|
+
| `momo` | MoMo (via NAPAS) | 999996 |
|
|
132
|
+
| `zalopay` | ZaloPay (via NAPAS) | 999997 |
|
|
133
|
+
| `viettelpay` | ViettelPay | 971005 |
|
|
134
|
+
|
|
135
|
+
### Alias Lookup
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const BANK_ALIASES: Record<string, string> = {
|
|
139
|
+
vcb: '970436', vietcombank: '970436',
|
|
140
|
+
bidv: '970418',
|
|
141
|
+
vietinbank: '970415', ctg: '970415',
|
|
142
|
+
agribank: '970405',
|
|
143
|
+
mb: '970422', mbbank: '970422',
|
|
144
|
+
techcombank: '970407', tcb: '970407',
|
|
145
|
+
acb: '970416',
|
|
146
|
+
vpbank: '970432',
|
|
147
|
+
tpbank: '970423',
|
|
148
|
+
// ... extend as needed
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Sharp Edges
|
|
155
|
+
|
|
156
|
+
- **VietQR image service**: `img.vietqr.io` is free but rate-limited — cache generated QR images locally for repeated sends
|
|
157
|
+
- **Amount encoding**: Must be integer (VND has no decimal). `500000` not `500,000` or `500.000`
|
|
158
|
+
- **Message encoding**: `addInfo` must be URL-encoded, max 25 characters (NAPAS limit), ASCII only — no Vietnamese diacritics
|
|
159
|
+
- **Bank BIN codes**: Change rarely but verify against NAPAS registry for production use
|
|
160
|
+
- **QR expiry**: VietQR codes don't expire — they encode static account info. But amounts can become stale if prices change
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rune-kit/rune",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "60-skill mesh for AI coding assistants — 5-layer architecture, 200+ connections, 8 platforms (Claude Code, Cursor, Windsurf, Antigravity, Codex, OpenCode, OpenClaw, Generic)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rune": "./compiler/bin/rune.js"
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"commands/",
|
|
51
51
|
"agents/",
|
|
52
52
|
"hooks/",
|
|
53
|
-
"docs/",
|
|
54
53
|
"references/"
|
|
55
54
|
],
|
|
56
55
|
"homepage": "https://rune-kit.github.io/rune",
|
|
@@ -28,6 +28,7 @@ Create marketing assets and execute launch strategy. Marketing generates landing
|
|
|
28
28
|
- `research` (L3): competitor analysis, SEO keyword data
|
|
29
29
|
- `asset-creator` (L3): generate OG images, social cards, banners
|
|
30
30
|
- `video-creator` (L3): create demo/explainer video plan
|
|
31
|
+
- `slides` (L3): generate presentation decks for launches and demos
|
|
31
32
|
- `browser-pilot` (L3): capture screenshots for marketing assets
|
|
32
33
|
- L4 extension packs: domain-specific content when context matches (e.g., @rune/content for blog posts, @rune/analytics for campaign measurement)
|
|
33
34
|
|
|
@@ -161,6 +162,8 @@ Call `rune:video-creator` to produce:
|
|
|
161
162
|
- 60-second demo video script (screen recording plan)
|
|
162
163
|
- Shot list with timestamps
|
|
163
164
|
|
|
165
|
+
Call `rune:slides` to generate presentation decks for launch demos, sprint reviews, or investor pitches.
|
|
166
|
+
|
|
164
167
|
If `rune:browser-pilot` is available, capture screenshots of the running app to use as real product imagery.
|
|
165
168
|
|
|
166
169
|
### Step 7 — Present for approval
|