Files
claude-code-mirror/claude-code源码-中文注释/src/hooks/useManagePlugins.ts
2026-04-03 13:01:19 +08:00

302 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect } from 'react'
import type { Command } from '../commands.js'
import { useNotifications } from '../context/notifications.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import { reinitializeLspServerManager } from '../services/lsp/manager.js'
import { useAppState, useSetAppState } from '../state/AppState.js'
import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js'
import { count } from '../utils/array.js'
import { logForDebugging } from '../utils/debug.js'
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
import { toError } from '../utils/errors.js'
import { logError } from '../utils/log.js'
import { loadPluginAgents } from '../utils/plugins/loadPluginAgents.js'
import { getPluginCommands } from '../utils/plugins/loadPluginCommands.js'
import { loadPluginHooks } from '../utils/plugins/loadPluginHooks.js'
import { loadPluginLspServers } from '../utils/plugins/lspPluginIntegration.js'
import { loadPluginMcpServers } from '../utils/plugins/mcpPluginIntegration.js'
import { detectAndUninstallDelistedPlugins } from '../utils/plugins/pluginBlocklist.js'
import { getFlaggedPlugins } from '../utils/plugins/pluginFlagging.js'
import { loadAllPlugins } from '../utils/plugins/pluginLoader.js'
/**
* 管理插件状态并与 AppState 同步的 Hook。
*
* 挂载时:加载所有插件,运行除名 enforcement显示标记插件
* 通知,填充 AppState.plugins。这是初始 Layer-3 加载 —
* 后续刷新通过 /reload-plugins 进行。
*
* 需要刷新时:显示通知引导用户运行 /reload-plugins。
* 不会自动刷新。所有 Layer-3 交换命令、代理、hooks、MCP
* 都通过 refreshActivePlugins() 进行,以保持一致的心智模型。
* 参见 Outline: declarative-settings-hXHBMDIf4b PR 5c。
*/
export function useManagePlugins({
enabled = true,
}: {
enabled?: boolean
} = {}) {
const setAppState = useSetAppState()
const needsRefresh = useAppState(s => s.plugins.needsRefresh)
const { addNotification } = useNotifications()
// 初始插件加载。挂载时运行一次。不用于刷新 —
// 所有挂载后刷新都通过 /reload-plugins → refreshActivePlugins() 进行。
// 与 refreshActivePlugins 不同,这也运行除名 enforcement 和
// 标记插件通知(会话启动关注点),并且不会增加
// mcp.pluginReconnectKeyMCP 效果在自己的挂载上触发)。
const initialPluginLoad = useCallback(async () => {
try {
// 加载所有插件 - 捕获错误数组
const { enabled, disabled, errors } = await loadAllPlugins()
// 检测已除名的插件,自动卸载并记录为标记。
await detectAndUninstallDelistedPlugins()
// 如果有标记的插件待处理则通知
const flagged = getFlaggedPlugins()
if (Object.keys(flagged).length > 0) {
addNotification({
key: 'plugin-delisted-flagged',
text: 'Plugins flagged. Check /plugins',
color: 'warning',
priority: 'high',
})
}
// 加载命令、代理和 hooks带有单独的錯誤處理
// 错误被添加到错误数组中,以便在 Doctor UI 中对用户可见
let commands: Command[] = []
let agents: AgentDefinition[] = []
try {
commands = await getPluginCommands()
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
errors.push({
type: 'generic-error',
source: 'plugin-commands',
error: `Failed to load plugin commands: ${errorMessage}`,
})
}
try {
agents = await loadPluginAgents()
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
errors.push({
type: 'generic-error',
source: 'plugin-agents',
error: `Failed to load plugin agents: ${errorMessage}`,
})
}
try {
await loadPluginHooks()
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
errors.push({
type: 'generic-error',
source: 'plugin-hooks',
error: `Failed to load plugin hooks: ${errorMessage}`,
})
}
// 加载每个插件的 MCP 服务器配置以获取准确计数。
// LoadedPlugin.mcpServers 不是由 loadAllPlugins 填充的 — 它是一个
// 缓存槽extractMcpServersFromPlugins 稍后填充,这与这个指标竞争。
// 直接调用 loadPluginMcpServers如同 cli/handlers/plugins.ts 所做)
// 可以获得正确的计数,并且还可以为 MCP 连接管理器预热缓存。
//
// 在 setAppState 之前运行,以便这些加载器推送的错误进入
// AppState.plugins.errorsDoctor UI而不仅仅是遥测。
const mcpServerCounts = await Promise.all(
enabled.map(async p => {
if (p.mcpServers) return Object.keys(p.mcpServers).length
const servers = await loadPluginMcpServers(p, errors)
if (servers) p.mcpServers = servers
return servers ? Object.keys(servers).length : 0
}),
)
const mcp_count = mcpServerCounts.reduce((sum, n) => sum + n, 0)
// LSP#15521 的主要修复在 refresh.ts通过
// performBackgroundPluginInstallations → refreshActivePlugins
// 它首先清除缓存)。这个重新初始化是防御性的 — 它读取与原始初始化
// 相同的 memoized loadAllPlugins() 结果,除非在 main.tsx:3203 和
// REPL 挂载之间发生了缓存失效(例如 seed marketplace 注册或
// policySettings 热重载)。
const lspServerCounts = await Promise.all(
enabled.map(async p => {
if (p.lspServers) return Object.keys(p.lspServers).length
const servers = await loadPluginLspServers(p, errors)
if (servers) p.lspServers = servers
return servers ? Object.keys(servers).length : 0
}),
)
const lsp_count = lspServerCounts.reduce((sum, n) => sum + n, 0)
reinitializeLspServerManager()
// 更新 AppState - 合并错误以保留 LSP 错误
setAppState(prevState => {
// 保留现有的 LSP/非插件加载错误source 为 'lsp-manager' 或 'plugin:*'
const existingLspErrors = prevState.plugins.errors.filter(
e => e.source === 'lsp-manager' || e.source.startsWith('plugin:'),
)
// 去重:删除也存在于新错误中的现有 LSP 错误
const newErrorKeys = new Set(
errors.map(e =>
e.type === 'generic-error'
? `generic-error:${e.source}:${e.error}`
: `${e.type}:${e.source}`,
),
)
const filteredExisting = existingLspErrors.filter(e => {
const key =
e.type === 'generic-error'
? `generic-error:${e.source}:${e.error}`
: `${e.type}:${e.source}`
return !newErrorKeys.has(key)
})
const mergedErrors = [...filteredExisting, ...errors]
return {
...prevState,
plugins: {
...prevState.plugins,
enabled,
disabled,
commands,
errors: mergedErrors,
},
}
})
logForDebugging(
`Loaded plugins - Enabled: ${enabled.length}, Disabled: ${disabled.length}, Commands: ${commands.length}, Agents: ${agents.length}, Errors: ${errors.length}`,
)
// 跨启用插件计数组件类型
const hook_count = enabled.reduce((sum, p) => {
if (!p.hooksConfig) return sum
return (
sum +
Object.values(p.hooksConfig).reduce(
(s, matchers) =>
s + (matchers?.reduce((h, m) => h + m.hooks.length, 0) ?? 0),
0,
)
)
}, 0)
return {
enabled_count: enabled.length,
disabled_count: disabled.length,
inline_count: count(enabled, p => p.source.endsWith('@inline')),
marketplace_count: count(enabled, p => !p.source.endsWith('@inline')),
error_count: errors.length,
skill_count: commands.length,
agent_count: agents.length,
hook_count,
mcp_count,
lsp_count,
// 仅 Ant启用了哪些插件以与 RSS/FPS 相关。
// 与基本指标分开,因此不会流入 logForDiagnosticsNoPII。
ant_enabled_names:
process.env.USER_TYPE === 'ant' && enabled.length > 0
? (enabled
.map(p => p.name)
.sort()
.join(
',',
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS)
: undefined,
}
} catch (error) {
// 只有插件加载错误应该到达这里 - 记录用于监控
const errorObj = toError(error)
logError(errorObj)
logForDebugging(`Error loading plugins: ${error}`)
// 出错时设置空状态,但保留 LSP 错误并添加新错误
setAppState(prevState => {
// 保留现有的 LSP/非插件加载错误
const existingLspErrors = prevState.plugins.errors.filter(
e => e.source === 'lsp-manager' || e.source.startsWith('plugin:'),
)
const newError = {
type: 'generic-error' as const,
source: 'plugin-system',
error: errorObj.message,
}
return {
...prevState,
plugins: {
...prevState.plugins,
enabled: [],
disabled: [],
commands: [],
errors: [...existingLspErrors, newError],
},
}
})
return {
enabled_count: 0,
disabled_count: 0,
inline_count: 0,
marketplace_count: 0,
error_count: 1,
skill_count: 0,
agent_count: 0,
hook_count: 0,
mcp_count: 0,
lsp_count: 0,
load_failed: true,
ant_enabled_names: undefined,
}
}
}, [setAppState, addNotification])
// 挂载时加载插件并发出遥测
useEffect(() => {
if (!enabled) return
void initialPluginLoad().then(metrics => {
const { ant_enabled_names, ...baseMetrics } = metrics
const allMetrics = {
...baseMetrics,
has_custom_plugin_cache_dir: !!process.env.CLAUDE_CODE_PLUGIN_CACHE_DIR,
}
logEvent('tengu_plugins_loaded', {
...allMetrics,
...(ant_enabled_names !== undefined && {
enabled_names: ant_enabled_names,
}),
})
logForDiagnosticsNoPII('info', 'tengu_plugins_loaded', allMetrics)
})
}, [initialPluginLoad, enabled])
// 插件状态在磁盘上更改(后台协调,/plugin 菜单,
// 外部设置编辑)。显示通知;用户运行 /reload-plugins 应用。
// 之前的自动刷新有陈旧缓存 bug只清除 loadAllPlugins
// 下游 memoized 加载器返回旧数据)而且不完整(无 MCP无 agentDefinitions
// /reload-plugins 通过 refreshActivePlugins() 正确处理所有这些。
useEffect(() => {
if (!enabled || !needsRefresh) return
addNotification({
key: 'plugin-reload-pending',
text: 'Plugins changed. Run /reload-plugins to activate.',
color: 'suggestion',
priority: 'low',
})
// 不要自动刷新。不要重置 needsRefresh — /reload-plugins
// 通过 refreshActivePlugins() 消费它。
}, [enabled, needsRefresh, addNotification])
}