common/utils.ts (116 lines of code) (raw):
import { getUserAgent } from "universal-user-agent";
import { createYunxiaoError } from "./errors.js";
import { VERSION } from "./version.js";
const DEFAULT_YUNXIAO_API_BASE_URL = "https://openapi-rdc.aliyuncs.com";
/**
* Get the Yunxiao API base URL from environment variables or use the default
* @returns The Yunxiao API base URL
*/
export function getYunxiaoApiBaseUrl(): string {
return process.env.YUNXIAO_API_BASE_URL || DEFAULT_YUNXIAO_API_BASE_URL;
}
type RequestOptions = {
method?: string;
body?: unknown;
headers?: Record<string, string>;
}
export function debug(message: string, data?: unknown): void {
if (data !== undefined) {
console.error(`[DEBUG] ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
} else {
console.error(`[DEBUG] ${message}`);
}
}
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
return response.text();
}
export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string {
// Handle baseUrl that doesn't have protocol
const isAbsolute = baseUrl.startsWith("http://") || baseUrl.startsWith("https://");
const fullBaseUrl = isAbsolute ? baseUrl : `${getYunxiaoApiBaseUrl()}${baseUrl.startsWith('/') ? baseUrl : `/${baseUrl}`}`;
try {
const url = new URL(fullBaseUrl);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value.toString());
}
});
const result = url.toString();
console.error(`[DEBUG] Final URL: ${result}`);
// If we started with a relative URL, return just the path portion
if (!baseUrl.startsWith('http')) {
// Extract the path and query string from the full URL
const urlObj = new URL(result);
return urlObj.pathname + urlObj.search;
}
return result;
} catch (error) {
console.error(`[ERROR] Failed to build URL: ${error}`);
// Fallback: manually append query parameters
let urlWithParams = baseUrl;
const queryParts: string[] = [];
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
}
});
if (queryParts.length > 0) {
urlWithParams += (urlWithParams.includes('?') ? '&' : '?') + queryParts.join('&');
}
console.error(`[DEBUG] Fallback URL: ${urlWithParams}`);
return urlWithParams;
}
}
const USER_AGENT = `modelcontextprotocol/servers/alibabacloud-devops-mcp-server/v${VERSION} ${getUserAgent()}`;
export async function yunxiaoRequest(
urlPath: string,
options: RequestOptions = {},
): Promise<unknown> {
// Check if the URL is already a full URL or a path
const isAbsolute = urlPath.startsWith("http://") || urlPath.startsWith("https://");
let url = isAbsolute ? urlPath : `${getYunxiaoApiBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
const requestHeaders: Record<string, string> = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
...options.headers,
};
if (process.env.YUNXIAO_ACCESS_TOKEN) {
requestHeaders["x-yunxiao-token"] = process.env.YUNXIAO_ACCESS_TOKEN;
}
debug(`Request: ${options.method} ${url}`);
debug(`Headers:`, requestHeaders);
const response = await fetch(url, {
method : options.method || "GET",
headers: requestHeaders,
body: options.body ? JSON.stringify(options.body) : undefined,
} as RequestInit);
const responseBody = await parseResponseBody(response);
debug(`Response Body:`, responseBody)
if (!response.ok) {
throw createYunxiaoError(response.status, responseBody);
}
return responseBody;
}
export function pathEscape(filePath: string): string {
// 先使用encodeURIComponent进行编码
let encoded = encodeURIComponent(filePath);
// 将编码后的%2F(/的编码)替换回/
encoded = encoded.replace(/%2F/gi, "/");
return encoded;
}
/**
* Handle repository ID encoding
* @param repositoryId Repository ID which may contain unencoded slash
* @returns Properly encoded repository ID
*/
export function handleRepositoryIdEncoding(repositoryId: string): string {
let encodedRepoId = repositoryId;
// Automatically handle unencoded slashes in repositoryId
if (repositoryId.includes("/")) {
// Found unencoded slash, automatically URL encode it
const parts = repositoryId.split("/", 2);
if (parts.length === 2) {
const encodedRepoName = encodeURIComponent(parts[1]);
// Remove + signs from encoding (spaces are encoded as +, but we need %20)
const formattedEncodedName = encodedRepoName.replace(/\+/g, "%20");
encodedRepoId = `${parts[0]}%2F${formattedEncodedName}`;
}
}
return encodedRepoId;
}
/**
* Converts a floating point number to an integer string (removes decimal point and decimal part)
* Used primarily for handling numeric IDs that might come as floats from JSON parsing
* @param value Value to convert
* @returns Integer string representation
*/
export function floatToIntString(value: any): string {
// 如果传入的是字符串,先尝试转为浮点数
if (typeof value === 'string') {
const floatValue = parseFloat(value);
if (!isNaN(floatValue)) {
value = floatValue;
} else {
return value; // 如果转换失败,返回原字符串
}
}
// 处理浮点数
if (typeof value === 'number') {
const intValue = Math.floor(value + 0.5); // 四舍五入转整数
return intValue.toString();
}
// 处理其他情况,直接转字符串
return String(value);
}
/**
* 将各种时间格式转换为毫秒时间戳
* 支持:
* - 已有时间戳(number)直接返回
* - Date对象转换为时间戳
* - ISO格式日期字符串 (如: '2023-01-01T00:00:00Z')
* - 日期字符串 (如: '2023-01-01')
*
* @param time 时间输入
* @returns 毫秒时间戳
*/
export function convertToTimestamp(time: number | string | Date): number {
if (typeof time === 'number') {
// 如果已经是数字,假设已是时间戳
return time;
} else if (time instanceof Date) {
// 如果是Date对象,转换为时间戳
return time.getTime();
} else if (typeof time === 'string') {
// 尝试解析日期字符串
const date = new Date(time);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
// 无法转换时返回原值(如果是数字)或当前时间戳
return typeof time === 'number' ? time : Date.now();
}
/**
* Get start of today timestamp
* @returns Timestamp for start of the current day (00:00:00)
*/
export function getStartOfTodayTimestamp(): number {
const now = new Date();
// Reset time to start of day (00:00:00.000)
now.setHours(0, 0, 0, 0);
return now.getTime();
}
/**
* Get end of today timestamp
* @returns Timestamp for end of the current day (23:59:59.999)
*/
export function getEndOfTodayTimestamp(): number {
const now = new Date();
// Set time to end of day (23:59:59.999)
now.setHours(23, 59, 59, 999);
return now.getTime();
}
/**
* Get timestamp for start of a specific day
* @param date Date object or date string
* @returns Timestamp for start of the specified day
*/
export function getStartOfDayTimestamp(date: Date | string): number {
const targetDate = typeof date === 'string' ? new Date(date) : new Date(date);
targetDate.setHours(0, 0, 0, 0);
return targetDate.getTime();
}
/**
* Get timestamp for end of a specific day
* @param date Date object or date string
* @returns Timestamp for end of the specified day
*/
export function getEndOfDayTimestamp(date: Date | string): number {
const targetDate = typeof date === 'string' ? new Date(date) : new Date(date);
targetDate.setHours(23, 59, 59, 999);
return targetDate.getTime();
}
/**
* Get timestamp for start of current week
* @param startOnMonday Whether week should start on Monday (true) or Sunday (false)
* @returns Timestamp for start of the current week
*/
export function getStartOfWeekTimestamp(startOnMonday: boolean = true): number {
const now = new Date();
const dayOfWeek = now.getDay(); // 0 is Sunday, 1 is Monday, etc.
const diff = startOnMonday ?
(dayOfWeek === 0 ? 6 : dayOfWeek - 1) : // If startOnMonday, set Sunday as day 7
dayOfWeek;
// Set to beginning of the week
now.setDate(now.getDate() - diff);
now.setHours(0, 0, 0, 0);
return now.getTime();
}
/**
* Get timestamp for end of current week
* @param startOnMonday Whether week should start on Monday (true) or Sunday (false)
* @returns Timestamp for end of the current week
*/
export function getEndOfWeekTimestamp(startOnMonday: boolean = true): number {
const now = new Date();
const dayOfWeek = now.getDay(); // 0 is Sunday, 1 is Monday, etc.
const diff = startOnMonday ?
(dayOfWeek === 0 ? 0 : 7 - dayOfWeek) : // If startOnMonday, set Sunday as day 7
(6 - dayOfWeek);
// Set to end of the week
now.setDate(now.getDate() + diff);
now.setHours(23, 59, 59, 999);
return now.getTime();
}
/**
* Get timestamp for start of current month
* @returns Timestamp for start of the current month
*/
export function getStartOfMonthTimestamp(): number {
const now = new Date();
now.setDate(1); // First day of current month
now.setHours(0, 0, 0, 0);
return now.getTime();
}
/**
* Get timestamp for end of current month
* @returns Timestamp for end of the current month
*/
export function getEndOfMonthTimestamp(): number {
const now = new Date();
now.setMonth(now.getMonth() + 1); // Move to next month
now.setDate(0); // Last day of previous month (i.e., current month)
now.setHours(23, 59, 59, 999);
return now.getTime();
}
/**
* Analyzes natural language date reference and returns corresponding timestamp range
* @param dateReference Natural language date reference (e.g., "today", "this week", "last month")
* @returns Object containing start and end timestamps
*/
export function parseDateReference(dateReference?: string): { startTime: number, endTime: number } {
if (!dateReference) {
// Default to all time
return {
startTime: 0,
endTime: Date.now()
};
}
const normalizedRef = dateReference.trim().toLowerCase();
// Today/yesterday
if (normalizedRef === 'today' || normalizedRef === '今天') {
return {
startTime: getStartOfTodayTimestamp(),
endTime: getEndOfTodayTimestamp()
};
}
if (normalizedRef === 'yesterday' || normalizedRef === '昨天') {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return {
startTime: getStartOfDayTimestamp(yesterday),
endTime: getEndOfDayTimestamp(yesterday)
};
}
// This week/last week
if (normalizedRef === 'this week' || normalizedRef === '本周' ||
normalizedRef === 'current week' || normalizedRef === '这周' ||
normalizedRef === '这个星期') {
return {
startTime: getStartOfWeekTimestamp(),
endTime: getEndOfWeekTimestamp()
};
}
if (normalizedRef === 'last week' || normalizedRef === '上周' ||
normalizedRef === '上個星期' || normalizedRef === '上个星期') {
const lastWeekStart = new Date(getStartOfWeekTimestamp());
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
const lastWeekEnd = new Date(getEndOfWeekTimestamp());
lastWeekEnd.setDate(lastWeekEnd.getDate() - 7);
return {
startTime: lastWeekStart.getTime(),
endTime: lastWeekEnd.getTime()
};
}
// This month/last month
if (normalizedRef === 'this month' || normalizedRef === '本月' ||
normalizedRef === 'current month' || normalizedRef === '这个月') {
return {
startTime: getStartOfMonthTimestamp(),
endTime: getEndOfMonthTimestamp()
};
}
if (normalizedRef === 'last month' || normalizedRef === '上月' ||
normalizedRef === '上个月') {
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
// Start of last month
const startOfLastMonth = new Date(lastMonth.getFullYear(), lastMonth.getMonth(), 1);
startOfLastMonth.setHours(0, 0, 0, 0);
// End of last month
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
endOfLastMonth.setHours(23, 59, 59, 999);
return {
startTime: startOfLastMonth.getTime(),
endTime: endOfLastMonth.getTime()
};
}
// Default to all time
return {
startTime: 0,
endTime: Date.now()
};
}