245 lines
6.2 KiB
TypeScript
245 lines
6.2 KiB
TypeScript
/**
|
|
* 插件统计 API 客户端
|
|
* 用于与 Cloudflare Workers 统计服务交互
|
|
*/
|
|
|
|
// 配置统计服务 API 地址(所有用户共享的云端统计服务)
|
|
const STATS_API_BASE_URL = 'https://maibot-plugin-stats.maibot-webui.workers.dev'
|
|
|
|
export interface PluginStatsData {
|
|
plugin_id: string
|
|
likes: number
|
|
dislikes: number
|
|
downloads: number
|
|
rating: number
|
|
rating_count: number
|
|
recent_ratings?: Array<{
|
|
user_id: string
|
|
rating: number
|
|
comment?: string
|
|
created_at: string
|
|
}>
|
|
}
|
|
|
|
export interface StatsResponse {
|
|
success: boolean
|
|
error?: string
|
|
remaining?: number
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/**
|
|
* 获取插件统计数据
|
|
*/
|
|
export async function getPluginStats(pluginId: string): Promise<PluginStatsData | null> {
|
|
try {
|
|
const response = await fetch(`${STATS_API_BASE_URL}/stats/${pluginId}`)
|
|
|
|
if (!response.ok) {
|
|
console.error('Failed to fetch plugin stats:', response.statusText)
|
|
return null
|
|
}
|
|
|
|
return await response.json()
|
|
} catch (error) {
|
|
console.error('Error fetching plugin stats:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 点赞插件
|
|
*/
|
|
export async function likePlugin(pluginId: string, userId?: string): Promise<StatsResponse> {
|
|
try {
|
|
const finalUserId = userId || getUserId()
|
|
|
|
const response = await fetch(`${STATS_API_BASE_URL}/stats/like`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ plugin_id: pluginId, user_id: finalUserId }),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (response.status === 429) {
|
|
return { success: false, error: '操作过于频繁,请稍后再试' }
|
|
}
|
|
|
|
if (!response.ok) {
|
|
return { success: false, error: data.error || '点赞失败' }
|
|
}
|
|
|
|
return { success: true, ...data }
|
|
} catch (error) {
|
|
console.error('Error liking plugin:', error)
|
|
return { success: false, error: '网络错误' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 点踩插件
|
|
*/
|
|
export async function dislikePlugin(pluginId: string, userId?: string): Promise<StatsResponse> {
|
|
try {
|
|
const finalUserId = userId || getUserId()
|
|
|
|
const response = await fetch(`${STATS_API_BASE_URL}/stats/dislike`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ plugin_id: pluginId, user_id: finalUserId }),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (response.status === 429) {
|
|
return { success: false, error: '操作过于频繁,请稍后再试' }
|
|
}
|
|
|
|
if (!response.ok) {
|
|
return { success: false, error: data.error || '点踩失败' }
|
|
}
|
|
|
|
return { success: true, ...data }
|
|
} catch (error) {
|
|
console.error('Error disliking plugin:', error)
|
|
return { success: false, error: '网络错误' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 评分插件
|
|
*/
|
|
export async function ratePlugin(
|
|
pluginId: string,
|
|
rating: number,
|
|
comment?: string,
|
|
userId?: string
|
|
): Promise<StatsResponse> {
|
|
if (rating < 1 || rating > 5) {
|
|
return { success: false, error: '评分必须在 1-5 之间' }
|
|
}
|
|
|
|
try {
|
|
const finalUserId = userId || getUserId()
|
|
|
|
const response = await fetch(`${STATS_API_BASE_URL}/stats/rate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ plugin_id: pluginId, rating, comment, user_id: finalUserId }),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (response.status === 429) {
|
|
return { success: false, error: '每天最多评分 3 次' }
|
|
}
|
|
|
|
if (!response.ok) {
|
|
return { success: false, error: data.error || '评分失败' }
|
|
}
|
|
|
|
return { success: true, ...data }
|
|
} catch (error) {
|
|
console.error('Error rating plugin:', error)
|
|
return { success: false, error: '网络错误' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 记录插件下载
|
|
*/
|
|
export async function recordPluginDownload(pluginId: string): Promise<StatsResponse> {
|
|
try {
|
|
const response = await fetch(`${STATS_API_BASE_URL}/stats/download`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ plugin_id: pluginId }),
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (response.status === 429) {
|
|
// 下载统计被限流时静默失败,不影响用户体验
|
|
console.warn('Download recording rate limited')
|
|
return { success: true }
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.error('Failed to record download:', data.error)
|
|
return { success: false, error: data.error }
|
|
}
|
|
|
|
return { success: true, ...data }
|
|
} catch (error) {
|
|
console.error('Error recording download:', error)
|
|
return { success: false, error: '网络错误' }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成用户指纹(基于浏览器特征)
|
|
* 用于在未登录时识别用户,防止重复投票
|
|
*/
|
|
export function generateUserFingerprint(): string {
|
|
const nav = navigator as Navigator & { deviceMemory?: number }
|
|
const features = [
|
|
navigator.userAgent,
|
|
navigator.language,
|
|
navigator.languages?.join(',') || '',
|
|
navigator.platform,
|
|
navigator.hardwareConcurrency || 0,
|
|
screen.width,
|
|
screen.height,
|
|
screen.colorDepth,
|
|
screen.pixelDepth,
|
|
new Date().getTimezoneOffset(),
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
navigator.maxTouchPoints || 0,
|
|
nav.deviceMemory || 0,
|
|
].join('|')
|
|
|
|
// 简单哈希函数
|
|
let hash = 0
|
|
for (let i = 0; i < features.length; i++) {
|
|
const char = features.charCodeAt(i)
|
|
hash = ((hash << 5) - hash) + char
|
|
hash = hash & hash // Convert to 32bit integer
|
|
}
|
|
|
|
return `fp_${Math.abs(hash).toString(36)}`
|
|
}
|
|
|
|
/**
|
|
* 生成或获取用户 UUID
|
|
* 存储在 localStorage 中持久化
|
|
*/
|
|
export function getUserId(): string {
|
|
const STORAGE_KEY = 'maibot_user_id'
|
|
|
|
// 尝试从 localStorage 获取
|
|
let userId = localStorage.getItem(STORAGE_KEY)
|
|
|
|
if (!userId) {
|
|
// 生成新的 UUID
|
|
const fingerprint = generateUserFingerprint()
|
|
const timestamp = Date.now().toString(36)
|
|
const random = Math.random().toString(36).substring(2, 15)
|
|
|
|
userId = `${fingerprint}_${timestamp}_${random}`
|
|
|
|
// 存储到 localStorage
|
|
localStorage.setItem(STORAGE_KEY, userId)
|
|
}
|
|
|
|
return userId
|
|
}
|