更新ios

This commit is contained in:
2026-01-19 17:29:55 +08:00
parent cbde81d6cd
commit 639325b7c9
11 changed files with 512 additions and 201 deletions

View File

@@ -204,7 +204,10 @@ function CustomDrawerContent({
return ( return (
<Pressable <Pressable
key={index} key={index}
onPress={() => navigation.navigate(item.navigateTo)} onPress={() => {
navigation.navigate(item.navigateTo);
navigation.closeDrawer();
}}
mb={2} mb={2}
> >
{isFocused ? ( {isFocused ? (

View File

@@ -266,12 +266,13 @@ function ArticlesStack(props) {
} }
// 事件中心导航栈 // 事件中心导航栈
function EventsStack(props) { function EventsStack({ navigation }) {
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
mode: "card", mode: "card",
headerShown: false, // 隐藏默认header使用自定义 headerShown: false, // 隐藏默认header使用自定义
gestureEnabled: true, // 启用返回手势
}} }}
> >
<Stack.Screen <Stack.Screen
@@ -280,12 +281,25 @@ function EventsStack(props) {
options={{ options={{
cardStyle: { backgroundColor: "#0F172A" }, cardStyle: { backgroundColor: "#0F172A" },
}} }}
listeners={{
focus: () => {
// 在列表页允许 Drawer 手势
navigation.setOptions({ swipeEnabled: true });
},
}}
/> />
<Stack.Screen <Stack.Screen
name="EventDetail" name="EventDetail"
component={EventDetail} component={EventDetail}
options={{ options={{
cardStyle: { backgroundColor: "#0F172A" }, cardStyle: { backgroundColor: "#0F172A" },
gestureEnabled: true, // 确保返回手势启用
}}
listeners={{
focus: () => {
// 在详情页禁用 Drawer 手势,让滑动只触发返回
navigation.setOptions({ swipeEnabled: false });
},
}} }}
/> />
<Stack.Screen <Stack.Screen
@@ -293,6 +307,12 @@ function EventsStack(props) {
component={StockDetailScreen} component={StockDetailScreen}
options={{ options={{
cardStyle: { backgroundColor: "#0A0A0F" }, cardStyle: { backgroundColor: "#0A0A0F" },
gestureEnabled: true,
}}
listeners={{
focus: () => {
navigation.setOptions({ swipeEnabled: false });
},
}} }}
/> />
</Stack.Navigator> </Stack.Navigator>

View File

@@ -27,6 +27,7 @@ export const SSEEventTypes = {
STEP_START: 'step_start', STEP_START: 'step_start',
STEP_COMPLETE: 'step_complete', STEP_COMPLETE: 'step_complete',
SUMMARY_CHUNK: 'summary_chunk', SUMMARY_CHUNK: 'summary_chunk',
SUMMARY_RESET: 'summary_reset', // 总结阶段检测到新工具调用,重置已有内容
SESSION_TITLE: 'session_title', SESSION_TITLE: 'session_title',
DONE: 'done', DONE: 'done',
ERROR: 'error', ERROR: 'error',

View File

@@ -26,24 +26,41 @@ export const AuthProvider = ({ children }) => {
// 先检查本地是否有登录状态 // 先检查本地是否有登录状态
const storedUser = await authService.getStoredUser(); const storedUser = await authService.getStoredUser();
if (storedUser) { const hasToken = await authService.getAccessToken();
if (storedUser && hasToken) {
// 有本地用户信息和 token先恢复登录状态
setUser(storedUser); setUser(storedUser);
setIsLoggedIn(true); setIsLoggedIn(true);
console.log('[AuthContext] 从本地恢复登录状态:', storedUser.username);
// 然后尝试从服务器获取最新用户信息 // 然后在后台尝试从服务器验证并获取最新用户信息
// 使用 setTimeout 确保 UI 先渲染
setTimeout(async () => {
try {
const serverUser = await authService.getCurrentUser(); const serverUser = await authService.getCurrentUser();
if (serverUser) { if (serverUser) {
setUser(serverUser); setUser(serverUser);
console.log('[AuthContext] 服务器验证成功,更新用户信息');
// 获取订阅信息 // 获取订阅信息
const subInfo = await authService.getSubscription(); const subInfo = await authService.getSubscription();
if (subInfo.success) { if (subInfo.success) {
setSubscription(subInfo.data || subInfo.subscription); setSubscription(subInfo.data || subInfo.subscription);
} }
} else { } else {
// 服务器验证失败,清除本地状态 // 服务器验证失败,但保留本地登录状态
setUser(null); // 用户可以继续使用,下次操作时如果需要会提示重新登录
setIsLoggedIn(false); console.warn('[AuthContext] 服务器验证失败,保留本地登录状态');
} }
} catch (error) {
// 网络错误等,保留本地状态
console.warn('[AuthContext] 后台验证出错,保留本地登录状态:', error.message);
}
}, 100);
} else if (storedUser) {
// 有用户信息但没有 token需要重新登录
console.log('[AuthContext] 没有有效 token需要重新登录');
await authService.clearLocalAuth();
} }
} catch (error) { } catch (error) {
console.error('[AuthContext] 初始化失败:', error); console.error('[AuthContext] 初始化失败:', error);

View File

@@ -212,6 +212,15 @@ export const useAgentChat = () => {
})); }));
break; break;
case SSEEventTypes.SUMMARY_RESET:
// 总结阶段检测到新工具调用,重置已有内容
// 这发生在模型在总结过程中又输出了工具调用标签的情况
console.log('[useAgentChat] 收到 summary_reset 事件,重置响应内容');
responseContentRef.current = '';
// 移除已有的 AGENT_RESPONSE 消息
dispatch(removeMessagesByType(MessageTypes.AGENT_RESPONSE));
break;
case SSEEventTypes.SUMMARY_CHUNK: case SSEEventTypes.SUMMARY_CHUNK:
// 总结流式输出 // 总结流式输出
if (data?.content) { if (data?.content) {

View File

@@ -44,14 +44,15 @@ import {
/** /**
* 头部导航栏 * 头部导航栏
*/ */
const Header = ({ title, onMenuPress, onNewPress }) => ( const Header = ({ title, onHistoryPress, onNewPress }) => (
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={onMenuPress} onPress={onHistoryPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.headerButtonText}></Text> {/* 使用时钟图标表示历史记录 */}
<Text style={styles.headerButtonText}>🕐</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerCenter}> <View style={styles.headerCenter}>
@@ -68,7 +69,8 @@ const Header = ({ title, onMenuPress, onNewPress }) => (
onPress={onNewPress} onPress={onNewPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={styles.headerButtonText}>+</Text> {/* 使用铅笔图标表示新建对话 */}
<Text style={styles.headerButtonText}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@@ -201,7 +203,7 @@ const AgentChatScreen = ({ navigation }) => {
{/* 头部 */} {/* 头部 */}
<Header <Header
title={currentSessionTitle || '价小前'} title={currentSessionTitle || '价小前'}
onMenuPress={openDrawer} onHistoryPress={openDrawer}
onNewPress={handleNewSession} onNewPress={handleNewSession}
/> />

View File

@@ -345,6 +345,13 @@ const ChannelList = ({ navigation }) => {
} }
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
// 性能优化 - 防止真机渲染问题
initialNumToRender={20}
maxToRenderPerBatch={20}
windowSize={10}
removeClippedSubviews={false}
// 确保所有 section 都渲染
getItemLayout={undefined}
/> />
</Box> </Box>
); );

View File

@@ -286,7 +286,7 @@ const StockDetailScreen = () => {
return ( return (
<Box flex={1} bg="#0A0A0F"> <Box flex={1} bg="#0A0A0F">
<SafeAreaView style={styles.container} edges={['top']}> <SafeAreaView style={styles.container} edges={['top']}>
{/* 价格头部 - Wind 风格(固定在顶部) */} {/* 价格头部 - 固定在顶部,使用 WebSocket 实时行情 */}
<PriceHeader <PriceHeader
stock={{ stock_code: stockCode, stock_name: displayStockName }} stock={{ stock_code: stockCode, stock_name: displayStockName }}
quote={quote} quote={quote}
@@ -296,6 +296,14 @@ const StockDetailScreen = () => {
isRealtime={wsConnected && !!realtimeQuote?.current_price} isRealtime={wsConnected && !!realtimeQuote?.current_price}
/> />
{/* 可滚动区域 - 包含图表和其他内容 */}
<ScrollView
flex={1}
showsVerticalScrollIndicator={false}
nestedScrollEnabled={true}
contentContainerStyle={styles.scrollContent}
bounces={true}
>
{/* 图表类型切换 */} {/* 图表类型切换 */}
<ChartTypeTabs <ChartTypeTabs
activeType={chartType} activeType={chartType}
@@ -321,13 +329,6 @@ const StockDetailScreen = () => {
)} )}
</Box> </Box>
{/* 5档盘口 + 行情数据 + 相关信息(可滚动区域) */}
<ScrollView
flex={1}
showsVerticalScrollIndicator={false}
nestedScrollEnabled={true}
contentContainerStyle={styles.scrollContent}
>
{/* 5档盘口 - 优先 WebSocket 实时数据,降级到 API 数据 */} {/* 5档盘口 - 优先 WebSocket 实时数据,降级到 API 数据 */}
{chartType === 'minute' && ( {chartType === 'minute' && (
<OrderBook <OrderBook
@@ -344,8 +345,8 @@ const StockDetailScreen = () => {
{/* 相关信息 Tab */} {/* 相关信息 Tab */}
<RelatedInfoTabs activeTab={infoTab} onChange={setInfoTab} /> <RelatedInfoTabs activeTab={infoTab} onChange={setInfoTab} />
{/* 相关信息内容(给予固定最小高度,内部自己滚动) */} {/* 相关信息内容 */}
<Box minH={350} pb={4}> <Box minH={350} pb={100}>
{renderInfoContent()} {renderInfoContent()}
</Box> </Box>
</ScrollView> </ScrollView>

View File

@@ -3,8 +3,8 @@
* 支持搜索股票并添加到自选 * 支持搜索股票并添加到自选
*/ */
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef, useMemo, memo } from 'react';
import { StyleSheet, Keyboard, ActivityIndicator } from 'react-native'; import { Keyboard, ActivityIndicator, Dimensions } from 'react-native';
import { import {
Modal, Modal,
Box, Box,
@@ -18,24 +18,33 @@ import {
useToast, useToast,
} from 'native-base'; } from 'native-base';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import { stockDetailService } from '../../services/stockService'; import { stockDetailService } from '../../services/stockService';
import { useWatchlist } from '../../hooks/useWatchlist'; import { useWatchlist } from '../../hooks/useWatchlist';
// 搜索结果项组件 const { height: SCREEN_HEIGHT } = Dimensions.get('window');
const SearchResultItem = ({ item, onAdd, isAdding, isInWatchlist }) => {
// 列表项高度常量(用于 getItemLayout 优化)
const ITEM_HEIGHT = 56;
// 搜索结果项组件 - 使用 memo 避免不必要的重渲染
const SearchResultItem = memo(({ item, onAdd, isAdding, alreadyAdded }) => {
const { stock_code, stock_name, industry } = item; const { stock_code, stock_name, industry } = item;
const alreadyAdded = isInWatchlist(stock_code);
const handlePress = useCallback(() => {
if (!alreadyAdded && !isAdding) {
onAdd(item);
}
}, [alreadyAdded, isAdding, onAdd, item]);
return ( return (
<Pressable <Pressable
onPress={() => !alreadyAdded && !isAdding && onAdd(item)} onPress={handlePress}
disabled={alreadyAdded || isAdding} disabled={alreadyAdded || isAdding}
> >
{({ pressed }) => ( {({ pressed }) => (
<HStack <HStack
h={ITEM_HEIGHT}
px={4} px={4}
py={3}
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
bg={pressed && !alreadyAdded ? 'rgba(255,255,255,0.05)' : 'transparent'} bg={pressed && !alreadyAdded ? 'rgba(255,255,255,0.05)' : 'transparent'}
@@ -95,7 +104,7 @@ const SearchResultItem = ({ item, onAdd, isAdding, isInWatchlist }) => {
)} )}
</Pressable> </Pressable>
); );
}; });
// 热门股票配置 // 热门股票配置
const HOT_STOCKS = [ const HOT_STOCKS = [
@@ -121,11 +130,24 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [addingCode, setAddingCode] = useState(null); const [addingCode, setAddingCode] = useState(null);
const searchTimeoutRef = useRef(null); const searchTimeoutRef = useRef(null);
const abortControllerRef = useRef(null);
const toast = useToast(); const toast = useToast();
const { addStock, isInWatchlist } = useWatchlist({ autoLoad: false }); const { addStock, isInWatchlist, stocks } = useWatchlist({ autoLoad: false });
// 搜索股票 // 创建已添加股票代码的 Set 用于快速查找
const watchlistCodesSet = useMemo(() => {
const normalizeCode = (code) => String(code).match(/\d{6}/)?.[0] || code;
return new Set(stocks.map(s => normalizeCode(s.stock_code)));
}, [stocks]);
// 检查是否在自选中(使用 Set 优化查找)
const checkInWatchlist = useCallback((stockCode) => {
const normalizeCode = (code) => String(code).match(/\d{6}/)?.[0] || code;
return watchlistCodesSet.has(normalizeCode(stockCode));
}, [watchlistCodesSet]);
// 搜索股票(带取消功能)
const handleSearch = useCallback(async (text) => { const handleSearch = useCallback(async (text) => {
setSearchText(text); setSearchText(text);
@@ -134,14 +156,22 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
clearTimeout(searchTimeoutRef.current); clearTimeout(searchTimeoutRef.current);
} }
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
if (!text.trim()) { if (!text.trim()) {
setSearchResults([]); setSearchResults([]);
setIsSearching(false);
return; return;
} }
// 防抖搜索 // 防抖搜索 - 200ms 更快响应
searchTimeoutRef.current = setTimeout(async () => { searchTimeoutRef.current = setTimeout(async () => {
setIsSearching(true); setIsSearching(true);
abortControllerRef.current = new AbortController();
try { try {
const response = await stockDetailService.searchStocks(text, 20); const response = await stockDetailService.searchStocks(text, 20);
if (response.success && Array.isArray(response.data)) { if (response.success && Array.isArray(response.data)) {
@@ -150,12 +180,14 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
setSearchResults([]); setSearchResults([]);
} }
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') {
console.error('搜索股票失败:', error); console.error('搜索股票失败:', error);
setSearchResults([]); setSearchResults([]);
}
} finally { } finally {
setIsSearching(false); setIsSearching(false);
} }
}, 300); }, 200);
}, []); }, []);
// 添加到自选 // 添加到自选
@@ -189,35 +221,67 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
} }
}, [addStock, toast]); }, [addStock, toast]);
// 关闭弹窗 // 关闭弹窗 - 清理资源
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
Keyboard.dismiss(); Keyboard.dismiss();
// 取消进行中的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
setSearchText(''); setSearchText('');
setSearchResults([]); setSearchResults([]);
setIsSearching(false);
onClose?.(); onClose?.();
}, [onClose]); }, [onClose]);
// 渲染列表数据 // 渲染列表数据 - 使用 useMemo 缓存
const displayData = searchText.trim() ? searchResults : HOT_STOCKS; const displayData = useMemo(() => {
return searchText.trim() ? searchResults : HOT_STOCKS;
}, [searchText, searchResults]);
const listTitle = searchText.trim() ? '搜索结果' : '热门股票'; const listTitle = searchText.trim() ? '搜索结果' : '热门股票';
// FlatList 性能优化配置
const getItemLayout = useCallback((_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}), []);
const keyExtractor = useCallback((item) => item.stock_code, []);
const renderItem = useCallback(({ item }) => (
<SearchResultItem
item={item}
onAdd={handleAdd}
isAdding={addingCode === item.stock_code}
alreadyAdded={checkInWatchlist(item.stock_code)}
/>
), [handleAdd, addingCode, checkInWatchlist]);
return ( return (
<Modal isOpen={isOpen} onClose={handleClose} size="full"> <Modal
isOpen={isOpen}
onClose={handleClose}
size="full"
animationPreset="slide"
_backdrop={{
bg: 'rgba(0,0,0,0.6)',
}}
>
<Modal.Content <Modal.Content
bg="transparent" bg="#14141E"
maxH="85%" h={SCREEN_HEIGHT * 0.75}
marginBottom={0} marginBottom={0}
marginTop="auto" marginTop="auto"
borderTopRadius={24} borderTopRadius={24}
borderBottomRadius={0} borderBottomRadius={0}
shadow={9}
> >
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} /> <Box flex={1} bg="#14141E" borderTopRadius={24}>
<Box
flex={1}
bg="rgba(20, 20, 30, 0.95)"
borderTopRadius={24}
overflow="hidden"
>
{/* 头部 */} {/* 头部 */}
<HStack <HStack
px={4} px={4}
@@ -297,15 +361,13 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
{/* 股票列表 */} {/* 股票列表 */}
<FlatList <FlatList
data={displayData} data={displayData}
keyExtractor={(item) => item.stock_code} keyExtractor={keyExtractor}
renderItem={({ item }) => ( renderItem={renderItem}
<SearchResultItem getItemLayout={getItemLayout}
item={item} initialNumToRender={10}
onAdd={handleAdd} maxToRenderPerBatch={10}
isAdding={addingCode === item.stock_code} windowSize={5}
isInWatchlist={isInWatchlist} removeClippedSubviews={true}
/>
)}
ListEmptyComponent={ ListEmptyComponent={
<Box py={10} alignItems="center"> <Box py={10} alignItems="center">
<Icon <Icon
@@ -329,6 +391,4 @@ const AddWatchlistModal = ({ isOpen, onClose }) => {
); );
}; };
const styles = StyleSheet.create({});
export default AddWatchlistModal; export default AddWatchlistModal;

View File

@@ -84,9 +84,10 @@ export const authService = {
/** /**
* 获取当前用户信息 * 获取当前用户信息
* @param {boolean} clearOnFail - 失败时是否清除本地状态(默认 false
* @returns {Promise<object>} 用户信息 * @returns {Promise<object>} 用户信息
*/ */
getCurrentUser: async () => { getCurrentUser: async (clearOnFail = false) => {
try { try {
const response = await apiRequest('/api/account/user-info'); const response = await apiRequest('/api/account/user-info');
@@ -97,11 +98,18 @@ export const authService = {
return response.user; return response.user;
} }
// 如果返回 401/403说明 token 无效
if (response.status === 401 || response.status === 403) {
console.warn('[AuthService] Token 无效,需要重新登录');
if (clearOnFail) {
await authService.clearLocalAuth();
}
}
return null; return null;
} catch (error) { } catch (error) {
console.error('[AuthService] 获取用户信息失败:', error); console.error('[AuthService] 获取用户信息失败:', error);
// 可能是未登录,清除本地状态 // 网络错误等,不清除本地状态,让用户可以继续使用
await authService.clearLocalAuth();
return null; return null;
} }
}, },

View File

@@ -2934,14 +2934,23 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
steps=[ToolCall(tool=s["tool"], arguments=s["arguments"], reason=s["reason"]) for s in plan_steps], steps=[ToolCall(tool=s["tool"], arguments=s["arguments"], reason=s["reason"]) for s in plan_steps],
) )
# 阶段3: 生成最终总结 # 阶段3: 生成最终总结(支持在总结中检测到工具调用后继续执行)
yield self._format_sse("status", {"stage": "summarizing", "message": "正在生成最终总结..."}) max_summary_iterations = 3 # 总结阶段最多迭代次数,防止无限循环
summary_iteration = 0
final_summary = ""
while summary_iteration < max_summary_iterations:
summary_iteration += 1
logger.info(f"[Summary] 开始第 {summary_iteration} 轮总结生成")
yield self._format_sse("status", {"stage": "summarizing", "message": f"正在生成最终总结{'(第 ' + str(summary_iteration) + ' 轮)' if summary_iteration > 1 else ''}..."})
# 收集成功的结果 # 收集成功的结果
successful_results = [r for r in step_results if r.status == "success"] successful_results = [r for r in step_results if r.status == "success"]
# 初始化 final_summary # 本轮是否需要继续执行工具调用
final_summary = "" need_more_tool_calls = False
detected_tool_calls = []
if not successful_results and step_index == 0: if not successful_results and step_index == 0:
# 如果没有执行任何工具(模型直接回复),使用模型的回复 # 如果没有执行任何工具(模型直接回复),使用模型的回复
@@ -2968,28 +2977,46 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
stream=True, # 启用流式输出 stream=True, # 启用流式输出
) )
# 状态机处理 <think>...</think> 标签 # 状态机处理 <think>...</think> 标签和 <minimax:tool_call>...</minimax:tool_call> 标签
# 状态: "normal" - 正常内容, "thinking" - 思考内容 # 状态: "normal" - 正常内容, "thinking" - 思考内容
parse_state = "normal" parse_state = "normal"
buffer = "" buffer = ""
thinking_content = "" thinking_content = ""
thinking_sent = False # 标记是否已发送过 thinking 事件 thinking_sent = False # 标记是否已发送过 thinking 事件
accumulated_content = "" # 累积全部内容用于检测工具调用
# 逐块发送总结内容 # 逐块发送总结内容
for chunk in summary_stream: for chunk in summary_stream:
if chunk.choices and chunk.choices[0].delta.content: if chunk.choices and chunk.choices[0].delta.content:
content_chunk = chunk.choices[0].delta.content content_chunk = chunk.choices[0].delta.content
buffer += content_chunk buffer += content_chunk
accumulated_content += content_chunk
# 检测是否有完整的工具调用标签
if '<minimax:tool_call>' in accumulated_content and '</minimax:tool_call>' in accumulated_content:
logger.info(f"[Summary] 检测到工具调用标签,准备解析")
detected_tool_calls = self._parse_text_tool_calls(accumulated_content)
if detected_tool_calls:
logger.info(f"[Summary] 解析到 {len(detected_tool_calls)} 个工具调用: {detected_tool_calls}")
need_more_tool_calls = True
# 发送提示信息
yield self._format_sse("status", {"stage": "executing", "message": f"模型需要继续调用 {len(detected_tool_calls)} 个工具..."})
break # 中断流式输出,准备执行工具调用
# 处理 buffer 中的内容 # 处理 buffer 中的内容
while buffer: while buffer:
if parse_state == "normal": if parse_state == "normal":
# 查找 <think> 开始标签 # 查找 <think> 开始标签
think_start = buffer.find("<think>") think_start = buffer.find("<think>")
# 查找 <minimax:tool_call> 开始标签(不完整时暂存)
tool_call_start = buffer.find("<minimax:tool_call>")
if think_start != -1: if think_start != -1:
# 发送 <think> 之前的内容 # 发送 <think> 之前的内容
if think_start > 0: if think_start > 0:
normal_content = buffer[:think_start] normal_content = buffer[:think_start]
# 过滤掉可能包含的工具调用标签
if '<minimax:tool_call>' not in normal_content:
final_summary += normal_content final_summary += normal_content
yield self._format_sse("summary_chunk", {"content": normal_content}) yield self._format_sse("summary_chunk", {"content": normal_content})
# 切换到思考状态 # 切换到思考状态
@@ -2998,10 +3025,26 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
if not thinking_sent: if not thinking_sent:
yield self._format_sse("thinking_start", {"message": "正在深度思考..."}) yield self._format_sse("thinking_start", {"message": "正在深度思考..."})
thinking_sent = True thinking_sent = True
elif tool_call_start != -1:
# 发送 <minimax:tool_call> 之前的内容
if tool_call_start > 0:
normal_content = buffer[:tool_call_start]
final_summary += normal_content
yield self._format_sse("summary_chunk", {"content": normal_content})
# 保留工具调用标签部分,等待完整内容
buffer = buffer[tool_call_start:]
break # 等待更多内容
else: else:
# 没有 <think> 标签,检查是否可能是不完整的标签 # 没有特殊标签,检查是否可能是不完整的标签
if buffer.endswith("<") or buffer.endswith("<t") or buffer.endswith("<th") or \ incomplete_patterns = [
buffer.endswith("<thi") or buffer.endswith("<thin") or buffer.endswith("<think"): "<", "<t", "<th", "<thi", "<thin", "<think",
"<m", "<mi", "<min", "<mini", "<minim", "<minima", "<minimax",
"<minimax:", "<minimax:t", "<minimax:to", "<minimax:too",
"<minimax:tool", "<minimax:tool_", "<minimax:tool_c",
"<minimax:tool_ca", "<minimax:tool_cal", "<minimax:tool_call"
]
is_incomplete = any(buffer.endswith(p) for p in incomplete_patterns)
if is_incomplete:
# 保留可能不完整的标签,等待更多内容 # 保留可能不完整的标签,等待更多内容
break break
else: else:
@@ -3035,15 +3078,22 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
yield self._format_sse("thinking_chunk", {"content": buffer}) yield self._format_sse("thinking_chunk", {"content": buffer})
buffer = "" buffer = ""
# 如果需要继续执行工具调用,跳过剩余 buffer 处理
if need_more_tool_calls:
pass # 后续会处理工具调用
else:
# 处理剩余的 buffer # 处理剩余的 buffer
if buffer: if buffer:
# 过滤掉可能残留的工具调用标签
buffer_filtered = self._filter_tool_call_tags(buffer)
if parse_state == "thinking": if parse_state == "thinking":
thinking_content += buffer thinking_content += buffer_filtered
yield self._format_sse("thinking_chunk", {"content": buffer}) yield self._format_sse("thinking_chunk", {"content": buffer_filtered})
yield self._format_sse("thinking_end", {"content": thinking_content}) yield self._format_sse("thinking_end", {"content": thinking_content})
else: else:
final_summary += buffer final_summary += buffer_filtered
yield self._format_sse("summary_chunk", {"content": buffer}) if buffer_filtered:
yield self._format_sse("summary_chunk", {"content": buffer_filtered})
logger.info(f"[Summary] 流式总结完成,思考内容长度: {len(thinking_content)}") logger.info(f"[Summary] 流式总结完成,思考内容长度: {len(thinking_content)}")
@@ -3058,6 +3108,139 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
yield self._format_sse("summary_chunk", {"content": final_summary}) yield self._format_sse("summary_chunk", {"content": final_summary})
logger.warning("[Summary] 使用降级方案") logger.warning("[Summary] 使用降级方案")
# 如果需要继续执行工具调用
if need_more_tool_calls and detected_tool_calls:
logger.info(f"[Summary] 执行在总结中检测到的 {len(detected_tool_calls)} 个工具调用")
# 将 assistant 消息添加到历史
messages.append({"role": "assistant", "content": accumulated_content})
# 发送 plan_update 事件
current_round_steps = [
{"tool": tc["name"], "arguments": tc["arguments"], "reason": f"调用 {tc['name']}"}
for tc in detected_tool_calls
]
yield self._format_sse("plan_update", {
"new_steps": current_round_steps,
"round": summary_iteration,
"message": f"模型在总结中决定继续调用 {len(detected_tool_calls)} 个工具"
})
# 执行每个工具调用
for tc in detected_tool_calls:
tool_name = tc["name"]
arguments = tc["arguments"]
tool_call_id = f"summary_call_{summary_iteration}_{step_index}_{tool_name}"
logger.info(f"[Tool Call] ========== 总结阶段工具调用开始 ==========")
logger.info(f"[Tool Call] 工具名: {tool_name}")
logger.info(f"[Tool Call] 参数内容: {json.dumps(arguments, ensure_ascii=False)}")
# 发送步骤开始事件
yield self._format_sse("step_start", {
"step_index": step_index,
"tool": tool_name,
"arguments": arguments,
"reason": f"调用 {tool_name}",
})
start_time = datetime.now()
try:
# 特殊处理 summarize_news
if tool_name == "summarize_news":
data_arg = arguments.get("data", "")
if data_arg in ["前面的新闻数据", "前面收集的所有数据", ""]:
arguments["data"] = json.dumps(collected_data, ensure_ascii=False, indent=2)
# 执行工具
result = await self.execute_tool(tool_name, arguments, tool_handlers)
execution_time = (datetime.now() - start_time).total_seconds()
# 记录结果
step_result = StepResult(
step_index=step_index,
tool=tool_name,
arguments=arguments,
status="success",
result=result,
execution_time=execution_time,
)
step_results.append(step_result)
collected_data[f"step_{step_index+1}_{tool_name}"] = result
plan_steps.append({"tool": tool_name, "arguments": arguments, "reason": f"调用 {tool_name}"})
# 更新 plan 对象
plan = ExecutionPlan(
goal=f"分析用户问题:{user_query[:50]}...",
reasoning="使用工具获取相关数据进行分析",
steps=[ToolCall(tool=s["tool"], arguments=s["arguments"], reason=s["reason"]) for s in plan_steps],
)
# 发送步骤完成事件
yield self._format_sse("step_complete", {
"step_index": step_index,
"tool": tool_name,
"status": "success",
"result": result,
"execution_time": execution_time,
})
# 将工具结果添加到消息历史
result_str = json.dumps(result, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result)
messages.append({
"role": "user",
"content": f"[工具调用结果] {tool_name}: {result_str[:3000]}"
})
logger.info(f"[Tool Call] 执行成功,耗时 {execution_time:.2f}s")
except Exception as e:
execution_time = (datetime.now() - start_time).total_seconds()
error_msg = str(e)
step_result = StepResult(
step_index=step_index,
tool=tool_name,
arguments=arguments,
status="failed",
error=error_msg,
execution_time=execution_time,
)
step_results.append(step_result)
yield self._format_sse("step_complete", {
"step_index": step_index,
"tool": tool_name,
"status": "failed",
"error": error_msg,
"execution_time": execution_time,
})
messages.append({
"role": "user",
"content": f"[工具调用失败] {tool_name}: {error_msg}"
})
logger.error(f"[Tool Call] 执行失败: {error_msg}")
logger.info(f"[Tool Call] ========== 总结阶段工具调用结束 ==========")
step_index += 1
# 重置 final_summary准备重新生成
final_summary = ""
# 发送重置事件,通知前端清空已有的响应内容
yield self._format_sse("summary_reset", {"message": "准备重新生成总结"})
# 继续循环,重新生成总结
continue
else:
# 没有更多工具调用,退出循环
break
# 如果达到最大迭代次数,记录警告
if summary_iteration >= max_summary_iterations:
logger.warning(f"[Summary] 达到最大迭代次数 {max_summary_iterations},停止工具调用循环")
# 过滤掉可能残留的工具调用标签MiniMax 等模型可能在总结中输出工具调用) # 过滤掉可能残留的工具调用标签MiniMax 等模型可能在总结中输出工具调用)
final_summary = self._filter_tool_call_tags(final_summary) final_summary = self._filter_tool_call_tags(final_summary)