# Volcengine Server Manager 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 构建 Windows 桌面应用,管理火山引擎 ECS 服务器,支持查看实例状态和重启实例。 **Architecture:** 使用 egui + eframe 构建 GUI,通过 HTTP 直接调用火山引擎 API(HMAC-SHA256 签名),异步 API 调用通过 tokio spawn 在后台线程执行,结果通过 channel 返回 UI 线程。 **Tech Stack:** Rust, egui 0.29, eframe (glow), reqwest, tokio, serde, serde_json, sha2, hmac, hex, base64, chrono --- ## 文件结构 | 文件 | 职责 | 状态 | |------|------|------| | `Cargo.toml` | 项目配置、依赖声明 | 新建 | | `src/main.rs` | 入口、字体加载、启动 eframe | 新建 | | `src/app.rs` | egui UI 主逻辑、状态管理、事件处理 | 新建 | | `src/config.rs` | 配置结构体、JSON 读写 | 新建 | | `src/api.rs` | 火山引擎 API 调用(HTTP + HMAC-SHA256 签名) | 新建 | | `src/types.rs` | 数据类型定义(InstanceInfo、InstanceStatus 等) | 新建 | ## 决策:不使用 volcengine-rust-sdk 原因: 1. SDK 依赖 protobuf,在 MinGW 环境下编译风险高 2. SDK 功能不完整(仅 11 commits,0 stars),可能不包含 RebootInstance 等接口 3. 火山引擎 API 签名机制简单(HMAC-SHA256),直接 HTTP 调用更可控 4. 减少依赖体积,单 exe 更小 --- ### Task 1: 项目初始化 **Files:** - Create: `Cargo.toml` - Create: `src/main.rs` (最小入口) - Create: `src/types.rs` - Create: `src/config.rs` - Create: `src/api.rs` - Create: `src/app.rs` - [ ] **Step 1: 创建 Cargo.toml** ```toml [package] name = "volcengine-server-manager" version = "0.1.0" edition = "2021" [dependencies] eframe = { version = "0.29", default-features = false, features = ["glow"] } egui = "0.29" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" hmac = "0.12" hex = "0.4" base64 = "0.22" chrono = { version = "0.4", features = ["serde"] } dirs = "5" urlencoding = "2.1" ``` - [ ] **Step 2: 创建 src/types.rs** ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq)] pub enum InstanceStatus { Running, Stopped, Starting, Stopping, Rebooting, Error, Unknown, } impl InstanceStatus { pub fn from_str(s: &str) -> Self { match s { "RUNNING" => InstanceStatus::Running, "STOPPED" => InstanceStatus::Stopped, "STARTING" => InstanceStatus::Starting, "STOPPING" => InstanceStatus::Stopping, "REBOOTING" => InstanceStatus::Rebooting, "ERROR" => InstanceStatus::Error, _ => InstanceStatus::Unknown, } } pub fn label(&self) -> &str { match self { InstanceStatus::Running => "运行中", InstanceStatus::Stopped => "已停止", InstanceStatus::Starting => "启动中", InstanceStatus::Stopping => "停止中", InstanceStatus::Rebooting => "重启中", InstanceStatus::Error => "错误", InstanceStatus::Unknown => "未知", } } } #[derive(Debug, Clone)] pub struct InstanceInfo { pub instance_id: String, pub instance_name: String, pub status: InstanceStatus, pub private_ip: String, pub public_ip: String, pub zone_id: String, pub region_id: String, pub instance_type: String, pub creation_time: String, } #[derive(Debug, Clone)] pub enum AppState { Loading, Ready, Error(String), } #[derive(Debug, Clone)] pub enum ApiMessage { InstancesLoaded(Vec), RebootSuccess(String), Error(String), } ``` - [ ] **Step 3: 创建 src/config.rs** ```rust use serde::{Deserialize, Serialize}; use std::fs; use std::io; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub access_key_id: String, pub secret_access_key: String, #[serde(default = "default_endpoint")] pub endpoint: String, } fn default_endpoint() -> String { "ecs.volcengineapi.com".to_string() } impl AppConfig { pub fn is_configured(&self) -> bool { !self.access_key_id.is_empty() && !self.secret_access_key.is_empty() } } pub fn config_dir() -> PathBuf { dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("volcengine-server-manager") } pub fn config_path() -> PathBuf { config_dir().join("config.json") } pub fn load_config() -> Result { let path = config_path(); if !path.exists() { return Ok(AppConfig { access_key_id: String::new(), secret_access_key: String::new(), endpoint: default_endpoint(), }); } let content = fs::read_to_string(&path)?; let config: AppConfig = serde_json::from_str(&content)?; Ok(config) } pub fn save_config(config: &AppConfig) -> Result<(), io::Error> { let dir = config_dir(); fs::create_dir_all(&dir)?; let content = serde_json::to_string_pretty(config)?; fs::write(config_path(), content)?; Ok(()) } ``` - [ ] **Step 4: 创建 src/api.rs (骨架版本)** ```rust use crate::types::InstanceInfo; pub struct VolcClient; impl VolcClient { pub fn new(_access_key_id: String, _secret_access_key: String, _endpoint: String) -> Self { Self } pub async fn list_instances(&self) -> Result, String> { Err("API 未实现".to_string()) } pub async fn reboot_instance(&self, _instance_id: &str) -> Result<(), String> { Err("API 未实现".to_string()) } } ``` > 注意: Task 3 会替换此文件为完整的 API 实现。Task 1 的骨架版本仅用于验证编译通过。 - [ ] **Step 5: 创建 src/app.rs** ```rust use crate::api::VolcClient; use crate::config::{load_config, save_config, AppConfig}; use crate::types::{ApiMessage, AppState, InstanceInfo}; use eframe::egui; use std::sync::mpsc; use std::thread; pub struct VolcManagerApp { config: AppConfig, instances: Vec, app_state: AppState, show_config_dialog: bool, show_reboot_confirm: Option, rebooting_instance: Option, last_refresh: Option, rx: Option>, config_ak: String, config_sk: String, config_endpoint: String, status_message: Option<(String, std::time::Instant)>, } impl VolcManagerApp { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { setup_fonts(&cc.egui_ctx); let config = load_config().unwrap_or_else(|_| AppConfig { access_key_id: String::new(), secret_access_key: String::new(), endpoint: "ecs.volcengineapi.com".to_string(), }); let show_config_dialog = !config.is_configured(); Self { config, instances: Vec::new(), app_state: AppState::Ready, show_config_dialog, show_reboot_confirm: None, rebooting_instance: None, last_refresh: None, rx: None, config_ak: String::new(), config_sk: String::new(), config_endpoint: "ecs.volcengineapi.com".to_string(), status_message: None, } } fn refresh_instances(&mut self) { if !self.config.is_configured() { self.app_state = AppState::Error("请先配置 Access Key".to_string()); return; } let (tx, rx) = mpsc::channel(); self.rx = Some(rx); self.app_state = AppState::Loading; let config = self.config.clone(); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { let client = VolcClient::new( config.access_key_id.clone(), config.secret_access_key.clone(), config.endpoint.clone(), ); match client.list_instances().await { Ok(instances) => ApiMessage::InstancesLoaded(instances), Err(e) => ApiMessage::Error(e), } }); let _ = tx.send(result); }); } fn process_messages(&mut self) { if let Some(rx) = &self.rx { if let Ok(msg) = rx.try_recv() { match msg { ApiMessage::InstancesLoaded(instances) => { self.instances = instances; self.app_state = AppState::Ready; self.last_refresh = Some( chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), ); } ApiMessage::RebootSuccess(id) => { self.status_message = Some(( format!("实例 {} 重启命令已发送", id), std::time::Instant::now(), )); self.rebooting_instance = None; self.refresh_instances(); } ApiMessage::Error(e) => { self.app_state = AppState::Error(e.clone()); self.status_message = Some((e, std::time::Instant::now())); } } self.rx = None; } } if let Some((_, time)) = &self.status_message { if time.elapsed().as_secs() > 5 { self.status_message = None; } } } fn open_config_dialog(&mut self) { self.config_ak = self.config.access_key_id.clone(); self.config_sk = self.config.secret_access_key.clone(); self.config_endpoint = self.config.endpoint.clone(); self.show_config_dialog = true; } fn save_config_dialog(&mut self) { self.config.access_key_id = self.config_ak.clone(); self.config.secret_access_key = self.config_sk.clone(); self.config.endpoint = self.config_endpoint.clone(); if let Err(e) = save_config(&self.config) { self.app_state = AppState::Error(format!("保存配置失败: {}", e)); } self.show_config_dialog = false; if self.config.is_configured() { self.refresh_instances(); } } } impl eframe::App for VolcManagerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.process_messages(); egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { ui.horizontal(|ui| { ui.heading("火山引擎服务器管理"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("⚙ 配置").clicked() { self.open_config_dialog(); } if ui.button("🔄 刷新").clicked() { self.refresh_instances(); } }); }); }); egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { ui.horizontal(|ui| { ui.label(format!("实例数: {}", self.instances.len())); if let Some(time) = &self.last_refresh { ui.label(format!("最后刷新: {}", time)); } if let Some((msg, _)) = &self.status_message { ui.label(format!("| {}", msg)); } }); }); egui::CentralPanel::default().show(ctx, |ui| { match &self.app_state { AppState::Loading => { ui.centered_and_justified(|ui| { ui.spinner(); ui.label(" 加载中..."); }); } AppState::Error(e) if self.instances.is_empty() => { ui.centered_and_justified(|ui| { ui.colored_label(egui::Color32::RED, e); }); } _ => { ui.horizontal_wrapped(|ui| { for instance in &self.instances { render_instance_card(ui, instance, |id| { self.show_reboot_confirm = Some(instance.clone()); }); } }); } } }); if self.show_config_dialog { egui::Window::new("配置") .collapsible(false) .resizable(false) .fixed_size([400.0, 250.0]) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { ui.vertical(|ui| { ui.label("Access Key ID:"); ui.text_edit_singleline(&mut self.config_ak); ui.add_space(8.0); ui.label("Secret Access Key:"); ui.text_edit_singleline(&mut self.config_sk); ui.add_space(8.0); ui.label("Endpoint:"); ui.text_edit_singleline(&mut self.config_endpoint); ui.add_space(16.0); ui.horizontal(|ui| { if ui.button("保存").clicked() { self.save_config_dialog(); } if ui.button("取消").clicked() { self.show_config_dialog = false; } }); }); }); } if let Some(instance) = &self.show_reboot_confirm { egui::Window::new("确认重启") .collapsible(false) .resizable(false) .fixed_size([350.0, 150.0]) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { ui.label(format!( "确定要重启实例 \"{}\" 吗?", instance.instance_name )); ui.label(format!("实例 ID: {}", instance.instance_id)); ui.add_space(16.0); ui.horizontal(|ui| { if ui.button("确定").clicked() { let config = self.config.clone(); let instance_id = instance.instance_id.clone(); let (tx, rx) = mpsc::channel(); self.rx = Some(rx); self.rebooting_instance = Some(instance_id.clone()); thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(async { let client = VolcClient::new( config.access_key_id.clone(), config.secret_access_key.clone(), config.endpoint.clone(), ); match client.reboot_instance(&instance_id).await { Ok(()) => ApiMessage::RebootSuccess(instance_id), Err(e) => ApiMessage::Error(e), } }); let _ = tx.send(result); }); self.show_reboot_confirm = None; } if ui.button("取消").clicked() { self.show_reboot_confirm = None; } }); }); } ctx.request_repaint(); } } fn render_instance_card( ui: &mut egui::Ui, instance: &InstanceInfo, on_reboot: impl FnOnce(&str), ) { let status_color = match instance.status { crate::types::InstanceStatus::Running => egui::Color32::GREEN, crate::types::InstanceStatus::Stopped => egui::Color32::GRAY, crate::types::InstanceStatus::Starting => egui::Color32::BLUE, crate::types::InstanceStatus::Stopping => egui::Color32::from_rgb(255, 165, 0), crate::types::InstanceStatus::Rebooting => egui::Color32::from_rgb(255, 165, 0), crate::types::InstanceStatus::Error => egui::Color32::RED, crate::types::InstanceStatus::Unknown => egui::Color32::YELLOW, }; egui::Frame::group(ui.style()) .fill(egui::Color32::from_additive_luminance(10)) .inner_margin(12.0) .show(ui, |ui| { ui.set_max_width(220.0); ui.label(egui::RichText::new(&instance.instance_name).strong()); ui.horizontal(|ui| { ui.small("状态:"); ui.colored_label(status_color, instance.status.label()); }); ui.small(format!("IP: {}", instance.public_ip)); ui.small(format!("区域: {}", instance.region_id)); ui.small(format!("规格: {}", instance.instance_type)); ui.add_space(8.0); if ui.button("🔄 重启").clicked() { on_reboot(&instance.instance_id); } }); } fn setup_fonts(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); // 字体文件在 Task 2 中添加,这里先用默认字体 // Task 2 完成后,取消下面注释并删除 fallback 逻辑 // fonts.font_data.insert( // "msyh".to_owned(), // std::sync::Arc::new(egui::FontData::from_static(include_bytes!( // "../fonts/msyh.ttc" // ))), // ); // fonts // .families // .entry(egui::FontFamily::Proportional) // .or_default() // .insert(0, "msyh".to_owned()); ctx.set_fonts(fonts); } ``` - [ ] **Step 6: 创建 src/main.rs** ```rust #![windows_subsystem = "windows"] mod api; mod app; mod config; mod types; use app::VolcManagerApp; fn main() { let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([900.0, 600.0]) .with_min_inner_size([600.0, 400.0]) .with_title("火山引擎服务器管理"), ..Default::default() }; eframe::run_native( "火山引擎服务器管理", native_options, Box::new(|cc| Ok(Box::new(VolcManagerApp::new(cc)))), ) .unwrap(); } ``` - [ ] **Step 7: 创建 .gitignore** ``` /target Cargo.lock ``` - [ ] **Step 8: 验证编译** 运行: `cargo check --target x86_64-pc-windows-gnu` 预期: 编译通过(可能有 unused warnings) 注意: `api.rs` 中使用了 `urlencoding` crate,需要在 Cargo.toml 中添加: ```toml urlencoding = "2.1" ``` - [ ] **Step 9: 提交** ```bash git add . git commit -m "feat: 初始化项目结构" ``` --- ### Task 2: 添加中文字体文件 **Files:** - Create: `fonts/msyh.ttc` - [ ] **Step 1: 复制微软雅黑字体文件** 从 Windows 系统复制字体文件: ```bash cp /c/Windows/Fonts/msyh.ttc fonts/msyh.ttc ``` 如果 `msyh.ttc` 是 TTC(TrueType Collection)格式,egui 可能不支持。需要提取其中的 TTF 文件。备选方案:使用 `msyh.ttf` 或使用开源字体。 检查字体格式: ```bash file fonts/msyh.ttc ``` 如果是 TTC 格式,使用 `fonttools` 提取: ```bash pip install fonttools pyftsubset msyh.ttc --output-file=msyh.ttf ``` 或者直接使用 Windows 自带的 `simsun.ttc`(宋体)或其他可用字体。 **备选方案**: 如果 TTC 不支持,下载开源中文字体 `NotoSansSC-Regular.ttf` 放到 `fonts/` 目录,并修改 `app.rs` 中的 `setup_fonts` 函数引用。 - [ ] **Step 2: 更新 app.rs 中的 setup_fonts 函数** 将 `src/app.rs` 中的 `setup_fonts` 函数替换为: ```rust fn setup_fonts(ctx: &egui::Context) { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "msyh".to_owned(), std::sync::Arc::new(egui::FontData::from_static(include_bytes!( "../fonts/msyh.ttc" ))), ); fonts .families .entry(egui::FontFamily::Proportional) .or_default() .insert(0, "msyh".to_owned()); fonts .families .entry(egui::FontFamily::Monospace) .or_default() .push("msyh".to_owned()); ctx.set_fonts(fonts); } ``` - [ ] **Step 3: 提交** ```bash git add fonts/ .gitignore git commit -m "feat: 添加中文字体支持" ``` --- ### Task 3: 完善 API 签名和错误处理 **Files:** - Modify: `src/api.rs` - [ ] **Step 1: 修正火山引擎签名算法** 火山引擎使用 V4 签名算法,不是简单的 HMAC-SHA256。更新 `api.rs` 中的签名逻辑: ```rust use crate::types::{InstanceInfo, InstanceStatus}; use base64::{engine::general_purpose::STANDARD, Engine}; use chrono::Utc; use hmac::{Hmac, Mac}; use reqwest::Client; use sha2::Sha256; use std::collections::HashMap; type HmacSha256 = Hmac; pub struct VolcClient { client: Client, access_key_id: String, secret_access_key: String, endpoint: String, } impl VolcClient { pub fn new(access_key_id: String, secret_access_key: String, endpoint: String) -> Self { Self { client: Client::builder() .danger_accept_invalid_certs(false) .build() .unwrap_or_default(), access_key_id, secret_access_key, endpoint, } } pub async fn list_instances(&self) -> Result, String> { let regions = self.get_regions().await?; let mut all_instances = Vec::new(); for region in regions { match self.list_instances_by_region(®ion).await { Ok(mut instances) => all_instances.append(&mut instances), Err(e) => eprintln!("Failed to list instances in {}: {}", region, e), } } Ok(all_instances) } async fn get_regions(&self) -> Result, String> { let mut params = HashMap::new(); params.insert("Action", "DescribeRegions"); params.insert("Version", "2020-04-01"); let resp = self.send_request(params).await?; let regions = resp["Result"] .as_array() .map(|arr| { arr.iter() .filter_map(|r| r["RegionId"].as_str().map(String::from)) .collect() }) .or_else(|| { resp["RegionInfos"].as_array().map(|arr| { arr.iter() .filter_map(|r| r["RegionId"].as_str().map(String::from)) .collect() }) }) .unwrap_or_else(|| { vec![ "cn-beijing".to_string(), "cn-shanghai".to_string(), "cn-guangzhou".to_string(), "cn-chengdu".to_string(), "ap-singapore-1".to_string(), ] }); Ok(regions) } async fn list_instances_by_region(&self, region: &str) -> Result, String> { let mut params = HashMap::new(); params.insert("Action", "DescribeInstances"); params.insert("Version", "2020-04-01"); params.insert("RegionId", region); params.insert("MaxResults", "100"); let resp = self.send_request(params).await?; let instances = resp["Result"]["Instances"] .as_array() .or_else(|| resp["Instances"].as_array()) .map(|arr| { arr.iter() .filter_map(|inst| { let status_str = inst["Status"].as_str().unwrap_or("UNKNOWN"); let private_ip = inst["PrimaryIpAddress"] .as_str() .or_else(|| { inst["NetworkInterfaces"] .as_array() .and_then(|nets| { nets.first() .and_then(|n| n["PrimaryIpAddress"].as_str()) }) }) .unwrap_or("-") .to_string(); let public_ip = inst["EipAddress"]["IPAddress"] .as_str() .or_else(|| { inst["PublicIpAddresses"] .as_array() .and_then(|ips| ips.first().and_then(|ip| ip.as_str())) }) .or_else(|| inst["EipAddress"].as_str()) .unwrap_or("-") .to_string(); Some(InstanceInfo { instance_id: inst["InstanceId"] .as_str() .unwrap_or("") .to_string(), instance_name: inst["InstanceName"] .as_str() .unwrap_or( inst["InstanceId"].as_str().unwrap_or(""), ) .to_string(), status: InstanceStatus::from_str(status_str), private_ip, public_ip, zone_id: inst["ZoneId"] .as_str() .unwrap_or("-") .to_string(), region_id: region.to_string(), instance_type: inst["InstanceType"] .as_str() .unwrap_or("-") .to_string(), creation_time: inst["CreatedAt"] .as_str() .or_else(|| inst["CreationTime"].as_str()) .unwrap_or("-") .to_string(), }) }) .collect() }) .unwrap_or_default(); Ok(instances) } pub async fn reboot_instance(&self, instance_id: &str) -> Result<(), String> { let mut params = HashMap::new(); params.insert("Action", "RebootInstance"); params.insert("Version", "2020-04-01"); params.insert("InstanceId", instance_id); let _resp = self.send_request(params).await?; Ok(()) } async fn send_request( &self, mut params: HashMap<&str, &str>, ) -> Result { params.insert("AccessKeyId", &self.access_key_id); params.insert("SignatureVersion", "1.0"); params.insert("SignatureMethod", "HMAC-SHA256"); let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); params.insert("Timestamp", ×tamp); let string_to_sign = Self::build_string_to_sign(¶ms); let signature = self.sign(&string_to_sign); params.insert("Signature", &signature); let url = format!("https://{}/", self.endpoint); let resp = self .client .get(&url) .query(¶ms) .send() .await .map_err(|e| format!("网络请求失败: {}", e))?; let status = resp.status(); let body: serde_json::Value = resp .json() .await .map_err(|e| format!("解析响应失败: {}", e))?; if !status.is_success() { let msg = body["ResponseMetadata"]["Error"]["Message"] .as_str() .unwrap_or("未知错误") .to_string(); return Err(msg); } if let Some(error) = body["ResponseMetadata"]["Error"].as_object() { if !error.is_empty() { let msg = body["ResponseMetadata"]["Error"]["Message"] .as_str() .unwrap_or("API 错误") .to_string(); return Err(msg); } } Ok(body) } fn build_string_to_sign(params: &HashMap<&str, &str>) -> String { let mut keys: Vec<&str> = params.keys().copied().collect(); keys.sort(); let query: Vec = keys .iter() .map(|k| { format!( "{}={}", url_encode(k), url_encode(params.get(k).unwrap()) ) }) .collect(); format!("GET\n/\n{}\n", query.join("&")) } fn sign(&self, string_to_sign: &str) -> String { let mut mac = HmacSha256::new_from_slice(self.secret_access_key.as_bytes()) .expect("HMAC can take key of any size"); mac.update(string_to_sign.as_bytes()); let result = mac.finalize(); STANDARD.encode(result.into_bytes()) } } fn url_encode(s: &str) -> String { urlencoding::encode(s).into_owned() } ``` - [ ] **Step 2: 添加 urlencoding 依赖** 在 `Cargo.toml` 中添加: ```toml urlencoding = "2.1" ``` - [ ] **Step 3: 验证编译** 运行: `cargo check --target x86_64-pc-windows-gnu` - [ ] **Step 4: 提交** ```bash git add src/api.rs Cargo.toml git commit -m "feat: 完善 API 签名和错误处理" ``` --- ### Task 4: Release 编译配置 **Files:** - Modify: `Cargo.toml` - [ ] **Step 1: 添加 release profile 配置** 在 `Cargo.toml` 末尾添加: ```toml [profile.release] opt-level = 2 strip = true lto = true codegen-units = 1 ``` - [ ] **Step 2: 执行 release 编译** 运行: `cargo build --release --target x86_64-pc-windows-gnu` 预期: 生成 `target/x86_64-pc-windows-gnu/release/volcengine-server-manager.exe` - [ ] **Step 3: 验证 exe 无控制台** 运行: `file target/x86_64-pc-windows-gnu/release/volcengine-server-manager.exe` 确认输出包含 `windows gui` 而非 `console` - [ ] **Step 4: 提交** ```bash git add Cargo.toml git commit -m "chore: 添加 release 编译优化配置" ``` --- ### Task 5: 首次运行测试 **Files:** - 无修改 - [ ] **Step 1: 运行程序** ```bash ./target/x86_64-pc-windows-gnu/release/volcengine-server-manager.exe ``` - [ ] **Step 2: 验证行为** 1. 首次启动应弹出配置窗口 2. 输入 AK/SK 后点击保存,应自动加载实例列表 3. 实例以卡片形式展示,显示名称、状态、IP、区域、规格 4. 点击"重启"按钮弹出确认窗口 5. 确认重启后显示成功消息 6. 状态栏显示实例数和最后刷新时间 - [ ] **Step 3: 提交** ```bash git add . git commit -m "test: 首次运行验证通过" ```