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
Direct model conversations can now execute bounded platform-native processes and delegate one level of programming work without duplicating tools in Agent Runtimes. Process and Subagent activity follows existing workspace, mode, approval, cancellation, and lifecycle boundaries, with chat attribution and complete feature documentation. Remove the static Renderer bundle baseline gate and its build-only manifests; production builds continue to compile all processes and verify the control-plane installer. Also include the pending documentation link normalization. Release note: 直连模型现在可在 Execute 模式运行本机命令,并可委派编程 Subagent 完成修改、测试和复核;构建流程不再因静态 Renderer 包体基线阻塞。
1532 lines
50 KiB
TypeScript
1532 lines
50 KiB
TypeScript
import {
|
|
app,
|
|
BrowserWindow,
|
|
dialog,
|
|
globalShortcut,
|
|
Menu,
|
|
safeStorage,
|
|
session,
|
|
shell,
|
|
Tray,
|
|
utilityProcess
|
|
} from 'electron'
|
|
import { homedir } from 'node:os'
|
|
import { mkdir, readFile } from 'node:fs/promises'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { dirname, join } from 'node:path'
|
|
import spawn from 'cross-spawn'
|
|
import { ipcChannels } from '../shared/ipc-channels'
|
|
import {
|
|
createAgentRuntime,
|
|
createDefaultModelRuntime,
|
|
createModelProfileRuntime
|
|
} from './agent/create-runtime'
|
|
import { AgentRuntimeController } from './agent/runtime-controller'
|
|
import type { AgentRuntime } from './agent/runtime'
|
|
import { SelectedRuntimeManager } from './agent/selected-runtime-manager'
|
|
import { KnowledgeMcpGateway } from './agent/knowledge-mcp-gateway'
|
|
import {
|
|
applyRuntimeSelection,
|
|
getConfiguredRuntimeTarget,
|
|
resolveConfiguredAgentRuntimeSelection,
|
|
type SelectedRuntimeTarget
|
|
} from './agent/runtime-selection'
|
|
import { CapabilityService } from './capabilities/capability-service'
|
|
import { ContextManager } from './context-manager'
|
|
import {
|
|
registerIpcHandlers,
|
|
sendRemoteProjectSaveProgress
|
|
} from './ipc'
|
|
import { KnowledgeService } from './knowledge/knowledge-service'
|
|
import { AssistantDatabase } from './assistant/assistant-database'
|
|
import { createModelGraphExtractor } from './knowledge/model-extractor'
|
|
import { OpenAIEmbeddingClient } from './knowledge/openai-embedding-client'
|
|
import { embeddingProviderFingerprint } from './knowledge/embedding-provider-key'
|
|
import type { EmbeddingProvider } from './knowledge/types'
|
|
import { EmbeddingModelManager } from './knowledge/embedding-model-manager'
|
|
import {
|
|
EmbeddingInferenceBroker,
|
|
type EmbeddingInferenceTransport
|
|
} from './knowledge/embedding-inference-broker'
|
|
import { CohereRerankClient } from './knowledge/cohere-rerank-client'
|
|
import { RuntimeSettingsStore } from './runtime-settings-store'
|
|
import type { ResolvedRuntimeSettings } from './runtime-settings-store'
|
|
import { ToolApprovalBroker } from './tool-approval-broker'
|
|
import {
|
|
createMainWindow,
|
|
loadMainWindow,
|
|
showWindow,
|
|
toggleWindow
|
|
} from './window'
|
|
import { createTrayIcon } from './tray-icon'
|
|
import { resolveBundledRuntimePaths } from './agent/bundled-runtimes'
|
|
import type { ContinueHostLauncher } from './agent/continue-host-adapter'
|
|
import { resolvePortableUserDataPath } from './portable-user-data'
|
|
import { BrowserService } from './browser/browser-service'
|
|
import { SubagentService } from './assistant/subagent-service'
|
|
import { SubagentScheduler } from './assistant/subagent-scheduler'
|
|
import { ChannelSettingsStore } from './channels/channel-settings-store'
|
|
import type {
|
|
WechatSidecarChild,
|
|
WechatSidecarLauncher
|
|
} from './channels/wechat-sidecar-client'
|
|
import { buildWechatSidecarEnvironment } from './channels/wechat-sidecar-environment'
|
|
import { ApplicationSettingsStore } from './application-settings-store'
|
|
import { VersionChecker } from './version-checker'
|
|
import { SpeechModelManager } from './speech/speech-model-manager'
|
|
import { SpeechTranscriptionService } from './speech/speech-transcription-service'
|
|
import { GlobalTlsPolicy } from './global-tls-policy'
|
|
import { FeedbackIdentityStore } from './feedback/feedback-identity-store'
|
|
import {
|
|
createStrictFeedbackDispatcher,
|
|
StrictFeedbackHttpClient
|
|
} from './feedback/feedback-http-client'
|
|
import { FeedbackService } from './feedback/feedback-service'
|
|
import { registerFeedbackIpcHandler } from './feedback/feedback-ipc'
|
|
import type { AgentRuntimeSelection } from '../shared/runtime-selection-contracts'
|
|
import {
|
|
runCleanupBeforeDeadline,
|
|
settleCleanupPhases
|
|
} from './shutdown'
|
|
import { DocumentParsingSettingsStore } from './document-parsing-settings-store'
|
|
import { DocumentOcrModelManager } from './document-ocr-model-manager'
|
|
import { DocumentOcrBroker } from './document-ocr-broker'
|
|
import { DocumentParsingService } from './document-parsing-service'
|
|
import { ReleaseNotesService } from './release-notes-service'
|
|
import { GoodBuddyConfigService } from './goodbuddy-config-service'
|
|
import {
|
|
createDeepSeekHarnessUtilityLauncher,
|
|
type DeepSeekHarnessFork
|
|
} from './agent/deepseek-harness-utility-launcher'
|
|
import { buildControlledHarnessEnvironment } from './agent/process-environment'
|
|
import {
|
|
createStartupFailureDiagnostic,
|
|
formatStartupFailureMessage,
|
|
runStartupPrerequisites
|
|
} from './startup-prerequisites'
|
|
import { RuntimeExtensionStore } from './agent/runtime-extension-store'
|
|
import {
|
|
DshNpmExtensionInstaller,
|
|
DshNpmMarketplaceCatalog
|
|
} from './agent/dsh-extension-marketplace'
|
|
import { registerDesktopNotificationActivation } from './desktop-notification'
|
|
import {
|
|
isInstalledWindowsBuild,
|
|
repairStaleWindowsNotificationShortcuts,
|
|
resolveWindowsAppUserModelId
|
|
} from './windows-notification-identity'
|
|
import { ShortcutSettingsStore } from './shortcut-settings-store'
|
|
import { ShortcutSettingsService } from './shortcut-settings-service'
|
|
import { SshHostStore } from './ssh/ssh-host-store'
|
|
import { Ssh2Transport } from './ssh/ssh-transport'
|
|
import { SshHostService } from './ssh/ssh-host-service'
|
|
import { SshHostDirectoryBrowser } from './ssh/ssh-host-directory-browser'
|
|
import {
|
|
SshHostRemoteEnvironmentInspector
|
|
} from './ssh/ssh-host-remote-environment'
|
|
import { defaultGlobalShortcutSettings } from '../shared/shortcut'
|
|
import { requestProcessTreeTermination } from './agent/child-process-termination'
|
|
import { createContinueUtilityProcessChild } from './agent/continue-utility-process-adapter'
|
|
import {
|
|
ExecutionSpaceResolver,
|
|
type ExecutionSpaceDescriptor
|
|
} from './execution-space'
|
|
import { RemoteAgentServices } from './remote-agent/remote-agent-services'
|
|
import {
|
|
resolveBundledAgentResourcePaths,
|
|
resolveControlPlanePackageInstallerPath
|
|
} from './remote-agent/bundled-agent-resources'
|
|
import {
|
|
AgentPackageManager
|
|
} from './remote-agent/agent-package-manager'
|
|
import { ManagedRemoteExecutionServices } from './remote-agent/managed-remote-execution-services'
|
|
import {
|
|
RemoteEnvironmentUpdateService
|
|
} from './remote-agent/remote-environment-update-service'
|
|
import {
|
|
RemoteEnvironmentPreparer
|
|
} from './remote-agent/remote-environment-preparer'
|
|
import {
|
|
RemoteEnvironmentOperationStore
|
|
} from './remote-agent/remote-environment-operation-store'
|
|
import type {
|
|
RuntimeSettings,
|
|
RuntimeSettingsInput
|
|
} from '../shared/contracts'
|
|
import {
|
|
DesktopDiagnostics,
|
|
type DesktopDiagnosticFailureObserver
|
|
} from './desktop-diagnostics'
|
|
import { TerminalSessionManager } from './terminal/terminal-session-manager'
|
|
|
|
const legacyDefaultShortcut =
|
|
defaultGlobalShortcutSettings.accelerator
|
|
const mainModuleDirectory = dirname(fileURLToPath(import.meta.url))
|
|
const portableUserDataPath = resolvePortableUserDataPath({
|
|
packaged: app.isPackaged,
|
|
platform: process.platform,
|
|
executablePath: process.execPath
|
|
})
|
|
if (portableUserDataPath) {
|
|
app.setPath('userData', portableUserDataPath)
|
|
}
|
|
const desktopDiagnostics = new DesktopDiagnostics(
|
|
join(app.getPath('userData'), 'diagnostics')
|
|
)
|
|
const observeDesktopFailure: DesktopDiagnosticFailureObserver = (
|
|
failure
|
|
) => {
|
|
void desktopDiagnostics.recordFailure(failure).catch(() => undefined)
|
|
}
|
|
const installedWindowsBuild = isInstalledWindowsBuild({
|
|
packaged: app.isPackaged,
|
|
platform: process.platform,
|
|
executablePath: process.execPath
|
|
})
|
|
if (process.platform === 'win32') {
|
|
app.setAppUserModelId(
|
|
resolveWindowsAppUserModelId({
|
|
installed: installedWindowsBuild,
|
|
executablePath: process.execPath
|
|
})
|
|
)
|
|
}
|
|
const hasSingleInstanceLock = app.requestSingleInstanceLock()
|
|
|
|
if (!hasSingleInstanceLock) {
|
|
app.quit()
|
|
}
|
|
|
|
let mainWindow: BrowserWindow | undefined
|
|
let tray: Tray | undefined
|
|
let isQuitting = false
|
|
let removeIpcHandlers: (() => Promise<void>) | undefined
|
|
let removeFeedbackIpcHandler: (() => void) | undefined
|
|
let runtime: AgentRuntimeController | undefined
|
|
let selectedRuntimeManager: SelectedRuntimeManager | undefined
|
|
let knowledgeService: KnowledgeService | undefined
|
|
let knowledgeGateway: KnowledgeMcpGateway | undefined
|
|
let assistantDatabase: AssistantDatabase | undefined
|
|
let browserService: BrowserService | undefined
|
|
let globalTlsPolicy: GlobalTlsPolicy | undefined
|
|
let feedbackService: FeedbackService | undefined
|
|
let documentOcrBroker: DocumentOcrBroker | undefined
|
|
let documentOcrModelManager: DocumentOcrModelManager | undefined
|
|
let embeddingModelManager: EmbeddingModelManager | undefined
|
|
const embeddingBrokers = new Set<EmbeddingInferenceBroker>()
|
|
let stopRuntimeReconfiguration: (() => Promise<void>) | undefined
|
|
let dshExtensionInstaller: DshNpmExtensionInstaller | undefined
|
|
let remoteAgentServices: RemoteAgentServices | undefined
|
|
let terminalSessionManager: TerminalSessionManager | undefined
|
|
let managedRemoteExecutionServices:
|
|
| ManagedRemoteExecutionServices
|
|
| undefined
|
|
let directModelSubagentScheduler: SubagentScheduler | undefined
|
|
|
|
type ManagedEmbeddingProvider = EmbeddingProvider & {
|
|
dispose?: () => void | Promise<void>
|
|
}
|
|
|
|
function createEmbeddingUtilityTransport(
|
|
modulePath: string,
|
|
modelDirectory: string
|
|
): EmbeddingInferenceTransport {
|
|
const child = utilityProcess.fork(
|
|
modulePath,
|
|
[modelDirectory],
|
|
{
|
|
serviceName: 'GoodBuddy Embedding Inference',
|
|
stdio: 'pipe',
|
|
allowLoadingUnsignedLibraries: false
|
|
}
|
|
)
|
|
return {
|
|
postMessage: (message) => child.postMessage(message),
|
|
onMessage: (listener) => {
|
|
child.on('message', listener)
|
|
return () => child.removeListener('message', listener)
|
|
},
|
|
onClose: (listener) => {
|
|
let closed = false
|
|
const notify = (): void => {
|
|
if (closed) {
|
|
return
|
|
}
|
|
closed = true
|
|
listener()
|
|
}
|
|
child.on('exit', notify)
|
|
child.on('error', notify)
|
|
return () => {
|
|
child.removeListener('exit', notify)
|
|
child.removeListener('error', notify)
|
|
}
|
|
},
|
|
close: () => {
|
|
child.kill()
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createEmbeddingProvider(
|
|
settings: ResolvedRuntimeSettings,
|
|
manager: EmbeddingModelManager,
|
|
connectionId = settings.activeEmbeddingConnectionId
|
|
): Promise<ManagedEmbeddingProvider | undefined> {
|
|
if (!settings.knowledgeEmbeddingEnabled) {
|
|
return undefined
|
|
}
|
|
const connection = settings.embeddingConnections?.find(
|
|
(candidate) => candidate.id === connectionId
|
|
)
|
|
if (!connection) {
|
|
throw new Error('当前向量连接不存在')
|
|
}
|
|
if (connection.kind !== 'builtin') {
|
|
return new OpenAIEmbeddingClient({
|
|
endpoint: connection.baseUrl,
|
|
model: connection.modelName,
|
|
apiKey: connection.apiKey
|
|
})
|
|
}
|
|
const snapshot = await manager.getSnapshot()
|
|
const model =
|
|
snapshot.catalog.find((candidate) => candidate.recommended) ??
|
|
snapshot.catalog[0]
|
|
if (!model) {
|
|
throw new Error('内置向量模型目录为空')
|
|
}
|
|
const modelDirectory =
|
|
await manager.getVerifiedModelDirectory(model.id)
|
|
const installed = snapshot.installed.find(
|
|
(candidate) => candidate.id === model.id
|
|
)
|
|
if (!installed) {
|
|
throw new Error('内置向量模型尚未安装')
|
|
}
|
|
const tokenizerDigest = installed.files.find(
|
|
(file) => file.role === 'tokenizer'
|
|
)?.sha256
|
|
const modelDigest = installed.files.find(
|
|
(file) => file.role === 'model'
|
|
)?.sha256
|
|
if (!tokenizerDigest || !modelDigest) {
|
|
throw new Error('内置向量模型安装清单不完整')
|
|
}
|
|
const broker = new EmbeddingInferenceBroker({
|
|
createTransport: () =>
|
|
createEmbeddingUtilityTransport(
|
|
join(mainModuleDirectory, 'embedding-inference-bootstrap.js'),
|
|
modelDirectory
|
|
)
|
|
})
|
|
embeddingBrokers.add(broker)
|
|
return {
|
|
provider: 'builtin',
|
|
model: model.id,
|
|
fingerprint: embeddingProviderFingerprint({
|
|
provider: 'builtin',
|
|
dataPath: { kind: 'device' },
|
|
model: model.id,
|
|
dimensions: model.dimensions,
|
|
encodingRecipe: {
|
|
recipeId: 'granite-embedding-97m-r2',
|
|
artifactDigest: modelDigest,
|
|
tokenizerDigest,
|
|
pooling: 'cls',
|
|
normalization: 'l2',
|
|
queryTemplate: '{text}',
|
|
documentTemplate: '{text}',
|
|
maximumSequenceTokens: model.contextTokens
|
|
}
|
|
}),
|
|
embed: (texts, signal) =>
|
|
broker.embed(texts, 'document', signal),
|
|
embedQuery: (texts, signal) =>
|
|
broker.embed(texts, 'query', signal),
|
|
embedDocuments: (texts, signal) =>
|
|
broker.embed(texts, 'document', signal),
|
|
dispose: async () => {
|
|
try {
|
|
await broker.shutdown()
|
|
} finally {
|
|
embeddingBrokers.delete(broker)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function runtimeSettingsInputWithEmbeddingSelection(
|
|
settings: RuntimeSettings,
|
|
activeEmbeddingConnectionId: string
|
|
): RuntimeSettingsInput {
|
|
const configured = settings.configured ?? settings
|
|
const defaultProfile =
|
|
settings.modelProfiles.find(
|
|
(profile) => profile.id === settings.defaultModelProfileId
|
|
) ?? settings.modelProfiles[0]
|
|
if (!defaultProfile || !settings.embeddingConnections) {
|
|
throw new Error('模型连接设置不完整')
|
|
}
|
|
return {
|
|
provider: settings.provider,
|
|
modelBaseUrl: defaultProfile.baseUrl,
|
|
modelName: defaultProfile.modelName,
|
|
modelProtocol: defaultProfile.protocol,
|
|
modelAuthentication: defaultProfile.authentication,
|
|
imageGenerationQuality: defaultProfile.imageGenerationQuality,
|
|
opencodeBaseUrl: configured.opencodeBaseUrl,
|
|
opencodeEmbedded: settings.opencodeEmbedded,
|
|
opencodeBinaryPath: configured.opencodeBinaryPath,
|
|
opencodeConfigPath: configured.opencodeConfigPath,
|
|
continueBinaryPath: configured.continueBinaryPath,
|
|
continueConfigPath: configured.continueConfigPath,
|
|
continueMode: settings.continueMode,
|
|
subagentSmartRoutingEnabled: settings.subagentSmartRoutingEnabled,
|
|
knowledgeEmbeddingEnabled: settings.knowledgeEmbeddingEnabled,
|
|
knowledgeEmbeddingBaseUrl: settings.knowledgeEmbeddingBaseUrl,
|
|
knowledgeEmbeddingModel: settings.knowledgeEmbeddingModel,
|
|
knowledgeEmbeddingApiKey: { action: 'keep' },
|
|
embeddingConnections: settings.embeddingConnections.map(
|
|
(connection) =>
|
|
connection.kind === 'builtin'
|
|
? {
|
|
id: connection.id,
|
|
name: connection.name,
|
|
kind: connection.kind
|
|
}
|
|
: {
|
|
id: connection.id,
|
|
name: connection.name,
|
|
kind: connection.kind,
|
|
baseUrl: connection.baseUrl,
|
|
modelName: connection.modelName,
|
|
authentication: connection.authentication,
|
|
apiKey: { action: 'keep' as const }
|
|
}
|
|
),
|
|
activeEmbeddingConnectionId,
|
|
knowledgeRerankEnabled: settings.knowledgeRerankEnabled ?? false,
|
|
knowledgeRerankEndpoint: settings.knowledgeRerankEndpoint ?? '',
|
|
knowledgeRerankModel: settings.knowledgeRerankModel ?? '',
|
|
knowledgeRerankApiKey: { action: 'keep' },
|
|
contextCompression: settings.contextCompression,
|
|
workspacePath: configured.workspacePath,
|
|
apiKey: { action: 'keep' },
|
|
modelProfiles: settings.modelProfiles.map((profile) => ({
|
|
id: profile.id,
|
|
name: profile.name,
|
|
baseUrl: profile.baseUrl,
|
|
modelName: profile.modelName,
|
|
protocol: profile.protocol,
|
|
authentication: profile.authentication,
|
|
supportsImageInput: profile.supportsImageInput,
|
|
contextWindowTokens: profile.contextWindowTokens,
|
|
imageGenerationQuality: profile.imageGenerationQuality,
|
|
apiKey: { action: 'keep' as const }
|
|
})),
|
|
defaultModelProfileId: settings.defaultModelProfileId,
|
|
opencodeModelSource: configured.opencodeModelSource,
|
|
continueModelSource: configured.continueModelSource,
|
|
deepseekHarnessModelSource:
|
|
configured.deepseekHarnessModelSource ?? { kind: 'platform' },
|
|
toolApproval: settings.toolApproval
|
|
}
|
|
}
|
|
|
|
function createRerankProvider(
|
|
settings: ResolvedRuntimeSettings
|
|
): CohereRerankClient | undefined {
|
|
return settings.knowledgeRerankEnabled
|
|
? new CohereRerankClient({
|
|
endpoint: settings.knowledgeRerankEndpoint,
|
|
model: settings.knowledgeRerankModel,
|
|
apiKey: settings.knowledgeRerankApiKey
|
|
})
|
|
: undefined
|
|
}
|
|
|
|
function createSubagentProfileRuntimes(
|
|
defaultWorkspace: string,
|
|
settings: ResolvedRuntimeSettings
|
|
): ReadonlyMap<string, AgentRuntime> {
|
|
return new Map(
|
|
settings.modelProfiles
|
|
.filter(
|
|
(profile) =>
|
|
profile.id !== settings.defaultModelProfileId &&
|
|
profile.protocol !== 'openai-images-generations'
|
|
)
|
|
.map(
|
|
(profile) =>
|
|
[
|
|
profile.id,
|
|
createModelProfileRuntime(
|
|
defaultWorkspace,
|
|
settings,
|
|
profile
|
|
)
|
|
] as const
|
|
)
|
|
)
|
|
}
|
|
|
|
const launchContinueHost: ContinueHostLauncher = (
|
|
entryPath,
|
|
args,
|
|
options
|
|
) => {
|
|
const utilityChild = utilityProcess.fork(
|
|
join(dirname(entryPath), 'utility-bootstrap.mjs'),
|
|
[entryPath, ...args],
|
|
{
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
serviceName: 'GoodBuddy Continue Host',
|
|
stdio: 'pipe'
|
|
}
|
|
)
|
|
return createContinueUtilityProcessChild({
|
|
get pid() {
|
|
return utilityChild.pid
|
|
},
|
|
stderr: utilityChild.stderr,
|
|
kill: () => utilityChild.kill(),
|
|
onExit: (listener) => {
|
|
utilityChild.on('exit', listener)
|
|
},
|
|
onceExit: (listener) => {
|
|
utilityChild.once('exit', listener)
|
|
},
|
|
onceError: (listener) => {
|
|
utilityChild.once('error', listener)
|
|
},
|
|
removeExitListener: (listener) => {
|
|
utilityChild.removeListener('exit', listener)
|
|
},
|
|
removeErrorListener: (listener) => {
|
|
utilityChild.removeListener('error', listener)
|
|
}
|
|
})
|
|
}
|
|
|
|
const forkDeepSeekHarness: DeepSeekHarnessFork = (
|
|
modulePath,
|
|
args,
|
|
options
|
|
) =>
|
|
utilityProcess.fork(modulePath, args, {
|
|
...options,
|
|
allowLoadingUnsignedLibraries: false,
|
|
disclaim: false
|
|
})
|
|
|
|
function terminateHarnessUtilityProcess(
|
|
child: ReturnType<DeepSeekHarnessFork>
|
|
): void {
|
|
requestProcessTreeTermination(child, { spawn })
|
|
}
|
|
|
|
const launchWechatSidecar: WechatSidecarLauncher = () => {
|
|
const utilityChild = utilityProcess.fork(
|
|
join(mainModuleDirectory, 'wechat-sidecar.js'),
|
|
[],
|
|
{
|
|
env: buildWechatSidecarEnvironment(),
|
|
serviceName: 'GoodBuddy Weixin Transport',
|
|
stdio: 'ignore'
|
|
}
|
|
)
|
|
const child: WechatSidecarChild = {
|
|
postMessage: (message) => utilityChild.postMessage(message),
|
|
kill: () => utilityChild.kill(),
|
|
on: (_event, listener) => {
|
|
utilityChild.on('message', listener)
|
|
return child
|
|
},
|
|
once: (
|
|
event: 'exit' | 'error',
|
|
listener: ((code: number | null) => void) | ((error: Error) => void)
|
|
) => {
|
|
if (event === 'exit') {
|
|
utilityChild.once('exit', (code) => {
|
|
;(listener as (code: number | null) => void)(code)
|
|
})
|
|
} else {
|
|
utilityChild.once('error', (_type, location, report) => {
|
|
;(listener as (error: Error) => void)(
|
|
new Error(
|
|
`微信 Sidecar 异常(${location}):${report.slice(0, 300)}`
|
|
)
|
|
)
|
|
})
|
|
}
|
|
return child
|
|
}
|
|
}
|
|
return child
|
|
}
|
|
|
|
function buildTray(): Tray {
|
|
const nextTray = new Tray(createTrayIcon())
|
|
nextTray.setToolTip('GoodBuddy')
|
|
nextTray.setContextMenu(
|
|
Menu.buildFromTemplate([
|
|
{
|
|
label: '打开 GoodBuddy',
|
|
click: () => mainWindow && showWindow(mainWindow)
|
|
},
|
|
{
|
|
label: '新建对话',
|
|
click: () => {
|
|
if (mainWindow) {
|
|
showWindow(mainWindow)
|
|
mainWindow.webContents.send(ipcChannels.conversationNew)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label: '设置',
|
|
click: () => {
|
|
if (mainWindow) {
|
|
showWindow(mainWindow)
|
|
mainWindow.webContents.send(ipcChannels.settingsOpen)
|
|
}
|
|
}
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
label: '退出',
|
|
click: () => {
|
|
isQuitting = true
|
|
app.quit()
|
|
}
|
|
}
|
|
])
|
|
)
|
|
nextTray.on('click', () => {
|
|
if (mainWindow) {
|
|
toggleWindow(mainWindow)
|
|
}
|
|
})
|
|
return nextTray
|
|
}
|
|
|
|
if (hasSingleInstanceLock) {
|
|
app.on('second-instance', () => {
|
|
if (mainWindow) {
|
|
showWindow(mainWindow)
|
|
}
|
|
})
|
|
|
|
void app.whenReady().then(async () => {
|
|
session.defaultSession.setPermissionRequestHandler(
|
|
(webContents, permission, callback, details) => {
|
|
const mediaTypes =
|
|
'mediaTypes' in details && Array.isArray(details.mediaTypes)
|
|
? details.mediaTypes
|
|
: []
|
|
callback(
|
|
permission === 'media' &&
|
|
webContents === mainWindow?.webContents &&
|
|
mediaTypes.includes('audio') &&
|
|
!mediaTypes.includes('video')
|
|
)
|
|
}
|
|
)
|
|
session.defaultSession.setPermissionCheckHandler(
|
|
(webContents, permission, _origin, details) =>
|
|
permission === 'media' &&
|
|
webContents === mainWindow?.webContents &&
|
|
details.mediaType === 'audio'
|
|
)
|
|
|
|
mainWindow = createMainWindow(() => isQuitting)
|
|
registerDesktopNotificationActivation(mainWindow)
|
|
tray = buildTray()
|
|
const defaultWorkspace = process.env.GOODBUDDY_WORKSPACE ?? homedir()
|
|
const secureCipher = {
|
|
isAvailable: () =>
|
|
safeStorage.isEncryptionAvailable() &&
|
|
(process.platform !== 'linux' ||
|
|
[
|
|
'gnome_libsecret',
|
|
'kwallet',
|
|
'kwallet5',
|
|
'kwallet6'
|
|
].includes(safeStorage.getSelectedStorageBackend())),
|
|
encrypt: (value: string) => safeStorage.encryptString(value),
|
|
decrypt: (value: Buffer) => safeStorage.decryptString(value)
|
|
}
|
|
const settingsStore = new RuntimeSettingsStore(
|
|
join(app.getPath('userData'), 'runtime-settings.json'),
|
|
secureCipher
|
|
)
|
|
const applicationSettingsStore = new ApplicationSettingsStore(
|
|
join(app.getPath('userData'), 'application-settings.json')
|
|
)
|
|
const sshHostStore = new SshHostStore(
|
|
join(app.getPath('userData'), 'ssh-hosts.json'),
|
|
secureCipher
|
|
)
|
|
const agentMetadataPaths = resolveBundledAgentResourcePaths({
|
|
appPath: app.getAppPath(),
|
|
resourcesPath: process.resourcesPath,
|
|
packaged: app.isPackaged
|
|
})
|
|
const controlPlanePackageInstaller = await readFile(
|
|
resolveControlPlanePackageInstallerPath({
|
|
appPath: app.getAppPath()
|
|
})
|
|
)
|
|
const agentPackageManager = new AgentPackageManager({
|
|
userDataPath: app.getPath('userData'),
|
|
desktopVersion: app.getVersion(),
|
|
keyRegistryPath: agentMetadataPaths.keyRegistryPath,
|
|
getUpdateSource: async () =>
|
|
(await applicationSettingsStore.get()).updateSource
|
|
})
|
|
const startupRemoteAgentServices = new RemoteAgentServices({
|
|
sshHostStore,
|
|
goodBuddyVersion: app.getVersion(),
|
|
userDataPath: app.getPath('userData'),
|
|
appPath: app.getAppPath(),
|
|
resourcesPath: process.resourcesPath,
|
|
packaged: app.isPackaged,
|
|
controlPlanePackageInstaller,
|
|
observeFailure: observeDesktopFailure
|
|
})
|
|
remoteAgentServices = startupRemoteAgentServices
|
|
const startupManagedRemoteExecutionServices =
|
|
new ManagedRemoteExecutionServices({
|
|
sshHostStore,
|
|
agentServices: startupRemoteAgentServices,
|
|
userDataPath: app.getPath('userData'),
|
|
appPath: app.getAppPath(),
|
|
resourcesPath: process.resourcesPath,
|
|
packaged: app.isPackaged,
|
|
resolveRuntimeSelection: async (selection) =>
|
|
resolveConfiguredAgentRuntimeSelection(
|
|
await settingsStore.getResolvedSettings(),
|
|
selection
|
|
),
|
|
resolveModelProfile: async (selection) =>
|
|
applyRuntimeSelection(
|
|
await settingsStore.getResolvedSettings(),
|
|
selection
|
|
).settings.opencodeModelProfile
|
|
})
|
|
await startupManagedRemoteExecutionServices.initialize()
|
|
managedRemoteExecutionServices =
|
|
startupManagedRemoteExecutionServices
|
|
const beginRemoteHostInvalidation = (hostId: string): void => {
|
|
const invalidations = [
|
|
startupRemoteAgentServices.invalidateHost(hostId)
|
|
]
|
|
startupManagedRemoteExecutionServices.invalidateHost(hostId)
|
|
if (selectedRuntimeManager) {
|
|
invalidations.push(
|
|
selectedRuntimeManager.invalidateHost(hostId)
|
|
)
|
|
}
|
|
void Promise.allSettled(invalidations).then((results) => {
|
|
if (results.some((result) => result.status === 'rejected')) {
|
|
console.warn(
|
|
'Failed to invalidate complete remote Host state'
|
|
)
|
|
}
|
|
})
|
|
}
|
|
const sshHostService = new SshHostService(
|
|
sshHostStore,
|
|
new Ssh2Transport(),
|
|
{
|
|
onHostEdited: beginRemoteHostInvalidation,
|
|
onHostRemoved: beginRemoteHostInvalidation
|
|
}
|
|
)
|
|
const sshHostDirectoryBrowser = new SshHostDirectoryBrowser({
|
|
sshHosts: sshHostStore,
|
|
sshPool: startupRemoteAgentServices.sshPool
|
|
})
|
|
const sshHostRemoteEnvironmentInspector =
|
|
new SshHostRemoteEnvironmentInspector({
|
|
sshHosts: sshHostStore,
|
|
sshPool: startupRemoteAgentServices.sshPool,
|
|
agentRuntimeLockPath:
|
|
startupRemoteAgentServices.resourcePaths.runtimeLockPath,
|
|
remoteRuntimeLockPath:
|
|
startupManagedRemoteExecutionServices.runtimeResourcePaths
|
|
.runtimeLockPath,
|
|
loadRemoteEnvironmentCatalog: (architecture, options) =>
|
|
agentPackageManager.getRemoteEnvironmentCatalog(
|
|
architecture,
|
|
options
|
|
)
|
|
})
|
|
const [initialRuntimeSettings, initialResolvedSettings] =
|
|
await Promise.all([
|
|
settingsStore.getPublicSettings(),
|
|
settingsStore.getResolvedSettings()
|
|
])
|
|
globalTlsPolicy = new GlobalTlsPolicy(app)
|
|
globalTlsPolicy.install()
|
|
const capabilityService = new CapabilityService(
|
|
join(app.getPath('userData'), 'capabilities.json'),
|
|
app.isPackaged
|
|
? join(process.resourcesPath, 'skills')
|
|
: join(app.getAppPath(), 'resources', 'skills'),
|
|
join(app.getPath('userData'), 'skills', 'imported'),
|
|
secureCipher
|
|
)
|
|
const channelSettingsStore = new ChannelSettingsStore(
|
|
join(app.getPath('userData'), 'channel-settings.json'),
|
|
secureCipher
|
|
)
|
|
const startupFeedbackService = new FeedbackService({
|
|
appVersion: app.getVersion(),
|
|
platform: process.platform,
|
|
architecture: process.arch,
|
|
identityStore: new FeedbackIdentityStore(
|
|
join(app.getPath('userData'), 'feedback-identity.json')
|
|
),
|
|
client: new StrictFeedbackHttpClient({
|
|
appVersion: app.getVersion(),
|
|
dispatcher: createStrictFeedbackDispatcher()
|
|
}),
|
|
diagnosticsProvider: desktopDiagnostics
|
|
})
|
|
feedbackService = startupFeedbackService
|
|
const startupEmbeddingModelManager = new EmbeddingModelManager({
|
|
userDataDirectory: app.getPath('userData'),
|
|
fetch: globalThis.fetch,
|
|
getDownloadSource: async () =>
|
|
(await applicationSettingsStore.get()).modelDownloadSource
|
|
})
|
|
embeddingModelManager = startupEmbeddingModelManager
|
|
const releaseNotesService = new ReleaseNotesService({
|
|
currentVersion: app.getVersion(),
|
|
filePath: app.isPackaged
|
|
? join(process.resourcesPath, 'release-notes.json')
|
|
: join(app.getAppPath(), 'resources', 'release-notes.json'),
|
|
settingsStore: applicationSettingsStore
|
|
})
|
|
const documentParsingSettingsStore =
|
|
new DocumentParsingSettingsStore(
|
|
join(app.getPath('userData'), 'document-parsing-settings.json')
|
|
)
|
|
documentOcrModelManager = new DocumentOcrModelManager({
|
|
userDataDirectory: app.getPath('userData'),
|
|
fetch: globalThis.fetch,
|
|
getDownloadSource: async () =>
|
|
(await applicationSettingsStore.get()).modelDownloadSource
|
|
})
|
|
documentOcrBroker = new DocumentOcrBroker(mainWindow)
|
|
const documentParsingService = new DocumentParsingService(
|
|
documentParsingSettingsStore,
|
|
documentOcrModelManager,
|
|
documentOcrBroker
|
|
)
|
|
const versionChecker = new VersionChecker({
|
|
fetch: globalThis.fetch,
|
|
currentVersion: app.getVersion(),
|
|
platform: process.platform,
|
|
arch: process.arch
|
|
})
|
|
const speechModelManager = new SpeechModelManager({
|
|
userDataDirectory: app.getPath('userData'),
|
|
fetch: globalThis.fetch,
|
|
getDownloadSource: async () =>
|
|
(await applicationSettingsStore.get()).modelDownloadSource
|
|
})
|
|
const speechTranscriptionService = new SpeechTranscriptionService(
|
|
speechModelManager
|
|
)
|
|
browserService = new BrowserService({ parentWindow: mainWindow })
|
|
const bundledRuntimePaths = resolveBundledRuntimePaths({
|
|
appPath: app.getAppPath(),
|
|
resourcesPath: process.resourcesPath,
|
|
packaged: app.isPackaged
|
|
})
|
|
const deepSeekHarnessHome = join(
|
|
app.getPath('userData'),
|
|
'deepseek-harness'
|
|
)
|
|
const startupDshExtensionInstaller = new DshNpmExtensionInstaller({
|
|
dshHome: deepSeekHarnessHome,
|
|
npmCliPath: app.isPackaged
|
|
? join(
|
|
process.resourcesPath,
|
|
'runtimes',
|
|
'npm',
|
|
'bin',
|
|
'npm-cli.js'
|
|
)
|
|
: join(
|
|
app.getAppPath(),
|
|
'node_modules',
|
|
'npm',
|
|
'bin',
|
|
'npm-cli.js'
|
|
)
|
|
})
|
|
dshExtensionInstaller = startupDshExtensionInstaller
|
|
const runtimeExtensionStore = new RuntimeExtensionStore(
|
|
app.getPath('userData'),
|
|
{
|
|
catalog: new DshNpmMarketplaceCatalog(),
|
|
install: (input) =>
|
|
startupDshExtensionInstaller.install(input)
|
|
}
|
|
)
|
|
const launchDeepSeekHarness =
|
|
createDeepSeekHarnessUtilityLauncher({
|
|
bundledHostPath: bundledRuntimePaths.deepseekHarness,
|
|
dshHome: deepSeekHarnessHome,
|
|
environment: buildControlledHarnessEnvironment(
|
|
deepSeekHarnessHome
|
|
),
|
|
fork: forkDeepSeekHarness,
|
|
terminateProcess: terminateHarnessUtilityProcess,
|
|
onExtensionStartupFailures: (extensionIds) =>
|
|
runtimeExtensionStore.markStartupFailed(extensionIds)
|
|
})
|
|
const startupKnowledgeService = new KnowledgeService({
|
|
databasePath: join(app.getPath('userData'), 'knowledge.sqlite'),
|
|
managedRoot: join(app.getPath('userData'), 'knowledge'),
|
|
extractStructured: createModelGraphExtractor(settingsStore),
|
|
parseDocument: documentParsingService.parse
|
|
})
|
|
knowledgeService = startupKnowledgeService
|
|
let activeEmbeddingProvider:
|
|
| Awaited<ReturnType<typeof createEmbeddingProvider>>
|
|
| undefined
|
|
let activeRerankProvider:
|
|
| ReturnType<typeof createRerankProvider>
|
|
| undefined
|
|
const startupAssistantDatabase = new AssistantDatabase(
|
|
join(app.getPath('userData'), 'assistant.sqlite'),
|
|
{
|
|
onMagicTodosChanged: () => {
|
|
queueMicrotask(() => {
|
|
if (
|
|
mainWindow &&
|
|
!mainWindow.isDestroyed() &&
|
|
!mainWindow.webContents.isDestroyed()
|
|
) {
|
|
mainWindow.webContents.send(
|
|
ipcChannels.magicTodosStatusChanged
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
)
|
|
assistantDatabase = startupAssistantDatabase
|
|
directModelSubagentScheduler = new SubagentScheduler({
|
|
concurrency: 3,
|
|
queueLimit: 20,
|
|
timeoutMs: 10 * 60_000
|
|
})
|
|
const executionSpaceResolver = new ExecutionSpaceResolver(
|
|
startupManagedRemoteExecutionServices.workspaceAccessFactory
|
|
)
|
|
const goodbuddyConfigService = new GoodBuddyConfigService(
|
|
applicationSettingsStore,
|
|
capabilityService
|
|
)
|
|
const startupKnowledgeGateway = new KnowledgeMcpGateway(
|
|
startupKnowledgeService,
|
|
{
|
|
magicNotesDatabase: startupAssistantDatabase,
|
|
configService: goodbuddyConfigService
|
|
}
|
|
)
|
|
knowledgeGateway = startupKnowledgeGateway
|
|
const createRuntimeWithCapabilities = async (
|
|
settings: ResolvedRuntimeSettings,
|
|
target: SelectedRuntimeTarget,
|
|
executionSpace?: ExecutionSpaceDescriptor
|
|
): Promise<AgentRuntime> => {
|
|
const [
|
|
skillContext,
|
|
mcpServers,
|
|
browserCapability,
|
|
webSearchCapability,
|
|
deepseekHarnessExtensions
|
|
] =
|
|
await Promise.all([
|
|
capabilityService.getRuntimeSkillContext(target),
|
|
capabilityService.getResolvedMcpServers(target),
|
|
target === 'model'
|
|
? capabilityService.getComputerCapabilityStatus(
|
|
'host-browser-control'
|
|
)
|
|
: Promise.resolve(undefined),
|
|
target === 'model' || target === 'deepseek-harness'
|
|
? capabilityService.getWebSearchCapabilityStatus()
|
|
: Promise.resolve(undefined),
|
|
target === 'deepseek-harness'
|
|
? runtimeExtensionStore.getEnabledExtensions()
|
|
: Promise.resolve([])
|
|
])
|
|
return createAgentRuntime(defaultWorkspace, settings, {
|
|
skillInstructions: skillContext.instructions,
|
|
skillPackages: skillContext.packages,
|
|
mcpServers,
|
|
continueHostCacheRoot: join(
|
|
app.getPath('userData'),
|
|
'continue-host'
|
|
),
|
|
bundledRuntimePaths,
|
|
continueHostLauncher: launchContinueHost,
|
|
deepseekHarnessLauncher: launchDeepSeekHarness,
|
|
deepseekHarnessExtensions,
|
|
browserService:
|
|
browserCapability?.enabled && browserCapability.supported
|
|
? browserService
|
|
: undefined,
|
|
knowledgeGateway: startupKnowledgeGateway,
|
|
webSearchEnabled: webSearchCapability?.enabled,
|
|
executionSpace,
|
|
workspaceAccess: executionSpace?.workspaceAccess,
|
|
directModelSubagentScheduler
|
|
})
|
|
}
|
|
const createConfiguredRuntime = async (
|
|
resolvedSettings?: ResolvedRuntimeSettings
|
|
): Promise<AgentRuntime> => {
|
|
const settings =
|
|
resolvedSettings ?? await settingsStore.getResolvedSettings()
|
|
return createRuntimeWithCapabilities(
|
|
settings,
|
|
getConfiguredRuntimeTarget(settings)
|
|
)
|
|
}
|
|
const createSelectedRuntime = async (
|
|
selection: AgentRuntimeSelection,
|
|
executionSpace?: ExecutionSpaceDescriptor
|
|
): Promise<AgentRuntime> => {
|
|
const resolved = applyRuntimeSelection(
|
|
await settingsStore.getResolvedSettings(),
|
|
selection
|
|
)
|
|
if (executionSpace?.kind === 'ssh') {
|
|
const profile = resolved.settings.opencodeModelProfile
|
|
if (profile === undefined) {
|
|
throw new Error(
|
|
'托管远程 OpenCode 需要选择一个可用的文本模型配置'
|
|
)
|
|
}
|
|
return await startupManagedRemoteExecutionServices.createRuntime({
|
|
executionSpace,
|
|
selection,
|
|
modelProfile: profile,
|
|
usage: {
|
|
provider: profile.protocol,
|
|
model: profile.modelName
|
|
}
|
|
})
|
|
}
|
|
return createRuntimeWithCapabilities(
|
|
executionSpace?.kind === 'local'
|
|
? {
|
|
...resolved.settings,
|
|
workspacePath: executionSpace.rootPath
|
|
}
|
|
: resolved.settings,
|
|
resolved.target,
|
|
executionSpace
|
|
)
|
|
}
|
|
const configuredRuntime = await runStartupPrerequisites({
|
|
prepareDeepSeekHome: async () => {
|
|
await mkdir(deepSeekHarnessHome, {
|
|
recursive: true,
|
|
mode: 0o700
|
|
})
|
|
},
|
|
initializeKnowledgeAndGateway: async () => {
|
|
await startupKnowledgeService.initialize()
|
|
const embeddingProvider = await createEmbeddingProvider(
|
|
initialResolvedSettings,
|
|
startupEmbeddingModelManager
|
|
)
|
|
const rerankProvider = createRerankProvider(
|
|
initialResolvedSettings
|
|
)
|
|
await Promise.all([
|
|
startupKnowledgeService.setEmbeddingProvider(
|
|
embeddingProvider
|
|
).then(
|
|
() => {
|
|
activeEmbeddingProvider = embeddingProvider
|
|
},
|
|
() => undefined
|
|
),
|
|
startupKnowledgeService.setRerankProvider(
|
|
rerankProvider
|
|
).then(
|
|
() => {
|
|
activeRerankProvider = rerankProvider
|
|
},
|
|
() => undefined
|
|
)
|
|
])
|
|
await startupKnowledgeGateway.start()
|
|
},
|
|
hydrateConfiguredRuntime: () =>
|
|
createConfiguredRuntime(initialResolvedSettings),
|
|
initializeAssistant: () => {
|
|
startupAssistantDatabase.initialize(defaultWorkspace)
|
|
startupAssistantDatabase.ensureChannelProjects(
|
|
defaultWorkspace,
|
|
initialRuntimeSettings.defaultModelProfileId
|
|
)
|
|
channelSettingsStore.reportRuntimeSelectionRepairs(
|
|
startupAssistantDatabase.repairConversationRuntimeSelections(
|
|
initialRuntimeSettings
|
|
)
|
|
)
|
|
}
|
|
})
|
|
const subagentService = new SubagentService(
|
|
createDefaultModelRuntime(
|
|
defaultWorkspace,
|
|
initialResolvedSettings
|
|
),
|
|
startupAssistantDatabase,
|
|
undefined,
|
|
createSubagentProfileRuntimes(
|
|
defaultWorkspace,
|
|
initialResolvedSettings
|
|
)
|
|
)
|
|
runtime = new AgentRuntimeController(
|
|
configuredRuntime,
|
|
undefined,
|
|
observeDesktopFailure
|
|
)
|
|
selectedRuntimeManager = new SelectedRuntimeManager(
|
|
createSelectedRuntime,
|
|
undefined,
|
|
undefined,
|
|
observeDesktopFailure
|
|
)
|
|
const contextManager = new ContextManager({
|
|
parseDocument: documentParsingService.parse
|
|
})
|
|
const approvalBroker = new ToolApprovalBroker()
|
|
|
|
const shortcutSettingsService = new ShortcutSettingsService(
|
|
new ShortcutSettingsStore(
|
|
join(app.getPath('userData'), 'shortcut-settings.json')
|
|
),
|
|
globalShortcut,
|
|
() => {
|
|
if (mainWindow) {
|
|
toggleWindow(mainWindow)
|
|
}
|
|
},
|
|
process.platform
|
|
)
|
|
await shortcutSettingsService.initialize()
|
|
|
|
let runtimeReconfigurationQueue: Promise<void> = Promise.resolve()
|
|
let runtimeReconfigurationClosing = false
|
|
const reconfigureRuntimes = (): Promise<void> => {
|
|
const operation = runtimeReconfigurationQueue.then(async () => {
|
|
if (runtimeReconfigurationClosing) {
|
|
throw new Error('Runtime 配置正在关闭')
|
|
}
|
|
const settings = await settingsStore.getResolvedSettings()
|
|
const nextEmbeddingProvider =
|
|
await createEmbeddingProvider(
|
|
settings,
|
|
startupEmbeddingModelManager
|
|
)
|
|
const nextRerankProvider = createRerankProvider(settings)
|
|
let nextRuntime: AgentRuntime | undefined
|
|
let nextSubagentRuntime: AgentRuntime | undefined
|
|
let nextSubagentProfileRuntimes:
|
|
| ReadonlyMap<string, AgentRuntime>
|
|
| undefined
|
|
try {
|
|
nextSubagentRuntime = createDefaultModelRuntime(
|
|
defaultWorkspace,
|
|
settings
|
|
)
|
|
nextSubagentProfileRuntimes =
|
|
createSubagentProfileRuntimes(
|
|
defaultWorkspace,
|
|
settings
|
|
)
|
|
if (runtime) {
|
|
nextRuntime = await createConfiguredRuntime(settings)
|
|
}
|
|
} catch (error) {
|
|
await Promise.allSettled([
|
|
nextRuntime?.dispose(),
|
|
nextSubagentRuntime?.dispose(),
|
|
...[
|
|
...(nextSubagentProfileRuntimes?.values() ?? [])
|
|
].map((candidate) => candidate.dispose())
|
|
])
|
|
throw error
|
|
}
|
|
|
|
let runtimeConsumed = false
|
|
let subagentRuntimesConsumed = false
|
|
try {
|
|
if (knowledgeService) {
|
|
await Promise.all([
|
|
knowledgeService.setEmbeddingProvider(
|
|
nextEmbeddingProvider
|
|
),
|
|
knowledgeService.setRerankProvider(
|
|
nextRerankProvider
|
|
)
|
|
])
|
|
}
|
|
if (runtime && nextRuntime) {
|
|
runtimeConsumed = true
|
|
await runtime.replace(nextRuntime)
|
|
}
|
|
subagentRuntimesConsumed = true
|
|
await subagentService.replaceRuntimes(
|
|
nextSubagentRuntime,
|
|
nextSubagentProfileRuntimes
|
|
)
|
|
await selectedRuntimeManager?.reset()
|
|
const previousEmbeddingProvider = activeEmbeddingProvider
|
|
activeEmbeddingProvider = nextEmbeddingProvider
|
|
activeRerankProvider = nextRerankProvider
|
|
await previousEmbeddingProvider?.dispose?.()
|
|
} catch (activationError) {
|
|
const rollbackResults = knowledgeService
|
|
? await Promise.allSettled([
|
|
knowledgeService.setEmbeddingProvider(
|
|
activeEmbeddingProvider
|
|
),
|
|
knowledgeService.setRerankProvider(
|
|
activeRerankProvider
|
|
)
|
|
])
|
|
: []
|
|
await Promise.allSettled([
|
|
nextEmbeddingProvider?.dispose?.(),
|
|
runtimeConsumed ? undefined : nextRuntime?.dispose(),
|
|
subagentRuntimesConsumed
|
|
? undefined
|
|
: nextSubagentRuntime.dispose(),
|
|
...(subagentRuntimesConsumed
|
|
? []
|
|
: [...nextSubagentProfileRuntimes.values()].map(
|
|
(candidate) => candidate.dispose()
|
|
))
|
|
])
|
|
const rollbackErrors = rollbackResults.flatMap((result) =>
|
|
result.status === 'rejected' ? [result.reason] : []
|
|
)
|
|
if (rollbackErrors.length > 0) {
|
|
throw new AggregateError(
|
|
[activationError, ...rollbackErrors],
|
|
'Runtime 激活失败,且模型服务回滚未能完成',
|
|
{ cause: activationError }
|
|
)
|
|
}
|
|
throw activationError
|
|
}
|
|
})
|
|
runtimeReconfigurationQueue = operation.catch(() => undefined)
|
|
return operation
|
|
}
|
|
stopRuntimeReconfiguration = async () => {
|
|
runtimeReconfigurationClosing = true
|
|
await runtimeReconfigurationQueue
|
|
}
|
|
const setCurrentEmbeddingConnection = async (
|
|
connectionId: string
|
|
): Promise<void> => {
|
|
const previous = await settingsStore.getPublicSettings()
|
|
if (previous.activeEmbeddingConnectionId === connectionId) {
|
|
return
|
|
}
|
|
await settingsStore.update(
|
|
runtimeSettingsInputWithEmbeddingSelection(
|
|
previous,
|
|
connectionId
|
|
)
|
|
)
|
|
try {
|
|
await reconfigureRuntimes()
|
|
} catch (activationError) {
|
|
try {
|
|
await settingsStore.update(
|
|
runtimeSettingsInputWithEmbeddingSelection(
|
|
previous,
|
|
previous.activeEmbeddingConnectionId ??
|
|
connectionId
|
|
)
|
|
)
|
|
await reconfigureRuntimes()
|
|
} catch (rollbackError) {
|
|
throw new AggregateError(
|
|
[activationError, rollbackError],
|
|
'向量模型连接激活失败,且回滚未能完成',
|
|
{ cause: rollbackError }
|
|
)
|
|
}
|
|
throw activationError
|
|
}
|
|
}
|
|
|
|
const remoteProjectSaveService =
|
|
startupManagedRemoteExecutionServices.createProjectSaveService({
|
|
database: startupAssistantDatabase,
|
|
notify: sendRemoteProjectSaveProgress
|
|
})
|
|
const remoteEnvironmentPreparer =
|
|
new RemoteEnvironmentPreparer({
|
|
resolver: startupRemoteAgentServices.targetResolver,
|
|
sshPool: startupRemoteAgentServices.sshPool,
|
|
agentPackageManager,
|
|
operationStore:
|
|
new RemoteEnvironmentOperationStore(
|
|
app.getPath('userData')
|
|
)
|
|
})
|
|
const remoteEnvironmentUpdateService =
|
|
new RemoteEnvironmentUpdateService(
|
|
remoteEnvironmentPreparer,
|
|
async (hostId) => {
|
|
startupManagedRemoteExecutionServices.invalidateHost(hostId)
|
|
const invalidations = [
|
|
startupRemoteAgentServices.invalidateHost(hostId)
|
|
]
|
|
if (selectedRuntimeManager) {
|
|
invalidations.push(
|
|
selectedRuntimeManager.invalidateHost(hostId)
|
|
)
|
|
}
|
|
await Promise.all(invalidations)
|
|
}
|
|
)
|
|
terminalSessionManager = new TerminalSessionManager({
|
|
database: startupAssistantDatabase,
|
|
executionSpaceResolver,
|
|
targetResolver: startupRemoteAgentServices.targetResolver,
|
|
sshPool: startupRemoteAgentServices.sshPool,
|
|
remoteEnabled: async () =>
|
|
(await applicationSettingsStore.get()).remoteProjectsEnabled,
|
|
deliverEvent: (ownerWebContentsId, event) => {
|
|
const targetWindow = mainWindow
|
|
if (
|
|
targetWindow &&
|
|
!targetWindow.isDestroyed() &&
|
|
targetWindow.webContents.id === ownerWebContentsId &&
|
|
!targetWindow.webContents.isDestroyed()
|
|
) {
|
|
targetWindow.webContents.send(ipcChannels.terminalEvent, event)
|
|
}
|
|
}
|
|
})
|
|
removeIpcHandlers = registerIpcHandlers(
|
|
mainWindow,
|
|
runtime,
|
|
legacyDefaultShortcut,
|
|
settingsStore,
|
|
capabilityService,
|
|
contextManager,
|
|
knowledgeService,
|
|
assistantDatabase,
|
|
approvalBroker,
|
|
bundledRuntimePaths,
|
|
reconfigureRuntimes,
|
|
async () => {
|
|
await Promise.all([
|
|
browserService?.clearSessions(),
|
|
startupFeedbackService.clear()
|
|
])
|
|
},
|
|
browserService,
|
|
subagentService,
|
|
channelSettingsStore,
|
|
applicationSettingsStore,
|
|
versionChecker,
|
|
speechModelManager,
|
|
undefined,
|
|
selectedRuntimeManager,
|
|
speechTranscriptionService,
|
|
knowledgeGateway,
|
|
launchWechatSidecar,
|
|
documentParsingService,
|
|
documentOcrModelManager,
|
|
documentOcrBroker,
|
|
releaseNotesService,
|
|
goodbuddyConfigService,
|
|
runtimeExtensionStore,
|
|
shortcutSettingsService,
|
|
sshHostService,
|
|
executionSpaceResolver,
|
|
remoteProjectSaveService,
|
|
sshHostDirectoryBrowser,
|
|
sshHostRemoteEnvironmentInspector,
|
|
startupEmbeddingModelManager,
|
|
async (connectionId) => {
|
|
const settings = await settingsStore.getResolvedSettings()
|
|
const provider = await createEmbeddingProvider(
|
|
settings,
|
|
startupEmbeddingModelManager,
|
|
connectionId
|
|
)
|
|
if (!provider) {
|
|
throw new Error('请先启用并保存向量模型设置')
|
|
}
|
|
return provider
|
|
},
|
|
setCurrentEmbeddingConnection,
|
|
remoteEnvironmentUpdateService,
|
|
agentPackageManager,
|
|
startupRemoteAgentServices.connectionManager,
|
|
terminalSessionManager
|
|
)
|
|
removeFeedbackIpcHandler = registerFeedbackIpcHandler(
|
|
mainWindow,
|
|
startupFeedbackService
|
|
)
|
|
loadMainWindow(mainWindow)
|
|
setImmediate(() => {
|
|
void repairStaleWindowsNotificationShortcuts({
|
|
platform: process.platform,
|
|
installed: installedWindowsBuild,
|
|
executablePath: process.execPath,
|
|
programsDirectory: join(
|
|
app.getPath('appData'),
|
|
'Microsoft',
|
|
'Windows',
|
|
'Start Menu',
|
|
'Programs'
|
|
),
|
|
shortcutAccess: {
|
|
readShortcutLink: (shortcutPath) =>
|
|
shell.readShortcutLink(shortcutPath),
|
|
writeShortcutLink: (
|
|
shortcutPath,
|
|
operation,
|
|
options
|
|
) =>
|
|
shell.writeShortcutLink(
|
|
shortcutPath,
|
|
operation,
|
|
options
|
|
)
|
|
}
|
|
}).then(
|
|
({ failed }) => {
|
|
if (failed > 0) {
|
|
console.warn(
|
|
`Failed to repair ${failed} stale notification shortcut(s)`
|
|
)
|
|
}
|
|
},
|
|
(error: unknown) => {
|
|
console.warn(
|
|
'Failed to inspect stale notification shortcuts',
|
|
error
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
app.on('activate', () => {
|
|
if (mainWindow) {
|
|
showWindow(mainWindow)
|
|
}
|
|
})
|
|
}).catch((error: unknown) => {
|
|
void desktopDiagnostics.recordFailure({
|
|
component: 'desktop',
|
|
stage: 'startup',
|
|
code: 'desktop.startup.failed',
|
|
error
|
|
}).catch(() => undefined)
|
|
console.error(
|
|
'GoodBuddy startup failed',
|
|
createStartupFailureDiagnostic(error)
|
|
)
|
|
dialog.showErrorBox(
|
|
'GoodBuddy 启动失败',
|
|
formatStartupFailureMessage(error)
|
|
)
|
|
app.quit()
|
|
})
|
|
}
|
|
|
|
let cleanupStarted = false
|
|
let cleanupComplete = false
|
|
|
|
app.on('before-quit', (event) => {
|
|
isQuitting = true
|
|
if (cleanupComplete) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
if (cleanupStarted) {
|
|
return
|
|
}
|
|
cleanupStarted = true
|
|
void (async () => {
|
|
try {
|
|
const cleanup = settleCleanupPhases([
|
|
[
|
|
() => feedbackService?.dispose(),
|
|
() => removeFeedbackIpcHandler?.(),
|
|
() => dshExtensionInstaller?.dispose(),
|
|
() => removeIpcHandlers?.()
|
|
],
|
|
[() => stopRuntimeReconfiguration?.()],
|
|
[
|
|
() => runtime?.detachForApplicationExit(),
|
|
() => selectedRuntimeManager?.detachForApplicationExit()
|
|
],
|
|
[
|
|
() => runtime?.dispose(),
|
|
() => selectedRuntimeManager?.dispose(),
|
|
() => {
|
|
directModelSubagentScheduler?.dispose()
|
|
return directModelSubagentScheduler?.waitForIdle()
|
|
},
|
|
() => browserService?.dispose(),
|
|
() => globalTlsPolicy?.dispose(),
|
|
() =>
|
|
Promise.allSettled(
|
|
[...embeddingBrokers].map((broker) => broker.shutdown())
|
|
).then(() => undefined),
|
|
() => embeddingModelManager?.dispose(),
|
|
() => documentOcrModelManager?.dispose(),
|
|
() => documentOcrBroker?.dispose()
|
|
],
|
|
[() => terminalSessionManager?.dispose()],
|
|
[() => managedRemoteExecutionServices?.dispose()],
|
|
[() => remoteAgentServices?.dispose()],
|
|
[() => knowledgeGateway?.dispose()],
|
|
[() => knowledgeService?.dispose()],
|
|
[() => desktopDiagnostics.dispose()]
|
|
])
|
|
globalShortcut.unregisterAll()
|
|
tray?.destroy()
|
|
await runCleanupBeforeDeadline(cleanup, 8_000, () => {
|
|
assistantDatabase?.close()
|
|
})
|
|
} finally {
|
|
cleanupComplete = true
|
|
app.exit(0)
|
|
}
|
|
})()
|
|
})
|
|
|
|
app.on('will-quit', () => {
|
|
cleanupComplete = true
|
|
})
|