630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
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()
|