v2.2: 桌面版同步 DESTACO 风格选型 + 网页预览后台启动

桌面版 (仅网页版 v2.1 已做的部分, 本次补齐桌面端, 两端语义一致)
- 新增 OptionTile 控件类: 三态选项块 (可选/已选/不可选), 替代 ComboBox 下拉
  不可选取值置灰+删除线但保留占位 (让用户看到选项存在, 而非从列表消失);
  悬停青色高亮, 再点一次取消选择
- 新增零件号实时条: 复用与网页版同一套「候选型号最长公共前缀」算法,
  已确定片段深青实心 + 待定位数虚化 + 进度条, 选满唯一命中时整块转青色实心
- 维度标题后回显当前已选值
- range 参数 (连续域) 保留可编辑下拉, 无法枚举成卡片
- 参数联动仍复用 Configurator.AllowedOptions, 规则引擎未改动

网页预览后台启动 (无 PowerShell 窗口)
- 新增 tools/serve-web-bg.ps1: Start-Process -WindowStyle Hidden 启动,
  带 -Stop / -Status; 用命令行匹配定位进程 (比只看 PID 文件可靠)
- _start_web.bat / _start_all.bat 改用之, 不再弹窗口

工具
- ui-diagnose.ps1 增强: 原只检查 _boxes (下拉), 现同时检查选项块数量、
  每维度是否有可选值、零件号条输出 —— 可自动发现"某维度全被置灰无从下手"
- 新增 assess-combo-risk.py: 用量化口径评估生成候选的风险
  (以同缸径真实数据值域为官方值域的代理)
- 新增 make-pending-list.py: 重写待补清单, 增加 nearest_real / diff_from_nearest
  列, 每个候选与其最近的真实型号并列, 便于逐个核实

文档
- 新增 工作总结-2026-09.md: 两阶段工作回顾、发现的问题、踩坑经验

验证
- 编译首次通过; UI 诊断 40/40 系列 0 问题
- 零件号条实测: KC0032-10 / ULP32020AEK0 / UAGP075OK120WS0 / LAE140NN31 均完整;
  UCBP 正确提示"该组合对应 2 个型号" (暴露已知数据问题)

踩坑记录
- PowerShell 变量名大小写不敏感: $root 与 $Root 是同一变量, 互相覆盖导致
  启动脚本指向错误目录 (改用 $projRoot)
- .ps1 必须带 UTF-8 BOM, 否则 PS 5.1 按 ANSI 读, 中文注释破坏解析

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
wangruiguo
2026-09-15 18:27:11 +08:00
parent fc5ee1dd36
commit e94d2aebc9
10 changed files with 4083 additions and 3192 deletions

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
用【同缸径的真实数据值域】作为官方值域的代理, 评估生成候选的保守程度。
为什么用代理: 官方图册是双栏排版, pdftotext 后左右栏交错, 正则解析多次尝试均不可靠
(把真实数据都判成越界 = 解析错误)。而【真实型号】是最可靠的一手证据:
某缸径的真实型号里从未出现过的取值, 生成它就是推测。
判定口径 (保守):
某候选的每个参数值, 是否都在【同缸径的真实型号】中出现过?
- 全部出现过 → 属"组合缺失", 补全风险低
- 有值从未出现 → 属"引入新值", 补全风险高 (需人工/AI 核对官方图册)
"""
import csv, io, os, collections
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
DIMS = ['stroke', 'rod', 'orient', 'sensor', 'mount', 'style', 'pos', 'type']
def main():
rows = list(csv.DictReader(io.open(os.path.join(CAT, 'csv', 'ULP.csv'), encoding='utf-8-sig')))
real = [r for r in rows if r['step_file'] != 'PLACEHOLDER.step']
gen = [r for r in rows if r['step_file'] == 'PLACEHOLDER.step']
dims = [d for d in DIMS if d in rows[0]]
# 每个缸径的真实取值集合
domain = collections.defaultdict(lambda: collections.defaultdict(set))
for r in real:
for d in dims:
if r[d] != '':
domain[r['size']][d].add(r[d])
print('=' * 74)
print('真实数据里各缸径的取值域 (作为官方值域的代理)')
print('=' * 74)
for b in sorted(domain, key=int):
parts = [f'{d}={sorted(domain[b][d])}' for d in dims if domain[b][d]]
n = sum(1 for r in real if r['size'] == b)
print(f' ULP{b:>3s} ({n:3d} 个真实型号)')
for p in parts:
print(f' {p}')
# 评估生成候选
safe, risky = 0, 0
risky_detail = collections.Counter()
risky_models = []
for r in gen:
b = r['size']
novel = [f'{d}={r[d]}' for d in dims
if r[d] != '' and r[d] not in domain.get(b, {}).get(d, set())]
if novel:
risky += 1
for x in novel:
risky_detail[f'ULP{b} {x}'] += 1
if len(risky_models) < 5:
risky_models.append((r['model_code'], novel))
else:
safe += 1
print()
print('=' * 74)
print(f'生成候选 {len(gen)} 个的保守度评估')
print('=' * 74)
print(f' 低风险 (全部取值在同缸径真实数据中出现过): {safe} ({safe*100//len(gen)}%)')
print(f' 高风险 (含该缸径从未出现的取值) : {risky} ({risky*100//len(gen)}%)')
if risky_detail:
print('\n 高风险取值 TOP15 (该缸径真实数据里从未出现):')
for k, v in risky_detail.most_common(15):
print(f' {k:28s} {v:5d} 个候选')
print('\n 高风险样本:')
for mc, novel in risky_models:
print(f' {mc:18s} 新值 {novel}')
if __name__ == '__main__':
main()