Spaces:
Sleeping
Sleeping
File size: 8,138 Bytes
c6dedd5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | /**
* Cursor2API v2 - 入口
*
* 将 Cursor 文档页免费 AI 接口代理为 Anthropic Messages API
* 通过提示词注入让 Claude Code 拥有完整工具调用能力
*/
import 'dotenv/config';
import { createRequire } from 'module';
import express from 'express';
import { getConfig, initConfigWatcher, stopConfigWatcher } from './config.js';
import { handleMessages, listModels, countTokens } from './handler.js';
import { handleOpenAIChatCompletions, handleOpenAIResponses } from './openai-handler.js';
import { serveLogViewer, apiGetLogs, apiGetRequests, apiGetStats, apiGetPayload, apiLogsStream, serveLogViewerLogin, apiClearLogs, serveVueApp } from './log-viewer.js';
import { apiGetConfig, apiSaveConfig } from './config-api.js';
import { loadLogsFromFiles } from './logger.js';
// 从 package.json 读取版本号,统一来源,避免多处硬编码
const require = createRequire(import.meta.url);
const { version: VERSION } = require('../package.json') as { version: string };
const app = express();
const config = getConfig();
// 解析 JSON body(增大限制以支持 base64 图片,单张图片可达 10MB+)
app.use(express.json({ limit: '50mb' }));
// CORS
app.use((_req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', '*');
if (_req.method === 'OPTIONS') {
res.sendStatus(200);
return;
}
next();
});
// ★ 静态文件路由(无需鉴权,CSS/JS 等)
app.use('/public', express.static('public'));
// ★ 日志查看器鉴权中间件:配置了 authTokens 时需要验证
const logViewerAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
const tokens = getConfig().authTokens;
if (!tokens || tokens.length === 0) return next(); // 未配置 token 则放行
// 支持多种传入方式: query ?token=xxx, Authorization header, x-api-key header
const tokenFromQuery = req.query.token as string | undefined;
const authHeader = req.headers['authorization'] || req.headers['x-api-key'];
const tokenFromHeader = authHeader ? String(authHeader).replace(/^Bearer\s+/i, '').trim() : undefined;
const token = tokenFromQuery || tokenFromHeader;
if (!token || !tokens.includes(token)) {
// HTML 页面请求 → 返回登录页; API 请求 → 返回 JSON 错误
if (req.path === '/logs') {
return serveLogViewerLogin(req, res);
}
res.status(401).json({ error: { message: 'Unauthorized. Provide token via ?token=xxx or Authorization header.', type: 'auth_error' } });
return;
}
next();
};
// ★ 日志查看器路由(带鉴权)
app.get('/logs', logViewerAuth, serveLogViewer);
// Vue3 日志 UI(无服务端鉴权,由 Vue 应用内部处理)
app.get('/vuelogs', serveVueApp);
app.get('/api/logs', logViewerAuth, apiGetLogs);
app.get('/api/requests', logViewerAuth, apiGetRequests);
app.get('/api/stats', logViewerAuth, apiGetStats);
app.get('/api/payload/:requestId', logViewerAuth, apiGetPayload);
app.get('/api/logs/stream', logViewerAuth, apiLogsStream);
app.post('/api/logs/clear', logViewerAuth, apiClearLogs);
app.get('/api/config', logViewerAuth, apiGetConfig);
app.post('/api/config', logViewerAuth, apiSaveConfig);
// ★ API 鉴权中间件:配置了 authTokens 则需要 Bearer token
app.use((req, res, next) => {
// 跳过无需鉴权的路径
if (req.method === 'GET' || req.path === '/health') {
return next();
}
const tokens = getConfig().authTokens;
if (!tokens || tokens.length === 0) {
return next(); // 未配置 token 则全部放行
}
const authHeader = req.headers['authorization'] || req.headers['x-api-key'];
if (!authHeader) {
res.status(401).json({ error: { message: 'Missing authentication token. Use Authorization: Bearer <token>', type: 'auth_error' } });
return;
}
const token = String(authHeader).replace(/^Bearer\s+/i, '').trim();
if (!tokens.includes(token)) {
console.log(`[Auth] 拒绝无效 token: ${token.substring(0, 8)}...`);
res.status(403).json({ error: { message: 'Invalid authentication token', type: 'auth_error' } });
return;
}
next();
});
// ==================== 路由 ====================
// Anthropic Messages API
app.post('/v1/messages', handleMessages);
app.post('/messages', handleMessages);
// OpenAI Chat Completions API(兼容)
app.post('/v1/chat/completions', handleOpenAIChatCompletions);
app.post('/chat/completions', handleOpenAIChatCompletions);
// OpenAI Responses API(Cursor IDE Agent 模式)
app.post('/v1/responses', handleOpenAIResponses);
app.post('/responses', handleOpenAIResponses);
// Token 计数
app.post('/v1/messages/count_tokens', countTokens);
app.post('/messages/count_tokens', countTokens);
// OpenAI 兼容模型列表
app.get('/v1/models', listModels);
// 健康检查
app.get('/health', (_req, res) => {
res.json({ status: 'ok', version: VERSION });
});
// 根路径
app.get('/', (_req, res) => {
res.json({
name: 'cursor2api',
version: VERSION,
description: 'Cursor Docs AI → Anthropic & OpenAI & Cursor IDE API Proxy',
endpoints: {
anthropic_messages: 'POST /v1/messages',
openai_chat: 'POST /v1/chat/completions',
openai_responses: 'POST /v1/responses',
models: 'GET /v1/models',
health: 'GET /health',
log_viewer: 'GET /logs',
log_viewer_vue: 'GET /vuelogs',
},
usage: {
claude_code: 'export ANTHROPIC_BASE_URL=http://localhost:' + config.port,
openai_compatible: 'OPENAI_BASE_URL=http://localhost:' + config.port + '/v1',
cursor_ide: 'OPENAI_BASE_URL=http://localhost:' + config.port + '/v1 (选用 Claude 模型)',
},
});
});
// ==================== 启动 ====================
// ★ 从日志文件加载历史(必须在 listen 之前)
loadLogsFromFiles();
app.listen(config.port, () => {
const auth = config.authTokens?.length ? `${config.authTokens.length} token(s)` : 'open';
const logPersist = config.logging?.file_enabled
? `file(${config.logging.persist_mode || 'summary'}) → ${config.logging.dir}`
: 'memory only';
// Tools 配置摘要
const toolsCfg = config.tools;
let toolsInfo = 'default (full, desc=full)';
if (toolsCfg) {
if (toolsCfg.disabled) {
toolsInfo = '\x1b[33mdisabled\x1b[0m (不注入工具定义,节省上下文)';
} else if (toolsCfg.passthrough) {
toolsInfo = '\x1b[36mpassthrough\x1b[0m (原始 JSON 嵌入)';
} else {
const parts: string[] = [];
parts.push(`schema=${toolsCfg.schemaMode}`);
parts.push(toolsCfg.descriptionMaxLength === 0 ? 'desc=full' : `desc≤${toolsCfg.descriptionMaxLength}`);
if (toolsCfg.includeOnly?.length) parts.push(`whitelist=${toolsCfg.includeOnly.length}`);
if (toolsCfg.exclude?.length) parts.push(`blacklist=${toolsCfg.exclude.length}`);
toolsInfo = parts.join(', ');
}
}
console.log('');
console.log(` \x1b[36m⚡ Cursor2API v${VERSION}\x1b[0m`);
console.log(` ├─ Server: \x1b[32mhttp://localhost:${config.port}\x1b[0m`);
console.log(` ├─ Model: ${config.cursorModel}`);
console.log(` ├─ Auth: ${auth}`);
console.log(` ├─ Tools: ${toolsInfo}`);
console.log(` ├─ Logging: ${logPersist}`);
console.log(` └─ Logs: \x1b[35mhttp://localhost:${config.port}/logs\x1b[0m`);
console.log(` └─ Logs Vue3: \x1b[35mhttp://localhost:${config.port}/vuelogs\x1b[0m`);
console.log('');
// ★ 启动 config.yaml 热重载监听
initConfigWatcher();
});
// ★ 优雅关闭:停止文件监听
process.on('SIGTERM', () => {
stopConfigWatcher();
process.exit(0);
});
process.on('SIGINT', () => {
stopConfigWatcher();
process.exit(0);
});
|