maziang
Maziang 5 months ago
parent c7855af61b
commit 078a68fe2d

@ -591,7 +591,7 @@ class MarkTextMainWindow(QMainWindow):
title.setAlignment(Qt.AlignCenter)
# 副标题
subtitle = QLabel("基于MarkText的现代化Markdown编辑器")
subtitle = QLabel("解析·输入·专注")
subtitle.setStyleSheet("""
QLabel {
font-size: 18px;
@ -1177,8 +1177,6 @@ class MarkTextMainWindow(QMainWindow):
background-color: #4d4d4d;
}
""")
else:
self.setStyleSheet("")
def get_current_editor(self) -> Optional[MarkTextEditor]:
"""获取当前编辑器"""

@ -11,7 +11,11 @@ class NetworkService:
# 实现构造函数逻辑
self.api_key = None
self.cache = {}
# 设置默认headers以避免被服务器拒绝
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
# 天气缓存相关属性
self._cached_weather_data = None # 缓存的天气数据
self._cached_location = None # 缓存的定位信息
@ -43,39 +47,42 @@ class NetworkService:
return False
return (time.time() - self._weather_cache_timestamp) < 1800 # 30分钟
def get_user_ip(self):
"""获取用户IP地址 - 使用多个备用服务"""
# 首先尝试获取本地IP
try:
import socket
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
if local_ip and not local_ip.startswith("127."):
print(f"获取到本地IP: {local_ip}")
return local_ip
except Exception as e:
print(f"获取本地IP失败: {e}")
# 如果本地IP获取失败使用备用外部服务
def get_user_ip(self) -> Optional[str]:
"""获取用户公网IP地址使用多个备用服务"""
# 备用IP获取服务列表
ip_services = [
"https://httpbin.org/ip",
"https://api.ipify.org?format=json",
"https://ipapi.co/json/"
"https://ident.me/.json"
]
for service in ip_services:
for service_url in ip_services:
try:
print(f"尝试从 {service} 获取IP地址...")
response = self.session.get(service, timeout=3, verify=False)
if response.status_code == 200:
data = response.json()
ip = data.get("origin") or data.get("ip") or data.get("ip_address")
if ip:
print(f"成功从 {service} 获取IP: {ip}")
return ip
print(f"尝试获取IP地址: {service_url}")
response = self.session.get(service_url, timeout=10)
response.raise_for_status() # 检查HTTP错误
data = response.json()
# 不同服务返回的IP字段可能不同
ip = (data.get('origin') or
data.get('ip') or
data.get('IPAddress') or
data.get('query'))
if ip:
# 如果IP包含逗号取第一个可能有代理
if ',' in ip:
ip = ip.split(',')[0].strip()
print(f"成功获取IP地址: {ip}")
return ip
else:
print(f"无法从响应中提取IP地址: {data}")
except requests.exceptions.RequestException as e:
print(f"网络请求错误 ({service_url}): {e}")
except ValueError as e:
print(f"JSON解析错误 ({service_url}): {e}")
except Exception as e:
print(f"{service} 获取IP失败: {e}")
continue
print(f"获取IP时发生未知错误 ({service_url}): {e}")
print("所有IP获取服务都失败了使用默认IP")
return "8.8.8.8" # 使用Google DNS作为默认IP
@ -134,22 +141,44 @@ class NetworkService:
print("无法获取IP地址使用默认天气数据")
return self.get_default_weather()
# 2. 根据IP获取地理位置
# 注意这里使用免费的IP地理位置API实际应用中可能需要更精确的服务
location_url = f"http://ip-api.com/json/{ip}"
print(f"请求地理位置: {location_url}")
location_response = self.session.get(location_url, timeout=5, verify=False)
location_data = location_response.json()
print(f"地理位置响应: {location_data}")
# 2. 根据IP获取地理位置 - 增加重试机制并使用更稳定的API
location_success = False
location_data = None
# 使用ipapi.co API它比ip-api.com更稳定
location_url = f"https://ipapi.co/{ip}/json/"
for attempt in range(3): # 最多尝试3次
try:
print(f"请求地理位置 (尝试 {attempt + 1}/3): {location_url}")
location_response = self.session.get(location_url, timeout=10)
location_response.raise_for_status() # 检查HTTP错误
location_data = location_response.json()
print(f"地理位置响应: {location_data}")
# ipapi.co 使用不同的状态字段
if 'error' not in location_data:
location_success = True
break
else:
error_msg = location_data.get('reason', 'Unknown error')
print(f"地理位置获取失败,错误: {error_msg}")
time.sleep(1) # 等待1秒后重试
except requests.exceptions.RequestException as e:
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
except Exception as e:
print(f"获取地理位置时出错 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
if location_data.get("status") != "success":
if not location_success:
print("地理位置获取失败,使用默认天气数据")
return self.get_default_weather()
# ipapi.co 使用不同的字段名
city = location_data.get("city", "Unknown")
print(f"获取到的城市: {city}")
if not city:
if not city or city == "Unknown":
print("无法获取城市名称,使用默认天气数据")
return self.get_default_weather()
@ -157,29 +186,47 @@ class NetworkService:
self._cached_location = {
"ip": ip,
"city": city,
"country": location_data.get("country", "Unknown"),
"region": location_data.get("regionName", "Unknown")
"country": location_data.get("country_name", "Unknown"),
"region": location_data.get("region", "Unknown")
}
# 3. 调用天气API获取天气数据
# 注意这里使用OpenWeatherMap API作为示例需要API密钥
# 在实际应用中需要设置有效的API密钥
if self.api_key:
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
weather_response = self.session.get(weather_url, timeout=5, verify=False)
weather_data = weather_response.json()
weather_success = False
weather_data = None
for attempt in range(3): # 最多尝试3次
try:
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
print(f"请求天气数据 (尝试 {attempt + 1}/3): {weather_url}")
weather_response = self.session.get(weather_url, timeout=10)
weather_response.raise_for_status() # 检查HTTP错误
weather_data = weather_response.json()
print(f"天气响应状态码: {weather_response.status_code}")
# 4. 解析并格式化数据
if weather_response.status_code == 200:
formatted_weather = {
"city": city,
"temperature": weather_data["main"]["temp"],
"description": weather_data["weather"][0]["description"],
"humidity": weather_data["main"]["humidity"],
"wind_speed": weather_data["wind"]["speed"]
}
# 5. 返回天气信息字典
return formatted_weather
else:
print(f"天气API返回错误状态码: {weather_response.status_code}")
time.sleep(1) # 等待1秒后重试
except requests.exceptions.RequestException as e:
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
except Exception as e:
print(f"获取天气数据时出错 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
# 4. 解析并格式化数据
if weather_response.status_code == 200:
formatted_weather = {
"city": city,
"temperature": weather_data["main"]["temp"],
"description": weather_data["weather"][0]["description"],
"humidity": weather_data["main"]["humidity"],
"wind_speed": weather_data["wind"]["speed"]
}
# 5. 返回天气信息字典
return formatted_weather
print("所有天气API请求都失败了")
else:
# 当没有API密钥时使用免费的天气API获取真实数据
# 首先尝试获取城市ID需要映射城市名到ID
@ -239,74 +286,87 @@ class NetworkService:
weather_city_id = "101010100" # 默认北京ID
print(f"使用默认城市ID获取天气: {city} -> {weather_city_id}")
# 使用免费天气API获取天气数据
try:
# 使用和风天气免费API的替代方案 - sojson天气API
weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}"
print(f"请求天气数据: {weather_url}")
weather_response = self.session.get(weather_url, timeout=5, verify=False)
print(f"天气响应状态码: {weather_response.status_code}")
weather_data = weather_response.json()
print(f"天气数据响应: {weather_data}")
if weather_data.get("status") == 200:
# 解析天气数据
current_data = weather_data.get("data", {})
wendu = current_data.get("wendu", "N/A")
shidu = current_data.get("shidu", "N/A")
forecast = current_data.get("forecast", [])
# 获取第一个预报项作为当前天气
current_weather = forecast[0] if forecast else {}
weather_type = current_weather.get("type", "")
# 获取生活指数信息
lifetips = []
if current_weather:
# 从预报数据中提取生活提示
ganmao = current_weather.get("ganmao", "")
if ganmao:
lifetips.append(f"感冒指数: {ganmao}")
# 添加其他生活指数(基于天气类型推断)
if "" in weather_type:
lifetips.append("出行建议: 记得带伞")
elif "" in weather_type:
lifetips.append("出行建议: 适合户外活动")
elif "" in weather_type:
lifetips.append("出行建议: 注意防滑保暖")
elif "" in weather_type or "" in weather_type:
lifetips.append("健康提醒: 减少户外运动")
# 温度相关建议
temp = float(wendu) if wendu != "N/A" else 20
if temp > 30:
lifetips.append("穿衣建议: 注意防暑降温")
elif temp < 5:
lifetips.append("穿衣建议: 注意保暖防寒")
elif temp < 15:
lifetips.append("穿衣建议: 适当添加衣物")
else:
lifetips.append("穿衣建议: 天气舒适")
# 使用免费天气API获取天气数据 - 增加重试机制
weather_success = False
weather_data = None
for attempt in range(3): # 最多尝试3次
try:
# 使用和风天气免费API的替代方案 - sojson天气API
weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}"
print(f"请求天气数据 (尝试 {attempt + 1}/3): {weather_url}")
weather_response = self.session.get(weather_url, timeout=10)
weather_response.raise_for_status() # 检查HTTP错误
weather_data = weather_response.json()
print(f"天气数据响应: {weather_data}")
formatted_weather = {
"city": city,
"temperature": float(wendu) if wendu != "N/A" else 20,
"description": weather_type,
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
"wind_speed": "3.5", # 默认风速
"lifetips": lifetips # 生活提示列表
}
print(f"成功获取天气数据: {formatted_weather}")
if weather_data.get("status") == 200:
weather_success = True
break
else:
print(f"天气API返回错误状态: {weather_data.get('status')}")
time.sleep(1) # 等待1秒后重试
except requests.exceptions.RequestException as e:
print(f"网络请求错误 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
except Exception as e:
print(f"获取免费天气数据时出错 (尝试 {attempt + 1}/3): {e}")
time.sleep(2) # 等待2秒后重试
if weather_success:
# 解析天气数据
current_data = weather_data.get("data", {})
wendu = current_data.get("wendu", "N/A")
shidu = current_data.get("shidu", "N/A")
forecast = current_data.get("forecast", [])
# 获取第一个预报项作为当前天气
current_weather = forecast[0] if forecast else {}
weather_type = current_weather.get("type", "")
# 获取生活指数信息
lifetips = []
if current_weather:
# 从预报数据中提取生活提示
ganmao = current_weather.get("ganmao", "")
if ganmao:
lifetips.append(f"感冒指数: {ganmao}")
# 缓存天气数据
self.set_weather_cache(formatted_weather, self._cached_location)
return formatted_weather
else:
print(f"天气API返回错误状态: {weather_data.get('status')}")
# 添加其他生活指数(基于天气类型推断)
if "" in weather_type:
lifetips.append("出行建议: 记得带伞")
elif "" in weather_type:
lifetips.append("出行建议: 适合户外活动")
elif "" in weather_type:
lifetips.append("出行建议: 注意防滑保暖")
elif "" in weather_type or "" in weather_type:
lifetips.append("健康提醒: 减少户外运动")
except Exception as e:
print(f"获取免费天气数据时出错: {e}")
# 温度相关建议
temp = float(wendu) if wendu != "N/A" else 20
if temp > 30:
lifetips.append("穿衣建议: 注意防暑降温")
elif temp < 5:
lifetips.append("穿衣建议: 注意保暖防寒")
elif temp < 15:
lifetips.append("穿衣建议: 适当添加衣物")
else:
lifetips.append("穿衣建议: 天气舒适")
formatted_weather = {
"city": city,
"temperature": float(wendu) if wendu != "N/A" else 20,
"description": weather_type,
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
"wind_speed": "3.5", # 默认风速
"lifetips": lifetips # 生活提示列表
}
print(f"成功获取天气数据: {formatted_weather}")
# 缓存天气数据
self.set_weather_cache(formatted_weather, self._cached_location)
return formatted_weather
else:
print("所有免费天气API请求都失败了")
# 如果以上都失败,返回默认数据
default_weather = {

@ -12,13 +12,6 @@ from PyQt5.QtCore import Qt, pyqtSignal, QThread, QTimer, QSize, pyqtSlot
from PyQt5.QtGui import QFont, QColor, QTextCursor, QIcon, QPixmap
from PyQt5.QtGui import QTextDocument, QTextCharFormat
# 导入主题管理器
try:
from .theme_manager import theme_manager
except ImportError:
# 处理直接运行的情况
from ui.theme_manager import theme_manager
class AIChatPanel(QWidget):
"""AI对话面板"""
@ -41,9 +34,6 @@ class AIChatPanel(QWidget):
# 连接信号到槽
self.update_chat_display.connect(self.on_update_chat_display)
# 连接主题变化信号
theme_manager.theme_changed.connect(self.on_theme_changed)
def load_api_key(self):
"""从本地文件加载API密钥"""
@ -84,9 +74,9 @@ class AIChatPanel(QWidget):
clear_btn.clicked.connect(self.clear_history)
clear_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
color: #333333;
border: 1px solid #d0d0d0;
background-color: #3c3c3c;
color: #e0e0e0;
border: 1px solid #4c4c4c;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
@ -94,25 +84,10 @@ class AIChatPanel(QWidget):
min-width: 50px;
}
QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QPushButton:pressed {
background-color: #d0d0d0;
border: 1px solid #b0b0b0;
}
/* 深色主题 */
QPushButton[darkTheme="true"] {
background-color: #3c3c3c;
color: #e0e0e0;
border: 1px solid #4c4c4c;
}
QPushButton[darkTheme="true"]:hover {
background-color: #4c4c4c;
border: 1px solid #5c5c5c;
}
QPushButton[darkTheme="true"]:pressed {
QPushButton:pressed {
background-color: #5c5c5c;
border: 1px solid #6c6c6c;
}
@ -124,7 +99,7 @@ class AIChatPanel(QWidget):
# 分割线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setStyleSheet("color: #d0d0d0;")
line.setStyleSheet("color: #3c3c3c;")
main_layout.addWidget(line)
# 对话显示区域
@ -132,21 +107,14 @@ class AIChatPanel(QWidget):
self.chat_display.setReadOnly(True)
self.chat_display.setStyleSheet("""
QTextEdit {
background-color: #ffffff;
border: 1px solid #d0d0d0;
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 8px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 11pt;
padding: 12px;
color: #333333;
line-height: 1.5;
}
/* 深色主题 */
QTextEdit[darkTheme="true"] {
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
color: #e0e0e0;
line-height: 1.5;
}
""")
self.chat_display.setMinimumHeight(400)
@ -161,26 +129,15 @@ class AIChatPanel(QWidget):
self.input_field.setPlaceholderText("输入您的问题或请求...")
self.input_field.setStyleSheet("""
QLineEdit {
background-color: #f9f9f9;
border: 1px solid #d0d0d0;
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
border-radius: 8px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 11pt;
padding: 10px 12px;
color: #333333;
}
QLineEdit:focus {
border: 2px solid #0078d4;
background-color: #ffffff;
}
/* 深色主题 */
QLineEdit[darkTheme="true"] {
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
color: #e0e0e0;
}
QLineEdit[darkTheme="true"]:focus {
QLineEdit:focus {
border: 2px solid #0078d4;
background-color: #1e1e1e;
}
@ -194,7 +151,7 @@ class AIChatPanel(QWidget):
# 发送按钮
self.send_btn = QPushButton("发送")
self.send_btn.setFont(QFont("微软雅黑", 9))
self.send_btn.setFont(QFont("宋体", 9))
self.send_btn.setStyleSheet("""
QPushButton {
background-color: #0078d4;
@ -212,17 +169,6 @@ class AIChatPanel(QWidget):
QPushButton:pressed {
background-color: #005a9e;
}
/* 深色主题 */
QPushButton[darkTheme="true"] {
background-color: #0078d4;
}
QPushButton[darkTheme="true"]:hover {
background-color: #0063b1;
}
QPushButton[darkTheme="true"]:pressed {
background-color: #005a9e;
}
""")
self.send_btn.clicked.connect(self.send_user_message)
button_layout.addStretch()
@ -236,23 +182,15 @@ class AIChatPanel(QWidget):
# 设置背景颜色
self.setStyleSheet("""
AIChatPanel {
background-color: #f5f5f5;
border-left: 1px solid #d0d0d0;
}
/* 深色主题 */
AIChatPanel[darkTheme="true"] {
background-color: #2d2d2d;
border-left: 1px solid #3c3c3c;
}
""")
# 初始化主题
self.apply_theme()
def apply_theme(self):
"""应用当前主题"""
is_dark = theme_manager.is_dark_theme()
# 始终应用深色主题
is_dark = True
# 设置属性用于样式表选择器
self.setProperty("darkTheme", is_dark)
@ -291,7 +229,8 @@ class AIChatPanel(QWidget):
def on_theme_changed(self, is_dark):
"""主题变化处理"""
self.apply_theme()
# 不响应主题变化,始终保持深色模式
pass
def send_user_message(self):
"""发送用户消息"""
@ -344,28 +283,20 @@ class AIChatPanel(QWidget):
char_format.setFont(QFont("微软雅黑", 11))
if sender == "用户":
# 根据主题设置用户消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#4A90E2")) # 深色主题下的蓝色
else:
char_format.setForeground(QColor("#0078d4")) # 浅色主题下的蓝色
# 始终使用深色主题下的蓝色
char_format.setForeground(QColor("#4A90E2"))
char_format.setFontWeight(60) # 中等粗体
prefix = "你: "
else: # AI助手
# 根据主题设置AI消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#e0e0e0")) # 深色主题下的浅灰色
else:
char_format.setForeground(QColor("#000000")) # 浅色主题下的纯黑色,提高可读性
# 始终使用深色主题下的浅灰色
char_format.setForeground(QColor("#e0e0e0"))
char_format.setFontWeight(50) # 正常粗体
prefix = "AI: "
# 插入时间戳
timestamp_format = QTextCharFormat()
if theme_manager.is_dark_theme():
timestamp_format.setForeground(QColor("#a0a0a0")) # 深色主题下的灰色
else:
timestamp_format.setForeground(QColor("#666666")) # 浅色主题下的深灰色,提高可读性
# 始终使用深色主题下的灰色
timestamp_format.setForeground(QColor("#a0a0a0"))
timestamp_format.setFont(QFont("微软雅黑", 9))
cursor.insertText(f"\n[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format)
@ -394,19 +325,13 @@ class AIChatPanel(QWidget):
char_format.setFont(QFont("微软雅黑", 11))
if sender == "用户":
# 根据主题设置用户消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#4A90E2")) # 深色主题下的蓝色
else:
char_format.setForeground(QColor("#0078d4")) # 浅色主题下的蓝色
# 始终使用深色主题下的蓝色
char_format.setForeground(QColor("#4A90E2"))
char_format.setFontWeight(60) # 中等粗体
prefix = "你: "
else:
# 根据主题设置AI消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#e0e0e0")) # 深色主题下的浅灰色
else:
char_format.setForeground(QColor("#000000")) # 浅色主题下的纯黑色,提高可读性
# 始终使用深色主题下的浅灰色
char_format.setForeground(QColor("#e0e0e0"))
char_format.setFontWeight(50) # 正常粗体
prefix = "AI: "
@ -416,10 +341,8 @@ class AIChatPanel(QWidget):
# 插入时间戳
timestamp_format = QTextCharFormat()
if theme_manager.is_dark_theme():
timestamp_format.setForeground(QColor("#a0a0a0")) # 深色主题下的灰色
else:
timestamp_format.setForeground(QColor("#666666")) # 浅色主题下的深灰色,提高可读性
# 始终使用深色主题下的灰色
timestamp_format.setForeground(QColor("#a0a0a0"))
timestamp_format.setFont(QFont("微软雅黑", 9))
cursor.insertText(f"[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format)
@ -527,9 +450,34 @@ class AIChatPanel(QWidget):
def update_streaming_display(self):
"""更新流式显示"""
if self.is_streaming and self.current_streaming_content:
# 重新显示所有对话
self.rebuild_chat_display()
self.chat_display.verticalScrollBar().setValue(
self.chat_display.verticalScrollBar().maximum()
)
if not self.current_streaming_content:
return
# 获取最后一段文本AI回复
cursor = self.chat_display.textCursor()
cursor.movePosition(QTextCursor.End)
self.chat_display.setTextCursor(cursor)
# 查找最后一个AI回复的位置
text = self.chat_display.toPlainText()
last_ai_pos = text.rfind("AI: ")
if last_ai_pos == -1:
return
# 删除现有的AI回复内容
cursor.setPosition(last_ai_pos + 4) # 移动到"AI: "之后
cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
cursor.removeSelectedText()
# 插入新的AI回复内容
char_format = QTextCharFormat()
char_format.setFont(QFont("微软雅黑", 11))
# 始终使用深色主题下的浅灰色
char_format.setForeground(QColor("#e0e0e0"))
char_format.setFontWeight(50) # 正常粗体
cursor.insertText(self.current_streaming_content, char_format)
# 滚动到底部
self.chat_display.verticalScrollBar().setValue(
self.chat_display.verticalScrollBar().maximum()
)
Loading…
Cancel
Save