fix_worker_ws_doc_encoding.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # -*- coding: utf-8 -*-
  2. import pathlib
  3. ROOT = pathlib.Path(r"D:\hdProject\newsaas\ylrz_saas\java\fs-worker-app\docs\worker-ws-task-push.md")
  4. CONTENT = r"""# 工作机 APP WebSocket 任务推送对接说明
  5. ## 1. 概述
  6. 销售 PC 端在「启动外呼任务」时,若任务为 **APP 外呼模式**(`callModel = 3 / 4 / 5`)且已绑定设备,服务端会通过 **同一条 WebSocket 长连接** 向对应工作机推送 `taskRun` 消息。
  7. - **心跳**与**任务推送**共用连接地址,通过消息体字段 `cmd` 区分类型。
  8. - APP 收到 `taskRun` 后,根据 `callModel` 跳转或刷新对应外呼页面。
  9. | 服务 | 端口 | 说明 |
  10. |------|------|------|
  11. | fs-worker-app | 8008 | WebSocket + APP HTTP 接口 |
  12. | fs-saas-company | 8006 | PC 端启动任务(`taskRun`) |
  13. ---
  14. ## 2. WebSocket 连接
  15. ### 2.1 连接地址(唯一)
  16. ```
  17. ws://{host}:8008/worker/webSocket/{meId}?tenantId={tenantId}
  18. ```
  19. | 参数 | 说明 |
  20. |------|------|
  21. | `meId` | 设备 IMEI,路径参数;须与登录 `meId`、主库 `company_sms_device.imei` 一致 |
  22. | `tenantId` | 租户 ID,查询参数,建议必传 |
  23. 示例:
  24. ```
  25. ws://192.168.11.62:8008/worker/webSocket/8601234561212129012?tenantId=338
  26. ```
  27. ### 2.2 连接生命周期
  28. 1. APP 登录成功后建立长连接(与心跳共用)。
  29. 2. 连接成功时,服务端主动推送一次当前设备状态。
  30. 3. APP 定时发送心跳(建议间隔 30s)。
  31. 4. PC 启动任务时,服务端可能额外推送 `taskRun`(无需 APP 回包)。
  32. ---
  33. ## 3. 消息协议(统一 JSON)
  34. 所有消息均为 JSON 文本,通过字段 `cmd` 区分类型。
  35. ### 3.1 消息类型一览
  36. | 方向 | `cmd` | 说明 |
  37. |------|-------|------|
  38. | APP → 服务端 | `heartbeat` | 心跳 |
  39. | APP → 服务端 | `deviceStatus` | 主动查询设备状态(可选) |
  40. | 服务端 → APP | `heartbeat` | 心跳回复 |
  41. | 服务端 → APP | `deviceStatus` | 禁用/离线等状态变更 |
  42. | 服务端 → APP | `taskRun` | **PC 启动任务推送** |
  43. | 服务端 → APP | `error` | 处理异常 |
  44. ---
  45. ## 4. 心跳(已有机制,与任务推送无关)
  46. ### 4.1 APP 发送
  47. ```json
  48. {
  49. "cmd": "heartbeat",
  50. "tenantId": 338,
  51. "appVersion": "1.0.0",
  52. "deviceName": "工作机A",
  53. "phone1": "13800138000",
  54. "phone2": "13900139000",
  55. "imei": "卡1IMEI,卡2IMEI"
  56. }
  57. ```
  58. ### 4.2 服务端回复
  59. ```json
  60. {
  61. "cmd": "heartbeat",
  62. "meId": "8601234561212129012",
  63. "status": 1,
  64. "msg": "ok",
  65. "appVersion": "1.0.0",
  66. "deviceName": "工作机A",
  67. "timestamp": 1718534400000
  68. }
  69. ```
  70. `status`:`0` 离线 / `1` 在线 / `2` 禁用。
  71. ### 4.3 APP 处理建议
  72. - 更新本地设备状态展示。
  73. - `status === 2` 时提示禁用并限制外呼。
  74. - 心跳逻辑保持现有实现,**无需为 `taskRun` 单独建连接**。
  75. ---
  76. ## 5. 任务推送 `taskRun`(本次对接重点)
  77. ### 5.1 触发条件(PC 端)
  78. PC 调用 `GET /company/companyVoiceRobotic/taskRun?id={任务ID}` 且满足:
  79. - 任务 `callModel` 为 `3`、`4` 或 `5`;
  80. - 任务 `deviceIds` 非空(已绑定设备);
  81. - 设备 IMEI 能解析成功。
  82. 不满足上述条件时 **不会推送** `taskRun`。
  83. ### 5.2 推送链路
  84. ```
  85. PC taskRun (8006)
  86. → Redis PUBLISH channel: worker:ws:task:run
  87. → fs-worker-app Redisson 订阅
  88. → WorkerWebSocketServer.pushTaskRunToDevices
  89. → 向在线设备的 WebSocket Session 发送消息
  90. ```
  91. **前置条件**:8006 与 8008 使用同一 Redis 实例;APP 已连接 WS 且 `meId` 在推送列表中。
  92. ### 5.3 服务端 → APP 消息体
  93. ```json
  94. {
  95. "cmd": "taskRun",
  96. "robotic": 294,
  97. "callModel": 3,
  98. "timestamp": 1718534400000
  99. }
  100. ```
  101. | 字段 | 类型 | 说明 |
  102. |------|------|------|
  103. | `cmd` | String | 固定 `taskRun` |
  104. | `robotic` | Long | 外呼任务 ID(`company_voice_robotic.id`) |
  105. | `callModel` | Integer | 外呼模式:`3` / `4` / `5` |
  106. | `timestamp` | Long | 服务端时间戳(毫秒) |
  107. **说明**:`taskRun` 为服务端单向推送,APP **不需要回包**。
  108. ---
  109. ## 6. callModel 与页面对应关系
  110. | callModel | 名称 | 业务含义 | APP 页面/行为建议 |
  111. |-----------|------|----------|-------------------|
  112. | **3** | APP-网页点呼 | 销售在 APP 上手动点选客户逐个拨打 | 跳转或刷新 **网页点呼页**;展示该 `robotic` 任务下的待拨客户列表,用户手动点击拨打 |
  113. | **4** | APP-自动呼叫 | APP 按任务自动连续外呼 | 跳转或刷新 **自动呼叫页**;进入自动拨号流程,按队列自动拨打 |
  114. | **5** | APP-AI呼叫 | APP 侧 AI 辅助 / AI 外呼流程 | 跳转或刷新 **AI 呼叫页**;进入 AI 外呼交互流程 |
  115. 后端枚举定义见 `AiCallModeEnum`:
  116. - `3` = `APP_WEB`(APP-网页点呼)
  117. - `4` = `APP_AUTO`(APP-自动呼叫)
  118. - `5` = `APP_AI`(APP-AI呼叫)
  119. **注意**:`callModel` 为 `1`(人工外呼)、`2`(AI 自动外呼 / EasyCall)时 **不会** 收到 `taskRun` 推送。
  120. ### 6.1 收到 `taskRun` 后的推荐处理流程
  121. ```
  122. onWebSocketMessage(json)
  123. → 解析 cmd
  124. → if cmd == "taskRun":
  125. roboticId = json.robotic
  126. callModel = json.callModel
  127. switch (callModel):
  128. 3 → 打开/刷新「网页点呼」页,带 roboticId,拉取任务与客户列表
  129. 4 → 打开/刷新「自动呼叫」页,带 roboticId,启动自动拨号
  130. 5 → 打开/刷新「AI 呼叫」页,带 roboticId,进入 AI 外呼流程
  131. ```
  132. 若当前已在对应模式页面,仅需 **刷新任务列表/客户列表**(调用下方 HTTP 接口),不必重复跳转。
  133. ---
  134. ## 7. 配套 HTTP 接口(刷新页面数据)
  135. 收到 `taskRun` 后,按 `callModel` 调用对应接口拉取数据(均需登录态 `WorkerToken`)。
  136. ### 7.1 执行中任务统计列表
  137. ```
  138. GET /company/companyVoiceRobotic/myTaskStatList?callModel={3|4|5}&meId={meId}
  139. ```
  140. - `callModel`:与推送消息一致(3 / 4 / 5)
  141. - `meId`:当前设备 IMEI
  142. 用于各外呼模式首页的任务列表刷新。
  143. ### 7.2 任务拨打统计 / 客户列表
  144. ```
  145. GET /company/companyVoiceRoboticCallLogCallphone/...?roboticId={robotic}&callModel={3|4|5}&meId={meId}
  146. ```
  147. (具体路径见 `CompanyVoiceRoboticCallLogCallphoneController`,参数需与 `callModel`、`meId` 一致。)
  148. ### 7.3 拨打前校验
  149. ```
  150. POST /app/aiSipCall/validateAppCall
  151. ```
  152. Body 示例:
  153. ```json
  154. {
  155. "roboticId": 294,
  156. "calleeId": 12345,
  157. "meId": "8601234561212129012",
  158. "callModel": 3
  159. }
  160. ```
  161. ### 7.4 外呼结果回调
  162. ```
  163. POST /app/aiSipCall/appCardCallBack
  164. ```
  165. 通话结束后回调,Body 中需带 `callModel`(默认 3)。
  166. ### 7.5 暂停/继续任务
  167. ```
  168. POST /company/companyVoiceRobotic/pauseRoboticActive
  169. ```
  170. 与 PC 端共用,APP / PC 任意一端操作会联动。
  171. ---
  172. ## 8. APP 端集成示例(伪代码)
  173. ```javascript
  174. // 同一 WebSocket 连接
  175. const ws = new WebSocket(
  176. `ws://${host}:8008/worker/webSocket/${meId}?tenantId=${tenantId}`
  177. );
  178. ws.onopen = () => {
  179. startHeartbeat(); // 定时发 cmd=heartbeat
  180. };
  181. ws.onmessage = (event) => {
  182. const msg = JSON.parse(event.data);
  183. switch (msg.cmd) {
  184. case 'heartbeat':
  185. handleHeartbeatReply(msg); // 更新在线/禁用状态
  186. break;
  187. case 'deviceStatus':
  188. handleDeviceStatus(msg); // 禁用、离线提示
  189. break;
  190. case 'taskRun':
  191. handleTaskRunPush(msg); // 任务推送(见下)
  192. break;
  193. case 'error':
  194. showError(msg.msg);
  195. break;
  196. }
  197. };
  198. function handleTaskRunPush(msg) {
  199. const { robotic, callModel } = msg;
  200. if (callModel === 3) {
  201. navigateOrRefresh('WebClickCallPage', { roboticId: robotic, callModel: 3 });
  202. } else if (callModel === 4) {
  203. navigateOrRefresh('AutoCallPage', { roboticId: robotic, callModel: 4 });
  204. } else if (callModel === 5) {
  205. navigateOrRefresh('AiCallPage', { roboticId: robotic, callModel: 5 });
  206. }
  207. fetchMyTaskStatList(callModel, meId);
  208. }
  209. function startHeartbeat() {
  210. setInterval(() => {
  211. if (ws.readyState === WebSocket.OPEN) {
  212. ws.send(JSON.stringify({
  213. cmd: 'heartbeat',
  214. tenantId,
  215. appVersion,
  216. deviceName,
  217. phone1,
  218. phone2,
  219. imei: cardImeiJoined
  220. }));
  221. }
  222. }, 30000);
  223. }
  224. ```
  225. ---
  226. ## 9. 联调检查清单
  227. | 步骤 | 期望现象 |
  228. |------|----------|
  229. | APP 连接 WS | 服务端日志:`[WorkerWS] 设备连接: meId=...` |
  230. | 8008 启动 | 日志:`[WorkerTaskRunWS] Redisson 订阅成功 channel=worker:ws:task:run` |
  231. | PC 点「启动任务」 | 8006:`[WorkerTaskRunWS] 已发布 roboticId=... callModel=... meIds=[...]` |
  232. | 8008 收 Redis | `[WorkerTaskRunWS] 收到消息 roboticId=...` |
  233. | 设备在线 | `[WorkerWS] 任务启动推送成功 meId=...` |
  234. | APP 收消息 | `cmd=taskRun`,按 `callModel` 进入对应页面 |
  235. **常见问题**:
  236. 1. **收不到 `taskRun`**:检查 `meId` 是否与 `deviceIds` 解析出的 IMEI 一致;APP 是否已连 WS。
  237. 2. **8008 收到消息但 APP 没反应**:检查 APP `onMessage` 是否处理 `cmd === 'taskRun'`。
  238. 3. **推送成功但页面无数据**:检查刷新接口的 `callModel`、`meId` 是否与推送一致。
  239. ---
  240. ## 10. 附录:完整消息字段(WorkerWsMsg)
  241. | 字段 | 类型 | 说明 |
  242. |------|------|------|
  243. | `cmd` | String | `heartbeat` / `deviceStatus` / `taskRun` / `error` |
  244. | `meId` | String | 设备 IMEI |
  245. | `tenantId` | Long | 租户 ID |
  246. | `appVersion` | String | APP 版本 |
  247. | `deviceName` | String | 设备名称 |
  248. | `phone1` / `phone2` | String | 双卡手机号 |
  249. | `imei` | String | SIM 卡 IMEI(逗号分隔) |
  250. | `status` | Integer | 0 离线 / 1 在线 / 2 禁用 |
  251. | `msg` | String | 提示信息 |
  252. | `timestamp` | Long | 时间戳 |
  253. | `robotic` | Long | 任务 ID(仅 `taskRun`) |
  254. | `callModel` | Integer | 外呼模式(仅 `taskRun`) |
  255. ---
  256. **文档版本**:与 `fs-worker-app` WebSocket + Redis 任务推送实现同步。
  257. **Redis 通道**:`worker:ws:task:run`(`WorkerWsConstants.REDIS_CHANNEL_TASK_RUN`)。
  258. """
  259. ROOT.write_text(CONTENT, encoding="utf-8", newline="\n")
  260. print("written:", ROOT, "bytes:", len(CONTENT.encode("utf-8")))