import type { UIMessage } from 'ai'; type HistoryMessage = Pick; /** 去掉 system / data-system / part.role=system 等权限污染,保留可继续对话的 user/assistant 内容。 */ export function scrubClientPrivilegeMessages(messages: unknown[]): unknown[] { const scrubbed: unknown[] = []; for (const rawMessage of messages) { if (!isRecord(rawMessage)) { continue; } if (rawMessage.role !== 'user' && rawMessage.role !== 'assistant') { continue; } if (!Array.isArray(rawMessage.parts)) { continue; } const parts: unknown[] = []; for (const rawPart of rawMessage.parts) { const part = scrubClientPrivilegePart(rawPart, rawMessage.role); if (part) { parts.push(part); } } if (parts.length === 0) { continue; } scrubbed.push({ ...rawMessage, parts, }); } return scrubbed; } /** * 仅保留 user/assistant 的纯文本 parts,供客户端上报与本地持久化。 * 与服务端模型上下文对齐,避免 tool/system 等 UI 结构触发 4513。 */ export function toPlainTextHistory(messages: T[]): Array<{ id: string; role: 'user' | 'assistant'; parts: Array<{ type: 'text'; text: string }>; }> { return messages .filter((message): message is T & { role: 'user' | 'assistant' } => ( message.role === 'user' || message.role === 'assistant' )) .map((message) => ({ id: message.id, role: message.role, parts: message.parts.flatMap((part) => ( isTextPart(part) ? [{ type: 'text' as const, text: part.text }] : [] )), })) .filter((message) => message.parts.length > 0); } function scrubClientPrivilegePart( rawPart: unknown, messageRole: 'user' | 'assistant', ): Record | null { if (!isRecord(rawPart) || typeof rawPart.type !== 'string') { return null; } if (rawPart.type === 'system' || rawPart.type === 'data-system') { return null; } if (messageRole === 'user') { if (rawPart.type !== 'text' || typeof rawPart.text !== 'string') { return null; } return { type: 'text', text: rawPart.text }; } if ('role' in rawPart && rawPart.role === 'system') { // 历史里偶发把 role 写进 text part;保留文本、去掉权限标记,避免整会话被拒。 if (rawPart.type === 'text' && typeof rawPart.text === 'string') { return { type: 'text', text: rawPart.text }; } return null; } return rawPart; } function isTextPart(part: unknown): part is { type: 'text'; text: string } { return isRecord(part) && part.type === 'text' && typeof part.text === 'string'; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }