全部项目详解

ResearchGo

即将上线

为 CIBES Research Centre 开发的平台,把一篇学术论文从投稿一路带到发表。已开发完成,正在做上线前的最后检查。

我的角色
架构设计与开发
年份
2026
使用技术
Next.js · Supabase (EU) · Python · FastAPI · GROBID · OpenAlex · Stripe
AUTHOR: PDF OR WORDNEXT.JS + SUPABASEPYTHON WORKER: FASTAPI + GROBIDVERSIONS: ENCRYPTEDTRACK CHANGES, REFERENCE CHECKJOURNAL MATCH: OPENALEXSIGN INSIGNED JOBPARSED TEXTCOMPAREEMBEDDINGALL DATA IN THE EURULES ONLY, NO LANGUAGE MODELTEXT STAYS ON OUR WORKER

横向滚动可以看到完整的图。

架构UPLOAD → PARSE ON ARRIVAL → ENCRYPTED VERSIONS → EDITOR TOOLS

问题

学术投稿靠的是邮件往来和电子表格。只要参与的人超过一个,版本、审稿意见和期刊要求就会对不上,编辑每次都得手动回答同样的问题:这是哪个版本?改了什么?参考文献有没有问题?投哪本期刊合适?

实现方式

一个跑在 Supabase 上的 Next.js 应用,所有数据都存放在欧盟境内。作者用 ORCID 或邮箱登录,上传 PDF 或 Word 格式的稿件。稿件一到,Python worker(FastAPI)就会解析它,PDF 交给 GROBID 处理,然后通过一个带签名和时间戳的回调把结构化文本发回来,伪造或重放的任务都会被这个回调拒绝。每次解析成功都会生成一个新版本并加密存储,但前提是文本确实有变化。编辑可以用 Word 式的修订模式对比任意两个版本,核对正文引用和参考文献列表是否对得上,还能拿到按语义排序的期刊推荐;推荐所用的 embedding 在我们自己的 worker 上计算,期刊数据来自 OpenAlex。支付走 Stripe。

关键决策

  • 规则能解决的,就不用语言模型

    修订对比和参考文献检查都是确定性的代码,不涉及 AI。编辑必须能信任 diff,而规则每次都会给出同样的答案。

  • 稿件不离开我们的服务器

    期刊匹配在我们自己的 worker 上对论文做 embedding,所以稿件文本不会发给任何第三方。排序完全按相似度。收录索引、开放获取和费用信息只作为标签显示,从不改变顺序。

  • 每个任务都带签名

    Web 应用和 worker 之间的任务都带有 HMAC 签名和时间戳。伪造或重放的任务会被拒绝;worker 在任务中途挂掉,稿件在队列里的位置也不会丢。

仓库里的代码

apps/web/lib/versions/diff.ts不用语言模型,生成两个稿件版本之间的修订对比
function diffParagraphPair(before: string, after: string): DiffSegment[] {
  return diffWordsWithSpace(before, after).map((part) => ({1
    op: part.added ? "ins" : part.removed ? "del" : "equal",
    text: part.value,
  }));
}
 
export function diffManuscripts(before: string, after: string): DiffResult {
  const a = splitParagraphs(before);
  const b = splitParagraphs(after);
  // …
  const parts = diffArrays(a, b);2
  let i = 0;
  while (i < parts.length) {
    const part = parts[i];
    if (!part) break;
    if (!part.added && !part.removed) {
      // …
      i++;
      continue;
    }
    const next = parts[i + 1];
    const paired = Boolean(part.removed && next?.added);3
    const removedList = part.removed ? part.value : [];
    const addedList = part.added ? part.value : paired && next ? next.value : [];
 
    const pairs = Math.min(removedList.length, addedList.length);
    for (let k = 0; k < pairs; k++) {
      const segments = diffParagraphPair(removedList[k] as string, addedList[k] as string);
      paragraphs.push({ kind: "changed", segments });
      // …
    }
    // …
    i += paired ? 2 : 1;
  }
 
  return { paragraphs, summary };
}
  1. 第 2 行. 在改动过的段落内部,diff 按单词进行并保留空白,读起来就像 Word 里的修订。

  2. 第 12 行. 第一遍:对整段做最长公共子序列 diff,找出哪些保留、哪些新增、哪些删除。

  3. 第 23 行. 删除后紧跟着新增,算作一个被编辑的段落,而不是两处独立的改动。

目前进展

已开发完成,正在做最后检查,检查一结束就上线。

下一篇Ürünereklam