feat: 重构对话功能

This commit is contained in:
DEV_DSW
2026-04-14 17:02:20 +08:00
parent b3f07c4cfe
commit c61e41049f
53 changed files with 5200 additions and 1982 deletions

View File

@@ -1,53 +1,167 @@
/// 消息角色枚举
export enum MessageRole {
// 智能体消息
AI = "AI",
// 我发送的消息
ME = "ME",
// 其他消息
OTHER = "OTHER",
};
/// Chat消息模型
export class ChatMessage {
// 消息唯一标识
messageId: string;
// 消息类型
messageRole: MessageRole;
// 消息内容
messageContent: string;
// 消息内容列表(用于流式更新)
messageContentList: string[];
// 是否加载中
isLoading?: boolean;
// 是否完成
finished?: boolean;
// 工具调用信息
toolCall?: any;
// 问题信息
question?: string;
// 时间戳
timestamp?: number;
constructor(
messageId: string,
messageRole: MessageRole,
messageContent: string,
messageContentList: string[] = [],
isLoading: boolean = false,
finished: boolean = false,
toolCall?: any,
question?: any,
timestamp?: number
) {
this.messageId = messageId;
this.messageRole = messageRole;
this.messageContent = messageContent;
this.messageContentList = messageContentList;
this.isLoading = isLoading;
this.finished = finished;
this.toolCall = toolCall;
this.question = question;
this.timestamp = timestamp || Date.now();
}
/// 附件文件元数据(与 ClawX 对齐)
export interface AttachedFileMeta {
fileName: string;
mimeType: string;
fileSize: number;
preview: string | null;
filePath?: string;
source?: 'user-upload' | 'tool-result' | 'message-ref';
}
/// 内容块(与 ClawX 对齐,用于未来扩展结构化消息)
export interface ContentBlock {
type: 'text' | 'image' | 'thinking' | 'tool_use' | 'tool_result' | 'toolCall' | 'toolResult';
text?: string;
thinking?: string;
source?: { type: string; media_type?: string; data?: string; url?: string };
data?: string;
mimeType?: string;
id?: string;
name?: string;
input?: unknown;
arguments?: unknown;
content?: unknown;
}
/// 原始消息(与 ClawX RawMessage 对齐)
export interface RawMessage {
role: 'user' | 'assistant' | 'system' | 'toolresult';
content: string | ContentBlock[];
timestamp?: number;
id?: string;
toolCallId?: string;
toolName?: string;
details?: unknown;
isError?: boolean;
/** zn-ai 特有:问题标签(保留现有能力) */
question?: string[];
/** zn-ai 特有:工具调用结果(保留现有能力) */
toolCall?: any;
/** 本地-only附件 */
_attachedFiles?: AttachedFileMeta[];
}
/// 工具状态(与 ClawX 对齐)
export interface ToolStatus {
id?: string;
toolCallId?: string;
name: string;
status: 'running' | 'completed' | 'error';
durationMs?: number;
summary?: string;
updatedAt: number;
}
/// 会话(与 ClawX ChatSession 对齐)
export interface ChatSession {
key: string;
label?: string;
displayName?: string;
thinkingLevel?: string;
model?: string;
updatedAt?: number;
}
/// 流式消息辅助:从 RawMessage 提取纯文本
export function extractText(message?: RawMessage | null): string {
if (!message) return '';
const content = message.content;
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return (content as Array<{ type?: string; text?: string }>)
.filter((b) => b.type === 'text' && b.text)
.map((b) => b.text!)
.join('\n');
}
return '';
}
/// 流式消息辅助:从 RawMessage 提取 thinking 文本
export function extractThinking(message?: RawMessage | null): string | null {
if (!message) return null;
const content = message.content;
if (Array.isArray(content)) {
const block = content.find((b: any) => b.type === 'thinking');
return block?.thinking || null;
}
return null;
}
/// 流式消息辅助:从 RawMessage 提取图片
export function extractImages(message?: RawMessage | null): Array<{ url?: string; data?: string; mimeType: string }> {
if (!message) return [];
const content = message.content;
if (!Array.isArray(content)) return [];
const images: Array<{ url?: string; data?: string; mimeType: string }> = [];
for (const block of content as ContentBlock[]) {
if (block.type === 'image') {
if (block.source) {
const src = block.source;
if (src.type === 'base64' && src.data) {
images.push({ data: src.data, mimeType: src.media_type || 'image/jpeg' });
} else if (src.type === 'url' && src.url) {
images.push({ url: src.url, mimeType: src.media_type || 'image/jpeg' });
}
} else if (block.data) {
images.push({ data: block.data, mimeType: block.mimeType || 'image/jpeg' });
}
}
if ((block.type === 'tool_result' || block.type === 'toolResult') && block.content) {
images.push(...extractImages({ role: 'toolresult', content: block.content }));
}
}
return images;
}
/// 流式消息辅助:从 RawMessage 提取 tool_use
export function extractToolUse(message?: RawMessage | null): Array<{ id?: string; name: string; input?: unknown }> {
if (!message) return [];
const content = message.content;
if (!Array.isArray(content)) return [];
return (content as ContentBlock[])
.filter((b) => b.type === 'tool_use' || b.type === 'toolCall')
.map((b) => ({ id: b.id, name: b.name || b.id || 'tool', input: b.input ?? b.arguments }));
}
/// 格式化时间戳(秒/ms 兼容)
export function formatTimestamp(ts?: number): string {
if (!ts) return '';
const ms = ts < 1e12 ? ts * 1000 : ts;
return new Date(ms).toLocaleString();
}
/// 判断是否为 tool-only 消息
export function isToolOnlyMessage(message?: RawMessage): boolean {
if (!message) return false;
const role = message.role;
if (role === 'toolresult' || role === 'tool_result') return true;
const content = message.content;
if (Array.isArray(content)) {
const hasTool = content.some((b: any) =>
['tool_use', 'tool_result', 'toolCall', 'toolResult'].includes(b.type)
);
const hasText = content.some(
(b: any) => b.type === 'text' && b.text?.trim?.()
);
const hasImage = content.some((b: any) => b.type === 'image');
return hasTool && !hasText && !hasImage;
}
return false;
}
/// 判断是否为 tool result 角色
export function isToolResultRole(role?: string): boolean {
if (!role) return false;
const normalized = role.toLowerCase();
return normalized === 'toolresult' || normalized === 'tool_result';
}
/// 判断是否为内部消息(不应展示在 UI
export function isInternalMessage(msg: { role?: string; content?: unknown }): boolean {
if (msg.role === 'system') return true;
if (msg.role === 'assistant') {
const text = typeof msg.content === 'string' ? msg.content : extractText(msg as RawMessage);
if (/^(HEARTBEAT_OK|NO_REPLY)\s*$/.test(text)) return true;
}
return false;
}