feat(Hooks): 支持秘塔配置和图片搜索结果处理
useSettings: - 添加 metasoApiKeyConfigured 状态 - 支持秘塔API Key的更新操作 useStreamChat: - 添加 SearchImageData 类型定义 - 处理 tool_search_images 事件,实时展示搜索图片 - 处理 tool_used 事件,追踪使用的工具 - 添加搜索图片的数据库持久化逻辑 - ChatMessage 接口添加 searchImages 和 usedTools 字段
This commit is contained in:
parent
5a6a147bd8
commit
3459f3821f
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user