forked from admin/PlantAI-rag-new
- 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 单测)
66 lines
2.5 KiB
Python
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 "")
|