Skip to content

feat(2.2.0): 多账号/四种登录/六渠道推送/日志页/深色模式 + 单工程多目标双端(含 Android) - #53

Closed
akexin wants to merge 38 commits into
star620:mainfrom
akexin:sync-2.2.0
Closed

akexin wants to merge 38 commits into
star620:mainfrom
akexin:sync-2.2.0

Conversation

@akexin

@akexin akexin commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

这个 PR 把 2.2.0 那条线(fork 的 main)整体推到上游。它包含全部自研功能,并且自带一套 Android 实现(单工程多目标),因此与 #52 刚合并进来的 Android/ 目录是两套并行方案 —— 请见下方「需要移除的东西」。

本分支已经把上游 main 合并了进来(upstream/main 是本分支的第二个父提交),所以不存在任何合并冲突,可以直接合并。

142 files changed, +10005 −7459

说明:fork 的 main 分支本身没有被动过,本 PR 从独立分支 sync-2.2.0 提出。

⚠️ 请一并移除 #52 引入的 Android/ 目录

2.2.0 的 Android 端是单工程多目标实现的,不需要第二个工程:

<TargetFrameworks>net10.0-windows;net10.0-android</TargetFrameworks>

Windows 专属能力(WinForms / WebView2 / DPAPI / 托盘)与 Android 平台实现(Platform/ 目录)全部按 TargetFramework 条件编译,一份代码两个产物。Android/ 那套「独立工程 + 链接共享源码」与它重复,请一并删掉:

  • Android/**(33 个文件)、.github/workflows/android.yml
  • TraeTools.csproj 里的 3 行 Remove="Android\**" 排除项

删掉之后 Android 端不会失去任何能力,CI 里 build-android 作业会继续编译 net10.0-android 并产出 APK。

2.2.0 带来什么

能力 说明
双端产物 net10.0-windows(win-x64 单文件 exe)+ net10.0-android(apk),versionName 与 version.json 同源
多账号切换 账号列表 / 顶栏下拉切换 / 冷切换,不再点一下就直接跳登录
四种登录方式 会话 Cookie 导入、剪贴板智能识别、账号导入导出 JSON、读取本机 TRAE 客户端
账号简介与数据面板 显示积分数量、消耗积分等用量数据
自定义设备号 可自定义 16 位设备号,并做账号与本地安全检查
保活 可配置保活开关与时间
六渠道推送 通用 Webhook、系统本地通知、钉钉、企业微信、Telegram Bot、自定义 SMTP 邮箱(可自定义端口)
日志页 最近日志 / 全部日志,可选自动滚动
深色模式 切换时自动适配整套界面配色
签名与发布 显式三方案签名 APK + 指纹比对;.NET 打包后重签;签名密钥轮换工作流

与上游现有实现的取舍

「以 2.2.0 为准」意味着上游在分叉点之后的部分架构工作会被 2.2.0 的对应实现取代。逐项说明如下,便于评估:

上游改动 2.2.0 的对应实现 结果
aac7764 拆出 TraeTools.LoginHost 独立进程(去 WinForms/WebView2,为 NativeAOT 让路) 进程内 WebView2 宿主自动取凭证;2.2.0 不做 NativeAOT(Android TFM 下也没有 WinForms) LoginHost 相关文件移除
3e34c46 JSON 走源生成(Services/Json) 2.2.0 未走 AOT/裁剪,直接用 System.Text.Json;且 AppJson 引用了 2.2.0 已删除的 TraeSwitch.Services.VaultMeta、GitHubSecretPayload,无法编译 Services/Json/** 移除
cb573f2 AOT 目录版 zip 整包分发 + 更新器整套文件替换 2.2.0 的 UpdaterService 走单文件 exe 覆盖 + zip 校验 以上游版本被 2.2.0 版取代
6b654a9 93cef5f 6d2e8b2 switch 系列修复 2.2.0 重写了账号/切换架构(删除 Services/Switch/* 旧实现、ViewModels/SwitchViewModel.cs、Views/SwitchView.axaml 等) 旧实现随 2.2.0 架构移除
4da8bf7 旧客户端目录不再优先探测 2.2.0 有对应的客户端探测与安全检查实现 以上游版本被 2.2.0 版取代
503a4d2 Android/ 独立工程 2.2.0 的单工程多目标 见上方「请一并移除」

其中 10 个 Android/** 文件被 git 识别为重命名而非删除(例如 Android/Platform/AndroidVault.cs → Platform/Secrets.Android.cs、Android/Platform/MainActivity.cs → Platform/MainActivity.cs、Android/Resources/** → Resources/**),所以 diff 里看起来是「挪了位置」。

迁移影响

  • 上游被 2.2.0 取代的 20 个文件(Views/{Checkin,Cloud,Dashboard,Switch,Usage}View.*、ViewModels/{Dashboard,Switch,Usage}ViewModel.cs、Services/Switch/{AuthProbe,CarrierProfiler,ClientController,SwitcherService,VaultService}.cs、Services/Usage/*)在 2.2.0 的单页架构里已合并进新页面。
  • version.json 会从 1.3.3 变为 2.2.0。
  • ApplicationId 为 cn.akexin.traetools、ApplicationVersion(versionCode)为 18。

验证

  • 本分支与上游 main 之间零冲突(upstream/main 已是本分支的父提交,合并可快进)。
  • 合并结果的树与 fork main(464f9b7)逐字节一致(git diff 为空),因此 2.2.0 既有的构建与发布流程不受影响。
  • CI:build-windows(windows-latest)与 build-android(ubuntu-latest,dotnet workload install android)两个作业均通过,分别产出单文件 exe 与 APK。

如果你更希望保留上游的某一部分(比如 TraeTools.LoginHost 的进程隔离,或 JSON 源生成),告诉我,我可以改成分支上保留、而不是取代。

akexin and others added 30 commits October 13, 2026 21:13
Android 移植(完整功能,账号切换按平台隐藏):
- 多目标构建 net9.0-windows(单文件 exe)+ net9.0-android(APK)
- Platform/ 跨平台抽象:Keystore(RSA+AES-GCM) 替代 DPAPI、Intent 打开浏览器、
  Android 数据目录(App 私有 files/TraeTools)、托盘/开机自启/进程控制平台分流
- 登录改造:Android 隐藏内嵌 WebView2 入口,保留浏览器+手动粘贴 Token 方案
- 账号切换仅 Windows 桌面端可用(Android 无本地客户端可操作),导航项按平台隐藏
- SQLite 增加 android 原生库(SQLitePCLRaw.lib.e_sqlite3.android)

TRAE 双端合并适配(2026-10-09 官方合并 TraeWork/TraeCode 为 TRAE):
- 设备号解析目录覆盖 TRAE/TRAE CN/TRAE SOLO CN/Trae CN
- 客户端进程名多名字匹配(TRAE,TRAE SOLO CN,Trae),exe/目录探测覆盖新旧

非强制更新:
- Android 检查更新仅提示并跳转 Release 页,不自动下载/安装;Windows 维持询问式

仓库指向:更新检查、云端签到 fork 源、PAT 提示等改为 akexin/TraeTools-Android
CI/Release:新增 android job(workload install → publish apk → 挂 Release)
…ick 处理器

1. NETSDK1100:多目标项目在 ubuntu 上执行 dotnet build/publish -f net10.0-android 时,
   restore 阶段仍会还原 net10.0-windows,需显式允许下载 Windows 目标包:
   - ci.yml / release.yml 的 ubuntu job 命令加 -p:EnableWindowsTargeting=true
   - csproj 同步加 <EnableWindowsTargeting>true</EnableWindowsTargeting>(非 Windows 机器通用兜底)
2. LoginWindow.axaml 的自动登录按钮绑定了 Click="OnAutoLoginClick",而该方法此前被
   #if !ANDROID 整体排除,Avalonia XAML 编译器在编译期解析事件处理器会直接报错。
   改为双平台共存:Android 端仅回写提示文案(按钮已隐藏),Windows 端逻辑不变。
1. CS8954 ClientController.cs:文件顶部 file-scoped namespace 两平台共用,
   #if ANDROID 分支又重复声明了一遍 → 删除重复声明
2. CS0104 App.axaml.cs:.NET Android SDK 隐式全局 using 含 Android.App,
   裸 Application 与 Avalonia.Application 歧义 → 基类写全称 Avalonia.Application
3. CS0311 MainActivity.cs:上一项的连带错误(App 未构成 Avalonia.Application),
   随 #2 一并解决;同时补 using Avalonia;(WithInterFont 扩展方法在该命名空间)
4. UiHost.cs 同 #2:Application.Current → Avalonia.Application.Current

已全景排查 Android.App/Android.Widget 全局 using 的全部常见冲突类型名,
Android 编译单元内无其他裸用冲突类型。
1. Secrets.Android.cs:Cipher.EncryptMode/DecryptMode 常量在 .NET Android 中已
   标记为 obsolete(报错级 CS0619),改用 Javax.Crypto.CipherMode 枚举成员
   (EncryptMode/DecryptMode,Init(CipherMode, IKey) 重载已确认存在)
2. Program.cs:BuildAvaloniaApp() 整段移入 #if !ANDROID——UsePlatformDetect 扩展
   由 Avalonia.Desktop 提供,Android 目标不引用该包;Android 端由
   AvaloniaAndroidApplication 基类自行构建 AppBuilder
- NU1102:多目标项目下命令行 -r win-x64 是全局属性,会渗入 net10.0-android 的
  还原阶段,去找不存在的 Microsoft.NETCore.App.Runtime.Mono.win-x64 而失败
- 改为在 csproj 的 net10.0-windows 条件组内声明 <RuntimeIdentifier>win-x64</RuntimeIdentifier>,
  release.yml 的 publish 命令去掉 -r win-x64
- README 编译指引同步(多目标须显式 -f;非 Windows 构建 Android 需 EnableWindowsTargeting)
问题:release.yml 用 find | head -1 取 APK,取到的是未签名的中间产物
xxx.apk(安装会报 INSTALL_PARSE_FAILED_NO_CERTIFICATES);且 SDK 默认用
每次运行临时生成的 ~/.android/debug.keystore 签名,跨版本签名不一致,
无法覆盖安装。

- 新增 signing/traetools.keystore(PKCS12,alias/口令 traetools,3DES-PBE
  最大化 JDK 兼容),随仓库公开,仅用于保证签名一致性
- publish 增加 AndroidKeyStore=true + 四个签名参数 → 产出 *-Signed.apk
- Collect 步骤改用 *-Signed.apk 并在找不到时报错;打印全部候选与
  apksigner verify 证书指纹
闪退根因:Avalonia 单视图生命周期不支持再创建 Window(官方文档明确),启动路径
MainWindow.OnOpenedEulaCheck 调用 EulaWindow.ShowDialog,异常经 async void 未捕获直接带崩进程。

- 新增 Views/OverlayDialog:Android 把对话框内容借用 MainWindow 顶部覆盖层呈现
 (TCS 返回结果),桌面端仍走原生 ShowDialog;6 个对话框关闭入口统一走
  OverlayDialog.CloseDialog;14 处 ShowDialog 调用全部改走 OverlayDialog.ShowAsync
- 新增 Platform/CrashLog:全局未捕获异常 + 启动轨迹日志;Android 写外部应用目录
  Android/data/cn.akexin.traetools/files/crash/(文件管理器免 root 可读)+ logcat
- EULA 对话框失败兜底:记录日志并视为同意,保证应用可用
- 定时签到 Tick 加异常防护(async void 不再可能带崩 UI)
- 版本 1.4.1(versionCode 11,可覆盖安装 1.4.0)
手机(Android 16 / 澎湃 OS)打开即闪退,日志无法回收,故本版以"让崩溃可见 + 排除 AOT 风险"为目标:

- 关闭 AOT 与链接器(RunAOTCompilation/AOTAssemblies=false、AndroidLinkMode=None):
  Release 默认 LLVM AOT 下反射路径(ViewLocator、SQLitePCLRaw provider 初始化等)
  可能抛 MissingRuntimeArtifactException,表现为启动即崩;本应用为网络 IO 型,稳定性优先。
- 新增原生 CrashReportActivity:进程最早点(Application 构造)注册全局异常捕获,
  AndroidEnvironment 事件里反射置 Handled=true 阻止进程被杀,改为弹出可读的原生错误页
  (堆栈可复制、可一键重启),不再"一闪而过"。
- 新增 StartupErrorView(纯代码构建,不依赖 XAML):Avalonia 可显示时用它呈现异常 + 启动日志。
- 崩溃标记 crash_flag:上次崩溃则本次直接进错误页,避免反复闪退,可清除后重试正常界面。
- CrashLog 增强:崩溃标记读写、日志目录暴露、ReadRecent 供界面展示。
- 启动链路全面兜底:App.Initialize / OnFrameworkInitializationCompleted / MainWindow.OnOpened
  全程 try-catch;ViewLocator 反射失败降级为占位文本;OverlayDialog 异常返回默认值。
真实堆栈(v1.4.2 崩溃日志)确认根因:
  System.NotSupportedException: Specified method is not supported.
     at Avalonia.Android.WindowingPlatformStub.CreateWindow()
     at Avalonia.Controls.Platform.PlatformManager.CreateWindow()
     at Avalonia.Controls.Window..ctor()
     at TraeTools.Views.MainWindow..ctor()
Android 是 Avalonia 单视图平台,构造任何 Window 都会抛异常。
此前把 MainWindow 当 MainView、并用 Window 做对话框,必然启动即崩。

改造:
- 新增 Views/MainView.axaml(.cs)(UserControl):承载原 MainWindow 的全部界面与
  双平台共享逻辑(覆盖层宿主、自动签到定时器、首次启动 EULA)。
- MainWindow(Window)仅保留桌面专属行为(尺寸/图标/托盘/关闭最小化),内部嵌 MainView;
  Android 端 App 直接用 MainView 作为 ISingleViewApplicationLifetime.MainView。
- 6 个对话框(Eula/Prompt/Input/PatInput/DeviceAuth/Login)由 Window 改为 UserControl,
  新增 DialogTitle 属性供桌面端窗口标题使用。
- 新增 Views/DialogHostWindow:桌面端把对话框 UserControl 包进真实 Window 后 ShowDialog;
  OverlayDialog 据此重写(Android 覆盖层 / 桌面 DialogHostWindow 双路径)。
- 修正 Android 上 owner 恒为 null 导致对话框被跳过:移除 ViewModels 中
  `owner != null &&` 短路与 `owner == null return` 守卫,桌面端 owner 缺省时兜底主窗口。
- EULA 触发改到 Loaded(构造期 desktop.MainWindow 尚未赋值,取 owner 会失败)。
- 版本 1.4.3(versionCode 13)。
手机(360dp 级别屏宽)上桌面布局严重溢出:190px 侧栏占去一半屏宽,
仪表盘双列卡片与页头按钮被裁掉。本版按宿主实际宽度自动切换布局:

- MainView 新增紧凑模式(IsCompact,宽度<640 逻辑像素触发,由
  OnPropertyChanged(Bounds) 驱动,桌面窄窗口同样生效):
  * 侧栏隐藏,改为顶部品牌栏 + 底部标签栏(仪表盘/用量/签到/云端/设置)
  * 内容区边距 28,32 → 14,12
  * 仪表盘:页头按钮堆叠到标题下方;趋势图与账号概览改上下排列
  * 签到/设置/用量:双列卡片上下堆叠;用量 4 张汇总卡 2x2 排布
  * 所有切换用 ^.compact 后代样式实现,宽屏/桌面布局零影响
- 修复 OverlayRoot 覆盖层未跨列的 bug:此前对话框宿主只占 Grid
  第一列单元格,EULA 等弹窗整体偏在侧栏上;现显式 RowSpan=3/ColumnSpan=2
- 版本 1.4.4(versionCode 14)
* feat: TraeTools 移动端 UI 优化方案

Co-authored-by: traeagent <traeagent@users.noreply.github.com>

* fix(xaml): 修复 CI 编译错误——Button 无 BoxShadow 属性;Border 单子元素限制用 Panel 包裹

* chore: bump version to 1.4.5

---------

Co-authored-by: akexin <akexin@users.noreply.github.com>
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
* feat(ui): 扁平弱卡片重构——去渐变/投影、圆角降级 12/8px、设置页垂直流修复遮挡

- 主题层:卡片 12px 圆角去投影;新增 HeroTint 淡蓝色块令牌;按钮/输入 8px 圆角;code-box 改浅灰文本区;移除渐变 CTA(统一 btn-primary)
- 仪表盘:Hero 换淡蓝扁平色块 + 深色文字
- 签到页:渐变块换淡蓝底 + 细线进度环;日历状态反转(已签淡底/今日实心);图例居中;空记录改轻量文案
- 用量页:重构为「主指标卡(Token 大数字)+ 三个带图标副指标」;会话/模型空态改轻量引导文案
- 设置页:左右双列改垂直列表流;深色模式并入列表项;签到时间/间隔改垂直表单;设备号与操作按钮分行修复重叠;9074 说明改折叠 Expander
- 云端页:部署流程改纵向步骤列表;部署状态改键值对行;警告条细化;CTA 统一扁平主色
- 导航:选中 Tab 图标轻微放大动效;覆盖层圆角 12

* chore: bump version to 1.4.6

---------

Co-authored-by: akexin <akexin@users.noreply.github.com>
* feat(ui): 异步操作动效——签到/刷新/拉取/部署加载反馈与防重复提交

- 新增 loading-bar 细加载条样式(页头标题下方,3px 主色进度条)
- 签到页:新增 IsCheckingIn;按钮文案「签到中…」、CanCheckin 禁用态、加载条;DoCheckin 加防重入
- 仪表盘:新增 IsRefreshing;「刷新状态」按钮文案与禁用联动、加载条;RefreshStatus 加防重入
- 用量页:新增 PullButtonText(「拉取中…」)+ 加载条,复用已有 IsLoading
- 云端页:复用已有 IsBusy,加载条 + 按钮禁用(与命令自身防重入一致)
- Tab 栏:新增按压缩放 0.92 触感反馈

* chore: bump version to 1.4.8

---------

Co-authored-by: akexin <akexin@users.noreply.github.com>
产品减法(Less is More):六页导航收敛为「单页仪表盘 + 二级设置」。

新增
- ViewModels/HomeViewModel.cs:聚合层,复用既有的 CheckinViewModel / CloudViewModel 引擎
  (刻意共享同一实例,保证定时自动签到与页面按钮作用于同一状态)
- Views/HomeView.axaml:单页三模块 —— 本地签到卡片 / 云端自动签到卡片 / 最近 3 条记录
- .github/workflows/bump-version.yml:version.json 与 Android versionCode 自增,推送后显式派发 CI

界面重构
- MainView 移除侧栏与底部 4-Tab 导航,改为顶栏:小号加粗标题 + 云端状态徽章(可点击展开详情)+ 设置入口
- 设计规范落地:卡片 16px 圆角 + 0 4 12 rgba(0,0,0,.03) 微投影(去边框)、主色胶囊 CTA、
  状态色徽章与圆点(绿=运行中 / 黄=待部署 / 红=未部署)、运行中呼吸灯动效
- 修正紧凑模式边距:原实现把 Padding 写在元素上,局部值压过样式导致紧凑覆盖失效,改为全部由样式给出

移除(能力下线,非仅隐藏入口)
- 用量统计:UsageViewModel / UsageView / TraeUsageClient / UsageStore
- 账号切换:SwitchViewModel / SwitchView / SwitcherService / VaultService / AuthProbe /
  ClientController / CarrierProfiler(Windows-only 能力)
- 仪表盘聚合页:DashboardViewModel / DashboardView(职责被单页吸收)
- 其余页面视图(CheckinView / CloudView)随单页化删除,引擎保留并被 HomeView 复用

保留
- 本地/自动签到、云端授权与部署、飞书推送、账号管理(登录唯一入口,仍在设置页)、
  更新检查、EULA、崩溃兜底与 Overlay 对话框宿主
## 1. 移动端版本号错乱(检查更新说 2.0.0,App 内却显示 1.0.0)

根因有两处,互相独立:

- Services/AppVersion.cs 用 Assembly.GetEntryAssembly() 作为版本读取入口。
  该 API 在 **.NET for Android 上返回 null**,导致「内嵌 version.json 资源」与
  「InformationalVersion 回退」两条路径同时落空,最终命中硬编码兜底常量 "1.0.0"。
  UpdaterService.CurrentVersion 同源,于是本地版本被算成 1.0.0、远端 2.0.0,
  升级判定一直错位。
  修复:改为按 CandidateAssemblies() 依次尝试(GetEntryAssembly 可能为 null 或抛异常),
  并以 typeof(AppVersion).Assembly 兜底;同时截掉 InformationalVersion 的 "+<commit>" 后缀。

- TraeTools.csproj 的版本同步写在 Target(BeforeTargets=GenerateAssemblyInfo)里。
  Target 内的赋值只对求值之后运行的 Target 可见,而 .NET for Android 生成
  AndroidManifest 的阶段更早,实测发布出的 v2.0.0 APK 清单里
  android:versionName 仍是 csproj 静态值 1.4.4(versionCode=15 是对的)。
  修复:版本改在「求值期」的 PropertyGroup 中从 version.json 读取,并让
  ApplicationDisplayVersion 直接引用该变量,csproj 不再有硬编码版本。

## 2. 签名校验:从「只打印」改为硬门禁

先厘清事实:v2.0.0 APK 实测签名是完整的(v1 JAR + v2 + v3 三方案齐全,
证书 CN=TraeTools/O=akexin,SHA-256 313fa330…),并非签名无效。
「签名验证不通过」的真实来源是**签名不一致**——signing/traetools.keystore 是
2026-10-13 的 0f0620a 才引入的,此前各版本用的是每次 CI 重新生成的临时 debug
keystore,故 ≤v1.4.7 的用户无法覆盖安装。

- 新增 .github/workflows/rotate-keystore.yml:用构建机 keytool 生成密钥
  (本机无 JRE,第三方工具生成的 PKCS#12 在 Java 侧 alias 映射不确定),
  自动写入 signing/EXPECTED_CERT_SHA256.txt 并提交回分支。
- release.yml 的签名步骤由 `apksigner verify … | head -8 || true`(既不校验退出码
  也不比对指纹)改为三项硬校验:退出码、证书 SHA-256 与基准文件一致、
  结构性校验 v1+v2+v3 齐全。方案齐全性刻意直接解析 APK 二进制,不 grep apksigner
  文本输出——实测该 CI 的 build-tools 输出格式是 "V3.0 Signer:" 而非
  "Signer #1 certificate …",文本匹配不可靠。

## 3. 「关于」页链接

「打开原仓库」与「打开我的 Fork 仓库」两行此前指向同一地址(本项目仓库),
是 fork 时代遗留的重复项且用词误导。合并为单行「项目仓库」,
OpenForkRepoCommand / ForkRepoUrl 一并移除。

## 4. 版本号 2.0.1

version.json 2.0.0 → 2.0.1,Android versionCode 15 → 16(必须单调递增)。
由 .github/workflows/rotate-keystore.yml 手动触发。轮换后所有已安装用户必须卸载旧版才能安装新版。同时更新 signing/EXPECTED_CERT_SHA256.txt(release.yml 的签名校验门禁基准)。
akexin and others added 8 commits October 14, 2026 15:48
第一次 v2.0.1 发布被新增的签名门禁拦下,暴露两个真问题(都值得修):

1. v1(JAR) 签名丢失
   `dotnet publish -p:AndroidKeyStore=true` 产出的包,v2.0.0 含 v1 签名,
   而换密钥后的 v2.0.1 为 `Verified using v1 scheme (JAR signing): false`。
   即 v1 是否产出取决于 .NET for Android 的内部启发式(与 apksigner 的
   minSdk 默认值交互),不是确定行为。
   改为:publish 用 AndroidKeyStore=false 只产未签名包,再由工作流显式
   `zipalign -f -p -P 16 4` + `apksigner sign --v1/--v2/--v3 true` 签名。
   行为从此确定,且不再依赖打包器的隐式决策。
   (-P 16 保证 .so 16KB 页对齐,Android 15+ 有要求;不支持 -P 的旧
   build-tools 回退 -p 4。)

2. 指纹比对误报
   rotate-keystore 里 keytool 输出大写、release 里 apksigner 输出小写,
   直接字符串比较判定"不一致"。改为两侧统一 `tr -cd 0-9A-Fa-f | tr a-f A-F`
   归一化后再比。

此次为上一提交 6f4cf19 的发布链路修复,v2.0.1 的 tag 与不完整 Release
已删除,修复后会重新打 tag 触发完整发布。
第二次发布仍被门禁拦下,两处问题:

1. 方案结构校验的读取窗口太小(本次真正的失败点)
   原脚本固定读「文件尾部 64KB」,但本包中央目录本身约 73KB,
   而 APK Signing Block 位于中央目录**之前**,于是 rfind 落空,
   误报 "APK Signing Block: 未找到"、v2/v3 判定为缺失。
   (同一次日志里 v1 判定为「存在」,说明上一步的显式三方案签名其实是成功的。)
   改为先读尾部 EOCD 取中央目录偏移,再自该偏移向前多读 64KB。
   已用 v2.0.0 的实测数据在本地回归:旧逻辑取不到,新逻辑正确解析出
   v2 / v3 / source stamp。

2. 自行 zipalign 会把 16KB 页对齐降级为 4KB
   上一版改为「未签名包 + zipalign + 签名」,但实测该 build-tools 的 zipalign
   不支持 -P 16(16KB 页对齐,Android 15+ 对含原生库的包有要求),
   只能回退 -p 4,等于把 .NET 打包器原有的对齐策略降级。
   改为:仍由 .NET 打包器签名(它自带对齐流程),然后对其产物**重签**
   以强制 v1+v2+v3——重签不触碰 zip 结构,对齐策略保持不变。

两轮失败都是新增门禁抓出来的真实缺陷,不是误报。
本轮把「获取登录态」和「结果送达」两端都补全,并解决了长期存在的深色模式失效问题。
本地已用 .NET 10.0.402 SDK 对 net10.0-windows 目标做过 Debug/Release 双配置编译校验
(0 错误 0 警告)。

### 登录方式(原仅内嵌浏览器 / 手动粘贴)
- Services/Login/CredentialImporter.cs:清洗并识别粘贴内容,剥掉
  `X-Cloudide-Session=`、`Authorization: Cloud-IDE-JWT`、Cookie/Set-Cookie 头名、
  成对引号与折断换行等外壳;JWT 与会话按结构判定;会话自动换取 Token 并保留用于静默续期
- Services/Login/LocalClientImporter.cs:只读扫描本机 TRAE 客户端数据目录
  (storage.json / Local Storage/leveldb)提取设备号、Token 与会话,多来源多策略,
  失败即给出可执行的下一步建议
- Services/Login/AccountBackup.cs:账号导出/导入。口令派生密钥
  (PBKDF2-SHA256 20 万次)+ AES-256-GCM 认证加密,不依赖 DPAPI/Keystore 的机器绑定,
  因而可跨设备迁移;导入按账号去重合并,不删除本地已有账号
- Views/LoginWindow:重写为四方式入口 + 内容类型下拉(自动识别/Token/会话)

### 账号切换(2.0 曾整体下线)
- 顶栏账号胶囊(首字头像 + 当前账号名)展开账号列表,点选即切换;
  只有一个账号时点击直接进账号管理页
- 设置页账号列表恢复「点选即切换为当前账号」
- 切换后签到引擎、首页状态与设置页账号状态整体重载

### 推送通知(原仅飞书)
- Services/Notify/NotifyDispatcher.cs:统一扇出,渠道彼此隔离,单个失败不阻断其它与签到主流程
- Services/Notify/EmailNotifier.cs:自实现 SMTP。内置 SmtpClient 不支持 465 隐式 TLS,
  而国内邮箱普遍使用 465,故自行实现 ssl / starttls / none 三种模式,
  并支持 AUTH LOGIN 与 AUTH PLAIN 回退、RFC 2047 中文主题
- Services/Notify/RobotNotifiers.cs:通用 Webhook({title}/{content} 模板,
  兼容 Server酱/PushPlus/Bark)、钉钉(含加签 HMAC-SHA256)、企业微信、Telegram
- Services/Notify/SystemNotifier.cs:Windows 主窗口通知 / Android 通知栏
- 设置页新增「推送通知」卡片:总开关 + 六渠道独立开关与配置 + 一键测试全部

### 日志
- Services/AppLog.cs:内存环形缓冲(800 条)+ 按月分文件落盘,
  格式 `[时间] [级别] [账号] 消息`,兼容旧格式与崩溃日志格式
- Views/LogView + ViewModels/LogViewModel:最近(本进程实时)/ 全部(历史归档文件)
  双视角,可复制、删除单个文件、清空全部
- 「自动滚动」可关闭并记忆偏好:关闭后新日志仍入列表,但视图停在当前位置,
  翻看早先记录时不会被不断涌入的新日志推着走

### 深色模式
- 主题改为三态(跟随系统 / 浅色 / 深色),App.ApplyThemeMode 统一切换
- 修复 AppConfig.CloneForStorage 的逐字段白名单拷贝导致 DarkMode 永不落盘
  (用户此前「切了深色、重启又变回浅色」的根因),改为 JSON 深拷贝
- LoginWindow / EulaWindow / PromptWindow / InputWindow / PatInputWindow /
  DeviceAuthWindow 六处对话框的硬编码浅色全部替换为主题令牌

### 修复的真实缺陷(均由本地编译/静态核对发现)
- AppConfig.CloneForStorage 误用 this.Accounts 而非 copy.Accounts,
  会就地加密运行中账号的 Token/Session/DeviceId —— 保存一次设置后,
  内存里的 Token 变成 DPAPI 密文,后续所有接口调用发出密文、签到全部失败
- SystemNotifier 桌面端调用 TrayIcon.ShowNotification,但 Avalonia 12 的 TrayIcon
  已无此方法(0.10 后移除);改走 WindowNotificationManager,经 App.DesktopNotifier 桥接
- EmailNotifier.SendAsync 返回 Task<MailResult>,而 NotifyDispatcher.WrapAsync
  要求 Task<NotifyResult>,类型不兼容;统一为 NotifyResult
- 剪贴板调用缺少 `using Avalonia.Input.Platform`(扩展方法所在命名空间);
  且 Avalonia 12 已把读取入口由 GetTextAsync 改名为 TryGetTextAsync
- LogView/SettingsView 的 XML 注释中出现 `--`,属非法 XML,Avalonia 编译期直接报 AVLN1001
- FeishuWebhook 由明文改为加密后,老配置解密失败会得到 null 而静默清空推送地址;
  改为解密失败时保留原值
- version.json: 2.0.1 -> 2.1.0(版本号单一真源)
- TraeTools.csproj: Android versionCode 16 -> 17
  (versionCode 必须严格单调递增,否则 APK 无法覆盖安装)
新增能力
- 账号简介:每个账号可写自由文本备注,设置页账号卡片与顶栏切换器均显示,停手 0.8s 自动落盘
- 积分数据面板:剩余积分 / 已消耗 / 累计获得 / 总额度 / 今日获得 / 连续签到 / 签到天数 /
  最近签到 + 会员·学生·今日已签·Token 有效期状态串;本地统计离线可看,额度三块点「刷新数据」
  从云端拉取(TraeApiClient 新增 GetEntitlementAsync,返回总配额与已消耗)
- 自定义 16 位设备号:手填 / 随机生成 / 保存,实时校验位数、字符与「不得与其它账号重复」
- 账号保活:每个账号独立开关 + 间隔(5–1440 分钟,默认 60),到点用会话 Cookie 静默换新
  Token 并刷新积分与资料,只探测不 claim,不影响今日签到判定;支持「立即保活」。
  实现见 Services/KeepAlive/KeepAliveService.cs,挂在 MainView 的 30 秒定时器上与自动签到共用节拍
- 安全检查:Services/Security/SecurityAudit.cs,8 项本地只读审计(配置加密状态、加解密往返自检、
  明文敏感字段残留、设备号合规与重复、Token/会话有效期、保活覆盖、数据目录私有性、日志凭证泄漏),
  结论按风险→警告→通过排序,只输出字段名不显示凭证内容;审计在后台线程执行,不阻塞界面

修复
- Android 上所有加密字段都无法解密(重要):Secrets.Unprotect 把「跨平台密文互不通用」的判断
  写在了共享层,而 Android 自己的密文就是 "and-v1:" 前缀,于是被自己的规则拦下——表现为 Token
  每次启动即丢、设备号退化成密文被当作设备号发出,直接触发风控 9074「参与用户太多」。
  现把该判断下移到 Windows 实现内部,并加自愈:运行时发现设备号不是 16 位数字自动换发新号
  (AccountHelpers.IsValidDeviceId)
- 「授权 GitHub」不再自动打开浏览器:BrowserLauncher.Open 改为返回 bool 且不再静默吞异常,
  Android 按 Activity → Application 上下文 → 系统选择器三级回退,失败写入日志(category=browser);
  CloudViewModel.OpenAuthPage 调整为先开浏览器再复制授权码;清单补 <queries> http/https
- 点账号旁下三角被强行带去设置页:ToggleAccountSwitcher 改为始终展开账号列表,
  列表底部固定提供「添加账号 / 管理账号」入口(ManageAccountsCommand)

其他
- TraeAccount 新增 Bio / KeepAliveEnabled / KeepAliveIntervalMinutes / LastKeepAliveAt /
  LastKeepAliveMessage / QuotaLimit / QuotaUsed / StatsUpdatedAt
- CheckinDatabase 新增 GetAccountStats / GetAccountSignedDates / GetAccountStreakDays
  (归属判定同时接受 account_id 与 account_name,兼容旧文本迁移数据)
- TokenUtils 新增 TryGetExpiry 解析 JWT exp
- 静态检查工具纳入仓库 tools/(check_bindings / lint_xaml_comments / lint_cs)
- version.json → 2.2.0,ApplicationVersion → 18

本地验证:net10.0-windows Debug 与 Release 均 0 错误 0 警告;
224 个绑定表达式全部可解析;0 处非法 XAML 注释;67 个 .cs 文件结构检查通过。
本合并提交的树等于 fork 的 2.2.0 源码本身(origin/main = 464f9b7),理由:

上游在分叉点之后做的架构工作与 2.2.0 的对应实现重复,且 2.2.0 侧已经删除了
它们所依赖的类型,无法在同一棵树里共存:

  · TraeTools.LoginHost 独立登录进程(aac7764)
      2.2.0 用进程内 WebView2 宿主自动取凭证,不依赖外部宿主进程。
  · Services/Json 源生成 JSON(3e34c46)
      引用了 2.2.0 已删除的 TraeSwitch.Services.VaultMeta 与 GitHubSecretPayload。
  · Android/ 独立工程 + .github/workflows/android.yml(503a4d2)
      2.2.0 用单工程多目标(net10.0-windows;net10.0-android)实现 Android 端。

因此「以 2.2.0 为准」即意味着上述改动被 2.2.0 的实现取代。

记录 upstream/main 为第二个父提交,使本分支与上游 main 之间不再存在合并冲突。
@akexin

akexin commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@star620 补充一下这个 PR 里最需要你拍板的地方,免得看起来像无理由整体覆盖。

为什么是「整条线取代」而不是「只加功能」

我本来想只提增量、保留你现在的架构,但编译上过不去,不是偏好问题:

你 Services/Json/AppJson.cs 走的是源生成 JSON,里面注册了这两个类型:

[JsonSerializable(typeof(GitHubSecretPayload))]   // CompactJsonContext
[JsonSerializable(typeof(VaultMeta))]             // IndentedJsonContext

而 2.2.0 里 VaultMeta、GitHubSecretPayload 已经不存在了(账号 / 凭证模型整个重写过)。只要把 2.2.0 的账号体系放进来,AppJson 就先编不过;接着 FeishuNotifier.cs、GitHubApiClient.cs、TraeApiClient.cs、UsageStore.cs 这几个引用了 AppJson 的文件会连锁报错。

我实测过直接 git merge:18 处冲突(10 个「2.2.0 已删、你这边还在改」+ 8 个内容冲突,含 TraeTools.csproj、ci.yml、release.yml、ViewLocator.cs、AppConfig.cs)。

这个 PR 怎么做到零冲突的

用策略级合并把上游 main 记为第二个父提交,让合并结果的树 = 2.2.0 本身:

git checkout -B sync-2.2.0 <2.2.0>
git merge -s ours upstream/main
  • 结果 mergeable: clean,你不用解任何冲突,可以直接合并
  • 你的全部历史完整保留在那个父提交里,随时可回看、可 revert
  • fork 的 main 我一个字节都没动,本 PR 从独立分支 sync-2.2.0 提出

哪一项你更想留你那边的,说一声我就改

你那边 2.2.0 的做法 想保留的话
TraeTools.LoginHost 独立进程(为 NativeAOT 让路) 进程内 WebView2 宿主自动取凭证;2.2.0 不做 AOT 可以保留你的进程隔离,我把取凭证改成走它
Services/Json 源生成 直接用 System.Text.Json 可以保留,需要给 2.2.0 的模型补类型映射
Android/ 独立工程 单工程多目标 net10.0-windows;net10.0-android 两者功能重叠,只能留一个;要留你的我就把 2.2.0 的 Android TFM 撤掉
version.json 1.3.3 → 2.2.0 跟随 fork 的版本线 可以只提功能、不动版本号

另外,如果你更想要一个完全不动你架构、只加功能的版本,我也可以重开一个:只搬 2.2.0 的自研功能(多账号切换、四种登录方式、六渠道推送含自定义 SMTP、日志页、深色模式、自定义设备号),其余全部保留你的实现。

你倾向哪个方向,我按那个来 —— 这个 PR 先挂着你慢慢看也行。

star620 commented Oct 15, 2026

Copy link
Copy Markdown
Owner

@akexin 谢谢这份大礼包,也谢谢你把取舍点讲得这么清楚 —— AppJson 里注册了 VaultMeta/GitHubSecretPayload、导致「只挑功能」编不过这点我核实了,确实如此,不是偏好问题。

我把 Android 这条线单独放到 android 分支上了(基于 #52 的独立 Android/ 工程),main 保持纯 Windows 产品线;#52 那次误合并我已经 revert 掉了。所以这个 PR 现在的形态和仓库方向不一致,我先关掉 —— 不代表否定里面的东西。

想请你按你自己提的那个方案,重开一个「只加功能、不动我架构」的 PR。要搬的功能:

功能 说明
多账号切换 账号列表 / 顶栏下拉切换 / 冷切换
四种登录方式 会话 Cookie 导入、剪贴板智能识别、账号导入导出 JSON、读取本机 TRAE 客户端
六渠道推送 通用 Webhook、系统通知、钉钉、企业微信、Telegram Bot、自定义 SMTP 邮箱
日志页 最近 / 全部日志,可选自动滚动
自定义设备号 16 位设备号 + 账号与本地安全检查

请务必保留上游这些实现(不要被取代):

  • TraeTools.LoginHost 独立进程(NativeAOT 的前置条件,我不想退回去)
  • Services/Json 源生成(TraeTools.csproj 的 PublishTrimmed=full / EnableAot 都依赖它无反射)
  • cb573f2 的 AOT 目录版 zip 整包更新器与整套文件原子替换
  • Services/Switch/* 与 switch 系列修复(6b654a9 / 93cef5f / 6d2e8b2)
  • version.json 不要跟到 2.2.0

两点额外说明:

  1. 深色模式:上游这边刚做完一套主题系统(Styles/AppTheme.axaml 的 Light/Dark ThemeDictionaries + Services/ThemeService.cs + 配置持久化 ThemeMode),请直接基于它做,别再引入第二套配色表。
  2. Android 不用带:Android 走的是 android 分支上的独立工程方案,单工程多目标那部分不用搬过来。

另外,2.2.0 的账号/凭证模型既然是新的,AppJson 里对应的类型映射需要你在新 PR 里补上(否则源生成上下文会报「类型未注册」)。需要我配合的地方随时说。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants