From c4c08cb59be87fba15d4f8b2bdced1f2629c136a Mon Sep 17 00:00:00 2001 From: Wenyan Date: Thu, 30 Oct 2025 09:47:48 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Dify=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=EF=BC=9A=E6=94=B9=E4=B8=BA=E9=80=9A=E8=BF=87FastAPI?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=B9=B6=E4=BD=BF=E7=94=A8JWT=E8=AE=A4?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: - 修改 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 --- CLAUDE.md | 293 ++++++++ app/config/api-config-b.ts | 399 ++++++++++ app/routes/api.chat-messages.tsx | 28 +- app/routes/api.conversations.$id.name.tsx | 37 +- app/routes/api.conversations.$id.tsx | 35 +- app/routes/api.conversations.tsx | 34 +- app/routes/api.messages.tsx | 35 +- app/routes/api.parameters.tsx | 34 +- app/services/dify-client.server.ts | 80 +- docs/dify-frontend-modification-summary.md | 300 ++++++++ docs/dify-proxy-backend-integration.md | 821 +++++++++++++++++++++ 11 files changed, 2036 insertions(+), 60 deletions(-) create mode 100644 CLAUDE.md create mode 100644 app/config/api-config-b.ts create mode 100644 docs/dify-frontend-modification-summary.md create mode 100644 docs/dify-proxy-backend-integration.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2e9eaf --- /dev/null +++ b/CLAUDE.md @@ -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: `` +- 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 diff --git a/app/config/api-config-b.ts b/app/config/api-config-b.ts new file mode 100644 index 0000000..a9f13ca --- /dev/null +++ b/app/config/api-config-b.ts @@ -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> = { + + // 测试主要服务实例 + '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 = { + // 开发环境 + 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: '***' // 隐藏敏感信息 + } + }, + }); +} \ No newline at end of file diff --git a/app/routes/api.chat-messages.tsx b/app/routes/api.chat-messages.tsx index 3e41563..e1debb9 100644 --- a/app/routes/api.chat-messages.tsx +++ b/app/routes/api.chat-messages.tsx @@ -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', }, diff --git a/app/routes/api.conversations.$id.name.tsx b/app/routes/api.conversations.$id.name.tsx index ffdd450..48f8d12 100644 --- a/app/routes/api.conversations.$id.name.tsx +++ b/app/routes/api.conversations.$id.name.tsx @@ -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), }, diff --git a/app/routes/api.conversations.$id.tsx b/app/routes/api.conversations.$id.tsx index d7a0451..3eb6cb6 100644 --- a/app/routes/api.conversations.$id.tsx +++ b/app/routes/api.conversations.$id.tsx @@ -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), }, diff --git a/app/routes/api.conversations.tsx b/app/routes/api.conversations.tsx index d0be5f5..5eb3032 100644 --- a/app/routes/api.conversations.tsx +++ b/app/routes/api.conversations.tsx @@ -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), }, diff --git a/app/routes/api.messages.tsx b/app/routes/api.messages.tsx index c32a624..6a6dc38 100644 --- a/app/routes/api.messages.tsx +++ b/app/routes/api.messages.tsx @@ -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), }, diff --git a/app/routes/api.parameters.tsx b/app/routes/api.parameters.tsx index 181712b..cea8995 100644 --- a/app/routes/api.parameters.tsx +++ b/app/routes/api.parameters.tsx @@ -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), }, diff --git a/app/services/dify-client.server.ts b/app/services/dify-client.server.ts index b40d4a6..c9c09c1 100644 --- a/app/services/dify-client.server.ts +++ b/app/services/dify-client.server.ts @@ -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)['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(); }, }; diff --git a/docs/dify-frontend-modification-summary.md b/docs/dify-frontend-modification-summary.md new file mode 100644 index 0000000..5439a16 --- /dev/null +++ b/docs/dify-frontend-modification-summary.md @@ -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 diff --git a/docs/dify-proxy-backend-integration.md b/docs/dify-proxy-backend-integration.md new file mode 100644 index 0000000..0ff813a --- /dev/null +++ b/docs/dify-proxy-backend-integration.md @@ -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 测试所有接口 +- [ ] 与前端联调测试通过 + +--- + +**祝开发顺利!** 🎉