fix: 密钥失效后自动清除并刷新UI

- _checkKeyStatus 清除无效密钥后重新调用 _sendState() 刷新面板
- 区分网络离线和后端明确返回无效,避免离线时误清密钥

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 20:18:02 +08:00
parent ac19d029da
commit cd427ede80

View File

@@ -186,6 +186,22 @@ class HummingbirdProViewProvider {
this._sendState();
this._checkKeyStatus();
}
/**
* 获取当前激活的密钥(统一入口)
* 优先返回当前选中号池的密钥,兼容旧版本的 hummingbird.key
*/
_getActiveKey() {
const selectedPool = this._context.globalState.get("hummingbird.selectedPool") || "auto";
const key = selectedPool === "pro"
? this._context.globalState.get("hummingbird.proKey")
: this._context.globalState.get("hummingbird.autoKey");
// 兼容旧版本:如果新字段没有值,回退到旧字段
if (!key) {
return this._context.globalState.get("hummingbird.key");
}
return key;
}
async _checkKeyStatus() {
// 检查 Auto 和 Pro 两个密钥的状态
const autoKey = this._context.globalState.get("hummingbird.autoKey");
@@ -205,6 +221,9 @@ class HummingbirdProViewProvider {
try {
const verifyResult = await client_1.verifyKey(autoKey);
if (verifyResult.success && verifyResult.valid) {
if (verifyResult.key && verifyResult.key !== autoKey) {
await this._context.globalState.update("hummingbird.autoKey", verifyResult.key);
}
keyStatus.auto = {
valid: true,
expireDate: verifyResult.expire_date,
@@ -212,8 +231,14 @@ class HummingbirdProViewProvider {
mergedCount: verifyResult.merged_count || 0
};
await this._context.globalState.update("hummingbird.autoExpireDate", verifyResult.expire_date);
} else {
keyStatus.auto = { valid: false, error: verifyResult.error };
} else if (!verifyResult.isOffline) {
// 后端明确返回无效(非网络错误),自动清除本地 Auto 密钥
console.warn("[蜂鸟Pro] Auto 密钥已失效,自动清除:", verifyResult.error);
await this._context.globalState.update("hummingbird.autoKey", undefined);
await this._context.globalState.update("hummingbird.autoExpireDate", undefined);
await this._context.globalState.update("hummingbird.autoMergedCount", undefined);
await this._context.globalState.update("hummingbird.key", undefined);
keyStatus.auto = { valid: false, error: verifyResult.error, cleared: true };
}
} catch (err) {
console.error("[蜂鸟Pro] 检查 Auto 密钥状态失败:", err);
@@ -225,6 +250,9 @@ class HummingbirdProViewProvider {
try {
const verifyResult = await client_1.verifyKey(proKey);
if (verifyResult.success && verifyResult.valid) {
if (verifyResult.key && verifyResult.key !== proKey) {
await this._context.globalState.update("hummingbird.proKey", verifyResult.key);
}
keyStatus.pro = {
valid: true,
quota: verifyResult.quota,
@@ -234,19 +262,45 @@ class HummingbirdProViewProvider {
};
await this._context.globalState.update("hummingbird.proQuota", verifyResult.quota);
await this._context.globalState.update("hummingbird.proQuotaUsed", verifyResult.quota_used);
} else {
keyStatus.pro = { valid: false, error: verifyResult.error };
} else if (!verifyResult.isOffline) {
// 后端明确返回无效(非网络错误),自动清除本地 Pro 密钥
console.warn("[蜂鸟Pro] Pro 密钥已失效,自动清除:", verifyResult.error);
await this._context.globalState.update("hummingbird.proKey", undefined);
await this._context.globalState.update("hummingbird.proQuota", undefined);
await this._context.globalState.update("hummingbird.proQuotaUsed", undefined);
await this._context.globalState.update("hummingbird.proMergedCount", undefined);
keyStatus.pro = { valid: false, error: verifyResult.error, cleared: true };
}
} catch (err) {
console.error("[蜂鸟Pro] 检查 Pro 密钥状态失败:", err);
}
}
// 如果有密钥被清除,同时清理无感换号状态
if (keyStatus.auto?.cleared || keyStatus.pro?.cleared) {
const remainingAuto = this._context.globalState.get("hummingbird.autoKey");
const remainingPro = this._context.globalState.get("hummingbird.proKey");
if (!remainingAuto && !remainingPro) {
// 所有密钥都失效了,清理全部状态
await this._context.globalState.update("hummingbird.selectedPool", undefined);
await this._context.globalState.update("hummingbird.seamlessInjected", undefined);
await this._context.globalState.update("hummingbird.seamlessCurrentAccount", undefined);
await this._context.globalState.update("hummingbird.expireDate", undefined);
await this._context.globalState.update("hummingbird.switchRemaining", undefined);
extension_1.hideStatusBar();
}
}
this._postMessage({
'type': "keyStatusChecked",
'auto': keyStatus.auto,
'pro': keyStatus.pro
});
// 如果有密钥被清除重新发送状态给面板刷新UI
if (keyStatus.auto?.cleared || keyStatus.pro?.cleared) {
await this._sendState();
}
}
async _handleActivate(key) {
try {
@@ -262,8 +316,9 @@ class HummingbirdProViewProvider {
this._cleanProxySettings();
const verifyResult = await client_1.verifyKey(key);
if (verifyResult.success && verifyResult.valid) {
const membershipType = verifyResult.membership_type || 'free';
const isAuto = membershipType === 'free';
const membershipType = verifyResult.membership_type || 'auto';
const isAuto = membershipType === 'auto' || membershipType === 'free';
const activatedKey = verifyResult.key || key;
console.log("[蜂鸟Pro] 激活成功,类型:", membershipType, "后端返回:", {
'expire_date': verifyResult.expire_date,
@@ -274,20 +329,25 @@ class HummingbirdProViewProvider {
// 根据类型存储到不同字段
if (isAuto) {
await this._context.globalState.update("hummingbird.autoKey", key);
await this._context.globalState.update("hummingbird.autoKey", activatedKey);
await this._context.globalState.update("hummingbird.autoExpireDate", verifyResult.expire_date);
await this._context.globalState.update("hummingbird.autoMergedCount", verifyResult.merged_count || 0);
// 同步写回旧字段(兼容旧逻辑)
await this._context.globalState.update("hummingbird.key", activatedKey);
await this._context.globalState.update("hummingbird.expireDate", verifyResult.expire_date);
} else {
await this._context.globalState.update("hummingbird.proKey", key);
await this._context.globalState.update("hummingbird.proKey", activatedKey);
await this._context.globalState.update("hummingbird.proQuota", verifyResult.quota);
await this._context.globalState.update("hummingbird.proQuotaUsed", verifyResult.quota_used || 0);
await this._context.globalState.update("hummingbird.proMergedCount", verifyResult.merged_count || 0);
// 同步写回旧字段(兼容旧逻辑)
await this._context.globalState.update("hummingbird.key", activatedKey);
}
this._postMessage({
'type': "activated",
'success': true,
'key': key,
'key': activatedKey,
'membershipType': membershipType,
'expireDate': verifyResult.expire_date,
'quota': verifyResult.quota,
@@ -332,6 +392,15 @@ class HummingbirdProViewProvider {
const switchResult = await client_1.switchSeamlessToken(savedKey);
if (switchResult.switched) {
await this._context.globalState.update("hummingbird.switchRemaining", switchResult.switchRemaining);
// 写入新 token 到本地 Cursor 存储
if (switchResult.data) {
try {
await this._writeAccountToLocal(switchResult.data);
console.log("[蜂鸟Pro] 换号成功,已写入新 token");
} catch (writeErr) {
console.error("[蜂鸟Pro] 写入 token 失败:", writeErr);
}
}
this._postMessage({
'type': "switched",
'success': true,
@@ -365,10 +434,10 @@ class HummingbirdProViewProvider {
}
async _writeAccountToLocal(accountData) {
try {
const condition = process.env.APPDATA || '';
const joinedPath = path.join(condition, "Cursor", "User", "globalStorage", "state.vscdb");
const joinedPath1 = path.join(condition, "Cursor", "User", "globalStorage", 'storage.json');
const joinedPath2 = path.join(condition, "Cursor", "machineid");
const cursorPaths = account_1.getCursorPaths();
const joinedPath = cursorPaths.dbPath;
const joinedPath1 = cursorPaths.storagePath;
const joinedPath2 = cursorPaths.machineidPath;
if (fs.existsSync(joinedPath)) {
const items = [];
if (accountData.accessToken) {
@@ -377,6 +446,9 @@ class HummingbirdProViewProvider {
if (accountData.refreshToken) {
items.push(["cursorAuth/refreshToken", accountData.refreshToken]);
}
if (accountData.workosSessionToken) {
items.push(["cursorAuth/WorkosCursorSessionToken", accountData.workosSessionToken]);
}
if (accountData.email) {
items.push(["cursorAuth/cachedEmail", accountData.email]);
}
@@ -433,6 +505,17 @@ class HummingbirdProViewProvider {
await this._context.globalState.update("hummingbird.key", undefined);
await this._context.globalState.update("hummingbird.expireDate", undefined);
await this._context.globalState.update("hummingbird.switchRemaining", undefined);
// 清理双池相关状态
await this._context.globalState.update("hummingbird.autoKey", undefined);
await this._context.globalState.update("hummingbird.proKey", undefined);
await this._context.globalState.update("hummingbird.autoExpireDate", undefined);
await this._context.globalState.update("hummingbird.proQuota", undefined);
await this._context.globalState.update("hummingbird.proQuotaUsed", undefined);
await this._context.globalState.update("hummingbird.proMergedCount", undefined);
await this._context.globalState.update("hummingbird.autoMergedCount", undefined);
await this._context.globalState.update("hummingbird.selectedPool", undefined);
await this._context.globalState.update("hummingbird.seamlessInjected", undefined);
await this._context.globalState.update("hummingbird.seamlessCurrentAccount", undefined);
extension_1.hideStatusBar();
this._postMessage({
'type': 'reset',
@@ -473,9 +556,9 @@ class HummingbirdProViewProvider {
}
const hostLine = account_1.getCursorPaths();
const {
dbPath: charIdx,
storagePath: lineItem,
machineidPath: lineIdx
dbPath: dbPath,
storagePath: storagePath,
machineidPath: machineidPath
} = hostLine;
const module = require("crypto");
const str = module.randomBytes(32).toString("hex");
@@ -484,16 +567,16 @@ class HummingbirdProViewProvider {
const result = '{' + module.randomUUID().toUpperCase() + '}';
let count = 0;
let items = [];
if (fs.existsSync(lineItem)) {
if (fs.existsSync(storagePath)) {
let num = 3;
while (num > 0) {
try {
const parsed = JSON.parse(fs.readFileSync(lineItem, "utf-8"));
const parsed = JSON.parse(fs.readFileSync(storagePath, "utf-8"));
parsed["telemetry.machineId"] = str;
parsed["telemetry.macMachineId"] = str1;
parsed["telemetry.devDeviceId"] = proxyLine;
parsed["telemetry.sqmId"] = result;
fs.writeFileSync(lineItem, JSON.stringify(parsed, null, 4));
fs.writeFileSync(storagePath, JSON.stringify(parsed, null, 4));
console.log("[蜂鸟Pro] storage.json 已更新");
count++;
break;
@@ -512,13 +595,13 @@ class HummingbirdProViewProvider {
let num = 3;
while (num > 0) {
try {
const dirPath = path.dirname(lineIdx);
const dirPath = path.dirname(machineidPath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, {
'recursive': true
});
}
fs.writeFileSync(lineIdx, str);
fs.writeFileSync(machineidPath, str);
console.log("[蜂鸟Pro] machineid 文件已更新");
count++;
break;
@@ -533,12 +616,12 @@ class HummingbirdProViewProvider {
}
}
}
if (fs.existsSync(charIdx)) {
if (fs.existsSync(dbPath)) {
let num = 3;
while (num > 0) {
try {
const proxyEntry = module.randomUUID();
const newHostsContent = await sqlite_1.sqliteSetBatch(charIdx, [['storage.serviceMachineId', proxyEntry]]);
const newHostsContent = await sqlite_1.sqliteSetBatch(dbPath, [['storage.serviceMachineId', proxyEntry]]);
if (newHostsContent) {
console.log("[蜂鸟Pro] SQLite 数据库已更新");
count++;
@@ -787,7 +870,7 @@ class HummingbirdProViewProvider {
if (lockedInfo) {
try {
fs.writeFileSync(content1, content, "utf-8");
remainingCount = true;
isFalse = true;
} catch (writeErr2) {
console.log("[蜂鸟Pro] Write still failed after permission grant");
}
@@ -815,8 +898,8 @@ class HummingbirdProViewProvider {
if (process.platform === "darwin") {
const pathStr = "/tmp/hosts_cursor_temp";
fs.writeFileSync(pathStr, content, "utf-8");
const content1 = "do shell script \"cp '" + pathStr + "' '" + content1 + "' && rm '" + pathStr + "' && dscacheutil -flushcache && killall -HUP mDNSResponder\" with administrator privileges";
await execAsync('osascript -e "' + content1.replace(/"/g, "\\\"") + "\"");
const osaCmd = "do shell script \"cp '" + pathStr + "' '" + content1 + "' && rm '" + pathStr + "' && dscacheutil -flushcache && killall -HUP mDNSResponder\" with administrator privileges";
await execAsync('osascript -e "' + osaCmd.replace(/"/g, "\\\"") + "\"");
} else {
fs.writeFileSync(content1, content, "utf-8");
}
@@ -830,7 +913,7 @@ class HummingbirdProViewProvider {
async _handleToggleProxy(enabled, silent) {
try {
if (enabled) {
const savedKey = this._context.globalState.get("hummingbird.key");
const savedKey = this._getActiveKey();
const expireDate = this._context.globalState.get('hummingbird.expireDate');
if (!savedKey) {
this._postMessage({
@@ -1012,7 +1095,7 @@ class HummingbirdProViewProvider {
if (regOut2 && regOut2.trim()) {
const matchResult = regOut2.match(/InstallLocation\s+REG_SZ\s+(.+)/);
if (matchResult && matchResult[1] && fs.existsSync(matchResult[1].trim())) {
cursorPath = matchResult[1].trim();
result = matchResult[1].trim();
}
}
} catch (e5) {
@@ -1048,7 +1131,7 @@ class HummingbirdProViewProvider {
for (const str of parts) {
const trimmed = str.trim();
if (trimmed && fs.existsSync(trimmed)) {
cursorPath = path.dirname(trimmed);
result = path.dirname(trimmed);
break;
}
}
@@ -1091,7 +1174,7 @@ class HummingbirdProViewProvider {
if (fileItem && fileItem.trim()) {
const matchResult = fileItem.match(/(\/.+\.app)/);
if (matchResult) {
cursorPath = matchResult[1];
result = matchResult[1];
}
}
} catch (findErr) {
@@ -1123,7 +1206,7 @@ class HummingbirdProViewProvider {
} = await execAsync("readlink -f /proc/" + condition + "/exe 2>/dev/null");
if (subDir && subDir.trim()) {
const trimmed = subDir.trim();
cursorPath = path.dirname(trimmed);
result = path.dirname(trimmed);
if (result.endsWith("/bin")) {
result = path.dirname(result);
}
@@ -1138,9 +1221,9 @@ class HummingbirdProViewProvider {
if (subItem && subItem.trim()) {
const execResult = await execAsync('readlink -f "' + subItem.trim() + '" 2>/dev/null');
if (execResult.stdout && execResult.stdout.trim()) {
cursorPath = path.dirname(execResult.stdout.trim());
result = path.dirname(execResult.stdout.trim());
if (result.endsWith('/bin')) {
cursorPath = path.dirname(result);
result = path.dirname(result);
}
}
}
@@ -1260,7 +1343,7 @@ class HummingbirdProViewProvider {
}
async _handleInjectSeamless() {
try {
const savedKey = this._context.globalState.get("hummingbird.key");
const savedKey = this._getActiveKey();
if (!savedKey) {
this._postMessage({
'type': "seamlessInjected",
@@ -1269,15 +1352,35 @@ class HummingbirdProViewProvider {
});
return;
}
const status = await client_1.getUserSwitchStatus(savedKey);
if (!status.valid) {
// 1. 先调用后端启用无感换号(分配账号)
const deviceId = client_1.getDeviceId();
console.log("[蜂鸟Pro] 正在调用后端启用无感换号...");
const enableResult = await client_1.enableSeamless(savedKey, deviceId);
if (!enableResult.success) {
this._postMessage({
'type': "seamlessInjected",
'success': false,
'error': status.error || "授权码无效"
'error': enableResult.error || "启用无感换号失败"
});
return;
}
// 2. 验证账号数据
const accountData = enableResult.data?.account;
if (!accountData || !(accountData.token || accountData.access_token)) {
this._postMessage({
'type': "seamlessInjected",
'success': false,
'error': "后端未返回账号信息"
});
return;
}
console.log("[蜂鸟Pro] 后端分配账号成功:", accountData.email);
// 3. 获取 workbench 路径并进行本地文件注入
const workbenchPath = await this._getWorkbenchPathAsync();
if (!workbenchPath) {
this._postMessage({
@@ -1368,12 +1471,33 @@ class HummingbirdProViewProvider {
throw writeErr;
}
await this._context.globalState.update("hummingbird.seamlessInjected", true);
// 4. 注入成功后,将后端分配的账号信息写入 Cursor
console.log("[蜂鸟Pro] 正在写入账号信息到 Cursor...");
try {
await this._writeAccountToLocal({
accessToken: accountData.access_token || accountData.token,
refreshToken: accountData.refresh_token,
workosSessionToken: accountData.workos_session_token,
email: accountData.email,
membership_type: accountData.membership_type
});
console.log("[蜂鸟Pro] 账号信息写入成功");
} catch (writeAccountErr) {
console.error("[蜂鸟Pro] 写入账号信息失败:", writeAccountErr);
// 写入失败不阻塞整个流程,因为注入已经成功
}
this._postMessage({
'type': 'seamlessInjected',
'success': true,
'applied': items,
'needRestart': true,
'message': "无感换号已启用"
'message': "无感换号已启用",
'account': {
email: accountData.email,
membership_type: accountData.membership_type
}
});
} catch (appDir) {
console.error("[蜂鸟Pro] Inject error:", appDir);
@@ -1435,6 +1559,17 @@ class HummingbirdProViewProvider {
}
throw writeErr;
}
// 通知后端释放锁定的账号
try {
const savedKey = this._context.globalState.get("hummingbird.key");
if (savedKey) {
await client_1.disableSeamless(savedKey);
}
} catch (disableErr) {
console.warn("[蜂鸟Pro] 通知后端禁用无感换号失败:", disableErr);
}
this._postMessage({
'type': "seamlessRestored",
'success': true,
@@ -1480,7 +1615,7 @@ class HummingbirdProViewProvider {
}
async _handleGetUserSwitchStatus() {
try {
const savedKey = this._context.globalState.get('hummingbird.key');
const savedKey = this._getActiveKey();
if (!savedKey) {
this._postMessage({
'type': "userSwitchStatus",
@@ -1713,7 +1848,7 @@ class HummingbirdProViewProvider {
}
async _handleCheckUsageBeforeSwitch(silent) {
try {
const savedKey = this._context.globalState.get("hummingbird.key");
const savedKey = this._getActiveKey();
if (!savedKey) {
this._postMessage({
'type': "usageCheckResult",
@@ -2129,4 +2264,4 @@ class HummingbirdProViewProvider {
// 以下为占位注释,保持文件结构不变
exports.HummingbirdProViewProvider = HummingbirdProViewProvider;
HummingbirdProViewProvider.CURRENT_VERSION = '2.0.0';
HummingbirdProViewProvider.CURRENT_VERSION = '2.1.0';