feat : 添加后端的统一日志系统
This commit is contained in:
parent
c97e227685
commit
a9b31f540f
|
|
@ -11,7 +11,7 @@ node_modules
|
|||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
uploads
|
||||
.env
|
||||
|
||||
# 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 };
|
||||
218
server/server.js
218
server/server.js
|
|
@ -8,23 +8,49 @@ const fs = require('fs');
|
|||
const morgan = require('morgan');
|
||||
require('dotenv').config();
|
||||
|
||||
// 引入自定义日志系统
|
||||
const { logger } = require('./logger');
|
||||
|
||||
const app = express();
|
||||
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,允许前端项目的跨域请求
|
||||
app.use(cors());
|
||||
|
||||
// --- 1. 流式对话请求代理配置 ---
|
||||
// 在请求交给代理组件之前,拦截所有的 /api/chat-ui/chat 并强行给 Header 加上 Bearer Token
|
||||
app.use('/api/chat-ui/chat', (req, res, next) => {
|
||||
logger.info('Received chat request', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
headers: {
|
||||
'content-type': req.headers['content-type'],
|
||||
'content-length': req.headers['content-length']
|
||||
}
|
||||
});
|
||||
|
||||
const apiKey = process.env.ALIYUN_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error("【错误】发送代理请求前未配置 ALIYUN_API_KEY !");
|
||||
logger.error("【错误】发送代理请求前未配置 ALIYUN_API_KEY !");
|
||||
} else {
|
||||
req.headers['authorization'] = `Bearer ${apiKey}`;
|
||||
logger.debug('Added authorization header for chat request');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
|
@ -39,6 +65,28 @@ app.use(
|
|||
// 去除路径前缀,确保发往阿里云的路径是纯正的 completions 路径
|
||||
pathRewrite: {
|
||||
'^/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,9 +94,19 @@ app.use(
|
|||
// --- 下面的路由专门走 Node.js 业务逻辑,因此需要解析 JSON Body ---
|
||||
app.use(express.json());
|
||||
|
||||
// 内存中暂时存放对话数据用于 Mock
|
||||
const conversationsDB = {};
|
||||
|
||||
// --- 2. 获取模型列表 ---
|
||||
app.get('/api/chat-ui/models', (req, res) => {
|
||||
res.json([
|
||||
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",
|
||||
|
|
@ -63,45 +121,115 @@ app.get('/api/chat-ui/models', (req, res) => {
|
|||
maxTokens: 8192,
|
||||
provider: "Aliyun"
|
||||
}
|
||||
]);
|
||||
});
|
||||
];
|
||||
|
||||
// 内存中暂时存放对话数据用于 Mock
|
||||
const conversationsDB = {};
|
||||
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. 获取所有对话历史 ---
|
||||
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. 获取单个对话 ---
|
||||
app.get('/api/chat-ui/conversations/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
logger.info('Getting conversation by ID', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
conversationId: id,
|
||||
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. 保存或更新对话 ---
|
||||
// 前端可能会在 /api/chat-ui/conversations/:id 用 POST 或 PUT 更新? 也可以直接提供一个保存接口
|
||||
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;
|
||||
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;
|
||||
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. 删除对话 ---
|
||||
app.delete('/api/chat-ui/conversations/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
logger.info('Deleting conversation', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
conversationId: id,
|
||||
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');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir);
|
||||
logger.info('Created uploads directory', { path: uploadDir });
|
||||
}
|
||||
|
||||
// 配置文件上传
|
||||
|
|
@ -128,31 +257,87 @@ app.use('/uploads', express.static(uploadDir));
|
|||
|
||||
// --- 7. 上传文件 ---
|
||||
app.post('/api/chat-ui/upload', upload.single('file'), (req, res) => {
|
||||
logger.info('Processing file upload', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
originalFilename: req.file ? req.file.originalname : 'none',
|
||||
mimetype: req.file ? req.file.mimetype : 'none',
|
||||
size: req.file ? req.file.size : 'none',
|
||||
ip: req.ip || req.connection.remoteAddress
|
||||
});
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: '没有文件上传' });
|
||||
const errorMsg = '没有文件上传';
|
||||
logger.warn(errorMsg, {
|
||||
method: req.method,
|
||||
url: req.url
|
||||
});
|
||||
return res.status(400).json({ error: errorMsg });
|
||||
}
|
||||
|
||||
try {
|
||||
// 返回供前端使用和访问的 URL
|
||||
res.json({
|
||||
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. 停止生成 ---
|
||||
// 这个接口对于本地代理没有实际效果,因为流的断开是通过底层 AbortController 控制的,此处直接返回成功
|
||||
app.post(['/api/chat-ui/stop', '/api/chat-ui/stop/:id'], (req, res) => {
|
||||
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
|
||||
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' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
logger.info('Server started successfully', {
|
||||
port: PORT,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
console.log('====================================');
|
||||
console.log(`本地代理服务器已启动,监听端口: ${PORT}`);
|
||||
console.log('====================================');
|
||||
|
|
@ -161,5 +346,6 @@ app.listen(PORT, () => {
|
|||
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
|
||||
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) {
|
||||
chatStore.createConversation();
|
||||
|
|
@ -144,10 +171,16 @@ async function handleSend(text: string, attachments: Attachment[]) {
|
|||
// 创建 AbortController
|
||||
abortController.value = new AbortController();
|
||||
try {
|
||||
// 提取图片URL用于发送给API
|
||||
const imageUrls = attachments
|
||||
.filter((a) => a.type === "image")
|
||||
.map(a => a.url);
|
||||
|
||||
const stream = chatApi.streamChat(
|
||||
{
|
||||
message: text,
|
||||
conversationId: currentConversation.value?.id || "",
|
||||
images: imageUrls, // 添加图片URL
|
||||
model: settings.value.defaultModel,
|
||||
stream: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ import {
|
|||
import AttachmentPreview from "./AttachmentPreview.vue";
|
||||
import { generateId } from "@/utils/helpers";
|
||||
import type { Attachment } from "@/types/chat";
|
||||
import { chatApi } from "@/services/api";
|
||||
|
||||
interface AttachmentWithProgress extends Attachment {
|
||||
uploading?: boolean;
|
||||
|
|
@ -338,31 +339,36 @@ async function addFileAsAttachment(
|
|||
|
||||
attachments.value.push(attachment);
|
||||
|
||||
// 模拟上传进度
|
||||
simulateUpload(id);
|
||||
// 实际上传文件到服务器
|
||||
await uploadFileToServer(id, file);
|
||||
}
|
||||
|
||||
function simulateUpload(id: string) {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 30;
|
||||
if (progress >= 100) {
|
||||
progress = 100;
|
||||
clearInterval(interval);
|
||||
// 上传文件到服务器
|
||||
async function uploadFileToServer(id: string, file: File) {
|
||||
try {
|
||||
const uploadResult = await chatApi.uploadFile(file);
|
||||
|
||||
const attachment = attachments.value.find((a) => a.id === id);
|
||||
if (attachment) {
|
||||
// 替换本地预览URL为服务器返回的真实URL
|
||||
attachment.url = uploadResult.url;
|
||||
attachment.uploading = false;
|
||||
attachment.progress = 100;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
|
||||
const attachment = attachments.value.find((a) => a.id === id);
|
||||
if (attachment) {
|
||||
attachment.uploading = false;
|
||||
attachment.progress = 100;
|
||||
// 设置错误状态或者移除附件
|
||||
removeAttachment(id);
|
||||
}
|
||||
} else {
|
||||
const attachment = attachments.value.find((a) => a.id === id);
|
||||
if (attachment) {
|
||||
attachment.progress = progress;
|
||||
|
||||
// 显示错误提示
|
||||
window.$toast && window.$toast('文件上传失败,请重试', 'error');
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// 移除附件
|
||||
function removeAttachment(id: string) {
|
||||
|
|
|
|||
|
|
@ -89,12 +89,34 @@ class ChatApi {
|
|||
request: ChatRequest,
|
||||
signal?: AbortSignal,
|
||||
): 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 兼容的规范请求体
|
||||
const openAiRequest = {
|
||||
model: request.model || "qwen-plus",
|
||||
model: request.model || "qwen-plus", // 可能需要指定支持视觉的模型
|
||||
messages: [
|
||||
{ role: "system", content: request.systemPrompt || "你是一个有用的助手。" },
|
||||
{ role: "user", content: request.message }
|
||||
{ role: "system", content: request.systemPrompt || "你是一个支持视觉理解的助手。" },
|
||||
{
|
||||
role: "user",
|
||||
content: userContent
|
||||
}
|
||||
],
|
||||
stream: true,
|
||||
temperature: request.temperature,
|
||||
|
|
@ -158,12 +180,36 @@ class ChatApi {
|
|||
* 非流式对话
|
||||
*/
|
||||
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}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue