From c53840006d5904f467fb964d3e521850b9d2f815 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Sun, 1 Mar 2026 19:35:36 +0800 Subject: [PATCH] refactor(lib): split plugin-api.ts into plugin-api/ directory Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- dashboard/src/lib/plugin-api.ts | 650 ------------------- dashboard/src/lib/plugin-api/config.ts | 150 +++++ dashboard/src/lib/plugin-api/index.ts | 5 + dashboard/src/lib/plugin-api/install-flow.ts | 50 ++ dashboard/src/lib/plugin-api/installed.ts | 56 ++ dashboard/src/lib/plugin-api/marketplace.ts | 252 +++++++ dashboard/src/lib/plugin-api/types.ts | 156 +++++ 7 files changed, 669 insertions(+), 650 deletions(-) delete mode 100644 dashboard/src/lib/plugin-api.ts create mode 100644 dashboard/src/lib/plugin-api/config.ts create mode 100644 dashboard/src/lib/plugin-api/index.ts create mode 100644 dashboard/src/lib/plugin-api/install-flow.ts create mode 100644 dashboard/src/lib/plugin-api/installed.ts create mode 100644 dashboard/src/lib/plugin-api/marketplace.ts create mode 100644 dashboard/src/lib/plugin-api/types.ts diff --git a/dashboard/src/lib/plugin-api.ts b/dashboard/src/lib/plugin-api.ts deleted file mode 100644 index a866bf6d..00000000 --- a/dashboard/src/lib/plugin-api.ts +++ /dev/null @@ -1,650 +0,0 @@ -import type { ApiResponse } from '@/types/api' -import type { PluginInfo } from '@/types/plugin' - -import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth' -import { parseResponse } from './api-helpers' -import { createReconnectingWebSocket } from './ws-utils' - -/** - * Git 安装状态 - */ -export interface GitStatus { - installed: boolean - version?: string - path?: string - error?: string -} - -/** - * 麦麦版本信息 - */ -export interface MaimaiVersion { - version: string - version_major: number - version_minor: number - version_patch: number -} - -/** - * 已安装插件信息 - */ -export interface InstalledPlugin { - id: string - manifest: { - manifest_version: number - name: string - version: string - description: string - author: { - name: string - url?: string - } - license: string - host_application: { - min_version: string - max_version?: string - } - homepage_url?: string - repository_url?: string - keywords?: string[] - categories?: string[] - [key: string]: unknown // 允许其他字段 - } - path: string -} - -/** - * 插件加载进度 - */ -export interface PluginLoadProgress { - operation: 'idle' | 'fetch' | 'install' | 'uninstall' | 'update' - stage: 'idle' | 'loading' | 'success' | 'error' - progress: number // 0-100 - message: string - error?: string - plugin_id?: string - total_plugins: number - loaded_plugins: number -} - -/** - * 插件仓库配置 - */ -const PLUGIN_REPO_OWNER = 'Mai-with-u' -const PLUGIN_REPO_NAME = 'plugin-repo' -const PLUGIN_REPO_BRANCH = 'main' -const PLUGIN_DETAILS_FILE = 'plugin_details.json' - -/** - * 插件列表 API 响应类型(只包含我们需要的字段) - */ -interface PluginApiResponse { - id: string - manifest: { - manifest_version: number - name: string - version: string - description: string - author: { - name: string - url?: string - } - license: string - host_application: { - min_version: string - max_version?: string - } - homepage_url?: string - repository_url?: string - keywords: string[] - categories?: string[] - default_locale: string - locales_path?: string - } - // 可能还有其他字段,但我们不关心 - [key: string]: unknown -} - -/** - * 从远程获取插件列表(通过后端代理避免 CORS) - */ -export async function fetchPluginList(): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/fetch-raw', { - method: 'POST', - body: JSON.stringify({ - owner: PLUGIN_REPO_OWNER, - repo: PLUGIN_REPO_NAME, - branch: PLUGIN_REPO_BRANCH, - file_path: PLUGIN_DETAILS_FILE - }) - }) - - const apiResult = await parseResponse<{ success: boolean; data: string; error?: string }>(response) - - if (!apiResult.success) { - return apiResult - } - - const result = apiResult.data - if (!result.success || !result.data) { - return { - success: false, - error: result.error || '获取插件列表失败' - } - } - - const data: PluginApiResponse[] = JSON.parse(result.data) - - const pluginList = data - .filter(item => { - if (!item?.id || !item?.manifest) { - console.warn('跳过无效插件数据:', item) - return false - } - if (!item.manifest.name || !item.manifest.version) { - console.warn('跳过缺少必需字段的插件:', item.id) - return false - } - return true - }) - .map((item) => ({ - id: item.id, - manifest: { - manifest_version: item.manifest.manifest_version || 1, - name: item.manifest.name, - version: item.manifest.version, - description: item.manifest.description || '', - author: item.manifest.author || { name: 'Unknown' }, - license: item.manifest.license || 'Unknown', - host_application: item.manifest.host_application || { min_version: '0.0.0' }, - homepage_url: item.manifest.homepage_url, - repository_url: item.manifest.repository_url, - keywords: item.manifest.keywords || [], - categories: item.manifest.categories || [], - default_locale: item.manifest.default_locale || 'zh-CN', - locales_path: item.manifest.locales_path, - }, - downloads: 0, - rating: 0, - review_count: 0, - installed: false, - published_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - })) - - return { - success: true, - data: pluginList - } -} - -/** - * 检查本机 Git 安装状态 - */ -export async function checkGitStatus(): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/git-status') - - const apiResult = await parseResponse(response) - - if (!apiResult.success) { - return { - success: true, - data: { - installed: false, - error: '无法检测 Git 安装状态' - } - } - } - - return apiResult -} - -/** - * 获取麦麦版本信息 - */ -export async function getMaimaiVersion(): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/version') - - const apiResult = await parseResponse(response) - - if (!apiResult.success) { - return { - success: true, - data: { - version: '0.0.0', - version_major: 0, - version_minor: 0, - version_patch: 0 - } - } - } - - return apiResult -} - -/** - * 比较版本号 - * - * @param pluginMinVersion 插件要求的最小版本 - * @param pluginMaxVersion 插件要求的最大版本(可选) - * @param maimaiVersion 麦麦当前版本 - * @returns true 表示兼容,false 表示不兼容 - */ -export function isPluginCompatible( - pluginMinVersion: string, - pluginMaxVersion: string | undefined, - maimaiVersion: MaimaiVersion -): boolean { - // 解析插件最小版本 - const minParts = pluginMinVersion.split('.').map(p => parseInt(p) || 0) - const minMajor = minParts[0] || 0 - const minMinor = minParts[1] || 0 - const minPatch = minParts[2] || 0 - - // 检查最小版本 - if (maimaiVersion.version_major < minMajor) return false - if (maimaiVersion.version_major === minMajor && maimaiVersion.version_minor < minMinor) return false - if (maimaiVersion.version_major === minMajor && - maimaiVersion.version_minor === minMinor && - maimaiVersion.version_patch < minPatch) return false - - // 检查最大版本(如果有) - if (pluginMaxVersion) { - const maxParts = pluginMaxVersion.split('.').map(p => parseInt(p) || 0) - const maxMajor = maxParts[0] || 0 - const maxMinor = maxParts[1] || 0 - const maxPatch = maxParts[2] || 0 - - if (maimaiVersion.version_major > maxMajor) return false - if (maimaiVersion.version_major === maxMajor && maimaiVersion.version_minor > maxMinor) return false - if (maimaiVersion.version_major === maxMajor && - maimaiVersion.version_minor === maxMinor && - maimaiVersion.version_patch > maxPatch) return false - } - - return true -} - -/** - * 连接插件加载进度 WebSocket - * - * 使用临时 token 进行认证,异步获取 token 后连接 - */ -export async function connectPluginProgressWebSocket( - onProgress: (progress: PluginLoadProgress) => void, - onError?: (error: Event) => void -): Promise { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' - const host = window.location.host - const wsUrl = `${protocol}//${host}/api/webui/ws/plugin-progress` - - // 使用 ws-utils 创建 WebSocket - const wsControl = createReconnectingWebSocket(wsUrl, { - onMessage: (data: string) => { - try { - const progressData = JSON.parse(data) as PluginLoadProgress - onProgress(progressData) - } catch (error) { - console.error('Failed to parse progress data:', error) - } - }, - onOpen: () => { - console.log('Plugin progress WebSocket connected') - }, - onClose: () => { - console.log('Plugin progress WebSocket disconnected') - }, - onError: (error) => { - console.error('Plugin progress WebSocket error:', error) - onError?.(error) - }, - heartbeatInterval: 30000, - maxRetries: 10, - backoffBase: 1000, - maxBackoff: 30000, - }) - - // 启动连接 - await wsControl.connect() - - // 返回 WebSocket 实例(用于外部检查连接状态) - return wsControl.getWebSocket() -} - -/** - * 获取已安装插件列表 - */ -export async function getInstalledPlugins(): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/installed', { - headers: getAuthHeaders() - }) - - const apiResult = await parseResponse<{ success: boolean; plugins?: InstalledPlugin[]; message?: string }>(response) - - if (!apiResult.success) { - return { - success: true, - data: [] - } - } - - const result = apiResult.data - if (!result.success) { - return { - success: true, - data: [] - } - } - - return { - success: true, - data: result.plugins || [] - } -} - -/** - * 检查插件是否已安装 - */ -export function checkPluginInstalled(pluginId: string, installedPlugins: InstalledPlugin[]): boolean { - return installedPlugins.some(p => p.id === pluginId) -} - -/** - * 获取已安装插件的版本 - */ -export function getInstalledPluginVersion(pluginId: string, installedPlugins: InstalledPlugin[]): string | undefined { - const plugin = installedPlugins.find(p => p.id === pluginId) - if (!plugin) return undefined - - // 兼容两种格式:新格式有 manifest,旧格式直接有 version - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return plugin.manifest?.version || (plugin as any).version -} - -/** - * 安装插件 - */ -export async function installPlugin(pluginId: string, repositoryUrl: string, branch: string = 'main'): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/install', { - method: 'POST', - body: JSON.stringify({ - plugin_id: pluginId, - repository_url: repositoryUrl, - branch: branch - }) - }) - - return await parseResponse<{ success: boolean; message: string }>(response) -} - -/** - * 卸载插件 - */ -export async function uninstallPlugin(pluginId: string): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/uninstall', { - method: 'POST', - body: JSON.stringify({ - plugin_id: pluginId - }) - }) - - return await parseResponse<{ success: boolean; message: string }>(response) -} - -/** - * 更新插件 - */ -export async function updatePlugin(pluginId: string, repositoryUrl: string, branch: string = 'main'): Promise> { - const response = await fetchWithAuth('/api/webui/plugins/update', { - method: 'POST', - body: JSON.stringify({ - plugin_id: pluginId, - repository_url: repositoryUrl, - branch: branch - }) - }) - - return await parseResponse<{ success: boolean; message: string; old_version: string; new_version: string }>(response) -} - - -// ============ 插件配置管理 ============ - -/** - * 列表项字段定义(用于 object 类型的数组项) - */ -export interface ItemFieldDefinition { - type: string - label?: string - placeholder?: string - default?: unknown -} - -/** - * 配置字段定义 - */ -export interface ConfigFieldSchema { - name: string - type: string - default: unknown - description: string - example?: string - required: boolean - choices?: unknown[] - min?: number - max?: number - step?: number - pattern?: string - max_length?: number - label: string - placeholder?: string - hint?: string - icon?: string - hidden: boolean - disabled: boolean - order: number - input_type?: string - ui_type: string - rows?: number - group?: string - depends_on?: string - depends_value?: unknown - // 列表类型专用 - item_type?: string // "string" | "number" | "object" - item_fields?: Record - min_items?: number - max_items?: number -} - -/** - * 配置节定义 - */ -export interface ConfigSectionSchema { - name: string - title: string - description?: string - icon?: string - collapsed: boolean - order: number - fields: Record -} - -/** - * 配置标签页定义 - */ -export interface ConfigTabSchema { - id: string - title: string - sections: string[] - icon?: string - order: number - badge?: string -} - -/** - * 配置布局定义 - */ -export interface ConfigLayoutSchema { - type: 'auto' | 'tabs' | 'pages' - tabs: ConfigTabSchema[] -} - -/** - * 插件配置 Schema - */ -export interface PluginConfigSchema { - plugin_id: string - plugin_info: { - name: string - version: string - description: string - author: string - } - sections: Record - layout: ConfigLayoutSchema - _note?: string -} - -/** - * 获取插件配置 Schema - */ -export async function getPluginConfigSchema(pluginId: string): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/schema`, { - headers: getAuthHeaders() - }) - - const apiResult = await parseResponse<{ success: boolean; schema?: PluginConfigSchema; message?: string }>(response) - - if (!apiResult.success) { - return apiResult - } - - const result = apiResult.data - if (!result.success || !result.schema) { - return { - success: false, - error: result.message || '获取配置 Schema 失败' - } - } - - return { - success: true, - data: result.schema - } -} - -/** - * 获取插件当前配置值 - */ -export async function getPluginConfig(pluginId: string): Promise>> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}`, { - headers: getAuthHeaders() - }) - - const apiResult = await parseResponse<{ success: boolean; config?: Record; message?: string }>(response) - - if (!apiResult.success) { - return apiResult - } - - const result = apiResult.data - if (!result.success || !result.config) { - return { - success: false, - error: result.message || '获取配置失败' - } - } - - return { - success: true, - data: result.config - } -} - -/** - * 获取插件原始 TOML 配置 - */ -export async function getPluginConfigRaw(pluginId: string): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/raw`, { - headers: getAuthHeaders() - }) - - const apiResult = await parseResponse<{ success: boolean; config?: string; message?: string }>(response) - - if (!apiResult.success) { - return apiResult - } - - const result = apiResult.data - if (!result.success || !result.config) { - return { - success: false, - error: result.message || '获取配置失败' - } - } - - return { - success: true, - data: result.config - } -} - -/** - * 更新插件配置 - */ -export async function updatePluginConfig( - pluginId: string, - config: Record -): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}`, { - method: 'PUT', - headers: getAuthHeaders(), - body: JSON.stringify({ config }) - }) - - return await parseResponse<{ success: boolean; message: string; note?: string }>(response) -} - -/** - * 更新插件原始 TOML 配置 - */ -export async function updatePluginConfigRaw( - pluginId: string, - configToml: string -): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/raw`, { - method: 'PUT', - headers: getAuthHeaders(), - body: JSON.stringify({ config: configToml }) - }) - - return await parseResponse<{ success: boolean; message: string; note?: string }>(response) -} - -/** - * 重置插件配置为默认值 - */ -export async function resetPluginConfig( - pluginId: string -): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/reset`, { - method: 'POST', - headers: getAuthHeaders() - }) - - return await parseResponse<{ success: boolean; message: string; backup?: string }>(response) -} - -/** - * 切换插件启用状态 - */ -export async function togglePlugin( - pluginId: string -): Promise> { - const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/toggle`, { - method: 'POST', - headers: getAuthHeaders() - }) - - return await parseResponse<{ success: boolean; enabled: boolean; message: string; note?: string }>(response) -} diff --git a/dashboard/src/lib/plugin-api/config.ts b/dashboard/src/lib/plugin-api/config.ts new file mode 100644 index 00000000..0b37de72 --- /dev/null +++ b/dashboard/src/lib/plugin-api/config.ts @@ -0,0 +1,150 @@ +import type { ApiResponse } from '@/types/api' + +import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth' +import { parseResponse } from '@/lib/api-helpers' + +import type { PluginConfigSchema } from './types' + +/** + * 获取插件配置 Schema + */ +export async function getPluginConfigSchema(pluginId: string): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/schema`, { + headers: getAuthHeaders() + }) + + const apiResult = await parseResponse<{ success: boolean; schema?: PluginConfigSchema; message?: string }>(response) + + if (!apiResult.success) { + return apiResult + } + + const result = apiResult.data + if (!result.success || !result.schema) { + return { + success: false, + error: result.message || '获取配置 Schema 失败' + } + } + + return { + success: true, + data: result.schema + } +} + +/** + * 获取插件当前配置值 + */ +export async function getPluginConfig(pluginId: string): Promise>> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}`, { + headers: getAuthHeaders() + }) + + const apiResult = await parseResponse<{ success: boolean; config?: Record; message?: string }>(response) + + if (!apiResult.success) { + return apiResult + } + + const result = apiResult.data + if (!result.success || !result.config) { + return { + success: false, + error: result.message || '获取配置失败' + } + } + + return { + success: true, + data: result.config + } +} + +/** + * 获取插件原始 TOML 配置 + */ +export async function getPluginConfigRaw(pluginId: string): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/raw`, { + headers: getAuthHeaders() + }) + + const apiResult = await parseResponse<{ success: boolean; config?: string; message?: string }>(response) + + if (!apiResult.success) { + return apiResult + } + + const result = apiResult.data + if (!result.success || !result.config) { + return { + success: false, + error: result.message || '获取配置失败' + } + } + + return { + success: true, + data: result.config + } +} + +/** + * 更新插件配置 + */ +export async function updatePluginConfig( + pluginId: string, + config: Record +): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify({ config }) + }) + + return await parseResponse<{ success: boolean; message: string; note?: string }>(response) +} + +/** + * 更新插件原始 TOML 配置 + */ +export async function updatePluginConfigRaw( + pluginId: string, + configToml: string +): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/raw`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify({ config: configToml }) + }) + + return await parseResponse<{ success: boolean; message: string; note?: string }>(response) +} + +/** + * 重置插件配置为默认值 + */ +export async function resetPluginConfig( + pluginId: string +): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/reset`, { + method: 'POST', + headers: getAuthHeaders() + }) + + return await parseResponse<{ success: boolean; message: string; backup?: string }>(response) +} + +/** + * 切换插件启用状态 + */ +export async function togglePlugin( + pluginId: string +): Promise> { + const response = await fetchWithAuth(`/api/webui/plugins/config/${pluginId}/toggle`, { + method: 'POST', + headers: getAuthHeaders() + }) + + return await parseResponse<{ success: boolean; enabled: boolean; message: string; note?: string }>(response) +} diff --git a/dashboard/src/lib/plugin-api/index.ts b/dashboard/src/lib/plugin-api/index.ts new file mode 100644 index 00000000..963acaf1 --- /dev/null +++ b/dashboard/src/lib/plugin-api/index.ts @@ -0,0 +1,5 @@ +export * from './types' +export * from './marketplace' +export * from './installed' +export * from './install-flow' +export * from './config' diff --git a/dashboard/src/lib/plugin-api/install-flow.ts b/dashboard/src/lib/plugin-api/install-flow.ts new file mode 100644 index 00000000..d01c0959 --- /dev/null +++ b/dashboard/src/lib/plugin-api/install-flow.ts @@ -0,0 +1,50 @@ +import type { ApiResponse } from '@/types/api' + +import { fetchWithAuth } from '@/lib/fetch-with-auth' +import { parseResponse } from '@/lib/api-helpers' + +/** + * 安装插件 + */ +export async function installPlugin(pluginId: string, repositoryUrl: string, branch: string = 'main'): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/install', { + method: 'POST', + body: JSON.stringify({ + plugin_id: pluginId, + repository_url: repositoryUrl, + branch: branch + }) + }) + + return await parseResponse<{ success: boolean; message: string }>(response) +} + +/** + * 卸载插件 + */ +export async function uninstallPlugin(pluginId: string): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/uninstall', { + method: 'POST', + body: JSON.stringify({ + plugin_id: pluginId + }) + }) + + return await parseResponse<{ success: boolean; message: string }>(response) +} + +/** + * 更新插件 + */ +export async function updatePlugin(pluginId: string, repositoryUrl: string, branch: string = 'main'): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/update', { + method: 'POST', + body: JSON.stringify({ + plugin_id: pluginId, + repository_url: repositoryUrl, + branch: branch + }) + }) + + return await parseResponse<{ success: boolean; message: string; old_version: string; new_version: string }>(response) +} diff --git a/dashboard/src/lib/plugin-api/installed.ts b/dashboard/src/lib/plugin-api/installed.ts new file mode 100644 index 00000000..79e077c1 --- /dev/null +++ b/dashboard/src/lib/plugin-api/installed.ts @@ -0,0 +1,56 @@ +import type { ApiResponse } from '@/types/api' + +import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth' +import { parseResponse } from '@/lib/api-helpers' + +import type { InstalledPlugin } from './types' + +/** + * 获取已安装插件列表 + */ +export async function getInstalledPlugins(): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/installed', { + headers: getAuthHeaders() + }) + + const apiResult = await parseResponse<{ success: boolean; plugins?: InstalledPlugin[]; message?: string }>(response) + + if (!apiResult.success) { + return { + success: true, + data: [] + } + } + + const result = apiResult.data + if (!result.success) { + return { + success: true, + data: [] + } + } + + return { + success: true, + data: result.plugins || [] + } +} + +/** + * 检查插件是否已安装 + */ +export function checkPluginInstalled(pluginId: string, installedPlugins: InstalledPlugin[]): boolean { + return installedPlugins.some(p => p.id === pluginId) +} + +/** + * 获取已安装插件的版本 + */ +export function getInstalledPluginVersion(pluginId: string, installedPlugins: InstalledPlugin[]): string | undefined { + const plugin = installedPlugins.find(p => p.id === pluginId) + if (!plugin) return undefined + + // 兼容两种格式:新格式有 manifest,旧格式直接有 version + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return plugin.manifest?.version || (plugin as any).version +} diff --git a/dashboard/src/lib/plugin-api/marketplace.ts b/dashboard/src/lib/plugin-api/marketplace.ts new file mode 100644 index 00000000..026e4457 --- /dev/null +++ b/dashboard/src/lib/plugin-api/marketplace.ts @@ -0,0 +1,252 @@ +import type { ApiResponse } from '@/types/api' +import type { PluginInfo } from '@/types/plugin' + +import { fetchWithAuth } from '@/lib/fetch-with-auth' +import { parseResponse } from '@/lib/api-helpers' + +import type { GitStatus, MaimaiVersion } from './types' + +/** + * 插件仓库配置 + */ +const PLUGIN_REPO_OWNER = 'Mai-with-u' +const PLUGIN_REPO_NAME = 'plugin-repo' +const PLUGIN_REPO_BRANCH = 'main' +const PLUGIN_DETAILS_FILE = 'plugin_details.json' + +/** + * 插件列表 API 响应类型(只包含我们需要的字段) + */ +interface PluginApiResponse { + id: string + manifest: { + manifest_version: number + name: string + version: string + description: string + author: { + name: string + url?: string + } + license: string + host_application: { + min_version: string + max_version?: string + } + homepage_url?: string + repository_url?: string + keywords: string[] + categories?: string[] + default_locale: string + locales_path?: string + } + // 可能还有其他字段,但我们不关心 + [key: string]: unknown +} + +/** + * 从远程获取插件列表(通过后端代理避免 CORS) + */ +export async function fetchPluginList(): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/fetch-raw', { + method: 'POST', + body: JSON.stringify({ + owner: PLUGIN_REPO_OWNER, + repo: PLUGIN_REPO_NAME, + branch: PLUGIN_REPO_BRANCH, + file_path: PLUGIN_DETAILS_FILE + }) + }) + + const apiResult = await parseResponse<{ success: boolean; data: string; error?: string }>(response) + + if (!apiResult.success) { + return apiResult + } + + const result = apiResult.data + if (!result.success || !result.data) { + return { + success: false, + error: result.error || '获取插件列表失败' + } + } + + const data: PluginApiResponse[] = JSON.parse(result.data) + + const pluginList = data + .filter(item => { + if (!item?.id || !item?.manifest) { + console.warn('跳过无效插件数据:', item) + return false + } + if (!item.manifest.name || !item.manifest.version) { + console.warn('跳过缺少必需字段的插件:', item.id) + return false + } + return true + }) + .map((item) => ({ + id: item.id, + manifest: { + manifest_version: item.manifest.manifest_version || 1, + name: item.manifest.name, + version: item.manifest.version, + description: item.manifest.description || '', + author: item.manifest.author || { name: 'Unknown' }, + license: item.manifest.license || 'Unknown', + host_application: item.manifest.host_application || { min_version: '0.0.0' }, + homepage_url: item.manifest.homepage_url, + repository_url: item.manifest.repository_url, + keywords: item.manifest.keywords || [], + categories: item.manifest.categories || [], + default_locale: item.manifest.default_locale || 'zh-CN', + locales_path: item.manifest.locales_path, + }, + downloads: 0, + rating: 0, + review_count: 0, + installed: false, + published_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + })) + + return { + success: true, + data: pluginList + } +} + +/** + * 检查本机 Git 安装状态 + */ +export async function checkGitStatus(): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/git-status') + + const apiResult = await parseResponse(response) + + if (!apiResult.success) { + return { + success: true, + data: { + installed: false, + error: '无法检测 Git 安装状态' + } + } + } + + return apiResult +} + +/** + * 获取麦麦版本信息 + */ +export async function getMaimaiVersion(): Promise> { + const response = await fetchWithAuth('/api/webui/plugins/version') + + const apiResult = await parseResponse(response) + + if (!apiResult.success) { + return { + success: true, + data: { + version: '0.0.0', + version_major: 0, + version_minor: 0, + version_patch: 0 + } + } + } + + return apiResult +} + +/** + * 比较版本号 + * + * @param pluginMinVersion 插件要求的最小版本 + * @param pluginMaxVersion 插件要求的最大版本(可选) + * @param maimaiVersion 麦麦当前版本 + * @returns true 表示兼容,false 表示不兼容 + */ +export function isPluginCompatible( + pluginMinVersion: string, + pluginMaxVersion: string | undefined, + maimaiVersion: MaimaiVersion +): boolean { + // 解析插件最小版本 + const minParts = pluginMinVersion.split('.').map(p => parseInt(p) || 0) + const minMajor = minParts[0] || 0 + const minMinor = minParts[1] || 0 + const minPatch = minParts[2] || 0 + + // 检查最小版本 + if (maimaiVersion.version_major < minMajor) return false + if (maimaiVersion.version_major === minMajor && maimaiVersion.version_minor < minMinor) return false + if (maimaiVersion.version_major === minMajor && + maimaiVersion.version_minor === minMinor && + maimaiVersion.version_patch < minPatch) return false + + // 检查最大版本(如果有) + if (pluginMaxVersion) { + const maxParts = pluginMaxVersion.split('.').map(p => parseInt(p) || 0) + const maxMajor = maxParts[0] || 0 + const maxMinor = maxParts[1] || 0 + const maxPatch = maxParts[2] || 0 + + if (maimaiVersion.version_major > maxMajor) return false + if (maimaiVersion.version_major === maxMajor && maimaiVersion.version_minor > maxMinor) return false + if (maimaiVersion.version_major === maxMajor && + maimaiVersion.version_minor === maxMinor && + maimaiVersion.version_patch > maxPatch) return false + } + + return true +} + +/** + * 连接插件加载进度 WebSocket + * + * 使用临时 token 进行认证,异步获取 token 后连接 + */ +export async function connectPluginProgressWebSocket( + onProgress: (progress: import('./types').PluginLoadProgress) => void, + onError?: (error: Event) => void +): Promise { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + const wsUrl = `${protocol}//${host}/api/webui/ws/plugin-progress` + + // 使用 ws-utils 创建 WebSocket + const { createReconnectingWebSocket } = await import('@/lib/ws-utils') + const wsControl = createReconnectingWebSocket(wsUrl, { + onMessage: (data: string) => { + try { + const progressData = JSON.parse(data) as import('./types').PluginLoadProgress + onProgress(progressData) + } catch (error) { + console.error('Failed to parse progress data:', error) + } + }, + onOpen: () => { + console.log('Plugin progress WebSocket connected') + }, + onClose: () => { + console.log('Plugin progress WebSocket disconnected') + }, + onError: (error) => { + console.error('Plugin progress WebSocket error:', error) + onError?.(error) + }, + heartbeatInterval: 30000, + maxRetries: 10, + backoffBase: 1000, + maxBackoff: 30000, + }) + + // 启动连接 + await wsControl.connect() + + // 返回 WebSocket 实例(用于外部检查连接状态) + return wsControl.getWebSocket() +} diff --git a/dashboard/src/lib/plugin-api/types.ts b/dashboard/src/lib/plugin-api/types.ts new file mode 100644 index 00000000..c51ec6bb --- /dev/null +++ b/dashboard/src/lib/plugin-api/types.ts @@ -0,0 +1,156 @@ +/** + * Git 安装状态 + */ +export interface GitStatus { + installed: boolean + version?: string + path?: string + error?: string +} + +/** + * 麦麦版本信息 + */ +export interface MaimaiVersion { + version: string + version_major: number + version_minor: number + version_patch: number +} + +/** + * 已安装插件信息 + */ +export interface InstalledPlugin { + id: string + manifest: { + manifest_version: number + name: string + version: string + description: string + author: { + name: string + url?: string + } + license: string + host_application: { + min_version: string + max_version?: string + } + homepage_url?: string + repository_url?: string + keywords?: string[] + categories?: string[] + [key: string]: unknown // 允许其他字段 + } + path: string +} + +/** + * 插件加载进度 + */ +export interface PluginLoadProgress { + operation: 'idle' | 'fetch' | 'install' | 'uninstall' | 'update' + stage: 'idle' | 'loading' | 'success' | 'error' + progress: number // 0-100 + message: string + error?: string + plugin_id?: string + total_plugins: number + loaded_plugins: number +} + +/** + * 列表项字段定义(用于 object 类型的数组项) + */ +export interface ItemFieldDefinition { + type: string + label?: string + placeholder?: string + default?: unknown +} + +/** + * 配置字段定义 + */ +export interface ConfigFieldSchema { + name: string + type: string + default: unknown + description: string + example?: string + required: boolean + choices?: unknown[] + min?: number + max?: number + step?: number + pattern?: string + max_length?: number + label: string + placeholder?: string + hint?: string + icon?: string + hidden: boolean + disabled: boolean + order: number + input_type?: string + ui_type: string + rows?: number + group?: string + depends_on?: string + depends_value?: unknown + // 列表类型专用 + item_type?: string // "string" | "number" | "object" + item_fields?: Record + min_items?: number + max_items?: number +} + +/** + * 配置节定义 + */ +export interface ConfigSectionSchema { + name: string + title: string + description?: string + icon?: string + collapsed: boolean + order: number + fields: Record +} + +/** + * 配置标签页定义 + */ +export interface ConfigTabSchema { + id: string + title: string + sections: string[] + icon?: string + order: number + badge?: string +} + +/** + * 配置布局定义 + */ +export interface ConfigLayoutSchema { + type: 'auto' | 'tabs' | 'pages' + tabs: ConfigTabSchema[] +} + +/** + * 插件配置 Schema + */ +export interface PluginConfigSchema { + plugin_id: string + plugin_info: { + name: string + version: string + description: string + author: string + } + sections: Record + layout: ConfigLayoutSchema + _note?: string +}