Files
prompt-optimizer/node-proxy/server.js
T
linshen ba63eff9ad feat: 重构图像模型管理架构并优化用户界面
## 核心改进

### 🏗️ 架构重构
- 实现ImageModelManager组件分离设计,关注点明确分离
- 重构ModelManager.vue为统一模型管理入口,支持文本/图像双模式
- 采用组件复用和扩展性设计,易于添加新模型类型

### 🎨 界面优化
- 摒弃5步导航式编辑,改为一体化界面设计
- 优化ImageModelEditModal为单页滚动布局,提升操作效率
- 统一文本和图像模型管理的交互体验和视觉风格

### 🔌 适配器扩展
- 新增OpenRouter和SiliconFlow图像适配器支持
- 完善AbstractImageProviderAdapter抽象基类设计
- 优化适配器注册表和动态模型发现机制

### 💻 类型系统
- 完善ImageModelConfig和相关类型定义
- 增强TypeScript类型安全和智能提示
- 优化服务层接口设计和依赖注入架构

### 🧪 测试覆盖
- 新增图像适配器单元测试和集成测试
- 添加E2E验收测试保证功能完整性
- 增强连接测试和参数验证测试覆盖

### 📚 文档完善
- 新增图像模型管理架构设计文档
- 添加一体化界面改进方案文档
- 更新项目结构和开发指南

## 技术特点

- **SOLID原则**:清晰的单一职责和开闭扩展设计
- **组合模式**:ImageModelManager + ModelManager.vue 组合架构
- **响应式设计**:基于Vue 3 Composition API的状态管理
- **依赖注入**:松耦合的服务架构和组件通信

这次重构显著提升了图像模型管理的用户体验和代码可维护性,
为后续音频、视频等多模态功能扩展奠定了坚实基础。
2025-09-21 20:17:03 +08:00

217 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import http from 'node:http';
import { Readable } from 'node:stream';
const PORT = process.env.PORT || 3001;
const STREAM_TIMEOUT = parseInt(process.env.STREAM_TIMEOUT || '300000'); // 默认5分钟
const PROXY_TIMEOUT = parseInt(process.env.PROXY_TIMEOUT || '120000'); // 默认2分钟
/**
* 统一JSON响应函数
*/
function json(res, code, obj) {
res.statusCode = code;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(obj));
}
/**
* 增强的请求日志
*/
function logRequest(req, targetUrl, status, startTime, requestId) {
const duration = Date.now() - startTime;
const timestamp = new Date().toISOString();
const clientIP = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
console.log(`[${timestamp}] [${requestId}] ${clientIP} ${req.method} ${targetUrl} -> ${status} (${duration}ms)`);
}
/**
* 生成简单的请求ID
*/
function generateRequestId() {
return Math.random().toString(36).substr(2, 9);
}
/**
* 主要的代理服务器
*/
const server = http.createServer(async (req, res) => {
const startTime = Date.now();
const requestId = generateRequestId();
try {
const url = new URL(req.url || '', 'http://localhost');
const isStream = url.pathname === '/api/stream';
let targetUrl = url.searchParams.get('targetUrl');
// 验证targetUrl参数
if (!targetUrl) {
return json(res, 400, { error: 'Missing targetUrl parameter' });
}
// 自动将本地主机地址替换为 host.docker.internal 以便在 Docker 容器内访问宿主机
// 同时验证 URL 的有效性
try {
const targetUrlObject = new URL(targetUrl);
const localhostNames = ['localhost', '127.0.0.1', '[::1]'];
if (localhostNames.includes(targetUrlObject.hostname)) {
const originalHostname = targetUrlObject.hostname;
targetUrlObject.hostname = 'host.docker.internal';
targetUrl = targetUrlObject.toString();
console.log(`[${new Date().toISOString()}] [${requestId}] Remapped localhost URL from ${originalHostname} to host.docker.internal. New target: ${targetUrl}`);
}
} catch {
return json(res, 400, { error: 'Invalid targetUrl parameter' });
}
// 复制请求头(排除会引发问题的头),强制使用 identity 编码,避免上游压缩
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
const lower = k.toLowerCase();
if (!['host', 'connection', 'content-length', 'accept-encoding'].includes(lower) && v) {
headers.set(k, Array.isArray(v) ? v.join(',') : String(v));
}
}
headers.set('accept-encoding', 'identity');
// 读取请求体(仅非GET/HEAD)
let body = undefined;
if (!['GET', 'HEAD'].includes(req.method || 'GET')) {
body = await new Promise((resolve) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
});
}
// 发起上游请求(添加超时防护:流式和普通请求不同超时)
const controller = new AbortController();
const timeoutMs = isStream ? STREAM_TIMEOUT : PROXY_TIMEOUT;
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
let upstream;
try {
upstream = await fetch(targetUrl, {
method: req.method,
headers,
body,
signal: controller.signal
});
} finally {
// 确保定时器总是被清理,防止内存泄漏
clearTimeout(timeoutId);
}
// 设置CORS头(简化版本)
const origin = req.headers['origin'];
if (origin && typeof origin === 'string') {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-KEY');
// 处理OPTIONS预检请求
if (req.method === 'OPTIONS') {
res.statusCode = 204;
logRequest(req, targetUrl, 204, startTime, requestId);
return res.end();
}
// 透传状态码和“安全响应头”
res.statusCode = upstream.status;
res.statusMessage = upstream.statusText;
const skipRespHeaders = new Set([
'access-control-allow-origin',
'access-control-allow-methods',
'access-control-allow-headers',
'connection',
'keep-alive',
'transfer-encoding',
'content-encoding',
'content-length'
]);
upstream.headers.forEach((val, key) => {
if (!skipRespHeaders.has(key.toLowerCase())) {
res.setHeader(key, val);
}
});
// HEAD请求特殊处理(只返回头部,不处理响应体)
if (req.method === 'HEAD') {
logRequest(req, targetUrl, upstream.status, startTime, requestId);
return res.end();
}
// 处理响应体
const finishLog = () => logRequest(req, targetUrl, upstream.status, startTime, requestId);
if (upstream.body) {
// 统一透传上游流,避免长度/编码不一致问题
Readable.fromWeb(upstream.body).once('end', finishLog).pipe(res);
} else {
// 无 body(如 HEAD),直接结束
finishLog();
res.end();
}
} catch (error) {
// 增强的错误处理
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] [${requestId}] Proxy error:`, error);
logRequest(req, req.url || 'unknown', 'ERROR', startTime, requestId);
if (!res.headersSent) {
// 区分不同类型的错误并提供友好消息
let statusCode = 500;
let errorMessage = 'Internal server error';
if (error.name === 'AbortError' || error.message.includes('aborted')) {
statusCode = 504;
errorMessage = 'Request timeout - the target server took too long to respond';
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
statusCode = 502;
errorMessage = 'Bad gateway - unable to connect to target server';
} else if (error.code === 'ECONNRESET') {
statusCode = 502;
errorMessage = 'Connection reset by target server';
} else if (error instanceof TypeError && error.message.includes('fetch')) {
statusCode = 400;
errorMessage = 'Invalid target URL or request format';
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
json(res, statusCode, {
error: errorMessage,
...(process.env.NODE_ENV === 'development' && {
details: error.stack,
requestId
})
});
}
}
});
// 启动服务器
server.listen(PORT, '127.0.0.1', () => {
console.log(`[${new Date().toISOString()}] Node Proxy server listening on 127.0.0.1:${PORT}`);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log(`[${new Date().toISOString()}] Received SIGTERM, shutting down gracefully`);
server.close(() => {
console.log(`[${new Date().toISOString()}] Node Proxy server closed`);
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log(`[${new Date().toISOString()}] Received SIGINT, shutting down gracefully`);
server.close(() => {
console.log(`[${new Date().toISOString()}] Node Proxy server closed`);
process.exit(0);
});
});