更新ios
This commit is contained in:
@@ -460,7 +460,10 @@
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
@@ -518,7 +521,10 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -323,6 +323,8 @@ function MarketStack(props) {
|
||||
<Header
|
||||
title={route.params?.sectorName || "板块详情"}
|
||||
back
|
||||
white
|
||||
bgColor="#0F172A"
|
||||
navigation={navigation}
|
||||
scene={scene}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 支持 Markdown 渲染和 ECharts 图表渲染
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo, useState, useCallback } from 'react';
|
||||
import React, { memo, useMemo, useState, useCallback, useContext, createContext } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -14,8 +14,12 @@ import {
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
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 CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
|
||||
const CHART_HEIGHT = 300;
|
||||
@@ -137,7 +141,7 @@ const TableView = memo(({ rows }) => {
|
||||
/**
|
||||
* Markdown 文本渲染器(支持表格)
|
||||
*/
|
||||
const MarkdownText = memo(({ content }) => {
|
||||
const MarkdownText = memo(({ content, navigation }) => {
|
||||
const lines = content.split('\n');
|
||||
const elements = [];
|
||||
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++;
|
||||
}
|
||||
|
||||
@@ -169,7 +173,7 @@ const MarkdownText = memo(({ content }) => {
|
||||
/**
|
||||
* 单行 Markdown 渲染
|
||||
*/
|
||||
const MarkdownLine = memo(({ line }) => {
|
||||
const MarkdownLine = memo(({ line, navigation }) => {
|
||||
// 空行
|
||||
if (!line.trim()) {
|
||||
return <View style={styles.emptyLine} />;
|
||||
@@ -188,7 +192,7 @@ const MarkdownLine = memo(({ line }) => {
|
||||
];
|
||||
return (
|
||||
<Text style={[styles.headerBase, headerStyles[level - 1]]}>
|
||||
{renderInlineStyles(text)}
|
||||
{renderInlineStyles(text, navigation)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -199,7 +203,7 @@ const MarkdownLine = memo(({ line }) => {
|
||||
return (
|
||||
<View style={styles.listItem}>
|
||||
<Text style={styles.listBullet}>•</Text>
|
||||
<Text style={styles.listText}>{renderInlineStyles(listMatch[1])}</Text>
|
||||
<Text style={styles.listText}>{renderInlineStyles(listMatch[1], navigation)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -210,7 +214,7 @@ const MarkdownLine = memo(({ line }) => {
|
||||
return (
|
||||
<View style={styles.listItem}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -225,7 +229,7 @@ const MarkdownLine = memo(({ line }) => {
|
||||
if (quoteMatch) {
|
||||
return (
|
||||
<View style={styles.quote}>
|
||||
<Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1])}</Text>
|
||||
<Text style={styles.quoteText}>{renderInlineStyles(quoteMatch[1], navigation)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -237,32 +241,113 @@ const MarkdownLine = memo(({ line }) => {
|
||||
|
||||
// 普通段落
|
||||
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;
|
||||
|
||||
// 先进行基本文本处理
|
||||
const processedText = text
|
||||
.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;
|
||||
|
||||
// 为简化实现,这里只做基本渲染
|
||||
// 完整实现需要递归解析
|
||||
elements.push(
|
||||
<Text key={key++} style={styles.text}>
|
||||
{text
|
||||
.replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
|
||||
.replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
|
||||
.replace(/`([^`]+)`/g, (_, m) => `[${m}]`) // 标记代码
|
||||
}
|
||||
</Text>
|
||||
);
|
||||
stockMatches.forEach((stockMatch) => {
|
||||
// 添加股票代码前的文本
|
||||
if (stockMatch.index > lastIndex) {
|
||||
elements.push(
|
||||
<Text key={key++} style={styles.text}>
|
||||
{processedText.substring(lastIndex, stockMatch.index)}
|
||||
</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 主组件
|
||||
*/
|
||||
const MarkdownRenderer = ({ content }) => {
|
||||
const navigation = useNavigation();
|
||||
const parts = useMemo(() => parseMarkdown(content), [content]);
|
||||
|
||||
return (
|
||||
@@ -681,7 +767,7 @@ const MarkdownRenderer = ({ content }) => {
|
||||
if (part.type === 'mermaid') {
|
||||
return <MermaidView key={index} code={part.content} />;
|
||||
}
|
||||
return <MarkdownText key={index} content={part.content} />;
|
||||
return <MarkdownText key={index} content={part.content} navigation={navigation} />;
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
@@ -795,6 +881,16 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 16,
|
||||
},
|
||||
|
||||
// 股票代码链接
|
||||
stockCodeLink: {
|
||||
color: '#F59E0B',
|
||||
fontWeight: '600',
|
||||
textDecorationLine: 'underline',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
// 图表容器
|
||||
chartContainer: {
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
|
||||
@@ -95,38 +95,6 @@ const WelcomeScreen = ({ onQuickQuestion }) => {
|
||||
<Text style={styles.tagline}>在市场的混沌中,找到价值与风险的平衡点</Text>
|
||||
</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.sectionHeader}>
|
||||
|
||||
@@ -630,7 +630,7 @@ const ConceptList = () => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
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 [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念
|
||||
const [leafLoading, setLeafLoading] = useState(false);
|
||||
@@ -1154,7 +1154,7 @@ const ConceptList = () => {
|
||||
</View>
|
||||
) : (
|
||||
// 父分类卡片列表
|
||||
<VStack space={4}>
|
||||
<VStack space={4} w="100%">
|
||||
{currentItems.map((item, index) => (
|
||||
<ParentCard
|
||||
key={item.name || index}
|
||||
@@ -1197,6 +1197,7 @@ const styles = StyleSheet.create({
|
||||
parentCard: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
},
|
||||
cardContainer: {
|
||||
borderRadius: 16,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import EventCard from './EventCard';
|
||||
import MainlineView from './MainlineView';
|
||||
import TodayStats from '../Market/TodayStats';
|
||||
import { gradients } from '../../theme';
|
||||
import {
|
||||
fetchEvents,
|
||||
@@ -36,6 +37,7 @@ const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const VIEW_MODES = {
|
||||
LIST: 'list',
|
||||
MAINLINE: 'mainline',
|
||||
STATS: 'stats',
|
||||
};
|
||||
|
||||
// 时间范围选项
|
||||
@@ -205,6 +207,34 @@ const EventList = ({ navigation }) => {
|
||||
)
|
||||
)}
|
||||
</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>
|
||||
</Box>
|
||||
);
|
||||
@@ -479,6 +509,19 @@ const EventList = ({ navigation }) => {
|
||||
|
||||
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) {
|
||||
return (
|
||||
|
||||
@@ -27,10 +27,20 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import ztService from '../../services/ztService';
|
||||
import tradingDayUtils from '../../utils/tradingDayUtils';
|
||||
import { gradients } from '../../theme';
|
||||
|
||||
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 d = new Date(date);
|
||||
@@ -73,7 +83,7 @@ const parseContinuousDays = (str) => {
|
||||
|
||||
const MarketHot = ({ navigation }) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [currentDate, setCurrentDate] = useState(getInitialDate);
|
||||
const [ztData, setZtData] = useState(null);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [hotSectors, setHotSectors] = useState([]);
|
||||
@@ -179,24 +189,14 @@ const MarketHot = ({ navigation }) => {
|
||||
</Text>
|
||||
</VStack>
|
||||
</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
|
||||
onPress={() => navigation.navigate('EventCalendar')}
|
||||
p={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.1)"
|
||||
>
|
||||
<Icon as={Ionicons} name="calendar" size="md" color="white" />
|
||||
</Pressable>
|
||||
</HStack>
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate('EventCalendar')}
|
||||
p={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.1)"
|
||||
>
|
||||
<Icon as={Ionicons} name="calendar" size="md" color="white" />
|
||||
</Pressable>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
@@ -212,7 +212,7 @@ const MarketHot = ({ navigation }) => {
|
||||
<Icon as={Ionicons} name="chevron-back" size="sm" color="gray.400" />
|
||||
</Pressable>
|
||||
|
||||
<Pressable onPress={() => setCurrentDate(new Date())}>
|
||||
<Pressable onPress={() => setCurrentDate(getInitialDate())}>
|
||||
<VStack alignItems="center">
|
||||
<Text fontSize="lg" fontWeight="bold" color="white">
|
||||
{formatDisplayDate(currentDate)}
|
||||
|
||||
@@ -302,7 +302,7 @@ const TopStockItem = ({ stock, rank }) => (
|
||||
</HStack>
|
||||
);
|
||||
|
||||
const TodayStats = ({ navigation }) => {
|
||||
const TodayStats = ({ navigation, isEmbedded = false }) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -364,7 +364,7 @@ const TodayStats = ({ navigation }) => {
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
paddingTop: insets.top,
|
||||
paddingTop: isEmbedded ? 0 : insets.top,
|
||||
paddingBottom: insets.bottom + 20,
|
||||
}}
|
||||
refreshControl={
|
||||
@@ -376,53 +376,55 @@ const TodayStats = ({ navigation }) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<Box px={4} pt={4} pb={2}>
|
||||
<HStack alignItems="center" justifyContent="space-between">
|
||||
<HStack alignItems="center">
|
||||
<Pressable
|
||||
onPress={() => navigation.goBack()}
|
||||
p={2}
|
||||
mr={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.05)"
|
||||
>
|
||||
<Icon as={Ionicons} name="arrow-back" size="md" color="white" />
|
||||
</Pressable>
|
||||
<VStack>
|
||||
<HStack alignItems="center" space={2}>
|
||||
<Text fontSize="xl" fontWeight="bold" color="white">
|
||||
今日统计
|
||||
{/* 标题栏 - 仅在非嵌入模式下显示 */}
|
||||
{!isEmbedded && (
|
||||
<Box px={4} pt={4} pb={2}>
|
||||
<HStack alignItems="center" justifyContent="space-between">
|
||||
<HStack alignItems="center">
|
||||
<Pressable
|
||||
onPress={() => navigation.goBack()}
|
||||
p={2}
|
||||
mr={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.05)"
|
||||
>
|
||||
<Icon as={Ionicons} name="arrow-back" size="md" color="white" />
|
||||
</Pressable>
|
||||
<VStack>
|
||||
<HStack alignItems="center" space={2}>
|
||||
<Text fontSize="xl" fontWeight="bold" color="white">
|
||||
今日统计
|
||||
</Text>
|
||||
<Box
|
||||
px={2}
|
||||
py={0.5}
|
||||
bg="rgba(34, 197, 94, 0.15)"
|
||||
borderWidth={1}
|
||||
borderColor="rgba(34, 197, 94, 0.3)"
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack space={1} alignItems="center">
|
||||
<Box w={1.5} h={1.5} borderRadius="full" bg="#22C55E" />
|
||||
<Text fontSize="2xs" color="#22C55E" fontWeight="medium">实时</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
事件胜率 · 市场统计 · TOP排行
|
||||
</Text>
|
||||
<Box
|
||||
px={2}
|
||||
py={0.5}
|
||||
bg="rgba(34, 197, 94, 0.15)"
|
||||
borderWidth={1}
|
||||
borderColor="rgba(34, 197, 94, 0.3)"
|
||||
borderRadius="full"
|
||||
>
|
||||
<HStack space={1} alignItems="center">
|
||||
<Box w={1.5} h={1.5} borderRadius="full" bg="#22C55E" />
|
||||
<Text fontSize="2xs" color="#22C55E" fontWeight="medium">实时</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
事件胜率 · 市场统计 · TOP排行
|
||||
</Text>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Pressable
|
||||
onPress={() => navigation.openDrawer?.()}
|
||||
p={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.1)"
|
||||
>
|
||||
<Icon as={Ionicons} name="menu" size="md" color="white" />
|
||||
</Pressable>
|
||||
</HStack>
|
||||
<Pressable
|
||||
onPress={() => navigation.openDrawer?.()}
|
||||
p={2}
|
||||
rounded="full"
|
||||
bg="rgba(255,255,255,0.1)"
|
||||
>
|
||||
<Icon as={Ionicons} name="menu" size="md" color="white" />
|
||||
</Pressable>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 双圆环仪表盘 */}
|
||||
<Box mx={4} mt={4}>
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
* 复用 Web 端的 API 逻辑,适配 React Native
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export const API_BASE_URL = 'https://api.valuefrontier.cn';
|
||||
|
||||
// 静态数据存储地址(腾讯 COS)
|
||||
export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com';
|
||||
|
||||
// Token 存储键名(与 authService 保持一致)
|
||||
const ACCESS_TOKEN_KEY = '@auth_access_token';
|
||||
|
||||
/**
|
||||
* 通用 API 请求函数
|
||||
* @param {string} url - API 路径
|
||||
@@ -21,13 +26,24 @@ export const apiRequest = async (url, options = {}) => {
|
||||
console.log(`[API] ${method} ${fullUrl}`);
|
||||
|
||||
try {
|
||||
// 获取存储的 access_token
|
||||
const accessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
|
||||
// 构建请求头
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
// 如果有 token,添加到 Authorization header
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
// 重要:携带 Cookie 以支持 Session 认证
|
||||
headers,
|
||||
// 保留 credentials 以支持 Cookie(作为备选认证方式)
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { apiRequest } from './api';
|
||||
const STORAGE_KEYS = {
|
||||
USER_INFO: '@auth_user_info',
|
||||
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.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);
|
||||
}
|
||||
|
||||
@@ -125,11 +133,25 @@ export const authService = {
|
||||
try {
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.USER_INFO);
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.IS_LOGGED_IN);
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
||||
} catch (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>} 用户信息
|
||||
|
||||
@@ -34,7 +34,7 @@ const initialState = {
|
||||
popularKeywords: [],
|
||||
// 主线/题材事件数据
|
||||
mainlineData: [],
|
||||
mainlineGroupBy: 'lv1',
|
||||
mainlineGroupBy: 'lv3',
|
||||
// 当前事件详情
|
||||
currentEvent: null,
|
||||
// 加载状态
|
||||
|
||||
@@ -2618,7 +2618,21 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
||||
raise
|
||||
|
||||
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,其次解析文本格式)
|
||||
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":' in content or
|
||||
'DSML' in content or # DeepSeek DSML 格式
|
||||
'|DSML|' in content # 全角竖线版本
|
||||
'|DSML|' in content or # 全角竖线版本
|
||||
'<minimax:tool_call>' in content # MiniMax 格式
|
||||
)
|
||||
if has_tool_markers:
|
||||
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})
|
||||
logger.warning("[Summary] 使用降级方案")
|
||||
|
||||
# 过滤掉可能残留的工具调用标签(MiniMax 等模型可能在总结中输出工具调用)
|
||||
final_summary = self._filter_tool_call_tags(final_summary)
|
||||
|
||||
# 发送完整的总结和元数据
|
||||
yield self._format_sse("summary", {
|
||||
"content": final_summary,
|
||||
@@ -3091,6 +3109,17 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
||||
"""格式化 SSE 消息"""
|
||||
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]]:
|
||||
"""
|
||||
解析文本格式的工具调用
|
||||
@@ -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>
|
||||
2. ```tool_call\n{"name": "xxx", "arguments": {...}}\n```
|
||||
3. DeepSeek DSML 格式: <|DSML|function_calls> <|DSML|invoke name="xxx"> <|DSML|parameter name="yyy" string="true">value</|DSML|parameter> </|DSML|invoke> </|DSML|function_calls>
|
||||
4. MiniMax 格式: <minimax:tool_call> <invoke name="xxx"> <parameter name="yyy">value</parameter> </invoke> </minimax:tool_call>
|
||||
|
||||
返回: [{"name": "tool_name", "arguments": {...}}, ...]
|
||||
"""
|
||||
@@ -3106,6 +3136,47 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00
|
||||
|
||||
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> 标签格式
|
||||
# 例如: <tool_call> <function=get_stock_concepts> <parameter=seccode> 300274 </parameter> </function> </tool_call>
|
||||
pattern1 = r'<tool_call>\s*<function=(\w+)>(.*?)</function>\s*</tool_call>'
|
||||
|
||||
Reference in New Issue
Block a user