更新ios

This commit is contained in:
2026-01-18 09:10:56 +08:00
parent 2ad5835813
commit 4313a8cc1a
12 changed files with 362 additions and 135 deletions

View File

@@ -460,7 +460,10 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = "$(inherited) "; OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
USE_HERMES = true; USE_HERMES = true;
@@ -518,7 +521,10 @@
); );
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = "$(inherited) "; OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
USE_HERMES = true; USE_HERMES = true;

View File

@@ -323,6 +323,8 @@ function MarketStack(props) {
<Header <Header
title={route.params?.sectorName || "板块详情"} title={route.params?.sectorName || "板块详情"}
back back
white
bgColor="#0F172A"
navigation={navigation} navigation={navigation}
scene={scene} scene={scene}
/> />

View File

@@ -3,7 +3,7 @@
* 支持 Markdown 渲染和 ECharts 图表渲染 * 支持 Markdown 渲染和 ECharts 图表渲染
*/ */
import React, { memo, useMemo, useState, useCallback } from 'react'; import React, { memo, useMemo, useState, useCallback, useContext, createContext } from 'react';
import { import {
View, View,
Text, Text,
@@ -14,8 +14,12 @@ import {
ActivityIndicator, ActivityIndicator,
} from 'react-native'; } from 'react-native';
import { WebView } from 'react-native-webview'; import { WebView } from 'react-native-webview';
import { useNavigation } from '@react-navigation/native';
import { AgentTheme } from '../../../constants/agentConstants'; import { AgentTheme } from '../../../constants/agentConstants';
// 股票代码正则:匹配 000001.SZ, 600000.SH, 或纯六位数字
const STOCK_CODE_REGEX = /\b(\d{6})(?:\.(SZ|SH|sz|sh|BJ|bj))?\b/g;
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距 const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
const CHART_HEIGHT = 300; const CHART_HEIGHT = 300;
@@ -137,7 +141,7 @@ const TableView = memo(({ rows }) => {
/** /**
* Markdown 文本渲染器(支持表格) * Markdown 文本渲染器(支持表格)
*/ */
const MarkdownText = memo(({ content }) => { const MarkdownText = memo(({ content, navigation }) => {
const lines = content.split('\n'); const lines = content.split('\n');
const elements = []; const elements = [];
let i = 0; let i = 0;
@@ -155,7 +159,7 @@ const MarkdownText = memo(({ content }) => {
} }
} }
elements.push(<MarkdownLine key={i} line={line} />); elements.push(<MarkdownLine key={i} line={line} navigation={navigation} />);
i++; i++;
} }
@@ -169,7 +173,7 @@ const MarkdownText = memo(({ content }) => {
/** /**
* 单行 Markdown 渲染 * 单行 Markdown 渲染
*/ */
const MarkdownLine = memo(({ line }) => { const MarkdownLine = memo(({ line, navigation }) => {
// 空行 // 空行
if (!line.trim()) { if (!line.trim()) {
return <View style={styles.emptyLine} />; return <View style={styles.emptyLine} />;
@@ -188,7 +192,7 @@ const MarkdownLine = memo(({ line }) => {
]; ];
return ( return (
<Text style={[styles.headerBase, headerStyles[level - 1]]}> <Text style={[styles.headerBase, headerStyles[level - 1]]}>
{renderInlineStyles(text)} {renderInlineStyles(text, navigation)}
</Text> </Text>
); );
} }
@@ -199,7 +203,7 @@ const MarkdownLine = memo(({ line }) => {
return ( return (
<View style={styles.listItem}> <View style={styles.listItem}>
<Text style={styles.listBullet}></Text> <Text style={styles.listBullet}></Text>
<Text style={styles.listText}>{renderInlineStyles(listMatch[1])}</Text> <Text style={styles.listText}>{renderInlineStyles(listMatch[1], navigation)}</Text>
</View> </View>
); );
} }
@@ -210,7 +214,7 @@ const MarkdownLine = memo(({ line }) => {
return ( return (
<View style={styles.listItem}> <View style={styles.listItem}>
<Text style={styles.listNumber}>{orderedListMatch[1]}.</Text> <Text style={styles.listNumber}>{orderedListMatch[1]}.</Text>
<Text style={styles.listText}>{renderInlineStyles(orderedListMatch[2])}</Text> <Text style={styles.listText}>{renderInlineStyles(orderedListMatch[2], navigation)}</Text>
</View> </View>
); );
} }
@@ -225,7 +229,7 @@ const MarkdownLine = memo(({ line }) => {
if (quoteMatch) { if (quoteMatch) {
return ( return (
<View style={styles.quote}> <View style={styles.quote}>
<Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1])}</Text> <Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1], navigation)}</Text>
</View> </View>
); );
} }
@@ -237,32 +241,113 @@ const MarkdownLine = memo(({ line }) => {
// 普通段落 // 普通段落
return ( return (
<Text style={styles.paragraph}>{renderInlineStyles(line)}</Text> <Text style={styles.paragraph}>{renderInlineStyles(line, navigation)}</Text>
); );
}); });
/** /**
* 渲染行内样式(粗体、斜体、代码、链接) * 股票代码可点击组件
*/ */
const renderInlineStyles = (text) => { const StockCodeLink = memo(({ code, exchange, navigation }) => {
// 确定交易所后缀
let exchangeSuffix = exchange?.toUpperCase() || '';
if (!exchangeSuffix) {
// 根据代码前缀推断交易所
if (code.startsWith('6')) {
exchangeSuffix = 'SH';
} else if (code.startsWith('0') || code.startsWith('3')) {
exchangeSuffix = 'SZ';
} else if (code.startsWith('4') || code.startsWith('8')) {
exchangeSuffix = 'BJ';
}
}
const handlePress = useCallback(() => {
const stockCode = exchangeSuffix ? `${code}.${exchangeSuffix}` : code;
navigation?.navigate('StockDetail', {
code: stockCode,
name: stockCode, // 名称可以在详情页获取
});
}, [code, exchangeSuffix, navigation]);
const displayCode = exchangeSuffix ? `${code}.${exchangeSuffix}` : code;
return (
<TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
<Text style={styles.stockCodeLink}>{displayCode}</Text>
</TouchableOpacity>
);
});
/**
* 渲染行内样式(粗体、斜体、代码、链接、股票代码)
*/
const renderInlineStyles = (text, navigation) => {
if (!text) return null; if (!text) return null;
const elements = []; // 先进行基本文本处理
let key = 0; const processedText = text
// 为简化实现,这里只做基本渲染
// 完整实现需要递归解析
elements.push(
<Text key={key++} style={styles.text}>
{text
.replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化) .replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
.replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记 .replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
.replace(/`([^`]+)`/g, (_, m) => `[${m}]`) // 标记代码 .replace(/`([^`]+)`/g, (_, m) => `[${m}]`); // 标记代码
// 检测股票代码并分割文本
const stockMatches = [];
let match;
const regex = new RegExp(STOCK_CODE_REGEX.source, 'g');
while ((match = regex.exec(processedText)) !== null) {
stockMatches.push({
code: match[1],
exchange: match[2],
index: match.index,
length: match[0].length,
fullMatch: match[0],
});
} }
// 如果没有股票代码,返回普通文本
if (stockMatches.length === 0) {
return <Text style={styles.text}>{processedText}</Text>;
}
// 有股票代码,需要分割并渲染
const elements = [];
let lastIndex = 0;
let key = 0;
stockMatches.forEach((stockMatch) => {
// 添加股票代码前的文本
if (stockMatch.index > lastIndex) {
elements.push(
<Text key={key++} style={styles.text}>
{processedText.substring(lastIndex, stockMatch.index)}
</Text> </Text>
); );
}
return elements; // 添加可点击的股票代码
elements.push(
<StockCodeLink
key={key++}
code={stockMatch.code}
exchange={stockMatch.exchange}
navigation={navigation}
/>
);
lastIndex = stockMatch.index + stockMatch.length;
});
// 添加最后剩余的文本
if (lastIndex < processedText.length) {
elements.push(
<Text key={key++} style={styles.text}>
{processedText.substring(lastIndex)}
</Text>
);
}
return <Text>{elements}</Text>;
}; };
/** /**
@@ -670,6 +755,7 @@ const MermaidView = memo(({ code }) => {
* MarkdownRenderer 主组件 * MarkdownRenderer 主组件
*/ */
const MarkdownRenderer = ({ content }) => { const MarkdownRenderer = ({ content }) => {
const navigation = useNavigation();
const parts = useMemo(() => parseMarkdown(content), [content]); const parts = useMemo(() => parseMarkdown(content), [content]);
return ( return (
@@ -681,7 +767,7 @@ const MarkdownRenderer = ({ content }) => {
if (part.type === 'mermaid') { if (part.type === 'mermaid') {
return <MermaidView key={index} code={part.content} />; return <MermaidView key={index} code={part.content} />;
} }
return <MarkdownText key={index} content={part.content} />; return <MarkdownText key={index} content={part.content} navigation={navigation} />;
})} })}
</View> </View>
); );
@@ -795,6 +881,16 @@ const styles = StyleSheet.create({
marginVertical: 16, marginVertical: 16,
}, },
// 股票代码链接
stockCodeLink: {
color: '#F59E0B',
fontWeight: '600',
textDecorationLine: 'underline',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
paddingHorizontal: 4,
borderRadius: 4,
},
// 图表容器 // 图表容器
chartContainer: { chartContainer: {
backgroundColor: 'rgba(99, 102, 241, 0.1)', backgroundColor: 'rgba(99, 102, 241, 0.1)',

View File

@@ -95,38 +95,6 @@ const WelcomeScreen = ({ onQuickQuestion }) => {
<Text style={styles.tagline}>在市场的混沌中找到价值与风险的平衡点</Text> <Text style={styles.tagline}>在市场的混沌中找到价值与风险的平衡点</Text>
</View> </View>
{/* Bento Grid 功能展示 */}
<View style={styles.bentoGrid}>
<View style={styles.bentoRow}>
<BentoCard
icon="📊"
title="深度分析"
description="基本面与技术面"
color="#8B5CF6"
/>
<BentoCard
icon="🔥"
title="热点追踪"
description="涨停板块监控"
color="#EC4899"
/>
</View>
<View style={styles.bentoRow}>
<BentoCard
icon="📈"
title="趋势研究"
description="行业投资机会"
color="#10B981"
/>
<BentoCard
icon="🧮"
title="量化分析"
description="因子指标计算"
color="#F59E0B"
/>
</View>
</View>
{/* 快捷问题 */} {/* 快捷问题 */}
<View style={styles.quickSection}> <View style={styles.quickSection}>
<View style={styles.sectionHeader}> <View style={styles.sectionHeader}>

View File

@@ -630,7 +630,7 @@ const ConceptList = () => {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [drillPath, setDrillPath] = useState([]); const [drillPath, setDrillPath] = useState([]);
const [viewMode, setViewMode] = useState('card'); // 'card' | 'list' const [viewMode, setViewMode] = useState('list'); // 'card' | 'list'
const [sortBy, setSortBy] = useState('change'); // 'change' | 'outbreak' const [sortBy, setSortBy] = useState('change'); // 'change' | 'outbreak'
const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念 const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念
const [leafLoading, setLeafLoading] = useState(false); const [leafLoading, setLeafLoading] = useState(false);
@@ -1154,7 +1154,7 @@ const ConceptList = () => {
</View> </View>
) : ( ) : (
// 父分类卡片列表 // 父分类卡片列表
<VStack space={4}> <VStack space={4} w="100%">
{currentItems.map((item, index) => ( {currentItems.map((item, index) => (
<ParentCard <ParentCard
key={item.name || index} key={item.name || index}
@@ -1197,6 +1197,7 @@ const styles = StyleSheet.create({
parentCard: { parentCard: {
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', overflow: 'hidden',
width: '100%',
}, },
cardContainer: { cardContainer: {
borderRadius: 16, borderRadius: 16,

View File

@@ -23,6 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import EventCard from './EventCard'; import EventCard from './EventCard';
import MainlineView from './MainlineView'; import MainlineView from './MainlineView';
import TodayStats from '../Market/TodayStats';
import { gradients } from '../../theme'; import { gradients } from '../../theme';
import { import {
fetchEvents, fetchEvents,
@@ -36,6 +37,7 @@ const { width: SCREEN_WIDTH } = Dimensions.get('window');
const VIEW_MODES = { const VIEW_MODES = {
LIST: 'list', LIST: 'list',
MAINLINE: 'mainline', MAINLINE: 'mainline',
STATS: 'stats',
}; };
// 时间范围选项 // 时间范围选项
@@ -205,6 +207,34 @@ const EventList = ({ navigation }) => {
) )
)} )}
</Pressable> </Pressable>
<Pressable onPress={() => handleModeChange(VIEW_MODES.STATS)}>
{({ isPressed }) => (
viewMode === VIEW_MODES.STATS ? (
<LinearGradient
colors={['#F59E0B', '#D97706']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.modeButtonActive}
>
<Icon as={Ionicons} name="stats-chart" size="sm" color="white" />
<Text fontSize="xs" fontWeight="bold" color="white" ml={1.5}>
统计
</Text>
</LinearGradient>
) : (
<Box
style={styles.modeButton}
opacity={isPressed ? 0.7 : 1}
>
<Icon as={Ionicons} name="stats-chart" size="sm" color="gray.400" />
<Text fontSize="xs" color="gray.400" ml={1.5}>
统计
</Text>
</Box>
)
)}
</Pressable>
</HStack> </HStack>
</Box> </Box>
); );
@@ -479,6 +509,19 @@ const EventList = ({ navigation }) => {
const keyExtractor = useCallback((item) => `event-${item.id}`, []); const keyExtractor = useCallback((item) => `event-${item.id}`, []);
// 统计视图模式
if (viewMode === VIEW_MODES.STATS) {
return (
<Box flex={1} bg="#0F172A">
<StatusBar barStyle="light-content" />
<VStack style={{ paddingTop: insets.top }}>
{renderHeader()}
</VStack>
<TodayStats navigation={navigation} isEmbedded />
</Box>
);
}
// 题材视图模式 // 题材视图模式
if (viewMode === VIEW_MODES.MAINLINE) { if (viewMode === VIEW_MODES.MAINLINE) {
return ( return (

View File

@@ -27,10 +27,20 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ztService from '../../services/ztService'; import ztService from '../../services/ztService';
import tradingDayUtils from '../../utils/tradingDayUtils';
import { gradients } from '../../theme'; import { gradients } from '../../theme';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
// 获取初始日期(如果今天不是交易日,返回上一个交易日)
const getInitialDate = () => {
const today = new Date();
if (tradingDayUtils.isTradingDay(today)) {
return today;
}
return tradingDayUtils.getPreviousTradingDay(today);
};
// 日期格式化 // 日期格式化
const formatDisplayDate = (date) => { const formatDisplayDate = (date) => {
const d = new Date(date); const d = new Date(date);
@@ -73,7 +83,7 @@ const parseContinuousDays = (str) => {
const MarketHot = ({ navigation }) => { const MarketHot = ({ navigation }) => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(getInitialDate);
const [ztData, setZtData] = useState(null); const [ztData, setZtData] = useState(null);
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
const [hotSectors, setHotSectors] = useState([]); const [hotSectors, setHotSectors] = useState([]);
@@ -179,15 +189,6 @@ const MarketHot = ({ navigation }) => {
</Text> </Text>
</VStack> </VStack>
</HStack> </HStack>
<HStack space={2}>
<Pressable
onPress={() => navigation.navigate('TodayStats')}
p={2}
rounded="full"
bg="rgba(255,255,255,0.1)"
>
<Icon as={Ionicons} name="stats-chart" size="md" color="white" />
</Pressable>
<Pressable <Pressable
onPress={() => navigation.navigate('EventCalendar')} onPress={() => navigation.navigate('EventCalendar')}
p={2} p={2}
@@ -197,7 +198,6 @@ const MarketHot = ({ navigation }) => {
<Icon as={Ionicons} name="calendar" size="md" color="white" /> <Icon as={Ionicons} name="calendar" size="md" color="white" />
</Pressable> </Pressable>
</HStack> </HStack>
</HStack>
</Box> </Box>
{/* 日期选择器 */} {/* 日期选择器 */}
@@ -212,7 +212,7 @@ const MarketHot = ({ navigation }) => {
<Icon as={Ionicons} name="chevron-back" size="sm" color="gray.400" /> <Icon as={Ionicons} name="chevron-back" size="sm" color="gray.400" />
</Pressable> </Pressable>
<Pressable onPress={() => setCurrentDate(new Date())}> <Pressable onPress={() => setCurrentDate(getInitialDate())}>
<VStack alignItems="center"> <VStack alignItems="center">
<Text fontSize="lg" fontWeight="bold" color="white"> <Text fontSize="lg" fontWeight="bold" color="white">
{formatDisplayDate(currentDate)} {formatDisplayDate(currentDate)}

View File

@@ -302,7 +302,7 @@ const TopStockItem = ({ stock, rank }) => (
</HStack> </HStack>
); );
const TodayStats = ({ navigation }) => { const TodayStats = ({ navigation, isEmbedded = false }) => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
@@ -364,7 +364,7 @@ const TodayStats = ({ navigation }) => {
<ScrollView <ScrollView
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: insets.top, paddingTop: isEmbedded ? 0 : insets.top,
paddingBottom: insets.bottom + 20, paddingBottom: insets.bottom + 20,
}} }}
refreshControl={ refreshControl={
@@ -376,7 +376,8 @@ const TodayStats = ({ navigation }) => {
/> />
} }
> >
{/* 标题栏 */} {/* 标题栏 - 仅在非嵌入模式下显示 */}
{!isEmbedded && (
<Box px={4} pt={4} pb={2}> <Box px={4} pt={4} pb={2}>
<HStack alignItems="center" justifyContent="space-between"> <HStack alignItems="center" justifyContent="space-between">
<HStack alignItems="center"> <HStack alignItems="center">
@@ -423,6 +424,7 @@ const TodayStats = ({ navigation }) => {
</Pressable> </Pressable>
</HStack> </HStack>
</Box> </Box>
)}
{/* 双圆环仪表盘 */} {/* 双圆环仪表盘 */}
<Box mx={4} mt={4}> <Box mx={4} mt={4}>

View File

@@ -3,11 +3,16 @@
* 复用 Web 端的 API 逻辑,适配 React Native * 复用 Web 端的 API 逻辑,适配 React Native
*/ */
import AsyncStorage from '@react-native-async-storage/async-storage';
export const API_BASE_URL = 'https://api.valuefrontier.cn'; export const API_BASE_URL = 'https://api.valuefrontier.cn';
// 静态数据存储地址(腾讯 COS // 静态数据存储地址(腾讯 COS
export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com'; export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com';
// Token 存储键名(与 authService 保持一致)
const ACCESS_TOKEN_KEY = '@auth_access_token';
/** /**
* 通用 API 请求函数 * 通用 API 请求函数
* @param {string} url - API 路径 * @param {string} url - API 路径
@@ -21,13 +26,24 @@ export const apiRequest = async (url, options = {}) => {
console.log(`[API] ${method} ${fullUrl}`); console.log(`[API] ${method} ${fullUrl}`);
try { try {
const response = await fetch(fullUrl, { // 获取存储的 access_token
...options, const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
headers: {
// 构建请求头
const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers, ...options.headers,
}, };
// 重要:携带 Cookie 以支持 Session 认证
// 如果有 token添加到 Authorization header
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
const response = await fetch(fullUrl, {
...options,
headers,
// 保留 credentials 以支持 Cookie作为备选认证方式
credentials: 'include', credentials: 'include',
}); });

View File

@@ -10,6 +10,7 @@ import { apiRequest } from './api';
const STORAGE_KEYS = { const STORAGE_KEYS = {
USER_INFO: '@auth_user_info', USER_INFO: '@auth_user_info',
IS_LOGGED_IN: '@auth_is_logged_in', IS_LOGGED_IN: '@auth_is_logged_in',
ACCESS_TOKEN: '@auth_access_token',
}; };
/** /**
@@ -64,6 +65,13 @@ export const authService = {
// 保存用户信息到本地存储 // 保存用户信息到本地存储
await AsyncStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(response.user)); await AsyncStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(response.user));
await AsyncStorage.setItem(STORAGE_KEYS.IS_LOGGED_IN, 'true'); await AsyncStorage.setItem(STORAGE_KEYS.IS_LOGGED_IN, 'true');
// 保存 access_token如果后端返回
if (response.access_token) {
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.access_token);
console.log('[AuthService] 已保存 access_token');
}
console.log('[AuthService] 登录成功,用户:', response.user.username); console.log('[AuthService] 登录成功,用户:', response.user.username);
} }
@@ -125,11 +133,25 @@ export const authService = {
try { try {
await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO); await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO);
await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN); await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN);
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) { } catch (error) {
console.error('[AuthService] 清除本地状态失败:', error); console.error('[AuthService] 清除本地状态失败:', error);
} }
}, },
/**
* 获取存储的 access_token
* @returns {Promise<string|null>}
*/
getAccessToken: async () => {
try {
return await AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
} catch (error) {
console.error('[AuthService] 读取 token 失败:', error);
return null;
}
},
/** /**
* 获取本地存储的用户信息 * 获取本地存储的用户信息
* @returns {Promise<object|null>} 用户信息 * @returns {Promise<object|null>} 用户信息

View File

@@ -34,7 +34,7 @@ const initialState = {
popularKeywords: [], popularKeywords: [],
// 主线/题材事件数据 // 主线/题材事件数据
mainlineData: [], mainlineData: [],
mainlineGroupBy: 'lv1', mainlineGroupBy: 'lv3',
// 当前事件详情 // 当前事件详情
currentEvent: null, currentEvent: null,
// 加载状态 // 加载状态

View File

@@ -2618,7 +2618,21 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
raise raise
assistant_message = response.choices[0].message assistant_message = response.choices[0].message
logger.info(f"[Agent Stream] LLM 响应: finish_reason={response.choices[0].finish_reason}") finish_reason = response.choices[0].finish_reason
logger.info(f"[Agent Stream] LLM 响应: finish_reason={finish_reason}")
# 详细日志:检查 vLLM 返回的内容
tool_calls_count = len(assistant_message.tool_calls) if assistant_message.tool_calls else 0
logger.info(f"[Agent Stream] tool_calls 数量: {tool_calls_count}, finish_reason: {finish_reason}")
if assistant_message.content:
content_preview = assistant_message.content[:500].replace('\n', ' ')
logger.info(f"[Agent Stream] content 预览: {content_preview}")
# 检查是否包含工具调用标签vLLM 应该已解析,如果仍有标签说明解析失败)
if '<minimax:tool_call>' in assistant_message.content:
logger.warning(f"[Agent Stream] ⚠️ vLLM 未解析工具调用finish_reason={finish_reason}")
logger.warning(f"[Agent Stream] ⚠️ 请检查 vLLM 启动参数: --tool-call-parser minimax_m2")
logger.warning(f"[Agent Stream] ⚠️ 将使用备用解析器处理...")
# 获取工具调用(优先使用原生 tool_calls其次解析文本格式 # 获取工具调用(优先使用原生 tool_calls其次解析文本格式
native_tool_calls = assistant_message.tool_calls or [] native_tool_calls = assistant_message.tool_calls or []
@@ -2633,7 +2647,8 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
'```tool_call' in content or '```tool_call' in content or
'"tool":' in content or '"tool":' in content or
'DSML' in content or # DeepSeek DSML 格式 'DSML' in content or # DeepSeek DSML 格式
'DSML' in content # 全角竖线版本 'DSML' in content or # 全角竖线版本
'<minimax:tool_call>' in content # MiniMax 格式
) )
if has_tool_markers: if has_tool_markers:
logger.info(f"[Agent Stream] 尝试从文本内容解析工具调用") logger.info(f"[Agent Stream] 尝试从文本内容解析工具调用")
@@ -3023,6 +3038,9 @@ 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] 使用降级方案")
# 过滤掉可能残留的工具调用标签MiniMax 等模型可能在总结中输出工具调用)
final_summary = self._filter_tool_call_tags(final_summary)
# 发送完整的总结和元数据 # 发送完整的总结和元数据
yield self._format_sse("summary", { yield self._format_sse("summary", {
"content": final_summary, "content": final_summary,
@@ -3091,6 +3109,17 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
"""格式化 SSE 消息""" """格式化 SSE 消息"""
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
def _filter_tool_call_tags(self, text: str) -> str:
"""过滤掉文本中的工具调用标签(总结阶段不应包含工具调用)"""
import re
# 过滤 MiniMax 格式
text = re.sub(r'<minimax:tool_call>.*?</minimax:tool_call>', '', text, flags=re.DOTALL)
# 过滤其他格式
text = re.sub(r'<tool_call>.*?</tool_call>', '', text, flags=re.DOTALL)
text = re.sub(r'```tool_call.*?```', '', text, flags=re.DOTALL)
text = re.sub(r'<[\|]DSML[\|]function_calls>.*?</[\|]DSML[\|]function_calls>', '', text, flags=re.DOTALL)
return text.strip()
def _parse_text_tool_calls(self, content: str) -> List[Dict[str, Any]]: def _parse_text_tool_calls(self, content: str) -> List[Dict[str, Any]]:
""" """
解析文本格式的工具调用 解析文本格式的工具调用
@@ -3099,6 +3128,7 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
1. <tool_call> <function=xxx> <parameter=yyy> value </parameter> </function> </tool_call> 1. <tool_call> <function=xxx> <parameter=yyy> value </parameter> </function> </tool_call>
2. ```tool_call\n{"name": "xxx", "arguments": {...}}\n``` 2. ```tool_call\n{"name": "xxx", "arguments": {...}}\n```
3. DeepSeek DSML 格式: <DSMLfunction_calls> <DSMLinvoke name="xxx"> <DSMLparameter name="yyy" string="true">value</DSMLparameter> </DSMLinvoke> </DSMLfunction_calls> 3. DeepSeek DSML 格式: <DSMLfunction_calls> <DSMLinvoke name="xxx"> <DSMLparameter name="yyy" string="true">value</DSMLparameter> </DSMLinvoke> </DSMLfunction_calls>
4. MiniMax 格式: <minimax:tool_call> <invoke name="xxx"> <parameter name="yyy">value</parameter> </invoke> </minimax:tool_call>
返回: [{"name": "tool_name", "arguments": {...}}, ...] 返回: [{"name": "tool_name", "arguments": {...}}, ...]
""" """
@@ -3106,6 +3136,47 @@ A股交易时间: 上午 9:30-11:30下午 13:00-15:00
tool_calls = [] tool_calls = []
# 格式0 (优先): MiniMax <minimax:tool_call> 格式
# <minimax:tool_call> <invoke name="get_stock_basic_info"> <parameter name="stock_code">002916.SZ</parameter> </invoke> </minimax:tool_call>
minimax_pattern = r'<minimax:tool_call>(.*?)</minimax:tool_call>'
minimax_matches = re.findall(minimax_pattern, content, re.DOTALL)
for minimax_content in minimax_matches:
# 解析 invoke 标签 - 支持带引号和不带引号的 name
invoke_pattern = r'<invoke\s+name=(?:"([^"]+)"|([^>\s]+))>(.*?)</invoke>'
invoke_matches = re.findall(invoke_pattern, minimax_content, re.DOTALL)
for name_quoted, name_unquoted, params_str in invoke_matches:
func_name = name_quoted or name_unquoted
if not func_name:
continue
arguments = {}
# 解析参数: <parameter name="xxx">value</parameter> 或 <parameter name=xxx>value</parameter>
param_pattern = r'<parameter\s+name=(?:"([^"]+)"|([^>\s]+))>(.*?)</parameter>'
param_matches = re.findall(param_pattern, params_str, re.DOTALL)
for pname_quoted, pname_unquoted, param_value in param_matches:
param_name = pname_quoted or pname_unquoted
if not param_name:
continue
param_value = param_value.strip()
# 尝试解析 JSON 值,否则作为字符串
try:
arguments[param_name] = json.loads(param_value)
except:
arguments[param_name] = param_value
tool_calls.append({
"name": func_name,
"arguments": arguments
})
# 如果 MiniMax 格式解析到了工具调用,直接返回(优先级最高)
if tool_calls:
logger.info(f"[MiniMax Tool Call] 解析到 {len(tool_calls)} 个工具调用: {tool_calls}")
return tool_calls
# 格式1: <tool_call> 标签格式 # 格式1: <tool_call> 标签格式
# 例如: <tool_call> <function=get_stock_concepts> <parameter=seccode> 300274 </parameter> </function> </tool_call> # 例如: <tool_call> <function=get_stock_concepts> <parameter=seccode> 300274 </parameter> </function> </tool_call>
pattern1 = r'<tool_call>\s*<function=(\w+)>(.*?)</function>\s*</tool_call>' pattern1 = r'<tool_call>\s*<function=(\w+)>(.*?)</function>\s*</tool_call>'