feat: implement task management store with IPC integration
- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses. - Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion. - Introduced persistence for tasks using IPC. - Created utility functions for normalizing room types and building subtasks. - Added a new CSS file for global styles in `src-react/styles.css`. - Created runtime types in `src-react/types/runtime.ts` and exported them. - Updated the main entry points for Vue and React applications to support dynamic framework loading. - Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`. - Updated TypeScript configuration to include paths for React components and types. - Enhanced Vite configuration to support both Vue and React frameworks.
This commit is contained in:
@@ -1,167 +1 @@
|
||||
/// 附件文件元数据(与 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;
|
||||
}
|
||||
export * from '@shared/chat-model';
|
||||
|
||||
Reference in New Issue
Block a user