const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); const fs = require('fs');

let chromiumPath = process.platform === 'linux' ? '/usr/bin/chromium-browser' : null; if (chromiumPath && !fs.existsSync(chromiumPath)) console.log('[node_characterai] Puppeteer - Warning: the specified Chromium path for puppeteer could not be located. If the script does not work properly, you may need to specify a path to the Chromium binary file/executable.');

class Requester { browser = undefined; page = undefined;

#initialized = false;
#hasDisplayed = false;

#headless = 'new';
puppeteerPath = undefined;
puppeteerLaunchArgs = [
    '--fast-start',
    '--disable-extensions',
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--no-gpu',
    '--disable-background-timer-throttling',
    '--disable-renderer-backgrounding',
    '--override-plugin-power-saver-for-testing=never',
    '--disable-extensions-http-throttling',
    '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.3'
];
puppeteerNoDefaultTimeout = false;
puppeteerProtocolTimeout = 0;
usePlus = false;
forceWaitingRoom = false;

constructor() {}
isInitialized() {
    return this.#initialized;
}
async waitForWaitingRoom(page) {
    // Enable force waiting room to ensure you check for waiting room even on C.AI+
    if (!this.usePlus || (this.usePlus && this.forceWaitingRoom)) {
        return new Promise(async (resolve) => {
            try {
                let interval;
                let pass = true;

                const minute = 60000; // Update every minute

                // Keep waiting until false
                async function check() {
                    if (pass) {
                        pass = false;

                        const waitingRoomTimeLeft = await page.evaluate(async() => {
                            try {
                                const contentContainer = document.querySelector('.content-container');
                                const sections = contentContainer.querySelectorAll('section');
                                const h2Element = sections[1].querySelector('h2');
                                const h2Text = h2Element.innerText;
                                const regex = /\d+/g;
                                const matches = h2Text.match(regex);

                                if (matches) return matches[0];
                            } catch (error) {
                                return;
                            }
                        }, minute);

                        const waiting = (waitingRoomTimeLeft != null);
                        if (waiting) {
                            console.log(`[node_characterai] Puppeteer - Currently in cloudflare's waiting room. Time left: ${waitingRoomTimeLeft}`);
                        } else {
                            resolve();
                            clearInterval(interval);
                        }
                        pass = true;
                    };
                }

                interval = setInterval(check, minute);
                await check();
            } catch (error) {
                console.log('[node_characterai] Puppeteer - There was a fatal error while checking for cloudflare's waiting room');
                console.log(error);
            }
        });
    }
}
async initialize() {
    if (!this.isInitialized());

    // Handle chromium tabs cleanup
    process.on('exit', () => {
        this.uninitialize();
    });

    console.log('[node_characterai] Puppeteer - This is an experimental feature. Please report any issues on github.');

    puppeteer.use(StealthPlugin());
    const browser = await puppeteer.launch({
        headless: this.#headless,
        args: this.puppeteerLaunchArgs,
        protocolTimeout: this.puppeteerProtocolTimeout || 0, // Props to monckey100
        executablePath: this.puppeteerPath || null
    });
    this.browser = browser;

    let page = await browser.pages();
    page = page[0];
    this.page = page;

    await page.setRequestInterception(false);

    page.setViewport({
        width: 1920 + Math.floor(Math.random() * 100),
        height: 3000 + Math.floor(Math.random() * 100),
        deviceScaleFactor: 1,
        hasTouch: false,
        isLandscape: false,
        isMobile: false,
    });
    await page.setJavaScriptEnabled(true);
    await page.setDefaultNavigationTimeout(0);

    const userAgent = 'CharacterAI/1.0.0 (iPhone; iOS 14.4.2; Scale/3.00)';
    await page.setUserAgent(userAgent);

    // Special thanks to @Parking-Master for this fix
    await page.deleteCookie();
    const client = await page.target().createCDPSession();
    await client.send('Network.clearBrowserCookies');
    await client.send('Network.clearBrowserCache');
    await page.goto('https://' + (this.usePlus ? 'plus' : 'beta') + '.character.ai');
    await page.evaluate(() => localStorage.clear());

    // If there is no waiting room, the script will continue anyway
    await this.waitForWaitingRoom(page);

    console.log('[node_characterai] Puppeteer - Done with setup');
}

async request(url, options) {
    const page = this.page;

    const method = options.method;

    const body = (method == 'GET' ? {} : options.body);
    const headers = options.headers;

    let response;

    try {
        const payload = {
            method: method,
            headers: headers,
            body: body
        };

        await page.setRequestInterception(false);
        if (!this.#hasDisplayed) {
            console.log('[node_characterai] Puppeteer - Eval-fetching is an experimental feature and may be slower. Please report any issues on github')
            this.#hasDisplayed = true;
        }

        if (url.endsWith('/streaming/')) {
            // Bless @roogue & @drizzle-mizzle for the code here!
            response = await page.evaluate(async (payload, url) => {
                const response = await fetch(url, payload);

                const data = await response.text();
                const matches = data.match(/\{.*\}/g);
                const responseText = matches[matches.length - 1];

                let result = {
                    code: 500
                };

                if (!matches) result = null;
                else {
                    result.code = 200;
                    result.response = responseText;
                };
                return result;
            }, payload, url);

            response.status = () => response.code; // compatibilty reasons
            response.text = () => response.response; // compatibilty reasons
        } else {
            await page.setRequestInterception(true);
            let initialRequest = true;

            page.once('request', request => {
                var data = {
                    'method': method,
                    'postData': body,
                    'headers': headers
                };

                if (request.isNavigationRequest() && !initialRequest) {
                    return request.abort();
                }

                try {
                    initialRequest = false;
                    request.continue(data);
                } catch (error) {
                    console.log('[node_characterai] Puppeteer - Non fatal error: ' + error);
                }
            });
            response = await page.goto(url, { waitUntil: 'domcontentloaded' });
        }
    } catch (error) {
        console.log('[node_characterai] Puppeteer - ' + error)
    }

    return response;
}

async imageUpload(url, headers, localFile = false) {
    const page = this.page;

    let response;

    try {
        await page.setRequestInterception(false);
        if (!this.#hasDisplayed) {
            console.log('[node_characterai] Puppeteer - Eval-fetching is an experimental feature and may be slower. Please report any issues on github')
            this.#hasDisplayed = true;
        }

        // The end of this repeated 2 times, is it intentional?
        // Can it be in the same spot at the end to avoid code repetition?
        if (localFile) {
            let dataUrl = fs.readFileSync(url, 'base64');
            response = await page.evaluate(
                async (uploadHeaders, dataUrl) => {
                        var result = {
                            code: 500
                        };

                        const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
                            const byteCharacters = atob(b64Data); // <- Watch out, this is deprecated!
                            const byteArrays = [];

                            for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                                const slice = byteCharacters.slice(offset, offset + sliceSize);

                                const byteNumbers = new Array(slice.length);
                                for (let i = 0; i < slice.length; i++) {
                                    byteNumbers[i] = slice.charCodeAt(i);
                                }

                                const byteArray = new Uint8Array(byteNumbers);
                                byteArrays.push(byteArray);
                            }

                            const blob = new Blob(byteArrays, {
                                type: contentType
                            });
                            return blob;
                        }

                        const blob = b64toBlob(dataUrl.includes('base64,') ? dataUrl.split('base64,')[1] : dataUrl);
                        const file = new File([blob], 'image');
                        const formData = new FormData();
                        formData.append('image', file);

                        let head = uploadHeaders;
                        delete head['Content-Type'];
                        // ^ Is this even being used?

                        const uploadResponse = await fetch('https://beta.character.ai/chat/upload-image/', {
                            headers: uploadHeaders,
                            method: 'POST',
                            body: formData
                        })

                        if (uploadResponse.status == 200) {
                            result.code = 200;

                            let uploadResponseJSON = await uploadResponse.json();
                            result.response = uploadResponseJSON.value;
                        }

                        return result;
                    },
                    headers, dataUrl
            );
        } else {
            response = await page.evaluate(
                async (uploadHeaders, url) => {
                        var result = {
                            code: 500
                        };

                        const uploadResponse = await fetch(url).then(uploadResponse => uploadResponse.blob()).then(async (blob) => {
                            const file = new File([blob], 'image');
                            const formData = new FormData();
                            formData.append('image', file);

                            let head = uploadHeaders;
                            delete head['Content-Type'];
                            // ^ Is this even being used?

                            const uploadResponse = await fetch('https://beta.character.ai/chat/upload-image/', {
                                headers: uploadHeaders,
                                method: 'POST',
                                body: formData
                            })
                            return uploadResponse;
                        }).then()

                        if (uploadResponse.status == 200) {
                            result.code = 200;

                            let uploadResponseJSON = await uploadResponse.json();
                            result.response = uploadResponseJSON.value;
                        }

                        return result;
                    },
                    headers, url
            );
        }

        response.status = () => response.code; // compatibilty reasons
        response.body = () => response.response; // compatibilty reasons
    } catch (error) {
        console.log('[node_characterai] Puppeteer - ' + error)
    }

    return response;
}

async uninitialize() {
    // Handle chromium tabs cleanup
    try {
        this.browser.close();
    } catch {}
}

};

module.exports = Requester;

CharacterAI API Request Library: Puppeteer-based Requester for Interacting with CharacterAI

原文地址: https://www.cveoy.top/t/topic/ml4 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录