重构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,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
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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 测试所有接口
|
||||
- [ ] 与前端联调测试通过
|
||||
|
||||
---
|
||||
|
||||
**祝开发顺利!** 🎉
|
||||
Reference in New Issue
Block a user