modify: 台账更改插件

This commit is contained in:
2026-09-22 16:08:12 +08:00
parent f468845f67
commit e1c02a9b80
37 changed files with 6377 additions and 380 deletions
+235
View File
@@ -0,0 +1,235 @@
"""生成文档用的界面截图(无头运行)。
用法::
uv run python tools/screenshot.py
产物写入 ``docs/images/``,供 ``docs/使用手册.md`` 引用;改完主题重跑即可。
**安全约定(重要)**:本脚本只画界面,绝不接触真实资产 ——
* ``QSettings`` 被重定向到临时 INI,不会读写用户真实的配置;
* 台账库指向临时目录,不会碰 ``~/.chongming/licenses.db``;
* 私钥是当场生成的一次性密钥,截图里不会出现任何真实密钥或客户信息;
* 演示数据统一带「示例」字样,一眼可与真实台账区分。
"""
from __future__ import annotations
import hashlib
import os
import sys
import tempfile
from datetime import timedelta
from pathlib import Path
# 必须在导入/创建 QApplication 之前设置:无头渲染,且不弹任何窗口
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from PySide6.QtWidgets import QApplication # noqa: E402
OUT_DIR = ROOT / "docs" / "images"
WINDOW_WIDTH = 1240
def _demo_sources(seed: str) -> dict[str, str]:
"""按种子造一台"设备"的来源指纹(已按库规则归一化:小写、仅 [0-9a-z])。"""
digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()
return {
"board_serial": f"cnx{digest[:12]}",
"product_uuid": digest[12:44],
"mac": f"001b44{digest[44:56]}",
"machine_id": digest[56:88],
}
def main() -> int:
# ---- 1) 隔离:配置、台账、密钥全部落在临时目录 ----------------------
sandbox = Path(tempfile.mkdtemp(prefix="license-gui-shots-"))
app = QApplication.instance() or QApplication([])
from chongming_license import (
build_license,
build_payload,
encode_request_code,
generate_keypair,
machine_code,
utc_today,
write_license,
write_private_key,
)
from chongming_license_gui import common, theme
from chongming_license_gui.app import MainWindow
from chongming_license_gui.registry_tab import ChangePluginsDialog, RenewDialog
theme.apply_theme(app)
# 一次性密钥(截图里出现的是这把,与任何真实密钥无关)
private_key, public_b64 = generate_keypair()
key_path = sandbox / "demo_private.pem"
write_private_key(private_key, key_path, overwrite=True)
# 配置隔离到临时目录;并用 fileName() 自证,而不是"以为隔离了"
settings = common.isolated_settings(sandbox)
common.assert_isolated_settings(settings, sandbox)
window = MainWindow(settings)
window.signing_key.load(str(key_path))
fingerprint = window.signing_key.fingerprint
# ---- 2) 演示台账:五种状态各一条,正好展示状态配色 ------------------
registry_tab = window.registry_tab
registry_tab.db_edit.setText(str(sandbox / "demo-licenses.db"))
registry_tab._open_db(silent=True)
today = utc_today()
demo = [
# (客户, 种子, 插件, not_before 偏移, expires 偏移, 台账批注)
("华东水务集团(示例)", "dev-water-01", ["aibox_sophon_adapter"],
None, -26, "合同 SM-2025-0417 · 已到期待续"),
("上海崇明智慧农业(示例)", "dev-farm-07", ["aibox_sophon_adapter", "ip_sound_column"],
None, 12, "已电话确认续期,等客户回申请码"),
("宁波港机(示例)", "dev-port-03", ["aibox_sophon_adapter"],
None, 248, "现场调试中"),
("苏州工业园区(示例)", "dev-suzhou-11", ["ip_sound_column"],
None, None, "战略合作 · 永久授权"),
("舟山渔场(示例)", "dev-zhoushan-05", ["aibox_sophon_adapter"],
45, 410, "新设备,发货前预签发"),
]
def _day(offset: int | None) -> str | None:
return None if offset is None else (today + timedelta(days=offset)).isoformat()
for customer, seed, plugins, not_before, expires, note in demo:
sources = _demo_sources(seed)
payload = build_payload(
machine_code=machine_code(sources),
plugins=plugins,
sources=sources,
customer=customer,
not_before=_day(not_before),
expires_at=_day(expires),
note="示例数据(由 tools/screenshot.py 生成)",
)
doc = build_license(payload, private_key, kid="vendor-demo")
window.registry.save(doc, public_key_fp=fingerprint, source="gui")
registry_tab.refresh()
# ---- 3) 签发页填好一份"正在签发"的数据 ------------------------------
issue = window.issue_tab
request_code = encode_request_code(_demo_sources("dev-new-42"))
issue.rc_edit.setPlainText(request_code)
issue._parse_request_code()
issue._set_all_checked(True)
issue.customer_edit.setText("南通冷链物流(示例)")
issue.note_edit.setText("合同 NT-2026-0921")
issue.kid_edit.setText("vendor-2026")
issue.days_spin.setValue(365)
issue._refresh_preview()
# ---- 4) 查看/校验页:先出一张"通过",再出一张"异机被拒" -----------
# 用**有效**(而非已过期)的那份来演示"校验通过",否则截图会是一张红条:
# 过期件也"签名有效",但结论是失败,拿它当"通过"示例会误导阅读手册的人。
ok_index = 2 # 宁波港机(示例):active
ok_sources = _demo_sources(demo[ok_index][1])
ok_license = sandbox / "license_active.lic"
ok_payload = build_payload(
machine_code=machine_code(ok_sources),
plugins=demo[ok_index][2],
sources=ok_sources,
customer=demo[ok_index][0],
expires_at=_day(demo[ok_index][4]),
note="示例数据(由 tools/screenshot.py 生成)",
)
write_license(build_license(ok_payload, private_key, kid="vendor-demo"), ok_license)
inspect = window.inspect_tab
inspect.lic_edit.setText(str(ok_license))
inspect.pub_edit.setText(public_b64)
inspect.rb_remote.setChecked(True)
inspect.rc_edit.setText(encode_request_code(ok_sources))
inspect._run()
# ---- 5) 逐页截图 ---------------------------------------------------
OUT_DIR.mkdir(parents=True, exist_ok=True)
shots: list[Path] = []
def _grab(widget, name: str) -> Path:
widget.show()
# 两轮事件循环:第一轮完成布局,第二轮把重绘落到位,再抓图才不会是半成品
QApplication.processEvents()
QApplication.processEvents()
target = OUT_DIR / name
widget.grab().save(str(target), "PNG")
shots.append(target)
return target
def _fit(page, minimum: int = 700) -> int:
"""把窗口调到"本页内容刚好放得下",避免截图中出现滚动条或截断。"""
area = window._area_by_page[page]
height = minimum
while height < 2000:
window.resize(WINDOW_WIDTH, height)
QApplication.processEvents()
if area.verticalScrollBar().maximum() == 0:
break
height += 60
return height
# 签发页
window.tabs.setCurrentWidget(window._area_by_page[issue])
_fit(issue)
_grab(window, "01-issue.png")
# 台账页
window.tabs.setCurrentWidget(window._area_by_page[registry_tab])
_fit(registry_tab)
_grab(window, "02-registry.png")
# 密钥页
window.tabs.setCurrentWidget(window._area_by_page[window.keys_tab])
_fit(window.keys_tab)
_grab(window, "03-keys.png")
# 查看/校验页:通过
window.tabs.setCurrentWidget(window._area_by_page[inspect])
_fit(inspect)
_grab(window, "04-inspect-ok.png")
# 查看/校验页:异机被拒(换一台机器的申请码)
inspect.rc_edit.setText(encode_request_code(_demo_sources("dev-other-99")))
inspect._run()
_grab(window, "05-inspect-fail.png")
# 续期对话框
records = registry_tab.records
renew_target = next((r for r in records if r.status() == "expiring"), records[0])
dialog = RenewDialog(renew_target, window)
dialog.resize(660, dialog.sizeHint().height())
_grab(dialog, "06-renew.png")
dialog.close()
# 改插件对话框:勾一个原先没有的、去掉一个原先有的,"新增/移除"差异才看得见
plugins_dialog = ChangePluginsDialog(renew_target, window.settings, window)
plugins_dialog.plugin_picker.ensure_types(["video_matrix_adapter"])
plugins_dialog.plugin_picker.set_checked(
[*renew_target.plugins[:1], "video_matrix_adapter"]
)
plugins_dialog.resize(660, plugins_dialog.sizeHint().height())
_grab(plugins_dialog, "07-change-plugins.png")
plugins_dialog.close()
# ---- 6) 收尾 -------------------------------------------------------
print(f"截图输出目录:{OUT_DIR}")
for path in shots:
print(f" {path.name} {path.stat().st_size // 1024} KB")
print(f"\n演示用临时目录(可直接删除):{sandbox}")
return 0
if __name__ == "__main__":
sys.exit(main())