重构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:
2025-10-30 09:47:48 +08:00
parent 064f05ffa5
commit c4c08cb59b
11 changed files with 2036 additions and 60 deletions
+293
View File
@@ -0,0 +1,293 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **Chinese Tobacco AI Contract and Case Audit System** (中国烟草AI合同及卷宗审核系统) - a Remix-based full-stack application for intelligent document review and evaluation. The system provides AI-powered contract auditing, risk assessment, and compliance checking.
**Tech Stack:**
- Frontend: Remix (React) + TypeScript + Vite
- Styling: Tailwind CSS + custom CSS
- Icons: Remixicon (locally hosted)
- Document Processing: react-pdf, mammoth, docx-preview
- Backend API: PostgreSQL + PostgREST
- Authentication: OAuth2.0 + JWT + cookie-based sessions
- Deployment: Docker + PM2 (multi-instance setup)
## Core Commands
### Development
```bash
npm run dev # Start dev server (port 5173)
npm run typecheck # Run TypeScript type checking
npm run lint # Run ESLint
```
### Building
```bash
npm run build # Production build
npm run build:production:multi # Build for production multi-instance
npm run build:test:multi # Build for testing multi-instance
npm run build:dev # Build for development
```
### Production Deployment
```bash
npm start # Start production server (single instance)
npm run start:pm2:production:multi # Build and start PM2 multi-instance (production)
npm run start:pm2:multi # Build and start PM2 multi-instance (testing)
```
### JWT Secret Generation
```bash
npm run generate:jwt-secret # Generate a secure JWT secret for production
```
### Docker Deployment
```bash
# Build image
docker build -t docreview-app .
# Run with docker-compose
docker-compose up -d
# The system exposes 6 ports for different regional clients:
# 51703: Meizhou (main)
# 51704: Yunfu
# 51705: Jieyang
# 51706: Chaozhou
# 51707: Province
# 51708: Test instance (limited to /cross-checking routes)
```
## Architecture
### Multi-Instance Deployment Strategy
The system uses a **port-based multi-client architecture** where:
- One codebase serves multiple regional clients
- Each client runs on a different port with isolated configurations
- Port-specific API configurations are defined in `app/config/api-config.ts`
- PM2 manages multiple instances via `ecosystem.config.cjs`
- Each instance has its own environment variables (PORT, CLIENT_ID, API_PORT_CONFIG)
### Authentication Flow
1. **OAuth2.0 Integration** (IDaaS-based):
- OAuth client configuration in `app/api/login/oauth-client.ts`
- Callback handling in `app/routes/callback.tsx`
- User info sync with local PostgreSQL database
2. **JWT Token Management**:
- Frontend JWT generated after OAuth success (`app/api/jwt-helper.server.ts`)
- JWT contains user info, role, and permissions
- Stored in encrypted cookie session
- Auto-refresh when OAuth token refreshes
- **CRITICAL**: JWT_SECRET must be set in `.env` (use `npm run generate:jwt-secret`)
3. **Session Management**:
- Cookie-based sessions via `createCookieSessionStorage`
- Global authentication check in `app/root.tsx` loader
- Role-based access control (common vs developer roles)
- Developer-only paths: /settings, /config-lists, /document-types, /prompts
### API Configuration System
**Port-Based Configuration** (`app/config/api-config.ts`):
- Automatically detects current port and applies correct API config
- Each port maps to specific backend services (PostgreSQL, MinIO, etc.)
- Environment variables can override port configs:
- `NEXT_PUBLIC_API_BASE_URL` - PostgreSQL API base URL
- `NEXT_PUBLIC_DOCUMENT_URL` - MinIO document storage URL
- `NEXT_PUBLIC_UPLOAD_URL` - File upload endpoint
**Configuration Priority**: Port-specific config > Environment variables > Default environment config
### Route Structure
- `app/routes/` - Remix file-based routing
- `_index.tsx` - Home page (redirects to /documents or /cross-checking based on port)
- `login.tsx` - Login page with OAuth flow
- `callback.tsx` - OAuth callback handler
- `documents.tsx` / `documents._index.tsx` - Document management
- `cross-checking.tsx` - Cross-examination system
- `contract-template.tsx` - Contract template search
- `chat-with-llm._index.tsx` - AI chat interface
- `api.*.tsx` - API routes for backend proxy
### API Layer Organization
- `app/api/` - API client modules
- `axios-client.ts` - Configured axios instance with auth headers
- `postgrest-client.ts` - PostgREST API client
- `db-client.server.ts` - Server-side database client
- `jwt-helper.server.ts` - JWT generation and validation
- `login/auth.server.ts` - Authentication service (getUserSession, logout)
- `login/oauth-client.ts` - OAuth2.0 client implementation
- `login/token-manager.server.ts` - Token refresh and management
**Important**: Always use `axios-client.ts` for API calls. It automatically:
- Adds JWT authentication headers
- Handles API base URL configuration
- Includes proper error handling
### Component Architecture
**Design System** (`app/components/ui/`):
- All components follow BEM naming convention
- Primary color: `#00684a` (tobacco corporate green)
- Components are self-contained with co-located styles
- Import styles in `app/styles/main.css`
**Key Components**:
- `Layout.tsx` - Main layout with sidebar and header
- `Sidebar.tsx` - Navigation sidebar (role-based menu filtering)
- `MessageModal.tsx` - Confirmation/alert modal system
- `Toast.tsx` - Toast notification provider
- `LoadingBar.tsx` - Top loading bar for route transitions
- `FilePreview.tsx` - PDF/Word document preview
**RemixIcon Usage**:
- Icons are locally hosted in `public/fonts/`
- Use class syntax: `<i className="ri-home-line"></i>`
- Preloaded via link tag for instant display
- **CRITICAL**: When using CSS isolation (like `all: unset`), add exception rules for RemixIcon classes (see `docs/docreview-development-standards.md` for examples)
### Cross-Checking System
**Overview**: Democratic proposal and voting system for document evaluation disputes.
**Key Concepts**:
- **Tasks**: Evaluation assignments with multiple reviewers
- **Proposals**: Suggestions to modify evaluation scores
- **Voting**: Democratic decision-making with approval threshold
- **Arbitration**: Automatic status determination based on vote counts
**Database Tables**:
- `cross_examination_tasks` - Task assignments
- `cross_task_document_mapping` - Document-task relationships
- `cross_scoring_proposals` - Score modification proposals
- `cross_opinion_votes` - Vote records
**API Routes** (via `app/routes/api.*.tsx` proxies):
- `POST /admin/cross_review/tasks/assign` - Assign cross-checking task
- `POST /admin/cross_review/proposals` - Create proposal
- `POST /admin/cross_review/proposals/{id}/votes` - Vote on proposal
- `DELETE /admin/cross_review/proposals/{id}` - Withdraw proposal
See `docs/交叉评查系统完整文档.md` for complete documentation.
## Environment Variables
**Required** (create `.env` file):
```bash
# JWT Secret - MUST be set for production
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# OAuth2.0 Configuration (set in PM2 config or env)
OAUTH_CLIENT_SECRET=your-oauth-client-secret
# API Configuration (optional, overrides port-based config)
NEXT_PUBLIC_API_BASE_URL=http://10.79.97.17:8000
NEXT_PUBLIC_DOCUMENT_URL=http://10.76.244.156:9000/docauditai/
NEXT_PUBLIC_UPLOAD_URL=http://10.79.97.17:8000/admin/documents
```
**PM2 Instance Variables** (set in `ecosystem.config.cjs`):
- `NODE_ENV` - Environment (production/testing/development)
- `PORT` - Server port
- `CLIENT_ID` - Regional client identifier
- `API_PORT_CONFIG` - Port-based API configuration selector
## Development Guidelines
### Code Style
**TypeScript**:
- Use explicit types for function parameters and return values
- Interface naming: PascalCase (e.g., `DocumentUI`, `ApiConfig`)
- Use string literal unions for status types
- Avoid `any` - use `unknown` if type is truly unknown
**React Components**:
- Use function declarations, not arrow functions: `export function Component() {}`
- Group hooks at top: useState first, then useEffect
- Event handlers: `handleClick`, `handleSubmit` naming
- Props interface: `ComponentNameProps`
**File Naming**:
- Components: `PascalCase.tsx` (Button.tsx)
- Routes: `kebab-case.tsx` (user-profile.tsx)
- Utilities: `camelCase.ts` (utils.ts)
**Import Order**:
1. React and Remix imports
2. Third-party libraries
3. Internal components with `~/` alias
4. Type imports
5. Styles
### Styling
**Color Variables** (defined in `app/root.tsx`):
```css
--color-primary: #00684a; /* Tobacco green */
--color-primary-hover: #005a3f;
--color-primary-light: rgba(0, 104, 74, 0.1);
--color-success: #52c41a;
--color-warning: #faad14;
--color-error: #f5222d;
```
**Spacing**: Use Tailwind defaults (4px base: p-4, mt-6, etc.)
**Border Radius**: `rounded-md` (6px) for buttons/cards
**Shadows**: `shadow-sm` default, `shadow-md` on hover
### Error Handling
- Use `ErrorBoundary` component for route-level errors
- API errors: Log and return appropriate Response.json with status
- Display user-friendly messages via MessageModal or Toast
- Never expose sensitive error details to users
### Performance
- Use `React.memo` for pure components that re-render frequently
- Lazy load heavy components (PDF viewers, code editors)
- Debounce search inputs (300ms standard)
- Optimize images and use appropriate formats
## Security Considerations
1. **JWT Secret**: NEVER commit JWT_SECRET to version control
2. **OAuth Client Secret**: Store in environment variables, not code
3. **API Keys**: Use server-side only (no NEXT_PUBLIC_ prefix)
4. **Session Cookies**: httpOnly, secure in production
5. **Port 51708 Restrictions**: Limited to /cross-checking routes only
## Testing
- Test OAuth flow with IDaaS server at `http://10.79.112.85`
- Verify JWT generation and validation
- Check port-based API configuration switching
- Test role-based access control (common vs developer)
- Validate cross-checking voting logic and thresholds
## Common Pitfalls
1. **RemixIcon not showing**: Check for CSS rules overriding `font-family: 'remixicon'`
2. **API config not updating**: Port detection may need window.location.port on client
3. **JWT expired**: Implement token refresh or re-authenticate
4. **PM2 env vars not working**: Ensure NEXT_PUBLIC_ prefix for client-side vars
5. **File upload fails**: Check UPLOAD_URL configuration for current port
## Additional Documentation
Detailed documentation is available in `docs/`:
- `docreview-development-standards.md` - Comprehensive coding standards
- `交叉评查系统完整文档.md` - Cross-checking system specification
- `JWT_IMPLEMENTATION.md` - JWT authentication implementation
- `OAuth2.0认证协议集成指南.md` - OAuth integration guide
- `docker-deployment.md` - Docker deployment instructions
- `deployment-config.md` - Multi-instance deployment configuration
+399
View File
@@ -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: '***' // 隐藏敏感信息
}
},
});
}
+25 -3
View File
@@ -8,7 +8,23 @@ export async function action({ request }: ActionFunctionArgs) {
} }
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user } = await getSessionInfo(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 body = await request.json();
const { const {
@@ -27,7 +43,8 @@ export async function action({ request }: ActionFunctionArgs) {
responseMode, responseMode,
hasInputs: !!inputs, hasInputs: !!inputs,
hasFiles: !!files && files.length > 0, hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0 filesCount: files?.length || 0,
hasJWT: !!frontendJWT
}); });
const response = await difyClient.createChatMessage( const response = await difyClient.createChatMessage(
@@ -36,7 +53,8 @@ export async function action({ request }: ActionFunctionArgs) {
user, user,
responseMode, responseMode,
conversationId, conversationId,
files files,
frontendJWT // 传递 JWT
); );
console.log('📡 [API] Dify响应状态:', { console.log('📡 [API] Dify响应状态:', {
@@ -77,10 +95,14 @@ export async function action({ request }: ActionFunctionArgs) {
stack: error.stack, stack: error.stack,
name: error.name name: error.name
}); });
// 检查是否是JWT认证失败
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
return new Response( return new Response(
JSON.stringify({ error: error.message || 'Failed to send message' }), JSON.stringify({ error: error.message || 'Failed to send message' }),
{ {
status: 500, status,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
+32 -5
View File
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
export async function action({ request, params }: ActionFunctionArgs) { export async function action({ request, params }: ActionFunctionArgs) {
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user, session } = await getSessionInfo(request); const { user, session } = await getSessionInfo(request);
const { id } = params; const { id } = params;
@@ -11,15 +14,35 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: '会话ID不能为空' }, { status: 400 }); 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 body = await request.json();
const { auto_generate, name } = body; 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重命名会话 // 调用服务端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, { return json(data, {
headers: { headers: {
@@ -27,13 +50,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
}, },
}); });
} catch (error: any) { } 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( return json(
{ {
error: error.message || '重命名会话失败' error: error.message || '重命名会话失败'
}, },
{ {
status: 500, status,
headers: { headers: {
'Set-Cookie': await commitSession((await getSessionInfo(request)).session), 'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
}, },
+30 -5
View File
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
export async function action({ request, params }: ActionFunctionArgs) { export async function action({ request, params }: ActionFunctionArgs) {
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user, session } = await getSessionInfo(request); const { user, session } = await getSessionInfo(request);
const { id } = params; const { id } = params;
@@ -11,15 +14,33 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: '会话ID不能为空' }, { status: 400 }); 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; const method = request.method;
if (method === 'DELETE') { if (method === 'DELETE') {
// console.log('🗑️ Delete Conversation API - User:', user, 'ID:', id); console.log('🗑️ [API] Delete Conversation API - 删除会话:', {
user,
id,
hasJWT: !!frontendJWT
});
// 调用服务端API删除会话 // 调用服务端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, { return json(data, {
headers: { headers: {
@@ -30,13 +51,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: '不支持的请求方法' }, { status: 405 }); return json({ error: '不支持的请求方法' }, { status: 405 });
} catch (error: any) { } 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( return json(
{ {
error: error.message || '删除会话失败' error: error.message || '删除会话失败'
}, },
{ {
status: 500, status,
headers: { headers: {
'Set-Cookie': await commitSession((await getSessionInfo(request)).session), 'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
}, },
+29 -5
View File
@@ -4,13 +4,33 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user, session } = await getSessionInfo(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, { return json(data, {
headers: { headers: {
@@ -18,14 +38,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
}, },
}); });
} catch (error: any) { } 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( return json(
{ {
data: [], data: [],
error: error.message || 'Failed to fetch conversations' error: error.message || 'Failed to fetch conversations'
}, },
{ {
status: 500, status,
headers: { headers: {
'Set-Cookie': await commitSession((await getSessionInfo(request)).session), 'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
}, },
+30 -5
View File
@@ -4,6 +4,9 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user, session } = await getSessionInfo(request); const { user, session } = await getSessionInfo(request);
const url = new URL(request.url); const url = new URL(request.url);
const conversationId = url.searchParams.get('conversation_id'); 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, { return json(data, {
headers: { headers: {
@@ -27,11 +48,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
}, },
}); });
} catch (error: any) { } 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( return json(
{ error: error.message || 'Failed to fetch messages' }, { error: error.message || 'Failed to fetch messages' },
{ {
status: 500, status,
headers: { headers: {
'Set-Cookie': await commitSession((await getSessionInfo(request)).session), 'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
}, },
+29 -5
View File
@@ -4,13 +4,33 @@ import { getSessionInfo, commitSession } from '../utils/session.server';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
// 获取用户会话信息和 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const { user, session } = await getSessionInfo(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, { return json(data, {
headers: { headers: {
@@ -18,11 +38,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
}, },
}); });
} catch (error: any) { } 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( return json(
{ error: error.message || 'Failed to fetch parameters' }, { error: error.message || 'Failed to fetch parameters' },
{ {
status: 500, status,
headers: { headers: {
'Set-Cookie': await commitSession((await getSessionInfo(request)).session), 'Set-Cookie': await commitSession((await getSessionInfo(request)).session),
}, },
+48 -32
View File
@@ -1,4 +1,4 @@
import { API_BASE_URL } from '~/config/api-config';
// 获取环境变量的服务端函数 // 获取环境变量的服务端函数
const getServerEnvVar = (name: string, defaultValue: string = '') => { const getServerEnvVar = (name: string, defaultValue: string = '') => {
@@ -12,8 +12,11 @@ const getServerEnvVar = (name: string, defaultValue: string = '') => {
}; };
// Dify API 客户端配置 // Dify API 客户端配置
// 注意:现在通过 FastAPI 后端的 /dify 路由代理访问 Dify,使用 JWT 认证
const DIFY_CONFIG = { 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', ''), API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''),
APP_ID: (() => { APP_ID: (() => {
const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', ''); const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', '');
@@ -27,24 +30,32 @@ console.log('🔧 Dify Client Config:', {
apiUrl: DIFY_CONFIG.API_URL, apiUrl: DIFY_CONFIG.API_URL,
appId: DIFY_CONFIG.APP_ID, appId: DIFY_CONFIG.APP_ID,
hasApiKey: !!DIFY_CONFIG.API_KEY, 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)
}); });
// 基础请求函数 // 基础请求函数 - 使用 JWT 认证通过 FastAPI 代理访问 Dify
const difyFetch = async (endpoint: string, options: RequestInit = {}) => { const difyFetch = async (endpoint: string, options: RequestInit = {}, jwt?: string) => {
const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`; const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`;
const headers = { // 使用 JWT 认证而非 API_KEY
const headers: HeadersInit = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${DIFY_CONFIG.API_KEY}`,
...options.headers, ...options.headers,
}; };
// console.log('🌐 Dify API Request:', { // 如果提供了 JWT,添加到请求头
// url, if (jwt) {
// method: options.method || 'GET', (headers as Record<string, string>)['Authorization'] = `Bearer ${jwt}`;
// hasAuth: !!DIFY_CONFIG.API_KEY } 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, { const response = await fetch(url, {
...options, ...options,
@@ -53,11 +64,17 @@ const difyFetch = async (endpoint: string, options: RequestInit = {}) => {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('❌ Dify API Error:', { console.error('❌ [DifyClient] Dify API Error:', {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
error: errorText error: errorText
}); });
// 如果是401错误,说明JWT过期或无效
if (response.status === 401) {
throw new Error('JWT认证失败,请重新登录');
}
throw new Error(`Dify API Error: ${response.status} ${response.statusText}`); 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}`; return `user_${DIFY_CONFIG.APP_ID}:${sessionId}`;
}; };
// Dify API 客户端 // Dify API 客户端 - 所有方法都需要传入 JWT
export const difyClient = { export const difyClient = {
// 获取应用参数 // 获取应用参数
async getApplicationParameters(user: string) { async getApplicationParameters(user: string, jwt?: string) {
const response = await difyFetch('parameters', { const response = await difyFetch('parameters', {
method: 'GET', method: 'GET',
headers: { }, jwt);
'Authorization': `Bearer ${DIFY_CONFIG.API_KEY}`,
},
});
return response.json(); return response.json();
}, },
// 获取会话列表 // 获取会话列表
async getConversations(user: string) { async getConversations(user: string, jwt?: string) {
const params = new URLSearchParams({ const params = new URLSearchParams({
user, user,
limit: '100', limit: '100',
@@ -92,12 +106,12 @@ export const difyClient = {
const response = await difyFetch(`conversations?${params}`, { const response = await difyFetch(`conversations?${params}`, {
method: 'GET', method: 'GET',
}); }, jwt);
return response.json(); return response.json();
}, },
// 获取会话消息 // 获取会话消息
async getConversationMessages(user: string, conversationId: string) { async getConversationMessages(user: string, conversationId: string, jwt?: string) {
const params = new URLSearchParams({ const params = new URLSearchParams({
user, user,
conversation_id: conversationId, conversation_id: conversationId,
@@ -107,7 +121,7 @@ export const difyClient = {
const response = await difyFetch(`messages?${params}`, { const response = await difyFetch(`messages?${params}`, {
method: 'GET', method: 'GET',
}); }, jwt);
return response.json(); return response.json();
}, },
@@ -118,7 +132,8 @@ export const difyClient = {
user: string, user: string,
responseMode: string = 'streaming', responseMode: string = 'streaming',
conversationId?: string, conversationId?: string,
files?: any[] files?: any[],
jwt?: string
) { ) {
const body = { const body = {
inputs, inputs,
@@ -138,13 +153,14 @@ export const difyClient = {
hasInputs: !!inputs && Object.keys(inputs).length > 0, hasInputs: !!inputs && Object.keys(inputs).length > 0,
inputsKeys: inputs ? Object.keys(inputs) : [], inputsKeys: inputs ? Object.keys(inputs) : [],
hasFiles: !!files && files.length > 0, hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0 filesCount: files?.length || 0,
hasJWT: !!jwt
}); });
const response = await difyFetch('chat-messages', { const response = await difyFetch('chat-messages', {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
}); }, jwt);
console.log('📡 [DifyClient] Dify API响应:', { console.log('📡 [DifyClient] Dify API响应:', {
status: response.status, 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 = { const body = {
name, name,
auto_generate: autoGenerate, auto_generate: autoGenerate,
@@ -175,12 +191,12 @@ export const difyClient = {
const response = await difyFetch(`conversations/${conversationId}/name`, { const response = await difyFetch(`conversations/${conversationId}/name`, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
}); }, jwt);
return response.json(); return response.json();
}, },
// 删除会话 // 删除会话
async deleteConversation(conversationId: string, user: string) { async deleteConversation(conversationId: string, user: string, jwt?: string) {
const body = { const body = {
user, user,
}; };
@@ -188,12 +204,12 @@ export const difyClient = {
const response = await difyFetch(`conversations/${conversationId}`, { const response = await difyFetch(`conversations/${conversationId}`, {
method: 'DELETE', method: 'DELETE',
body: JSON.stringify(body), body: JSON.stringify(body),
}); }, jwt);
return response.json(); 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 = { const body = {
rating, rating,
user, user,
@@ -202,7 +218,7 @@ export const difyClient = {
const response = await difyFetch(`messages/${messageId}/feedbacks`, { const response = await difyFetch(`messages/${messageId}/feedbacks`, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
}); }, jwt);
return response.json(); return response.json();
}, },
}; };
+300
View File
@@ -0,0 +1,300 @@
# Dify 客户端 JWT 认证改造 - 前端修改总结
## 📅 修改信息
- **修改日期**: 2025-01-XX
- **版本**: v2.0 - JWT 认证版本
- **修改类型**: 架构升级 - 从直连改为代理模式
---
## 🎯 修改目标
将 Dify AI 服务调用从 **前端直连** 改为 **通过 FastAPI 后端代理**,并使用 **JWT 认证**替代原有的 API KEY。
---
## 📋 核心变更
### **架构变更**
```
旧: 前端 → Dify API (使用 API_KEY)
新: 前端 → FastAPI 后端 (使用 JWT) → Dify API (使用 API_KEY)
```
### **认证方式变更**
```
旧: Authorization: Bearer {DIFY_API_KEY}
新: Authorization: Bearer {frontendJWT}
```
### **API 端点变更**
```
旧: https://api.dify.ai/v1/chat-messages
新: http://172.16.0.55:8000/dify/chat-messages
```
---
## 📁 修改的文件清单
### **1. 核心服务层** (1 个文件)
-`app/services/dify-client.server.ts` - Dify 客户端核心
**主要修改**
- 导入 `API_BASE_URL` 从配置文件
- `DIFY_CONFIG.API_URL` 改为 `${API_BASE_URL}/dify`
- `difyFetch` 函数添加 `jwt` 参数
- 所有 client 方法添加 `jwt?` 参数
- 添加 401 错误处理(JWT 认证失败)
### **2. API 路由层** (6 个文件)
所有路由都添加了 JWT 获取、验证和传递逻辑:
-`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` - 会话重命名
**统一修改模式**
```typescript
// 1. 获取 JWT
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
// 2. JWT 验证
if (!frontendJWT) {
return json({ error: 'JWT认证失败,请重新登录' }, { status: 401 });
}
// 3. 传递给 difyClient
await difyClient.method(..., frontendJWT);
// 4. 错误处理
const status = error.message?.includes('JWT认证失败') ? 401 : 500;
```
---
## 🔐 配置迁移说明
### **前端配置(已废弃)**
```bash
# .env - 这些配置不再使用
NEXT_PUBLIC_APP_ID=http://nas.7bm.co:12980/app/46539478-3281-4e98-a445-6da9dc078e95/configuration
NEXT_PUBLIC_APP_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
```
### **后端配置(需要添加)**
**这些配置应该移到 FastAPI 后端**:
```python
# FastAPI 环境变量配置
DIFY_API_URL = "http://nas.7bm.co:12980/v1"
DIFY_API_KEY = "app-N3su9tKyMMnqxt2EMgOkVof7"
DIFY_APP_ID = "46539478-3281-4e98-a445-6da9dc078e95"
```
### **前端新配置**
```bash
# .env - 前端只需要配置 FastAPI 地址
API_BASE_URL=http://172.16.0.55:8000
```
---
## 🛣️ 后端需要实现的路由
| 路由 | 方法 | 说明 |
|------|------|------|
| `/dify/parameters` | GET | 获取应用参数 |
| `/dify/conversations` | GET | 获取会话列表 |
| `/dify/messages` | GET | 获取会话消息历史 |
| `/dify/chat-messages` | POST | 发送聊天消息(支持流式) |
| `/dify/conversations/{id}/name` | POST | 重命名会话 |
| `/dify/conversations/{id}` | DELETE | 删除会话 |
| `/dify/messages/{id}/feedbacks` | POST | 消息反馈 |
**详细对接文档**: `docs/dify-proxy-backend-integration.md`
---
## ⚠️ 关键注意事项
### **1. JWT 认证必须实现**
- 所有请求都携带 `Authorization: Bearer {JWT}`
- JWT 验证失败必须返回 **401 状态码**
- 错误格式:`{"error": "JWT认证失败,请重新登录"}`
### **2. 流式响应必须支持**
- `/dify/chat-messages` 接口支持流式响应(SSE
- 响应头:`Content-Type: text/event-stream`
- 不能缓冲,必须实时转发
### **3. 配置安全**
- `DIFY_API_KEY` 只能存在后端
- 前端代码中不再包含任何 Dify 凭据
- 所有敏感配置通过环境变量管理
### **4. 错误处理**
- JWT 不存在: **401**
- JWT 过期/无效: **401**
- 其他错误: **500**
---
## 🧪 测试清单
### **前端测试**
- [ ] 聊天消息发送(流式)
- [ ] 聊天消息发送(非流式)
- [ ] 会话列表加载
- [ ] 会话切换和消息历史
- [ ] 会话重命名
- [ ] 会话删除
- [ ] JWT 过期后跳转登录
### **后端测试**
- [ ] JWT 验证逻辑
- [ ] 所有 API 路由返回正确
- [ ] 流式响应正常工作
- [ ] 错误返回正确状态码
- [ ] 日志记录完整
---
## 📊 代码统计
- **修改文件数**: 7 个
- **新增代码行数**: ~150 行
- **修改代码行数**: ~200 行
- **核心逻辑变更**: 认证方式 + API 端点
---
## 🚀 部署步骤
### **1. 后端准备**
```bash
# 1. 在 FastAPI 中实现 /dify/* 路由
# 2. 配置 Dify API 凭据
# 3. 集成 JWT 验证逻辑
# 4. 测试所有接口
```
### **2. 前端部署**
```bash
# 1. 确认后端已就绪
npm run build
npm run start
```
### **3. 联调测试**
```bash
# 1. 测试聊天功能
# 2. 测试会话管理
# 3. 测试 JWT 过期处理
# 4. 检查日志输出
```
---
## 📝 代码示例
### **前端调用示例**
```typescript
// app/routes/api.chat-messages.tsx
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT } = await getUserSession(request);
const response = await difyClient.createChatMessage(
inputs,
query,
user,
responseMode,
conversationId,
files,
frontendJWT // 传递 JWT
);
```
### **后端实现示例**
```python
# FastAPI 路由
@app.post("/dify/chat-messages")
async def create_chat_message(
request: Request,
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败")
# 2. 调用 Dify API
headers = {"Authorization": f"Bearer {DIFY_API_KEY}"}
body = await request.json()
# 3. 转发流式响应
if body.get("response_mode") == "streaming":
return StreamingResponse(...)
```
---
## 📞 支持与文档
### **详细文档**
- **后端对接文档**: `docs/dify-proxy-backend-integration.md`
- **JWT 实现文档**: `docs/JWT_IMPLEMENTATION.md`
- **CLAUDE.md**: 项目总体架构说明
### **相关链接**
- [Dify 官方文档](https://docs.dify.ai/)
- [FastAPI 文档](https://fastapi.tiangolo.com/)
- [JWT 规范](https://jwt.io/)
---
## ✅ 检查清单
### **前端(已完成)**
- [x] dify-client.server.ts 修改
- [x] 所有 API 路由添加 JWT
- [x] 错误处理完善
- [x] 日志输出优化
- [x] 配置迁移说明
### **后端(待实现)**
- [ ] /dify/* 路由实现
- [ ] JWT 验证集成
- [ ] 流式响应支持
- [ ] 错误处理规范
- [ ] 日志记录完善
- [ ] CORS 配置(如需)
### **联调测试(待完成)**
- [ ] 基础功能测试
- [ ] JWT 认证测试
- [ ] 流式响应测试
- [ ] 错误处理测试
- [ ] 性能测试
---
## 🎉 总结
本次修改完成了 Dify 服务调用的架构升级:
-**安全性提升**: API KEY 不再暴露在前端
-**统一认证**: 使用项目统一的 JWT 认证体系
-**便于管理**: 所有 Dify 配置集中在后端
-**向后兼容**: 保留了原有的 API 接口设计
**下一步**: 等待后端实现完成后进行联调测试。
---
**修改完成日期**: 2025-01-XX
**文档版本**: v1.0
+821
View File
@@ -0,0 +1,821 @@
# Dify 代理服务后端对接文档
## 📋 文档说明
本文档描述了前端 Remix 应用如何通过 FastAPI 后端代理访问 Dify AI 服务,以及后端需要实现的接口规范。
**更新时间**: 2025-01-XX
**前端修改版本**: v2.0 - JWT 认证版本
---
## 🎯 架构变更
### **旧架构**
```
前端 Remix App → 直接调用 Dify API (使用 API KEY)
```
### **新架构**
```
前端 Remix App → FastAPI 后端 /dify 路由 (使用 JWT) → Dify API (使用 API KEY)
```
---
## 🔐 认证流程说明
### **前端侧**
1. 用户登录后获得 `frontendJWT` (前端JWT)
2. 所有 Dify 相关请求携带 `Authorization: Bearer {frontendJWT}`
3. JWT 由 OAuth2.0 + 自定义签名生成,包含用户信息
### **后端侧**
1. 接收前端请求,验证 `frontendJWT` 是否有效
2. JWT 验证通过后,使用配置的 `DIFY_API_KEY` 调用真正的 Dify API
3. 将 Dify API 的响应原样返回给前端
### **关键配置迁移**
前端原本使用的配置:
```bash
# .env (前端配置 - 已不再使用)
NEXT_PUBLIC_APP_ID=http://nas.7bm.co:12980/app/46539478-3281-4e98-a445-6da9dc078e95/configuration
NEXT_PUBLIC_APP_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
```
**现在这两个配置应该移到 FastAPI 后端**
```python
# FastAPI 配置 (后端配置)
DIFY_API_URL = "http://nas.7bm.co:12980/v1" # Dify API 基础URL
DIFY_API_KEY = "app-N3su9tKyMMnqxt2EMgOkVof7" # Dify API Key
DIFY_APP_ID = "46539478-3281-4e98-a445-6da9dc078e95" # Dify App ID
```
---
## 🛣️ API 路由规范
### **基础路由前缀**
```
http://{FASTAPI_HOST}:{PORT}/dify
```
### **需要实现的路由列表**
| 路由 | 方法 | 说明 | 前端调用频率 |
|------|------|------|--------------|
| `/dify/parameters` | GET | 获取应用参数 | 初始化时 |
| `/dify/conversations` | GET | 获取会话列表 | 每次打开聊天 |
| `/dify/messages` | GET | 获取会话消息历史 | 切换会话时 |
| `/dify/chat-messages` | POST | 发送聊天消息 | 用户发送消息时 |
| `/dify/conversations/{id}/name` | POST | 重命名会话 | 用户重命名时 |
| `/dify/conversations/{id}` | DELETE | 删除会话 | 用户删除时 |
| `/dify/messages/{id}/feedbacks` | POST | 消息反馈(点赞/点踩) | 用户反馈时 |
---
## 📡 详细接口规范
### **1. 获取应用参数**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/parameters
Authorization: Bearer {frontendJWT}
```
#### 后端处理逻辑
```python
@app.get("/dify/parameters")
async def get_parameters(
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token) # 你们的JWT验证函数
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.get(
f"{DIFY_API_URL}/parameters",
headers=headers
)
# 3. 返回 Dify 响应
return response.json()
```
#### Dify API 响应示例
```json
{
"opening_statement": "你好!我是AI助手...",
"suggested_questions": ["问题1", "问题2"],
"speech_to_text": { "enabled": false },
"retriever_resource": { "enabled": false },
"annotation_reply": { "enabled": false },
"user_input_form": [],
"file_upload": {
"image": {
"enabled": false,
"number_limits": 3,
"transfer_methods": ["remote_url", "local_file"]
}
},
"system_parameters": {}
}
```
---
### **2. 获取会话列表**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/conversations?user={user_id}&limit=100&first_id=
Authorization: Bearer {frontendJWT}
```
**查询参数**
- `user`: 用户标识(从 JWT 中可提取)
- `limit`: 返回数量限制(默认100
- `first_id`: 分页游标(可选)
#### 后端处理逻辑
```python
@app.get("/dify/conversations")
async def get_conversations(
user: str,
limit: int = 100,
first_id: str = "",
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
params = {
"user": user,
"limit": limit,
"first_id": first_id
}
response = requests.get(
f"{DIFY_API_URL}/conversations",
headers=headers,
params=params
)
# 3. 返回 Dify 响应
return response.json()
```
#### Dify API 响应示例
```json
{
"data": [
{
"id": "conv-123456",
"name": "会话标题",
"inputs": {},
"status": "normal",
"introduction": "",
"created_at": 1234567890,
"updated_at": 1234567890
}
],
"has_more": false,
"limit": 100
}
```
---
### **3. 获取会话消息历史**
#### 前端请求
```http
GET http://{FASTAPI_HOST}/dify/messages?user={user_id}&conversation_id={conv_id}&limit=20&last_id=
Authorization: Bearer {frontendJWT}
```
**查询参数**
- `user`: 用户标识
- `conversation_id`: 会话ID(必填)
- `limit`: 返回数量限制(默认20
- `last_id`: 分页游标(可选)
#### 后端处理逻辑
```python
@app.get("/dify/messages")
async def get_messages(
user: str,
conversation_id: str,
limit: int = 20,
last_id: str = "",
authorization: str = Header(None)
):
# 验证 JWT 和调用 Dify API(同上)
# ...
params = {
"user": user,
"conversation_id": conversation_id,
"limit": limit,
"last_id": last_id
}
response = requests.get(
f"{DIFY_API_URL}/messages",
headers=headers,
params=params
)
return response.json()
```
---
### **4. 发送聊天消息(流式响应)⭐**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/chat-messages
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"inputs": {},
"query": "",
"user": "user_app-id:session-id",
"response_mode": "streaming",
"conversation_id": "conv-123456",
"files": []
}
```
**请求体参数**
- `inputs`: 输入变量(对象)
- `query`: 用户消息内容(必填)
- `user`: 用户标识(必填)
- `response_mode`: 响应模式,`"streaming"``"blocking"`
- `conversation_id`: 会话ID(可选,不传则创建新会话)
- `files`: 上传的文件列表(可选)
#### 后端处理逻辑(重要!)
```python
from fastapi import StreamingResponse
import httpx
@app.post("/dify/chat-messages")
async def create_chat_message(
request: Request,
authorization: str = Header(None)
):
# 1. 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 2. 获取请求体
body = await request.json()
response_mode = body.get("response_mode", "streaming")
# 3. 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
# 4. 根据响应模式处理
if response_mode == "streaming":
# 流式响应 - 直接转发流
async def stream_response():
async with httpx.AsyncClient() as client:
async with client.stream(
"POST",
f"{DIFY_API_URL}/chat-messages",
headers=headers,
json=body,
timeout=60.0
) as response:
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
}
)
else:
# 非流式响应
response = requests.post(
f"{DIFY_API_URL}/chat-messages",
headers=headers,
json=body
)
return response.json()
```
#### Dify API 流式响应格式
```
data: {"event": "message", "message_id": "msg-123", "conversation_id": "conv-123", "answer": "你好"}
data: {"event": "message", "message_id": "msg-123", "conversation_id": "conv-123", "answer": ""}
data: {"event": "message_end", "id": "msg-123", "metadata": {...}}
data: {"event": "workflow_finished", "data": {...}}
```
---
### **5. 重命名会话**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/conversations/{conversation_id}/name
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"name": "",
"auto_generate": false,
"user": "user_app-id:session-id"
}
```
#### 后端处理逻辑
```python
@app.post("/dify/conversations/{conversation_id}/name")
async def rename_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 获取请求体
body = await request.json()
# 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.post(
f"{DIFY_API_URL}/conversations/{conversation_id}/name",
headers=headers,
json=body
)
return response.json()
```
---
### **6. 删除会话**
#### 前端请求
```http
DELETE http://{FASTAPI_HOST}/dify/conversations/{conversation_id}
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"user": "user_app-id:session-id"
}
```
#### 后端处理逻辑
```python
@app.delete("/dify/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 获取请求体
body = await request.json()
# 调用 Dify API
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
response = requests.delete(
f"{DIFY_API_URL}/conversations/{conversation_id}",
headers=headers,
json=body
)
return response.json()
```
---
### **7. 消息反馈(点赞/点踩)**
#### 前端请求
```http
POST http://{FASTAPI_HOST}/dify/messages/{message_id}/feedbacks
Authorization: Bearer {frontendJWT}
Content-Type: application/json
{
"rating": "like",
"user": "user_app-id:session-id"
}
```
**请求体参数**
- `rating`: 评价类型,`"like"` | `"dislike"` | `null`
- `user`: 用户标识
---
## 🔧 完整后端实现示例(FastAPI)
```python
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import StreamingResponse
import httpx
import requests
from typing import Optional
app = FastAPI()
# 配置
DIFY_API_URL = "http://nas.7bm.co:12980/v1"
DIFY_API_KEY = "app-N3su9tKyMMnqxt2EMgOkVof7"
DIFY_APP_ID = "46539478-3281-4e98-a445-6da9dc078e95"
# JWT 验证函数(使用你们现有的JWT验证逻辑)
def verify_jwt(token: str) -> Optional[dict]:
"""
验证 JWT Token
返回: 用户信息字典,如果验证失败返回 None
"""
try:
# 这里使用你们现有的 JWT 验证逻辑
# payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# return payload
pass
except Exception as e:
print(f"JWT 验证失败: {e}")
return None
# 通用 Dify 请求包装器
async def dify_request(
method: str,
endpoint: str,
authorization: str,
params: dict = None,
json_data: dict = None,
stream: bool = False
):
"""通用 Dify API 请求处理"""
# 验证 JWT
jwt_token = authorization.replace("Bearer ", "") if authorization else None
if not jwt_token:
raise HTTPException(status_code=401, detail="缺少认证令牌")
user_info = verify_jwt(jwt_token)
if not user_info:
raise HTTPException(status_code=401, detail="JWT认证失败,请重新登录")
# 构建 Dify 请求头
headers = {
"Authorization": f"Bearer {DIFY_API_KEY}",
"Content-Type": "application/json"
}
url = f"{DIFY_API_URL}/{endpoint.lstrip('/')}"
if stream:
# 流式响应
async def stream_response():
async with httpx.AsyncClient() as client:
async with client.stream(
method,
url,
headers=headers,
params=params,
json=json_data,
timeout=60.0
) as response:
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(
stream_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)
else:
# 普通响应
response = requests.request(
method,
url,
headers=headers,
params=params,
json=json_data
)
if response.status_code >= 400:
raise HTTPException(
status_code=response.status_code,
detail=response.text
)
return response.json()
# 路由实现
@app.get("/dify/parameters")
async def get_parameters(authorization: str = Header(None)):
return await dify_request("GET", "/parameters", authorization)
@app.get("/dify/conversations")
async def get_conversations(
user: str,
limit: int = 100,
first_id: str = "",
authorization: str = Header(None)
):
params = {"user": user, "limit": limit, "first_id": first_id}
return await dify_request("GET", "/conversations", authorization, params=params)
@app.get("/dify/messages")
async def get_messages(
user: str,
conversation_id: str,
limit: int = 20,
last_id: str = "",
authorization: str = Header(None)
):
params = {
"user": user,
"conversation_id": conversation_id,
"limit": limit,
"last_id": last_id
}
return await dify_request("GET", "/messages", authorization, params=params)
@app.post("/dify/chat-messages")
async def create_chat_message(
request: Request,
authorization: str = Header(None)
):
body = await request.json()
response_mode = body.get("response_mode", "streaming")
stream = (response_mode == "streaming")
return await dify_request(
"POST",
"/chat-messages",
authorization,
json_data=body,
stream=stream
)
@app.post("/dify/conversations/{conversation_id}/name")
async def rename_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"POST",
f"/conversations/{conversation_id}/name",
authorization,
json_data=body
)
@app.delete("/dify/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"DELETE",
f"/conversations/{conversation_id}",
authorization,
json_data=body
)
@app.post("/dify/messages/{message_id}/feedbacks")
async def message_feedback(
message_id: str,
request: Request,
authorization: str = Header(None)
):
body = await request.json()
return await dify_request(
"POST",
f"/messages/{message_id}/feedbacks",
authorization,
json_data=body
)
```
---
## ⚠️ 重要注意事项
### **1. JWT 验证**
- 必须验证 JWT 的签名和过期时间
- JWT 验证失败时返回 **401 状态码**
- 错误信息格式:`{"error": "JWT认证失败,请重新登录"}`
### **2. 流式响应处理**
- `/chat-messages` 接口必须支持流式响应(SSE
- 响应头必须包含:
```
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
```
- 不要缓冲流式响应,直接转发给前端
### **3. 错误处理**
所有错误响应应遵循统一格式:
```json
{
"error": "错误描述信息"
}
```
常见错误码:
- `400`: 请求参数错误
- `401`: JWT 认证失败
- `403`: 权限不足
- `500`: 服务器内部错误
### **4. CORS 配置**
如果前后端分离部署,需要配置 CORS:
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://your-frontend-domain"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
### **5. 日志记录**
建议记录以下信息:
- 请求时间、用户ID、接口路径
- JWT 验证结果
- Dify API 调用结果
- 错误信息和堆栈
---
## 🧪 测试验证
### **测试工具**
推荐使用以下工具测试:
- **Postman** - API 接口测试
- **curl** - 命令行测试
- **httpie** - 友好的命令行工具
### **测试步骤**
#### 1. 获取 JWT Token
从前端登录后,在浏览器开发者工具中获取 `frontendJWT`
```javascript
// 在浏览器控制台执行
console.log(document.cookie);
```
#### 2. 测试基础接口
```bash
# 测试获取参数
curl -X GET "http://localhost:8000/dify/parameters" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# 测试获取会话列表
curl -X GET "http://localhost:8000/dify/conversations?user=test_user&limit=10" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
#### 3. 测试流式聊天
```bash
# 测试发送消息(流式)
curl -X POST "http://localhost:8000/dify/chat-messages" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inputs": {},
"query": "你好",
"user": "test_user",
"response_mode": "streaming"
}'
```
---
## 📞 联系方式
如有疑问或需要协助,请联系:
- **前端开发**: [你的联系方式]
- **文档位置**: `E:\A_Wrok\Porject\docreview\docs\dify-proxy-backend-integration.md`
- **前端代码仓库**: [仓库地址]
---
## 📚 附录
### **相关文档**
- [Dify 官方 API 文档](https://docs.dify.ai/v/zh-hans/guides/application-publishing/developing-with-apis)
- [JWT 认证实现文档](./JWT_IMPLEMENTATION.md)
- [FastAPI 官方文档](https://fastapi.tiangolo.com/zh/)
### **配置清单**
#### 后端需要的环境变量
```bash
# Dify API 配置
DIFY_API_URL=http://nas.7bm.co:12980/v1
DIFY_API_KEY=app-N3su9tKyMMnqxt2EMgOkVof7
DIFY_APP_ID=46539478-3281-4e98-a445-6da9dc078e95
# JWT 验证配置(使用现有的)
JWT_SECRET=your-jwt-secret
JWT_ALGORITHM=HS256
```
#### 前端配置(已修改)
```bash
# API Base URL - 指向 FastAPI 后端
API_BASE_URL=http://172.16.0.55:8000
# Dify 配置已移除,不再使用
# NEXT_PUBLIC_APP_ID=... (已废弃)
# NEXT_PUBLIC_APP_KEY=... (已废弃)
```
---
## ✅ 检查清单
后端实现完成后,请确认:
- [ ] 所有 7 个 Dify API 路由已实现
- [ ] JWT 验证逻辑已集成
- [ ] 流式响应(SSE)正常工作
- [ ] 错误处理返回正确的状态码
- [ ] CORS 配置已启用(如需要)
- [ ] 日志记录已完善
- [ ] 已使用 Postman/curl 测试所有接口
- [ ] 与前端联调测试通过
---
**祝开发顺利!** 🎉