Compare commits

..

9 Commits

@ -0,0 +1 @@
db/

2
.gitignore vendored

@ -198,7 +198,9 @@ temp/
*.orig
# Project specific
macos_release/
resources/config/deepseek_api.json
src/resources/config/ai_config.json
*.key
*.secret
config/*.json

Binary file not shown.

@ -0,0 +1,351 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord macOS完整打包脚本
用于构建macOS Apple Silicon原生应用程序包含所有必需资源
"""
import os
import sys
import subprocess
import platform
import shutil
import plistlib
from datetime import datetime
def run_command(command, shell=False, cwd=None):
"""运行命令并返回结果"""
try:
result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8', cwd=cwd)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, "", str(e)
def check_system():
"""检查系统是否为macOS"""
if platform.system() != "Darwin":
print("错误: 此脚本仅支持macOS系统")
return False
# 检查架构
machine = platform.machine()
if machine == 'arm64':
print(f"系统信息: macOS {platform.mac_ver()[0]}, Apple Silicon ({machine})")
elif machine == 'x86_64':
print(f"系统信息: macOS {platform.mac_ver()[0]}, Intel ({machine})")
else:
print(f"系统信息: macOS {platform.mac_ver()[0]}, {machine}")
return True
def clean_build_dirs():
"""清理构建目录"""
print("清理构建目录...")
dirs_to_clean = ['build', 'dist', '__pycache__', '*.egg-info']
for dir_name in dirs_to_clean:
if '*' in dir_name:
import glob
for path in glob.glob(dir_name):
if os.path.isdir(path):
shutil.rmtree(path, ignore_errors=True)
elif os.path.exists(dir_name):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name, ignore_errors=True)
else:
os.remove(dir_name)
# 清理src目录下的__pycache__
for root, dirs, files in os.walk('src'):
for dir_name in dirs:
if dir_name == '__pycache__':
cache_path = os.path.join(root, dir_name)
shutil.rmtree(cache_path, ignore_errors=True)
print(f"清理缓存: {cache_path}")
def install_dependencies():
"""安装依赖"""
print("安装项目依赖...")
# 首先确保pip是最新的
run_command([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
# 安装requirements.txt中的依赖
if os.path.exists("requirements.txt"):
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
if code != 0:
print(f"依赖安装失败: {stderr}")
return False
else:
print("警告: 找不到requirements.txt文件")
# 安装PyInstaller
print("安装PyInstaller...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"])
if code != 0:
print(f"PyInstaller安装失败: {stderr}")
return False
print("依赖安装成功")
return True
def build_macos_app():
"""构建macOS应用包"""
print("构建macOS应用包...")
# 确定架构
machine = platform.machine()
if machine == 'arm64':
target_arch = 'arm64'
elif machine == 'x86_64':
target_arch = 'x86_64'
else:
target_arch = 'universal2' # 通用架构
# PyInstaller命令 - 针对macOS优化
pyinstaller_cmd = [
"pyinstaller",
"--name", "MagicWord",
"--version", "1.0.0",
"--distpath", "dist",
"--workpath", "build",
"--specpath", ".",
# 数据文件 - macOS格式使用冒号分隔
"--add-data", "resources:resources",
"--add-data", "src:src",
"--add-data", "src/ui/UI.png:ui",
"--add-data", "src/ui/114514.png:ui",
# 图标文件
"--icon", "resources/icons/app_icon_256X256.png",
# 隐藏导入模块
"--hidden-import", "PyQt5",
"--hidden-import", "PyQt5.QtCore",
"--hidden-import", "PyQt5.QtGui",
"--hidden-import", "PyQt5.QtWidgets",
"--hidden-import", "requests",
"--hidden-import", "beautifulsoup4",
"--hidden-import", "python-docx",
"--hidden-import", "PyPDF2",
"--hidden-import", "ebooklib",
"--hidden-import", "chardet",
"--hidden-import", "PIL",
# macOS应用包选项
"--windowed", # 无控制台窗口
"--osx-bundle-identifier", "com.magicword.app",
"--target-architecture", target_arch,
"--noconfirm",
"src/main.py"
]
print(f"目标架构: {target_arch}")
print("运行PyInstaller...")
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"构建失败: {stderr}")
print("尝试通用架构...")
# 尝试通用架构
pyinstaller_cmd[-2] = "universal2"
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"通用架构构建也失败: {stderr}")
return False
print("macOS应用包构建成功")
return True
def create_app_bundle():
"""创建macOS应用束"""
print("创建macOS应用束...")
app_path = "dist/MagicWord.app"
if not os.path.exists(app_path):
print(f"错误: 找不到应用包 {app_path}")
return False
# 创建Info.plist文件
info_plist = {
'CFBundleName': 'MagicWord',
'CFBundleDisplayName': 'MagicWord - 隐私学习软件',
'CFBundleIdentifier': 'com.magicword.app',
'CFBundleVersion': '1.0.0',
'CFBundleShortVersionString': '1.0.0',
'CFBundleExecutable': 'MagicWord',
'CFBundlePackageType': 'APPL',
'CFBundleSignature': '????',
'LSMinimumSystemVersion': '11.0', # macOS Big Sur及更高版本
'NSHighResolutionCapable': True,
'NSHumanReadableCopyright': 'Copyright © 2024 MagicWord Team. All rights reserved.',
'CFBundleDocumentTypes': [
{
'CFBundleTypeName': 'Text Document',
'CFBundleTypeExtensions': ['txt', 'docx', 'pdf'],
'CFBundleTypeRole': 'Editor'
}
]
}
plist_path = os.path.join(app_path, "Contents", "Info.plist")
with open(plist_path, 'wb') as f:
plistlib.dump(info_plist, f)
# 复制图标文件到应用束
icon_files = [
'resources/icons/app_icon_32X32.png',
'resources/icons/app_icon_64X64.png',
'resources/icons/app_icon_128X128.png',
'resources/icons/app_icon_256X256.png'
]
resources_dir = os.path.join(app_path, "Contents", "Resources")
os.makedirs(resources_dir, exist_ok=True)
for icon_file in icon_files:
if os.path.exists(icon_file):
shutil.copy2(icon_file, resources_dir)
print(f"复制图标: {icon_file}")
# 复制UI图片文件
ui_files = [
'src/ui/UI.png',
'src/ui/114514.png'
]
for ui_file in ui_files:
if os.path.exists(ui_file):
shutil.copy2(ui_file, resources_dir)
print(f"复制UI文件: {ui_file}")
print("macOS应用束创建完成")
return True
def create_dmg():
"""创建DMG安装包"""
print("创建DMG安装包...")
app_path = "dist/MagicWord.app"
if not os.path.exists(app_path):
print(f"错误: 找不到应用包 {app_path}")
return False
# 创建发布目录
release_dir = "macos_release"
if os.path.exists(release_dir):
shutil.rmtree(release_dir)
os.makedirs(release_dir)
# 复制应用到发布目录
release_app_path = os.path.join(release_dir, "MagicWord.app")
shutil.copytree(app_path, release_app_path)
# 创建Applications链接
applications_link = os.path.join(release_dir, "Applications")
os.symlink("/Applications", applications_link)
# 创建README文件
readme_content = f"""# MagicWord 1.0.0 for macOS
## 安装说明
1. MagicWord.app 拖拽到 Applications 文件夹
2. 首次运行时如果出现安全提示请前往 系统设置 > 隐私与安全性 允许应用运行
3. 或者右键点击应用选择"打开"
## 系统要求
- macOS Big Sur (11.0) 或更高版本
- Apple Silicon (M1/M2/M3) Intel 处理器
## 功能特性
- 隐私学习通过打字练习来学习文档内容
- 支持多种文档格式TXT, DOCX, PDF
- 智能打字模式
- 美观的Word风格界面
- 内置小游戏扫雷和贪吃蛇
## 版本信息
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
平台: {platform.system()} {platform.machine()}
Python版本: {platform.python_version()}
## 技术支持
如有问题请查看项目文档或联系开发团队
"""
with open(os.path.join(release_dir, "README.txt"), "w") as f:
f.write(readme_content)
# 创建DMG文件如果系统支持
machine = platform.machine()
dmg_name = f"MagicWord-1.0.0-macOS-{machine}.dmg"
dmg_path = os.path.join("dist", dmg_name)
# 使用hdiutil创建DMG
create_dmg_cmd = [
"hdiutil", "create",
"-volname", "MagicWord",
"-srcfolder", release_dir,
"-ov",
"-format", "UDZO",
dmg_path
]
code, stdout, stderr = run_command(create_dmg_cmd)
if code == 0:
print(f"✅ DMG创建成功: {dmg_path}")
return True
else:
print(f"⚠️ DMG创建失败: {stderr}")
print("已创建应用包可手动打包DMG")
return False
def main():
"""主函数"""
print("=== MagicWord macOS完整打包脚本 ===")
print(f"构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 检查系统
if not check_system():
return False
# 清理构建目录
clean_build_dirs()
# 安装依赖
if not install_dependencies():
print("依赖安装失败")
return False
# 构建应用
if not build_macos_app():
print("应用构建失败")
return False
# 创建应用束
if not create_app_bundle():
print("应用束创建失败")
return False
# 创建DMG
create_dmg()
print("\n=== 构建完成 ===")
print("📱 应用位置: dist/MagicWord.app")
print("💿 DMG文件: dist/MagicWord-*.dmg")
print("")
print("🔧 安装步骤:")
print(" 1. 将 MagicWord.app 拖拽到 Applications 文件夹")
print(" 2. 首次运行时,右键点击应用选择'打开'")
print(" 3. 或者在 系统设置 > 隐私与安全性 中允许应用运行")
print("")
print("✨ 应用特点:")
print(" ✅ 包含app_icon_256X256.png图标")
print(" ✅ 包含UI.png界面图片")
print(" ✅ 支持拖拽安装")
print(" ✅ 双击即可运行")
if __name__ == "__main__":
main()

@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简化版macOS应用构建脚本
专注于正确打包UI.png和图标文件
"""
import os
import sys
import subprocess
import shutil
def run_command(cmd, cwd=None):
"""运行命令"""
print(f"运行: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
print(f"错误: {result.stderr}")
return False
return True
def build_app():
"""构建应用"""
print("=== 开始构建macOS应用 ===")
# 清理旧的构建
print("清理旧构建...")
for dir_name in ['build', 'dist']:
if os.path.exists(dir_name):
shutil.rmtree(dir_name)
# PyInstaller命令
cmd = [
'pyinstaller',
'--name', 'MagicWord',
'--windowed', # 无控制台
'--icon', 'resources/icons/app_icon_256X256.png', # 256x256图标
'--add-data', 'src/main.py:.', # 主程序
'--add-data', 'src:src', # 整个src目录
'--add-data', 'resources:resources', # 资源目录
'--add-data', 'src/ui/UI.png:ui', # UI.png图片
'--add-data', 'src/ui/114514.png:ui', # 其他UI图片
'--hidden-import', 'PyQt5',
'--hidden-import', 'PyQt5.QtCore',
'--hidden-import', 'PyQt5.QtGui',
'--hidden-import', 'PyQt5.QtWidgets',
'--hidden-import', 'requests',
'--hidden-import', 'PIL',
'--hidden-import', 'PIL.Image',
'--target-architecture', 'arm64',
'--noconfirm',
'--clean',
'src/main.py'
]
if not run_command(cmd):
return False
print("=== 构建完成 ===")
print(f"应用位置: dist/MagicWord.app")
return True
def verify_resources():
"""验证资源文件是否正确打包"""
print("\n=== 验证资源文件 ===")
app_path = "dist/MagicWord.app"
# 检查UI.png
ui_path = os.path.join(app_path, "Contents", "Resources", "ui", "UI.png")
if os.path.exists(ui_path):
size = os.path.getsize(ui_path)
print(f"✅ UI.png 已打包: {size} bytes")
else:
print("❌ UI.png 未找到")
return False
# 检查图标
icon_path = os.path.join(app_path, "Contents", "Resources", "resources", "icons", "app_icon_256X256.png")
if os.path.exists(icon_path):
size = os.path.getsize(icon_path)
print(f"✅ app_icon_256X256.png 已打包: {size} bytes")
else:
print("❌ app_icon_256X256.png 未找到")
return False
# 检查应用图标
app_icon_path = os.path.join(app_path, "Contents", "Resources", "generated-*.icns")
import glob
icon_files = glob.glob(app_icon_path)
if icon_files:
print(f"✅ 应用图标已生成: {len(icon_files)}")
else:
print("⚠️ 应用图标未生成")
return True
if __name__ == "__main__":
if build_app():
verify_resources()
print("\n🎉 macOS应用构建成功")
print("应用位置: dist/MagicWord.app")
print("可以使用: open dist/MagicWord.app 来测试运行")
else:
print("\n❌ 构建失败")
sys.exit(1)

@ -0,0 +1,115 @@
文档标题: 荆棘之恋
创建时间: 2025-11-28 20:00:30
字符数: 3111
行数: 109
==================================================
荆棘之恋
约克郡的荒野上,风是永恒的主人。它呼啸着掠过石南丛生的丘陵,携带着大西洋的咸涩与水汽,将荒原上的每一株草木都塑造成它狂放不羁的模样。在这里,就连时间也仿佛屈服于风的意志,变得缓慢而沉重,如同那些散落在山谷间的古老巨石,承载着数个世纪的沉默与秘密。
在这样的荒野中,荆棘庄园突兀地耸立在一处高地上,像是一位拒绝向岁月低头的倔强老人。庄园的哥特式塔楼已被风雨侵蚀得棱角模糊,石墙上爬满了深色的常春藤,它们在风中微微颤动,宛如城堡起伏的呼吸。远处的天空低垂,铅灰色的云层不断变换形状,偶尔透出的阳光如同探照灯,在荒原上投下转瞬即逝的光斑。
埃德加·洛瑞斯特就站在这片光与影的交界处。他望着眼前这座几乎已被遗忘的庄园,双手不自觉地攥紧。十年的流亡生涯在他脸上刻下了风霜的痕迹,但那对灰色的眼睛里燃烧着的,是比离开时更加炽烈的火焰。
“我回来了,凯瑟琳。”他低语道,声音立刻被呼啸而过的风夺走。
十年前,他还是个身无分文的孤儿,靠着老洛瑞斯特先生的怜悯才在荆棘庄园获得一席之地。他与庄园主人的独女凯瑟琳一起长大,在荒原的每一处角落留下过足迹。他们曾在黎明时分爬上断崖看日出,在月光下的石圈中许下稚嫩的誓言,在温暖的谷仓里分享过第一个颤抖的亲吻。
直到那个决定命运的夜晚。
“你必须离开,埃德加。”老洛瑞斯特的声音冷得像冬夜的岩石,“凯瑟琳要嫁给林顿家的继承人。你配不上她。”
埃德加至今仍能清晰地回忆起凯瑟琳哭泣的声音,像一把钝刀在他的心上反复切割。然后是那个雨夜,他被指控盗窃家族珠宝而被驱逐——这一切,据他后来所知,都是哈里斯·林顿的精心设计。
而今,哈里斯已是荆棘庄园的新主人,而凯瑟琳...埃德加不知道她是否还活着,是否已成了哈里斯的妻子。
一阵马蹄声打断了他的思绪。埃德加迅速躲到一块巨石后面,看着一个身影骑着一匹黑色的母马从庄园方向疾驰而来。当骑手靠近时,他的呼吸几乎停止。
那是凯瑟琳。
岁月将她从一个青涩的少女雕琢成了一位冷峻的美人。她的眼中依然闪烁着埃德加熟悉的那种野性,但如今却被一层坚冰所覆盖。她勒住马缰,停在离他藏身之处不过十几码的地方,眺望着远方的山谷。
埃德安几乎要迈步上前,但一个声音阻止了他。
“凯瑟琳!”一个男人骑着另一匹马从庄园方向赶来,“你应该等我一起。”
哈里斯·林顿。他比埃德加记忆中更加高大英俊,衣着华丽而得体,但眉宇间那种傲慢与占有欲却丝毫未变。
“我需要独处的时间,哈里斯。”凯瑟琳的声音平静无波,目光仍停留在远方的某处。
“这片荒原不安全,你知道的。”哈里斯拉住她的马缰,语气中带着不容置疑的控制欲,“尤其是那些吉普赛人又开始在附近扎营了。”
凯瑟琳微微扬起下巴:“我能照顾自己。”
“就像你能照顾父亲留下的庄园一样?”哈里斯的声音带着一丝讥讽,“我听说东边的佃农又拒绝交租了。你太软弱了,凯瑟琳。这世界只尊重力量。”
埃德加紧紧握住拳头,指甲深深陷入掌心。他几乎要冲出去,将哈里斯从那匹高头大马上拽下来。但他控制住了自己——复仇是一道需要文火慢炖的佳肴,不能急于一时。
当那对夫妇骑马远去,埃德加才从藏身之处走出。风更猛烈了,扯动着他的衣角,仿佛要把他再次推离这片土地。但他站得稳如磐石。
他在附近的一个吉普赛营地暂时安身,用这些年积累的财富换取信息和忠诚。几天后,他得知哈里斯将在周五前往附近的城市处理事务,晚上才会回来。
机会来了。
周五的黄昏,埃德加埋伏在通往庄园必经的小路旁。天空飘起了细雨,将荒原笼罩在一片灰蒙蒙的帷幕中。远处,一只乌鸦站在枯树上发出刺耳的叫声,像是在为即将上演的戏剧报幕。
哈里斯的马车终于出现在小路上。埃德加跨上马,戴好面具,在马车经过弯道减速时冲了出去。
车夫试图反抗,但埃德加轻易地制服了他,用枪指着车厢:“出来,林顿先生。只要你配合,我不会伤害你。”
哈里斯走出车厢,脸上混杂着愤怒与恐惧:“你知道我是谁吗?你敢——”
埃德安没有让他说完,一记精准的击打让他昏了过去。
当哈里斯在荆棘庄园一间废弃的谷仓里醒来时,他发现自己被牢牢绑在一把破旧的椅子上。埃德加坐在他对面,已经摘下了面具。
“你...”哈里斯瞪大眼睛,难以置信地看着眼前这张熟悉又陌生的脸,“埃德加·洛瑞斯特?不可能...他们说你死在美洲了。”
“死神拒绝收留我。”埃德加的声音平静得可怕,“他说我还有未完成的事业。”
哈里斯试图保持镇定,但颤抖的声音出卖了他:“你想要什么?钱?我可以给你钱。”
“钱?”埃德安轻笑一声,站起身在昏暗的谷仓中踱步,“我拥有足够的财富,哈里斯。我回来是为了真相——那个你诬陷我偷窃的夜晚的真相。”
哈里斯的眼神闪烁:“我不知道你在说什么。珠宝确实不见了,而在你的行李中找到了——”
一记响亮的耳光打断了哈里斯的辩解,埃德加的手像铁钳一样捏住他的下巴:“别用谎言侮辱我的智商。十年时间足以让一个人查明真相,也足以让仇恨发酵成毒药。”
谷仓外,风声中隐约夹杂着呼唤哈里斯的聲音。是庄园的搜寻队。
埃德加靠近哈里斯,声音低沉而危险:“我们的游戏才刚刚开始,老朋友。你会为十年前那个夜晚付出代价,我发誓。”
说完,他迅速收拾好东西,消失在谷仓后的秘密通道中。当搜寻队破门而入时,只留下哈里斯和空荡荡的谷仓,还有在风中摇曳的孤灯。
哈里斯被解救回庄园后,对外宣称只是一场意外事故。但埃德加知道,这场游戏已经开始搅动荆棘庄园平静表面下的暗流。
几天后的一个下午,机会再次降临。埃德加在荒野上漫步时,发现凯瑟琳独自一人站在断崖边,下方是汹涌的海浪。她的姿态中有种令人不安的决绝。
当凯瑟琳向前迈出危险的一步时,埃德加毫不犹豫地冲了上去,将她从边缘拉回。
“放开我!”凯瑟琳挣扎着,直到她的目光落在埃德加脸上。那一刻,时间仿佛静止了。她的眼睛睁大,嘴唇微微颤抖:“埃德加?真的是你吗?他们都说你死了...”
“某种意义上,那个年轻的、天真的埃德加确实死了。”他松开手,向后退了一步,“你还好吗,林顿夫人?”
凯瑟琳的脸上掠过一丝痛苦:“请不要这样叫我。”她深吸一口气,“你为什么要回来,埃德加? after all these years?”
“为了答案。”他望向远处波涛汹涌的海面,“为什么?为什么你当年没有为我辩护?为什么你相信那些诬陷我的证据?”
凯瑟琳的眼神复杂难辨:“你永远不会明白,埃德加。有些枷锁比爱情更加牢固。”
就在这时,一阵马蹄声由远及近。哈里斯带着几个仆人赶到了,他的目光在埃德加和凯瑟琳之间来回扫视,脸上酝酿着风暴。
“我就知道,”哈里斯的声音冷得像冰,“那些吉普赛人说看到一个陌生人在附近徘徊。原来是你,你这个贼,不仅偷东西,还想偷走我的妻子!”
凯瑟琳上前一步:“哈里斯,不是你想的那样。我刚才太靠近悬崖,是埃德加救了我。”
哈里斯冷笑一声:“多么巧合。”他转向埃德加,“我警告你,洛瑞斯特,立刻离开约克郡,否则我会让你后悔活着回来。”
埃德加平静地迎上他的目光:“这次我不会逃跑,哈里斯。我有很多问题需要答案,而在得到它们之前,我哪儿也不去。”
两个男人之间的空气仿佛凝固了,仇恨如同实质的火焰在视线交汇处燃烧。
狂风掠过断崖,扯动着三个人的衣襟,将凯瑟琳的低语吹散在风中:“上帝保佑我们所有人。”
埃德加望着眼前这对貌合神离的夫妇,心中复仇的火焰燃烧得更加炽烈。这场游戏已经开始,而他已经掷下了骰子。在约克郡荒芜的天空下,一段被埋葬的过往正缓缓揭开序幕,如同荆棘庄园石墙上的常春藤,注定要将所有人缠绕其中,直至窒息。

@ -9,6 +9,7 @@ import os
import sys
import json
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
@ -18,7 +19,7 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QDesktopWidget, QStyleFactory, QStyle, QGroupBox, QProgressBar,
QScrollArea, QTextBrowser, QFontComboBox, QComboBox, QColorDialog,
QDialog, QLineEdit, QFormLayout, QListWidget, QHBoxLayout,
QCalendarWidget)
QCalendarWidget, QActionGroup)
from PyQt5.QtCore import (Qt, QTimer, pyqtSignal, QThread, QObject, QUrl,
QSettings, QPoint, QSize, QEvent, QPropertyAnimation,
QEasingCurve, QRect)
@ -591,7 +592,7 @@ class MarkTextMainWindow(QMainWindow):
title.setAlignment(Qt.AlignCenter)
# 副标题
subtitle = QLabel("基于MarkText的现代化Markdown编辑器")
subtitle = QLabel("解析·输入·专注")
subtitle.setStyleSheet("""
QLabel {
font-size: 18px;
@ -924,6 +925,73 @@ class MarkTextMainWindow(QMainWindow):
insert_quote_action.triggered.connect(self.insert_quote_to_editor)
tools_menu.addAction(insert_quote_action)
tools_menu.addSeparator()
# AI API密钥配置
ai_config_menu = tools_menu.addMenu('AI配置(&A)')
config_api_key_action = QAction('配置API密钥(&K)...', self)
config_api_key_action.triggered.connect(self.configure_ai_api_key)
ai_config_menu.addAction(config_api_key_action)
ai_config_menu.addSeparator()
# AI服务提供商选择
self.ai_provider_group = QActionGroup(self)
self.ai_provider_group.setExclusive(True)
deepseek_action = QAction('DeepSeek', self)
deepseek_action.setCheckable(True)
deepseek_action.setChecked(True)
deepseek_action.triggered.connect(lambda: self.set_ai_provider('deepseek'))
self.ai_provider_group.addAction(deepseek_action)
ai_config_menu.addAction(deepseek_action)
openai_action = QAction('OpenAI', self)
openai_action.setCheckable(True)
openai_action.triggered.connect(lambda: self.set_ai_provider('openai'))
self.ai_provider_group.addAction(openai_action)
ai_config_menu.addAction(openai_action)
kimi_action = QAction('Kimi', self)
kimi_action.setCheckable(True)
kimi_action.triggered.connect(lambda: self.set_ai_provider('kimi'))
self.ai_provider_group.addAction(kimi_action)
ai_config_menu.addAction(kimi_action)
siliconflow_action = QAction('硅基流动', self)
siliconflow_action.setCheckable(True)
siliconflow_action.triggered.connect(lambda: self.set_ai_provider('siliconflow'))
self.ai_provider_group.addAction(siliconflow_action)
ai_config_menu.addAction(siliconflow_action)
claude_action = QAction('Claude', self)
claude_action.setCheckable(True)
claude_action.triggered.connect(lambda: self.set_ai_provider('claude'))
self.ai_provider_group.addAction(claude_action)
ai_config_menu.addAction(claude_action)
gemini_action = QAction('Gemini', self)
gemini_action.setCheckable(True)
gemini_action.triggered.connect(lambda: self.set_ai_provider('gemini'))
self.ai_provider_group.addAction(gemini_action)
ai_config_menu.addAction(gemini_action)
qwen_action = QAction('通义千问', self)
qwen_action.setCheckable(True)
qwen_action.triggered.connect(lambda: self.set_ai_provider('qwen'))
self.ai_provider_group.addAction(qwen_action)
ai_config_menu.addAction(qwen_action)
ernie_action = QAction('文心一言', self)
ernie_action.setCheckable(True)
ernie_action.triggered.connect(lambda: self.set_ai_provider('ernie'))
self.ai_provider_group.addAction(ernie_action)
ai_config_menu.addAction(ernie_action)
# 加载保存的AI提供商设置
self.load_ai_provider_config()
tools_menu.addSeparator()
# 主题切换功能已移动到主题菜单中
@ -1177,8 +1245,6 @@ class MarkTextMainWindow(QMainWindow):
background-color: #4d4d4d;
}
""")
else:
self.setStyleSheet("")
def get_current_editor(self) -> Optional[MarkTextEditor]:
"""获取当前编辑器"""
@ -2235,6 +2301,291 @@ class MarkTextMainWindow(QMainWindow):
self.weather_label.setText("🌤 天气: 获取失败")
self.quote_label.setText("📖 名言: 获取失败")
def configure_ai_api_key(self):
"""配置AI API密钥"""
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("AI API密钥配置")
dialog.setFixedSize(500, 300)
dialog.setStyleSheet(self.get_dialog_style())
# 创建布局
layout = QVBoxLayout(dialog)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# 服务提供商选择
provider_label = QLabel("服务提供商:")
provider_label.setStyleSheet("color: #ffffff; font-size: 14px;")
provider_combo = QComboBox()
provider_combo.addItems([
"DeepSeek", "OpenAI", "Kimi", "硅基流动",
"Claude", "Gemini", "通义千问", "文心一言"
])
provider_combo.setStyleSheet(self.get_combobox_style())
# 加载当前配置
config = self.load_ai_config()
current_provider = config.get("provider", "DeepSeek")
current_api_key = config.get("api_key", "")
# 设置当前选择
provider_index = provider_combo.findText(current_provider)
if provider_index >= 0:
provider_combo.setCurrentIndex(provider_index)
# API密钥输入
api_key_label = QLabel("API密钥:")
api_key_label.setStyleSheet("color: #ffffff; font-size: 14px;")
api_key_input = QLineEdit()
api_key_input.setText(current_api_key)
api_key_input.setEchoMode(QLineEdit.Password)
api_key_input.setStyleSheet(self.get_line_edit_style())
api_key_input.setPlaceholderText("请输入您的API密钥")
# 按钮布局
button_layout = QHBoxLayout()
button_layout.addStretch()
save_button = QPushButton("保存")
save_button.setStyleSheet(self.get_button_style())
save_button.clicked.connect(lambda: self.save_ai_config(dialog, provider_combo, api_key_input))
cancel_button = QPushButton("取消")
cancel_button.setStyleSheet(self.get_button_style())
cancel_button.clicked.connect(dialog.reject)
button_layout.addWidget(save_button)
button_layout.addWidget(cancel_button)
# 添加到主布局
layout.addWidget(provider_label)
layout.addWidget(provider_combo)
layout.addWidget(api_key_label)
layout.addWidget(api_key_input)
layout.addLayout(button_layout)
# 显示对话框
dialog.exec_()
def save_ai_config(self, dialog, provider_combo, api_key_input):
"""保存AI配置"""
try:
provider = provider_combo.currentText()
api_key = api_key_input.text().strip()
if not api_key:
QMessageBox.warning(dialog, "警告", "API密钥不能为空")
return
# 创建配置
config = {
"provider": provider,
"api_key": api_key,
"updated_at": datetime.now().isoformat()
}
# 确保配置目录存在
config_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', 'config')
os.makedirs(config_dir, exist_ok=True)
# 保存配置文件
config_file = os.path.join(config_dir, 'ai_config.json')
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# 更新状态栏消息
self.statusBar().showMessage(f"AI配置已保存: {provider}", 3000)
# 关闭对话框
dialog.accept()
# 重新加载配置到网络服务
if self.network_service:
self.network_service.load_ai_config()
except Exception as e:
QMessageBox.critical(dialog, "错误", f"保存配置失败: {str(e)}")
def load_ai_config(self):
"""加载AI配置"""
try:
config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', 'config', 'ai_config.json')
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
else:
# 尝试加载DeepSeek配置
deepseek_config = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', 'config', 'deepseek_api.json')
if os.path.exists(deepseek_config):
with open(deepseek_config, 'r', encoding='utf-8') as f:
old_config = json.load(f)
return {
"provider": "DeepSeek",
"api_key": old_config.get("api_key", ""),
"updated_at": datetime.now().isoformat()
}
return {"provider": "DeepSeek", "api_key": "", "updated_at": ""}
except Exception as e:
print(f"加载AI配置失败: {e}")
return {"provider": "DeepSeek", "api_key": "", "updated_at": ""}
def get_dialog_style(self):
"""获取对话框样式"""
return """
QDialog {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
}
QLabel {
color: #ffffff;
font-size: 14px;
}
QLineEdit {
background-color: #4a5568;
color: #ffffff;
border: 1px solid #718096;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
QLineEdit:focus {
border-color: #4299e1;
}
QPushButton {
background-color: #4299e1;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #3182ce;
}
QPushButton:pressed {
background-color: #2c5282;
}
"""
def get_combobox_style(self):
"""获取下拉框样式"""
return """
QComboBox {
background-color: #4a5568;
color: #ffffff;
border: 1px solid #718096;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
QComboBox:focus {
border-color: #4299e1;
}
QComboBox::drop-down {
border: none;
padding-right: 8px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #ffffff;
}
QComboBox QAbstractItemView {
background-color: #4a5568;
color: #ffffff;
border: 1px solid #718096;
border-radius: 4px;
selection-background-color: #4299e1;
}
"""
def get_line_edit_style(self):
"""获取输入框样式"""
return """
QLineEdit {
background-color: #4a5568;
color: #ffffff;
border: 1px solid #718096;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
QLineEdit:focus {
border-color: #4299e1;
}
QLineEdit::placeholder {
color: #a0aec0;
}
"""
def get_button_style(self):
"""获取按钮样式"""
return """
QPushButton {
background-color: #4299e1;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #3182ce;
}
QPushButton:pressed {
background-color: #2c5282;
}
"""
def set_ai_provider(self, provider):
"""设置AI服务提供商"""
try:
# 加载当前配置
config = self.load_ai_config()
config["provider"] = provider
config["updated_at"] = datetime.now().isoformat()
# 保存配置
config_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', 'config')
os.makedirs(config_dir, exist_ok=True)
config_file = os.path.join(config_dir, 'ai_config.json')
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# 更新状态栏
self.statusBar().showMessage(f"AI服务提供商已切换至: {provider}", 3000)
# 重新加载网络服务配置
if self.network_service:
self.network_service.load_ai_config()
except Exception as e:
QMessageBox.critical(self, "错误", f"切换AI服务提供商失败: {str(e)}")
def load_ai_provider_config(self):
"""加载AI提供商配置"""
try:
config = self.load_ai_config()
current_provider = config.get("provider", "DeepSeek")
# 更新菜单中的选中状态
for action in self.ai_provider_group.actions():
if action.text() == current_provider:
action.setChecked(True)
break
except Exception as e:
print(f"加载AI提供商配置失败: {e}")
def closeEvent(self, event):
"""关闭事件"""
# 检查是否有未保存的文件

@ -1,8 +1,8 @@
# services/network_service.py
import requests
import json
import os
import time
import os
import json
from typing import Optional, Dict, Any
class NetworkService:
@ -10,12 +10,21 @@ class NetworkService:
# 实现构造函数逻辑
self.api_key = None
self.ai_provider = "DeepSeek" # 默认AI服务提供商
self.ai_api_key = None # AI API密钥
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 # 缓存的定位信息
self._weather_cache_timestamp = None # 缓存时间戳
# 加载AI配置
self.load_ai_config()
def get_cached_weather_data(self):
"""获取缓存的天气数据"""
@ -25,6 +34,94 @@ class NetworkService:
"""获取缓存的定位信息"""
return self._cached_location
def load_ai_config(self):
"""加载AI配置"""
try:
config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'config', 'ai_config.json')
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.ai_provider = config.get("provider", "DeepSeek")
self.ai_api_key = config.get("api_key", "")
print(f"AI配置已加载: 提供商={self.ai_provider}, API密钥={'已设置' if self.ai_api_key else '未设置'}")
else:
# 尝试加载旧的DeepSeek配置
old_config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'resources', 'config', 'deepseek_api.json')
if os.path.exists(old_config_file):
with open(old_config_file, 'r', encoding='utf-8') as f:
old_config = json.load(f)
self.ai_provider = "DeepSeek"
self.ai_api_key = old_config.get("api_key", "")
print(f"旧的DeepSeek配置已加载: API密钥={'已设置' if self.ai_api_key else '未设置'}")
else:
print("未找到AI配置文件使用默认配置")
self.ai_provider = "DeepSeek"
self.ai_api_key = None
except Exception as e:
print(f"加载AI配置失败: {e}")
self.ai_provider = "DeepSeek"
self.ai_api_key = None
def get_ai_provider_info(self, provider_name):
"""获取AI提供商的详细信息"""
provider_info = {
'deepseek': {
'name': 'DeepSeek',
'base_url': 'https://api.deepseek.com',
'model': 'deepseek-chat',
'description': '深度求索 - 中国AI公司提供高质量对话模型'
},
'openai': {
'name': 'OpenAI',
'base_url': 'https://api.openai.com',
'model': 'gpt-3.5-turbo',
'description': 'OpenAI - GPT系列模型提供商'
},
'kimi': {
'name': 'Kimi',
'base_url': 'https://api.moonshot.cn',
'model': 'moonshot-v1-8k',
'description': '月之暗面 - 支持长文本处理的AI助手'
},
'siliconflow': {
'name': '硅基流动',
'base_url': 'https://api.siliconflow.cn',
'model': 'deepseek-ai/deepseek-llm-67b-chat',
'description': '硅基流动 - 提供多种开源模型服务'
},
'claude': {
'name': 'Claude',
'base_url': 'https://api.anthropic.com',
'model': 'claude-3-sonnet-20240229',
'description': 'Anthropic - Claude系列对话AI'
},
'gemini': {
'name': 'Gemini',
'base_url': 'https://generativelanguage.googleapis.com',
'model': 'gemini-pro',
'description': 'Google - Gemini系列多模态AI模型'
},
'qwen': {
'name': '通义千问',
'base_url': 'https://dashscope.aliyuncs.com',
'model': 'qwen-turbo',
'description': '阿里云 - 通义千问大模型系列'
},
'ernie': {
'name': '文心一言',
'base_url': 'https://aip.baidubce.com',
'model': 'ernie-bot-turbo',
'description': '百度 - 文心一言大语言模型'
}
}
# 如果提供商不存在返回默认的DeepSeek信息
return provider_info.get(provider_name.lower(), provider_info['deepseek'])
def get_current_ai_provider_info(self):
"""获取当前AI提供商的详细信息"""
return self.get_ai_provider_info(self.ai_provider)
def set_weather_cache(self, weather_data, location):
"""设置天气缓存"""
self._cached_weather_data = weather_data
@ -43,39 +140,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 +234,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 +279,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 +379,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 = {

@ -1,6 +1,7 @@
# ai_chat_panel.py - AI对话面板组件
import json
import os
import sys
import requests
import threading
from datetime import datetime
@ -12,13 +13,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,24 +35,44 @@ 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密钥"""
config_file = os.path.join(
os.path.dirname(__file__),
"..", "..", "resources", "config", "deepseek_api.json"
)
try:
if os.path.exists(config_file):
# 尝试多种路径,适应打包前后的不同情况
possible_paths = [
# 开发模式路径
os.path.join(os.path.dirname(__file__), "..", "..", "resources", "config", "deepseek_api.json"),
# 打包后路径(相对于可执行文件)
os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "config", "deepseek_api.json"),
# 使用当前工作目录
os.path.join(os.getcwd(), "resources", "config", "deepseek_api.json"),
# 使用sys._MEIPASSPyInstaller打包后的临时目录
os.path.join(getattr(sys, '_MEIPASS', os.getcwd()), "resources", "config", "deepseek_api.json"),
]
config_file = None
for path in possible_paths:
if os.path.exists(path):
config_file = path
break
# 调试信息
print(f"尝试加载配置文件,可能的候选路径:")
for i, path in enumerate(possible_paths):
print(f" {i+1}. {path} - 存在: {os.path.exists(path)}")
if config_file:
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.api_key = config.get('api_key', '')
except Exception as e:
print(f"加载API密钥失败: {e}")
print(f"成功从 {config_file} 加载API密钥: {self.api_key[:10]}..." if self.api_key else "API密钥为空")
except Exception as e:
print(f"加载API密钥失败: {e}")
else:
print(f"未找到配置文件")
print(f"最终API密钥状态: {'已设置' if self.api_key else '未设置'}")
def init_ui(self):
"""初始化UI"""
@ -73,6 +87,7 @@ class AIChatPanel(QWidget):
header_font.setBold(True)
header_font.setPointSize(11)
header_label.setFont(header_font)
header_label.setStyleSheet("color: white;") # 设置标题为白色
header_layout.addWidget(header_label)
header_layout.addStretch()
@ -84,9 +99,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 +109,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 +124,7 @@ class AIChatPanel(QWidget):
# 分割线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setStyleSheet("color: #d0d0d0;")
line.setStyleSheet("color: #3c3c3c;")
main_layout.addWidget(line)
# 对话显示区域
@ -132,21 +132,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 +154,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 +176,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 +194,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 +207,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 +254,8 @@ class AIChatPanel(QWidget):
def on_theme_changed(self, is_dark):
"""主题变化处理"""
self.apply_theme()
# 不响应主题变化,始终保持深色模式
pass
def send_user_message(self):
"""发送用户消息"""
@ -344,28 +308,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 +350,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 +366,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 +475,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