Download character.ai chat

Downloads the current character.ai chat as a text file. Right click on page to use.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Download character.ai chat
// @namespace    https://github.com/wiger3/downloadchat/
// @version      2.5
// @author       wiger3
// @description  Downloads the current character.ai chat as a text file. Right click on page to use.
// @match        https://old.character.ai/chat2*
// @match        https://character.ai/chat/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=character.ai
// @run-at       context-menu
// ==/UserScript==

/* Changelog
    2.0 - First version, the chat gets printed into the browser console
    2.1 - Replaced the console logging with actual downloading. The chat will now download as "<character name> <date>.txt"
    2.2 - Fixed an issue with downloading longer chats
    2.3 - Added a dialog to select how to format the downloaded file. First public release
    2.4 - Added support for new character.ai website
    2.5 - Fixed an issue with downloading empty messages. Now will download as empty instead of "undefined".
*/

(async ()=>{
    var cai_version = -1;
    if(location.hostname == "old.character.ai")
        cai_version = 1;
    else if(location.pathname.startsWith("/chat/"))
        cai_version = 2;
    else
        return alert("Unsupported character.ai version");

    var token;
    if(cai_version == 1)
        token = JSON.parse(localStorage['char_token']).value;
    else if(cai_version == 2)
        token = JSON.parse(document.getElementById("__NEXT_DATA__").innerHTML).props.pageProps.token;

    var _cache; // avoid double request
    async function _fetchchats(charid) {
        if(!_cache) {
            let url = 'https://neo.character.ai/chats/recent/' + charid;

            let response = await fetch(url, {
                headers: { "Authorization": `Token ${token}` }
            });
            let json = await response.json();
            _cache = json['chats'];
        }
        return _cache;
    }
    async function getChats(charid) {
        let json = await _fetchchats(charid);
        let chats = [];
        for(let x of json) chats.push(x.chat_id);
        return chats;
    }
    async function getMessages(chat, format) {
        let url = 'https://neo.character.ai/turns/' + chat + '/';
        let next_token = null;

        let turns = [];
        do {
            let url2 = url;
            if(next_token != null)
                url2 += "?next_token=" + encodeURIComponent(next_token);
            let response = await fetch(url2, {
                headers: { "Authorization": `Token ${token}` }
            });
            let json = await response.json();

            for(let turn of json['turns']) {
                let o = {};
                if(format == "definition")
                    o.author = turn.author.is_human ? "{{user}}" : "{{char}}";
                else if(format == "names")
                    o.author = turn.author.name;
                o.message = turn.candidates.find(x => x.candidate_id === turn.primary_candidate_id).raw_content;
                if (o.message == undefined)
                    o.message = "";
                turns.push(o);
            }
            next_token = json['meta']['next_token'];
        } while(next_token != null);
        return turns.reverse();
    }
    async function getCharacterName(charid) {
        let json = await _fetchchats(charid);
        return json[0].character_name;
    }
    async function saveChat(e) {
        let format = e.formData.get('format');
        dialog.close();
        let char;
        if(cai_version == 1)
            char = params('char');
        else if(cai_version == 2)
            char = location.pathname.split("/")[2];
        let history = params('hist');
        if(history === null) {
            let chats = await getChats(char);
            history = chats[0];
        }
        let msgs = await getMessages(history, format);
        let str = "";
        for(let msg of msgs) {
            str += `${msg.author}: ${msg.message}\n`;
        }
        let date = new Date();
        let date_str = `${date.getDate()}-${date.getMonth()+1}-${date.getFullYear()} ${date.getHours()}.${date.getMinutes()}`;
        download(`${await getCharacterName(char)} ${date_str}.txt`, str.trimEnd());
    }
    
    var dialog = open("", "caiDownloader", "popup");
    if(!dialog)
        return alert("Failed to open downloader dialog. Check browser pop-up settings?");
    dialog.resizeTo(600, 600);
    let ddocument = dialog.document;
    ddocument.body.style.backgroundColor = "white";
    ddocument.body.style.fontFamily = "sans-serif";
    let el, label;
    el = ddocument.createElement("h2");
    el.appendChild(ddocument.createTextNode("Please select downloader format"));
    ddocument.body.appendChild(el);
    let form = ddocument.createElement("form");
        el = ddocument.createElement("input"); // like definition
            el.type = "radio";
            el.name = "format";
            el.id = "definition";
            el.value = el.id;
            el.checked = true;
            form.appendChild(el);
            label = ddocument.createElement("label");
                label.htmlFor = "definition";
                label.innerHTML = "Like definition";
                el = ddocument.createElement("div");
                    el.style.paddingLeft = "2em";
                    el.style.color = "gray";
                    el.innerHTML = "{{char}}: Hello! I am the bot!<br>{{user}}: Hi. I'm the person chatting with the bot.";
                    label.appendChild(el);
            form.appendChild(label);
        el = ddocument.createElement("input"); // using names
            el.type = "radio";
            el.name = "format";
            el.id = "names";
            el.value = el.id;
            form.appendChild(el);
            label = ddocument.createElement("label");
                label.htmlFor = "names";
                label.innerHTML = "Using names";
                el = ddocument.createElement("div");
                    el.style.paddingLeft = "2em";
                    el.style.color = "gray";
                    el.innerHTML = "Chatty AI: Hello! I am the bot!<br>You: Hi. I'm the person chatting with the bot.";
                    label.appendChild(el);
            form.appendChild(label);
        el = document.createElement("button"); // submit
            el.innerHTML = "Download";
            el.style.float = "right";
            el.style.backgroundColor = "cornflowerblue";
            el.style.borderRadius = "1em";
            el.style.padding = "0.5em";
            el.style.margin = "2em";
            form.appendChild(el);
        form.onformdata = saveChat;
    ddocument.body.appendChild(form);

    function download(filename, text) {
        var element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        element.setAttribute('download', filename);
        element.style.display = 'none';
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
    }
    function params(parameterName) {
        var result = null,
            tmp = [];
        location.search
            .substr(1)
            .split("&")
            .forEach(function (item) {
              tmp = item.split("=");
              if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
            });
        return result;
    }
})();