Spaces:
Paused
Paused
| import express from 'express'; | |
| import dotenv from 'dotenv'; | |
| import { randomUUID } from 'crypto'; | |
| import { fileURLToPath } from 'url'; | |
| import { dirname, join } from 'path'; | |
| import chalk from 'chalk'; | |
| import { | |
| ChatMessage, ChatCompletionRequest, Choice, ChoiceDelta, ChatCompletionChunk | |
| } from './models.js'; | |
| import { | |
| initialize, | |
| buildNotionRequest, | |
| streamNotionResponse, | |
| INITIALIZED_SUCCESSFULLY | |
| } from './lightweight-client.js'; | |
| import { cookieManager } from './CookieManager.js'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = dirname(__filename); | |
| dotenv.config({ path: join(dirname(__dirname), '.env') }); | |
| const logger = { | |
| info: (message) => console.log(chalk.blue(`[info] ${message}`)), | |
| error: (message) => console.error(chalk.red(`[error] ${message}`)), | |
| warning: (message) => console.warn(chalk.yellow(`[warn] ${message}`)), | |
| success: (message) => console.log(chalk.green(`[success] ${message}`)), | |
| request: (method, path, status, time) => { | |
| const statusColor = status >= 500 ? chalk.red : | |
| status >= 400 ? chalk.yellow : | |
| status >= 300 ? chalk.cyan : | |
| status >= 200 ? chalk.green : chalk.white; | |
| console.log(`${chalk.magenta(`[${method}]`)} - ${path} ${statusColor(status)} ${chalk.gray(`${time}ms`)}`); | |
| } | |
| }; | |
| const EXPECTED_TOKEN = process.env.PROXY_AUTH_TOKEN || "default_token"; | |
| const app = express(); | |
| app.use(express.json({ limit: '50mb' })); | |
| app.use(express.urlencoded({ extended: true, limit: '50mb' })); | |
| // 请求计数器和内存监控 | |
| let requestCount = 0; | |
| let lastResetTime = Date.now(); | |
| const startTime = Date.now(); | |
| const MAX_UPTIME = 2 * 60 * 60 * 1000; // 2小时 | |
| const MAX_MEMORY = 400 * 1024 * 1024; // 400MB | |
| // 请求日志中间件 | |
| app.use((req, res, next) => { | |
| const start = Date.now(); | |
| const originalEnd = res.end; | |
| res.end = function(...args) { | |
| const duration = Date.now() - start; | |
| logger.request(req.method, req.path, res.statusCode, duration); | |
| return originalEnd.apply(this, args); | |
| }; | |
| next(); | |
| }); | |
| // 请求频率限制中间件 | |
| app.use('/v1/chat/completions', (req, res, next) => { | |
| const now = Date.now(); | |
| // 每分钟重置计数 | |
| if (now - lastResetTime > 60000) { | |
| requestCount = 0; | |
| lastResetTime = now; | |
| } | |
| requestCount++; | |
| // 限制每分钟最多15个请求 | |
| if (requestCount > 15) { | |
| logger.warning(`请求频率过高,当前: ${requestCount}/分钟`); | |
| return res.status(429).json({ | |
| error: { | |
| message: "Too many requests, please try again later", | |
| type: "rate_limit_error" | |
| } | |
| }); | |
| } | |
| next(); | |
| }); | |
| // 认证中间件 | |
| function authenticate(req, res, next) { | |
| const authHeader = req.headers.authorization; | |
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | |
| return res.status(401).json({ | |
| error: { | |
| message: "Authentication required. Please provide a valid Bearer token.", | |
| type: "authentication_error" | |
| } | |
| }); | |
| } | |
| const token = authHeader.split(' ')[1]; | |
| if (token !== EXPECTED_TOKEN) { | |
| return res.status(401).json({ | |
| error: { | |
| message: "Invalid authentication credentials", | |
| type: "authentication_error" | |
| } | |
| }); | |
| } | |
| next(); | |
| } | |
| // 根路径 | |
| app.get('/', (req, res) => { | |
| const memUsage = process.memoryUsage(); | |
| const uptime = Date.now() - startTime; | |
| res.json({ | |
| message: "Notion2API NodeJS Service", | |
| version: "1.0.0", | |
| status: "running", | |
| uptime: Math.floor(uptime / 1000), | |
| memory: { | |
| used: Math.round(memUsage.heapUsed / 1024 / 1024), | |
| total: Math.round(memUsage.heapTotal / 1024 / 1024) | |
| }, | |
| endpoints: { | |
| health: "/health", | |
| models: "/v1/models", | |
| chat: "/v1/chat/completions" | |
| } | |
| }); | |
| }); | |
| // 获取模型列表 - 添加了所有原始项目中的模型 | |
| app.get('/v1/models', authenticate, (req, res) => { | |
| const modelList = { | |
| object: "list", | |
| data: [ | |
| { | |
| id: "openai-gpt-4.1", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| }, | |
| { | |
| id: "anthropic-opus-4", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| }, | |
| { | |
| id: "anthropic-sonnet-4", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| }, | |
| { | |
| id: "anthropic-sonnet-3.x-stable", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| }, | |
| { | |
| id: "google-gemini-2.5-pro", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| }, | |
| { | |
| id: "google-gemini-2.5-flash", | |
| object: "model", | |
| created: Math.floor(Date.now() / 1000), | |
| owned_by: "notion" | |
| } | |
| ] | |
| }; | |
| res.json(modelList); | |
| }); | |
| // 聊天完成端点 | |
| app.post('/v1/chat/completions', authenticate, async (req, res) => { | |
| try { | |
| if (!INITIALIZED_SUCCESSFULLY) { | |
| return res.status(500).json({ | |
| error: { | |
| message: "系统未成功初始化。请检查您的NOTION_COOKIE是否有效。", | |
| type: "server_error" | |
| } | |
| }); | |
| } | |
| if (cookieManager.getValidCount() === 0) { | |
| return res.status(500).json({ | |
| error: { | |
| message: "没有可用的有效cookie。请检查您的NOTION_COOKIE配置。", | |
| type: "server_error" | |
| } | |
| }); | |
| } | |
| const requestData = req.body; | |
| if (!requestData.messages || !Array.isArray(requestData.messages) || requestData.messages.length === 0) { | |
| return res.status(400).json({ | |
| error: { | |
| message: "Invalid request: 'messages' field must be a non-empty array.", | |
| type: "invalid_request_error" | |
| } | |
| }); | |
| } | |
| if (!requestData.model) { | |
| requestData.model = "anthropic-sonnet-4"; | |
| } | |
| const notionRequestBody = buildNotionRequest(requestData); | |
| if (requestData.stream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| res.setHeader('Access-Control-Allow-Headers', 'Cache-Control'); | |
| logger.info(`开始流式响应`); | |
| const stream = await streamNotionResponse(notionRequestBody); | |
| stream.pipe(res); | |
| req.on('close', () => { | |
| logger.info('客户端断开连接,结束流'); | |
| stream.destroy(); | |
| }); | |
| req.on('aborted', () => { | |
| logger.info('请求被中止,结束流'); | |
| stream.destroy(); | |
| }); | |
| } else { | |
| logger.info(`开始非流式响应`); | |
| const chunks = []; | |
| const stream = await streamNotionResponse(notionRequestBody); | |
| return new Promise((resolve, reject) => { | |
| stream.on('data', (chunk) => { | |
| const chunkStr = chunk.toString(); | |
| if (chunkStr.startsWith('data: ') && !chunkStr.includes('[DONE]')) { | |
| try { | |
| const dataJson = chunkStr.substring(6).trim(); | |
| if (dataJson) { | |
| const chunkData = JSON.parse(dataJson); | |
| if (chunkData.choices && chunkData.choices[0].delta && chunkData.choices[0].delta.content) { | |
| chunks.push(chunkData.choices[0].delta.content); | |
| } | |
| } | |
| } catch (error) { | |
| logger.error(`解析非流式响应块时出错: ${error}`); | |
| } | |
| } | |
| }); | |
| stream.on('end', () => { | |
| const fullResponse = { | |
| id: `chatcmpl-${randomUUID()}`, | |
| object: "chat.completion", | |
| created: Math.floor(Date.now() / 1000), | |
| model: requestData.model, | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { | |
| role: "assistant", | |
| content: chunks.join('') | |
| }, | |
| finish_reason: "stop" | |
| } | |
| ], | |
| usage: { | |
| prompt_tokens: null, | |
| completion_tokens: null, | |
| total_tokens: null | |
| } | |
| }; | |
| res.json(fullResponse); | |
| resolve(); | |
| }); | |
| stream.on('error', (error) => { | |
| logger.error(`非流式响应出错: ${error}`); | |
| if (!res.headersSent) { | |
| res.status(500).json({ | |
| error: { | |
| message: `Stream error: ${error.message}`, | |
| type: "server_error" | |
| } | |
| }); | |
| } | |
| reject(error); | |
| }); | |
| }); | |
| } | |
| } catch (error) { | |
| logger.error(`聊天完成端点错误: ${error}`); | |
| if (!res.headersSent) { | |
| res.status(500).json({ | |
| error: { | |
| message: `Internal server error: ${error.message}`, | |
| type: "server_error" | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| // 增强的健康检查端点 | |
| app.get('/health', (req, res) => { | |
| const memUsage = process.memoryUsage(); | |
| const memUsageMB = { | |
| rss: Math.round(memUsage.rss / 1024 / 1024), | |
| heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), | |
| heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), | |
| external: Math.round(memUsage.external / 1024 / 1024) | |
| }; | |
| const uptime = Date.now() - startTime; | |
| const status = memUsage.heapUsed > MAX_MEMORY ? 'warning' : 'ok'; | |
| res.json({ | |
| status: status, | |
| timestamp: new Date().toISOString(), | |
| initialized: INITIALIZED_SUCCESSFULLY, | |
| valid_cookies: cookieManager.getValidCount(), | |
| uptime: Math.floor(uptime / 1000), | |
| memory: memUsageMB, | |
| requests_per_minute: requestCount, | |
| version: "1.0.0" | |
| }); | |
| }); | |
| // Cookie状态查询端点 | |
| app.get('/cookies/status', authenticate, (req, res) => { | |
| res.json({ | |
| total_cookies: cookieManager.getValidCount(), | |
| cookies: cookieManager.getStatus(), | |
| initialized: cookieManager.initialized | |
| }); | |
| }); | |
| // 内存监控和自动重启 | |
| setInterval(() => { | |
| const uptime = Date.now() - startTime; | |
| const memUsage = process.memoryUsage(); | |
| // 记录内存使用情况 | |
| if (memUsage.heapUsed > 300 * 1024 * 1024) { // 300MB警告 | |
| logger.warning(`内存使用较高: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`); | |
| } | |
| // 检查是否需要重启 | |
| if (uptime > MAX_UPTIME || memUsage.heapUsed > MAX_MEMORY) { | |
| logger.warning('达到重启条件:'); | |
| logger.warning(`运行时间: ${Math.floor(uptime / 1000 / 60)}分钟 (最大: ${Math.floor(MAX_UPTIME / 1000 / 60)}分钟)`); | |
| logger.warning(`内存使用: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB (最大: ${Math.round(MAX_MEMORY / 1024 / 1024)}MB)`); | |
| logger.warning('正在重启应用以释放内存...'); | |
| // 优雅关闭 | |
| setTimeout(() => { | |
| process.exit(0); // Hugging Face 会自动重启 | |
| }, 1000); | |
| } | |
| }, 5 * 60 * 1000); // 每5分钟检查一次 | |
| // 错误处理中间件 | |
| app.use((error, req, res, next) => { | |
| logger.error(`未处理的错误: ${error.message}`); | |
| if (!res.headersSent) { | |
| res.status(500).json({ | |
| error: { | |
| message: "Internal server error", | |
| type: "server_error" | |
| } | |
| }); | |
| } | |
| }); | |
| // 404处理 | |
| app.use((req, res) => { | |
| res.status(404).json({ | |
| error: { | |
| message: `Not found: ${req.method} ${req.path}`, | |
| type: "not_found_error" | |
| } | |
| }); | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| // 初始化并启动服务器 | |
| initialize().then(() => { | |
| app.listen(PORT, '0.0.0.0', () => { | |
| logger.info(`服务已启动 - 端口: ${PORT}`); | |
| logger.info(`访问地址: http://0.0.0.0:${PORT}`); | |
| logger.info(`环境: ${process.env.NODE_ENV || 'development'}`); | |
| if (INITIALIZED_SUCCESSFULLY) { | |
| logger.success(`系统初始化状态: ✅`); | |
| logger.success(`可用cookie数量: ${cookieManager.getValidCount()}`); | |
| } else { | |
| logger.warning(`系统初始化状态: ❌`); | |
| logger.warning(`警告: 系统未成功初始化,API调用将无法正常工作`); | |
| logger.warning(`请检查COOKIE_FILE_CONTENT或NOTION_COOKIE配置是否有效`); | |
| } | |
| }); | |
| }).catch((error) => { | |
| logger.error(`初始化失败: ${error}`); | |
| process.exit(1); | |
| }); | |
| // 优雅关闭处理 | |
| process.on('SIGTERM', () => { | |
| logger.info('收到SIGTERM信号,正在优雅关闭...'); | |
| process.exit(0); | |
| }); | |
| process.on('SIGINT', () => { | |
| logger.info('收到SIGINT信号,正在优雅关闭...'); | |
| process.exit(0); | |
| }); | |