huz1xuan

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

# WebScreen 高光录制接口文档
## 1. 文档范围
本文汇总 WebScreen 对外提供的全部高光接口,以及 WebScreen 依赖的 SaaS 内部接口。
高光录制与整堂录制是两套独立业务:
- 原整堂录制接口保持不变。
- 所有高光接口统一使用 `/highlight` 前缀。
- `HIGHLIGHTCONFIG.enabled` 只控制高光查询、录制和文件检查,不影响原整堂录制;`GET /highlight/status` 始终可用于健康检查。
- 高光文件使用 `{classId}_highlight_{highlightId}.mp4`,不会覆盖整堂录像 `{classId}.mp4`。
示例服务地址:
```text
http://127.0.0.1:3001
```
除 `GET /highlight/status` 外,请求均使用:
```http
Content-Type: application/json
```
当前 WebScreen 接口没有应用层鉴权,只应开放给可信内网或经过访问控制的调用方。
## 2. 接口总览
| 方法 | 路径 | 用途 | 是否启动录制 |
|---|---|---|---|
| POST | `/highlight/preview/by-class` | 预览指定课堂的全部高光及目标文件信息 | 否 |
| POST | `/highlight/preview/by-site` | 预览指定站点、时间范围内的全部高光 | 否 |
| POST | `/highlight/recording/by-class` | 提交指定课堂的全部高光录制 | 是 |
| POST | `/highlight/recording/by-site` | 提交指定站点、时间范围内的全部高光录制 | 是 |
| POST | `/highlight/recording/scheduled` | 运行一次独立的高光定时任务 | 是 |
| POST | `/highlight/fileExists` | 查询指定课堂的全部高光文件状态 | 否 |
| POST | `/highlight/files` | 根据高光明细批量查询文件状态 | 否 |
| GET | `/highlight/status` | 查询当前进程内的高光队列状态 | 否 |
## 3. 公共约定
### 3.1 高光字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `highlightId` | Integer | 高光唯一 ID,对应 SaaS 高光记录的 `id` |
| `classId` | String | 课堂号,对应 SaaS 高光记录的 `meetingNumber` |
| `siteId` | String | 机构编码,必须位于 `HIGHLIGHTCONFIG.siteIds` |
| `beginTime` | Integer | 高光开始时间,13 位毫秒时间戳 |
| `endTime` | Integer | 高光结束时间,13 位毫秒时间戳 |
`classId`、`siteId` 只允许字母、数字、下划线和短横线,最长 128 个字符。
### 3.2 任务状态
| 状态 | 说明 |
|---|---|
| `not_generated` | 本地、OSS 和当前任务队列均未发现文件 |
| `queued` | 已进入等待队列 |
| `recording` | `web_capture_c` 正在录制 |
| `uploading` | 本地文件已生成,等待服务器搬运到 OSS |
| `generated` | OSS 文件已经存在,可以返回播放地址 |
| `invalid` | 高光字段、站点或时间段校验失败 |
| `failed` | 录制任务执行失败 |
### 3.3 HTTP 状态和错误响应
| HTTP 状态 | `code` | 说明 |
|---|---:|---|
| 200 | 0 或 1 | 请求正常完成;文件查询接口使用 1 表示尚未全部生成 |
| 400 | 10 | 请求参数错误、站点未启用或高光功能未启用 |
| 502 | SaaS 返回码或 -1 | SaaS 高光接口、录制任务接口或 OSS 查询失败 |
| 500 | -1 | WebScreen 内部异常 |
错误示例:
```json
{
"code": 10,
"message": "高光录制功能未启用"
}
```
## 4. 预览课堂高光
```http
POST /highlight/preview/by-class
```
该接口查询 SaaS 高光数据并计算录制地址、文件名和当前文件状态,但不会加入录制队列。
请求:
```json
{
"classId": "2008975651"
}
```
成功响应:
```json
{
"code": 0,
"message": "success",
"data": [
{
"highlightId": 5,
"classId": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000,
"endTime": 1785895343000,
"duration": 45000,
"status": "not_generated",
"fileName": "2008975651_highlight_5.mp4",
"localPath": "/root/web_capture_release/media/xdyui2/20260805/2008975651_highlight_5.mp4",
"ossKey": "oss/xdyui2/20260805/2008975651_highlight_5.mp4",
"url": null,
"playbackUrl": "https://pclive.xuedianyun.com/...&recBeginTime=1785895298000&recEndTime=1785895343000"
}
]
}
```
没有高光时返回 `code=0`、`message=无高光数据`、`data=[]`。
## 5. 预览站点高光
```http
POST /highlight/preview/by-site
```
请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `siteId` | String | 是 | 机构编码 |
| `beginTime` | Integer | 否 | 查询开始时间,13 位毫秒时间戳 |
| `endTime` | Integer | 否 | 查询结束时间,13 位毫秒时间戳 |
| `pageSize` | Integer | 否 | 调用 SaaS 时的分页大小,范围 1~1000 |
请求:
```json
{
"siteId": "xdyui2",
"beginTime": 1785859200000,
"endTime": 1785945599999,
"pageSize": 100
}
```
WebScreen 会自动读取全部分页。响应项与 `/highlight/preview/by-class` 相同。
## 6. 按课堂提交高光录制
```http
POST /highlight/recording/by-class
```
请求:
```json
{
"classId": "2008975651"
}
```
成功响应:
```json
{
"code": 0,
"message": "success",
"data": [
{
"highlightId": 5,
"classId": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000,
"endTime": 1785895343000,
"status": "queued"
}
]
}
```
接口返回表示任务已经接收,不表示 MP4 已经生成。调用方应继续查询 `/highlight/fileExists` 或 `/highlight/status`。
重复提交同一 `siteId + highlightId` 时,WebScreen 会检查内存任务、本地文件和 OSS,不会重复录制已经存在的高光。
## 7. 按站点提交高光录制
```http
POST /highlight/recording/by-site
```
请求字段与 `/highlight/preview/by-site` 相同。
请求:
```json
{
"siteId": "xdyui2",
"beginTime": 1785859200000,
"endTime": 1785945599999
}
```
成功响应:
```json
{
"code": 0,
"message": "success",
"data": {
"received": 3,
"queued": 2,
"recording": 1,
"uploading": 0,
"generated": 0,
"invalid": 0,
"failed": 0
}
}
```
这里的状态数量是本次提交结果的快照,不是全局队列统计。
## 8. 运行高光定时任务
```http
POST /highlight/recording/scheduled
```
无请求体。该接口与原整堂录制 `GET /recording` 完全独立。
当 `sourceMode=site` 时:
1. 按 Asia/Shanghai 计算前一天 `00:00:00.000` 至 `23:59:59.999`。
2. 遍历 `HIGHLIGHTCONFIG.siteIds`。
3. 查询并提交所有有效高光。
响应:
```json
{
"code": 0,
"message": "success",
"data": {
"mode": "site",
"beginTime": 1785859200000,
"endTime": 1785945599999,
"received": 3,
"queued": 3,
"recording": 0,
"uploading": 0,
"generated": 0,
"invalid": 0,
"failed": 0
}
}
```
当 `sourceMode=task` 时,只处理同时满足以下条件的 SaaS 录制任务:
```text
status = 0
onlyHighlight = 1
siteId 位于 HIGHLIGHTCONFIG.siteIds
```
响应额外包含:
```json
{
"mode": "task",
"sourceTasks": 10,
"pendingTasks": 2
}
```
任务模式目前尚未实现向 SaaS 回写完成状态,不建议在正式环境启用。
建议的独立 cron:
```cron
57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1
```
原整堂录制 cron 保持原样,两者不要使用同一个接口。
## 9. 查询课堂全部高光文件
```http
POST /highlight/fileExists
```
请求:
```json
{
"siteId": "xdyui2",
"classId": "2008975651"
}
```
全部生成时:
```json
{
"code": 0,
"message": "文件已生成",
"onlyHighlight": 1,
"classUrlList": [
{
"highlightId": 5,
"classId": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000,
"generated": true,
"status": "generated",
"url": "https://xdymp4.xuedianyun.com/oss/xdyui2/20260805/2008975651_highlight_5.mp4"
}
]
}
```
只要存在未生成项,就返回:
```json
{
"code": 1,
"message": "部分文件未生成",
"onlyHighlight": 1,
"classUrlList": []
}
```
实际 `classUrlList` 仍包含所有已生成和未生成项目;上例省略了项目明细。课堂没有高光时返回 `code=1`、`message=文件未生成`。
## 10. 按明细批量查询高光文件
```http
POST /highlight/files
```
适合调用方已经持有高光 ID 和开始时间、不希望 WebScreen 再按课堂查询 SaaS 的场景。一次最多 1000 条。
请求:
```json
{
"items": [
{
"highlightId": 5,
"classId": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000
}
]
}
```
响应:
```json
{
"code": 0,
"message": "success",
"data": [
{
"highlightId": 5,
"classId": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000,
"generated": false,
"status": "recording",
"url": null
}
]
}
```
## 11. 查询高光队列状态
```http
GET /highlight/status
```
响应:
```json
{
"code": 0,
"message": "success",
"data": {
"queued": 2,
"recording": 1,
"knownTasks": 5
}
}
```
| 字段 | 说明 |
|---|---|
| `queued` | 当前等待录制的任务数 |
| `recording` | 当前正在录制的任务数 |
| `knownTasks` | 当前进程内保留的全部已知任务数 |
队列状态只保存在当前 Node.js 进程内,PM2 重启后会清空;本地文件和 OSS 文件不会丢失。
## 12. 配置
配置文件:`config/config.json`。
```json
"HIGHLIGHTCONFIG": {
"enabled": true,
"sourceMode": "site",
"siteIds": ["xdyui2"],
"apiBaseUrl": "https://saas.xuedianyun.com",
"pageSize": 100,
"taskPageSize": 100,
"maxPages": 1000,
"maxConcurrent": 2,
"maxDurationMs": 21600000,
"apiTimeoutMs": 10000,
"apiRetryCount": 2,
"apiRetryBaseDelayMs": 500,
"loadGraceMs": 60000,
"endGraceMs": 10000,
"taskRetentionMs": 86400000,
"outputNamespace": "",
"outputBaseUrl": "https://xdymp4.xuedianyun.com"
}
```
| 字段 | 说明 |
|---|---|
| `enabled` | 高光业务开关;不影响整堂录制,关闭后状态接口仍可访问 |
| `sourceMode` | 定时入口的数据源:`site` 或 `task` |
| `siteIds` | 允许处理的机构编码列表 |
| `pageSize` | SaaS 高光站点查询分页大小 |
| `taskPageSize` | SaaS 录制任务查询分页大小 |
| `maxPages` | 自动分页安全上限 |
| `maxConcurrent` | 高光录制最大并发数 |
| `maxDurationMs` | 单条高光允许的最大时长 |
| `apiTimeoutMs` | SaaS 接口超时时间 |
| `apiRetryCount` | SaaS 接口失败重试次数 |
| `loadGraceMs` | 回放页面加载预留时间 |
| `endGraceMs` | 录制结束预留时间 |
| `taskRetentionMs` | 完成任务在内存中的保留时间 |
| `outputNamespace` | 可选输出隔离目录;正式环境通常留空 |
| `outputBaseUrl` | OSS 对外访问地址 |
OSS 文件查询还需要环境变量:
```text
ALIBABA_CLOUD_ACCESS_KEY_ID
ALIBABA_CLOUD_ACCESS_KEY_SECRET
```
## 13. 文件规则
```text
文件名:{classId}_highlight_{highlightId}.mp4
本地: {PROJECTCATALOG}/media/{siteId}/{yyyyMMdd}/{fileName}
OSS: oss/{siteId}/{yyyyMMdd}/{fileName}
URL: {outputBaseUrl}/oss/{siteId}/{yyyyMMdd}/{fileName}
```
`yyyyMMdd` 按 `beginTime` 对应的 Asia/Shanghai 日期计算。
录制先写入 `{PROJECTCATALOG}/.highlight_tmp`,完成后再移动到正式 `media` 目录。WebScreen 不主动上传 OSS,由服务器已有搬运程序处理,并使用 `download.json` 作为目录完成标记。
## 14. WebScreen 依赖的 SaaS 内部接口
以下接口由 WebScreen 内部调用,不应由普通前端直接调用。
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | `/3m/api/highlight/getByClassPrivate.do` | 根据课堂号获取全部高光 |
| POST | `/3m/api/highlight/getBySitePrivate.do` | 根据站点和时间范围分页获取高光 |
| POST | `/3m/api/recording/getRecordingTasksPrivate.do` | `sourceMode=task` 时获取录制任务 |
SaaS 高光记录示例:
```json
{
"id": 5,
"meetingNumber": "2008975651",
"siteId": "xdyui2",
"beginTime": 1785895298000,
"endTime": 1785895343000,
"type": 0,
"more": ""
}
```
字段映射:
```text
id → highlightId
meetingNumber → classId
```
更详细的 SaaS 请求签名和响应说明见 `getByClassPrivate.md` 与 `getBySitePrivate.md`。
## 15. 调用顺序建议
单课堂人工验证:
```text
POST /highlight/preview/by-class
→ POST /highlight/recording/by-class
→ GET /highlight/status
→ POST /highlight/fileExists
```
每日自动录制:
```text
cron
→ POST /highlight/recording/scheduled
→ 查询前一天高光
→ 加入高光队列
→ 生成本地 MP4 和 download.json
→ 服务器搬运到 OSS
→ POST /highlight/fileExists 查询结果
```
... ...
... ... @@ -123,10 +123,10 @@ curl -X POST http://127.0.0.1:3001/highlight/fileExists \
## 8. 验证 cron 全量模式
先手工执行原 cron 入口:
先手工执行独立的高光 cron 入口:
```bash
wget -qO- http://127.0.0.1:3001/recording
curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled
```
响应 `data.mode` 应为 `site`,时间窗应为 Asia/Shanghai 前一天。
... ... @@ -134,7 +134,7 @@ wget -qO- http://127.0.0.1:3001/recording
确认无误后配置:
```cron
57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1
57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1
```
## 9. 切换任务模式
... ... @@ -188,7 +188,7 @@ https://xdymp4.xuedianyun.com/oss/xdyui2/{yyyyMMdd}/{classId}_highlight_{highlig
pm2 restart webScreen
```
关闭后,`GET /recording` 继续运行原整课录制逻辑。高光使用独立文件名,不覆盖旧整课 MP4。
关闭后,高光查询、录制和文件检查接口停止处理,状态接口仍可访问;`GET /recording` 始终运行原整堂录制逻辑。高光使用独立文件名,不覆盖旧整堂 MP4。
## 12. 上线 CrazyTalk 前检查
... ...
... ... @@ -334,7 +334,7 @@ N 条高光时,`classUrlList` 必须有 N 条记录。
手工调用 cron 入口:
```bash
wget -qO- http://127.0.0.1:3001/recording
curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled
```
检查:
... ... @@ -365,7 +365,7 @@ pm2 restart webScreen
4. 其他站点 `status=0, onlyHighlight=1`:不录制。
```bash
wget -qO- http://127.0.0.1:3001/recording
curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled
```
检查:
... ... @@ -381,7 +381,7 @@ wget -qO- http://127.0.0.1:3001/recording
只有单课堂、重复录制、OSS 搬运和全量模式全部通过后才启用:
```cron
57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1
57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1
```
```bash
... ... @@ -465,7 +465,7 @@ echo "$DISPLAY"
pm2 restart webScreen
```
关闭后,`GET /recording` 恢复原整课逻辑。`POST /recording`、`POST /recordingTask`、`POST /fileExists` 和实时录制接口不变。
关闭后,高光查询、录制和文件检查接口停止处理,状态接口仍可访问;`GET /recording` 始终执行原整堂录制逻辑。`POST /recording`、`POST /recordingTask`、`POST /fileExists` 和实时录制接口不变。
## 21. 测试记录模板
... ...
... ... @@ -10,12 +10,12 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录
- 一个高光时间段生成一个 MP4;N 条高光生成 N 个文件。
- 当前只在 `xdyui2` 测试,验证后再增加 CrazyTalk。
- WebScreen 只生成本地文件;服务器已有任务定时移动到 OSS。
- 原定时任务每天 `07:57` 调用 `GET /recording`。
- 高光定时任务使用独立的 `POST /highlight/recording/scheduled`,不占用整堂录制入口。
- 任务模式暂时只处理 `status=0`;后端状态回写规则待确认。
## 2. 兼容原则
高光开关缺失或关闭时,`GET /recording` 完整执行原整课录制逻辑。
原 `GET /recording` 始终执行整堂录制逻辑,不读取高光开关。
```json
{
... ... @@ -25,10 +25,11 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录
}
```
只有显式配置 `enabled: true` 才进入高光录制。因此旧服务器、旧站点和旧客户不受影响。
只有显式配置 `enabled: true` 才能调用 `/highlight/*` 高光业务接口。关闭高光不会关闭或改变整堂录制。
以下接口保持原行为:
- `GET /recording`
- `POST /recording`
- `POST /recordingTask`
- `POST /fileExists`
... ... @@ -38,19 +39,13 @@ WebScreen 根据 SaaS 高光记录,把每个 `beginTime/endTime` 时间段录
## 3. 定时触发
WebScreen 本身不创建 cron。服务器已有 cron:
WebScreen 本身不创建 cron。高光需要独立 cron:
```cron
57 7 * * * wget http://127.0.0.1:3001/recording &
57 7 * * * curl -fsS -X POST http://127.0.0.1:3001/highlight/recording/scheduled >/dev/null 2>&1
```
建议部署时改成不落 wget 临时文件的等价写法:
```cron
57 7 * * * wget -qO- http://127.0.0.1:3001/recording >/dev/null 2>&1
```
`GET /recording` 读取 `HIGHLIGHTCONFIG.sourceMode`:
`POST /highlight/recording/scheduled` 读取 `HIGHLIGHTCONFIG.sourceMode`:
- `site`:全量高光模式。
- `task`:指定课堂任务模式。
... ... @@ -97,7 +92,7 @@ WebScreen 本身不创建 cron。服务器已有 cron:
流程:
1. cron 调用 `GET /recording`。
1. cron 调用 `POST /highlight/recording/scheduled`。
2. 按 Asia/Shanghai 计算前一天 `00:00:00.000` 至 `23:59:59.999`。
3. 遍历 `HIGHLIGHTCONFIG.siteIds`。
4. 分页调用 `getBySitePrivate.do`。
... ... @@ -288,7 +283,7 @@ WebScreen 调用 `getByClassPrivate.do` 获取该课堂全部高光,再逐条
5. `sourceMode=task` 只处理 `status=0 && onlyHighlight=1`。
6. `/highlight/fileExists` 返回课堂全部高光文件状态和地址。
7. 原 `/fileExists` 响应不变。
8. 删除或关闭 `HIGHLIGHTCONFIG` 后,`GET /recording` 恢复原整课录制。
8. 删除或关闭 `HIGHLIGHTCONFIG` 后,高光接口拒绝处理,原 `GET /recording` 仍执行整堂录制。
## 13. 已知待办
... ...
node调用录制文件
2025-0306 新的dev分支
\ No newline at end of file
2025-0306 新的dev分支
高光录制接口见 [HIGHLIGHT_API.md](./HIGHLIGHT_API.md)。
... ...
... ... @@ -72,6 +72,20 @@ router.post('/recording/by-site', async function (req, res) {
}
});
// 独立的高光定时入口,不占用原整堂录制 GET /recording。
router.post('/recording/scheduled', async function (req, res) {
try {
const data = await service.runScheduledRecording();
return res.send({
code: 0,
message: data.received ? 'success' : '无高光数据',
data
});
} catch (error) {
return sendError(res, error);
}
});
router.post('/fileExists', async function (req, res) {
try {
const data = await service.getClassFileStatuses(req.body || {});
... ...
... ... @@ -8,11 +8,6 @@ require('dotenv').config(); // 加载环境变量
const method = require("../config/method")
const config = require("../config/config")
const {
highlightRecordingService,
HighlightValidationError,
HighlightUpstreamError
} = require('../services/highlightRecordingService');
const version ='v1.2.0.20251208';
// const { GETCLASSURL, GETCLASSURLPARAMETER, PROJECTCATALOG, PROJECTWINCATALOG, BACKMEDIACONFIG } = config
const { YesterdayTime,getDayTime, getRequestClassIds, dayTimeYMD } = method
... ... @@ -219,23 +214,6 @@ router.get('/recording', async function (req, res, next) {
if (!fileConfig) return false
const parsedConfig = JSON.parse(fileConfig)
if (parsedConfig.HIGHLIGHTCONFIG && parsedConfig.HIGHLIGHTCONFIG.enabled === true) {
try {
const data = await highlightRecordingService.runScheduledRecording()
res.send({ code: "0", message: data.received ? "success" : "无高光数据", data })
} catch (error) {
new MediaCreat().wrieLog("高光录制任务读取失败:------>" + (error.message || error))
if (error instanceof HighlightValidationError) {
res.status(400).send({ code: "1", message: error.message })
} else if (error instanceof HighlightUpstreamError) {
res.status(502).send({ code: String(error.upstreamCode), message: error.message })
} else {
res.status(500).send({ code: "-1", message: "高光录制任务读取失败" })
}
}
return
}
const { GETCLASSURLPARAMETER } = parsedConfig
siteIds = GETCLASSURLPARAMETER.siteId
if (siteIds.length == 0) {
... ...
... ... @@ -821,6 +821,7 @@ class HighlightRecordingService {
}
async getFileStatuses(items) {
this.ensureFeatureEnabled();
this.cleanupTasks();
const config = this.readConfig();
const allowedSiteIds = new Set(this.getAllowedSiteIds());
... ...
... ... @@ -42,6 +42,13 @@ async function run() {
assert.strictEqual(invalidFiles.status, 400);
assert.strictEqual(invalidFiles.body.code, 10);
const disabledFiles = await request(server, 'POST', '/highlight/files', {
items: [{ highlightId: 5, classId: '1001', siteId: 'xdyui2', beginTime: 1785895298000 }]
});
assert.strictEqual(disabledFiles.status, 400);
assert.strictEqual(disabledFiles.body.code, 10);
assert.strictEqual(disabledFiles.body.message, '高光录制功能未启用');
const invalidPreview = await request(server, 'POST', '/highlight/preview/by-class', {});
assert.strictEqual(invalidPreview.status, 400);
assert.strictEqual(invalidPreview.body.code, 10);
... ... @@ -50,6 +57,51 @@ async function run() {
assert.strictEqual(invalidClassFiles.status, 400);
assert.strictEqual(invalidClassFiles.body.code, 10);
const disabledScheduled = await request(server, 'POST', '/highlight/recording/scheduled');
assert.strictEqual(disabledScheduled.status, 400);
assert.strictEqual(disabledScheduled.body.code, 10);
assert.strictEqual(disabledScheduled.body.message, '高光录制功能未启用');
const originalRunScheduledRecording = highlightRecordingService.runScheduledRecording;
try {
highlightRecordingService.runScheduledRecording = async () => ({
mode: 'site',
beginTime: 1785945600000,
endTime: 1786031999999,
received: 2,
queued: 2,
recording: 0,
uploading: 0,
generated: 0,
invalid: 0,
failed: 0
});
const scheduled = await request(server, 'POST', '/highlight/recording/scheduled');
assert.strictEqual(scheduled.status, 200);
assert.strictEqual(scheduled.body.code, 0);
assert.strictEqual(scheduled.body.message, 'success');
assert.strictEqual(scheduled.body.data.mode, 'site');
assert.strictEqual(scheduled.body.data.received, 2);
highlightRecordingService.runScheduledRecording = async () => ({
mode: 'site',
beginTime: 1785945600000,
endTime: 1786031999999,
received: 0,
queued: 0,
recording: 0,
uploading: 0,
generated: 0,
invalid: 0,
failed: 0
});
const scheduledEmpty = await request(server, 'POST', '/highlight/recording/scheduled');
assert.strictEqual(scheduledEmpty.body.code, 0);
assert.strictEqual(scheduledEmpty.body.message, '无高光数据');
} finally {
highlightRecordingService.runScheduledRecording = originalRunScheduledRecording;
}
const originalGetClassFileStatuses = highlightRecordingService.getClassFileStatuses;
highlightRecordingService.getClassFileStatuses = async () => [
{ highlightId: 4, classId: '1001', siteId: 'xdyui2', generated: true, status: 'generated', url: 'https://example/4.mp4' },
... ...