Files
goodbuddy/src/shared/assistant-contracts.ts
T
2026-08-05 19:40:40 +08:00

508 lines
12 KiB
TypeScript

import { z } from 'zod'
export const assistantIdSchema = z.string().uuid()
export const workModeSchema = z.enum(['ask', 'plan', 'execute'])
export const interactiveWorkModes = ['ask', 'execute'] as const
export type WorkMode = z.infer<typeof workModeSchema>
export type InteractiveWorkMode = (typeof interactiveWorkModes)[number]
export function normalizeInteractiveWorkMode(
workMode: WorkMode | undefined
): InteractiveWorkMode {
return workMode === 'execute' ? 'execute' : 'ask'
}
export const projectCreateSchema = z
.object({
name: z.string().trim().min(1).max(120),
description: z.string().trim().max(2_000),
rootPath: z.string().trim().max(4_096),
defaultWorkMode: workModeSchema
})
.strict()
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,
projectId: assistantIdSchema.optional(),
title: z.string().trim().min(1).max(200),
updatedAt: z.number().int().nonnegative(),
messages: z
.array(
z
.object({
id: assistantIdSchema,
role: z.enum(['user', 'assistant']),
content: z.string().max(1_000_000),
createdAt: z.number().int().nonnegative(),
state: z.enum(['streaming', 'complete', 'error']),
status: z.string().max(4_000).optional(),
tools: z
.array(
z
.object({
callId: z.string().max(256).optional(),
name: z.string().max(200),
state: z.enum([
'pending',
'running',
'completed',
'failed',
'recoverable',
'cancelled',
'interrupted'
]),
summary: z.string().max(2_000),
error: z.string().max(2_000).optional()
})
.strict()
)
.max(100)
.optional(),
sources: z.array(z.string().max(8_192)).max(100).optional(),
sourceReferences: z
.array(
z
.object({
libraryId: assistantIdSchema,
libraryName: z.string().max(200),
documentId: assistantIdSchema,
documentName: z.string().max(500),
sourceName: z.string().max(500),
sourceLocation: z.string().max(4_096).optional(),
locator: z.string().max(1_000).optional(),
snippet: z.string().max(16_000),
rank: z.number().finite(),
retrievalChannels: z
.array(z.enum(['fts', 'vector', 'graph']))
.max(3)
.optional(),
evidenceIds: z
.array(assistantIdSchema)
.max(100)
.optional()
})
.strict()
)
.max(20)
.optional(),
artifactIds: z.array(assistantIdSchema).max(8).optional(),
attachments: z
.array(conversationAttachmentSchema)
.max(8)
.optional()
})
.strict()
)
.max(500)
})
.strict()
export type ConversationSnapshot = z.infer<
typeof conversationSnapshotSchema
>
export const conversationSnapshotsSchema = z
.array(conversationSnapshotSchema)
.max(100)
export type AssistantProject = ProjectCreateInput & {
id: string
status: 'active' | 'archived'
createdAt: string
updatedAt: string
}
export type WorkspaceChanges = {
rootPath: string
available: boolean
status: string
patch: string
files: WorkspaceChangedFile[]
truncated: boolean
error?: string
}
export type WorkspaceChangedFile = {
path: string
status: string
previousPath?: string
}
export type WorkspaceDirectoryEntry = {
name: string
path: string
type: 'file' | 'directory'
}
export type WorkspaceDirectoryListing = {
path: string
entries: WorkspaceDirectoryEntry[]
truncated: boolean
}
export type WorkspaceFilePreview = {
path: string
name: string
content: string
mimeType: 'text/markdown' | 'text/plain' | 'application/json'
size: number
}
export type AssistantTaskStatus =
| 'queued'
| 'running'
| 'waiting_approval'
| 'paused'
| 'completed'
| 'failed'
| 'cancelled'
| 'interrupted'
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'
status: AssistantTaskStatus
progress?: number
createdAt: string
startedAt?: string
completedAt?: string
error?: string
}
export type ModelUsageCallInput = {
requestId: string
callId: string
runtime: string
provider: string
model: string
input: number
output: number
cacheRead: number
cacheWrite: number
}
export type TokenUsageRecord = {
requestId: string
projectId?: string
projectName?: string
conversationId?: string
conversationTitle?: string
runtime: string
provider: string
model: string
callCount: number
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens: number
}
export type TokenUsageSummary = {
totals: {
callCount: number
input: number
output: number
cacheRead: number
cacheWrite: number
totalTokens: number
}
records: TokenUsageRecord[]
}
export type AssistantArtifact = {
id: string
projectId?: string
taskId?: string
kind: 'markdown' | 'text' | 'json' | 'image' | 'file'
title: string
mimeType: string
content?: string
byteSize: number
createdAt: string
updatedAt: string
}
export const memoryCreateSchema = z
.object({
scope: z.enum(['global', 'project', 'conversation']),
scopeId: z.string().max(256).optional(),
type: z.enum(['preference', 'fact', 'summary', 'procedure']),
content: z.string().trim().min(1).max(8_000)
})
.strict()
export type MemoryCreateInput = z.infer<typeof memoryCreateSchema>
export type AssistantMemory = MemoryCreateInput & {
id: string
confidence: number
salience: number
status: 'proposed' | 'confirmed' | 'rejected'
createdAt: string
updatedAt: string
}
export const scheduleCreateSchema = z
.object({
projectId: z.string().uuid().optional(),
title: z.string().trim().min(1).max(120),
prompt: z.string().trim().min(1).max(100_000),
workMode: z.enum(['ask', 'plan']),
recurrence: z.enum(['once', 'daily', 'weekly']),
nextRunAt: z.string().datetime({ offset: true })
})
.strict()
export type ScheduleCreateInput = z.infer<typeof scheduleCreateSchema>
export type AssistantSchedule = ScheduleCreateInput & {
id: string
enabled: boolean
lastRunAt?: string
createdAt: string
updatedAt: string
}
export const heartbeatRecurrenceSchema = z.discriminatedUnion('type', [
z
.object({
type: z.literal('daily'),
localTime: z
.string()
.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/)
})
.strict(),
z
.object({
type: z.literal('weekly'),
localTime: z
.string()
.regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/),
weekday: z.number().int().min(0).max(6)
})
.strict()
])
export const heartbeatCreateSchema = z
.object({
projectId: assistantIdSchema.optional(),
name: z.string().trim().min(1).max(120),
timezone: z.string().trim().min(1).max(100),
recurrence: heartbeatRecurrenceSchema,
enabled: z.boolean(),
lookbackHours: z.number().int().min(1).max(24 * 30),
retentionDays: z.number().int().min(1).max(365)
})
.strict()
export const heartbeatUpdateSchema = heartbeatCreateSchema
export const heartbeatListSchema = z
.object({
projectId: assistantIdSchema.optional()
})
.strict()
export const heartbeatHistorySchema = z
.object({
configId: assistantIdSchema.optional(),
limit: z.number().int().min(1).max(200).default(50)
})
.strict()
export const heartbeatIdSchema = z
.object({
id: assistantIdSchema
})
.strict()
export const heartbeatUpdateRequestSchema = z
.object({
id: assistantIdSchema,
config: heartbeatUpdateSchema
})
.strict()
export const heartbeatPauseSchema = z
.object({
id: assistantIdSchema,
paused: z.boolean()
})
.strict()
export const heartbeatRunNowSchema = z
.object({
id: assistantIdSchema,
idempotencyKey: z.string().trim().min(1).max(200)
})
.strict()
export const heartbeatSummaryOutputSchema = z
.object({
summary: z.string().trim().min(1).max(12_000),
highlights: z.array(z.string().trim().min(1).max(1_000)).max(20),
proposedMemories: z
.array(
z
.object({
scope: z.enum(['global', 'project']),
type: z.enum([
'preference',
'fact',
'summary',
'procedure'
]),
content: z.string().trim().min(1).max(8_000),
confidence: z.number().min(0).max(1),
salience: z.number().min(0).max(1)
})
.strict()
)
.max(10),
followUpTasks: z
.array(
z
.object({
title: z.string().trim().min(1).max(200),
instructions: z.string().trim().min(1).max(8_000)
})
.strict()
)
.max(10)
})
.strict()
export type HeartbeatRecurrence = z.infer<
typeof heartbeatRecurrenceSchema
>
export type HeartbeatCreateInput = z.infer<
typeof heartbeatCreateSchema
>
export type HeartbeatUpdateInput = z.infer<
typeof heartbeatUpdateSchema
>
export type HeartbeatSummaryOutput = z.infer<
typeof heartbeatSummaryOutputSchema
>
export type HeartbeatRunStatus =
| 'claimed'
| 'completed'
| 'failed'
| 'skipped'
export type AssistantHeartbeatConfig = HeartbeatCreateInput & {
id: string
nextRunAt: string
lastRunAt?: string
lastStatus?: HeartbeatRunStatus
createdAt: string
updatedAt: string
}
export type AssistantHeartbeatRun = {
id: string
configId: string
trigger: 'scheduled' | 'manual'
scheduledFor: string
status: HeartbeatRunStatus
attemptCount: number
nextAttemptAt?: string
startedAt?: string
completedAt?: string
error?: string
entryId?: string
createdAt: string
updatedAt: string
}
export type AssistantHeartbeatEntry = {
id: string
configId: string
runId: string
scheduledFor: string
summary: string
highlights: string[]
artifactId?: string
proposedMemoryIds: string[]
followUpTaskIds: string[]
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),
routingKeywords: z
.array(routingKeywordSchema)
.max(32)
.default([])
.transform((keywords) => [...new Set(keywords)])
})
.strict()
export type ExpertCreateInput = z.input<typeof expertCreateSchema>
export type ExpertUpdateInput = ExpertCreateInput
export type AssistantExpert = z.output<typeof expertCreateSchema> & {
id: string
enabled: boolean
createdAt: string
updatedAt: string
}