Add an opt-in persistent frosted glass setting for the top bar and runtime checklist, with chat scrolling behind the header while preserving scroll anchors and window controls. Retry transient local MCP initialization failures once after cleanup, include redacted connection diagnostics in feedback, and index lifecycle events to reduce execution statistics scans. Release note: Add optional frosted glass appearance with chat visible behind the title bar; improve local tool connection recovery and diagnostic feedback.
418 lines
15 KiB
TypeScript
418 lines
15 KiB
TypeScript
import {
|
|
ModelAgentRuntime,
|
|
type ModelRuntimeOptions
|
|
} from './model-runtime'
|
|
import { ContinueAgentRuntime } from './continue-runtime'
|
|
import { OpenCodeRuntime, type OpenCodeRuntimeOptions } from './opencode-runtime'
|
|
import type { LocalRuntimeRegistry } from './local-runtime-registry'
|
|
import {
|
|
DeepSeekHarnessRuntime,
|
|
type DeepSeekHarnessRuntimeOptions
|
|
} from './deepseek-harness-runtime'
|
|
import type { AgentRuntime } from './runtime'
|
|
import { UnconfiguredAgentRuntime } from './unconfigured-runtime'
|
|
import type {
|
|
ResolvedModelProfile,
|
|
ResolvedRuntimeSettings
|
|
} from '../runtime-settings-store'
|
|
import {
|
|
defaultRuntimeSettings,
|
|
isDeepSeekHarnessModelProfile,
|
|
isAgentRuntimeModelProtocol
|
|
} from '../../shared/contracts'
|
|
import type {
|
|
ResolvedMcpServer,
|
|
RuntimeSkillPackage
|
|
} from '../capabilities/capability-service'
|
|
import type { BundledRuntimePaths } from './bundled-runtimes'
|
|
import {
|
|
ContinueHostAdapter,
|
|
type ContinueHostLauncher
|
|
} from './continue-host-adapter'
|
|
import type { BrowserToolService } from '../browser/browser-model-tools'
|
|
import type { ModelToolProviderLike } from './model-tool-provider'
|
|
import type { SubagentScheduler } from '../assistant/subagent-scheduler'
|
|
import type { KnowledgeMcpGateway } from './knowledge-mcp-gateway'
|
|
import { ModelToolProvider } from './model-tool-provider'
|
|
import type { ControlledHarnessExtensionPackage } from './deepseek-harness-extension-loader'
|
|
import {
|
|
LocalWorkspaceAccess,
|
|
type WorkspaceAccess
|
|
} from '../workspace'
|
|
import {
|
|
REMOTE_EXECUTION_SPACE_UNAVAILABLE,
|
|
type ExecutionSpaceDescriptor
|
|
} from '../execution-space'
|
|
import type { LaunchEnvironmentProvider } from '../local-tool-environment/launch-environment-provider'
|
|
import type { DesktopDiagnosticFailureObserver } from '../desktop-diagnostics'
|
|
|
|
const noModelTaskTools: ModelToolProviderLike = {
|
|
listTools: async () => [],
|
|
getApproval: () => {
|
|
throw new Error('此模型任务不允许工具调用')
|
|
},
|
|
callTool: async () => {
|
|
throw new Error('此模型任务不允许工具调用')
|
|
},
|
|
releaseConversation: async () => undefined,
|
|
dispose: async () => undefined
|
|
}
|
|
|
|
export type AgentCapabilityContext = {
|
|
observeFailure?: DesktopDiagnosticFailureObserver
|
|
localRuntimeRegistry?: LocalRuntimeRegistry
|
|
skillInstructions?: string
|
|
skillPackages?: RuntimeSkillPackage[]
|
|
mcpServers?: ResolvedMcpServer[]
|
|
continueHostCacheRoot?: string
|
|
opencodeSharedCacheRoot?: string
|
|
bundledRuntimePaths?: BundledRuntimePaths
|
|
continueHostLauncher?: ContinueHostLauncher
|
|
deepseekHarnessLauncher?: DeepSeekHarnessRuntimeOptions['launch']
|
|
deepseekHarnessExtensions?: ControlledHarnessExtensionPackage[]
|
|
browserService?: BrowserToolService
|
|
knowledgeGateway?: KnowledgeMcpGateway
|
|
webSearchEnabled?: boolean
|
|
executionSpace?: ExecutionSpaceDescriptor
|
|
workspaceAccess?: WorkspaceAccess
|
|
directModelSubagentScheduler?: SubagentScheduler
|
|
launchEnvironmentProvider?: LaunchEnvironmentProvider
|
|
}
|
|
|
|
function resolveContextCompression(
|
|
settings: ResolvedRuntimeSettings,
|
|
currentProfile: ResolvedModelProfile | undefined
|
|
): ModelRuntimeOptions['contextCompression'] {
|
|
const compression =
|
|
settings.contextCompression ?? defaultRuntimeSettings.contextCompression
|
|
const source = compression.modelSource
|
|
const summaryProfile =
|
|
source.kind === 'profile'
|
|
? settings.modelProfiles.find(
|
|
(profile) =>
|
|
profile.id === source.profileId &&
|
|
isAgentRuntimeModelProtocol(profile.protocol)
|
|
)
|
|
: undefined
|
|
return {
|
|
settings: compression,
|
|
contextWindowTokens: currentProfile?.contextWindowTokens,
|
|
...(summaryProfile
|
|
? {
|
|
summaryModel: {
|
|
apiKey: summaryProfile.apiKey,
|
|
baseUrl: summaryProfile.baseUrl,
|
|
model: summaryProfile.modelName,
|
|
protocol: summaryProfile.protocol as Exclude<
|
|
typeof summaryProfile.protocol,
|
|
'openai-images-generations'
|
|
>,
|
|
authentication: summaryProfile.authentication,
|
|
contextWindowTokens: summaryProfile.contextWindowTokens,
|
|
maximumOutputTokens: summaryProfile.maximumOutputTokens,
|
|
requestHeaders: summaryProfile.requestHeaders,
|
|
requestBody: summaryProfile.requestBody
|
|
}
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
|
|
export function createDefaultModelRuntime(
|
|
defaultWorkspace: string,
|
|
settings: ResolvedRuntimeSettings
|
|
): AgentRuntime {
|
|
if (settings.modelProtocol === 'openai-images-generations') {
|
|
return new UnconfiguredAgentRuntime()
|
|
}
|
|
const currentProfile = settings.modelProfiles.find(
|
|
(profile) => profile.id === settings.defaultModelProfileId
|
|
)
|
|
return new ModelAgentRuntime({
|
|
apiKey: settings.apiKey,
|
|
baseUrl: settings.modelBaseUrl,
|
|
model: settings.modelName,
|
|
protocol: settings.modelProtocol,
|
|
authentication: settings.modelAuthentication,
|
|
requestHeaders: currentProfile?.requestHeaders,
|
|
requestBody: currentProfile?.requestBody,
|
|
supportsImageInput: settings.supportsImageInput,
|
|
maximumOutputTokens: currentProfile?.maximumOutputTokens,
|
|
defaultWorkspace: settings.workspacePath || defaultWorkspace,
|
|
contextCompression: resolveContextCompression(
|
|
settings,
|
|
currentProfile
|
|
),
|
|
toolProvider: noModelTaskTools
|
|
})
|
|
}
|
|
|
|
export function createModelProfileRuntime(
|
|
defaultWorkspace: string,
|
|
settings: ResolvedRuntimeSettings,
|
|
profile: ResolvedModelProfile
|
|
): ModelAgentRuntime {
|
|
return new ModelAgentRuntime({
|
|
apiKey: profile.apiKey,
|
|
baseUrl: profile.baseUrl,
|
|
model: profile.modelName,
|
|
protocol: profile.protocol,
|
|
authentication: profile.authentication,
|
|
requestHeaders: profile.requestHeaders,
|
|
requestBody: profile.requestBody,
|
|
supportsImageInput: profile.supportsImageInput,
|
|
maximumOutputTokens: profile.maximumOutputTokens,
|
|
imageGenerationQuality:
|
|
profile.imageGenerationQuality ??
|
|
defaultRuntimeSettings.imageGenerationQuality,
|
|
contextCompression: resolveContextCompression(settings, profile),
|
|
defaultWorkspace: settings.workspacePath || defaultWorkspace,
|
|
toolProvider: noModelTaskTools
|
|
})
|
|
}
|
|
|
|
export function createAgentRuntime(
|
|
defaultWorkspace: string,
|
|
settings?: ResolvedRuntimeSettings,
|
|
capabilities: AgentCapabilityContext = {}
|
|
): AgentRuntime {
|
|
const executionSpace = capabilities.executionSpace
|
|
const remote = executionSpace?.kind === 'ssh'
|
|
const baseUrl = (
|
|
settings?.opencodeBaseUrl ||
|
|
process.env.GOODBUDDY_OPENCODE_URL ||
|
|
''
|
|
).trim()
|
|
const embedded = !baseUrl
|
|
const workspace =
|
|
executionSpace?.kind === 'local'
|
|
? executionSpace.rootPath
|
|
: settings?.workspacePath || defaultWorkspace
|
|
const getWorkspaceAccess = (): WorkspaceAccess =>
|
|
capabilities.workspaceAccess ??
|
|
executionSpace?.workspaceAccess ??
|
|
new LocalWorkspaceAccess(workspace)
|
|
const provider = settings?.provider ?? defaultRuntimeSettings.provider
|
|
|
|
if (
|
|
remote &&
|
|
(provider === 'continue' ||
|
|
provider === 'opencode' ||
|
|
provider === 'deepseek-harness' ||
|
|
provider === 'auto')
|
|
) {
|
|
throw new Error(REMOTE_EXECUTION_SPACE_UNAVAILABLE)
|
|
}
|
|
|
|
if (provider === 'deepseek-harness') {
|
|
const profile = settings?.deepseekHarnessModelProfile
|
|
if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
|
|
throw new Error(
|
|
'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
|
|
)
|
|
}
|
|
if (!profile.apiKey) {
|
|
throw new Error('DeepSeek Harness 模型连接未配置 API Key')
|
|
}
|
|
if (!capabilities.deepseekHarnessLauncher) {
|
|
throw new Error('DeepSeek Harness 受控 Host 启动器不可用')
|
|
}
|
|
const options: DeepSeekHarnessRuntimeOptions = {
|
|
defaultWorkspace: capabilities.localRuntimeRegistry ? defaultWorkspace : workspace,
|
|
baseUrl: profile.baseUrl,
|
|
model: profile.modelName,
|
|
supportsImageInput: profile.supportsImageInput === true,
|
|
requestHeaders: profile.requestHeaders,
|
|
launch: capabilities.deepseekHarnessLauncher,
|
|
credentialRefs: {
|
|
GOODBUDDY_HARNESS_MODEL_API_KEY: profile.apiKey
|
|
},
|
|
skillPackages: capabilities.skillPackages,
|
|
extensionPackages: capabilities.deepseekHarnessExtensions,
|
|
createToolProvider: (directory) => new ModelToolProvider(
|
|
new LocalWorkspaceAccess(directory), capabilities.mcpServers, undefined,
|
|
capabilities.knowledgeGateway, capabilities.webSearchEnabled === true,
|
|
{}, capabilities.launchEnvironmentProvider
|
|
)
|
|
}
|
|
const create = () => new DeepSeekHarnessRuntime(options)
|
|
if (!capabilities.localRuntimeRegistry) return create()
|
|
const configuration = { ...options, defaultWorkspace: undefined, createToolProvider: undefined }
|
|
return capabilities.localRuntimeRegistry.acquire(
|
|
`deepseek-harness:${profile.id}`,
|
|
{
|
|
...configuration,
|
|
mcpServers: capabilities.mcpServers,
|
|
webSearchEnabled: capabilities.webSearchEnabled === true,
|
|
launchEnvironment: capabilities.launchEnvironmentProvider?.()
|
|
},
|
|
workspace,
|
|
create
|
|
)
|
|
}
|
|
|
|
if (provider === 'continue') {
|
|
if (
|
|
settings?.continueModelProfile &&
|
|
!isAgentRuntimeModelProtocol(
|
|
settings.continueModelProfile.protocol
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'Continue 独立模型连接仅支持文本对话协议,不支持图像生成协议'
|
|
)
|
|
}
|
|
return new ContinueAgentRuntime({
|
|
binaryPath:
|
|
settings?.continueBinaryPath ??
|
|
process.env.GOODBUDDY_CONTINUE_BINARY?.trim() ??
|
|
process.env.GOODBUDDY_CONTINUE_COMMAND?.trim() ??
|
|
'',
|
|
bundledBinaryPath: capabilities.bundledRuntimePaths?.continue,
|
|
configPath:
|
|
settings?.continueConfigPath ??
|
|
process.env.GOODBUDDY_CONTINUE_CONFIG?.trim() ??
|
|
'',
|
|
modelProfile: settings?.continueModelProfile,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
skillPackages: capabilities.skillPackages,
|
|
defaultWorkspace: workspace,
|
|
hostCacheRoot:
|
|
capabilities.continueHostCacheRoot ??
|
|
process.env.GOODBUDDY_CONTINUE_HOST_CACHE?.trim() ??
|
|
'',
|
|
launchHost: capabilities.continueHostLauncher,
|
|
createHostAdapter: capabilities.launchEnvironmentProvider
|
|
? (options) =>
|
|
new ContinueHostAdapter({
|
|
...options,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
})
|
|
: undefined,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
mcpServers: capabilities.mcpServers,
|
|
customization: settings?.runtimeCustomization.continue
|
|
})
|
|
}
|
|
|
|
if (provider === 'opencode' || (provider === 'auto' && (baseUrl || embedded))) {
|
|
if (
|
|
settings?.opencodeModelProfile &&
|
|
!isAgentRuntimeModelProtocol(
|
|
settings.opencodeModelProfile.protocol
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'OpenCode 独立模型连接仅支持文本对话协议,不支持图像生成协议'
|
|
)
|
|
}
|
|
const options: OpenCodeRuntimeOptions = {
|
|
observeFailure: capabilities.observeFailure,
|
|
baseUrl,
|
|
embedded,
|
|
binaryPath:
|
|
settings?.opencodeBinaryPath ??
|
|
process.env.GOODBUDDY_OPENCODE_BINARY?.trim() ??
|
|
'',
|
|
bundledBinaryPath: capabilities.bundledRuntimePaths?.opencode,
|
|
bundledConfigPath:
|
|
capabilities.bundledRuntimePaths?.opencodeConfig,
|
|
configPath:
|
|
settings?.opencodeConfigPath ??
|
|
process.env.GOODBUDDY_OPENCODE_CONFIG?.trim() ??
|
|
'',
|
|
modelProfile: settings?.opencodeModelProfile,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
skillPackages: capabilities.skillPackages,
|
|
sharedCacheRoot: capabilities.opencodeSharedCacheRoot,
|
|
defaultWorkspace: capabilities.localRuntimeRegistry ? defaultWorkspace : workspace,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
mcpServers: capabilities.mcpServers,
|
|
customization: settings?.runtimeCustomization.opencode,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
}
|
|
const create = () => new OpenCodeRuntime(options)
|
|
if (!capabilities.localRuntimeRegistry) return create()
|
|
const configuration = { ...options, defaultWorkspace: undefined }
|
|
return capabilities.localRuntimeRegistry.acquire(
|
|
`opencode:${settings?.opencodeModelProfile?.id ?? 'native'}`,
|
|
{
|
|
...configuration,
|
|
launchEnvironment: capabilities.launchEnvironmentProvider?.()
|
|
},
|
|
workspace,
|
|
create
|
|
)
|
|
}
|
|
|
|
const defaultModelProfile =
|
|
settings?.modelProfiles.find(
|
|
(profile) => profile.id === settings.defaultModelProfileId
|
|
) ?? settings?.modelProfiles[0]
|
|
const modelApiKey =
|
|
defaultModelProfile?.apiKey ||
|
|
settings?.apiKey ||
|
|
process.env.GOODBUDDY_MODEL_API_KEY?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_API_KEY?.trim()
|
|
const modelAuthentication =
|
|
defaultModelProfile?.authentication ??
|
|
settings?.modelAuthentication ??
|
|
defaultRuntimeSettings.modelAuthentication
|
|
if (
|
|
provider === 'model' ||
|
|
(provider === 'auto' &&
|
|
(modelAuthentication === 'none' || modelApiKey))
|
|
) {
|
|
return new ModelAgentRuntime({
|
|
apiKey: modelApiKey ?? '',
|
|
baseUrl:
|
|
defaultModelProfile?.baseUrl ||
|
|
settings?.modelBaseUrl ||
|
|
process.env.GOODBUDDY_MODEL_BASE_URL?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_BASE_URL?.trim() ||
|
|
defaultRuntimeSettings.modelBaseUrl,
|
|
model:
|
|
defaultModelProfile?.modelName ||
|
|
settings?.modelName ||
|
|
process.env.GOODBUDDY_MODEL_NAME?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_MODEL?.trim() ||
|
|
defaultRuntimeSettings.modelName,
|
|
protocol:
|
|
defaultModelProfile?.protocol ??
|
|
settings?.modelProtocol ??
|
|
defaultRuntimeSettings.modelProtocol,
|
|
authentication: modelAuthentication,
|
|
requestHeaders: defaultModelProfile?.requestHeaders,
|
|
requestBody: defaultModelProfile?.requestBody,
|
|
supportsImageInput:
|
|
defaultModelProfile?.supportsImageInput ??
|
|
settings?.supportsImageInput ??
|
|
defaultRuntimeSettings.supportsImageInput,
|
|
maximumOutputTokens: defaultModelProfile?.maximumOutputTokens,
|
|
imageGenerationQuality:
|
|
defaultModelProfile?.imageGenerationQuality ??
|
|
settings?.imageGenerationQuality ??
|
|
defaultRuntimeSettings.imageGenerationQuality,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
defaultWorkspace: workspace,
|
|
mcpServers: capabilities.mcpServers,
|
|
browserService: capabilities.browserService,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
webSearchEnabled: capabilities.webSearchEnabled,
|
|
contextCompression: settings
|
|
? resolveContextCompression(settings, defaultModelProfile)
|
|
: undefined,
|
|
workspaceAccess: getWorkspaceAccess(),
|
|
directModelSubagentScheduler:
|
|
capabilities.directModelSubagentScheduler,
|
|
ripgrepExecutablePath: capabilities.bundledRuntimePaths?.ripgrep,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
})
|
|
}
|
|
|
|
return new UnconfiguredAgentRuntime()
|
|
}
|