apix/lib/providers/meta-client.ts
Khoa.vo 8741e3b89f
Some checks are pending
CI / build (18.x) (push) Waiting to run
CI / build (20.x) (push) Waiting to run
feat: Initial commit with multi-provider image generation
2026-01-05 13:50:35 +07:00

372 lines
13 KiB
TypeScript

/**
* Meta AI Client for Image Generation
*
* Uses the Meta AI web interface via GraphQL
* Requires cookies from a logged-in meta.ai session
*
* Image Model: Imagine (Emu)
*
* Based on: https://github.com/Strvm/meta-ai-api
*/
const META_AI_BASE = "https://www.meta.ai";
const GRAPHQL_ENDPOINT = `${META_AI_BASE}/api/graphql/`;
interface MetaAIOptions {
cookies: string;
}
interface MetaImageResult {
url: string;
data?: string; // base64
prompt: string;
model: string;
}
interface MetaSession {
lsd?: string;
fb_dtsg?: string;
accessToken?: string;
}
export class MetaAIClient {
private cookies: string;
private session: MetaSession = {};
constructor(options: MetaAIOptions) {
this.cookies = this.normalizeCookies(options.cookies);
this.parseSessionFromCookies();
}
/**
* Normalize cookies from string or JSON format
* Handles cases where user pastes JSON array from extension/devtools
*/
private normalizeCookies(cookies: string): string {
if (!cookies) return "";
try {
// Check if it looks like JSON
if (cookies.trim().startsWith('[')) {
const parsed = JSON.parse(cookies);
if (Array.isArray(parsed)) {
return parsed
.map((c: any) => `${c.name}=${c.value}`)
.join('; ');
}
}
} catch (e) {
// Not JSON, assume string
}
return cookies;
}
/**
* Parse session tokens from cookies
*/
private parseSessionFromCookies(): void {
// Extract lsd token if present in cookies
const lsdMatch = this.cookies.match(/lsd=([^;]+)/);
if (lsdMatch) {
this.session.lsd = lsdMatch[1];
}
// Extract fb_dtsg if present
const dtsgMatch = this.cookies.match(/fb_dtsg=([^;]+)/);
if (dtsgMatch) {
this.session.fb_dtsg = dtsgMatch[1];
}
}
/**
* Generate images using Meta AI's Imagine model
*/
async generate(prompt: string, numImages: number = 4): Promise<MetaImageResult[]> {
console.log(`[Meta AI] Generating images for: "${prompt.substring(0, 50)}..."`);
// First, get the access token and session info if not already fetched
if (!this.session.accessToken) {
await this.initSession();
}
// Use "Imagine" prefix for image generation
const imagePrompt = prompt.toLowerCase().startsWith('imagine')
? prompt
: `Imagine ${prompt}`;
// Send the prompt via GraphQL
const response = await this.sendPrompt(imagePrompt);
// Extract image URLs from response
const images = this.extractImages(response, prompt);
if (images.length === 0) {
// Meta AI might return the prompt response without images immediately
// We may need to poll for the result
console.log("[Meta AI] No images in initial response, polling...");
return this.pollForImages(response, prompt);
}
return images;
}
/**
* Initialize session - get access token from meta.ai page
*/
private async initSession(): Promise<void> {
console.log("[Meta AI] Initializing session...");
const response = await fetch(META_AI_BASE, {
headers: {
"Cookie": this.cookies,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
"Accept-Language": "en-US,en;q=0.9"
}
});
const html = await response.text();
// Extract access token from page HTML
// Pattern 1: Simple JSON key
let tokenMatch = html.match(/"accessToken":"([^"]+)"/);
// Pattern 2: Inside a config object or different spacing
if (!tokenMatch) {
tokenMatch = html.match(/accessToken["']\s*:\s*["']([^"']+)["']/);
}
// Pattern 3: LSD token backup (sometimes needed)
const lsdMatch = html.match(/"LSD",\[\],{"token":"([^"]+)"/) ||
html.match(/"lsd":"([^"]+)"/) ||
html.match(/name="lsd" value="([^"]+)"/);
if (lsdMatch) {
this.session.lsd = lsdMatch[1];
}
// Pattern 4: DTSG token (critical for some requests)
const dtsgMatch = html.match(/"DTSGInitialData".*?"token":"([^"]+)"/) ||
html.match(/"token":"([^"]+)"/); // Less specific fallback
if (tokenMatch) {
this.session.accessToken = tokenMatch[1];
console.log("[Meta AI] Got access token");
} else if (html.includes('login_form') || html.includes('login_page')) {
throw new Error("Meta AI: Cookies expired or invalid (Login page detected). Please update cookies.");
} else {
console.warn("[Meta AI] Warning: Failed to extract access token. functionality may be limited.");
console.log("HTML Preview:", html.substring(0, 200));
// Don't throw here, try to proceed with just cookies/LSD
}
if (dtsgMatch) {
this.session.fb_dtsg = dtsgMatch[1];
}
// We no longer strictly enforce accessToken presence here
// as some requests might work with just cookies
}
/**
* Send prompt via GraphQL mutation
*/
private async sendPrompt(prompt: string): Promise<any> {
const variables = {
message: {
text: prompt,
content_type: "TEXT"
},
source: "PDT_CHAT_INPUT",
external_message_id: Math.random().toString(36).substring(2) + Date.now().toString(36)
};
const body = new URLSearchParams({
fb_api_caller_class: "RelayModern",
fb_api_req_friendly_name: "useAbraSendMessageMutation",
variables: JSON.stringify(variables),
doc_id: "7783822248314888",
...(this.session.lsd && { lsd: this.session.lsd }),
...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg })
});
const response = await fetch(GRAPHQL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": this.cookies,
"Origin": META_AI_BASE,
"Referer": `${META_AI_BASE}/`,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Accept-Language": "en-US,en;q=0.9",
...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` })
},
body: body.toString()
});
if (!response.ok) {
const errorText = await response.text();
console.error("[Meta AI] GraphQL Error:", response.status, errorText);
throw new Error(`Meta AI Error: ${response.status} - ${errorText.substring(0, 200)}`);
}
// Check if response is actually JSON
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("text/html")) {
const text = await response.text();
if (text.includes("login_form") || text.includes("facebook.com/login")) {
throw new Error("Meta AI: Session expired. Please refresh your cookies.");
}
throw new Error(`Meta AI returned HTML error: ${text.substring(0, 100)}...`);
}
const data = await response.json();
console.log("[Meta AI] Response:", JSON.stringify(data, null, 2).substring(0, 500));
return data;
}
/**
* Extract image URLs from Meta AI response
*/
private extractImages(response: any, originalPrompt: string): MetaImageResult[] {
const images: MetaImageResult[] = [];
// Navigate through the response structure
const messageData = response?.data?.node?.bot_response_message ||
response?.data?.xabraAIPreviewMessageSendMutation?.message;
if (!messageData) {
return images;
}
// Check for imagine_card (image generation response)
const imagineCard = messageData?.imagine_card;
if (imagineCard?.session?.media_sets) {
for (const mediaSet of imagineCard.session.media_sets) {
if (mediaSet?.imagine_media) {
for (const media of mediaSet.imagine_media) {
if (media?.uri) {
images.push({
url: media.uri,
prompt: originalPrompt,
model: "imagine"
});
}
}
}
}
}
// Check for attachments
const attachments = messageData?.attachments;
if (attachments) {
for (const attachment of attachments) {
if (attachment?.media?.image_uri) {
images.push({
url: attachment.media.image_uri,
prompt: originalPrompt,
model: "imagine"
});
}
}
}
return images;
}
/**
* Poll for image generation completion
*/
private async pollForImages(initialResponse: any, prompt: string): Promise<MetaImageResult[]> {
const maxAttempts = 30;
const pollInterval = 2000;
// Get the fetch_id from initial response for polling
const fetchId = initialResponse?.data?.node?.id ||
initialResponse?.data?.xabraAIPreviewMessageSendMutation?.message?.id;
if (!fetchId) {
console.warn("[Meta AI] No fetch ID for polling, returning empty");
return [];
}
for (let attempt = 0; attempt < maxAttempts; attempt++) {
console.log(`[Meta AI] Polling attempt ${attempt + 1}/${maxAttempts}...`);
await new Promise(resolve => setTimeout(resolve, pollInterval));
try {
// Query for the message status
const statusResponse = await this.queryMessageStatus(fetchId);
const images = this.extractImages(statusResponse, prompt);
if (images.length > 0) {
console.log(`[Meta AI] Got ${images.length} images!`);
return images;
}
// Check if generation failed
const status = statusResponse?.data?.node?.imagine_card?.session?.status;
if (status === "FAILED" || status === "ERROR") {
throw new Error("Meta AI image generation failed");
}
} catch (e) {
console.error("[Meta AI] Poll error:", e);
if (attempt === maxAttempts - 1) throw e;
}
}
throw new Error("Meta AI: Image generation timed out");
}
/**
* Query message status for polling
*/
private async queryMessageStatus(messageId: string): Promise<any> {
const variables = {
id: messageId
};
const body = new URLSearchParams({
fb_api_caller_class: "RelayModern",
fb_api_req_friendly_name: "useAbraMessageQuery",
variables: JSON.stringify(variables),
doc_id: "7654946557897648",
...(this.session.lsd && { lsd: this.session.lsd }),
...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg })
});
const response = await fetch(GRAPHQL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": this.cookies,
"Origin": META_AI_BASE,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` })
},
body: body.toString()
});
return response.json();
}
/**
* Download image from URL and convert to base64
*/
async downloadAsBase64(url: string): Promise<string> {
const response = await fetch(url, {
headers: {
"Cookie": this.cookies,
"Referer": META_AI_BASE
}
});
const buffer = await response.arrayBuffer();
return Buffer.from(buffer).toString('base64');
}
}