diff --git a/docs/superpowers/plans/2026-10-19-customer-service-im-reliability.md b/docs/superpowers/plans/2026-10-19-customer-service-im-reliability.md new file mode 100644 index 0000000..ddc3c0f --- /dev/null +++ b/docs/superpowers/plans/2026-10-19-customer-service-im-reliability.md @@ -0,0 +1,78 @@ +# Customer Service IM Reliability Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore customer-service message and image delivery after WebSocket interruptions and route every browser IM request through its current frontend origin. + +**Architecture:** Browser-facing session endpoints are relative paths resolved against the current origin, while server-to-server calls use Docker-internal endpoints. Caddy routes the IM API, file service, and WebSocket before each site's frontend fallback. The mobile chat client reauthenticates and resubscribes after SDK reconnects. + +**Tech Stack:** Go, Vue 3, TypeScript, Tinode SDK, Vitest, Docker Compose, Caddy, Python/Bash deployment contract tests. + +--- + +### Task 1: Relative browser endpoint contract + +**Files:** +- Modify: `internal/support/service.go` +- Modify: `internal/support/service_test.go` + +- [ ] Write a failing Go test asserting `OpenSession` returns `/v1/im`, `/v0/channels`, and `/v0/file` even when internal IM configuration contains an absolute host. +- [ ] Run `go test ./internal/support -run TestOpenSessionReturnsSameOriginEndpoints -v` and confirm the absolute-host assertion failure. +- [ ] Return the three relative public endpoints from `OpenSession` without changing `imclient.Config()` used for server calls. +- [ ] Re-run the targeted test and `go test ./internal/support ./internal/imclient ./internal/httpapi`. + +### Task 2: Mobile reconnect and media authentication + +**Files:** +- Modify: `web/package.json` +- Modify: `web/pnpm-lock.yaml` +- Modify: `web/src/mobile/im.ts` +- Create: `web/src/mobile/im.test.ts` +- Modify: `web/src/mobile/views/SupportView.vue` + +- [ ] Add Vitest and a test script. +- [ ] Write failing tests for same-origin endpoint resolution, initial connect/login/subscribe, reconnect login/resubscribe with later data, and intentional disconnect. +- [ ] Run `pnpm test --run` and confirm the failures are caused by missing reconnect behavior. +- [ ] Add serialized reconnect recovery, connection-state callbacks, relative endpoint resolution, and Header-based upload authentication. +- [ ] Add an offline/reconnecting banner, disable sending while disconnected, and add media error/retry UI. +- [ ] Run `pnpm test --run`, `pnpm run build`, and the Go package tests. + +### Task 3: Remove fixed IM public origins from im_webapp + +**Files:** +- Modify: `../im_webapp/tests/test_actions_workflow.py` +- Modify: `../im_webapp/.gitea/workflows/deploy.yml` +- Modify: `../im_webapp/Dockerfile` +- Modify: `../im_webapp/web/src/lib/config.ts` +- Modify or create: `../im_webapp/web/tests/same-origin-endpoints.test.mjs` + +- [ ] Change contract tests first so they reject fixed production API/file build arguments and the legacy default WebSocket host. +- [ ] Run the Python and Node tests and confirm they fail on the current fixed values. +- [ ] Remove fixed endpoint build arguments, make file base default empty, and derive the fallback WebSocket host from `window.location.host`. +- [ ] Run Python contracts, Node tests, and `pnpm run build`. + +### Task 4: Caddy same-origin route contract + +**Files:** +- Modify: `../deploy/olns-stack/test-deployment.sh` +- Modify: `../deploy/olns-stack/Caddyfile` +- Modify: `../deploy/olns-stack/compose.yaml` +- Modify: `.gitea/workflows/deploy.yml` + +- [ ] Add failing deployment assertions requiring `/v1/im*`, `/v0/file*`, and `/v0/channels*` to be handled before frontend fallbacks for both site groups. +- [ ] Run `bash ../deploy/olns-stack/test-deployment.sh` and confirm failure. +- [ ] Add the Caddy route handlers with no-store responses. +- [ ] Switch server-side IM HTTP/WS/base values to `im-server:6060` in Compose and the OLONS deployment overlay. +- [ ] Re-run deployment contracts and workflow tests. + +### Task 5: Integration and production verification + +**Files:** +- Update: `../DEPLOYMENT_RECORD.md` with non-secret route and acceptance notes. + +- [ ] Run full Go, web, workflow and deployment test suites and builds. +- [ ] Commit each repository, merge the task branch into current `pro`, and push `pro` only after all local gates pass. +- [ ] Back up `/opt/olns/Caddyfile` and Compose files, install the tested Caddy/Compose changes, validate Caddy, and reload atomically. +- [ ] Wait for both Actions deployments and verify image digests, container health and restart counts. +- [ ] Verify all five domains return IM responses instead of SPA HTML, WebSocket upgrades to 101, and file responses are not HTML. +- [ ] Exercise a real support session, interrupt/recover its network connection, and verify a subsequent text and image arrive without refreshing. diff --git a/docs/superpowers/specs/2026-10-19-customer-service-im-reliability-design.md b/docs/superpowers/specs/2026-10-19-customer-service-im-reliability-design.md new file mode 100644 index 0000000..047ea07 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-customer-service-im-reliability-design.md @@ -0,0 +1,43 @@ +# 客服 IM 投递可靠性设计 + +## 目标 + +修复 OLONS 会员端在 WebSocket 异常断线后无法继续接收客服文本或图片的问题,并保证 IM API、文件和 WebSocket 全部通过用户当前访问的前端域名转发。 + +## 范围 + +- `olons_server` 会员客服客户端的断线恢复、连接状态和媒体加载错误处理。 +- `olons_server` 客服会话返回给浏览器的公开端点。 +- `im_webapp` 生产构建中的固定公网域名。 +- 单机 Compose Caddy 的同源路径路由和部署契约。 +- 生产发布及五个前端域名的文本、图片、WebSocket 验收。 + +不修改会员、消息或文件业务数据,不改支付、提现及其他业务功能。 + +## 架构 + +浏览器公开端点使用相对路径:`/v1/im`、`/v0/file`、`/v0/channels`。客户端以 `window.location.origin` 解析 HTTP/文件地址,并将当前页面协议转换为 `ws` 或 `wss`。因此同一镜像在 `olons.yuexuan.shop` 和三个公测域名下会自动使用当前域名,不读取或信任任意 Host Header。 + +Caddy 在五个前端域名中优先把上述路径代理到 `im-server:6060`,其余路径仍进入原来的前端或 OLONS 服务。代理响应明确禁止缓存。`olons-server` 自身调用 IM 时使用 Docker 网络中的 `im-server:6060`,不再绕行 Cloudflare。 + +## 断线恢复 + +`CustomerServiceChat` 保存 token、当前 Topic 和最新 seq。首次连接仍执行 connect、token 登录和历史订阅。SDK 重新建立 WebSocket 后,客户端再次 token 登录,重新订阅当前 Topic,并通过 later-data 查询补拉本地最后 seq 之后的数据。恢复过程串行化,避免多个 reconnect 回调造成重复登录或订阅。 + +页面接收连接状态:`connecting`、`online`、`reconnecting`、`offline`。断线时保留已显示消息但禁止发送,恢复成功后解除禁止。主动离开页面不触发恢复。 + +## 图片处理 + +上传使用 `X-MeetingIM-APIKey` 和 `Authorization: Token ` Header,不再把 token 放在 URL 查询参数中。下载继续使用当前域名;图片或视频加载失败时显示失败提示和重试入口,不能静默显示空白。 + +## 测试 + +- TypeScript 单测覆盖相对端点解析、首次订阅、断线重连、重新登录、重新订阅、补拉和主动断开。 +- Go 单测覆盖客服会话始终返回相对公开端点。 +- `im_webapp` 工作流契约禁止固定 `im-api.yuexuan.shop` 和旧 `118im.com` 文件/WS地址。 +- 部署契约要求五个域名都有三条同源路由,并保证 catch-all 顺序正确。 +- 生产验收验证 JSON API、文件、WebSocket 101、断线恢复后的文本和图片,不以数据库序号代替浏览器展示验收。 + +## 回滚 + +应用镜像可回滚到发布前 digest;Caddy 修改前保留生产文件副本。回滚只涉及 `olons-server`、`im-webapp` 和 Caddy,不修改数据库或 MinIO 数据。