This commit is contained in:
2025-09-24 16:43:10 +08:00
parent 12f46e6f8e
commit 9dbc054483
130 changed files with 16474 additions and 4660 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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()

View 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)
}
}
})
}
}

View 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)
}
}
}

View 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)
}
}