正在显示
9 个修改的文件
包含
619 行增加
和
45 行删除
HIGHLIGHT_API.md
0 → 100644
| 1 | +# WebScreen 高光录制接口文档 | ||
| 2 | + | ||
| 3 | +## 1. 文档范围 | ||
| 4 | + | ||
| 5 | +本文汇总 WebScreen 对外提供的全部高光接口,以及 WebScreen 依赖的 SaaS 内部接口。 | ||
| 6 | + | ||
| 7 | +高光录制与整堂录制是两套独立业务: | ||
| 8 | + | ||
| 9 | +- 原整堂录制接口保持不变。 | ||
| 10 | +- 所有高光接口统一使用 `/highlight` 前缀。 | ||
| 11 | +- `HIGHLIGHTCONFIG.enabled` 只控制高光查询、录制和文件检查,不影响原整堂录制;`GET /highlight/status` 始终可用于健康检查。 | ||
| 12 | +- 高光文件使用 `{classId}_highlight_{highlightId}.mp4`,不会覆盖整堂录像 `{classId}.mp4`。 | ||
| 13 | + | ||
| 14 | +示例服务地址: | ||
| 15 | + | ||
| 16 | +```text | ||
| 17 | +http://127.0.0.1:3001 | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +除 `GET /highlight/status` 外,请求均使用: | ||
| 21 | + | ||
| 22 | +```http | ||
| 23 | +Content-Type: application/json | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +当前 WebScreen 接口没有应用层鉴权,只应开放给可信内网或经过访问控制的调用方。 | ||
| 27 | + | ||
| 28 | +## 2. 接口总览 | ||
| 29 | + | ||
| 30 | +| 方法 | 路径 | 用途 | 是否启动录制 | | ||
| 31 | +|---|---|---|---| | ||
| 32 | +| POST | `/highlight/preview/by-class` | 预览指定课堂的全部高光及目标文件信息 | 否 | | ||
| 33 | +| POST | `/highlight/preview/by-site` | 预览指定站点、时间范围内的全部高光 | 否 | | ||
| 34 | +| POST | `/highlight/recording/by-class` | 提交指定课堂的全部高光录制 | 是 | | ||
| 35 | +| POST | `/highlight/recording/by-site` | 提交指定站点、时间范围内的全部高光录制 | 是 | | ||
| 36 | +| POST | `/highlight/recording/scheduled` | 运行一次独立的高光定时任务 | 是 | | ||
| 37 | +| POST | `/highlight/fileExists` | 查询指定课堂的全部高光文件状态 | 否 | | ||
| 38 | +| POST | `/highlight/files` | 根据高光明细批量查询文件状态 | 否 | | ||
| 39 | +| GET | `/highlight/status` | 查询当前进程内的高光队列状态 | 否 | | ||
| 40 | + | ||
| 41 | +## 3. 公共约定 | ||
| 42 | + | ||
| 43 | +### 3.1 高光字段 | ||
| 44 | + | ||
| 45 | +| 字段 | 类型 | 说明 | | ||
| 46 | +|---|---|---| | ||
| 47 | +| `highlightId` | Integer | 高光唯一 ID,对应 SaaS 高光记录的 `id` | | ||
| 48 | +| `classId` | String | 课堂号,对应 SaaS 高光记录的 `meetingNumber` | | ||
| 49 | +| `siteId` | String | 机构编码,必须位于 `HIGHLIGHTCONFIG.siteIds` | | ||
| 50 | +| `beginTime` | Integer | 高光开始时间,13 位毫秒时间戳 | | ||
| 51 | +| `endTime` | Integer | 高光结束时间,13 位毫秒时间戳 | | ||
| 52 | + | ||
| 53 | +`classId`、`siteId` 只允许字母、数字、下划线和短横线,最长 128 个字符。 | ||
| 54 | + | ||
| 55 | +### 3.2 任务状态 | ||
| 56 | + | ||
| 57 | +| 状态 | 说明 | | ||
| 58 | +|---|---| | ||
| 59 | +| `not_generated` | 本地、OSS 和当前任务队列均未发现文件 | | ||
| 60 | +| `queued` | 已进入等待队列 | | ||
| 61 | +| `recording` | `web_capture_c` 正在录制 | | ||
| 62 | +| `uploading` | 本地文件已生成,等待服务器搬运到 OSS | | ||
| 63 | +| `generated` | OSS 文件已经存在,可以返回播放地址 | | ||
| 64 | +| `invalid` | 高光字段、站点或时间段校验失败 | | ||
| 65 | +| `failed` | 录制任务执行失败 | | ||
| 66 | + | ||
| 67 | +### 3.3 HTTP 状态和错误响应 | ||
| 68 | + | ||
| 69 | +| HTTP 状态 | `code` | 说明 | | ||
| 70 | +|---|---:|---| | ||
| 71 | +| 200 | 0 或 1 | 请求正常完成;文件查询接口使用 1 表示尚未全部生成 | | ||
| 72 | +| 400 | 10 | 请求参数错误、站点未启用或高光功能未启用 | | ||
| 73 | +| 502 | SaaS 返回码或 -1 | SaaS 高光接口、录制任务接口或 OSS 查询失败 | | ||
| 74 | +| 500 | -1 | WebScreen 内部异常 | | ||
| 75 | + | ||
| 76 | +错误示例: | ||
| 77 | + | ||
| 78 | +```json | ||
| 79 | +{ | ||
| 80 | + "code": 10, | ||
| 81 | + "message": "高光录制功能未启用" | ||
| 82 | +} | ||
| 83 | +``` | ||
| 84 | + | ||
| 85 | +## 4. 预览课堂高光 | ||
| 86 | + | ||
| 87 | +```http | ||
| 88 | +POST /highlight/preview/by-class | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +该接口查询 SaaS 高光数据并计算录制地址、文件名和当前文件状态,但不会加入录制队列。 | ||
| 92 | + | ||
| 93 | +请求: | ||
| 94 | + | ||
| 95 | +```json | ||
| 96 | +{ | ||
| 97 | + "classId": "2008975651" | ||
| 98 | +} | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +成功响应: | ||
| 102 | + | ||
| 103 | +```json | ||
| 104 | +{ | ||
| 105 | + "code": 0, | ||
| 106 | + "message": "success", | ||
| 107 | + "data": [ | ||
| 108 | + { | ||
| 109 | + "highlightId": 5, | ||
| 110 | + "classId": "2008975651", | ||
| 111 | + "siteId": "xdyui2", | ||
| 112 | + "beginTime": 1785895298000, | ||
| 113 | + "endTime": 1785895343000, | ||
| 114 | + "duration": 45000, | ||
| 115 | + "status": "not_generated", | ||
| 116 | + "fileName": "2008975651_highlight_5.mp4", | ||
| 117 | + "localPath": "/root/web_capture_release/media/xdyui2/20260805/2008975651_highlight_5.mp4", | ||
| 118 | + "ossKey": "oss/xdyui2/20260805/2008975651_highlight_5.mp4", | ||
| 119 | + "url": null, | ||
| 120 | + "playbackUrl": "https://pclive.xuedianyun.com/...&recBeginTime=1785895298000&recEndTime=1785895343000" | ||
| 121 | + } | ||
| 122 | + ] | ||
| 123 | +} | ||
| 124 | +``` | ||
| 125 | + | ||
| 126 | +没有高光时返回 `code=0`、`message=无高光数据`、`data=[]`。 | ||
| 127 | + | ||
| 128 | +## 5. 预览站点高光 | ||
| 129 | + | ||
| 130 | +```http | ||
| 131 | +POST /highlight/preview/by-site | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +请求字段: | ||
| 135 | + | ||
| 136 | +| 字段 | 类型 | 必填 | 说明 | | ||
| 137 | +|---|---|---|---| | ||
| 138 | +| `siteId` | String | 是 | 机构编码 | | ||
| 139 | +| `beginTime` | Integer | 否 | 查询开始时间,13 位毫秒时间戳 | | ||
| 140 | +| `endTime` | Integer | 否 | 查询结束时间,13 位毫秒时间戳 | | ||
| 141 | +| `pageSize` | Integer | 否 | 调用 SaaS 时的分页大小,范围 1~1000 | | ||
| 142 | + | ||
| 143 | +请求: | ||
| 144 | + | ||
| 145 | +```json | ||
| 146 | +{ | ||
| 147 | + "siteId": "xdyui2", | ||
| 148 | + "beginTime": 1785859200000, | ||
| 149 | + "endTime": 1785945599999, | ||
| 150 | + "pageSize": 100 | ||
| 151 | +} | ||
| 152 | +``` | ||
| 153 | + | ||
| 154 | +WebScreen 会自动读取全部分页。响应项与 `/highlight/preview/by-class` 相同。 | ||
| 155 | + | ||
| 156 | +## 6. 按课堂提交高光录制 | ||
| 157 | + | ||
| 158 | +```http | ||
| 159 | +POST /highlight/recording/by-class | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +请求: | ||
| 163 | + | ||
| 164 | +```json | ||
| 165 | +{ | ||
| 166 | + "classId": "2008975651" | ||
| 167 | +} | ||
| 168 | +``` | ||
| 169 | + | ||
| 170 | +成功响应: | ||
| 171 | + | ||
| 172 | +```json | ||
| 173 | +{ | ||
| 174 | + "code": 0, | ||
| 175 | + "message": "success", | ||
| 176 | + "data": [ | ||
| 177 | + { | ||
| 178 | + "highlightId": 5, | ||
| 179 | + "classId": "2008975651", | ||
| 180 | + "siteId": "xdyui2", | ||
| 181 | + "beginTime": 1785895298000, | ||
| 182 | + "endTime": 1785895343000, | ||
| 183 | + "status": "queued" | ||
| 184 | + } | ||
| 185 | + ] | ||
| 186 | +} | ||
| 187 | +``` | ||
| 188 | + | ||
| 189 | +接口返回表示任务已经接收,不表示 MP4 已经生成。调用方应继续查询 `/highlight/fileExists` 或 `/highlight/status`。 | ||
| 190 | + | ||
| 191 | +重复提交同一 `siteId + highlightId` 时,WebScreen 会检查内存任务、本地文件和 OSS,不会重复录制已经存在的高光。 | ||
| 192 | + | ||
| 193 | +## 7. 按站点提交高光录制 | ||
| 194 | + | ||
| 195 | +```http | ||
| 196 | +POST /highlight/recording/by-site | ||
| 197 | +``` | ||
| 198 | + | ||
| 199 | +请求字段与 `/highlight/preview/by-site` 相同。 | ||
| 200 | + | ||
| 201 | +请求: | ||
| 202 | + | ||
| 203 | +```json | ||
| 204 | +{ | ||
| 205 | + "siteId": "xdyui2", | ||
| 206 | + "beginTime": 1785859200000, | ||
| 207 | + "endTime": 1785945599999 | ||
| 208 | +} | ||
| 209 | +``` | ||
| 210 | + | ||
| 211 | +成功响应: | ||
| 212 | + | ||
| 213 | +```json | ||
| 214 | +{ | ||
| 215 | + "code": 0, | ||
| 216 | + "message": "success", | ||
| 217 | + "data": { | ||
| 218 | + "received": 3, | ||
| 219 | + "queued": 2, | ||
| 220 | + "recording": 1, | ||
| 221 | + "uploading": 0, | ||
| 222 | + "generated": 0, | ||
| 223 | + "invalid": 0, | ||
| 224 | + "failed": 0 | ||
| 225 | + } | ||
| 226 | +} | ||
| 227 | +``` | ||
| 228 | + | ||
| 229 | +这里的状态数量是本次提交结果的快照,不是全局队列统计。 | ||
| 230 | + | ||
| 231 | +## 8. 运行高光定时任务 | ||
| 232 | + | ||
| 233 | +```http | ||
| 234 | +POST /highlight/recording/scheduled | ||
| 235 | +``` | ||
| 236 | + | ||
| 237 | +无请求体。该接口与原整堂录制 `GET /recording` 完全独立。 | ||
| 238 | + | ||
| 239 | +当 `sourceMode=site` 时: | ||
| 240 | + | ||
| 241 | +1. 按 Asia/Shanghai 计算前一天 `00:00:00.000` 至 `23:59:59.999`。 | ||
| 242 | +2. 遍历 `HIGHLIGHTCONFIG.siteIds`。 | ||
| 243 | +3. 查询并提交所有有效高光。 | ||
| 244 | + | ||
| 245 | +响应: | ||
| 246 | + | ||
| 247 | +```json | ||
| 248 | +{ | ||
| 249 | + "code": 0, | ||
| 250 | + "message": "success", | ||
| 251 | + "data": { | ||
| 252 | + "mode": "site", | ||
| 253 | + "beginTime": 1785859200000, | ||
| 254 | + "endTime": 1785945599999, | ||
| 255 | + "received": 3, | ||
| 256 | + "queued": 3, | ||
| 257 | + "recording": 0, | ||
| 258 | + "uploading": 0, | ||
| 259 | + "generated": 0, | ||
| 260 | + "invalid": 0, | ||
| 261 | + "failed": 0 | ||
| 262 | + } | ||
| 263 | +} | ||
| 264 | +``` | ||
| 265 | + | ||
| 266 | +当 `sourceMode=task` 时,只处理同时满足以下条件的 SaaS 录制任务: | ||
| 267 | + | ||
| 268 | +```text | ||
| 269 | +status = 0 | ||
| 270 | +onlyHighlight = 1 | ||
| 271 | +siteId 位于 HIGHLIGHTCONFIG.siteIds | ||
| 272 | +``` | ||
| 273 | + | ||
| 274 | +响应额外包含: | ||
| 275 | + | ||
| 276 | +```json | ||
| 277 | +{ | ||
| 278 | + "mode": "task", | ||
| 279 | + "sourceTasks": 10, | ||
| 280 | + "pendingTasks": 2 | ||
| 281 | +} | ||
| 282 | +``` | ||
| 283 | + | ||
| 284 | +任务模式目前尚未实现向 SaaS 回写完成状态,不建议在正式环境启用。 | ||
| 285 | + | ||
| 286 | +建议的独立 cron: | ||
| 287 | + | ||
| 288 | +```cron | ||
| 289 | +57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1 | ||
| 290 | +``` | ||
| 291 | + | ||
| 292 | +原整堂录制 cron 保持原样,两者不要使用同一个接口。 | ||
| 293 | + | ||
| 294 | +## 9. 查询课堂全部高光文件 | ||
| 295 | + | ||
| 296 | +```http | ||
| 297 | +POST /highlight/fileExists | ||
| 298 | +``` | ||
| 299 | + | ||
| 300 | +请求: | ||
| 301 | + | ||
| 302 | +```json | ||
| 303 | +{ | ||
| 304 | + "siteId": "xdyui2", | ||
| 305 | + "classId": "2008975651" | ||
| 306 | +} | ||
| 307 | +``` | ||
| 308 | + | ||
| 309 | +全部生成时: | ||
| 310 | + | ||
| 311 | +```json | ||
| 312 | +{ | ||
| 313 | + "code": 0, | ||
| 314 | + "message": "文件已生成", | ||
| 315 | + "onlyHighlight": 1, | ||
| 316 | + "classUrlList": [ | ||
| 317 | + { | ||
| 318 | + "highlightId": 5, | ||
| 319 | + "classId": "2008975651", | ||
| 320 | + "siteId": "xdyui2", | ||
| 321 | + "beginTime": 1785895298000, | ||
| 322 | + "generated": true, | ||
| 323 | + "status": "generated", | ||
| 324 | + "url": "https://xdymp4.xuedianyun.com/oss/xdyui2/20260805/2008975651_highlight_5.mp4" | ||
| 325 | + } | ||
| 326 | + ] | ||
| 327 | +} | ||
| 328 | +``` | ||
| 329 | + | ||
| 330 | +只要存在未生成项,就返回: | ||
| 331 | + | ||
| 332 | +```json | ||
| 333 | +{ | ||
| 334 | + "code": 1, | ||
| 335 | + "message": "部分文件未生成", | ||
| 336 | + "onlyHighlight": 1, | ||
| 337 | + "classUrlList": [] | ||
| 338 | +} | ||
| 339 | +``` | ||
| 340 | + | ||
| 341 | +实际 `classUrlList` 仍包含所有已生成和未生成项目;上例省略了项目明细。课堂没有高光时返回 `code=1`、`message=文件未生成`。 | ||
| 342 | + | ||
| 343 | +## 10. 按明细批量查询高光文件 | ||
| 344 | + | ||
| 345 | +```http | ||
| 346 | +POST /highlight/files | ||
| 347 | +``` | ||
| 348 | + | ||
| 349 | +适合调用方已经持有高光 ID 和开始时间、不希望 WebScreen 再按课堂查询 SaaS 的场景。一次最多 1000 条。 | ||
| 350 | + | ||
| 351 | +请求: | ||
| 352 | + | ||
| 353 | +```json | ||
| 354 | +{ | ||
| 355 | + "items": [ | ||
| 356 | + { | ||
| 357 | + "highlightId": 5, | ||
| 358 | + "classId": "2008975651", | ||
| 359 | + "siteId": "xdyui2", | ||
| 360 | + "beginTime": 1785895298000 | ||
| 361 | + } | ||
| 362 | + ] | ||
| 363 | +} | ||
| 364 | +``` | ||
| 365 | + | ||
| 366 | +响应: | ||
| 367 | + | ||
| 368 | +```json | ||
| 369 | +{ | ||
| 370 | + "code": 0, | ||
| 371 | + "message": "success", | ||
| 372 | + "data": [ | ||
| 373 | + { | ||
| 374 | + "highlightId": 5, | ||
| 375 | + "classId": "2008975651", | ||
| 376 | + "siteId": "xdyui2", | ||
| 377 | + "beginTime": 1785895298000, | ||
| 378 | + "generated": false, | ||
| 379 | + "status": "recording", | ||
| 380 | + "url": null | ||
| 381 | + } | ||
| 382 | + ] | ||
| 383 | +} | ||
| 384 | +``` | ||
| 385 | + | ||
| 386 | +## 11. 查询高光队列状态 | ||
| 387 | + | ||
| 388 | +```http | ||
| 389 | +GET /highlight/status | ||
| 390 | +``` | ||
| 391 | + | ||
| 392 | +响应: | ||
| 393 | + | ||
| 394 | +```json | ||
| 395 | +{ | ||
| 396 | + "code": 0, | ||
| 397 | + "message": "success", | ||
| 398 | + "data": { | ||
| 399 | + "queued": 2, | ||
| 400 | + "recording": 1, | ||
| 401 | + "knownTasks": 5 | ||
| 402 | + } | ||
| 403 | +} | ||
| 404 | +``` | ||
| 405 | + | ||
| 406 | +| 字段 | 说明 | | ||
| 407 | +|---|---| | ||
| 408 | +| `queued` | 当前等待录制的任务数 | | ||
| 409 | +| `recording` | 当前正在录制的任务数 | | ||
| 410 | +| `knownTasks` | 当前进程内保留的全部已知任务数 | | ||
| 411 | + | ||
| 412 | +队列状态只保存在当前 Node.js 进程内,PM2 重启后会清空;本地文件和 OSS 文件不会丢失。 | ||
| 413 | + | ||
| 414 | +## 12. 配置 | ||
| 415 | + | ||
| 416 | +配置文件:`config/config.json`。 | ||
| 417 | + | ||
| 418 | +```json | ||
| 419 | +"HIGHLIGHTCONFIG": { | ||
| 420 | + "enabled": true, | ||
| 421 | + "sourceMode": "site", | ||
| 422 | + "siteIds": ["xdyui2"], | ||
| 423 | + "apiBaseUrl": "https://saas.xuedianyun.com", | ||
| 424 | + "pageSize": 100, | ||
| 425 | + "taskPageSize": 100, | ||
| 426 | + "maxPages": 1000, | ||
| 427 | + "maxConcurrent": 2, | ||
| 428 | + "maxDurationMs": 21600000, | ||
| 429 | + "apiTimeoutMs": 10000, | ||
| 430 | + "apiRetryCount": 2, | ||
| 431 | + "apiRetryBaseDelayMs": 500, | ||
| 432 | + "loadGraceMs": 60000, | ||
| 433 | + "endGraceMs": 10000, | ||
| 434 | + "taskRetentionMs": 86400000, | ||
| 435 | + "outputNamespace": "", | ||
| 436 | + "outputBaseUrl": "https://xdymp4.xuedianyun.com" | ||
| 437 | +} | ||
| 438 | +``` | ||
| 439 | + | ||
| 440 | +| 字段 | 说明 | | ||
| 441 | +|---|---| | ||
| 442 | +| `enabled` | 高光业务开关;不影响整堂录制,关闭后状态接口仍可访问 | | ||
| 443 | +| `sourceMode` | 定时入口的数据源:`site` 或 `task` | | ||
| 444 | +| `siteIds` | 允许处理的机构编码列表 | | ||
| 445 | +| `pageSize` | SaaS 高光站点查询分页大小 | | ||
| 446 | +| `taskPageSize` | SaaS 录制任务查询分页大小 | | ||
| 447 | +| `maxPages` | 自动分页安全上限 | | ||
| 448 | +| `maxConcurrent` | 高光录制最大并发数 | | ||
| 449 | +| `maxDurationMs` | 单条高光允许的最大时长 | | ||
| 450 | +| `apiTimeoutMs` | SaaS 接口超时时间 | | ||
| 451 | +| `apiRetryCount` | SaaS 接口失败重试次数 | | ||
| 452 | +| `loadGraceMs` | 回放页面加载预留时间 | | ||
| 453 | +| `endGraceMs` | 录制结束预留时间 | | ||
| 454 | +| `taskRetentionMs` | 完成任务在内存中的保留时间 | | ||
| 455 | +| `outputNamespace` | 可选输出隔离目录;正式环境通常留空 | | ||
| 456 | +| `outputBaseUrl` | OSS 对外访问地址 | | ||
| 457 | + | ||
| 458 | +OSS 文件查询还需要环境变量: | ||
| 459 | + | ||
| 460 | +```text | ||
| 461 | +ALIBABA_CLOUD_ACCESS_KEY_ID | ||
| 462 | +ALIBABA_CLOUD_ACCESS_KEY_SECRET | ||
| 463 | +``` | ||
| 464 | + | ||
| 465 | +## 13. 文件规则 | ||
| 466 | + | ||
| 467 | +```text | ||
| 468 | +文件名:{classId}_highlight_{highlightId}.mp4 | ||
| 469 | +本地: {PROJECTCATALOG}/media/{siteId}/{yyyyMMdd}/{fileName} | ||
| 470 | +OSS: oss/{siteId}/{yyyyMMdd}/{fileName} | ||
| 471 | +URL: {outputBaseUrl}/oss/{siteId}/{yyyyMMdd}/{fileName} | ||
| 472 | +``` | ||
| 473 | + | ||
| 474 | +`yyyyMMdd` 按 `beginTime` 对应的 Asia/Shanghai 日期计算。 | ||
| 475 | + | ||
| 476 | +录制先写入 `{PROJECTCATALOG}/.highlight_tmp`,完成后再移动到正式 `media` 目录。WebScreen 不主动上传 OSS,由服务器已有搬运程序处理,并使用 `download.json` 作为目录完成标记。 | ||
| 477 | + | ||
| 478 | +## 14. WebScreen 依赖的 SaaS 内部接口 | ||
| 479 | + | ||
| 480 | +以下接口由 WebScreen 内部调用,不应由普通前端直接调用。 | ||
| 481 | + | ||
| 482 | +| 方法 | 路径 | 用途 | | ||
| 483 | +|---|---|---| | ||
| 484 | +| POST | `/3m/api/highlight/getByClassPrivate.do` | 根据课堂号获取全部高光 | | ||
| 485 | +| POST | `/3m/api/highlight/getBySitePrivate.do` | 根据站点和时间范围分页获取高光 | | ||
| 486 | +| POST | `/3m/api/recording/getRecordingTasksPrivate.do` | `sourceMode=task` 时获取录制任务 | | ||
| 487 | + | ||
| 488 | +SaaS 高光记录示例: | ||
| 489 | + | ||
| 490 | +```json | ||
| 491 | +{ | ||
| 492 | + "id": 5, | ||
| 493 | + "meetingNumber": "2008975651", | ||
| 494 | + "siteId": "xdyui2", | ||
| 495 | + "beginTime": 1785895298000, | ||
| 496 | + "endTime": 1785895343000, | ||
| 497 | + "type": 0, | ||
| 498 | + "more": "" | ||
| 499 | +} | ||
| 500 | +``` | ||
| 501 | + | ||
| 502 | +字段映射: | ||
| 503 | + | ||
| 504 | +```text | ||
| 505 | +id → highlightId | ||
| 506 | +meetingNumber → classId | ||
| 507 | +``` | ||
| 508 | + | ||
| 509 | +更详细的 SaaS 请求签名和响应说明见 `getByClassPrivate.md` 与 `getBySitePrivate.md`。 | ||
| 510 | + | ||
| 511 | +## 15. 调用顺序建议 | ||
| 512 | + | ||
| 513 | +单课堂人工验证: | ||
| 514 | + | ||
| 515 | +```text | ||
| 516 | +POST /highlight/preview/by-class | ||
| 517 | + → POST /highlight/recording/by-class | ||
| 518 | + → GET /highlight/status | ||
| 519 | + → POST /highlight/fileExists | ||
| 520 | +``` | ||
| 521 | + | ||
| 522 | +每日自动录制: | ||
| 523 | + | ||
| 524 | +```text | ||
| 525 | +cron | ||
| 526 | + → POST /highlight/recording/scheduled | ||
| 527 | + → 查询前一天高光 | ||
| 528 | + → 加入高光队列 | ||
| 529 | + → 生成本地 MP4 和 download.json | ||
| 530 | + → 服务器搬运到 OSS | ||
| 531 | + → POST /highlight/fileExists 查询结果 | ||
| 532 | +``` |
| @@ -123,10 +123,10 @@ curl -X POST http://127.0.0.1:3001/highlight/fileExists \ | @@ -123,10 +123,10 @@ curl -X POST http://127.0.0.1:3001/highlight/fileExists \ | ||
| 123 | 123 | ||
| 124 | ## 8. 验证 cron 全量模式 | 124 | ## 8. 验证 cron 全量模式 |
| 125 | 125 | ||
| 126 | -先手工执行原 cron 入口: | 126 | +先手工执行独立的高光 cron 入口: |
| 127 | 127 | ||
| 128 | ```bash | 128 | ```bash |
| 129 | -wget -qO- http://127.0.0.1:3001/recording | 129 | +curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled |
| 130 | ``` | 130 | ``` |
| 131 | 131 | ||
| 132 | 响应 `data.mode` 应为 `site`,时间窗应为 Asia/Shanghai 前一天。 | 132 | 响应 `data.mode` 应为 `site`,时间窗应为 Asia/Shanghai 前一天。 |
| @@ -134,7 +134,7 @@ wget -qO- http://127.0.0.1:3001/recording | @@ -134,7 +134,7 @@ wget -qO- http://127.0.0.1:3001/recording | ||
| 134 | 确认无误后配置: | 134 | 确认无误后配置: |
| 135 | 135 | ||
| 136 | ```cron | 136 | ```cron |
| 137 | -57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1 | 137 | +57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1 |
| 138 | ``` | 138 | ``` |
| 139 | 139 | ||
| 140 | ## 9. 切换任务模式 | 140 | ## 9. 切换任务模式 |
| @@ -188,7 +188,7 @@ https://xdymp4.xuedianyun.com/oss/xdyui2/{yyyyMMdd}/{classId}_highlight_{highlig | @@ -188,7 +188,7 @@ https://xdymp4.xuedianyun.com/oss/xdyui2/{yyyyMMdd}/{classId}_highlight_{highlig | ||
| 188 | pm2 restart webScreen | 188 | pm2 restart webScreen |
| 189 | ``` | 189 | ``` |
| 190 | 190 | ||
| 191 | -关闭后,`GET /recording` 继续运行原整课录制逻辑。高光使用独立文件名,不覆盖旧整课 MP4。 | 191 | +关闭后,高光查询、录制和文件检查接口停止处理,状态接口仍可访问;`GET /recording` 始终运行原整堂录制逻辑。高光使用独立文件名,不覆盖旧整堂 MP4。 |
| 192 | 192 | ||
| 193 | ## 12. 上线 CrazyTalk 前检查 | 193 | ## 12. 上线 CrazyTalk 前检查 |
| 194 | 194 |
| @@ -334,7 +334,7 @@ N 条高光时,`classUrlList` 必须有 N 条记录。 | @@ -334,7 +334,7 @@ N 条高光时,`classUrlList` 必须有 N 条记录。 | ||
| 334 | 手工调用 cron 入口: | 334 | 手工调用 cron 入口: |
| 335 | 335 | ||
| 336 | ```bash | 336 | ```bash |
| 337 | -wget -qO- http://127.0.0.1:3001/recording | 337 | +curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled |
| 338 | ``` | 338 | ``` |
| 339 | 339 | ||
| 340 | 检查: | 340 | 检查: |
| @@ -365,7 +365,7 @@ pm2 restart webScreen | @@ -365,7 +365,7 @@ pm2 restart webScreen | ||
| 365 | 4. 其他站点 `status=0, onlyHighlight=1`:不录制。 | 365 | 4. 其他站点 `status=0, onlyHighlight=1`:不录制。 |
| 366 | 366 | ||
| 367 | ```bash | 367 | ```bash |
| 368 | -wget -qO- http://127.0.0.1:3001/recording | 368 | +curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled |
| 369 | ``` | 369 | ``` |
| 370 | 370 | ||
| 371 | 检查: | 371 | 检查: |
| @@ -381,7 +381,7 @@ wget -qO- http://127.0.0.1:3001/recording | @@ -381,7 +381,7 @@ wget -qO- http://127.0.0.1:3001/recording | ||
| 381 | 只有单课堂、重复录制、OSS 搬运和全量模式全部通过后才启用: | 381 | 只有单课堂、重复录制、OSS 搬运和全量模式全部通过后才启用: |
| 382 | 382 | ||
| 383 | ```cron | 383 | ```cron |
| 384 | -57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1 | 384 | +57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1 |
| 385 | ``` | 385 | ``` |
| 386 | 386 | ||
| 387 | ```bash | 387 | ```bash |
| @@ -465,7 +465,7 @@ echo "$DISPLAY" | @@ -465,7 +465,7 @@ echo "$DISPLAY" | ||
| 465 | pm2 restart webScreen | 465 | pm2 restart webScreen |
| 466 | ``` | 466 | ``` |
| 467 | 467 | ||
| 468 | -关闭后,`GET /recording` 恢复原整课逻辑。`POST /recording`、`POST /recordingTask`、`POST /fileExists` 和实时录制接口不变。 | 468 | +关闭后,高光查询、录制和文件检查接口停止处理,状态接口仍可访问;`GET /recording` 始终执行原整堂录制逻辑。`POST /recording`、`POST /recordingTask`、`POST /fileExists` 和实时录制接口不变。 |
| 469 | 469 | ||
| 470 | ## 21. 测试记录模板 | 470 | ## 21. 测试记录模板 |
| 471 | 471 |
| @@ -10,12 +10,12 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | @@ -10,12 +10,12 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | ||
| 10 | - 一个高光时间段生成一个 MP4;N 条高光生成 N 个文件。 | 10 | - 一个高光时间段生成一个 MP4;N 条高光生成 N 个文件。 |
| 11 | - 当前只在 `xdyui2` 测试,验证后再增加 CrazyTalk。 | 11 | - 当前只在 `xdyui2` 测试,验证后再增加 CrazyTalk。 |
| 12 | - WebScreen 只生成本地文件;服务器已有任务定时移动到 OSS。 | 12 | - WebScreen 只生成本地文件;服务器已有任务定时移动到 OSS。 |
| 13 | -- 原定时任务每天 `07:57` 调用 `GET /recording`。 | 13 | +- 高光定时任务使用独立的 `POST /highlight/recording/scheduled`,不占用整堂录制入口。 |
| 14 | - 任务模式暂时只处理 `status=0`;后端状态回写规则待确认。 | 14 | - 任务模式暂时只处理 `status=0`;后端状态回写规则待确认。 |
| 15 | 15 | ||
| 16 | ## 2. 兼容原则 | 16 | ## 2. 兼容原则 |
| 17 | 17 | ||
| 18 | -高光开关缺失或关闭时,`GET /recording` 完整执行原整课录制逻辑。 | 18 | +原 `GET /recording` 始终执行整堂录制逻辑,不读取高光开关。 |
| 19 | 19 | ||
| 20 | ```json | 20 | ```json |
| 21 | { | 21 | { |
| @@ -25,10 +25,11 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | @@ -25,10 +25,11 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | ||
| 25 | } | 25 | } |
| 26 | ``` | 26 | ``` |
| 27 | 27 | ||
| 28 | -只有显式配置 `enabled: true` 才进入高光录制。因此旧服务器、旧站点和旧客户不受影响。 | 28 | +只有显式配置 `enabled: true` 才能调用 `/highlight/*` 高光业务接口。关闭高光不会关闭或改变整堂录制。 |
| 29 | 29 | ||
| 30 | 以下接口保持原行为: | 30 | 以下接口保持原行为: |
| 31 | 31 | ||
| 32 | +- `GET /recording` | ||
| 32 | - `POST /recording` | 33 | - `POST /recording` |
| 33 | - `POST /recordingTask` | 34 | - `POST /recordingTask` |
| 34 | - `POST /fileExists` | 35 | - `POST /fileExists` |
| @@ -38,19 +39,13 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | @@ -38,19 +39,13 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录 | ||
| 38 | 39 | ||
| 39 | ## 3. 定时触发 | 40 | ## 3. 定时触发 |
| 40 | 41 | ||
| 41 | -WebScreen 本身不创建 cron。服务器已有 cron: | 42 | +WebScreen 本身不创建 cron。高光需要独立 cron: |
| 42 | 43 | ||
| 43 | ```cron | 44 | ```cron |
| 44 | -57 7 * * * wget http://127.0.0.1:3001/recording & | 45 | +57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1 |
| 45 | ``` | 46 | ``` |
| 46 | 47 | ||
| 47 | -建议部署时改成不落 wget 临时文件的等价写法: | ||
| 48 | - | ||
| 49 | -```cron | ||
| 50 | -57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1 | ||
| 51 | -``` | ||
| 52 | - | ||
| 53 | -`GET /recording` 读取 `HIGHLIGHTCONFIG.sourceMode`: | 48 | +`POST /highlight/recording/scheduled` 读取 `HIGHLIGHTCONFIG.sourceMode`: |
| 54 | 49 | ||
| 55 | - `site`:全量高光模式。 | 50 | - `site`:全量高光模式。 |
| 56 | - `task`:指定课堂任务模式。 | 51 | - `task`:指定课堂任务模式。 |
| @@ -97,7 +92,7 @@ WebScreen 本身不创建 cron。服务器已有 cron: | @@ -97,7 +92,7 @@ WebScreen 本身不创建 cron。服务器已有 cron: | ||
| 97 | 92 | ||
| 98 | 流程: | 93 | 流程: |
| 99 | 94 | ||
| 100 | -1. cron 调用 `GET /recording`。 | 95 | +1. cron 调用 `POST /highlight/recording/scheduled`。 |
| 101 | 2. 按 Asia/Shanghai 计算前一天 `00:00:00.000` 至 `23:59:59.999`。 | 96 | 2. 按 Asia/Shanghai 计算前一天 `00:00:00.000` 至 `23:59:59.999`。 |
| 102 | 3. 遍历 `HIGHLIGHTCONFIG.siteIds`。 | 97 | 3. 遍历 `HIGHLIGHTCONFIG.siteIds`。 |
| 103 | 4. 分页调用 `getBySitePrivate.do`。 | 98 | 4. 分页调用 `getBySitePrivate.do`。 |
| @@ -288,7 +283,7 @@ WebScreen 调用 `getByClassPrivate.do` 获取该课堂全部高光,再逐条 | @@ -288,7 +283,7 @@ WebScreen 调用 `getByClassPrivate.do` 获取该课堂全部高光,再逐条 | ||
| 288 | 5. `sourceMode=task` 只处理 `status=0 && onlyHighlight=1`。 | 283 | 5. `sourceMode=task` 只处理 `status=0 && onlyHighlight=1`。 |
| 289 | 6. `/highlight/fileExists` 返回课堂全部高光文件状态和地址。 | 284 | 6. `/highlight/fileExists` 返回课堂全部高光文件状态和地址。 |
| 290 | 7. 原 `/fileExists` 响应不变。 | 285 | 7. 原 `/fileExists` 响应不变。 |
| 291 | -8. 删除或关闭 `HIGHLIGHTCONFIG` 后,`GET /recording` 恢复原整课录制。 | 286 | +8. 删除或关闭 `HIGHLIGHTCONFIG` 后,高光接口拒绝处理,原 `GET /recording` 仍执行整堂录制。 |
| 292 | 287 | ||
| 293 | ## 13. 已知待办 | 288 | ## 13. 已知待办 |
| 294 | 289 |
| @@ -72,6 +72,20 @@ router.post('/recording/by-site', async function (req, res) { | @@ -72,6 +72,20 @@ router.post('/recording/by-site', async function (req, res) { | ||
| 72 | } | 72 | } |
| 73 | }); | 73 | }); |
| 74 | 74 | ||
| 75 | +// 独立的高光定时入口,不占用原整堂录制 GET /recording。 | ||
| 76 | +router.post('/recording/scheduled', async function (req, res) { | ||
| 77 | + try { | ||
| 78 | + const data = await service.runScheduledRecording(); | ||
| 79 | + return res.send({ | ||
| 80 | + code: 0, | ||
| 81 | + message: data.received ? 'success' : '无高光数据', | ||
| 82 | + data | ||
| 83 | + }); | ||
| 84 | + } catch (error) { | ||
| 85 | + return sendError(res, error); | ||
| 86 | + } | ||
| 87 | +}); | ||
| 88 | + | ||
| 75 | router.post('/fileExists', async function (req, res) { | 89 | router.post('/fileExists', async function (req, res) { |
| 76 | try { | 90 | try { |
| 77 | const data = await service.getClassFileStatuses(req.body || {}); | 91 | const data = await service.getClassFileStatuses(req.body || {}); |
| @@ -8,11 +8,6 @@ require('dotenv').config(); // 加载环境变量 | @@ -8,11 +8,6 @@ require('dotenv').config(); // 加载环境变量 | ||
| 8 | 8 | ||
| 9 | const method = require("../config/method") | 9 | const method = require("../config/method") |
| 10 | const config = require("../config/config") | 10 | const config = require("../config/config") |
| 11 | -const { | ||
| 12 | - highlightRecordingService, | ||
| 13 | - HighlightValidationError, | ||
| 14 | - HighlightUpstreamError | ||
| 15 | -} = require('../services/highlightRecordingService'); | ||
| 16 | const version ='v1.2.0.20251208'; | 11 | const version ='v1.2.0.20251208'; |
| 17 | // const { GETCLASSURL, GETCLASSURLPARAMETER, PROJECTCATALOG, PROJECTWINCATALOG, BACKMEDIACONFIG } = config | 12 | // const { GETCLASSURL, GETCLASSURLPARAMETER, PROJECTCATALOG, PROJECTWINCATALOG, BACKMEDIACONFIG } = config |
| 18 | const { YesterdayTime,getDayTime, getRequestClassIds, dayTimeYMD } = method | 13 | const { YesterdayTime,getDayTime, getRequestClassIds, dayTimeYMD } = method |
| @@ -219,23 +214,6 @@ router.get('/recording', async function (req, res, next) { | @@ -219,23 +214,6 @@ router.get('/recording', async function (req, res, next) { | ||
| 219 | if (!fileConfig) return false | 214 | if (!fileConfig) return false |
| 220 | 215 | ||
| 221 | const parsedConfig = JSON.parse(fileConfig) | 216 | const parsedConfig = JSON.parse(fileConfig) |
| 222 | - if (parsedConfig.HIGHLIGHTCONFIG && parsedConfig.HIGHLIGHTCONFIG.enabled === true) { | ||
| 223 | - try { | ||
| 224 | - const data = await highlightRecordingService.runScheduledRecording() | ||
| 225 | - res.send({ code: "0", message: data.received ? "success" : "无高光数据", data }) | ||
| 226 | - } catch (error) { | ||
| 227 | - new MediaCreat().wrieLog("高光录制任务读取失败:------>" + (error.message || error)) | ||
| 228 | - if (error instanceof HighlightValidationError) { | ||
| 229 | - res.status(400).send({ code: "1", message: error.message }) | ||
| 230 | - } else if (error instanceof HighlightUpstreamError) { | ||
| 231 | - res.status(502).send({ code: String(error.upstreamCode), message: error.message }) | ||
| 232 | - } else { | ||
| 233 | - res.status(500).send({ code: "-1", message: "高光录制任务读取失败" }) | ||
| 234 | - } | ||
| 235 | - } | ||
| 236 | - return | ||
| 237 | - } | ||
| 238 | - | ||
| 239 | const { GETCLASSURLPARAMETER } = parsedConfig | 217 | const { GETCLASSURLPARAMETER } = parsedConfig |
| 240 | siteIds = GETCLASSURLPARAMETER.siteId | 218 | siteIds = GETCLASSURLPARAMETER.siteId |
| 241 | if (siteIds.length == 0) { | 219 | if (siteIds.length == 0) { |
| @@ -821,6 +821,7 @@ class HighlightRecordingService { | @@ -821,6 +821,7 @@ class HighlightRecordingService { | ||
| 821 | } | 821 | } |
| 822 | 822 | ||
| 823 | async getFileStatuses(items) { | 823 | async getFileStatuses(items) { |
| 824 | + this.ensureFeatureEnabled(); | ||
| 824 | this.cleanupTasks(); | 825 | this.cleanupTasks(); |
| 825 | const config = this.readConfig(); | 826 | const config = this.readConfig(); |
| 826 | const allowedSiteIds = new Set(this.getAllowedSiteIds()); | 827 | const allowedSiteIds = new Set(this.getAllowedSiteIds()); |
| @@ -42,6 +42,13 @@ async function run() { | @@ -42,6 +42,13 @@ async function run() { | ||
| 42 | assert.strictEqual(invalidFiles.status, 400); | 42 | assert.strictEqual(invalidFiles.status, 400); |
| 43 | assert.strictEqual(invalidFiles.body.code, 10); | 43 | assert.strictEqual(invalidFiles.body.code, 10); |
| 44 | 44 | ||
| 45 | + const disabledFiles = await request(server, 'POST', '/highlight/files', { | ||
| 46 | + items: [{ highlightId: 5, classId: '1001', siteId: 'xdyui2', beginTime: 1785895298000 }] | ||
| 47 | + }); | ||
| 48 | + assert.strictEqual(disabledFiles.status, 400); | ||
| 49 | + assert.strictEqual(disabledFiles.body.code, 10); | ||
| 50 | + assert.strictEqual(disabledFiles.body.message, '高光录制功能未启用'); | ||
| 51 | + | ||
| 45 | const invalidPreview = await request(server, 'POST', '/highlight/preview/by-class', {}); | 52 | const invalidPreview = await request(server, 'POST', '/highlight/preview/by-class', {}); |
| 46 | assert.strictEqual(invalidPreview.status, 400); | 53 | assert.strictEqual(invalidPreview.status, 400); |
| 47 | assert.strictEqual(invalidPreview.body.code, 10); | 54 | assert.strictEqual(invalidPreview.body.code, 10); |
| @@ -50,6 +57,51 @@ async function run() { | @@ -50,6 +57,51 @@ async function run() { | ||
| 50 | assert.strictEqual(invalidClassFiles.status, 400); | 57 | assert.strictEqual(invalidClassFiles.status, 400); |
| 51 | assert.strictEqual(invalidClassFiles.body.code, 10); | 58 | assert.strictEqual(invalidClassFiles.body.code, 10); |
| 52 | 59 | ||
| 60 | + const disabledScheduled = await request(server, 'POST', '/highlight/recording/scheduled'); | ||
| 61 | + assert.strictEqual(disabledScheduled.status, 400); | ||
| 62 | + assert.strictEqual(disabledScheduled.body.code, 10); | ||
| 63 | + assert.strictEqual(disabledScheduled.body.message, '高光录制功能未启用'); | ||
| 64 | + | ||
| 65 | + const originalRunScheduledRecording = highlightRecordingService.runScheduledRecording; | ||
| 66 | + try { | ||
| 67 | + highlightRecordingService.runScheduledRecording = async () => ({ | ||
| 68 | + mode: 'site', | ||
| 69 | + beginTime: 1785945600000, | ||
| 70 | + endTime: 1786031999999, | ||
| 71 | + received: 2, | ||
| 72 | + queued: 2, | ||
| 73 | + recording: 0, | ||
| 74 | + uploading: 0, | ||
| 75 | + generated: 0, | ||
| 76 | + invalid: 0, | ||
| 77 | + failed: 0 | ||
| 78 | + }); | ||
| 79 | + const scheduled = await request(server, 'POST', '/highlight/recording/scheduled'); | ||
| 80 | + assert.strictEqual(scheduled.status, 200); | ||
| 81 | + assert.strictEqual(scheduled.body.code, 0); | ||
| 82 | + assert.strictEqual(scheduled.body.message, 'success'); | ||
| 83 | + assert.strictEqual(scheduled.body.data.mode, 'site'); | ||
| 84 | + assert.strictEqual(scheduled.body.data.received, 2); | ||
| 85 | + | ||
| 86 | + highlightRecordingService.runScheduledRecording = async () => ({ | ||
| 87 | + mode: 'site', | ||
| 88 | + beginTime: 1785945600000, | ||
| 89 | + endTime: 1786031999999, | ||
| 90 | + received: 0, | ||
| 91 | + queued: 0, | ||
| 92 | + recording: 0, | ||
| 93 | + uploading: 0, | ||
| 94 | + generated: 0, | ||
| 95 | + invalid: 0, | ||
| 96 | + failed: 0 | ||
| 97 | + }); | ||
| 98 | + const scheduledEmpty = await request(server, 'POST', '/highlight/recording/scheduled'); | ||
| 99 | + assert.strictEqual(scheduledEmpty.body.code, 0); | ||
| 100 | + assert.strictEqual(scheduledEmpty.body.message, '无高光数据'); | ||
| 101 | + } finally { | ||
| 102 | + highlightRecordingService.runScheduledRecording = originalRunScheduledRecording; | ||
| 103 | + } | ||
| 104 | + | ||
| 53 | const originalGetClassFileStatuses = highlightRecordingService.getClassFileStatuses; | 105 | const originalGetClassFileStatuses = highlightRecordingService.getClassFileStatuses; |
| 54 | highlightRecordingService.getClassFileStatuses = async () => [ | 106 | highlightRecordingService.getClassFileStatuses = async () => [ |
| 55 | { highlightId: 4, classId: '1001', siteId: 'xdyui2', generated: true, status: 'generated', url: 'https://example/4.mp4' }, | 107 | { highlightId: 4, classId: '1001', siteId: 'xdyui2', generated: true, status: 'generated', url: 'https://example/4.mp4' }, |
-
请 注册 或 登录 后发表评论