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

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