Skip to content
This repository was archived by the owner on Oct 23, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout
Expand All @@ -22,12 +23,41 @@ jobs:
with:
dotnet-version: "10.0.x"

- name: Build (Release)
run: dotnet build TraeTools.csproj -c Release --nologo
# 主程序现在是双目标(net10.0-windows;net10.0-android),本作业只负责桌面端:
# 用 -f 限定框架,否则会连带构建 android TFM 而本作业未安装 Android workload
- name: Build Windows (Release)
run: dotnet build TraeTools.csproj -c Release -f net10.0-windows --nologo

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: trae-tools-build
name: trae-tools-build-windows
path: bin/Release/net10.0-windows/
if-no-files-found: error
if-no-files-found: error

build-android:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

# Android workload:含 Microsoft.Android SDK / OpenJDK 等构建依赖(首次安装约 5-10 分钟)
- name: Install Android workload
run: dotnet workload install android

# EnableWindowsTargeting:双目标项目还原时会一并还原 net10.0-windows,
# ubuntu 上必须允许下载 Windows 目标包,否则报 NETSDK1100
- name: Build Android (Release)
run: dotnet build TraeTools.csproj -c Release -f net10.0-android -p:EnableWindowsTargeting=true --nologo

- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: trae-tools-android-apk
path: bin/Release/net10.0-android/*.apk
if-no-files-found: error
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ jobs:
# libsodium 必须与 exe 同目录,只替换 exe 会得到"缺 DLL 打不开"的残废程序。
# 应用内更新器(UpdaterService)现在下载 `TraeTools-win-x64.zip` → 校验 SHA-256 → 解包 →
# 整套文件原子替换 → 重启;zip 压缩也抵消了 AOT 原生库带来的体积增长。
# ⚠️ 主程序是双目标(net10.0-windows;net10.0-android),必须用 -f 限定框架:
# 否则 -r win-x64 会渗入 android TFM 的还原,报 NU1102(找不到 Mono 的 win-x64 运行时包)。
- name: Publish main (NativeAOT directory form)
run: dotnet publish TraeTools.csproj -c Release -r win-x64 --self-contained true -p:EnableAot=true -o publish_aot
run: dotnet publish TraeTools.csproj -c Release -f net10.0-windows -r win-x64 --self-contained true -p:EnableAot=true -o publish_aot

# 兼容引导包:历史客户端内的旧更新器只会下载名为 `TraeTools.exe` 的资产并替换自身。
# 提供单文件自包含版(csproj 已开 PublishTrimmed=full,约 23MB),老客户端可一次性升到新版,
# 之后其内置的新更新器会改走上面的 zip 整包。
- name: Publish main (single-file bootstrap, legacy updater compatible)
run: dotnet publish TraeTools.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -o publish
run: dotnet publish TraeTools.csproj -c Release -f net10.0-windows -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -o publish

# P2-1:登录宿主(WinForms + WebView2)是独立进程,不参与主程序的 NativeAOT,
# 因此单独发布并随压缩包/Release 一起分发;缺失时主程序自动回退到"手动粘贴 Token"。
Expand Down
134 changes: 130 additions & 4 deletions App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,152 @@

namespace TraeTools;

public partial class App : Application
/// <summary>
/// 应用入口(双平台)。
/// 注意:.NET Android SDK 会生成含 Android.App 的全局 using,
/// 本文件中的 Application 一律写全称 Avalonia.Application,否则与之歧义(CS0104)。
/// </summary>
public partial class App : Avalonia.Application
{
/// <summary>系统托盘图标全局引用(TrayIcon 无源头引用会被 GC 回收,故存于此)。</summary>
/// <summary>系统托盘图标全局引用(TrayIcon 无源头引用会被 GC 回收,故存于此;仅桌面端使用)。</summary>
public static TrayIcon? TrayRef;

/// <summary>
/// 「需要真正退出」的退出入口。桌面端由 MainWindow 挂上(关闭窗口默认是「最小化到托盘」,
/// 该行为会拦住退出),Android 上没有窗口,<see cref="RequestExit"/> 会退回
/// ApplicationLifetime 的默认退出方式。
/// </summary>
public static System.Action? ExitRequested;

/// <summary>请求退出应用(不同意 EULA 等场景使用)。</summary>
public static void RequestExit()
{
try
{
if (ExitRequested != null)
{
ExitRequested();
return;
}
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.Shutdown();
}
catch
{
// 退出失败不阻塞其他逻辑
}
}

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
try
{
AvaloniaXamlLoader.Load(this);
Platform.CrashLog.Boot("App.Initialize completed");
}
catch (System.Exception ex)
{
Platform.CrashLog.Write("App.Initialize", ex, true);
throw;
}
}

public override void OnFrameworkInitializationCompleted()
{
Platform.CrashLog.Install();
Platform.CrashLog.Boot(
$"App.OnFrameworkInitializationCompleted lifetime={ApplicationLifetime?.GetType().FullName}");

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel(),
};
Platform.CrashLog.Boot("App: desktop MainWindow created");
}
#if ANDROID
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
// 上次运行崩溃过:本次先进错误页(可复制堆栈 / 重试),避免反复闪退
var flag = Platform.CrashLog.ReadFlag();
if (!string.IsNullOrEmpty(flag))
{
Platform.CrashLog.Boot("App: previous crash detected -> startup error view");
singleView.MainView = BuildErrorView(singleView, "上次启动崩溃了", flag);
base.OnFrameworkInitializationCompleted();
return;
}

try
{
// Android:单视图生命周期,MainView(UserControl)作为根视图全屏展示。
// 不能用 MainWindow(Window):单视图平台创建 Window 会抛 NotSupportedException。
singleView.MainView = new MainView
{
DataContext = new MainViewModel(),
};
Platform.CrashLog.Boot("App: single-view MainView created");
}
catch (System.Exception ex)
{
// 主界面构建失败也不能闪退:显示可读的错误页
Platform.CrashLog.Write("App: create MainView failed", ex, true);
singleView.MainView = BuildErrorView(singleView, "界面初始化失败", ex.ToString());
}
}
#endif

base.OnFrameworkInitializationCompleted();
}
}

#if ANDROID
/// <summary>构建启动错误页:异常堆栈 + 启动日志 + 「清除并重新加载界面」。</summary>
private static Control BuildErrorView(
ISingleViewApplicationLifetime lifetime,
string title,
string detail)
{
var body = detail + System.Environment.NewLine + System.Environment.NewLine
+ "==== 启动日志(尾部)====" + System.Environment.NewLine
+ Platform.CrashLog.ReadRecent();

var text = new SelectableTextBlock
{
Text = title + System.Environment.NewLine + System.Environment.NewLine + body,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
FontFamily = new Avalonia.Media.FontFamily("Consolas,monospace"),
FontSize = 12,
Margin = new Thickness(16),
};

var retry = new Button
{
Content = "清除崩溃标记并重新加载界面",
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Margin = new Thickness(16, 0, 16, 24),
Padding = new Thickness(16, 8),
};
retry.Click += (_, _) =>
{
Platform.CrashLog.ClearFlag();
try
{
lifetime.MainView = new MainView { DataContext = new MainViewModel() };
Platform.CrashLog.Boot("App: retry create MainView ok");
}
catch (System.Exception ex)
{
Platform.CrashLog.Write("App: retry create MainView failed", ex, true);
}
};

var grid = new Grid { RowDefinitions = new RowDefinitions("*,Auto") };
var scroller = new ScrollViewer { Content = text };
grid.Children.Add(scroller);
Grid.SetRow(retry, 1);
grid.Children.Add(retry);
return grid;
}
#endif
}
113 changes: 113 additions & 0 deletions Platform/BrowserLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Diagnostics;

namespace TraeTools.Platform;

/// <summary>
/// 用系统默认浏览器打开 URL(跨平台封装)。
///
/// <para><b>失败必须可见</b>:早期实现把异常全部吞掉且返回 void,于是 Android 上
/// 「授权 GitHub」按钮点了没反应时,用户与日志都拿不到任何线索。
/// 现在返回 bool,并把失败原因写进平台日志(category = browser)。</para>
///
/// <para><b>Android 侧要点</b>:
/// <list type="bullet">
/// <item>优先用 Activity 启动(<c>MainActivity.Current</c>),Activity 不可用时回退 Application 上下文
/// —— 后者必须带 FLAG_ACTIVITY_NEW_TASK,否则抛 AndroidRuntimeException。</item>
/// <item>部分定制 ROM 上直接 startActivity 会被拦,此时用 <c>Intent.CreateChooser</c> 再试一次。</item>
/// <item>浏览器属于「隐式 Intent 解析」,Android 11+ 的包可见性默认不影响 startActivity,
/// 但为兼容个别 ROM 仍在清单里声明了 &lt;queries&gt;(见 Properties/AndroidManifest.xml)。</item>
/// </list></para>
/// </summary>
internal static class BrowserLauncher
{
/// <summary>打开 URL:成功返回 true;失败返回 false 并已写日志。</summary>
public static bool Open(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
PlatformLog.Warn("browser", "打开浏览器失败:URL 为空");
return false;
}
try
{
#if ANDROID
return OpenOnAndroid(url);
#else
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
return true;
#endif
}
catch (Exception ex)
{
PlatformLog.Warn("browser", $"打开浏览器失败:{ex.GetType().Name}: {ex.Message}(URL={url})");
return false;
}
}

#if ANDROID
private static bool OpenOnAndroid(string url)
{
var uri = Android.Net.Uri.Parse(url);
if (uri == null)
{
PlatformLog.Warn("browser", $"打开浏览器失败:URL 无法解析({url})");
return false;
}

var context = Android.App.Application.Context;
var activity = TraeTools.MainActivity.Current;

// ① 首选:Activity 直接启动(无需 NEW_TASK,前台切换最自然)
if (activity != null)
{
try
{
var intent = new Android.Content.Intent(Android.Content.Intent.ActionView, uri);
activity.StartActivity(intent);
return true;
}
catch (Exception ex)
{
PlatformLog.Warn("browser", "Activity 启动浏览器失败,回退 Application 上下文:" + ex.Message);
}
}

// ② 回退:Application 上下文(必须 NEW_TASK)
if (context != null)
{
try
{
var intent = new Android.Content.Intent(Android.Content.Intent.ActionView, uri);
intent.AddFlags(Android.Content.ActivityFlags.NewTask);
context.StartActivity(intent);
return true;
}
catch (Exception ex)
{
PlatformLog.Warn("browser", "Application 启动浏览器失败,改用选择器:" + ex.Message);
}

// ③ 再回退:让系统弹「用哪个应用打开」,规避个别 ROM 对直接 startActivity 的拦截
try
{
var view = new Android.Content.Intent(Android.Content.Intent.ActionView, uri);
view.AddFlags(Android.Content.ActivityFlags.NewTask);
var chooser = Android.Content.Intent.CreateChooser(view, "选择浏览器打开授权页");
chooser!.AddFlags(Android.Content.ActivityFlags.NewTask);
context.StartActivity(chooser);
return true;
}
catch (Exception ex)
{
PlatformLog.Warn("browser",
$"打开浏览器失败(三种方式均未成功):{ex.GetType().Name}: {ex.Message}(URL={url})");
return false;
}
}

PlatformLog.Warn("browser", "打开浏览器失败:Android 上下文不可用(Application.Context 为 null)");
return false;
}
#endif
}
Loading