diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc index 81c425da..319f8fbb 100644 Binary files a/__pycache__/mcp_server.cpython-310.pyc and b/__pycache__/mcp_server.cpython-310.pyc differ diff --git a/mcp_server.py b/mcp_server.py index ac55ace4..274db305 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -2868,7 +2868,8 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00 if not successful_results and step_index == 0: # 如果没有执行任何工具(模型直接回复),使用模型的回复 if assistant_message and assistant_message.content: - final_summary = assistant_message.content + # 清理模型输出中的特殊标记(、minimax:tool_call 等) + final_summary = clean_deepseek_tool_markers(assistant_message.content) # 流式发送(虽然已经是完整的,但保持前端兼容) yield self._format_sse("summary_chunk", {"content": final_summary}) else: @@ -2890,16 +2891,58 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00 stream=True, # 启用流式输出 ) - # 逐块发送总结内容 + # 逐块发送总结内容(带缓冲过滤特殊标签) + buffer = "" # 缓冲区,用于检测和过滤 等标签 + in_think_tag = False # 是否在 标签内 + in_minimax_tag = False # 是否在 minimax:tool_call 标签内 + for chunk in summary_stream: if chunk.choices and chunk.choices[0].delta.content: content_chunk = chunk.choices[0].delta.content final_summary += content_chunk + buffer += content_chunk - # 发送总结片段 - yield self._format_sse("summary_chunk", { - "content": content_chunk - }) + # 检测 开始 + if '' in buffer and not in_think_tag: + in_think_tag = True + # 发送 之前的内容 + before_think = buffer.split('')[0] + if before_think: + yield self._format_sse("summary_chunk", {"content": before_think}) + buffer = '' + buffer.split('', 1)[1] + + # 检测 结束 + if '' in buffer and in_think_tag: + in_think_tag = False + # 丢弃 ... 内容,保留之后的内容 + buffer = buffer.split('', 1)[1].lstrip() + + # 检测 minimax:tool_call 开始 + if 'minimax:tool_call' in buffer and not in_minimax_tag: + in_minimax_tag = True + before_minimax = buffer.split('minimax:tool_call')[0] + if before_minimax: + yield self._format_sse("summary_chunk", {"content": before_minimax}) + buffer = 'minimax:tool_call' + buffer.split('minimax:tool_call', 1)[1] + + # 检测 结束 + if '' in buffer and in_minimax_tag: + in_minimax_tag = False + buffer = buffer.split('', 1)[1].lstrip() + + # 如果不在特殊标签内,且缓冲区有足够内容,发送出去 + if not in_think_tag and not in_minimax_tag: + # 保留一些缓冲以防标签跨 chunk + if len(buffer) > 50: + to_send = buffer[:-50] + buffer = buffer[-50:] + yield self._format_sse("summary_chunk", {"content": to_send}) + + # 发送剩余缓冲区内容(最终清理) + if buffer and not in_think_tag and not in_minimax_tag: + buffer = clean_deepseek_tool_markers(buffer) + if buffer: + yield self._format_sse("summary_chunk", {"content": buffer}) logger.info("[Summary] 流式总结完成") @@ -2914,7 +2957,8 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00 yield self._format_sse("summary_chunk", {"content": final_summary}) logger.warning("[Summary] 使用降级方案") - # 发送完整的总结和元数据 + # 发送完整的总结和元数据(最终清理确保无残留标记) + final_summary = clean_deepseek_tool_markers(final_summary) yield self._format_sse("summary", { "content": final_summary, "metadata": { @@ -3090,6 +3134,30 @@ A股交易时间: 上午 9:30-11:30,下午 13:00-15:00 "arguments": arguments }) + # 格式5: MiniMax 格式 + # minimax:tool_call 20260129 + minimax_pattern = r'minimax:tool_call\s*(.*?)\s*' + minimax_matches = re.findall(minimax_pattern, content, re.DOTALL) + + for func_name, params_str in minimax_matches: + arguments = {} + # 解析参数: value + param_pattern = r'(.*?)' + param_matches = re.findall(param_pattern, params_str, re.DOTALL) + + for param_name, param_value in param_matches: + 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 + }) + logger.info(f"[Text Tool Call] 解析到 {len(tool_calls)} 个工具调用: {tool_calls}") return tool_calls @@ -3504,18 +3572,33 @@ MEETING_MODEL_CONFIGS = { def clean_deepseek_tool_markers(content: str) -> str: """ - 清理 DeepSeek 模型输出中的工具调用标记 - DeepSeek 有时会以文本形式输出工具调用,格式如: - <|tool▁calls▁begin|><|tool▁call▁begin|>tool_name<|tool▁sep|>{"args": "value"}<|tool▁call▁end|><|tool▁calls▁end|> + 清理模型输出中的工具调用标记和思考标签 + 支持多种模型格式: + 1. DeepSeek: <|tool▁calls▁begin|>...<|tool▁calls▁end|> + 2. MiniMax: minimax:tool_call ... + 3. 思考标签: ... """ import re if not content: return content + # 清理 ... 思考标签(多种模型都可能输出) + cleaned = re.sub(r'.*?\s*', '', content, flags=re.DOTALL) + + # 清理 MiniMax 工具调用标记 + # 格式: minimax:tool_call ... + cleaned = re.sub(r'minimax:tool_call\s*]*>.*?\s*\s*', '', cleaned, flags=re.DOTALL) + # 清理 DeepSeek 工具调用标记 # 匹配 <|tool▁calls▁begin|> ... <|tool▁calls▁end|> 整个块 pattern = r'<|tool▁calls▁begin|>.*?<|tool▁calls▁end|>' - cleaned = re.sub(pattern, '', content, flags=re.DOTALL) + cleaned = re.sub(pattern, '', cleaned, flags=re.DOTALL) + + # 清理 DSML 格式工具调用 + cleaned = re.sub(r'<[|\|]DSML[|\|]function_calls>.*?\s*', '', cleaned, flags=re.DOTALL) + + # 清理通用 ... 格式 + cleaned = re.sub(r'.*?\s*', '', cleaned, flags=re.DOTALL) # 也清理可能残留的单个标记 markers = [ diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index 02e25ad0..ed9528b3 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { Form, Input, Button, Typography, Modal, message, Tooltip } from 'antd'; -import { LockOutlined, WechatOutlined, MobileOutlined, QrcodeOutlined } from '@ant-design/icons'; +import { LockOutlined, WechatOutlined, MobileOutlined, QrcodeOutlined, UserOutlined } from '@ant-design/icons'; import { useBreakpointValue } from "@chakra-ui/react"; import { useAuth } from "../../contexts/AuthContext"; import { useAuthModal } from "../../hooks/useAuthModal"; @@ -162,6 +162,11 @@ const AUTH_CONFIG = { title: "手机号登录", subtitle: "未注册手机号登录时将自动创建价值前沿账号", }, + // 密码登录内容区文案 + password: { + title: "账号密码登录", + subtitle: "使用用户名、手机号或邮箱登录", + }, buttonText: "登录/注册", loadingText: "验证中...", successDescription: "欢迎!", @@ -192,6 +197,7 @@ export default function AuthFormContent() { const [showNicknamePrompt, setShowNicknamePrompt] = useState(false); const [currentPhone, setCurrentPhone] = useState(""); const [formData, setFormData] = useState({ phone: "", verificationCode: "" }); + const [passwordFormData, setPasswordFormData] = useState({ username: "", password: "" }); const [verificationCodeSent, setVerificationCodeSent] = useState(false); const [sendingCode, setSendingCode] = useState(false); const [countdown, setCountdown] = useState(0); @@ -365,6 +371,52 @@ export default function AuthFormContent() { } }; + // 密码登录输入变化处理 + const handlePasswordInputChange = (e) => { + const { name, value } = e.target; + setPasswordFormData(prev => ({ ...prev, [name]: value })); + }; + + // 密码登录提交 + const handlePasswordSubmit = async (e) => { + e?.preventDefault?.(); + setIsLoading(true); + + try { + const { username, password } = passwordFormData; + if (!username || !password) { + message.warning("用户名和密码不能为空"); + return; + } + + authEvents.trackLoginPageViewed(); // 追踪密码登录尝试 + + const data = await authService.loginWithPassword(username, password); + if (!isMountedRef.current) return; + + if (data.success) { + await checkSession(); + authEvents.trackLoginSuccess(data.user, 'password', false); + message.success('登录成功'); + setTimeout(() => handleLoginSuccess({ username }), config.features.successDelay); + + setTimeout(() => { + if (showWelcomeGuide) showWelcomeGuide(); + }, 10000); + } else { + throw new Error(data.error || '登录失败'); + } + } catch (error) { + authEvents.trackLoginFailed('password', 'api', error.message, { + username_masked: passwordFormData.username ? passwordFormData.username.substring(0, 2) + '****' : 'N/A', + }); + logger.error('AuthFormContent', 'handlePasswordSubmit', error); + message.error(error.message || "登录失败"); + } finally { + if (isMountedRef.current) setIsLoading(false); + } + }; + // 微信H5登录(移动端) const handleWechatH5Login = async () => { authEvents.trackWechatLoginInitiated('icon_button'); @@ -477,6 +529,78 @@ export default function AuthFormContent() { ); + // 渲染密码登录表单 + const renderPasswordForm = () => ( +
+ {/* 内容区小标题 */} +
+ {config.password.title} +
+
{config.password.subtitle}
+ +
+ + } + /> + + + + } + /> + + + + + + +
+ 登录即表示您同意价值前沿{" "} + + 《用户协议》 + + {" "}和{" "} + + 《隐私政策》 + +
+
+
+ ); + // 渲染微信登录区域 const renderWechatLogin = () => (
@@ -506,79 +630,117 @@ export default function AuthFormContent() { // 渲染底部其他登录方式 const renderBottomDivider = () => { - const isWechatTab = activeTab === 'wechat'; - // 移动端微信 Tab 下不显示底部切换(因为移动端微信是 H5 跳转) - if (isMobile && isWechatTab) { + if (isMobile && activeTab === 'wechat') { return null; } + // 根据当前 tab 显示其他两种登录方式 + const otherOptions = []; + + if (activeTab !== 'wechat') { + otherOptions.push({ + key: 'wechat', + icon: , + label: '微信', + color: THEME.wechat, + hoverBg: 'rgba(7, 193, 96, 0.1)', + onClick: isMobile ? handleWechatH5Login : () => handleTabChange('wechat'), + }); + } + + if (activeTab !== 'phone') { + otherOptions.push({ + key: 'phone', + icon: , + label: '验证码', + color: THEME.goldPrimary, + hoverBg: 'rgba(212, 175, 55, 0.1)', + onClick: () => handleTabChange('phone'), + }); + } + + if (activeTab !== 'password') { + otherOptions.push({ + key: 'password', + icon: , + label: '密码', + color: THEME.textSecondary, + hoverBg: 'rgba(255, 255, 255, 0.1)', + onClick: () => handleTabChange('password'), + }); + } + return (
其他登录方式: - {isWechatTab ? ( + {otherOptions.map((option) => ( handleTabChange('phone')} + onClick={option.onClick} onMouseEnter={(e) => { - e.currentTarget.style.background = 'rgba(212, 175, 55, 0.1)'; + e.currentTarget.style.background = option.hoverBg; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }} > - 手机 + {option.icon} {option.label} - ) : ( - handleTabChange('wechat')} - onMouseEnter={(e) => { - e.currentTarget.style.background = 'rgba(7, 193, 96, 0.1)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = 'transparent'; - }} - > - 微信 - - )} + ))}
); }; + // 获取下一个登录方式(循环切换:wechat -> phone -> password -> wechat) + const getNextTab = () => { + if (activeTab === 'wechat') return 'phone'; + if (activeTab === 'phone') return 'password'; + return 'wechat'; // password -> wechat + }; + // 获取右上角折角的颜色(根据要切换到的登录方式) const getCornerColor = () => { - // 显示要切换到的方式的颜色 - return activeTab === 'wechat' ? THEME.goldPrimary : THEME.wechat; + const nextTab = getNextTab(); + if (nextTab === 'wechat') return THEME.wechat; + if (nextTab === 'phone') return THEME.goldPrimary; + return THEME.textSecondary; // password }; // 获取右上角图标 const getCornerIcon = () => { - // 显示要切换到的方式的图标:手机图标 或 二维码图标 - return activeTab === 'wechat' ? : ; + const nextTab = getNextTab(); + if (nextTab === 'wechat') return ; + if (nextTab === 'phone') return ; + return ; // password + }; + + // 获取右上角提示文字 + const getCornerTooltip = () => { + const nextTab = getNextTab(); + if (nextTab === 'wechat') return '切换到微信登录'; + if (nextTab === 'phone') return '切换到验证码登录'; + return '切换到密码登录'; }; return (
{/* 右上角折角切换图标 */}
handleTabChange(activeTab === 'wechat' ? 'phone' : 'wechat')} + onClick={() => handleTabChange(getNextTab())} >
- {activeTab === 'wechat' ? renderWechatLogin() : renderPhoneForm()} + {activeTab === 'wechat' && renderWechatLogin()} + {activeTab === 'phone' && renderPhoneForm()} + {activeTab === 'password' && renderPasswordForm()}
{/* 底部其他登录方式 */} diff --git a/src/services/authService.js b/src/services/authService.js index 57fe7ebe..1d201588 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -134,6 +134,50 @@ export const authService = { body: JSON.stringify({ session_id: sessionId }), }); }, + + /** + * 使用用户名/手机号/邮箱 + 密码登录 + * @param {string} username - 用户名、手机号或邮箱 + * @param {string} password - 密码 + * @returns {Promise<{success: boolean, user?: object, error?: string}>} + */ + loginWithPassword: async (username, password) => { + const method = 'POST'; + const url = '/api/auth/login'; + + logger.api.request(method, url, { username: username }); + + try { + // 后端使用 form-data 格式 + const formData = new URLSearchParams(); + formData.append('username', username); + formData.append('password', password); + + const response = await fetch(`${API_BASE_URL}${url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + credentials: 'include', + body: formData, + }); + + const data = await response.json(); + logger.api.response(method, url, response.status, data); + + if (!response.ok) { + throw new Error(data.error || data.message || `登录失败 (${response.status})`); + } + + return data; + } catch (error) { + logger.api.error(method, url, error, { username }); + if (error.message === 'Failed to fetch' || error.name === 'TypeError') { + throw new Error('网络连接失败,请检查网络设置'); + } + throw error; + } + }, }; /**