|
|
@@ -220,6 +220,100 @@ CREATE TABLE IF NOT EXISTS tools_ingest_log (
|
|
|
"""
|
|
|
|
|
|
|
|
|
+# step 维度归类结果独立表(3-I,见 docs/step_classification_3-I_设计.md):一行 = 某 case 某版本
|
|
|
+# 某工序某 step 某维度某子项的命中分类。与 mode_process 解耦,归类重跑只覆盖本表、不重写 steps blob;
|
|
|
+# 存 matched_path 全路径,使「某节点及其子树的所有帖」一句 SQL(LIKE 前缀)即可,且与显示同源。
|
|
|
+DDL_STEP_CLASSIFICATION = """
|
|
|
+CREATE TABLE IF NOT EXISTS step_classification (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ case_id VARCHAR(128) NOT NULL,
|
|
|
+ query_id VARCHAR(32) NULL COMMENT 'record 时的 post_id,便于溯源',
|
|
|
+ version VARCHAR(32) NULL COMMENT '对齐 mode_process 最新真实版,重跑覆盖',
|
|
|
+ procedure_id VARCHAR(16) NULL COMMENT 'p1,p2…',
|
|
|
+ step_id VARCHAR(16) NULL COMMENT 's1,s2…',
|
|
|
+ dimension VARCHAR(8) NOT NULL COMMENT '实质/形式/类型/作用/动作',
|
|
|
+ sub_index SMALLINT NOT NULL COMMENT '原值「、」拆分后的子项下标',
|
|
|
+ raw_term VARCHAR(255) NULL COMMENT '原始词',
|
|
|
+ matched_name VARCHAR(255) NULL COMMENT '命中分类名(单一最优;无命中不入库)',
|
|
|
+ matched_path VARCHAR(512) NULL COMMENT '命中分类全路径,如 /表象/视觉/空间/空间环境',
|
|
|
+ matched_id INT NULL COMMENT 'cat-api stable_id',
|
|
|
+ score FLOAT NULL,
|
|
|
+ match_run_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_step_dim_sub (case_id, version, procedure_id, step_id, dimension, sub_index),
|
|
|
+ KEY idx_case (case_id),
|
|
|
+ KEY idx_dim_name (dimension, matched_name),
|
|
|
+ KEY idx_dim_path (dimension, matched_path)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='step 维度归类结果(与 mode_process 解耦)';
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+# 工序解构拆表(3-II,见 docs/拆表_3-II_设计.md):process_run → process_procedure → process_step 三层,
|
|
|
+# 与旧 mode_process 并行。step_json 逐字保留原 step → ProcessPayload 还原零漂移;抽取列供 DB 层查询。
|
|
|
+DDL_PROCESS_RUN = """
|
|
|
+CREATE TABLE IF NOT EXISTS process_run (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ query_id VARCHAR(32) NOT NULL,
|
|
|
+ case_id VARCHAR(128) NOT NULL,
|
|
|
+ version VARCHAR(32) NOT NULL,
|
|
|
+ is_latest TINYINT NOT NULL DEFAULT 0 COMMENT '该 case 最新真实版=1(link_ 恒 0)',
|
|
|
+ platform VARCHAR(32) NULL,
|
|
|
+ post_title VARCHAR(512) NULL,
|
|
|
+ source JSON NULL,
|
|
|
+ model VARCHAR(64) NULL,
|
|
|
+ cost_usd DECIMAL(10,6) NULL,
|
|
|
+ duration_s FLOAT NULL,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY uk_q_case_ver (query_id, case_id, version),
|
|
|
+ KEY idx_case_latest (case_id, is_latest),
|
|
|
+ KEY idx_qid (query_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工序解构运行级(一行=一帖一版本)';
|
|
|
+"""
|
|
|
+
|
|
|
+DDL_PROCESS_PROCEDURE = """
|
|
|
+CREATE TABLE IF NOT EXISTS process_procedure (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ run_id BIGINT NOT NULL,
|
|
|
+ case_id VARCHAR(128) NOT NULL,
|
|
|
+ version VARCHAR(32) NOT NULL,
|
|
|
+ procedure_id VARCHAR(16) NULL,
|
|
|
+ seq SMALLINT NOT NULL,
|
|
|
+ name VARCHAR(255) NULL,
|
|
|
+ purpose TEXT NULL,
|
|
|
+ category VARCHAR(32) NULL,
|
|
|
+ declarations JSON NULL,
|
|
|
+ type_registry JSON NULL,
|
|
|
+ tools_used JSON NULL,
|
|
|
+ step_count INT NULL,
|
|
|
+ UNIQUE KEY uk_run_seq (run_id, seq),
|
|
|
+ KEY idx_run (run_id),
|
|
|
+ KEY idx_case_ver (case_id, version)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工序解构工序级(一行=一个 procedure)';
|
|
|
+"""
|
|
|
+
|
|
|
+DDL_PROCESS_STEP = """
|
|
|
+CREATE TABLE IF NOT EXISTS process_step (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ procedure_pk BIGINT NOT NULL,
|
|
|
+ case_id VARCHAR(128) NOT NULL,
|
|
|
+ version VARCHAR(32) NOT NULL,
|
|
|
+ step_id VARCHAR(16) NULL,
|
|
|
+ seq SMALLINT NOT NULL,
|
|
|
+ kind VARCHAR(16) NULL,
|
|
|
+ grp VARCHAR(16) NULL COMMENT '原 step.group(group 为保留字)',
|
|
|
+ via VARCHAR(255) NULL,
|
|
|
+ effect VARCHAR(255) NULL,
|
|
|
+ action VARCHAR(255) NULL,
|
|
|
+ substance VARCHAR(512) NULL,
|
|
|
+ form VARCHAR(512) NULL,
|
|
|
+ step_json JSON NOT NULL COMMENT '原 step 逐字 JSON,payload 还原即用它',
|
|
|
+ UNIQUE KEY uk_proc_seq (procedure_pk, seq),
|
|
|
+ KEY idx_procedure (procedure_pk),
|
|
|
+ KEY idx_case_ver (case_id, version),
|
|
|
+ KEY idx_action (action), KEY idx_substance (substance), KEY idx_form (form)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工序解构步骤级(一行=一个 step)';
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
def _ensure_column(cur, table, column, column_ddl):
|
|
|
"""给已存在的表幂等补列:列已存在则跳过(MySQL ADD COLUMN 无 IF NOT EXISTS)。
|
|
|
column_ddl 为 ADD COLUMN 后的完整定义,如 \"source JSON NULL ... AFTER post_title\"。"""
|
|
|
@@ -250,6 +344,10 @@ def init_tables():
|
|
|
cur.execute(DDL_TOOLS)
|
|
|
cur.execute(DDL_INGEST_LOG)
|
|
|
cur.execute(DDL_TOOLS_INGEST_LOG)
|
|
|
+ cur.execute(DDL_STEP_CLASSIFICATION)
|
|
|
+ cur.execute(DDL_PROCESS_RUN)
|
|
|
+ cur.execute(DDL_PROCESS_PROCEDURE)
|
|
|
+ cur.execute(DDL_PROCESS_STEP)
|
|
|
# 历史库迁移:version 由 VARCHAR(16) 放宽到 32,容纳 link_v_mopN_* 复制版本。
|
|
|
# MODIFY 幂等(已是 32 则 MySQL 元数据无操作),建表后表必存在,可安全执行。
|
|
|
for t in ("mode_process", "mode_tools"):
|
|
|
@@ -280,7 +378,7 @@ def init_tables():
|
|
|
_ensure_unique_index(cur, t, "uk_q_case_ver_seq",
|
|
|
"query_id, case_id, version, seq")
|
|
|
print("✅ 建表完成:search_process, search_tools, mode_process, mode_tools, "
|
|
|
- "knowledge_ingest_log, tools_ingest_log")
|
|
|
+ "knowledge_ingest_log, tools_ingest_log, step_classification")
|
|
|
finally:
|
|
|
conn.close()
|
|
|
|
|
|
@@ -290,7 +388,8 @@ def clear_tables():
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- for t in ("search_process", "search_tools", "mode_process", "mode_tools"):
|
|
|
+ for t in ("search_process", "search_tools", "mode_process", "mode_tools",
|
|
|
+ "process_run", "process_procedure", "process_step", "step_classification"):
|
|
|
cur.execute(f"TRUNCATE TABLE {t}")
|
|
|
print(f"🧹 已清空 {t}")
|
|
|
finally:
|
|
|
@@ -566,7 +665,7 @@ def fetch_queries(mode="process"):
|
|
|
for r in cur.fetchall():
|
|
|
if is_adopted_rel(r["overall_score"], r["rel"], r["publish_time"], r["repro"]):
|
|
|
hits[r["query_id"]] = hits.get(r["query_id"], 0) + 1
|
|
|
- cur.execute("SELECT query_id, COUNT(DISTINCT case_id) AS n FROM mode_process GROUP BY query_id")
|
|
|
+ cur.execute("SELECT query_id, COUNT(DISTINCT case_id) AS n FROM process_run GROUP BY query_id") # 3-II
|
|
|
np = {r["query_id"]: r["n"] for r in cur.fetchall()}
|
|
|
cur.execute("SELECT query_id, COUNT(DISTINCT case_id) AS n FROM mode_tools GROUP BY query_id")
|
|
|
nt = {r["query_id"]: r["n"] for r in cur.fetchall()}
|
|
|
@@ -595,19 +694,21 @@ def fetch_posts(query_id, mode="process"):
|
|
|
FROM {table} WHERE query_id=%s AND {_REAL_POST}
|
|
|
ORDER BY overall_score DESC, id""", (query_id,))
|
|
|
rows = cur.fetchall()
|
|
|
- cur.execute("SELECT DISTINCT case_id FROM mode_process WHERE query_id=%s", (query_id,))
|
|
|
+ cur.execute("SELECT DISTINCT case_id FROM process_run WHERE query_id=%s", (query_id,)) # 3-II
|
|
|
hp = {r["case_id"] for r in cur.fetchall()}
|
|
|
cur.execute("SELECT DISTINCT case_id FROM mode_tools WHERE query_id=%s", (query_id,))
|
|
|
ht = {r["case_id"] for r in cur.fetchall()}
|
|
|
- # 已归类(工序):hp 中各 case 最新真实版的工序里有任一步骤含 substanceMatch(与归类回写
|
|
|
- # 同口径)。库端 LIKE 算、只回 0/1,不拉 steps。聚合逻辑见 _categorized_from_rows
|
|
|
- # (不能只看 id 最大行——空 steps 的 procedure 行永远不含,会误判)。
|
|
|
+ # 已归类(工序):hp 中各 case 最新真实版的工序里有任一 step 含 substanceMatch(同归类回写口径)。
|
|
|
+ # 3-II:改查三表——run 级聚合「该 run 是否有任一 step_json 含 substanceMatch」,逻辑同 _categorized_from_rows。
|
|
|
hc = set()
|
|
|
if hp:
|
|
|
ph = ",".join(["%s"] * len(hp))
|
|
|
- cur.execute(f"""SELECT case_id, version, id,
|
|
|
- (LEFT(version,5)='link_') AS islink, (steps LIKE %s) AS cat
|
|
|
- FROM mode_process WHERE case_id IN ({ph})""",
|
|
|
+ cur.execute(f"""SELECT r.case_id, r.version, r.id,
|
|
|
+ (LEFT(r.version,5)='link_') AS islink,
|
|
|
+ EXISTS(SELECT 1 FROM process_step s
|
|
|
+ JOIN process_procedure p ON s.procedure_pk=p.id
|
|
|
+ WHERE p.run_id=r.id AND s.step_json LIKE %s) AS cat
|
|
|
+ FROM process_run r WHERE r.case_id IN ({ph})""",
|
|
|
['%substanceMatch%'] + list(hp))
|
|
|
hc = _categorized_from_rows(cur.fetchall())
|
|
|
finally:
|
|
|
@@ -679,7 +780,7 @@ def fetch_all_posts(mode="process", *, query_ids=None, case_ids=None, adopted_on
|
|
|
ORDER BY overall_score DESC, id""", params)
|
|
|
rows = cur.fetchall()
|
|
|
# has_process/has_tools 全局判定:跨 query 的「该帖是否已解构」,两张解构表各取一次
|
|
|
- cur.execute("SELECT DISTINCT case_id FROM mode_process")
|
|
|
+ cur.execute("SELECT DISTINCT case_id FROM process_run") # 3-II
|
|
|
hp = {r["case_id"] for r in cur.fetchall()}
|
|
|
cur.execute("SELECT DISTINCT case_id FROM mode_tools")
|
|
|
ht = {r["case_id"] for r in cur.fetchall()}
|
|
|
@@ -759,6 +860,9 @@ def replace_process(query_id, case_id, platform, post_title, payload,
|
|
|
tools_used, model, version, cost_usd, duration_s, seq)
|
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
|
""", rows)
|
|
|
+ # 3-II 双写:同一事务内覆盖写 process_run/procedure/step(新表为主,mode_process 兜底)
|
|
|
+ _split_write_run(cur, query_id, case_id, platform, post_title, source,
|
|
|
+ procedures, model, version, cost_usd, duration_s)
|
|
|
conn.commit()
|
|
|
return len(procedures)
|
|
|
except Exception:
|
|
|
@@ -769,36 +873,25 @@ def replace_process(query_id, case_id, platform, post_title, payload,
|
|
|
|
|
|
|
|
|
def fetch_process_versions(case_id):
|
|
|
+ # 3-II:版本列表读自三表(n=该版本工序数,口径同旧 mode_process 行数)
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- cur.execute("""SELECT version, COUNT(*) AS n, MAX(model) AS model
|
|
|
- FROM mode_process WHERE case_id=%s
|
|
|
- GROUP BY version
|
|
|
- ORDER BY (LEFT(version,5)='link_') ASC, MAX(id) DESC""", (case_id,))
|
|
|
+ cur.execute("""SELECT r.version, COUNT(p.id) AS n, MAX(r.model) AS model
|
|
|
+ FROM process_run r
|
|
|
+ LEFT JOIN process_procedure p ON p.run_id = r.id
|
|
|
+ WHERE r.case_id=%s
|
|
|
+ GROUP BY r.version
|
|
|
+ ORDER BY (LEFT(r.version,5)='link_') ASC, MAX(r.id) DESC""", (case_id,))
|
|
|
return cur.fetchall()
|
|
|
finally:
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
def fetch_process(case_id, version=None):
|
|
|
- """重建 {case_id, version, model, source, procedures:[...]}。version=None 取最新。"""
|
|
|
- conn = _conn()
|
|
|
- try:
|
|
|
- with conn.cursor() as cur:
|
|
|
- if version is None:
|
|
|
- cur.execute("""SELECT version FROM mode_process WHERE case_id=%s
|
|
|
- ORDER BY (LEFT(version,5)='link_') ASC, id DESC LIMIT 1""", (case_id,))
|
|
|
- row = cur.fetchone()
|
|
|
- if not row:
|
|
|
- return None
|
|
|
- version = row["version"]
|
|
|
- cur.execute("""SELECT * FROM mode_process WHERE case_id=%s AND version=%s
|
|
|
- ORDER BY id""", (case_id, version))
|
|
|
- rows = cur.fetchall()
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
- return _proc_payload(case_id, version, rows)
|
|
|
+ """重建 {case_id, version, model, source, procedures:[...]}。version=None 取最新。
|
|
|
+ 3-II:读自 process_run/procedure/step(返回形状与旧 mode_process 口径完全一致)。"""
|
|
|
+ return rebuild_process_from_split(case_id, version)
|
|
|
|
|
|
|
|
|
def fetch_all_process(case_ids=None, lite=False):
|
|
|
@@ -808,6 +901,7 @@ def fetch_all_process(case_ids=None, lite=False):
|
|
|
返回 {case_id: _proc_payload(...)};无解构记录的 case_id 不出现在结果里。"""
|
|
|
if case_ids is not None and not case_ids:
|
|
|
return {}
|
|
|
+ # 3-II:读自三表,3 条批量 SQL(run → procedure → step),避免按 case 逐次往返(保首屏性能)。
|
|
|
where, params = "", []
|
|
|
if case_ids is not None:
|
|
|
where = " WHERE case_id IN (" + ",".join(["%s"] * len(case_ids)) + ")"
|
|
|
@@ -815,24 +909,382 @@ def fetch_all_process(case_ids=None, lite=False):
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- cur.execute(f"SELECT * FROM mode_process{where} ORDER BY case_id, id", params)
|
|
|
+ cur.execute(f"SELECT * FROM process_run{where} ORDER BY case_id, id", params)
|
|
|
+ runs = cur.fetchall()
|
|
|
+ best = {} # case_id -> (key, run);口径同 fetch_process(is_latest→真实版→id 最大)
|
|
|
+ for r in runs:
|
|
|
+ c = r["case_id"]
|
|
|
+ k = ((r.get("is_latest") or 0), not str(r["version"]).startswith("link_"), r["id"])
|
|
|
+ if c not in best or k > best[c][0]:
|
|
|
+ best[c] = (k, r)
|
|
|
+ run_ids = [run["id"] for _k, run in best.values()]
|
|
|
+ procs, steps_by_proc = [], {}
|
|
|
+ if run_ids:
|
|
|
+ ph = ",".join(["%s"] * len(run_ids))
|
|
|
+ cur.execute(f"SELECT * FROM process_procedure WHERE run_id IN ({ph}) "
|
|
|
+ "ORDER BY run_id, seq, id", run_ids)
|
|
|
+ procs = cur.fetchall()
|
|
|
+ pid = [p["id"] for p in procs]
|
|
|
+ if pid:
|
|
|
+ ph2 = ",".join(["%s"] * len(pid))
|
|
|
+ cur.execute(f"SELECT procedure_pk, step_json FROM process_step "
|
|
|
+ f"WHERE procedure_pk IN ({ph2}) ORDER BY procedure_pk, seq, id", pid)
|
|
|
+ for s in cur.fetchall():
|
|
|
+ steps_by_proc.setdefault(s["procedure_pk"], []).append(
|
|
|
+ _loads(s["step_json"], {}))
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+ procs_by_run = {}
|
|
|
+ for p in procs:
|
|
|
+ procs_by_run.setdefault(p["run_id"], []).append(p)
|
|
|
+ out = {}
|
|
|
+ for cid, (_k, run) in best.items():
|
|
|
+ rprocs = procs_by_run.get(run["id"], [])
|
|
|
+ if lite:
|
|
|
+ procedures = [{"id": p["procedure_id"], "name": p["name"],
|
|
|
+ "steps": _lite_steps(steps_by_proc.get(p["id"], []))} for p in rprocs]
|
|
|
+ out[cid] = {"case_id": cid, "version": run["version"],
|
|
|
+ "title": run["post_title"], "procedures": procedures}
|
|
|
+ else:
|
|
|
+ procedures = [{
|
|
|
+ "id": p["procedure_id"], "name": p["name"], "purpose": p["purpose"],
|
|
|
+ "category": p["category"], "declarations": _loads(p["declarations"]),
|
|
|
+ "type_registry": _loads(p["type_registry"]),
|
|
|
+ "steps": steps_by_proc.get(p["id"], []),
|
|
|
+ "tools_used": _loads(p["tools_used"], [])} for p in rprocs]
|
|
|
+ out[cid] = {"case_id": cid, "version": run["version"], "platform": run["platform"],
|
|
|
+ "title": run["post_title"], "model": run["model"],
|
|
|
+ "cost_usd": float(run["cost_usd"]) if run["cost_usd"] is not None else None,
|
|
|
+ "duration_s": run["duration_s"], "source": _loads(run["source"]),
|
|
|
+ "procedures": procedures}
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def fetch_all_process_pairs():
|
|
|
+ """全量重跑归类用:每个有解构记录的 case 取其「最新真实版」(link_ 排后、id 最大)
|
|
|
+ 所在行的 (query_id, case_id)。选版口径与 fetch_process / fetch_all_process 完全一致,
|
|
|
+ 保证回写的版本即前端展示的版本。query_id 作 post_id 供 category-match record。
|
|
|
+ 返回 [(query_id, case_id)],按 case_id 排序、按 case 去重。"""
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, query_id, case_id, version FROM process_run ORDER BY case_id, id")
|
|
|
+ rows = cur.fetchall()
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+ best = {} # case_id -> ((is_real, id), query_id);取 max,与 fetch_all_process 同口径
|
|
|
+ for r in rows:
|
|
|
+ c = r["case_id"]
|
|
|
+ key = (not str(r["version"]).startswith("link_"), r["id"])
|
|
|
+ if c not in best or key > best[c][0]:
|
|
|
+ best[c] = (key, r["query_id"])
|
|
|
+ return [(qid, c) for c, (_k, qid) in sorted(best.items())]
|
|
|
+
|
|
|
+
|
|
|
+# ── 拆表(3-II):mode_process → process_run/procedure/step ────────────────────────
|
|
|
+
|
|
|
+def _jn(v):
|
|
|
+ """JSON 列搬运规整:先 _loads 再 _j,无论 pymysql 返回 str 还是已解析对象都得到合法 JSON 文本/None。"""
|
|
|
+ return _j(_loads(v))
|
|
|
+
|
|
|
+
|
|
|
+def _t(s, n):
|
|
|
+ """抽取列按字符数截断(只用于可查询列;完整值在 step_json,不丢)。None 原样。"""
|
|
|
+ return s if (s is None or len(str(s)) <= n) else str(s)[:n]
|
|
|
+
|
|
|
+
|
|
|
+def migrate_to_split_tables(truncate=True):
|
|
|
+ """把 mode_process 灌入 process_run/procedure/step(加法,不动 mode_process)。
|
|
|
+ 幂等:默认先 TRUNCATE 三表再灌。step_json 逐字保留原 step。返回 {runs, procedures, steps}。"""
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT * FROM mode_process ORDER BY query_id, case_id, version, seq, id")
|
|
|
rows = cur.fetchall()
|
|
|
finally:
|
|
|
conn.close()
|
|
|
- by_case = {}
|
|
|
+ # 每 case 最新真实版(口径同 fetch_all_process),用于置 is_latest
|
|
|
+ best = {}
|
|
|
+ for r in rows:
|
|
|
+ c = r["case_id"]; k = (not str(r["version"]).startswith("link_"), r["id"])
|
|
|
+ if c not in best or k > best[c][0]:
|
|
|
+ best[c] = (k, r["version"])
|
|
|
+ latest_ver = {c: v for c, (_k, v) in best.items()}
|
|
|
+ # 按 (query_id, case_id, version) 分组 = 一个 run(保序)
|
|
|
+ runs = {}
|
|
|
+ order = []
|
|
|
for r in rows:
|
|
|
- by_case.setdefault(r["case_id"], []).append(r)
|
|
|
+ key = (r["query_id"], r["case_id"], r["version"])
|
|
|
+ if key not in runs:
|
|
|
+ runs[key] = []; order.append(key)
|
|
|
+ runs[key].append(r)
|
|
|
+
|
|
|
+ nrun = nproc = nstep = 0
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ conn.begin()
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ if truncate:
|
|
|
+ for t in ("process_step", "process_procedure", "process_run"):
|
|
|
+ cur.execute(f"TRUNCATE TABLE {t}")
|
|
|
+ for key in order:
|
|
|
+ qid, cid, ver = key
|
|
|
+ prows = sorted(runs[key],
|
|
|
+ key=lambda r: (r["seq"] if r["seq"] is not None else 0, r["id"]))
|
|
|
+ first = prows[0]
|
|
|
+ is_latest = 1 if (not str(ver).startswith("link_") and latest_ver.get(cid) == ver) else 0
|
|
|
+ cur.execute("""INSERT INTO process_run
|
|
|
+ (query_id, case_id, version, is_latest, platform, post_title, source,
|
|
|
+ model, cost_usd, duration_s, created_at)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (qid, cid, ver, is_latest, first["platform"], first["post_title"],
|
|
|
+ _jn(first["source"]), first["model"], first["cost_usd"], first["duration_s"],
|
|
|
+ first["created_at"])) # 保留原解构时间(Dashboard 成本趋势按此)
|
|
|
+ run_id = cur.lastrowid; nrun += 1
|
|
|
+ for pseq, pr in enumerate(prows):
|
|
|
+ steps = _loads(pr["steps"], [])
|
|
|
+ cur.execute("""INSERT INTO process_procedure
|
|
|
+ (run_id, case_id, version, procedure_id, seq, name, purpose, category,
|
|
|
+ declarations, type_registry, tools_used, step_count)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (run_id, cid, ver, pr["procedure_id"], pseq, pr["name"], pr["purpose"],
|
|
|
+ pr["category"], _jn(pr["declarations"]), _jn(pr["type_registry"]),
|
|
|
+ _jn(pr["tools_used"]), pr["step_count"]))
|
|
|
+ proc_pk = cur.lastrowid; nproc += 1
|
|
|
+ for sseq, st in enumerate(steps if isinstance(steps, list) else []):
|
|
|
+ if not isinstance(st, dict):
|
|
|
+ st = {"value": st}
|
|
|
+ cur.execute("""INSERT INTO process_step
|
|
|
+ (procedure_pk, case_id, version, step_id, seq, kind, grp, via,
|
|
|
+ effect, action, substance, form, step_json)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (proc_pk, cid, ver, _t(st.get("id"), 16), sseq,
|
|
|
+ _t(st.get("kind"), 16), _t(st.get("group"), 16), _t(st.get("via"), 255),
|
|
|
+ _t(st.get("effect"), 255), _t(st.get("action"), 255),
|
|
|
+ _t(st.get("substance"), 512), _t(st.get("form"), 512),
|
|
|
+ _j(st)))
|
|
|
+ nstep += 1
|
|
|
+ conn.commit()
|
|
|
+ except Exception:
|
|
|
+ conn.rollback(); raise
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+ return {"runs": nrun, "procedures": nproc, "steps": nstep}
|
|
|
+
|
|
|
+
|
|
|
+def _steps_by_proc(cur, run_id):
|
|
|
+ """取某 run 下所有 step 的 step_json,按 procedure_pk 分组(组内按 seq 序)。"""
|
|
|
+ cur.execute("""SELECT procedure_pk, step_json FROM process_step
|
|
|
+ WHERE procedure_pk IN (SELECT id FROM process_procedure WHERE run_id=%s)
|
|
|
+ ORDER BY procedure_pk, seq, id""", (run_id,))
|
|
|
out = {}
|
|
|
- for cid, crows in by_case.items():
|
|
|
- # 选版本:非 link_ 优先(is_real=True 排前),再按 id 最大——口径同 fetch_process
|
|
|
- best = max(crows, key=lambda r: (not str(r["version"]).startswith("link_"), r["id"]))
|
|
|
- ver = best["version"]
|
|
|
- vrows = sorted((r for r in crows if r["version"] == ver),
|
|
|
- key=lambda r: (r["seq"] if r["seq"] is not None else 0, r["id"]))
|
|
|
- out[cid] = _proc_payload(cid, ver, vrows, lite=lite)
|
|
|
+ for s in cur.fetchall():
|
|
|
+ out.setdefault(s["procedure_pk"], []).append(_loads(s["step_json"], {}))
|
|
|
return out
|
|
|
|
|
|
|
|
|
+def _split_payload(cur, run, lite=False):
|
|
|
+ """run 行 → ProcessPayload(形状与 _proc_payload 完全一致)。需开放 cursor。无 run 返回 None。"""
|
|
|
+ if not run:
|
|
|
+ return None
|
|
|
+ cur.execute("SELECT * FROM process_procedure WHERE run_id=%s ORDER BY seq, id", (run["id"],))
|
|
|
+ procs = cur.fetchall()
|
|
|
+ sbp = _steps_by_proc(cur, run["id"])
|
|
|
+ case_id, version = run["case_id"], run["version"]
|
|
|
+ if lite:
|
|
|
+ procedures = [{
|
|
|
+ "id": p["procedure_id"], "name": p["name"],
|
|
|
+ "steps": _lite_steps(sbp.get(p["id"], [])),
|
|
|
+ } for p in procs]
|
|
|
+ return {"case_id": case_id, "version": version,
|
|
|
+ "title": run["post_title"], "procedures": procedures}
|
|
|
+ procedures = [{
|
|
|
+ "id": p["procedure_id"], "name": p["name"], "purpose": p["purpose"],
|
|
|
+ "category": p["category"], "declarations": _loads(p["declarations"]),
|
|
|
+ "type_registry": _loads(p["type_registry"]), "steps": sbp.get(p["id"], []),
|
|
|
+ "tools_used": _loads(p["tools_used"], []),
|
|
|
+ } for p in procs]
|
|
|
+ return {"case_id": case_id, "version": version, "platform": run["platform"],
|
|
|
+ "title": run["post_title"], "model": run["model"],
|
|
|
+ "cost_usd": float(run["cost_usd"]) if run["cost_usd"] is not None else None,
|
|
|
+ "duration_s": run["duration_s"],
|
|
|
+ "source": _loads(run["source"]), "procedures": procedures}
|
|
|
+
|
|
|
+
|
|
|
+def _select_run(cur, case_id, version=None, query_id=None):
|
|
|
+ """选 run:version 指定则精确取;否则取最新(is_latest 优先 → 非 link_ → id 最大,
|
|
|
+ 口径同旧 fetch_process)。query_id 给定则限定该 query。无则 None。"""
|
|
|
+ where = "case_id=%s"; params = [case_id]
|
|
|
+ if query_id is not None:
|
|
|
+ where += " AND query_id=%s"; params.append(query_id)
|
|
|
+ if version is not None:
|
|
|
+ cur.execute(f"SELECT * FROM process_run WHERE {where} AND version=%s LIMIT 1",
|
|
|
+ params + [version])
|
|
|
+ else:
|
|
|
+ cur.execute(f"""SELECT * FROM process_run WHERE {where}
|
|
|
+ ORDER BY is_latest DESC, (LEFT(version,5)='link_') ASC, id DESC
|
|
|
+ LIMIT 1""", params)
|
|
|
+ return cur.fetchone()
|
|
|
+
|
|
|
+
|
|
|
+def rebuild_process_from_split(case_id, version=None):
|
|
|
+ """从三表重建 ProcessPayload(迁移校验/读侧共用)。version=None 取最新。"""
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ return _split_payload(cur, _select_run(cur, case_id, version))
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+
|
|
|
+def _refresh_is_latest(cur, case_id):
|
|
|
+ """重算某 case 各 run 的 is_latest:最新真实版(非 link_、id 最大)置 1,其余 0。
|
|
|
+ 口径同 fetch_process 选版。只有 link_ 时无 1(读侧 rebuild 仍能按排序兜底)。需在事务内调用。"""
|
|
|
+ cur.execute("SELECT id, version FROM process_run WHERE case_id=%s", (case_id,))
|
|
|
+ rows = cur.fetchall()
|
|
|
+ if not rows:
|
|
|
+ return
|
|
|
+ cur.execute("UPDATE process_run SET is_latest=0 WHERE case_id=%s", (case_id,))
|
|
|
+ best = max(rows, key=lambda r: (not str(r["version"]).startswith("link_"), r["id"]))
|
|
|
+ if not str(best["version"]).startswith("link_"):
|
|
|
+ cur.execute("UPDATE process_run SET is_latest=1 WHERE id=%s", (best["id"],))
|
|
|
+
|
|
|
+
|
|
|
+def _split_write_run(cur, query_id, case_id, platform, post_title, source,
|
|
|
+ procedures, model, version, cost_usd, duration_s):
|
|
|
+ """在三表覆盖写某 (query_id, case_id, version) 的 run/procedure/step(需在调用方事务内)。
|
|
|
+ 与 replace_process 写 mode_process 同口径(tools_used 从 steps[].via 去重)。维护 is_latest。"""
|
|
|
+ cur.execute("SELECT id FROM process_run WHERE query_id=%s AND case_id=%s AND version=%s",
|
|
|
+ (query_id, case_id, version))
|
|
|
+ old = cur.fetchone()
|
|
|
+ if old:
|
|
|
+ cur.execute("""DELETE FROM process_step WHERE procedure_pk IN
|
|
|
+ (SELECT id FROM process_procedure WHERE run_id=%s)""", (old["id"],))
|
|
|
+ cur.execute("DELETE FROM process_procedure WHERE run_id=%s", (old["id"],))
|
|
|
+ cur.execute("DELETE FROM process_run WHERE id=%s", (old["id"],))
|
|
|
+ cur.execute("""INSERT INTO process_run
|
|
|
+ (query_id, case_id, version, is_latest, platform, post_title, source,
|
|
|
+ model, cost_usd, duration_s)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (query_id, case_id, version, 0, platform, (post_title or "")[:500],
|
|
|
+ _j(source), model, cost_usd, duration_s))
|
|
|
+ run_id = cur.lastrowid
|
|
|
+ for pseq, p in enumerate(procedures or []):
|
|
|
+ steps = p.get("steps") or []
|
|
|
+ vias = []
|
|
|
+ for s in steps:
|
|
|
+ v = s.get("via") if isinstance(s, dict) else None
|
|
|
+ if v and v not in vias:
|
|
|
+ vias.append(v)
|
|
|
+ cur.execute("""INSERT INTO process_procedure
|
|
|
+ (run_id, case_id, version, procedure_id, seq, name, purpose, category,
|
|
|
+ declarations, type_registry, tools_used, step_count)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (run_id, case_id, version, p.get("id"), pseq, (p.get("name") or "")[:250],
|
|
|
+ p.get("purpose"), p.get("category"), _j(p.get("declarations")),
|
|
|
+ _j(p.get("type_registry")), _j(vias), len(steps)))
|
|
|
+ proc_pk = cur.lastrowid
|
|
|
+ for sseq, st in enumerate(steps):
|
|
|
+ if not isinstance(st, dict):
|
|
|
+ st = {"value": st}
|
|
|
+ cur.execute("""INSERT INTO process_step
|
|
|
+ (procedure_pk, case_id, version, step_id, seq, kind, grp, via,
|
|
|
+ effect, action, substance, form, step_json)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (proc_pk, case_id, version, _t(st.get("id"), 16), sseq,
|
|
|
+ _t(st.get("kind"), 16), _t(st.get("group"), 16), _t(st.get("via"), 255),
|
|
|
+ _t(st.get("effect"), 255), _t(st.get("action"), 255),
|
|
|
+ _t(st.get("substance"), 512), _t(st.get("form"), 512), _j(st)))
|
|
|
+ _refresh_is_latest(cur, case_id)
|
|
|
+
|
|
|
+
|
|
|
+def _split_update_steps(cur, case_id, version, steps_in_order):
|
|
|
+ """归类回写:按工序顺序覆盖某 (case_id, version) 各 procedure 的 step_json(及抽取列)。
|
|
|
+ steps_in_order 与 process_procedure(按 seq) 一一对应。需在事务内。返回更新的 step 行数。"""
|
|
|
+ cur.execute("""SELECT id FROM process_procedure
|
|
|
+ WHERE case_id=%s AND version=%s ORDER BY seq, id""", (case_id, version))
|
|
|
+ proc_ids = [r["id"] for r in cur.fetchall()]
|
|
|
+ if len(proc_ids) != len(steps_in_order):
|
|
|
+ raise ValueError(f"process_procedure 行数({len(proc_ids)})与工序数({len(steps_in_order)})不一致")
|
|
|
+ n = 0
|
|
|
+ for proc_pk, steps in zip(proc_ids, steps_in_order):
|
|
|
+ cur.execute("DELETE FROM process_step WHERE procedure_pk=%s", (proc_pk,))
|
|
|
+ for sseq, st in enumerate(steps or []):
|
|
|
+ if not isinstance(st, dict):
|
|
|
+ st = {"value": st}
|
|
|
+ cur.execute("""INSERT INTO process_step
|
|
|
+ (procedure_pk, case_id, version, step_id, seq, kind, grp, via,
|
|
|
+ effect, action, substance, form, step_json)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
|
+ (proc_pk, case_id, version, _t(st.get("id"), 16), sseq,
|
|
|
+ _t(st.get("kind"), 16), _t(st.get("group"), 16), _t(st.get("via"), 255),
|
|
|
+ _t(st.get("effect"), 255), _t(st.get("action"), 255),
|
|
|
+ _t(st.get("substance"), 512), _t(st.get("form"), 512), _j(st)))
|
|
|
+ n += 1
|
|
|
+ return n
|
|
|
+
|
|
|
+
|
|
|
+# ── step_classification(归类独立表,3-I)────────────────────────────────────────
|
|
|
+
|
|
|
+def replace_step_classification(case_id, version, records):
|
|
|
+ """覆盖写某 (case_id, version) 的 step 维度归类结果(DELETE+INSERT 原子,语义同 replace_process)。
|
|
|
+ records:[{query_id, procedure_id, step_id, dimension, sub_index, raw_term,
|
|
|
+ matched_name, matched_path, matched_id, score}](只含有命中的子项)。
|
|
|
+ 同版本重跑幂等、洗净旧归属。返回写入条数。"""
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ conn.begin()
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ cur.execute("DELETE FROM step_classification WHERE case_id=%s AND version=%s",
|
|
|
+ (case_id, version))
|
|
|
+ if records:
|
|
|
+ rows = [(
|
|
|
+ case_id, r.get("query_id"), version,
|
|
|
+ r.get("procedure_id"), r.get("step_id"),
|
|
|
+ r.get("dimension"), r.get("sub_index"), r.get("raw_term"),
|
|
|
+ r.get("matched_name"), r.get("matched_path"),
|
|
|
+ r.get("matched_id"), r.get("score"),
|
|
|
+ ) for r in records]
|
|
|
+ cur.executemany("""
|
|
|
+ INSERT INTO step_classification
|
|
|
+ (case_id, query_id, version, procedure_id, step_id, dimension,
|
|
|
+ sub_index, raw_term, matched_name, matched_path, matched_id, score)
|
|
|
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
|
|
+ """, rows)
|
|
|
+ conn.commit()
|
|
|
+ return len(records)
|
|
|
+ except Exception:
|
|
|
+ conn.rollback()
|
|
|
+ raise
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+
|
|
|
+def fetch_classified_cases(dimension, path=None, name=None):
|
|
|
+ """维度联动取帖:返回某维度下命中「该节点(及其整棵子树)」的 distinct case_id 集合。
|
|
|
+ - path:给定分类全路径,取该节点 **及其子树**(matched_path = path 或 LIKE 'path/%')。
|
|
|
+ - name:给定分类名精确匹配(matched_name = name)。
|
|
|
+ path 与 name 至少给一个;都给则取并集口径以 path 为主(name 作补充 OR)。
|
|
|
+ 与 mode_process.steps[].substanceMatch 同源(同一次归类双写),不依赖 cat-api 树。"""
|
|
|
+ conds, params = [], [dimension]
|
|
|
+ if path:
|
|
|
+ conds.append("(matched_path = %s OR matched_path LIKE %s)")
|
|
|
+ params += [path, path.rstrip("/") + "/%"]
|
|
|
+ if name:
|
|
|
+ conds.append("matched_name = %s")
|
|
|
+ params.append(name)
|
|
|
+ if not conds:
|
|
|
+ return set()
|
|
|
+ where = "dimension=%s AND (" + " OR ".join(conds) + ")"
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ cur.execute(f"SELECT DISTINCT case_id FROM step_classification WHERE {where}", params)
|
|
|
+ return {r["case_id"] for r in cur.fetchall()}
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+
|
|
|
def fetch_process_by_query(query_id, case_id, version=None):
|
|
|
"""同 fetch_process,但用 (query_id, case_id) 精确定位某 query 下该帖的工序
|
|
|
(category-match 用:post_id=query_id / knowledge_id=case_id)。
|
|
|
@@ -840,20 +1292,9 @@ def fetch_process_by_query(query_id, case_id, version=None):
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- if version is None:
|
|
|
- cur.execute("""SELECT version FROM mode_process WHERE query_id=%s AND case_id=%s
|
|
|
- ORDER BY (LEFT(version,5)='link_') ASC, id DESC LIMIT 1""",
|
|
|
- (query_id, case_id))
|
|
|
- row = cur.fetchone()
|
|
|
- if not row:
|
|
|
- return None
|
|
|
- version = row["version"]
|
|
|
- cur.execute("""SELECT * FROM mode_process WHERE query_id=%s AND case_id=%s AND version=%s
|
|
|
- ORDER BY seq, id""", (query_id, case_id, version))
|
|
|
- rows = cur.fetchall()
|
|
|
+ return _split_payload(cur, _select_run(cur, case_id, version, query_id=query_id))
|
|
|
finally:
|
|
|
conn.close()
|
|
|
- return _proc_payload(case_id, version, rows)
|
|
|
|
|
|
|
|
|
def update_process_steps_by_query(query_id, case_id, version, steps_in_order):
|
|
|
@@ -874,6 +1315,8 @@ def update_process_steps_by_query(query_id, case_id, version, steps_in_order):
|
|
|
for row_id, steps in zip(ids, steps_in_order):
|
|
|
cur.execute("UPDATE mode_process SET steps=%s WHERE id=%s", (_j(steps), row_id))
|
|
|
n += cur.rowcount
|
|
|
+ # 3-II 双写:同步更新 process_step.step_json(同事务)
|
|
|
+ _split_update_steps(cur, case_id, version, steps_in_order)
|
|
|
conn.commit()
|
|
|
return n
|
|
|
except Exception:
|
|
|
@@ -902,6 +1345,8 @@ def update_process_steps(case_id, version, steps_in_order):
|
|
|
for row_id, steps in zip(ids, steps_in_order):
|
|
|
cur.execute("UPDATE mode_process SET steps=%s WHERE id=%s", (_j(steps), row_id))
|
|
|
n += cur.rowcount
|
|
|
+ # 3-II 双写:同步更新 process_step.step_json(同事务)
|
|
|
+ _split_update_steps(cur, case_id, version, steps_in_order)
|
|
|
conn.commit()
|
|
|
return n
|
|
|
except Exception:
|
|
|
@@ -933,16 +1378,25 @@ def fetch_categorized_cases(case_ids, mode="process"):
|
|
|
供前端判断「是否已全部归类 → 提示重新归类」。仅工序方向有意义(mode_process)。"""
|
|
|
if not case_ids:
|
|
|
return set()
|
|
|
- table = _mode_table(mode)
|
|
|
ph = ",".join(["%s"] * len(case_ids))
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- # 拉各行的 (case_id, version, id, islink, cat);steps LIKE 在库端算,不拉 steps 大字段。
|
|
|
- cur.execute(f"""SELECT case_id, version, id,
|
|
|
- (LEFT(version,5)='link_') AS islink, (steps LIKE %s) AS cat
|
|
|
- FROM {table} WHERE case_id IN ({ph})""",
|
|
|
- ['%substanceMatch%'] + list(case_ids))
|
|
|
+ if mode == "process":
|
|
|
+ # 3-II:run 级聚合「该 run 是否有任一 step_json 含 substanceMatch」
|
|
|
+ cur.execute(f"""SELECT r.case_id, r.version, r.id,
|
|
|
+ (LEFT(r.version,5)='link_') AS islink,
|
|
|
+ EXISTS(SELECT 1 FROM process_step s
|
|
|
+ JOIN process_procedure p ON s.procedure_pk=p.id
|
|
|
+ WHERE p.run_id=r.id AND s.step_json LIKE %s) AS cat
|
|
|
+ FROM process_run r WHERE r.case_id IN ({ph})""",
|
|
|
+ ['%substanceMatch%'] + list(case_ids))
|
|
|
+ else:
|
|
|
+ table = _mode_table(mode)
|
|
|
+ cur.execute(f"""SELECT case_id, version, id,
|
|
|
+ (LEFT(version,5)='link_') AS islink, (steps LIKE %s) AS cat
|
|
|
+ FROM {table} WHERE case_id IN ({ph})""",
|
|
|
+ ['%substanceMatch%'] + list(case_ids))
|
|
|
rows = cur.fetchall()
|
|
|
finally:
|
|
|
conn.close()
|
|
|
@@ -1102,7 +1556,24 @@ def fetch_extract(mode, case_id, version=None):
|
|
|
"""一次取版本列表 + 解构详情,复用同一条池连接、最少往返。
|
|
|
返回 {versions, data, missing}。mode: process / tools。"""
|
|
|
is_proc = mode != "tools"
|
|
|
- mtable = _mode_table("process" if is_proc else "tools")
|
|
|
+ if is_proc:
|
|
|
+ # 3-II:工序方向版本列表 + 详情都读自三表(形状不变)
|
|
|
+ conn = _conn()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cur:
|
|
|
+ cur.execute("""SELECT r.version, COUNT(p.id) AS n, MAX(r.model) AS model
|
|
|
+ FROM process_run r
|
|
|
+ LEFT JOIN process_procedure p ON p.run_id = r.id
|
|
|
+ WHERE r.case_id=%s GROUP BY r.version
|
|
|
+ ORDER BY (LEFT(r.version,5)='link_') ASC, MAX(r.id) DESC""", (case_id,))
|
|
|
+ versions = cur.fetchall()
|
|
|
+ target = version or (versions[0]["version"] if versions else None)
|
|
|
+ payload = _split_payload(cur, _select_run(cur, case_id, target)) if target else None
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+ return {"versions": versions, "data": payload, "missing": payload is None}
|
|
|
+ # 工具方向:仍读 mode_tools(本期不拆)
|
|
|
+ mtable = _mode_table("tools")
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
@@ -1111,7 +1582,6 @@ def fetch_extract(mode, case_id, version=None):
|
|
|
GROUP BY version
|
|
|
ORDER BY (LEFT(version,5)='link_') ASC, MAX(id) DESC""", (case_id,))
|
|
|
versions = cur.fetchall()
|
|
|
- # 详情:把"取最新版本"折进同一条 SQL,版本指定时直接用;省一次往返。
|
|
|
target = version or (versions[0]["version"] if versions else None)
|
|
|
rows = []
|
|
|
if target is not None:
|
|
|
@@ -1120,7 +1590,7 @@ def fetch_extract(mode, case_id, version=None):
|
|
|
rows = cur.fetchall()
|
|
|
finally:
|
|
|
conn.close()
|
|
|
- payload = (_proc_payload if is_proc else _tools_payload)(case_id, target, rows)
|
|
|
+ payload = _tools_payload(case_id, target, rows)
|
|
|
return {"versions": versions, "data": payload, "missing": payload is None}
|
|
|
|
|
|
|
|
|
@@ -1131,7 +1601,7 @@ def fetch_extract(mode, case_id, version=None):
|
|
|
def latest_real_version(case_id, mode="process"):
|
|
|
"""该 case 是否已有「真实」解构(任意 query;link_* 是复制品,不算源)。
|
|
|
返回最新一行 {"version","query_id"} 或 None。给解构前去重判定用。"""
|
|
|
- table = _mode_table(mode)
|
|
|
+ table = "process_run" if mode == "process" else _mode_table(mode) # 3-II:工序读三表的 run
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
@@ -1176,6 +1646,18 @@ def link_process(query_id, case_id, mode="process"):
|
|
|
cur.execute(
|
|
|
f"INSERT INTO {table} ({','.join(cols)}) VALUES ({','.join(['%s']*len(cols))})",
|
|
|
[row[k] for k in cols])
|
|
|
+ # 3-II 双写(仅工序方向):把 link_ 复制版同步写进三表
|
|
|
+ if mode == "process" and rows:
|
|
|
+ srows = sorted(rows, key=lambda r: (r.get("seq") if r.get("seq") is not None else 0))
|
|
|
+ f = srows[0]
|
|
|
+ procedures = [{
|
|
|
+ "id": r.get("procedure_id"), "name": r.get("name"), "purpose": r.get("purpose"),
|
|
|
+ "category": r.get("category"), "declarations": _loads(r.get("declarations")),
|
|
|
+ "type_registry": _loads(r.get("type_registry")), "steps": _loads(r.get("steps"), []),
|
|
|
+ } for r in srows]
|
|
|
+ _split_write_run(cur, query_id, case_id, f.get("platform"), f.get("post_title"),
|
|
|
+ _loads(f.get("source")), procedures, f.get("model"), newver,
|
|
|
+ 0, f.get("duration_s"))
|
|
|
return len(rows)
|
|
|
finally:
|
|
|
conn.close()
|
|
|
@@ -1210,7 +1692,7 @@ def fetch_adopted_process_cases(query_id=None):
|
|
|
sql = (f"SELECT DISTINCT s.case_id, s.overall_score, s.publish_time, "
|
|
|
f"{_REL_SQL} AS rel, {_REPRO_SQL} AS repro "
|
|
|
"FROM search_process s "
|
|
|
- "JOIN (SELECT DISTINCT case_id FROM mode_process) m ON s.case_id = m.case_id")
|
|
|
+ "JOIN (SELECT DISTINCT case_id FROM process_run) m ON s.case_id = m.case_id") # 3-II
|
|
|
params = ()
|
|
|
if query_id:
|
|
|
sql += " WHERE s.query_id=%s"
|
|
|
@@ -1396,17 +1878,25 @@ def fetch_dashboard_rows():
|
|
|
for p in st:
|
|
|
p["mode"] = "tools"
|
|
|
posts += st
|
|
|
- # 成本/耗时按全部版本计;steps 仅最新版需要 → 非最新版只回 NULL,省传输。
|
|
|
- cur.execute("""SELECT p.id, p.case_id, p.version, p.cost_usd, p.duration_s, p.created_at,
|
|
|
- CASE WHEN p.version = m.maxv THEN p.steps END AS steps
|
|
|
- FROM mode_process p
|
|
|
- JOIN (SELECT t.case_id, t.version AS maxv FROM mode_process t
|
|
|
- JOIN (SELECT case_id, MAX(id) AS mid FROM mode_process
|
|
|
- WHERE LEFT(version,5) <> 'link_' GROUP BY case_id) x
|
|
|
- ON t.id = x.mid) m
|
|
|
- ON p.case_id = m.case_id
|
|
|
- ORDER BY p.id""")
|
|
|
- procs = cur.fetchall()
|
|
|
+ # 3-II:成本/耗时按 run(=case+version)取自 process_run;steps 仅最新版(is_latest)需要,
|
|
|
+ # 其余 run steps=[] 省传输。返回形状与旧 mode_process 段一致(id/case_id/version/cost/dur/created_at/steps)。
|
|
|
+ cur.execute("""SELECT id, case_id, version, cost_usd, duration_s, created_at, is_latest
|
|
|
+ FROM process_run ORDER BY id""")
|
|
|
+ run_rows = cur.fetchall()
|
|
|
+ latest_ids = [r["id"] for r in run_rows if r["is_latest"]]
|
|
|
+ steps_by_run = {}
|
|
|
+ if latest_ids:
|
|
|
+ ph = ",".join(["%s"] * len(latest_ids))
|
|
|
+ cur.execute(f"""SELECT p.run_id, s.step_json FROM process_step s
|
|
|
+ JOIN process_procedure p ON s.procedure_pk = p.id
|
|
|
+ WHERE p.run_id IN ({ph})
|
|
|
+ ORDER BY p.run_id, p.seq, s.seq, s.id""", latest_ids)
|
|
|
+ for row in cur.fetchall():
|
|
|
+ steps_by_run.setdefault(row["run_id"], []).append(_loads(row["step_json"], {}))
|
|
|
+ procs = [{"id": r["id"], "case_id": r["case_id"], "version": r["version"],
|
|
|
+ "cost_usd": r["cost_usd"], "duration_s": r["duration_s"],
|
|
|
+ "created_at": r["created_at"],
|
|
|
+ "steps": steps_by_run.get(r["id"], [])} for r in run_rows]
|
|
|
cur.execute("""SELECT id, case_id, version, tool_name, substance_scope,
|
|
|
form_scope, cost_usd, duration_s, created_at
|
|
|
FROM mode_tools""")
|
|
|
@@ -1432,7 +1922,8 @@ def check():
|
|
|
conn = _conn()
|
|
|
try:
|
|
|
with conn.cursor() as cur:
|
|
|
- for t in ("search_process", "search_tools", "mode_process", "mode_tools"):
|
|
|
+ for t in ("search_process", "search_tools", "mode_process", "mode_tools",
|
|
|
+ "process_run", "process_procedure", "process_step", "step_classification"):
|
|
|
cur.execute(f"SELECT COUNT(*) AS n FROM {t}")
|
|
|
print(f"{t}: {cur.fetchone()['n']} 行")
|
|
|
finally:
|