File size: 8,000 Bytes
ede84b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env python3
"""modutech_v30_2.json → modutech_v30_3.json — ใช้ "พนักงานฝ่ายขาย" แทน "ฝ่ายเทคนิค" (11 ต.ค. 2026)

ทำไม
  ผลรอบ 4 (RESULTS_phase2_answer_review.md): คำตอบมี "ฝ่ายเทคนิค" 22 จุด ปนกับ "ฝ่ายขาย" บางข้อในประโยคเดียว
  ("ยืนยันกับพนักงานฝ่ายขายหรือฝ่ายเทคนิค") ทั้งที่ช่องทางติดต่อที่บอตให้ลูกค้ามีแต่ฝ่ายขาย
  เจ้าของตัดสิน 11 ต.ค.: ให้ใช้ "พนักงานฝ่ายขาย" ทุกจุด

ทำอะไร
  ไล่ทุกสตริงในไฟล์ ที่มีคำว่า ฝ่ายเทคนิค
    "ยืนยันกับฝ่ายเทคนิคก่อนเสนอ" → "ยืนยันกับพนักงานฝ่ายขายก่อนใช้งานจริง"  (ก่อนเสนอ เป็นคำของฝ่ายขาย)
    "ฝ่ายเทคนิค" ที่เหลือ          → "พนักงานฝ่ายขาย"
  ของเดิมทุกจุดเก็บใน _text_patch_v30_3 ที่รากของไฟล์ (path · เดิม · ใหม่)
  ไม่เก็บเป็นฟิลด์คู่ข้าง ๆ เพราะบาง dict (variant) ถูกคัดลอกเข้า Context ทั้งก้อน — ข้อความเดิมจะหลุดไปถึง LLM

ไม่แตะ: ตัวเลข · สถานะ · ฟิลด์อื่น · จำนวนนับ · ไฟล์ต้นทาง

รัน: python patch_v30_3.py [modutech_v30_2.json] [modutech_v30_3.json] [--changelog CHANGELOG_v30_3.md]
"""
from __future__ import annotations

import argparse
import copy
import hashlib
import json
import sys

OLD = "ฝ่ายเทคนิค"
SUBS = [("ยืนยันกับฝ่ายเทคนิคก่อนเสนอ", "ยืนยันกับพนักงานฝ่ายขายก่อนใช้งานจริง"),
        ("ฝ่ายเทคนิค", "พนักงานฝ่ายขาย")]
LOG_KEY = "_text_patch_v30_3"


def fingerprint(o) -> str:
    return hashlib.sha256(json.dumps(o, ensure_ascii=False, sort_keys=True).encode()).hexdigest()


def fix(s: str) -> str:
    for a, b in SUBS:
        s = s.replace(a, b)
    return s


def walk(o, path, log):
    """แก้ในที่ คืน log ของจุดที่แก้"""
    if isinstance(o, dict):
        for k in list(o):
            o[k] = walk(o[k], f"{path}/{k}", log)
    elif isinstance(o, list):
        for i in range(len(o)):
            o[i] = walk(o[i], f"{path}/{i}", log)
    elif isinstance(o, str) and OLD in o:
        new = fix(o)
        log.append({"path": path, "old": o, "new": new})
        return new
    return o


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("src", nargs="?", default="modutech_v30_2.json")
    ap.add_argument("dst", nargs="?", default="modutech_v30_3.json")
    ap.add_argument("--changelog")
    a = ap.parse_args()
    if a.src == a.dst:
        raise SystemExit("ห้ามเขียนทับไฟล์ต้นทาง")

    data = json.load(open(a.src, encoding="utf-8"))
    if LOG_KEY in data:
        raise SystemExit(f"มี {LOG_KEY} อยู่แล้ว — ไฟล์นี้ถูกแพตช์แล้ว หยุด")
    shadow = copy.deepcopy(data)
    log: list[dict] = []
    walk(data, "", log)

    print(f"1) แก้ {len(log)} สตริง")
    left = json.dumps(data, ensure_ascii=False).count(OLD)
    if left:
        raise SystemExit(f"❌ ยังเหลือ '{OLD}' {left} จุด")
    if any("ก่อนเสนอ" in x["new"] and "ก่อนเสนอ" not in x["old"] for x in log):
        raise SystemExit("❌ เกิดคำว่า ก่อนเสนอ ใหม่")
    print(f"   ✅ ไม่เหลือ '{OLD}'")

    print("2) เทียบกับ shadow copy — ย้อนทุกจุดใน log แล้วต้องได้ไฟล์เดิมทุกไบต์")
    back = copy.deepcopy(data)
    for x in log:
        keys = x["path"].split("/")[1:]
        o = back
        for k in keys[:-1]:
            o = o[int(k)] if isinstance(o, list) else o[k]
        last = keys[-1]
        if isinstance(o, list):
            o[int(last)] = x["old"]
        else:
            o[last] = x["old"]
    if fingerprint(back) != fingerprint(shadow):
        raise SystemExit("❌ มีอย่างอื่นเปลี่ยน — ไม่เขียนไฟล์")
    print("   ✅ นอกจากจุดใน log ไม่มีอะไรเปลี่ยน")

    prods = data["products"]
    n_pair = sum(len(p.get("industries") or []) for p in prods)
    n_app = sum(len(x.get("applications") or []) for p in prods for x in p.get("industries") or [])
    if (len(prods), n_pair, n_app) != (68, 211, 651):
        raise SystemExit("❌ จำนวนนับเปลี่ยน")
    print(f"3) จำนวนนับ {len(prods)} · {n_pair} · {n_app}")

    data[LOG_KEY] = log
    json.dump(data, open(a.dst, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
    print(f"\nเขียน {a.dst} แล้ว · {a.src} ไม่ถูกแตะต้อง")

    if a.changelog:
        groups: dict[tuple, list] = {}
        for x in log:
            field = [k for k in x["path"].split("/") if not k.isdigit()][-1]
            groups.setdefault((field, x["old"], x["new"]), []).append(x["path"])
        out = [f"# CHANGELOG {a.dst}", "",
               f"สร้างจาก `{a.src}` ด้วย `patch_v30_3.py` · แทน \"ฝ่ายเทคนิค\" ด้วย \"พนักงานฝ่ายขาย\" {len(log)} สตริง",
               f"(\"ยืนยันกับฝ่ายเทคนิคก่อนเสนอ\" → \"ยืนยันกับพนักงานฝ่ายขายก่อนใช้งานจริง\") · ของเดิมเก็บใน `{LOG_KEY}`", "",
               "เหตุผล: เจ้าของตัดสิน 11 ต.ค. ให้ทุกจุดที่ให้ลูกค้ายืนยันใช้พนักงานฝ่ายขาย (ช่องทางติดต่อที่บอตให้มีแต่ฝ่ายขาย)", "",
               "ฟิลด์ที่ไปถึงคำตอบลูกค้า: `width_note_th` · `temperature_unconfirmed_th` · "
               "`forbidden_th` อยู่ในส่วนกฎของ prompt · `temperature_note_th` และ `answering_policy` ไม่ได้ส่งให้ LLM", "",
               f"## ก่อน → หลัง ({len(groups)} แบบข้อความ)", ""]
        for i, ((field, old, new), paths) in enumerate(sorted(groups.items(), key=lambda kv: (kv[0][0], -len(kv[1]))), 1):
            codes = sorted({data["products"][int(p.split('/')[2])]["belt_code"] for p in paths
                            if p.startswith("/products/")})
            out += [f"### {i}. `{field}` · {len(paths)} จุด" + (f" · {', '.join(codes)}" if codes else ""), "",
                    f"- ก่อน: {old}", f"- หลัง: {new}", ""]
        open(a.changelog, "w", encoding="utf-8").write("\n".join(out))
        print(f"เขียน {a.changelog} แล้ว")
    return 0


if __name__ == "__main__":
    sys.exit(main())