保存
This commit is contained in:
689
src/services/ResourceService.ts
Normal file
689
src/services/ResourceService.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user