diff --git a/Dockerfile b/Dockerfile index e4e42318..d1ba3436 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f9fd9a27..a09f4ea4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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: diff --git a/docker/nginx.conf b/docker/nginx.conf index 44b5818d..66745055 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -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由前端同源请求处理,无需额外设置 diff --git a/docs/workspace/architecture-comparison.md b/docs/workspace/architecture-comparison.md index acb463b8..3722c909 100644 --- a/docs/workspace/architecture-comparison.md +++ b/docs/workspace/architecture-comparison.md @@ -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代理服务**的简化架构,项目在所有部署环境中提供了一致的跨域解决方案,既满足了功能需求,又避免了过度工程化,是一个务实且可维护的选择。 diff --git a/docs/workspace/design.md b/docs/workspace/design.md index 4e079fee..35797475 100644 --- a/docs/workspace/design.md +++ b/docs/workspace/design.md @@ -138,17 +138,38 @@ export async function checkDockerApiAvailability(): Promise { #### 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集成 diff --git a/docs/workspace/experience.md b/docs/workspace/experience.md index e27a634f..49cba4ed 100644 --- a/docs/workspace/experience.md +++ b/docs/workspace/experience.md @@ -105,25 +105,4 @@ 1. **及时记录** - 遇到重要经验立即记录 2. **分类整理** - 按照上述分类组织内容 3. **定期回顾** - 每周回顾一次,提取可复用经验 -4. **整合整理** - 任务完成时将相关经验合并到本文件 - - -## 🧷 合并后的简版任务记录(来自原 scratchpad 与 todo) - -- 阶段1 基础代理功能:Nginx 本地转发到 Node Proxy;新增 /api/docker-status;Node Proxy 支持普通/流式、超时与 HEAD 处理;统一 JSON 错误与简洁日志 -- 阶段2 流式与前端集成:关闭缓冲并添加 X-Accel-Buffering;UI 增加“使用 Docker 代理”选项;i18n 文案;配置保存/加载与构建验证 -- 阶段3 错误与体验优化:基础错误分类(400/504/500);LLM 服务对接 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 diff --git a/docs/workspace/implementation-plan.md b/docs/workspace/implementation-plan.md index 66051363..48b62a0d 100644 --- a/docs/workspace/implementation-plan.md +++ b/docs/workspace/implementation-plan.md @@ -112,7 +112,7 @@ **目标**:完善错误处理和用户提示 **具体任务**: -- [ ] 优化nginx错误响应格式(可选,当前未实施) +- [ ] 优化nginx错误响应格式 - [ ] 添加前端错误处理逻辑 - [ ] 实现用户友好的错误提示 - [ ] 添加调试信息(开发环境) diff --git a/node-proxy/server.js b/node-proxy/server.js index 519d5157..94cf9400 100644 --- a/node-proxy/server.js +++ b/node-proxy/server.js @@ -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' }); } diff --git a/packages/ui/src/components/ModelManager.vue b/packages/ui/src/components/ModelManager.vue index 04327b42..ac5b9951 100644 --- a/packages/ui/src/components/ModelManager.vue +++ b/packages/ui/src/components/ModelManager.vue @@ -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 }; // 获取模型列表