GoChat 商务通 Connector
该服务把多个商务通客服账号接入 GoChat 的 Channel::Shangwutong 收件箱。一个 Connector 进程统一管理全部账号,每个 Inbox 对应一个独立 supervisor;账号、session、cursor、入站队列和出站队列持久化在同一个 SQLite 文件中。
GoChat 商务通 Inbox 是唯一配置入口。Connector 不提供账号 CRUD 后台,也不接受启动参数中的账号密码。Inbox 创建或更新后,GoChat 发送签名 lifecycle webhook,Connector 再使用 account-scoped service token 拉取收件箱级配置。
完整协议、Schema、消息映射和验收边界见 开发计划,生产操作见 运行手册。
本地构建与测试
需要 Go 1.26.x。所有命令从本目录执行:
go tool sqlc generate
go test ./...
go vet ./...
go build -o /tmp/shangwutong-build-check ./cmd/shangwutong
# 500 账号等价协议长时间验证(非常规 CI)
SWT_SOAK_DURATION=1h go test ./internal/account \
-run TestManagerMaintainsOneSupervisorPer500Accounts -count=1 -timeout 70m
sqlc 生成代码提交在 db/generated/。修改 db/queries/*.sql 或 migration 后必须重新生成并确认工作区没有生成漂移。长时间测试使用 fake SWT 协议客户端验证 supervisor、心跳调度、SQLite 和退出清理;不代替真实商务通的单 IP/域名限流证据。
启动配置
SWT_CONNECTOR_LISTEN=:9100
SWT_CONNECTOR_DB_PATH=/data/connector.db
GOCHAT_BASE_URL=http://gochat:3000
GOCHAT_CONNECTOR_SERVICE_TOKEN=<account-scoped service token>
SWT_MAX_INFLIGHT_HEARTBEATS=64
SWT_INBOUND_WORKERS=8
SWT_OUTBOUND_WORKERS=8
SWT_SHUTDOWN_TIMEOUT=30s
只有前四项中的数据库路径、GoChat 地址和 service token 必填,监听地址有默认值。GOCHAT_CONNECTOR_SERVICE_TOKEN 只授予要接入的 GoChat Account,并仅允许 Connector 路由。不要增加 GOCHAT_SWT_* 全局配置;账号登录字段、presence 和 webhook URL 都属于 Inbox 配置。
SQLite 父目录必须已存在并且对运行用户可写。Connector 会把数据库、WAL/SHM 和在线备份权限收紧为 0600,镜像内 /data 和 /backup 为 0700;宿主机挂载目录仍需由部署层限制为 Connector 运行用户可访问。密码按内部系统约定明文保存在 GoChat 专用配置表和 Connector SQLite 中。
export SWT_CONNECTOR_DB_PATH="$PWD/tmp/connector.db"
export GOCHAT_BASE_URL="http://127.0.0.1:3000"
export GOCHAT_CONNECTOR_SERVICE_TOKEN="..."
mkdir -p "$(dirname "$SWT_CONNECTOR_DB_PATH")"
go run ./cmd/shangwutong serve
容器
镜像构建上下文必须是仓库根目录:
docker build -f channels/shangwutong/Dockerfile -t gochat/shangwutong:dev .
生产 Compose 已包含 shangwutong 服务、SQLite 数据卷和独立 /backup 备份卷。Quickstart 中该服务位于 shangwutong profile,避免未配置 service token 时影响普通 GoChat 启动:
cd deploy/quickstart
GOCHAT_CONNECTOR_SERVICE_TOKEN='...' docker compose --profile shangwutong up -d --build
在 GoChat 创建 Inbox 时 webhook URL 填写容器网络地址:
http://shangwutong:9100/webhooks/gochat/v1
运维端点与命令
GET /healthz
GET /readyz
GET /metrics
POST /internal/reconcile # 仅 loopback
POST /internal/operations/accept-transfer # 仅 loopback
POST /internal/operations/transfer-conversation # 仅 loopback
接管请求体为 {"inbox_id":10,"conversation_id":100,"sid":"...","event_id":"...","occurred_at":"2026-08-15T07:00:00Z"},其中 conversation_id 是 GoChat internal ID。新请求只接受已映射到该 conversation 且最新商务通状态为“转接中”的 SID;相同请求返回同一队列记录。只有 operation 进入 delivered 才证明 oc/accepttransfer.aspx 返回了 r=ok,最终坐席归属继续以现有 swt_state=transfer_accepted 和 swt_assignee_name 同步结果为准。
发起请求体字段相同。它只接受当前由其他坐席持有、最新状态为 5 的 SID,目标坐席从已同步的 swt_assignee_name 推导;Connector 以当前登录坐席为 oname、原坐席为 oname1 调用 oc/Transfer0.aspx。成功后仍须等待状态 7 再调用接受入口;接受后的归属以状态 8 及后续 swt_assignee_name 回执为准。
shangwutong migrate up
shangwutong migrate status
shangwutong reconcile
shangwutong backup --output /backup/connector-$(date +%F).db
shangwutong doctor
healthz 只表示进程存活;readyz 使用轻量 SQLite quick check 并检查写锁。doctor 检查配置、migration、完整的 SQLite integrity check 和 GoChat 配置 API,但不会登录商务通。backup 使用 SQLite VACUUM INTO,不要直接复制活动中的 DB/WAL 文件。
资料与分类能力边界
- GoChat 人工改名只发送
sid/cid/cname;在远程cnote缺省/空值语义完成协议验证前,Connector 不发送空cnote,也不把 GoChat Contact Note 映射到商务通备注。 - 改名结果通过持久化 operation 回写
pending/succeeded/failed/uncertain;没有 CID 时操作保留在队列中等待入站 CID,CID 到达会唤醒同一 SID 的待处理操作,24 小时后以cid_wait_timeout失败,不能静默丢弃。 set_chat_kind与set_customer_color的结果状态独立保存;客户颜色只在同一 GoChat account/inbox/contact-inbox(同一远程 CID 作用域)传播,不按裸 CID 跨 inbox/account 合并。- 商务通分类/颜色不创建或修改 GoChat Contact Label、Conversation Label、CRM 标签;GoChat 原生联系人备注仍使用本地
notes链路。 - kind=52 当前只用于 CID/映射语义,未验证的历史正文、主动历史拉取、消息编辑、反应和 RESET 均不宣称支持。
可靠性边界
-
GoChat webhook 的 2xx 只表示 Connector 已持久化,不表示商务通已发送。
-
商务通明确成功后回写
sent;永久失败回写failed;请求已经写出但结果未知时回写uncertain。 -
uncertain 默认观察 5 分钟。同账号后续发送在此期间被顺序屏障阻塞;消息超时转 failed 后释放,分类/改名仍保留 uncertain(含
uncertain_timeout)并等待人工/结果补偿。晚到且唯一匹配的 kind=3 回显仍可纠正为 sent。 -
oc/send.aspx不返回消息 ID。kind=2/3 心跳中的原始seq_id才是商务通消息 ID;组合 source ID 只用于幂等,不能冒充外部消息 ID。 -
kind=52 没有真实版本 fixture 前保留 raw-only,不伪造 child 消息 ID。
-
Connector 联系人写回携带 origin/event ID;GoChat 远程来源事件不反向生成 rename。联系人状态和 webhook outbox 采用同一事务,enqueue 失败会回滚 pending。
-
启动时先从 SQLite 恢复 supervisor,再异步拉取 GoChat 全量配置;首次完整快照失败会从 1 秒指数退避到 5 分钟持续重试,不依赖 GoChat health 才启动进程。分类 catalog 同步先持久化本地
classification_sync_results,读取失败与 GoChat 回传失败分开重试;重启恢复pending/syncing/failed,不把 202 当作已完成。 -
SIGTERM 会先停止 readiness 和 HTTP 接收,再取消 worker、等待 supervisor、checkpoint WAL;升级不会主动批量 logout。
-
结果回调耗尽时使用
compensate-result --account-id ... --operation-id ... --actor ... --reason ...;该命令只重放已保存结果,不重复商务通操作,并写入审计表。Connector webhook 明确 4xx 会将匹配的本地 pending 标为 failed,超时/耗尽只标为 uncertain。
自动化测试通过不等于真实商务通生产可用。本地 500 账号一小时 fake protocol soak 只证明调度、SQLite 和事件持久化;登录、收发、presence、密码更新、负 kind cursor、kind=52、真实媒体 URL、验证码流程和真实商务通限流必须按运行手册留存证据。