feat : 添加后端的统一日志系统
This commit is contained in:
parent
c97e227685
commit
a9b31f540f
|
|
@ -11,7 +11,7 @@ node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
uploads
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start Vite dev server
|
||||||
|
npm run build # Type-check and build for production
|
||||||
|
npm run preview # Preview production build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a Vue 3 + TypeScript AI chat UI application built with the following stack:
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- Vue 3 (Composition API with `<script setup>`)
|
||||||
|
- Pinia for state management
|
||||||
|
- TypeScript with path alias `@/*` → `src/*`
|
||||||
|
- Vite for build tooling
|
||||||
|
|
||||||
|
**Styling:**
|
||||||
|
- TailwindCSS 4 + UnoCSS (dual setup)
|
||||||
|
- Custom SCSS for global styles
|
||||||
|
- Dark mode support via `.dark` class
|
||||||
|
|
||||||
|
**Key Dependencies:**
|
||||||
|
- `markstream-vue` - Streaming markdown rendering
|
||||||
|
- `lucide-vue-next` - Icon library
|
||||||
|
- `@vueuse/core` - Vue composables utilities
|
||||||
|
- `echarts`, `mermaid`, `shiki` - Visualization and code highlighting
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Vue components organized by feature
|
||||||
|
│ ├── chat/ # Main chat interface (ChatMain, ChatHeader, MessageList, WelcomeScreen)
|
||||||
|
│ ├── message/ # Message rendering (MessageBubble, CodeBlock, ThinkingNode, etc.)
|
||||||
|
│ ├── input/ # Chat input and attachments
|
||||||
|
│ ├── sidebar/ # Conversation list sidebar
|
||||||
|
│ ├── modals/ # Settings and search modals
|
||||||
|
│ └── ui/ # Reusable form components
|
||||||
|
├── stores/ # Pinia stores
|
||||||
|
│ ├── chat.ts # Conversation and message management
|
||||||
|
│ └── settings.ts # App settings and theme
|
||||||
|
├── composables/ # Reusable composables (useKeyboard)
|
||||||
|
├── services/ # API layer (api.ts - chat streaming, file upload)
|
||||||
|
├── types/ # TypeScript type definitions
|
||||||
|
└── utils/ # Helper functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
Two Pinia stores manage application state:
|
||||||
|
|
||||||
|
**`useChatStore`** (`src/stores/chat.ts`):
|
||||||
|
- Manages conversations (CRUD operations)
|
||||||
|
- Handles message streaming state
|
||||||
|
- Persists to localStorage
|
||||||
|
|
||||||
|
**`useSettingsStore`** (`src/stores/settings.ts`):
|
||||||
|
- App settings (theme, font size, preferences)
|
||||||
|
- Sidebar state
|
||||||
|
- Modal visibility controls
|
||||||
|
- Persists to localStorage
|
||||||
|
|
||||||
|
## API Layer
|
||||||
|
|
||||||
|
`src/services/api.ts` defines the API contract. The backend must implement these endpoints:
|
||||||
|
- `POST /api/chat-ui/chat` - Chat (streaming supported)
|
||||||
|
- `GET /api/chat-ui/conversations` - List conversations
|
||||||
|
- `POST /api/chat-ui/upload` - File upload
|
||||||
|
- `GET /api/chat-ui/models` - Model list
|
||||||
|
- `POST /api/chat-ui/stop/:id` - Stop generation
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- Base path configured as `/chat-ui/` in `vite.config.ts`
|
||||||
|
- Proxy configured for `/api/chat-ui` → `http://localhost:3000`
|
||||||
|
- UnoCSS shortcuts available: `flex-center`, `full`, and regex-based patterns
|
||||||
|
- Toast notifications available globally via `window.$toast(message, type)`
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 日志系统文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目实现了统一的后端日志系统,为所有接口提供了详细的日志记录功能,包括请求、响应、错误等状态。
|
||||||
|
|
||||||
|
## 日志系统结构
|
||||||
|
|
||||||
|
### 1. logger.js
|
||||||
|
- 统一日志管理模块
|
||||||
|
- 支持多种日志级别:ERROR, WARN, INFO, DEBUG
|
||||||
|
- 支持控制台和文件双重输出
|
||||||
|
- 包含HTTP请求专用日志方法
|
||||||
|
|
||||||
|
### 2. 日志文件位置
|
||||||
|
- 存储在 `server/logs/` 目录
|
||||||
|
- 按日期命名:`server-YYYY-MM-DD.log`
|
||||||
|
- JSON格式便于查询和分析
|
||||||
|
|
||||||
|
## 日志级别
|
||||||
|
|
||||||
|
- `ERROR`: 错误和异常情况
|
||||||
|
- `WARN`: 警告和潜在问题
|
||||||
|
- `INFO`: 重要操作和状态信息
|
||||||
|
- `DEBUG`: 详细调试信息
|
||||||
|
|
||||||
|
## 接口日志详情
|
||||||
|
|
||||||
|
### 1. 对话接口 (`/api/chat-ui/chat`)
|
||||||
|
- 记录请求代理开始和结束
|
||||||
|
- 记录目标API响应状态
|
||||||
|
- 记录代理错误
|
||||||
|
|
||||||
|
### 2. 模型列表接口 (`/api/chat-ui/models`)
|
||||||
|
- 记录请求基本信息
|
||||||
|
- 记录返回的模型数量
|
||||||
|
|
||||||
|
### 3. 对话管理接口 (`/api/chat-ui/conversations/*`)
|
||||||
|
- 记录操作类型(获取/创建/删除)
|
||||||
|
- 记录对话ID
|
||||||
|
- 记录操作结果
|
||||||
|
|
||||||
|
### 4. 文件上传接口 (`/api/chat-ui/upload`)
|
||||||
|
- 记录上传文件信息(名称、大小、类型)
|
||||||
|
- 记录上传结果和生成的URL
|
||||||
|
|
||||||
|
### 5. 停止生成接口 (`/api/chat-ui/stop/*`)
|
||||||
|
- 记录停止请求的ID
|
||||||
|
- 记录操作结果
|
||||||
|
|
||||||
|
### 6. HTTP通用日志
|
||||||
|
- 记录所有请求的方法、URL、状态码
|
||||||
|
- 记录请求耗时
|
||||||
|
- 记录客户端IP和User-Agent
|
||||||
|
- 根据状态码自动分类日志级别
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
```bash
|
||||||
|
LOG_LEVEL=DEBUG # 设置日志级别,默认为INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在代码中使用
|
||||||
|
```javascript
|
||||||
|
const { logger } = require('./logger');
|
||||||
|
|
||||||
|
logger.info('Some info message', { additional: 'data' });
|
||||||
|
logger.error('Error occurred', { error: error.message });
|
||||||
|
logger.debug('Debug info', { variable: value });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志示例
|
||||||
|
|
||||||
|
### HTTP请求日志
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-03-03T03:26:58.631Z",
|
||||||
|
"level": "INFO",
|
||||||
|
"message": "HTTP GET /api/chat-ui/models 200 15ms",
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/api/chat-ui/models",
|
||||||
|
"statusCode": 200,
|
||||||
|
"duration": "15ms",
|
||||||
|
"userAgent": "Mozilla/5.0...",
|
||||||
|
"ip": "::1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特定接口日志
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2026-03-03T03:27:12.123Z",
|
||||||
|
"level": "INFO",
|
||||||
|
"message": "Successfully uploaded file",
|
||||||
|
"filename": "1642342342-file.jpg",
|
||||||
|
"originalName": "file.jpg",
|
||||||
|
"url": "http://localhost:3000/uploads/1642342342-file.jpg",
|
||||||
|
"size": 123456
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 日志级别枚举
|
||||||
|
const LOG_LEVELS = {
|
||||||
|
ERROR: 0,
|
||||||
|
WARN: 1,
|
||||||
|
INFO: 2,
|
||||||
|
DEBUG: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建logs目录
|
||||||
|
const logsDir = path.join(__dirname, 'logs');
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志记录器类
|
||||||
|
class Logger {
|
||||||
|
constructor(level = 'INFO') {
|
||||||
|
this.level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
|
||||||
|
this.logFilePath = path.join(logsDir, `server-${new Date().toISOString().split('T')[0]}.log`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_shouldLog(level) {
|
||||||
|
return LOG_LEVELS[level.toUpperCase()] <= this.level;
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLog(level, message, meta = {}) {
|
||||||
|
if (!this._shouldLog(level)) return;
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logEntry = {
|
||||||
|
timestamp,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
...meta,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 控制台输出
|
||||||
|
console.log(`[${timestamp}] [${level}] ${message}`, meta);
|
||||||
|
|
||||||
|
// 文件输出
|
||||||
|
try {
|
||||||
|
const logLine = JSON.stringify(logEntry) + '\n';
|
||||||
|
fs.appendFileSync(this.logFilePath, logLine);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to write to log file:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, meta = {}) {
|
||||||
|
this._writeLog('ERROR', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message, meta = {}) {
|
||||||
|
this._writeLog('WARN', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, meta = {}) {
|
||||||
|
this._writeLog('INFO', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message, meta = {}) {
|
||||||
|
this._writeLog('DEBUG', message, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP请求日志专用方法
|
||||||
|
http(req, res, startTime, error = null) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
const logMeta = {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
ip: req.ip || req.connection.remoteAddress,
|
||||||
|
...(error ? { error: error.message || error } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusCategory = Math.floor(res.statusCode / 100);
|
||||||
|
let level = 'INFO';
|
||||||
|
if (error || statusCategory >= 5) {
|
||||||
|
level = 'ERROR';
|
||||||
|
} else if (statusCategory >= 4) {
|
||||||
|
level = 'WARN';
|
||||||
|
}
|
||||||
|
|
||||||
|
this._writeLog(level, `HTTP ${req.method} ${req.url} ${res.statusCode} ${duration}ms`, logMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局日志实例
|
||||||
|
const logger = new Logger(process.env.LOG_LEVEL || 'INFO');
|
||||||
|
|
||||||
|
module.exports = { logger, LOG_LEVELS };
|
||||||
314
server/server.js
314
server/server.js
|
|
@ -8,25 +8,51 @@ const fs = require('fs');
|
||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// 引入自定义日志系统
|
||||||
|
const { logger } = require('./logger');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
// 配置全局请求日志,可以在终端里看到每个到达 Node 端点的请求记录
|
// 配置全局请求日志中间件
|
||||||
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));
|
app.use((req, res, next) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 保存原始的res.end方法
|
||||||
|
const originalEnd = res.end;
|
||||||
|
res.end = function(chunk, encoding) {
|
||||||
|
// 调用自定义日志记录
|
||||||
|
logger.http(req, res, startTime);
|
||||||
|
|
||||||
|
// 调用原始的res.end
|
||||||
|
originalEnd.call(this, chunk, encoding);
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// 配置 CORS,允许前端项目的跨域请求
|
// 配置 CORS,允许前端项目的跨域请求
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// --- 1. 流式对话请求代理配置 ---
|
// --- 1. 流式对话请求代理配置 ---
|
||||||
// 在请求交给代理组件之前,拦截所有的 /api/chat-ui/chat 并强行给 Header 加上 Bearer Token
|
|
||||||
app.use('/api/chat-ui/chat', (req, res, next) => {
|
app.use('/api/chat-ui/chat', (req, res, next) => {
|
||||||
const apiKey = process.env.ALIYUN_API_KEY;
|
logger.info('Received chat request', {
|
||||||
if (!apiKey) {
|
method: req.method,
|
||||||
console.error("【错误】发送代理请求前未配置 ALIYUN_API_KEY !");
|
url: req.url,
|
||||||
} else {
|
headers: {
|
||||||
req.headers['authorization'] = `Bearer ${apiKey}`;
|
'content-type': req.headers['content-type'],
|
||||||
|
'content-length': req.headers['content-length']
|
||||||
}
|
}
|
||||||
next();
|
});
|
||||||
|
|
||||||
|
const apiKey = process.env.ALIYUN_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
logger.error("【错误】发送代理请求前未配置 ALIYUN_API_KEY !");
|
||||||
|
} else {
|
||||||
|
req.headers['authorization'] = `Bearer ${apiKey}`;
|
||||||
|
logger.debug('Added authorization header for chat request');
|
||||||
|
}
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 注意:代理中间件需要在 body-parser (express.json) 之前,不然代理会导致请求体丢失
|
// 注意:代理中间件需要在 body-parser (express.json) 之前,不然代理会导致请求体丢失
|
||||||
|
|
@ -39,6 +65,28 @@ app.use(
|
||||||
// 去除路径前缀,确保发往阿里云的路径是纯正的 completions 路径
|
// 去除路径前缀,确保发往阿里云的路径是纯正的 completions 路径
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
'^/api/chat-ui/chat': '',
|
'^/api/chat-ui/chat': '',
|
||||||
|
},
|
||||||
|
// 代理日志
|
||||||
|
onProxyReq: (proxyReq, req, res) => {
|
||||||
|
logger.info('Proxying chat request to DashScope API', {
|
||||||
|
target: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||||||
|
method: req.method,
|
||||||
|
url: req.url
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProxyRes: (proxyRes, req, res) => {
|
||||||
|
logger.info('Received response from DashScope API', {
|
||||||
|
statusCode: proxyRes.statusCode,
|
||||||
|
headers: proxyRes.headers
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err, req, res) => {
|
||||||
|
logger.error('Error occurred during proxying chat request', {
|
||||||
|
error: err.message,
|
||||||
|
method: req.method,
|
||||||
|
url: req.url
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Proxy error: ' + err.message });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -46,62 +94,142 @@ app.use(
|
||||||
// --- 下面的路由专门走 Node.js 业务逻辑,因此需要解析 JSON Body ---
|
// --- 下面的路由专门走 Node.js 业务逻辑,因此需要解析 JSON Body ---
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// --- 2. 获取模型列表 ---
|
|
||||||
app.get('/api/chat-ui/models', (req, res) => {
|
|
||||||
res.json([
|
|
||||||
{
|
|
||||||
id: "qwen-max",
|
|
||||||
name: "通义千问 Max",
|
|
||||||
description: "最强大的模型",
|
|
||||||
maxTokens: 8192,
|
|
||||||
provider: "Aliyun"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qwen-plus",
|
|
||||||
name: "通义千问 Plus",
|
|
||||||
description: "能力均衡",
|
|
||||||
maxTokens: 8192,
|
|
||||||
provider: "Aliyun"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 内存中暂时存放对话数据用于 Mock
|
// 内存中暂时存放对话数据用于 Mock
|
||||||
const conversationsDB = {};
|
const conversationsDB = {};
|
||||||
|
|
||||||
|
// --- 2. 获取模型列表 ---
|
||||||
|
app.get('/api/chat-ui/models', (req, res) => {
|
||||||
|
logger.info('Getting models list', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = [
|
||||||
|
{
|
||||||
|
id: "qwen-max",
|
||||||
|
name: "通义千问 Max",
|
||||||
|
description: "最强大的模型",
|
||||||
|
maxTokens: 8192,
|
||||||
|
provider: "Aliyun"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "qwen-plus",
|
||||||
|
name: "通义千问 Plus",
|
||||||
|
description: "能力均衡",
|
||||||
|
maxTokens: 8192,
|
||||||
|
provider: "Aliyun"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
res.json(models);
|
||||||
|
logger.info('Successfully returned models list', { count: models.length });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting models list', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- 3. 获取所有对话历史 ---
|
// --- 3. 获取所有对话历史 ---
|
||||||
app.get('/api/chat-ui/conversations', (req, res) => {
|
app.get('/api/chat-ui/conversations', (req, res) => {
|
||||||
res.json(Object.values(conversationsDB));
|
logger.info('Getting all conversations', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversations = Object.values(conversationsDB);
|
||||||
|
res.json(conversations);
|
||||||
|
logger.info('Successfully returned conversations', { count: conversations.length });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting conversations', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 4. 获取单个对话 ---
|
// --- 4. 获取单个对话 ---
|
||||||
app.get('/api/chat-ui/conversations/:id', (req, res) => {
|
app.get('/api/chat-ui/conversations/:id', (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const conversation = conversationsDB[id];
|
logger.info('Getting conversation by ID', {
|
||||||
if (conversation) {
|
method: req.method,
|
||||||
res.json(conversation);
|
url: req.url,
|
||||||
} else {
|
conversationId: id,
|
||||||
res.status(404).json({ error: '对话不存在' });
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversation = conversationsDB[id];
|
||||||
|
if (conversation) {
|
||||||
|
res.json(conversation);
|
||||||
|
logger.info('Successfully returned conversation', { conversationId: id });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: '对话不存在' });
|
||||||
|
logger.warn('Conversation not found', { conversationId: id });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting conversation by ID', {
|
||||||
|
conversationId: id,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 5. 保存或更新对话 ---
|
// --- 5. 保存或更新对话 ---
|
||||||
// 前端可能会在 /api/chat-ui/conversations/:id 用 POST 或 PUT 更新? 也可以直接提供一个保存接口
|
|
||||||
app.post('/api/chat-ui/conversations', (req, res) => {
|
app.post('/api/chat-ui/conversations', (req, res) => {
|
||||||
|
logger.info('Creating or updating conversation', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
body: {
|
||||||
|
hasId: !!req.body.id,
|
||||||
|
title: req.body.title
|
||||||
|
},
|
||||||
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
if(!data.id) data.id = uuidv4();
|
if(!data.id) {
|
||||||
|
data.id = uuidv4();
|
||||||
|
logger.debug('Generated new conversation ID', { conversationId: data.id });
|
||||||
|
}
|
||||||
|
|
||||||
conversationsDB[data.id] = data;
|
conversationsDB[data.id] = data;
|
||||||
res.json(data);
|
res.json(data);
|
||||||
})
|
logger.info('Successfully saved conversation', { conversationId: data.id });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error saving conversation', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- 6. 删除对话 ---
|
// --- 6. 删除对话 ---
|
||||||
app.delete('/api/chat-ui/conversations/:id', (req, res) => {
|
app.delete('/api/chat-ui/conversations/:id', (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (conversationsDB[id]) {
|
logger.info('Deleting conversation', {
|
||||||
delete conversationsDB[id];
|
method: req.method,
|
||||||
res.json({ success: true, message: "删除成功" });
|
url: req.url,
|
||||||
} else {
|
conversationId: id,
|
||||||
res.status(404).json({ error: '对话不存在' });
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (conversationsDB[id]) {
|
||||||
|
delete conversationsDB[id];
|
||||||
|
res.json({ success: true, message: "删除成功" });
|
||||||
|
logger.info('Successfully deleted conversation', { conversationId: id });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: '对话不存在' });
|
||||||
|
logger.warn('Attempted to delete non-existent conversation', { conversationId: id });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting conversation', {
|
||||||
|
conversationId: id,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -109,6 +237,7 @@ app.delete('/api/chat-ui/conversations/:id', (req, res) => {
|
||||||
const uploadDir = path.join(__dirname, 'uploads');
|
const uploadDir = path.join(__dirname, 'uploads');
|
||||||
if (!fs.existsSync(uploadDir)) {
|
if (!fs.existsSync(uploadDir)) {
|
||||||
fs.mkdirSync(uploadDir);
|
fs.mkdirSync(uploadDir);
|
||||||
|
logger.info('Created uploads directory', { path: uploadDir });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置文件上传
|
// 配置文件上传
|
||||||
|
|
@ -128,38 +257,95 @@ app.use('/uploads', express.static(uploadDir));
|
||||||
|
|
||||||
// --- 7. 上传文件 ---
|
// --- 7. 上传文件 ---
|
||||||
app.post('/api/chat-ui/upload', upload.single('file'), (req, res) => {
|
app.post('/api/chat-ui/upload', upload.single('file'), (req, res) => {
|
||||||
if (!req.file) {
|
logger.info('Processing file upload', {
|
||||||
return res.status(400).json({ error: '没有文件上传' });
|
method: req.method,
|
||||||
}
|
url: req.url,
|
||||||
|
originalFilename: req.file ? req.file.originalname : 'none',
|
||||||
// 返回供前端使用和访问的 URL
|
mimetype: req.file ? req.file.mimetype : 'none',
|
||||||
res.json({
|
size: req.file ? req.file.size : 'none',
|
||||||
url: `http://localhost:${PORT}/uploads/${req.file.filename}`,
|
ip: req.ip || req.connection.remoteAddress
|
||||||
name: req.file.originalname,
|
|
||||||
size: req.file.size,
|
|
||||||
mimeType: req.file.mimetype
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!req.file) {
|
||||||
|
const errorMsg = '没有文件上传';
|
||||||
|
logger.warn(errorMsg, {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url
|
||||||
|
});
|
||||||
|
return res.status(400).json({ error: errorMsg });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 返回供前端使用和访问的 URL
|
||||||
|
const response = {
|
||||||
|
url: `http://localhost:${PORT}/uploads/${req.file.filename}`,
|
||||||
|
name: req.file.originalname,
|
||||||
|
size: req.file.size,
|
||||||
|
mimeType: req.file.mimetype
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(response);
|
||||||
|
logger.info('Successfully uploaded file', {
|
||||||
|
filename: req.file.filename,
|
||||||
|
originalName: req.file.originalname,
|
||||||
|
url: response.url,
|
||||||
|
size: req.file.size
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error processing file upload', {
|
||||||
|
originalFilename: req.file.originalname,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'File upload processing failed' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 8. 停止生成 ---
|
// --- 8. 停止生成 ---
|
||||||
// 这个接口对于本地代理没有实际效果,因为流的断开是通过底层 AbortController 控制的,此处直接返回成功
|
|
||||||
app.post(['/api/chat-ui/stop', '/api/chat-ui/stop/:id'], (req, res) => {
|
app.post(['/api/chat-ui/stop', '/api/chat-ui/stop/:id'], (req, res) => {
|
||||||
res.json({ success: true, message: "已发出停止指令" });
|
const id = req.params.id;
|
||||||
|
logger.info('Stop generation request received', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
messageId: id,
|
||||||
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.json({ success: true, message: "已发出停止指令" });
|
||||||
|
logger.info('Stop generation request processed', { messageId: id });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error processing stop generation request', {
|
||||||
|
messageId: id,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 其他所有路由返回404
|
// 其他所有路由返回404
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
|
logger.warn('Route not found', {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
ip: req.ip || req.connection.remoteAddress
|
||||||
|
});
|
||||||
res.status(404).json({ error: 'Endpoint not found' });
|
res.status(404).json({ error: 'Endpoint not found' });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log('====================================');
|
logger.info('Server started successfully', {
|
||||||
console.log(`本地代理服务器已启动,监听端口: ${PORT}`);
|
port: PORT,
|
||||||
console.log('====================================');
|
timestamp: new Date().toISOString()
|
||||||
if (!process.env.ALIYUN_API_KEY) {
|
});
|
||||||
console.log('⚠️ 警告: 未在 .env 文件中检测到 ALIYUN_API_KEY!');
|
|
||||||
console.log('请在 server/.env 中添加您的百炼 API Key。');
|
console.log('====================================');
|
||||||
} else {
|
console.log(`本地代理服务器已启动,监听端口: ${PORT}`);
|
||||||
console.log('✅ 检测到了 API Key。');
|
console.log('====================================');
|
||||||
}
|
if (!process.env.ALIYUN_API_KEY) {
|
||||||
|
console.log('⚠️ 警告: 未在 .env 文件中检测到 ALIYUN_API_KEY!');
|
||||||
|
console.log('请在 server/.env 中添加您的百炼 API Key。');
|
||||||
|
} else {
|
||||||
|
console.log('✅ 检测到了 API Key。');
|
||||||
|
logger.info('API Key detected in environment');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,33 @@ function handlePin() {
|
||||||
|
|
||||||
// 发送消息 - 使用真实 API
|
// 发送消息 - 使用真实 API
|
||||||
async function handleSend(text: string, attachments: Attachment[]) {
|
async function handleSend(text: string, attachments: Attachment[]) {
|
||||||
|
// 检查是否还有正在上传的附件
|
||||||
|
const uploadingAttachments = attachments.filter(a => a.uploading);
|
||||||
|
if (uploadingAttachments.length > 0) {
|
||||||
|
// 等待所有上传完成
|
||||||
|
const uploads = uploadingAttachments.map(async (attachment) => {
|
||||||
|
// 这里我们可以通过检查附件状态来判断是否上传完成
|
||||||
|
// 但更简单的方法是等待一小段时间,让上传有机会完成
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const checkUpload = () => {
|
||||||
|
const stillUploading = attachments.some(a => a.id === attachment.id && a.uploading);
|
||||||
|
if (!stillUploading) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
setTimeout(checkUpload, 100); // 每100ms检查一次
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkUpload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(uploads);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('等待上传完成时发生错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有当前对话,创建新对话
|
// 如果没有当前对话,创建新对话
|
||||||
if (!currentConversation.value) {
|
if (!currentConversation.value) {
|
||||||
chatStore.createConversation();
|
chatStore.createConversation();
|
||||||
|
|
@ -144,10 +171,16 @@ async function handleSend(text: string, attachments: Attachment[]) {
|
||||||
// 创建 AbortController
|
// 创建 AbortController
|
||||||
abortController.value = new AbortController();
|
abortController.value = new AbortController();
|
||||||
try {
|
try {
|
||||||
|
// 提取图片URL用于发送给API
|
||||||
|
const imageUrls = attachments
|
||||||
|
.filter((a) => a.type === "image")
|
||||||
|
.map(a => a.url);
|
||||||
|
|
||||||
const stream = chatApi.streamChat(
|
const stream = chatApi.streamChat(
|
||||||
{
|
{
|
||||||
message: text,
|
message: text,
|
||||||
conversationId: currentConversation.value?.id || "",
|
conversationId: currentConversation.value?.id || "",
|
||||||
|
images: imageUrls, // 添加图片URL
|
||||||
model: settings.value.defaultModel,
|
model: settings.value.defaultModel,
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ import {
|
||||||
import AttachmentPreview from "./AttachmentPreview.vue";
|
import AttachmentPreview from "./AttachmentPreview.vue";
|
||||||
import { generateId } from "@/utils/helpers";
|
import { generateId } from "@/utils/helpers";
|
||||||
import type { Attachment } from "@/types/chat";
|
import type { Attachment } from "@/types/chat";
|
||||||
|
import { chatApi } from "@/services/api";
|
||||||
|
|
||||||
interface AttachmentWithProgress extends Attachment {
|
interface AttachmentWithProgress extends Attachment {
|
||||||
uploading?: boolean;
|
uploading?: boolean;
|
||||||
|
|
@ -338,30 +339,35 @@ async function addFileAsAttachment(
|
||||||
|
|
||||||
attachments.value.push(attachment);
|
attachments.value.push(attachment);
|
||||||
|
|
||||||
// 模拟上传进度
|
// 实际上传文件到服务器
|
||||||
simulateUpload(id);
|
await uploadFileToServer(id, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulateUpload(id: string) {
|
// 上传文件到服务器
|
||||||
let progress = 0;
|
async function uploadFileToServer(id: string, file: File) {
|
||||||
const interval = setInterval(() => {
|
try {
|
||||||
progress += Math.random() * 30;
|
const uploadResult = await chatApi.uploadFile(file);
|
||||||
if (progress >= 100) {
|
|
||||||
progress = 100;
|
|
||||||
clearInterval(interval);
|
|
||||||
|
|
||||||
const attachment = attachments.value.find((a) => a.id === id);
|
const attachment = attachments.value.find((a) => a.id === id);
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
attachment.uploading = false;
|
// 替换本地预览URL为服务器返回的真实URL
|
||||||
attachment.progress = 100;
|
attachment.url = uploadResult.url;
|
||||||
}
|
attachment.uploading = false;
|
||||||
} else {
|
attachment.progress = 100;
|
||||||
const attachment = attachments.value.find((a) => a.id === id);
|
|
||||||
if (attachment) {
|
|
||||||
attachment.progress = progress;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 200);
|
} catch (error) {
|
||||||
|
console.error('文件上传失败:', error);
|
||||||
|
|
||||||
|
const attachment = attachments.value.find((a) => a.id === id);
|
||||||
|
if (attachment) {
|
||||||
|
attachment.uploading = false;
|
||||||
|
// 设置错误状态或者移除附件
|
||||||
|
removeAttachment(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
window.$toast && window.$toast('文件上传失败,请重试', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除附件
|
// 移除附件
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,34 @@ class ChatApi {
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): AsyncGenerator<string> {
|
): AsyncGenerator<string> {
|
||||||
|
// 构建消息数组,考虑是否包含图片
|
||||||
|
let userContent;
|
||||||
|
if (request.images && request.images.length > 0) {
|
||||||
|
// 如果有图片,则构建内容数组(针对阿里云DashScope API的格式)
|
||||||
|
userContent = [
|
||||||
|
{ type: "text", text: request.message }
|
||||||
|
];
|
||||||
|
// 添加图片URL到内容中(阿里云格式)
|
||||||
|
request.images.forEach(imageUrl => {
|
||||||
|
userContent.push({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: imageUrl // 注意:阿里云格式不需要嵌套对象
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 没有图片时,使用简单的文本
|
||||||
|
userContent = request.message;
|
||||||
|
}
|
||||||
|
|
||||||
// 将前端简化的请求翻译为 OpenAI 兼容的规范请求体
|
// 将前端简化的请求翻译为 OpenAI 兼容的规范请求体
|
||||||
const openAiRequest = {
|
const openAiRequest = {
|
||||||
model: request.model || "qwen-plus",
|
model: request.model || "qwen-plus", // 可能需要指定支持视觉的模型
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: request.systemPrompt || "你是一个有用的助手。" },
|
{ role: "system", content: request.systemPrompt || "你是一个支持视觉理解的助手。" },
|
||||||
{ role: "user", content: request.message }
|
{
|
||||||
|
role: "user",
|
||||||
|
content: userContent
|
||||||
|
}
|
||||||
],
|
],
|
||||||
stream: true,
|
stream: true,
|
||||||
temperature: request.temperature,
|
temperature: request.temperature,
|
||||||
|
|
@ -158,12 +180,36 @@ class ChatApi {
|
||||||
* 非流式对话
|
* 非流式对话
|
||||||
*/
|
*/
|
||||||
async chat(request: ChatRequest): Promise<ChatResponse> {
|
async chat(request: ChatRequest): Promise<ChatResponse> {
|
||||||
|
// 构建消息数组,考虑是否包含图片
|
||||||
|
let userContent;
|
||||||
|
if (request.images && request.images.length > 0) {
|
||||||
|
// 如果有图片,则构建内容数组
|
||||||
|
userContent = [
|
||||||
|
{ type: "text", text: request.message }
|
||||||
|
];
|
||||||
|
// 添加图片URL到内容中
|
||||||
|
request.images.forEach(imageUrl => {
|
||||||
|
userContent.push({
|
||||||
|
type: "image_url",
|
||||||
|
image_url: { url: imageUrl }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 没有图片时,使用简单的文本
|
||||||
|
userContent = request.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
...request,
|
||||||
|
message: userContent
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.CHAT}`, {
|
const response = await fetch(`${this.baseUrl}${API_ENDPOINTS.CHAT}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(requestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue