diff --git a/MeAgent/assets/imgs/luoxi.jpg b/MeAgent/assets/imgs/luoxi.jpg
new file mode 100644
index 00000000..4134069e
Binary files /dev/null and b/MeAgent/assets/imgs/luoxi.jpg differ
diff --git a/MeAgent/navigation/Menu.js b/MeAgent/navigation/Menu.js
index 87d6a8c4..1aa9a697 100644
--- a/MeAgent/navigation/Menu.js
+++ b/MeAgent/navigation/Menu.js
@@ -167,6 +167,7 @@ function CustomDrawerContent({
{ title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] },
{ title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] },
{ title: "社区论坛", navigateTo: "CommunityDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] },
+ { title: "AI 助手", navigateTo: "AgentDrawer", icon: "sparkles", gradient: ["#8B5CF6", "#EC4899"] },
{ title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] },
];
return (
diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js
index b5ddcc77..31d0f6d5 100644
--- a/MeAgent/navigation/Screens.js
+++ b/MeAgent/navigation/Screens.js
@@ -65,6 +65,9 @@ import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile";
// 认证页面
import { LoginScreen } from "../src/screens/Auth";
+// AI 助手页面
+import { AgentChatScreen } from "../src/screens/Agent";
+
// 推送通知处理
import PushNotificationHandler from "../src/components/PushNotificationHandler";
@@ -489,6 +492,26 @@ function NewProfileStack(props) {
);
}
+// AI 助手导航栈
+function AgentStack(props) {
+ return (
+
+
+
+ );
+}
+
function ProfileStack(props) {
return (
+
tool.category !== 'quant')
+ .map(tool => tool.id);
+
+// Agent 名称
+export const AGENT_NAME = '洛希';
+
+// 欢迎消息模板
+export const WELCOME_MESSAGE_TEMPLATE = (nickname) => `你好${nickname ? ` ${nickname}` : ''}!
+
+我是**洛希**,你的 AI 投研助手。
+
+「洛希」源自洛希极限(Roche Limit)—— 天体力学中描述引力边界的临界点。正如我帮你在市场的混沌中,找到价值与风险的平衡点。
+
+**我能做什么?**
+• 📊 深度分析股票基本面和技术面
+• 🔥 追踪市场热点和涨停板块
+• 📈 研究行业趋势和投资机会
+• 📰 汇总最新财经新闻和研报
+• 🧮 量化指标计算和回测分析
+
+有什么可以帮你的?`;
+
+// 快捷问题
+export const QUICK_QUESTIONS = [
+ '今天有哪些涨停板块?',
+ '帮我分析一下贵州茅台',
+ '最近有什么热门概念?',
+ '查看我的自选股表现',
+];
+
+// API 配置
+export const AGENT_API_CONFIG = {
+ // 生产环境 - 使用 api.valuefrontier.cn
+ BASE_URL: 'https://api.valuefrontier.cn/mcp',
+ // 开发环境 - 本地测试时使用
+ // BASE_URL: 'http://localhost:8900',
+ ENDPOINTS: {
+ CHAT_STREAM: '/agent/chat/stream',
+ SESSIONS: '/agent/sessions',
+ HISTORY: '/agent/history',
+ },
+ DEFAULT_TIMEOUT: 60000, // 60 秒
+ MAX_HISTORY_LIMIT: 100,
+ MAX_SESSIONS_LIMIT: 50,
+};
diff --git a/MeAgent/src/hooks/useAgentChat.js b/MeAgent/src/hooks/useAgentChat.js
new file mode 100644
index 00000000..f936ab85
--- /dev/null
+++ b/MeAgent/src/hooks/useAgentChat.js
@@ -0,0 +1,391 @@
+/**
+ * useAgentChat Hook
+ * 处理 Agent 聊天核心逻辑:发送消息、SSE 流式响应、状态更新
+ */
+
+import { useCallback, useRef, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useAuth } from '../contexts/AuthContext';
+import { streamAgentChat, buildConversationHistory } from '../services/agentService';
+import {
+ MessageTypes,
+ SSEEventTypes,
+} from '../constants/agentConstants';
+import {
+ addMessage,
+ updateLastMessageByType,
+ removeMessagesByType,
+ setIsProcessing,
+ resetStreamState,
+ appendThinkingContent,
+ appendDeepThinkingContent,
+ updateStreamState,
+ setPlan,
+ addStepResult,
+ setCurrentSession,
+ selectMessages,
+ selectIsProcessing,
+ selectCurrentSessionId,
+ selectSelectedModel,
+ selectSelectedTools,
+ selectStreamState,
+ fetchSessions,
+} from '../store/slices/agentSlice';
+
+/**
+ * Agent 聊天 Hook
+ *
+ * @returns {Object} 聊天相关的状态和方法
+ */
+export const useAgentChat = () => {
+ const dispatch = useDispatch();
+ const { user, subscription } = useAuth();
+
+ // 从 Redux 获取状态
+ const messages = useSelector(selectMessages);
+ const isProcessing = useSelector(selectIsProcessing);
+ const currentSessionId = useSelector(selectCurrentSessionId);
+ const selectedModel = useSelector(selectSelectedModel);
+ const selectedTools = useSelector(selectSelectedTools);
+ const streamState = useSelector(selectStreamState);
+
+ // AbortController 引用,用于取消请求
+ const abortControllerRef = useRef(null);
+
+ // 使用 ref 追踪累积的内容(避免闭包问题)
+ const thinkingContentRef = useRef('');
+ const deepThinkingContentRef = useRef('');
+ const responseContentRef = useRef('');
+ const stepResultsRef = useRef([]);
+ const messagesRef = useRef(messages);
+
+ // 保持 messagesRef 同步
+ messagesRef.current = messages;
+
+ // 组件卸载时清理
+ useEffect(() => {
+ return () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ };
+ }, []);
+
+ /**
+ * 处理 SSE 事件
+ * 使用 ref 追踪累积内容,避免闭包导致的状态过时问题
+ */
+ const handleSSEEvent = useCallback((event) => {
+ const { type, data } = event;
+
+ switch (type) {
+ case SSEEventTypes.STATUS:
+ // 状态更新(如 "正在思考...")
+ if (data?.message) {
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_THINKING,
+ updates: { content: data.message },
+ }));
+ }
+ break;
+
+ case SSEEventTypes.THINKING:
+ // 普通思考内容
+ if (data?.content) {
+ thinkingContentRef.current += data.content;
+ dispatch(appendThinkingContent(data.content));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_THINKING,
+ updates: { thinkingContent: thinkingContentRef.current },
+ }));
+ }
+ break;
+
+ case SSEEventTypes.THINKING_START:
+ // 深度思考开始
+ deepThinkingContentRef.current = '';
+ dispatch(updateStreamState({ isDeepThinking: true }));
+ dispatch(addMessage({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ content: '',
+ isStreaming: true,
+ }));
+ break;
+
+ case SSEEventTypes.THINKING_CHUNK:
+ // 深度思考内容流
+ if (data?.content) {
+ deepThinkingContentRef.current += data.content;
+ dispatch(appendDeepThinkingContent(data.content));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ updates: {
+ content: deepThinkingContentRef.current,
+ },
+ }));
+ }
+ break;
+
+ case SSEEventTypes.THINKING_END:
+ // 深度思考结束
+ dispatch(updateStreamState({ isDeepThinking: false }));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ updates: { isStreaming: false },
+ }));
+ break;
+
+ case SSEEventTypes.REASONING:
+ // 推理过程
+ if (data?.content) {
+ thinkingContentRef.current += data.content;
+ dispatch(appendThinkingContent(data.content));
+ }
+ break;
+
+ case SSEEventTypes.PLAN:
+ // 执行计划
+ stepResultsRef.current = [];
+ dispatch(setPlan(data));
+ dispatch(addMessage({
+ type: MessageTypes.AGENT_PLAN,
+ content: data?.goal || '执行计划',
+ plan: data,
+ }));
+ // 添加执行中消息
+ dispatch(addMessage({
+ type: MessageTypes.AGENT_EXECUTING,
+ content: '正在执行...',
+ stepResults: [],
+ }));
+ break;
+
+ case SSEEventTypes.STEP_START:
+ // 步骤开始
+ if (data?.tool) {
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_EXECUTING,
+ updates: {
+ content: `正在执行: ${data.tool}`,
+ currentStep: data,
+ },
+ }));
+ }
+ break;
+
+ case SSEEventTypes.STEP_COMPLETE:
+ // 步骤完成
+ stepResultsRef.current = [...stepResultsRef.current, data];
+ dispatch(addStepResult(data));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_EXECUTING,
+ updates: {
+ stepResults: stepResultsRef.current,
+ },
+ }));
+ break;
+
+ case SSEEventTypes.SUMMARY_CHUNK:
+ // 总结流式输出
+ if (data?.content) {
+ // 首次收到内容时创建消息
+ if (responseContentRef.current === '') {
+ responseContentRef.current = data.content;
+ dispatch(addMessage({
+ type: MessageTypes.AGENT_RESPONSE,
+ content: data.content,
+ isStreaming: true,
+ }));
+ } else {
+ responseContentRef.current += data.content;
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_RESPONSE,
+ updates: {
+ content: responseContentRef.current,
+ },
+ }));
+ }
+ }
+ break;
+
+ case SSEEventTypes.SESSION_TITLE:
+ // 会话标题
+ if (data?.title) {
+ dispatch(setCurrentSession({
+ sessionId: data.session_id || currentSessionId,
+ title: data.title,
+ }));
+ }
+ break;
+
+ case SSEEventTypes.DONE:
+ // 完成 - 清理所有中间状态消息,确保动画停止
+ dispatch(removeMessagesByType(MessageTypes.AGENT_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_EXECUTING));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ updates: { isStreaming: false },
+ }));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_RESPONSE,
+ updates: { isStreaming: false },
+ }));
+ dispatch(setIsProcessing(false));
+ // 重新加载会话列表
+ if (user?.id) {
+ dispatch(fetchSessions(user.id));
+ }
+ break;
+
+ case SSEEventTypes.ERROR:
+ // 错误 - 移除 thinking 消息后再添加错误消息
+ dispatch(removeMessagesByType(MessageTypes.AGENT_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_DEEP_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_EXECUTING));
+ dispatch(addMessage({
+ type: MessageTypes.ERROR,
+ content: data?.message || '发生错误,请重试',
+ }));
+ dispatch(setIsProcessing(false));
+ break;
+
+ case 'stream_end':
+ // 流结束 - 确保所有动画停止
+ dispatch(removeMessagesByType(MessageTypes.AGENT_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_EXECUTING));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ updates: { isStreaming: false },
+ }));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_RESPONSE,
+ updates: { isStreaming: false },
+ }));
+ dispatch(setIsProcessing(false));
+ break;
+
+ case 'aborted':
+ // 请求被取消 - 移除所有中间状态消息,停止动画
+ dispatch(removeMessagesByType(MessageTypes.AGENT_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_DEEP_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_EXECUTING));
+ dispatch(updateLastMessageByType({
+ type: MessageTypes.AGENT_RESPONSE,
+ updates: { isStreaming: false },
+ }));
+ dispatch(setIsProcessing(false));
+ break;
+
+ default:
+ console.log('[useAgentChat] 未处理的事件类型:', type, data);
+ }
+ }, [dispatch, currentSessionId, user?.id]);
+
+ /**
+ * 发送消息
+ */
+ const sendMessage = useCallback(async (inputText) => {
+ if (!inputText?.trim() || isProcessing) return;
+
+ const messageText = inputText.trim();
+
+ // 创建并取消之前的请求
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ abortControllerRef.current = new AbortController();
+
+ // 重置 ref 追踪值
+ thinkingContentRef.current = '';
+ deepThinkingContentRef.current = '';
+ responseContentRef.current = '';
+ stepResultsRef.current = [];
+
+ // 添加用户消息
+ dispatch(addMessage({
+ type: MessageTypes.USER,
+ content: messageText,
+ }));
+
+ // 重置流式状态
+ dispatch(resetStreamState());
+ dispatch(setIsProcessing(true));
+
+ // 添加思考中消息
+ dispatch(addMessage({
+ type: MessageTypes.AGENT_THINKING,
+ content: '正在思考...',
+ }));
+
+ try {
+ // 构建请求参数(使用 ref 获取最新 messages,避免依赖变化)
+ const params = {
+ message: messageText,
+ conversationHistory: buildConversationHistory(messagesRef.current),
+ userId: user?.id || 'anonymous',
+ userNickname: user?.nickname || user?.username || '',
+ userAvatar: user?.avatar || '',
+ subscriptionType: subscription?.type || 'free', // 从 subscription 对象获取
+ sessionId: currentSessionId,
+ model: selectedModel,
+ tools: selectedTools,
+ };
+
+ // 发起 SSE 请求
+ await streamAgentChat(params, handleSSEEvent, abortControllerRef.current.signal);
+
+ } catch (error) {
+ if (error.name !== 'AbortError') {
+ console.error('[useAgentChat] 发送消息失败:', error);
+ // 移除 thinking 相关消息
+ dispatch(removeMessagesByType(MessageTypes.AGENT_THINKING));
+ dispatch(removeMessagesByType(MessageTypes.AGENT_DEEP_THINKING));
+ dispatch(addMessage({
+ type: MessageTypes.ERROR,
+ content: error.message || '网络错误,请检查网络连接后重试',
+ }));
+ }
+ dispatch(setIsProcessing(false));
+ }
+ }, [dispatch, isProcessing, currentSessionId, selectedModel, selectedTools, user?.id, user?.nickname, user?.username, user?.avatar, subscription?.type, handleSSEEvent]);
+
+ /**
+ * 取消当前请求
+ */
+ const cancelRequest = useCallback(() => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ dispatch(setIsProcessing(false));
+ }
+ }, [dispatch]);
+
+ /**
+ * 重试最后一条消息
+ */
+ const retryLastMessage = useCallback(() => {
+ // 找到最后一条用户消息
+ const lastUserMessage = [...messages].reverse().find(m => m.type === MessageTypes.USER);
+ if (lastUserMessage) {
+ // 移除错误消息和 AI 响应
+ // 这里简化处理,实际可能需要更精细的逻辑
+ sendMessage(lastUserMessage.content);
+ }
+ }, [messages, sendMessage]);
+
+ return {
+ // 状态
+ messages,
+ isProcessing,
+ streamState,
+ currentSessionId,
+ selectedModel,
+ selectedTools,
+
+ // 方法
+ sendMessage,
+ cancelRequest,
+ retryLastMessage,
+ };
+};
+
+export default useAgentChat;
diff --git a/MeAgent/src/hooks/useAgentSessions.js b/MeAgent/src/hooks/useAgentSessions.js
new file mode 100644
index 00000000..35c99694
--- /dev/null
+++ b/MeAgent/src/hooks/useAgentSessions.js
@@ -0,0 +1,141 @@
+/**
+ * useAgentSessions Hook
+ * 处理 Agent 会话管理:会话列表、切换会话、新建会话
+ */
+
+import { useCallback, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useAuth } from '../contexts/AuthContext';
+import {
+ fetchSessions,
+ fetchSessionHistory,
+ initNewSession,
+ clearCurrentSession,
+ setSessionDrawerOpen,
+ selectSessions,
+ selectGroupedSessions,
+ selectSessionsLoading,
+ selectCurrentSessionId,
+ selectCurrentSessionTitle,
+ selectIsSessionDrawerOpen,
+} from '../store/slices/agentSlice';
+
+/**
+ * Agent 会话管理 Hook
+ *
+ * @returns {Object} 会话相关的状态和方法
+ */
+export const useAgentSessions = () => {
+ const dispatch = useDispatch();
+ const { user } = useAuth();
+
+ // 从 Redux 获取状态
+ const sessions = useSelector(selectSessions);
+ const groupedSessions = useSelector(selectGroupedSessions);
+ const isLoading = useSelector(selectSessionsLoading);
+ const currentSessionId = useSelector(selectCurrentSessionId);
+ const currentSessionTitle = useSelector(selectCurrentSessionTitle);
+ const isDrawerOpen = useSelector(selectIsSessionDrawerOpen);
+
+ /**
+ * 加载会话列表
+ */
+ const loadSessions = useCallback(() => {
+ if (user?.id) {
+ dispatch(fetchSessions(String(user.id)));
+ }
+ }, [dispatch, user?.id]);
+
+ /**
+ * 切换到指定会话
+ */
+ const switchSession = useCallback((sessionId) => {
+ if (sessionId && sessionId !== currentSessionId) {
+ dispatch(fetchSessionHistory(sessionId));
+ dispatch(setSessionDrawerOpen(false));
+ }
+ }, [dispatch, currentSessionId]);
+
+ /**
+ * 新建会话
+ */
+ const createNewSession = useCallback(() => {
+ dispatch(initNewSession({ nickname: user?.nickname || user?.username }));
+ dispatch(setSessionDrawerOpen(false));
+ }, [dispatch, user?.nickname, user?.username]);
+
+ /**
+ * 打开会话抽屉
+ */
+ const openDrawer = useCallback(() => {
+ dispatch(setSessionDrawerOpen(true));
+ }, [dispatch]);
+
+ /**
+ * 关闭会话抽屉
+ */
+ const closeDrawer = useCallback(() => {
+ dispatch(setSessionDrawerOpen(false));
+ }, [dispatch]);
+
+ /**
+ * 切换会话抽屉
+ */
+ const toggleDrawer = useCallback(() => {
+ dispatch(setSessionDrawerOpen(!isDrawerOpen));
+ }, [dispatch, isDrawerOpen]);
+
+ /**
+ * 搜索会话
+ */
+ const searchSessions = useCallback((keyword) => {
+ if (!keyword?.trim()) {
+ return groupedSessions;
+ }
+
+ const lowerKeyword = keyword.toLowerCase();
+
+ // 过滤匹配的会话
+ const filteredGroups = groupedSessions
+ .map(group => ({
+ ...group,
+ sessions: group.sessions.filter(session =>
+ (session.title || '').toLowerCase().includes(lowerKeyword) ||
+ (session.session_id || '').toLowerCase().includes(lowerKeyword)
+ ),
+ }))
+ .filter(group => group.sessions.length > 0);
+
+ return filteredGroups;
+ }, [groupedSessions]);
+
+ /**
+ * 初始化:加载会话列表
+ */
+ useEffect(() => {
+ if (user?.id) {
+ loadSessions();
+ }
+ }, [user?.id, loadSessions]);
+
+ return {
+ // 状态
+ sessions,
+ groupedSessions,
+ isLoading,
+ currentSessionId,
+ currentSessionTitle,
+ isDrawerOpen,
+
+ // 方法
+ loadSessions,
+ switchSession,
+ createNewSession,
+ openDrawer,
+ closeDrawer,
+ toggleDrawer,
+ searchSessions,
+ };
+};
+
+export default useAgentSessions;
diff --git a/MeAgent/src/screens/Agent/AgentChatScreen.js b/MeAgent/src/screens/Agent/AgentChatScreen.js
new file mode 100644
index 00000000..19210204
--- /dev/null
+++ b/MeAgent/src/screens/Agent/AgentChatScreen.js
@@ -0,0 +1,330 @@
+/**
+ * AgentChatScreen
+ * AI 投研助手主聊天屏幕
+ */
+
+import React, { useEffect, useRef, useCallback, useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ KeyboardAvoidingView,
+ Platform,
+ TouchableOpacity,
+ StatusBar,
+ SafeAreaView,
+ Image,
+} from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { useAuth } from '../../contexts/AuthContext';
+import { useAgentChat } from '../../hooks/useAgentChat';
+import { useAgentSessions } from '../../hooks/useAgentSessions';
+import {
+ MessageBubble,
+ ChatInput,
+ WelcomeScreen,
+ SessionDrawer,
+} from './components';
+import {
+ AgentTheme,
+ MessageTypes,
+ AGENT_NAME,
+} from '../../constants/agentConstants';
+
+// 洛希头像
+const LuoxiAvatar = require('../../../assets/imgs/luoxi.jpg');
+import {
+ initNewSession,
+ selectMessages,
+} from '../../store/slices/agentSlice';
+
+/**
+ * 头部导航栏
+ */
+const Header = ({ title, onMenuPress, onNewPress }) => (
+
+
+ ☰
+
+
+
+
+
+
+
+ {title || AGENT_NAME}
+
+
+
+
+ +
+
+
+);
+
+/**
+ * AgentChatScreen 主组件
+ */
+const AgentChatScreen = ({ navigation }) => {
+ const dispatch = useDispatch();
+ const { user, isLoggedIn } = useAuth();
+ const flatListRef = useRef(null);
+
+ // 智能滚动:跟踪用户是否在底部附近
+ const isNearBottomRef = useRef(true);
+ const contentHeightRef = useRef(0);
+ const scrollOffsetRef = useRef(0);
+ const layoutHeightRef = useRef(0);
+
+ // 使用自定义 Hooks
+ const {
+ messages,
+ isProcessing,
+ sendMessage,
+ cancelRequest,
+ } = useAgentChat();
+
+ const {
+ groupedSessions,
+ isLoading: sessionsLoading,
+ currentSessionId,
+ currentSessionTitle,
+ isDrawerOpen,
+ openDrawer,
+ closeDrawer,
+ switchSession,
+ createNewSession,
+ } = useAgentSessions();
+
+ /**
+ * 初始化:如果没有消息,显示欢迎界面
+ */
+ useEffect(() => {
+ if (messages.length === 0) {
+ dispatch(initNewSession({ nickname: user?.nickname || user?.username }));
+ }
+ }, []);
+
+ /**
+ * 智能滚动:只有当用户在底部附近时才自动滚动
+ */
+ useEffect(() => {
+ if (messages.length > 0 && flatListRef.current && isNearBottomRef.current) {
+ setTimeout(() => {
+ flatListRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ }
+ }, [messages]);
+
+ /**
+ * 处理滚动事件,检测用户是否在底部附近
+ */
+ const handleScroll = useCallback((event) => {
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
+ scrollOffsetRef.current = contentOffset.y;
+ contentHeightRef.current = contentSize.height;
+ layoutHeightRef.current = layoutMeasurement.height;
+
+ // 判断是否在底部附近(距离底部 100 像素以内)
+ const distanceFromBottom = contentSize.height - layoutMeasurement.height - contentOffset.y;
+ isNearBottomRef.current = distanceFromBottom < 100;
+ }, []);
+
+ /**
+ * 处理内容大小变化
+ */
+ const handleContentSizeChange = useCallback((width, height) => {
+ // 只有当用户在底部附近时才自动滚动
+ if (isNearBottomRef.current && flatListRef.current) {
+ flatListRef.current.scrollToEnd({ animated: true });
+ }
+ }, []);
+
+ /**
+ * 处理快捷问题点击
+ */
+ const handleQuickQuestion = useCallback((question) => {
+ // 发送消息时重置滚动状态,确保自动滚动到底部
+ isNearBottomRef.current = true;
+ sendMessage(question);
+ }, [sendMessage]);
+
+ /**
+ * 处理发送消息(包装 sendMessage 以重置滚动状态)
+ */
+ const handleSendMessage = useCallback((text) => {
+ isNearBottomRef.current = true;
+ sendMessage(text);
+ }, [sendMessage]);
+
+ /**
+ * 处理新建对话
+ */
+ const handleNewSession = useCallback(() => {
+ isNearBottomRef.current = true;
+ createNewSession();
+ }, [createNewSession]);
+
+ /**
+ * 渲染消息项
+ */
+ const renderMessageItem = useCallback(({ item }) => (
+
+
+
+ ), []);
+
+ /**
+ * 消息列表 Key 提取器
+ */
+ const keyExtractor = useCallback((item) => item.id?.toString() || Math.random().toString(), []);
+
+ // 判断是否显示欢迎屏幕(只有一条欢迎消息时)
+ const showWelcome = messages.length <= 1 &&
+ messages[0]?.type === MessageTypes.AGENT_RESPONSE;
+
+ return (
+
+
+
+ {/* 头部 */}
+
+
+ {/* 主体内容 */}
+
+ {showWelcome ? (
+ /* 欢迎屏幕 */
+
+ ) : (
+ /* 消息列表 */
+
+ )}
+
+ {/* 输入框 */}
+
+
+
+ {/* 会话抽屉 */}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: AgentTheme.background,
+ },
+
+ // 头部
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: AgentTheme.border,
+ },
+ headerButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: AgentTheme.cardBg,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ headerButtonText: {
+ fontSize: 20,
+ color: AgentTheme.textPrimary,
+ },
+ headerCenter: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ marginHorizontal: 12,
+ },
+ headerAvatarContainer: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ overflow: 'hidden',
+ marginRight: 10,
+ borderWidth: 2,
+ borderColor: 'rgba(139, 92, 246, 0.5)',
+ },
+ headerAvatar: {
+ width: '100%',
+ height: '100%',
+ resizeMode: 'cover',
+ },
+ headerTitle: {
+ fontSize: 17,
+ fontWeight: '600',
+ color: AgentTheme.textPrimary,
+ maxWidth: 200,
+ flexShrink: 1,
+ },
+
+ // 内容区域
+ content: {
+ flex: 1,
+ },
+
+ // 消息列表
+ messageList: {
+ flex: 1,
+ },
+ messageListContent: {
+ paddingVertical: 16,
+ },
+});
+
+export default AgentChatScreen;
diff --git a/MeAgent/src/screens/Agent/components/ChatInput.js b/MeAgent/src/screens/Agent/components/ChatInput.js
new file mode 100644
index 00000000..93f19407
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/ChatInput.js
@@ -0,0 +1,191 @@
+/**
+ * ChatInput 组件
+ * 聊天输入框,支持发送消息
+ */
+
+import React, { memo, useState, useRef } from 'react';
+import {
+ View,
+ TextInput,
+ TouchableOpacity,
+ StyleSheet,
+ Keyboard,
+ Platform,
+ ActivityIndicator,
+} from 'react-native';
+import { BlurView } from 'expo-blur';
+import { AgentTheme } from '../../../constants/agentConstants';
+
+/**
+ * 发送按钮图标
+ */
+const SendIcon = ({ color = '#FFFFFF', size = 20 }) => (
+
+
+
+);
+
+/**
+ * 停止按钮图标
+ */
+const StopIcon = ({ color = '#FFFFFF', size = 16 }) => (
+
+);
+
+/**
+ * ChatInput 组件
+ */
+const ChatInput = ({
+ onSend,
+ onCancel,
+ isProcessing = false,
+ placeholder = '输入消息...',
+ disabled = false,
+}) => {
+ const [inputText, setInputText] = useState('');
+ const inputRef = useRef(null);
+
+ /**
+ * 处理发送
+ */
+ const handleSend = () => {
+ if (inputText.trim() && !isProcessing && !disabled) {
+ onSend(inputText);
+ setInputText('');
+ Keyboard.dismiss();
+ }
+ };
+
+ /**
+ * 处理取消
+ */
+ const handleCancel = () => {
+ if (isProcessing && onCancel) {
+ onCancel();
+ }
+ };
+
+ /**
+ * 处理按键提交
+ */
+ const handleSubmitEditing = () => {
+ handleSend();
+ };
+
+ const canSend = inputText.trim().length > 0 && !isProcessing && !disabled;
+
+ return (
+
+
+
+
+
+
+ {isProcessing ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ borderTopWidth: 1,
+ borderTopColor: AgentTheme.border,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ paddingBottom: Platform.OS === 'ios' ? 28 : 12,
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ },
+ inputContainer: {
+ flex: 1,
+ backgroundColor: AgentTheme.backgroundSecondary,
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ paddingHorizontal: 16,
+ paddingVertical: Platform.OS === 'ios' ? 10 : 6,
+ marginRight: 10,
+ minHeight: 40,
+ maxHeight: 120,
+ },
+ input: {
+ color: AgentTheme.textPrimary,
+ fontSize: 15,
+ lineHeight: 20,
+ maxHeight: 100,
+ },
+ sendButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ sendButtonActive: {
+ backgroundColor: AgentTheme.accent,
+ },
+ sendButtonDisabled: {
+ backgroundColor: AgentTheme.textMuted,
+ opacity: 0.5,
+ },
+ cancelButton: {
+ backgroundColor: AgentTheme.error,
+ },
+});
+
+export default memo(ChatInput);
diff --git a/MeAgent/src/screens/Agent/components/MarkdownRenderer.js b/MeAgent/src/screens/Agent/components/MarkdownRenderer.js
new file mode 100644
index 00000000..1a2a3d63
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/MarkdownRenderer.js
@@ -0,0 +1,975 @@
+/**
+ * MarkdownRenderer 组件
+ * 支持 Markdown 渲染和 ECharts 图表渲染
+ */
+
+import React, { memo, useMemo, useState, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Dimensions,
+ ActivityIndicator,
+} from 'react-native';
+import { WebView } from 'react-native-webview';
+import { AgentTheme } from '../../../constants/agentConstants';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const CHART_WIDTH = SCREEN_WIDTH - 48; // 减去边距
+const CHART_HEIGHT = 300;
+
+/**
+ * 简单的 Markdown 解析器
+ * 支持:标题、粗体、斜体、代码块、链接、列表、表格、Mermaid 图
+ */
+const parseMarkdown = (text) => {
+ if (!text) return [];
+
+ const parts = [];
+ let remaining = text;
+
+ // 匹配所有特殊代码块:echarts, mermaid
+ const codeBlockRegex = /```(echarts|mermaid)\s*\n?([\s\S]*?)```/g;
+ let lastIndex = 0;
+ let match;
+
+ while ((match = codeBlockRegex.exec(text)) !== null) {
+ // 添加代码块前的文本
+ if (match.index > lastIndex) {
+ const textBefore = text.substring(lastIndex, match.index);
+ parts.push({ type: 'markdown', content: textBefore });
+ }
+
+ const blockType = match[1]; // echarts 或 mermaid
+ let content = match[2].trim();
+
+ // 处理转义字符
+ if (content.includes('\\n')) {
+ content = content.replace(/\\n/g, '\n');
+ }
+
+ if (blockType === 'echarts') {
+ parts.push({ type: 'chart', content });
+ } else if (blockType === 'mermaid') {
+ parts.push({ type: 'mermaid', content });
+ }
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // 添加剩余文本
+ if (lastIndex < text.length) {
+ parts.push({ type: 'markdown', content: text.substring(lastIndex) });
+ }
+
+ // 如果没有匹配到任何内容,返回原始文本
+ if (parts.length === 0) {
+ parts.push({ type: 'markdown', content: text });
+ }
+
+ return parts;
+};
+
+/**
+ * 解析表格
+ */
+const parseTable = (lines, startIndex) => {
+ const rows = [];
+ let i = startIndex;
+
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
+ const line = lines[i].trim();
+ // 跳过分隔行 |---|---|
+ if (!/^\|[\s-:|]+\|$/.test(line)) {
+ const cells = line
+ .split('|')
+ .filter((cell, idx, arr) => idx > 0 && idx < arr.length - 1)
+ .map(cell => cell.trim());
+ if (cells.length > 0) {
+ rows.push(cells);
+ }
+ }
+ i++;
+ }
+
+ return { rows, endIndex: i };
+};
+
+/**
+ * 表格组件
+ */
+const TableView = memo(({ rows }) => {
+ if (!rows || rows.length === 0) return null;
+
+ const header = rows[0];
+ const body = rows.slice(1);
+
+ return (
+
+
+
+ {/* 表头 */}
+
+ {header.map((cell, idx) => (
+
+ {cell}
+
+ ))}
+
+ {/* 表体 */}
+ {body.map((row, rowIdx) => (
+
+ {row.map((cell, cellIdx) => (
+
+ {cell}
+
+ ))}
+
+ ))}
+
+
+
+ );
+});
+
+/**
+ * Markdown 文本渲染器(支持表格)
+ */
+const MarkdownText = memo(({ content }) => {
+ const lines = content.split('\n');
+ const elements = [];
+ let i = 0;
+
+ while (i < lines.length) {
+ const line = lines[i];
+
+ // 检测表格开始
+ if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
+ const { rows, endIndex } = parseTable(lines, i);
+ if (rows.length > 1) {
+ elements.push();
+ i = endIndex;
+ continue;
+ }
+ }
+
+ elements.push();
+ i++;
+ }
+
+ return (
+
+ {elements}
+
+ );
+});
+
+/**
+ * 单行 Markdown 渲染
+ */
+const MarkdownLine = memo(({ line }) => {
+ // 空行
+ if (!line.trim()) {
+ return ;
+ }
+
+ // 标题 (# ## ### ####)
+ const headerMatch = line.match(/^(#{1,4})\s+(.*)$/);
+ if (headerMatch) {
+ const level = headerMatch[1].length;
+ const text = headerMatch[2];
+ const headerStyles = [
+ styles.h1,
+ styles.h2,
+ styles.h3,
+ styles.h4,
+ ];
+ return (
+
+ {renderInlineStyles(text)}
+
+ );
+ }
+
+ // 无序列表 (- 或 *)
+ const listMatch = line.match(/^[\s]*[-*]\s+(.*)$/);
+ if (listMatch) {
+ return (
+
+ •
+ {renderInlineStyles(listMatch[1])}
+
+ );
+ }
+
+ // 有序列表 (1. 2. 3.)
+ const orderedListMatch = line.match(/^[\s]*(\d+)\.\s+(.*)$/);
+ if (orderedListMatch) {
+ return (
+
+ {orderedListMatch[1]}.
+ {renderInlineStyles(orderedListMatch[2])}
+
+ );
+ }
+
+ // 代码块 (```)
+ if (line.startsWith('```')) {
+ return null; // 代码块在外层处理
+ }
+
+ // 引用 (>)
+ const quoteMatch = line.match(/^>\s*(.*)$/);
+ if (quoteMatch) {
+ return (
+
+ {renderInlineStyles(quoteMatch[1])}
+
+ );
+ }
+
+ // 分隔线 (--- or ***)
+ if (/^[-*]{3,}$/.test(line.trim())) {
+ return ;
+ }
+
+ // 普通段落
+ return (
+ {renderInlineStyles(line)}
+ );
+});
+
+/**
+ * 渲染行内样式(粗体、斜体、代码、链接)
+ */
+const renderInlineStyles = (text) => {
+ if (!text) return null;
+
+ const elements = [];
+ let key = 0;
+
+ // 为简化实现,这里只做基本渲染
+ // 完整实现需要递归解析
+ elements.push(
+
+ {text
+ .replace(/\*\*(.+?)\*\*/g, (_, m) => m) // 移除粗体标记(简化)
+ .replace(/\*(.+?)\*/g, (_, m) => m) // 移除斜体标记
+ .replace(/`([^`]+)`/g, (_, m) => `[${m}]`) // 标记代码
+ }
+
+ );
+
+ return elements;
+};
+
+/**
+ * 生成 ECharts HTML
+ */
+const generateEChartsHTML = (option, width, height) => {
+ // 深色主题配色
+ const darkTheme = {
+ backgroundColor: 'transparent',
+ textStyle: {
+ color: '#E2E8F0'
+ },
+ title: {
+ textStyle: {
+ color: '#F8FAFC'
+ },
+ subtextStyle: {
+ color: '#94A3B8'
+ }
+ },
+ legend: {
+ textStyle: {
+ color: '#CBD5E1'
+ }
+ },
+ xAxis: {
+ axisLine: {
+ lineStyle: {
+ color: '#475569'
+ }
+ },
+ axisLabel: {
+ color: '#94A3B8'
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#334155'
+ }
+ }
+ },
+ yAxis: {
+ axisLine: {
+ lineStyle: {
+ color: '#475569'
+ }
+ },
+ axisLabel: {
+ color: '#94A3B8'
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#334155'
+ }
+ }
+ }
+ };
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
+
+/**
+ * ECharts 图表组件
+ * 使用 WebView 渲染真实的 ECharts 图表
+ */
+const EChartsView = memo(({ config }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [showData, setShowData] = useState(false);
+
+ // 解析图表配置
+ const { chartOption, chartTitle, chartData } = useMemo(() => {
+ try {
+ const parsed = JSON.parse(config);
+ return {
+ chartOption: config,
+ chartTitle: parsed?.title?.text || '图表',
+ chartData: parsed,
+ };
+ } catch (e) {
+ return {
+ chartOption: null,
+ chartTitle: '图表',
+ chartData: null,
+ error: e.message,
+ };
+ }
+ }, [config]);
+
+ const handleMessage = useCallback((event) => {
+ try {
+ const data = JSON.parse(event.nativeEvent.data);
+ if (data.type === 'loaded') {
+ setIsLoading(false);
+ } else if (data.type === 'error') {
+ setError(data.message);
+ setIsLoading(false);
+ }
+ } catch (e) {
+ // 忽略解析错误
+ }
+ }, []);
+
+ // 如果配置解析失败,显示错误
+ if (!chartOption) {
+ return (
+
+
+ 📊
+ 图表解析失败
+
+ 无法解析图表配置
+
+ );
+ }
+
+ const html = generateEChartsHTML(chartOption, CHART_WIDTH, CHART_HEIGHT);
+
+ return (
+
+
+ 📊
+ {chartTitle}
+ setShowData(!showData)}
+ style={styles.chartToggle}
+ >
+
+ {showData ? '显示图表' : '查看数据'}
+
+
+
+
+ {error ? (
+
+ 图表渲染失败: {error}
+
+ ) : showData ? (
+ // 显示数据视图
+
+
+ {chartData?.series?.map((series, idx) => (
+
+ {series.name || `系列 ${idx + 1}`}
+ {series.data?.slice(0, 10).map((item, i) => (
+
+ {typeof item === 'object' ? `${item.name}: ${item.value}` : item}
+
+ ))}
+ {series.data?.length > 10 && (
+ ...还有 {series.data.length - 10} 项
+ )}
+
+ ))}
+
+
+ ) : (
+ // 显示图表
+
+ {isLoading && (
+
+
+ 加载图表中...
+
+ )}
+
+
+ )}
+
+ );
+});
+
+/**
+ * 生成 Mermaid HTML
+ */
+const generateMermaidHTML = (code, width) => {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+};
+
+/**
+ * Mermaid 图表组件
+ */
+const MermaidView = memo(({ code }) => {
+ const [height, setHeight] = useState(200);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const handleMessage = useCallback((event) => {
+ try {
+ const data = JSON.parse(event.nativeEvent.data);
+ if (data.type === 'loaded') {
+ setIsLoading(false);
+ if (data.height) {
+ setHeight(Math.min(data.height, 400)); // 最大高度 400
+ }
+ }
+ } catch (e) {
+ // 忽略解析错误
+ }
+ }, []);
+
+ const html = generateMermaidHTML(code, CHART_WIDTH);
+
+ return (
+
+
+ 📊
+ 流程图
+
+
+ {isLoading && (
+
+
+ 加载中...
+
+ )}
+
+
+
+ );
+});
+
+/**
+ * MarkdownRenderer 主组件
+ */
+const MarkdownRenderer = ({ content }) => {
+ const parts = useMemo(() => parseMarkdown(content), [content]);
+
+ return (
+
+ {parts.map((part, index) => {
+ if (part.type === 'chart') {
+ return ;
+ }
+ if (part.type === 'mermaid') {
+ return ;
+ }
+ return ;
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ // 不使用 flex: 1,让内容自适应
+ },
+ markdownContainer: {
+ // 不使用 flex: 1,让内容自适应
+ },
+
+ // 文本样式
+ text: {
+ color: AgentTheme.textPrimary,
+ fontSize: 15,
+ lineHeight: 22,
+ },
+ paragraph: {
+ color: AgentTheme.textPrimary,
+ fontSize: 15,
+ lineHeight: 22,
+ marginBottom: 8,
+ },
+ emptyLine: {
+ height: 8,
+ },
+
+ // 标题
+ headerBase: {
+ color: AgentTheme.textPrimary,
+ fontWeight: '600',
+ marginTop: 16,
+ marginBottom: 8,
+ },
+ h1: {
+ fontSize: 22,
+ },
+ h2: {
+ fontSize: 20,
+ },
+ h3: {
+ fontSize: 18,
+ },
+ h4: {
+ fontSize: 16,
+ },
+
+ // 列表
+ listItem: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginBottom: 4,
+ },
+ listBullet: {
+ color: AgentTheme.accent,
+ fontSize: 14,
+ marginRight: 8,
+ marginTop: 2,
+ },
+ listNumber: {
+ color: AgentTheme.accent,
+ fontSize: 14,
+ marginRight: 8,
+ minWidth: 20,
+ },
+ listText: {
+ color: AgentTheme.textPrimary,
+ fontSize: 15,
+ lineHeight: 22,
+ flex: 1,
+ },
+
+ // 引用
+ quote: {
+ borderLeftWidth: 3,
+ borderLeftColor: AgentTheme.accent,
+ paddingLeft: 12,
+ marginVertical: 8,
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
+ paddingVertical: 8,
+ borderRadius: 4,
+ },
+ quoteText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 14,
+ fontStyle: 'italic',
+ },
+
+ // 行内样式
+ bold: {
+ fontWeight: '700',
+ },
+ italic: {
+ fontStyle: 'italic',
+ },
+ inlineCode: {
+ fontFamily: 'monospace',
+ backgroundColor: 'rgba(139, 92, 246, 0.2)',
+ paddingHorizontal: 4,
+ borderRadius: 3,
+ fontSize: 13,
+ color: AgentTheme.accent,
+ },
+
+ // 分隔线
+ hr: {
+ height: 1,
+ backgroundColor: AgentTheme.border,
+ marginVertical: 16,
+ },
+
+ // 图表容器
+ chartContainer: {
+ backgroundColor: 'rgba(99, 102, 241, 0.1)',
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: 'rgba(99, 102, 241, 0.3)',
+ padding: 12,
+ marginVertical: 12,
+ overflow: 'hidden',
+ },
+ chartHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ chartIcon: {
+ fontSize: 18,
+ marginRight: 8,
+ },
+ chartTitle: {
+ color: AgentTheme.accentSecondary,
+ fontSize: 15,
+ fontWeight: '600',
+ flex: 1,
+ },
+ chartToggle: {
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ backgroundColor: 'rgba(99, 102, 241, 0.2)',
+ borderRadius: 12,
+ },
+ chartToggleText: {
+ color: AgentTheme.accentSecondary,
+ fontSize: 12,
+ },
+
+ // WebView 图表
+ chartWebViewContainer: {
+ height: CHART_HEIGHT,
+ borderRadius: 8,
+ overflow: 'hidden',
+ backgroundColor: 'rgba(15, 23, 42, 0.5)',
+ },
+ chartWebView: {
+ flex: 1,
+ backgroundColor: 'transparent',
+ },
+ chartLoading: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 1,
+ },
+ chartLoadingText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 12,
+ marginTop: 8,
+ },
+
+ // 图表错误
+ chartErrorContainer: {
+ padding: 20,
+ alignItems: 'center',
+ },
+ chartError: {
+ color: '#F87171',
+ fontSize: 13,
+ textAlign: 'center',
+ },
+
+ // 数据展示
+ chartDataScroll: {
+ maxHeight: 200,
+ marginTop: 8,
+ },
+ chartDataContainer: {
+ flexDirection: 'row',
+ },
+ seriesContainer: {
+ marginRight: 16,
+ minWidth: 120,
+ },
+ seriesName: {
+ color: AgentTheme.textPrimary,
+ fontSize: 13,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ dataItem: {
+ color: AgentTheme.textSecondary,
+ fontSize: 12,
+ marginVertical: 1,
+ },
+ dataMore: {
+ color: AgentTheme.textMuted,
+ fontSize: 11,
+ marginTop: 4,
+ },
+
+ // 表格样式
+ tableContainer: {
+ marginVertical: 12,
+ borderRadius: 8,
+ overflow: 'hidden',
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ },
+ tableHeader: {
+ flexDirection: 'row',
+ backgroundColor: 'rgba(99, 102, 241, 0.15)',
+ },
+ tableHeaderCell: {
+ backgroundColor: 'rgba(99, 102, 241, 0.15)',
+ },
+ tableHeaderText: {
+ color: AgentTheme.textPrimary,
+ fontSize: 13,
+ fontWeight: '600',
+ },
+ tableRow: {
+ flexDirection: 'row',
+ borderTopWidth: 1,
+ borderTopColor: AgentTheme.border,
+ },
+ tableRowEven: {
+ backgroundColor: 'rgba(30, 41, 59, 0.5)',
+ },
+ tableCell: {
+ minWidth: 100,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ },
+ tableCellText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 13,
+ lineHeight: 18,
+ },
+
+ // Mermaid 样式
+ mermaidContainer: {
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: 'rgba(139, 92, 246, 0.3)',
+ padding: 12,
+ marginVertical: 12,
+ overflow: 'hidden',
+ },
+ mermaidHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+ mermaidIcon: {
+ fontSize: 18,
+ marginRight: 8,
+ },
+ mermaidTitle: {
+ color: AgentTheme.accent,
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ mermaidWebViewContainer: {
+ borderRadius: 8,
+ overflow: 'hidden',
+ backgroundColor: 'rgba(15, 23, 42, 0.5)',
+ },
+ mermaidWebView: {
+ flex: 1,
+ backgroundColor: 'transparent',
+ },
+});
+
+export default memo(MarkdownRenderer);
diff --git a/MeAgent/src/screens/Agent/components/MessageBubble.js b/MeAgent/src/screens/Agent/components/MessageBubble.js
new file mode 100644
index 00000000..b1f5543b
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/MessageBubble.js
@@ -0,0 +1,743 @@
+/**
+ * MessageBubble 组件
+ * 消息气泡,支持不同消息类型的渲染
+ * 支持 Markdown 渲染和图表展示
+ */
+
+import React, { memo, useState, useEffect, useRef } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ActivityIndicator,
+ Animated,
+ LayoutAnimation,
+ Platform,
+ UIManager,
+ ScrollView,
+} from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { MessageTypes, AgentTheme } from '../../../constants/agentConstants';
+import MarkdownRenderer from './MarkdownRenderer';
+
+// 启用 LayoutAnimation (Android)
+if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
+ UIManager.setLayoutAnimationEnabledExperimental(true);
+}
+
+/**
+ * 用户消息气泡
+ */
+const UserBubble = memo(({ content }) => (
+
+
+ {content}
+
+
+));
+
+/**
+ * 思考中气泡 - 带动画效果
+ */
+const ThinkingBubble = memo(({ content }) => {
+ const pulseAnim = useRef(new Animated.Value(1)).current;
+
+ useEffect(() => {
+ const pulse = Animated.loop(
+ Animated.sequence([
+ Animated.timing(pulseAnim, {
+ toValue: 0.6,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ Animated.timing(pulseAnim, {
+ toValue: 1,
+ duration: 800,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+ pulse.start();
+ return () => pulse.stop();
+ }, []);
+
+ return (
+
+
+
+
+ {content || '正在思考...'}
+
+
+
+
+
+
+ );
+});
+
+/**
+ * 思考中动态点
+ */
+const ThinkingDots = memo(() => {
+ const [dots, setDots] = useState('');
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setDots(prev => prev.length >= 3 ? '' : prev + '.');
+ }, 500);
+ return () => clearInterval(interval);
+ }, []);
+
+ return {dots};
+});
+
+/**
+ * 深度思考气泡(可折叠)- 类似 Gemini 风格
+ */
+const DeepThinkingBubble = memo(({ content, isStreaming }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const charCount = content?.length || 0;
+ const rotateAnim = useRef(new Animated.Value(0)).current;
+ const sparkleAnim = useRef(new Animated.Value(0)).current;
+
+ // 折叠动画
+ useEffect(() => {
+ Animated.timing(rotateAnim, {
+ toValue: isExpanded ? 1 : 0,
+ duration: 200,
+ useNativeDriver: true,
+ }).start();
+ }, [isExpanded]);
+
+ // 思考中闪烁动画
+ useEffect(() => {
+ if (isStreaming) {
+ const sparkle = Animated.loop(
+ Animated.sequence([
+ Animated.timing(sparkleAnim, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ Animated.timing(sparkleAnim, {
+ toValue: 0,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+ sparkle.start();
+ return () => sparkle.stop();
+ }
+ }, [isStreaming]);
+
+ const handleToggle = () => {
+ if (!isStreaming) {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+ setIsExpanded(!isExpanded);
+ }
+ };
+
+ const rotation = rotateAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0deg', '180deg'],
+ });
+
+ return (
+
+
+ {/* 标题栏 */}
+
+
+ {isStreaming ? (
+
+ ✨
+
+ ) : (
+ 🧠
+ )}
+
+ {isStreaming ? '深度思考中...' : '深度思考'}
+
+ {isStreaming && (
+
+ )}
+
+
+
+ {charCount > 0 && (
+
+ {charCount} 字
+
+ )}
+ {!isStreaming && (
+
+ ▼
+
+ )}
+
+
+
+ {/* 思考内容 */}
+ {(isExpanded || isStreaming) && content && (
+
+ {content}
+ {isStreaming && (
+
+
+
+ )}
+
+ )}
+
+
+ );
+});
+
+/**
+ * 执行计划气泡
+ */
+const PlanBubble = memo(({ content, plan }) => (
+
+
+
+ 📋
+ 执行计划
+
+ {plan?.goal || content}
+ {plan?.steps && (
+
+ {plan.steps.map((step, index) => (
+
+
+ {index + 1}
+
+
+ {step.tool}
+ {step.reason && (
+ {step.reason}
+ )}
+
+
+ ))}
+
+ )}
+
+
+));
+
+/**
+ * 执行中气泡 - 带进度显示
+ */
+const ExecutingBubble = memo(({ content, stepResults = [], currentStep }) => {
+ const spinAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const spin = Animated.loop(
+ Animated.timing(spinAnim, {
+ toValue: 1,
+ duration: 1500,
+ useNativeDriver: true,
+ })
+ );
+ spin.start();
+ return () => spin.stop();
+ }, []);
+
+ const rotation = spinAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0deg', '360deg'],
+ });
+
+ return (
+
+
+
+
+ ⚙️
+
+ {content || '正在执行...'}
+
+
+ {stepResults.length > 0 && (
+
+ {stepResults.map((result, index) => (
+
+
+ {result.status === 'success' ? '✓' : '✗'}
+
+ {result.tool}
+ {result.execution_time && (
+
+ {(result.execution_time * 1000).toFixed(0)}ms
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+});
+
+/**
+ * AI 响应气泡 - 支持 Markdown 和图表
+ */
+const ResponseBubble = memo(({ content, isStreaming }) => {
+ const cursorAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ if (isStreaming) {
+ const blink = Animated.loop(
+ Animated.sequence([
+ Animated.timing(cursorAnim, {
+ toValue: 1,
+ duration: 500,
+ useNativeDriver: true,
+ }),
+ Animated.timing(cursorAnim, {
+ toValue: 0,
+ duration: 500,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+ blink.start();
+ return () => blink.stop();
+ }
+ }, [isStreaming]);
+
+ return (
+
+
+
+ {isStreaming && (
+
+
+
+ )}
+
+
+ );
+});
+
+/**
+ * 错误气泡
+ */
+const ErrorBubble = memo(({ content }) => (
+
+
+ ⚠️
+
+ 出错了
+ {content}
+
+
+
+));
+
+/**
+ * MessageBubble 主组件
+ */
+const MessageBubble = ({ message }) => {
+ const { type, content, isStreaming, plan, stepResults, currentStep, thinkingContent } = message;
+
+ switch (type) {
+ case MessageTypes.USER:
+ return ;
+
+ case MessageTypes.AGENT_THINKING:
+ return ;
+
+ case MessageTypes.AGENT_DEEP_THINKING:
+ return ;
+
+ case MessageTypes.AGENT_PLAN:
+ return ;
+
+ case MessageTypes.AGENT_EXECUTING:
+ return ;
+
+ case MessageTypes.AGENT_RESPONSE:
+ return ;
+
+ case MessageTypes.ERROR:
+ return ;
+
+ default:
+ return ;
+ }
+};
+
+const styles = StyleSheet.create({
+ // 用户消息
+ userBubbleContainer: {
+ width: '100%',
+ alignItems: 'flex-end',
+ marginVertical: 6,
+ paddingHorizontal: 16,
+ },
+ userBubble: {
+ maxWidth: '80%',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderRadius: 18,
+ borderBottomRightRadius: 4,
+ },
+ userText: {
+ color: '#FFFFFF',
+ fontSize: 15,
+ lineHeight: 22,
+ },
+
+ // AI 消息容器
+ agentBubbleContainer: {
+ width: '100%',
+ alignItems: 'flex-start',
+ marginVertical: 6,
+ paddingHorizontal: 16,
+ },
+
+ // 思考中
+ thinkingBubble: {
+ maxWidth: '70%',
+ backgroundColor: AgentTheme.cardBg,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ },
+ thinkingRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ thinkingText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 14,
+ marginLeft: 10,
+ },
+ thinkingDots: {
+ marginTop: 4,
+ marginLeft: 34,
+ },
+ dots: {
+ color: AgentTheme.accent,
+ fontSize: 18,
+ letterSpacing: 2,
+ },
+
+ // 深度思考
+ deepThinkingBubble: {
+ maxWidth: '92%',
+ minWidth: '60%',
+ backgroundColor: 'rgba(139, 92, 246, 0.08)',
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: 'rgba(139, 92, 246, 0.25)',
+ overflow: 'hidden',
+ },
+ deepThinkingBubbleActive: {
+ borderColor: 'rgba(139, 92, 246, 0.5)',
+ shadowColor: '#8B5CF6',
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.3,
+ shadowRadius: 12,
+ elevation: 8,
+ },
+ deepThinkingHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ backgroundColor: 'rgba(139, 92, 246, 0.12)',
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(139, 92, 246, 0.15)',
+ },
+ deepThinkingTitleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ sparkleIcon: {
+ fontSize: 16,
+ marginRight: 8,
+ },
+ brainIcon: {
+ fontSize: 16,
+ marginRight: 8,
+ },
+ deepThinkingTitle: {
+ color: AgentTheme.accent,
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ deepThinkingMeta: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ charCountBadge: {
+ backgroundColor: 'rgba(139, 92, 246, 0.2)',
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 10,
+ marginRight: 8,
+ },
+ charCount: {
+ color: AgentTheme.accent,
+ fontSize: 11,
+ fontWeight: '500',
+ },
+ expandIcon: {
+ color: AgentTheme.textMuted,
+ fontSize: 10,
+ },
+ deepThinkingContent: {
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ maxHeight: 250,
+ },
+ deepThinkingContentStreaming: {
+ maxHeight: 300,
+ },
+ deepThinkingText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 13,
+ lineHeight: 20,
+ fontStyle: 'italic',
+ },
+ cursorContainer: {
+ marginTop: 4,
+ },
+ cursor: {
+ width: 2,
+ height: 16,
+ backgroundColor: AgentTheme.accent,
+ borderRadius: 1,
+ },
+
+ // 执行计划
+ planBubble: {
+ maxWidth: '92%',
+ backgroundColor: 'rgba(16, 185, 129, 0.08)',
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: 'rgba(16, 185, 129, 0.25)',
+ padding: 14,
+ },
+ planHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ planIcon: {
+ fontSize: 18,
+ marginRight: 8,
+ },
+ planTitle: {
+ color: AgentTheme.success,
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ planGoal: {
+ color: AgentTheme.textPrimary,
+ fontSize: 14,
+ marginBottom: 12,
+ lineHeight: 20,
+ },
+ planSteps: {
+ marginTop: 4,
+ },
+ planStep: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ marginVertical: 6,
+ },
+ stepNumber: {
+ width: 22,
+ height: 22,
+ borderRadius: 11,
+ backgroundColor: 'rgba(16, 185, 129, 0.2)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 10,
+ },
+ stepNumberText: {
+ color: AgentTheme.success,
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ stepContent: {
+ flex: 1,
+ },
+ stepTool: {
+ color: AgentTheme.textPrimary,
+ fontSize: 13,
+ fontWeight: '500',
+ },
+ stepReason: {
+ color: AgentTheme.textMuted,
+ fontSize: 12,
+ marginTop: 2,
+ },
+
+ // 执行中
+ executingBubble: {
+ maxWidth: '92%',
+ backgroundColor: 'rgba(99, 102, 241, 0.08)',
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: 'rgba(99, 102, 241, 0.25)',
+ padding: 14,
+ },
+ executingHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ gearIcon: {
+ fontSize: 18,
+ marginRight: 10,
+ },
+ executingTitle: {
+ color: AgentTheme.accentSecondary,
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ stepResults: {
+ marginTop: 12,
+ borderTopWidth: 1,
+ borderTopColor: 'rgba(99, 102, 241, 0.15)',
+ paddingTop: 10,
+ },
+ stepResult: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginVertical: 4,
+ },
+ stepStatus: {
+ fontSize: 14,
+ marginRight: 8,
+ width: 18,
+ },
+ stepSuccess: {
+ color: AgentTheme.success,
+ },
+ stepFailed: {
+ color: AgentTheme.error,
+ },
+ stepName: {
+ color: AgentTheme.textSecondary,
+ fontSize: 13,
+ flex: 1,
+ },
+ stepTime: {
+ color: AgentTheme.textMuted,
+ fontSize: 11,
+ },
+
+ // AI 响应
+ responseBubble: {
+ maxWidth: '92%',
+ minWidth: '50%',
+ backgroundColor: AgentTheme.cardBg,
+ paddingHorizontal: 16,
+ paddingVertical: 14,
+ borderRadius: 18,
+ borderBottomLeftRadius: 4,
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ },
+ streamingIndicator: {
+ marginTop: 8,
+ },
+ streamingCursor: {
+ width: 2,
+ height: 16,
+ backgroundColor: AgentTheme.accent,
+ borderRadius: 1,
+ },
+
+ // 错误
+ errorBubble: {
+ maxWidth: '85%',
+ backgroundColor: 'rgba(239, 68, 68, 0.08)',
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: 'rgba(239, 68, 68, 0.25)',
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ },
+ errorIcon: {
+ fontSize: 18,
+ marginRight: 10,
+ marginTop: 2,
+ },
+ errorContent: {
+ flex: 1,
+ },
+ errorTitle: {
+ color: AgentTheme.error,
+ fontSize: 14,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ errorText: {
+ color: AgentTheme.textSecondary,
+ fontSize: 13,
+ lineHeight: 18,
+ },
+});
+
+export default memo(MessageBubble);
diff --git a/MeAgent/src/screens/Agent/components/SessionDrawer.js b/MeAgent/src/screens/Agent/components/SessionDrawer.js
new file mode 100644
index 00000000..c3050a7c
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/SessionDrawer.js
@@ -0,0 +1,402 @@
+/**
+ * SessionDrawer 组件
+ * 会话历史抽屉,支持会话列表、搜索、新建
+ */
+
+import React, { memo, useState, useCallback } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ TextInput,
+ FlatList,
+ Modal,
+ Dimensions,
+ Animated,
+} from 'react-native';
+import { BlurView } from 'expo-blur';
+import { AgentTheme } from '../../../constants/agentConstants';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const DRAWER_WIDTH = SCREEN_WIDTH * 0.85;
+
+/**
+ * 会话卡片
+ */
+const SessionCard = memo(({ session, isActive, onPress }) => {
+ const title = session.title || '新对话';
+ const messageCount = session.message_count || 0;
+
+ return (
+ onPress(session.session_id)}
+ activeOpacity={0.7}
+ >
+
+ 💬
+
+
+
+ {title}
+
+
+ {messageCount} 条消息
+
+
+
+ );
+});
+
+/**
+ * 日期分组标题
+ */
+const DateGroupHeader = memo(({ title }) => (
+
+ {title}
+
+));
+
+/**
+ * SessionDrawer 组件
+ */
+const SessionDrawer = ({
+ visible,
+ onClose,
+ groupedSessions = [],
+ currentSessionId,
+ onSelectSession,
+ onNewSession,
+ isLoading,
+}) => {
+ const [searchText, setSearchText] = useState('');
+
+ /**
+ * 过滤会话
+ */
+ const filteredGroups = useCallback(() => {
+ if (!searchText.trim()) {
+ return groupedSessions;
+ }
+
+ const lowerKeyword = searchText.toLowerCase();
+
+ return groupedSessions
+ .map(group => ({
+ ...group,
+ sessions: group.sessions.filter(session =>
+ (session.title || '').toLowerCase().includes(lowerKeyword) ||
+ (session.session_id || '').toLowerCase().includes(lowerKeyword)
+ ),
+ }))
+ .filter(group => group.sessions.length > 0);
+ }, [groupedSessions, searchText]);
+
+ /**
+ * 渲染会话项
+ */
+ const renderSessionItem = ({ item: session }) => (
+
+ );
+
+ /**
+ * 渲染分组
+ */
+ const renderGroup = ({ item: group }) => (
+
+
+ {group.sessions.map(session => (
+
+ ))}
+
+ );
+
+ const groups = filteredGroups();
+
+ return (
+
+
+ {/* 点击遮罩关闭 */}
+
+
+ {/* 抽屉内容 */}
+
+
+ {/* 头部 */}
+
+ 对话历史
+
+ ✕
+
+
+
+ {/* 新建按钮 */}
+
+ +
+ 新建对话
+
+
+ {/* 搜索框 */}
+
+ 🔍
+
+ {searchText.length > 0 && (
+ setSearchText('')}
+ style={styles.clearButton}
+ >
+ ✕
+
+ )}
+
+
+ {/* 会话列表 */}
+ {isLoading ? (
+
+ 加载中...
+
+ ) : groups.length === 0 ? (
+
+ 📭
+
+ {searchText ? '没有找到匹配的对话' : '暂无对话历史'}
+
+
+ ) : (
+ item.title}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContent}
+ />
+ )}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ backdrop: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ drawer: {
+ width: DRAWER_WIDTH,
+ backgroundColor: AgentTheme.background,
+ },
+ drawerContent: {
+ flex: 1,
+ },
+
+ // 头部
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingTop: 60,
+ paddingBottom: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: AgentTheme.border,
+ },
+ headerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: AgentTheme.textPrimary,
+ },
+ closeButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: AgentTheme.cardBg,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ closeButtonText: {
+ fontSize: 16,
+ color: AgentTheme.textSecondary,
+ },
+
+ // 新建按钮
+ newButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginHorizontal: 20,
+ marginTop: 16,
+ paddingVertical: 12,
+ backgroundColor: AgentTheme.accent,
+ borderRadius: 12,
+ },
+ newButtonIcon: {
+ fontSize: 20,
+ color: '#FFFFFF',
+ marginRight: 8,
+ },
+ newButtonText: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: '#FFFFFF',
+ },
+
+ // 搜索框
+ searchContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginHorizontal: 20,
+ marginTop: 16,
+ marginBottom: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ backgroundColor: AgentTheme.cardBg,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ },
+ searchIcon: {
+ fontSize: 14,
+ marginRight: 8,
+ },
+ searchInput: {
+ flex: 1,
+ fontSize: 14,
+ color: AgentTheme.textPrimary,
+ },
+ clearButton: {
+ padding: 4,
+ },
+ clearButtonText: {
+ fontSize: 12,
+ color: AgentTheme.textMuted,
+ },
+
+ // 列表
+ listContent: {
+ paddingHorizontal: 20,
+ paddingBottom: 40,
+ },
+ groupContainer: {
+ marginTop: 16,
+ },
+
+ // 日期分组
+ dateGroup: {
+ paddingVertical: 8,
+ },
+ dateGroupText: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: AgentTheme.textMuted,
+ textTransform: 'uppercase',
+ },
+
+ // 会话卡片
+ sessionCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ backgroundColor: AgentTheme.cardBg,
+ borderRadius: 10,
+ marginVertical: 4,
+ borderWidth: 1,
+ borderColor: 'transparent',
+ },
+ sessionCardActive: {
+ borderColor: AgentTheme.accent,
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
+ },
+ sessionIcon: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: AgentTheme.backgroundSecondary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ sessionIconText: {
+ fontSize: 18,
+ },
+ sessionInfo: {
+ flex: 1,
+ },
+ sessionTitle: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: AgentTheme.textPrimary,
+ marginBottom: 2,
+ },
+ sessionMeta: {
+ fontSize: 12,
+ color: AgentTheme.textMuted,
+ },
+
+ // 加载和空状态
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: 14,
+ color: AgentTheme.textMuted,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 40,
+ },
+ emptyIcon: {
+ fontSize: 48,
+ marginBottom: 16,
+ },
+ emptyText: {
+ fontSize: 14,
+ color: AgentTheme.textMuted,
+ textAlign: 'center',
+ },
+});
+
+export default memo(SessionDrawer);
diff --git a/MeAgent/src/screens/Agent/components/WelcomeScreen.js b/MeAgent/src/screens/Agent/components/WelcomeScreen.js
new file mode 100644
index 00000000..c2f13105
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/WelcomeScreen.js
@@ -0,0 +1,335 @@
+/**
+ * WelcomeScreen 组件
+ * Agent 聊天欢迎界面 - Bento Card 风格
+ */
+
+import React, { memo } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ ScrollView,
+ Image,
+ Dimensions,
+} from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { AgentTheme, QUICK_QUESTIONS, AGENT_NAME } from '../../../constants/agentConstants';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const CARD_WIDTH = (SCREEN_WIDTH - 48 - 12) / 2; // 两列布局
+
+// 洛希头像
+const LuoxiAvatar = require('../../../../assets/imgs/luoxi.jpg');
+
+/**
+ * AI 头像组件
+ */
+const AIAvatar = () => (
+
+
+
+
+);
+
+/**
+ * Bento 功能卡片
+ */
+const BentoCard = memo(({ icon, title, description, color, size = 'small' }) => (
+
+ {icon}
+ {title}
+ {description}
+
+));
+
+/**
+ * 快捷问题卡片
+ */
+const QuickQuestionCard = memo(({ question, onPress, index }) => (
+ onPress(question)}
+ activeOpacity={0.7}
+ >
+
+
+ {question}
+
+ →
+
+
+
+
+));
+
+/**
+ * WelcomeScreen 组件
+ */
+const WelcomeScreen = ({ onQuickQuestion }) => {
+ return (
+
+ {/* 头部 - 洛希介绍 */}
+
+
+ {AGENT_NAME}
+ AI 投研助手 · 洛希极限
+ 在市场的混沌中,找到价值与风险的平衡点
+
+
+ {/* Bento Grid 功能展示 */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* 快捷问题 */}
+
+
+ 快速开始
+ 试试这些问题
+
+
+ {QUICK_QUESTIONS.map((question, index) => (
+
+ ))}
+
+
+
+ {/* 底部提示 */}
+
+
+ 💡
+
+ 直接输入股票代码或名称,我会为你进行全面分析
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ contentContainer: {
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ paddingBottom: 30,
+ },
+
+ // 头部
+ header: {
+ alignItems: 'center',
+ marginBottom: 28,
+ },
+ avatarContainer: {
+ width: 88,
+ height: 88,
+ marginBottom: 16,
+ position: 'relative',
+ },
+ avatarGlow: {
+ position: 'absolute',
+ width: 96,
+ height: 96,
+ borderRadius: 48,
+ top: -4,
+ left: -4,
+ },
+ avatar: {
+ width: 88,
+ height: 88,
+ borderRadius: 44,
+ borderWidth: 3,
+ borderColor: 'rgba(139, 92, 246, 0.5)',
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: '800',
+ color: AgentTheme.textPrimary,
+ marginBottom: 4,
+ letterSpacing: 1,
+ },
+ subtitle: {
+ fontSize: 15,
+ color: AgentTheme.accent,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ tagline: {
+ fontSize: 13,
+ color: AgentTheme.textMuted,
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+
+ // Bento Grid
+ bentoGrid: {
+ marginBottom: 28,
+ },
+ bentoRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 12,
+ },
+ bentoCard: {
+ width: CARD_WIDTH,
+ backgroundColor: AgentTheme.cardBg,
+ borderRadius: 16,
+ padding: 16,
+ borderWidth: 1,
+ borderColor: AgentTheme.border,
+ borderLeftWidth: 3,
+ },
+ bentoCardLarge: {
+ width: '100%',
+ },
+ bentoIcon: {
+ fontSize: 24,
+ marginBottom: 8,
+ },
+ bentoTitle: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: AgentTheme.textPrimary,
+ marginBottom: 4,
+ },
+ bentoDesc: {
+ fontSize: 12,
+ color: AgentTheme.textMuted,
+ },
+
+ // 快捷问题区域
+ quickSection: {
+ marginBottom: 20,
+ },
+ sectionHeader: {
+ marginBottom: 14,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '700',
+ color: AgentTheme.textPrimary,
+ marginBottom: 2,
+ },
+ sectionSubtitle: {
+ fontSize: 13,
+ color: AgentTheme.textMuted,
+ },
+ questionList: {
+ gap: 10,
+ },
+ questionCard: {
+ borderRadius: 14,
+ overflow: 'hidden',
+ marginBottom: 8,
+ },
+ questionGradient: {
+ borderRadius: 14,
+ borderWidth: 1,
+ borderColor: 'rgba(139, 92, 246, 0.2)',
+ },
+ questionContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 14,
+ },
+ questionText: {
+ fontSize: 14,
+ color: AgentTheme.textPrimary,
+ flex: 1,
+ },
+ questionArrowContainer: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: 'rgba(139, 92, 246, 0.2)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginLeft: 12,
+ },
+ questionArrow: {
+ fontSize: 14,
+ color: AgentTheme.accent,
+ fontWeight: '600',
+ },
+
+ // 底部提示
+ footer: {
+ marginTop: 8,
+ },
+ tipCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
+ borderRadius: 12,
+ padding: 14,
+ borderWidth: 1,
+ borderColor: 'rgba(245, 158, 11, 0.2)',
+ },
+ tipIcon: {
+ fontSize: 18,
+ marginRight: 10,
+ },
+ tipText: {
+ fontSize: 13,
+ color: AgentTheme.textSecondary,
+ flex: 1,
+ lineHeight: 18,
+ },
+});
+
+export default memo(WelcomeScreen);
diff --git a/MeAgent/src/screens/Agent/components/index.js b/MeAgent/src/screens/Agent/components/index.js
new file mode 100644
index 00000000..d31f7dc8
--- /dev/null
+++ b/MeAgent/src/screens/Agent/components/index.js
@@ -0,0 +1,9 @@
+/**
+ * Agent 组件导出
+ */
+
+export { default as MessageBubble } from './MessageBubble';
+export { default as ChatInput } from './ChatInput';
+export { default as WelcomeScreen } from './WelcomeScreen';
+export { default as SessionDrawer } from './SessionDrawer';
+export { default as MarkdownRenderer } from './MarkdownRenderer';
diff --git a/MeAgent/src/screens/Agent/index.js b/MeAgent/src/screens/Agent/index.js
new file mode 100644
index 00000000..14f635c3
--- /dev/null
+++ b/MeAgent/src/screens/Agent/index.js
@@ -0,0 +1,5 @@
+/**
+ * Agent 模块导出
+ */
+
+export { default as AgentChatScreen } from './AgentChatScreen';
diff --git a/MeAgent/src/services/agentService.js b/MeAgent/src/services/agentService.js
new file mode 100644
index 00000000..b6ae2db3
--- /dev/null
+++ b/MeAgent/src/services/agentService.js
@@ -0,0 +1,439 @@
+/**
+ * Agent 服务层
+ * 处理与 MCP Server 的通信,包括 SSE 流式聊天、会话管理等
+ */
+
+import { AGENT_API_CONFIG, SSEEventTypes } from '../constants/agentConstants';
+
+const { BASE_URL, ENDPOINTS, DEFAULT_TIMEOUT, MAX_HISTORY_LIMIT, MAX_SESSIONS_LIMIT } = AGENT_API_CONFIG;
+
+/**
+ * SSE 流式聊天请求 (React Native 兼容版本)
+ * 使用 XMLHttpRequest 处理流式响应,因为 React Native 的 fetch 不支持 response.body.getReader()
+ *
+ * @param {Object} params - 请求参数
+ * @param {string} params.message - 用户消息
+ * @param {Array} params.conversationHistory - 对话历史
+ * @param {string} params.userId - 用户ID
+ * @param {string} params.userNickname - 用户昵称
+ * @param {string} params.userAvatar - 用户头像
+ * @param {string} params.subscriptionType - 订阅类型
+ * @param {string|null} params.sessionId - 会话ID
+ * @param {string} params.model - 模型名称
+ * @param {Array} params.tools - 工具列表
+ * @param {Function} onEvent - SSE 事件回调
+ * @param {AbortSignal} signal - 取消信号
+ * @returns {Promise}
+ */
+export const streamAgentChat = async (params, onEvent, signal) => {
+ const {
+ message,
+ conversationHistory = [],
+ userId,
+ userNickname,
+ userAvatar,
+ subscriptionType = 'free',
+ sessionId = null,
+ model = 'deepmoney',
+ tools = [],
+ } = params;
+
+ const requestBody = {
+ message,
+ conversation_history: conversationHistory.map(msg => ({
+ isUser: msg.isUser,
+ content: msg.content,
+ })),
+ user_id: String(userId), // 确保 user_id 是字符串类型
+ user_nickname: userNickname || '',
+ user_avatar: userAvatar || '',
+ subscription_type: subscriptionType,
+ session_id: sessionId,
+ model,
+ tools,
+ };
+
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ let buffer = '';
+ let lastProcessedIndex = 0;
+
+ // 处理取消信号
+ if (signal) {
+ signal.addEventListener('abort', () => {
+ xhr.abort();
+ });
+ }
+
+ xhr.open('POST', `${BASE_URL}${ENDPOINTS.CHAT_STREAM}`, true);
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ xhr.setRequestHeader('Accept', 'text/event-stream');
+ xhr.withCredentials = true; // 携带 Cookie
+
+ xhr.onreadystatechange = () => {
+ // readyState 3 = LOADING (接收到部分数据)
+ if (xhr.readyState === 3 || xhr.readyState === 4) {
+ // 获取新增的响应文本
+ const newData = xhr.responseText.substring(lastProcessedIndex);
+ lastProcessedIndex = xhr.responseText.length;
+
+ if (newData) {
+ buffer += newData;
+
+ // 按双换行符分割事件
+ const events = buffer.split('\n\n');
+ // 最后一个可能不完整,保留在 buffer 中
+ buffer = events.pop() || '';
+
+ // 处理完整的事件
+ for (const eventStr of events) {
+ if (eventStr.trim()) {
+ processSSEEvent(eventStr, onEvent);
+ }
+ }
+ }
+ }
+
+ // readyState 4 = DONE (请求完成)
+ if (xhr.readyState === 4) {
+ // 处理剩余的 buffer
+ if (buffer.trim()) {
+ processSSEBuffer(buffer, onEvent);
+ }
+
+ if (xhr.status >= 200 && xhr.status < 300) {
+ onEvent({ type: 'stream_end' });
+ resolve();
+ } else if (xhr.status === 0) {
+ // 请求被取消
+ console.log('[AgentService] 请求被取消');
+ onEvent({ type: 'aborted' });
+ reject(new DOMException('Aborted', 'AbortError'));
+ } else {
+ const errorMessage = `HTTP ${xhr.status}: ${xhr.responseText}`;
+ console.error('[AgentService] SSE 错误:', errorMessage);
+ onEvent({
+ type: SSEEventTypes.ERROR,
+ data: { message: xhr.responseText || '网络请求失败' }
+ });
+ reject(new Error(errorMessage));
+ }
+ }
+ };
+
+ xhr.onerror = () => {
+ console.error('[AgentService] 网络错误');
+ onEvent({
+ type: SSEEventTypes.ERROR,
+ data: { message: '网络连接失败,请检查网络设置' }
+ });
+ reject(new Error('网络连接失败'));
+ };
+
+ xhr.ontimeout = () => {
+ console.error('[AgentService] 请求超时');
+ onEvent({
+ type: SSEEventTypes.ERROR,
+ data: { message: '请求超时,请稍后重试' }
+ });
+ reject(new Error('请求超时'));
+ };
+
+ xhr.timeout = DEFAULT_TIMEOUT;
+ xhr.send(JSON.stringify(requestBody));
+ });
+};
+
+/**
+ * 处理 SSE 缓冲区
+ */
+const processSSEBuffer = (buffer, onEvent) => {
+ const lines = buffer.split('\n');
+ let eventType = null;
+ let eventData = null;
+
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ eventType = line.slice(6).trim();
+ } else if (line.startsWith('data:')) {
+ const dataStr = line.slice(5).trim();
+ if (dataStr === '[DONE]') {
+ onEvent({ type: SSEEventTypes.DONE });
+ return;
+ }
+ try {
+ eventData = JSON.parse(dataStr);
+ } catch (e) {
+ eventData = { raw: dataStr };
+ }
+ }
+ }
+
+ if (eventData) {
+ onEvent({ type: eventType || 'message', data: eventData });
+ }
+};
+
+/**
+ * 处理单个 SSE 事件
+ */
+const processSSEEvent = (eventStr, onEvent) => {
+ const lines = eventStr.split('\n');
+ let eventType = null;
+ let eventData = null;
+
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ eventType = line.slice(6).trim();
+ } else if (line.startsWith('data:')) {
+ const dataStr = line.slice(5).trim();
+
+ // 检查是否是结束标记
+ if (dataStr === '[DONE]') {
+ onEvent({ type: SSEEventTypes.DONE });
+ return;
+ }
+
+ try {
+ eventData = JSON.parse(dataStr);
+ } catch (e) {
+ // 如果不是 JSON,保留原始字符串
+ eventData = { raw: dataStr };
+ }
+ }
+ }
+
+ if (eventData) {
+ onEvent({ type: eventType || 'message', data: eventData });
+ }
+};
+
+/**
+ * 创建带超时的 fetch 请求
+ * @param {string} url - 请求 URL
+ * @param {Object} options - fetch 选项
+ * @param {number} timeout - 超时时间(毫秒)
+ * @returns {Promise}
+ */
+const fetchWithTimeout = async (url, options = {}, timeout = DEFAULT_TIMEOUT) => {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ signal: controller.signal,
+ // 重要:携带 Cookie 以支持 Session 认证
+ credentials: 'include',
+ });
+ clearTimeout(timeoutId);
+ return response;
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error.name === 'AbortError') {
+ throw new Error('请求超时,请稍后重试');
+ }
+ throw error;
+ }
+};
+
+/**
+ * 获取用户会话列表
+ *
+ * @param {string} userId - 用户ID
+ * @param {number} limit - 返回数量限制
+ * @returns {Promise} 会话列表
+ */
+export const getSessions = async (userId, limit = MAX_SESSIONS_LIMIT) => {
+ try {
+ const response = await fetchWithTimeout(
+ `${BASE_URL}${ENDPOINTS.SESSIONS}?user_id=${encodeURIComponent(userId)}&limit=${limit}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ DEFAULT_TIMEOUT
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const data = await response.json();
+ // 后端返回格式: { success: true, data: [...] }
+ if (data.success) {
+ return data.data || [];
+ }
+ return [];
+ } catch (error) {
+ console.error('[AgentService] 获取会话列表失败:', error);
+ throw error;
+ }
+};
+
+/**
+ * 获取会话历史消息
+ *
+ * @param {string} sessionId - 会话ID
+ * @param {number} limit - 返回数量限制
+ * @returns {Promise