Files
vue-desktop/src/services/ExternalAppDiscovery.ts
2025-09-24 16:43:10 +08:00

630 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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