feat: expand secure runtime and workspace UX

This commit is contained in:
lofyer
2026-08-04 00:57:08 +08:00
parent 09e9fbf5e2
commit 2910d315f7
56 changed files with 6164 additions and 882 deletions
+84 -18
View File
@@ -21,6 +21,8 @@ import {
knowledgeUrlImportSchema,
runtimeFileSelectionKindSchema,
runtimeSettingsInputSchema,
workspaceDirectoryRequestSchema,
workspaceFileRequestSchema,
type AgentRuntimeDetection,
type AgentEvent,
type AppInfo,
@@ -71,11 +73,22 @@ import type { ToolApprovalBroker } from './tool-approval-broker'
import { showWindow } from './window'
import type { AssistantDatabase } from './assistant/assistant-database'
import { RemoteDelegationService } from './assistant/remote-delegation-service'
import { getWorkspaceChanges } from './assistant/workspace-changes-service'
import {
getWorkspaceChanges,
listWorkspaceDirectory,
readWorkspaceFile
} from './assistant/workspace-changes-service'
import { HeartbeatService } from './assistant/heartbeat-service'
const requestIdSchema = z.string().uuid()
function isAgentRuntime(runtime: AgentRuntime): boolean {
return (
runtime.runtimeId === 'opencode' ||
runtime.runtimeId === 'continue'
)
}
function safeRuntimeError(error: unknown, fallback: string): string {
return redactSensitiveText(
error instanceof Error ? error.message : fallback
@@ -352,13 +365,25 @@ export function registerIpcHandlers(
(channel) =>
channel !== ipcChannels.agentEvent &&
channel !== ipcChannels.conversationNew &&
channel !== ipcChannels.settingsOpen
channel !== ipcChannels.settingsOpen &&
channel !== ipcChannels.windowMaximizedChanged
)
for (const channel of channels) {
ipcMain.removeHandler(channel)
}
const notifyMaximizedChanged = (): void => {
if (!window.isDestroyed()) {
window.webContents.send(
ipcChannels.windowMaximizedChanged,
window.isMaximized()
)
}
}
window.on('maximize', notifyMaximizedChanged)
window.on('unmaximize', notifyMaximizedChanged)
const abortActiveRequests = (reason: string): void => {
for (const controller of activeRequests.values()) {
controller.abort(new Error(reason))
@@ -543,10 +568,6 @@ export function registerIpcHandlers(
: 'Work mode: Execute. Tool actions remain subject to GoodBuddy permission controls.'
let output = ''
let completed = false
const toolStates = new Map<
string,
Extract<AgentEvent, { type: 'tool' }>
>()
try {
for await (const agentEvent of runtime.run(
{
@@ -611,18 +632,10 @@ export function registerIpcHandlers(
if (taskEvent.type === 'text') {
output = `${output}${taskEvent.delta}`.slice(0, 1_000_000)
} else if (taskEvent.type === 'tool') {
toolStates.set(taskEvent.callId, taskEvent)
throw new Error('只读定时任务不允许调用工具')
} else if (taskEvent.type === 'error') {
throw new Error(taskEvent.message)
} else if (taskEvent.type === 'done') {
const unsuccessfulTool = [...toolStates.values()].find(
(tool) => tool.state !== 'completed'
)
if (unsuccessfulTool) {
throw new Error(
`${unsuccessfulTool.name} 工具未成功完成,定时任务已失败`
)
}
completed = true
break
}
@@ -888,6 +901,30 @@ export function registerIpcHandlers(
window.hide()
})
ipcMain.handle(ipcChannels.windowMinimize, (event) => {
assertTrustedSender(event, window)
window.minimize()
})
ipcMain.handle(ipcChannels.windowToggleMaximize, (event) => {
assertTrustedSender(event, window)
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
})
ipcMain.handle(ipcChannels.windowClose, (event) => {
assertTrustedSender(event, window)
window.close()
})
ipcMain.handle(ipcChannels.windowIsMaximized, (event): boolean => {
assertTrustedSender(event, window)
return window.isMaximized()
})
ipcMain.handle(ipcChannels.appClearLocalData, async (event) => {
assertTrustedSender(event, window)
executionPaused = true
@@ -916,9 +953,12 @@ export function registerIpcHandlers(
throw new Error('本地数据维护期间暂不接受新任务')
}
const parsedInput = agentRequestSchema.parse(input)
const agentRuntimeSelected = isAgentRuntime(runtime)
const parsedRequest = {
...parsedInput,
workMode: parsedInput.workMode ?? ('ask' as const)
workMode: agentRuntimeSelected
? ('execute' as const)
: (parsedInput.workMode ?? ('ask' as const))
}
if (
parsedRequest.workMode === 'execute' &&
@@ -940,7 +980,9 @@ export function registerIpcHandlers(
: enrichedRequest.workMode === 'plan'
? 'Work mode: Plan. Do not call tools or make changes. Produce a concrete reviewable plan and wait for user confirmation.'
: enrichedRequest.workMode === 'execute'
? 'Work mode: Execute. Follow the approved request; all tool actions remain subject to GoodBuddy permission controls.'
? agentRuntimeSelected
? 'Work mode: Execute. Follow the user request. Agent Runtime tool calls execute without GoodBuddy approval and must remain visible in runtime activity.'
: 'Work mode: Execute. Follow the approved request; all tool actions remain subject to GoodBuddy permission controls.'
: ''
const expertInstruction =
enrichedRequest.expertId && !imageGeneration
@@ -1020,7 +1062,11 @@ export function registerIpcHandlers(
}
const eventStream = request.teamMode
? runExpertTeam(request, controller.signal)
: runtime.run(request, controller.signal, authorize)
: runtime.run(
request,
controller.signal,
agentRuntimeSelected ? undefined : authorize
)
for await (const agentEvent of eventStream) {
if (agentEvent.type === 'model-usage') {
persistModelUsage(agentEvent)
@@ -1321,6 +1367,24 @@ export function registerIpcHandlers(
return getWorkspaceChanges(project.rootPath)
}
)
ipcMain.handle(
ipcChannels.workspaceDirectoryList,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = workspaceDirectoryRequestSchema.parse(input)
const project = assistantDatabase.getProject(value.projectId)
return listWorkspaceDirectory(project.rootPath, value.path)
}
)
ipcMain.handle(
ipcChannels.workspaceFileRead,
async (event, input: unknown) => {
assertTrustedSender(event, window)
const value = workspaceFileRequestSchema.parse(input)
const project = assistantDatabase.getProject(value.projectId)
return readWorkspaceFile(project.rootPath, value.path)
}
)
ipcMain.handle(ipcChannels.tasksList, (event) => {
assertTrustedSender(event, window)
@@ -1979,6 +2043,8 @@ export function registerIpcHandlers(
approvalBroker.clear()
contextManager.clear()
await Promise.allSettled([...activeExecutions])
window.removeListener('maximize', notifyMaximizedChanged)
window.removeListener('unmaximize', notifyMaximizedChanged)
for (const channel of channels) {
ipcMain.removeHandler(channel)
}