fix:修复一些BUG

This commit is contained in:
2025-12-13 19:44:27 +08:00
parent 9d347f9bc9
commit 93d1e8687a
18 changed files with 584 additions and 638 deletions

View File

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4"> <module version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (growth_report) (2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings"> <component name="PyDocumentationSettings">
<option name="format" value="PLAIN" /> <option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" /> <option name="myDocStringFormat" value="Plain" />
@@ -18,4 +11,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module> </module>

4
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black"> <component name="Black">
<option name="sdkName" value="uv (growth_report) (2)" /> <option name="sdkName" value="uv (growth_report)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (growth_report) (2)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="uvgrowth_report" project-jdk-type="Python SDK" />
</project> </project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/growth_report.iml" filepath="$PROJECT_DIR$/.idea/growth_report.iml" />
</modules>
</component>
</project>

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
</component> </component>
</project> </project>

95
.idea/workspace.xml generated
View File

@@ -4,19 +4,46 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="fix:更新代码开源协议" /> <list default="true" id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="">
<change afterPath="$PROJECT_DIR$/script/setup.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/growth_report.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/growth_report.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config.env.toml" beforeDir="false" afterPath="$PROJECT_DIR$/config.env.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/data/names.xlsx" beforeDir="false" afterPath="$PROJECT_DIR$/data/names.xlsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.pyw" beforeDir="false" afterPath="$PROJECT_DIR$/main.pyw" afterDir="false" />
<change beforePath="$PROJECT_DIR$/old/main.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/public/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/public/icon.ico" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/app_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/app_window.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/font_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/font_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/generate_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/generate_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/growt_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/growt_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/pdf_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/pdf_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" /> <option name="LAST_RESOLUTION" value="IGNORE" />
</component> </component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="ProjectColorInfo"><![CDATA[{ <component name="ProjectColorInfo">{
"associatedIndex": 0 &quot;customColor&quot;: &quot;&quot;,
}]]></component> &quot;associatedIndex&quot;: 0
<component name="ProjectId" id="36hYCM0j8RdgslpqW2LFtFj8NEK" /> }</component>
<component name="ProjectId" id="36mY8RowM4y8kcGPt6V9WQiPGN9" />
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
@@ -24,12 +51,12 @@
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent"><![CDATA[{
"keyToString": { "keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true", "ModuleVcsDetector.initialDetectionPerformed": "true",
"Python.UI.executor": "Run", "Python.main.pyw.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master", "git-widget-placeholder": "master",
"last_opened_file_path": "D:/working/tools/growth_report", "last_opened_file_path": "D:/working/tools/growth_report/public",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",
@@ -39,8 +66,13 @@
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
} }
}]]></component> }]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\working\tools\growth_report\public" />
</key>
</component>
<component name="RunManager"> <component name="RunManager">
<configuration name="UI" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="main.pyw" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="growth_report" /> <module name="growth_report" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
@@ -54,7 +86,7 @@
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/UI.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.pyw" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />
@@ -65,56 +97,27 @@
</configuration> </configuration>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Python.UI" /> <item itemvalue="Python.main.pyw" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
<component name="TaskManager"> <component name="TaskManager">
<task active="true" id="Default" summary="默认任务"> <task active="true" id="Default" summary="默认任务">
<changelist id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="" /> <changelist id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="" />
<created>1765460141811</created> <created>1765613055475</created>
<option name="number" value="Default" /> <option name="number" value="Default" />
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1765460141811</updated> <updated>1765613055475</updated>
<workItem from="1765460142948" duration="1948000" /> <workItem from="1765613057798" duration="372000" />
<workItem from="1765613448098" duration="48000" />
<workItem from="1765613503892" duration="10202000" />
</task> </task>
<task id="LOCAL-00001" summary="fix:优化一些BUG">
<option name="closed" value="true" />
<created>1765460523294</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1765460523294</updated>
</task>
<task id="LOCAL-00002" summary="fix:优化配置项目">
<option name="closed" value="true" />
<created>1765460614548</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1765460614548</updated>
</task>
<task id="LOCAL-00003" summary="fix:更新代码开源协议">
<option name="closed" value="true" />
<created>1765460996031</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1765460996031</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="VcsManagerConfiguration">
<MESSAGE value="fix:优化一些BUG" />
<MESSAGE value="fix:优化配置项目" />
<MESSAGE value="fix:更新代码开源协议" />
<option name="LAST_COMMIT_MESSAGE" value="fix:更新代码开源协议" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/growth_report$UI.coverage" NAME="UI 覆盖结果" MODIFIED="1765460384581" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/growth_report$main_pyw.coverage" NAME="main.pyw 覆盖结果" MODIFIED="1765626060279" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component> </component>
</project> </project>

View File

@@ -4,9 +4,9 @@ source_file = "大班幼儿学期发展报告.pptx"
# 输出文件夹 # 输出文件夹
output_folder = "output" output_folder = "output"
# Excel数据文件路径 # Excel数据文件路径
excel_file = "data/names.xlsx" excel_file = "names.xlsx"
# 图片资源文件夹 # 图片资源文件夹
image_folder = "data/images" image_folder = "images"
# 字体文件夹 # 字体文件夹
fonts_dir = "fonts" fonts_dir = "fonts"

View File

@@ -12,30 +12,46 @@ except ImportError:
sys.exit(1) sys.exit(1)
def get_main_path(): def get_base_dir():
""" """
获取程序运行的根目录 获取程序运行的基准目录 (即 EXE 所在的目录 或 开发环境的项目根目录)
兼容: 用于确定 output_folder 等需要写入的路径。
1. PyInstaller 打包后的 .exe 环境
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
""" """
if getattr(sys, "frozen", False): if getattr(sys, 'frozen', False):
# --- 情况 A: 打包后的 exe --- # 打包环境: EXE 所在目录
# exe 就在根目录下,直接取 exe 所在目录
return os.path.dirname(sys.executable) return os.path.dirname(sys.executable)
else: else:
# --- 情况 B: 开发环境 (.py) --- # 开发环境: 项目根目录 (假设此脚本在 utils/ 文件夹中,需要向上两级)
# 1. 获取当前脚本的绝对路径 (例如: .../MyProject/utils/config_loader.py) return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
current_file_path = os.path.abspath(__file__)
# 2. 获取当前脚本所在的文件夹 (例如: .../MyProject/utils)
current_dir = os.path.dirname(current_file_path)
# 3. 【关键修改】再往上一层,获取项目根目录 (例如: .../MyProject) def get_resource_path(relative_path):
# 如果你的脚本藏得更深,就再套一层 os.path.dirname """
project_root = os.path.dirname(current_dir) 智能路径获取:
1. 优先检查 EXE 旁边是否有该文件 (外部资源)。
2. 如果没有,则使用 EXE 内部打包的资源 (内部资源)。
"""
# 1. 获取外部基准路径
base_path = get_base_dir()
return project_root # 拼接外部路径
external_path = os.path.join(base_path, relative_path)
# 如果外部文件存在,直接返回 (优先使用用户修改过的文件)
if os.path.exists(external_path):
return external_path
# 2. 如果外部不存在,且处于打包环境,则回退到内部临时目录 (sys._MEIPASS)
if getattr(sys, 'frozen', False):
# sys._MEIPASS 是 PyInstaller 解压临时文件的目录
internal_path = os.path.join(sys._MEIPASS, relative_path)
if os.path.exists(internal_path):
return internal_path
# 3. 默认返回外部路径
# (如果都没找到,让报错信息指向外部路径,提示用户文件缺失)
return external_path
# ========================================== # ==========================================
@@ -44,32 +60,58 @@ def get_main_path():
def load_config(config_filename="config.toml"): def load_config(config_filename="config.toml"):
"""读取 TOML 配置文件""" """读取 TOML 配置文件"""
# 1. 先获取正确的根目录 # 1. 智能获取配置文件路径
main_dir = get_main_path() # (优先找 EXE 旁边的 config.toml找不到则用打包在里面的)
config_path = get_resource_path(config_filename)
# 2. 拼接配置文件的绝对路径 (防止在不同目录下运行脚本时找不到配置文件)
config_path = os.path.join(main_dir, config_filename)
if not os.path.exists(config_path): if not os.path.exists(config_path):
print(f"错误: 在路径 {main_dir}找不到配置文件 {config_filename}") print(f"错误: 找不到配置文件 {config_filename}")
print(f"尝试寻找的完整路径是: {config_path}") print(f"尝试寻找的路径是: {config_path}")
# 如果是打包环境,提示用户可能需要把 config.toml 复制出来
if getattr(sys, 'frozen', False):
print("提示: 请确保 config.toml 位于程序同级目录下,或已正确打包。")
sys.exit(1) sys.exit(1)
try: try:
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
data = toml.load(f) data = toml.load(f)
# 获取基准目录(用于 output_folder
base_dir = get_base_dir()
# 将 TOML 的层级结构映射回扁平结构 # 将 TOML 的层级结构映射回扁平结构
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录) # ⚠️ 注意:
# - 读取类文件 (模板, Excel, 图片, 字体) 使用 get_resource_path (支持内外回退)
# - 写入类文件夹 (output_folder) 使用 os.path.join(base_dir, ...) (必须在外部)
config = { config = {
"root_path": main_dir, # 方便调试,把根目录也存进去 "root_path": base_dir,
"source_file": os.path.join(
main_dir, "templates", data["paths"]["source_file"] # --- 资源文件 (使用智能路径) ---
# 假设 config.toml 里写的是 "report_template.pptx",文件在 templates 文件夹下
"source_file": get_resource_path(
os.path.join("templates", data["paths"]["source_file"])
), ),
"output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
"excel_file": os.path.join(main_dir, data["paths"]["excel_file"]), # 假设 config.toml 里写的是 "names.xlsx",文件在 data 文件夹下
"image_folder": os.path.join(main_dir, data["paths"]["image_folder"]), # 如果 config.toml 里写的是 "data/names.xlsx",则不需要 os.path.join("data", ...)
"fonts_dir": os.path.join(main_dir, data["paths"]["fonts_dir"]), "excel_file": get_resource_path(
os.path.join("data", data["paths"]["excel_file"])
),
"image_folder": get_resource_path(
os.path.join("data", data["paths"]["image_folder"])
),
"fonts_dir": get_resource_path(
os.path.join(data["paths"]["fonts_dir"])
),
# --- 输出文件夹 (必须强制在外部,不能指向临时目录) ---
"output_folder": os.path.join(base_dir, data["paths"]["output_folder"]),
# --- 其他配置 ---
"class_name": data["class_info"]["class_name"], "class_name": data["class_info"]["class_name"],
"teachers": data["class_info"]["teachers"], "teachers": data["class_info"]["teachers"],
"default_comment": data["defaults"].get("default_comment", "暂无评语"), "default_comment": data["defaults"].get("default_comment", "暂无评语"),
@@ -77,6 +119,12 @@ def load_config(config_filename="config.toml"):
"ai": data["ai"], "ai": data["ai"],
} }
return config return config
except KeyError as e:
print(f"配置文件格式错误,缺少键值: {e}")
sys.exit(1)
except Exception as e: except Exception as e:
print(f"读取配置文件出错: {e}") print(f"读取配置文件出错: {e}")
import traceback
traceback.print_exc()
sys.exit(1) sys.exit(1)

Binary file not shown.

View File

@@ -1,23 +1,81 @@
import sys
import tkinter as tk import tkinter as tk
from utils.log_handler import setup_logging
from ui.app_window import ReportApp
from loguru import logger from loguru import logger
def main(): from ui.app_window import ReportApp
# 1. 初始化日志 from utils.log_handler import setup_logging
setup_logging()
logger.info("正在启动应用程序...")
# 2. 启动 UI # 全局变量,用于判断日志是否已初始化
root = tk.Tk() LOGGING_INITIALIZED = False
# 这一行可以设置图标 (如果有 icon.ico 文件)
# root.iconbitmap("icon.ico")
app = ReportApp(root) # --- 全局错误处理 ---
def handle_exception(exc_type, exc_value, exc_traceback):
"""
捕获未被 try/except 块处理的全局异常(如线程崩溃)。
"""
if exc_type is KeyboardInterrupt:
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
# 尝试使用 loguru 记录
if LOGGING_INITIALIZED:
logger.error("捕获到未处理的全局异常:", exc_info=(exc_type, exc_value, exc_traceback))
else:
# 如果日志系统未初始化,直接打印到标准错误流,确保用户看到
print("FATAL ERROR (Log Not Initialized):", file=sys.stderr)
import traceback
traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stderr)
sys.excepthook = handle_exception
# --------------------
def create_main_window():
global LOGGING_INITIALIZED
# 顶级 try 块,捕获日志初始化阶段的错误
try:
# 1. 初始化日志
setup_logging()
LOGGING_INITIALIZED = True
logger.info("正在启动应用程序...")
# 2. 启动 UI
root = tk.Tk()
# 这一行可以设置图标 (如果有 icon.ico 文件)
# root.iconbitmap(os.path.join(os.path.dirname(__file__), "public", "icon.ico"))
# 确保 ReportApp 实例化时不会出现路径错误
app = ReportApp(root)
# 3. 进入主循环
root.mainloop()
except Exception as e:
# 如果日志系统已启动,使用 logger 记录
if LOGGING_INITIALIZED:
logger.error(f"应用程序启动/主循环出错: {e}", exc_info=True)
else:
# 如果日志系统未初始化,直接打印到控制台
print(f"FATAL STARTUP ERROR: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# 确保窗口被销毁
if 'root' in locals() and root:
root.destroy()
# 非窗口模式下,在启动错误时等待用户查看
if not getattr(sys, 'frozen', False) or not any(arg in sys.argv for arg in ('--windowed', '-w')):
input("按任意键退出...")
sys.exit(1)
# 3. 进入主循环
root.mainloop()
if __name__ == "__main__": if __name__ == "__main__":
main() create_main_window()

View File

@@ -1,415 +0,0 @@
import os
import platform
import shutil
import sys
import time
from pathlib import Path
import pandas as pd
from pptx import Presentation
from pptx.util import Pt
def get_system_fonts():
"""获取系统中可用的字体列表"""
fonts = set()
if platform.system() == "Windows":
try:
# 读取Windows字体目录
system_fonts_dir = Path(os.environ['WINDIR']) / 'Fonts'
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
# 检查系统字体目录
if system_fonts_dir.exists():
for font_file in system_fonts_dir.glob('*.ttf'):
fonts.add(font_file.stem)
for font_file in system_fonts_dir.glob('*.ttc'):
fonts.add(font_file.stem)
for font_file in system_fonts_dir.glob('*.otf'):
fonts.add(font_file.stem)
# 检查用户字体目录
if user_fonts_dir.exists():
for font_file in user_fonts_dir.glob('*.ttf'):
fonts.add(font_file.stem)
for font_file in user_fonts_dir.glob('*.ttc'):
fonts.add(font_file.stem)
for font_file in user_fonts_dir.glob('*.otf'):
fonts.add(font_file.stem)
except Exception as e:
print(f"读取系统字体时出错: {e}")
# 备选方案:返回常见字体
fonts = {"微软雅黑", "宋体", "黑体", "楷体", "仿宋", "Arial", "Times New Roman", "Courier New",
"Microsoft YaHei"}
return fonts
def is_font_available(font_name):
"""检查字体是否在系统中可用"""
system_fonts = get_system_fonts()
# 检查字体名称的多种可能形式
check_names = [font_name, font_name.replace(" ", ""), font_name.replace("-", ""), font_name.lower(),
font_name.upper()]
for name in check_names:
if name in system_fonts:
return True
return False
def install_fonts_from_directory(fonts_dir="fonts"):
"""从指定目录安装字体到系统"""
if platform.system() != "Windows":
print("字体安装功能目前仅支持Windows系统")
return False
# 获取系统字体目录
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
# 优先使用用户字体目录(不需要管理员权限)
target_font_dir = user_fonts_dir
# 创建目标目录(如果不存在)
target_font_dir.mkdir(parents=True, exist_ok=True)
# 检查字体目录是否存在
if not os.path.exists(fonts_dir):
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
return False
# 遍历字体目录中的字体文件
font_extensions = ['.ttf', '.ttc', '.otf', '.fon']
font_files = []
for ext in font_extensions:
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
if not font_files:
print(f"{fonts_dir} 目录中未找到字体文件 (.ttf, .ttc, .otf, .fon)")
return False
# 安装字体文件
installed_fonts = []
for font_file in font_files:
try:
# 复制字体文件到系统字体目录
target_path = target_font_dir / font_file.name
if not target_path.exists(): # 避免重复安装
shutil.copy2(font_file, target_path)
print(f"已安装字体: {font_file.name}")
installed_fonts.append(font_file.name)
else:
pass
# print(f"字体已存在: {font_file.name}") # 减少日志输出
except Exception as e:
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
continue
if installed_fonts:
print(f"共安装了 {len(installed_fonts)} 个新字体文件")
print("注意新安装的字体可能需要重启Python环境后才能在PowerPoint中使用")
return True
else:
print("没有安装新字体文件,可能已存在于系统中")
return False
def replace_text_in_slide(prs, slide_index, placeholder, text):
"""
在指定幻灯片中替换指定占位符的文本,并保持原有格式。
:param prs: Presentation 对象
:param slide_index: 要操作的幻灯片索引从0开始
:param placeholder: 占位符名称 (shape.name)
:param text: 要替换的文本
"""
slide = prs.slides[slide_index]
for shape in slide.shapes:
if shape.name == placeholder:
if not shape.has_text_frame:
continue
# --- 1. 保存原有格式信息 ---
original_paragraph_formats = []
for paragraph in shape.text_frame.paragraphs:
paragraph_format = {
'alignment': paragraph.alignment,
'space_before': getattr(paragraph, 'space_before', None),
'space_after': getattr(paragraph, 'space_after', None),
'line_spacing': getattr(paragraph, 'line_spacing', None),
# [修复] 补充读取缩进属性,防止后面恢复时 KeyError
'left_indent': getattr(paragraph, 'left_indent', None),
'right_indent': getattr(paragraph, 'right_indent', None),
'first_line_indent': getattr(paragraph, 'first_line_indent', None),
'font_info': []
}
for run in paragraph.runs:
run_format = {
'font_name': run.font.name,
'font_size': run.font.size,
'bold': run.font.bold,
'italic': run.font.italic,
'underline': run.font.underline,
'color': run.font.color,
'character_space': getattr(run.font, 'space', None),
'all_caps': getattr(run.font, 'all_caps', None),
'small_caps': getattr(run.font, 'small_caps', None)
}
paragraph_format['font_info'].append(run_format)
original_paragraph_formats.append(paragraph_format)
# --- 2. 设置新文本内容 ---
shape.text = text
# --- 3. 恢复原有格式 ---
for i, paragraph in enumerate(shape.text_frame.paragraphs):
# 如果新文本段落数超过原格式数量,使用最后一个格式或默认格式
orig_idx = i if i < len(original_paragraph_formats) else -1
if not original_paragraph_formats: break # 防止空列表
original_para = original_paragraph_formats[orig_idx]
# 恢复段落格式
if original_para['alignment'] is not None:
paragraph.alignment = original_para['alignment']
if original_para['space_before'] is not None:
paragraph.space_before = original_para['space_before']
if original_para['space_after'] is not None:
paragraph.space_after = original_para['space_after']
if original_para['line_spacing'] is not None:
paragraph.line_spacing = original_para['line_spacing']
if original_para['left_indent'] is not None:
paragraph.left_indent = original_para['left_indent']
if original_para['right_indent'] is not None:
paragraph.right_indent = original_para['right_indent']
if original_para['first_line_indent'] is not None:
paragraph.first_line_indent = original_para['first_line_indent']
# 恢复字体格式 (尽量应用到所有 runs)
# 注意shape.text = text 会把所有内容变成一个 run但也可能有多个
for j, run in enumerate(paragraph.runs):
# 通常取第一个run的格式或者按顺序取
font_idx = j if j < len(original_para['font_info']) else 0
if not original_para['font_info']: break
original_font = original_para['font_info'][font_idx]
# 字体名称
if original_font['font_name'] is not None:
font_name = original_font['font_name']
if is_font_available(font_name):
run.font.name = font_name
else:
run.font.name = "微软雅黑"
# print(f"警告: 字体 '{font_name}' 不可用,已替换")
# 其他属性
if original_font['font_size'] is not None:
run.font.size = original_font['font_size']
if original_font['bold'] is not None:
run.font.bold = original_font['bold']
if original_font['italic'] is not None:
run.font.italic = original_font['italic']
if original_font['underline'] is not None:
run.font.underline = original_font['underline']
if original_font['character_space'] is not None:
try:
run.font.space = original_font['character_space']
except:
pass
if original_font['all_caps'] is not None:
run.font.all_caps = original_font['all_caps']
# 颜色处理
if original_font['color'] is not None:
try:
# 仅当它是RGB类型时才复制主题色可能导致报错
if hasattr(original_font['color'], 'rgb'):
run.font.color.rgb = original_font['color'].rgb
except:
pass
def replace_picture(prs, slide_index, placeholder, img_path):
"""
在指定幻灯片中替换指定占位符的图片。
:param prs: Presentation 对象
:param slide_index: 要操作的幻灯片索引从0开始
:param placeholder: 占位符名称
:param img_path: 要替换的图片路径
"""
if not os.path.exists(img_path):
print(f"警告: 图片路径不存在 {img_path}")
return
slide = prs.slides[slide_index]
sp_tree = slide.shapes._spTree
# 先找到要替换的形状及其索引位置
for i, shape in enumerate(slide.shapes):
if shape.name == placeholder:
# 保存旧形状的位置信息
left = shape.left
top = shape.top
width = shape.width
height = shape.height
# 从幻灯片中移除旧图片占位符
sp_tree.remove(shape._element)
# 添加新图片
new_shape = slide.shapes.add_picture(img_path, left, top, width, height)
# 插入新图片到旧形状原来的位置
sp_tree.insert(i, new_shape._element)
break
if __name__ == "__main__":
# --- 1. 资源准备 ---
# 安装字体如果存在fonts目录
if install_fonts_from_directory("../fonts"):
print("等待系统识别新安装的字体...")
time.sleep(2)
source_file = r"../templates/大班幼儿学期发展报告.pptx"
output_folder = "output"
excel_file = os.path.join("../data/names.xlsx")
image_folder = os.path.join("../data/images")
# 创建输出文件夹
os.makedirs(output_folder, exist_ok=True)
# 检查源文件是否存在
if not os.path.exists(source_file):
print(f"错误: 找不到模版文件 {source_file}")
sys.exit(1)
if not os.path.exists(excel_file):
print(f"错误: 找不到数据文件 {excel_file}")
sys.exit(1)
# --- 2. 定义辅助函数 (显式传入 prs) ---
def replace_one_page(current_prs, name, class_name):
"""替换第一页信息"""
replace_text_in_slide(current_prs, 0, "name", name)
replace_text_in_slide(current_prs, 0, "class", class_name)
def replace_two_page(current_prs, comments, teacher_name):
"""替换第二页信息"""
replace_text_in_slide(current_prs, 1, "comments", comments)
replace_text_in_slide(current_prs, 1, "teacher_name", teacher_name)
def replace_three_page(current_prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image):
"""替换第三页信息
:param current_prs: Presentation对象当前演示文稿
:param name: 学生姓名
:param english_name: 学生英文名
:param sex: 性别
:param birthday: 生日
:param zodiac: 生肖
:param friend: 好朋友
:param hobby: 爱好
:param game: 爱玩的游戏
:param food: 爱吃的食物
:param me_image: 自己的照片
"""
replace_text_in_slide(current_prs, 2, "name", name)
replace_text_in_slide(current_prs, 2, "english_name", english_name)
replace_text_in_slide(current_prs, 2, "sex", sex)
replace_text_in_slide(current_prs, 2, "birthday", birthday)
replace_text_in_slide(current_prs, 2, "zodiac", zodiac)
replace_text_in_slide(current_prs, 2, "friend", friend)
replace_text_in_slide(current_prs, 2, "hobby", hobby)
replace_text_in_slide(current_prs, 2, "game", game)
replace_text_in_slide(current_prs, 2, "food", food)
replace_picture(current_prs, 2, "me_image", me_image)
def replace_four_page(current_prs, class_image):
"""替换第四页信息"""
replace_picture(current_prs, 3, "class_image", class_image)
def replace_five_page(current_prs, image1, image2):
"""替换第五页信息"""
replace_picture(current_prs, 4, "image1", image1)
replace_picture(current_prs, 4, "image2", image2)
# --- 3. 读取数据并处理 ---
try:
df = pd.read_excel(excel_file, sheet_name="Sheet1")
datas = df[["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
"评价"]].values.tolist()
class_name = "K4D"
teacher_name = ["简蜜", "王敏千", "李玉香"]
print(f"开始处理,共 {len(datas)} 位学生...")
# --- 4. 循环处理 ---
for i, (name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) in enumerate(datas):
# 班级图片
print(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# [修复] 每次循环重新加载模版,保证文件干净
prs = Presentation(source_file)
# 替换第一页内容
replace_one_page(prs, name, class_name)
# 替换第二页内容
teacher_names = " ".join(teacher_name)
replace_two_page(prs, comments, teacher_names)
# 替换第三页内容
student_image_folder = os.path.join(image_folder, name)
if os.path.exists(student_image_folder):
me_image = os.path.join(student_image_folder, "me_image.jpg")
replace_three_page(prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image)
else:
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# 替换第四页内容
class_image = os.path.join(image_folder, class_name + ".jpg")
if os.path.exists(class_image):
replace_four_page(prs, class_image)
else:
print(f"错误: 班级图片文件不存在 {class_image}")
continue
# 替换第五页内容
if os.path.exists(student_image_folder):
image1 = os.path.join(student_image_folder, "1.jpg")
image2 = os.path.join(student_image_folder, "1.jpg")
replace_five_page(prs, image1, image2)
else:
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# 获取文件拓展名
file_ext = os.path.splitext(source_file)[1]
safe_name = str(name).strip() # 去除可能存在的空格
new_filename = f"{class_name} {safe_name} 幼儿成长报告{file_ext}"
output_path = os.path.join(output_folder, new_filename)
# 保存
try:
prs.save(output_path)
except PermissionError:
print(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
print("\n所有报告生成完毕!")
except Exception as e:
print(f"程序运行出错: {str(e)}")
import traceback
traceback.print_exc()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 74 KiB

109
script/setup.py Normal file
View File

@@ -0,0 +1,109 @@
# setup.py
import os
import subprocess
import sys
import shutil
def copy_resources():
"""
将资源文件从项目根目录复制到 dist 文件夹中,
以便用户可以直接在 exe 旁边修改这些文件。
"""
# 1. 定义路径
# setup.py 所在的目录 (script/)
current_dir = os.path.dirname(os.path.abspath(__file__))
# 项目根目录 (script/ 的上一级)
project_root = os.path.dirname(current_dir)
# 输出目录 (script/dist)
dist_dir = os.path.join(current_dir, "dist")
print(f"\n--- 正在复制外部资源到 {dist_dir} ---")
if not os.path.exists(dist_dir):
print("错误: dist 文件夹不存在,请先运行打包。")
return
# 2. 定义要复制的资源清单
# 格式: (源路径相对root, 目标文件夹相对dist)
# 如果目标是根目录,用 "" 表示
resources_to_copy = [
("config.toml", ""), # 复制 config.toml 到 dist/
("fonts", "fonts"), # 复制 fonts 文件夹 到 dist/fonts
("data", "data"), # 复制 data 文件夹 到 dist/data
("templates", "templates"), # 复制 templates 文件夹 到 dist/templates
("public", "public"), # 复制 public 文件夹 到 dist/public
]
for src_name, dest_name in resources_to_copy:
src_path = os.path.join(project_root, src_name)
dest_path = os.path.join(dist_dir, dest_name)
try:
if os.path.isfile(src_path):
# --- 复制文件 ---
shutil.copy2(src_path, dest_path) # copy2 保留文件元数据
print(f"✅ 已复制文件: {src_name}")
elif os.path.isdir(src_path):
# --- 复制文件夹 ---
# dirs_exist_ok=True 允许覆盖已存在的目录 (Python 3.8+)
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
print(f"✅ 已复制目录: {src_name}")
else:
print(f"⚠️ 警告: 源文件不存在,跳过: {src_path}")
except Exception as e:
print(f"❌ 复制失败 {src_name}: {e}")
def build_exe():
"""使用 PyInstaller 打包 main_app.py"""
# --- 内部资源 (打入包内的资源) ---
# 即使我们在外部复制了一份,为了保证 exe 独立运行(万一外部文件被删),
# 建议依然保留这些作为“默认出厂设置”打入包内。
resource_paths = [
"--add-data=../config.toml:.",
"--add-data=../fonts:fonts",
"--add-data=../data:data",
"--add-data=../templates:templates",
"--add-data=../public:public",
]
try:
command = [
sys.executable, "-m", "PyInstaller",
"--onefile",
"--windowed", # 调试阶段建议先注释掉,确认无误后再开启
"--name=尚城幼儿园幼儿学期发展报告",
"--icon=../public/icon.ico",
"../main.pyw"
]
# 添加资源参数
command.extend(resource_paths)
print("--- 开始打包 (PyInstaller) ---")
# 运行 PyInstaller
subprocess.run(command, check=True, cwd=os.path.dirname(os.path.abspath(__file__)))
print("\n--- PyInstaller 打包完成!---")
# === 执行资源复制 ===
copy_resources()
print(f"\n🎉 全部完成!请查看 'dist' 文件夹。")
except subprocess.CalledProcessError as e:
print(f"\n--- 打包失败 ---")
print(f"命令执行出错: {e}")
except FileNotFoundError:
print(f"\n--- 打包失败 ---")
print("错误:找不到 PyInstaller。")
if __name__ == "__main__":
build_exe()

View File

@@ -1,3 +1,5 @@
import os
import time
import tkinter as tk import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog from tkinter import ttk, scrolledtext, messagebox, filedialog
import threading import threading
@@ -6,6 +8,7 @@ import sys
from loguru import logger from loguru import logger
from config.config import load_config from config.config import load_config
from utils.font_utils import install_fonts_from_directory
from utils.log_handler import log_queue from utils.log_handler import log_queue
# 导入业务逻辑 # 导入业务逻辑
@@ -20,6 +23,7 @@ from utils.file_utils import export_templates_folder, initialize_project, export
config = load_config("config.toml") config = load_config("config.toml")
class ReportApp: class ReportApp:
def __init__(self, root): def __init__(self, root):
self.root = root self.root = root
@@ -30,7 +34,24 @@ class ReportApp:
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.is_running = False self.is_running = False
self._setup_ui() # 尝试初始化 UI
try:
self._setup_ui()
except Exception as e:
logger.critical(f"UI 初始化失败: {e}", exc_info=True)
messagebox.showerror("致命错误", f"界面初始化失败,请检查日志。\n错误: {e}")
self.root.destroy()
sys.exit(1)
# 尝试初始化项目资源
try:
self.init_project()
except Exception as e:
logger.critical(f"项目资源初始化失败: {e}", exc_info=True)
messagebox.showerror("致命错误", f"项目资源初始化失败,请检查日志。\n错误: {e}")
self.root.destroy()
sys.exit(1)
self._start_log_polling() self._start_log_polling()
def _setup_ui(self): def _setup_ui(self):
@@ -48,9 +69,21 @@ class ReportApp:
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack() ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
# 2. 功能区容器 # 2. 功能区容器
main_content = ttk.Frame(self.root, padding=10) main_content = ttk.Frame(self.root, padding="10 15 10 5")
main_content.pack(fill=tk.X) main_content.pack(fill=tk.X)
# === 进度条区域 ===
progress_frame = ttk.Frame(self.root, padding="10 15 10 5")
progress_frame.pack(fill=tk.X, pady=(0, 10))
# 进度条 Label
self.progress_label = ttk.Label(progress_frame, text="⛳ 任务进度: 待命", font=("微软雅黑", 10))
self.progress_label.pack(fill=tk.X, pady=(0, 2))
# 进度条
self.progressbar = ttk.Progressbar(progress_frame, orient="horizontal", mode="determinate")
self.progressbar.pack(fill=tk.X, expand=True)
# === A组: 核心功能 === # === A组: 核心功能 ===
self._create_btn_group(main_content, "🛠️ 核心功能", [ self._create_btn_group(main_content, "🛠️ 核心功能", [
("📁 生成图片路径", lambda: self.run_task(generate_template)), ("📁 生成图片路径", lambda: self.run_task(generate_template)),
@@ -74,7 +107,7 @@ class ReportApp:
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"}) ], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
# 3. 日志区 # 3. 日志区
log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding=10) log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding="10 15 10 5")
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9)) self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9))
@@ -106,6 +139,15 @@ class ReportApp:
break break
self.root.after(100, self._start_log_polling) self.root.after(100, self._start_log_polling)
def init_project(self):
# 1. 资源准备
if install_fonts_from_directory(config["fonts_dir"]):
logger.info("等待系统识别新安装的字体...")
time.sleep(2)
# 2. 创建输出文件夹
os.makedirs(config["output_folder"], exist_ok=True)
logger.success("项目初始化完成.....")
# --- 任务运行核心逻辑 --- # --- 任务运行核心逻辑 ---
def run_task(self, target_func, *args, **kwargs): def run_task(self, target_func, *args, **kwargs):
if self.is_running: if self.is_running:
@@ -115,6 +157,9 @@ class ReportApp:
self.stop_event.clear() self.stop_event.clear()
self.is_running = True self.is_running = True
# 将进度更新方法作为参数传入
kwargs['progress_callback'] = self.update_progress
def thread_worker(): def thread_worker():
try: try:
# 尝试传入 stop_event # 尝试传入 stop_event
@@ -129,7 +174,8 @@ class ReportApp:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
self.is_running = False self.is_running = False
logger.info("--- 就绪 ---") logger.info("系统准备就绪.....")
self.reset_progress() # 重置进度条
threading.Thread(target=thread_worker, daemon=True).start() threading.Thread(target=thread_worker, daemon=True).start()
@@ -159,3 +205,32 @@ class ReportApp:
return return
self.root.destroy() self.root.destroy()
sys.exit() sys.exit()
# --- 进度条更新(实现线程安全更新) ---
def update_progress(self, current, total, task_name="任务"):
"""
线程安全地更新进度条和标签
:param current: 当前完成的项目数
:param total: 总项目数
:param task_name: 当前任务名称
"""
if total <= 0:
# 重置进度条
self.progressbar['value'] = 0
self.progress_label.config(text=f"任务进度: {task_name} 完成或待命")
return
percentage = int((current / total) * 100)
display_text = f"{task_name}: {current}/{total} ({percentage}%)"
# 使用 after 确保在主线程中更新 UI
self.root.after(0, self._set_progress_ui, percentage, display_text)
def _set_progress_ui(self, percentage, display_text):
"""实际更新 UI 的私有方法"""
self.progressbar['value'] = percentage
self.progress_label.config(text=display_text)
def reset_progress(self):
"""任务结束后重置进度条"""
self.root.after(0, self._set_progress_ui, 0, "任务进度: 就绪")

View File

@@ -4,10 +4,15 @@
import os import os
import platform import platform
import shutil import shutil
import time
from pathlib import Path from pathlib import Path
from loguru import logger from loguru import logger
from config.config import load_config
config = load_config("config.toml")
def get_system_fonts(): def get_system_fonts():
"""获取系统中可用的字体列表""" """获取系统中可用的字体列表"""

View File

@@ -1,4 +1,5 @@
import os import os
import threading
import time import time
import pythoncom import pythoncom
@@ -12,7 +13,6 @@ import comtypes.client
from config.config import load_config from config.config import load_config
from utils.agent_utils import generate_comment from utils.agent_utils import generate_comment
from utils.file_utils import check_file_exists from utils.file_utils import check_file_exists
from utils.font_utils import install_fonts_from_directory
from utils.image_utils import find_image_path from utils.image_utils import find_image_path
from utils.zodiac_utils import calculate_zodiac from utils.zodiac_utils import calculate_zodiac
from utils.growt_utils import ( from utils.growt_utils import (
@@ -23,7 +23,6 @@ from utils.growt_utils import (
replace_five_page, replace_five_page,
) )
# 如果你之前没有全局定义 console这里定义一个 # 如果你之前没有全局定义 console这里定义一个
console = Console() console = Console()
@@ -36,26 +35,33 @@ config = load_config("config.toml")
# ========================================== # ==========================================
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹) # 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ========================================== # ==========================================
def generate_template(): def generate_template(stop_event: threading.Event = None, progress_callback=None):
""""
根据学生姓名生成相对应的以学生姓名的存放照片的文件夹
:params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数
"""
try: try:
# 2. 读取数据 # 1. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1") df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# 2. 获取姓名数据
# --- 修改点开始 ---
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
datas = df["姓名"].values.tolist() datas = df["姓名"].values.tolist()
# --- 修改点结束 --- total_count = len(datas)
logger.info(f"开始生成学生模版文件,共 {total_count} 位学生...")
logger.info(f"开始生成学生模版文件,共 {len(datas)} 位学生...")
# 3. 循环处理 # 3. 循环处理
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
for i, name in enumerate(datas): for i, name in enumerate(datas):
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}") # 判断是否有停止事件
if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....")
return # 停止任务
# 添加进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成学生图片文件夹")
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 确保 name 是字符串且去除了空格 (增加健壮性) # 确保 name 是字符串且去除了空格 (增加健壮性)
name = str(name).strip() name = str(name).strip()
# 生成学生图片的文件夹
student_folder = os.path.join(config["image_folder"], name) student_folder = os.path.join(config["image_folder"], name)
if os.path.exists(student_folder): if os.path.exists(student_folder):
@@ -63,7 +69,10 @@ def generate_template():
else: else:
logger.info(f"正在生成学生图片文件夹 {student_folder}") logger.info(f"正在生成学生图片文件夹 {student_folder}")
os.makedirs(student_folder, exist_ok=True) os.makedirs(student_folder, exist_ok=True)
# 更新进度条为100%
if progress_callback:
progress_callback(total_count, total_count, "生成学生图片文件夹")
logger.success("✅ 所有学生模版文件已生成完毕")
except Exception as e: except Exception as e:
logger.error(f"程序运行出错: {str(e)}") logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试 # 打印详细报错位置,方便调试
@@ -73,7 +82,12 @@ def generate_template():
# ========================================== # ==========================================
# 2. 生成评语(根据names.xlsx文件生成评价) # 2. 生成评语(根据names.xlsx文件生成评价)
# ========================================== # ==========================================
def generate_comment_all(): def generate_comment_all(stop_event: threading.Event = None, progress_callback=None):
"""
根据学生姓名生成评价
:params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数
"""
try: try:
# 1. 读取数据 # 1. 读取数据
excel_path = config["excel_file"] excel_path = config["excel_file"]
@@ -85,23 +99,27 @@ def generate_comment_all():
# 获取学生数据行数 # 获取学生数据行数
total_count = len(df) total_count = len(df)
logger.info(f"开始生成学生评语,共 {total_count} 位学生...") logger.info(f"开始生成学生评语,共 {len(df)} 位学生...")
# 强制将“评价”列转换为 object 类型 # 强制将“评价”列转换为 object 类型
df["评价"] = df["评价"].astype("object") df["评价"] = df["评价"].astype("object")
# --- 遍历 DataFrame 的索引 (index) --- # --- 遍历 DataFrame 的索引 (index) ---
# 这样我们可以通过索引 i 精准地把数据写回某一行
for i in df.index: for i in df.index:
name = df.at[i, "姓名"] # 获取当前行的姓名
sex = df.at[i, "性别"]
if pd.isna(sex):
sex = ""
else:
sex = str(sex).strip()
# 健壮性处理 if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....")
return # 停止任务
# 添加进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成学生评语")
# 获取学生姓名
name = df.at[i, "姓名"]
if pd.isna(name): if pd.isna(name):
continue # 跳过空行 continue
name = str(name).strip() else:
name = str(name).strip()
# 获取性别
sex = pd.isna(df.at[i, "性别"]) if "" else str(df.at[i, "性别"]).strip()
# 获取当前行的特征如果Excel里有“特征”这一列就读没有就用默认值 # 获取当前行的特征如果Excel里有“特征”这一列就读没有就用默认值
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..." # 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
@@ -124,11 +142,7 @@ def generate_comment_all():
generated_text = generate_comment( generated_text = generate_comment(
name, config["age_group"], traits, sex name, config["age_group"], traits, sex
) )
if generated_text: df.at[i, "评价"] = generated_text if str(generated_text).strip() else ""
# 赋值
df.at[i, "评价"] = str(generated_text).strip()
else:
df.at[i, "评价"] = "" # 防空处理
logger.success(f"学生:{name},评语生成完毕") logger.success(f"学生:{name},评语生成完毕")
# 可选:每生成 5 个就保存一次 # 可选:每生成 5 个就保存一次
@@ -139,11 +153,12 @@ def generate_comment_all():
time.sleep(1) time.sleep(1)
except Exception as e: except Exception as e:
logger.error(f"学生:{name},生成评语出错: {str(e)}") logger.error(f"学生:{name},生成评语出错: {str(e)}")
# --- 循环结束后最终保存文件 --- # --- 循环结束后最终保存文件 ---
# index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列 # index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列
df.to_excel(excel_path, index=False) df.to_excel(excel_path, index=False)
logger.success(f"所有评语已生成并写入文件:{excel_path}") logger.success(f"所有评语已生成并写入文件:{excel_path}")
if progress_callback:
progress_callback(total_count, total_count, "生成学生评语")
except PermissionError: except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}") logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
@@ -155,26 +170,23 @@ def generate_comment_all():
# ========================================== # ==========================================
# 3. 生成成长报告(根据names.xlsx文件生成) # 3. 生成成长报告(根据names.xlsx文件生成)
# ========================================== # ==========================================
def generate_report(): def generate_report(stop_event: threading.Event = None, progress_callback=None):
# 1. 资源准备 """
if install_fonts_from_directory(config["fonts_dir"]): 根据学生姓名生成成长报告
logger.info("等待系统识别新安装的字体...") :params stop_event 任务是否停止事件监听UI的事件监听
time.sleep(2) :params progress_callback 进度回调函数
"""
os.makedirs(config["output_folder"], exist_ok=True) # 1. 检查模版文件是否存在
# 检查模版文件是否存在
if not os.path.exists(config["source_file"]): if not os.path.exists(config["source_file"]):
logger.info(f"错误: 找不到模版文件 {config["source_file"]}") logger.info(f"错误: 找不到模版文件 {config["source_file"]}")
return # 2. 检查数据文件是否存在
# 检查数据文件是否存在
if not os.path.exists(config["excel_file"]): if not os.path.exists(config["excel_file"]):
logger.info(f"错误: 找不到数据文件 {config['excel_file']}") logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
return
try: try:
# 2. 读取数据 # 1. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1") df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# 确保列名对应 # 2. 确保列名对应
columns = [ columns = [
"姓名", "姓名",
"英文名", "英文名",
@@ -187,14 +199,21 @@ def generate_report():
"喜欢吃的食物", "喜欢吃的食物",
"评价", "评价",
] ]
# 获取数据列表
datas = df[columns].values.tolist() datas = df[columns].values.tolist()
total_count = len(datas)
# 获取配置文件的教师签名
teacher_names_str = " ".join(config["teachers"]) teacher_names_str = " ".join(config["teachers"])
logger.info(f"开始处理,共 {total_count} 位学生...")
logger.info(f"开始处理,共 {len(datas)} 位学生...")
# 3. 循环处理 # 3. 循环处理
for i, row_data in enumerate(datas): for i, row_data in enumerate(datas):
if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....")
return
# 更新进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成报告")
# 解包数据 # 解包数据
( (
name, name,
@@ -237,16 +256,16 @@ def generate_report():
# 构造学生信息字典 # 构造学生信息字典
student_info_dict = { student_info_dict = {
"name": name, "name": name,
"english_name": english_name if pd.notna(english_name) else "", "english_name": english_name if pd.notna(english_name) else " ",
"sex": sex if pd.notna(sex) else "", "sex": sex if pd.notna(sex) else "",
"birthday": ( "birthday": (
birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else "" birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else " "
), ),
"zodiac": zodiac if pd.notna(zodiac) else "", "zodiac": str(zodiac).strip() if str(zodiac).strip() or not str(zodiac).strip().lower() else " ",
"friend": friend if pd.notna(friend) else "", "friend": str(friend).strip() if str(friend).strip() or not str(friend).strip().lower() else " ",
"hobby": hobby if pd.notna(hobby) else "", "hobby": str(hobby).strip() if str(hobby).strip() or not str(hobby).strip().lower() else " ",
"game": game if pd.notna(game) else "", "game": str(game).strip() if str(game).strip() or not str(game).strip().lower() else " ",
"food": food if pd.notna(food) else "", "food": str(food).strip() if str(food).strip() or not str(food).strip().lower() else " ",
} }
# 获取学生个人照片路径 # 获取学生个人照片路径
me_image_path = find_image_path(student_image_folder, "me") me_image_path = find_image_path(student_image_folder, "me")
@@ -254,6 +273,7 @@ def generate_report():
if check_file_exists(me_image_path): if check_file_exists(me_image_path):
replace_three_page(prs, student_info_dict, me_image_path) replace_three_page(prs, student_info_dict, me_image_path)
else: else:
replace_three_page(prs, student_info_dict)
logger.warning(f"⚠️ 警告: 学生图片文件不存在 {me_image_path}") logger.warning(f"⚠️ 警告: 学生图片文件不存在 {me_image_path}")
# --- 页面 4 --- # --- 页面 4 ---
@@ -301,6 +321,8 @@ def generate_report():
f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。" f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。"
) )
if progress_callback:
progress_callback(total_count, total_count, "生成报告")
logger.success("所有报告生成完毕!") logger.success("所有报告生成完毕!")
except Exception as e: except Exception as e:
@@ -311,10 +333,12 @@ def generate_report():
# ========================================== # ==========================================
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF) # 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ========================================== # ==========================================
def batch_convert_folder(folder_path): def batch_convert_folder(folder_path, stop_event: threading.Event = None, progress_callback=None):
""" """
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快) 批量转换文件夹下的所有 PPT
已修复多线程 CoInitialize 报错,并适配 GUI 日志 :params folder_path 需要转换的PPT文件夹
:params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数
""" """
# 子线程初始化 COM 组件 # 子线程初始化 COM 组件
pythoncom.CoInitialize() pythoncom.CoInitialize()
@@ -333,18 +357,24 @@ def batch_convert_folder(folder_path):
logger.warning("没有找到 PPT 文件") logger.warning("没有找到 PPT 文件")
return return
logger.info(f"发现 {len(files)} 个文件,准备开始转换...") total_count = len(files)
logger.info(f"发现 {total_count} 个文件,准备开始转换...")
powerpoint = None powerpoint = None
try: try:
# 1. 启动应用 (只启动一次) # 1. 启动应用 (只启动一次)
powerpoint = comtypes.client.CreateObject("PowerPoint.Application") powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
# 【建议】在后台线程运行时,有时设置为不可见更稳定, # 设置是否显示转化页面,但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
# 但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
# powerpoint.Visible = 1 # powerpoint.Visible = 1
for filename in files: for filename in files:
if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....")
return
# 添加进度条
if progress_callback:
progress_callback(files.index(filename), total_count, "转换PDF")
ppt_path = os.path.join(folder_path, filename) ppt_path = os.path.join(folder_path, filename)
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf" pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
@@ -353,7 +383,7 @@ def batch_convert_folder(folder_path):
logger.info(f"[跳过] 已存在: {filename}") logger.info(f"[跳过] 已存在: {filename}")
continue continue
logger.info(f"正在转换: {filename} ...") logger.info(f"[{files.index(filename)}/{total_count}]正在转换: {filename} ...")
try: try:
# 打开 -> 另存为 -> 关闭 # 打开 -> 另存为 -> 关闭
@@ -363,6 +393,9 @@ def batch_convert_folder(folder_path):
except Exception as e: except Exception as e:
logger.error(f"文件 {filename} 转换出错: {e}") logger.error(f"文件 {filename} 转换出错: {e}")
# 添加进度条
if progress_callback:
progress_callback(total_count, total_count, "转换PDF")
except Exception as e: except Exception as e:
logger.error(f"PowerPoint 进程启动出错: {e}") logger.error(f"PowerPoint 进程启动出错: {e}")
finally: finally:
@@ -384,11 +417,15 @@ def batch_convert_folder(folder_path):
# ========================================== # ==========================================
# 5. 生成属相(根据names.xlsx文件生成属相) # 5. 生成属相(根据names.xlsx文件生成属相)
# ========================================== # ==========================================
def generate_zodiac(): def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
"""
生成学生属相,如果“生日”列为空,则跳过该学生。
:params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数
"""
try: try:
# 1. 读取数据 # 1. 读取数据
excel_path = config["excel_file"] excel_path = config["excel_file"]
# sheet_name 根据实际情况修改,如果不确定可以用 sheet_name=0 读取第一个
df = pd.read_excel(excel_path, sheet_name="Sheet1") df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 2. 检查必要的列 # 2. 检查必要的列
@@ -399,30 +436,63 @@ def generate_zodiac():
logger.error(f"Excel中找不到列名{date_column}】,请检查表头。") logger.error(f"Excel中找不到列名{date_column}】,请检查表头。")
return return
# 检查是否存在"属相"列,不存在则新建
if target_column not in df.columns: if target_column not in df.columns:
df[target_column] = "" df[target_column] = ""
# --- 获取总行数,用于日志 ---
total_count = len(df) total_count = len(df)
logger.info(f"开始生成学生属相,共 {total_count} 位学生...") logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
# 3. 数据清洗与计算 # 3. 预处理:将“生日”列转换为 datetime 格式
temp_dates = pd.to_datetime(df[date_column], errors="coerce") df['temp_date'] = pd.to_datetime(df[date_column], errors="coerce")
df[target_column] = temp_dates.apply(calculate_zodiac)
# 4. 遍历 DataFrame 并计算/更新数据
for i, row in df.iterrows():
# 关键点 1: 检查停止信号
if stop_event and stop_event.is_set():
logger.warning("任务已接收到停止信号,正在中断...")
return
# 添加进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成属相")
name = row.get("姓名", f"学生_{i + 1}")
date = row['temp_date']
logger.info(f"[{i + 1}/{total_count}] 正在处理学生:{name}...")
# === 关键点 2: 检查生日是否为空 ===
if pd.isna(date):
# 记录警告日志并跳过当前循环迭代
logger.warning(f"跳过:学生【{name}】的生日数据为空或格式错误。")
# 可以选择将属相字段清空或设置为特定值,此处设置为“待补充”
df.loc[i, target_column] = "待补充"
continue # 跳到下一个学生
# =================================
# 5. 计算并赋值
zodiac = calculate_zodiac(date)
df.loc[i, target_column] = zodiac
logger.info(f" -> 属相计算成功:{name} ,属相: {zodiac}")
# 6. 清理和保存结果
df = df.drop(columns=['temp_date'])
# 5. 保存结果
save_path = excel_path save_path = excel_path
try: try:
df.to_excel(save_path, index=False) df.to_excel(save_path, sheet_name="Sheet1", index=False)
logger.success(f"所有属相已更新并写入文件:{save_path}") logger.success(f"所有属相已更新并写入文件:{save_path}")
logger.warning(f"请检查文件 {save_path} 修改日期格式。")
except PermissionError: except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}") logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}")
# 添加进度条
if progress_callback:
progress_callback(total_count, total_count, "生成属相")
except FileNotFoundError: except FileNotFoundError:
logger.error(f"找不到文件 {config.get('excel_file')}") logger.error(f"找不到文件 {config.get('excel_file')}")
logger.error(traceback.format_exc())
except Exception as e: except Exception as e:
logger.error(f"程序运行出错: {str(e)}") logger.error(f"程序运行出错: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())

View File

@@ -11,6 +11,8 @@ console = Console()
# 1. 配置区域 (Configuration) # 1. 配置区域 (Configuration)
# ========================================== # ==========================================
config = load_config("config.toml") config = load_config("config.toml")
def replace_one_page(prs, name, class_name): def replace_one_page(prs, name, class_name):
"""替换第一页信息""" """替换第一页信息"""
replace_text_in_slide(prs, 0, "name", name) replace_text_in_slide(prs, 0, "name", name)
@@ -23,7 +25,7 @@ def replace_two_page(prs, comments, teacher_name):
replace_text_in_slide(prs, 1, "teacher_name", teacher_name) replace_text_in_slide(prs, 1, "teacher_name", teacher_name)
def replace_three_page(prs, info_dict, me_image): def replace_three_page(prs, info_dict, me_image=None):
"""替换第三页信息""" """替换第三页信息"""
# 使用字典解包传递多个字段,减少参数数量 # 使用字典解包传递多个字段,减少参数数量
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"] fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]

View File

@@ -1,5 +1,5 @@
import os import os
import pythoncom
import comtypes.client import comtypes.client
@@ -9,6 +9,8 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
:param ppt_path: PPT 文件路径 :param ppt_path: PPT 文件路径
:param pdf_path: PDF 输出路径 (可选,默认同名) :param pdf_path: PDF 输出路径 (可选,默认同名)
""" """
# 子线程初始化 COM 组件
pythoncom.CoInitialize()
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径 ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
if pdf_path is None: if pdf_path is None:
@@ -40,3 +42,4 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
finally: finally:
if powerpoint: if powerpoint:
powerpoint.Quit() powerpoint.Quit()
pythoncom.CoUninitialize()

44
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]] [[package]]
@@ -289,7 +289,7 @@ wheels = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "1.1.3" version = "1.2.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@@ -301,28 +301,28 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "uuid-utils" }, { name = "uuid-utils" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0b/6f/9e959821df556cf32d57d26aa7b86905a63a91e6e9e744994f8045fe2d70/langchain_core-1.1.3.tar.gz", hash = "sha256:ff0bc5e6e701c4d6fe00c73c4feae5c993a7a8e0b91f0a1d07015277d4634275" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6f/ae/2041e14c8781b1696bb161b78152f1523b5128bdb16c95199632eb034c6f/langchain_core-1.2.0.tar.gz", hash = "sha256:e3f6450ae88505ec509ffa6f5c7ba3fa377a35b5d73f307b3ba1fc5aeb8a95b1" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/58/41/6db768d4b208a33b4f09d5415e617d489f68167bb5dd27f87c7a49d13caf/langchain_core-1.1.3-py3-none-any.whl", hash = "sha256:e06efbf55bf7c7e4fcffc2f5b0a39a855176df16b02077add063534d7dabb740" }, { url = "https://mirrors.aliyun.com/pypi/packages/dd/bb/ddac30cba0c246f7c15d81851311a23dc1455b6e908f624e71fa3b82b3d1/langchain_core-1.2.0-py3-none-any.whl", hash = "sha256:ed95ee5cbab0d1188c91ad230bb6a513427bc1e2ed5a8329075ab24412cd7727" },
] ]
[[package]] [[package]]
name = "langchain-openai" name = "langchain-openai"
version = "1.1.1" version = "1.1.3"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
{ name = "openai" }, { name = "openai" },
{ name = "tiktoken" }, { name = "tiktoken" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/38/c6517187ea5f0db6d682083116a020409b01eef0547b4330542c117cd25d/langchain_openai-1.1.1.tar.gz", hash = "sha256:72aa7262854104e0b2794522a90c49353c79d0132caa1be27ef253852685d5e7" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/93/67/6126a1c645b34388edee917473e51b2158812af1fcc8fedc23a330478329/langchain_openai-1.1.3.tar.gz", hash = "sha256:d8be85e4d1151258e1d2ed29349179ad971499115948b01364c2a1ab0474b1bf" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/1d/95/d65d3e187cd717baeb62988ae90a995f93564657f67518fb775af4090880/langchain_openai-1.1.1-py3-none-any.whl", hash = "sha256:69b9be37e6ae3372b4d937cb9365cf55c0c59b5f7870e7507cb7d802a8b98b30" }, { url = "https://mirrors.aliyun.com/pypi/packages/d1/11/2b3b4973495fc5f0456ed5c8c88a6ded7ca34c8608c72faafa87088acf5a/langchain_openai-1.1.3-py3-none-any.whl", hash = "sha256:58945d9e87c1ab3a91549c3f3744c6c9571511cdc3cf875b8842aaec5b3e32a6" },
] ]
[[package]] [[package]]
name = "langgraph" name = "langgraph"
version = "1.0.4" version = "1.0.5"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "langchain-core" }, { name = "langchain-core" },
@@ -332,9 +332,9 @@ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "xxhash" }, { name = "xxhash" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d6/3c/af87902d300c1f467165558c8966d8b1e1f896dace271d3f35a410a5c26a/langgraph-1.0.4.tar.gz", hash = "sha256:86d08e25d7244340f59c5200fa69fdd11066aa999b3164b531e2a20036fac156" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7d/47/28f4d4d33d88f69de26f7a54065961ac0c662cec2479b36a2db081ef5cb6/langgraph-1.0.5.tar.gz", hash = "sha256:7f6ae59622386b60fe9fa0ad4c53f42016b668455ed604329e7dc7904adbf3f8" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/14/52/4eb25a3f60399da34ba34adff1b3e324cf0d87eb7a08cebf1882a9b5e0d5/langgraph-1.0.4-py3-none-any.whl", hash = "sha256:b1a835ceb0a8d69b9db48075e1939e28b1ad70ee23fa3fa8f90149904778bacf" }, { url = "https://mirrors.aliyun.com/pypi/packages/23/1b/e318ee76e42d28f515d87356ac5bd7a7acc8bad3b8f54ee377bef62e1cbf/langgraph-1.0.5-py3-none-any.whl", hash = "sha256:b4cfd173dca3c389735b47228ad8b295e6f7b3df779aba3a1e0c23871f81281e" },
] ]
[[package]] [[package]]
@@ -365,20 +365,20 @@ wheels = [
[[package]] [[package]]
name = "langgraph-sdk" name = "langgraph-sdk"
version = "0.2.15" version = "0.3.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },
{ name = "orjson" }, { name = "orjson" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/71/46/a0bc5914e4a418ad5e8558b19bccd6f0baf56d0c674d6d65a0acf4f22590/langgraph_sdk-0.2.15.tar.gz", hash = "sha256:8faaafe2c1193b89f782dd66c591060cd67862aa6aaf283749b7846f331d5334" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/2b/1b/f328afb4f24f6e18333ff357d9580a3bb5b133ff2c7aae34fef7f5b87f31/langgraph_sdk-0.3.0.tar.gz", hash = "sha256:4145bc3c34feae227ae918341f66d3ba7d1499722c1ef4a8aae5ea828897d1d4" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/c9/bf2bff18f85bb7973fa5280838580049574bd7649c36e3dd346c49304997/langgraph_sdk-0.2.15-py3-none-any.whl", hash = "sha256:746566a5d89aa47160eccc17d71682a78771c754126f6c235a68353d61ed7462" }, { url = "https://mirrors.aliyun.com/pypi/packages/69/48/ee4d7afb3c3d38bd2ebe51a4d37f1ed7f1058dd242f35994b562203067aa/langgraph_sdk-0.3.0-py3-none-any.whl", hash = "sha256:c1ade483fba17ae354ee920e4779042b18d5aba875f2a858ba569f62f628f26f" },
] ]
[[package]] [[package]]
name = "langsmith" name = "langsmith"
version = "0.4.58" version = "0.4.59"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },
@@ -390,9 +390,9 @@ dependencies = [
{ name = "uuid-utils" }, { name = "uuid-utils" },
{ name = "zstandard" }, { name = "zstandard" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a8/c1/a2f04ccbd5ef44b9bd41fb0174fff539b4d41f273ad3649cd656e87b44b2/langsmith-0.4.58.tar.gz", hash = "sha256:ec72e2fcfbbf90b827212c75acac8fc58bb921726f4326633bd774b50133eea3" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/61/71/d61524c3205bde7ec90423d997cf1a228d8adf2811110ec91ed40c8e8a34/langsmith-0.4.59.tar.gz", hash = "sha256:6b143214c2303dafb29ab12dcd05ac50bdfc60dac01c6e0450e50cee1d2415e0" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/69/1a48e6b6c4d1e244a272b36e029fd27a70d37c8f4f002367c3d78f6c8e1d/langsmith-0.4.58-py3-none-any.whl", hash = "sha256:f6999b2f7956f030c32a80c1d631b49677f35648560207a09973c8d5ab74279d" }, { url = "https://mirrors.aliyun.com/pypi/packages/63/54/4577ef9424debea2fa08af338489d593276520d2e2f8950575d292be612c/langsmith-0.4.59-py3-none-any.whl", hash = "sha256:97c26399286441a7b7b06b912e2801420fbbf3a049787e609d49dc975ab10bc5" },
] ]
[[package]] [[package]]
@@ -557,7 +557,7 @@ wheels = [
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.9.0" version = "2.11.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@@ -569,9 +569,9 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/09/48/516290f38745cc1e72856f50e8afed4a7f9ac396a5a18f39e892ab89dfc2/openai-2.9.0.tar.gz", hash = "sha256:b52ec65727fc8f1eed2fbc86c8eac0998900c7ef63aa2eb5c24b69717c56fa5f" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f4/8c/aa6aea6072f985ace9d6515046b9088ff00c157f9654da0c7b1e129d9506/openai-2.11.0.tar.gz", hash = "sha256:b3da01d92eda31524930b6ec9d7167c535e843918d7ba8a76b1c38f1104f321e" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/59/fd/ae2da789cd923dd033c99b8d544071a827c92046b150db01cfa5cea5b3fd/openai-2.9.0-py3-none-any.whl", hash = "sha256:0d168a490fbb45630ad508a6f3022013c155a68fd708069b6a1a01a5e8f0ffad" }, { url = "https://mirrors.aliyun.com/pypi/packages/e5/f1/d9251b565fce9f8daeb45611e3e0d2f7f248429e40908dcee3b6fe1b5944/openai-2.11.0-py3-none-any.whl", hash = "sha256:21189da44d2e3d027b08c7a920ba4454b8b7d6d30ae7e64d9de11dbe946d4faa" },
] ]
[[package]] [[package]]
@@ -1260,11 +1260,11 @@ wheels = [
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.1" version = "2.6.2"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797" }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b" }, { url = "https://mirrors.aliyun.com/pypi/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd" },
] ]
[[package]] [[package]]