重构: 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>
This commit is contained in:
lzh
2026-04-23 19:33:37 +08:00
parent 0aa4a2f68e
commit ea15ef332c
5 changed files with 68 additions and 216 deletions

View File

@@ -1,16 +1,18 @@
/**
* Video 模块 - 设备管理 API
*
* 所有请求通过 wvpRequestClient 发送到 WVP 视频平台后端,
* 由 Vite 代理按路径分发并 rewrite
* /video/device/proxy/* → WVP /api/proxy/*
* /video/device/user/* → WVP /api/user/*
* /video/device/server/* → WVP /api/server/media_server/*
* /video/device/* → WVP /api/ai/*
* 请求走统一的 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 { getWvpToken, wvpRequestClient } from '#/api/video/request';
import { wvpRequestClient } from '#/api/video/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
@@ -106,19 +108,19 @@ export function getCameraList(params: {
pulling?: boolean;
}) {
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Camera>>(
'/video/device/proxy/list',
'/video/proxy/list',
{ params },
);
}
/** 新增摄像头 */
export function addCamera(data: Partial<VideoDeviceApi.Camera>) {
return wvpRequestClient.post('/video/device/proxy/add', data);
return wvpRequestClient.post('/video/proxy/add', data);
}
/** 编辑摄像头 */
export function updateCamera(data: Partial<VideoDeviceApi.Camera>) {
return wvpRequestClient.post('/video/device/proxy/update', data);
return wvpRequestClient.post('/video/proxy/update', data);
}
/** 保存摄像头(新增/编辑,有 id 为编辑) */
@@ -128,25 +130,25 @@ export function saveCamera(data: Partial<VideoDeviceApi.Camera>) {
/** 删除摄像头 */
export function deleteCamera(id: number) {
return wvpRequestClient.delete('/video/device/proxy/delete', {
return wvpRequestClient.delete('/video/proxy/delete', {
params: { id },
});
}
/** 开始拉流 */
export function startCamera(id: number) {
return wvpRequestClient.get('/video/device/proxy/start', { params: { id } });
return wvpRequestClient.get('/video/proxy/start', { params: { id } });
}
/** 停止拉流 */
export function stopCamera(id: number) {
return wvpRequestClient.get('/video/device/proxy/stop', { params: { id } });
return wvpRequestClient.get('/video/proxy/stop', { params: { id } });
}
/** 在线媒体服务器列表 */
export function getMediaServerList() {
return wvpRequestClient.get<VideoDeviceApi.MediaServer[]>(
'/video/device/server/online/list',
'/video/server/media_server/online/list',
);
}
@@ -154,7 +156,7 @@ export function getMediaServerList() {
export function getCameraOptions() {
return wvpRequestClient.get<
{ cameraCode: string; cameraName: string }[]
>('/video/device/camera/options');
>('/video/ai/camera/options');
}
// ==================== ROI 区域管理 ====================
@@ -168,68 +170,71 @@ export function getRoiList(params: {
query?: string;
}) {
return wvpRequestClient.get<VideoDeviceApi.PageResult<VideoDeviceApi.Roi>>(
'/video/device/roi/list',
'/video/ai/roi/list',
{ params },
);
}
/** ROI 详情(含算法绑定) */
export function getRoiDetail(id: number) {
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/device/roi/${id}`);
return wvpRequestClient.get<VideoDeviceApi.Roi>(`/video/ai/roi/${id}`);
}
/** 某摄像头的所有 ROI */
export function getRoiByCameraId(cameraId: string) {
return wvpRequestClient.get<VideoDeviceApi.Roi[]>(
'/video/device/roi/channel',
'/video/ai/roi/channel',
{ params: { cameraId } },
);
}
/** 保存 ROI新增/编辑) */
export function saveRoi(data: Partial<VideoDeviceApi.Roi>) {
return wvpRequestClient.post('/video/device/roi/save', data);
return wvpRequestClient.post('/video/ai/roi/save', data);
}
/** 删除 ROI */
export function deleteRoi(roiId: string) {
return wvpRequestClient.delete(`/video/device/roi/delete/${roiId}`);
return wvpRequestClient.delete(`/video/ai/roi/delete/${roiId}`);
}
/**
* 获取摄像头截图 URL
*
* 非 force 模式:直接返回 /snap/image 代理 URL无时间戳浏览器自动缓存
* force 模式:先触发边缘端截图,再返回时间戳的代理 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> {
const token = await getWvpToken();
if (force) {
// 强制刷新:先触发边缘端截图(等待完成)
try {
await wvpRequestClient.get('/video/device/roi/snap', {
await wvpRequestClient.get('/video/ai/roi/snap', {
params: { cameraCode, force: true },
});
} catch (err) {
// 截图请求可能超时,但 COS 上可能已有图片,继续返回代理 URL
// 留一条日志便于图片加载 401/404 时回溯根因
// 截图请求可能超时,但 OSS 上可能已有图片,继续返回代理 URL
if (import.meta.env.DEV) {
console.debug('[WVP] force snap 触发失败,继续返回已有代理 URL', err);
console.debug('[video] force snap 触发失败,继续返回已有代理 URL', err);
}
}
// 加时间戳破浏览器缓存
return (
`${apiURL}/video/device/roi/snap/image` +
`${apiURL}/video/ai/roi/snap/image` +
`?cameraCode=${encodeURIComponent(cameraCode)}` +
`&access-token=${encodeURIComponent(token)}` +
`&t=${Date.now()}`
);
}
// 非 force使用代理端点不加时间戳浏览器自动缓存
return (
`${apiURL}/video/device/roi/snap/image` +
`?cameraCode=${encodeURIComponent(cameraCode)}` +
`&access-token=${encodeURIComponent(token)}`
`${apiURL}/video/ai/roi/snap/image` +
`?cameraCode=${encodeURIComponent(cameraCode)}`
);
}
@@ -238,7 +243,7 @@ export async function getSnapUrl(cameraCode: string, force = false): Promise<str
/** 算法列表 */
export function getAlgorithmList(deviceId?: string) {
return wvpRequestClient.get<VideoDeviceApi.Algorithm[]>(
'/video/device/algorithm/list',
'/video/ai/algorithm/list',
{ params: deviceId ? { deviceId } : {} },
);
}
@@ -246,7 +251,7 @@ export function getAlgorithmList(deviceId?: string) {
/** 保存算法全局参数 */
export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
return wvpRequestClient.post(
`/video/device/algorithm/global-params/${algoCode}`,
`/video/ai/algorithm/global-params/${algoCode}`,
{ globalParams },
);
}
@@ -254,7 +259,7 @@ export function saveAlgoGlobalParams(algoCode: string, globalParams: string) {
/** 批量更新设备算法参数 */
export function updateDeviceAlgoParams(deviceId: string, algoCode: string, params: string) {
return wvpRequestClient.post(
`/video/device/algorithm/device-binds/${deviceId}/${algoCode}`,
`/video/ai/algorithm/device-binds/${deviceId}/${algoCode}`,
{ params },
);
}
@@ -263,12 +268,12 @@ export function updateDeviceAlgoParams(deviceId: string, algoCode: string, param
/** 绑定算法到 ROI */
export function bindAlgo(data: { roiId: string; algoCode: string }) {
return wvpRequestClient.post('/video/device/roi/bindAlgo', data);
return wvpRequestClient.post('/video/ai/roi/bindAlgo', data);
}
/** 解绑算法 */
export function unbindAlgo(bindId: string) {
return wvpRequestClient.delete('/video/device/roi/unbindAlgo', {
return wvpRequestClient.delete('/video/ai/roi/unbindAlgo', {
params: { bindId },
});
}
@@ -279,14 +284,14 @@ export function updateAlgoParams(data: {
params?: string;
enabled?: number;
}) {
return wvpRequestClient.post('/video/device/roi/updateAlgoParams', data);
return wvpRequestClient.post('/video/ai/roi/updateAlgoParams', data);
}
// ==================== 配置推送 ====================
/** 推送配置到边缘端 */
export function pushConfig(cameraId: string) {
return wvpRequestClient.post('/video/device/config/push', null, {
return wvpRequestClient.post('/video/ai/config/push', null, {
params: { cameraId },
});
}
@@ -294,30 +299,15 @@ export function pushConfig(cameraId: string) {
/** 一次性推送全部配置到本地Edge */
export function pushAllConfig() {
return wvpRequestClient.post<Record<string, any>>(
'/video/device/config/push-all',
'/video/ai/config/push-all',
);
}
/** 导出摄像头配置 JSON */
export function exportConfig(cameraId: string) {
return wvpRequestClient.get<Record<string, any>>(
'/video/device/config/export',
'/video/ai/config/export',
{ params: { cameraId } },
);
}
// ==================== 告警图片代理 ====================
/**
* 构造告警图片代理 URL通过 WVP 下载 COS 图片后返回字节流)
* @param imagePath COS 对象路径或完整 URL
*/
export async function getAlertImageUrl(imagePath: string): Promise<string> {
if (!imagePath) return '';
const token = await getWvpToken();
return (
`${apiURL}/video/device/alert/image` +
`?imagePath=${encodeURIComponent(imagePath)}` +
`&access-token=${encodeURIComponent(token)}`
);
}