huz1xuan

feat: 拆分高光录制接口与整堂录制

  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
1 node调用录制文件 1 node调用录制文件
2 -2025-0306 新的dev分支  
  2 +2025-0306 新的dev分支
  3 +
  4 +高光录制接口见 [HIGHLIGHT_API.md](./HIGHLIGHT_API.md)。
@@ -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' },