| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- // 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_PROXY_NAME = 'shadowrocket';
- 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 normalizeSub2ApiProxyPreference(value) {
- return String(value || '').trim();
- }
- function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) {
- if (payload.sub2apiDefaultProxyName !== undefined) {
- return normalizeSub2ApiProxyPreference(payload.sub2apiDefaultProxyName) || SUB2API_DEFAULT_PROXY_NAME;
- }
- if (backgroundState.sub2apiDefaultProxyName !== undefined) {
- return normalizeSub2ApiProxyPreference(backgroundState.sub2apiDefaultProxyName) || SUB2API_DEFAULT_PROXY_NAME;
- }
- return SUB2API_DEFAULT_PROXY_NAME;
- }
- function normalizeProxyId(value) {
- if (value === undefined || value === null || value === '') {
- return null;
- }
- const normalized = Number(value);
- if (!Number.isSafeInteger(normalized) || normalized <= 0) {
- return null;
- }
- return normalized;
- }
- function buildProxyDisplayName(proxy = {}) {
- const id = normalizeProxyId(proxy.id);
- const name = String(proxy.name || '').trim();
- const protocol = String(proxy.protocol || '').trim();
- const host = String(proxy.host || '').trim();
- const port = proxy.port === undefined || proxy.port === null ? '' : String(proxy.port).trim();
- const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
- const parts = [
- name || '(未命名代理)',
- id ? `#${id}` : '',
- address,
- ].filter(Boolean);
- return parts.join(' ');
- }
- function buildProxySearchText(proxy = {}) {
- return [
- proxy.id,
- proxy.name,
- proxy.protocol,
- proxy.host,
- proxy.port,
- buildProxyDisplayName(proxy),
- ]
- .filter((value) => value !== undefined && value !== null && value !== '')
- .map((value) => String(value).trim().toLowerCase())
- .filter(Boolean)
- .join(' ');
- }
- function isActiveProxy(proxy = {}) {
- const status = String(proxy.status || '').trim().toLowerCase();
- return !status || status === 'active';
- }
- function findSub2ApiProxy(proxies = [], preference = '') {
- const activeProxies = (Array.isArray(proxies) ? proxies : [])
- .filter(isActiveProxy)
- .filter((proxy) => normalizeProxyId(proxy.id));
- const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
- const preferredId = normalizeProxyId(normalizedPreference);
- if (preferredId) {
- const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
- return {
- proxy: matchedById || null,
- reason: matchedById ? 'id' : 'missing-id',
- candidates: activeProxies,
- };
- }
- if (normalizedPreference) {
- const exactMatches = activeProxies.filter((proxy) => {
- const name = String(proxy.name || '').trim().toLowerCase();
- return name === normalizedPreference;
- });
- if (exactMatches.length === 1) {
- return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
- }
- if (exactMatches.length > 1) {
- return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
- }
- const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
- if (fuzzyMatches.length === 1) {
- return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
- }
- if (fuzzyMatches.length > 1) {
- return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
- }
- return { proxy: null, reason: 'missing-name', candidates: activeProxies };
- }
- if (activeProxies.length === 1) {
- return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
- }
- return {
- proxy: null,
- reason: activeProxies.length ? 'no-preference' : 'none-active',
- candidates: activeProxies,
- };
- }
- async function resolveSub2ApiProxy(origin, token, preference = '') {
- const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
- method: 'GET',
- token,
- });
- if (!Array.isArray(proxies)) {
- throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
- }
- const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
- if (proxy) {
- return proxy;
- }
- const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
- const available = (candidates || [])
- .slice(0, 8)
- .map(buildProxyDisplayName)
- .join(',') || '无可用代理';
- if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
- throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
- }
- if (reason === 'missing-id') {
- throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
- }
- if (reason === 'missing-name') {
- throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
- }
- if (reason === 'no-preference') {
- throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID。可用代理:${available}`);
- }
- throw new Error('SUB2API 没有可用代理;当前流程要求账号必须绑定代理。');
- }
- 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 proxyPreference = resolveSub2ApiProxyPreference(payload);
- const proxy = await resolveSub2ApiProxy(origin, token, proxyPreference);
- const proxyId = normalizeProxyId(proxy.id);
- const draftName = buildDraftAccountName(group.name || groupName);
- log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${group.name}(#${group.id})。`);
- log(`步骤 ${logStep}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`);
- 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,
- proxy_id: proxyId,
- },
- });
- 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,
- sub2apiProxyId: proxyId,
- };
- 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 proxyPreference = resolveSub2ApiProxyPreference(payload, backgroundState);
- const preferredProxyId = normalizeProxyId(payload.sub2apiProxyId || backgroundState.sub2apiProxyId);
- const proxy = await resolveSub2ApiProxy(origin, token, preferredProxyId || proxyPreference);
- const proxyId = normalizeProxyId(proxy.id);
- 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 授权码...');
- log(`步骤 10:使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}。`);
- const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
- method: 'POST',
- token,
- body: {
- session_id: sessionId,
- code: callback.code,
- state: callback.state,
- proxy_id: proxyId,
- },
- });
- 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,
- proxy_id: proxyId,
- 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();
|