fix: make Agent updates reliable and visible
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s

Valid Agent packages could be rejected when packaged trusted-key JSON used CRLF, and Settings could not show whether a newer compatible Agent existed. Registry formatting is now parsed independently of cryptographic trust, and Settings reads only the signed catalog to report local and latest versions.

New installations use a credential-free local Ollama-compatible profile. The v20 migration changes only untouched historical defaults, while explicit profiles and legacy environment aliases remain compatible. Magic Notes count badges no longer shift navigation rows.

Release note: 修复 Agent 下载兼容性并在设置中显示更新状态;新安装默认连接本地模型,保留显式用户配置;魔法笔记待办数字不再改变侧栏菜单高度。
This commit is contained in:
mesalogo
2026-08-26 15:43:41 +08:00
parent f9137f0463
commit de64e9768a
33 changed files with 884 additions and 188 deletions
+9
View File
@@ -6148,12 +6148,19 @@ describe('App', () => {
})),
getAgentPackageInventory: vi.fn(async () => ({
checkedAt: '2026-08-24T00:00:00.000Z',
catalog: {
state: 'available' as const,
checkedAt: '2026-08-24T00:00:00.000Z',
error: null
},
entries: [
{
platform: 'linux' as const,
architecture: 'x64' as const,
state: 'verified' as const,
version: '0.11.2',
latestVersion: '0.11.4',
updateAvailable: true,
agentProtocol: { major: 2, minor: 0 },
remoteRuntimeVersion: '1.18.9'
},
@@ -6162,6 +6169,8 @@ describe('App', () => {
architecture: 'arm64' as const,
state: 'not-downloaded' as const,
version: null,
latestVersion: '0.11.4',
updateAvailable: false,
agentProtocol: null,
remoteRuntimeVersion: null
}
@@ -930,10 +930,26 @@ export function PlatformFeaturesSettingsSection({
Linux {entry.architecture}
</strong>
<span
className={`bundled-agent-inventory__badge bundled-agent-inventory__badge--${entry.state}`}
className={`bundled-agent-inventory__badge bundled-agent-inventory__badge--${
entry.updateAvailable
? 'update-available'
: agentInventory.catalog.state ===
'available' &&
entry.state === 'verified' &&
entry.latestVersion !== null
? 'up-to-date'
: entry.state
}`}
>
{t(
`platformFeatures.remoteProjects.agentInventory.states.${entry.state}`
entry.updateAvailable
? 'platformFeatures.remoteProjects.agentInventory.states.updateAvailable'
: agentInventory.catalog.state ===
'available' &&
entry.state === 'verified' &&
entry.latestVersion !== null
? 'platformFeatures.remoteProjects.agentInventory.states.upToDate'
: `platformFeatures.remoteProjects.agentInventory.states.${entry.state}`
)}
</span>
</div>
@@ -946,6 +962,14 @@ export function PlatformFeaturesSettingsSection({
</dt>
<dd>{entry.version ?? '—'}</dd>
</div>
<div>
<dt>
{t(
'platformFeatures.remoteProjects.agentInventory.fields.latestVersion'
)}
</dt>
<dd>{entry.latestVersion ?? '—'}</dd>
</div>
<div>
<dt>
{t(
@@ -985,23 +1009,36 @@ export function PlatformFeaturesSettingsSection({
</p>
)}
<div className="capability-card__actions">
<button
className="secondary-button"
disabled={agentPackageBusy !== undefined}
onClick={() =>
void downloadAgentPackage(
entry.architecture
)
}
type="button"
>
<Download aria-hidden="true" size={13} />
{t(
entry.state === 'verified'
? 'platformFeatures.remoteProjects.agentInventory.update'
: 'platformFeatures.remoteProjects.agentInventory.download'
)}
</button>
{(
entry.updateAvailable ||
(
entry.state !== 'verified' &&
entry.latestVersion !== null
) ||
agentInventory.catalog.state !== 'available'
) && (
<button
className="secondary-button"
disabled={agentPackageBusy !== undefined}
onClick={() =>
void downloadAgentPackage(
entry.architecture
)
}
type="button"
>
<Download aria-hidden="true" size={13} />
{t(
entry.updateAvailable
? 'platformFeatures.remoteProjects.agentInventory.updateTo'
: entry.state !== 'verified' &&
entry.latestVersion !== null
? 'platformFeatures.remoteProjects.agentInventory.downloadVersion'
: 'platformFeatures.remoteProjects.agentInventory.update',
{ version: entry.latestVersion }
)}
</button>
)}
{entry.state === 'verified' && (
<button
className="secondary-button"
@@ -1024,6 +1061,21 @@ export function PlatformFeaturesSettingsSection({
))}
</div>
) : null}
{agentInventory?.catalog.state === 'available' && (
<p className="settings-notice" role="status">
{t(
'platformFeatures.remoteProjects.agentInventory.catalog.available'
)}
</p>
)}
{agentInventory?.catalog.state === 'unavailable' && (
<p className="settings-warning" role="alert">
{t(
'platformFeatures.remoteProjects.agentInventory.catalog.unavailable',
{ error: agentInventory.catalog.error }
)}
</p>
)}
</article>
</section>
+87 -7
View File
@@ -514,12 +514,19 @@ let applicationSettings: ApplicationSettings = {
}
const agentPackageInventory: AgentPackageInventory = {
checkedAt: '2026-08-24T00:00:00.000Z',
catalog: {
state: 'available',
checkedAt: '2026-08-24T00:00:00.000Z',
error: null
},
entries: [
{
platform: 'linux',
architecture: 'x64',
state: 'verified',
version: '0.11.2',
latestVersion: '0.11.4',
updateAvailable: true,
agentProtocol: { major: 2, minor: 0 },
remoteRuntimeVersion: '1.18.9'
},
@@ -528,6 +535,8 @@ const agentPackageInventory: AgentPackageInventory = {
architecture: 'arm64',
state: 'not-downloaded',
version: null,
latestVersion: '0.11.4',
updateAvailable: false,
agentProtocol: null,
remoteRuntimeVersion: null
}
@@ -1878,13 +1887,13 @@ describe('SettingsPanel runtime files', () => {
).toBeInTheDocument()
expect(screen.getByText('Linux x64')).toBeInTheDocument()
expect(screen.getByText('Linux arm64')).toBeInTheDocument()
expect(screen.getByText('已下载并验证')).toBeInTheDocument()
expect(screen.getByText('有更新')).toBeInTheDocument()
expect(screen.getByText('未下载')).toBeInTheDocument()
expect(getAgentPackageInventory).toHaveBeenCalledOnce()
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(1, false)
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
await waitFor(() =>
expect(getAgentPackageInventory).toHaveBeenCalledTimes(2)
@@ -1899,14 +1908,34 @@ describe('SettingsPanel runtime files', () => {
.closest('[role="listitem"]')
expect(x64Card).not.toBeNull()
expect(arm64Card).not.toBeNull()
downloadAgentPackage.mockResolvedValueOnce({
...agentPackageInventory,
entries: agentPackageInventory.entries.map((entry) =>
entry.architecture === 'x64'
? {
...entry,
version: '0.11.4',
updateAvailable: false
}
: entry
)
})
fireEvent.click(
within(x64Card as HTMLElement).getByRole('button', {
name: '检查并更新'
name: '更新到 0.11.4'
})
)
await waitFor(() =>
expect(downloadAgentPackage).toHaveBeenCalledWith('x64')
)
expect(
await within(x64Card as HTMLElement).findByText('已是最新')
).toBeInTheDocument()
expect(
within(x64Card as HTMLElement).queryByRole('button', {
name: /更新到/u
})
).not.toBeInTheDocument()
fireEvent.click(
within(x64Card as HTMLElement).getByRole('button', {
name: '导出离线包'
@@ -1917,7 +1946,7 @@ describe('SettingsPanel runtime files', () => {
)
fireEvent.click(
within(arm64Card as HTMLElement).getByRole('button', {
name: '下载'
name: '下载 0.11.4'
})
)
await waitFor(() =>
@@ -1967,7 +1996,9 @@ describe('SettingsPanel runtime files', () => {
})
)
expect(
await screen.findByText('正在校验本地 Agent 包…')
await screen.findByText(
'正在校验本地 Agent 包并检查在线版本…'
)
).toBeInTheDocument()
await act(async () => {
@@ -1982,14 +2013,14 @@ describe('SettingsPanel runtime files', () => {
new Error('inventory unavailable')
)
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
expect(
await screen.findByText('inventory unavailable')
).toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
await waitFor(() =>
expect(
@@ -1999,6 +2030,55 @@ describe('SettingsPanel runtime files', () => {
expect(getAgentPackageInventory).toHaveBeenCalledTimes(3)
})
it('keeps local Agent packages usable when the online catalog is unavailable', async () => {
getAgentPackageInventory.mockResolvedValueOnce({
...agentPackageInventory,
catalog: {
state: 'unavailable',
checkedAt: '2026-08-24T00:00:00.000Z',
error: 'network unavailable'
},
entries: agentPackageInventory.entries.map((entry) => ({
...entry,
latestVersion: null,
updateAvailable: false
}))
})
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
fireEvent.click(
await screen.findByRole('tab', {
name: '远程项目(技术预览)'
})
)
expect(
await screen.findByText(/network unavailable/u)
).toBeInTheDocument()
const x64Card = screen
.getByText('Linux x64')
.closest('[role="listitem"]')
expect(
within(x64Card as HTMLElement).getByRole('button', {
name: '检查并更新'
})
).toBeInTheDocument()
expect(
within(x64Card as HTMLElement).getByRole('button', {
name: '导出离线包'
})
).toBeInTheDocument()
})
it('shows the SSH Hosts category only while Remote Projects is enabled', () => {
const commonProps = {
...heartbeatSettingsProps,
@@ -520,27 +520,38 @@ export const settingsSections = {
agentInventory: {
title: 'GoodBuddy Agent packages',
description:
'Agent packages are released independently and include the Agent, pinned Node, and the remote OpenCode Runtime adapted by GoodBuddy. Online actions use the source selected under About & Updates; nothing is downloaded until remote projects are used.',
loading: 'Verifying local Agent packages…',
refresh: 'Refresh local inventory',
'Agent packages are released independently and include the Agent, pinned Node, and the remote OpenCode Runtime adapted by GoodBuddy. Opening this page reads only the small signed catalog to show available updates and never downloads an Agent package automatically. Online actions use the source selected under About & Updates.',
loading: 'Verifying local Agent packages and checking online versions…',
refresh: 'Refresh package inventory',
import: 'Import offline package',
export: 'Export offline package',
download: 'Download',
update: 'Check for updates',
updateTo: 'Update to {{version}}',
downloadVersion: 'Download {{version}}',
listLabel: 'Local Agent package inventory',
summary:
'{{available}} of {{total}} Linux architectures are available.',
states: {
verified: 'Downloaded and verified',
'not-downloaded': 'Not downloaded',
invalid: 'Verification failed'
invalid: 'Verification failed',
updateAvailable: 'Update available',
upToDate: 'Up to date'
},
fields: {
agentVersion: 'Agent version',
agentVersion: 'Local Agent',
latestVersion: 'Latest online version',
architecture: 'Architecture',
runtimeVersion: 'Remote OpenCode',
protocol: 'Agent protocol'
},
catalog: {
available:
'Online versions were checked through the signed catalog. Agent packages are transferred only after you choose Download or Update.',
unavailable:
'Could not check online Agent versions: {{error}}. Verified local packages and offline imports remain available. Refresh to try again.'
},
progress: {
catalog: 'Reading the signed release catalog…',
downloading: 'Downloading the Agent package…',
@@ -556,7 +567,7 @@ export const settingsSections = {
unavailable:
'Agent package management is not available in this version.',
readFailed:
'Could not load local Agent packages. Try again.',
'Could not load Agent packages. Try again.',
downloadFailed:
'Agent package download or verification failed',
importFailed:
@@ -491,26 +491,37 @@ export const settingsSections = {
agentInventory: {
title: 'GoodBuddy Agent 包',
description:
'Agent 包独立发布,包含 Agent、固定 Node 和桌面版本适配的远端 OpenCode Runtime。在线操作使用“关于与更新”中选择的更新源;未使用远程项目时不会下载。',
loading: '正在校验本地 Agent 包…',
refresh: '刷新本地清单',
'Agent 包独立发布,包含 Agent、固定 Node 和桌面版本适配的远端 OpenCode Runtime。打开此页只读取小型签名目录并显示可用更新,不会自动下载 Agent 包;在线操作使用“关于与更新”中选择的更新源。',
loading: '正在校验本地 Agent 包并检查在线版本…',
refresh: '刷新包清单',
import: '导入离线包',
export: '导出离线包',
download: '下载',
update: '检查并更新',
updateTo: '更新到 {{version}}',
downloadVersion: '下载 {{version}}',
listLabel: '本地 Agent 包清单',
summary: '当前 {{available}} / {{total}} 个 Linux 架构可用。',
states: {
verified: '已下载并验证',
'not-downloaded': '未下载',
invalid: '校验失败'
invalid: '校验失败',
updateAvailable: '有更新',
upToDate: '已是最新'
},
fields: {
agentVersion: 'Agent 版本',
agentVersion: '本地 Agent',
latestVersion: '在线最新版本',
architecture: '架构',
runtimeVersion: '远端 OpenCode',
protocol: 'Agent 协议'
},
catalog: {
available:
'在线版本已通过签名目录检查;只有点击下载或更新后才会传输 Agent 包。',
unavailable:
'未能检查在线 Agent 版本:{{error}}。本地已验证包和离线导入仍可使用,可刷新重试。'
},
progress: {
catalog: '正在读取签名发布目录…',
downloading: '正在下载 Agent 包…',
@@ -524,7 +535,7 @@ export const settingsSections = {
},
errors: {
unavailable: '当前版本未提供 Agent 包管理服务',
readFailed: '读取本地 Agent 包清单失败,请重试',
readFailed: '读取 Agent 包清单失败,请重试',
downloadFailed: 'Agent 包下载或校验失败',
importFailed: 'Agent 离线包导入或校验失败',
exportFailed: 'Agent 离线包导出失败'
+14 -3
View File
@@ -3084,14 +3084,15 @@ textarea:focus-visible {
.nav-item__badge {
display: grid;
min-width: 19px;
height: 19px;
padding: 0 5px;
min-width: 17px;
height: 17px;
padding: 0 4px;
border-radius: 999px;
background: var(--danger-solid);
color: var(--text-on-accent);
font-size: 10px;
font-weight: 700;
line-height: 1;
place-items: center;
}
@@ -6630,6 +6631,16 @@ button > svg {
color: var(--success);
}
.bundled-agent-inventory__badge--up-to-date {
background: var(--success-subtle);
color: var(--success);
}
.bundled-agent-inventory__badge--update-available {
background: var(--accent-subtle);
color: var(--accent);
}
.bundled-agent-inventory__badge--not-downloaded,
.bundled-agent-inventory__badge--invalid {
background: var(--warning-subtle);