690 lines
18 KiB
TypeScript
690 lines
18 KiB
TypeScript
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)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|