From 56fcbbdc30b707f540016e2a3742d67a0a387705 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 19 May 2026 14:26:06 +0800 Subject: [PATCH] fix sub2api session import account naming --- background/sub2api-api.js | 64 +++++++++ .../background-sub2api-session-import.test.js | 127 ++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/background/sub2api-api.js b/background/sub2api-api.js index 2c8c97d..6e3cf90 100644 --- a/background/sub2api-api.js +++ b/background/sub2api-api.js @@ -349,6 +349,68 @@ return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } + function normalizeEmailValue(value = '') { + const email = normalizeString(value); + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? email : ''; + } + + function decodeCodexBase64UrlSegment(segment = '') { + const normalized = normalizeString(segment) + .replace(/-/g, '+') + .replace(/_/g, '/'); + if (!normalized) { + return ''; + } + const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4); + try { + if (typeof Buffer !== 'undefined') { + return Buffer.from(padded, 'base64').toString('utf8'); + } + if (typeof atob === 'function') { + const binary = atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + if (typeof TextDecoder !== 'undefined') { + return new TextDecoder().decode(bytes); + } + return binary; + } + } catch { + return ''; + } + return ''; + } + + function parseCodexAccessTokenClaims(accessToken = '') { + const token = normalizeString(accessToken); + if (!token) { + return null; + } + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } + try { + return JSON.parse(decodeCodexBase64UrlSegment(parts[1])); + } catch { + return null; + } + } + + function resolveCodexSessionImportAccountName(state = {}, session = null, accessToken = '') { + const sessionObject = normalizeCodexSessionObject(session); + const claims = parseCodexAccessTokenClaims(accessToken || sessionObject?.accessToken); + const accountIdentifierType = normalizeString(state?.accountIdentifierType).toLowerCase(); + const accountIdentifierEmail = accountIdentifierType === 'email' + ? normalizeEmailValue(state?.accountIdentifier) + : ''; + + return normalizeEmailValue(sessionObject?.user?.email) + || normalizeEmailValue(sessionObject?.email) + || normalizeEmailValue(claims?.email) + || normalizeEmailValue(state?.email) + || accountIdentifierEmail; + } + function buildCodexSessionImportContent(session, accessToken = '') { const normalizedAccessToken = normalizeString(accessToken); const sessionObject = normalizeCodexSessionObject(session); @@ -666,6 +728,7 @@ ); const importContent = buildCodexSessionImportContent(session, accessToken); const importExpiresAt = resolveCodexSessionImportExpiresAt(session); + const preferredAccountName = resolveCodexSessionImportAccountName(state, session, accessToken); await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并准备导入当前 ChatGPT 会话...`, 'info', options); const { origin, token } = await loginSub2Api(state, options); @@ -689,6 +752,7 @@ group_ids: groups .map((group) => Number(group?.id)) .filter((id) => Number.isFinite(id) && id > 0), + ...(preferredAccountName ? { name: preferredAccountName } : {}), priority: accountPriority, auto_pause_on_expired: true, update_existing: true, diff --git a/tests/background-sub2api-session-import.test.js b/tests/background-sub2api-session-import.test.js index 3790344..f5a8783 100644 --- a/tests/background-sub2api-session-import.test.js +++ b/tests/background-sub2api-session-import.test.js @@ -20,6 +20,21 @@ function loadSub2ApiSessionImportModule() { return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiSessionImport;`)({}); } +function encodeBase64UrlJson(value) { + return Buffer.from(JSON.stringify(value)).toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); +} + +function createJwtToken(payload = {}) { + return [ + encodeBase64UrlJson({ alg: 'HS256', typ: 'JWT' }), + encodeBase64UrlJson(payload), + 'signature', + ].join('.'); +} + test('sub2api api imports current ChatGPT session through codex-session endpoint', async () => { const apiModule = loadSub2ApiApiModule(); const fetchCalls = []; @@ -70,6 +85,7 @@ test('sub2api api imports current ChatGPT session through codex-session endpoint const parsedContent = JSON.parse(body.content); assert.equal(parsedContent.accessToken, 'access-token-from-state'); assert.equal(parsedContent.user?.email, 'flow@example.com'); + assert.equal(body.name, 'flow@example.com'); assert.equal(body.priority, 3); assert.equal(body.proxy_id, 7); assert.deepStrictEqual(body.group_ids, [5]); @@ -108,9 +124,11 @@ test('sub2api api imports current ChatGPT session through codex-session endpoint expires: '2026-05-20T12:34:56.000Z', user: { email: 'flow@example.com', + name: 'dyson willion', }, }, accessToken: 'access-token-from-state', + email: 'registration@example.com', }, { logLabel: '步骤 10', logOptions: { step: 10, stepKey: 'sub2api-session-import' }, @@ -127,6 +145,115 @@ test('sub2api api imports current ChatGPT session through codex-session endpoint ); }); +test('sub2api session import falls back to JWT email before registration email', async () => { + const apiModule = loadSub2ApiApiModule(); + const jwtToken = createJwtToken({ email: 'jwt@example.com' }); + let importBody = null; + + const api = apiModule.createSub2ApiApi({ + addLog: async () => {}, + normalizeSub2ApiUrl: (value) => value, + DEFAULT_SUB2API_GROUP_NAME: 'codex', + fetchImpl: async (url, options = {}) => { + const parsed = new URL(url); + const body = options.body ? JSON.parse(options.body) : null; + + if (parsed.pathname === '/api/v1/auth/login') { + return createJsonResponse({ code: 0, data: { access_token: 'admin-token' } }); + } + if (parsed.pathname === '/api/v1/admin/groups/all') { + return createJsonResponse({ + code: 0, + data: [{ id: 5, name: 'codex', platform: 'openai' }], + }); + } + if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') { + importBody = body; + return createJsonResponse({ + code: 0, + data: { total: 1, created: 1, updated: 0, skipped: 0, failed: 0 }, + }); + } + + return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404); + }, + }); + + await api.importCurrentChatGptSession({ + sub2apiUrl: 'https://sub.example/admin/accounts', + sub2apiEmail: 'admin@example.com', + sub2apiPassword: 'secret', + sub2apiGroupName: 'codex', + session: { + accessToken: jwtToken, + user: { + name: 'dyson willion', + }, + }, + accessToken: jwtToken, + email: 'registration@example.com', + accountIdentifierType: 'email', + accountIdentifier: 'identifier@example.com', + }); + + assert.ok(importBody, 'expected codex-session import call'); + assert.equal(importBody.name, 'jwt@example.com'); +}); + +test('sub2api session import falls back to registration email when session has no readable email', async () => { + const apiModule = loadSub2ApiApiModule(); + let importBody = null; + + const api = apiModule.createSub2ApiApi({ + addLog: async () => {}, + normalizeSub2ApiUrl: (value) => value, + DEFAULT_SUB2API_GROUP_NAME: 'codex', + fetchImpl: async (url, options = {}) => { + const parsed = new URL(url); + const body = options.body ? JSON.parse(options.body) : null; + + if (parsed.pathname === '/api/v1/auth/login') { + return createJsonResponse({ code: 0, data: { access_token: 'admin-token' } }); + } + if (parsed.pathname === '/api/v1/admin/groups/all') { + return createJsonResponse({ + code: 0, + data: [{ id: 5, name: 'codex', platform: 'openai' }], + }); + } + if (parsed.pathname === '/api/v1/admin/accounts/import/codex-session') { + importBody = body; + return createJsonResponse({ + code: 0, + data: { total: 1, created: 1, updated: 0, skipped: 0, failed: 0 }, + }); + } + + return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404); + }, + }); + + await api.importCurrentChatGptSession({ + sub2apiUrl: 'https://sub.example/admin/accounts', + sub2apiEmail: 'admin@example.com', + sub2apiPassword: 'secret', + sub2apiGroupName: 'codex', + session: { + accessToken: 'opaque-token', + user: { + name: 'dyson willion', + }, + }, + accessToken: 'opaque-token', + email: 'registration@example.com', + accountIdentifierType: 'email', + accountIdentifier: 'identifier@example.com', + }); + + assert.ok(importBody, 'expected codex-session import call'); + assert.equal(importBody.name, 'registration@example.com'); +}); + test('session import step reads current ChatGPT session and completes node', async () => { const moduleApi = loadSub2ApiSessionImportModule(); const completed = [];