Auto_Award_Muli

Steam自动打赏 — 极速多账户版

// ==UserScript==
// @name         Auto_Award_Muli
// @name:zh-CN   Steam自动打赏【极速多账户版】
// @namespace    https://blog.chrxw.com
// @version      2.2
// @description  Steam自动打赏 — 极速多账户版
// @description:zh-CN  Steam自动打赏 — 极速多账户版
// @author       Chr_
// @include      /^https:\/\/steamcommunity\.com\/id\/[^/]+/?$/
// @include      /^https:\/\/steamcommunity\.com\/profiles\/\d+/?$/
// @connect      steamcommunity.com
// @connect      steampowered.com
// @connect      api.steampowered.com
// @license      AGPL-3.0
// @icon         https://blog.chrxw.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// ==/UserScript==

(() => {
    'use strict';

    // 多语言
    const LANG = {
        'ZH': {
            'done': '完成',
            'changeLang': '修改语言',
            'langName': '中文',
            'operating': '操作进行中……',
            'runningLog': '运行日志',
            'close': '关闭',
            'logWaring': '【日志不会保存, 打赏记录可以在【打赏历史】中查看】',
            'botListTitle': '【机器人账户管理】【ID | 账户名 | SteamID | 点数余额】',
            'addCurrentAccount': '添加当前账号',
            'delSelectAccount': '删除选中账号',
            'reloadAccountPoints': '刷新所有账号点数',
            'historyTitle': '【点数打赏历史记录】【ID | 昵称 | SteamID | 收到点数】',
            'profile': '个人资料',
            'deleteSelectedHistory': '删除选中',
            'clearHistory': '清空历史',
            'reloadHistory': '刷新历史',
            'feedBack': '作者',
            'feedBackTitle': '觉得好用也欢迎给我打赏',
            'notSelected': '---自动使用当前登录账号---',
            'steamID64': 'Steam 64位 ID',
            'awardPoints': '打赏点数(收到)',
            'recommands': '评测',
            'screenshots': '截图',
            'artworks': '艺术作品',
            'setToCurrentUser': '设置为当前用户',
            'calculator': '打赏计算器',
            'save': '保存',
            'reset': '重置',
            'awardHistory': '打赏历史',
            'startAward': '开始打赏',
            'stopAward': '停止打赏',
            'stop': '停止',
            'senderBotAccount': '打赏机器人账户: ',
            'receverAccount': '被打赏人SteamID: ',
            'sendPoints': '打赏点数(收到): ',
            'awardPrefer': '打赏类型(优先级从上到下从左到右): ',
            'botList': '机器人列表',
            'fetchLoginAccount': '获取登陆账户……',
            'fetchToken': '获取Token……',
            'fetchPoints': '获取点数信息……',
            'success': '成功',
            'failure': '失败',
            'error': '错误',
            'confirm': '确认',
            'tips': '提示',
            'addAccountSuccessTips1': '添加账户成功',
            'addAccountSuccessTips2': '当前账户可用点数',
            'deleteAccountConfirmTips': '确定要删除选定的账号吗?',
            'deleteAccountDoneTips1': '删除了',
            'deleteAccountDoneTips2': '个机器人',
            'notSelectedAnyBotsTips': '尚未选中任何机器人!',
            'currentProcess': '当前进度',
            'updateFailed': '更新出错',
            'fetchingAccountPoints': '读取账户点数中……',
            'allDataLoaded': '所有数据刷新完毕',
            'someDataLoadFailed': '部分数据刷新失败, 如果点数显示为【-1】,代表数据刷新失败',
            'botListEmpty': '机器人列表为空',
            'noBotAccountTips': '-- 无机器人账号, 请使用【➕添加当前账号】自动添加 --',
            'awardTaskWasResetTips': '机器人账号已修改, 打赏设置已重置!',
            'noHistoryTips': '-- 无历史记录, 执行打赏任务后会自动记录 --',
            'steamIDisEmpty': '未填入SteamID!',
            'fetchingProfile': '获取个人资料中……',
            'fetchingAwardableItems': '获取可打赏项目……',
            'fetchError': '读取出错',
            'awardableAmount': '可打赏约',
            'nickName': '用户名',
            'totalPoints': '总计点数',
            'calcTips': '根据项目数量计算所得, 不准确',
            'profileNotExistsTips': '个人资料不存在, 请检查SteamID是否正确, 或者使用【🤵设置为当前用户】自动获取。',
            'profileLoadFailedTips': '网络错误, 读取个人资料失败',
            'notSelectedAnyHistoryTips': '未选中历史记录!',
            'clearHistoryConfirmTips': '确定要清除打赏历史记录吗?',
            'clearHistorySuccess': '清除成功',
            'historyListEmpty': '历史记录是空的!',
            'deleteHistoryConfirmTips': '确定要删除选定的打赏历史记录吗?',
            'deleteResultTips1': '删除了',
            'deleteResultTips2': '条打赏历史记录',
            'notSelectedAwardBotsTips': '尚未选择打赏机器人!',
            'steamIDEmptyWithTips': '未填写【被打赏人SteamID】, 建议使用【🤵设置为当前用户】功能!',
            'steamIDErrorWithTips': '【被打赏人SteamID】格式有误, 建议使用【🤵设置为当前用户】功能!',
            'pointsErrorWithTips': '【打赏点数】格式有误, 只能为整数!',
            'awardTypeEmptyTips': '请选择【打赏类型】!',
            'awardReadyToStartTips': '设置保存成功, 可以【✅开始打赏】了',
            'resetConfigConfirmTips': '确定要重置设定吗?',
            'configResetSuccessTips': '设置已清除',
            'awardTaskDataInvalid': '任务数据非法',
            'fetchingTargetProfile': '读取被打赏人个人资料……',
            'awardConfig': '打赏设置',
            'targetNickName': '被打赏人昵称',
            'targetReceivePoints': '预计收到点数',
            'targetBot': '打赏机器人',
            'taskReadyToStartTips': '打赏任务【2秒】后开始, 点击【⛔停止打赏】可以提前终止操作!',
            'taskFailedProfileNotFound': '未找到个人资料, 打赏进程停止!',
            'taskAlreadyStartTips': '打赏任务已经开始了!',
            'taskEndManually': '打赏任务手动终止, 点击【❌关闭】可以关闭面板.',
            'taskNotStart': '打赏任务未开始!',
            'running': '运行',
            'taskStartPointsSummary': '开始打赏, 剩余打赏 / 预计打赏',
            'fetchAwardItemFailedRetry': '获取打赏项目失败, 重试……',
            'fetchNoAwardItemSkip': '没有合适的打赏, 跳过……',
            'beforeSendAward': '将要打赏',
            'itemAndTotal': '项, 总计',
            'points': '点',
            'sendingAwards': '发送打赏中……',
            'fetchSuccessAndFailed': '请求成功 / 请求失败',
            'fetchAwardItemFailedRetryIn2Min': '获取打赏项目失败, 【2秒】后重试……',
            'awardSuccess': '成功打赏',
            'taskFinishedPointsSummary': '打赏完成, 剩余打赏 / 预计打赏',
            'updateBotPointsBalance': '更新机器人点数余额……',
            'bot': '机器人',
            'pointsBalanceUpdateSuccess': '点数余额更新成功, 可用点数',
            'lackOfPointsTaskEnd': '点数余额不足, 终止操作',
            'pointBalanceUpdateFailed': '点数余额更新失败',
            'fetchAwardItemFailedSkip': '获取打赏项目失败, 跳过……',
            'taskEndListEmpty': '列表为空, 结束',
            'fetchCompletedTotal': '获取成功, 共',
            'entries': '个',
            'objectID': '项目ID',
            'noAwardableObjectSkip': '没有合适的打赏, 跳过',
            'willAward': '将要打赏',
            'requestsSummary': '请求成功 / 请求失败',
            'wait2Seconds': '*等待2秒,防止打赏过多*',
            'botDataError': '机器人数据错误, 无法开始打赏!',
            'awardTaskFinish': '✅打赏任务完成, 点击【❌关闭】可以关闭面板。',
            'awardTaskNotFinish': '⛔打赏任务未完成, 点击【❌关闭】可以关闭面板。',
            'cancel': '取消',
            'steamStoreNotLogin': '【STEAM商店】未登录,请重新登录',
            'parseDataFailedMaybeNetworkError': '解析数据失败, 可能是Token失效或者网络错误',
            'typeError': 'type错误',
            'networkError': '网络错误',
            'parseError': '解析出错',
            'importAccount': '手动导入账号',
            'importAccountSteamId64': '请输入Steam64位ID',
            'importAccountInvalidSteamId64': '请输入正确的SteamID',
            'importAccountGetToken': '请访问【 https://store.steampowered.com/pointssummary/ajaxgetasyncconfig 】,并把所有内容粘贴到下面',
            'importAccountNickName': '请输入机器人显示昵称',
            'importAccountNickNameTips': '手动添加',
            'importAccountSuccess': '添加账号成功, 请手动刷新点数',
        },
        'EN': {
            'done': 'Done',
            'changeLang': 'Change Language',
            'langName': 'English',
            'operating': 'Operating……',
            'runningLog': 'Log',
            'close': 'Close',
            'logWaring': '【Log will not save, award history will list in【History】】',
            'botListTitle': '【Bot Accounts】【ID | NickName | SteamID | Points Balance】',
            'addCurrentAccount': 'Add Account',
            'delSelectAccount': 'Del Selected',
            'reloadAccountPoints': 'Refresh Points',
            'historyTitle': '【History】【ID | NickName | SteamID | Received Points】',
            'profile': 'Profile',
            'deleteSelectedHistory': 'Del Selected',
            'clearHistory': 'Clear',
            'reloadHistory': 'Reload',
            'feedBack': 'Author',
            'feedBackTitle': '觉得好用也欢迎给我打赏',
            'notSelected': '---Use Current Account---',
            'steamID64': 'Steam 64 ID',
            'awardPoints': 'Points (Receive)',
            'recommands': 'Recommands',
            'screenshots': 'Screenshots',
            'artworks': 'Artworks',
            'setToCurrentUser': 'Set to page\'s user',
            'calculator': 'Calculator',
            'save': 'Save',
            'reset': 'Reset',
            'awardHistory': 'History',
            'startAward': 'Start Award',
            'stopAward': 'Stop',
            'stop': 'Stopped',
            'senderBotAccount': 'Award Bot: ',
            'receverAccount': 'Target SteamID: ',
            'sendPoints': 'Points (Receive): ',
            'awardPrefer': 'Award Type (Up to down left to right): ',
            'botList': 'Bots List',
            'fetchLoginAccount': 'Fetching current logined account……',
            'fetchToken': 'Fetching token……',
            'fetchPoints': 'Fetching points balance……',
            'success': 'Success',
            'failure': 'Failure',
            'error': 'Error',
            'confirm': 'Confirm',
            'tips': 'Tips',
            'addAccountSuccessTips1': 'Add account successful',
            'addAccountSuccessTips2': 'Current account\'s points balance',
            'deleteAccountConfirmTips': 'Are sure to delete selected bots?',
            'deleteAccountDoneTips1': 'Deleted',
            'deleteAccountDoneTips2': 'bots',
            'notSelectedAnyBotsTips': 'You have not selected any bot!',
            'currentProcess': 'Current process',
            'updateFailed': 'Update Failed',
            'fetchingAccountPoints': 'Fetching points balance……',
            'allDataLoaded': 'All data loaded',
            'someDataLoadFailed': 'Some data loaded failed, if some bot\'s balance is 【-1】, it means fetch error',
            'botListEmpty': 'Bot list is empty',
            'noBotAccountTips': '-- No Bot Account, you can add via【➕Add ACcount】(Loginin required) --',
            'awardTaskWasResetTips': 'Bot account had been modified, Award config reseted!',
            'noHistoryTips': '-- No History Records, It Will Record When Awarding --',
            'steamIDisEmpty': 'You must specify SteamID!',
            'fetchingProfile': 'Fetching profile……',
            'fetchingAwardableItems': 'Fetching awardable items……',
            'fetchError': 'Fetch error',
            'awardableAmount': '可打赏约',
            'nickName': 'Nickname',
            'totalPoints': 'Total points',
            'calcTips': 'According to the total number of the items, inaccurate',
            'profileNotExistsTips': 'Profile not found, please check if the steamID is correct, or use 【🤵Set to page\'s user】 instead',
            'profileLoadFailedTips': 'Network error, fetch profile failed',
            'notSelectedAnyHistoryTips': 'No records selected!',
            'clearHistoryConfirmTips': 'Are you sure to clear all award records?',
            'clearHistorySuccess': 'History cleared',
            'historyListEmpty': 'History is empty!',
            'deleteHistoryConfirmTips': 'Are you sure to delete seleted award records?',
            'deleteResultTips1': 'Deleted',
            'deleteResultTips2': 'history records',
            'notSelectedAwardBotsTips': 'No bots selected!',
            'steamIDEmptyWithTips': '【Target SteamID】is empty, It is recommended to use【🤵Set to page\'s user】!',
            'steamIDErrorWithTips': '【Target SteamID】is invalid, It is recommended to use【🤵Set to page\'s user】!',
            'pointsErrorWithTips': '【Points】is invalid, only integers are accepted!',
            'awardTypeEmptyTips': 'Please select【Award Type】!',
            'awardReadyToStartTips': 'Config saved, it is ready to【✅Start Award】',
            'resetConfigConfirmTips': 'Are you sure to reset the config?',
            'configResetSuccessTips': 'Config reseted!',
            'awardTaskDataInvalid': 'Task data invalid',
            'fetchingTargetProfile': 'Fetching target user\'s profile……',
            'awardConfig': 'Award config',
            'targetNickName': 'Target user\'s nickname',
            'targetReceivePoints': 'Expected points received',
            'targetBot': 'Selected bot',
            'taskReadyToStartTips': 'Award task will start in 【2 seconds】, click【⛔Stop】to interrupt award task!',
            'taskFailedProfileNotFound': 'Profile not found, award task end!',
            'taskAlreadyStartTips': 'Award task is already running!',
            'taskEndManually': 'Award task interrupted manually, click【❌Close】to hide the log panel.',
            'taskNotStart': 'Award task not running!',
            'running': 'Running',
            'taskStartPointsSummary': 'Start sending award, points left / points expected',
            'fetchAwardItemFailedRetry': 'Fetch awardable items failed, retry……',
            'fetchNoAwardItemSkip': 'No suitable award items, skip……',
            'beforeSendAward': 'Will send award',
            'itemAndTotal': 'items, total',
            'points': 'points',
            'sendingAwards': 'Sending awards……',
            'fetchSuccessAndFailed': 'Success / Failure',
            'fetchAwardItemFailedRetryIn2Min': 'Fetch awardable items failed, will retry in【2 seconds】……',
            'awardSuccess': 'Successful send award',
            'taskFinishedPointsSummary': 'Award task complete, points left / points expected',
            'updateBotPointsBalance': 'Update bot\'s points balance……',
            'bot': 'bots',
            'pointsBalanceUpdateSuccess': 'points balance, avilable points',
            'lackOfPointsTaskEnd': 'Lack of points balance, stop operation',
            'pointBalanceUpdateFailed': 'Update points balance failed',
            'fetchAwardItemFailedSkip': 'Fetch awardable items failed, skip……',
            'taskEndListEmpty': 'Award items list is empty, end',
            'fetchCompletedTotal': 'Fetch success, total',
            'entries': 'entries',
            'objectID': 'Target ID',
            'noAwardableObjectSkip': 'No suitable award items, skip...',
            'willAward': 'Will send award',
            'requestsSummary': 'Success / Failure',
            'wait2Seconds': '*Delay 2 seconds, to avoid exceed award*',
            'botDataError': 'Bot data error, can\'t start award task!',
            'awardTaskFinish': '✅Award task completed, click【❌Close】to hide the log panel',
            'awardTaskNotFinish': '⛔Award task not completed, click【❌close】to hide the log panel',
            'cancel': 'Cancel',
            'steamStoreNotLogin': '【STEAM Store】not logined, please sign in first',
            'parseDataFailedMaybeNetworkError': 'Parse data failed, maybe token expired or network error',
            'typeError': 'Type Error',
            'networkError': 'Network Error',
            'parseError': 'Parse Error',
            'importAccount': '手动导入账号',
            'importAccountSteamId64': '请输入Steam64位ID',
            'importAccountInvalidSteamId64': '请输入正确的SteamID',
            'importAccountGetToken': '请访问【 https://store.steampowered.com/pointssummary/ajaxgetasyncconfig 】,并把所有内容粘贴到下面',
            'importAccountNickName': '请输入机器人显示昵称',
            'importAccountNickNameTips': '手动添加',
            'importAccountSuccess': '添加账号成功, 请手动刷新点数',
        }
    }

    // 判断语言
    let language = GM_getValue("lang", null);
    if (!(language in LANG)) {
        showAlert('申明', `<p>本脚本现已免费提供</p><p>如果你在<a href="https://afdian.net/a/chr233">爱发电</a>以外的地方购买了本脚本, 请申请退款</p><p>觉得好用也欢迎给 <a href="https://steamcommunity.com/id/Chr_">作者</a> 打赏</p>`, true);
        language = "ZH";
        GM_setValue("lang", language);
    }
    // 获取翻译文本
    function t(key) {
        return LANG[language][key] || key;
    }
    {// 自动弹出提示
        const languageTips = GM_getValue("languageTips", true);
        if (languageTips && language === "ZH") {
            if (!document.querySelector("html").lang.startsWith("zh")) {
                ShowConfirmDialog("tips", "Auto Award now support English, switch?", "Using English", "Don't show again")
                    .done(() => {
                        GM_setValue("lang", "EN");
                        GM_setValue("languageTips", false);
                        window.location.reload();
                    })
                    .fail((bool) => {
                        if (bool) {
                            showAlert("", "You can switch the plugin's language using TamperMonkey's menu.");
                            GM_setValue("languageTips", false);
                        }
                    });
            }
        }
    }
    GM_registerMenuCommand(`${t("changeLang")} (${t("langName")})`, () => {
        switch (language) {
            case "EN":
                language = "ZH";
                break;
            case "ZH":
                language = "EN";
                break;
        }
        GM_setValue("lang", language);
        window.location.reload();
    });

    GM_registerMenuCommand(t("importAccount"), () => {
        const steamID = prompt(t("importAccountSteamId64"));
        const v_steamID = parseInt(steamID);
        if (v_steamID !== v_steamID) {
            alert(t("importAccountInvalidSteamId64"));
            return;
        }
        const ajaxJson = prompt(t("importAccountGetToken"));
        const json = JSON.parse(ajaxJson);
        const token = json?.data?.webapi_token;
        if (!token) {
            alert(t("parseError"));
            return;
        }

        let botCount = 0;
        for (let _ in GBots) {
            botCount++;
        }
        const nick = prompt(t("importAccountNickName"), `${t("importAccountNickNameTips")} ${botCount}`);

        alert(t("importAccountSuccess"));
        GBots[steamID] = { nick, token, points: -1 }
        GM_setValue('bots', GBots);
        flashBotList();
    });

    //机器人账号
    let GBots = {};
    //打赏历史记录
    let GHistory = {};
    //打赏任务
    let GTask = {};
    //面板状态
    let GPanel = {};
    //控件字典
    let GObjs = {};

    //初始化
    (() => {
        loadConf();

        graphGUI();
        flashBotList();
        flashHistoryList();

        const { panelMain, panelLeft } = GPanel;
        if (panelMain) {
            GPanel.panelMain = false;
            panelSwitch();
        }
        if (panelLeft) {
            GPanel.panelLeft = false;
            leftPanelSwitch();
        }
        if (!isEmptyObject(GTask)) {
            GTask.work = false;
        }
        appllyTask();
    })();

    //====================================================================================
    //添加控制面板
    function graphGUI() {
        function genButton(text, foo, enable = true) {
            const b = document.createElement('button');
            b.textContent = text;
            b.className = 'aam_button';
            b.disabled = !enable;
            b.addEventListener('click', foo);
            return b;
        }
        function genDiv(cls = 'aam_div') {
            const d = document.createElement('div');
            d.className = cls;
            return d;
        }
        function genA(text, url) {
            const a = document.createElement('a');
            a.textContent = text;
            a.className = 'aam_a';
            a.target = '_blank';
            a.href = url;
            return a;
        }
        function genInput(value, tips, number = false) {
            const i = document.createElement('input');
            i.className = 'aam_input';
            if (value) { i.value = value; }
            if (tips) { i.placeholder = tips; }
            if (number) {
                i.type = 'number';
                i.step = 100;
                i.min = 0;
            }
            return i;
        }
        function genTextArea(value, tips) {
            const i = document.createElement('textarea');
            i.className = 'aam_textarea';
            if (value) { i.value = value; }
            if (tips) { i.placeholder = tips; }
            return i;
        }
        function genCheckbox(name, checked = false) {
            const l = document.createElement('label');
            const i = document.createElement('input');
            const s = genSpace(name);
            i.textContent = name;
            i.title = name;
            i.type = 'checkbox';
            i.className = 'aam_checkbox';
            i.checked = checked;
            l.appendChild(i);
            l.appendChild(s);
            return [l, i];
        }
        function genSelect(choose = [], choice = null) {
            const s = document.createElement('select');
            s.className = 'aam_select';
            choose.forEach(([text, value]) => {
                s.options.add(new Option(text, value));
            });
            if (choice) { s.value = choice; }
            return s;
        }
        function genList(choose = [], choice = null) {
            const s = genSelect(choose, choice);
            s.className = 'aam_list';
            s.setAttribute('multiple', 'multiple');
            return s;
        }
        function genP(text) {
            const p = document.createElement('p');
            p.textContent = text;
            return p;
        }
        function genSpan(text = '    ') {
            const s = document.createElement('span');
            s.textContent = text;
            return s;
        }
        const genSpace = genSpan;
        function genBr() {
            return document.createElement('br');
        }
        function genHr() {
            return document.createElement('hr');
        }
        function genMidBtn(text, foo) {
            const a = document.createElement('a');
            const s = genSpan(text);
            a.className = 'btn_profile_action btn_medium';
            a.addEventListener('click', foo);
            a.appendChild(s);
            return [a, s];
        }

        const btnArea = document.querySelector('.profile_header_actions');
        const [btnSwitch, bSwitch] = genMidBtn('⭕', panelSwitch);
        btnArea.appendChild(genSpace());
        btnArea.appendChild(btnSwitch);
        btnArea.appendChild(genSpace());

        const panelArea = document.querySelector('.profile_leftcol');
        const panelMain = genDiv('aam_panel profile_customization');
        panelMain.style.display = 'none';
        panelArea.insertBefore(panelMain, panelArea.firstChild);

        const busyPanel = genDiv('aam_busy');
        const busyPanelContent = genDiv('aam_busy_content');
        const busyMessage = genP(t('operating'));
        const busyImg = new Image();
        busyImg.src = 'https://steamcommunity-a.akamaihd.net/public/images/login/throbber.gif';

        busyPanelContent.appendChild(busyMessage);
        busyPanelContent.appendChild(busyImg);

        busyPanel.appendChild(busyPanelContent);

        panelMain.appendChild(busyPanel);

        const workPanel = genDiv('aam_busy aam_work');
        const workLog = genTextArea('', t('runningLog'),);
        const workHide = genButton(`❌${t('close')}`, () => { workScreen(false, null); }, true);

        workPanel.appendChild(workLog);
        workPanel.appendChild(genSpan(t('logWaring')));
        workPanel.appendChild(workHide);

        panelMain.appendChild(workPanel);

        const leftPanel = genDiv('aam_left');
        const accountPanel = genDiv('aam_account');
        const accountTitle = genSpan(t('botListTitle'));
        const accountList = genList([], null);
        const accountBtns = genDiv('aam_btns');
        const acAdd = genButton(`➕${t('addCurrentAccount')}`, accountAdd);
        const acDel = genButton(`➖${t('delSelectAccount')}`, accountDel);
        const acUpdate = genButton(`🔄${t('reloadAccountPoints')}`, flashAllAccounts);

        accountBtns.appendChild(acAdd);
        accountBtns.appendChild(acDel);
        accountBtns.appendChild(acUpdate);

        accountPanel.appendChild(accountTitle);
        accountPanel.appendChild(genBr());
        accountPanel.appendChild(accountList);
        accountPanel.appendChild(accountBtns);

        leftPanel.appendChild(accountPanel);

        const historyPanel = genDiv('aam_history');
        historyPanel.style.display = 'none';

        const historyTitle = genSpan(t('historyTitle'));
        const historyList = genList([], null);
        const historyBtns = genDiv('aam_btns');
        const hsProfile = genButton(`🌏${t('profile')}`, showProfile);
        const hsDelete = genButton(`➖${t('deleteSelectedHistory')}`, deleteHistory);
        const hsClear = genButton(`🗑️${t('clearHistory')}`, clearHistory);
        const hsReload = genButton(`🔄${t('reloadHistory')}`, flashHistoryList);

        historyBtns.appendChild(hsProfile);
        historyBtns.appendChild(hsDelete);
        historyBtns.appendChild(hsClear);
        historyBtns.appendChild(hsReload);

        historyPanel.appendChild(historyTitle);
        historyPanel.appendChild(genBr());
        historyPanel.appendChild(historyList);
        historyPanel.appendChild(historyBtns);

        leftPanel.appendChild(historyPanel);
        panelMain.appendChild(leftPanel);

        const awardPanel = genDiv('aam_award');
        const feedbackLink = genA(t('feedBack'), 'https://steamcommunity.com/id/Chr_/');
        feedbackLink.title = t('feedBackTitle');
        const awardBot = genSelect([[t('notSelected'), '']], null);
        const awardSteamID = genInput('', t('steamID64'), false);
        const awardPoints = genInput('', t('awardPoints'), true);
        const [awardCProfile, awardProfile] = genCheckbox(t('profile'), true);
        const [awardCRecommand, awardRecommand] = genCheckbox(t('recommands'), true);
        const [awardCScreenshot, awardScreenshot] = genCheckbox(t('screenshots'), true);
        const [awardCImage, awardImage] = genCheckbox(t('artworks'), true);
        const awardBtns1 = genDiv('aam_btns');
        const awardBtnCurrent = genButton(`🤵${t('setToCurrentUser')}`, getCurrentProfile);
        const awardBtnCalc = genButton(`📊${t('calculator')}`, calcAwardItems);
        const awardBtns2 = genDiv('aam_btns');
        const awardBtnSet = genButton(`💾${t('save')}`, applyAwardConfig);
        const awardBtnReset = genButton(`🔨${t('reset')}`, restoreAwardConfig);
        const hSwitch = genButton(`🕒${t('awardHistory')}`, leftPanelSwitch);
        const awardBtns3 = genDiv('aam_btns aam_award_btns');
        const awardBtnStart = genButton(`✅${t('startAward')}`, startAward, false);
        const awardBtnStop = genButton(`⛔${t('stopAward')}`, stopAward, false);
        const awardStatus = genSpan(`🟥 ${t('stop')}`);

        awardBtns1.appendChild(awardBtnCurrent);
        awardBtns1.appendChild(awardBtnCalc);

        awardBtns2.appendChild(awardBtnSet);
        awardBtns2.appendChild(awardBtnReset);
        awardBtns2.appendChild(hSwitch);

        awardBtns3.appendChild(awardBtnStart);
        awardBtns3.appendChild(awardBtnStop);
        awardBtns3.appendChild(awardStatus);

        awardPanel.appendChild(genSpan(t('senderBotAccount')));
        awardPanel.appendChild(feedbackLink);
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardBot);
        awardPanel.appendChild(genSpan(t('receverAccount')));
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardSteamID);
        awardPanel.appendChild(awardBtns1);
        awardPanel.appendChild(genSpan(t('sendPoints')));
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardPoints);
        awardPanel.appendChild(genSpan(t('awardPrefer')));
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardCProfile);
        awardPanel.appendChild(awardCRecommand);
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardCScreenshot);
        awardPanel.appendChild(awardCImage);
        awardPanel.appendChild(genBr());
        awardPanel.appendChild(awardBtns2);
        awardPanel.appendChild(genHr());
        awardPanel.appendChild(awardBtns3);

        panelMain.appendChild(awardPanel);

        Object.assign(GObjs, {
            bSwitch, hSwitch, panelMain,
            busyPanel, busyMessage, workPanel, workLog, workHide,
            accountPanel, accountList, historyPanel, historyList,
            awardBot, awardSteamID, awardPoints, awardStatus,
            awardProfile, awardRecommand, awardScreenshot, awardImage,
            awardBtnStart, awardBtnStop, awardBtnSet, awardBtnReset
        });
    }
    //面板显示开关
    function panelSwitch() {
        const { bSwitch, panelMain } = GObjs;

        if (GPanel.panelMain !== true) {
            panelMain.style.display = '';
            bSwitch.textContent = '🔴';
            GPanel.panelMain = true;
        } else {
            panelMain.style.display = 'none';
            bSwitch.textContent = '⭕';
            GPanel.panelMain = false;
        }
        GM_setValue('panel', GPanel);
    }
    //左侧面板切换
    function leftPanelSwitch() {
        const { hSwitch, accountPanel, historyPanel } = GObjs;
        if (GPanel.panelLeft !== true) {
            accountPanel.style.display = 'none';
            historyPanel.style.display = '';
            hSwitch.textContent = `🤖${t('botList')}`;
            GPanel.panelLeft = true;
        } else {
            historyPanel.style.display = 'none';
            accountPanel.style.display = '';
            hSwitch.textContent = `🕒${t('awardHistory')}`;
            GPanel.panelLeft = false;
        }
        GM_setValue('panel', GPanel);
    }
    //添加账户
    function accountAdd() {
        let v_nick, v_token, v_steamID;
        loadScreen(true, t('fetchLoginAccount'));
        getMySteamID()
            .then(({ nick, steamID }) => {
                v_nick = nick;
                v_steamID = steamID;
                loadScreen(true, t('fetchToken'));
                return getToken();
            })
            .then((tk) => {
                v_token = tk;
                loadScreen(true, t('fetchPoints'));
                return getPoints(v_steamID, tk);
            })
            .then((points) => {
                showAlert(t('success'), `<p>${t('addAccountSuccessTips1')}</p><p>${t('addAccountSuccessTips2')}: ${points} ${t('points')}</p>`, true);
                GBots[v_steamID] = { nick: v_nick, token: v_token, points }
                GM_setValue('bots', GBots);
                flashBotList();
            })
            .catch((reason) => {
                showAlert(t('error'), reason, false);
            }).finally(() => {
                loadScreen(false, null);
            });
    }
    //删除账户
    function accountDel() {
        const { accountList } = GObjs;
        if (accountList.selectedIndex >= 0) {
            showConfirm(t('confirm'), t('deleteAccountConfirmTips'), () => {
                let i = 0;
                for (const opt of accountList.selectedOptions) {
                    delete GBots[opt.value];
                    i++;
                }
                flashBotList();
                GM_setValue('bots', GBots);
                showAlert(t('tips'), `${t('deleteAccountDoneTips1')} ${i} ${t('deleteAccountDoneTips2')}`, true);
            }, null);
        } else {
            showAlert(t('tips'), t('notSelectedAnyBotsTips'), false);
        }
    }
    //刷新账户点数
    async function flashAllAccounts() {
        //刷新点数
        function makePromise(sid, tk) {
            return new Promise((resolve, reject) => {
                getPoints(sid, tk)
                    .then((points) => {
                        GBots[sid].points = points;
                        loadScreen(true, `${t('currentProcess')}: ${++fin} / ${count}`);
                    }).catch((reason) => {
                        GBots[sid].points = -1;
                        // GBots[sid].nick = '读取失败';
                        loadScreen(true, `${sid} ${t('updateFailed')}: ${reason}`);
                    }).finally(() => {
                        GM_setValue('bots', GBots);
                        resolve();
                    });
            });
        }
        let count = 0, fin = 0;
        for (const _ in GBots) {
            count++;
        }
        if (count > 0) {
            loadScreen(true, t('fetchingAccountPoints'));
            const pList = [];
            for (const steamID in GBots) {
                const { token } = GBots[steamID];
                pList.push(makePromise(steamID, token));
            }
            Promise.all(pList)
                .finally(() => {
                    loadScreen(false, null);
                    flashBotList();
                    if (fin >= count) {
                        showAlert(t('done'), t('allDataLoaded'), true);
                    } else {
                        showAlert(t('done'), t('someDataLoadFailed'), true);
                    }
                });
        } else {
            showAlert(t('error'), t('botListEmpty'), false);
        }
    }
    //刷新账户列表
    function flashBotList() {
        const { bot } = GTask;
        const { accountList, awardBot } = GObjs;
        accountList.options.length = 0;
        awardBot.options.length = 0;
        awardBot.options.add(new Option(t('notSelected'), ''))
        let i = 1;
        let flag = false;
        if (!isEmptyObject(GBots)) {
            for (const steamID in GBots) {
                const { nick, points } = GBots[steamID];
                const pointsStr = parseInt(points).toLocaleString();
                accountList.options.add(new Option(`${i} | ${nick} | ${steamID} | ${pointsStr} 点`, steamID));
                awardBot.options.add(new Option(`${i++} | ${nick} | ${pointsStr} 点`, steamID))
                if (steamID === bot) {
                    flag = true;
                    awardBot.selectedIndex = i - 1;
                }
            }
        } else {
            accountList.options.add(new Option(t('noBotAccountTips'), ''));
        }
        if ((!isEmptyObject(GTask)) && (!flag)) {
            GTask = {};
            GM_setValue('task', GTask);
            appllyTask();
            showAlert(t('tips'), t('awardTaskWasResetTips'), false);
        }
    }
    //刷新历史记录列表
    function flashHistoryList() {
        const { historyList } = GObjs;
        historyList.options.length = 0;
        let i = 1;
        if (!isEmptyObject(GHistory)) {
            for (const steamID in GHistory) {
                const [nick, points] = GHistory[steamID];
                const pointsStr = parseInt(points).toLocaleString();
                historyList.options.add(new Option(`${i++} | ${nick} | ${steamID} | ${pointsStr} 点`, steamID));
            }
        } else {
            historyList.options.add(new Option(t('noHistoryTips'), ''));
        }
    }
    //历史记录增加点数
    function addHistory(steamID, nick, points) {
        if (GHistory[steamID] !== undefined) {
            GHistory[steamID] = [nick, GHistory[steamID][1] + points];
        } else {
            GHistory[steamID] = [nick, points];
        }
        GM_setValue('history', GHistory);
    }
    //获取当前个人资料
    function getCurrentProfile() {
        const { awardSteamID } = GObjs;
        awardSteamID.value = g_rgProfileData.steamid;
    }
    //计算可打赏项目
    function calcAwardItems() {
        const { awardSteamID } = GObjs;
        const steamID = awardSteamID.value.trim();
        if (steamID === '') {
            showAlert(t('error'), t('steamIDisEmpty!'), false);
        } else {
            loadScreen(true, t('fetchingProfile'));
            getProfile(steamID)
                .then(([succ, nick]) => {
                    if (succ) {
                        loadScreen(true, t('fetchingAwardableItems'));
                        const pList = [
                            getAwardCounts(steamID, 'r'),
                            getAwardCounts(steamID, 's'),
                            getAwardCounts(steamID, 'i')
                        ];
                        Promise.all(pList)
                            .then((result) => {
                                const data = {};
                                let sum = 0;
                                for (const [type, succ, count] of result) {
                                    if (succ) {
                                        const points = count * 6600;
                                        data[type] = `${count} , ${t('awardableAmount')}: ${points.toLocaleString()} ${t('points')}`;
                                        sum += points;
                                    } else {
                                        data[type] = t('fetchError');
                                    }
                                }
                                let text = `<p>${t('nickName')}: ${nick}</p><p>${t('recommands')}: ${data.r}</p><p>${t('screenshots')}: ${data.s}</p><p>${t('artworks')}: ${data.i}</p><p>${t('totalPoints')}: ${sum.toLocaleString()}</p><p>*${t('calcTips')}*</p>`
                                showAlert(t('tips'), text, true);
                            })
                            .finally(() => {
                                loadScreen(false, null);
                            });
                    } else {
                        showAlert(t('error'), t('profileNotExistsTips'), false);
                        loadScreen(false, null);
                    }
                })
                .catch((reason) => {
                    showAlert(t('error'), `<p>${t('profileLoadFailedTips')}</p><p>${reason}</p>`, false)
                    loadScreen(false, null);
                });
        }
    }
    //查看个人资料
    function showProfile() {
        const { historyList } = GObjs;
        const i = historyList.selectedIndex;
        if (i > -1) {
            const { value } = historyList.options[i];
            if (value != '') {
                window.open(`https://steamcommunity.com/profiles/${value}`);
            }
        } else {
            showAlert(t('tips'), t('notSelectedAnyHistoryTips'), false);
        }
    }
    //清除历史
    function clearHistory() {
        if (!isEmptyObject(GHistory)) {
            showConfirm(t('confirm'), t('clearHistoryConfirmTips'), () => {
                GHistory = {};
                flashHistoryList();
                GM_setValue('history', GHistory);
                showAlert(t('tips'), t('clearHistorySuccess'), true);
            }, null);
        } else {
            showAlert(t('tips'), t('historyListEmpty'), false);
        }
    }
    //删除历史
    function deleteHistory() {
        const { historyList } = GObjs;
        if (historyList.selectedIndex >= 0) {
            showConfirm(t('confirm'), t('deleteHistoryConfirmTips'), () => {
                let i = 0;
                for (const opt of historyList.selectedOptions) {
                    delete GHistory[opt.value];
                    i++;
                }
                flashHistoryList();
                GM_setValue('history', GHistory);
                showAlert(t('tips'), `${t('deleteResultTips1')} ${i} ${t('deleteResultTips2')}`, true);
            }, null);
        } else {
            showAlert(t('tips'), t('notSelectedAnyHistoryTips'), false);
        }
    }
    //保存打赏设置
    async function applyAwardConfig() {
        const {
            awardBtnStart, awardBtnStop,
            awardBot, awardSteamID, awardPoints,
            awardProfile, awardRecommand, awardScreenshot, awardImage
        } = GObjs;

        awardBtnStart.disabled = awardBtnStop.disabled = true;

        let bot = awardBot.value;
        let points = parseInt(awardPoints.value);
        let steamID = String(awardSteamID.value).trim();

        let type = 0;
        if (!awardProfile.checked) { type += 1; }
        if (!awardRecommand.checked) { type += 2; }
        if (!awardScreenshot.checked) { type += 4; }
        if (!awardImage.checked) { type += 8; }

        if (bot == '') {
            // 未选择机器人则自动使用当前登录账号
            loadScreen(true, t('fetchLoginAccount'));
            const nick = document.querySelector("#account_pulldown")?.textContent?.trim();
            if (g_steamID && nick) {
                try {
                    loadScreen(true, t('fetchToken'));
                    const token = await getToken();
                    loadScreen(true, t('fetchPoints'));
                    const points = await getPoints(g_steamID, token);
                    GBots[g_steamID] = { nick: nick, token: token, points }
                    GM_setValue('bots', GBots);
                    flashBotList();
                    bot = g_steamID;
                } catch (reason) {
                    showAlert(t('error'), reason, false);
                } finally {
                    loadScreen(false, null);
                }
            }
        }

        if (bot == '') {
            showAlert(t('error'), t('notSelectedAwardBotsTips'), false);
        } else if (steamID === '') {
            showAlert(t('error'), t('steamIDEmptyWithTips'), false);
        } else if (!steamID.match(/^\d+$/)) {
            showAlert(t('error'), t('steamIDErrorWithTips'), false);
        } else if (points !== points || points < 100) {
            showAlert(t('error'), t('pointsErrorWithTips'), false);
        } else if (type === 15) {
            showAlert(t('error'), t('awardTypeEmptyTips'), false);
        } else {
            points = Math.ceil(points / 100) * 100;
            GTask = { bot, steamID, points, type, work: false, nick: null };
            awardBtnStart.disabled = awardBtnStop.disabled = false;
            GM_setValue('task', GTask);
            showAlert(t('tips'), t('awardReadyToStartTips'), true);
        }
    }
    //重置打赏设置
    function restoreAwardConfig() {
        showConfirm(t('confirm'), t('resetConfigConfirmTips'), () => {
            GTask = {};
            GM_setValue('task', GTask);
            appllyTask();
            showAlert(t('tips'), t('configResetSuccessTips'), true);
        }, null);
    }
    //读取设置到界面
    function appllyTask() {
        const {
            awardBtnStart, awardBtnStop,
            awardBot, awardSteamID, awardPoints,
            awardProfile, awardRecommand, awardScreenshot, awardImage
        } = GObjs;
        const { bot, steamID, points, type } = GTask;

        awardBtnStart.disabled = awardBtnStop.disabled = isEmptyObject(GTask);

        awardBot.value = bot ? bot : '';
        awardSteamID.value = steamID ? steamID : '';
        awardPoints.value = points ? points : '';

        awardProfile.checked = !Boolean(type & 1);
        awardRecommand.checked = !Boolean(type & 2);
        awardScreenshot.checked = !Boolean(type & 4);
        awardImage.checked = !Boolean(type & 8);
    }
    //开始自动打赏
    async function startAward() {
        if (isEmptyObject(GTask)) {
            showAlert(t('error'), t('awardTaskDataInvalid'), false);
            return;
        }
        const { steamID, work, points, bot, nick: taskNick } = GTask;
        const { nick: botNick } = GBots[bot];
        const pointsStr = parseInt(points).toLocaleString();

        if (!work) {
            spaceLine(1);
            if (!taskNick) {
                loadScreen(true, t('fetchingTargetProfile'));
                getProfile(steamID)
                    .then(([succ, nickName]) => {
                        if (succ) {
                            GTask.work = true;
                            GTask.nick = nickName;
                            GM_setValue('task', GTask);
                            print(`${t('awardConfig')}:\n〖${t('targetNickName')}: ${nickName}, ${t('targetReceivePoints')}: ${pointsStr}, ${t('targetBot')}: ${botNick}〗`);
                            print(t('taskReadyToStartTips'));
                            workScreen(true);
                            setTimeout(() => {
                                autoAward();
                            }, 2000);
                        } else {
                            print(t('taskFailedProfileNotFound'), 'E');
                            showAlert(t('error'), t('profileNotExistsTips'), false);
                        }
                    })
                    .catch((reason) => {
                        showAlert(t('error'), `<p>${t('profileLoadFailedTips')}</p><p>${reason}</p>`, false)
                    }).finally(() => {
                        loadScreen(false, null);
                    });
            } else {
                GTask.work = true;
                GM_setValue('task', GTask);
                print(`〖${t('targetNickName')}: ${taskNick}, ${t('targetReceivePoints')}: ${pointsStr}, ${t('targetBot')}: ${botNick}〗`);
                print(t('taskReadyToStartTips'));
                workScreen(true);
                setTimeout(() => {
                    autoAward();
                }, 2000);
            }
        } else {
            print(t('taskAlreadyStartTips'));
        }
    }
    //停止自动打赏
    async function stopAward() {
        if (isEmptyObject(GTask)) {
            showAlert(t('error'), t('awardTaskDataInvalid'), false);
            return;
        }
        const { work } = GTask;
        if (work) {
            spaceLine(4);
            print(t('taskEndManually'));
            GTask.work = false;
            GM_setValue('task', GTask);
            showStatus(t('stop'), false);
        } else {
            showAlert(t('error'), t('taskNotStart'), false);
        }
    }
    //打赏项目
    const reactionsDict = {
        1: 300, 2: 300, 3: 300, 4: 300, 5: 300, 6: 300, 7: 300, 8: 300, 9: 600,
        10: 1200, 11: 2400, 12: 300, 13: 2400, 14: 600, 15: 1200, 16: 600,
        17: 4800, 18: 300, 19: 600, 20: 1200, 21: 300, 22: 600, 23: 300
    };
    const reactionValues = [
        300, 300, 300, 300, 300, 300, 300, 300, 600, 1200, 2400, 300,
        2400, 600, 1200, 600, 4800, 300, 600, 1200, 300, 600, 300
    ];
    const reactionIDs = [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
        13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23
    ];
    //自动打赏  
    async function autoAward() {
        //打赏类型
        const reactionType = {
            'p': ['3', t('profile')], 'r': ['1', t('recommands')], 's': ['2', t('screenshots')], 'i': ['2', t('artworks')]
        };
        const { bot, steamID, type, points: pointsGoal, nick: taskNick } = GTask;
        const { nick: botNick, token } = GBots[bot];

        appllyTask();
        addHistory(steamID, taskNick, 0);
        showStatus(t('running'), true);
        let pointsLeft = pointsGoal;

        if (token) {
            const workflow = [];
            if (!Boolean(type & 8)) { workflow.push('i') };
            if (!Boolean(type & 4)) { workflow.push('s') };
            if (!Boolean(type & 2)) { workflow.push('r') };
            if (!Boolean(type & 1)) { workflow.push('p') };

            while (GTask.work && workflow.length > 0) {
                const award_type = workflow.pop();
                const [target_type, target_name] = reactionType[award_type];
                let process = genProgressBar((pointsGoal - pointsLeft) / pointsGoal * 100);

                spaceLine(3);
                print(`【${target_name}】${t('taskStartPointsSummary')}: ${pointsLeft.toLocaleString()} / ${pointsGoal.toLocaleString()} ${t('points')}`);
                print(`${t('currentProcess')}: ${process}`);
                spaceLine(3);

                let coast = 0;

                if (target_type === '3') { //个人资料
                    let GoldReactions = null;
                    for (let i = 0; i < 3; i++) { //重试3次
                        if (GoldReactions === null) { //旧打赏列表为空,重新读取并打赏
                            const [succOld, oldReactions] = await getAwardRecords(token, target_type, steamID);
                            if (!succOld) {
                                print(t('fetchAwardItemFailedRetry'));
                                continue;
                            }
                            GoldReactions = oldReactions;
                            const todoReactions = selectFitableReactions(pointsLeft, GoldReactions);
                            if (todoReactions.length === 0) {
                                print(`【${target_name}】${t('fetchNoAwardItemSkip')}`);
                                break;
                            }
                            coast = sumReactionsPoints(todoReactions);
                            print(`【${target_name}】${t('beforeSendAward')}: ${todoReactions.length} ${t('itemAndTotal')}: ${coast.toLocaleString()} ${t('points')}`)
                            const plist = [];
                            for (const id of todoReactions) {
                                plist.push(sendAwardReaction(token, target_type, steamID, id));
                            }
                            print(t('sendingAwards'));
                            const result = await Promise.all(plist);
                            const [succ, fail] = countSuccess(result);
                            print(`${t('fetchSuccessAndFailed')}: ${succ} / ${fail}`);
                        }
                        //统计新的打赏列表,计算打赏点数
                        const [succNew, newReactions] = await getAwardRecords(token, target_type, steamID);
                        if (!succNew) {
                            print(t('fetchAwardItemFailedRetryIn2Min'));
                            await aiosleep(2500);
                            continue;
                        }
                        const diffReactions = filterDiffReactions(newReactions, GoldReactions);
                        coast = sumReactionsPoints(diffReactions);
                        pointsLeft -= coast;
                        addHistory(steamID, taskNick, coast);
                        print(`【${target_name}】${t('awardSuccess')}: ${diffReactions.length} ${t('itemAndTotal')}: ${coast.toLocaleString()} ${t('points')}`);
                        break;
                    }
                    GTask.points = pointsLeft;
                    if (pointsLeft <= 0) {
                        GTask.work = false;
                    }
                    GM_setValue('task', GTask);
                    process = genProgressBar((pointsGoal - pointsLeft) / pointsGoal * 100);

                    spaceLine(3);
                    print(`【${target_name}】${t('taskFinishedPointsSummary')}: ${pointsLeft.toLocaleString()} / ${pointsGoal.toLocaleString()} ${t('points')}`);
                    print(`${t('currentProcess')}: ${process}`);
                    spaceLine(3);

                    print(t('updateBotPointsBalance'));

                    await getPoints(bot, token)
                        .then((p) => {
                            GBots[bot].points = p;
                            GM_setValue('bots', GBots);
                            print(`${t('bot')}【${botNick}】${t('pointsBalanceUpdateSuccess')}: ${p.toLocaleString()} ${t('points')}`);
                            if (p < 300) {
                                print(`${t('bot')}【${botNick}】${t('lackOfPointsTaskEnd')}`);
                                GTask.work = false;
                            }
                        }).catch((r) => {
                            print(`${t('bot')}【${botNick}】${t('pointBalanceUpdateFailed')}: ${r}`);
                        });


                } else { //截图 
                    let page = 1;
                    while (GTask.work) {
                        let j = 0;
                        print(t('fetchingAwardableItems'));
                        const [succ, items] = await getAwardItems(steamID, award_type, page++);
                        if (!succ) {
                            page--;
                            if (++j < 3) {
                                print(t('fetchAwardItemFailedRetryIn2Min'));
                                await aiosleep(2500);
                                continue;
                            } else {
                                print(t('fetchAwardItemFailedSkip'));
                                break;
                            }
                        }
                        if (items.length === 0) {
                            print(`【${target_name}】${t('taskEndListEmpty')}`);
                            break;
                        }

                        print(`【${target_name}】${t('fetchCompletedTotal')} ${items.length} ${t('entries')}`);

                        for (const itemID of items) {

                            print(`【${target_name}】${t('objectID')}: ${itemID}`);
                            let GoldReactions = null;

                            for (let i = 0; i < 3; i++) {
                                if (GoldReactions === null) { //旧打赏列表为空,重新读取并打赏
                                    const [succOld, oldReactions] = await getAwardRecords(token, target_type, itemID);
                                    if (!succOld) {
                                        print(t('fetchAwardItemFailedRetry'));
                                        continue;
                                    }
                                    GoldReactions = oldReactions;
                                    const todoReactions = selectFitableReactions(pointsLeft, GoldReactions);
                                    if (todoReactions.length === 0) {
                                        print(`【${target_name}】${t('noAwardableObjectSkip')}`);
                                        break;
                                    }
                                    coast = sumReactionsPoints(todoReactions);
                                    print(`【${target_name}】${t('willAward')}: ${todoReactions.length} ${t('itemAndTotal')}: ${coast.toLocaleString()} ${t('points')}`)
                                    const plist = [];
                                    for (const id of todoReactions) {
                                        plist.push(sendAwardReaction(token, target_type, itemID, id));
                                    }
                                    print(t('sendingAwards'));
                                    const result = await Promise.all(plist);
                                    const [succ, fail] = countSuccess(result);
                                    print(`${t('requestsSummary')}: ${succ} / ${fail}`);
                                }
                                print(t('wait2Seconds'));
                                await asleep(2000);
                                //统计新的打赏列表,计算打赏点数
                                const [succNew, newReactions] = await getAwardRecords(token, target_type, itemID);
                                if (!succNew) {
                                    print(t('fetchAwardItemFailedRetry'));
                                    continue;
                                }
                                const diffReactions = filterDiffReactions(newReactions, GoldReactions);
                                coast = sumReactionsPoints(diffReactions);
                                pointsLeft -= coast;
                                addHistory(steamID, taskNick, coast);
                                print(`【${target_name}】${t('awardSuccess')}: ${diffReactions.length} ${t('itemAndTotal')}: ${coast.toLocaleString()} ${t('points')}`);
                                break;
                            }
                            GTask.points = pointsLeft;
                            if (pointsLeft <= 0) {
                                GTask.work = false;
                            }
                            GM_setValue('task', GTask);
                            process = genProgressBar((pointsGoal - pointsLeft) / pointsGoal * 100);

                            spaceLine(3);
                            print(`【${target_name}】${t('taskFinishedPointsSummary')}: ${pointsLeft.toLocaleString()} / ${pointsGoal.toLocaleString()} ${t('points')}`);
                            print(`${t('currentProcess')}: ${process}`);
                            spaceLine(3);

                            print(t('updateBotPointsBalance'));

                            await getPoints(bot, token)
                                .then((p) => {
                                    GBots[bot].points = p;
                                    GM_setValue('bots', GBots);
                                    print(`${t('bot')}【${botNick}】${t('pointsBalanceUpdateSuccess')}: ${p.toLocaleString()} ${t('points')}`);
                                    if (p < 300) {
                                        print(`${t('bot')}【${botNick}】${t('lackOfPointsTaskEnd')}`);
                                        GTask.work = false;
                                    }
                                }).catch((r) => {
                                    print(`${t('bot')}【${botNick}】${t('pointBalanceUpdateFailed')}: ${r}`);
                                });

                            if (!GTask.work) {
                                break;
                            }
                        }
                    }
                }
                if (workflow.length > 0) {
                    await aiosleep(1500);
                }
            }
        } else {
            delete GBots[bot];
            GM_setValue('bots', GBots);
            print(t('botDataError'));
            showAlert(t('error'), t('botDataError'), false);
        }
        spaceLine(4);
        if (pointsLeft <= 0) {
            GTask = {};
            print(t('awardTaskFinish'));
        } else {
            GTask.work = false;
            print(t('awardTaskNotFinish'));
        }
        GM_setValue('task', GTask);
        appllyTask();
        showStatus(t('stop'), false);
        flashHistoryList();
    }
    //====================================================================================
    //显示提示
    function showAlert(title, text, succ = true) {
        ShowAlertDialog(`${succ ? '✅' : '❌'}${title}`, `<div>${text}</div>`);
    }
    //显示确认
    function showConfirm(title, text, done = null, cancel = null) {
        ShowConfirmDialog(`⚠️${title}`, `<div>${text}</div>`, t('confirm'), t('cancel'))
            .done(() => {
                if (done) { done(); }
            })
            .fail(() => {
                if (cancel) { cancel(); }
            })
    }
    //显示状态
    function showStatus(text, run = true) {
        const { awardStatus, workHide } = GObjs;
        workHide.disabled = run;
        awardStatus.textContent = `${run ? '🟩' : '🟥'} ${text}`;
    }
    //读取设置
    function loadConf() {
        const bots = GM_getValue('bots');
        GBots = isEmptyObject(bots) ? {} : bots;
        const hs = GM_getValue('history');
        GHistory = isEmptyObject(hs) ? {} : hs;
        const task = GM_getValue('task');
        GTask = isEmptyObject(task) ? {} : task;
        const panel = GM_getValue('panel');
        GPanel = isEmptyObject(panel) ? {} : panel;
    }
    //保存设置
    function saveConf() {
        GM_setValue('bots', GBots);
        GM_setValue('history', GHistory);
        GM_setValue('task', GTask);
        GM_setValue('panel', GPanel);
    }
    //是不是空对象
    function isEmptyObject(obj) {
        for (const _ in obj) { return false; }
        return true;
    }
    //显示加载面板
    function loadScreen(show = true, msg = t('operating')) {
        const { busyPanel, busyMessage } = GObjs;
        if (show) {
            busyPanel.style.opacity = '1';
            busyPanel.style.visibility = 'visible';
            if (msg) {
                busyMessage.textContent = msg;
            }
        } else {
            busyPanel.style.opacity = '0';
            busyPanel.style.visibility = 'hidden';
        }
    }
    //显示日志面板
    function workScreen(show = true) {
        const { workPanel } = GObjs;
        if (show) {
            workPanel.style.opacity = '1';
            workPanel.style.visibility = 'visible';
        } else {
            workPanel.style.opacity = '0';
            workPanel.style.visibility = 'hidden';
        }
    }
    //生成进度条
    const BAR_STYLE = '⣀⣄⣤⣦⣶⣷⣿';
    function genProgressBar(percent) {
        const full_symbol = '⣿';
        const none_symbol = '⣀';
        const percentStr = ` ${percent.toFixed(2)}%`
        if (percent >= 100) {
            return full_symbol.repeat(40) + percentStr;
        } else {
            percent = percent / 100;
            let full = Math.floor(percent * 40);
            let rest = percent * 40 - full;
            let middle = Math.floor(rest * 6);
            if (percent !== 0 && full === 0 && middle === 0) { middle = 1; }
            let d = Math.abs(percent - (full + middle / 6) / 40) * 100;
            if (d < Number.POSITIVE_INFINITY) {
                let m = BAR_STYLE[middle];
                if (full === 40) { m = ""; }
                return full_symbol.repeat(full) + m + BAR_STYLE[0].repeat(39 - full) + percentStr;
            }
            return none_symbol.repeat(40) + percentStr;
        }
    }
    //日志时间
    function formatTime() {
        const date = new Date();
        return `${date.toLocaleDateString()} ${date.toTimeString().substr(0, 8)}`;
    }
    //输出日志
    function print(msg, level = 'I') {
        const { workLog } = GObjs;
        const time = formatTime();
        workLog.value += `${time} - ${level} - ${msg}\n`;
        workLog.scrollTop = workLog.scrollHeight;
        console.log(`${time} - ${level} - ${msg}`);
    }
    //画分割线
    function spaceLine(style = 1) {
        switch (style) {
            case 1:
                print('#'.repeat(68));
                return
            case 2:
                print('='.repeat(68));
                return
            case 3:
                print('+'.repeat(68));
                return
            case 4:
                print('~'.repeat(68));
                return
        }
    }
    //异步延时
    function asleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    //====================================================================================
    //计算合适的打赏项目
    function selectFitableReactions(goal, doneList) {
        const fitableList = [];
        const aviableList = [];
        for (const id of reactionIDs) {
            if (doneList.indexOf(id) === -1) {
                aviableList.push(id);
            }
        }
        aviableList.sort((a, b) => { return reactionsDict[a] - reactionsDict[b]; });
        for (const id of aviableList) {
            if (goal < 100) {
                break;
            }
            const value = reactionsDict[id] / 3;
            if (goal >= value) {
                fitableList.push(id);
                goal -= value;
            }
        }
        return fitableList;
    }
    //获取新增打赏项目
    function filterDiffReactions(newList, oldList) {
        const diffList = [];
        for (const id of newList) {
            if (oldList.indexOf(id) === -1) {
                diffList.push(id);
            }
        }
        return diffList;
    }
    //计算打赏项目点数开销
    function sumReactionsPoints(reactions) {
        let points = 0;
        for (const id of reactions) {
            points += reactionsDict[id];
        }
        return points / 3;
    }
    //统计成功失败
    function countSuccess(result) {
        let succ = 0, fail = 0;
        for (const r of result) {
            if (r) {
                succ++;
            } else {
                fail++;
            }
        }
        return ([succ, fail]);
    }
    //异步延时
    function aiosleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }
    //====================================================================================
    function getMySteamID() {
        return new Promise((resolve, reject) => {
            try {
                const steamID = g_steamID;
                const nick = document.querySelector("div.playerAvatar>a>img")?.getAttribute("alt");

                if (nick && steamID) {
                    resolve({ nick, steamID });
                } else {
                    reject(t('steamStoreNotLogin'));
                }
            } catch (err) {
                reject(err);
            }
        });
    }
    function getToken() {
        return new Promise((resolve, reject) => {
            try {
                let token = document.querySelector("#application_config")?.getAttribute("data-loyalty_webapi_token");

                if (isEmptyObject(token)) {
                    reject(t('steamStoreNotLogin'));
                }
                else {
                    token =token.replace(/"/g, "");
                    resolve(token);
                }
            } catch (err) {
                reject(err);
            }
        });
    }
    function getPoints(steamID, token) {
        return new Promise((resolve, reject) => {
            $http.get(`https://api.steampowered.com/ILoyaltyRewardsService/GetSummary/v1/?access_token=${token}&steamid=${steamID}`)
                .then(({ response }) => {
                    if (isEmptyObject(response)) {
                        reject(t('steamStoreNotLogin'));
                    }
                    try {
                        const points = parseInt(response.summary.points);
                        if (points === points) {
                            resolve(points);
                        } else {
                            reject(t('parseDataFailedMaybeNetworkError'));
                        }
                    } catch (e) {
                        reject(t('parseDataFailedMaybeNetworkError'));
                    }
                })
                .catch((reason) => {
                    reject(reason);
                });
        });
    }
    function getProfile(steamID) {
        return new Promise((resolve, reject) => {
            $http.getText(`https://steamcommunity.com/profiles/${steamID}/?xml=1`)
                .then((text) => {
                    try {
                        const match = text.match(/<steamID><!\[CDATA\[([\s\S]*)\]\]><\/steamID>/) ||
                            text.match(/<steamID>([\s\S]*)<\/steamID>/);
                        if (match) {
                            resolve([true, match[1].substring()]);
                        } else {
                            resolve([false, null]);
                        }
                    } catch (e) {
                        reject(e);
                    }
                })
                .catch((reason) => {
                    reject(reason);
                });
        });
    }
    function getAwardCounts(steamID, type) {
        let subPath, preg;
        switch (type) {
            case 'r':
                subPath = 'recommended/?l=schinese';
                preg = /共 (\d+) 项条目/;
                break;
            case 's':
                subPath = 'screenshots/?l=schinese&appid=0&sort=newestfirst&browsefilter=myfiles&view=grid';
                preg = /共 (\d+) 张/;
                break;
            case 'i':
                subPath = 'images/?l=schinese&appid=0&sort=newestfirst&browsefilter=myfiles&view=grid';
                preg = /共 (\d+) 张/;
                break;
            default:
                throw 'type错误';
        }
        return new Promise((resolve, reject) => {
            $http.getText(`https://steamcommunity.com/profiles/${steamID}/${subPath}`)
                .then((text) => {
                    try {
                        const match = text.match(preg);
                        const count = match ? Number(match[1]) : 0;
                        resolve([type, true, count]);
                    } catch (e) {
                        resolve([type, false, 0]);
                    }
                })
                .catch((reason) => {
                    console.error(reason);
                    resolve([type, false, 0]);
                });
        });
    }
    function getAwardItems(steamID, type, p = 1) {
        let subPath, preg;
        switch (type) {
            case 'r':
                subPath = `recommended/?p=${p}&l=schinese`;
                preg = /id="RecommendationVoteUpBtn(\d+)"/g;
                break;
            case 's':
                subPath = `screenshots/?p=${p}&view=grid&l=schinese`;
                preg = /id="imgWallHover(\d+)"/g;
                break;
            case 'i':
                subPath = `images/?p=${p}&view=grid&l=schinese`;
                preg = /id="imgWallHover(\d+)"/g;
                break;
            default:
                throw t('typeError');
        }
        return new Promise((resolve, reject) => {
            $http.getText(`https://steamcommunity.com/profiles/${steamID}/${subPath}`)
                .then((text) => {
                    try {
                        const result = [];
                        const matches = text.matchAll(preg);
                        for (const match of matches) {
                            result.push(match[1]);
                        }
                        resolve([true, result]);
                    } catch (e) {
                        console.error(e);
                        resolve([false, e]);
                    }
                })
                .catch((reason) => {
                    console.error(reason);
                    resolve([false, reason]);
                });
        });
    }
    function getAwardRecords(token, targetType, targetID) {
        return new Promise((resolve, reject) => {
            const params = `access_token=${token}&target_type=${targetType}&targetid=${targetID}`;
            $http.get('https://api.steampowered.com/ILoyaltyRewardsService/GetReactions/v1/?' + params)
                .then(({ response }) => {
                    const { reactionids } = response;
                    resolve([true, reactionids || []]);
                })
                .catch((reason) => {
                    console.error(reason);
                    resolve([false, null]);
                });
        });
    }
    function sendAwardReaction(token, targetType, targetID, reactionID) {
        return new Promise((resolve, reject) => {
            const params = `access_token=${token}&target_type=${targetType}&targetid=${targetID}&reactionid=${reactionID}`;
            $http.post('https://api.steampowered.com/ILoyaltyRewardsService/AddReaction/v1/?' + params)
                .then((json) => {
                    console.log(json);
                    resolve(true);
                })
                .catch((reason) => {
                    console.error(reason);
                    resolve(false);
                });
        });
    }
})();
//====================================================================================
class Request {
    constructor(timeout = 3000) {
        this.timeout = timeout;
    }
    get(url, opt = {}) {
        return this.baseRequest(url, 'GET', opt, 'json');
    }
    getHtml(url, opt = {}) {
        return this.baseRequest(url, 'GET', opt, '');
    }
    getText(url, opt = {}) {
        return this.baseRequest(url, 'GET', opt, 'text');
    }
    post(url, data, opt = {}) {
        opt.data = JSON.stringify(data);
        return this.baseRequest(url, 'POST', opt, 'json');
    }
    baseRequest(url, method = 'GET', opt = {}, responseType = 'json') {
        Object.assign(opt, {
            url, method, responseType, timeout: this.timeout
        });
        return new Promise((resolve, reject) => {
            opt.ontimeout = opt.onerror = reject;
            opt.onload = ({ readyState, status, response, responseText }) => {
                if (readyState === 4 && status === 200) {
                    if (responseType == 'json') {
                        resolve(response);
                    } else if (responseType == 'text') {
                        resolve(responseText);
                    }
                } else {
                    console.error("网络错误");
                    console.log(readyState);
                    console.log(status);
                    console.log(response);
                    reject("解析失败");
                }
            }
            GM_xmlhttpRequest(opt);
        });
    }
}
const $http = new Request();

//CSS表
GM_addStyle(`.aam_panel,
.aam_work {
  padding: 10px;
  display: flex;
}
.aam_work {
  z-index: 500 !important;
}
.aam_busy {
  width: 100%;
  height: 100%;
  z-index: 700;
  position: absolute;
  top: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.7);
  display: table;
  visibility: hidden;
  opacity: 0;
  transition: all 0.1s;
}
.aam_busy_content {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
.aam_left {
  width: 61%;
  padding-right: 10px;
}
.aam_award {
  width: 39%;
}
.aam_list,
.aam_select,
.aam_input,
.aam_textarea {
  background-color: #fff !important;
  color: #000 !important;
  border: none !important;
  border-radius: 0 !important;
}
.aam_input {
  width: 98% !important;
  text-align: center;
}
.aam_list {
  height: 230px;
}
.aam_textarea {
  height: calc(100% - 85px);
  width: calc(100% - 45px);
  resize: none;
  font-size: 12px;
}
.aam_left > div > *,
.aam_award:not(span, button) > * {
  width: 100%;
  margin-bottom: 5px;
}
.aam_btns > button:not(:last-child) {
  margin-right: 4px;
}
.aam_award_btns {
  z-index: 600;
  bottom: 10px;
  position: absolute;
}
.aam_work > * {
  position: absolute;
}
.aam_work > span {
  bottom: 12%;
  left: 70px;
}
.aam_work > button {
  bottom: 11%;
}
.aam_a {
  margin-left: 110px;
}`);