Allow SUB2API session import from open ChatGPT tabs
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user