cf6e9c2421
根本问题:客户端代码直接调用Dify API(12980端口),绕过了服务端代理 修改内容: 1. app/config/api-config.ts - 添加独立的 difyBaseUrl 配置(指向外网 nas.7bm.co:8000) - 导出 DIFY_BASE_URL 供服务端使用 2. app/config/chat.ts - 移除直接Dify API配置(NEXT_PUBLIC_API_URL, APP_ID, API_KEY) - 移除 generateUserId 函数 - API_URL 改为 '/api'(指向Remix API routes) 3. app/services/api.client.ts - 所有fetch调用改为相对路径 /api/* - 移除所有 Authorization 头(服务端自动处理JWT) - 移除所有 user 参数传递(服务端从JWT提取) - credentials 改为 'include' 以携带cookie 4. app/services/dify-client.server.ts - 使用 DIFY_BASE_URL 替代 API_BASE_URL 5. app/utils/dify-test.client.ts - 测试函数改为调用Remix API routes 调用链路: 客户端 → /api/* → Remix API routes → dify-client.server.ts → FastAPI /dify → Dify 解决问题: - ✅ 不再直接调用 nas.7bm.co:12980(Dify端口) - ✅ 统一通过 nas.7bm.co:8000/dify(FastAPI代理) - ✅ 所有请求都经过JWT认证 - ✅ user字段由后端自动管理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
import { DIFY_BASE_URL } from '~/config/api-config';
|
||
|
||
// 获取环境变量的服务端函数
|
||
const getServerEnvVar = (name: string, defaultValue: string = '') => {
|
||
const value = process.env[name] || defaultValue;
|
||
// console.log(`🌐 [DifyClient] 读取环境变量 ${name}:`, {
|
||
// hasValue: !!process.env[name],
|
||
// value: process.env[name] ? `${process.env[name].substring(0, 20)}...` : 'undefined',
|
||
// usingDefault: !process.env[name]
|
||
// });
|
||
return value;
|
||
};
|
||
|
||
// Dify API 客户端配置
|
||
// 注意:现在通过 FastAPI 后端的 /dify 路由代理访问 Dify,使用 JWT 认证
|
||
const DIFY_CONFIG = {
|
||
// API_URL 指向 FastAPI 后端的 /dify 路由(使用外网地址 nas.7bm.co:8000)
|
||
API_URL: `${DIFY_BASE_URL}/dify`,
|
||
// API_KEY 保留用于配置验证(实际不再使用,改用JWT)
|
||
API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''),
|
||
APP_ID: (() => {
|
||
const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', '');
|
||
// 从完整URL中提取APP ID
|
||
const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/);
|
||
return match ? match[1] : rawAppId;
|
||
})(),
|
||
};
|
||
|
||
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)
|
||
});
|
||
|
||
// 基础请求函数 - 使用 JWT 认证通过 FastAPI 代理访问 Dify
|
||
const difyFetch = async (endpoint: string, options: RequestInit = {}, jwt?: string) => {
|
||
const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`;
|
||
|
||
// 使用 JWT 认证而非 API_KEY
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...options.headers,
|
||
};
|
||
|
||
// 如果提供了 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,
|
||
headers,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
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}`);
|
||
}
|
||
|
||
return response;
|
||
};
|
||
|
||
// Dify API 客户端 - 所有方法都需要传入 JWT
|
||
// 注意:user 参数已移除,由后端自动从 JWT 中提取 username
|
||
export const difyClient = {
|
||
// 获取应用参数
|
||
async getApplicationParameters(jwt?: string) {
|
||
const response = await difyFetch('parameters', {
|
||
method: 'GET',
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
|
||
// 获取会话列表
|
||
async getConversations(jwt?: string) {
|
||
const params = new URLSearchParams({
|
||
limit: '100',
|
||
first_id: '',
|
||
});
|
||
|
||
const response = await difyFetch(`conversations?${params}`, {
|
||
method: 'GET',
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
|
||
// 获取会话消息
|
||
async getConversationMessages(conversationId: string, jwt?: string) {
|
||
const params = new URLSearchParams({
|
||
conversation_id: conversationId,
|
||
limit: '20',
|
||
last_id: '',
|
||
});
|
||
|
||
const response = await difyFetch(`messages?${params}`, {
|
||
method: 'GET',
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
|
||
// 发送聊天消息
|
||
async createChatMessage(
|
||
inputs: Record<string, any>,
|
||
query: string,
|
||
responseMode: string = 'streaming',
|
||
conversationId?: string,
|
||
files?: any[],
|
||
jwt?: string
|
||
) {
|
||
const body = {
|
||
inputs,
|
||
query,
|
||
// user 字段已移除,后端会自动从 JWT 中提取 username
|
||
response_mode: responseMode,
|
||
conversation_id: conversationId,
|
||
files: files || [],
|
||
};
|
||
|
||
console.log('🌐 [DifyClient] 发送聊天消息:', {
|
||
queryLength: query.length,
|
||
queryPreview: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
|
||
responseMode,
|
||
conversationId,
|
||
hasInputs: !!inputs && Object.keys(inputs).length > 0,
|
||
inputsKeys: inputs ? Object.keys(inputs) : [],
|
||
hasFiles: !!files && 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,
|
||
statusText: response.statusText,
|
||
hasBody: !!response.body,
|
||
contentType: response.headers.get('Content-Type'),
|
||
responseMode
|
||
});
|
||
|
||
// 对于流式响应,直接返回Response对象
|
||
if (responseMode === 'streaming') {
|
||
console.log('🌊 [DifyClient] 返回流式响应对象');
|
||
return response;
|
||
}
|
||
|
||
console.log('📄 [DifyClient] 解析JSON响应');
|
||
return response.json();
|
||
},
|
||
|
||
// 重命名会话
|
||
async renameConversation(conversationId: string, name: string, autoGenerate: boolean = false, jwt?: string) {
|
||
const body = {
|
||
name,
|
||
auto_generate: autoGenerate,
|
||
// user 字段已移除,后端会自动从 JWT 中提取 username
|
||
};
|
||
|
||
const response = await difyFetch(`conversations/${conversationId}/name`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(body),
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
|
||
// 删除会话
|
||
async deleteConversation(conversationId: string, jwt?: string) {
|
||
// user 字段已移除,后端会自动从 JWT 中提取 username
|
||
const body = {};
|
||
|
||
const response = await difyFetch(`conversations/${conversationId}`, {
|
||
method: 'DELETE',
|
||
body: JSON.stringify(body),
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
|
||
// 更新消息反馈
|
||
async updateMessageFeedback(messageId: string, rating: 'like' | 'dislike' | null, jwt?: string) {
|
||
const body = {
|
||
rating,
|
||
// user 字段已移除,后端会自动从 JWT 中提取 username
|
||
};
|
||
|
||
const response = await difyFetch(`messages/${messageId}/feedbacks`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(body),
|
||
}, jwt);
|
||
return response.json();
|
||
},
|
||
};
|
||
|
||
// 工具函数
|
||
export const difyUtils = {
|
||
getConfig: () => DIFY_CONFIG,
|
||
};
|