{"record":{"id":"c56a22a4e0f11887","repo":"mihomo-party-org/clash-party","slug":"plugin-not-found-c56a22","errorCode":null,"errorMessage":"Plugin not found","messagePattern":"Plugin not found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/main/resolve/plugin/index.ts","lineNumber":148,"sourceCode":"  return new Error('PLUGIN_LOGIN_FAILED')\n}\n\n// 登录（首次登录与重新认证同一入口）。对外抛错经 sanitizeLoginError 脱敏。\nexport async function loginPlugin(id: string): Promise<void> {\n  try {\n    await runLogin(id)\n  } catch (e) {\n    throw sanitizeLoginError(e)\n  }\n}\n\n// 设备复用仅限「needs-login 且已有 vault」这一种情形：上次 enroll 成功但首份订阅拉取失败留下的\n// “孤儿设备”，重拉即可，避免每次重试都 enroll 新设备、消耗服务端设备数上限。\n// 其它情形——needs-reauth（显式重新登录）、active（刷新）、无 vault（首装/换机/Linux 无 safeStorage）——\n// 一律走全新浏览器登录 + 新设备，与 spec §9「reauth = 再走一次 login 流程、新设备密钥」一致。\nasync function runLogin(id: string): Promise<void> {\n  const record = await getPluginItem(id)\n  if (!record) throw new Error('Plugin not found')\n  const net = await netOpts(record)\n\n  const existingResult = await readVault(id)\n  if (existingResult.kind === 'unavailable') throw new VaultUnavailableError()\n  const existing = existingResult.kind === 'ok' ? existingResult.vault : undefined\n  if (existing && record.status === 'needs-login') {\n    try {\n      const content = await fetchWithRediscovery(id, record, existing, net)\n      await finishLogin(id, record, content)\n      return\n    } catch (e) {\n      // 孤儿设备已被吊销 → 丢弃旧 vault，落到下面的全新浏览器登录 + 新设备\n      if (!(e instanceof GatewayError && e.kind === 'revoked')) throw e\n      await removeVault(id)\n    }\n  }\n\n  // 先确认 Keychain/secret store 可以实际加密，再打开 OAuth 和 enroll，避免用户完成","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/main/resolve/plugin/index.ts#L130-L166","documentation":"runLogin looks up the plugin record by id via getPluginItem and throws 'Plugin not found' when no record exists for that id. Login (including device-reuse fast path) can only proceed for an already-installed/enrolled plugin. This is an identity/lookup failure, not a network failure.","triggerScenarios":"Calling loginPlugin(id) (or runLogin) with an id that was never installed, was uninstalled, or has a different id spelling — e.g. a stale id held by UI state after the plugin record was removed.","commonSituations":"Retrying login from a cached list after uninstalling the plugin; typo or casing mismatch in the plugin id; the plugin store was reset/cleared between sessions.","solutions":["List installed plugins and confirm the id exists before calling login (match against getPluginItem results).","Re-install/enroll the plugin if the record was removed, then login.","Fix the id being passed — trim whitespace, correct casing, use the canonical id from the plugin record."],"exampleFix":"// before\nawait loginPlugin(staleIdFromUi)\n// after\nconst record = await getPluginItem(id)\nif (!record) await installPlugin(descriptor) // re-install before login\nawait loginPlugin(id)","handlingStrategy":"try-catch","validationCode":"const record = await getPluginItem(id)\nif (!record) throw new Error(`plugin ${id} is not installed — install before login`)","typeGuard":"const isInstalled = async (id: string): Promise<boolean> => (await getPluginItem(id)) != null","tryCatchPattern":"try {\n  await loginPlugin(id)\n} catch (e) {\n  if (e.message === 'Plugin not found') {\n    // refresh plugin list / re-install, then retry once\n  } else throw e\n}","preventionTips":["Refresh the UI plugin list after any uninstall so stale ids are not reused","Validate the id against getPluginItem before login","Treat plugin records as volatile — handle removal between sessions","Use canonical ids from the record, not user-typed strings"],"tags":["plugin","not-found","lookup","state"],"backgroundTag":"resource-not-found","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}