feat: share built-in browser across runtimes
The built-in browser was limited to direct-model tools, and its Agent switch also blocked manual Browser workbar actions. It is now a request-scoped GoodBuddy capability assignable to direct models, managed OpenCode, and Continue, while manual browser controls remain available when Agent access is off. Existing browser enablement migrates to capability settings version 6. DeepSeek Harness remains unsupported. Release note: 内置浏览器现在可分配给直连模型、OpenCode 和 Continue;关闭 Agent 浏览器能力不会影响用户在浏览器工作栏中的手动操作。
This commit is contained in:
@@ -206,6 +206,14 @@ describe('CapabilityService', () => {
|
||||
await expect(
|
||||
service.getComputerCapabilityStatus('host-browser-control')
|
||||
).resolves.toEqual({ enabled: true, supported: true })
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
builtinMcpServers: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'builtin-browser',
|
||||
enabled: true
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('enables direct-model web search by default and persists its switch', async () => {
|
||||
@@ -256,6 +264,11 @@ describe('CapabilityService', () => {
|
||||
id: 'goodbuddy-config',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
},
|
||||
{
|
||||
id: 'builtin-browser',
|
||||
enabled: false,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -264,6 +277,10 @@ describe('CapabilityService', () => {
|
||||
await service.setBuiltinMcpServerAssignments('knowledge-base', [
|
||||
'model'
|
||||
])
|
||||
await service.setBuiltinMcpServerEnabled('builtin-browser', true)
|
||||
await service.setBuiltinMcpServerAssignments('builtin-browser', [
|
||||
'continue'
|
||||
])
|
||||
expect(() =>
|
||||
service.setBuiltinMcpServerAssignments('knowledge-base', [
|
||||
'deepseek-harness'
|
||||
@@ -276,6 +293,12 @@ describe('CapabilityService', () => {
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('opencode')
|
||||
).resolves.toEqual(['goodbuddy-config'])
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('continue')
|
||||
).resolves.toEqual([
|
||||
'goodbuddy-config',
|
||||
'builtin-browser'
|
||||
])
|
||||
await expect(
|
||||
service.getEnabledBuiltinMcpServerIds('deepseek-harness')
|
||||
).resolves.toEqual([])
|
||||
@@ -295,6 +318,11 @@ describe('CapabilityService', () => {
|
||||
expect.objectContaining({
|
||||
id: 'magic-notes',
|
||||
enabled: false
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'builtin-browser',
|
||||
enabled: true,
|
||||
assignments: ['continue']
|
||||
})
|
||||
])
|
||||
})
|
||||
@@ -915,7 +943,7 @@ describe('CapabilityService', () => {
|
||||
}
|
||||
})
|
||||
const persisted = await readFile(filePath, 'utf8')
|
||||
expect(persisted).toContain('"version": 5')
|
||||
expect(persisted).toContain('"version": 6')
|
||||
expect(persisted).toContain(credential)
|
||||
expect(persisted).not.toContain('preserved-secret')
|
||||
})
|
||||
@@ -951,7 +979,7 @@ describe('CapabilityService', () => {
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
webSearch: { enabled: true }
|
||||
})
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 5')
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 6')
|
||||
})
|
||||
|
||||
it('migrates v3 MCP servers with dynamic tools disabled', async () => {
|
||||
@@ -1001,7 +1029,7 @@ describe('CapabilityService', () => {
|
||||
]
|
||||
})
|
||||
const persisted = await readFile(filePath, 'utf8')
|
||||
expect(persisted).toContain('"version": 5')
|
||||
expect(persisted).toContain('"version": 6')
|
||||
expect(persisted).toContain('"allowDynamicTools": false')
|
||||
})
|
||||
|
||||
@@ -1043,7 +1071,62 @@ describe('CapabilityService', () => {
|
||||
})
|
||||
])
|
||||
})
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 5')
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 6')
|
||||
})
|
||||
|
||||
it('migrates the legacy browser switch into the built-in browser assignment', async () => {
|
||||
const { filePath, builtinRoot, importedRoot } = await createService()
|
||||
await writeFile(
|
||||
filePath,
|
||||
JSON.stringify({
|
||||
version: 5,
|
||||
skills: {},
|
||||
builtinMcpServers: {
|
||||
'knowledge-base': {
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
},
|
||||
'magic-notes': {
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
},
|
||||
'goodbuddy-config': {
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
},
|
||||
mcpServers: [],
|
||||
webSearch: { enabled: true },
|
||||
computerCapabilities: {
|
||||
'host-browser-control': {
|
||||
enabled: true,
|
||||
browserProfileId: null
|
||||
},
|
||||
'linux-desktop-control': {
|
||||
enabled: false,
|
||||
browserProfileId: null
|
||||
}
|
||||
}
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
const service = new CapabilityService(
|
||||
filePath,
|
||||
builtinRoot,
|
||||
importedRoot,
|
||||
cipher
|
||||
)
|
||||
|
||||
await expect(service.getSnapshot()).resolves.toMatchObject({
|
||||
builtinMcpServers: expect.arrayContaining([
|
||||
{
|
||||
id: 'builtin-browser',
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
])
|
||||
})
|
||||
expect(await readFile(filePath, 'utf8')).toContain('"version": 6')
|
||||
})
|
||||
|
||||
it('preserves capabilities created by a newer unsupported version', async () => {
|
||||
|
||||
@@ -122,7 +122,7 @@ const builtinMcpServerStateSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const builtinMcpServerStatesSchema = z
|
||||
const legacyBuiltinMcpServerStatesSchema = z
|
||||
.object({
|
||||
'knowledge-base': builtinMcpServerStateSchema,
|
||||
'magic-notes': builtinMcpServerStateSchema,
|
||||
@@ -130,6 +130,11 @@ const builtinMcpServerStatesSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const builtinMcpServerStatesSchema =
|
||||
legacyBuiltinMcpServerStatesSchema.extend({
|
||||
'builtin-browser': builtinMcpServerStateSchema
|
||||
})
|
||||
|
||||
const encryptedSecretSchema =
|
||||
encryptedSettingsCredentialSchema.optional()
|
||||
|
||||
@@ -222,8 +227,13 @@ const storedCapabilitiesV4Schema = storedCapabilitiesV3Schema.extend({
|
||||
version: z.literal(4)
|
||||
})
|
||||
|
||||
const storedCapabilitiesSchema = storedCapabilitiesV4Schema.extend({
|
||||
const storedCapabilitiesV5Schema = storedCapabilitiesV4Schema.extend({
|
||||
version: z.literal(5),
|
||||
builtinMcpServers: legacyBuiltinMcpServerStatesSchema
|
||||
})
|
||||
|
||||
const storedCapabilitiesSchema = storedCapabilitiesV5Schema.extend({
|
||||
version: z.literal(6),
|
||||
builtinMcpServers: builtinMcpServerStatesSchema
|
||||
})
|
||||
|
||||
@@ -284,7 +294,9 @@ function defaultComputerCapabilityStates(): StoredCapabilities['computerCapabili
|
||||
}
|
||||
}
|
||||
|
||||
function defaultBuiltinMcpServerStates(): StoredCapabilities['builtinMcpServers'] {
|
||||
function defaultBuiltinMcpServerStates(
|
||||
browserEnabled = false
|
||||
): StoredCapabilities['builtinMcpServers'] {
|
||||
const defaultState = (): z.infer<typeof builtinMcpServerStateSchema> => ({
|
||||
enabled: true,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
@@ -292,7 +304,11 @@ function defaultBuiltinMcpServerStates(): StoredCapabilities['builtinMcpServers'
|
||||
return {
|
||||
'knowledge-base': defaultState(),
|
||||
'magic-notes': defaultState(),
|
||||
'goodbuddy-config': defaultState()
|
||||
'goodbuddy-config': defaultState(),
|
||||
'builtin-browser': {
|
||||
enabled: browserEnabled,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +316,7 @@ function emptyStoredCapabilities(
|
||||
webSearchEnabled = true
|
||||
): StoredCapabilities {
|
||||
return {
|
||||
version: 5,
|
||||
version: 6,
|
||||
skills: {},
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
mcpServers: [],
|
||||
@@ -775,7 +791,7 @@ export class CapabilityService {
|
||||
let shouldPersist = false
|
||||
try {
|
||||
const raw = JSON.parse(await readFile(this.filePath, 'utf8')) as unknown
|
||||
assertSupportedSettingsVersion(raw, 5, (version) =>
|
||||
assertSupportedSettingsVersion(raw, 6, (version) =>
|
||||
`当前 GoodBuddy 不支持能力设置版本 ${version},请升级应用后重试`
|
||||
)
|
||||
const version = z
|
||||
@@ -785,7 +801,8 @@ export class CapabilityService {
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
z.literal(4),
|
||||
z.literal(5)
|
||||
z.literal(5),
|
||||
z.literal(6)
|
||||
])
|
||||
})
|
||||
.passthrough()
|
||||
@@ -794,7 +811,7 @@ export class CapabilityService {
|
||||
const legacy: StoredCapabilitiesV1 =
|
||||
storedCapabilitiesV1Schema.parse(raw)
|
||||
loaded = {
|
||||
version: 5,
|
||||
version: 6,
|
||||
skills: legacy.skills,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
mcpServers: legacy.mcpServers,
|
||||
@@ -806,8 +823,10 @@ export class CapabilityService {
|
||||
const legacy = storedCapabilitiesV2Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(),
|
||||
version: 6,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(
|
||||
legacy.computerCapabilities['host-browser-control'].enabled
|
||||
),
|
||||
webSearch: { enabled: true }
|
||||
}
|
||||
shouldPersist = true
|
||||
@@ -815,16 +834,35 @@ export class CapabilityService {
|
||||
const legacy = storedCapabilitiesV3Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates()
|
||||
version: 6,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(
|
||||
legacy.computerCapabilities['host-browser-control'].enabled
|
||||
)
|
||||
}
|
||||
shouldPersist = true
|
||||
} else if (version === 4) {
|
||||
const legacy = storedCapabilitiesV4Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 5,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates()
|
||||
version: 6,
|
||||
builtinMcpServers: defaultBuiltinMcpServerStates(
|
||||
legacy.computerCapabilities['host-browser-control'].enabled
|
||||
)
|
||||
}
|
||||
shouldPersist = true
|
||||
} else if (version === 5) {
|
||||
const legacy = storedCapabilitiesV5Schema.parse(raw)
|
||||
loaded = {
|
||||
...legacy,
|
||||
version: 6,
|
||||
builtinMcpServers: {
|
||||
...legacy.builtinMcpServers,
|
||||
'builtin-browser': {
|
||||
enabled:
|
||||
legacy.computerCapabilities['host-browser-control'].enabled,
|
||||
assignments: ['model', 'opencode', 'continue']
|
||||
}
|
||||
}
|
||||
}
|
||||
shouldPersist = true
|
||||
} else {
|
||||
@@ -970,7 +1008,10 @@ export class CapabilityService {
|
||||
id: capability.id,
|
||||
name: capability.name,
|
||||
description: capability.description,
|
||||
enabled: state.computerCapabilities[capability.id].enabled,
|
||||
enabled:
|
||||
capability.id === 'host-browser-control'
|
||||
? state.builtinMcpServers['builtin-browser'].enabled
|
||||
: state.computerCapabilities[capability.id].enabled,
|
||||
supported: isComputerCapabilitySupported(
|
||||
capability,
|
||||
this.platform,
|
||||
@@ -1027,7 +1068,10 @@ export class CapabilityService {
|
||||
const capability = getComputerCapability(id)
|
||||
const state = await this.load()
|
||||
return {
|
||||
enabled: state.computerCapabilities[id].enabled,
|
||||
enabled:
|
||||
id === 'host-browser-control'
|
||||
? state.builtinMcpServers['builtin-browser'].enabled
|
||||
: state.computerCapabilities[id].enabled,
|
||||
supported: isComputerCapabilitySupported(
|
||||
capability,
|
||||
this.platform,
|
||||
@@ -1077,6 +1121,17 @@ export class CapabilityService {
|
||||
const state = await this.load()
|
||||
await this.persistUserChange({
|
||||
...state,
|
||||
...(id === 'host-browser-control'
|
||||
? {
|
||||
builtinMcpServers: {
|
||||
...state.builtinMcpServers,
|
||||
'builtin-browser': {
|
||||
...state.builtinMcpServers['builtin-browser'],
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
computerCapabilities: {
|
||||
...state.computerCapabilities,
|
||||
[id]: {
|
||||
@@ -1197,7 +1252,9 @@ export class CapabilityService {
|
||||
const state = await this.load()
|
||||
return this.diagnoseComputerCapabilityState(
|
||||
id,
|
||||
state.computerCapabilities[id].enabled
|
||||
id === 'host-browser-control'
|
||||
? state.builtinMcpServers['builtin-browser'].enabled
|
||||
: state.computerCapabilities[id].enabled
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1652,8 +1709,39 @@ export class CapabilityService {
|
||||
return this.queue(async () => {
|
||||
const id = builtinMcpServerIdSchema.parse(serverId)
|
||||
const state = await this.load()
|
||||
if (id === 'builtin-browser' && update.enabled === true) {
|
||||
const capability = getComputerCapability('host-browser-control')
|
||||
if (
|
||||
!isComputerCapabilitySupported(
|
||||
capability,
|
||||
this.platform,
|
||||
this.architecture,
|
||||
this.availableComputerCapabilityImplementations
|
||||
)
|
||||
) {
|
||||
throw new Error('当前操作系统或处理器架构不支持此能力')
|
||||
}
|
||||
const report = await this.diagnoseComputerCapabilityState(
|
||||
'host-browser-control',
|
||||
true
|
||||
)
|
||||
if (report.status === 'unavailable') {
|
||||
throw new Error('能力诊断不可用,未启用此能力')
|
||||
}
|
||||
}
|
||||
await this.persistUserChange({
|
||||
...state,
|
||||
...(id === 'builtin-browser' && update.enabled !== undefined
|
||||
? {
|
||||
computerCapabilities: {
|
||||
...state.computerCapabilities,
|
||||
'host-browser-control': {
|
||||
...state.computerCapabilities['host-browser-control'],
|
||||
enabled: update.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
builtinMcpServers: {
|
||||
...state.builtinMcpServers,
|
||||
[id]: {
|
||||
@@ -1915,7 +2003,18 @@ export class CapabilityService {
|
||||
const state = await this.load()
|
||||
return builtinMcpServerIdSchema.options.filter((id) => {
|
||||
const server = state.builtinMcpServers[id]
|
||||
return server.enabled && server.assignments.includes(runtime)
|
||||
if (!server.enabled || !server.assignments.includes(runtime)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
id !== 'builtin-browser' ||
|
||||
isComputerCapabilitySupported(
|
||||
getComputerCapability('host-browser-control'),
|
||||
this.platform,
|
||||
this.architecture,
|
||||
this.availableComputerCapabilityImplementations
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const computerCapabilityCatalog: readonly ComputerCapabilityCatalogEntry[
|
||||
})
|
||||
]),
|
||||
riskSummary:
|
||||
'总开关关闭时不会向直连模型提供浏览器工具;开启后可在 Execute 模式直接读取网页并操作网站,不再逐次询问。',
|
||||
'总开关关闭时不会向任何 Runtime 提供浏览器工具,但不影响你在浏览器工作栏中手动操作;开启后,已分配的 Runtime 可在 Execute 模式直接读取网页并操作网站,不再逐次询问。',
|
||||
requiredDiagnostics: Object.freeze([
|
||||
'browser-executable',
|
||||
'managed-profile-root'
|
||||
|
||||
Reference in New Issue
Block a user