platform-assistant/scripts/direv-publish/extract_notes.py
Sisyphus afac18e843 feat(F002): W26 + W29.x + W30 + W31 平台助手全部改动首次 commit
- W26: NestJS API + Next.js Web + Prisma + SQLite scaffold
- W26: Lead 模型 + Crawler + 询价发布助手 (线索池 + 一键发布)
- W29.x: MCP 集成 (Integration 模型 + connectMcp + publish_rfq 5 工具端到端跑通)
- W30: 3 字段补充 (notes / quotationType / quotationDemand)
- W31 P1: 自动填 enterpriseId / userId (Prisma companyUserId 字段 + get_app_identity tool + lead.service.ts publishArgs)
- W31: detail 详情页 + admin UI 集成管理 (OpenAppListView + AuditLogListView)

关联: ~/Documents/02-平台开发/产品开发/F002-W31-交接文档.md
GIT-AUDIT-REPORT-2026-06-24.md
2026-06-24 11:07:11 +08:00

186 lines
5.6 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
从招标公告 PDF/HTML 中提取关键商业要求,整理成要点摘要。
用法:
python3 extract_notes.py <pdf_or_html_path>
输出: 纯文本要点(每条以 - 开头),可直接填入 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 <pdf_or_html_path>")
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)