diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
deleted file mode 100644
index cf2674e..0000000
--- a/.github/workflows/android.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Android
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- 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"
-
- - name: Install Android workload
- run: dotnet workload install android
-
- - name: Build Android (Release)
- run: dotnet build Android/TraeTools.Android.csproj -c Release --nologo
-
- - name: Upload APK
- uses: actions/upload-artifact@v4
- with:
- name: trae-tools-android-apk
- path: Android/bin/Release/net10.0-android/*.apk
- if-no-files-found: error
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
new file mode 100644
index 0000000..05dfad9
--- /dev/null
+++ b/.github/workflows/bump-version.yml
@@ -0,0 +1,122 @@
+name: Bump Version
+
+# 手动触发的版本号自增工作流。
+#
+# 设计要点:
+# 1. version.json 是**唯一版本源**(csproj 的 Version/InformationalVersion/ApplicationDisplayVersion
+# 由 ReadVersionFromJson 目标自动读取并覆盖),因此只需改 version.json;
+# 但 Android 的 ApplicationVersion(versionCode)必须**单调递增**才能覆盖安装,需一并自增。
+# 2. 使用 GITHUB_TOKEN 推送的提交**不会**触发其它工作流(GitHub 官方限制),
+# 因此这里在推送后显式派发 ci.yml 再验证一次(workflow_dispatch 不受该限制影响)。
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "目标版本号,如 2.0.0(填写后忽略自增类型)"
+ required: false
+ type: string
+ bump:
+ description: "自增类型(version 留空时生效)"
+ required: false
+ default: minor
+ type: choice
+ options:
+ - patch
+ - minor
+ - major
+
+permissions:
+ contents: write
+ actions: write
+
+concurrency:
+ group: bump-version
+ cancel-in-progress: false
+
+jobs:
+ bump:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.calc.outputs.version }}
+ steps:
+ - name: Checkout main
+ uses: actions/checkout@v4
+ with:
+ ref: main
+ fetch-depth: 0
+
+ - name: Calculate and apply new version
+ id: calc
+ env:
+ TARGET_VERSION: ${{ inputs.version }}
+ BUMP_TYPE: ${{ inputs.bump }}
+ run: |
+ set -euo pipefail
+ python3 - <<'PY'
+ import datetime, json, os, re
+
+ path = "version.json"
+ with open(path, encoding="utf-8") as f:
+ data = json.load(f)
+
+ old = str(data.get("version", "0.0.0")).strip()
+ target = (os.environ.get("TARGET_VERSION") or "").strip()
+
+ if not target:
+ kind = (os.environ.get("BUMP_TYPE") or "minor").strip()
+ nums = [int(x) for x in re.split(r"[.\-+]", old) if x.isdigit()]
+ while len(nums) < 3:
+ nums.append(0)
+ major, minor, patch = nums[0], nums[1], nums[2]
+ if kind == "major":
+ major, minor, patch = major + 1, 0, 0
+ elif kind == "patch":
+ patch += 1
+ else:
+ minor, patch = minor + 1, 0
+ target = f"{major}.{minor}.{patch}"
+
+ if not re.fullmatch(r"\d+\.\d+\.\d+", target):
+ raise SystemExit(f"非法版本号:{target}(应形如 1.2.3)")
+
+ data["version"] = target
+ data["buildDate"] = datetime.date.today().isoformat()
+ with open(path, "w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ f.write("\n")
+
+ csproj_path = "TraeTools.csproj"
+ with open(csproj_path, encoding="utf-8") as f:
+ csproj = f.read()
+ m = re.search(r"(\d+)", csproj)
+ if not m:
+ raise SystemExit("TraeTools.csproj 中未找到 ")
+ new_code = int(m.group(1)) + 1
+ csproj = csproj[: m.start(1)] + str(new_code) + csproj[m.end(1) :]
+ with open(csproj_path, "w", encoding="utf-8") as f:
+ f.write(csproj)
+
+ print(f"version.json: {old} -> {target}")
+ print(f"Android versionCode: {m.group(1)} -> {new_code}")
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
+ f.write(f"version={target}\n")
+ PY
+
+ - name: Commit and push to main
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add version.json TraeTools.csproj
+ if git diff --cached --quiet; then
+ echo "无版本变化,跳过提交"
+ exit 0
+ fi
+ git commit -m "chore(release): bump version to v${{ steps.calc.outputs.version }}"
+ git push origin HEAD:main
+
+ - name: Dispatch CI to validate the bumped commit
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: gh workflow run ci.yml --ref main
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ee536d6..ff7e56e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,6 +3,7 @@ name: CI
on:
push:
branches: [main]
+ workflow_dispatch:
pull_request:
branches: [main]
@@ -10,24 +11,43 @@ permissions:
contents: read
jobs:
- build:
+ build-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- # P2-2:主程序已升到 net10.0-windows(NativeAOT 前置)
- - name: Setup .NET 10
+ - name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- - name: Build (Release)
- run: dotnet build TraeTools.csproj -c Release --nologo
+ - 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
\ No newline at end of file
+ if-no-files-found: error
+
+ build-android:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 9
+ 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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eae2c43..5f1d5be 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,40 +19,23 @@ jobs:
fetch-depth: 0 # 拉取完整历史与所有 tag,供生成"上一版本→本版本"更新差异
fetch-tags: true
- # P2-2:主程序已升到 net10.0-windows
- - name: Setup .NET 10
+ - name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- # P2-4:更新分发包改为「NativeAOT 目录版整包 zip」。
- # NativeAOT 产物不是单文件:libSkiaSharp / av_libglesv2 / e_sqlite3 / libHarfBuzzSharp /
- # libsodium 必须与 exe 同目录,只替换 exe 会得到"缺 DLL 打不开"的残废程序。
- # 应用内更新器(UpdaterService)现在下载 `TraeTools-win-x64.zip` → 校验 SHA-256 → 解包 →
- # 整套文件原子替换 → 重启;zip 压缩也抵消了 AOT 原生库带来的体积增长。
- - 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
-
- # 兼容引导包:历史客户端内的旧更新器只会下载名为 `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
-
- # P2-1:登录宿主(WinForms + WebView2)是独立进程,不参与主程序的 NativeAOT,
- # 因此单独发布并随压缩包/Release 一起分发;缺失时主程序自动回退到"手动粘贴 Token"。
- - name: Publish login host (separate process, not AOT)
- run: dotnet publish TraeTools.LoginHost/TraeTools.LoginHost.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish_aot
-
- - name: Zip distribution (NativeAOT package, no PDBs)
- # 必须用 pwsh:Windows PowerShell 5.1 的 Compress-Archive 会把条目名里的目录分隔符写成
- # 反斜杠(实测 `a\b\z.pdb`),依赖"zip 内路径用 /"的解包器/校验器可能踩坑。
- shell: pwsh
+ # 发布单文件自包含 exe:应用内更新器(UpdaterService)会下载名为 TraeTools.exe 的资产,
+ # 直接覆盖本地 exe 后重启,因此必须产出可独立运行的单文件程序。
+ # 注意:RuntimeIdentifier(win-x64) 定义在 csproj 的 windows 条件组内,
+ # 不能在此用 -r 传入(全局属性会污染 android TFM 的还原,报 NU1102)。
+ - name: Publish (self-contained single-file win-x64)
+ run: dotnet publish TraeTools.csproj -c Release -f net10.0-windows --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish
+
+ - name: Zip distribution
+ shell: powershell
run: |
- Copy-Item publish_aot\TraeTools.LoginHost.exe publish\ -Force
- Get-ChildItem -Path publish_aot -Recurse -Include *.pdb | Remove-Item -Force
- Compress-Archive -Path publish_aot\* -DestinationPath TraeTools-win-x64.zip -Force
- Get-ChildItem -Path publish, publish_aot, TraeTools-win-x64.zip | Select-Object Name, Length
+ Compress-Archive -Path publish\* -DestinationPath TraeTools-win-x64.zip -Force
+ Get-ChildItem -Path publish, TraeTools-win-x64.zip | Select-Object Name, Length
# 自动生成更新摘要:取「版本号不大于当前版本」的最近一个 v* tag,
# 汇总其到本版本之间的全部提交(跳过 merge 提交)。
@@ -80,9 +63,9 @@ jobs:
{
echo "TraeTools ${GITHUB_REF_NAME}"
echo ""
- echo "- Windows x64:\`TraeTools-win-x64.zip\`(NativeAOT 整包,解压运行,无需安装 .NET 运行时)"
- echo "- 应用内「检查更新」会下载本 Release 的 \`TraeTools-win-x64.zip\`,校验后自动替换整套文件并重启"
- echo "- \`TraeTools.exe\` 为单文件引导包,仅用于老版本客户端首次升级"
+ echo "- 单文件自包含 Windows x64 程序(无需额外安装 .NET 运行时)"
+ echo "- Android 手机版 APK(Android 8.0+,见 TraeTools-Android.apk)"
+ echo "- 应用内「检查更新」为**非强制**:仅提示,经用户确认后才下载安装(Windows 端后台替换,Android 端跳转下载页手动安装),可随时取消"
echo ""
if [ -n "${PREV}" ]; then
echo "## 更新差异(${PREV} → ${GITHUB_REF_NAME})"
@@ -103,6 +86,178 @@ jobs:
body_path: release_notes.md
files: |
publish/TraeTools.exe
- publish/TraeTools.LoginHost.exe
TraeTools-win-x64.zip
- generate_release_notes: false
\ No newline at end of file
+ generate_release_notes: false
+
+ publish-android:
+ runs-on: ubuntu-latest
+ needs: publish # 等 Windows job 创建好 Release 后再追加 APK 资产
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 9
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Install Android workload
+ run: dotnet workload install android
+
+ # Android APK:先由 .NET 打包器签名(它会按自己的策略完成 zipalign),
+ # 再**重签**一次以强制三种签名方案齐全。
+ #
+ # 为什么不自己 zipalign + 签名:.NET 打包出的包已经过它的对齐流程,
+ # 而本机 build-tools 的 zipalign 不支持 -P 16(16KB 页对齐,Android 15+ 对含
+ # 原生库的包有要求),自行 zipalign 会把 16KB 对齐**降级**成 4KB。
+ # 只做重签则完全不触碰 zip 结构,对齐策略保持不变。
+ #
+ # EnableWindowsTargeting:多目标项目还原需一并还原 net10.0-windows,ubuntu 上必须显式开启
+ - name: Publish (android apk, signed by dotnet)
+ run: |
+ dotnet publish TraeTools.csproj -c Release -f net10.0-android \
+ -p:EnableWindowsTargeting=true \
+ -p:AndroidPackageFormat=apk \
+ -p:AndroidKeyStore=true \
+ -p:AndroidSigningKeyStore="$PWD/signing/traetools.keystore" \
+ -p:AndroidSigningKeyAlias=traetools \
+ -p:AndroidSigningKeyPass=traetools \
+ -p:AndroidSigningStorePass=traetools \
+ -o publish-android
+ echo "== publish-android 产物 =="
+ find publish-android -name "*.apk" -printf "%s\t%p\n" | sort -rn || true
+
+ - name: Re-sign APK (force v1 + v2 + v3)
+ run: |
+ set -euo pipefail
+ SRC=$(find publish-android -iname "*-Signed.apk" | head -1)
+ [ -n "$SRC" ] || SRC=$(find publish-android -name "*.apk" | head -1)
+ [ -n "$SRC" ] || { echo "::error::未找到 APK"; exit 1; }
+ echo "input apk: $SRC"
+
+ BT=$(ls -d "${ANDROID_HOME:-/usr/local/lib/android/sdk}"/build-tools/* 2>/dev/null | sort -V | tail -1)
+ AS="$BT/apksigner"
+ [ -x "$AS" ] || { echo "::error::apksigner not found"; exit 1; }
+
+ # 不传给 v1 的既有签名文件做剥离:apksigner 重签时自行替换签名块与
+ # META-INF 下的 JAR 签名文件。
+ "$AS" sign \
+ --ks signing/traetools.keystore --ks-type PKCS12 \
+ --ks-pass pass:traetools \
+ --ks-key-alias traetools \
+ --key-pass pass:traetools \
+ --v1-signing-enabled true \
+ --v2-signing-enabled true \
+ --v3-signing-enabled true \
+ --out TraeTools-Android.apk "$SRC"
+ ls -lh TraeTools-Android.apk
+
+ # 对齐检查(信息性,不阻断:对齐由 .NET 打包器负责)
+ ZA="$BT/zipalign"
+ if [ -x "$ZA" ]; then
+ if "$ZA" -c -v 4 TraeTools-Android.apk >/dev/null 2>&1; then
+ echo "✅ zipalign 4 字节对齐通过"
+ else
+ echo "::warning::zipalign 检查未通过(信息性,不影响签名有效性)"
+ fi
+ fi
+
+ # ===== 签名校验门禁 =====
+ # 旧实现只 `apksigner verify --print-certs | head -8 || true`:既不校验退出码,
+ # 也不比对指纹,等于「打印一下就放行」。签名一旦出问题(密钥被换、方案缺失、
+ # 误传未签名包),用户侧表现为「应用未安装 / 签名验证不通过」,而 CI 全绿。
+ # 现在改为硬门禁:任一条件不满足即中断发布。
+ - name: Verify APK signature (gate)
+ run: |
+ set -euo pipefail
+ BT=$(ls -d "${ANDROID_HOME:-/usr/local/lib/android/sdk}"/build-tools/* 2>/dev/null | sort -V | tail -1)
+ [ -n "$BT" ] && [ -x "$BT/apksigner" ] || { echo "::error::apksigner not found"; exit 1; }
+ echo "== apksigner verify --verbose --print-certs =="
+ # 退出码是主判据:apksigner 对签名无效/缺方案会返回非 0
+ "$BT/apksigner" verify --verbose --print-certs TraeTools-Android.apk | tee verify.txt
+
+ # 指纹比对:基准由 rotate-keystore.yml 生成,避免两处手工同步。
+ # 必须做大小写归一——keytool 输出大写、apksigner 输出小写,
+ # 直接字符串比较会因大小写差异误报"不一致"(实测踩过)。
+ EXPECTED=$(tr -cd '0-9A-Fa-f' < signing/EXPECTED_CERT_SHA256.txt 2>/dev/null | tr 'a-f' 'A-F')
+ ACTUAL=$(sed -n 's/.*certificate SHA-256 digest: *//p' verify.txt | head -1 | tr -cd '0-9A-Fa-f' | tr 'a-f' 'A-F')
+ echo "expected = ${EXPECTED:-<缺失>}"
+ echo "actual = ${ACTUAL:-<未解析到>}"
+ [ -n "$EXPECTED" ] || { echo "::error::缺少 signing/EXPECTED_CERT_SHA256.txt"; exit 1; }
+ [ -n "$ACTUAL" ] || { echo "::error::未能从 apksigner 输出解析证书 SHA-256"; exit 1; }
+ [ "$EXPECTED" = "$ACTUAL" ] || { echo "::error::签名证书指纹与 EXPECTED_CERT_SHA256.txt 不一致(密钥被换或产物来源不对)"; exit 1; }
+ echo "✅ 证书指纹一致"
+
+ # 结构性校验三种签名方案是否齐全。刻意不复用 apksigner 的文本输出——
+ # 不同 build-tools 版本的行格式不同(实测本仓库 CI 输出的是 "V3.0 Signer:" 而非
+ # "Signer #1 certificate ..."),文本 grep 不可靠;直接解析 APK 二进制最稳。
+ - name: Verify signature schemes present (v1 + v2 + v3)
+ run: |
+ python3 - <<'PY'
+ import os
+ import struct
+ import sys
+ import zipfile
+
+ APK = "TraeTools-Android.apk"
+ ok = True
+
+ # --- v1:JAR 签名文件存在于 META-INF ---
+ with zipfile.ZipFile(APK) as z:
+ names = [n for n in z.namelist() if n.upper().startswith("META-INF/")]
+ blk = [n for n in names if n.upper().endswith((".RSA", ".DSA", ".EC"))]
+ sf = [n for n in names if n.upper().endswith(".SF")]
+ mf = [n for n in names if n.upper().endswith("MANIFEST.MF")]
+ v1 = bool(blk and sf and mf)
+ print(f"v1 (JAR) : {'存在' if v1 else '缺失'} block={blk} SF={sf} MF={mf}")
+ ok = ok and v1
+
+ # --- v2 / v3:APK Signing Block 中的方案 ID ---
+ # ⚠️ 不能只读「文件尾部 64KB」:中央目录本身在本包里就有 ~73KB,
+ # 签名块位于中央目录**之前**,固定窗口会取不到它而误报「缺失」。
+ # 正确做法:先从尾部 EOCD 读中央目录偏移,再自该偏移向前多读 64KB。
+ total = os.path.getsize(APK)
+ cd_off = total
+ with open(APK, "rb") as f:
+ f.seek(max(0, total - 65536))
+ last = f.read()
+ e = last.rfind(b"PK\x05\x06")
+ if e >= 0:
+ cd_off = struct.unpack_from(" end:
+ break
+ ids[struct.unpack_from(" signing/EXPECTED_CERT_SHA256.txt
+ echo "---- signing/EXPECTED_CERT_SHA256.txt ----"
+ cat signing/EXPECTED_CERT_SHA256.txt
+ echo
+ echo "===== base64(便于本地核对,与文件内容一致)====="
+ base64 -w 0 "$KS"; echo
+
+ - name: 提交回仓库
+ if: ${{ inputs.commit }}
+ run: |
+ set -euo pipefail
+ git config user.name "akexin"
+ git config user.email "315600306+akexin@users.noreply.github.com"
+ git add signing/traetools.keystore signing/EXPECTED_CERT_SHA256.txt
+ if git diff --cached --quiet; then
+ echo "密钥与指纹均未变化,无需提交"
+ else
+ git commit \
+ -m "chore(signing): 轮换 APK 签名密钥(keytool 生成,alias=${{ inputs.alias }})" \
+ -m "由 .github/workflows/rotate-keystore.yml 手动触发。轮换后所有已安装用户必须卸载旧版才能安装新版。同时更新 signing/EXPECTED_CERT_SHA256.txt(release.yml 的签名校验门禁基准)。"
+ git push origin "HEAD:${GITHUB_REF_NAME}"
+ fi
diff --git a/Android/App.axaml b/Android/App.axaml
deleted file mode 100644
index 7aa3b1e..0000000
--- a/Android/App.axaml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/App.axaml.cs b/Android/App.axaml.cs
deleted file mode 100644
index 3995e61..0000000
--- a/Android/App.axaml.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Markup.Xaml;
-using TraeTools.Platform;
-using TraeTools.ViewModels;
-using TraeTools.Views;
-
-namespace TraeTools;
-
-///
-/// Android 版应用入口。
-///
-/// 与仓库根的 App.axaml.cs 的区别只有两处:
-/// 1) 没有 IClassicDesktopStyleApplicationLifetime 分支(单视图平台无 Window);
-/// 2) Application 一律写全称 Avalonia.Application,与 Android.App.Application 区分。
-///
-public partial class App : Avalonia.Application
-{
- public override void Initialize()
- {
- try
- {
- AvaloniaXamlLoader.Load(this);
- CrashLog.Boot("App.Initialize completed");
- }
- catch (Exception ex)
- {
- CrashLog.Write("App.Initialize", ex, true);
- throw;
- }
- }
-
- public override void OnFrameworkInitializationCompleted()
- {
- CrashLog.Install(text => CrashReportActivity.Launch(text));
- CrashLog.Boot($"App.OnFrameworkInitializationCompleted lifetime={ApplicationLifetime?.GetType().FullName}");
-
- if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
- {
- // 上次运行崩溃过:本次先进错误页(可复制堆栈 / 重试),避免反复闪退
- var flag = CrashLog.ReadFlag();
- if (!string.IsNullOrEmpty(flag))
- {
- CrashLog.Boot("App: previous crash detected -> startup error view");
- singleView.MainView = BuildErrorView(singleView, "上次启动崩溃了", flag!);
- base.OnFrameworkInitializationCompleted();
- return;
- }
-
- try
- {
- // 单视图生命周期:MainView(UserControl)作为根视图全屏展示。
- // 不能用 MainWindow(Window):单视图平台创建 Window 会抛 NotSupportedException。
- singleView.MainView = new MainView
- {
- DataContext = new MainViewModel(),
- };
- CrashLog.Boot("App: single-view MainView created");
- }
- catch (Exception ex)
- {
- // 主界面构建失败也不能闪退:显示可读的错误页
- CrashLog.Write("App: create MainView failed", ex, true);
- singleView.MainView = BuildErrorView(singleView, "界面初始化失败", ex.ToString());
- }
- }
-
- base.OnFrameworkInitializationCompleted();
- }
-
- /// 构建启动错误页:异常堆栈 + 启动日志 + 「清除并重新加载界面」。
- private static Control BuildErrorView(
- ISingleViewApplicationLifetime lifetime,
- string title,
- string detail)
- {
- var body = detail + Environment.NewLine + Environment.NewLine
- + "==== 启动日志(尾部)====" + Environment.NewLine
- + CrashLog.ReadRecent();
-
- var text = new SelectableTextBlock
- {
- Text = title + Environment.NewLine + 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 += (_, _) =>
- {
- CrashLog.ClearFlag();
- try
- {
- lifetime.MainView = new MainView { DataContext = new MainViewModel() };
- CrashLog.Boot("App: retry create MainView ok");
- }
- catch (Exception ex)
- {
- CrashLog.Write("App: retry create MainView failed", ex, true);
- }
- };
-
- var grid = new Grid { RowDefinitions = new RowDefinitions("*,Auto") };
- grid.Children.Add(new ScrollViewer { Content = text });
- Grid.SetRow(retry, 1);
- grid.Children.Add(retry);
- return grid;
- }
-}
diff --git a/Android/Platform/BrowserLauncher.cs b/Android/Platform/BrowserLauncher.cs
deleted file mode 100644
index 08addf6..0000000
--- a/Android/Platform/BrowserLauncher.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System;
-
-namespace TraeTools.Platform;
-
-///
-/// 用系统默认浏览器打开 URL(Android 实现)。
-///
-/// 失败必须可见:早先的实现把异常全部吞掉且返回 void,于是「授权 GitHub」
-/// 按钮点了没反应时,用户与日志都拿不到任何线索。现在返回 bool,
-/// 并把失败原因写进平台日志(category = browser)。
-///
-/// 三级回退:
-///
-/// - 优先用当前 Activity 启动(前台切换最自然,无需 FLAG_ACTIVITY_NEW_TASK);
-/// - Activity 不可用时改用 Application 上下文 —— 此时必须带 NEW_TASK,
-/// 否则抛 AndroidRuntimeException;
-/// - 个别定制 ROM 会拦截直接 startActivity,最后用 Intent.CreateChooser 再试一次。
-///
-///
-/// 浏览器属于「隐式 Intent 解析」,Android 11+ 的包可见性限制默认不影响
-/// startActivity,但为兼容个别 ROM 仍在清单里声明了 <queries>(见 Properties/AndroidManifest.xml)。
-///
-internal static class BrowserLauncher
-{
- /// 打开 URL:成功返回 true;失败返回 false 并已写日志。
- public static bool Open(string? url)
- {
- if (string.IsNullOrWhiteSpace(url))
- {
- PlatformLog.Warn("browser", "打开浏览器失败:URL 为空");
- return false;
- }
- try
- {
- var uri = Android.Net.Uri.Parse(url);
- if (uri == null)
- {
- PlatformLog.Warn("browser", $"打开浏览器失败:URL 无法解析({url})");
- return false;
- }
-
- var activity = MainActivity.Current;
- var context = Android.App.Application.Context;
-
- // ① 首选:Activity 直接启动
- if (activity != null)
- {
- try
- {
- activity.StartActivity(new Android.Content.Intent(Android.Content.Intent.ActionView, uri));
- return true;
- }
- catch (Exception ex)
- {
- PlatformLog.Warn("browser", "Activity 启动浏览器失败,回退 Application 上下文:" + ex.Message);
- }
- }
-
- if (context == null)
- {
- PlatformLog.Warn("browser", "打开浏览器失败:Android 上下文不可用(Application.Context 为 null)");
- return false;
- }
-
- // ② 回退:Application 上下文(必须 NEW_TASK)
- 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);
- }
-
- // ③ 再回退:让系统弹「用哪个应用打开」
- 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;
- }
- }
- catch (Exception ex)
- {
- PlatformLog.Warn("browser", $"打开浏览器失败:{ex.GetType().Name}: {ex.Message}(URL={url})");
- return false;
- }
- }
-}
diff --git a/Android/Platform/DpapiCompat.cs b/Android/Platform/DpapiCompat.cs
deleted file mode 100644
index bb4369d..0000000
--- a/Android/Platform/DpapiCompat.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Text;
-
-// ⚠️ 在 System.Security.Cryptography 命名空间下定义 BCL 同名类型是刻意的,理由见下方注释。
-namespace System.Security.Cryptography;
-
-/// 与 DPAPI 同名的作用域枚举(Android 上只有应用本地密钥库,语义上等同 CurrentUser)。
-public enum DataProtectionScope
-{
- CurrentUser,
- LocalMachine,
-}
-
-///
-/// DPAPI 兼容层(仅 Android 编译)。
-///
-/// 背景:共享的 Services/Checkin/AppConfig.cs 用
-/// ProtectedData.Protect/Unprotect 加密 Token / Session / DeviceId 等敏感字段;
-/// 而 System.Security.Cryptography.ProtectedData 是 Windows 专属包,
-/// net10.0-android 上不存在该类型,直接编译会失败。
-///
-/// 本工程的前提是不改动仓库根目录下的任何文件,因此这里提供一份
-/// 同名同签名的兼容层,把 DPAPI 的语义映射到
-/// (Keystore + AES-256-GCM),
-/// 于是上游的业务代码一行不改即可在手机上正常工作。
-///
-/// 两处语义差异,均已被上游代码的用法包容:
-///
-/// - 密文格式不同 —— AppConfig 只把结果当不透明字符串存取,跨平台拷贝时解密失败会
-/// 走它自己的兜底(字段置 null → 提示重新登录),不会把密文当明文使用;
-/// - 失败时抛 —— 与 DPAPI 行为一致,
-/// AppConfig 的 DecryptSecret 本来就 catch 全部异常。
-///
-///
-/// 注:若某个 .NET Android 版本将来自带了同名类型,编译器会以本文件为准并给出
-/// CS0436 警告(构建时已抑制),行为仍然是我们的 Keystore 实现 —— 正是期望的结果。
-///
-public static class ProtectedData
-{
- public static byte[] Protect(byte[] userData, byte[]? optionalEntropy, DataProtectionScope scope)
- {
- ArgumentNullException.ThrowIfNull(userData);
- var encrypted = TraeTools.Platform.AndroidVault.Protect(Encoding.UTF8.GetString(userData))
- ?? throw new CryptographicException("Android Keystore 加密失败");
- return Encoding.UTF8.GetBytes(encrypted);
- }
-
- public static byte[] Unprotect(byte[] encryptedData, byte[]? optionalEntropy, DataProtectionScope scope)
- {
- ArgumentNullException.ThrowIfNull(encryptedData);
- var plain = TraeTools.Platform.AndroidVault.Unprotect(Encoding.UTF8.GetString(encryptedData))
- ?? throw new CryptographicException("Android Keystore 解密失败(换机 / 清除数据 / 跨平台拷贝)");
- return Encoding.UTF8.GetBytes(plain);
- }
-}
diff --git a/Android/Platform/PlatformLog.cs b/Android/Platform/PlatformLog.cs
deleted file mode 100644
index 6fde30d..0000000
--- a/Android/Platform/PlatformLog.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.IO;
-
-namespace TraeTools.Platform;
-
-///
-/// 平台层的最小日志出口。
-///
-/// 页面级日志走 ViewModels.AccountHelpers.AppLog(按业务分类归档),
-/// 但 Platform/ 下的代码属于更底层的设施(加解密、浏览器跳转、崩溃捕获),
-/// 不应反向依赖 ViewModels,因此在这里提供一个同格式、同目录的写入点:
-/// 文件名与格式与页面日志保持一致({category}_log_yyyyMM.txt),
-/// 打开日志目录时能看到完整的一批记录。
-///
-internal static class PlatformLog
-{
- private static readonly object Lock = new();
-
- public static void Info(string category, string message) => Write(category, "INFO", message);
-
- public static void Warn(string category, string message) => Write(category, "WARN", message);
-
- public static void Error(string category, string message) => Write(category, "ERROR", message);
-
- private static void Write(string category, string level, string message)
- {
- try
- {
- lock (Lock)
- {
- var file = Path.Combine(
- TraeTools.Services.DataPaths.LogsDir,
- $"{category}_log_{DateTime.Now:yyyyMM}.txt");
- File.AppendAllText(file,
- $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {message}{Environment.NewLine}");
- }
- }
- catch
- {
- // 日志写入失败绝不能影响主流程(尤其是崩溃与加解密路径)
- }
- }
-}
diff --git a/Android/README.md b/Android/README.md
deleted file mode 100644
index ba28ebe..0000000
--- a/Android/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# TraeTools for Android
-
-给 TraeTools 加上 **Android 端**。本目录是一个独立的 `net10.0-android` 工程,
-Windows 版的构建、发布流程与产物**不受影响**。
-
-根目录只有 `TraeTools.csproj` 增加了 3 行 `Remove`(把本目录排除出主程序的默认通配符),
-原因见下方「与根工程的关系」。除此之外,仓库其余文件一行未动。
-
-## 包含内容
-
-| 路径 | 说明 |
-|---|---|
-| `TraeTools.Android.csproj` | 独立的 `net10.0-android` 工程。共享代码(`Models/` `Services/` `ViewModels/` `Views/` 的六个页面、`Styles/` `Assets/`)以链接方式复用,单一真源仍在仓库根目录 |
-| `App.axaml(.cs)` | Android 应用入口(单视图生命周期) |
-| `Platform/` | `AndroidVault`(Keystore + AES-256-GCM)、`DpapiCompat`(DPAPI 兼容层)、`BrowserLauncher`、`CrashLog`、`CrashReportActivity`、`MainActivity`、`PlatformLog` |
-| `Views/` | `MainView`(单视图外壳:顶栏 + 横向标签导航 + 覆盖层对话框宿主)、`OverlayDialog`、5 个对话框(`PromptWindow` / `InputWindow` / `PatInputWindow` / `LoginWindow` / `EulaWindow`) |
-| `Services/` | `DataPaths`、`AutoStartManager` 的 Android 实现 |
-| `ViewModels/UiHost.cs` | 把宿主解析结果放宽为 `TopLevel`,使上游 ViewModel 无需改动即可正常弹窗与访问剪贴板 |
-| `Properties/` `Resources/` | 应用清单(INTERNET + 浏览器查询意图)、自适应图标、主题与配色 |
-
-## 编译
-
-需要 .NET 10 SDK,并安装 Android 工作负载:
-
-```bash
-dotnet workload install android
-dotnet build Android/TraeTools.Android.csproj -c Release
-```
-
-产物:`Android/bin/Release/net10.0-android/*.apk`(最低 Android 8.0 / API 26)。
-包名 `com.traetools.android`,`versionName` 取自根目录 `version.json`。
-Release 默认使用调试密钥签名,正式分发请自行配置 keystore。
-
-CI 见仓库根的 `.github/workflows/android.yml`(编译并上传 APK)。
-
-## 与根工程的关系
-
-本工程位于 Windows 主程序的工程目录之内,而 MSBuild 与 Avalonia 的默认通配符会向下递归,
-且并不认「子目录里有自己的 csproj」:
-
-| 来源 | 默认通配符 |
-|---|---|
-| .NET SDK | `Compile Include="**/*.cs"`、`None Include="**/*"` |
-| Avalonia | `AvaloniaXaml Include="**\*.axaml"`(另有 `**\*.paml`) |
-
-若不排除,主程序会把 Android 源码一并编译,实测报
-`AVLN2002: Duplicate x:Class directive, TraeTools.App is already used in /Android/App.axaml`,
-并且 `Android/Platform/*.cs` 依赖的 `Android.App` / `Avalonia.Android` 在主程序里无法解析。
-
-因此 `TraeTools.csproj` 中增加了:
-
-```xml
-
-
-
-
-
-```
-
-写法与仓库里既有的 `` 一致。
-
-## 平台差异
-
-| 能力 | Windows | Android |
-|---|---|---|
-| 签到 / 自动签到 / 云端签到 / 用量统计 / 仪表盘 | ✅ | ✅ |
-| 账号切换(vault 建档 / 冷切换) | ✅ 直接操作本地客户端 | ⚠️ 手机端无本地 Trae 客户端,页面仅作展示 |
-| 登录方式 | ✅ 内嵌浏览器(WebView2 宿主)自动取凭证 | ✅ 手动粘贴 Cloud-IDE-Token |
-| 开机自启 | ✅ 注册表 `Run` | ❌ 无等价机制,已降级为无操作 |
-| 凭证加密 | DPAPI(当前用户) | Android Keystore(RSA-2048 包裹 AES-256-GCM 数据密钥) |
-| 系统托盘 / 通知栏 | ✅ 托盘图标 + 原生菜单 | ❌ 无对应概念(自动签到在进程存活期间执行) |
-
-## 已知限制
-
-- **应用内自动更新不可用**:上游 `SettingsViewModel` 的更新流程会下载 zip 并替换 exe,
- 手机上无法执行。建议直接到 Releases 下载新 APK 覆盖安装。
-- **部分「打开网页」入口点了没反应**:上游 `CloudViewModel` / `SettingsViewModel` 里
- 打开浏览器走的是 `Process.Start(...)`,该调用在 Android 上不可用(失败已被 try/catch 捕获,
- 日志里会留痕)。设备码授权流程仍可用 —— 授权码会自动复制到剪贴板,
- 按日志提示手动访问授权页即可。要修好这两处需要给上游 VM 加一个平台跳转抽象(约 7 处调用点)。
-- 仅编译过 `net10.0-android`;未在真机长时间运行验证。
diff --git a/Android/Services/Checkin/AutoStartManager.cs b/Android/Services/Checkin/AutoStartManager.cs
deleted file mode 100644
index 225ec1d..0000000
--- a/Android/Services/Checkin/AutoStartManager.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace TraeCheckin;
-
-///
-/// 开机自启动(Android 实现)。
-///
-/// Windows 版通过注册表 HKCU\...\CurrentVersion\Run 注册 exe 路径;
-/// Android 在应用层面没有等价的、无需额外权限的通用机制:
-///
-/// - BOOT_COMPLETED 广播 + 前台服务需要 RECEIVE_BOOT_COMPLETED 权限,
-/// 且 Android 12+ 起后台启动前台服务受限,Play 政策也不允许「静默常驻」;
-/// - 本应用真正的「无人值守签到」场景由「云端签到」承担
-/// (GitHub Actions 每天定时跑 checkin.py,不依赖手机开机)。
-///
-/// 因此这里保留与原实现完全一致的 API,但固定返回「未启用 / 设置无效」,
-/// 让设置页能如实展示「本平台不支持」,而不是静默假装成功。
-///
-internal static class AutoStartManager
-{
- /// 是否已注册开机自启动 —— Android 恒为 false。
- public static bool IsEnabled() => false;
-
- /// 开启或关闭开机自启动 —— Android 为空操作(无注册表可写)。
- public static void SetEnabled(bool enabled)
- {
- // 有意留空:调用方(设置页)会依据 IsEnabled() 的返回值把开关复位并给出提示。
- }
-}
diff --git a/Android/Services/DataPaths.cs b/Android/Services/DataPaths.cs
deleted file mode 100644
index a76cb3a..0000000
--- a/Android/Services/DataPaths.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using System;
-using System.IO;
-
-namespace TraeTools.Services;
-
-///
-/// 统一数据根目录(Android 实现)。
-///
-/// Windows 版落在 %APPDATA%\TraeTools;Android 没有「用户漫游目录」的概念,
-/// 应用私有目录就是 Context.FilesDir(/data/data/<包名>/files,
-/// 非 root 不可读、随卸载清除),因此根目录取它的 TraeTools 子目录。
-///
-/// 目录布局与桌面端保持一致(config.json / data / logs / switch / vault),
-/// 这样 Services/Checkin/*、Services/Switch/* 等共享代码无需任何分支。
-///
-/// Migrate() 在本平台是空操作:TraeCheckin / TraeSwitch 的历史目录只存在于 Windows。
-///
-public static class DataPaths
-{
- /// 根目录:应用私有 files 目录下的 TraeTools 子目录。
- public static string Root
- {
- get
- {
- try
- {
- var files = Android.App.Application.Context?.FilesDir?.AbsolutePath;
- if (!string.IsNullOrEmpty(files))
- return Path.Combine(files!, "TraeTools");
- }
- catch
- {
- // Context 尚未就绪时走下面的兜底路径
- }
- // 兜底:极早期调用(Application.Context 为 null)时退化到 HOME 目录,
- // 保证 Root 永远返回可用字符串而不是抛异常。
- return Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.Personal), "TraeTools");
- }
- }
-
- /// 签到配置 config.json。
- public static string ConfigPath => Path.Combine(Root, "config.json");
-
- /// 数据目录:存放 checkin.db、history_*.txt、credits_total_*.txt、usage_*.jsonl。
- public static string DataDir
- {
- get
- {
- var dir = Path.Combine(Root, "data");
- Directory.CreateDirectory(dir);
- return dir;
- }
- }
-
- /// 日志目录:存放各模块调试日志(checkin_log / account_log / cloud_log / switch_log / usage_log)。
- public static string LogsDir
- {
- get
- {
- var dir = Path.Combine(Root, "logs");
- Directory.CreateDirectory(dir);
- return dir;
- }
- }
-
- /// 账号切换配置(settings.json)。
- public static string SwitchDir => Path.Combine(Root, "switch");
-
- /// 登录态备份 vault。手机端不做客户端切换,该目录仅用于放置共享代码。
- public static string VaultDir => Path.Combine(Root, "vault");
-
- /// 确保所有子目录存在(启动时调用一次)。
- public static void EnsureDirectories()
- {
- Directory.CreateDirectory(Root);
- Directory.CreateDirectory(DataDir);
- Directory.CreateDirectory(LogsDir);
- Directory.CreateDirectory(SwitchDir);
- Directory.CreateDirectory(VaultDir);
- }
-
- ///
- /// Windows 版用于把 TraeCheckin / TraeSwitch 历史目录迁移到统一根;
- /// Android 是全新平台、没有历史目录可迁移,因此只保证目录存在。
- /// 保留同名方法是为了让共享的调用方(如各 ViewModel 的初始化路径)不改一行代码。
- ///
- public static void Migrate()
- {
- try
- {
- EnsureDirectories();
- }
- catch
- {
- // 目录创建失败不阻塞启动:后续写入会各自失败并提示
- }
- }
-}
diff --git a/Android/TraeTools.Android.csproj b/Android/TraeTools.Android.csproj
deleted file mode 100644
index 5655c47..0000000
--- a/Android/TraeTools.Android.csproj
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-
-
-
- net10.0-android
- Exe
-
- TraeTools
- TraeTools
- enable
- enable
- true
- true
- CA1416;XA0121;CS0436
-
-
-
-
- <_VJsonRaw Condition="Exists('$(MSBuildProjectDirectory)\..\version.json')">$([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\..\version.json'))
- <_VJsonVer>$([System.Text.RegularExpressions.Regex]::Match('$(_VJsonRaw)', '"version"\s*:\s*"([^"]+)"').Groups[1].Value)
- $(_VJsonVer)
- 1.0.0
- $(_VJsonVer)
- 1.0.0
-
-
-
-
-
- 26
-
- com.traetools.android
-
- $(_VJsonVer)
- 1.0.0
-
- 1
- apk
-
-
- android-arm64;android-arm
-
-
- false
- false
- None
- false
-
- false
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/ViewModels/UiHost.cs b/Android/ViewModels/UiHost.cs
deleted file mode 100644
index 669117e..0000000
--- a/Android/ViewModels/UiHost.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-
-namespace TraeTools.ViewModels;
-
-///
-/// 解析当前可用于承载对话框的宿主,供页面 VM 弹模态与访问剪贴板 / 文件选择器。
-///
-/// 关键设计(整个 Android 移植能「零改动共享 VM」的原因):
-/// 上游桌面版的 MainWindow 属性声明为 Window?,各 VM 的用法只有四种 ——
-///
-/// - dlg.ShowDialog<T>(owner):把 owner 透传给对话框;
-/// - owner != null ? … : …:判空守卫;
-/// - top?.Clipboard:复制 Token / 授权码;
-/// - win.StorageProvider:文件选择器。
-///
-/// 这四者全部是 的成员,而 Window 只是 TopLevel 的子类。
-/// 上游 VM 一律用 var 接收该属性,因此这里把返回类型放宽成 TopLevel? 后:
-///
-/// - 上游 VM 一行都不用改;
-/// - Android 单视图平台也能拿到非 null 的宿主(单视图的根视图反查 TopLevel),
-/// owner != null 的守卫照样通过,弹窗与剪贴板全部可用。
-///
-/// 若这里坚持返回 Window?,Android 上恒为 null,
-/// 上游那些 owner != null ? await … : false 的确认框会被整体跳过(功能性缺失)。
-///
-internal static class UiHost
-{
- ///
- /// 对话框宿主 / 剪贴板来源。桌面端是主窗口,Android 是承载单视图的根 TopLevel;
- /// 尚未挂载到视觉树时返回 null(调用方已有判空守卫)。
- ///
- public static TopLevel? MainWindow => Resolve();
-
- private static TopLevel? Resolve()
- {
- try
- {
- var lifetime = Avalonia.Application.Current?.ApplicationLifetime;
-
- // 桌面端(本工程不编译该分支,保留以便共享代码语义完整)
- if (lifetime is IClassicDesktopStyleApplicationLifetime desktop)
- return desktop.MainWindow;
-
- // Android:单视图生命周期,从根视图反查 TopLevel
- if (lifetime is ISingleViewApplicationLifetime singleView && singleView.MainView is Control root)
- return TopLevel.GetTopLevel(root);
- }
- catch
- {
- // 视觉树尚未就绪 / 生命周期切换中:返回 null,由调用方的守卫兜底
- }
- return null;
- }
-}
diff --git a/Android/Views/EulaWindow.axaml b/Android/Views/EulaWindow.axaml
deleted file mode 100644
index 8673c48..0000000
--- a/Android/Views/EulaWindow.axaml
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-TRAETOOLS 最终用户许可协议(EULA)
-
-欢迎使用本软件。在使用本软件前,请仔细阅读以下条款。
-点击「同意并继续」即表示您接受本协议全部内容;如不同意,请立即关闭并删除本软件。
-
-1. 软件性质
- 本软件为作者个人技术学习成果,以开源形式(GPL-3.0)发布,按"现状"提供,
- 不包含任何形式的明示或默示担保。本软件由「TRAE 每日签到助手」与
- 「TRAE 多账号冷切换助手」整合而来,本协议同时适用于两项功能。
-
-2. 账号归属限制(使用前提)
- 您只能将本软件用于管理您本人合法拥有并有权使用的 Trae 账号。
- 签到与冷切换均仅限本人账号场景,您不得使用本软件从事批量注册、账号交易、
- 积分倒卖、共享账号租赁、工作室多开、破坏平台正常运营或任何违反
- 中华人民共和国法律法规及 Trae 平台服务条款的行为。
-
-3. 数据安全责任
- 本软件会在您的本地设备上存储登录凭证(Token / 会话 Cookie)以及
- 账号登录态备份(vault 目录,等同账号凭证)。您有责任确保这些文件
- 不被上传至公共网络、云盘或分享给任何第三方。
- 因您未妥善保管本地数据导致的账号被盗、信息泄露或其他损失,作者不承担责任。
-
-4. 切换与登录态说明
- 账号冷切换依赖 vault 中保存的登录态字节。切换仅在本机完成,
- 不涉及对 Trae 客户端程序代码的任何修改、注入或破解。
- 请您务必保管好 vault 目录,不要将其置于公共云盘或分享。
-
-5. 平台关系声明
- 本软件与 Trae 及其运营方无任何关联。
- 如官方推出同类功能或要求停止使用,您应立即停用本软件。
-
-6. 禁止逆向与滥用
- 您不得对本软件进行反编译、破解或二次开发后用于违反平台规则的目的。
- 您不得移除、修改或绕过本软件中的任何使用限制或安全提示。
-
-7. 责任免除
- 在法律允许的最大范围内,作者不对因使用或无法使用本软件而产生的任何
- 直接、间接、附带或惩罚性损害承担责任,包括但不限于账号限制、积分调整、
- 数据丢失、登录态失效等。
-
-8. 协议更新
- 作者保留随时修改本协议的权利,修改后的协议将在软件更新或仓库中公布。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/Views/EulaWindow.axaml.cs b/Android/Views/EulaWindow.axaml.cs
deleted file mode 100644
index 661a5fe..0000000
--- a/Android/Views/EulaWindow.axaml.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-
-namespace TraeTools.Views;
-
-///
-/// 首次启动的最终用户许可协议对话框。
-/// ShowDialog<bool> 返回值:true=同意并继续,false=不同意并退出。
-///
-public partial class EulaWindow : UserControl
-{
- public EulaWindow()
- {
- InitializeComponent();
- // 勾选同意后才允许点「同意并继续」
- AgreeCheck.IsCheckedChanged += (_, _) =>
- {
- AgreeButton.IsEnabled = AgreeCheck.IsChecked == true;
- };
- }
-
- /// 与 Window.ShowDialog<TResult>(Window owner) 对齐的入口。
- public Task ShowDialog(TopLevel? owner) => OverlayDialog.ShowAsync(this, owner);
-
- private void OnAgreeClick(object? sender, RoutedEventArgs e)
- => OverlayDialog.CloseDialog(this, true);
-
- private void OnDeclineClick(object? sender, RoutedEventArgs e)
- => OverlayDialog.CloseDialog(this, false);
-}
diff --git a/Android/Views/InputWindow.axaml b/Android/Views/InputWindow.axaml
deleted file mode 100644
index f1525f0..0000000
--- a/Android/Views/InputWindow.axaml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/Views/InputWindow.axaml.cs b/Android/Views/InputWindow.axaml.cs
deleted file mode 100644
index c2dfbf6..0000000
--- a/Android/Views/InputWindow.axaml.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-
-namespace TraeTools.Views;
-
-///
-/// 通用单行文本输入对话框。
-/// ShowDialog<string> 返回:确认时返回输入文本(trim 后),取消返回 null。
-///
-public partial class InputWindow : UserControl
-{
- public string? ResultText { get; private set; }
-
- public InputWindow()
- {
- InitializeComponent();
- }
-
- /// 标题。
- /// 提示文案。
- /// 输入框占位符。
- public InputWindow(string title, string message, string placeholder)
- : this()
- {
- TitleText.Text = title;
- MessageText.Text = message;
- InputBox.PlaceholderText = placeholder;
- }
-
- /// 与 Window.ShowDialog<TResult>(Window owner) 对齐的入口。
- public Task ShowDialog(TopLevel? owner) => OverlayDialog.ShowAsync(this, owner);
-
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- var value = InputBox.Text?.Trim();
- if (string.IsNullOrWhiteSpace(value))
- {
- InputBox.Focus();
- return;
- }
- ResultText = value;
- OverlayDialog.CloseDialog(this, ResultText);
- }
-
- private void OnCancelClick(object? sender, RoutedEventArgs e)
- => OverlayDialog.CloseDialog(this, null);
-
- private void OnInputKeyDown(object? sender, KeyEventArgs e)
- {
- if (e.Key == Key.Enter) OnOkClick(sender, e);
- }
-}
diff --git a/Android/Views/LoginWindow.axaml b/Android/Views/LoginWindow.axaml
deleted file mode 100644
index b22dddc..0000000
--- a/Android/Views/LoginWindow.axaml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-手机端无法内嵌浏览器自动截取凭证,请按下述步骤手动获取 Cloud-IDE-Token。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/Views/LoginWindow.axaml.cs b/Android/Views/LoginWindow.axaml.cs
deleted file mode 100644
index e0982cd..0000000
--- a/Android/Views/LoginWindow.axaml.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using TraeCheckin;
-using TraeTools.ViewModels;
-
-namespace TraeTools.Views;
-
-///
-/// 登录对话框(Android)。
-///
-/// 桌面端优先走独立宿主进程里的内嵌 WebView2 自动读取 token + session;
-/// Android 没有 WebView2 宿主进程(Services/Login/** 也不在本工程编译范围内),
-/// 因此只保留「手动粘贴 Cloud-IDE-Token」这一条通道 ——
-/// 校验、写入账号、持久化的逻辑与桌面端逐字一致。
-///
-public partial class LoginWindow : UserControl
-{
- private readonly TraeAccount? _account;
-
- public LoginWindow()
- {
- InitializeComponent();
- }
-
- public LoginWindow(TraeAccount? account)
- : this()
- {
- _account = account;
- }
-
- /// 与 Window.ShowDialog<TResult>(Window owner) 对齐的入口。
- public Task ShowDialog(TopLevel? owner) => OverlayDialog.ShowAsync(this, owner);
-
- /// 手机端获取 Token 的引导(桌面端此处是内嵌浏览器自动登录)。
- private void OnAutoLoginClick(object? sender, RoutedEventArgs e)
- {
- AutoStatus.Text =
- "1)在电脑浏览器打开 www.trae.cn 并登录;\n" +
- "2)按 F12 打开开发者工具 → Application → Local Storage → 找到 Cloud-IDE-Token;\n" +
- "3)复制它的值(一串以 eyJ 开头的 JWT,很长);\n" +
- "4)回到本页粘贴到下方输入框并点「保存」。";
- }
-
- private void OnOpenBrowserClick(object? sender, RoutedEventArgs e)
- => TraeTools.Platform.BrowserLauncher.Open("https://www.trae.cn");
-
- private void OnSaveClick(object? sender, RoutedEventArgs e)
- {
- var pasted = TokenBox.Text?.Trim();
- if (string.IsNullOrWhiteSpace(pasted))
- {
- StatusText.Text = "请先粘贴 Cloud-IDE-Token";
- return;
- }
- if (_account == null)
- {
- StatusText.Text = "无当前账号,无法保存";
- return;
- }
- var initial = _account.Token;
- if (!TokenUtils.ShouldAcceptNewToken(initial, pasted))
- {
- StatusText.Text = "Token 无效(长度不足或与当前账号相同),请重新复制";
- return;
- }
- _account.Token = pasted;
- _account.TokenUpdatedAt = DateTime.Now;
- var uid = TokenUtils.ParseAccountUid(pasted);
- if (!string.IsNullOrEmpty(uid)) _account.AccountUid = uid;
- MainViewModel.AppConfig?.Save();
- OverlayDialog.CloseDialog(this, true);
- }
-
- private void OnCancelClick(object? sender, RoutedEventArgs e)
- => OverlayDialog.CloseDialog(this, false);
-}
diff --git a/Android/Views/MainView.axaml b/Android/Views/MainView.axaml
deleted file mode 100644
index 0ef5cbb..0000000
--- a/Android/Views/MainView.axaml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/Views/MainView.axaml.cs b/Android/Views/MainView.axaml.cs
deleted file mode 100644
index 8ffa194..0000000
--- a/Android/Views/MainView.axaml.cs
+++ /dev/null
@@ -1,159 +0,0 @@
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Threading;
-using TraeTools.Platform;
-using TraeTools.ViewModels;
-
-namespace TraeTools.Views;
-
-///
-/// Android 主视图(单视图根 UserControl)。
-///
-/// 承担三件上游由 MainWindow 负责的事:
-/// 1) 把覆盖层挂给 (对话框宿主);
-/// 2) 每 30 秒检查一次自动签到;
-/// 3) 首次启动弹出 EULA。
-/// 桌面端那些能力(托盘图标、最小化到托盘、任务栏图标、系统通知)在手机上无对应概念,故不移植。
-///
-public partial class MainView : UserControl
-{
- /// 自动签到定时器:每 30 秒检查一次,到点且当天未签时自动签到。
- private readonly DispatcherTimer _autoCheckinTimer;
-
- /// 计时器重入守卫:上一轮任务未结束前跳过本轮 Tick。
- private bool _tickBusy;
- private bool _started;
- private bool _eulaChecked;
-
- public MainView()
- {
- InitializeComponent();
-
- // 覆盖层宿主:所有对话框以 UserControl 形式挂到这里
- OverlayDialog.Attach(OverlayRoot, OverlayContent);
- CrashLog.Boot("MainView: ctor, overlay attached");
-
- _autoCheckinTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
- _autoCheckinTimer.Tick += async (_, _) =>
- {
- if (_tickBusy) return;
- _tickBusy = true;
- try
- {
- if (DataContext is MainViewModel vm)
- {
- // 与桌面端 MainWindow 的定时器保持一致的两步
- await vm.AutoCheckinIfDue();
- await vm.RefreshSnapshotIfNeeded();
- }
- }
- catch (Exception ex)
- {
- // 定时任务异常不允许带崩 UI(未捕获异常会直接闪退)
- CrashLog.Write("AutoCheckin timer tick", ex);
- }
- finally
- {
- _tickBusy = false;
- }
- };
-
- // EULA 需要视觉树就绪后才能取到 TopLevel,故放到 Loaded
- Loaded += OnLoaded;
- }
-
- private void OnLoaded(object? sender, RoutedEventArgs e)
- {
- if (_eulaChecked) return;
- _eulaChecked = true;
- CrashLog.Boot("MainView: loaded");
- try
- {
- _ = EulaCheckAsync();
- }
- catch (Exception ex)
- {
- CrashLog.Write("MainView.OnLoaded", ex);
- }
- }
-
- protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
- {
- base.OnAttachedToVisualTree(e);
- if (_started) return;
- _started = true;
- CrashLog.Boot("MainView: attached to visual tree");
- try
- {
- // 自动签到仅在 App 运行期间有效;长期无人值守请使用「云端签到」
- _autoCheckinTimer.Start();
- }
- catch (Exception ex)
- {
- CrashLog.Write("MainView.OnAttachedToVisualTree", ex);
- }
- }
-
- protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
- {
- try { _autoCheckinTimer.Stop(); }
- catch { /* 停止定时器失败不影响退出 */ }
- base.OnDetachedFromVisualTree(e);
- }
-
- ///
- /// 首次启动弹出免责声明协议(EULA)。同意标记存放于数据根目录 eula_accepted,
- /// 与 SettingsViewModel 里「重置协议同意状态」用的是同一个路径,两边保持一致。
- ///
- private async Task EulaCheckAsync()
- {
- try
- {
- var flag = Path.Combine(TraeTools.Services.DataPaths.Root, "eula_accepted");
- if (File.Exists(flag)) return;
-
- CrashLog.Boot("EULA: showing agreement");
- var eula = new EulaWindow();
- bool agreed;
- try
- {
- agreed = await eula.ShowDialog(TopLevel.GetTopLevel(this));
- }
- catch (Exception ex)
- {
- // 兜底:对话框呈现失败则记录并视为同意,保证应用可用(日志留痕)
- CrashLog.Write("EULA dialog failed", ex);
- agreed = true;
- }
- CrashLog.Boot($"EULA: agreed={agreed}");
-
- if (agreed)
- {
- try
- {
- Directory.CreateDirectory(Path.GetDirectoryName(flag)!);
- File.WriteAllText(flag, DateTime.UtcNow.ToString("o"));
- }
- catch
- {
- // 标记写失败下次仍会弹,可接受
- }
- }
- else
- {
- // 不同意:与桌面端「直接退出」对齐 —— 手机端关闭唯一 Activity 即回到桌面。
- // 不做 Process.Kill:那会让系统记录成异常退出。
- CrashLog.Boot("EULA: declined -> finish activity");
- MainActivity.Current?.Finish();
- }
- }
- catch (Exception ex)
- {
- CrashLog.Write("MainView.EulaCheckAsync", ex);
- }
- }
-}
diff --git a/Android/Views/OverlayDialog.cs b/Android/Views/OverlayDialog.cs
deleted file mode 100644
index 0fb5381..0000000
--- a/Android/Views/OverlayDialog.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Controls.Presenters;
-
-namespace TraeTools.Views;
-
-///
-/// 覆盖层模态对话框宿主(Android)。
-///
-/// 为什么必须有这一层:Avalonia 的 Android 后端是单视图平台,
-/// new Window() 会在 Avalonia.Android.WindowingPlatformStub.CreateWindow()
-/// 抛 NotSupportedException,所以整个移植里所有对话框内容都写成 ,
-/// 由本类把它们挂到主视图的全屏覆盖层上呈现,并用
-/// 模拟「模态 + 返回值」的语义。
-///
-/// 与上游 VM 的衔接:上游 ViewModel 调用的是 dlg.ShowDialog<bool>(owner)
-/// (Window 的实例方法)。Android\Views\ 下的对话框类因此都额外提供了一个
-/// 同名的 ShowDialog<TResult>(TopLevel? owner) 方法,内部转调本类 ——
-/// 于是共享 ViewModel 一行都不用改。
-///
-public static class OverlayDialog
-{
- private static Panel? _root;
- private static ContentPresenter? _host;
- private static UserControl? _current;
- private static TaskCompletionSource