| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- // content/sub2api-panel.js — 页内脚本:SUB2API 后台(步骤 1、9)
- console.log('[MultiPage:sub2api-panel] Content script loaded on', location.href);
- const SUB2API_PANEL_LISTENER_SENTINEL = 'data-multipage-sub2api-panel-listener';
- const SUB2API_DEFAULT_GROUP_NAME = 'codex';
- const SUB2API_DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
- const SUB2API_DEFAULT_CONCURRENCY = 10;
- const SUB2API_DEFAULT_PRIORITY = 1;
- const SUB2API_DEFAULT_RATE_MULTIPLIER = 1;
- if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== '1') {
- document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1');
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- if (message.type === 'EXECUTE_STEP' || message.type === 'REQUEST_OAUTH_URL') {
- resetStopState();
- const handler = message.type === 'REQUEST_OAUTH_URL'
- ? requestOAuthUrl(message.payload)
- : handleStep(message.step, message.payload);
- handler.then((result) => {
- sendResponse({ ok: true, ...(result || {}) });
- }).catch((err) => {
- if (isStopError(err)) {
- if (message.step) {
- log(`步骤 ${message.step}:已被用户停止。`, 'warn');
- }
- sendResponse({ stopped: true, error: err.message });
- return;
- }
- if (message.step) {
- reportError(message.step, err.message);
- }
- sendResponse({ error: err.message });
- });
- return true;
- }
- });
- } else {
- console.log('[MultiPage:sub2api-panel] 消息监听已存在,跳过重复注册');
- }
- function getSub2ApiOrigin(payload = {}) {
- const rawUrl = payload.sub2apiUrl || location.href;
- try {
- return new URL(rawUrl).origin;
- } catch {
- return location.origin;
- }
- }
- function normalizeRedirectUri() {
- const input = SUB2API_DEFAULT_REDIRECT_URI;
- const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
- const parsed = new URL(withProtocol);
- if (!parsed.pathname || parsed.pathname === '/') {
- parsed.pathname = '/auth/callback';
- }
- if (parsed.pathname !== '/auth/callback') {
- throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
- }
- return parsed.toString();
- }
- async function handleStep(step, payload = {}) {
- switch (step) {
- case 1:
- return step1_generateOpenAiAuthUrl(payload);
- case 10:
- return step9_submitOpenAiCallback(payload);
- default:
- throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
- }
- }
- async function requestOAuthUrl(payload = {}) {
- return step1_generateOpenAiAuthUrl(payload, { report: false });
- }
- async function requestJson(origin, path, options = {}) {
- throwIfStopped();
- const {
- method = 'GET',
- token = '',
- body = undefined,
- } = options;
- const response = await fetch(`${origin}${path}`, {
- method,
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json',
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- },
- body: body === undefined ? undefined : JSON.stringify(body),
- });
- const text = await response.text();
- let json = null;
- try {
- json = text ? JSON.parse(text) : null;
- } catch {
- json = null;
- }
- if (json && typeof json === 'object' && 'code' in json) {
- if (json.code === 0) {
- return json.data;
- }
- throw new Error(json.message || json.detail || `请求失败(${path})`);
- }
- if (!response.ok) {
- throw new Error((json && (json.message || json.detail)) || `请求失败(HTTP ${response.status}):${path}`);
- }
- return json;
- }
- function storeAuthSession(loginData) {
- if (!loginData?.access_token) {
- throw new Error('SUB2API 登录返回缺少 access_token。');
- }
- localStorage.setItem('auth_token', loginData.access_token);
- if (loginData.refresh_token) {
- localStorage.setItem('refresh_token', loginData.refresh_token);
- } else {
- localStorage.removeItem('refresh_token');
- }
- if (loginData.expires_in) {
- localStorage.setItem('token_expires_at', String(Date.now() + Number(loginData.expires_in) * 1000));
- }
- if (loginData.user) {
- localStorage.setItem('auth_user', JSON.stringify(loginData.user));
- }
- sessionStorage.removeItem('auth_expired');
- }
- async function loginSub2Api(payload = {}) {
- const email = (payload.sub2apiEmail || '').trim();
- const password = payload.sub2apiPassword || '';
- const origin = getSub2ApiOrigin(payload);
- if (!email) {
- throw new Error('缺少 SUB2API 登录邮箱,请先在侧边栏填写。');
- }
- if (!password) {
- throw new Error('缺少 SUB2API 登录密码,请先在侧边栏填写。');
- }
- log('步骤:正在登录 SUB2API 后台...');
- const loginData = await requestJson(origin, '/api/v1/auth/login', {
- method: 'POST',
- body: {
- email,
- password,
- },
- });
- storeAuthSession(loginData);
- return {
- origin,
- token: loginData.access_token,
- user: loginData.user || null,
- };
- }
- async function getGroupByName(origin, token, groupName) {
- const targetName = (groupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
- const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
- method: 'GET',
- token,
- });
- const normalized = targetName.toLowerCase();
- const group = (groups || []).find((item) => {
- const itemName = String(item?.name || '').trim().toLowerCase();
- if (!itemName) return false;
- if (itemName !== normalized) return false;
- return !item.platform || item.platform === 'openai';
- });
- if (!group) {
- throw new Error(`SUB2API 中未找到名为“${targetName}”的 openai 分组。`);
- }
- return group;
- }
- function buildDraftAccountName(groupName) {
- const prefix = (groupName || SUB2API_DEFAULT_GROUP_NAME)
- .trim()
- .replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
- .replace(/^-+|-+$/g, '') || SUB2API_DEFAULT_GROUP_NAME;
- const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
- const random = Math.floor(Math.random() * 9000 + 1000);
- return `${prefix}-${stamp}-${random}`;
- }
- function extractStateFromAuthUrl(authUrl) {
- try {
- return new URL(authUrl).searchParams.get('state') || '';
- } catch {
- return '';
- }
- }
- function parseLocalhostCallback(rawUrl) {
- let parsed;
- try {
- parsed = new URL(rawUrl);
- } catch {
- throw new Error('提供的回调 URL 不是合法链接。');
- }
- if (!['http:', 'https:'].includes(parsed.protocol)) {
- throw new Error('回调 URL 协议不正确。');
- }
- if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
- throw new Error('步骤 10 只接受 localhost / 127.0.0.1 回调地址。');
- }
- if (parsed.pathname !== '/auth/callback') {
- throw new Error('回调 URL 路径必须是 /auth/callback。');
- }
- const code = (parsed.searchParams.get('code') || '').trim();
- const state = (parsed.searchParams.get('state') || '').trim();
- if (!code || !state) {
- throw new Error('回调 URL 中缺少 code 或 state。');
- }
- return {
- url: parsed.toString(),
- code,
- state,
- };
- }
- function buildOpenAiCredentials(exchangeData) {
- const credentials = {};
- const allowedKeys = [
- 'access_token',
- 'refresh_token',
- 'id_token',
- 'expires_at',
- 'email',
- 'chatgpt_account_id',
- 'chatgpt_user_id',
- 'organization_id',
- 'plan_type',
- 'client_id',
- ];
- for (const key of allowedKeys) {
- if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
- credentials[key] = exchangeData[key];
- }
- }
- if (!credentials.access_token) {
- throw new Error('SUB2API 交换授权码后未返回 access_token。');
- }
- return credentials;
- }
- function buildOpenAiExtra(exchangeData) {
- const extra = {};
- const allowedKeys = ['email', 'name', 'privacy_mode'];
- for (const key of allowedKeys) {
- if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
- extra[key] = exchangeData[key];
- }
- }
- return Object.keys(extra).length ? extra : undefined;
- }
- async function getBackgroundState() {
- try {
- return await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sub2api-panel' });
- } catch {
- return {};
- }
- }
- function openAccountsPageSoon(origin) {
- const accountsUrl = `${origin}/admin/accounts`;
- if (location.href === accountsUrl || location.pathname.startsWith('/admin/accounts')) {
- return;
- }
- setTimeout(() => {
- try {
- location.replace(accountsUrl);
- } catch { }
- }, 500);
- }
- async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
- const { report = true } = options;
- const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
- const redirectUri = normalizeRedirectUri();
- const groupName = (payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
- const { origin, token } = await loginSub2Api(payload);
- const group = await getGroupByName(origin, token, groupName);
- const draftName = buildDraftAccountName(group.name || groupName);
- log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
- log(`步骤 ${logStep}:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}。`);
- const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
- method: 'POST',
- token,
- body: {
- redirect_uri: redirectUri,
- },
- });
- const oauthUrl = String(authData?.auth_url || '').trim();
- const sessionId = String(authData?.session_id || '').trim();
- const oauthState = String(authData?.state || extractStateFromAuthUrl(oauthUrl)).trim();
- if (!oauthUrl || !sessionId) {
- throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
- }
- log(`步骤 ${logStep}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
- const result = {
- oauthUrl,
- sub2apiSessionId: sessionId,
- sub2apiOAuthState: oauthState,
- sub2apiGroupId: group.id,
- sub2apiDraftName: draftName,
- };
- if (report) {
- reportComplete(1, result);
- }
- openAccountsPageSoon(origin);
- return result;
- }
- async function step9_submitOpenAiCallback(payload = {}) {
- const callback = parseLocalhostCallback(payload.localhostUrl || '');
- const backgroundState = await getBackgroundState();
- const flowEmail = String(backgroundState.email || '').trim();
- const sessionId = String(payload.sub2apiSessionId || backgroundState.sub2apiSessionId || '').trim();
- const expectedState = String(payload.sub2apiOAuthState || backgroundState.sub2apiOAuthState || '').trim();
- const accountName = flowEmail
- || String(payload.sub2apiDraftName || backgroundState.sub2apiDraftName || '').trim()
- || buildDraftAccountName(payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
- const { origin, token } = await loginSub2Api(payload);
- const group = payload.sub2apiGroupId
- ? { id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }
- : await getGroupByName(origin, token, payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
- if (!sessionId) {
- throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
- }
- if (expectedState && expectedState !== callback.state) {
- throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
- }
- log('步骤 10:正在向 SUB2API 交换 OpenAI 授权码...');
- const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
- method: 'POST',
- token,
- body: {
- session_id: sessionId,
- code: callback.code,
- state: callback.state,
- },
- });
- const credentials = buildOpenAiCredentials(exchangeData);
- const extra = buildOpenAiExtra(exchangeData);
- const groupId = Number(group.id);
- if (!Number.isFinite(groupId) || groupId <= 0) {
- throw new Error('SUB2API 返回的目标分组 ID 无效。');
- }
- const createPayload = {
- name: accountName,
- notes: '',
- platform: 'openai',
- type: 'oauth',
- credentials,
- concurrency: SUB2API_DEFAULT_CONCURRENCY,
- priority: SUB2API_DEFAULT_PRIORITY,
- rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
- group_ids: [groupId],
- auto_pause_on_expired: true,
- };
- if (extra) {
- createPayload.extra = extra;
- }
- log(`步骤 10:授权码交换成功,正在创建 SUB2API 账号(名称:${accountName})...`);
- const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
- method: 'POST',
- token,
- body: createPayload,
- });
- const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
- log(`步骤 10:${verifiedStatus}`, 'ok');
- reportComplete(10, {
- localhostUrl: callback.url,
- verifiedStatus,
- });
- openAccountsPageSoon(origin);
- }
- reportReady();
|