// ==UserScript==
// @name ChatGPT Team 长链接生成器(小红书前后网络)
// @namespace https://chatgpt.com/
// @version 1.0.5
// @description 在 chatgpt.com 任意页面生成 ChatGPT Team hosted checkout 长链接,默认澳洲 firstfocus,支持优惠码自动匹配国家/币种、自定义优惠码按钮、手动填入 /api/auth/session 内容、居中方形可拖拽窗口。
// @author 小红书前后网络
// @match https://chatgpt.com/*
// @match https://www.chatgpt.com/*
// @include /^https:\/\/chatgpt\.com(?:\/.*)?$/
// @include /^https:\/\/www\.chatgpt\.com(?:\/.*)?$/
// @run-at document-start
// @grant GM_addStyle
// @grant GM_setClipboard
// ==/UserScript==
(function () {
'use strict';
const VERSION = '1.0.5';
const ROOT_ID = 'tm-team-link-root';
const LAUNCHER_ID = 'tm-team-link-launcher';
const MASK_ID = 'tm-team-link-mask';
const PANEL_ID = 'tm-team-link-panel';
const HEADER_ID = 'tm-team-link-header';
const SESSION_URL = 'https://chatgpt.com/api/auth/session';
const CHECKOUT_URL = 'https://chatgpt.com/backend-api/payments/checkout';
const COUPONS = [
{ group: '澳洲优惠码', label: '澳洲 · firstfocus(默认)', promoCode: 'firstfocus', country: 'AU', currency: 'AUD' },
{ group: '美国优惠码', label: '美国 · talentgeniusus', promoCode: 'talentgeniusus', country: 'US', currency: 'USD' },
{ group: '美国优惠码', label: '美国 · thealloynetwork', promoCode: 'thealloynetwork', country: 'US', currency: 'USD' },
{ group: '美国优惠码', label: '美国 · alongsideus', promoCode: 'alongsideus', country: 'US', currency: 'USD' },
{ group: '美国优惠码', label: '美国 · monicaius', promoCode: 'monicaius', country: 'US', currency: 'USD' },
{ group: '英国优惠码', label: '英国 · aibuildgroupgb', promoCode: 'aibuildgroupgb', country: 'GB', currency: 'GBP' },
{ group: '英国优惠码', label: '英国 · geccogb', promoCode: 'geccogb', country: 'GB', currency: 'GBP' },
{ group: '英国优惠码', label: '英国 · codestonegb', promoCode: 'codestonegb', country: 'GB', currency: 'GBP' },
{ group: '肯尼亚优惠码', label: '肯尼亚 · wildmangoke', promoCode: 'wildmangoke', country: 'KE', currency: 'USD' },
];
const state = {
lastUrl: '',
lastData: null,
lastPayload: null,
dragged: false,
};
function addStyle(css) {
if (typeof GM_addStyle === 'function') {
GM_addStyle(css);
return;
}
const style = document.createElement('style');
style.textContent = css;
document.documentElement.appendChild(style);
}
addStyle(`
#${LAUNCHER_ID} {
position: fixed !important;
right: 22px !important;
bottom: 22px !important;
z-index: 2147483644 !important;
border: 0 !important;
border-radius: 999px !important;
padding: 13px 18px !important;
background: #111827 !important;
color: #fff !important;
font: 600 14px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
box-shadow: 0 16px 45px rgba(0,0,0,.24) !important;
cursor: pointer !important;
user-select: none !important;
}
#${LAUNCHER_ID}:hover { filter: brightness(1.08) !important; }
#${MASK_ID} {
display: none;
position: fixed;
inset: 0;
z-index: 2147483645;
background: rgba(17, 24, 39, .36);
backdrop-filter: blur(2px);
}
#${PANEL_ID} {
display: none;
position: fixed;
z-index: 2147483646;
width: min(720px, calc(100vw - 24px));
height: min(720px, calc(100vh - 24px));
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
box-sizing: border-box;
background: #fff;
color: #111827;
border: 1px solid #d9d9e3;
border-radius: 20px;
box-shadow: 0 26px 90px rgba(0, 0, 0, .26);
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
#${PANEL_ID} * { box-sizing: border-box; }
#${HEADER_ID} {
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 16px;
border-bottom: 1px solid #ececf1;
background: #fff;
cursor: move;
user-select: none;
}
.tm-team-title {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.tm-team-title strong {
font-size: 17px;
line-height: 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tm-team-title span {
font-size: 12px;
color: #6b7280;
}
.tm-team-header-actions {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.tm-icon-btn {
width: 34px;
height: 34px;
border: 0;
border-radius: 10px;
background: #f3f4f6;
color: #111827;
cursor: pointer;
font-size: 18px;
line-height: 1;
}
.tm-icon-btn:hover { background: #e5e7eb; }
.tm-team-body {
height: calc(100% - 128px);
overflow: auto;
padding: 16px;
background: #fff;
}
.tm-team-footer {
height: 70px;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-top: 1px solid #ececf1;
background: #fff;
}
.tm-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.tm-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.tm-field-full { grid-column: 1 / -1; }
.tm-label { font-weight: 700; color: #1f2937; }
.tm-hint { color: #6b7280; font-size: 12px; }
.tm-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#${PANEL_ID} input,
#${PANEL_ID} select,
#${PANEL_ID} textarea {
width: 100%;
min-height: 44px;
border: 1px solid #d1d5db;
border-radius: 12px;
padding: 10px 12px;
outline: none;
background: #fff;
color: #111827;
font: 14px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
#${PANEL_ID} textarea {
min-height: 92px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 12.5px;
}
#${PANEL_ID} input:focus,
#${PANEL_ID} select:focus,
#${PANEL_ID} textarea:focus {
border-color: #111827;
box-shadow: 0 0 0 3px rgba(17, 24, 39, .10);
}
.tm-card, #${PANEL_ID} details {
border: 1px solid #ececf1;
border-radius: 14px;
background: #fafafa;
padding: 12px;
margin: 12px 0;
}
#${PANEL_ID} details summary {
cursor: pointer;
font-weight: 800;
color: #1f2937;
user-select: none;
}
#${PANEL_ID} details .tm-details-inner { padding-top: 12px; }
.tm-status {
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #f9fafb;
padding: 10px 12px;
color: #374151;
margin-top: 10px;
min-height: 40px;
white-space: pre-wrap;
word-break: break-word;
}
.tm-status.ok { border-color: #bbf7d0; background: #f0fdf4; color: #166534; }
.tm-status.err { border-color: #fecaca; background: #fef2f2; color: #991b1b; }
.tm-status.warn { border-color: #fde68a; background: #fffbeb; color: #92400e; }
.tm-btn {
min-height: 44px;
border: 0;
border-radius: 12px;
padding: 10px 14px;
cursor: pointer;
font-weight: 800;
white-space: nowrap;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.tm-primary { background: #111827; color: #fff; }
.tm-secondary { background: #f3f4f6; color: #111827; }
.tm-danger { background: #b91c1c; color: #fff; }
.tm-btn:disabled { opacity: .48; cursor: not-allowed; }
.tm-table-wrap { overflow: auto; max-height: 220px; border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; }
.tm-table { width: 100%; border-collapse: collapse; min-width: 520px; }
.tm-table th, .tm-table td { padding: 9px 10px; border-bottom: 1px solid #f1f5f9; text-align: left; font-size: 12px; }
.tm-table th { position: sticky; top: 0; background: #f9fafb; color: #374151; }
.tm-table code { font-size: 12px; }
@media (max-width: 680px) {
#${PANEL_ID} {
width: calc(100vw - 16px);
height: calc(100vh - 16px);
border-radius: 16px;
}
.tm-grid { grid-template-columns: 1fr; }
.tm-team-footer { overflow-x: auto; }
#${LAUNCHER_ID} { right: 14px !important; bottom: 14px !important; }
}
`);
function waitForBody(cb) {
if (document.body) {
cb();
return;
}
const timer = setInterval(() => {
if (document.body) {
clearInterval(timer);
cb();
}
}, 80);
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function getEl(id) {
return document.getElementById(id);
}
function getCouponByCode(code) {
return COUPONS.find((x) => x.promoCode === code) || COUPONS[0];
}
function couponOptionsHtml() {
const groups = [...new Set(COUPONS.map((x) => x.group))];
return groups.map((group) => {
const options = COUPONS
.filter((x) => x.group === group)
.map((x) => ``)
.join('');
return ``;
}).join('');
}
function couponTableHtml() {
return COUPONS.map((x) => `
| ${escapeHtml(x.group.replace('优惠码', ''))} |
${escapeHtml(x.promoCode)} |
${escapeHtml(x.country)} |
${escapeHtml(x.currency)} |
`).join('');
}
function ensureUi() {
if (getEl(ROOT_ID) && getEl(LAUNCHER_ID) && getEl(PANEL_ID)) return;
const oldRoot = getEl(ROOT_ID);
if (oldRoot) oldRoot.remove();
const root = document.createElement('div');
root.id = ROOT_ID;
root.innerHTML = `
高级设置 / 国家币种手动覆盖
Session Token 来源 / 支持手动填入
查看优惠码对照表
| 国家/地区 | 优惠码 | country | currency |
${couponTableHtml()}
准备就绪。选择优惠码后会自动匹配国家和币种。
提示:生成后请在 checkout 页面核对国家、币种、价格和优惠是否正确。不要使用不适用于你真实计费信息的优惠。
`;
document.body.appendChild(root);
bindEvents();
refreshCouponHint();
}
function setStatus(message, type = '') {
const el = getEl('tm-status');
if (!el) return;
el.textContent = message;
el.className = `tm-status${type ? ' ' + type : ''}`;
}
function centerPanel() {
const panel = getEl(PANEL_ID);
if (!panel) return;
const w = Math.min(720, window.innerWidth - 24);
const h = Math.min(720, window.innerHeight - 24);
panel.style.width = `${Math.max(320, w)}px`;
panel.style.height = `${Math.max(420, h)}px`;
panel.style.left = `${Math.max(8, (window.innerWidth - Math.max(320, w)) / 2)}px`;
panel.style.top = `${Math.max(8, (window.innerHeight - Math.max(420, h)) / 2)}px`;
panel.style.transform = 'none';
state.dragged = false;
}
function clampPanelIntoView() {
const panel = getEl(PANEL_ID);
if (!panel || panel.style.display === 'none') return;
const rect = panel.getBoundingClientRect();
const left = Math.max(8, Math.min(rect.left, window.innerWidth - rect.width - 8));
const top = Math.max(8, Math.min(rect.top, window.innerHeight - rect.height - 8));
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
panel.style.transform = 'none';
}
function showPanel() {
ensureUi();
const mask = getEl(MASK_ID);
const panel = getEl(PANEL_ID);
if (!mask || !panel) return;
mask.style.display = 'block';
panel.style.display = 'block';
centerPanel();
}
function hidePanel() {
const mask = getEl(MASK_ID);
const panel = getEl(PANEL_ID);
if (mask) mask.style.display = 'none';
if (panel) panel.style.display = 'none';
}
function bindEvents() {
getEl(LAUNCHER_ID)?.addEventListener('click', showPanel);
getEl(MASK_ID)?.addEventListener('click', hidePanel);
getEl('tm-close-panel')?.addEventListener('click', hidePanel);
getEl('tm-center-panel')?.addEventListener('click', centerPanel);
getEl('tm-coupon-select')?.addEventListener('change', refreshCouponHint);
getEl('tm-apply-custom-promo')?.addEventListener('click', applyCustomPromo);
getEl('tm-generate')?.addEventListener('click', generateLink);
getEl('tm-copy')?.addEventListener('click', copyResult);
getEl('tm-open')?.addEventListener('click', openResult);
getEl('tm-clear')?.addEventListener('click', clearResult);
getEl('tm-fill-session')?.addEventListener('click', fillSessionManually);
getEl('tm-clear-session')?.addEventListener('click', () => {
const ta = getEl('tm-manual-session');
if (ta) ta.value = '';
setStatus('已清空手动 Session 内容。');
});
enablePanelDrag(getEl(PANEL_ID), getEl(HEADER_ID));
}
function refreshCouponHint() {
const select = getEl('tm-coupon-select');
const hint = getEl('tm-coupon-hint');
if (!select || !hint) return;
const cfg = getCouponByCode(select.value);
const promoOverride = (getEl('tm-promo-override')?.value || '').trim();
const countryOverride = (getEl('tm-country-override')?.value || '').trim().toUpperCase();
const currencyOverride = (getEl('tm-currency-override')?.value || '').trim().toUpperCase();
if (promoOverride) {
hint.textContent = `当前使用自定义/覆盖:country=${countryOverride || cfg.country},currency=${currencyOverride || cfg.currency},promo_code=${promoOverride}`;
} else {
hint.textContent = `当前匹配:country=${cfg.country},currency=${cfg.currency},promo_code=${cfg.promoCode}`;
}
}
function applyCustomPromo() {
const code = (getEl('tm-custom-promo')?.value || '').trim();
if (!code) {
setStatus('请先填写自定义优惠码。', 'warn');
return;
}
const selectedCode = getEl('tm-coupon-select')?.value || COUPONS[0].promoCode;
const cfg = getCouponByCode(selectedCode);
const country = (getEl('tm-custom-country')?.value || cfg.country || '').trim().toUpperCase();
const currency = (getEl('tm-custom-currency')?.value || cfg.currency || '').trim().toUpperCase();
const promoOverride = getEl('tm-promo-override');
const countryOverride = getEl('tm-country-override');
const currencyOverride = getEl('tm-currency-override');
if (promoOverride) promoOverride.value = code;
if (countryOverride) countryOverride.value = country;
if (currencyOverride) currencyOverride.value = currency;
refreshCouponHint();
setStatus(`已应用自定义优惠码:${code},国家/币种:${country}/${currency}。现在点击“生成长链接”即可。`, 'ok');
}
function enablePanelDrag(panel, handle) {
if (!panel || !handle || panel.dataset.dragBound === '1') return;
panel.dataset.dragBound = '1';
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
handle.addEventListener('mousedown', (event) => {
if (event.target.closest('button,input,textarea,select,summary')) return;
dragging = true;
state.dragged = true;
const rect = panel.getBoundingClientRect();
startX = event.clientX;
startY = event.clientY;
startLeft = rect.left;
startTop = rect.top;
panel.style.transform = 'none';
document.body.style.userSelect = 'none';
event.preventDefault();
});
window.addEventListener('mousemove', (event) => {
if (!dragging) return;
const rect = panel.getBoundingClientRect();
let left = startLeft + event.clientX - startX;
let top = startTop + event.clientY - startY;
left = Math.max(8, Math.min(left, window.innerWidth - rect.width - 8));
top = Math.max(8, Math.min(top, window.innerHeight - rect.height - 8));
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
});
window.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
document.body.style.userSelect = '';
});
}
function normalizeToken(raw) {
return String(raw || '')
.trim()
.replace(/^Bearer\s+/i, '')
.replace(/^['"]|['"]$/g, '')
.trim();
}
function extractAccessTokenFromManualInput(input) {
const text = String(input || '').trim();
if (!text) throw new Error('手动模式已开启,但没有填写 Session JSON 或 accessToken。');
try {
const obj = JSON.parse(text);
const token = normalizeToken(obj?.accessToken || obj?.token || obj?.session?.accessToken);
if (token) return token;
} catch (_) {
// 不是标准 JSON 时,继续尝试正则或纯 token。
}
const match = text.match(/["']accessToken["']\s*:\s*["']([^"']+)["']/);
if (match?.[1]) return normalizeToken(match[1]);
const direct = normalizeToken(text);
if (direct.length > 20) return direct;
throw new Error('无法从手动内容里解析 accessToken,请粘贴完整 JSON 或纯 accessToken。');
}
async function getAutoAccessToken() {
const resp = await window.fetch(SESSION_URL, {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' },
});
const text = await resp.text();
let data;
try {
data = JSON.parse(text);
} catch (_) {
throw new Error(`Session 响应不是 JSON,HTTP ${resp.status}`);
}
if (!resp.ok) {
throw new Error(`获取 Session 失败,HTTP ${resp.status}:${JSON.stringify(data).slice(0, 300)}`);
}
const token = normalizeToken(data?.accessToken);
if (!token) throw new Error('Session 里没有 accessToken,请确认已经登录 ChatGPT。');
return { token, data };
}
async function getAccessToken() {
const mode = getEl('tm-token-mode')?.value || 'auto';
if (mode === 'manual') {
const token = extractAccessTokenFromManualInput(getEl('tm-manual-session')?.value || '');
return { token, data: null, source: 'manual' };
}
const { token, data } = await getAutoAccessToken();
return { token, data, source: 'auto' };
}
async function fillSessionManually() {
const btn = getEl('tm-fill-session');
try {
if (btn) btn.disabled = true;
setStatus('正在读取 /api/auth/session ...');
const { data } = await getAutoAccessToken();
const ta = getEl('tm-manual-session');
const mode = getEl('tm-token-mode');
if (ta) ta.value = JSON.stringify(data, null, 2);
if (mode) mode.value = 'manual';
setStatus('已自动读取并填入 Session JSON,当前已切换为手动模式。', 'ok');
} catch (err) {
setStatus(`读取 Session 失败:${err.message}`, 'err');
} finally {
if (btn) btn.disabled = false;
}
}
function buildPayload() {
const selectedCode = getEl('tm-coupon-select')?.value || COUPONS[0].promoCode;
const cfg = getCouponByCode(selectedCode);
const workspaceName = (getEl('tm-workspace')?.value || 'MyTeam').trim() || 'MyTeam';
const seatsRaw = Number.parseInt(getEl('tm-seats')?.value || '2', 10);
const seatQuantity = Math.max(2, Number.isFinite(seatsRaw) ? seatsRaw : 2);
const priceInterval = getEl('tm-interval')?.value === 'year' ? 'year' : 'month';
const planName = (getEl('tm-plan')?.value || 'chatgptteamplan').trim() || 'chatgptteamplan';
const promoOverride = (getEl('tm-promo-override')?.value || '').trim();
const countryOverride = (getEl('tm-country-override')?.value || '').trim().toUpperCase();
const currencyOverride = (getEl('tm-currency-override')?.value || '').trim().toUpperCase();
const promoCode = promoOverride || cfg.promoCode;
const country = countryOverride || cfg.country;
const currency = currencyOverride || cfg.currency;
const payload = {
plan_name: planName,
team_plan_data: {
workspace_name: workspaceName,
price_interval: priceInterval,
seat_quantity: seatQuantity,
},
billing_details: {
country,
currency,
},
cancel_url: `https://chatgpt.com/?promoCode=${encodeURIComponent(promoCode)}`,
promo_code: promoCode,
checkout_ui_mode: 'hosted',
};
return { payload, cfg, promoCode, country, currency };
}
async function generateLink() {
const btn = getEl('tm-generate');
const output = getEl('tm-output');
try {
if (btn) btn.disabled = true;
if (output) output.value = '';
setStatus('正在准备 payload 并获取 accessToken ...');
const { payload, promoCode, country, currency } = buildPayload();
const { token, source } = await getAccessToken();
setStatus(`Token 来源:${source === 'manual' ? '手动填入' : '自动获取'}。正在请求 Stripe hosted checkout 长链接 ...`);
const resp = await window.fetch(CHECKOUT_URL, {
method: 'POST',
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});
const text = await resp.text();
let data;
try {
data = JSON.parse(text);
} catch (_) {
throw new Error(`checkout 响应不是 JSON,HTTP ${resp.status}:${text.slice(0, 300)}`);
}
state.lastData = data;
state.lastPayload = payload;
if (!resp.ok) {
const detail = data?.detail || data?.message || data?.error || JSON.stringify(data).slice(0, 600);
throw new Error(`请求失败,HTTP ${resp.status}:${detail}`);
}
const hostedUrl = data?.url || data?.stripe_hosted_url || data?.checkout_url;
if (!hostedUrl) {
if (output) output.value = JSON.stringify(data, null, 2);
setStatus('请求成功,但没有找到 url / stripe_hosted_url / checkout_url。已把原始响应输出到结果框。', 'warn');
return;
}
state.lastUrl = hostedUrl;
if (output) output.value = hostedUrl;
const sessionId = data?.checkout_session_id || data?.id || '未知';
setStatus(
`生成成功!\n优惠码:${promoCode}\n国家/币种:${country}/${currency}\n席位:${payload.team_plan_data.seat_quantity}\n周期:${payload.team_plan_data.price_interval}\nCheckout Session:${sessionId}`,
'ok'
);
} catch (err) {
console.error('[team-link] 生成失败:', err);
setStatus(`生成失败:${err.message}`, 'err');
} finally {
if (btn) btn.disabled = false;
}
}
async function copyResult() {
const value = (getEl('tm-output')?.value || state.lastUrl || '').trim();
if (!value) {
setStatus('没有可复制的结果,请先生成长链接。', 'warn');
return;
}
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(value);
} else if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
} else {
const ta = getEl('tm-output');
ta?.focus();
ta?.select();
document.execCommand('copy');
}
setStatus('已复制结果到剪贴板。', 'ok');
} catch (err) {
setStatus(`复制失败:${err.message}`, 'err');
}
}
function openResult() {
const value = (getEl('tm-output')?.value || state.lastUrl || '').trim();
if (!value || !/^https?:\/\//i.test(value)) {
setStatus('没有可打开的长链接,请先生成。', 'warn');
return;
}
window.open(value, '_blank', 'noopener,noreferrer');
}
function clearResult() {
state.lastUrl = '';
state.lastData = null;
state.lastPayload = null;
const output = getEl('tm-output');
if (output) output.value = '';
setStatus('已清空输出结果。');
}
function boot() {
waitForBody(() => {
ensureUi();
const observer = new MutationObserver(() => {
if (!getEl(LAUNCHER_ID) || !getEl(PANEL_ID)) {
ensureUi();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
window.addEventListener('resize', () => {
const panel = getEl(PANEL_ID);
if (!panel || panel.style.display === 'none') return;
if (state.dragged) clampPanelIntoView();
else centerPanel();
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') hidePanel();
});
boot();
})();