网站 AI 对话子页(/ai/)上线记录:Hermes 双后端 + 文件交换
网站 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>/
- 浏览器上传 → Django 存到
/app/aichat_uploads/u<uid>/(按用户 id 分目录) - 发消息时前端把文件的 agent 侧路径(
/mnt/uploads/u1/xxx.txt)一起传给后端 - Django 在最后一条用户消息末尾追加提示:附件路径 + 「生成的文件请写入同一目录」
- Hermes agent 用文件工具直接读;生成的文件也落在同一目录
- 前端右侧「我的文件」列目录 + 下载链接(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 ✅ |
七、踩过的坑
docker run <mihomo镜像> sh -c '…'会挂住不返回:mihomo 镜像带 ENTRYPOINT,sh被当成程序参数直接跑代理。必须--entrypoint sh。- 测试客户端被 ALLOWED_HOSTS 拦:Django
Client()默认HTTP_HOST=testserver,返回 400。要Client(HTTP_HOST="www.liufeisheng.cn")。 - 测试客户端默认跳过 CSRF:验证真实浏览器行为必须
enforce_csrf_checks=True,否则会误以为 CSRF 已通。 - Hermes 返回的
model字段是回显:不能拿它判断实际用了哪个模型(和 LiteLLM 那边同一个坑,那边要用 spend.db 判断)。 - 容器重建丢临时层:hermes 加挂载后
compose -p herms up -d重建,/root下的东西会丢(数据在/opt/data卷,无影响)。 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、部署与验证脚本 |