chore: import original project baseline

Import the pre-repair source tree as the history baseline.
Runtime data (data/), virtualenvs, bytecode caches and logs are
gitignored so local secrets and user state stay out of the repo.
This commit is contained in:
2026-09-17 16:40:01 +08:00
commit a7412824e0
124 changed files with 26747 additions and 0 deletions
+399
View File
@@ -0,0 +1,399 @@
# -*- coding: utf-8 -*-
"""模型选择弹窗 · 可视化调参工具
运行: C:\\Users\\14890\\miniconda3\\envs\\haocode\\python.exe tests\\tune_model_popup.py
- 左侧:调参窗(改参数 → 右侧弹窗实时重渲染)
- 右侧:真实的 ModelSelectPopup 本体(独立窗口,点调参窗不会消失)
- 满意后点「确定」→ 参数 JSON 写入 data/model_popup_tune.json → 自动退出
"""
import os
import sys
import json
import traceback
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
os.environ.setdefault("HAOCODE_RENDER", "software")
from PyQt6.QtWidgets import ( # noqa: E402
QApplication, QWidget, QFormLayout, QHBoxLayout, QSpinBox,
QPushButton, QCheckBox, QLabel, QMessageBox, QComboBox,
)
from PyQt6.QtCore import Qt, QSize # noqa: E402
from PyQt6.QtGui import QPixmap, QIcon, QPainter, QFont, QFontDatabase # noqa: E402
from ui.views.main_window import ModelSelectPopup, MainWindow, _popup_svg_path # noqa: E402 (先于 QApplication)
DEFAULTS = {
# —— 字体 ——
"font_family": "Microsoft YaHei", # 字体族(下拉选择;调参确认值)
# —— 模型行 ——
"item_font": 13, # 模型名字号(经 list_widget.setFont 生效)
"model_item_h": 27, # 模型行高
"model_icon": 16, # 模型图标尺寸
"model_icon_pad": 34, # 模型行左边距(图标透明左垫,仅作用模型行)
# —— 供应商行 ——
"header_height": 38, # 供应商行高(= widget 高;默认 38 = 当前代码实际行高)
"header_font": 13, # 供应商名字号
"count_font": 10, # 数量字号
"provider_svg": "provider.svg", # 供应商图标(3 个 SVG 可切换)
"header_icon": 16, # 供应商图标尺寸
"header_icon_pad": 0, # 供应商图标水平位置(左侧留白,仅推图标与后续内容)
"header_icon_pad_v": 1, # 供应商图标垂直位置(正=下移,负=上移)
"arrow_size": 13, # 展开/收起箭头尺寸
"header_hpad_l": 3, # 供应商行左边距(头部按钮左 margin)
# —— 整体 ——
"popup_width": 340, # 弹窗宽度
"container_vmargin": 4, # 容器上下边距
}
RANGES = {
"item_font": (10, 20),
"model_item_h": (18, 40), "model_icon": (8, 28),
"model_icon_pad": (0, 48), "header_height": (20, 60), "header_font": (9, 18),
"count_font": (8, 16), "header_icon": (10, 28), "header_icon_pad": (0, 30),
"header_icon_pad_v": (-10, 10),
"arrow_size": (8, 20),
"header_hpad_l": (0, 20),
"popup_width": (260, 420), "container_vmargin": (0, 12),
}
CN = {
"font_family": "字体(下拉选择)",
"item_font": "模型名字号 (px)",
"model_item_h": "模型行高 (px)",
"model_icon": "模型图标尺寸 (px)", "model_icon_pad": "模型行左边距 (px)",
"header_height": "供应商行高 (px)", "header_font": "供应商名字号 (px)",
"count_font": "数量字号 (px)", "header_icon": "供应商图标尺寸 (px)",
"provider_svg": "供应商图标(SVG 切换)",
"header_icon_pad": "供应商图标水平位置 (px)",
"header_icon_pad_v": "供应商图标垂直位置 (px,正=下移)",
"arrow_size": "箭头尺寸 (px)", "header_hpad_l": "供应商行左边距 (px)",
"popup_width": "弹窗宽度 (px)",
"container_vmargin": "容器上下边距 (px)",
}
SECTIONS = [
("字体", ["font_family"]),
("模型行", ["item_font", "model_item_h", "model_icon", "model_icon_pad"]),
("供应商行", ["header_height", "header_font", "count_font",
"provider_svg", "header_icon", "header_icon_pad",
"header_icon_pad_v", "arrow_size", "header_hpad_l"]),
("整体", ["popup_width", "container_vmargin"]),
]
# 供应商图标候选(文件名, 显示名)
PROVIDER_SVGS = [
("provider.svg", "图标 1 · 服务器"),
("provider2.svg", "图标 2 · 二级服务器"),
("provider3.svg", "图标 3 · 服务器(细线)"),
]
FONT_STACK = '"HarmonyOS Sans SC", "Microsoft YaHei", "Noto Sans SC", sans-serif'
QSS_TMPL = """
* {
font-family: FONT_STACK;
}
#popup_container {
background-color: #ffffff;
border: 1px solid #dcdcdc;
border-radius: 12px;
}
#group_toggle_btn { background: transparent; border: none; }
#group_toggle_btn:hover { background-color: #f2f5f9; border-radius: 6px; }
#model_list {
border: none;
background: transparent;
outline: none;
}
#model_list::item {
font-family: FONT_STACK;
color: #333333;
font-weight: normal;
/* 实测本 Qt/PyQt6 构建下 ::item 仅 background-color/color 生效;
font-size/font 缩写/font-family/margin/padding/border-radius 全部无效——
字号走 setFont,行高走 sizeHint,边距走图标透明垫/按钮 margin */
}
#model_list::item:hover {
background-color: #f0f4f9;
color: #111111;
}
#model_list::item:selected {
background-color: #e8f0fe;
color: #1a73e8;
font-weight: bold;
}
QScrollBar:vertical {
border: none;
background: transparent;
width: 5px;
margin: 12px 2px;
}
QScrollBar::handle:vertical {
background: #d0d0d0;
min-height: 20px;
border-radius: 3px;
}
QScrollBar::handle:vertical:hover {
background: #a0a0a0;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
"""
class Tuner:
def __init__(self):
self.app = QApplication(sys.argv)
self.params = dict(DEFAULTS)
# ---- 弹窗本体(真实类 + 真实 config + 真实祖先上下文) ----
# ⚠️ 保真关键:实际 App 中弹窗是 MainWindow 的子部件,会继承其全局样式表
# QListWidget::item 的 padding/margin/选中蓝条等);若 parent=None 渲染,
# 就会出“调试与实际不符”的矛盾 → 借 MainWindow.setup_stylesheet 把真实
# 样式表套到宿主上,弹窗挂在宿主下渲染(与生产同构)
self._host = QWidget()
MainWindow.setup_stylesheet(self._host)
self._host.resize(1, 1)
self._host.move(-200, -200)
self._host.show()
cfg_path = os.path.join(os.path.dirname(__file__), "..", "data", "config.json")
with open(cfg_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
self.popup = ModelSelectPopup(self._host, cfg)
self.popup.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.FramelessWindowHint)
self.list = self.popup.list_widget
self._tag_headers()
screen = self.app.primaryScreen().availableGeometry()
self.px = screen.center().x() - 200
self.py = screen.center().y() - 300
self.popup.move(self.px, self.py)
self.popup.show()
# ---- 调参窗 ----
self.win = QWidget()
self.win.setWindowTitle("模型选择弹窗调参 —— 调好后点「确定」")
form = QFormLayout(self.win)
form.setSpacing(7)
self.spins = {}
self.combos = {}
self.broken = set() # 用户标记的“不生效”参数
for title, keys in SECTIONS:
form.addRow(QLabel(f"—— {title} ——"))
for key in keys:
mark = QPushButton("不生效")
mark.setCheckable(True)
mark.setFixedWidth(58)
mark.setCursor(Qt.CursorShape.PointingHandCursor)
mark.setStyleSheet("""
QPushButton { font-size: 11px; color: #888888; border: 1px solid #cccccc;
border-radius: 4px; background: #fafafa; }
QPushButton:checked { color: #ffffff; background: #d93025; border-color: #d93025; }
""")
mark.toggled.connect(lambda on, k=key: self.on_mark(k, on))
row = QHBoxLayout()
row.setSpacing(4)
if key == "font_family":
combo = QComboBox()
fams = ["HarmonyOS Sans SC", "Microsoft YaHei", "Noto Sans SC",
"Segoe UI", "Arial", "Consolas", "SimHei", "PingFang SC"]
seen = set()
for fam in fams + sorted(QFontDatabase.families()):
if fam and fam not in seen:
seen.add(fam)
combo.addItem(fam)
combo.setCurrentText(self.params[key])
combo.currentTextChanged.connect(lambda v, k=key: self.on_change(k, v))
self.combos[key] = combo
row.addWidget(combo, 1)
elif key == "provider_svg":
combo = QComboBox()
for fname, cname in PROVIDER_SVGS:
combo.addItem(cname, fname)
combo.setCurrentIndex(max(combo.findData(self.params[key]), 0))
combo.currentIndexChanged.connect(
lambda i, k=key, c=combo:
self.on_change(k, c.itemData(i) or "provider.svg"))
self.combos[key] = combo
row.addWidget(combo, 1)
else:
lo, hi = RANGES[key]
sp = QSpinBox()
sp.setRange(lo, hi)
sp.setValue(self.params[key])
sp.valueChanged.connect(lambda v, k=key: self.on_change(k, v))
self.spins[key] = sp
row.addWidget(sp, 1)
row.addWidget(mark)
form.addRow(CN[key], row)
self.chk_selected = QCheckBox("显示选中行(预览)")
self.chk_selected.toggled.connect(self.on_toggle_selected)
form.addRow(self.chk_selected)
btns = QHBoxLayout()
b_reset = QPushButton("重置默认")
b_reset.clicked.connect(self.reset)
b_ok = QPushButton("✅ 确定(写入日志并退出)")
b_ok.setStyleSheet("font-weight: bold;")
b_ok.clicked.connect(self.finish)
btns.addWidget(b_reset)
btns.addWidget(b_ok)
form.addRow(btns)
self.win.resize(380, min(self.win.sizeHint().height(), screen.height() - 80))
self.win.move(max(20, self.px - 400), self.py)
self.win.show()
self.apply_all()
# ---------- 启动时给头部标签打角色标签(此时尺寸还是默认值,判定可靠) ----------
def _tag_headers(self):
for g in self.popup._groups:
hwd = self.list.itemWidget(g["header_item"])
for lbl in hwd.findChildren(QLabel):
txt = lbl.text()
if txt == "":
# 默认: 供应商图标 16×16, 箭头 12×12 → 启动时按尺寸区分(此后不再变判定基准)
lbl.setProperty("_role", "icon" if lbl.size().width() >= 14 else "chevron")
elif txt.isdigit():
lbl.setProperty("_role", "count")
else:
lbl.setProperty("_role", "name")
# ---------- 实时渲染 ----------
def on_mark(self, k, on):
if on:
self.broken.add(k)
else:
self.broken.discard(k)
def on_change(self, k, v):
self.params[k] = v
self.apply_all()
def _rebuild_icons(self):
p = self.params
pop = self.popup
pop._provider_pixmap = QPixmap(_popup_svg_path(p.get("provider_svg", "provider.svg"))).scaled(
p["header_icon"], p["header_icon"],
Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
pop._arrow_expanded = QIcon(_popup_svg_path("chevron_down.svg")).pixmap(p["arrow_size"], p["arrow_size"])
pop._arrow_collapsed = QIcon(_popup_svg_path("chevron_right.svg")).pixmap(p["arrow_size"], p["arrow_size"])
m = QPixmap(_popup_svg_path("model.svg")).scaled(
p["model_icon"], p["model_icon"],
Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
pad_w = p["model_icon"] + p["model_icon_pad"]
_pad = QPixmap(pad_w, p["model_icon"])
_pad.fill(Qt.GlobalColor.transparent)
_pp = QPainter(_pad)
_pp.drawPixmap(p["model_icon_pad"], 0, m)
_pp.end()
pop._model_icon = QIcon(_pad)
# 关键:item 持有 QIcon 的拷贝,类属性替换后必须逐个 setIcon 才会更新(实测)
for g in pop._groups:
for it in g["model_items"]:
it.setIcon(pop._model_icon)
self.list.setIconSize(QSize(pad_w, p["model_icon"]))
def apply_all(self):
p = self.params
pop = self.popup
family = p["font_family"]
stack = f'"{family}", "Microsoft YaHei", "Noto Sans SC", sans-serif'
pop.setFixedWidth(p["popup_width"])
pop.container_layout.setContentsMargins(0, p["container_vmargin"], 0, p["container_vmargin"])
self._rebuild_icons()
for g in pop._groups:
hwd = self.list.itemWidget(g["header_item"])
hwd.setFixedHeight(p["header_height"])
g["header_item"].setSizeHint(QSize(0, p["header_height"])) # 行高 = widget 高
btns = hwd.findChildren(QPushButton)
if btns:
bl = btns[0].layout()
if bl is not None:
bl.setContentsMargins(p["header_hpad_l"], 0, 8, 0)
for lbl in hwd.findChildren(QLabel):
role = lbl.property("_role")
if role == "icon":
pad = p["header_icon_pad"]
pv = p["header_icon_pad_v"]
# 2 倍 margin:抵消 label 行内居中吸收的一半增高 → 1:1 位移
top, bot = 2 * max(pv, 0), 2 * max(-pv, 0)
lbl.setFixedSize(p["header_icon"] + pad, p["header_icon"] + top + bot)
lbl.setContentsMargins(pad, top, 0, bot)
lbl.setPixmap(pop._provider_pixmap)
elif role == "chevron":
lbl.setFixedSize(p["arrow_size"], p["arrow_size"])
lbl.setPixmap(pop._arrow_expanded if g["expanded"] else pop._arrow_collapsed)
elif role == "name":
lbl.setStyleSheet(
f"font-family: {stack}; color: #555555; font-weight: normal;"
f"font-size: {p['header_font']}px; letter-spacing: 1px; background: transparent;")
elif role == "count":
lbl.setStyleSheet(
f"font-family: {stack}; color: #888888;"
f"font-size: {p['count_font']}px; font-weight: normal; background: transparent;")
for it in g["model_items"]:
it.setSizeHint(QSize(0, p["model_item_h"]))
pop.setStyleSheet(QSS_TMPL
.replace("FONT_STACK", stack))
# 模型行字体:widget 级 setFontQSS ::item 的 font-size/缩写都不可靠,实测)
_f = self.list.font()
_f.setPixelSize(p["item_font"])
_f.setFamily(family)
self.list.setFont(_f)
self.app.processEvents()
pop.adjust_popup_height()
pop.adjustSize()
def on_toggle_selected(self, on):
if on:
for g in self.popup._groups:
if g["model_items"] and not g["model_items"][0].isHidden():
self.list.setCurrentItem(g["model_items"][0])
break
else:
self.list.clearSelection()
def reset(self):
self.params = dict(DEFAULTS)
for k, sp in self.spins.items():
sp.blockSignals(True)
sp.setValue(self.params[k])
sp.blockSignals(False)
for k, cb in self.combos.items():
cb.blockSignals(True)
cb.setCurrentText(self.params[k])
cb.blockSignals(False)
self.apply_all()
# ---------- 确定 → 写日志 ----------
def finish(self):
log = os.path.join(os.path.dirname(__file__), "..", "data", "model_popup_tune.json")
os.makedirs(os.path.dirname(log), exist_ok=True)
payload = dict(self.params)
payload["not_working"] = sorted(self.broken) # 用户标记的不生效参数
with open(log, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print("TUNE_LOG=" + os.path.abspath(log), flush=True)
print(json.dumps(payload, ensure_ascii=False), flush=True)
self.app.quit()
def main():
try:
Tuner()
QApplication.instance().exec()
except Exception:
err = traceback.format_exc()
try:
with open(os.path.join(os.path.dirname(__file__), "..", "data",
"tune_error.log"), "w", encoding="utf-8") as f:
f.write(err)
except OSError:
pass
QMessageBox.critical(None, "调参工具启动失败", err)
sys.exit(1)
if __name__ == "__main__":
main()