跳转至

听云 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=1
A3=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/=200
A4=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/=200
A5=✅ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/mfr/login=200
A6=✅ 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/Caddyfilesystemctl 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.mjsoutput:"standalone" + basePath:"/mfr";改 .env.productionNEXT_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 validatecaddy 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 协作不越权。