mirror of
https://github.com/linshenkx/prompt-optimizer.git
synced 2026-05-06 21:50:27 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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由前端同源请求处理,无需额外设置
|
||||
|
||||
|
||||
@@ -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代理服务**的简化架构,项目在所有部署环境中提供了一致的跨域解决方案,既满足了功能需求,又避免了过度工程化,是一个务实且可维护的选择。
|
||||
|
||||
@@ -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集成
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
**目标**:完善错误处理和用户提示
|
||||
|
||||
**具体任务**:
|
||||
- [ ] 优化nginx错误响应格式(可选,当前未实施)
|
||||
- [ ] 优化nginx错误响应格式
|
||||
- [ ] 添加前端错误处理逻辑
|
||||
- [ ] 实现用户友好的错误提示
|
||||
- [ ] 添加调试信息(开发环境)
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
// 获取模型列表
|
||||
|
||||
Reference in New Issue
Block a user