更新ios
This commit is contained in:
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
// 然后尝试从服务器获取最新用户信息
|
// 然后在后台尝试从服务器验证并获取最新用户信息
|
||||||
const serverUser = await authService.getCurrentUser();
|
// 使用 setTimeout 确保 UI 先渲染
|
||||||
if (serverUser) {
|
setTimeout(async () => {
|
||||||
setUser(serverUser);
|
try {
|
||||||
// 获取订阅信息
|
const serverUser = await authService.getCurrentUser();
|
||||||
const subInfo = await authService.getSubscription();
|
if (serverUser) {
|
||||||
if (subInfo.success) {
|
setUser(serverUser);
|
||||||
setSubscription(subInfo.data || subInfo.subscription);
|
console.log('[AuthContext] 服务器验证成功,更新用户信息');
|
||||||
|
// 获取订阅信息
|
||||||
|
const subInfo = await authService.getSubscription();
|
||||||
|
if (subInfo.success) {
|
||||||
|
setSubscription(subInfo.data || subInfo.subscription);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 服务器验证失败,但保留本地登录状态
|
||||||
|
// 用户可以继续使用,下次操作时如果需要会提示重新登录
|
||||||
|
console.warn('[AuthContext] 服务器验证失败,保留本地登录状态');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 网络错误等,保留本地状态
|
||||||
|
console.warn('[AuthContext] 后台验证出错,保留本地登录状态:', error.message);
|
||||||
}
|
}
|
||||||
} else {
|
}, 100);
|
||||||
// 服务器验证失败,清除本地状态
|
} else if (storedUser) {
|
||||||
setUser(null);
|
// 有用户信息但没有 token,需要重新登录
|
||||||
setIsLoggedIn(false);
|
console.log('[AuthContext] 没有有效 token,需要重新登录');
|
||||||
}
|
await authService.clearLocalAuth();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AuthContext] 初始化失败:', error);
|
console.error('[AuthContext] 初始化失败:', error);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,38 +296,39 @@ const StockDetailScreen = () => {
|
|||||||
isRealtime={wsConnected && !!realtimeQuote?.current_price}
|
isRealtime={wsConnected && !!realtimeQuote?.current_price}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 图表类型切换 */}
|
{/* 可滚动区域 - 包含图表和其他内容 */}
|
||||||
<ChartTypeTabs
|
|
||||||
activeType={chartType}
|
|
||||||
onChange={handleChartTypeChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 图表区域 */}
|
|
||||||
<Box mt={2}>
|
|
||||||
{chartType === 'minute' ? (
|
|
||||||
<MinuteChart
|
|
||||||
data={currentChartData}
|
|
||||||
preClose={minutePrevClose || quote.pre_close}
|
|
||||||
loading={isChartLoading}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<KlineChart
|
|
||||||
data={currentChartData}
|
|
||||||
type={chartType}
|
|
||||||
loading={isChartLoading}
|
|
||||||
riseAnalysisData={riseAnalysisData}
|
|
||||||
onAnalysisPress={handleAnalysisPress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 5档盘口 + 行情数据 + 相关信息(可滚动区域) */}
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
flex={1}
|
flex={1}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
nestedScrollEnabled={true}
|
nestedScrollEnabled={true}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
bounces={true}
|
||||||
>
|
>
|
||||||
|
{/* 图表类型切换 */}
|
||||||
|
<ChartTypeTabs
|
||||||
|
activeType={chartType}
|
||||||
|
onChange={handleChartTypeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 图表区域 */}
|
||||||
|
<Box mt={2}>
|
||||||
|
{chartType === 'minute' ? (
|
||||||
|
<MinuteChart
|
||||||
|
data={currentChartData}
|
||||||
|
preClose={minutePrevClose || quote.pre_close}
|
||||||
|
loading={isChartLoading}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<KlineChart
|
||||||
|
data={currentChartData}
|
||||||
|
type={chartType}
|
||||||
|
loading={isChartLoading}
|
||||||
|
riseAnalysisData={riseAnalysisData}
|
||||||
|
onAnalysisPress={handleAnalysisPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* 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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
console.error('搜索股票失败:', error);
|
if (error.name !== 'AbortError') {
|
||||||
setSearchResults([]);
|
console.error('搜索股票失败:', error);
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
401
mcp_server.py
401
mcp_server.py
@@ -2934,129 +2934,312 @@ 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
|
||||||
# 收集成功的结果
|
|
||||||
successful_results = [r for r in step_results if r.status == "success"]
|
|
||||||
|
|
||||||
# 初始化 final_summary
|
|
||||||
final_summary = ""
|
final_summary = ""
|
||||||
|
|
||||||
if not successful_results and step_index == 0:
|
while summary_iteration < max_summary_iterations:
|
||||||
# 如果没有执行任何工具(模型直接回复),使用模型的回复
|
summary_iteration += 1
|
||||||
if assistant_message and assistant_message.content:
|
logger.info(f"[Summary] 开始第 {summary_iteration} 轮总结生成")
|
||||||
final_summary = assistant_message.content
|
|
||||||
# 流式发送(虽然已经是完整的,但保持前端兼容)
|
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"]
|
||||||
|
|
||||||
|
# 本轮是否需要继续执行工具调用
|
||||||
|
need_more_tool_calls = False
|
||||||
|
detected_tool_calls = []
|
||||||
|
|
||||||
|
if not successful_results and step_index == 0:
|
||||||
|
# 如果没有执行任何工具(模型直接回复),使用模型的回复
|
||||||
|
if assistant_message and assistant_message.content:
|
||||||
|
final_summary = assistant_message.content
|
||||||
|
# 流式发送(虽然已经是完整的,但保持前端兼容)
|
||||||
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
|
else:
|
||||||
|
final_summary = "抱歉,我无法处理您的请求。"
|
||||||
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
|
elif not successful_results:
|
||||||
|
# 所有步骤都失败
|
||||||
|
final_summary = "很抱歉,所有步骤都执行失败,无法生成分析报告。"
|
||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
yield self._format_sse("summary_chunk", {"content": final_summary})
|
||||||
else:
|
else:
|
||||||
final_summary = "抱歉,我无法处理您的请求。"
|
# 有成功的工具调用,使用流式 API 生成最终回复
|
||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
try:
|
||||||
elif not successful_results:
|
# 使用流式 API 生成最终回复(不再传入 tools,让模型生成文本回复)
|
||||||
# 所有步骤都失败
|
summary_stream = llm_client.chat.completions.create(
|
||||||
final_summary = "很抱歉,所有步骤都执行失败,无法生成分析报告。"
|
model=llm_model,
|
||||||
yield self._format_sse("summary_chunk", {"content": final_summary})
|
messages=messages, # messages 已包含所有工具调用历史
|
||||||
else:
|
temperature=0.7,
|
||||||
# 有成功的工具调用,使用流式 API 生成最终回复
|
max_tokens=llm_max_tokens,
|
||||||
try:
|
stream=True, # 启用流式输出
|
||||||
# 使用流式 API 生成最终回复(不再传入 tools,让模型生成文本回复)
|
)
|
||||||
summary_stream = llm_client.chat.completions.create(
|
|
||||||
model=llm_model,
|
|
||||||
messages=messages, # messages 已包含所有工具调用历史
|
|
||||||
temperature=0.7,
|
|
||||||
max_tokens=llm_max_tokens,
|
|
||||||
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
|
||||||
|
|
||||||
# 处理 buffer 中的内容
|
# 检测是否有完整的工具调用标签
|
||||||
while buffer:
|
if '<minimax:tool_call>' in accumulated_content and '</minimax:tool_call>' in accumulated_content:
|
||||||
if parse_state == "normal":
|
logger.info(f"[Summary] 检测到工具调用标签,准备解析")
|
||||||
# 查找 <think> 开始标签
|
detected_tool_calls = self._parse_text_tool_calls(accumulated_content)
|
||||||
think_start = buffer.find("<think>")
|
if detected_tool_calls:
|
||||||
if think_start != -1:
|
logger.info(f"[Summary] 解析到 {len(detected_tool_calls)} 个工具调用: {detected_tool_calls}")
|
||||||
# 发送 <think> 之前的内容
|
need_more_tool_calls = True
|
||||||
if think_start > 0:
|
# 发送提示信息
|
||||||
normal_content = buffer[:think_start]
|
yield self._format_sse("status", {"stage": "executing", "message": f"模型需要继续调用 {len(detected_tool_calls)} 个工具..."})
|
||||||
final_summary += normal_content
|
break # 中断流式输出,准备执行工具调用
|
||||||
yield self._format_sse("summary_chunk", {"content": normal_content})
|
|
||||||
# 切换到思考状态
|
# 处理 buffer 中的内容
|
||||||
parse_state = "thinking"
|
while buffer:
|
||||||
buffer = buffer[think_start + 7:] # 跳过 <think>
|
if parse_state == "normal":
|
||||||
if not thinking_sent:
|
# 查找 <think> 开始标签
|
||||||
yield self._format_sse("thinking_start", {"message": "正在深度思考..."})
|
think_start = buffer.find("<think>")
|
||||||
thinking_sent = True
|
# 查找 <minimax:tool_call> 开始标签(不完整时暂存)
|
||||||
else:
|
tool_call_start = buffer.find("<minimax:tool_call>")
|
||||||
# 没有 <think> 标签,检查是否可能是不完整的标签
|
|
||||||
if buffer.endswith("<") or buffer.endswith("<t") or buffer.endswith("<th") or \
|
if think_start != -1:
|
||||||
buffer.endswith("<thi") or buffer.endswith("<thin") or buffer.endswith("<think"):
|
# 发送 <think> 之前的内容
|
||||||
# 保留可能不完整的标签,等待更多内容
|
if think_start > 0:
|
||||||
break
|
normal_content = buffer[:think_start]
|
||||||
|
# 过滤掉可能包含的工具调用标签
|
||||||
|
if '<minimax:tool_call>' not in normal_content:
|
||||||
|
final_summary += normal_content
|
||||||
|
yield self._format_sse("summary_chunk", {"content": normal_content})
|
||||||
|
# 切换到思考状态
|
||||||
|
parse_state = "thinking"
|
||||||
|
buffer = buffer[think_start + 7:] # 跳过 <think>
|
||||||
|
if not thinking_sent:
|
||||||
|
yield self._format_sse("thinking_start", {"message": "正在深度思考..."})
|
||||||
|
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:
|
||||||
# 发送全部内容
|
# 没有特殊标签,检查是否可能是不完整的标签
|
||||||
final_summary += buffer
|
incomplete_patterns = [
|
||||||
yield self._format_sse("summary_chunk", {"content": buffer})
|
"<", "<t", "<th", "<thi", "<thin", "<think",
|
||||||
buffer = ""
|
"<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
|
||||||
|
else:
|
||||||
|
# 发送全部内容
|
||||||
|
final_summary += buffer
|
||||||
|
yield self._format_sse("summary_chunk", {"content": buffer})
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
elif parse_state == "thinking":
|
elif parse_state == "thinking":
|
||||||
# 查找 </think> 结束标签
|
# 查找 </think> 结束标签
|
||||||
think_end = buffer.find("</think>")
|
think_end = buffer.find("</think>")
|
||||||
if think_end != -1:
|
if think_end != -1:
|
||||||
# 发送思考内容
|
# 发送思考内容
|
||||||
if think_end > 0:
|
if think_end > 0:
|
||||||
think_chunk = buffer[:think_end]
|
think_chunk = buffer[:think_end]
|
||||||
thinking_content += think_chunk
|
thinking_content += think_chunk
|
||||||
yield self._format_sse("thinking_chunk", {"content": think_chunk})
|
yield self._format_sse("thinking_chunk", {"content": think_chunk})
|
||||||
# 切换回正常状态
|
# 切换回正常状态
|
||||||
parse_state = "normal"
|
parse_state = "normal"
|
||||||
buffer = buffer[think_end + 8:] # 跳过 </think>
|
buffer = buffer[think_end + 8:] # 跳过 </think>
|
||||||
yield self._format_sse("thinking_end", {"content": thinking_content})
|
yield self._format_sse("thinking_end", {"content": thinking_content})
|
||||||
else:
|
|
||||||
# 没有 </think> 标签,检查是否可能是不完整的标签
|
|
||||||
if buffer.endswith("<") or buffer.endswith("</") or buffer.endswith("</t") or \
|
|
||||||
buffer.endswith("</th") or buffer.endswith("</thi") or buffer.endswith("</thin") or buffer.endswith("</think"):
|
|
||||||
# 保留可能不完整的标签,等待更多内容
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
# 发送全部思考内容
|
# 没有 </think> 标签,检查是否可能是不完整的标签
|
||||||
thinking_content += buffer
|
if buffer.endswith("<") or buffer.endswith("</") or buffer.endswith("</t") or \
|
||||||
yield self._format_sse("thinking_chunk", {"content": buffer})
|
buffer.endswith("</th") or buffer.endswith("</thi") or buffer.endswith("</thin") or buffer.endswith("</think"):
|
||||||
buffer = ""
|
# 保留可能不完整的标签,等待更多内容
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 发送全部思考内容
|
||||||
|
thinking_content += buffer
|
||||||
|
yield self._format_sse("thinking_chunk", {"content": buffer})
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
# 处理剩余的 buffer
|
# 如果需要继续执行工具调用,跳过剩余 buffer 处理
|
||||||
if buffer:
|
if need_more_tool_calls:
|
||||||
if parse_state == "thinking":
|
pass # 后续会处理工具调用
|
||||||
thinking_content += buffer
|
|
||||||
yield self._format_sse("thinking_chunk", {"content": buffer})
|
|
||||||
yield self._format_sse("thinking_end", {"content": thinking_content})
|
|
||||||
else:
|
else:
|
||||||
final_summary += buffer
|
# 处理剩余的 buffer
|
||||||
yield self._format_sse("summary_chunk", {"content": buffer})
|
if buffer:
|
||||||
|
# 过滤掉可能残留的工具调用标签
|
||||||
|
buffer_filtered = self._filter_tool_call_tags(buffer)
|
||||||
|
if parse_state == "thinking":
|
||||||
|
thinking_content += buffer_filtered
|
||||||
|
yield self._format_sse("thinking_chunk", {"content": buffer_filtered})
|
||||||
|
yield self._format_sse("thinking_end", {"content": thinking_content})
|
||||||
|
else:
|
||||||
|
final_summary += buffer_filtered
|
||||||
|
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)}")
|
||||||
|
|
||||||
except Exception as llm_error:
|
except Exception as llm_error:
|
||||||
logger.error(f"[Summary] 流式总结失败: {llm_error}")
|
logger.error(f"[Summary] 流式总结失败: {llm_error}")
|
||||||
# 降级:使用工具调用结果的简单拼接
|
# 降级:使用工具调用结果的简单拼接
|
||||||
results_text = "\n\n".join([
|
results_text = "\n\n".join([
|
||||||
f"**{r.tool}**: {str(r.result)[:500]}..."
|
f"**{r.tool}**: {str(r.result)[:500]}..."
|
||||||
for r in successful_results[:5]
|
for r in successful_results[:5]
|
||||||
])
|
])
|
||||||
final_summary = f"根据查询结果:\n\n{results_text}"
|
final_summary = f"根据查询结果:\n\n{results_text}"
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user