Files

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,在 LoginPageMainShellPage 之间切换。

所有存储 provider 在 state/storage_providers.dart 里默认 throw UnimplementedError(...),必须由 bootstrap 阶段 override任何其它代码路径不要直接 new。

状态管理 (Riverpod)

  • state/auth_notifier.dartsignIn() 写本地存储并切到登录态;signOut() 关闭所有终端会话、清 token / cookie / deviceId。
  • state/auth_state.dart:登录后唯一持有的 ApiClient + 服务端公钥 PEM。
  • state/host_list_notifier.dartterminal_providers.dartapi_providers.dart:主机列表、终端会话、各 Repository 的装配。

网络层

  • core/api/api_client.dartbaseUrl = $serverAddress/api/v1Dio 拦截器注入 token header 和 Cookie;响应里 set-cookie 自动回写 SessionCookieStore401/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。
  • SftpTabSFTP 文件操作。
  • ScriptsTab:脚本库与脚本分组。
  • SettingsTab:账户安全 / 凭据 / 代理 / 登录日志 / Plus。

AI Agent 通过 AgentOverlay 浮窗全局可用,支持多种 AI 提供商OpenAI、Anthropic、Google实时 Socket.IO 通信,工具调用需用户审批。

登出由 authProvider 状态变更触发 _AppRoot 回到 LoginPage,不需要手动 pop。

终端 / SSH

  • features/terminal/ssh_terminal_controller.dartdartssh2 起 shell sessionstdout/stderr 写入 xterm.Terminalterminal.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.dartSocket.IO 客户端,连接 /agent 命名空间,处理 message / tool-call / stream-end 事件。
  • agent_controller.dart + agent_reducer.dart:消息流状态机,处理用户输入、工具审批、历史会话。
  • agent_overlay.dart + agent_window.dart:全局可拖拽浮窗,支持最小化 / 展开,跨 tab 保持状态。
  • agent_repository.dartGET /api/v1/agent/provider-config 获取 AI 配置,POST /agent/history 保存会话。
  • 工具调用(如执行 SSH 命令、读文件)需要用户在 agent_approval_card.dart 中点击批准,未批准的调用自动拒绝。

存储分层

  • AppStorageSharedPreferences普通偏好例如 server address、username、save password 开关。
  • SecureAppStorageflutter_secure_storagetoken、session cookie、密码、deviceId。
  • device_id.dartdeviceId 生成与缓存。

本地开发

所有命令在 native/ 目录下执行。

flutter pub get          # 拉依赖
flutter run              # 连接设备 / 模拟器调试
flutter analyze          # 静态分析,对齐 package:flutter_lints/flutter.yaml
flutter test             # 单元测试(仅在显式需要时执行)

协作约束:默认只跑格式化和 flutter analyze,不跑 flutter test;只有明确要求时才运行测试。控制器 / repository 通过构造参数注入依赖,测试里走 fake不要打真实网络。

打包步骤

Android

  1. 准备签名密钥(仅首次)

    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

    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. 构建产物

    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 SecretsSettings → Secrets and variables → Actions

  • ANDROID_KEYSTORE_BASE64native/android/easynode-release.jks 的 base64 内容
  • ANDROID_STORE_PASSWORDkeystore store password
  • ANDROID_KEY_PASSWORDkey password
  • ANDROID_KEY_ALIAS:默认 easynode

生成 keystore 的 base64任选其一

# Windows PowerShell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("native/android/easynode-release.jks"))
# 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/*.apkAPK 可直接安装/侧载,当前分发方式)

AABGoogle Play 上架格式默认不构建workflow 里 Build app bundle 步骤已注释。需要上架 Google Play 时取消注释,并恢复两个上传步骤里的 *.aab 路径。

发布版本时需要同步更新:

  • native/pubspec.yamlversion: x.y.z+build+build/versionCode 必须单调递增,否则无法上架 Google Play
  • server/version.jsonnativeVersion: native-vx.y.z

提示:发布 native-v* 的 GitHub Release 不会触发服务端 Docker 构建(docker-builder.yml 已对 native-/client 前缀加了跳过守卫Web 端更新检测也已忽略 native-v* 标签。

iOS

iOS 构建需要 macOS + Xcode。

  1. 首次准备

    cd native/ios
    pod install
    
  2. 在 Xcode 配置签名

    用 Xcode 打开 native/ios/Runner.xcworkspace,在 RunnerSigning & Capabilities 配置 Team / Bundle Identifier / Provisioning Profile。

  3. 构建归档

    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.jsnative/lib/features/servers/server_repository.dartnative/lib/core/crypto/aes_gcm_crypto.dart
    • GET /api/v1/agent/provider-config:返回 AI Agent 配置(提供商、模型列表、上下文限制)。
  • WebSocket 命名空间:
    • /agentAI Agent 实时通信,事件包括 message(流式文本)、tool-call(工具调用请求)、stream-end(会话结束)。
  • 解密后的 SSH 凭据不得写入磁盘或日志