Files
PlantAI-rag-new/paper_writer_service/app/services/paper/cite.py
T
admin 91f6a1c242 feat: 新增论文撰写功能 paper-writer(独立 MCP 服务 + BFF 大脑 + 前端三卡)
- paper_writer_service(8097):FastAPI + MCP streamable-http 单端点,9 工具
  (propose_paper_outline/write_paper_section/get_paper_progress/search_refs/
  revise_paper_section/revise_paper/get_paper/complete_paper/cancel_paper),
  SQLite 三表(papers/paper_sections/citations,WAL),GB/T 7714 引用协议
- BFF:services/paper/{mcp_client,agent}.py(裸 JSON-RPC 客户端 + 规则状态机)、
  routers/paper.py、schemas.py 4 个 Paper*Block + ArtifactBlock 加 paper_id/citation_count、
  config.py 开关、main.py 条件注册、docgen.py 抽 insert_artifact 公共 helper
- 前端:intent.js paperIntent 纯函数、Composer docgen 分支 sub-route、sendPaper/轮询、
  PaperBriefCard/PaperOutlineCard/PaperProgressCard、DocPane cite-chip
- deploy/paper_restart.sh + toggle_paper.sh;compile_sdk.sh 白名单追加 paper 文件
- tests/paper 26 条(工具逻辑 + MCP 协议 + BFF 状态机 + 块 schema + 前端 intent 单测)
2026-09-30 22:29:22 +08:00

66 lines
2.5 KiB
Python

"""cite.py —— 引用协议:GB/T 7714 渲染、[n] 校验、complete 按出现序重排。"""
from __future__ import annotations
import re
from typing import Any
_CITE_RE = re.compile(r"\[(\d+)\]")
def find_cite_numbers(markdown: str | None) -> list[int]:
"""正文中出现的所有 [n] 编号(含重复,按出现顺序)。"""
return [int(m) for m in _CITE_RE.findall(markdown or "")]
def render_gbt7714(meta: dict[str, Any], *, fallback_year: str = "2026") -> str:
"""按可用元数据渲染 GB/T 7714 近似条目。
- 有 DOI → [J/OL] + DOI
- 有 PMID → [J] + PMID
- 无 DOI/PMID(知识库资料)→ 降级格式 + [EB/OL]
"""
title = (meta.get("title") or "").strip()
journal = (meta.get("journal") or "").strip()
authors = (meta.get("authors") or "").strip()
year = (meta.get("year") or fallback_year).strip()
doi = (meta.get("doi") or "").strip()
pmid = (meta.get("pmid") or "").strip()
head = f"{authors} " if authors else ""
if doi:
jpart = f"{journal}, {year}." if journal else f"{year}."
return f"{head}{title}[J/OL]. {jpart} DOI:{doi}."
if pmid:
jpart = f"{journal}, {year}." if journal else f"{year}."
return f"{head}{title}[J]. {jpart} PMID:{pmid}."
return f"{head}{title}. 知识库资料, {year}. [EB/OL]."
def render_disclaimer() -> str:
return "参考文献依据知识库资料生成,格式为 GB/T 7714 近似,建议人工核对。"
def first_appearance_order(full_markdown: str, temp_numbers: list[int]) -> list[int]:
"""按全文首次出现顺序返回 temp_n;未出现的排最后(保持原序)。"""
seen: dict[int, int] = {}
for m in _CITE_RE.finditer(full_markdown or ""):
n = int(m.group(1))
if n not in seen and n in temp_numbers:
seen[n] = m.start()
ordered = sorted((n for n in temp_numbers if n in seen), key=lambda n: seen[n])
ordered += [n for n in temp_numbers if n not in seen]
return ordered
def renumber_map(temp_numbers: list[int], full_markdown: str) -> dict[int, int]:
"""temp_n → final_n(按全文首次出现顺序 1..N)。"""
ordered = first_appearance_order(full_markdown, temp_numbers)
return {temp: final for final, temp in enumerate(ordered, start=1)}
def replace_numbers(markdown: str | None, mapping: dict[int, int]) -> str:
def _sub(m: re.Match[str]) -> str:
n = int(m.group(1))
return f"[{mapping.get(n, n)}]"
return _CITE_RE.sub(_sub, markdown or "")