fix: stabilize review detail and collabora loading

This commit is contained in:
wren
2026-05-08 10:59:04 +08:00
parent 7fdb7386ee
commit 3da2a8d088
20 changed files with 319 additions and 284 deletions
+90 -116
View File
@@ -1,82 +1,97 @@
/**
* Dify Chat API 模块
* 自有 RAG Chat API 模块
*
* 提供客户端调用 Dify API 的函数
* 用于 Remix loader/action 中调用 Dify API
* 保持前端 dify-chat 调用面不变,内部转发到新的 /api/v3/rag/* 接口。
*
* @module api/dify/chat
* @module api/dify-chat/chat
*/
import { difyFetch } from './client.server';
// ============================================================================
// Dify Chat API 客户端
// ============================================================================
function unwrapResult<T>(payload: any): T {
if (payload && typeof payload === 'object' && 'data' in payload) {
return payload.data as T;
}
return payload as T;
}
function toIntAppId(appId?: string): string | undefined {
if (!appId) return undefined;
const parsed = Number(appId);
return Number.isFinite(parsed) ? String(parsed) : undefined;
}
function normalizeConversationName(name: string): string {
const compact = (name || '').replace(/\s+/g, ' ').trim();
if (!compact) return '新对话';
return compact.length > 20 ? `${compact.slice(0, 20)}...` : compact;
}
/**
* Dify Chat API 客户端
*
* @param jwt - JWT 认证令牌
* user 参数由后端自动从 JWT 中提取
*/
export const difyClient = {
/**
* 获取应用参数
*/
async getApplicationParameters(jwt?: string): Promise<any> {
const response = await difyFetch('parameters', {
async getApplicationParameters(jwt?: string, appId?: string): Promise<any> {
const response = await difyFetch('chat/parameters', {
method: 'GET',
appId: toIntAppId(appId),
}, jwt);
return response.json();
const payload = unwrapResult<any>(await response.json());
return {
opening_statement: payload?.openingStatement || '',
suggested_questions: payload?.suggestedQuestions || [],
user_input_form: payload?.userInputForm || [],
file_upload: payload?.fileUpload || { enabled: false },
};
},
/**
* 获取会话列表
*
* @param jwt - JWT 认证令牌
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
*/
async getConversations(jwt?: string, appId?: string): Promise<any> {
const params = new URLSearchParams({
limit: '100',
first_id: '',
page: '1',
pageSize: '100',
});
const normalizedAppId = toIntAppId(appId);
if (normalizedAppId) {
params.set('appId', normalizedAppId);
}
const response = await difyFetch(`conversations?${params}`, {
const response = await difyFetch(`chat/conversations?${params.toString()}`, {
method: 'GET',
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt);
return response.json();
const payload = unwrapResult<any>(await response.json());
return {
data: (payload?.data || []).map((item: any) => ({
id: item.id,
name: item.name,
introduction: item.introduction || '',
created_at: item.createdAt || 0,
updated_at: item.updatedAt || 0,
})),
has_more: Boolean(payload?.hasMore),
limit: payload?.limit || 100,
};
},
/**
* 获取会话消息
*/
async getConversationMessages(conversationId: string, jwt?: string): Promise<any> {
const params = new URLSearchParams({
conversation_id: conversationId,
limit: '20',
last_id: '',
page: '1',
pageSize: '100',
});
const response = await difyFetch(`messages?${params}`, {
const response = await difyFetch(`chat/conversations/${conversationId}/messages?${params.toString()}`, {
method: 'GET',
}, jwt);
return response.json();
const payload = unwrapResult<any>(await response.json());
return {
data: (payload?.data || []).map((item: any) => ({
id: item.id,
query: item.query,
answer: item.answer,
feedback: item.feedback || undefined,
retriever_resources: item.retrieverResources || [],
created_at: item.createdAt || 0,
})),
has_more: Boolean(payload?.hasMore),
limit: payload?.limit || 100,
};
},
/**
* 发送聊天消息
*
* @param inputs - 输入参数
* @param query - 用户问题
* @param responseMode - 响应模式 ('streaming' | 'blocking')
* @param conversationId - 会话 ID
* @param files - 附件文件
* @param jwt - JWT 认证令牌
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
* @returns 对于流式响应返回 Response 对象,否则返回 JSON
*/
async createChatMessage(
inputs: Record<string, any>,
query: string,
@@ -92,99 +107,58 @@ export const difyClient = {
response_mode: responseMode,
conversation_id: conversationId,
files: files || [],
appId: toIntAppId(appId) ? Number(appId) : null,
conversationId: conversationId || null,
};
const response = await difyFetch('chat-messages', {
const response = await difyFetch('chat/messages', {
method: 'POST',
body: JSON.stringify(body),
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt);
// 对于流式响应,直接返回 Response 对象
if (responseMode === 'streaming') {
return response;
}
console.log('[Dify Chat] 解析 JSON 响应');
return response.json();
},
/**
* 重命名会话
*/
async renameConversation(
conversationId: string,
name: string,
autoGenerate: boolean = false,
jwt?: string
): Promise<any> {
const body = {
name,
auto_generate: autoGenerate,
};
let nextName = name?.trim() || '';
const response = await difyFetch(`conversations/${conversationId}/name`, {
method: 'POST',
body: JSON.stringify(body),
}, jwt);
return response.json();
},
/**
* 删除会话
*/
async deleteConversation(conversationId: string, jwt?: string): Promise<any> {
console.log('[Dify Chat] 删除会话:', conversationId);
try {
const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE',
body: JSON.stringify({}),
}, jwt);
// 对于 204 No Content 响应,直接返回成功
if (response.status === 204) {
console.log('[Dify Chat] 删除会话成功:', conversationId);
return { result: 'success' };
}
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
return data;
}
const text = await response.text();
console.log('[Dify Chat] 删除会话文本响应:', text);
return { result: 'success' };
} catch (error: any) {
// 权限不足等明确错误需要抛出,不能吞掉
if (error.message?.includes('403') || error.message?.includes('401')) {
throw error;
}
// 网络超时等不确定错误才降级为成功(Dify 可能已执行删除)
console.warn('[Dify Chat] 删除会话请求失败,但可能已成功删除:', error.message);
return { result: 'success' };
if (autoGenerate || !nextName) {
const messages = await this.getConversationMessages(conversationId, jwt);
const firstQuestion = messages?.data?.find((item: any) => item?.query)?.query || '';
nextName = normalizeConversationName(firstQuestion);
}
const response = await difyFetch(`chat/conversations/${conversationId}`, {
method: 'PATCH',
body: JSON.stringify({ name: nextName }),
}, jwt);
return unwrapResult<any>(await response.json());
},
async deleteConversation(conversationId: string, jwt?: string): Promise<any> {
const response = await difyFetch(`chat/conversations/${conversationId}`, {
method: 'DELETE',
}, jwt);
return unwrapResult<any>(await response.json());
},
/**
* 更新消息反馈
*/
async updateMessageFeedback(
messageId: string,
rating: 'like' | 'dislike' | null,
jwt?: string
): Promise<any> {
const body = {
rating,
};
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
const response = await difyFetch(`chat/messages/${messageId}/feedback`, {
method: 'POST',
body: JSON.stringify(body),
body: JSON.stringify({ rating }),
}, jwt);
return response.json();
return unwrapResult<any>(await response.json());
},
};
+26 -53
View File
@@ -1,98 +1,71 @@
/**
* Dify Chat 服务端 API 模块
* RAG Chat 服务端 API 模块
*
* 提供 Node.js 服务端调用 FastAPI 后端的基础功能
* Dify 的 API_KEY 和 APP_ID 由 FastAPI 后端管理,前端只负责转发请求
*
* 调用链路:
* Remix Server → FastAPI /dify_chat/* → Dify
* 提供 Node.js 服务端调用 FastAPI 后端的基础功能
* 现已改为走自有 RAG 接口:
* Remix Server -> FastAPI /api/v3/rag/*
*
* @module api/dify-chat/client.server
*/
import { API_BASE_URL } from '~/config/api-config';
// ============================================================================
// 配置
// ============================================================================
const RAG_API_ROOT = `${API_BASE_URL}/v3/rag`;
/**
* Dify Chat API 代理地址
* 通过 FastAPI 后端的 /dify_chat 路由代理访问 Dify
* Dify 的认证(API_KEY)由 FastAPI 后端处理
*/
const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
// ============================================================================
// 基础请求函数
// ============================================================================
/**
* Dify Fetch 请求选项
*/
export interface DifyFetchOptions extends RequestInit {
/** 对话应用 ID,用于切换不同的 Dify 应用 */
appId?: string;
}
/**
* Dify Chat API 基础请求函数
*
* 使用用户 JWT 认证通过 FastAPI 代理访问 Dify
* FastAPI 后端会验证 JWT 并添加 Dify API_KEY
*
* @param endpoint - API 端点路径
* @param options - fetch 请求选项(可包含 appId
* @param jwt - 用户 JWT 认证令牌
* @returns Response 对象
*/
export async function difyFetch(
endpoint: string,
options: DifyFetchOptions = {},
jwt?: string
): Promise<Response> {
const { appId, ...fetchOptions } = options;
const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`;
function buildHeaders(fetchOptions: RequestInit, jwt?: string): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
...fetchOptions.headers,
};
if (jwt) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`;
(headers as Record<string, string>).Authorization = `Bearer ${jwt}`;
} else {
console.warn('[Dify Chat] 没有提供 JWTFastAPI 请求可能失败');
console.warn('[RAG Chat] 没有提供 JWTFastAPI 请求可能失败');
}
// 如果指定了应用 ID,添加 X-Dify-App-Id 请求头
return headers;
}
export async function difyFetch(
endpoint: string,
options: DifyFetchOptions = {},
jwt?: string
): Promise<Response> {
const { appId, ...fetchOptions } = options;
const cleanEndpoint = endpoint.replace(/^\//, '');
let url = `${RAG_API_ROOT}/${cleanEndpoint}`;
if (appId) {
(headers as Record<string, string>)['X-Dify-App-Id'] = appId;
console.log('[Dify Chat] 使用应用 ID:', appId);
const separator = url.includes('?') ? '&' : '?';
url = `${url}${separator}appId=${encodeURIComponent(appId)}`;
}
const response = await fetch(url, {
...fetchOptions,
headers,
headers: buildHeaders(fetchOptions, jwt),
});
if (!response.ok) {
const errorText = await response.text();
console.error('[Dify Chat] API 转发错误:', {
console.error('[RAG Chat] API 转发错误:', {
status: response.status,
statusText: response.statusText,
error: errorText
error: errorText,
});
if (response.status === 401) {
throw new Error('JWT认证失败,请重新登录');
}
throw new Error(`Dify API Error: ${response.status} ${response.statusText}`);
throw new Error(`RAG API Error: ${response.status} ${response.statusText}`);
}
return response;
}
// 重新导出 chat 模块的 difyClient
export { difyClient } from './chat';