security-camera-sdk
Version:
Universal SDK for interfacing with various security camera vendors including Hikvision, Dahua, Uniview and others
409 lines (358 loc) • 11.4 kB
JavaScript
/**
* 海康威视 OpenAPI 客户端
*/
const axios = require("axios");
const https = require("https");
const { HikvisionAuth } = require("./auth");
const { ApiError, AuthError, NetworkError } = require("../../utils/errors/cameraErrors");
const { Logger } = require("../../utils/logger");
const { HikvisionAPI } = require("./api");
// 创建logger实例
const logger = new Logger();
class HikvisionClient {
/**
* 初始化海康威视客户端
* @param {Object} config 配置对象
* @param {string} config.host 海康威视平台地址
* @param {number} config.port 端口号,默认443
* @param {string} config.protocol 协议,默认https
* @param {string|number} config.appKey 应用密钥
* @param {string} config.appSecret 应用秘钥
* @param {boolean} config.debug 是否开启调试模式,默认false
* @param {number} config.timeout 请求超时时间(ms),默认30000
* @param {boolean} config.rejectUnauthorized 是否验证SSL证书,默认false
*/
constructor(config) {
// 验证必需参数
this.validateConfig(config);
// 配置属性
this.host = config.host;
this.port = config.port || 443;
this.protocol = config.protocol || "https";
this.appKey = String(config.appKey); // 确保是字符串格式
this.appSecret = config.appSecret;
this.debug = config.debug || false;
this.timeout = config.timeout || 30000;
// 构建基础URL
this.baseURL = `${this.protocol}://${this.host}:${this.port}`;
// 初始化认证处理器
this.auth = new HikvisionAuth(this.appKey, this.appSecret, this.debug);
// 创建logger实例
this.logger = new Logger(this.debug);
// 初始化HTTP客户端
this.initHttpClient(config);
// 初始化API封装
this.api = new HikvisionAPI(this);
// 自动代理API方法,使可以直接调用client.methodName()而不是client.api.methodName()
this.setupAPIMethodProxy();
if (this.debug) {
this.logger.info("海康威视SDK初始化成功", {
baseURL: this.baseURL,
appKey: this.appKey,
timeout: this.timeout,
});
}
}
/**
* 验证配置参数
* @param {Object} config 配置对象
*/
validateConfig(config) {
if (!config) {
throw new Error("配置对象不能为空");
}
if (!config.host) {
throw new Error("host参数不能为空");
}
if (!config.appKey) {
throw new Error("appKey参数不能为空");
}
if (!config.appSecret) {
throw new Error("appSecret参数不能为空");
}
}
/**
* 初始化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": "Hikvision-Node-SDK/1.0.0",
},
});
// 添加请求拦截器
this.httpClient.interceptors.request.use(
(requestConfig) => {
// 构建完整的URL(包含查询参数,用于签名)
let fullUrl = requestConfig.url;
if (
requestConfig.params &&
Object.keys(requestConfig.params).length > 0
) {
const searchParams = new URLSearchParams(requestConfig.params);
fullUrl = `${requestConfig.url}?${searchParams.toString()}`;
}
// 生成签名并添加认证头
const authHeaders = this.auth.generateAuthHeaders(
requestConfig.method.toUpperCase(),
fullUrl,
requestConfig.data
);
requestConfig.headers = {
...requestConfig.headers,
...authHeaders,
};
if (this.debug) {
this.logger.debug("请求配置", {
method: requestConfig.method,
url: requestConfig.url,
fullUrl: fullUrl,
params: requestConfig.params,
headers: requestConfig.headers,
data: requestConfig.data,
});
// 生成并输出对应的curl命令
const curlCommand = this.generateCurlCommand(requestConfig, fullUrl);
console.log("\n🔗 对应的 curl 命令:");
console.log("─".repeat(80));
console.log(curlCommand);
console.log("─".repeat(80));
}
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;
},
(error) => {
return this.handleResponseError(error);
}
);
}
/**
* 处理响应错误
* @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 Promise.reject(new AuthError("认证失败", data));
}
return Promise.reject(new ApiError(`API错误 (${status})`, data, status));
} else if (error.request) {
// 请求已发出但没有收到响应
return Promise.reject(
new NetworkError("网络错误,请检查网络连接", error)
);
} else {
// 其他错误
return Promise.reject(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.code && data.code !== "0") {
throw new ApiError(
`API业务错误: ${data.msg || "未知错误"}`,
data,
status
);
}
return {
success: true,
status: status,
data: data,
message: data.msg || "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;
}
/**
* 生成对应的curl命令
* @param {Object} requestConfig axios请求配置
* @param {string} fullUrl 完整的URL路径
* @returns {string} curl命令字符串
*/
generateCurlCommand(requestConfig, fullUrl) {
const method = requestConfig.method.toUpperCase();
const completeUrl = `${this.baseURL}${fullUrl}`;
// 开始构建curl命令
let curlCommand = `curl -X ${method}`;
// 添加URL,用引号包围以处理特殊字符
curlCommand += ` '${completeUrl}'`;
// 添加headers
if (requestConfig.headers) {
Object.entries(requestConfig.headers).forEach(([key, value]) => {
// 跳过一些axios自动添加的headers
if (
!["content-length", "host", "connection"].includes(key.toLowerCase())
) {
curlCommand += ` \\\n -H '${key}: ${value}'`;
}
});
}
// 添加请求体数据(对于POST、PUT等请求)
if (
requestConfig.data &&
(method === "POST" || method === "PUT" || method === "PATCH")
) {
let dataString;
if (typeof requestConfig.data === "object") {
dataString = JSON.stringify(requestConfig.data);
} else {
dataString = requestConfig.data;
}
// 转义单引号
dataString = dataString.replace(/'/g, "'\"'\"'");
curlCommand += ` \\\n -d '${dataString}'`;
}
// 添加一些有用的curl选项
curlCommand += " \\\n --insecure \\\n --connect-timeout 30 \\\n --max-time 60 \\\n -v";
return curlCommand;
}
/**
* 设置API方法代理,自动将API对象上的方法代理到客户端实例上
*/
setupAPIMethodProxy() {
// 遍历API对象的所有属性
Object.getOwnPropertyNames(Object.getPrototypeOf(this.api)).forEach(key => {
// 跳过构造函数和非函数属性
if (key === 'constructor' || typeof this.api[key] !== 'function') {
return;
}
// 将API方法绑定到客户端实例上
this[key] = this.api[key].bind(this.api);
});
}
}
module.exports = { HikvisionClient };