fix: bot-review robustness fixes (TTS sync, updater, nightly, a11y) (#4659)

Cherry-picked and re-verified the applicable subset of
julianshen/readest@fa1b74a0 (its "address PR #15 bot reviews" commit). The
fork-only AI-annotation-tool change was dropped — readest has no 'ai' toolbar
tool. Each logic fix is covered by a failing-first test.

- TTS position sequence is now an app-wide monotonic counter, so a fresh
  TTSController (constructed per `tts-speak`) isn't dropped by consumers holding
  `lastSequenceSeen` from a prior session.
- share.ts only swallows AbortError (user cancel); other failures — e.g.
  NotAllowedError when a quick action fires without a user gesture — fall back to
  the clipboard so the text still reaches the user.
- document.isTxt tolerates MIME params (text/plain;charset=utf-8), uppercase
  extensions (BOOK.TXT), and a nameless Blob, so a TXT can't slip onto the
  non-text path and yield a null book.
- updater getNightlyPlatformKey matches x86_64/aarch64 explicitly; a 32-bit or
  otherwise unknown arch yields no nightly instead of mis-routing to aarch64.
- UpdaterWindow downloadWithProgress resolves on tauriDownload completion even
  when Content-Length is absent (no more hang on portable/AppImage/Android).
- nightly_update.rs uses async tokio::fs::read in the async command.
- nightly.yml: serialize runs via a concurrency group (no cancel) and
  persist-credentials:false on checkouts.
- edge TTS route only emits the word-boundary header when it fits under ~8KB;
  oversized values get dropped by proxies, and the client falls back to [].
- RSVPOverlay drops the contradictory aria-disabled on the functional rate
  button (it opens the pace picker).
- nightly verify harness handles artifact stream errors instead of crashing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-19 16:04:34 +08:00
committed by GitHub
parent e327d0c992
commit 86f5502724
14 changed files with 169 additions and 59 deletions
@@ -186,40 +186,33 @@ export const UpdaterContent = ({
} as GenericUpdate);
}
};
const downloadWithProgress = (
const downloadWithProgress = async (
downloadUrl: string,
filePath: string,
onEvent?: (progress: DownloadEvent) => void,
): Promise<void> => {
return new Promise<void>(async (resolve, reject) => {
let downloaded = 0;
let total = 0;
await tauriDownload(downloadUrl, filePath, (progress) => {
if (!onEvent) return;
if (!total && progress.total) {
total = progress.total;
onEvent({
event: 'Started',
data: { contentLength: total },
});
} else if (downloaded > 0 && progress.progress === progress.total) {
console.log('File downloaded to', filePath);
onEvent?.({ event: 'Finished' });
setTimeout(() => {
resolve();
}, 1000);
}
onEvent({
event: 'Progress',
data: { chunkLength: progress.progress - downloaded },
});
downloaded = progress.progress;
}).catch((error) => {
console.error('Download failed:', error);
reject(error);
});
let downloaded = 0;
let total = 0;
let finished = false;
// Resolve when tauriDownload itself completes — NOT only when a progress
// tick reports progress === total. Servers that omit Content-Length leave
// total at 0, so that tick never fires and the await would hang forever
// after the file is fully written (nightly portable/AppImage/Android).
await tauriDownload(downloadUrl, filePath, (progress) => {
if (!onEvent) return;
if (!total && progress.total) {
total = progress.total;
onEvent({ event: 'Started', data: { contentLength: total } });
}
onEvent({ event: 'Progress', data: { chunkLength: progress.progress - downloaded } });
downloaded = progress.progress;
if (progress.total && progress.progress === progress.total && !finished) {
finished = true;
onEvent({ event: 'Finished' });
}
});
console.log('File downloaded to', filePath);
if (onEvent && !finished) onEvent({ event: 'Finished' });
};
const checkWindowsPortableUpdate = async () => {
if (!appService) return;