Repository navigation
Conversation
C11。现状:save() 直接 writeFile(overwrite = true),接口里明明有 lastModified() 但保存路径没用 —— 编辑器开着、你在服务器上改了同一文件、回来点保存,服务器侧 改动被静默覆盖。 做法(审计建议的「提示不阻断」): - CodeEditorViewModel 打开文件时记下 mtime(loadedModifiedAt,取不到则 0)。 保存前比对:mtime 变新 = 被外部改过 → 发 SaveResult.Conflict(新增的密封分支) 并且**不写盘**;UI 弹 AlertDialog 二次确认,确认后带 force = true 重来。 取消则留在编辑器里继续改(pendingExit 一并清掉,与保存失败分支一致)。 - 自己写完会刷新基准(重新读一次 mtime),否则下一次保存必然把自己的写入误判成外部改动。 - 换文件时清空基准,避免拿上一个文件的时间戳判新文件。 误报可能性(按你的要求给判断): - 真实误报来源只有一类:内容没变但 mtime 前进了(git checkout、touch、rsync 扫描、 某些编辑器保存时重写)。此时会多弹一次对话框,用户点「仍要保存」即可 —— 不丢数据, 代价是一次多余交互。所以只提示不硬拦。 - 漏报(检测不到外部改动):SFTP v3 的 mtime 只有秒级精度,若外部改动与「本机保存」 落在同一秒内,或远端 FS 时间戳不可靠(取不到 → 基准 0 → 直接跳过冲突检测), 则不提示。宁可漏报也不误报卡人。 - 时钟偏移不影响:比较的是远端文件自己的 mtime 前后值,不掺本地时间。 行为变化:保存多一次 statExistence(本地一次 stat / 远程一次 SFTP 往返); 仅在检测到冲突时多一个确认框;其余路径不变。新增 3 条字符串(中英各一, 键集合 1477/1477)。
C10。现状:SshShellBackend.waitForExit() 恒返回 0(远程 shell 没有有意义的退出码),
网断/服务器重启后 Termux 只会打一句「[Process completed]」,与用户敲 exit 长得一样,
用户完全看不出连接已经没了。
选了「加标记 + 标签横幅」,不动 waitForExit 的返回值语义:
- 返回值是 SessionBackend 接口约定,本地终端(SubprocessBackend)共用,
改语义会连带影响本地终端的退出码显示。
- SshShellBackend 增加 closedByDisconnect 标记 + 可选的 isConnectionAlive 回调
(默认 { true },既有单参构造调用不受影响)。waitForExit 里:join 抛异常,或 join
正常返回但此时连接已不在,就置位。时序是安全的:Termux 的 TermSessionWaiter 线程先
拿到 waitForExit 的返回值,再经主线程 handler 回调 onSessionFinished,标记一定先于
回调写入(@volatile 保证可见性)。
- RemoteTerminalSessionManager 记录 标签 id → backend(关闭标签时清理),
onSessionFinished 时若 backend.closedByDisconnect 则把 TerminalTab.droppedByDisconnect
置位并记日志。
- TerminalScreen 在当前标签 droppedByDisconnect 时于标签栏下方挂一条 errorContainer
横幅(新增两条字符串,中英各一,键集合仍一致 1479/1479)。
已知局限(不能靠这条兜住所有断线):断网瞬间 sshj 的 transport 未必立刻判定失败
(isConnected 可能仍为 true,要等 keepalive/下一次写失败),此时 shell 结束会被当成
正常退出、没有横幅。连接级的断线由聊天页的连接指示器与「重试连接」按钮覆盖。
行为变化:纯粹新增提示与日志,无既有判定改动。本地终端模式完全不受影响
(isConnectionAlive 默认 true,droppedByDisconnect 恒 false)。
用户用的是 Stellar(包名 roro.stellar.manager)→ 界面显示「未安装」、点了跳下载页,
AI 也用不了 Shizuku。真因在状态机第一关:
computeState(): if (!isShizukuInstalled()) return NOT_INSTALLED
isShizukuInstalled(): getPackageInfo("moe.shizuku.privileged.api", 0)
而授权判定本身走 binder(Shizuku.checkSelfPermission),与包名无关;库层也不校验
管理器包名(api-13.1.5.aar 里该字符串零命中,ShizukuProvider.handleSendBinder 只判
pingBinder 后直接 onBinderReceived)—— binder 其实早就被 Stellar 推进来了,只是
代码从没走到 pingBinder()。Sui 是 Magisk 模块、没有独立管理器包,同样被这一关判死。
A1:computeState() 改成以 binder 为准(pingBinder → isPreV11 → checkSelfPermission),
全程 runCatching,异常降为 NOT_RUNNING;删掉 isShizukuInstalled();_state 初值
改 NOT_RUNNING。NOT_INSTALLED 常量保留(ShizukuTool / AppPermissionsSection 的
when 是表达式,删了跨文件编不过),只是不再产出。
A2:openShizukuApp() 改为 官方 → Stellar → 按 provider authority 精确匹配扫描
(${pkg}.shizuku / ${pkg}.stellar,排除自身)→ 兜底下载页。
B2:新增只读 peerInfo()(uid/version/SELinux,区分 adb 2000 与 root 0),UI 半边待接。
两条信任锚未动:provider 的 INTERACT_ACROSS_USERS_FULL(只有 shell/root 能推 binder)
与 READY 只由 checkSelfPermission 决定 —— 放宽包名检测不降低授权强度。
明确不做:客户端主动拉 binder(会让抢注 authority 的应用拿到命令原文)、
QUERY_ALL_PACKAGES、引入 Stellar 原生 API。
为什么:状态判定改成以 binder 为准之后(管理器包名不在官方契约内,按包名判「装没装」 必然误判),NOT_RUNNING 既表示「服务没启动」也表示「设备上根本没有管理器」,但文案还写着 「点击打开 Shizuku 并启动服务」——没装 Shizuku/Stellar 的设备上,那个 App 根本不存在, 用户只能照着做一件做不到的事(点击按钮其实会跳下载页,文案没提)。 改动: 1. 状态文案与状态初值自洽:删掉不再产出的 ShizukuState.NOT_INSTALLED 及其两条死文案 (中英各一条),NOT_RUNNING 的状态改成「未就绪」、副标题改成 「点击打开 Shizuku 并启动服务;未安装会跳转下载页」(复用原有 key,只改内容), 工具侧的 stateHint 同步(原来也分成「未安装 / 服务未运行」两句,现在一句讲清两种情况)。 2. 顺手接上零调用的 ShizukuManager.peerInfo():设置 → 软件权限区在就绪时多一行「当前连接」, 写明当前是以 adb shell(uid 2000)还是 root(uid 0)身份连上的——两者能做的事不一样 (root 才读得到其它应用的私有目录),只写「已就绪」看不出这点。新增四组文案 (标题 + adb/root/其它 uid),中英键集一致(1481 / 1481)。 身份只能从服务端读,所以走 state 派生 + flowOn(IO),未就绪时不问 binder。 行为变化: - 未装管理器的设备:状态文字由「服务未运行」变为「未就绪」,提示里多了「未安装会跳转下载页」; - 就绪时设置页多出一行「当前连接」(原来没有这一行); - ShizukuState 少一个常量,枚举的 when 全套改为三档;Shizuku 工具未就绪的报错文案同步改口径。
远程模式下「当前项目」技能组一直空着,真因不是来源写死本地(列/读/写/删/导入 本来就经 DelegatingFileAccess 按 ExecutionModeHolder 转发),而是缺一个刷新触发点: 建连与工作区加载是两条异步链,connectionState=CONNECTED 时工作区可能还没落定, 此时 currentPath() 回退到「工作区根目录」而非某个工作区(WorkspaceRepository.kt:501-503), ProjectDirectorySkillSource 扫的路径少一层 <工作区名>,必然空列表;工作区随后落定 或用户切换工作区,都不再触发重扫,页面就停在空/旧内容上。 补上 workspaceRepository.current 的收集器(与既有 executionMode / connectionState 两处同层),工作区一变就 refreshSkills() + refreshSubAgents()。顺带修掉本地模式下 「切工作区后项目技能不更新」的同类陈旧问题。 技能来源根路径、仓库读写、UI 结构、文案分组均未改动 —— 它们本来就正确。
android.util.Base64 的 DEFAULT 解码是宽容的:非法字符按 SKIP 忽略、不抛异常,旧版明文凭据因此也能被 「成功解码」成乱码、被误判成本编码格式,凭据全部读空。改用往返校验判定格式:只有 encode(decode(raw)) == raw 才认定为编码格式,否则按明文处理(旧版明文仍需回退解析)。
为什么只摘这一小块:fork 上的 c6b62ba 是混合体(跨 19 文件、改 4 个 repository 签名、与上游 5 处冲突),整条不能提。但其中「RemoteSftpFileAccess.copyToLocal 把断线/通道异常也包成 NoSuchFileException」这一条是本地已提交的附件改动 (打开附件区分「文件不存在」与「这次取不到」)的前置:不先修数据侧的异常语义, 新增的 LocalFileResolution.Unavailable 分支在上游基线上永远走不到——远程路径上 任何失败都被说成「文件不存在」,用户会去删/重建工作区而不是重连。 改动逐行(只有两处,都在异常语义上,无行为以外的牵连): 1. RemoteSftpFileAccess.kt copyToLocal 的 catch 块末行 `throw NoSuchFileException(File(remote))` → `throw IOException(friendlySshError(e), e)` - 原有的 `if (e is NoSuchFileException) throw e` 保留:真「远端没有这个文件」 (statExistence 为 null)仍抛 NoSuchFileException,语义不变。 - 其余失败(断线、SFTP 通道异常、SSH 未连接)改为抛 IOException,文案取 friendlySshError——该方法与本文件的 withSftp 已经在用(同一文件既有用法), 没有引入新依赖,也没有新增 import。 - 注释两行说明「为什么不能一律报不存在」,与上游既有注释风格一致。 2. FileAccessProvider.kt copyToLocal 的 KDoc 补「异常契约」五行。 - 接口里 readFile/writeFile/rename/copy/move 都写了会抛什么,唯独 copyToLocal 没写,而调用方正是靠这段区分两类失败——不写清契约,调用侧的 `catch (NoSuchFileException) → Missing / catch (Exception) → Unavailable` 就是无据可依的假设。 - 文案按上游基线的真实行为写(本实现不检查存在性;远程实现 NoSuchFileException 与其它 IOException 分开),并删掉了 fork 版本里对 WorkspaceNotReadyException 的引用——那个类只存在于 fork,上游没有。只改注释,无行为变化。 明确不带:c6b62ba1 的 repository 签名改造、SettingsViewModel/SettingsScreen 的项目级 写入提示、ManageMcpTool 的 ToolResult.Error、FtpSyncClient/SftpSyncClient 的英文文案 中文化、friendlySshError 的中文透传分支,以及 fork 私有的一切内容。
打开已发送附件时 resolveLocalFile 用 runCatching 吞掉异常、只回 File?,调用方一律提示「文件不存在或已被移动」——远程 SSH 断线、SFTP 通道异常、工作区尚未落定都被说成文件没了,用户会去删/重建工作区而不是重连。 - 新增 private sealed interface LocalFileResolution:Found / Missing / Unavailable - 本地模式 copyToLocal 对不存在的路径不抛异常(返回 isFile=false 的 File),仍归 Missing - 远程模式 NoSuchFileException → Missing;断线/通道异常/工作区未落定(IOException、WorkspaceNotReadyException)→ Unavailable - 行为变化:Unavailable 走新文案 chat_open_file_unavailable(中英同键集,values 与 values-en 各 1482 条) - 行为修复:原来连 CancellationException 一起吞,协程取消也会弹一次提示;现在取消照常向上传播 - Unavailable 记一条 FileLogger.w,日志里能看到真实失败原因 - 触发的场景不变:localPath 有效仍直用,containerPath 为空仍算 Missing
生图 OpenAI 分支直接裸调 openAIApi.createImage:429/5xx/超时既不退避也不重发,一次失败就返回 IMAGE_GEN_FAILED(多 Key 下也只把当前 Key 打进冷却、本次调用照样失败)。文本路径早就统一走 retryStaircase(maxRetries + onKeyFailure 切 Key 重发),生图路径当时创建时就漏了。 - 用同一套 retryStaircase 包住 createImage;onKeyFailure 复用 ProviderKeyRotator.reportFailure(providerId, sessionId, key, triedKeys),判定与文本路径一致(isKeySwitchFailure + 供应商自定义触发码),候选用尽抛 AllKeysFailedException - 重试次数与文本路径有区别:走用户设置「重试次数」,但再封顶 MAX_IMAGE_GEN_RETRIES=2(最多 3 次尝试)。理由:文本重试几乎无代价,生图几十秒一张且按张计费,超时/连接中断时服务端可能已生成并计费(Images API 无幂等键),重发就是赌重复扣费;429/5xx 这类服务端明确未受理的失败仍值得退避重试。用户设 0 照传 0 - Key 切换不受该上限约束(与文本路径一致:预算按候选 Key 数,切完立即重发) - 失败出口的补报改为带 triedKeys:不再把本次已试过的 Key 又选回来;自定义触发码把 5xx 也算作 Key 失败时(重试块内不走 onKeyFailure)仍保留原补报行为 - 行为变化:可恢复失败现在会重发成功并返回图片;不可恢复(400 等)仍一次都不重试
stopAgentSession 在 cancel() 之后、若当时没有新 job 接管,会 agentNotificationCenter.clear(sessionId)。但同一处的注释自己写着「不预先清除待送通知—— 它们应由 finally 正常 flush 给新 job 处理」:cancel() 的 finally 未必同步跑完, 在那个窗口里 clear 会把待送通知(用户插话 / 后台任务完成 / 子代理结束)直接抹掉, finally 的 flushPendingNotifications 再 drain 就只剩空列表,用户那边就是「消息没了」。 去掉这处 clear,交回 finally 的既有兜底路径送达(作为消息发出去); 状态清理(置 Idle)保持不变。切工作区的 stopAllAgents 与回退清队列仍按原样清空—— 那两处是整段重置/回退,不是「停止任务」。
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 59 minutes. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (22)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
单点修复批:Shizuku / 终端 / 编辑器 / 凭据 / 附件 / 生图 / 通知(10 条)
概述
这批是彼此独立的单点修复的集合:10 个提交、22 个文件(Kotlin 18 个、
strings.xml2 个、文档 2 篇),+482 / -77。每条各自对应一个明确的用户可见缺陷,彼此没有依赖关系(可单独 review、可单独回退)。
其中两条带前置依赖关系,放在批内一起提交:
RemoteSftpFileAccess.copyToLocal先不再把断线异常也说成
NoSuchFileException,否则新的「这次取不到」分支在远程路径上永远走不到。这条前置只取了异常语义这一小块(见清单第 7 条),不是 fork 上那个跨 19 文件的混合提交。
NOT_RUNNING的语义对不上文案;同时它把第 3 条带进来的零调用
peerInfo()接上了线(消除「新增公开 API 没有调用方」)。全部 10 个提交都能从
upstream/main干净摘取(无冲突),并已通过 CI 编译验证(见文末)。修复清单
按提交顺序,每条给出:症状 → 机理 → 改法,并附原始 commit subject。
1. 保存前检测「文件已被外部改动」,二次确认后再覆盖
save()直接writeFile(overwrite = true);接口里明明有lastModified(),保存路径没用。CodeEditorViewModel打开文件时记下 mtime(取不到记 0),保存前比对:mtime 变新 → 发新增的SaveResult.Conflict分支且不写盘,UI 弹AlertDialog二次确认,确认后带force = true重来;取消则留在编辑器继续改。自己写完会重新读一次 mtime 刷新基准(否则下次保存必然把自己的写入误判成外部改动);
换文件时清空基准。只提示不硬拦(SFTP v3 的 mtime 只有秒级精度,硬拦会误报)。
feat(editor): 保存前检测「文件已被外部改动」,二次确认后再覆盖2. 断线导致的会话结束打出「连接已断开」横幅
[Process completed],与用户敲exit长得一模一样,看不出连接已经没了。SshShellBackend.waitForExit()恒返回 0(远程 shell 没有有意义的退出码),断开与正常退出在 UI 上不可区分。waitForExit的返回值语义(它是SessionBackend接口约定,本地终端SubprocessBackend共用),改为
SshShellBackend增加closedByDisconnect标记 + 可选isConnectionAlive回调(默认{ true },既有单参构造不受影响);RemoteTerminalSessionManager记录标签 id → backend,onSessionFinished时置TerminalTab.droppedByDisconnect;TerminalScreen在该标签下显示errorContainer横幅。已知局限:断网瞬间 sshj 的 transport 未必立刻判定失败,此时仍会被当成正常退出、没有横幅;连接级的断线由聊天页的连接指示器覆盖。
feat(terminal): 断线导致的会话结束打出「连接已断开」横幅,不再伪装成正常退出3. Shizuku 不再用官方包名判「装没装」
roro.stellar.manager)→ 设置页显示「未安装」、点了跳下载页,AI 也用不了 Shizuku。computeState()第一关是isShizukuInstalled(),它按官方包名moe.shizuku.privileged.api查getPackageInfo。而授权判定本身走 binder(
checkSelfPermission),与包名无关;库层也不校验管理器包名(
provider:13.1.5的 aar 里该字符串零命中,ShizukuProvider.handleSendBinder只判pingBinder后直接onBinderReceived)——binder 其实早就被 Stellar 推进来了,只是代码从没走到
pingBinder()。Sui 是 Magisk 模块、没有独立管理器包,同样被这关判死。computeState()改成以 binder 为准(pingBinder → isPreV11 → checkSelfPermission),全程runCatching,异常降为
NOT_RUNNING;删掉isShizukuInstalled();_state初值改NOT_RUNNING。openShizukuApp()改为 官方 → Stellar → 按 provider authority 精确匹配扫描(${pkg}.shizuku/${pkg}.stellar,排除自身)→ 兜底下载页。新增只读
peerInfo()(uid / version / SELinux,区分 adb uid 2000 与 root uid 0)。两条信任锚未动:provider 的
INTERACT_ACROSS_USERS_FULL(只有 shell / root 能推 binder)与READY 只由
checkSelfPermission决定——放宽包名检测不降低授权强度。fix(shizuku): 不再用官方包名判「装没装」,支持 Stellar / Sui 等分支4. Shizuku 未装设备的文案不再让人去开不存在的应用,并显示当前连接身份
NOT_RUNNING既表示「服务没启动」也表示「设备上根本没有管理器」,文案却还写着「点击打开 Shizuku 并启动服务」——没装的设备上那个 App 根本不存在(点击实际会跳下载页,文案没提)。
另外第 3 条带进来的
peerInfo()没有任何调用方。ShizukuState.NOT_INSTALLED不再被产出,ShizukuTool/AppPermissionsSection里两条「未安装」文案成为死代码。NOT_INSTALLED常量及其两条死文案(中英各一条),NOT_RUNNING状态文案改「未就绪」、副标题改「点击打开 Shizuku 并启动服务;未安装会跳转下载页」(复用原 key,只改内容),工具侧
stateHint同步。接线
peerInfo():设置 → 软件权限区在就绪时多一行「当前连接」,写明当前是以 adb shell(uid 2000)还是 root(uid 0)连上的(root 才读得到其它应用的私有目录,只写「已就绪」看不出这点)。新增四组文案,中英键集一致。
枚举的
when全套改为三档(编译期强制穷举)。fix(settings): Shizuku 未装设备的文案不再让人去开不存在的应用,并显示当前连接身份5. 技能 / 子代理列表在工作区落定 / 切换后重扫
DelegatingFileAccess按ExecutionModeHolder转发),而是缺一个刷新触发点:建连与工作区加载是两条异步链,
connectionState = CONNECTED时工作区可能还没落定,此时
currentPath()回退到「工作区根目录」而非某个工作区,ProjectDirectorySkillSource扫的路径少一层<工作区名>,必然空列表;工作区随后落定或用户切换工作区都不再触发重扫,页面就停在空/旧内容上。
workspaceRepository.current的收集器(与既有executionMode/connectionState两处同层),工作区一变就
refreshSkills()+refreshSubAgents();顺带修掉本地模式下「切工作区后项目技能不更新」的同类陈旧问题。技能来源根路径、仓库读写、UI 结构、文案分组均未改动。
fix(skills): 工作区落定/切换后重扫技能与子代理列表6. git 凭据不再被误判格式而读空
android.util.Base64的DEFAULT解码是「宽容」的——:、/、@等非法字符一律按 SKIP 忽略、不抛异常,于是旧版明文凭据也能被「解码」成乱码,代码据「未抛异常」判定为编码格式 → 明文凭据全被读空。
encode(decode(raw)) == raw判定格式,不成立就按明文处理(保留旧版明文回退解析)。fix(credentials): 修复 git 凭据编码格式误判导致凭据丢失7.
copyToLocal异常语义:只对真「不存在」抛NoSuchFileException而文件明明在服务器上——用户会去删 / 重建工作区,而不是重连。
RemoteSftpFileAccess.copyToLocal的收尾catch (e: Exception)把一切异常都包成NoSuchFileException(File(remote)),调用方根本无法区分「文件没了」与「这次取不到」。RemoteSftpFileAccess.kt:249-255的 catch 块末行throw NoSuchFileException(File(remote))→throw IOException(friendlySshError(e), e)。原有的if (e is NoSuchFileException) throw e保留:真「远端没有这个文件」(
statExistence == null)仍抛NoSuchFileException,语义不变;其余失败(断线、通道异常、SSH 未连接)改为
IOException,文案取friendlySshError——该方法与本文件的
withSftp已经在用,没有新增 import、没有引入新依赖。FileAccessProvider.kt:110的copyToLocalKDoc 补五行「异常契约」。接口里readFile/writeFile/rename/copy/move都写了会抛什么,唯独copyToLocal没写,而调用方正是靠这段区分两类失败——不写清契约,调用侧的
catch (NoSuchFileException) → Missing / catch (Exception) → Unavailable就是无据可依的假设。文案按本基线的真实行为写(本地实现不检查存在性;远程实现
NoSuchFileException与其它IOException分开),只改注释,无行为变化。
其余(repository 返回
Boolean、项目级写入提示、ManageMcpTool错误、FTP/SFTP 文案中文化)一律不带。手工适配:copyToLocal 异常语义(摘自上 fork 的 c6b62ba1,仅此一语义)8. 打开附件区分「文件不存在」与「这次取不到」
resolveLocalFile用runCatching吞掉异常、只回File?,调用方一律按「文件不存在」处理;runCatching连CancellationException一起吞,取消协程也会触发一次提示。private sealed interface LocalFileResolution { Found / Missing / Unavailable }:本地模式
copyToLocal对不存在的路径不抛异常(返回isFile = false的File),仍归Missing;远程模式
NoSuchFileException → Missing,其余异常 →Unavailable。CancellationException显式重抛照常传播。Unavailable走新文案chat_open_file_unavailable(中英同键集),并记一条FileLogger.w让日志能看到真实失败原因。触发场景不变:
localPath有效仍直用,containerPath为空仍算Missing。fix(agent): 打开附件区分「文件不存在」与「这次取不到」,断线不再误报文件不存在9. 生图 429 不再既不退避也不重发
IMAGE_GEN_FAILED,多 Key 下也只把当前 Key 打进冷却,本次调用照样失败。GenerateImageTool的 OpenAI 分支直接裸调createImage,没走文本路径早就统一的retryStaircase(
maxRetries+onKeyFailure切 Key 重发)——生图路径创建时就漏了。retryStaircase包住createImage,onKeyFailure复用ProviderKeyRotator.reportFailure(providerId, sessionId, key, triedKeys),判定与文本路径一致,候选用尽抛AllKeysFailedException。重试次数走用户设置的「重试次数」,但再封顶
MAX_IMAGE_GEN_RETRIES = 2(最多 3 次尝试):文本重试几乎无代价,生图几十秒一张且按张计费、Images API 无幂等键,超时重发就是赌重复扣费;
429 / 5xx 这类服务端明确未受理的失败仍值得退避重试。Key 切换不受该上限约束(与文本路径一致)。
失败出口的补报改为带
triedKeys,不再把已试过的 Key 又选回来。fix(agent): 生图 createImage 接进 retryStaircase,429 不再既不退避也不重发10. 点「停止」不再清空待送通知,消息不再凭空消失
stopAgentSession在cancel()之后、若当时没有新 job 接管,会agentNotificationCenter.clear(sessionId);而同处注释自己写着「不预先清除待送通知——它们应由
finally正常 flush 给新 job 处理」。cancel()的finally未必同步跑完,在那个窗口里clear会把待送通知直接抹掉,finally再 drain 就只剩空列表。clear,交回finally的既有兜底路径送达(作为消息发出去);状态清理(置 Idle)保持不变。切工作区的
stopAllAgents与回退清队列仍按原样清空——那两处是整段重置 / 回退,不是「停止任务」。fix(agent): 点停止不再清空待送通知,消息不再凭空消失为什么对上游有价值
风险 / 边界(请 reviewer 重点看)
NOT_INSTALLED(本批把它连常量一起删了,所有
when分支同步改为三档,编译期穷举保证不漏);初始状态从NOT_INSTALLED改为NOT_RUNNING。未装管理器的设备现在看到的是「未就绪」。两条信任锚未动(provider 的
INTERACT_ACROSS_USERS_FULL、READY 只由
checkSelfPermission决定),但「按包名判装没装」这条判定被彻底移除,属于安全边界的变化,建议 reviewer 明确认可。明确不做:客户端主动拉 binder(会让抢注 authority 的应用拿到命令原文)、
QUERY_ALL_PACKAGES、引入 Stellar 原生 API。getInstalledPackages(GET_PROVIDERS)不用QUERY_ALL_PACKAGES也能看到所有包,是因为本仓库
targetSdk = 28(app/build.gradle.kts:114,低于 30),不触发包可见性过滤。若将来把 targetSdk 提到 30+,这段 authority 扫描会静默只看到部分包(官方 / Stellar 两个硬编码包名不受影响,
因为它们在
KNOWN_MANAGER_PACKAGES里先命中),届时需要加<queries>或接受「只认已知包名」。peerInfo()在就绪时才问 binder:走state.map { peerInfo() }.flowOn(Dispatchers.IO),ShizukuManager.peerInfo()内部先判state == READY再调用;未就绪时返回 null,不触发权限申请。异常一律降级为 null 并记日志。取不到时间戳时(基准 0)直接跳过冲突检测。代价是每次保存多一次 stat。
理由是 Images API 无幂等键、超时重发可能重复扣费。这是产品取舍,值得 reviewer 确认。
这版只在「join 抛异常」或「join 返回时连接已不在」时置位,不追求全覆盖。
copyToLocal的异常类型变化(重要):非「不存在」的失败路径上,抛出的异常从NoSuchFileException变为
IOException(文案来自friendlySshError)。仓库内调用点已逐个核查:StatefulAgentWorkflow.kt:1143、ChatImageLoader.kt:35、SendFileTool.kt:122、MarkdownImageSupport.kt:78、ImageTools.kt:351、GenerateImageTool.kt:563、ChatAttachmentUtils.kt:214、BrowserManager.kt:850都没有按
NoSuchFileException分支处理(只在意「拿到 / 没拿到」),行为不变;全仓唯一 catchNoSuchFileException的调用点就是本批同改的MessageAttachmentComponents.kt:324。若上游还有未合并的
copyToLocal调用方按NoSuchFileException判定,需要一并复核。FileAccessProvider的 KDoc 只是注释:不改变任何实现。契约文案已按本基线的真实行为书写(未引用基线不存在的类型)。
FileCredentialRepository的往返校验:Base64.encodeToString+reversed()的编码格式保持不变,只把「是否编码格式」的判定从「解码未抛异常」换成「往返一致」。旧版明文凭据现在能正确回退解析。
本批明确不含
pr/nav-backstack(导航返回栈)与pr/ui-scroll-input(键盘 / 滚动 / 草稿 / 渲染)两批。Boolean(SkillConfigRepository/AgentDefinitionConfigRepository/McpConfigRepository/PermissionRulesRepository)、项目级写入被跳过时的 Toast 提示、ManageMcpTool的ToolResult.Error、FtpSyncClient/SftpSyncClient的英文异常文案中文化、friendlySshError的「已是中文则透传」分支,均未纳入——它们跨 4 个 repository 的签名与多处用户文案,超出本批「单点修复」的范围,需要单独一批。
(
retryStaircase、reportFailure(…, triedKeys)在上游已有)。applicationId差异、CI 触发条件差异、FORK_PRIVATE清单、备份 / 记忆相关改动,一律不在本批。
验证情况
upstream/main(6c615e38)。values/与values-en/的strings.xml条目数一致(1482 = 1482,键集完全相同、无重复键);改动文件里新增
import的项目内符号(ShizukuPeerInfo、AppTextField、retryStaircase、IOException、friendlySshError等)全部可在仓库内找到定义;改动到的符号没有任何测试文件引用;Shizuku 相关改动依赖的
dev.rikka.shizuku:api:13.1.5/provider:13.1.5版本与本基线一致(app/build.gradle.kts:398-399,未改动)。beta.yml手动workflow_dispatch(分支pr/single-fixes),跑:app:assembleUniversalBeta(JDK 17 + R8 + 资源压缩)。
completed / success,attempt 1。Build universal beta APK步骤success,整个 job 08:27:26Z → 08:34:35Z(约 7 分 9 秒)。aicode-beta-ed330766995d812810a93d7f98f5425dbc8bf30d,25,402,701 字节(说明不止编译过了,R8 与资源处理也跑完了)。ci.yml的单测门禁只在 push 到main/fork/release时触发,本轮只能跑 beta 编译;assembleUniversalBeta不编译app/src/test。没有真机复现(容器内没有 Android 设备 / JDK)。