@rune-kit/rune 2.10.0 → 2.12.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/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,317 +1,317 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: zalo-oa-mcp
|
|
3
|
-
pack: "@rune/zalo"
|
|
4
|
-
description: MCP server blueprint — AI agent reads and sends Zalo OA messages. Webhook-to-MCP bridge, tool definitions, conversation loop.
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# zalo-oa-mcp
|
|
10
|
-
|
|
11
|
-
MCP server blueprint that bridges AI agents (Claude) with Zalo OA — enabling the use case "AI agent chats via Zalo".
|
|
12
|
-
|
|
13
|
-
#### Architecture
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
User sends message via Zalo
|
|
17
|
-
→ Zalo webhook POST to your server
|
|
18
|
-
→ Webhook handler verifies signature, stores in message queue
|
|
19
|
-
→ MCP server exposes tools:
|
|
20
|
-
zalo_read_messages — poll queue for new messages
|
|
21
|
-
zalo_send_message — send reply via OA API
|
|
22
|
-
zalo_get_profile — fetch user info (cached)
|
|
23
|
-
zalo_list_followers — list OA followers
|
|
24
|
-
zalo_send_broadcast — broadcast with targeting (confirm-gated)
|
|
25
|
-
→ AI agent (Claude) calls these tools in a conversation loop
|
|
26
|
-
→ Agent processes message, decides response
|
|
27
|
-
→ Calls zalo_send_message to reply
|
|
28
|
-
→ User receives reply in Zalo
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Webhook server and MCP server run in the **same Node.js process** — no IPC overhead, shared in-memory queue.
|
|
32
|
-
|
|
33
|
-
#### MCP Tool Definitions
|
|
34
|
-
|
|
35
|
-
**1. zalo_read_messages** — query, auto-approve
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"name": "zalo_read_messages",
|
|
40
|
-
"description": "Poll the webhook queue for new Zalo OA messages. Returns messages received since last read.",
|
|
41
|
-
"inputSchema": {
|
|
42
|
-
"type": "object",
|
|
43
|
-
"properties": {
|
|
44
|
-
"limit": { "type": "number", "default": 10, "description": "Max messages to return" },
|
|
45
|
-
"since_timestamp": { "type": "number", "description": "Unix ms — only return messages after this time" }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Returns: `{ messages: [{ user_id, user_name, text, attachments, timestamp, msg_id }] }`
|
|
52
|
-
|
|
53
|
-
**2. zalo_send_message** — mutation, confirm before send
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"name": "zalo_send_message",
|
|
58
|
-
"description": "Send a message to a Zalo OA follower. Requires confirmation before execution.",
|
|
59
|
-
"inputSchema": {
|
|
60
|
-
"type": "object",
|
|
61
|
-
"required": ["user_id", "text"],
|
|
62
|
-
"properties": {
|
|
63
|
-
"user_id": { "type": "string", "description": "OA-scoped user ID" },
|
|
64
|
-
"text": { "type": "string", "description": "Message text (max 2000 chars)" },
|
|
65
|
-
"message_type": { "type": "string", "enum": ["text", "image", "template"], "default": "text" },
|
|
66
|
-
"attachment_id": { "type": "string", "description": "Required when message_type is image" }
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Returns: `{ success: true, msg_id: "..." }` or `{ success: false, error: "..." }`
|
|
73
|
-
|
|
74
|
-
**3. zalo_get_profile** — query, auto-approve, 1-hour TTL cache
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
"name": "zalo_get_profile",
|
|
79
|
-
"description": "Get Zalo user profile. Cached for 1 hour to avoid repeated API calls.",
|
|
80
|
-
"inputSchema": {
|
|
81
|
-
"type": "object",
|
|
82
|
-
"required": ["user_id"],
|
|
83
|
-
"properties": {
|
|
84
|
-
"user_id": { "type": "string" }
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Returns: `{ display_name, avatar, user_id, is_follower }`
|
|
91
|
-
|
|
92
|
-
**4. zalo_list_followers** — query, auto-approve
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"name": "zalo_list_followers",
|
|
97
|
-
"description": "List OA followers with pagination.",
|
|
98
|
-
"inputSchema": {
|
|
99
|
-
"type": "object",
|
|
100
|
-
"properties": {
|
|
101
|
-
"offset": { "type": "number", "default": 0 },
|
|
102
|
-
"count": { "type": "number", "default": 50, "maximum": 50 }
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Returns: `{ followers: [{ user_id, display_name }], total }`
|
|
109
|
-
|
|
110
|
-
**5. zalo_send_broadcast** — mutation, ALWAYS confirm with preview
|
|
111
|
-
|
|
112
|
-
```json
|
|
113
|
-
{
|
|
114
|
-
"name": "zalo_send_broadcast",
|
|
115
|
-
"description": "Broadcast message to all followers or filtered segment. Always shows preview before sending.",
|
|
116
|
-
"inputSchema": {
|
|
117
|
-
"type": "object",
|
|
118
|
-
"required": ["text"],
|
|
119
|
-
"properties": {
|
|
120
|
-
"text": { "type": "string" },
|
|
121
|
-
"target": {
|
|
122
|
-
"type": "object",
|
|
123
|
-
"properties": {
|
|
124
|
-
"gender": { "type": "string", "enum": ["male", "female"] },
|
|
125
|
-
"age_range": { "type": "object", "properties": { "min": { "type": "number" }, "max": { "type": "number" } } },
|
|
126
|
-
"city": { "type": "string" }
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Returns: `{ success: true, sent_count: 1240 }` — always preview target before sending.
|
|
135
|
-
|
|
136
|
-
#### MCP Server Implementation
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
140
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
141
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
142
|
-
import { Hono } from 'hono'
|
|
143
|
-
import { serve } from '@hono/node-server'
|
|
144
|
-
|
|
145
|
-
// ── Message Queue (webhook → MCP bridge) ──────────────────────────────────
|
|
146
|
-
const MAX_QUEUE = 1000
|
|
147
|
-
const messageQueue: ZaloMessage[] = []
|
|
148
|
-
|
|
149
|
-
function enqueue(msg: ZaloMessage): void {
|
|
150
|
-
messageQueue.push(msg)
|
|
151
|
-
if (messageQueue.length > MAX_QUEUE) messageQueue.shift() // drop oldest
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── Profile Cache (1-hour TTL) ─────────────────────────────────────────────
|
|
155
|
-
const profileCache = new Map<string, { data: ZaloProfile; expiry: number }>()
|
|
156
|
-
|
|
157
|
-
async function getCachedProfile(userId: string): Promise<ZaloProfile> {
|
|
158
|
-
const cached = profileCache.get(userId)
|
|
159
|
-
if (cached && Date.now() < cached.expiry) return cached.data
|
|
160
|
-
const data = await fetchOAProfile(userId)
|
|
161
|
-
profileCache.set(userId, { data, expiry: Date.now() + 3_600_000 })
|
|
162
|
-
return data
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ── MCP Server ─────────────────────────────────────────────────────────────
|
|
166
|
-
const server = new Server(
|
|
167
|
-
{ name: 'zalo-oa-mcp', version: '1.0.0' },
|
|
168
|
-
{ capabilities: { tools: {} } }
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
172
|
-
tools: [
|
|
173
|
-
{ name: 'zalo_read_messages', description: '...', inputSchema: { /* as above */ } },
|
|
174
|
-
{ name: 'zalo_send_message', description: '...', inputSchema: { /* as above */ } },
|
|
175
|
-
{ name: 'zalo_get_profile', description: '...', inputSchema: { /* as above */ } },
|
|
176
|
-
{ name: 'zalo_list_followers',description: '...', inputSchema: { /* as above */ } },
|
|
177
|
-
{ name: 'zalo_send_broadcast',description: '...', inputSchema: { /* as above */ } },
|
|
178
|
-
]
|
|
179
|
-
}))
|
|
180
|
-
|
|
181
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
182
|
-
const { name, arguments: args } = request.params
|
|
183
|
-
|
|
184
|
-
switch (name) {
|
|
185
|
-
case 'zalo_read_messages': {
|
|
186
|
-
const { limit = 10, since_timestamp } = args as { limit?: number; since_timestamp?: number }
|
|
187
|
-
const msgs = since_timestamp
|
|
188
|
-
? messageQueue.filter(m => m.timestamp > since_timestamp).slice(-limit)
|
|
189
|
-
: messageQueue.slice(-limit)
|
|
190
|
-
return { content: [{ type: 'text', text: JSON.stringify({ messages: msgs }) }] }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
case 'zalo_send_message': {
|
|
194
|
-
const { user_id, text, message_type = 'text', attachment_id } = args as SendMessageArgs
|
|
195
|
-
const result = await sendOAMessage({ user_id, text, message_type, attachment_id })
|
|
196
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
case 'zalo_get_profile': {
|
|
200
|
-
const profile = await getCachedProfile((args as { user_id: string }).user_id)
|
|
201
|
-
return { content: [{ type: 'text', text: JSON.stringify(profile) }] }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
case 'zalo_list_followers': {
|
|
205
|
-
const { offset = 0, count = 50 } = args as { offset?: number; count?: number }
|
|
206
|
-
const result = await listOAFollowers(offset, Math.min(count, 50))
|
|
207
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
case 'zalo_send_broadcast': {
|
|
211
|
-
// Confirmation preview MUST be shown before sending
|
|
212
|
-
const { text, target } = args as BroadcastArgs
|
|
213
|
-
const preview = `BROADCAST PREVIEW:\nText: "${text}"\nTarget: ${JSON.stringify(target ?? 'all followers')}\nConfirm?`
|
|
214
|
-
return { content: [{ type: 'text', text: preview }] }
|
|
215
|
-
// On confirmed re-call with confirmed: true, execute sendOABroadcast()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
default:
|
|
219
|
-
throw new Error(`Unknown tool: ${name}`)
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
// ── Webhook Server (same process) ──────────────────────────────────────────
|
|
224
|
-
const app = new Hono()
|
|
225
|
-
|
|
226
|
-
app.post('/webhook/zalo', async (c) => {
|
|
227
|
-
const signature = c.req.header('X-ZEvent-Signature') ?? ''
|
|
228
|
-
const body = await c.req.text()
|
|
229
|
-
|
|
230
|
-
if (!verifySignature(body, signature)) return c.json({ error: 'Invalid signature' }, 403)
|
|
231
|
-
|
|
232
|
-
const event = JSON.parse(body)
|
|
233
|
-
c.executionCtx?.waitUntil(handleWebhookEvent(event))
|
|
234
|
-
return c.json({ received: true })
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
async function handleWebhookEvent(event: ZaloEvent): Promise<void> {
|
|
238
|
-
if (event.event_name !== 'user_send_text') return // extend as needed
|
|
239
|
-
const profile = await getCachedProfile(event.sender.id).catch(() => null)
|
|
240
|
-
enqueue({
|
|
241
|
-
user_id: event.sender.id,
|
|
242
|
-
user_name: profile?.display_name ?? event.sender.id,
|
|
243
|
-
text: event.message.text,
|
|
244
|
-
attachments: event.message.attachments ?? [],
|
|
245
|
-
timestamp: event.timestamp,
|
|
246
|
-
msg_id: event.message.msg_id,
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ── Boot both in same process ───────────────────────────────────────────────
|
|
251
|
-
serve({ fetch: app.fetch, port: 3000 })
|
|
252
|
-
const transport = new StdioServerTransport()
|
|
253
|
-
await server.connect(transport)
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
#### Credential Management
|
|
257
|
-
|
|
258
|
-
Store credentials in `~/.zalo-mcp/credentials.json` — never commit this file.
|
|
259
|
-
|
|
260
|
-
```json
|
|
261
|
-
{
|
|
262
|
-
"oa_token": "OA_ACCESS_TOKEN",
|
|
263
|
-
"oa_secret_key": "OA_SECRET_KEY_FOR_WEBHOOK",
|
|
264
|
-
"refresh_token": "REFRESH_TOKEN",
|
|
265
|
-
"expires_at": 1712345678000
|
|
266
|
-
}
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
MCP server reads on startup and auto-refreshes before expiry. Never expose tokens via MCP tool responses.
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
import { readFileSync, writeFileSync } from 'fs'
|
|
273
|
-
import { homedir } from 'os'
|
|
274
|
-
import { join } from 'path'
|
|
275
|
-
|
|
276
|
-
const CREDS_PATH = join(homedir(), '.zalo-mcp', 'credentials.json')
|
|
277
|
-
|
|
278
|
-
function loadCredentials(): ZaloCredentials {
|
|
279
|
-
const raw = readFileSync(CREDS_PATH, 'utf-8')
|
|
280
|
-
return JSON.parse(raw) as ZaloCredentials
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
#### Conversation Loop (Agent Side)
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
// Claude agent system prompt excerpt
|
|
288
|
-
const systemPrompt = `
|
|
289
|
-
You are a Zalo OA customer support agent.
|
|
290
|
-
- Call zalo_read_messages to get new messages from users
|
|
291
|
-
- Call zalo_get_profile to personalize responses
|
|
292
|
-
- Reply via zalo_send_message — ALWAYS confirm before sending
|
|
293
|
-
- You cannot reply to users who haven't messaged in the last 7 days (OA API constraint)
|
|
294
|
-
- Keep replies concise — Zalo UI shows ~160 chars before truncation on mobile
|
|
295
|
-
`
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
#### Tool Safety Classification
|
|
299
|
-
|
|
300
|
-
| Tool | Class | Approval |
|
|
301
|
-
|------|-------|----------|
|
|
302
|
-
| `zalo_read_messages` | query | auto-approve |
|
|
303
|
-
| `zalo_get_profile` | query | auto-approve |
|
|
304
|
-
| `zalo_list_followers` | query | auto-approve |
|
|
305
|
-
| `zalo_send_message` | mutation | confirm before send |
|
|
306
|
-
| `zalo_send_broadcast` | mutation | ALWAYS confirm — shows preview |
|
|
307
|
-
|
|
308
|
-
Rate limiting: call `zalo-rate-guard` before `zalo_send_message` and `zalo_send_broadcast`. See [@rune/zalo rate guard skill](zalo-rate-guard.md).
|
|
309
|
-
|
|
310
|
-
#### Sharp Edges
|
|
311
|
-
|
|
312
|
-
- **Queue overflow**: Queue caps at 1000 messages, drops oldest. If agent polls infrequently in high-traffic OA, messages are lost. Use Redis `LPUSH/LTRIM` in production.
|
|
313
|
-
- **7-day CS window**: OA API rejects sends to users who haven't initiated contact in 7 days. Agent must check `timestamp` before attempting reply — surface this as a graceful error, not a crash.
|
|
314
|
-
- **Rate limiting is mandatory**: `zalo_send_message` must go through `zalo-rate-guard` — OA bans are silent and permanent.
|
|
315
|
-
- **user_id is OA-scoped**: The same Zalo user has different IDs per OA. Cache `display_name` via `zalo_get_profile` to humanize logs and agent context.
|
|
316
|
-
- **Single process, not microservices**: Webhook and MCP server share the same queue in-memory. Splitting into separate processes requires a Redis or HTTP bridge — adds latency and operational overhead with no benefit at typical OA traffic volumes.
|
|
317
|
-
- **Broadcast confirmation is non-negotiable**: `zalo_send_broadcast` without preview risks mass-spamming followers. Always return preview on first call; only execute on explicit re-call with `confirmed: true`.
|
|
1
|
+
---
|
|
2
|
+
name: zalo-oa-mcp
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: MCP server blueprint — AI agent reads and sends Zalo OA messages. Webhook-to-MCP bridge, tool definitions, conversation loop.
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-oa-mcp
|
|
10
|
+
|
|
11
|
+
MCP server blueprint that bridges AI agents (Claude) with Zalo OA — enabling the use case "AI agent chats via Zalo".
|
|
12
|
+
|
|
13
|
+
#### Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
User sends message via Zalo
|
|
17
|
+
→ Zalo webhook POST to your server
|
|
18
|
+
→ Webhook handler verifies signature, stores in message queue
|
|
19
|
+
→ MCP server exposes tools:
|
|
20
|
+
zalo_read_messages — poll queue for new messages
|
|
21
|
+
zalo_send_message — send reply via OA API
|
|
22
|
+
zalo_get_profile — fetch user info (cached)
|
|
23
|
+
zalo_list_followers — list OA followers
|
|
24
|
+
zalo_send_broadcast — broadcast with targeting (confirm-gated)
|
|
25
|
+
→ AI agent (Claude) calls these tools in a conversation loop
|
|
26
|
+
→ Agent processes message, decides response
|
|
27
|
+
→ Calls zalo_send_message to reply
|
|
28
|
+
→ User receives reply in Zalo
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Webhook server and MCP server run in the **same Node.js process** — no IPC overhead, shared in-memory queue.
|
|
32
|
+
|
|
33
|
+
#### MCP Tool Definitions
|
|
34
|
+
|
|
35
|
+
**1. zalo_read_messages** — query, auto-approve
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"name": "zalo_read_messages",
|
|
40
|
+
"description": "Poll the webhook queue for new Zalo OA messages. Returns messages received since last read.",
|
|
41
|
+
"inputSchema": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"limit": { "type": "number", "default": 10, "description": "Max messages to return" },
|
|
45
|
+
"since_timestamp": { "type": "number", "description": "Unix ms — only return messages after this time" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Returns: `{ messages: [{ user_id, user_name, text, attachments, timestamp, msg_id }] }`
|
|
52
|
+
|
|
53
|
+
**2. zalo_send_message** — mutation, confirm before send
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"name": "zalo_send_message",
|
|
58
|
+
"description": "Send a message to a Zalo OA follower. Requires confirmation before execution.",
|
|
59
|
+
"inputSchema": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["user_id", "text"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"user_id": { "type": "string", "description": "OA-scoped user ID" },
|
|
64
|
+
"text": { "type": "string", "description": "Message text (max 2000 chars)" },
|
|
65
|
+
"message_type": { "type": "string", "enum": ["text", "image", "template"], "default": "text" },
|
|
66
|
+
"attachment_id": { "type": "string", "description": "Required when message_type is image" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Returns: `{ success: true, msg_id: "..." }` or `{ success: false, error: "..." }`
|
|
73
|
+
|
|
74
|
+
**3. zalo_get_profile** — query, auto-approve, 1-hour TTL cache
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"name": "zalo_get_profile",
|
|
79
|
+
"description": "Get Zalo user profile. Cached for 1 hour to avoid repeated API calls.",
|
|
80
|
+
"inputSchema": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"required": ["user_id"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"user_id": { "type": "string" }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Returns: `{ display_name, avatar, user_id, is_follower }`
|
|
91
|
+
|
|
92
|
+
**4. zalo_list_followers** — query, auto-approve
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"name": "zalo_list_followers",
|
|
97
|
+
"description": "List OA followers with pagination.",
|
|
98
|
+
"inputSchema": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
"offset": { "type": "number", "default": 0 },
|
|
102
|
+
"count": { "type": "number", "default": 50, "maximum": 50 }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Returns: `{ followers: [{ user_id, display_name }], total }`
|
|
109
|
+
|
|
110
|
+
**5. zalo_send_broadcast** — mutation, ALWAYS confirm with preview
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"name": "zalo_send_broadcast",
|
|
115
|
+
"description": "Broadcast message to all followers or filtered segment. Always shows preview before sending.",
|
|
116
|
+
"inputSchema": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["text"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"text": { "type": "string" },
|
|
121
|
+
"target": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"gender": { "type": "string", "enum": ["male", "female"] },
|
|
125
|
+
"age_range": { "type": "object", "properties": { "min": { "type": "number" }, "max": { "type": "number" } } },
|
|
126
|
+
"city": { "type": "string" }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Returns: `{ success: true, sent_count: 1240 }` — always preview target before sending.
|
|
135
|
+
|
|
136
|
+
#### MCP Server Implementation
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
140
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
141
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
142
|
+
import { Hono } from 'hono'
|
|
143
|
+
import { serve } from '@hono/node-server'
|
|
144
|
+
|
|
145
|
+
// ── Message Queue (webhook → MCP bridge) ──────────────────────────────────
|
|
146
|
+
const MAX_QUEUE = 1000
|
|
147
|
+
const messageQueue: ZaloMessage[] = []
|
|
148
|
+
|
|
149
|
+
function enqueue(msg: ZaloMessage): void {
|
|
150
|
+
messageQueue.push(msg)
|
|
151
|
+
if (messageQueue.length > MAX_QUEUE) messageQueue.shift() // drop oldest
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Profile Cache (1-hour TTL) ─────────────────────────────────────────────
|
|
155
|
+
const profileCache = new Map<string, { data: ZaloProfile; expiry: number }>()
|
|
156
|
+
|
|
157
|
+
async function getCachedProfile(userId: string): Promise<ZaloProfile> {
|
|
158
|
+
const cached = profileCache.get(userId)
|
|
159
|
+
if (cached && Date.now() < cached.expiry) return cached.data
|
|
160
|
+
const data = await fetchOAProfile(userId)
|
|
161
|
+
profileCache.set(userId, { data, expiry: Date.now() + 3_600_000 })
|
|
162
|
+
return data
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── MCP Server ─────────────────────────────────────────────────────────────
|
|
166
|
+
const server = new Server(
|
|
167
|
+
{ name: 'zalo-oa-mcp', version: '1.0.0' },
|
|
168
|
+
{ capabilities: { tools: {} } }
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
172
|
+
tools: [
|
|
173
|
+
{ name: 'zalo_read_messages', description: '...', inputSchema: { /* as above */ } },
|
|
174
|
+
{ name: 'zalo_send_message', description: '...', inputSchema: { /* as above */ } },
|
|
175
|
+
{ name: 'zalo_get_profile', description: '...', inputSchema: { /* as above */ } },
|
|
176
|
+
{ name: 'zalo_list_followers',description: '...', inputSchema: { /* as above */ } },
|
|
177
|
+
{ name: 'zalo_send_broadcast',description: '...', inputSchema: { /* as above */ } },
|
|
178
|
+
]
|
|
179
|
+
}))
|
|
180
|
+
|
|
181
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
182
|
+
const { name, arguments: args } = request.params
|
|
183
|
+
|
|
184
|
+
switch (name) {
|
|
185
|
+
case 'zalo_read_messages': {
|
|
186
|
+
const { limit = 10, since_timestamp } = args as { limit?: number; since_timestamp?: number }
|
|
187
|
+
const msgs = since_timestamp
|
|
188
|
+
? messageQueue.filter(m => m.timestamp > since_timestamp).slice(-limit)
|
|
189
|
+
: messageQueue.slice(-limit)
|
|
190
|
+
return { content: [{ type: 'text', text: JSON.stringify({ messages: msgs }) }] }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'zalo_send_message': {
|
|
194
|
+
const { user_id, text, message_type = 'text', attachment_id } = args as SendMessageArgs
|
|
195
|
+
const result = await sendOAMessage({ user_id, text, message_type, attachment_id })
|
|
196
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'zalo_get_profile': {
|
|
200
|
+
const profile = await getCachedProfile((args as { user_id: string }).user_id)
|
|
201
|
+
return { content: [{ type: 'text', text: JSON.stringify(profile) }] }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case 'zalo_list_followers': {
|
|
205
|
+
const { offset = 0, count = 50 } = args as { offset?: number; count?: number }
|
|
206
|
+
const result = await listOAFollowers(offset, Math.min(count, 50))
|
|
207
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'zalo_send_broadcast': {
|
|
211
|
+
// Confirmation preview MUST be shown before sending
|
|
212
|
+
const { text, target } = args as BroadcastArgs
|
|
213
|
+
const preview = `BROADCAST PREVIEW:\nText: "${text}"\nTarget: ${JSON.stringify(target ?? 'all followers')}\nConfirm?`
|
|
214
|
+
return { content: [{ type: 'text', text: preview }] }
|
|
215
|
+
// On confirmed re-call with confirmed: true, execute sendOABroadcast()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// ── Webhook Server (same process) ──────────────────────────────────────────
|
|
224
|
+
const app = new Hono()
|
|
225
|
+
|
|
226
|
+
app.post('/webhook/zalo', async (c) => {
|
|
227
|
+
const signature = c.req.header('X-ZEvent-Signature') ?? ''
|
|
228
|
+
const body = await c.req.text()
|
|
229
|
+
|
|
230
|
+
if (!verifySignature(body, signature)) return c.json({ error: 'Invalid signature' }, 403)
|
|
231
|
+
|
|
232
|
+
const event = JSON.parse(body)
|
|
233
|
+
c.executionCtx?.waitUntil(handleWebhookEvent(event))
|
|
234
|
+
return c.json({ received: true })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
async function handleWebhookEvent(event: ZaloEvent): Promise<void> {
|
|
238
|
+
if (event.event_name !== 'user_send_text') return // extend as needed
|
|
239
|
+
const profile = await getCachedProfile(event.sender.id).catch(() => null)
|
|
240
|
+
enqueue({
|
|
241
|
+
user_id: event.sender.id,
|
|
242
|
+
user_name: profile?.display_name ?? event.sender.id,
|
|
243
|
+
text: event.message.text,
|
|
244
|
+
attachments: event.message.attachments ?? [],
|
|
245
|
+
timestamp: event.timestamp,
|
|
246
|
+
msg_id: event.message.msg_id,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Boot both in same process ───────────────────────────────────────────────
|
|
251
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
252
|
+
const transport = new StdioServerTransport()
|
|
253
|
+
await server.connect(transport)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Credential Management
|
|
257
|
+
|
|
258
|
+
Store credentials in `~/.zalo-mcp/credentials.json` — never commit this file.
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"oa_token": "OA_ACCESS_TOKEN",
|
|
263
|
+
"oa_secret_key": "OA_SECRET_KEY_FOR_WEBHOOK",
|
|
264
|
+
"refresh_token": "REFRESH_TOKEN",
|
|
265
|
+
"expires_at": 1712345678000
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
MCP server reads on startup and auto-refreshes before expiry. Never expose tokens via MCP tool responses.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { readFileSync, writeFileSync } from 'fs'
|
|
273
|
+
import { homedir } from 'os'
|
|
274
|
+
import { join } from 'path'
|
|
275
|
+
|
|
276
|
+
const CREDS_PATH = join(homedir(), '.zalo-mcp', 'credentials.json')
|
|
277
|
+
|
|
278
|
+
function loadCredentials(): ZaloCredentials {
|
|
279
|
+
const raw = readFileSync(CREDS_PATH, 'utf-8')
|
|
280
|
+
return JSON.parse(raw) as ZaloCredentials
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Conversation Loop (Agent Side)
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// Claude agent system prompt excerpt
|
|
288
|
+
const systemPrompt = `
|
|
289
|
+
You are a Zalo OA customer support agent.
|
|
290
|
+
- Call zalo_read_messages to get new messages from users
|
|
291
|
+
- Call zalo_get_profile to personalize responses
|
|
292
|
+
- Reply via zalo_send_message — ALWAYS confirm before sending
|
|
293
|
+
- You cannot reply to users who haven't messaged in the last 7 days (OA API constraint)
|
|
294
|
+
- Keep replies concise — Zalo UI shows ~160 chars before truncation on mobile
|
|
295
|
+
`
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Tool Safety Classification
|
|
299
|
+
|
|
300
|
+
| Tool | Class | Approval |
|
|
301
|
+
|------|-------|----------|
|
|
302
|
+
| `zalo_read_messages` | query | auto-approve |
|
|
303
|
+
| `zalo_get_profile` | query | auto-approve |
|
|
304
|
+
| `zalo_list_followers` | query | auto-approve |
|
|
305
|
+
| `zalo_send_message` | mutation | confirm before send |
|
|
306
|
+
| `zalo_send_broadcast` | mutation | ALWAYS confirm — shows preview |
|
|
307
|
+
|
|
308
|
+
Rate limiting: call `zalo-rate-guard` before `zalo_send_message` and `zalo_send_broadcast`. See [@rune/zalo rate guard skill](zalo-rate-guard.md).
|
|
309
|
+
|
|
310
|
+
#### Sharp Edges
|
|
311
|
+
|
|
312
|
+
- **Queue overflow**: Queue caps at 1000 messages, drops oldest. If agent polls infrequently in high-traffic OA, messages are lost. Use Redis `LPUSH/LTRIM` in production.
|
|
313
|
+
- **7-day CS window**: OA API rejects sends to users who haven't initiated contact in 7 days. Agent must check `timestamp` before attempting reply — surface this as a graceful error, not a crash.
|
|
314
|
+
- **Rate limiting is mandatory**: `zalo_send_message` must go through `zalo-rate-guard` — OA bans are silent and permanent.
|
|
315
|
+
- **user_id is OA-scoped**: The same Zalo user has different IDs per OA. Cache `display_name` via `zalo_get_profile` to humanize logs and agent context.
|
|
316
|
+
- **Single process, not microservices**: Webhook and MCP server share the same queue in-memory. Splitting into separate processes requires a Redis or HTTP bridge — adds latency and operational overhead with no benefit at typical OA traffic volumes.
|
|
317
|
+
- **Broadcast confirmation is non-negotiable**: `zalo_send_broadcast` without preview risks mass-spamming followers. Always return preview on first call; only execute on explicit re-call with `confirmed: true`.
|