Files
vue-desktop/src/services/ResourceService.ts
2025-09-24 16:43:10 +08:00

690 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { reactive, ref } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
/**
* 资源类型枚举
*/
export enum ResourceType {
LOCAL_STORAGE = 'localStorage',
NETWORK = 'network',
FILE_SYSTEM = 'fileSystem',
NOTIFICATION = 'notification',
CLIPBOARD = 'clipboard',
MEDIA = 'media',
GEOLOCATION = 'geolocation',
}
/**
* 权限级别枚举
*/
export enum PermissionLevel {
DENIED = 'denied',
GRANTED = 'granted',
PROMPT = 'prompt',
}
/**
* 权限请求结果
*/
export interface PermissionRequest {
id: string
appId: string
resourceType: ResourceType
description: string
requestedAt: Date
status: PermissionLevel
approvedAt?: Date
deniedAt?: Date
expiresAt?: Date
}
/**
* 资源访问配置
*/
export interface ResourceAccessConfig {
maxStorageSize: number // 本地存储最大容量(MB)
allowedDomains: string[] // 允许访问的网络域名
maxNetworkRequests: number // 每分钟最大网络请求数
allowFileAccess: boolean // 是否允许文件系统访问
allowNotifications: boolean // 是否允许通知
allowClipboard: boolean // 是否允许剪贴板访问
allowMedia: boolean // 是否允许摄像头麦克风
allowGeolocation: boolean // 是否允许地理位置
}
/**
* 网络请求记录
*/
export interface NetworkRequest {
id: string
appId: string
url: string
method: string
timestamp: Date
status?: number
responseSize?: number
}
/**
* 存储使用情况
*/
export interface StorageUsage {
appId: string
usedSpace: number // 已使用空间(MB)
maxSpace: number // 最大空间(MB)
lastAccessed: Date
}
/**
* 资源事件接口
*/
export interface ResourceEvents extends IEventMap {
onPermissionRequest: (request: PermissionRequest) => void
onPermissionGranted: (appId: string, resourceType: ResourceType) => void
onPermissionDenied: (appId: string, resourceType: ResourceType) => void
onResourceQuotaExceeded: (appId: string, resourceType: ResourceType) => void
onNetworkRequest: (request: NetworkRequest) => void
onStorageChange: (appId: string, usage: StorageUsage) => void
}
/**
* 资源管理服务类
*/
export class ResourceService {
private permissions = reactive(new Map<string, Map<ResourceType, PermissionRequest>>())
private networkRequests = reactive(new Map<string, NetworkRequest[]>())
private storageUsage = reactive(new Map<string, StorageUsage>())
private defaultConfig: ResourceAccessConfig
private eventBus: IEventBuilder<ResourceEvents>
constructor(eventBus: IEventBuilder<ResourceEvents>) {
this.eventBus = eventBus
// 默认资源访问配置
this.defaultConfig = {
maxStorageSize: 10, // 10MB
allowedDomains: [],
maxNetworkRequests: 60, // 每分钟60次
allowFileAccess: false,
allowNotifications: false,
allowClipboard: false,
allowMedia: false,
allowGeolocation: false,
}
this.initializeStorageMonitoring()
}
/**
* 请求资源权限
*/
async requestPermission(
appId: string,
resourceType: ResourceType,
description: string,
): Promise<PermissionLevel> {
const requestId = `${appId}-${resourceType}-${Date.now()}`
const request: PermissionRequest = {
id: requestId,
appId,
resourceType,
description,
requestedAt: new Date(),
status: PermissionLevel.PROMPT,
}
// 检查是否已有权限
const existingPermission = this.getPermission(appId, resourceType)
if (existingPermission) {
if (existingPermission.status === PermissionLevel.GRANTED) {
// 检查权限是否过期
if (!existingPermission.expiresAt || existingPermission.expiresAt > new Date()) {
return PermissionLevel.GRANTED
}
} else if (existingPermission.status === PermissionLevel.DENIED) {
return PermissionLevel.DENIED
}
}
// 触发权限请求事件UI层处理用户确认
this.eventBus.notifyEvent('onPermissionRequest', request)
// 根据资源类型的默认策略处理
return this.handlePermissionRequest(request)
}
/**
* 授予权限
*/
grantPermission(appId: string, resourceType: ResourceType, expiresIn?: number): boolean {
try {
const request = this.getPermission(appId, resourceType)
if (!request) return false
request.status = PermissionLevel.GRANTED
request.approvedAt = new Date()
if (expiresIn) {
request.expiresAt = new Date(Date.now() + expiresIn)
}
this.setPermission(appId, resourceType, request)
this.eventBus.notifyEvent('onPermissionGranted', appId, resourceType)
return true
} catch (error) {
console.error('授予权限失败:', error)
return false
}
}
/**
* 拒绝权限
*/
denyPermission(appId: string, resourceType: ResourceType): boolean {
try {
const request = this.getPermission(appId, resourceType)
if (!request) return false
request.status = PermissionLevel.DENIED
request.deniedAt = new Date()
this.setPermission(appId, resourceType, request)
this.eventBus.notifyEvent('onPermissionDenied', appId, resourceType)
return true
} catch (error) {
console.error('拒绝权限失败:', error)
return false
}
}
/**
* 检查应用是否有指定资源权限
*/
hasPermission(appId: string, resourceType: ResourceType): boolean {
const permission = this.getPermission(appId, resourceType)
if (!permission || permission.status !== PermissionLevel.GRANTED) {
return false
}
// 检查权限是否过期
if (permission.expiresAt && permission.expiresAt <= new Date()) {
permission.status = PermissionLevel.DENIED
this.setPermission(appId, resourceType, permission)
return false
}
return true
}
/**
* 本地存储操作
*/
async setStorage(appId: string, key: string, value: any): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
const permission = await this.requestPermission(
appId,
ResourceType.LOCAL_STORAGE,
'应用需要访问本地存储来保存数据',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
const storageKey = `app-${appId}-${key}`
const serializedValue = JSON.stringify(value)
// 检查存储配额
const usage = this.getStorageUsage(appId)
const valueSize = new Blob([serializedValue]).size / (1024 * 1024) // MB
if (usage.usedSpace + valueSize > usage.maxSpace) {
this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.LOCAL_STORAGE)
return false
}
localStorage.setItem(storageKey, serializedValue)
// 更新存储使用情况
this.updateStorageUsage(appId)
return true
} catch (error) {
console.error('存储数据失败:', error)
return false
}
}
/**
* 获取本地存储数据
*/
async getStorage(appId: string, key: string): Promise<any> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return null
}
try {
const storageKey = `app-${appId}-${key}`
const value = localStorage.getItem(storageKey)
if (value === null) {
return null
}
// 更新最后访问时间
this.updateStorageUsage(appId)
return JSON.parse(value)
} catch (error) {
console.error('获取存储数据失败:', error)
return null
}
}
/**
* 删除本地存储数据
*/
async removeStorage(appId: string, key: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return false
}
try {
const storageKey = `app-${appId}-${key}`
localStorage.removeItem(storageKey)
// 更新存储使用情况
this.updateStorageUsage(appId)
return true
} catch (error) {
console.error('删除存储数据失败:', error)
return false
}
}
/**
* 清空应用存储
*/
async clearStorage(appId: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return false
}
try {
const prefix = `app-${appId}-`
const keysToRemove: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(prefix)) {
keysToRemove.push(key)
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key))
// 重置存储使用情况
this.resetStorageUsage(appId)
return true
} catch (error) {
console.error('清空存储失败:', error)
return false
}
}
/**
* 网络请求
*/
async makeNetworkRequest(
appId: string,
url: string,
options: RequestInit = {},
): Promise<Response | null> {
if (!this.hasPermission(appId, ResourceType.NETWORK)) {
const permission = await this.requestPermission(
appId,
ResourceType.NETWORK,
`应用需要访问网络来请求数据: ${url}`,
)
if (permission !== PermissionLevel.GRANTED) {
return null
}
}
// 检查域名白名单
try {
const urlObj = new URL(url)
const config = this.getAppResourceConfig(appId)
if (
config.allowedDomains.length > 0 &&
!config.allowedDomains.some((domain) => urlObj.hostname.endsWith(domain))
) {
console.warn(`域名 ${urlObj.hostname} 不在白名单中`)
return null
}
// 检查请求频率限制
if (!this.checkNetworkRateLimit(appId)) {
this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.NETWORK)
return null
}
// 记录网络请求
const requestRecord: NetworkRequest = {
id: `${appId}-${Date.now()}`,
appId,
url,
method: options.method || 'GET',
timestamp: new Date(),
}
const response = await fetch(url, options)
// 更新请求记录
requestRecord.status = response.status
requestRecord.responseSize = parseInt(response.headers.get('content-length') || '0')
this.recordNetworkRequest(requestRecord)
this.eventBus.notifyEvent('onNetworkRequest', requestRecord)
return response
} catch (error) {
console.error('网络请求失败:', error)
return null
}
}
/**
* 显示通知
*/
async showNotification(
appId: string,
title: string,
options?: NotificationOptions,
): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.NOTIFICATION)) {
const permission = await this.requestPermission(
appId,
ResourceType.NOTIFICATION,
'应用需要显示通知来提醒您重要信息',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
if ('Notification' in window) {
// 请求浏览器通知权限
if (Notification.permission === 'default') {
await Notification.requestPermission()
}
if (Notification.permission === 'granted') {
new Notification(`[${appId}] ${title}`, options)
return true
}
}
return false
} catch (error) {
console.error('显示通知失败:', error)
return false
}
}
/**
* 访问剪贴板
*/
async getClipboard(appId: string): Promise<string | null> {
if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) {
const permission = await this.requestPermission(
appId,
ResourceType.CLIPBOARD,
'应用需要访问剪贴板来读取您复制的内容',
)
if (permission !== PermissionLevel.GRANTED) {
return null
}
}
try {
if (navigator.clipboard && navigator.clipboard.readText) {
return await navigator.clipboard.readText()
}
return null
} catch (error) {
console.error('读取剪贴板失败:', error)
return null
}
}
/**
* 写入剪贴板
*/
async setClipboard(appId: string, text: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) {
const permission = await this.requestPermission(
appId,
ResourceType.CLIPBOARD,
'应用需要访问剪贴板来复制内容',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
return true
}
return false
} catch (error) {
console.error('写入剪贴板失败:', error)
return false
}
}
/**
* 获取应用权限列表
*/
getAppPermissions(appId: string): PermissionRequest[] {
const appPermissions = this.permissions.get(appId)
return appPermissions ? Array.from(appPermissions.values()) : []
}
/**
* 获取所有网络请求记录
*/
getNetworkRequests(appId: string): NetworkRequest[] {
return this.networkRequests.get(appId) || []
}
/**
* 获取存储使用情况
*/
getStorageUsage(appId: string): StorageUsage {
let usage = this.storageUsage.get(appId)
if (!usage) {
usage = {
appId,
usedSpace: 0,
maxSpace: this.defaultConfig.maxStorageSize,
lastAccessed: new Date(),
}
this.storageUsage.set(appId, usage)
}
return usage
}
/**
* 获取应用资源配置
*/
getAppResourceConfig(appId: string): ResourceAccessConfig {
// 这里可以从数据库或配置文件加载应用特定配置
// 目前返回默认配置
return { ...this.defaultConfig }
}
/**
* 撤销应用所有权限
*/
revokeAllPermissions(appId: string): boolean {
try {
this.permissions.delete(appId)
this.networkRequests.delete(appId)
this.clearStorage(appId)
return true
} catch (error) {
console.error('撤销权限失败:', error)
return false
}
}
// 私有方法
/**
* 处理权限请求
*/
private async handlePermissionRequest(request: PermissionRequest): Promise<PermissionLevel> {
// 对于本地存储,默认授权
if (request.resourceType === ResourceType.LOCAL_STORAGE) {
this.grantPermission(request.appId, request.resourceType)
return PermissionLevel.GRANTED
}
// 其他资源需要用户确认,这里模拟用户同意
// 实际实现中,这里应该显示权限确认对话框
return new Promise((resolve) => {
setTimeout(() => {
// 模拟用户操作
const userResponse = Math.random() > 0.3 // 70%的概率同意
if (userResponse) {
this.grantPermission(request.appId, request.resourceType, 24 * 60 * 60 * 1000) // 24小时有效
resolve(PermissionLevel.GRANTED)
} else {
this.denyPermission(request.appId, request.resourceType)
resolve(PermissionLevel.DENIED)
}
}, 1000)
})
}
/**
* 获取权限记录
*/
private getPermission(appId: string, resourceType: ResourceType): PermissionRequest | undefined {
const appPermissions = this.permissions.get(appId)
return appPermissions?.get(resourceType)
}
/**
* 设置权限记录
*/
private setPermission(
appId: string,
resourceType: ResourceType,
request: PermissionRequest,
): void {
if (!this.permissions.has(appId)) {
this.permissions.set(appId, new Map())
}
this.permissions.get(appId)!.set(resourceType, request)
}
/**
* 检查网络请求频率限制
*/
private checkNetworkRateLimit(appId: string): boolean {
const requests = this.networkRequests.get(appId) || []
const now = new Date()
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000)
const recentRequests = requests.filter((req) => req.timestamp > oneMinuteAgo)
const config = this.getAppResourceConfig(appId)
return recentRequests.length < config.maxNetworkRequests
}
/**
* 记录网络请求
*/
private recordNetworkRequest(request: NetworkRequest): void {
if (!this.networkRequests.has(request.appId)) {
this.networkRequests.set(request.appId, [])
}
const requests = this.networkRequests.get(request.appId)!
requests.push(request)
// 保留最近1000条记录
if (requests.length > 1000) {
requests.splice(0, requests.length - 1000)
}
}
/**
* 更新存储使用情况
*/
private updateStorageUsage(appId: string): void {
const usage = this.getStorageUsage(appId)
usage.lastAccessed = new Date()
// 计算实际使用空间
let usedSpace = 0
const prefix = `app-${appId}-`
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(prefix)) {
const value = localStorage.getItem(key)
if (value) {
usedSpace += new Blob([value]).size
}
}
}
usage.usedSpace = usedSpace / (1024 * 1024) // 转换为MB
this.eventBus.notifyEvent('onStorageChange', appId, usage)
}
/**
* 重置存储使用情况
*/
private resetStorageUsage(appId: string): void {
const usage = this.getStorageUsage(appId)
usage.usedSpace = 0
usage.lastAccessed = new Date()
this.eventBus.notifyEvent('onStorageChange', appId, usage)
}
/**
* 初始化存储监控
*/
private initializeStorageMonitoring(): void {
// 监听存储变化事件
window.addEventListener('storage', (e) => {
if (e.key && e.key.startsWith('app-')) {
const parts = e.key.split('-')
if (parts.length >= 2) {
const appId = parts[1]
this.updateStorageUsage(appId)
}
}
})
}
}