网站 AI 对话子页(/ai/)上线记录:Hermes 双后端 + 文件交换

写于 2026-10-20

网站 AI 对话子页(/ai/)上线记录:Hermes 双后端 + 文件交换

  • 日期:2026-10-20
  • 站点:https://www.liufeisheng.cn/ai/ (注意:用户口述的 www.liufeshen.com 无法解析,真实域名是 liufeisheng.cn)
  • 需求:网站里加一个独立的 AI 对话子页,调用 NAS 上的 Hermes,能对话、能切模型、能上传/下载文件,必须登录后才能用
  • 结论:已上线并真机验证通过

一、先厘清的一个关键事实:Hermes 不支持"按请求切模型"

动手前实测了 Hermes API Server 的能力边界:

GET  /v1/models        → 只返回一个 "hermes-agent"
GET  /v1/capabilities  → admin_config_rw: false(远程改不了模型配置)
POST /v1/chat/completions 传 model=glm-4.7-flash / gemini-3.6-flash → 都返回 200
POST 同上传 model=totally-fake-model-xyz → 也返回 200 并作答

最后一条是判据:model 参数被原样回显但不生效,模型由容器内 config.yaml 决定。 所以"在网页里切模型"不能靠 Hermes 一个后端实现,除非在 NAS 上跑多个 Hermes 网关进程(每进程一套 profile,吃内存、难维护)。

二、采用的方案:双后端,UI 里说清差别

后端 端点 能力 模型
hermes NAS 容器 hermes:8642/v1/chat/completions 全部工具:搜索、读写文件、执行命令、记忆、技能 固定为容器配置(当前 opencode-go / deepseek-v4.1-flash)
direct https://opencode.ai/zen/go/v1/chat/completions 纯对话,无工具 model 参数真实生效,可选 36 个模型

前端一个下拉框切后端、第二个下拉框切模型(只在 direct 模式下出现)。这样既满足"能切模型",又保留了 Agent 的文件/工具能力,用户不会误以为直连模式也能改文件。

过滤掉的模型(实测不可用):beam-free、cat-jam-free(403 RegionError 地区限制)、step-5-preview-free(400 Model unavailable)、test(脏数据)。

三、文件上传/下载怎么打通(核心设计)

Hermes API 不接受 file / input_file / file_id(返回 400 unsupported_content_type,只有图片走 image_url 可用)。所以文件不能走 API 上传,改成共享目录 + 路径告知:

MySQL 侧 Django 写入         宿主目录                           hermes 容器内
/app/aichat_uploads/u<uid>/  ↔  /volume2/docker/mysite/aichat_uploads  ↔  /mnt/uploads/u<uid>/
  1. 浏览器上传 → Django 存到 /app/aichat_uploads/u<uid>/(按用户 id 分目录)
  2. 发消息时前端把文件的 agent 侧路径(/mnt/uploads/u1/xxx.txt)一起传给后端
  3. Django 在最后一条用户消息末尾追加提示:附件路径 + 「生成的文件请写入同一目录」
  4. Hermes agent 用文件工具直接读;生成的文件也落在同一目录
  5. 前端右侧「我的文件」列目录 + 下载链接(agent 产出的文件刷新即可见)

为此给 hermes 容器新增了一条 bind mount(compose 里):

- /volume2/docker/mysite/aichat_uploads:/mnt/uploads:rw

四、代码与集成点

Django app /app/aichat/(本地暂存 D:\Code\20261020_aichat_page\stage\aichat):

aichat/
├── __init__.py / apps.py
├── urls.py            /ai/、api/models|chat|upload|files|download
├── views.py           登录校验 + SSE 流式转发 + 文件管理
└── templates/aichat/index.html   独立页面 UI(单文件,原生 JS)

集成三处(脚本幂等,可重复执行): 1. mysite/settings.py:INSTALLED_APPS 加 'aichat',末尾追加 AICHAT_CONFIG_PATH、AICHAT_UPLOAD_ROOT 2. mysite/urls.py:path('ai/', include('aichat.urls')) 3. myapp/templates/base.html:导航「飞云课程」之后插入「AI对话」

密钥不写在代码里:/app/aichat_config.json(600,root 属主)由脚本从 hermes 容器的 /opt/data/.env 读出 API_SERVER_KEY 与 OPENCODE_GO_API_KEY 合成。

五、安全设计

  • 登录强制:页面与全部 API 都 @login_required;未登录访问 /ai/ 实测 302 到 /accounts/login/?next=/ai/
  • CSRF:全局 CsrfViewMiddleware 生效,前端从 cookie 取 token 放 X-CSRFToken;实测无 token → 403,带 token → 200
  • 路径遍历防护:文件名白名单正则(中英文/数字/空格 . - _ ( ) [ ],≤120 字),下载时 realpath 前缀校验
  • 单文件 50MB 上限;同名文件不覆盖(加时分秒后缀)
  • 按用户隔离目录:u<user.id>
  • 日志不打印密钥;临时 .env 副本用完即删(注意 docker cp 到宿主的文件是 root 属主,fei 删不掉,要用 docker run --entrypoint sh -v /tmp:/t 清理)

六、真机验证记录

项目 方式 结果
未登录访问 curl https://www.liufeisheng.cn/ai/ 302 → 登录页 ✅
登录态页面 Django Client + force_login + HTTP_HOST=站点域名 200,17653 字节,含导航与脚本 ✅
模型清单 GET /ai/api/models/ 200,36 个直连模型 ✅
上传 POST /ai/api/upload/ 36 字节中文文件 200,返回 agent_path=/mnt/uploads/u1/… ✅
文件列表/下载 GET /ai/api/files/、/ai/api/files/<name>/ 200,下载内容与原文一致 ✅
对话 direct SSE 流式 200,回答「链路正常」✅
对话 hermes SSE 流式 200,回答「链路正常」✅
Agent 读附件 附件 + 「读取附件原样返回」 200,把文件内容一字不差返回 ✅ 证明共享挂载生效
CSRF 强制 enforce_csrf_checks=True 无 token 403 / 带 token 200 ✅
Django 自检 manage.py check 0 issues ✅

七、踩过的坑

  1. docker run <mihomo镜像> sh -c '…' 会挂住不返回:mihomo 镜像带 ENTRYPOINT,sh 被当成程序参数直接跑代理。必须 --entrypoint sh。
  2. 测试客户端被 ALLOWED_HOSTS 拦:Django Client() 默认 HTTP_HOST=testserver,返回 400。要 Client(HTTP_HOST="www.liufeisheng.cn")。
  3. 测试客户端默认跳过 CSRF:验证真实浏览器行为必须 enforce_csrf_checks=True,否则会误以为 CSRF 已通。
  4. Hermes 返回的 model 字段是回显:不能拿它判断实际用了哪个模型(和 LiteLLM 那边同一个坑,那边要用 spend.db 判断)。
  5. 容器重建丢临时层:hermes 加挂载后 compose -p herms up -d 重建,/root 下的东西会丢(数据在 /opt/data 卷,无影响)。
  6. aichat_uploads 设 777 是必须的:mysite 容器 root 写、hermes 容器 uid 10000 也要写(生成文件给用户下载)。

八、已知限制 / 后续可做

  • Hermes 模式无法切模型(设计限制,见第一节);要真切模型只能在 NAS 上多跑 profile
  • 没有限流与配额:登录用户理论上可无限次调用;OpenCode 额度与 Hermes 的模型额度是共用的
  • 会话历史存浏览器内存,刷新即清空(Hermes 侧靠 X-Hermes-Session-Key: aichat:user:<id> 归组日志,未做服务端持久化)
  • 工具进度事件暂未在 UI 上显示(流里能看到 hermes.tool.progress,前端已做解析但实测该版本未下发,只显示「思考中…」)
  • 8642 端口在宿主是 0.0.0.0 监听:LAN 内可访问(需 key)。若 frp/nginx 把 8642 也映射到公网,务必撤掉——API Server 有完整工具权限

九、变更清单

位置 变更
/app/aichat/(新增) Django app:views/urls/templates
/app/aichat_config.json(新增) 后端地址与两把密钥,600 root
/app/aichat_uploads/(新增) 文件交换目录,777
/app/mysite/settings.py INSTALLED_APPS += aichat;新增两个 AICHAT 配置项;原文件备份 .bak_aichat
/app/mysite/urls.py += path('ai/', …);备份 .bak_aichat
/app/myapp/templates/base.html 导航加「AI对话」;备份 .bak_aichat
/volume2/docker/hermes/compose.yaml volumes 新增 aichat_uploads:/mnt/uploads:rw
/opt/data/.env(hermes) 新增 API_SERVER_KEY(48 位,启用 API Server 必需)
本地 D:\Code\20261020_aichat_page\ 工程暂存:stage/aichat、hermes-compose.yaml、部署与验证脚本