重构Dify客户端:改为通过FastAPI代理并使用JWT认证
主要变更: - 修改 dify-client.server.ts 使用 JWT 认证通过 FastAPI 后端代理访问 Dify API - 所有 Dify API 路由(chat-messages, parameters, conversations, messages)添加 JWT 获取和传递逻辑 - API_URL 从直连 Dify 改为 FastAPI 后端的 /dify 路由 - 增强 JWT 认证失败的错误处理(返回401状态码) - 添加详细的日志输出,便于调试 安全提升: - DIFY_API_KEY 从前端移至后端,不再暴露在客户端代码 - 使用统一的 JWT 认证体系,提高系统安全性 文档: - 新增 dify-proxy-backend-integration.md - 后端对接文档(包含完整 FastAPI 实现示例) - 新增 dify-frontend-modification-summary.md - 前端修改总结 - 新增 CLAUDE.md - 项目架构说明文档 影响范围: - app/services/dify-client.server.ts - 核心服务层 - app/routes/api.chat-messages.tsx - 聊天消息 - app/routes/api.parameters.tsx - 应用参数 - app/routes/api.conversations.tsx - 会话列表 - app/routes/api.messages.tsx - 消息历史 - app/routes/api.conversations.$id.tsx - 删除会话 - app/routes/api.conversations.$id.name.tsx - 重命名会话 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* API配置文件
|
||||
* 统一管理所有API地址,方便部署时修改
|
||||
* 支持环境变量覆盖配置
|
||||
*/
|
||||
// 环境配置类型
|
||||
interface ApiConfig {
|
||||
// 主API基础URL
|
||||
baseUrl: string;
|
||||
// 文档服务URL
|
||||
documentUrl: string;
|
||||
// 文档上传API URL
|
||||
uploadUrl: string;
|
||||
// OAuth2.0配置
|
||||
oauth: {
|
||||
// IDaaS服务器地址
|
||||
serverUrl: string;
|
||||
// OAuth2应用Client ID
|
||||
clientId: string;
|
||||
// OAuth2应用Client Secret
|
||||
clientSecret: string;
|
||||
// 回调地址
|
||||
redirectUri: string;
|
||||
// 应用ID(用于登出)
|
||||
appId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 端口特定配置映射
|
||||
// 根据不同端口提供不同的API配置
|
||||
const portConfigs: Record<string, Partial<ApiConfig>> = {
|
||||
|
||||
// 测试主要服务实例
|
||||
'5173': {
|
||||
baseUrl: 'http://172.16.0.55:8000',
|
||||
documentUrl: 'http://172.16.0.55:8000/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8000/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5174': {
|
||||
baseUrl: 'http://172.16.0.55:5174',
|
||||
documentUrl: 'http://172.16.0.55:5174/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5174/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5175': {
|
||||
baseUrl: 'http://172.16.0.55:5175',
|
||||
documentUrl: 'http://172.16.0.55:5175/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5175/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5176': {
|
||||
baseUrl: 'http://172.16.0.55:5176',
|
||||
documentUrl: 'http://172.16.0.55:5176/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5176/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5177': {
|
||||
baseUrl: 'http://172.16.0.55:5177',
|
||||
documentUrl: 'http://172.16.0.55:5177/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:5177/admin/documents'
|
||||
},
|
||||
// 测试客户端实例
|
||||
'5178': {
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents'
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 主要
|
||||
// 梅州
|
||||
'51703': {
|
||||
baseUrl: 'http://172.16.0.55:8073',
|
||||
documentUrl: 'http://172.16.0.55:8073/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8073/admin/documents'
|
||||
// baseUrl: 'http://nas.7bm.co:8873',
|
||||
// documentUrl: 'http://nas.7bm.co:8873/docauditai/',
|
||||
// uploadUrl: 'http://nas.7bm.co:8873/admin/documents'
|
||||
},
|
||||
|
||||
|
||||
// 云浮
|
||||
'51704': {
|
||||
baseUrl: 'http://10.79.97.17:8001',
|
||||
documentUrl: 'http://10.79.97.17:8001/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8001/admin/documents'
|
||||
},
|
||||
|
||||
// 揭阳
|
||||
'51705': {
|
||||
baseUrl: 'http://10.79.97.17:8002',
|
||||
documentUrl: 'http://10.79.97.17:8002/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8002/admin/documents'
|
||||
},
|
||||
|
||||
// 潮州
|
||||
'51706': {
|
||||
baseUrl: 'http://10.79.97.17:8003',
|
||||
documentUrl: 'http://10.79.97.17:8003/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8003/admin/documents'
|
||||
},
|
||||
|
||||
// 省局
|
||||
'51707': {
|
||||
baseUrl: 'http://10.79.97.17:8004',
|
||||
documentUrl: 'http://10.79.97.17:8004/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8004/admin/documents'
|
||||
},
|
||||
//test
|
||||
'51708': {
|
||||
baseUrl: 'http://10.79.97.17:8005',
|
||||
documentUrl: 'http://10.79.97.17:8005/docauditai/',
|
||||
uploadUrl: 'http://10.79.97.17:8005/admin/documents'
|
||||
},
|
||||
};
|
||||
|
||||
// 不同环境的默认配置
|
||||
// 由于合同模板的上传,后续的的uploadUrl都不需要/upload,直接写/admin/documents,由程序自动添加/upload或/upload_contract_template
|
||||
const configs: Record<string, ApiConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
baseUrl: 'http://172.16.0.55:8000',
|
||||
documentUrl: 'http://172.16.0.55:8000/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8000/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: 'none',
|
||||
clientSecret: 'none', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
|
||||
// 测试环境
|
||||
testing: {
|
||||
baseUrl: 'http://nas.7bm.co:8873',
|
||||
documentUrl: 'http://nas.7bm.co:8873/docauditai/',
|
||||
uploadUrl: 'http://nas.7bm.co:8873/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'placeholder', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
|
||||
// 生产环境
|
||||
production: {
|
||||
// postgrest
|
||||
baseUrl: 'http://10.79.97.17:8000',
|
||||
// minio
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
// 文件上传
|
||||
uploadUrl: 'http://10.79.97.17:8000/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
// clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
|
||||
// ⚠️ 安全警告:clientSecret 不应该硬编码在代码中
|
||||
// 请在生产环境使用环境变量 OAUTH_CLIENT_SECRET
|
||||
clientSecret: 'placeholder', // 占位符,实际值从环境变量获取
|
||||
redirectUri: 'http://10.79.97.17/', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
},
|
||||
|
||||
// 备用配置 (可以根据需要添加更多环境)
|
||||
staging: {
|
||||
baseUrl: 'http://172.16.0.119:9000/admin',
|
||||
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.119:8000/admin/documents/upload',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: 'none', // 需要替换为实际的Client ID
|
||||
clientSecret: 'your_client_secret', // 需要替换为实际的Client Secret
|
||||
redirectUri: 'http://172.16.0.119:3000/callback', // 回调地址
|
||||
appId: 'idaasoauth2' // 应用ID,用于登出
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前环境,默认为development
|
||||
const getCurrentEnvironment = (): string => {
|
||||
// 在服务器端,优先使用PM2设置的环境变量
|
||||
if (typeof window === 'undefined') {
|
||||
// 服务器端:直接使用process.env.NODE_ENV
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
console.log('🔧 服务器端环境检测:', {
|
||||
NODE_ENV: nodeEnv,
|
||||
result: nodeEnv || 'development'
|
||||
});
|
||||
return nodeEnv || 'development';
|
||||
}
|
||||
|
||||
// 客户端:优先使用NEXT_PUBLIC_前缀的环境变量
|
||||
const nextPublicNodeEnv = process.env.NEXT_PUBLIC_NODE_ENV;
|
||||
const nodeEnv = process.env.NODE_ENV;
|
||||
const result = nextPublicNodeEnv || nodeEnv || 'development';
|
||||
|
||||
console.log('🔧 客户端环境检测:', {
|
||||
NEXT_PUBLIC_NODE_ENV: nextPublicNodeEnv,
|
||||
NODE_ENV: nodeEnv,
|
||||
result: result
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 从环境变量获取配置,如果环境变量不存在则使用默认配置
|
||||
const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
|
||||
return {
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || defaultConfig.baseUrl,
|
||||
documentUrl: process.env.NEXT_PUBLIC_DOCUMENT_URL || defaultConfig.documentUrl,
|
||||
uploadUrl: process.env.NEXT_PUBLIC_UPLOAD_URL || defaultConfig.uploadUrl,
|
||||
oauth: {
|
||||
serverUrl: process.env.NEXT_PUBLIC_OAUTH_SERVER_URL || defaultConfig.oauth.serverUrl,
|
||||
clientId: process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID || defaultConfig.oauth.clientId,
|
||||
// ⚠️ 注意:clientSecret 不应该使用 NEXT_PUBLIC_ 前缀
|
||||
// 应该只在服务器端通过 process.env.OAUTH_CLIENT_SECRET 访问
|
||||
clientSecret: process.env.OAUTH_CLIENT_SECRET || defaultConfig.oauth.clientSecret,
|
||||
redirectUri: process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI || defaultConfig.oauth.redirectUri,
|
||||
appId: process.env.NEXT_PUBLIC_OAUTH_APP_ID || defaultConfig.oauth.appId
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前端口号
|
||||
* 优先从浏览器location获取,然后从环境变量获取
|
||||
*/
|
||||
const getCurrentPort = (): string => {
|
||||
// 在客户端,优先从浏览器location获取端口
|
||||
let windowPort = '';
|
||||
if (typeof window !== 'undefined') {
|
||||
windowPort = window.location.port || '';
|
||||
}
|
||||
|
||||
// 在服务器端,优先使用运行时端口检测
|
||||
if (typeof window === 'undefined') {
|
||||
const runtimePort = getRuntimePort();
|
||||
if (runtimePort) {
|
||||
console.log('🔧 服务器端运行时端口检测:', runtimePort);
|
||||
return runtimePort;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用环境变量中的端口配置
|
||||
const nextPublicApiPortConfig = process.env.NEXT_PUBLIC_API_PORT_CONFIG;
|
||||
const nextPublicPort = process.env.NEXT_PUBLIC_PORT;
|
||||
const apiPortConfig = process.env.API_PORT_CONFIG;
|
||||
const portEnv = process.env.PORT;
|
||||
|
||||
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
||||
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
||||
|
||||
console.log('🔧 端口检测:', {
|
||||
windowPort: windowPort,
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
||||
NEXT_PUBLIC_PORT: nextPublicPort,
|
||||
API_PORT_CONFIG: apiPortConfig,
|
||||
PORT: portEnv,
|
||||
result: result
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 运行时端口检测 - 从服务器启动参数或环境变量获取实际端口
|
||||
* 这个方法只在服务器端运行,用于动态获取实际运行端口
|
||||
*/
|
||||
const getRuntimePort = (): string => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return ''; // 客户端不执行此逻辑
|
||||
}
|
||||
|
||||
// 尝试从进程参数中获取端口
|
||||
const args = process.argv;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' && i + 1 < args.length) {
|
||||
return args[i + 1];
|
||||
}
|
||||
if (args[i].startsWith('--port=')) {
|
||||
return args[i].split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 从环境变量获取
|
||||
return process.env.PORT || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
* 支持根据端口动态切换API配置
|
||||
*/
|
||||
const getCurrentConfig = (): ApiConfig => {
|
||||
const env = getCurrentEnvironment();
|
||||
const port = getCurrentPort();
|
||||
|
||||
console.log('🔧 配置调试信息:', {
|
||||
environment: env,
|
||||
port: port,
|
||||
hasPortConfig: !!(port && portConfigs[port]),
|
||||
portConfig: port ? portConfigs[port] : null
|
||||
});
|
||||
|
||||
// 获取基础配置
|
||||
let defaultConfig = configs[env] || configs.development;
|
||||
|
||||
// 如果有端口特定配置,则合并配置
|
||||
if (port && portConfigs[port]) {
|
||||
console.log(`🔧 使用端口特定配置: ${port}`, portConfigs[port]);
|
||||
defaultConfig = {
|
||||
...defaultConfig,
|
||||
...portConfigs[port],
|
||||
// 保持oauth配置不变,只覆盖API相关配置
|
||||
oauth: defaultConfig.oauth
|
||||
};
|
||||
} else {
|
||||
console.log(`🔧 使用环境配置: ${env}`, defaultConfig);
|
||||
}
|
||||
|
||||
// 只有在明确设置了环境变量的情况下才覆盖配置
|
||||
const hasEnvOverrides = process.env.NEXT_PUBLIC_API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_DOCUMENT_URL ||
|
||||
process.env.NEXT_PUBLIC_UPLOAD_URL;
|
||||
|
||||
if (hasEnvOverrides) {
|
||||
console.log('🔧 检测到环境变量覆盖,使用环境变量配置');
|
||||
return getConfigFromEnv(defaultConfig);
|
||||
}
|
||||
|
||||
console.log('🔧 最终配置:', defaultConfig);
|
||||
return defaultConfig;
|
||||
};
|
||||
|
||||
// 导出当前环境的配置
|
||||
export const apiConfig = getCurrentConfig();
|
||||
|
||||
// 导出具体的配置项,方便使用
|
||||
export const {
|
||||
baseUrl: API_BASE_URL,
|
||||
documentUrl: DOCUMENT_URL,
|
||||
uploadUrl: UPLOAD_URL,
|
||||
oauth: OAUTH_CONFIG
|
||||
} = apiConfig;
|
||||
|
||||
/**
|
||||
* 🔓 客户端安全的 OAuth 配置(不包含 clientSecret)
|
||||
* 可以安全地在客户端代码中使用
|
||||
*/
|
||||
export const CLIENT_OAUTH_CONFIG = {
|
||||
serverUrl: OAUTH_CONFIG.serverUrl,
|
||||
clientId: OAUTH_CONFIG.clientId,
|
||||
redirectUri: OAUTH_CONFIG.redirectUri,
|
||||
appId: OAUTH_CONFIG.appId,
|
||||
// 客户端不需要 clientSecret
|
||||
};
|
||||
|
||||
// 导出所有配置,供调试使用
|
||||
export { configs };
|
||||
|
||||
// 工具函数:设置环境(主要用于测试)
|
||||
export const setEnvironment = (env: string): ApiConfig => {
|
||||
return configs[env] || configs.development;
|
||||
};
|
||||
|
||||
/**
|
||||
* 工具函数:获取当前端口配置信息(用于调试)
|
||||
*/
|
||||
export const getCurrentPortConfig = () => {
|
||||
const port = getCurrentPort();
|
||||
const env = getCurrentEnvironment();
|
||||
return {
|
||||
currentPort: port,
|
||||
currentEnvironment: env,
|
||||
hasPortConfig: !!(port && portConfigs[port]),
|
||||
portConfig: port ? portConfigs[port] : null
|
||||
};
|
||||
};
|
||||
|
||||
// 调试信息(仅在开发环境显示)
|
||||
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'testing') {
|
||||
console.log('📦 API配置信息:', {
|
||||
environment: getCurrentEnvironment(),
|
||||
currentEnv: process.env.NODE_ENV,
|
||||
port: getCurrentPort(),
|
||||
config: {
|
||||
...apiConfig,
|
||||
oauth: {
|
||||
...apiConfig.oauth,
|
||||
clientSecret: '***' // 隐藏敏感信息
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,23 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户会话信息和 JWT
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const { user } = await getSessionInfo(request);
|
||||
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Chat Messages API - JWT不存在');
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const {
|
||||
@@ -27,7 +43,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
responseMode,
|
||||
hasInputs: !!inputs,
|
||||
hasFiles: !!files && files.length > 0,
|
||||
filesCount: files?.length || 0
|
||||
filesCount: files?.length || 0,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
const response = await difyClient.createChatMessage(
|
||||
@@ -36,7 +53,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
user,
|
||||
responseMode,
|
||||
conversationId,
|
||||
files
|
||||
files,
|
||||
frontendJWT // 传递 JWT
|
||||
);
|
||||
|
||||
console.log('📡 [API] Dify响应状态:', {
|
||||
@@ -77,10 +95,14 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
});
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to send message' }),
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息和 JWT
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const { user, session } = await getSessionInfo(request);
|
||||
const { id } = params;
|
||||
|
||||
@@ -11,15 +14,35 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return json({ error: '会话ID不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Rename Conversation API - JWT不存在');
|
||||
return json(
|
||||
{ error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { auto_generate, name } = body;
|
||||
|
||||
// console.log('💬 Rename Conversation API - User:', user, 'ID:', id, 'Auto Generate:', auto_generate, 'Name:', name);
|
||||
console.log('💬 [API] Rename Conversation API - 重命名会话:', {
|
||||
user,
|
||||
id,
|
||||
autoGenerate: auto_generate,
|
||||
name,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
// 调用服务端API重命名会话
|
||||
const data = await difyClient.renameConversation(id, name, user, auto_generate);
|
||||
const data = await difyClient.renameConversation(id, name, user, auto_generate, frontendJWT);
|
||||
|
||||
// console.log('✅ Rename Conversation API - Success:', data);
|
||||
console.log('✅ [API] Rename Conversation API - Success');
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
@@ -27,13 +50,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ Rename Conversation API - Error:', error);
|
||||
console.error('❌ [API] Rename Conversation API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return json(
|
||||
{
|
||||
error: error.message || '重命名会话失败'
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息和 JWT
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const { user, session } = await getSessionInfo(request);
|
||||
const { id } = params;
|
||||
|
||||
@@ -11,15 +14,33 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return json({ error: '会话ID不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Delete Conversation API - JWT不存在');
|
||||
return json(
|
||||
{ error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const method = request.method;
|
||||
|
||||
if (method === 'DELETE') {
|
||||
// console.log('🗑️ Delete Conversation API - User:', user, 'ID:', id);
|
||||
console.log('🗑️ [API] Delete Conversation API - 删除会话:', {
|
||||
user,
|
||||
id,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
// 调用服务端API删除会话
|
||||
const data = await difyClient.deleteConversation(id, user);
|
||||
const data = await difyClient.deleteConversation(id, user, frontendJWT);
|
||||
|
||||
// console.log('✅ Delete Conversation API - Success:', data);
|
||||
console.log('✅ [API] Delete Conversation API - Success');
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
@@ -30,13 +51,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
|
||||
return json({ error: '不支持的请求方法' }, { status: 405 });
|
||||
} catch (error: any) {
|
||||
console.error('❌ Delete Conversation API - Error:', error);
|
||||
console.error('❌ [API] Delete Conversation API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return json(
|
||||
{
|
||||
error: error.message || '删除会话失败'
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
|
||||
@@ -4,13 +4,33 @@ import { getSessionInfo, commitSession } 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 { user, session } = await getSessionInfo(request);
|
||||
|
||||
// ('💬 Conversations API - User:', user);
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Conversations API - JWT不存在');
|
||||
return json(
|
||||
{ data: [], error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const data = await difyClient.getConversations(user);
|
||||
console.log('💬 [API] Conversations API - 获取会话列表:', {
|
||||
user,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
// ('✅ Conversations API - Success:', data);
|
||||
const data = await difyClient.getConversations(user, frontendJWT);
|
||||
|
||||
console.log('✅ [API] Conversations API - Success');
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
@@ -18,14 +38,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ Conversations API - Error:', error);
|
||||
console.error('❌ [API] Conversations API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return json(
|
||||
{
|
||||
data: [],
|
||||
error: error.message || 'Failed to fetch conversations'
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } 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 { user, session } = await getSessionInfo(request);
|
||||
const url = new URL(request.url);
|
||||
const conversationId = url.searchParams.get('conversation_id');
|
||||
@@ -15,11 +18,29 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
// ('📨 Messages API - User:', user, 'ConversationId:', conversationId);
|
||||
// 检查 JWT 是否存在
|
||||
if (!frontendJWT) {
|
||||
console.error('❌ [API] Messages API - JWT不存在');
|
||||
return json(
|
||||
{ error: 'JWT认证失败,请重新登录' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const data = await difyClient.getConversationMessages(user, conversationId);
|
||||
console.log('📨 [API] Messages API - 获取会话消息:', {
|
||||
user,
|
||||
conversationId,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
// ('✅ Messages API - Success:', data);
|
||||
const data = await difyClient.getConversationMessages(user, conversationId, frontendJWT);
|
||||
|
||||
console.log('✅ [API] Messages API - Success');
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
@@ -27,11 +48,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ Messages API - Error:', error);
|
||||
console.error('❌ [API] Messages API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return json(
|
||||
{ error: error.message || 'Failed to fetch messages' },
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
|
||||
@@ -4,13 +4,33 @@ import { getSessionInfo, commitSession } 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 { user, session } = await getSessionInfo(request);
|
||||
|
||||
// ('📋 Parameters API - User:', user);
|
||||
// 检查 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(user);
|
||||
console.log('📋 [API] Parameters API - 获取应用参数:', {
|
||||
user,
|
||||
hasJWT: !!frontendJWT
|
||||
});
|
||||
|
||||
// ('✅ Parameters API - Success:', data);
|
||||
const data = await difyClient.getApplicationParameters(user, frontendJWT);
|
||||
|
||||
console.log('✅ [API] Parameters API - Success');
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
@@ -18,11 +38,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ Parameters API - Error:', error);
|
||||
console.error('❌ [API] Parameters API - Error:', error);
|
||||
|
||||
// 检查是否是JWT认证失败
|
||||
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
|
||||
|
||||
return json(
|
||||
{ error: error.message || 'Failed to fetch parameters' },
|
||||
{
|
||||
status: 500,
|
||||
status,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
// 获取环境变量的服务端函数
|
||||
const getServerEnvVar = (name: string, defaultValue: string = '') => {
|
||||
@@ -12,8 +12,11 @@ const getServerEnvVar = (name: string, defaultValue: string = '') => {
|
||||
};
|
||||
|
||||
// Dify API 客户端配置
|
||||
// 注意:现在通过 FastAPI 后端的 /dify 路由代理访问 Dify,使用 JWT 认证
|
||||
const DIFY_CONFIG = {
|
||||
API_URL: getServerEnvVar('NEXT_PUBLIC_API_URL', 'https://api.dify.ai/v1'),
|
||||
// API_URL 指向 FastAPI 后端的 /dify 路由
|
||||
API_URL: `${API_BASE_URL}/dify`,
|
||||
// API_KEY 保留用于配置验证(实际不再使用,改用JWT)
|
||||
API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''),
|
||||
APP_ID: (() => {
|
||||
const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', '');
|
||||
@@ -27,24 +30,32 @@ console.log('🔧 Dify Client Config:', {
|
||||
apiUrl: DIFY_CONFIG.API_URL,
|
||||
appId: DIFY_CONFIG.APP_ID,
|
||||
hasApiKey: !!DIFY_CONFIG.API_KEY,
|
||||
configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID && DIFY_CONFIG.API_KEY)
|
||||
configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID)
|
||||
});
|
||||
|
||||
// 基础请求函数
|
||||
const difyFetch = async (endpoint: string, options: RequestInit = {}) => {
|
||||
// 基础请求函数 - 使用 JWT 认证通过 FastAPI 代理访问 Dify
|
||||
const difyFetch = async (endpoint: string, options: RequestInit = {}, jwt?: string) => {
|
||||
const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`;
|
||||
|
||||
const headers = {
|
||||
// 使用 JWT 认证而非 API_KEY
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${DIFY_CONFIG.API_KEY}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
// console.log('🌐 Dify API Request:', {
|
||||
// url,
|
||||
// method: options.method || 'GET',
|
||||
// hasAuth: !!DIFY_CONFIG.API_KEY
|
||||
// });
|
||||
// 如果提供了 JWT,添加到请求头
|
||||
if (jwt) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`;
|
||||
} else {
|
||||
console.warn('⚠️ [DifyClient] 没有提供 JWT,请求可能失败');
|
||||
}
|
||||
|
||||
console.log('🌐 [DifyClient] Dify API Request:', {
|
||||
url,
|
||||
method: options.method || 'GET',
|
||||
hasJWT: !!jwt,
|
||||
jwtPreview: jwt ? `${jwt.substring(0, 20)}...` : 'none'
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
@@ -53,11 +64,17 @@ const difyFetch = async (endpoint: string, options: RequestInit = {}) => {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Dify API Error:', {
|
||||
console.error('❌ [DifyClient] Dify API Error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
|
||||
// 如果是401错误,说明JWT过期或无效
|
||||
if (response.status === 401) {
|
||||
throw new Error('JWT认证失败,请重新登录');
|
||||
}
|
||||
|
||||
throw new Error(`Dify API Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
@@ -69,21 +86,18 @@ const generateUserId = (sessionId: string) => {
|
||||
return `user_${DIFY_CONFIG.APP_ID}:${sessionId}`;
|
||||
};
|
||||
|
||||
// Dify API 客户端
|
||||
// Dify API 客户端 - 所有方法都需要传入 JWT
|
||||
export const difyClient = {
|
||||
// 获取应用参数
|
||||
async getApplicationParameters(user: string) {
|
||||
async getApplicationParameters(user: string, jwt?: string) {
|
||||
const response = await difyFetch('parameters', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${DIFY_CONFIG.API_KEY}`,
|
||||
},
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 获取会话列表
|
||||
async getConversations(user: string) {
|
||||
async getConversations(user: string, jwt?: string) {
|
||||
const params = new URLSearchParams({
|
||||
user,
|
||||
limit: '100',
|
||||
@@ -92,12 +106,12 @@ export const difyClient = {
|
||||
|
||||
const response = await difyFetch(`conversations?${params}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 获取会话消息
|
||||
async getConversationMessages(user: string, conversationId: string) {
|
||||
async getConversationMessages(user: string, conversationId: string, jwt?: string) {
|
||||
const params = new URLSearchParams({
|
||||
user,
|
||||
conversation_id: conversationId,
|
||||
@@ -107,7 +121,7 @@ export const difyClient = {
|
||||
|
||||
const response = await difyFetch(`messages?${params}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
|
||||
@@ -118,7 +132,8 @@ export const difyClient = {
|
||||
user: string,
|
||||
responseMode: string = 'streaming',
|
||||
conversationId?: string,
|
||||
files?: any[]
|
||||
files?: any[],
|
||||
jwt?: string
|
||||
) {
|
||||
const body = {
|
||||
inputs,
|
||||
@@ -138,13 +153,14 @@ export const difyClient = {
|
||||
hasInputs: !!inputs && Object.keys(inputs).length > 0,
|
||||
inputsKeys: inputs ? Object.keys(inputs) : [],
|
||||
hasFiles: !!files && files.length > 0,
|
||||
filesCount: files?.length || 0
|
||||
filesCount: files?.length || 0,
|
||||
hasJWT: !!jwt
|
||||
});
|
||||
|
||||
const response = await difyFetch('chat-messages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}, jwt);
|
||||
|
||||
console.log('📡 [DifyClient] Dify API响应:', {
|
||||
status: response.status,
|
||||
@@ -165,7 +181,7 @@ export const difyClient = {
|
||||
},
|
||||
|
||||
// 重命名会话
|
||||
async renameConversation(conversationId: string, name: string, user: string, autoGenerate: boolean = false) {
|
||||
async renameConversation(conversationId: string, name: string, user: string, autoGenerate: boolean = false, jwt?: string) {
|
||||
const body = {
|
||||
name,
|
||||
auto_generate: autoGenerate,
|
||||
@@ -175,12 +191,12 @@ export const difyClient = {
|
||||
const response = await difyFetch(`conversations/${conversationId}/name`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 删除会话
|
||||
async deleteConversation(conversationId: string, user: string) {
|
||||
async deleteConversation(conversationId: string, user: string, jwt?: string) {
|
||||
const body = {
|
||||
user,
|
||||
};
|
||||
@@ -188,12 +204,12 @@ export const difyClient = {
|
||||
const response = await difyFetch(`conversations/${conversationId}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// 更新消息反馈
|
||||
async updateMessageFeedback(messageId: string, rating: 'like' | 'dislike' | null, user: string) {
|
||||
async updateMessageFeedback(messageId: string, rating: 'like' | 'dislike' | null, user: string, jwt?: string) {
|
||||
const body = {
|
||||
rating,
|
||||
user,
|
||||
@@ -202,7 +218,7 @@ export const difyClient = {
|
||||
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user