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

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:
iven
2026-04-05 07:57:53 +08:00
parent d6b1f44119
commit af0acff2aa
6 changed files with 81 additions and 11 deletions

View File

@@ -79,7 +79,18 @@ export function ChatArea() {
provider: m.provider_id,
}));
}
return configModels;
if (configModels.length > 0) {
return configModels;
}
// Fallback: provide common models when no backend is connected
return [
{ id: 'glm-4-flash', name: 'GLM-4 Flash', provider: 'zhipu' },
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai' },
{ id: 'deepseek-chat', name: 'DeepSeek V3', provider: 'deepseek' },
{ id: 'qwen-max', name: 'Qwen Max', provider: 'qwen' },
{ id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', provider: 'anthropic' },
];
}, [isLoggedIn, saasModels, configModels]);
const [input, setInput] = useState('');

View File

@@ -171,7 +171,7 @@ function ConversationItem({
);
}
export function ConversationList() {
export function ConversationList({ searchQuery = '' }: { searchQuery?: string }) {
const conversations = useConversationStore((s) => s.conversations);
const currentConversationId = useConversationStore((s) => s.currentConversationId);
const { switchConversation, deleteConversation } = useChatStore();
@@ -192,13 +192,40 @@ export function ConversationList() {
exportConversation(conv.title, conv.messages);
};
// Deduplicate by id (keep most recent) then filter by search query
const uniqueConversations = (() => {
const seen = new Map<string, typeof conversations[number]>();
for (const conv of conversations) {
const existing = seen.get(conv.id);
if (!existing || conv.updatedAt > existing.updatedAt) {
seen.set(conv.id, conv);
}
}
return Array.from(seen.values()).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
})();
const filtered = searchQuery.trim()
? uniqueConversations.filter((c) =>
c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.messages.some((m) => m.content.toLowerCase().includes(searchQuery.toLowerCase()))
)
: uniqueConversations;
if (conversations.length === 0) {
return <EmptyConversations />;
}
if (filtered.length === 0 && searchQuery.trim()) {
return (
<div className="text-center text-sm text-gray-400 py-4">
</div>
);
}
return (
<div className="flex flex-col gap-0.5 py-1">
{conversations.map((conv) => (
{filtered.map((conv) => (
<ConversationItem
key={conv.id}
id={conv.id}

View File

@@ -707,7 +707,11 @@ export function RightPanel() {
</div>
)}
{error && (
<p className="mt-2 text-xs text-red-500 truncate" title={error}>{error}</p>
<p className="mt-2 text-xs text-red-500 truncate" title={error}>
{error.includes('Cannot read properties') || error.includes('TypeError')
? '连接状态获取失败,请重新连接'
: error}
</p>
)}
</motion.div>

View File

@@ -123,7 +123,7 @@ export function Sidebar({
</button>
)}
</div>
<ConversationList />
<ConversationList searchQuery={searchQuery} />
</div>
)}
{activeTab === 'clones' && <CloneManager />}