feat(proxy): 优化 Docker 代理功能并完善错误处理 (develop)

本次提交主要对 Docker 代理功能进行了优化,并完善了相关的错误处理逻辑。具体内容包括:

- 精确匹配 API 代理路径,使用 `location =` 确保 Nginx 配置中的代理路径精确匹配,避免被其他正则匹配拦截。
- 添加对本地主机 URL 的自动转换支持,在 Docker 容器内部访问宿主机时,自动将 `localhost`、`127.0.0.1` 等地址转换为 `host.docker.internal`。
- 优化 ModelManager 组件,将 `useDockerProxy` 选项添加到模型配置中,调整 API URL 和 API Key 的输入位置,增强用户体验。
- 完善错误处理逻辑,为 Node Proxy 服务增加更详细的错误处理,并提供统一的 JSON 错误响应格式。
- 在开发环境的 `docker-compose.dev.yml` 文件中添加 `host.docker.internal` 映射,允许容器访问宿主机。
- 更新了相关文档,包括架构对比、实施计划和设计文档,详细描述了 Docker 代理功能的实现原理和使用方法。
- 调整了文档的分类和归档方式,将经验记录归档到 archives 目录。
This commit is contained in:
linshen
2025-08-17 10:37:12 +08:00
parent 8ac9665781
commit 80977ec8a1
9 changed files with 81 additions and 39 deletions

View File

@@ -28,6 +28,9 @@ COPY --from=build /app/packages/mcp-server/dist /app/mcp-server/dist
COPY --from=build /app/packages/mcp-server/package.json /app/mcp-server/
COPY --from=build /app/packages/mcp-server/preload-env.js /app/mcp-server/
COPY --from=build /app/packages/mcp-server/preload-env.cjs /app/mcp-server/
# 复制Node Proxy服务
COPY --from=build /app/node-proxy /app/node-proxy
# 复制构建后的包到正确位置
COPY --from=build /app/packages /app/packages
# 复制必要的node_modules

View File

@@ -10,6 +10,8 @@ services:
restart: unless-stopped
ports:
- "8082:${NGINX_PORT:-80}" # Web应用端口包含MCP服务器通过/mcp路径访问
extra_hosts:
- "host.docker.internal:host-gateway" # 允许容器访问宿主机
env_file:
- .env.local # 读取本地环境变量文件
healthcheck:

View File

@@ -57,8 +57,8 @@ server {
}
}
# API代理服务转发到Node Proxy
location /api/proxy {
# API代理服务转发到Node Proxy(精确匹配,避免被正则匹配拦截)
location = /api/proxy {
# 本地转发到Node Proxy避免nginx动态代理复杂性
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
@@ -71,8 +71,8 @@ server {
# CORS由Node Proxy统一处理避免重复头
}
# 流式API代理转发到Node Proxy
location /api/stream {
# 流式API代理转发到Node Proxy(精确匹配,避免被正则匹配拦截)
location = /api/stream {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
@@ -89,8 +89,8 @@ server {
# CORS由Node Proxy统一处理避免重复头
}
# Docker环境状态检测
location /api/docker-status {
# Docker环境状态检测(精确匹配,避免被正则匹配拦截)
location = /api/docker-status {
add_header Content-Type 'application/json';
# CORS由前端同源请求处理无需额外设置

View File

@@ -89,7 +89,7 @@ Docker容器
**代理实现**
- nginx简单的proxy_pass配置
- Node.js处理所有代理逻辑
- 最简CORS配置Allow-Origin: *,由 Node Proxy 设置
- 最简CORS配置Allow-Origin: *
## 🔄 代理流程对比
@@ -201,6 +201,21 @@ if (isStream && upstream.body) {
- ✅ 透明的用户体验
- ✅ 相同的API接口
## 📝 总结
### 简化方案的优势
Docker代理功能采用**简化优先**的设计理念,实现了:
1. **架构统一性**:三种部署方式都有一致的代理解决方案
2. **实现简洁性**避免复杂的nginx动态代理配置
3. **维护便利性**零依赖Node.js实现配置简单
4. **功能完整性**:支持普通请求和流式响应代理
### 实施价值
通过**nginx本地转发 + Node.js代理服务**的简化架构,项目在所有部署环境中提供了一致的跨域解决方案,既满足了功能需求,又避免了过度工程化,是一个务实且可维护的选择。
- SSE 流式透传WebStream → Node 流)
- OPTIONS 预检204
- 错误:统一 JSON{ error: string }
## 🔄 从旧方案迁移Nginx 动态代理/DNS → Node Proxy
@@ -221,3 +236,15 @@ if (isStream && upstream.body) {
- 无需改 Nginx新增/变更上游 API 无需修改 Nginx只需通过 targetUrl 传入
- 受限环境:若需要对可访问域名做控制,建议在 Node Proxy 层实现白名单/黑名单与审计(可选增强)
## 📝 总结
### 简化方案的优势
Docker代理功能采用**简化优先**的设计理念,实现了:
1. **架构统一性**:三种部署方式都有一致的代理解决方案
2. **实现简洁性**避免复杂的nginx动态代理配置
3. **维护便利性**零依赖Node.js实现配置简单
4. **功能完整性**:支持普通请求和流式响应代理
### 实施价值
通过**nginx本地转发 + Node.js代理服务**的简化架构,项目在所有部署环境中提供了一致的跨域解决方案,既满足了功能需求,又避免了过度工程化,是一个务实且可维护的选择。

View File

@@ -138,17 +138,38 @@ export async function checkDockerApiAvailability(): Promise<boolean> {
#### 2.3 代理URL生成扩展
```typescript
/** 获取API代理URL支持Vercel和Docker环境 */
/**
* 获取API代理URL支持Vercel和Docker环境
*/
export const getProxyUrl = (baseURL: string | undefined, isStream: boolean = false): string => {
if (!baseURL) return '';
if (!baseURL) {
return '';
}
const origin = isBrowser() ? window.location.origin : '';
const proxyEndpoint = isStream ? 'stream' : 'proxy';
// 返回完整的绝对URL
return `${origin}/api/${proxyEndpoint}?targetUrl=${encodeURIComponent(baseURL)}`;
};
/** 当前实现由 core 包提供isVercel()/isDocker()/isProxyAvailable() */
```
/**
* 检查当前环境是否支持代理
*/
export const isProxyAvailable = (): boolean => {
// 可以是Vercel环境或Docker环境
return isVercel() || isDocker();
};
/**
* 检查是否在Docker环境中简化版
*/
export const isDocker = (): boolean => {
// 简化实现:可以通过检测特定的环境标识
// 或者与checkDockerApiAvailability结合使用
return false; // 具体实现根据需要调整
};
```
### 3. 前端UI集成

View File

@@ -105,25 +105,4 @@
1. **及时记录** - 遇到重要经验立即记录
2. **分类整理** - 按照上述分类组织内容
3. **定期回顾** - 每周回顾一次,提取可复用经验
4. **整合整理** - 任务完成时将相关经验合并到本文件
## 🧷 合并后的简版任务记录(来自原 scratchpad 与 todo
- 阶段1 基础代理功能Nginx 本地转发到 Node Proxy新增 /api/docker-statusNode Proxy 支持普通/流式、超时与 HEAD 处理;统一 JSON 错误与简洁日志
- 阶段2 流式与前端集成:关闭缓冲并添加 X-Accel-BufferingUI 增加“使用 Docker 代理”选项i18n 文案;配置保存/加载与构建验证
- 阶段3 错误与体验优化基础错误分类400/504/500LLM 服务对接 Docker 代理OpenAI/Gemini端到端基础验证
## 🚀 快速使用与验证
- 前端:在模型配置中勾选“使用 Docker 代理”(仅在 Docker 环境检测通过时显示)
- 代理路径:/api/proxy 与 /api/stream → Nginx 本地转发 → Node Proxy(127.0.0.1:3001)
- 验证建议:
- 普通请求:浏览器或 curl 访问 /api/proxy?targetUrl=https://httpbin.org/get
- 流式请求:访问 /api/stream 并观察打字机/逐段输出(需目标 API 支持流式)
## 🔭 可选后续(保持简化,可不做)
- 将“连接失败/解析失败”归类为 502当前为 500
- 如需请求追踪:生成简单的 requestId 并写入日志
- 如需 Nginx JSON 错误:为 /api/* 增加 error_page 502/504 → JSON
4. **归档整理** - 任务完成时将相关经验归档到archives

View File

@@ -112,7 +112,7 @@
**目标**:完善错误处理和用户提示
**具体任务**
- [ ] 优化nginx错误响应格式(可选,当前未实施)
- [ ] 优化nginx错误响应格式
- [ ] 添加前端错误处理逻辑
- [ ] 实现用户友好的错误提示
- [ ] 添加调试信息(开发环境)

View File

@@ -41,16 +41,24 @@ const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url || '', 'http://localhost');
const isStream = url.pathname === '/api/stream';
const targetUrl = url.searchParams.get('targetUrl');
let targetUrl = url.searchParams.get('targetUrl');
// 验证targetUrl参数
if (!targetUrl) {
return json(res, 400, { error: 'Missing targetUrl parameter' });
}
// 验证targetUrl是否为有效URL
// 自动将本地主机地址替换为 host.docker.internal 以便在 Docker 容器内访问宿主机
// 同时验证 URL 的有效性
try {
new URL(targetUrl);
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' });
}

View File

@@ -886,7 +886,8 @@ const handleFetchEditingModels = async () => {
baseURL: baseURL,
apiKey: apiKey,
provider: editingModel.value.provider || 'custom',
useVercelProxy: editingModel.value.useVercelProxy
useVercelProxy: editingModel.value.useVercelProxy,
useDockerProxy: editingModel.value.useDockerProxy
};
// 确定要使用的 provider key使用原始key或临时key
@@ -947,7 +948,8 @@ const handleFetchNewModels = async () => {
baseURL: baseURL,
apiKey: apiKey,
provider: currentProviderType.value || 'custom',
useVercelProxy: newModel.value.useVercelProxy
useVercelProxy: newModel.value.useVercelProxy,
useDockerProxy: newModel.value.useDockerProxy
};
// 获取模型列表