我用 Codex 给自己的网站上线了一个智能体客服:从 Dify 到服务器部署,全程实战复盘

我用 Codex 给自己的网站上线了一个智能体客服:从 Dify 到服务器部署,全程实战复盘

这不是一篇"教你点几个按钮"的教程。

这是一次真实上线:从截图判断架构,到搭 Dify 智能体,再到服务器注入客服组件、修 CSP、排查 token 错误,最后让网站真的跑起来。

如果你也有一个网站,想给它加一个"能回答问题、能引导用户、能转人工"的 AI 客服,这篇可以直接照着走。

先说结果

我给 codex.chatgpt-plus.top 上线了一个 Dify 智能体客服。

最终效果是:

  • 网站右下角出现 Dify 悬浮客服入口
  • 用户可以直接问注册、充值、API Key、调用报错、余额、节点状态等问题
  • 客服接入的是 Dify Chatflow
  • 网站后端不需要改源码
  • Caddy 负责把 HTML 页面请求转给一个本地注入服务
  • API 和静态资源仍然走原来的后端,不影响业务请求

整个过程里,Codex 做了几件事:

  • 分析现有网站架构
  • 设计 Dify Chatflow 客服方案
  • 生成知识库和 Prompt
  • SSH 登录服务器
  • 备份 Caddy 和站点相关文件
  • 写了一个本地 HTML 注入代理
  • 修改 Caddy 路由
  • 排查 Dify token、CSP、systemd、Python 兼容问题
  • 最后完成验证和回滚方案

一句话总结:

不是"让 AI 给你建议",而是让 Codex 直接参与上线。

为什么不用直接改前端源码?

因为这个网站不是一个普通静态站。

上线前先查了一下,发现 Caddy 配置不是 root /var/www/html 这种静态目录,而是:

caddy 复制代码
reverse_proxy localhost:8080

也就是说,网站本体跑在 Docker 容器里,Caddy 只是反向代理。

进一步检查后发现:

  • Caddy 在服务器上运行
  • 后端服务是 Docker 容器
  • 容器镜像是类似 backend-app 的应用
  • 前端 HTML 是应用动态返回的
  • 容器里没有常规的 dist/index.html 可以直接改

所以如果硬改容器内部文件,会有两个问题:

  1. 容器重启或升级后改动可能丢失
  2. 不知道镜像构建链路,盲改风险很高

最后选了一个更稳的方案:

在 Caddy 和后端之间加一个本地 HTML 注入代理。

只有浏览器访问页面 HTML 时,才经过注入代理;API、静态资源、健康检查继续直连后端。

整体架构

上线后的结构大概是这样:
#mermaid-svg-OLF2IlehJj4tBNb0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OLF2IlehJj4tBNb0 .error-icon{fill:#552222;}#mermaid-svg-OLF2IlehJj4tBNb0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OLF2IlehJj4tBNb0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OLF2IlehJj4tBNb0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OLF2IlehJj4tBNb0 .marker.cross{stroke:#333333;}#mermaid-svg-OLF2IlehJj4tBNb0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OLF2IlehJj4tBNb0 p{margin:0;}#mermaid-svg-OLF2IlehJj4tBNb0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster-label text{fill:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster-label span{color:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster-label span p{background-color:transparent;}#mermaid-svg-OLF2IlehJj4tBNb0 .label text,#mermaid-svg-OLF2IlehJj4tBNb0 span{fill:#333;color:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 .node rect,#mermaid-svg-OLF2IlehJj4tBNb0 .node circle,#mermaid-svg-OLF2IlehJj4tBNb0 .node ellipse,#mermaid-svg-OLF2IlehJj4tBNb0 .node polygon,#mermaid-svg-OLF2IlehJj4tBNb0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OLF2IlehJj4tBNb0 .rough-node .label text,#mermaid-svg-OLF2IlehJj4tBNb0 .node .label text,#mermaid-svg-OLF2IlehJj4tBNb0 .image-shape .label,#mermaid-svg-OLF2IlehJj4tBNb0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-OLF2IlehJj4tBNb0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OLF2IlehJj4tBNb0 .rough-node .label,#mermaid-svg-OLF2IlehJj4tBNb0 .node .label,#mermaid-svg-OLF2IlehJj4tBNb0 .image-shape .label,#mermaid-svg-OLF2IlehJj4tBNb0 .icon-shape .label{text-align:center;}#mermaid-svg-OLF2IlehJj4tBNb0 .node.clickable{cursor:pointer;}#mermaid-svg-OLF2IlehJj4tBNb0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OLF2IlehJj4tBNb0 .arrowheadPath{fill:#333333;}#mermaid-svg-OLF2IlehJj4tBNb0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OLF2IlehJj4tBNb0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OLF2IlehJj4tBNb0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OLF2IlehJj4tBNb0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OLF2IlehJj4tBNb0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OLF2IlehJj4tBNb0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster text{fill:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 .cluster span{color:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OLF2IlehJj4tBNb0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OLF2IlehJj4tBNb0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-OLF2IlehJj4tBNb0 .icon-shape,#mermaid-svg-OLF2IlehJj4tBNb0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OLF2IlehJj4tBNb0 .icon-shape p,#mermaid-svg-OLF2IlehJj4tBNb0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OLF2IlehJj4tBNb0 .icon-shape .label rect,#mermaid-svg-OLF2IlehJj4tBNb0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OLF2IlehJj4tBNb0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OLF2IlehJj4tBNb0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OLF2IlehJj4tBNb0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTML 页面
API / 静态资源
用户浏览器
Caddy
本地注入服务 127.0.0.1:18080
原后端 localhost:8080
注入 Dify embed 脚本
udify.app WebApp

核心点是:

  • GET / 这种页面请求走注入服务
  • /assets/* 静态资源不走注入
  • /api/*/v1/* 业务接口不走注入
  • 注入服务只改 HTML,不碰业务接口

这比"全站代理重写所有响应"安全得多。

第一步:先搭 Dify Chatflow

客服不是随便丢一个大 Prompt 就完事。

一个网站客服至少要处理这些问题:

  • 注册登录
  • 充值不到账
  • API Key 怎么创建
  • API 调用报错
  • 余额和账单
  • 服务状态
  • 备用节点
  • 退款、风控、封号、投诉转人工

我最开始给 Dify 设计的是一个更完整的 Chatflow:
#mermaid-svg-FkdPQDfMp2sj7J0z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FkdPQDfMp2sj7J0z .error-icon{fill:#552222;}#mermaid-svg-FkdPQDfMp2sj7J0z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FkdPQDfMp2sj7J0z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FkdPQDfMp2sj7J0z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FkdPQDfMp2sj7J0z .marker.cross{stroke:#333333;}#mermaid-svg-FkdPQDfMp2sj7J0z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FkdPQDfMp2sj7J0z p{margin:0;}#mermaid-svg-FkdPQDfMp2sj7J0z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster-label text{fill:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster-label span{color:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster-label span p{background-color:transparent;}#mermaid-svg-FkdPQDfMp2sj7J0z .label text,#mermaid-svg-FkdPQDfMp2sj7J0z span{fill:#333;color:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z .node rect,#mermaid-svg-FkdPQDfMp2sj7J0z .node circle,#mermaid-svg-FkdPQDfMp2sj7J0z .node ellipse,#mermaid-svg-FkdPQDfMp2sj7J0z .node polygon,#mermaid-svg-FkdPQDfMp2sj7J0z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FkdPQDfMp2sj7J0z .rough-node .label text,#mermaid-svg-FkdPQDfMp2sj7J0z .node .label text,#mermaid-svg-FkdPQDfMp2sj7J0z .image-shape .label,#mermaid-svg-FkdPQDfMp2sj7J0z .icon-shape .label{text-anchor:middle;}#mermaid-svg-FkdPQDfMp2sj7J0z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FkdPQDfMp2sj7J0z .rough-node .label,#mermaid-svg-FkdPQDfMp2sj7J0z .node .label,#mermaid-svg-FkdPQDfMp2sj7J0z .image-shape .label,#mermaid-svg-FkdPQDfMp2sj7J0z .icon-shape .label{text-align:center;}#mermaid-svg-FkdPQDfMp2sj7J0z .node.clickable{cursor:pointer;}#mermaid-svg-FkdPQDfMp2sj7J0z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FkdPQDfMp2sj7J0z .arrowheadPath{fill:#333333;}#mermaid-svg-FkdPQDfMp2sj7J0z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FkdPQDfMp2sj7J0z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FkdPQDfMp2sj7J0z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FkdPQDfMp2sj7J0z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FkdPQDfMp2sj7J0z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FkdPQDfMp2sj7J0z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster text{fill:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z .cluster span{color:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FkdPQDfMp2sj7J0z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FkdPQDfMp2sj7J0z rect.text{fill:none;stroke-width:0;}#mermaid-svg-FkdPQDfMp2sj7J0z .icon-shape,#mermaid-svg-FkdPQDfMp2sj7J0z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FkdPQDfMp2sj7J0z .icon-shape p,#mermaid-svg-FkdPQDfMp2sj7J0z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FkdPQDfMp2sj7J0z .icon-shape .label rect,#mermaid-svg-FkdPQDfMp2sj7J0z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FkdPQDfMp2sj7J0z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FkdPQDfMp2sj7J0z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FkdPQDfMp2sj7J0z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
问题分类器
产品咨询
账号登录
充值账单
API 调用排障
节点状态
转人工
知识检索
LLM
直接回复
收集联系方式/订单号/问题摘要
转人工

但实际上,第一版为了快速上线,先用了最小可用流程:

text 复制代码
开始 -> 知识检索 -> LLM -> 直接回复

这已经能覆盖很多基础问答。

后续再加分类器、变量、转人工节点,会更完整。

第二步:准备知识库

客服的质量很大程度取决于知识库。

我为这个客服准备了一份 knowledge-base.md,内容包括:

  • 平台基本信息
  • 文档入口
  • 服务状态页
  • API Base URL
  • 备用节点
  • 创建 API Key 的步骤
  • 常见 API 报错排查
  • 充值不到账处理方式
  • 退款与争议处理边界
  • 敏感信息禁止收集规则
  • 人工联系方式

最重要的是安全边界:

客服不能索要用户密码、API Key 原文、Cookie、完整支付凭证、身份证件、私钥。

这点一定要写进 Prompt 和知识库里。

第三步:设计客服 Prompt

核心系统提示词大概是这个方向:

text 复制代码
你是 示例 API 平台 网站客服。

你的职责:
1. 用简洁中文帮助用户解决注册登录、充值到账、API Key、API 调用、余额账单、节点状态、文档入口等问题。
2. 优先基于知识库内容回答。
3. 没有依据时明确说"不确定",并给出下一步排查或转人工。
4. 不编造价格、模型名单、支付状态、账号状态、订单状态、退款承诺。
5. 不索要密钥、密码、完整支付凭证、身份证件、Cookie、后台 token。
6. 涉及退款、封号、风控、投诉、支付争议等问题,只能收集信息并转人工。

回答风格:
- 先给结论,再给步骤。
- 每次最多 5 个步骤。
- 排障问题按"先确认 -> 再排查 -> 最后转人工"。

这类客服 Prompt 的重点不是"会说话",而是:

知道什么能答,什么不能答。

特别是涉及支付、退款、风控、封号时,AI 不能乱承诺。

第四步:拿 Dify 嵌入代码

Dify 发布后,会给一段类似这样的代码:

html 复制代码
<script>
 window.difyChatbotConfig = {
  token: 'YOUR_WEBAPP_TOKEN',
  inputs: {},
  systemVariables: {},
  userVariables: {},
 }
</script>
<script
 src="https://udify.app/embed.min.js"
 id="YOUR_WEBAPP_TOKEN"
 defer>
</script>

这里有一个很容易踩的坑:

Dify 的嵌入 token 不一定是 <wrong-app-code>

我一开始就踩了。

最开始复制了一个 <wrong-app-code> 形式的值,结果网站报:

text 复制代码
App with code <wrong-app-code> not found

这说明:

  • token 不是 WebApp 嵌入 token
  • 或者 token 和 baseUrl 不属于同一个 Dify 环境
  • 或者 App 没发布

后来换成 Dify 官方"嵌入网站"里给的短 token,问题才解决。

所以这里一定要记住:

复制 Dify 发布面板里的完整嵌入代码,不要自己猜 token。

第五步:服务器部署前先备份

这是 Codex 做上线时最重要的一步。

在改服务器之前,先备份:

bash 复制代码
mkdir -p /srv/backups/customer-service/$(date +%Y%m%d-%H%M%S)
cp -a /etc/caddy/Caddyfile.example /srv/backups/customer-service/.../Caddyfile.example.bak
cp -a /srv/backend-app/deploy/docker-compose.yml.example /srv/backups/customer-service/.../docker-compose.yml.example.bak
cp -a /srv/backend-app/deploy/data/app-config.yaml /srv/backups/customer-service/.../app-config.yaml.bak

另外还打包了数据目录,但排除了日志:

bash 复制代码
tar --exclude='data/logs' -czf backend-data-no-logs.tgz -C /srv/backend-app/deploy data

为什么这么谨慎?

因为这次改的是线上入口层,一旦 Caddy 配错,网站首页就可能 502。

上线不是炫技,先给自己留退路。

第六步:写一个本地 HTML 注入服务

核心逻辑是:

  1. 本地监听 127.0.0.1:18080
  2. 收到 HTML 页面请求
  3. 去请求原来的 localhost:8080
  4. 如果响应是 text/html,就在 </body> 前插入 Dify 脚本
  5. 顺便补充 CSP,允许 https://udify.app
  6. 返回给浏览器

这个服务用 Python 写,部署在:

text 复制代码
/srv/customer-service/html_inject_proxy.py

配置放在:

text 复制代码
/srv/customer-service/customer-service.env

里面只保存 Dify WebApp token 和基础地址:

env 复制代码
DIFY_WEBAPP_TOKEN=YOUR_DIFY_WEBAPP_TOKEN
DIFY_BASE_URL=https://udify.app
BACKEND_HOST=127.0.0.1
BACKEND_PORT=8080
LISTEN_HOST=127.0.0.1
LISTEN_PORT=18080

然后用 systemd 托管:

text 复制代码
/etc/systemd/system/codex-customer-service-injector.service

启动:

bash 复制代码
systemctl daemon-reload
systemctl enable --now codex-customer-service-injector.service
systemctl restart codex-customer-service-injector.service

验证:

bash 复制代码
systemctl is-active codex-customer-service-injector.service
curl -sS -H 'Accept: text/html' http://127.0.0.1:18080/ | grep embed.min.js

第七步:改 Caddy,只让 HTML 走注入服务

Caddy 原来是:

caddy 复制代码
reverse_proxy localhost:8080

改成:

caddy 复制代码
@customer_service_html {
    method GET HEAD
    not path /assets/* /api/* /v1/* /health /logo.png /favicon.ico /robots.txt /sitemap.xml
}

handle @customer_service_html {
    reverse_proxy localhost:18080
}

handle {
    reverse_proxy localhost:8080
}

这里非常关键。

不能让所有请求都走注入服务,否则 API 流式响应、模型调用、静态资源都有可能被误处理。

所以要把这些路径排除:

  • /assets/*
  • /api/*
  • /v1/*
  • /health
  • /logo.png
  • /favicon.ico

改完后必须验证:

bash 复制代码
caddy validate --config /etc/caddy/Caddyfile.example
systemctl reload caddy

然后测试:

bash 复制代码
curl -k -sS https://codex.chatgpt-plus.top/ | grep embed.min.js
curl -k -sS -o /dev/null -w "%{http_code}" https://codex.chatgpt-plus.top/assets/index-xxx.js

最终验证结果:

text 复制代码
caddy_active=active
injector_active=active
public_status=200
public_has_new_token=2
public_has_embed=1
asset_status=200

这说明:

  • 首页正常
  • Dify 脚本注入成功
  • 静态资源不受影响

中间踩了哪些坑?

这部分最有价值。

坑 1:把 Dify token 搞错了

一开始用了 <wrong-app-code>,结果页面报:

text 复制代码
App with code <wrong-app-code> not found

解决方式:

必须使用 Dify 发布面板给的 WebApp 嵌入 token。

不要用 API Key,不要用应用 ID,不要用 URL 里的 UUID。

坑 2:CSP 报 unpkg.com 被拦

页面出现过:

text 复制代码
Connecting to https://unpkg.com/...wasm.map violates CSP

这个看起来吓人,但它不是主因。

这是 Dify iframe 里加载 wasm sourcemap 被它自己的 CSP 拦了,通常不影响客服功能。

真正导致不可用的是上面的:

text 复制代码
App with code ... not found

排障时一定要分清楚主错误和噪音错误。

坑 3:Python 版本不支持 list[str]

服务器上的 Python 是 3.8,脚本里写了:

python 复制代码
def add_csp_source(policy: str, directive: str, sources: list[str]) -> str:

结果服务启动失败:

text 复制代码
TypeError: 'type' object is not subscriptable

解决:

python 复制代码
def add_csp_source(policy: str, directive: str, sources) -> str:

这就是线上环境和本地开发环境不一致的典型坑。

坑 4:Caddy 路由一度回到了原始状态

中间发现线上首页没有注入,检查 Caddyfile.example 后发现还是:

caddy 复制代码
reverse_proxy localhost:8080

也就是说注入路由没在当前配置里。

解决方式是重新插入:

caddy 复制代码
handle @customer_service_html {
    reverse_proxy localhost:18080
}

然后重新:

bash 复制代码
caddy validate --config /etc/caddy/Caddyfile.example
systemctl reload caddy

坑 5:敏感配置不能随便打印

这次排查里有一次误把 app-config.yaml 的敏感字段输出到了终端,包括数据库凭据和应用签名密钥。

这件事要严肃处理。

上线后建议:

  • 轮换数据库凭据
  • 轮换应用签名密钥
  • 轮换 root 密码
  • 后续不要用 root 密码直连
  • 改成专用部署用户 + SSH key + sudo

这也是使用 AI 上线时要特别注意的一点:

Codex 能干活,但你要给它安全边界。

最终回滚方案

任何线上改动都要有回滚命令。

这次回滚很简单:

bash 复制代码
cp -a /srv/backups/customer-service/xxxx/Caddyfile.example.bak /etc/caddy/Caddyfile.example
systemctl disable --now codex-customer-service-injector.service
caddy validate --config /etc/caddy/Caddyfile.example
systemctl reload caddy

如果只是换 Dify token,不需要改 Caddy,只要:

bash 复制代码
sed -i 's/^DIFY_WEBAPP_TOKEN=.*/DIFY_WEBAPP_TOKEN=新的token/' /srv/customer-service/customer-service.env
systemctl restart codex-customer-service-injector.service

这次上线的关键经验

1. Codex 不只是写代码,它可以参与真实运维

这次它做的不只是"生成一段 HTML"。

它参与了:

  • 服务器探测
  • Caddy 配置分析
  • Docker 架构判断
  • 备份
  • 写注入代理
  • systemd 服务
  • CSP 排查
  • 线上验证
  • 回滚方案

这已经接近一个小型 DevOps 协作流程。

2. 不要一上来就改源码

很多时候,源码不在你手里,或者改源码成本很高。

这次用"Caddy + 本地注入服务"的方式,绕开了容器镜像和前端构建链路。

这类方案适合:

  • 不方便重构前端
  • 前端在容器镜像里
  • 只是想快速加一个全站客服
  • 需要可回滚

3. 智能体客服的核心不是"智能",而是"边界"

客服最怕什么?

不是答得不够花哨。

而是乱答:

  • 乱承诺退款
  • 乱判断账号状态
  • 乱要用户 API Key
  • 乱解释支付问题
  • 乱给合规建议

所以一个好客服 Prompt,一定要写清楚:

text 复制代码
哪些能回答
哪些不能回答
哪些必须转人工
哪些信息不能收集

4. 每一步都要验证

上线过程中一直在做这些验证:

bash 复制代码
systemctl is-active caddy
systemctl is-active codex-customer-service-injector.service
caddy validate --config /etc/caddy/Caddyfile.example
curl -k -sS https://domain.com/ | grep embed.min.js
curl -k -sS -o /dev/null -w "%{http_code}" https://domain.com/assets/index.js

不要凭感觉上线。

验证结果比"我觉得没问题"可靠。

给你一份可复用清单

如果你也想用 Codex 给网站上线智能体客服,可以照这个清单走:

Dify 侧

  • 创建 Chatflow
  • 配置知识库
  • 配置 LLM Prompt
  • 发布 WebApp
  • 复制"嵌入网站"完整代码
  • 确认 token 不是 API Key

服务器侧

  • 备份 Caddyfile.example
  • 备份站点配置
  • 确认网站是静态目录还是反向代理
  • 如果是静态站,直接加 embed 代码
  • 如果是反向代理,考虑 HTML 注入代理
  • 修改 Caddy 前先 validate
  • reload 后测试首页和静态资源

安全侧

  • 不打印密钥
  • 不把 API Key 放前端
  • 不让 AI 索要用户敏感信息
  • 操作后轮换高危凭据
  • 保留回滚命令

最后

这次最大的感受是:

以前上线一个客服,可能要自己查文档、写代码、改服务器、排查 CSP、重启服务、看日志。

现在可以把 Codex 当成一个"能动手的搭档":

你给目标,它做探测;

你给边界,它做修改;

你给权限,它做验证;

中间出错,它继续定位和修复。

真正的变化不是"AI 会聊天"。

真正的变化是:

AI 开始能把一个想法推进到线上。

而这,才是 Codex 最值得被认真使用的地方。

相关推荐
聚名网1 小时前
域名net,com,cn有区别吗?有哪些不同呢?
服务器·开发语言·php
java_cj2 小时前
深入kubectl create源码:从YAML到Pod的完整链路拆解
运维·云原生·容器·kubernetes
小小小花儿2 小时前
SSH密钥配置(免密连接远程服务器)
服务器·ssh
深圳恒讯3 小时前
越南服务器BGP多线和单线有什么区别?
运维·服务器
志栋智能3 小时前
超自动化运维如何提升安全合规水平?
运维·安全·自动化
A_humble_scholar4 小时前
Linux(九) 进程管理完全指南:从入门到实战
linux·运维·chrome
江华森4 小时前
Linux 操作命令完全指南
linux·运维
源图客4 小时前
【AI向量数据库】Weaviate介绍与部署
运维·docker·容器
用什么都重名4 小时前
Git分支合并与远程服务器同步实战:保留关键配置文件
运维·服务器·git