Cai
2026-08-29 3608f74e165a7118a602ce4b3d97d805e7912cb1
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import csv
import hashlib
from collections import Counter
from datetime import datetime, timezone, timedelta
from pathlib import Path
 
import fitz
 
 
TZ = timezone(timedelta(hours=8))
NOW = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M:%S+08:00")
CASE_ID = "ANA-YS-INDUSTRY-001"
RUN_ID = "RUN-ANA-YS-PDF-TABLE-RECOVERY-019"
 
 
def read_csv(path):
    with path.open("r", encoding="utf-8-sig", newline="") as f:
        return list(csv.DictReader(f))
 
 
def write_csv(path, rows):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)
 
 
def main():
    queue_path = next(Path("ana-data/cases").glob("*/evidence/key_fact_pdf_table_review_queue_pass004.csv"))
    case_root = queue_path.parents[1]
    industry_name = queue_path.parts[2]
    case_dir = case_root / CASE_ID
    outputs_dir = case_dir / "outputs"
    evidence_dir = case_root / "evidence"
    case_evidence_dir = case_dir / "evidence"
    img_dir = Path("ana-data/img") / industry_name / CASE_ID / RUN_ID
    for path in (outputs_dir, evidence_dir, case_evidence_dir, img_dir):
        path.mkdir(parents=True, exist_ok=True)
 
    queue_rows = read_csv(queue_path)
    recovery_rows = []
    for row in queue_rows:
        rec = dict(row)
        rec["recovery_run_id"] = RUN_ID
        rec["recovery_status"] = ""
        rec["recovery_image_path"] = ""
        rec["recovery_error"] = ""
        rec["manual_review_required"] = "YES"
        rec["formal_upgrade_status"] = "DRAFT_PAGE_IMAGE_READY_NEEDS_MANUAL_TABLE_REVIEW"
        rec["updated_at"] = NOW
 
        if row.get("pdf_header_ok") == "YES" and row.get("page_known") == "YES":
            raw = Path(row["raw_file_path"])
            try:
                page_no = int(row["precheck_page_no"])
                doc = fitz.open(str(raw))
                idx = max(0, min(page_no - 1, len(doc) - 1))
                page = doc.load_page(idx)
                pix = page.get_pixmap(matrix=fitz.Matrix(1.8, 1.8), alpha=False)
                out = img_dir / f"页图_{row['pdf_table_review_id']}_第{page_no}页.png"
                pix.save(str(out))
                rec["recovery_status"] = "PDF_PAGE_IMAGE_RENDERED"
                rec["recovery_image_path"] = out.as_posix()
                rec["rendered_page_index"] = str(idx)
                rec["pdf_page_count"] = str(len(doc))
                rec["image_sha256"] = hashlib.sha256(out.read_bytes()).hexdigest()
                rec["image_bytes"] = str(out.stat().st_size)
            except Exception as exc:
                rec["recovery_status"] = "RENDER_FAILED"
                rec["recovery_error"] = repr(exc)
                rec["formal_upgrade_status"] = "HELD_BY_RENDER_ERROR"
        elif row.get("pdf_header_ok") != "YES":
            rec["recovery_status"] = "HELD_BY_NOT_PDF_HEADER"
            rec["formal_upgrade_status"] = "HELD_BY_SOURCE_FILE_TYPE"
        else:
            rec["recovery_status"] = "HELD_BY_PAGE_UNKNOWN"
            rec["formal_upgrade_status"] = "HELD_BY_PAGE_UNKNOWN"
        recovery_rows.append(rec)
 
    recovery_path = evidence_dir / "原页表格复核恢复表_pass019.csv"
    write_csv(recovery_path, recovery_rows)
 
    manifest_path = case_evidence_dir / "原页表格复核恢复manifest_pass019.csv"
    manifest_rows = [{
        "case_id": CASE_ID,
        "batch_id": "BATCH-003",
        "run_id": RUN_ID,
        "artifact_type": "pdf_page_table_recovery",
        "artifact_path": recovery_path.as_posix(),
        "row_count": str(len(recovery_rows)),
        "rendered_count": str(sum(1 for r in recovery_rows if r["recovery_status"] == "PDF_PAGE_IMAGE_RENDERED")),
        "held_by_page_unknown": str(sum(1 for r in recovery_rows if r["recovery_status"] == "HELD_BY_PAGE_UNKNOWN")),
        "held_by_not_pdf_header": str(sum(1 for r in recovery_rows if r["recovery_status"] == "HELD_BY_NOT_PDF_HEADER")),
        "image_dir": img_dir.as_posix(),
        "review_status": "DRAFT_FOR_REVIEW",
        "created_at": NOW,
    }]
    write_csv(manifest_path, manifest_rows)
 
    cross_path = case_root / "evidence/key_fact_external_crosscheck_pass006.csv"
    cross_rows = read_csv(cross_path)
    image_by_evidence_card = {r.get("evidence_card_id"): r for r in recovery_rows if r.get("evidence_card_id")}
    price_rows = [r for r in cross_rows if r.get("evidence_kind") == "PRICE_OR_MARKET_DATA"]
    status_counter = Counter(r.get("cross_check_status") for r in price_rows)
    metal_counter = Counter(r.get("metal_tags") or "未标注" for r in price_rows)
 
    lines = [
        "# 原研报价格库存线索表",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"生成时间:{NOW}",
        f"关联案例:{CASE_ID}",
        f"关联运行:{RUN_ID}",
        "",
        "## 使用边界",
        "",
        "1. 本文件优先整理已提供 PDF/converted text 中出现的价格、库存、供需、成本和市场变量线索,用于回应“价格库存是否已在研报中提供”。",
        "2. 本文件不是正式价格数据库,也不是正式指标证据;正式输出前仍需核对报告日期、原始表格行、单位、口径和必要外部来源。",
        "3. 已生成页图的记录只表示 PDF 原页可查看,不等于表头、单位、日期和数值一致性已经人工通过。",
        "",
        "## 汇总",
        "",
        f"- 价格/库存/市场数据候选记录:{len(price_rows)} 条。",
        f"- 外部来源已有候选映射:{status_counter.get('PRICE_INVENTORY_SOURCE_MATCHED', 0)} 条。",
        f"- 仍需价格库存外部来源或日期/表格行补核:{status_counter.get('NEEDS_PRICE_INVENTORY_EXTERNAL_SOURCE', 0)} 条。",
        f"- 本轮 PDF 原页页图已恢复:{sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')} 张。",
        "",
        "### 金属分布",
        "",
        "| 金属标签 | 线索条数 |",
        "|---|---:|",
    ]
    for metal, count in metal_counter.most_common():
        lines.append(f"| {metal} | {count} |")
 
    lines.extend([
        "",
        "### 证据状态分布",
        "",
        "| 状态 | 条数 |",
        "|---|---:|",
    ])
    for status, count in status_counter.most_common():
        lines.append(f"| {status or '未标注'} | {count} |")
 
    lines.extend([
        "",
        "## 明细",
        "",
        "| 序号 | doc_id | 金属 | 主题 | 页码/位置 | PDF页图状态 | 价格库存线索 | 数值/单位候选 | 日期候选 | 后续动作 |",
        "|---:|---|---|---|---|---|---|---|---|---|",
    ])
    for index, row in enumerate(price_rows, 1):
        rec = image_by_evidence_card.get(row.get("evidence_card_id"), {})
        img_status = rec.get("recovery_status") or "未进入PASS-004页图队列"
        if rec.get("recovery_image_path"):
            img_status = f"[已生成页图]({rec['recovery_image_path']})"
        indicators = (row.get("matched_price_inventory_indicators") or "").replace("|", "<br>")
        values = (row.get("value_unit_candidates") or "").replace("|", "<br>")
        dates = (row.get("date_candidates") or "").replace("|", "<br>")
        loc = f"第{row.get('precheck_page_no')}页 / {row.get('precheck_location')}"
        action = row.get("next_action") or ""
        lines.append(
            f"| {index} | {row.get('doc_id')} | {row.get('metal_tags')} | {row.get('theme_tags')} | "
            f"{loc} | {img_status} | {indicators} | {values} | {dates} | {action} |"
        )
 
    price_doc_path = outputs_dir / "原研报价格库存线索表.md"
    price_doc_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
 
    summary_path = outputs_dir / "原页表格复核恢复摘要.md"
    summary_lines = [
        "# 原页表格复核恢复摘要",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"生成时间:{NOW}",
        f"关联案例:{CASE_ID}",
        f"关联运行:{RUN_ID}",
        "",
        "## 恢复原因",
        "",
        "用户要求重新尝试安装 PyMuPDF。分析员确认此前失败原因是本机代理环境变量指向 `127.0.0.1:1080` 但端口无服务;临时清除代理变量后,PyMuPDF 安装和 `fitz` 导入成功。该轮属于“环境恢复且可一次性解决阻断”的例外。",
        "",
        "## 执行结果",
        "",
        f"- PASS-004 输入表格候选:{len(recovery_rows)} 条。",
        f"- PDF 头正常且页码已知,已生成页图:{sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')} 条。",
        f"- 页码未知仍保持 HELD:{sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_PAGE_UNKNOWN')} 条。",
        f"- 文件头不是 PDF 仍保持 HELD:{sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_NOT_PDF_HEADER')} 条。",
        f"- 页图目录:`{img_dir.as_posix()}`。",
        "",
        "## 输出入口",
        "",
        f"1. 恢复表:`{recovery_path.as_posix()}`",
        f"2. manifest:`{manifest_path.as_posix()}`",
        f"3. 价格库存线索表:`{price_doc_path.as_posix()}`",
        "",
        "## 仍保留的边界",
        "",
        "1. 页图生成不等于正式表格证据通过;表头、单位、日期、来源和数值一致性仍需人工核验。",
        "2. 页码未知和非 PDF 头文件不在本轮扩大处理范围内。",
        "3. 本轮不输出正式行业结论、正式公司结论、正式指标、交易指令或收益承诺。",
    ]
    summary_path.write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
 
    print(f"recovery_path={recovery_path}")
    print(f"manifest_path={manifest_path}")
    print(f"summary_path={summary_path}")
    print(f"price_doc_path={price_doc_path}")
    print(f"image_dir={img_dir}")
    print(f"rendered={sum(1 for r in recovery_rows if r['recovery_status'] == 'PDF_PAGE_IMAGE_RENDERED')}")
    print(f"held_page_unknown={sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_PAGE_UNKNOWN')}")
    print(f"held_not_pdf={sum(1 for r in recovery_rows if r['recovery_status'] == 'HELD_BY_NOT_PDF_HEADER')}")
    print(f"price_rows={len(price_rows)}")
 
 
if __name__ == "__main__":
    main()