From 7003141e561ffd1204919ea48215d46b47e7e11e Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sun, 6 Sep 2026 19:25:40 +0800 Subject: [PATCH] fix: restore GitHub Agent update checks with paginated discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GitHub release history exceeded the catalog response limit, preventing users from checking Agent updates. Read ten releases per page and stop at the first eligible catalog while preserving the existing 100-release search window and signature checks. Release note: 修复选择 GitHub 来源时 Agent 更新检查提示发布目录超出大小限制的问题;镜像更新和已有本地包保持不变。 --- docs/features/remote-host/technical-design.md | 5 +++ .../agent-package-manager.test.ts | 42 +++++++++++++++++++ .../remote-agent/agent-package-manager.ts | 30 +++++++++---- tests/agent-package.test.ts | 10 ++--- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/docs/features/remote-host/technical-design.md b/docs/features/remote-host/technical-design.md index 1b4705b5..a398135e 100644 --- a/docs/features/remote-host/technical-design.md +++ b/docs/features/remote-host/technical-design.md @@ -104,6 +104,11 @@ Detached GoodBuddy Agent 小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、 协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线 包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。 +- GitHub 来源按每页 10 条读取 Release 元数据,最多检查原有的 100 条历史发布, + 找到首个有效 Agent 目录及签名地址后立即停止。每次响应仍保留 1 MiB 上限, + 避免一次读取全部历史正文和资产详情导致目录检查失败;签名、URL 与包校验不变。 + 2026-09-06 使用当前 Main 目录读取实现验证公开 GitHub 与北京镜像,两者均可读取 + Agent `0.11.19` 的 Linux x64、Linux arm64 和 macOS arm64 条目,未下载或安装包。 - “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确 可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或 adoption 失败后都不跨 acquisition 自动 fallback。 diff --git a/src/main/remote-agent/agent-package-manager.test.ts b/src/main/remote-agent/agent-package-manager.test.ts index 6671c845..86debc98 100644 --- a/src/main/remote-agent/agent-package-manager.test.ts +++ b/src/main/remote-agent/agent-package-manager.test.ts @@ -257,6 +257,48 @@ function response( } describe('AgentPackageManager remote install candidates', () => { + it('pages GitHub release metadata and stops at the first signed catalog', async () => { + const fixture = await createFixture('github') + const pages: number[] = [] + const desktopReleases = Array.from({ length: 10 }, () => ({ + tag_name: 'v9.0.0', + body: 'release notes'.repeat(1500), + assets: [] + })) + expect(JSON.stringify(Array(10).fill(desktopReleases)).length) + .toBeGreaterThan(1024 * 1024) + const transport = vi.fn(async (input) => { + const url = new URL(input.toString()) + if (url.hostname === 'api.github.com') { + expect(url.searchParams.get('per_page')).toBe('10') + const page = Number(url.searchParams.get('page')) + pages.push(page) + return response(JSON.stringify( + page === 1 ? desktopReleases : fixture.releases + )) + } + return response(url.pathname.endsWith('.sig') + ? fixture.signature : fixture.catalogBytes) + }) + + const snapshot = await fixture.manager(transport).getSnapshot() + expect(snapshot.catalog.state).toBe('available') + expect(snapshot.entries[0]?.latestVersion).toBe('2.0.0') + expect(pages).toEqual([1, 2]) + }) + + it.each([0, 10])('bounds GitHub discovery when each page contains %i releases', async (count) => { + const fixture = await createFixture('github') + const transport = vi.fn(async () => + response(JSON.stringify(Array.from({ length: count }, () => ({ + tag_name: 'v9.0.0' + })))) + ) + const snapshot = await fixture.manager(transport).getSnapshot() + expect(snapshot.catalog.state).toBe('unavailable') + expect(transport).toHaveBeenCalledTimes(count === 0 ? 1 : 10) + }) + it('selects Linux and Darwin arm64 independently from one catalog', async () => { await createFixture('mirror', (catalog) => { for (const platform of ['linux', 'darwin'] as const) { diff --git a/src/main/remote-agent/agent-package-manager.ts b/src/main/remote-agent/agent-package-manager.ts index d9933062..b6ec256f 100644 --- a/src/main/remote-agent/agent-package-manager.ts +++ b/src/main/remote-agent/agent-package-manager.ts @@ -61,7 +61,7 @@ const CATALOG_SIGNATURE_DOMAIN = Buffer.from( const MIRROR_ROOT = 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/' const GITHUB_RELEASES_API = - 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=100' + 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=10' const GITHUB_RELEASE_ROOT = 'https://github.com/mesalogo/goodbuddy/releases/download/' const MAXIMUM_CATALOG_BYTES = 1024 * 1024 @@ -1449,17 +1449,29 @@ export class AgentPackageManager { ]) } + async *#githubReleases(signal?: AbortSignal): AsyncGenerator { + // Keep the existing 100-release search window without downloading all + // historical release bodies and asset metadata in one response. + for (let page = 1; page <= 10; page += 1) { + signal?.throwIfAborted() + const releases = await this.#fetchJson( + `${GITHUB_RELEASES_API}&page=${page}`, + signal + ) + if (!Array.isArray(releases)) { + throw new Error('GitHub Agent 发布目录无效') + } + yield* releases.slice(0, 10) + if (releases.length < 10) { + return + } + } + } + async #fetchGithubCatalog( signal?: AbortSignal ): Promise<[Buffer, Buffer]> { - const releases = await this.#fetchJson( - GITHUB_RELEASES_API, - signal - ) - if (!Array.isArray(releases)) { - throw new Error('GitHub Agent 发布目录无效') - } - for (const untrusted of releases.slice(0, 100)) { + for await (const untrusted of this.#githubReleases(signal)) { if ( typeof untrusted !== 'object' || untrusted === null || diff --git a/tests/agent-package.test.ts b/tests/agent-package.test.ts index 9dfe724b..c93d9ed7 100644 --- a/tests/agent-package.test.ts +++ b/tests/agent-package.test.ts @@ -1652,7 +1652,7 @@ describe('compound Agent packages', () => { const transport = vi.fn(async (input: string | URL) => { const url = input.toString() requested.push(url) - if (url.includes('/releases?per_page=100')) { + if (url.includes('/releases?per_page=10&page=1')) { return jsonResponse([{ tag_name: `agent-v${agentLock.agentVersion}`, draft: false, @@ -1713,7 +1713,7 @@ describe('compound Agent packages', () => { }) expect(requested).toContain( source === 'github' - ? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=100' + ? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=10&page=1' : 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/latest.json' ) expect(requested).not.toContain( @@ -1736,7 +1736,7 @@ describe('compound Agent packages', () => { await manager.download('x64') expect(requested).toContain( source === 'github' - ? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=100' + ? 'https://api.github.com/repos/mesalogo/goodbuddy/releases?per_page=10&page=1' : 'https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/latest.json' ) expect(requested).toContain( @@ -1769,7 +1769,7 @@ describe('compound Agent packages', () => { let packageRequested = false const transport = vi.fn(async (input: string | URL) => { const url = input.toString() - if (url.includes('/releases?per_page=100')) { + if (url.includes('/releases?per_page=10&page=1')) { return jsonResponse([{ tag_name: `agent-v${agentLock.agentVersion}`, draft: false, @@ -1838,7 +1838,7 @@ describe('compound Agent packages', () => { let packageRequested = false const transport = vi.fn(async (input: string | URL) => { const url = input.toString() - if (url.includes('/releases?per_page=100')) { + if (url.includes('/releases?per_page=10&page=1')) { return jsonResponse([{ tag_name: `agent-v${agentLock.agentVersion}`, draft: false,