1 Commits

Author SHA1 Message Date
wren 3da2a8d088 fix: stabilize review detail and collabora loading 2026-05-08 10:59:04 +08:00
20 changed files with 319 additions and 284 deletions
+85 -111
View File
@@ -1,82 +1,97 @@
/** /**
* Dify Chat API 模块 * 自有 RAG Chat API 模块
* *
* 提供客户端调用 Dify API 的函数 * 保持前端 dify-chat 调用面不变,内部转发到新的 /api/v3/rag/* 接口。
* 用于 Remix loader/action 中调用 Dify API
* *
* @module api/dify/chat * @module api/dify-chat/chat
*/ */
import { difyFetch } from './client.server'; import { difyFetch } from './client.server';
// ============================================================================ function unwrapResult<T>(payload: any): T {
// Dify Chat API 客户端 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 = { export const difyClient = {
/** async getApplicationParameters(jwt?: string, appId?: string): Promise<any> {
* 获取应用参数 const response = await difyFetch('chat/parameters', {
*/
async getApplicationParameters(jwt?: string): Promise<any> {
const response = await difyFetch('parameters', {
method: 'GET', method: 'GET',
appId: toIntAppId(appId),
}, jwt); }, 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> { async getConversations(jwt?: string, appId?: string): Promise<any> {
const params = new URLSearchParams({ const params = new URLSearchParams({
limit: '100', page: '1',
first_id: '', 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', method: 'GET',
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt); }, 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> { async getConversationMessages(conversationId: string, jwt?: string): Promise<any> {
const params = new URLSearchParams({ const params = new URLSearchParams({
conversation_id: conversationId, page: '1',
limit: '20', pageSize: '100',
last_id: '',
}); });
const response = await difyFetch(`chat/conversations/${conversationId}/messages?${params.toString()}`, {
const response = await difyFetch(`messages?${params}`, {
method: 'GET', method: 'GET',
}, jwt); }, 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( async createChatMessage(
inputs: Record<string, any>, inputs: Record<string, any>,
query: string, query: string,
@@ -92,99 +107,58 @@ export const difyClient = {
response_mode: responseMode, response_mode: responseMode,
conversation_id: conversationId, conversation_id: conversationId,
files: files || [], 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', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt); }, jwt);
// 对于流式响应,直接返回 Response 对象
if (responseMode === 'streaming') { if (responseMode === 'streaming') {
return response; return response;
} }
console.log('[Dify Chat] 解析 JSON 响应');
return response.json(); return response.json();
}, },
/**
* 重命名会话
*/
async renameConversation( async renameConversation(
conversationId: string, conversationId: string,
name: string, name: string,
autoGenerate: boolean = false, autoGenerate: boolean = false,
jwt?: string jwt?: string
): Promise<any> { ): Promise<any> {
const body = { let nextName = name?.trim() || '';
name,
auto_generate: autoGenerate,
};
const response = await difyFetch(`conversations/${conversationId}/name`, { if (autoGenerate || !nextName) {
method: 'POST', const messages = await this.getConversationMessages(conversationId, jwt);
body: JSON.stringify(body), 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); }, jwt);
return response.json(); return unwrapResult<any>(await response.json());
}, },
/**
* 删除会话
*/
async deleteConversation(conversationId: string, jwt?: string): Promise<any> { async deleteConversation(conversationId: string, jwt?: string): Promise<any> {
console.log('[Dify Chat] 删除会话:', conversationId); const response = await difyFetch(`chat/conversations/${conversationId}`, {
try {
const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE', method: 'DELETE',
body: JSON.stringify({}),
}, jwt); }, jwt);
return unwrapResult<any>(await response.json());
// 对于 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' };
}
}, },
/**
* 更新消息反馈
*/
async updateMessageFeedback( async updateMessageFeedback(
messageId: string, messageId: string,
rating: 'like' | 'dislike' | null, rating: 'like' | 'dislike' | null,
jwt?: string jwt?: string
): Promise<any> { ): Promise<any> {
const body = { const response = await difyFetch(`chat/messages/${messageId}/feedback`, {
rating,
};
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify({ rating }),
}, jwt); }, 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 后端的基础功能 * 提供 Node.js 服务端调用 FastAPI 后端的基础功能
* Dify 的 API_KEY 和 APP_ID 由 FastAPI 后端管理,前端只负责转发请求 * 现已改为走自有 RAG 接口:
* * Remix Server -> FastAPI /api/v3/rag/*
* 调用链路:
* Remix Server → FastAPI /dify_chat/* → Dify
* *
* @module api/dify-chat/client.server * @module api/dify-chat/client.server
*/ */
import { API_BASE_URL } from '~/config/api-config'; 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 { export interface DifyFetchOptions extends RequestInit {
/** 对话应用 ID,用于切换不同的 Dify 应用 */
appId?: string; appId?: string;
} }
/** function buildHeaders(fetchOptions: RequestInit, jwt?: string): HeadersInit {
* 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(/^\//, '')}`;
const headers: HeadersInit = { const headers: HeadersInit = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...fetchOptions.headers, ...fetchOptions.headers,
}; };
if (jwt) { if (jwt) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`; (headers as Record<string, string>).Authorization = `Bearer ${jwt}`;
} else { } 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) { if (appId) {
(headers as Record<string, string>)['X-Dify-App-Id'] = appId; const separator = url.includes('?') ? '&' : '?';
console.log('[Dify Chat] 使用应用 ID:', appId); url = `${url}${separator}appId=${encodeURIComponent(appId)}`;
} }
const response = await fetch(url, { const response = await fetch(url, {
...fetchOptions, ...fetchOptions,
headers, headers: buildHeaders(fetchOptions, jwt),
}); });
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[Dify Chat] API 转发错误:', { console.error('[RAG Chat] API 转发错误:', {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
error: errorText error: errorText,
}); });
if (response.status === 401) { if (response.status === 401) {
throw new Error('JWT认证失败,请重新登录'); 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; return response;
} }
// 重新导出 chat 模块的 difyClient
export { difyClient } from './chat'; export { difyClient } from './chat';
+29 -2
View File
@@ -77,6 +77,26 @@ export interface SsoUser {
deleted_at?: string; 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 refreshToken = session.get("refreshToken");
const tokenIssuedAt = session.get("tokenIssuedAt"); const tokenIssuedAt = session.get("tokenIssuedAt");
let tokenExpiresIn = session.get("tokenExpiresIn"); 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"); const frontendJWT = session.get("frontendJWT");
// 🔑 检查是否是公共路径(不需要认证的路径) // 🔑 检查是否是公共路径(不需要认证的路径)
@@ -369,7 +396,7 @@ export async function createUserSession(params: {
// 用户信息和JWT // 用户信息和JWT
if (params.userInfo) { if (params.userInfo) {
session.set("userInfo", params.userInfo); session.set("userInfo", compactUserInfoForSession(params.userInfo, params.userRole));
} }
if (params.frontendJWT) { if (params.frontendJWT) {
session.set("frontendJWT", params.frontendJWT); session.set("frontendJWT", params.frontendJWT);
+1 -2
View File
@@ -106,7 +106,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName);
// 2. 监听文档加载状态 // 2. 监听文档加载状态
const { isDocumentLoaded } = useDocumentReady(iframeRef); const { isDocumentLoaded } = useDocumentReady(iframeRef, config?.iframeUrl);
// 2.5. 保存 iframe window 引用并在文档加载时清除所有高亮 // 2.5. 保存 iframe window 引用并在文档加载时清除所有高亮
useEffect(() => { useEffect(() => {
@@ -1194,4 +1194,3 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
// 导出类型和 hook // 导出类型和 hook
export { useCollaboraUnoCommands }; export { useCollaboraUnoCommands };
export type { CollaboraViewerHandle }; export type { CollaboraViewerHandle };
+51 -11
View File
@@ -110,35 +110,75 @@ export function useCollaboraConfig(
* @param iframeRef - iframe 引用 * @param iframeRef - iframe 引用
* @returns 文档加载状态 * @returns 文档加载状态
*/ */
export function useDocumentReady(iframeRef: RefObject<HTMLIFrameElement>) { export function useDocumentReady(
iframeRef: RefObject<HTMLIFrameElement>,
iframeUrl?: string
) {
const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false);
useEffect(() => { useEffect(() => {
const handleMessage = (event: MessageEvent) => { setIsDocumentLoaded(false);
// 验证消息来源
const collaboraOrigin = new URL(COLLABORA_URL).origin;
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; return;
} }
try { try {
const msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; const msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (msg.MessageId === 'App_LoadingStatus' && msg.Values?.Status === 'Document_Loaded') { if (
console.log('[DocumentReady] 文档加载完成'); msg?.MessageId === 'App_LoadingStatus' &&
setIsDocumentLoaded(true); ['Document_Loaded', 'Document_Loaded_Editing', 'UI_Loaded'].includes(msg.Values?.Status)
) {
if (fallbackTimer) {
clearTimeout(fallbackTimer);
fallbackTimer = null;
} }
} catch (err) { markLoaded(`postmessage:${msg.Values?.Status}`);
console.warn('[DocumentReady] 解析消息失败:', err); }
} catch {
// Collabora 也会发送非 JSON 消息,这里忽略即可。
} }
}; };
iframe?.addEventListener('load', handleIframeLoad);
window.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage);
return () => { return () => {
if (fallbackTimer) {
clearTimeout(fallbackTimer);
}
iframe?.removeEventListener('load', handleIframeLoad);
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
}; };
}, [iframeRef]); }, [iframeRef, iframeUrl]);
return { isDocumentLoaded }; return { isDocumentLoaded };
} }
+1 -1
View File
@@ -49,7 +49,7 @@ export default function Chat() {
// 权限检查 // 权限检查
const { hasPermission: checkPerm } = usePermission(); const { hasPermission: checkPerm } = usePermission();
const canChat = checkPerm('dify:chat:use'); const canChat = checkPerm('rag:chat:use');
// 侧边栏状态 // 侧边栏状态
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+1 -1
View File
@@ -61,7 +61,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
conversationReadOnly = false, conversationReadOnly = false,
}, ref) => { }, ref) => {
const { hasPermission } = usePermission(); const { hasPermission } = usePermission();
const canDeleteConversation = hasPermission('dify:conversation:delete'); const canDeleteConversation = hasPermission('rag:conversation:delete');
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [renameModalVisible, setRenameModalVisible] = useState(false); const [renameModalVisible, setRenameModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false);
+1 -1
View File
@@ -180,7 +180,7 @@ const configs: Record<string, ApiConfig> = {
// documentUrl: 'http://172.16.0.84:8073/docauditai/', // documentUrl: 'http://172.16.0.84:8073/docauditai/',
// uploadUrl: 'http://172.16.0.84:8073/api/v2/documents', // uploadUrl: 'http://172.16.0.84:8073/api/v2/documents',
// 公网访问 reviewsTest 时,iframe 不能再直连内网 Collabora,否则浏览器会拦截 // 公网页面发起到 172.16.* 的 iframe 请求会触发浏览器私网访问拦截,联调环境必须走公网可达地址
collaboraUrl: 'http://nas.7bm.co:9980', collaboraUrl: 'http://nas.7bm.co:9980',
appUrl: 'http://nas.7bm.co:5173', appUrl: 'http://nas.7bm.co:5173',
+3 -1
View File
@@ -23,7 +23,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
); );
} }
const data = await difyClient.getApplicationParameters(frontendJWT); const url = new URL(request.url);
const appId = url.searchParams.get('app_id') || undefined;
const data = await difyClient.getApplicationParameters(frontendJWT, appId);
return json(data, { return json(data, {
headers: { headers: {
+17 -17
View File
@@ -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 { LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; 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) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
const { frontendJWT } = await getUserSession(request); 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 response = await fetch(`${API_BASE_URL}/v3/rag/apps/default`, {
// 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps/default`;
const response = await fetch(apiUrl, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`, Authorization: `Bearer ${frontendJWT}`,
}, },
}); });
const data = await response.json(); const data = await response.json();
console.log('[API] Get Default Chat App - Backend response:', data); return json({ data: normalizeApp(data) }, { status: response.status });
return json(data, { status: response.status });
} catch (error: any) { } catch (error: any) {
console.error('[API] Get Default Chat App - Error:', error.message); console.error('[API] Get Default Chat App - Error:', error.message);
return json( return json(
+24 -18
View File
@@ -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 { LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; 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) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
const { frontendJWT } = await getUserSession(request); 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'); const response = await fetch(`${API_BASE_URL}/v3/rag/apps`, {
// 转发请求到后端 - 使用正确的接口路径
// 根据文档:GET /api/v3/dify/chat-apps
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps`;
const response = await fetch(apiUrl, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`, Authorization: `Bearer ${frontendJWT}`,
}, },
}); });
const data = await response.json(); const data = await response.json();
console.log('[API] Get My Chat Apps - Backend response:', data); const normalized = normalizeApps(data);
return json(normalized, { status: response.status });
return json(data, { status: response.status });
} catch (error: any) { } catch (error: any) {
console.error('[API] Get My Chat Apps - Error:', error.message); console.error('[API] Get My Chat Apps - Error:', error.message);
return json( return json(
+2 -1
View File
@@ -28,8 +28,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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); const rootsRes = await getDocumentTypeRoots({}, frontendJWT);
return { return {
+2 -1
View File
@@ -35,7 +35,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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 url = new URL(request.url);
const editId = url.searchParams.get("id"); const editId = url.searchParams.get("id");
+3 -2
View File
@@ -356,12 +356,13 @@ export async function loader({ request }: LoaderFunctionArgs): Promise<Response>
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request); const { userInfo, frontendJWT } = await getUserSession(request);
if (!frontendJWT || !userInfo?.role) { const userRole = userInfo?.role || userInfo?.user_role || '';
if (!frontendJWT || !userRole) {
throw redirect('/login'); throw redirect('/login');
} }
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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); const reviewData = await getReviewPoints_fromApi(id, request);
+2 -1
View File
@@ -148,8 +148,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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 { try {
const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([ const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([
+4 -2
View File
@@ -240,7 +240,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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 参数中提取查询条件 // 从 URL 参数中提取查询条件
const params = { const params = {
@@ -287,7 +288,8 @@ export async function action({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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 formData = await request.formData();
const _action = formData.get('_action'); const _action = formData.get('_action');
const ruleId = formData.get('ruleId'); const ruleId = formData.get('ruleId');
+2 -1
View File
@@ -21,8 +21,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server"); const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); 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') { if (url.pathname === '/rules') {
const query = url.searchParams.toString(); const query = url.searchParams.toString();
+2 -1
View File
@@ -446,7 +446,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const { frontendJWT, userInfo } = await getUserSession(request); const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server'); 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) { if (!frontendJWT) {
return json<ActionData>({ success: false, intent: 'save', message: '登录已失效,请重新登录后再保存。' }, { status: 401 }); return json<ActionData>({ success: false, intent: 'save', message: '登录已失效,请重新登录后再保存。' }, { status: 401 });
} }
+2 -3
View File
@@ -87,9 +87,8 @@ function buildCollaboraIframeUrl(params: {
}): string { }): string {
const { collaboraUrl, wopiSrc, accessToken, mode } = params; const { collaboraUrl, wopiSrc, accessToken, mode } = params;
// Collabora iframe 基础 URL // 使用稳定的 dist 入口,避免 Collabora 升级后版本号路径失效。
// fa80579 是 Collabora 的版本号标识,实际部署时可能需要调整 const baseUrl = `${collaboraUrl}/browser/dist/cool.html`;
const baseUrl = `${collaboraUrl}/browser/fa80579/cool.html`;
const url = new URL(baseUrl); const url = new URL(baseUrl);
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"file": "loader.js",
"sources": [],
"names": [],
"mappings": ""
}