hyperclaw 4.0.2 → 5.0.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 +246 -60
- package/dist/a2ui-protocol-CfBI44-Q.js +75 -0
- package/dist/agents-routing-ChHiZp36.js +327 -0
- package/dist/agents-routing-ChqZ6l2S.js +4 -0
- package/dist/api-keys-guide-BCcOl0Q7.js +149 -0
- package/dist/api-keys-guide-CGn5BSF7.js +149 -0
- package/dist/audit-BJohI_vC.js +441 -0
- package/dist/audit-BaIiyWFu.js +441 -0
- package/dist/bounty-tools-CY_i91DU.js +211 -0
- package/dist/bounty-tools-DWudyZie.js +211 -0
- package/dist/browser-tools-BsTeGMnX.js +5 -0
- package/dist/browser-tools-D8_rLe2p.js +179 -0
- package/dist/claw-tasks-CgTsiNE8.js +80 -0
- package/dist/claw-tasks-Cyzdbhz_.js +80 -0
- package/dist/connector-5N0-X_xs.js +194 -0
- package/dist/connector-B3v0qcXg.js +425 -0
- package/dist/connector-B8R3iBY1.js +280 -0
- package/dist/connector-BAM-08NN.js +189 -0
- package/dist/connector-BC8FIVu4.js +181 -0
- package/dist/connector-BDmwwaVc.js +213 -0
- package/dist/connector-BGjbBy69.js +225 -0
- package/dist/connector-BO2SRzfG.js +218 -0
- package/dist/connector-BfXky0L3.js +167 -0
- package/dist/connector-BiiSJpx3.js +192 -0
- package/dist/connector-BnDmIhIu.js +85 -0
- package/dist/connector-C1HSoUyk.js +189 -0
- package/dist/connector-CKQHZOXg.js +568 -0
- package/dist/connector-CRl-iidy.js +239 -0
- package/dist/connector-Ci9glMD-.js +340 -0
- package/dist/connector-CjtZIEDj.js +181 -0
- package/dist/connector-Ck6JtOsX.js +531 -0
- package/dist/connector-D8Kelee0.js +286 -0
- package/dist/connector-DAnRJ0oP.js +162 -0
- package/dist/connector-DXTp5PE8.js +508 -0
- package/dist/connector-Dih6dUPP.js +173 -0
- package/dist/connector-DqTH_tPX.js +182 -0
- package/dist/connector-DrnEiiyP.js +419 -0
- package/dist/connector-DtR5GGTX.js +167 -0
- package/dist/connector-Tky_qS_K.js +350 -0
- package/dist/connector-ZSc3oTTy.js +305 -0
- package/dist/connector-sW5yhU1m.js +498 -0
- package/dist/connector-u3ICd3Ic.js +552 -0
- package/dist/cost-tracker-Ca1UPZ33.js +103 -0
- package/dist/cost-tracker-DD9wtWsr.js +103 -0
- package/dist/credentials-store-C6ir0Dae.js +4 -0
- package/dist/credentials-store-CA8UtK0T.js +77 -0
- package/dist/credentials-store-Cm7DH-kh.js +4 -0
- package/dist/credentials-store-H13LqOwJ.js +77 -0
- package/dist/cron-tasks-Bli7Kzd2.js +82 -0
- package/dist/cron-tasks-_pqQCmxc.js +82 -0
- package/dist/daemon-7ViroziB.js +5 -0
- package/dist/daemon-BfyKmZhr.js +318 -0
- package/dist/daemon-Bg4GtCmc.js +318 -0
- package/dist/daemon-DhmwY8k4.js +5 -0
- package/dist/delivery-BmIYy9VQ.js +4 -0
- package/dist/delivery-DVHmv1IR.js +4 -0
- package/dist/delivery-DpMX0Yyc.js +95 -0
- package/dist/delivery-pWUPBp1F.js +95 -0
- package/dist/destructive-gate-D6vWOdEl.js +101 -0
- package/dist/destructive-gate-DZt71UZR.js +101 -0
- package/dist/developer-keys-CPWT7Q6S.js +8 -0
- package/dist/developer-keys-DrrcUqFa.js +127 -0
- package/dist/doctor-BvCe8BBk.js +230 -0
- package/dist/doctor-CxyPLYsJ.js +6 -0
- package/dist/engine-B0kLfRL0.js +256 -0
- package/dist/engine-BJUpRUOv.js +7 -0
- package/dist/engine-CEDSqXfw.js +256 -0
- package/dist/engine-Da4JMNpI.js +7 -0
- package/dist/env-resolve-17ekEU6p.js +10 -0
- package/dist/env-resolve-CiXbWYwe.js +10 -0
- package/dist/env-resolve-CmGWhWXJ.js +115 -0
- package/dist/env-resolve-Z2XF6leB.js +115 -0
- package/dist/extraction-tools-HOZstZ0y.js +91 -0
- package/dist/extraction-tools-m4lmAv7l.js +5 -0
- package/dist/form_data-Cz040rio.js +8657 -0
- package/dist/gmail-watch-setup-Du7DVV7S.js +40 -0
- package/dist/health-B-asI__D.js +6 -0
- package/dist/health-Ds2YlpTB.js +152 -0
- package/dist/heartbeat-engine-BYT5ayQH.js +83 -0
- package/dist/heartbeat-engine-Ut6pXBD6.js +83 -0
- package/dist/hub-9LaKnLjY.js +6 -0
- package/dist/hub-CfwUz9YW.js +515 -0
- package/dist/hub-D0XwdjM-.js +515 -0
- package/dist/hub-LiD5Iztb.js +6 -0
- package/dist/hyperclawbot-CBiDSKsa.js +505 -0
- package/dist/hyperclawbot-zvczQgKx.js +505 -0
- package/dist/inference-0mlFQqIm.js +922 -0
- package/dist/inference-BKVkBREb.js +6 -0
- package/dist/inference-DCXH4Q3x.js +922 -0
- package/dist/inference-SzqFe_nk.js +6 -0
- package/dist/knowledge-graph-DE5lSF02.js +131 -0
- package/dist/knowledge-graph-iBG76fvm.js +131 -0
- package/dist/loader-BkDi8MD9.js +400 -0
- package/dist/loader-CC45xGpC.js +4 -0
- package/dist/loader-CnEdOyjT.js +400 -0
- package/dist/loader-DI2qDRPC.js +4 -0
- package/dist/logger-Cp8wC7F8.js +83 -0
- package/dist/logger-ybOp7VOC.js +83 -0
- package/dist/manager-03ipO9R0.js +105 -0
- package/dist/manager-B2Gls5RG.js +218 -0
- package/dist/manager-BpDfbDjg.js +117 -0
- package/dist/manager-Bxl0sqlh.js +4 -0
- package/dist/manager-CWNSML5D.js +117 -0
- package/dist/manager-CrVDn6eN.js +6 -0
- package/dist/manager-FCgF1plu.js +218 -0
- package/dist/manager-SJe9gt-q.js +4 -0
- package/dist/manager-rgCsaWT1.js +40 -0
- package/dist/mcp-CfoSU4Uz.js +139 -0
- package/dist/mcp-loader-CvxRDtPC.js +94 -0
- package/dist/mcp-loader-DkRBsLpk.js +94 -0
- package/dist/memory-BlHL7JCO.js +4 -0
- package/dist/memory-DsS_eFvJ.js +270 -0
- package/dist/memory-auto-BkvtSFUw.js +5 -0
- package/dist/memory-auto-Bnz_-1wP.js +306 -0
- package/dist/memory-auto-CpQHZlEJ.js +306 -0
- package/dist/memory-auto-Z6LCf-iK.js +5 -0
- package/dist/memory-integration-cSYkZyEo.js +91 -0
- package/dist/memory-integration-g2vxwgoE.js +91 -0
- package/dist/moltbook-BtLDZTfM.js +81 -0
- package/dist/moltbook-Cl8cQfxJ.js +81 -0
- package/dist/node-Dw2Gi-cP.js +222 -0
- package/dist/nodes-registry-B8dmrlLv.js +52 -0
- package/dist/nodes-registry-C9dCFwjh.js +52 -0
- package/dist/oauth-flow-CeaaGAz0.js +150 -0
- package/dist/oauth-flow-DQPvMHRH.js +150 -0
- package/dist/oauth-provider-B4dzn56l.js +110 -0
- package/dist/oauth-provider-Uo4Nib_c.js +110 -0
- package/dist/observability-BV-Yx0V9.js +89 -0
- package/dist/observability-nZ3CBIxG.js +89 -0
- package/dist/onboard-0WoDxbv_.js +10 -0
- package/dist/onboard-BBBWcfhp.js +10 -0
- package/dist/onboard-BXNXCQp4.js +4070 -0
- package/dist/onboard-Bw28IRQ3.js +4070 -0
- package/dist/orchestrator-BovkM63z.js +6 -0
- package/dist/orchestrator-DSbpkP1X.js +189 -0
- package/dist/orchestrator-DmnEvMaL.js +189 -0
- package/dist/orchestrator-RI3bpqqc.js +6 -0
- package/dist/osint-B4_m3VHQ.js +277 -0
- package/dist/pairing-6iM27aD8.js +196 -0
- package/dist/pairing-dGoiGepK.js +4 -0
- package/dist/pc-access-CgCsYrpt.js +8 -0
- package/dist/pc-access-_iH2aorG.js +819 -0
- package/dist/pending-approval-BgNjjuI2.js +22 -0
- package/dist/pending-approval-CUXjysAo.js +22 -0
- package/dist/reminders-store-Drjed_-h.js +58 -0
- package/dist/renderer-BVQrd0_g.js +225 -0
- package/dist/rules-BE4GV6cV.js +103 -0
- package/dist/run-main.js +1639 -460
- package/dist/runner-CJFJUtPm.js +1271 -0
- package/dist/runner-DatMMYYE.js +1271 -0
- package/dist/sdk/index.js +2 -2
- package/dist/sdk/index.mjs +2 -2
- package/dist/security-BqNyT4ID.js +4 -0
- package/dist/security-tpgqPWWH.js +73 -0
- package/dist/server-Brl_HQUB.js +1255 -0
- package/dist/server-D4wVHiX9.js +4 -0
- package/dist/server-Dh3JlBFB.js +1255 -0
- package/dist/server-DhfipkwN.js +4 -0
- package/dist/session-store-BUiPz0Vv.js +5 -0
- package/dist/session-store-is4B6qmD.js +113 -0
- package/dist/sessions-tools-CbUTFe4i.js +5 -0
- package/dist/sessions-tools-CeqD7iil.js +95 -0
- package/dist/skill-loader-BaNLVmJy.js +7 -0
- package/dist/skill-loader-HgpF6Vqs.js +159 -0
- package/dist/skill-runtime-BXWd-Ktf.js +102 -0
- package/dist/skill-runtime-CJN24QPW.js +102 -0
- package/dist/skill-runtime-jgklm02e.js +5 -0
- package/dist/skill-runtime-w1ig_lcw.js +5 -0
- package/dist/src-Bhybpk1J.js +63 -0
- package/dist/src-BxPHKO5x.js +63 -0
- package/dist/src-DIc-L2IG.js +20 -0
- package/dist/src-DMJ4-uqk.js +458 -0
- package/dist/src-g_rNx5rh.js +458 -0
- package/dist/sub-agent-tools-CHQoHz9c.js +39 -0
- package/dist/sub-agent-tools-DHY-4WWM.js +39 -0
- package/dist/theme-DcxwcUgZ.js +180 -0
- package/dist/theme-cx0fkgWC.js +8 -0
- package/dist/tool-policy-CNT-mF2Z.js +189 -0
- package/dist/tool-policy-DZvF8xlQ.js +189 -0
- package/dist/tts-elevenlabs-BRosZv-f.js +61 -0
- package/dist/tts-elevenlabs-C06nUxMK.js +61 -0
- package/dist/update-check-C2Dz85wJ.js +81 -0
- package/dist/update-check-w4XuxVl7.js +81 -0
- package/dist/vision-BMmiIKy7.js +121 -0
- package/dist/vision-JOtOS1Br.js +121 -0
- package/dist/vision-tools-CB28ZCO_.js +5 -0
- package/dist/vision-tools-DVuYc17I.js +51 -0
- package/dist/vision-tools-U3YC4L-g.js +5 -0
- package/dist/vision-tools-vPPwQ-0N.js +51 -0
- package/dist/voice-transcription-B555DbWR.js +138 -0
- package/dist/voice-transcription-DBo5hXmu.js +138 -0
- package/dist/website-watch-tools-DFMrJU-R.js +139 -0
- package/dist/website-watch-tools-Du3W5sN7.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-jS-bbMI5.js');
|
|
2
|
+
const chalk = require_chunk.__toESM(require("chalk"));
|
|
3
|
+
const fs_extra = require_chunk.__toESM(require("fs-extra"));
|
|
4
|
+
const path = require_chunk.__toESM(require("path"));
|
|
5
|
+
const os = require_chunk.__toESM(require("os"));
|
|
6
|
+
const https = require_chunk.__toESM(require("https"));
|
|
7
|
+
const events = require_chunk.__toESM(require("events"));
|
|
8
|
+
|
|
9
|
+
//#region extensions/whatsapp/src/connector.ts
|
|
10
|
+
const STATE_FILE = path.default.join(os.default.homedir(), ".hyperclaw", "whatsapp-state.json");
|
|
11
|
+
const GRAPH_API = "graph.facebook.com";
|
|
12
|
+
const API_VERSION = "v18.0";
|
|
13
|
+
function graphReq(token, method, endpoint, body) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
16
|
+
const req = https.default.request({
|
|
17
|
+
hostname: GRAPH_API,
|
|
18
|
+
port: 443,
|
|
19
|
+
path: `/${API_VERSION}${endpoint}`,
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
"Authorization": `Bearer ${token}`,
|
|
23
|
+
...payload ? {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
26
|
+
} : {}
|
|
27
|
+
}
|
|
28
|
+
}, (res) => {
|
|
29
|
+
let data = "";
|
|
30
|
+
res.on("data", (c) => data += c);
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
const r = JSON.parse(data);
|
|
34
|
+
if (r.error) reject(new Error(`${r.error.code}: ${r.error.message}`));
|
|
35
|
+
else resolve(r);
|
|
36
|
+
} catch {
|
|
37
|
+
reject(new Error("Invalid JSON"));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.on("error", reject);
|
|
42
|
+
if (payload) req.write(payload);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
var WhatsAppConnector = class extends events.EventEmitter {
|
|
47
|
+
config;
|
|
48
|
+
running = false;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
super();
|
|
51
|
+
this.config = {
|
|
52
|
+
dmPolicy: "allowlist",
|
|
53
|
+
allowFrom: [],
|
|
54
|
+
approvedPairings: [],
|
|
55
|
+
pendingPairings: {},
|
|
56
|
+
...config
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async connect() {
|
|
60
|
+
const info = await graphReq(this.config.accessToken, "GET", `/${this.config.phoneNumberId}`);
|
|
61
|
+
await this.loadState();
|
|
62
|
+
this.running = true;
|
|
63
|
+
console.log(chalk.default.green(` 🦅 WhatsApp: ${info.display_phone_number || this.config.phoneNumberId} connected`));
|
|
64
|
+
this.emit("connected", {
|
|
65
|
+
phoneNumberId: this.config.phoneNumberId,
|
|
66
|
+
number: info.display_phone_number
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
disconnect() {
|
|
70
|
+
this.running = false;
|
|
71
|
+
}
|
|
72
|
+
verifyWebhook(mode, token, challenge) {
|
|
73
|
+
if (mode === "subscribe" && token === this.config.verifyToken) return challenge;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
async handleWebhook(body) {
|
|
77
|
+
let payload;
|
|
78
|
+
try {
|
|
79
|
+
payload = JSON.parse(body);
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const entry = payload.entry?.[0];
|
|
84
|
+
const changes = entry?.changes?.[0];
|
|
85
|
+
const value = changes?.value;
|
|
86
|
+
if (!value?.messages) return;
|
|
87
|
+
for (const msg of value.messages) {
|
|
88
|
+
if (msg.type !== "text") continue;
|
|
89
|
+
const from = msg.from;
|
|
90
|
+
const text = msg.text?.body;
|
|
91
|
+
if (!text) continue;
|
|
92
|
+
await this.markRead(msg.id).catch(() => {});
|
|
93
|
+
const allowed = await this.checkDMPolicy(from, text);
|
|
94
|
+
if (!allowed) continue;
|
|
95
|
+
this.emit("message", {
|
|
96
|
+
id: msg.id,
|
|
97
|
+
channelId: "whatsapp",
|
|
98
|
+
from,
|
|
99
|
+
chatId: from,
|
|
100
|
+
text,
|
|
101
|
+
timestamp: new Date(parseInt(msg.timestamp) * 1e3).toISOString(),
|
|
102
|
+
isDM: true,
|
|
103
|
+
name: value.contacts?.[0]?.profile?.name
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async checkDMPolicy(from, text) {
|
|
108
|
+
if (this.config.dmPolicy === "none") return false;
|
|
109
|
+
if (this.config.dmPolicy === "open") return true;
|
|
110
|
+
if (this.config.dmPolicy === "allowlist") {
|
|
111
|
+
if (this.config.allowFrom.includes(from)) return true;
|
|
112
|
+
await this.sendMessage(from, "🦅 HyperClaw: You are not on the allowlist.");
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (this.config.dmPolicy === "pairing") {
|
|
116
|
+
if (this.config.approvedPairings.includes(from)) return true;
|
|
117
|
+
const upper = text.trim().toUpperCase();
|
|
118
|
+
if (this.config.pendingPairings[upper]) {
|
|
119
|
+
this.config.approvedPairings.push(from);
|
|
120
|
+
delete this.config.pendingPairings[upper];
|
|
121
|
+
await this.saveState();
|
|
122
|
+
await this.sendMessage(from, "🦅 *Paired!* You can now send messages.");
|
|
123
|
+
this.emit("pairing:approved", {
|
|
124
|
+
userId: from,
|
|
125
|
+
channelId: "whatsapp"
|
|
126
|
+
});
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const code = Array.from({ length: 6 }, () => "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"[Math.floor(Math.random() * 32)]).join("");
|
|
130
|
+
this.config.pendingPairings[code] = from;
|
|
131
|
+
await this.saveState();
|
|
132
|
+
await this.sendMessage(from, `🦅 HyperClaw Pairing\n\nCode: *${code}*\nAsk owner: hyperclaw pairing approve whatsapp ${code}`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
async sendMessage(to, text) {
|
|
138
|
+
await graphReq(this.config.accessToken, "POST", `/${this.config.phoneNumberId}/messages`, {
|
|
139
|
+
messaging_product: "whatsapp",
|
|
140
|
+
to,
|
|
141
|
+
type: "text",
|
|
142
|
+
text: {
|
|
143
|
+
body: text.slice(0, 4096),
|
|
144
|
+
preview_url: false
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async markRead(messageId) {
|
|
149
|
+
await graphReq(this.config.accessToken, "POST", `/${this.config.phoneNumberId}/messages`, {
|
|
150
|
+
messaging_product: "whatsapp",
|
|
151
|
+
status: "read",
|
|
152
|
+
message_id: messageId
|
|
153
|
+
}).catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
async sendTemplate(to, templateName, languageCode = "en_US") {
|
|
156
|
+
await graphReq(this.config.accessToken, "POST", `/${this.config.phoneNumberId}/messages`, {
|
|
157
|
+
messaging_product: "whatsapp",
|
|
158
|
+
to,
|
|
159
|
+
type: "template",
|
|
160
|
+
template: {
|
|
161
|
+
name: templateName,
|
|
162
|
+
language: { code: languageCode }
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
approvePairing(code) {
|
|
167
|
+
const upper = code.toUpperCase();
|
|
168
|
+
if (!this.config.pendingPairings[upper]) return false;
|
|
169
|
+
this.config.approvedPairings.push(this.config.pendingPairings[upper]);
|
|
170
|
+
delete this.config.pendingPairings[upper];
|
|
171
|
+
this.saveState();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
async loadState() {
|
|
175
|
+
try {
|
|
176
|
+
const s = await fs_extra.default.readJson(STATE_FILE);
|
|
177
|
+
if (s.p) this.config.pendingPairings = s.p;
|
|
178
|
+
if (s.a) this.config.approvedPairings = s.a;
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
async saveState() {
|
|
182
|
+
await fs_extra.default.ensureDir(path.default.dirname(STATE_FILE));
|
|
183
|
+
await fs_extra.default.writeJson(STATE_FILE, {
|
|
184
|
+
p: this.config.pendingPairings,
|
|
185
|
+
a: this.config.approvedPairings
|
|
186
|
+
}, { spaces: 2 });
|
|
187
|
+
}
|
|
188
|
+
isRunning() {
|
|
189
|
+
return this.running;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
exports.WhatsAppConnector = WhatsAppConnector;
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-jS-bbMI5.js');
|
|
2
|
+
const chalk = require_chunk.__toESM(require("chalk"));
|
|
3
|
+
const fs_extra = require_chunk.__toESM(require("fs-extra"));
|
|
4
|
+
const path = require_chunk.__toESM(require("path"));
|
|
5
|
+
const os = require_chunk.__toESM(require("os"));
|
|
6
|
+
const https = require_chunk.__toESM(require("https"));
|
|
7
|
+
const events = require_chunk.__toESM(require("events"));
|
|
8
|
+
|
|
9
|
+
//#region extensions/zalo/src/connector.ts
|
|
10
|
+
const STATE_BASE = path.default.join(os.default.homedir(), ".hyperclaw");
|
|
11
|
+
const BOT_API_HOST = "bot.zaloplatforms.com";
|
|
12
|
+
const BOT_API_BASE = "/v3";
|
|
13
|
+
const TEXT_CHUNK = 2e3;
|
|
14
|
+
const DEFAULT_MEDIA_MB = 5;
|
|
15
|
+
const PAIRING_TTL_MS = 60 * 60 * 1e3;
|
|
16
|
+
const DEDUP_TTL_MS = 6e4;
|
|
17
|
+
function zaloApi(token, method, apiPath, body) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
20
|
+
const qs = method === "GET" && !body ? "" : "";
|
|
21
|
+
const req = https.default.request({
|
|
22
|
+
hostname: BOT_API_HOST,
|
|
23
|
+
port: 443,
|
|
24
|
+
path: `${BOT_API_BASE}${apiPath}${qs}`,
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bot ${token}`,
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
...payload ? {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
32
|
+
} : {}
|
|
33
|
+
}
|
|
34
|
+
}, (res) => {
|
|
35
|
+
let data = "";
|
|
36
|
+
res.on("data", (c) => data += c);
|
|
37
|
+
res.on("end", () => {
|
|
38
|
+
try {
|
|
39
|
+
const r = JSON.parse(data);
|
|
40
|
+
if (r.error && r.error !== 0) reject(new Error(r.message || `Zalo error ${r.error}`));
|
|
41
|
+
else resolve(r);
|
|
42
|
+
} catch {
|
|
43
|
+
reject(new Error("Zalo: invalid JSON"));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
req.on("error", reject);
|
|
48
|
+
req.setTimeout(35e3, () => {
|
|
49
|
+
req.destroy();
|
|
50
|
+
reject(new Error("Zalo API timeout"));
|
|
51
|
+
});
|
|
52
|
+
if (payload) req.write(payload);
|
|
53
|
+
req.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function zaloApiQuery(token, apiPath, params) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const qs = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))).toString();
|
|
59
|
+
const req = https.default.request({
|
|
60
|
+
hostname: BOT_API_HOST,
|
|
61
|
+
port: 443,
|
|
62
|
+
path: `${BOT_API_BASE}${apiPath}?${qs}`,
|
|
63
|
+
method: "GET",
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bot ${token}`,
|
|
66
|
+
Accept: "application/json"
|
|
67
|
+
}
|
|
68
|
+
}, (res) => {
|
|
69
|
+
let data = "";
|
|
70
|
+
res.on("data", (c) => data += c);
|
|
71
|
+
res.on("end", () => {
|
|
72
|
+
try {
|
|
73
|
+
const r = JSON.parse(data);
|
|
74
|
+
if (r.error && r.error !== 0) reject(new Error(r.message || `Zalo error ${r.error}`));
|
|
75
|
+
else resolve(r);
|
|
76
|
+
} catch {
|
|
77
|
+
reject(new Error("Zalo: invalid JSON"));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.on("error", reject);
|
|
82
|
+
req.setTimeout(4e4, () => {
|
|
83
|
+
req.destroy();
|
|
84
|
+
reject(new Error("Zalo poll timeout"));
|
|
85
|
+
});
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
var ZaloAccount = class extends events.EventEmitter {
|
|
90
|
+
cfg;
|
|
91
|
+
accountId;
|
|
92
|
+
token = "";
|
|
93
|
+
running = false;
|
|
94
|
+
offset = 0;
|
|
95
|
+
mediaMaxMb;
|
|
96
|
+
/** Dedup set: "eventName:messageId" → expiry timestamp */
|
|
97
|
+
dedupMap = /* @__PURE__ */ new Map();
|
|
98
|
+
constructor(accountId, cfg) {
|
|
99
|
+
super();
|
|
100
|
+
this.accountId = accountId;
|
|
101
|
+
this.cfg = {
|
|
102
|
+
dmPolicy: "pairing",
|
|
103
|
+
allowFrom: [],
|
|
104
|
+
groupPolicy: "allowlist",
|
|
105
|
+
groupAllowFrom: [],
|
|
106
|
+
mediaMaxMb: DEFAULT_MEDIA_MB,
|
|
107
|
+
pendingPairingTs: {},
|
|
108
|
+
...cfg
|
|
109
|
+
};
|
|
110
|
+
this.cfg.approvedPairings = this.cfg.approvedPairings ?? [];
|
|
111
|
+
this.cfg.pendingPairings = this.cfg.pendingPairings ?? {};
|
|
112
|
+
this.mediaMaxMb = this.cfg.mediaMaxMb ?? DEFAULT_MEDIA_MB;
|
|
113
|
+
}
|
|
114
|
+
async resolveToken() {
|
|
115
|
+
this.token = this.cfg.botToken || (this.cfg.tokenFile ? (await fs_extra.default.readFile(this.cfg.tokenFile, "utf8")).trim() : "");
|
|
116
|
+
if (!this.token) throw new Error(`Zalo [${this.accountId}]: botToken or tokenFile is required`);
|
|
117
|
+
}
|
|
118
|
+
async connect() {
|
|
119
|
+
await this.resolveToken();
|
|
120
|
+
await this.loadState();
|
|
121
|
+
this.running = true;
|
|
122
|
+
const useWebhook = !!this.cfg.webhookUrl;
|
|
123
|
+
if (useWebhook) {
|
|
124
|
+
await this.registerWebhook();
|
|
125
|
+
console.log(chalk.default.green(` 🔵 Zalo [${this.accountId}]: webhook mode — ${this.cfg.webhookUrl}`));
|
|
126
|
+
} else {
|
|
127
|
+
console.log(chalk.default.green(` 🔵 Zalo [${this.accountId}]: long-polling started`));
|
|
128
|
+
this.pollLoop();
|
|
129
|
+
}
|
|
130
|
+
this.emit("connected", {
|
|
131
|
+
accountId: this.accountId,
|
|
132
|
+
mode: useWebhook ? "webhook" : "polling"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
disconnect() {
|
|
136
|
+
this.running = false;
|
|
137
|
+
}
|
|
138
|
+
async registerWebhook() {
|
|
139
|
+
if (!this.cfg.webhookUrl || !this.cfg.webhookSecret) return;
|
|
140
|
+
try {
|
|
141
|
+
await zaloApi(this.token, "POST", "/webhook", {
|
|
142
|
+
url: this.cfg.webhookUrl,
|
|
143
|
+
secret_token: this.cfg.webhookSecret
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.log(chalk.default.yellow(` ⚠ Zalo [${this.accountId}]: webhook registration: ${e.message}`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async pollLoop() {
|
|
150
|
+
while (this.running) try {
|
|
151
|
+
const resp = await zaloApiQuery(this.token, "/event", {
|
|
152
|
+
timeout: 30,
|
|
153
|
+
offset: this.offset
|
|
154
|
+
});
|
|
155
|
+
const events$1 = Array.isArray(resp?.updates) ? resp.updates : Array.isArray(resp?.result) ? resp.result : Array.isArray(resp) ? resp : [];
|
|
156
|
+
for (const event of events$1) {
|
|
157
|
+
const id = event.update_id ?? event.id;
|
|
158
|
+
if (id != null && id >= this.offset) this.offset = id + 1;
|
|
159
|
+
await this.handleEvent(event);
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (this.running) {
|
|
163
|
+
console.log(chalk.default.yellow(` ⚠ Zalo [${this.accountId}] poll: ${e.message}`));
|
|
164
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async handleWebhook(rawBody, secretHeader) {
|
|
169
|
+
if (!this.verifyWebhookSecret(secretHeader)) {
|
|
170
|
+
console.log(chalk.default.yellow(` ⚠ Zalo [${this.accountId}]: invalid webhook secret`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let event;
|
|
174
|
+
try {
|
|
175
|
+
event = JSON.parse(rawBody);
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await this.handleEvent(event);
|
|
180
|
+
}
|
|
181
|
+
verifyWebhookSecret(headerValue) {
|
|
182
|
+
if (!this.cfg.webhookSecret) return true;
|
|
183
|
+
return headerValue === this.cfg.webhookSecret;
|
|
184
|
+
}
|
|
185
|
+
async handleEvent(event) {
|
|
186
|
+
if (!event) return;
|
|
187
|
+
const eventName = event.event_name || event.type || "";
|
|
188
|
+
const messageId = String(event.message?.msg_id || event.message_id || event.id || "");
|
|
189
|
+
if (messageId) {
|
|
190
|
+
const key = `${eventName}:${messageId}`;
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (this.dedupMap.has(key)) return;
|
|
193
|
+
this.dedupMap.set(key, now + DEDUP_TTL_MS);
|
|
194
|
+
if (this.dedupMap.size > 1e3) {
|
|
195
|
+
for (const [k, exp] of this.dedupMap) if (exp < now) this.dedupMap.delete(k);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const sender = event.sender?.id || event.from?.id || event.user_id || "";
|
|
199
|
+
const groupId = event.message?.group_id || event.group_id || "";
|
|
200
|
+
const isGroup = !!groupId;
|
|
201
|
+
let text = "";
|
|
202
|
+
switch (eventName) {
|
|
203
|
+
case "user_send_text":
|
|
204
|
+
case "message":
|
|
205
|
+
text = event.message?.text || event.text || "";
|
|
206
|
+
break;
|
|
207
|
+
case "user_send_image":
|
|
208
|
+
case "message_image": {
|
|
209
|
+
const url = event.message?.attachments?.[0]?.payload?.url || event.image_url || "";
|
|
210
|
+
const size = event.message?.attachments?.[0]?.payload?.filesize || 0;
|
|
211
|
+
const maxBytes = this.mediaMaxMb * 1024 * 1024;
|
|
212
|
+
if (size > maxBytes) return;
|
|
213
|
+
text = url ? `[image:${url}]` : "[image]";
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "user_send_sticker":
|
|
217
|
+
case "message_sticker": return;
|
|
218
|
+
default: return;
|
|
219
|
+
}
|
|
220
|
+
if (!text || !sender) return;
|
|
221
|
+
if (isGroup) {
|
|
222
|
+
if (!this.checkGroupPolicy(groupId, sender)) return;
|
|
223
|
+
this.emit("message", {
|
|
224
|
+
channelId: "zalo",
|
|
225
|
+
accountId: this.accountId,
|
|
226
|
+
chatId: groupId,
|
|
227
|
+
from: sender,
|
|
228
|
+
text,
|
|
229
|
+
isDM: false
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
const allowed = await this.checkDMPolicy(sender, text);
|
|
233
|
+
if (!allowed) return;
|
|
234
|
+
this.emit("message", {
|
|
235
|
+
channelId: "zalo",
|
|
236
|
+
accountId: this.accountId,
|
|
237
|
+
chatId: sender,
|
|
238
|
+
from: sender,
|
|
239
|
+
text,
|
|
240
|
+
isDM: true
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async checkDMPolicy(from, text) {
|
|
245
|
+
const policy = this.cfg.dmPolicy ?? "pairing";
|
|
246
|
+
const allowFrom = this.cfg.allowFrom ?? [];
|
|
247
|
+
this.pruneExpiredCodes();
|
|
248
|
+
switch (policy) {
|
|
249
|
+
case "disabled": return false;
|
|
250
|
+
case "open": return true;
|
|
251
|
+
case "allowlist":
|
|
252
|
+
if (allowFrom.includes(from) || allowFrom.includes("*")) return true;
|
|
253
|
+
await this.sendText(from, "HyperClaw: Not on allowlist.");
|
|
254
|
+
return false;
|
|
255
|
+
case "pairing": {
|
|
256
|
+
if (this.cfg.approvedPairings.includes(from)) return true;
|
|
257
|
+
const upper = text.trim().toUpperCase().match(/[A-Z0-9]{6}/)?.[0];
|
|
258
|
+
if (upper && this.cfg.pendingPairings[upper]) {
|
|
259
|
+
this.cfg.approvedPairings.push(from);
|
|
260
|
+
delete this.cfg.pendingPairings[upper];
|
|
261
|
+
delete this.cfg.pendingPairingTs[upper];
|
|
262
|
+
await this.saveState();
|
|
263
|
+
await this.sendText(from, "Paired!");
|
|
264
|
+
this.emit("pairing:approved", {
|
|
265
|
+
userId: from,
|
|
266
|
+
channelId: "zalo",
|
|
267
|
+
accountId: this.accountId
|
|
268
|
+
});
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const code = Array.from({ length: 6 }, () => "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"[Math.floor(Math.random() * 32)]).join("");
|
|
272
|
+
this.cfg.pendingPairings[code] = from;
|
|
273
|
+
this.cfg.pendingPairingTs[code] = Date.now();
|
|
274
|
+
await this.saveState();
|
|
275
|
+
await this.sendText(from, `Pairing code: ${code}\nApprove: hyperclaw pairing approve zalo ${code}\n(expires in 1 hour)`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
pruneExpiredCodes() {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
const ts = this.cfg.pendingPairingTs;
|
|
284
|
+
for (const code of Object.keys(ts)) if (now - ts[code] > PAIRING_TTL_MS) {
|
|
285
|
+
delete this.cfg.pendingPairings[code];
|
|
286
|
+
delete ts[code];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
checkGroupPolicy(groupId, userId) {
|
|
290
|
+
const policy = this.cfg.groupPolicy ?? "allowlist";
|
|
291
|
+
if (policy === "disabled") return false;
|
|
292
|
+
if (policy === "open") {
|
|
293
|
+
const list$1 = this.cfg.groupAllowFrom ?? this.cfg.allowFrom ?? [];
|
|
294
|
+
return list$1.length === 0 || list$1.includes(userId);
|
|
295
|
+
}
|
|
296
|
+
const list = this.cfg.groupAllowFrom?.length ? this.cfg.groupAllowFrom : this.cfg.allowFrom ?? [];
|
|
297
|
+
return list.includes(groupId) || list.includes(userId);
|
|
298
|
+
}
|
|
299
|
+
async sendMessage(chatId, text) {
|
|
300
|
+
const chunks = [];
|
|
301
|
+
for (let i = 0; i < text.length; i += TEXT_CHUNK) chunks.push(text.slice(i, i + TEXT_CHUNK));
|
|
302
|
+
for (const chunk of chunks) await this.sendText(chatId, chunk);
|
|
303
|
+
}
|
|
304
|
+
async sendText(to, text) {
|
|
305
|
+
await zaloApi(this.token, "POST", "/message/sendtext", {
|
|
306
|
+
to,
|
|
307
|
+
text
|
|
308
|
+
}).catch((e) => console.error(`[zalo:${this.accountId}] sendText: ${e.message}`));
|
|
309
|
+
}
|
|
310
|
+
async sendPhoto(to, imageUrl, caption) {
|
|
311
|
+
await zaloApi(this.token, "POST", "/message/sendimage", {
|
|
312
|
+
to,
|
|
313
|
+
image_url: imageUrl,
|
|
314
|
+
...caption ? { caption } : {}
|
|
315
|
+
}).catch((e) => console.error(`[zalo:${this.accountId}] sendPhoto: ${e.message}`));
|
|
316
|
+
}
|
|
317
|
+
approvePairing(code) {
|
|
318
|
+
const upper = code.toUpperCase();
|
|
319
|
+
if (!this.cfg.pendingPairings[upper]) return false;
|
|
320
|
+
this.cfg.approvedPairings.push(this.cfg.pendingPairings[upper]);
|
|
321
|
+
delete this.cfg.pendingPairings[upper];
|
|
322
|
+
delete this.cfg.pendingPairingTs[upper];
|
|
323
|
+
this.saveState();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
stateFile() {
|
|
327
|
+
return path.default.join(STATE_BASE, `zalo-bot-state-${this.accountId}.json`);
|
|
328
|
+
}
|
|
329
|
+
async loadState() {
|
|
330
|
+
try {
|
|
331
|
+
const s = await fs_extra.default.readJson(this.stateFile());
|
|
332
|
+
this.offset = s.offset || 0;
|
|
333
|
+
if (s.p) this.cfg.pendingPairings = s.p;
|
|
334
|
+
if (s.a) this.cfg.approvedPairings = s.a;
|
|
335
|
+
if (s.pts) this.cfg.pendingPairingTs = s.pts;
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
async saveState() {
|
|
339
|
+
await fs_extra.default.ensureDir(STATE_BASE);
|
|
340
|
+
await fs_extra.default.writeJson(this.stateFile(), {
|
|
341
|
+
offset: this.offset,
|
|
342
|
+
p: this.cfg.pendingPairings,
|
|
343
|
+
a: this.cfg.approvedPairings,
|
|
344
|
+
pts: this.cfg.pendingPairingTs
|
|
345
|
+
}, { spaces: 2 });
|
|
346
|
+
}
|
|
347
|
+
isRunning() {
|
|
348
|
+
return this.running;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var ZaloConnector = class extends events.EventEmitter {
|
|
352
|
+
config;
|
|
353
|
+
accounts = [];
|
|
354
|
+
constructor(config) {
|
|
355
|
+
super();
|
|
356
|
+
this.config = config;
|
|
357
|
+
}
|
|
358
|
+
async connect() {
|
|
359
|
+
const sharedState = {
|
|
360
|
+
approvedPairings: this.config.approvedPairings ?? [],
|
|
361
|
+
pendingPairings: this.config.pendingPairings ?? {},
|
|
362
|
+
pendingPairingTs: this.config.pendingPairingTs ?? {}
|
|
363
|
+
};
|
|
364
|
+
const accountEntries = Object.entries(this.config.accounts || {});
|
|
365
|
+
if (accountEntries.length === 0) {
|
|
366
|
+
const acct = new ZaloAccount("default", {
|
|
367
|
+
...this.config,
|
|
368
|
+
...sharedState
|
|
369
|
+
});
|
|
370
|
+
this.wire(acct);
|
|
371
|
+
await acct.connect();
|
|
372
|
+
this.accounts.push(acct);
|
|
373
|
+
} else for (const [id, acctCfg] of accountEntries) {
|
|
374
|
+
if (acctCfg.enabled === false) continue;
|
|
375
|
+
const merged = {
|
|
376
|
+
...this.config,
|
|
377
|
+
...acctCfg,
|
|
378
|
+
mediaMaxMb: this.config.mediaMaxMb,
|
|
379
|
+
...sharedState
|
|
380
|
+
};
|
|
381
|
+
if (!merged.botToken && !merged.tokenFile) {
|
|
382
|
+
console.error(`[zalo] Account "${id}" has no botToken or tokenFile — skipping`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const acct = new ZaloAccount(id, merged);
|
|
386
|
+
this.wire(acct);
|
|
387
|
+
try {
|
|
388
|
+
await acct.connect();
|
|
389
|
+
this.accounts.push(acct);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
console.error(`[zalo] Account "${id}" failed: ${e.message}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
wire(acct) {
|
|
396
|
+
acct.on("message", (msg) => this.emit("message", msg));
|
|
397
|
+
acct.on("connected", (info) => this.emit("connected", info));
|
|
398
|
+
acct.on("pairing:approved", (info) => this.emit("pairing:approved", info));
|
|
399
|
+
}
|
|
400
|
+
async sendMessage(chatId, text) {
|
|
401
|
+
const acct = this.accounts[0];
|
|
402
|
+
if (!acct) throw new Error("Zalo: no connected account");
|
|
403
|
+
await acct.sendMessage(chatId, text);
|
|
404
|
+
}
|
|
405
|
+
/** Called by gateway webhook handler for webhook mode accounts. */
|
|
406
|
+
async handleWebhook(rawBody, secretHeader) {
|
|
407
|
+
for (const acct of this.accounts) if (acct.verifyWebhookSecret(secretHeader)) {
|
|
408
|
+
await acct.handleWebhook(rawBody, secretHeader);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
approvePairing(code) {
|
|
413
|
+
return this.accounts.some((a) => a.approvePairing(code));
|
|
414
|
+
}
|
|
415
|
+
disconnect() {
|
|
416
|
+
for (const a of this.accounts) a.disconnect();
|
|
417
|
+
this.accounts = [];
|
|
418
|
+
}
|
|
419
|
+
isRunning() {
|
|
420
|
+
return this.accounts.some((a) => a.isRunning());
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
//#endregion
|
|
425
|
+
exports.ZaloConnector = ZaloConnector;
|