/**
* 禅道 API 客户端
*/
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { ZenTaoConfig, ZenTaoError, IApiResponse } from './types';
import { Logger, LogLevel } from './utils/logger';
export class ZenTaoClient {
private config: ZenTaoConfig;
private http: AxiosInstance;
private logger: Logger;
constructor(config: ZenTaoConfig, logger?: Logger) {
this.config = {
timeout: 30000,
retry: 3,
retryDelay: 1000,
...config,
};
this.logger = logger || new Logger(LogLevel.INFO);
this.http = axios.create({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
this.http.interceptors.request.use(
(config) => {
// 添加认证头
config.headers = config.headers || {};
config.headers['Cookie'] = `zentaosid=${this.config.token.split(':')[1] || this.config.token}`;
return config;
},
(error) => {
this.logger.error('请求配置错误', error);
return Promise.reject(error);
}
);
// 响应拦截器
this.http.interceptors.response.use(
(response: AxiosResponse<IApiResponse>) => {
this.logger.debug(`API 请求成功: ${response.config.url}`);
return response;
},
async (error) => {
const originalRequest = error.config;
this.logger.error(`API 请求失败: ${originalRequest?.url}`, error.message);
// 重试逻辑
if (this.shouldRetry(error) && originalRequest && this.config.retry! > 0) {
originalRequest._retryCount = originalRequest._retryCount || 0;
if (originalRequest._retryCount < this.config.retry!) {
originalRequest._retryCount++;
this.logger.info(`重试请求 (${originalRequest._retryCount}/${this.config.retry}): ${originalRequest.url}`);
await this.delay(this.config.retryDelay || 1000);
return this.http(originalRequest);
}
}
return Promise.reject(this.transformError(error));
}
);
}
/**
* GET 请求
*/
async get<T = any>(endpoint: string, params?: any): Promise<T> {
try {
const response = await this.http.get(endpoint, { params });
return this.handleResponse<T>(response);
} catch (error) {
this.logger.error(`GET 请求失败: ${endpoint}`, error);
throw error;
}
}
/**
* POST 请求
*/
async post<T = any>(endpoint: string, data?: any): Promise<T> {
try {
const response = await this.http.post(endpoint, data);
return this.handleResponse<T>(response);
} catch (error) {
this.logger.error(`POST 请求失败: ${endpoint}`, error);
throw error;
}
}
/**
* PUT 请求
*/
async put<T = any>(endpoint: string, data?: any): Promise<T> {
try {
const response = await this.http.put(endpoint, data);
return this.handleResponse<T>(response);
} catch (error) {
this.logger.error(`PUT 请求失败: ${endpoint}`, error);
throw error;
}
}
/**
* DELETE 请求
*/
async delete<T = any>(endpoint: string): Promise<T> {
try {
const response = await this.http.delete(endpoint);
return this.handleResponse<T>(response);
} catch (error) {
this.logger.error(`DELETE 请求失败: ${endpoint}`, error);
throw error;
}
}
/**
* 验证连接
*/
async verifyConnection(): Promise<boolean> {
try {
// 尝试获取一个项目来验证认证
await this.get('/api.php/v1/projects', { limit: 1 });
this.logger.info('连接验证成功');
return true;
} catch (error) {
this.logger.error('连接验证失败', error);
return false;
}
}
/**
* 获取禅道版本
*/
async getVersion(): Promise<string> {
try {
const response = await this.get('/api.php/v1/system');
// 提取版本信息
return response.version || '未知';
} catch (error) {
this.logger.error('获取版本信息失败', error);
return '未知';
}
}
/**
* 处理响应数据
*/
private handleResponse<T>(response: AxiosResponse<IApiResponse<T>>): T {
const data = response.data;
if (data.status !== 200 && data.status !== 0) {
const error = new ZenTaoError(data.message || 'API 请求失败') as ZenTaoError;
error.code = data.status;
error.status = response.status;
error.details = data;
throw error;
}
return data.data;
}
/**
* 转换错误
*/
private transformError(error: any): ZenTaoError {
const zenTaoError = new ZenTaoError(error.message || '未知错误') as ZenTaoError;
zenTaoError.code = error.code || -1;
zenTaoError.status = error.response?.status || 500;
zenTaoError.details = error.response?.data;
if (error.response?.status === 401) {
zenTaoError.hint = '认证失败,请检查 token 是否正确';
} else if (error.response?.status === 403) {
zenTaoError.hint = '权限不足,请检查用户权限';
} else if (error.code === 'ECONNABORTED') {
zenTaoError.hint = '请求超时,请检查网络连接';
}
return zenTaoError;
}
/**
* 检查是否应该重试
*/
private shouldRetry(error: any): boolean {
// 网络错误或服务器错误
return (
error.code === 'ECONNABORTED' ||
error.code === 'ETIMEDOUT' ||
(error.response && error.response.status >= 500)
);
}
/**
* 延迟函数
*/
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}