PentAGI Installer 故障排查指南:从导航栈、表单滚动到性能优化的 TUI 实战
PentAGI Installer 故障排查指南从导航栈、表单滚动到性能优化的 TUI 实战【免费下载链接】pentagiFully autonomous AI Agents system capable of performing complex penetration testing tasks项目地址: https://gitcode.com/GitHub_Trending/pe/pentagiPentAGI 的安装器是一个基于 Charmbubbletea / lipgloss构建的交互式终端向导TUI负责系统环境检查、LLM Provider 配置、集成组件Langfuse、Graphiti、可观测性设置以及最终的应用安装与运维。本篇指南以仓库中的官方排障文档installer-troubleshooting.md为骨架结合 backend/cmd/installer 下的真实源码实现系统讲解开发期 TUI 工作流、导航状态保持、表单滚动、环境变量持久化、响应式布局、渲染性能与错误恢复等常见问题的症状、根因、调试方法与修复方案。读完本文你将具备独立诊断和修复 PentAGI 安装器 TUI 各类问题的完整方法论。开发期专属问题避免 TUI 破坏终端会话症状与根因安装器运行时会启用备用屏幕alt screen并接管终端输入。在开发过程中直接在同一个终端里执行go run一旦程序退出或异常当前终端会话可能被残留的终端状态破坏表现为光标位置错乱、回显失效、历史命令不可用。解决方案构建与运行分离仓库 main.go 是安装器唯一入口推荐使用“先构建、后运行”的两段式工作流# ✅ 正确构建与测试分离 cd backend/ go build -o ../build/installer ./cmd/installer/main.go # 在独立的终端会话中运行 cd ../build/ ./installer # ❌ 错误开发时直接运行会破坏当前终端 cd backend/ go run ./cmd/installer/main.go # 破坏当前终端main.go中通过tea.NewProgram(app, tea.WithAltScreen(), tea.WithMouseCellMotion())启动 TUI同时注册了SIGINT/SIGTERM信号处理器用于优雅退出独立终端运行可以确保任何异常退出都不影响你的开发环境。调试监控结构化日志流安装器内置了结构化日志输出logger.LogWithData写 JSON 格式日志到log.json。开发时可以实时观察组件行为# 监控调试输出 tail -f log.json | jq . # 按组件过滤例如只看表单模型 tail -f log.json | jq select(.component FormModel) # 美化输出时间戳、级别与消息 tail -f log.json | jq -r \(.timestamp) [\(.level)] \(.message)从源码看所有核心模型如FormModel、导航器、App在状态变更时都会输出带组件名与数据的日志这为问题定位提供了统一入口。复合 ScreenID 导航系统在页面跳转中保留状态问题背景安装器存在大量“列表页 → 表单页 → 返回列表页”的交互。早期实现中跳转只传递目标屏幕标识导致返回后用户之前选中的菜单项、Provider 选择全部丢失。解决方案§分隔符复合标识仓库在 models/types.go 中定义了ScreenID类型格式为screen或screen§arg1§arg2§...并提供三个类型安全的辅助方法GetScreen()返回§之前的基础屏幕标识GetArgs()返回§之后的参数列表CreateScreenID(screen, args...)拼装复合标识修复前后的关键差异如下// ❌ 修复前跳转丢失选中状态 func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) { return NavigationMsg{Target: LLMProvidersScreen} // 未携带任何上下文 } // ✅ 修复后选中项作为参数随导航传递 func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) { selectedItem : m.getSelectedItem() return NavigationMsg{ Target: CreateScreenID(llm_providers, selectedItem.ID), } } // 结果为 llm_providers§openai —— 选中状态被保留导航栈在 navigator.go 中实现Push/Pop每次操作都会同步持久化到 state 中保证重启安装器后导航栈也能恢复App.Update收到NavigationMsg时执行Push/Pop并切换当前模型见 app.go。该方案带来三个收益类型安全传参借助GetScreen()、GetArgs()、CreateScreenID()参数传递不再依赖脆弱的手工字符串拼接状态自动恢复用户按 ESC 返回时回到精确的选中项如llm_providers§openai导航栈干净完整上下文随栈保留不会产生环或脏状态。实际使用中registryregistry.go预注册了全部带参数的屏幕例如LLMProviderOpenAIScreen llm_provider_form§openai未知屏幕 ID 会回退到MockFormModel兜底显示避免空指针。完整本地化架构消灭硬编码字符串问题背景UI 组件中散落的硬编码字符串导致文案难以统一管理、翻译与修改。解决方案集中式 locale 常量所有用户可见文本被集中到 locale/locale.go 中的结构化常量里多行文本直接以单个常量保存// 多行文本以单个常量存储 const MainMenuLLMProvidersInfo Configure AI language model providers for PentAGI. Supported providers: • OpenAI (GPT-4, GPT-3.5-turbo) • Anthropic (Claude-3, Claude-2) ... // 组件中的使用方式 sections append(sections, m.styles.Paragraph.Render(locale.MainMenuLLMProvidersInfo))覆盖范围包括菜单描述与帮助文本表单标签与错误消息Provider 专属文档键盘快捷键与提示如locale.NavBack Esc: Back、locale.NavCtrlS CtrlS: Save目前用户可见文本已 100% 迁移到 locale 常量热键文案通过 app.go 中的initHotkeysLocale映射表与当前屏幕的GetFormHotKeys()动态组合渲染到底部页脚。基于 Viewport 的表单滚动小终端也能顺畅导航问题背景字段很多的表单如 Ollama、Custom Provider 配置页在较小终端上无法完整显示且焦点切换时看不到当前编辑的字段。解决方案自动滚动视口 焦点跟踪核心实现在 models/base_screen.go 中要点如下自动滚动焦点字段始终保持可见智能定位根据每个字段的渲染高度精确计算滚动位置无缝导航Tab / ShiftTab实现为focusNext/focusPrev切换字段时按需滚动零新增快捷键完全复用现有导航键无需额外按键学习成本。关键实现与文档示例对应的真实源码// 焦点变化时自动滚动 func (b *BaseScreen) ensureFocusVisible() { if b.focusedIndex len(b.fieldHeights) { return } focusY : 0 if b.focusedIndex len(b.fieldHeights)-1 { focusY b.bottomHeight } for i : range b.focusedIndex { focusY b.fieldHeights[i] 1 // 字段间的空行 } visibleRows : b.viewportForm.Height offset : b.viewportForm.YOffset if focusY offset { b.viewportForm.YOffset focusY // 向上滚动 } if focusYb.fieldHeights[b.focusedIndex] offsetvisibleRows { b.viewportForm.YOffset focusY b.fieldHeights[b.focusedIndex] - visibleRows 1 // 向下滚动 } }字段高度在updateFormContent()中通过lipgloss.Height(renderedField)逐字段计算并存入fieldHeights滚动逻辑依赖这份数据focusNext在切换焦点后依次调用updateFormContent()与ensureFocusVisible()保证位置计算始终与最新渲染一致。增强的 Provider 配置完整字段集与动态表单问题背景早期缺少多个 LLM Provider 的配置字段导致部分供应商无法在向导中完成配置。解决方案为所有受支持 Provider 补齐字段每个 Provider 的字段映射在 controller/controller.go 的GetLLMProviderConfig/UpdateLLMProviderConfig中逐项实现对应ResetLLMProviderConfig中定义的环境变量清单。各 Provider 的字段集如下Provider字段集OpenAI / Anthropic / GeminiBase URL API KeyAWS BedrockRegion Default Auth 或 Bearer Token 或Access Key Secret Key Session Token Base URLDeepSeekBase URL API Key Provider NameLiteLLM 前缀如deepseekGLMBase URL API Key Provider NameLiteLLM 前缀如zaiKimiBase URL API Key Provider NameLiteLLM 前缀如moonshotQwenBase URL API Key Provider NameLiteLLM 前缀如dashscopeMiniMaxBase URL API Key Provider NameLiteLLM 前缀如minimaxOllamaBase URL API Key仅云端 Model Config Path Pull/Load 设置CustomBase URL API Key Model Config Path Provider Name Reasoning 选项关于 Ollama 与 Custom 的补充说明本地场景Ollama 无需 API Key仅需 Base URL云端场景需要从 Ollama 官方设置页获取 API Key配置路径表单会区分“容器内路径”OLLAMA_SERVER_CONFIG_PATH/LLM_SERVER_CONFIG_PATH与“宿主机路径”PENTAGI_...前缀当选择的配置属于内嵌的providers-configs时自动同步否则挂载宿主机自定义文件到/opt/pentagi/conf/下的默认路径Custom 特有LLM_SERVER_LEGACY_REASONING、LLM_SERVER_PRESERVE_REASONING控制推理内容处理方式。表单会根据 Provider 类型动态生成字段并附带对应的校验与帮助文本Suggestions字段支持 Tab 补全Masked字段支持CtrlH显示/隐藏。此外每个表单还支持CtrlS保存、CtrlR重置到默认值。常见问题与解决方案按类别macOS 将安装器报告为恶意软件症状macOS 阻止下载的 PentAGI 安装器运行并提示其可能包含恶意软件。目前已知情况只应从项目 README 中链接的官方下载地址获取安装器按 Mac CPU 架构选择压缩包Intel Mac 选darwin/amd64Apple 芯片选darwin/arm64维护者在 issue #249 中说明安装器文件已用项目证书签名且经核验的arm64与amd64构建在 VirusTotal 上的检测结果均为零维护者已联系 Apple 说明该警告正在等待回复。建议处理方式将任何意外的恶意软件警告视为暂停信号先核实下载来源再继续若压缩包来自镜像站、聊天附件或其他非官方渠道请从官方链接重新下载在确认官方发布版本可信后可尝试打开安装器一次然后进入系统设置 → 隐私与安全性找到“安装器已被阻止”的安全消息点击仍要打开并确认后续提示以允许该应用运行若你的 macOS 版本提示文案不同遵循 Apple 官方关于打开“未识别开发者”Mac 应用的指引操作即使文件名与官方一致也不要复用一个来源不明的安装器压缩包。导航问题导航栈损坏症状卡在某个屏幕、ESC 无效、无法返回上级页面。根因循环导航模式或导航栈被污染。调试通过日志输出当前导航栈全貌func (n *Navigator) debugStack() { stackInfo : make([]string, len(n.stack)) for i, screenID : range n.stack { stackInfo[i] string(screenID) } logger.LogWithData(Navigation Stack, map[string]interface{}{ stack: stackInfo, current: string(n.Current()), depth: len(n.stack), }) }对应源码中NavigatorStack.Strings()已将整条栈序列化为welcome - main_menu - ...的字符串配合logger.Log([Nav] PUSH/POP: ...)可以还原完整跳转轨迹。解决方案返回上级页面一律使用GoBack禁止直接设置目标屏幕// ✅ 正确使用 GoBack 防止循环 func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) { if err : m.saveConfiguration(); err ! nil { return m, nil // 保存失败则停留在表单页 } return m, func() tea.Msg { return NavigationMsg{GoBack: true} // 返回上一屏 } } // ❌ 错误直接导航会制造循环 func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) { m.saveConfiguration() return m, func() tea.Msg { return NavigationMsg{Target: ProvidersScreen} // 造成导航循环 } }在 app.go 中App.Update对NavigationMsg的处理逻辑与此对应GoBack时执行navigator.Pop()并加载上一屏幕Target非空时执行navigator.Push()。注意Pop()在栈深度 ≤ 1 时会原样返回当前屏幕避免弹空。选中状态丢失症状菜单选择被重置、Provider 选择被遗忘、配置丢失。根因模型在构造时没有接收导航参数。解决方案基于 args 构造模型初始化时恢复选中项// ✅ 正确基于参数构造 func NewModel(controller *StateController, styles *Styles, window *Window, args []string) *Model { selectedIndex : 0 if len(args) 0 args[0] ! { // 从导航参数恢复选中项 for i, item : range items { if item.ID args[0] { selectedIndex i break } } } return Model{selectedIndex: selectedIndex, args: args} }这在 registry.go 中得到印证initProcessorOperationForm通过id.GetArgs()解析出stack与operation参数再据此构造对应表单模型。表单问题字段宽度异常症状输入框过窄或过宽不随终端尺寸变化。根因字段创建时固化了宽度。调试输出终端、视口、输入框三层尺寸func (m *FormModel) debugFormDimensions() { width, height : m.styles.GetSize() viewportWidth, viewportHeight : m.getViewportSize() inputWidth : m.getInputWidth() logger.LogWithData(Form Dimensions, map[string]interface{}{ terminal_size: fmt.Sprintf(%dx%d, width, height), viewport_size: fmt.Sprintf(%dx%d, viewportWidth, viewportHeight), input_width: inputWidth, is_vertical: m.isVerticalLayout(), field_count: len(m.fields), }) }解决方案宽度在渲染阶段动态计算而非初始化阶段写死// ✅ 正确动态宽度计算 func (m *FormModel) updateFormContent() { inputWidth : m.getInputWidth() for i, field : range m.fields { field.Input.Width inputWidth - 3 // 渲染时应用宽度 field.Input.SetValue(field.Input.Value()) // 触发宽度更新 } } // ❌ 错误创建时固定宽度 func (m *FormModel) addField() { input : textinput.New() input.Width 50 // 破坏响应式设计 }源码 base_screen.go 的GetInputWidth()正是这样实现的横向布局取viewportWidth - 6纵向布局取viewportWidth - 4随后在updateFormContent()中逐字段应用并调用SetValue强制重绘。表单滚动失效症状无法到达所有字段、焦点字段滚出屏幕。根因缺少自动滚动实现或字段高度计算错误。调试检查焦点索引、视口偏移与内容高度是否一致func (m *FormModel) debugScrollState() { logger.LogWithData(Scroll State, map[string]interface{}{ focused_index: m.focusedIndex, viewport_offset: m.viewport.YOffset, viewport_height: m.viewport.Height, content_height: lipgloss.Height(m.formContent), field_heights: m.fieldHeights, total_fields: len(m.fields), }) }解决方案焦点切换后立即更新内容并触发自动滚动// ✅ 正确自动滚动实现 func (m *FormModel) focusNext() { m.fields[m.focusedIndex].Input.Blur() m.focusedIndex (m.focusedIndex 1) % len(m.fields) m.fields[m.focusedIndex].Input.Focus() m.updateFormContent() m.ensureFocusVisible() // 自动滚动的关键 }注意源码中focusNext还会考虑列表元素getTotalElements()在字段数基础上加 1滚动计算同样适用。环境变量问题配置不持久化症状设置跨会话丢失、环境变量未保存。根因没有调用 controller 的保存方法或清理逻辑不正确。调试逐字段对比输入值、环境变量值与“初始已设置”标记func (m *FormModel) debugEnvVarState() { for _, field : range m.fields { envVar, _ : m.controller.GetVar(m.getEnvVarName(getEnvSuffixFromKey(field.Key))) logger.LogWithData(Field State, map[string]interface{}{ field_key: field.Key, input_value: field.Input.Value(), env_var_name: m.getEnvVarName(getEnvSuffixFromKey(field.Key)), env_var_value: envVar.Value, env_var_default: envVar.Default, is_present: envVar.IsPresent(), initially_set: m.initiallySetFields[field.Key], }) } }解决方案保存分两遍进行——先清空“曾经有值但现在为空”的变量再写入非空值// ✅ 正确完整保存实现 func (m *FormModel) saveConfiguration() error { // 第一遍移除被清空的字段 for _, field : range m.fields { value : strings.TrimSpace(field.Input.Value()) if value m.initiallySetFields[field.Key] { // 原本有值但现在为空 —— 从环境中移除 if err : m.controller.SetVar(field.EnvVarName, ); err ! nil { return fmt.Errorf(failed to clear %s: %w, field.EnvVarName, err) } logger.Log([FormModel] SAVE: cleared %s, field.EnvVarName) } } // 第二遍保存非空值 for _, field : range m.fields { value : strings.TrimSpace(field.Input.Value()) if value ! { if err : m.controller.SetVar(field.EnvVarName, value); err ! nil { return fmt.Errorf(failed to set %s: %w, field.EnvVarName, err) } logger.Log([FormModel] SAVE: set %s%s, field.EnvVarName, value) } } return nil }BaseScreen.saveConfiguration()见 base_screen.go会调用 handler 的HandleSave()失败时记录错误日志并停留在当前页成功后才把hasChanges置为 false。真正落盘由controller桥接的state.State完成——安装器通过 main.go 在启动时把.env路径传给state.NewState所有SetVar都会写入该环境文件。布局问题内容不适应终端尺寸症状内容被截断、面板不缩放、出现横向滚动。根因缺少响应式布局逻辑或尺寸处理错误。调试对比终端尺寸、内容区尺寸与最小宽度常量func (m *Model) debugLayoutState() { width, height : m.styles.GetSize() contentWidth, contentHeight : m.window.GetContentSize() logger.LogWithData(Layout State, map[string]interface{}{ terminal_size: fmt.Sprintf(%dx%d, width, height), content_size: fmt.Sprintf(%dx%d, contentWidth, contentHeight), is_vertical: m.isVerticalLayout(), min_terminal: MinTerminalWidth, min_menu_width: MinMenuWidth, min_info_width: MinInfoWidth, }) }解决方案按可用宽度在纵向与横向布局间切换// ✅ 正确响应式布局实现 func (m *Model) View() string { width, height : m.styles.GetSize() leftPanel : m.renderContent() rightPanel : m.renderInfo() if m.isVerticalLayout() { return m.renderVerticalLayout(leftPanel, rightPanel, width, height) } return m.renderHorizontalLayout(leftPanel, rightPanel, width, height) } func (m *Model) isVerticalLayout() bool { contentWidth : m.window.GetContentWidth() return contentWidth (MinMenuWidth MinInfoWidth PaddingWidth) }源码中这些常量定义在 models/types.goMinMenuWidth 38、MaxMenuWidth 88、MinInfoWidth 34、PaddingWidth 8、PaddingHeight 2。横向布局下额外宽度会按比例分配给左菜单上限 88 列当窗口过窄时自动退化为上下堆叠的纵向布局。底部页脚高度不一致症状页脚占用空间超出预期导致布局计算错位。根因使用边框border方案实现页脚边框使高度随内容变化。解决方案改用背景色方案页脚永远只有一行// ✅ 正确背景方案恒定 1 行 func (a *App) renderFooter() string { actions : a.buildFooterActions() footerText : strings.Join(actions, • ) return a.styles.Footer.Render(footerText) } // styles.go 中的样式定义 func (s *Styles) updateStyles() { s.Footer lipgloss.NewStyle(). Width(s.width). Background(lipgloss.Color(240)). Foreground(lipgloss.Color(255)). Padding(0, 1, 0, 1) } // ❌ 错误边框方案高度不固定 footer : lipgloss.NewStyle(). Height(1). Border(lipgloss.Border{Top: true}). Render(text)仓库当前实现的页脚样式styles.go与此一致不依赖边框而是通过App.updateScreenMargins()在每次屏幕切换与窗口缩放后用lipgloss.Height重新计算页脚高度进而得到准确的内容区尺寸再分发给所有屏幕。性能问题渲染缓慢症状界面卡顿、按键响应延迟。根因创建了多个 glamour Markdown 渲染器实例、内容更新过于频繁。调试分段测量内容构建、写入视口与最终渲染的耗时func (m *Model) debugRenderPerformance() { start : time.Now() content : m.buildContent() buildDuration : time.Since(start) start time.Now() m.viewport.SetContent(content) setContentDuration : time.Since(start) start time.Now() view : m.viewport.View() viewDuration : time.Since(start) logger.LogWithData(Render Performance, map[string]interface{}{ content_size: len(content), rendered_size: len(view), build_ms: buildDuration.Milliseconds(), set_content_ms: setContentDuration.Milliseconds(), view_render_ms: viewDuration.Milliseconds(), }) }解决方案全局共享单个渲染器// ✅ 正确单一共享渲染器 // styles.go 中 func New() *Styles { renderer, _ : glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(80), ) return Styles{renderer: renderer} } // 使用方式 rendered, err : m.styles.GetRenderer().Render(content) // ❌ 错误每次渲染都新建渲染器 func (m *Model) renderMarkdown(content string) string { renderer, _ : glamour.NewTermRenderer(...) // 性能杀手 return renderer.Render(content) }这与 styles.go 中Styles.New()的实现一致——渲染器在程序启动时创建一次通过GetRenderer()复用。内存泄漏症状内存占用持续增长、应用随时间变得迟钝。根因资源未正确清理、创建了多个渲染器实例。解决方案进入屏幕时做完整的状态重置// ✅ 正确完整状态重置 func (m *Model) Init() tea.Cmd { // 完全重置所有状态 m.content m.ready false m.error nil m.initialized false // 重置组件状态 m.viewport.GotoTop() m.viewport.SetContent() // 重置表单状态 m.focusedIndex 0 m.hasChanges false for i : range m.fields { m.fields[i].Input.Blur() } return m.loadContent }base_screen.go 中Init()的做法与此一脉相承先由 handlerBuildForm()重建字段再updateViewports()刷新视口避免旧内容与新状态互相污染。错误恢复模式优雅的状态恢复func (m *Model) recoverFromError(err error) tea.Cmd { logger.Errorf([%s] ERROR: %v, m.componentName, err) // 尝试恢复状态 m.error err m.ready true return func() tea.Msg { logger.Log([%s] RECOVERY: attempting state recovery, m.componentName) // 尝试重新加载内容 if content, loadErr : m.loadFallbackContent(); loadErr nil { logger.Log([%s] RECOVERY: fallback content loaded, m.componentName) return ContentLoadedMsg{content} } // 兜底使用最小内容 logger.Log([%s] RECOVERY: using minimal content, m.componentName) return ContentLoadedMsg{# Error\n\nContent temporarily unavailable.} } }安全的异步操作func (m *Model) loadContent() tea.Cmd { return func() tea.Msg { defer func() { if r : recover(); r ! nil { logger.Errorf([%s] PANIC: recovered from panic: %v, m.componentName, r) return ErrorMsg{fmt.Errorf(panic in loadContent: %v, r)} } }() content, err : m.loadFromSource() if err ! nil { return ErrorMsg{err} } return ContentLoadedMsg{content} } }异步加载用defer recover兜住 panic 并转为ErrorMsg交给 TUI 事件循环处理配合ContentLoadedMsg消息驱动 UI 更新——这正是 bubbletea 推荐的命令式异步模式也是 app.go 中forwardMsgToCurrentModel的消息转发基础。测试策略手动测试清单// 测试各档终端尺寸 // 1. 把终端缩放到各种尺寸 // 2. 测试最小尺寸80x24 // 3. 测试极窄终端 80 列 // 4. 测试极矮终端 24 行 func testDimensions() { testSizes : []struct{ width, height int }{ {80, 24}, // 标准 {40, 12}, // 小 {120, 40}, // 大 {20, 10}, // 极小 } for _, size : range testSizes { logger.LogWithData(Dimension Test, map[string]interface{}{ test_size: fmt.Sprintf(%dx%d, size.width, size.height), layout_mode: getLayoutMode(size.width, size.height), }) } }结合MinMenuWidth MinInfoWidth PaddingWidth 38 34 8 80可推导宽度小于 80 列时表单与列表页会自动进入纵向布局因此 80x24 是最小验证基准。导航流程测试func testNavigationFlow() { testSteps : []struct { action string expected string }{ {start, welcome}, {continue, main_menu}, {select_providers, llm_providers}, {select_openai, llm_provider_form§openai}, {go_back, llm_providers§openai}, {esc, welcome}, } for _, step : range testSteps { logger.LogWithData(Navigation Test, map[string]interface{}{ action: step.action, expected: step.expected, actual: string(navigator.Current()), }) } }这一测试矩阵可以直接对照 navigator_test.go 中的单元测试来编写自动化版本——该测试文件通过mockState验证NewNavigator的栈初始化与Push/Pop/Current行为是导航回归测试的现成脚手架。总结本排障指南覆盖了 PentAGI 安装器 TUI 的六类核心问题域开发工作流构建/运行分离避免 TUI 破坏终端会话导航问题用复合ScreenID携带参数、用GoBack防止循环、基于 args 构造模型恢复状态表单问题渲染期动态计算宽度、基于字段高度精确滚动配置持久化两遍保存法处理“清空”与“写入”全部经 controller 落到.env文件性能优化单例 glamour 渲染器、进入屏幕时完整重置状态恢复与测试recover兜底异步加载、按尺寸矩阵与导航矩阵做回归验证。这些修复模式在 backend/cmd/installer 中均有对应实现可供对照若需进一步理解安装器的整体架构与各组件职责可继续阅读 installer-architecture-design.md 与 installer-overview.md构建与运行安装器的方式可参考 installer.md。【免费下载链接】pentagiFully autonomous AI Agents system capable of performing complex penetration testing tasks项目地址: https://gitcode.com/GitHub_Trending/pe/pentagi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考