Compare commits
4 Commits
5fc3a7a254
..
wren
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da2a8d088 | |||
| 7fdb7386ee | |||
| ba113a0e24 | |||
| 050aa679be |
@@ -116,39 +116,16 @@ export async function uploadCrossCheckingDocument(
|
||||
token: string | null = null
|
||||
): Promise<{data: CrossCheckingFileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
console.log('【交叉评查上传】开始上传文档:', { fileName, fileSize: binaryData.byteLength, typeId });
|
||||
|
||||
// 创建FormData对象
|
||||
const formData = new FormData();
|
||||
|
||||
// 将二进制数据转换为Blob并添加到FormData
|
||||
const blob = new Blob([binaryData], { type: fileType });
|
||||
formData.append('file', blob, fileName);
|
||||
console.log('【交叉评查上传】Blob已创建,文件大小:', blob.size);
|
||||
|
||||
// 将信息添加到一个JSON对象中
|
||||
const uploadInfo = {
|
||||
type_id: typeId,
|
||||
evaluation_level: priority,
|
||||
document_number: documentNumber || null,
|
||||
remark: remark || null,
|
||||
is_test_document: isTestDocument,
|
||||
document_id: documentId || null,
|
||||
is_reupload: isReupload
|
||||
};
|
||||
|
||||
// 添加JSON字符串到FormData
|
||||
formData.append('upload_info', JSON.stringify(uploadInfo));
|
||||
console.log('【交叉评查上传】FormData准备完成:', JSON.stringify(uploadInfo));
|
||||
|
||||
// 根据是否有documentId决定使用哪个接口
|
||||
const uploadEndpoint = '/batch_upload';
|
||||
const uploadUrl = UPLOAD_URL + uploadEndpoint;
|
||||
console.log('【交叉评查上传】准备发送请求到服务器:', uploadUrl);
|
||||
|
||||
// 发送请求
|
||||
formData.append('typeId', String(typeId));
|
||||
formData.append('region', 'default');
|
||||
formData.append('fileRole', 'primary');
|
||||
formData.append('autoRun', 'true');
|
||||
formData.append('speed', priority === 'urgent' ? 'urgent' : 'normal');
|
||||
|
||||
try {
|
||||
console.log('【交叉评查上传】开始axios请求...');
|
||||
const headers: Record<string, string> = {
|
||||
'X-File-Name': encodeURIComponent(fileName),
|
||||
};
|
||||
@@ -157,30 +134,43 @@ export async function uploadCrossCheckingDocument(
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await axios.post(uploadUrl, formData, {
|
||||
const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, {
|
||||
headers
|
||||
});
|
||||
|
||||
console.log('【交叉评查上传】收到服务器响应:', { status: response.status, statusText: response.statusText });
|
||||
|
||||
console.log('【交叉评查上传】JSON响应解析成功:', response.data);
|
||||
|
||||
const extractedData = extractApiData<CrossCheckingFileUploadResponse>(response.data);
|
||||
console.log('【交叉评查上传】提取的数据:', extractedData);
|
||||
|
||||
if (!extractedData) {
|
||||
console.error('【交叉评查上传】无法提取数据');
|
||||
return { error: '处理上传响应失败', status: 500 };
|
||||
const uploadData = response.data?.data;
|
||||
if (!uploadData?.documentId) {
|
||||
return { error: response.data?.message || response.data?.msg || '处理上传响应失败', status: response.status };
|
||||
}
|
||||
|
||||
console.log('【交叉评查上传】上传成功,返回数据');
|
||||
return { data: extractedData as CrossCheckingFileUploadResponse };
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
result: {
|
||||
id: Number(uploadData.documentId),
|
||||
file_name: uploadData.fileName || fileName,
|
||||
file_size: binaryData.byteLength,
|
||||
file_url: uploadData.storagePath || '',
|
||||
type_id: Number(uploadData.typeId || typeId),
|
||||
type_description: uploadData.typeCode || '',
|
||||
document_number: documentNumber || null,
|
||||
storage_type: 'oss',
|
||||
is_test_document: isTestDocument,
|
||||
remark: remark || null,
|
||||
background_processing: true,
|
||||
evaluation_level: priority,
|
||||
},
|
||||
error: null,
|
||||
}
|
||||
};
|
||||
} catch (axiosError) {
|
||||
console.error('【交叉评查上传】axios请求失败:', axiosError);
|
||||
if (axios.isAxiosError(axiosError)) {
|
||||
const errorText = axiosError.response?.data || axiosError.message;
|
||||
const errorText =
|
||||
axiosError.response?.data?.message ||
|
||||
axiosError.response?.data?.msg ||
|
||||
axiosError.response?.data?.detail ||
|
||||
axiosError.message;
|
||||
return {
|
||||
error: `上传失败: ${axiosError.response?.status || 500} ${axiosError.response?.statusText || ''} - ${errorText}`,
|
||||
error: `上传失败: ${errorText}`,
|
||||
status: axiosError.response?.status || 500
|
||||
};
|
||||
}
|
||||
@@ -190,7 +180,6 @@ export async function uploadCrossCheckingDocument(
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('【交叉评查上传】上传过程中发生错误:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '上传失败',
|
||||
status: 500
|
||||
|
||||
@@ -946,33 +946,24 @@ export async function updateDocumentAuditStatus(id: string, auditStatus: number,
|
||||
*/
|
||||
export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise<ApiResponse<DocumentType[]>> {
|
||||
try {
|
||||
// console.log('[getCrossCheckingDocumentTypes] 开始获取交叉评查文档类型');
|
||||
|
||||
const response = await postgrestGet<DocumentType>('/api/postgrest/proxy/document_types',{
|
||||
select: 'id,name,code,evaluation_point_groups_ids',
|
||||
filter: {
|
||||
evaluation_point_groups_ids: 'not.is.null'
|
||||
},
|
||||
token: jwtToken
|
||||
const response = await axios.get<{ data?: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
isEnabled?: boolean;
|
||||
ruleSetIds?: number[];
|
||||
}> }>(`${API_BASE_URL}/api/document-types`, {
|
||||
headers: jwtToken ? { Authorization: `Bearer ${jwtToken}` } : undefined,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error('[getCrossCheckingDocumentTypes] 获取失败:', response.error);
|
||||
return {
|
||||
success: false,
|
||||
error: response.error
|
||||
};
|
||||
}
|
||||
|
||||
// 进一步过滤,确保 evaluation_point_groups_ids 是非空数组
|
||||
const dataArray = Array.isArray(response.data) ? response.data : [];
|
||||
const filteredData = dataArray.filter(
|
||||
(item: DocumentType) => item.evaluation_point_groups_ids &&
|
||||
Array.isArray(item.evaluation_point_groups_ids) &&
|
||||
item.evaluation_point_groups_ids.length > 0
|
||||
);
|
||||
|
||||
// console.log('[getCrossCheckingDocumentTypes] 获取成功,共', filteredData.length, '个文档类型');
|
||||
const rawItems = Array.isArray(response.data?.data) ? response.data.data : [];
|
||||
const filteredData = rawItems
|
||||
.filter((item) => item && item.isEnabled !== false)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
+90
-116
@@ -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());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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] 没有提供 JWT,FastAPI 请求可能失败');
|
||||
console.warn('[RAG Chat] 没有提供 JWT,FastAPI 请求可能失败');
|
||||
}
|
||||
|
||||
// 如果指定了应用 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';
|
||||
|
||||
@@ -77,6 +77,26 @@ export interface SsoUser {
|
||||
deleted_at?: string;
|
||||
}
|
||||
|
||||
function compactUserInfoForSession(userInfo?: UserInfo, userRole?: string): UserInfo | undefined {
|
||||
if (!userInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Cookie Session 直接存整份 userInfo 很容易超过浏览器 4KB 限制;
|
||||
// 服务端鉴权实际只依赖这几个核心字段,其余信息交给接口按需取回。
|
||||
return {
|
||||
user_id: userInfo.user_id,
|
||||
sub: userInfo.sub,
|
||||
username: userInfo.username,
|
||||
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name,
|
||||
ou_id: userInfo.ou_id,
|
||||
ou_name: userInfo.ou_name,
|
||||
is_leader: userInfo.is_leader,
|
||||
area: userInfo.area,
|
||||
user_role: userInfo.user_role || userRole,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话存储配置
|
||||
*
|
||||
@@ -201,7 +221,14 @@ export async function getUserSession(request: Request) {
|
||||
const refreshToken = session.get("refreshToken");
|
||||
const tokenIssuedAt = session.get("tokenIssuedAt");
|
||||
let tokenExpiresIn = session.get("tokenExpiresIn");
|
||||
const userInfo = session.get("userInfo");
|
||||
const storedUserInfo = session.get("userInfo");
|
||||
const userInfo = storedUserInfo
|
||||
? {
|
||||
...storedUserInfo,
|
||||
role: storedUserInfo.role || storedUserInfo.user_role || userRole,
|
||||
user_role: storedUserInfo.user_role || userRole,
|
||||
}
|
||||
: storedUserInfo;
|
||||
const frontendJWT = session.get("frontendJWT");
|
||||
|
||||
// 🔑 检查是否是公共路径(不需要认证的路径)
|
||||
@@ -369,7 +396,7 @@ export async function createUserSession(params: {
|
||||
|
||||
// 用户信息和JWT
|
||||
if (params.userInfo) {
|
||||
session.set("userInfo", params.userInfo);
|
||||
session.set("userInfo", compactUserInfoForSession(params.userInfo, params.userRole));
|
||||
}
|
||||
if (params.frontendJWT) {
|
||||
session.set("frontendJWT", params.frontendJWT);
|
||||
@@ -534,4 +561,4 @@ async function callIDaaSLogout(accessToken: string, appId: string): Promise<void
|
||||
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+271
-47
@@ -62,6 +62,207 @@ export interface ApiResponse<T> {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
type RbacUserRole = {
|
||||
role_id?: number;
|
||||
role_key?: string;
|
||||
role_name?: string;
|
||||
};
|
||||
|
||||
type RbacUserItem = {
|
||||
id: number;
|
||||
username: string;
|
||||
nick_name: string;
|
||||
area: string;
|
||||
ou_id: string | null;
|
||||
ou_name: string | null;
|
||||
is_leader: boolean;
|
||||
status: number;
|
||||
tenant_name: string | null;
|
||||
dep_name: string | null;
|
||||
dep_short_name?: string | null;
|
||||
email?: string | null;
|
||||
phone_number?: string | null;
|
||||
roles?: RbacUserRole[];
|
||||
};
|
||||
|
||||
type RbacUsersPayload = {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: RbacUserItem[];
|
||||
};
|
||||
|
||||
let rbacUsersAvailable: boolean | null = null;
|
||||
|
||||
function normalizeUser(user: RbacUserItem): UserInfo {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nick_name: user.nick_name,
|
||||
area: user.area,
|
||||
ou_id: user.ou_id || '',
|
||||
ou_name: user.ou_name || '',
|
||||
is_leader: Boolean(user.is_leader),
|
||||
status: Number(user.status ?? 0),
|
||||
tenant_name: user.tenant_name ?? null,
|
||||
dep_name: user.dep_name ?? null,
|
||||
dep_short_name: user.dep_short_name ?? null,
|
||||
email: user.email ?? undefined,
|
||||
phone_number: user.phone_number ?? undefined,
|
||||
organization_path: {
|
||||
tenant_name: user.tenant_name || '未分组租户',
|
||||
dep_name: user.dep_name || '未分组部门',
|
||||
dep_short_name: user.dep_short_name || user.dep_name || '未分组部门',
|
||||
ou_name: user.ou_name || '未分组组织',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchRbacUsers(
|
||||
jwtToken?: string,
|
||||
search?: string,
|
||||
): Promise<ApiResponse<RbacUsersPayload>> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', '1');
|
||||
params.set('pageSize', '5000');
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (jwtToken) {
|
||||
headers.Authorization = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
const response = await axios.get<{ code?: number; message?: string; data?: RbacUsersPayload }>(
|
||||
`${API_BASE_URL}/api/v3/rbac/users?${params.toString()}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (!response.data?.data) {
|
||||
return {
|
||||
success: false,
|
||||
error: response.data?.message || '用户列表返回为空',
|
||||
};
|
||||
}
|
||||
|
||||
const keyword = search?.trim().toLowerCase();
|
||||
const items = keyword
|
||||
? response.data.data.items.filter((item) => {
|
||||
const haystacks = [
|
||||
item.username,
|
||||
item.nick_name,
|
||||
item.area,
|
||||
item.tenant_name,
|
||||
item.dep_name,
|
||||
item.ou_name,
|
||||
];
|
||||
return haystacks.some((value) => value?.toLowerCase().includes(keyword));
|
||||
})
|
||||
: response.data.data.items;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...response.data.data,
|
||||
total: items.length,
|
||||
items,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
let errorMessage = '获取用户列表失败';
|
||||
if (axios.isAxiosError(error)) {
|
||||
errorMessage = error.response?.data?.detail || error.response?.data?.message || error.message;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrganizationTreeFromUsers(
|
||||
users: UserInfo[],
|
||||
includeUsers: boolean,
|
||||
rootUuid?: string,
|
||||
): OrganizationResponse {
|
||||
const tenantMap = new Map<string, OrganizationNode>();
|
||||
|
||||
const ensureChild = (
|
||||
parent: OrganizationNode,
|
||||
key: string,
|
||||
create: () => OrganizationNode,
|
||||
): OrganizationNode => {
|
||||
const existing = parent.children.find((item) => item.ou_id === key);
|
||||
if (existing) return existing;
|
||||
const next = create();
|
||||
parent.children.push(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
users.forEach((user) => {
|
||||
const tenantName = user.tenant_name || user.area || '未分组租户';
|
||||
const depName = user.dep_name || '未分组部门';
|
||||
const ouId = user.ou_id || `ou_${depName}`;
|
||||
const ouName = user.ou_name || depName || '未分组组织';
|
||||
|
||||
const tenantKey = `tenant:${tenantName}`;
|
||||
let tenantNode = tenantMap.get(tenantKey);
|
||||
if (!tenantNode) {
|
||||
tenantNode = {
|
||||
ou_id: tenantKey,
|
||||
ou_name: tenantName,
|
||||
parent_ou_id: null,
|
||||
level: 1,
|
||||
children: [],
|
||||
users: [],
|
||||
};
|
||||
tenantMap.set(tenantKey, tenantNode);
|
||||
}
|
||||
|
||||
const depKey = `dep:${tenantName}:${depName}`;
|
||||
const depNode = ensureChild(tenantNode, depKey, () => ({
|
||||
ou_id: depKey,
|
||||
ou_name: depName,
|
||||
parent_ou_id: tenantNode!.ou_id,
|
||||
level: 2,
|
||||
children: [],
|
||||
users: [],
|
||||
}));
|
||||
|
||||
const orgKey = ouId.startsWith('ou_') ? `org:${tenantName}:${depName}:${ouName}` : ouId;
|
||||
const orgNode = ensureChild(depNode, orgKey, () => ({
|
||||
ou_id: orgKey,
|
||||
ou_name: ouName,
|
||||
parent_ou_id: depNode.ou_id,
|
||||
level: 3,
|
||||
children: [],
|
||||
users: [],
|
||||
}));
|
||||
|
||||
if (includeUsers) {
|
||||
orgNode.users.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
const organizations = Array.from(tenantMap.values());
|
||||
const pickSubtree = (nodes: OrganizationNode[], target: string): OrganizationNode[] => {
|
||||
for (const node of nodes) {
|
||||
if (node.ou_id === target) return [node];
|
||||
const nested = pickSubtree(node.children || [], target);
|
||||
if (nested.length > 0) return nested;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return {
|
||||
organizations: rootUuid ? pickSubtree(organizations, rootUuid) : organizations,
|
||||
total_organizations: organizations.length,
|
||||
total_users: users.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组织架构树(新版接口,支持按需加载)
|
||||
* @param includeUsers 是否包含用户信息
|
||||
@@ -74,9 +275,39 @@ export async function getOrganizationTree(
|
||||
jwtToken?: string,
|
||||
rootUuid?: string
|
||||
): Promise<ApiResponse<OrganizationResponse>> {
|
||||
try {
|
||||
// console.log('[getOrganizationTree] 开始调用获取组织架构API:', { includeUsers, rootUuid });
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (jwtToken) {
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// 新平台直接基于 RBAC 用户列表构造组织树,避免依赖已下线的旧 v2 接口。
|
||||
const fallbackUsers = await fetchRbacUsers(jwtToken);
|
||||
if (!fallbackUsers.success || !fallbackUsers.data) {
|
||||
throw new Error(fallbackUsers.error || '获取组织架构失败');
|
||||
}
|
||||
|
||||
rbacUsersAvailable = true;
|
||||
const normalizedUsers = fallbackUsers.data.items.map(normalizeUser);
|
||||
return {
|
||||
success: true,
|
||||
data: buildOrganizationTreeFromUsers(normalizedUsers, includeUsers, rootUuid),
|
||||
};
|
||||
} catch (error) {
|
||||
if (rbacUsersAvailable === false) {
|
||||
console.error('[getOrganizationTree] 获取组织架构失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取组织架构失败',
|
||||
};
|
||||
}
|
||||
|
||||
rbacUsersAvailable = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const params: string[] = [];
|
||||
params.push(`include_users=${includeUsers}`);
|
||||
if (rootUuid) {
|
||||
@@ -84,26 +315,13 @@ export async function getOrganizationTree(
|
||||
}
|
||||
|
||||
const url = `${API_BASE_URL}/api/v2/users/organizations/tree?${params.join('&')}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (jwtToken) {
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
const response = await axios.get<OrganizationResponse>(url, { headers });
|
||||
const responseData = response.data;
|
||||
|
||||
// console.log('[getOrganizationTree] API响应:', responseData);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: responseData
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getOrganizationTree] 获取组织架构失败:', error);
|
||||
|
||||
let errorMessage = '获取组织架构失败';
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data?.detail) {
|
||||
@@ -139,16 +357,13 @@ export async function searchUsers(
|
||||
users: UserInfo[];
|
||||
total: number;
|
||||
}>> {
|
||||
try {
|
||||
// console.log('[searchUsers] 搜索用户:', { search, page, pageSize });
|
||||
|
||||
const fallbackUsers = await fetchRbacUsers(jwtToken, search);
|
||||
if (!fallbackUsers.success || !fallbackUsers.data) {
|
||||
const params: string[] = [];
|
||||
if (search) params.push(`search=${encodeURIComponent(search)}`);
|
||||
params.push(`page=${page}`);
|
||||
params.push(`page_size=${pageSize}`);
|
||||
|
||||
const url = `${API_BASE_URL}/api/v2/users?${params.join('&')}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
@@ -156,34 +371,43 @@ export async function searchUsers(
|
||||
headers['Authorization'] = `Bearer ${jwtToken}`;
|
||||
}
|
||||
|
||||
const response = await axios.get<{ users: UserInfo[]; total: number }>(url, { headers });
|
||||
const responseData = response.data;
|
||||
|
||||
// console.log('[searchUsers] 搜索结果:', { total: responseData.total, count: responseData.users?.length });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: responseData
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[searchUsers] 搜索用户失败:', error);
|
||||
|
||||
let errorMessage = '搜索用户失败';
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data?.detail) {
|
||||
errorMessage = error.response.data.detail;
|
||||
} else if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
try {
|
||||
const url = `${API_BASE_URL}/api/v2/users?${params.join('&')}`;
|
||||
const response = await axios.get<{ users: UserInfo[]; total: number }>(url, { headers });
|
||||
return {
|
||||
success: true,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[searchUsers] 搜索用户失败:', error);
|
||||
let errorMessage = fallbackUsers.error || '搜索用户失败';
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.data?.detail) {
|
||||
errorMessage = error.response.data.detail;
|
||||
} else if (error.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
};
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = fallbackUsers.data.items.map(normalizeUser);
|
||||
const start = (page - 1) * pageSize;
|
||||
const paged = normalized.slice(start, start + pageSize);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
users: paged,
|
||||
total: normalized.length,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,4 +604,4 @@ export async function getFlatOrganizations(includeUsers: boolean = true): Promis
|
||||
error: error instanceof Error ? error.message : '获取扁平化组织列表失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName);
|
||||
|
||||
// 2. 监听文档加载状态
|
||||
const { isDocumentLoaded } = useDocumentReady(iframeRef);
|
||||
const { isDocumentLoaded } = useDocumentReady(iframeRef, config?.iframeUrl);
|
||||
|
||||
// 2.5. 保存 iframe window 引用并在文档加载时清除所有高亮
|
||||
useEffect(() => {
|
||||
@@ -1194,4 +1194,3 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
// 导出类型和 hook
|
||||
export { useCollaboraUnoCommands };
|
||||
export type { CollaboraViewerHandle };
|
||||
|
||||
|
||||
@@ -110,35 +110,75 @@ export function useCollaboraConfig(
|
||||
* @param iframeRef - iframe 引用
|
||||
* @returns 文档加载状态
|
||||
*/
|
||||
export function useDocumentReady(iframeRef: RefObject<HTMLIFrameElement>) {
|
||||
export function useDocumentReady(
|
||||
iframeRef: RefObject<HTMLIFrameElement>,
|
||||
iframeUrl?: string
|
||||
) {
|
||||
const [isDocumentLoaded, setIsDocumentLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// 验证消息来源
|
||||
const collaboraOrigin = new URL(COLLABORA_URL).origin;
|
||||
setIsDocumentLoaded(false);
|
||||
|
||||
if (event.origin !== collaboraOrigin) {
|
||||
const iframe = iframeRef.current;
|
||||
const expectedOrigin = iframeUrl
|
||||
? new URL(iframeUrl).origin
|
||||
: new URL(COLLABORA_URL).origin;
|
||||
let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const markLoaded = (source: string) => {
|
||||
setIsDocumentLoaded((prev) => {
|
||||
if (!prev) {
|
||||
console.log(`[DocumentReady] 文档已就绪(${source})`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
// 某些环境下 Collabora 不一定会抛出 Document_Loaded 消息,
|
||||
// 先在 iframe load 后给一个兜底超时,避免页面一直被遮罩层盖住。
|
||||
fallbackTimer = setTimeout(() => {
|
||||
markLoaded('iframe-load-fallback');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== expectedOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iframeRef.current?.contentWindow && event.source !== iframeRef.current.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
||||
|
||||
if (msg.MessageId === 'App_LoadingStatus' && msg.Values?.Status === 'Document_Loaded') {
|
||||
console.log('[DocumentReady] 文档加载完成');
|
||||
setIsDocumentLoaded(true);
|
||||
if (
|
||||
msg?.MessageId === 'App_LoadingStatus' &&
|
||||
['Document_Loaded', 'Document_Loaded_Editing', 'UI_Loaded'].includes(msg.Values?.Status)
|
||||
) {
|
||||
if (fallbackTimer) {
|
||||
clearTimeout(fallbackTimer);
|
||||
fallbackTimer = null;
|
||||
}
|
||||
markLoaded(`postmessage:${msg.Values?.Status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[DocumentReady] 解析消息失败:', err);
|
||||
} catch {
|
||||
// Collabora 也会发送非 JSON 消息,这里忽略即可。
|
||||
}
|
||||
};
|
||||
|
||||
iframe?.addEventListener('load', handleIframeLoad);
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => {
|
||||
if (fallbackTimer) {
|
||||
clearTimeout(fallbackTimer);
|
||||
}
|
||||
iframe?.removeEventListener('load', handleIframeLoad);
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, [iframeRef]);
|
||||
}, [iframeRef, iframeUrl]);
|
||||
|
||||
return { isDocumentLoaded };
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function Chat() {
|
||||
|
||||
// 权限检查
|
||||
const { hasPermission: checkPerm } = usePermission();
|
||||
const canChat = checkPerm('dify:chat:use');
|
||||
const canChat = checkPerm('rag:chat:use');
|
||||
|
||||
// 侧边栏状态
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
@@ -61,7 +61,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
conversationReadOnly = false,
|
||||
}, ref) => {
|
||||
const { hasPermission } = usePermission();
|
||||
const canDeleteConversation = hasPermission('dify:conversation:delete');
|
||||
const canDeleteConversation = hasPermission('rag:conversation:delete');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [renameModalVisible, setRenameModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
|
||||
@@ -180,7 +180,7 @@ const configs: Record<string, ApiConfig> = {
|
||||
// documentUrl: 'http://172.16.0.84:8073/docauditai/',
|
||||
// uploadUrl: 'http://172.16.0.84:8073/api/v2/documents',
|
||||
|
||||
// 公网访问 reviewsTest 时,iframe 不能再直连内网 Collabora,否则浏览器会拦截。
|
||||
// 从公网页面发起到 172.16.* 的 iframe 请求会触发浏览器私网访问拦截,联调环境必须走公网可达地址。
|
||||
collaboraUrl: 'http://nas.7bm.co:9980',
|
||||
appUrl: 'http://nas.7bm.co:5173',
|
||||
|
||||
|
||||
@@ -1,50 +1,52 @@
|
||||
import { json, type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { difyClient } from '~/api/dify-chat/client.server';
|
||||
import { commitSession, getSessionInfo } from '../utils/session.server';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息和 JWT
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const { session } = await getSessionInfo(request);
|
||||
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Parameters API - JWT不存在');
|
||||
return json(
|
||||
{ error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const data = await difyClient.getApplicationParameters(frontendJWT);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [API] Parameters API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0;
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500;
|
||||
|
||||
return json(
|
||||
{ error: error.message || 'Failed to fetch parameters' },
|
||||
{
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
import { json, type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { difyClient } from '~/api/dify-chat/client.server';
|
||||
import { commitSession, getSessionInfo } from '../utils/session.server';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息和 JWT
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const { session } = await getSessionInfo(request);
|
||||
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Parameters API - JWT不存在');
|
||||
return json(
|
||||
{ error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const appId = url.searchParams.get('app_id') || undefined;
|
||||
const data = await difyClient.getApplicationParameters(frontendJWT, appId);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [API] Parameters API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const sm = error.message?.match(/(\d{3})/); const os = sm ? parseInt(sm[1]) : 0;
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : os >= 400 && os < 500 ? os : 500;
|
||||
|
||||
return json(
|
||||
{ error: error.message || 'Failed to fetch parameters' },
|
||||
{
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
/**
|
||||
* GET /api/v3/dify/chat-apps/default - 获取默认对话应用
|
||||
*
|
||||
* 转发请求到后端 API,后端从配置文件读取默认对话应用
|
||||
* 参考文档:docs/new-dify/dify_api_doc.md - 对话应用多实例支持
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs, json } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
function normalizeApp(payload: any) {
|
||||
const app = payload?.data?.data || payload?.data || null;
|
||||
if (!app) return null;
|
||||
return {
|
||||
app_id: String(app.appId),
|
||||
app_name: app.appName,
|
||||
description: app.description || '',
|
||||
is_default: Boolean(app.isDefault),
|
||||
type: 'rag',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
@@ -20,23 +27,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Get Default Chat App - Forwarding to backend');
|
||||
|
||||
// 转发请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps/default`;
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await fetch(`${API_BASE_URL}/v3/rag/apps/default`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API] Get Default Chat App - Backend response:', data);
|
||||
|
||||
return json(data, { status: response.status });
|
||||
|
||||
return json({ data: normalizeApp(data) }, { status: response.status });
|
||||
} catch (error: any) {
|
||||
console.error('[API] Get Default Chat App - Error:', error.message);
|
||||
return json(
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
/**
|
||||
* GET /api/v3/dify/chat-apps/my - 获取当前用户可访问的对话应用列表
|
||||
*
|
||||
* 转发请求到后端 API,后端从配置文件读取对话应用列表
|
||||
* 参考文档:docs/new-dify/dify_api_doc.md - 对话应用多实例支持
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs, json } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
function normalizeApps(payload: any) {
|
||||
const apps = payload?.data?.data || payload?.data || [];
|
||||
return {
|
||||
data: Array.isArray(apps)
|
||||
? apps.map((app: any) => ({
|
||||
app_id: String(app.appId),
|
||||
app_name: app.appName,
|
||||
description: app.description || '',
|
||||
is_default: Boolean(app.isDefault),
|
||||
type: 'rag',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
}))
|
||||
: [],
|
||||
total: Array.isArray(apps) ? apps.length : 0,
|
||||
page: 1,
|
||||
page_size: Array.isArray(apps) ? apps.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
@@ -20,24 +33,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Get My Chat Apps - Forwarding to backend');
|
||||
|
||||
// 转发请求到后端 - 使用正确的接口路径
|
||||
// 根据文档:GET /api/v3/dify/chat-apps
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps`;
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await fetch(`${API_BASE_URL}/v3/rag/apps`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API] Get My Chat Apps - Backend response:', data);
|
||||
|
||||
return json(data, { status: response.status });
|
||||
|
||||
const normalized = normalizeApps(data);
|
||||
return json(normalized, { status: response.status });
|
||||
} catch (error: any) {
|
||||
console.error('[API] Get My Chat Apps - Error:', error.message);
|
||||
return json(
|
||||
|
||||
@@ -28,8 +28,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
|
||||
await requireRoutePermission("/document-types", userInfo?.role || "", frontendJWT || undefined);
|
||||
await requireRoutePermission("/document-types", userRole, frontendJWT || undefined);
|
||||
const rootsRes = await getDocumentTypeRoots({}, frontendJWT);
|
||||
|
||||
return {
|
||||
|
||||
@@ -35,7 +35,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/document-types/new", userInfo?.role || "", frontendJWT || undefined);
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
await requireRoutePermission("/document-types/new", userRole, frontendJWT || undefined);
|
||||
const url = new URL(request.url);
|
||||
const editId = url.searchParams.get("id");
|
||||
|
||||
|
||||
@@ -356,12 +356,13 @@ export async function loader({ request }: LoaderFunctionArgs): Promise<Response>
|
||||
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
if (!frontendJWT || !userInfo?.role) {
|
||||
const userRole = userInfo?.role || userInfo?.user_role || '';
|
||||
if (!frontendJWT || !userRole) {
|
||||
throw redirect('/login');
|
||||
}
|
||||
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission('/reviewsTest', userInfo.role, frontendJWT);
|
||||
await requireRoutePermission('/reviewsTest', userRole, frontendJWT);
|
||||
|
||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||
|
||||
|
||||
@@ -148,8 +148,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
|
||||
await requireRoutePermission("/rule-groups", userInfo?.role || "", frontendJWT || undefined);
|
||||
await requireRoutePermission("/rule-groups", userRole, frontendJWT || undefined);
|
||||
|
||||
try {
|
||||
const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([
|
||||
|
||||
@@ -240,7 +240,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/rules/list", userInfo?.role || "", frontendJWT || undefined);
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
await requireRoutePermission("/rules/list", userRole, frontendJWT || undefined);
|
||||
|
||||
// 从 URL 参数中提取查询条件
|
||||
const params = {
|
||||
@@ -287,7 +288,8 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/rules/list", userInfo?.role || "", frontendJWT || undefined);
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
await requireRoutePermission("/rules/list", userRole, frontendJWT || undefined);
|
||||
const formData = await request.formData();
|
||||
const _action = formData.get('_action');
|
||||
const ruleId = formData.get('ruleId');
|
||||
|
||||
@@ -21,8 +21,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
const userRole = userInfo?.role || userInfo?.user_role || "";
|
||||
|
||||
await requireRoutePermission("/rules", userInfo?.role || "", frontendJWT || undefined);
|
||||
await requireRoutePermission("/rules", userRole, frontendJWT || undefined);
|
||||
|
||||
if (url.pathname === '/rules') {
|
||||
const query = url.searchParams.toString();
|
||||
|
||||
@@ -446,7 +446,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server');
|
||||
await requireRoutePermission('/rulesTest/detail', userInfo?.role || '', frontendJWT || undefined);
|
||||
const userRole = userInfo?.role || userInfo?.user_role || '';
|
||||
await requireRoutePermission('/rulesTest/detail', userRole, frontendJWT || undefined);
|
||||
if (!frontendJWT) {
|
||||
return json<ActionData>({ success: false, intent: 'save', message: '登录已失效,请重新登录后再保存。' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -87,9 +87,8 @@ function buildCollaboraIframeUrl(params: {
|
||||
}): string {
|
||||
const { collaboraUrl, wopiSrc, accessToken, mode } = params;
|
||||
|
||||
// Collabora iframe 基础 URL
|
||||
// fa80579 是 Collabora 的版本号标识,实际部署时可能需要调整
|
||||
const baseUrl = `${collaboraUrl}/browser/fa80579/cool.html`;
|
||||
// 使用稳定的 dist 入口,避免 Collabora 升级后版本号路径失效。
|
||||
const baseUrl = `${collaboraUrl}/browser/dist/cool.html`;
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ function normalizeBooleanText(value: string | boolean | undefined): boolean {
|
||||
}
|
||||
|
||||
function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string, unknown>> {
|
||||
const topLevelFields = fields.filter((field) => !field.name.includes('[*].'));
|
||||
const topLevelFields = fields.filter((field) => field.group !== '派生字段' && !field.name.includes('[*].'));
|
||||
return Array.from(groupBy(topLevelFields, (field) => field.group || '未分组').entries()).map(([group, items]) => ({
|
||||
group,
|
||||
fields: items.map((field) => ({
|
||||
@@ -343,6 +343,46 @@ function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string
|
||||
}));
|
||||
}
|
||||
|
||||
function rewriteDerivedFieldNodes(
|
||||
fields: ExtractFieldSummary[],
|
||||
existingNodes: unknown,
|
||||
): Array<Record<string, unknown>> {
|
||||
const derivedFields = fields.filter((field) => field.group === '派生字段');
|
||||
if (derivedFields.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingMap = new Map<string, Record<string, unknown>>();
|
||||
if (Array.isArray(existingNodes)) {
|
||||
existingNodes.forEach((node) => {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
const record = deepClone(node as Record<string, unknown>);
|
||||
const name = String(record.name || '').trim();
|
||||
if (name) {
|
||||
existingMap.set(name, record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return derivedFields.map((field) => {
|
||||
const existing = existingMap.get(field.name) || {};
|
||||
const nextNode: Record<string, unknown> = {
|
||||
...existing,
|
||||
name: field.name,
|
||||
type: field.type || String(existing.type || 'computed'),
|
||||
};
|
||||
const nextCompute = field.description && field.description !== '由其他字段计算得出'
|
||||
? field.description
|
||||
: String(existing.compute || '').trim();
|
||||
if (nextCompute) {
|
||||
nextNode.compute = nextCompute;
|
||||
} else {
|
||||
delete nextNode.compute;
|
||||
}
|
||||
return nextNode;
|
||||
});
|
||||
}
|
||||
|
||||
function rewriteSubDocumentNodes(subDocuments: SubDocumentSummary[]): Array<Record<string, unknown>> {
|
||||
return subDocuments.map((document) => ({
|
||||
id: document.id,
|
||||
@@ -474,7 +514,6 @@ function rewriteRuleNode(baseRule: Record<string, unknown> | undefined, rule: Ru
|
||||
}
|
||||
|
||||
delete nextRule.rules;
|
||||
delete nextRule.logic;
|
||||
|
||||
const existingStages = Array.isArray(nextRule.stages) ? (nextRule.stages as Array<Record<string, unknown>>) : [];
|
||||
const stages = existingStages.length > 0 ? deepClone(existingStages) : (buildMinimalRuleNode(rule).stages as Array<Record<string, unknown>>);
|
||||
@@ -494,6 +533,8 @@ function rewriteRuleNode(baseRule: Record<string, unknown> | undefined, rule: Ru
|
||||
}
|
||||
|
||||
nextRule.stages = stages;
|
||||
if (rule.logic?.trim()) nextRule.logic = rule.logic.trim();
|
||||
else if (typeof nextRule.logic === 'string' && !String(nextRule.logic).trim()) delete nextRule.logic;
|
||||
return nextRule;
|
||||
}
|
||||
|
||||
@@ -512,6 +553,7 @@ export function serializeEditableRuleConfig(config: EditableRuleConfig): string
|
||||
if (Array.isArray(config.metadata.inheritsFrom) && config.metadata.inheritsFrom.length > 0) metadata.inherits_from = [...config.metadata.inheritsFrom];
|
||||
root.metadata = metadata;
|
||||
root.extract = rewriteExtractNodes(config.fields);
|
||||
root.derived_fields = rewriteDerivedFieldNodes(config.fields, root.derived_fields);
|
||||
root.sub_documents = rewriteSubDocumentNodes(config.subDocuments);
|
||||
root.visual_elements = rewriteVisualElementNodes(config.visualElements);
|
||||
|
||||
|
||||
@@ -415,9 +415,9 @@ function parseRules(source: string): RuleSummary[] {
|
||||
group,
|
||||
risk: stripYamlValue(ruleBlock.match(/^\s+risk:\s*(.+)$/m)?.[1] || 'medium'),
|
||||
score: stripYamlValue(ruleBlock.match(/^\s+score:\s*(.+)$/m)?.[1] || '-'),
|
||||
type: stripYamlValue(ruleBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'deterministic'),
|
||||
type: stripYamlValue(ruleBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || 'deterministic'),
|
||||
checkTypes,
|
||||
logic: stripYamlValue(ruleBlock.match(/^\s+logic:\s*(.+)$/m)?.[1] || ''),
|
||||
logic: stripYamlValue(ruleBlock.match(/^\s{4}logic:\s*(.+)$/m)?.[1] || ''),
|
||||
subRules,
|
||||
subRuleIds: readList(ruleBlock, 'rules'),
|
||||
scope: scope.slice(0, 8),
|
||||
@@ -425,7 +425,7 @@ function parseRules(source: string): RuleSummary[] {
|
||||
stageCount: subRules.length,
|
||||
appliesIn: readFlexibleList(ruleBlock, 'applies_in'),
|
||||
prompt: prompts.join('\n\n'),
|
||||
description: stripYamlValue(ruleBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
|
||||
description: stripYamlValue(ruleBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"file": "loader.js",
|
||||
"sources": [],
|
||||
"names": [],
|
||||
"mappings": ""
|
||||
}
|
||||
Reference in New Issue
Block a user