feat: support macOS arm64 remote Agent hosts
Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation. Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release. Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded. Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
This commit is contained in:
@@ -109,7 +109,7 @@ export function PlatformFeaturesSettingsSection({
|
||||
const [agentInventoryError, setAgentInventoryError] =
|
||||
useState<string>()
|
||||
const [agentPackageBusy, setAgentPackageBusy] =
|
||||
useState<AgentArchitecture | 'import'>()
|
||||
useState<string>()
|
||||
const [agentPackageProgress, setAgentPackageProgress] =
|
||||
useState<AgentPackageDownloadProgress>()
|
||||
const agentInventoryRequested = useRef(false)
|
||||
@@ -195,23 +195,24 @@ export function PlatformFeaturesSettingsSection({
|
||||
}, [])
|
||||
|
||||
const downloadAgentPackage = async (
|
||||
architecture: AgentArchitecture
|
||||
architecture: AgentArchitecture,
|
||||
platform: 'linux' | 'darwin'
|
||||
): Promise<void> => {
|
||||
const download =
|
||||
window.goodbuddy.sshHosts?.downloadAgentPackage
|
||||
if (!download) {
|
||||
return
|
||||
}
|
||||
setAgentPackageBusy(architecture)
|
||||
setAgentPackageBusy(`${platform}-${architecture}`)
|
||||
setAgentPackageProgress(undefined)
|
||||
setAgentInventoryError(undefined)
|
||||
try {
|
||||
setAgentInventory(await download(architecture))
|
||||
setAgentInventory(await download(architecture, platform))
|
||||
onNotify?.({
|
||||
dedupeKey: `agent-package-downloaded:${architecture}`,
|
||||
dedupeKey: `agent-package-downloaded:${platform}-${architecture}`,
|
||||
message: t(
|
||||
'platformFeatures.remoteProjects.agentInventory.notifications.downloaded',
|
||||
{ architecture }
|
||||
{ architecture, platform: platform === 'darwin' ? 'macOS' : 'Linux' }
|
||||
),
|
||||
tone: 'success'
|
||||
})
|
||||
@@ -265,22 +266,23 @@ export function PlatformFeaturesSettingsSection({
|
||||
}
|
||||
|
||||
const exportAgentPackage = async (
|
||||
architecture: AgentArchitecture
|
||||
architecture: AgentArchitecture,
|
||||
platform: 'linux' | 'darwin'
|
||||
): Promise<void> => {
|
||||
const exportPackage =
|
||||
window.goodbuddy.sshHosts?.exportAgentPackage
|
||||
if (!exportPackage) {
|
||||
return
|
||||
}
|
||||
setAgentPackageBusy(architecture)
|
||||
setAgentPackageBusy(`${platform}-${architecture}`)
|
||||
setAgentInventoryError(undefined)
|
||||
try {
|
||||
await exportPackage(architecture)
|
||||
await exportPackage(architecture, platform)
|
||||
onNotify?.({
|
||||
dedupeKey: `agent-package-exported:${architecture}`,
|
||||
dedupeKey: `agent-package-exported:${platform}-${architecture}`,
|
||||
message: t(
|
||||
'platformFeatures.remoteProjects.agentInventory.notifications.exported',
|
||||
{ architecture }
|
||||
{ architecture, platform: platform === 'darwin' ? 'macOS' : 'Linux' }
|
||||
),
|
||||
tone: 'success'
|
||||
})
|
||||
@@ -983,13 +985,13 @@ export function PlatformFeaturesSettingsSection({
|
||||
{agentInventory.entries.map((entry) => (
|
||||
<article
|
||||
className="bundled-agent-inventory__item"
|
||||
key={entry.architecture}
|
||||
key={`${entry.platform}-${entry.architecture}`}
|
||||
role="listitem"
|
||||
>
|
||||
<div className="bundled-agent-inventory__item-header">
|
||||
<Cpu aria-hidden="true" size={16} />
|
||||
<strong>
|
||||
Linux {entry.architecture}
|
||||
{entry.platform === 'darwin' ? 'macOS' : 'Linux'} {entry.architecture}
|
||||
</strong>
|
||||
<span
|
||||
className={`bundled-agent-inventory__badge bundled-agent-inventory__badge--${
|
||||
@@ -1063,7 +1065,8 @@ export function PlatformFeaturesSettingsSection({
|
||||
</dl>
|
||||
{agentPackageProgress?.architecture ===
|
||||
entry.architecture &&
|
||||
agentPackageBusy === entry.architecture && (
|
||||
agentPackageProgress.platform === entry.platform &&
|
||||
agentPackageBusy === `${entry.platform}-${entry.architecture}` && (
|
||||
<p className="settings-notice" role="status">
|
||||
{t(
|
||||
`platformFeatures.remoteProjects.agentInventory.progress.${agentPackageProgress.phase}`
|
||||
@@ -1084,7 +1087,8 @@ export function PlatformFeaturesSettingsSection({
|
||||
disabled={agentPackageBusy !== undefined}
|
||||
onClick={() =>
|
||||
void downloadAgentPackage(
|
||||
entry.architecture
|
||||
entry.architecture,
|
||||
entry.platform
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
@@ -1107,7 +1111,8 @@ export function PlatformFeaturesSettingsSection({
|
||||
disabled={agentPackageBusy !== undefined}
|
||||
onClick={() =>
|
||||
void exportAgentPackage(
|
||||
entry.architecture
|
||||
entry.architecture,
|
||||
entry.platform
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
|
||||
@@ -2149,7 +2149,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
await screen.findByText('GoodBuddy Agent 包')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('当前 1 / 2 个 Linux 架构可用。')
|
||||
screen.getByText('当前 1 / 2 个平台与架构目标可用。')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('Linux x64')).toBeInTheDocument()
|
||||
expect(screen.getByText('Linux arm64')).toBeInTheDocument()
|
||||
@@ -2192,7 +2192,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(downloadAgentPackage).toHaveBeenCalledWith('x64')
|
||||
expect(downloadAgentPackage).toHaveBeenCalledWith('x64', 'linux')
|
||||
)
|
||||
expect(
|
||||
await within(x64Card as HTMLElement).findByText('已是最新')
|
||||
@@ -2208,7 +2208,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(exportAgentPackage).toHaveBeenCalledWith('x64')
|
||||
expect(exportAgentPackage).toHaveBeenCalledWith('x64', 'linux')
|
||||
)
|
||||
fireEvent.click(
|
||||
within(arm64Card as HTMLElement).getByRole('button', {
|
||||
@@ -2216,7 +2216,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(downloadAgentPackage).toHaveBeenCalledWith('arm64')
|
||||
expect(downloadAgentPackage).toHaveBeenCalledWith('arm64', 'linux')
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '导入离线包' })
|
||||
@@ -2272,7 +2272,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(
|
||||
await screen.findByText('当前 1 / 2 个 Linux 架构可用。')
|
||||
await screen.findByText('当前 1 / 2 个平台与架构目标可用。')
|
||||
).toBeInTheDocument()
|
||||
|
||||
getAgentPackageInventory.mockRejectedValueOnce(
|
||||
|
||||
@@ -585,7 +585,7 @@ export const settingsSections = {
|
||||
downloadVersion: 'Download {{version}}',
|
||||
listLabel: 'Local Agent package inventory',
|
||||
summary:
|
||||
'{{available}} of {{total}} Linux architectures are available.',
|
||||
'{{available}} of {{total}} platform and architecture targets are available.',
|
||||
states: {
|
||||
verified: 'Downloaded and verified',
|
||||
'not-downloaded': 'Not downloaded',
|
||||
@@ -613,9 +613,9 @@ export const settingsSections = {
|
||||
installing: 'Writing the local cache…'
|
||||
},
|
||||
notifications: {
|
||||
downloaded: 'Linux {{architecture}} Agent package updated',
|
||||
downloaded: '{{platform}} {{architecture}} Agent package updated',
|
||||
imported: 'Agent offline package imported and verified',
|
||||
exported: 'Linux {{architecture}} Agent package exported'
|
||||
exported: '{{platform}} {{architecture}} Agent package exported'
|
||||
},
|
||||
errors: {
|
||||
unavailable:
|
||||
|
||||
@@ -554,7 +554,7 @@ export const settingsSections = {
|
||||
updateTo: '更新到 {{version}}',
|
||||
downloadVersion: '下载 {{version}}',
|
||||
listLabel: '本地 Agent 包清单',
|
||||
summary: '当前 {{available}} / {{total}} 个 Linux 架构可用。',
|
||||
summary: '当前 {{available}} / {{total}} 个平台与架构目标可用。',
|
||||
states: {
|
||||
verified: '已下载并验证',
|
||||
'not-downloaded': '未下载',
|
||||
@@ -582,9 +582,9 @@ export const settingsSections = {
|
||||
installing: '正在写入本地缓存…'
|
||||
},
|
||||
notifications: {
|
||||
downloaded: 'Linux {{architecture}} Agent 包已更新',
|
||||
downloaded: '{{platform}} {{architecture}} Agent 包已更新',
|
||||
imported: 'Agent 离线包已导入并验证',
|
||||
exported: 'Linux {{architecture}} Agent 包已导出'
|
||||
exported: '{{platform}} {{architecture}} Agent 包已导出'
|
||||
},
|
||||
errors: {
|
||||
unavailable: '当前版本未提供 Agent 包管理服务',
|
||||
|
||||
Reference in New Issue
Block a user