Endor · Tabularium
Endor 的文档站与 AI 公司研究工作台,包含文档阅读、全文搜索、研究数据展示和制图工具。使用 Next.js Pages Router、React、TypeScript 与 Tailwind CSS;JavaScript workspace 使用 pnpm,制图 Python 项目使用 uv。
代码与内容放在哪里
文档正文、目录元数据和修订记录由站点 SQLite 数据库管理,日常写作直接更新数据库,无需提交代码、构建或部署。研究业务数据仍沿用版本化 JSON 导入机制,后续单独迁移。
| 内容 | 权威来源与维护方式 |
|---|---|
| 文档正文、分类、合集、分组、摘要、草案和推荐状态 | 数据库;通过文档 API 或服务器上的 docs 管理工具读写 |
| 文档旧地址与修订历史 | 数据库;改名保留别名,修改追加修订 |
| AI 公司、观测、证据、候选与核查记录 | content/research 中的版本化快照;服务端显式按版本导入同库后查询 |
| 页面、组件、管理工具和部署逻辑 | 本仓库,通过工作分支与 PR 评审 |
| 图形源码与静态资源 | tools/figures 与 apps/docs/public;生成步骤在远程 Runner 执行 |
以上数据库数据共用 AUTH_CONFIG_FILE 同目录下的 login-state.sqlite,该文件名沿用历史命名。生产环境挂载到容器 /auth,不随镜像替换。文档使用 documents、document_aliases、document_revisions、document_imports 表,研究数据使用 research_* 表,登录状态使用 login_state 表。
仓库不接收新的文档正文 Markdown、目录 JSON、数据库文件或日常正文快照。历史 Git 正文仅用于一次性迁移;运行时不读取 content/docs 或 content/catalog.json。README 维护程序使用与运维约定,研究个案、统计数量与内容更新日志在工作台和数据库文档中查看。
阅读与访问
首页、阅读页、目录及全文搜索按请求读取数据库,内容更新后刷新即可看到。旧文件式地址和改名前的语义地址继续跳转到当前地址,并保留查询参数。
- 文档原文:
GET /api/documents/source?slug=<文档地址>。 - 全文搜索:
GET /api/search。 - 研究工作台:
/docs/other-research/ai-company-research/overview。 - 收入样本:
/docs/other-research/ai-company-research/income-snapshot。 - 覆盖与缺项:
/docs/other-research/ai-company-research/coverage。
正文、搜索、研究 API 与静态资料均经过站点登录认证,私有响应使用 private, no-store。Markdown 支持 GFM 表格,不解析原始 HTML;正文图片写入时检查压缩格式。草案标记用于展示状态,不构成单独的访问权限。
管理文档
部署后,管理员可在服务器上使用 /opt/endorphin-tabularium-docs/docs。该工具在当前容器中操作数据库,需要服务器运维权限。自动化上传使用下文的文档 API,日常读写无需登录服务器。
sudo /opt/endorphin-tabularium-docs/docs list
sudo /opt/endorphin-tabularium-docs/docs get endor/endor-purpose-draft
sudo /opt/endorphin-tabularium-docs/docs history 04-endor-purpose-draft
save 从标准输入接收完整 JSON。必填字段如下:
| 字段 | 用途 |
|---|---|
slug |
语义化公开地址,不含数字章节前缀 |
sourceSlug |
稳定标识;修改公开地址时保持不变 |
title、content |
标题与 Markdown 正文 |
category、collection、group |
分类、合集与目录分组 |
description、draft、featured |
摘要、草案状态与推荐状态 |
revision |
新建传 0;修改传最近一次读取到的版本号 |
修改现有文档的流程:
umask 077
doc_work_dir=$(mktemp -d)
sudo /opt/endorphin-tabularium-docs/docs get endor/endor-purpose-draft > "$doc_work_dir/document.json"
# 编辑该私有 JSON 文件,保留读取到的 revision 和 sourceSlug。
sudo /opt/endorphin-tabularium-docs/docs save < "$doc_work_dir/document.json"
保存成功后,正文、目录和搜索通过后续请求读取新内容。发生版本冲突时重新读取并合并修改,不能仅改大 revision 覆盖并发编辑。调整分类或分组也走同一保存流程;history 使用稳定的 sourceSlug 查询修订。
文档上传 API
文档 API 直接读写同一个数据库,供自动化整理和大量文档上传使用。它只接受独立的文档 API Key,不接受网页 Cookie 或通用网页会话令牌;Key 不能用于研究、登录或其他站点 API。
| 方法与地址 | 用途 |
|---|---|
GET /api/documents?limit=50&offset=0 |
分页读取目录元数据;不返回全部正文 |
GET /api/documents/item/<slug> |
读取单篇完整正文、目录字段和当前 revision;支持旧地址 |
GET /api/documents/item/<slug>?history=1&limit=50&offset=0 |
分页读取修订元数据与时间,按新到旧排列 |
GET /api/documents/item/<slug>?revision=1 |
读取指定修订的完整正文 |
POST /api/documents |
保存一个完整文档 JSON;字段与 docs save 相同 |
POST /api/documents/batch |
保存 { "documents": [...] },每个请求 1—100 篇 |
GET /api/documents/sections/<slug> |
读取章节 ID、标题和内部摘要,不返回正文 |
GET /api/documents/sections/<slug>?section=<id> |
读取选中章节正文;重复 section 可选多章 |
GET /api/documents/sections/<slug>?revision=2 |
读取对应修订的章节目录;也可搭配 section |
POST /api/documents/sections/<slug> |
原子执行章节新增、更新、删除和移动 |
写入使用 Authorization: Bearer <文档 API Key>。新建传 revision: 0;更新先读取当前全文,再使用读到的 revision。每篇正文最多 2,000,000 个 JavaScript 字符单位;每次请求最多 8 MiB。目录和修订分页的 limit 范围为 1—100,响应包含 total 和 nextOffset。
单个批量请求在同一事务提交,任意一篇格式错误、版本冲突或地址冲突都会整体回滚。成功返回文档地址、稳定标识和新修订号;版本或幂等键冲突返回 409,格式错误返回 400,超出请求大小返回 413。缺少、过期或撤销的 Key 返回 401,只读 Key 写入返回 403。
建议写入时提供 Idempotency-Key,由 1—128 个字母、数字或 ._:- 组成。同一个 Key、相同请求 ID 和相同输入重试时,返回原回执并标记 replayed: true,不追加修订;相同 ID 改换输入会冲突。回执描述原请求,不代表之后没有其他修改;需要当前内容时重新读取文档。改过正文或 revision 后,应使用新的请求 ID。
先读摘要,再按章节编辑
章节接口使用相同的文档 Key 和读写权限。编辑流程为:先读章节目录及摘要,选择需要深入的章节,再读取这些章节的正文,最后提交一组操作。不要只凭摘要修改未读正文;目录与正文响应的 revision 应一致,否则重新读取。接口不调用模型生成摘要:由编辑 Agent 阅读正文、撰写摘要,并在编辑请求中保存。
章节按行首 Markdown ATX 二级标题(##)划分,三级至六级标题随所属章节一起编辑;代码围栏内的标题不会切章。二级标题之前的文档标题及导言属于固定 ID preamble,可以修改但不能删除或移动。Setext 标题、带缩进或引用、列表标记的标题不作为章节边界。正文无行首二级标题时,全文作为导言处理。拆分及组合保持换行和未编辑内容原样,不自动格式化 Markdown。
目录中的每章包含 id、title、position、summary、summaryStatus 和 contentHash。摘要为编辑内部数据,单独保存在 SQLite 的 document_sections 和 document_section_revisions 中,不加入普通文档响应、网页 props、搜索索引或页面正文。完整数据库备份包含这些表;docs export 仍只导出当前文档,不包含内部摘要和修订。
旧文章无需重写,首次读取即可按原文返回目录,初始摘要为空,summaryStatus 为 missing。Agent 应按需读取这些章节,生成摘要并通过 update 保存;保存后状态为 current。这里的 current 仅表示摘要与该版正文一起保存,不保证模型的总结正确。全文接口仍兼容;未变化章节保留摘要与 ID,正文变化的章节清空摘要,等待更新。全文改名且改写章节时可能产生新 ID,因此全文保存后应重新读取目录。
章节编辑请求示例(示例 ID 需要替换为目录实际返回的值):
{
"revision": 3,
"operations": [
{
"op": "update",
"id": "s_existing",
"content": "## 原有章节\n\n修改后的正文。\n\n",
"summary": "本章说明修改后的核心观点与边界。"
},
{
"op": "insert",
"after": "s_existing",
"content": "## 新章节\n\n新增论证。\n\n",
"summary": "补充上一章尚未覆盖的论证。"
}
]
}
update 可只改 summary,也可改正文;正文改变时若不提供新摘要,旧摘要会清空。insert 必须提供正文和非空摘要;摘要最多 8000 个 JavaScript 字符单位。delete 使用 { "op": "delete", "id": "s_existing" };move 使用 { "op": "move", "id": "s_existing", "after": "preamble" }。按数组顺序执行,最多 100 个操作;新章 ID 由服务器生成,在回执目录中返回。章正文应从自己的 ## 标题开始,只包含一个二级标题,并保留与下一章之间的换行;跨章代码围栏或章节边界改变会被拒绝,拆章请使用多个操作。
章节变更、内部摘要、组合后的全文和历史快照在同一事务内提交。每批操作只增加一次文章 revision;摘要单独更新也增加版本号。后续操作失败或全文版本过期时全部回滚。写入沿用幂等规则,回执包含新版本、章节目录和 replayed;章节接口与全文接口的幂等记录分开。修改章节不会改变其 ID,移动亦保留 ID;删除后的正文和摘要仍可从对应的旧修订读取。启用前的历史修订只有原文,摘要显示缺失。
Python 客户端对应命令:
python3 scripts/documents-client.py outline endor/endor-purpose-draft
python3 scripts/documents-client.py sections endor/endor-purpose-draft preamble s_existing
python3 scripts/documents-client.py edit-sections endor/endor-purpose-draft /private/path/edit.json --request-id chapter-edit-1
outline 和 sections 支持 --revision;请求文件只包含 revision 和 operations,不要把内部摘要文件提交到 Git。
创建、查看与撤销 Key
管理员只需通过已验证的服务器连接进行 Key 管理:
sudo /opt/endorphin-tabularium-docs/docs key-create codex-upload /auth/codex-docs-key.json read-write 90
sudo /opt/endorphin-tabularium-docs/docs key-list
sudo /opt/endorphin-tabularium-docs/docs key-revoke <key-id>
创建命令将完整 Key 写入认证私有目录中的指定新文件,权限 0600,终端仅输出 ID、权限和到期信息。文件已存在时拒绝覆盖。权限可选 read 或 read-write;有效期默认 90 天,可设 1—365 天。数据库仅保存 Key 摘要,列表不返回原文;撤销后不能继续认证。独立 Key 文件和认证 config.json 都不包含在 SQLite 整库备份中。
使用上传客户端
scripts/documents-client.py 仅使用 Python 标准库。通过 TABULARIUM_DOCS_URL 指定站点 origin,通过 TABULARIUM_DOCS_KEY_FILE 指定本机私有 Key 文件(支持创建命令输出的 JSON),不把 Key 放入命令行参数。
客户端要求 HTTPS;尚未配置 HTTPS 时,可通过已验证的 SSH 连接建立本机端口转发,连接 http://127.0.0.1:18136。以下 $DOCS_SSH_TARGET 应事先设置为受信任的 SSH 别名或 user@host:
ssh -N -L 18136:127.0.0.1:18135 "$DOCS_SSH_TARGET"
将服务器私有目录中的 Key 文件复制到受保护的本机位置后,在另一个终端配置客户端。$TABULARIUM_DOCS_KEY_FILE 文件须仅本人可读写,不能提交到仓库:
export TABULARIUM_DOCS_URL=http://127.0.0.1:18136
export TABULARIUM_DOCS_KEY_FILE="$HOME/.config/tabularium/codex-docs-key.json"
python3 scripts/documents-client.py list --limit 50
python3 scripts/documents-client.py get endor/endor-purpose-draft
python3 scripts/documents-client.py get endor/endor-purpose-draft --history
python3 scripts/documents-client.py get endor/endor-purpose-draft --revision 1
修改流程与 CLI 相同:
umask 077
api_work_dir=$(mktemp -d)
python3 scripts/documents-client.py get endor/endor-purpose-draft > "$api_work_dir/document.json"
# 编辑正文或目录字段,保留 sourceSlug 和读取到的 revision。
python3 scripts/documents-client.py save "$api_work_dir/document.json"
大量文档可使用仅含 documents 数组的私有 JSON 文件。客户端按 100 篇和 8 MiB 请求上限自动分批,并为各批派生固定的幂等请求 ID。每批独立提交;后续批次失败时,前面的成功批次保留。客户端会打印根请求 ID 和各批进度,使用相同 ID 与未修改的输入重试可跳过已完成的批次;可通过 --request-id 显式指定根 ID(客户端最多 100 个字符)。若后续批次因业务冲突失败,按进度中已提交的文档数量提取尚未成功的部分,修正后使用新的根 ID 上传。
客户端只自动重试一次临时网络或 502/503/504 错误,保持正文和请求 ID 不变;409 等业务错误不会自动覆盖。它不跟随 HTTP 重定向,避免将 Bearer 凭据转发到其他地址。正文和 Key 继续保存在数据库或私有文件中,不因使用 API 而进入 Git。
研究数据与检查
研究工作台提供公司筛选、经营指标、原始证据、收费与交付模式、技术材料及核查记录。它区分公司披露、支付平台指标、第三方估算、融资和客户收益;未知收入不填零,材料可访问也不代表性能或商业效果已经验证。研究范围、样本限制和个案结论以工作台记录为准。
| 仓库资料 | 用途 |
|---|---|
content/research/ai-companies.seed.json |
研究数据导入快照;更新必须更换 version |
content/research/ai-companies.sources.json |
来源范围、用途、覆盖边界与后续缺项 |
content/research/ai-companies.coverage.json |
候选池逐条筛选台账 |
content/research/ai-income-analysis.json |
独立复算的统计参考快照 |
服务端以事务导入未载入的版本,保留旧研究版本与登录、文档数据;同一版本标识下内容变化会拒绝导入。页面和 GET /api/research/ai-companies 从数据库读取对应研究版本。回退程序可能选择旧研究版本,不能等同于撤销数据库中的正文编辑。
以下 Python 检查不触发编译或构建,可在工作分支执行:
PYTHONDONTWRITEBYTECODE=1 python3 scripts/audit-ai-research.py --check
PYTHONDONTWRITEBYTECODE=1 python3 scripts/summarize-ai-income.py --check
若需对照研究数据检查报告正文,先用 docs export 导出当前文档,再给对应脚本传入 --document-export <导出文件>。默认 --check 核对研究数据的统计与审计不变量,不将历史迁移快照当作当前正文。脚本不带参数时仅在 .artifacts/reports 生成候选正文,需审阅后通过 docs save 写入数据库。
备份与首次迁移
sudo /opt/endorphin-tabularium-docs/docs backup /auth/manual-backup.sqlite
sudo /opt/endorphin-tabularium-docs/docs export /auth/current-documents.json
路径在容器内,输出文件已存在时拒绝覆盖。backup 是整个共享 SQLite 数据库的一致性备份,包含登录状态、研究数据、正文、别名和修订;export 只保存当前正文与目录,供空库初始化或外部迁移使用,不包含完整修订历史。认证配置单独保存在 /auth/config.json,整库备份不包含该配置文件。备份与导出应保存在私有挂载或受控备份位置。
首次迁移由 scripts/export-legacy-documents.py 从固定历史版本导出正文和目录,基线与研究正文覆盖版本在脚本中明确记录。导出写入 Git 忽略目录,权限为 0600,不进入生产镜像:
python3 scripts/export-legacy-documents.py --output .artifacts/documents-migration.json
该命令用于迁移准备,不是日常写作入口;目标文件存在时拒绝覆盖。部署先备份共享数据库,再用 import --if-empty 向空文档表事务导入。已有文档时跳过,不覆盖后续编辑;导入失败整体回滚并中止部署。--ref 仅用于明确选择历史导出版本。
程序回退不会撤销数据库编辑;回退到旧文件式站点则会显示旧正文。完整数据库恢复会同时影响登录、研究和文档状态,需先停止写入、保留当前备份,再明确选择恢复点。部署脚本只回退镜像,不自动恢复数据库。
登录管理
站点使用共享验证器的 6 位 TOTP 动态码,每 30 秒更新。会话使用 HttpOnly、SameSite=Lax Cookie,有效期 7 天;共享尝试上限为每 5 分钟 20 次,重放与限流状态在数据库中持久保存。缺少或损坏认证配置时拒绝私有访问。
sudo /opt/endorphin-tabularium-docs/auth qr
sudo /opt/endorphin-tabularium-docs/auth verify
认证配置和数据库位于服务器 /opt/endorphin-tabularium-docs/private-auth。正常部署保留验证器与会话签名密钥。qr 只在交互终端显示,qr-png 可导出到私有文件;只有 auth rotate --confirm 会轮换密钥并使旧会话失效。二维码和令牌不进入仓库、网页或 CI 日志。
当前 Compose 配置默认 AUTH_COOKIE_SECURE=false;实际采用 HTTPS 时,应在独立的生产配置中设置为 true。HTTP 不加密动态码与会话传输。生产部署使用无副作用的 verify,不执行会消耗动态码和触发限流的 exercise。
开发、验证与发布
代码依赖要求见 package.json 和锁文件;当前要求 Node.js 22.13+、pnpm 11.25.0,制图使用 Python 与 uv。构建、打包、制图及会隐式编译的检查只在已确认的 ces-control Runner 执行,本机不执行这些步骤。
工作分支完成轻量检查后推送并创建 PR,由用户手动合并。CI 仅在 main 推送或针对 main 的手动运行时执行,工作分支和 PR 不运行 CI。 不自动合并,也不因确认 Runner 而扩大触发范围。
主线通过 .gitea/workflows/ci-deploy.yml 执行 scripts/ci.py:
- 检查研究统计、审计与 Python 测试,生成首次迁移所需的历史导出。
- 根据程序、研究快照、图形、锁文件和构建脚本计算内容指纹。命中 verified 镜像时复用,否则安装依赖并执行 lint、类型检查、数据库测试、图形检查与构建。
- 使用隔离数据库卷核对完整迁移、认证、全部文档与资源,以及不重建内容更新和重启持久化;通过后标记镜像为 verified。
scripts/deploy.py确认仍为最新 main,备份并初始化文档数据,使用--no-build部署;失败时回退上一版镜像。
pnpm check、pnpm typecheck、pnpm test:auth、pnpm test:research、pnpm test:docs、pnpm figures、pnpm build 和 pnpm dev 等命令涉及编译、构建或产物生成,不在本机运行。图形源码与生成资源保持可复现,前端继续使用 Pages Router。
根 README 等非构建输入变更不要求重新构建;数据库正文和目录更新也不触发构建。保留 pnpm、uv、Next.js 与 SWC 缓存,不清理共享 Docker 缓存或卷,不修改共享 Runner 或其他项目。
部署位置与仓库
| 项目 | 位置或默认值 |
|---|---|
| Compose 项目 | endorphin-tabularium-docs |
| 容器监听 | 0.0.0.0:3000 |
| 服务器入口 | 默认端口 18135,由生产配置决定 |
| 生产配置 | /etc/endorphin-tabularium-docs/production.env |
| 发布记录与当前版本 | /opt/endorphin-tabularium-docs/releases/<commit> 与 current |
| 健康检查 | /api/health |
服务入口使用 TABULARIUM_BIND、TABULARIUM_PORT 配置。部署前检查端口是否空闲或属于本服务,不停止其他服务抢占端口;本项目不自动修改防火墙、域名或反向代理。
仓库:endorphin-tabularium-docs。Git 操作仅使用 codex-bot 专用 SSH 身份与仓库级提交配置;Actions 检出使用 Gitea 内置任务令牌且不保存凭据。