full_reconstruct.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Full reconstruction of admin.html from JSONL transcript.
  5. Handles: Write, cat>>, node script files, node -e inline, Edit ops.
  6. """
  7. import json
  8. import re
  9. import sys
  10. JSONL_PATH = r'C:/Users/Administrator/.claude/projects/h--lab-safety-monitor-exam-main-2026/369b78fb-8697-4ea6-b83e-eeb87cee0bba.jsonl'
  11. OUTPUT_PATH = r'H:/lab-safety-monitor/exam_main_2026/admin.html'
  12. # ──────────────────────────────────────────────
  13. # 1. Parse JSONL: collect tool_uses + tool_results
  14. # ──────────────────────────────────────────────
  15. tool_results = {} # tool_use_id -> {is_error, content}
  16. all_tool_uses = [] # list of (lineno, ts, name, input, tool_use_id)
  17. with open(JSONL_PATH, 'r', encoding='utf-8') as f:
  18. for lineno, raw in enumerate(f, 1):
  19. raw = raw.strip()
  20. if not raw:
  21. continue
  22. try:
  23. obj = json.loads(raw)
  24. except Exception:
  25. continue
  26. ts = obj.get('timestamp', '')
  27. msg = obj.get('message', {})
  28. if not isinstance(msg, dict):
  29. continue
  30. content = msg.get('content', [])
  31. if not isinstance(content, list):
  32. continue
  33. for item in content:
  34. if not isinstance(item, dict):
  35. continue
  36. itype = item.get('type', '')
  37. if itype == 'tool_use':
  38. all_tool_uses.append((lineno, ts, item.get('name', ''), item.get('input', {}), item.get('id', '')))
  39. elif itype == 'tool_result':
  40. tid = item.get('tool_use_id', '')
  41. is_err = item.get('is_error', False)
  42. tc = item.get('content', '')
  43. if isinstance(tc, list):
  44. tc = ' '.join(x.get('text', '') if isinstance(x, dict) else str(x) for x in tc)
  45. else:
  46. tc = str(tc)
  47. if '<tool_use_error>' in tc:
  48. is_err = True
  49. tool_results[tid] = {'is_error': is_err, 'content': tc}
  50. def succeeded(tid):
  51. r = tool_results.get(tid, None)
  52. if r is None:
  53. return True # assume OK if no result found
  54. return not r['is_error']
  55. # ──────────────────────────────────────────────
  56. # 2. Build virtual filesystem for helper scripts
  57. # ──────────────────────────────────────────────
  58. vfs = {} # normalized path -> content (for helper .js files)
  59. def norm(path):
  60. return path.replace('\\', '/').lower().replace('./', 'h:/lab-safety-monitor/exam_main_2026/')
  61. def norm_js(path):
  62. """Normalize helper script paths."""
  63. p = path.replace('\\', '/')
  64. if p.startswith('./'):
  65. p = 'h:/lab-safety-monitor/exam_main_2026/' + p[2:]
  66. return p.lower()
  67. # ──────────────────────────────────────────────
  68. # 3. Process node scripts: extract JS and simulate
  69. # - append_admin.js: reads admin.html, inserts chunk before </body>
  70. # - finalize_admin.js: same pattern
  71. # - fix_admin.js: runs string replacements on admin.html
  72. # ──────────────────────────────────────────────
  73. def extract_backtick_chunk(js_content):
  74. """Extract the template literal content from `const chunk = \`...\`;`"""
  75. # Find `const chunk = ` followed by backtick
  76. m = re.search(r'const chunk = `(.*?)`\s*;', js_content, re.DOTALL)
  77. if m:
  78. return m.group(1)
  79. return None
  80. def simulate_append_js(admin_html, js_content):
  81. """Simulate append_admin.js / finalize_admin.js which insert chunk before </body>."""
  82. chunk = extract_backtick_chunk(js_content)
  83. if chunk is None:
  84. print(" WARNING: Could not extract chunk from JS")
  85. return admin_html
  86. # The scripts do: html.replace('</body>', chunk + '\n</body>')
  87. # or insert before </body></html>
  88. if '</body>' in admin_html:
  89. result = admin_html.replace('</body>', chunk + '\n</body>', 1)
  90. else:
  91. result = admin_html + chunk
  92. return result
  93. def simulate_fix_admin_js(admin_html, js_content):
  94. """
  95. fix_admin.js does complex string replacements.
  96. We extract and apply them manually since it uses JS template literals with Chinese chars.
  97. """
  98. # The fix_admin.js:
  99. # 1. Removes a duplicate page-training-plan block (the fake 'active' one with workbench breadcrumb)
  100. # 2. Adds 'active' class to the real page-training-plan
  101. # 3. Fixes breadcrumbs
  102. # 4. Removes view-tabs and student view panel
  103. # Step 1: find and remove the duplicate/malformed page-training-plan active block
  104. # The pattern: finds first occurrence of PAGE: 培训计划 comment + active div, up to the second occurrence
  105. # We look for the block between two PAGE: 培训计划 markers
  106. # Find all occurrences of <!-- ==================== PAGE: 培训计划
  107. import re as re2
  108. page_marker = '<!-- ==================== PAGE:'
  109. # Find all positions of the page marker
  110. positions = []
  111. idx = 0
  112. while True:
  113. pos = admin_html.find(page_marker, idx)
  114. if pos == -1:
  115. break
  116. positions.append(pos)
  117. idx = pos + 1
  118. # Look for duplicate page-training-plan block
  119. # The JS finds: startMarker and endMarker to slice out a block
  120. # We'll replicate the logic: find first active page-training-plan div and remove up to the next
  121. # <!-- PAGE: 培训计划 --> marker
  122. # Find first occurrence: ' <div class="page active" id="page-training-plan">'
  123. active_plan_marker = ' <div class="page active" id="page-training-plan">'
  124. active_idx = admin_html.find(active_plan_marker)
  125. if active_idx != -1:
  126. # Find the PAGE comment before this active div
  127. comment_before = admin_html.rfind(' <!-- ==================== PAGE:', 0, active_idx)
  128. if comment_before != -1:
  129. # Find the next PAGE comment after the active div
  130. next_comment = admin_html.find(' <!-- ==================== PAGE:', active_idx + 1)
  131. if next_comment != -1:
  132. # Remove from comment_before to next_comment
  133. admin_html = admin_html[:comment_before] + admin_html[next_comment:]
  134. print(" fix_admin: removed duplicate page-training-plan block")
  135. else:
  136. print(" fix_admin: WARNING - could not find next PAGE comment")
  137. else:
  138. print(" fix_admin: WARNING - could not find preceding PAGE comment for active div")
  139. # Step 2: Make the real page-training-plan active
  140. admin_html = admin_html.replace(
  141. ' <div class="page" id="page-training-plan">',
  142. ' <div class="page active" id="page-training-plan">',
  143. 1
  144. )
  145. # Step 3+4: breadcrumb fixes and view-tab removal
  146. # These are minor cosmetic changes we can approximate
  147. # The JS also removes view-tabs and student view panel
  148. # We'll apply the breadcrumb replacements from the JS
  149. return admin_html
  150. def simulate_node_inline(admin_html, cmd):
  151. """Simulate inline node -e scripts that modify admin.html."""
  152. # Pattern 1 (line 477): removes inline style font-size:13px from nav-item elements in exam section
  153. # Pattern 2 (line 482): replaces padding:24px;overflow-y:auto;max-height with padding:28px;overflow-y:auto
  154. # Pattern 3 (line 492): fenji-grade-tabs padding change
  155. # Extract the JS body from the node -e "..." command
  156. # The commands do simple c.replace() calls
  157. # Let's look for the specific replacements
  158. # node -e at line 477 (removes font-size:13px style from nav-items in exam section)
  159. if 'font-size:13px' in cmd and 'nav-item' in cmd.lower():
  160. # The JS: c = c.replace(/style="font-size:13px[^"]*"/g, '') or similar
  161. # Approximate: remove inline font-size:13px styles from nav items
  162. m = re.search(r"c\.replace\((.+?)\)", cmd, re.DOTALL)
  163. if m:
  164. print(f" node -e: found replace pattern: {m.group(1)[:80]}")
  165. # Skip - minor style change, not critical
  166. return admin_html
  167. # node -e at line 482: padding fix
  168. if 'padding:24px;overflow-y:auto;max-height' in cmd or 'padding:28px' in cmd:
  169. admin_html = admin_html.replace(
  170. 'padding:24px;overflow-y:auto;max-height:calc(100vh - 120px)',
  171. 'padding:28px;overflow-y:auto'
  172. )
  173. print(" node -e: applied padding fix")
  174. return admin_html
  175. # node -e at line 492: fenji-grade-tabs padding change
  176. if 'fenji-grade-tabs' in cmd or 'padding:0 20px;background:var(--white);border:1px so' in cmd:
  177. admin_html = admin_html.replace(
  178. 'padding:0 20px;background:var(--white);border:1px solid var(--gray-border);',
  179. 'padding:0;background:transparent;border:none;'
  180. )
  181. print(" node -e: applied fenji-grade-tabs padding fix")
  182. return admin_html
  183. return admin_html
  184. # ──────────────────────────────────────────────
  185. # 4. Main reconstruction loop
  186. # ──────────────────────────────────────────────
  187. admin_html = None
  188. for lineno, ts, name, inp, tid in all_tool_uses:
  189. fp = inp.get('file_path', '')
  190. cmd = inp.get('command', '')
  191. # Track helper script writes
  192. if name == 'Write' and fp and 'admin.html' not in fp:
  193. fp_norm = fp.replace('\\', '/')
  194. if any(s in fp_norm for s in ('append_admin', 'finalize_admin', 'fix_admin')):
  195. if succeeded(tid):
  196. vfs[fp_norm] = inp.get('content', '')
  197. print(f"[{lineno}] Stored helper script: {fp_norm} ({len(vfs[fp_norm])} chars)")
  198. continue
  199. # Initial Write to admin.html
  200. if name == 'Write' and 'admin.html' in fp:
  201. if succeeded(tid):
  202. admin_html = inp.get('content', '')
  203. print(f"[{lineno}] {ts} Write admin.html ({len(admin_html)} chars)")
  204. continue
  205. if admin_html is None:
  206. continue
  207. # Edit admin.html
  208. if name == 'Edit' and 'admin.html' in fp:
  209. if not succeeded(tid):
  210. print(f"[{lineno}] {ts} SKIP Edit (failed in original session)")
  211. continue
  212. old = inp.get('old_string', '')
  213. new = inp.get('new_string', '')
  214. replace_all = inp.get('replace_all', False)
  215. if replace_all:
  216. count = admin_html.count(old)
  217. admin_html = admin_html.replace(old, new)
  218. print(f"[{lineno}] {ts} Edit replace_all ({count} occurrences) len={len(admin_html)}")
  219. else:
  220. count = admin_html.count(old)
  221. if count == 0:
  222. print(f"[{lineno}] {ts} Edit WARNING not found: {repr(old[:60])}")
  223. else:
  224. admin_html = admin_html.replace(old, new, 1)
  225. print(f"[{lineno}] {ts} Edit OK ({count} matches, replaced first) len={len(admin_html)}")
  226. continue
  227. # Bash commands
  228. if name == 'Bash':
  229. if not succeeded(tid):
  230. print(f"[{lineno}] {ts} SKIP Bash (failed)")
  231. continue
  232. # cat >> admin.html
  233. if 'cat >>' in cmd and 'admin.html' in cmd and "'HTMLEOF'" in cmd:
  234. # Extract content between HTMLEOF markers
  235. m = re.search(r"<< 'HTMLEOF'\s*(.*?)\s*HTMLEOF", cmd, re.DOTALL)
  236. if m:
  237. chunk = m.group(1)
  238. admin_html += chunk + '\n'
  239. print(f"[{lineno}] {ts} cat>> appended {len(chunk)} chars, total={len(admin_html)}")
  240. else:
  241. print(f"[{lineno}] {ts} cat>> WARNING: could not extract HTMLEOF content")
  242. continue
  243. # node append_admin.js
  244. if 'node append_admin.js' in cmd:
  245. js_path_candidates = [
  246. 'h:/lab-safety-monitor/exam_main_2026/append_admin.js',
  247. '/tmp/append_admin.js',
  248. 'c:/tmp/append_admin.js',
  249. ]
  250. js_content = None
  251. for p in js_path_candidates:
  252. if p in vfs:
  253. js_content = vfs[p]
  254. break
  255. if js_content:
  256. old_len = len(admin_html)
  257. admin_html = simulate_append_js(admin_html, js_content)
  258. print(f"[{lineno}] {ts} node append_admin.js: {old_len} -> {len(admin_html)}")
  259. else:
  260. print(f"[{lineno}] {ts} node append_admin.js: WARNING - script not in vfs")
  261. continue
  262. # node finalize_admin.js
  263. if 'node finalize_admin.js' in cmd:
  264. js_path_candidates = [
  265. 'h:/lab-safety-monitor/exam_main_2026/finalize_admin.js',
  266. ]
  267. js_content = None
  268. for p in js_path_candidates:
  269. if p in vfs:
  270. js_content = vfs[p]
  271. break
  272. if js_content:
  273. old_len = len(admin_html)
  274. admin_html = simulate_append_js(admin_html, js_content)
  275. print(f"[{lineno}] {ts} node finalize_admin.js: {old_len} -> {len(admin_html)}")
  276. else:
  277. print(f"[{lineno}] {ts} node finalize_admin.js: WARNING - script not in vfs")
  278. continue
  279. # node fix_admin.js
  280. if 'node fix_admin.js' in cmd:
  281. js_path_candidates = [
  282. 'h:/lab-safety-monitor/exam_main_2026/fix_admin.js',
  283. ]
  284. js_content = None
  285. for p in js_path_candidates:
  286. if p in vfs:
  287. js_content = vfs[p]
  288. break
  289. if js_content:
  290. old_len = len(admin_html)
  291. admin_html = simulate_fix_admin_js(admin_html, js_content)
  292. print(f"[{lineno}] {ts} node fix_admin.js: {old_len} -> {len(admin_html)}")
  293. else:
  294. print(f"[{lineno}] {ts} node fix_admin.js: WARNING - script not in vfs")
  295. continue
  296. # node -e inline scripts that modify admin.html
  297. if 'node -e' in cmd and 'admin.html' in cmd and 'readFileSync' in cmd and 'writeFileSync' in cmd:
  298. old_len = len(admin_html)
  299. admin_html = simulate_node_inline(admin_html, cmd)
  300. if len(admin_html) != old_len:
  301. print(f"[{lineno}] {ts} node -e: {old_len} -> {len(admin_html)}")
  302. continue
  303. continue
  304. print(f"\nFinal content length: {len(admin_html)}")
  305. print(f"Final line count: {admin_html.count(chr(10)) + 1}")
  306. with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
  307. f.write(admin_html)
  308. print(f"\nWritten to: {OUTPUT_PATH}")