Allow SUB2API session import from open ChatGPT tabs

This commit is contained in:
QLHazyCoder
2026-05-19 12:40:03 +08:00
parent ef700be3e2
commit 996ec92168
2 changed files with 300 additions and 16 deletions
+109 -14
View File
@@ -71,32 +71,127 @@
}
}
function getSessionTabHostPriority(url = '') {
try {
const hostname = String(new URL(String(url || '')).hostname || '').trim().toLowerCase();
if (/(^|\.)chatgpt\.com$/.test(hostname)) {
return 0;
}
if (hostname === 'chat.openai.com') {
return 1;
}
if (/(^|\.)openai\.com$/.test(hostname)) {
return 2;
}
} catch {
return Number.POSITIVE_INFINITY;
}
return Number.POSITIVE_INFINITY;
}
function getSessionTabActivityPriority(tab = {}) {
if (tab?.active && tab?.currentWindow) {
return 0;
}
if (tab?.active) {
return 1;
}
return 2;
}
function pickPreferredSessionTab(tabs = []) {
const candidates = (Array.isArray(tabs) ? tabs : [])
.filter((tab) => Number.isInteger(tab?.id) && isSupportedChatGptSessionUrl(tab.url));
if (!candidates.length) {
return null;
}
return candidates.reduce((best, candidate) => {
if (!best) {
return candidate;
}
const candidateHostPriority = getSessionTabHostPriority(candidate.url);
const bestHostPriority = getSessionTabHostPriority(best.url);
if (candidateHostPriority !== bestHostPriority) {
return candidateHostPriority < bestHostPriority ? candidate : best;
}
const candidateActivityPriority = getSessionTabActivityPriority(candidate);
const bestActivityPriority = getSessionTabActivityPriority(best);
if (candidateActivityPriority !== bestActivityPriority) {
return candidateActivityPriority < bestActivityPriority ? candidate : best;
}
const candidateLastAccessed = Number(candidate?.lastAccessed) || 0;
const bestLastAccessed = Number(best?.lastAccessed) || 0;
if (candidateLastAccessed !== bestLastAccessed) {
return candidateLastAccessed > bestLastAccessed ? candidate : best;
}
return Number(candidate.id) < Number(best.id) ? candidate : best;
}, null);
}
async function readSupportedSessionTab(tabId) {
const numericTabId = Number(tabId) || 0;
if (!numericTabId || !chrome?.tabs?.get) {
return null;
}
const tab = await chrome.tabs.get(numericTabId).catch(() => null);
return tab?.id && isSupportedChatGptSessionUrl(tab.url)
? tab
: null;
}
async function findFallbackSessionTab() {
if (!chrome?.tabs?.query) {
return null;
}
const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true }).catch(() => []);
const activeMatch = pickPreferredSessionTab(activeTabs);
const allTabs = await chrome.tabs.query({}).catch(() => []);
const globalMatch = pickPreferredSessionTab(allTabs);
return pickPreferredSessionTab([activeMatch, globalMatch]);
}
async function resolveSessionTabId(state = {}) {
const registeredTabId = typeof getTabId === 'function'
? await getTabId(PLUS_CHECKOUT_SOURCE)
: null;
if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) {
return Number(registeredTabId) || 0;
}
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
if (storedTabId && chrome?.tabs?.get) {
const tab = await chrome.tabs.get(storedTabId).catch(() => null);
if (tab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, tab.id);
}
return tab.id;
const registeredTab = await readSupportedSessionTab(registeredTabId);
if (registeredTab?.id) {
return registeredTab.id;
}
}
throw new Error('未找到可读取 ChatGPT 会话的 Plus 标签页,请先完成当前 Plus 支付链路。');
const storedTabId = Number(state?.plusCheckoutTabId) || 0;
const storedTab = await readSupportedSessionTab(storedTabId);
if (storedTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, storedTab.id);
}
return storedTab.id;
}
const fallbackTab = await findFallbackSessionTab();
if (fallbackTab?.id) {
if (typeof registerTab === 'function') {
await registerTab(PLUS_CHECKOUT_SOURCE, fallbackTab.id);
}
return fallbackTab.id;
}
throw new Error('未找到可读取 ChatGPT 会话的标签页,请先打开一个已登录的 ChatGPT / OpenAI 页面,或完成当前 Plus 支付链路。');
}
async function getResolvedSessionTab(tabId, visibleStep) {
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
if (!tab?.id) {
throw new Error(`步骤 ${visibleStep}Plus 会话标签页不存在或已关闭,无法继续导入 SUB2API。`);
throw new Error(`步骤 ${visibleStep}ChatGPT 会话标签页不存在或已关闭,无法继续导入 SUB2API。`);
}
if (!isSupportedChatGptSessionUrl(tab.url)) {
throw new Error(`步骤 ${visibleStep}:当前标签页不在 ChatGPT / OpenAI 页面,无法读取当前登录会话。`);
@@ -147,7 +242,7 @@
const visibleStep = resolveVisibleStep(state);
const api = getSub2ApiApi();
await addStepLog(visibleStep, '正在定位当前 Plus 会话页并准备导入 SUB2API...', 'info');
await addStepLog(visibleStep, '正在定位当前 ChatGPT 会话页并准备导入 SUB2API...', 'info');
const tabId = await resolveSessionTabId(state);
const tab = await getResolvedSessionTab(tabId, visibleStep);
if (chrome?.tabs?.update) {
+191 -2
View File
@@ -238,7 +238,196 @@ test('session import step reads current ChatGPT session and completes node', asy
);
});
test('session import step rejects unsupported non-chatgpt tabs before reading session', async () => {
test('session import step falls back to an active ChatGPT tab when no checkout tab is tracked', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const completed = [];
const importedPayloads = [];
const queryCalls = [];
const registerCalls = [];
const sentMessages = [];
const sessionTab = {
id: 77,
url: 'https://chatgpt.com/?model=gpt-4o',
active: true,
currentWindow: true,
lastAccessed: 1234,
};
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => ({
...sessionTab,
id: tabId,
}),
query: async (queryInfo = {}) => {
queryCalls.push(queryInfo);
if (queryInfo.active && queryInfo.currentWindow) {
return [sessionTab];
}
return [sessionTab];
},
update: async () => {},
},
},
completeNodeFromBackground: async (nodeId, payload) => {
completed.push({ nodeId, payload });
},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async (state, options) => {
importedPayloads.push({ state, options });
return {
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
sub2apiImportCreated: 1,
sub2apiImportUpdated: 0,
sub2apiImportSkipped: 0,
sub2apiImportFailed: 0,
};
},
}),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
getTabId: async () => null,
isTabAlive: async () => false,
normalizeSub2ApiUrl: (value) => value,
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
user: {
email: 'fallback@example.com',
},
},
accessToken: 'session-access-token',
};
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabCompleteUntilStopped: async () => {},
DEFAULT_SUB2API_GROUP_NAME: 'codex',
});
await executor.executeSub2ApiSessionImport({
nodeId: 'sub2api-session-import',
visibleStep: 10,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.deepStrictEqual(queryCalls, [
{ active: true, currentWindow: true },
{},
]);
assert.deepStrictEqual(registerCalls, [{
source: 'plus-checkout',
tabId: 77,
}]);
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].tabId, 77);
assert.equal(importedPayloads.length, 1);
assert.equal(importedPayloads[0].state.session.user.email, 'fallback@example.com');
assert.equal(completed.length, 1);
});
test('session import step ignores unusable tracked tabs and prefers a real ChatGPT tab from open tabs', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
const registerCalls = [];
const sentMessages = [];
const tabsById = {
91: {
id: 91,
url: 'https://www.paypal.com/checkoutnow',
active: false,
currentWindow: false,
lastAccessed: 10,
},
101: {
id: 101,
url: 'https://platform.openai.com/settings/profile',
active: true,
currentWindow: true,
lastAccessed: 50,
},
203: {
id: 203,
url: 'https://chatgpt.com/c/abc123',
active: false,
currentWindow: false,
lastAccessed: 40,
},
};
const executor = moduleApi.createSub2ApiSessionImportExecutor({
addLog: async () => {},
chrome: {
tabs: {
get: async (tabId) => tabsById[tabId] || null,
query: async (queryInfo = {}) => {
if (queryInfo.active && queryInfo.currentWindow) {
return [tabsById[101]];
}
return [tabsById[101], tabsById[203]];
},
update: async () => {},
},
},
completeNodeFromBackground: async () => {},
createSub2ApiApi: () => ({
importCurrentChatGptSession: async () => ({
verifiedStatus: 'SUB2API 会话导入完成:新建 1,更新 0,跳过 0,失败 0',
}),
}),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
getTabId: async () => 91,
isTabAlive: async () => true,
normalizeSub2ApiUrl: (value) => value,
registerTab: async (source, tabId) => {
registerCalls.push({ source, tabId });
},
sendTabMessageUntilStopped: async (tabId, source, message) => {
sentMessages.push({ tabId, source, message });
return {
session: {
accessToken: 'session-access-token',
user: {
email: 'best-match@example.com',
},
},
accessToken: 'session-access-token',
};
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
waitForTabCompleteUntilStopped: async () => {},
DEFAULT_SUB2API_GROUP_NAME: 'codex',
});
await executor.executeSub2ApiSessionImport({
nodeId: 'sub2api-session-import',
visibleStep: 10,
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiGroupName: 'codex',
});
assert.deepStrictEqual(registerCalls, [{
source: 'plus-checkout',
tabId: 203,
}]);
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].tabId, 203);
});
test('session import step reports missing readable session tab when tracked tabs are unusable', async () => {
const moduleApi = loadSub2ApiSessionImportModule();
let sendCalled = false;
@@ -275,7 +464,7 @@ test('session import step rejects unsupported non-chatgpt tabs before reading se
nodeId: 'sub2api-session-import',
visibleStep: 10,
}),
/当前标签页不在 ChatGPT \/ OpenAI 页面/
/未找到可读取 ChatGPT 会话的标签页/
);
assert.equal(sendCalled, false);