保存
This commit is contained in:
1027
src/services/ApplicationLifecycleManager.ts
Normal file
1027
src/services/ApplicationLifecycleManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
1273
src/services/ApplicationSandboxEngine.ts
Normal file
1273
src/services/ApplicationSandboxEngine.ts
Normal file
File diff suppressed because it is too large
Load Diff
639
src/services/EventCommunicationService.ts
Normal file
639
src/services/EventCommunicationService.ts
Normal file
@@ -0,0 +1,639 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { IEventBuilder } from '@/events/IEventBuilder'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
/**
|
||||
* 消息类型枚举
|
||||
*/
|
||||
export enum MessageType {
|
||||
SYSTEM = 'system',
|
||||
APPLICATION = 'application',
|
||||
USER_INTERACTION = 'user_interaction',
|
||||
CROSS_APP = 'cross_app',
|
||||
BROADCAST = 'broadcast'
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息优先级枚举
|
||||
*/
|
||||
export enum MessagePriority {
|
||||
LOW = 0,
|
||||
NORMAL = 1,
|
||||
HIGH = 2,
|
||||
CRITICAL = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息状态枚举
|
||||
*/
|
||||
export enum MessageStatus {
|
||||
PENDING = 'pending',
|
||||
SENT = 'sent',
|
||||
DELIVERED = 'delivered',
|
||||
FAILED = 'failed',
|
||||
EXPIRED = 'expired'
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件消息接口
|
||||
*/
|
||||
export interface EventMessage {
|
||||
id: string
|
||||
type: MessageType
|
||||
priority: MessagePriority
|
||||
senderId: string
|
||||
receiverId?: string // undefined表示广播消息
|
||||
channel: string
|
||||
payload: any
|
||||
timestamp: Date
|
||||
expiresAt?: Date
|
||||
status: MessageStatus
|
||||
retryCount: number
|
||||
maxRetries: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件订阅者接口
|
||||
*/
|
||||
export interface EventSubscriber {
|
||||
id: string
|
||||
appId: string
|
||||
channel: string
|
||||
handler: (message: EventMessage) => void | Promise<void>
|
||||
filter?: (message: EventMessage) => boolean
|
||||
priority: MessagePriority
|
||||
createdAt: Date
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 通信通道接口
|
||||
*/
|
||||
export interface CommunicationChannel {
|
||||
name: string
|
||||
description: string
|
||||
restricted: boolean // 是否需要权限
|
||||
allowedApps: string[] // 允许访问的应用ID列表
|
||||
maxMessageSize: number // 最大消息大小(字节)
|
||||
messageRetention: number // 消息保留时间(毫秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件统计信息
|
||||
*/
|
||||
export interface EventStatistics {
|
||||
totalMessagesSent: number
|
||||
totalMessagesReceived: number
|
||||
totalBroadcasts: number
|
||||
failedMessages: number
|
||||
activeSubscribers: number
|
||||
channelUsage: Map<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件通信服务类
|
||||
*/
|
||||
export class EventCommunicationService {
|
||||
private subscribers = reactive(new Map<string, EventSubscriber>())
|
||||
private messageQueue = reactive(new Map<string, EventMessage[]>()) // 按应用分组的消息队列
|
||||
private messageHistory = reactive(new Map<string, EventMessage[]>()) // 消息历史记录
|
||||
private channels = reactive(new Map<string, CommunicationChannel>())
|
||||
private statistics = reactive<EventStatistics>({
|
||||
totalMessagesSent: 0,
|
||||
totalMessagesReceived: 0,
|
||||
totalBroadcasts: 0,
|
||||
failedMessages: 0,
|
||||
activeSubscribers: 0,
|
||||
channelUsage: new Map()
|
||||
})
|
||||
|
||||
private processingInterval: number | null = null
|
||||
private eventBus: IEventBuilder<any>
|
||||
|
||||
constructor(eventBus: IEventBuilder<any>) {
|
||||
this.eventBus = eventBus
|
||||
this.initializeDefaultChannels()
|
||||
this.startMessageProcessing()
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件频道
|
||||
*/
|
||||
subscribe(
|
||||
appId: string,
|
||||
channel: string,
|
||||
handler: (message: EventMessage) => void | Promise<void>,
|
||||
options: {
|
||||
filter?: (message: EventMessage) => boolean
|
||||
priority?: MessagePriority
|
||||
} = {}
|
||||
): string {
|
||||
// 检查通道权限
|
||||
if (!this.canAccessChannel(appId, channel)) {
|
||||
throw new Error(`应用 ${appId} 无权访问频道 ${channel}`)
|
||||
}
|
||||
|
||||
const subscriberId = uuidv4()
|
||||
const subscriber: EventSubscriber = {
|
||||
id: subscriberId,
|
||||
appId,
|
||||
channel,
|
||||
handler,
|
||||
filter: options.filter,
|
||||
priority: options.priority || MessagePriority.NORMAL,
|
||||
createdAt: new Date(),
|
||||
active: true
|
||||
}
|
||||
|
||||
this.subscribers.set(subscriberId, subscriber)
|
||||
this.updateActiveSubscribersCount()
|
||||
|
||||
console.log(`应用 ${appId} 订阅了频道 ${channel}`)
|
||||
return subscriberId
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
unsubscribe(subscriberId: string): boolean {
|
||||
const result = this.subscribers.delete(subscriberId)
|
||||
if (result) {
|
||||
this.updateActiveSubscribersCount()
|
||||
console.log(`取消订阅: ${subscriberId}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(
|
||||
senderId: string,
|
||||
channel: string,
|
||||
payload: any,
|
||||
options: {
|
||||
receiverId?: string
|
||||
priority?: MessagePriority
|
||||
type?: MessageType
|
||||
expiresIn?: number // 过期时间(毫秒)
|
||||
maxRetries?: number
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
// 检查发送者权限
|
||||
if (!this.canAccessChannel(senderId, channel)) {
|
||||
throw new Error(`应用 ${senderId} 无权向频道 ${channel} 发送消息`)
|
||||
}
|
||||
|
||||
// 检查消息大小
|
||||
const messageSize = JSON.stringify(payload).length
|
||||
const channelConfig = this.channels.get(channel)
|
||||
if (channelConfig && messageSize > channelConfig.maxMessageSize) {
|
||||
throw new Error(`消息大小超出限制: ${messageSize} > ${channelConfig.maxMessageSize}`)
|
||||
}
|
||||
|
||||
const messageId = uuidv4()
|
||||
const now = new Date()
|
||||
|
||||
const message: EventMessage = {
|
||||
id: messageId,
|
||||
type: options.type || MessageType.APPLICATION,
|
||||
priority: options.priority || MessagePriority.NORMAL,
|
||||
senderId,
|
||||
receiverId: options.receiverId,
|
||||
channel,
|
||||
payload,
|
||||
timestamp: now,
|
||||
expiresAt: options.expiresIn ? new Date(now.getTime() + options.expiresIn) : undefined,
|
||||
status: MessageStatus.PENDING,
|
||||
retryCount: 0,
|
||||
maxRetries: options.maxRetries || 3
|
||||
}
|
||||
|
||||
// 如果是点对点消息,直接发送
|
||||
if (options.receiverId) {
|
||||
await this.deliverMessage(message)
|
||||
} else {
|
||||
// 广播消息,加入队列处理
|
||||
this.addToQueue(message)
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
this.statistics.totalMessagesSent++
|
||||
if (!options.receiverId) {
|
||||
this.statistics.totalBroadcasts++
|
||||
}
|
||||
|
||||
const channelUsage = this.statistics.channelUsage.get(channel) || 0
|
||||
this.statistics.channelUsage.set(channel, channelUsage + 1)
|
||||
|
||||
// 记录消息历史
|
||||
this.recordMessage(message)
|
||||
|
||||
console.log(`[EventCommunication] 消息 ${messageId} 已发送到频道 ${channel}[发送者: ${senderId}]`)
|
||||
return messageId
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息到所有订阅者
|
||||
*/
|
||||
async broadcast(
|
||||
senderId: string,
|
||||
channel: string,
|
||||
payload: any,
|
||||
options: {
|
||||
priority?: MessagePriority
|
||||
expiresIn?: number
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
return this.sendMessage(senderId, channel, payload, {
|
||||
...options,
|
||||
type: MessageType.BROADCAST
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送跨应用消息
|
||||
*/
|
||||
async sendCrossAppMessage(
|
||||
senderId: string,
|
||||
receiverId: string,
|
||||
payload: any,
|
||||
options: {
|
||||
priority?: MessagePriority
|
||||
expiresIn?: number
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
const channel = 'cross-app'
|
||||
|
||||
return this.sendMessage(senderId, channel, payload, {
|
||||
...options,
|
||||
receiverId,
|
||||
type: MessageType.CROSS_APP
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息历史
|
||||
*/
|
||||
getMessageHistory(
|
||||
appId: string,
|
||||
options: {
|
||||
channel?: string
|
||||
limit?: number
|
||||
since?: Date
|
||||
} = {}
|
||||
): EventMessage[] {
|
||||
const history = this.messageHistory.get(appId) || []
|
||||
|
||||
let filtered = history.filter(msg =>
|
||||
msg.senderId === appId || msg.receiverId === appId
|
||||
)
|
||||
|
||||
if (options.channel) {
|
||||
filtered = filtered.filter(msg => msg.channel === options.channel)
|
||||
}
|
||||
|
||||
if (options.since) {
|
||||
filtered = filtered.filter(msg => msg.timestamp >= options.since!)
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
filtered = filtered.slice(-options.limit)
|
||||
}
|
||||
|
||||
return filtered.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用的订阅列表
|
||||
*/
|
||||
getAppSubscriptions(appId: string): EventSubscriber[] {
|
||||
return Array.from(this.subscribers.values()).filter(sub => sub.appId === appId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取频道订阅者数量
|
||||
*/
|
||||
getChannelSubscriberCount(channel: string): number {
|
||||
return Array.from(this.subscribers.values()).filter(
|
||||
sub => sub.channel === channel && sub.active
|
||||
).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通信频道
|
||||
*/
|
||||
createChannel(
|
||||
channel: string,
|
||||
config: Omit<CommunicationChannel, 'name'>
|
||||
): boolean {
|
||||
if (this.channels.has(channel)) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.channels.set(channel, {
|
||||
name: channel,
|
||||
...config
|
||||
})
|
||||
|
||||
console.log(`创建通信频道: ${channel}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通信频道
|
||||
*/
|
||||
deleteChannel(channel: string): boolean {
|
||||
// 移除所有相关订阅
|
||||
const subscribersToRemove = Array.from(this.subscribers.entries())
|
||||
.filter(([, sub]) => sub.channel === channel)
|
||||
.map(([id]) => id)
|
||||
|
||||
subscribersToRemove.forEach(id => this.unsubscribe(id))
|
||||
|
||||
// 删除频道
|
||||
const result = this.channels.delete(channel)
|
||||
|
||||
if (result) {
|
||||
console.log(`删除通信频道: ${channel}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStatistics(): EventStatistics {
|
||||
return { ...this.statistics }
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期消息和订阅
|
||||
*/
|
||||
cleanup(): void {
|
||||
const now = new Date()
|
||||
|
||||
// 清理过期消息
|
||||
for (const [appId, messages] of this.messageQueue.entries()) {
|
||||
const validMessages = messages.filter(msg =>
|
||||
!msg.expiresAt || msg.expiresAt > now
|
||||
)
|
||||
|
||||
if (validMessages.length !== messages.length) {
|
||||
this.messageQueue.set(appId, validMessages)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理消息历史(保留最近7天)
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
for (const [appId, history] of this.messageHistory.entries()) {
|
||||
const recentHistory = history.filter(msg => msg.timestamp > sevenDaysAgo)
|
||||
this.messageHistory.set(appId, recentHistory)
|
||||
}
|
||||
|
||||
console.log('事件通信服务清理完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁服务
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.processingInterval) {
|
||||
clearInterval(this.processingInterval)
|
||||
this.processingInterval = null
|
||||
}
|
||||
|
||||
this.subscribers.clear()
|
||||
this.messageQueue.clear()
|
||||
this.messageHistory.clear()
|
||||
this.channels.clear()
|
||||
|
||||
console.log('事件通信服务已销毁')
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 初始化默认频道
|
||||
*/
|
||||
private initializeDefaultChannels(): void {
|
||||
// 系统事件频道
|
||||
this.createChannel('system', {
|
||||
description: '系统级事件通信',
|
||||
restricted: true,
|
||||
allowedApps: ['system'],
|
||||
maxMessageSize: 1024 * 10, // 10KB
|
||||
messageRetention: 24 * 60 * 60 * 1000 // 24小时
|
||||
})
|
||||
|
||||
// 应用间通信频道
|
||||
this.createChannel('cross-app', {
|
||||
description: '应用间通信',
|
||||
restricted: false,
|
||||
allowedApps: [],
|
||||
maxMessageSize: 1024 * 100, // 100KB
|
||||
messageRetention: 7 * 24 * 60 * 60 * 1000 // 7天
|
||||
})
|
||||
|
||||
// 用户交互频道
|
||||
this.createChannel('user-interaction', {
|
||||
description: '用户交互事件',
|
||||
restricted: false,
|
||||
allowedApps: [],
|
||||
maxMessageSize: 1024 * 5, // 5KB
|
||||
messageRetention: 60 * 60 * 1000 // 1小时
|
||||
})
|
||||
|
||||
// 广播频道
|
||||
this.createChannel('broadcast', {
|
||||
description: '系统广播',
|
||||
restricted: true,
|
||||
allowedApps: ['system'],
|
||||
maxMessageSize: 1024 * 50, // 50KB
|
||||
messageRetention: 24 * 60 * 60 * 1000 // 24小时
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查应用是否可以访问频道
|
||||
*/
|
||||
private canAccessChannel(appId: string, channel: string): boolean {
|
||||
const channelConfig = this.channels.get(channel)
|
||||
|
||||
if (!channelConfig) {
|
||||
// 频道不存在,默认允许
|
||||
return true
|
||||
}
|
||||
|
||||
if (!channelConfig.restricted) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 系统应用总是有权限
|
||||
if (appId === 'system') {
|
||||
return true
|
||||
}
|
||||
|
||||
return channelConfig.allowedApps.includes(appId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到队列
|
||||
*/
|
||||
private addToQueue(message: EventMessage): void {
|
||||
const queueKey = message.receiverId || 'broadcast'
|
||||
|
||||
if (!this.messageQueue.has(queueKey)) {
|
||||
this.messageQueue.set(queueKey, [])
|
||||
}
|
||||
|
||||
const queue = this.messageQueue.get(queueKey)!
|
||||
|
||||
// 按优先级插入
|
||||
const insertIndex = queue.findIndex(msg => msg.priority < message.priority)
|
||||
if (insertIndex === -1) {
|
||||
queue.push(message)
|
||||
} else {
|
||||
queue.splice(insertIndex, 0, message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接投递消息
|
||||
*/
|
||||
private async deliverMessage(message: EventMessage): Promise<void> {
|
||||
try {
|
||||
const subscribers = this.getRelevantSubscribers(message)
|
||||
|
||||
if (subscribers.length === 0) {
|
||||
message.status = MessageStatus.FAILED
|
||||
console.warn(`[EventCommunication] 没有找到频道 ${message.channel} 的订阅者[消息 ID: ${message.id}]`)
|
||||
return
|
||||
}
|
||||
|
||||
// 并行发送给所有订阅者
|
||||
const deliveryPromises = subscribers.map(async (subscriber) => {
|
||||
try {
|
||||
// 应用过滤器
|
||||
if (subscriber.filter && !subscriber.filter(message)) {
|
||||
return
|
||||
}
|
||||
|
||||
await subscriber.handler(message)
|
||||
this.statistics.totalMessagesReceived++
|
||||
console.log(`[EventCommunication] 消息 ${message.id} 已投递给订阅者 ${subscriber.id}[频道: ${message.channel}]`)
|
||||
} catch (error) {
|
||||
console.error(`向订阅者 ${subscriber.id} 发送消息失败:`, error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.allSettled(deliveryPromises)
|
||||
message.status = MessageStatus.DELIVERED
|
||||
|
||||
} catch (error) {
|
||||
message.status = MessageStatus.FAILED
|
||||
this.statistics.failedMessages++
|
||||
console.error('消息投递失败:', error)
|
||||
|
||||
// 重试机制
|
||||
if (message.retryCount < message.maxRetries) {
|
||||
message.retryCount++
|
||||
message.status = MessageStatus.PENDING
|
||||
setTimeout(() => this.deliverMessage(message), 1000 * message.retryCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相关订阅者
|
||||
*/
|
||||
private getRelevantSubscribers(message: EventMessage): EventSubscriber[] {
|
||||
return Array.from(this.subscribers.values()).filter(subscriber => {
|
||||
if (!subscriber.active) return false
|
||||
if (subscriber.channel !== message.channel) return false
|
||||
|
||||
// 点对点消息检查接收者
|
||||
if (message.receiverId && subscriber.appId !== message.receiverId) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始消息处理循环
|
||||
*/
|
||||
private startMessageProcessing(): void {
|
||||
this.processingInterval = setInterval(() => {
|
||||
this.processMessageQueue()
|
||||
this.cleanupExpiredMessages()
|
||||
}, 100) // 每100ms处理一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息队列
|
||||
*/
|
||||
private processMessageQueue(): void {
|
||||
for (const [queueKey, messages] of this.messageQueue.entries()) {
|
||||
if (messages.length === 0) continue
|
||||
|
||||
// 处理优先级最高的消息
|
||||
const message = messages.shift()!
|
||||
|
||||
// 检查消息是否过期
|
||||
if (message.expiresAt && message.expiresAt <= new Date()) {
|
||||
message.status = MessageStatus.EXPIRED
|
||||
continue
|
||||
}
|
||||
|
||||
this.deliverMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期消息
|
||||
*/
|
||||
private cleanupExpiredMessages(): void {
|
||||
const now = new Date()
|
||||
|
||||
for (const [queueKey, messages] of this.messageQueue.entries()) {
|
||||
const validMessages = messages.filter(msg =>
|
||||
!msg.expiresAt || msg.expiresAt > now
|
||||
)
|
||||
|
||||
if (validMessages.length !== messages.length) {
|
||||
this.messageQueue.set(queueKey, validMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录消息历史
|
||||
*/
|
||||
private recordMessage(message: EventMessage): void {
|
||||
// 记录发送者历史
|
||||
if (!this.messageHistory.has(message.senderId)) {
|
||||
this.messageHistory.set(message.senderId, [])
|
||||
}
|
||||
this.messageHistory.get(message.senderId)!.push(message)
|
||||
|
||||
// 记录接收者历史
|
||||
if (message.receiverId && message.receiverId !== message.senderId) {
|
||||
if (!this.messageHistory.has(message.receiverId)) {
|
||||
this.messageHistory.set(message.receiverId, [])
|
||||
}
|
||||
this.messageHistory.get(message.receiverId)!.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活跃订阅者数量
|
||||
*/
|
||||
private updateActiveSubscribersCount(): void {
|
||||
this.statistics.activeSubscribers = Array.from(this.subscribers.values())
|
||||
.filter(sub => sub.active).length
|
||||
}
|
||||
}
|
||||
629
src/services/ExternalAppDiscovery.ts
Normal file
629
src/services/ExternalAppDiscovery.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { AppManifest } from './ApplicationLifecycleManager'
|
||||
|
||||
/**
|
||||
* 外置应用信息
|
||||
*/
|
||||
export interface ExternalApp {
|
||||
id: string
|
||||
manifest: AppManifest
|
||||
basePath: string
|
||||
manifestPath: string
|
||||
entryPath: string
|
||||
discovered: boolean
|
||||
lastScanned: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* 外置应用发现服务
|
||||
* 自动扫描 public/apps 目录下的外部应用
|
||||
*
|
||||
* 注意:
|
||||
* - 仅处理外部应用,不扫描内置应用
|
||||
* - 内置应用通过 AppRegistry 静态注册
|
||||
* - 已排除内置应用: calculator, notepad, todo
|
||||
*/
|
||||
export class ExternalAppDiscovery {
|
||||
private static instance: ExternalAppDiscovery | null = null
|
||||
private discoveredApps = reactive(new Map<string, ExternalApp>())
|
||||
private isScanning = false
|
||||
private hasStarted = false // 添加标志防止重复启动
|
||||
|
||||
constructor() {
|
||||
console.log('[ExternalAppDiscovery] 服务初始化')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): ExternalAppDiscovery {
|
||||
if (!ExternalAppDiscovery.instance) {
|
||||
ExternalAppDiscovery.instance = new ExternalAppDiscovery()
|
||||
}
|
||||
return ExternalAppDiscovery.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动应用发现服务(只执行一次扫描,不设置定时器)
|
||||
*/
|
||||
async startDiscovery(): Promise<void> {
|
||||
// 防止重复启动
|
||||
if (this.hasStarted) {
|
||||
console.log('[ExternalAppDiscovery] 服务已启动,跳过重复启动')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[ExternalAppDiscovery] 启动应用发现服务')
|
||||
this.hasStarted = true
|
||||
|
||||
// 只执行一次扫描,不设置定时器
|
||||
console.log('[ExternalAppDiscovery] 开始执行扫描...')
|
||||
await this.scanExternalApps()
|
||||
console.log('[ExternalAppDiscovery] 扫描完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止应用发现服务
|
||||
*/
|
||||
stopDiscovery(): void {
|
||||
console.log('[ExternalAppDiscovery] 停止应用发现服务')
|
||||
|
||||
this.hasStarted = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描外置应用
|
||||
*/
|
||||
async scanExternalApps(): Promise<void> {
|
||||
if (this.isScanning) {
|
||||
console.log('[ExternalAppDiscovery] 正在扫描中,跳过本次扫描')
|
||||
return
|
||||
}
|
||||
|
||||
this.isScanning = true
|
||||
console.log('[ExternalAppDiscovery] ==> 开始扫描外置应用')
|
||||
|
||||
try {
|
||||
// 获取 public/apps 目录下的所有应用文件夹
|
||||
const appDirs = await this.getAppDirectories()
|
||||
console.log(`[ExternalAppDiscovery] 发现 ${appDirs.length} 个应用目录:`, appDirs)
|
||||
|
||||
const newApps = new Map<string, ExternalApp>()
|
||||
|
||||
// 扫描每个应用目录
|
||||
for (const appDir of appDirs) {
|
||||
try {
|
||||
console.log(`[ExternalAppDiscovery] 扫描应用目录: ${appDir}`)
|
||||
const app = await this.scanAppDirectory(appDir)
|
||||
if (app) {
|
||||
newApps.set(app.id, app)
|
||||
console.log(`[ExternalAppDiscovery] ✓ 成功扫描应用: ${app.manifest.name} (${app.id})`)
|
||||
} else {
|
||||
console.log(`[ExternalAppDiscovery] ✗ 应用目录 ${appDir} 扫描失败或不存在`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError && error.message.includes('Unexpected token')) {
|
||||
console.warn(
|
||||
`[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 格式错误或返回HTML页面`,
|
||||
)
|
||||
} else {
|
||||
console.warn(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 失败:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新发现的应用列表
|
||||
this.updateDiscoveredApps(newApps)
|
||||
|
||||
console.log(`[ExternalAppDiscovery] ==> 扫描完成,发现 ${newApps.size} 个有效应用`)
|
||||
console.log(`[ExternalAppDiscovery] 当前总共有 ${this.discoveredApps.size} 个已发现应用`)
|
||||
} catch (error) {
|
||||
console.error('[ExternalAppDiscovery] 扫描外置应用失败:', error)
|
||||
} finally {
|
||||
this.isScanning = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用目录列表
|
||||
*/
|
||||
private async getAppDirectories(): Promise<string[]> {
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 开始获取应用目录列表')
|
||||
|
||||
// 方案1:使用Vite的glob功能(推荐)
|
||||
console.log('[ExternalAppDiscovery] 尝试使用Vite glob功能')
|
||||
const knownApps = await this.getKnownAppDirectories()
|
||||
console.log('[ExternalAppDiscovery] Vite glob结果:', knownApps)
|
||||
const validApps: string[] = []
|
||||
|
||||
// 验证已知应用是否真实存在
|
||||
for (const appDir of knownApps) {
|
||||
try {
|
||||
const manifestPath = `/apps/${appDir}/manifest.json`
|
||||
console.log(`[ExternalAppDiscovery] 检查应用 ${appDir} 的 manifest.json: ${manifestPath}`)
|
||||
const response = await fetch(manifestPath, { method: 'HEAD' })
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type')
|
||||
console.log(
|
||||
`[ExternalAppDiscovery] 应用 ${appDir} 的响应状态: ${response.status}, 内容类型: ${contentType}`,
|
||||
)
|
||||
// 检查是否返回JSON内容
|
||||
if (
|
||||
contentType &&
|
||||
(contentType.includes('application/json') || contentType.includes('text/json'))
|
||||
) {
|
||||
validApps.push(appDir)
|
||||
console.log(`[ExternalAppDiscovery] 确认应用存在: ${appDir}`)
|
||||
} else {
|
||||
console.warn(`[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 返回非JSON内容`)
|
||||
}
|
||||
} else {
|
||||
console.warn(`[ExternalAppDiscovery] 应用不存在: ${appDir} (HTTP ${response.status})`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[ExternalAppDiscovery] 检查应用 ${appDir} 时出错:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ExternalAppDiscovery] 验证后的有效应用:', validApps)
|
||||
|
||||
// 如果Vite glob没有找到应用,尝试其他方法
|
||||
if (validApps.length === 0) {
|
||||
console.log('[ExternalAppDiscovery] Vite glob未找到有效应用,尝试网络请求方式')
|
||||
|
||||
// 方案2:尝试目录列表扫描
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 尝试目录列表扫描')
|
||||
const additionalApps = await this.tryDirectoryListing()
|
||||
console.log('[ExternalAppDiscovery] 目录列表扫描结果:', additionalApps)
|
||||
// 合并去重
|
||||
for (const app of additionalApps) {
|
||||
if (!validApps.includes(app)) {
|
||||
validApps.push(app)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[ExternalAppDiscovery] 目录列表扫描失败')
|
||||
}
|
||||
|
||||
// 方案3:尝试扫描常见应用名称
|
||||
if (validApps.length === 0) {
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 尝试扫描常见应用名称')
|
||||
const commonApps = await this.tryCommonAppNames()
|
||||
console.log('[ExternalAppDiscovery] 常见应用扫描结果:', commonApps)
|
||||
for (const app of commonApps) {
|
||||
if (!validApps.includes(app)) {
|
||||
validApps.push(app)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[ExternalAppDiscovery] 常见应用扫描失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ExternalAppDiscovery] 最终发现 ${validApps.length} 个应用目录:`, validApps)
|
||||
return validApps
|
||||
} catch (error) {
|
||||
console.warn('[ExternalAppDiscovery] 获取目录列表失败,使用静态列表:', error)
|
||||
const fallbackList = [
|
||||
'music-player', // 音乐播放器应用
|
||||
// 可以在这里添加更多已知的外部应用
|
||||
]
|
||||
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
|
||||
return fallbackList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试通过 fetch 获取目录列表(开发环境可能失败)
|
||||
*/
|
||||
private async tryDirectoryListing(): Promise<string[]> {
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表')
|
||||
const response = await fetch('/apps/')
|
||||
|
||||
console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const html = await response.text()
|
||||
console.log('[ExternalAppDiscovery] 响应内容类型:', response.headers.get('content-type'))
|
||||
console.log('[ExternalAppDiscovery] 响应内容长度:', html.length)
|
||||
|
||||
// 检查是否真的是目录列表还是index.html
|
||||
if (html.includes('<!DOCTYPE html') || html.includes('<html')) {
|
||||
console.log('[ExternalAppDiscovery] 响应是HTML页面,不是目录列表')
|
||||
throw new Error('服务器返回HTML页面而不是目录列表')
|
||||
}
|
||||
|
||||
const directories = this.parseDirectoryListing(html)
|
||||
|
||||
if (directories.length === 0) {
|
||||
throw new Error('未从目录列表中解析到任何应用目录')
|
||||
}
|
||||
|
||||
return directories
|
||||
} catch (error) {
|
||||
console.warn('[ExternalAppDiscovery] 目录列表扫描失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试扫描常见的应用名称
|
||||
*/
|
||||
private async tryCommonAppNames(): Promise<string[]> {
|
||||
// 排除内置应用,只扫描外部应用
|
||||
const builtInApps = ['calculator', 'notepad', 'todo']
|
||||
|
||||
// 常见的外部应用名称列表
|
||||
const commonNames = [
|
||||
'file-manager',
|
||||
'text-editor',
|
||||
'image-viewer',
|
||||
'video-player',
|
||||
'chat-app',
|
||||
'weather-app',
|
||||
'calendar-app',
|
||||
'email-client',
|
||||
'web-browser',
|
||||
'code-editor',
|
||||
].filter((name) => !builtInApps.includes(name)) // 过滤掉内置应用
|
||||
|
||||
const validApps: string[] = []
|
||||
|
||||
// 检查每个常见应用是否实际存在
|
||||
for (const appName of commonNames) {
|
||||
try {
|
||||
const manifestPath = `/apps/${appName}/manifest.json`
|
||||
const response = await fetch(manifestPath, { method: 'HEAD' })
|
||||
|
||||
// 检查响应状态和内容类型
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type')
|
||||
// 只有在返回JSON内容时才认为找到了有效应用
|
||||
if (
|
||||
contentType &&
|
||||
(contentType.includes('application/json') || contentType.includes('text/json'))
|
||||
) {
|
||||
validApps.push(appName)
|
||||
console.log(`[ExternalAppDiscovery] 发现常见应用: ${appName}`)
|
||||
} else {
|
||||
console.debug(
|
||||
`[ExternalAppDiscovery] 应用 ${appName} 存在但 manifest.json 返回非JSON内容`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.debug(`[ExternalAppDiscovery] 应用 ${appName} 不存在 (HTTP ${response.status})`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 静默失败,不记录日志避免噪音
|
||||
console.debug(`[ExternalAppDiscovery] 检查应用 ${appName} 时出现网络错误`)
|
||||
}
|
||||
}
|
||||
|
||||
return validApps
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析目录列表HTML
|
||||
*/
|
||||
private parseDirectoryListing(html: string): string[] {
|
||||
console.log('[ExternalAppDiscovery] 解析目录列表HTML (前1000字符):', html.substring(0, 1000)) // 调试输出
|
||||
|
||||
const directories: string[] = []
|
||||
const builtInApps = ['calculator', 'notepad', 'todo'] // 内置应用列表
|
||||
|
||||
// 使用最简单有效的方法
|
||||
// 查找所有形如 /apps/dirname/ 的路径
|
||||
const pattern = /\/apps\/([^\/"'\s>]+)\//g
|
||||
let match
|
||||
while ((match = pattern.exec(html)) !== null) {
|
||||
const dirName = match[1]
|
||||
console.log(`[ExternalAppDiscovery] 匹配到目录: ${dirName}`)
|
||||
// 确保目录名有效且不是内置应用
|
||||
if (
|
||||
dirName &&
|
||||
dirName.length > 0 &&
|
||||
!dirName.startsWith('.') &&
|
||||
!builtInApps.includes(dirName) &&
|
||||
!directories.includes(dirName)
|
||||
) {
|
||||
directories.push(dirName)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
const uniqueDirs = [...new Set(directories)]
|
||||
console.log('[ExternalAppDiscovery] 最终解析结果:', uniqueDirs)
|
||||
return uniqueDirs
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试目录解析功能
|
||||
*/
|
||||
private testParseDirectoryListing(): void {
|
||||
// 测试方法已移除
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已知的应用目录
|
||||
*/
|
||||
private async getKnownAppDirectories(): Promise<string[]> {
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 使用Vite glob导入获取应用目录')
|
||||
|
||||
// 使用Vite的glob功能静态导入所有manifest.json文件
|
||||
const manifestModules = import.meta.glob('/public/apps/*/manifest.json')
|
||||
|
||||
// 从文件路径中提取应用目录名
|
||||
const appDirs: string[] = []
|
||||
for (const path in manifestModules) {
|
||||
// 路径格式: /public/apps/app-name/manifest.json
|
||||
const match = path.match(/\/public\/apps\/([^\/]+)\/manifest\.json/)
|
||||
if (match && match[1]) {
|
||||
const appDir = match[1]
|
||||
// 排除内置应用
|
||||
if (!this.isBuiltInApp(appDir)) {
|
||||
appDirs.push(appDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ExternalAppDiscovery] 通过Vite glob发现外部应用目录: ${appDirs.join(', ')}`)
|
||||
return appDirs
|
||||
} catch (error) {
|
||||
console.warn('[ExternalAppDiscovery] 使用Vite glob读取应用目录失败:', error)
|
||||
// 回退到静态列表
|
||||
const fallbackList = [
|
||||
'music-player', // 音乐播放器应用
|
||||
// 可以在这里添加更多已知的外部应用
|
||||
]
|
||||
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
|
||||
return fallbackList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过网络请求获取应用目录(备用方法)
|
||||
*/
|
||||
private async getKnownAppDirectoriesViaNetwork(): Promise<string[]> {
|
||||
try {
|
||||
console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表 /apps/')
|
||||
|
||||
// 尝试通过网络请求获取目录列表
|
||||
const response = await fetch('/public/apps/')
|
||||
|
||||
console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[ExternalAppDiscovery] 响应不成功,使用回退列表')
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type')
|
||||
console.log('[ExternalAppDiscovery] 响应内容类型:', contentType)
|
||||
|
||||
const html = await response.text()
|
||||
console.log(11111111, html)
|
||||
|
||||
console.log('[ExternalAppDiscovery] 目录列表HTML长度:', html.length)
|
||||
|
||||
const appDirs = this.parseDirectoryListing(html)
|
||||
console.log('[ExternalAppDiscovery] 解析到的应用目录:', appDirs)
|
||||
|
||||
// 过滤掉内置应用
|
||||
const externalApps = appDirs.filter((dir) => !this.isBuiltInApp(dir))
|
||||
|
||||
console.log(`[ExternalAppDiscovery] 通过目录列表发现外部应用目录: ${externalApps.join(', ')}`)
|
||||
return externalApps
|
||||
} catch (error) {
|
||||
console.warn('[ExternalAppDiscovery] 获取目录列表失败:', error)
|
||||
// 回退到静态列表
|
||||
const fallbackList = [
|
||||
'music-player', // 音乐播放器应用
|
||||
// 可以在这里添加更多已知的外部应用
|
||||
]
|
||||
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
|
||||
return fallbackList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描单个应用目录
|
||||
*/
|
||||
private async scanAppDirectory(appDir: string): Promise<ExternalApp | null> {
|
||||
try {
|
||||
// 首先检查是否为内置应用
|
||||
if (this.isBuiltInApp(appDir)) {
|
||||
console.log(`[ExternalAppDiscovery] 跳过内置应用: ${appDir}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const basePath = `/apps/${appDir}`
|
||||
const manifestPath = `${basePath}/manifest.json`
|
||||
|
||||
console.log(`[ExternalAppDiscovery] 扫描外部应用目录: ${appDir}`)
|
||||
|
||||
// 尝试获取 manifest.json
|
||||
const manifestResponse = await fetch(manifestPath)
|
||||
|
||||
if (!manifestResponse.ok) {
|
||||
console.warn(
|
||||
`[ExternalAppDiscovery] 未找到 manifest.json: ${manifestPath} (HTTP ${manifestResponse.status})`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查响应内容类型
|
||||
const contentType = manifestResponse.headers.get('content-type')
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
console.warn(
|
||||
`[ExternalAppDiscovery] manifest.json 返回了非JSON内容: ${manifestPath}, content-type: ${contentType}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
let manifest: AppManifest
|
||||
try {
|
||||
manifest = (await manifestResponse.json()) as AppManifest
|
||||
} catch (parseError) {
|
||||
console.warn(`[ExternalAppDiscovery] 解析 manifest.json 失败: ${manifestPath}`, parseError)
|
||||
return null
|
||||
}
|
||||
|
||||
// 验证 manifest 格式
|
||||
if (!this.validateManifest(manifest)) {
|
||||
console.warn(`[ExternalAppDiscovery] 无效的 manifest.json: ${manifestPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 再次检查 manifest.id 是否为内置应用
|
||||
if (this.isBuiltInApp(manifest.id)) {
|
||||
console.warn(`[ExternalAppDiscovery] 检测到内置应用 ID: ${manifest.id},跳过`)
|
||||
return null
|
||||
}
|
||||
|
||||
const entryPath = `${basePath}/${manifest.entryPoint}`
|
||||
|
||||
// 验证入口文件是否存在
|
||||
const entryResponse = await fetch(entryPath, { method: 'HEAD' })
|
||||
if (!entryResponse.ok) {
|
||||
console.warn(`[ExternalAppDiscovery] 入口文件不存在: ${entryPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const app: ExternalApp = {
|
||||
id: manifest.id,
|
||||
manifest,
|
||||
basePath,
|
||||
manifestPath,
|
||||
entryPath,
|
||||
discovered: true,
|
||||
lastScanned: new Date(),
|
||||
}
|
||||
|
||||
console.log(`[ExternalAppDiscovery] 发现有效外部应用: ${manifest.name} (${manifest.id})`)
|
||||
return app
|
||||
} catch (error) {
|
||||
console.error(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 时出错:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为内置应用
|
||||
*/
|
||||
private isBuiltInApp(appId: string): boolean {
|
||||
const builtInApps = ['calculator', 'notepad', 'todo']
|
||||
return builtInApps.includes(appId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证应用清单
|
||||
*/
|
||||
private validateManifest(manifest: any): manifest is AppManifest {
|
||||
if (!manifest || typeof manifest !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查必需字段
|
||||
const requiredFields = ['id', 'name', 'version', 'entryPoint']
|
||||
for (const field of requiredFields) {
|
||||
if (!manifest[field] || typeof manifest[field] !== 'string') {
|
||||
console.warn(`[ExternalAppDiscovery] manifest 缺少必需字段: ${field}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 验证版本格式
|
||||
if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
||||
console.warn(`[ExternalAppDiscovery] 版本号格式不正确: ${manifest.version}`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证应用ID格式
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(manifest.id)) {
|
||||
console.warn(`[ExternalAppDiscovery] 应用ID格式不正确: ${manifest.id}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新发现的应用列表
|
||||
*/
|
||||
private updateDiscoveredApps(newApps: Map<string, ExternalApp>): void {
|
||||
// 移除不再存在的应用
|
||||
for (const [appId] of this.discoveredApps) {
|
||||
if (!newApps.has(appId)) {
|
||||
console.log(`[ExternalAppDiscovery] 应用已移除: ${appId}`)
|
||||
this.discoveredApps.delete(appId)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加或更新应用
|
||||
for (const [appId, app] of newApps) {
|
||||
const existingApp = this.discoveredApps.get(appId)
|
||||
|
||||
if (!existingApp) {
|
||||
console.log(`[ExternalAppDiscovery] 发现新应用: ${app.manifest.name} (${appId})`)
|
||||
this.discoveredApps.set(appId, app)
|
||||
} else if (existingApp.manifest.version !== app.manifest.version) {
|
||||
console.log(
|
||||
`[ExternalAppDiscovery] 应用版本更新: ${appId} ${existingApp.manifest.version} -> ${app.manifest.version}`,
|
||||
)
|
||||
this.discoveredApps.set(appId, app)
|
||||
} else {
|
||||
// 只更新扫描时间
|
||||
existingApp.lastScanned = app.lastScanned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有发现的应用
|
||||
*/
|
||||
getDiscoveredApps(): ExternalApp[] {
|
||||
return Array.from(this.discoveredApps.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定应用
|
||||
*/
|
||||
getApp(appId: string): ExternalApp | undefined {
|
||||
return this.discoveredApps.get(appId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查应用是否存在
|
||||
*/
|
||||
hasApp(appId: string): boolean {
|
||||
return this.discoveredApps.has(appId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用数量
|
||||
*/
|
||||
getAppCount(): number {
|
||||
return this.discoveredApps.size
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动刷新应用列表
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
console.log('[ExternalAppDiscovery] 手动刷新应用列表')
|
||||
await this.scanExternalApps()
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const externalAppDiscovery = ExternalAppDiscovery.getInstance()
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
873
src/services/SystemServiceIntegration.ts
Normal file
873
src/services/SystemServiceIntegration.ts
Normal file
@@ -0,0 +1,873 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl'
|
||||
import type { IEventBuilder } from '@/events/IEventBuilder'
|
||||
|
||||
// 导入所有服务
|
||||
import { WindowService } from './WindowService'
|
||||
import { ResourceService } from './ResourceService'
|
||||
import { EventCommunicationService } from './EventCommunicationService'
|
||||
import { ApplicationSandboxEngine } from './ApplicationSandboxEngine'
|
||||
import { ApplicationLifecycleManager } from './ApplicationLifecycleManager'
|
||||
import { externalAppDiscovery } from './ExternalAppDiscovery'
|
||||
|
||||
/**
|
||||
* 系统服务配置接口
|
||||
*/
|
||||
export interface SystemServiceConfig {
|
||||
debug?: boolean
|
||||
maxMemoryUsage?: number
|
||||
maxCpuUsage?: number
|
||||
enablePerformanceMonitoring?: boolean
|
||||
enableSecurityAudit?: boolean
|
||||
autoCleanup?: boolean
|
||||
cleanupInterval?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统状态接口
|
||||
*/
|
||||
export interface SystemStatus {
|
||||
initialized: boolean
|
||||
running: boolean
|
||||
servicesStatus: {
|
||||
windowService: boolean
|
||||
resourceService: boolean
|
||||
eventService: boolean
|
||||
sandboxEngine: boolean
|
||||
lifecycleManager: boolean
|
||||
}
|
||||
performance: {
|
||||
memoryUsage: number
|
||||
cpuUsage: number
|
||||
activeApps: number
|
||||
activeWindows: number
|
||||
}
|
||||
uptime: number
|
||||
lastError?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK调用接口
|
||||
*/
|
||||
export interface SDKCall {
|
||||
requestId: string
|
||||
method: string
|
||||
data?: any
|
||||
appId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统服务集成层
|
||||
* 统一管理所有核心服务,提供统一的对外接口
|
||||
*/
|
||||
export class SystemServiceIntegration {
|
||||
private initialized = ref(false)
|
||||
private running = ref(false)
|
||||
private config: SystemServiceConfig
|
||||
private startTime: Date
|
||||
|
||||
// 核心服务实例
|
||||
private eventBus: IEventBuilder<any>
|
||||
private windowService!: WindowService
|
||||
private resourceService!: ResourceService
|
||||
private eventService!: EventCommunicationService
|
||||
private sandboxEngine!: ApplicationSandboxEngine
|
||||
private lifecycleManager!: ApplicationLifecycleManager
|
||||
|
||||
// 系统状态
|
||||
private systemStatus = reactive<SystemStatus>({
|
||||
initialized: false,
|
||||
running: false,
|
||||
servicesStatus: {
|
||||
windowService: false,
|
||||
resourceService: false,
|
||||
eventService: false,
|
||||
sandboxEngine: false,
|
||||
lifecycleManager: false,
|
||||
},
|
||||
performance: {
|
||||
memoryUsage: 0,
|
||||
cpuUsage: 0,
|
||||
activeApps: 0,
|
||||
activeWindows: 0,
|
||||
},
|
||||
uptime: 0,
|
||||
})
|
||||
|
||||
// 性能监控
|
||||
private cleanupInterval: number | null = null
|
||||
private performanceInterval: number | null = null
|
||||
|
||||
constructor(config: SystemServiceConfig = {}) {
|
||||
this.config = {
|
||||
debug: false,
|
||||
maxMemoryUsage: 1024, // 1GB
|
||||
maxCpuUsage: 80, // 80%
|
||||
enablePerformanceMonitoring: true,
|
||||
enableSecurityAudit: true,
|
||||
autoCleanup: true,
|
||||
cleanupInterval: 5 * 60 * 1000, // 5分钟
|
||||
...config,
|
||||
}
|
||||
|
||||
this.startTime = new Date()
|
||||
this.eventBus = new EventBuilderImpl()
|
||||
|
||||
this.setupGlobalErrorHandling()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统服务
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized.value) {
|
||||
throw new Error('系统服务已初始化')
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始初始化系统服务...')
|
||||
|
||||
// 按依赖顺序初始化服务
|
||||
await this.initializeServices()
|
||||
|
||||
// 设置服务间通信
|
||||
this.setupServiceCommunication()
|
||||
|
||||
// 设置SDK消息处理
|
||||
this.setupSDKMessageHandling()
|
||||
|
||||
// 启动性能监控
|
||||
if (this.config.enablePerformanceMonitoring) {
|
||||
this.startPerformanceMonitoring()
|
||||
}
|
||||
|
||||
// 启动自动清理
|
||||
if (this.config.autoCleanup) {
|
||||
this.startAutoCleanup()
|
||||
}
|
||||
|
||||
// 启动外置应用发现服务
|
||||
// 注意:外置应用发现服务统一由 SystemServiceIntegration 管理,
|
||||
// ApplicationLifecycleManager 只负责使用已发现的应用,避免重复启动
|
||||
console.log('启动外置应用发现服务...')
|
||||
await externalAppDiscovery.startDiscovery()
|
||||
|
||||
this.initialized.value = true
|
||||
this.running.value = true
|
||||
this.systemStatus.initialized = true
|
||||
this.systemStatus.running = true
|
||||
|
||||
console.log('系统服务初始化完成')
|
||||
|
||||
// 发送系统就绪事件
|
||||
this.eventService.sendMessage('system', 'system-ready', {
|
||||
timestamp: new Date(),
|
||||
services: Object.keys(this.systemStatus.servicesStatus),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('系统服务初始化失败:', error)
|
||||
this.systemStatus.lastError = error instanceof Error ? error.message : String(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
*/
|
||||
getSystemStatus(): SystemStatus {
|
||||
this.updateSystemStatus()
|
||||
return { ...this.systemStatus }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗体服务
|
||||
*/
|
||||
getWindowService(): WindowService {
|
||||
this.checkInitialized()
|
||||
return this.windowService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源服务
|
||||
*/
|
||||
getResourceService(): ResourceService {
|
||||
this.checkInitialized()
|
||||
return this.resourceService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件服务
|
||||
*/
|
||||
getEventService(): EventCommunicationService {
|
||||
this.checkInitialized()
|
||||
return this.eventService
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取沙箱引擎
|
||||
*/
|
||||
getSandboxEngine(): ApplicationSandboxEngine {
|
||||
this.checkInitialized()
|
||||
return this.sandboxEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取生命周期管理器
|
||||
*/
|
||||
getLifecycleManager(): ApplicationLifecycleManager {
|
||||
this.checkInitialized()
|
||||
return this.lifecycleManager
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SDK调用
|
||||
*/
|
||||
async handleSDKCall(call: SDKCall): Promise<any> {
|
||||
this.checkInitialized()
|
||||
|
||||
const { requestId, method, data, appId } = call
|
||||
|
||||
try {
|
||||
this.debugLog(`处理SDK调用: ${method}`, { appId, data })
|
||||
|
||||
const result = await this.executeSDKMethod(method, data, appId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
requestId,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SDK调用失败:', error)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
requestId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启系统服务
|
||||
*/
|
||||
async restart(): Promise<void> {
|
||||
console.log('重启系统服务...')
|
||||
|
||||
await this.shutdown()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
await this.initialize()
|
||||
|
||||
console.log('系统服务重启完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭系统服务
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
console.log('关闭系统服务...')
|
||||
|
||||
this.running.value = false
|
||||
this.systemStatus.running = false
|
||||
|
||||
// 停止定时器
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval)
|
||||
this.cleanupInterval = null
|
||||
}
|
||||
|
||||
if (this.performanceInterval) {
|
||||
clearInterval(this.performanceInterval)
|
||||
this.performanceInterval = null
|
||||
}
|
||||
|
||||
// 停止外置应用发现服务(由 SystemServiceIntegration 统一管理)
|
||||
externalAppDiscovery.stopDiscovery()
|
||||
|
||||
// 按相反顺序关闭服务
|
||||
try {
|
||||
if (this.lifecycleManager) {
|
||||
// 停止所有运行中的应用
|
||||
const runningApps = this.lifecycleManager.getRunningApps()
|
||||
for (const app of runningApps) {
|
||||
await this.lifecycleManager.stopApp(app.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.sandboxEngine) {
|
||||
this.sandboxEngine.destroy()
|
||||
}
|
||||
|
||||
if (this.eventService) {
|
||||
this.eventService.destroy()
|
||||
}
|
||||
|
||||
if (this.windowService) {
|
||||
// 关闭所有窗体
|
||||
const windows = this.windowService.getAllWindows()
|
||||
for (const window of windows) {
|
||||
await this.windowService.destroyWindow(window.id)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关闭服务时发生错误:', error)
|
||||
}
|
||||
|
||||
this.initialized.value = false
|
||||
this.systemStatus.initialized = false
|
||||
|
||||
console.log('系统服务已关闭')
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 初始化所有服务
|
||||
*/
|
||||
private async initializeServices(): Promise<void> {
|
||||
// 1. 初始化资源服务
|
||||
console.log('初始化资源服务...')
|
||||
this.resourceService = new ResourceService(this.eventBus)
|
||||
this.systemStatus.servicesStatus.resourceService = true
|
||||
|
||||
// 2. 初始化事件通信服务
|
||||
console.log('初始化事件通信服务...')
|
||||
this.eventService = new EventCommunicationService(this.eventBus)
|
||||
this.systemStatus.servicesStatus.eventService = true
|
||||
|
||||
// 3. 初始化窗体服务
|
||||
console.log('初始化窗体服务...')
|
||||
this.windowService = new WindowService(this.eventBus)
|
||||
this.systemStatus.servicesStatus.windowService = true
|
||||
|
||||
// 4. 初始化沙箱引擎
|
||||
console.log('初始化沙箱引擎...')
|
||||
this.sandboxEngine = new ApplicationSandboxEngine(this.resourceService, this.eventService)
|
||||
this.systemStatus.servicesStatus.sandboxEngine = true
|
||||
|
||||
// 5. 初始化生命周期管理器
|
||||
console.log('初始化生命周期管理器...')
|
||||
this.lifecycleManager = new ApplicationLifecycleManager(
|
||||
this.windowService,
|
||||
this.resourceService,
|
||||
this.eventService,
|
||||
this.sandboxEngine,
|
||||
)
|
||||
this.systemStatus.servicesStatus.lifecycleManager = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务间通信
|
||||
*/
|
||||
private setupServiceCommunication(): void {
|
||||
// 监听应用生命周期事件
|
||||
this.eventService.subscribe('system', 'app-lifecycle', (message) => {
|
||||
this.debugLog('[AppLifecycle] 应用生命周期事件:', message.payload)
|
||||
})
|
||||
|
||||
// 监听窗口状态变化事件
|
||||
this.eventService.subscribe('system', 'window-state-change', (message) => {
|
||||
this.debugLog('[WindowState] 窗口状态变化消息已处理:', message.payload)
|
||||
})
|
||||
|
||||
// 监听窗体状态变化(来自 WindowService 的 onStateChange 事件)
|
||||
this.eventBus.addEventListener(
|
||||
'onStateChange',
|
||||
(windowId: string, newState: string, oldState: string) => {
|
||||
console.log(
|
||||
`[SystemIntegration] 接收到窗体状态变化事件: ${windowId} ${oldState} -> ${newState}`,
|
||||
)
|
||||
this.eventService.sendMessage('system', 'window-state-change', {
|
||||
windowId,
|
||||
newState,
|
||||
oldState,
|
||||
})
|
||||
console.log(`[SystemIntegration] 已发送 window-state-change 消息到事件通信服务`)
|
||||
},
|
||||
)
|
||||
|
||||
// 监听窗体关闭事件,自动停止对应的应用
|
||||
this.eventBus.addEventListener('onClose', async (windowId: string) => {
|
||||
console.log(`[SystemIntegration] 接收到窗体关闭事件: ${windowId}`)
|
||||
// 查找对应的应用
|
||||
const runningApps = this.lifecycleManager.getRunningApps()
|
||||
for (const app of runningApps) {
|
||||
if (app.windowId === windowId) {
|
||||
try {
|
||||
console.log(`窗口关闭,自动停止应用: ${app.id}`)
|
||||
await this.lifecycleManager.stopApp(app.id)
|
||||
} catch (error) {
|
||||
console.error(`停止应用 ${app.id} 失败:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听资源配额超出
|
||||
this.eventBus.addEventListener(
|
||||
'onResourceQuotaExceeded',
|
||||
(appId: string, resourceType: string) => {
|
||||
console.log(`[SystemIntegration] 接收到资源配额超出事件: ${appId} - ${resourceType}`)
|
||||
this.eventService.sendMessage('system', 'resource-quota-exceeded', {
|
||||
appId,
|
||||
resourceType,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SDK消息处理
|
||||
*/
|
||||
private setupSDKMessageHandling(): void {
|
||||
// 监听来自iframe的SDK调用
|
||||
window.addEventListener('message', async (event) => {
|
||||
const data = event.data
|
||||
if (!data) return
|
||||
|
||||
// 处理安全存储消息
|
||||
if (data.type?.startsWith('sdk:storage:')) {
|
||||
await this.handleStorageMessage(event)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理其他SDK调用
|
||||
if (data.type === 'sdk:call') {
|
||||
const call: SDKCall = data
|
||||
const result = await this.handleSDKCall(call)
|
||||
|
||||
// 发送响应回iframe
|
||||
const iframe = this.findIframeBySource(event.source as Window)
|
||||
if (iframe) {
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'system:response',
|
||||
...result,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理安全存储消息
|
||||
*/
|
||||
private async handleStorageMessage(event: MessageEvent): Promise<void> {
|
||||
const { type, requestId, appId, key, value } = event.data
|
||||
|
||||
if (!requestId || !appId) {
|
||||
console.warn('存储消息缺少必需参数')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证应用权限
|
||||
const app = this.lifecycleManager.getApp(appId)
|
||||
if (!app) {
|
||||
console.warn(`未找到应用: ${appId}`)
|
||||
return
|
||||
}
|
||||
|
||||
let result: any = null
|
||||
let success = false
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'sdk:storage:get':
|
||||
result = await this.resourceService.getStorage(appId, key)
|
||||
success = true
|
||||
break
|
||||
|
||||
case 'sdk:storage:set':
|
||||
result = await this.resourceService.setStorage(appId, key, value)
|
||||
success = result === true
|
||||
break
|
||||
|
||||
case 'sdk:storage:remove':
|
||||
result = await this.resourceService.removeStorage(appId, key)
|
||||
success = result === true
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn(`未知的存储操作: ${type}`)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('存储操作失败:', error)
|
||||
success = false
|
||||
}
|
||||
|
||||
// 发送响应回iframe
|
||||
const iframe = this.findIframeBySource(event.source as Window)
|
||||
if (iframe?.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'system:storage-response',
|
||||
requestId,
|
||||
result,
|
||||
success,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SDK方法
|
||||
*/
|
||||
private async executeSDKMethod(method: string, data: any, appId: string): Promise<any> {
|
||||
const [service, action] = method.split('.')
|
||||
|
||||
switch (service) {
|
||||
case 'window':
|
||||
return this.executeWindowMethod(action, data, appId)
|
||||
|
||||
case 'storage':
|
||||
return this.executeStorageMethod(action, data, appId)
|
||||
|
||||
case 'network':
|
||||
return this.executeNetworkMethod(action, data, appId)
|
||||
|
||||
case 'events':
|
||||
return this.executeEventMethod(action, data, appId)
|
||||
|
||||
case 'ui':
|
||||
return this.executeUIMethod(action, data, appId)
|
||||
|
||||
case 'system':
|
||||
return this.executeSystemMethod(action, data, appId)
|
||||
|
||||
case 'sdk':
|
||||
return this.executeSDKMethod(action, data, appId)
|
||||
|
||||
default:
|
||||
throw new Error(`未知的服务: ${service}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行窗体相关方法
|
||||
*/
|
||||
private async executeWindowMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
// 查找应用的窗体
|
||||
const app = this.lifecycleManager.getApp(appId)
|
||||
if (!app?.windowId) {
|
||||
throw new Error('应用窗体不存在')
|
||||
}
|
||||
|
||||
const windowId = app.windowId
|
||||
|
||||
switch (action) {
|
||||
case 'setTitle':
|
||||
return this.windowService.setWindowTitle(windowId, data.title)
|
||||
|
||||
case 'resize':
|
||||
return this.windowService.setWindowSize(windowId, data.width, data.height)
|
||||
|
||||
case 'move':
|
||||
// 需要实现窗体移动功能
|
||||
return true
|
||||
|
||||
case 'minimize':
|
||||
return this.windowService.minimizeWindow(windowId)
|
||||
|
||||
case 'maximize':
|
||||
return this.windowService.maximizeWindow(windowId)
|
||||
|
||||
case 'restore':
|
||||
return this.windowService.restoreWindow(windowId)
|
||||
|
||||
case 'close':
|
||||
return this.lifecycleManager.stopApp(appId)
|
||||
|
||||
case 'getState':
|
||||
const window = this.windowService.getWindow(windowId)
|
||||
return window?.state
|
||||
|
||||
case 'getSize':
|
||||
const windowInfo = this.windowService.getWindow(windowId)
|
||||
return {
|
||||
width: windowInfo?.config.width,
|
||||
height: windowInfo?.config.height,
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`未知的窗体操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行存储相关方法
|
||||
*/
|
||||
private async executeStorageMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
switch (action) {
|
||||
case 'set':
|
||||
return this.resourceService.setStorage(appId, data.key, data.value)
|
||||
|
||||
case 'get':
|
||||
return this.resourceService.getStorage(appId, data.key)
|
||||
|
||||
case 'remove':
|
||||
return this.resourceService.removeStorage(appId, data.key)
|
||||
|
||||
case 'clear':
|
||||
return this.resourceService.clearStorage(appId)
|
||||
|
||||
case 'keys':
|
||||
// 需要实现获取所有键的功能
|
||||
return []
|
||||
|
||||
case 'has':
|
||||
const value = await this.resourceService.getStorage(appId, data.key)
|
||||
return value !== null
|
||||
|
||||
case 'getStats':
|
||||
return this.resourceService.getStorageUsage(appId)
|
||||
|
||||
default:
|
||||
throw new Error(`未知的存储操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行网络相关方法
|
||||
*/
|
||||
private async executeNetworkMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
switch (action) {
|
||||
case 'request':
|
||||
const response = await this.resourceService.makeNetworkRequest(appId, data.url, data.config)
|
||||
return response
|
||||
? {
|
||||
data: await response.text(),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: {} as Record<string, string>, // 简化headers处理
|
||||
url: response.url,
|
||||
}
|
||||
: null
|
||||
|
||||
case 'isOnline':
|
||||
return navigator.onLine
|
||||
|
||||
case 'getStats':
|
||||
const requests = this.resourceService.getNetworkRequests(appId)
|
||||
return {
|
||||
requestCount: requests.length,
|
||||
failureCount: requests.filter((r) => r.status && r.status >= 400).length,
|
||||
averageTime: 0, // 需要实现时间统计
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`未知的网络操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行事件相关方法
|
||||
*/
|
||||
private async executeEventMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
switch (action) {
|
||||
case 'emit':
|
||||
return this.eventService.sendMessage(appId, data.channel, data.data)
|
||||
|
||||
case 'on':
|
||||
return this.eventService.subscribe(appId, data.channel, (message) => {
|
||||
// 发送事件到应用
|
||||
const app = this.lifecycleManager.getApp(appId)
|
||||
if (app?.sandboxId) {
|
||||
this.sandboxEngine.sendMessage(app.sandboxId, {
|
||||
type: 'system:event',
|
||||
subscriptionId: data.subscriptionId,
|
||||
message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
case 'off':
|
||||
return this.eventService.unsubscribe(data.subscriptionId)
|
||||
|
||||
case 'broadcast':
|
||||
return this.eventService.broadcast(appId, data.channel, data.data)
|
||||
|
||||
case 'sendTo':
|
||||
return this.eventService.sendCrossAppMessage(appId, data.targetAppId, data.data)
|
||||
|
||||
default:
|
||||
throw new Error(`未知的事件操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行UI相关方法
|
||||
*/
|
||||
private async executeUIMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
switch (action) {
|
||||
case 'showNotification':
|
||||
return this.resourceService.showNotification(appId, data.title, data)
|
||||
|
||||
case 'showToast':
|
||||
// 需要实现Toast功能
|
||||
console.log(`[Toast] ${data.message}`)
|
||||
return 'toast-' + Date.now()
|
||||
|
||||
default:
|
||||
throw new Error(`未知的UI操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行系统相关方法
|
||||
*/
|
||||
private async executeSystemMethod(action: string, data: any, appId: string): Promise<any> {
|
||||
switch (action) {
|
||||
case 'getSystemInfo':
|
||||
return {
|
||||
platform: navigator.platform,
|
||||
userAgent: navigator.userAgent,
|
||||
language: navigator.language,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
screenResolution: {
|
||||
width: screen.width,
|
||||
height: screen.height,
|
||||
},
|
||||
colorDepth: screen.colorDepth,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
}
|
||||
|
||||
case 'getAppInfo':
|
||||
const app = this.lifecycleManager.getApp(appId)
|
||||
return app
|
||||
? {
|
||||
id: app.id,
|
||||
name: app.manifest.name,
|
||||
version: app.version,
|
||||
permissions: app.manifest.permissions,
|
||||
createdAt: app.installedAt,
|
||||
lastActiveAt: app.lastActiveAt,
|
||||
}
|
||||
: null
|
||||
|
||||
case 'getClipboard':
|
||||
return this.resourceService.getClipboard(appId)
|
||||
|
||||
case 'setClipboard':
|
||||
return this.resourceService.setClipboard(appId, data.text)
|
||||
|
||||
case 'getCurrentTime':
|
||||
return new Date()
|
||||
|
||||
case 'generateUUID':
|
||||
return crypto.randomUUID()
|
||||
|
||||
default:
|
||||
throw new Error(`未知的系统操作: ${action}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找消息来源的iframe
|
||||
*/
|
||||
private findIframeBySource(source: Window): HTMLIFrameElement | null {
|
||||
const iframes = Array.from(document.querySelectorAll('iframe'))
|
||||
|
||||
for (const iframe of iframes) {
|
||||
if (iframe.contentWindow === source) {
|
||||
return iframe
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始性能监控
|
||||
*/
|
||||
private startPerformanceMonitoring(): void {
|
||||
this.performanceInterval = setInterval(() => {
|
||||
this.updateSystemStatus()
|
||||
|
||||
// 检查性能阈值
|
||||
if (this.systemStatus.performance.memoryUsage > this.config.maxMemoryUsage!) {
|
||||
this.eventService.sendMessage('system', 'performance-alert', {
|
||||
type: 'memory',
|
||||
usage: this.systemStatus.performance.memoryUsage,
|
||||
limit: this.config.maxMemoryUsage,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.systemStatus.performance.cpuUsage > this.config.maxCpuUsage!) {
|
||||
this.eventService.sendMessage('system', 'performance-alert', {
|
||||
type: 'cpu',
|
||||
usage: this.systemStatus.performance.cpuUsage,
|
||||
limit: this.config.maxCpuUsage,
|
||||
})
|
||||
}
|
||||
}, 10000) // 每10秒检查一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自动清理
|
||||
*/
|
||||
private startAutoCleanup(): void {
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.debugLog('执行自动清理...')
|
||||
|
||||
// 清理事件服务
|
||||
this.eventService.cleanup()
|
||||
|
||||
// 清理沙箱引擎缓存
|
||||
// this.sandboxEngine.cleanup()
|
||||
|
||||
this.debugLog('自动清理完成')
|
||||
}, this.config.cleanupInterval!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统状态
|
||||
*/
|
||||
private updateSystemStatus(): void {
|
||||
this.systemStatus.uptime = Date.now() - this.startTime.getTime()
|
||||
this.systemStatus.performance.activeApps = this.lifecycleManager?.getRunningApps().length || 0
|
||||
this.systemStatus.performance.activeWindows = this.windowService?.getAllWindows().length || 0
|
||||
|
||||
// 简化的内存和CPU使用率计算
|
||||
this.systemStatus.performance.memoryUsage =
|
||||
(performance as any).memory?.usedJSHeapSize / 1024 / 1024 || 0
|
||||
this.systemStatus.performance.cpuUsage = Math.random() * 20 // 模拟CPU使用率
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
private checkInitialized(): void {
|
||||
if (!this.initialized.value) {
|
||||
throw new Error('系统服务未初始化')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局错误处理
|
||||
*/
|
||||
private setupGlobalErrorHandling(): void {
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('全局错误:', event.error)
|
||||
this.systemStatus.lastError = event.error?.message || '未知错误'
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('未处理的Promise拒绝:', event.reason)
|
||||
this.systemStatus.lastError = event.reason?.message || '未处理的Promise拒绝'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志
|
||||
*/
|
||||
private debugLog(message: string, data?: any): void {
|
||||
if (this.config.debug) {
|
||||
console.log(`[SystemService] ${message}`, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
651
src/services/WindowService.ts
Normal file
651
src/services/WindowService.ts
Normal file
@@ -0,0 +1,651 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
/**
|
||||
* 窗体状态枚举
|
||||
*/
|
||||
export enum WindowState {
|
||||
CREATING = 'creating',
|
||||
LOADING = 'loading',
|
||||
ACTIVE = 'active',
|
||||
MINIMIZED = 'minimized',
|
||||
MAXIMIZED = 'maximized',
|
||||
CLOSING = 'closing',
|
||||
DESTROYED = 'destroyed',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗体配置接口
|
||||
*/
|
||||
export interface WindowConfig {
|
||||
title: string
|
||||
width: number
|
||||
height: number
|
||||
minWidth?: number
|
||||
minHeight?: number
|
||||
maxWidth?: number
|
||||
maxHeight?: number
|
||||
resizable?: boolean
|
||||
movable?: boolean
|
||||
closable?: boolean
|
||||
minimizable?: boolean
|
||||
maximizable?: boolean
|
||||
modal?: boolean
|
||||
alwaysOnTop?: boolean
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗体实例接口
|
||||
*/
|
||||
export interface WindowInstance {
|
||||
id: string
|
||||
appId: string
|
||||
config: WindowConfig
|
||||
state: WindowState
|
||||
element?: HTMLElement
|
||||
iframe?: HTMLIFrameElement
|
||||
zIndex: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗体事件接口
|
||||
*/
|
||||
export interface WindowEvents extends IEventMap {
|
||||
onStateChange: (windowId: string, newState: WindowState, oldState: WindowState) => void
|
||||
onResize: (windowId: string, width: number, height: number) => void
|
||||
onMove: (windowId: string, x: number, y: number) => void
|
||||
onFocus: (windowId: string) => void
|
||||
onBlur: (windowId: string) => void
|
||||
onClose: (windowId: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗体管理服务类
|
||||
*/
|
||||
export class WindowService {
|
||||
private windows = reactive(new Map<string, WindowInstance>())
|
||||
private activeWindowId = ref<string | null>(null)
|
||||
private nextZIndex = 1000
|
||||
private eventBus: IEventBuilder<WindowEvents>
|
||||
|
||||
constructor(eventBus: IEventBuilder<WindowEvents>) {
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新窗体
|
||||
*/
|
||||
async createWindow(appId: string, config: WindowConfig): Promise<WindowInstance> {
|
||||
const windowId = uuidv4()
|
||||
const now = new Date()
|
||||
|
||||
const windowInstance: WindowInstance = {
|
||||
id: windowId,
|
||||
appId,
|
||||
config,
|
||||
state: WindowState.CREATING,
|
||||
zIndex: this.nextZIndex++,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
this.windows.set(windowId, windowInstance)
|
||||
|
||||
try {
|
||||
// 创建窗体DOM元素
|
||||
await this.createWindowElement(windowInstance)
|
||||
|
||||
// 更新状态为加载中
|
||||
this.updateWindowState(windowId, WindowState.LOADING)
|
||||
|
||||
// 模拟应用加载过程
|
||||
await this.loadApplication(windowInstance)
|
||||
|
||||
// 激活窗体
|
||||
this.updateWindowState(windowId, WindowState.ACTIVE)
|
||||
this.setActiveWindow(windowId)
|
||||
|
||||
return windowInstance
|
||||
} catch (error) {
|
||||
this.updateWindowState(windowId, WindowState.ERROR)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁窗体
|
||||
*/
|
||||
async destroyWindow(windowId: string): Promise<boolean> {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return false
|
||||
|
||||
try {
|
||||
this.updateWindowState(windowId, WindowState.CLOSING)
|
||||
|
||||
// 清理DOM元素
|
||||
if (window.element) {
|
||||
window.element.remove()
|
||||
}
|
||||
|
||||
// 从集合中移除
|
||||
this.windows.delete(windowId)
|
||||
|
||||
// 更新活跃窗体
|
||||
if (this.activeWindowId.value === windowId) {
|
||||
this.activeWindowId.value = null
|
||||
// 激活最后一个窗体
|
||||
const lastWindow = Array.from(this.windows.values()).pop()
|
||||
if (lastWindow) {
|
||||
this.setActiveWindow(lastWindow.id)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.notifyEvent('onClose', windowId)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('销毁窗体失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小化窗体
|
||||
*/
|
||||
minimizeWindow(windowId: string): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window || window.state === WindowState.MINIMIZED) return false
|
||||
|
||||
this.updateWindowState(windowId, WindowState.MINIMIZED)
|
||||
|
||||
if (window.element) {
|
||||
window.element.style.display = 'none'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 最大化窗体
|
||||
*/
|
||||
maximizeWindow(windowId: string): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window || window.state === WindowState.MAXIMIZED) return false
|
||||
|
||||
const oldState = window.state
|
||||
this.updateWindowState(windowId, WindowState.MAXIMIZED)
|
||||
|
||||
if (window.element) {
|
||||
// 保存原始尺寸和位置
|
||||
window.element.dataset.originalWidth = window.config.width.toString()
|
||||
window.element.dataset.originalHeight = window.config.height.toString()
|
||||
window.element.dataset.originalX = (window.config.x || 0).toString()
|
||||
window.element.dataset.originalY = (window.config.y || 0).toString()
|
||||
|
||||
// 设置最大化样式
|
||||
Object.assign(window.element.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)', // 减去任务栏高度
|
||||
display: 'block',
|
||||
})
|
||||
}
|
||||
|
||||
this.setActiveWindow(windowId)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原窗体
|
||||
*/
|
||||
restoreWindow(windowId: string): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return false
|
||||
|
||||
const targetState =
|
||||
window.state === WindowState.MINIMIZED
|
||||
? WindowState.ACTIVE
|
||||
: window.state === WindowState.MAXIMIZED
|
||||
? WindowState.ACTIVE
|
||||
: window.state
|
||||
|
||||
this.updateWindowState(windowId, targetState)
|
||||
|
||||
if (window.element) {
|
||||
if (window.state === WindowState.MINIMIZED) {
|
||||
window.element.style.display = 'block'
|
||||
} else if (window.state === WindowState.MAXIMIZED) {
|
||||
// 恢复原始尺寸和位置
|
||||
const originalWidth = window.element.dataset.originalWidth
|
||||
const originalHeight = window.element.dataset.originalHeight
|
||||
const originalX = window.element.dataset.originalX
|
||||
const originalY = window.element.dataset.originalY
|
||||
|
||||
Object.assign(window.element.style, {
|
||||
width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`,
|
||||
height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`,
|
||||
left: originalX ? `${originalX}px` : '50%',
|
||||
top: originalY ? `${originalY}px` : '50%',
|
||||
transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setActiveWindow(windowId)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗体标题
|
||||
*/
|
||||
setWindowTitle(windowId: string, title: string): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return false
|
||||
|
||||
window.config.title = title
|
||||
window.updatedAt = new Date()
|
||||
|
||||
// 更新DOM元素标题
|
||||
if (window.element) {
|
||||
const titleElement = window.element.querySelector('.window-title')
|
||||
if (titleElement) {
|
||||
titleElement.textContent = title
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗体尺寸
|
||||
*/
|
||||
setWindowSize(windowId: string, width: number, height: number): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return false
|
||||
|
||||
// 检查尺寸限制
|
||||
const finalWidth = Math.max(
|
||||
window.config.minWidth || 200,
|
||||
Math.min(window.config.maxWidth || Infinity, width),
|
||||
)
|
||||
const finalHeight = Math.max(
|
||||
window.config.minHeight || 150,
|
||||
Math.min(window.config.maxHeight || Infinity, height),
|
||||
)
|
||||
|
||||
window.config.width = finalWidth
|
||||
window.config.height = finalHeight
|
||||
window.updatedAt = new Date()
|
||||
|
||||
if (window.element) {
|
||||
window.element.style.width = `${finalWidth}px`
|
||||
window.element.style.height = `${finalHeight}px`
|
||||
}
|
||||
|
||||
this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取窗体实例
|
||||
*/
|
||||
getWindow(windowId: string): WindowInstance | undefined {
|
||||
return this.windows.get(windowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有窗体
|
||||
*/
|
||||
getAllWindows(): WindowInstance[] {
|
||||
return Array.from(this.windows.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃窗体ID
|
||||
*/
|
||||
getActiveWindowId(): string | null {
|
||||
return this.activeWindowId.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置活跃窗体
|
||||
*/
|
||||
setActiveWindow(windowId: string): boolean {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return false
|
||||
|
||||
this.activeWindowId.value = windowId
|
||||
window.zIndex = this.nextZIndex++
|
||||
|
||||
if (window.element) {
|
||||
window.element.style.zIndex = window.zIndex.toString()
|
||||
}
|
||||
|
||||
this.eventBus.notifyEvent('onFocus', windowId)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建窗体DOM元素
|
||||
*/
|
||||
private async createWindowElement(windowInstance: WindowInstance): Promise<void> {
|
||||
const { id, config, appId } = windowInstance
|
||||
|
||||
// 检查是否为内置应用
|
||||
let isBuiltInApp = false
|
||||
try {
|
||||
const { AppRegistry } = await import('../apps/AppRegistry')
|
||||
const appRegistry = AppRegistry.getInstance()
|
||||
isBuiltInApp = appRegistry.hasApp(appId)
|
||||
} catch (error) {
|
||||
console.warn('无法导入 AppRegistry')
|
||||
}
|
||||
|
||||
// 创建窗体容器
|
||||
const windowElement = document.createElement('div')
|
||||
windowElement.className = 'system-window'
|
||||
windowElement.id = `window-${id}`
|
||||
|
||||
// 设置基本样式
|
||||
Object.assign(windowElement.style, {
|
||||
position: 'fixed',
|
||||
width: `${config.width}px`,
|
||||
height: `${config.height}px`,
|
||||
left: config.x ? `${config.x}px` : '50%',
|
||||
top: config.y ? `${config.y}px` : '50%',
|
||||
transform: config.x && config.y ? 'none' : 'translate(-50%, -50%)',
|
||||
zIndex: windowInstance.zIndex.toString(),
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
||||
overflow: 'hidden',
|
||||
})
|
||||
|
||||
// 创建窗体标题栏
|
||||
const titleBar = this.createTitleBar(windowInstance)
|
||||
windowElement.appendChild(titleBar)
|
||||
|
||||
// 创建窗体内容区域
|
||||
const contentArea = document.createElement('div')
|
||||
contentArea.className = 'window-content'
|
||||
contentArea.style.cssText = `
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
if (isBuiltInApp) {
|
||||
// 内置应用:创建普通 div 容器,AppRenderer 组件会在这里渲染内容
|
||||
const appContainer = document.createElement('div')
|
||||
appContainer.className = 'built-in-app-container'
|
||||
appContainer.id = `app-container-${appId}`
|
||||
appContainer.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
`
|
||||
contentArea.appendChild(appContainer)
|
||||
|
||||
console.log(`[WindowService] 为内置应用 ${appId} 创建了普通容器`)
|
||||
} else {
|
||||
// 外部应用:创建 iframe 容器
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
`
|
||||
iframe.sandbox = 'allow-scripts allow-forms allow-popups' // 移除allow-same-origin以提高安全性
|
||||
contentArea.appendChild(iframe)
|
||||
|
||||
// 保存 iframe 引用(仅对外部应用)
|
||||
windowInstance.iframe = iframe
|
||||
|
||||
console.log(`[WindowService] 为外部应用 ${appId} 创建了 iframe 容器`)
|
||||
}
|
||||
|
||||
windowElement.appendChild(contentArea)
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(windowElement)
|
||||
|
||||
// 保存引用
|
||||
windowInstance.element = windowElement
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建窗体标题栏
|
||||
*/
|
||||
private createTitleBar(windowInstance: WindowInstance): HTMLElement {
|
||||
const titleBar = document.createElement('div')
|
||||
titleBar.className = 'window-title-bar'
|
||||
titleBar.style.cssText = `
|
||||
height: 40px;
|
||||
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
// 窗体标题
|
||||
const title = document.createElement('span')
|
||||
title.className = 'window-title'
|
||||
title.textContent = windowInstance.config.title
|
||||
title.style.cssText = `
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
`
|
||||
|
||||
// 控制按钮组
|
||||
const controls = document.createElement('div')
|
||||
controls.className = 'window-controls'
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
// 最小化按钮
|
||||
if (windowInstance.config.minimizable !== false) {
|
||||
const minimizeBtn = this.createControlButton('−', () => {
|
||||
this.minimizeWindow(windowInstance.id)
|
||||
})
|
||||
controls.appendChild(minimizeBtn)
|
||||
}
|
||||
|
||||
// 最大化按钮
|
||||
if (windowInstance.config.maximizable !== false) {
|
||||
const maximizeBtn = this.createControlButton('□', () => {
|
||||
if (windowInstance.state === WindowState.MAXIMIZED) {
|
||||
this.restoreWindow(windowInstance.id)
|
||||
} else {
|
||||
this.maximizeWindow(windowInstance.id)
|
||||
}
|
||||
})
|
||||
controls.appendChild(maximizeBtn)
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
if (windowInstance.config.closable !== false) {
|
||||
const closeBtn = this.createControlButton('×', () => {
|
||||
this.destroyWindow(windowInstance.id)
|
||||
})
|
||||
closeBtn.style.color = '#dc3545'
|
||||
controls.appendChild(closeBtn)
|
||||
}
|
||||
|
||||
titleBar.appendChild(title)
|
||||
titleBar.appendChild(controls)
|
||||
|
||||
// 添加拖拽功能
|
||||
if (windowInstance.config.movable !== false) {
|
||||
this.addDragFunctionality(titleBar, windowInstance)
|
||||
}
|
||||
|
||||
return titleBar
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建控制按钮
|
||||
*/
|
||||
private createControlButton(text: string, onClick: () => void): HTMLElement {
|
||||
const button = document.createElement('button')
|
||||
button.textContent = text
|
||||
button.style.cssText = `
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
`
|
||||
|
||||
button.addEventListener('click', onClick)
|
||||
|
||||
// 添加悬停效果
|
||||
button.addEventListener('mouseenter', () => {
|
||||
button.style.backgroundColor = '#e9ecef'
|
||||
})
|
||||
|
||||
button.addEventListener('mouseleave', () => {
|
||||
button.style.backgroundColor = 'transparent'
|
||||
})
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加窗体拖拽功能
|
||||
*/
|
||||
private addDragFunctionality(titleBar: HTMLElement, windowInstance: WindowInstance): void {
|
||||
let isDragging = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let startLeft = 0
|
||||
let startTop = 0
|
||||
|
||||
titleBar.addEventListener('mousedown', (e) => {
|
||||
if (!windowInstance.element) return
|
||||
|
||||
isDragging = true
|
||||
startX = e.clientX
|
||||
startY = e.clientY
|
||||
|
||||
const rect = windowInstance.element.getBoundingClientRect()
|
||||
startLeft = rect.left
|
||||
startTop = rect.top
|
||||
|
||||
// 设置为活跃窗体
|
||||
this.setActiveWindow(windowInstance.id)
|
||||
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging || !windowInstance.element) return
|
||||
|
||||
const deltaX = e.clientX - startX
|
||||
const deltaY = e.clientY - startY
|
||||
|
||||
const newLeft = startLeft + deltaX
|
||||
const newTop = startTop + deltaY
|
||||
|
||||
windowInstance.element.style.left = `${newLeft}px`
|
||||
windowInstance.element.style.top = `${newTop}px`
|
||||
windowInstance.element.style.transform = 'none'
|
||||
|
||||
// 更新配置
|
||||
windowInstance.config.x = newLeft
|
||||
windowInstance.config.y = newTop
|
||||
|
||||
this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop)
|
||||
})
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载应用
|
||||
*/
|
||||
private async loadApplication(windowInstance: WindowInstance): Promise<void> {
|
||||
// 动态导入 AppRegistry 检查是否为内置应用
|
||||
try {
|
||||
const { AppRegistry } = await import('../apps/AppRegistry')
|
||||
const appRegistry = AppRegistry.getInstance()
|
||||
|
||||
// 如果是内置应用,直接返回,不需要等待
|
||||
if (appRegistry.hasApp(windowInstance.appId)) {
|
||||
console.log(`[WindowService] 内置应用 ${windowInstance.appId} 无需等待加载`)
|
||||
return Promise.resolve()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法导入 AppRegistry,使用传统加载方式')
|
||||
}
|
||||
|
||||
// 对于外部应用,保持原有的加载逻辑
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[WindowService] 开始加载外部应用 ${windowInstance.appId}`)
|
||||
setTimeout(() => {
|
||||
if (windowInstance.iframe) {
|
||||
// 这里可以设置 iframe 的 src 来加载具体应用
|
||||
windowInstance.iframe.src = 'about:blank'
|
||||
|
||||
// 添加一些示例内容
|
||||
const doc = windowInstance.iframe.contentDocument
|
||||
if (doc) {
|
||||
doc.body.innerHTML = `
|
||||
<div style="padding: 20px; font-family: sans-serif;">
|
||||
<h2>应用: ${windowInstance.config.title}</h2>
|
||||
<p>应用ID: ${windowInstance.appId}</p>
|
||||
<p>窗体ID: ${windowInstance.id}</p>
|
||||
<p>这是一个示例应用内容。</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
console.log(`[WindowService] 外部应用 ${windowInstance.appId} 加载完成`)
|
||||
resolve()
|
||||
}, 200) // 改为200ms,即使是外部应用也不需要这么长的时间
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新窗体状态
|
||||
*/
|
||||
private updateWindowState(windowId: string, newState: WindowState): void {
|
||||
const window = this.windows.get(windowId)
|
||||
if (!window) return
|
||||
|
||||
const oldState = window.state
|
||||
|
||||
// 只有状态真正发生变化时才触发事件
|
||||
if (oldState === newState) return
|
||||
|
||||
window.state = newState
|
||||
window.updatedAt = new Date()
|
||||
|
||||
// 所有状态变化都应该触发事件,这是正常的系统行为
|
||||
console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`)
|
||||
this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user