听云 OCM 外派任务单 · B1+B2 · MFR-Portal 生产部署¶
| 字段 | 值 |
|---|---|
| 任务编号 | OCL-TASK-20260827-01 |
| 外派方 | 听写 DT(本任务单签发人) |
| 执行方 | 听云 OCM(SSH 生产 ECS 执行) |
| 签发时间 | 2026-08-27 |
| 关联日志 | DTL L-20260827-01(阻塞根因确诊)、DTL L-20260827-02(外派通知) |
| 关联工作包 | Phase 4 · D01 MFR 开放报价生产回归(解除阻塞前置动作) |
| 三 AI 合规声明 | 本任务单由听写签发,仅通过文档通道外派;听云收到后须自行 SSH ECS 执行,听写不触碰生产环境 |
一、任务名称¶
B1:启动 mfr-portal systemd 服务 + B2:配置 Caddy /mfr/* 反代规则
阻塞根因(详见 DTL L-20260827-01 §三):
- 生产 GET http://8.218.165.94/mfr/ 返回 404,响应体引用 /admin/_next/static/...
- → 说明 MFR-Portal 的 systemd service 未启动(B1 缺失)
- → 同时 Caddy 兜底 handle 把未知路径反代到 OPR-Admin :3100,即 /mfr/* 反代规则缺失(B2 缺失)
二、WBS 分解(2 个子任务,按顺序执行 B1 → B2)¶
WBS 1 · B1:启动 mfr-portal.service(ECS 本地 4 条命令)¶
目标:让 MFR-Portal Next.js standalone 在 127.0.0.1:3000 正常监听并返回 HTTP 200
前置确认:ECS 上
/etc/systemd/system/mfr-portal.service已就位(模板见ocm/private/tools/mfr-portal.service), WorkingDirectory=/opt/dip1/dip1/frontend/apps/mfr-portal,PORT=3000,ExecStart=standalone server.js
逐条执行以下 bash 命令,每步输出须符合【断言】:
# ── B1-1:启用并启动服务 ──────────────────────────────
systemctl enable --now mfr-portal.service
# 断言:无报错;退出码 0
# ── B1-2:验证服务活跃状态 ────────────────────────────
systemctl is-active mfr-portal.service
# 断言:stdout = "active"(若为 "inactive" 或 "failed",请检查 journalctl -u mfr-portal.service -n 50)
# ── B1-3:验证 3000 端口监听 ──────────────────────────
ss -tlnp | grep ':3000'
# 断言:输出含 LISTEN 且关联 node 进程(若无,等 5s 再试一次;仍无则 cat /var/log/mfr-portal.err)
# ── B1-4:本地 HTTP 探活(关键) ───────────────────────
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/
# 断言:stdout = "200"(非 200 → 检查 node 进程是否仍在 + /var/log/mfr-portal.log 前 30 行)
B1 失败时的排查顺序(仅在 B1-1~B1-4 断言未通过时使用):
1. journalctl -u mfr-portal.service -n 80 --no-pager → 看启动异常栈
2. ls -la /opt/dip1/dip1/frontend/apps/mfr-portal/.next/standalone/apps/mfr-portal/server.js → 确认 standalone 构建产物存在
3. ls -la /opt/dip1/dip1/frontend/apps/mfr-portal/.next/static/ → 确认静态资源存在(standalone 需要与 static 同级)
4. 若 standalone 缺失 → 通知听写,由听写协调听码重新打包 MFR 代码包
WBS 2 · B2:配置 Caddy /mfr/* 反代 + reload(ECS 本地 5 步)¶
目标:在 Caddyfile 的 handle /admin/* 之前插入 handle_path /mfr/*,使 /mfr/* → 127.0.0.1:3000
(放 /admin/* 前的原因:Caddy handle 按声明顺序匹配,放后面会被兜底 handle 先抢走)
B2-1:备份当前 Caddyfile(必须先备份)¶
cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%Y%m%d%H%M%S)
# 断言:备份文件生成,ls /etc/caddy/Caddyfile.bak.* 能看到
B2-2:编辑 /etc/caddy/Caddyfile,插入 /mfr/* handle_path¶
插入位置:在现有 handle /admin/* 块的紧前面(即 Caddyfile 第 46 行 # ========== OPR-Admin Next.js... 注释行之前)
插入内容(整块复制粘贴):
# ========== MFR-Portal Next.js(handle_path 剥离 /mfr 前缀) ==========
handle_path /mfr/* {
reverse_proxy http://127.0.0.1:3000
}
插入后 Caddyfile 的关键片段应如下(顺序不可错):
# ========== WKR / CST / DL 三端静态页(handle_path 剥离前缀) ==========
handle_path /wkr/* { reverse_proxy http://127.0.0.1:3101 }
handle_path /cst/* { reverse_proxy http://127.0.0.1:3102 }
handle_path /dl/* { reverse_proxy http://127.0.0.1:8080 }
# ========== MFR-Portal Next.js(handle_path 剥离 /mfr 前缀) ========== ← 新增块
handle_path /mfr/* { ← 新增块
reverse_proxy http://127.0.0.1:3000 ← 新增块
} ← 新增块
# ========== OPR-Admin Next.js(basePath="/admin":不剥离前缀) ========== ← 原有块,保持在 MFR 之后
handle /admin/* {
reverse_proxy http://127.0.0.1:3100
}
⚠ 严禁把
/mfr/*放在handle /admin/*之后或文件末尾,否则会被兜底 handle 抢走,B2 整体失败。
B2-3:Caddy 配置语法校验(必做,防止 reload 失败导致全站 502)¶
caddy validate --config /etc/caddy/Caddyfile
# 断言:stdout 含 "valid configuration"(若失败 → 根据报错修正,或回滚到 bak 文件再试)
B2-4:Reload Caddy(热重载,不停服)¶
systemctl reload caddy
# 断言:退出码 0;若失败 → systemctl status caddy -n 30 查看错误
B2-5:本地 HTTP 断言(3 条,全部通过才算 B2 完成)¶
# B2-5a:/mfr/ 根路径 200
curl -s -o /dev/null -w "B2-5a(mfr-root)=%{http_code}\n" http://127.0.0.1/mfr/
# 断言:200
# B2-5b:/mfr/login 子路径 200(验证 handle_path 剥离前缀后子路由可达)
curl -s -o /dev/null -w "B2-5b(mfr-login)=%{http_code}\n" http://127.0.0.1/mfr/login
# 断言:200
# B2-5c:验证反代到的是 MFR 而非 OPR-Admin(关键!避免兜底仍生效)
curl -s http://127.0.0.1/mfr/ | grep -o '_next/static/[^"]*' | head -3
# 断言:输出路径中**不含** "/admin/_next/"(若含 /admin/_next/ → 说明 MFR 规则未生效,仍被兜底到 :3100,需回查 B2-2 的插入顺序)
B2 失败回滚(仅在 B2-3 校验失败且无法修复,或 B2-5c 断言始终含 /admin/_next/ 时):
cp /etc/caddy/Caddyfile.bak.<时间戳> /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
三、验收标准(6 条断言,全部 ✅ 才算任务完成)¶
| # | 所属子任务 | 断言命令 | 预期输出 | 权重 |
|---|---|---|---|---|
| A1 | B1 | systemctl is-active mfr-portal.service |
active |
必过 |
| A2 | B1 | ss -tlnp \| grep :3000 \| grep -c LISTEN |
>= 1 |
必过 |
| A3 | B1 | curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/ |
200 |
必过 |
| A4 | B2 | curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/ |
200 |
必过 |
| A5 | B2 | curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/login |
200 |
必过 |
| A6 | B2 | curl -s http://127.0.0.1/mfr/ \| grep -c '/admin/_next/' |
0(不含 /admin/_next/) |
必过(关键防回归) |
四、督办节点 & 闭环机制¶
听云收到本任务单
│
▼
SSH ECS 执行 B1(WBS1)→ 记录 A1~A3 断言结果
│
├── A1~A3 任一失败 → 按 B1 排查顺序定位,无法解决 → 通知听写阻塞
│
▼ A1~A3 全部通过
SSH ECS 执行 B2(WBS2,先备份 → 编辑 → validate → reload → 断言)
│
├── validate 失败 → 根据报错修正或回滚 bak → 重试 validate
├── reload 失败 → 回滚 bak → 通知听写(高优,因影响全站 Caddy)
├── A4~A6 任一失败 → 检查插入顺序是否在 /admin/* 之前
│
▼ A1~A6 全部 ✅
通知听写(协作通道回传以下信息):
① 听云执行时间戳(起止)
② A1~A6 每条断言的实际输出截图或文本
③ B2 备份的 Caddyfile 时间戳 & 最终 Caddyfile /mfr/* 块的 cat 输出
④ OCL 日志编号(听云须同步写入 ocm/worklog/ocl/L-YYYYMMDD-NN)
│
▼
听写收到通知 → 启动 D01 回归 4 项(/mfr/ /mfr/login /mfr/quote /api/v2/qsv/quote/mfr-open)
│
▼ D01 通过 → 启动 D02 全量复测 8 项 → Phase 4 闭环
五、参考坐标(听云无需操作,仅用于交叉验证)¶
- 生产 ECS:
8.218.165.94(root SSH) - MFR systemd 模板:
/etc/systemd/system/mfr-portal.service(副本:ocm/private/tools/mfr-portal.service) - Caddy 模板基准:
ocm/deploy/aliyun-prod/Caddyfile(缺 /mfr/* 是本次修的点) - MFR 工作目录:
/opt/dip1/dip1/frontend/apps/mfr-portal - MFR 日志:
/var/log/mfr-portal.log(stdout)、/var/log/mfr-portal.err(stderr) - Caddy 日志:
/var/log/caddy/access.log - 听写 DTL 阻塞报告:
docs/工作日志/dtl/L-20260827-01_phase4_cloud_verification.md(已发布至 https://hk2026-docs.pages.dev/dtl2026082701/ §七 含 B1/B2 初版步骤)
六、执行确认回执(听云执行后填写并传回听写)¶
听云:请在完成后将下列 5 项信息填入,作为协作闭环凭证。
| 项 | 听云填写 |
|---|---|
| 执行开始时间 | 2026-08-27 20:10 |
| 执行结束时间 | 2026-08-27 21:05(🟢完成,耗时约 55 分钟;阻塞解除策略:S3-alt 从 112.74 打包回救 + PM2 替代 systemd + Caddy Referer 路由兜底 _next/*) |
| A1~A6 断言结果(6 条实际输出) | A1=✅ pm2 describe mfr-portal → name=mfr-portal status=online(因 G1 偏差等价替换原 systemctl is-active 断言)A2=✅ ss -tlnp | awk '/:3000/ && /LISTEN/' | wc -l=1A3=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/=200A4=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/=200A5=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/login=200A6=✅ curl -s http://127.0.0.1/mfr/ | grep -c '/admin/_next/'=0(含 /mfr/_next 资源前缀改写生效) |
| OCL 日志编号 | L-20260827-03(架构偏差阻塞报告) → L-20260827-04(部署执行日志·已闭环) |
| 备注 / 异常 / 风险点 | 关键修复点(7 偏差 G1~G7 逐项处理见 OCL L-20260827-04 §一): ① G1 用 PM2 替代 systemd 管理 mfr-portal(注册为 PM2 条目并 pm2 save)② G2 S3-alt 从 112.74 打包 V1 standalone tarball 4.79MB 回救上传 8.218,补 /apps/mfr-portal ③ G3 PORT=3000 node 进程 ss LISTEN count=1 ④ G4 Caddyfile 走宿主机 /opt/weavely/Caddyfile → docker bind mount → validate/reload 走 docker exec dip1-prod-caddy caddy ... ⑤ G5 handle_path /mfr/ 插入 /admin/ 紧前面(LINE 56),顺序正确 ⑥ G6 本地已改 next.config.mjs 加 standalone+basePath="/mfr"(112 打包 V1 未包含,用 S8-v3 Referer 路由兜底 MFR _next/ 资源正确路由,OPR 互不干扰)⑦ G7 .env.production NEXT_PUBLIC_API_BASE_URL=/api 相对路径。额外修复(S8 语法错误回退): S8-v2 尝试 sub_filter → Caddy 报 unrecognized directive: sub_filter at handle_path(语法层级错误)→ 最终改用 @mfrNextStatic { path /_next/*; header Referer */mfr* } request matcher + 嵌套 handle,validate=✅ Valid configuration;reload exit 0。S11 D01 预验证结论: D01-1~3 公网 /mfr/ /mfr/login /mfr/quote 全部 200 ✅;MFR CSS/JS 带 Referer 请求 200 ✅;OPR 无 Referer 的 /_next/ 仍回 :3100 互不冲突 ✅;D01-4 /api/v2/qsv/quote/mfr-open POST=404(后端端点未实现)、OPTIONS CORS=400(Origin 未加入白名单)→ 不属于 B1+B2 部署范围,需听码补齐后端路由 / CORS origins 后方可通过 D01。凭据补登 CR-023/024/025 已入 credential-inventory.md(见 OCL §六)。 |
— 本任务单签发完毕 —
七、听云阻塞补记(2026-08-27 20:15 · 架构差距 7 项)¶
7.1 差距速查(外派单假设 vs 实际)¶
| # | 外派单 WBS 假设(来自 L-20260823-02 112.74.108.189 版) | 目标主机 8.218.165.94 实际(V2 完整生产架构) | 对原 WBS 的影响 |
|---|---|---|---|
| G1 | 前端用 systemd 管理:/etc/systemd/system/mfr-portal.service |
❌ /etc/systemd 无 mfr/dip1 条目。四前端实际由 PM2 管理:opr-admin(Next standalone 3100) / cst-app(serve 3102) / wkr-app(serve 3101) / dl-weavely(serve 8080) | B1 的 systemctl 命令失效,需改写 pm2 start |
| G2 | 目录 /opt/dip1/dip1/frontend/apps/mfr-portal 已就绪 |
❌ /opt/dip1 不存在。实际 Git repo 根目录是 /opt/weavely/src/dip1。apps/ 目录仅有 opr-admin / cst-app / wkr-app 三个前端,mfr-portal 未上传到目标主机 |
B1 的"启动 mfr-portal 进程"无从谈起 |
| G3 | 端口 3000 是 MFR listen port,监听可达 | ❌ 3000 无任何监听。已占用端口:3100=OPR、3101=WKR、3102=CST、8080=DL、8000=API(docker) | A2/A3 断言不成立 |
| G4 | Caddyfile 位置 /etc/caddy/Caddyfile,systemctl reload caddy 生效 |
❌ /etc/caddy 不存在。Caddy 由 Docker 容器 dip1-prod-caddy 运行,宿主机文件 /opt/weavely/Caddyfile → bind mount → 容器 /etc/caddy/Caddyfile(ro)。validate/reload 命令需走 docker exec dip1-prod-caddy caddy ... |
B2 命令可做,但要改工具链(不能原封不动执行) |
| G5 | 仅 Caddyfile /mfr/* 规则缺失 | ✅ 确实缺失;已实锤 /mfr/* 被兜底 handle { reverse_proxy :3100 } 扔到 OPR-Admin |
插入顺序逻辑正确,但因 B1 无效,插了也 502 |
| G6 | MFR 已带 basePath:"/mfr" + output:"standalone" |
❌ 本地 mfr-portal/next.config.mjs 只有 transpilePackages,无 standalone,无 basePath。对比 OPR-Admin 正确写法 basePath+standalone | 即便 deploy 也会 MFR 资源断链(/_next/ 未加 /mfr 前缀) |
| G7 | 后端 MFR-open 走 127.0.0.1:9100 uvicorn |
❌ 9100 不监听。后端 Docker dip1-prod-api 容器提供 127.0.0.1:8000 → Caddy /api/* 反代;/health 返回 v2.0.0 db=ok |
构建时 NEXT_PUBLIC_API_BASE_URL 须改为相对路径 /api 或直接 http://127.0.0.1:8000 |
7.2 建议的新 13 步路径(待用户决策启动)¶
详细见 OCL L-20260827-03 §五 S1~S13,简述:
1. S1:凭据补登 CR-023/024
2. S2:本地听码改 mfr-portal/next.config.mjs 加 output:"standalone" + basePath:"/mfr";改 .env.production 的 NEXT_PUBLIC_API_BASE_URL=/api
3. S3:本地听码在 workspace build MFR → .next/standalone/apps/mfr-portal/server.js + static
4. S4:打包 scp 到 8.218.165.94 解压到 /opt/weavely/src/dip1/frontend/apps/mfr-portal/
5. S5:PM2 注册 PORT=3000 pm2 start .next/standalone/apps/mfr-portal/server.js --name mfr-portal --max-memory-restart 256M + pm2 save
6. S6:本地断言 A1/A2/A3(pm2 online / ss :3000 / curl 200)
7. S7:备份 /opt/weavely/Caddyfile 带时间戳
8. S8:在 /admin/* 块前插入 handle_path /mfr/* { reverse_proxy http://127.0.0.1:3000 }
9. S9:docker exec dip1-prod-caddy caddy validate → caddy reload(两者都成功)
10. S10:本地断言 A4/A5/A6(/mfr/ 200、/mfr/login 200、无 /admin/_next/)
11. S11:公网预验证 D01 四项:/mfr/、/mfr/login、/mfr/quote、/api/v2/qsv/quote/mfr-open → 200
12. S12:OCL L-20260827-04 日志 + 回传听写(§六 A1~A6 补全通过)
13. S13:凭据补记、轮换计划、协作留痕
请用户就 §7.2 方案给出明确批准后,听云方可启动。若批准,建议由听码先完成 S2/S3(next.config.mjs 修改 + 本地构建),再由听云接收构建产物走 S4~S12 部署闭环,跨 AI 协作不越权。