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,