【更新】GPT Team 账户管理面板 - Tampermonkey 脚本

cogoxu 2026-06-14 15:55 1

一个简洁实用的油猴脚本,帮助你更高效地管理 ChatGPT Team 成员。不适用于bugTeam


^-^ 核心功能



  • ^-^ 实时统计:总邮箱数、ChatGPT 席位数、今日已用席位

  • ^-^ 邮箱管理:添加/删除邮箱,一键移出、邀请

  • ^-^ 自动同步:每 5 秒自动同步成员状态,无需手动刷新

  • ^-^ 智能标识:已加入成员绿色背景,待加入成员灰色背景,自动排序

  • ^-^ 席位提醒:ChatGPT 席位数 ≥2 时邀请会弹窗提醒

  • ^-^ 跨页面标记:复制邮箱后,在 auth.openai.com 页面自动标记该邮箱(黄色边框)

  • ^-^ 今日统计:自动记录当日使用的 ChatGPT 席位数(去重)


^-^ 安装方法



  1. 安装 Tampermonkey 浏览器扩展

  2. 点击下方链接安装脚本

  3. 访问 https://chatgpt.com/admin/members 即可看到右侧管理面板


^-^ 使用场景


适合场景



  • 管理多个 ChatGPT Team 成员邮箱

  • 需要控制 ChatGPT 席位使用量

  • 频繁邀请新成员加入团队





// ==UserScript==
// @name ChatGPT Team 账户管理面板
// @namespace http://tampermonkey.net/
// @version 3.5.0
// @description 功能增强版 - 点击邮箱复制、邀请成员、角色过滤、紧凑布局、一键移出成员、全站注入仅members显示、tab检测、批量添加、搜索过滤、批量删除、备注功能、自动同步、跨页面标记
// @author xcg
// @match https://chatgpt.com/*
// @match https://auth.openai.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @run-at document-end
// ==/UserScript==

(function() {
'use strict';

// 检测当前页面
const isAuthPage = window.location.hostname === 'auth.openai.com';
const isChatGPTSite = window.location.hostname === 'chatgpt.com';

// auth.openai.com 页面:只标记邮箱行
if (isAuthPage) {
initAuthPageMarker();
return;
}

// chatgpt.com 其他页面:不处理
if (!isChatGPTSite) {
return;
}

// ==================== Auth 页面标记功能 ====================
function initAuthPageMarker() {
console.log('Auth 页面标记功能已启动');

// 添加标记样式
GM_addStyle(`
.last-copied-email-row {
border: 3px solid #ffc107 !important;
border-radius: 4px;
}
`);

let isMarked = false; // 标记是否已完成
let intervalId = null;

// 定时检测并标记
function markLastCopiedEmail() {
// 如果已经标记成功,停止定时器
if (isMarked) {
if (intervalId) {
clearInterval(intervalId);
console.log('已找到并标记邮箱,停止检测');
}
return;
}

const lastCopiedEmail = GM_getValue('last_copied_email', '');
if (!lastCopiedEmail) {
return;
}

console.log('查找邮箱:', lastCopiedEmail);

// 移除之前的标记
document.querySelectorAll('.last-copied-email-row').forEach(el => {
el.classList.remove('last-copied-email-row');
});

// 查找包含该邮箱的元素
const emailRegex = new RegExp(lastCopiedEmail.replace(/[.*+?^$()|[\]\\]/g, '\\$&'), 'i');
let found = false;

// 方法1:查找所有包含 @ 的元素
document.querySelectorAll('*').forEach(el => {
if (el.children.length === 0 && el.textContent.includes('@')) {
if (emailRegex.test(el.textContent)) {
// 找到包含邮箱的元素,标记其父级行
const row = el.closest('tr, div[role="row"], li, [class*="row"], [class*="item"]');
if (row && !row.classList.contains('last-copied-email-row')) {
row.classList.add('last-copied-email-row');
console.log('✓ 已标记邮箱:', lastCopiedEmail);
found = true;
isMarked = true; // 标记成功,设置标志
}
}
}
});

if (!found) {
console.log('未找到邮箱:', lastCopiedEmail);
}
}

// 初始标记
setTimeout(markLastCopiedEmail, 1000);

// 每 1 秒检测一次,找到后自动停止
intervalId = setInterval(markLastCopiedEmail, 1000);
}

// ==================== Members 页面管理面板 ====================

// 样式定义
GM_addStyle(`
#account-panel {
position: fixed;
right: 20px;
top: 80px;
width: 380px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
overflow: hidden;
}

#account-panel.collapsed {
width: 180px;
}

#account-panel.collapsed .panel-body {
display: none;
}

.panel-header {
padding: 16px 20px;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
color: white;
}

.panel-title {
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}

.panel-toggle {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}

.panel-toggle:hover {
background: rgba(255, 255, 255, 0.3);
}

.panel-body {
padding: 14px;
background: white;
max-height: calc(85vh - 70px);
overflow-y: auto;
}

.panel-body::-webkit-scrollbar {
width: 8px;
}

.panel-body::-webkit-scrollbar-track {
background: #f1f1f1;
}

.panel-body::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}

.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 10px;
}

.stat-card {
background: rgba(255, 255, 255, 0.95);
padding: 6px 8px;
border-radius: 8px;
text-align: center;
border: 1px solid rgba(102, 126, 234, 0.2);
}

.stat-value {
font-size: 18px;
font-weight: 700;
color: #667eea;
margin-bottom: 2px;
line-height: 1;
}

.stat-label {
font-size: 9px;
color: #666;
font-weight: 500;
}

.section {
margin-bottom: 10px;
}

.section-title {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}

.search-box {
margin-bottom: 10px;
}

.search-input {
width: 100%;
padding: 6px 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 12px;
outline: none;
transition: all 0.2s;
background: white;
color: #333;
}

.search-input:focus {
border-color: #667eea;
}

.batch-actions {
display: flex;
gap: 6px;
margin-bottom: 10px;
}

.btn-batch {
flex: 1;
padding: 6px 10px;
background: #ef4444;
border: none;
border-radius: 6px;
color: white;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-batch:hover {
background: #dc2626;
}

.btn-batch.disabled {
background: #ccc;
cursor: not-allowed;
}

.input-group {
display: flex;
gap: 8px;
margin-bottom: 10px;
}

.email-input {
flex: 1;
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 13px;
outline: none;
transition: all 0.2s;
background: white;
color: #333;
resize: vertical;
min-height: 34px;
max-height: 100px;
font-family: inherit;
}

.email-input:focus {
border-color: #667eea;
}

.email-input::placeholder {
color: #999;
}

.btn-add {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-add:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.email-list {
display: flex;
flex-direction: column;
gap: 4px;
}

.email-item {
background: #f9f9f9;
padding: 6px 8px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #e0e0e0;
transition: all 0.2s;
}

.email-item:hover {
border-color: #667eea;
}

.email-item.joined {
background: #d1fae5;
border-color: #6ee7b7;
}

.email-item.selected {
border-color: #667eea;
background: #e0e7ff;
}

.email-item.selected.joined {
background: #a7f3d0;
}

.email-checkbox {
margin-right: 8px;
cursor: pointer;
width: 16px;
height: 16px;
}

.email-info {
flex: 1;
min-width: 0;
}

.email-text {
font-size: 12px;
color: #333;
word-break: break-all;
margin-bottom: 2px;
cursor: pointer;
user-select: text;
}

.email-text:hover {
color: #667eea;
}

.email-text:active {
color: #059669;
}

.email-meta {
font-size: 9px;
color: #666;
display: flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}

.email-note {
font-size: 10px;
color: #888;
font-style: italic;
margin-top: 2px;
}

.btn-note {
padding: 3px 6px;
background: #8b5cf6;
border: none;
border-radius: 4px;
color: white;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-note:hover {
background: #7c3aed;
}

.badge {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}

.badge-joined {
background: #10b981;
color: white;
}

.badge-pending {
background: #fbbf24;
color: white;
}

.btn-invite {
padding: 4px 10px;
background: #3b82f6;
border: none;
border-radius: 6px;
color: white;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-right: 4px;
}

.btn-invite:hover {
background: #2563eb;
}

.btn-invite.disabled {
background: #ccc;
cursor: not-allowed;
}

.btn-remove {
padding: 4px 10px;
background: #f59e0b;
border: none;
border-radius: 6px;
color: white;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-right: 4px;
}

.btn-remove:hover {
background: #d97706;
}

.btn-remove.disabled {
background: #ccc;
cursor: not-allowed;
}

.btn-delete {
padding: 4px 10px;
background: #ef4444;
border: none;
border-radius: 6px;
color: white;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-delete:hover {
background: #dc2626;
}

.btn-copy {
padding: 6px 12px;
background: #10b981;
border: none;
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-right: 4px;
}

.btn-copy:hover {
background: #059669;
}

.email-actions {
display: flex;
gap: 4px;
}

.btn-sync {
width: 100%;
padding: 8px;
background: #667eea;
border: none;
border-radius: 8px;
color: white;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-sync:hover {
background: #5568d3;
}

.btn-fill {
width: 100%;
padding: 10px;
background: #10b981;
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 12px;
}

.btn-fill:hover {
background: #059669;
}

.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
}

.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}

.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 8px;
}

.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

@keyframes spin {
to { transform: rotate(360deg); }
}

.loading-text {
margin-top: 12px;
color: #666;
font-size: 14px;
}

/* 自定义弹框 */
.custom-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000000;
animation: fadeIn 0.2s ease;
}

@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.modal-content {
background: white;
border-radius: 16px;
padding: 24px;
min-width: 320px;
max-width: 400px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s ease;
}

@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

.modal-title {
font-size: 18px;
font-weight: 700;
color: #333;
margin-bottom: 12px;
}

.modal-message {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-bottom: 20px;
white-space: pre-wrap;
}

.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}

.modal-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.modal-btn-cancel {
background: #e0e0e0;
color: #666;
}

.modal-btn-cancel:hover {
background: #d0d0d0;
}

.modal-btn-confirm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}

.modal-btn-confirm:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

.modal-btn-ok {
background: #10b981;
color: white;
}

.modal-btn-ok:hover {
background: #059669;
}

.modal-btn-warning {
background: #ffc107;
color: #000;
}

.modal-btn-warning:hover {
background: #ffb300;
}
`);

// 自定义弹框组件
class CustomModal {
// 确认对话框
static confirm(title, message) {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'custom-modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-title">${title}</div>
<div class="modal-message">${message}</div>
<div class="modal-buttons">
<button class="modal-btn modal-btn-cancel" id="modal-cancel">取消</button>
<button class="modal-btn modal-btn-confirm" id="modal-confirm">确定</button>
</div>
</div>
`;

document.body.appendChild(modal);

const handleConfirm = () => {
modal.remove();
resolve(true);
};

const handleCancel = () => {
modal.remove();
resolve(false);
};

modal.querySelector('#modal-confirm').addEventListener('click', handleConfirm);
modal.querySelector('#modal-cancel').addEventListener('click', handleCancel);
modal.addEventListener('click', (e) => {
if (e.target === modal) handleCancel();
});
});
}

// 警告对话框
static alert(title, message, type = 'warning') {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'custom-modal';
const btnClass = type === 'warning' ? 'modal-btn-warning' : 'modal-btn-ok';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-title">${title}</div>
<div class="modal-message">${message}</div>
<div class="modal-buttons">
<button class="modal-btn ${btnClass}" id="modal-ok">确定</button>
</div>
</div>
`;

document.body.appendChild(modal);

const handleOk = () => {
modal.remove();
resolve();
};

modal.querySelector('#modal-ok').addEventListener('click', handleOk);
modal.addEventListener('click', (e) => {
if (e.target === modal) handleOk();
});
});
}
}

// 数据管理
class AccountManager {
constructor() {
this.storageKey = 'chatgpt_accounts';
this.accounts = this.load();
}

load() {
try {
return JSON.parse(GM_getValue(this.storageKey, '[]'));
} catch (e) {
return [];
}
}

save() {
GM_setValue(this.storageKey, JSON.stringify(this.accounts));
}

// 获取今日日期字符串 YYYY-MM-DD
getTodayKey() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}

// 获取今日已使用的 ChatGPT 席位数(基于 lastGptSeatAt)
getTodayChatGPTUsage() {
try {
const todayKey = this.getTodayKey();
return this.accounts.filter(account => {
if (!account.lastGptSeatAt) return false;
const lastDate = new Date(account.lastGptSeatAt);
const dateKey = `${lastDate.getFullYear()}-${String(lastDate.getMonth() + 1).padStart(2, '0')}-${String(lastDate.getDate()).padStart(2, '0')}`;
return dateKey === todayKey;
}).length;
} catch (e) {
return 0;
}
}

add(email) {
email = email.trim().toLowerCase();
if (!this.validate(email)) {
return { success: false, message: '邮箱格式不正确' };
}
if (this.accounts.some(a => a.email === email)) {
return { success: false, message: '邮箱已存在' };
}
this.accounts.push({
email: email,
addedAt: new Date().toISOString(),
joinedAt: null,
status: 'pending',
seatType: null,
lastGptSeatAt: null,
role: null,
note: ''
});
this.save();
return { success: true };
}

// 批量添加邮箱
addBatch(emailsText) {
const emails = emailsText
.split(/[\n,;,;]+/) // 支持换行、逗号、分号分隔
.map(e => e.trim().toLowerCase())
.filter(e => e); // 过滤空字符串

const results = {
success: [],
failed: [],
duplicate: []
};

emails.forEach(email => {
if (!this.validate(email)) {
results.failed.push(email);
} else if (this.accounts.some(a => a.email === email)) {
results.duplicate.push(email);
} else {
this.accounts.push({
email: email,
addedAt: new Date().toISOString(),
joinedAt: null,
status: 'pending',
seatType: null,
lastGptSeatAt: null,
role: null,
note: ''
});
results.success.push(email);
}
});

this.save();
return results;
}

remove(email) {
this.accounts = this.accounts.filter(a => a.email !== email);
this.save();
}

// 批量删除
removeBatch(emails) {
this.accounts = this.accounts.filter(a => !emails.includes(a.email));
this.save();
}

update(email, updates) {
const account = this.accounts.find(a => a.email === email);
if (account) {
Object.assign(account, updates);
this.save();
}
}

// 更新备注
updateNote(email, note) {
this.update(email, { note: note });
}

validate(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

getAll() {
return this.accounts;
}

getStats() {
const total = this.accounts.length;
const joined = this.accounts.filter(a => a.status === 'joined').length;
// 修复:检查席位类型是否包含 ChatGPT
const chatgptSeats = this.accounts.filter(a =>
a.seatType && (
a.seatType === 'ChatGPT' ||
a.seatType.includes('ChatGPT') ||
a.seatType === 'chatgpt'
)
).length;
const todayUsed = this.getTodayChatGPTUsage();
return { total, joined, pending: total - joined, chatgptSeats, todayUsed };
}
}

// 页面扫描
class PageScanner {
scanMembers() {
const members = new Map();
const emailRegex = /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g;

// 扫描所有 title 属性
document.querySelectorAll('[title*="@"]').forEach(el => {
const title = el.getAttribute('title');
const matches = title?.match(emailRegex);
if (matches) {
matches.forEach(email => {
const row = el.closest('tr');
if (row) {
// 查找席位类型
let seatType = null;
let role = null;

const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
const text = cell.textContent.trim();

// 查找角色列(通常包含 "所有者"/"Owner"/"成员"/"Member")
if (text === '所有者' || text === 'Owner') {
role = '所有者';
console.log('找到所有者角色:', email);
} else if (text === '成员' || text === 'Member') {
role = '成员';
}

// 查找席位类型
if (text === 'ChatGPT' || text === 'ChatGPT Plus' ||
text.includes('ChatGPT') || text === 'API' ||
text === 'chatgpt' || text.toLowerCase().includes('chatgpt')) {
seatType = text;
console.log('找到席位类型:', email, '→', text);
}
});

// 如果没找到席位,尝试从按钮查找
if (!seatType) {
const buttons = row.querySelectorAll('button');
buttons.forEach(btn => {
const text = btn.textContent.trim();
if (text === 'ChatGPT' || text === 'ChatGPT Plus' ||
text.includes('ChatGPT') || text === 'API' ||
text === 'chatgpt' || text.toLowerCase().includes('chatgpt')) {
seatType = text;
}
});
}

members.set(email.toLowerCase(), { seatType, role });
}
});
}
});

console.log('扫描完成,共找到', members.size, '个成员');
members.forEach((info, email) => {
console.log(' -', email, '席位:', info.seatType || '未知', '角色:', info.role || '未知');
});

return members;
}
}

// UI 管理
class PanelUI {
constructor(manager, scanner) {
this.manager = manager;
this.scanner = scanner;
this.panel = null;
this.isCollapsed = false;
this.dragState = { isDragging: false, startX: 0, startY: 0, initialX: 0, initialY: 0 };
this.copiedEmails = new Set(); // 记录已复制的邮箱
this.hasSynced = false; // 是否已同步过
this.searchKeyword = ''; // 搜索关键词
this.selectedEmails = new Set(); // 批量选中的邮箱
this.init();
}

init() {
this.createPanel();
this.bindEvents();
this.render();
this.startAutoSync();
this.watchUrlChange(); // 监听 URL 变化
}

watchUrlChange() {
// 监听 URL 变化(SPA 应用)
let lastPath = window.location.pathname;
setInterval(() => {
const currentPath = window.location.pathname;
if (currentPath !== lastPath) {
lastPath = currentPath;
const isMembersPage = currentPath.includes('/admin/members');
if (this.panel) {
this.panel.style.display = isMembersPage ? 'block' : 'none';
}
}
}, 500);
}

createPanel() {
this.panel = document.createElement('div');
this.panel.id = 'account-panel';

// 根据当前页面决定是否显示
const isMembersPage = window.location.pathname.includes('/admin/members');
this.panel.style.display = isMembersPage ? 'block' : 'none';

this.panel.innerHTML = `
<div class="panel-header">
<div class="panel-title">GPTeam v3.5.0</div>
<button class="panel-toggle" id="toggle-btn">−</button>
</div>
<div class="panel-body">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">总邮箱</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-chatgpt">0</div>
<div class="stat-label">ChatGPT 席位</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-today">0</div>
<div class="stat-label">今日已用</div>
</div>
</div>

<div class="section">
<div class="section-title">✉️ 添加邮箱</div>
<div class="input-group">
<textarea class="email-input" id="email-input" placeholder="输入邮箱(支持多行或逗号分隔)..." rows="1"></textarea>
<button class="btn-add" id="add-btn">添加</button>
</div>
</div>

<div class="section">
<div class="section-title">📋 邮箱列表</div>
<div class="search-box">
<input type="text" class="search-input" id="search-input" placeholder="🔍 搜索邮箱或备注...">
</div>
<div class="batch-actions">
<button class="btn-batch" id="batch-delete-btn">删除选中</button>
</div>
<div style="position: relative;">
<div class="email-list" id="email-list"></div>
<div class="loading-overlay" id="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
<div class="loading-text">正在同步...</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(this.panel);
}

bindEvents() {
// 折叠
document.getElementById('toggle-btn').addEventListener('click', () => {
this.isCollapsed = !this.isCollapsed;
this.panel.classList.toggle('collapsed');
document.getElementById('toggle-btn').textContent = this.isCollapsed ? '+' : '−';
});

// 添加邮箱
document.getElementById('add-btn').addEventListener('click', () => this.handleAdd());
document.getElementById('email-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleAdd();
}
});

// 搜索
document.getElementById('search-input').addEventListener('input', (e) => {
this.searchKeyword = e.target.value.toLowerCase();
this.render();
});

// 批量删除
document.getElementById('batch-delete-btn').addEventListener('click', () => this.handleBatchDelete());

// 拖拽
const header = this.panel.querySelector('.panel-header');
header.addEventListener('mousedown', (e) => this.startDrag(e));
document.addEventListener('mousemove', (e) => this.onDrag(e));
document.addEventListener('mouseup', () => this.endDrag());
}

handleAdd() {
const input = document.getElementById('email-input');
const emailsText = input.value.trim();
if (!emailsText) return;

// 检测是否为批量添加(包含换行或分隔符)
if (emailsText.includes('\n') || emailsText.includes(',') || emailsText.includes(',') || emailsText.includes(';') || emailsText.includes(';')) {
// 批量添加
const results = this.manager.addBatch(emailsText);

let message = '';
if (results.success.length > 0) {
message += `✓ 成功添加 ${results.success.length} 个邮箱\n`;
}
if (results.duplicate.length > 0) {
message += `⚠ ${results.duplicate.length} 个邮箱已存在\n`;
}
if (results.failed.length > 0) {
message += `✗ ${results.failed.length} 个邮箱格式错误\n`;
}

if (results.success.length > 0) {
input.value = '';
this.render();
}

if (message) {
CustomModal.alert('批量添加结果', message.trim(), results.failed.length > 0 ? 'warning' : 'ok');
}
} else {
// 单个添加
const result = this.manager.add(emailsText);
if (result.success) {
input.value = '';
this.render();
} else {
CustomModal.alert('添加失败', result.message, 'warning');
}
}
}

handleDelete(email) {
CustomModal.confirm('删除邮箱', `确定要删除 ${email} 吗?`).then(confirmed => {
if (confirmed) {
this.manager.remove(email);
this.selectedEmails.delete(email);
this.render();
}
});
}

handleBatchDelete() {
if (this.selectedEmails.size === 0) {
CustomModal.alert('批量删除', '请先选择要删除的邮箱', 'warning');
return;
}

CustomModal.confirm('批量删除', `确定要删除选中的 ${this.selectedEmails.size} 个邮箱吗?`).then(confirmed => {
if (confirmed) {
this.manager.removeBatch([...this.selectedEmails]);
this.selectedEmails.clear();
this.render();
}
});
}

handleNote(email, currentNote) {
const note = prompt('输入备注:', currentNote || '');
if (note !== null) {
this.manager.updateNote(email, note.trim());
this.render();
}
}

handleRemove(email) {
CustomModal.confirm('移出成员', `确定要从团队移出 ${email} 吗?`).then(confirmed => {
if (confirmed) {
this.clickMenuAndRemoveByEmail(email);
}
});
}

handleInvite(email) {
// 检查 ChatGPT 席位数
const stats = this.manager.getStats();
if (stats.chatgptSeats >= 2) {
// 席位数 >= 2,弹窗警告并确认
CustomModal.confirm('⚠️ 席位警告', `当前已有 ${stats.chatgptSeats} 个 ChatGPT 席位,确定要邀请 ${email} 吗?`).then(confirmed => {
if (confirmed) {
// 先复制邮箱
this.handleCopy(email).then(() => {
this.clickInviteButton(email);
});
}
});
} else {
// 席位数 < 2,直接邀请,不需要确认
// 先复制邮箱
this.handleCopy(email).then(() => {
this.clickInviteButton(email);
});
}
}

clickInviteButton(email) {
// 查找"邀请成员"按钮
const inviteBtn = Array.from(document.querySelectorAll('button'))
.find(btn => {
const text = btn.textContent.trim();
return text === '邀请成员' || text === 'Invite member';
});

if (!inviteBtn) {
console.warn('未找到"邀请成员"按钮');
CustomModal.alert('邀请失败', '页面上未找到"邀请成员"按钮', 'warning');
return;
}

console.log('找到邀请按钮:', inviteBtn);
inviteBtn.click();
console.log('已点击"邀请成员"按钮');

// 等待弹窗出现,填充邮箱
setTimeout(() => {
const emailInput = document.querySelector('input#email[type="email"]');
if (!emailInput) {
console.warn('未找到邮箱输入框');
CustomModal.alert('邀请失败', '未找到邮箱输入框', 'warning');
return;
}

console.log('找到邮箱输入框:', emailInput);

// React 受控组件需要设置 nativeValue
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(emailInput, email);

// 触发 React 事件
const inputEvent = new Event('input', { bubbles: true });
emailInput.dispatchEvent(inputEvent);

const changeEvent = new Event('change', { bubbles: true });
emailInput.dispatchEvent(changeEvent);

console.log('已填充邮箱:', email);
console.log('当前输入框值:', emailInput.value);

// 等待一下,自动点击发送按钮
setTimeout(() => {
// 排除我们自己面板的按钮
const sendBtn = Array.from(document.querySelectorAll('button'))
.filter(btn => !btn.closest('#account-panel'))
.find(btn => {
const text = btn.textContent.trim();
return text === '发送邀请' || text === 'Send invite' || text.includes('发送') || text.includes('Send');
});

if (sendBtn) {
console.log('找到发送按钮:', sendBtn);
console.log('发送按钮 outerHTML:', sendBtn.outerHTML);
sendBtn.click();
console.log('已点击发送按钮');

// 邀请成功,1秒后同步状态
setTimeout(() => {
this.syncStatus();
}, 1000);
} else {
console.log('未找到发送按钮');
}
}, 800);
}, 800);
}

clickMenuAndRemoveByEmail(email) {
// 找到包含指定邮箱的元素
const emailCell = Array.from(document.querySelectorAll('*'))
.find(el => el.textContent.trim() === email);

console.log('匹配到的邮箱元素:', emailCell);

if (!emailCell) {
console.warn('页面上没有找到该邮箱:', email);
CustomModal.alert('移除失败', '页面上未找到该邮箱,请确保在用户tab', 'warning');
return;
}

// 找到该行
const row = emailCell.closest('tr') || emailCell.closest('.member-row');
console.log('匹配到的行元素:', row);

if (!row) {
console.warn('未找到包含邮箱的行');
CustomModal.alert('移除失败', '未找到邮箱所在行', 'warning');
return;
}

// 查找按钮
let button = row.querySelector('button[aria-haspopup="menu"]');
if (!button) {
button = row.querySelector('.ellipsis, .more-options, button');
}

console.log('匹配到的按钮:', button);
console.log('按钮 outerHTML:', button?.outerHTML);

if (!button) {
console.warn('未找到该行的菜单按钮');
CustomModal.alert('移除失败', '未找到菜单按钮', 'warning');
return;
}

// 点击按钮,触发菜单
const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
['pointerdown','mousedown','mouseup','click'].forEach(type => {
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: win
});
button.dispatchEvent(event);
console.log(`已触发 ${type} 事件`);
});

// 等待菜单出现并点击"移除成员"
let elapsed = 0;
const interval = setInterval(() => {
elapsed += 500;
const menu = document.querySelector('div[role="menu"], div[aria-expanded="true"], body > div:nth-of-type(6) div');
if (menu) {
console.log('检测到菜单:', menu);

const removeBtn = Array.from(menu.querySelectorAll('div[role="menuitem"], button, div'))
.find(el => {
const text = el.textContent.trim();
return text === '移除成员' || text === 'Remove member';
});

if (removeBtn) {
console.log('匹配到移除成员按钮:', removeBtn);
console.log('移除按钮 outerHTML:', removeBtn.outerHTML);

// 如果匹配到的是外层 group,找到内层的 menuitem
let actualBtn = removeBtn;
if (removeBtn.getAttribute('role') === 'group') {
actualBtn = removeBtn.querySelector('div[role="menuitem"]');
console.log('找到内层 menuitem:', actualBtn);
}

if (!actualBtn) {
console.warn('未找到实际可点击的元素');
return;
}

const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: win
});
actualBtn.dispatchEvent(clickEvent);
console.log('已点击"移除成员"按钮');
clearInterval(interval);

// 等待确认对话框并点击删除按钮
setTimeout(() => {
// 排除我们自己面板内的按钮
const confirmBtn = Array.from(document.querySelectorAll('button.btn-danger, button'))
.filter(btn => !btn.closest('#account-panel')) // 排除面板内的按钮
.find(btn => {
const text = btn.textContent.trim();
return text === '删除' || text === 'Delete' || text === '移除' || text === 'Remove';
});
if (confirmBtn) {
console.log('找到删除确认按钮:', confirmBtn);
console.log('删除按钮 outerHTML:', confirmBtn.outerHTML);
confirmBtn.click();
console.log('已点击删除确认按钮');

// 移除成功,1秒后同步状态
setTimeout(() => {
this.syncStatus();
}, 1000);
} else {
console.log('未找到删除确认按钮');
}
}, 500);
}
}

if (elapsed >= 5000) {
console.warn('5 秒内未检测到菜单或移除按钮,可能触发失败');
CustomModal.alert('移除失败', '未能找到移除按钮,操作超时', 'warning');
clearInterval(interval);
}
}, 500); // 每 500ms 检查一次
}

async handleCopy(email) {
// 先同步状态
this.syncStatus();
await this.sleep(500);

// 复制邮箱到剪贴板(不检查席位数)
try {
await navigator.clipboard.writeText(email);
console.log('已复制:', email);

// 保存最后复制的邮箱(跨页面共享)
GM_setValue('last_copied_email', email);
console.log('已保存最后复制的邮箱:', email);

// 标记为已复制
this.copiedEmails.add(email);

// 立即更新按钮显示
this.render();

} catch (err) {
console.error('复制失败:', err);
// 备用方案:使用旧方法
const textarea = document.createElement('textarea');
textarea.value = email;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);

// 保存最后复制的邮箱
GM_setValue('last_copied_email', email);
console.log('已保存最后复制的邮箱:', email);

// 标记为已复制
this.copiedEmails.add(email);

// 立即更新按钮显示
this.render();
}
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

fillInviteInput() {
const pending = this.manager.getAll().filter(a => a.status === 'pending');
if (pending.length === 0) {
alert('没有待加入的邮箱');
return;
}

// 查找邀请输入框
const inviteInput = document.querySelector('input#email[type="email"]') ||
document.querySelector('input[aria-label="电子邮件"]') ||
document.querySelector('input[placeholder*="电子邮件"]') ||
document.querySelector('input[placeholder*="email" i]');

if (!inviteInput) {
alert('未找到邀请输入框\n\n请先打开"邀请成员"弹框');
return;
}

// 填充第一个待加入的邮箱
const email = pending[0].email;
inviteInput.value = email;
inviteInput.focus();

// 触发 input 事件
inviteInput.dispatchEvent(new Event('input', { bubbles: true }));
inviteInput.dispatchEvent(new Event('change', { bubbles: true }));

console.log('已填充邮箱:', email);
alert(`已填充邮箱:${email}\n\n请点击"发送邀请"按钮`);
}

syncStatus() {
// 检查当前 URL 是否是 members 页面
if (!window.location.pathname.includes('/admin/members')) {
console.log('不在 members 页面,跳过同步');
return;
}

// 检查是否在"用户"tab
// 方法:检测页面上是否有"所有者/Owner"关键词,这是用户tab的列标题
const pageText = document.body.innerText;
const isUserTab = pageText.includes('所有者') || pageText.includes('Owner');

if (!isUserTab) {
console.log('不在用户 tab,跳过同步');
return;
}

const pageMembers = this.scanner.scanMembers();
const accounts = this.manager.getAll();

// 1. 先将页面上的成员自动添加到列表(排除所有者)
pageMembers.forEach((memberInfo, email) => {
// 跳过所有者角色
if (memberInfo.role === '所有者') {
console.log('跳过所有者:', email);
return;
}

const exists = accounts.find(a => a.email === email);
if (!exists) {
// 自动添加页面上的成员
const isChatGPTSeat = memberInfo.seatType && (
memberInfo.seatType === 'ChatGPT' ||
memberInfo.seatType.includes('ChatGPT') ||
memberInfo.seatType === 'chatgpt'
);
this.manager.accounts.push({
email: email,
addedAt: new Date().toISOString(),
joinedAt: new Date().toISOString(),
status: 'joined',
seatType: memberInfo.seatType,
lastGptSeatAt: isChatGPTSeat ? new Date().toISOString() : null,
role: memberInfo.role,
note: ''
});
console.log('自动添加成员:', email);
}
});

// 保存一次
this.manager.save();

// 2. 更新现有账户的状态
const updatedAccounts = this.manager.getAll();
updatedAccounts.forEach(account => {
const pageMember = pageMembers.get(account.email);
if (pageMember) {
// 跳过所有者角色
if (pageMember.role === '所有者') {
console.log('检测到所有者,从列表移除:', account.email);
this.manager.remove(account.email);
return;
}

// 已在团队中
const isChatGPTSeat = pageMember.seatType && (
pageMember.seatType === 'ChatGPT' ||
pageMember.seatType.includes('ChatGPT') ||
pageMember.seatType === 'chatgpt'
);

if (account.status !== 'joined') {
this.manager.update(account.email, {
status: 'joined',
joinedAt: new Date().toISOString(),
seatType: pageMember.seatType,
role: pageMember.role,
lastGptSeatAt: isChatGPTSeat ? new Date().toISOString() : account.lastGptSeatAt
});
} else {
// 更新席位类型和角色
const updates = {};
if (account.seatType !== pageMember.seatType) {
updates.seatType = pageMember.seatType;
}
if (account.role !== pageMember.role) {
updates.role = pageMember.role;
}
// 如果是 ChatGPT 席位,更新最后时间
if (isChatGPTSeat) {
updates.lastGptSeatAt = new Date().toISOString();
}
if (Object.keys(updates).length > 0) {
this.manager.update(account.email, updates);
}
}
} else {
// 不在团队中(已移除)
if (account.status === 'joined') {
this.manager.update(account.email, {
status: 'pending',
joinedAt: null,
seatType: null
// 注意:不清空 lastGptSeatAt,保留最后时间
});
}
}
});

// 3. 清空已复制标记(下次同步后按钮恢复)
this.copiedEmails.clear();

// 4. 只有获取到至少一个账户才标记为已同步
const finalAccounts = this.manager.getAll();
if (finalAccounts.length > 0) {
this.hasSynced = true;
const loadingOverlay = document.getElementById('loading-overlay');
if (loadingOverlay) {
loadingOverlay.style.display = 'none';
}
console.log('同步完成,共', finalAccounts.length, '个账户');
} else {
console.log('同步完成,但未找到任何账户');
}

this.render();
}

render() {
const accounts = this.manager.getAll();
const stats = this.manager.getStats();
const listEl = document.getElementById('email-list');
const loadingOverlay = document.getElementById('loading-overlay');

// 更新统计
document.getElementById('stat-total').textContent = stats.total;
document.getElementById('stat-chatgpt').textContent = stats.chatgptSeats;
document.getElementById('stat-today').textContent = stats.todayUsed;

// 如果还没同步过,显示 loading
if (!this.hasSynced) {
if (loadingOverlay) {
loadingOverlay.style.display = 'flex';
}
listEl.innerHTML = '<div class="empty-state"><div class="empty-icon">📭</div><div>等待同步...</div></div>';
return;
}

// 隐藏 loading
if (loadingOverlay) {
loadingOverlay.style.display = 'none';
}

// 过滤搜索
let filteredAccounts = accounts;
if (this.searchKeyword) {
filteredAccounts = accounts.filter(a =>
a.email.includes(this.searchKeyword) ||
(a.note && a.note.toLowerCase().includes(this.searchKeyword))
);
}

// 渲染列表
if (filteredAccounts.length === 0) {
listEl.innerHTML = '<div class="empty-state"><div class="empty-icon">📭</div><div>' + (this.searchKeyword ? '无匹配结果' : '暂无邮箱') + '</div></div>';
return;
}

// 排序:已加入的排在前面
const sortedAccounts = [...filteredAccounts].sort((a, b) => {
if (a.status === 'joined' && b.status !== 'joined') return -1;
if (a.status !== 'joined' && b.status === 'joined') return 1;
return 0;
});

// 计算已移除时长的辅助函数
const getRemovalDuration = (lastGptSeatAt) => {
if (!lastGptSeatAt) return null;
const now = new Date();
const lastTime = new Date(lastGptSeatAt);
const diffMs = now - lastTime;
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
};

listEl.innerHTML = sortedAccounts.map(account => {
const isJoined = account.status === 'joined';
const isCopied = this.copiedEmails.has(account.email);
const isSelected = this.selectedEmails.has(account.email);
const removalDuration = !isJoined && account.lastGptSeatAt ? getRemovalDuration(account.lastGptSeatAt) : null;

return `
<div class="email-item ${isJoined ? 'joined' : ''} ${isSelected ? 'selected' : ''}" data-email="${account.email}">
<input type="checkbox" class="email-checkbox" data-email="${account.email}" ${isSelected ? 'checked' : ''}>
<div class="email-info">
<div class="email-text" data-email="${account.email}">${account.email}</div>
${account.note ? `<div class="email-note">📝 ${account.note}</div>` : ''}
<div class="email-meta">
${account.seatType ? `<span>席位: ${account.seatType}</span>` : ''}
${removalDuration ? `<span>已移除 ${removalDuration}</span>` : ''}
</div>
</div>
<div class="email-actions">
<button class="btn-note" data-email="${account.email}" title="添加备注">📝</button>
${isJoined ? `<button class="btn-remove" data-email="${account.email}">移出</button>` : `<button class="btn-invite" data-email="${account.email}">邀请</button>`}
<button class="btn-delete" data-email="${account.email}">删除</button>
</div>
</div>
`;
}).join('');

// 绑定复选框
listEl.querySelectorAll('.email-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const email = e.target.dataset.email;
if (e.target.checked) {
this.selectedEmails.add(email);
} else {
this.selectedEmails.delete(email);
}
this.render();
});
});

// 绑定备注按钮
listEl.querySelectorAll('.btn-note').forEach(btn => {
btn.addEventListener('click', (e) => {
const email = e.target.dataset.email;
const account = this.manager.getAll().find(a => a.email === email);
this.handleNote(email, account ? account.note : '');
});
});

// 绑定邮箱文本点击复制
listEl.querySelectorAll('.email-text').forEach(el => {
el.addEventListener('click', (e) => {
const email = e.target.dataset.email;
this.handleCopy(email);
});
});

// 绑定移出按钮
listEl.querySelectorAll('.btn-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const email = e.target.dataset.email;
this.handleRemove(email);
});
});

// 绑定邀请按钮
listEl.querySelectorAll('.btn-invite').forEach(btn => {
btn.addEventListener('click', (e) => {
const email = e.target.dataset.email;
this.handleInvite(email);
});
});

// 绑定删除按钮
listEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
const email = e.target.dataset.email;
this.handleDelete(email);
});
});
}

startDrag(e) {
if (e.target.closest('.panel-toggle')) return;
this.dragState.isDragging = true;
this.dragState.startX = e.clientX;
this.dragState.startY = e.clientY;
const rect = this.panel.getBoundingClientRect();
this.dragState.initialX = rect.left;
this.dragState.initialY = rect.top;
}

onDrag(e) {
if (!this.dragState.isDragging) return;
e.preventDefault();
const deltaX = e.clientX - this.dragState.startX;
const deltaY = e.clientY - this.dragState.startY;
this.panel.style.left = (this.dragState.initialX + deltaX) + 'px';
this.panel.style.top = (this.dragState.initialY + deltaY) + 'px';
this.panel.style.right = 'auto';
}

endDrag() {
this.dragState.isDragging = false;
}

startAutoSync() {
// 初始同步
setTimeout(() => this.syncStatus(), 2000);

// 每 5 秒自动同步(仅在 members 页面)
setInterval(() => {
if (!this.isCollapsed && window.location.pathname.includes('/admin/members')) {
this.syncStatus();
}
}, 5000);
}
}

// 初始化
function init() {
const manager = new AccountManager();
const scanner = new PageScanner();
const ui = new PanelUI(manager, scanner);
console.log('ChatGPT Team 账户管理面板已加载');
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 1000);
}
})();

最新回复 (4)
  • cogoxu 楼主 06-14 21:06
    1

    新增 一键移出和邀请

    新增 已移除时间显示

  • xielaoban 06-14 21:45
    2

    这个项目是什么意思呢,无限 chatgpt 席位的 team 吗,踢了新增我记得不是 chatgpt 席位吧

  • cogoxu 楼主 06-14 21:59
    3

    两席位优惠team,方便切换用户蹭新席位额度

  • xielaoban 06-14 22:01
    4

    优惠 team 这又是什么项目,大佬能指路一下么

* 帖子来源Linux.do
返回