Checkpoint production workflow updates

This commit is contained in:
2026-06-30 15:25:29 +08:00
parent 19ae3ec37f
commit 9c80b95385
66 changed files with 7639 additions and 267 deletions
+327
View File
@@ -0,0 +1,327 @@
from __future__ import annotations
import json
import queue
import threading
from pathlib import Path
from tkinter import BooleanVar, StringVar, Tk, filedialog, messagebox, ttk
from tkinter.scrolledtext import ScrolledText
from sync_core import (
FileLogger,
copy_unc_missing_assets,
import_disk_assets_to_server,
normalize_path,
parse_path_list,
scan_assets,
stamp_text,
write_inventory,
)
APP_DIR = Path(__file__).resolve().parent
CONFIG_PATH = APP_DIR / "config.json"
DEFAULT_CONFIG = {
"server_asset_roots": r"D:\LuTan1_Image_Pool_Zip" + "\n" + r"D:\LT1_data_lsarorbit",
"inventory_output": "",
"server_inventory_json": "",
"unc_roots": "",
"unc_copy_targets": "",
"disk_roots": "",
"import_server_roots": r"D:\LuTan1_Image_Pool_Zip" + "\n" + r"D:\LT1_data_lsarorbit",
"report_dir": "",
"move_on_import": False,
}
class LT1AssetTool:
def __init__(self, root: Tk) -> None:
self.root = root
self.root.title("LT1 资产搬运工具")
self.root.geometry("1120x820")
self.log_queue: queue.Queue[str] = queue.Queue()
self.worker: threading.Thread | None = None
self.text_widgets: dict[str, ScrolledText] = {}
config = self.load_config()
self.vars = {
"server_asset_roots": StringVar(value=config["server_asset_roots"]),
"inventory_output": StringVar(value=config["inventory_output"]),
"server_inventory_json": StringVar(value=config["server_inventory_json"]),
"unc_roots": StringVar(value=config["unc_roots"]),
"unc_copy_targets": StringVar(value=config["unc_copy_targets"]),
"disk_roots": StringVar(value=config["disk_roots"]),
"import_server_roots": StringVar(value=config["import_server_roots"]),
"report_dir": StringVar(value=config["report_dir"]),
}
self.move_on_import = BooleanVar(value=bool(config["move_on_import"]))
self.summary_text = StringVar(value="准备就绪")
self.build_ui()
self.root.after(100, self.drain_logs)
def load_config(self) -> dict:
if not CONFIG_PATH.exists():
return dict(DEFAULT_CONFIG)
try:
payload = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
return {**DEFAULT_CONFIG, **payload}
except Exception:
return dict(DEFAULT_CONFIG)
def sync_text_vars(self) -> None:
for key, widget in self.text_widgets.items():
self.vars[key].set(widget.get("1.0", "end").strip())
def save_config(self) -> None:
self.sync_text_vars()
payload = {key: var.get() for key, var in self.vars.items()}
payload["move_on_import"] = self.move_on_import.get()
CONFIG_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
def build_ui(self) -> None:
outer = ttk.Frame(self.root, padding=12)
outer.pack(fill="both", expand=True)
common = ttk.LabelFrame(outer, text="公共输出", padding=10)
common.pack(fill="x")
self.add_path_row(common, 0, "报告/日志目录", "report_dir")
scan_box = ttk.LabelFrame(outer, text="1. 扫描服务器资产路径,生成随身 JSON", padding=10)
scan_box.pack(fill="x", pady=(10, 0))
self.add_multi_path_row(scan_box, 0, "服务器资产路径(多个用分号或换行)", "server_asset_roots")
self.add_file_save_row(scan_box, 1, "服务器资产 JSON 保存为", "inventory_output")
ttk.Button(scan_box, text="执行 1:扫描服务器并生成 JSON", command=lambda: self.start_worker(self.run_scan_server)).grid(
row=2,
column=1,
sticky="w",
pady=(8, 0),
)
unc_box = ttk.LabelFrame(outer, text="2. 扫描 UNC 多路径,按服务器 JSON 跳过已有,剩余复制到指定路径", padding=10)
unc_box.pack(fill="x", pady=(10, 0))
self.add_file_row(unc_box, 0, "读取服务器资产 JSON", "server_inventory_json")
self.add_multi_path_row(unc_box, 1, "UNC 源路径(多个用分号或换行)", "unc_roots")
self.add_multi_path_row(unc_box, 2, "复制目标路径(多个用分号或换行)", "unc_copy_targets")
ttk.Button(unc_box, text="预览 2:只生成复制报告", command=lambda: self.start_worker(lambda logger: self.run_unc_copy(logger, execute=False))).grid(
row=3,
column=1,
sticky="w",
pady=(8, 0),
)
ttk.Button(unc_box, text="执行 2:复制缺失资产", command=lambda: self.start_worker(lambda logger: self.run_unc_copy(logger, execute=True))).grid(
row=3,
column=1,
sticky="w",
padx=(180, 0),
pady=(8, 0),
)
import_box = ttk.LabelFrame(outer, text="3. 将指定磁盘路径资产复制或剪切到服务器资产路径", padding=10)
import_box.pack(fill="x", pady=(10, 0))
self.add_multi_path_row(import_box, 0, "磁盘资产路径(多个用分号或换行)", "disk_roots")
self.add_multi_path_row(import_box, 1, "服务器资产路径(多个用分号或换行)", "import_server_roots")
ttk.Checkbutton(import_box, text="剪切到服务器(不勾选则复制)", variable=self.move_on_import).grid(row=2, column=1, sticky="w")
ttk.Button(import_box, text="预览 3:只生成导入报告", command=lambda: self.start_worker(lambda logger: self.run_import(logger, execute=False))).grid(
row=3,
column=1,
sticky="w",
pady=(8, 0),
)
ttk.Button(import_box, text="执行 3:导入到服务器", command=lambda: self.start_worker(lambda logger: self.run_import(logger, execute=True))).grid(
row=3,
column=1,
sticky="w",
padx=(180, 0),
pady=(8, 0),
)
ttk.Label(import_box, text="执行 3 后,请回到服务器重新执行 1,生成新的随身 JSON。", foreground="#92400e").grid(
row=4,
column=1,
sticky="w",
pady=(8, 0),
)
control = ttk.Frame(outer)
control.pack(fill="x", pady=10)
ttk.Button(control, text="保存配置", command=self.handle_save_config).pack(side="left")
ttk.Label(control, textvariable=self.summary_text, font=("Microsoft YaHei UI", 10, "bold")).pack(side="left", padx=16)
log_box = ttk.LabelFrame(outer, text="日志", padding=10)
log_box.pack(fill="both", expand=True)
self.log_view = ScrolledText(log_box, height=14, wrap="word")
self.log_view.pack(fill="both", expand=True)
def add_path_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
ttk.Button(frame, text="选择", command=lambda: self.choose_dir(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
frame.columnconfigure(1, weight=1)
def add_file_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
ttk.Button(frame, text="选择", command=lambda: self.choose_file(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
frame.columnconfigure(1, weight=1)
def add_file_save_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
ttk.Entry(frame, textvariable=self.vars[key], width=105).grid(row=row, column=1, sticky="ew", pady=4)
ttk.Button(frame, text="选择", command=lambda: self.choose_save_file(key)).grid(row=row, column=2, padx=(8, 0), pady=4)
frame.columnconfigure(1, weight=1)
def add_multi_path_row(self, frame: ttk.Frame, row: int, label: str, key: str) -> None:
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="nw", padx=(0, 8), pady=4)
text = ScrolledText(frame, width=105, height=3, wrap="none")
text.insert("1.0", self.vars[key].get())
text.grid(row=row, column=1, sticky="ew", pady=4)
self.text_widgets[key] = text
ttk.Button(frame, text="追加目录", command=lambda: self.append_dir(key)).grid(row=row, column=2, padx=(8, 0), pady=4, sticky="n")
frame.columnconfigure(1, weight=1)
def choose_dir(self, key: str) -> None:
selected = filedialog.askdirectory()
if selected:
self.vars[key].set(selected)
def append_dir(self, key: str) -> None:
selected = filedialog.askdirectory()
if not selected:
return
widget = self.text_widgets.get(key)
if not widget:
self.vars[key].set(selected)
return
current = widget.get("1.0", "end").strip()
widget.delete("1.0", "end")
widget.insert("1.0", f"{current}\n{selected}" if current else selected)
def choose_file(self, key: str) -> None:
selected = filedialog.askopenfilename(filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")])
if selected:
self.vars[key].set(selected)
def choose_save_file(self, key: str) -> None:
selected = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")],
initialfile=f"server_assets_{stamp_text()}.json",
)
if selected:
self.vars[key].set(selected)
def report_dir(self) -> Path:
text = self.vars["report_dir"].get().strip()
if not text:
raise ValueError("请指定报告/日志目录")
return normalize_path(text)
def logger(self) -> FileLogger:
return FileLogger(self.report_dir() / "logs" / f"run_{stamp_text()}.log", self.log)
def handle_save_config(self) -> None:
self.save_config()
messagebox.showinfo("已保存", f"配置已保存到:{CONFIG_PATH}")
def log(self, message: str) -> None:
self.log_queue.put(message)
def drain_logs(self) -> None:
while True:
try:
message = self.log_queue.get_nowait()
except queue.Empty:
break
self.log_view.insert("end", message + "\n")
self.log_view.see("end")
self.root.after(100, self.drain_logs)
def start_worker(self, action) -> None:
if self.worker and self.worker.is_alive():
messagebox.showwarning("正在运行", "已有任务正在运行。")
return
self.save_config()
self.log_view.delete("1.0", "end")
self.worker = threading.Thread(target=self.run_action, args=(action,), daemon=True)
self.worker.start()
def run_action(self, action) -> None:
try:
action(self.logger())
except Exception as exc:
self.log(f"任务失败:{exc}")
self.root.after(0, lambda: messagebox.showerror("任务失败", str(exc)))
def run_scan_server(self, logger: FileLogger) -> None:
roots = parse_path_list(self.vars["server_asset_roots"].get())
if not roots:
raise ValueError("请填写服务器资产路径")
output = self.vars["inventory_output"].get().strip()
if not output:
raise ValueError("请指定服务器资产 JSON 保存路径")
logger("开始扫描服务器资产路径")
assets = scan_assets(roots, log=logger)
write_inventory(normalize_path(output), assets, [str(root) for root in roots])
self.root.after(0, lambda: self.summary_text.set(f"服务器资产 {len(assets)} 个,JSON 已生成"))
logger(f"完成:{output}")
def run_unc_copy(self, logger: FileLogger, *, execute: bool) -> None:
inventory = self.vars["server_inventory_json"].get().strip()
unc_roots = parse_path_list(self.vars["unc_roots"].get())
targets = parse_path_list(self.vars["unc_copy_targets"].get())
if not inventory:
raise ValueError("请指定服务器资产 JSON")
if not unc_roots:
raise ValueError("请填写 UNC 源路径")
if not targets:
raise ValueError("请填写复制目标路径")
logger("开始扫描 UNC 并按服务器 JSON 跳过已有资产")
report = copy_unc_missing_assets(
unc_roots,
normalize_path(inventory),
targets,
self.report_dir() / "reports",
execute=execute,
log=logger,
)
copied = sum(1 for item in report if item.action == "copied")
planned = sum(1 for item in report if item.action == "planned")
skipped = sum(1 for item in report if item.action == "skip")
failed = sum(1 for item in report if item.action == "failed")
text = f"UNC 处理完成:copied={copied}, planned={planned}, skipped={skipped}, failed={failed}"
self.root.after(0, lambda: self.summary_text.set(text))
logger(text)
def run_import(self, logger: FileLogger, *, execute: bool) -> None:
disk_roots = parse_path_list(self.vars["disk_roots"].get())
server_roots = parse_path_list(self.vars["import_server_roots"].get())
if not disk_roots:
raise ValueError("请填写磁盘资产路径")
if not server_roots:
raise ValueError("请填写服务器资产路径")
logger("开始从磁盘导入资产到服务器")
report = import_disk_assets_to_server(
disk_roots,
server_roots,
self.report_dir() / "reports",
execute=execute,
move=self.move_on_import.get(),
log=logger,
)
imported = sum(1 for item in report if item.action in {"copied", "moved"})
planned = sum(1 for item in report if item.action == "planned")
skipped = sum(1 for item in report if item.action == "skip")
failed = sum(1 for item in report if item.action == "failed")
text = f"磁盘导入完成:imported={imported}, planned={planned}, skipped={skipped}, failed={failed}。请重新执行 1。"
self.root.after(0, lambda: self.summary_text.set(text))
logger(text)
def main() -> None:
root = Tk()
LT1AssetTool(root)
root.mainloop()
if __name__ == "__main__":
main()