291 lines
9.6 KiB
TypeScript
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,
|
|
}));
|
|
}
|