## 当前状态 - 插件界面已完成重命名 (cursorpro → hummingbird) - 双账号池 UI 已实现 (Auto/Pro 卡片) - 后端已切换到 MySQL 数据库 - 添加了 Cursor 官方用量 API 文档 ## 已知问题 (待修复) 1. 激活时检查账号导致无账号时激活失败 2. 未启用无感换号时不应获取账号 3. 账号用量模块不显示 (seamless 未启用时应隐藏) 4. 积分显示为 0 (后端未正确返回) 5. Auto/Pro 双密钥逻辑混乱,状态不同步 6. 账号添加后无自动分析功能 ## 下一版本计划 - 重构数据模型,优化账号状态管理 - 实现 Cursor API 自动分析账号 - 修复激活流程,不依赖账号 - 启用无感时才分配账号 - 完善账号用量实时显示 ## 文件说明 - docs/系统设计文档.md - 完整架构设计 - cursor 官方用量接口.md - Cursor API 文档 - 参考计费/ - Vibeviewer 开源项目参考 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2348 lines
87 KiB
HTML
2348 lines
87 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-{{NONCE}}'; img-src {{CSP_SOURCE}} https: data:; font-src {{CSP_SOURCE}}; worker-src 'none';">
|
||
<title>蜂鸟Pro</title>
|
||
<script nonce="{{NONCE}}">
|
||
// 尽早清理 Service Worker(在 head 中执行,比 body 更早)
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.getRegistrations().then(function(regs) {
|
||
regs.forEach(function(reg) { reg.unregister(); });
|
||
}).catch(function() {});
|
||
}
|
||
</script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
background: #1e1e1e;
|
||
color: #cccccc;
|
||
padding: 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 16px;
|
||
padding: 12px;
|
||
background: #252526;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.section-title .icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.status-badge {
|
||
margin-left: auto;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.status-badge.inactive {
|
||
background: #6e3232;
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.status-badge.active {
|
||
background: #2d4a3e;
|
||
color: #4ade80;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
input[type="text"] {
|
||
flex: 1;
|
||
padding: 8px 12px;
|
||
background: #3c3c3c;
|
||
border: 1px solid #4a4a4a;
|
||
border-radius: 4px;
|
||
color: #ffffff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
input[type="text"]::placeholder {
|
||
color: #888888;
|
||
}
|
||
|
||
input[type="text"]:focus {
|
||
outline: none;
|
||
border-color: #007acc;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #007acc;
|
||
color: white;
|
||
}
|
||
|
||
.btn-purple {
|
||
background: #8b5cf6;
|
||
color: white;
|
||
}
|
||
|
||
.btn-blue {
|
||
background: #3b82f6;
|
||
color: white;
|
||
}
|
||
|
||
.btn-red {
|
||
background: #ef4444;
|
||
color: white;
|
||
}
|
||
|
||
.btn-block {
|
||
display: block;
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid #3c3c3c;
|
||
}
|
||
|
||
.info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-label {
|
||
color: #888888;
|
||
}
|
||
|
||
.info-value {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.usage-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.usage-row:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.usage-item {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: #2d2d2d;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.switch-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.switch {
|
||
position: relative;
|
||
width: 40px;
|
||
height: 20px;
|
||
}
|
||
|
||
.switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #4a4a4a;
|
||
border-radius: 20px;
|
||
transition: 0.3s;
|
||
}
|
||
|
||
.slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 16px;
|
||
width: 16px;
|
||
left: 2px;
|
||
bottom: 2px;
|
||
background-color: white;
|
||
border-radius: 50%;
|
||
transition: 0.3s;
|
||
}
|
||
|
||
input:checked + .slider {
|
||
background-color: #8b5cf6;
|
||
}
|
||
|
||
input:checked + .slider:before {
|
||
transform: translateX(20px);
|
||
}
|
||
|
||
/* 小尺寸开关样式 */
|
||
.switch-sm {
|
||
position: relative;
|
||
width: 32px;
|
||
height: 16px;
|
||
}
|
||
|
||
.switch-sm .slider:before {
|
||
height: 12px;
|
||
width: 12px;
|
||
left: 2px;
|
||
bottom: 2px;
|
||
}
|
||
|
||
.switch-sm input:checked + .slider:before {
|
||
transform: translateX(16px);
|
||
}
|
||
|
||
.pro-badge {
|
||
background: linear-gradient(90deg, #8b5cf6, #d946ef);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
color: white;
|
||
}
|
||
|
||
.footer {
|
||
margin-top: 16px;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, rgba(60, 60, 60, 0.3) 0%, rgba(40, 40, 40, 0.5) 100%);
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.footer-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.auto-start {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 11px;
|
||
color: #888;
|
||
}
|
||
|
||
.cursor-version {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: #666;
|
||
padding: 4px 10px;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.cursor-version .version-num {
|
||
color: #a78bfa;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 自定义弹窗样式 */
|
||
.modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.85);
|
||
backdrop-filter: blur(4px);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.9) translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
}
|
||
|
||
.modal-overlay.show {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: linear-gradient(145deg, #1e1e1e 0%, #2a2a2a 100%);
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
max-width: 260px;
|
||
width: 90%;
|
||
text-align: center;
|
||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255,255,255,0.05);
|
||
animation: slideIn 0.2s ease;
|
||
}
|
||
|
||
.modal-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
margin: 0 auto 12px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
}
|
||
|
||
.modal-icon.warning {
|
||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||
}
|
||
|
||
.modal-icon.success {
|
||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.modal-message {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin-bottom: 16px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.modal-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.modal-btn.primary {
|
||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||
color: white;
|
||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4);
|
||
}
|
||
|
||
.modal-btn.primary:hover {
|
||
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.5);
|
||
}
|
||
|
||
.modal-btn.secondary {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: #9ca3af;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.modal-btn.secondary:hover {
|
||
background: rgba(255, 255, 255, 0.12);
|
||
color: #fff;
|
||
}
|
||
|
||
.modal-btn.single {
|
||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||
color: white;
|
||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
|
||
min-width: 100px;
|
||
}
|
||
|
||
.modal-btn.single:hover {
|
||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.5);
|
||
}
|
||
|
||
.highlight {
|
||
color: #4ade80;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.key-display {
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.key-display:hover {
|
||
color: #007acc;
|
||
}
|
||
|
||
.key-display.copied {
|
||
color: #4ade80 !important;
|
||
}
|
||
|
||
/* Loading 状态样式 */
|
||
.btn.loading {
|
||
position: relative;
|
||
pointer-events: none;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.btn.loading .btn-text {
|
||
visibility: hidden;
|
||
}
|
||
|
||
.btn.loading::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 16px;
|
||
height: 16px;
|
||
top: 50%;
|
||
left: 50%;
|
||
margin-left: -8px;
|
||
margin-top: -8px;
|
||
border: 2px solid transparent;
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.refresh-btn.loading {
|
||
animation: spin 1s linear infinite;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 公告样式 */
|
||
.announcement-badge {
|
||
margin-left: auto;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.announcement-badge.info {
|
||
background: #1e3a5f;
|
||
color: #60a5fa;
|
||
}
|
||
|
||
.announcement-badge.warning {
|
||
background: #5c4a1f;
|
||
color: #fbbf24;
|
||
}
|
||
|
||
.announcement-badge.error {
|
||
background: #6e3232;
|
||
color: #f87171;
|
||
}
|
||
|
||
.announcement-badge.success {
|
||
background: #2d4a3e;
|
||
color: #4ade80;
|
||
}
|
||
|
||
.announcement-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
margin-bottom: 8px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.announcement-content {
|
||
font-size: 12px;
|
||
color: #b0b0b0;
|
||
line-height: 1.6;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.announcement-link {
|
||
color: #60a5fa;
|
||
text-decoration: none;
|
||
border-bottom: 1px dashed #60a5fa;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.announcement-link:hover {
|
||
color: #93c5fd;
|
||
border-bottom-color: #93c5fd;
|
||
}
|
||
|
||
/* 号池选择器样式 */
|
||
.pool-selector {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
padding: 4px;
|
||
background: #2d2d2d;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.pool-btn {
|
||
flex: 1;
|
||
padding: 10px 12px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
background: transparent;
|
||
color: #888;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.pool-btn:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: #aaa;
|
||
}
|
||
|
||
.pool-btn.active {
|
||
background: #3c3c3c;
|
||
color: #fff;
|
||
}
|
||
|
||
.pool-btn.auto.active {
|
||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.pool-btn.pro.active {
|
||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
|
||
}
|
||
|
||
.pool-btn .pool-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pool-btn .pool-desc {
|
||
font-size: 10px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.pool-btn .pool-status {
|
||
font-size: 10px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.pool-btn.auto .pool-status {
|
||
color: #4ade80;
|
||
}
|
||
|
||
.pool-btn.pro .pool-status {
|
||
color: #a78bfa;
|
||
}
|
||
|
||
.pool-btn.active .pool-status {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
/* 双密钥信息显示 */
|
||
.dual-key-info {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.key-card {
|
||
flex: 1;
|
||
padding: 8px 10px;
|
||
background: #2d2d2d;
|
||
border-radius: 6px;
|
||
border: 1px solid transparent;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.key-card:hover {
|
||
border-color: #4a4a4a;
|
||
}
|
||
|
||
.key-card.active {
|
||
border-color: #4ade80;
|
||
}
|
||
|
||
.key-card.auto.active {
|
||
border-color: #10b981;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
}
|
||
|
||
.key-card.pro.active {
|
||
border-color: #8b5cf6;
|
||
background: rgba(139, 92, 246, 0.1);
|
||
}
|
||
|
||
.key-card .key-type {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.key-card.auto .key-type {
|
||
color: #4ade80;
|
||
}
|
||
|
||
.key-card.pro .key-type {
|
||
color: #a78bfa;
|
||
}
|
||
|
||
.key-card .key-value {
|
||
font-size: 11px;
|
||
color: #ccc;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.key-card .key-expire {
|
||
font-size: 10px;
|
||
color: #888;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.key-card .key-quota {
|
||
font-size: 10px;
|
||
color: #fbbf24;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.key-card .clear-btn {
|
||
font-size: 10px;
|
||
color: #ef4444;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 2px 4px;
|
||
margin-top: 4px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.key-card .clear-btn:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Toast 通知样式 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 12px;
|
||
pointer-events: none;
|
||
z-index: 2000;
|
||
}
|
||
|
||
.toast {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(74, 222, 128, 0.1);
|
||
transform: translateY(-100px);
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.toast.show {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.toast-message {
|
||
font-size: 12px;
|
||
color: #e0e0e0;
|
||
max-width: 280px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 离线状态提示样式 */
|
||
.offline-banner {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 14px;
|
||
margin-bottom: 12px;
|
||
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
border-radius: 8px;
|
||
animation: slideDown 0.3s ease;
|
||
}
|
||
|
||
.offline-banner.show {
|
||
display: flex;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.offline-banner .offline-icon {
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.offline-banner .offline-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.offline-banner .offline-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #fca5a5;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.offline-banner .offline-desc {
|
||
font-size: 11px;
|
||
color: #fecaca;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.offline-banner .retry-btn {
|
||
padding: 4px 10px;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 4px;
|
||
color: #fff;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.offline-banner .retry-btn:hover {
|
||
background: rgba(255, 255, 255, 0.25);
|
||
}
|
||
|
||
.offline-banner .retry-btn.loading {
|
||
pointer-events: none;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 顶部更新提醒条 */
|
||
.update-banner {
|
||
position: sticky;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||
color: #fff;
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
z-index: 1000;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||
}
|
||
.update-banner.show {
|
||
display: flex;
|
||
}
|
||
.update-banner .update-icon {
|
||
font-size: 14px;
|
||
}
|
||
.update-banner .update-text {
|
||
font-weight: 500;
|
||
}
|
||
.update-banner .update-version {
|
||
background: rgba(255,255,255,0.2);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
}
|
||
.update-banner .update-close {
|
||
margin-left: auto;
|
||
background: none;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
padding: 0 4px;
|
||
opacity: 0.8;
|
||
}
|
||
.update-banner .update-close:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 顶部更新提醒条 -->
|
||
<div class="update-banner" id="updateBanner">
|
||
<span class="update-icon">🚀</span>
|
||
<span class="update-text">发现新版本</span>
|
||
<span class="update-version" id="updateBannerVersion">initOut.0</span>
|
||
<button class="update-close" id="updateBannerClose" title="关闭">×</button>
|
||
</div>
|
||
|
||
<!-- 管理员权限提示弹窗 -->
|
||
<div class="modal-overlay" id="adminModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon warning">🔐</div>
|
||
<div class="modal-title">需要管理员权限</div>
|
||
<div class="modal-message">
|
||
请关闭 Cursor,右键点击图标<br>
|
||
选择 <span class="highlight">以管理员身份运行</span>
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn single" id="adminModalClose">我知道了</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重置机器码权限提示弹窗 -->
|
||
<div class="modal-overlay" id="resetPermissionModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon warning">🔐</div>
|
||
<div class="modal-title">需要管理员权限</div>
|
||
<div class="modal-message" style="text-align: left; line-height: 1.8;">
|
||
重置机器码需要管理员权限才能完整执行。<br><br>
|
||
请按以下步骤操作:<br>
|
||
<span style="color: #fbbf24;">1.</span> 完全关闭 Cursor<br>
|
||
<span style="color: #fbbf24;">2.</span> 右键点击 Cursor 图标<br>
|
||
<span style="color: #fbbf24;">3.</span> 选择 <span class="highlight">以管理员身份运行</span><br>
|
||
<span style="color: #fbbf24;">4.</span> 再次点击重置机器码
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn single" id="resetPermissionClose">我知道了</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重启提示弹窗 -->
|
||
<div class="modal-overlay" id="restartModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon success">✓</div>
|
||
<div class="modal-title" id="restartModalTitle">操作成功</div>
|
||
<div class="modal-message">
|
||
需要重启 Cursor 才能生效
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn primary" id="restartNowBtn">立即重启</button>
|
||
<button class="modal-btn secondary" id="restartLaterBtn">稍后</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 激活码过期弹窗 -->
|
||
<div class="modal-overlay" id="expiredModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);">⏰</div>
|
||
<div class="modal-title">激活码已过期</div>
|
||
<div class="modal-message">
|
||
您的激活码已过期,请续费后继续使用
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn single" id="expiredModalClose">我知道了</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 清理环境确认弹窗 -->
|
||
<div class="modal-overlay" id="cleanEnvModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon warning">⚠️</div>
|
||
<div class="modal-title">清理 Cursor 环境</div>
|
||
<div class="modal-message">
|
||
此操作会删除所有配置和登录信息<br>确定要继续吗?
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn primary" id="cleanEnvConfirmBtn">确定清理</button>
|
||
<button class="modal-btn secondary" id="cleanEnvCancelBtn">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 换号确认弹窗 -->
|
||
<div class="modal-overlay" id="switchConfirmModal">
|
||
<div class="modal-content">
|
||
<div class="modal-icon warning">💰</div>
|
||
<div class="modal-title">账号未使用完</div>
|
||
<div class="modal-message">
|
||
当前账号 <span id="switchConfirmEmail" style="color:#4caf50;"></span><br>
|
||
已用额度: <span id="switchConfirmCost" style="color:#ff9800;font-weight:bold;">$0.00</span> (不足 $10)<br><br>
|
||
确定要换号吗?
|
||
</div>
|
||
<div class="modal-buttons">
|
||
<button class="modal-btn primary" id="switchConfirmBtn">确认换号</button>
|
||
<button class="modal-btn secondary" id="switchCancelBtn">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 离线状态提示 -->
|
||
<div class="offline-banner" id="offlineBanner">
|
||
<span class="offline-icon">📡</span>
|
||
<div class="offline-text">
|
||
<div class="offline-title">网络连接失败</div>
|
||
<div class="offline-desc">请检查网络后重试</div>
|
||
</div>
|
||
<button class="retry-btn" id="retryConnectBtn">重试</button>
|
||
</div>
|
||
|
||
<!-- 软件授权 -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<span class="icon">🔐</span>
|
||
<span>软件授权</span>
|
||
<span class="status-badge" id="authStatus">未授权</span>
|
||
</div>
|
||
|
||
<!-- 号池选择器 -->
|
||
<div class="pool-selector">
|
||
<button class="pool-btn auto active" id="poolAutoBtn" data-pool="auto">
|
||
<span class="pool-name">🌿 Auto</span>
|
||
<span class="pool-desc">基础模型 · 无限换号</span>
|
||
<span class="pool-status" id="autoPoolStatus">未激活</span>
|
||
</button>
|
||
<button class="pool-btn pro" id="poolProBtn" data-pool="pro">
|
||
<span class="pool-name">⚡ Pro</span>
|
||
<span class="pool-desc">高级模型 · 积分制</span>
|
||
<span class="pool-status" id="proPoolStatus">未激活</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<input type="text" id="keyInput" placeholder="请输入CDK激活码">
|
||
<button class="btn btn-primary" id="activateBtn"><span class="btn-text">激活</span></button>
|
||
</div>
|
||
|
||
<!-- 双密钥卡片显示 -->
|
||
<div class="dual-key-info">
|
||
<div class="key-card auto" id="autoKeyCard">
|
||
<div class="key-type">🌿 AUTO 密钥</div>
|
||
<div class="key-value" id="autoKeyDisplay">未激活</div>
|
||
<div class="key-expire" id="autoKeyExpire"></div>
|
||
<button class="clear-btn" id="clearAutoKeyBtn" style="display:none;">清除</button>
|
||
</div>
|
||
<div class="key-card pro" id="proKeyCard">
|
||
<div class="key-type">⚡ PRO 密钥</div>
|
||
<div class="key-value" id="proKeyDisplay">未激活</div>
|
||
<div class="key-quota" id="proKeyQuota"></div>
|
||
<button class="clear-btn" id="clearProKeyBtn" style="display:none;">清除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 当前选中密钥详情(兼容旧逻辑) -->
|
||
<div class="info-row" style="display:none;">
|
||
<span class="info-label">激活码</span>
|
||
<span class="info-value key-display" id="keyDisplay" title="点击复制">尚未激活</span>
|
||
</div>
|
||
<div class="info-row" style="display:none;">
|
||
<span class="info-label">到期时间</span>
|
||
<span class="info-value" id="expireDate">尚未激活</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号数据 (已隐藏) -->
|
||
<div class="section" style="display:none;">
|
||
<div class="section-title">
|
||
<span class="icon">👤</span>
|
||
<span>账号数据</span>
|
||
<span class="status-badge" id="accountStatus">未激活</span>
|
||
</div>
|
||
|
||
<div class="info-row">
|
||
<span class="info-label">CI积分余额</span>
|
||
<span class="info-value">0 <button style="background:none;border:none;color:#007acc;cursor:pointer;">🔄</button></span>
|
||
</div>
|
||
|
||
<button class="btn btn-purple btn-block" id="switchBtn" disabled>换号</button>
|
||
<button class="btn btn-blue btn-block" id="resetBtn">重置机器码</button>
|
||
<button class="btn btn-blue btn-block" id="disableUpdateBtn">禁用自动更新</button>
|
||
<button class="btn btn-blue btn-block" id="cleanEnvBtn">清理Cursor环境</button>
|
||
<button class="btn btn-red btn-block" id="disableBtn">停用插件</button>
|
||
</div>
|
||
|
||
<!-- 无感换号 -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<span class="icon">⚡</span>
|
||
<span>无感换号</span>
|
||
<span class="status-badge" id="seamlessStatus">未启用</span>
|
||
</div>
|
||
|
||
<div class="info-row">
|
||
<span class="info-label">积分</span>
|
||
<span class="info-value" id="seamlessSwitchRemaining">0</span>
|
||
</div>
|
||
|
||
<div class="info-row">
|
||
<span class="info-label">当前账号</span>
|
||
<span class="info-value" style="font-size:11px;" id="seamlessCurrentAccount">未分配</span>
|
||
</div>
|
||
|
||
<div class="switch-container" style="margin: 12px 0;">
|
||
<span>免魔法模式</span>
|
||
<span class="pro-badge">PRO</span>
|
||
<span style="margin-left: auto; color: #888; font-size: 11px;"></span>
|
||
<label class="switch">
|
||
<input type="checkbox" id="seamlessProxySwitch">
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<button class="btn btn-purple btn-block" id="enableSeamlessBtn" disabled><span class="btn-text">启用无感换号</span></button>
|
||
<button class="btn btn-red btn-block" id="seamlessResetMachineBtn" style="display:none;"><span class="btn-text">重置机器码</span></button>
|
||
<button class="btn btn-red btn-block" id="disableSeamlessBtn" style="display:none;"><span class="btn-text">禁用无感换号</span></button>
|
||
<button class="btn btn-blue btn-block" id="manualSwitchBtn" style="display:none;" disabled><span class="btn-text">一键换号(扣1积分)</span></button>
|
||
</div>
|
||
|
||
<!-- 账号用量 -->
|
||
<div class="section" id="usageSection" style="display:none;">
|
||
<div class="section-title">
|
||
<span class="icon">📊</span>
|
||
<span>账号用量</span>
|
||
<button class="btn" style="margin-left:auto;padding:4px 8px;font-size:11px;background:#3c3c3c;" id="refreshUsageBtn">🔄</button>
|
||
</div>
|
||
|
||
<div class="usage-row">
|
||
<div class="usage-item">
|
||
<span class="info-label">会员类型</span>
|
||
<span class="info-value" id="usageMemberType">-</span>
|
||
</div>
|
||
<div class="usage-item">
|
||
<span class="info-label">试用剩余</span>
|
||
<span class="info-value" id="usageTrialDays">-</span>
|
||
</div>
|
||
</div>
|
||
<div class="usage-row">
|
||
<div class="usage-item">
|
||
<span class="info-label">请求次数</span>
|
||
<span class="info-value" id="usageRequestCount">-</span>
|
||
</div>
|
||
<div class="usage-item">
|
||
<span class="info-label">已用额度</span>
|
||
<span class="info-value" id="usageCostUSD">-</span>
|
||
</div>
|
||
</div>
|
||
<p style="font-size:10px;color:#666;margin-top:8px;text-align:center;" id="usageUpdateTime">-</p>
|
||
</div>
|
||
|
||
<!-- 公告 -->
|
||
<div class="section" id="announcementSection" style="display:none;">
|
||
<div class="section-title">
|
||
<span class="icon" id="announcementIcon">📢</span>
|
||
<span>公告</span>
|
||
<span class="announcement-badge" id="announcementBadge">info</span>
|
||
</div>
|
||
<div class="announcement-title" id="announcementTitle"></div>
|
||
<div class="announcement-content" id="announcementContent"></div>
|
||
<p style="font-size:10px;color:#666;margin-top:8px;text-align:right;" id="announcementTime"></p>
|
||
</div>
|
||
|
||
<!-- 版本信息 -->
|
||
<div class="section" id="versionSection">
|
||
<div class="section-title">
|
||
<span class="icon">📦</span>
|
||
<span>版本信息</span>
|
||
<span class="status-badge" id="versionStatus" style="display:none;">有更新</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">当前版本</span>
|
||
<span class="info-value" id="currentVersion">-</span>
|
||
</div>
|
||
<div class="info-row" id="latestVersionRow" style="display:none;">
|
||
<span class="info-label">最新版本</span>
|
||
<span class="info-value" id="latestVersion" style="color:#4caf50;">-</span>
|
||
</div>
|
||
<p id="updateHint" style="font-size:11px;color:#ff9800;margin-top:8px;display:none;">
|
||
⚠️ 发现新版本,请更新插件以获取最新功能
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 页脚 -->
|
||
<div class="footer">
|
||
<div class="footer-row">
|
||
<div class="auto-start">
|
||
<span>自动启动</span>
|
||
<label class="switch switch-sm">
|
||
<input type="checkbox" id="autoStartSwitch" checked>
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="cursor-version">
|
||
<span>Cursor</span>
|
||
<span class="version-num" id="cursorVersion">0.0.0</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer-row" style="margin-top: 8px;">
|
||
<div style="font-size: 10px; color: #666; word-break: break-all;">
|
||
<span>路径: </span>
|
||
<span id="cursorPath" style="color: #888;">获取中...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast 通知 -->
|
||
<div class="toast-container" id="toastContainer">
|
||
<div class="toast" id="toast">
|
||
<span class="toast-icon" id="toastIcon">✅</span>
|
||
<span class="toast-message" id="toastMessage"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<script nonce="{{NONCE}}">
|
||
const vscode = acquireVsCodeApi();
|
||
|
||
// Trusted Types policy for innerHTML
|
||
var htmlPolicy = null;
|
||
if (typeof trustedTypes !== 'undefined' && trustedTypes.createPolicy) {
|
||
try { htmlPolicy = trustedTypes.createPolicy('hummingbird', { createHTML: function(html) { return html; } }); } catch(e) {}
|
||
}
|
||
function setInnerHTML(el, html) { if (htmlPolicy) { el.innerHTML = htmlPolicy.createHTML(html); } else { el.innerHTML = html; } }
|
||
|
||
// 元素引用
|
||
const keyInput = document.getElementById('keyInput');
|
||
const activateBtn = document.getElementById('activateBtn');
|
||
const switchBtn = document.getElementById('switchBtn');
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
const disableUpdateBtn = document.getElementById('disableUpdateBtn');
|
||
const cleanEnvBtn = document.getElementById('cleanEnvBtn');
|
||
const disableBtn = document.getElementById('disableBtn');
|
||
const authStatus = document.getElementById('authStatus');
|
||
const accountStatus = document.getElementById('accountStatus');
|
||
const keyDisplay = document.getElementById('keyDisplay');
|
||
const switchCount = document.getElementById('switchCount');
|
||
const expireDate = document.getElementById('expireDate');
|
||
const cursorVersion = document.getElementById('cursorVersion');
|
||
const cursorPath = document.getElementById('cursorPath');
|
||
|
||
// 离线状态元素
|
||
const offlineBanner = document.getElementById('offlineBanner');
|
||
const retryConnectBtn = document.getElementById('retryConnectBtn');
|
||
|
||
// 号池选择器元素
|
||
const poolAutoBtn = document.getElementById('poolAutoBtn');
|
||
const poolProBtn = document.getElementById('poolProBtn');
|
||
const autoPoolStatus = document.getElementById('autoPoolStatus');
|
||
const proPoolStatus = document.getElementById('proPoolStatus');
|
||
const autoKeyCard = document.getElementById('autoKeyCard');
|
||
const proKeyCard = document.getElementById('proKeyCard');
|
||
const autoKeyDisplay = document.getElementById('autoKeyDisplay');
|
||
const proKeyDisplay = document.getElementById('proKeyDisplay');
|
||
const autoKeyExpire = document.getElementById('autoKeyExpire');
|
||
const proKeyQuota = document.getElementById('proKeyQuota');
|
||
const clearAutoKeyBtn = document.getElementById('clearAutoKeyBtn');
|
||
const clearProKeyBtn = document.getElementById('clearProKeyBtn');
|
||
|
||
// 当前选中的号池
|
||
let selectedPool = 'auto';
|
||
// 双密钥存储
|
||
let autoKey = '';
|
||
let proKey = '';
|
||
let autoExpireDate = '';
|
||
let proQuota = 0;
|
||
let proQuotaUsed = 0;
|
||
|
||
// 无感换号元素
|
||
const seamlessStatus = document.getElementById('seamlessStatus');
|
||
const seamlessProxySwitch = document.getElementById('seamlessProxySwitch');
|
||
const enableSeamlessBtn = document.getElementById('enableSeamlessBtn');
|
||
const disableSeamlessBtn = document.getElementById('disableSeamlessBtn');
|
||
const manualSwitchBtn = document.getElementById('manualSwitchBtn');
|
||
const seamlessResetMachineBtn = document.getElementById('seamlessResetMachineBtn');
|
||
const seamlessSwitchRemaining = document.getElementById('seamlessSwitchRemaining');
|
||
const seamlessCurrentAccount = document.getElementById('seamlessCurrentAccount');
|
||
|
||
// 用量显示元素
|
||
const usageSection = document.getElementById('usageSection');
|
||
const refreshUsageBtn = document.getElementById('refreshUsageBtn');
|
||
const usageMemberType = document.getElementById('usageMemberType');
|
||
const usageTrialDays = document.getElementById('usageTrialDays');
|
||
const usageRequestCount = document.getElementById('usageRequestCount');
|
||
const usageCostUSD = document.getElementById('usageCostUSD');
|
||
const usageUpdateTime = document.getElementById('usageUpdateTime');
|
||
|
||
// 公告元素
|
||
const announcementSection = document.getElementById('announcementSection');
|
||
const announcementIcon = document.getElementById('announcementIcon');
|
||
const announcementBadge = document.getElementById('announcementBadge');
|
||
const announcementTitle = document.getElementById('announcementTitle');
|
||
const announcementContent = document.getElementById('announcementContent');
|
||
const announcementTime = document.getElementById('announcementTime');
|
||
|
||
// 版本元素
|
||
const versionSection = document.getElementById('versionSection');
|
||
const versionStatus = document.getElementById('versionStatus');
|
||
const currentVersionEl = document.getElementById('currentVersion');
|
||
const latestVersionEl = document.getElementById('latestVersion');
|
||
const latestVersionRow = document.getElementById('latestVersionRow');
|
||
const updateHint = document.getElementById('updateHint');
|
||
|
||
// 顶部更新提醒条
|
||
const updateBanner = document.getElementById('updateBanner');
|
||
const updateBannerVersion = document.getElementById('updateBannerVersion');
|
||
const updateBannerClose = document.getElementById('updateBannerClose');
|
||
|
||
// Toast 元素
|
||
const toast = document.getElementById('toast');
|
||
const toastIcon = document.getElementById('toastIcon');
|
||
const toastMessage = document.getElementById('toastMessage');
|
||
let toastTimer = null;
|
||
|
||
// 显示 Toast 通知
|
||
function showToast(message, icon = '✅', duration = 10000) {
|
||
// 清除之前的定时器
|
||
if (toastTimer) {
|
||
clearTimeout(toastTimer);
|
||
}
|
||
|
||
toastIcon.textContent = icon;
|
||
toastMessage.textContent = message;
|
||
toast.classList.add('show');
|
||
|
||
// 设置自动隐藏
|
||
toastTimer = setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
}, duration);
|
||
}
|
||
|
||
// 禁用换号按钮并显示倒计时
|
||
let switchBtnCountdownTimer = null;
|
||
const originalSwitchBtnText = '一键换号(扣1积分)';
|
||
|
||
function disableSwitchBtnWithCountdown(seconds) {
|
||
// 清除之前的定时器
|
||
if (switchBtnCountdownTimer) {
|
||
clearInterval(switchBtnCountdownTimer);
|
||
}
|
||
|
||
let remaining = seconds;
|
||
manualSwitchBtn.disabled = true;
|
||
manualSwitchBtn.querySelector('.btn-text').textContent = remaining + '秒后可用';
|
||
|
||
switchBtnCountdownTimer = setInterval(() => {
|
||
remaining--;
|
||
if (remaining <= 0) {
|
||
clearInterval(switchBtnCountdownTimer);
|
||
switchBtnCountdownTimer = null;
|
||
manualSwitchBtn.disabled = false;
|
||
manualSwitchBtn.querySelector('.btn-text').textContent = originalSwitchBtnText;
|
||
} else {
|
||
manualSwitchBtn.querySelector('.btn-text').textContent = remaining + '秒后可用';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// 弹窗元素
|
||
const adminModal = document.getElementById('adminModal');
|
||
const adminModalClose = document.getElementById('adminModalClose');
|
||
const resetPermissionModal = document.getElementById('resetPermissionModal');
|
||
const resetPermissionClose = document.getElementById('resetPermissionClose');
|
||
const restartModal = document.getElementById('restartModal');
|
||
const restartModalTitle = document.getElementById('restartModalTitle');
|
||
const restartNowBtn = document.getElementById('restartNowBtn');
|
||
const restartLaterBtn = document.getElementById('restartLaterBtn');
|
||
const expiredModal = document.getElementById('expiredModal');
|
||
const expiredModalClose = document.getElementById('expiredModalClose');
|
||
const cleanEnvModal = document.getElementById('cleanEnvModal');
|
||
const cleanEnvConfirmBtn = document.getElementById('cleanEnvConfirmBtn');
|
||
const cleanEnvCancelBtn = document.getElementById('cleanEnvCancelBtn');
|
||
|
||
// 换号确认弹窗元素
|
||
const switchConfirmModal = document.getElementById('switchConfirmModal');
|
||
const switchConfirmEmail = document.getElementById('switchConfirmEmail');
|
||
const switchConfirmCost = document.getElementById('switchConfirmCost');
|
||
const switchConfirmBtn = document.getElementById('switchConfirmBtn');
|
||
const switchCancelBtn = document.getElementById('switchCancelBtn');
|
||
|
||
// 显示管理员权限弹窗
|
||
function showAdminModal() {
|
||
adminModal.classList.add('show');
|
||
}
|
||
|
||
// 显示重置机器码权限提示弹窗
|
||
function showAdminPermissionModal() {
|
||
resetPermissionModal.classList.add('show');
|
||
}
|
||
|
||
// 重置机器码权限弹窗 - 关闭按钮
|
||
resetPermissionClose.addEventListener('click', () => {
|
||
resetPermissionModal.classList.remove('show');
|
||
});
|
||
|
||
// 点击遮罩关闭权限提示弹窗
|
||
resetPermissionModal.addEventListener('click', (e) => {
|
||
if (e.target === resetPermissionModal) {
|
||
resetPermissionModal.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 显示重启提示弹窗
|
||
let restartModalAction = 'reload'; // 'reload' 或 'close'
|
||
|
||
function showRestartModal(title, action = 'reload') {
|
||
restartModalTitle.textContent = title || '操作成功';
|
||
restartModalAction = action;
|
||
// 根据操作类型更新按钮文字
|
||
restartNowBtn.textContent = action === 'close' ? '立即关闭 Cursor' : '立即重启';
|
||
restartModal.classList.add('show');
|
||
}
|
||
|
||
// 显示过期弹窗
|
||
function showExpiredModal() {
|
||
expiredModal.classList.add('show');
|
||
}
|
||
|
||
// 关闭管理员弹窗
|
||
adminModalClose.addEventListener('click', () => {
|
||
adminModal.classList.remove('show');
|
||
});
|
||
|
||
// 点击遮罩关闭管理员弹窗
|
||
adminModal.addEventListener('click', (e) => {
|
||
if (e.target === adminModal) {
|
||
adminModal.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 立即重启/关闭按钮
|
||
restartNowBtn.addEventListener('click', () => {
|
||
restartModal.classList.remove('show');
|
||
if (restartModalAction === 'close') {
|
||
// 完全关闭 Cursor
|
||
vscode.postMessage({ type: 'closeCursor' });
|
||
} else {
|
||
// 重新加载窗口
|
||
vscode.postMessage({ type: 'reloadWindow' });
|
||
}
|
||
});
|
||
|
||
// 稍后手动按钮
|
||
restartLaterBtn.addEventListener('click', () => {
|
||
restartModal.classList.remove('show');
|
||
});
|
||
|
||
// 点击遮罩关闭重启弹窗
|
||
restartModal.addEventListener('click', (e) => {
|
||
if (e.target === restartModal) {
|
||
restartModal.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 关闭过期弹窗
|
||
expiredModalClose.addEventListener('click', () => {
|
||
expiredModal.classList.remove('show');
|
||
});
|
||
|
||
// 点击遮罩关闭过期弹窗
|
||
expiredModal.addEventListener('click', (e) => {
|
||
if (e.target === expiredModal) {
|
||
expiredModal.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 当前账号邮箱(用于查询用量)
|
||
let currentAccountEmail = '';
|
||
let usageRefreshInterval = null;
|
||
// 存储完整激活码(用于复制)
|
||
let fullActivationKey = '';
|
||
// 当前剩余换号次数
|
||
let currentSwitchRemaining = 0;
|
||
// 当前到期时间
|
||
let currentExpireDate = '';
|
||
|
||
// 检查卡密是否已过期
|
||
function isKeyExpired() {
|
||
if (!currentExpireDate) return true;
|
||
try {
|
||
const expireTime = new Date(currentExpireDate).getTime();
|
||
return Date.now() > expireTime;
|
||
} catch {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 格式化到期时间为北京时间
|
||
function formatExpireDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
try {
|
||
// 后端返回的时间没有时区标识,假设是 UTC 时间
|
||
// 将空格替换为T,并添加Z表示UTC
|
||
let utcStr = dateStr;
|
||
if (!dateStr.includes('T') && !dateStr.includes('Z') && !dateStr.includes('+')) {
|
||
utcStr = dateStr.replace(' ', 'T') + 'Z';
|
||
}
|
||
const date = new Date(utcStr);
|
||
|
||
// 使用中国时区格式化(UTC+8)
|
||
return date.toLocaleString('zh-CN', {
|
||
timeZone: 'Asia/Shanghai',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
});
|
||
} catch {
|
||
return dateStr; // 格式化失败返回原始值
|
||
}
|
||
}
|
||
|
||
// 隐藏激活码后几位
|
||
function maskKey(key) {
|
||
if (!key || key.length <= 8) return key;
|
||
return key.substring(0, key.length - 4) + '****';
|
||
}
|
||
|
||
// 点击激活码复制
|
||
keyDisplay.addEventListener('click', () => {
|
||
if (!fullActivationKey) return;
|
||
navigator.clipboard.writeText(fullActivationKey).then(() => {
|
||
keyDisplay.classList.add('copied');
|
||
const originalText = keyDisplay.textContent;
|
||
keyDisplay.textContent = '已复制!';
|
||
setTimeout(() => {
|
||
keyDisplay.textContent = maskKey(fullActivationKey);
|
||
keyDisplay.classList.remove('copied');
|
||
}, 1000);
|
||
}).catch(() => {
|
||
// 降级方案
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = fullActivationKey;
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textarea);
|
||
keyDisplay.classList.add('copied');
|
||
keyDisplay.textContent = '已复制!';
|
||
setTimeout(() => {
|
||
keyDisplay.textContent = maskKey(fullActivationKey);
|
||
keyDisplay.classList.remove('copied');
|
||
}, 1000);
|
||
});
|
||
});
|
||
|
||
// Loading 状态控制
|
||
function setButtonLoading(btn, loading) {
|
||
if (loading) {
|
||
btn.classList.add('loading');
|
||
btn.disabled = true;
|
||
} else {
|
||
btn.classList.remove('loading');
|
||
// 注意:某些按钮可能需要保持禁用状态,由调用方控制
|
||
}
|
||
}
|
||
|
||
function setRefreshLoading(btn, loading) {
|
||
if (loading) {
|
||
btn.classList.add('loading');
|
||
} else {
|
||
btn.classList.remove('loading');
|
||
}
|
||
}
|
||
|
||
// 获取初始状态
|
||
vscode.postMessage({ type: 'getState' });
|
||
vscode.postMessage({ type: 'getSeamlessStatus' });
|
||
vscode.postMessage({ type: 'getUserSwitchStatus' });
|
||
vscode.postMessage({ type: 'getProxyStatus' });
|
||
vscode.postMessage({ type: 'getAnnouncement' });
|
||
vscode.postMessage({ type: 'checkVersion' });
|
||
vscode.postMessage({ type: 'getCursorRunningPath' });
|
||
|
||
// ========== 号池选择器逻辑 ==========
|
||
|
||
// 选择号池
|
||
function selectPool(pool) {
|
||
selectedPool = pool;
|
||
if (pool === 'auto') {
|
||
poolAutoBtn.classList.add('active');
|
||
poolProBtn.classList.remove('active');
|
||
autoKeyCard.classList.add('active');
|
||
proKeyCard.classList.remove('active');
|
||
} else {
|
||
poolAutoBtn.classList.remove('active');
|
||
poolProBtn.classList.add('active');
|
||
autoKeyCard.classList.remove('active');
|
||
proKeyCard.classList.add('active');
|
||
}
|
||
// 通知后端选中的号池
|
||
vscode.postMessage({ type: 'selectPool', pool: pool });
|
||
}
|
||
|
||
// 更新双密钥显示
|
||
function updateDualKeyDisplay() {
|
||
// Auto密钥显示
|
||
if (autoKey) {
|
||
autoKeyDisplay.textContent = maskKey(autoKey);
|
||
autoKeyExpire.textContent = autoExpireDate ? '到期: ' + formatExpireDate(autoExpireDate) : '';
|
||
autoPoolStatus.textContent = '已激活';
|
||
clearAutoKeyBtn.style.display = 'inline';
|
||
} else {
|
||
autoKeyDisplay.textContent = '未激活';
|
||
autoKeyExpire.textContent = '';
|
||
autoPoolStatus.textContent = '未激活';
|
||
clearAutoKeyBtn.style.display = 'none';
|
||
}
|
||
|
||
// Pro密钥显示
|
||
if (proKey) {
|
||
proKeyDisplay.textContent = maskKey(proKey);
|
||
proKeyQuota.textContent = '积分: ' + (proQuota - proQuotaUsed) + '/' + proQuota;
|
||
proPoolStatus.textContent = '已激活';
|
||
clearProKeyBtn.style.display = 'inline';
|
||
} else {
|
||
proKeyDisplay.textContent = '未激活';
|
||
proKeyQuota.textContent = '';
|
||
proPoolStatus.textContent = '未激活';
|
||
clearProKeyBtn.style.display = 'none';
|
||
}
|
||
|
||
// 更新授权状态
|
||
if (autoKey || proKey) {
|
||
authStatus.textContent = '已授权';
|
||
authStatus.className = 'status-badge active';
|
||
} else {
|
||
authStatus.textContent = '未授权';
|
||
authStatus.className = 'status-badge inactive';
|
||
}
|
||
}
|
||
|
||
// 号池按钮点击
|
||
poolAutoBtn.addEventListener('click', () => selectPool('auto'));
|
||
poolProBtn.addEventListener('click', () => selectPool('pro'));
|
||
|
||
// 密钥卡片点击(切换选中)
|
||
autoKeyCard.addEventListener('click', (e) => {
|
||
if (e.target !== clearAutoKeyBtn) selectPool('auto');
|
||
});
|
||
proKeyCard.addEventListener('click', (e) => {
|
||
if (e.target !== clearProKeyBtn) selectPool('pro');
|
||
});
|
||
|
||
// 清除密钥按钮
|
||
clearAutoKeyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
if (confirm('确定要清除Auto密钥吗?')) {
|
||
vscode.postMessage({ type: 'clearKey', keyType: 'auto' });
|
||
}
|
||
});
|
||
clearProKeyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
if (confirm('确定要清除Pro密钥吗?')) {
|
||
vscode.postMessage({ type: 'clearKey', keyType: 'pro' });
|
||
}
|
||
});
|
||
|
||
// ========== 结束号池选择器逻辑 ==========
|
||
|
||
// 激活按钮
|
||
activateBtn.addEventListener('click', () => {
|
||
const key = keyInput.value.trim();
|
||
if (!key) {
|
||
return;
|
||
}
|
||
setButtonLoading(activateBtn, true);
|
||
vscode.postMessage({ type: 'activate', key });
|
||
});
|
||
|
||
// 换号按钮
|
||
switchBtn.addEventListener('click', () => {
|
||
vscode.postMessage({ type: 'switch' });
|
||
});
|
||
|
||
// 重置机器码按钮
|
||
resetBtn.addEventListener('click', () => {
|
||
vscode.postMessage({ type: 'resetMachineId' });
|
||
});
|
||
|
||
// 禁用自动更新按钮
|
||
disableUpdateBtn.addEventListener('click', () => {
|
||
vscode.postMessage({ type: 'disableUpdate' });
|
||
});
|
||
|
||
// 清理Cursor环境按钮 - 显示确认弹窗
|
||
cleanEnvBtn.addEventListener('click', () => {
|
||
cleanEnvModal.classList.add('show');
|
||
});
|
||
|
||
// 确认清理
|
||
cleanEnvConfirmBtn.addEventListener('click', () => {
|
||
cleanEnvModal.classList.remove('show');
|
||
vscode.postMessage({ type: 'cleanEnv' });
|
||
});
|
||
|
||
// 取消清理
|
||
cleanEnvCancelBtn.addEventListener('click', () => {
|
||
cleanEnvModal.classList.remove('show');
|
||
});
|
||
|
||
// 点击遮罩关闭清理弹窗
|
||
cleanEnvModal.addEventListener('click', (e) => {
|
||
if (e.target === cleanEnvModal) {
|
||
cleanEnvModal.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// 停用按钮
|
||
disableBtn.addEventListener('click', () => {
|
||
vscode.postMessage({ type: 'disable' });
|
||
});
|
||
|
||
// 关闭更新提醒条
|
||
updateBannerClose.addEventListener('click', () => {
|
||
updateBanner.classList.remove('show');
|
||
});
|
||
|
||
// 免魔法开关
|
||
seamlessProxySwitch.addEventListener('change', (e) => {
|
||
const wantEnabled = e.target.checked;
|
||
|
||
// 如果要开启免魔法,检查卡密是否过期(只要没过期就可以用,不管换号次数)
|
||
if (wantEnabled && isKeyExpired()) {
|
||
e.target.checked = false;
|
||
showToast('授权码已过期,无法开启免魔法', '⚠️', 3000);
|
||
return;
|
||
}
|
||
|
||
vscode.postMessage({
|
||
type: 'toggleProxy',
|
||
enabled: wantEnabled,
|
||
url: ''
|
||
});
|
||
});
|
||
|
||
// 无感换号 - 启用按钮
|
||
enableSeamlessBtn.addEventListener('click', () => {
|
||
setButtonLoading(enableSeamlessBtn, true);
|
||
vscode.postMessage({ type: 'injectSeamless' });
|
||
});
|
||
|
||
// 无感换号 - 禁用按钮
|
||
disableSeamlessBtn.addEventListener('click', () => {
|
||
setButtonLoading(disableSeamlessBtn, true);
|
||
vscode.postMessage({ type: 'restoreSeamless' });
|
||
});
|
||
|
||
// 无感换号 - 手动换号按钮(先检查用量)
|
||
manualSwitchBtn.addEventListener('click', () => {
|
||
setButtonLoading(manualSwitchBtn, true);
|
||
// 传递当前显示的账号邮箱
|
||
vscode.postMessage({ type: 'checkUsageBeforeSwitch', email: currentAccountEmail });
|
||
});
|
||
|
||
// 换号确认弹窗 - 确认按钮
|
||
switchConfirmBtn.addEventListener('click', () => {
|
||
switchConfirmModal.classList.remove('show');
|
||
setButtonLoading(manualSwitchBtn, true);
|
||
vscode.postMessage({ type: 'confirmSwitch' });
|
||
});
|
||
|
||
// 换号确认弹窗 - 取消按钮
|
||
switchCancelBtn.addEventListener('click', () => {
|
||
switchConfirmModal.classList.remove('show');
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
manualSwitchBtn.disabled = false;
|
||
});
|
||
|
||
// 换号确认弹窗 - 点击遮罩关闭
|
||
switchConfirmModal.addEventListener('click', (e) => {
|
||
if (e.target === switchConfirmModal) {
|
||
switchConfirmModal.classList.remove('show');
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
manualSwitchBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// 无感换号区域 - 重置机器码按钮
|
||
seamlessResetMachineBtn.addEventListener('click', () => {
|
||
vscode.postMessage({ type: 'resetMachineId' });
|
||
});
|
||
|
||
// 刷新用量按钮
|
||
refreshUsageBtn.addEventListener('click', () => {
|
||
if (currentAccountEmail) {
|
||
setRefreshLoading(refreshUsageBtn, true);
|
||
vscode.postMessage({ type: 'getAccountUsage', email: currentAccountEmail });
|
||
}
|
||
});
|
||
|
||
// 刷新用量函数
|
||
function refreshUsage() {
|
||
if (currentAccountEmail) {
|
||
vscode.postMessage({ type: 'getAccountUsage', email: currentAccountEmail });
|
||
}
|
||
}
|
||
|
||
// 启动用量定时刷新 (每分钟一次)
|
||
function startUsageRefresh() {
|
||
if (usageRefreshInterval) {
|
||
clearInterval(usageRefreshInterval);
|
||
}
|
||
// 立即刷新一次
|
||
refreshUsage();
|
||
// 每60秒刷新一次
|
||
usageRefreshInterval = setInterval(refreshUsage, 60000);
|
||
}
|
||
|
||
// 停止用量刷新
|
||
function stopUsageRefresh() {
|
||
if (usageRefreshInterval) {
|
||
clearInterval(usageRefreshInterval);
|
||
usageRefreshInterval = null;
|
||
}
|
||
}
|
||
|
||
// 更新用量显示
|
||
function updateUsageDisplay(data) {
|
||
if (!data) return;
|
||
|
||
const subscription = data.subscription || {};
|
||
const usage = data.usage || {};
|
||
|
||
// 会员类型
|
||
const memberTypeMap = {
|
||
'free_trial': '免费试用',
|
||
'pro': 'Pro会员',
|
||
'free': '免费版',
|
||
'business': '商业版'
|
||
};
|
||
usageMemberType.textContent = memberTypeMap[subscription.membershipType] || subscription.membershipType || '-';
|
||
|
||
// 试用剩余天数
|
||
if (subscription.daysRemainingOnTrial !== undefined && subscription.daysRemainingOnTrial !== null) {
|
||
usageTrialDays.textContent = subscription.daysRemainingOnTrial + ' 天';
|
||
usageTrialDays.style.color = subscription.daysRemainingOnTrial <= 3 ? '#f87171' : '#4ade80';
|
||
} else {
|
||
usageTrialDays.textContent = '-';
|
||
usageTrialDays.style.color = '#fff';
|
||
}
|
||
|
||
// 请求次数
|
||
usageRequestCount.textContent = (usage.totalUsageCount || 0) + ' 次';
|
||
|
||
// 已用额度
|
||
const costUSD = usage.totalCostUSD || 0;
|
||
usageCostUSD.textContent = '$' + costUSD.toFixed(2);
|
||
usageCostUSD.style.color = costUSD > 5 ? '#f87171' : (costUSD > 2 ? '#fbbf24' : '#4ade80');
|
||
|
||
// 更新时间
|
||
usageUpdateTime.textContent = '更新于 ' + new Date().toLocaleTimeString();
|
||
}
|
||
|
||
// 解析公告内容中的链接 {文字URL}
|
||
function parseAnnouncementContent(content) {
|
||
if (!content) return '';
|
||
|
||
// 转义 HTML 特殊字符
|
||
let escaped = content
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
|
||
// 匹配 {文字https://...} 或 {文字http://...} 格式
|
||
const linkRegex = /\{([^}]+?)(https?:\/\/[^}]+)\}/g;
|
||
|
||
escaped = escaped.replace(linkRegex, function(match, text, url) {
|
||
return '<a href="' + url + '" class="announcement-link" target="_blank">' + text + '</a>';
|
||
});
|
||
|
||
// 将换行符转换为 <br>
|
||
escaped = escaped.replace(/\n/g, '<br>');
|
||
|
||
return escaped;
|
||
}
|
||
|
||
// 更新公告显示
|
||
function updateAnnouncementDisplay(data) {
|
||
if (!data || !data.is_active) {
|
||
announcementSection.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 显示公告区域
|
||
announcementSection.style.display = 'block';
|
||
|
||
// 设置图标和类型徽章
|
||
const typeConfig = {
|
||
'info': { icon: '📢', text: '通知', class: 'info' },
|
||
'warning': { icon: '⚠️', text: '警告', class: 'warning' },
|
||
'error': { icon: '🚨', text: '重要', class: 'error' },
|
||
'success': { icon: '✅', text: '好消息', class: 'success' }
|
||
};
|
||
|
||
const config = typeConfig[data.type] || typeConfig.info;
|
||
announcementIcon.textContent = config.icon;
|
||
announcementBadge.textContent = config.text;
|
||
announcementBadge.className = 'announcement-badge ' + config.class;
|
||
|
||
// 设置标题和内容(解析链接)
|
||
announcementTitle.textContent = data.title || '';
|
||
setInnerHTML(announcementContent, parseAnnouncementContent(data.content || ''));
|
||
|
||
// 设置时间
|
||
if (data.created_at) {
|
||
const date = new Date(data.created_at);
|
||
announcementTime.textContent = date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
} else {
|
||
announcementTime.textContent = '';
|
||
}
|
||
}
|
||
|
||
// 处理来自扩展的消息
|
||
window.addEventListener('message', event => {
|
||
const message = event.data;
|
||
|
||
switch (message.type) {
|
||
case 'state':
|
||
updateUI(message);
|
||
break;
|
||
case 'activated':
|
||
setButtonLoading(activateBtn, false);
|
||
activateBtn.disabled = false;
|
||
if (message.success) {
|
||
// 调试日志
|
||
console.log('[蜂鸟Pro] 前端收到激活成功消息:', message);
|
||
|
||
const activatedKey = message.key || keyInput.value;
|
||
const membershipType = message.membershipType || message.membership_type || 'free';
|
||
const isAuto = membershipType === 'free' || membershipType === 'auto';
|
||
|
||
// 根据密钥类型存储到对应字段
|
||
if (isAuto) {
|
||
autoKey = activatedKey;
|
||
autoExpireDate = message.expireDate || message.expire_date || '';
|
||
// 自动选择Auto号池
|
||
selectPool('auto');
|
||
} else {
|
||
proKey = activatedKey;
|
||
proQuota = message.quota || 0;
|
||
proQuotaUsed = message.quotaUsed || message.quota_used || 0;
|
||
// 自动选择Pro号池
|
||
selectPool('pro');
|
||
}
|
||
|
||
// 更新双密钥显示
|
||
updateDualKeyDisplay();
|
||
|
||
// 兼容旧逻辑
|
||
authStatus.textContent = '已授权';
|
||
authStatus.className = 'status-badge active';
|
||
accountStatus.textContent = '已激活';
|
||
accountStatus.className = 'status-badge active';
|
||
switchBtn.disabled = false;
|
||
fullActivationKey = activatedKey;
|
||
keyDisplay.textContent = maskKey(fullActivationKey);
|
||
currentExpireDate = message.expireDate || message.expire_date || '';
|
||
expireDate.textContent = formatExpireDate(currentExpireDate) || '未知';
|
||
if (message.switchRemaining !== undefined) {
|
||
currentSwitchRemaining = message.switchRemaining;
|
||
}
|
||
|
||
// 清空输入框
|
||
keyInput.value = '';
|
||
const typeLabel = isAuto ? 'Auto' : 'Pro';
|
||
showToast(typeLabel + ' 密钥激活成功!', '✅', 10000);
|
||
} else {
|
||
showToast(message.error || '激活失败', '❌', 10000);
|
||
}
|
||
break;
|
||
case 'switched':
|
||
if (message.success) {
|
||
switchCount.textContent = message.switchRemaining + '/' + (message.switchLimit || 100);
|
||
showToast('换号成功: ' + (message.email || ''), '✅', 10000);
|
||
} else {
|
||
showToast(message.error || '换号失败', '❌', 10000);
|
||
}
|
||
break;
|
||
case 'reset':
|
||
authStatus.textContent = '未授权';
|
||
authStatus.className = 'status-badge inactive';
|
||
accountStatus.textContent = '未激活';
|
||
accountStatus.className = 'status-badge inactive';
|
||
switchBtn.disabled = true;
|
||
keyInput.value = '';
|
||
fullActivationKey = '';
|
||
keyDisplay.textContent = '尚未激活';
|
||
expireDate.textContent = '尚未激活';
|
||
// 清除双密钥
|
||
autoKey = '';
|
||
proKey = '';
|
||
autoExpireDate = '';
|
||
proQuota = 0;
|
||
proQuotaUsed = 0;
|
||
updateDualKeyDisplay();
|
||
break;
|
||
|
||
// 密钥清除
|
||
case 'keyCleared':
|
||
if (message.success) {
|
||
if (message.keyType === 'auto') {
|
||
autoKey = '';
|
||
autoExpireDate = '';
|
||
} else if (message.keyType === 'pro') {
|
||
proKey = '';
|
||
proQuota = 0;
|
||
proQuotaUsed = 0;
|
||
}
|
||
updateDualKeyDisplay();
|
||
showToast((message.keyType === 'auto' ? 'Auto' : 'Pro') + ' 密钥已清除', '✅', 3000);
|
||
}
|
||
break;
|
||
|
||
// 激活码状态检查结果
|
||
case 'keyStatusChecked':
|
||
if (message.valid) {
|
||
// 激活码有效,更新显示
|
||
currentExpireDate = message.expireDate || '';
|
||
currentSwitchRemaining = message.switchRemaining || 0;
|
||
expireDate.textContent = formatExpireDate(currentExpireDate);
|
||
switchCount.textContent = message.switchRemaining + '/' + (message.switchLimit || 100);
|
||
} else if (message.expired) {
|
||
// 激活码已过期,显示提示并重置状态
|
||
currentExpireDate = '';
|
||
currentSwitchRemaining = 0;
|
||
authStatus.textContent = '已过期';
|
||
authStatus.className = 'status-badge inactive';
|
||
authStatus.style.background = '#6e3232';
|
||
authStatus.style.color = '#ff6b6b';
|
||
expireDate.textContent = '已过期';
|
||
expireDate.style.color = '#f87171';
|
||
switchBtn.disabled = true;
|
||
enableSeamlessBtn.disabled = true;
|
||
// 如果免魔法已开启,自动关闭
|
||
if (seamlessProxySwitch.checked) {
|
||
seamlessProxySwitch.checked = false;
|
||
vscode.postMessage({ type: 'toggleProxy', enabled: false, url: '' });
|
||
}
|
||
// 显示过期弹窗
|
||
showExpiredModal();
|
||
}
|
||
break;
|
||
|
||
// 用户换号状态
|
||
case 'userSwitchStatus':
|
||
const remaining = message.switchRemaining || 0;
|
||
const canSwitch = remaining > 0;
|
||
|
||
// 更新全局变量
|
||
currentSwitchRemaining = remaining;
|
||
|
||
seamlessSwitchRemaining.textContent = remaining.toString();
|
||
seamlessSwitchRemaining.style.color = canSwitch ? '#4ade80' : '#f87171';
|
||
|
||
if (message.lockedAccount) {
|
||
seamlessCurrentAccount.textContent = message.lockedAccount.email;
|
||
|
||
// 设置当前账号邮箱并启动用量刷新
|
||
if (message.lockedAccount.email && message.lockedAccount.email !== currentAccountEmail) {
|
||
currentAccountEmail = message.lockedAccount.email;
|
||
usageSection.style.display = 'block';
|
||
startUsageRefresh();
|
||
}
|
||
} else {
|
||
seamlessCurrentAccount.textContent = '未分配';
|
||
|
||
// 没有锁定账号时隐藏用量区域
|
||
currentAccountEmail = '';
|
||
usageSection.style.display = 'none';
|
||
stopUsageRefresh();
|
||
}
|
||
|
||
// 根据剩余次数控制手动换号按钮状态
|
||
if (!canSwitch) {
|
||
manualSwitchBtn.disabled = true;
|
||
}
|
||
// 启用无感换号按钮不受积分限制,只有过期才禁用
|
||
enableSeamlessBtn.disabled = isKeyExpired();
|
||
|
||
// 如果无感换号已启用,显示手动换号按钮和重置机器码按钮
|
||
if (message.seamlessEnabled && canSwitch) {
|
||
manualSwitchBtn.style.display = 'block';
|
||
manualSwitchBtn.disabled = false;
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
seamlessResetMachineBtn.style.display = 'block';
|
||
}
|
||
break;
|
||
|
||
// 账号用量
|
||
case 'accountUsage':
|
||
setRefreshLoading(refreshUsageBtn, false);
|
||
if (message.success && message.data) {
|
||
updateUsageDisplay(message.data);
|
||
} else {
|
||
usageUpdateTime.textContent = '获取失败: ' + (message.error || '未知错误');
|
||
usageUpdateTime.style.color = '#f87171';
|
||
}
|
||
break;
|
||
|
||
// 无感换号状态
|
||
case 'seamlessStatus':
|
||
if (message.is_injected) {
|
||
seamlessStatus.textContent = '已启用';
|
||
seamlessStatus.className = 'status-badge active';
|
||
enableSeamlessBtn.style.display = 'none';
|
||
disableSeamlessBtn.style.display = 'block';
|
||
disableSeamlessBtn.disabled = false;
|
||
setButtonLoading(disableSeamlessBtn, false);
|
||
manualSwitchBtn.style.display = 'block';
|
||
manualSwitchBtn.disabled = false;
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
seamlessResetMachineBtn.style.display = 'block';
|
||
} else {
|
||
seamlessStatus.textContent = '未启用';
|
||
seamlessStatus.className = 'status-badge inactive';
|
||
enableSeamlessBtn.style.display = 'block';
|
||
setButtonLoading(enableSeamlessBtn, false);
|
||
// 启用按钮不受积分限制,只有过期才禁用
|
||
enableSeamlessBtn.disabled = isKeyExpired();
|
||
disableSeamlessBtn.style.display = 'none';
|
||
manualSwitchBtn.style.display = 'none';
|
||
seamlessResetMachineBtn.style.display = 'none';
|
||
}
|
||
break;
|
||
|
||
case 'seamlessInjected':
|
||
setButtonLoading(enableSeamlessBtn, false);
|
||
enableSeamlessBtn.disabled = false;
|
||
if (message.success) {
|
||
seamlessStatus.textContent = '已启用';
|
||
seamlessStatus.className = 'status-badge active';
|
||
enableSeamlessBtn.style.display = 'none';
|
||
disableSeamlessBtn.style.display = 'block';
|
||
manualSwitchBtn.style.display = 'block';
|
||
seamlessResetMachineBtn.style.display = 'block';
|
||
// 刷新用户状态
|
||
vscode.postMessage({ type: 'getUserSwitchStatus' });
|
||
// 显示重启提示弹窗
|
||
if (message.needRestart) {
|
||
showRestartModal(message.message || '无感换号已启用');
|
||
}
|
||
} else {
|
||
// 如果是权限错误,显示自定义弹窗
|
||
if (message.needAdmin) {
|
||
// Mac/Linux 权限问题,显示详细提示
|
||
var errorMsg = message.error || '没有写入权限';
|
||
if (message.path) {
|
||
errorMsg += '\n路径: ' + message.path;
|
||
}
|
||
showToast(errorMsg, '🔐', 15000);
|
||
} else {
|
||
// 显示详细错误
|
||
var detailMsg = message.error || '启用失败';
|
||
if (message.details) {
|
||
detailMsg += '\n' + message.details;
|
||
}
|
||
showToast(detailMsg, '❌', 15000);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'seamlessRestored':
|
||
setButtonLoading(disableSeamlessBtn, false);
|
||
disableSeamlessBtn.disabled = false;
|
||
if (message.success) {
|
||
seamlessStatus.textContent = '未启用';
|
||
seamlessStatus.className = 'status-badge inactive';
|
||
enableSeamlessBtn.style.display = 'block';
|
||
disableSeamlessBtn.style.display = 'none';
|
||
manualSwitchBtn.style.display = 'none';
|
||
seamlessResetMachineBtn.style.display = 'none';
|
||
// 显示重启提示弹窗
|
||
if (message.needRestart) {
|
||
showRestartModal(message.message || '无感换号已禁用');
|
||
}
|
||
} else {
|
||
// 如果是权限错误,显示自定义弹窗
|
||
if (message.needAdmin) {
|
||
showAdminModal();
|
||
} else {
|
||
showToast(message.error || '禁用失败', '❌', 10000);
|
||
}
|
||
}
|
||
break;
|
||
|
||
// 用量检查结果
|
||
case 'usageCheckResult':
|
||
if (message.success) {
|
||
if (message.needConfirm) {
|
||
// 需要确认,显示弹窗(按钮保持可用状态,等用户选择)
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
manualSwitchBtn.disabled = false;
|
||
switchConfirmEmail.textContent = message.email || '';
|
||
switchConfirmCost.textContent = '$' + (message.costUSD || '0.00');
|
||
switchConfirmModal.classList.add('show');
|
||
} else {
|
||
// 不需要确认,直接换号
|
||
vscode.postMessage({ type: 'confirmSwitch' });
|
||
}
|
||
} else {
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
manualSwitchBtn.disabled = false;
|
||
showToast(message.error || '检查失败', '❌', 5000);
|
||
}
|
||
break;
|
||
|
||
case 'manualSeamlessSwitched':
|
||
setButtonLoading(manualSwitchBtn, false);
|
||
if (message.success) {
|
||
seamlessSwitchRemaining.textContent = (message.switchRemaining || 0).toString();
|
||
seamlessCurrentAccount.textContent = message.email || '未知';
|
||
// 显示 Toast 通知,10秒后消失
|
||
showToast('已切换到: ' + (message.email || '新账号') + ',约10秒内自动生效', '✅', 10000);
|
||
// 刷新状态
|
||
vscode.postMessage({ type: 'getUserSwitchStatus' });
|
||
// 禁用按钮10秒,显示倒计时
|
||
disableSwitchBtnWithCountdown(10);
|
||
} else {
|
||
manualSwitchBtn.disabled = false;
|
||
showToast(message.error || '换号失败', '❌', 5000);
|
||
}
|
||
break;
|
||
|
||
case 'proxyStatus':
|
||
// 设置免魔法开关状态
|
||
seamlessProxySwitch.checked = message.enabled;
|
||
break;
|
||
|
||
// 公告
|
||
case 'announcement':
|
||
if (message.success && message.data) {
|
||
updateAnnouncementDisplay(message.data);
|
||
} else {
|
||
announcementSection.style.display = 'none';
|
||
}
|
||
break;
|
||
|
||
// 版本检查
|
||
case 'versionCheck':
|
||
currentVersionEl.textContent = message.currentVersion || '-';
|
||
if (message.success && message.hasUpdate) {
|
||
// 有更新
|
||
latestVersionEl.textContent = message.latestVersion;
|
||
latestVersionRow.style.display = 'flex';
|
||
versionStatus.style.display = 'inline-block';
|
||
versionStatus.style.background = '#ff9800';
|
||
updateHint.style.display = 'block';
|
||
|
||
// 显示顶部更新提醒条
|
||
updateBannerVersion.textContent = 'v' + message.latestVersion;
|
||
updateBanner.classList.add('show');
|
||
} else if (message.success) {
|
||
// 已是最新版
|
||
versionStatus.textContent = '最新';
|
||
versionStatus.style.display = 'inline-block';
|
||
versionStatus.style.background = '#4caf50';
|
||
latestVersionRow.style.display = 'none';
|
||
updateHint.style.display = 'none';
|
||
updateBanner.classList.remove('show');
|
||
}
|
||
break;
|
||
|
||
// Cursor 运行路径
|
||
case 'cursorRunningPath':
|
||
if (message.path) {
|
||
const pathText = message.path + (message.packageExists ? ' ✓' : ' ✗');
|
||
cursorPath.textContent = pathText;
|
||
cursorPath.style.color = message.packageExists ? '#4ade80' : '#f87171';
|
||
// 同时更新版本号
|
||
if (message.cursorVersion) {
|
||
cursorVersion.textContent = message.cursorVersion;
|
||
}
|
||
} else {
|
||
cursorPath.textContent = '未找到';
|
||
cursorPath.style.color = '#f87171';
|
||
}
|
||
break;
|
||
|
||
// 管理员权限不足提示
|
||
case 'adminPermissionRequired':
|
||
showAdminPermissionModal();
|
||
break;
|
||
|
||
// 机器码重置
|
||
case 'machineIdReset':
|
||
if (message.success && message.needRestart) {
|
||
// 机器码重置需要完全关闭 Cursor,不是 reload
|
||
showRestartModal(message.message || '机器码重置成功', 'close');
|
||
}
|
||
break;
|
||
|
||
// 通用 Toast 消息
|
||
case 'showToast':
|
||
showToast(message.message || '', message.icon || '📢', 10000);
|
||
break;
|
||
|
||
// 网络状态
|
||
case 'networkStatus':
|
||
updateOfflineStatus(!message.online);
|
||
break;
|
||
}
|
||
});
|
||
|
||
// 离线状态显示/隐藏
|
||
let wasOffline = false; // 跟踪之前是否离线
|
||
function updateOfflineStatus(isOffline) {
|
||
if (isOffline) {
|
||
offlineBanner.classList.add('show');
|
||
wasOffline = true;
|
||
} else {
|
||
offlineBanner.classList.remove('show');
|
||
// 只有从离线恢复到在线时才刷新状态
|
||
if (wasOffline) {
|
||
wasOffline = false;
|
||
vscode.postMessage({ type: 'getState' });
|
||
vscode.postMessage({ type: 'getUserSwitchStatus' });
|
||
}
|
||
}
|
||
}
|
||
|
||
// 重试连接按钮
|
||
retryConnectBtn.addEventListener('click', async () => {
|
||
retryConnectBtn.classList.add('loading');
|
||
retryConnectBtn.textContent = '连接中...';
|
||
|
||
// 发起真正的网络请求来测试网络
|
||
vscode.postMessage({ type: 'retryConnect' });
|
||
|
||
// 5秒后恢复按钮状态(给网络请求足够时间)
|
||
setTimeout(() => {
|
||
retryConnectBtn.classList.remove('loading');
|
||
retryConnectBtn.textContent = '重试';
|
||
}, 5000);
|
||
});
|
||
|
||
function updateUI(state) {
|
||
// 恢复双密钥状态
|
||
if (state.autoKey) {
|
||
autoKey = state.autoKey;
|
||
autoExpireDate = state.autoExpireDate || '';
|
||
}
|
||
if (state.proKey) {
|
||
proKey = state.proKey;
|
||
proQuota = state.proQuota || 0;
|
||
proQuotaUsed = state.proQuotaUsed || 0;
|
||
}
|
||
// 恢复选中的号池
|
||
if (state.selectedPool) {
|
||
selectPool(state.selectedPool);
|
||
}
|
||
// 更新双密钥显示
|
||
updateDualKeyDisplay();
|
||
|
||
// 兼容旧逻辑
|
||
if (state.isActivated) {
|
||
authStatus.textContent = '已授权';
|
||
authStatus.className = 'status-badge active';
|
||
accountStatus.textContent = '已激活';
|
||
accountStatus.className = 'status-badge active';
|
||
switchBtn.disabled = false;
|
||
fullActivationKey = state.key;
|
||
keyDisplay.textContent = maskKey(fullActivationKey);
|
||
// 更新到期时间
|
||
currentExpireDate = state.expireDate || '';
|
||
expireDate.textContent = formatExpireDate(currentExpireDate);
|
||
// 更新换号次数
|
||
if (state.switchRemaining !== undefined) {
|
||
currentSwitchRemaining = state.switchRemaining;
|
||
}
|
||
// 启用无感换号按钮(只有过期才禁用)
|
||
enableSeamlessBtn.disabled = isKeyExpired();
|
||
}
|
||
cursorVersion.textContent = state.cursorVersion || '0.0.0';
|
||
|
||
// 根据网络状态显示/隐藏离线提示
|
||
if (state.isOnline === false) {
|
||
offlineBanner.classList.add('show');
|
||
wasOffline = true;
|
||
} else if (state.isOnline === true) {
|
||
// 网络恢复,隐藏离线提示
|
||
offlineBanner.classList.remove('show');
|
||
wasOffline = false;
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |