7 Commits

Author SHA1 Message Date
D8D Developer
9e97c52f76 feat: 应用监听端口改为 80,添加 Spot 实例支持
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 2m28s
- 默认端口从 3000 改为 80
- Dockerfile 更新 EXPOSE 和健康检查端口
- 添加 ECI Spot 抢占式实例注解
- 移除 USER node 以支持监听 80 端口

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-20 13:12:28 +00:00
D8D Developer
e743fc9bcd fix: 使用 dotenv override 确保正确读取 .env 配置
- 添加 override: true 选项覆盖系统环境变量
- 确保 ANTHROPIC_BASE_URL 从项目 .env 文件读取

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 12:12:34 +00:00
D8D Developer
ce0b822b63 fix: 添加 Spot 抢占式实例注解和环境变量注入
- 添加 eci-spot-instance 注解启用抢占式实例
- 从 .env 读取 ANTHROPIC_BASE_URL 并注入容器环境变量

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 12:08:01 +00:00
D8D Developer
77965b2c9f fix: 使用 HTTP API 代理方式调用 K8S
- 重写 k8s-client.ts,改用 HTTP API 代理方式而非 SDK 直连
- @kubernetes/client-node 仅用于类型定义
- 修复部署功能,正确调用 K8S API 代理服务

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 12:01:19 +00:00
D8D Developer
35eb7f93eb fix: 添加 skipTLSVerify 支持 HTTP API
Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 11:44:25 +00:00
D8D Developer
d901603148 fix: 修复 deploy 脚本参数解析逻辑
Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 10:21:41 +00:00
D8D Developer
fec235eade feat: 添加 K8S 部署功能
- 新增 src/k8s-client.ts: K8S 客户端封装
- 新增 src/deploy.ts: 部署/删除/状态查询逻辑
- 新增 scripts/deploy.ts: 命令行部署工具
- 更新 .env.example: 添加 K8S 配置项
- 安装 @kubernetes/client-node@1.3.0 依赖

Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-20 10:19:37 +00:00
9 changed files with 1486 additions and 8 deletions

View File

@@ -7,3 +7,11 @@ ANTHROPIC_BASE_URL=https://api.anthropic.com
# 服务端口(默认 3000
PORT=3000
# K8S 部署配置
K8S_API_URL=http://10.0.0.52:23972
K8S_API_TOKEN=your_fixed_token_here
# 域名配置
DEV_HOST="*.dev.d8d.fun"
DEV_TLS_SECRET_NAME="dev-d8d-tls"

View File

@@ -46,14 +46,11 @@ RUN pnpm install --prod --frozen-lockfile || pnpm install --prod
COPY --from=builder /app/dist ./dist
# 暴露端口
EXPOSE 3000
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD node -e "require('http').get('http://localhost:80/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# 使用非 root 用户运行
USER node
# 启动命令
# 启动命令(监听 80 端口需要 root 权限)
CMD ["node", "dist/index.js"]

View File

@@ -0,0 +1,167 @@
---
title: 'K8S 部署脚本'
slug: 'k8s-deploy'
created: '2026-03-20T07:00:00.000Z'
status: 'ready-for-dev'
stepsCompleted: [1, 2, 3, 4]
tech_stack: ['TypeScript', '@kubernetes/client-node', 'Node.js', 'dotenv', 'express']
files_to_modify:
- 'package.json'
- 'scripts/deploy.ts'
code_patterns:
- 'K8S 客户端: @kubernetes/client-node SDK 封装'
- '部署流程: Deployment → Service → Ingress'
- 'EIP: k8s.aliyun.com/eci-open-enable-eip 自动创建'
- '抢占式实例: 未启用(可选优化)'
- '命名空间: d8d-proxy (专用命名空间)'
- '实例规格: ecs.t5-lc2m1.nano (完整字符串)'
- 'Ingress 域名: {name}-{port}.{hostSuffix}'
test_patterns: []
---
# Tech-Spec: K8S 部署脚本
**Created:** 2026-03-20T07:00:00.000Z
## Overview
### Problem Statement
Claude API 代理服务已完成,需要添加 K8S 部署脚本来动态创建和管理 Pod 实例。每个 Pod 需要独立的出口 IP (通过 EIP),以便分散请求绕过官方 API 的 IP 并发限制。
### Solution
1. 安装 `@kubernetes/client-node` 依赖
2. 使用已实现的 `src/k8s-client.ts` K8S 客户端封装
3. 使用已实现的 `src/deploy.ts` 部署模块
4. 创建 `scripts/deploy.ts` 命令行入口
### Scope
**In Scope:**
- 安装 @kubernetes/client-node 依赖
- 命令行部署工具
- 部署脚本:创建/删除 Deployment、Service、Ingress
- 自动 EIP 创建支持
- 专用命名空间 `d8d-proxy`
- 健康检查配置
**Out of Scope:**
- API Key 池管理(由 D8D 主项目处理)
- 复杂的监控和告警
- HPA 自动扩缩容
## Context for Development
### Codebase Patterns
**已实现的文件:**
| 文件 | 状态 | 描述 |
|------|------|------|
| `src/k8s-client.ts` | ✅ 已完成 | K8S SDK 封装类 |
| `src/deploy.ts` | ✅ 已完成 | 部署/删除/状态查询功能 |
| `.env.example` | ✅ 已完成 | K8S 环境变量配置 |
| `package.json` | ❌ 缺少依赖 | 需添加 @kubernetes/client-node |
| `scripts/deploy.ts` | ❌ 未创建 | 命令行入口 |
**阿里云 ECI 注解(已实现写法):**
- `k8s.aliyun.com/eci-use-specs: "ecs.t5-lc2m1.nano"` - 指定 ECI 规格
- `k8s.aliyun.com/eci-open-enable-eip: "true"` - 自动创建 EIP
- `k8s.aliyun.com/eci-eip-bandwidth: "5"` - EIP 带宽 5Mbps
- `k8s.aliyun.com/eci-eip-internet-charge-type: "PayByBandwidth"` - 按带宽计费
### Files to Reference
| File | Purpose |
| ---- | ------- |
| `src/k8s-client.ts` | K8S SDK 封装(已实现) |
| `src/deploy.ts` | 部署模块(已实现) |
### Technical Decisions
- **命名空间**: `d8d-proxy` (专用,避免与 d8d-dev/d8d-prd 冲突)
- **K8S 客户端**: 使用 @kubernetes/client-node SDK
- **认证方式**: K8S_API_URL + K8S_API_TOKEN 环境变量
- **EIP 配置**: 自动创建(通过 ECI annotation
- **容器规格**: `ecs.t5-lc2m1.nano` (0.5 CPU, 1G 内存)
- **Ingress 域名**: `{name}-{port}.{hostSuffix}`
- **镜像**: registry.cn-beijing.aliyuncs.com/d8dcloud/claude-api-proxy:${VERSION}
- **健康检查**: /health 端点
- **Ingress**: 无需 SSO 认证(代理服务)
## Implementation Plan
### Tasks
**Task 1: 安装 K8S 依赖**
- [ ] 执行 `pnpm add @kubernetes/client-node`
- [ ] 验证依赖安装成功
**Task 2: 创建命令行入口**
- [ ] File: `scripts/deploy.ts`
- [ ] Action: 创建命令行工具,支持以下命令:
- `deploy <name> <image>` - 部署代理服务
- `delete <name>` - 删除代理服务
- `status <name>` - 查询服务状态
- [ ] Notes: 使用 tsx 运行,参考参数:
- `--replicas` 副本数(默认 1
- `--port` 端口(默认 3000
- `--eip-bandwidth` EIP 带宽(默认 5
**Task 3: 添加 npm 脚本**
- [ ] File: `package.json`
- [ ] Action: 添加部署相关脚本:
```json
"deploy": "tsx scripts/deploy.ts"
```
### Acceptance Criteria
- [ ] **AC1: 依赖安装成功**
- Given: 执行 pnpm install
- When: 安装 @kubernetes/client-node
- Then: 依赖正确安装TypeScript 编译无错误
- [ ] **AC2: 部署命令可执行**
- Given: 环境变量 K8S_API_URL 和 K8S_API_TOKEN 已配置
- When: 执行 `pnpm deploy <name> <image>`
- Then: 成功创建 Deployment、Service、Ingress返回访问 URL
- [ ] **AC3: EIP 自动创建**
- Given: 部署时启用 EIP 选项(默认启用)
- When: Pod 启动
- Then: 自动创建并绑定 EIP
- [ ] **AC4: 删除功能正常**
- Given: 已部署的代理服务
- When: 执行 `pnpm deploy delete <name>`
- Then: 清理所有 K8S 资源Deployment、Service、Ingress
- [ ] **AC5: 状态查询正常**
- Given: 已部署的代理服务
- When: 执行 `pnpm deploy status <name>`
- Then: 返回服务运行状态和访问 URL
## Additional Context
### Dependencies
**新增依赖:**
- @kubernetes/client-node - K8S 官方 Node.js SDK
### Testing Strategy
**手动测试步骤:**
1. 配置 `.env` 文件中的 K8S 环境变量
2. 执行 `pnpm deploy claude-proxy-1 registry.cn-beijing.aliyuncs.com/d8dcloud/claude-api-proxy:v0.0.1`
3. 验证 Deployment、Service、Ingress 创建成功
4. 访问返回的 HTTPS URL 验证服务可用
5. 执行删除命令验证资源清理
### Notes
- 命名空间独立,避免与 d8d-dev/d8d-prd 冲突
- 代理服务无需 SSO 认证
- EIP 自动创建简化了 IP 管理
- 当前未启用抢占式实例,后续可优化成本

View File

@@ -6,7 +6,8 @@
"scripts": {
"build": "tsc && tsc-alias",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts"
"dev": "tsx watch src/index.ts",
"deploy": "tsx scripts/deploy.ts"
},
"keywords": [
"claude",
@@ -16,6 +17,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@kubernetes/client-node": "1.3.0",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"express": "^4.21.2"

426
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@kubernetes/client-node':
specifier: 1.3.0
version: 1.3.0
axios:
specifier: ^1.7.9
version: 1.13.6
@@ -192,6 +195,21 @@ packages:
cpu: [x64]
os: [win32]
'@jsep-plugin/assignment@1.3.0':
resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==}
engines: {node: '>= 10.16.0'}
peerDependencies:
jsep: ^0.4.0||^1.0.0
'@jsep-plugin/regex@1.0.4':
resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==}
engines: {node: '>= 10.16.0'}
peerDependencies:
jsep: ^0.4.0||^1.0.0
'@kubernetes/client-node@1.3.0':
resolution: {integrity: sha512-IE0yrIpOT97YS5fg2QpzmPzm8Wmcdf4ueWMn+FiJSI3jgTTQT1u+LUhoYpdfhdHAVxdrNsaBg2C0UXSnOgMoCQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -219,6 +237,12 @@ packages:
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
@@ -234,14 +258,24 @@ packages:
'@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@types/stream-buffers@3.0.8':
resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
@@ -255,6 +289,52 @@ packages:
axios@1.13.6:
resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==}
b4a@1.8.0:
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
peerDependencies:
react-native-b4a: '*'
peerDependenciesMeta:
react-native-b4a:
optional: true
bare-events@2.8.2:
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
bare-fs@4.5.6:
resolution: {integrity: sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==}
engines: {bare: '>=1.16.0'}
peerDependencies:
bare-buffer: '*'
peerDependenciesMeta:
bare-buffer:
optional: true
bare-os@3.8.0:
resolution: {integrity: sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==}
engines: {bare: '>=1.14.0'}
bare-path@3.0.0:
resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==}
bare-stream@2.10.0:
resolution: {integrity: sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==}
peerDependencies:
bare-buffer: '*'
bare-events: '*'
peerDependenciesMeta:
bare-buffer:
optional: true
bare-events:
optional: true
bare-url@2.4.0:
resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -314,6 +394,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
@@ -345,6 +434,9 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -373,10 +465,16 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
events-universal@1.0.1:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
express@4.22.1:
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
engines: {node: '>= 0.10.0'}
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
@@ -456,6 +554,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hpagent@1.2.0:
resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==}
engines: {node: '>=14'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -471,6 +573,10 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -491,6 +597,27 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
isomorphic-ws@5.0.0:
resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
peerDependencies:
ws: '*'
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsep@1.4.0:
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
engines: {node: '>= 10.16.0'}
jsonpath-plus@10.4.0:
resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==}
engines: {node: '>=18.0.0'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -541,10 +668,22 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
oauth4webapi@3.8.5:
resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -553,6 +692,12 @@ packages:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openid-client@6.8.2:
resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -579,6 +724,9 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
qs@6.14.2:
resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
engines: {node: '>=0.6'}
@@ -609,6 +757,9 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rfc4648@1.5.4:
resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -649,10 +800,41 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.7:
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
stream-buffers@3.0.3:
resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==}
engines: {node: '>= 0.10.0'}
streamx@2.25.0:
resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==}
tar-fs@3.1.2:
resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==}
tar-stream@3.1.8:
resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==}
teex@1.0.1:
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
text-decoder@1.2.7:
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -661,6 +843,9 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tsc-alias@1.8.16:
resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==}
engines: {node: '>=16.20.2'}
@@ -695,6 +880,27 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
'@esbuild/aix-ppc64@0.27.4':
@@ -775,6 +981,41 @@ snapshots:
'@esbuild/win32-x64@0.27.4':
optional: true
'@jsep-plugin/assignment@1.3.0(jsep@1.4.0)':
dependencies:
jsep: 1.4.0
'@jsep-plugin/regex@1.0.4(jsep@1.4.0)':
dependencies:
jsep: 1.4.0
'@kubernetes/client-node@1.3.0':
dependencies:
'@types/js-yaml': 4.0.9
'@types/node': 22.19.15
'@types/node-fetch': 2.6.13
'@types/stream-buffers': 3.0.8
form-data: 4.0.5
hpagent: 1.2.0
isomorphic-ws: 5.0.0(ws@8.19.0)
js-yaml: 4.1.1
jsonpath-plus: 10.4.0
node-fetch: 2.7.0
openid-client: 6.8.2
rfc4648: 1.5.4
socks-proxy-agent: 8.0.5
stream-buffers: 3.0.3
tar-fs: 3.1.2
ws: 8.19.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- bufferutil
- encoding
- react-native-b4a
- supports-color
- utf-8-validate
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -811,6 +1052,13 @@ snapshots:
'@types/http-errors@2.0.5': {}
'@types/js-yaml@4.0.9': {}
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 22.19.15
form-data: 4.0.5
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
@@ -828,16 +1076,24 @@ snapshots:
'@types/http-errors': 2.0.5
'@types/node': 22.19.15
'@types/stream-buffers@3.0.8':
dependencies:
'@types/node': 22.19.15
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
agent-base@7.1.4: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
argparse@2.0.1: {}
array-flatten@1.1.1: {}
array-union@2.1.0: {}
@@ -852,6 +1108,41 @@ snapshots:
transitivePeerDependencies:
- debug
b4a@1.8.0: {}
bare-events@2.8.2: {}
bare-fs@4.5.6:
dependencies:
bare-events: 2.8.2
bare-path: 3.0.0
bare-stream: 2.10.0(bare-events@2.8.2)
bare-url: 2.4.0
fast-fifo: 1.3.2
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
bare-os@3.8.0: {}
bare-path@3.0.0:
dependencies:
bare-os: 3.8.0
bare-stream@2.10.0(bare-events@2.8.2):
dependencies:
streamx: 2.25.0
teex: 1.0.1
optionalDependencies:
bare-events: 2.8.2
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
bare-url@2.4.0:
dependencies:
bare-path: 3.0.0
binary-extensions@2.3.0: {}
body-parser@1.20.4:
@@ -919,6 +1210,10 @@ snapshots:
dependencies:
ms: 2.0.0
debug@4.4.3:
dependencies:
ms: 2.1.3
delayed-stream@1.0.0: {}
depd@2.0.0: {}
@@ -941,6 +1236,10 @@ snapshots:
encodeurl@2.0.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -989,6 +1288,12 @@ snapshots:
etag@1.8.1: {}
events-universal@1.0.1:
dependencies:
bare-events: 2.8.2
transitivePeerDependencies:
- bare-abort-controller
express@4.22.1:
dependencies:
accepts: 1.3.8
@@ -1025,6 +1330,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
fast-fifo@1.3.2: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -1119,6 +1426,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
hpagent@1.2.0: {}
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -1135,6 +1444,8 @@ snapshots:
inherits@2.0.4: {}
ip-address@10.1.0: {}
ipaddr.js@1.9.1: {}
is-binary-path@2.1.0:
@@ -1149,6 +1460,24 @@ snapshots:
is-number@7.0.0: {}
isomorphic-ws@5.0.0(ws@8.19.0):
dependencies:
ws: 8.19.0
jose@6.2.2: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
jsep@1.4.0: {}
jsonpath-plus@10.4.0:
dependencies:
'@jsep-plugin/assignment': 1.3.0(jsep@1.4.0)
'@jsep-plugin/regex': 1.0.4(jsep@1.4.0)
jsep: 1.4.0
math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
@@ -1180,14 +1509,29 @@ snapshots:
negotiator@0.6.3: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
normalize-path@3.0.0: {}
oauth4webapi@3.8.5: {}
object-inspect@1.13.4: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
once@1.4.0:
dependencies:
wrappy: 1.0.2
openid-client@6.8.2:
dependencies:
jose: 6.2.2
oauth4webapi: 3.8.5
parseurl@1.3.3: {}
path-to-regexp@0.1.12: {}
@@ -1207,6 +1551,11 @@ snapshots:
proxy-from-env@1.1.0: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
qs@6.14.2:
dependencies:
side-channel: 1.1.0
@@ -1232,6 +1581,8 @@ snapshots:
reusify@1.1.0: {}
rfc4648@1.5.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -1299,14 +1650,78 @@ snapshots:
slash@3.0.0: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
socks: 2.8.7
transitivePeerDependencies:
- supports-color
socks@2.8.7:
dependencies:
ip-address: 10.1.0
smart-buffer: 4.2.0
statuses@2.0.2: {}
stream-buffers@3.0.3: {}
streamx@2.25.0:
dependencies:
events-universal: 1.0.1
fast-fifo: 1.3.2
text-decoder: 1.2.7
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
tar-fs@3.1.2:
dependencies:
pump: 3.0.4
tar-stream: 3.1.8
optionalDependencies:
bare-fs: 4.5.6
bare-path: 3.0.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- react-native-b4a
tar-stream@3.1.8:
dependencies:
b4a: 1.8.0
bare-fs: 4.5.6
fast-fifo: 1.3.2
streamx: 2.25.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- react-native-b4a
teex@1.0.1:
dependencies:
streamx: 2.25.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
text-decoder@1.2.7:
dependencies:
b4a: 1.8.0
transitivePeerDependencies:
- react-native-b4a
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
toidentifier@1.0.1: {}
tr46@0.0.3: {}
tsc-alias@1.8.16:
dependencies:
chokidar: 3.6.0
@@ -1338,3 +1753,14 @@ snapshots:
utils-merge@1.0.1: {}
vary@1.1.2: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
wrappy@1.0.2: {}
ws@8.19.0: {}

183
scripts/deploy.ts Normal file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env node
/**
* K8S 部署命令行工具
* 用于部署 Claude API 代理服务到 ECI
*
* 用法:
* pnpm deploy <name> <image> [options] - 部署代理服务
* pnpm deploy delete <name> - 删除代理服务
* pnpm deploy status <name> - 查询服务状态
*
* 选项:
* --replicas <n> 副本数 (默认: 1)
* --port <port> 容器端口 (默认: 3000)
* --eip-bandwidth <n> EIP 带宽 Mbps (默认: 5)
*/
import { config } from 'dotenv';
// 使用 override: true 确保覆盖系统环境变量
config({ override: true });
import { deployProxy, deleteProxy, getProxyStatus } from '../src/deploy';
// 解析命令行参数
function parseArgs() {
const args = process.argv.slice(2);
if (args.length === 0) {
printUsage();
process.exit(1);
}
const command = args[0];
// 解析选项
const getOption = (name: string, defaultValue: string): string => {
const idx = args.indexOf(name);
if (idx !== -1 && idx + 1 < args.length) {
return args[idx + 1];
}
return defaultValue;
};
const getOptionNumber = (name: string, defaultValue: number): number => {
return Number(getOption(name, String(defaultValue)));
};
// 判断是否是子命令模式
const isSubCommand = command === 'delete' || command === 'status';
return {
command,
name: isSubCommand ? args[1] : command,
image: isSubCommand ? undefined : args[1],
options: {
replicas: getOptionNumber('--replicas', 1),
port: getOptionNumber('--port', 3000),
eipBandwidth: getOptionNumber('--eip-bandwidth', 5),
},
};
}
function printUsage() {
console.log(`
K8S 部署命令行工具 - Claude API 代理服务
用法:
pnpm deploy <name> <image> [options] 部署代理服务
pnpm deploy delete <name> 删除代理服务
pnpm deploy status <name> 查询服务状态
选项:
--replicas <n> 副本数 (默认: 1)
--port <port> 容器端口 (默认: 3000)
--eip-bandwidth <n> EIP 带宽 Mbps (默认: 5)
示例:
# 部署单个代理实例
pnpm deploy claude-proxy-1 registry.cn-beijing.aliyuncs.com/d8dcloud/claude-api-proxy:v0.0.1
# 部署 3 个副本
pnpm deploy claude-proxy-1 registry.cn-beijing.aliyuncs.com/d8dcloud/claude-api-proxy:v0.0.1 --replicas 3
# 删除服务
pnpm deploy delete claude-proxy-1
# 查询状态
pnpm deploy status claude-proxy-1
`);
}
async function main() {
const { command, name, image, options } = parseArgs();
// 检查环境变量
if (!process.env.K8S_API_URL || !process.env.K8S_API_TOKEN) {
console.error('❌ 错误: 请配置 K8S_API_URL 和 K8S_API_TOKEN 环境变量');
console.error(' 可以在 .env 文件中配置或通过环境变量传入');
process.exit(1);
}
try {
switch (command) {
case 'delete': {
if (!name) {
console.error('❌ 错误: 请指定服务名称');
process.exit(1);
}
console.log(`🗑️ 正在删除服务 ${name}...`);
const result = await deleteProxy(name);
if (result.success) {
console.log(`${result.message}`);
} else {
console.error(`${result.message}`);
process.exit(1);
}
break;
}
case 'status': {
if (!name) {
console.error('❌ 错误: 请指定服务名称');
process.exit(1);
}
console.log(`📊 查询服务状态 ${name}...`);
const status = await getProxyStatus(name);
if (status.exists) {
console.log(`✅ 服务状态: ${status.status}`);
if (status.hosts && status.hosts.length > 0) {
console.log(`🌐 访问地址: ${status.hosts.join(', ')}`);
}
} else {
console.log(`⚠️ 服务 ${name} 不存在`);
}
break;
}
default: {
// deploy 命令
if (!name || !image) {
console.error('❌ 错误: 请指定服务名称和镜像');
process.exit(1);
}
console.log(`🚀 正在部署 ${name}...`);
console.log(` 镜像: ${image}`);
console.log(` 副本: ${options.replicas}`);
console.log(` 端口: ${options.port}`);
console.log(` EIP 带宽: ${options.eipBandwidth} Mbps`);
// 构建环境变量 - 从当前 .env 读取关键配置注入容器
const containerEnv: Record<string, string> = {
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
};
const result = await deployProxy({
name,
image,
replicas: options.replicas,
port: options.port,
eipBandwidth: options.eipBandwidth,
env: containerEnv,
});
if (result.success) {
console.log(`\n✅ ${result.message}`);
if (result.hosts && result.hosts.length > 0) {
console.log(`\n🌐 访问地址:`);
result.hosts.forEach(host => console.log(` ${host}`));
}
} else {
console.error(`\n❌ ${result.message}`);
process.exit(1);
}
break;
}
}
} catch (error) {
console.error('❌ 执行失败:', error instanceof Error ? error.message : error);
process.exit(1);
}
}
main();

378
src/deploy.ts Normal file
View File

@@ -0,0 +1,378 @@
/**
* K8S 部署模块 - 用于部署 Claude API 代理服务到 ECI
*/
import { config } from 'dotenv';
// 使用 override: true 确保覆盖系统环境变量
config({ override: true });
import { K8sClient, V1Deployment, V1Ingress, V1IngressRule, V1ServicePort } from './k8s-client';
// 环境变量
const K8S_API_URL = process.env.K8S_API_URL || '';
const K8S_API_TOKEN = process.env.K8S_API_TOKEN || '';
const DEV_HOST = process.env.DEV_HOST || 'dev.d8d.fun';
const TLS_SECRET_NAME = process.env.DEV_TLS_SECRET_NAME || 'd8d-proxy-tls';
// 专用命名空间
const NAMESPACE = 'd8d-proxy';
// 初始化 K8S 客户端
export const k8sClient = new K8sClient(K8S_API_URL, K8S_API_TOKEN);
export interface DeployProxyParams {
/** 容器组名称(唯一标识) */
name: string;
/** Docker 镜像 */
image: string;
/** 副本数 */
replicas?: number;
/** 容器端口 */
port?: number;
/** ECI 实例规格 */
instanceType?: string;
/** 是否自动创建 EIP */
autoEip?: boolean;
/** EIP 带宽 (Mbps) */
eipBandwidth?: number;
/** 环境变量 */
env?: Record<string, string>;
/** 标签 */
labels?: Record<string, string>;
/** 域名后缀 */
hostSuffix?: string;
}
export interface DeployProxyResult {
success: boolean;
message: string;
hosts?: string[];
deploymentName?: string;
serviceName?: string;
ingressName?: string;
}
/**
* 部署 Claude API 代理服务
*/
export async function deployProxy(params: DeployProxyParams): Promise<DeployProxyResult> {
const {
name,
image,
replicas = 1,
port = 3000,
instanceType = 'ecs.t5-lc2m1.nano',
autoEip = true,
eipBandwidth = 5,
env = {},
labels = {},
hostSuffix = DEV_HOST,
} = params;
const deploymentName = name;
const serviceName = `service-${name}`;
const ingressName = `${name}-ingress`;
// 清理域名后缀
const cleanHostSuffix = hostSuffix.replace(/^\*\./, '').replace(/^\.+/, '');
// 构建域名
const host = `${name}-${port}.${cleanHostSuffix}`;
try {
// 1. 创建 Deployment
const deployment: V1Deployment = {
metadata: {
name: deploymentName,
labels: {
app: name,
'proxy-container': 'true',
...labels,
},
annotations: {
// ECI 实例规格
'k8s.aliyun.com/eci-use-specs': instanceType,
// 抢占式实例
'k8s.aliyun.com/eci-spot-instance': 'true',
// 自动创建 EIP
...(autoEip && {
'k8s.aliyun.com/eci-open-enable-eip': 'true',
'k8s.aliyun.com/eci-eip-bandwidth': String(eipBandwidth),
'k8s.aliyun.com/eci-eip-internet-charge-type': 'PayByBandwidth',
}),
},
},
spec: {
replicas,
selector: {
matchLabels: {
app: name,
},
},
template: {
metadata: {
labels: {
app: name,
'proxy-container': 'true',
...labels,
},
annotations: {
// ECI 实例规格
'k8s.aliyun.com/eci-use-specs': instanceType,
// 抢占式实例
'k8s.aliyun.com/eci-spot-instance': 'true',
// 自动创建 EIP
...(autoEip && {
'k8s.aliyun.com/eci-open-enable-eip': 'true',
'k8s.aliyun.com/eci-eip-bandwidth': String(eipBandwidth),
'k8s.aliyun.com/eci-eip-internet-charge-type': 'PayByBandwidth',
}),
},
},
spec: {
automountServiceAccountToken: false,
enableServiceLinks: false,
nodeSelector: {
type: 'virtual-kubelet',
},
containers: [
{
name: name,
image: image,
ports: [
{
containerPort: port,
protocol: 'TCP',
},
],
env: Object.entries(env).map(([key, value]) => ({
name: key,
value,
})),
resources: {
requests: {
cpu: '250m',
memory: '256Mi',
},
},
readinessProbe: {
httpGet: {
path: '/health',
port: port,
},
initialDelaySeconds: 5,
periodSeconds: 10,
},
livenessProbe: {
httpGet: {
path: '/health',
port: port,
},
initialDelaySeconds: 15,
periodSeconds: 20,
},
},
],
tolerations: [
{
key: 'alibabacloud.com/eci',
operator: 'Exists',
effect: 'NoSchedule',
},
],
},
},
},
};
await k8sClient.createDeployment({
json: {
namespace: NAMESPACE,
deployment,
},
});
console.log(`✅ Deployment ${deploymentName} 创建成功`);
// 2. 创建 Service
const ports: V1ServicePort[] = [
{
port: port,
targetPort: port,
name: 'http',
},
];
await k8sClient.createService({
json: {
namespace: NAMESPACE,
service: {
metadata: {
name: serviceName,
labels: {
app: name,
},
},
spec: {
type: 'ClusterIP',
selector: {
app: name,
},
ports,
},
},
},
});
console.log(`✅ Service ${serviceName} 创建成功`);
// 3. 创建 Ingress
const ingressRule: V1IngressRule = {
host,
http: {
paths: [
{
path: '/',
pathType: 'Prefix',
backend: {
service: {
name: serviceName,
port: {
number: port,
},
},
},
},
],
},
};
const ingress: V1Ingress = {
metadata: {
name: ingressName,
annotations: {
'nginx.ingress.kubernetes.io/proxy-body-size': '50m',
'nginx.ingress.kubernetes.io/proxy-read-timeout': '300',
'nginx.ingress.kubernetes.io/proxy-send-timeout': '300',
},
},
spec: {
tls: [
{
hosts: [host],
secretName: TLS_SECRET_NAME,
},
],
rules: [ingressRule],
},
};
await k8sClient.createIngress({
json: {
namespace: NAMESPACE,
ingress,
},
});
console.log(`✅ Ingress ${ingressName} 创建成功`);
return {
success: true,
message: `代理服务 ${name} 部署成功`,
hosts: [`https://${host}`],
deploymentName,
serviceName,
ingressName,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`❌ 部署失败: ${errorMessage}`);
return {
success: false,
message: `部署失败: ${errorMessage}`,
};
}
}
/**
* 删除代理服务
*/
export async function deleteProxy(name: string): Promise<{ success: boolean; message: string }> {
const ingressName = `${name}-ingress`;
const serviceName = `service-${name}`;
try {
// 1. 删除 Ingress
await k8sClient.deleteIngress({
param: {
namespace: NAMESPACE,
ingressName,
},
}).catch(err => console.warn(`删除 Ingress 失败: ${err.message}`));
// 2. 删除 Service
await k8sClient.deleteService({
param: {
namespace: NAMESPACE,
serviceName,
},
}).catch(err => console.warn(`删除 Service 失败: ${err.message}`));
// 3. 删除 Deployment
await k8sClient.deleteDeployment({
param: {
namespace: NAMESPACE,
name,
},
}).catch(err => console.warn(`删除 Deployment 失败: ${err.message}`));
console.log(`✅ 代理服务 ${name} 删除成功`);
return { success: true, message: `代理服务 ${name} 删除成功` };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { success: false, message: `删除失败: ${errorMessage}` };
}
}
/**
* 获取代理服务状态
*/
export async function getProxyStatus(name: string): Promise<{
exists: boolean;
status?: string;
hosts?: string[];
}> {
try {
const deployment = await k8sClient.getDeployment({
param: {
namespace: NAMESPACE,
name,
},
});
const readyReplicas = deployment.status?.readyReplicas || 0;
const replicas = deployment.spec?.replicas || 1;
// 获取 Ingress 域名
const ingressResult = await k8sClient.getIngress({
param: {
namespace: NAMESPACE,
ingressName: `${name}-ingress`,
},
});
const hosts = ingressResult.ingress?.spec?.rules?.map(r => `https://${r.host}`) || [];
return {
exists: true,
status: readyReplicas >= replicas ? 'Running' : 'Pending',
hosts,
};
} catch (error) {
const errorCode = (error as { code?: number })?.code;
if (errorCode === 404) {
return { exists: false };
}
throw error;
}
}

View File

@@ -4,7 +4,7 @@ import { proxyHandler } from './proxy.js';
import { healthHandler } from './health.js';
const app = express();
const PORT = parseInt(process.env.PORT || '3000', 10);
const PORT = parseInt(process.env.PORT || '80', 10);
// 解析 JSON 请求体,设置较大的限制以支持大请求
app.use(express.json({ limit: '50mb' }));

317
src/k8s-client.ts Normal file
View File

@@ -0,0 +1,317 @@
/**
* K8S 客户端 - 通过 HTTP API 代理调用 K8S
* @kubernetes/client-node 仅用于类型定义
*/
import axios from 'axios';
import * as k8s from '@kubernetes/client-node';
// 导出类型
export type V1Deployment = k8s.V1Deployment;
export type V1DeploymentList = k8s.V1DeploymentList;
export type V1Ingress = k8s.V1Ingress;
export type V1IngressRule = k8s.V1IngressRule;
export type V1Pod = k8s.V1Pod;
export type V1Service = k8s.V1Service;
export type V1ServicePort = k8s.V1ServicePort;
// 构建 URL 路径
const buildUrl = (baseUrl: string, path: string, params?: Record<string, string>) => {
let url = baseUrl.replace(/\/+$/, '') + '/api/v1/kubernetes/' + path.replace(/^\/+/, '');
if (params) {
for (const [key, value] of Object.entries(params)) {
url = url.replace(`:${key}`, value);
}
}
return url;
};
// 构建查询字符串
const buildQuery = (query?: Record<string, string | number | boolean | undefined>) => {
if (!query) return '';
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined) {
params.set(key, String(value));
}
}
return params.toString() ? '?' + params.toString() : '';
};
// HTTP 请求封装
const request = async (
baseUrl: string,
token: string,
method: string,
path: string,
args?: {
param?: Record<string, string>;
query?: Record<string, string | number | boolean | undefined>;
json?: unknown;
}
) => {
let url = buildUrl(baseUrl, path, args?.param);
url += buildQuery(args?.query);
const response = await axios.request({
url,
method,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: args?.json ? JSON.stringify(args.json) : undefined,
validateStatus: () => true, // 不抛出 HTTP 错误
});
return response;
};
/**
* K8S 客户端类
*/
export class K8sClient {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl;
this.token = token;
}
// ========== Pod 操作 ==========
async getPods(args: {
param: { namespace?: string };
query?: { labelSelector?: string; fieldSelector?: string; limit?: number; continue?: string };
}): Promise<{ pods: V1Pod[] }> {
try {
const namespace = args.param.namespace || 'default';
const res = await request(this.baseUrl, this.token, 'GET', 'pods/:namespace', {
param: { namespace },
query: args.query,
});
if (res.status !== 200) {
throw new Error(res.data?.message || '获取 Pods 失败');
}
return { pods: res.data?.pods || [] };
} catch (error) {
console.error('获取 Pods 失败:', error);
return { pods: [] };
}
}
async getPod(args: { param: { namespace: string; podName: string } }): Promise<V1Pod> {
const res = await request(this.baseUrl, this.token, 'GET', 'pods/:namespace/:podName', {
param: { namespace: args.param.namespace, podName: args.param.podName },
});
if (res.status !== 200) {
throw new Error(res.data?.message || '获取 Pod 失败');
}
return res.data;
}
async createPod(args: { json: { namespace: string; pod: V1Pod } }): Promise<{ success: boolean; message: string; pod?: V1Pod }> {
try {
const res = await request(this.baseUrl, this.token, 'POST', 'pods', { json: args.json });
if (res.status !== 200) {
return { success: false, message: res.data?.message || '创建 Pod 失败' };
}
return { success: true, message: 'Pod 创建成功', pod: res.data };
} catch (error) {
return { success: false, message: `创建 Pod 失败: ${error}` };
}
}
async deletePod(args: { param: { namespace: string; podName: string } }): Promise<{ success: boolean; message: string }> {
try {
const res = await request(this.baseUrl, this.token, 'DELETE', 'pods/:namespace/:podName', {
param: { namespace: args.param.namespace, podName: args.param.podName },
});
if (res.status !== 200) {
return { success: false, message: res.data?.message || '删除 Pod 失败' };
}
return { success: true, message: 'Pod 删除成功' };
} catch (error) {
return { success: false, message: `删除 Pod 失败: ${error}` };
}
}
// ========== Deployment 操作 ==========
async getDeployment(args: { param: { namespace: string; name: string } }): Promise<V1Deployment> {
const res = await request(this.baseUrl, this.token, 'GET', 'deployments/:namespace/:name', {
param: { namespace: args.param.namespace, name: args.param.name },
});
if (res.status !== 200) {
const error = new Error(res.data?.message || `Deployment ${args.param.name} not found`);
(error as any).code = res.status;
throw error;
}
return res.data;
}
async getDeployments(args: {
param: { namespace: string };
query?: { limit?: number; continue?: string };
}): Promise<V1DeploymentList> {
try {
const res = await request(this.baseUrl, this.token, 'GET', 'deployments/:namespace', {
param: { namespace: args.param.namespace },
query: args.query,
});
if (res.status !== 200) {
return { items: [], metadata: {} };
}
return { items: res.data?.items || [], metadata: res.data?.metadata || {} };
} catch (error) {
console.error('获取 Deployments 失败:', error);
return { items: [], metadata: {} };
}
}
async createDeployment(args: {
json: { namespace: string; deployment: V1Deployment };
}): Promise<{ success: boolean; message: string; deployment?: V1Deployment }> {
try {
const res = await request(this.baseUrl, this.token, 'POST', 'deployments', { json: args.json });
if (res.status !== 200) {
return { success: false, message: res.data?.message || '创建 Deployment 失败' };
}
return { success: true, message: 'Deployment 创建成功', deployment: res.data };
} catch (error) {
return { success: false, message: `创建 Deployment 失败: ${error}` };
}
}
async deleteDeployment(args: {
param: { namespace: string; name: string };
}): Promise<{ success: boolean; message: string }> {
try {
const res = await request(this.baseUrl, this.token, 'DELETE', 'deployments/:namespace/:name', {
param: { namespace: args.param.namespace, name: args.param.name },
});
if (res.status !== 200) {
return { success: false, message: res.data?.message || '删除 Deployment 失败' };
}
return { success: true, message: 'Deployment 删除成功' };
} catch (error) {
return { success: false, message: `删除 Deployment 失败: ${error}` };
}
}
async restartDeployment(args: {
param: { namespace: string; name: string };
}): Promise<{ success: boolean; message: string }> {
try {
const pods = await this.getPods({
param: { namespace: args.param.namespace },
query: { labelSelector: `app=${args.param.name}` },
});
for (const pod of pods.pods) {
if (pod.metadata?.name) {
await this.deletePod({ param: { namespace: args.param.namespace, podName: pod.metadata.name } });
}
}
return { success: true, message: 'Deployment 重启成功' };
} catch (error) {
return { success: false, message: `重启 Deployment 失败: ${error}` };
}
}
// ========== Service 操作 ==========
async createService(args: {
json: { namespace: string; service: V1Service };
}): Promise<{ success: boolean; message: string; service?: V1Service }> {
try {
const res = await request(this.baseUrl, this.token, 'POST', 'services', { json: args.json });
if (res.status !== 200) {
return { success: false, message: res.data?.message || '创建 Service 失败' };
}
return { success: true, message: 'Service 创建成功', service: res.data };
} catch (error) {
return { success: false, message: `创建 Service 失败: ${error}` };
}
}
async deleteService(args: {
param: { namespace: string; serviceName: string };
}): Promise<{ success: boolean; message: string }> {
try {
const res = await request(this.baseUrl, this.token, 'DELETE', 'services/:namespace/:serviceName', {
param: { namespace: args.param.namespace, serviceName: args.param.serviceName },
});
if (res.status !== 200) {
return { success: false, message: res.data?.message || '删除 Service 失败' };
}
return { success: true, message: 'Service 删除成功' };
} catch (error) {
return { success: false, message: `删除 Service 失败: ${error}` };
}
}
async getService(args: {
param: { namespace: string; serviceName: string };
}): Promise<{ service?: V1Service }> {
try {
const res = await request(this.baseUrl, this.token, 'GET', 'services/:namespace/:serviceName', {
param: { namespace: args.param.namespace, serviceName: args.param.serviceName },
});
if (res.status !== 200) {
return {};
}
return { service: res.data };
} catch {
return {};
}
}
// ========== Ingress 操作 ==========
async createIngress(args: {
json: { namespace: string; ingress: V1Ingress };
}): Promise<{ success: boolean; message: string; ingress?: V1Ingress }> {
try {
const res = await request(this.baseUrl, this.token, 'POST', 'ingresses', { json: args.json });
if (res.status !== 200) {
return { success: false, message: res.data?.message || '创建 Ingress 失败' };
}
return { success: true, message: 'Ingress 创建成功', ingress: res.data };
} catch (error) {
return { success: false, message: `创建 Ingress 失败: ${error}` };
}
}
async deleteIngress(args: {
param: { namespace: string; ingressName: string };
}): Promise<{ success: boolean; message: string }> {
try {
const res = await request(this.baseUrl, this.token, 'DELETE', 'ingresses/:namespace/:ingressName', {
param: { namespace: args.param.namespace, ingressName: args.param.ingressName },
});
if (res.status !== 200) {
return { success: false, message: res.data?.message || '删除 Ingress 失败' };
}
return { success: true, message: 'Ingress 删除成功' };
} catch (error) {
return { success: false, message: `删除 Ingress 失败: ${error}` };
}
}
async getIngress(args: {
param: { namespace: string; ingressName: string };
}): Promise<{ ingress?: V1Ingress }> {
try {
const res = await request(this.baseUrl, this.token, 'GET', 'ingresses/:namespace/:ingressName', {
param: { namespace: args.param.namespace, ingressName: args.param.ingressName },
});
if (res.status !== 200) {
return {};
}
return { ingress: res.data };
} catch {
return {};
}
}
}