feat(Hooks): 支持秘塔配置和图片搜索结果处理

useSettings:
- 添加 metasoApiKeyConfigured 状态
- 支持秘塔API Key的更新操作

useStreamChat:
- 添加 SearchImageData 类型定义
- 处理 tool_search_images 事件,实时展示搜索图片
- 处理 tool_used 事件,追踪使用的工具
- 添加搜索图片的数据库持久化逻辑
- ChatMessage 接口添加 searchImages 和 usedTools 字段
This commit is contained in:
gaoziman 2025-12-22 12:23:22 +08:00
parent 5a6a147bd8
commit 3459f3821f
2 changed files with 100 additions and 3 deletions

View File

@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from 'react';
export interface Settings { export interface Settings {
cchUrl: string; cchUrl: string;
cchApiKeyConfigured: boolean; cchApiKeyConfigured: boolean;
metasoApiKeyConfigured: boolean;
apiFormat: 'claude' | 'openai'; apiFormat: 'claude' | 'openai';
defaultModel: string; defaultModel: string;
defaultTools: string[]; defaultTools: string[];
@ -51,9 +52,10 @@ export interface Model {
const defaultSettings: Settings = { const defaultSettings: Settings = {
cchUrl: 'http://localhost:13500', cchUrl: 'http://localhost:13500',
cchApiKeyConfigured: false, cchApiKeyConfigured: false,
metasoApiKeyConfigured: false,
apiFormat: 'claude', apiFormat: 'claude',
defaultModel: 'claude-sonnet-4-5-20250929', defaultModel: 'claude-sonnet-4-5-20250929',
defaultTools: ['web_search', 'web_fetch'], defaultTools: ['web_search', 'web_fetch', 'mita_search', 'mita_reader'],
systemPrompt: '', systemPrompt: '',
temperature: '0.7', temperature: '0.7',
theme: 'light', theme: 'light',
@ -88,7 +90,7 @@ export function useSettings() {
}, []); }, []);
// 更新设置 // 更新设置
const updateSettings = useCallback(async (updates: Partial<Settings & { cchApiKey?: string }>) => { const updateSettings = useCallback(async (updates: Partial<Settings & { cchApiKey?: string; metasoApiKey?: string }>) => {
try { try {
setSaving(true); setSaving(true);
setError(null); setError(null);

View File

@ -4,7 +4,7 @@ import { useState, useCallback, useRef } from 'react';
import { executePythonInPyodide, type LoadingCallback } from '@/services/tools/pyodideRunner'; import { executePythonInPyodide, type LoadingCallback } from '@/services/tools/pyodideRunner';
export interface StreamMessage { export interface StreamMessage {
type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'pyodide_execution_required' | 'done' | 'error'; type: 'thinking' | 'text' | 'tool_use_start' | 'tool_execution_result' | 'tool_search_images' | 'pyodide_execution_required' | 'tool_used' | 'done' | 'error';
content?: string; content?: string;
id?: string; id?: string;
name?: string; name?: string;
@ -19,6 +19,22 @@ export interface StreamMessage {
success?: boolean; success?: boolean;
result?: string; result?: string;
images?: string[]; images?: string[];
// 搜索到的图片
searchImages?: SearchImageData[];
// 工具使用相关
toolName?: string;
usedTools?: string[];
}
// 搜索图片数据类型
export interface SearchImageData {
title: string;
imageUrl: string;
width: number;
height: number;
score: string;
position: number;
sourceUrl?: string;
} }
export interface ChatMessage { export interface ChatMessage {
@ -32,10 +48,14 @@ export interface ChatMessage {
outputTokens?: number; outputTokens?: number;
// 工具执行产生的图片 // 工具执行产生的图片
images?: string[]; images?: string[];
// 搜索到的图片
searchImages?: SearchImageData[];
// 用户上传的图片Base64 // 用户上传的图片Base64
uploadedImages?: string[]; uploadedImages?: string[];
// 用户上传的文档 // 用户上传的文档
uploadedDocuments?: UploadedDocument[]; uploadedDocuments?: UploadedDocument[];
// 使用的工具列表
usedTools?: string[];
// Pyodide 加载状态 // Pyodide 加载状态
pyodideStatus?: { pyodideStatus?: {
stage: 'loading' | 'ready' | 'error'; stage: 'loading' | 'ready' | 'error';
@ -67,6 +87,29 @@ async function saveMessageImages(messageId: string, images: string[]): Promise<v
} }
} }
/**
*
*/
async function saveMessageSearchImages(messageId: string, searchImages: SearchImageData[]): Promise<void> {
if (!messageId || !searchImages || searchImages.length === 0) return;
try {
const response = await fetch(`/api/messages/${messageId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ searchImages }),
});
if (!response.ok) {
console.error('Failed to save search images:', await response.text());
}
} catch (error) {
console.error('Error saving search images:', error);
}
}
/** /**
* Base64 * Base64
*/ */
@ -159,6 +202,8 @@ export function useStreamChat() {
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
// 临时存储 Pyodide 执行产生的图片,等待 messageId // 临时存储 Pyodide 执行产生的图片,等待 messageId
const pendingImagesRef = useRef<string[]>([]); const pendingImagesRef = useRef<string[]>([]);
// 临时存储搜索到的图片,等待 messageId
const pendingSearchImagesRef = useRef<SearchImageData[]>([]);
// 发送消息 // 发送消息
const sendMessage = useCallback(async (options: { const sendMessage = useCallback(async (options: {
@ -371,6 +416,47 @@ export function useStreamChat() {
return updated; return updated;
}); });
} }
} else if (event.type === 'tool_search_images') {
// 处理图片搜索结果
if (event.searchImages && event.searchImages.length > 0) {
// 存储到临时变量,等待 messageId 后保存到数据库
pendingSearchImagesRef.current = [
...pendingSearchImagesRef.current,
...event.searchImages,
];
// 更新 UI
setMessages((prev) => {
const updated = [...prev];
const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') {
const existingSearchImages = updated[lastIndex].searchImages || [];
updated[lastIndex] = {
...updated[lastIndex],
searchImages: [...existingSearchImages, ...event.searchImages!],
};
}
return updated;
});
}
} else if (event.type === 'tool_used') {
// 实时工具使用事件
if (event.toolName) {
setMessages((prev) => {
const updated = [...prev];
const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') {
const existingTools = updated[lastIndex].usedTools || [];
// 避免重复添加
if (!existingTools.includes(event.toolName!)) {
updated[lastIndex] = {
...updated[lastIndex],
usedTools: [...existingTools, event.toolName!],
};
}
}
return updated;
});
}
} else if (event.type === 'pyodide_execution_required') { } else if (event.type === 'pyodide_execution_required') {
// 需要在浏览器端执行 Python 图形代码 // 需要在浏览器端执行 Python 图形代码
const code = event.code || ''; const code = event.code || '';
@ -444,16 +530,25 @@ export function useStreamChat() {
pendingImagesRef.current = []; // 清空临时存储 pendingImagesRef.current = []; // 清空临时存储
} }
// 如果有待保存的搜索图片,保存到数据库
if (event.messageId && pendingSearchImagesRef.current.length > 0) {
saveMessageSearchImages(event.messageId, pendingSearchImagesRef.current);
pendingSearchImagesRef.current = []; // 清空临时存储
}
setMessages((prev) => { setMessages((prev) => {
const updated = [...prev]; const updated = [...prev];
const lastIndex = updated.length - 1; const lastIndex = updated.length - 1;
if (updated[lastIndex]?.role === 'assistant') { if (updated[lastIndex]?.role === 'assistant') {
// 如果 done 事件包含 usedTools使用它保证完整性
const finalUsedTools = event.usedTools || updated[lastIndex].usedTools;
updated[lastIndex] = { updated[lastIndex] = {
...updated[lastIndex], ...updated[lastIndex],
id: event.messageId || updated[lastIndex].id, id: event.messageId || updated[lastIndex].id,
status: 'completed', status: 'completed',
inputTokens: event.inputTokens, inputTokens: event.inputTokens,
outputTokens: event.outputTokens, outputTokens: event.outputTokens,
usedTools: finalUsedTools,
}; };
} }
return updated; return updated;