feat(web): 添加全局 Toast 通知系统和资产预览导航功能

- 新增 ToastProvider 和 useToast hook,支持全局成功/错误/信息提示
- 资产预览增加左右导航按钮、键盘快捷键(方向键)和计数器显示
- 资产库增加图片/视频类型筛选标签页及计数
- 对话列表增加最近对话展示、搜索优化和删除确认
- 消息增加删除确认对话框
- 优化聊天自动滚动行为,仅在用户未手动滚动时跟随新内容
- 新增删除消息 API 端点
- 优化消息历史清理逻辑,过滤错误消息和孤儿 tool 消息
- 添加自定义滚动条样式
- 优化账户参考图显示逻辑,支持本地文件显示
- 修复对话创建流程,直接导航到新创建的对话
This commit is contained in:
2026-05-08 00:23:36 +08:00
parent 088bdb9a8e
commit 66d170066a
23 changed files with 801 additions and 374 deletions

View File

@@ -381,6 +381,48 @@ async function streamOpenAI(
}
}
// Clean and validate message history before sending to API
function sanitizeHistory(messages: DbMessage[]): DbMessage[] {
// 1. Filter out error messages stored in DB
const cleaned = messages.filter((m) => {
if (m.role === 'assistant' && m.content.startsWith('抱歉,出错了:')) return false;
return true;
});
// 2. Validate sequence: tool messages must follow assistant with tool_calls
const result: DbMessage[] = [];
for (let i = 0; i < cleaned.length; i++) {
const msg = cleaned[i];
// Tool message: check if preceding assistant has tool_calls
if (msg.role === 'tool') {
let hasPrecedingToolCall = false;
for (let j = result.length - 1; j >= 0; j--) {
const prev = result[j];
if (prev.role === 'assistant') {
if (prev.tool_calls) {
try {
const parsed = JSON.parse(prev.tool_calls);
const blocks = Array.isArray(parsed) ? parsed : parsed.content_blocks;
if (blocks?.some((b: ContentBlock) => b.type === 'tool_use')) {
hasPrecedingToolCall = true;
}
} catch {}
}
break; // stop at nearest assistant
}
if (prev.role === 'tool') continue; // skip consecutive tool messages
break;
}
if (!hasPrecedingToolCall) continue; // skip orphan tool message
}
result.push(msg);
}
return result;
}
async function handleChatMessage(ws: WebSocket, convId: string, content: string) {
const userMsgId = randomUUID();
getDb().prepare(
@@ -398,9 +440,10 @@ async function handleChatMessage(ws: WebSocket, convId: string, content: string)
}
getDb().prepare('UPDATE conversations SET updated_at = datetime(\'now\') WHERE id = ?').run(convId);
const history = getDb().prepare(
const rawHistory = getDb().prepare(
'SELECT * FROM messages WHERE conversation_id = ? AND id != ? ORDER BY created_at'
).all(convId, userMsgId) as DbMessage[];
const history = sanitizeHistory(rawHistory);
ws.send(JSON.stringify({ type: 'status', data: { status: 'thinking' } }));
@@ -419,9 +462,7 @@ async function handleChatMessage(ws: WebSocket, convId: string, content: string)
const errMsg = (err as Error).message;
console.error('[chat] LLM error:', errMsg);
const errId = randomUUID();
getDb().prepare(
'INSERT INTO messages (id, conversation_id, role, content) VALUES (?, ?, ?, ?)'
).run(errId, convId, 'assistant', `抱歉,出错了:${errMsg}`);
// Don't store error in DB to avoid polluting history
ws.send(JSON.stringify({
type: 'message',
data: { id: errId, role: 'assistant', content: `抱歉,出错了:${errMsg}` },