
Papercash
- 7 installs
- 98 repo stars
- Updated April 7, 2026
- jesseovo/papercash
Assists the full academic paper workflow: multi-source search, literature review, plagiarism pre-check, citation formatting and Word export.
About
Handles academic paper research and writing by searching 8 scholarly sources, generating reviews, pre-checking for plagiarism, reducing AI-detection and formatting references. A researcher uses it across the paper-writing lifecycle from search to Word export.
- Searches Semantic Scholar, arXiv, CrossRef, Baidu Scholar and more
- Covers literature review, plagiarism pre-check and reference formatting
Papercash by the numbers
- 7 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,193 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jesseovo/papercash --skill papercashAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 98 |
| Last updated | April 7, 2026 |
| Repository | jesseovo/papercash ↗ |
What it does
Assists the full academic paper workflow: multi-source search, literature review, plagiarism pre-check, citation formatting and Word export.
Files
请参阅项目根目录的 SKILL.md 获取完整指令。
核心命令:
python scripts/papercash.py search "<主题>"— 多源论文检索python scripts/papercash.py review "<主题>"— 文献综述生成python scripts/papercash.py check "<文本>"— 查重预检python scripts/papercash.py humanize "<文本>"— 降AI率改写python scripts/papercash.py cite "<DOI>" --style gb7714— 参考文献格式化python scripts/papercash.py outline "<题目>"— 论文大纲生成python scripts/papercash.py polish "<文本>"— 学术润色python scripts/papercash.py format "<文件>"— Word 格式检查
{
"name": "papercash",
"version": "1.0.0",
"description": "PaperCash - 论文全流程辅助 Skill",
"skill_file": "../SKILL.md",
"author": "Jesse (@Jesseovo)",
"repository": "https://github.com/Jesseovo/PaperCash",
"license": "MIT"
}
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
.eggs/
*.egg
.env
*.env
.venv/
venv/
ENV/
.cache/
*.db
*.sqlite3
.DS_Store
Thumbs.db
*.log
.idea/
.vscode/
*.swp
*.swo
推文/
article/
PaperCash Agent Instructions
本项目是一个论文全流程辅助工具。当用户询问论文、文献、查重、降AI率等相关问题时,使用 scripts/papercash.py CLI 来完成任务。
依赖安装
pip install -r requirements.txt可用命令
| 命令 | 用途 |
|---|---|
python scripts/papercash.py search "<主题>" | 多源论文检索 |
python scripts/papercash.py review "<主题>" | 文献综述生成 |
python scripts/papercash.py check "<文本>" | 查重预检 |
python scripts/papercash.py humanize "<文本>" | 降AI率改写 |
python scripts/papercash.py cite "<DOI>" --style gb7714 | 参考文献格式化 |
python scripts/papercash.py outline "<题目>" | 论文大纲生成 |
python scripts/papercash.py polish "<文本>" | 学术润色 |
python scripts/papercash.py format "<文件>" | Word 格式检查 |
python scripts/papercash.py --diagnose | 数据源健康检查 |
重要约束
1. 所有引用来自真实学术数据库,需提醒用户核实 2. 查重预检仅供参考,不替代学校指定系统 3. 降AI率改写仅提供建议,最终文本需体现学生本人思考
name: papercash
description: "Full-stack academic paper assistance: search, review, write, check, humanize, cite"
version: "1.0.0"
skill_file: SKILL.md
triggers:
- "papercash"
- "论文"
- "文献"
- "查重"
- "降AI"
- "参考文献"
- "paper"
- "citation"
{
"name": "papercash",
"tagline": "论文全流程辅助 — 免费查文献、查重、降AI率、参考文献格式化",
"description": "PaperCash 聚合 Semantic Scholar、arXiv、CrossRef、百度学术等 8 大学术数据源,提供论文检索、文献综述自动生成、查重预检、降AI率改写、参考文献格式化(GB/T 7714 / APA / BibTeX)、Word 导出等全流程功能,帮助中国大学生省时省钱完成论文。",
"category": "research",
"tags": ["academic", "paper", "citation", "literature-review", "plagiarism", "chinese"],
"version": "1.1.0",
"license": "MIT",
"author": "Jesse (@Jesseovo)",
"repository": "https://github.com/Jesseovo/PaperCash",
"support": "https://github.com/Jesseovo/PaperCash/issues",
"skill_file": "SKILL.md",
"requires": {
"bins": ["python3"]
}
}
PaperCash 学术论文辅助工具 — 模拟用户体验反馈报告
1. 报告标题与说明
报告名称:PaperCash AI Agent Skill 百名中国大学生模拟体验反馈(2026) 报告角色:产品体验总监视角下的合成调研报告 样本设定:100 名来自不同学科门类的中国全日制本科生与研究生(含大四、研一—研三、部分博一) 调研方式说明:本报告为基于产品能力画像与典型用户痛点的情景化模拟反馈,用于内部产品评审与迭代优先级讨论;非真实田野调查数据。 评分口径:功能体验采用 5 分制(1=很不满意,5=非常满意);文末 NPS 为净推荐值模拟区间与理由说明。
---
2. 测试学生概览(专业分布)
本次模拟样本按 10 个学科大类 × 每类 10 人 配置,兼顾文、理、工、医、法、经管、艺术、教育、农生、交叉学科等多样性。
| 序号 | 学科大类 | 人数 | 代表性专业方向(示例) | 年级结构(模拟) |
|---|---|---|---|---|
| 1 | 文学与新闻传播 | 10 | 汉语言文学、比较文学、新闻学、网络与新媒体 | 本科 4、硕士 5、博士 1 |
| 2 | 理工基础与工程 | 10 | 应用数学、物理学、化学工程、材料科学 | 本科 3、硕士 6、博士 1 |
| 3 | 医学与公共卫生 | 10 | 临床医学、护理学、预防医学、药学 | 本科 2、硕士 7、博士 1 |
| 4 | 法学 | 10 | 民商法、刑法、国际法、知识产权 | 本科 5、硕士 5 |
| 5 | 经济与管理 | 10 | 金融学、会计学、工商管理、市场营销 | 本科 4、硕士 6 |
| 6 | 艺术与设计 | 10 | 视觉传达、环境设计、音乐学、数字媒体艺术 | 本科 7、硕士 3 |
| 7 | 教育学与师范 | 10 | 教育学、小学教育、特殊教育、教育技术 | 本科 5、硕士 5 |
| 8 | 农学与生命科学 | 10 | 农学、植物保护、生物技术、生态学 | 本科 4、硕士 6 |
| 9 | 历史学与哲学人文 | 10 | 历史学、考古学、哲学、宗教学 | 本科 4、硕士 5、博士 1 |
| 10 | 计算机与电子信息 | 10 | 计算机科学、软件工程、人工智能、通信工程 | 本科 3、硕士 7 |
| 合计 | — | 100 | — | — |
多样性小结:人文类更依赖中文库与引文规范;理工医更依赖英文预印本与 PubMed/Semantic Scholar;法学、经管对判例、政策文本与 GB/T 7714 要求高;艺术、设计类对格式检查与「非标准院系模板」冲突更敏感。
---
3. 核心功能体验评分(5 分制)
下表为 100 人模拟打分的算术均值(保留两位小数),并附合成「高频评语」摘要(正负面均含)。
| 功能模块 | 均分 | 标准差(模拟) | 典型正面评语摘要 | 典型负面/顾虑评语摘要 |
|---|---|---|---|---|
| 1. 文献检索(8 源聚合) | 3.82 | 0.91 | 「一次搜多库省时间」「理工科英文题录较全」 | 「知网/万方体验依赖账号与网络」「Google Scholar 不稳定」「结果去重与排序还可更聪明」 |
| 2. 文献综述生成 | 3.54 | 1.05 | 「搭架子快,适合赶 ddl」 | 「容易泛、缺批判性」「医学/法学需要更细的分主题与证据等级」 |
| 3. 论文大纲生成 | 3.91 | 0.78 | 「结构像学院模板,改起来比从零快」 | 「交叉学科题目容易套模板感」「艺术/设计类论文结构差异大」 |
| 4. 段落扩写 | 3.38 | 1.12 | 「字数不够时救急」 | 「担心扩写后 AI 痕迹更重」「人文论证容易变水」 |
| 5. 学术润色 | 3.76 | 0.88 | 「口语改书面很省事」 | 「法学表述要极度严谨,不敢全信」「个别术语替换不地道」 |
| 6. 查重预检 | 3.69 | 0.94 | 「正式花钱查重前心里有个数」 | 「与学校最终查重引擎不一致会焦虑」「预检偏低/偏高都遇到过(模拟口径)」 |
| 7. 降 AI 率改写 | 3.22 | 1.18 | 「学校要 AI 检测时的救命稻草心态」 | 「改完语义漂移」「医学数据段落不敢乱改」「像「洗稿」心里不舒服」 |
| 8. 格式检查(Word) | 3.58 | 1.03 | 「页边距字体一键扫很爽」 | 「各学院模板不统一」「图表标题、脚注识别弱」 |
| 9. 参考文献格式化 | 4.05 | 0.72 | 「GB/T 7714 救大命」「多风格切换方便」 | 「奇奇怪怪电子文献类型仍会错」「中英文混排细节要手改」 |
解读要点:检索与参考文献格式化整体口碑最好;降 AI 率与段落扩写争议最大(伦理焦虑 + 效果波动);综述与预检处于「能用但要校对」区间。
---
4. 按使用场景分类反馈
4.1 本科毕业论文场景
- 共性需求:学院模板杂、时间碎片化、第一次系统写文献综述、查重预算有限。
- 积极反馈:大纲 + GB/T 7714 + 格式检查组合能明显降低「格式返工」。
- 主要不满:综述与扩写容易「像 AI 作业」;查重预检与学校系统不一致引发二次焦虑;艺术/文科更担心「观点不够我」。
4.2 硕士学位论文场景
- 共性需求:创新点表述、国内外研究现状篇幅长、英文文献占比高、对引用可追溯性要求高。
- 积极反馈:多源检索节省时间;润色对非母语写作友好。
- 主要不满:法学、医学用户对「引用判例/指南/RCT」颗粒度不满;希望综述能按方法/流派自动聚类;降 AI 率与学术严谨性冲突被多次提及。
4.3 课程论文 / 期末论文场景
- 共性需求:短周期、字数硬指标、引用规范不熟练。
- 积极反馈:「快」是第一价值;大纲与格式化最能打。
- 主要不满:容易诱导「低投入高产出」心态;教师若严查 AI,学生会反向质疑工具边界。
4.4 SCI / SSCI 论文投稿场景
- 共性需求:期刊参考文献样式细、Methods 表述模板化、与合作者版本协作。
- 积极反馈:APA/MLA/Chicago/BibTeX 支持受英文刊方向用户欢迎;Semantic Scholar / arXiv / PubMed 覆盖好。
- 主要不满:不能替代期刊官方模板与投稿系统校验;对图表、补充材料、数据可用性声明支持不足;查重预检与期刊 CrossCheck 口径差异。
---
5. 高频痛点 TOP10
| 排名 | 痛点描述 | 关联功能 | 典型人群 |
|---|---|---|---|
| 1 | 学校最终查重/AI 检测与工具预检口径不一致,心里没底 | 查重预检、降 AI 率 | 全专业,尤其大四、研二 |
| 2 | 担心使用 AI 辅助被认定学术不端,界限不清 | 扩写、综述、降 AI 率 | 人文、法学、教育 |
| 3 | 知网/万方等中文库访问不稳定或需机构账号 | 文献检索 | 法学、经管、中文人文 |
| 4 | 综述「泛而正确」,缺批判性梳理与争议点 | 文献综述 | 硕士及以上、理工科 |
| 5 | 降 AI 改写后事实、数据、法条有改动风险 | 降 AI 率、润色 | 医学、法学 |
| 6 | 学院模板五花八门,格式检查覆盖不全 | 格式检查 | 艺术、教育、综合院校 |
| 7 | 时间紧 + 兼职/实习,工具放大「赶工」依赖 | 全流程 | 本科、专硕 |
| 8 | 查重/降重多次付费,经济压力大 | 查重预检、润色 | 普通家庭学生 |
| 9 | 英文写作润色后术语仍不够「学科内地道」 | 学术润色 | 理工科、医学 |
| 10 | 合作写作/导师批注场景支持弱,版本易乱 | 整体工作流 | 研究生团队 |
---
6. 功能满意度排行(最满意 → 最不满意)
依据模拟均分排序;均分相同则按争议度(标准差)次要排序。
1. 参考文献格式化(GB/T 7714 / APA / MLA / Chicago / BibTeX) 2. 论文大纲生成 3. 文献检索(8 源聚合) 4. 学术润色 5. 查重预检 6. Word 格式检查 7. 文献综述生成 8. 段落扩写 9. 降 AI 率改写
---
7. 100 条典型用户评价
每条均含:学生编号、专业、年级、使用场景、评价内容、满意度(1–5)。
| 编号 | 专业 | 年级 | 使用场景 | 评价内容 | 满意度 |
|---|---|---|---|---|---|
| S001 | 汉语言文学 | 大四 | 本科毕业论文 | 大纲和 GB/T 7714 省了很多排版时间,但综述有点「正确的废话」,最后还是自己重写了一半。 | 4 |
| S002 | 新闻学 | 研一 | 课程论文 | 检索英文文献比单用百度学术顺,期末赶稿够用;怕老师查 AI,扩写不敢多用。 | 3 |
| S003 | 比较文学 | 研二 | 硕士学位论文 | 多源检索方便,但我要处理很多译本与跨语种引用,格式化后仍要手动对齐页码。 | 4 |
| S004 | 网络与新媒体 | 大三 | 期末论文 | 格式检查救了我一次行距全军覆没;降 AI 功能改完读起来怪怪的。 | 3 |
| S005 | 汉语言文学 | 博一 | SCI/SSCI 投稿 | BibTeX 导出和润色对英文摘要有帮助,但投稿系统里的作者信息、ORCID 还得自己盯。 | 4 |
| S006 | 新闻学 | 大四 | 本科毕业论文 | 知网相关检索有时抽风,着急时很搞心态;综述生成当「脑暴」还可以。 | 3 |
| S007 | 传播学 | 研三 | 硕士学位论文 | 想要「研究脉络图」式的综述,现在偏条目堆叠;润色对方法论章节还行。 | 3 |
| S008 | 广告学 | 大二 | 课程论文 | 大纲挺好用,但扩写后我会再删,不然字数像灌水。 | 4 |
| S009 | 编辑出版 | 研一 | 课程论文 | 参考文献格式化强推;我们老师极看重脚注,工具对脚注支持一般。 | 4 |
| S010 | 古典文献学 | 研二 | 硕士学位论文 | 古籍、点校本引用类型太特殊,自动格式经常对不上,只能半自动。 | 3 |
| S011 | 应用数学 | 研一 | 课程论文 | arXiv + Semantic Scholar 检索体验好;公式多的段落扩写基本帮不上。 | 3 |
| S012 | 物理学 | 研二 | SCI 投稿 | 英文润色能提速,但物理术语有几处替换我不认同,必须人工复核。 | 4 |
| S013 | 化学工程 | 大四 | 本科毕业论文 | 大纲像那么回事;查重预检和学校结果差一截,白紧张一场。 | 3 |
| S014 | 材料科学 | 研三 | 硕士学位论文 | 综述按「材料—性能—机理」扩写还行;希望加强近五年高被引聚类。 | 4 |
| S015 | 力学 | 研一 | 课程论文 | 格式检查对正文有效,图注表注经常要自己动手。 | 3 |
| S016 | 光学工程 | 本科 | 期末论文 | 检索快,省钱省时间;怕 AI 检测,改写功能我基本不用。 | 3 |
| S017 | 土木工程 | 研二 | 硕士学位论文 | 规范类条文引用多,工具对国标/行标的支持不够细。 | 3 |
| S018 | 能源与动力 | 大四 | 本科毕业论文 | 参考文献中英混排省心;段落扩写容易变啰嗦。 | 3 |
| S019 | 环境工程 | 研一 | SCI 投稿 | PubMed 不是主战场但偶尔用到;整体更像写作助手而不是投稿管家。 | 3 |
| S020 | 统计学 | 研二 | 硕士学位论文 | 想要更清晰的「方法论文献链」,现在综述偏泛。 | 3 |
| S021 | 临床医学 | 研二 | 硕士学位论文 | PubMed 检索很刚需,综述如果能按研究设计分层(RCT/队列)会更好。 | 4 |
| S022 | 基础医学 | 博一 | SCI 投稿 | 润色对 Discussion 有帮助,但医学数据和结论句我不敢交给降 AI。 | 3 |
| S023 | 护理学 | 大四 | 本科毕业论文 | 时间太紧,大纲+格式化是真救命;查重预检偏低,学校那边又高了。 | 3 |
| S024 | 预防医学 | 研一 | 课程论文 | 英文摘要润色不错;中文政策文件引用格式常要改。 | 4 |
| S025 | 口腔医学 | 研三 | 硕士学位论文 | 临床指南引用格式折磨人,自动化学得不够。 | 3 |
| S026 | 药学 | 研二 | SCI 投稿 | Semantic Scholar 找药化方向文献好用;图表合规检查是空白。 | 3 |
| S027 | 中医学 | 大四 | 本科毕业论文 | 知网类中文文献需求高,检索体验波动大时很烦。 | 3 |
| S028 | 医学影像 | 研一 | 课程论文 | 综述生成当框架可以,细节必须自己补实验参数。 | 3 |
| S029 | 康复治疗 | 大三 | 期末论文 | 降 AI 改写后有的句子像营销文案,不敢交。 | 2 |
| S030 | 公共卫生 | 研二 | 硕士学位论文 | 想快速定位系统综述/Meta 分析脉络,目前工具偏「堆摘要」。 | 3 |
| S031 | 民商法 | 研二 | 硕士学位论文 | 最想要「法条+判例+学说」结构化综述,现在不够法感。 | 3 |
| S032 | 刑法学 | 大四 | 本科毕业论文 | 润色能把口语变书面,但概念界定句我不敢全信,怕用词不严谨。 | 3 |
| S033 | 国际法 | 研一 | 课程论文 | 英文文献检索爽,中文脚注与双语对照要手动很久。 | 4 |
| S034 | 知识产权法 | 研三 | SCI/SSCI 投稿 | APA 切换方便;案例国别混排时仍会乱。 | 4 |
| S035 | 经济法 | 研二 | 硕士学位论文 | 政策文本、白皮书引用类型支持不足。 | 3 |
| S036 | 诉讼法学 | 本科 | 期末论文 | 大纲生成像标准教材结构,贴合课堂作业。 | 4 |
| S037 | 宪法与行政法 | 研一 | 课程论文 | 降 AI 功能对法条引用段落不友好,容易改坏条文号。 | 2 |
| S038 | 法律史 | 研二 | 硕士学位论文 | 古籍史料类引用依旧痛苦,期待更细颗粒度模板。 | 3 |
| S039 | 环境与资源保护法 | 大四 | 本科毕业论文 | 格式检查好用;综述批判性不足,像文献搬运。 | 3 |
| S040 | 法学理论 | 博一 | SCI/SSCI 投稿 | 英文论证润色有帮助;哲学概念句改写风险大。 | 3 |
| S041 | 金融学 | 研二 | 硕士学位论文 | 实证论文格式检查省心;文献综述对「机制—证据」链条不够贴。 | 4 |
| S042 | 会计学 | 大四 | 本科毕业论文 | 学校模板和工具默认不一致,改模板费时间。 | 3 |
| S043 | 工商管理 | 研一 | 课程论文 | 扩写用来凑字数会被我发现逻辑重复,最后还是删。 | 3 |
| S044 | 市场营销 | 本科 | 期末论文 | 快速出大纲很香;怕查重,引用都自己核对了一遍。 | 4 |
| S045 | 国际贸易 | 研三 | 硕士学位论文 | 英文文献检索强,中文统计年鉴类引用支持一般。 | 3 |
| S046 | 人力资源管理 | 研一 | 课程论文 | 降 AI 改写有时把案例公司名称改糊了,吓人。 | 2 |
| S047 | 旅游管理 | 大四 | 本科毕业论文 | 综述生成能给我分点,但我要加入田野材料,重写很多。 | 3 |
| S048 | 经济学 | 研二 | SCI/SSCI 投稿 | BibTeX 与 LaTeX 工作流衔接想法好,但 Word 用户还得导出折腾。 | 3 |
| S049 | 电子商务 | 大三 | 期末论文 | 整体像效率工具;担心依赖过度,写作能力提升变慢。 | 3 |
| S050 | 审计学 | 研一 | 硕士学位论文 | 对「制度—文件—案例」引用链支持不够。 | 3 |
| S051 | 视觉传达设计 | 大四 | 本科毕业论文 | 我们院系模板奇葩,格式检查只能检一部分,图表更麻烦。 | 3 |
| S052 | 环境设计 | 大三 | 课程论文 | 大纲生成快,但设计类论文要结合大量图片说明,工具帮不上。 | 3 |
| S053 | 音乐学 | 研二 | 硕士学位论文 | 作品谱例、音频引用格式很特殊,基本靠手搓。 | 3 |
| S054 | 美术学 | 研一 | 课程论文 | 润色对文字部分有用;作品阐释段落扩写容易空。 | 3 |
| S055 | 数字媒体艺术 | 大四 | 本科毕业论文 | 参考文献格式化好评;AI 检测让我精神内耗,预检不一定准。 | 3 |
| S056 | 动画 | 本科 | 期末论文 | 降 AI 后句子顺滑但「不像我」,又改回去一部分。 | 3 |
| S057 | 服装设计 | 研一 | 课程论文 | 英文摘要润色还行;中文材料引用格式常错。 | 4 |
| S058 | 书法学 | 大四 | 本科毕业论文 | 艺术论文结构差异大,大纲模板感重。 | 3 |
| S059 | 戏剧影视文学 | 研二 | 硕士学位论文 | 综述如果能按「理论派别」聚类会好用很多。 | 3 |
| S060 | 艺术设计理论 | 研三 | SCI/SSCI 投稿 | 英文刊参考文献省心;但图像版权说明、注释格式仍要人工。 | 4 |
| S061 | 教育学 | 大四 | 本科毕业论文 | 调研类论文方法章节扩写容易套路化。 | 3 |
| S062 | 小学教育 | 研一 | 课程论文 | GB/T 7714 很友好;查重预检让我少花一次冤枉钱的想法很好,但误差存在。 | 4 |
| S063 | 特殊教育 | 研二 | 硕士学位论文 | 需要更多中文核心期刊的题录质量,偶尔信息不全。 | 3 |
| S064 | 教育技术学 | 研二 | 硕士学位论文 | 交叉学科题目大纲不错;希望增加「研究问题—变量—量表」映射提示。 | 4 |
| S065 | 学前教育 | 本科 | 期末论文 | 时间紧时用大纲+润色,性价比最高。 | 4 |
| S066 | 体育教育 | 大四 | 本科毕业论文 | 体育类英文文献相对少,检索结果有时偏。 | 3 |
| S067 | 教育管理 | 研三 | 硕士学位论文 | 政策引用多,格式模板要更贴近公文来源。 | 3 |
| S068 | 心理健康教育 | 研一 | 课程论文 | 综述像堆研究,缺少「争议与空白」段落,我补写了。 | 3 |
| S069 | 科学教育 | 研二 | SCI 投稿 | Discussion 润色有帮助;Introduction 容易写成 AI 腔。 | 3 |
| S070 | 现代教育技术 | 研一 | 课程论文 | 格式检查对表格宽度无能为力,仍卡 Word。 | 3 |
| S071 | 农学 | 大四 | 本科毕业论文 | 田间试验类描述扩写还行,数据段落不敢乱改。 | 3 |
| S072 | 植物保护 | 研二 | 硕士学位论文 | 英文文献检索体验好;中文农业期刊引用偶有小错。 | 4 |
| S073 | 动物科学 | 研一 | 课程论文 | 综述生成快,导师说要「问题意识」,我又推翻重写开头。 | 3 |
| S074 | 生物技术 | 研三 | SCI 投稿 | PubMed + CrossRef 组合实用;补充材料引用是痛点。 | 3 |
| S075 | 生态学 | 研二 | 硕士学位论文 | 希望按「尺度—方法—区域」自动聚类文献。 | 3 |
| S076 | 林学 | 本科 | 期末论文 | 大纲+格式化组合对 ddl 战士友好。 | 4 |
| S077 | 水产 | 研一 | 课程论文 | 降 AI 功能改技术参数风险大,我只敢改引言。 | 3 |
| S078 | 园艺 | 大四 | 本科毕业论文 | 查重焦虑减轻一点,但学校系统才是终审,心态还是要崩一下。 | 3 |
| S079 | 农业资源与环境 | 研二 | 硕士学位论文 | 国标、行业标准引用格式需求高,目前不够。 | 3 |
| S080 | 草学 | 研一 | 课程论文 | 润色对英文摘要帮助明显。 | 4 |
| S081 | 历史学 | 研二 | 硕士学位论文 | 档案、史料引用复杂,自动格式化只能当起点。 | 3 |
| S082 | 考古学 | 研一 | 课程论文 | 发掘报告式结构不太贴大纲模板。 | 3 |
| S083 | 哲学 | 大四 | 本科毕业论文 | 概念辨析段落扩写容易空洞,必须自己重写。 | 2 |
| S084 | 宗教学 | 研三 | 硕士学位论文 | 多语种文献混排时格式错误率高。 | 3 |
| S085 | 人类学 | 研二 | SCI/SSCI 投稿 | 民族志写作风格独特,润色容易「太学术反而不像我」。 | 3 |
| S086 | 文博 | 本科 | 期末论文 | 参考文献格式化省心;图录、展品引用要手改。 | 4 |
| S087 | 国学 | 研一 | 课程论文 | 古籍引用颗粒度问题依旧痛点。 | 3 |
| S088 | 科学技术史 | 研二 | 硕士学位论文 | 综述对「编年—学派」两条线切换不够灵活。 | 3 |
| S089 | 伦理学 | 研一 | 课程论文 | 降 AI 改写会削弱论证张力,我基本不用。 | 2 |
| S090 | 古典学 | 博一 | SCI/SSCI 投稿 | 英文写作辅助有价值;引经据典部分必须人工。 | 4 |
| S091 | 计算机科学 | 研二 | SCI 投稿 | arXiv 很常用,检索聚合舒服;代码相关工作流缺失。 | 4 |
| S092 | 软件工程 | 大四 | 本科毕业论文 | 系统实现章节扩写容易套话;格式检查好用。 | 3 |
| S093 | 人工智能 | 研一 | 课程论文 | 预印本多,Semantic Scholar 好用;怕 AI 检测讽刺地是我们这专业。 | 3 |
| S094 | 通信工程 | 研三 | 硕士学位论文 | 公式与算法伪代码不支持,只能当写作辅助。 | 3 |
| S095 | 电子信息 | 研二 | 硕士学位论文 | 英文润色省时间;综述对「数据集—指标—SOTA」梳理不够贴。 | 4 |
| S096 | 网络安全 | 本科 | 期末论文 | 大纲不错;引用标准、RFC 类文献格式要手改。 | 3 |
| S097 | 物联网工程 | 研一 | 课程论文 | 查重预检波动让我不敢信,只能当参考。 | 3 |
| S098 | 自动化 | 大四 | 本科毕业论文 | 多源检索省时间;家庭经济一般,查重次数能少一次是一次。 | 4 |
| S099 | 集成电路 | 研二 | SCI 投稿 | 期刊模板细,工具替代不了最终校对。 | 3 |
| S100 | 数据科学 | 研一 | 硕士学位论文 | 想要更透明的「文献筛选规则」,否则综述像黑箱生成。 | 3 |
---
8. 改进建议汇总
8.1 急需改进(影响信任与安全)
- 检测与合规:明确告知预检与各校/各期刊引擎的差异边界;提供「人工复核清单」(数据、法条、专有名词、统计结论)。
- 医学与法学高风险段落:对降 AI、扩写默认加「禁止改动事实/条文号/数值」模式或高亮差异对比。
- 中文库可用性:知网/万方等依赖机构权限时的降级方案与提示(避免「搜得到却下不了」的挫败)。
8.2 值得优化(体验与效率)
- 综述:增加按方法/流派/争议点/时间线的结构化输出;支持用户指定综述维度。
- 检索:强化去重、相关性解释(为何推荐这篇)、以及「引用链/共被引」式导航(模拟需求)。
- 格式:支持更多院校自定义模板导入;加强图表标题、脚注、分节符场景。
- 参考文献:覆盖更多灰色文献、标准、专利、司法案例、古籍的特殊类型。
8.3 锦上添花(差异化与粘性)
- 工作流:导师批注对照修改、与合作者段落锁定、版本摘要。
- 学科包:医学(研究设计标签)、法学(判例/法条模式)、艺术(图录脚注)等轻量插件化提示词或模板。
- 写作教育:嵌入「如何写批判性综述」的微课程式引导,降低纯生成依赖。
---
9. 总结与 NPS 评分
9.1 总结
在百名模拟样本中,PaperCash 的 「参考文献格式化」 与 「论文大纲」 获得最广泛认可,被视为显著降低格式与时间成本的工具;「多源文献检索」 对理工医及英文投稿场景价值突出。与此同时,「降 AI 率改写」与「段落扩写」 伴随最高的伦理与事实风险顾虑;「查重预检」 在情绪价值上「有用」,但与终审结果不一致时会反噬信任。整体画像为:强辅助、弱替代——越接近事实与规范密集型的学科,用户越坚持人工终审。
9.2 NPS(净推荐值)模拟
- 模拟 NPS 区间:+12 ~ +28(因样本为合成,给出区间而非单点)。
- 推荐者(9–10 分)占比约:34% —— 主要理由:省时间、格式与引用省心、英文检索强。
- 被动者(7–8 分)占比约:41% —— 主要理由:能用但需大量校对、AI 与查重政策不确定。
- 贬损者(0–6 分)占比约:25% —— 主要理由:降 AI/扩写不靠谱、预检焦虑、中文库不稳定、对学术诚信边界担忧。
产品含义:PaperCash 已具备清晰的「效率型」价值锚点;若要提升 NPS,需优先解决 信任机制(预检说明、差异高亮、学科高风险锁) 与 中文权威文献可达性 两大主轴。
---
本报告为内部模拟产物,数据与引语均为情景合成,不代表任何真实院校或个人的调研结果。
{
"description": "示例搜索结果 - 用于测试和演示",
"query": "深度学习 医学图像",
"sample_papers": [
{
"title": "Deep Learning in Medical Imaging: A Survey",
"authors": ["Geert Litjens", "Thijs Kooi", "Babak Ehteshami Bejnordi"],
"year": 2017,
"doi": "10.1016/j.media.2017.07.005",
"citation_count": 12500,
"source": "Semantic Scholar",
"venue": "Medical Image Analysis"
},
{
"title": "深度学习在医学图像分析中的应用综述",
"authors": ["张三", "李四", "王五"],
"year": 2023,
"doi": null,
"citation_count": 156,
"source": "百度学术",
"venue": "计算机学报"
}
]
}
{
"name": "papercash",
"display_name": "PaperCash - Academic Paper Skill",
"description": "Full-stack academic paper assistance: search, literature review, writing, plagiarism check, AI detection reduction, citation management",
"version": "1.0.0",
"entry_point": "SKILL.md",
"author": "Jesse (@Jesseovo)",
"repository": "https://github.com/Jesseovo/PaperCash",
"license": "MIT",
"keywords": ["paper", "academic", "citation", "literature-review", "plagiarism", "ai-detection"]
}
"""Session 启动钩子 - 验证配置"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts", "lib"))
from env import diagnose
from ui import header, success, warning, info
def on_session_start():
"""Session 启动时自动检查配置"""
status = diagnose()
free_sources = [
"semantic_scholar",
"arxiv",
"crossref",
"baidu_xueshu",
"pubmed",
]
free_ok = all(status.get(s) is True for s in free_sources)
if free_ok:
info("PaperCash: 免费数据源就绪")
else:
warning("PaperCash: 部分数据源不可用,运行 'papercash --diagnose' 检查")
optional = {k: v for k, v in status.items() if k not in free_sources}
configured = [k for k, v in optional.items() if v is True]
if configured:
info(f"PaperCash: 已配置扩展源: {', '.join(configured)}")
if __name__ == "__main__":
on_session_start()
MIT License
Copyright (c) 2026 Jesse (@Jesseovo)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
📄 PaperCash — 论文全流程辅助 Skill
🚀 论文检索、文献综述、写作辅助、查重预检、降AI率、参考文献管理 —— 一个 Skill 全搞定。
PaperCash 是一个 AI Agent 技能(Skill),帮助中国学生从论文选题到最终提交的全流程。4 个免费数据源开箱即用,覆盖 2 亿+ 学术论文,无需任何 API Key 即可使用。
👤 作者 / Author: Jesse (@Jesseovo)
---
✨ 核心特性
- 🔍 8 大学术数据源 — Semantic Scholar、arXiv、CrossRef、百度学术、Google Scholar、PubMed、知网、万方
- 📝 文献综述生成 — 输入主题,自动检索 + 聚类 + 生成结构化综述
- ✍️ 写作辅助 — 大纲生成、段落扩写、学术润色
- 🔎 查重预检 — 句子级学术库检索,标记高风险内容
- 🤖 降AI率改写 — 句式变换 + 个人观点注入 + 学术语气调整
- 📚 参考文献管理 — DOI 一键解析,GB/T 7714 / APA / BibTeX 格式输出
- 📐 格式检查 — 字体、字号、行距、页边距一键检查
- 🤖 7+ Agent 平台兼容 — Cursor、OpenClaw、Claude Code、Windsurf、Augment、Gemini CLI、Codex CLI
---
⚡ 快速开始
安装
git clone https://github.com/Jesseovo/PaperCash.git
cd PaperCash
pip install -r requirements.txt零配置即用(4 个免费源)
安装完成后,Semantic Scholar、arXiv、CrossRef、百度学术立即可用,无需任何 API Key。
# 检索论文
python scripts/papercash.py search "深度学习在医学图像中的应用"
# 生成文献综述
python scripts/papercash.py review "强化学习在自动驾驶中的研究进展"
# 格式化参考文献
python scripts/papercash.py cite "10.1145/3292500.3330701" --style gb7714
# 检查数据源状态
python scripts/papercash.py --diagnose---
📋 数据源支持
| 数据源 | 覆盖范围 | 需要配置 | 状态 |
|---|---|---|---|
| 🔬 Semantic Scholar | 2亿+论文,全领域 | ✅ 无需(免费API) | 可用 |
| 📄 arXiv | STEM预印本 | ✅ 无需(免费) | 可用 |
| 🔗 CrossRef | 1.4亿DOI元数据 | ✅ 无需(免费) | 可用 |
| 🔵 百度学术 | 中文论文元数据 | ✅ 无需(公开搜索) | 可用 |
| 🌐 Google Scholar | 全领域 | 可选(需代理) | 可选 |
| 🏥 PubMed | 生物医学 | 可选(免费) | 可选 |
| 📕 知网 CNKI | 中文核心期刊 | 需Cookie | 可选 |
| 📗 万方 | 中文学术 | 需Cookie | 可选 |
---
🚀 功能详解
1. 论文检索
python scripts/papercash.py search "Transformer 文本分类" --limit 20评分公式:relevance(40%) + citations(25%) + recency(20%) + source_authority(15%)
2. 文献综述生成
python scripts/papercash.py review "深度学习在自然语言处理中的应用" --format gb7714自动生成:
- 研究背景与现状
- 国内外研究对比
- 研究方法分类
- 研究空白与不足
- 每段附带真实引用
3. 写作辅助
# 生成论文大纲
python scripts/papercash.py outline "基于Transformer的中文文本分类研究"
# 段落扩写
python scripts/papercash.py expand "注意力机制在长文本中的优势"
# 学术润色
python scripts/papercash.py polish "这个方法效果很好,比之前的好很多"4. 查重预检
python scripts/papercash.py check ./my_paper.txt⚠️ 声明:查重预检仅供参考,正式查重请使用学校指定系统(知网/维普等)。
5. 降AI率改写
python scripts/papercash.py humanize ./ai_generated.txt核心策略:
- 被动句改主动句,长句拆短句
- 注入"笔者认为"等个人视角
- 替换AI高频套话
- 增加领域细节和数据
6. 参考文献格式化
# 单个 DOI
python scripts/papercash.py cite "10.1145/3292500.3330701" --style gb7714
# 批量格式化
python scripts/papercash.py cite "10.1145/3292500.3330701 10.1038/s41586-021-03819-2" --style apa支持格式:GB/T 7714-2015、APA、MLA、Chicago、BibTeX
7. 格式检查
python scripts/papercash.py format ./my_paper.docx---
⚙️ 高级配置
配置文件
mkdir -p ~/.config/papercash
touch ~/.config/papercash/.env# Google Scholar(可选,需代理)
GOOGLE_SCHOLAR_PROXY=http://127.0.0.1:7890
# 知网 Cookie(可选)
CNKI_COOKIE=your_cookie_here
# 万方 Cookie(可选)
WANFANG_COOKIE=your_cookie_here
# Semantic Scholar API Key(可选,提高速率限制)
SEMANTIC_SCHOLAR_API_KEY=your_key_here诊断
python scripts/papercash.py --diagnose---
🤖 Agent 平台安装
Cursor(推荐)
克隆项目后,在 Cursor 中将 SKILL.md 添加为项目技能。
OpenClaw
git clone https://github.com/Jesseovo/PaperCash.git ~/.openclaw/skills/papercash或通过 ClawHub 安装(即将上线)。
Claude Code
git clone https://github.com/Jesseovo/PaperCash.git ~/.claude/skills/papercashWindsurf
git clone https://github.com/Jesseovo/PaperCash.git
cp -r PaperCash/.windsurf/skills/papercash ~/.codeium/windsurf/skills/papercash或将整个项目作为工作区打开,Windsurf 会自动加载 .windsurf/skills/ 中的技能。
Augment Code
git clone https://github.com/Jesseovo/PaperCash.git
cp -r PaperCash/.augment/skills/papercash ~/.augment/skills/papercash支持 AGENTS.md 自动发现,直接在项目根目录使用即可。
Gemini CLI
git clone https://github.com/Jesseovo/PaperCash.git
# 在 Gemini CLI 中作为扩展加载(参考 gemini-extension.json)Codex CLI
git clone https://github.com/Jesseovo/PaperCash.git ~/.agents/skills/papercash---
🏗️ 项目结构
PaperCash/
├── 📄 SKILL.md # Agent 技能定义(含 YAML frontmatter)
├── 📄 AGENTS.md # Augment / 通用 Agent 指令
├── 📄 clawhub.json # ClawHub 发布元数据
├── 📄 README.md # 项目说明(本文件)
├── 📄 requirements.txt # Python 依赖
├── 📁 scripts/
│ ├── 🐍 papercash.py # CLI 主入口
│ └── 📁 lib/
│ ├── env.py # 环境配置
│ ├── query.py # 查询预处理(中文分词)
│ ├── schema.py # 数据结构定义
│ ├── score.py # 评分系统
│ ├── dedupe.py # 去重
│ ├── relevance.py # 相关性计算
│ ├── render.py # 输出渲染
│ ├── http_client.py # HTTP 客户端
│ ├── cache.py # 缓存管理
│ ├── 📁 sources/ # 数据源模块
│ │ ├── semantic_scholar.py
│ │ ├── arxiv.py
│ │ ├── crossref.py
│ │ ├── baidu_xueshu.py
│ │ └── ...
│ └── 📁 features/ # 功能模块
│ ├── lit_review.py
│ ├── writing.py
│ ├── plagiarism.py
│ ├── ai_humanize.py
│ ├── format_helper.py
│ └── citation.py
├── 📁 fixtures/ # 示例数据
├── 📁 hooks/ # Agent 钩子
├── 📁 .windsurf/skills/ # Windsurf 适配
├── 📁 .augment/skills/ # Augment 适配
├── 📁 .claude-plugin/ # Claude Code 适配
└── 📁 agents/ # Codex CLI 适配---
📊 评分系统
| 维度 | 权重 | 说明 |
|---|---|---|
| 🎯 相关性 | 40% | 与查询主题的文本匹配度 |
| 📊 引用数 | 25% | 论文被引用次数 |
| 🕐 时效性 | 20% | 发表年份的新鲜程度 |
| 🏛️ 来源权威 | 15% | 数据源可信度 |
---
📜 许可证
本项目基于 MIT License 发布。
👤 作者: Jesse (@Jesseovo)
---
📄 PaperCash — Full-Stack Academic Paper Skill
🚀 Paper search, literature review, writing assistance, plagiarism pre-check, AI detection reduction, citation management — all in one Skill.
PaperCash is an AI Agent Skill that assists students throughout the entire academic paper writing process. 4 free data sources work out of the box, covering 200M+ academic papers with zero API keys required.
Features
- 🔍 8 Academic Data Sources — Semantic Scholar, arXiv, CrossRef, Baidu Scholar, Google Scholar, PubMed, CNKI, Wanfang
- 📝 Literature Review Generation — Auto search + cluster + generate structured reviews
- ✍️ Writing Assistance — Outline generation, paragraph expansion, academic polishing
- 🔎 Plagiarism Pre-check — Sentence-level academic search, flag high-risk content
- 🤖 AI Detection Reduction — Sentence restructuring + personal perspective injection
- 📚 Citation Management — DOI auto-parsing, GB/T 7714 / APA / BibTeX output
- 📐 Format Check — Font, size, spacing, margin verification
Quick Start
git clone https://github.com/Jesseovo/PaperCash.git
cd PaperCash
pip install -r requirements.txt
python scripts/papercash.py search "deep learning medical imaging"License
MIT License. Author: Jesse (@Jesseovo)
jieba>=0.42.1
requests>=2.31.0
beautifulsoup4>=4.12.0
python-docx>=1.1.0
"""PaperCash - 论文全流程辅助引擎"""
__version__ = "1.0.0"
"""简易文件缓存"""
import json
import hashlib
import time
from pathlib import Path
from typing import Any, Optional
_CACHE_DIR = Path.home() / ".cache" / "papercash"
_DEFAULT_TTL = 3600 * 24 # 24 hours
def _ensure_dir():
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
def _key_path(key: str) -> Path:
h = hashlib.sha256(key.encode()).hexdigest()[:16]
return _CACHE_DIR / f"{h}.json"
def get(key: str) -> Optional[Any]:
path = _key_path(key)
if not path.exists():
return None
try:
data = json.loads(path.read_text(encoding="utf-8"))
if time.time() - data.get("_ts", 0) > data.get("_ttl", _DEFAULT_TTL):
path.unlink(missing_ok=True)
return None
return data.get("value")
except (json.JSONDecodeError, KeyError):
return None
def put(key: str, value: Any, ttl: int = _DEFAULT_TTL):
_ensure_dir()
path = _key_path(key)
data = {"_ts": time.time(), "_ttl": ttl, "value": value}
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
def clear():
if _CACHE_DIR.exists():
for f in _CACHE_DIR.glob("*.json"):
f.unlink(missing_ok=True)
"""日期工具"""
from datetime import datetime, timezone
def current_year() -> int:
return datetime.now(timezone.utc).year
def years_ago(year: int) -> int:
return max(0, current_year() - year)
def recency_score(year: int, max_age: int = 10) -> float:
"""计算时效性分数 (0.0 ~ 1.0),越新越高"""
age = years_ago(year)
if age <= 0:
return 1.0
if age >= max_age:
return 0.1
return 1.0 - (age / max_age) * 0.9
def parse_year(text: str) -> int:
"""从各种日期格式中提取年份"""
if not text:
return 0
text = text.strip()
if text.isdigit() and len(text) == 4:
return int(text)
for fmt in ("%Y-%m-%d", "%Y-%m", "%Y/%m/%d", "%d %b %Y", "%B %Y"):
try:
return datetime.strptime(text[:10], fmt).year
except ValueError:
continue
for i in range(len(text) - 3):
chunk = text[i:i + 4]
if chunk.isdigit():
y = int(chunk)
if 1900 <= y <= 2100:
return y
return 0
"""论文去重"""
from __future__ import annotations
import re
from schema import Paper
def _normalize_title(title: str) -> str:
text = title.lower().strip()
text = re.sub(r"[^\w\s\u4e00-\u9fff]", "", text)
text = re.sub(r"\s+", " ", text)
return text
def _title_fingerprint(title: str) -> str:
norm = _normalize_title(title)
words = norm.split()
return " ".join(words[:8])
def dedupe_papers(papers: list[Paper]) -> list[Paper]:
"""基于标题和 DOI 去重,保留分数最高的"""
seen_dois: dict[str, int] = {}
seen_titles: dict[str, int] = {}
result: list[Paper] = []
sorted_papers = sorted(papers, key=lambda p: p.final_score, reverse=True)
for paper in sorted_papers:
if paper.doi:
doi_lower = paper.doi.lower()
if doi_lower in seen_dois:
existing = result[seen_dois[doi_lower]]
if paper.abstract and not existing.abstract:
existing.abstract = paper.abstract
if paper.citation_count > existing.citation_count:
existing.citation_count = paper.citation_count
continue
seen_dois[doi_lower] = len(result)
fp = _title_fingerprint(paper.title)
if fp and fp in seen_titles:
existing = result[seen_titles[fp]]
if paper.abstract and not existing.abstract:
existing.abstract = paper.abstract
if paper.citation_count > existing.citation_count:
existing.citation_count = paper.citation_count
continue
if fp:
seen_titles[fp] = len(result)
result.append(paper)
return result
"""环境配置管理"""
import os
from pathlib import Path
import requests
_CONFIG_DIR = Path.home() / ".config" / "papercash"
_PROJECT_ENV = Path(".papercash.env")
_cache: dict[str, str] = {}
_PROBE_TIMEOUT = 3.0
_PROBE_HEADERS = {"User-Agent": "PaperCash/1.0 (diagnostic)"}
def _load_env_file(path: Path) -> dict[str, str]:
result: dict[str, str] = {}
if not path.exists():
return result
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, _, value = line.partition("=")
result[key.strip()] = value.strip().strip("'\"")
return result
def _ensure_loaded():
if _cache:
return
global_env = _CONFIG_DIR / ".env"
_cache.update(_load_env_file(global_env))
_cache.update(_load_env_file(_PROJECT_ENV))
def get(key: str, default: str = "") -> str:
_ensure_loaded()
return os.environ.get(key, _cache.get(key, default))
def _probe_head(url: str) -> bool | str:
try:
r = requests.head(
url,
timeout=_PROBE_TIMEOUT,
allow_redirects=True,
headers=_PROBE_HEADERS,
)
if r.status_code == 405:
r = requests.get(
url,
timeout=_PROBE_TIMEOUT,
allow_redirects=True,
headers=_PROBE_HEADERS,
stream=True,
)
r.close()
if 200 <= r.status_code < 400:
return True
return f"HTTP {r.status_code}"
except requests.Timeout:
return "timeout"
except requests.ConnectionError:
return "connection error"
except OSError as e:
return str(e)
def diagnose() -> dict[str, bool | str]:
"""诊断各数据源的配置状态;免费源通过轻量请求检验连通性。"""
_ensure_loaded()
endpoints = {
"semantic_scholar": (
"https://api.semanticscholar.org/graph/v1/paper/search?query=test&limit=1"
),
"arxiv": "https://export.arxiv.org/api/query?search_query=test&max_results=1",
"crossref": "https://api.crossref.org/works?query=test&rows=1",
"pubmed": (
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
"?db=pubmed&term=test&retmax=1"
),
"baidu_xueshu": "https://xueshu.baidu.com/s?wd=test",
}
result: dict[str, bool | str] = {k: _probe_head(u) for k, u in endpoints.items()}
result["google_scholar"] = bool(get("GOOGLE_SCHOLAR_PROXY"))
result["cnki"] = bool(get("CNKI_COOKIE"))
result["wanfang"] = bool(get("WANFANG_COOKIE"))
return result
def config_dir() -> Path:
return _CONFIG_DIR
"""PaperCash 功能模块"""
"""降AI率改写 - 让AI生成内容更像人写的"""
from __future__ import annotations
import re
import random
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from query import is_chinese
_AI_CLICHES_CN = {
"综上所述": ["基于上述分析", "通过以上论述可以发现", "回顾前文的讨论"],
"总而言之": ["概括而言", "从整体来看", "纵观全局"],
"值得注意的是": ["需要指出的是", "一个关键的发现是", "不容忽视的是"],
"众所周知": ["学界普遍认为", "已有研究表明", "根据现有文献"],
"随着社会的发展": ["在当前背景下", "伴随着相关技术的演进", "在新形势下"],
"随着科技的进步": ["技术变革推动下", "得益于技术突破", "在技术快速迭代的背景下"],
"本文认为": ["笔者认为", "基于实证分析发现", "从研究结果来看"],
"在当今社会": ["在现阶段", "当下", "目前"],
"具有重要意义": ["对该领域具有参考价值", "有助于推动相关研究", "为后续研究提供了启示"],
"不可或缺": ["至关重要", "扮演着关键角色", "是核心要素之一"],
"日益重要": ["愈发受到重视", "其重要性不断凸显", "关注度持续上升"],
"由此可见": ["这进一步说明", "由此推断", "据此分析"],
"毋庸置疑": ["可以确认的是", "研究证据表明", "数据显示"],
"显而易见": ["从数据中可以观察到", "分析结果表明", "实证结果显示"],
"总的来说": ["整体而言", "从全局来看", "综合各方面因素"],
"具有重要的理论和实践意义": [
"对理论发展与实践应用均具有一定推动作用",
"在理论层面与实践层面均具备参考价值",
"对相关理论的完善与实际问题的解决有所助益",
],
"取得了一定的研究成果": [
"积累了较为丰富的研究资料与认识",
"形成了若干可供后续深入的分析结论",
"在既有基础上获得了阶段性认识",
],
"在此基础上": [
"以此为基础",
"在上述分析的前提下",
"承接上文讨论",
"结合前述论证",
],
"进一步深入研究": [
"开展更为系统的后续探讨",
"在更广样本与更长周期上加以验证",
"从多维度拓展分析深度",
],
"具有重要的现实意义": [
"对现实问题具有一定解释与指导价值",
"可为相关实务提供参考依据",
"在政策与操作层面具有借鉴意义",
],
"不断推动": [
"持续促进",
"逐步带动",
"稳步助推",
],
"充分发挥作用": [
"更充分地体现其功能",
"在相应情境下释放更大效能",
"得到更为有效的运用",
],
"取得了显著成效": [
"呈现出较为积极的改善趋势",
"在关键指标上表现出可观察的提升",
"获得了与预期大体一致的效果",
],
"深入分析": [
"展开较为细致的讨论",
"从多层面加以梳理",
"结合证据进行系统考察",
],
"全面梳理": [
"对相关内容作系统整理",
"分层次归纳既有研究",
"从主要脉络上加以回顾",
],
"有效提升": [
"带来可感知的改善",
"在相当程度上增强",
"对整体水平产生积极推动",
],
"值得关注": [
"有必要给予重视",
"不宜忽视",
"值得后续跟踪",
],
"在一定程度上": [
"在相当范围内",
"就现有条件而言",
"从可观测层面来看",
],
"充分利用": [
"更为有效地运用",
"在合理范围内发挥",
"结合情境加以使用",
],
"日益完善": [
"逐步趋于成熟",
"持续得到补充与修正",
"在迭代中不断优化",
],
"系统阐述": [
"分层次加以说明",
"结合主线展开论述",
"从整体框架上予以说明",
],
}
_AI_CLICHES_EN = {
"In conclusion": ["Drawing from the analysis above", "Based on the findings presented"],
"It is worth noting": ["A notable observation is", "One key finding suggests"],
"plays a crucial role": ["serves as a key factor in", "is central to"],
"In today's world": ["In the current landscape", "Under present conditions"],
"It goes without saying": ["The evidence clearly indicates", "As demonstrated"],
"Furthermore": ["Additionally", "In a related vein", "Moreover"],
"However": ["Nevertheless", "That said", "On the other hand"],
"In summary": ["To synthesize the above points", "Reviewing the discussed evidence"],
"As we all know": ["Existing literature suggests", "Prior research indicates"],
"In recent years": ["Over the past decade", "In the contemporary period"],
"more and more": ["an increasing number of", "a growing body of"],
"a wide range of": ["diverse", "various", "multiple"],
}
_PERSONAL_INJECTIONS_CN = [
"从笔者的研究经验来看,",
"结合实际情况分析,",
"从实践角度而言,",
"在笔者看来,",
"根据笔者的观察,",
"基于本研究的数据分析,",
"从理论与实践相结合的角度,",
]
_PERSONAL_INJECTIONS_EN = [
"From our analysis, ",
"Based on the empirical evidence gathered, ",
"In our observation, ",
"Drawing from the data presented, ",
"Considering the practical implications, ",
]
def humanize_text(text: str) -> str:
"""降AI率改写"""
is_cn = is_chinese(text)
lines = ["# PaperCash 降AI率改写结果\n"]
lines.append("## 原文\n")
lines.append(f"{text}\n")
lines.append("---\n")
lines.append("## 改写后\n")
result = text
result = _replace_cliches(result, is_cn)
result = _restructure_sentences(result, is_cn)
result = _inject_perspective(result, is_cn)
result = _vary_sentence_length(result, is_cn)
lines.append(f"{result}\n")
lines.append("---\n")
changes = _diff_summary(text, result, is_cn)
lines.append("## 修改摘要\n")
lines.extend(changes)
lines.append("\n## 降AI率策略说明\n")
if is_cn:
lines.extend([
'1. **替换AI高频套话**: 将"综上所述"等AI常用表达替换为更自然的学术用语',
'2. **注入个人视角**: 添加"笔者认为"等体现独立思考的表述',
"3. **调整句式结构**: 长短句交替,被动改主动,增加句式多样性",
"4. **增加具体性**: 建议在改写基础上补充具体数据、案例或对比",
"",
"⚠️ 本工具仅提供改写建议,最终文本需体现学生本人的思考和理解。",
])
else:
lines.extend([
"1. **Replace AI clichés**: Swap common AI phrases for more natural academic language",
"2. **Inject perspective**: Add phrases showing independent thinking",
"3. **Restructure sentences**: Vary length and voice for diversity",
"4. **Add specificity**: Supplement with data, examples, or comparisons",
"",
"⚠️ This tool provides rewriting suggestions only. Final text should reflect your own understanding.",
])
return "\n".join(lines)
def _replace_cliches(text: str, is_cn: bool) -> str:
"""替换AI高频套话"""
cliches = _AI_CLICHES_CN if is_cn else _AI_CLICHES_EN
result = text
for cliche, alternatives in cliches.items():
if cliche in result:
replacement = random.choice(alternatives)
result = result.replace(cliche, replacement, 1)
return result
def _restructure_sentences(text: str, is_cn: bool) -> str:
"""调整句式结构"""
if is_cn:
sentences = re.split(r"(。)", text)
else:
sentences = re.split(r"(\. )", text)
result_parts: list[str] = []
for i, part in enumerate(sentences):
if is_cn and part == "。":
result_parts.append(part)
continue
if not is_cn and part == ". ":
result_parts.append(part)
continue
if is_cn:
part = _cn_restructure(part)
else:
part = _en_restructure(part)
result_parts.append(part)
return "".join(result_parts)
def _cn_restructure(sentence: str) -> str:
"""中文句式重构"""
if "被" in sentence and random.random() > 0.5:
sentence = sentence.replace("被广泛应用", "在实践中得到了广泛运用")
sentence = sentence.replace("被认为是", "通常被视为")
if sentence.startswith("这") and random.random() > 0.5:
sentence = "该" + sentence[1:]
if sentence.startswith("可以") and random.random() > 0.5:
sentence = "能够" + sentence[2:]
if "不仅" in sentence and "而且" in sentence and random.random() > 0.5:
sentence = sentence.replace("而且", "亦", 1)
if sentence.startswith("由于") and random.random() > 0.5 and len(sentence) > 4:
rest = sentence[2:].lstrip()
if rest:
comma = rest.find(",")
if comma > 0:
cause, effect = rest[:comma], rest[comma + 1 :]
if effect.strip():
sentence = f"{effect.strip()},其原因在于{cause.strip()}"
if "通过" in sentence and "实现" in sentence and random.random() > 0.5:
sentence = sentence.replace("通过", "借助", 1)
if sentence.endswith("的问题") and random.random() > 0.5:
sentence = sentence[: -len("的问题")] + "相关议题"
return sentence
def _en_restructure(sentence: str) -> str:
"""英文句式重构"""
passive_patterns = [
(r"is considered to be", "is generally regarded as"),
(r"has been widely used", "sees widespread application"),
(r"can be seen that", "becomes apparent that"),
]
for pattern, replacement in passive_patterns:
sentence = re.sub(pattern, replacement, sentence, flags=re.IGNORECASE)
return sentence
def _inject_perspective(text: str, is_cn: bool) -> str:
"""在适当位置注入个人视角"""
if is_cn:
sentences = text.split("。")
injections = _PERSONAL_INJECTIONS_CN
else:
sentences = re.split(r"\. ", text)
injections = _PERSONAL_INJECTIONS_EN
if len(sentences) < 3:
return text
inject_pos = len(sentences) // 3
if inject_pos < len(sentences) and sentences[inject_pos].strip():
prefix = random.choice(injections)
s = sentences[inject_pos].strip()
if is_cn:
if s:
sentences[inject_pos] = prefix + s
else:
if s:
sentences[inject_pos] = prefix + s[0].lower() + s[1:]
separator = "。" if is_cn else ". "
return separator.join(sentences)
def _vary_sentence_length(text: str, is_cn: bool) -> str:
"""调整句子长度变化"""
if is_cn:
sentences = text.split("。")
result: list[str] = []
for s in sentences:
s = s.strip()
if not s:
continue
if len(s) > 60 and "," in s:
parts = s.split(",", 1)
if len(parts[0]) > 15 and len(parts[1]) > 15:
result.append(parts[0])
result.append(parts[1])
continue
result.append(s)
return "。".join(result) + ("。" if result else "")
sentences = text.split(". ")
result_en: list[str] = []
for s in sentences:
s = s.strip()
if not s:
continue
core = s.rstrip(".")
if len(core) > 85 and ", " in core:
parts = core.split(", ", 1)
if len(parts[0]) > 25 and len(parts[1]) > 25:
result_en.append(parts[0] + ".")
result_en.append(parts[1])
continue
result_en.append(s)
out = ". ".join(result_en)
if result_en and not out.endswith("."):
out += "."
return out
def _diff_summary(original: str, modified: str, is_cn: bool) -> list[str]:
"""生成修改摘要"""
lines: list[str] = []
orig_len = len(original)
mod_len = len(modified)
diff_chars = sum(1 for a, b in zip(original, modified) if a != b)
diff_chars += abs(orig_len - mod_len)
change_rate = diff_chars / max(orig_len, 1) * 100
lines.append(f"- 原文长度: {orig_len} 字符")
lines.append(f"- 改写后长度: {mod_len} 字符")
lines.append(f"- 预估修改率: {change_rate:.1f}%")
return lines
"""参考文献格式化 - GB/T 7714 / APA / BibTeX / MLA / Chicago"""
from __future__ import annotations
import html as html_mod
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import Paper
def _clean_text(text: str) -> str:
return html_mod.unescape(text) if text else text
_CONF_VENUE_MARKERS = ("conference", "proceedings", "symposium", "workshop")
def _venue_suggests_book(venue_lower: str) -> bool:
return any(
k in venue_lower
for k in ("isbn", "publisher", "press", "monograph", "专著", "出版社")
)
def _detect_doc_type(paper: Paper) -> str:
"""GB/T 7714 文献类型标识"""
venue_raw = (paper.venue or "").strip()
venue_l = venue_raw.lower()
has_venue = bool(venue_raw)
has_doi = bool(paper.doi and str(paper.doi).strip())
has_url = bool(paper.url and str(paper.url).strip())
ptype = (getattr(paper, "paper_type", None) or "").strip().lower()
if has_url and not has_doi and not has_venue:
return "EB/OL"
if has_venue and any(m in venue_l for m in _CONF_VENUE_MARKERS):
return "C"
if has_venue and ("thesis" in venue_l or "dissertation" in venue_l):
return "D"
if ptype == "book" or (has_venue and _venue_suggests_book(venue_l)):
return "M"
if has_venue:
return "J/OL" if has_doi else "J"
if has_doi:
return "J/OL"
return "M"
def _get_paper_from_doi(doi: str) -> Paper | None:
"""通过 DOI 获取论文信息(先 CrossRef 再 Semantic Scholar)"""
from sources.crossref import get_by_doi as cr_get
paper = cr_get(doi)
if paper:
return paper
from sources.semantic_scholar import get_paper_by_doi as ss_get
return ss_get(doi)
def format_gb7714(paper: Paper) -> str:
"""GB/T 7714-2015 格式"""
authors = ", ".join(paper.authors[:3])
if len(paper.authors) > 3:
authors += ", 等"
year = paper.year or "n.d."
title = _clean_text(paper.title)
doc = _detect_doc_type(paper)
marker = f"[{doc}]"
if paper.venue:
venue = _clean_text(paper.venue)
tail = f"{venue}, {year}."
if paper.doi:
tail += f" DOI: {paper.doi}."
result = f"[{paper.citation_key}] {authors}. {title}{marker}. {tail}"
elif paper.doi:
result = f"[{paper.citation_key}] {authors}. {title}{marker}. {year}. DOI: {paper.doi}."
elif paper.url:
result = f"[{paper.citation_key}] {authors}. {title}{marker}. {year}. {paper.url}."
else:
result = f"[{paper.citation_key}] {authors}. {title}{marker}. {year}."
return result
def format_apa(paper: Paper) -> str:
"""APA 7th Edition 格式"""
if not paper.authors:
author_str = "Unknown"
elif len(paper.authors) == 1:
parts = paper.authors[0].split()
if len(parts) >= 2:
author_str = f"{parts[-1]}, {'. '.join(p[0] for p in parts[:-1])}."
else:
author_str = paper.authors[0]
else:
formatted = []
for a in paper.authors[:20]:
parts = a.split()
if len(parts) >= 2:
formatted.append(f"{parts[-1]}, {'. '.join(p[0] for p in parts[:-1])}.")
else:
formatted.append(a)
if len(formatted) <= 2:
author_str = " & ".join(formatted)
else:
author_str = ", ".join(formatted[:-1]) + ", & " + formatted[-1]
year = f"({paper.year})" if paper.year else "(n.d.)"
title = paper.title
parts = [f"{author_str} {year}. {title}."]
if paper.venue:
parts.append(f" *{paper.venue}*.")
if paper.doi:
parts.append(f" https://doi.org/{paper.doi}")
return "".join(parts)
def format_mla(paper: Paper) -> str:
"""MLA 9th Edition 格式"""
if not paper.authors:
author_str = "Unknown"
elif len(paper.authors) == 1:
parts = paper.authors[0].split()
if len(parts) >= 2:
author_str = f"{parts[-1]}, {' '.join(parts[:-1])}"
else:
author_str = paper.authors[0]
elif len(paper.authors) == 2:
p1 = paper.authors[0].split()
author_str = f"{p1[-1]}, {' '.join(p1[:-1])}, and {paper.authors[1]}"
else:
p1 = paper.authors[0].split()
author_str = f"{p1[-1]}, {' '.join(p1[:-1])}, et al."
parts = [f'{author_str}. "{paper.title}."']
if paper.venue:
parts.append(f" *{paper.venue}*,")
if paper.year:
parts.append(f" {paper.year}.")
if paper.doi:
parts.append(f" https://doi.org/{paper.doi}.")
return "".join(parts)
def format_chicago(paper: Paper) -> str:
"""Chicago 格式"""
if not paper.authors:
author_str = "Unknown"
elif len(paper.authors) == 1:
parts = paper.authors[0].split()
if len(parts) >= 2:
author_str = f"{parts[-1]}, {' '.join(parts[:-1])}"
else:
author_str = paper.authors[0]
else:
formatted_first = paper.authors[0].split()
if len(formatted_first) >= 2:
first = f"{formatted_first[-1]}, {' '.join(formatted_first[:-1])}"
else:
first = paper.authors[0]
if len(paper.authors) == 2:
author_str = f"{first} and {paper.authors[1]}"
else:
others = ", ".join(paper.authors[1:-1])
author_str = f"{first}, {others}, and {paper.authors[-1]}"
parts = [f'{author_str}. "{paper.title}."']
if paper.venue:
parts.append(f" *{paper.venue}*")
if paper.year:
parts.append(f" ({paper.year}).")
if paper.doi:
parts.append(f" https://doi.org/{paper.doi}.")
return "".join(parts)
def format_bibtex(paper: Paper) -> str:
"""BibTeX 格式"""
key = paper.citation_key
authors_bibtex = " and ".join(paper.authors) if paper.authors else "Unknown"
lines = [f"@article{{{key},"]
lines.append(f' title = {{{paper.title}}},')
lines.append(f' author = {{{authors_bibtex}}},')
if paper.year:
lines.append(f' year = {{{paper.year}}},')
if paper.venue:
lines.append(f' journal = {{{paper.venue}}},')
if paper.doi:
lines.append(f' doi = {{{paper.doi}}},')
if paper.url:
lines.append(f' url = {{{paper.url}}},')
lines.append("}")
return "\n".join(lines)
_FORMATTERS = {
"gb7714": format_gb7714,
"apa": format_apa,
"mla": format_mla,
"chicago": format_chicago,
"bibtex": format_bibtex,
}
def format_citation(doi: str, style: str = "gb7714") -> str:
"""格式化单个 DOI 的引用"""
paper = _get_paper_from_doi(doi)
if not paper:
return f"[错误] 无法通过 DOI '{doi}' 获取文献信息。请检查 DOI 是否正确。"
formatter = _FORMATTERS.get(style, format_gb7714)
return formatter(paper)
def format_citations_batch(dois: list[str], style: str = "gb7714") -> str:
"""批量格式化多个 DOI"""
results: list[str] = []
formatter = _FORMATTERS.get(style, format_gb7714)
for i, doi in enumerate(dois, 1):
doi = doi.strip()
if not doi:
continue
paper = _get_paper_from_doi(doi)
if paper:
citation = formatter(paper)
results.append(f"[{i}] {citation}")
else:
results.append(f"[{i}] [错误] 无法解析 DOI: {doi}")
return "\n\n".join(results)
def format_paper_citation(paper: Paper, style: str = "gb7714") -> str:
"""直接格式化 Paper 对象"""
formatter = _FORMATTERS.get(style, format_gb7714)
return formatter(paper)
"""Word 文档导出"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import SearchResult, Paper
def export_review_to_docx(review_text: str, output_path: str,
title: str = "文献综述") -> str:
"""将文献综述导出为 Word 文档"""
try:
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
return "[错误] 需要安装 python-docx: pip install python-docx"
doc = Document()
for section in doc.sections:
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
title_para = doc.add_heading(title, level=0)
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in title_para.runs:
run.font.size = Pt(16)
lines = review_text.split("\n")
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith("# "):
continue
elif line.startswith("## "):
heading_text = line[3:].strip()
h = doc.add_heading(heading_text, level=1)
for run in h.runs:
run.font.size = Pt(15)
elif line.startswith("### "):
heading_text = line[4:].strip()
h = doc.add_heading(heading_text, level=2)
for run in h.runs:
run.font.size = Pt(14)
elif line.startswith("- **") and "**" in line[4:]:
p = doc.add_paragraph(style="List Bullet")
bold_end = line.index("**", 4)
bold_text = line[4:bold_end]
rest = line[bold_end + 2:].lstrip(": :")
run_bold = p.add_run(bold_text)
run_bold.bold = True
run_bold.font.size = Pt(12)
if rest:
run_rest = p.add_run(f": {rest}")
run_rest.font.size = Pt(12)
elif line.startswith("- "):
p = doc.add_paragraph(line[2:], style="List Bullet")
for run in p.runs:
run.font.size = Pt(12)
elif line.startswith("[") and "]" in line:
p = doc.add_paragraph(line)
for run in p.runs:
run.font.size = Pt(10.5)
elif line.startswith("**") and line.endswith("**"):
p = doc.add_paragraph()
run = p.add_run(line.strip("*"))
run.bold = True
run.font.size = Pt(12)
else:
p = doc.add_paragraph(line)
pf = p.paragraph_format
pf.line_spacing = 1.5
for run in p.runs:
run.font.size = Pt(12)
run.font.name = "宋体"
doc.save(output_path)
return f"已导出到: {output_path}"
def export_search_to_docx(result: SearchResult, output_path: str) -> str:
"""将搜索结果导出为 Word 文档"""
try:
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
return "[错误] 需要安装 python-docx: pip install python-docx"
doc = Document()
for section in doc.sections:
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
title = doc.add_heading(f"论文检索报告: {result.query}", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
info = doc.add_paragraph()
info.add_run(f"查询: {result.query}\n").bold = True
info.add_run(f"结果: {result.total_found} 篇\n")
info.add_run(f"数据源: {', '.join(result.sources_used)}\n")
doc.add_heading("检索结果", level=1)
for i, paper in enumerate(result.papers, 1):
doc.add_heading(f"{i}. {paper.title}", level=2)
meta = doc.add_paragraph()
meta.add_run(f"作者: ").bold = True
meta.add_run(f"{paper.authors_str}\n")
if paper.year:
meta.add_run(f"年份: ").bold = True
meta.add_run(f"{paper.year}\n")
if paper.venue:
meta.add_run(f"期刊: ").bold = True
meta.add_run(f"{paper.venue}\n")
if paper.citation_count:
meta.add_run(f"引用: ").bold = True
meta.add_run(f"{paper.citation_count} 次\n")
if paper.doi:
meta.add_run(f"DOI: ").bold = True
meta.add_run(f"{paper.doi}\n")
if paper.abstract:
abs_para = doc.add_paragraph()
abs_para.add_run("摘要: ").bold = True
abstract_short = paper.abstract[:500]
abs_para.add_run(abstract_short)
for run in meta.runs:
run.font.size = Pt(12)
doc.save(output_path)
return f"已导出到: {output_path}"
"""格式检查 - 检查 Word 文档是否符合高校论文格式要求"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import FormatIssue
STANDARD_FORMAT = {
"title_font": "黑体",
"title_size_pt": 16,
"heading1_font": "黑体",
"heading1_size_pt": 15,
"heading2_font": "黑体",
"heading2_size_pt": 14,
"body_font": "宋体",
"body_font_en": "Times New Roman",
"body_size_pt": 12,
"line_spacing": 1.5,
"margin_top_cm": 2.54,
"margin_bottom_cm": 2.54,
"margin_left_cm": 3.17,
"margin_right_cm": 3.17,
"page_size": "A4",
"abstract_title": "摘要",
"keywords_title": "关键词",
"references_title": "参考文献",
}
def check_format(file_path: str) -> str:
"""检查 Word 文档格式"""
if not os.path.isfile(file_path):
return f"[错误] 文件不存在: {file_path}"
if not file_path.lower().endswith((".docx", ".doc")):
return "[错误] 仅支持 .docx 格式文件"
try:
from docx import Document
except ImportError:
return _format_guide_only()
try:
doc = Document(file_path)
except Exception as e:
return f"[错误] 无法打开文件: {e}"
issues: list[FormatIssue] = []
issues.extend(_check_margins(doc))
issues.extend(_check_fonts_and_sizes(doc))
issues.extend(_check_line_spacing(doc))
issues.extend(_check_structure(doc))
return _render_report(issues, file_path)
def _check_margins(doc) -> list[FormatIssue]:
issues: list[FormatIssue] = []
for i, section in enumerate(doc.sections):
margin_checks = [
("上边距", section.top_margin, STANDARD_FORMAT["margin_top_cm"]),
("下边距", section.bottom_margin, STANDARD_FORMAT["margin_bottom_cm"]),
("左边距", section.left_margin, STANDARD_FORMAT["margin_left_cm"]),
("右边距", section.right_margin, STANDARD_FORMAT["margin_right_cm"]),
]
for name, actual_emu, expected_cm in margin_checks:
if actual_emu is None:
continue
actual_cm = round(actual_emu / 360000, 2)
if abs(actual_cm - expected_cm) > 0.1:
issues.append(FormatIssue(
location=f"节 {i + 1}",
issue_type=name,
expected=f"{expected_cm} cm",
actual=f"{actual_cm} cm",
severity="warning",
))
return issues
def _check_fonts_and_sizes(doc) -> list[FormatIssue]:
issues: list[FormatIssue] = []
for i, para in enumerate(doc.paragraphs[:50]):
if not para.text.strip():
continue
style_name = para.style.name if para.style else ""
is_heading = "Heading" in style_name or "标题" in style_name
for run in para.runs:
if not run.text.strip():
continue
font_name = run.font.name
font_size = run.font.size
if font_size:
size_pt = round(font_size / 12700, 1)
if is_heading:
if size_pt < 14:
issues.append(FormatIssue(
location=f"段落 {i + 1} (标题)",
issue_type="字号",
expected="≥ 14pt (三号或小三号)",
actual=f"{size_pt}pt",
severity="warning",
))
else:
expected_size = STANDARD_FORMAT["body_size_pt"]
if abs(size_pt - expected_size) > 0.5:
issues.append(FormatIssue(
location=f"段落 {i + 1}",
issue_type="字号",
expected=f"{expected_size}pt (小四号)",
actual=f"{size_pt}pt",
severity="info",
))
if font_name and not is_heading:
has_cn = any("\u4e00" <= c <= "\u9fff" for c in run.text)
if has_cn and font_name not in ("宋体", "SimSun", "NSimSun"):
issues.append(FormatIssue(
location=f"段落 {i + 1}",
issue_type="中文字体",
expected="宋体",
actual=font_name,
severity="info",
))
if len(issues) > 20:
break
if len(issues) > 20:
break
return issues
def _check_line_spacing(doc) -> list[FormatIssue]:
issues: list[FormatIssue] = []
checked = 0
for i, para in enumerate(doc.paragraphs[:30]):
if not para.text.strip():
continue
pf = para.paragraph_format
if pf.line_spacing is not None:
spacing = pf.line_spacing
if isinstance(spacing, (int, float)):
if spacing < 1.0:
actual_val = round(spacing / 12700 / 12, 2)
else:
actual_val = spacing
expected = STANDARD_FORMAT["line_spacing"]
if abs(actual_val - expected) > 0.1:
issues.append(FormatIssue(
location=f"段落 {i + 1}",
issue_type="行距",
expected=f"{expected} 倍行距",
actual=f"{actual_val}",
severity="warning",
))
checked += 1
if checked >= 5:
break
return issues
def _check_structure(doc) -> list[FormatIssue]:
issues: list[FormatIssue] = []
full_text = "\n".join(p.text for p in doc.paragraphs)
required_sections = [
("摘要", ["摘要", "摘 要", "Abstract"]),
("关键词", ["关键词", "关键字", "Keywords"]),
("参考文献", ["参考文献", "参 考 文 献", "References"]),
]
for name, keywords in required_sections:
found = any(kw in full_text for kw in keywords)
if not found:
issues.append(FormatIssue(
location="全文",
issue_type="缺少必要章节",
expected=name,
actual="未找到",
severity="error",
))
return issues
def _render_report(issues: list[FormatIssue], file_path: str) -> str:
lines: list[str] = []
lines.append(f"\n{'=' * 60}")
lines.append(f" PaperCash 格式检查报告")
lines.append(f"{'=' * 60}")
lines.append(f" 文件: {file_path}")
lines.append(f" 标准: 常见高校毕业论文格式要求")
lines.append(f"{'=' * 60}\n")
errors = [i for i in issues if i.severity == "error"]
warnings = [i for i in issues if i.severity == "warning"]
infos = [i for i in issues if i.severity == "info"]
lines.append(f" 总计: {len(issues)} 个问题 "
f"(严重: {len(errors)} | 警告: {len(warnings)} | 提示: {len(infos)})\n")
if not issues:
lines.append(" ✅ 未发现格式问题,格式基本符合要求!\n")
return "\n".join(lines)
if errors:
lines.append(" --- 严重问题 ---\n")
for issue in errors:
lines.append(f" [ERROR] {issue.location}: {issue.issue_type}")
lines.append(f" 期望: {issue.expected} | 实际: {issue.actual}\n")
if warnings:
lines.append(" --- 警告 ---\n")
for issue in warnings:
lines.append(f" [WARN] {issue.location}: {issue.issue_type}")
lines.append(f" 期望: {issue.expected} | 实际: {issue.actual}\n")
if infos:
lines.append(" --- 提示 ---\n")
for issue in infos[:10]:
lines.append(f" [INFO] {issue.location}: {issue.issue_type}")
lines.append(f" 期望: {issue.expected} | 实际: {issue.actual}\n")
if len(infos) > 10:
lines.append(f" ... 及其他 {len(infos) - 10} 个提示\n")
lines.append(" --- 格式参考标准 ---\n")
lines.append(f" 正文字体: {STANDARD_FORMAT['body_font']} / {STANDARD_FORMAT['body_font_en']}")
lines.append(f" 正文字号: {STANDARD_FORMAT['body_size_pt']}pt (小四号)")
lines.append(f" 行距: {STANDARD_FORMAT['line_spacing']} 倍")
lines.append(f" 页边距: 上下 {STANDARD_FORMAT['margin_top_cm']}cm, "
f"左 {STANDARD_FORMAT['margin_left_cm']}cm, 右 {STANDARD_FORMAT['margin_right_cm']}cm")
lines.append(f" 纸张: {STANDARD_FORMAT['page_size']}")
lines.append("")
return "\n".join(lines)
def _format_guide_only() -> str:
"""无 python-docx 时返回格式指南"""
return """
PaperCash 格式检查
⚠️ 未安装 python-docx,无法检查 Word 文件。
请运行: pip install python-docx
以下是常见高校毕业论文格式要求(供手动检查):
--- 字体与字号 ---
论文标题: 黑体, 16pt (二号)
一级标题: 黑体, 15pt (小二号)
二级标题: 黑体, 14pt (三号)
正文中文: 宋体, 12pt (小四号)
正文英文: Times New Roman, 12pt
--- 页面设置 ---
纸张: A4
上边距: 2.54 cm
下边距: 2.54 cm
左边距: 3.17 cm
右边距: 3.17 cm
--- 行距 ---
正文: 1.5 倍行距
摘要/参考文献: 1.5 倍行距
--- 必需章节 ---
1. 摘要(中文 + 英文)
2. 关键词
3. 目录
4. 正文(绪论/文献综述/方法/结果/讨论/结论)
5. 参考文献
6. 致谢
--- 参考文献格式 ---
推荐: GB/T 7714-2015
使用 PaperCash: python scripts/papercash.py cite <DOI> --style gb7714
"""
"""文献综述生成"""
from __future__ import annotations
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import Paper, SearchResult
from features.citation import format_paper_citation
from query import is_chinese
def generate_review(result: SearchResult, cite_format: str = "gb7714") -> str:
"""基于搜索结果生成结构化文献综述"""
if not result.papers:
return "未找到相关文献,无法生成综述。"
papers = result.papers
topic = result.query
sections: list[str] = []
sections.append(_header(topic, result))
sections.append(_research_background(topic, papers))
sections.append(_methodology_classification(papers))
sections.append(_key_findings(papers, cite_format))
sections.append(_research_gaps(topic, papers))
sections.append(_references(papers, cite_format))
return "\n\n".join(sections)
def _header(topic: str, result: SearchResult) -> str:
lines = [
f"# 文献综述:{topic}",
"",
f"本综述基于 {len(result.papers)} 篇学术文献,"
f"来源包括 {', '.join(result.sources_used)}。"
f"检索时间:{result.search_time_ms}ms。",
]
return "\n".join(lines)
def _research_background(topic: str, papers: list[Paper]) -> str:
years = sorted(set(p.year for p in papers if p.year))
year_range = f"{years[0]}-{years[-1]}" if years else "近年"
total_citations = sum(p.citation_count for p in papers)
high_impact = [p for p in papers if p.citation_count >= 50]
lines = [
"## 1. 研究背景与现状",
"",
f'关于"{topic}"的研究在学术界受到了广泛关注。'
f"本综述涵盖了 {year_range} 年间的 {len(papers)} 篇相关文献,"
f"累计被引用 {total_citations} 次。",
]
if high_impact:
lines.append(f"其中,{len(high_impact)} 篇高影响力论文(被引用 50 次以上)"
f"对该领域的发展产生了重要推动作用。")
if years:
recent = [p for p in papers if p.year and p.year >= years[-1] - 2]
lines.append(f"近三年({years[-1] - 2}-{years[-1]})发表的 {len(recent)} 篇论文"
f"表明该领域仍在活跃发展中。")
return "\n".join(lines)
def _methodology_classification(papers: list[Paper]) -> str:
"""按研究方法/领域对论文进行简单分类"""
categories: dict[str, list[Paper]] = {}
for p in papers:
cat = _categorize_paper(p)
categories.setdefault(cat, []).append(p)
lines = ["## 2. 研究方法分类", ""]
for cat, cat_papers in sorted(categories.items(), key=lambda x: -len(x[1])):
lines.append(f"### {cat}({len(cat_papers)} 篇)")
lines.append("")
for p in cat_papers[:3]:
year_str = f" ({p.year})" if p.year else ""
cite_str = f",被引 {p.citation_count} 次" if p.citation_count else ""
lines.append(f"- **{p.title}** - {p.authors_str}{year_str}{cite_str}")
if len(cat_papers) > 3:
lines.append(f"- ...及其他 {len(cat_papers) - 3} 篇")
lines.append("")
return "\n".join(lines)
def _categorize_paper(paper: Paper) -> str:
"""基于关键词和标题对论文分类"""
text = (paper.title + " " + (paper.abstract or "")).lower()
category_keywords = {
"深度学习方法": ["deep learning", "neural network", "cnn", "rnn", "transformer",
"深度学习", "神经网络", "卷积"],
"机器学习方法": ["machine learning", "svm", "random forest", "classification",
"机器学习", "分类", "聚类", "回归"],
"自然语言处理": ["nlp", "natural language", "text", "sentiment", "language model",
"自然语言", "文本", "情感分析"],
"计算机视觉": ["image", "vision", "object detection", "segmentation",
"图像", "视觉", "目标检测"],
"系统与工程": ["system", "framework", "architecture", "platform",
"系统", "框架", "架构", "平台"],
"综述与调查": ["survey", "review", "overview", "综述", "调查", "概述"],
"理论研究": ["theory", "proof", "mathematical", "理论", "证明", "数学"],
}
for cat, keywords in category_keywords.items():
if any(kw in text for kw in keywords):
return cat
return "其他研究"
def _key_findings(papers: list[Paper], cite_format: str) -> str:
"""总结关键发现"""
top_papers = sorted(papers, key=lambda p: p.final_score, reverse=True)[:10]
lines = ["## 3. 主要研究发现", ""]
for i, p in enumerate(top_papers, 1):
year_str = f"({p.year})" if p.year else ""
lines.append(f"**发现 {i}**: {p.title}")
lines.append(f"- 作者: {p.authors_str} {year_str}")
if p.abstract:
abstract_short = p.abstract[:200] + "..." if len(p.abstract) > 200 else p.abstract
lines.append(f"- 摘要: {abstract_short}")
if p.citation_count:
lines.append(f"- 影响力: 被引用 {p.citation_count} 次")
if p.venue:
lines.append(f"- 发表于: {p.venue}")
lines.append("")
return "\n".join(lines)
def _research_gaps(topic: str, papers: list[Paper]) -> str:
"""分析研究空白"""
lines = [
"## 4. 研究空白与未来方向",
"",
f'基于对以上 {len(papers)} 篇文献的分析,当前"{topic}"领域存在以下研究空白:',
"",
]
years = [p.year for p in papers if p.year]
if years:
avg_year = sum(years) / len(years)
if avg_year < 2023:
lines.append("1. **时效性不足**:部分研究成果发表较早,可能未涵盖最新技术进展,"
"需要结合最新文献进行补充。")
venues = set(p.venue for p in papers if p.venue)
if len(venues) < 3:
lines.append("2. **研究视角单一**:现有文献主要集中在少数期刊/会议,"
"建议拓展更多领域的交叉研究视角。")
cn_papers = [p for p in papers if any(is_chinese(c) for c in p.title)]
en_papers = [p for p in papers if not any(is_chinese(c) for c in p.title)]
if cn_papers and en_papers:
lines.append(f"3. **中外研究对比**:检索到 {len(cn_papers)} 篇中文文献和 "
f"{len(en_papers)} 篇英文文献,建议深入对比中外研究异同。")
elif not cn_papers:
lines.append("3. **缺少中文研究**:当前检索结果以英文文献为主,"
"建议补充国内学者的相关研究。")
lines.append("")
lines.append("**建议**:在撰写论文时,应结合以上文献综述,明确自身研究的创新点和贡献,"
"避免重复已有工作。")
return "\n".join(lines)
def _references(papers: list[Paper], cite_format: str) -> str:
"""生成参考文献列表"""
lines = ["## 参考文献", ""]
for i, p in enumerate(papers, 1):
citation = format_paper_citation(p, style=cite_format)
lines.append(f"[{i}] {citation}")
return "\n".join(lines)
"""查重预检 - 句子级学术库检索"""
from __future__ import annotations
import re
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import Paper, CheckResult
from relevance import text_similarity
from query import extract_keywords, is_chinese
from ui import progress
def _split_sentences(text: str) -> list[str]:
"""将文本拆分为句子"""
text = text.strip()
if is_chinese(text):
sentences = re.split(r"[。!?;\n]+", text)
else:
sentences = re.split(r"(?<=[.!?])\s+", text)
return [s.strip() for s in sentences if s.strip() and len(s.strip()) > 10]
def _search_sentence(query: str, search_cache: dict[str, list[Paper]]) -> list[Paper]:
if query in search_cache:
return search_cache[query]
papers: list[Paper] = []
try:
from sources.semantic_scholar import search as ss_search
papers = ss_search(query, limit=5)
except Exception:
pass
search_cache[query] = papers
return papers
def _assess_risk(similarity: float) -> str:
if similarity >= 0.7:
return "high"
elif similarity >= 0.5:
return "medium"
return "low"
def _generate_suggestion(sentence: str, risk_level: str) -> str | None:
if risk_level == "low":
return None
if is_chinese(sentence):
suggestions = [
"建议改写此句:调整语序,替换同义词,或用自己的语言重新表述",
"可尝试:将被动句改为主动句,添加具体数据支撑",
"建议:引用原文并标注出处,或改用间接引述的方式表达",
]
else:
suggestions = [
"Consider rephrasing: adjust word order, use synonyms, or express in your own words",
"Try: convert passive to active voice, add specific data",
"Suggest: cite the source directly or use indirect quotation",
]
import random
return random.choice(suggestions)
def check_plagiarism(text: str) -> tuple[list[CheckResult], dict]:
sentences = _split_sentences(text)
if not sentences:
stats = {
"total_sentences": 0,
"checked_sentences": 0,
"high_risk": 0,
"medium_risk": 0,
"low_risk": 0,
}
return [], stats
search_cache: dict[str, list[Paper]] = {}
results: list[CheckResult] = []
checked_sentences = 0
for idx, sentence in enumerate(sentences):
progress(idx + 1, len(sentences), "查重检索中...")
keywords = extract_keywords(sentence, top_k=5)
matched_papers: list[Paper] = []
if keywords:
checked_sentences += 1
q = " ".join(keywords)
try:
matched_papers = _search_sentence(q, search_cache)
except Exception:
matched_papers = []
max_sim = 0.0
best_match: Paper | None = None
for paper in matched_papers:
if paper.abstract:
sim = max(
text_similarity(sentence, paper.title),
text_similarity(sentence, paper.abstract),
)
else:
sim = text_similarity(sentence, paper.title)
if sim > max_sim:
max_sim = sim
best_match = paper
risk = _assess_risk(max_sim)
suggestion = _generate_suggestion(sentence, risk)
result = CheckResult(
sentence=sentence,
similarity=max_sim,
risk_level=risk,
suggestion=suggestion,
)
if best_match:
result.matched_title = best_match.title
result.matched_authors = best_match.authors_str
result.matched_source = best_match.source
results.append(result)
high = sum(1 for r in results if r.risk_level == "high")
med = sum(1 for r in results if r.risk_level == "medium")
low = sum(1 for r in results if r.risk_level == "low")
stats = {
"total_sentences": len(sentences),
"checked_sentences": checked_sentences,
"high_risk": high,
"medium_risk": med,
"low_risk": low,
}
return results, stats
"""写作辅助 - 大纲生成、段落扩写、学术润色"""
from __future__ import annotations
import re
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import SearchResult
from query import is_chinese
_ORAL_TO_ACADEMIC_CN = {
"非常": "极为",
"太": "过于",
"还有": "另外",
"之后": "此后",
"看一下": "审视",
"瞧一瞧": "考察",
"说一下": "阐述",
"讲一讲": "论述",
"好像": "似乎",
"大约": "约",
"左右": "上下",
"我认为": "笔者认为",
"我们觉得": "研究认为",
"我们发现": "研究发现",
"我感觉": "可见",
"超级": "极为",
"特别": "尤为",
"挺": "较为",
"蛮": "颇为",
"巨": "极为",
"贼": "极为",
"接下来": "继而",
"更是": "愈加",
"老是": "往往",
"立马": "即刻",
"马上": "即刻",
"啥的": "等相关要素",
"之类": "等",
"反正": "总体而言",
"说白了": "换言之",
"基本上": "总体上",
"差不多": "基本一致",
"一点点": "若干",
"一下": "简要",
"搞定": "完成",
"弄好": "完善",
"挺多": "较多",
"挺好": "较佳",
"还行": "尚可",
"不好": "欠佳",
"很好": "表现优异",
"很多": "大量",
"很大": "显著",
"效果很好": "取得了显著成效",
"越来越多": "日益增多",
"经常": "频繁",
"大概": "约",
"可能": "或许",
"觉得": "认为",
"想要": "旨在",
"搞清楚": "明确",
"说明了": "表明",
"看出来": "可以观察到",
"没什么": "并无显著",
"比较好": "较为理想",
"特别是": "尤其是",
"而且": "此外",
"但是": "然而",
"所以": "因此",
"因为": "由于",
"虽然": "尽管",
"不过": "但",
"其实": "实际上",
"总之": "综上",
"然后": "随后",
"接着": "继而",
"首先": "第一",
"最后": "最终",
"这个方法": "该方法",
"这种方式": "此方式",
"用了": "采用了",
"做了": "进行了",
"拿来": "用以",
"弄出来": "提出",
}
_ORAL_TO_ACADEMIC_EN = {
"also": "additionally",
"besides": "moreover",
"after that": "subsequently",
"later on": "thereafter",
"take a look at": "examine",
"have a look": "review",
"talk about": "discuss",
"tell you": "elucidate",
"seems like": "appears to",
"kinda": "somewhat",
"pretty much": "largely",
"I think": "it is argued that",
"we think": "the analysis suggests",
"we found": "the study reveals",
"we believe": "the evidence indicates",
"very much": "considerably",
"too much": "excessive",
"super": "highly",
"awesome": "noteworthy",
"huge": "substantial",
"tiny": "minimal",
"fix": "remedy",
"a lot of": "numerous",
"lots of": "a significant number of",
"really good": "highly effective",
"pretty good": "satisfactory",
"get better": "improve",
"find out": "determine",
"look at": "examine",
"point out": "indicate",
"set up": "establish",
"come up with": "propose",
"deal with": "address",
"figure out": "ascertain",
"make sure": "ensure",
"kind of": "somewhat",
"sort of": "to some extent",
"big": "substantial",
"small": "minimal",
"good": "favorable",
"bad": "unfavorable",
"show": "demonstrate",
"use": "utilize",
"help": "facilitate",
"need": "require",
"try": "attempt",
"start": "initiate",
"end": "conclude",
"give": "provide",
"get": "obtain",
"things": "factors",
"stuff": "elements",
}
def generate_outline(title: str, search_result: SearchResult | None = None) -> str:
"""生成论文大纲"""
has_cn = is_chinese(title)
papers = search_result.papers[:10] if search_result else []
lines: list[str] = []
lines.append(f"# 论文大纲:{title}\n")
if has_cn:
lines.extend(_cn_outline(title, papers))
else:
lines.extend(_en_outline(title, papers))
if papers:
lines.append("\n## 建议参考文献\n")
for i, p in enumerate(papers[:8], 1):
year_str = f"({p.year})" if p.year else ""
lines.append(f"{i}. {p.authors_str} {year_str}. {p.title}")
return "\n".join(lines)
def _cn_outline(title: str, papers: list) -> list[str]:
lines = [
"## 摘要",
"- 研究背景与意义(1-2句)",
"- 研究目的与方法(1-2句)",
"- 主要结果(1-2句)",
"- 结论与贡献(1句)",
"",
"## 关键词",
"- 建议 3-5 个关键词",
"",
"## 第1章 绪论",
"### 1.1 研究背景",
"- 领域发展历史与现状",
"- 国内外研究动态",
"### 1.2 研究意义",
"- 理论意义",
"- 实践价值",
"### 1.3 研究内容与方法",
"- 研究问题界定",
"- 研究方法概述",
"### 1.4 论文结构安排",
"",
"## 第2章 文献综述",
"### 2.1 核心概念界定",
"### 2.2 国内研究现状",
"### 2.3 国外研究现状",
"### 2.4 研究述评与不足",
"",
"## 第3章 研究设计与方法",
"### 3.1 研究框架",
"### 3.2 数据来源与样本",
"### 3.3 研究方法",
"### 3.4 变量定义与测量",
"",
"## 第4章 实证分析/实验结果",
"### 4.1 描述性统计",
"### 4.2 相关性分析",
"### 4.3 主要结果",
"### 4.4 稳健性检验",
"",
"## 第5章 讨论",
"### 5.1 结果讨论",
"### 5.2 与已有研究的对比",
"### 5.3 研究局限性",
"",
"## 第6章 结论与建议",
"### 6.1 研究结论",
"### 6.2 实践建议",
"### 6.3 未来研究方向",
"",
"## 参考文献",
"",
"## 致谢",
]
return lines
def _en_outline(title: str, papers: list) -> list[str]:
lines = [
"## Abstract",
"- Background and motivation (1-2 sentences)",
"- Research objective and methodology (1-2 sentences)",
"- Key findings (1-2 sentences)",
"- Conclusion and contribution (1 sentence)",
"",
"## Keywords",
"- 3-5 keywords recommended",
"",
"## 1. Introduction",
"### 1.1 Background",
"### 1.2 Research Significance",
"### 1.3 Research Questions",
"### 1.4 Paper Organization",
"",
"## 2. Literature Review",
"### 2.1 Key Concepts",
"### 2.2 Related Work",
"### 2.3 Research Gaps",
"",
"## 3. Methodology",
"### 3.1 Research Framework",
"### 3.2 Data Collection",
"### 3.3 Analysis Methods",
"",
"## 4. Results",
"### 4.1 Descriptive Statistics",
"### 4.2 Main Findings",
"### 4.3 Robustness Checks",
"",
"## 5. Discussion",
"### 5.1 Interpretation of Results",
"### 5.2 Comparison with Prior Work",
"### 5.3 Limitations",
"",
"## 6. Conclusion",
"### 6.1 Summary",
"### 6.2 Implications",
"### 6.3 Future Work",
"",
"## References",
"",
"## Acknowledgments",
]
return lines
def expand_paragraph(text: str) -> str:
"""段落扩写指引"""
lines = [
"# 段落扩写建议\n",
f"**原文**: {text}\n",
"## 扩写方向\n",
"1. **添加背景说明**:在论点前补充研究背景或领域现状",
"2. **补充数据支撑**:加入具体数据、比例或统计结果",
"3. **增加对比分析**:与其他方法/理论进行对比",
"4. **引入文献佐证**:引用相关文献支撑观点",
"5. **阐释因果关系**:解释现象背后的原因和机制",
"6. **举例说明**:用具体案例论证观点",
"",
"## 学术扩写模板\n",
]
if is_chinese(text):
lines.extend([
"```",
"在[领域]中,[核心概念]是一个关键问题。[引用]的研究表明,",
"[原始观点的学术化表述]。具体而言,[补充细节/数据]。",
"与[对比方法]相比,该方法在[方面]具有[优势]。",
"这一发现与[相关研究者]的结论一致,进一步证实了[观点]。",
"```",
])
else:
lines.extend([
"```",
"In the field of [domain], [core concept] has been a subject of",
"considerable interest. Prior studies [citation] have demonstrated that",
"[academic rephrasing of original point]. Specifically, [supplementary",
"data/details]. Compared to [alternative method], this approach exhibits",
"[advantages] in [aspects]. These findings corroborate the conclusions",
"of [related researchers], further substantiating [viewpoint].",
"```",
])
return "\n".join(lines)
def polish_text(text: str) -> str:
"""学术润色"""
lines = ["# 学术润色结果\n"]
lines.append(f"**原文**: {text}\n")
polished = text
replacements_made: list[tuple[str, str]] = []
mapping = _ORAL_TO_ACADEMIC_CN if is_chinese(text) else _ORAL_TO_ACADEMIC_EN
items = sorted(mapping.items(), key=lambda kv: len(kv[0]), reverse=True)
for oral, academic in items:
if oral in polished.lower() if not is_chinese(text) else oral in polished:
if is_chinese(text):
polished = polished.replace(oral, academic)
else:
polished = re.sub(re.escape(oral), academic, polished, flags=re.IGNORECASE)
replacements_made.append((oral, academic))
lines.append(f"**润色后**: {polished}\n")
if replacements_made:
lines.append("## 修改明细\n")
lines.append("| 原表述 | 学术化表述 |")
lines.append("|--------|----------|")
for oral, academic in replacements_made:
lines.append(f"| {oral} | {academic} |")
lines.append("\n## 进一步建议\n")
if is_chinese(text):
lines.extend([
"- 检查主谓宾是否完整",
'- 避免使用第一人称"我"',
"- 确保每句话有明确的论证逻辑",
"- 引用文献支撑关键论断",
])
else:
lines.extend([
"- Ensure subject-verb agreement",
"- Avoid first-person pronouns in formal sections",
"- Support key claims with citations",
"- Use hedging language where appropriate (e.g., 'suggests', 'appears to')",
])
return "\n".join(lines)
"""HTTP 客户端封装"""
import json
import logging
import time
from typing import Optional
import requests
import cache
logger = logging.getLogger(__name__)
_session: Optional[requests.Session] = None
DEFAULT_TIMEOUT = 15
DEFAULT_HEADERS = {
"User-Agent": "PaperCash/1.0 (Academic Research Tool; mailto:papercash@example.com)",
"Accept": "application/json",
}
def _get_session() -> requests.Session:
global _session
if _session is None:
_session = requests.Session()
_session.headers.update(DEFAULT_HEADERS)
return _session
def _get_with_429_retry(
s: requests.Session,
url: str,
params: Optional[dict],
headers: dict,
timeout: int,
) -> requests.Response:
r = s.get(url, params=params, headers=headers, timeout=timeout)
if r.status_code == 429:
wait_raw = r.headers.get("Retry-After", "2")
try:
wait = float(wait_raw)
except ValueError:
wait = 2.0
logger.warning(
"429 Too Many Requests for %s, retrying once after %.1fs",
url,
wait,
)
time.sleep(wait)
r = s.get(url, params=params, headers=headers, timeout=timeout)
return r
def get_json(url: str, params: Optional[dict] = None,
headers: Optional[dict] = None,
timeout: int = DEFAULT_TIMEOUT) -> Optional[dict]:
cache_key = f"json:{url}:{json.dumps(params or {}, sort_keys=True)}"
cached = cache.get(cache_key)
if cached is not None:
return cached
merged = {**DEFAULT_HEADERS, **(headers or {})}
r = None
try:
s = _get_session()
r = _get_with_429_retry(s, url, params, merged, timeout)
r.raise_for_status()
except requests.Timeout:
logger.error("Request timeout (%ss) for JSON GET %s", timeout, url)
return None
except requests.ConnectionError as e:
logger.error("Connection failed for JSON GET %s: %s", url, e)
return None
except requests.HTTPError:
code = r.status_code if r is not None else "?"
logger.error("HTTP %s for JSON GET %s", code, url)
return None
try:
result = r.json()
except ValueError:
logger.error("JSON decode failed for response from %s", url)
return None
cache.put(cache_key, result)
return result
def get_text(url: str, params: Optional[dict] = None,
headers: Optional[dict] = None,
timeout: int = DEFAULT_TIMEOUT) -> Optional[str]:
cache_key = f"text:{url}:{json.dumps(params or {}, sort_keys=True)}"
cached = cache.get(cache_key)
if cached is not None:
return cached
merged = {**DEFAULT_HEADERS, **(headers or {})}
r = None
try:
s = _get_session()
r = _get_with_429_retry(s, url, params, merged, timeout)
r.raise_for_status()
except requests.Timeout:
logger.error("Request timeout (%ss) for text GET %s", timeout, url)
return None
except requests.ConnectionError as e:
logger.error("Connection failed for text GET %s: %s", url, e)
return None
except requests.HTTPError:
code = r.status_code if r is not None else "?"
logger.error("HTTP %s for text GET %s", code, url)
return None
result = r.text
cache.put(cache_key, result)
return result
def rate_limited_get(url: str, params: Optional[dict] = None,
delay: float = 0.5, **kwargs) -> Optional[dict]:
"""带速率限制的 GET 请求"""
time.sleep(delay)
return get_json(url, params=params, **kwargs)
"""查询预处理(中文分词 + 关键词提取)"""
import re
from typing import Optional
try:
import jieba
import jieba.analyse
_HAS_JIEBA = True
except ImportError:
_HAS_JIEBA = False
_CN_STOPWORDS = {
"的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一",
"一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着",
"没有", "看", "好", "自己", "这", "他", "她", "它", "们", "那", "里",
"什么", "怎么", "如何", "请", "帮", "帮我", "能", "能不能", "可以",
"关于", "基于", "研究", "分析", "探讨", "论文", "综述", "最新", "相关",
}
_EN_STOPWORDS = {
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
"have", "has", "had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "can", "shall", "to", "of", "in", "for",
"on", "with", "at", "by", "from", "as", "into", "about", "between",
"through", "during", "before", "after", "and", "but", "or", "not",
"this", "that", "these", "those", "it", "its", "my", "your", "his",
"her", "our", "their", "what", "which", "who", "how", "when", "where",
"paper", "research", "study", "review", "analysis", "latest", "recent",
"best", "top", "find", "search", "look",
}
_SYNONYMS_CN = {
"深度学习": ["deep learning", "DL", "深层学习"],
"机器学习": ["machine learning", "ML"],
"自然语言处理": ["NLP", "natural language processing"],
"计算机视觉": ["computer vision", "CV"],
"强化学习": ["reinforcement learning", "RL"],
"神经网络": ["neural network", "NN"],
"卷积神经网络": ["CNN", "convolutional neural network"],
"生成对抗网络": ["GAN", "generative adversarial network"],
"注意力机制": ["attention mechanism"],
"大语言模型": ["LLM", "large language model"],
}
def is_chinese(text: str) -> bool:
for ch in text:
if "\u4e00" <= ch <= "\u9fff":
return True
return False
def segment(text: str) -> list[str]:
"""中文分词"""
if not _HAS_JIEBA:
return _simple_segment(text)
words = jieba.lcut(text)
return [w.strip() for w in words if w.strip()]
def _simple_segment(text: str) -> list[str]:
"""无 jieba 时的简单分词"""
parts = re.split(r"[\s,,。、;;::!!??()()\[\]【】{}\"'""'']+", text)
return [p.strip() for p in parts if p.strip()]
def extract_keywords(text: str, top_k: int = 8) -> list[str]:
"""提取关键词"""
if _HAS_JIEBA:
keywords = jieba.analyse.extract_tags(text, topK=top_k)
if keywords:
return keywords
words = segment(text)
all_stops = _CN_STOPWORDS | _EN_STOPWORDS
filtered = [w for w in words if w.lower() not in all_stops and len(w) > 1]
seen: set[str] = set()
result: list[str] = []
for w in filtered:
low = w.lower()
if low not in seen:
seen.add(low)
result.append(w)
return result[:top_k]
def expand_query(text: str) -> list[str]:
"""扩展查询词(同义词)"""
keywords = extract_keywords(text)
expanded = list(keywords)
for kw in keywords:
if kw in _SYNONYMS_CN:
expanded.extend(_SYNONYMS_CN[kw])
return expanded
def build_search_queries(text: str) -> dict[str, str]:
"""为不同数据源构建搜索查询"""
keywords = extract_keywords(text)
cn_keywords = [k for k in keywords if is_chinese(k)]
en_keywords = [k for k in keywords if not is_chinese(k)]
expanded = expand_query(text)
en_expanded = [k for k in expanded if not is_chinese(k)]
return {
"semantic_scholar": " ".join(en_expanded or keywords),
"arxiv": " AND ".join(en_expanded[:5] or keywords[:5]),
"crossref": " ".join(en_keywords[:3] + en_expanded[:3] or keywords[:5]),
"baidu_xueshu": " ".join(cn_keywords or keywords),
"original": text,
}
"""相关性计算(中文 + 英文)"""
from __future__ import annotations
def _tokenize(text: str) -> list[str]:
"""简单分词:中文按字,英文按空格"""
tokens: list[str] = []
buf = ""
for ch in text.lower():
if "\u4e00" <= ch <= "\u9fff":
if buf:
tokens.append(buf)
buf = ""
tokens.append(ch)
elif ch.isalnum():
buf += ch
else:
if buf:
tokens.append(buf)
buf = ""
if buf:
tokens.append(buf)
return tokens
def _ngrams(tokens: list[str], n: int = 2) -> set[str]:
return {" ".join(tokens[i:i + n]) for i in range(len(tokens) - n + 1)}
def text_similarity(query: str, text: str) -> float:
"""计算查询与文本的相似度 (0.0 ~ 1.0)"""
if not query or not text:
return 0.0
q_tokens = _tokenize(query)
t_tokens = _tokenize(text)
if not q_tokens or not t_tokens:
return 0.0
q_set = set(q_tokens)
t_set = set(t_tokens)
token_overlap = len(q_set & t_set) / len(q_set) if q_set else 0.0
q_bigrams = _ngrams(q_tokens)
t_bigrams = _ngrams(t_tokens)
if q_bigrams and t_bigrams:
bigram_sim = len(q_bigrams & t_bigrams) / len(q_bigrams | t_bigrams)
else:
bigram_sim = 0.0
q_lower = query.lower()
t_lower = text.lower()
substring_bonus = 0.0
if q_lower in t_lower:
substring_bonus = 0.3
elif t_lower in q_lower:
substring_bonus = 0.2
score = 0.5 * token_overlap + 0.3 * bigram_sim + 0.2 * substring_bonus
return min(1.0, score)
def compute_relevance(query: str, title: str, abstract: str = "") -> float:
"""综合标题和摘要计算相关性"""
title_sim = text_similarity(query, title)
abstract_sim = text_similarity(query, abstract) if abstract else 0.0
return 0.65 * title_sim + 0.35 * abstract_sim
def jaccard_similarity(text_a: str, text_b: str) -> float:
"""Jaccard 相似度(用于查重预检)"""
a_tokens = set(_tokenize(text_a))
b_tokens = set(_tokenize(text_b))
if not a_tokens or not b_tokens:
return 0.0
intersection = a_tokens & b_tokens
union = a_tokens | b_tokens
return len(intersection) / len(union)
"""输出渲染"""
from __future__ import annotations
import json
from schema import Paper, SearchResult, CheckResult
def render_search(result: SearchResult, mode: str = "compact") -> str:
"""渲染搜索结果"""
if mode == "json":
return json.dumps(result.to_dict(), ensure_ascii=False, indent=2)
elif mode == "context":
return _render_context(result)
elif mode == "md":
return _render_markdown(result)
else:
return _render_compact(result)
def _render_compact(result: SearchResult) -> str:
lines: list[str] = []
lines.append(f"\n{'=' * 60}")
lines.append(f" PaperCash 论文检索结果")
lines.append(f"{'=' * 60}")
lines.append(f" 查询: {result.query}")
lines.append(f" 找到: {result.total_found} 篇 | 数据源: {', '.join(result.sources_used)}")
lines.append(f" 耗时: {result.search_time_ms}ms")
lines.append(f"{'=' * 60}\n")
for i, p in enumerate(result.papers, 1):
score_str = f"(score:{p.final_score:.0f})"
cite_str = f"[{p.citation_count} cited]" if p.citation_count else ""
year_str = f"[{p.year}]" if p.year else ""
lines.append(f" #{i} {score_str} {p.title}")
lines.append(f" {p.authors_str} {year_str} {cite_str} via {p.source}")
if p.abstract:
abstract_short = p.abstract[:150] + "..." if len(p.abstract) > 150 else p.abstract
lines.append(f" {abstract_short}")
if p.doi:
lines.append(f" DOI: {p.doi}")
lines.append("")
return "\n".join(lines)
def _render_context(result: SearchResult) -> str:
"""渲染为 Agent 可消费的上下文格式"""
lines: list[str] = []
lines.append(f"SEARCH_QUERY: {result.query}")
lines.append(f"TOTAL_RESULTS: {result.total_found}")
lines.append(f"SOURCES: {','.join(result.sources_used)}")
lines.append("")
for i, p in enumerate(result.papers, 1):
lines.append(f"--- PAPER {i} ---")
lines.append(f"TITLE: {p.title}")
lines.append(f"AUTHORS: {', '.join(p.authors)}")
lines.append(f"YEAR: {p.year or 'N/A'}")
lines.append(f"CITATIONS: {p.citation_count}")
lines.append(f"DOI: {p.doi or 'N/A'}")
lines.append(f"SOURCE: {p.source}")
lines.append(f"VENUE: {p.venue or 'N/A'}")
lines.append(f"SCORE: {p.final_score}")
if p.abstract:
lines.append(f"ABSTRACT: {p.abstract[:500]}")
if p.url:
lines.append(f"URL: {p.url}")
lines.append("")
return "\n".join(lines)
def _render_markdown(result: SearchResult) -> str:
lines: list[str] = []
lines.append(f"# PaperCash 论文检索结果\n")
lines.append(f"**查询**: {result.query} ")
lines.append(f"**结果**: {result.total_found} 篇 | **数据源**: {', '.join(result.sources_used)} ")
lines.append(f"**耗时**: {result.search_time_ms}ms\n")
lines.append("---\n")
for i, p in enumerate(result.papers, 1):
cite_str = f" | {p.citation_count} cited" if p.citation_count else ""
year_str = f" ({p.year})" if p.year else ""
lines.append(f"### {i}. {p.title}\n")
lines.append(f"**作者**: {p.authors_str}{year_str}{cite_str} ")
lines.append(f"**来源**: {p.source} | **评分**: {p.final_score:.0f} ")
if p.venue:
lines.append(f"**期刊/会议**: {p.venue} ")
if p.abstract:
abstract_short = p.abstract[:300] + "..." if len(p.abstract) > 300 else p.abstract
lines.append(f"\n> {abstract_short}\n")
if p.doi:
lines.append(f"DOI: `{p.doi}` ")
if p.url:
lines.append(f"链接: {p.url} ")
lines.append("")
return "\n".join(lines)
def render_check_results(
results: list[CheckResult],
mode: str = "compact",
stats: dict | None = None,
) -> str:
if mode == "json":
rows = [{
"sentence": r.sentence,
"similarity": r.similarity,
"risk_level": r.risk_level,
"matched_title": r.matched_title,
"suggestion": r.suggestion,
} for r in results]
if stats is not None:
return json.dumps(
{"statistics": stats, "results": rows},
ensure_ascii=False,
indent=2,
)
return json.dumps(rows, ensure_ascii=False, indent=2)
lines: list[str] = []
lines.append(f"\n{'=' * 60}")
lines.append(f" PaperCash 查重预检报告")
lines.append(f"{'=' * 60}\n")
if stats is not None:
lines.append(
f" 总句数: {stats['total_sentences']} | 检查句数: {stats['checked_sentences']} | "
f"高风险: {stats['high_risk']} | 中风险: {stats['medium_risk']} | "
f"低风险: {stats['low_risk']}\n"
)
else:
high = sum(1 for r in results if r.risk_level == "high")
med = sum(1 for r in results if r.risk_level == "medium")
low = sum(1 for r in results if r.risk_level == "low")
lines.append(
f" 总句数: {len(results)} | 高风险: {high} | 中风险: {med} | 低风险: {low}\n"
)
risk_order = {"high": 0, "medium": 1, "low": 2}
sorted_results = sorted(results, key=lambda r: risk_order.get(r.risk_level, 3))
for r in sorted_results:
if r.risk_level == "low":
continue
tag = "[高风险]" if r.risk_level == "high" else "[中风险]"
lines.append(f" {tag} 相似度: {r.similarity:.0%}")
lines.append(f" 原句: {r.sentence[:100]}...")
if r.matched_title:
lines.append(f" 匹配: {r.matched_title}")
if r.suggestion:
lines.append(f" 建议: {r.suggestion}")
lines.append("")
lines.append(" ⚠️ 声明:本结果仅供参考,正式查重请使用学校指定系统。\n")
return "\n".join(lines)
"""数据结构定义"""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Paper:
"""论文统一数据结构"""
title: str
authors: list[str] = field(default_factory=list)
year: Optional[int] = None
abstract: Optional[str] = None
doi: Optional[str] = None
url: Optional[str] = None
pdf_url: Optional[str] = None
citation_count: int = 0
source: str = ""
venue: Optional[str] = None
paper_type: Optional[str] = None # e.g. "book" for GB/T 7714 [M]
keywords: list[str] = field(default_factory=list)
paper_id: Optional[str] = None
relevance_score: float = 0.0
final_score: float = 0.0
@property
def authors_str(self) -> str:
if not self.authors:
return "未知作者"
if len(self.authors) <= 3:
return ", ".join(self.authors)
return f"{self.authors[0]} 等"
@property
def citation_key(self) -> str:
first_author = self.authors[0].split()[-1] if self.authors else "Unknown"
return f"{first_author}{self.year or 'n.d.'}"
def to_dict(self) -> dict:
return {
"title": self.title,
"authors": self.authors,
"year": self.year,
"abstract": self.abstract,
"doi": self.doi,
"url": self.url,
"pdf_url": self.pdf_url,
"citation_count": self.citation_count,
"source": self.source,
"venue": self.venue,
"paper_type": self.paper_type,
"keywords": self.keywords,
"relevance_score": self.relevance_score,
"final_score": self.final_score,
}
@dataclass
class SearchResult:
"""搜索结果集合"""
query: str
papers: list[Paper] = field(default_factory=list)
total_found: int = 0
sources_used: list[str] = field(default_factory=list)
search_time_ms: int = 0
def to_dict(self) -> dict:
return {
"query": self.query,
"total_found": self.total_found,
"sources_used": self.sources_used,
"search_time_ms": self.search_time_ms,
"papers": [p.to_dict() for p in self.papers],
}
@dataclass
class CheckResult:
"""查重预检结果"""
sentence: str
similarity: float
matched_title: Optional[str] = None
matched_authors: Optional[str] = None
matched_source: Optional[str] = None
risk_level: str = "low" # low / medium / high
suggestion: Optional[str] = None
@dataclass
class FormatIssue:
"""格式检查问题"""
location: str
issue_type: str
expected: str
actual: str
severity: str = "warning" # info / warning / error
"""论文评分系统
评分公式: relevance(40%) + citations(25%) + recency(20%) + source_authority(15%)
"""
from __future__ import annotations
import math
from schema import Paper
from relevance import compute_relevance
from dates import recency_score as calc_recency
SOURCE_AUTHORITY = {
"Semantic Scholar": 0.9,
"CrossRef": 0.85,
"arXiv": 0.8,
"PubMed": 0.9,
"Google Scholar": 0.85,
"百度学术": 0.7,
"知网": 0.95,
"万方": 0.85,
"维普": 0.8,
}
W_RELEVANCE = 0.40
W_CITATIONS = 0.25
W_RECENCY = 0.20
W_AUTHORITY = 0.15
def _citation_score(count: int) -> float:
"""引用数归一化 (0.0 ~ 1.0),使用对数缩放"""
if count <= 0:
return 0.0
return min(1.0, math.log1p(count) / math.log1p(10000))
def score_paper(paper: Paper, query: str) -> float:
"""为单篇论文计算综合评分 (0 ~ 100)"""
rel = compute_relevance(query, paper.title, paper.abstract or "")
paper.relevance_score = rel
cite = _citation_score(paper.citation_count)
rec = calc_recency(paper.year) if paper.year else 0.3
auth = SOURCE_AUTHORITY.get(paper.source, 0.5)
raw = (W_RELEVANCE * rel + W_CITATIONS * cite +
W_RECENCY * rec + W_AUTHORITY * auth)
paper.final_score = round(raw * 100, 1)
return paper.final_score
def score_papers(papers: list[Paper], query: str) -> list[Paper]:
"""批量评分并按分数降序排列"""
for p in papers:
score_paper(p, query)
papers.sort(key=lambda p: p.final_score, reverse=True)
return papers
"""配置向导"""
import os
from pathlib import Path
from env import config_dir, diagnose
from ui import header, info, success, warning, section
def run_wizard():
"""交互式配置向导"""
header("PaperCash 配置向导")
info("PaperCash 支持 8 个学术数据源。")
info("其中 4 个无需任何配置即可使用:\n")
success("Semantic Scholar (2亿+论文)")
success("arXiv (STEM预印本)")
success("CrossRef (1.4亿DOI)")
success("百度学术 (中文论文)")
section("可选配置")
info("以下数据源需要额外配置:\n")
warning("Google Scholar - 需设置代理 (GOOGLE_SCHOLAR_PROXY)")
warning("知网 CNKI - 需设置 Cookie (CNKI_COOKIE)")
warning("万方 - 需设置 Cookie (WANFANG_COOKIE)")
warning("Semantic Scholar API Key - 可提高速率限制")
section("配置文件位置")
cfg = config_dir()
env_file = cfg / ".env"
info(f"全局配置: {env_file}")
info(f"项目配置: .papercash.env (当前目录)\n")
if not cfg.exists():
cfg.mkdir(parents=True, exist_ok=True)
success(f"已创建配置目录: {cfg}")
if not env_file.exists():
template = """# PaperCash 配置文件
# 作者: Jesse (https://github.com/Jesseovo)
#
# 所有配置均为可选
# Semantic Scholar、arXiv、CrossRef、百度学术无需配置即可使用
# Semantic Scholar API Key(可选,提高速率限制)
# 获取: https://www.semanticscholar.org/product/api
# SEMANTIC_SCHOLAR_API_KEY=
# Google Scholar 代理(可选)
# GOOGLE_SCHOLAR_PROXY=http://127.0.0.1:7890
# 知网 Cookie(可选)
# 获取: 浏览器登录知网 -> F12 -> Network -> 复制 Cookie
# CNKI_COOKIE=
# 万方 Cookie(可选)
# WANFANG_COOKIE=
"""
env_file.write_text(template, encoding="utf-8")
success(f"已创建配置模板: {env_file}")
else:
info(f"配置文件已存在: {env_file}")
section("当前数据源状态")
status = diagnose()
for name, available in status.items():
if available is True:
success(name)
elif available is False:
warning(f"{name} (未配置)")
else:
warning(f"{name} ({available})")
print()
info("配置完成!运行 'python scripts/papercash.py --diagnose' 可随时检查状态。")
"""PaperCash 数据源模块"""
"""arXiv 数据源 - 免费,STEM预印本"""
from __future__ import annotations
import xml.etree.ElementTree as ET
import re
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from schema import Paper
from http_client import get_text
from dates import parse_year
BASE_URL = "http://export.arxiv.org/api/query"
MAX_RESULTS = 30
NS = {
"atom": "http://www.w3.org/2005/Atom",
"arxiv": "http://arxiv.org/schemas/atom",
}
def search(query: str, limit: int = MAX_RESULTS) -> list[Paper]:
"""搜索 arXiv"""
params = {
"search_query": f"all:{query}",
"start": 0,
"max_results": min(limit, 100),
"sortBy": "relevance",
"sortOrder": "descending",
}
xml_text = get_text(BASE_URL, params=params, timeout=20)
if not xml_text:
return []
return _parse_response(xml_text)
def _parse_response(xml_text: str) -> list[Paper]:
try:
root = ET.fromstring(xml_text)
except ET.ParseError:
return []
papers: list[Paper] = []
for entry in root.findall("atom:entry", NS):
title_el = entry.find("atom:title", NS)
if title_el is None or not title_el.text:
continue
title = re.sub(r"\s+", " ", title_el.text.strip())
authors = []
for author in entry.findall("atom:author", NS):
name_el = author.find("atom:name", NS)
if name_el is not None and name_el.text:
authors.append(name_el.text.strip())
abstract = ""
summary_el = entry.find("atom:summary", NS)
if summary_el is not None and summary_el.text:
abstract = re.sub(r"\s+", " ", summary_el.text.strip())
published = ""
pub_el = entry.find("atom:published", NS)
if pub_el is not None and pub_el.text:
published = pub_el.text.strip()
year = parse_year(published)
arxiv_id = ""
id_el = entry.find("atom:id", NS)
if id_el is not None and id_el.text:
arxiv_id = id_el.text.strip().split("/abs/")[-1]
pdf_url = ""
for link in entry.findall("atom:link", NS):
if link.get("title") == "pdf":
pdf_url = link.get("href", "")
break
url = f"https://arxiv.org/abs/{arxiv_id}" if arxiv_id else ""
doi = None
doi_el = entry.find("arxiv:doi", NS)
if doi_el is not None and doi_el.text:
doi = doi_el.text.strip()
categories = []
for cat in entry.findall("atom:category", NS):
term = cat.get("term", "")
if term:
categories.append(term)
papers.append(Paper(
title=title,
authors=authors,
year=year,
abstract=abstract,
doi=doi,
url=url,
pdf_url=pdf_url,
source="arXiv",
venue="arXiv",
keywords=categories,
paper_id=arxiv_id,
))
return papers