跳过正文
  1. 文章/

Poe显示消耗积分脚本

·887 字·5 分钟
Poe Tampermonkey 脚本 实用工具
目录

0. 写在开头
#

今年年初的时候,出于跟进先进生产力的需求,在众多AI平台中选中了Poe

当时,它的优势如下:

序号项目优势
1价格当时尼区价格是99900奈拉,加上一些消耗,大概500一年。
而且可以年付,相对GPT和Claude的月付,低价区的收益更高。
2功能支持多平台的AI服务,包括各版本GPT、各版本Claude、各服务的长上下文版本,更新的也较快。
并且是平台方接入官方API,质量方面相对其他第三方更有保障。
3是否易用支持对话、支持机器人API、IP限制少(ps: 不像Claude老封号)
4用量是否足够每月100万积分,每次对话消耗固定积分,默认API的上下文。

当时是一个人使用每个月都用不完积分,直到前几天Poe更新了计算点系统,等于是每次对话是在基础固定积分上加上Token的消耗,这样相对之前来说,消耗积分更多了,特别是长上下文情况下,积分消耗几乎是指数上升。

不过,相对来说,Poe价格也还算实惠。再相比最近GPT降智、Claude封号,Poe仍然是一个不错的选择。

下面是Poe和官Key对比:

图片来源Naproxen

涨价之后我简单算了一下,转变思想,把Poe当成个6折API省着用。使用习惯上,非必要不追问,随手清除上下文,问题尽量一段话问出来(甚至使用沉浸式翻译把输入翻译成英文),总比买官key便宜。

但是呢,这Poe又不显示每次对话消耗的积分,只能在设置页面看到当前总积分,很难去看到每次对话消耗的积分。 所以,我写了一个脚本,用来显示每次对话的消耗积分,方便自己控制消耗。

2. 脚本效果
#

Poe积分显示

显示内容:

  1. 重置时间
  2. 当前积分
  3. 本次消耗积分

3. 脚本内容
#

// ==UserScript==
// @name         Poe积分显示
// @namespace    http://tampermonkey.net/
// @version      1.2
// @author       xiadengma
// @description  在每次对话的下方显示当前积分和本次对话消耗的积分
// @match        *://poe.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
    'use strict';

    const DEBUG = false;
    const log = DEBUG ? console.log.bind(console, '[Poe积分显示]') : () => { };
    const error = DEBUG ? console.error.bind(console, '[Poe积分显示]') : () => { };

    const SELECTORS = {
        messagePair: '.ChatMessagesView_messagePair__ZEXUz',
        inputMessage: '.Message_rightSideMessageBubble__ioa_i',
        outputMessage: '.Message_leftSideMessageBubble__VPdk6',
        stopButton: 'button[aria-label="停止信息"]',
        actionBar: '.ChatMessageActionBar_actionBar__gyeEs',
        pointsElement: '.SettingsSubscriptionSection_computePointsValue___DLOM',
        resetElement: '.SettingsSubscriptionSection_subtext__cZuI6',
        messagePointLimitElement: '.DefaultMessagePointLimit_computePointsValue__YYJkB'
    };

    const CONFIG = {
        checkInterval: 200,
        stableCount: 1,
        cacheExpiry: 5 * 60 * 1000,
        retryLimit: 3,
        retryDelay: 1000,
        maxPointsFetchAttempts: 5
    };

    const state = {
        pointsBeforeOutput: null,
        resetDate: '',
        processedInputNodes: new WeakSet(),
        processedOutputNodes: new WeakSet(),
        initialLoadCompleted: false,
        isFetching: false,
        isInitialized: false,
        observer: null,
    };

    GM_addStyle(`
        .points-info {
            font-size: 12px;
            padding: 8px 16px;
            margin: 8px 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background: rgba(255, 255, 255, 0.05);
            border-radius: 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
        }
    `);

    const throttle = (func, limit) => {
        let lastFunc;
        let lastRan;
        return function () {
            const context = this;
            const args = arguments;
            if (!lastRan) {
                func.apply(context, args);
                lastRan = Date.now();
            } else {
                clearTimeout(lastFunc);
                lastFunc = setTimeout(function () {
                    if ((Date.now() - lastRan) >= limit) {
                        func.apply(context, args);
                        lastRan = Date.now();
                    }
                }, limit - (Date.now() - lastRan));
            }
        }
    }

    async function fetchPoints(retryCount = 0) {
        if (state.isFetching) {
            await new Promise(resolve => setTimeout(resolve, 100));
            return fetchPoints(retryCount);
        }

        state.isFetching = true;
        try {
            log('正在获取积分信息...');
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://poe.com/settings',
                    onload: resolve,
                    onerror: reject
                });
            });

            const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
            const pointsElement = doc.querySelector(SELECTORS.pointsElement);
            const resetElement = doc.querySelector(SELECTORS.resetElement);
            const messagePointLimitElement = doc.querySelector(SELECTORS.messagePointLimitElement);

            if (!pointsElement) {
                throw new Error('积分元素丢失');
            }

            const currentPoints = parseInt(pointsElement.textContent.replace(/,/g, ''), 10);
            log('当前积分:', currentPoints);

            if (resetElement) {
                state.resetDate = resetElement.textContent.trim();
                log('重置时间:', state.resetDate);
            }

            if (messagePointLimitElement) {
                state.messagePointLimit = parseInt(messagePointLimitElement.textContent.replace(/,/g, ''), 10);
                log('全局单条信息预算:', state.messagePointLimit);
            }

            state.pointsBeforeOutput = currentPoints;
            return currentPoints;
        } catch (err) {
            error('获取积分信息失败', err);
            if (retryCount < CONFIG.retryLimit) {
                log(`重试获取积分 (${retryCount + 1}/${CONFIG.retryLimit})...`);
                await new Promise(resolve => setTimeout(resolve, CONFIG.retryDelay));
                return fetchPoints(retryCount + 1);
            }
            throw err;
        } finally {
            state.isFetching = false;
        }
    }

    function monitorMessages() {
        log('开始监听消息...');
        if (state.observer) {
            state.observer.disconnect();
        }
        state.observer = new MutationObserver(throttledHandleMutations);
        state.observer.observe(document.body, { childList: true, subtree: true });
        detectInitialLoadCompletion();
    }

    function detectInitialLoadCompletion() {
        let messageCount = 0;
        let lastMessageCount = 0;
        let stableCount = 0;

        const checkComplete = () => {
            messageCount = document.querySelectorAll(SELECTORS.messagePair).length;

            if (messageCount === lastMessageCount) {
                if (++stableCount >= CONFIG.stableCount) {
                    log('初始加载完成,开始忽略历史消息');
                    state.initialLoadCompleted = true;
                    return;
                }
            } else {
                stableCount = 0;
            }

            lastMessageCount = messageCount;
            setTimeout(checkComplete, CONFIG.checkInterval);
        };

        checkComplete();
    }

    const throttledHandleMutations = throttle(handleMutations, 200);

    function handleMutations(mutations) {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    const messagePair = node.closest(SELECTORS.messagePair);
                    if (messagePair) {
                        processMessagePair(messagePair);
                    }
                }
            }
        }
    }

    const isMessageGenerating = () => !!document.querySelector(SELECTORS.stopButton);

    function waitForMessageCompletion(outputMessage) {
        return new Promise(resolve => {
            let lastContent = outputMessage.textContent;
            let stableCount = 0;

            const checkComplete = () => {
                const currentContent = outputMessage.textContent;
                if (currentContent === lastContent && !isMessageGenerating()) {
                    if (++stableCount >= CONFIG.stableCount) {
                        log('消息输出已完成');
                        resolve();
                        return;
                    }
                } else {
                    stableCount = 0;
                }
                lastContent = currentContent;
                setTimeout(checkComplete, CONFIG.checkInterval);
            };

            checkComplete();
        });
    }

    async function processMessagePair(messagePair) {
        if (!state.isInitialized || !state.initialLoadCompleted) {
            log('脚本尚未完全初始化或页面未加载完成,跳过消息处理');
            return;
        }

        const inputMessage = messagePair.querySelector(SELECTORS.inputMessage);
        if (inputMessage && !state.processedInputNodes.has(inputMessage)) {
            log('检测到新的输入消息');
            state.processedInputNodes.add(inputMessage);
            log('输入前积分:', state.pointsBeforeOutput);
        }

        const outputMessage = messagePair.querySelector(SELECTORS.outputMessage);
        if (outputMessage && !state.processedOutputNodes.has(outputMessage)) {
            if (!outputMessage.textContent.trim()) {
                log('输出消息尚未完整,等待加载...');
                return;
            }
            log('检测到新的输出消息');
            state.processedOutputNodes.add(outputMessage);

            const pointsBeforeOutput = state.pointsBeforeOutput;
            log('输出前积分:', pointsBeforeOutput);

            try {
                await waitForMessageCompletion(outputMessage);
                log('消息已完全输出,等待积分更新...');

                await new Promise(resolve => setTimeout(resolve, 500));

                let pointsAfterOutput = pointsBeforeOutput;
                for (let i = 0; i < CONFIG.maxPointsFetchAttempts; i++) {
                    const newPoints = await fetchPoints();
                    if (newPoints !== pointsBeforeOutput) {
                        pointsAfterOutput = newPoints;
                        break;
                    }
                    log(`第 ${i + 1} 次尝试获取积分,未发现变化`);
                    if (i < CONFIG.maxPointsFetchAttempts - 1) {
                        await new Promise(resolve => setTimeout(resolve, 1000));
                    }
                }

                const pointsUsed = pointsBeforeOutput - pointsAfterOutput;
                log('输出后积分:', pointsAfterOutput);
                log('本次对话消耗积分:', pointsUsed);
                if (pointsUsed > 0) {
                    displayPointsInfo(messagePair, pointsAfterOutput, pointsUsed);
                }
                state.pointsBeforeOutput = pointsAfterOutput;
            } catch (err) {
                error('积分更新或消息完成失败:', err);
            }
        }
    }

    function displayPointsInfo(messagePair, currentPoints, pointsUsed) {
        if (messagePair.querySelector('.points-info')) return;

        log('显示积分信息');
        const pointsInfo = createPointsInfoElement(currentPoints, pointsUsed);
        const actionBar = messagePair.querySelector(SELECTORS.actionBar);
        if (actionBar) {
            actionBar.parentNode.insertBefore(pointsInfo, actionBar);
        } else {
            messagePair.appendChild(pointsInfo);
        }
    }

    function createPointsInfoElement(currentPoints, pointsUsed) {
        const pointsInfo = document.createElement('div');
        pointsInfo.className = 'points-info';

        const infoItems = [
            { text: `重置时间: ${state.resetDate}`, color: '#555' },
            { text: `当前积分: ${currentPoints.toLocaleString()}`, color: '#888' },
            { text: `本次消耗积分: ${pointsUsed}`, color: '#fff' }
        ];

        pointsInfo.innerHTML = infoItems.map(item => `<div style="color: ${item.color}">${item.text}</div>`).join('');

        return pointsInfo;
    }

    function handleUrlChange() {
        log('URL已更改,重置状态');
        state.processedInputNodes = new WeakSet();
        state.processedOutputNodes = new WeakSet();
        state.initialLoadCompleted = false;
        state.pointsBeforeOutput = null;

        init();
    }

    async function init() {
        try {
            const initialPoints = await fetchPoints();
            state.pointsBeforeOutput = initialPoints;
            state.isInitialized = true;
            log('初始化完成,当前积分:', initialPoints);
            monitorMessages();

            window.addEventListener('popstate', handleUrlChange);
            let lastUrl = location.href;
            new MutationObserver(() => {
                const url = location.href;
                if (url !== lastUrl) {
                    lastUrl = url;
                    handleUrlChange();
                }
            }).observe(document, { subtree: true, childList: true });

        } catch (err) {
            error('初始化失败:', err);
        }
    }

    init();
})();
xiadengma
作者
xiadengma