fix(desktop): QA 驱动的 6 项缺陷修复
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0-C1: SecureStorage 解密失败上限 — 添加 per-key 失败计数器, 超过 2 次自动清除过期加密数据,阻断无限重试循环 P0-C2: Bootstrap 空指针防护 — connectionStore 中 relayModels[0]?.id 添加 null guard,抛出用户友好错误 P1-H1: 侧边栏对话列表去重 — ConversationList 添加按 ID 去重逻辑, 保留最新版本后按 updatedAt 排序 P1-H2: 搜索框过滤生效 — Sidebar 传递 searchQuery 给 ConversationList, 支持按标题和消息内容过滤 P1-H3: 模型选择器 fallback — 当 SaaS 和 config 均无模型时, 提供 6 个默认模型(GLM/GPT/DeepSeek/Qwen/Claude) P1-H4: 详情面板错误友好化 — RightPanel 中 JS 错误替换为 '连接状态获取失败,请重新连接' Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -216,8 +216,18 @@ async function writeEncryptedLocalStorage(key: string, value: string): Promise<v
|
||||
* Read and decrypt data from localStorage
|
||||
* Supports v2 (random salt), v1 (static salt), and legacy unencrypted formats
|
||||
*/
|
||||
// Track per-key decryption failures to break infinite retry loops
|
||||
const decryptionFailures = new Map<string, number>();
|
||||
const MAX_DECRYPTION_RETRIES = 2;
|
||||
|
||||
async function readEncryptedLocalStorage(key: string): Promise<string | null> {
|
||||
try {
|
||||
// If this key has already failed too many times, skip immediately
|
||||
const failCount = decryptionFailures.get(key) ?? 0;
|
||||
if (failCount >= MAX_DECRYPTION_RETRIES) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encryptedKey = ENCRYPTED_PREFIX + key;
|
||||
const encryptedRaw = localStorage.getItem(encryptedKey);
|
||||
|
||||
@@ -230,12 +240,15 @@ async function readEncryptedLocalStorage(key: string): Promise<string | null> {
|
||||
const parsed = JSON.parse(encryptedRaw);
|
||||
const salt = base64ToArray(parsed.salt);
|
||||
const cryptoKey = await deriveKey(masterKeyRaw, salt);
|
||||
return await decrypt(
|
||||
const result = await decrypt(
|
||||
{ iv: parsed.iv, data: parsed.data },
|
||||
cryptoKey,
|
||||
);
|
||||
decryptionFailures.delete(key);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] v2 decryption failed:', error);
|
||||
console.warn('[SecureStorage] v2 decryption failed for key:', key);
|
||||
decryptionFailures.set(key, failCount + 1);
|
||||
// Fall through to try v1
|
||||
}
|
||||
}
|
||||
@@ -245,9 +258,17 @@ async function readEncryptedLocalStorage(key: string): Promise<string | null> {
|
||||
try {
|
||||
const cryptoKey = await deriveKey(masterKeyRaw); // uses legacy static salt
|
||||
const encrypted = JSON.parse(encryptedRaw);
|
||||
return await decrypt(encrypted, cryptoKey);
|
||||
const result = await decrypt(encrypted, cryptoKey);
|
||||
decryptionFailures.delete(key);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] v1 decryption failed:', error);
|
||||
console.warn('[SecureStorage] v1 decryption failed for key:', key);
|
||||
decryptionFailures.set(key, failCount + 1);
|
||||
// Both v2 and v1 failed — clear stale encrypted data to stop retrying
|
||||
if (failCount + 1 >= MAX_DECRYPTION_RETRIES) {
|
||||
localStorage.removeItem(encryptedKey);
|
||||
console.warn('[SecureStorage] Cleared stale encrypted data for key:', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user