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()) 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 { // 防止重复启动 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 { 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() // 扫描每个应用目录 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 { 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 { 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(' { // 排除内置应用,只扫描外部应用 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 { 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 { 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 { 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): 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 { console.log('[ExternalAppDiscovery] 手动刷新应用列表') await this.scanExternalApps() } } // 导出单例实例 export const externalAppDiscovery = ExternalAppDiscovery.getInstance()