Files
Agent-System/web/src/api/chatWindow.ts
T

291 lines
9.6 KiB
TypeScript

import {
ChatMessage,
ChatDayBucket,
ChatWindowResult,
ChatDateStat,
ChatCategoryStat,
} from '../types/workbench';
import { MessageContentKind, MessageExportCapability } from '../types/replyExport';
import { getBearerAuthHeaders } from './authHeaders';
import { mapErrorInfo } from './chatStream';
export interface RawChatMessageRecord {
id: number;
session_id: string;
user_id: number;
role: 'user' | 'assistant' | 'system';
content: string;
intent?: string;
branch?: string;
metadata?: Record<string, any>;
day_key: string;
category?: any;
created_at: string;
content_kind?: MessageContentKind;
export_policy?: 'full' | 'restricted' | 'none';
content_revision?: number;
content_sha256?: string;
export_capability?: MessageExportCapability;
}
export interface RawChatDayBucket {
day_key: string;
count: number;
loaded_count: number;
truncated: boolean;
first_message_id: number;
last_message_id: number;
messages: RawChatMessageRecord[];
}
export interface RawChatWindowResult {
session_id: string;
days: RawChatDayBucket[];
has_more_before: boolean;
oldest_loaded_day: string;
}
export interface ChatDayMessagesResult {
sessionId: string;
dayKey: string;
totalCount: number;
loadedCount: number;
hasMoreBefore: boolean;
hasMoreAfter: boolean;
messages: ChatMessage[];
}
export function mapRecordToMessage(r: RawChatMessageRecord): ChatMessage {
const timestamp = r.created_at
? new Date(r.created_at).toTimeString().substring(0, 5)
: new Date().toTimeString().substring(0, 5);
const meta = r.metadata || {};
const rawTaskRequest =
meta.task_request && typeof meta.task_request === 'object'
? (meta.task_request as Record<string, any>)
: undefined;
const taskRequest = rawTaskRequest
? {
taskKey: String(rawTaskRequest.task_key || meta.task_key || ''),
projectName: String(rawTaskRequest.project_name || ''),
customerName: String(rawTaskRequest.customer_name || ''),
deviceType: String(rawTaskRequest.device_type || ''),
intendedUse: rawTaskRequest.intended_use,
targetMarket: rawTaskRequest.target_market,
sessionId: String(rawTaskRequest.session_id || r.session_id),
userId: Number(rawTaskRequest.user_id || r.user_id || 0),
projectId: Number(rawTaskRequest.project_id || meta.project_id || 0),
clientTurnId: String(rawTaskRequest.client_turn_id || meta.client_turn_id || ''),
userMessage: String(rawTaskRequest.user_message || ''),
docId: rawTaskRequest.doc_id ? String(rawTaskRequest.doc_id) : undefined,
}
: meta.task_key && meta.project_id
? {
taskKey: String(meta.task_key),
projectName: '',
customerName: '',
deviceType: '',
sessionId: r.session_id,
userId: r.user_id,
projectId: Number(meta.project_id),
clientTurnId: String(meta.client_turn_id || ''),
}
: undefined;
const persistedError =
meta.error_info && typeof meta.error_info === 'object'
? mapErrorInfo({ type: 'error', error_info: meta.error_info })
: undefined;
const legacyBusyError =
r.role === 'assistant' && meta.task_status === 'failed' && r.content.includes('禁止并行修改')
? {
userMessage: '该文档正在由项目助手或其他用户编辑中;当前请求未执行,请等待现有任务完成',
code: 'DOC_BUSY',
class: 'fatal' as const,
actions: [],
}
: undefined;
const legacyTaskError =
r.role === 'assistant' && meta.task_status === 'failed' && !legacyBusyError
? {
userMessage: meta.workspace_id
? '任务执行中断,可继续或重新开始'
: '任务执行失败,可按原请求重新开始',
code: meta.workspace_id ? 'TASK_INTERRUPTED' : 'TASK_FAILED',
class: (meta.workspace_id ? 'resumable' : 'fatal') as 'resumable' | 'fatal',
upstream: r.content,
workspaceId: typeof meta.workspace_id === 'string' ? meta.workspace_id : undefined,
actions: meta.workspace_id
? (['continue', 'restart'] as Array<'continue' | 'restart'>)
: (['restart'] as Array<'restart'>),
}
: undefined;
const msgId = meta.client_id || (r.id ? `msg-${r.id}` : `msg-tmp-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`);
return {
id: msgId,
clientTurnId: meta.client_turn_id || undefined,
clientMessageId: meta.client_id || undefined,
serverId: r.id,
sessionId: r.session_id,
role: r.role,
authorName: meta.author_name || (r.role === 'user' ? '用户' : '项目助手'),
content: r.content || '',
intentTag: r.intent || undefined,
branchTag: r.branch || undefined,
dayKey: r.day_key,
category: r.category,
timestamp,
createdAt: r.created_at,
attachments: meta.attachments,
interactiveCard: meta.interactive_card,
recommendedCases: meta.recommended_cases,
quotes: meta.quotes || (meta.quote ? [meta.quote] : undefined),
status:
r.role === 'assistant'
? meta.task_status === 'running'
? 'streaming_status'
: meta.task_status === 'failed' || meta.task_status === 'finalizing_failed'
? 'failed'
: meta.task_status === 'quality_failed'
? 'completed'
: 'completed'
: undefined,
errorInfo: persistedError || legacyBusyError || legacyTaskError,
taskRequest,
taskRunId: typeof meta.run_id === 'string' ? meta.run_id : undefined,
taskWorkspaceId: typeof meta.workspace_id === 'string' ? meta.workspace_id : undefined,
taskArtifact:
meta.artifact && typeof meta.artifact === 'object'
? (meta.artifact as ChatMessage['taskArtifact'])
: undefined,
contentKind: r.content_kind,
exportCapability: r.export_capability,
};
}
export function mapRawBucket(b: RawChatDayBucket): ChatDayBucket {
return {
dayKey: b.day_key,
count: b.count,
loadedCount: b.loaded_count,
truncated: b.truncated,
firstMessageId: b.first_message_id,
lastMessageId: b.last_message_id,
messages: (b.messages || []).map(mapRecordToMessage),
};
}
export async function fetchChatWindow(
sessionId: string,
days: number = 7,
beforeDay?: string,
signal?: AbortSignal,
): Promise<ChatWindowResult> {
let url = `/api/v1/chat/messages/window?session_id=${encodeURIComponent(sessionId)}&days=${days}`;
if (beforeDay) {
url += `&before_day=${encodeURIComponent(beforeDay)}`;
}
const res = await fetch(url, { signal, headers: getBearerAuthHeaders() });
if (!res.ok) {
throw Object.assign(new Error(`fetchChatWindow failed: HTTP ${res.status}`), {
httpStatus: res.status,
});
}
const raw: RawChatWindowResult = await res.json();
return {
sessionId: raw.session_id,
days: (raw.days || []).map(mapRawBucket),
hasMoreBefore: Boolean(raw.has_more_before),
oldestLoadedDay: raw.oldest_loaded_day || '',
};
}
export async function fetchDayMessages(
sessionId: string,
date: string,
options?: {
anchorId?: number;
beforeId?: number;
afterId?: number;
limit?: number;
}
): Promise<ChatDayMessagesResult> {
const params = new URLSearchParams({
session_id: sessionId,
date,
});
if (options?.anchorId) params.set('anchor_id', String(options.anchorId));
if (options?.beforeId) params.set('before_id', String(options.beforeId));
if (options?.afterId) params.set('after_id', String(options.afterId));
if (options?.limit) params.set('limit', String(options.limit));
const res = await fetch(`/api/v1/chat/messages/day?${params.toString()}`, {
headers: getBearerAuthHeaders(),
});
if (!res.ok) {
throw new Error(`fetchDayMessages failed: HTTP ${res.status}`);
}
const raw = await res.json();
return {
sessionId: raw.session_id,
dayKey: raw.day_key,
totalCount: raw.total_count || 0,
loadedCount: raw.loaded_count || 0,
hasMoreBefore: Boolean(raw.has_more_before),
hasMoreAfter: Boolean(raw.has_more_after),
messages: (raw.messages || []).map(mapRecordToMessage),
};
}
export async function fetchChatDates(sessionId: string): Promise<ChatDateStat[]> {
const res = await fetch(`/api/v1/chat/dates?session_id=${encodeURIComponent(sessionId)}`, {
headers: getBearerAuthHeaders(),
});
if (!res.ok) {
throw new Error(`fetchChatDates failed: HTTP ${res.status}`);
}
const raw = await res.json();
const items = Array.isArray(raw) ? raw : [];
return items.map((item: any) => ({
dayKey: item.day_key,
count: item.count || 0,
categories: item.categories || {},
digestStatus: item.digest_status,
}));
}
export async function importChatMessages(
messages: Array<Record<string, any>>
): Promise<{ imported_count: number; total: number }> {
const res = await fetch('/api/v1/chat/messages/import', {
method: 'POST',
headers: getBearerAuthHeaders('application/json'),
body: JSON.stringify(messages),
});
if (!res.ok) {
throw new Error(`importChatMessages failed: HTTP ${res.status}`);
}
return res.json();
}
export async function fetchChatCategories(sessionId: string): Promise<ChatCategoryStat[]> {
const res = await fetch(`/api/v1/chat/categories?session_id=${encodeURIComponent(sessionId)}`, {
headers: getBearerAuthHeaders(),
});
if (!res.ok) {
throw new Error(`fetchChatCategories failed: HTTP ${res.status}`);
}
const raw = await res.json();
const items = Array.isArray(raw) ? raw : [];
return items.map((item: any) => ({
category: item.category,
label: item.label,
count: item.count || 0,
first_message_id: item.first_message_id,
last_message_id: item.last_message_id,
}));
}