← 返回文章列表

记一次华为杯数学建模协作经验

记录华为杯数学建模三人协作的一次实践:在 Windows 上用 Docker 自建 Overleaf 并通过校园网共享,把没有官方 API 的 CE 版逆向成一套 CLI,再用 DeepSeek Harness(管同步)与 Codex(管写作)围绕同一个本地文件夹搭起写作闭环。

#数学建模#LaTeX#Overleaf#Docker#经验分享#自动化

前言

研赛的节奏是这样的:9 月 23 日早上八点开题,9 月 27 日中午十二点前必须提交论文的 MD5 码——四天四小时,三个人,一篇几十页的论文。

先交 MD5、再传正文,意味着十二点那一刻你的文件必须已经是最终版,改一个字节都对不上。关于论文写作,如果用word的话,调整格式太麻烦了且不方便AI操作;如果使用latex的话,三个人各自在本地写 .tex,合并的时候宏包冲突、\label 重名、图片路径不对、公式编号全乱。

所以华为杯之前,我提前做了两件事:

  1. 用 Windows 电脑 + Docker 搭一个属于我们三个人的 Overleaf,通过校园网这个大局域网共享出去;
  2. 让 DeepSeek Harness 和 Codex 两个 AI 代理接管 LaTeX 的同步与写作(这一部分只针对笔者工作而言)。

这篇文章就是这两件事的完整记录。

一、为什么自己搭一个 Overleaf

在线版 Overleaf 当然能用,但放在比赛场景里,有三个绕不过去的点:

  • 免费版编译慢、协作人数受限,冲刺阶段等一次编译几十秒,很难受
  • 宏包受限,数学建模常用的 algorithm2e、booktabs、国标参考文献之类的宏包不一定能随手装
  • 数据在别人服务器上,比赛论文虽然不算机密,但心里总归不踏实

自己搭社区版(CE)就全解决了:无限项目、随便装宏包、三个人实时协作,跟 Google Docs 一样的体验。

我之前在 NAS 上完整部署过一套 Overleaf(含域名、HTTPS、外网访问),步骤和踩坑都写在《Overleaf 部署实战指南》里了。这次是另一个场景:比赛现场只有一台 Windows 电脑,不需要公网,只需要"在场的三个人能同时编辑"。所以部署方式完全不同——不搞域名和证书,直接 80 端口映射到校园网。

二、在 Windows 上用 Docker 部署

2.1 三个容器

Overleaf CE 本体是三个服务:Web 应用(sharelatex)+ 数据库(mongo)+ 缓存(redis)。用 Docker 把它们拉起来,宿主机上只需要暴露一个 80 端口。

# docker-compose.yml
services:
  sharelatex:
    image: sharelatex/sharelatex:latest
    container_name: sharelatex
    depends_on:
      - mongo
      - redis
    ports:
      - "80:80"                             # ← 映射到宿主机 80
    volumes:
      - ./data/overleaf:/var/lib/overleaf
      - ./data/texlive:/usr/local/texlive   # ← 宏包持久化,见踩坑表
    environment:
      OVERLEAF_SITE_URL: http://10.198.119.0   # ← 换成你的校园网 IP
      OVERLEAF_NAV_TITLE: Overleaf
      OVERLEAF_ADMIN_EMAIL: you@example.com
      OVERLEAF_MONGO_URL: mongodb://mongo/sharelatex
      OVERLEAF_REDIS_HOST: redis

  mongo:
    image: mongo:8
    volumes:
      - ./data/mongo:/data/db

  redis:
    image: redis:7
    volumes:
      - ./data/redis:/data
docker compose up -d
docker compose ps        # 三个容器都是 Up 就对了

2.2 建三个账号(CE 版没有注册页)

Overleaf 社区版的注册页是写死的,注册不了,只能直接往 MongoDB 里插用户(或者通过邀请别人进行注册)。这部分的完整脚本我在《Overleaf 部署实战指南》里写过。

相关文章Overleaf部署实战指南本文记录在 fnOS NAS 上自建 Overleaf 6.2.2 社区版的完整实战:用 overleaf-toolkit 部署,迁移数据目录到用户可见路径,持久化挂载 eX Live 并切换清华镜像安装 ctex 等宏包,直插 MongoDB 创建管理员账号,最后经 Lucky 反代实现域名 H PS 外网访问与多人协作,并总结 7 个关键踩坑点。

我们三个人是按人一人一个账号分配的,邮箱统一用队内的 mcm.com 域名,密码用「姓名缩写 + 年份 + 用途」的格式,好记又不容易记混:

账号密码角色
hwl@mcm.comHWL2026latex管理员
skq@mcm.com SKQ2026latex管理员
wyh@mcm.comWYH2026latex管理员

三、把 Overleaf 共享到校园网

这一步才是这次和上次最大的区别——不需要域名,不需要证书,只需要一个局域网 IP。

3.1 拿到本机在校园网里的 IP

ipconfig
# 找到正在用的那块网卡,记下 IPv4 地址(我们这台是 10.198.119.0)

3.2 放行防火墙

Windows 默认会拦掉入站的 80 端口,队友会看到"无法访问此网站"。加一条入站规则:

netsh advfirewall firewall add rule name="Overleaf-80" dir=in action=allow protocol=TCP localport=80

3.3 队友访问

把地址发给队友,浏览器打开:

http://10.198.119.0/

三个人各自登录自己的账号,进同一个项目——实时协作就成立了。谁在改哪一段,光标都是可见的,公式一边写一边就能看到渲染结果,这比微信传文件强太多。

四、为什么要再落一份到本地文件夹

到这里,"三个人同时编辑"的问题已经解决了。但真正开始写论文之后,我发现光有 Overleaf 还不够:

  • 几十页的 .tex 在网页编辑器里翻起来很累,跨文件改动更是折磨
  • 比赛的图和表是脚本生成的,需要有人(或者有 AI)批量改
  • 最关键的一点:AI 读不了 Overleaf 的网页。它只能读磁盘上的文件

于是就有了这次真正的核心:在本地建一个文件夹,让两个 AI 代理都去读它。

我们最后的工作区长这样:

E题工作区间/
├── E题/paper_local/          ← 论文文件夹(两个 AI 共同读写的唯一接口)
│   ├── main.tex              ← 入口
│   ├── sec_q1.tex            ← 问题一正文
│   ├── sec_q2.tex            ← 问题二正文
│   ├── sec_q3.tex            ← 问题三正文
│   ├── sec_dataset.tex / sec_symbols.tex / sec_validation.tex
│   ├── appendix_A.tex / appendix_B.tex
│   ├── reference.bib
│   ├── figures/              ← 图
│   ├── papers/               ← 参考论文 PDF
│   └── support_q1 ~ q3/      ← 支撑材料代码
│
└── overleaf_api/             ← DeepSeek Harness 逆向出来的接口层
    ├── overleaf_client.py    ← 核心库(curl 传输 + socket.io + CRUD)
    ├── overleaf_cli.py       ← 命令行
    ├── overleaf_server.py    ← 统一 REST 服务
    ├── probe/                ← 逆向过程的探针与证据
    └── README.md             ← 接口文档

注意这个布局的关键点:接口层的代码一行都不放进论文文件夹,两个目录完全分开。论文文件夹里只有论文,AI 看它的时候不会被一堆脚本干扰。

五、双 Agent 分工:一个管同步,一个管写作

先看整体架构:

┌──────────────────────── Windows 电脑 ────────────────────────┐
│                                                              │
│   ┌──────────────┐    ┌────────┐    ┌────────┐               │
│   │  sharelatex  │◄──►│ mongo  │    │ redis  │               │
│   │   容器 :80   │    └────────┘    └────────┘               │
│   └──────┬───────┘                                           │
│          │ 80:80 端口映射                                     │
│   ┌──────▼────────────────────────────────┐                  │
│   │      paper_local/  论文文件夹          │                  │
│   │  main.tex  sec_q*.tex  figures/  ...  │                  │
│   └───┬───────────────────────────▲───────┘                  │
│       │ 读 / 写                    │ pull / push              │
│   ┌───▼────────┐             ┌─────┴──────────────┐          │
│   │   Codex    │             │  DeepSeek Harness  │          │
│   │  负责写作   │             │  负责同步 + 接口层   │          │
│   └────────────┘             └────────────────────┘          │
└──────────────────────────────────────────────────────────────┘
                    ▲ 校园网 80 端口
        ┌───────────┼───────────┐
    队员 A       队员 B      队员 C
   (浏览器)     (浏览器)    (浏览器)

两个 AI 各干一件事,互不干扰。

5.1 DeepSeek Harness:把 Overleaf 逆向成一套 CLI

开题第一天我就发现了一个问题:这台自建 Overleaf 根本没有官方 API。

Overleaf 社区版不提供 SaaS/Pro 那套 /api/v1/... 的 REST 接口,官方文档里的端点在自建实例上直接 404。也就是说,想用脚本操作项目,只有一条路:把它当成一个普通网站,从网页版的行为里把接口抠出来。

所以我让 DeepSeek Harness 做了这件事,完整走完了四步:

  1. 拿会话:走网页端登录流程,拿到 overleaf.sid cookie 和 CSRF token
  2. 找端点:把它自己下发的前端 JS bundle 扒下来读,从中还原出内部端点
  3. 逐个实测:每个端点都在真实服务器上验证一遍,宁可不写也不能猜
  4. 统一封装:把混杂的 JSON / multipart / socket.io 收敛成一套干净的接口

三个真正花时间的点

① 文件树必须走 socket.io

GET /project/:pid/entities 只返回路径,没有 id:

{"project_id":"...","entities":[{"path":"/main.tex","type":"doc"}]}

但后续所有增删改查都要用 entity id。id 只能从实时协议里拿——这台实例用的是 socket.io 0.9,WebSocket 升级被 nginx 挡掉了(502),好在 xhr-polling 是纯 HTTP,可以完全用 curl 驱动:

GET  /socket.io/1/?t=<ms>&projectId=<pid>     →  <sid>:60:60:websocket,xhr-polling
GET  /socket.io/1/xhr-polling/<sid>?t=<ms>    →  "1::"
POST /socket.io/1/xhr-polling/<sid>?t=<ms>
     body: 5:1::{"name":"joinProject","args":[{"project_id":"<pid>"}]}
GET  /socket.io/1/xhr-polling/<sid>?t=<ms>    →  joinProjectResponse(带完整目录树)

② 上传接口的两个反直觉设计

行为实测结果
folder_id 放哪必须在 query string。放 body 一律 422 folder_not_found,而且对根目录、子目录报的错一模一样,极易误判成"目录 id 错了"
name 字段必需。只给 qqfile 不给 name → 422 invalid_filename。文件名的权威来源是 multipart 的 name,不是 part 的 filename

③ 最大的坑:macOS 不让第三方解释器访问局域网

这是整个项目里最折磨人的一条,而且跟代码完全无关:

程序能否访问 10.198.119.0
/usr/bin/curl(Apple 系统自带)✅ 可以
/usr/bin/nc✅ 可以
python3(python.org / Homebrew)❌ Errno 65 No route to host
node(Homebrew v22)❌ EHOSTUNREACH

macOS 的本地网络隐私权限只信任 Apple 签名的系统二进制,第三方解释器一律被拒。目标和本机明明在同一个内网段,ping 也通,但 Python 就是连不上。

所以这个项目的 HTTP 传输层全部基于 /usr/bin/curl 子进程,会话存在磁盘上的 Netscape cookie jar 里。这是个刻意的架构决定,不能"顺手改成 requests"——改了直接连不上。

最后沉淀下来的东西

逆向的产出不是一个临时脚本,而是三件可以长期复用的东西(都在 overleaf_api/):

文件作用
overleaf_client.py核心库:curl 传输层、登录、socket.io 客户端、路径寻址的 CRUD
overleaf_cli.py命令行工具:tree / cat / write / mkdir / rm / rename / move / grep / pull / push / compile
overleaf_server.py统一 REST 服务:把零散端点收敛成路径寻址的接口,调用方只用 /main.tex 这种人类路径,完全不需要知道 entity id

命令行长这样:

python3 overleaf_cli.py tree -r                              # 看整棵树
python3 overleaf_cli.py cat /main.tex                        # 读
python3 overleaf_cli.py write /sec_q1.tex --file sec_q1.tex  # 写(同名即覆盖)
python3 overleaf_cli.py grep '\cite\{' --ext .tex            # 全项目检索
python3 overleaf_cli.py pull ./paper_local                   # 整包拉下来
python3 overleaf_cli.py push ./paper_local --prefix /        # 整个目录推上去
python3 overleaf_cli.py compile --pdf main.pdf               # 触发线上编译并取回 PDF

💡 这一步的价值在于"可复现"。接口被固化成命令 + 文档之后,同步就不再依赖某个人的记忆,也不需要每次打开网页手动上传——后续所有上传修改都是一条命令的事。

一个我很在意的细节:不污染源项目

写接口最怕的事,是测试的时候把真实项目改坏了。所以三套测试(库层 25 项、REST 28 项、CLI 全流程)用的是指纹法:

  1. 先对真实项目取指纹:sha256(每个节点的 id|type|path|hash)
  2. 所有写操作只在临时项目 __..._SELFTEST_DO_NOT_USE__ 上做
  3. 跑完删掉临时项目
  4. 再取一次真实项目指纹,不一致就判定失败

最终结果:

selftest.py   ===== 25 passed, 0 failed =====   (真实项目指纹前后一致)
rest_test.py  ===== 28 passed, 0 failed =====   (真实项目指纹前后一致)

真实项目 19 个节点,指纹前后完全相同。

5.2 Codex:只关心论文怎么写

Codex 那边就简单了:它只读同一个文件夹(paper_local/)。

它的职责是纯粹的写作:

  • 读 main.tex 和 sec_q*.tex,理解当前论文结构
  • 补写、润色问题一到问题三的正文,统一符号与术语
  • 生成三线表、改写摘要、检查交叉引用和公式编号是否断裂

它不需要知道 Overleaf 在哪、端口是多少、cookie 怎么带、socket.io 怎么握手——这些全被隔离在接口层了。

5.3 为什么不让一个 AI 全干

这是我这次体会最深的一点。把"同步"和"写作"交给同一个代理,会出两个问题:

  • 上下文被污染:socket.io 的分帧格式、422 报错、cookie 过期这些细节会挤占写作需要的长上下文,写到后面它已经忘了前面用过什么符号
  • 职责边界模糊:一旦论文出了问题,你分不清是"文件没同步对"还是"内容写错了"

拆开之后,两个代理各自有一个非常清晰的成功标准:同步代理的成功标准是"本地与线上完全一致",写作代理的成功标准是"这一节写得好"。排查问题时,责任一目了然。

六、这次协作真正的收获

比赛结束后回头看,我觉得最有价值的不是"搭了一个 Overleaf",而是把协作拆成了两层:

第一层是人跟人的协作。Overleaf 解决的是"三个人同时写一篇 LaTeX"这件事——不用合并文件、不用对齐版本、不用问"你改到哪了"。这一层的关键是把服务放在所有人都够得着的地方,所以"校园网内网共享"这个看起来最土的做法,反而是最合适的:不依赖公网、不依赖域名、不依赖任何第三方服务。

第二层是人和 AI 的协作。DeepSeek Harness 负责同步,Codex 负责写作,paper_local/ 这个文件夹是它们共同的接口。这一层的关键是职责单一:一个代理只做一件事,成功标准清清楚楚。

总结

四天四小时,这套链路是这样的:

  1. 部署:Windows + Docker 起 Overleaf,80 端口映射到校园网,三个人浏览器直接访问
  2. 协作:实时共同编辑一份 LaTeX,谁改哪里一目了然
  3. 逆向:DeepSeek Harness 把没有官方 API 的 Overleaf CE 逆向成一套 CLI 和 REST 服务
  4. 写作:Codex 读本地 paper_local/,专心写论文
  5. 回传:本地文件一变,DeepSeek Harness 识别到变化并 push 回线上
  6. 收口:线上编译出 PDF,交 MD5 码前把版本彻底对齐

本文由 DeepSeek Harness 根据本次华为杯数学建模的实践过程整理,环境:Windows + Docker Desktop 自建 Overleaf CE,macOS 客户端(笔者),三人小队协作。