上一件事是给 Cloudreve 接上本机的 NPU 嵌入模型,做完之后会出现一个新的诱惑:既然这台盒子上已经常驻了一个能出 512 维向量的服务,那别的程序是不是也能直接用它?
这篇记录的就是这一步。两个新"客户"先后接进同一个进程:一是 Windows 上的 DSH graph-memory 插件(要求零源码修改),二是自建的笔记应用。三个调用方共用同一个 NPU 模型、同一把推理锁、同一个进程,但彼此之间在端口、密钥和来源白名单上完全隔离——Cloudreve 那条链路的技术参数一个都没动。
为什么不新起一个服务
最直观的做法是复制一份服务、换个端口、再加载一次模型。没有这么做,原因很实际:
NPU 只有一份,模型已经常驻在内存里。再起一份意味着再占一份模型内存和虚拟环境开销。
三个调用方都是低频请求,共享一个进程完全够用,没必要各自持锁。
多一个常驻进程就多一份要维护、要归档、要绑定小核、要写 systemd 单元的东西。
所以最终形态是:同一个 cloudreve-embedding 进程,两个监听地址。
两个监听共享同一个模型、同一把锁,全局最多 8 个并发请求。排队超时是分开的:DSH 入口 5 秒,Cloudreve 原入口保持 15 秒。没有另加载模型,也没有启动新容器。
同一进程怎么隔离开
共享一个进程不等于共享一切,隔离靠三样东西:网络来源、凭据、监听地址。
只监听指定内网地址,按单 IP 放行
新的 18091 入口不使用通配监听,只绑定指定的 IPv4 内网地址。应用层来源白名单是 192.168.10.202/32;systemd 侧在这个基础上增加该单 IP 的允许范围,同时保留原有的 FTS 网桥与 localhost。笔记应用接入时再增加 172.24.0.2/32。
应用白名单必须是明确的 RFC1918 单 IP
/32,不扩大成整网段。Cloudreve 的私网网桥设置不改,两套授权并存。
这一步在 systemd 里就是一个 drop-in。仓库里的源文件是 /root/AI/cloudreve-embedding/dsh.conf,此时只授权 Windows 这一台:
[Service]
# DSH专用内网入口,和Cloudreve共享模型;来源为当前Windows主机。
Environment=DSH_BIND_ADDRESS=192.168.10.62
Environment=DSH_BIND_PORT=18091
Environment=DSH_AUTH_FILE=/opt/DockerDate/cloudreve-embedding/dsh_key
Environment=DSH_ALLOWED_NETWORKS=192.168.10.202/32
IPAddressAllow=192.168.10.202/32
独立认证密钥
DSH 与新客户使用独立密钥,路径 /opt/DockerDate/cloudreve-embedding/dsh_key,属主是专用服务用户、权限 400,不进 Git 也不进普通归档。Cloudreve 原来的 Bearer 密钥保持不变。
需要说清一点:HTTP 没有 TLS,认证和来源白名单都不等于加密。因此禁止把它做公网映射或通配监听;如果链路不可信,必须自己再套 HTTPS、SSH 隧道或 VPN。
两套编码策略互不干扰
这是最容易踩的一个坑。原版 DSH 插件并不发送"查询 / 文档"模式信息,而 Cloudreve 那条链路用的是非对称编码(查询和文档加不同的指令前缀)。如果让同一个入口按正文内容去猜类型,结果会不可控。
解决办法是给 DSH 入口一条统一的、无指令的对称编码——调用引擎时固定用 document 模式,不去猜。关键设计是:模型标识里带策略版本,这里用的名字是 bge-small-zh-v1.5-rknn-symmetric-v1。一旦编码策略变化,就必须换一个新的模型标识并重算向量,避免新旧向量混在同一个索引里。
客户一:DSH graph-memory 插件,只改配置
需求很明确:插件要接本机 NPU 服务,但不改插件源码。原本尝试过的插件 input_type 补丁全部撤回,没有构建也没有安装。
插件侧改了什么
Windows 上只修改了 profile:graph-memory 条目的 embedding 指向上述 baseURL、模型名 bge-small-zh-v1.5-rknn-symmetric-v1、512 维,凭据通过环境变量引用 GRAPH_MEMORY_RK3588_API_KEY。密钥只存在本机凭据库里。旧的百炼凭据和用户已有的环境变量都保留,不需要覆盖。压缩开关仍为 false,抽取用的 LLM 不变(它仍可能使用云端,这一点没有被改动)。
阈值方面,semanticScoreThreshold 校准为 0.58,仍可由原环境变量覆盖。settings 文件放在插件仓库之外,路径是 C:/Users/17737/Code/other/DSH/plugins/docs/graph-memory-rk3588-settings.json,配套的迁移 / 评估运维脚本也在这个目录下;迁移脚本默认 dry-run,加 --apply 才会备份并原子写入向量,源数据在并发变化时会回滚。
落到配置上就是这样一个片段(即插件仓库之外的 settings 文件内容,密钥字段只写环境变量名,本机凭据库里的真实值不出现在文档、仓库和归档里):
{
"baseURL": "http://192.168.10.62:18091/v1",
"model": "bge-small-zh-v1.5-rknn-symmetric-v1",
"dimensions": 512,
"semanticScoreThreshold": 0.58,
"apiKeyEnv": "GRAPH_MEMORY_RK3588_API_KEY"
}
其中 apiKeyEnv 指向凭据库中的变量名 GRAPH_MEMORY_RK3588_API_KEY,值只存在本机;semanticScoreThreshold 是 0.58 这个校准值。
已有向量怎么迁移
已有 223 条摘要全部转为 512 维:原 217 条 1024 维被重算,另外补齐缺失的 6 条。
其他非向量表内容逐表 hash 一致,
quick_check=ok,短事务原子提交。没有重新抽取 LLM 摘要,只是重算向量。
实验数据在本机备份:~/.dsh/backups/graph-memory-rk3588-20261009-221125,以及 graph-memory-rk3588-migrate-2026-10-09T14-24-35-680Z。这两个包含凭据、数据库和摘要报告,不进入普通服务器归档。
验证到什么程度
8 项双接口局部测试通过:原版请求、对称类型、Cloudreve 默认策略、独立密钥隔离、非法模型 / 维度 / 正文、来源白名单,以及共享实例。
Windows 原版插件的 createEmbedFn 探针、query、db 均成功;原版 DSH 适配器的隔离内存库初始化 vector-ready。没有启动第二个 DSH 服务。
Cloudreve 侧同时复验:554 分块 / 554 向量、待处理任务 0、混合命中 5 条、用户过滤通过、HTTP 200,认证 / 批量 / 长文尾部回归通过。
服务重启后两个监听和全部 5 个线程都落在 CPU0–3 通过,实测服务 cgroup 约 189 MiB。内存上限 1 GiB 不代表实际用量。
一个必须说明的状态
Windows 当前的 DSH 进程没有重启。配置和历史向量都已准备好,但需要用户重启 DSH 并刷新页面才生效。不能把没有观察到的宿主热重载当作成功。旧进程如果新增了 1024 维向量,重启后可以用补齐脚本重跑。
运维与回退
systemctl status cloudreve-embedding --no-pager
systemctl show cloudreve-embedding -p IPAddressAllow -p CPUAffinity -p MemoryCurrent
ss -lntp '( sport = :18090 or sport = :18091 )'
docker exec cloudreve-fts-meilisearch sh -c 'wget -T 5 -qO- http://172.27.0.1:18090/health'
cd /root/AI/cloudreve-embedding
PYTHONDONTWRITEBYTECODE=1 .venv/bin/python tests/dsh_compat.py
.venv/bin/python check_index.py
修改来源地址前,先备份 /etc/systemd/system/cloudreve-embedding.service.d/dsh.conf,再同步 DSH_ALLOWED_NETWORKS 和 IPAddressAllow,然后 daemon-reload / restart 并复验。
回退时服务器原源码有就地备份 /opt/cloudreve-embedding/server.py.bak.20261009-221125。顺序是:先停止 DSH 使用远程入口,再备份移走 dsh.conf、恢复原源码、daemon-reload / restart。不要停用整个 NPU 服务,否则会影响 Cloudreve。Windows 回退只需合并旧的 embedding / 阈值配置,并恢复对应旧向量或重算新增摘要;禁止用整个旧数据库覆盖,那会丢掉新记忆。
客户二:自建笔记应用
笔记应用是 Docker 部署的(nowen-note v1.5.0),这里遇到的坑和 DSH 完全不同。
容器里的 127.0.0.1 不是宿主机
这是最容易误判的一点:请求是由笔记后端发出的,不是浏览器直接发往模型。所以当配置里写 127.0.0.1 时,指的是容器自己,自然连不上宿主机的模型服务。
修复分三步:
原
nowen-note_nowen-network网桥172.24.0.0/16保留不重建;Compose 以外部引用的方式使用这个既有网络,笔记容器固定为172.24.0.2。内部的网络、端口、镜像、持久目录和 CPU0–3 / 1 GiB 限制都保留。在
dsh.conf的 DSH_ALLOWED_NETWORKS 中增加172.24.0.2/32,IPAddressAllow 同样增加这个单 IP。Windows 的192.168.10.202/32和 Cloudreve 私网设置保留,不扩大整网段,也不新增监听。页面此前没有保存局域网地址,数据库里的原值仍是
127.0.0.1。做一致性备份之后,只把该用户的 ai_embedding_url 改成192.168.10.62地址;Key、模型、对话设置和正文都没有修改。
第二步落盘之后,这个 drop-in 就是下面这样(部署路径 /etc/systemd/system/cloudreve-embedding.service.d/dsh.conf,两条 IPAddressAllow 对应两个调用方):
[Service]
# 内网兼容入口与Cloudreve共享模型,仅授权Windows及固定IP的笔记容器。
Environment=DSH_BIND_ADDRESS=192.168.10.62
Environment=DSH_BIND_PORT=18091
Environment=DSH_AUTH_FILE=/opt/DockerDate/cloudreve-embedding/dsh_key
Environment=DSH_ALLOWED_NETWORKS=192.168.10.202/32,172.24.0.2/32
IPAddressAllow=192.168.10.202/32
IPAddressAllow=172.24.0.2/32
关于索引恢复
这一点需要如实记录:按用户后续要求,已经取消索引恢复。助手没有重置失败任务、没有重新入队、没有调用重建接口、也没有清空向量。修复地址时队列、向量和正文指纹都没有变化。收尾时观察到队列与向量已由应用侧发生变化,因此不能宣称最终仍是"8 个失败 / 0 向量"——那些索引操作不是助手执行的。失败索引由用户自行决定何时重建。
验证结果
笔记容器读取已保存配置,调用应用原版 testEmbeddingModel:success=true,512 维,约 215 ms。
笔记容器健康接口 HTTP 200;不带 Key 的嵌入请求 HTTP 401。
Windows 原入口 HTTP 200、512 维;Cloudreve 私网健康接口与网页 HTTP 200。
Compose 重建后静态 IP 为
172.24.0.2,两个网络 ID 都没有变化,其他容器 ID 保持。用户确认真实页面测试可用。
已知限制
模型服务没有 /v1/models 接口,刷新模型列表返回 404 是已知限制;手动填写模型名后再用测试功能即可。本次没有为此增加接口或修改模型 / 应用源码。
模型与笔记共用单 NPU 及推理锁。
HTTP 无 TLS,仅限可信内网,禁止公网映射。
容器重建或更新必须保留静态 IP 配置;应用内更新器将来是否会继承静态 IP 尚未实测,更新后要核查 Docker IPAMConfig 并重测模型调用。
既有网桥是 external=true,重装时需要先准备同名的
172.24.0.0/16网络。另外提醒一句:AI 聊天模型不是这个 Embedding 模型,两者不要混为一谈。
阈值 0.58 这类经验值,能信到什么程度
校准过程中得到的召回表现是:
10 组相关查询里 9 组命中预期主题,1 组因为上下文压缩问题失败。
5 组无关查询返回 0 条记忆。
这是小样本校准,不代表普遍准确率,既可能漏召回也可能误召回。0.58 这个数应当被当作起点而不是定论,索引内容和查询风格变化后需要重新评估。
同样的态度适用于本文所有数字:8 项局部测试通过、223 条摘要 512 维、189 MiB cgroup 内存——它们是当时那一轮的证据,不是长期承诺。
共同的注意事项
保留专用非 root 用户、CPU0–3 绑定、MemoryMax 1 GiB、NPU 设备白名单和原有沙箱设置。
Windows 或 RK 的 IP 变化时需要同步修改配置,建议在路由器上做 DHCP 地址保留。
三个调用方共享单 NPU 和推理锁,任何一方的批量任务都会影响另外两方的排队时间。
未做网页完整回归、整机重启、长期并发或实测功耗。
归档在
/root/AI/backup/dsh-embedding-rk3588与/root/AI/backup/nowen-embedding-access,均为四件套;不含密钥、Windows 数据库、原始问答、召回摘要、模型和虚拟环境。恢复前要先还原 cloudreve-embedding-npu 基础环境、服务用户和 venv,然后预演./restore.sh,确认后--apply。新密钥需要重新更新到 Windows 凭据库;restore 不会修改 Windows 或 Cloudreve 的 AI 设置。顺序上有依赖:基础归档如果最后覆盖 server.py 会撤销 DSH 补丁,所以本主题必须最后应用。归档要拷出设备,旧的全量备份包不包含这个主题。
回头看,这件事最值得记的不是"多接了两个人",而是那个设计选择:在模型标识里带上编码策略版本,用端口、密钥和单 IP 白名单把不同客户隔开。正因为这样,一个已经在跑的服务才能在不打扰原有业务的前提下,安静地多服务两个调用方。
附:完整脚本
上面步骤里用到的完整脚本不再整段贴在正文,已作为附件上传到对象存储,点击文件名即可下载:
restore-dsh.sh —— DSH 侧接入还原脚本
restore.sh —— nowen-embedding-access 还原脚本
对象存储策略会自动把文件名改写成 UUID(例如
95D96A8A-FB38-4CCB-AB42-F6E906E70760.py),这是存储策略的行为,不影响文件内容;下载后按正文里的路径改名放好即可。