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对比:
涨价之后我简单算了一下,转变思想,把Poe当成个6折API省着用。使用习惯上,非必要不追问,随手清除上下文,问题尽量一段话问出来(甚至使用沉浸式翻译把输入翻译成英文),总比买官key便宜。
但是呢,这Poe又不显示每次对话消耗的积分,只能在设置页面看到当前总积分,很难去看到每次对话消耗的积分。 所以,我写了一个脚本,用来显示每次对话的消耗积分,方便自己控制消耗。
2. 脚本效果#
显示内容:
- 重置时间
- 当前积分
- 本次消耗积分
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();
})();