#!/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)