#!/usr/bin/env python3 """ 从招标公告 PDF/HTML 中提取关键商业要求,整理成要点摘要。 用法: python3 extract_notes.py 输出: 纯文本要点(每条以 - 开头),可直接填入 direv 备注字段 """ import json import os import re import sys import fitz # PyMuPDF import requests from bs4 import BeautifulSoup # ============ 配置 ============ AUTH_FILE = os.path.expanduser("~/.openclaw/agents/main/agent/auth-profiles.json") LLM_ENDPOINT = "https://api.deepseek.com/v1/chat/completions" LLM_MODEL = "deepseek-chat" LLM_TIMEOUT = 60 MAX_CHARS = 4000 # 输入文本最大字符数(太长则截断) def load_llm_key(): """从 OpenClaw auth-profiles 读取 DeepSeek API Key""" try: with open(AUTH_FILE) as f: data = json.load(f) container = data.get("profiles", data) if not isinstance(container, dict): return None for key, val in container.items(): if isinstance(val, dict) and val.get("provider") == "deepseek": return val.get("key") except Exception: pass return None # ============ 文本提取 ============ def extract_from_pdf(filepath: str) -> str: """从 PDF 提取纯文本""" doc = fitz.open(filepath) pages = [] for page in doc: text = page.get_text() if text.strip(): pages.append(text.strip()) doc.close() return "\n---\n".join(pages) def extract_from_html(filepath: str) -> str: """从 HTML 提取可见文本(过滤 script/style)""" with open(filepath, "r", encoding="utf-8") as f: html = f.read() soup = BeautifulSoup(html, "lxml" if "lxml" in sys.modules else "html.parser") for tag in soup(["script", "style", "meta", "link", "noscript"]): tag.decompose() text = soup.get_text(separator="\n") lines = [line.strip() for line in text.split("\n") if line.strip()] return "\n".join(lines) # ============ LLM 摘要 ============ SYSTEM_PROMPT = """你是一个招标公告信息提取助手。请从以下公告内容中提取关键商业要求,整理成简明要点。 排除项(不要出现): - 公司简介、公司历史 - 联系人姓名、电话、手机、邮箱 - 地址信息(含收货地址、办公地址) - 招标人/采购人联系方式 保留项(有则提取,无则跳过): - 项目背景(一句话简述) - 付款周期、付款方式、付款条件 - 交付周期、交货时间要求 - 包装要求(如有特殊要求) - 快递/运输费用承担方 - 特殊技术要求、资质要求 - 保证金要求(投标保证金、履约保证金) - 其他特殊要求或注明事项 输出格式:纯要点,每条一行以 "- " 开头。不要标题、分类头、解释文字。 每条要点控制在20字以内,最多输出6条。如果某类信息原文没提到,就跳过不写。""" def summarize_with_llm(raw_text: str) -> str: """用 DeepSeek LLM 将公告内容提取为要点摘要""" api_key = load_llm_key() if not api_key: raise RuntimeError("DeepSeek API Key 未配置") if len(raw_text) > MAX_CHARS: raw_text = raw_text[:MAX_CHARS] + "\n[内容过长已截断]" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } payload = { "model": LLM_MODEL, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": raw_text}, ], "temperature": 0.1, "max_tokens": 500, } r = requests.post(LLM_ENDPOINT, headers=headers, json=payload, timeout=LLM_TIMEOUT) if r.status_code != 200: raise RuntimeError(f"DeepSeek API 返回 {r.status_code}: {r.text[:200]}") data = r.json() content = "" choices = data.get("choices", []) if choices: content = choices[0].get("message", {}).get("content", "") if not content: raise RuntimeError(f"DeepSeek 返回空内容: {json.dumps(data, ensure_ascii=False)[:300]}") # 清理输出 lines = [line.strip() for line in content.split("\n") if line.strip()] return "\n".join(lines[:6]) # 最多6条 # ============ 主入口 ============ def get_notes(source_type: str, source_path: str) -> str: """ 从公告文件提取备注内容 Args: source_type: "pdf" 或 "html" source_path: 文件路径 Returns: 要点字符串,如 "- 项目背景...\n- 付款周期..." """ if not os.path.exists(source_path): raise FileNotFoundError(f"文件不存在: {source_path}") if source_type == "pdf": raw_text = extract_from_pdf(source_path) elif source_type == "html": raw_text = extract_from_html(source_path) else: raise ValueError(f"不支持的类型: {source_type}") if not raw_text or len(raw_text) < 20: raise ValueError(f"文件内容过短或无法提取 ({len(raw_text)} 字符)") print(f" 原始文本长度: {len(raw_text)} 字符") notes = summarize_with_llm(raw_text) print(f" LLM 摘要: {notes}") return notes # ============ CLI ============ if __name__ == "__main__": if len(sys.argv) < 2: print("用法: python3 extract_notes.py ") print("自动判断文件类型") sys.exit(1) path = sys.argv[1] ext = os.path.splitext(path)[1].lower() source_type = "pdf" if ext == ".pdf" else "html" try: result = get_notes(source_type, path) print("\n--- 备注内容 ---") print(result) except Exception as e: print(f"❌ 错误: {e}") sys.exit(1)