fix sub2api session import account naming
This commit is contained in:
@@ -349,6 +349,68 @@
|
|||||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
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 = '') {
|
function buildCodexSessionImportContent(session, accessToken = '') {
|
||||||
const normalizedAccessToken = normalizeString(accessToken);
|
const normalizedAccessToken = normalizeString(accessToken);
|
||||||
const sessionObject = normalizeCodexSessionObject(session);
|
const sessionObject = normalizeCodexSessionObject(session);
|
||||||
@@ -666,6 +728,7 @@
|
|||||||
);
|
);
|
||||||
const importContent = buildCodexSessionImportContent(session, accessToken);
|
const importContent = buildCodexSessionImportContent(session, accessToken);
|
||||||
const importExpiresAt = resolveCodexSessionImportExpiresAt(session);
|
const importExpiresAt = resolveCodexSessionImportExpiresAt(session);
|
||||||
|
const preferredAccountName = resolveCodexSessionImportAccountName(state, session, accessToken);
|
||||||
|
|
||||||
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并准备导入当前 ChatGPT 会话...`, 'info', options);
|
await logWithOptions(`${logLabel}:正在通过 SUB2API 管理接口登录并准备导入当前 ChatGPT 会话...`, 'info', options);
|
||||||
const { origin, token } = await loginSub2Api(state, options);
|
const { origin, token } = await loginSub2Api(state, options);
|
||||||
@@ -689,6 +752,7 @@
|
|||||||
group_ids: groups
|
group_ids: groups
|
||||||
.map((group) => Number(group?.id))
|
.map((group) => Number(group?.id))
|
||||||
.filter((id) => Number.isFinite(id) && id > 0),
|
.filter((id) => Number.isFinite(id) && id > 0),
|
||||||
|
...(preferredAccountName ? { name: preferredAccountName } : {}),
|
||||||
priority: accountPriority,
|
priority: accountPriority,
|
||||||
auto_pause_on_expired: true,
|
auto_pause_on_expired: true,
|
||||||
update_existing: true,
|
update_existing: true,
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ function loadSub2ApiSessionImportModule() {
|
|||||||
return new Function('self', `${source}; return self.MultiPageBackgroundSub2ApiSessionImport;`)({});
|
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 () => {
|
test('sub2api api imports current ChatGPT session through codex-session endpoint', async () => {
|
||||||
const apiModule = loadSub2ApiApiModule();
|
const apiModule = loadSub2ApiApiModule();
|
||||||
const fetchCalls = [];
|
const fetchCalls = [];
|
||||||
@@ -70,6 +85,7 @@ test('sub2api api imports current ChatGPT session through codex-session endpoint
|
|||||||
const parsedContent = JSON.parse(body.content);
|
const parsedContent = JSON.parse(body.content);
|
||||||
assert.equal(parsedContent.accessToken, 'access-token-from-state');
|
assert.equal(parsedContent.accessToken, 'access-token-from-state');
|
||||||
assert.equal(parsedContent.user?.email, 'flow@example.com');
|
assert.equal(parsedContent.user?.email, 'flow@example.com');
|
||||||
|
assert.equal(body.name, 'flow@example.com');
|
||||||
assert.equal(body.priority, 3);
|
assert.equal(body.priority, 3);
|
||||||
assert.equal(body.proxy_id, 7);
|
assert.equal(body.proxy_id, 7);
|
||||||
assert.deepStrictEqual(body.group_ids, [5]);
|
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',
|
expires: '2026-05-20T12:34:56.000Z',
|
||||||
user: {
|
user: {
|
||||||
email: 'flow@example.com',
|
email: 'flow@example.com',
|
||||||
|
name: 'dyson willion',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
accessToken: 'access-token-from-state',
|
accessToken: 'access-token-from-state',
|
||||||
|
email: 'registration@example.com',
|
||||||
}, {
|
}, {
|
||||||
logLabel: '步骤 10',
|
logLabel: '步骤 10',
|
||||||
logOptions: { step: 10, stepKey: 'sub2api-session-import' },
|
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 () => {
|
test('session import step reads current ChatGPT session and completes node', async () => {
|
||||||
const moduleApi = loadSub2ApiSessionImportModule();
|
const moduleApi = loadSub2ApiSessionImportModule();
|
||||||
const completed = [];
|
const completed = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user