feat: prepare GoodBuddy 0.8.0

This commit is contained in:
lofyer
2026-08-05 19:40:40 +08:00
parent 38ac2206f2
commit 39a457ded8
96 changed files with 14221 additions and 1027 deletions
+59 -5
View File
@@ -26,6 +26,40 @@ export const projectUpdateSchema = projectCreateSchema
export type ProjectCreateInput = z.infer<typeof projectCreateSchema>
export const conversationAttachmentSchema = z
.object({
id: assistantIdSchema,
name: z.string().trim().min(1).max(500),
size: z.number().int().nonnegative().max(12 * 1024 * 1024),
preview: z.string().max(500),
kind: z.enum(['text', 'image']),
thumbnailUrl: z
.string()
.max(2_000_000)
.refine(
(value) =>
value.startsWith('data:image/png;base64,') ||
value.startsWith('data:image/jpeg;base64,'),
'会话附件缩略图格式无效'
)
.optional(),
contentUrl: z
.string()
.max(400_000)
.refine(
(value) =>
value.startsWith('data:image/png;base64,') ||
value.startsWith('data:image/jpeg;base64,'),
'会话附件图片格式无效'
)
.optional()
})
.strict()
export type ConversationAttachment = z.infer<
typeof conversationAttachmentSchema
>
export const conversationSnapshotSchema = z
.object({
id: assistantIdSchema,
@@ -57,7 +91,8 @@ export const conversationSnapshotSchema = z
'cancelled',
'interrupted'
]),
summary: z.string().max(2_000)
summary: z.string().max(2_000),
error: z.string().max(2_000).optional()
})
.strict()
)
@@ -90,7 +125,11 @@ export const conversationSnapshotSchema = z
)
.max(20)
.optional(),
artifactIds: z.array(assistantIdSchema).max(8).optional()
artifactIds: z.array(assistantIdSchema).max(8).optional(),
attachments: z
.array(conversationAttachmentSchema)
.max(8)
.optional()
})
.strict()
)
@@ -162,6 +201,9 @@ export type AssistantTask = {
id: string
projectId?: string
conversationId?: string
parentTaskId?: string
expertId?: string
routingMode?: 'manual' | 'smart'
title: string
instructions: string
origin: 'user' | 'assistant' | 'schedule' | 'delegation' | 'subagent'
@@ -434,18 +476,30 @@ export type AssistantHeartbeatEntry = {
createdAt: string
}
const routingKeywordSchema = z
.string()
.transform((value) =>
value.normalize('NFKC').trim().replace(/\s+/gu, ' ').toLowerCase()
)
.pipe(z.string().min(2).max(48))
export const expertCreateSchema = z
.object({
name: z.string().trim().min(1).max(80),
description: z.string().trim().max(500),
systemInstructions: z.string().trim().min(1).max(20_000)
systemInstructions: z.string().trim().min(1).max(20_000),
routingKeywords: z
.array(routingKeywordSchema)
.max(32)
.default([])
.transform((keywords) => [...new Set(keywords)])
})
.strict()
export type ExpertCreateInput = z.infer<typeof expertCreateSchema>
export type ExpertCreateInput = z.input<typeof expertCreateSchema>
export type ExpertUpdateInput = ExpertCreateInput
export type AssistantExpert = ExpertCreateInput & {
export type AssistantExpert = z.output<typeof expertCreateSchema> & {
id: string
enabled: boolean
createdAt: string
+1 -1
View File
@@ -64,7 +64,7 @@ export const builtinModelTools = [
{
name: 'browser_screenshot',
displayName: '截取浏览器页面',
description: '截取当前可见页面区域的有界 PNG 图片。',
description: '截取当前可见页面区域、约 200KB 的有界 JPEG 图片。',
access: 'read'
}
] as const satisfies readonly BuiltinModelToolSummary[]
+100
View File
@@ -0,0 +1,100 @@
import { z } from 'zod'
export const CHANNEL_LIMITS = {
maximumChannelLength: 64,
maximumEventIdLength: 256,
maximumIdentityLength: 256,
maximumTextLength: 32_000,
maximumResultLength: 16_000,
maximumErrorLength: 1_000,
maximumStatusLength: 64
} as const
const channelIdentifierSchema = z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumIdentityLength)
export const channelWorkModeSchema = z.enum(['ask', 'plan'])
export type ChannelWorkMode = z.infer<typeof channelWorkModeSchema>
export const channelInboundTextSchema = z
.object({
channel: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumChannelLength),
eventId: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumEventIdLength),
senderId: channelIdentifierSchema,
conversationId: channelIdentifierSchema,
conversationType: z.enum(['direct', 'group']),
text: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumTextLength),
mentioned: z.boolean().default(false),
workMode: channelWorkModeSchema.default('ask'),
receivedAt: z.number().int().nonnegative().optional()
})
.strict()
export type ChannelInboundText = z.infer<
typeof channelInboundTextSchema
>
export const channelExecutorResultSchema = z
.object({
status: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumStatusLength),
output: z.string().optional(),
error: z.string().optional()
})
.strict()
export type ChannelExecutorResult = z.infer<
typeof channelExecutorResultSchema
>
export const channelResultMessageSchema = z
.object({
channel: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumChannelLength),
eventId: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumEventIdLength),
conversationId: channelIdentifierSchema,
recipientId: channelIdentifierSchema,
status: z
.string()
.trim()
.min(1)
.max(CHANNEL_LIMITS.maximumStatusLength),
output: z
.string()
.max(CHANNEL_LIMITS.maximumResultLength)
.optional(),
error: z
.string()
.max(CHANNEL_LIMITS.maximumErrorLength)
.optional()
})
.strict()
export type ChannelResultMessage = z.infer<
typeof channelResultMessageSchema
>
+78 -12
View File
@@ -23,6 +23,7 @@ import {
type AssistantTask,
type TokenUsageSummary,
type ConversationSnapshot,
type ConversationAttachment,
type WorkspaceChanges,
type WorkspaceDirectoryListing,
type WorkspaceFilePreview,
@@ -75,6 +76,7 @@ export const agentRequestSchema = z
projectId: z.string().uuid().optional(),
expertId: z.string().uuid().optional(),
teamMode: z.boolean().optional(),
smartRouting: z.boolean().optional(),
workMode: workModeSchema.optional(),
prompt: z.string().trim().min(1).max(100_000),
contextIds: z.array(z.string().uuid()).max(8).optional(),
@@ -131,10 +133,19 @@ export const modelProtocolSchema = z.enum([
'openai-images-generations'
])
export const modelAuthenticationSchema = z.enum(['api-key', 'none'])
export const imageGenerationQualitySchema = z.enum([
'auto',
'low',
'medium',
'high'
])
export type ModelProtocol = z.infer<typeof modelProtocolSchema>
export type ModelAuthentication = z.infer<
typeof modelAuthenticationSchema
>
export type ImageGenerationQuality = z.infer<
typeof imageGenerationQualitySchema
>
export const defaultModelProfileId =
'00000000-0000-4000-8000-000000000001'
@@ -144,6 +155,7 @@ export const defaultRuntimeSettings = {
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
imageGenerationQuality: 'auto',
opencodeBaseUrl: '',
opencodeEmbedded: false,
opencodeBinaryPath: '',
@@ -152,8 +164,10 @@ export const defaultRuntimeSettings = {
continueConfigPath: '',
continueMode: 'chat',
runtimeSandboxMode: 'auto',
subagentSmartRoutingEnabled: false,
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl: 'http://127.0.0.1:11434',
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
knowledgeEmbeddingModel: 'nomic-embed-text',
workspacePath: '',
toolApproval: 'always'
@@ -220,6 +234,7 @@ const modelProfileInputSchema = z
.regex(/^[\w./:-]+$/, '模型名称包含不支持的字符'),
protocol: modelProtocolSchema,
authentication: modelAuthenticationSchema,
imageGenerationQuality: imageGenerationQualitySchema,
apiKey: modelApiKeyUpdateSchema
})
.strict()
@@ -246,6 +261,7 @@ export const runtimeSettingsInputSchema = z
.regex(/^[\w./:-]+$/, '模型名称包含不支持的字符'),
modelProtocol: modelProtocolSchema,
modelAuthentication: modelAuthenticationSchema,
imageGenerationQuality: imageGenerationQualitySchema,
opencodeBaseUrl: z.union([
z.literal(''),
z.string().url().max(2_048)
@@ -257,6 +273,7 @@ export const runtimeSettingsInputSchema = z
continueConfigPath: runtimePathSchema,
continueMode: continueModeSchema,
runtimeSandboxMode: runtimeSandboxModeSchema,
subagentSmartRoutingEnabled: z.boolean().optional(),
knowledgeEmbeddingEnabled: z.boolean(),
knowledgeEmbeddingBaseUrl: z.string().url().max(2_048),
knowledgeEmbeddingModel: z
@@ -265,6 +282,7 @@ export const runtimeSettingsInputSchema = z
.min(1)
.max(256)
.regex(/^[\w./:-]+$/, '向量模型名称包含不支持的字符'),
knowledgeEmbeddingApiKey: modelApiKeyUpdateSchema.optional(),
workspacePath: z.string().trim().min(1).max(4_096),
apiKey: modelApiKeyUpdateSchema,
modelProfiles: z.array(modelProfileInputSchema).min(1).max(20).optional(),
@@ -440,18 +458,19 @@ export const runtimeSettingsInputSchema = z
embeddingUrl.password ||
embeddingUrl.search ||
embeddingUrl.hash ||
(embeddingUrl.pathname !== '/' && embeddingUrl.pathname !== '')
embeddingUrl.pathname === '/' ||
embeddingUrl.pathname === ''
) {
context.addIssue({
code: 'custom',
path: ['knowledgeEmbeddingBaseUrl'],
message:
'Ollama 向量地址必须使用 HTTPS,或使用本机/私有网络 HTTP origin,且不得包含凭据、路径、查询参数或片段'
'向量接口 URL 必须是完整的 HTTPS 端点;本机私有网络可使用 HTTP,且不得包含凭据、查询参数或片段'
})
}
})
export type RuntimeSettingsInput = z.infer<typeof runtimeSettingsInputSchema>
export type RuntimeSettingsInput = z.input<typeof runtimeSettingsInputSchema>
export type RuntimeModelSource = z.infer<typeof runtimeModelSourceSchema>
@@ -462,6 +481,7 @@ export type ModelConnectionSettings = {
modelName: string
protocol: ModelProtocol
authentication: ModelAuthentication
imageGenerationQuality: ImageGenerationQuality
apiKeyConfigured: boolean
credentialSource: 'none' | 'encrypted' | 'environment'
}
@@ -472,6 +492,7 @@ export type RuntimeSettings = {
modelName: string
modelProtocol: ModelProtocol
modelAuthentication: ModelAuthentication
imageGenerationQuality: ImageGenerationQuality
opencodeBaseUrl: string
opencodeEmbedded: boolean
opencodeBinaryPath: string
@@ -480,9 +501,12 @@ export type RuntimeSettings = {
continueConfigPath: string
continueMode: RuntimeSettingsInput['continueMode']
runtimeSandboxMode: RuntimeSettingsInput['runtimeSandboxMode']
subagentSmartRoutingEnabled: boolean
knowledgeEmbeddingEnabled: boolean
knowledgeEmbeddingBaseUrl: string
knowledgeEmbeddingModel: string
knowledgeEmbeddingApiKeyConfigured: boolean
knowledgeEmbeddingCredentialSource: 'none' | 'encrypted' | 'environment'
workspacePath: string
apiKeyConfigured: boolean
credentialSource: 'none' | 'encrypted' | 'environment'
@@ -495,13 +519,30 @@ export type RuntimeSettings = {
warning?: string
}
export type ContextAttachment = {
export type ContextAttachment = ConversationAttachment
export const windowCaptureSourceIdSchema = z
.string()
.min(1)
.max(512)
.refine(
(value) =>
[...value].every((character) => {
const code = character.charCodeAt(0)
return code > 31 && code !== 127
}),
'窗口来源 ID 无效'
)
export const windowCaptureRequestSchema = z
.object({
sourceId: windowCaptureSourceIdSchema
})
.strict()
export type WindowCaptureOption = {
id: string
name: string
size: number
preview: string
kind: 'text' | 'image'
thumbnailUrl?: string
}
export type AgentRuntimeStatus = {
@@ -541,6 +582,28 @@ export const approvalDecisionSchema = z.enum([
export type ApprovalDecision = z.infer<typeof approvalDecisionSchema>
export const subagentEventSchema = z
.object({
requestId: z.string().uuid(),
type: z.literal('subagent'),
childTaskId: z.string().uuid(),
expertId: z.string().uuid(),
expertName: z.string().trim().min(1).max(80),
routingMode: z.enum(['manual', 'smart']),
state: z.enum([
'queued',
'running',
'completed',
'failed',
'cancelled'
]),
reason: z.string().trim().min(1).max(240).optional(),
error: z.string().trim().min(1).max(1_000).optional()
})
.strict()
export type SubagentEvent = z.infer<typeof subagentEventSchema>
export type AgentEvent =
| {
requestId: string
@@ -564,6 +627,7 @@ export type AgentEvent =
| 'failed'
| 'recoverable'
summary: string
error?: string
}
| {
requestId: string
@@ -593,6 +657,7 @@ export type AgentEvent =
status: 'failed' | 'cancelled'
message: string
}
| SubagentEvent
export type AppInfo = {
name: string
@@ -616,9 +681,9 @@ export const browserLiveStateSchema = z
url: z.string().max(2_048).optional(),
frameDataUrl: z
.string()
.max(7_000_000)
.max(400_000)
.refine(
(value) => value.startsWith('data:image/png;base64,'),
(value) => value.startsWith('data:image/jpeg;base64,'),
'浏览器画面格式无效'
)
.optional(),
@@ -936,7 +1001,8 @@ export type DesktopApi = {
context: {
selectFiles: () => Promise<ContextAttachment[]>
captureScreen: () => Promise<ContextAttachment>
captureWindow: () => Promise<ContextAttachment>
listWindows: () => Promise<WindowCaptureOption[]>
captureWindow: (sourceId: string) => Promise<ContextAttachment>
readClipboard: () => Promise<ContextAttachment>
remove: (contextId: string) => Promise<void>
}
+1
View File
@@ -75,6 +75,7 @@ export const ipcChannels = {
capabilitiesRemoveBrowserProfile: 'capabilities:browser-profile:remove',
contextSelectFiles: 'context:select-files',
contextCaptureScreen: 'context:capture-screen',
contextListWindows: 'context:list-windows',
contextCaptureWindow: 'context:capture-window',
contextReadClipboard: 'context:read-clipboard',
contextRemove: 'context:remove',