From c56f5f2f7f3081688498883eaf830b027729bf8e Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sat, 31 Jan 2026 15:32:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0app.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/mcp_server.cpython-310.pyc | Bin 95348 -> 96577 bytes mcp_server.py | 105 +++++++++-- src/components/Auth/AuthFormContent.js | 230 +++++++++++++++++++++---- src/services/authService.js | 44 +++++ 4 files changed, 335 insertions(+), 44 deletions(-) diff --git a/__pycache__/mcp_server.cpython-310.pyc b/__pycache__/mcp_server.cpython-310.pyc index 81c425da38f4a4cc96c3b145edc9247470f06880..319f8fbb875a272fa0d0f1bcd747794186254acb 100644 GIT binary patch delta 5494 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 delta 4303 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、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; + } + }, }; /**