mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 13:38:14 +08:00
268 lines
13 KiB
Markdown
268 lines
13 KiB
Markdown
# 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=<your-store-password>
|
||
keyPassword=<your-key-password>
|
||
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 凭据**不得写入磁盘或日志**。
|