118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import type {
|
|
AgentConversation,
|
|
AgentConversationMessage,
|
|
AgentConversationSummary,
|
|
SceneStore,
|
|
} from '@pascal-app/mcp/storage'
|
|
import type { UIMessage } from 'ai'
|
|
import { getSceneStore } from './scene-store-server'
|
|
|
|
const MAX_MESSAGES = 80
|
|
const MAX_TEXT_LENGTH = 20_000
|
|
const DEFAULT_TITLE = '新对话'
|
|
|
|
type ConversationStore = Required<
|
|
Pick<
|
|
SceneStore,
|
|
| 'createAgentConversation'
|
|
| 'listAgentConversations'
|
|
| 'loadAgentConversation'
|
|
| 'saveAgentConversation'
|
|
| 'deleteAgentConversation'
|
|
>
|
|
>
|
|
|
|
type UnknownPart = {
|
|
data?: unknown
|
|
errorText?: unknown
|
|
state?: unknown
|
|
text?: unknown
|
|
toolName?: unknown
|
|
type?: unknown
|
|
}
|
|
|
|
function isConversationStore(store: SceneStore): store is SceneStore & ConversationStore {
|
|
return (
|
|
typeof store.createAgentConversation === 'function' &&
|
|
typeof store.listAgentConversations === 'function' &&
|
|
typeof store.loadAgentConversation === 'function' &&
|
|
typeof store.saveAgentConversation === 'function' &&
|
|
typeof store.deleteAgentConversation === 'function'
|
|
)
|
|
}
|
|
|
|
export async function getAgentConversationStore(): Promise<ConversationStore> {
|
|
const store = await getSceneStore()
|
|
if (!isConversationStore(store)) {
|
|
throw new Error('Agent conversation storage is unavailable')
|
|
}
|
|
return store
|
|
}
|
|
|
|
function toolNameFromPart(part: UnknownPart): string | null {
|
|
if (typeof part.toolName === 'string' && part.toolName) return part.toolName
|
|
if (typeof part.type === 'string' && part.type.startsWith('tool-')) return part.type.slice(5)
|
|
return null
|
|
}
|
|
|
|
function sanitizePart(part: unknown): unknown | null {
|
|
if (!part || typeof part !== 'object') return null
|
|
const value = part as UnknownPart
|
|
if (value.type === 'text' && typeof value.text === 'string') {
|
|
return { type: 'text', text: value.text.slice(0, MAX_TEXT_LENGTH) }
|
|
}
|
|
if (value.type === 'data-pascal-tool-activity' && value.data && typeof value.data === 'object') {
|
|
const data = value.data as UnknownPart
|
|
const toolName = typeof data.toolName === 'string' ? data.toolName : null
|
|
if (!toolName) return null
|
|
return {
|
|
type: 'data-pascal-tool-activity',
|
|
data: {
|
|
toolName,
|
|
state: typeof data.state === 'string' ? data.state : 'output-available',
|
|
...(typeof data.errorText === 'string' ? { errorText: data.errorText.slice(0, 500) } : {}),
|
|
},
|
|
}
|
|
}
|
|
|
|
const toolName = toolNameFromPart(value)
|
|
if (!toolName) return null
|
|
return {
|
|
type: 'data-pascal-tool-activity',
|
|
data: {
|
|
toolName,
|
|
state: typeof value.state === 'string' ? value.state : 'output-available',
|
|
...(typeof value.errorText === 'string' ? { errorText: value.errorText.slice(0, 500) } : {}),
|
|
},
|
|
}
|
|
}
|
|
|
|
export function sanitizeAgentMessages(messages: UIMessage[]): AgentConversationMessage[] {
|
|
const sanitized: AgentConversationMessage[] = []
|
|
for (const [messageIndex, message] of messages.slice(-MAX_MESSAGES).entries()) {
|
|
if (!['system', 'user', 'assistant'].includes(message.role)) continue
|
|
const parts: unknown[] = message.parts.map(sanitizePart).filter((part) => part !== null)
|
|
if (parts.length === 0) continue
|
|
sanitized.push({
|
|
id:
|
|
typeof message.id === 'string' && message.id.length > 0
|
|
? message.id.slice(0, 128)
|
|
: `message-${messageIndex}`,
|
|
role: message.role,
|
|
parts,
|
|
})
|
|
}
|
|
return sanitized
|
|
}
|
|
|
|
export function conversationTitle(messages: UIMessage[]): string {
|
|
const userMessage = messages.find((message) => message.role === 'user')
|
|
const textPart = userMessage?.parts.find((part) => part.type === 'text') as
|
|
| { text?: string }
|
|
| undefined
|
|
const title = textPart?.text?.trim().replace(/\s+/g, ' ')
|
|
return title ? title.slice(0, 32) : DEFAULT_TITLE
|
|
}
|
|
|
|
export type { AgentConversation, AgentConversationSummary }
|