aboutsummaryrefslogtreecommitdiffstats
path: root/.agents/skills/citation-verification/scripts
diff options
context:
space:
mode:
Diffstat (limited to '.agents/skills/citation-verification/scripts')
-rw-r--r--.agents/skills/citation-verification/scripts/README.md81
-rw-r--r--.agents/skills/citation-verification/scripts/api-clients.py542
-rw-r--r--.agents/skills/citation-verification/scripts/format-checker.py605
-rw-r--r--.agents/skills/citation-verification/scripts/verify-citations.py678
4 files changed, 1906 insertions, 0 deletions
diff --git a/.agents/skills/citation-verification/scripts/README.md b/.agents/skills/citation-verification/scripts/README.md
new file mode 100644
index 0000000..5f1bd38
--- /dev/null
+++ b/.agents/skills/citation-verification/scripts/README.md
@@ -0,0 +1,81 @@
+# Citation Verification Scripts
+
+## 状态说明
+
+**这些脚本是参考实现,不是主要工作流的一部分。**
+
+本目录中的Python脚本提供了基于API的引用验证实现,但**实际的引用验证工作流使用WebSearch和Google Scholar**,而不是这些脚本。
+
+## 为什么保留这些脚本?
+
+这些脚本作为**参考实现**保留,用于:
+
+1. **理解验证逻辑** - 展示引用验证的完整逻辑和步骤
+2. **学习API使用** - 了解如何使用CrossRef、arXiv、Semantic Scholar等API
+3. **高级用例** - 对于需要批量验证或自动化的场景,可以参考这些实现
+
+## 主要工作流
+
+**实际的引用验证应该使用 `ml-paper-writing` skill 中的 Citation Workflow:**
+
+1. 使用 WebSearch 查找论文
+2. 在 Google Scholar 上验证
+3. 从 Google Scholar 获取 BibTeX
+4. 验证声明(如需要)
+5. 添加到 bibliography
+
+详见 `ml-paper-writing` skill 的 "Citation Workflow (Hallucination Prevention)" 部分。
+
+## 脚本说明
+
+### verify-citations.py
+
+完整的引用验证脚本,包含:
+- 四层验证机制(格式、存在性、信息匹配、内容验证)
+- 多API支持(CrossRef、arXiv、Semantic Scholar)
+- 报告生成
+
+**用途**: 参考实现,了解完整的验证逻辑
+
+### api-clients.py
+
+API客户端库,包含:
+- CrossRefClient - DOI验证
+- ArXivClient - arXiv论文验证
+- SemanticScholarClient - 通用学术搜索
+- CitationAPIManager - 统一API管理
+
+**用途**: 参考实现,了解如何使用学术API
+
+### format-checker.py
+
+BibTeX和LaTeX格式检查工具,包含:
+- BibTeX格式验证
+- LaTeX引用检查
+- 格式错误报告
+
+**用途**: 参考实现,了解格式检查逻辑
+
+## 使用建议
+
+**对于日常论文写作**:
+- ✅ 使用 `ml-paper-writing` skill 的 Citation Workflow
+- ✅ 使用 WebSearch 和 Google Scholar
+- ❌ 不要使用这些Python脚本
+
+**对于批量验证或自动化**:
+- 可以参考这些脚本的实现
+- 根据需要修改和使用
+- 注意API速率限制
+
+## 依赖安装
+
+如果需要运行这些脚本(仅用于参考或高级用例):
+
+```bash
+pip install bibtexparser requests semanticscholar arxiv
+```
+
+## 更多信息
+
+详见 `citation-verification` skill 的 SKILL.md 文件。
diff --git a/.agents/skills/citation-verification/scripts/api-clients.py b/.agents/skills/citation-verification/scripts/api-clients.py
new file mode 100644
index 0000000..6528ad6
--- /dev/null
+++ b/.agents/skills/citation-verification/scripts/api-clients.py
@@ -0,0 +1,542 @@
+#!/usr/bin/env python3
+"""
+API Clients for Citation Verification
+
+提供三个主要 API 客户端:
+1. CrossRefClient - DOI 验证
+2. ArXivClient - arXiv 论文验证
+3. SemanticScholarClient - 通用学术搜索
+
+每个客户端都包含:
+- 错误处理
+- 重试机制
+- 速率限制
+- 结果标准化
+"""
+
+import time
+import requests
+from typing import Dict, List, Optional
+from abc import ABC, abstractmethod
+
+
+class RateLimiter:
+ """速率限制器"""
+
+ def __init__(self, calls_per_minute: int):
+ self.calls_per_minute = calls_per_minute
+ self.last_call = 0
+ self.min_interval = 60.0 / calls_per_minute
+
+ def wait_if_needed(self):
+ """如果需要,等待以满足速率限制"""
+ elapsed = time.time() - self.last_call
+ if elapsed < self.min_interval:
+ time.sleep(self.min_interval - elapsed)
+ self.last_call = time.time()
+
+
+class APIClient(ABC):
+ """API 客户端基类"""
+
+ def __init__(self, rate_limit: int = 20):
+ """
+ Args:
+ rate_limit: 每分钟最大请求数
+ """
+ self.rate_limiter = RateLimiter(rate_limit)
+
+ @abstractmethod
+ def search(self, **kwargs) -> Optional[Dict]:
+ """搜索论文"""
+ pass
+
+ def _retry_request(self, func, max_retries: int = 3):
+ """带重试的请求"""
+ for i in range(max_retries):
+ try:
+ self.rate_limiter.wait_if_needed()
+ return func()
+ except requests.exceptions.RequestException as e:
+ if i == max_retries - 1:
+ raise
+ time.sleep(2 ** i) # 指数退避
+ return None
+
+
+class CrossRefClient(APIClient):
+ """CrossRef API 客户端
+
+ 用于通过 DOI 验证论文信息
+
+ API 文档: https://api.crossref.org/
+ """
+
+ def __init__(self, rate_limit: int = 50):
+ """
+ Args:
+ rate_limit: 每分钟最大请求数 (CrossRef 限制较宽松)
+ """
+ super().__init__(rate_limit)
+ self.base_url = "https://api.crossref.org"
+
+ def search_by_doi(self, doi: str) -> Optional[Dict]:
+ """通过 DOI 搜索论文
+
+ Args:
+ doi: DOI 标识符 (如 10.1038/nature12345)
+
+ Returns:
+ 标准化的论文信息字典,如果未找到则返回 None
+ """
+ def request():
+ url = f"{self.base_url}/works/{doi}"
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ return response.json()
+
+ try:
+ data = self._retry_request(request)
+ if data and 'message' in data:
+ return self._normalize_result(data['message'])
+ return None
+ except Exception as e:
+ print(f"CrossRef API 错误: {e}")
+ return None
+
+ def search(self, doi: str = None, **kwargs) -> Optional[Dict]:
+ """搜索论文 (统一接口)"""
+ if doi:
+ return self.search_by_doi(doi)
+ return None
+
+ def _normalize_result(self, data: Dict) -> Dict:
+ """标准化 CrossRef 返回结果"""
+ # 提取标题
+ title = data.get('title', [''])[0] if 'title' in data else ''
+
+ # 提取作者
+ authors = []
+ if 'author' in data:
+ for author in data['author']:
+ given = author.get('given', '')
+ family = author.get('family', '')
+ if given and family:
+ authors.append(f"{given} {family}")
+ elif family:
+ authors.append(family)
+
+ # 提取年份
+ year = None
+ if 'published' in data:
+ date_parts = data['published'].get('date-parts', [[]])[0]
+ if date_parts:
+ year = date_parts[0]
+ elif 'created' in data:
+ date_parts = data['created'].get('date-parts', [[]])[0]
+ if date_parts:
+ year = date_parts[0]
+
+ # 提取期刊/会议名称
+ venue = ''
+ if 'container-title' in data:
+ venue = data['container-title'][0] if data['container-title'] else ''
+
+ return {
+ 'title': title,
+ 'authors': authors,
+ 'year': year,
+ 'venue': venue,
+ 'doi': data.get('DOI', ''),
+ 'type': data.get('type', ''),
+ 'source': 'crossref'
+ }
+
+ def get_bibtex(self, doi: str) -> Optional[str]:
+ """通过 DOI 获取 BibTeX
+
+ Args:
+ doi: DOI 标识符
+
+ Returns:
+ BibTeX 字符串,如果失败则返回 None
+ """
+ def request():
+ url = f"https://doi.org/{doi}"
+ headers = {"Accept": "application/x-bibtex"}
+ response = requests.get(url, headers=headers, timeout=10)
+ response.raise_for_status()
+ return response.text
+
+ try:
+ return self._retry_request(request)
+ except Exception as e:
+ print(f"获取 BibTeX 失败: {e}")
+ return None
+
+
+class ArXivClient(APIClient):
+ """arXiv API 客户端
+
+ 用于验证 arXiv 预印本论文
+
+ API 文档: https://info.arxiv.org/help/api/
+ """
+
+ def __init__(self, rate_limit: int = 20):
+ """
+ Args:
+ rate_limit: 每分钟最大请求数
+ """
+ super().__init__(rate_limit)
+ try:
+ import arxiv
+ self.arxiv = arxiv
+ except ImportError:
+ raise ImportError("需要安装 arxiv 库: pip install arxiv")
+
+ def search_by_id(self, arxiv_id: str) -> Optional[Dict]:
+ """通过 arXiv ID 搜索论文
+
+ Args:
+ arxiv_id: arXiv 标识符 (如 2301.12345 或 cs/0703001)
+
+ Returns:
+ 标准化的论文信息字典,如果未找到则返回 None
+ """
+ def request():
+ search = self.arxiv.Search(id_list=[arxiv_id])
+ paper = next(search.results())
+ return paper
+
+ try:
+ self.rate_limiter.wait_if_needed()
+ paper = request()
+ return self._normalize_result(paper)
+ except StopIteration:
+ print(f"arXiv 论文未找到: {arxiv_id}")
+ return None
+ except Exception as e:
+ print(f"arXiv API 错误: {e}")
+ return None
+
+ def search_by_title(self, title: str, max_results: int = 5) -> Optional[Dict]:
+ """通过标题搜索论文
+
+ Args:
+ title: 论文标题
+ max_results: 最大返回结果数
+
+ Returns:
+ 标准化的论文信息字典(第一个结果),如果未找到则返回 None
+ """
+ def request():
+ search = self.arxiv.Search(
+ query=f'ti:"{title}"',
+ max_results=max_results,
+ sort_by=self.arxiv.SortCriterion.Relevance
+ )
+ results = list(search.results())
+ return results[0] if results else None
+
+ try:
+ self.rate_limiter.wait_if_needed()
+ paper = request()
+ if paper:
+ return self._normalize_result(paper)
+ return None
+ except Exception as e:
+ print(f"arXiv API 错误: {e}")
+ return None
+
+ def search(self, arxiv_id: str = None, title: str = None, **kwargs) -> Optional[Dict]:
+ """搜索论文 (统一接口)"""
+ if arxiv_id:
+ return self.search_by_id(arxiv_id)
+ elif title:
+ return self.search_by_title(title)
+ return None
+
+ def _normalize_result(self, paper) -> Dict:
+ """标准化 arXiv 返回结果"""
+ # 提取 arXiv ID
+ arxiv_id = paper.entry_id.split('/')[-1]
+
+ return {
+ 'title': paper.title,
+ 'authors': [a.name for a in paper.authors],
+ 'year': paper.published.year,
+ 'venue': 'arXiv',
+ 'arxiv_id': arxiv_id,
+ 'doi': paper.doi if hasattr(paper, 'doi') else None,
+ 'abstract': paper.summary,
+ 'pdf_url': paper.pdf_url,
+ 'source': 'arxiv'
+ }
+
+ @staticmethod
+ def extract_arxiv_id(text: str) -> Optional[str]:
+ """从文本中提取 arXiv ID
+
+ Args:
+ text: 包含 arXiv ID 的文本
+
+ Returns:
+ arXiv ID,如果未找到则返回 None
+ """
+ import re
+
+ # 匹配新格式: YYMM.NNNNN
+ match = re.search(r'\d{4}\.\d{4,5}', text)
+ if match:
+ return match.group()
+
+ # 匹配旧格式: arch-ive/YYMMNNN
+ match = re.search(r'[a-z-]+/\d{7}', text)
+ if match:
+ return match.group()
+
+ return None
+
+
+class SemanticScholarClient(APIClient):
+ """Semantic Scholar API 客户端
+
+ 用于通用学术论文搜索和验证
+
+ API 文档: https://api.semanticscholar.org/api-docs/
+ """
+
+ def __init__(self, rate_limit: int = 20):
+ """
+ Args:
+ rate_limit: 每分钟最大请求数 (Semantic Scholar 限制: 100 requests/5min)
+ """
+ super().__init__(rate_limit)
+ try:
+ from semanticscholar import SemanticScholar
+ self.sch = SemanticScholar()
+ except ImportError:
+ raise ImportError("需要安装 semanticscholar 库: pip install semanticscholar")
+
+ def search_by_title(self, title: str, max_results: int = 5) -> Optional[Dict]:
+ """通过标题搜索论文
+
+ Args:
+ title: 论文标题
+ max_results: 最大返回结果数
+
+ Returns:
+ 标准化的论文信息字典(第一个结果),如果未找到则返回 None
+ """
+ try:
+ self.rate_limiter.wait_if_needed()
+ results = self.sch.search_paper(title, limit=max_results)
+
+ if not results:
+ return None
+
+ # 返回第一个结果
+ paper = results[0]
+ return self._normalize_result(paper)
+ except Exception as e:
+ print(f"Semantic Scholar API 错误: {e}")
+ return None
+
+ def search_by_doi(self, doi: str) -> Optional[Dict]:
+ """通过 DOI 搜索论文
+
+ Args:
+ doi: DOI 标识符
+
+ Returns:
+ 标准化的论文信息字典,如果未找到则返回 None
+ """
+ try:
+ self.rate_limiter.wait_if_needed()
+ paper = self.sch.get_paper(f"DOI:{doi}")
+ if paper:
+ return self._normalize_result(paper)
+ return None
+ except Exception as e:
+ print(f"Semantic Scholar API 错误: {e}")
+ return None
+
+ def search(self, title: str = None, doi: str = None, **kwargs) -> Optional[Dict]:
+ """搜索论文 (统一接口)"""
+ if doi:
+ return self.search_by_doi(doi)
+ elif title:
+ return self.search_by_title(title)
+ return None
+
+ def _normalize_result(self, paper) -> Dict:
+ """标准化 Semantic Scholar 返回结果"""
+ # 提取作者
+ authors = []
+ if paper.authors:
+ authors = [a.name for a in paper.authors]
+
+ # 提取外部 ID
+ external_ids = paper.externalIds if hasattr(paper, 'externalIds') else {}
+ doi = external_ids.get('DOI') if external_ids else None
+ arxiv_id = external_ids.get('ArXiv') if external_ids else None
+
+ return {
+ 'title': paper.title,
+ 'authors': authors,
+ 'year': paper.year,
+ 'venue': paper.venue if hasattr(paper, 'venue') else '',
+ 'paperId': paper.paperId,
+ 'doi': doi,
+ 'arxiv_id': arxiv_id,
+ 'citationCount': paper.citationCount if hasattr(paper, 'citationCount') else 0,
+ 'abstract': paper.abstract if hasattr(paper, 'abstract') else '',
+ 'source': 'semantic_scholar'
+ }
+
+
+class CitationAPIManager:
+ """统一的 API 管理器
+
+ 协调三个 API 客户端,实现智能的 API 选择策略
+ """
+
+ def __init__(self):
+ """初始化所有 API 客户端"""
+ self.crossref = None
+ self.arxiv = None
+ self.semantic_scholar = None
+
+ # 尝试初始化各个客户端
+ try:
+ self.crossref = CrossRefClient()
+ except Exception as e:
+ print(f"警告: CrossRef 客户端初始化失败: {e}")
+
+ try:
+ self.arxiv = ArXivClient()
+ except Exception as e:
+ print(f"警告: arXiv 客户端初始化失败: {e}")
+
+ try:
+ self.semantic_scholar = SemanticScholarClient()
+ except Exception as e:
+ print(f"警告: Semantic Scholar 客户端初始化失败: {e}")
+
+ def verify_citation(self, citation_info: Dict) -> tuple[bool, Optional[str], Optional[Dict]]:
+ """验证引用
+
+ 实现 API 选择策略:
+ 1. DOI 优先 → CrossRef
+ 2. arXiv ID → arXiv
+ 3. 标题搜索 → Semantic Scholar
+
+ Args:
+ citation_info: 引用信息字典,可能包含 doi, arxiv_id, title, authors 等字段
+
+ Returns:
+ (exists, api_source, api_data)
+ - exists: 论文是否存在
+ - api_source: 验证来源 ('crossref', 'arxiv', 'semantic_scholar')
+ - api_data: API 返回的标准化数据
+ """
+ # 策略 1: DOI 优先
+ if 'doi' in citation_info and self.crossref:
+ data = self.crossref.search_by_doi(citation_info['doi'])
+ if data:
+ return True, 'crossref', data
+
+ # 策略 2: arXiv ID
+ arxiv_id = citation_info.get('arxiv_id')
+ if not arxiv_id and 'note' in citation_info:
+ # 尝试从 note 字段提取 arXiv ID
+ arxiv_id = ArXivClient.extract_arxiv_id(citation_info['note'])
+
+ if arxiv_id and self.arxiv:
+ data = self.arxiv.search_by_id(arxiv_id)
+ if data:
+ return True, 'arxiv', data
+
+ # 策略 3: 通用搜索 (Semantic Scholar)
+ if 'title' in citation_info and self.semantic_scholar:
+ data = self.semantic_scholar.search_by_title(citation_info['title'])
+ if data:
+ return True, 'semantic_scholar', data
+
+ return False, None, None
+
+ def get_bibtex(self, doi: str) -> Optional[str]:
+ """通过 DOI 获取 BibTeX
+
+ Args:
+ doi: DOI 标识符
+
+ Returns:
+ BibTeX 字符串,如果失败则返回 None
+ """
+ if self.crossref:
+ return self.crossref.get_bibtex(doi)
+ return None
+
+
+# ============================================================================
+# 使用示例
+# ============================================================================
+
+if __name__ == '__main__':
+ # 示例 1: 使用 CrossRef 客户端
+ print("示例 1: CrossRef 客户端")
+ print("-" * 60)
+ crossref = CrossRefClient()
+ result = crossref.search_by_doi("10.48550/arXiv.1706.03762")
+ if result:
+ print(f"标题: {result['title']}")
+ print(f"作者: {', '.join(result['authors'][:3])}")
+ print(f"年份: {result['year']}")
+ print()
+
+ # 示例 2: 使用 arXiv 客户端
+ print("示例 2: arXiv 客户端")
+ print("-" * 60)
+ try:
+ arxiv_client = ArXivClient()
+ result = arxiv_client.search_by_id("1706.03762")
+ if result:
+ print(f"标题: {result['title']}")
+ print(f"作者: {', '.join(result['authors'][:3])}")
+ print(f"年份: {result['year']}")
+ except ImportError as e:
+ print(f"跳过: {e}")
+ print()
+
+ # 示例 3: 使用 Semantic Scholar 客户端
+ print("示例 3: Semantic Scholar 客户端")
+ print("-" * 60)
+ try:
+ ss_client = SemanticScholarClient()
+ result = ss_client.search_by_title("Attention is All You Need")
+ if result:
+ print(f"标题: {result['title']}")
+ print(f"作者: {', '.join(result['authors'][:3])}")
+ print(f"年份: {result['year']}")
+ print(f"引用数: {result['citationCount']}")
+ except ImportError as e:
+ print(f"跳过: {e}")
+ print()
+
+ # 示例 4: 使用统一管理器
+ print("示例 4: 统一 API 管理器")
+ print("-" * 60)
+ manager = CitationAPIManager()
+ citation_info = {
+ 'title': 'Attention is All You Need',
+ 'authors': ['Vaswani', 'Shazeer'],
+ 'year': '2017'
+ }
+ exists, source, data = manager.verify_citation(citation_info)
+ if exists:
+ print(f"验证成功!")
+ print(f"来源: {source}")
+ print(f"标题: {data['title']}")
+ print(f"年份: {data['year']}")
diff --git a/.agents/skills/citation-verification/scripts/format-checker.py b/.agents/skills/citation-verification/scripts/format-checker.py
new file mode 100644
index 0000000..c222702
--- /dev/null
+++ b/.agents/skills/citation-verification/scripts/format-checker.py
@@ -0,0 +1,605 @@
+#!/usr/bin/env python3
+"""
+BibTeX and LaTeX Format Checker
+
+独立的格式检查工具,用于验证 BibTeX 和 LaTeX 引用格式。
+
+功能:
+1. BibTeX 格式检查 - 验证条目结构、必填字段、字段格式
+2. LaTeX 引用检查 - 提取引用、检查一致性
+3. 快速格式验证 - 无需 API 调用的快速检查
+
+使用方法:
+ python format-checker.py references.bib
+ python format-checker.py paper.tex --check-latex
+ python format-checker.py references.bib --strict
+"""
+
+import argparse
+import sys
+import re
+from pathlib import Path
+from typing import Dict, List, Tuple, Optional
+from dataclasses import dataclass
+from enum import Enum
+
+# 尝试导入 bibtexparser
+try:
+ import bibtexparser
+ from bibtexparser.bparser import BibTexParser
+ BIBTEX_AVAILABLE = True
+except ImportError:
+ print("警告: bibtexparser 未安装,BibTeX 解析功能受限")
+ print("运行: pip install bibtexparser")
+ BIBTEX_AVAILABLE = False
+
+
+class ErrorLevel(Enum):
+ """错误级别"""
+ ERROR = "error" # 严重错误,必须修复
+ WARNING = "warning" # 警告,建议修复
+ INFO = "info" # 信息,可选修复
+
+
+@dataclass
+class FormatError:
+ """格式错误数据类"""
+ level: ErrorLevel
+ location: str # 文件位置 (如 "entry:smith2020" 或 "line:42")
+ field: Optional[str] # 字段名 (如 "author", "year")
+ message: str # 错误描述
+ suggestion: Optional[str] = None # 修复建议
+
+
+def parse_arguments():
+ """解析命令行参数"""
+ parser = argparse.ArgumentParser(
+ description='检查 BibTeX 和 LaTeX 引用格式',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+示例:
+ %(prog)s references.bib
+ %(prog)s paper.tex --check-latex
+ %(prog)s references.bib --strict --output report.txt
+ %(prog)s references.bib --fix-common
+ """
+ )
+
+ parser.add_argument(
+ 'input_file',
+ type=str,
+ help='BibTeX 文件(.bib)或 LaTeX 文件(.tex)'
+ )
+
+ parser.add_argument(
+ '--check-latex',
+ action='store_true',
+ help='检查 LaTeX 引用(需要提供 .tex 文件)'
+ )
+
+ parser.add_argument(
+ '--strict',
+ action='store_true',
+ help='严格模式 - 将警告视为错误'
+ )
+
+ parser.add_argument(
+ '--output',
+ type=str,
+ help='输出报告文件路径'
+ )
+
+ parser.add_argument(
+ '--fix-common',
+ action='store_true',
+ help='自动修复常见格式问题'
+ )
+
+ parser.add_argument(
+ '--verbose',
+ action='store_true',
+ help='显示详细信息'
+ )
+
+ parser.add_argument(
+ '--entry-type',
+ type=str,
+ help='只检查特定类型的条目(如 article, inproceedings)'
+ )
+
+ return parser.parse_args()
+
+
+def load_bibtex_file(file_path: str) -> List[Dict]:
+ """加载 BibTeX 文件
+
+ Args:
+ file_path: BibTeX 文件路径
+
+ Returns:
+ BibTeX 条目列表
+
+ Raises:
+ FileNotFoundError: 文件不存在
+ ValueError: 文件格式错误
+ """
+ if not BIBTEX_AVAILABLE:
+ raise ImportError("需要安装 bibtexparser: pip install bibtexparser")
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ parser = BibTexParser(common_strings=True)
+ bib_database = bibtexparser.load(f, parser)
+ return bib_database.entries
+ except FileNotFoundError:
+ raise FileNotFoundError(f"文件不存在: {file_path}")
+ except Exception as e:
+ raise ValueError(f"无法解析 BibTeX 文件: {e}")
+
+
+def load_latex_file(file_path: str) -> str:
+ """加载 LaTeX 文件
+
+ Args:
+ file_path: LaTeX 文件路径
+
+ Returns:
+ 文件内容
+
+ Raises:
+ FileNotFoundError: 文件不存在
+ """
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ return f.read()
+ except FileNotFoundError:
+ raise FileNotFoundError(f"文件不存在: {file_path}")
+ except Exception as e:
+ raise ValueError(f"无法读取 LaTeX 文件: {e}")
+
+
+# ============================================================================
+# BibTeX 格式检查函数
+# ============================================================================
+
+def get_required_fields(entry_type: str) -> List[str]:
+ """获取 BibTeX 条目类型的必填字段
+
+ Args:
+ entry_type: 条目类型 (如 'article', 'inproceedings')
+
+ Returns:
+ 必填字段列表
+ """
+ required_fields = {
+ 'article': ['author', 'title', 'journal', 'year'],
+ 'inproceedings': ['author', 'title', 'booktitle', 'year'],
+ 'book': ['title', 'publisher', 'year'],
+ 'incollection': ['author', 'title', 'booktitle', 'publisher', 'year'],
+ 'inbook': ['author', 'title', 'chapter', 'publisher', 'year'],
+ 'proceedings': ['title', 'year'],
+ 'phdthesis': ['author', 'title', 'school', 'year'],
+ 'mastersthesis': ['author', 'title', 'school', 'year'],
+ 'techreport': ['author', 'title', 'institution', 'year'],
+ 'manual': ['title'],
+ 'misc': ['title'],
+ 'unpublished': ['author', 'title', 'note'],
+ }
+ return required_fields.get(entry_type.lower(), ['title'])
+
+
+def get_optional_fields(entry_type: str) -> List[str]:
+ """获取 BibTeX 条目类型的可选字段
+
+ Args:
+ entry_type: 条目类型
+
+ Returns:
+ 可选字段列表
+ """
+ optional_fields = {
+ 'article': ['volume', 'number', 'pages', 'month', 'doi', 'url'],
+ 'inproceedings': ['editor', 'volume', 'series', 'pages', 'address',
+ 'month', 'organization', 'publisher', 'doi', 'url'],
+ 'book': ['author', 'editor', 'volume', 'series', 'address',
+ 'edition', 'month', 'isbn', 'doi', 'url'],
+ }
+ return optional_fields.get(entry_type.lower(), [])
+
+
+def check_entry_structure(entry: Dict) -> List[FormatError]:
+ """检查 BibTeX 条目基本结构
+
+ Args:
+ entry: BibTeX 条目字典
+
+ Returns:
+ 错误列表
+ """
+ errors = []
+
+ # 检查条目类型
+ if 'ENTRYTYPE' not in entry:
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry.get('ID', 'unknown')}",
+ field='ENTRYTYPE',
+ message="缺少条目类型",
+ suggestion="添加条目类型,如 @article, @inproceedings"
+ ))
+ return errors
+
+ # 检查 ID
+ if 'ID' not in entry or not entry['ID'].strip():
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location="entry:unknown",
+ field='ID',
+ message="缺少 citation key",
+ suggestion="添加唯一的 citation key"
+ ))
+
+ # 检查必填字段
+ entry_type = entry.get('ENTRYTYPE', '')
+ required = get_required_fields(entry_type)
+ for field in required:
+ if field not in entry or not entry[field].strip():
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry.get('ID', 'unknown')}",
+ field=field,
+ message=f"缺少必填字段: {field}",
+ suggestion=f"添加 {field} 字段"
+ ))
+
+ return errors
+
+
+def check_field_formats(entry: Dict) -> List[FormatError]:
+ """检查字段格式
+
+ Args:
+ entry: BibTeX 条目字典
+
+ Returns:
+ 错误列表
+ """
+ errors = []
+ entry_id = entry.get('ID', 'unknown')
+
+ # 年份格式检查
+ if 'year' in entry:
+ year = entry['year'].strip()
+ if not year.isdigit():
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry_id}",
+ field='year',
+ message=f"年份格式错误: {year} (应为4位数字)",
+ suggestion="使用4位数字年份,如 2023"
+ ))
+ elif len(year) != 4:
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry_id}",
+ field='year',
+ message=f"年份格式错误: {year} (应为4位数字)",
+ suggestion="使用4位数字年份,如 2023"
+ ))
+ else:
+ year_int = int(year)
+ if year_int < 1900 or year_int > 2030:
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location=f"entry:{entry_id}",
+ field='year',
+ message=f"年份超出合理范围: {year}",
+ suggestion="检查年份是否正确"
+ ))
+
+ # DOI 格式检查
+ if 'doi' in entry:
+ doi = entry['doi'].strip()
+ if not doi.startswith('10.'):
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry_id}",
+ field='doi',
+ message=f"DOI 格式错误: {doi}",
+ suggestion="DOI 应以 '10.' 开头,如 10.1038/nature12345"
+ ))
+ # 检查是否包含 URL 前缀
+ if 'doi.org' in doi or 'dx.doi.org' in doi:
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location=f"entry:{entry_id}",
+ field='doi',
+ message=f"DOI 包含 URL 前缀: {doi}",
+ suggestion="只保留 DOI 本身,移除 https://doi.org/ 前缀"
+ ))
+
+ # 作者名格式检查
+ if 'author' in entry:
+ author = entry['author'].strip()
+ # 检查是否为空
+ if not author:
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{entry_id}",
+ field='author',
+ message="作者字段为空",
+ suggestion="添加作者信息"
+ ))
+ # 检查格式一致性
+ elif ' and ' in author:
+ authors = author.split(' and ')
+ formats = []
+ for a in authors:
+ if ',' in a:
+ formats.append('last_first') # "Last, First"
+ else:
+ formats.append('first_last') # "First Last"
+
+ if len(set(formats)) > 1:
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location=f"entry:{entry_id}",
+ field='author',
+ message="作者名格式不一致",
+ suggestion="统一使用 'Last, First' 或 'First Last' 格式"
+ ))
+
+ # 页码格式检查
+ if 'pages' in entry:
+ pages = entry['pages'].strip()
+ # 检查是否使用了正确的分隔符
+ if '-' in pages and '--' not in pages:
+ errors.append(FormatError(
+ level=ErrorLevel.INFO,
+ location=f"entry:{entry_id}",
+ field='pages',
+ message=f"页码使用单连字符: {pages}",
+ suggestion="建议使用双连字符 '--',如 123--145"
+ ))
+
+ # URL 格式检查
+ if 'url' in entry:
+ url = entry['url'].strip()
+ if not url.startswith(('http://', 'https://')):
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location=f"entry:{entry_id}",
+ field='url',
+ message=f"URL 缺少协议前缀: {url}",
+ suggestion="添加 http:// 或 https:// 前缀"
+ ))
+
+ return errors
+
+
+def check_consistency(entries: List[Dict]) -> List[FormatError]:
+ """检查条目间的一致性
+
+ Args:
+ entries: BibTeX 条目列表
+
+ Returns:
+ 错误列表
+ """
+ errors = []
+
+ # 检查重复的 citation key
+ ids = [e.get('ID', '') for e in entries]
+ duplicates = [id for id in ids if ids.count(id) > 1]
+ if duplicates:
+ for dup_id in set(duplicates):
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"entry:{dup_id}",
+ field='ID',
+ message=f"重复的 citation key: {dup_id}",
+ suggestion="使用唯一的 citation key"
+ ))
+
+ # 检查作者名格式一致性
+ author_formats = {}
+ for entry in entries:
+ if 'author' in entry and ' and ' in entry['author']:
+ entry_id = entry.get('ID', 'unknown')
+ authors = entry['author'].split(' and ')
+ for author in authors:
+ if ',' in author:
+ author_formats[entry_id] = 'last_first'
+ else:
+ author_formats[entry_id] = 'first_last'
+ break
+
+ if len(set(author_formats.values())) > 1:
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location="global",
+ field='author',
+ message="不同条目使用了不同的作者名格式",
+ suggestion="统一使用 'Last, First' 或 'First Last' 格式"
+ ))
+
+ return errors
+
+
+# ============================================================================
+# LaTeX 引用检查函数
+# ============================================================================
+
+def extract_latex_citations(tex_content: str) -> List[str]:
+ """从 LaTeX 文件中提取引用
+
+ Args:
+ tex_content: LaTeX 文件内容
+
+ Returns:
+ 引用 key 列表
+ """
+ # 匹配 \cite{...} 命令
+ cite_pattern = r'\\cite(?:\[[^\]]*\])?(?:\[[^\]]*\])?\{([^}]+)\}'
+ citations = re.findall(cite_pattern, tex_content)
+
+ # 展开多个引用
+ all_keys = []
+ for cite in citations:
+ keys = [k.strip() for k in cite.split(',')]
+ all_keys.extend(keys)
+
+ return list(set(all_keys)) # 去重
+
+
+def check_latex_consistency(tex_keys: List[str], bib_keys: List[str]) -> List[FormatError]:
+ """检查 LaTeX 引用与 BibTeX 的一致性
+
+ Args:
+ tex_keys: LaTeX 中的引用 key 列表
+ bib_keys: BibTeX 中的 key 列表
+
+ Returns:
+ 错误列表
+ """
+ errors = []
+
+ tex_set = set(tex_keys)
+ bib_set = set(bib_keys)
+
+ # 未定义的引用
+ undefined = tex_set - bib_set
+ if undefined:
+ for key in sorted(undefined):
+ errors.append(FormatError(
+ level=ErrorLevel.ERROR,
+ location=f"latex:cite",
+ field=key,
+ message=f"未定义的引用: {key}",
+ suggestion=f"在 BibTeX 文件中添加 {key} 条目"
+ ))
+
+ # 未使用的引用
+ unused = bib_set - tex_set
+ if unused:
+ for key in sorted(unused):
+ errors.append(FormatError(
+ level=ErrorLevel.WARNING,
+ location=f"bibtex:entry",
+ field=key,
+ message=f"未使用的引用: {key}",
+ suggestion=f"在 LaTeX 文件中引用 {key} 或从 BibTeX 中删除"
+ ))
+
+ return errors
+
+
+# ============================================================================
+# 报告生成函数
+# ============================================================================
+
+def print_errors(errors: List[FormatError], verbose: bool = False):
+ """打印错误列表
+
+ Args:
+ errors: 错误列表
+ verbose: 是否显示详细信息
+ """
+ if not errors:
+ print("✅ 未发现格式错误")
+ return
+
+ # 按级别分组
+ errors_by_level = {
+ ErrorLevel.ERROR: [],
+ ErrorLevel.WARNING: [],
+ ErrorLevel.INFO: []
+ }
+
+ for error in errors:
+ errors_by_level[error.level].append(error)
+
+ # 打印统计
+ print("\n" + "="*60)
+ print("格式检查结果")
+ print("="*60)
+ print(f"❌ 错误: {len(errors_by_level[ErrorLevel.ERROR])}")
+ print(f"⚠️ 警告: {len(errors_by_level[ErrorLevel.WARNING])}")
+ print(f"ℹ️ 信息: {len(errors_by_level[ErrorLevel.INFO])}")
+ print("="*60)
+
+ # 打印详细错误
+ for level in [ErrorLevel.ERROR, ErrorLevel.WARNING, ErrorLevel.INFO]:
+ level_errors = errors_by_level[level]
+ if not level_errors:
+ continue
+
+ level_symbol = {
+ ErrorLevel.ERROR: "❌",
+ ErrorLevel.WARNING: "⚠️",
+ ErrorLevel.INFO: "ℹ️"
+ }[level]
+
+ print(f"\n{level_symbol} {level.value.upper()} ({len(level_errors)}):\n")
+
+ for error in level_errors:
+ print(f" [{error.location}]", end="")
+ if error.field:
+ print(f" {error.field}:", end="")
+ print(f" {error.message}")
+
+ if verbose and error.suggestion:
+ print(f" 💡 建议: {error.suggestion}")
+ print()
+
+
+def generate_report(errors: List[FormatError], output_file: str):
+ """生成文本格式的检查报告
+
+ Args:
+ errors: 错误列表
+ output_file: 输出文件路径
+ """
+ # 按级别分组
+ errors_by_level = {
+ ErrorLevel.ERROR: [],
+ ErrorLevel.WARNING: [],
+ ErrorLevel.INFO: []
+ }
+
+ for error in errors:
+ errors_by_level[error.level].append(error)
+
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write("# BibTeX/LaTeX 格式检查报告\n\n")
+
+ # 总体统计
+ f.write("## 总体统计\n\n")
+ f.write(f"- **错误**: {len(errors_by_level[ErrorLevel.ERROR])}\n")
+ f.write(f"- **警告**: {len(errors_by_level[ErrorLevel.WARNING])}\n")
+ f.write(f"- **信息**: {len(errors_by_level[ErrorLevel.INFO])}\n\n")
+
+ # 详细错误
+ for level in [ErrorLevel.ERROR, ErrorLevel.WARNING, ErrorLevel.INFO]:
+ level_errors = errors_by_level[level]
+ if not level_errors:
+ continue
+
+ level_name = {
+ ErrorLevel.ERROR: "错误",
+ ErrorLevel.WARNING: "警告",
+ ErrorLevel.INFO: "信息"
+ }[level]
+
+ f.write(f"## {level_name} ({len(level_errors)})\n\n")
+
+ for error in level_errors:
+ f.write(f"### [{error.location}]")
+ if error.field:
+ f.write(f" {error.field}")
+ f.write("\n\n")
+ f.write(f"**问题**: {error.message}\n\n")
+ if error.suggestion:
+ f.write(f"**建议**: {error.suggestion}\n\n")
+
+ print(f"\n报告已保存到: {output_file}")
diff --git a/.agents/skills/citation-verification/scripts/verify-citations.py b/.agents/skills/citation-verification/scripts/verify-citations.py
new file mode 100644
index 0000000..ea092be
--- /dev/null
+++ b/.agents/skills/citation-verification/scripts/verify-citations.py
@@ -0,0 +1,678 @@
+#!/usr/bin/env python3
+"""
+Citation Verification Script
+
+四层验证机制:
+1. Format Validation - BibTeX 格式检查
+2. Existence Verification - API 验证论文存在性
+3. Information Matching - 信息匹配(标题、作者、年份)
+4. Content Validation - 综合评分和判定
+
+使用方法:
+ python verify-citations.py references.bib
+ python verify-citations.py paper.tex --check-latex
+ python verify-citations.py references.bib --verbose --output report.md
+"""
+
+import argparse
+import sys
+import json
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+from dataclasses import dataclass, asdict
+import re
+from difflib import SequenceMatcher
+
+# 尝试导入 bibtexparser
+try:
+ import bibtexparser
+ from bibtexparser.bparser import BibTexParser
+except ImportError:
+ print("错误: 需要安装 bibtexparser")
+ print("运行: pip install bibtexparser")
+ sys.exit(1)
+
+# 尝试导入 API 客户端库
+try:
+ from semanticscholar import SemanticScholar
+except ImportError:
+ print("警告: semanticscholar 未安装,将跳过 Semantic Scholar 验证")
+ print("运行: pip install semanticscholar")
+
+try:
+ import arxiv
+except ImportError:
+ print("警告: arxiv 未安装,将跳过 arXiv 验证")
+ print("运行: pip install arxiv")
+
+try:
+ import requests
+except ImportError:
+ print("错误: 需要安装 requests")
+ print("运行: pip install requests")
+ sys.exit(1)
+
+
+@dataclass
+class VerificationResult:
+ """验证结果数据类"""
+ citation_key: str
+ status: str # verified, partial_match, low_match, failed, not_found
+ confidence: str # high_confidence, medium_confidence, low_confidence, no_confidence
+ match_score: float
+ format_errors: List[str]
+ api_source: Optional[str] # crossref, arxiv, semantic_scholar
+ message: str
+
+
+def parse_arguments():
+ """解析命令行参数"""
+ parser = argparse.ArgumentParser(
+ description='验证 BibTeX 引用的准确性和完整性',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+示例:
+ %(prog)s references.bib
+ %(prog)s paper.tex --check-latex
+ %(prog)s references.bib --verbose --output report.md
+ %(prog)s references.bib --api-only
+ """
+ )
+
+ parser.add_argument(
+ 'input_file',
+ type=str,
+ help='BibTeX 文件(.bib)或 LaTeX 文件(.tex)'
+ )
+
+ parser.add_argument(
+ '--check-latex',
+ action='store_true',
+ help='检查 LaTeX 引用一致性(需要提供 .tex 文件)'
+ )
+
+ parser.add_argument(
+ '--verbose',
+ action='store_true',
+ help='显示详细验证信息'
+ )
+
+ parser.add_argument(
+ '--output',
+ type=str,
+ help='输出报告文件路径(Markdown 格式)'
+ )
+
+ parser.add_argument(
+ '--api-only',
+ action='store_true',
+ help='仅进行 API 验证,跳过格式检查'
+ )
+
+ parser.add_argument(
+ '--format-only',
+ action='store_true',
+ help='仅进行格式检查,跳过 API 验证'
+ )
+
+ parser.add_argument(
+ '--threshold',
+ type=float,
+ default=0.85,
+ help='匹配阈值(0.0-1.0),默认 0.85'
+ )
+
+ return parser.parse_args()
+
+
+def load_bibtex(file_path: str) -> List[Dict]:
+ """加载 BibTeX 文件"""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ parser = BibTexParser(common_strings=True)
+ bib_database = bibtexparser.load(f, parser)
+ return bib_database.entries
+ except FileNotFoundError:
+ print(f"错误: 文件不存在: {file_path}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"错误: 无法解析 BibTeX 文件: {e}")
+ sys.exit(1)
+
+
+def extract_latex_citations(tex_file: str) -> List[str]:
+ """从 LaTeX 文件中提取引用"""
+ try:
+ with open(tex_file, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # 匹配 \cite{...} 命令
+ cite_pattern = r'\\cite(?:\[[^\]]*\])?\{([^}]+)\}'
+ citations = re.findall(cite_pattern, content)
+
+ # 展开多个引用
+ all_keys = []
+ for cite in citations:
+ keys = [k.strip() for k in cite.split(',')]
+ all_keys.extend(keys)
+
+ return list(set(all_keys)) # 去重
+ except FileNotFoundError:
+ print(f"错误: 文件不存在: {tex_file}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"错误: 无法解析 LaTeX 文件: {e}")
+ sys.exit(1)
+
+
+# ============================================================================
+# Layer 1: Format Validation (格式验证)
+# ============================================================================
+
+def get_required_fields(entry_type: str) -> List[str]:
+ """获取 BibTeX 条目类型的必填字段"""
+ required_fields = {
+ 'article': ['author', 'title', 'journal', 'year'],
+ 'inproceedings': ['author', 'title', 'booktitle', 'year'],
+ 'book': ['title', 'publisher', 'year'],
+ 'misc': ['title'],
+ 'phdthesis': ['author', 'title', 'school', 'year'],
+ 'mastersthesis': ['author', 'title', 'school', 'year'],
+ 'techreport': ['author', 'title', 'institution', 'year'],
+ }
+ return required_fields.get(entry_type.lower(), ['title'])
+
+
+def check_bibtex_format(entry: Dict) -> List[str]:
+ """检查 BibTeX 条目格式
+
+ Returns:
+ 错误列表
+ """
+ errors = []
+
+ # 检查条目类型
+ if 'ENTRYTYPE' not in entry:
+ errors.append("缺少条目类型")
+ return errors
+
+ # 检查 ID
+ if 'ID' not in entry:
+ errors.append("缺少 citation key")
+
+ # 检查必填字段
+ entry_type = entry.get('ENTRYTYPE', '')
+ required = get_required_fields(entry_type)
+ for field in required:
+ if field not in entry or not entry[field].strip():
+ errors.append(f"缺少必填字段: {field}")
+
+ # 年份格式检查
+ if 'year' in entry:
+ year = entry['year'].strip()
+ if not year.isdigit() or len(year) != 4:
+ errors.append(f"年份格式错误: {year}")
+ else:
+ year_int = int(year)
+ if year_int < 1900 or year_int > 2030:
+ errors.append(f"年份超出合理范围: {year}")
+
+ # DOI 格式检查
+ if 'doi' in entry:
+ doi = entry['doi'].strip()
+ if not doi.startswith('10.'):
+ errors.append(f"DOI 格式错误: {doi}")
+
+ return errors
+
+
+def check_citation_consistency(tex_keys: List[str], bib_keys: List[str]) -> Dict:
+ """检查 LaTeX 引用与 BibTeX 的一致性
+
+ Returns:
+ {'undefined': [...], 'unused': [...]}
+ """
+ tex_set = set(tex_keys)
+ bib_set = set(bib_keys)
+
+ return {
+ 'undefined': list(tex_set - bib_set),
+ 'unused': list(bib_set - tex_set)
+ }
+
+
+# ============================================================================
+# Layer 2: Existence Verification (存在性验证)
+# ============================================================================
+
+def verify_with_crossref(doi: str) -> Optional[Dict]:
+ """通过 CrossRef API 验证 DOI"""
+ try:
+ url = f"https://api.crossref.org/works/{doi}"
+ response = requests.get(url, timeout=10)
+ if response.status_code == 200:
+ data = response.json()
+ return data.get('message')
+ return None
+ except Exception as e:
+ print(f"CrossRef API 错误: {e}")
+ return None
+
+
+def verify_with_arxiv(arxiv_id: str) -> Optional[Dict]:
+ """通过 arXiv API 验证"""
+ try:
+ search = arxiv.Search(id_list=[arxiv_id])
+ paper = next(search.results())
+ return {
+ 'title': paper.title,
+ 'authors': [a.name for a in paper.authors],
+ 'year': paper.published.year,
+ 'arxiv_id': arxiv_id
+ }
+ except Exception as e:
+ print(f"arXiv API 错误: {e}")
+ return None
+
+
+def verify_with_semantic_scholar(title: str, authors: Optional[List[str]] = None) -> Optional[Dict]:
+ """通过 Semantic Scholar API 验证"""
+ try:
+ sch = SemanticScholar()
+ results = sch.search_paper(title, limit=5)
+
+ if not results:
+ return None
+
+ # 返回第一个结果
+ paper = results[0]
+ return {
+ 'title': paper.title,
+ 'authors': [a.name for a in paper.authors] if paper.authors else [],
+ 'year': paper.year,
+ 'paperId': paper.paperId
+ }
+ except Exception as e:
+ print(f"Semantic Scholar API 错误: {e}")
+ return None
+
+
+def verify_existence(entry: Dict) -> Tuple[bool, Optional[str], Optional[Dict]]:
+ """验证论文存在性
+
+ Returns:
+ (exists, api_source, api_data)
+ """
+ # 策略 1: DOI 优先
+ if 'doi' in entry:
+ data = verify_with_crossref(entry['doi'])
+ if data:
+ return True, 'crossref', data
+
+ # 策略 2: arXiv ID
+ if 'eprint' in entry or 'arxiv' in entry.get('note', '').lower():
+ arxiv_id = entry.get('eprint', '')
+ if not arxiv_id:
+ # 尝试从 note 中提取
+ match = re.search(r'arXiv:(\d{4}\.\d{4,5})', entry.get('note', ''))
+ if match:
+ arxiv_id = match.group(1)
+
+ if arxiv_id:
+ data = verify_with_arxiv(arxiv_id)
+ if data:
+ return True, 'arxiv', data
+
+ # 策略 3: 通用搜索
+ if 'title' in entry:
+ authors = entry.get('author', '').split(' and ') if 'author' in entry else None
+ data = verify_with_semantic_scholar(entry['title'], authors)
+ if data:
+ return True, 'semantic_scholar', data
+
+ return False, None, None
+
+
+# ============================================================================
+# Layer 3 & 4: Information Matching & Content Validation (信息匹配和内容验证)
+# ============================================================================
+
+def normalize_text(text: str) -> str:
+ """标准化文本用于匹配"""
+ text = text.lower()
+ text = re.sub(r'[^\w\s]', '', text)
+ return ' '.join(text.split())
+
+
+def match_title(title1: str, title2: str, threshold: float = 0.85) -> Dict:
+ """标题匹配"""
+ t1 = normalize_text(title1)
+ t2 = normalize_text(title2)
+
+ ratio = SequenceMatcher(None, t1, t2).ratio()
+
+ return {
+ 'match': ratio >= threshold,
+ 'similarity': ratio
+ }
+
+
+def normalize_author_name(name: str) -> str:
+ """标准化作者名"""
+ parts = name.replace(',', '').split()
+ return ' '.join(sorted(parts)).lower()
+
+
+def match_authors(authors1: List[str], authors2: List[str], threshold: float = 0.7) -> Dict:
+ """作者匹配"""
+ names1 = [normalize_author_name(a) for a in authors1]
+ names2 = [normalize_author_name(a) for a in authors2]
+
+ set1 = set(names1)
+ set2 = set(names2)
+ intersection = len(set1 & set2)
+ union = len(set1 | set2)
+
+ if union == 0:
+ return {'match': False, 'similarity': 0}
+
+ ratio = intersection / union
+
+ return {
+ 'match': ratio >= threshold,
+ 'similarity': ratio
+ }
+
+
+def match_year(year1: str, year2: int, tolerance: int = 1) -> Dict:
+ """年份匹配"""
+ try:
+ y1 = int(year1)
+ y2 = int(year2)
+ diff = abs(y1 - y2)
+ return {
+ 'match': diff <= tolerance,
+ 'difference': diff
+ }
+ except (ValueError, TypeError):
+ return {'match': False, 'difference': None}
+
+
+def calculate_match_score(entry: Dict, api_data: Dict, threshold: float) -> float:
+ """计算综合匹配分数"""
+ scores = {}
+ weights = {
+ 'title': 0.4,
+ 'authors': 0.3,
+ 'year': 0.2,
+ 'venue': 0.1
+ }
+
+ # 标题匹配
+ if 'title' in entry and 'title' in api_data:
+ result = match_title(entry['title'], api_data['title'], threshold)
+ scores['title'] = result['similarity']
+
+ # 作者匹配
+ if 'author' in entry and 'authors' in api_data:
+ entry_authors = entry['author'].split(' and ')
+ result = match_authors(entry_authors, api_data['authors'])
+ scores['authors'] = result['similarity']
+
+ # 年份匹配
+ if 'year' in entry and 'year' in api_data:
+ result = match_year(entry['year'], api_data['year'])
+ scores['year'] = 1.0 if result['match'] else 0.0
+
+ # 计算加权总分
+ total_score = 0
+ total_weight = 0
+ for key, weight in weights.items():
+ if key in scores:
+ total_score += scores[key] * weight
+ total_weight += weight
+
+ if total_weight == 0:
+ return 0
+
+ return total_score / total_weight
+
+
+def judge_verification_result(match_score: float) -> Dict:
+ """判定验证结果"""
+ if match_score >= 0.9:
+ return {
+ 'status': 'verified',
+ 'level': 'high_confidence',
+ 'message': '✅ 验证通过 - 信息完全匹配'
+ }
+ elif match_score >= 0.7:
+ return {
+ 'status': 'partial_match',
+ 'level': 'medium_confidence',
+ 'message': '⚠️ 部分匹配 - 信息有轻微差异,建议人工确认'
+ }
+ elif match_score >= 0.5:
+ return {
+ 'status': 'low_match',
+ 'level': 'low_confidence',
+ 'message': '❌ 匹配度低 - 信息差异较大,需要人工验证'
+ }
+ else:
+ return {
+ 'status': 'failed',
+ 'level': 'no_confidence',
+ 'message': '❌ 验证失败 - 信息严重不匹配或论文不存在'
+ }
+
+
+def verify_citation(entry: Dict, args) -> VerificationResult:
+ """完整的引用验证流程"""
+ citation_key = entry.get('ID', 'unknown')
+
+ # Layer 1: 格式验证
+ format_errors = []
+ if not args.api_only:
+ format_errors = check_bibtex_format(entry)
+
+ # Layer 2: 存在性验证
+ if args.format_only:
+ return VerificationResult(
+ citation_key=citation_key,
+ status='format_checked',
+ confidence='n/a',
+ match_score=0.0,
+ format_errors=format_errors,
+ api_source=None,
+ message='仅格式检查'
+ )
+
+ exists, api_source, api_data = verify_existence(entry)
+
+ if not exists:
+ return VerificationResult(
+ citation_key=citation_key,
+ status='not_found',
+ confidence='no_confidence',
+ match_score=0.0,
+ format_errors=format_errors,
+ api_source=None,
+ message='❌ 论文不存在 - 无法通过任何 API 验证'
+ )
+
+ # Layer 3 & 4: 信息匹配和内容验证
+ match_score = calculate_match_score(entry, api_data, args.threshold)
+ judgment = judge_verification_result(match_score)
+
+ return VerificationResult(
+ citation_key=citation_key,
+ status=judgment['status'],
+ confidence=judgment['level'],
+ match_score=match_score,
+ format_errors=format_errors,
+ api_source=api_source,
+ message=judgment['message']
+ )
+
+
+# ============================================================================
+# Report Generation (报告生成)
+# ============================================================================
+
+def print_summary(results: List[VerificationResult], verbose: bool = False):
+ """打印验证摘要"""
+ total = len(results)
+ verified = sum(1 for r in results if r.status == 'verified')
+ partial = sum(1 for r in results if r.status == 'partial_match')
+ low = sum(1 for r in results if r.status == 'low_match')
+ failed = sum(1 for r in results if r.status in ['failed', 'not_found'])
+
+ print("\n" + "="*60)
+ print("验证摘要")
+ print("="*60)
+ print(f"总引用数: {total}")
+ print(f"✅ 验证通过: {verified} ({verified/total*100:.1f}%)")
+ print(f"⚠️ 部分匹配: {partial} ({partial/total*100:.1f}%)")
+ print(f"❌ 匹配度低: {low} ({low/total*100:.1f}%)")
+ print(f"❌ 验证失败: {failed} ({failed/total*100:.1f}%)")
+ print("="*60)
+
+ if verbose:
+ print("\n详细结果:\n")
+ for result in results:
+ print(f"[{result.citation_key}]")
+ print(f" 状态: {result.message}")
+ print(f" 匹配分数: {result.match_score:.2f}")
+ if result.api_source:
+ print(f" 验证源: {result.api_source}")
+ if result.format_errors:
+ print(f" 格式错误: {', '.join(result.format_errors)}")
+ print()
+
+
+def generate_markdown_report(results: List[VerificationResult], output_file: str):
+ """生成 Markdown 格式的验证报告"""
+ total = len(results)
+ verified = sum(1 for r in results if r.status == 'verified')
+ partial = sum(1 for r in results if r.status == 'partial_match')
+ low = sum(1 for r in results if r.status == 'low_match')
+ failed = sum(1 for r in results if r.status in ['failed', 'not_found'])
+
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write("# Citation Verification Report\n\n")
+
+ # 总体统计
+ f.write("## 总体统计\n\n")
+ f.write(f"- **总引用数**: {total}\n")
+ f.write(f"- **✅ 验证通过**: {verified} ({verified/total*100:.1f}%)\n")
+ f.write(f"- **⚠️ 部分匹配**: {partial} ({partial/total*100:.1f}%)\n")
+ f.write(f"- **❌ 匹配度低**: {low} ({low/total*100:.1f}%)\n")
+ f.write(f"- **❌ 验证失败**: {failed} ({failed/total*100:.1f}%)\n\n")
+
+ # 详细结果
+ f.write("## 详细结果\n\n")
+
+ # 按状态分组
+ for status, emoji, title in [
+ ('verified', '✅', '验证通过'),
+ ('partial_match', '⚠️', '部分匹配'),
+ ('low_match', '❌', '匹配度低'),
+ ('failed', '❌', '验证失败'),
+ ('not_found', '❌', '论文不存在')
+ ]:
+ status_results = [r for r in results if r.status == status]
+ if status_results:
+ f.write(f"### {emoji} {title} ({len(status_results)})\n\n")
+ for result in status_results:
+ f.write(f"#### `{result.citation_key}`\n\n")
+ f.write(f"- **状态**: {result.message}\n")
+ f.write(f"- **匹配分数**: {result.match_score:.2f}\n")
+ f.write(f"- **置信度**: {result.confidence}\n")
+ if result.api_source:
+ f.write(f"- **验证源**: {result.api_source}\n")
+ if result.format_errors:
+ f.write(f"- **格式错误**:\n")
+ for error in result.format_errors:
+ f.write(f" - {error}\n")
+ f.write("\n")
+
+ # 建议操作
+ f.write("## 建议操作\n\n")
+ if failed > 0:
+ f.write("### 需要修正的引用\n\n")
+ failed_results = [r for r in results if r.status in ['failed', 'not_found']]
+ for result in failed_results:
+ f.write(f"- `{result.citation_key}`: {result.message}\n")
+ f.write("\n")
+
+ if partial > 0 or low > 0:
+ f.write("### 需要人工确认的引用\n\n")
+ check_results = [r for r in results if r.status in ['partial_match', 'low_match']]
+ for result in check_results:
+ f.write(f"- `{result.citation_key}`: {result.message}\n")
+ f.write("\n")
+
+ print(f"\n报告已保存到: {output_file}")
+
+
+# ============================================================================
+# Main Function (主函数)
+# ============================================================================
+
+def main():
+ """主函数"""
+ args = parse_arguments()
+
+ # 加载 BibTeX 文件
+ print(f"正在加载 BibTeX 文件: {args.input_file}")
+ entries = load_bibtex(args.input_file)
+ print(f"找到 {len(entries)} 个引用条目")
+
+ # LaTeX 一致性检查
+ if args.check_latex:
+ tex_file = args.input_file.replace('.bib', '.tex')
+ if Path(tex_file).exists():
+ print(f"\n正在检查 LaTeX 引用一致性: {tex_file}")
+ tex_keys = extract_latex_citations(tex_file)
+ bib_keys = [e['ID'] for e in entries]
+ consistency = check_citation_consistency(tex_keys, bib_keys)
+
+ if consistency['undefined']:
+ print(f"⚠️ 未定义的引用 ({len(consistency['undefined'])}): {', '.join(consistency['undefined'])}")
+ if consistency['unused']:
+ print(f"⚠️ 未使用的引用 ({len(consistency['unused'])}): {', '.join(consistency['unused'])}")
+ if not consistency['undefined'] and not consistency['unused']:
+ print("✅ LaTeX 引用与 BibTeX 完全一致")
+
+ # 验证每个引用
+ print("\n开始验证引用...")
+ results = []
+ for i, entry in enumerate(entries, 1):
+ citation_key = entry.get('ID', 'unknown')
+ print(f"[{i}/{len(entries)}] 验证 {citation_key}...", end=' ')
+
+ result = verify_citation(entry, args)
+ results.append(result)
+
+ # 简短状态输出
+ if result.status == 'verified':
+ print("✅")
+ elif result.status == 'partial_match':
+ print("⚠️")
+ else:
+ print("❌")
+
+ # 打印摘要
+ print_summary(results, args.verbose)
+
+ # 生成报告
+ if args.output:
+ generate_markdown_report(results, args.output)
+
+ # 返回退出码
+ failed_count = sum(1 for r in results if r.status in ['failed', 'not_found'])
+ return 0 if failed_count == 0 else 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())