2e77792042
* docs: enhance LoggerService documentation and usage guidelines - Added details about `initWindowSource` method, emphasizing its return of the LoggerService instance for method chaining. - Introduced a section on using LoggerService within `worker` threads, highlighting the need to call `initWindowSource` first. - Updated both English and Chinese documentation files to reflect these changes, ensuring clarity in usage instructions for developers. * docs: update LoggerService documentation and improve environment checks - Enhanced documentation for using LoggerService in worker threads, clarifying logging support limitations in main and renderer processes. - Added environment checks for development and production modes directly in the LoggerService. - Removed the unused env utility file to streamline the codebase. * refactor(ShikiStreamService): update highlighter management and improve test assertions - Modified the highlighter management in ShikiStreamService to clear the reference instead of disposing it directly, as it is now managed by AsyncInitializer. - Enhanced unit tests to verify the initialization of worker and main highlighters, ensuring that either one is active but not both, and updated assertions related to highlighter disposal.
237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
import { loggerService } from '@logger'
|
|
import { Model, Provider } from '@renderer/types'
|
|
import { ModalFuncProps } from 'antd'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
const logger = loggerService.withContext('Utils')
|
|
|
|
/**
|
|
* 异步执行一个函数。
|
|
* @param {() => void} fn 要执行的函数
|
|
* @returns {Promise<void>} 执行结果
|
|
*/
|
|
export const runAsyncFunction = async (fn: () => void): Promise<void> => {
|
|
await fn()
|
|
}
|
|
|
|
/**
|
|
* 创建一个延迟的 Promise,在指定秒数后解析。
|
|
* @param {number} seconds 延迟的秒数
|
|
* @returns {Promise<any>} 在指定秒数后解析的 Promise
|
|
*/
|
|
export const delay = (seconds: number): Promise<any> => {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
resolve(true)
|
|
}, seconds * 1000)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 等待异步函数返回 true。
|
|
* @param {() => Promise<any>} fn 要等待的异步函数
|
|
* @param {number} [interval=200] 检查间隔时间(毫秒)
|
|
* @param {number} [stopTimeout=60000] 停止等待的超时时间(毫秒)
|
|
* @returns {Promise<any>} 异步函数返回 true 后的 Promise
|
|
*/
|
|
export const waitAsyncFunction = (
|
|
fn: () => Promise<any>,
|
|
interval: number = 200,
|
|
stopTimeout: number = 60000
|
|
): Promise<any> => {
|
|
let timeout = false
|
|
const timer = setTimeout(() => (timeout = true), stopTimeout)
|
|
|
|
return (async function check(): Promise<any> {
|
|
if (await fn()) {
|
|
clearTimeout(timer)
|
|
return Promise.resolve()
|
|
} else if (!timeout) {
|
|
return delay(interval / 1000).then(check)
|
|
} else {
|
|
return Promise.resolve()
|
|
}
|
|
})()
|
|
}
|
|
|
|
export const uuid = () => uuidv4()
|
|
|
|
export function isFreeModel(model: Model) {
|
|
return (model.id + model.name).toLocaleLowerCase().includes('free')
|
|
}
|
|
|
|
/**
|
|
* 从错误对象中提取错误信息。
|
|
* @param {any} error 错误对象或字符串
|
|
* @returns {string} 提取的错误信息,如果没有则返回空字符串
|
|
*/
|
|
export function getErrorMessage(error: any): string {
|
|
if (!error) {
|
|
return ''
|
|
}
|
|
|
|
if (typeof error === 'string') {
|
|
return error
|
|
}
|
|
|
|
if (error?.error) {
|
|
return getErrorMessage(error.error)
|
|
}
|
|
|
|
if (error?.message) {
|
|
return error.message
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* 移除字符串中的引号。
|
|
* @param {string} str 输入字符串
|
|
* @returns {string} 新字符串
|
|
*/
|
|
export function removeQuotes(str: string): string {
|
|
return str.replace(/['"]+/g, '')
|
|
}
|
|
|
|
/**
|
|
* 移除字符串中的特殊字符。
|
|
* @param {string} str 输入字符串
|
|
* @returns {string} 新字符串
|
|
*/
|
|
export function removeSpecialCharacters(str: string): string {
|
|
// First remove newlines and quotes, then remove other special characters
|
|
return str.replace(/[\n"]/g, '').replace(/[\p{M}\p{P}]/gu, '')
|
|
}
|
|
|
|
/**
|
|
* 检查 URL 是否是有效的代理 URL。
|
|
* @param {string} url 代理 URL
|
|
* @returns {boolean} 是否有效
|
|
*/
|
|
export const isValidProxyUrl = (url: string): boolean => {
|
|
return url.includes('://')
|
|
}
|
|
|
|
/**
|
|
* 动态加载 JavaScript 脚本。
|
|
* @param url 脚本的 URL 地址
|
|
* @returns Promise<void> 脚本加载成功或失败的 Promise
|
|
*/
|
|
export function loadScript(url: string) {
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement('script')
|
|
script.type = 'text/javascript'
|
|
script.src = url
|
|
|
|
script.onload = resolve
|
|
script.onerror = reject
|
|
|
|
document.head.appendChild(script)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 检查 URL 是否包含路径部分。
|
|
* @param {string} url 输入 URL 字符串
|
|
* @returns {boolean} 如果 URL 包含路径则返回 true,否则返回 false
|
|
*/
|
|
export function hasPath(url: string): boolean {
|
|
try {
|
|
const parsedUrl = new URL(url)
|
|
return parsedUrl.pathname !== '/' && parsedUrl.pathname !== ''
|
|
} catch (error) {
|
|
logger.error('Invalid URL:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 比较两个版本号字符串。
|
|
* @param {string} v1 第一个版本号
|
|
* @param {string} v2 第二个版本号
|
|
* @returns {number} 比较结果,1 表示 v1 大于 v2,-1 表示 v1 小于 v2,0 表示相等
|
|
*/
|
|
export const compareVersions = (v1: string, v2: string): number => {
|
|
const v1Parts = v1.split('.').map(Number)
|
|
const v2Parts = v2.split('.').map(Number)
|
|
|
|
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
const v1Part = v1Parts[i] || 0
|
|
const v2Part = v2Parts[i] || 0
|
|
if (v1Part > v2Part) return 1
|
|
if (v1Part < v2Part) return -1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* 显示确认模态框。
|
|
* @param {ModalFuncProps} params 模态框参数
|
|
* @returns {Promise<boolean>} 用户确认返回 true,取消返回 false
|
|
*/
|
|
export function modalConfirm(params: ModalFuncProps): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
window.modal.confirm({
|
|
centered: true,
|
|
...params,
|
|
onOk: () => resolve(true),
|
|
onCancel: () => resolve(false)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 检查对象是否包含特定键。
|
|
* @param {any} obj 输入对象
|
|
* @param {string} key 要检查的键
|
|
* @returns {boolean} 包含该键则返回 true,否则返回 false
|
|
*/
|
|
export function hasObjectKey(obj: any, key: string): boolean {
|
|
if (typeof obj !== 'object' || obj === null) {
|
|
return false
|
|
}
|
|
|
|
return Object.keys(obj).includes(key)
|
|
}
|
|
|
|
/**
|
|
* 从npm readme中提取 npx mcp config
|
|
* @param {string} readme readme字符串
|
|
* @returns {Record<string, any> | null} mcp config sample
|
|
*/
|
|
export function getMcpConfigSampleFromReadme(readme: string): Record<string, any> | null {
|
|
if (readme) {
|
|
try {
|
|
const regex = /"mcpServers"\s*:\s*({(?:[^{}]*|{(?:[^{}]*|{[^{}]*})*})*})/g
|
|
for (const match of readme.matchAll(regex)) {
|
|
let orgSample = JSON.parse(match[1])
|
|
orgSample = orgSample[Object.keys(orgSample)[0] ?? '']
|
|
if (orgSample.command === 'npx') {
|
|
return orgSample
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.error('getMcpConfigSampleFromReadme', e)
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 判断是否为 OpenAI 兼容的提供商
|
|
* @param {Provider} provider 提供商对象
|
|
* @returns {boolean} 是否为 OpenAI 兼容提供商
|
|
*/
|
|
export function isOpenAIProvider(provider: Provider): boolean {
|
|
return !['anthropic', 'gemini', 'vertexai'].includes(provider.type)
|
|
}
|
|
|
|
export * from './api'
|
|
export * from './file'
|
|
export * from './image'
|
|
export * from './json'
|
|
export * from './naming'
|
|
export * from './sort'
|
|
export * from './style'
|