Files
iot-device-management-frontend/apps/web-antd/src/api/video/device/index.ts
lzh ea15ef332c 重构: video 模块走芋道网关统一鉴权,移除独立 WVP JWT 客户端
- api/video/request.ts: 删除自实现的 WVP access-token 登录/缓存/401 续期逻辑,
  直接 re-export requestClient(yudao Authorization: Bearer),
  wvpRequestClient 名字仅作过渡期别名,TODO 标记后续统一重命名。
- api/video/device/index.ts: 路径从 /video/device/{proxy,user,server,...} 迁移到
  后端 @RequestMapping 对齐的 /video/{proxy,ai/*,server/media_server};
  删除 getAlertImageUrl(老 WVP 时代产物),删除截图 URL 里的 ?access-token。
- api/video/edge/index.ts: /video/device/device/* -> /video/ai/device/*(对齐 AiEdgeDeviceController)。
- views/video/device/camera: HEAD 探活 URL 同步,补注释说明后端需对
  /video/ai/roi/snap/image permitAll 或依赖 session cookie。
- vite.config: 删除 /admin-api/video/device/* 按子路径 rewrite 到 WVP:18080 的规则,
  统一走 /admin-api 到芋道网关 48080;去掉 VITE_WVP_USERNAME/PASSWORD_MD5 依赖
  (安全改进:凭据不再打进客户端 bundle)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:33:37 +08:00

314 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Video 模块 - 设备管理 API
*
* 请求走统一的 requestClientyudao 框架鉴权Authorization: Bearer
* video 微服务已接入 gateway路径对齐后端 @RequestMapping
* /video/proxy/* 拉流代理StreamProxyController
* /video/server/* 媒体服务器ServerController
* /video/ai/camera/* 摄像头
* /video/ai/roi/* ROI 及算法绑定
* /video/ai/algorithm/* 算法
* /video/ai/config/* 配置推送
*/
import { useAppConfig } from '@vben/hooks';
import { wvpRequestClient } from '#/api/video/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
// ==================== 类型定义 ====================
export namespace VideoDeviceApi {
/** 分页响应结构 */
export interface PageResult<T> {
list: T[];
total: number;
}
/** 摄像头(拉流代理) */
export interface Camera {
id?: number;
type?: string; // 'default' | 'ffmpeg'
app?: string;
stream?: string;
cameraCode?: string; // 摄像头唯一编码
cameraName?: string; // 摄像头名称(用户自定义)
edgeDeviceId?: string; // 绑定的边缘设备ID
srcUrl?: string;
timeout?: number;
rtspType?: string; // '0'=TCP, '1'=UDP, '2'=Multicast
enable?: boolean;
enableAudio?: boolean;
enableMp4?: boolean;
enableDisableNoneReader?: boolean;
relatesMediaServerId?: string;
ffmpegCmdKey?: string;
pulling?: boolean;
mediaServerId?: string;
streamKey?: string;
createTime?: string;
}
/** ROI 区域 */
export interface Roi {
id?: number;
roiId?: string;
cameraId?: string;
deviceId?: string;
name?: string;
roiType?: string; // 'rectangle' | 'polygon'
coordinates?: string;
color?: string;
priority?: number;
enabled?: number; // 0 | 1
description?: string;
algorithms?: RoiAlgoBinding[];
}
/** ROI 算法绑定(详情嵌套结构) */
export interface RoiAlgoBinding {
bind: AlgoBind;
algorithm?: Algorithm;
}
/** 算法绑定记录 */
export interface AlgoBind {
bindId?: string;
roiId?: string;
algoCode?: string;
enabled?: number; // 0 | 1
params?: string;
}
/** 算法定义 */
export interface Algorithm {
id?: number;
algoCode?: string;
algoName?: string;
description?: string;
isActive?: boolean;
paramSchema?: string;
globalParams?: string;
}
/** 媒体服务器 */
export interface MediaServer {
id: string;
ip: string;
}
}
// ==================== 摄像头管理 ====================
/** 摄像头列表(分页) */
export function getCameraList(params: {
page: number;
count: number;
query?: string;
pulling?: boolean;
}) {
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Camera>>(
'/video/proxy/list',
{ params },
);
}
/** 新增摄像头 */
export function addCamera(data: Partial<VideoDeviceApi.Camera>) {
return wvpRequestClient.post('/video/proxy/add', data);
}
/** 编辑摄像头 */
export function updateCamera(data: Partial<VideoDeviceApi.Camera>) {
return wvpRequestClient.post('/video/proxy/update', data);
}
/** 保存摄像头(新增/编辑,有 id 为编辑) */
export function saveCamera(data: Partial<VideoDeviceApi.Camera>) {
return data.id ? updateCamera(data) : addCamera(data);
}
/** 删除摄像头 */
export function deleteCamera(id: number) {
return wvpRequestClient.delete('/video/proxy/delete', {
params: { id },
});
}
/** 开始拉流 */
export function startCamera(id: number) {
return wvpRequestClient.get('/video/proxy/start', { params: { id } });
}
/** 停止拉流 */
export function stopCamera(id: number) {
return wvpRequestClient.get('/video/proxy/stop', { params: { id } });
}
/** 在线媒体服务器列表 */
export function getMediaServerList() {
return wvpRequestClient.get<VideoDeviceApi.MediaServer[]>(
'/video/server/media_server/online/list',
);
}
/** 摄像头选项列表(用于下拉搜索选择) */
export function getCameraOptions() {
return wvpRequestClient.get<
{ cameraCode: string; cameraName: string }[]
>('/video/ai/camera/options');
}
// ==================== ROI 区域管理 ====================
/** ROI 列表(分页) */
export function getRoiList(params: {
page: number;
count: number;
cameraId?: string;
deviceId?: string;
query?: string;
}) {
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Roi>>(
'/video/ai/roi/list',
{ params },
);
}
/** ROI 详情(含算法绑定) */
export function getRoiDetail(id: number) {
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/ai/roi/${id}`);
}
/** 某摄像头的所有 ROI */
export function getRoiByCameraId(cameraId: string) {
return wvpRequestClient.get<VideoDeviceApi.Roi[]>(
'/video/ai/roi/channel',
{ params: { cameraId } },
);
}
/** 保存 ROI新增/编辑) */
export function saveRoi(data: Partial<VideoDeviceApi.Roi>) {
return wvpRequestClient.post('/video/ai/roi/save', data);
}
/** 删除 ROI */
export function deleteRoi(roiId: string) {
return wvpRequestClient.delete(`/video/ai/roi/delete/${roiId}`);
}
/**
* 获取摄像头截图 URL
*
* 非 force返回代理 URL浏览器按 URL 缓存。
* force先触发边缘端截图再返回加时间戳的代理 URL 破缓存。
*
* 图片鉴权假设(重要):
* - 老 WVP 的 ?access-token=xxx 已移除。
* - 返回的 URL 会被 <img src> 使用,浏览器不会带上 Authorization 头;
* 该端点必须在后端满足以下任一条件之一才能正常显示:
* (a) SecurityConfig 里对 /video/ai/roi/snap/image 放行permitAll
* (b) 依赖 yudao session cookie非 JWT做鉴权
* (c) 后端返回预签名的 OSS URL 并由调用方改造成 302 重定向。
* - 任何 401/403 不会在这里抛出,调用方需要处理 <img onerror> 以显示占位图。
*/
export async function getSnapUrl(cameraCode: string, force = false): Promise<string> {
if (force) {
// 强制刷新:先触发边缘端截图(等待完成)
try {
await wvpRequestClient.get('/video/ai/roi/snap', {
params: { cameraCode, force: true },
});
} catch (err) {
// 截图请求可能超时,但 OSS 上可能已有图片,继续返回代理 URL
if (import.meta.env.DEV) {
console.debug('[video] force snap 触发失败,继续返回已有代理 URL', err);
}
}
return (
`${apiURL}/video/ai/roi/snap/image` +
`?cameraCode=${encodeURIComponent(cameraCode)}` +
`&t=${Date.now()}`
);
}
return (
`${apiURL}/video/ai/roi/snap/image` +
`?cameraCode=${encodeURIComponent(cameraCode)}`
);
}
// ==================== 算法管理 ====================
/** 算法列表 */
export function getAlgorithmList(deviceId?: string) {
return wvpRequestClient.get<VideoDeviceApi.Algorithm[]>(
'/video/ai/algorithm/list',
{ params: deviceId ? { deviceId } : {} },
);
}
/** 保存算法全局参数 */
export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
return wvpRequestClient.post(
`/video/ai/algorithm/global-params/${algoCode}`,
{ globalParams },
);
}
/** 批量更新设备算法参数 */
export function updateDeviceAlgoParams(deviceId: string, algoCode: string, params: string) {
return wvpRequestClient.post(
`/video/ai/algorithm/device-binds/${deviceId}/${algoCode}`,
{ params },
);
}
// ==================== 算法绑定 ====================
/** 绑定算法到 ROI */
export function bindAlgo(data: { roiId: string; algoCode: string }) {
return wvpRequestClient.post('/video/ai/roi/bindAlgo', data);
}
/** 解绑算法 */
export function unbindAlgo(bindId: string) {
return wvpRequestClient.delete('/video/ai/roi/unbindAlgo', {
params: { bindId },
});
}
/** 更新算法绑定参数 */
export function updateAlgoParams(data: {
bindId: string;
params?: string;
enabled?: number;
}) {
return wvpRequestClient.post('/video/ai/roi/updateAlgoParams', data);
}
// ==================== 配置推送 ====================
/** 推送配置到边缘端 */
export function pushConfig(cameraId: string) {
return wvpRequestClient.post('/video/ai/config/push', null, {
params: { cameraId },
});
}
/** 一次性推送全部配置到本地Edge */
export function pushAllConfig() {
return wvpRequestClient.post<Record<string, any>>(
'/video/ai/config/push-all',
);
}
/** 导出摄像头配置 JSON */
export function exportConfig(cameraId: string) {
return wvpRequestClient.get<Record<string, any>>(
'/video/ai/config/export',
{ params: { cameraId } },
);
}