fix:正确识别已安装插件,并且修复无法更新和卸载自行安装插件的bug
This commit is contained in:
@@ -21,6 +21,7 @@ interface PluginApiResponse {
|
|||||||
id: string
|
id: string
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
@@ -56,6 +57,7 @@ function normalizePluginManifest(manifest: PluginApiResponse['manifest']): Plugi
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
manifest_version: manifest.manifest_version || 1,
|
manifest_version: manifest.manifest_version || 1,
|
||||||
|
id: manifest.id,
|
||||||
name: manifest.name,
|
name: manifest.name,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
description: manifest.description || '',
|
description: manifest.description || '',
|
||||||
@@ -104,10 +106,15 @@ export async function fetchPluginList(): Promise<ApiResponse<PluginInfo[]>> {
|
|||||||
|
|
||||||
const pluginList = data
|
const pluginList = data
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
if (!item?.id || !item?.manifest) {
|
if (!item?.manifest) {
|
||||||
console.warn('跳过无效插件数据:', item)
|
console.warn('跳过无效插件数据:', item)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
const pluginId = item.manifest.id || item.id
|
||||||
|
if (!pluginId) {
|
||||||
|
console.warn('跳过缺少 ID 的插件:', item)
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!item.manifest.name || !item.manifest.version) {
|
if (!item.manifest.name || !item.manifest.version) {
|
||||||
console.warn('跳过缺少必需字段的插件:', item.id)
|
console.warn('跳过缺少必需字段的插件:', item.id)
|
||||||
return false
|
return false
|
||||||
@@ -115,7 +122,7 @@ export async function fetchPluginList(): Promise<ApiResponse<PluginInfo[]>> {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
id: item.id,
|
id: item.manifest.id || item.id,
|
||||||
manifest: normalizePluginManifest(item.manifest),
|
manifest: normalizePluginManifest(item.manifest),
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
rating: 0,
|
rating: 0,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface InstalledPlugin {
|
|||||||
id: string
|
id: string
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
|
|||||||
@@ -104,21 +104,23 @@ export function PluginDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pluginList = JSON.parse(result.data)
|
const pluginList = JSON.parse(result.data)
|
||||||
const foundPlugin = pluginList.find((p: any) => p.id === search.pluginId)
|
const foundPlugin = pluginList.find((p: any) => (p.manifest?.id || p.id) === search.pluginId)
|
||||||
|
|
||||||
if (!foundPlugin) {
|
if (!foundPlugin) {
|
||||||
throw new Error('未找到该插件')
|
throw new Error('未找到该插件')
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawManifest = foundPlugin.manifest || {}
|
const rawManifest = foundPlugin.manifest || {}
|
||||||
|
const pluginId = rawManifest.id || foundPlugin.id
|
||||||
const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository
|
const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository
|
||||||
const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage
|
const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage
|
||||||
|
|
||||||
// 转换为 PluginInfo 格式
|
// 转换为 PluginInfo 格式
|
||||||
const pluginInfo: PluginInfo = {
|
const pluginInfo: PluginInfo = {
|
||||||
id: foundPlugin.id,
|
id: pluginId,
|
||||||
manifest: {
|
manifest: {
|
||||||
...rawManifest,
|
...rawManifest,
|
||||||
|
id: pluginId,
|
||||||
homepage_url: homepageUrl,
|
homepage_url: homepageUrl,
|
||||||
repository_url: repositoryUrl,
|
repository_url: repositoryUrl,
|
||||||
default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN',
|
default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN',
|
||||||
@@ -170,8 +172,8 @@ export function PluginDetailPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsInstalled(checkPluginInstalled(search.pluginId, installedPlugins.data))
|
setIsInstalled(checkPluginInstalled(pluginId, installedPlugins.data))
|
||||||
setInstalledVersion(getInstalledPluginVersion(search.pluginId, installedPlugins.data))
|
setInstalledVersion(getInstalledPluginVersion(pluginId, installedPlugins.data))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '加载失败')
|
setError(err instanceof Error ? err.message : '加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -196,7 +198,7 @@ export function PluginDetailPage() {
|
|||||||
// 如果插件已安装,优先尝试从本地读取 README
|
// 如果插件已安装,优先尝试从本地读取 README
|
||||||
if (isInstalled && search.pluginId) {
|
if (isInstalled && search.pluginId) {
|
||||||
try {
|
try {
|
||||||
const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${search.pluginId}`)
|
const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${plugin.id}`)
|
||||||
|
|
||||||
if (localResponse.ok) {
|
if (localResponse.ok) {
|
||||||
const localResult = await localResponse.json()
|
const localResult = await localResponse.json()
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ function PluginsPageContent() {
|
|||||||
id: installedPlugin.id,
|
id: installedPlugin.id,
|
||||||
manifest: {
|
manifest: {
|
||||||
manifest_version: installedPlugin.manifest.manifest_version || 1,
|
manifest_version: installedPlugin.manifest.manifest_version || 1,
|
||||||
|
id: installedPlugin.manifest.id || installedPlugin.id,
|
||||||
name: installedPlugin.manifest.name,
|
name: installedPlugin.manifest.name,
|
||||||
version: installedPlugin.manifest.version,
|
version: installedPlugin.manifest.version,
|
||||||
description: installedPlugin.manifest.description || '',
|
description: installedPlugin.manifest.description || '',
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface HostApplication {
|
|||||||
export interface PluginManifest {
|
export interface PluginManifest {
|
||||||
/** 清单文件版本 */
|
/** 清单文件版本 */
|
||||||
manifest_version: number
|
manifest_version: number
|
||||||
|
/** Manifest 声明的插件唯一标识 */
|
||||||
|
id?: string
|
||||||
/** 插件名称 */
|
/** 插件名称 */
|
||||||
name: string
|
name: string
|
||||||
/** 插件版本 */
|
/** 插件版本 */
|
||||||
|
|||||||
@@ -47,3 +47,17 @@ def test_installed_plugins_only_scan_plugins_dir_and_exclude_a_memorix(client: T
|
|||||||
assert ids == ["test.demo"]
|
assert ids == ["test.demo"]
|
||||||
assert "a-dawn.a-memorix" not in ids
|
assert "a-dawn.a-memorix" not in ids
|
||||||
assert all("/src/plugins/built_in/" not in plugin["path"] for plugin in payload["plugins"])
|
assert all("/src/plugins/built_in/" not in plugin["path"] for plugin in payload["plugins"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_installed_plugin_path_falls_back_to_manifest_id(client: TestClient):
|
||||||
|
plugin_path = support_module.resolve_installed_plugin_path("test.demo")
|
||||||
|
|
||||||
|
assert plugin_path is not None
|
||||||
|
assert plugin_path.name == "demo_plugin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_installed_plugin_path_accepts_manifest_id_case_mismatch(client: TestClient):
|
||||||
|
plugin_path = support_module.resolve_installed_plugin_path("Test.Demo")
|
||||||
|
|
||||||
|
assert plugin_path is not None
|
||||||
|
assert plugin_path.name == "demo_plugin"
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ def resolve_installed_plugin_path(plugin_id: str) -> Optional[Path]:
|
|||||||
return _resolve_safe_plugin_directory(new_format_path, plugins_dir, strict=True)
|
return _resolve_safe_plugin_directory(new_format_path, plugins_dir, strict=True)
|
||||||
if old_format_path.exists():
|
if old_format_path.exists():
|
||||||
return _resolve_safe_plugin_directory(old_format_path, plugins_dir, strict=True)
|
return _resolve_safe_plugin_directory(old_format_path, plugins_dir, strict=True)
|
||||||
return None
|
return find_plugin_path_by_id(plugin_id)
|
||||||
|
|
||||||
|
|
||||||
def parse_repository_url(repository_url: str) -> Tuple[str, str, str]:
|
def parse_repository_url(repository_url: str) -> Tuple[str, str, str]:
|
||||||
@@ -256,11 +256,29 @@ def iter_plugin_directories() -> List[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def find_plugin_path_by_id(plugin_id: str) -> Optional[Path]:
|
def find_plugin_path_by_id(plugin_id: str) -> Optional[Path]:
|
||||||
|
casefold_matched_path: Optional[Path] = None
|
||||||
|
normalized_plugin_id = plugin_id.casefold()
|
||||||
|
|
||||||
for plugin_path in iter_plugin_directories():
|
for plugin_path in iter_plugin_directories():
|
||||||
manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
|
manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
|
||||||
manifest = load_manifest_json(manifest_path)
|
manifest = load_manifest_json(manifest_path)
|
||||||
if manifest is not None and (manifest.get("id") == plugin_id or plugin_path.name == plugin_id):
|
if manifest is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
manifest_id = str(manifest.get("id", ""))
|
||||||
|
if manifest_id == plugin_id or plugin_path.name == plugin_id:
|
||||||
return plugin_path
|
return plugin_path
|
||||||
|
|
||||||
|
if (
|
||||||
|
casefold_matched_path is None
|
||||||
|
and (manifest_id.casefold() == normalized_plugin_id or plugin_path.name.casefold() == normalized_plugin_id)
|
||||||
|
):
|
||||||
|
casefold_matched_path = plugin_path
|
||||||
|
|
||||||
|
if casefold_matched_path is not None:
|
||||||
|
logger.warning(f"插件 ID 大小写不一致,已按大小写不敏感匹配: {plugin_id} -> {casefold_matched_path}")
|
||||||
|
return casefold_matched_path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user