diff --git a/.gitignore b/.gitignore index 149ad8a2..9180d8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ build/icon-1024.png .env .env.* !.env.example +.coaligne/ +.coaligneignore + diff --git a/src/main/index.ts b/src/main/index.ts index ccbdaf95..942ad8c8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,7 +17,7 @@ import { extractFailureCause, extractOffendingPlugins, HarnessRuntime } from './ import { LanMobileBridge } from './mobile/lan-mobile-bridge' import { secureWindow } from './security' import { ensureLaunchRoot } from './state/launch-root' -import { resetPluginProfile, uninstallPluginFromProfile } from './state/plugin-recovery' +import { getInstalledThirdPartyPlugins, resetPluginProfile, uninstallPluginFromProfile } from './state/plugin-recovery' import { isAbortedNavigationError, shouldLoadHarnessUrl } from './window-navigation' import { checkForUpdates, @@ -523,7 +523,13 @@ async function showRuntimeFailure(snapshot: RuntimeSnapshot): Promise { try { while (!quitting && runtime.snapshot().phase === 'failed') { snapshot = runtime.snapshot() - const offendingPlugins = extractOffendingPlugins(snapshot.logs) + let offendingPlugins = extractOffendingPlugins(snapshot.logs) + if (offendingPlugins.length === 0) { + const thirdParty = await getInstalledThirdPartyPlugins(dshHome) + if (thirdParty.length > 0) { + offendingPlugins = thirdParty + } + } const action = await waitForPluginRecoveryAction({ snapshot, plugins: offendingPlugins, diff --git a/src/main/plugin-recovery-view.ts b/src/main/plugin-recovery-view.ts index e4924131..416d64c3 100644 --- a/src/main/plugin-recovery-view.ts +++ b/src/main/plugin-recovery-view.ts @@ -62,6 +62,19 @@ export function describePluginFailure( } } + if (/duplicate loader entry id/i.test(text)) { + const entryId = text.match(/duplicate loader entry id:\s*([^\s]+)/i)?.[1] + return locale === 'zh' + ? { + title: '插件注册了重复的服务组件', + detail: `启动日志显示组件 ${entryId ? `"${entryId}"` : ''} 被重复定义,插件之间存在加载冲突,因此 Harness 无法继续启动。` + } + : { + title: 'A plugin registered a duplicate service component', + detail: `The startup log shows that component ${entryId ? `"${entryId}"` : ''} was registered more than once due to a plugin conflict.` + } + } + if (/cannot resolve profile bundle/i.test(text)) { return locale === 'zh' ? { diff --git a/src/main/state/plugin-recovery.ts b/src/main/state/plugin-recovery.ts index 17b54d1d..e11affe5 100644 --- a/src/main/state/plugin-recovery.ts +++ b/src/main/state/plugin-recovery.ts @@ -21,42 +21,40 @@ interface ProfileManifest { } } -export async function uninstallPluginFromProfile( - dshHome: string, - pluginName: string -): Promise { +const CORE_BUNDLES = new Set(['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dshmarket']) + +export async function getInstalledThirdPartyPlugins(dshHome: string): Promise { const manifestPath = profilePackageJsonPath(dshHome) - if (!existsSync(manifestPath)) return false + if (!existsSync(manifestPath)) return [] try { const raw = await readFile(manifestPath, 'utf8') const manifest = JSON.parse(raw) as ProfileManifest - let modified = false - - if (manifest.dependencies && pluginName in manifest.dependencies) { - delete manifest.dependencies[pluginName] - modified = true - } + const thirdParty = new Set() if (manifest.dsh?.profile?.bundles) { - const originalLength = manifest.dsh.profile.bundles.length - manifest.dsh.profile.bundles = manifest.dsh.profile.bundles.filter( - (bundle) => bundle !== pluginName - ) - if (manifest.dsh.profile.bundles.length !== originalLength) { - modified = true + for (const bundle of manifest.dsh.profile.bundles) { + if (!CORE_BUNDLES.has(bundle)) thirdParty.add(bundle) } } - if (modified) { - await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8') - return true + if (manifest.dependencies) { + for (const dep of Object.keys(manifest.dependencies)) { + if (!CORE_BUNDLES.has(dep)) thirdParty.add(dep) + } } + + return Array.from(thirdParty) } catch { - return false + return [] } +} - return false +export async function uninstallPluginFromProfile( + dshHome: string, + pluginName: string +): Promise { + return resetPluginProfile(dshHome, pluginName) } export async function resetPluginProfile( @@ -69,11 +67,15 @@ export async function resetPluginProfile( try { const raw = await readFile(manifestPath, 'utf8') const manifest = JSON.parse(raw) as ProfileManifest + let modified = false if (failingPlugin) { const scope = failingPlugin.startsWith('@') ? failingPlugin.split('/')[0] : undefined if (manifest.dependencies) { - delete manifest.dependencies[failingPlugin] + if (failingPlugin in manifest.dependencies) { + delete manifest.dependencies[failingPlugin] + modified = true + } for (const dep of Object.keys(manifest.dependencies)) { if ( failingPlugin.includes(dep) || @@ -81,10 +83,12 @@ export async function resetPluginProfile( (scope && dep.startsWith(scope)) ) { delete manifest.dependencies[dep] + modified = true } } } if (manifest.dsh?.profile?.bundles) { + const origLen = manifest.dsh.profile.bundles.length manifest.dsh.profile.bundles = manifest.dsh.profile.bundles.filter( (b) => b !== failingPlugin && @@ -92,25 +96,43 @@ export async function resetPluginProfile( !b.includes(failingPlugin) && (!scope || !b.startsWith(scope)) ) + if (manifest.dsh.profile.bundles.length !== origLen) { + modified = true + } } } else { - // If no specific plugin given, reset bundles to safe core bundles + // If no specific plugin given, reset to safe core bundles and clean all third-party dependencies const safeBundles = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'] if (manifest.dependencies?.dshmarket) safeBundles.push('dshmarket') - if (manifest.dsh?.profile?.bundles) { - manifest.dsh.profile.bundles = safeBundles + manifest.dsh ??= {} + manifest.dsh.profile ??= {} + manifest.dsh.profile.bundles = safeBundles + modified = true + if (manifest.dependencies) { + for (const dep of Object.keys(manifest.dependencies)) { + if (!CORE_BUNDLES.has(dep)) { + delete manifest.dependencies[dep] + modified = true + } + } } } - await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8') + if (modified) { + await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8') + } // Reset cordis.patch.yml to clean state const patchPath = profileCordisPatchPath(dshHome) if (existsSync(patchPath)) { - await writeFile(patchPath, '[]\n', 'utf8') + const patchContent = await readFile(patchPath, 'utf8') + if (patchContent.trim() !== '[]') { + await writeFile(patchPath, '[]\n', 'utf8') + modified = true + } } - return true + return modified } catch { return false } diff --git a/test/plugin-recovery-view.test.ts b/test/plugin-recovery-view.test.ts index d6bb406a..ef70a765 100644 --- a/test/plugin-recovery-view.test.ts +++ b/test/plugin-recovery-view.test.ts @@ -38,7 +38,8 @@ describe('plugin recovery view model', () => { it.each([ ['cannot resolve profile bundle example', '插件没有完整安装'], ['package declares no dsh.bundle', '安装的包不是兼容的 DSH 插件'], - ['failed to import loader entry example', '插件代码加载失败'] + ['failed to import loader entry example', '插件代码加载失败'], + ['duplicate loader entry id: storage', '插件注册了重复的服务组件'] ])('describes known startup failures: %s', (log, expectedTitle) => { expect(describePluginFailure([`[stderr] ${log}`], 'zh').title).toBe(expectedTitle) })