跳至正文
App — Project NOMAD 源码分析:离线内容、容器编排与本地 AI

Project NOMAD 源码分析:离线内容、容器编排与本地 AI

Project NOMAD 项目仓库

AI 参与说明(Agent:Codex):本文由 Codex 根据固定提交的公开源码协助研究、撰写与校验,涵盖架构、调用链和最小缺陷复现。执行入口为 Codex Desktop;完整模型标识与 reasoning effort 未取得任务级运行记录。

Project NOMAD 把离线阅读、地图、教育应用和本地 AI 组织成一台可通过浏览器使用的知识服务器。源码可以分三层理解:管理设备和应用的控制层、保存原始内容的本地存储层,以及从内容生成向量索引的 AI 检索层。项目将这些组件的安装、状态、更新和恢复连接起来。

英文术语中文名称简要解释
Command Center控制中心NOMAD 用来管理应用、内容和设备状态的界面与 API
Inertia保留原名将后端页面路由及数据交给 React 渲染的连接层
BullMQ保留原名基于 Redis 分派和执行后台任务的队列库
RAG检索增强生成先查找相关资料,再把资料放进模型输入中生成回答
embedding嵌入将文本转换成可用于相似度检索的数值向量
ZIM离线归档格式将网页和相关资源打包供离线阅读的文件格式
PMTiles地图瓦片归档格式把地图瓦片放进单个文件,并按需读取其中的部分数据
sidecar边车服务与主应用配合运行、承担独立职责的辅助服务

本文面向希望理解架构、排查问题或参与二次开发的读者,重点分析启动过程、容器生命周期、内容下载、RAG 和前端通信。分析基线为 2026 年 10 月 26 日读取的 main 提交 54ee30a44902e5c252b477f8685c26ec3064ab72,根目录版本为 1.35.1,许可证为 Apache 2.0。下文源码链接均固定到该提交;部署与性能判断限定于源码行为,个别实现问题另列最小复现条件。

项目定位与代码边界

NOMAD implements the Command Center, unified APIs, content management, and chat and RAG orchestration. 阅读器、学习平台和推理引擎等能力来自独立应用;NOMAD 负责把它们接到统一的设备管理流程中。

能力NOMAD 负责什么外部组件负责什么
应用管理目录、预检、安装、端口、状态、更新Docker 运行各应用
离线资料下载 ZIM、维护书库、选择内容、管理版本Kiwix 提供阅读体验
AI 助手会话、提示词、检索、上下文预算、引用、流式输出Ollama 或 OpenAI 兼容服务推理,Qdrant 保存向量
离线地图下载、地区提取、本地资源服务PMTiles 保存地图,MapLibre 渲染
教育安装和启动 KolibriKolibri 管理课程、学习用户和进度
药品资料数据摄取、SQL 搜索、精选条件目录openFDA 提供原始标签数据

预置应用以数据库 seed 记录表达,包含镜像、Docker 配置、依赖和展示信息。增加普通容器应用通常先改目录;需要专属初始化时,再扩展编排服务。这里没有统一的第三方插件执行协议。参见 预置服务目录 和 Service 模型。

项目所说的离线优先,是相关镜像、资料、模型、地图字体等准备好之后可以本地消费。刷新目录、下载新资源、拉取更新、从远端全球地图提取地区,以及使用外部模型地址,仍涉及网络。清单缓存 会先尝试远端刷新、再读本地缓存;联网状态检查 也说明离线优先不等于没有网络请求。

总体架构与源码地图

后端位于 admin/,采用 TypeScript、AdonisJS 6 和 Lucid ORM;前端通过 Inertia 连接 React 19,使用 Vite、Tailwind CSS 和 TanStack React Query。业务数据库是 MySQL,异步任务由 BullMQ 与 Redis 承担。主镜像使用 Node 22 的多阶段构建。版本与依赖以 admin/package.json、Dockerfile 为准。

flowchart TD
  B["浏览器 React 与 Inertia"] --> W["Command Center HTTP 进程"]
  B --> A["Kiwix Kolibri 等应用界面"]
  W --> DB["MySQL 业务状态"]
  W --> R["Redis 队列与事件"]
  R <--> J["BullMQ Worker 进程"]
  W --> D["宿主 Docker daemon"]
  D --> A
  D --> O["Ollama"]
  D --> Q["Qdrant"]
  W --> O
  W --> Q
  J --> O
  J --> Q
  J --> DB
  J --> F["宿主持久化 storage"]
  A --> F
  W --> U["共享更新卷"]
  U --> UP["Updater sidecar"]
  UP --> D

HTTP 与 Worker 是同一个 admin 容器里的两个进程;可选应用则由宿主 Docker 创建,和 admin 并列运行。核心 Compose 只声明 admin、MySQL、Redis、Dozzle、Updater 和磁盘采集容器,应用目录里的其他工具由管理端动态创建。证据见 进程启动、核心 Compose 和 Docker socket 连接。

路径阅读重点
install/安装、Compose、启动、更新及 sidecar
admin/bin/server.tsHTTP 启动与启动后校正
admin/adonisrc.ts 和 admin/start/Provider、中间件、页面与 API 路由
admin/app/controllers/参数校验、调用服务、组织响应
admin/app/services/容器、内容、RAG、系统管理的核心逻辑
admin/app/jobs/ 和 admin/commands/queue/可恢复长任务及 Worker
admin/app/models/ 和 admin/database/持久状态、迁移、应用目录
admin/inertia/页面、组件、请求、轮询与流式交互
admin/types/ 和 admin/constants/前后端共用类型与策略常量
collections/精选内容与配置清单
admin/tests/单元测试、功能测试及 RAG 质量评估

业务逻辑主要集中在 DockerService、RagService 和 BenchmarkService 等大服务中。阅读时沿一次操作的调用链前进,更容易看清这些服务如何协作。

启动流程与请求入口

官方安装脚本检查系统与依赖、按需安装 Docker 并检查 Compose v2、配置 GPU 支持、生成目录和凭据,再启动核心服务。当前安装路径主要面向 Debian 系统及 x86_64;虽然部分镜像构建逻辑包含 ARM64 分支,不能据此推断全部可选应用均已支持 ARM。参见 架构检查 和 安装主流程。

管理容器启动后的顺序如下:

  1. 创建日志及知识库上传目录,等待 Redis 可连接。
  2. 执行数据库迁移。
  3. 执行 ServiceSeeder,补充新应用并同步预置配置。
  4. 后台启动 node ace queue:work --all。
  5. 前台运行 node bin/server.js,启动 HTTP 服务。
  6. HTTP 应用就绪后,从文件系统校正内容安装账本。

入口见 entrypoint.sh 与 server.ts。文件系统校正失败会被捕获,避免内容登记异常直接阻止管理界面启动。

Seeder 每次启动都会运行,但它会跳过自定义应用和用户已修改的预置应用;对于已安装的预置应用,更新目录配置时还会保留合适的宿主端口映射。这是一种目录升级与用户配置并存的策略,见 ServiceSeeder.run。

普通请求沿以下路径流动:

text
浏览器
  → start/routes.ts
  → Controller 的 Vine 参数校验
  → Service 组织业务操作
  → Lucid / Docker / 文件系统 / 队列
  → Inertia 页面或 JSON 响应

页面路由和 API 路由共存于 routes.ts。例如 HomeController.home 读取已安装服务,通过 inertia.render('home', props) 交给 React。全局版本、运行环境和助手名称则由 sharedData 统一注入。React 的 SSR 明确关闭,因此这里是服务端组织页面数据、客户端渲染 React。

数据模型与跨存储一致性

项目同时维护关系数据库、文件系统、Redis 与向量库。它们各有职责,判断“内容是否可用”时不能只看某一个状态。

存储主要内容能说明什么
MySQL services应用配置、installed、安装状态、更新策略目录与管理状态,不等同于当前正在运行
MySQL installed_resources资源 ID、类型、版本、路径、大小内容已安装的账本
MySQL kb_ingest_state摄取决策、索引状态、chunk 数量、错误文件在知识库中的处理结果
MySQL 会话与 KV 表消息、来源、设置、更新状态持久业务数据
文件系统ZIM、PMTiles、上传文件、模型和各应用数据原始内容与应用持久数据
RedisBullMQ 任务、进度、取消标志、Transmit 事件执行与跨进程通信状态
Qdrantchunk 向量、正文与来源等 payloadAI 语义检索所需的派生索引

InstalledResource 的类型包括 zim、map、dataset;KbIngestState 则独立记录文件是否选择索引、索引是否完成。把两者拆开,解决了“下载成功就算进入 AI 知识库”和“存在一些向量就算完整索引”的歧义。

应用也有类似区别:容器停止后仍可能是已安装。容器与数据库同步 依据容器存在性修正 installed,并为远程 Ollama、链接卡片等情况保留例外。

一次内容安装可能依次修改文件、MySQL、Kiwix XML 和 Qdrant,这些修改没有共同事务。项目采用原子文件替换、幂等更新及重扫来补偿:reconcileFromFilesystem 从磁盘修正账本,Kiwix 可重建书库,知识库单独重试。因此,“文件已下载”“可以阅读”“已完整索引”是不同的结果。

容器安装与应用生命周期

点击安装时,调用链为:

text
SystemController.installService
  → DockerService.createContainerPreflight
  → 标记 installing 并立即返回
  → 异步 _createContainer
  → 安装依赖 → 获取镜像 → 应用初始化与 GPU 配置
  → Docker create → start
  → 更新 Service → 广播完成或错误

Controller 入口 校验请求后,createContainerPreflight 检查安装状态,将服务加入内存集合并写入数据库,再启动异步安装。Application installation runs in the Web process, not in a durable BullMQ job. 安装进度通过 Transmit 推送,下载和索引任务则由独立 Worker 执行。

_createContainer 根据 depends_on 递归安装依赖,已有镜像可直接使用;否则拉取镜像并等待实际完成。随后针对 Kiwix、Calibre 等应用执行初始化,配置 Ollama 的 NVIDIA 或 AMD 设备,再创建容器。AMD 设备不可用时还存在清理后回退 CPU 的路径。

管理端通过宿主 socket 操作 Docker,动态应用挂到管理网络中。存储路径要转换成宿主能理解的绝对路径,因为 Docker daemon 运行在宿主,不能把 admin 容器内部路径直接当作 bind mount 来源。这也是迁移 storage 时必须保持各处配置一致的原因,见 存储与 socket 挂载。

自定义应用复用相同生命周期。custom_app_guard 会检查镜像和挂载,拒绝 Docker socket、关键系统路径等明显危险配置;资源输入由 validator 校验,再由 Controller 转成 Docker 限额。它提供的是受约束的容器表单,并不支持把任意 Compose 文件原样导入。镜像仓库提示也不构成镜像内容签名验证。

这种实现适合单设备管理,安装流程直观且可加入应用专属处理;恢复能力则依赖状态校正。按静态调用链推断,Web 进程在安装中途退出,会中断尚未完成的 JavaScript 操作,而数据库 installing 标记可能已经写入。实例内集合也不能作为跨进程互斥锁。若将来需要更强的崩溃恢复或并发管理,应用安装应有持久任务 ID、明确阶段和可重入的恢复逻辑。

离线内容下载与本地服务

清单和安装账本

collections/ 的 JSON 定义分类、资源、推荐层级和版本。CollectionManifestService 从仓库 main 分支拉取清单,经 Vine 校验后缓存到 MySQL。tier 可通过 includesTier 继承资源,安装状态则按资源 ID 与 InstalledResource 比对。

应用镜像固定了版本,运行时清单仍可能跟随 main 前进。本地缓存改善断网可用性,但旧应用与未来清单语义的兼容性仍要单独考虑。需要严格复现离线镜像时,应同时记录镜像版本、清单版本和内容摘要。

一份 ZIM 如何变成可阅读的书库内容

flowchart TD
  C["校验 URL 与资源"] --> Q["下载任务"]
  Q --> T["写临时文件并续传"]
  T --> F["rename 为最终 ZIM"]
  F --> I["更新安装账本"]
  I --> K["重建 Kiwix XML"]
  K --> R["Kiwix 热加载并提供阅读"]
  I --> E["按用户策略决定是否摄取"]
  E --> V["Embedding 与 Qdrant"]

代表性入口是 ZimController.downloadRemote → ZimService.downloadRemote → RunDownloadJob.dispatch。HTTP 请求只提交任务,长时间下载交给 Worker。

底层 doResumableDownload 先通过 HEAD 获取文件长度和 Range 能力,流写入 .tmp,支持断点续传。远端不支持 Range、忽略 Range 返回完整文件、或本地 partial 大于远端时,都有重新下载的分支;五分钟没有数据会视作停滞。成功后 rename 到最终文件名,避免阅读器扫描到未完成的归档。任务按 URL 生成稳定 ID,并配置指数退避。

文件完成后,任务写入 InstalledResource,再调用 ZIM 回调。KiwixLibraryService.rebuildFromDisk 使用 libzim 读取归档元数据,生成指向 Kiwix 容器 /data/ 的书库 XML,采用临时文件与 rename 更新。Kiwix 的 library monitor 能热加载书库,手工复制资料后也能通过 rescan 进入同一流程。

下载后的 AI 摄取取决于是否安装 AI、是否跳过 embedding,以及用户选择的自动或手动策略,见 下载后的摄取决策。已有旧版本的更新还会继承用户先前的索引选择。由此,浏览资料与让模型检索资料可以独立使用。

地图和教育应用

地图由本地 PMTiles、样式、字体和 sprite 组成。MapService.generateStylesJSON 把样式资源地址改到本机;前端 MapLibre 通过 PMTiles Protocol 读取归档,静态服务支持 HTTP Range。除了完整下载,地区提取还会由任务调用 pmtiles extract,从远端全球归档取所需区域,见 地区提取进程。这条提取路径需要联网,地图下载完成后的浏览则使用本地资源。

Kolibri 的课程与学习记录属于其自身业务。当前目录使用独立 Gen2 实例及数据卷;旧版保留为 deprecated,避免强行跨越多个数据版本就地迁移。配置见 Kolibri Gen2 目录项。

AI 模型接入与知识摄取

双协议模型适配

AI 主链由本仓库的 TypeScript 服务自行组织。OllamaService 优先使用用户配置的远端地址,否则查找本地 Ollama 容器;通过 /api/tags 等响应识别原生后端,并支持 OpenAI 兼容调用。

兼容协议存在能力差异:对话可以转到 /v1/chat/completions,但 Ollama 的 num_ctx、模型管理和部分结构化输出约束不能简单照搬。连接兼容服务不代表模型下载、窗口控制和 JSON grammar 都获得同等支持。具体转换位于 原生与兼容参数适配。

默认 embedding 模型是 nomic-embed-text:v1.5,Qdrant 的物理 collection 为 nomad_knowledge_base,向量固定为 768 维、Cosine 距离。界面中的 collection 是 payload 中的逻辑分类。见 默认 embedding 模型 与 向量库配置。因此任意替换 embedding 模型可能需要重建 schema 与索引,不能只改聊天模型名称。

文档处理与索引

上传入口 RagController.upload 清理文件名并添加随机后缀,把文件放入 storage/kb_uploads,提交 EmbedFileJob 后返回 202。摄取任务与用户请求分离,避免大文件解析占住 HTTP 请求。

processAndEmbedFile 按文件类型提取文本:DOCX 使用 mammoth,EPUB 按 OPF spine 顺序读取正文,PDF 优先文本解析,图片及低文本量 PDF 使用 OCR,ZIM 则按文章和章节批次提取。OCR 当前显式使用英语模型;PDF 的 OCR 触发条件是全文件文本量不足,因此不能推断中文扫描件或混合 PDF 的所有图中文字都会被识别。

embedAndStoreText 分块后以 search_document: 前缀计算向量,每批处理 8 个 chunks,并将正文、来源、chunk 序号、分类及 ZIM 元信息写入 point payload。常量中的目标块长需要经过字符估算,不能当成精确 tokenizer 的固定 token 数。

大 ZIM 采用 continuation job,按已经消费的文章数推进 offset。即使某批文章没有有效正文,也能继续向后推进;最终把跨批累计结果写入 KbIngestState。相关逻辑见 ZIM 批次推进 与 continuation 和完成状态。这样,临时任务历史被清理后,知识库状态仍有持久记录。

原始文件可以长期保存,解析和 embedding 可以重做。向量库承担检索,安装账本只记录内容安装结果,各层分别处理自己的状态。

一次 AI 对话的检索与生成

从问题到候选片段

聊天入口 OllamaController.chat 校验模型、会话、图片等输入,再调用生产和质量评估共用的 RagPipelineService.buildPrompt。

sequenceDiagram
  participant UI as 浏览器
  participant API as OllamaController
  participant P as RagPipeline
  participant Q as Qdrant
  participant M as 模型服务
  participant DB as MySQL
  UI->>API: 消息与 sessionId
  API->>P: buildPrompt
  opt 追问需要补全
    P->>M: 改写为独立检索问题
  end
  P->>Q: 向量召回
  Q-->>P: chunks 与元数据
  P->>P: 重排 相关性过滤 上下文预算
  P-->>API: messages 与 trace
  API->>DB: 保存用户消息
  API->>M: 流式生成
  M-->>API: 文本与 thinking chunks
  API-->>UI: SSE 格式事件与 sources
  API->>DB: 流处理正常结束后保存助手消息

提示词管线先分离 system 与对话,按条件加入默认提示词,并把用户管理的 NOMAD.md 放入 system blocks。该文件通过临时文件加 rename 保存,作为持久行为配置,见 NomadMdService。

首轮问题可直接检索,追问则可利用近期非 system 消息补全指代;改写失败回退到原问题。随后 searchSimilarDocuments 查询 Qdrant,默认先召回 3 × topK 候选,再结合词项重叠、问题词、标题命中重排,并对重复来源作多样性惩罚。

Retrieval uses dense vector search followed by lexical and title-based reranking. 源码注释中的 hybrid 指这一组合,没有 BM25 与向量检索两套独立召回结果合并;重排也不是单独训练的神经 reranker。其直接后果是:没有进入向量候选集的段落,不能仅凭关键词加权被补召回。实现见 重排与来源多样性。

此外还可开启 LLM 相关性判断,过滤整体偏题的候选;默认不开启,判断失败时保留结果,见 RelevanceJudgeService。

上下文预算

planPrompt 将可用窗口分配给 system、本轮问题、完整 RAG chunks、历史轮次和回答。它为回答预留空间,优先保护本轮输入,按预算保留完整片段,再从最近历史向前保留完整对话轮次。放不下的 RAG chunk 会跳过,后面较短的片段仍有机会进入。

窗口也不是简单写死一个数字。ContextWindowService 会结合模型元数据、硬件内存及用户上限确定窗口;TokenCalibrationService 再用实际 prompt token 用量校准估算。非原生后端的窗口能力较难探知,仍需要保守估算或用户配置。

在本地设备上,模型参数量、VRAM、内容长度和回答空间相互制约。检索结果越多,会占用更多输入窗口,并不必然改善回答。

流式输出与持久化

服务端发送的是 data: JSON\n\n 格式事件。浏览器通过 fetch 和 ReadableStream 逐块解析,不是 EventSource,也不经过 Transmit。正文、thinking、sources 和 done_reason 分别处理,长度截断时可提供续写入口。两端实现见 服务端生成流 与 浏览器解析流。

有 sessionId 时,后端先保存用户消息,流处理正常结束后保存助手正文与来源;首轮还会异步生成标题。达到生成长度上限的回答也会保存。断连取消会提前返回,因此部分助手回答不会按完整回答落库;图片和 thinking 也没有对应的 ChatMessage 持久字段。参见 消息保存时序 与 ChatMessage。浏览器当前展示与刷新后的历史回放,因而不是完全相同的记录。

前端状态与通信机制

Inertia 负责页面入口和首屏数据,React Query 管理交互期服务端状态,主题、通知和弹窗由 Provider 统一组织。入口根据页面名加载相应 React 页面,见 createInertiaApp。普通 API 请求集中在同源 Axios 客户端,类型来自共用的 admin/types/。

场景通信方式主要原因
页面进入和局部页面刷新Inertia复用后端路由与页面 props
文件下载任务列表React Query 轮询读取 Redis 中可恢复的任务状态
服务安装、模型下载、跑分事件Transmit SSE跨进程发送阶段与日志事件
聊天文本生成fetch 读取 SSE 格式响应同一次 POST 请求持续消费模型输出

普通下载活跃时约每 2 秒查询,空闲时约每 3 秒查询,见 useDownloads。Transmit 使用 Redis transport,使 Worker 发出的事件可到达 HTTP 进程持有的浏览器连接,配置见 Transmit Redis transport。

队列管理也专门处理连接复用:QueueService 是进程内单例,按名称缓存 Queue;共享 ioredis 延迟连接并配置退避,避免频繁创建队列对象造成连接膨胀。

Worker 采用不同并发和 stall 策略。普通下载可较宽松地恢复,embedding 则对 stalled job 更保守,以避免重复 continuation 链。见 各队列并发 与 stalled 恢复策略。可恢复执行需要考虑具体任务的幂等性,不能统一提高重试次数。

In BullMQ v5, maxStalledCount limits stalled-job recovery, while lockDuration controls a renewable processing lock. 后者不是任务的硬超时;前者超过次数后,任务会转入 failed。区分这两个参数,才能判断任务是在正常长时间处理,还是失去锁后反复恢复。参见 BullMQ v5 WorkerOptions。

核心更新与应用更新

应用更新和 NOMAD 自更新采用两条机制,因为它们的执行者与生命周期不同。

DockerService.updateContainer 先拉新镜像,停止并把原容器改名为 _old,再创建替代容器。创建或启动失败时尝试恢复旧容器,成功则移除旧容器。但它只短暂等待并检查 Running,不验证业务 HTTP 就绪;共用的数据卷也没有回滚,所以旧镜像恢复不等于数据恢复。

核心更新由 SystemUpdateService.requestUpdate 写入共享卷中的请求文件,独立 Updater 读取后修改 Compose 镜像标签、拉取并重建核心服务。这样 admin 消失后,更新执行者仍然存在。执行逻辑见 perform_update。

自动更新又分别区分核心、应用和内容,各有开关,并设置预检与失败退避。核心和应用共享窗口与冷却配置,内容更新使用独立配置。核心决策会排除跨 major、草稿和预发布版本,并把联网失败当作跳过,避免离线设备仅因没有网络就累计故障。evaluate 把决策与实际请求分开,dry-run 可以复用同一决策管线。

这里的 success 有明确边界:自动更新记录结果 在请求文件写入成功时就记录 success,sidecar 的部署动作发生在之后。判断新版本是否真正可用,还要结合 sidecar 状态、当前版本及应用就绪情况。核心 sidecar 也没有应用更新那样保留旧容器的回退流程。

其他模块体现的架构取舍

药品资料采用 openFDA 分片下载、独立 ingest 队列与 MySQL 索引。入库按批处理,利用唯一键及 upsert 去重;查询优先 FULLTEXT,短词或全文检索失败时回退 LIKE。它直接使用结构化 SQL,见 药品批次入库 与 药品搜索。

精选 conditions、自然疗法和居家处理信息从 collections/ JSON 生成 TypeScript,构建镜像时重新生成,以减少源数据和提交的生成文件漂移,见 精选数据构建流程。这表明项目会根据数据性质选择静态目录、SQL 或向量检索。

Benchmark 复用容器能力执行 CPU、内存、磁盘和 AI 测试。Benchmark telemetry is broadcast to the local UI; leaderboard submission is a separate operation. BenchmarkTelemetrySampler 在跑分期间采样,榜单上传则由独立的 submit 调用触发。参见 本地采样 与 榜单提交。

可选的 Translated Library 则以独立 Python 代理保留 Kiwix 路径,对 HTML 正文进行本地翻译;模型齐全后可脱网,失败时退回原文。当前实现仍有架构和语言范围限制。它展示了在应用容器外增加专用代理的扩展方式,见 翻译代理设计。

工程质量与需要注意的实现问题

测试资产与自动检查

该提交有 59 个 unit spec 文件、1 个 functional spec,另外提供 standalone 数据检查和 RAG corpus、goldens、baselines。文件数量不等于测试用例数或覆盖率。测试涉及下载与 URL 规则、容器 guard、流取消、上下文预算、引用和自动更新等边界。

运行入口需要区分:npm run test:unit 使用 Node test runner;npm test 进入 Ace/Japa 配置;npm run test:eval 是评估相关单元测试子集。真正的检索和生成质量评估使用单独 Ace 命令,并可比较 oracle、端到端、无检索和 mock 模式。配置见 测试 scripts 与 RAG 评估流程。

生产聊天与评估共用 buildPrompt 是优点,有助于避免评估路径偏离实际逻辑。不过 PR workflow 只显式运行依赖安装和构建,没有独立的 unit、lint、typecheck 门禁;现有测试资产不等于每次 PR 都强制执行。

已复现的来源错配

上下文预算可能跳过大 chunk 并保留后面的小 chunk,但 pipeline 用以下方式重建实际注入集合:

typescript
trace.injected = candidateDocs.slice(0, plan.trace.chunksKept)

前者的实际保留集合未必是候选前缀,后者却只按数量取前缀。预算选择循环、trace.injected 和 引用生成 连接起来,便形成来源错配。

最小验证给候选按顺序放入大段 large.pdf 和小段 small.pdf,窗口设为 1024。执行原预算函数后,只保留 small;按 pipeline 的切片逻辑,来源却报告 large:

json
{
  "chunksKept": 1,
  "actualPromptIncludesLarge": false,
  "actualPromptIncludesSmall": true,
  "reportedSources": ["large.pdf"]
}

该验证覆盖预算与来源集合的纯逻辑,不代表整个部署的端到端测试。修正方向是让预算器返回实际保留的 chunks 或稳定索引,直接据此构造引用;仅返回保留数量不足以表达选择结果。

下载后的处理没有共同事务

下载完成回调处理 捕获后处理错误后仍可把进度设为 100%,书库重建也存在日志化错误的分支。因此“下载完成”不能严格解释为书库登记和索引全部成功。通用下载器还主要依赖长度与 Range 语义,没有在该函数执行强摘要校验;同尺寸最终文件会提前返回,见 已有文件快速返回。

项目已有 rescan、reconcile 和重新索引入口。进一步完善时,应分别保存下载、注册和索引的终态,让用户能只重试失败阶段。

安装和更新的维护边界

安装脚本在生成新凭据时,若已存在 MySQL 数据目录,会直接删除该目录。代码位于 已有 MySQL 目录处理。重跑安装脚本不能当作无损升级或修复操作。

应用版本更新创建新容器时只复制部分 HostConfig,未完整带回 Memory、NanoCpus 等字段,见 更新后的容器配置。按静态配置路径判断,原有限额可能在该更新路径后丢失。后续宜复用统一的配置重建方法,并测试更新前后的约束一致性。

安全依赖明确的网络信任边界

项目安全模型 明确把无鉴权作为设计选择,管理端以可信本地网络为边界。中间件配置 没有用户权限隔离,CSP、CSRF 和 HSTS 在 Shield 配置 中关闭,CORS 设置也较宽松。

与此同时,admin 持有宿主 Docker socket。能访问管理 API 的人可以触发设备级容器操作,因此管理界面与普通离线阅读界面的权限意义不同。对外或多人使用时,需要在网络入口设置访问控制;只给 admin 增加登录页,也不会自动保护各应用独立暴露的端口。

项目仍使用 Vine 参数校验、路径 guard、下载地址规则及云 metadata 地址限制;这些措施与支持本地模型、局域网内容源的需求并存。这里的结论是部署信任边界较强,不能仅凭配置就宣称已验证某条跨源攻击或远程执行漏洞。

二次开发的阅读顺序与切入点

以下命令使用 Git 取得本文固定的提交,再查看路由与核心服务。请在尚无 project-nomad-reading 目录的位置执行;git rev-parse 应输出下方的完整 SHA。

bash
git init project-nomad-reading
git -C project-nomad-reading remote add origin https://github.com/Crosstalk-Solutions/project-nomad.git
git -C project-nomad-reading fetch --depth=1 origin 54ee30a44902e5c252b477f8685c26ec3064ab72
git -C project-nomad-reading switch --detach FETCH_HEAD
git -C project-nomad-reading rev-parse HEAD
git -C project-nomad-reading show HEAD:admin/start/routes.ts
git -C project-nomad-reading show HEAD:admin/app/services/rag_pipeline_service.ts

预期提交:54ee30a44902e5c252b477f8685c26ec3064ab72。这组命令只取得并阅读源码,不会运行项目安装脚本或启动应用。

建议先沿三条调用链阅读,再决定扩展位置:

  1. 设备和应用:routes.ts → SystemController → DockerService → ServiceSeeder。适合增加目录项、预安装动作、端口与资源规则。
  2. 内容管理:ZimController → ZimService → RunDownloadJob → downloads.ts → KiwixLibraryService。适合增加资源来源、完整性验证及后处理恢复。
  3. AI 检索:OllamaController → RagPipelineService → RagService → planPrompt → OllamaService。适合改进召回、预算、引用和后端兼容性。

贡献代码时,优先补上与变更相关的单元测试,并对照实际入口运行。涉及 Docker、数据迁移、模型行为时,再使用隔离环境做集成验证;纯构建不能代替这些行为检查。

NOMAD 值得借鉴的部分,是把离线内容、宿主容器和本地 AI 连接成可操作的设备产品。下载续传、存储路径、用户选择、索引状态和更新执行者都进入了同一套管理流程。这些选择适合可信环境中的单设备知识服务器;若要发展成多人、可审计、强恢复的平台,持久安装工作流、权限模型、跨阶段结果与更新就绪验证会成为主要改造点。

博客助手

正在打开博客助手…