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 目录镜像组织
架构概览
启动链路
main.dart调用EasyNodeApp.bootstrap()。bootstrap()内同步初始化AppStorage/SecureAppStorage/SessionCookieStore,读取已保存的 token、session cookie、deviceId。- 若三者齐全则尝试预拉服务端公钥并构造
AuthState,失败时清理本地登录态。 - 通过
ProviderScope.overrides把上述存储与AuthNotifier注入根作用域。 _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 拦截器注入tokenheader 和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.dartAES-GCM 解密。- 修改加密协议必须 server / native 同步升级,否则破坏现有 App 兼容性。
登录流程
- 校验并规范化服务器地址(去尾斜杠,HTTP 需要二次确认)。
GET /get-pub-pem获取 RSA 公钥。- RSA 加密密码,
POST /login拿 token + session cookie。 - 回调
_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复用现有Terminalbuffer,避免清屏。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/ 目录下执行。
flutter pub get # 拉依赖
flutter run # 连接设备 / 模拟器调试
flutter analyze # 静态分析,对齐 package:flutter_lints/flutter.yaml
flutter test # 单元测试(仅在显式需要时执行)
协作约束:默认只跑格式化和 flutter analyze,不跑 flutter test;只有明确要求时才运行测试。控制器 / repository 通过构造参数注入依赖,测试里走 fake,不要打真实网络。
打包步骤
Android
-
准备签名密钥(仅首次)
keytool -genkeypair -v -keystore easynode-release.jks \ -alias easynode -keyalg RSA -keysize 2048 -validity 10000把生成的
easynode-release.jks放到native/android/下。 -
创建
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。 -
更新版本号
编辑
native/pubspec.yaml顶部的version: x.y.z+build,+之前是versionName,之后是versionCode。 -
构建产物
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/
- APK:
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 passwordANDROID_KEY_PASSWORD:key passwordANDROID_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/*.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。
-
首次准备
cd native/ios pod install -
在 Xcode 配置签名
用 Xcode 打开
native/ios/Runner.xcworkspace,在Runner→Signing & Capabilities配置 Team / Bundle Identifier / Provisioning Profile。 -
构建归档
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 端一致:tokenheader +sessioncookie。 - 专属端点:
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 凭据不得写入磁盘或日志。