Files
cursornew2026/extension_clean/out/webview/panel_formatted.html
ccdojox-crypto 73a71f198f 蜂鸟Pro v2.0.1 - 基础框架版本 (待完善)
## 当前状态
- 插件界面已完成重命名 (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>
2025-12-18 11:21:52 +08:00

1995 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
}
/* 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="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="info-row">
<span class="info-label">激活码</span>
<span class="info-value key-display" id="keyDisplay" title="点击复制">尚未激活</span>
</div>
<div class="info-row">
<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 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' });
// 激活按钮
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
// 匹配 {文字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);
authStatus.textContent = '已授权';
authStatus.className = 'status-badge active';
accountStatus.textContent = '已激活';
accountStatus.className = 'status-badge active';
switchBtn.disabled = false;
// 更新激活码显示(使用后端返回的 key
fullActivationKey = message.key || keyInput.value;
keyDisplay.textContent = maskKey(fullActivationKey);
// 更新到期时间
console.log('[蜂鸟Pro] 更新到期时间:', message.expireDate);
currentExpireDate = message.expireDate || '';
expireDate.textContent = formatExpireDate(currentExpireDate) || '未知';
// 更新换号次数
if (message.switchRemaining !== undefined) {
currentSwitchRemaining = message.switchRemaining;
switchCount.textContent = message.switchRemaining + '/' + (message.switchLimit || 100);
}
// 清空输入框
keyInput.value = '';
showToast('授权码激活成功!', '✅', 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 = '尚未激活';
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 += '\
路径: ' + message.path;
}
showToast(errorMsg, '🔐', 15000);
} else {
// 显示详细错误
var detailMsg = message.error || '启用失败';
if (message.details) {
detailMsg += '\
' + 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.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;
switchCount.textContent = state.switchRemaining + '/' + (state.switchLimit || 100);
}
// 启用无感换号按钮(只有过期才禁用)
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>