From 724be707ec15ac6558f1561366dd367d75f28bf7 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 16 十月 2026 18:00:49 +0800
Subject: [PATCH] chore(scripts): 新增二维码与联调诊断脚本

---
 scripts/CompareQrRender.java    |   82 +++++++++
 scripts/diag-pill.cjs           |   65 +++++++
 scripts/split-hunks.py          |   49 +++++
 scripts/DecodeQr.java           |  146 ++++++++++++++++
 scripts/DiagnoseQrFailures.java |   91 ++++++++++
 scripts/diag-login.cjs          |   51 +++++
 scripts/diag-bagcode.cjs        |   50 +++++
 7 files changed, 534 insertions(+), 0 deletions(-)

diff --git a/scripts/CompareQrRender.java b/scripts/CompareQrRender.java
new file mode 100644
index 0000000..bfd4da6
--- /dev/null
+++ b/scripts/CompareQrRender.java
@@ -0,0 +1,82 @@
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * 鍒ゅ畾"瑙d笉寮�鐨勬爣绛�"鍒板簳鏄爣绛惧潖浜嗭紝杩樻槸瑙g爜鍣ㄧ殑灞�闄�
+ *
+ * 鐢ㄦ硶锛歫ava CompareQrRender <鏍囩鐩綍> <鏂囦欢鍚�1> [鏂囦欢鍚�2 ...]
+ *
+ * 鍋氭硶锛氱敤涓庡鍑哄畬鍏ㄧ浉鍚岀殑缂栫爜鍙傛暟锛堢籂閿欑骇鍒� H銆侀潤鍖� 4銆佽竟闀� 260锛夐噸鏂扮敓鎴愬悓涓�鐮佸�肩殑浜岀淮鐮侊紝
+ * 鍐嶄笌鏍囩鍥剧墖涓殑浜岀淮鐮佸尯鍩熼�愬儚绱犳瘮瀵广��
+ * - 瀹屽叏涓�鑷� 鈫� 鏍囩鏄鐮佸�肩殑蹇犲疄娓叉煋锛屼簩缁寸爜鏁版嵁鏈夋晥锛岃В涓嶅嚭鏄В鐮佸櫒鐨勯棶棰�
+ * - 瀛樺湪宸紓 鈫� 鏍囩娓叉煋鐜妭纭疄鍑轰簡闂
+ */
+public class CompareQrRender {
+
+    /** 涓� QrCodeImageUtils 淇濇寔涓�鑷� */
+    private static final int PADDING = 16;
+    private static final int QR_SIZE = 260;
+    /** 涓� MesProQrLabelBuilder 涓�鑷达細浜岀淮鐮佸唴瀹逛负杩芥函缃戝潃 + code 鍙傛暟 */
+    private static final String TRACE_BASE = "http://192.168.0.10:48080/trace.html";
+
+    public static void main(String[] args) throws Exception {
+        File dir = new File(args[0]);
+        for (int i = 1; i < args.length; i++) {
+            File f = new File(dir, args[i]);
+            String name = args[i];
+            String code = name.endsWith(".png") ? name.substring(0, name.length() - 4) : name;
+            BufferedImage label = ImageIO.read(f);
+            if (label == null) {
+                System.out.println(name + " -> 鏃犳硶璇诲彇");
+                continue;
+            }
+            BufferedImage expect = generate(code);
+            BufferedImage actual = label.getSubimage(PADDING, PADDING, QR_SIZE, QR_SIZE);
+            if (expect.getWidth() != actual.getWidth() || expect.getHeight() != actual.getHeight()) {
+                System.out.println(name + " -> 灏哄涓嶄竴鑷达紝鏃犳硶姣斿");
+                continue;
+            }
+            long diff = 0;
+            for (int y = 0; y < QR_SIZE; y++) {
+                for (int x = 0; x < QR_SIZE; x++) {
+                    if (expect.getRGB(x, y) != actual.getRGB(x, y)) {
+                        diff++;
+                    }
+                }
+            }
+            long total = (long) QR_SIZE * QR_SIZE;
+            System.out.printf("%s%n  閲嶆柊鐢熸垚涓庢爣绛句簩缁寸爜鍖哄煙閫愬儚绱犳瘮瀵癸細涓嶄竴鑷� %d / %d (%.4f%%)%n",
+                    name, diff, total, diff * 100.0 / total);
+            System.out.println("  鍒ゅ畾锛�" + (diff == 0
+                    ? "鏍囩涓鸿鐮佸�肩殑蹇犲疄娓叉煋 鈫� 浜岀淮鐮佹暟鎹湁鏁堬紝瑙d笉鍑哄睘瑙g爜绔棶棰�"
+                    : "鏍囩娓叉煋涓庨鏈熶笉绗� 鈫� 娓叉煋鐜妭瀛樺湪闂"));
+        }
+    }
+
+    private static BufferedImage generate(String code) throws Exception {
+        String content = TRACE_BASE + "?code=" + code;
+        Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
+        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
+        hints.put(EncodeHintType.MARGIN, 4);
+        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+        BitMatrix matrix = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, QR_SIZE, QR_SIZE, hints);
+        int w = matrix.getWidth();
+        int h = matrix.getHeight();
+        BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                img.setRGB(x, y, matrix.get(x, y) ? 0x000000 : 0xFFFFFF);
+            }
+        }
+        return img;
+    }
+}
diff --git a/scripts/DecodeQr.java b/scripts/DecodeQr.java
new file mode 100644
index 0000000..61f9c0a
--- /dev/null
+++ b/scripts/DecodeQr.java
@@ -0,0 +1,146 @@
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.Result;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.GlobalHistogramBinarizer;
+import com.google.zxing.common.HybridBinarizer;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 浜岀淮鐮佹爣绛捐В鐮佹牎楠�
+ *
+ * 鐢ㄦ硶锛歫ava DecodeQr <鏍囩鐩綍>
+ *
+ * 瀵规瘡寮犳爣绛惧垎鍒敤銆屾暣鍥俱�嶅拰銆岃鍒颁簩缁寸爜鍖哄煙銆嶄袱绉嶆柟寮忚В鐮侊細
+ * 鏁村浘澶辫触鑰岃鍥炬垚鍔燂紝璇存槑鏍囩鏈韩娌″潖锛屾槸涓嬫柟鏂囧瓧骞叉壈浜嗘娴嬪櫒锛屽睘瑙g爜绔棶棰橈紱
+ * 涓ょ鏂瑰紡閮藉け璐ワ紝鎵嶆槸鏍囩鐪熺殑涓嶅彲璇嗚銆�
+ */
+public class DecodeQr {
+
+    /** 鏍囩鎺掔増鍙傛暟锛岄』涓� QrCodeImageUtils 淇濇寔涓�鑷� */
+    private static final int PADDING = 16;
+    private static final int QR_SIZE = 260;
+    /** 瑁佸浘鏃跺悜澶栧鐣欑殑浣欓噺锛屼繚璇佷簩缁寸爜闈欏尯涓嶈鍒囨帀 */
+    private static final int CROP_MARGIN = 10;
+
+    public static void main(String[] args) throws Exception {
+        if (args.length == 0) {
+            System.out.println("鐢ㄦ硶: java DecodeQr <鏍囩鐩綍>");
+            return;
+        }
+        File dir = new File(args[0]);
+        File[] files = dir.listFiles((d, n) -> n.toLowerCase().endsWith(".png"));
+        if (files == null || files.length == 0) {
+            System.out.println("鐩綍鍐呮病鏈� PNG 鏂囦欢锛�" + dir.getAbsolutePath());
+            return;
+        }
+        Arrays.sort(files);
+
+        int fullOk = 0;
+        int cropOk = 0;
+        int bothFail = 0;
+        List<String> fullFailCropOk = new ArrayList<>();
+        List<String> hardFail = new ArrayList<>();
+        List<String> mismatched = new ArrayList<>();
+        String sampleContent = null;
+
+        for (File f : files) {
+            String name = f.getName();
+            String code = name.endsWith(".png") ? name.substring(0, name.length() - 4) : name;
+            BufferedImage image = ImageIO.read(f);
+            if (image == null) {
+                hardFail.add(name + " -> 鍥剧墖鏃犳硶璇诲彇");
+                bothFail++;
+                continue;
+            }
+            String full = decode(image);
+            String crop = decode(cropQrArea(image));
+            if (full != null) {
+                fullOk++;
+            }
+            if (crop != null) {
+                cropOk++;
+            }
+            if (full == null && crop == null) {
+                bothFail++;
+                hardFail.add(name + " -> 鏁村浘涓庤鍥惧潎瑙g爜澶辫触");
+                continue;
+            }
+            if (full == null) {
+                fullFailCropOk.add(name);
+            }
+            String text = full != null ? full : crop;
+            if (!text.contains(code)) {
+                mismatched.add(name + " -> 鍐呭涓庣爜鍊间笉绗�: " + text);
+            }
+            if (sampleContent == null) {
+                sampleContent = "鐮佸�� " + code + "\n        浜岀淮鐮佸唴瀹� " + text;
+            }
+        }
+
+        int total = files.length;
+        System.out.println("鏍囩鎬绘暟        : " + total);
+        System.out.printf ("鏁村浘瑙g爜鎴愬姛    : %d (%.2f%%)%n", fullOk, fullOk * 100.0 / total);
+        System.out.printf ("瑁佸浘瑙g爜鎴愬姛    : %d (%.2f%%)%n", cropOk, cropOk * 100.0 / total);
+        System.out.printf ("涓ょ鏂瑰紡閮藉け璐�  : %d (%.2f%%)%n", bothFail, bothFail * 100.0 / total);
+        System.out.println("鏁村浘澶辫触浣嗚鍥炬垚鍔燂紙妫�娴嬪彈涓嬫柟鏂囧瓧骞叉壈锛屾爣绛炬湰韬甯革級: " + fullFailCropOk.size());
+        System.out.println("\n--- 鏍锋湰 ---\n         " + sampleContent);
+
+        if (!fullFailCropOk.isEmpty()) {
+            System.out.println("\n--- 浠呮暣鍥惧け璐ョ殑鐮侊紙鍓� 10锛� ---");
+            fullFailCropOk.stream().limit(10).forEach(s -> System.out.println("  " + s));
+        }
+        if (!mismatched.isEmpty()) {
+            System.out.println("\n!!! 鍐呭涓庣爜鍊间笉绗︼紙鍓� 10锛� !!!");
+            mismatched.stream().limit(10).forEach(s -> System.out.println("  " + s));
+        }
+        if (!hardFail.isEmpty()) {
+            System.out.println("\n--- 鐪熸涓嶅彲璇嗚鐨勬爣绛撅紙鍓� 20锛� ---");
+            hardFail.stream().limit(20).forEach(s -> System.out.println("  " + s));
+        }
+    }
+
+    /** 瑁佸嚭浜岀淮鐮佹墍鍦ㄥ尯鍩燂紝鍘绘帀涓嬫柟鍙犲姞鐨勬枃瀛� */
+    private static BufferedImage cropQrArea(BufferedImage src) {
+        int x = Math.max(0, PADDING - CROP_MARGIN);
+        int y = Math.max(0, PADDING - CROP_MARGIN);
+        int w = Math.min(src.getWidth() - x, QR_SIZE + CROP_MARGIN * 2);
+        int h = Math.min(src.getHeight() - y, QR_SIZE + CROP_MARGIN * 2);
+        return src.getSubimage(x, y, w, h);
+    }
+
+    /** 灏濊瘯澶氱浜屽�煎寲绛栫暐瑙g爜锛屽叏閮ㄥけ璐ヨ繑鍥� null */
+    private static String decode(BufferedImage image) {
+        LuminanceSource source = new BufferedImageLuminanceSource(image);
+        Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
+        hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
+        hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
+        hints.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE));
+        // HybridBinarizer 閫傚悎鍏夌収涓嶅潎锛孏lobalHistogramBinarizer 瀵归珮瀵规瘮搴﹀浘鏇寸ǔ锛屼袱鑰呬簰琛�
+        for (var binarizer : List.of(new HybridBinarizer(source), new GlobalHistogramBinarizer(source))) {
+            try {
+                Result r = new MultiFormatReader().decode(new BinaryBitmap(binarizer), hints);
+                if (r != null && r.getText() != null) {
+                    return r.getText();
+                }
+            } catch (NotFoundException ignored) {
+                // 鎹笅涓�绉嶇瓥鐣�
+            } catch (Exception ignored) {
+                // 鍚屼笂
+            }
+        }
+        return null;
+    }
+}
diff --git a/scripts/DiagnoseQrFailures.java b/scripts/DiagnoseQrFailures.java
new file mode 100644
index 0000000..4f6292a
--- /dev/null
+++ b/scripts/DiagnoseQrFailures.java
@@ -0,0 +1,91 @@
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.Result;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.HybridBinarizer;
+
+import javax.imageio.ImageIO;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 瑙g爜澶辫触鏍锋湰鐨勬垚鍥犲垽瀹�
+ *
+ * 鐢ㄦ硶锛歫ava DiagnoseQrFailures <鏍囩鐩綍> <鏂囦欢鍚�1> [鏂囦欢鍚�2 ...]
+ *
+ * 鎬濊矾锛氭妸鍥剧墖鎸夋暣鏁板�嶆渶杩戦偦鏀惧ぇ鍚庨噸鏂拌В鐮併�備簩缁寸爜鏄函榛戠櫧鏂瑰潡锛�
+ * 鏈�杩戦偦鏀惧ぇ涓嶅紩鍏ユ柊淇℃伅锛屽彧鏄粰瑙g爜鍣ㄧ殑閲囨牱缃戞牸鏇村浣欏湴銆�
+ * - 鏀惧ぇ鍚庤兘瑙e嚭 鈫� 浜岀淮鐮佹暟鎹湰韬槸瀵圭殑锛屽彧鏄ā鍧楀瘑搴�/閲囨牱浣嶇疆杩囦簬涓寸晫
+ *   锛堝睘瑙g爜绔棶棰橈紝浣嗗嵃鍒峰昂瀵稿亸灏忔椂鐪熷疄鎵爜鏋篃鍙兘璇讳笉鍑猴紝搴旂缉鐭簩缁寸爜鍐呭鎴栧姞澶ф爣绛撅級
+ * - 鏀惧ぇ鍚庝粛瑙d笉鍑� 鈫� 璇ユ爣绛剧‘鏈夌己闄凤紝闇�鎸夊簾鍝佸鐞�
+ */
+public class DiagnoseQrFailures {
+
+    public static void main(String[] args) throws Exception {
+        File dir = new File(args[0]);
+        for (int i = 1; i < args.length; i++) {
+            File f = new File(dir, args[i]);
+            if (!f.isFile()) {
+                System.out.println(args[i] + " -> 鏂囦欢涓嶅瓨鍦�");
+                continue;
+            }
+            BufferedImage img = ImageIO.read(f);
+            String at1 = decode(img);
+            System.out.println("\n=== " + args[i] + " ===");
+            System.out.println("  鍘熷灏哄        : " + img.getWidth() + " x " + img.getHeight());
+            System.out.println("  1x 瑙g爜         : " + (at1 == null ? "澶辫触" : "鎴愬姛 -> " + at1));
+            if (at1 != null) {
+                continue;
+            }
+            boolean rescued = false;
+            for (int scale : List.of(2, 3, 4, 6)) {
+                BufferedImage up = scaleNearest(img, scale);
+                String text = decode(up);
+                System.out.println("  " + scale + "x 鏀惧ぇ瑙g爜     : " + (text == null ? "澶辫触" : "鎴愬姛 -> " + text));
+                if (text != null) {
+                    rescued = true;
+                    break;
+                }
+            }
+            System.out.println("  缁撹            : " + (rescued
+                    ? "浜岀淮鐮佹暟鎹湁鏁堬紝灞炴ā鍧楀瘑搴︿复鐣岋紙瑙g爜绔級锛岄潪搴熺爜"
+                    : "鏀惧ぇ鍚庝粛涓嶅彲璇嗚锛屾爣绛剧‘鏈夌己闄�"));
+        }
+    }
+
+    /** 鏁存暟鍊嶆渶杩戦偦鏀惧ぇ锛氫笉鎻掑�笺�佷笉寮曞叆鏂板儚绱犲�硷紝鍙斁澶ч噰鏍风┖闂� */
+    private static BufferedImage scaleNearest(BufferedImage src, int scale) {
+        BufferedImage out = new BufferedImage(
+                src.getWidth() * scale, src.getHeight() * scale, BufferedImage.TYPE_INT_RGB);
+        var g = out.createGraphics();
+        try {
+            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+            g.drawImage(src, 0, 0, out.getWidth(), out.getHeight(), null);
+        } finally {
+            g.dispose();
+        }
+        return out;
+    }
+
+    private static String decode(BufferedImage image) {
+        LuminanceSource source = new BufferedImageLuminanceSource(image);
+        Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
+        hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
+        hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
+        hints.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE));
+        try {
+            Result r = new MultiFormatReader().decode(
+                    new BinaryBitmap(new HybridBinarizer(source)), hints);
+            return r == null ? null : r.getText();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
diff --git a/scripts/diag-bagcode.cjs b/scripts/diag-bagcode.cjs
new file mode 100644
index 0000000..e1252f5
--- /dev/null
+++ b/scripts/diag-bagcode.cjs
@@ -0,0 +1,50 @@
+/** 琚嬬爜椤电粨鏋勮瘖鏂細鐧诲綍鍚� dump 鎼滅储椤逛笌宸ュ叿鏍忔寜閽紝鐢ㄤ簬鏍″噯娴嬭瘯閫夋嫨鍣� */
+const { chromium } = require('playwright');
+
+const BASE = 'http://127.0.0.1:5666';
+
+(async () => {
+  const browser = await chromium.launch({ headless: true });
+  const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
+  page.on('pageerror', (e) => console.log('pageerror:', e.message));
+
+  await page.goto(`${BASE}/auth/login`, { waitUntil: 'domcontentloaded' });
+  await page.locator('input[name=username]').waitFor({ timeout: 60_000 });
+  await page.locator('input[name=username]').fill('admin');
+  await page.locator('input[name=password]').fill('admin123');
+  await page.locator('button:has-text("鐧诲綍")').last().click();
+  await page.waitForURL((u) => !u.pathname.includes('/auth/login'), { timeout: 60_000 });
+  console.log('鐧诲綍鍚� URL:', page.url());
+  await page.waitForTimeout(3000);
+
+  console.log('\n--- 鑿滃崟椤癸紙鍚�滆鐮�/璧嬬爜鈥濈殑锛�---');
+  for (const el of await page.locator('a, span, div').all()) {
+    const t = (await el.innerText().catch(() => '')).trim();
+    if (t && t.length < 20 && /琚嬬爜|璧嬬爜|涓�琚嬩竴鐮�/.test(t)) {
+      console.log(JSON.stringify(t));
+    }
+  }
+
+  for (const target of ['/bagcode/bag-code', '/bagcode', '/mes/pro/bagCode']) {
+    await page.goto(`${BASE}${target}`, { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(4000);
+    const url = page.url();
+    const hasExport = (await page.getByRole('button', { name: '瀵煎嚭浜岀淮鐮�' }).count()) > 0;
+    console.log(`\n>>> 灏濊瘯 ${target} 鈫� 瀹為檯 ${url}锛屽鍑烘寜閽暟=${hasExport}`);
+    if (!hasExport) continue;
+
+    console.log('--- 鎼滅储杈撳叆妗� placeholder ---');
+    for (const el of await page.locator('input').all()) {
+      const ph = await el.getAttribute('placeholder');
+      if (ph && (await el.isVisible().catch(() => false))) console.log(JSON.stringify(ph));
+    }
+    console.log('--- 鍏ㄩ儴鍙鎸夐挳鏂囨湰 ---');
+    for (const el of await page.locator('button').all()) {
+      const t = (await el.innerText().catch(() => '')).trim();
+      if (t && (await el.isVisible().catch(() => false))) console.log(JSON.stringify(t));
+    }
+    break;
+  }
+
+  await browser.close();
+})();
diff --git a/scripts/diag-login.cjs b/scripts/diag-login.cjs
new file mode 100644
index 0000000..cc76649
--- /dev/null
+++ b/scripts/diag-login.cjs
@@ -0,0 +1,51 @@
+/** 鐧诲綍椤电粨鏋勮瘖鏂細鎶婂疄闄呮覆鏌撳嚭鏉ョ殑琛ㄥ崟鍏冪礌鍏ㄩ儴鎵撳嚭鏉� */
+const { chromium } = require('playwright');
+
+(async () => {
+  const browser = await chromium.launch({ headless: true });
+  const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
+  const errors = [];
+  page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
+  page.on('console', (m) => {
+    if (m.type() === 'error') errors.push('console: ' + m.text());
+  });
+  page.on('requestfailed', (r) =>
+    errors.push('reqfail: ' + r.url() + ' ' + (r.failure() || {}).errorText),
+  );
+
+  await page.goto('http://127.0.0.1:5666/auth/login', { waitUntil: 'domcontentloaded' });
+  await page.waitForTimeout(8000);
+
+  console.log('URL:', page.url());
+  console.log('TITLE:', await page.title());
+  const bodyText = (await page.locator('body').innerText().catch(() => '')).slice(0, 800);
+  console.log('--- 鍙鏂囨湰 ---');
+  console.log(bodyText);
+
+  console.log('--- input 鍏冪礌 ---');
+  for (const el of await page.locator('input').all()) {
+    console.log(
+      JSON.stringify({
+        type: await el.getAttribute('type'),
+        name: await el.getAttribute('name'),
+        autocomplete: await el.getAttribute('autocomplete'),
+        placeholder: await el.getAttribute('placeholder'),
+        id: await el.getAttribute('id'),
+        visible: await el.isVisible().catch(() => false),
+      }),
+    );
+  }
+
+  console.log('--- button/role=radio 鍏冪礌 ---');
+  for (const el of await page.locator('button, [role=radio], .ant-radio-wrapper').all()) {
+    const t = (await el.innerText().catch(() => '')).trim().slice(0, 40);
+    if (t) console.log(JSON.stringify({ tag: await el.evaluate((n) => n.tagName), text: t }));
+  }
+
+  console.log('--- 閿欒 ---');
+  console.log(errors.slice(0, 20).join('\n') || '(鏃�)');
+  await page.screenshot({
+    path: require('node:path').join(__dirname, 'qr-export-test-out', 'diag-login.png'),
+  });
+  await browser.close();
+})();
diff --git a/scripts/diag-pill.cjs b/scripts/diag-pill.cjs
new file mode 100644
index 0000000..2d5adb8
--- /dev/null
+++ b/scripts/diag-pill.cjs
@@ -0,0 +1,65 @@
+/** 娴爣灞曞紑璇婃柇锛氬垽鏂�岀偣娴爣鎭㈠闈㈡澘銆嶅け璐ユ槸鍓嶇 bug 杩樻槸娴嬭瘯閫夋嫨鍣ㄩ棶棰� */
+const { chromium } = require('playwright');
+
+const BASE = 'http://127.0.0.1:5666';
+const UUID = 'ae1229655a3e48d7b4bc842e3e74e15c';
+
+(async () => {
+  const browser = await chromium.launch({ headless: true });
+  const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
+  await page.addInitScript(() => {
+    Object.defineProperty(window, 'showSaveFilePicker', { value: undefined, configurable: true });
+  });
+
+  await page.goto(`${BASE}/auth/login`, { waitUntil: 'domcontentloaded' });
+  await page.locator('input[name=username]').waitFor({ timeout: 60_000 });
+  await page.locator('input[name=username]').fill('admin');
+  await page.locator('input[name=password]').fill('admin123');
+  await page.locator('button:has-text("鐧诲綍")').last().click();
+  await page.waitForURL((u) => !u.pathname.includes('/auth/login'), { timeout: 60_000 });
+
+  await page.goto(`${BASE}/bagcode/bag-code`, { waitUntil: 'domcontentloaded' });
+  await page.getByRole('button', { name: '瀵煎嚭浜岀淮鐮�' }).first().waitFor({ timeout: 60_000 });
+  await page.getByPlaceholder('璇疯緭鍏ョ敓鎴愮粍缂栧彿').fill(UUID);
+  const searchBtn = page.locator('button').filter({ hasText: /鎼淺s*绱�/ });
+  await searchBtn.first().click();
+  await page.waitForTimeout(3000);
+  await page.getByRole('button', { name: '瀵煎嚭浜岀淮鐮�' }).first().click();
+  await page.locator('text=姝e湪鐢熸垚浜岀淮鐮�').waitFor({ timeout: 90_000 });
+  console.log('闈㈡澘宸插嚭鐜�');
+
+  // 鐐归殣钘�
+  const hideBtn = page.locator('button').filter({ hasText: /闅怽s*钘�/ });
+  console.log('闅愯棌鎸夐挳鏁�:', await hideBtn.count());
+  await hideBtn.first().click();
+  await page.waitForTimeout(1500);
+
+  // 鎵惧嚭鎵�鏈夊惈銆屼簩缁寸爜瀵煎嚭銆嶇殑 div锛屾墦鍗板眰绾т笌浣嶇疆
+  const cands = page.locator('div').filter({ hasText: /浜岀淮鐮佸鍑�/ });
+  const n = await cands.count();
+  console.log('\n鍚�屼簩缁寸爜瀵煎嚭銆嶇殑 div 鏁伴噺:', n);
+  for (let i = 0; i < n; i++) {
+    const el = cands.nth(i);
+    const info = await el.evaluate((node) => ({
+      cls: node.className?.toString().slice(0, 70),
+      text: (node.innerText || '').replace(/\s+/g, ' ').slice(0, 40),
+      depth: (() => { let d = 0, p = node; while (p.parentElement) { d++; p = p.parentElement; } return d; })(),
+    }));
+    console.log(`  [${i}] depth=${info.depth} cls="${info.cls}" text="${info.text}"`);
+  }
+
+  const pill = cands.last();
+  console.log('\n鎷熺偣鍑� last() 鍏冪礌锛屽叾 class:', await pill.evaluate((n) => n.className?.toString().slice(0, 70)));
+  await pill.click();
+  await page.waitForTimeout(2000);
+
+  const bodyText = await page.locator('body').innerText();
+  console.log('\n鐐瑰嚮鍚庯細');
+  console.log('  body 鍚�屾鍦ㄧ敓鎴愪簩缁寸爜銆�:', bodyText.includes('姝e湪鐢熸垚浜岀淮鐮�'));
+  console.log('  body 鍚�屼簩缁寸爜瀵煎嚭銆�:', bodyText.includes('浜岀淮鐮佸鍑�'));
+  console.log('  闅愯棌鎸夐挳鏄惁杩樺湪:', await page.locator('button').filter({ hasText: /闅怽s*钘�/ }).count());
+  const m = bodyText.match(/姝e湪鐢熸垚锛圽d+\s*\/\s*\d+锛�/);
+  console.log('  杩涘害鏂囨湰:', m ? m[0] : '(鏈壘鍒�)');
+
+  await browser.close();
+})();
diff --git a/scripts/split-hunks.py b/scripts/split-hunks.py
new file mode 100644
index 0000000..811caf1
--- /dev/null
+++ b/scripts/split-hunks.py
@@ -0,0 +1,49 @@
+"""浠� git diff 琛ヤ竵涓彧淇濈暀鎸囧畾搴忓彿鐨� hunk锛岀敤浜庣簿纭殏瀛橈紙git add -p 鏄氦浜掑紡鐨勶紝鑴氭湰閲岀敤涓嶄簡锛夈��
+
+鐢ㄦ硶:
+  python split-hunks.py <琛ヤ竵鏂囦欢> <淇濈暀鐨刪unk搴忓彿,閫楀彿鍒嗛殧> [--list]
+  --list  鍙垪鍑� hunk 搴忓彿涓庨琛岋紝涓嶈緭鍑鸿ˉ涓�
+"""
+import sys
+
+# 蹇呴』寮哄埗 UTF-8 涓斾笉鍋氭崲琛岀炕璇戯細鍚﹀垯涓枃涓婁笅鏂囪浼氳鎸� GBK 鍐欏潖銆乗n 琚啓鎴� \r\n锛�
+# 涓よ�呴兘浼氳 git apply 鍖归厤涓嶄笂锛堢函 ASCII 鐨勮ˉ涓佷笉浼氭毚闇茶繖涓棶棰橈紝瀹规槗璇垽锛�
+try:
+    sys.stdout.reconfigure(encoding='utf-8', newline='')
+except Exception:
+    pass
+
+path = sys.argv[1]
+if len(sys.argv) < 3:
+    print(__doc__)
+    sys.exit(1)
+list_only = '--list' in sys.argv
+keep = set()
+if not list_only:
+    keep = {int(x) for x in sys.argv[2].split(',') if x.strip() != ''}
+
+lines = open(path, encoding='utf-8', errors='surrogateescape').read().split('\n')
+header, hunks, cur = [], [], None
+for ln in lines:
+    if ln.startswith('@@'):
+        if cur is not None:
+            hunks.append(cur)
+        cur = [ln]
+    elif cur is None:
+        header.append(ln)
+    else:
+        cur.append(ln)
+if cur is not None:
+    hunks.append(cur)
+
+if list_only:
+    for i, h in enumerate(hunks):
+        print(f'  [{i}] {h[0]}')
+    sys.exit(0)
+
+out = list(header)
+for i, h in enumerate(hunks):
+    if i in keep:
+        out.extend(h)
+# 缁撳熬琛ュ洖鎹㈣
+sys.stdout.write('\n'.join(out))

--
Gitblit v1.9.3