# EasyNode Native EasyNode 的 Flutter Native App,复用现有后端 (`/api/v1`),在原生端上提供服务器列表、SSH 终端、SFTP 文件管理、脚本库、账户安全等能力。App 自身不打包后端地址,登录时由用户填写。 ## 技术栈 - Flutter `^3.11.0` - Riverpod (`flutter_riverpod`) 状态管理 - Dio + dio_cookie_manager + flutter_secure_storage 网络与持久化 - dartssh2 + xterm 终端 - Socket.IO (`socket_io_client`) AI 助手实时通信 - pointycastle + basic_utils RSA / AES-GCM 加密 - re_editor / photo_view 文件预览与编辑 - flutter_markdown_plus Markdown 渲染 ## 目录结构 ``` native/ ├── lib/ │ ├── main.dart # 入口,调用 EasyNodeApp.bootstrap() │ ├── app.dart # 启动装配、ProviderScope override、登录态路由、AI Agent overlay │ ├── core/ │ │ ├── api/ # ApiClient / Cookie / 通用错误 │ │ ├── crypto/ # RSA、AES-GCM、CryptoJS 兼容 │ │ ├── storage/ # SharedPreferences + SecureStorage + deviceId │ │ ├── ui/ # 主题色板 │ │ └── utils/ # JWT、表单校验 │ ├── features/ │ │ ├── auth/ # 登录页、登录控制器、AuthSession │ │ ├── servers/ # 服务器列表、表单、Repository、模型 │ │ ├── terminal/ # SSH 通道、xterm 控制器、会话管理、工具栏 │ │ ├── ai_agent/ # AI 助手:Socket.IO 客户端、消息流、工具调用审批 │ │ ├── scripts/ # 脚本库与脚本分组 │ │ ├── settings/ # 账户安全、凭据、代理、登录日志、Plus │ │ └── shell/ # MainShell / SFTP / 编辑器 / 媒体预览 │ │ ├── editor/ # 文件编辑器(re_editor) │ │ └── media/ # 媒体预览(photo_view) │ ├── state/ # Riverpod providers (auth、host list、terminal、agent …) │ └── l10n/ # 多语言入口:AppLocalizations、strings_zh/en ├── android/ # Android 工程,含 key.properties.example ├── ios/ # iOS 工程 ├── assets/ # 图标 / 图片资源 └── test/ # 单元测试,按 lib 目录镜像组织 ``` ## 架构概览 ### 启动链路 1. `main.dart` 调用 `EasyNodeApp.bootstrap()`。 2. `bootstrap()` 内同步初始化 `AppStorage` / `SecureAppStorage` / `SessionCookieStore`,读取已保存的 token、session cookie、deviceId。 3. 若三者齐全则尝试预拉服务端公钥并构造 `AuthState`,失败时清理本地登录态。 4. 通过 `ProviderScope.overrides` 把上述存储与 `AuthNotifier` 注入根作用域。 5. `_AppRoot` 监听 `authProvider.signedIn`,在 `LoginPage` 与 `MainShellPage` 之间切换。 > 所有存储 provider 在 `state/storage_providers.dart` 里默认 `throw UnimplementedError(...)`,必须由 bootstrap 阶段 override;任何其它代码路径不要直接 new。 ### 状态管理 (Riverpod) - `state/auth_notifier.dart`:`signIn()` 写本地存储并切到登录态;`signOut()` 关闭所有终端会话、清 token / cookie / deviceId。 - `state/auth_state.dart`:登录后唯一持有的 `ApiClient` + 服务端公钥 PEM。 - `state/host_list_notifier.dart`、`terminal_providers.dart`、`api_providers.dart`:主机列表、终端会话、各 Repository 的装配。 ### 网络层 - `core/api/api_client.dart`:`baseUrl = $serverAddress/api/v1`,Dio 拦截器注入 `token` header 和 `Cookie`;响应里 `set-cookie` 自动回写 `SessionCookieStore`;401/403 抛 `UnauthorizedFailure`。 - `core/api/cookie_store.dart`:基于 `flutter_secure_storage` 持久化 cookie,启动时回放给 Dio。 - 所有 feature 都通过 `authProvider` 暴露的 `ApiClient` 调用接口,不要再 new。 ### 加密协议 - 登录密码:`core/crypto/rsa_crypto.dart#encryptPassword` → PKCS1 + utf8,对应服务端 `node-rsa.decrypt(ct, 'utf8')`。 - Native 端 SSH 临时密钥:32 字节 AES key → base64 → utf8 → RSA,对应 `RSADecryptAsync` + `Buffer.from(text, 'base64')`。 - `/native/ssh-connection` 返回的 `{ iv, tag, ciphertext }` 由 `core/crypto/aes_gcm_crypto.dart` AES-GCM 解密。 - **修改加密协议必须 server / native 同步升级**,否则破坏现有 App 兼容性。 ### 登录流程 1. 校验并规范化服务器地址(去尾斜杠,HTTP 需要二次确认)。 2. `GET /get-pub-pem` 获取 RSA 公钥。 3. RSA 加密密码,`POST /login` 拿 token + session cookie。 4. 回调 `_onLoginSuccess`,由 `AuthNotifier.signIn` 持久化并触发跳转。 ### 主壳 `features/shell/main_shell_page.dart` 是四个 tab 的 IndexedStack 保活容器 + AI Agent 全局浮窗: - `ServersTab`:服务器列表,点击连接走 `ApiServerRepository.fetchSshConfig(hostId)` → `TerminalSessionManager.openSession()` 在本地起 dartssh2 session。 - `SftpTab`:SFTP 文件操作。 - `ScriptsTab`:脚本库与脚本分组。 - `SettingsTab`:账户安全 / 凭据 / 代理 / 登录日志 / Plus。 AI Agent 通过 `AgentOverlay` 浮窗全局可用,支持多种 AI 提供商(OpenAI、Anthropic、Google),实时 Socket.IO 通信,工具调用需用户审批。 登出由 `authProvider` 状态变更触发 `_AppRoot` 回到 `LoginPage`,不需要手动 pop。 ### 终端 / SSH - `features/terminal/ssh_terminal_controller.dart`:dartssh2 起 shell session,stdout/stderr 写入 `xterm.Terminal`;`terminal.onOutput` 把按键回送给 SSH session;支持 `Ctrl + 字母` 一次性修饰键。Shell 启动后主动 `resizeTerminal()` 一次,避免 PTY 卡在 80x24。 - `terminal_session_manager.dart`:所有终端会话集合 + 当前激活 id,提供 open / setActive / reconnect / close / closeAll。`reconnect` 复用现有 `Terminal` buffer,避免清屏。 - `ssh_connection_config.dart`:与服务端 native SSH payload 对齐的纯数据类。 - `http_proxy_connector.dart` / `socks5_connector.dart` / `ssh_transport.dart`:代理与跳板机连接通道。 ### AI Agent - `features/ai_agent/agent_socket_client.dart`:Socket.IO 客户端,连接 `/agent` 命名空间,处理 `message` / `tool-call` / `stream-end` 事件。 - `agent_controller.dart` + `agent_reducer.dart`:消息流状态机,处理用户输入、工具审批、历史会话。 - `agent_overlay.dart` + `agent_window.dart`:全局可拖拽浮窗,支持最小化 / 展开,跨 tab 保持状态。 - `agent_repository.dart`:`GET /api/v1/agent/provider-config` 获取 AI 配置,`POST /agent/history` 保存会话。 - 工具调用(如执行 SSH 命令、读文件)需要用户在 `agent_approval_card.dart` 中点击批准,未批准的调用自动拒绝。 ### 存储分层 - `AppStorage`(SharedPreferences):普通偏好,例如 server address、username、save password 开关。 - `SecureAppStorage`(flutter_secure_storage):token、session cookie、密码、deviceId。 - `device_id.dart`:deviceId 生成与缓存。 ## 本地开发 所有命令在 `native/` 目录下执行。 ```bash flutter pub get # 拉依赖 flutter run # 连接设备 / 模拟器调试 flutter analyze # 静态分析,对齐 package:flutter_lints/flutter.yaml flutter test # 单元测试(仅在显式需要时执行) ``` 协作约束:默认只跑格式化和 `flutter analyze`,不跑 `flutter test`;只有明确要求时才运行测试。控制器 / repository 通过构造参数注入依赖,测试里走 fake,不要打真实网络。 ## 打包步骤 ### Android 1. **准备签名密钥**(仅首次) ```bash keytool -genkeypair -v -keystore easynode-release.jks \ -alias easynode -keyalg RSA -keysize 2048 -validity 10000 ``` 把生成的 `easynode-release.jks` 放到 `native/android/` 下。 2. **创建 `native/android/key.properties`** 参考 `key.properties.example`: ```properties storePassword= keyPassword= keyAlias=easynode storeFile=easynode-release.jks ``` 该文件已被 `.gitignore` 忽略,**不要提交**。如果不提供,`build.gradle.kts` 会回退到 debug keystore,仅供本地 `flutter run --release` 使用,正式产物必须有 release keystore。 3. **更新版本号** 编辑 `native/pubspec.yaml` 顶部的 `version: x.y.z+build`,`+` 之前是 `versionName`,之后是 `versionCode`。 4. **构建产物** ```bash flutter pub get flutter clean # 可选,更新插件后建议执行 flutter build apk --release # 通用 APK flutter build apk --release --split-per-abi # 按 ABI 拆分(推荐用于分发) flutter build appbundle --release # Google Play 上架用 AAB ``` 产物位置: - APK: `native/build/app/outputs/flutter-apk/` - AAB: `native/build/app/outputs/bundle/release/` ### Android CI 构建 GitHub Actions 使用 `.github/workflows/native-android-release.yml` 构建 Android 产物。 触发方式: - 推送 `native-v*` 标签,例如 `native-v0.1.0-beta.1`(会构建并上传到对应 GitHub Release) - 在 Actions 页面手动运行 `Build Native Android`(只产出 artifact,不写 Release) 需要在仓库配置以下 GitHub Secrets(`Settings → Secrets and variables → Actions`): - `ANDROID_KEYSTORE_BASE64`:`native/android/easynode-release.jks` 的 base64 内容 - `ANDROID_STORE_PASSWORD`:keystore store password - `ANDROID_KEY_PASSWORD`:key password - `ANDROID_KEY_ALIAS`:默认 `easynode` 生成 keystore 的 base64(任选其一): ```powershell # Windows PowerShell [Convert]::ToBase64String([IO.File]::ReadAllBytes("native/android/easynode-release.jks")) ``` ```bash # Linux base64 -w 0 native/android/easynode-release.jks # macOS base64 -i native/android/easynode-release.jks ``` workflow 会在还原签名后校验上述 Secret 非空,并在构建后用 `apksigner` 校验产物证书不是 debug keystore,任一不满足直接失败。 CI 产物: - APK: `native/build/app/outputs/flutter-apk/*.apk`(APK 可直接安装/侧载,当前分发方式) > AAB(Google Play 上架格式)默认不构建,workflow 里 `Build app bundle` 步骤已注释。需要上架 Google Play 时取消注释,并恢复两个上传步骤里的 `*.aab` 路径。 发布版本时需要同步更新: - `native/pubspec.yaml` 的 `version: x.y.z+build`(`+build`/versionCode 必须单调递增,否则无法上架 Google Play) - `server/version.json` 的 `nativeVersion: native-vx.y.z` > 提示:发布 `native-v*` 的 GitHub Release 不会触发服务端 Docker 构建(`docker-builder.yml` 已对 `native-`/`client` 前缀加了跳过守卫),Web 端更新检测也已忽略 `native-v*` 标签。 ### iOS iOS 构建需要 macOS + Xcode。 1. **首次准备** ```bash cd native/ios pod install ``` 2. **在 Xcode 配置签名** 用 Xcode 打开 `native/ios/Runner.xcworkspace`,在 `Runner` → `Signing & Capabilities` 配置 Team / Bundle Identifier / Provisioning Profile。 3. **构建归档** ```bash flutter build ipa --release ``` 或在 Xcode 中选择 `Product → Archive`,再通过 Organizer 上传到 App Store Connect / 导出 Ad-hoc IPA。 产物位置:`native/build/ios/archive/` 与 `native/build/ios/ipa/`。 ### HarmonyOS 参见 `OHOS_PATCH.md` 获取完整的 HarmonyOS 构建说明。 ### 发布前自检 - `flutter analyze` 通过。 - 在真机 release 模式运行一次(`flutter run --release`)。 - 确认登录页能输入服务器地址、HTTP 地址有风险提示、HTTPS 不提示。 - 确认终端、SFTP、脚本三个 tab 能正常访问后端。 - 确认登出后 token / cookie / deviceId 已清空。 ## 后端约定 - Native 端复用 `/api/v1` 全部接口,鉴权与 Web 端一致:`token` header + `session` cookie。 - 专属端点: - `POST /api/v1/native/ssh-connection`:返回 AES-GCM 加密后的 SSH 连接参数。修改时同步更新 `server/app/controller/native.js` 与 `native/lib/features/servers/server_repository.dart`、`native/lib/core/crypto/aes_gcm_crypto.dart`。 - `GET /api/v1/agent/provider-config`:返回 AI Agent 配置(提供商、模型列表、上下文限制)。 - WebSocket 命名空间: - `/agent`:AI Agent 实时通信,事件包括 `message`(流式文本)、`tool-call`(工具调用请求)、`stream-end`(会话结束)。 - 解密后的 SSH 凭据**不得写入磁盘或日志**。