security-camera-sdk
Version:
Universal SDK for interfacing with various security camera vendors including Hikvision, Dahua, Uniview and others
491 lines (427 loc) • 13.8 kB
JavaScript
/**
* 大华ICC平台 OpenAPI 客户端
*/
const axios = require("axios");
const https = require("https");
const { DahuaAuth } = require("./auth");
const { ApiError, AuthError, NetworkError, ParameterError } = require("../../utils/errors/cameraErrors");
const { Logger } = require("../../utils/logger");
const { DahuaAPI, API_PATHS } = require("./api");
// 创建logger实例
const logger = new Logger();
class DahuaClient {
/**
* 初始化大华ICC平台客户端
* @param {Object} config 配置对象
* @param {string} config.host 大华平台地址
* @param {number} config.port 端口号,默认443
* @param {string} config.protocol 协议,默认https
* @param {string} config.username 用户名
* @param {string} config.password 密码
* @param {string} config.client_id 客户端ID
* @param {string} config.client_secret 客户端密钥
* @param {boolean} config.debug 是否开启调试模式,默认false
* @param {number} config.timeout 请求超时时间(ms),默认10000
* @param {boolean} config.rejectUnauthorized 是否验证SSL证书,默认false
*/
constructor(config) {
// 验证必需参数
this.validateConfig(config);
// 配置属性
this.host = config.host.trim();
this.port = config.port || 443;
this.protocol = config.protocol || "https";
this.username = config.username;
this.password = config.password;
this.clientId = config.client_id;
this.clientSecret = config.client_secret;
this.debug = config.debug || false;
this.timeout = config.timeout || 10000;
// 构建基础URL
this.baseURL = `${this.protocol}://${this.host}:${this.port}`;
// 认证相关
this.accessToken = null;
this.tokenType = 'bearer';
this.tokenExpiresAt = null;
// ICC OAuth 接口前缀
this.iccApiPrefix = '/evo-apigw/evo-oauth/1.0.0/oauth';
// 初始化认证处理器
this.auth = new DahuaAuth(
this.username,
this.password,
this.clientId,
this.clientSecret,
this.debug
);
// 创建logger实例
this.logger = new Logger(this.debug);
// 初始化HTTP客户端
this.initHttpClient(config);
// 初始化API封装
this.api = new DahuaAPI(this);
// 自动代理API方法,使可以直接调用client.methodName()而不是client.api.methodName()
this.setupAPIMethodProxy();
if (this.debug) {
this.logger.info("大华ICC SDK初始化成功", {
baseURL: this.baseURL,
username: this.username,
timeout: this.timeout,
});
}
}
/**
* 验证配置参数
* @param {Object} config 配置对象
*/
validateConfig(config) {
if (!config) {
throw new Error("配置对象不能为空");
}
if (!config.host) {
throw new Error("host参数不能为空");
}
if (!config.username) {
throw new Error("username参数不能为空");
}
if (!config.password) {
throw new Error("password参数不能为空");
}
if (!config.client_id) {
throw new Error("client_id参数不能为空");
}
if (!config.client_secret) {
throw new Error("client_secret参数不能为空");
}
}
/**
* 初始化HTTP客户端
* @param {Object} config 配置对象
*/
initHttpClient(config) {
const httpsAgent = new https.Agent({
rejectUnauthorized: config.rejectUnauthorized || false,
keepAlive: true,
timeout: this.timeout,
});
this.httpClient = axios.create({
baseURL: this.baseURL,
timeout: this.timeout,
httpsAgent: httpsAgent,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "Dahua-ICC-SDK/1.0.0",
},
});
// 添加请求拦截器
this.httpClient.interceptors.request.use(
async (requestConfig) => {
// 检查是否是认证相关请求
const isAuthRequest = this.isAuthRequest(requestConfig);
// 只对非认证请求执行认证检查
if (!isAuthRequest) {
// 自动确保认证
await this.ensureAuthenticated();
// 添加认证头
if (this.isAuthenticated()) {
requestConfig.headers.Authorization = `${this.tokenType} ${this.accessToken}`;
}
}
if (this.debug) {
this.logger.debug("请求配置", {
method: requestConfig.method,
url: requestConfig.url,
params: requestConfig.params,
headers: requestConfig.headers,
data: requestConfig.data,
isAuthRequest: isAuthRequest
});
}
return requestConfig;
},
(error) => {
if (this.debug) {
this.logger.error("请求拦截器错误", error);
}
return Promise.reject(new NetworkError("请求配置失败", error));
}
);
// 添加响应拦截器
this.httpClient.interceptors.response.use(
(response) => {
if (this.debug) {
this.logger.debug("响应数据", {
status: response.status,
headers: response.headers,
data: response.data,
});
}
return response;
},
async (error) => {
// 如果是认证错误,尝试重新登录
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
const originalRequest = error.config;
// 检查是否是认证相关请求
const isAuthRequest = this.isAuthRequest(originalRequest);
// 只对非认证请求尝试重新登录
if (!isAuthRequest) {
if (this.debug) {
this.logger.info("检测到认证错误,尝试重新登录...");
}
const loginResult = await this.login();
if (loginResult.success) {
// 更新认证头
originalRequest.headers.Authorization = `${this.tokenType} ${this.accessToken}`;
// 重新发送请求
try {
const retryResponse = await this.httpClient.request(originalRequest);
return retryResponse;
} catch (retryError) {
return Promise.reject(this.handleResponseError(retryError));
}
}
}
}
return Promise.reject(this.handleResponseError(error));
}
);
}
/**
* 判断是否是认证相关请求
* @param {Object} requestConfig 请求配置
* @returns {boolean} 是否是认证相关请求
*/
isAuthRequest(requestConfig) {
const url = requestConfig.url;
return url.includes(API_PATHS.PUBLIC_KEY) || url.includes(API_PATHS.ACCESS_TOKEN);
}
/**
* 处理响应错误
* @param {Error} error 错误对象
*/
handleResponseError(error) {
if (this.debug) {
this.logger.error("响应错误", error);
}
// axios 1.x 版本的错误对象结构有所不同
if (error.response) {
// 服务器返回了错误状态码
const { status, data } = error.response;
if (status === 401 || status === 403) {
return new AuthError("认证失败", data);
}
return new ApiError(`API错误 (${status})`, data, status);
} else if (error.request) {
// 请求已发出但没有收到响应
return new NetworkError("网络错误,请检查网络连接", error);
} else {
// 其他错误
return new NetworkError("未知错误", error);
}
}
/**
* 发送GET请求
* @param {string} path API路径
* @param {Object} params 查询参数
* @returns {Promise} 响应数据
*/
async get(path, params = {}) {
try {
const response = await this.httpClient.get(path, { params });
return this.processResponse(response);
} catch (error) {
throw this.enhanceError(error, "GET", path);
}
}
/**
* 发送POST请求
* @param {string} path API路径
* @param {Object} data 请求体数据
* @returns {Promise} 响应数据
*/
async post(path, data = {}) {
try {
const response = await this.httpClient.post(path, data);
return this.processResponse(response);
} catch (error) {
throw this.enhanceError(error, "POST", path);
}
}
/**
* 发送PUT请求
* @param {string} path API路径
* @param {Object} data 请求体数据
* @returns {Promise} 响应数据
*/
async put(path, data = {}) {
try {
const response = await this.httpClient.put(path, data);
return this.processResponse(response);
} catch (error) {
throw this.enhanceError(error, "PUT", path);
}
}
/**
* 发送DELETE请求
* @param {string} path API路径
* @returns {Promise} 响应数据
*/
async delete(path) {
try {
const response = await this.httpClient.delete(path);
return this.processResponse(response);
} catch (error) {
throw this.enhanceError(error, "DELETE", path);
}
}
/**
* 通用请求方法,允许用户直接发送请求
* @param {string} method HTTP方法 (GET, POST, PUT, DELETE)
* @param {string} path API路径
* @param {Object} options 请求选项
* @param {Object} options.params 查询参数
* @param {Object} options.data 请求体数据
* @returns {Promise} 响应数据
*/
async request(method, path, options = {}) {
try {
const config = {
method: method.toUpperCase(),
url: path,
params: options.params || {},
data: options.data || {}
};
const response = await this.httpClient.request(config);
return this.processResponse(response);
} catch (error) {
throw this.enhanceError(error, method, path);
}
}
/**
* 处理响应数据
* @param {Object} response axios响应对象
* @returns {Object} 处理后的响应数据
*/
processResponse(response) {
const { status, data } = response;
// 检查大华API返回的业务状态
if (data && data.hasOwnProperty('success') && !data.success) {
throw new ApiError(
`API业务错误: ${data.errMsg || "未知错误"}`,
data,
status
);
}
return {
success: true,
status: status,
data: data,
message: data.desc || data.errMsg || "success",
};
}
/**
* 增强错误信息
* @param {Error} error 原始错误
* @param {string} method HTTP方法
* @param {string} path API路径
* @returns {Error} 增强后的错误
*/
enhanceError(error, method, path) {
if (error.isCameraError) {
return error;
}
error.method = method;
error.path = path;
error.timestamp = new Date().toISOString();
return error;
}
/**
* 判断是否已认证且 token 未过期
* @returns {boolean}
*/
isAuthenticated() {
if (!this.accessToken) return false;
if (this.tokenExpiresAt && Date.now() >= this.tokenExpiresAt) {
return false; // token 已过期
}
return true;
}
/**
* 获取访问令牌(登录)
* @returns {Promise<{ success: boolean, data?: object, msg?: string }>}
*/
async login() {
try {
if (this.debug) {
this.logger.info('开始登录流程...');
}
const publicKey = await this.api.getPublicKey();
const tokenData = await this.api.getAccessToken(publicKey);
if (tokenData?.success) {
const { access_token, token_type, expires_in } = tokenData.data;
this.accessToken = access_token;
this.tokenType = token_type || 'bearer';
// 设置过期时间(提前 60 秒刷新)
this.tokenExpiresAt = Date.now() + (expires_in - 60) * 1000;
if (this.debug) {
this.logger.info('ICC 登录成功,access_token 已设置');
this.logger.info('token 过期时间:', new Date(this.tokenExpiresAt).toLocaleString());
}
return { success: true, data: tokenData.data };
} else {
if (this.debug) {
this.logger.error('登录失败:', tokenData?.errMsg || '未知错误');
}
return { success: false, msg: `登录失败: ${tokenData?.errMsg || '未知错误'}` };
}
} catch (err) {
if (this.debug) {
this.logger.error('登录失败:', err.message);
if (err.response) {
this.logger.error('响应:', err.response.data);
}
}
return { success: false, msg: `登录失败: ${err.message}` };
}
}
/**
* 确保已认证
* @returns {Promise<{ success: boolean, msg?: string }>}
*/
async ensureAuthenticated() {
if (this.isAuthenticated()) {
return { success: true, msg: '已认证' };
}
if (this.debug) {
this.logger.info('token 无效或未登录,正在重新登录...');
}
const result = await this.login();
return result;
}
/**
* 关闭客户端
*/
async close() {
this.accessToken = null;
this.tokenExpiresAt = null;
if (this.debug) {
this.logger.info('大华 SDK 已关闭');
}
}
/**
* 设置API方法代理,自动将API对象上的方法代理到客户端实例上
*/
setupAPIMethodProxy() {
// 遍历API对象的所有属性
Object.getOwnPropertyNames(Object.getPrototypeOf(this.api)).forEach(key => {
// 跳过构造函数和非函数属性
if (key === 'constructor' || typeof this.api[key] !== 'function' || key === 'login') {
return;
}
// 将API方法绑定到客户端实例上
this[key] = this.api[key].bind(this.api);
});
}
}
module.exports = { DahuaClient };