我用 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可以直接改
所以如果硬改容器内部文件,会有两个问题:
- 容器重启或升级后改动可能丢失
- 不知道镜像构建链路,盲改风险很高
最后选了一个更稳的方案:
在 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 注入服务
核心逻辑是:
- 本地监听
127.0.0.1:18080 - 收到 HTML 页面请求
- 去请求原来的
localhost:8080 - 如果响应是
text/html,就在</body>前插入 Dify 脚本 - 顺便补充 CSP,允许
https://udify.app - 返回给浏览器
这个服务用 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 最值得被认真使用的地方。