first commit

This commit is contained in:
H
2026-04-03 13:01:19 +08:00
commit 538eced414
2575 changed files with 645911 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
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])
}