Compare commits

..
Author SHA1 Message Date
AxTps d17a66e44f chore: 补充 1.0.2-beta7 更新公告
Sync source to CNB / Sync Git ref (push) Skipped
Repository checks / typos (push) Canceled after 0s
Repository checks / tombi (push) Canceled after 0s
Rust checks / shear (push) Canceled after 0s
Starlight release / Release preflight (push) Canceled after 0s
Starlight release / Build (Windows x64) (push) Canceled after 0s
Starlight release / Build (Linux arm64) (push) Canceled after 0s
Starlight release / Build (Linux x64) (push) Canceled after 0s
Starlight release / Build (macOS universal) (push) Canceled after 0s
Starlight release / Verify and publish (push) Canceled after 0s
Prepare pnpm cache / prepare (push) Canceled after 0s
Axolotl desktop CI / guardrails (push) Canceled after 0s
Axolotl desktop CI / desktop (macos-latest) (push) Canceled after 0s
Axolotl desktop CI / desktop (ubuntu-latest) (push) Canceled after 0s
Axolotl desktop CI / desktop (windows-latest) (push) Canceled after 0s
Axolotl desktop CI / website (push) Canceled after 0s
汇总 beta6 以来的改动:实时日志积压修复、更新通道默认值跟随安装
包版本、Linux 游戏目录默认回退到用户主目录。
2026-10-17 18:56:23 +08:00
AxTps 871b0ee001 Merge pull request #21 from ApTx2012/feat/instance
Sync source to CNB / Sync Git ref (push) Skipped
Rust checks / shear (push) Canceled after 0s
Axolotl desktop CI / guardrails (push) Canceled after 0s
Axolotl desktop CI / desktop (macos-latest) (push) Canceled after 0s
Axolotl desktop CI / desktop (ubuntu-latest) (push) Canceled after 0s
Axolotl desktop CI / desktop (windows-latest) (push) Canceled after 0s
Axolotl desktop CI / website (push) Canceled after 0s
Repository checks / typos (push) Canceled after 0s
Repository checks / tombi (push) Canceled after 0s
fix: Linux 系统安装时游戏目录默认回退到用户主目录
2026-10-17 18:17:54 +08:00
AxTps adebde714d Merge pull request #20 from ApTx2012/feat/updater
fix: 更新渠道默认值跟随安装包版本
2026-10-17 18:17:41 +08:00
AxTps 13a56f4200 fix: Linux 系统安装时游戏目录默认回退到用户主目录
Sync source to CNB / Sync Git ref (push) Skipped
从 deb 等系统包安装时,启动器可执行文件位于 /usr/bin,此前一键安装
StarLight 实例的"选择游戏目录"弹窗会默认到这个系统目录,普通用户既
无写权限,也不适合存放游戏数据。

将默认目录计算移入 theseus::default_game_dir_root():Linux 下若可执行
文件位于 /usr、/opt、/snap 等系统路径,回退到用户主目录;便携版与其
他平台仍使用可执行文件所在目录。
2026-10-17 16:12:28 +08:00
AxTps 8b94e7dbbe fix: 更新渠道默认值跟随安装包版本
Sync source to CNB / Sync Git ref (push) Skipped
从 GitHub 下载 Beta 安装包安装后,更新通道默认选中 Release,
因为 update-channel.json 状态文件不存在时会硬编码回退到 release。

新增 theseus::default_update_channel(),按编译期版本号判断:含预发布
后缀(如 1.0.2-beta7)默认 Beta,否则 Release。数据库渠道与设置页
渠道两处默认值统一改为调用它,保证两者一致。
2026-10-13 22:11:06 +08:00
7 changed files with 104 additions and 88 deletions

No files matched your search

@@ -32,6 +32,31 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
] ]
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [ export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
{
id: 'launcher-1.0.2-beta7',
version: '1.0.2-beta7',
publishedAt: '2026-10-17',
title: {
'en-US': 'Starlight Launcher 1.0.2-beta7',
'zh-CN': 'Starlight Launcher 1.0.2-beta7',
},
changes: {
fixed: [
{
'en-US': 'Fixed real-time log backlog that could stall the console under heavy output.',
'zh-CN': '修复实时日志积压导致控制台卡顿的问题。',
},
{
'en-US': 'The update channel now defaults to the build type: installing a Beta build starts on the Beta channel instead of Release.',
'zh-CN': '更新通道默认值改为跟随安装包版本:安装 Beta 版后默认使用 Beta 通道,不再回落为 Release。',
},
{
'en-US': 'On Linux, the default game directory for StarLight instances no longer points at the system install path (e.g. /usr/bin); it falls back to the home directory.',
'zh-CN': 'Linux 上 StarLight 实例的默认游戏目录不再指向系统安装路径(如 /usr/bin),改为回退到用户主目录。',
},
],
},
},
{ {
id: 'launcher-1.0.2-beta6', id: 'launcher-1.0.2-beta6',
version: '1.0.2-beta6', version: '1.0.2-beta6',
+3 -6
View File
@@ -324,12 +324,9 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
/// external game-directory root for the one-click StarLight install flow. /// external game-directory root for the one-click StarLight install flow.
#[tauri::command] #[tauri::command]
fn get_launcher_root_dir() -> api::Result<String> { fn get_launcher_root_dir() -> api::Result<String> {
let exe = std::env::current_exe().map_err(|error| { let dir = theseus::default_game_dir_root().ok_or_else(|| {
theseus::Error::from(theseus::ErrorKind::FSError(error.to_string()))
})?;
let dir = exe.parent().ok_or_else(|| {
theseus::Error::from(theseus::ErrorKind::FSError( theseus::Error::from(theseus::ErrorKind::FSError(
"Launcher executable has no parent directory".to_string(), "Could not determine a default game directory root".to_string(),
)) ))
})?; })?;
Ok(dir.to_string_lossy().into_owned()) Ok(dir.to_string_lossy().into_owned())
@@ -339,7 +336,7 @@ fn get_launcher_root_dir() -> api::Result<String> {
fn get_update_channel(app: tauri::AppHandle) -> api::Result<String> { fn get_update_channel(app: tauri::AppHandle) -> api::Result<String> {
let channel = read_update_channel_state(&app)? let channel = read_update_channel_state(&app)?
.active_channel .active_channel
.unwrap_or_else(|| "release".to_string()); .unwrap_or_else(|| theseus::default_update_channel().to_string());
match channel.as_str() { match channel.as_str() {
"release" | "beta" => Ok(channel), "release" | "beta" => Ok(channel),
@@ -109,7 +109,6 @@ pub struct CrashAnalysisMod {
struct SourceCandidate { struct SourceCandidate {
path: PathBuf, path: PathBuf,
filename: String, filename: String,
upload_name: String,
source_type: &'static str, source_type: &'static str,
modified: SystemTime, modified: SystemTime,
} }
@@ -117,25 +116,10 @@ struct SourceCandidate {
#[derive(Debug)] #[derive(Debug)]
struct SourceText { struct SourceText {
filename: String, filename: String,
upload_name: String,
source_type: &'static str, source_type: &'static str,
modified: SystemTime, modified: SystemTime,
content: String, content: String,
} }
pub(super) struct CollectedCrashLog {
pub name: String,
pub content: CensoredString,
}
pub(super) struct CollectedCrashLogs {
pub combined_log: CensoredString,
pub sources: Vec<CollectedCrashLog>,
}
struct CrashContext {
sources: Vec<SourceText>,
credentials: Vec<Credentials>,
}
struct Rule { struct Rule {
id: &'static str, id: &'static str,
@@ -1201,25 +1185,20 @@ async fn collect_candidates(
) -> crate::Result<Vec<SourceCandidate>> { ) -> crate::Result<Vec<SourceCandidate>> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
let mut locations = vec![ let mut locations = vec![
( (directories.game_logs_dir(instance_root), "minecraft_log"),
directories.game_logs_dir(instance_root),
"minecraft_log",
None,
),
( (
directories.game_crash_reports_dir(instance_root), directories.game_crash_reports_dir(instance_root),
"crash_report", "crash_report",
Some("crash-reports"),
), ),
(instance_root.to_path_buf(), "instance_log", None), (instance_root.to_path_buf(), "instance_log"),
]; ];
if let Some(dot_minecraft) = isolated_minecraft_root(instance_root) { if let Some(dot_minecraft) = isolated_minecraft_root(instance_root) {
// PCL-CE also searches the shared `.minecraft` root for JVM crash // PCL-CE also searches the shared `.minecraft` root for JVM crash
// logs when a version-isolated instance is active. // logs when a version-isolated instance is active.
locations.push((dot_minecraft, "minecraft_root_log", None)); locations.push((dot_minecraft, "minecraft_root_log"));
} }
for (directory, default_type, upload_prefix) in locations { for (directory, default_type) in locations {
if !tokio::fs::try_exists(&directory).await? { if !tokio::fs::try_exists(&directory).await? {
continue; continue;
} }
@@ -1269,9 +1248,6 @@ async fn collect_candidates(
}; };
candidates.push(SourceCandidate { candidates.push(SourceCandidate {
path: entry.path(), path: entry.path(),
upload_name: upload_prefix
.map(|prefix| format!("{prefix}/{filename}"))
.unwrap_or_else(|| filename.clone()),
filename, filename,
source_type, source_type,
modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH), modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
@@ -1281,53 +1257,6 @@ async fn collect_candidates(
Ok(candidates) Ok(candidates)
} }
async fn collect_crash_context(
instance_id: &str,
) -> crate::Result<CrashContext> {
let state = State::get().await?;
let (instance_path, game_dir_override) =
resolve_instance_path(instance_id, &state).await?;
let instance_root = state
.directories
.resolve_game_dir(&instance_path, game_dir_override.as_deref());
let candidates =
collect_candidates(&instance_root, &state.directories).await?;
let selected = select_run_candidates(candidates);
let sources = read_sources(selected).await;
let credentials = Credentials::get_all(&state.pool)
.await?
.into_iter()
.map(|credential| credential.1)
.collect::<Vec<_>>();
Ok(CrashContext {
sources,
credentials,
})
}
pub(super) async fn collect_crash_logs(
instance_id: &str,
) -> crate::Result<CollectedCrashLogs> {
let CrashContext {
sources,
credentials,
..
} = collect_crash_context(instance_id).await?;
let combined_log =
CensoredString::censor(build_combined_log(&sources), &credentials);
let sources = sources
.into_iter()
.map(|source| CollectedCrashLog {
name: source.upload_name,
content: CensoredString::censor(source.content, &credentials),
})
.collect();
Ok(CollectedCrashLogs {
combined_log,
sources,
})
}
fn isolated_minecraft_root(instance_root: &Path) -> Option<PathBuf> { fn isolated_minecraft_root(instance_root: &Path) -> Option<PathBuf> {
let versions_dir = instance_root.parent()?; let versions_dir = instance_root.parent()?;
if versions_dir if versions_dir
@@ -1391,7 +1320,6 @@ async fn read_sources(candidates: Vec<SourceCandidate>) -> Vec<SourceText> {
}; };
sources.push(SourceText { sources.push(SourceText {
filename: candidate.filename, filename: candidate.filename,
upload_name: candidate.upload_name,
source_type: candidate.source_type, source_type: candidate.source_type,
modified: candidate.modified, modified: candidate.modified,
content: String::from_utf8_lossy(bytes).into_owned(), content: String::from_utf8_lossy(bytes).into_owned(),
@@ -1851,7 +1779,6 @@ mod tests {
fn source(filename: &str, content: &str) -> SourceText { fn source(filename: &str, content: &str) -> SourceText {
SourceText { SourceText {
filename: filename.to_string(), filename: filename.to_string(),
upload_name: filename.to_string(),
source_type: "minecraft_log", source_type: "minecraft_log",
modified: SystemTime::now(), modified: SystemTime::now(),
content: content.to_string(), content: content.to_string(),
@@ -1875,7 +1802,6 @@ mod tests {
let candidates = vec![SourceCandidate { let candidates = vec![SourceCandidate {
path: PathBuf::from("launcher_log.txt"), path: PathBuf::from("launcher_log.txt"),
filename: "launcher_log.txt".to_string(), filename: "launcher_log.txt".to_string(),
upload_name: "launcher_log.txt".to_string(),
source_type: "minecraft_log", source_type: "minecraft_log",
modified: SystemTime::now() - Duration::from_secs(181), modified: SystemTime::now() - Duration::from_secs(181),
}]; }];
+28 -1
View File
@@ -4,7 +4,8 @@ use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
use super::crash_analysis::collect_crash_logs; use super::CensoredString;
use super::crash_analysis::analyze_crash;
use crate::State; use crate::State;
use crate::emit_logshare_ai_event; use crate::emit_logshare_ai_event;
@@ -199,6 +200,32 @@ fn source_identifier() -> String {
format!("{LOGSHARE_SOURCE_PREFIX}/{}", env!("CARGO_PKG_VERSION")) format!("{LOGSHARE_SOURCE_PREFIX}/{}", env!("CARGO_PKG_VERSION"))
} }
struct CollectedCrashLog {
name: String,
content: CensoredString,
}
struct CollectedCrashLogs {
combined_log: CensoredString,
sources: Vec<CollectedCrashLog>,
}
async fn collect_crash_logs(
instance_id: &str,
) -> crate::Result<CollectedCrashLogs> {
let analysis = analyze_crash(instance_id).await?;
Ok(CollectedCrashLogs {
combined_log: analysis.combined_log,
sources: analysis
.sources
.into_iter()
.map(|source| CollectedCrashLog {
name: source.filename,
content: source.content,
})
.collect(),
})
}
async fn prepare_upload(instance_id: &str) -> crate::Result<UploadBody> { async fn prepare_upload(instance_id: &str) -> crate::Result<UploadBody> {
let settings = get_log_share_settings().await?; let settings = get_log_share_settings().await?;
let logs = collect_crash_logs(instance_id).await?; let logs = collect_crash_logs(instance_id).await?;
+2 -2
View File
@@ -33,13 +33,13 @@ pub use logger::start_logger;
pub use state::db::{ pub use state::db::{
backup_current_app_db_for_update, beta_database_exists, backup_current_app_db_for_update, beta_database_exists,
copy_database_between_channels, copy_release_database_to_beta, copy_database_between_channels, copy_release_database_to_beta,
current_app_database_path, current_app_database_path, default_update_channel,
}; };
pub use state::{DirectoryInfo, State}; pub use state::{DirectoryInfo, State};
pub use storage::*; pub use storage::*;
pub use util::fetch::build_proxied_client; pub use util::fetch::build_proxied_client;
pub use util::file_lock::{LockingProcess, get_locking_processes}; pub use util::file_lock::{LockingProcess, get_locking_processes};
pub use util::platform::is_process_elevated; pub use util::platform::{default_game_dir_root, is_process_elevated};
pub use util::proxy::{ProxyConfig, ProxyMode, ProxyTestResult}; pub use util::proxy::{ProxyConfig, ProxyMode, ProxyTestResult};
pub use util::symlink::SymlinkCapability; pub use util::symlink::SymlinkCapability;
+13 -1
View File
@@ -304,6 +304,18 @@ async fn app_db_path(settings_dir: &Path) -> crate::Result<PathBuf> {
Ok(settings_dir.join(channel).join(LEGACY_APP_DB_FILE)) Ok(settings_dir.join(channel).join(LEGACY_APP_DB_FILE))
} }
/// The update channel a fresh install should default to. Beta builds carry a
/// pre-release version (e.g. `1.0.2-beta7`), so their installers should start
/// on the Beta channel instead of silently falling back to Release.
pub fn default_update_channel() -> &'static str {
let version = env!("CARGO_PKG_VERSION");
if version.contains('-') {
"beta"
} else {
"release"
}
}
async fn read_update_channel( async fn read_update_channel(
settings_dir: &Path, settings_dir: &Path,
) -> crate::Result<&'static str> { ) -> crate::Result<&'static str> {
@@ -311,7 +323,7 @@ async fn read_update_channel(
let contents = match tokio::fs::read_to_string(&path).await { let contents = match tokio::fs::read_to_string(&path).await {
Ok(contents) => contents, Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => { Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok("release"); return Ok(default_update_channel());
} }
Err(error) => return Err(error.into()), Err(error) => return Err(error.into()),
}; };
+29
View File
@@ -1,5 +1,6 @@
//! Platform-related code //! Platform-related code
use daedalus::minecraft::{Os, OsRule}; use daedalus::minecraft::{Os, OsRule};
use std::path::PathBuf;
/// (e.g. "Run as administrator" on Windows). /// (e.g. "Run as administrator" on Windows).
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -143,3 +144,31 @@ pub fn classpath_separator(java_arch: &str) -> &'static str {
Os::Windows | Os::WindowsArm64 => ";", Os::Windows | Os::WindowsArm64 => ";",
} }
} }
/// Root directory used as the default for one-click StarLight instance
/// installs. Portable builds live next to their executable, which is a fine
/// place for game data. System installs (e.g. a Linux `.deb` under `/usr/bin`)
/// do not, so fall back to the user's home directory there.
pub fn default_game_dir_root() -> Option<PathBuf> {
let exe_dir = std::env::current_exe().ok()?.parent()?.to_path_buf();
if cfg!(target_os = "linux") && is_system_install_dir(&exe_dir) {
return dirs::home_dir();
}
Some(exe_dir)
}
#[cfg(target_os = "linux")]
fn is_system_install_dir(path: &std::path::Path) -> bool {
const SYSTEM_PREFIXES: &[&str] = &[
"/usr/", "/opt/", "/snap/", "/bin/", "/sbin/", "/lib/", "/lib64/",
];
let display = path.to_string_lossy();
SYSTEM_PREFIXES
.iter()
.any(|prefix| display.starts_with(prefix))
}
#[cfg(not(target_os = "linux"))]
fn is_system_install_dir(_path: &std::path::Path) -> bool {
false
}