#!/usr/bin/env python3 """为 SOP 模板正文表非主操作区行添加 sop-row-hidden,并重新生成 seed SQL。""" from __future__ import annotations import re from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TEMPLATES_DIR = ROOT / "docs" / "sop-templates" SQL_DIR = ROOT / "docs" / "sql" SOP_CODES = [ "SOP-CC-001-E01-01", "SOP-CC-002-E01-01", "SOP-CC-003-E01-01", "SOP-CC-006-E01-01", "SOP-SC-003-E01-01", "SOP-SC-004-E01-01", "SOP-SC-005-E01-01", "SOP-SC-006-E01-01", "SOP-SC-007-E01-01", "SOP-SC-008-E01-01", "SOP-SC-009-E01-01", "SOP-SC-010-E01-01", "SOP-SC-011-E01-01", "SOP-SC-012-E01-01", "SOP-SC-013-E01-01", "SOP-SC-014-E01-01", ] SC014_NK_ROW_OLD = ( '\n' ' ' "NK细胞
培养\n" ' ' ) SC014_NK_ROW_NEW = ( '\n' ' ' "NK细胞
培养\n" ' ' ) def _is_tr_open(html: str, pos: int) -> bool: if not html.startswith("", "/", "\n", "\t", "\r") def _split_top_level_trs(html: str) -> list[str]: rows: list[str] = [] i = 0 n = len(html) while i < n: start = html.find("", pos): depth -= 1 pos += 5 if depth == 0: rows.append(html[start:pos]) i = pos break continue pos += 1 else: raise ValueError("unclosed in sop-body-table tbody") return rows def _add_hidden_class(tr_html: str) -> str: open_tag = tr_html.split(">", 1)[0] + ">" if "sop-fill-row" in open_tag: return tr_html def _patch_tr_tag(match: re.Match[str]) -> str: tag = match.group(0) if 'class="' in tag: return re.sub( r'class="([^"]*)"', lambda m: f'class="{m.group(1)} sop-row-hidden"', tag, count=1, ) return tag.replace("]*>", _patch_tr_tag, tr_html, count=1) def _find_body_tbody_span(html: str) -> tuple[int, int] | None: body_start = html.find('", body_start) if tbody_open < 0: return None inner_start = tbody_open + len("") table_depth = 1 tbody_depth = 1 pos = inner_start n = len(html) while pos < n: if html.startswith("", pos): table_depth -= 1 pos += 8 continue if html.startswith("", pos): tbody_depth += 1 pos += 7 continue if html.startswith("", pos): tbody_depth -= 1 pos += 8 if tbody_depth == 0: return inner_start, pos - 8 continue pos += 1 raise ValueError("无法定位 sop-body-table 的 tbody 结束位置") def process_body_table(html: str) -> str: span = _find_body_tbody_span(html) if span is None: return html inner_start, inner_end = span tbody_inner = html[inner_start:inner_end] rows = _split_top_level_trs(tbody_inner) processed = [_add_hidden_class(row) for row in rows] return html[:inner_start] + "".join(processed) + html[inner_end:] def process_template(html: str, code: str) -> str: html = html.replace( '
str: return html.replace("\\", "\\\\").replace("'", "''") def write_seed_sql(code: str, html: str) -> None: short = code.replace("SOP-", "").replace("-E01-01", "").lower() sql_path = SQL_DIR / f"2026-08-25-seed-sop-{short}-content.sql" comment = html.split("\n", 1)[0].strip().removeprefix("") escaped = sql_escape(html) sql_path.write_text( "\n".join( [ f"-- 种子数据: {code} {comment.split('(')[0].strip()}", f"-- 源文件: docs/sop-templates/{code}.html", "", "UPDATE `biz_process`", "SET `sop_content` = '" f"{escaped}" "',", " `update_time` = NOW()", f"WHERE `process_code` = '{code}'", " AND `del_flag` = 0;", "", ] ), encoding="utf-8", ) def write_deploy_bundle() -> Path: """合并 seed + sync SQL,供测试/生产一次性执行。""" deploy_path = SQL_DIR / "2026-08-28-sop-visible-section-only-deploy.sql" parts = [ "-- =============================================================================", "-- SOP 精简展示:仅保留主操作记录区(页眉与其余正文行均已隐藏)", "-- 生成时间: 由 backend/scripts/apply_sop_row_hidden.py 自动生成", "-- 执行顺序: 先 seed 工艺库 → 再 sync 路线/任务步骤", "--", "-- 【生产环境】推荐方式(二选一):", "-- A) mysql -h -u -p < docs/sql/2026-08-28-sop-visible-section-only-deploy.sql", "-- B) ENV_FILE=.env.production python3 backend/scripts/run_sop_visible_deploy.py", "-- =============================================================================", "", ] for code in SOP_CODES: short = code.replace("SOP-", "").replace("-E01-01", "").lower() seed = SQL_DIR / f"2026-08-25-seed-sop-{short}-content.sql" sync = SQL_DIR / f"2026-08-25-sync-sop-{short}-content-to-routes-tasks.sql" parts.append(f"-- ---------- {code} seed ----------") parts.append(seed.read_text(encoding="utf-8").strip()) parts.append("") if sync.exists(): parts.append(f"-- ---------- {code} sync ----------") parts.append(sync.read_text(encoding="utf-8").strip()) parts.append("") deploy_path.write_text("\n".join(parts) + "\n", encoding="utf-8") return deploy_path def main() -> None: for code in SOP_CODES: path = TEMPLATES_DIR / f"{code}.html" html = path.read_text(encoding="utf-8") updated = process_template(html, code) path.write_text(updated, encoding="utf-8") write_seed_sql(code, updated) print(f"{code}: done") deploy = write_deploy_bundle() print(f"deploy bundle: {deploy}") if __name__ == "__main__": main()