项目文件夹

文件
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

514 行
12 KiB
TypeScript

/**
* HTTP client for the WhatsApp Cloud API (Meta Graph). Sends text, media,
* reactions, location, and interactive (button/list) messages, marks messages
* read, resolves media URLs, and verifies the webhook token — one Graph request
* per call against the configured phone number ID. Implements IWhatsAppClient;
* media links pass through the SSRF guard before dispatch.
*/
import { EventEmitter } from "node:events";
import type { IWhatsAppClient } from "./clients/interface";
import { assertValidWhatsAppMediaLink } from "./media";
import type {
CloudAPIConfig,
ConnectionStatus,
SendReactionParams,
SendReactionResult,
WhatsAppInteractiveMessage,
WhatsAppLocationMessage,
WhatsAppMediaMessage,
WhatsAppMessage,
WhatsAppMessageResponse,
WhatsAppReactionMessage,
} from "./types";
const DEFAULT_API_VERSION = "v24.0";
interface WhatsAppApiResponse<T> {
data: T;
status: number;
statusText: string;
headers: Headers;
}
export class WhatsAppClient extends EventEmitter implements IWhatsAppClient {
private baseUrl: string;
private headers: HeadersInit;
private config: CloudAPIConfig;
private connectionStatus: ConnectionStatus = "close";
constructor(config: CloudAPIConfig) {
super();
this.config = config;
const apiVersion = config.apiVersion || DEFAULT_API_VERSION;
this.baseUrl = `https://graph.facebook.com/${apiVersion}`;
this.headers = {
Authorization: `Bearer ${config.accessToken}`,
"Content-Type": "application/json",
};
}
async start(): Promise<void> {
this.connectionStatus = "open";
this.emit("connection", "open");
this.emit("ready");
}
async stop(): Promise<void> {
this.connectionStatus = "close";
this.emit("connection", "close");
}
getConnectionStatus(): ConnectionStatus {
return this.connectionStatus;
}
/**
* Get the configured phone number ID.
*/
getPhoneNumberId(): string {
return this.config.phoneNumberId;
}
/**
* Send a message of any supported type.
*/
async sendMessage(
message: WhatsAppMessage
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
const endpoint = `/${this.config.phoneNumberId}/messages`;
const payload = this.buildMessagePayload(message);
return this.post<WhatsAppMessageResponse>(endpoint, payload);
}
/**
* Send a text message.
*/
async sendTextMessage(
to: string,
text: string,
_previewUrl = false
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "text",
to,
content: text,
});
}
/**
* Send a reaction to a message.
*/
async sendReaction(params: SendReactionParams): Promise<SendReactionResult> {
const endpoint = `/${this.config.phoneNumberId}/messages`;
const payload = {
messaging_product: "whatsapp",
recipient_type: "individual",
to: params.to,
type: "reaction",
reaction: {
message_id: params.messageId,
emoji: params.emoji,
},
};
try {
const response = await this.post<WhatsAppMessageResponse>(endpoint, payload);
return {
success: true,
messageId: response.data.messages[0]?.id,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
error: errorMessage,
};
}
}
/**
* Remove a reaction from a message (send empty emoji).
*/
async removeReaction(to: string, messageId: string): Promise<SendReactionResult> {
return this.sendReaction({
to,
messageId,
emoji: "",
});
}
/**
* Send an image message.
*/
async sendImage(
to: string,
imageUrl: string,
caption?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "image",
to,
content: {
link: imageUrl,
caption,
} as WhatsAppMediaMessage,
});
}
/**
* Send a video message.
*/
async sendVideo(
to: string,
videoUrl: string,
caption?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "video",
to,
content: {
link: videoUrl,
caption,
} as WhatsAppMediaMessage,
});
}
/**
* Send an audio message.
*/
async sendAudio(
to: string,
audioUrl: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "audio",
to,
content: {
link: audioUrl,
} as WhatsAppMediaMessage,
});
}
/**
* Send a document message.
*/
async sendDocument(
to: string,
documentUrl: string,
filename?: string,
caption?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "document",
to,
content: {
link: documentUrl,
filename,
caption,
} as WhatsAppMediaMessage,
});
}
/**
* Send a location message.
*/
async sendLocation(
to: string,
latitude: number,
longitude: number,
name?: string,
address?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
return this.sendMessage({
type: "location",
to,
content: {
latitude,
longitude,
name,
address,
} as WhatsAppLocationMessage,
});
}
/**
* Send an interactive button message.
*/
async sendButtonMessage(
to: string,
bodyText: string,
buttons: Array<{ id: string; title: string }>,
headerText?: string,
footerText?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
const interactive: WhatsAppInteractiveMessage = {
type: "button",
body: { text: bodyText },
action: {
buttons: buttons.map((btn) => ({
type: "reply" as const,
reply: { id: btn.id, title: btn.title },
})),
},
};
if (headerText) {
interactive.header = { type: "text", text: headerText };
}
if (footerText) {
interactive.footer = { text: footerText };
}
return this.sendMessage({
type: "interactive",
to,
content: interactive,
});
}
/**
* Send an interactive list message.
*/
async sendListMessage(
to: string,
bodyText: string,
buttonText: string,
sections: Array<{
title?: string;
rows: Array<{ id: string; title: string; description?: string }>;
}>,
headerText?: string,
footerText?: string
): Promise<WhatsAppApiResponse<WhatsAppMessageResponse>> {
const interactive: WhatsAppInteractiveMessage = {
type: "list",
body: { text: bodyText },
action: {
button: buttonText,
sections,
},
};
if (headerText) {
interactive.header = { type: "text", text: headerText };
}
if (footerText) {
interactive.footer = { text: footerText };
}
return this.sendMessage({
type: "interactive",
to,
content: interactive,
});
}
/**
* Mark a message as read.
*/
async markMessageAsRead(messageId: string): Promise<boolean> {
const endpoint = `/${this.config.phoneNumberId}/messages`;
const payload = {
messaging_product: "whatsapp",
status: "read",
message_id: messageId,
};
try {
await this.post<WhatsAppMessageResponse>(endpoint, payload);
return true;
} catch {
return false;
}
}
/**
* Download media by ID.
*/
async getMediaUrl(mediaId: string): Promise<string | null> {
try {
const response = await this.get<{ url?: string }>(`/${mediaId}`);
return response.data.url || null;
} catch {
return null;
}
}
/**
* Verify webhook token.
*/
async verifyWebhook(token: string): Promise<boolean> {
return token === this.config.webhookVerifyToken;
}
private get<T>(endpoint: string): Promise<WhatsAppApiResponse<T>> {
return this.request<T>(endpoint, { method: "GET" });
}
private post<T>(endpoint: string, payload: unknown): Promise<WhatsAppApiResponse<T>> {
return this.request<T>(endpoint, {
method: "POST",
body: JSON.stringify(payload),
});
}
private async request<T>(endpoint: string, init: RequestInit): Promise<WhatsAppApiResponse<T>> {
const normalizedEndpoint = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
const response = await fetch(`${this.baseUrl}/${normalizedEndpoint}`, {
...init,
headers: {
...this.headers,
...init.headers,
},
});
const text = await response.text();
const data = text ? this.parseResponseBody(text) : undefined;
if (!response.ok) {
const detail =
typeof data === "string" ? data : data ? JSON.stringify(data) : response.statusText;
throw new Error(
`WhatsApp Cloud API request failed (${response.status} ${response.statusText}): ${detail}`
);
}
return {
data: data as T,
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
}
private parseResponseBody(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return text;
}
}
/**
* Build the message payload based on message type.
*/
private buildMessagePayload(message: WhatsAppMessage): Record<string, unknown> {
const basePayload = {
messaging_product: "whatsapp",
recipient_type: "individual",
to: message.to,
type: message.type,
};
// Add context for replies
const contextPayload = message.replyToMessageId
? { context: { message_id: message.replyToMessageId } }
: {};
switch (message.type) {
case "text":
return {
...basePayload,
...contextPayload,
text: {
body: message.content as string,
},
};
case "template":
return {
...basePayload,
...contextPayload,
template: message.content,
};
case "image": {
const imageContent = message.content as WhatsAppMediaMessage;
return {
...basePayload,
...contextPayload,
image: {
link: assertValidWhatsAppMediaLink(imageContent.link, "image"),
caption: imageContent.caption,
},
};
}
case "video": {
const videoContent = message.content as WhatsAppMediaMessage;
return {
...basePayload,
...contextPayload,
video: {
link: assertValidWhatsAppMediaLink(videoContent.link, "video"),
caption: videoContent.caption,
},
};
}
case "audio": {
const audioContent = message.content as WhatsAppMediaMessage;
return {
...basePayload,
...contextPayload,
audio: {
link: assertValidWhatsAppMediaLink(audioContent.link, "audio"),
},
};
}
case "document": {
const docContent = message.content as WhatsAppMediaMessage;
return {
...basePayload,
...contextPayload,
document: {
link: assertValidWhatsAppMediaLink(docContent.link, "document"),
filename: docContent.filename,
caption: docContent.caption,
},
};
}
case "location": {
const locContent = message.content as WhatsAppLocationMessage;
return {
...basePayload,
...contextPayload,
location: {
latitude: locContent.latitude,
longitude: locContent.longitude,
name: locContent.name,
address: locContent.address,
},
};
}
case "reaction": {
const reactionContent = message.content as WhatsAppReactionMessage;
return {
...basePayload,
reaction: {
message_id: reactionContent.messageId,
emoji: reactionContent.emoji,
},
};
}
case "interactive": {
const interactiveContent = message.content as WhatsAppInteractiveMessage;
return {
...basePayload,
...contextPayload,
interactive: interactiveContent,
};
}
default:
return basePayload;
}
}
}